6

I'm using QGIS 3.2. I have 4 fields ("kk1"-"kk4") where at least one of the fields contains a number (real). I want to calculate the mean of these 1-4 fields and save the result (the average) in another field called "mkviskrans". I calculate the average (based on this answer) of these 1-4 fields using this function in the Function Editor window in the Layer Properties:

@qgsfunction(args=-1, group='Custom')
def mean_col(cols, feature, parent):
  vals = [feature[i] for i in cols if feature[i] is not None]
  return sum(vals)/float(len(vals))

and calling the function from the Expression window in the widget's Default value option in Layer Properties:

mean_col('kk1','kk2','kk3','kk4')

UPDATE: I tried return 2.3instead of return sum(vals)/float(len(vals)). In this case the attribute table is updated automatically with the value 2.3. This indicates that the values typed in by the user in "kk1"-"kk4" actually have to be updated/evaluated/"commited" before returning the mean value, i.e.the division-expression will always return "None"? I have tried to update the feature/field values of "kk1"-"kk4" before return, but without any luck so far.

enter image description here However, the mean value does not automatically appear in the "mkviskrans" "Text Edit" widget (alias "Midlere kvistkransavstand (dm)"): enter image description here Nor in the attribute table: enter image description here When I click in a cell in the attribute table, then the mean value appears: enter image description here Is it possible to get/show/save the value "on the fly"? I have tried different variants of update and/or commit within the function:

@qgsfunction(args=-1, group='Custom')
def mean_col(cols, feature, parent):
  layer = qgis.utils.iface.activeLayer()
  fieldIndex = layer.fields().indexFromName( 'mkviskrans' )
  vals = [feature[i] for i in cols if feature[i] is not None]
  """return sum(vals)/float(len(vals))"""
  mv = sum(vals)/float(len(vals))
  layer.changeAttributeValue(feature.id(), fieldIndex, mv)
  layer.commitChanges()

If I save the edits without clicking in a cell in the attribute table, the average (the field "mkvistkrans") will not contain any mean values.

(I may of course calculate the mean values after the field work, but I want the user to see the mean value "on the fly" - if possible.)

1

NULL and None

Fun fact: NULL is not None

With this in mind it's easy to fix the code

@qgsfunction(args=-1, group='Custom')
def mean_col(cols, feature, parent):
  vals = [feature[i] for i in cols if feature[i] != NULL]
  return sum(vals)/float(len(vals))

Do not write to layers in expressions

Additional advice: never write to layers in expressions. Expressions are often executed in threads and in different contexts and using layer.changeAttributeValue() and similar functions may result in undefined behavior (read: crash).

Using values instead of attribute names

Another interesting experiment for educational purpose.

Instead of providing column names to the function, you can also provide values.

@qgsfunction(args='auto', group='Custom')
def mean_value(vals, feature, parent):
    vals = [val for val in vals if val != NULL]
    return sum(vals)/float(len(vals))

And then call it with

mean_value("kk1", "kk2", "kk3", "kk4")

The interesting result is: it will return NULL as soon as at least one column is NULL. How comes? QGIS assumes the result for an expression function is NULL as soon as at least one parameter is NULL for performance reasons. It's possible to control this behavior, but unfortunately the @qgsfunction decorator does not support this to date (QGIS 3.2).

To workaround this behavior we can either provide a full-blown QgsExpressionFunction implementation which is out of scope for this answer. Or just take a little detour and use an array instead.

@qgsfunction(args='auto', group='Custom')
def mean_value(vals, feature, parent):
    vals = [val for val in vals if val != NULL]
    return sum(vals)/float(len(vals))

and then call it with

mean_value(array("kk1", "kk2", "kk3", "kk4"))
  • @Matthias_Kuhn, thank you very much for the thorough answer. I've now tried all your solutions. However, the field "mkviskrans" is still not auomatically filled out. I still have to click in a cell in the attribute table (or revisit the point with the Identify-tool and click ok) to see the mean value. – 9ls1 Jun 27 '18 at 7:38
  • Not easily, the attribute table (so far) does not take dependencies on virtual fields into account. But it would be a nice enhancement to QGIS itself. – Matthias Kuhn Jun 27 '18 at 8:48
  • @Matthias_Kuhn: Virtual fields? The fields "kk1"-"kk4" and "mkviskrans" (in which I want the mean value to be automatically saved) are all predefined fields (existing columns) in the point shapefile. They are just initially empty (since I start with an empty shapefile (created with ogr/python). Then the user fill in the attribute values ("kk1"-"kk4" are measured distances) for one point (a tree) at the time. Is the problem that the attributes don't actually have values before OK is clicked and the values typed into "kk1"-"kk4" are saved/commited? – 9ls1 Jun 27 '18 at 9:51
  • Oh, sorry, I see what you are doing there now with auto update default values. The effect is similar as virtual fields, just that the data is saved. The requirement would be the same as for virtual fields, some kind of dependency handling in the attribute table (i.e. the logic "if any of the fields used to calculate the default value changed, the table should check if the value changed"). Would be welcome as an improvement to QGIS. – Matthias Kuhn Jun 28 '18 at 13:20
  • OK, to conclude: It is not possible to do it instantly in current QGIS versions < 3.3, but perhaps the possibility will be included in the future. – 9ls1 Jun 28 '18 at 15:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.