PyQGIS – Function to Calculate Percentage of Each Feature Over the Sum of That Feature’s Field

pyqgisqgis-3

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 🙂

Best Answer

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

Related Question