Comparing 2SFCA Variants for Provider Supply

This guide, part of Spatial Equity Index Calculation for Public Health Infrastructure, puts the main two-step floating catchment area variants side by side on the same data.

Problem Context & Constraints

The two-step floating catchment area (2SFCA) method measures access as a provider-to-population ratio that respects travel. In step one, each provider’s supply is divided by the population within its catchment, giving a supply ratio per provider. In step two, each residential area sums the ratios of all providers within its own catchment. The result reads like providers per capita, but it counts only providers people can reach and shares each provider among everyone who can reach it.

The basic form has well-known weaknesses, and a family of variants has grown to address them. Basic 2SFCA treats everyone inside the catchment equally and everyone outside as having no access — a provider 29 minutes away counts fully and one 31 minutes away not at all. Enhanced 2SFCA (E2SFCA) divides the catchment into travel-time zones with declining weights, so nearer providers count more. Kernel density 2SFCA (KD2SFCA) replaces zones with a continuous distance-decay function. Three-step FCA (3SFCA) adds a competition step: a population near several providers is split among them according to selection weights, rather than counted in full by each, which reduces the demand inflation that makes basic 2SFCA understate access in dense areas.

These are not just refinements. On the same data they can rank areas differently, and when a score threshold defines a shortage area, the choice of variant decides which communities qualify.

How Each Variant Weights Distance A chart of weight against travel time from zero to forty-five minutes. Basic 2SFCA is a step: weight one up to the thirty-minute threshold and zero after. Enhanced 2SFCA is a staircase with weights one, zero point six eight and zero point two two in zones of ten, twenty and thirty minutes. Kernel 2SFCA is a smooth curve declining from one at zero to zero at the threshold. The three shapes treat providers between ten and thirty minutes very differently. Same threshold, three weighting shapes travel time (minutes) weight basic (step) enhanced (zones) kernel (smooth) 0 10 20 30 a provider at 25 minutes counts fully, at 22%, or at about 9%, depending on variant

Prerequisites

Step-by-Step Solution

Implement all variants against one matrix with a shared weighting interface, compute scores, and compare them by correlation, rank change and the areas each would designate as short of supply.

# Basic, enhanced, kernel 2SFCA and 3SFCA on a shared travel time matrix.
# Pinned: numpy==1.26.4, pandas==2.2.2
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("fca")

D0 = 30.0   # catchment threshold, minutes


def w_basic(t):     return (t <= D0).astype(float)
def w_enhanced(t):  return np.select([t <= 10, t <= 20, t <= D0], [1.0, 0.68, 0.22], 0.0)
def w_kernel(t):    return np.where(t <= D0, (1 - (t / D0) ** 2) ** 2, 0.0)   # quartic


def two_step(T: np.ndarray, supply: np.ndarray, pop: np.ndarray, wfun) -> np.ndarray:
    """T: minutes, shape (n_demand, n_providers)."""
    W = wfun(T)
    demand_at_j = (W * pop[:, None]).sum(axis=0)
    R = np.divide(supply, demand_at_j, out=np.zeros_like(supply, dtype=float), where=demand_at_j > 0)
    return (W * R[None, :]).sum(axis=1)


def three_step(T, supply, pop, wfun) -> np.ndarray:
    """Adds selection weights: each demand area's population is shared among
    providers in proportion to their distance weights."""
    W = wfun(T)
    rowsum = W.sum(axis=1, keepdims=True)
    G = np.divide(W, rowsum, out=np.zeros_like(W), where=rowsum > 0)
    demand_at_j = (G * W * pop[:, None]).sum(axis=0)
    R = np.divide(supply, demand_at_j, out=np.zeros_like(supply, dtype=float), where=demand_at_j > 0)
    return (G * W * R[None, :]).sum(axis=1)


def compare(T: pd.DataFrame, supply: pd.Series, pop: pd.Series, shortage_ratio: float) -> pd.DataFrame:
    Tm, s, p = T.to_numpy(float), supply.reindex(T.columns).to_numpy(float), pop.reindex(T.index).to_numpy(float)
    scores = pd.DataFrame({
        "basic": two_step(Tm, s, p, w_basic),
        "enhanced": two_step(Tm, s, p, w_enhanced),
        "kernel": two_step(Tm, s, p, w_kernel),
        "three_step": three_step(Tm, s, p, w_enhanced),
    }, index=T.index) * 100_000        # providers per 100,000

    ranks = scores.rank(pct=True)
    short = scores < shortage_ratio
    log.info("spearman vs basic: %s", ranks.corr(method="spearman")["basic"].round(3).to_dict())
    log.info("shortage areas: %s", short.sum().to_dict())
    log.info("areas short under some but not all variants: %d",
             int((short.any(axis=1) & ~short.all(axis=1)).sum()))
    return scores.join(short.add_suffix("_short"))

Scaling all scores to providers per 100,000 puts them on the familiar scale of shortage designations. The totals are not equal across variants, however: E2SFCA and KD2SFCA shrink the effective demand at each provider because distant population carries lower weight, and 3SFCA shrinks it further through selection weights. Compare ranks and designations, not raw levels.

The count of areas “short under some but not all variants” is the most important number in the log. Those are the communities whose designation depends on a methodological choice, and they deserve the most scrutiny before any funding decision is based on the result. Mapping them usually shows a pattern: they sit at the edges of the robust shortage core, or in areas with a single provider just inside or just outside the threshold, where the step function and the smooth decay disagree most.

Which Areas Every Variant Agrees On A bar divides the tracts identified as short of primary care by at least one variant. Sixty-one tracts are short under all four variants, forming a robust core. Twenty-seven are short under two or three variants. Nineteen are short under only one, most often the basic variant, which counts providers just inside the threshold fully. The contested tracts are where the variant choice decides the designation. 107 tracts short under at least one variant 61 short under all four 27 under 2–3 19 under 1 robust core designation depends on method review the 46 contested tracts before any designation is filed

Validation & Edge Cases

Threshold sensitivity outweighs variant choice. Moving the catchment from 30 to 45 minutes often changes scores more than switching variants. Run each variant at two or three thresholds and report the stability of designations across both dimensions.

Border effects. Providers just outside the study area serve residents inside it. Excluding them understates access along borders, a problem that affects every variant equally. Include providers within the threshold beyond the boundary, and population beyond the boundary within the threshold of included providers.

Provider supply measures. Headcounts overstate supply where physicians work part-time or split between sites. Use full-time equivalents, and for multi-site providers allocate their time across locations. Specialists who also do primary care need an explicit rule.

3SFCA and double counting. 3SFCA was designed to fix demand inflation, but its selection weights assume people choose among providers by distance alone. Where insurance networks restrict choice — Medicaid acceptance, for example — use supply measured as capacity available to the population in question, not total supply.

Enhanced zone weights borrowed without thought. The weights 1.00, 0.68 and 0.22 used above come from a Gaussian decay fitted in the original enhanced 2SFCA work, for one region and one travel mode. They are a reasonable default, not a law. Where patient origin data are available, fit the decay from observed travel to providers — the share of visits by travel-time band — and use weights that describe how far people in this region actually travel. Record whichever weights are used, since two analyses with different zone weights are not comparable even if both call themselves enhanced 2SFCA.

Zero-population areas. Parks, industrial zones and water tracts have zero population and a defined but meaningless score. Exclude them from maps and designation counts.

Mode. The matrix mode shapes every variant. Areas with low vehicle availability should be scored on transit times, or on a mixed matrix, or the scores will describe access for residents with cars.

Comparing across years. When comparing scores over time, hold the variant, threshold and decay parameters fixed, and rebuild the travel matrix only if the network has materially changed. A switch from basic to enhanced between years will look like a change in access in every tract, and no reader of a trend chart will be able to tell it apart from a real one.

INFO fca spearman vs basic: {'basic': 1.0, 'enhanced': 0.912, 'kernel': 0.897, 'three_step': 0.861}
INFO fca shortage areas: {'basic': 83, 'enhanced': 74, 'kernel': 71, 'three_step': 79}
INFO fca areas short under some but not all variants: 46
Choosing a Variant by Purpose Three boxes. For regulatory or grant submissions that specify a method, use exactly the specified variant and threshold. For equity reporting and planning, use the enhanced or kernel variant, which avoid the sharp threshold edge. For dense urban areas with many competing providers, use the three-step variant to reduce demand inflation. In every case, report the areas whose designation changes under other variants. Pick by purpose, report the sensitivity Regulatory filing method specified by the program use it exactly Equity and planning avoid the sharp threshold edge enhanced or kernel Dense urban supply many competing providers nearby three-step in every case, list the areas whose designation changes under other variants

Compliance Notes

Record the variant, threshold, zone weights or kernel, provider supply source and date, full-time-equivalent rules, population source, travel matrix mode and date, border handling, and the cross-variant designation table.

When scores support a shortage designation or funding allocation, the filing must use the method the program specifies. Supporting analysis showing sensitivity to other variants strengthens a filing for a robust-core area and gives an honest warning for contested ones. Provider rosters can include personal data such as physician names and practice addresses; publish supply only in aggregate by location or area.