Edge Correction Choices for County Boundaries
Point-pattern estimators assume the study window contains everything relevant, and a county boundary violates that assumption in a specific, quantifiable way: cases just outside it exist and were never observed. This guide, part of K-Function & Point Pattern Analysis, covers choosing an edge correction for an irregular administrative window and testing the choice.
Problem Context & Constraints
Three corrections are in common use and they trade bias against variance differently.
Ripley’s isotropic correction weights each observed pair by the reciprocal of the proportion of the circle through them that lies inside the window. It is nearly unbiased, it is the default in most software, and its variance grows as the correction weights grow — which happens fast for a ragged boundary at long distances.
Translation correction weights by the area of overlap between the window and a copy of itself translated by the pair’s vector. It is also nearly unbiased and is generally better behaved than isotropic on very irregular windows, at higher computational cost.
Border (reduced-sample) correction simply discards, at each distance , every point closer than to the boundary. It is conceptually the simplest and unarguably unbiased, and it throws away data at a rate that becomes severe at long distances on a compact county.
The constraint that decides between them is the shape of the window relative to the distances being tested. On a compact county tested to a distance small relative to its width, all three agree closely and the choice barely matters. On a long, narrow or ragged county tested to a distance comparable to its width — which is the common case for vector-borne and environmental work — they diverge sharply and border correction may retain almost no points.
Prerequisites
- Case points and an explicit study window polygon in a metric CRS
python3.11,geopandas1.0.1,shapely2.0.6,numpy1.26.4,pointpats2.5.0- A stated maximum distance for the analysis, ideally no more than a quarter of the window’s shorter dimension
The three corrections make different trades, and stating them side by side makes the decision rule below readable:
Step-by-Step Solution
# Compare edge corrections on the actual study window.
# Pinned: geopandas==1.0.1, shapely==2.0.6, 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("kfunc.edge")
def border_retention(points: gpd.GeoDataFrame, window, distances) -> pd.DataFrame:
"""Share of points surviving border correction at each distance.
Computing this BEFORE choosing a correction is the whole point: if retention
at the maximum distance is under about a third, border correction is not a
viable option on this window and saying so is a result."""
inner = {d: window.buffer(-d) for d in distances}
rows = []
for d in distances:
keep = points.geometry.within(inner[d]).sum()
rows.append({"d": d, "retained": int(keep), "share": keep / len(points)})
out = pd.DataFrame(rows)
log.info("border retention: %.0f%% at d=%g, %.0f%% at d=%g",
100 * out["share"].iloc[0], out["d"].iloc[0],
100 * out["share"].iloc[-1], out["d"].iloc[-1])
return out
def isotropic_weight_profile(points: gpd.GeoDataFrame, window, distances,
sample: int = 400, seed: int = 42) -> pd.DataFrame:
"""Median Ripley correction weight at each distance, on a sample of points.
The weight is 1 / (fraction of the circle of radius d inside the window). A
rising median weight means the estimator is extrapolating from fewer and
fewer genuinely observed pairs."""
rng = np.random.default_rng(seed)
idx = rng.choice(len(points), size=min(sample, len(points)), replace=False)
pts = points.geometry.iloc[idx]
rows = []
for d in distances:
fracs = []
for p in pts:
circ = p.buffer(d).exterior
inside = circ.intersection(window)
fracs.append(max(inside.length / circ.length, 1e-6))
w = 1.0 / np.array(fracs)
rows.append({"d": d, "median_weight": float(np.median(w)),
"p95_weight": float(np.percentile(w, 95))})
out = pd.DataFrame(rows)
log.info("isotropic weight: median %.2f at d=%g rising to %.2f at d=%g",
out["median_weight"].iloc[0], out["d"].iloc[0],
out["median_weight"].iloc[-1], out["d"].iloc[-1])
return out
def recommend_correction(retention: pd.DataFrame, weights: pd.DataFrame,
min_retention: float = 0.35,
max_median_weight: float = 2.0) -> dict:
"""A decision rule, applied to this window rather than to convention."""
ret = float(retention["share"].iloc[-1])
wt = float(weights["median_weight"].iloc[-1])
if ret >= min_retention:
choice, why = "border", f"retention {ret:.0%} is adequate and the estimator is exactly unbiased"
elif wt <= max_median_weight:
choice, why = "isotropic", f"retention {ret:.0%} too low, but weights stay modest ({wt:.2f})"
else:
choice, why = "translation", (f"retention {ret:.0%} and isotropic weights {wt:.2f} "
"both poor; the window is too ragged for either")
log.info("recommended: %s (%s)", choice, why)
return {"correction": choice, "reason": why, "retention": ret, "median_weight": wt}
The recommendation function encodes a decision rule rather than a preference, and the thresholds in it are arguable. What is not arguable is that the decision should be made from the window at hand rather than from whichever correction the software defaults to.
Validation & Edge Cases
1. Cut the maximum distance rather than fighting the correction. If neither correction is comfortable at 5 km, the honest response is usually that this window does not support inference at 5 km. Reporting to 2 km with a clean correction beats reporting to 5 km with a heavily corrected one:
INFO border retention: 98% at d=250, 12% at d=5000
INFO isotropic weight: median 1.02 at d=250 rising to 2.41 at d=5000
INFO recommended: translation (retention 12% and isotropic weights 2.41 both poor; the window is too ragged for either)
2. Use the real window, not its bounding box or convex hull. A county’s window is its boundary, and substituting a simpler shape systematically understates the edge problem — which is precisely why it is tempting.
3. Exclude uninhabitable area from the window where it is substantial. A county that is one-third lake has an effective window much smaller than its administrative boundary, and treating the lake as study area both dilutes the intensity estimate and misstates the edge geometry.
4. Report the correction and the maximum distance together. They are a pair: neither is interpretable alone, and a curve plotted beyond the distance the correction supports invites exactly the over-reading described in the parent guide.
5. Consider a buffered data extract instead. Where cases from neighbouring counties can be obtained, the cleanest solution to the edge problem is to remove it: analyse the target county’s window using points from a buffered region around it. This requires a data-sharing arrangement rather than a statistical correction, and it is usually worth pursuing.
6. Treat internal holes like external boundary. A large uninhabited lake or military reservation inside the county is a hole in the window, and the same edge logic applies to its perimeter. Most implementations handle it correctly if the hole is present in the polygon, and silently ignore it if the window was simplified to its outer ring — which is the more common failure, because outer-ring simplification is a routine cleaning step.
7. Report the correction alongside the envelope construction. The simulation envelope that a K-function curve is judged against must be built with the same window and the same correction as the observed curve. Mixing them — an observed curve with isotropic correction against an envelope simulated without — produces an apparent departure from randomness that is entirely an artefact of the mismatch, and it is a mistake that is invisible in the plot.
8. Prefer a single correction across a multi-county study. Where several counties are analysed in one report, using the best correction for each makes their curves incomparable. Choose the correction that is adequate for the most awkward window and apply it throughout, noting where it was more conservative than necessary.
The buffered extract removes the problem instead of correcting for it, and the difference in what it costs is administrative rather than statistical:
Compliance Notes
- Record the window geometry used, including any exclusion of water or uninhabitable land, since the correction and the intensity estimate both depend on it.
- Log the retention and weight profiles with the analysis; they are the justification for the correction and the maximum distance.
- State the correction in every figure caption, because two K-function curves computed with different corrections are not comparable.
- Note where a buffered extract was used, since the analysis then depends on a data-sharing agreement that a later reader may not have.
Related Topics
- K-Function & Point Pattern Analysis — the parent guide, including the estimator this correction enters.
- Implementing Ripley’s K-Function in Python for Vector-Borne Diseases — the worked implementation, and what an uncorrected curve looks like.
- Geocoding Quality & Address Standardization — the upstream step whose centroid matches distort short-distance estimates regardless of edge correction.