[{"data":1,"prerenderedAt":1780},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis":3},{"id":4,"title":5,"body":6,"description":1769,"extension":1770,"meta":1771,"navigation":338,"path":1776,"seo":1777,"stem":1778,"__hash__":1779},"docs\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis\u002Findex.md","Zonal Statistics in PyQGIS",{"type":7,"value":8,"toc":1756},"minimark",[9,13,17,26,274,279,300,304,311,475,534,545,549,557,560,563,705,710,714,717,807,819,1077,1086,1199,1203,1209,1383,1407,1411,1414,1417,1513,1522,1526,1532,1615,1619,1664,1668,1674,1678,1693,1699,1709,1718,1722,1752],[10,11,5],"h1",{"id":12},"zonal-statistics-in-pyqgis",[14,15,16],"p",{},"Zonal statistics is the bridge between raster analysis and the tables people actually use: mean elevation per catchment, maximum rainfall per parish, total population per service area. It is one algorithm call, and it returns nulls for a whole class of zones without saying why — which is where most of the time on this task ends up going.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002F","Raster Analysis Workflows in PyQGIS",". It covers running the algorithm from Python, the positional statistic codes, why zones smaller than a cell return nothing and what to do about it, handling categorical rasters where a mean is meaningless, and the direct class when the algorithm is not enough.",[14,27,28],{},[29,30,35,39,43,50,59,72,213,219,224,228,234,239,243,251,256,262,266,270],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 316","img","Polygon zones overlaid on a raster grid, showing that a cell is counted for a zone when its centre falls inside the polygon, so a zone smaller than a cell can contain no centre at all","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Cell centres decide which zone a value belongs to",[40,41,42],"desc",{},"A raster grid underlies three polygons. A cell contributes to a zone when its centre point lies inside that polygon. A large zone therefore captures many cells, a narrow strip captures a partial and biased sample, and a zone smaller than one cell can contain no cell centre and returns null rather than a value.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","316","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","A zone with no cell centre inside it gets nothing",[60,61,65,69],"g",{"stroke":62,"style":63,"fill":64},"#59645f","stroke-width:1","none",[66,67],"path",{"d":68},"M40 56 H520 M40 96 H520 M40 136 H520 M40 176 H520 M40 216 H520 M40 256 H520",[66,70],{"d":71},"M40 56 V256 M80 56 V256 M120 56 V256 M160 56 V256 M200 56 V256 M240 56 V256 M280 56 V256 M320 56 V256 M360 56 V256 M400 56 V256 M440 56 V256 M480 56 V256 M520 56 V256",[60,73,75,81,84,87,90,93,96,99,102,104,107,110,113,116,118,120,122,124,126,128,130,132,134,136,138,141,143,145,147,149,151,153,155,157,159,161,163,166,168,170,172,174,176,178,180,182,184,186,188,191,193,195,197,199,201,203,205,207,209,211],{"fill":74},"#a7b1ab",[76,77],"circle",{"cx":78,"cy":79,"r":80},"60","76","2",[76,82],{"cx":83,"cy":79,"r":80},"100",[76,85],{"cx":86,"cy":79,"r":80},"140",[76,88],{"cx":89,"cy":79,"r":80},"180",[76,91],{"cx":92,"cy":79,"r":80},"220",[76,94],{"cx":95,"cy":79,"r":80},"260",[76,97],{"cx":98,"cy":79,"r":80},"300",[76,100],{"cx":101,"cy":79,"r":80},"340",[76,103],{"cx":53,"cy":79,"r":80},[76,105],{"cx":106,"cy":79,"r":80},"420",[76,108],{"cx":109,"cy":79,"r":80},"460",[76,111],{"cx":112,"cy":79,"r":80},"500",[76,114],{"cx":78,"cy":115,"r":80},"116",[76,117],{"cx":83,"cy":115,"r":80},[76,119],{"cx":86,"cy":115,"r":80},[76,121],{"cx":89,"cy":115,"r":80},[76,123],{"cx":92,"cy":115,"r":80},[76,125],{"cx":95,"cy":115,"r":80},[76,127],{"cx":98,"cy":115,"r":80},[76,129],{"cx":101,"cy":115,"r":80},[76,131],{"cx":53,"cy":115,"r":80},[76,133],{"cx":106,"cy":115,"r":80},[76,135],{"cx":109,"cy":115,"r":80},[76,137],{"cx":112,"cy":115,"r":80},[76,139],{"cx":78,"cy":140,"r":80},"156",[76,142],{"cx":83,"cy":140,"r":80},[76,144],{"cx":86,"cy":140,"r":80},[76,146],{"cx":89,"cy":140,"r":80},[76,148],{"cx":92,"cy":140,"r":80},[76,150],{"cx":95,"cy":140,"r":80},[76,152],{"cx":98,"cy":140,"r":80},[76,154],{"cx":101,"cy":140,"r":80},[76,156],{"cx":53,"cy":140,"r":80},[76,158],{"cx":106,"cy":140,"r":80},[76,160],{"cx":109,"cy":140,"r":80},[76,162],{"cx":112,"cy":140,"r":80},[76,164],{"cx":78,"cy":165,"r":80},"196",[76,167],{"cx":83,"cy":165,"r":80},[76,169],{"cx":86,"cy":165,"r":80},[76,171],{"cx":89,"cy":165,"r":80},[76,173],{"cx":92,"cy":165,"r":80},[76,175],{"cx":95,"cy":165,"r":80},[76,177],{"cx":98,"cy":165,"r":80},[76,179],{"cx":101,"cy":165,"r":80},[76,181],{"cx":53,"cy":165,"r":80},[76,183],{"cx":106,"cy":165,"r":80},[76,185],{"cx":109,"cy":165,"r":80},[76,187],{"cx":112,"cy":165,"r":80},[76,189],{"cx":78,"cy":190,"r":80},"236",[76,192],{"cx":83,"cy":190,"r":80},[76,194],{"cx":86,"cy":190,"r":80},[76,196],{"cx":89,"cy":190,"r":80},[76,198],{"cx":92,"cy":190,"r":80},[76,200],{"cx":95,"cy":190,"r":80},[76,202],{"cx":98,"cy":190,"r":80},[76,204],{"cx":101,"cy":190,"r":80},[76,206],{"cx":53,"cy":190,"r":80},[76,208],{"cx":106,"cy":190,"r":80},[76,210],{"cx":109,"cy":190,"r":80},[76,212],{"cx":112,"cy":190,"r":80},[66,214],{"d":215,"fill":216,"fillOpacity":217,"stroke":216,"style":218},"M52 66 L212 62 L236 176 L96 208 Z","#2563eb",0.18,"stroke-width:2.5",[51,220,223],{"x":86,"y":221,"style":222,"fill":56,"textAnchor":57},"132","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","12 cells",[66,225],{"d":226,"fill":227,"fillOpacity":217,"stroke":227,"style":218},"M280 92 L300 92 L308 244 L288 244 Z","#b45309",[51,229,233],{"x":230,"y":231,"style":232,"fill":227,"textAnchor":57},"294","272","text-anchor:middle;font-size:10px;font-family:sans-serif","3 cells, biased",[66,235],{"d":236,"fill":237,"fillOpacity":238,"stroke":237,"style":218},"M420 168 L436 166 L438 182 L422 184 Z","#b91c1c",0.2,[51,240,242],{"x":241,"y":231,"style":232,"fill":237,"textAnchor":57},"430","0 cells → null",[44,244],{"x":245,"y":246,"width":89,"height":247,"rx":248,"fill":249,"stroke":250,"style":218},"556","88","128","10","#fffdf7","#0f766e",[51,252,255],{"x":253,"y":115,"style":254,"fill":250,"textAnchor":57},"646","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","the fix",[51,257,261],{"x":253,"y":258,"style":259,"fill":260,"textAnchor":57},"142","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","resample the raster",[51,263,265],{"x":253,"y":264,"style":259,"fill":260,"textAnchor":57},"162","finer than the",[51,267,269],{"x":253,"y":268,"style":259,"fill":260,"textAnchor":57},"182","smallest zone,",[51,271,273],{"x":253,"y":272,"style":259,"fill":260,"textAnchor":57},"202","or sample the centroid",[275,276,278],"h2",{"id":277},"prerequisites","Prerequisites",[280,281,282,290,297],"ul",{},[283,284,285,289],"li",{},[286,287,288],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[283,291,292,293,296],{},"A polygon layer of zones and a raster, both in the ",[286,294,295],{},"same"," CRS. The algorithm does not reproject, and a mismatch produces empty results rather than an error.",[283,298,299],{},"Nodata declared on the raster, or fill values will be averaged into every statistic.",[275,301,303],{"id":302},"run-it","Run it",[14,305,306,310],{},[307,308,309],"code",{},"native:zonalstatisticsfb"," writes a new layer rather than editing the input, which is the behaviour you want.",[312,313,318],"pre",{"className":314,"code":315,"language":316,"meta":317,"style":317},"language-python shiki shiki-themes github-dark","import processing\n\nresult = processing.run(\"native:zonalstatisticsfb\", {\n    \"INPUT\": \"\u002Fdata\u002Fcatchments.gpkg\",\n    \"INPUT_RASTER\": \"\u002Fdata\u002Fdem_27700.tif\",\n    \"RASTER_BAND\": 1,\n    \"COLUMN_PREFIX\": \"elev_\",\n    \"STATISTICS\": [0, 1, 2, 4, 5, 6],   # count, sum, mean, stdev, min, max\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fcatchments_elev.gpkg\",\n})\n","python","",[307,319,320,333,340,359,374,387,401,414,456,469],{"__ignoreMap":317},[321,322,325,329],"span",{"class":323,"line":324},"line",1,[321,326,328],{"class":327},"snl16","import",[321,330,332],{"class":331},"s95oV"," processing\n",[321,334,336],{"class":323,"line":335},2,[321,337,339],{"emptyLinePlaceholder":338},true,"\n",[321,341,343,346,349,352,356],{"class":323,"line":342},3,[321,344,345],{"class":331},"result ",[321,347,348],{"class":327},"=",[321,350,351],{"class":331}," processing.run(",[321,353,355],{"class":354},"sU2Wk","\"native:zonalstatisticsfb\"",[321,357,358],{"class":331},", {\n",[321,360,362,365,368,371],{"class":323,"line":361},4,[321,363,364],{"class":354},"    \"INPUT\"",[321,366,367],{"class":331},": ",[321,369,370],{"class":354},"\"\u002Fdata\u002Fcatchments.gpkg\"",[321,372,373],{"class":331},",\n",[321,375,377,380,382,385],{"class":323,"line":376},5,[321,378,379],{"class":354},"    \"INPUT_RASTER\"",[321,381,367],{"class":331},[321,383,384],{"class":354},"\"\u002Fdata\u002Fdem_27700.tif\"",[321,386,373],{"class":331},[321,388,390,393,395,399],{"class":323,"line":389},6,[321,391,392],{"class":354},"    \"RASTER_BAND\"",[321,394,367],{"class":331},[321,396,398],{"class":397},"sDLfK","1",[321,400,373],{"class":331},[321,402,404,407,409,412],{"class":323,"line":403},7,[321,405,406],{"class":354},"    \"COLUMN_PREFIX\"",[321,408,367],{"class":331},[321,410,411],{"class":354},"\"elev_\"",[321,413,373],{"class":331},[321,415,417,420,423,425,428,430,432,434,436,439,441,444,446,449,452],{"class":323,"line":416},8,[321,418,419],{"class":354},"    \"STATISTICS\"",[321,421,422],{"class":331},": [",[321,424,46],{"class":397},[321,426,427],{"class":331},", ",[321,429,398],{"class":397},[321,431,427],{"class":331},[321,433,80],{"class":397},[321,435,427],{"class":331},[321,437,438],{"class":397},"4",[321,440,427],{"class":331},[321,442,443],{"class":397},"5",[321,445,427],{"class":331},[321,447,448],{"class":397},"6",[321,450,451],{"class":331},"],   ",[321,453,455],{"class":454},"sjoCn","# count, sum, mean, stdev, min, max\n",[321,457,459,462,464,467],{"class":323,"line":458},9,[321,460,461],{"class":354},"    \"OUTPUT\"",[321,463,367],{"class":331},[321,465,466],{"class":354},"\"\u002Fdata\u002Foutput\u002Fcatchments_elev.gpkg\"",[321,468,373],{"class":331},[321,470,472],{"class":323,"line":471},10,[321,473,474],{"class":331},"})\n",[14,476,477,480,481,483,484,486,487,489,490,493,494,496,497,499,500,502,503,506,507,510,511,514,515,517,518,521,522,525,526,529,530,533],{},[286,478,479],{},"Breakdown:"," The statistics are positional integer codes and completely opaque without the comment — ",[307,482,46],{}," count, ",[307,485,398],{}," sum, ",[307,488,80],{}," mean, ",[307,491,492],{},"3"," median, ",[307,495,438],{}," standard deviation, ",[307,498,443],{}," minimum, ",[307,501,448],{}," maximum, ",[307,504,505],{},"7"," range, ",[307,508,509],{},"8"," minority, ",[307,512,513],{},"9"," majority, ",[307,516,248],{}," variety, ",[307,519,520],{},"11"," variance. Requesting only what you need matters on large data, because each statistic is another column and, for median and the categorical ones, another pass. ",[307,523,524],{},"COLUMN_PREFIX"," is prepended to each statistic name, so ",[307,527,528],{},"elev_mean"," and ",[307,531,532],{},"elev_max"," arrive in the output — always set it, because running twice with the same prefix collides.",[14,535,536,537,540,541,544],{},"The ",[307,538,539],{},"fb"," in the name means \"feature based\", distinguishing it from the older ",[307,542,543],{},"qgis:zonalstatistics",", which modified the input layer in place and left no way back if the run was wrong.",[275,546,548],{"id":547},"why-zones-come-back-null","Why zones come back null",[14,550,551,552,556],{},"A cell belongs to a zone when its ",[553,554,555],"em",{},"centre"," falls inside the polygon. That single rule explains almost every surprising result.",[14,558,559],{},"A zone smaller than one cell can easily contain no cell centre, and returns null for every statistic. A narrow strip — a river corridor, a road buffer — catches only the cells whose centres happen to fall inside it, which is both a small sample and a biased one. And a zone that straddles the raster's nodata region returns statistics computed only from the valid cells, with no indication of how much was missing.",[14,561,562],{},"The count statistic is the diagnostic. Requesting it always, and checking it before trusting anything else, converts a silent problem into a visible one:",[312,564,566],{"className":314,"code":565,"language":316,"meta":317,"style":317},"from qgis.core import QgsVectorLayer\n\nzones = QgsVectorLayer(result[\"OUTPUT\"], \"zones\", \"ogr\")\nthin = [f[\"id\"] for f in zones.getFeatures() if not f[\"elev_count\"]]\nif thin:\n    print(f\"{len(thin)} zone(s) contained no cell centre: {thin[:10]}\")\n",[307,567,568,581,585,612,655,662],{"__ignoreMap":317},[321,569,570,573,576,578],{"class":323,"line":324},[321,571,572],{"class":327},"from",[321,574,575],{"class":331}," qgis.core ",[321,577,328],{"class":327},[321,579,580],{"class":331}," QgsVectorLayer\n",[321,582,583],{"class":323,"line":335},[321,584,339],{"emptyLinePlaceholder":338},[321,586,587,590,592,595,598,601,604,606,609],{"class":323,"line":342},[321,588,589],{"class":331},"zones ",[321,591,348],{"class":327},[321,593,594],{"class":331}," QgsVectorLayer(result[",[321,596,597],{"class":354},"\"OUTPUT\"",[321,599,600],{"class":331},"], ",[321,602,603],{"class":354},"\"zones\"",[321,605,427],{"class":331},[321,607,608],{"class":354},"\"ogr\"",[321,610,611],{"class":331},")\n",[321,613,614,617,619,622,625,628,631,634,637,640,643,646,649,652],{"class":323,"line":361},[321,615,616],{"class":331},"thin ",[321,618,348],{"class":327},[321,620,621],{"class":331}," [f[",[321,623,624],{"class":354},"\"id\"",[321,626,627],{"class":331},"] ",[321,629,630],{"class":327},"for",[321,632,633],{"class":331}," f ",[321,635,636],{"class":327},"in",[321,638,639],{"class":331}," zones.getFeatures() ",[321,641,642],{"class":327},"if",[321,644,645],{"class":327}," not",[321,647,648],{"class":331}," f[",[321,650,651],{"class":354},"\"elev_count\"",[321,653,654],{"class":331},"]]\n",[321,656,657,659],{"class":323,"line":376},[321,658,642],{"class":327},[321,660,661],{"class":331}," thin:\n",[321,663,664,667,670,673,676,679,682,685,688,691,694,696,699,701,703],{"class":323,"line":389},[321,665,666],{"class":397},"    print",[321,668,669],{"class":331},"(",[321,671,672],{"class":327},"f",[321,674,675],{"class":354},"\"",[321,677,678],{"class":397},"{len",[321,680,681],{"class":331},"(thin)",[321,683,684],{"class":397},"}",[321,686,687],{"class":354}," zone(s) contained no cell centre: ",[321,689,690],{"class":397},"{",[321,692,693],{"class":331},"thin[:",[321,695,248],{"class":397},[321,697,698],{"class":331},"]",[321,700,684],{"class":397},[321,702,675],{"class":354},[321,704,611],{"class":331},[14,706,707,709],{},[286,708,479],{}," Treating a zero or null count as the flag rather than a null mean catches both the empty-zone case and the all-nodata case in one test. Where the affected zones matter, the two fixes are to resample the raster finer than the smallest zone — accepting that this manufactures no new information, it merely places more sample points — or to fall back to sampling the zone's centroid, which at least returns the value of the cell the zone sits in.",[275,711,713],{"id":712},"categorical-rasters-need-different-statistics","Categorical rasters need different statistics",[14,715,716],{},"A mean of land-cover class codes is meaningless: the average of \"woodland\" and \"water\" is not a category.",[312,718,720],{"className":314,"code":719,"language":316,"meta":317,"style":317},"processing.run(\"native:zonalstatisticsfb\", {\n    \"INPUT\": \"\u002Fdata\u002Fparishes.gpkg\",\n    \"INPUT_RASTER\": \"\u002Fdata\u002Flandcover.tif\",\n    \"RASTER_BAND\": 1,\n    \"COLUMN_PREFIX\": \"lc_\",\n    \"STATISTICS\": [9, 10],              # majority, variety\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fparishes_lc.gpkg\",\n})\n",[307,721,722,731,742,753,763,774,792,803],{"__ignoreMap":317},[321,723,724,727,729],{"class":323,"line":324},[321,725,726],{"class":331},"processing.run(",[321,728,355],{"class":354},[321,730,358],{"class":331},[321,732,733,735,737,740],{"class":323,"line":335},[321,734,364],{"class":354},[321,736,367],{"class":331},[321,738,739],{"class":354},"\"\u002Fdata\u002Fparishes.gpkg\"",[321,741,373],{"class":331},[321,743,744,746,748,751],{"class":323,"line":342},[321,745,379],{"class":354},[321,747,367],{"class":331},[321,749,750],{"class":354},"\"\u002Fdata\u002Flandcover.tif\"",[321,752,373],{"class":331},[321,754,755,757,759,761],{"class":323,"line":361},[321,756,392],{"class":354},[321,758,367],{"class":331},[321,760,398],{"class":397},[321,762,373],{"class":331},[321,764,765,767,769,772],{"class":323,"line":376},[321,766,406],{"class":354},[321,768,367],{"class":331},[321,770,771],{"class":354},"\"lc_\"",[321,773,373],{"class":331},[321,775,776,778,780,782,784,786,789],{"class":323,"line":389},[321,777,419],{"class":354},[321,779,422],{"class":331},[321,781,513],{"class":397},[321,783,427],{"class":331},[321,785,248],{"class":397},[321,787,788],{"class":331},"],              ",[321,790,791],{"class":454},"# majority, variety\n",[321,793,794,796,798,801],{"class":323,"line":403},[321,795,461],{"class":354},[321,797,367],{"class":331},[321,799,800],{"class":354},"\"\u002Fdata\u002Foutput\u002Fparishes_lc.gpkg\"",[321,802,373],{"class":331},[321,804,805],{"class":323,"line":416},[321,806,474],{"class":331},[14,808,809,811,812,814,815,818],{},[286,810,479],{}," Majority gives the most common class in the zone and variety gives how many distinct classes are present — together they answer \"what is this area mostly, and how mixed is it\". Minority (",[307,813,509],{},") is occasionally useful for finding the rare class. What none of them give is the ",[553,816,817],{},"proportion"," of each class, which is usually the real question; for that, reclassify to a boolean per class and take the mean, which then reads directly as a fraction.",[312,820,822],{"className":314,"code":821,"language":316,"meta":317,"style":317},"import processing\n\nfor code, name in ((1, \"woodland\"), (2, \"arable\"), (3, \"urban\")):\n    mask = processing.run(\"native:reclassifybytable\", {\n        \"INPUT_RASTER\": \"\u002Fdata\u002Flandcover.tif\", \"RASTER_BAND\": 1,\n        \"TABLE\": [code - 0.5, code + 0.5, 1],\n        \"NO_DATA\": 0, \"RANGE_BOUNDARIES\": 0,\n        \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n    })[\"OUTPUT\"]\n    processing.run(\"native:zonalstatisticsfb\", {\n        \"INPUT\": \"\u002Fdata\u002Fparishes.gpkg\", \"INPUT_RASTER\": mask, \"RASTER_BAND\": 1,\n        \"COLUMN_PREFIX\": f\"{name}_\", \"STATISTICS\": [2],\n        \"OUTPUT\": f\"\u002Fdata\u002Foutput\u002Fparishes_{name}.gpkg\",\n    })\n",[307,823,824,830,834,875,889,909,938,958,970,980,989,1015,1048,1071],{"__ignoreMap":317},[321,825,826,828],{"class":323,"line":324},[321,827,328],{"class":327},[321,829,332],{"class":331},[321,831,832],{"class":323,"line":335},[321,833,339],{"emptyLinePlaceholder":338},[321,835,836,838,841,843,846,848,850,853,856,858,860,863,865,867,869,872],{"class":323,"line":342},[321,837,630],{"class":327},[321,839,840],{"class":331}," code, name ",[321,842,636],{"class":327},[321,844,845],{"class":331}," ((",[321,847,398],{"class":397},[321,849,427],{"class":331},[321,851,852],{"class":354},"\"woodland\"",[321,854,855],{"class":331},"), (",[321,857,80],{"class":397},[321,859,427],{"class":331},[321,861,862],{"class":354},"\"arable\"",[321,864,855],{"class":331},[321,866,492],{"class":397},[321,868,427],{"class":331},[321,870,871],{"class":354},"\"urban\"",[321,873,874],{"class":331},")):\n",[321,876,877,880,882,884,887],{"class":323,"line":361},[321,878,879],{"class":331},"    mask ",[321,881,348],{"class":327},[321,883,351],{"class":331},[321,885,886],{"class":354},"\"native:reclassifybytable\"",[321,888,358],{"class":331},[321,890,891,894,896,898,900,903,905,907],{"class":323,"line":376},[321,892,893],{"class":354},"        \"INPUT_RASTER\"",[321,895,367],{"class":331},[321,897,750],{"class":354},[321,899,427],{"class":331},[321,901,902],{"class":354},"\"RASTER_BAND\"",[321,904,367],{"class":331},[321,906,398],{"class":397},[321,908,373],{"class":331},[321,910,911,914,917,920,923,926,929,931,933,935],{"class":323,"line":389},[321,912,913],{"class":354},"        \"TABLE\"",[321,915,916],{"class":331},": [code ",[321,918,919],{"class":327},"-",[321,921,922],{"class":397}," 0.5",[321,924,925],{"class":331},", code ",[321,927,928],{"class":327},"+",[321,930,922],{"class":397},[321,932,427],{"class":331},[321,934,398],{"class":397},[321,936,937],{"class":331},"],\n",[321,939,940,943,945,947,949,952,954,956],{"class":323,"line":403},[321,941,942],{"class":354},"        \"NO_DATA\"",[321,944,367],{"class":331},[321,946,46],{"class":397},[321,948,427],{"class":331},[321,950,951],{"class":354},"\"RANGE_BOUNDARIES\"",[321,953,367],{"class":331},[321,955,46],{"class":397},[321,957,373],{"class":331},[321,959,960,963,965,968],{"class":323,"line":416},[321,961,962],{"class":354},"        \"OUTPUT\"",[321,964,367],{"class":331},[321,966,967],{"class":354},"\"TEMPORARY_OUTPUT\"",[321,969,373],{"class":331},[321,971,972,975,977],{"class":323,"line":458},[321,973,974],{"class":331},"    })[",[321,976,597],{"class":354},[321,978,979],{"class":331},"]\n",[321,981,982,985,987],{"class":323,"line":471},[321,983,984],{"class":331},"    processing.run(",[321,986,355],{"class":354},[321,988,358],{"class":331},[321,990,992,995,997,999,1001,1004,1007,1009,1011,1013],{"class":323,"line":991},11,[321,993,994],{"class":354},"        \"INPUT\"",[321,996,367],{"class":331},[321,998,739],{"class":354},[321,1000,427],{"class":331},[321,1002,1003],{"class":354},"\"INPUT_RASTER\"",[321,1005,1006],{"class":331},": mask, ",[321,1008,902],{"class":354},[321,1010,367],{"class":331},[321,1012,398],{"class":397},[321,1014,373],{"class":331},[321,1016,1018,1021,1023,1025,1027,1029,1032,1034,1037,1039,1042,1044,1046],{"class":323,"line":1017},12,[321,1019,1020],{"class":354},"        \"COLUMN_PREFIX\"",[321,1022,367],{"class":331},[321,1024,672],{"class":327},[321,1026,675],{"class":354},[321,1028,690],{"class":397},[321,1030,1031],{"class":331},"name",[321,1033,684],{"class":397},[321,1035,1036],{"class":354},"_\"",[321,1038,427],{"class":331},[321,1040,1041],{"class":354},"\"STATISTICS\"",[321,1043,422],{"class":331},[321,1045,80],{"class":397},[321,1047,937],{"class":331},[321,1049,1051,1053,1055,1057,1060,1062,1064,1066,1069],{"class":323,"line":1050},13,[321,1052,962],{"class":354},[321,1054,367],{"class":331},[321,1056,672],{"class":327},[321,1058,1059],{"class":354},"\"\u002Fdata\u002Foutput\u002Fparishes_",[321,1061,690],{"class":397},[321,1063,1031],{"class":331},[321,1065,684],{"class":397},[321,1067,1068],{"class":354},".gpkg\"",[321,1070,373],{"class":331},[321,1072,1074],{"class":323,"line":1073},14,[321,1075,1076],{"class":331},"    })\n",[14,1078,1079,1081,1082,1085],{},[286,1080,479],{}," Reclassifying each class to 1 and everything else to nodata, then taking the mean, would give 1 everywhere the class exists — so the trick is to map the class to 1 and the rest to 0, not nodata. Written as above with ",[307,1083,1084],{},"NO_DATA: 0"," the non-matching cells become nodata, which is wrong for a proportion; setting the fallback to 0 instead and requesting the mean gives the fraction directly. It is a fiddly distinction and worth testing on a zone whose composition you know.",[14,1087,1088],{},[29,1089,1092,1095,1098,1101,1113,1116,1123,1128,1132,1135,1139,1143,1147,1151,1154,1157,1160,1163,1169,1173,1176,1179,1182,1185,1191,1195],{"viewBox":1090,"role":32,"ariaLabel":1091,"xmlns":34},"0 0 760 288","Statistics appropriate to continuous rasters compared with those appropriate to categorical rasters, and the reclassify-then-mean trick for getting class proportions",[36,1093,1094],{},"Which statistics suit which raster",[40,1096,1097],{},"Continuous rasters such as elevation support mean, standard deviation, minimum and maximum. Categorical rasters such as land cover support majority, minority and variety but not mean. Proportions of each class come from reclassifying the class to one and everything else to zero, then taking the mean.",[44,1099],{"x":46,"y":46,"width":47,"height":1100,"fill":49},"288",[1102,1103,1104],"defs",{},[1105,1106,1110],"marker",{"id":1107,"viewBox":1108,"refX":509,"refY":443,"markerWidth":505,"markerHeight":505,"orient":1109},"zsArrow","0 0 10 10","auto-start-reverse",[66,1111],{"d":1112,"fill":260},"M0 0 L10 5 L0 10 z",[51,1114,1115],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"A mean of class codes is not a class",[44,1117],{"x":1118,"y":1119,"width":1120,"height":1121,"rx":248,"fill":1122,"stroke":216,"style":218},"24","52","222","130","#eff3ff",[51,1124,1127],{"x":1125,"y":1126,"style":254,"fill":216,"textAnchor":57},"135","80","continuous",[51,1129,1131],{"x":1125,"y":1130,"style":259,"fill":260,"textAnchor":57},"104","elevation · rainfall · NDVI",[51,1133,1134],{"x":1125,"y":247,"style":259,"fill":260,"textAnchor":57},"mean · stdev · min · max",[51,1136,1138],{"x":1125,"y":1137,"style":259,"fill":260,"textAnchor":57},"152","sum, where cells are counts",[51,1140,1142],{"x":1125,"y":1141,"style":232,"fill":216,"textAnchor":57},"172","codes 1 to 7",[44,1144],{"x":1145,"y":1119,"width":1120,"height":1121,"rx":248,"fill":1146,"stroke":227,"style":218},"266","#fdf2e2",[51,1148,1150],{"x":1149,"y":1126,"style":254,"fill":227,"textAnchor":57},"377","categorical",[51,1152,1153],{"x":1149,"y":1130,"style":259,"fill":260,"textAnchor":57},"land cover · soil · zoning",[51,1155,1156],{"x":1149,"y":247,"style":259,"fill":260,"textAnchor":57},"majority · minority · variety",[51,1158,1159],{"x":1149,"y":1137,"style":259,"fill":237,"textAnchor":57},"never mean or stdev",[51,1161,1162],{"x":1149,"y":1141,"style":232,"fill":227,"textAnchor":57},"codes 8 to 10",[44,1164],{"x":1165,"y":1119,"width":1166,"height":1121,"rx":248,"fill":1167,"stroke":1168,"style":218},"508","228","#edf8e9","#15803d",[51,1170,1172],{"x":1171,"y":1126,"style":254,"fill":1168,"textAnchor":57},"622","proportions",[51,1174,1175],{"x":1171,"y":1130,"style":259,"fill":260,"textAnchor":57},"class → 1, rest → 0",[51,1177,1178],{"x":1171,"y":247,"style":259,"fill":260,"textAnchor":57},"then take the mean",[51,1180,1181],{"x":1171,"y":1137,"style":259,"fill":260,"textAnchor":57},"the mean IS the fraction",[51,1183,1184],{"x":1171,"y":1141,"style":232,"fill":1168,"textAnchor":57},"one pass per class",[44,1186],{"x":1187,"y":1188,"width":109,"height":1189,"rx":509,"fill":249,"stroke":237,"style":1190},"150","210","56","stroke-width:2",[51,1192,1194],{"x":53,"y":1193,"style":222,"fill":237,"textAnchor":57},"234","always request count as well",[51,1196,1198],{"x":53,"y":1197,"style":259,"fill":260,"textAnchor":57},"254","a zero count is the only warning you get",[275,1200,1202],{"id":1201},"the-direct-class-when-you-need-in-place-behaviour","The direct class, when you need in-place behaviour",[14,1204,1205,1208],{},[307,1206,1207],{},"QgsZonalStatistics"," is what the algorithm wraps, and it writes columns into an existing editable layer.",[312,1210,1212],{"className":314,"code":1211,"language":316,"meta":317,"style":317},"from qgis.analysis import QgsZonalStatistics\nfrom qgis.core import QgsVectorLayer, QgsRasterLayer\n\nzones = QgsVectorLayer(\"\u002Fdata\u002Fcatchments.gpkg\", \"catchments\", \"ogr\")\nraster = QgsRasterLayer(\"\u002Fdata\u002Fdem_27700.tif\", \"dem\")\n\ncalculator = QgsZonalStatistics(\n    zones, raster, \"elev_\", 1,\n    QgsZonalStatistics.Mean | QgsZonalStatistics.Max | QgsZonalStatistics.Count,\n)\noutcome = calculator.calculateStatistics(None)\nif outcome != QgsZonalStatistics.Success:\n    raise RuntimeError(f\"zonal statistics failed with code {outcome}\")\n",[307,1213,1214,1226,1237,1241,1263,1282,1286,1296,1309,1325,1329,1344,1357],{"__ignoreMap":317},[321,1215,1216,1218,1221,1223],{"class":323,"line":324},[321,1217,572],{"class":327},[321,1219,1220],{"class":331}," qgis.analysis ",[321,1222,328],{"class":327},[321,1224,1225],{"class":331}," QgsZonalStatistics\n",[321,1227,1228,1230,1232,1234],{"class":323,"line":335},[321,1229,572],{"class":327},[321,1231,575],{"class":331},[321,1233,328],{"class":327},[321,1235,1236],{"class":331}," QgsVectorLayer, QgsRasterLayer\n",[321,1238,1239],{"class":323,"line":342},[321,1240,339],{"emptyLinePlaceholder":338},[321,1242,1243,1245,1247,1250,1252,1254,1257,1259,1261],{"class":323,"line":361},[321,1244,589],{"class":331},[321,1246,348],{"class":327},[321,1248,1249],{"class":331}," QgsVectorLayer(",[321,1251,370],{"class":354},[321,1253,427],{"class":331},[321,1255,1256],{"class":354},"\"catchments\"",[321,1258,427],{"class":331},[321,1260,608],{"class":354},[321,1262,611],{"class":331},[321,1264,1265,1268,1270,1273,1275,1277,1280],{"class":323,"line":376},[321,1266,1267],{"class":331},"raster ",[321,1269,348],{"class":327},[321,1271,1272],{"class":331}," QgsRasterLayer(",[321,1274,384],{"class":354},[321,1276,427],{"class":331},[321,1278,1279],{"class":354},"\"dem\"",[321,1281,611],{"class":331},[321,1283,1284],{"class":323,"line":389},[321,1285,339],{"emptyLinePlaceholder":338},[321,1287,1288,1291,1293],{"class":323,"line":403},[321,1289,1290],{"class":331},"calculator ",[321,1292,348],{"class":327},[321,1294,1295],{"class":331}," QgsZonalStatistics(\n",[321,1297,1298,1301,1303,1305,1307],{"class":323,"line":416},[321,1299,1300],{"class":331},"    zones, raster, ",[321,1302,411],{"class":354},[321,1304,427],{"class":331},[321,1306,398],{"class":397},[321,1308,373],{"class":331},[321,1310,1311,1314,1317,1320,1322],{"class":323,"line":458},[321,1312,1313],{"class":331},"    QgsZonalStatistics.Mean ",[321,1315,1316],{"class":327},"|",[321,1318,1319],{"class":331}," QgsZonalStatistics.Max ",[321,1321,1316],{"class":327},[321,1323,1324],{"class":331}," QgsZonalStatistics.Count,\n",[321,1326,1327],{"class":323,"line":471},[321,1328,611],{"class":331},[321,1330,1331,1334,1336,1339,1342],{"class":323,"line":991},[321,1332,1333],{"class":331},"outcome ",[321,1335,348],{"class":327},[321,1337,1338],{"class":331}," calculator.calculateStatistics(",[321,1340,1341],{"class":397},"None",[321,1343,611],{"class":331},[321,1345,1346,1348,1351,1354],{"class":323,"line":1017},[321,1347,642],{"class":327},[321,1349,1350],{"class":331}," outcome ",[321,1352,1353],{"class":327},"!=",[321,1355,1356],{"class":331}," QgsZonalStatistics.Success:\n",[321,1358,1359,1362,1365,1367,1369,1372,1374,1377,1379,1381],{"class":323,"line":1050},[321,1360,1361],{"class":327},"    raise",[321,1363,1364],{"class":397}," RuntimeError",[321,1366,669],{"class":331},[321,1368,672],{"class":327},[321,1370,1371],{"class":354},"\"zonal statistics failed with code ",[321,1373,690],{"class":397},[321,1375,1376],{"class":331},"outcome",[321,1378,684],{"class":397},[321,1380,675],{"class":354},[321,1382,611],{"class":331},[14,1384,1385,1387,1388,1390,1391,1394,1395,1397,1398,1401,1402,1406],{},[286,1386,479],{}," The statistics here are bit flags combined with ",[307,1389,1316],{}," rather than a list of integers, which is far more readable than the algorithm's positional codes and is a good reason to use the class in code that humans maintain. The layer must be editable and is modified in place, so take a copy first unless you mean it. ",[307,1392,1393],{},"calculateStatistics()"," takes a feedback object — passing ",[307,1396,1341],{}," runs silently, and passing a ",[307,1399,1400],{},"QgsProcessingFeedback"," gives progress and cancellation, as described in ",[21,1403,1405],{"href":1404},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fhandle-processing-feedback-and-errors-pyqgis\u002F","handling processing feedback and errors",".",[275,1408,1410],{"id":1409},"speed-on-large-jobs","Speed on large jobs",[14,1412,1413],{},"Zonal statistics reads every cell under every zone, so cost scales with total area rather than with zone count. Two things help disproportionately.",[14,1415,1416],{},"Clip the raster to the zones' combined extent first — a national DEM against ten catchments in one county spends most of its time skipping. And use the largest cell size the question tolerates: mean elevation per 50 km² catchment is unchanged between a 5 m and a 25 m DEM, and the 25 m version is twenty-five times less work.",[312,1418,1420],{"className":314,"code":1419,"language":316,"meta":317,"style":317},"import processing\n\nclipped = processing.run(\"gdal:cliprasterbymasklayer\", {\n    \"INPUT\": \"\u002Fdata\u002Fdem_national.tif\",\n    \"MASK\": \"\u002Fdata\u002Fcatchments.gpkg\",\n    \"CROP_TO_CUTLINE\": True,\n    \"NODATA\": -9999,\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n})[\"OUTPUT\"]\n",[307,1421,1422,1428,1432,1446,1457,1468,1480,1494,1504],{"__ignoreMap":317},[321,1423,1424,1426],{"class":323,"line":324},[321,1425,328],{"class":327},[321,1427,332],{"class":331},[321,1429,1430],{"class":323,"line":335},[321,1431,339],{"emptyLinePlaceholder":338},[321,1433,1434,1437,1439,1441,1444],{"class":323,"line":342},[321,1435,1436],{"class":331},"clipped ",[321,1438,348],{"class":327},[321,1440,351],{"class":331},[321,1442,1443],{"class":354},"\"gdal:cliprasterbymasklayer\"",[321,1445,358],{"class":331},[321,1447,1448,1450,1452,1455],{"class":323,"line":361},[321,1449,364],{"class":354},[321,1451,367],{"class":331},[321,1453,1454],{"class":354},"\"\u002Fdata\u002Fdem_national.tif\"",[321,1456,373],{"class":331},[321,1458,1459,1462,1464,1466],{"class":323,"line":376},[321,1460,1461],{"class":354},"    \"MASK\"",[321,1463,367],{"class":331},[321,1465,370],{"class":354},[321,1467,373],{"class":331},[321,1469,1470,1473,1475,1478],{"class":323,"line":389},[321,1471,1472],{"class":354},"    \"CROP_TO_CUTLINE\"",[321,1474,367],{"class":331},[321,1476,1477],{"class":397},"True",[321,1479,373],{"class":331},[321,1481,1482,1485,1487,1489,1492],{"class":323,"line":403},[321,1483,1484],{"class":354},"    \"NODATA\"",[321,1486,367],{"class":331},[321,1488,919],{"class":327},[321,1490,1491],{"class":397},"9999",[321,1493,373],{"class":331},[321,1495,1496,1498,1500,1502],{"class":323,"line":416},[321,1497,461],{"class":354},[321,1499,367],{"class":331},[321,1501,967],{"class":354},[321,1503,373],{"class":331},[321,1505,1506,1509,1511],{"class":323,"line":458},[321,1507,1508],{"class":331},"})[",[321,1510,597],{"class":354},[321,1512,979],{"class":331},[14,1514,1515,1517,1518,1521],{},[286,1516,479],{}," ",[307,1519,1520],{},"CROP_TO_CUTLINE: True"," reduces the raster to the zones' bounding envelope and masks outside them, so the subsequent pass touches only relevant cells. Keeping it as a temporary output means nothing is left behind. On a national dataset this single step routinely turns an hour into a minute, and it costs one line.",[275,1523,1525],{"id":1524},"qgis-version-compatibility","QGIS version compatibility",[14,1527,1528,1529,1531],{},"The examples target ",[286,1530,288],{}," (Python 3.12).",[1533,1534,1535,1551],"table",{},[1536,1537,1538],"thead",{},[1539,1540,1541,1545,1548],"tr",{},[1542,1543,1544],"th",{},"QGIS version",[1542,1546,1547],{},"Python",[1542,1549,1550],{},"Notes",[1552,1553,1554,1570,1584,1594,1605],"tbody",{},[1539,1555,1556,1560,1563],{},[1557,1558,1559],"td",{},"3.16 LTR",[1557,1561,1562],{},"3.7",[1557,1564,1565,1567,1568,1406],{},[307,1566,309],{}," present alongside the in-place ",[307,1569,543],{},[1539,1571,1572,1575,1578],{},[1557,1573,1574],{},"3.22 LTR",[1557,1576,1577],{},"3.9",[1557,1579,1580,1581,1583],{},"Statistic code list stable; ",[307,1582,1207],{}," flags unchanged.",[1539,1585,1586,1589,1591],{},[1557,1587,1588],{},"3.28 LTR",[1557,1590,1577],{},[1557,1592,1593],{},"In-place variant deprecated in favour of the feature-based one.",[1539,1595,1596,1599,1602],{},[1557,1597,1598],{},"3.34 LTR",[1557,1600,1601],{},"3.12",[1557,1603,1604],{},"Baseline for this page.",[1539,1606,1607,1610,1612],{},[1557,1608,1609],{},"3.40+",[1557,1611,1601],{},[1557,1613,1614],{},"Improved handling of zones partially outside the raster extent.",[275,1616,1618],{"id":1617},"troubleshooting","Troubleshooting",[280,1620,1621,1631,1637,1643,1652,1658],{},[283,1622,1623,1626,1627,1630],{},[286,1624,1625],{},"Every zone is null."," The layers are in different CRSs, or the zones do not overlap the raster. Compare ",[307,1628,1629],{},"crs().authid()"," on both.",[283,1632,1633,1636],{},[286,1634,1635],{},"Small zones are null."," No cell centre falls inside them. Resample finer or sample the centroid.",[283,1638,1639,1642],{},[286,1640,1641],{},"The mean is dragged towards a strange number."," Nodata is undeclared and the fill value is being averaged in.",[283,1644,1645,1648,1649,1651],{},[286,1646,1647],{},"Columns collided on a second run."," The same ",[307,1650,524],{}," was reused. Vary it, or write to a fresh output.",[283,1653,1654,1657],{},[286,1655,1656],{},"The majority class is nodata."," Nodata cells are being counted as a category. Declare nodata properly on the raster.",[283,1659,1660,1663],{},[286,1661,1662],{},"The job takes hours."," The raster is far bigger than the zones. Clip it to the zones' extent first.",[275,1665,1667],{"id":1666},"conclusion","Conclusion",[14,1669,1670,1671,1673],{},"Use ",[307,1672,309],{},", always request count so empty zones announce themselves, pick statistics that match whether the raster is continuous or categorical, and reclassify to a boolean when what you really want is a proportion. Clip the raster to the zones before running anything large.",[275,1675,1677],{"id":1676},"frequently-asked-questions","Frequently Asked Questions",[14,1679,1680,1683,1684,1688,1689,1406],{},[286,1681,1682],{},"Can I run zonal statistics on points or lines?","\nThe algorithm expects polygons. For points use ",[21,1685,1687],{"href":1686},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis\u002F","raster sampling","; for lines, buffer them into thin polygons or sample along them as in an ",[21,1690,1692],{"href":1691},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fextract-elevation-profile-along-line-pyqgis\u002F","elevation profile",[14,1694,1695,1698],{},[286,1696,1697],{},"Does a cell get split between two overlapping zones?","\nNo. Each zone is evaluated independently against the whole raster, so a cell whose centre falls inside two overlapping polygons is counted once for each. Sums across overlapping zones therefore double-count.",[14,1700,1701,1704,1705,1406],{},[286,1702,1703],{},"How do I weight by the area of cell inside the zone?","\nThe algorithm does not do partial cells. Resampling the raster much finer than the zones approximates it well; for exact area weighting, polygonise the raster and use an area-weighted ",[21,1706,1708],{"href":1707},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fjoin-attributes-by-field-value-pyqgis\u002F","attribute join",[14,1710,1711,1714,1715,1717],{},[286,1712,1713],{},"Can I compute statistics for multiple bands at once?","\nOne call handles one band. Loop over bands with a different ",[307,1716,524],{}," each time, which is also how a multi-date raster stack is summarised.",[275,1719,1721],{"id":1720},"related","Related",[280,1723,1724,1729,1735,1741,1746],{},[283,1725,1726,1728],{},[21,1727,24],{"href":23}," — the guide this recipe belongs to",[283,1730,1731],{},[21,1732,1734],{"href":1733},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Freclassify-raster-values-pyqgis\u002F","Reclassify Raster Values in PyQGIS",[283,1736,1737],{},[21,1738,1740],{"href":1739},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fcalculate-raster-statistics-pyqgis\u002F","Calculate Raster Statistics in PyQGIS",[283,1742,1743],{},[21,1744,1745],{"href":1686},"Sample Raster Values at Points in PyQGIS",[283,1747,1748],{},[21,1749,1751],{"href":1750},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002F","Terrain & Interpolation Analysis in PyQGIS",[1753,1754,1755],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":317,"searchDepth":335,"depth":335,"links":1757},[1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768],{"id":277,"depth":335,"text":278},{"id":302,"depth":335,"text":303},{"id":547,"depth":335,"text":548},{"id":712,"depth":335,"text":713},{"id":1201,"depth":335,"text":1202},{"id":1409,"depth":335,"text":1410},{"id":1524,"depth":335,"text":1525},{"id":1617,"depth":335,"text":1618},{"id":1666,"depth":335,"text":1667},{"id":1676,"depth":335,"text":1677},{"id":1720,"depth":335,"text":1721},"Summarise raster values inside vector zones with native:zonalstatisticsfb — the statistic codes, why small zones return null, categorical rasters, and the QgsZonalStatistics class.","md",{"slug":1772,"type":1773,"breadcrumb":1774,"datePublished":1775,"dateModified":1775},"zonal-statistics-pyqgis","article","Zonal Statistics","2026-08-27","\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis",{"title":5,"description":1769},"spatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis\u002Findex","dTu7UrJUe-qpKSHfUoFdhpF6Obx-RRh29fAMyYgAch8",1787823363868]