[{"data":1,"prerenderedAt":1575},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Freclassify-raster-values-pyqgis":3},{"id":4,"title":5,"body":6,"description":1564,"extension":1565,"meta":1566,"navigation":219,"path":1571,"seo":1572,"stem":1573,"__hash__":1574},"docs\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Freclassify-raster-values-pyqgis\u002Findex.md","Reclassify Raster Values in PyQGIS",{"type":7,"value":8,"toc":1550},"minimark",[9,13,17,26,159,164,186,190,193,460,490,494,500,525,556,564,568,574,577,667,672,749,753,756,934,955,961,1013,1027,1031,1037,1165,1174,1178,1181,1187,1274,1289,1293,1299,1377,1381,1443,1447,1460,1464,1475,1489,1499,1514,1518,1546],[10,11,5],"h1",{"id":12},"reclassify-raster-values-in-pyqgis",[14,15,16],"p",{},"Reclassification is how a continuous surface becomes a decision: slope becomes buildable or not, elevation becomes flood risk bands, land cover codes become a habitat suitability score. The algorithm is simple; the two things that go wrong are the boundary rule at each class edge and what happens to values the table does not mention.",[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 building the flat parameter table, the four boundary conventions and which one you want, handling values outside the table, choosing an output data type that fits the classes, and the layer-driven variant when the table is itself data.",[14,27,28],{},[29,30,35,39,43,50,59,67,75,81,86,89,94,98,104,109,114,118,121,125,130,134,137,140,149,154],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 312","img","A continuous value axis divided into class ranges, showing that each boundary belongs to exactly one class depending on the chosen range boundary rule, and that unmatched values become nodata","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Boundaries, and the values the table forgot",[40,41,42],"desc",{},"A continuous axis is divided into three ranges mapped to three class codes. Each boundary value must belong to exactly one range, decided by the range boundaries setting. Values below the first range or above the last are not mentioned by the table and become nodata unless the algorithm is told otherwise.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","312","#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","Every boundary belongs to exactly one side",[60,61],"line",{"x1":62,"y1":63,"x2":64,"y2":63,"stroke":65,"style":66},"60","150","700","#2f3b35","stroke-width:2",[44,68],{"x":69,"y":70,"width":71,"height":72,"fill":73,"stroke":74,"style":66},"140","112","160","38","#edf8e9","#15803d",[51,76,80],{"x":77,"y":78,"style":79,"fill":65,"textAnchor":57},"220","137","text-anchor:middle;font-size:11px;font-family:sans-serif","0 – 5 → class 1",[44,82],{"x":83,"y":70,"width":71,"height":72,"fill":84,"stroke":85,"style":66},"300","#fdf2e2","#b45309",[51,87,88],{"x":53,"y":78,"style":79,"fill":65,"textAnchor":57},"5 – 15 → class 2",[44,90],{"x":91,"y":70,"width":71,"height":72,"fill":92,"stroke":93,"style":66},"460","#eff3ff","#2563eb",[51,95,97],{"x":96,"y":78,"style":79,"fill":65,"textAnchor":57},"540","15 – 30 → class 3",[60,99],{"x1":83,"y1":100,"x2":83,"y2":101,"stroke":102,"style":103},"100","170","#b91c1c","stroke-width:2.5",[51,105,108],{"x":83,"y":106,"style":107,"fill":102,"textAnchor":57},"190","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","exactly 5",[51,110,113],{"x":83,"y":111,"style":112,"fill":65,"textAnchor":57},"210","text-anchor:middle;font-size:10px;font-family:sans-serif","class 1 or class 2?",[51,115,46],{"x":69,"y":116,"style":112,"fill":117,"textAnchor":57},"176","#59645f",[51,119,120],{"x":91,"y":116,"style":112,"fill":117,"textAnchor":57},"15",[51,122,124],{"x":123,"y":116,"style":112,"fill":117,"textAnchor":57},"620","30",[44,126],{"x":62,"y":70,"width":127,"height":72,"fill":128,"stroke":117,"style":129},"76","#efeadd","stroke-width:1.6;stroke-dasharray:4 3",[51,131,133],{"x":132,"y":78,"style":112,"fill":117,"textAnchor":57},"98","nodata",[44,135],{"x":136,"y":70,"width":127,"height":72,"fill":128,"stroke":117,"style":129},"624",[51,138,133],{"x":139,"y":78,"style":112,"fill":117,"textAnchor":57},"662",[44,141],{"x":142,"y":143,"width":144,"height":145,"rx":146,"fill":147,"stroke":148,"style":103},"120","236","520","56","8","#fffdf7","#0f766e",[51,150,153],{"x":53,"y":151,"style":152,"fill":148,"textAnchor":57},"260","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","values outside every range become nodata",[51,155,158],{"x":53,"y":156,"style":157,"fill":65,"textAnchor":57},"280","text-anchor:middle;font-size:10.5px;font-family:sans-serif","extend the first and last ranges to infinity, or accept the holes",[160,161,163],"h2",{"id":162},"prerequisites","Prerequisites",[165,166,167,175,178],"ul",{},[168,169,170,174],"li",{},[171,172,173],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[168,176,177],{},"A single-band raster. Multi-band inputs are handled one band at a time.",[168,179,180,181,185],{},"Knowledge of the raster's actual value range. ",[182,183,184],"code",{},"layer.dataProvider().bandStatistics(1)"," reports it, and reclassifying against a guessed range is how holes appear.",[160,187,189],{"id":188},"build-the-table-and-run-it","Build the table and run it",[14,191,192],{},"The table is a flat list of triples, not a list of lists, which is the first thing to get right.",[194,195,200],"pre",{"className":196,"code":197,"language":198,"meta":199,"style":199},"language-python shiki shiki-themes github-dark","import processing\n\nTABLE = [\n    0,   5,  1,      # gentle\n    5,  15,  2,      # moderate\n    15, 30,  3,      # steep\n    30, 90,  4,      # unbuildable\n]\n\nprocessing.run(\"native:reclassifybytable\", {\n    \"INPUT_RASTER\": \"\u002Fdata\u002Foutput\u002Fslope_deg.tif\",\n    \"RASTER_BAND\": 1,\n    \"TABLE\": TABLE,\n    \"NO_DATA\": -9999,\n    \"RANGE_BOUNDARIES\": 0,\n    \"NODATA_FOR_MISSING\": True,\n    \"DATA_TYPE\": 1,                     # Byte\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fslope_class.tif\",\n})\n","python","",[182,201,202,214,221,234,259,279,300,321,327,332,345,360,372,384,400,412,425,441,454],{"__ignoreMap":199},[203,204,206,210],"span",{"class":60,"line":205},1,[203,207,209],{"class":208},"snl16","import",[203,211,213],{"class":212},"s95oV"," processing\n",[203,215,217],{"class":60,"line":216},2,[203,218,220],{"emptyLinePlaceholder":219},true,"\n",[203,222,224,228,231],{"class":60,"line":223},3,[203,225,227],{"class":226},"sDLfK","TABLE",[203,229,230],{"class":208}," =",[203,232,233],{"class":212}," [\n",[203,235,237,240,243,246,249,252,255],{"class":60,"line":236},4,[203,238,239],{"class":226},"    0",[203,241,242],{"class":212},",   ",[203,244,245],{"class":226},"5",[203,247,248],{"class":212},",  ",[203,250,251],{"class":226},"1",[203,253,254],{"class":212},",      ",[203,256,258],{"class":257},"sjoCn","# gentle\n",[203,260,262,265,267,269,271,274,276],{"class":60,"line":261},5,[203,263,264],{"class":226},"    5",[203,266,248],{"class":212},[203,268,120],{"class":226},[203,270,248],{"class":212},[203,272,273],{"class":226},"2",[203,275,254],{"class":212},[203,277,278],{"class":257},"# moderate\n",[203,280,282,285,288,290,292,295,297],{"class":60,"line":281},6,[203,283,284],{"class":226},"    15",[203,286,287],{"class":212},", ",[203,289,124],{"class":226},[203,291,248],{"class":212},[203,293,294],{"class":226},"3",[203,296,254],{"class":212},[203,298,299],{"class":257},"# steep\n",[203,301,303,306,308,311,313,316,318],{"class":60,"line":302},7,[203,304,305],{"class":226},"    30",[203,307,287],{"class":212},[203,309,310],{"class":226},"90",[203,312,248],{"class":212},[203,314,315],{"class":226},"4",[203,317,254],{"class":212},[203,319,320],{"class":257},"# unbuildable\n",[203,322,324],{"class":60,"line":323},8,[203,325,326],{"class":212},"]\n",[203,328,330],{"class":60,"line":329},9,[203,331,220],{"emptyLinePlaceholder":219},[203,333,335,338,342],{"class":60,"line":334},10,[203,336,337],{"class":212},"processing.run(",[203,339,341],{"class":340},"sU2Wk","\"native:reclassifybytable\"",[203,343,344],{"class":212},", {\n",[203,346,348,351,354,357],{"class":60,"line":347},11,[203,349,350],{"class":340},"    \"INPUT_RASTER\"",[203,352,353],{"class":212},": ",[203,355,356],{"class":340},"\"\u002Fdata\u002Foutput\u002Fslope_deg.tif\"",[203,358,359],{"class":212},",\n",[203,361,363,366,368,370],{"class":60,"line":362},12,[203,364,365],{"class":340},"    \"RASTER_BAND\"",[203,367,353],{"class":212},[203,369,251],{"class":226},[203,371,359],{"class":212},[203,373,375,378,380,382],{"class":60,"line":374},13,[203,376,377],{"class":340},"    \"TABLE\"",[203,379,353],{"class":212},[203,381,227],{"class":226},[203,383,359],{"class":212},[203,385,387,390,392,395,398],{"class":60,"line":386},14,[203,388,389],{"class":340},"    \"NO_DATA\"",[203,391,353],{"class":212},[203,393,394],{"class":208},"-",[203,396,397],{"class":226},"9999",[203,399,359],{"class":212},[203,401,403,406,408,410],{"class":60,"line":402},15,[203,404,405],{"class":340},"    \"RANGE_BOUNDARIES\"",[203,407,353],{"class":212},[203,409,46],{"class":226},[203,411,359],{"class":212},[203,413,415,418,420,423],{"class":60,"line":414},16,[203,416,417],{"class":340},"    \"NODATA_FOR_MISSING\"",[203,419,353],{"class":212},[203,421,422],{"class":226},"True",[203,424,359],{"class":212},[203,426,428,431,433,435,438],{"class":60,"line":427},17,[203,429,430],{"class":340},"    \"DATA_TYPE\"",[203,432,353],{"class":212},[203,434,251],{"class":226},[203,436,437],{"class":212},",                     ",[203,439,440],{"class":257},"# Byte\n",[203,442,444,447,449,452],{"class":60,"line":443},18,[203,445,446],{"class":340},"    \"OUTPUT\"",[203,448,353],{"class":212},[203,450,451],{"class":340},"\"\u002Fdata\u002Foutput\u002Fslope_class.tif\"",[203,453,359],{"class":212},[203,455,457],{"class":60,"line":456},19,[203,458,459],{"class":212},"})\n",[14,461,462,465,466,469,470,473,474,477,478,481,482,485,486,489],{},[171,463,464],{},"Breakdown:"," Laying the table out three values per line with a comment is the difference between reviewable and unreviewable; QGIS reads it as a flat sequence regardless. ",[182,467,468],{},"NO_DATA"," sets the output's nodata value, which should be a number no class uses — with a Byte output, ",[182,471,472],{},"255"," is the conventional choice and ",[182,475,476],{},"-9999"," will not fit, so the two settings must agree. ",[182,479,480],{},"DATA_TYPE: 1"," is Byte, correct for four classes and eight times smaller than the Float32 default. ",[182,483,484],{},"NODATA_FOR_MISSING: True"," makes unmatched values nodata explicitly; with ",[182,487,488],{},"False"," they pass through unchanged, which mixes raw slope degrees into a class raster and is almost never what anyone wants.",[160,491,493],{"id":492},"the-boundary-rule","The boundary rule",[14,495,496,499],{},[182,497,498],{},"RANGE_BOUNDARIES"," takes four values, and picking the wrong one puts every boundary value in the neighbouring class.",[14,501,502,504,505,507,508,510,511,513,514,516,517,519,520,524],{},[182,503,46],{}," is min \u003C value ≤ max, ",[182,506,251],{}," is min ≤ value \u003C max, ",[182,509,273],{}," is min \u003C value \u003C max, and ",[182,512,294],{}," is min ≤ value ≤ max. The first two are the sensible choices because they tile the axis with no gaps and no overlaps. ",[182,515,273],{}," leaves every boundary value unmatched, so a slope of exactly 5 degrees becomes nodata. ",[182,518,294],{}," matches boundary values in ",[521,522,523],"em",{},"both"," adjacent ranges, and the earlier one in the table wins — which works but makes the table order significant in a way that is easy to forget.",[194,526,528],{"className":196,"code":527,"language":198,"meta":199,"style":199},"BOUNDARY_MIN_EXCLUSIVE = 0     # min \u003C  v \u003C= max\nBOUNDARY_MAX_EXCLUSIVE = 1     # min \u003C= v \u003C  max\n",[182,529,530,543],{"__ignoreMap":199},[203,531,532,535,537,540],{"class":60,"line":205},[203,533,534],{"class":226},"BOUNDARY_MIN_EXCLUSIVE",[203,536,230],{"class":208},[203,538,539],{"class":226}," 0",[203,541,542],{"class":257},"     # min \u003C  v \u003C= max\n",[203,544,545,548,550,553],{"class":60,"line":216},[203,546,547],{"class":226},"BOUNDARY_MAX_EXCLUSIVE",[203,549,230],{"class":208},[203,551,552],{"class":226}," 1",[203,554,555],{"class":257},"     # min \u003C= v \u003C  max\n",[14,557,558,560,561,563],{},[171,559,464],{}," Naming the constants once, at the top of a script, removes the need to remember which integer is which every time a table is written. Convention in most classification work is ",[182,562,251],{}," — lower bound inclusive — because it reads the way people describe classes out loud: \"five to fifteen\" usually means from five up to but not including fifteen.",[160,565,567],{"id":566},"values-the-table-does-not-mention","Values the table does not mention",[14,569,570,571,573],{},"Anything outside every range is unmatched, and with ",[182,572,484],{}," becomes nodata. On a raster whose real minimum is slightly below your first range — a slope raster with a few cells at −0.0001 from floating-point noise — that produces a scatter of holes.",[14,575,576],{},"The robust fix is to extend the outer ranges beyond the data.",[194,578,580],{"className":196,"code":579,"language":198,"meta":199,"style":199},"TABLE = [\n    float(\"-inf\"),  5,  1,\n    5,             15,  2,\n    15,            30,  3,\n    30, float(\"inf\"),   4,\n]\n",[182,581,582,590,612,627,642,663],{"__ignoreMap":199},[203,583,584,586,588],{"class":60,"line":205},[203,585,227],{"class":226},[203,587,230],{"class":208},[203,589,233],{"class":212},[203,591,592,595,598,601,604,606,608,610],{"class":60,"line":216},[203,593,594],{"class":226},"    float",[203,596,597],{"class":212},"(",[203,599,600],{"class":340},"\"-inf\"",[203,602,603],{"class":212},"),  ",[203,605,245],{"class":226},[203,607,248],{"class":212},[203,609,251],{"class":226},[203,611,359],{"class":212},[203,613,614,616,619,621,623,625],{"class":60,"line":223},[203,615,264],{"class":226},[203,617,618],{"class":212},",             ",[203,620,120],{"class":226},[203,622,248],{"class":212},[203,624,273],{"class":226},[203,626,359],{"class":212},[203,628,629,631,634,636,638,640],{"class":60,"line":236},[203,630,284],{"class":226},[203,632,633],{"class":212},",            ",[203,635,124],{"class":226},[203,637,248],{"class":212},[203,639,294],{"class":226},[203,641,359],{"class":212},[203,643,644,646,648,651,653,656,659,661],{"class":60,"line":261},[203,645,305],{"class":226},[203,647,287],{"class":212},[203,649,650],{"class":226},"float",[203,652,597],{"class":212},[203,654,655],{"class":340},"\"inf\"",[203,657,658],{"class":212},"),   ",[203,660,315],{"class":226},[203,662,359],{"class":212},[203,664,665],{"class":60,"line":281},[203,666,326],{"class":212},[14,668,669,671],{},[171,670,464],{}," The algorithm accepts infinities in the table, which is much safer than picking a number you believe is beyond the data. It also documents intent: the first class is \"everything up to 5\", not \"everything between an arbitrary lower bound and 5\". Where an outer range genuinely should be bounded — because values beyond it are errors — leave it bounded and let them become nodata deliberately, then count them so the decision is visible.",[14,673,674],{},[29,675,678,681,684,687,690,696,702,706,710,713,717,720,723,726,730,734,738,740,743,746],{"viewBox":676,"role":32,"ariaLabel":677,"xmlns":34},"0 0 760 292","The four range boundary conventions shown against a boundary value, with two producing a clean tiling of the axis and two producing either gaps or overlaps",[36,679,680],{},"The four boundary conventions",[40,682,683],{},"With min exclusive and max inclusive, or min inclusive and max exclusive, the ranges tile the axis with no value falling in two classes or none. With both bounds exclusive, boundary values match no range and become nodata. With both inclusive, boundary values match two ranges and the earlier table entry wins.",[44,685],{"x":46,"y":46,"width":47,"height":686,"fill":49},"292",[51,688,689],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Only two of the four tile the axis cleanly",[44,691],{"x":692,"y":693,"width":694,"height":132,"rx":695,"fill":73,"stroke":74,"style":103},"24","52","352","10",[51,697,701],{"x":698,"y":699,"style":700,"fill":74,"textAnchor":57},"200","78","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:monospace","0 — min \u003C v ≤ max",[51,703,705],{"x":698,"y":704,"style":157,"fill":65,"textAnchor":57},"102","5 lands in the lower class",[51,707,709],{"x":698,"y":708,"style":157,"fill":65,"textAnchor":57},"126","no gaps, no overlaps",[44,711],{"x":712,"y":693,"width":694,"height":132,"rx":695,"fill":73,"stroke":74,"style":103},"384",[51,714,716],{"x":715,"y":699,"style":700,"fill":74,"textAnchor":57},"560","1 — min ≤ v \u003C max",[51,718,719],{"x":715,"y":704,"style":157,"fill":65,"textAnchor":57},"5 lands in the upper class",[51,721,722],{"x":715,"y":708,"style":157,"fill":65,"textAnchor":57},"matches how classes are spoken",[44,724],{"x":692,"y":725,"width":694,"height":132,"rx":695,"fill":84,"stroke":102,"style":103},"166",[51,727,729],{"x":698,"y":728,"style":700,"fill":102,"textAnchor":57},"192","2 — min \u003C v \u003C max",[51,731,733],{"x":698,"y":732,"style":157,"fill":65,"textAnchor":57},"216","5 matches nothing",[51,735,737],{"x":698,"y":736,"style":157,"fill":102,"textAnchor":57},"240","a hole at every boundary",[44,739],{"x":712,"y":725,"width":694,"height":132,"rx":695,"fill":84,"stroke":85,"style":103},[51,741,742],{"x":715,"y":728,"style":700,"fill":85,"textAnchor":57},"3 — min ≤ v ≤ max",[51,744,745],{"x":715,"y":732,"style":157,"fill":65,"textAnchor":57},"5 matches two ranges",[51,747,748],{"x":715,"y":736,"style":157,"fill":85,"textAnchor":57},"table order silently decides",[160,750,752],{"id":751},"building-a-table-from-class-breaks","Building a table from class breaks",[14,754,755],{},"Classification methods that produce breaks — quantiles, natural breaks, equal intervals — give a list of boundaries rather than a table of ranges. Converting between the two is three lines and worth having as a helper.",[194,757,759],{"className":196,"code":758,"language":198,"meta":199,"style":199},"def table_from_breaks(breaks, first_class=1, unbounded=True):\n    \"\"\"[5, 15, 30] -> a flat reclassify table with 4 classes.\"\"\"\n    edges = list(breaks)\n    lower = float(\"-inf\") if unbounded else edges[0]\n    table = []\n    code = first_class\n    for edge in edges:\n        table += [lower, edge, code]\n        lower, code = edge, code + 1\n    table += [lower, float(\"inf\") if unbounded else lower, code]\n    return table\n",[182,760,761,788,793,806,839,849,859,873,884,900,926],{"__ignoreMap":199},[203,762,763,766,770,773,776,778,781,783,785],{"class":60,"line":205},[203,764,765],{"class":208},"def",[203,767,769],{"class":768},"svObZ"," table_from_breaks",[203,771,772],{"class":212},"(breaks, first_class",[203,774,775],{"class":208},"=",[203,777,251],{"class":226},[203,779,780],{"class":212},", unbounded",[203,782,775],{"class":208},[203,784,422],{"class":226},[203,786,787],{"class":212},"):\n",[203,789,790],{"class":60,"line":216},[203,791,792],{"class":340},"    \"\"\"[5, 15, 30] -> a flat reclassify table with 4 classes.\"\"\"\n",[203,794,795,798,800,803],{"class":60,"line":223},[203,796,797],{"class":212},"    edges ",[203,799,775],{"class":208},[203,801,802],{"class":226}," list",[203,804,805],{"class":212},"(breaks)\n",[203,807,808,811,813,816,818,820,823,826,829,832,835,837],{"class":60,"line":236},[203,809,810],{"class":212},"    lower ",[203,812,775],{"class":208},[203,814,815],{"class":226}," float",[203,817,597],{"class":212},[203,819,600],{"class":340},[203,821,822],{"class":212},") ",[203,824,825],{"class":208},"if",[203,827,828],{"class":212}," unbounded ",[203,830,831],{"class":208},"else",[203,833,834],{"class":212}," edges[",[203,836,46],{"class":226},[203,838,326],{"class":212},[203,840,841,844,846],{"class":60,"line":261},[203,842,843],{"class":212},"    table ",[203,845,775],{"class":208},[203,847,848],{"class":212}," []\n",[203,850,851,854,856],{"class":60,"line":281},[203,852,853],{"class":212},"    code ",[203,855,775],{"class":208},[203,857,858],{"class":212}," first_class\n",[203,860,861,864,867,870],{"class":60,"line":302},[203,862,863],{"class":208},"    for",[203,865,866],{"class":212}," edge ",[203,868,869],{"class":208},"in",[203,871,872],{"class":212}," edges:\n",[203,874,875,878,881],{"class":60,"line":323},[203,876,877],{"class":212},"        table ",[203,879,880],{"class":208},"+=",[203,882,883],{"class":212}," [lower, edge, code]\n",[203,885,886,889,891,894,897],{"class":60,"line":329},[203,887,888],{"class":212},"        lower, code ",[203,890,775],{"class":208},[203,892,893],{"class":212}," edge, code ",[203,895,896],{"class":208},"+",[203,898,899],{"class":226}," 1\n",[203,901,902,904,906,909,911,913,915,917,919,921,923],{"class":60,"line":334},[203,903,843],{"class":212},[203,905,880],{"class":208},[203,907,908],{"class":212}," [lower, ",[203,910,650],{"class":226},[203,912,597],{"class":212},[203,914,655],{"class":340},[203,916,822],{"class":212},[203,918,825],{"class":208},[203,920,828],{"class":212},[203,922,831],{"class":208},[203,924,925],{"class":212}," lower, code]\n",[203,927,928,931],{"class":60,"line":347},[203,929,930],{"class":208},"    return",[203,932,933],{"class":212}," table\n",[14,935,936,938,939,942,943,945,946,949,950,954],{},[171,937,464],{}," Producing one more class than there are breaks is the arithmetic people get wrong by hand — three breaks make four classes, not three. Defaulting ",[182,940,941],{},"unbounded"," to ",[182,944,422],{}," gives the safe behaviour discussed above, with the option of a bounded table when values outside the range genuinely are errors. Feeding this from ",[182,947,948],{},"QgsClassificationQuantile().classes()"," on the equivalent vector data keeps a raster classification and a ",[21,951,953],{"href":952},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","choropleth"," using literally the same breaks, which matters whenever the two appear on the same page.",[14,956,957,958,960],{},"Keeping the breaks and the class labels together in one structure is the other half of the discipline, because a class raster with no record of what ",[182,959,294],{}," means is nearly useless six months later:",[194,962,964],{"className":196,"code":963,"language":198,"meta":199,"style":199},"CLASSES = {1: \"gentle (≤5°)\", 2: \"moderate (5–15°)\", 3: \"steep (15–30°)\", 4: \"unbuildable (>30°)\"}\n",[182,965,966],{"__ignoreMap":199},[203,967,968,971,973,976,978,980,983,985,987,989,992,994,996,998,1001,1003,1005,1007,1010],{"class":60,"line":205},[203,969,970],{"class":226},"CLASSES",[203,972,230],{"class":208},[203,974,975],{"class":212}," {",[203,977,251],{"class":226},[203,979,353],{"class":212},[203,981,982],{"class":340},"\"gentle (≤5°)\"",[203,984,287],{"class":212},[203,986,273],{"class":226},[203,988,353],{"class":212},[203,990,991],{"class":340},"\"moderate (5–15°)\"",[203,993,287],{"class":212},[203,995,294],{"class":226},[203,997,353],{"class":212},[203,999,1000],{"class":340},"\"steep (15–30°)\"",[203,1002,287],{"class":212},[203,1004,315],{"class":226},[203,1006,353],{"class":212},[203,1008,1009],{"class":340},"\"unbuildable (>30°)\"",[203,1011,1012],{"class":212},"}\n",[14,1014,1015,1017,1018,1021,1022,1026],{},[171,1016,464],{}," Writing this dictionary next to the breaks lets the same source drive the reclassification, the renderer's category labels and the legend text, so the three can never disagree. It is also what a ",[182,1019,1020],{},".qmd"," metadata sidecar should contain, as described in ",[21,1023,1025],{"href":1024},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fread-and-write-layer-metadata-pyqgis\u002F","reading and writing layer metadata",".",[160,1028,1030],{"id":1029},"when-the-table-is-data","When the table is data",[14,1032,1033,1036],{},[182,1034,1035],{},"native:reclassifybylayer"," takes the ranges from a table layer instead of a parameter, which is the right shape when the classification is maintained by somebody who does not edit Python.",[194,1038,1040],{"className":196,"code":1039,"language":198,"meta":199,"style":199},"processing.run(\"native:reclassifybylayer\", {\n    \"INPUT_RASTER\": \"\u002Fdata\u002Flandcover.tif\",\n    \"RASTER_BAND\": 1,\n    \"INPUT_TABLE\": \"\u002Fdata\u002Flookup\u002Fhabitat_scores.csv\",\n    \"MIN_FIELD\": \"code_min\",\n    \"MAX_FIELD\": \"code_max\",\n    \"VALUE_FIELD\": \"score\",\n    \"RANGE_BOUNDARIES\": 3,\n    \"NODATA_FOR_MISSING\": True,\n    \"DATA_TYPE\": 1,\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fhabitat.tif\",\n})\n",[182,1041,1042,1051,1062,1072,1084,1096,1108,1120,1130,1140,1150,1161],{"__ignoreMap":199},[203,1043,1044,1046,1049],{"class":60,"line":205},[203,1045,337],{"class":212},[203,1047,1048],{"class":340},"\"native:reclassifybylayer\"",[203,1050,344],{"class":212},[203,1052,1053,1055,1057,1060],{"class":60,"line":216},[203,1054,350],{"class":340},[203,1056,353],{"class":212},[203,1058,1059],{"class":340},"\"\u002Fdata\u002Flandcover.tif\"",[203,1061,359],{"class":212},[203,1063,1064,1066,1068,1070],{"class":60,"line":223},[203,1065,365],{"class":340},[203,1067,353],{"class":212},[203,1069,251],{"class":226},[203,1071,359],{"class":212},[203,1073,1074,1077,1079,1082],{"class":60,"line":236},[203,1075,1076],{"class":340},"    \"INPUT_TABLE\"",[203,1078,353],{"class":212},[203,1080,1081],{"class":340},"\"\u002Fdata\u002Flookup\u002Fhabitat_scores.csv\"",[203,1083,359],{"class":212},[203,1085,1086,1089,1091,1094],{"class":60,"line":261},[203,1087,1088],{"class":340},"    \"MIN_FIELD\"",[203,1090,353],{"class":212},[203,1092,1093],{"class":340},"\"code_min\"",[203,1095,359],{"class":212},[203,1097,1098,1101,1103,1106],{"class":60,"line":281},[203,1099,1100],{"class":340},"    \"MAX_FIELD\"",[203,1102,353],{"class":212},[203,1104,1105],{"class":340},"\"code_max\"",[203,1107,359],{"class":212},[203,1109,1110,1113,1115,1118],{"class":60,"line":302},[203,1111,1112],{"class":340},"    \"VALUE_FIELD\"",[203,1114,353],{"class":212},[203,1116,1117],{"class":340},"\"score\"",[203,1119,359],{"class":212},[203,1121,1122,1124,1126,1128],{"class":60,"line":323},[203,1123,405],{"class":340},[203,1125,353],{"class":212},[203,1127,294],{"class":226},[203,1129,359],{"class":212},[203,1131,1132,1134,1136,1138],{"class":60,"line":329},[203,1133,417],{"class":340},[203,1135,353],{"class":212},[203,1137,422],{"class":226},[203,1139,359],{"class":212},[203,1141,1142,1144,1146,1148],{"class":60,"line":334},[203,1143,430],{"class":340},[203,1145,353],{"class":212},[203,1147,251],{"class":226},[203,1149,359],{"class":212},[203,1151,1152,1154,1156,1159],{"class":60,"line":347},[203,1153,446],{"class":340},[203,1155,353],{"class":212},[203,1157,1158],{"class":340},"\"\u002Fdata\u002Foutput\u002Fhabitat.tif\"",[203,1160,359],{"class":212},[203,1162,1163],{"class":60,"line":362},[203,1164,459],{"class":212},[14,1166,1167,1169,1170,1173],{},[171,1168,464],{}," For discrete input codes — land cover classes rather than a continuous surface — the min and max fields hold the same value and ",[182,1171,1172],{},"RANGE_BOUNDARIES: 3"," (both inclusive) is correct, because you are matching exact codes rather than ranges. A CSV read this way must have numeric columns; a text column of codes silently produces no matches, and the result is an entirely nodata raster. Keeping the lookup in version control alongside the script gives the best of both: reviewable history and an editable file.",[160,1175,1177],{"id":1176},"choosing-the-output-type","Choosing the output type",[14,1179,1180],{},"The default Float32 is right for almost nothing after a reclassification.",[14,1182,1183,1184,1186],{},"Four classes fit in a Byte, which is a quarter the size of Int16 and an eighth of Float32 — meaningful on a national raster. Byte holds 0–255, so nodata must be a value inside that range and outside the class codes; ",[182,1185,472],{}," is conventional. Where scores are fractional — a suitability index from 0 to 1 — Float32 is correct and the size cost is unavoidable. What is never correct is an integer type holding fractional scores, which truncates every score to 0 or 1 and produces a map that looks decisive and is wrong.",[194,1188,1190],{"className":196,"code":1189,"language":198,"meta":199,"style":199},"from qgis.core import QgsRasterLayer\n\nout = QgsRasterLayer(\"\u002Fdata\u002Foutput\u002Fslope_class.tif\", \"classes\")\nprovider = out.dataProvider()\nprint(provider.dataType(1), provider.sourceNoDataValue(1))\nprint(provider.bandStatistics(1).minimumValue, provider.bandStatistics(1).maximumValue)\n",[182,1191,1192,1205,1209,1229,1239,1257],{"__ignoreMap":199},[203,1193,1194,1197,1200,1202],{"class":60,"line":205},[203,1195,1196],{"class":208},"from",[203,1198,1199],{"class":212}," qgis.core ",[203,1201,209],{"class":208},[203,1203,1204],{"class":212}," QgsRasterLayer\n",[203,1206,1207],{"class":60,"line":216},[203,1208,220],{"emptyLinePlaceholder":219},[203,1210,1211,1214,1216,1219,1221,1223,1226],{"class":60,"line":223},[203,1212,1213],{"class":212},"out ",[203,1215,775],{"class":208},[203,1217,1218],{"class":212}," QgsRasterLayer(",[203,1220,451],{"class":340},[203,1222,287],{"class":212},[203,1224,1225],{"class":340},"\"classes\"",[203,1227,1228],{"class":212},")\n",[203,1230,1231,1234,1236],{"class":60,"line":236},[203,1232,1233],{"class":212},"provider ",[203,1235,775],{"class":208},[203,1237,1238],{"class":212}," out.dataProvider()\n",[203,1240,1241,1244,1247,1249,1252,1254],{"class":60,"line":261},[203,1242,1243],{"class":226},"print",[203,1245,1246],{"class":212},"(provider.dataType(",[203,1248,251],{"class":226},[203,1250,1251],{"class":212},"), provider.sourceNoDataValue(",[203,1253,251],{"class":226},[203,1255,1256],{"class":212},"))\n",[203,1258,1259,1261,1264,1266,1269,1271],{"class":60,"line":281},[203,1260,1243],{"class":226},[203,1262,1263],{"class":212},"(provider.bandStatistics(",[203,1265,251],{"class":226},[203,1267,1268],{"class":212},").minimumValue, provider.bandStatistics(",[203,1270,251],{"class":226},[203,1272,1273],{"class":212},").maximumValue)\n",[14,1275,1276,1278,1279,1281,1282,1285,1286,1288],{},[171,1277,464],{}," Checking the written file rather than trusting the parameters catches the mismatch between ",[182,1280,468],{}," and ",[182,1283,1284],{},"DATA_TYPE"," — asking for ",[182,1287,476],{}," on a Byte output silently stores something else, and the nodata mask then does not work. Comparing the statistics against the expected class codes is a two-line sanity check that catches an off-by-one in the table.",[160,1290,1292],{"id":1291},"qgis-version-compatibility","QGIS version compatibility",[14,1294,1295,1296,1298],{},"The examples target ",[171,1297,173],{}," (Python 3.12).",[1300,1301,1302,1318],"table",{},[1303,1304,1305],"thead",{},[1306,1307,1308,1312,1315],"tr",{},[1309,1310,1311],"th",{},"QGIS version",[1309,1313,1314],{},"Python",[1309,1316,1317],{},"Notes",[1319,1320,1321,1333,1344,1356,1367],"tbody",{},[1306,1322,1323,1327,1330],{},[1324,1325,1326],"td",{},"3.16 LTR",[1324,1328,1329],{},"3.7",[1324,1331,1332],{},"Both reclassify algorithms present with the same parameter names.",[1306,1334,1335,1338,1341],{},[1324,1336,1337],{},"3.22 LTR",[1324,1339,1340],{},"3.9",[1324,1342,1343],{},"Infinite bounds accepted in the table parameter.",[1306,1345,1346,1349,1351],{},[1324,1347,1348],{},"3.28 LTR",[1324,1350,1340],{},[1324,1352,1353,1355],{},[182,1354,1284],{}," codes stable across the raster algorithms.",[1306,1357,1358,1361,1364],{},[1324,1359,1360],{},"3.34 LTR",[1324,1362,1363],{},"3.12",[1324,1365,1366],{},"Baseline for this page.",[1306,1368,1369,1372,1374],{},[1324,1370,1371],{},"3.40+",[1324,1373,1363],{},[1324,1375,1376],{},"Improved reporting of unmatched value counts in the algorithm log.",[160,1378,1380],{"id":1379},"troubleshooting","Troubleshooting",[165,1382,1383,1392,1409,1420,1426,1437],{},[168,1384,1385,1388,1389,1026],{},[171,1386,1387],{},"The output is entirely nodata."," No value matched. Check the table covers the raster's real range with ",[182,1390,1391],{},"bandStatistics(1)",[168,1393,1394,1397,1398,1400,1401,1403,1404,1406,1407,1026],{},[171,1395,1396],{},"Holes at every class boundary."," ",[182,1399,498],{}," is ",[182,1402,273],{},", which excludes both bounds. Use ",[182,1405,46],{}," or ",[182,1408,251],{},[168,1410,1411,1397,1414,1400,1417,1419],{},[171,1412,1413],{},"Raw values appear alongside classes.",[182,1415,1416],{},"NODATA_FOR_MISSING",[182,1418,488],{},", so unmatched values pass through.",[168,1421,1422,1425],{},[171,1423,1424],{},"A scatter of holes at the extremes."," The outer ranges do not extend far enough. Use infinities.",[168,1427,1428,1397,1431,1433,1434,1436],{},[171,1429,1430],{},"Nodata is not being honoured downstream.",[182,1432,468],{}," does not fit ",[182,1435,1284],{},". Check the written file's actual nodata value.",[168,1438,1439,1442],{},[171,1440,1441],{},"All scores became 0 or 1."," An integer output type truncated fractional values. Use Float32 for scores.",[160,1444,1446],{"id":1445},"conclusion","Conclusion",[14,1448,1449,1450,1406,1452,1454,1455,942,1457,1459],{},"Write the table three values per line with comments, extend the outer ranges to infinity, choose ",[182,1451,46],{},[182,1453,251],{}," for the boundary rule and say which in a comment, set ",[182,1456,1416],{},[182,1458,422],{},", and pick the smallest data type that holds the classes with room for a nodata value. Then read the written file back and check its statistics.",[160,1461,1463],{"id":1462},"frequently-asked-questions","Frequently Asked Questions",[14,1465,1466,1469,1470,1474],{},[171,1467,1468],{},"Can I reclassify with an expression instead?","\nYes — the ",[21,1471,1473],{"href":1472},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fraster-calculator-pyqgis\u002F","raster calculator"," handles simple cases as sums of boolean products. Reclassification is clearer for more than two or three classes, and much clearer to review.",[14,1476,1477,1480,1481,1484,1485,1488],{},[171,1478,1479],{},"How do I reclassify several bands?","\nOne call per band, each writing its own output, then combine with ",[182,1482,1483],{},"gdal:merge"," using ",[182,1486,1487],{},"SEPARATE: True"," to stack them back into a multi-band file.",[14,1490,1491,1494,1495,1026],{},[171,1492,1493],{},"Does reclassification change the cell size or extent?","\nNo. It is a per-cell value mapping, so the grid is identical to the input. That makes reclassified rasters directly stackable with the original in ",[21,1496,1498],{"href":1497},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fgenerate-slope-aspect-hillshade-pyqgis\u002F","cell statistics",[14,1500,1501,1504,1505,1508,1509,1513],{},[171,1502,1503],{},"How do I get class areas afterwards?","\nReport unique values with ",[182,1506,1507],{},"native:rasterlayeruniquevaluesreport",", which gives a pixel count and area per class. For per-zone breakdowns use ",[21,1510,1512],{"href":1511},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis\u002F","zonal statistics"," with the majority and variety statistics.",[160,1515,1517],{"id":1516},"related","Related",[165,1519,1520,1525,1530,1535,1541],{},[168,1521,1522,1524],{},[21,1523,24],{"href":23}," — the guide this recipe belongs to",[168,1526,1527],{},[21,1528,1529],{"href":1511},"Zonal Statistics in PyQGIS",[168,1531,1532],{},[21,1533,1534],{"href":1472},"Use the Raster Calculator in PyQGIS",[168,1536,1537],{},[21,1538,1540],{"href":1539},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fcalculate-raster-statistics-pyqgis\u002F","Calculate Raster Statistics in PyQGIS",[168,1542,1543],{},[21,1544,1545],{"href":1497},"Generate Slope, Aspect and Hillshade in PyQGIS",[1547,1548,1549],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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":199,"searchDepth":216,"depth":216,"links":1551},[1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563],{"id":162,"depth":216,"text":163},{"id":188,"depth":216,"text":189},{"id":492,"depth":216,"text":493},{"id":566,"depth":216,"text":567},{"id":751,"depth":216,"text":752},{"id":1029,"depth":216,"text":1030},{"id":1176,"depth":216,"text":1177},{"id":1291,"depth":216,"text":1292},{"id":1379,"depth":216,"text":1380},{"id":1445,"depth":216,"text":1446},{"id":1462,"depth":216,"text":1463},{"id":1516,"depth":216,"text":1517},"Turn continuous rasters into classes with native:reclassifybytable and reclassifybylayer — building the flat table, range boundary rules, nodata handling, and choosing an output data type.","md",{"slug":1567,"type":1568,"breadcrumb":1569,"datePublished":1570,"dateModified":1570},"reclassify-raster-values-pyqgis","article","Reclassify Raster","2026-08-27","\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Freclassify-raster-values-pyqgis",{"title":5,"description":1564},"spatial-data-processing-automation\u002Fraster-analysis-workflows\u002Freclassify-raster-values-pyqgis\u002Findex","2fRzTIzc44NTv3iDGxjSoKY57zSy4IGjBORMBdTnw68",1787823363821]