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.
Prerequisites
python3.11numpy1.26.4,pandas2.2.2- Provider supply by location — full-time-equivalent primary care physicians, or clinic appointment capacity
- Population by demand area, preferably the population in need of the service
- A travel time matrix from demand areas to provider locations, with times up to at least the largest threshold
- The basic method from Calculating the Two-Step Floating Catchment Area Index and decay choices from Choosing Distance Decay Functions for 2SFCA
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.
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
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.
Related Topics
- Spatial Equity Index Calculation for Public Health Infrastructure — the parent guide.
- Siting New Clinics With P-Median Optimization — using access gaps to place new supply.
- Catchment & Service Area Delineation — fixed catchments as an alternative to floating ones.