[{"data":1,"prerenderedAt":1558},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis":3},{"id":4,"title":5,"body":6,"description":1547,"extension":1548,"meta":1549,"navigation":262,"path":1554,"seo":1555,"stem":1556,"__hash__":1557},"docs\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis\u002Findex.md","Sample Raster Values at Points in PyQGIS",{"type":7,"value":8,"toc":1534},"minimark",[9,13,17,26,200,205,231,235,343,364,367,371,514,540,544,550,646,661,665,668,820,834,919,923,926,1154,1167,1182,1295,1299,1305,1376,1380,1426,1430,1440,1444,1450,1456,1466,1472,1478,1491,1497,1501,1530],[10,11,5],"h1",{"id":12},"sample-raster-values-at-points-in-pyqgis",[14,15,16],"p",{},"\"What is the elevation at each of my survey stations?\" is one of the most common questions in applied GIS, and it has two very different answers in PyQGIS depending on how many points there are. For a whole layer, one Processing algorithm does the job and writes the values into new fields. For a handful of coordinates, a direct provider call returns a number in microseconds. Using the wrong one turns a two-second job into a twenty-minute one.",[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 both routes, the CRS mismatch that silently returns nothing, nodata handling, and when a point sample should really have been a zonal statistic.",[14,27,28],{},[29,30,35,39,43,50,59,96,99,105,112,115,119,123,126,129,132,136,138,141,144,151,155,159,163,170,175,179,182,186,190,192,196],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 264","img","A point layer overlaid on a raster grid, showing that each point falls inside one cell and takes that cell's value, with one point falling on a nodata cell and one outside the raster extent","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"What point sampling actually reads",[40,41,42],"desc",{},"Points are drawn over a raster grid. Each point takes the value of the single cell it falls inside, regardless of how close it is to a boundary. One point falls on a nodata cell and returns no value. One point lies outside the raster extent and returns an invalid result rather than zero.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","264","#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","One point, one cell — proximity to the edge changes nothing",[60,61,65,71,74,77,80,83,85,87,90,92,94],"g",{"stroke":62,"style":63,"fill":64},"#59645f","stroke-width:1.5","#fffdf7",[44,66],{"x":67,"y":68,"width":69,"height":70},"120","56","60","52",[44,72],{"x":73,"y":68,"width":69,"height":70},"180",[44,75],{"x":76,"y":68,"width":69,"height":70},"240",[44,78],{"x":79,"y":68,"width":69,"height":70},"300",[44,81],{"x":67,"y":82,"width":69,"height":70},"108",[44,84],{"x":76,"y":82,"width":69,"height":70},[44,86],{"x":79,"y":82,"width":69,"height":70},[44,88],{"x":67,"y":89,"width":69,"height":70},"160",[44,91],{"x":73,"y":89,"width":69,"height":70},[44,93],{"x":76,"y":89,"width":69,"height":70},[44,95],{"x":79,"y":89,"width":69,"height":70},[44,97],{"x":73,"y":82,"width":69,"height":70,"fill":98,"stroke":62,"style":63},"#efeadd",[51,100,104],{"x":101,"y":102,"style":103,"fill":62,"textAnchor":57},"210","139","text-anchor:middle;font-size:10px;font-family:sans-serif","nodata",[51,106,111],{"x":107,"y":108,"style":109,"fill":110,"textAnchor":57},"150","87","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","112",[51,113,114],{"x":101,"y":108,"style":109,"fill":110,"textAnchor":57},"118",[51,116,118],{"x":117,"y":108,"style":109,"fill":110,"textAnchor":57},"270","124",[51,120,122],{"x":121,"y":108,"style":109,"fill":110,"textAnchor":57},"330","131",[51,124,125],{"x":107,"y":102,"style":109,"fill":110,"textAnchor":57},"109",[51,127,128],{"x":117,"y":102,"style":109,"fill":110,"textAnchor":57},"127",[51,130,131],{"x":121,"y":102,"style":109,"fill":110,"textAnchor":57},"135",[51,133,135],{"x":107,"y":134,"style":109,"fill":110,"textAnchor":57},"191","104",[51,137,82],{"x":101,"y":134,"style":109,"fill":110,"textAnchor":57},[51,139,140],{"x":117,"y":134,"style":109,"fill":110,"textAnchor":57},"121",[51,142,143],{"x":121,"y":134,"style":109,"fill":110,"textAnchor":57},"130",[145,146],"circle",{"cx":147,"cy":148,"r":149,"fill":150},"176","80","5","#15803d",[145,152],{"cx":153,"cy":154,"r":149,"fill":150},"268","132",[145,156],{"cx":157,"cy":143,"r":149,"fill":158},"212","#b45309",[145,160],{"cx":161,"cy":107,"r":149,"fill":162},"404","#b91c1c",[44,164],{"x":165,"y":68,"width":166,"height":70,"rx":167,"fill":168,"stroke":150,"style":169},"420","320","6","#edf8e9","stroke-width:2",[51,171,174],{"x":172,"y":173,"style":109,"fill":110,"textAnchor":57},"580","78","inside a valid cell — returns that cell's value",[51,176,178],{"x":172,"y":177,"style":109,"fill":150,"textAnchor":57},"96","112, even though 118 is two pixels away",[44,180],{"x":165,"y":114,"width":166,"height":70,"rx":167,"fill":181,"stroke":158,"style":169},"#fdf2e2",[51,183,185],{"x":172,"y":184,"style":109,"fill":110,"textAnchor":57},"140","inside a nodata cell — returns no value",[51,187,189],{"x":172,"y":188,"style":109,"fill":158,"textAnchor":57},"158","the field is NULL, not zero",[44,191],{"x":165,"y":73,"width":166,"height":70,"rx":167,"fill":64,"stroke":162,"style":169},[51,193,195],{"x":172,"y":194,"style":109,"fill":110,"textAnchor":57},"202","outside the raster extent — invalid",[51,197,199],{"x":172,"y":198,"style":109,"fill":162,"textAnchor":57},"220","check the validity flag, never the value",[201,202,204],"h2",{"id":203},"prerequisites","Prerequisites",[206,207,208,216,223],"ul",{},[209,210,211,215],"li",{},[212,213,214],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[209,217,218,219,222],{},"A point layer and a raster. They do ",[212,220,221],{},"not"," have to share a CRS for the Processing route, but they do for the direct API.",[209,224,225,226,230],{},"Knowledge of the raster's nodata value — ",[227,228,229],"code",{},"provider.sourceNoDataValue(1)"," reports it.",[201,232,234],{"id":233},"sample-a-whole-layer","Sample a whole layer",[236,237,242],"pre",{"className":238,"code":239,"language":240,"meta":241,"style":241},"language-python shiki shiki-themes github-dark","import processing\n\nresult = processing.run(\"native:rastersampling\", {\n    \"INPUT\": \"\u002Fdata\u002Fstations.gpkg|layername=stations\",\n    \"RASTERCOPY\": \"\u002Fdata\u002Fdem.tif\",\n    \"COLUMN_PREFIX\": \"elev_\",\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fstations_elev.gpkg\",\n})\n","python","",[227,243,244,257,264,283,298,311,324,337],{"__ignoreMap":241},[245,246,249,253],"span",{"class":247,"line":248},"line",1,[245,250,252],{"class":251},"snl16","import",[245,254,256],{"class":255},"s95oV"," processing\n",[245,258,260],{"class":247,"line":259},2,[245,261,263],{"emptyLinePlaceholder":262},true,"\n",[245,265,267,270,273,276,280],{"class":247,"line":266},3,[245,268,269],{"class":255},"result ",[245,271,272],{"class":251},"=",[245,274,275],{"class":255}," processing.run(",[245,277,279],{"class":278},"sU2Wk","\"native:rastersampling\"",[245,281,282],{"class":255},", {\n",[245,284,286,289,292,295],{"class":247,"line":285},4,[245,287,288],{"class":278},"    \"INPUT\"",[245,290,291],{"class":255},": ",[245,293,294],{"class":278},"\"\u002Fdata\u002Fstations.gpkg|layername=stations\"",[245,296,297],{"class":255},",\n",[245,299,301,304,306,309],{"class":247,"line":300},5,[245,302,303],{"class":278},"    \"RASTERCOPY\"",[245,305,291],{"class":255},[245,307,308],{"class":278},"\"\u002Fdata\u002Fdem.tif\"",[245,310,297],{"class":255},[245,312,314,317,319,322],{"class":247,"line":313},6,[245,315,316],{"class":278},"    \"COLUMN_PREFIX\"",[245,318,291],{"class":255},[245,320,321],{"class":278},"\"elev_\"",[245,323,297],{"class":255},[245,325,327,330,332,335],{"class":247,"line":326},7,[245,328,329],{"class":278},"    \"OUTPUT\"",[245,331,291],{"class":255},[245,333,334],{"class":278},"\"\u002Fdata\u002Foutput\u002Fstations_elev.gpkg\"",[245,336,297],{"class":255},[245,338,340],{"class":247,"line":339},8,[245,341,342],{"class":255},"})\n",[14,344,345,348,349,352,353,356,357,359,360,363],{},[212,346,347],{},"Breakdown:"," The algorithm adds one field per raster band, named with the prefix and the band number — ",[227,350,351],{},"elev_1",", ",[227,354,355],{},"elev_2"," and so on — so a single-band DEM produces ",[227,358,351],{},". Points outside the raster or on nodata cells get a ",[227,361,362],{},"NULL"," rather than a fabricated value, which is the behaviour you want and the reason to prefer this over a hand-written loop. Reprojection between the point layer's CRS and the raster's happens automatically, using the project's transform context.",[14,365,366],{},"For several rasters — elevation, slope, rainfall — run it once per raster, feeding each output into the next. The fields accumulate, and one pass is added per raster rather than one pass per point.",[201,368,370],{"id":369},"sample-a-single-coordinate","Sample a single coordinate",[236,372,374],{"className":238,"code":373,"language":240,"meta":241,"style":241},"from qgis.core import QgsPointXY, QgsRasterLayer\n\nraster = QgsRasterLayer(\"\u002Fdata\u002Fdem.tif\", \"dem\")\nprovider = raster.dataProvider()\n\nvalue, ok = provider.sample(QgsPointXY(433250, 189400), 1)\nif ok:\n    print(f\"elevation {value:.1f} m\")\nelse:\n    print(\"no data at that location\")\n",[227,375,376,389,393,413,423,427,454,462,493,502],{"__ignoreMap":241},[245,377,378,381,384,386],{"class":247,"line":248},[245,379,380],{"class":251},"from",[245,382,383],{"class":255}," qgis.core ",[245,385,252],{"class":251},[245,387,388],{"class":255}," QgsPointXY, QgsRasterLayer\n",[245,390,391],{"class":247,"line":259},[245,392,263],{"emptyLinePlaceholder":262},[245,394,395,398,400,403,405,407,410],{"class":247,"line":266},[245,396,397],{"class":255},"raster ",[245,399,272],{"class":251},[245,401,402],{"class":255}," QgsRasterLayer(",[245,404,308],{"class":278},[245,406,352],{"class":255},[245,408,409],{"class":278},"\"dem\"",[245,411,412],{"class":255},")\n",[245,414,415,418,420],{"class":247,"line":285},[245,416,417],{"class":255},"provider ",[245,419,272],{"class":251},[245,421,422],{"class":255}," raster.dataProvider()\n",[245,424,425],{"class":247,"line":300},[245,426,263],{"emptyLinePlaceholder":262},[245,428,429,432,434,437,441,443,446,449,452],{"class":247,"line":313},[245,430,431],{"class":255},"value, ok ",[245,433,272],{"class":251},[245,435,436],{"class":255}," provider.sample(QgsPointXY(",[245,438,440],{"class":439},"sDLfK","433250",[245,442,352],{"class":255},[245,444,445],{"class":439},"189400",[245,447,448],{"class":255},"), ",[245,450,451],{"class":439},"1",[245,453,412],{"class":255},[245,455,456,459],{"class":247,"line":326},[245,457,458],{"class":251},"if",[245,460,461],{"class":255}," ok:\n",[245,463,464,467,470,473,476,479,482,485,488,491],{"class":247,"line":339},[245,465,466],{"class":439},"    print",[245,468,469],{"class":255},"(",[245,471,472],{"class":251},"f",[245,474,475],{"class":278},"\"elevation ",[245,477,478],{"class":439},"{",[245,480,481],{"class":255},"value",[245,483,484],{"class":251},":.1f",[245,486,487],{"class":439},"}",[245,489,490],{"class":278}," m\"",[245,492,412],{"class":255},[245,494,496,499],{"class":247,"line":495},9,[245,497,498],{"class":251},"else",[245,500,501],{"class":255},":\n",[245,503,505,507,509,512],{"class":247,"line":504},10,[245,506,466],{"class":439},[245,508,469],{"class":255},[245,510,511],{"class":278},"\"no data at that location\"",[245,513,412],{"class":255},[14,515,516,518,519,522,523,526,527,530,531,534,535,539],{},[212,517,347],{}," ",[227,520,521],{},"sample()"," takes a point ",[212,524,525],{},"in the raster's own CRS"," and a one-based band number, and returns a ",[227,528,529],{},"(value, valid)"," tuple. The validity flag is the part that matters: a point outside the extent or on a nodata cell returns ",[227,532,533],{},"(nan, False)",", and code that reads only the first element will happily record a NaN as an elevation. This call is a direct read with no algorithm overhead, which makes it the right tool inside an interactive map tool — see ",[21,536,538],{"href":537},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fidentify-feature-under-cursor-pyqgis\u002F","Identify the Feature Under the Cursor in PyQGIS"," for the canvas side of that pattern.",[201,541,543],{"id":542},"transform-coordinates-before-sampling","Transform coordinates before sampling",[14,545,546,547,549],{},"Unlike the algorithm, ",[227,548,521],{}," does no reprojection. Give it a point in the wrong CRS and it will return values from wherever those numbers happen to land — usually outside the extent, occasionally inside it, which is the worse outcome.",[236,551,553],{"className":238,"code":552,"language":240,"meta":241,"style":241},"from qgis.core import QgsCoordinateTransform, QgsCoordinateReferenceSystem, QgsProject\n\ntransform = QgsCoordinateTransform(\n    QgsCoordinateReferenceSystem(\"EPSG:4326\"),\n    raster.crs(),\n    QgsProject.instance(),\n)\n\npoint = transform.transform(QgsPointXY(-1.4701, 53.3811))\nvalue, ok = provider.sample(point, 1)\n",[227,554,555,566,570,580,591,596,601,605,609,633],{"__ignoreMap":241},[245,556,557,559,561,563],{"class":247,"line":248},[245,558,380],{"class":251},[245,560,383],{"class":255},[245,562,252],{"class":251},[245,564,565],{"class":255}," QgsCoordinateTransform, QgsCoordinateReferenceSystem, QgsProject\n",[245,567,568],{"class":247,"line":259},[245,569,263],{"emptyLinePlaceholder":262},[245,571,572,575,577],{"class":247,"line":266},[245,573,574],{"class":255},"transform ",[245,576,272],{"class":251},[245,578,579],{"class":255}," QgsCoordinateTransform(\n",[245,581,582,585,588],{"class":247,"line":285},[245,583,584],{"class":255},"    QgsCoordinateReferenceSystem(",[245,586,587],{"class":278},"\"EPSG:4326\"",[245,589,590],{"class":255},"),\n",[245,592,593],{"class":247,"line":300},[245,594,595],{"class":255},"    raster.crs(),\n",[245,597,598],{"class":247,"line":313},[245,599,600],{"class":255},"    QgsProject.instance(),\n",[245,602,603],{"class":247,"line":326},[245,604,412],{"class":255},[245,606,607],{"class":247,"line":339},[245,608,263],{"emptyLinePlaceholder":262},[245,610,611,614,616,619,622,625,627,630],{"class":247,"line":495},[245,612,613],{"class":255},"point ",[245,615,272],{"class":251},[245,617,618],{"class":255}," transform.transform(QgsPointXY(",[245,620,621],{"class":251},"-",[245,623,624],{"class":439},"1.4701",[245,626,352],{"class":255},[245,628,629],{"class":439},"53.3811",[245,631,632],{"class":255},"))\n",[245,634,635,637,639,642,644],{"class":247,"line":504},[245,636,431],{"class":255},[245,638,272],{"class":251},[245,640,641],{"class":255}," provider.sample(point, ",[245,643,451],{"class":439},[245,645,412],{"class":255},[14,647,648,650,651,655,656,660],{},[212,649,347],{}," Building the transform once outside the loop matters — constructing it per point is one of the classic profile-topping mistakes described in ",[21,652,654],{"href":653},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fprofile-slow-pyqgis-code\u002F","Profile Slow PyQGIS Code",". Passing the project as the third argument gives the transform access to the configured datum transformations, so the result matches what the canvas shows. The mechanics of transformation are covered in ",[21,657,659],{"href":658},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002F","Transform Point Coordinates in PyQGIS",".",[201,662,664],{"id":663},"sampling-many-points-efficiently","Sampling many points efficiently",[14,666,667],{},"When the points are already a layer, the algorithm wins. When they are generated — a grid, a route, model output — sample directly, and keep the loop lean.",[236,669,671],{"className":238,"code":670,"language":240,"meta":241,"style":241},"from qgis.core import QgsFeatureRequest\n\nrequest = QgsFeatureRequest().setSubsetOfAttributes([])\nsamples = []\n\nfor feature in points.getFeatures(request):\n    point = feature.geometry().asPoint()\n    value, ok = provider.sample(point, 1)\n    samples.append(value if ok else None)\n\nprint(f\"{sum(1 for s in samples if s is None)} points had no value\")\n",[227,672,673,684,688,698,708,712,726,736,749,766,770],{"__ignoreMap":241},[245,674,675,677,679,681],{"class":247,"line":248},[245,676,380],{"class":251},[245,678,383],{"class":255},[245,680,252],{"class":251},[245,682,683],{"class":255}," QgsFeatureRequest\n",[245,685,686],{"class":247,"line":259},[245,687,263],{"emptyLinePlaceholder":262},[245,689,690,693,695],{"class":247,"line":266},[245,691,692],{"class":255},"request ",[245,694,272],{"class":251},[245,696,697],{"class":255}," QgsFeatureRequest().setSubsetOfAttributes([])\n",[245,699,700,703,705],{"class":247,"line":285},[245,701,702],{"class":255},"samples ",[245,704,272],{"class":251},[245,706,707],{"class":255}," []\n",[245,709,710],{"class":247,"line":300},[245,711,263],{"emptyLinePlaceholder":262},[245,713,714,717,720,723],{"class":247,"line":313},[245,715,716],{"class":251},"for",[245,718,719],{"class":255}," feature ",[245,721,722],{"class":251},"in",[245,724,725],{"class":255}," points.getFeatures(request):\n",[245,727,728,731,733],{"class":247,"line":326},[245,729,730],{"class":255},"    point ",[245,732,272],{"class":251},[245,734,735],{"class":255}," feature.geometry().asPoint()\n",[245,737,738,741,743,745,747],{"class":247,"line":339},[245,739,740],{"class":255},"    value, ok ",[245,742,272],{"class":251},[245,744,641],{"class":255},[245,746,451],{"class":439},[245,748,412],{"class":255},[245,750,751,754,756,759,761,764],{"class":247,"line":495},[245,752,753],{"class":255},"    samples.append(value ",[245,755,458],{"class":251},[245,757,758],{"class":255}," ok ",[245,760,498],{"class":251},[245,762,763],{"class":439}," None",[245,765,412],{"class":255},[245,767,768],{"class":247,"line":504},[245,769,263],{"emptyLinePlaceholder":262},[245,771,773,776,778,780,783,786,788,790,793,796,798,801,803,805,808,810,813,815,818],{"class":247,"line":772},11,[245,774,775],{"class":439},"print",[245,777,469],{"class":255},[245,779,472],{"class":251},[245,781,782],{"class":278},"\"",[245,784,785],{"class":439},"{sum",[245,787,469],{"class":255},[245,789,451],{"class":439},[245,791,792],{"class":251}," for",[245,794,795],{"class":255}," s ",[245,797,722],{"class":251},[245,799,800],{"class":255}," samples ",[245,802,458],{"class":251},[245,804,795],{"class":255},[245,806,807],{"class":251},"is",[245,809,763],{"class":439},[245,811,812],{"class":255},")",[245,814,487],{"class":439},[245,816,817],{"class":278}," points had no value\"",[245,819,412],{"class":255},[14,821,822,824,825,829,830,833],{},[212,823,347],{}," Requesting no attributes halves the iteration cost when only geometry is needed, as explained in ",[21,826,828],{"href":827},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002F","Speed Up Feature Iteration with QgsFeatureRequest",". Storing ",[227,831,832],{},"None"," rather than a sentinel keeps the missing values honestly missing when the list is later written to a field. Counting the failures and reporting them is the habit that catches a CRS mismatch immediately: if every point failed, the coordinates are in the wrong system.",[14,835,836],{},[29,837,840,843,846,849,852,859,864,868,871,875,879,883,887,890,893,896,899,903,907,910,913,916],{"viewBox":838,"role":32,"ariaLabel":839,"xmlns":34},"0 0 760 250","Decision guide between point sampling, bilinear interpolation and zonal statistics depending on whether the value is at a location, near a boundary, or across an area",[36,841,842],{},"Point sample, interpolate, or summarise an area",[40,844,845],{},"Sampling one cell suits a value that genuinely belongs to a location, such as a land cover class. Interpolating between neighbouring cells suits a continuous surface where the point sits near a cell boundary. Zonal statistics suit a value that belongs to an area rather than a point, such as mean rainfall over a catchment.",[44,847],{"x":46,"y":46,"width":47,"height":848,"fill":49},"250",[51,850,851],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Is the answer really a single cell?",[44,853],{"x":854,"y":70,"width":855,"height":856,"rx":857,"fill":168,"stroke":150,"style":858},"16","236","164","10","stroke-width:2.5",[51,860,863],{"x":861,"y":173,"style":862,"fill":150,"textAnchor":57},"134","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","point sample",[51,865,867],{"x":861,"y":866,"style":109,"fill":110,"textAnchor":57},"106","native:rastersampling",[51,869,870],{"x":861,"y":143,"style":109,"fill":110,"textAnchor":57},"categorical rasters",[51,872,874],{"x":861,"y":873,"style":109,"fill":110,"textAnchor":57},"152","land cover, soil class",[51,876,878],{"x":861,"y":877,"style":109,"fill":150,"textAnchor":57},"182","averaging classes is meaningless",[44,880],{"x":48,"y":70,"width":855,"height":856,"rx":857,"fill":881,"stroke":882,"style":169},"#eff3ff","#2563eb",[51,884,886],{"x":885,"y":173,"style":862,"fill":882,"textAnchor":57},"382","interpolate",[51,888,889],{"x":885,"y":866,"style":109,"fill":110,"textAnchor":57},"warp to a finer grid first",[51,891,892],{"x":885,"y":143,"style":109,"fill":110,"textAnchor":57},"continuous surfaces",[51,894,895],{"x":885,"y":873,"style":109,"fill":110,"textAnchor":57},"elevation, temperature",[51,897,898],{"x":885,"y":877,"style":109,"fill":882,"textAnchor":57},"smooths the cell-edge step",[44,900],{"x":901,"y":70,"width":902,"height":856,"rx":857,"fill":181,"stroke":158,"style":169},"512","232",[51,904,906],{"x":905,"y":173,"style":862,"fill":158,"textAnchor":57},"628","zonal statistics",[51,908,909],{"x":905,"y":866,"style":109,"fill":110,"textAnchor":57},"native:zonalstatisticsfb",[51,911,912],{"x":905,"y":143,"style":109,"fill":110,"textAnchor":57},"the value belongs to an area",[51,914,915],{"x":905,"y":873,"style":109,"fill":110,"textAnchor":57},"mean rainfall per catchment",[51,917,918],{"x":905,"y":877,"style":109,"fill":158,"textAnchor":57},"a centroid sample is not a mean",[201,920,922],{"id":921},"sample-a-stack-of-dated-rasters","Sample a stack of dated rasters",[14,924,925],{},"Monitoring work rarely samples one raster. It samples the same stations against a monthly series, and the result is a table with one column per date.",[236,927,929],{"className":238,"code":928,"language":240,"meta":241,"style":241},"from pathlib import Path\nimport processing\n\nrasters = sorted(Path(\"\u002Fdata\u002Frainfall\").glob(\"rainfall_*.tif\"))\ncurrent = \"\u002Fdata\u002Fstations.gpkg|layername=stations\"\n\nfor raster in rasters:\n    period = raster.stem.split(\"_\")[-1]          # e.g. 2026_07 -> \"07\"\n    current = processing.run(\"native:rastersampling\", {\n        \"INPUT\": current,\n        \"RASTERCOPY\": str(raster),\n        \"COLUMN_PREFIX\": f\"rain_{period}_\",\n        \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n    })[\"OUTPUT\"]\n\nprocessing.run(\"native:savefeatures\", {\n    \"INPUT\": current,\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fstations_rainfall.gpkg\",\n})\n",[227,930,931,943,949,953,977,987,991,1003,1030,1043,1051,1064,1089,1102,1114,1119,1130,1137,1149],{"__ignoreMap":241},[245,932,933,935,938,940],{"class":247,"line":248},[245,934,380],{"class":251},[245,936,937],{"class":255}," pathlib ",[245,939,252],{"class":251},[245,941,942],{"class":255}," Path\n",[245,944,945,947],{"class":247,"line":259},[245,946,252],{"class":251},[245,948,256],{"class":255},[245,950,951],{"class":247,"line":266},[245,952,263],{"emptyLinePlaceholder":262},[245,954,955,958,960,963,966,969,972,975],{"class":247,"line":285},[245,956,957],{"class":255},"rasters ",[245,959,272],{"class":251},[245,961,962],{"class":439}," sorted",[245,964,965],{"class":255},"(Path(",[245,967,968],{"class":278},"\"\u002Fdata\u002Frainfall\"",[245,970,971],{"class":255},").glob(",[245,973,974],{"class":278},"\"rainfall_*.tif\"",[245,976,632],{"class":255},[245,978,979,982,984],{"class":247,"line":300},[245,980,981],{"class":255},"current ",[245,983,272],{"class":251},[245,985,986],{"class":278}," \"\u002Fdata\u002Fstations.gpkg|layername=stations\"\n",[245,988,989],{"class":247,"line":313},[245,990,263],{"emptyLinePlaceholder":262},[245,992,993,995,998,1000],{"class":247,"line":326},[245,994,716],{"class":251},[245,996,997],{"class":255}," raster ",[245,999,722],{"class":251},[245,1001,1002],{"class":255}," rasters:\n",[245,1004,1005,1008,1010,1013,1016,1019,1021,1023,1026],{"class":247,"line":339},[245,1006,1007],{"class":255},"    period ",[245,1009,272],{"class":251},[245,1011,1012],{"class":255}," raster.stem.split(",[245,1014,1015],{"class":278},"\"_\"",[245,1017,1018],{"class":255},")[",[245,1020,621],{"class":251},[245,1022,451],{"class":439},[245,1024,1025],{"class":255},"]          ",[245,1027,1029],{"class":1028},"sjoCn","# e.g. 2026_07 -> \"07\"\n",[245,1031,1032,1035,1037,1039,1041],{"class":247,"line":495},[245,1033,1034],{"class":255},"    current ",[245,1036,272],{"class":251},[245,1038,275],{"class":255},[245,1040,279],{"class":278},[245,1042,282],{"class":255},[245,1044,1045,1048],{"class":247,"line":504},[245,1046,1047],{"class":278},"        \"INPUT\"",[245,1049,1050],{"class":255},": current,\n",[245,1052,1053,1056,1058,1061],{"class":247,"line":772},[245,1054,1055],{"class":278},"        \"RASTERCOPY\"",[245,1057,291],{"class":255},[245,1059,1060],{"class":439},"str",[245,1062,1063],{"class":255},"(raster),\n",[245,1065,1067,1070,1072,1074,1077,1079,1082,1084,1087],{"class":247,"line":1066},12,[245,1068,1069],{"class":278},"        \"COLUMN_PREFIX\"",[245,1071,291],{"class":255},[245,1073,472],{"class":251},[245,1075,1076],{"class":278},"\"rain_",[245,1078,478],{"class":439},[245,1080,1081],{"class":255},"period",[245,1083,487],{"class":439},[245,1085,1086],{"class":278},"_\"",[245,1088,297],{"class":255},[245,1090,1092,1095,1097,1100],{"class":247,"line":1091},13,[245,1093,1094],{"class":278},"        \"OUTPUT\"",[245,1096,291],{"class":255},[245,1098,1099],{"class":278},"\"TEMPORARY_OUTPUT\"",[245,1101,297],{"class":255},[245,1103,1105,1108,1111],{"class":247,"line":1104},14,[245,1106,1107],{"class":255},"    })[",[245,1109,1110],{"class":278},"\"OUTPUT\"",[245,1112,1113],{"class":255},"]\n",[245,1115,1117],{"class":247,"line":1116},15,[245,1118,263],{"emptyLinePlaceholder":262},[245,1120,1122,1125,1128],{"class":247,"line":1121},16,[245,1123,1124],{"class":255},"processing.run(",[245,1126,1127],{"class":278},"\"native:savefeatures\"",[245,1129,282],{"class":255},[245,1131,1133,1135],{"class":247,"line":1132},17,[245,1134,288],{"class":278},[245,1136,1050],{"class":255},[245,1138,1140,1142,1144,1147],{"class":247,"line":1139},18,[245,1141,329],{"class":278},[245,1143,291],{"class":255},[245,1145,1146],{"class":278},"\"\u002Fdata\u002Foutput\u002Fstations_rainfall.gpkg\"",[245,1148,297],{"class":255},[245,1150,1152],{"class":247,"line":1151},19,[245,1153,342],{"class":255},[14,1155,1156,1158,1159,1162,1163,1166],{},[212,1157,347],{}," Each pass takes the previous result as its input, so the columns accumulate on one layer rather than producing a dozen layers to join afterwards. Sorting the paths matters: the column order in the output then follows the chronological order, which is what makes the table readable and any later reshaping straightforward. Keeping every intermediate as ",[227,1160,1161],{},"TEMPORARY_OUTPUT"," avoids writing eleven files that exist only to feed the next step, and the single ",[227,1164,1165],{},"native:savefeatures"," at the end is the one write to disk.",[14,1168,1169,1170,1173,1174,1177,1178,660],{},"Two cautions apply to a series. All the rasters must share a grid, or each pass silently samples a slightly different footprint — check with ",[227,1171,1172],{},"layer.extent()"," and ",[227,1175,1176],{},"layer.rasterUnitsPerPixelX()"," before starting rather than after. And a wide result is often not the shape you want: a table with one row per station per date is far easier to chart or aggregate, which means reshaping the wide output afterwards, most simply by exporting to CSV and pivoting there — see ",[21,1179,1181],{"href":1180},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fexport-attribute-table-to-csv-pyqgis\u002F","Export an Attribute Table to CSV in PyQGIS",[14,1183,1184],{},[29,1185,1188,1191,1194,1197,1212,1215,1220,1225,1227,1230,1232,1236,1243,1248,1252,1256,1260,1267,1272,1274,1277,1279,1282,1287,1291],{"viewBox":1186,"role":32,"ariaLabel":1187,"xmlns":34},"0 0 760 244","A stack of monthly rasters sampled in sequence against one point layer, each pass adding a dated column to the same table",[36,1189,1190],{},"One layer, one column per date",[40,1192,1193],{},"Monthly rainfall rasters are sampled one after another against the same station layer. Each pass takes the previous result as its input and appends a column named for its month, so the final table carries one row per station and one column per date rather than a dozen separate layers to join.",[44,1195],{"x":46,"y":46,"width":47,"height":1196,"fill":49},"244",[1198,1199,1200],"defs",{},[1201,1202,1208],"marker",{"id":1203,"viewBox":1204,"refX":1205,"refY":149,"markerWidth":1206,"markerHeight":1206,"orient":1207},"stackArrow","0 0 10 10","8","7","auto-start-reverse",[1209,1210],"path",{"d":1211,"fill":110},"M0 0 L10 5 L0 10 z",[51,1213,1214],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Chain the passes; do not join twelve layers afterwards",[44,1216],{"x":1217,"y":1218,"width":154,"height":1219,"rx":149,"fill":881,"stroke":882,"style":63},"20","72","34",[51,1221,1224],{"x":1222,"y":1223,"style":103,"fill":110,"textAnchor":57},"86","94","rainfall_2026_05.tif",[44,1226],{"x":1217,"y":111,"width":154,"height":1219,"rx":149,"fill":881,"stroke":882,"style":63},[51,1228,1229],{"x":1222,"y":861,"style":103,"fill":110,"textAnchor":57},"rainfall_2026_06.tif",[44,1231],{"x":1217,"y":873,"width":154,"height":1219,"rx":149,"fill":881,"stroke":882,"style":63},[51,1233,1235],{"x":1222,"y":1234,"style":103,"fill":110,"textAnchor":57},"174","rainfall_2026_07.tif",[44,1237],{"x":1238,"y":1239,"width":73,"height":1240,"rx":1205,"fill":1241,"stroke":1242,"style":858},"196","88","82","#eef7f4","#0f766e",[51,1244,1247],{"x":1245,"y":67,"style":1246,"fill":1242,"textAnchor":57},"286","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","rastersampling",[51,1249,1251],{"x":1245,"y":1250,"style":103,"fill":110,"textAnchor":57},"142","once per raster",[44,1253],{"x":1254,"y":69,"width":1255,"height":184,"rx":1205,"fill":168,"stroke":150,"style":858},"424","316",[51,1257,1259],{"x":1258,"y":1222,"style":1246,"fill":150,"textAnchor":57},"582","stations_rainfall.gpkg",[44,1261],{"x":1262,"y":1263,"width":1264,"height":1265,"rx":1266,"fill":64,"stroke":62,"style":63},"444","100","276","26","4",[51,1268,1271],{"x":1269,"y":114,"style":1270,"fill":110},"456","font-size:10px;font-family:sans-serif","station rain_05_1 rain_06_1 rain_07_1",[44,1273],{"x":1262,"y":154,"width":1264,"height":1265,"rx":1266,"fill":64,"stroke":62,"style":63},[51,1275,1276],{"x":1269,"y":107,"style":1270,"fill":110},"S-014   61.2      48.9      12.4",[44,1278],{"x":1262,"y":856,"width":1264,"height":1265,"rx":1266,"fill":64,"stroke":62,"style":63},[51,1280,1281],{"x":1269,"y":877,"style":1270,"fill":110},"S-021   58.7      51.3      NULL",[247,1283],{"x1":873,"y1":1284,"x2":1285,"y2":1284,"stroke":110,"style":1286},"129","190","stroke-width:2;marker-end:url(#stackArrow)",[247,1288],{"x1":1289,"y1":1284,"x2":1290,"y2":1284,"stroke":110,"style":1286},"376","418",[51,1292,1294],{"x":53,"y":1293,"style":109,"fill":62,"textAnchor":57},"228","A NULL is a station that fell on nodata that month, not a zero",[201,1296,1298],{"id":1297},"qgis-version-compatibility","QGIS version compatibility",[14,1300,1301,1302,1304],{},"The examples target ",[212,1303,214],{}," (Python 3.12).",[1306,1307,1308,1324],"table",{},[1309,1310,1311],"thead",{},[1312,1313,1314,1318,1321],"tr",{},[1315,1316,1317],"th",{},"QGIS version",[1315,1319,1320],{},"Python",[1315,1322,1323],{},"Notes",[1325,1326,1327,1344,1355,1366],"tbody",{},[1312,1328,1329,1333,1336],{},[1330,1331,1332],"td",{},"3.16 LTR",[1330,1334,1335],{},"3.7",[1330,1337,1338,1340,1341,1343],{},[227,1339,867],{}," added; ",[227,1342,521],{}," present since 3.4.",[1312,1345,1346,1349,1352],{},[1330,1347,1348],{},"3.28 LTR",[1330,1350,1351],{},"3.9",[1330,1353,1354],{},"Behaviour matches this page.",[1312,1356,1357,1360,1363],{},[1330,1358,1359],{},"3.34 LTR",[1330,1361,1362],{},"3.12",[1330,1364,1365],{},"Baseline for this page.",[1312,1367,1368,1371,1373],{},[1330,1369,1370],{},"3.40 \u002F 3.44",[1330,1372,1362],{},[1330,1374,1375],{},"Unchanged; zonal statistics gained additional statistic types.",[201,1377,1379],{"id":1378},"troubleshooting","Troubleshooting",[206,1381,1382,1391,1397,1403,1411,1420],{},[209,1383,1384,1387,1388,1390],{},[212,1385,1386],{},"Every sampled value is NULL."," The point CRS and raster CRS differ and you used ",[227,1389,521],{},". Transform first, or use the algorithm.",[209,1392,1393,1396],{},[212,1394,1395],{},"Values are wrong but plausible."," Same cause, when the coordinates happen to land inside the extent. Check one point against the canvas readout.",[209,1398,1399,1402],{},[212,1400,1401],{},"NaN appears in the output."," The validity flag was ignored. Test the second element of the tuple.",[209,1404,1405,518,1408,1410],{},[212,1406,1407],{},"The band is wrong.",[227,1409,521],{}," is one-based. Band 1 is the first band, not band 0.",[209,1412,1413,1416,1417,1419],{},[212,1414,1415],{},"Sampling a multi-band raster is slow."," Each call reads one band; for many bands, ",[227,1418,867],{}," reads them in one pass.",[209,1421,1422,1425],{},[212,1423,1424],{},"A point on a boundary gives an unexpected value."," Cell membership follows the raster's geotransform; a point exactly on a boundary belongs to one specific cell. If the boundary matters, interpolate rather than sample.",[201,1427,1429],{"id":1428},"conclusion","Conclusion",[14,1431,1432,1433,1435,1436,1439],{},"Sampling a layer is ",[227,1434,867],{}," with a column prefix — it reprojects, honours nodata and writes NULLs where there is no value. Sampling a coordinate is ",[227,1437,1438],{},"provider.sample()",", which is fast, does not reproject, and must have its validity flag checked. If the value logically belongs to an area rather than a location, the question was a zonal statistic all along.",[201,1441,1443],{"id":1442},"frequently-asked-questions","Frequently Asked Questions",[14,1445,1446,1449],{},[212,1447,1448],{},"Does sampling interpolate between cells?","\nNo. Both routes return the value of the cell containing the point. For an interpolated value, warp the raster to a finer grid with bilinear resampling and sample that.",[14,1451,1452,1455],{},[212,1453,1454],{},"How do I sample several rasters into one layer?","\nRun the algorithm once per raster, chaining each output into the next input. Each pass appends its own prefixed fields.",[14,1457,1458,1461,1462,1465],{},[212,1459,1460],{},"Can I sample along a line?","\nConvert the line to points first with ",[227,1463,1464],{},"native:pointsalonglines",", then sample those. That is also how a terrain profile is built.",[14,1467,1468,1471],{},[212,1469,1470],{},"Why is the added field an integer when the raster is float?","\nThe output format's field type inference. Write to GeoPackage rather than shapefile, and check the raster's actual data type.",[14,1473,1474,1477],{},[212,1475,1476],{},"Can I sample a raster that is only available over the network?","\nYes, if QGIS can open it — a Cloud Optimized GeoTIFF over HTTP works, and only the tiles containing your points are fetched. Expect the first sample to be slow while the header is read, and set a timeout so an unreachable host fails rather than hangs.",[14,1479,1480,1483,1484,1486,1487,1490],{},[212,1481,1482],{},"How do I record which raster a value came from?","\nInclude it in the column prefix, as the dated series above does. A value in a column called ",[227,1485,351],{}," is anonymous a year later; one in ",[227,1488,1489],{},"elev_lidar2024_1"," explains itself.",[14,1492,1493,1496],{},[212,1494,1495],{},"What does sampling return on a mosaic with gaps?","\nNodata, and therefore a NULL. That is correct — a gap is an absence of measurement, not a zero.",[201,1498,1500],{"id":1499},"related","Related",[206,1502,1503,1508,1514,1520,1524],{},[209,1504,1505,1507],{},[21,1506,24],{"href":23}," — the guide this recipe belongs to",[209,1509,1510],{},[21,1511,1513],{"href":1512},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fraster-calculator-pyqgis\u002F","Use the Raster Calculator in PyQGIS",[209,1515,1516],{},[21,1517,1519],{"href":1518},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fcalculate-raster-statistics-pyqgis\u002F","Calculate Raster Statistics in PyQGIS",[209,1521,1522],{},[21,1523,659],{"href":658},[209,1525,1526],{},[21,1527,1529],{"href":1528},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fupdate-attribute-values-pyqgis\u002F","Update Attribute Values in PyQGIS",[1531,1532,1533],"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 .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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":241,"searchDepth":259,"depth":259,"links":1535},[1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546],{"id":203,"depth":259,"text":204},{"id":233,"depth":259,"text":234},{"id":369,"depth":259,"text":370},{"id":542,"depth":259,"text":543},{"id":663,"depth":259,"text":664},{"id":921,"depth":259,"text":922},{"id":1297,"depth":259,"text":1298},{"id":1378,"depth":259,"text":1379},{"id":1428,"depth":259,"text":1429},{"id":1442,"depth":259,"text":1443},{"id":1499,"depth":259,"text":1500},"Read raster values at point locations with native:rastersampling and provider.sample(), handle CRS differences and nodata, and choose between point sampling and zonal statistics.","md",{"slug":1550,"type":1551,"breadcrumb":1552,"datePublished":1553,"dateModified":1553},"sample-raster-values-at-points-pyqgis","article","Sample Raster at Points","2026-08-10","\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis",{"title":5,"description":1547},"spatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis\u002Findex","etv8Pc6M8YK78MpG8H9gVbI3jOP710L_ETBzW6YAuu0",1786401338540]