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.

Five Reported Clusters, Two Distinct Places On the left, the default overlap criterion reports five significant clusters: three heavily overlapping circles around one excess in the north-west and two around a second excess in the south-east. On the right, a no-geographic-overlap criterion reports two clusters, one at each location. The underlying data are identical; only the reporting rule changed, and a summary quoting five clusters would double the apparent number of events. The same excesses, two reporting rules default overlap criterion no geographic overlap 5 significant clusters reported 2 significant clusters reported A count of clusters is a property of the reporting rule until you make it a property of the data

Prerequisites

  • A completed scan producing a cluster list with member areas, likelihood ratios and p-values
  • The area geometry, to compute overlap
  • python 3.11, geopandas 1.0.1, pandas 2.2.2, shapely 2.0.6

The relationship between reported and distinct clusters depends entirely on the overlap rule, and three rules are in common use:

Reported Cluster Count Under Three Overlap Rules Cluster counts for the same scan output under three overlap criteria. Reporting every significant window gives 23 clusters. The common default, which excludes clusters containing a more likely cluster's centre, gives 5. Requiring no geographic overlap at all gives 2. All three describe the same two excesses, and only the last count is a count of places. Same output, three counts, two excesses all significant windows 23 default overlap rule 5 no overlap at all 2 places Only the bottom bar answers “how many places should we investigate”

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]) + ("&#8230;" 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:

Scan Output Against Investigation Record Two record shapes for the same cluster. The scan emits a centre coordinate, a radius in kilometres, a log likelihood ratio and a p-value. The investigation record carries the list of member areas, the observed and expected counts, the relative risk and the p-value. The second can be acted on by a field team; the first requires the recipient to reconstruct the geography before they can begin. The scan emits the left column; the investigation needs the right what the scan emits centre: 42.3712, −71.0589 radius: 8.4 km log likelihood ratio: 14.72 p = 0.001 what an investigation needs areas: 36001, 36003, 36007, +6 observed 84, expected 31.2 relative risk 2.69 p = 0.001 Producing the right-hand record is a few lines, and it is the difference between a result and a report

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.