diff --git a/python/plugins/processing/algs/qgis/StatisticsByCategories.py b/python/plugins/processing/algs/qgis/StatisticsByCategories.py
old mode 100644
new mode 100755
index dfe0097f06f..9e04aa20c9c
--- a/python/plugins/processing/algs/qgis/StatisticsByCategories.py
+++ b/python/plugins/processing/algs/qgis/StatisticsByCategories.py
@@ -28,6 +28,8 @@ __revision__ = '$Format:%H$'
from qgis.core import (QgsProcessingParameterFeatureSource,
QgsStatisticalSummary,
+ QgsDateTimeStatisticalSummary,
+ QgsStringStatisticalSummary,
QgsFeatureRequest,
QgsProcessingParameterField,
QgsProcessingParameterFeatureSink,
@@ -36,13 +38,16 @@ from qgis.core import (QgsProcessingParameterFeatureSource,
QgsWkbTypes,
QgsCoordinateReferenceSystem,
QgsFeature,
- QgsFeatureSink)
+ QgsFeatureSink,
+ QgsProcessing,
+ NULL)
from qgis.PyQt.QtCore import QVariant
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
+from collections import defaultdict
+
class StatisticsByCategories(QgisAlgorithm):
-
INPUT = 'INPUT'
VALUES_FIELD_NAME = 'VALUES_FIELD_NAME'
CATEGORIES_FIELD_NAME = 'CATEGORIES_FIELD_NAME'
@@ -56,13 +61,16 @@ class StatisticsByCategories(QgisAlgorithm):
def initAlgorithm(self, config=None):
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
- self.tr('Input vector layer')))
+ self.tr('Input vector layer'),
+ types=[QgsProcessing.TypeVector]))
self.addParameter(QgsProcessingParameterField(self.VALUES_FIELD_NAME,
- self.tr('Field to calculate statistics on'),
- parentLayerParameterName=self.INPUT, type=QgsProcessingParameterField.Numeric))
+ self.tr(
+ 'Field to calculate statistics on (if empty, only count is calculated)'),
+ parentLayerParameterName=self.INPUT, optional=True))
self.addParameter(QgsProcessingParameterField(self.CATEGORIES_FIELD_NAME,
- self.tr('Field with categories'),
- parentLayerParameterName=self.INPUT, type=QgsProcessingParameterField.Any))
+ self.tr('Field(s) with categories'),
+ parentLayerParameterName=self.INPUT,
+ type=QgsProcessingParameterField.Any, allowMultiple=True))
self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Statistics by category')))
@@ -75,49 +83,213 @@ class StatisticsByCategories(QgisAlgorithm):
def processAlgorithm(self, parameters, context, feedback):
source = self.parameterAsSource(parameters, self.INPUT, context)
value_field_name = self.parameterAsString(parameters, self.VALUES_FIELD_NAME, context)
- category_field_name = self.parameterAsString(parameters, self.CATEGORIES_FIELD_NAME, context)
+ category_field_names = self.parameterAsFields(parameters, self.CATEGORIES_FIELD_NAME, context)
value_field_index = source.fields().lookupField(value_field_name)
- category_field_index = source.fields().lookupField(category_field_name)
+ if value_field_index >= 0:
+ value_field = source.fields().at(value_field_index)
+ else:
+ value_field = None
+ category_field_indexes = [source.fields().lookupField(n) for n in category_field_names]
- features = source.getFeatures(QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry))
- total = 100.0 / source.featureCount() if source.featureCount() else 0
- values = {}
+ # generate output fields
+ fields = QgsFields()
+ for c in category_field_indexes:
+ fields.append(source.fields().at(c))
+
+ def addField(name):
+ """
+ Adds a field to the output, keeping the same data type as the value_field
+ """
+ field = value_field
+ field.setName(name)
+ fields.append(field)
+
+ if value_field is None:
+ field_type = 'none'
+ fields.append(QgsField('count', QVariant.Int))
+ elif value_field.isNumeric():
+ field_type = 'numeric'
+ fields.append(QgsField('count', QVariant.Int))
+ fields.append(QgsField('unique', QVariant.Int))
+ fields.append(QgsField('min', QVariant.Double))
+ fields.append(QgsField('max', QVariant.Double))
+ fields.append(QgsField('range', QVariant.Double))
+ fields.append(QgsField('sum', QVariant.Double))
+ fields.append(QgsField('mean', QVariant.Double))
+ fields.append(QgsField('median', QVariant.Double))
+ fields.append(QgsField('stddev', QVariant.Double))
+ fields.append(QgsField('minority', QVariant.Double))
+ fields.append(QgsField('majority', QVariant.Double))
+ fields.append(QgsField('q1', QVariant.Double))
+ fields.append(QgsField('q3', QVariant.Double))
+ fields.append(QgsField('iqr', QVariant.Double))
+ elif value_field.type() in (QVariant.Date, QVariant.Time, QVariant.DateTime):
+ field_type = 'datetime'
+ fields.append(QgsField('count', QVariant.Int))
+ fields.append(QgsField('unique', QVariant.Int))
+ fields.append(QgsField('empty', QVariant.Int))
+ fields.append(QgsField('filled', QVariant.Int))
+ # keep same data type for these fields
+ addField('min')
+ addField('max')
+ else:
+ field_type = 'string'
+ fields.append(QgsField('count', QVariant.Int))
+ fields.append(QgsField('unique', QVariant.Int))
+ fields.append(QgsField('empty', QVariant.Int))
+ fields.append(QgsField('filled', QVariant.Int))
+ # keep same data type for these fields
+ addField('min')
+ addField('max')
+ fields.append(QgsField('min_length', QVariant.Int))
+ fields.append(QgsField('max_length', QVariant.Int))
+ fields.append(QgsField('mean_length', QVariant.Double))
+
+ request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry)
+ if value_field is not None:
+ attrs = [value_field_index]
+ else:
+ attrs = []
+ attrs.extend(category_field_indexes)
+ request.setSubsetOfAttributes(attrs)
+ features = source.getFeatures(request)
+ total = 50.0 / source.featureCount() if source.featureCount() else 0
+ if field_type == 'none':
+ values = defaultdict(lambda: 0)
+ else:
+ values = defaultdict(list)
for current, feat in enumerate(features):
if feedback.isCanceled():
break
feedback.setProgress(int(current * total))
attrs = feat.attributes()
- try:
- value = float(attrs[value_field_index])
- cat = attrs[category_field_index]
- if cat not in values:
- values[cat] = []
- values[cat].append(value)
- except:
- pass
-
- fields = QgsFields()
- fields.append(source.fields().at(category_field_index))
- fields.append(QgsField('min', QVariant.Double))
- fields.append(QgsField('max', QVariant.Double))
- fields.append(QgsField('mean', QVariant.Double))
- fields.append(QgsField('stddev', QVariant.Double))
- fields.append(QgsField('sum', QVariant.Double))
- fields.append(QgsField('count', QVariant.Int))
+ cat = tuple([attrs[c] for c in category_field_indexes])
+ if field_type == 'none':
+ values[cat] += 1
+ continue
+ if field_type == 'numeric':
+ if attrs[value_field_index] == NULL:
+ continue
+ else:
+ value = float(attrs[value_field_index])
+ elif field_type == 'string':
+ if attrs[value_field_index] == NULL:
+ value = ''
+ else:
+ value = str(attrs[value_field_index])
+ elif attrs[value_field_index] == NULL:
+ value = NULL
+ else:
+ value = attrs[value_field_index]
+ values[cat].append(value)
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
fields, QgsWkbTypes.NoGeometry, QgsCoordinateReferenceSystem())
- stat = QgsStatisticalSummary(QgsStatisticalSummary.Min | QgsStatisticalSummary.Max |
- QgsStatisticalSummary.Mean | QgsStatisticalSummary.StDevSample |
- QgsStatisticalSummary.Sum | QgsStatisticalSummary.Count)
-
- for (cat, v) in list(values.items()):
- stat.calculate(v)
- f = QgsFeature()
- f.setAttributes([cat, stat.min(), stat.max(), stat.mean(), stat.sampleStDev(), stat.sum(), stat.count()])
- sink.addFeature(f, QgsFeatureSink.FastInsert)
+ if field_type == 'none':
+ self.saveCounts(values, sink, feedback)
+ elif field_type == 'numeric':
+ self.calcNumericStats(values, sink, feedback)
+ elif field_type == 'datetime':
+ self.calcDateTimeStats(values, sink, feedback)
+ else:
+ self.calcStringStats(values, sink, feedback)
return {self.OUTPUT: dest_id}
+
+ def saveCounts(self, values, sink, feedback):
+ total = 50.0 / len(values) if values else 0
+ current = 0
+ for cat, v in values.items():
+ if feedback.isCanceled():
+ break
+
+ feedback.setProgress(int(current * total) + 50)
+ f = QgsFeature()
+ f.setAttributes(list(cat) + [v])
+ sink.addFeature(f, QgsFeatureSink.FastInsert)
+ current += 1
+
+ def calcNumericStats(self, values, sink, feedback):
+ stat = QgsStatisticalSummary()
+
+ total = 50.0 / len(values) if values else 0
+ current = 0
+ for cat, v in values.items():
+ if feedback.isCanceled():
+ break
+
+ feedback.setProgress(int(current * total) + 50)
+
+ stat.calculate(v)
+ f = QgsFeature()
+ f.setAttributes(list(cat) + [stat.count(),
+ stat.variety(),
+ stat.min(),
+ stat.max(),
+ stat.range(),
+ stat.sum(),
+ stat.mean(),
+ stat.median(),
+ stat.stDev(),
+ stat.minority(),
+ stat.majority(),
+ stat.firstQuartile(),
+ stat.thirdQuartile(),
+ stat.interQuartileRange()])
+
+ sink.addFeature(f, QgsFeatureSink.FastInsert)
+ current += 1
+
+ def calcDateTimeStats(self, values, sink, feedback):
+ stat = QgsDateTimeStatisticalSummary()
+
+ total = 50.0 / len(values) if values else 0
+ current = 0
+ for cat, v in values.items():
+ if feedback.isCanceled():
+ break
+
+ feedback.setProgress(int(current * total) + 50)
+
+ stat.calculate(v)
+ f = QgsFeature()
+ f.setAttributes(list(cat) + [stat.count(),
+ stat.countDistinct(),
+ stat.countMissing(),
+ stat.count() - stat.countMissing(),
+ stat.statistic(QgsDateTimeStatisticalSummary.Min),
+ stat.statistic(QgsDateTimeStatisticalSummary.Max)
+ ])
+
+ sink.addFeature(f, QgsFeatureSink.FastInsert)
+ current += 1
+
+ def calcStringStats(self, values, sink, feedback):
+ stat = QgsStringStatisticalSummary()
+
+ total = 50.0 / len(values) if values else 0
+ current = 0
+ for cat, v in values.items():
+ if feedback.isCanceled():
+ break
+
+ feedback.setProgress(int(current * total) + 50)
+
+ stat.calculate(v)
+ f = QgsFeature()
+ f.setAttributes(list(cat) + [stat.count(),
+ stat.countDistinct(),
+ stat.countMissing(),
+ stat.count() - stat.countMissing(),
+ stat.min(),
+ stat.max(),
+ stat.minLength(),
+ stat.maxLength(),
+ stat.meanLength()
+ ])
+
+ sink.addFeature(f, QgsFeatureSink.FastInsert)
+ current += 1
diff --git a/python/plugins/processing/algs/qgis/scripts/Frequency_analysis.py b/python/plugins/processing/algs/qgis/scripts/Frequency_analysis.py
deleted file mode 100644
index e6c1885ee33..00000000000
--- a/python/plugins/processing/algs/qgis/scripts/Frequency_analysis.py
+++ /dev/null
@@ -1,47 +0,0 @@
-##Vector analysis=group
-
-#inputs
-
-##Input=source
-##Fields=field multiple Input
-##Frequency=sink table
-
-
-from processing.tools.vector import TableWriter
-from collections import defaultdict
-from qgis.core import QgsProcessingUtils, QgsFields, QgsField, QgsWkbTypes, QgsFeature
-from qgis.PyQt.QtCore import QVariant
-from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
-
-inputFields = Input.fields()
-fieldIdxs = []
-out_fields = QgsFields()
-for f in Fields:
- idx = inputFields.indexFromName(f)
- if idx == -1:
- raise GeoAlgorithmExecutionException('Field not found:' + f)
- fieldIdxs.append(idx)
- out_fields.append(inputFields.at(idx))
-
-out_fields.append(QgsField('FREQ', QVariant.Int))
-
-(sink, Frequency) = self.parameterAsSink(parameters, 'Frequency', context,
- out_fields)
-
-counts = {}
-feats = Input.getFeatures()
-nFeats = Input.featureCount()
-counts = defaultdict(int)
-for i, feat in enumerate(feats):
- feedback.setProgress(int(100 * i / nFeats))
- if feedback.isCanceled():
- break
-
- attrs = feat.attributes()
- clazz = tuple([attrs[i] for i in fieldIdxs])
- counts[clazz] += 1
-
-for c in counts:
- f = QgsFeature()
- f.setAttributes(list(c) + [counts[c]])
- sink.addFeature(f)
diff --git a/python/plugins/processing/algs/qgis/scripts/Number_of_unique_values_in_classes.py b/python/plugins/processing/algs/qgis/scripts/Number_of_unique_values_in_classes.py
deleted file mode 100644
index 5335c66e3f9..00000000000
--- a/python/plugins/processing/algs/qgis/scripts/Number_of_unique_values_in_classes.py
+++ /dev/null
@@ -1,52 +0,0 @@
-##Vector analysis=group
-
-# inputs
-
-
-##input=source
-##class_field=field input
-##value_field=field input
-##N_unique_values=sink
-
-
-from qgis.PyQt.QtCore import QVariant
-from qgis.core import QgsFeature, QgsField, QgsProcessingUtils
-
-fields = input.fields()
-fields.append(QgsField('UNIQ_COUNT', QVariant.Int))
-
-(sink, N_unique_values) = self.parameterAsSink(parameters, 'N_unique_values', context,
- fields, input.wkbType(), input.sourceCrs())
-
-
-class_field_index = input.fields().lookupField(class_field)
-value_field_index = input.fields().lookupField(value_field)
-
-outFeat = QgsFeature()
-classes = {}
-feats = input.getFeatures()
-nFeat = input.featureCount()
-for n, inFeat in enumerate(feats):
- if feedback.isCanceled():
- break
- feedback.setProgress(int(100 * n / nFeat))
- attrs = inFeat.attributes()
- clazz = attrs[class_field_index]
- value = attrs[value_field_index]
- if clazz not in classes:
- classes[clazz] = []
- if value not in classes[clazz]:
- classes[clazz].append(value)
-
-feats = input.getFeatures()
-for n, inFeat in enumerate(feats):
- if feedback.isCanceled():
- break
- feedback.setProgress(int(100 * n / nFeat))
- inGeom = inFeat.geometry()
- outFeat.setGeometry(inGeom)
- attrs = inFeat.attributes()
- clazz = attrs[class_field_index]
- attrs.append(len(classes[clazz]))
- outFeat.setAttributes(attrs)
- sink.addFeature(outFeat)
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gfs
new file mode 100644
index 00000000000..1e740b97e92
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gfs
@@ -0,0 +1,48 @@
+
+
+ stats_by_cat_date
+ stats_by_cat_date
+ 100
+
+ 4
+
+
+ date
+ date
+ String
+ 10
+
+
+ count
+ count
+ Integer
+
+
+ unique
+ unique
+ Integer
+
+
+ empty
+ empty
+ Integer
+
+
+ filled
+ filled
+ Integer
+
+
+ min
+ min
+ String
+ 10
+
+
+ max
+ max
+ String
+ 10
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gml
new file mode 100644
index 00000000000..27c0f84cb3b
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_date.gml
@@ -0,0 +1,50 @@
+
+
+ missing
+
+
+
+ 2016/11/30
+ 1
+ 1
+ 0
+ 1
+ 2016/11/30
+ 2016/11/30
+
+
+
+
+ 2016/11/10
+ 1
+ 1
+ 0
+ 1
+ 2016/11/10
+ 2016/11/10
+
+
+
+
+ 1
+ 0
+ 1
+ 0
+
+
+
+
+ 2014/11/30
+ 1
+ 1
+ 0
+ 1
+ 2014/11/30
+ 2014/11/30
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gfs
new file mode 100644
index 00000000000..1069d98aeb8
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gfs
@@ -0,0 +1,86 @@
+
+
+ stats_by_cat_float
+ stats_by_cat_float
+ 100
+
+ 5
+
+
+ name
+ name
+ String
+ 2
+
+
+ count
+ count
+ Integer
+
+
+ unique
+ unique
+ Integer
+
+
+ min
+ min
+ Real
+
+
+ max
+ max
+ Real
+
+
+ range
+ range
+ Real
+
+
+ sum
+ sum
+ Real
+
+
+ mean
+ mean
+ Real
+
+
+ median
+ median
+ Real
+
+
+ stddev
+ stddev
+ Real
+
+
+ minority
+ minority
+ Real
+
+
+ majority
+ majority
+ Real
+
+
+ q1
+ q1
+ Real
+
+
+ q3
+ q3
+ Real
+
+
+ iqr
+ iqr
+ Real
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gml
new file mode 100644
index 00000000000..1fa21ebeadc
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_float.gml
@@ -0,0 +1,103 @@
+
+
+ missing
+
+
+
+ aa
+ 2
+ 2
+ 3.33
+ 44.123456
+ 40.793456
+ 47.453456
+ 23.726728
+ 23.726728
+ 20.396728
+ 3.33
+ 3.33
+ 3.33
+ 44.123456
+ 40.793456
+
+
+
+
+ dd
+ 1
+ 1
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+
+
+
+
+ bb
+ 4
+ 1
+ 0.123
+ 0.123
+ 0
+ 0.492
+ 0.123
+ 0.123
+ 0
+ 0.123
+ 0.123
+ 0.123
+ 0.123
+ 0
+
+
+
+
+ 1
+ 1
+ -100291.43213
+ -100291.43213
+ 0
+ -100291.43213
+ -100291.43213
+ -100291.43213
+ 0
+ -100291.43213
+ -100291.43213
+ -100291.43213
+ -100291.43213
+ 0
+
+
+
+
+ cc
+ 1
+ 1
+ 0.123
+ 0.123
+ 0
+ 0.123
+ 0.123
+ 0.123
+ 0
+ 0.123
+ 0.123
+ 0.123
+ 0.123
+ 0
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gfs
new file mode 100644
index 00000000000..b8719d76b93
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gfs
@@ -0,0 +1,26 @@
+
+
+ stats_by_cat_no_value
+ stats_by_cat_no_value
+ 100
+
+ 6
+
+
+ intval
+ intval
+ Integer
+
+
+ name
+ name
+ String
+ 2
+
+
+ count
+ count
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gml
new file mode 100644
index 00000000000..5590c2625a4
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_no_value.gml
@@ -0,0 +1,48 @@
+
+
+ missing
+
+
+
+ 1
+ aa
+ 2
+
+
+
+
+ dd
+ 2
+
+
+
+
+ 1
+ bb
+ 3
+
+
+
+
+ 120
+ 1
+
+
+
+
+ cc
+ 1
+
+
+
+
+ 2
+ bb
+ 1
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gfs
new file mode 100644
index 00000000000..0fb2358c926
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gfs
@@ -0,0 +1,62 @@
+
+
+ stats_by_cat_string
+ stats_by_cat_string
+ 100
+
+ 4
+
+
+ intval
+ intval
+ Integer
+
+
+ count
+ count
+ Integer
+
+
+ unique
+ unique
+ Integer
+
+
+ empty
+ empty
+ Integer
+
+
+ filled
+ filled
+ Integer
+
+
+ min
+ min
+ String
+ 2
+
+
+ max
+ max
+ String
+ 2
+
+
+ min_length
+ min_length
+ Integer
+
+
+ max_length
+ max_length
+ Integer
+
+
+ mean_length
+ mean_length
+ Integer
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gml
new file mode 100644
index 00000000000..600e578de7d
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_string.gml
@@ -0,0 +1,64 @@
+
+
+ missing
+
+
+
+ 1
+ 5
+ 2
+ 0
+ 5
+ aa
+ bb
+ 2
+ 2
+ 2
+
+
+
+
+ 3
+ 2
+ 0
+ 3
+ cc
+ dd
+ 2
+ 2
+ 2
+
+
+
+
+ 120
+ 1
+ 1
+ 1
+ 0
+
+
+ 0
+ 0
+ 0
+
+
+
+
+ 2
+ 1
+ 1
+ 0
+ 1
+ bb
+ bb
+ 2
+ 2
+ 2
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gfs
new file mode 100644
index 00000000000..348c809ee01
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gfs
@@ -0,0 +1,91 @@
+
+
+ stats_by_cat_two_fields
+ stats_by_cat_two_fields
+ 100
+
+ 6
+
+
+ intval
+ intval
+ Integer
+
+
+ name
+ name
+ String
+ 2
+
+
+ count
+ count
+ Integer
+
+
+ unique
+ unique
+ Integer
+
+
+ min
+ min
+ Real
+
+
+ max
+ max
+ Real
+
+
+ range
+ range
+ Real
+
+
+ sum
+ sum
+ Real
+
+
+ mean
+ mean
+ Real
+
+
+ median
+ median
+ Real
+
+
+ stddev
+ stddev
+ Real
+
+
+ minority
+ minority
+ Real
+
+
+ majority
+ majority
+ Real
+
+
+ q1
+ q1
+ Real
+
+
+ q3
+ q3
+ Real
+
+
+ iqr
+ iqr
+ Real
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gml b/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gml
new file mode 100644
index 00000000000..e8668bfdc2b
--- /dev/null
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_cat_two_fields.gml
@@ -0,0 +1,126 @@
+
+
+ missing
+
+
+
+ 1
+ aa
+ 2
+ 2
+ 3.33
+ 44.123456
+ 40.793456
+ 47.453456
+ 23.726728
+ 23.726728
+ 20.396728
+ 3.33
+ 3.33
+ 3.33
+ 44.123456
+ 40.793456
+
+
+
+
+ dd
+ 1
+ 1
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+
+
+
+
+ 1
+ bb
+ 3
+ 1
+ 0.123
+ 0.123
+ 0
+ 0.369
+ 0.123
+ 0.123
+ 0
+ 0.123
+ 0.123
+ 0.123
+ 0.123
+ 0
+
+
+
+
+ 120
+ 1
+ 1
+ -100291.43213
+ -100291.43213
+ 0
+ -100291.43213
+ -100291.43213
+ -100291.43213
+ 0
+ -100291.43213
+ -100291.43213
+ -100291.43213
+ -100291.43213
+ 0
+
+
+
+
+ cc
+ 1
+ 1
+ 0.123
+ 0.123
+ 0
+ 0.123
+ 0.123
+ 0.123
+ 0
+ 0.123
+ 0.123
+ 0.123
+ 0.123
+ 0
+
+
+
+
+ 2
+ bb
+ 1
+ 1
+ 0.123
+ 0.123
+ 0
+ 0.123
+ 0.123
+ 0.123
+ 0
+ 0.123
+ 0.123
+ 0.123
+ 0.123
+ 0
+
+
+
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_category.gfs b/python/plugins/processing/tests/testdata/expected/stats_by_category.gfs
index ba663caa0c0..99c98501a5b 100644
--- a/python/plugins/processing/tests/testdata/expected/stats_by_category.gfs
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_category.gfs
@@ -11,6 +11,16 @@
id2
Integer
+
+ count
+ count
+ Integer
+
+
+ unique
+ unique
+ Integer
+
min
min
@@ -21,24 +31,54 @@
max
Integer
+
+ range
+ range
+ Integer
+
+
+ sum
+ sum
+ Integer
+
mean
mean
Real
+
+ median
+ median
+ Real
+
stddev
stddev
Real
- sum
- sum
+ minority
+ minority
Integer
- count
- count
+ majority
+ majority
+ Integer
+
+
+ q1
+ q1
+ Integer
+
+
+ q3
+ q3
+ Integer
+
+
+ iqr
+ iqr
Integer
diff --git a/python/plugins/processing/tests/testdata/expected/stats_by_category.gml b/python/plugins/processing/tests/testdata/expected/stats_by_category.gml
index 4647a986f75..7fcd964d673 100644
--- a/python/plugins/processing/tests/testdata/expected/stats_by_category.gml
+++ b/python/plugins/processing/tests/testdata/expected/stats_by_category.gml
@@ -9,34 +9,58 @@
2
+ 2
+ 2
1
4
- 2.5
- 2.12132034355964
+ 3
5
- 2
+ 2.5
+ 2.5
+ 1.5
+ 1
+ 1
+ 1
+ 4
+ 3
1
+ 2
+ 2
2
5
- 3.5
- 2.12132034355964
+ 3
7
- 2
+ 3.5
+ 3.5
+ 1.5
+ 2
+ 2
+ 2
+ 5
+ 3
0
+ 5
+ 5
3
9
- 6.6
- 2.30217288664427
+ 6
33
- 5
+ 6.6
+ 7
+ 2.0591260281974
+ 3
+ 3
+ 6
+ 8
+ 2
diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
index 0bcff7fcfa1..887813449e4 100644
--- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
+++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml
@@ -3476,3 +3476,98 @@ tests:
name: expected/collect_two_fields.gml
type: vector
+
+ - algorithm: qgis:statisticsbycategories
+ name: Stats by cat (float field)
+ params:
+ CATEGORIES_FIELD_NAME:
+ - name
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ VALUES_FIELD_NAME: floatval
+ results:
+ OUTPUT:
+ name: expected/stats_by_cat_float.gml
+ type: vector
+ pk: name
+ compare:
+ fields:
+ fid: skip
+
+ - algorithm: qgis:statisticsbycategories
+ name: Stats by cat (string field)
+ params:
+ CATEGORIES_FIELD_NAME:
+ - intval
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ VALUES_FIELD_NAME: name
+ results:
+ OUTPUT:
+ name: expected/stats_by_cat_string.gml
+ type: vector
+ pk: intval
+ compare:
+ fields:
+ fid: skip
+
+ - algorithm: qgis:statisticsbycategories
+ name: Stats by cat (two category fields)
+ params:
+ CATEGORIES_FIELD_NAME:
+ - intval
+ - name
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ VALUES_FIELD_NAME: floatval
+ results:
+ OUTPUT:
+ name: expected/stats_by_cat_two_fields.gml
+ type: vector
+ pk:
+ - intval
+ - name
+ compare:
+ fields:
+ fid: skip
+
+ - algorithm: qgis:statisticsbycategories
+ name: Stats by cat (no value field)
+ params:
+ CATEGORIES_FIELD_NAME:
+ - intval
+ - name
+ INPUT:
+ name: dissolve_polys.gml
+ type: vector
+ results:
+ OUTPUT:
+ name: expected/stats_by_cat_no_value.gml
+ type: vector
+ pk:
+ - intval
+ - name
+ compare:
+ fields:
+ fid: skip
+
+ - algorithm: qgis:statisticsbycategories
+ name: Stats by cat (date field)
+ params:
+ CATEGORIES_FIELD_NAME:
+ - date
+ INPUT:
+ name: custom/datetimes.tab
+ type: vector
+ VALUES_FIELD_NAME: date
+ results:
+ OUTPUT:
+ name: expected/stats_by_cat_date.gml
+ type: vector
+ pk: date
+ compare:
+ fields:
+ fid: skip
\ No newline at end of file
diff --git a/python/testing/__init__.py b/python/testing/__init__.py
index 7b8359de5ee..4cea7c414cf 100644
--- a/python/testing/__init__.py
+++ b/python/testing/__init__.py
@@ -105,7 +105,11 @@ class TestCase(_TestCase):
def sort_by_pk_or_fid(f):
if 'pk' in kwargs and kwargs['pk'] is not None:
- return f[kwargs['pk']]
+ key = kwargs['pk']
+ if isinstance(key, list) or isinstance(key, tuple):
+ return [f[k] for k in key]
+ else:
+ return f[kwargs['pk']]
else:
return f.id()
diff --git a/src/core/processing/qgsprocessingparameters.cpp b/src/core/processing/qgsprocessingparameters.cpp
index 59561855195..ac1265060a6 100644
--- a/src/core/processing/qgsprocessingparameters.cpp
+++ b/src/core/processing/qgsprocessingparameters.cpp
@@ -2301,6 +2301,9 @@ bool QgsProcessingParameterField::checkValueIsAcceptable( const QVariant &input,
{
if ( !mAllowMultiple )
return false;
+
+ if ( input.toList().isEmpty() && !( mFlags & FlagOptional ) )
+ return false;
}
else if ( input.type() == QVariant::String )
{
diff --git a/tests/src/core/testqgsprocessing.cpp b/tests/src/core/testqgsprocessing.cpp
index de11fe2c9cc..0bc2c122043 100644
--- a/tests/src/core/testqgsprocessing.cpp
+++ b/tests/src/core/testqgsprocessing.cpp
@@ -2258,6 +2258,8 @@ void TestQgsProcessing::parameterLayerList()
QVERIFY( !def->checkValueIsAcceptable( true ) );
QVERIFY( !def->checkValueIsAcceptable( 5 ) );
QVERIFY( !def->checkValueIsAcceptable( "layer12312312" ) );
+ QVERIFY( !def->checkValueIsAcceptable( QVariantList() ) );
+ QVERIFY( !def->checkValueIsAcceptable( QStringList() ) );
QVERIFY( def->checkValueIsAcceptable( QStringList() << "layer12312312" << "layerB" ) );
QVERIFY( def->checkValueIsAcceptable( QVariantList() << "layer12312312" << "layerB" ) );
QVERIFY( !def->checkValueIsAcceptable( "" ) );
@@ -3183,6 +3185,8 @@ void TestQgsProcessing::parameterField()
QVERIFY( def->checkValueIsAcceptable( QVariantList() << "a" << "b" ) );
QVERIFY( !def->checkValueIsAcceptable( "" ) );
QVERIFY( !def->checkValueIsAcceptable( QVariant() ) );
+ QVERIFY( !def->checkValueIsAcceptable( QStringList() ) );
+ QVERIFY( !def->checkValueIsAcceptable( QVariantList() ) );
params.insert( "non_optional", QString( "a;b" ) );
fields = QgsProcessingParameters::parameterAsFields( def.get(), params, context );