Diagnosing Over-Smoothing in a Disease Map
Smoothing is meant to remove small-number noise, and a smoothed map always looks better than a raw one. That is exactly the problem: a map that has smoothed away a real localised excess looks just as good as one that has not. This guide, part of Bayesian Disease Mapping & Rate Smoothing, gives three diagnostics that distinguish the two.
Problem Context & Constraints
Over-smoothing has no visual signature. Both an appropriately smoothed map and an over-smoothed one show fewer extremes than the raw map, smoother gradients, and no isolated spikes. The difference is only visible against something external: the residuals, a held-out prediction, or a signal you planted yourself.
Three checks cover the ground, and they fail in different ways.
Residual structure. If the smoother is right, the standardised residuals should be spatially unstructured. Leftover spatial autocorrelation in the residuals means the model has not captured the geography; leftover anti-correlation — adjacent residuals of opposite sign — is the specific signature of over-smoothing, because a suppressed peak leaves a positive residual surrounded by negatives.
Predictive score. Leave-one-out predictive density scores the model on data it did not see. An over-smoothed model predicts the held-out area’s count too close to its neighbours’ level, and the score falls.
Planted-signal recovery. Inject a known excess into a known area, refit, and measure how much of it survives. This is the most direct diagnostic and the one most rarely run, and it is what turns “the model may suppress outbreaks” into a number.
Prerequisites
- A fitted smoothing model with posterior draws or, for empirical Bayes, the smoothed estimates and shrinkage weights
- The raw observed and expected counts
- A weights matrix for the residual autocorrelation test
python3.11,numpy1.26.4,pandas2.2.2,libpysal4.12.1,esda2.6.0
The residual signature is directional, and knowing which direction means what is the whole of the first diagnostic:
Step-by-Step Solution
# Three over-smoothing diagnostics: residual structure, LOO score, planted signal.
# Pinned: numpy==1.26.4, pandas==2.2.2, libpysal==4.12.1, esda==2.6.0, scipy==1.13.1
import logging
import numpy as np
import pandas as pd
from esda.moran import Moran
from scipy.stats import poisson
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("bayes.oversmooth")
def residual_structure(observed, expected, fitted_rr, w, seed: int = 42) -> dict:
"""Moran's I on standardised residuals.
Positive I means unmodelled spatial structure remains. Negative I is the
over-smoothing signature: a suppressed peak leaves a positive residual ringed
by negatives, which reads as spatial anti-correlation."""
mu = expected * fitted_rr
resid = (observed - mu) / np.sqrt(np.maximum(mu, 1e-9))
mi = Moran(resid, w, permutations=999, seed=seed)
verdict = ("under-smoothed / unmodelled structure" if mi.I > 0 and mi.p_sim < 0.05
else "over-smoothed" if mi.I < 0 and mi.p_sim < 0.05
else "no residual structure detected")
log.info("residual Moran's I = %.4f (p=%.4f) -> %s", mi.I, mi.p_sim, verdict)
return {"I": float(mi.I), "p": float(mi.p_sim), "verdict": verdict}
def loo_predictive_score(observed, expected, draws) -> float:
"""Mean log pointwise predictive density, computed from posterior draws.
Higher is better. Comparing this across smoothing configurations is the
cheapest defensible way to say one is over-smoothed relative to another."""
lppd = []
for i in range(len(observed)):
mu = expected[i] * draws[:, i]
lppd.append(np.log(np.mean(poisson.pmf(observed[i], mu))))
score = float(np.mean(lppd))
log.info("mean log pointwise predictive density: %.4f", score)
return score
def planted_signal_recovery(observed, expected, target_idx: int, factor: float,
refit, seed: int = 42) -> dict:
"""Inject a `factor`-fold excess into one area and measure what survives.
`refit` takes (observed, expected) and returns fitted relative risks. Run this
in a SPARSE area: recovery in a dense area is always high and tells you nothing
about the case you are worried about."""
y = observed.copy().astype(float)
added = expected[target_idx] * (factor - 1.0)
y[target_idx] += added
rr = refit(y, expected)
baseline = refit(observed, expected)[target_idx]
recovered = (rr[target_idx] - baseline) / (factor - 1.0)
log.info("planted %.1fx in area %d (expected %.1f): recovered %.0f%% of the excess",
factor, target_idx, expected[target_idx], 100 * recovered)
return {"area": int(target_idx), "factor": factor,
"expected": float(expected[target_idx]), "recovered_fraction": float(recovered)}
def recovery_profile(observed, expected, refit, factor=2.0, n=12, seed=42) -> pd.DataFrame:
"""Recovery across the expected-count range, which is the publishable summary."""
rng = np.random.default_rng(seed)
order = np.argsort(expected)
picks = order[np.linspace(0, len(order) - 1, n).astype(int)]
rows = [planted_signal_recovery(observed, expected, int(i), factor, refit) for i in picks]
out = pd.DataFrame(rows).sort_values("expected")
log.info("recovery ranges from %.0f%% (expected %.1f) to %.0f%% (expected %.1f)",
100 * out["recovered_fraction"].iloc[0], out["expected"].iloc[0],
100 * out["recovered_fraction"].iloc[-1], out["expected"].iloc[-1])
return out
The recovery profile is the artefact worth publishing. It converts a vague caveat — “smoothing may suppress local excesses” — into a curve showing exactly how large an excess has to be, in an area of a given size, before the published map would show it.
Validation & Edge Cases
1. Run the residual test both ways. Most implementations check only for positive residual autocorrelation. The negative direction is the one that indicates over-smoothing and it is routinely ignored:
INFO residual Moran's I = -0.0871 (p=0.0140) -> over-smoothed
INFO mean log pointwise predictive density: -2.4118 [fitted BYM2]
INFO mean log pointwise predictive density: -2.6902 [legacy gamma prior]
INFO planted 2.0x in area 41 (expected 3.2): recovered 19% of the excess
INFO recovery ranges from 14% (expected 1.8) to 96% (expected 412.0)
2. Plant the signal in a sparse area, and in several. Recovery in a well-populated area is near-perfect under any configuration. A single test in a randomly chosen area will usually be reassuring and uninformative.
3. Compare predictive scores only between models on identical data. The score is not interpretable in absolute terms; a difference of 0.28 in mean log predictive density between two configurations on the same counts is meaningful, the value itself is not.
4. Distinguish over-smoothing from a missing covariate. Positive residual autocorrelation can mean either. Adding a plausible spatially varying covariate and re-testing separates them: if the residual structure disappears, the model was under-specified rather than under-smoothed.
5. Report the recovery profile with the map, not only in the methods. A reader deciding whether to act on the absence of a hotspot needs to know that an excess below a certain size in an area below a certain population would not have appeared.
6. Run the diagnostics on the published configuration, not a convenient one. It is easy to validate a fast approximation and then publish a fuller model, or to validate on a subset and publish on the whole state. Both leave the actual published map untested, and the difference between configurations is exactly what the diagnostics exist to measure.
7. Repeat the recovery test annually. The population denominators shift, the case counts change scale, and a configuration whose recovery profile was acceptable three years ago may no longer be. Because the test is a simulation on top of an existing fit, re-running it costs a fraction of the original analysis, and the resulting profile is the natural place to notice that a surveillance programme’s effective sensitivity has drifted downward as its areas have grown or its counts have fallen. Where the profile has moved materially, that is worth reporting in its own right rather than silently re-baselining.
The recovery profile is more useful as a curve than as a single number, because it tells a reader exactly which excesses the map could not have shown:
Compliance Notes
- Publish the recovery profile alongside any smoothed surveillance map used for resource allocation. It is the map’s sensitivity, and a map without a stated sensitivity cannot support a decision not to act.
- Record the diagnostic results in the run metadata, including the residual Moran’s I and its sign, so a later reviewer can see they were run.
- Do not tune the smoothing parameter to maximise recovery. Recovery and noise suppression trade off directly, and optimising one produces a map that is unstable in the other direction.
- Treat a negative residual Moran’s I as a blocking finding, not a footnote: it means the published map is suppressing structure that the data contains.
Related Topics
- Bayesian Disease Mapping & Rate Smoothing — the parent guide, including the cost of shrinkage this quantifies.
- Choosing Priors for Spatial Random Effects in Disease Maps — the parameter that most often causes the over-smoothing this detects.
- Space-Time Cluster Detection — the detector to pair with a smoothed map, since scan statistics are not subject to shrinkage.