Stabilizing Catchment Boundaries Across Years

This guide, part of Catchment & Service Area Delineation, addresses what happens after the first delineation: keeping boundaries steady enough that facility rates can be compared from one year to the next.

Problem Context & Constraints

A hospital’s preventable admission rate for 2025 divides its admissions by its service-area population. If the service area gained three ZIP codes between 2024 and 2025, the denominator rose, and the rate may fall even though nothing about care changed. Multiply that across a hundred hospitals re-delineated every year and trend analysis becomes an analysis of boundary changes.

Most year-to-year reassignments are noise. A rural ZIP code with 45 discharges, split 21 to 19 between two hospitals, will flip whenever a handful of patients choose differently. Some reassignments are real: a hospital closes its obstetric unit, a new facility opens, an insurer changes its network. The task is to let real changes through and hold back noise.

Three tools work together. Rolling pooling windows — a three-year window advanced one year at a time — smooth the flow matrix so that one unusual year cannot move an area on its own. Hysteresis — an area changes assignment only if the challenger’s lead exceeds a margin, and the lead persists for more than one window — stops areas that are near a tie from flipping back and forth. Event overrides — forced re-evaluation when a facility opens, closes or merges — make sure the rules do not hold back changes that are known to be real.

Hysteresis Stops Flip-Flopping Two rows showing a contested ZIP code's assignment each year from 2020 to 2025. Without hysteresis, the plurality switches between hospital A and hospital B four times as small patient counts vary. With hysteresis, requiring a challenger to lead by more than ten points for two consecutive windows, the area stays with hospital A until 2024, when hospital B's lead has persisted, and then switches once. One contested area, six annual delineations plurality only A B A B A B with hysteresis A A A A B B 2020 2021 2022 2023 2024 2025 five switches become one — at the point B's lead has persisted challenger must lead by more than 10 points in two consecutive windows

Prerequisites

  • python 3.11
  • pandas 2.2.2, numpy 1.26.4
  • Annual flow matrices of residential area by facility, built as in Delineating Hospital Service Areas From Discharge Flows
  • The previous year’s published catchment file with its basis column
  • A facility event log: openings, closures, mergers and service-line changes, with dates
  • A consistent geography across years, or a crosswalk between vintages

Step-by-Step Solution

Compute the current window’s flows, compare each area’s current assignment with its strongest challenger, change only when the lead passes the margin for the required number of windows or when a facility event forces re-evaluation, and emit a change report.

# Stable year-over-year catchment updates with hysteresis and event overrides.
# Pinned: pandas==2.2.2, numpy==1.26.4
import logging

import numpy as np
import pandas as pd

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("stable_catchments")

WINDOW = 3            # years pooled per delineation
MARGIN = 0.10         # challenger must lead the incumbent by this share
PERSIST = 2           # ...in this many consecutive windows


def window_flows(annual: dict[int, pd.DataFrame], end_year: int) -> pd.DataFrame:
    years = range(end_year - WINDOW + 1, end_year + 1)
    return sum(annual[y] for y in years).fillna(0)


def challenger_lead(flows: pd.DataFrame, incumbent: pd.Series) -> pd.DataFrame:
    shares = flows.div(flows.sum(axis=1), axis=0)
    inc_share = pd.Series([shares.at[a, h] if h in shares.columns else 0.0
                           for a, h in incumbent.reindex(shares.index).items()], index=shares.index)
    top = shares.idxmax(axis=1)
    return pd.DataFrame({"incumbent": incumbent.reindex(shares.index), "challenger": top,
                         "lead": shares.max(axis=1) - inc_share})


def update(prev: pd.DataFrame, flows: pd.DataFrame, events: pd.DataFrame, year: int) -> pd.DataFrame:
    """prev: index area; columns hospital, pending_challenger, pending_count."""
    c = challenger_lead(flows, prev["hospital"])
    evented = set(events.loc[events["year"] == year, "facility_id"])
    rows = {}
    for area, r in c.iterrows():
        old = prev.loc[area] if area in prev.index else None
        hospital = old["hospital"] if old is not None else r["challenger"]
        pend = old["pending_challenger"] if old is not None else None
        count = int(old["pending_count"]) if old is not None else 0
        reason = "new_area" if old is None else ""

        if hospital in evented or r["challenger"] in evented:
            # Facility events force re-evaluation without hysteresis.
            if r["challenger"] != hospital:
                hospital, reason = r["challenger"], "facility_event"
            pend, count = None, 0
        elif r["challenger"] != hospital and r["lead"] > MARGIN:
            count = count + 1 if pend == r["challenger"] else 1
            pend = r["challenger"]
            if count >= PERSIST:
                hospital, reason, pend, count = r["challenger"], "persistent_lead", None, 0
        else:
            pend, count = None, 0
        rows[area] = {"hospital": hospital, "pending_challenger": pend,
                      "pending_count": count, "change_reason": reason}

    out = pd.DataFrame.from_dict(rows, orient="index")
    log.info("year=%d areas=%d persistent_switches=%d event_switches=%d pending=%d",
             year, len(out), int((out.change_reason == "persistent_lead").sum()),
             int((out.change_reason == "facility_event").sum()),
             int((out.pending_count > 0).sum()))
    return out

Pending challengers are the useful middle state. An area whose challenger has led once is not yet reassigned, but it is flagged, and planners know it may move next year. Publishing the pending list with each release gives hospitals notice before a boundary change affects their rates. It also gives analysts a way to test sensitivity: recomputing facility rates as if every pending area had already moved shows the largest boundary effect the next release could bring.

Facility events bypass hysteresis because the reason for waiting — uncertainty about whether a change is real — does not apply. When a hospital closes, its areas must move this year; when one opens, its early flows are an understatement, so re-evaluate in the year it opens and again two years later.

Fewer Changes, and the Ones That Remain Are Real Paired bars for five update years show the number of areas reassigned. Plurality-only updates reassign between seventy and ninety areas each year. Stabilised updates reassign between eight and fifteen, except in 2023, when a hospital closure forces thirty-four areas to move. The stabilised series shows spikes only where facility events occurred, so each spike has an explanation. Areas reassigned per year 2021 2022 2023 closure 2024 2025 plurality only stabilised every spike in the stabilised series has a facility event behind it

Validation & Edge Cases

Backtesting the rules. Run the stabilised procedure over past years and compare against plurality-only delineation. Check that areas that moved under plurality-only and stayed under the rules were genuinely contested — near-ties in the flows — and that real shifts, such as those after a known closure, were let through within one or two windows.

Margins that are too wide. A margin of 25 points will hold back genuine changes for years. Set the margin from observed noise: compute the year-to-year standard deviation of the top share in areas with no facility events, and set the margin at about twice that.

Geography changes. A new census vintage changes ZCTA boundaries, which is a forced re-delineation of its own. Crosswalk flows to the new geography, re-delineate from scratch with the rolling window, and publish a bridge showing how old areas map to new ones.

Mergers. When two hospitals merge under one identifier, areas split between them become firm assignments to the merged entity. Treat a merger as a facility event for both identifiers, and backfill the lineage table so past windows are recomputed on the merged identifier for comparison.

Trend analysis across a boundary change. Where a change is unavoidable, compute rates for the change year on both the old and new catchment, and report both. Analysts can then separate the effect of the boundary from the change in the outcome.

Small-count areas that fall back to travel time. Areas assigned by travel time because they have too few patients should not flip between travel-time and flow-based assignment as their counts hover around the minimum. Apply the same persistence rule to the switch between bases: an area moves to a flow-based assignment only after it has cleared the minimum count in two consecutive windows.

Pending lists that never resolve. Some areas keep a challenger that leads once, falls back, then leads again, resetting the counter every time. Log areas that have entered the pending state three or more times in five years; they are structurally shared and are better labelled as shared than held with one hospital indefinitely.

Service-line catchments on a different cycle. Obstetric or behavioural health catchments built from much smaller flows are noisier than general ones. Use a longer window or a wider margin for them rather than the general settings, and document the difference so that no one compares the two sets of boundaries as if they were built the same way.

INFO stable_catchments year=2025 areas=1068 persistent_switches=9 event_switches=0 pending=23
INFO stable_catchments bridge: 2020->2030 ZCTA crosswalk not yet required
The Annual Change Report Three boxes describe what each annual release includes alongside the catchment file. Changed areas lists each reassigned area with old and new facility and the reason, either persistent lead or facility event. Pending areas lists areas whose challenger has led once and may change next year. Denominator impact shows, per facility, the change in catchment population caused by reassignments, so rate users can separate boundary effects from outcome changes. Published with every catchment release Changed areas old and new facility reason for change Pending areas challenger has led once may change next year Denominator impact population change per facility lets rate users separate boundary effects from real change

Compliance Notes

Record the window length, margin and persistence settings, the facility event log used, each year’s change report, pending list and denominator impact table, and the version lineage linking every catchment file to its predecessor.

Hospitals subject to community health needs assessment requirements rely on these boundaries, and a change can alter which populations a hospital is expected to serve. Give affected facilities advance notice through the pending list, provide a route to query an assignment, and keep a record of queries and outcomes alongside the release.