Choosing a Buffer Radius for Proximity Exposure
This guide, part of Assigning Exposure to Residential Locations, addresses the single most under-justified number in environmental epidemiology: the radius of the circle drawn around a residence to decide whether someone was exposed.
Problem Context & Constraints
Proximity exposure reduces a continuous field to a binary or a count: is there a facility within 500 m, how many road kilometres lie within 1 km. The radius is not a nuisance parameter — it is the exposure definition. Change it from 400 m to 800 m and a different set of people is classified as exposed, the contrast between groups changes, and the effect estimate moves. A radius adopted because a previous paper used it inherits that paper’s source, geography and geocode quality along with the number.
Three distinct considerations set the radius, and conflating them is how indefensible choices happen.
Source physics. How far does the agent actually travel at a concentration that matters? Near-road pollutant gradients flatten within 150–300 m; odour complaints from a facility extend further; a groundwater plume follows the hydraulic gradient and is not circular at all. The radius should encode a mechanism, and where the mechanism is directional, a circle is the wrong shape regardless of size.
Positional error. If residences are geocoded to street segments with 120 m of typical error, a 100 m buffer classifies exposure essentially at random for records near the boundary. The radius cannot be smaller than the geocode uncertainty without the classification becoming noise.
Statistical power. A tight radius produces few exposed people and a wide confidence interval; a loose one produces many exposed people who are not really exposed, diluting the contrast. Both extremes lose power, and the optimum depends on the true gradient, which is what you are trying to estimate.
The constraint that makes this tractable is honesty about multiplicity. Testing eight radii and reporting the one that reached significance is a well-known way to produce a finding that does not replicate.
Prerequisites
python3.11geopandas0.14.4,numpy1.26.4,pandas2.2.2- Geocoded residences with a match type, and the positional error table from the parent guide
- A hazard or source layer with its own positional accuracy documented
- A pre-registered primary radius and, optionally, a declared sensitivity ladder
Step-by-Step Solution
Compute the admissible window from the two hard constraints, choose the primary radius inside it on physical grounds, and evaluate the ladder as a sensitivity analysis whose spread is reported rather than mined.
# Buffer radius selection and a pre-declared sensitivity ladder.
# Pinned: geopandas==0.14.4, numpy==1.26.4, pandas==2.2.2
import logging
import numpy as np
import pandas as pd
import geopandas as gpd
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("exposure.buffer")
def admissible_window(pos_err_m: pd.Series, source_reach_m: float,
quantile: float = 0.9) -> tuple[float, float]:
"""Lower bound from geocode error, upper bound from the source's physical reach."""
lo = float(np.quantile(pos_err_m.dropna(), quantile))
hi = float(source_reach_m)
if lo >= hi:
raise ValueError(
f"no admissible radius: geocode error at q{quantile:.2f} is {lo:.0f} m but "
f"the source reaches only {hi:.0f} m — proximity exposure is not estimable "
"with this address quality")
log.info("admissible radius window: %.0f–%.0f m", lo, hi)
return lo, hi
def proximity_flags(cases: gpd.GeoDataFrame, sources: gpd.GeoDataFrame,
radii_m) -> pd.DataFrame:
"""Exposed/unexposed and source count at each radius, per record."""
if cases.crs != sources.crs or cases.crs.is_geographic:
raise ValueError("both layers must share one projected metric CRS")
cases = cases.sort_values("study_id").reset_index(drop=True)
# One nearest-distance computation serves every radius in the ladder.
nearest = gpd.sjoin_nearest(cases[["study_id", "geometry"]],
sources[["geometry"]],
how="left", distance_col="dist_m")
nearest = nearest.groupby("study_id", as_index=False)["dist_m"].min()
out = nearest.copy()
for r in sorted(radii_m):
out[f"exposed_{int(r)}"] = (out["dist_m"] <= r).astype(int)
n = int(out[f"exposed_{int(r)}"].sum())
log.info("radius %5.0f m: exposed=%d (%.1f%%)", r, n, 100.0 * n / len(out))
return out
def ladder_sensitivity(flags: pd.DataFrame, outcome: np.ndarray,
radii_m, primary_m: float) -> pd.DataFrame:
"""Effect estimate at every declared radius. The PRIMARY one is the result."""
rows = []
for r in sorted(radii_m):
col = f"exposed_{int(r)}"
a = outcome[flags[col].to_numpy() == 1]
b = outcome[flags[col].to_numpy() == 0]
if len(a) < 10 or len(b) < 10:
rows.append({"radius_m": r, "n_exposed": len(a), "estimate": np.nan,
"note": "too few in one arm"})
continue
rows.append({"radius_m": r, "n_exposed": len(a),
"estimate": float(a.mean() - b.mean()),
"note": "PRIMARY" if r == primary_m else "sensitivity"})
df = pd.DataFrame(rows)
spread = df["estimate"].max() - df["estimate"].min()
log.info("ladder spread across %d radii = %.4f (primary at %.0f m)",
len(df), spread, primary_m)
return df
The admissible_window function can refuse. If the ninetieth-percentile positional error exceeds the distance the source’s influence extends, then proximity exposure simply is not estimable with this address data, and building the buffer anyway produces a classification with no relationship to reality. Saying so at the top of the analysis is far better than discovering it in a null result.
The ladder function marks exactly one radius as the primary result. The others are there to show how sensitive the conclusion is to a choice that was necessarily somewhat arbitrary — a spread that is small strengthens the finding, a spread that changes sign undermines it. What the ladder must never become is a menu from which the most favourable row is promoted after the fact.
Validation & Edge Cases
A source layer with its own positional error. Facility coordinates from a permit database are often the mailing address of the operator, not the stack. Add the source’s positional error to the residence’s before setting the lower bound.
Directional exposure. Prevailing wind, river flow and groundwater gradient all make a circle wrong. Where a direction is known, use a wedge or a plume polygon; where it is not, acknowledge that the circular buffer averages exposed and unexposed directions together and attenuates the estimate.
Overlapping sources. With several facilities, a binary “within 500 m of any” discards dose. A count, or an inverse-distance sum, retains more information at no extra data cost — but changes the exposure definition, so it must be declared rather than swapped in.
Edge of the study area. Residences near the boundary have sources outside it that are not in the layer, so their counts are systematic undercounts. Extend the source layer beyond the study boundary by at least the largest radius.
Too few exposed. If the primary radius yields fewer than a couple of dozen exposed cases, the analysis is underpowered whatever the radius. Report that rather than widening the buffer until the count looks comfortable.
INFO exposure.buffer admissible radius window: 120–1500 m
INFO exposure.buffer radius 150 m: exposed=38 (0.7%)
INFO exposure.buffer radius 500 m: exposed=412 (7.6%)
INFO exposure.buffer radius 1500 m: exposed=2871 (52.8%)
INFO exposure.buffer ladder spread across 6 radii = 0.0182 (primary at 500 m)
The edge problem in that list has a characteristic geometry worth drawing, because it produces an undercount that looks like a real low exposure:
Compliance Notes
The radius is a pre-specified analytic choice and must be recorded as one, before the outcome is examined. Store the positional error distribution used for the lower bound with its quantile, the source reach used for the upper bound with the physical reasoning or citation behind it, the primary radius and the date it was fixed, the declared ladder, the exposed counts at each rung, and the ladder spread.
Where a buffer analysis will be published, state the primary radius and its justification in the methods rather than in a supplement. A reader who cannot tell whether the radius was chosen before or after the results cannot evaluate the finding, and the difference between those two situations is the whole credibility of a proximity study.
Related Topics
- Assigning Exposure to Residential Locations — the parent guide and where positional error enters assignment.
- Computing Distance to the Nearest Hazard Site at Scale — the continuous alternative to a binary buffer.
- Measuring Geocoder Match Rate Bias in Surveillance Data — where the positional error distribution comes from.