[{"data":1,"prerenderedAt":2391},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fcalculate-mean-center-and-standard-distance-pyqgis":3},{"id":4,"title":5,"body":6,"description":2380,"extension":2381,"meta":2382,"navigation":241,"path":2387,"seo":2388,"stem":2389,"__hash__":2390},"docs\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fcalculate-mean-center-and-standard-distance-pyqgis\u002Findex.md","Calculate Mean Centre and Standard Distance in PyQGIS",{"type":7,"value":8,"toc":2366},"minimark",[9,13,17,26,174,179,192,196,199,465,484,488,491,554,981,986,990,993,1178,1183,1187,1190,1279,1609,1618,1622,1625,1979,1992,1996,1999,2205,2218,2222,2251,2255,2284,2288,2294,2298,2304,2310,2321,2327,2331,2362],[10,11,5],"h1",{"id":12},"calculate-mean-centre-and-standard-distance-in-pyqgis",[14,15,16],"p",{},"Descriptive statistics for numbers — mean, median, standard deviation — have direct spatial equivalents. The mean centre is the average location of a set of points. The standard distance is how far, typically, points lie from that centre. The standard deviational ellipse adds direction: is the spread stretched along a valley, a coastline or a motorway? Together they compress thousands of points into a few numbers and shapes that are easy to compare between groups and between years — how the centre of a city's population has drifted, or whether one species ranges more widely than another.",[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",". It computes weighted and unweighted mean centres with Processing, the median centre and standard distance with NumPy, builds a standard deviational ellipse, and compares these summaries across groups and time.",[14,27,28],{},[29,30,35,39,43,50,59,69,76,80,83,85,89,92,96,100,104,108,111,114,121,129,133,140,146,151,155,159,163,166,170],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 290","img","A point scatter elongated diagonally with its mean centre, a standard distance circle and a rotated standard deviational ellipse","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Centre, circle and ellipse",[40,41,42],"desc",{},"A scatter of points elongated from lower left to upper right. The mean centre is marked at the average location. A standard distance circle around it has a radius equal to the root mean square distance of points from the centre. A standard deviational ellipse is also drawn, rotated along the direction of greatest spread and narrower across it, capturing the elongation that the circle misses.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","290","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Where, how spread, and in which direction",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","44","460","230","8","#fffdf7","#59645f","stroke-width:1.5",[70,71],"circle",{"cx":72,"cy":73,"r":74,"fill":67,"stroke":67,"style":75},"140","210","4","stroke-width:1",[70,77],{"cx":78,"cy":79,"r":74,"fill":67,"stroke":67,"style":75},"170","190",[70,81],{"cx":82,"cy":82,"r":74,"fill":67,"stroke":67,"style":75},"200",[70,84],{"cx":73,"cy":78,"r":74,"fill":67,"stroke":67,"style":75},[70,86],{"cx":87,"cy":88,"r":74,"fill":67,"stroke":67,"style":75},"240","160",[70,90],{"cx":91,"cy":72,"r":74,"fill":67,"stroke":67,"style":75},"260",[70,93],{"cx":94,"cy":95,"r":74,"fill":67,"stroke":67,"style":75},"280","155",[70,97],{"cx":98,"cy":99,"r":74,"fill":67,"stroke":67,"style":75},"300","120",[70,101],{"cx":102,"cy":103,"r":74,"fill":67,"stroke":67,"style":75},"320","110",[70,105],{"cx":106,"cy":107,"r":74,"fill":67,"stroke":67,"style":75},"345","95",[70,109],{"cx":64,"cy":110,"r":74,"fill":67,"stroke":67,"style":75},"130",[70,112],{"cx":48,"cy":113,"r":74,"fill":67,"stroke":67,"style":75},"175",[70,115],{"cx":116,"cy":95,"r":117,"fill":118,"stroke":119,"style":120},"255","70","none","#2563eb","stroke-width:2",[122,123],"ellipse",{"cx":116,"cy":95,"rx":124,"ry":125,"transform":126,"fill":118,"stroke":127,"style":128},"115","38","rotate(-35 255 155)","#b45309","stroke-width:2.5",[70,130],{"cx":116,"cy":95,"r":131,"fill":132,"stroke":132,"style":75},"7","#b91c1c",[44,134],{"x":135,"y":136,"width":137,"height":138,"rx":65,"fill":139,"stroke":132,"style":120},"510","60","226","56","#fdf2e2",[51,141,145],{"x":142,"y":143,"style":144,"fill":132,"textAnchor":57},"623","81.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","mean centre",[51,147,150],{"x":142,"y":148,"style":149,"fill":67,"textAnchor":57},"101.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","average location",[44,152],{"x":135,"y":153,"width":137,"height":138,"rx":65,"fill":154,"stroke":119,"style":120},"128","#eff3ff",[51,156,158],{"x":142,"y":157,"style":144,"fill":119,"textAnchor":57},"149.78","standard distance",[51,160,162],{"x":142,"y":161,"style":149,"fill":67,"textAnchor":57},"169.78","circle, no direction",[44,164],{"x":135,"y":165,"width":137,"height":138,"rx":65,"fill":139,"stroke":127,"style":120},"196",[51,167,169],{"x":142,"y":168,"style":144,"fill":127,"textAnchor":57},"217.78","deviational ellipse",[51,171,173],{"x":142,"y":172,"style":149,"fill":67,"textAnchor":57},"237.78","spread + orientation",[175,176,178],"h2",{"id":177},"prerequisites","Prerequisites",[180,181,182,186,189],"ul",{},[183,184,185],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series. NumPy ships with QGIS.",[183,187,188],{},"A point layer in a projected CRS. A mean of latitudes and longitudes is not a meaningful centre over large areas, and distances in degrees are meaningless.",[183,190,191],{},"Polygon data converted to representative points first — centroids or points on surface — if you want to summarise polygons.",[175,193,195],{"id":194},"mean-centre-with-processing","Mean centre with Processing",[14,197,198],{},"The mean coordinates algorithm computes the average x and y of a layer, optionally weighted by a field and optionally grouped by a category.",[200,201,206],"pre",{"className":202,"code":203,"language":204,"meta":205,"style":205},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsProject\n\nhouseholds = QgsProject.instance().mapLayersByName(\"households\")[0]\n\ncentre = processing.run(\"native:meancoordinates\", {\n    \"INPUT\": households, \"WEIGHT\": None, \"UID\": None,\n    \"OUTPUT\": \"memory:mean_centre\"})[\"OUTPUT\"]\nweighted = processing.run(\"native:meancoordinates\", {\n    \"INPUT\": households, \"WEIGHT\": \"persons\", \"UID\": None,\n    \"OUTPUT\": \"memory:mean_centre_persons\"})[\"OUTPUT\"]\n\nfor layer in (centre, weighted):\n    f = next(layer.getFeatures())\n    print(layer.name(), round(f[\"MEAN_X\"]), round(f[\"MEAN_Y\"]))\nQgsProject.instance().addMapLayers([centre, weighted])\n","python","",[207,208,209,222,236,243,268,273,290,321,340,354,378,394,399,414,428,459],"code",{"__ignoreMap":205},[210,211,214,218],"span",{"class":212,"line":213},"line",1,[210,215,217],{"class":216},"snl16","import",[210,219,221],{"class":220},"s95oV"," processing\n",[210,223,225,228,231,233],{"class":212,"line":224},2,[210,226,227],{"class":216},"from",[210,229,230],{"class":220}," qgis.core ",[210,232,217],{"class":216},[210,234,235],{"class":220}," QgsProject\n",[210,237,239],{"class":212,"line":238},3,[210,240,242],{"emptyLinePlaceholder":241},true,"\n",[210,244,246,249,252,255,259,262,265],{"class":212,"line":245},4,[210,247,248],{"class":220},"households ",[210,250,251],{"class":216},"=",[210,253,254],{"class":220}," QgsProject.instance().mapLayersByName(",[210,256,258],{"class":257},"sU2Wk","\"households\"",[210,260,261],{"class":220},")[",[210,263,46],{"class":264},"sDLfK",[210,266,267],{"class":220},"]\n",[210,269,271],{"class":212,"line":270},5,[210,272,242],{"emptyLinePlaceholder":241},[210,274,276,279,281,284,287],{"class":212,"line":275},6,[210,277,278],{"class":220},"centre ",[210,280,251],{"class":216},[210,282,283],{"class":220}," processing.run(",[210,285,286],{"class":257},"\"native:meancoordinates\"",[210,288,289],{"class":220},", {\n",[210,291,293,296,299,302,305,308,311,314,316,318],{"class":212,"line":292},7,[210,294,295],{"class":257},"    \"INPUT\"",[210,297,298],{"class":220},": households, ",[210,300,301],{"class":257},"\"WEIGHT\"",[210,303,304],{"class":220},": ",[210,306,307],{"class":264},"None",[210,309,310],{"class":220},", ",[210,312,313],{"class":257},"\"UID\"",[210,315,304],{"class":220},[210,317,307],{"class":264},[210,319,320],{"class":220},",\n",[210,322,324,327,329,332,335,338],{"class":212,"line":323},8,[210,325,326],{"class":257},"    \"OUTPUT\"",[210,328,304],{"class":220},[210,330,331],{"class":257},"\"memory:mean_centre\"",[210,333,334],{"class":220},"})[",[210,336,337],{"class":257},"\"OUTPUT\"",[210,339,267],{"class":220},[210,341,343,346,348,350,352],{"class":212,"line":342},9,[210,344,345],{"class":220},"weighted ",[210,347,251],{"class":216},[210,349,283],{"class":220},[210,351,286],{"class":257},[210,353,289],{"class":220},[210,355,357,359,361,363,365,368,370,372,374,376],{"class":212,"line":356},10,[210,358,295],{"class":257},[210,360,298],{"class":220},[210,362,301],{"class":257},[210,364,304],{"class":220},[210,366,367],{"class":257},"\"persons\"",[210,369,310],{"class":220},[210,371,313],{"class":257},[210,373,304],{"class":220},[210,375,307],{"class":264},[210,377,320],{"class":220},[210,379,381,383,385,388,390,392],{"class":212,"line":380},11,[210,382,326],{"class":257},[210,384,304],{"class":220},[210,386,387],{"class":257},"\"memory:mean_centre_persons\"",[210,389,334],{"class":220},[210,391,337],{"class":257},[210,393,267],{"class":220},[210,395,397],{"class":212,"line":396},12,[210,398,242],{"emptyLinePlaceholder":241},[210,400,402,405,408,411],{"class":212,"line":401},13,[210,403,404],{"class":216},"for",[210,406,407],{"class":220}," layer ",[210,409,410],{"class":216},"in",[210,412,413],{"class":220}," (centre, weighted):\n",[210,415,417,420,422,425],{"class":212,"line":416},14,[210,418,419],{"class":220},"    f ",[210,421,251],{"class":216},[210,423,424],{"class":264}," next",[210,426,427],{"class":220},"(layer.getFeatures())\n",[210,429,431,434,437,440,443,446,449,451,453,456],{"class":212,"line":430},15,[210,432,433],{"class":264},"    print",[210,435,436],{"class":220},"(layer.name(), ",[210,438,439],{"class":264},"round",[210,441,442],{"class":220},"(f[",[210,444,445],{"class":257},"\"MEAN_X\"",[210,447,448],{"class":220},"]), ",[210,450,439],{"class":264},[210,452,442],{"class":220},[210,454,455],{"class":257},"\"MEAN_Y\"",[210,457,458],{"class":220},"]))\n",[210,460,462],{"class":212,"line":461},16,[210,463,464],{"class":220},"QgsProject.instance().addMapLayers([centre, weighted])\n",[14,466,467,471,472,475,476,479,480,483],{},[468,469,470],"strong",{},"Breakdown:"," The output is a point layer with ",[207,473,474],{},"MEAN_X"," and ",[207,477,478],{},"MEAN_Y"," fields as well as the geometry. Weighting by persons per household moves the centre towards large households — the population centre rather than the dwelling centre. With a ",[207,481,482],{},"UID"," field, the algorithm produces one centre per group, which is the basis for the comparisons later on. The mean centre is sensitive to outliers: a few distant points pull it noticeably, which is one reason to compute the median centre as well.",[175,485,487],{"id":486},"median-centre","Median centre",[14,489,490],{},"The median centre is the location that minimises the total straight-line distance to all points. It resists outliers in the way the ordinary median does, and has a practical meaning: it is the best single site for a facility serving everyone, if travel is by straight line.",[14,492,493],{},[29,494,497,500,503,505,508,510,512,514,516,519,521,523,526,529,532,537,540,545,548,551],{"viewBox":495,"role":32,"ariaLabel":496,"xmlns":34},"0 0 760 240","A compact group with distant outliers: the mean centre is pulled towards the outliers while the median centre stays with the main group",[36,498,499],{},"Mean versus median centre with outliers",[40,501,502],{},"A compact group of points and three distant outliers to the right. The mean centre is pulled noticeably towards the outliers. The median centre, which minimises total distance, stays within the main group. Weiszfeld's algorithm finds the median centre by repeatedly re-weighting points by the inverse of their distance to the current estimate.",[44,504],{"x":46,"y":46,"width":47,"height":87,"fill":49},[51,506,507],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Outliers move the mean, not the median",[70,509],{"cx":99,"cy":99,"r":74,"fill":67,"stroke":67,"style":75},[70,511],{"cx":72,"cy":72,"r":74,"fill":67,"stroke":67,"style":75},[70,513],{"cx":88,"cy":103,"r":74,"fill":67,"stroke":67,"style":75},[70,515],{"cx":110,"cy":88,"r":74,"fill":67,"stroke":67,"style":75},[70,517],{"cx":78,"cy":518,"r":74,"fill":67,"stroke":67,"style":75},"150",[70,520],{"cx":518,"cy":110,"r":74,"fill":67,"stroke":67,"style":75},[70,522],{"cx":103,"cy":72,"r":74,"fill":67,"stroke":67,"style":75},[70,524],{"cx":525,"cy":103,"r":74,"fill":67,"stroke":67,"style":75},"600",[70,527],{"cx":528,"cy":518,"r":74,"fill":67,"stroke":67,"style":75},"640",[70,530],{"cx":531,"cy":99,"r":74,"fill":67,"stroke":67,"style":75},"680",[70,533],{"cx":534,"cy":535,"r":131,"fill":536,"stroke":536,"style":75},"145","136","#15803d",[70,538],{"cx":48,"cy":539,"r":131,"fill":132,"stroke":132,"style":75},"132",[51,541,544],{"x":534,"y":165,"style":542,"fill":543,"textAnchor":57},"text-anchor:middle;font-size:11.0px;font-family:sans-serif","#166534","median centre",[51,546,145],{"x":48,"y":547,"style":542,"fill":132,"textAnchor":57},"172",[51,549,550],{"x":528,"y":165,"style":542,"fill":67,"textAnchor":57},"outliers",[51,552,553],{"x":53,"y":137,"style":149,"fill":67,"textAnchor":57},"Weiszfeld iteration: re-weight by 1 \u002F distance until it settles",[200,555,557],{"className":202,"code":556,"language":204,"meta":205,"style":205},"import numpy as np\n\ndef median_centre(xy, weights=None, tol=0.01, max_iter=200):\n    w = np.ones(len(xy)) if weights is None else np.asarray(weights, float)\n    est = np.average(xy, axis=0, weights=w)\n    for _ in range(max_iter):\n        d = np.linalg.norm(xy - est, axis=1)\n        d = np.where(d \u003C 1e-9, 1e-9, d)\n        k = w \u002F d\n        new = (xy * k[:, None]).sum(axis=0) \u002F k.sum()\n        if np.linalg.norm(new - est) \u003C tol:\n            return new\n        est = new\n    return est\n\nxy = np.array([[f.geometry().asPoint().x(), f.geometry().asPoint().y()]\n               for f in households.getFeatures()])\npersons = np.array([f[\"persons\"] or 0 for f in households.getFeatures()], float)\nmed = median_centre(xy, persons)\nmean = np.average(xy, axis=0, weights=persons)\nprint(\"median\", med.round(), \"mean\", mean.round(), \"gap\", round(float(np.linalg.norm(med - mean))), \"m\")\n",[207,558,559,572,576,610,650,678,694,719,742,758,793,811,819,828,836,840,850,864,900,911,936],{"__ignoreMap":205},[210,560,561,563,566,569],{"class":212,"line":213},[210,562,217],{"class":216},[210,564,565],{"class":220}," numpy ",[210,567,568],{"class":216},"as",[210,570,571],{"class":220}," np\n",[210,573,574],{"class":212,"line":224},[210,575,242],{"emptyLinePlaceholder":241},[210,577,578,581,585,588,590,592,595,597,600,603,605,607],{"class":212,"line":238},[210,579,580],{"class":216},"def",[210,582,584],{"class":583},"svObZ"," median_centre",[210,586,587],{"class":220},"(xy, weights",[210,589,251],{"class":216},[210,591,307],{"class":264},[210,593,594],{"class":220},", tol",[210,596,251],{"class":216},[210,598,599],{"class":264},"0.01",[210,601,602],{"class":220},", max_iter",[210,604,251],{"class":216},[210,606,82],{"class":264},[210,608,609],{"class":220},"):\n",[210,611,612,615,617,620,623,626,629,632,635,638,641,644,647],{"class":212,"line":245},[210,613,614],{"class":220},"    w ",[210,616,251],{"class":216},[210,618,619],{"class":220}," np.ones(",[210,621,622],{"class":264},"len",[210,624,625],{"class":220},"(xy)) ",[210,627,628],{"class":216},"if",[210,630,631],{"class":220}," weights ",[210,633,634],{"class":216},"is",[210,636,637],{"class":264}," None",[210,639,640],{"class":216}," else",[210,642,643],{"class":220}," np.asarray(weights, ",[210,645,646],{"class":264},"float",[210,648,649],{"class":220},")\n",[210,651,652,655,657,660,664,666,668,670,673,675],{"class":212,"line":270},[210,653,654],{"class":220},"    est ",[210,656,251],{"class":216},[210,658,659],{"class":220}," np.average(xy, ",[210,661,663],{"class":662},"s9osk","axis",[210,665,251],{"class":216},[210,667,46],{"class":264},[210,669,310],{"class":220},[210,671,672],{"class":662},"weights",[210,674,251],{"class":216},[210,676,677],{"class":220},"w)\n",[210,679,680,683,686,688,691],{"class":212,"line":275},[210,681,682],{"class":216},"    for",[210,684,685],{"class":220}," _ ",[210,687,410],{"class":216},[210,689,690],{"class":264}," range",[210,692,693],{"class":220},"(max_iter):\n",[210,695,696,699,701,704,707,710,712,714,717],{"class":212,"line":292},[210,697,698],{"class":220},"        d ",[210,700,251],{"class":216},[210,702,703],{"class":220}," np.linalg.norm(xy ",[210,705,706],{"class":216},"-",[210,708,709],{"class":220}," est, ",[210,711,663],{"class":662},[210,713,251],{"class":216},[210,715,716],{"class":264},"1",[210,718,649],{"class":220},[210,720,721,723,725,728,731,734,736,739],{"class":212,"line":323},[210,722,698],{"class":220},[210,724,251],{"class":216},[210,726,727],{"class":220}," np.where(d ",[210,729,730],{"class":216},"\u003C",[210,732,733],{"class":264}," 1e-9",[210,735,310],{"class":220},[210,737,738],{"class":264},"1e-9",[210,740,741],{"class":220},", d)\n",[210,743,744,747,749,752,755],{"class":212,"line":342},[210,745,746],{"class":220},"        k ",[210,748,251],{"class":216},[210,750,751],{"class":220}," w ",[210,753,754],{"class":216},"\u002F",[210,756,757],{"class":220}," d\n",[210,759,760,763,765,768,771,774,776,779,781,783,785,788,790],{"class":212,"line":356},[210,761,762],{"class":220},"        new ",[210,764,251],{"class":216},[210,766,767],{"class":220}," (xy ",[210,769,770],{"class":216},"*",[210,772,773],{"class":220}," k[:, ",[210,775,307],{"class":264},[210,777,778],{"class":220},"]).sum(",[210,780,663],{"class":662},[210,782,251],{"class":216},[210,784,46],{"class":264},[210,786,787],{"class":220},") ",[210,789,754],{"class":216},[210,791,792],{"class":220}," k.sum()\n",[210,794,795,798,801,803,806,808],{"class":212,"line":380},[210,796,797],{"class":216},"        if",[210,799,800],{"class":220}," np.linalg.norm(new ",[210,802,706],{"class":216},[210,804,805],{"class":220}," est) ",[210,807,730],{"class":216},[210,809,810],{"class":220}," tol:\n",[210,812,813,816],{"class":212,"line":396},[210,814,815],{"class":216},"            return",[210,817,818],{"class":220}," new\n",[210,820,821,824,826],{"class":212,"line":401},[210,822,823],{"class":220},"        est ",[210,825,251],{"class":216},[210,827,818],{"class":220},[210,829,830,833],{"class":212,"line":416},[210,831,832],{"class":216},"    return",[210,834,835],{"class":220}," est\n",[210,837,838],{"class":212,"line":430},[210,839,242],{"emptyLinePlaceholder":241},[210,841,842,845,847],{"class":212,"line":461},[210,843,844],{"class":220},"xy ",[210,846,251],{"class":216},[210,848,849],{"class":220}," np.array([[f.geometry().asPoint().x(), f.geometry().asPoint().y()]\n",[210,851,853,856,859,861],{"class":212,"line":852},17,[210,854,855],{"class":216},"               for",[210,857,858],{"class":220}," f ",[210,860,410],{"class":216},[210,862,863],{"class":220}," households.getFeatures()])\n",[210,865,867,870,872,875,877,880,883,886,889,891,893,896,898],{"class":212,"line":866},18,[210,868,869],{"class":220},"persons ",[210,871,251],{"class":216},[210,873,874],{"class":220}," np.array([f[",[210,876,367],{"class":257},[210,878,879],{"class":220},"] ",[210,881,882],{"class":216},"or",[210,884,885],{"class":264}," 0",[210,887,888],{"class":216}," for",[210,890,858],{"class":220},[210,892,410],{"class":216},[210,894,895],{"class":220}," households.getFeatures()], ",[210,897,646],{"class":264},[210,899,649],{"class":220},[210,901,903,906,908],{"class":212,"line":902},19,[210,904,905],{"class":220},"med ",[210,907,251],{"class":216},[210,909,910],{"class":220}," median_centre(xy, persons)\n",[210,912,914,917,919,921,923,925,927,929,931,933],{"class":212,"line":913},20,[210,915,916],{"class":220},"mean ",[210,918,251],{"class":216},[210,920,659],{"class":220},[210,922,663],{"class":662},[210,924,251],{"class":216},[210,926,46],{"class":264},[210,928,310],{"class":220},[210,930,672],{"class":662},[210,932,251],{"class":216},[210,934,935],{"class":220},"persons)\n",[210,937,939,942,945,948,951,954,957,960,962,964,966,968,971,973,976,979],{"class":212,"line":938},21,[210,940,941],{"class":264},"print",[210,943,944],{"class":220},"(",[210,946,947],{"class":257},"\"median\"",[210,949,950],{"class":220},", med.round(), ",[210,952,953],{"class":257},"\"mean\"",[210,955,956],{"class":220},", mean.round(), ",[210,958,959],{"class":257},"\"gap\"",[210,961,310],{"class":220},[210,963,439],{"class":264},[210,965,944],{"class":220},[210,967,646],{"class":264},[210,969,970],{"class":220},"(np.linalg.norm(med ",[210,972,706],{"class":216},[210,974,975],{"class":220}," mean))), ",[210,977,978],{"class":257},"\"m\"",[210,980,649],{"class":220},[14,982,983,985],{},[468,984,470],{}," Weiszfeld's algorithm starts from the mean and repeatedly recomputes a weighted average in which each point's weight is divided by its distance from the current estimate, so nearby points count more each round. It converges quickly for typical data; the tolerance is in metres. The guard against zero distance prevents a division by zero when the estimate lands exactly on a point. A large gap between mean and median centre is itself a finding: it means the distribution is lopsided, with a long tail in the direction of the mean.",[175,987,989],{"id":988},"standard-distance","Standard distance",[14,991,992],{},"The standard distance is the spatial standard deviation: the root of the mean squared distance from the mean centre. Drawn as a circle, it encloses roughly two thirds of the points for a compact, round distribution.",[200,994,996],{"className":202,"code":995,"language":204,"meta":205,"style":205},"from qgis.core import QgsGeometry, QgsPointXY, QgsVectorLayer, QgsFeature\n\nd2 = ((xy - mean) ** 2).sum(axis=1)\nsd = float(np.sqrt(np.average(d2, weights=persons)))\nprint(f\"standard distance: {sd:.0f} m\")\n\ncircle = QgsVectorLayer(f\"Polygon?crs={households.crs().authid()}&field=sd_m:double\",\n                        \"standard distance\", \"memory\")\nf = QgsFeature(circle.fields())\nf.setAttributes([sd])\nf.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(*mean)).buffer(sd, 64))\ncircle.dataProvider().addFeatures([f])\nQgsProject.instance().addMapLayer(circle)\n",[207,997,998,1009,1013,1045,1065,1094,1098,1125,1137,1147,1152,1168,1173],{"__ignoreMap":205},[210,999,1000,1002,1004,1006],{"class":212,"line":213},[210,1001,227],{"class":216},[210,1003,230],{"class":220},[210,1005,217],{"class":216},[210,1007,1008],{"class":220}," QgsGeometry, QgsPointXY, QgsVectorLayer, QgsFeature\n",[210,1010,1011],{"class":212,"line":224},[210,1012,242],{"emptyLinePlaceholder":241},[210,1014,1015,1018,1020,1023,1025,1028,1031,1034,1037,1039,1041,1043],{"class":212,"line":238},[210,1016,1017],{"class":220},"d2 ",[210,1019,251],{"class":216},[210,1021,1022],{"class":220}," ((xy ",[210,1024,706],{"class":216},[210,1026,1027],{"class":220}," mean) ",[210,1029,1030],{"class":216},"**",[210,1032,1033],{"class":264}," 2",[210,1035,1036],{"class":220},").sum(",[210,1038,663],{"class":662},[210,1040,251],{"class":216},[210,1042,716],{"class":264},[210,1044,649],{"class":220},[210,1046,1047,1050,1052,1055,1058,1060,1062],{"class":212,"line":245},[210,1048,1049],{"class":220},"sd ",[210,1051,251],{"class":216},[210,1053,1054],{"class":264}," float",[210,1056,1057],{"class":220},"(np.sqrt(np.average(d2, ",[210,1059,672],{"class":662},[210,1061,251],{"class":216},[210,1063,1064],{"class":220},"persons)))\n",[210,1066,1067,1069,1071,1074,1077,1080,1083,1086,1089,1092],{"class":212,"line":270},[210,1068,941],{"class":264},[210,1070,944],{"class":220},[210,1072,1073],{"class":216},"f",[210,1075,1076],{"class":257},"\"standard distance: ",[210,1078,1079],{"class":264},"{",[210,1081,1082],{"class":220},"sd",[210,1084,1085],{"class":216},":.0f",[210,1087,1088],{"class":264},"}",[210,1090,1091],{"class":257}," m\"",[210,1093,649],{"class":220},[210,1095,1096],{"class":212,"line":275},[210,1097,242],{"emptyLinePlaceholder":241},[210,1099,1100,1103,1105,1108,1110,1113,1115,1118,1120,1123],{"class":212,"line":292},[210,1101,1102],{"class":220},"circle ",[210,1104,251],{"class":216},[210,1106,1107],{"class":220}," QgsVectorLayer(",[210,1109,1073],{"class":216},[210,1111,1112],{"class":257},"\"Polygon?crs=",[210,1114,1079],{"class":264},[210,1116,1117],{"class":220},"households.crs().authid()",[210,1119,1088],{"class":264},[210,1121,1122],{"class":257},"&field=sd_m:double\"",[210,1124,320],{"class":220},[210,1126,1127,1130,1132,1135],{"class":212,"line":323},[210,1128,1129],{"class":257},"                        \"standard distance\"",[210,1131,310],{"class":220},[210,1133,1134],{"class":257},"\"memory\"",[210,1136,649],{"class":220},[210,1138,1139,1142,1144],{"class":212,"line":342},[210,1140,1141],{"class":220},"f ",[210,1143,251],{"class":216},[210,1145,1146],{"class":220}," QgsFeature(circle.fields())\n",[210,1148,1149],{"class":212,"line":356},[210,1150,1151],{"class":220},"f.setAttributes([sd])\n",[210,1153,1154,1157,1159,1162,1165],{"class":212,"line":380},[210,1155,1156],{"class":220},"f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(",[210,1158,770],{"class":216},[210,1160,1161],{"class":220},"mean)).buffer(sd, ",[210,1163,1164],{"class":264},"64",[210,1166,1167],{"class":220},"))\n",[210,1169,1170],{"class":212,"line":396},[210,1171,1172],{"class":220},"circle.dataProvider().addFeatures([f])\n",[210,1174,1175],{"class":212,"line":401},[210,1176,1177],{"class":220},"QgsProject.instance().addMapLayer(circle)\n",[14,1179,1180,1182],{},[468,1181,470],{}," Weighting the squared distances by the same weights as the centre keeps the two statistics consistent. Buffering the centre point by the standard distance with 64 segments produces a smooth circle. The number is directly comparable between groups measured in the same CRS: a standard distance of 2.1 km for one group and 4.8 km for another means the second is more than twice as spread out. It ignores direction, which the ellipse addresses.",[175,1184,1186],{"id":1185},"standard-deviational-ellipse","Standard deviational ellipse",[14,1188,1189],{},"Spread is often directional — settlement along a river, incidents along a road corridor. The standard deviational ellipse finds the axis of greatest spread and measures the spread along and across it.",[14,1191,1192],{},[29,1193,1196,1199,1202,1205,1220,1223,1226,1231,1236,1240,1245,1248,1252,1255,1258,1262,1266,1269,1272,1275],{"viewBox":1194,"role":32,"ariaLabel":1195,"xmlns":34},"0 0 760 250","The covariance matrix of coordinates yields the ellipse orientation from its eigenvectors and the axis lengths from its eigenvalues",[36,1197,1198],{},"Ellipse from the covariance matrix",[40,1200,1201],{},"The ellipse comes from the covariance matrix of the point coordinates around the mean centre. Its eigenvectors give the directions of greatest and least spread, and the square roots of its eigenvalues give the standard deviations along each. The ratio of the axes measures elongation; the angle of the major axis gives orientation.",[44,1203],{"x":46,"y":46,"width":47,"height":1204,"fill":49},"250",[1206,1207,1208],"defs",{},[1209,1210,1215],"marker",{"id":1211,"viewBox":1212,"refX":65,"refY":1213,"markerWidth":131,"markerHeight":131,"orient":1214},"mcEllipseArrow","0 0 10 10","5","auto-start-reverse",[1216,1217],"path",{"d":1218,"fill":1219},"M0 0 L10 5 L0 10 z","#2f3b35",[51,1221,1222],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Covariance → axes and angle",[44,1224],{"x":61,"y":136,"width":1225,"height":72,"rx":65,"fill":66,"stroke":67,"style":120},"222",[51,1227,1230],{"x":1228,"y":1229,"style":144,"fill":56,"textAnchor":57},"135","107.78","covariance",[51,1232,1235],{"x":1228,"y":1233,"style":1234,"fill":1219,"textAnchor":57},"133.78","text-anchor:middle;font-size:10.0px;font-family:monospace","var(x)  cov(x,y)",[51,1237,1239],{"x":1228,"y":1238,"style":1234,"fill":1219,"textAnchor":57},"159.78","cov(x,y)  var(y)",[212,1241],{"x1":1242,"y1":110,"x2":1243,"y2":110,"stroke":1219,"style":1244},"246","266","stroke-width:1.8;marker-end:url(#mcEllipseArrow)",[44,1246],{"x":1247,"y":136,"width":1225,"height":72,"rx":65,"fill":154,"stroke":119,"style":120},"270",[51,1249,1251],{"x":1250,"y":1229,"style":144,"fill":119,"textAnchor":57},"381","eigen-decomposition",[51,1253,1254],{"x":1250,"y":1233,"style":149,"fill":1219,"textAnchor":57},"vectors → directions",[51,1256,1257],{"x":1250,"y":1238,"style":149,"fill":1219,"textAnchor":57},"values → spreads",[212,1259],{"x1":1260,"y1":110,"x2":1261,"y2":110,"stroke":1219,"style":1244},"492","512",[44,1263],{"x":1264,"y":136,"width":1265,"height":72,"rx":65,"fill":139,"stroke":127,"style":120},"516","220",[51,1267,122],{"x":1268,"y":1229,"style":144,"fill":127,"textAnchor":57},"626",[51,1270,1271],{"x":1268,"y":1233,"style":149,"fill":1219,"textAnchor":57},"major, minor axis",[51,1273,1274],{"x":1268,"y":1238,"style":149,"fill":1219,"textAnchor":57},"angle, elongation",[51,1276,1278],{"x":53,"y":1277,"style":149,"fill":67,"textAnchor":57},"228","√2 × axis lengths is the common convention for display",[200,1280,1282],{"className":202,"code":1281,"language":204,"meta":205,"style":205},"import math\n\ndef deviational_ellipse(xy, w, centre, scale=math.sqrt(2)):\n    d = xy - centre\n    cov = np.cov(d.T, aweights=w, bias=True)\n    vals, vecs = np.linalg.eigh(cov)          # ascending\n    minor, major = np.sqrt(vals) * scale\n    angle = math.degrees(math.atan2(vecs[1, 1], vecs[0, 1]))\n    return major, minor, angle\n\nmajor, minor, angle = deviational_ellipse(xy, persons, mean)\nprint(f\"major {major:.0f} m, minor {minor:.0f} m, angle {angle:.0f}°, \"\n      f\"elongation {major \u002F minor:.2f}\")\n\ncircle_pts = [QgsPointXY(math.cos(t) * major, math.sin(t) * minor)\n              for t in np.linspace(0, 2 * math.pi, 73)]\nellipse = QgsGeometry.fromPolygonXY([circle_pts])\nellipse.rotate(-angle, QgsPointXY(0, 0))\nellipse.translate(mean[0], mean[1])\n",[207,1283,1284,1291,1295,1316,1331,1359,1373,1388,1415,1422,1426,1436,1483,1511,1515,1535,1566,1576,1594],{"__ignoreMap":205},[210,1285,1286,1288],{"class":212,"line":213},[210,1287,217],{"class":216},[210,1289,1290],{"class":220}," math\n",[210,1292,1293],{"class":212,"line":224},[210,1294,242],{"emptyLinePlaceholder":241},[210,1296,1297,1299,1302,1305,1307,1310,1313],{"class":212,"line":238},[210,1298,580],{"class":216},[210,1300,1301],{"class":583}," deviational_ellipse",[210,1303,1304],{"class":220},"(xy, w, centre, scale",[210,1306,251],{"class":216},[210,1308,1309],{"class":220},"math.sqrt(",[210,1311,1312],{"class":264},"2",[210,1314,1315],{"class":220},")):\n",[210,1317,1318,1321,1323,1326,1328],{"class":212,"line":245},[210,1319,1320],{"class":220},"    d ",[210,1322,251],{"class":216},[210,1324,1325],{"class":220}," xy ",[210,1327,706],{"class":216},[210,1329,1330],{"class":220}," centre\n",[210,1332,1333,1336,1338,1341,1344,1346,1349,1352,1354,1357],{"class":212,"line":270},[210,1334,1335],{"class":220},"    cov ",[210,1337,251],{"class":216},[210,1339,1340],{"class":220}," np.cov(d.T, ",[210,1342,1343],{"class":662},"aweights",[210,1345,251],{"class":216},[210,1347,1348],{"class":220},"w, ",[210,1350,1351],{"class":662},"bias",[210,1353,251],{"class":216},[210,1355,1356],{"class":264},"True",[210,1358,649],{"class":220},[210,1360,1361,1364,1366,1369],{"class":212,"line":275},[210,1362,1363],{"class":220},"    vals, vecs ",[210,1365,251],{"class":216},[210,1367,1368],{"class":220}," np.linalg.eigh(cov)          ",[210,1370,1372],{"class":1371},"sjoCn","# ascending\n",[210,1374,1375,1378,1380,1383,1385],{"class":212,"line":292},[210,1376,1377],{"class":220},"    minor, major ",[210,1379,251],{"class":216},[210,1381,1382],{"class":220}," np.sqrt(vals) ",[210,1384,770],{"class":216},[210,1386,1387],{"class":220}," scale\n",[210,1389,1390,1393,1395,1398,1400,1402,1404,1407,1409,1411,1413],{"class":212,"line":323},[210,1391,1392],{"class":220},"    angle ",[210,1394,251],{"class":216},[210,1396,1397],{"class":220}," math.degrees(math.atan2(vecs[",[210,1399,716],{"class":264},[210,1401,310],{"class":220},[210,1403,716],{"class":264},[210,1405,1406],{"class":220},"], vecs[",[210,1408,46],{"class":264},[210,1410,310],{"class":220},[210,1412,716],{"class":264},[210,1414,458],{"class":220},[210,1416,1417,1419],{"class":212,"line":342},[210,1418,832],{"class":216},[210,1420,1421],{"class":220}," major, minor, angle\n",[210,1423,1424],{"class":212,"line":356},[210,1425,242],{"emptyLinePlaceholder":241},[210,1427,1428,1431,1433],{"class":212,"line":380},[210,1429,1430],{"class":220},"major, minor, angle ",[210,1432,251],{"class":216},[210,1434,1435],{"class":220}," deviational_ellipse(xy, persons, mean)\n",[210,1437,1438,1440,1442,1444,1447,1449,1452,1454,1456,1459,1461,1464,1466,1468,1471,1473,1476,1478,1480],{"class":212,"line":396},[210,1439,941],{"class":264},[210,1441,944],{"class":220},[210,1443,1073],{"class":216},[210,1445,1446],{"class":257},"\"major ",[210,1448,1079],{"class":264},[210,1450,1451],{"class":220},"major",[210,1453,1085],{"class":216},[210,1455,1088],{"class":264},[210,1457,1458],{"class":257}," m, minor ",[210,1460,1079],{"class":264},[210,1462,1463],{"class":220},"minor",[210,1465,1085],{"class":216},[210,1467,1088],{"class":264},[210,1469,1470],{"class":257}," m, angle ",[210,1472,1079],{"class":264},[210,1474,1475],{"class":220},"angle",[210,1477,1085],{"class":216},[210,1479,1088],{"class":264},[210,1481,1482],{"class":257},"°, \"\n",[210,1484,1485,1488,1491,1493,1496,1498,1501,1504,1506,1509],{"class":212,"line":401},[210,1486,1487],{"class":216},"      f",[210,1489,1490],{"class":257},"\"elongation ",[210,1492,1079],{"class":264},[210,1494,1495],{"class":220},"major ",[210,1497,754],{"class":216},[210,1499,1500],{"class":220}," minor",[210,1502,1503],{"class":216},":.2f",[210,1505,1088],{"class":264},[210,1507,1508],{"class":257},"\"",[210,1510,649],{"class":220},[210,1512,1513],{"class":212,"line":416},[210,1514,242],{"emptyLinePlaceholder":241},[210,1516,1517,1520,1522,1525,1527,1530,1532],{"class":212,"line":430},[210,1518,1519],{"class":220},"circle_pts ",[210,1521,251],{"class":216},[210,1523,1524],{"class":220}," [QgsPointXY(math.cos(t) ",[210,1526,770],{"class":216},[210,1528,1529],{"class":220}," major, math.sin(t) ",[210,1531,770],{"class":216},[210,1533,1534],{"class":220}," minor)\n",[210,1536,1537,1540,1543,1545,1548,1550,1552,1554,1557,1560,1563],{"class":212,"line":461},[210,1538,1539],{"class":216},"              for",[210,1541,1542],{"class":220}," t ",[210,1544,410],{"class":216},[210,1546,1547],{"class":220}," np.linspace(",[210,1549,46],{"class":264},[210,1551,310],{"class":220},[210,1553,1312],{"class":264},[210,1555,1556],{"class":216}," *",[210,1558,1559],{"class":220}," math.pi, ",[210,1561,1562],{"class":264},"73",[210,1564,1565],{"class":220},")]\n",[210,1567,1568,1571,1573],{"class":212,"line":852},[210,1569,1570],{"class":220},"ellipse ",[210,1572,251],{"class":216},[210,1574,1575],{"class":220}," QgsGeometry.fromPolygonXY([circle_pts])\n",[210,1577,1578,1581,1583,1586,1588,1590,1592],{"class":212,"line":866},[210,1579,1580],{"class":220},"ellipse.rotate(",[210,1582,706],{"class":216},[210,1584,1585],{"class":220},"angle, QgsPointXY(",[210,1587,46],{"class":264},[210,1589,310],{"class":220},[210,1591,46],{"class":264},[210,1593,1167],{"class":220},[210,1595,1596,1599,1601,1604,1606],{"class":212,"line":902},[210,1597,1598],{"class":220},"ellipse.translate(mean[",[210,1600,46],{"class":264},[210,1602,1603],{"class":220},"], mean[",[210,1605,716],{"class":264},[210,1607,1608],{"class":220},"])\n",[14,1610,1611,1613,1614,1617],{},[468,1612,470],{}," The weighted covariance matrix of coordinates around the centre holds everything needed: its eigenvectors are the ellipse's axes and the square roots of its eigenvalues are the standard deviations along them. Multiplying by √2 is a common display convention that makes the ellipse enclose a larger share of points; whatever you choose, use the same factor for every group you compare. The angle is measured anticlockwise from the x-axis; ",[207,1615,1616],{},"QgsGeometry.rotate"," rotates clockwise, hence the sign. An elongation near 1 means no preferred direction; values above 2 indicate strong directionality.",[175,1619,1621],{"id":1620},"compare-groups-and-years","Compare groups and years",[14,1623,1624],{},"The summaries become informative when compared: the population centre in 2015 and 2025, the spread of two species, the orientation of incidents by time of day.",[200,1626,1628],{"className":202,"code":1627,"language":204,"meta":205,"style":205},"from collections import defaultdict\n\ngroups = defaultdict(list)\nfor f in households.getFeatures():\n    p = f.geometry().asPoint()\n    groups[f[\"census_year\"]].append((p.x(), p.y(), f[\"persons\"] or 0))\n\nsummary = {}\nfor year, rows in sorted(groups.items()):\n    a = np.array(rows)\n    pts, wts = a[:, :2], a[:, 2]\n    c = np.average(pts, axis=0, weights=wts)\n    sdist = math.sqrt(np.average(((pts - c) ** 2).sum(axis=1), weights=wts))\n    summary[year] = (c, sdist)\n    print(year, c.round(), round(sdist))\n\nyears = sorted(summary)\nfor a, b in zip(years, years[1:]):\n    shift = np.linalg.norm(summary[b][0] - summary[a][0])\n    print(f\"{a}→{b}: centre moved {shift:.0f} m, spread changed \"\n          f\"{summary[b][1] - summary[a][1]:+.0f} m\")\n",[207,1629,1630,1642,1646,1661,1672,1682,1703,1707,1717,1732,1742,1761,1786,1823,1833,1845,1849,1861,1881,1904,1945],{"__ignoreMap":205},[210,1631,1632,1634,1637,1639],{"class":212,"line":213},[210,1633,227],{"class":216},[210,1635,1636],{"class":220}," collections ",[210,1638,217],{"class":216},[210,1640,1641],{"class":220}," defaultdict\n",[210,1643,1644],{"class":212,"line":224},[210,1645,242],{"emptyLinePlaceholder":241},[210,1647,1648,1651,1653,1656,1659],{"class":212,"line":238},[210,1649,1650],{"class":220},"groups ",[210,1652,251],{"class":216},[210,1654,1655],{"class":220}," defaultdict(",[210,1657,1658],{"class":264},"list",[210,1660,649],{"class":220},[210,1662,1663,1665,1667,1669],{"class":212,"line":245},[210,1664,404],{"class":216},[210,1666,858],{"class":220},[210,1668,410],{"class":216},[210,1670,1671],{"class":220}," households.getFeatures():\n",[210,1673,1674,1677,1679],{"class":212,"line":270},[210,1675,1676],{"class":220},"    p ",[210,1678,251],{"class":216},[210,1680,1681],{"class":220}," f.geometry().asPoint()\n",[210,1683,1684,1687,1690,1693,1695,1697,1699,1701],{"class":212,"line":275},[210,1685,1686],{"class":220},"    groups[f[",[210,1688,1689],{"class":257},"\"census_year\"",[210,1691,1692],{"class":220},"]].append((p.x(), p.y(), f[",[210,1694,367],{"class":257},[210,1696,879],{"class":220},[210,1698,882],{"class":216},[210,1700,885],{"class":264},[210,1702,1167],{"class":220},[210,1704,1705],{"class":212,"line":292},[210,1706,242],{"emptyLinePlaceholder":241},[210,1708,1709,1712,1714],{"class":212,"line":323},[210,1710,1711],{"class":220},"summary ",[210,1713,251],{"class":216},[210,1715,1716],{"class":220}," {}\n",[210,1718,1719,1721,1724,1726,1729],{"class":212,"line":342},[210,1720,404],{"class":216},[210,1722,1723],{"class":220}," year, rows ",[210,1725,410],{"class":216},[210,1727,1728],{"class":264}," sorted",[210,1730,1731],{"class":220},"(groups.items()):\n",[210,1733,1734,1737,1739],{"class":212,"line":356},[210,1735,1736],{"class":220},"    a ",[210,1738,251],{"class":216},[210,1740,1741],{"class":220}," np.array(rows)\n",[210,1743,1744,1747,1749,1752,1754,1757,1759],{"class":212,"line":380},[210,1745,1746],{"class":220},"    pts, wts ",[210,1748,251],{"class":216},[210,1750,1751],{"class":220}," a[:, :",[210,1753,1312],{"class":264},[210,1755,1756],{"class":220},"], a[:, ",[210,1758,1312],{"class":264},[210,1760,267],{"class":220},[210,1762,1763,1766,1768,1771,1773,1775,1777,1779,1781,1783],{"class":212,"line":396},[210,1764,1765],{"class":220},"    c ",[210,1767,251],{"class":216},[210,1769,1770],{"class":220}," np.average(pts, ",[210,1772,663],{"class":662},[210,1774,251],{"class":216},[210,1776,46],{"class":264},[210,1778,310],{"class":220},[210,1780,672],{"class":662},[210,1782,251],{"class":216},[210,1784,1785],{"class":220},"wts)\n",[210,1787,1788,1791,1793,1796,1798,1801,1803,1805,1807,1809,1811,1813,1816,1818,1820],{"class":212,"line":401},[210,1789,1790],{"class":220},"    sdist ",[210,1792,251],{"class":216},[210,1794,1795],{"class":220}," math.sqrt(np.average(((pts ",[210,1797,706],{"class":216},[210,1799,1800],{"class":220}," c) ",[210,1802,1030],{"class":216},[210,1804,1033],{"class":264},[210,1806,1036],{"class":220},[210,1808,663],{"class":662},[210,1810,251],{"class":216},[210,1812,716],{"class":264},[210,1814,1815],{"class":220},"), ",[210,1817,672],{"class":662},[210,1819,251],{"class":216},[210,1821,1822],{"class":220},"wts))\n",[210,1824,1825,1828,1830],{"class":212,"line":416},[210,1826,1827],{"class":220},"    summary[year] ",[210,1829,251],{"class":216},[210,1831,1832],{"class":220}," (c, sdist)\n",[210,1834,1835,1837,1840,1842],{"class":212,"line":430},[210,1836,433],{"class":264},[210,1838,1839],{"class":220},"(year, c.round(), ",[210,1841,439],{"class":264},[210,1843,1844],{"class":220},"(sdist))\n",[210,1846,1847],{"class":212,"line":461},[210,1848,242],{"emptyLinePlaceholder":241},[210,1850,1851,1854,1856,1858],{"class":212,"line":852},[210,1852,1853],{"class":220},"years ",[210,1855,251],{"class":216},[210,1857,1728],{"class":264},[210,1859,1860],{"class":220},"(summary)\n",[210,1862,1863,1865,1868,1870,1873,1876,1878],{"class":212,"line":866},[210,1864,404],{"class":216},[210,1866,1867],{"class":220}," a, b ",[210,1869,410],{"class":216},[210,1871,1872],{"class":264}," zip",[210,1874,1875],{"class":220},"(years, years[",[210,1877,716],{"class":264},[210,1879,1880],{"class":220},":]):\n",[210,1882,1883,1886,1888,1891,1893,1895,1897,1900,1902],{"class":212,"line":902},[210,1884,1885],{"class":220},"    shift ",[210,1887,251],{"class":216},[210,1889,1890],{"class":220}," np.linalg.norm(summary[b][",[210,1892,46],{"class":264},[210,1894,879],{"class":220},[210,1896,706],{"class":216},[210,1898,1899],{"class":220}," summary[a][",[210,1901,46],{"class":264},[210,1903,1608],{"class":220},[210,1905,1906,1908,1910,1912,1914,1916,1918,1920,1923,1925,1928,1930,1933,1935,1938,1940,1942],{"class":212,"line":913},[210,1907,433],{"class":264},[210,1909,944],{"class":220},[210,1911,1073],{"class":216},[210,1913,1508],{"class":257},[210,1915,1079],{"class":264},[210,1917,21],{"class":220},[210,1919,1088],{"class":264},[210,1921,1922],{"class":257},"→",[210,1924,1079],{"class":264},[210,1926,1927],{"class":220},"b",[210,1929,1088],{"class":264},[210,1931,1932],{"class":257},": centre moved ",[210,1934,1079],{"class":264},[210,1936,1937],{"class":220},"shift",[210,1939,1085],{"class":216},[210,1941,1088],{"class":264},[210,1943,1944],{"class":257}," m, spread changed \"\n",[210,1946,1947,1950,1952,1954,1957,1959,1961,1963,1965,1967,1970,1973,1975,1977],{"class":212,"line":938},[210,1948,1949],{"class":216},"          f",[210,1951,1508],{"class":257},[210,1953,1079],{"class":264},[210,1955,1956],{"class":220},"summary[b][",[210,1958,716],{"class":264},[210,1960,879],{"class":220},[210,1962,706],{"class":216},[210,1964,1899],{"class":220},[210,1966,716],{"class":264},[210,1968,1969],{"class":220},"]",[210,1971,1972],{"class":216},":+.0f",[210,1974,1088],{"class":264},[210,1976,1091],{"class":257},[210,1978,649],{"class":220},[14,1980,1981,1983,1984,1987,1988,1991],{},[468,1982,470],{}," Computing each group's centre and standard distance with the same weights and CRS makes them directly comparable. The movement of the centre between periods — its distance and, with ",[207,1985,1986],{},"atan2",", its direction — is a compact way to describe drift such as suburban growth. Drawing successive centres as points joined in order with ",[207,1989,1990],{},"native:pointstopath"," produces the familiar \"centre of population\" track. Small groups give noisy centres; report the count with each.",[175,1993,1995],{"id":1994},"draw-the-centres-track-over-time","Draw the centre's track over time",[14,1997,1998],{},"The comparison above prints numbers; a map of the centre moving from year to year communicates the same result instantly. Writing each period's centre as a point with its year, then joining the points in order, produces the classic \"centre of population\" track.",[200,2000,2002],{"className":202,"code":2001,"language":204,"meta":205,"style":205},"from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry, QgsPointXY\n\ntrack = QgsVectorLayer(f\"Point?crs={households.crs().authid()}&field=year:integer\"\n                       \"&field=sd_m:double\", \"population centre by year\", \"memory\")\nrows = []\nfor year in years:\n    c, sdist = summary[year]\n    f = QgsFeature(track.fields())\n    f.setAttributes([int(year), round(sdist, 1)])\n    f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(*c)))\n    rows.append(f)\ntrack.dataProvider().addFeatures(rows)\n\npath = processing.run(\"native:pointstopath\", {\n    \"INPUT\": track, \"ORDER_EXPRESSION\": '\"year\"', \"GROUP_EXPRESSION\": \"\",\n    \"OUTPUT\": \"memory:centre_path\"})[\"OUTPUT\"]\nQgsProject.instance().addMapLayers([track, path])\n",[207,2003,2004,2015,2019,2042,2058,2068,2080,2090,2099,2120,2130,2135,2140,2144,2158,2185,2200],{"__ignoreMap":205},[210,2005,2006,2008,2010,2012],{"class":212,"line":213},[210,2007,227],{"class":216},[210,2009,230],{"class":220},[210,2011,217],{"class":216},[210,2013,2014],{"class":220}," QgsVectorLayer, QgsFeature, QgsGeometry, QgsPointXY\n",[210,2016,2017],{"class":212,"line":224},[210,2018,242],{"emptyLinePlaceholder":241},[210,2020,2021,2024,2026,2028,2030,2033,2035,2037,2039],{"class":212,"line":238},[210,2022,2023],{"class":220},"track ",[210,2025,251],{"class":216},[210,2027,1107],{"class":220},[210,2029,1073],{"class":216},[210,2031,2032],{"class":257},"\"Point?crs=",[210,2034,1079],{"class":264},[210,2036,1117],{"class":220},[210,2038,1088],{"class":264},[210,2040,2041],{"class":257},"&field=year:integer\"\n",[210,2043,2044,2047,2049,2052,2054,2056],{"class":212,"line":245},[210,2045,2046],{"class":257},"                       \"&field=sd_m:double\"",[210,2048,310],{"class":220},[210,2050,2051],{"class":257},"\"population centre by year\"",[210,2053,310],{"class":220},[210,2055,1134],{"class":257},[210,2057,649],{"class":220},[210,2059,2060,2063,2065],{"class":212,"line":270},[210,2061,2062],{"class":220},"rows ",[210,2064,251],{"class":216},[210,2066,2067],{"class":220}," []\n",[210,2069,2070,2072,2075,2077],{"class":212,"line":275},[210,2071,404],{"class":216},[210,2073,2074],{"class":220}," year ",[210,2076,410],{"class":216},[210,2078,2079],{"class":220}," years:\n",[210,2081,2082,2085,2087],{"class":212,"line":292},[210,2083,2084],{"class":220},"    c, sdist ",[210,2086,251],{"class":216},[210,2088,2089],{"class":220}," summary[year]\n",[210,2091,2092,2094,2096],{"class":212,"line":323},[210,2093,419],{"class":220},[210,2095,251],{"class":216},[210,2097,2098],{"class":220}," QgsFeature(track.fields())\n",[210,2100,2101,2104,2107,2110,2112,2115,2117],{"class":212,"line":342},[210,2102,2103],{"class":220},"    f.setAttributes([",[210,2105,2106],{"class":264},"int",[210,2108,2109],{"class":220},"(year), ",[210,2111,439],{"class":264},[210,2113,2114],{"class":220},"(sdist, ",[210,2116,716],{"class":264},[210,2118,2119],{"class":220},")])\n",[210,2121,2122,2125,2127],{"class":212,"line":356},[210,2123,2124],{"class":220},"    f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(",[210,2126,770],{"class":216},[210,2128,2129],{"class":220},"c)))\n",[210,2131,2132],{"class":212,"line":380},[210,2133,2134],{"class":220},"    rows.append(f)\n",[210,2136,2137],{"class":212,"line":396},[210,2138,2139],{"class":220},"track.dataProvider().addFeatures(rows)\n",[210,2141,2142],{"class":212,"line":401},[210,2143,242],{"emptyLinePlaceholder":241},[210,2145,2146,2149,2151,2153,2156],{"class":212,"line":416},[210,2147,2148],{"class":220},"path ",[210,2150,251],{"class":216},[210,2152,283],{"class":220},[210,2154,2155],{"class":257},"\"native:pointstopath\"",[210,2157,289],{"class":220},[210,2159,2160,2162,2165,2168,2170,2173,2175,2178,2180,2183],{"class":212,"line":430},[210,2161,295],{"class":257},[210,2163,2164],{"class":220},": track, ",[210,2166,2167],{"class":257},"\"ORDER_EXPRESSION\"",[210,2169,304],{"class":220},[210,2171,2172],{"class":257},"'\"year\"'",[210,2174,310],{"class":220},[210,2176,2177],{"class":257},"\"GROUP_EXPRESSION\"",[210,2179,304],{"class":220},[210,2181,2182],{"class":257},"\"\"",[210,2184,320],{"class":220},[210,2186,2187,2189,2191,2194,2196,2198],{"class":212,"line":461},[210,2188,326],{"class":257},[210,2190,304],{"class":220},[210,2192,2193],{"class":257},"\"memory:centre_path\"",[210,2195,334],{"class":220},[210,2197,337],{"class":257},[210,2199,267],{"class":220},[210,2201,2202],{"class":212,"line":852},[210,2203,2204],{"class":220},"QgsProject.instance().addMapLayers([track, path])\n",[14,2206,2207,2209,2210,2212,2213,2217],{},[468,2208,470],{}," Storing the standard distance with each centre lets you scale the point symbols by spread, so the map shows both where the centre went and how the distribution widened or narrowed. ",[207,2211,1990],{}," orders points by the expression and joins them into one line; with an arrow symbol layer on the line, as in ",[21,2214,2216],{"href":2215},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Farrow-symbol-layer-pyqgis\u002F","arrow symbol layers",", direction is clear at a glance. Label points with the year and keep the basemap quiet so the track stands out.",[175,2219,2221],{"id":2220},"qgis-version-compatibility","QGIS version compatibility",[14,2223,2224,2227,2228,2231,2232,310,2235,2238,2239,310,2241,2244,2245,475,2247,2250],{},[207,2225,2226],{},"native:meancoordinates"," has the same parameters on QGIS 3.34 LTR, 3.40 LTR and QGIS 4 (earlier 3.x releases used ",[207,2229,2230],{},"qgis:meancoordinates","). The NumPy functions used — ",[207,2233,2234],{},"average",[207,2236,2237],{},"cov"," with ",[207,2240,1343],{},[207,2242,2243],{},"linalg.eigh"," — are available in every NumPy version shipped with current QGIS. ",[207,2246,1616],{},[207,2248,2249],{},"translate"," work in place on all releases.",[175,2252,2254],{"id":2253},"troubleshooting","Troubleshooting",[180,2256,2257,2263,2269,2278],{},[183,2258,2259,2262],{},[468,2260,2261],{},"The centre is in the sea."," The distribution is crescent-shaped or bimodal; the mean centre need not lie inside it. Report the median centre, or analyse the parts separately.",[183,2264,2265,2268],{},[468,2266,2267],{},"Distances are tiny decimals."," The layer is geographic; reproject first.",[183,2270,2271,2274,2275,2277],{},[468,2272,2273],{},"The ellipse points the wrong way."," The rotation sign is reversed; ",[207,2276,1616],{}," uses clockwise degrees.",[183,2279,2280,2283],{},[468,2281,2282],{},"Weighted results equal unweighted."," The weight field is mostly NULL, which becomes 0 or 1 depending on how you read it; check it first.",[175,2285,2287],{"id":2286},"conclusion","Conclusion",[14,2289,2290,2291,2293],{},"Compute the mean centre with ",[207,2292,2226],{},", add the median centre for robustness to outliers, measure spread with the standard distance and its direction with a covariance-based ellipse, keep weights and CRS identical across groups, and compare groups and years by centre movement and change in spread.",[175,2295,2297],{"id":2296},"frequently-asked-questions","Frequently Asked Questions",[14,2299,2300,2303],{},[468,2301,2302],{},"Can I compute these for polygons?","\nUse centroids or points on surface, weighted by area or population, as inputs.",[14,2305,2306,2309],{},[468,2307,2308],{},"What share of points does the standard distance circle contain?","\nAbout 63–68 % for a roughly circular normal distribution; less for skewed or clustered data.",[14,2311,2312,2315,2316,2320],{},[468,2313,2314],{},"Is the median centre the best facility location?","\nOnly for straight-line distance. For road travel, use network analysis such as an ",[21,2317,2319],{"href":2318},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-origin-destination-matrix-pyqgis\u002F","origin–destination matrix",".",[14,2322,2323,2326],{},[468,2324,2325],{},"Does QGIS have a deviational ellipse algorithm?","\nNot in core Processing; the NumPy approach above is short and transparent.",[175,2328,2330],{"id":2329},"related","Related",[180,2332,2333,2338,2344,2350,2356],{},[183,2334,2335,2337],{},[21,2336,24],{"href":23}," — the guide this recipe belongs to",[183,2339,2340],{},[21,2341,2343],{"href":2342},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fnearest-neighbour-analysis-pyqgis\u002F","Run a Nearest Neighbour Analysis in PyQGIS",[183,2345,2346],{},[21,2347,2349],{"href":2348},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fcluster-points-with-dbscan-and-kmeans-pyqgis\u002F","Cluster Points with DBSCAN and K-Means in PyQGIS",[183,2351,2352],{},[21,2353,2355],{"href":2354},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fwrite-numpy-array-to-raster-pyqgis\u002F","Write a NumPy Array to a Raster in PyQGIS",[183,2357,2358],{},[21,2359,2361],{"href":2360},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002F","Transform Point Coordinates Between CRS in PyQGIS",[2363,2364,2365],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":205,"searchDepth":224,"depth":224,"links":2367},[2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379],{"id":177,"depth":224,"text":178},{"id":194,"depth":224,"text":195},{"id":486,"depth":224,"text":487},{"id":988,"depth":224,"text":989},{"id":1185,"depth":224,"text":1186},{"id":1620,"depth":224,"text":1621},{"id":1994,"depth":224,"text":1995},{"id":2220,"depth":224,"text":2221},{"id":2253,"depth":224,"text":2254},{"id":2286,"depth":224,"text":2287},{"id":2296,"depth":224,"text":2297},{"id":2329,"depth":224,"text":2330},"Summarise where a set of points is and how spread out it is — weighted mean centre, median centre, standard distance circle and standard deviational ellipse — computed with Processing and NumPy, compared across groups and tracked over time.","md",{"slug":2383,"type":2384,"breadcrumb":2385,"datePublished":2386,"dateModified":2386},"calculate-mean-center-and-standard-distance-pyqgis","article","Mean Centre and Standard Distance","2026-10-02","\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fcalculate-mean-center-and-standard-distance-pyqgis",{"title":5,"description":2380},"spatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fcalculate-mean-center-and-standard-distance-pyqgis\u002Findex","2wy6SPn2a6FOKIJt21IlOQv-NLPWDewqj0CpOJkHdfg",1790966264870]