Skip to content

Commit a17facf

Browse files
authoredDec 1, 2016
Merge pull request #3816 from nyalldawson/processing_datetime
[processing][FEATURE] New unified basic stats algorithm
2 parents 2c4eb3c + b30a1ff commit a17facf

25 files changed

+525
-116
lines changed
 

‎python/core/qgsstringstatisticalsummary.sip

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class QgsStringStatisticalSummary
3131
Max, //!< Maximum string value
3232
MinimumLength, //!< Minimum length of string
3333
MaximumLength, //!< Maximum length of string
34+
MeanLength, //!< Mean length of strings
3435
All, //! All statistics
3536
};
3637
typedef QFlags<QgsStringStatisticalSummary::Statistic> Statistics;
@@ -147,6 +148,12 @@ class QgsStringStatisticalSummary
147148
*/
148149
int maxLength() const;
149150

151+
/**
152+
* Returns the mean length of strings.
153+
* @note added in QGIS 3.0
154+
*/
155+
double meanLength() const;
156+
150157
/** Returns the friendly display name for a statistic
151158
* @param statistic statistic to return name for
152159
*/

‎python/plugins/processing/algs/help/qgis.yaml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,10 @@ qgis:advancedpythonfieldcalculator: >
2222

2323
qgis:barplot:
2424

25+
qgis:basicstatisticsforfields: >
26+
This algorithm generates basic statistics from the analysis of a values in a field in the attribute table of a vector layer. Numeric, date, time and string fields are supported.
2527

26-
qgis:basicstatisticsfornumericfields: >
27-
This algorithm generates basic statistics from the analysis of a numeric field in the attribute table of a vector layer.
28-
29-
Statistics are generated as an HTML file.
30-
31-
qgis:basicstatisticsfortextfields: >
32-
This algorithm generates basic statistics from the analysis of a text field in the attribute table of a vector layer.
28+
The statistics returned will depend on the field type.
3329

3430
Statistics are generated as an HTML file.
3531

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
***************************************************************************
5+
BasicStatistics.py
6+
---------------------
7+
Date : November 2016
8+
Copyright : (C) 2016 by Nyall Dawson
9+
Email : nyall dot dawson at gmail dot com
10+
***************************************************************************
11+
* *
12+
* This program is free software; you can redistribute it and/or modify *
13+
* it under the terms of the GNU General Public License as published by *
14+
* the Free Software Foundation; either version 2 of the License, or *
15+
* (at your option) any later version. *
16+
* *
17+
***************************************************************************
18+
"""
19+
20+
__author__ = 'Nyall Dawson'
21+
__date__ = 'November 2016'
22+
__copyright__ = '(C) 2016, Nyall Dawson'
23+
24+
# This will get replaced with a git SHA1 when you do a git archive
25+
26+
__revision__ = '$Format:%H$'
27+
28+
import os
29+
import codecs
30+
31+
from qgis.PyQt.QtCore import QVariant
32+
from qgis.PyQt.QtGui import QIcon
33+
34+
from qgis.core import (QgsStatisticalSummary,
35+
QgsStringStatisticalSummary,
36+
QgsDateTimeStatisticalSummary,
37+
QgsFeatureRequest)
38+
39+
from processing.core.GeoAlgorithm import GeoAlgorithm
40+
from processing.core.parameters import ParameterTable
41+
from processing.core.parameters import ParameterTableField
42+
from processing.core.outputs import OutputHTML
43+
from processing.core.outputs import OutputNumber
44+
from processing.tools import dataobjects, vector
45+
46+
47+
pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
48+
49+
50+
class BasicStatisticsForField(GeoAlgorithm):
51+
52+
INPUT_LAYER = 'INPUT_LAYER'
53+
FIELD_NAME = 'FIELD_NAME'
54+
OUTPUT_HTML_FILE = 'OUTPUT_HTML_FILE'
55+
56+
MIN = 'MIN'
57+
MAX = 'MAX'
58+
COUNT = 'COUNT'
59+
UNIQUE = 'UNIQUE'
60+
EMPTY = 'EMPTY'
61+
FILLED = 'FILLED'
62+
MIN_LENGTH = 'MIN_LENGTH'
63+
MAX_LENGTH = 'MAX_LENGTH'
64+
MEAN_LENGTH = 'MEAN_LENGTH'
65+
CV = 'CV'
66+
SUM = 'SUM'
67+
MEAN = 'MEAN'
68+
STD_DEV = 'STD_DEV'
69+
RANGE = 'RANGE'
70+
MEDIAN = 'MEDIAN'
71+
MINORITY = 'MINORITY'
72+
MAJORITY = 'MAJORITY'
73+
FIRSTQUARTILE = 'FIRSTQUARTILE'
74+
THIRDQUARTILE = 'THIRDQUARTILE'
75+
IQR = 'IQR'
76+
77+
def getIcon(self):
78+
return QIcon(os.path.join(pluginPath, 'images', 'ftools', 'basic_statistics.png'))
79+
80+
def defineCharacteristics(self):
81+
self.name, self.i18n_name = self.trAlgorithm('Basic statistics for fields')
82+
self.group, self.i18n_group = self.trAlgorithm('Vector table tools')
83+
self.tags = self.tr('stats,statistics,date,time,datetime,string,number,text,table,layer,maximum,minimum,mean,average,standard,deviation,'
84+
'count,distinct,unique,variance,median,quartile,range,majority,minority')
85+
86+
self.addParameter(ParameterTable(self.INPUT_LAYER,
87+
self.tr('Input table')))
88+
self.addParameter(ParameterTableField(self.FIELD_NAME,
89+
self.tr('Field to calculate statistics on'),
90+
self.INPUT_LAYER))
91+
92+
self.addOutput(OutputHTML(self.OUTPUT_HTML_FILE,
93+
self.tr('Statistics')))
94+
95+
self.addOutput(OutputNumber(self.COUNT, self.tr('Count')))
96+
self.addOutput(OutputNumber(self.UNIQUE, self.tr('Number of unique values')))
97+
self.addOutput(OutputNumber(self.EMPTY, self.tr('Number of empty (null) values')))
98+
self.addOutput(OutputNumber(self.FILLED, self.tr('Number of non-empty values')))
99+
self.addOutput(OutputNumber(self.MIN, self.tr('Minimum value')))
100+
self.addOutput(OutputNumber(self.MAX, self.tr('Maximum value')))
101+
self.addOutput(OutputNumber(self.MIN_LENGTH, self.tr('Minimum length')))
102+
self.addOutput(OutputNumber(self.MAX_LENGTH, self.tr('Maximum length')))
103+
self.addOutput(OutputNumber(self.MEAN_LENGTH, self.tr('Mean length')))
104+
self.addOutput(OutputNumber(self.CV, self.tr('Coefficient of Variation')))
105+
self.addOutput(OutputNumber(self.SUM, self.tr('Sum')))
106+
self.addOutput(OutputNumber(self.MEAN, self.tr('Mean value')))
107+
self.addOutput(OutputNumber(self.STD_DEV, self.tr('Standard deviation')))
108+
self.addOutput(OutputNumber(self.RANGE, self.tr('Range')))
109+
self.addOutput(OutputNumber(self.MEDIAN, self.tr('Median')))
110+
self.addOutput(OutputNumber(self.MINORITY, self.tr('Minority (rarest occurring value)')))
111+
self.addOutput(OutputNumber(self.MAJORITY, self.tr('Majority (most frequently occurring value)')))
112+
self.addOutput(OutputNumber(self.FIRSTQUARTILE, self.tr('First quartile')))
113+
self.addOutput(OutputNumber(self.THIRDQUARTILE, self.tr('Third quartile')))
114+
self.addOutput(OutputNumber(self.IQR, self.tr('Interquartile Range (IQR)')))
115+
116+
def processAlgorithm(self, progress):
117+
layer = dataobjects.getObjectFromUri(
118+
self.getParameterValue(self.INPUT_LAYER))
119+
field_name = self.getParameterValue(self.FIELD_NAME)
120+
field = layer.fields().at(layer.fields().lookupField(field_name))
121+
122+
output_file = self.getOutputValue(self.OUTPUT_HTML_FILE)
123+
124+
request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes([field_name], layer.fields())
125+
features = vector.features(layer, request)
126+
127+
data = []
128+
data.append(self.tr('Analyzed layer: {}').format(layer.name()))
129+
data.append(self.tr('Analyzed field: {}').format(field_name))
130+
131+
if field.isNumeric():
132+
data.extend(self.calcNumericStats(features, progress, field))
133+
elif field.type() in (QVariant.Date, QVariant.Time, QVariant.DateTime):
134+
data.extend(self.calcDateTimeStats(features, progress, field))
135+
else:
136+
data.extend(self.calcStringStats(features, progress, field))
137+
138+
self.createHTML(output_file, data)
139+
140+
def calcNumericStats(self, features, progress, field):
141+
count = len(features)
142+
total = 100.0 / float(count)
143+
stat = QgsStatisticalSummary()
144+
for current, ft in enumerate(features):
145+
stat.addVariant(ft[field.name()])
146+
progress.setPercentage(int(current * total))
147+
stat.finalize()
148+
149+
cv = stat.stDev() / stat.mean() if stat.mean() != 0 else 0
150+
151+
self.setOutputValue(self.COUNT, stat.count())
152+
self.setOutputValue(self.UNIQUE, stat.variety())
153+
self.setOutputValue(self.EMPTY, stat.countMissing())
154+
self.setOutputValue(self.FILLED, count - stat.countMissing())
155+
self.setOutputValue(self.MIN, stat.min())
156+
self.setOutputValue(self.MAX, stat.max())
157+
self.setOutputValue(self.RANGE, stat.range())
158+
self.setOutputValue(self.SUM, stat.sum())
159+
self.setOutputValue(self.MEAN, stat.mean())
160+
self.setOutputValue(self.MEDIAN, stat.median())
161+
self.setOutputValue(self.STD_DEV, stat.stDev())
162+
self.setOutputValue(self.CV, cv)
163+
self.setOutputValue(self.MINORITY, stat.minority())
164+
self.setOutputValue(self.MAJORITY, stat.majority())
165+
self.setOutputValue(self.FIRSTQUARTILE, stat.firstQuartile())
166+
self.setOutputValue(self.THIRDQUARTILE, stat.thirdQuartile())
167+
self.setOutputValue(self.IQR, stat.interQuartileRange())
168+
169+
data = []
170+
data.append(self.tr('Count: {}').format(stat.count()))
171+
data.append(self.tr('Unique values: {}').format(stat.variety()))
172+
data.append(self.tr('NULL (missing) values: {}').format(stat.countMissing()))
173+
data.append(self.tr('Minimum value: {}').format(stat.min()))
174+
data.append(self.tr('Maximum value: {}').format(stat.max()))
175+
data.append(self.tr('Range: {}').format(stat.range()))
176+
data.append(self.tr('Sum: {}').format(stat.sum()))
177+
data.append(self.tr('Mean value: {}').format(stat.mean()))
178+
data.append(self.tr('Median value: {}').format(stat.median()))
179+
data.append(self.tr('Standard deviation: {}').format(stat.stDev()))
180+
data.append(self.tr('Coefficient of Variation: {}').format(cv))
181+
data.append(self.tr('Minority (rarest occurring value): {}').format(stat.minority()))
182+
data.append(self.tr('Majority (most frequently occurring value): {}').format(stat.majority()))
183+
data.append(self.tr('First quartile: {}').format(stat.firstQuartile()))
184+
data.append(self.tr('Third quartile: {}').format(stat.thirdQuartile()))
185+
data.append(self.tr('Interquartile Range (IQR): {}').format(stat.interQuartileRange()))
186+
return data
187+
188+
def calcStringStats(self, features, progress, field):
189+
count = len(features)
190+
total = 100.0 / float(count)
191+
stat = QgsStringStatisticalSummary()
192+
for current, ft in enumerate(features):
193+
stat.addValue(ft[field.name()])
194+
progress.setPercentage(int(current * total))
195+
stat.finalize()
196+
197+
self.setOutputValue(self.COUNT, stat.count())
198+
self.setOutputValue(self.UNIQUE, stat.countDistinct())
199+
self.setOutputValue(self.EMPTY, stat.countMissing())
200+
self.setOutputValue(self.FILLED, stat.count() - stat.countMissing())
201+
self.setOutputValue(self.MIN, stat.min())
202+
self.setOutputValue(self.MAX, stat.max())
203+
self.setOutputValue(self.MIN_LENGTH, stat.minLength())
204+
self.setOutputValue(self.MAX_LENGTH, stat.maxLength())
205+
self.setOutputValue(self.MEAN_LENGTH, stat.meanLength())
206+
207+
data = []
208+
data.append(self.tr('Count: {}').format(count))
209+
data.append(self.tr('Unique values: {}').format(stat.countDistinct()))
210+
data.append(self.tr('NULL (missing) values: {}').format(stat.countMissing()))
211+
data.append(self.tr('Minimum value: {}').format(stat.min()))
212+
data.append(self.tr('Maximum value: {}').format(stat.max()))
213+
data.append(self.tr('Minimum length: {}').format(stat.minLength()))
214+
data.append(self.tr('Maximum length: {}').format(stat.maxLength()))
215+
data.append(self.tr('Mean length: {}').format(stat.meanLength()))
216+
217+
return data
218+
219+
def calcDateTimeStats(self, features, progress, field):
220+
count = len(features)
221+
total = 100.0 / float(count)
222+
stat = QgsDateTimeStatisticalSummary()
223+
for current, ft in enumerate(features):
224+
stat.addValue(ft[field.name()])
225+
progress.setPercentage(int(current * total))
226+
stat.finalize()
227+
228+
self.setOutputValue(self.COUNT, stat.count())
229+
self.setOutputValue(self.UNIQUE, stat.countDistinct())
230+
self.setOutputValue(self.EMPTY, stat.countMissing())
231+
self.setOutputValue(self.FILLED, stat.count() - stat.countMissing())
232+
self.setOutputValue(self.MIN, stat.statistic(QgsDateTimeStatisticalSummary.Min))
233+
self.setOutputValue(self.MAX, stat.statistic(QgsDateTimeStatisticalSummary.Max))
234+
235+
data = []
236+
data.append(self.tr('Count: {}').format(count))
237+
data.append(self.tr('Unique values: {}').format(stat.countDistinct()))
238+
data.append(self.tr('NULL (missing) values: {}').format(stat.countMissing()))
239+
data.append(self.tr('Minimum value: {}').format(field.displayString(stat.statistic(QgsDateTimeStatisticalSummary.Min))))
240+
data.append(self.tr('Maximum value: {}').format(field.displayString(stat.statistic(QgsDateTimeStatisticalSummary.Max))))
241+
242+
return data
243+
244+
def createHTML(self, outputFile, algData):
245+
with codecs.open(outputFile, 'w', encoding='utf-8') as f:
246+
f.write('<html><head>\n')
247+
f.write('<meta http-equiv="Content-Type" content="text/html; \
248+
charset=utf-8" /></head><body>\n')
249+
for s in algData:
250+
f.write('<p>' + str(s) + '</p>\n')
251+
f.write('</body></html>\n')

‎python/plugins/processing/algs/qgis/BasicStatisticsNumbers.py

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131

3232
from qgis.PyQt.QtGui import QIcon
3333

34-
from qgis.core import QgsStatisticalSummary
34+
from qgis.core import (QgsStatisticalSummary,
35+
QgsFeatureRequest)
3536

3637
from processing.core.GeoAlgorithm import GeoAlgorithm
37-
from processing.core.parameters import ParameterVector
38+
from processing.core.parameters import ParameterTable
3839
from processing.core.parameters import ParameterTableField
3940
from processing.core.outputs import OutputHTML
4041
from processing.core.outputs import OutputNumber
@@ -67,15 +68,21 @@ class BasicStatisticsNumbers(GeoAlgorithm):
6768
NULLVALUES = 'NULLVALUES'
6869
IQR = 'IQR'
6970

71+
def __init__(self):
72+
GeoAlgorithm.__init__(self)
73+
# this algorithm is deprecated - use BasicStatistics instead
74+
self.showInToolbox = False
75+
7076
def getIcon(self):
7177
return QIcon(os.path.join(pluginPath, 'images', 'ftools', 'basic_statistics.png'))
7278

7379
def defineCharacteristics(self):
7480
self.name, self.i18n_name = self.trAlgorithm('Basic statistics for numeric fields')
7581
self.group, self.i18n_group = self.trAlgorithm('Vector table tools')
82+
self.tags = self.tr('stats,statistics,number,table,layer')
7683

77-
self.addParameter(ParameterVector(self.INPUT_LAYER,
78-
self.tr('Input vector layer')))
84+
self.addParameter(ParameterTable(self.INPUT_LAYER,
85+
self.tr('Input vector layer')))
7986
self.addParameter(ParameterTableField(self.FIELD_NAME,
8087
self.tr('Field to calculate statistics on'),
8188
self.INPUT_LAYER, ParameterTableField.DATA_TYPE_NUMBER))
@@ -107,36 +114,16 @@ def processAlgorithm(self, progress):
107114

108115
outputFile = self.getOutputValue(self.OUTPUT_HTML_FILE)
109116

110-
cvValue = 0
111-
minValue = 0
112-
maxValue = 0
113-
sumValue = 0
114-
meanValue = 0
115-
medianValue = 0
116-
stdDevValue = 0
117-
minority = 0
118-
majority = 0
119-
firstQuartile = 0
120-
thirdQuartile = 0
121-
nullValues = 0
122-
iqr = 0
123-
124-
values = []
125-
126-
features = vector.features(layer)
117+
request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes([fieldName], layer.fields())
118+
stat = QgsStatisticalSummary()
119+
features = vector.features(layer, request)
127120
count = len(features)
128121
total = 100.0 / float(count)
129122
for current, ft in enumerate(features):
130-
value = ft[fieldName]
131-
if value or value == 0:
132-
values.append(float(value))
133-
else:
134-
nullValues += 1
135-
123+
stat.addVariant(ft[fieldName])
136124
progress.setPercentage(int(current * total))
137125

138-
stat = QgsStatisticalSummary()
139-
stat.calculate(values)
126+
stat.finalize()
140127

141128
count = stat.count()
142129
uniqueValue = stat.variety()
@@ -147,13 +134,13 @@ def processAlgorithm(self, progress):
147134
meanValue = stat.mean()
148135
medianValue = stat.median()
149136
stdDevValue = stat.stDev()
150-
if meanValue != 0.00:
151-
cvValue = stdDevValue / meanValue
137+
cvValue = stdDevValue / meanValue if meanValue != 0 else 0
152138
minority = stat.minority()
153139
majority = stat.majority()
154140
firstQuartile = stat.firstQuartile()
155141
thirdQuartile = stat.thirdQuartile()
156142
iqr = stat.interQuartileRange()
143+
nullValues = stat.countMissing()
157144

158145
data = []
159146
data.append(self.tr('Analyzed layer: {}').format(layer.name()))

‎python/plugins/processing/algs/qgis/BasicStatisticsStrings.py

Lines changed: 42 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@
3131

3232
from qgis.PyQt.QtGui import QIcon
3333

34+
from qgis.core import (QgsStringStatisticalSummary,
35+
QgsFeatureRequest)
36+
3437
from processing.core.GeoAlgorithm import GeoAlgorithm
35-
from processing.core.parameters import ParameterVector
38+
from processing.core.parameters import ParameterTable
3639
from processing.core.parameters import ParameterTableField
3740
from processing.core.outputs import OutputHTML
3841
from processing.core.outputs import OutputNumber
@@ -54,16 +57,24 @@ class BasicStatisticsStrings(GeoAlgorithm):
5457
EMPTY = 'EMPTY'
5558
FILLED = 'FILLED'
5659
UNIQUE = 'UNIQUE'
60+
MIN_VALUE = 'MIN_VALUE'
61+
MAX_VALUE = 'MAX_VALUE'
62+
63+
def __init__(self):
64+
GeoAlgorithm.__init__(self)
65+
# this algorithm is deprecated - use BasicStatistics instead
66+
self.showInToolbox = False
5767

5868
def getIcon(self):
5969
return QIcon(os.path.join(pluginPath, 'images', 'ftools', 'basic_statistics.png'))
6070

6171
def defineCharacteristics(self):
6272
self.name, self.i18n_name = self.trAlgorithm('Basic statistics for text fields')
6373
self.group, self.i18n_group = self.trAlgorithm('Vector table tools')
74+
self.tags = self.tr('stats,statistics,string,table,layer')
6475

65-
self.addParameter(ParameterVector(self.INPUT_LAYER,
66-
self.tr('Input vector layer')))
76+
self.addParameter(ParameterTable(self.INPUT_LAYER,
77+
self.tr('Input vector layer')))
6778
self.addParameter(ParameterTableField(self.FIELD_NAME,
6879
self.tr('Field to calculate statistics on'),
6980
self.INPUT_LAYER, ParameterTableField.DATA_TYPE_STRING))
@@ -78,6 +89,8 @@ def defineCharacteristics(self):
7889
self.addOutput(OutputNumber(self.EMPTY, self.tr('Number of empty values')))
7990
self.addOutput(OutputNumber(self.FILLED, self.tr('Number of non-empty values')))
8091
self.addOutput(OutputNumber(self.UNIQUE, self.tr('Number of unique values')))
92+
self.addOutput(OutputNumber(self.MIN_VALUE, self.tr('Minimum string value')))
93+
self.addOutput(OutputNumber(self.MAX_VALUE, self.tr('Maximum string value')))
8194

8295
def processAlgorithm(self, progress):
8396
layer = dataobjects.getObjectFromUri(
@@ -86,77 +99,47 @@ def processAlgorithm(self, progress):
8699

87100
outputFile = self.getOutputValue(self.OUTPUT_HTML_FILE)
88101

89-
index = layer.fields().lookupField(fieldName)
90-
91-
sumValue = 0
92-
minValue = 0
93-
maxValue = 0
94-
meanValue = 0
95-
nullValues = 0
96-
filledValues = 0
97-
98-
isFirst = True
99-
values = []
100-
101-
features = vector.features(layer)
102+
request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes([fieldName],
103+
layer.fields())
104+
stat = QgsStringStatisticalSummary()
105+
features = vector.features(layer, request)
102106
count = len(features)
103-
total = 100.0 / count
107+
total = 100.0 / float(count)
104108
for current, ft in enumerate(features):
105-
value = ft[fieldName]
106-
if value:
107-
length = float(len(value))
108-
filledValues += 1
109-
else:
110-
nullValues += 1
111-
progress.setPercentage(int(current * total))
112-
continue
113-
114-
if isFirst:
115-
minValue = length
116-
maxValue = length
117-
isFirst = False
118-
else:
119-
if length < minValue:
120-
minValue = length
121-
if length > maxValue:
122-
maxValue = length
123-
124-
values.append(length)
125-
sumValue += length
126-
109+
stat.addValue(ft[fieldName])
127110
progress.setPercentage(int(current * total))
128111

129-
n = float(len(values))
130-
if n > 0:
131-
meanValue = sumValue / n
132-
133-
uniqueValues = vector.getUniqueValuesCount(layer, index)
112+
stat.finalize()
134113

135114
data = []
136115
data.append(self.tr('Analyzed layer: {}').format(layer.name()))
137116
data.append(self.tr('Analyzed field: {}').format(fieldName))
138-
data.append(self.tr('Minimum length: {}').format(minValue))
139-
data.append(self.tr('Maximum length: {}').format(maxValue))
140-
data.append(self.tr('Mean length: {}').format(meanValue))
141-
data.append(self.tr('Filled values: {}').format(filledValues))
142-
data.append(self.tr('NULL (missing) values: {}').format(nullValues))
143-
data.append(self.tr('Count: {}').format(count))
144-
data.append(self.tr('Unique: {}').format(uniqueValues))
117+
data.append(self.tr('Minimum length: {}').format(stat.minLength()))
118+
data.append(self.tr('Maximum length: {}').format(stat.maxLength()))
119+
data.append(self.tr('Mean length: {}').format(stat.meanLength()))
120+
data.append(self.tr('Filled values: {}').format(stat.count() - stat.countMissing()))
121+
data.append(self.tr('NULL (missing) values: {}').format(stat.countMissing()))
122+
data.append(self.tr('Count: {}').format(stat.count()))
123+
data.append(self.tr('Unique: {}').format(stat.countDistinct()))
124+
data.append(self.tr('Minimum string value: {}').format(stat.min()))
125+
data.append(self.tr('Maximum string value: {}').format(stat.max()))
145126

146127
self.createHTML(outputFile, data)
147128

148-
self.setOutputValue(self.MIN_LEN, minValue)
149-
self.setOutputValue(self.MAX_LEN, maxValue)
150-
self.setOutputValue(self.MEAN_LEN, meanValue)
151-
self.setOutputValue(self.FILLED, filledValues)
152-
self.setOutputValue(self.EMPTY, nullValues)
153-
self.setOutputValue(self.COUNT, count)
154-
self.setOutputValue(self.UNIQUE, uniqueValues)
129+
self.setOutputValue(self.MIN_LEN, stat.minLength())
130+
self.setOutputValue(self.MAX_LEN, stat.maxLength())
131+
self.setOutputValue(self.MEAN_LEN, stat.meanLength())
132+
self.setOutputValue(self.FILLED, stat.count() - stat.countMissing())
133+
self.setOutputValue(self.EMPTY, stat.countMissing())
134+
self.setOutputValue(self.COUNT, stat.count())
135+
self.setOutputValue(self.UNIQUE, stat.countDistinct())
136+
self.setOutputValue(self.MIN_VALUE, stat.min())
137+
self.setOutputValue(self.MAX_VALUE, stat.max())
155138

156139
def createHTML(self, outputFile, algData):
157140
with codecs.open(outputFile, 'w', encoding='utf-8') as f:
158141
f.write('<html><head>\n')
159142
f.write('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>\n')
160143
for s in algData:
161144
f.write('<p>' + str(s) + '</p>\n')
162-
f.write('</body></html>')
145+
f.write('</body></html>\n')

‎python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
from .PoleOfInaccessibility import PoleOfInaccessibility
181181
from .CreateAttributeIndex import CreateAttributeIndex
182182
from .DropGeometry import DropGeometry
183+
from .BasicStatistics import BasicStatisticsForField
183184

184185
pluginPath = os.path.normpath(os.path.join(
185186
os.path.split(os.path.dirname(__file__))[0], os.pardir))
@@ -243,7 +244,8 @@ def __init__(self):
243244
TinInterpolationZValue(), TinInterpolationAttribute(),
244245
RemoveNullGeometry(), ExtractByExpression(), ExtendLines(),
245246
ExtractSpecificNodes(), GeometryByExpression(), SnapGeometriesToLayer(),
246-
PoleOfInaccessibility(), CreateAttributeIndex(), DropGeometry()
247+
PoleOfInaccessibility(), CreateAttributeIndex(), DropGeometry(),
248+
BasicStatisticsForField()
247249
]
248250

249251
if hasMatplotlib:

‎python/plugins/processing/core/parameters.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,7 @@ class ParameterTableField(Parameter):
13391339

13401340
DATA_TYPE_NUMBER = 0
13411341
DATA_TYPE_STRING = 1
1342+
DATA_TYPE_DATETIME = 2
13421343
DATA_TYPE_ANY = -1
13431344

13441345
def __init__(self, name='', description='', parent=None, datatype=-1,
@@ -1376,6 +1377,8 @@ def dataType(self):
13761377
return 'numeric'
13771378
elif self.datatype == self.DATA_TYPE_STRING:
13781379
return 'string'
1380+
elif self.datatype == self.DATA_TYPE_DATETIME:
1381+
return 'datetime'
13791382
else:
13801383
return 'any'
13811384

@@ -1397,6 +1400,9 @@ def fromScriptCode(self, line):
13971400
elif definition.lower().strip().startswith('field string'):
13981401
parent = definition.strip()[len('field string') + 1:]
13991402
datatype = ParameterTableField.DATA_TYPE_STRING
1403+
elif definition.lower().strip().startswith('field datetime'):
1404+
parent = definition.strip()[len('field datetime') + 1:]
1405+
datatype = ParameterTableField.DATA_TYPE_DATETIME
14001406
else:
14011407
parent = definition.strip()[len('field') + 1:]
14021408
datatype = ParameterTableField.DATA_TYPE_ANY

‎python/plugins/processing/gui/ResultsDialog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def __init__(self):
4949
self.keyIcon = QIcon()
5050
self.keyIcon.addPixmap(self.style().standardPixmap(QStyle.SP_FileIcon))
5151

52-
self.tree.itemClicked.connect(self.changeResult)
52+
self.tree.currentItemChanged.connect(self.changeResult)
5353

5454
self.fillTree()
5555

‎python/plugins/processing/gui/wrappers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1022,7 +1022,8 @@ def createWidget(self):
10221022
widget.setFilters(QgsFieldProxyModel.Numeric)
10231023
elif self.param.datatype == ParameterTableField.DATA_TYPE_STRING:
10241024
widget.setFilters(QgsFieldProxyModel.String)
1025-
1025+
elif self.param.datatype == ParameterTableField.DATA_TYPE_DATETIME:
1026+
widget.setFilters(QgsFieldProxyModel.Date | QgsFieldProxyModel.Time)
10261027
return widget
10271028
else:
10281029
widget = QComboBox()
@@ -1067,6 +1068,8 @@ def getFields(self):
10671068
elif self.param.datatype == ParameterTableField.DATA_TYPE_NUMBER:
10681069
fieldTypes = [QVariant.Int, QVariant.Double, QVariant.LongLong,
10691070
QVariant.UInt, QVariant.ULongLong]
1071+
elif self.param.datatype == ParameterTableField.DATA_TYPE_DATETIME:
1072+
fieldTypes = [QVariant.Date, QVariant.Time, QVariant.DateTime]
10701073

10711074
fieldNames = set()
10721075
for field in self._layer.fields():

‎python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def setupUi(self):
140140
self.datatypeCombo.addItem(self.tr('Any'), -1)
141141
self.datatypeCombo.addItem(self.tr('Number'), 0)
142142
self.datatypeCombo.addItem(self.tr('String'), 1)
143+
self.datatypeCombo.addItem(self.tr('Date/time'), 2)
143144
self.verticalLayout.addWidget(self.datatypeCombo)
144145

145146
if self.param is not None and self.param.datatype is not None:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
!table
2+
!version 900
3+
!charset Neutral
4+
5+
Definition Table
6+
Type NATIVE Charset "Neutral"
7+
Fields 3
8+
date Date ;
9+
time Time ;
10+
date_time DateTime ;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<html><head>
2+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
3+
<p>Analyzed layer: custom/datetimes.tab</p>
4+
<p>Analyzed field: date</p>
5+
<p>Count: 4</p>
6+
<p>Unique values: 3</p>
7+
<p>NULL (missing) values: 1</p>
8+
<p>Minimum value: 2014-11-30T00:00:00</p>
9+
<p>Maximum value: 2016-11-30T00:00:00</p>
10+
</body></html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<html><head>
2+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
3+
<p>Analyzed layer: custom/datetimes.tab</p>
4+
<p>Analyzed field: date_time</p>
5+
<p>Count: 4</p>
6+
<p>Unique values: 3</p>
7+
<p>NULL (missing) values: 1</p>
8+
<p>Minimum value: 2014-11-30T14:30:02</p>
9+
<p>Maximum value: 2016-11-30T14:29:22</p>
10+
</body></html>

‎python/plugins/processing/tests/testdata/expected/basic_statistics_string.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
33
<p>Analyzed layer: multipolys.gml</p>
44
<p>Analyzed field: Bname</p>
5-
<p>Minimum length: 4.0</p>
6-
<p>Maximum length: 4.0</p>
7-
<p>Mean length: 4.0</p>
5+
<p>Minimum length: 0</p>
6+
<p>Maximum length: 4</p>
7+
<p>Mean length: 3.0</p>
88
<p>Filled values: 3</p>
99
<p>NULL (missing) values: 1</p>
1010
<p>Count: 4</p>
1111
<p>Unique: 2</p>
12-
</body></html>
12+
<p>Minimum string value: Test</p>
13+
<p>Maximum string value: Test</p>
14+
</body></html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<html><head>
2+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
3+
<p>Analyzed layer: custom/datetimes.tab</p>
4+
<p>Analyzed field: time</p>
5+
<p>Count: 4</p>
6+
<p>Unique values: 3</p>
7+
<p>NULL (missing) values: 1</p>
8+
<p>Minimum value: 03:29:40</p>
9+
<p>Maximum value: 15:29:22</p>
10+
</body></html>

‎python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ tests:
152152
fields:
153153
fid: skip
154154

155-
- algorithm: qgis:basicstatisticsfornumericfields
155+
- algorithm: qgis:basicstatisticsforfields
156156
name: Basic statistics for numeric fields
157157
params:
158158
- name: multipolys.gml
@@ -182,7 +182,7 @@ tests:
182182
- 'NULL \(missing\) values: 1'
183183
- 'Interquartile Range \(IQR\): 0.123'
184184

185-
- algorithm: qgis:basicstatisticsfortextfields
185+
- algorithm: qgis:basicstatisticsforfields
186186
name: Basic statistics for text fields
187187
params:
188188
- name: multipolys.gml
@@ -191,7 +191,18 @@ tests:
191191
results:
192192
OUTPUT_HTML_FILE:
193193
name: expected/basic_statistics_string.html
194-
type: file
194+
type: regex
195+
rules:
196+
- 'Analyzed layer: multipolys.gml'
197+
- 'Analyzed field: Bname'
198+
- 'Count: 4'
199+
- 'Unique values: 2'
200+
- 'Minimum value: Test'
201+
- 'Maximum value: Test'
202+
- 'Minimum length: 0'
203+
- 'Maximum length: 4'
204+
- 'Mean length: 3.0'
205+
- 'NULL \(missing\) values: 1'
195206

196207
# Split lines with lines considers two cases
197208
# case 1: two different layers
@@ -1753,3 +1764,63 @@ tests:
17531764
OUTPUT:
17541765
name: expected/removed_holes_min_area.gml
17551766
type: vector
1767+
1768+
- algorithm: qgis:basicstatisticsforfields
1769+
name: Basic stats datetime
1770+
params:
1771+
FIELD_NAME: date_time
1772+
INPUT_LAYER:
1773+
name: custom/datetimes.tab
1774+
type: table
1775+
results:
1776+
OUTPUT_HTML_FILE:
1777+
name: expected/basic_statistics_datetime.html
1778+
type: regex
1779+
rules:
1780+
- 'Analyzed layer: custom/datetimes.tab'
1781+
- 'Analyzed field: date_time'
1782+
- 'Count: 4'
1783+
- 'Unique values: 3'
1784+
- 'Minimum value: 2014-11-30T14:30:02'
1785+
- 'Maximum value: 2016-11-30T14:29:22'
1786+
- 'NULL \(missing\) values: 1'
1787+
1788+
- algorithm: qgis:basicstatisticsforfields
1789+
name: Basic stats date
1790+
params:
1791+
FIELD_NAME: date
1792+
INPUT_LAYER:
1793+
name: custom/datetimes.tab
1794+
type: table
1795+
results:
1796+
OUTPUT_HTML_FILE:
1797+
name: expected/basic_statistics_date.html
1798+
type: regex
1799+
rules:
1800+
- 'Analyzed layer: custom/datetimes.tab'
1801+
- 'Analyzed field: date'
1802+
- 'Count: 4'
1803+
- 'Unique values: 3'
1804+
- 'Minimum value: 2014-11-30T00:00:00'
1805+
- 'Maximum value: 2016-11-30T00:00:00'
1806+
- 'NULL \(missing\) values: 1'
1807+
1808+
- algorithm: qgis:basicstatisticsforfields
1809+
name: Basic stats time
1810+
params:
1811+
FIELD_NAME: time
1812+
INPUT_LAYER:
1813+
name: custom/datetimes.tab
1814+
type: table
1815+
results:
1816+
OUTPUT_HTML_FILE:
1817+
name: expected/basic_statistics_time.html
1818+
type: regex
1819+
rules:
1820+
- 'Analyzed layer: custom/datetimes.tab'
1821+
- 'Analyzed field: time'
1822+
- 'Count: 4'
1823+
- 'Unique values: 3'
1824+
- 'Minimum value: 03:29:40'
1825+
- 'Maximum value: 15:29:22'
1826+
- 'NULL \(missing\) values: 1'

‎src/core/qgsdatetimestatisticalsummary.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ void QgsDateTimeStatisticalSummary::reset()
4141
mCountMissing = 0;
4242
mMin = QDateTime();
4343
mMax = QDateTime();
44+
mIsTimes = false;
4445
}
4546

4647
void QgsDateTimeStatisticalSummary::calculate( const QVariantList& values )
@@ -66,6 +67,18 @@ void QgsDateTimeStatisticalSummary::addValue( const QVariant& value )
6667
testDateTime( date.isValid() ? QDateTime( date, QTime( 0, 0, 0 ) )
6768
: QDateTime() );
6869
}
70+
else if ( value.type() == QVariant::Time )
71+
{
72+
mIsTimes = true;
73+
QTime time = value.toTime();
74+
testDateTime( time.isValid() ? QDateTime( QDate::fromJulianDay( 0 ), time )
75+
: QDateTime() );
76+
}
77+
else //not a date
78+
{
79+
mCountMissing++;
80+
mCount++;
81+
}
6982
// QTime?
7083
}
7184

@@ -121,11 +134,11 @@ QVariant QgsDateTimeStatisticalSummary::statistic( QgsDateTimeStatisticalSummary
121134
case CountMissing:
122135
return mCountMissing;
123136
case Min:
124-
return mMin;
137+
return mIsTimes ? QVariant( mMin.time() ) : QVariant( mMin );
125138
case Max:
126-
return mMax;
139+
return mIsTimes ? QVariant( mMax.time() ) : QVariant( mMax );
127140
case Range:
128-
return QVariant::fromValue( mMax - mMin );
141+
return mIsTimes ? QVariant::fromValue( mMax.time() - mMin.time() ) : QVariant::fromValue( mMax - mMin );
129142
case All:
130143
return 0;
131144
}

‎src/core/qgsdatetimestatisticalsummary.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ class CORE_EXPORT QgsDateTimeStatisticalSummary
154154
int mCountMissing;
155155
QDateTime mMin;
156156
QDateTime mMax;
157+
bool mIsTimes;
157158

158159
void testDateTime( const QDateTime& dateTime );
159160
};

‎src/core/qgsstringstatisticalsummary.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ void QgsStringStatisticalSummary::reset()
4242
mMax.clear();
4343
mMinLength = INT_MAX;
4444
mMaxLength = 0;
45+
mSumLengths = 0;
46+
mMeanLength = 0;
4547
}
4648

4749
void QgsStringStatisticalSummary::calculate( const QStringList& values )
@@ -71,8 +73,7 @@ void QgsStringStatisticalSummary::addValue( const QVariant& value )
7173

7274
void QgsStringStatisticalSummary::finalize()
7375
{
74-
//nothing to do for now - this method has been added for forward compatibility
75-
//if statistics are implemented which require a post-calculation step
76+
mMeanLength = mSumLengths / static_cast< double >( mCount );
7677
}
7778

7879
void QgsStringStatisticalSummary::calculateFromVariants( const QVariantList& values )
@@ -121,6 +122,8 @@ void QgsStringStatisticalSummary::testString( const QString& string )
121122
mMax = string;
122123
}
123124
}
125+
if ( mStatistics & MeanLength )
126+
mSumLengths += string.length();
124127
mMinLength = qMin( mMinLength, string.length() );
125128
mMaxLength = qMax( mMaxLength, string.length() );
126129
}
@@ -143,6 +146,8 @@ QVariant QgsStringStatisticalSummary::statistic( QgsStringStatisticalSummary::St
143146
return mMinLength;
144147
case MaximumLength:
145148
return mMaxLength;
149+
case MeanLength:
150+
return mMeanLength;
146151
case All:
147152
return 0;
148153
}
@@ -167,6 +172,8 @@ QString QgsStringStatisticalSummary::displayName( QgsStringStatisticalSummary::S
167172
return QObject::tr( "Minimum length" );
168173
case MaximumLength:
169174
return QObject::tr( "Maximum length" );
175+
case MeanLength:
176+
return QObject::tr( "Mean length" );
170177
case All:
171178
return QString();
172179
}

‎src/core/qgsstringstatisticalsummary.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ class CORE_EXPORT QgsStringStatisticalSummary
5151
Max = 16, //!< Maximum string value
5252
MinimumLength = 32, //!< Minimum length of string
5353
MaximumLength = 64, //!< Maximum length of string
54-
All = Count | CountDistinct | CountMissing | Min | Max, //! All statistics
54+
MeanLength = 128, //!< Mean length of strings
55+
All = Count | CountDistinct | CountMissing | Min | Max | MinimumLength | MaximumLength | MeanLength, //! All statistics
5556
};
5657
Q_DECLARE_FLAGS( Statistics, Statistic )
5758

@@ -167,6 +168,12 @@ class CORE_EXPORT QgsStringStatisticalSummary
167168
*/
168169
int maxLength() const { return mMaxLength; }
169170

171+
/**
172+
* Returns the mean length of strings.
173+
* @note added in QGIS 3.0
174+
*/
175+
double meanLength() const { return mMeanLength; }
176+
170177
/** Returns the friendly display name for a statistic
171178
* @param statistic statistic to return name for
172179
*/
@@ -183,6 +190,8 @@ class CORE_EXPORT QgsStringStatisticalSummary
183190
QString mMax;
184191
int mMinLength;
185192
int mMaxLength;
193+
long mSumLengths;
194+
double mMeanLength;
186195

187196
void testString( const QString& string );
188197
};

‎tests/src/python/test_qgsdatetimestatisticalsummary.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
import qgis # NOQA
1616

1717
from qgis.core import (QgsDateTimeStatisticalSummary,
18-
QgsInterval
18+
QgsInterval,
19+
NULL
1920
)
2021
from qgis.PyQt.QtCore import QDateTime, QDate, QTime
2122
from qgis.testing import unittest
@@ -122,13 +123,13 @@ def testVariantStats(self):
122123
QDateTime(QDate(1998, 1, 2), QTime(1, 10, 54)),
123124
QDateTime(),
124125
QDateTime(QDate(2011, 1, 5), QTime(11, 10, 54))])
125-
self.assertEqual(s.count(), 7)
126+
self.assertEqual(s.count(), 9)
126127
self.assertEqual(set(s.distinctValues()), set([QDateTime(QDate(2015, 3, 4), QTime(11, 10, 54)),
127128
QDateTime(QDate(2019, 12, 28), QTime(23, 10, 1)),
128129
QDateTime(QDate(1998, 1, 2), QTime(1, 10, 54)),
129130
QDateTime(QDate(2011, 1, 5), QTime(11, 10, 54)),
130131
QDateTime()]))
131-
self.assertEqual(s.countMissing(), 2)
132+
self.assertEqual(s.countMissing(), 4)
132133
self.assertEqual(s.min(), QDateTime(QDate(1998, 1, 2), QTime(1, 10, 54)))
133134
self.assertEqual(s.max(), QDateTime(QDate(2019, 12, 28), QTime(23, 10, 1)))
134135
self.assertEqual(s.range(), QgsInterval(693871147))
@@ -156,6 +157,32 @@ def testDates(self):
156157
self.assertEqual(s.max(), QDateTime(QDate(2019, 12, 28), QTime()))
157158
self.assertEqual(s.range(), QgsInterval(693792000))
158159

160+
def testTimes(self):
161+
""" test with time values """
162+
s = QgsDateTimeStatisticalSummary()
163+
self.assertEqual(s.statistics(), QgsDateTimeStatisticalSummary.All)
164+
s.calculate([QTime(11, 3, 4),
165+
QTime(15, 3, 4),
166+
QTime(19, 12, 28),
167+
QTime(),
168+
QTime(8, 1, 2),
169+
QTime(),
170+
QTime(19, 12, 28)])
171+
self.assertEqual(s.count(), 7)
172+
self.assertEqual(s.countDistinct(), 5)
173+
self.assertEqual(s.countMissing(), 2)
174+
self.assertEqual(s.min().time(), QTime(8, 1, 2))
175+
self.assertEqual(s.max().time(), QTime(19, 12, 28))
176+
self.assertEqual(s.statistic(QgsDateTimeStatisticalSummary.Min), QTime(8, 1, 2))
177+
self.assertEqual(s.statistic(QgsDateTimeStatisticalSummary.Max), QTime(19, 12, 28))
178+
self.assertEqual(s.range(), QgsInterval(40286))
179+
180+
def testMissing(self):
181+
s = QgsDateTimeStatisticalSummary()
182+
s.calculate([NULL,
183+
'not a date'])
184+
self.assertEqual(s.countMissing(), 2)
185+
159186

160187
if __name__ == '__main__':
161188
unittest.main()

‎tests/src/python/test_qgsstringstatisticalsummary.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def testStats(self):
4848
self.assertEqual(s2.minLength(), 0)
4949
self.assertEqual(s.maxLength(), 8)
5050
self.assertEqual(s2.maxLength(), 8)
51+
self.assertEqual(s.meanLength(), 3.33333333333333333333333)
52+
self.assertEqual(s2.meanLength(), 3.33333333333333333333333)
5153

5254
#extra check for minLength without empty strings
5355
s.calculate(['1111111', '111', '11111'])
@@ -63,6 +65,7 @@ def testIndividualStats(self):
6365
{'stat': QgsStringStatisticalSummary.Max, 'expected': 'eeee'},
6466
{'stat': QgsStringStatisticalSummary.MinimumLength, 'expected': 0},
6567
{'stat': QgsStringStatisticalSummary.MaximumLength, 'expected': 8},
68+
{'stat': QgsStringStatisticalSummary.MeanLength, 'expected': 3.3333333333333335},
6669
]
6770

6871
s = QgsStringStatisticalSummary()

0 commit comments

Comments
 (0)
Please sign in to comment.