Public Transit Accessibility Modeling
Every drive-time access metric assumes a working car. Between six and twelve percent of US households do not have one, that share is far higher among the low-income, elderly and disabled populations most dependent on public health services, and for those households a thirty-minute drive-time catchment describes a journey they cannot make. This guide is part of Healthcare Access & Network Analysis Automation, and it covers building transit accessibility models that answer the question a drive-time map cannot.
Concept & Method Alignment
Transit travel time differs from driving time in ways that break the machinery of a road-network model.
It is not a single number. A trip from A to B takes 34 minutes at 08:15 on a Tuesday, 61 minutes at 10:40, and is impossible at 21:00. There is no “the” transit travel time; there is a distribution over departure times, and the summary chosen from that distribution is a modelling decision with consequences.
It is not symmetric. The return trip may use a different route with different frequency, and an appointment that is reachable in the morning may strand a patient in the evening. A clinic access model that only tests the outbound leg answers half the question.
It is composed of legs. A transit journey is walk, wait, ride, possibly transfer, and walk again. Riders do not weight those legs equally: waiting and transferring are experienced as roughly twice as onerous as riding, and a model that sums raw minutes systematically overrates high-transfer itineraries.
It has a schedule, and schedules expire. A GTFS feed describes service over a stated date range. Running an analysis against a feed whose service period has ended silently produces a network with no trips, and the usual symptom is an accessibility map that is entirely zero without any error being raised.
Method-Selection Table
| Approach | Handles departure time | Handles transfers | Cost | Use when |
|---|---|---|---|---|
| Straight-line to nearest stop | no | no | trivial | never, for published work |
| Single-departure routing | no | yes | low | a fixed appointment time is genuinely the question |
| Departure-window percentile | yes | yes | moderate | the standard default |
| RAPTOR / range query | yes | yes | moderate | many origins, full arrival-time profile needed |
| Isochrone from a router | yes | yes | high per origin | small numbers of facilities, map output |
The departure-window percentile approach is the default because it answers the operational question — “if someone leaves during the morning, how long does the journey usually take” — with one interpretable parameter. Report the median and the 90th percentile together; the gap between them is the reliability of the service, and that gap is invisible in any single-departure model.
Spatial Data Prerequisites
- A GTFS feed whose service period covers the analysis date. Validate before use; see the dedicated guide on feed validation.
- A walk network covering the whole study area, since first and last legs dominate transit access in low-density areas.
- Stop locations in a metric CRS aligned with the population layer, per Coordinate Reference Systems for Public Health.
- Facility opening hours, because a journey that arrives after closing is not access. This is the input most often omitted and it changes results substantially for services with short hours.
- A stated maximum walk distance to and from stops, justified from the population being modelled rather than from convention.
Production Implementation
# Departure-window transit travel times from tract centroids to clinics.
# Pinned: python 3.11, pandas==2.2.2, geopandas==1.0.1, numpy==1.26.4,
# gtfs-kit==10.1.1, r5py==0.1.2 (r5py needs a JDK 21 runtime)
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("transit.access")
def departure_window(date: str, start: str = "07:00", end: str = "10:00",
step_min: int = 5) -> pd.DatetimeIndex:
"""Departure times to evaluate. The window is a modelling choice: a window
that includes a period with no service will drag the median toward infinity,
which is correct and must be reported rather than clipped away."""
idx = pd.date_range(f"{date} {start}", f"{date} {end}", freq=f"{step_min}min")
log.info("evaluating %d departures from %s to %s", len(idx), start, end)
return idx
def summarise_profile(times_min: np.ndarray, percentiles=(50, 90)) -> dict:
"""Collapse a departure-time profile to reportable numbers.
Unreachable departures are kept as NaN and counted separately rather than
dropped: 'reachable 60% of the time, median 41 minutes when reachable' is a
very different finding from 'median 41 minutes'."""
reachable = ~np.isnan(times_min)
share = float(reachable.mean())
if share == 0:
return {"reachable_share": 0.0, **{f"p{p}": np.nan for p in percentiles}}
vals = times_min[reachable]
out = {"reachable_share": share}
out.update({f"p{p}": float(np.percentile(vals, p)) for p in percentiles})
out["spread_p90_p50"] = out.get("p90", np.nan) - out.get("p50", np.nan)
return out
def access_by_origin(profiles: dict[str, np.ndarray], threshold_min: float = 45.0) -> pd.DataFrame:
"""One row per origin: reachability, median, reliability and a threshold flag."""
rows = []
for origin, arr in profiles.items():
s = summarise_profile(arr)
s["origin"] = origin
s["within_threshold"] = bool(s.get("p50", np.inf) <= threshold_min
and s["reachable_share"] >= 0.9)
rows.append(s)
df = pd.DataFrame(rows).set_index("origin")
log.info("%d of %d origins reach a clinic within %.0f min at the median "
"and are reachable on at least 90%% of departures",
int(df["within_threshold"].sum()), len(df), threshold_min)
return df
The reachable_share field is what makes the output honest. A tract from which a clinic is reachable on only half the morning departures is not a 45-minute tract; it is a tract with intermittent access, and collapsing that to a median hides the failure mode that matters most to the people living there.
Parameter Selection & Tuning
- Departure window should match the service being modelled. A same-day clinic is a morning window; a scheduled specialist appointment might be a single departure. Say which and why.
- Maximum walk distance of 800 m is a common default and is too long for many elderly and mobility-limited populations. Model at least two values and report both.
- Transfer and wait penalties of around 1.5 to 2.0 times in-vehicle time reflect observed rider behaviour. Applying no penalty overstates access for high-transfer itineraries, which are concentrated in exactly the peripheral areas the analysis is about.
- Threshold for the binary access flag should come from a standard rather than convention, exactly as in Facility Capacity Allocation Models.
Edge Cases & Failure Modes
A feed that has expired. Produces an all-zero accessibility map with no error. Always assert that trips exist on the analysis date before routing.
Service that runs only on schooldays. Many rural feeds carry calendar exceptions that remove most service in July. An analysis run on a summer date describes summer.
Origins with no stop within the walk threshold. These are not slow; they are unreachable, and coding them as a large travel time rather than as unreachable will make every summary statistic wrong.
Facilities whose hours do not overlap the arrival window. A clinic open 09:00 to 12:00 is not accessible on a route arriving at 13:30 regardless of travel time. Intersect arrival times with opening hours before computing access.
Comparing transit and drive maps on the same colour scale. Transit times are longer and more variable, and a shared scale makes the transit map look uniformly catastrophic while hiding its internal variation. Scale them separately and say so.
Compliance & Audit Controls
- Pin the GTFS feed version and the analysis date in the run signature. A transit result is meaningless without both, and feeds change weekly.
- Record the departure window, the percentile reported, the walk threshold and the transfer penalty. Any of the four moves the result more than the routing engine does.
- Publish reachability alongside travel time. A map showing only medians conceals intermittent service, which is the defining feature of the areas that need attention.
- State the mode assumption in the metric name, as
access_transit_p50_0700_1000_walk800rather thanaccess_score, following the naming discipline in the parent section. - Keep drive-time and transit results as separate published layers, never combined into a single “access” figure whose modal composition is invisible.
Combining Transit Results With Drive-Time Results
Most agencies already hold a drive-time access layer, and the temptation on producing a transit layer is to merge them into a single access figure weighted by car ownership. That is a defensible statistic and a poor headline, for two reasons worth separating.
The first is that the weighted average conceals its own composition. A tract at 92% car ownership with excellent drive access and no transit at all produces a blended figure that looks adequate, and the 8% of households for whom access is genuinely absent disappear into the arithmetic. Since those households are systematically poorer, older and more likely to have the conditions the service addresses, the blended figure is worst exactly where precision matters most.
The second is that the two layers have different uncertainties. A drive-time estimate for a given tract is stable across the day and across reasonable parameter choices. A transit estimate varies by departure time, by walk-speed assumption, by leg weighting, and it carries a reachability share that has no drive-time analogue. Averaging a stable number with a volatile one produces a number whose stability nobody can characterise.
The recommended presentation is two layers and one derived indicator. Publish drive-time access and transit access as separate maps with separate scales, and derive a single gap indicator — the share of households without a vehicle in tracts that fail the transit threshold — as the headline figure. That indicator is a count of people rather than an average of minutes, it is directly interpretable by a commissioner, and it points at the intervention: either a service change or a facility change, depending on which side of the gap the tract sits on.
Where a blended figure is genuinely required, for instance by a reporting template that expects one access column, compute it and publish its inputs beside it. The blended number then remains auditable, and the reader who wants to know how it was composed can find out without re-running the analysis.
Vehicle access is not distributed evenly, and the distribution is what makes a transit layer a necessity rather than a refinement:
Production Implementation Checklist
Frequently Asked Questions
Why not just use a straight-line distance to the nearest stop? Because it answers a question nobody has. Access depends on where the service goes and how often, not on proximity to a stop; a stop served twice a day is not access.
Which percentile should I report? The median and the 90th, together. The median is the typical journey and the gap between the two is the service reliability, which is what distinguishes a usable route from an unusable one.
Should I model the return trip? Yes, for any service where the visit has a duration. Evening service is typically much sparser than morning service, and a model of the outbound leg alone can report good access to a clinic a patient could not leave.
How do I handle a study area spanning several transit agencies? Merge the feeds and validate the merged product, checking specifically for duplicated stops and for route identifiers colliding between agencies.
Sensitivity Testing a Transit Access Result
Transit accessibility results depend on more free parameters than drive-time results, and a single configuration presented without a sensitivity analysis overstates its own precision. Four parameters account for almost all of the movement, and testing them is cheap once the matrix machinery exists.
Walk threshold. Model at 400 m and 800 m as a minimum. The difference is large in low-density areas, where the 800 m assumption often supplies the only stop within reach, and it is exactly the assumption most questionable for elderly patients.
Departure window. A three-hour morning window and a full-day window give different medians and very different reachability shares. Which is right depends on whether appointments are scheduled or walk-in, and the analysis should say which service it modelled.
Leg weighting. The generalised-cost weights change which origins fall inside a threshold by tens of percent, as the companion guide shows, and their uncertainty is real.
Analysis date. Re-run on a second typical weekday. Agreement confirms the feed is stable; disagreement usually reveals a calendar exception that the validation missed.
Report the four as a small table of qualifying-origin counts, one row per parameter varied from the central configuration. The table takes a paragraph, it makes the central figure’s uncertainty visible, and it pre-empts the first question any reviewer will ask. Where one parameter dominates — and it is usually the walk threshold — say so, because that identifies where better data would most improve the estimate.
A Note on Data Availability
Transit accessibility work is limited less by method than by whether a usable feed exists. Large urban agencies publish GTFS as a matter of course; small rural operators, county paratransit services, tribal transit programmes and hospital shuttle routes frequently do not, and those are precisely the services that carry the populations an equity analysis is about. An analysis built only on the feeds that happen to be published will therefore understate access in rural areas and overstate the isolation of communities served by an unpublished shuttle.
Two responses are worth the effort. The first is to inventory the operators in the study area before modelling anything, from the state transit directory or the metropolitan planning organisation, and to record which of them have machine-readable schedules. That inventory is a finding in itself: “seven of eleven operators publish a feed” belongs in the methods and explains the shape of the resulting map. The second is to build a minimal feed by hand for a small unpublished service where it materially affects the result. A rural route with four daily trips is a few dozen rows of CSV, and constructing it from a printed timetable is an afternoon’s work that can change the conclusion for an entire county.
Where neither is possible, mark the affected areas as not modelled rather than as unserved. The two look identical on a map and mean opposite things to a commissioner.
Related Topics
- Healthcare Access & Network Analysis Automation — the parent section, and the drive-time methods this one complements.
- Drive-Time Isochrone Generation — the car-based counterpart, and the layer transit results should be published beside rather than merged into.
- Spatial Equity Index Calculation — where a transit travel-time matrix feeds a catchment-based equity score.
- Batch Routing Error Handling — the retry and caching discipline a large departure-window sweep needs.
- Facility Capacity Allocation Models — where a transit-based impedance changes which facility a population is assigned to.