=== versions === python 3.12.0 | scikit-learn 1.9.1 | hdbscan 0.8.44 | geopandas 1.2.0 | shapely 2.2.0 | duckdb 1.5.6 | numpy 2.5.4 === Glasgow City boundary === Glasgow City: 174.6 km2, lon/lat bbox -4.3924, 55.7813, -4.0717, 55.9296 === Overture Maps places === Overture release 2026-10-21.0: 53,115 places of all kinds in the bounding box inside Glasgow City: 41,382 places; food and drink (restaurant, casual_eatery, cafe, coffee_shop, fast_food_restaurant, bar), confidence >= 0.5, not permanently closed: 3,950 basic_category restaurant 1754 bar 708 casual_eatery 498 cafe 404 coffee_shop 311 fast_food_restaurant 275 points sharing exact coordinates with another: 184 (4.7%) postcode district available for 94% of places === k-distance plot === distance to the 9th nearest other place: median 95 m, 90th percentile 513 m, max 3,331 m knee of the k-distance curve at rank 2698 (68%): 171 m chosen eps = 150 m (65% of places have 9 others within it) === DBSCAN: haversine on lat/lon vs Euclidean on metres === haversine (radians) : 44 clusters, noise 29.1%, 25 ms projected metres : 44 clusters, noise 29.1%, 13 ms adjusted Rand index between the two: 0.9994; points labelled differently as noise/non-noise: 1 mistake check: eps=150 passed straight to haversine (radians) -> 1 cluster(s), noise 0% mistake check: Euclidean on raw degrees (eps = 150/111,320) -> 57 clusters, ARI vs metres 0.803 core points 2,569, border points 230, noise 1,151 largest cluster 1,205 places (31% of all); median cluster size 19 === DBSCAN sensitivity === rows min_samples, columns eps (m): number of clusters / % noise / largest cluster as % of places 75 100 150 200 300 400 5 120 / 29% / 25% 108 / 23% / 28% 99 / 17% / 46% 89 / 13% / 47% 61 / 8% / 59% 48 / 4% / 73% 10 49 / 48% / 14% 56 / 37% / 27% 44 / 29% / 31% 37 / 25% / 46% 32 / 16% / 52% 26 / 10% / 71% 15 26 / 60% / 14% 20 / 53% / 25% 29 / 38% / 30% 31 / 30% / 35% 25 / 22% / 48% 18 / 15% / 62% 20 23 / 70% / 10% 16 / 58% / 25% 19 / 47% / 28% 18 / 38% / 35% 21 / 26% / 47% 18 / 18% / 58% 30 9 / 85% / 4% 8 / 73% / 14% 12 / 56% / 27% 9 / 49% / 30% 10 / 36% / 47% 15 / 26% / 50% === HDBSCAN === sklearn HDBSCAN(min_cluster_size=15, min_samples=10): 54 clusters, noise 28.2%, largest 523 places, median size 34, 63 ms same on lat/lon radians with metric='haversine': 53 clusters, ARI vs metres 0.9772 cluster_selection_method='leaf': 67 clusters, noise 34.4% min_cluster_size=10: 88 clusters, noise 34.1% min_cluster_size=25: 39 clusters, noise 22.7% min_cluster_size=50: 18 clusters, noise 29.9% ARI DBSCAN vs HDBSCAN: 0.315 HDBSCAN cluster density (places per km2 of convex hull): min 16, median 332, max 3711 (227x range) hdbscan package: 51 clusters, ARI vs sklearn 0.8595, relative_validity_ (fast DBCV approximation) 0.205 membership strength: median 0.99; outlier scores > 0.9: 649 DBSCAN density floor: 10 places in a 150 m circle = 141 places per km2; 18 of 54 HDBSCAN clusters are sparser than that === cluster polygons: convex vs concave hulls === 54 clusters: convex hulls cover 21.09 km2, concave hulls (ratio 0.3) 9.48 km2 (45% of convex) clustered places: 2,835 of 3,950 (72%) in 5.4% of the city's area ten largest clusters: cluster n district concave_km2 convex_km2 top bars 52 523 G1 0.42 0.54 restaurant 41% 0.29 53 154 G2 0.09 0.09 restaurant 51% 0.26 21 106 G41 0.23 0.40 restaurant 42% 0.11 38 104 G3 0.05 0.08 restaurant 54% 0.14 49 103 G1 0.07 0.10 restaurant 44% 0.32 42 87 G12 0.06 0.13 restaurant 45% 0.15 45 79 G11 0.03 0.07 restaurant 59% 0.16 3 78 G69 1.83 3.22 restaurant 45% 0.23 27 69 G31 0.08 0.18 restaurant 43% 0.12 22 69 G31 0.48 0.84 restaurant 36% 0.20 noise: 1,115 places; most common categories: restaurant 42%, bar 17%, casual eatery 12% distance from each noise place to the nearest clustered place: median 297 m, 30% more than 500 m away === internal validation: silhouette and DBCV === DBSCAN : silhouette (non-noise only) 0.270 | DBCV -0.022 | 44 clusters | noise 29.1% HDBSCAN : silhouette (non-noise only) 0.516 | DBCV 0.193 | 54 clusters | noise 28.2% null check: 2,380 uniformly random points in the city -> HDBSCAN finds 28 clusters, noise 51%, DBCV 0.096 null check: DBSCAN (eps=150 m, min_samples=10) on the same random points -> 0 clusters, noise 100% === scaling: BallTree neighbourhoods === all Overture places in Glasgow City: 41,382 BallTree build 15 ms; 41,382 radius queries of 150 m in 451 ms; neighbours per point median 71, max 1,301 a dense haversine distance matrix for all of them would need 13.7 GB n= 2,000: DBSCAN ball_tree 0.01 s | brute 0.02 s | HDBSCAN 0.11 s n= 4,000: DBSCAN ball_tree 0.03 s | brute 0.04 s | HDBSCAN 0.38 s n= 8,000: DBSCAN ball_tree 0.07 s | brute 0.16 s | HDBSCAN 1.33 s n=16,000: DBSCAN ball_tree 0.19 s | brute 0.61 s | HDBSCAN 5.20 s n=32,000: DBSCAN ball_tree 0.44 s | HDBSCAN 20.16 s n=41,382: DBSCAN ball_tree 0.70 s | HDBSCAN 34.05 s HDBSCAN on all 41,382 places in projected metres (KD-tree): 6.10 s wrote output/glasgow_dining_clusters.gpkg, output/metrics.json and 6 figures