An interactive lab with Python, R and MapLibre — upload a point file (or use the built-in example), run DBSCAN, see how a distance in meters requires picking a real coordinate system first, and why the two languages label the exact same groups with different numbers.
Published
September 9, 2026
Some questions aren’t “is this one point inside that polygon” — they’re “which of these points belong together, and which don’t belong anywhere.” DBSCAN (Density-Based Spatial Clustering of Applications with Noise) answers that by density: a point belongs to a cluster if enough other points sit close enough to it — no need to decide the number of clusters in advance, and no need to force every point into a group. A point too far from everything else stays noise, not a cluster of one.
A .geojson, or a shapefile (either as a single .zip, or as .shp/.dbf/.shx selected together) — see the upload section below.
Just want to cluster a file with adjustable settings and a download button, without the rest of the reading? Use the standalone tool — same engine, less prose.
1. Why the coordinate system matters here
DBSCAN takes one distance, ε (eps): how close two points need to be to count as neighbors. For that distance to mean anything, it has to be in real units — meters — not degrees of longitude/latitude, where the same number of degrees covers very different real distances depending on where on Earth you are. Both languages below pick a working UTM zone from the data’s own bounding-box center and reproject into it first — same reasoning, same formula, as this site’s Viewshed tool: a reasonable default for one local dataset, not a universally correct choice for points spanning multiple UTM zones or very large areas.
{const result =await WebGeoDS.Upload.load(uploadedFiles); mutable uploadStatus = result.message;}
No file uploaded? Both cells below fall back to the same small built-in example: two tight groups of six points, and two points far from everything — including each other — chosen to keep the point-by-point label comparison in section 4 readable. ε and the minimum group size are fixed at 100m/4 points here to keep the focus on how the two languages report a result, not on tuning — the standalone tool exposes both as adjustable controls.
// Warm-palette hues from _brand.yml, same palette/roles as the// Spatial Clustering Explorer tool -- reused here rather than// invented fresh, so a reader who's seen the tool recognizes the// same visual language.CLUSTER_PALETTE = ["#ab502b","#42583c","#3d5a73","#c48a2e","#8b2f24"]
NOISE_COLOR ="#766851"
// The map shows Python's own partition/coloring only -- not because// R's result is somehow secondary, but because both languages agree// on WHICH points group together (that's the whole point of section// 4 below); drawing the same partition twice, in two different label// numberings, would just be two circle layers stacked on identical// coordinates with one hiding the other.clusterPaint = (clusterIds) => {const expr = ["match", ["get","cluster"]]; clusterIds.forEach((id, i) => { expr.push(id, CLUSTER_PALETTE[i % CLUSTER_PALETTE.length]); }); expr.push(NOISE_COLOR);return {"circle-color": expr,"circle-radius":6,"circle-stroke-width":1,"circle-stroke-color":"#2a2117" };}
Both cards above found the same thing: two groups of six, and two noise points (with the built-in example — an upload of your own may find something different). But the numbers don’t match — and that’s not a bug in either language.
Two separate reasons, easy to conflate:
DBSCAN’s own cluster numbering is arbitrary. Nothing about “cluster 0” or “cluster 1” is meaningful on its own — it’s just the order the algorithm happened to visit points in. Run the same algorithm again with the points in a different order (or on a different implementation entirely) and the groupings can come out identical while the numbers attached to them differ.
scikit-learn and dbscan also disagree on noise, specifically. This one isn’t arbitrary — it’s a real, documented difference between the two libraries: scikit-learn labels noise -1 and starts real clusters at 0; R’s dbscan package labels noise 0 and starts real clusters at 1. Every non-noise label is shifted by exactly one.
Read down the table: every point Python calls 0, R calls 1; every point Python calls 1, R calls 2; every point Python calls -1 (noise), R calls 0. The partition — which points end up together — is identical. The labels attached to it aren’t, and never had to be.
Note
This is why comparing two clustering results — across languages, or across two runs of the same algorithm — should compare who’s grouped with whom, never raw label equality. In practice: pick any two points, and ask “does language A say they’re in the same group, and does language B agree?” — repeat for every pair, and you’re comparing partitions, not numbers that were never meant to match.
5. Where to next
Just needed to cluster a file, with adjustable settings and a download button? → The standalone Spatial Clustering Explorer runs the same detection on an uploaded point file (or its own example), with adjustable ε and minimum group size, colored on the map, downloadable.
Wondering why the coordinate system needed picking at all? → Coordinate Reference Systems covers the geographic/projected distinction this article leaned on in section 1.