[{"data":1,"prerenderedAt":1667},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fkernel-density-heatmap-raster-pyqgis":3},{"id":4,"title":5,"body":6,"description":1656,"extension":1657,"meta":1658,"navigation":193,"path":1663,"seo":1664,"stem":1665,"__hash__":1666},"docs\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fkernel-density-heatmap-raster-pyqgis\u002Findex.md","Create a Kernel Density Heatmap Raster in PyQGIS",{"type":7,"value":8,"toc":1641},"minimark",[9,13,17,36,128,133,146,150,153,452,459,463,466,538,546,699,704,708,711,799,808,812,815,1017,1034,1038,1041,1110,1113,1117,1120,1297,1315,1319,1322,1505,1514,1518,1527,1531,1557,1561,1564,1568,1574,1580,1591,1605,1609,1637],[10,11,5],"h1",{"id":12},"create-a-kernel-density-heatmap-raster-in-pyqgis",[14,15,16],"p",{},"A map of ten thousand points says little beyond \"there are many\". A density surface turns the same points into a continuous field: high where points crowd together, low where they thin out, smooth in between. Kernel density estimation does this by placing a small hill — the kernel — over every point and adding the hills up. The result is a raster you can classify, contour, sample and compare, which a renderer-only heatmap cannot give you.",[14,18,19,20,25,26,30,31,35],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002F","Spatial Statistics & Pattern Analysis",". It runs ",[27,28,29],"code",{},"qgis:heatmapkerneldensityestimation",", explains how radius and pixel size shape the result, adds weights, chooses kernels and output units, and styles and normalises the surface for honest comparison. For the on-the-fly display version, see the ",[21,32,34],{"href":33},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fcreate-heatmap-renderer-pyqgis\u002F","heatmap renderer",".",[14,37,38],{},[39,40,45,49,53,60,69,77,83,86,89,92,97,100,106,109,112,115,120,124],"svg",{"viewBox":41,"role":42,"ariaLabel":43,"xmlns":44},"0 0 760 280","img","Kernels placed over five points along a line sum to a tall peak where points are close and a low hill where a point is isolated","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[46,47,48],"title",{},"Kernels add up to a surface",[50,51,52],"desc",{},"Along a line of five points, each point contributes a smooth kernel shaped like a hill with a width equal to the search radius. Where points are close together their kernels overlap and sum to a tall peak; an isolated point produces a single low hill. The density surface is the sum of all kernels.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59},"0","760","280","#f6f3ea",[61,62,68],"text",{"x":63,"y":64,"style":65,"fill":66,"textAnchor":67},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Each point adds a hill; the hills add up",[70,71],"line",{"x1":72,"y1":73,"x2":74,"y2":73,"stroke":75,"style":76},"40","220","720","#59645f","stroke-width:1.5",[78,79],"path",{"d":80,"fill":81,"stroke":82,"style":76},"M 100 220 Q 140 150 180 220","none","#2563eb",[78,84],{"d":85,"fill":81,"stroke":82,"style":76},"M 130 220 Q 170 150 210 220",[78,87],{"d":88,"fill":81,"stroke":82,"style":76},"M 160 220 Q 200 150 240 220",[78,90],{"d":91,"fill":81,"stroke":82,"style":76},"M 520 220 Q 560 150 600 220",[78,93],{"d":94,"fill":81,"stroke":95,"style":96},"M 90 220 C 130 210 140 70 170 70 C 200 70 210 210 250 220","#b45309","stroke-width:3",[78,98],{"d":99,"fill":81,"stroke":95,"style":96},"M 510 220 C 540 210 545 150 560 150 C 575 150 580 210 610 220",[101,102],"circle",{"cx":103,"cy":73,"r":104,"fill":66,"stroke":66,"style":105},"140","5","stroke-width:1",[101,107],{"cx":108,"cy":73,"r":104,"fill":66,"stroke":66,"style":105},"170",[101,110],{"cx":111,"cy":73,"r":104,"fill":66,"stroke":66,"style":105},"200",[101,113],{"cx":114,"cy":73,"r":104,"fill":66,"stroke":66,"style":105},"560",[61,116,119],{"x":108,"y":117,"style":118,"fill":95,"textAnchor":67},"58","text-anchor:middle;font-size:10.5px;font-family:sans-serif","close points: tall peak",[61,121,123],{"x":114,"y":122,"style":118,"fill":95,"textAnchor":67},"138","isolated: low hill",[61,125,127],{"x":63,"y":126,"style":118,"fill":75,"textAnchor":67},"250","blue: individual kernels · amber: their sum, the density",[129,130,132],"h2",{"id":131},"prerequisites","Prerequisites",[134,135,136,140,143],"ul",{},[137,138,139],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[137,141,142],{},"A point layer in a projected CRS with metre units; radius and pixel size are in layer units.",[137,144,145],{},"An idea of the scale of the pattern you care about — a city block, a neighbourhood, a region — because that decides the radius.",[129,147,149],{"id":148},"run-the-algorithm","Run the algorithm",[14,151,152],{},"The algorithm needs a radius, a pixel size and an output path. Everything else has workable defaults to start from.",[154,155,160],"pre",{"className":156,"code":157,"language":158,"meta":159,"style":159},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsProject, QgsRasterLayer\n\ncollisions = QgsProject.instance().mapLayersByName(\"road_collisions\")[0]\nout = processing.run(\"qgis:heatmapkerneldensityestimation\", {\n    \"INPUT\": collisions,\n    \"RADIUS\": 400,             # metres\n    \"RADIUS_FIELD\": None,\n    \"PIXEL_SIZE\": 20,          # metres\n    \"WEIGHT_FIELD\": None,\n    \"KERNEL\": 0,               # 0 = quartic\n    \"DECAY\": 0,\n    \"OUTPUT_VALUE\": 0,         # 0 = raw, 1 = scaled\n    \"OUTPUT\": \"\u002Fdata\u002Fresults\u002Fcollision_density.tif\",\n})[\"OUTPUT\"]\n\ndensity = QgsRasterLayer(out, \"collision density\")\nstats = density.dataProvider().bandStatistics(1)\nprint(density.width(), \"x\", density.height(), \"pixels; max\", round(stats.maximumValue, 2))\nQgsProject.instance().addMapLayer(density)\n","python","",[27,161,162,174,188,195,220,237,246,265,279,295,307,323,335,351,364,375,380,397,413,446],{"__ignoreMap":159},[163,164,166,170],"span",{"class":70,"line":165},1,[163,167,169],{"class":168},"snl16","import",[163,171,173],{"class":172},"s95oV"," processing\n",[163,175,177,180,183,185],{"class":70,"line":176},2,[163,178,179],{"class":168},"from",[163,181,182],{"class":172}," qgis.core ",[163,184,169],{"class":168},[163,186,187],{"class":172}," QgsProject, QgsRasterLayer\n",[163,189,191],{"class":70,"line":190},3,[163,192,194],{"emptyLinePlaceholder":193},true,"\n",[163,196,198,201,204,207,211,214,217],{"class":70,"line":197},4,[163,199,200],{"class":172},"collisions ",[163,202,203],{"class":168},"=",[163,205,206],{"class":172}," QgsProject.instance().mapLayersByName(",[163,208,210],{"class":209},"sU2Wk","\"road_collisions\"",[163,212,213],{"class":172},")[",[163,215,56],{"class":216},"sDLfK",[163,218,219],{"class":172},"]\n",[163,221,223,226,228,231,234],{"class":70,"line":222},5,[163,224,225],{"class":172},"out ",[163,227,203],{"class":168},[163,229,230],{"class":172}," processing.run(",[163,232,233],{"class":209},"\"qgis:heatmapkerneldensityestimation\"",[163,235,236],{"class":172},", {\n",[163,238,240,243],{"class":70,"line":239},6,[163,241,242],{"class":209},"    \"INPUT\"",[163,244,245],{"class":172},": collisions,\n",[163,247,249,252,255,258,261],{"class":70,"line":248},7,[163,250,251],{"class":209},"    \"RADIUS\"",[163,253,254],{"class":172},": ",[163,256,257],{"class":216},"400",[163,259,260],{"class":172},",             ",[163,262,264],{"class":263},"sjoCn","# metres\n",[163,266,268,271,273,276],{"class":70,"line":267},8,[163,269,270],{"class":209},"    \"RADIUS_FIELD\"",[163,272,254],{"class":172},[163,274,275],{"class":216},"None",[163,277,278],{"class":172},",\n",[163,280,282,285,287,290,293],{"class":70,"line":281},9,[163,283,284],{"class":209},"    \"PIXEL_SIZE\"",[163,286,254],{"class":172},[163,288,289],{"class":216},"20",[163,291,292],{"class":172},",          ",[163,294,264],{"class":263},[163,296,298,301,303,305],{"class":70,"line":297},10,[163,299,300],{"class":209},"    \"WEIGHT_FIELD\"",[163,302,254],{"class":172},[163,304,275],{"class":216},[163,306,278],{"class":172},[163,308,310,313,315,317,320],{"class":70,"line":309},11,[163,311,312],{"class":209},"    \"KERNEL\"",[163,314,254],{"class":172},[163,316,56],{"class":216},[163,318,319],{"class":172},",               ",[163,321,322],{"class":263},"# 0 = quartic\n",[163,324,326,329,331,333],{"class":70,"line":325},12,[163,327,328],{"class":209},"    \"DECAY\"",[163,330,254],{"class":172},[163,332,56],{"class":216},[163,334,278],{"class":172},[163,336,338,341,343,345,348],{"class":70,"line":337},13,[163,339,340],{"class":209},"    \"OUTPUT_VALUE\"",[163,342,254],{"class":172},[163,344,56],{"class":216},[163,346,347],{"class":172},",         ",[163,349,350],{"class":263},"# 0 = raw, 1 = scaled\n",[163,352,354,357,359,362],{"class":70,"line":353},14,[163,355,356],{"class":209},"    \"OUTPUT\"",[163,358,254],{"class":172},[163,360,361],{"class":209},"\"\u002Fdata\u002Fresults\u002Fcollision_density.tif\"",[163,363,278],{"class":172},[163,365,367,370,373],{"class":70,"line":366},15,[163,368,369],{"class":172},"})[",[163,371,372],{"class":209},"\"OUTPUT\"",[163,374,219],{"class":172},[163,376,378],{"class":70,"line":377},16,[163,379,194],{"emptyLinePlaceholder":193},[163,381,383,386,388,391,394],{"class":70,"line":382},17,[163,384,385],{"class":172},"density ",[163,387,203],{"class":168},[163,389,390],{"class":172}," QgsRasterLayer(out, ",[163,392,393],{"class":209},"\"collision density\"",[163,395,396],{"class":172},")\n",[163,398,400,403,405,408,411],{"class":70,"line":399},18,[163,401,402],{"class":172},"stats ",[163,404,203],{"class":168},[163,406,407],{"class":172}," density.dataProvider().bandStatistics(",[163,409,410],{"class":216},"1",[163,412,396],{"class":172},[163,414,416,419,422,425,428,431,434,437,440,443],{"class":70,"line":415},19,[163,417,418],{"class":216},"print",[163,420,421],{"class":172},"(density.width(), ",[163,423,424],{"class":209},"\"x\"",[163,426,427],{"class":172},", density.height(), ",[163,429,430],{"class":209},"\"pixels; max\"",[163,432,433],{"class":172},", ",[163,435,436],{"class":216},"round",[163,438,439],{"class":172},"(stats.maximumValue, ",[163,441,442],{"class":216},"2",[163,444,445],{"class":172},"))\n",[163,447,449],{"class":70,"line":448},20,[163,450,451],{"class":172},"QgsProject.instance().addMapLayer(density)\n",[14,453,454,458],{},[455,456,457],"strong",{},"Breakdown:"," The output is a single-band GeoTIFF covering the points' extent plus the radius. Each pixel's value is the sum of kernel contributions from all points within the radius of the pixel centre. With raw output, values are in \"kernel units\" — not directly points per square kilometre — so use them for relative comparison within one surface, or switch to scaled output and normalise as described below. A 20 m pixel over a city produces a modest raster; halving the pixel size quadruples it.",[129,460,462],{"id":461},"choose-radius-and-pixel-size","Choose radius and pixel size",[14,464,465],{},"The radius is the single most important choice. It sets the scale at which the surface sees structure: small radii show individual hot corners, large radii show broad districts.",[14,467,468],{},[39,469,472,475,478,481,484,492,496,500,504,507,509,512,515,518,523,528,532,535],{"viewBox":470,"role":42,"ariaLabel":471,"xmlns":44},"0 0 760 260","Three density surfaces from the same points with small, medium and large radius, showing fragmented spots, clear corridors and one broad blob",[46,473,474],{},"Radius sets the scale of what you see",[50,476,477],{},"The same collisions with three radii. 100 metres: many small spots at individual junctions, noisy and fragmented. 400 metres: corridors and clusters along main roads, the scale of the question. 1500 metres: a single broad blob over the city centre that hides the road structure. Pixel size should be several times smaller than the radius.",[54,479],{"x":56,"y":56,"width":57,"height":480,"fill":59},"260",[61,482,483],{"x":63,"y":64,"style":65,"fill":66,"textAnchor":67},"Too small, about right, too large",[54,485],{"x":486,"y":487,"width":488,"height":489,"rx":490,"fill":491,"stroke":75,"style":76},"24","52","222","150","8","#fffdf7",[101,493],{"cx":494,"cy":495,"r":490,"fill":95,"stroke":95,"style":105},"70","90",[101,497],{"cx":498,"cy":103,"r":499,"fill":95,"stroke":95,"style":105},"120","7",[101,501],{"cx":502,"cy":503,"r":490,"fill":95,"stroke":95,"style":105},"190","100",[101,505],{"cx":495,"cy":108,"r":506,"fill":95,"stroke":95,"style":105},"6",[101,508],{"cx":111,"cy":108,"r":499,"fill":95,"stroke":95,"style":105},[54,510],{"x":511,"y":487,"width":488,"height":489,"rx":490,"fill":491,"stroke":75,"style":76},"269",[78,513],{"d":514,"fill":95,"stroke":95,"style":76},"M300,112 H368 V70 H392 V112 H460 V134 H392 V180 H368 V134 H300 Z",[54,516],{"x":517,"y":487,"width":488,"height":489,"rx":490,"fill":491,"stroke":75,"style":76},"514",[101,519],{"cx":520,"cy":521,"r":522,"fill":95,"stroke":95,"style":105},"625","127","62",[61,524,527],{"x":525,"y":526,"style":118,"fill":75,"textAnchor":67},"135","224","100 m: junction noise",[61,529,531],{"x":63,"y":526,"style":118,"fill":530,"textAnchor":67},"#166534","400 m: road corridors",[61,533,534],{"x":520,"y":526,"style":118,"fill":75,"textAnchor":67},"1500 m: one blob",[61,536,537],{"x":63,"y":126,"style":118,"fill":75,"textAnchor":67},"pixel size: about a fifth of the radius or smaller",[14,539,540,541,545],{},"There is no universally correct radius; there is a radius that matches the question. For junction safety, a radius of 50–100 m keeps junctions separate. For neighbourhood-level patterns, 300–800 m. A practical starting point is a few times the median nearest-neighbour distance — the ",[21,542,544],{"href":543},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fnearest-neighbour-analysis-pyqgis\u002F","nearest neighbour analysis"," gives it — and then compare a smaller and a larger value.",[154,547,549],{"className":156,"code":548,"language":158,"meta":159,"style":159},"for radius in (150, 400, 1000):\n    path = f\"\u002Fdata\u002Fresults\u002Fcollision_density_r{radius}.tif\"\n    processing.run(\"qgis:heatmapkerneldensityestimation\", {\n        \"INPUT\": collisions, \"RADIUS\": radius, \"PIXEL_SIZE\": max(radius \u002F 10, 10),\n        \"KERNEL\": 0, \"OUTPUT_VALUE\": 1, \"OUTPUT\": path})\n    QgsProject.instance().addMapLayer(QgsRasterLayer(path, f\"density r={radius}\"))\n",[27,550,551,579,604,613,652,677],{"__ignoreMap":159},[163,552,553,556,559,562,565,567,569,571,573,576],{"class":70,"line":165},[163,554,555],{"class":168},"for",[163,557,558],{"class":172}," radius ",[163,560,561],{"class":168},"in",[163,563,564],{"class":172}," (",[163,566,489],{"class":216},[163,568,433],{"class":172},[163,570,257],{"class":216},[163,572,433],{"class":172},[163,574,575],{"class":216},"1000",[163,577,578],{"class":172},"):\n",[163,580,581,584,586,589,592,595,598,601],{"class":70,"line":176},[163,582,583],{"class":172},"    path ",[163,585,203],{"class":168},[163,587,588],{"class":168}," f",[163,590,591],{"class":209},"\"\u002Fdata\u002Fresults\u002Fcollision_density_r",[163,593,594],{"class":216},"{",[163,596,597],{"class":172},"radius",[163,599,600],{"class":216},"}",[163,602,603],{"class":209},".tif\"\n",[163,605,606,609,611],{"class":70,"line":190},[163,607,608],{"class":172},"    processing.run(",[163,610,233],{"class":209},[163,612,236],{"class":172},[163,614,615,618,621,624,627,630,632,635,638,641,644,646,649],{"class":70,"line":197},[163,616,617],{"class":209},"        \"INPUT\"",[163,619,620],{"class":172},": collisions, ",[163,622,623],{"class":209},"\"RADIUS\"",[163,625,626],{"class":172},": radius, ",[163,628,629],{"class":209},"\"PIXEL_SIZE\"",[163,631,254],{"class":172},[163,633,634],{"class":216},"max",[163,636,637],{"class":172},"(radius ",[163,639,640],{"class":168},"\u002F",[163,642,643],{"class":216}," 10",[163,645,433],{"class":172},[163,647,648],{"class":216},"10",[163,650,651],{"class":172},"),\n",[163,653,654,657,659,661,663,666,668,670,672,674],{"class":70,"line":222},[163,655,656],{"class":209},"        \"KERNEL\"",[163,658,254],{"class":172},[163,660,56],{"class":216},[163,662,433],{"class":172},[163,664,665],{"class":209},"\"OUTPUT_VALUE\"",[163,667,254],{"class":172},[163,669,410],{"class":216},[163,671,433],{"class":172},[163,673,372],{"class":209},[163,675,676],{"class":172},": path})\n",[163,678,679,682,685,688,690,692,694,697],{"class":70,"line":239},[163,680,681],{"class":172},"    QgsProject.instance().addMapLayer(QgsRasterLayer(path, ",[163,683,684],{"class":168},"f",[163,686,687],{"class":209},"\"density r=",[163,689,594],{"class":216},[163,691,597],{"class":172},[163,693,600],{"class":216},[163,695,696],{"class":209},"\"",[163,698,445],{"class":172},[14,700,701,703],{},[455,702,457],{}," Generating a few radii side by side makes the scale dependence visible and prevents the first surface you produce from being taken as the truth. Tying pixel size to radius keeps each surface smooth without wasting pixels: a pixel much larger than a fifth of the radius makes the hills blocky, while one much smaller adds file size without adding information.",[129,705,707],{"id":706},"weight-points","Weight points",[14,709,710],{},"Not every point counts the same. A collision with fatalities matters more than a minor one; a shop with 200 staff more than one with two. A weight field multiplies each point's kernel.",[154,712,714],{"className":156,"code":713,"language":158,"meta":159,"style":159},"weighted = processing.run(\"qgis:heatmapkerneldensityestimation\", {\n    \"INPUT\": collisions, \"RADIUS\": 400, \"PIXEL_SIZE\": 20,\n    \"WEIGHT_FIELD\": \"severity_weight\",     # e.g. 1 minor, 3 serious, 10 fatal\n    \"KERNEL\": 0, \"OUTPUT_VALUE\": 1,\n    \"OUTPUT\": \"\u002Fdata\u002Fresults\u002Fcollision_density_weighted.tif\"})[\"OUTPUT\"]\n",[27,715,716,729,751,766,784],{"__ignoreMap":159},[163,717,718,721,723,725,727],{"class":70,"line":165},[163,719,720],{"class":172},"weighted ",[163,722,203],{"class":168},[163,724,230],{"class":172},[163,726,233],{"class":209},[163,728,236],{"class":172},[163,730,731,733,735,737,739,741,743,745,747,749],{"class":70,"line":176},[163,732,242],{"class":209},[163,734,620],{"class":172},[163,736,623],{"class":209},[163,738,254],{"class":172},[163,740,257],{"class":216},[163,742,433],{"class":172},[163,744,629],{"class":209},[163,746,254],{"class":172},[163,748,289],{"class":216},[163,750,278],{"class":172},[163,752,753,755,757,760,763],{"class":70,"line":190},[163,754,300],{"class":209},[163,756,254],{"class":172},[163,758,759],{"class":209},"\"severity_weight\"",[163,761,762],{"class":172},",     ",[163,764,765],{"class":263},"# e.g. 1 minor, 3 serious, 10 fatal\n",[163,767,768,770,772,774,776,778,780,782],{"class":70,"line":197},[163,769,312],{"class":209},[163,771,254],{"class":172},[163,773,56],{"class":216},[163,775,433],{"class":172},[163,777,665],{"class":209},[163,779,254],{"class":172},[163,781,410],{"class":216},[163,783,278],{"class":172},[163,785,786,788,790,793,795,797],{"class":70,"line":222},[163,787,356],{"class":209},[163,789,254],{"class":172},[163,791,792],{"class":209},"\"\u002Fdata\u002Fresults\u002Fcollision_density_weighted.tif\"",[163,794,369],{"class":172},[163,796,372],{"class":209},[163,798,219],{"class":172},[14,800,801,803,804,807],{},[455,802,457],{}," Weights are multiplied into the kernel heights, so a weight of 10 is equivalent to ten points at the same place. NULL weights count as zero, which silently drops points — check the weight field for missing values first. Weighting by a severity score is a judgement call; document the weights next to the map, because a reader cannot recover them from the surface. ",[27,805,806],{},"RADIUS_FIELD"," similarly lets each point have its own radius, useful when points represent features of different reach.",[129,809,811],{"id":810},"sample-the-density-at-locations","Sample the density at locations",[14,813,814],{},"A density surface becomes an attribute when you read it at points of interest: the collision density at each school entrance, the crime density at each bus stop. Sampling turns the raster into numbers that can be ranked, joined and reported.",[154,816,818],{"className":156,"code":817,"language":158,"meta":159,"style":159},"schools = QgsProject.instance().mapLayersByName(\"school_entrances\")[0]\nsampled = processing.run(\"native:rastersampling\", {\n    \"INPUT\": schools, \"RASTERCOPY\": weighted,\n    \"COLUMN_PREFIX\": \"kde_\", \"OUTPUT\": \"memory:schools_kde\"})[\"OUTPUT\"]\n\nranked = sorted(sampled.getFeatures(), key=lambda f: f[\"kde_1\"] or 0, reverse=True)\nfor f in ranked[:10]:\n    print(f\"{f['school_name']:\u003C30} {f['kde_1'] * 1e6:8.1f} weighted collisions \u002F km²\")\nQgsProject.instance().addMapLayer(sampled)\n",[27,819,820,838,852,865,890,894,941,958,1012],{"__ignoreMap":159},[163,821,822,825,827,829,832,834,836],{"class":70,"line":165},[163,823,824],{"class":172},"schools ",[163,826,203],{"class":168},[163,828,206],{"class":172},[163,830,831],{"class":209},"\"school_entrances\"",[163,833,213],{"class":172},[163,835,56],{"class":216},[163,837,219],{"class":172},[163,839,840,843,845,847,850],{"class":70,"line":176},[163,841,842],{"class":172},"sampled ",[163,844,203],{"class":168},[163,846,230],{"class":172},[163,848,849],{"class":209},"\"native:rastersampling\"",[163,851,236],{"class":172},[163,853,854,856,859,862],{"class":70,"line":190},[163,855,242],{"class":209},[163,857,858],{"class":172},": schools, ",[163,860,861],{"class":209},"\"RASTERCOPY\"",[163,863,864],{"class":172},": weighted,\n",[163,866,867,870,872,875,877,879,881,884,886,888],{"class":70,"line":197},[163,868,869],{"class":209},"    \"COLUMN_PREFIX\"",[163,871,254],{"class":172},[163,873,874],{"class":209},"\"kde_\"",[163,876,433],{"class":172},[163,878,372],{"class":209},[163,880,254],{"class":172},[163,882,883],{"class":209},"\"memory:schools_kde\"",[163,885,369],{"class":172},[163,887,372],{"class":209},[163,889,219],{"class":172},[163,891,892],{"class":70,"line":222},[163,893,194],{"emptyLinePlaceholder":193},[163,895,896,899,901,904,907,911,914,917,920,923,926,929,931,934,936,939],{"class":70,"line":239},[163,897,898],{"class":172},"ranked ",[163,900,203],{"class":168},[163,902,903],{"class":216}," sorted",[163,905,906],{"class":172},"(sampled.getFeatures(), ",[163,908,910],{"class":909},"s9osk","key",[163,912,913],{"class":168},"=lambda",[163,915,916],{"class":172}," f: f[",[163,918,919],{"class":209},"\"kde_1\"",[163,921,922],{"class":172},"] ",[163,924,925],{"class":168},"or",[163,927,928],{"class":216}," 0",[163,930,433],{"class":172},[163,932,933],{"class":909},"reverse",[163,935,203],{"class":168},[163,937,938],{"class":216},"True",[163,940,396],{"class":172},[163,942,943,945,948,950,953,955],{"class":70,"line":248},[163,944,555],{"class":168},[163,946,947],{"class":172}," f ",[163,949,561],{"class":168},[163,951,952],{"class":172}," ranked[:",[163,954,648],{"class":216},[163,956,957],{"class":172},"]:\n",[163,959,960,963,966,968,970,972,975,978,981,984,986,989,991,994,996,999,1002,1005,1007,1010],{"class":70,"line":267},[163,961,962],{"class":216},"    print",[163,964,965],{"class":172},"(",[163,967,684],{"class":168},[163,969,696],{"class":209},[163,971,594],{"class":216},[163,973,974],{"class":172},"f[",[163,976,977],{"class":209},"'school_name'",[163,979,980],{"class":172},"]",[163,982,983],{"class":168},":\u003C30",[163,985,600],{"class":216},[163,987,988],{"class":216}," {",[163,990,974],{"class":172},[163,992,993],{"class":209},"'kde_1'",[163,995,922],{"class":172},[163,997,998],{"class":168},"*",[163,1000,1001],{"class":216}," 1e6",[163,1003,1004],{"class":168},":8.1f",[163,1006,600],{"class":216},[163,1008,1009],{"class":209}," weighted collisions \u002F km²\"",[163,1011,396],{"class":172},[163,1013,1014],{"class":70,"line":281},[163,1015,1016],{"class":172},"QgsProject.instance().addMapLayer(sampled)\n",[14,1018,1019,1021,1022,1025,1026,1029,1030,35],{},[455,1020,457],{}," ",[27,1023,1024],{},"native:rastersampling"," adds one field per band, named with the prefix and band number, holding the pixel value under each point. Multiplying scaled values by a million converts them to events per square kilometre, which is the figure to show in a ranking table. Points outside the raster's extent get NULL, hence the ",[27,1027,1028],{},"or 0"," in the sort key. Because the surface already integrates everything within the radius, a single sample summarises the surroundings of each location — a quicker and smoother measure than counting points in a buffer, and directly comparable between locations. The general technique is covered in ",[21,1031,1033],{"href":1032},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis\u002F","sampling raster values at points",[129,1035,1037],{"id":1036},"choose-kernel-and-output-values","Choose kernel and output values",[14,1039,1040],{},"The kernel shape matters much less than the radius, but the output scaling matters for interpretation.",[14,1042,1043],{},[39,1044,1047,1050,1053,1056,1059,1066,1072,1077,1081,1085,1089,1093,1098,1102,1106],{"viewBox":1045,"role":42,"ariaLabel":1046,"xmlns":44},"0 0 760 240","Kernel shapes from smooth dome to flat disk, and the choice between raw and scaled output values",[46,1048,1049],{},"Kernel shapes and output scaling",[50,1051,1052],{},"Kernel shapes include quartic, the default, a smooth dome; triangular, a cone; uniform, a flat disk; triweight, a steeper dome; and Epanechnikov. All fall to zero at the radius. Output values can be raw, the summed kernel heights, or scaled, which normalises each kernel to integrate to one so the surface estimates points per unit area.",[54,1054],{"x":56,"y":56,"width":57,"height":1055,"fill":59},"240",[61,1057,1058],{"x":63,"y":64,"style":65,"fill":66,"textAnchor":67},"Shape matters little; scaling matters more",[54,1060],{"x":486,"y":1061,"width":1062,"height":1063,"rx":490,"fill":1064,"stroke":82,"style":1065},"56","340","160","#eff3ff","stroke-width:2",[61,1067,1071],{"x":1068,"y":1069,"style":1070,"fill":82,"textAnchor":67},"194","87.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","KERNEL",[61,1073,1076],{"x":1068,"y":1074,"style":118,"fill":1075,"textAnchor":67},"113.78","#2f3b35","0 quartic (smooth, default)",[61,1078,1080],{"x":1068,"y":1079,"style":118,"fill":1075,"textAnchor":67},"139.78","1 triangular · 2 uniform",[61,1082,1084],{"x":1068,"y":1083,"style":118,"fill":1075,"textAnchor":67},"165.78","3 triweight · 4 Epanechnikov",[61,1086,1088],{"x":1068,"y":1087,"style":118,"fill":75,"textAnchor":67},"191.78","all reach zero at the radius",[54,1090],{"x":1091,"y":1061,"width":1062,"height":1063,"rx":490,"fill":1092,"stroke":95,"style":1065},"396","#fdf2e2",[61,1094,1097],{"x":1095,"y":1096,"style":1070,"fill":95,"textAnchor":67},"566","94.78","OUTPUT_VALUE",[61,1099,1101],{"x":1095,"y":1100,"style":118,"fill":1075,"textAnchor":67},"124.78","0 raw: summed heights",[61,1103,1105],{"x":1095,"y":1104,"style":118,"fill":1075,"textAnchor":67},"154.78","1 scaled: density per area",[61,1107,1109],{"x":1095,"y":1108,"style":118,"fill":75,"textAnchor":67},"184.78","scaled → comparable surfaces",[14,1111,1112],{},"The quartic default produces smooth, natural-looking surfaces and is the right choice almost always. A uniform kernel counts points within the radius and gives a flat-topped, blocky result; it is useful when you want \"number of events within 400 m\" literally. Scaled output divides each kernel by its integral, so pixel values estimate events per square map unit — per square metre for a metric CRS. Multiply by 1,000,000 for events per square kilometre.",[129,1114,1116],{"id":1115},"normalise-for-comparison","Normalise for comparison",[14,1118,1119],{},"Two density surfaces can only be compared if they are on the same scale. Comparing this year's collisions with last year's, or one city with another, needs scaled output and a common unit.",[154,1121,1123],{"className":156,"code":1122,"language":158,"meta":159,"style":159},"import numpy as np\nfrom osgeo import gdal\n\ndef per_km2(path):\n    ds = gdal.Open(path)\n    arr = ds.GetRasterBand(1).ReadAsArray().astype(\"float64\") * 1e6\n    return arr, ds\n\nthis_year, ds = per_km2(\"\u002Fdata\u002Fresults\u002Fcollisions_2026_scaled.tif\")\nlast_year, _ = per_km2(\"\u002Fdata\u002Fresults\u002Fcollisions_2025_scaled.tif\")\nchange = this_year - last_year\nprint(f\"peak density {this_year.max():.1f} \u002Fkm²; largest increase {change.max():.1f} \u002Fkm²\")\n",[27,1124,1125,1138,1150,1154,1166,1176,1202,1210,1214,1229,1243,1259],{"__ignoreMap":159},[163,1126,1127,1129,1132,1135],{"class":70,"line":165},[163,1128,169],{"class":168},[163,1130,1131],{"class":172}," numpy ",[163,1133,1134],{"class":168},"as",[163,1136,1137],{"class":172}," np\n",[163,1139,1140,1142,1145,1147],{"class":70,"line":176},[163,1141,179],{"class":168},[163,1143,1144],{"class":172}," osgeo ",[163,1146,169],{"class":168},[163,1148,1149],{"class":172}," gdal\n",[163,1151,1152],{"class":70,"line":190},[163,1153,194],{"emptyLinePlaceholder":193},[163,1155,1156,1159,1163],{"class":70,"line":197},[163,1157,1158],{"class":168},"def",[163,1160,1162],{"class":1161},"svObZ"," per_km2",[163,1164,1165],{"class":172},"(path):\n",[163,1167,1168,1171,1173],{"class":70,"line":222},[163,1169,1170],{"class":172},"    ds ",[163,1172,203],{"class":168},[163,1174,1175],{"class":172}," gdal.Open(path)\n",[163,1177,1178,1181,1183,1186,1188,1191,1194,1197,1199],{"class":70,"line":239},[163,1179,1180],{"class":172},"    arr ",[163,1182,203],{"class":168},[163,1184,1185],{"class":172}," ds.GetRasterBand(",[163,1187,410],{"class":216},[163,1189,1190],{"class":172},").ReadAsArray().astype(",[163,1192,1193],{"class":209},"\"float64\"",[163,1195,1196],{"class":172},") ",[163,1198,998],{"class":168},[163,1200,1201],{"class":216}," 1e6\n",[163,1203,1204,1207],{"class":70,"line":248},[163,1205,1206],{"class":168},"    return",[163,1208,1209],{"class":172}," arr, ds\n",[163,1211,1212],{"class":70,"line":267},[163,1213,194],{"emptyLinePlaceholder":193},[163,1215,1216,1219,1221,1224,1227],{"class":70,"line":281},[163,1217,1218],{"class":172},"this_year, ds ",[163,1220,203],{"class":168},[163,1222,1223],{"class":172}," per_km2(",[163,1225,1226],{"class":209},"\"\u002Fdata\u002Fresults\u002Fcollisions_2026_scaled.tif\"",[163,1228,396],{"class":172},[163,1230,1231,1234,1236,1238,1241],{"class":70,"line":297},[163,1232,1233],{"class":172},"last_year, _ ",[163,1235,203],{"class":168},[163,1237,1223],{"class":172},[163,1239,1240],{"class":209},"\"\u002Fdata\u002Fresults\u002Fcollisions_2025_scaled.tif\"",[163,1242,396],{"class":172},[163,1244,1245,1248,1250,1253,1256],{"class":70,"line":309},[163,1246,1247],{"class":172},"change ",[163,1249,203],{"class":168},[163,1251,1252],{"class":172}," this_year ",[163,1254,1255],{"class":168},"-",[163,1257,1258],{"class":172}," last_year\n",[163,1260,1261,1263,1265,1267,1270,1272,1275,1278,1280,1283,1285,1288,1290,1292,1295],{"class":70,"line":325},[163,1262,418],{"class":216},[163,1264,965],{"class":172},[163,1266,684],{"class":168},[163,1268,1269],{"class":209},"\"peak density ",[163,1271,594],{"class":216},[163,1273,1274],{"class":172},"this_year.max()",[163,1276,1277],{"class":168},":.1f",[163,1279,600],{"class":216},[163,1281,1282],{"class":209}," \u002Fkm²; largest increase ",[163,1284,594],{"class":216},[163,1286,1287],{"class":172},"change.max()",[163,1289,1277],{"class":168},[163,1291,600],{"class":216},[163,1293,1294],{"class":209}," \u002Fkm²\"",[163,1296,396],{"class":172},[14,1298,1299,1301,1302,1305,1306,1310,1311,1314],{},[455,1300,457],{}," Scaled values multiplied by a million are events per square kilometre, a unit people understand. Differencing two surfaces needs identical extents and pixel grids — run both with the same ",[27,1303,1304],{},"PIXEL_SIZE"," and clip both to a common extent, or the arrays will not align; ",[21,1307,1309],{"href":1308},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fresample-and-align-rasters-pyqgis\u002F","resampling and aligning rasters"," covers the general case. Writing ",[27,1312,1313],{},"change"," back to a GeoTIFF with the same geotransform gives a map of where density rose and fell.",[129,1316,1318],{"id":1317},"style-the-surface-so-it-reads-correctly","Style the surface so it reads correctly",[14,1320,1321],{},"A density raster's look is part of its message. A sequential colour ramp, transparent low values and a stretch that does not let one extreme pixel flatten everything else make the difference between a useful map and a misleading one.",[154,1323,1325],{"className":156,"code":1324,"language":158,"meta":159,"style":159},"from qgis.core import (QgsSingleBandPseudoColorRenderer, QgsColorRampShader,\n                       QgsRasterShader, QgsStyle)\n\nramp = QgsStyle.defaultStyle().colorRamp(\"Inferno\")\np95 = float(np.percentile(this_year[this_year > 0], 95)) \u002F 1e6\nshader_fn = QgsColorRampShader(0, p95, ramp)\nshader_fn.classifyColorRamp(8)\nitems = shader_fn.colorRampItemList()\nlowest = items[0].color\nlowest.setAlpha(0)                              # lowest class transparent\nitems[0].color = lowest\nshader_fn.setColorRampItemList(items)\nshader = QgsRasterShader()\nshader.setRasterShaderFunction(shader_fn)\ndensity.setRenderer(QgsSingleBandPseudoColorRenderer(density.dataProvider(), 1, shader))\ndensity.triggerRepaint()\n",[27,1326,1327,1338,1343,1347,1362,1393,1408,1417,1427,1442,1455,1470,1475,1485,1490,1500],{"__ignoreMap":159},[163,1328,1329,1331,1333,1335],{"class":70,"line":165},[163,1330,179],{"class":168},[163,1332,182],{"class":172},[163,1334,169],{"class":168},[163,1336,1337],{"class":172}," (QgsSingleBandPseudoColorRenderer, QgsColorRampShader,\n",[163,1339,1340],{"class":70,"line":176},[163,1341,1342],{"class":172},"                       QgsRasterShader, QgsStyle)\n",[163,1344,1345],{"class":70,"line":190},[163,1346,194],{"emptyLinePlaceholder":193},[163,1348,1349,1352,1354,1357,1360],{"class":70,"line":197},[163,1350,1351],{"class":172},"ramp ",[163,1353,203],{"class":168},[163,1355,1356],{"class":172}," QgsStyle.defaultStyle().colorRamp(",[163,1358,1359],{"class":209},"\"Inferno\"",[163,1361,396],{"class":172},[163,1363,1364,1367,1369,1372,1375,1378,1380,1383,1386,1389,1391],{"class":70,"line":222},[163,1365,1366],{"class":172},"p95 ",[163,1368,203],{"class":168},[163,1370,1371],{"class":216}," float",[163,1373,1374],{"class":172},"(np.percentile(this_year[this_year ",[163,1376,1377],{"class":168},">",[163,1379,928],{"class":216},[163,1381,1382],{"class":172},"], ",[163,1384,1385],{"class":216},"95",[163,1387,1388],{"class":172},")) ",[163,1390,640],{"class":168},[163,1392,1201],{"class":216},[163,1394,1395,1398,1400,1403,1405],{"class":70,"line":239},[163,1396,1397],{"class":172},"shader_fn ",[163,1399,203],{"class":168},[163,1401,1402],{"class":172}," QgsColorRampShader(",[163,1404,56],{"class":216},[163,1406,1407],{"class":172},", p95, ramp)\n",[163,1409,1410,1413,1415],{"class":70,"line":248},[163,1411,1412],{"class":172},"shader_fn.classifyColorRamp(",[163,1414,490],{"class":216},[163,1416,396],{"class":172},[163,1418,1419,1422,1424],{"class":70,"line":267},[163,1420,1421],{"class":172},"items ",[163,1423,203],{"class":168},[163,1425,1426],{"class":172}," shader_fn.colorRampItemList()\n",[163,1428,1429,1432,1434,1437,1439],{"class":70,"line":281},[163,1430,1431],{"class":172},"lowest ",[163,1433,203],{"class":168},[163,1435,1436],{"class":172}," items[",[163,1438,56],{"class":216},[163,1440,1441],{"class":172},"].color\n",[163,1443,1444,1447,1449,1452],{"class":70,"line":297},[163,1445,1446],{"class":172},"lowest.setAlpha(",[163,1448,56],{"class":216},[163,1450,1451],{"class":172},")                              ",[163,1453,1454],{"class":263},"# lowest class transparent\n",[163,1456,1457,1460,1462,1465,1467],{"class":70,"line":309},[163,1458,1459],{"class":172},"items[",[163,1461,56],{"class":216},[163,1463,1464],{"class":172},"].color ",[163,1466,203],{"class":168},[163,1468,1469],{"class":172}," lowest\n",[163,1471,1472],{"class":70,"line":325},[163,1473,1474],{"class":172},"shader_fn.setColorRampItemList(items)\n",[163,1476,1477,1480,1482],{"class":70,"line":337},[163,1478,1479],{"class":172},"shader ",[163,1481,203],{"class":168},[163,1483,1484],{"class":172}," QgsRasterShader()\n",[163,1486,1487],{"class":70,"line":353},[163,1488,1489],{"class":172},"shader.setRasterShaderFunction(shader_fn)\n",[163,1491,1492,1495,1497],{"class":70,"line":366},[163,1493,1494],{"class":172},"density.setRenderer(QgsSingleBandPseudoColorRenderer(density.dataProvider(), ",[163,1496,410],{"class":216},[163,1498,1499],{"class":172},", shader))\n",[163,1501,1502],{"class":70,"line":377},[163,1503,1504],{"class":172},"density.triggerRepaint()\n",[14,1506,1507,1509,1510,35],{},[455,1508,457],{}," Stretching to the 95th percentile of non-zero values rather than the maximum stops a single extreme pixel from compressing the rest of the surface into the bottom of the ramp. Making the lowest class transparent lets the basemap show through where density is negligible. A perceptually uniform ramp such as Inferno or Viridis keeps equal steps in value looking like equal steps in colour. More on raster styling in ",[21,1511,1513],{"href":1512},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002Fapply-color-ramp-to-raster-pyqgis\u002F","applying a colour ramp to a raster",[129,1515,1517],{"id":1516},"qgis-version-compatibility","QGIS version compatibility",[14,1519,1520,1522,1523,1526],{},[27,1521,29],{}," has the same parameters on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. Kernel and output-value enums are numbered identically. ",[27,1524,1525],{},"QgsColorRampShader.classifyColorRamp"," with a class count has been available throughout QGIS 3.",[129,1528,1530],{"id":1529},"troubleshooting","Troubleshooting",[134,1532,1533,1539,1545,1551],{},[137,1534,1535,1538],{},[455,1536,1537],{},"The raster is enormous or the run takes forever."," Pixel size is far too small for the extent; check width × height before running.",[137,1540,1541,1544],{},[455,1542,1543],{},"The surface is one smooth blob."," The radius is too large for the pattern.",[137,1546,1547,1550],{},[455,1548,1549],{},"All values are tiny decimals."," You chose scaled output in a metric CRS; multiply by 1e6 for per km².",[137,1552,1553,1556],{},[455,1554,1555],{},"Weighted output looks like unweighted."," The weight field is text or mostly NULL.",[129,1558,1560],{"id":1559},"conclusion","Conclusion",[14,1562,1563],{},"Run kernel density with a radius chosen for the question and a pixel size about a fifth of it, compare a few radii before trusting one, weight points deliberately, use scaled output for anything you compare, and style with a sequential ramp stretched to a high percentile with transparent low values.",[129,1565,1567],{"id":1566},"frequently-asked-questions","Frequently Asked Questions",[14,1569,1570,1573],{},[455,1571,1572],{},"Is this the same as the heatmap renderer?","\nThe renderer computes a similar surface on the fly for display only. The algorithm produces a raster you can analyse, export and compare.",[14,1575,1576,1579],{},[455,1577,1578],{},"Does density respect roads or barriers?","\nNo, kernels are circular in straight-line distance. Network kernel density needs specialised tools.",[14,1581,1582,1585,1586,1590],{},[455,1583,1584],{},"Can I get a hot spot significance map from this?","\nNot directly; density shows where events concentrate, not whether that is significant. Use ",[21,1587,1589],{"href":1588},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fhotspot-analysis-getis-ord-pyqgis\u002F","Getis-Ord Gi*"," for that.",[14,1592,1593,1596,1597,1600,1601,35],{},[455,1594,1595],{},"How do I contour the surface?","\nRun ",[27,1598,1599],{},"gdal:contour"," on the output, as shown in ",[21,1602,1604],{"href":1603},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fcreate-contours-from-dem-pyqgis\u002F","creating contours from a DEM",[129,1606,1608],{"id":1607},"related","Related",[134,1610,1611,1616,1621,1626,1631],{},[137,1612,1613,1615],{},[21,1614,24],{"href":23}," — the guide this recipe belongs to",[137,1617,1618],{},[21,1619,1620],{"href":33},"Create a Heatmap Renderer in PyQGIS",[137,1622,1623],{},[21,1624,1625],{"href":543},"Run a Nearest Neighbour Analysis in PyQGIS",[137,1627,1628],{},[21,1629,1630],{"href":1588},"Run a Hot Spot Analysis with Getis-Ord Gi* in PyQGIS",[137,1632,1633],{},[21,1634,1636],{"href":1635},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fcalculate-raster-statistics-pyqgis\u002F","Calculate Raster Statistics in PyQGIS",[1638,1639,1640],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":159,"searchDepth":176,"depth":176,"links":1642},[1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655],{"id":131,"depth":176,"text":132},{"id":148,"depth":176,"text":149},{"id":461,"depth":176,"text":462},{"id":706,"depth":176,"text":707},{"id":810,"depth":176,"text":811},{"id":1036,"depth":176,"text":1037},{"id":1115,"depth":176,"text":1116},{"id":1317,"depth":176,"text":1318},{"id":1516,"depth":176,"text":1517},{"id":1529,"depth":176,"text":1530},{"id":1559,"depth":176,"text":1560},{"id":1566,"depth":176,"text":1567},{"id":1607,"depth":176,"text":1608},"Turn points into a continuous density surface with the Heatmap (Kernel Density Estimation) algorithm — choosing radius and pixel size, weighting points, picking a kernel and output scaling, normalising for comparison, and styling the raster so it reads correctly.","md",{"slug":1659,"type":1660,"breadcrumb":1661,"datePublished":1662,"dateModified":1662},"kernel-density-heatmap-raster-pyqgis","article","Kernel Density Heatmap Raster","2026-10-02","\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fkernel-density-heatmap-raster-pyqgis",{"title":5,"description":1656},"spatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fkernel-density-heatmap-raster-pyqgis\u002Findex","g_b3kYRrcAsb8a9lJmfrmQ4UQdF8i0eoaNfbshRprn4",1790966264951]