[{"data":1,"prerenderedAt":2538},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fhotspot-analysis-getis-ord-pyqgis":3},{"id":4,"title":5,"body":6,"description":2527,"extension":2528,"meta":2529,"navigation":236,"path":2534,"seo":2535,"stem":2536,"__hash__":2537},"docs\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fhotspot-analysis-getis-ord-pyqgis\u002Findex.md","Run a Hot Spot Analysis with Getis-Ord Gi* in PyQGIS",{"type":7,"value":8,"toc":2513},"minimark",[9,13,17,26,170,175,188,192,195,520,532,536,539,625,980,985,989,992,1236,1245,1249,1252,1311,1601,1606,1610,1613,1906,1920,1924,1927,2353,2365,2369,2398,2402,2428,2432,2435,2439,2457,2463,2469,2475,2479,2509],[10,11,5],"h1",{"id":12},"run-a-hot-spot-analysis-with-getis-ord-gi-in-pyqgis",[14,15,16],"p",{},"A density map shows where events concentrate, but every random scatter of points has some dense areas. A hot spot analysis asks a sharper question: is the concentration around this location higher than you would expect if values were spread at random across the study area? The Getis-Ord Gi* statistic answers it for every feature, returning a z-score — large and positive for hot spots, large and negative for cold spots, near zero for nothing unusual.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002F","Spatial Statistics & Pattern Analysis",". QGIS has no built-in Gi* algorithm, so the recipe computes it directly: aggregate points into a regular grid, define which cells are neighbours, calculate Gi* with NumPy, correct for testing many cells at once, and map hot and cold spots with a diverging style.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,103,108,111,114,118,123,128,132,136,140,146,150,153,161,165],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 290","img","Pipeline from points to hexagon counts, neighbourhoods, Gi* z-scores and significant hot and cold spots","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"From points to significant hot spots",[40,41,42],"desc",{},"Incident points are counted into hexagon cells. Each cell's neighbourhood is defined, for example all cells within 600 metres including itself. Gi* compares the sum of counts in each neighbourhood with what the overall mean would predict, scaled by the variance, giving a z-score. Cells with z-scores beyond the corrected significance threshold are hot or cold spots.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","290","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gsFlowArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Count, define neighbours, score, test",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"24","70","160","120","#fffdf7","#59645f","stroke-width:2",[68,86,90],{"x":87,"y":88,"style":89,"fill":73,"textAnchor":74},"104","121.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","points",[68,92,95],{"x":87,"y":93,"style":94,"fill":83,"textAnchor":74},"145.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","incidents",[97,98],"line",{"x1":99,"y1":100,"x2":101,"y2":100,"stroke":66,"style":102},"184","130","206","stroke-width:1.8;marker-end:url(#gsFlowArrow)",[44,104],{"x":105,"y":79,"width":80,"height":81,"rx":58,"fill":106,"stroke":107,"style":84},"210","#eef7f4","#0f766e",[68,109,110],{"x":48,"y":88,"style":89,"fill":107,"textAnchor":74},"hexagon counts",[68,112,113],{"x":48,"y":93,"style":94,"fill":83,"textAnchor":74},"one value per cell",[97,115],{"x1":116,"y1":100,"x2":117,"y2":100,"stroke":66,"style":102},"370","392",[44,119],{"x":120,"y":79,"width":80,"height":81,"rx":58,"fill":121,"stroke":122,"style":84},"396","#eff3ff","#2563eb",[68,124,127],{"x":125,"y":126,"style":89,"fill":122,"textAnchor":74},"476","109.78","neighbourhoods",[68,129,131],{"x":125,"y":130,"style":94,"fill":83,"textAnchor":74},"133.78","distance band",[68,133,135],{"x":125,"y":134,"style":94,"fill":83,"textAnchor":74},"157.78","includes self",[97,137],{"x1":138,"y1":100,"x2":139,"y2":100,"stroke":66,"style":102},"556","578",[44,141],{"x":142,"y":79,"width":143,"height":81,"rx":58,"fill":144,"stroke":145,"style":84},"582","154","#fdf2e2","#b45309",[68,147,149],{"x":148,"y":88,"style":89,"fill":145,"textAnchor":74},"659","Gi* z-score",[68,151,152],{"x":148,"y":93,"style":94,"fill":83,"textAnchor":74},"per cell",[44,154],{"x":155,"y":156,"width":157,"height":158,"rx":58,"fill":159,"stroke":160,"style":84},"140","214","480","50","#e8efe6","#15803d",[68,162,164],{"x":70,"y":163,"style":94,"fill":66,"textAnchor":74},"242.78","significant after correction → hot · cold · not significant",[97,166],{"x1":148,"y1":167,"x2":168,"y2":169,"stroke":66,"style":102},"190","560","212",[171,172,174],"h2",{"id":173},"prerequisites","Prerequisites",[176,177,178,182,185],"ul",{},[179,180,181],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series. NumPy ships with QGIS.",[179,183,184],{},"A point layer of events in a projected CRS, and the boundary of the study area.",[179,186,187],{},"A neighbourhood scale in mind — the distance at which you expect events to influence each other.",[171,189,191],{"id":190},"aggregate-points-to-a-grid","Aggregate points to a grid",[14,193,194],{},"Gi* works on values attached to areas. Counting points into a regular hexagon grid gives every cell a value and every cell the same size and shape, which keeps neighbourhoods comparable.",[196,197,202],"pre",{"className":198,"code":199,"language":200,"meta":201,"style":201},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsProject\n\nincidents = QgsProject.instance().mapLayersByName(\"burglaries\")[0]\ncity = QgsProject.instance().mapLayersByName(\"city_boundary\")[0]\n\ngrid = processing.run(\"native:creategrid\", {\n    \"TYPE\": 4, \"EXTENT\": city.extent(), \"HSPACING\": 250, \"VSPACING\": 250,\n    \"CRS\": city.crs(), \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\ngrid = processing.run(\"native:extractbylocation\", {\n    \"INPUT\": grid, \"PREDICATE\": [0], \"INTERSECT\": city, \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\ncounts = processing.run(\"native:countpointsinpolygon\", {\n    \"POLYGONS\": grid, \"POINTS\": incidents, \"FIELD\": \"n\",\n    \"OUTPUT\": \"memory:hex_counts\"})[\"OUTPUT\"]\nprint(counts.featureCount(), \"cells;\", sum(f[\"n\"] for f in counts.getFeatures()), \"points\")\n","python","",[203,204,205,217,231,238,263,282,287,304,345,369,383,421,436,460,477],"code",{"__ignoreMap":201},[206,207,209,213],"span",{"class":97,"line":208},1,[206,210,212],{"class":211},"snl16","import",[206,214,216],{"class":215},"s95oV"," processing\n",[206,218,220,223,226,228],{"class":97,"line":219},2,[206,221,222],{"class":211},"from",[206,224,225],{"class":215}," qgis.core ",[206,227,212],{"class":211},[206,229,230],{"class":215}," QgsProject\n",[206,232,234],{"class":97,"line":233},3,[206,235,237],{"emptyLinePlaceholder":236},true,"\n",[206,239,241,244,247,250,254,257,260],{"class":97,"line":240},4,[206,242,243],{"class":215},"incidents ",[206,245,246],{"class":211},"=",[206,248,249],{"class":215}," QgsProject.instance().mapLayersByName(",[206,251,253],{"class":252},"sU2Wk","\"burglaries\"",[206,255,256],{"class":215},")[",[206,258,46],{"class":259},"sDLfK",[206,261,262],{"class":215},"]\n",[206,264,266,269,271,273,276,278,280],{"class":97,"line":265},5,[206,267,268],{"class":215},"city ",[206,270,246],{"class":211},[206,272,249],{"class":215},[206,274,275],{"class":252},"\"city_boundary\"",[206,277,256],{"class":215},[206,279,46],{"class":259},[206,281,262],{"class":215},[206,283,285],{"class":97,"line":284},6,[206,286,237],{"emptyLinePlaceholder":236},[206,288,290,293,295,298,301],{"class":97,"line":289},7,[206,291,292],{"class":215},"grid ",[206,294,246],{"class":211},[206,296,297],{"class":215}," processing.run(",[206,299,300],{"class":252},"\"native:creategrid\"",[206,302,303],{"class":215},", {\n",[206,305,307,310,313,316,319,322,325,328,330,333,335,338,340,342],{"class":97,"line":306},8,[206,308,309],{"class":252},"    \"TYPE\"",[206,311,312],{"class":215},": ",[206,314,315],{"class":259},"4",[206,317,318],{"class":215},", ",[206,320,321],{"class":252},"\"EXTENT\"",[206,323,324],{"class":215},": city.extent(), ",[206,326,327],{"class":252},"\"HSPACING\"",[206,329,312],{"class":215},[206,331,332],{"class":259},"250",[206,334,318],{"class":215},[206,336,337],{"class":252},"\"VSPACING\"",[206,339,312],{"class":215},[206,341,332],{"class":259},[206,343,344],{"class":215},",\n",[206,346,348,351,354,357,359,362,365,367],{"class":97,"line":347},9,[206,349,350],{"class":252},"    \"CRS\"",[206,352,353],{"class":215},": city.crs(), ",[206,355,356],{"class":252},"\"OUTPUT\"",[206,358,312],{"class":215},[206,360,361],{"class":252},"\"memory:\"",[206,363,364],{"class":215},"})[",[206,366,356],{"class":252},[206,368,262],{"class":215},[206,370,372,374,376,378,381],{"class":97,"line":371},10,[206,373,292],{"class":215},[206,375,246],{"class":211},[206,377,297],{"class":215},[206,379,380],{"class":252},"\"native:extractbylocation\"",[206,382,303],{"class":215},[206,384,386,389,392,395,398,400,403,406,409,411,413,415,417,419],{"class":97,"line":385},11,[206,387,388],{"class":252},"    \"INPUT\"",[206,390,391],{"class":215},": grid, ",[206,393,394],{"class":252},"\"PREDICATE\"",[206,396,397],{"class":215},": [",[206,399,46],{"class":259},[206,401,402],{"class":215},"], ",[206,404,405],{"class":252},"\"INTERSECT\"",[206,407,408],{"class":215},": city, ",[206,410,356],{"class":252},[206,412,312],{"class":215},[206,414,361],{"class":252},[206,416,364],{"class":215},[206,418,356],{"class":252},[206,420,262],{"class":215},[206,422,424,427,429,431,434],{"class":97,"line":423},12,[206,425,426],{"class":215},"counts ",[206,428,246],{"class":211},[206,430,297],{"class":215},[206,432,433],{"class":252},"\"native:countpointsinpolygon\"",[206,435,303],{"class":215},[206,437,439,442,444,447,450,453,455,458],{"class":97,"line":438},13,[206,440,441],{"class":252},"    \"POLYGONS\"",[206,443,391],{"class":215},[206,445,446],{"class":252},"\"POINTS\"",[206,448,449],{"class":215},": incidents, ",[206,451,452],{"class":252},"\"FIELD\"",[206,454,312],{"class":215},[206,456,457],{"class":252},"\"n\"",[206,459,344],{"class":215},[206,461,463,466,468,471,473,475],{"class":97,"line":462},14,[206,464,465],{"class":252},"    \"OUTPUT\"",[206,467,312],{"class":215},[206,469,470],{"class":252},"\"memory:hex_counts\"",[206,472,364],{"class":215},[206,474,356],{"class":252},[206,476,262],{"class":215},[206,478,480,483,486,489,491,494,497,499,502,505,508,511,514,517],{"class":97,"line":479},15,[206,481,482],{"class":259},"print",[206,484,485],{"class":215},"(counts.featureCount(), ",[206,487,488],{"class":252},"\"cells;\"",[206,490,318],{"class":215},[206,492,493],{"class":259},"sum",[206,495,496],{"class":215},"(f[",[206,498,457],{"class":252},[206,500,501],{"class":215},"] ",[206,503,504],{"class":211},"for",[206,506,507],{"class":215}," f ",[206,509,510],{"class":211},"in",[206,512,513],{"class":215}," counts.getFeatures()), ",[206,515,516],{"class":252},"\"points\"",[206,518,519],{"class":215},")\n",[14,521,522,526,527,531],{},[523,524,525],"strong",{},"Breakdown:"," Grid type 4 is hexagonal; hexagons have six equidistant neighbours, which suits distance-based neighbourhoods better than squares. Keeping only cells that intersect the city removes empty sea or countryside cells that would drag the mean down and make everything look hot. Cell size should be small enough to locate hot spots usefully but large enough that most cells contain a few events; ",[21,528,530],{"href":529},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fcreate-hexagon-grid-and-count-points-pyqgis\u002F","creating a hexagon grid and counting points"," covers the choice.",[171,533,535],{"id":534},"define-neighbours","Define neighbours",[14,537,538],{},"Gi* sums values over each cell's neighbourhood, so the neighbourhood definition is the analysis's scale. A distance band — all cells whose centres lie within a given distance, including the cell itself — is the standard choice.",[14,540,541],{},[29,542,545,548,551,553,556,561,565,568,570,574,577,579,581,584,587,590,596,601,605,607,611,615,617,621],{"viewBox":543,"role":32,"ariaLabel":544,"xmlns":34},"0 0 760 250","A central cell with the surrounding cells inside a distance band forming its neighbourhood, and cells outside the band excluded",[36,546,547],{},"A distance band neighbourhood",[40,549,550],{},"A central hexagon and the ring of cells whose centres lie within the distance band, here 600 metres, around it. All cells inside the band, including the central one, form the neighbourhood with weight one; cells outside have weight zero. A larger band smooths results and finds broader hot spots; a smaller band finds tighter ones.",[44,552],{"x":46,"y":46,"width":47,"height":332,"fill":49},[68,554,555],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Everything within the band counts, including itself",[557,558],"circle",{"cx":559,"cy":100,"r":560,"fill":121,"stroke":122,"style":84},"260","95",[557,562],{"cx":559,"cy":100,"r":563,"fill":145,"stroke":145,"style":564},"14","stroke-width:1",[557,566],{"cx":559,"cy":79,"r":567,"fill":122,"stroke":122,"style":564},"10",[557,569],{"cx":559,"cy":167,"r":567,"fill":122,"stroke":122,"style":564},[557,571],{"cx":572,"cy":573,"r":567,"fill":122,"stroke":122,"style":564},"208","100",[557,575],{"cx":576,"cy":573,"r":567,"fill":122,"stroke":122,"style":564},"312",[557,578],{"cx":572,"cy":80,"r":567,"fill":122,"stroke":122,"style":564},[557,580],{"cx":576,"cy":80,"r":567,"fill":122,"stroke":122,"style":564},[557,582],{"cx":80,"cy":583,"r":58,"fill":83,"stroke":83,"style":564},"40",[557,585],{"cx":116,"cy":586,"r":58,"fill":83,"stroke":83,"style":564},"230",[557,588],{"cx":589,"cy":583,"r":58,"fill":83,"stroke":83,"style":564},"390",[44,591],{"x":592,"y":593,"width":594,"height":595,"rx":58,"fill":144,"stroke":145,"style":84},"430","56","306","52",[68,597,600],{"x":598,"y":599,"style":89,"fill":145,"textAnchor":74},"583","75.78","the cell itself",[68,602,604],{"x":598,"y":603,"style":94,"fill":83,"textAnchor":74},"95.78","weight 1 (the * in Gi*)",[44,606],{"x":592,"y":81,"width":594,"height":595,"rx":58,"fill":121,"stroke":122,"style":84},[68,608,610],{"x":598,"y":609,"style":89,"fill":122,"textAnchor":74},"139.78","centres within 600 m",[68,612,614],{"x":598,"y":613,"style":94,"fill":83,"textAnchor":74},"159.78","weight 1",[44,616],{"x":592,"y":99,"width":594,"height":595,"rx":58,"fill":82,"stroke":83,"style":84},[68,618,620],{"x":598,"y":619,"style":89,"fill":73,"textAnchor":74},"203.78","outside the band",[68,622,624],{"x":598,"y":623,"style":94,"fill":83,"textAnchor":74},"223.78","weight 0",[196,626,628],{"className":198,"code":627,"language":200,"meta":201,"style":201},"import numpy as np\nfrom qgis.core import QgsSpatialIndex\n\nfeats = list(counts.getFeatures())\nids = [f.id() for f in feats]\nx = np.array([f[\"n\"] for f in feats], dtype=float)\ncentres = [f.geometry().centroid() for f in feats]\npos = {fid: i for i, fid in enumerate(ids)}\n\nBAND = 600  # metres\nindex = QgsSpatialIndex()\nfor f, c in zip(feats, centres):\n    index.addFeature(f.id(), c.boundingBox())\n\nneighbours = []\nfor i, c in enumerate(centres):\n    cand = index.intersects(c.boundingBox().buffered(BAND))\n    neighbours.append([pos[j] for j in cand if c.distance(centres[pos[j]]) \u003C= BAND])\nsizes = [len(n) for n in neighbours]\nprint(f\"neighbours per cell: min {min(sizes)}, median {sorted(sizes)[len(sizes) \u002F\u002F 2]}, max {max(sizes)}\")\n",[203,629,630,643,654,658,671,690,724,742,765,769,784,794,809,814,818,828,843,859,890,917],{"__ignoreMap":201},[206,631,632,634,637,640],{"class":97,"line":208},[206,633,212],{"class":211},[206,635,636],{"class":215}," numpy ",[206,638,639],{"class":211},"as",[206,641,642],{"class":215}," np\n",[206,644,645,647,649,651],{"class":97,"line":219},[206,646,222],{"class":211},[206,648,225],{"class":215},[206,650,212],{"class":211},[206,652,653],{"class":215}," QgsSpatialIndex\n",[206,655,656],{"class":97,"line":233},[206,657,237],{"emptyLinePlaceholder":236},[206,659,660,663,665,668],{"class":97,"line":240},[206,661,662],{"class":215},"feats ",[206,664,246],{"class":211},[206,666,667],{"class":259}," list",[206,669,670],{"class":215},"(counts.getFeatures())\n",[206,672,673,676,678,681,683,685,687],{"class":97,"line":265},[206,674,675],{"class":215},"ids ",[206,677,246],{"class":211},[206,679,680],{"class":215}," [f.id() ",[206,682,504],{"class":211},[206,684,507],{"class":215},[206,686,510],{"class":211},[206,688,689],{"class":215}," feats]\n",[206,691,692,695,697,700,702,704,706,708,710,713,717,719,722],{"class":97,"line":284},[206,693,694],{"class":215},"x ",[206,696,246],{"class":211},[206,698,699],{"class":215}," np.array([f[",[206,701,457],{"class":252},[206,703,501],{"class":215},[206,705,504],{"class":211},[206,707,507],{"class":215},[206,709,510],{"class":211},[206,711,712],{"class":215}," feats], ",[206,714,716],{"class":715},"s9osk","dtype",[206,718,246],{"class":211},[206,720,721],{"class":259},"float",[206,723,519],{"class":215},[206,725,726,729,731,734,736,738,740],{"class":97,"line":289},[206,727,728],{"class":215},"centres ",[206,730,246],{"class":211},[206,732,733],{"class":215}," [f.geometry().centroid() ",[206,735,504],{"class":211},[206,737,507],{"class":215},[206,739,510],{"class":211},[206,741,689],{"class":215},[206,743,744,747,749,752,754,757,759,762],{"class":97,"line":306},[206,745,746],{"class":215},"pos ",[206,748,246],{"class":211},[206,750,751],{"class":215}," {fid: i ",[206,753,504],{"class":211},[206,755,756],{"class":215}," i, fid ",[206,758,510],{"class":211},[206,760,761],{"class":259}," enumerate",[206,763,764],{"class":215},"(ids)}\n",[206,766,767],{"class":97,"line":347},[206,768,237],{"emptyLinePlaceholder":236},[206,770,771,774,777,780],{"class":97,"line":371},[206,772,773],{"class":259},"BAND",[206,775,776],{"class":211}," =",[206,778,779],{"class":259}," 600",[206,781,783],{"class":782},"sjoCn","  # metres\n",[206,785,786,789,791],{"class":97,"line":385},[206,787,788],{"class":215},"index ",[206,790,246],{"class":211},[206,792,793],{"class":215}," QgsSpatialIndex()\n",[206,795,796,798,801,803,806],{"class":97,"line":423},[206,797,504],{"class":211},[206,799,800],{"class":215}," f, c ",[206,802,510],{"class":211},[206,804,805],{"class":259}," zip",[206,807,808],{"class":215},"(feats, centres):\n",[206,810,811],{"class":97,"line":438},[206,812,813],{"class":215},"    index.addFeature(f.id(), c.boundingBox())\n",[206,815,816],{"class":97,"line":462},[206,817,237],{"emptyLinePlaceholder":236},[206,819,820,823,825],{"class":97,"line":479},[206,821,822],{"class":215},"neighbours ",[206,824,246],{"class":211},[206,826,827],{"class":215}," []\n",[206,829,831,833,836,838,840],{"class":97,"line":830},16,[206,832,504],{"class":211},[206,834,835],{"class":215}," i, c ",[206,837,510],{"class":211},[206,839,761],{"class":259},[206,841,842],{"class":215},"(centres):\n",[206,844,846,849,851,854,856],{"class":97,"line":845},17,[206,847,848],{"class":215},"    cand ",[206,850,246],{"class":211},[206,852,853],{"class":215}," index.intersects(c.boundingBox().buffered(",[206,855,773],{"class":259},[206,857,858],{"class":215},"))\n",[206,860,862,865,867,870,872,875,878,881,884,887],{"class":97,"line":861},18,[206,863,864],{"class":215},"    neighbours.append([pos[j] ",[206,866,504],{"class":211},[206,868,869],{"class":215}," j ",[206,871,510],{"class":211},[206,873,874],{"class":215}," cand ",[206,876,877],{"class":211},"if",[206,879,880],{"class":215}," c.distance(centres[pos[j]]) ",[206,882,883],{"class":211},"\u003C=",[206,885,886],{"class":259}," BAND",[206,888,889],{"class":215},"])\n",[206,891,893,896,898,901,904,907,909,912,914],{"class":97,"line":892},19,[206,894,895],{"class":215},"sizes ",[206,897,246],{"class":211},[206,899,900],{"class":215}," [",[206,902,903],{"class":259},"len",[206,905,906],{"class":215},"(n) ",[206,908,504],{"class":211},[206,910,911],{"class":215}," n ",[206,913,510],{"class":211},[206,915,916],{"class":215}," neighbours]\n",[206,918,920,922,925,928,931,934,937,940,943,946,949,951,954,957,960,963,965,968,971,973,975,978],{"class":97,"line":919},20,[206,921,482],{"class":259},[206,923,924],{"class":215},"(",[206,926,927],{"class":211},"f",[206,929,930],{"class":252},"\"neighbours per cell: min ",[206,932,933],{"class":259},"{min",[206,935,936],{"class":215},"(sizes)",[206,938,939],{"class":259},"}",[206,941,942],{"class":252},", median ",[206,944,945],{"class":259},"{sorted",[206,947,948],{"class":215},"(sizes)[",[206,950,903],{"class":259},[206,952,953],{"class":215},"(sizes) ",[206,955,956],{"class":211},"\u002F\u002F",[206,958,959],{"class":259}," 2",[206,961,962],{"class":215},"]",[206,964,939],{"class":259},[206,966,967],{"class":252},", max ",[206,969,970],{"class":259},"{max",[206,972,936],{"class":215},[206,974,939],{"class":259},[206,976,977],{"class":252},"\"",[206,979,519],{"class":215},[14,981,982,984],{},[523,983,525],{}," Indexing cell centres rather than hexagons makes the distance test a centre-to-centre distance, which is the conventional definition. Each list includes the cell itself, because its distance to itself is zero — that is what distinguishes Gi* from Gi. Every cell should have several neighbours; cells with only themselves give meaningless z-scores, so if the minimum is 1, the band is too small for the grid spacing. A band of two to three cell widths is a reasonable start, and comparing results at two bands shows how scale-dependent the hot spots are.",[171,986,988],{"id":987},"compute-gi-with-numpy","Compute Gi* with NumPy",[14,990,991],{},"With values and neighbour lists in hand, the statistic is a few lines of arithmetic: compare each neighbourhood's sum with what the global mean predicts, and scale by the expected spread.",[196,993,995],{"className":198,"code":994,"language":200,"meta":201,"style":201},"n = len(x)\nxbar = x.mean()\ns = np.sqrt((x ** 2).mean() - xbar ** 2)\n\nz = np.zeros(n)\nfor i, nb in enumerate(neighbours):\n    w_sum = len(nb)                         # binary weights: Σw = Σw²\n    local = x[nb].sum()\n    num = local - xbar * w_sum\n    den = s * np.sqrt((n * w_sum - w_sum ** 2) \u002F (n - 1))\n    z[i] = num \u002F den if den > 0 else 0.0\nprint(f\"z range {z.min():.2f} to {z.max():.2f}\")\n",[203,996,997,1010,1020,1050,1054,1064,1078,1093,1103,1123,1167,1198],{"__ignoreMap":201},[206,998,999,1002,1004,1007],{"class":97,"line":208},[206,1000,1001],{"class":215},"n ",[206,1003,246],{"class":211},[206,1005,1006],{"class":259}," len",[206,1008,1009],{"class":215},"(x)\n",[206,1011,1012,1015,1017],{"class":97,"line":219},[206,1013,1014],{"class":215},"xbar ",[206,1016,246],{"class":211},[206,1018,1019],{"class":215}," x.mean()\n",[206,1021,1022,1025,1027,1030,1033,1035,1038,1041,1044,1046,1048],{"class":97,"line":233},[206,1023,1024],{"class":215},"s ",[206,1026,246],{"class":211},[206,1028,1029],{"class":215}," np.sqrt((x ",[206,1031,1032],{"class":211},"**",[206,1034,959],{"class":259},[206,1036,1037],{"class":215},").mean() ",[206,1039,1040],{"class":211},"-",[206,1042,1043],{"class":215}," xbar ",[206,1045,1032],{"class":211},[206,1047,959],{"class":259},[206,1049,519],{"class":215},[206,1051,1052],{"class":97,"line":240},[206,1053,237],{"emptyLinePlaceholder":236},[206,1055,1056,1059,1061],{"class":97,"line":265},[206,1057,1058],{"class":215},"z ",[206,1060,246],{"class":211},[206,1062,1063],{"class":215}," np.zeros(n)\n",[206,1065,1066,1068,1071,1073,1075],{"class":97,"line":284},[206,1067,504],{"class":211},[206,1069,1070],{"class":215}," i, nb ",[206,1072,510],{"class":211},[206,1074,761],{"class":259},[206,1076,1077],{"class":215},"(neighbours):\n",[206,1079,1080,1083,1085,1087,1090],{"class":97,"line":289},[206,1081,1082],{"class":215},"    w_sum ",[206,1084,246],{"class":211},[206,1086,1006],{"class":259},[206,1088,1089],{"class":215},"(nb)                         ",[206,1091,1092],{"class":782},"# binary weights: Σw = Σw²\n",[206,1094,1095,1098,1100],{"class":97,"line":306},[206,1096,1097],{"class":215},"    local ",[206,1099,246],{"class":211},[206,1101,1102],{"class":215}," x[nb].sum()\n",[206,1104,1105,1108,1110,1113,1115,1117,1120],{"class":97,"line":347},[206,1106,1107],{"class":215},"    num ",[206,1109,246],{"class":211},[206,1111,1112],{"class":215}," local ",[206,1114,1040],{"class":211},[206,1116,1043],{"class":215},[206,1118,1119],{"class":211},"*",[206,1121,1122],{"class":215}," w_sum\n",[206,1124,1125,1128,1130,1133,1135,1138,1140,1143,1145,1147,1149,1151,1154,1157,1160,1162,1165],{"class":97,"line":371},[206,1126,1127],{"class":215},"    den ",[206,1129,246],{"class":211},[206,1131,1132],{"class":215}," s ",[206,1134,1119],{"class":211},[206,1136,1137],{"class":215}," np.sqrt((n ",[206,1139,1119],{"class":211},[206,1141,1142],{"class":215}," w_sum ",[206,1144,1040],{"class":211},[206,1146,1142],{"class":215},[206,1148,1032],{"class":211},[206,1150,959],{"class":259},[206,1152,1153],{"class":215},") ",[206,1155,1156],{"class":211},"\u002F",[206,1158,1159],{"class":215}," (n ",[206,1161,1040],{"class":211},[206,1163,1164],{"class":259}," 1",[206,1166,858],{"class":215},[206,1168,1169,1172,1174,1177,1179,1182,1184,1186,1189,1192,1195],{"class":97,"line":385},[206,1170,1171],{"class":215},"    z[i] ",[206,1173,246],{"class":211},[206,1175,1176],{"class":215}," num ",[206,1178,1156],{"class":211},[206,1180,1181],{"class":215}," den ",[206,1183,877],{"class":211},[206,1185,1181],{"class":215},[206,1187,1188],{"class":211},">",[206,1190,1191],{"class":259}," 0",[206,1193,1194],{"class":211}," else",[206,1196,1197],{"class":259}," 0.0\n",[206,1199,1200,1202,1204,1206,1209,1212,1215,1218,1220,1223,1225,1228,1230,1232,1234],{"class":97,"line":423},[206,1201,482],{"class":259},[206,1203,924],{"class":215},[206,1205,927],{"class":211},[206,1207,1208],{"class":252},"\"z range ",[206,1210,1211],{"class":259},"{",[206,1213,1214],{"class":215},"z.min()",[206,1216,1217],{"class":211},":.2f",[206,1219,939],{"class":259},[206,1221,1222],{"class":252}," to ",[206,1224,1211],{"class":259},[206,1226,1227],{"class":215},"z.max()",[206,1229,1217],{"class":211},[206,1231,939],{"class":259},[206,1233,977],{"class":252},[206,1235,519],{"class":215},[14,1237,1238,1240,1241,1244],{},[523,1239,525],{}," With binary weights — 1 inside the band, 0 outside — the sum of weights and the sum of squared weights are both just the neighbour count, which simplifies the formula. The numerator is how much the neighbourhood's total exceeds what the global mean would give for that many cells; the denominator is its standard deviation under randomness. The result is a z-score directly: values beyond ±1.96 are significant at the 5 % level for a single test — but this analysis runs hundreds of tests at once, which the next step addresses. If ",[203,1242,1243],{},"s"," is zero every cell has the same count and there is nothing to find.",[171,1246,1248],{"id":1247},"correct-for-many-tests","Correct for many tests",[14,1250,1251],{},"Testing every cell at the 5 % level guarantees false hot spots: with a thousand cells, about fifty pass by chance alone. Neighbouring cells also share data, so their tests are not independent. A false discovery rate correction is the usual remedy.",[14,1253,1254],{},[29,1255,1258,1261,1264,1267,1274,1277,1282,1287,1290,1294,1296,1301,1304,1307],{"viewBox":1256,"role":32,"ariaLabel":1257,"xmlns":34},"0 0 760 216","Uncorrected testing producing many chance hot spots compared with a false discovery rate correction that keeps fewer, trustworthy ones",[36,1259,1260],{},"Uncorrected versus corrected thresholds",[40,1262,1263],{},"With one thousand cells tested at the five percent level, roughly fifty appear significant by chance alone. The Benjamini–Hochberg false discovery rate correction ranks p-values and raises the bar so that, among cells called significant, the expected share of false discoveries stays at five percent. Fewer cells pass, and those that do are trustworthy.",[44,1265],{"x":46,"y":46,"width":47,"height":1266,"fill":49},"216",[51,1268,1269],{},[54,1270,1272],{"id":1271,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gsFdrArrow",[63,1273],{"d":65,"fill":66},[68,1275,1276],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Many tests need a stricter bar",[44,1278],{"x":78,"y":1279,"width":1280,"height":155,"rx":58,"fill":144,"stroke":1281,"style":84},"60","340","#b91c1c",[68,1283,1286],{"x":1284,"y":1285,"style":89,"fill":1281,"textAnchor":74},"194","105.78","uncorrected, |z| > 1.96",[68,1288,1289],{"x":1284,"y":130,"style":94,"fill":83,"textAnchor":74},"1000 cells × 5 % ≈ 50",[68,1291,1293],{"x":1284,"y":1292,"style":94,"fill":83,"textAnchor":74},"161.78","chance hot spots",[44,1295],{"x":120,"y":1279,"width":1280,"height":155,"rx":58,"fill":159,"stroke":160,"style":84},[68,1297,1300],{"x":1298,"y":1285,"style":89,"fill":1299,"textAnchor":74},"566","#166534","FDR corrected",[68,1302,1303],{"x":1298,"y":130,"style":94,"fill":83,"textAnchor":74},"expected false share 5 %",[68,1305,1306],{"x":1298,"y":1292,"style":94,"fill":83,"textAnchor":74},"fewer, trustworthy",[97,1308],{"x1":1309,"y1":100,"x2":117,"y2":100,"stroke":66,"style":1310},"364","stroke-width:1.8;marker-end:url(#gsFdrArrow)",[196,1312,1314],{"className":198,"code":1313,"language":200,"meta":201,"style":201},"from math import erf, sqrt\n\np = np.array([2 * (1 - 0.5 * (1 + erf(abs(v) \u002F sqrt(2)))) for v in z])   # two-sided\norder = np.argsort(p)\nm = len(p)\nthresh = 0.05 * (np.arange(1, m + 1) \u002F m)\npassed = p[order] \u003C= thresh\ncutoff = p[order][passed].max() if passed.any() else 0.0\nsignificant = p \u003C= cutoff\n\nhot = int(((z > 0) & significant).sum())\ncold = int(((z \u003C 0) & significant).sum())\nprint(f\"{hot} hot and {cold} cold cells after FDR correction (cutoff p = {cutoff:.4f})\")\n",[203,1315,1316,1328,1332,1401,1411,1423,1455,1470,1490,1505,1509,1534,1556],{"__ignoreMap":201},[206,1317,1318,1320,1323,1325],{"class":97,"line":208},[206,1319,222],{"class":211},[206,1321,1322],{"class":215}," math ",[206,1324,212],{"class":211},[206,1326,1327],{"class":215}," erf, sqrt\n",[206,1329,1330],{"class":97,"line":219},[206,1331,237],{"emptyLinePlaceholder":236},[206,1333,1334,1337,1339,1342,1345,1348,1351,1354,1357,1360,1362,1364,1366,1369,1372,1375,1378,1380,1383,1385,1388,1390,1393,1395,1398],{"class":97,"line":233},[206,1335,1336],{"class":215},"p ",[206,1338,246],{"class":211},[206,1340,1341],{"class":215}," np.array([",[206,1343,1344],{"class":259},"2",[206,1346,1347],{"class":211}," *",[206,1349,1350],{"class":215}," (",[206,1352,1353],{"class":259},"1",[206,1355,1356],{"class":211}," -",[206,1358,1359],{"class":259}," 0.5",[206,1361,1347],{"class":211},[206,1363,1350],{"class":215},[206,1365,1353],{"class":259},[206,1367,1368],{"class":211}," +",[206,1370,1371],{"class":215}," erf(",[206,1373,1374],{"class":259},"abs",[206,1376,1377],{"class":215},"(v) ",[206,1379,1156],{"class":211},[206,1381,1382],{"class":215}," sqrt(",[206,1384,1344],{"class":259},[206,1386,1387],{"class":215},")))) ",[206,1389,504],{"class":211},[206,1391,1392],{"class":215}," v ",[206,1394,510],{"class":211},[206,1396,1397],{"class":215}," z])   ",[206,1399,1400],{"class":782},"# two-sided\n",[206,1402,1403,1406,1408],{"class":97,"line":240},[206,1404,1405],{"class":215},"order ",[206,1407,246],{"class":211},[206,1409,1410],{"class":215}," np.argsort(p)\n",[206,1412,1413,1416,1418,1420],{"class":97,"line":265},[206,1414,1415],{"class":215},"m ",[206,1417,246],{"class":211},[206,1419,1006],{"class":259},[206,1421,1422],{"class":215},"(p)\n",[206,1424,1425,1428,1430,1433,1435,1438,1440,1443,1446,1448,1450,1452],{"class":97,"line":284},[206,1426,1427],{"class":215},"thresh ",[206,1429,246],{"class":211},[206,1431,1432],{"class":259}," 0.05",[206,1434,1347],{"class":211},[206,1436,1437],{"class":215}," (np.arange(",[206,1439,1353],{"class":259},[206,1441,1442],{"class":215},", m ",[206,1444,1445],{"class":211},"+",[206,1447,1164],{"class":259},[206,1449,1153],{"class":215},[206,1451,1156],{"class":211},[206,1453,1454],{"class":215}," m)\n",[206,1456,1457,1460,1462,1465,1467],{"class":97,"line":289},[206,1458,1459],{"class":215},"passed ",[206,1461,246],{"class":211},[206,1463,1464],{"class":215}," p[order] ",[206,1466,883],{"class":211},[206,1468,1469],{"class":215}," thresh\n",[206,1471,1472,1475,1477,1480,1482,1485,1488],{"class":97,"line":306},[206,1473,1474],{"class":215},"cutoff ",[206,1476,246],{"class":211},[206,1478,1479],{"class":215}," p[order][passed].max() ",[206,1481,877],{"class":211},[206,1483,1484],{"class":215}," passed.any() ",[206,1486,1487],{"class":211},"else",[206,1489,1197],{"class":259},[206,1491,1492,1495,1497,1500,1502],{"class":97,"line":347},[206,1493,1494],{"class":215},"significant ",[206,1496,246],{"class":211},[206,1498,1499],{"class":215}," p ",[206,1501,883],{"class":211},[206,1503,1504],{"class":215}," cutoff\n",[206,1506,1507],{"class":97,"line":371},[206,1508,237],{"emptyLinePlaceholder":236},[206,1510,1511,1514,1516,1519,1522,1524,1526,1528,1531],{"class":97,"line":385},[206,1512,1513],{"class":215},"hot ",[206,1515,246],{"class":211},[206,1517,1518],{"class":259}," int",[206,1520,1521],{"class":215},"(((z ",[206,1523,1188],{"class":211},[206,1525,1191],{"class":259},[206,1527,1153],{"class":215},[206,1529,1530],{"class":211},"&",[206,1532,1533],{"class":215}," significant).sum())\n",[206,1535,1536,1539,1541,1543,1545,1548,1550,1552,1554],{"class":97,"line":423},[206,1537,1538],{"class":215},"cold ",[206,1540,246],{"class":211},[206,1542,1518],{"class":259},[206,1544,1521],{"class":215},[206,1546,1547],{"class":211},"\u003C",[206,1549,1191],{"class":259},[206,1551,1153],{"class":215},[206,1553,1530],{"class":211},[206,1555,1533],{"class":215},[206,1557,1558,1560,1562,1564,1566,1568,1571,1573,1576,1578,1581,1583,1586,1588,1591,1594,1596,1599],{"class":97,"line":438},[206,1559,482],{"class":259},[206,1561,924],{"class":215},[206,1563,927],{"class":211},[206,1565,977],{"class":252},[206,1567,1211],{"class":259},[206,1569,1570],{"class":215},"hot",[206,1572,939],{"class":259},[206,1574,1575],{"class":252}," hot and ",[206,1577,1211],{"class":259},[206,1579,1580],{"class":215},"cold",[206,1582,939],{"class":259},[206,1584,1585],{"class":252}," cold cells after FDR correction (cutoff p = ",[206,1587,1211],{"class":259},[206,1589,1590],{"class":215},"cutoff",[206,1592,1593],{"class":211},":.4f",[206,1595,939],{"class":259},[206,1597,1598],{"class":252},")\"",[206,1600,519],{"class":215},[14,1602,1603,1605],{},[523,1604,525],{}," The two-sided p-value comes from the standard normal distribution via the error function, so no SciPy is needed. The Benjamini–Hochberg procedure sorts p-values, compares each with an increasing threshold, and declares significant everything up to the largest p-value that passes. It controls the expected share of false discoveries among the cells called significant, which is the property a hot spot map needs. Bonferroni — dividing 0.05 by the number of tests — is simpler but far stricter and usually finds nothing on real data.",[171,1607,1609],{"id":1608},"write-results-and-map-them","Write results and map them",[14,1611,1612],{},"Writing z-scores, p-values and a class back to the grid makes the result a normal layer: style it, label it, export it.",[196,1614,1616],{"className":198,"code":1615,"language":200,"meta":201,"style":201},"from qgis.core import QgsField, QgsCategorizedSymbolRenderer, QgsRendererCategory, QgsFillSymbol\nfrom qgis.PyQt.QtCore import QVariant\n\nprov = counts.dataProvider()\nprov.addAttributes([QgsField(\"gi_z\", QVariant.Double), QgsField(\"gi_p\", QVariant.Double),\n                    QgsField(\"spot\", QVariant.String, len=8)])\ncounts.updateFields()\niz, ip, isp = (counts.fields().indexOf(n) for n in (\"gi_z\", \"gi_p\", \"spot\"))\nchanges = {}\nfor i, fid in enumerate(ids):\n    label = \"hot\" if significant[i] and z[i] > 0 else \"cold\" if significant[i] else \"none\"\n    changes[fid] = {iz: float(z[i]), ip: float(p[i]), isp: label}\nprov.changeAttributeValues(changes)\n\ncats = [QgsRendererCategory(v, QgsFillSymbol.createSimple({\"color\": c, \"outline_style\": \"no\"}), v)\n        for v, c in ((\"hot\", \"#b91c1c\"), (\"cold\", \"#2563eb\"), (\"none\", \"#e8e4d8\"))]\ncounts.setRenderer(QgsCategorizedSymbolRenderer(\"spot\", cats))\nQgsProject.instance().addMapLayer(counts)\n",[203,1617,1618,1629,1641,1645,1655,1672,1692,1697,1727,1737,1750,1790,1810,1815,1819,1846,1891,1901],{"__ignoreMap":201},[206,1619,1620,1622,1624,1626],{"class":97,"line":208},[206,1621,222],{"class":211},[206,1623,225],{"class":215},[206,1625,212],{"class":211},[206,1627,1628],{"class":215}," QgsField, QgsCategorizedSymbolRenderer, QgsRendererCategory, QgsFillSymbol\n",[206,1630,1631,1633,1636,1638],{"class":97,"line":219},[206,1632,222],{"class":211},[206,1634,1635],{"class":215}," qgis.PyQt.QtCore ",[206,1637,212],{"class":211},[206,1639,1640],{"class":215}," QVariant\n",[206,1642,1643],{"class":97,"line":233},[206,1644,237],{"emptyLinePlaceholder":236},[206,1646,1647,1650,1652],{"class":97,"line":240},[206,1648,1649],{"class":215},"prov ",[206,1651,246],{"class":211},[206,1653,1654],{"class":215}," counts.dataProvider()\n",[206,1656,1657,1660,1663,1666,1669],{"class":97,"line":265},[206,1658,1659],{"class":215},"prov.addAttributes([QgsField(",[206,1661,1662],{"class":252},"\"gi_z\"",[206,1664,1665],{"class":215},", QVariant.Double), QgsField(",[206,1667,1668],{"class":252},"\"gi_p\"",[206,1670,1671],{"class":215},", QVariant.Double),\n",[206,1673,1674,1677,1680,1683,1685,1687,1689],{"class":97,"line":284},[206,1675,1676],{"class":215},"                    QgsField(",[206,1678,1679],{"class":252},"\"spot\"",[206,1681,1682],{"class":215},", QVariant.String, ",[206,1684,903],{"class":715},[206,1686,246],{"class":211},[206,1688,58],{"class":259},[206,1690,1691],{"class":215},")])\n",[206,1693,1694],{"class":97,"line":289},[206,1695,1696],{"class":215},"counts.updateFields()\n",[206,1698,1699,1702,1704,1707,1709,1711,1713,1715,1717,1719,1721,1723,1725],{"class":97,"line":306},[206,1700,1701],{"class":215},"iz, ip, isp ",[206,1703,246],{"class":211},[206,1705,1706],{"class":215}," (counts.fields().indexOf(n) ",[206,1708,504],{"class":211},[206,1710,911],{"class":215},[206,1712,510],{"class":211},[206,1714,1350],{"class":215},[206,1716,1662],{"class":252},[206,1718,318],{"class":215},[206,1720,1668],{"class":252},[206,1722,318],{"class":215},[206,1724,1679],{"class":252},[206,1726,858],{"class":215},[206,1728,1729,1732,1734],{"class":97,"line":347},[206,1730,1731],{"class":215},"changes ",[206,1733,246],{"class":211},[206,1735,1736],{"class":215}," {}\n",[206,1738,1739,1741,1743,1745,1747],{"class":97,"line":371},[206,1740,504],{"class":211},[206,1742,756],{"class":215},[206,1744,510],{"class":211},[206,1746,761],{"class":259},[206,1748,1749],{"class":215},"(ids):\n",[206,1751,1752,1755,1757,1760,1763,1766,1769,1772,1774,1776,1778,1781,1783,1785,1787],{"class":97,"line":385},[206,1753,1754],{"class":215},"    label ",[206,1756,246],{"class":211},[206,1758,1759],{"class":252}," \"hot\"",[206,1761,1762],{"class":211}," if",[206,1764,1765],{"class":215}," significant[i] ",[206,1767,1768],{"class":211},"and",[206,1770,1771],{"class":215}," z[i] ",[206,1773,1188],{"class":211},[206,1775,1191],{"class":259},[206,1777,1194],{"class":211},[206,1779,1780],{"class":252}," \"cold\"",[206,1782,1762],{"class":211},[206,1784,1765],{"class":215},[206,1786,1487],{"class":211},[206,1788,1789],{"class":252}," \"none\"\n",[206,1791,1792,1795,1797,1800,1802,1805,1807],{"class":97,"line":423},[206,1793,1794],{"class":215},"    changes[fid] ",[206,1796,246],{"class":211},[206,1798,1799],{"class":215}," {iz: ",[206,1801,721],{"class":259},[206,1803,1804],{"class":215},"(z[i]), ip: ",[206,1806,721],{"class":259},[206,1808,1809],{"class":215},"(p[i]), isp: label}\n",[206,1811,1812],{"class":97,"line":438},[206,1813,1814],{"class":215},"prov.changeAttributeValues(changes)\n",[206,1816,1817],{"class":97,"line":462},[206,1818,237],{"emptyLinePlaceholder":236},[206,1820,1821,1824,1826,1829,1832,1835,1838,1840,1843],{"class":97,"line":479},[206,1822,1823],{"class":215},"cats ",[206,1825,246],{"class":211},[206,1827,1828],{"class":215}," [QgsRendererCategory(v, QgsFillSymbol.createSimple({",[206,1830,1831],{"class":252},"\"color\"",[206,1833,1834],{"class":215},": c, ",[206,1836,1837],{"class":252},"\"outline_style\"",[206,1839,312],{"class":215},[206,1841,1842],{"class":252},"\"no\"",[206,1844,1845],{"class":215},"}), v)\n",[206,1847,1848,1851,1854,1856,1859,1862,1864,1867,1870,1873,1875,1878,1880,1883,1885,1888],{"class":97,"line":830},[206,1849,1850],{"class":211},"        for",[206,1852,1853],{"class":215}," v, c ",[206,1855,510],{"class":211},[206,1857,1858],{"class":215}," ((",[206,1860,1861],{"class":252},"\"hot\"",[206,1863,318],{"class":215},[206,1865,1866],{"class":252},"\"#b91c1c\"",[206,1868,1869],{"class":215},"), (",[206,1871,1872],{"class":252},"\"cold\"",[206,1874,318],{"class":215},[206,1876,1877],{"class":252},"\"#2563eb\"",[206,1879,1869],{"class":215},[206,1881,1882],{"class":252},"\"none\"",[206,1884,318],{"class":215},[206,1886,1887],{"class":252},"\"#e8e4d8\"",[206,1889,1890],{"class":215},"))]\n",[206,1892,1893,1896,1898],{"class":97,"line":845},[206,1894,1895],{"class":215},"counts.setRenderer(QgsCategorizedSymbolRenderer(",[206,1897,1679],{"class":252},[206,1899,1900],{"class":215},", cats))\n",[206,1902,1903],{"class":97,"line":861},[206,1904,1905],{"class":215},"QgsProject.instance().addMapLayer(counts)\n",[14,1907,1908,1910,1911,1914,1915,1919],{},[523,1909,525],{}," Bulk ",[203,1912,1913],{},"changeAttributeValues"," writes every cell in one provider call. Three categories — hot, cold, not significant — make the map honest: a graduated ramp on raw z-scores invites readers to see hot spots in cells that did not pass the test. Red for hot and blue for cold is the convention; a neutral fill for the rest keeps attention on what matters. For the z-scores themselves, a diverging ramp centred on zero is the right choice if you show them at all — see ",[21,1916,1918],{"href":1917},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","graduated renderers",".",[171,1921,1923],{"id":1922},"check-sensitivity-before-you-report","Check sensitivity before you report",[14,1925,1926],{},"Hot spots that appear at one cell size and band but vanish at another are fragile. A quick sensitivity check reruns the analysis over a few settings and keeps the cells that are hot in most of them.",[196,1928,1930],{"className":198,"code":1929,"language":200,"meta":201,"style":201},"def gi_star(values, centres, band, q=0.05):\n    n = len(values)\n    xbar, sd = values.mean(), values.std()\n    zs = np.zeros(n)\n    for i, c in enumerate(centres):\n        cand = index.intersects(c.boundingBox().buffered(band))\n        nb = [pos[j] for j in cand if c.distance(centres[pos[j]]) \u003C= band]\n        w = len(nb)\n        den = sd * np.sqrt((n * w - w * w) \u002F (n - 1))\n        zs[i] = (values[nb].sum() - xbar * w) \u002F den if den > 0 else 0.0\n    ps = np.array([2 * (1 - 0.5 * (1 + erf(abs(v) \u002F sqrt(2)))) for v in zs])\n    srt = np.sort(ps)\n    ok = srt \u003C= q * np.arange(1, n + 1) \u002F n\n    cut = srt[ok].max() if ok.any() else 0.0\n    return zs, ps \u003C= cut\n\nstable = np.zeros(n, dtype=int)\nfor band in (450, 600, 900):\n    zb, sig = gi_star(x, centres, band)\n    stable += ((zb > 0) & sig).astype(int)\nprint((stable >= 2).sum(), \"cells hot at two or more bands\")\n",[203,1931,1932,1952,1964,1974,1983,1996,2006,2033,2045,2083,2117,2169,2179,2215,2234,2247,2251,2270,2296,2306,2332],{"__ignoreMap":201},[206,1933,1934,1937,1941,1944,1946,1949],{"class":97,"line":208},[206,1935,1936],{"class":211},"def",[206,1938,1940],{"class":1939},"svObZ"," gi_star",[206,1942,1943],{"class":215},"(values, centres, band, q",[206,1945,246],{"class":211},[206,1947,1948],{"class":259},"0.05",[206,1950,1951],{"class":215},"):\n",[206,1953,1954,1957,1959,1961],{"class":97,"line":219},[206,1955,1956],{"class":215},"    n ",[206,1958,246],{"class":211},[206,1960,1006],{"class":259},[206,1962,1963],{"class":215},"(values)\n",[206,1965,1966,1969,1971],{"class":97,"line":233},[206,1967,1968],{"class":215},"    xbar, sd ",[206,1970,246],{"class":211},[206,1972,1973],{"class":215}," values.mean(), values.std()\n",[206,1975,1976,1979,1981],{"class":97,"line":240},[206,1977,1978],{"class":215},"    zs ",[206,1980,246],{"class":211},[206,1982,1063],{"class":215},[206,1984,1985,1988,1990,1992,1994],{"class":97,"line":265},[206,1986,1987],{"class":211},"    for",[206,1989,835],{"class":215},[206,1991,510],{"class":211},[206,1993,761],{"class":259},[206,1995,842],{"class":215},[206,1997,1998,2001,2003],{"class":97,"line":284},[206,1999,2000],{"class":215},"        cand ",[206,2002,246],{"class":211},[206,2004,2005],{"class":215}," index.intersects(c.boundingBox().buffered(band))\n",[206,2007,2008,2011,2013,2016,2018,2020,2022,2024,2026,2028,2030],{"class":97,"line":289},[206,2009,2010],{"class":215},"        nb ",[206,2012,246],{"class":211},[206,2014,2015],{"class":215}," [pos[j] ",[206,2017,504],{"class":211},[206,2019,869],{"class":215},[206,2021,510],{"class":211},[206,2023,874],{"class":215},[206,2025,877],{"class":211},[206,2027,880],{"class":215},[206,2029,883],{"class":211},[206,2031,2032],{"class":215}," band]\n",[206,2034,2035,2038,2040,2042],{"class":97,"line":306},[206,2036,2037],{"class":215},"        w ",[206,2039,246],{"class":211},[206,2041,1006],{"class":259},[206,2043,2044],{"class":215},"(nb)\n",[206,2046,2047,2050,2052,2055,2057,2059,2061,2064,2066,2068,2070,2073,2075,2077,2079,2081],{"class":97,"line":347},[206,2048,2049],{"class":215},"        den ",[206,2051,246],{"class":211},[206,2053,2054],{"class":215}," sd ",[206,2056,1119],{"class":211},[206,2058,1137],{"class":215},[206,2060,1119],{"class":211},[206,2062,2063],{"class":215}," w ",[206,2065,1040],{"class":211},[206,2067,2063],{"class":215},[206,2069,1119],{"class":211},[206,2071,2072],{"class":215}," w) ",[206,2074,1156],{"class":211},[206,2076,1159],{"class":215},[206,2078,1040],{"class":211},[206,2080,1164],{"class":259},[206,2082,858],{"class":215},[206,2084,2085,2088,2090,2093,2095,2097,2099,2101,2103,2105,2107,2109,2111,2113,2115],{"class":97,"line":371},[206,2086,2087],{"class":215},"        zs[i] ",[206,2089,246],{"class":211},[206,2091,2092],{"class":215}," (values[nb].sum() ",[206,2094,1040],{"class":211},[206,2096,1043],{"class":215},[206,2098,1119],{"class":211},[206,2100,2072],{"class":215},[206,2102,1156],{"class":211},[206,2104,1181],{"class":215},[206,2106,877],{"class":211},[206,2108,1181],{"class":215},[206,2110,1188],{"class":211},[206,2112,1191],{"class":259},[206,2114,1194],{"class":211},[206,2116,1197],{"class":259},[206,2118,2119,2122,2124,2126,2128,2130,2132,2134,2136,2138,2140,2142,2144,2146,2148,2150,2152,2154,2156,2158,2160,2162,2164,2166],{"class":97,"line":385},[206,2120,2121],{"class":215},"    ps ",[206,2123,246],{"class":211},[206,2125,1341],{"class":215},[206,2127,1344],{"class":259},[206,2129,1347],{"class":211},[206,2131,1350],{"class":215},[206,2133,1353],{"class":259},[206,2135,1356],{"class":211},[206,2137,1359],{"class":259},[206,2139,1347],{"class":211},[206,2141,1350],{"class":215},[206,2143,1353],{"class":259},[206,2145,1368],{"class":211},[206,2147,1371],{"class":215},[206,2149,1374],{"class":259},[206,2151,1377],{"class":215},[206,2153,1156],{"class":211},[206,2155,1382],{"class":215},[206,2157,1344],{"class":259},[206,2159,1387],{"class":215},[206,2161,504],{"class":211},[206,2163,1392],{"class":215},[206,2165,510],{"class":211},[206,2167,2168],{"class":215}," zs])\n",[206,2170,2171,2174,2176],{"class":97,"line":423},[206,2172,2173],{"class":215},"    srt ",[206,2175,246],{"class":211},[206,2177,2178],{"class":215}," np.sort(ps)\n",[206,2180,2181,2184,2186,2189,2191,2194,2196,2199,2201,2204,2206,2208,2210,2212],{"class":97,"line":438},[206,2182,2183],{"class":215},"    ok ",[206,2185,246],{"class":211},[206,2187,2188],{"class":215}," srt ",[206,2190,883],{"class":211},[206,2192,2193],{"class":215}," q ",[206,2195,1119],{"class":211},[206,2197,2198],{"class":215}," np.arange(",[206,2200,1353],{"class":259},[206,2202,2203],{"class":215},", n ",[206,2205,1445],{"class":211},[206,2207,1164],{"class":259},[206,2209,1153],{"class":215},[206,2211,1156],{"class":211},[206,2213,2214],{"class":215}," n\n",[206,2216,2217,2220,2222,2225,2227,2230,2232],{"class":97,"line":462},[206,2218,2219],{"class":215},"    cut ",[206,2221,246],{"class":211},[206,2223,2224],{"class":215}," srt[ok].max() ",[206,2226,877],{"class":211},[206,2228,2229],{"class":215}," ok.any() ",[206,2231,1487],{"class":211},[206,2233,1197],{"class":259},[206,2235,2236,2239,2242,2244],{"class":97,"line":479},[206,2237,2238],{"class":211},"    return",[206,2240,2241],{"class":215}," zs, ps ",[206,2243,883],{"class":211},[206,2245,2246],{"class":215}," cut\n",[206,2248,2249],{"class":97,"line":830},[206,2250,237],{"emptyLinePlaceholder":236},[206,2252,2253,2256,2258,2261,2263,2265,2268],{"class":97,"line":845},[206,2254,2255],{"class":215},"stable ",[206,2257,246],{"class":211},[206,2259,2260],{"class":215}," np.zeros(n, ",[206,2262,716],{"class":715},[206,2264,246],{"class":211},[206,2266,2267],{"class":259},"int",[206,2269,519],{"class":215},[206,2271,2272,2274,2277,2279,2281,2284,2286,2289,2291,2294],{"class":97,"line":861},[206,2273,504],{"class":211},[206,2275,2276],{"class":215}," band ",[206,2278,510],{"class":211},[206,2280,1350],{"class":215},[206,2282,2283],{"class":259},"450",[206,2285,318],{"class":215},[206,2287,2288],{"class":259},"600",[206,2290,318],{"class":215},[206,2292,2293],{"class":259},"900",[206,2295,1951],{"class":215},[206,2297,2298,2301,2303],{"class":97,"line":892},[206,2299,2300],{"class":215},"    zb, sig ",[206,2302,246],{"class":211},[206,2304,2305],{"class":215}," gi_star(x, centres, band)\n",[206,2307,2308,2311,2314,2317,2319,2321,2323,2325,2328,2330],{"class":97,"line":919},[206,2309,2310],{"class":215},"    stable ",[206,2312,2313],{"class":211},"+=",[206,2315,2316],{"class":215}," ((zb ",[206,2318,1188],{"class":211},[206,2320,1191],{"class":259},[206,2322,1153],{"class":215},[206,2324,1530],{"class":211},[206,2326,2327],{"class":215}," sig).astype(",[206,2329,2267],{"class":259},[206,2331,519],{"class":215},[206,2333,2335,2337,2340,2343,2345,2348,2351],{"class":97,"line":2334},21,[206,2336,482],{"class":259},[206,2338,2339],{"class":215},"((stable ",[206,2341,2342],{"class":211},">=",[206,2344,959],{"class":259},[206,2346,2347],{"class":215},").sum(), ",[206,2349,2350],{"class":252},"\"cells hot at two or more bands\"",[206,2352,519],{"class":215},[14,2354,2355,2357,2358,2361,2362,2364],{},[523,2356,525],{}," Wrapping the neighbour construction, z-scores and correction in one function makes reruns trivial; ",[203,2359,2360],{},"values.std()"," is the same population standard deviation as ",[203,2363,1243],{}," above, and the spatial index of cell centres is reused across bands. Cells hot at most bands are robust findings worth acting on; cells hot at one band only are scale artefacts. Report the band and cell size with every hot spot map, because a reader cannot judge the result without them.",[171,2366,2368],{"id":2367},"qgis-version-compatibility","QGIS version compatibility",[14,2370,2371,2372,318,2375,318,2378,2381,2382,2385,2386,2389,2390,2393,2394,2397],{},"The algorithms used — ",[203,2373,2374],{},"native:creategrid",[203,2376,2377],{},"native:extractbylocation",[203,2379,2380],{},"native:countpointsinpolygon"," — have the same parameters on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. On QGIS 4, ",[203,2383,2384],{},"QgsField"," takes ",[203,2387,2388],{},"QMetaType.Type.Double"," and ",[203,2391,2392],{},"QMetaType.Type.QString"," instead of ",[203,2395,2396],{},"QVariant"," types. NumPy is bundled with all official QGIS installers.",[171,2399,2401],{"id":2400},"troubleshooting","Troubleshooting",[176,2403,2404,2410,2416,2422],{},[179,2405,2406,2409],{},[523,2407,2408],{},"Everything near the edge is cold."," Empty cells outside the real area were kept; clip the grid to the study area.",[179,2411,2412,2415],{},[523,2413,2414],{},"No cells are significant after correction."," The band is too small, the grid too fine, or there genuinely is no hot spot.",[179,2417,2418,2421],{},[523,2419,2420],{},"z-scores are all NaN."," Every cell has the same value, so the standard deviation is zero.",[179,2423,2424,2427],{},[523,2425,2426],{},"Hot spots follow population."," Counts reflect where people live; analyse rates (events per household) instead.",[171,2429,2431],{"id":2430},"conclusion","Conclusion",[14,2433,2434],{},"Count events into a hexagon grid clipped to the study area, define neighbours with a distance band that includes each cell itself, compute Gi* z-scores with NumPy, correct for many tests with the false discovery rate, map hot, cold and not significant as three categories, and report only spots that survive a change of scale.",[171,2436,2438],{"id":2437},"frequently-asked-questions","Frequently Asked Questions",[14,2440,2441,2444,2445,2448,2449,2452,2453,2456],{},[523,2442,2443],{},"Can I use PySAL instead?","\nYes. The ",[203,2446,2447],{},"esda"," package's ",[203,2450,2451],{},"G_Local"," with ",[203,2454,2455],{},"star=True"," computes the same statistic with permutation-based p-values; install it into QGIS's Python first.",[14,2458,2459,2462],{},[523,2460,2461],{},"Should I analyse counts or rates?","\nRates, when the population at risk varies — otherwise hot spots simply show where people are.",[14,2464,2465,2468],{},[523,2466,2467],{},"What about Local Moran's I?","\nIt finds clusters of similar values and outliers (high surrounded by low); Gi* only finds concentrations of high or low values. Use Moran's I when outliers matter.",[14,2470,2471,2474],{},[523,2472,2473],{},"Does the cell shape matter?","\nHexagons reduce edge-orientation effects; squares work but have uneven neighbour distances.",[171,2476,2478],{"id":2477},"related","Related",[176,2480,2481,2486,2492,2498,2503],{},[179,2482,2483,2485],{},[21,2484,24],{"href":23}," — the guide this recipe belongs to",[179,2487,2488],{},[21,2489,2491],{"href":2490},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fkernel-density-heatmap-raster-pyqgis\u002F","Create a Kernel Density Heatmap Raster in PyQGIS",[179,2493,2494],{},[21,2495,2497],{"href":2496},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fcreate-voronoi-and-delaunay-pyqgis\u002F","Create Voronoi Polygons and Delaunay Triangles in PyQGIS",[179,2499,2500],{},[21,2501,2502],{"href":529},"Create a Hexagon Grid and Count Points in PyQGIS",[179,2504,2505],{},[21,2506,2508],{"href":2507},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002Fcreate-choropleth-map-pyqgis\u002F","Create a Choropleth Map in PyQGIS",[2510,2511,2512],"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 .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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":201,"searchDepth":219,"depth":219,"links":2514},[2515,2516,2517,2518,2519,2520,2521,2522,2523,2524,2525,2526],{"id":173,"depth":219,"text":174},{"id":190,"depth":219,"text":191},{"id":534,"depth":219,"text":535},{"id":987,"depth":219,"text":988},{"id":1247,"depth":219,"text":1248},{"id":1608,"depth":219,"text":1609},{"id":1922,"depth":219,"text":1923},{"id":2367,"depth":219,"text":2368},{"id":2400,"depth":219,"text":2401},{"id":2430,"depth":219,"text":2431},{"id":2437,"depth":219,"text":2438},{"id":2477,"depth":219,"text":2478},"Find statistically significant hot and cold spots with the Getis-Ord Gi* statistic — aggregating points to a hexagon grid, defining neighbours by distance band or contiguity, computing z-scores with NumPy, correcting for multiple tests and mapping the result with a diverging style.","md",{"slug":2530,"type":2531,"breadcrumb":2532,"datePublished":2533,"dateModified":2533},"hotspot-analysis-getis-ord-pyqgis","article","Hot Spot Analysis with Getis-Ord Gi*","2026-10-02","\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fhotspot-analysis-getis-ord-pyqgis",{"title":5,"description":2527},"spatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fhotspot-analysis-getis-ord-pyqgis\u002Findex","CWWJ_X5YiEVbF7uFDcQ2RzVNzYIfqSJl_DNAc2KRytQ",1790966264936]