Kernel Density & Spatial Relative Risk Surfaces
This guide is part of the Disease Clustering & Spatial Statistical Modeling section. It covers continuous surfaces estimated from point data — the method to use when cases are geocoded to addresses and the question is where risk is elevated, without first forcing the points into administrative areas.
Concept & Epidemiological Alignment
A kernel density estimate places a small smooth bump on every case point and sums them, producing a continuous surface of case intensity. It answers where cases are concentrated, and on its own it is almost useless for epidemiology, because cases are concentrated where people are. A density map of any disease in a state looks like a population map of that state.
The epidemiologically meaningful quantity is the ratio of two densities: the density of cases divided by the density of the population at risk. Where the population at risk is represented by controls — a random sample of the population, or cases of an unrelated condition — the log of that ratio is the spatial log relative risk surface, , where is the case density and the control density. It is zero where cases are distributed like controls and positive where cases are over-represented.
This approach sits between the areal methods and the point-pattern methods elsewhere in the section. Unlike Global & Local Moran’s I Implementation, it needs no areal units and so avoids the modifiable areal unit problem. Unlike K-Function & Point Pattern Analysis, which asks whether clustering exists at some scale, it asks where — and produces a map a program can act on.
Its assumptions are specific. The controls must represent the population at risk, so their spatial distribution matches the at-risk population; a control series drawn from one hospital’s catchment does not. The bandwidth must suit the scale of the effect; the ratio is exquisitely sensitive to it. And the surface near the study-area boundary is biased unless edge-corrected, because the kernel’s mass partly falls outside the region.
Surveillance use differs from research use in one important respect. A research study chooses its controls once and analyses a fixed dataset; a surveillance program re-estimates the surface as cases accrue, often weekly. That makes the control series a standing asset rather than a one-off sample — it must be refreshed as the population changes, geocoded with the same pipeline as incoming cases, and versioned so that a surface from March can be reproduced in December. Programs that treat the control set as disposable end up comparing surfaces built against different denominators and reading the difference as a change in risk.
Method Choices
| Choice | Options | Consequence |
|---|---|---|
| Kernel | Gaussian, quartic | Shape matters little; bandwidth matters a lot |
| Bandwidth | Fixed, adaptive, jointly optimal | Fixed over-smooths cities or under-smooths countryside |
| Denominator | Controls, population raster | Controls allow significance testing; rasters give a rate-like surface |
| Edge correction | None, uniform, Jones–Diggle | Uncorrected surfaces dip at the boundary |
| Significance | Asymptotic, Monte Carlo tolerance contours | Monte Carlo is more robust with few cases |
Spatial Data Prerequisites
- Geometry: case and control points as geocoded coordinates with match quality; a study-region polygon for edge correction.
- Coordinate reference system: a projected metric system, since bandwidths are distances.
- Sample size: the ratio is unstable with fewer than about fifty cases; below that, prefer an areal method or a global test.
- Control design: controls sampled from the same population and period as cases, with the same geocoding process, so geocoding error affects both alike.
- Positional accuracy: a bandwidth smaller than the geocode positional error resolves noise, not risk.
Production Implementation
The routine below computes case and control kernel densities on a grid with a shared bandwidth, applies a uniform edge correction, forms the log relative risk, and builds Monte Carlo tolerance contours by random relabelling.
# Spatial log relative risk with edge correction and Monte Carlo tolerance.
# Pinned: numpy==1.26.4, scipy==1.13.1, geopandas==0.14.4, shapely==2.0.4
import logging
import numpy as np
import geopandas as gpd
from scipy.spatial import cKDTree
from shapely.geometry import Point
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("kde.rr")
SEED = 20260918
def grid_over(region: gpd.GeoDataFrame, step: float) -> np.ndarray:
minx, miny, maxx, maxy = region.total_bounds
xs = np.arange(minx + step / 2, maxx, step)
ys = np.arange(miny + step / 2, maxy, step)
xx, yy = np.meshgrid(xs, ys)
g = np.c_[xx.ravel(), yy.ravel()]
poly = region.union_all()
inside = np.array([poly.contains(Point(p)) for p in g])
return g[inside]
def gaussian_kde(points: np.ndarray, grid: np.ndarray, h: float) -> np.ndarray:
"""Normalised Gaussian kernel density; neighbours beyond 4h ignored for speed."""
tree = cKDTree(points)
dens = np.zeros(len(grid))
for i, idx in enumerate(tree.query_ball_point(grid, 4 * h)):
if idx:
d2 = ((points[idx] - grid[i]) ** 2).sum(1)
dens[i] = np.exp(-d2 / (2 * h * h)).sum()
return dens / (len(points) * 2 * np.pi * h * h)
def edge_weights(grid: np.ndarray, region, h: float, n_mc: int = 64, seed: int = SEED):
"""Share of each grid point's kernel mass inside the region (uniform correction)."""
rng = np.random.default_rng(seed)
offs = rng.normal(0, h, size=(n_mc, 2))
poly = region.union_all()
w = np.array([np.mean([poly.contains(Point(p + o)) for o in offs]) for p in grid])
return np.clip(w, 0.05, 1.0)
def log_relative_risk(cases: np.ndarray, controls: np.ndarray, grid: np.ndarray,
region, h: float, n_sim: int = 199, seed: int = SEED) -> dict:
ew = edge_weights(grid, region, h)
f = gaussian_kde(cases, grid, h) / ew
g = gaussian_kde(controls, grid, h) / ew
floor = 1e-12
rho = np.log(np.maximum(f, floor)) - np.log(np.maximum(g, floor))
# Monte Carlo: relabel pooled points at random, recompute, rank the observed.
rng = np.random.default_rng(seed)
pooled = np.vstack([cases, controls])
n1 = len(cases)
exceed = np.zeros(len(grid))
for _ in range(n_sim):
perm = rng.permutation(len(pooled))
fs = gaussian_kde(pooled[perm[:n1]], grid, h) / ew
gs = gaussian_kde(pooled[perm[n1:]], grid, h) / ew
rs = np.log(np.maximum(fs, floor)) - np.log(np.maximum(gs, floor))
exceed += rs >= rho
p_upper = (exceed + 1) / (n_sim + 1)
log.info("relative risk: %d grid cells, bandwidth %.0f m, %d cells with p<0.05",
len(grid), h, int((p_upper < 0.05).sum()))
return {"rho": rho, "p_upper": p_upper, "edge_weight": ew}
Using one bandwidth for both densities is a deliberate choice: different bandwidths for cases and controls make the ratio reflect the difference in smoothing rather than the difference in distribution. The Monte Carlo relabelling tests the null hypothesis that cases and controls come from the same spatial distribution, and the resulting cell-wise p-values define tolerance contours — the regions where the surface is significantly elevated — which are what a map should outline.
The edge correction divides each density by the fraction of the kernel’s mass that falls inside the region. Without it both densities dip near the boundary; the dips partly cancel in the ratio, but not exactly, and the uncorrected surface shows spurious gradients along coastlines and state lines.
Parameter Selection & Tuning
Bandwidth is the dominant choice. Data-driven selectors that jointly minimise the error of the ratio exist and are the defensible default; the details and their failure modes are in Choosing a Kernel Bandwidth for Case Density Maps.
Fixed versus adaptive. A fixed bandwidth suits a study area of roughly uniform density; where density varies strongly, an adaptive bandwidth that shrinks in cities and widens in the countryside is better, as covered in Adaptive Bandwidth KDE for Uneven Population Density.
Grid resolution should be a fraction of the bandwidth — a quarter or less — so the surface is sampled finely enough to draw smooth contours. Finer adds computation without information.
Simulation count for tolerance contours: 199 is a practical minimum for a 0.05 threshold; 999 gives more stable contours and is worth it for a published map.
Control ratio. Two to four controls per case is usually adequate; beyond that the control density is already well estimated and more controls add little.
Kernel shape. The Gaussian and quartic kernels give nearly identical ratios at matched effective bandwidths; the practical difference is that the quartic kernel has finite support, so each grid cell depends only on nearby points, which makes computation faster and keeps a distant outlier from contributing. Choose on computational grounds and match bandwidths when comparing with published work that used the other shape — a quartic bandwidth corresponds to a Gaussian bandwidth roughly 2.5 times smaller.
Temporal window. A surface pooled over five years mixes periods in which risk may have moved. For surveillance, estimate on a rolling window long enough to accumulate the minimum case count and short enough to reflect current risk, and state the window alongside the bandwidth, since both control the effective resolution of the map.
Edge Cases & Failure Modes
Controls that do not represent the population. A control series from a single clinic or a convenience sample reproduces that source’s catchment, and the ratio then maps the difference between case and control catchments. Validate the control distribution against census population before use.
Differential geocoding. If cases geocode at rooftop level and controls at street level, the smaller-scale structure of the two densities differs for a reason unrelated to risk. Use the same geocoder and the same quality filter for both.
Too few cases. With twenty cases the relative risk surface is dominated by the kernel shape around each point. Report a global test instead of a map.
Multiple testing across the grid. Cell-wise p-values are not independent and a 0.05 threshold over thousands of cells will outline some noise. Tolerance contours are exploratory; the significance procedure is covered in Testing Significance of a Relative Risk Surface.
Clustered controls. Controls sampled by household or by clinic arrive in clumps, and the control density then has spikes that the case density lacks, producing apparent troughs of risk around each clump. Sample controls independently at the individual level, or thin clumped controls to one per location before estimation.
Changing boundaries of the study region. Edge correction depends on the region polygon. A surface recomputed after the region’s boundary is edited — a county added, a coastline refined — changes near the edges for that reason alone. Hold the region polygon fixed per vintage and record it with the surface.
Disclosure. A kernel surface at a small bandwidth reveals the locations of individual cases — each spike is a person. Publication bandwidths and output resolution must satisfy disclosure review regardless of what the analysis used internally.
Compliance & Audit Controls
Record the case and control definitions and sampling, the geocoder and quality filter applied to both, the kernel, the bandwidth and how it was selected, the edge-correction method, the grid resolution, the simulation count and seed, and the significance threshold for tolerance contours.
Kernel surfaces are among the most disclosive outputs in spatial epidemiology because they are derived directly from point locations. Treat the internal analytic surface as protected data. The published surface should use a bandwidth and grid resolution approved under Privacy-Preserving Spatial Analytics and should never be released alongside the point count per cell.
Production Implementation Checklist
Frequently Asked Questions
Why not just map case density? Because case density mostly reproduces population density. A cluster of cases in a city centre is expected; what matters is whether cases are over-represented relative to the population there, which only the ratio of case to control densities shows.
Can I use a population raster instead of controls? Yes, which gives a rate-like surface. You lose the relabelling test, since there are no control points to permute, and must use a different significance approach such as simulating cases from the population surface under the null.
What if cases and controls are geocoded differently? Fix it before analysis. Differential positional error changes the fine-scale structure of one density and not the other, producing spurious relative risk at scales below the error.
How do I report a relative risk surface? Map the log relative risk with tolerance contours outlining significant areas, state the bandwidth and edge correction, and describe the contours as exploratory unless a global test has confirmed a departure from the null.
Is a kernel surface safe to publish? Only at a bandwidth and resolution approved by disclosure review. At small bandwidths each case appears as a distinct peak at its address.
Related Topics
- Disease Clustering & Spatial Statistical Modeling — the parent section and the areal and point alternatives.
- Estimating a Spatial Relative Risk Surface — an end-to-end case-control run.
- Choosing a Kernel Bandwidth for Case Density Maps — the decision that dominates the result.
- Using the Cross-K Function for Case-Control Data — testing whether clustering exists before mapping where.