Interpreting Overlapping SaTScan Clusters
A scan reports one most likely cluster and a list of secondary clusters, and the secondary list is where most misinterpretation happens. Under the default settings a large share of secondary clusters are nested variants of the primary one rather than separate findings, and a report that lists them as distinct locations overstates the number of events by a factor of several. This guide, part of Spatial Scan Statistics Configuration, covers reading the list correctly.
Problem Context & Constraints
The scan evaluates a very large family of overlapping windows and ranks them by likelihood. The most likely cluster is the top of that ranking. Everything below it is also a window, and many of those windows share most of their area with the winner — a circle one unit larger, one unit smaller, or centred on the adjacent area.
The software’s overlap criterion decides which of those are reported. The common default reports secondary clusters that do not contain the centre of a more likely cluster, which still permits substantial overlap, so the output contains several descriptions of one excess. A stricter criterion — no geographic overlap at all — reports fewer clusters and each is a distinct place.
Two consequences follow. A count of significant clusters is not a count of outbreaks unless the overlap criterion has been chosen to make it one. And the secondary clusters’ p-values are conditional on the primary having been removed in the ranking, so they are not independent tests and should not be counted as such in any multiplicity argument.
Prerequisites
- A completed scan producing a cluster list with member areas, likelihood ratios and p-values
- The area geometry, to compute overlap
python3.11,geopandas1.0.1,pandas2.2.2,shapely2.0.6
The relationship between reported and distinct clusters depends entirely on the overlap rule, and three rules are in common use:
Step-by-Step Solution
# Collapse an overlapping cluster list into distinct findings.
# Pinned: geopandas==1.0.1, pandas==2.2.2, shapely==2.0.6
import logging
import pandas as pd
import geopandas as gpd
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("scan.overlap")
def cluster_geometries(clusters: pd.DataFrame, areas: gpd.GeoDataFrame,
id_col: str = "geoid") -> gpd.GeoDataFrame:
"""Dissolve each cluster's member areas into one polygon."""
geoms, rows = [], []
lookup = areas.set_index(id_col).geometry
for _, c in clusters.iterrows():
members = list(c["member_ids"])
geoms.append(lookup.loc[members].union_all())
rows.append({k: c[k] for k in ("cluster_id", "llr", "p_value", "observed", "expected")})
return gpd.GeoDataFrame(rows, geometry=geoms, crs=areas.crs)
def collapse_overlapping(cg: gpd.GeoDataFrame, max_jaccard: float = 0.0) -> gpd.GeoDataFrame:
"""Greedy selection by likelihood, rejecting anything overlapping a kept cluster.
max_jaccard=0.0 means no shared area at all, which is the strictest and the
most interpretable rule: every reported cluster is a distinct place. Relaxing
it is defensible for genuinely adjacent outbreaks and must be stated."""
kept = []
for _, cand in cg.sort_values("llr", ascending=False).iterrows():
clash = False
for k in kept:
inter = cand.geometry.intersection(k.geometry).area
union = cand.geometry.union(k.geometry).area
if union > 0 and inter / union > max_jaccard:
clash = True
break
if not clash:
kept.append(cand)
out = gpd.GeoDataFrame(kept, crs=cg.crs)
log.info("collapsed %d reported clusters to %d distinct findings at jaccard <= %.2f",
len(cg), len(out), max_jaccard)
return out
def describe_for_investigation(collapsed: gpd.GeoDataFrame, areas: gpd.GeoDataFrame,
id_col: str = "geoid") -> pd.DataFrame:
"""The fields an investigator needs, rather than the fields the scan emits."""
rows = []
for _, c in collapsed.iterrows():
members = areas.loc[areas.geometry.intersects(c.geometry), id_col].tolist()
rows.append({
"cluster_id": c["cluster_id"],
"n_areas": len(members),
"areas": ", ".join(sorted(members)[:8]) + ("…" if len(members) > 8 else ""),
"observed": int(c["observed"]),
"expected": round(float(c["expected"]), 1),
"relative_risk": round(float(c["observed"]) / max(float(c["expected"]), 1e-9), 2),
"p_value": float(c["p_value"]),
})
df = pd.DataFrame(rows)
log.info("investigation table:\n%s", df.to_string(index=False))
return df
The describe_for_investigation step matters as much as the collapse. A scan’s native output identifies a cluster by a centroid and a radius, which is not how anyone works: an investigator needs the list of areas, the observed and expected counts, and the relative risk. Producing that table is a few lines and it is the difference between a result and a report.
Validation & Edge Cases
1. Check whether the collapsed clusters are adjacent. Two distinct clusters sharing a boundary may be one excess split by the scan’s circular window, particularly when the true cluster is elongated along a road or river. Where that is plausible, note it rather than reporting two independent findings:
INFO collapsed 5 reported clusters to 2 distinct findings at jaccard <= 0.00
INFO investigation table:
cluster_id n_areas areas observed expected relative_risk p_value
1 9 36001,36003,36007… 84 31.2 2.69 0.0010
4 4 36041,36042,36045 29 12.7 2.28 0.0270
2. Do not re-test the collapsed set. The p-values come from the scan’s own permutation procedure and already account for the multiplicity of the window search. Applying a further correction across the reported clusters double-counts.
3. Treat a cluster whose relative risk is near one with suspicion regardless of its p-value. A very large cluster can reach significance with a small relative risk simply because it contains many cases, and such a cluster rarely corresponds to anything an investigation can act on. Report the relative risk beside the p-value so the reader can weigh both.
4. Watch for clusters bounded by the maximum window size. A cluster whose size equals the configured maximum is a truncated cluster: the scan wanted to grow it further and was not allowed. That is a configuration finding, and it should prompt a re-run at a larger maximum rather than a report of a cluster of exactly that size.
5. Keep the full list, publish the collapsed one. The secondary clusters are useful evidence about the shape and stability of an excess, and discarding them loses that. Store the complete output and report the collapsed set.
6. Look at the cluster’s shape before believing its extent. The circular scan reports circles, and a real excess along a river valley or a commuting corridor will be covered by a circle that includes a great deal of unaffected territory. The collapsed cluster’s area list is what reveals this: if the elevated areas within the reported circle form a line and the rest are ordinary, the finding is a corridor and the circle is the scan’s best approximation to it. Saying so in the report prevents an investigation being scoped to the whole circle.
7. Compare the cluster against the previous period’s clusters. In a repeated retrospective programme, the same location appearing period after period usually indicates a stable baseline difference that the expected counts have not captured — a demographic composition effect, a reporting-practice difference, a facility catchment. That is a specification finding rather than an outbreak, and it is only visible by keeping the cluster history.
8. Report clusters that fell just short. A cluster with a p-value of 0.06 is not evidence of nothing, and in a programme that will re-run next period it is worth carrying forward as a watch item. Publishing the near-misses alongside the significant clusters gives the next period’s analyst context that a bare list of significant results does not.
9. Say how many clusters were evaluated, not only how many were reported. The scan searched thousands of windows and the reported list is the tip of that search. Quoting the number of windows evaluated alongside the number of clusters reported gives a reader the scale of the multiplicity the permutation procedure had to absorb, which is otherwise invisible and is the usual source of the question “how do we know these are not chance”.
The fields an investigator needs are not the fields the scan emits, and the translation is worth making once:
Compliance Notes
- State the overlap criterion in the methods and in any table of cluster counts, because the count is meaningless without it.
- Publish observed, expected and relative risk for every reported cluster, not only the p-value.
- Flag clusters at the maximum window size, since they indicate the configuration constrained the result.
- Apply disclosure review to the area lists, which name small geographies with elevated counts and are among the more sensitive outputs a surveillance programme produces.
Related Topics
- Spatial Scan Statistics Configuration — the parent guide, including maximum cluster size and model choice.
- Configuring SaTScan for Retrospective Cluster Detection — the run that produces the list this guide reads.
- Space-Time Cluster Detection — the prospective setting, where overlapping reports compound across days.