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 dd, every point closer than dd 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.

What Each Correction Costs as Distance Grows Two curves against distance for a typical county window. The share of points retained under border correction falls from one hundred percent at short distances to about twelve percent at five kilometres, so almost the whole sample is discarded. The median isotropic correction weight rises from one to about 2.4 over the same range, meaning observed pairs are being scaled up substantially and the variance of the estimate grows with them. Neither is free; they fail in different directions. Both corrections degrade with distance, in opposite ways 0% 50% 100% points retained — border correction median isotropic weight (right scale) 2.4× 1.0× 0 2.5 km 5 km distance d The useful range ends where one of these curves becomes unacceptable, and that is a decision

Prerequisites

  • Case points and an explicit study window polygon in a metric CRS
  • python 3.11, geopandas 1.0.1, shapely 2.0.6, numpy 1.26.4, pointpats 2.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:

Three Corrections and What Each Gives Up Three edge corrections compared. Ripley isotropic weighting is nearly unbiased and cheap, and its variance grows as the correction weights grow on a ragged boundary. Translation correction is nearly unbiased and better behaved on irregular windows, at a higher computational cost. Border correction is exactly unbiased and conceptually simple, and discards every point closer to the boundary than the distance being tested. Nothing is free; they differ in what they spend Ripley isotropic nearly unbiased, cheap spends: variance weights grow on ragged edges Translation nearly unbiased, robust spends: compute best on very irregular windows Border exactly unbiased, simple spends: sample size unusable at long distances Which currency you can afford is a property of your window, not of the literature

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:

Correcting the Edge Against Removing It Two approaches to the boundary problem. Statistical correction analyses the county window alone and applies a weighting or discards points, which is always available and always costs either variance or sample size. A buffered extract obtains case points from a ring around the county and analyses the county window against the fuller point set, which removes the edge problem entirely and costs a data-sharing agreement rather than a statistical compromise. Two ways to deal with an edge correct for it always available, always costs something remove it costs a data-sharing agreement, not a compromise Ask for the buffer before designing around its absence

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.