4

I would like to calculate the percentage of each feature (i.e. each row in "HISTO_1") over the sum of that feature's field ("HISTO_1") as a new column (i.e. "Percent_HISTO_1") in an attribute table by creating a function using PyQGIS but I can't seems to do so.

I can do it easily using the expression in the field calculator as follow:

enter image description here

Result field:

enter image description here

Here is the function that I tried to create following this guide: https://docs.qgis.org/3.28/en/docs/user_manual/expressions/expression.html#function-editor

from qgis.core import *
from qgis.gui import *

@qgsfunction(args='auto', group='Custom')
def percent_feature(field1, feature, parent):
    
    totalSum = aggregate(QgsAggregateCalculator.Sum, field1)
    
    percent = (field1/totalSum)*int(100)
    
    return percent

I can't get it to work with the above function. Could someone please help me with this :)

2
  • Do you want to calculate all fields percentage as new columns?
    – BERA
    Apr 17, 2023 at 11:04
  • @BERA Yup this is what I wanted ;)
    – Toboggan
    Apr 17, 2023 at 11:08

1 Answer 1

6

A couple of problems. Firstly, the aggregate method you are using is method of the QgsVectorLayer class, so you need to call it on a QgsVectorLayer object. Then, the second argument needs to be a field name string, however the field1 object is the value in the field you pass to your custom function, evaluated for each feature.

Try the following:

from qgis.core import *
from qgis.gui import *

@qgsfunction(args='auto', group='Custom', referenced_columns=[])
def feature_percent(field1, field_name, feature, parent, context):
    layer_id = context.variable('layer_id')
    layer = QgsProject.instance().mapLayer(layer_id)
    totalSum = layer.aggregate(QgsAggregateCalculator.Sum, field_name)[0]
    percent = (field1/totalSum)*100
    return int(percent)

If you want decimal places, change the last line e.g.

return round(percent, 2)

The first argument of this custom function is the field (double quoted) which will be evaluated for each feature. The second argument is the name of the field as a string (single quoted) which will be passed to the aggregate method.

Example usage:

enter image description here

Result:

enter image description here

Honestly, I don't really see a huge advantage of a custom function over the field calculator expression. Perhaps you would prefer a small script which you could run in the Python console which will add and fill all your percentage fields in one go.

Try:

layer = QgsProject.instance().mapLayersByName('canopyChangeDistrict_2018-2022')[0]

field_names = ['HISTO_0',
                'HISTO_1',
                'HISTO_2',
                'HISTO_3',
                'HISTO_4',
                'HISTO_5']

atts_map = {}
                
for fname in field_names:
    new_field = QgsField(f'Percent_{fname}', QVariant.Double, len=5, prec=2)
    layer.dataProvider().addAttributes([new_field])
    layer.updateFields()
    
for ft in layer.getFeatures():
    atts = {}
    for fname in field_names:
        fld_idx = layer.fields().lookupField(f'Percent_{fname}')
        total_sum = layer.aggregate(QgsAggregateCalculator.Sum, fname)[0]
        pcnt = (ft[fname]/total_sum)*100
        atts[fld_idx] = pcnt
    atts_map[ft.id()] = atts
    
#print(atts_map)
layer.dataProvider().changeAttributeValues(atts_map)

Test result:

enter image description here

6
  • Hi @Ben W, Thank you for your answer, I have tried to create the function from your code but I am running into an error: "Eval Error: int() can't convert non-string with explicit base" Do you know how I can fix this?
    – Toboggan
    Apr 17, 2023 at 13:03
  • @Toboggan, the int() method converts a float or a string to an integer. That error message suggests that it is being passed a value that is neither. I am not sure why if your HISTO_0 field is numeric and contains all integers. Type the error into a search engine for more info. I tested the custom function with zeroes/NULLs etc. it worked fine for me. Try just returning percent or a rounded percent. It is late here I am going to bed. I can have another look tomorrow if you are still having trouble.
    – Ben W
    Apr 17, 2023 at 13:36
  • Ahaa I tried to return the percent object and it works ! Thanks so much for your help man !
    – Toboggan
    Apr 17, 2023 at 13:44
  • @Toboggan, I added a Python script which will add and fill all your percent fields at once.
    – Ben W
    Apr 18, 2023 at 3:32
  • Awesome thanks so much for all your help Ben ! I really appreciate it
    – Toboggan
    Apr 18, 2023 at 13:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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