pyscan

What is a spatial scan statistic?

populationcases (confirmed COVID-19)

The real spread of COVID-19, from the Wuhan cluster on 31 December 2019 to 31 March 2020. Black dots show where people live; each red dot is about 100 confirmed cases. Planes fly the first documented routes, and on 11 March the WHO declares a pandemic. At the end of March the scan settles on a window around the Alps. Data: Johns Hopkins CSSE; Italy, Protezione Civile. Earth: NASA Blue Marble.

Say we want to know where COVID-19 cases were unusually dense, and by how much. The black dots show where people live: that’s the population, b. The red dots are confirmed cases: we call them m. The scan slides a window over the map, at every size and position, and gives every window a score, Kulldorff’s log-likelihood ratio:

phi of Y equals m of Y times log of m of Y over b of Y, plus 1 minus m of Y, times log of 1 minus m of Y over 1 minus b of Y

Here m(Y) and b(Y) are the shares of all cases and of all people inside the window Y, and the expected share is what the same rate everywhere would give.

In plain English, for the window it found:

just over 1% of all people live here
so we expect just over 1% of the cases
17% of the cases are here

13× more than expected → high score

The scan keeps the window with the highest score.

Scan statistics are used to find cancer clusters, disease outbreaks such as COVID-19, emerging public-health threats and street crime: hotspots of cases, where they are and how unusual. The common tools work on points. SaTScan is the most widely used; for regions, it uses one point per region, typically the centroid. pyscan is a fast, open-source scanner, Python on a C++ backend, by my co-author Michael Matheny, and it is what we use.

The gap

Real data come as counts per region

The problem. Public-health and crime maps almost never come with addresses — you get one total for a whole county or zip code, and some counties are the size of a small state. The fastest algorithms for spotting a hotspot need actual points on the map, so the standard shortcut squashes each county down to a single dot at its center. That dot erases the county’s real shape and size — and a disease outbreak that a health department finds late, or not at all, is measured in lives, not in decimals of statistical power.

California counties shaded by Valley Fever rate, darkest in the San Joaquin Valley, outlined with a dashed line
Published · one rate per county
California with one red dot per county; the scan's red window covers only the southern part of the dashed valley
Centroid · one point per county
Valley Fever is a fungal infection. Its fungus lives in the soil of California's San Joaquin Valley (dashed), so we know where the hotspot should be. The state publishes one rate per county, with no addresses, for privacy. With one point per county, at its centroid, the scan's window misses most of the valley. Data: CDPH, 2014–2018.
Why it misses: one point stands for the whole region. Kern County, the big Valley Fever county: a window over 46% of it sees zero cases with the centroid, and 48% with 50 points. A ZIP code in Henderson, Nevada: the centroid is inside, but near a thin neck; a window over 68% of the ZIP code sees zero cases, and 68% with points. Henrico County, Virginia, wraps around the City of Richmond, so its centroid is not even in Henrico; a window over 58% of the county sees zero cases, and 58% with points. A region is not a point. The grey box is one scan window.

Existing methods

Existing ways to scan regions are lossy, slow, or capped

Counties drawn as one dot each, with a window that contains some dots whole
Centroid · fast, lossy
Counties shaded by their exact overlap with a window
Buchin et al. · exact, slow
A connected group of whole counties outlined as one cluster
FlexScan · whole regions, capped
The centroid is fast, but each region becomes one dot, and a window sees all of it or nothing. Buchin and colleagues compute exactly how much each window overlaps each region: accurate, but slow, and the window size must be given in advance. FlexScan searches connected groups of whole regions, but it is slow, and its code caps a cluster at 15 regions. What is missing is a fast and accurate way to run point-based scanners on region data.

The method

Replace each region with 20–50 random points

If your data are counts per region, don't shrink each region to a single point. Scatter 20–50 random points inside it, and share the counts among them. Same data, same scan, far more statistical power — and with pyscan, almost no extra time.

For each region, draw k points uniformly at random inside it, with k between 20 and 50. Each point carries 1/k of the region’s cases and people (Geom k): with k = 20, a county with 120 cases and 10,000 people becomes 20 points with 6 cases and 500 people each. The region keeps its spatial extent, and the scan algorithm is untouched: it is a preprocessing step.

One county drawn twice: with a single red centroid dot, then, after an arrow, with 20 purple points spread inside it
120 cases
10,000 people
6 cases each
500 people each
Arkansas counties with one red dot each
Before · 75 centroids
Arkansas counties filled with purple sampled points, 20 per county
After · 20 points per county

How I solved it. Our fix is almost embarrassingly simple: instead of one dot at the center, scatter 20–50 points across the county’s actual shape and share its case count among them. The map gets its geography back, and the fast algorithms never notice the difference. The proof it works: given nothing but county totals of Valley Fever cases, the method redraws California’s San Joaquin Valley endemic region — a region fixed by soil ecology — far more faithfully than the centroid shortcut ever could, while scanning the entire continental-U.S. map — 3,711 region polygons spanning 3,108 counties — in 0.33 seconds, roughly 3,000× faster than FlexScan’s 1,109 s (scan step only; FlexScan additionally runs a significance test the other methods do not).

Speed

At national scale, the whole US scans in 0.33 s

The whole pipeline on the US map: the county borders, 50 points per county, a planted anomaly in red, and the scan trying windows of every size and position until it locks onto the anomaly. The moving windows are an illustration; the time is real. Satellite image: NASA Blue Marble.

Fifty times more points does not mean a much slower scan. pyscan’s time is set by a data-adaptive grid, not by the number of points:

Scan time, one discovery 1 point / region 50 points / region (ours) Buchin et al. FlexScan
USA (3,711 regions) 0.29 s 0.33 s 114 s 1,109 s
Arkansas (75 counties) 0.21 s 0.28 s 25.3 s 8.00 s

Scan step only, one discovery, same machine. The USA map is the county × congressional-district partition (3,711 regions). FlexScan's time includes a Monte Carlo significance test the others do not run; Buchin et al. sweeps nine window sizes.

Results

A synthetic test: plant a hotspot, see which input finds it

Arkansas centroids, orange for cases; the planted region in green and the found window, red dashed, too big and off-centre
Centroid · found window PJD 0.54
Arkansas with 50 points per county; the found purple window sits on the green planted region
50 points · found window PJD 0.08
Arkansas, one typical trial. Inside the planted region (green), each point is a case with probability p = 0.5; outside, with q = 0.2. The difference p − q is the signal strength. On centroids the scan finds a window too big and off-centre; with 50 points it lands on the planted region. The Point Jaccard distance (PJD) measures how much the found and planted windows disagree: 0 is perfect, and below 0.2 is a good match.
Point Jaccard distance against signal strength on Arkansas: the centroid and one-random-point curves stay above the 0.2 line, 5 and 10 points per region cross it at 0.45 and 0.30, and 50 points crosses it at 0.15
More points per region, better detection. Twenty trials at each signal strength on Arkansas; lower is better, and below the grey line is a good match. The centroid never gets there, and neither does one random point per region. With 5 points per region a good match comes at a signal of 0.45, with 10 at 0.30, and with 50 at just 0.15.
Six small charts, for Utah, California, Arkansas, Georgia, New York City and the contiguous US; on each, the 50-points curve drops below the good-match line before the centroid curve
Sampling wins on all six maps, from 29 to 3,108 regions: on each, 50 points per region crosses the good-match line first.
Point Jaccard distance against the planted region's size in Georgia: 50 points per region stays below the good-match line from 4.1% of the state; the centroid only reaches it at 60.8%
Georgia with nested planted rectangles from 4.1% to 60.8% of the state
In Georgia, a planted rectangle grows from 1.7% of the state to 60.8%. At 4.1%, 50 points per region find it and the centroid does not; the centroid succeeds only at 60.8%, the largest size tested. Sampling finds planted regions 15× smaller than centroids can.
Point Jaccard distance against signal strength on Arkansas for the centroid, FlexScan, Buchin et al. and 50 points: FlexScan plateaus near 0.35, Buchin et al. gets just below 0.2, and 50 points goes well below it
Against the region methods on the same Arkansas test. FlexScan's own code caps a cluster at 15 regions; the planted region spans more than 15 counties, so it plateaus near 0.35. Buchin and colleagues' code takes one window size per run and cannot find the size; run with the authors' protocol, nine sizes around the true size, which is its best case, it gets just below the line, but 90× slower than ours. Ours has no cap and no size input, and goes well below the line.
Point Jaccard distance against points sampled per region, for six maps, with a 1 over root k reference line; the US reaches a good match at k=3 and Utah at k=75
The finest maps need the fewest points. At a weak signal (0.15), the first k that reaches a good match is 3 for the US, 7 for New York City, 20 for California, 50 for Arkansas and Georgia, and 75 for Utah. A convergence analysis in the paper explains why: as a map is divided into more regions, fewer sample points per region are needed, and the error roughly follows 1/√k.

Real data

Valley Fever: within 0.036 of the best rectangle

California with 50 sampled points per county; the scan's purple window covers most of the dashed San Joaquin Valley
50 points per county
Distance to the valley against points sampled per county: the centroid at 0.717, 50 points at 0.448, and the best axis-aligned rectangle at 0.412
The ground truth is the eight San Joaquin Valley counties, fixed by the soil, not by the counts. With 50 points per county, the scan's window sits on the valley: its Point Jaccard distance to the truth drops from 0.717 with the centroid to 0.448, within 0.036 of 0.412, the best any axis-aligned rectangle can do, because the valley runs diagonally.

Paper

Sampling for Region-Aggregated Spatial Scan Statistics

Accepted as a full paper at ACM SIGSPATIAL 2026 — 58 of 248 submissions. Joint work with Drew McClelland (Taptap Send), Michael Matheny (Meta), and Jeff M. Phillips.

We recommend this sampling-based conversion as the default way to apply point-based spatial scan statistics to region-aggregated data. The companion repository contains the Python experiments and figure-rendering scripts that reproduce every figure and runtime table in the paper, built on pyscan’s C++ backend.