[{"data":1,"prerenderedAt":1690},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis":3},{"id":4,"title":5,"body":6,"description":1683,"extension":1684,"meta":1685,"navigation":165,"path":1686,"seo":1687,"stem":1688,"__hash__":1689},"docs\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis\u002Findex.md","Classify a Layer with Natural Breaks (Jenks) in PyQGIS",{"type":7,"value":8,"toc":1669},"minimark",[9,13,33,38,66,70,96,364,390,394,401,597,614,618,621,809,818,874,878,885,999,1010,1014,1017,1353,1365,1369,1372,1447,1472,1476,1483,1544,1554,1558,1569,1583,1592,1603,1607,1616,1620,1626,1632,1638,1647,1651,1665],[10,11,5],"h1",{"id":12},"classify-a-layer-with-natural-breaks-jenks-in-pyqgis",[14,15,16,17,21,22,26,27,32],"p",{},"Natural breaks (the Jenks optimization) is the classification method that respects the ",[18,19,20],"em",{},"shape"," of your data. Instead of cutting a numeric field into equal-width bins or equal-count groups, Jenks searches for class boundaries that sit in the natural gaps of the distribution — minimizing variance within each class and maximizing it between classes. For real-world data with clusters and outliers, it produces the most faithful thematic map. This task-focused guide applies Jenks programmatically with ",[23,24,25],"code",{},"QgsClassificationJenks",", then compares it against the alternatives, as part of the ",[28,29,31],"a",{"href":30},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","Graduated & Categorized Renderers in PyQGIS"," cluster.",[34,35,37],"h2",{"id":36},"prerequisites","Prerequisites",[39,40,41,49,52,63],"ul",{},[42,43,44,48],"li",{},[45,46,47],"strong",{},"QGIS 3.34 LTR"," (Python 3.12) with PyQGIS, or a comparable 3.x release.",[42,50,51],{},"A vector layer loaded in the project with a numeric field that has a non-uniform (clustered or skewed) distribution — that is where Jenks shines.",[42,53,54,55,58,59,62],{},"The ",[45,56,57],{},"QGIS Python Console"," open (",[23,60,61],{},"Ctrl+Alt+P",").",[42,64,65],{},"Optional: familiarity with graduated renderers from the parent cluster.",[34,67,69],{"id":68},"step-1-build-a-graduated-renderer-with-the-jenks-method","Step 1: Build a Graduated Renderer with the Jenks Method",[14,71,72,73,76,77,80,81,84,85,87,88,91,92,95],{},"The modern PyQGIS pattern separates the ",[18,74,75],{},"renderer"," from the ",[18,78,79],{},"classification method",". You instantiate ",[23,82,83],{},"QgsGraduatedSymbolRenderer"," on a field, hand it a ",[23,86,25],{}," instance via ",[23,89,90],{},"setClassificationMethod()",", then call ",[23,93,94],{},"updateClasses()"," to compute the breaks.",[97,98,103],"pre",{"className":99,"code":100,"language":101,"meta":102,"style":102},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsProject,\n    QgsGraduatedSymbolRenderer,\n    QgsClassificationJenks,\n    QgsSymbol,\n    QgsStyle,\n)\n\nlayer = QgsProject.instance().mapLayersByName(\"counties\")[0]\nfield_name = \"median_income\"\nclass_count = 5\n\nrenderer = QgsGraduatedSymbolRenderer(field_name)\nrenderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))\n\n# Plug in Jenks natural breaks as the classification algorithm.\nrenderer.setClassificationMethod(QgsClassificationJenks())\n\n# Compute the break values from the layer's field.\nrenderer.updateClasses(layer, class_count)\n\nrenderer.updateColorRamp(QgsStyle.defaultStyle().colorRamp(\"YlOrRd\"))\nlayer.setRenderer(renderer)\nlayer.triggerRepaint()\n\nfor r in renderer.ranges():\n    print(f\"{r.lowerValue():.0f} – {r.upperValue():.0f}\")\n","python","",[23,104,105,124,130,136,142,148,154,160,167,193,204,215,220,231,237,242,249,255,260,266,272,277,289,295,301,306,321],{"__ignoreMap":102},[106,107,110,114,118,121],"span",{"class":108,"line":109},"line",1,[106,111,113],{"class":112},"snl16","from",[106,115,117],{"class":116},"s95oV"," qgis.core ",[106,119,120],{"class":112},"import",[106,122,123],{"class":116}," (\n",[106,125,127],{"class":108,"line":126},2,[106,128,129],{"class":116},"    QgsProject,\n",[106,131,133],{"class":108,"line":132},3,[106,134,135],{"class":116},"    QgsGraduatedSymbolRenderer,\n",[106,137,139],{"class":108,"line":138},4,[106,140,141],{"class":116},"    QgsClassificationJenks,\n",[106,143,145],{"class":108,"line":144},5,[106,146,147],{"class":116},"    QgsSymbol,\n",[106,149,151],{"class":108,"line":150},6,[106,152,153],{"class":116},"    QgsStyle,\n",[106,155,157],{"class":108,"line":156},7,[106,158,159],{"class":116},")\n",[106,161,163],{"class":108,"line":162},8,[106,164,166],{"emptyLinePlaceholder":165},true,"\n",[106,168,170,173,176,179,183,186,190],{"class":108,"line":169},9,[106,171,172],{"class":116},"layer ",[106,174,175],{"class":112},"=",[106,177,178],{"class":116}," QgsProject.instance().mapLayersByName(",[106,180,182],{"class":181},"sU2Wk","\"counties\"",[106,184,185],{"class":116},")[",[106,187,189],{"class":188},"sDLfK","0",[106,191,192],{"class":116},"]\n",[106,194,196,199,201],{"class":108,"line":195},10,[106,197,198],{"class":116},"field_name ",[106,200,175],{"class":112},[106,202,203],{"class":181}," \"median_income\"\n",[106,205,207,210,212],{"class":108,"line":206},11,[106,208,209],{"class":116},"class_count ",[106,211,175],{"class":112},[106,213,214],{"class":188}," 5\n",[106,216,218],{"class":108,"line":217},12,[106,219,166],{"emptyLinePlaceholder":165},[106,221,223,226,228],{"class":108,"line":222},13,[106,224,225],{"class":116},"renderer ",[106,227,175],{"class":112},[106,229,230],{"class":116}," QgsGraduatedSymbolRenderer(field_name)\n",[106,232,234],{"class":108,"line":233},14,[106,235,236],{"class":116},"renderer.setSourceSymbol(QgsSymbol.defaultSymbol(layer.geometryType()))\n",[106,238,240],{"class":108,"line":239},15,[106,241,166],{"emptyLinePlaceholder":165},[106,243,245],{"class":108,"line":244},16,[106,246,248],{"class":247},"sAwPA","# Plug in Jenks natural breaks as the classification algorithm.\n",[106,250,252],{"class":108,"line":251},17,[106,253,254],{"class":116},"renderer.setClassificationMethod(QgsClassificationJenks())\n",[106,256,258],{"class":108,"line":257},18,[106,259,166],{"emptyLinePlaceholder":165},[106,261,263],{"class":108,"line":262},19,[106,264,265],{"class":247},"# Compute the break values from the layer's field.\n",[106,267,269],{"class":108,"line":268},20,[106,270,271],{"class":116},"renderer.updateClasses(layer, class_count)\n",[106,273,275],{"class":108,"line":274},21,[106,276,166],{"emptyLinePlaceholder":165},[106,278,280,283,286],{"class":108,"line":279},22,[106,281,282],{"class":116},"renderer.updateColorRamp(QgsStyle.defaultStyle().colorRamp(",[106,284,285],{"class":181},"\"YlOrRd\"",[106,287,288],{"class":116},"))\n",[106,290,292],{"class":108,"line":291},23,[106,293,294],{"class":116},"layer.setRenderer(renderer)\n",[106,296,298],{"class":108,"line":297},24,[106,299,300],{"class":116},"layer.triggerRepaint()\n",[106,302,304],{"class":108,"line":303},25,[106,305,166],{"emptyLinePlaceholder":165},[106,307,309,312,315,318],{"class":108,"line":308},26,[106,310,311],{"class":112},"for",[106,313,314],{"class":116}," r ",[106,316,317],{"class":112},"in",[106,319,320],{"class":116}," renderer.ranges():\n",[106,322,324,327,330,333,336,339,342,345,348,351,353,356,358,360,362],{"class":108,"line":323},27,[106,325,326],{"class":188},"    print",[106,328,329],{"class":116},"(",[106,331,332],{"class":112},"f",[106,334,335],{"class":181},"\"",[106,337,338],{"class":188},"{",[106,340,341],{"class":116},"r.lowerValue()",[106,343,344],{"class":112},":.0f",[106,346,347],{"class":188},"}",[106,349,350],{"class":181}," – ",[106,352,338],{"class":188},[106,354,355],{"class":116},"r.upperValue()",[106,357,344],{"class":112},[106,359,347],{"class":188},[106,361,335],{"class":181},[106,363,159],{"class":116},[14,365,366,369,370,373,374,377,378,381,382,385,386,389],{},[45,367,368],{},"Breakdown:"," ",[23,371,372],{},"setClassificationMethod(QgsClassificationJenks())"," is the line that selects natural breaks; swapping the class object is all it takes to change strategy. ",[23,375,376],{},"updateClasses(layer, class_count)"," runs the Jenks optimization over ",[23,379,380],{},"median_income"," and creates five ",[23,383,384],{},"QgsRendererRange"," objects whose boundaries fall in the data's natural gaps. The sequential ",[23,387,388],{},"YlOrRd"," ramp encodes the ordered magnitude. Printing the ranges lets you eyeball where Jenks placed the breaks before trusting the map.",[34,391,393],{"id":392},"step-2-handle-nulls-and-invalid-values","Step 2: Handle Nulls and Invalid Values",[14,395,396,397,400],{},"Jenks operates only on valid numeric values; ",[23,398,399],{},"NULL"," entries are excluded from the optimization but still render as \"no value\" (typically grey) on the map. Decide explicitly how to treat them rather than letting them disappear silently.",[97,402,404],{"className":99,"code":403,"language":101,"meta":102,"style":102},"# Count how many features carry a usable value.\ntotal = layer.featureCount()\nvalid = sum(\n    1 for f in layer.getFeatures()\n    if f[field_name] is not None and f[field_name] != \"\"\n)\nprint(f\"{valid} of {total} features have a value; {total - valid} are null\")\n\n# Option A: build a clean subset by filtering, then classify on it.\nlayer.setSubsetString(f'\"{field_name}\" IS NOT NULL')\nrenderer.updateClasses(layer, class_count)\nlayer.setSubsetString(\"\")  # restore full layer for display\n\n# Option B: keep nulls visible but labelled distinctly.\n# (Leave them unclassified; QGIS draws them with the layer's\n#  \"no value\" symbol, which you can style separately.)\n",[23,405,406,411,421,434,450,478,482,530,534,539,561,565,578,582,587,592],{"__ignoreMap":102},[106,407,408],{"class":108,"line":109},[106,409,410],{"class":247},"# Count how many features carry a usable value.\n",[106,412,413,416,418],{"class":108,"line":126},[106,414,415],{"class":116},"total ",[106,417,175],{"class":112},[106,419,420],{"class":116}," layer.featureCount()\n",[106,422,423,426,428,431],{"class":108,"line":132},[106,424,425],{"class":116},"valid ",[106,427,175],{"class":112},[106,429,430],{"class":188}," sum",[106,432,433],{"class":116},"(\n",[106,435,436,439,442,445,447],{"class":108,"line":138},[106,437,438],{"class":188},"    1",[106,440,441],{"class":112}," for",[106,443,444],{"class":116}," f ",[106,446,317],{"class":112},[106,448,449],{"class":116}," layer.getFeatures()\n",[106,451,452,455,458,461,464,467,470,472,475],{"class":108,"line":144},[106,453,454],{"class":112},"    if",[106,456,457],{"class":116}," f[field_name] ",[106,459,460],{"class":112},"is",[106,462,463],{"class":112}," not",[106,465,466],{"class":188}," None",[106,468,469],{"class":112}," and",[106,471,457],{"class":116},[106,473,474],{"class":112},"!=",[106,476,477],{"class":181}," \"\"\n",[106,479,480],{"class":108,"line":150},[106,481,159],{"class":116},[106,483,484,487,489,491,493,495,498,500,503,505,508,510,513,515,517,520,523,525,528],{"class":108,"line":156},[106,485,486],{"class":188},"print",[106,488,329],{"class":116},[106,490,332],{"class":112},[106,492,335],{"class":181},[106,494,338],{"class":188},[106,496,497],{"class":116},"valid",[106,499,347],{"class":188},[106,501,502],{"class":181}," of ",[106,504,338],{"class":188},[106,506,507],{"class":116},"total",[106,509,347],{"class":188},[106,511,512],{"class":181}," features have a value; ",[106,514,338],{"class":188},[106,516,415],{"class":116},[106,518,519],{"class":112},"-",[106,521,522],{"class":116}," valid",[106,524,347],{"class":188},[106,526,527],{"class":181}," are null\"",[106,529,159],{"class":116},[106,531,532],{"class":108,"line":162},[106,533,166],{"emptyLinePlaceholder":165},[106,535,536],{"class":108,"line":169},[106,537,538],{"class":247},"# Option A: build a clean subset by filtering, then classify on it.\n",[106,540,541,544,546,549,551,554,556,559],{"class":108,"line":195},[106,542,543],{"class":116},"layer.setSubsetString(",[106,545,332],{"class":112},[106,547,548],{"class":181},"'\"",[106,550,338],{"class":188},[106,552,553],{"class":116},"field_name",[106,555,347],{"class":188},[106,557,558],{"class":181},"\" IS NOT NULL'",[106,560,159],{"class":116},[106,562,563],{"class":108,"line":206},[106,564,271],{"class":116},[106,566,567,569,572,575],{"class":108,"line":217},[106,568,543],{"class":116},[106,570,571],{"class":181},"\"\"",[106,573,574],{"class":116},")  ",[106,576,577],{"class":247},"# restore full layer for display\n",[106,579,580],{"class":108,"line":222},[106,581,166],{"emptyLinePlaceholder":165},[106,583,584],{"class":108,"line":233},[106,585,586],{"class":247},"# Option B: keep nulls visible but labelled distinctly.\n",[106,588,589],{"class":108,"line":239},[106,590,591],{"class":247},"# (Leave them unclassified; QGIS draws them with the layer's\n",[106,593,594],{"class":108,"line":244},[106,595,596],{"class":247},"#  \"no value\" symbol, which you can style separately.)\n",[14,598,599,601,602,605,606,608,609,613],{},[45,600,368],{}," Counting first tells you whether nulls are a rounding issue or a data-quality problem. ",[23,603,604],{},"setSubsetString()"," applies a temporary filter so ",[23,607,94],{}," computes breaks from clean data only; clearing it afterward keeps every feature visible. Because Jenks break positions depend on the value set it sees, classifying on the filtered subset prevents a single stray null-coerced zero from distorting the lowest class. To style the null features' fill explicitly, manipulate the symbol as shown in ",[28,610,612],{"href":611},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fset-vector-layer-symbol-color-pyqgis\u002F","Set a Vector Layer Symbol Color in PyQGIS",".",[34,615,617],{"id":616},"step-3-compare-jenks-against-equal-interval-and-quantile","Step 3: Compare Jenks Against Equal Interval and Quantile",[14,619,620],{},"The value of Jenks is clearest when you see the alternatives on the same data. Loop the three methods and print where each puts its breaks — identical data, three very different maps.",[97,622,624],{"className":99,"code":623,"language":101,"meta":102,"style":102},"from qgis.core import (\n    QgsClassificationEqualInterval,\n    QgsClassificationQuantile,\n    QgsClassificationJenks,\n)\n\nmethods = {\n    \"Equal Interval\": QgsClassificationEqualInterval(),\n    \"Quantile\": QgsClassificationQuantile(),\n    \"Jenks\": QgsClassificationJenks(),\n}\n\nfor name, method in methods.items():\n    renderer.setClassificationMethod(method)\n    renderer.updateClasses(layer, class_count)\n    breaks = [round(r.upperValue(), 1) for r in renderer.ranges()]\n    print(f\"{name:>14}: upper bounds {breaks}\")\n\n# Settle on Jenks for the final map.\nrenderer.setClassificationMethod(QgsClassificationJenks())\nrenderer.updateClasses(layer, class_count)\nlayer.triggerRepaint()\n",[23,625,626,636,641,646,650,654,658,668,676,684,692,697,701,713,718,723,754,788,792,797,801,805],{"__ignoreMap":102},[106,627,628,630,632,634],{"class":108,"line":109},[106,629,113],{"class":112},[106,631,117],{"class":116},[106,633,120],{"class":112},[106,635,123],{"class":116},[106,637,638],{"class":108,"line":126},[106,639,640],{"class":116},"    QgsClassificationEqualInterval,\n",[106,642,643],{"class":108,"line":132},[106,644,645],{"class":116},"    QgsClassificationQuantile,\n",[106,647,648],{"class":108,"line":138},[106,649,141],{"class":116},[106,651,652],{"class":108,"line":144},[106,653,159],{"class":116},[106,655,656],{"class":108,"line":150},[106,657,166],{"emptyLinePlaceholder":165},[106,659,660,663,665],{"class":108,"line":156},[106,661,662],{"class":116},"methods ",[106,664,175],{"class":112},[106,666,667],{"class":116}," {\n",[106,669,670,673],{"class":108,"line":162},[106,671,672],{"class":181},"    \"Equal Interval\"",[106,674,675],{"class":116},": QgsClassificationEqualInterval(),\n",[106,677,678,681],{"class":108,"line":169},[106,679,680],{"class":181},"    \"Quantile\"",[106,682,683],{"class":116},": QgsClassificationQuantile(),\n",[106,685,686,689],{"class":108,"line":195},[106,687,688],{"class":181},"    \"Jenks\"",[106,690,691],{"class":116},": QgsClassificationJenks(),\n",[106,693,694],{"class":108,"line":206},[106,695,696],{"class":116},"}\n",[106,698,699],{"class":108,"line":217},[106,700,166],{"emptyLinePlaceholder":165},[106,702,703,705,708,710],{"class":108,"line":222},[106,704,311],{"class":112},[106,706,707],{"class":116}," name, method ",[106,709,317],{"class":112},[106,711,712],{"class":116}," methods.items():\n",[106,714,715],{"class":108,"line":233},[106,716,717],{"class":116},"    renderer.setClassificationMethod(method)\n",[106,719,720],{"class":108,"line":239},[106,721,722],{"class":116},"    renderer.updateClasses(layer, class_count)\n",[106,724,725,728,730,733,736,739,742,745,747,749,751],{"class":108,"line":244},[106,726,727],{"class":116},"    breaks ",[106,729,175],{"class":112},[106,731,732],{"class":116}," [",[106,734,735],{"class":188},"round",[106,737,738],{"class":116},"(r.upperValue(), ",[106,740,741],{"class":188},"1",[106,743,744],{"class":116},") ",[106,746,311],{"class":112},[106,748,314],{"class":116},[106,750,317],{"class":112},[106,752,753],{"class":116}," renderer.ranges()]\n",[106,755,756,758,760,762,764,766,769,772,774,777,779,782,784,786],{"class":108,"line":251},[106,757,326],{"class":188},[106,759,329],{"class":116},[106,761,332],{"class":112},[106,763,335],{"class":181},[106,765,338],{"class":188},[106,767,768],{"class":116},"name",[106,770,771],{"class":112},":>14",[106,773,347],{"class":188},[106,775,776],{"class":181},": upper bounds ",[106,778,338],{"class":188},[106,780,781],{"class":116},"breaks",[106,783,347],{"class":188},[106,785,335],{"class":181},[106,787,159],{"class":116},[106,789,790],{"class":108,"line":257},[106,791,166],{"emptyLinePlaceholder":165},[106,793,794],{"class":108,"line":262},[106,795,796],{"class":247},"# Settle on Jenks for the final map.\n",[106,798,799],{"class":108,"line":268},[106,800,254],{"class":116},[106,802,803],{"class":108,"line":274},[106,804,271],{"class":116},[106,806,807],{"class":108,"line":279},[106,808,300],{"class":116},[14,810,811,813,814,817],{},[45,812,368],{}," Equal interval will return evenly spaced upper bounds regardless of where the data actually sits, so a skewed field leaves some classes nearly empty. Quantile returns bounds that put equal feature counts in each class, often splitting visually identical values across a boundary. Jenks returns bounds clustered around the data's real gaps. Comparing the printed ",[23,815,816],{},"upper bounds"," lists makes the trade-off concrete before you commit.",[819,820,821,837],"table",{},[822,823,824],"thead",{},[825,826,827,831,834],"tr",{},[828,829,830],"th",{},"Method",[828,832,833],{},"How breaks are placed",[828,835,836],{},"When it wins",[838,839,840,852,863],"tbody",{},[825,841,842,846,849],{},[843,844,845],"td",{},"Equal interval",[843,847,848],{},"Range divided into equal widths",[843,850,851],{},"Uniform data; legends that must read as round numbers",[825,853,854,857,860],{},[843,855,856],{},"Quantile",[843,858,859],{},"Equal feature count per class",[843,861,862],{},"Ranked maps; balancing map ink",[825,864,865,868,871],{},[843,866,867],{},"Jenks",[843,869,870],{},"Minimizes within-class variance at natural gaps",[843,872,873],{},"Clustered or skewed real-world data",[34,875,877],{"id":876},"step-4-choosing-the-class-count-for-jenks","Step 4: Choosing the Class Count for Jenks",[14,879,880,881,884],{},"Unlike equal interval, Jenks break positions ",[18,882,883],{},"change"," with the class count because the optimization re-partitions the whole distribution each time. Test a few counts and inspect the goodness of variance fit (GVF) implicitly by checking how tight the printed ranges are around clusters.",[97,886,888],{"className":99,"code":887,"language":101,"meta":102,"style":102},"for n in (4, 5, 6, 7):\n    renderer.setClassificationMethod(QgsClassificationJenks())\n    renderer.updateClasses(layer, n)\n    spans = [round(r.upperValue() - r.lowerValue(), 1)\n             for r in renderer.ranges()]\n    print(f\"{n} classes -> span widths {spans}\")\n",[23,889,890,924,929,934,957,968],{"__ignoreMap":102},[106,891,892,894,897,899,902,905,908,911,913,916,918,921],{"class":108,"line":109},[106,893,311],{"class":112},[106,895,896],{"class":116}," n ",[106,898,317],{"class":112},[106,900,901],{"class":116}," (",[106,903,904],{"class":188},"4",[106,906,907],{"class":116},", ",[106,909,910],{"class":188},"5",[106,912,907],{"class":116},[106,914,915],{"class":188},"6",[106,917,907],{"class":116},[106,919,920],{"class":188},"7",[106,922,923],{"class":116},"):\n",[106,925,926],{"class":108,"line":126},[106,927,928],{"class":116},"    renderer.setClassificationMethod(QgsClassificationJenks())\n",[106,930,931],{"class":108,"line":132},[106,932,933],{"class":116},"    renderer.updateClasses(layer, n)\n",[106,935,936,939,941,943,945,948,950,953,955],{"class":108,"line":138},[106,937,938],{"class":116},"    spans ",[106,940,175],{"class":112},[106,942,732],{"class":116},[106,944,735],{"class":188},[106,946,947],{"class":116},"(r.upperValue() ",[106,949,519],{"class":112},[106,951,952],{"class":116}," r.lowerValue(), ",[106,954,741],{"class":188},[106,956,159],{"class":116},[106,958,959,962,964,966],{"class":108,"line":144},[106,960,961],{"class":112},"             for",[106,963,314],{"class":116},[106,965,317],{"class":112},[106,967,753],{"class":116},[106,969,970,972,974,976,978,980,983,985,988,990,993,995,997],{"class":108,"line":150},[106,971,326],{"class":188},[106,973,329],{"class":116},[106,975,332],{"class":112},[106,977,335],{"class":181},[106,979,338],{"class":188},[106,981,982],{"class":116},"n",[106,984,347],{"class":188},[106,986,987],{"class":181}," classes -> span widths ",[106,989,338],{"class":188},[106,991,992],{"class":116},"spans",[106,994,347],{"class":188},[106,996,335],{"class":181},[106,998,159],{"class":116},[14,1000,1001,1003,1004,1006,1007,1009],{},[45,1002,368],{}," Re-running ",[23,1005,94],{}," with different ",[23,1008,982],{}," shows how the partition shifts. Look for the smallest class count where each class still maps to a meaningful group; adding classes past that point yields diminishing returns and a harder-to-read legend. Five classes is a reliable default, but clustered data with three obvious tiers may read better with four.",[34,1011,1013],{"id":1012},"step-5-measure-the-fit-yourself","Step 5: Measure the Fit Yourself",[14,1015,1016],{},"QGIS does not expose a built-in goodness of variance fit (GVF) score, but it is cheap to compute and it turns \"this map looks better\" into a number you can defend. GVF compares the variance the classification removes against the total variance in the data; values closer to 1 mean tighter, more faithful classes.",[97,1018,1020],{"className":99,"code":1019,"language":101,"meta":102,"style":102},"def gvf(values, ranges):\n    \"\"\"Goodness of variance fit for a set of classification ranges.\"\"\"\n    mean = sum(values) \u002F len(values)\n    sdam = sum((v - mean) ** 2 for v in values)  # total variance\n    sdcm = 0.0\n    for r in ranges:\n        members = [v for v in values\n                   if r.lowerValue() \u003C= v \u003C= r.upperValue()]\n        if members:\n            cmean = sum(members) \u002F len(members)\n            sdcm += sum((v - cmean) ** 2 for v in members)\n    return 1 - (sdcm \u002F sdam) if sdam else 0.0\n\n\nvalues = [f[field_name] for f in layer.getFeatures()\n          if f[field_name] is not None]\n\nfor method_cls in (QgsClassificationEqualInterval,\n                   QgsClassificationQuantile,\n                   QgsClassificationJenks):\n    renderer.setClassificationMethod(method_cls())\n    renderer.updateClasses(layer, class_count)\n    print(f\"{method_cls.__name__}: GVF = {gvf(values, renderer.ranges()):.3f}\")\n",[23,1021,1022,1034,1039,1060,1096,1106,1118,1137,1155,1163,1182,1212,1242,1246,1250,1268,1283,1287,1299,1304,1309,1314,1318],{"__ignoreMap":102},[106,1023,1024,1027,1031],{"class":108,"line":109},[106,1025,1026],{"class":112},"def",[106,1028,1030],{"class":1029},"svObZ"," gvf",[106,1032,1033],{"class":116},"(values, ranges):\n",[106,1035,1036],{"class":108,"line":126},[106,1037,1038],{"class":181},"    \"\"\"Goodness of variance fit for a set of classification ranges.\"\"\"\n",[106,1040,1041,1044,1046,1048,1051,1054,1057],{"class":108,"line":132},[106,1042,1043],{"class":116},"    mean ",[106,1045,175],{"class":112},[106,1047,430],{"class":188},[106,1049,1050],{"class":116},"(values) ",[106,1052,1053],{"class":112},"\u002F",[106,1055,1056],{"class":188}," len",[106,1058,1059],{"class":116},"(values)\n",[106,1061,1062,1065,1067,1069,1072,1074,1077,1080,1083,1085,1088,1090,1093],{"class":108,"line":138},[106,1063,1064],{"class":116},"    sdam ",[106,1066,175],{"class":112},[106,1068,430],{"class":188},[106,1070,1071],{"class":116},"((v ",[106,1073,519],{"class":112},[106,1075,1076],{"class":116}," mean) ",[106,1078,1079],{"class":112},"**",[106,1081,1082],{"class":188}," 2",[106,1084,441],{"class":112},[106,1086,1087],{"class":116}," v ",[106,1089,317],{"class":112},[106,1091,1092],{"class":116}," values)  ",[106,1094,1095],{"class":247},"# total variance\n",[106,1097,1098,1101,1103],{"class":108,"line":144},[106,1099,1100],{"class":116},"    sdcm ",[106,1102,175],{"class":112},[106,1104,1105],{"class":188}," 0.0\n",[106,1107,1108,1111,1113,1115],{"class":108,"line":150},[106,1109,1110],{"class":112},"    for",[106,1112,314],{"class":116},[106,1114,317],{"class":112},[106,1116,1117],{"class":116}," ranges:\n",[106,1119,1120,1123,1125,1128,1130,1132,1134],{"class":108,"line":156},[106,1121,1122],{"class":116},"        members ",[106,1124,175],{"class":112},[106,1126,1127],{"class":116}," [v ",[106,1129,311],{"class":112},[106,1131,1087],{"class":116},[106,1133,317],{"class":112},[106,1135,1136],{"class":116}," values\n",[106,1138,1139,1142,1145,1148,1150,1152],{"class":108,"line":162},[106,1140,1141],{"class":112},"                   if",[106,1143,1144],{"class":116}," r.lowerValue() ",[106,1146,1147],{"class":112},"\u003C=",[106,1149,1087],{"class":116},[106,1151,1147],{"class":112},[106,1153,1154],{"class":116}," r.upperValue()]\n",[106,1156,1157,1160],{"class":108,"line":169},[106,1158,1159],{"class":112},"        if",[106,1161,1162],{"class":116}," members:\n",[106,1164,1165,1168,1170,1172,1175,1177,1179],{"class":108,"line":195},[106,1166,1167],{"class":116},"            cmean ",[106,1169,175],{"class":112},[106,1171,430],{"class":188},[106,1173,1174],{"class":116},"(members) ",[106,1176,1053],{"class":112},[106,1178,1056],{"class":188},[106,1180,1181],{"class":116},"(members)\n",[106,1183,1184,1187,1190,1192,1194,1196,1199,1201,1203,1205,1207,1209],{"class":108,"line":206},[106,1185,1186],{"class":116},"            sdcm ",[106,1188,1189],{"class":112},"+=",[106,1191,430],{"class":188},[106,1193,1071],{"class":116},[106,1195,519],{"class":112},[106,1197,1198],{"class":116}," cmean) ",[106,1200,1079],{"class":112},[106,1202,1082],{"class":188},[106,1204,441],{"class":112},[106,1206,1087],{"class":116},[106,1208,317],{"class":112},[106,1210,1211],{"class":116}," members)\n",[106,1213,1214,1217,1220,1223,1226,1228,1231,1234,1237,1240],{"class":108,"line":217},[106,1215,1216],{"class":112},"    return",[106,1218,1219],{"class":188}," 1",[106,1221,1222],{"class":112}," -",[106,1224,1225],{"class":116}," (sdcm ",[106,1227,1053],{"class":112},[106,1229,1230],{"class":116}," sdam) ",[106,1232,1233],{"class":112},"if",[106,1235,1236],{"class":116}," sdam ",[106,1238,1239],{"class":112},"else",[106,1241,1105],{"class":188},[106,1243,1244],{"class":108,"line":222},[106,1245,166],{"emptyLinePlaceholder":165},[106,1247,1248],{"class":108,"line":233},[106,1249,166],{"emptyLinePlaceholder":165},[106,1251,1252,1255,1257,1260,1262,1264,1266],{"class":108,"line":239},[106,1253,1254],{"class":116},"values ",[106,1256,175],{"class":112},[106,1258,1259],{"class":116}," [f[field_name] ",[106,1261,311],{"class":112},[106,1263,444],{"class":116},[106,1265,317],{"class":112},[106,1267,449],{"class":116},[106,1269,1270,1273,1275,1277,1279,1281],{"class":108,"line":244},[106,1271,1272],{"class":112},"          if",[106,1274,457],{"class":116},[106,1276,460],{"class":112},[106,1278,463],{"class":112},[106,1280,466],{"class":188},[106,1282,192],{"class":116},[106,1284,1285],{"class":108,"line":251},[106,1286,166],{"emptyLinePlaceholder":165},[106,1288,1289,1291,1294,1296],{"class":108,"line":257},[106,1290,311],{"class":112},[106,1292,1293],{"class":116}," method_cls ",[106,1295,317],{"class":112},[106,1297,1298],{"class":116}," (QgsClassificationEqualInterval,\n",[106,1300,1301],{"class":108,"line":262},[106,1302,1303],{"class":116},"                   QgsClassificationQuantile,\n",[106,1305,1306],{"class":108,"line":268},[106,1307,1308],{"class":116},"                   QgsClassificationJenks):\n",[106,1310,1311],{"class":108,"line":274},[106,1312,1313],{"class":116},"    renderer.setClassificationMethod(method_cls())\n",[106,1315,1316],{"class":108,"line":279},[106,1317,722],{"class":116},[106,1319,1320,1322,1324,1326,1328,1330,1333,1336,1339,1341,1344,1347,1349,1351],{"class":108,"line":291},[106,1321,326],{"class":188},[106,1323,329],{"class":116},[106,1325,332],{"class":112},[106,1327,335],{"class":181},[106,1329,338],{"class":188},[106,1331,1332],{"class":116},"method_cls.",[106,1334,1335],{"class":188},"__name__}",[106,1337,1338],{"class":181},": GVF = ",[106,1340,338],{"class":188},[106,1342,1343],{"class":116},"gvf(values, renderer.ranges())",[106,1345,1346],{"class":112},":.3f",[106,1348,347],{"class":188},[106,1350,335],{"class":181},[106,1352,159],{"class":116},[14,1354,1355,369,1357,1360,1361,1364],{},[45,1356,368],{},[23,1358,1359],{},"sdam"," is the sum of squared deviations about the grand mean — the total variance to \"explain.\" ",[23,1362,1363],{},"sdcm"," is the residual variance left inside the classes after partitioning. Their ratio subtracted from 1 is the GVF. Run it across the three methods and Jenks will almost always post the highest score on clustered data, giving you objective evidence that its breaks fit the distribution better than equal interval or quantile. Note the inclusive comparison on both bounds can double-count a value that lands exactly on a boundary; for production accuracy use the half-open intervals QGIS itself applies.",[34,1366,1368],{"id":1367},"step-6-apply-the-final-renderer-to-the-map","Step 6: Apply the Final Renderer to the Map",[14,1370,1371],{},"Once Jenks is your chosen method and nulls are handled, commit the renderer and refresh the display so the canvas and legend reflect the natural-breaks classification.",[97,1373,1375],{"className":99,"code":1374,"language":101,"meta":102,"style":102},"from qgis.utils import iface\n\nrenderer.setClassificationMethod(QgsClassificationJenks())\nrenderer.updateClasses(layer, class_count)\nrenderer.updateColorRamp(QgsStyle.defaultStyle().colorRamp(\"YlOrRd\"))\n\nlayer.setRenderer(renderer)\nlayer.triggerRepaint()\nif iface is not None:\n    iface.layerTreeView().refreshLayerSymbology(layer.id())\n    iface.mapCanvas().refresh()\n",[23,1376,1377,1389,1393,1397,1401,1409,1413,1417,1421,1437,1442],{"__ignoreMap":102},[106,1378,1379,1381,1384,1386],{"class":108,"line":109},[106,1380,113],{"class":112},[106,1382,1383],{"class":116}," qgis.utils ",[106,1385,120],{"class":112},[106,1387,1388],{"class":116}," iface\n",[106,1390,1391],{"class":108,"line":126},[106,1392,166],{"emptyLinePlaceholder":165},[106,1394,1395],{"class":108,"line":132},[106,1396,254],{"class":116},[106,1398,1399],{"class":108,"line":138},[106,1400,271],{"class":116},[106,1402,1403,1405,1407],{"class":108,"line":144},[106,1404,282],{"class":116},[106,1406,285],{"class":181},[106,1408,288],{"class":116},[106,1410,1411],{"class":108,"line":150},[106,1412,166],{"emptyLinePlaceholder":165},[106,1414,1415],{"class":108,"line":156},[106,1416,294],{"class":116},[106,1418,1419],{"class":108,"line":162},[106,1420,300],{"class":116},[106,1422,1423,1425,1428,1430,1432,1434],{"class":108,"line":169},[106,1424,1233],{"class":112},[106,1426,1427],{"class":116}," iface ",[106,1429,460],{"class":112},[106,1431,463],{"class":112},[106,1433,466],{"class":188},[106,1435,1436],{"class":116},":\n",[106,1438,1439],{"class":108,"line":195},[106,1440,1441],{"class":116},"    iface.layerTreeView().refreshLayerSymbology(layer.id())\n",[106,1443,1444],{"class":108,"line":206},[106,1445,1446],{"class":116},"    iface.mapCanvas().refresh()\n",[14,1448,1449,1003,1451,1454,1455,1458,1459,1462,1463,1466,1467,1471],{},[45,1450,368],{},[23,1452,1453],{},"setClassificationMethod"," and ",[23,1456,1457],{},"updateClasses"," immediately before ",[23,1460,1461],{},"setRenderer"," guarantees the committed renderer uses Jenks even after the comparison loops in earlier steps changed the method. ",[23,1464,1465],{},"refreshLayerSymbology"," rebuilds the legend swatches so they match the natural-breaks ranges. The result is ready to feed into a polygon choropleth — see ",[28,1468,1470],{"href":1469},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","Create a Choropleth Map in PyQGIS"," for the full thematic-map workflow.",[34,1473,1475],{"id":1474},"qgis-version-compatibility","QGIS Version Compatibility",[14,1477,1478,1479,1482],{},"This recipe targets ",[45,1480,1481],{},"QGIS 3.34 LTR (Python 3.12)"," and relies on the method-object classification API.",[819,1484,1485,1495],{},[822,1486,1487],{},[825,1488,1489,1492],{},[828,1490,1491],{},"QGIS \u002F Python",[828,1493,1494],{},"Notes",[838,1496,1497,1514,1522,1530],{},[825,1498,1499,1504],{},[843,1500,1501],{},[45,1502,1503],{},"3.34 LTR (Python 3.12)",[843,1505,1506,1507,907,1509,907,1511,1513],{},"Baseline. ",[23,1508,25],{},[23,1510,1453],{},[23,1512,1457],{}," all current.",[825,1515,1516,1519],{},[843,1517,1518],{},"3.28 LTR (Python 3.9)",[843,1520,1521],{},"Fully supported with identical code.",[825,1523,1524,1527],{},[843,1525,1526],{},"3.40 \u002F 3.44",[843,1528,1529],{},"Same API; Jenks performance improved for large layers.",[825,1531,1532,1535],{},[843,1533,1534],{},"Pre-3.10",[843,1536,1537,1538,1540,1541,613],{},"No ",[23,1539,1453],{},". Legacy code used ",[23,1542,1543],{},"QgsGraduatedSymbolRenderer.createRenderer(layer, field, classes, QgsGraduatedSymbolRenderer.Jenks, symbol, ramp)",[14,1545,1546,1547,1549,1550,1553],{},"The deprecated ",[23,1548,867],{}," mode enum still resolves on the LTR builds, but new code should use ",[23,1551,1552],{},"QgsClassificationJenks()"," so it keeps working as the enum is phased out.",[34,1555,1557],{"id":1556},"troubleshooting","Troubleshooting",[14,1559,1560,1565,1566,1568],{},[45,1561,1562,1564],{},[23,1563,1457],{}," is slow or freezes on a huge layer."," Jenks is computationally heavier than equal interval or quantile. Pre-filter with ",[23,1567,604],{},", sample the layer, or reduce the class count. On layers over a few hundred thousand features, consider classifying on a representative sample.",[14,1570,1571,1574,1575,1578,1579,1582],{},[45,1572,1573],{},"Breaks change every time I run it."," That is expected behavior, not a bug, ",[18,1576,1577],{},"only"," if you changed the class count or the underlying values. With identical inputs Jenks is deterministic. If values shift, check for an active ",[23,1580,1581],{},"setSubsetString"," filter affecting the value set.",[14,1584,1585,1588,1589,1591],{},[45,1586,1587],{},"Some polygons render grey."," They hold ",[23,1590,399],{}," values excluded from classification. Use the null-handling pattern in step 2 to either filter them or style them deliberately.",[14,1593,1594,1599,1600,613],{},[45,1595,1596,613],{},[23,1597,1598],{},"AttributeError: colorRamp returned None"," The ramp name is not installed. List valid names with ",[23,1601,1602],{},"QgsStyle.defaultStyle().colorRampNames()",[34,1604,1606],{"id":1605},"conclusion","Conclusion",[14,1608,1609,1610,1612,1613,1615],{},"Jenks natural breaks gives you a classification that mirrors the real structure of your data rather than imposing an arbitrary grid on it. The PyQGIS workflow is compact: instantiate the renderer, call ",[23,1611,372],{},", run ",[23,1614,94],{},", and handle nulls deliberately. Compare it against equal interval and quantile on your own field, and let the printed break values — not habit — guide the final choice. From here, apply the result to a polygon layer to produce a faithful choropleth.",[34,1617,1619],{"id":1618},"frequently-asked-questions","Frequently Asked Questions",[14,1621,1622,1625],{},[45,1623,1624],{},"What exactly does Jenks optimize?","\nIt minimizes the sum of squared deviations within each class and maximizes the deviation between classes, placing boundaries in the natural gaps of the value distribution.",[14,1627,1628,1631],{},[45,1629,1630],{},"Is Jenks always the best choice?","\nNo. For uniform data, equal interval is simpler and yields rounder legend numbers. For ranked maps where balanced feature counts matter, quantile is better. Jenks excels specifically on clustered or skewed data.",[14,1633,1634,1637],{},[45,1635,1636],{},"Why do my Jenks breaks differ from QGIS 3.10?","\nThe algorithm was refined for performance and edge cases across releases, and break positions depend on the exact value set. Small differences on identical data across major versions are expected.",[14,1639,1640,1643,1644,1646],{},[45,1641,1642],{},"Does the class count affect Jenks more than other methods?","\nYes. Jenks re-partitions the entire distribution for each class count, so breaks move when you change ",[23,1645,982],{},", whereas equal-interval bounds simply subdivide a fixed range.",[34,1648,1650],{"id":1649},"related","Related",[39,1652,1653,1657,1661],{},[42,1654,1655],{},[28,1656,31],{"href":30},[42,1658,1659],{},[28,1660,1470],{"href":1469},[42,1662,1663],{},[28,1664,612],{"href":611},[1666,1667,1668],"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":102,"searchDepth":126,"depth":126,"links":1670},[1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682],{"id":36,"depth":126,"text":37},{"id":68,"depth":126,"text":69},{"id":392,"depth":126,"text":393},{"id":616,"depth":126,"text":617},{"id":876,"depth":126,"text":877},{"id":1012,"depth":126,"text":1013},{"id":1367,"depth":126,"text":1368},{"id":1474,"depth":126,"text":1475},{"id":1556,"depth":126,"text":1557},{"id":1605,"depth":126,"text":1606},{"id":1618,"depth":126,"text":1619},{"id":1649,"depth":126,"text":1650},"Apply Jenks natural breaks in PyQGIS with QgsClassificationJenks, setClassificationMethod, and updateClasses, compare to equal interval and quantile, and handle nulls.","md",{},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis",{"title":5,"description":1683},"pyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fclassify-layer-natural-breaks-jenks-pyqgis\u002Findex","MIfdHWUByEhDbkDAptBDF6Ee0vNI-858onKo2hNOakU",1781792483472]