Tutorial 2 — Accessibility step-by-step (Boston)

This tutorial builds an accessibility analysis on the Boston inner-core data in nine progressive steps. Each step changes one or two settings and shows what happens to the result. Open UNA_Workspace.py in VS Code with the una conda environment active; modify the script in place and rerun python UNA_Workspace.py after each variation.

The data files live in docs/Boston/ (in the UNA repository):

  • Network (steps a–f, h): 20260703_PercLenNetwork_InnerCore.geojson — 69,957 pedestrian-network edges covering the Boston inner core (Cambridge + Boston core), 2D.

  • Network (step g, elevation): 20260703_PercLenNetwork_InnerCore_3D.geojson — the same network with elevation sampled at every vertex (x, y, z).

  • Origins (steps a–e): MIT_SAP_Harvard_GSD.geojson — two points: the MIT School of Architecture + Planning and the Harvard Graduate School of Design.

  • Destinations: MA_transit_stations.geojson — 6,919 transit stops across the MBTA region, with a weekly_departures attribute.

  • Origins (steps f–h): Cambridge_building_centroids.geojson — 14,751 building centroids across Cambridge.

Each step below shows only the settings that change relative to the previous step.

(a) Reach to transit stops — two origins, radius 400 m

The simplest possible accessibility analysis: count transit stops reachable within a 400 m network walk, with no destination weighting.

from urban_network_analysis import UNA
una = UNA()

una.settings.data_folder       = r"Boston"
una.settings.network_file      = "20260703_PercLenNetwork_InnerCore.geojson"
una.settings.origins_file      = "MIT_SAP_Harvard_GSD.geojson"
una.settings.destinations_file = "MA_transit_stations.geojson"
una.settings.search_radius     = 400

una.settings.calculate_reach               = True
una.settings.calculate_exponential_gravity = False
una.settings.calculate_logistic_gravity    = False
una.settings.calculate_knn_access          = False

una.settings.origin_weight_column      = "Count"
una.settings.destination_weight_column = "Count"

una.RunAccessibility()

Origin

Reach

MIT_SAP

7.00

Harvard_GSD

9.00

../_images/t2_image1.png

Within a 400 m walk, MIT_SAP reaches 7 transit stops and Harvard_GSD reaches 9 — raw counts, every stop contributing 1.

(b) Add destination weights (weekly_departures)

Weight each stop by its weekly departures — a proxy for service frequency. Reach becomes a weighted sum rather than a raw count.

una.settings.destination_weight_column = "weekly_departures"

Origin

Reach

MIT_SAP

3,456.00

Harvard_GSD

3,427.00

../_images/t2_image2.png

MIT_SAP’s fewer stops turn out to carry almost the same aggregate service level as Harvard’s more numerous ones.

(c) Add Gravity (exponential), β = 0.001

Gravity additionally penalizes distance — each destination contributes weight × exp(−β·d). See Gravity and decay models.

una.settings.calculate_exponential_gravity = True
una.settings.gravity_beta = 0.001

Origin

Reach

Gravity (exp)

MIT_SAP

3,456.00

2,969.86

Harvard_GSD

3,427.00

2,762.70

../_images/t2_image3.png

Gravity is always ≤ Reach because exp(−β·d) < 1 for any d > 0. With β = 0.001 the half-distance is ln(2)/β 693 m. Harvard’s gravity falls off more because its stops are on average farther away.

(d) Add Gravity (logistic), midpoint = 500 m

The logistic (S-shape) decay stays close to 1.0 below the midpoint and falls off rapidly past it — often a better fit for walking behavior: close is close, far is far, with a fairly sharp threshold in between.

una.settings.calculate_logistic_gravity = True
una.settings.gravity_logistic_midpoint  = 500

Origin

Reach

Gravity (exp)

Gravity (log)

MIT_SAP

3,456.00

2,969.86

3,279.87

Harvard_GSD

3,427.00

2,762.70

3,105.15

../_images/t2_image4.png

The logistic values sit between Reach and exponential gravity because at 0–400 m the logistic curve is still near its plateau.

(e) Add KNN access — knn_weights = (1.0,)

The K-Nearest-Neighbor index limits the analysis to only the K nearest destinations. K is set by the length of the knn_weights tuple, each element’s value by its contribution weight.

Warning

Even for a single weight, the trailing comma is required: (1.0,) is a one-element tuple, while (1.0) is just a parenthesized float and the setting will silently misbehave.

una.settings.calculate_knn_access = True
una.settings.knn_decay   = "logistic"
una.settings.knn_weights = (1.0,)   # ← note the trailing comma

Origin

Reach

Gravity (exp)

Gravity (log)

KNN

MIT_SAP

3,456.00

2,969.86

3,279.87

157.70

Harvard_GSD

3,427.00

2,762.70

3,105.15

617.80

../_images/t2_image5.png

KNN is dominated by the single nearest stop: Harvard’s nearest stop has far higher weekly departures than MIT’s, so despite nearly identical Reach and Gravity totals, the KNN scores diverge sharply.

(f) Scale up — all 14,751 Cambridge building centroids

Same settings as (e), but computed for every building in Cambridge.

una.settings.origins_file = "Cambridge_building_centroids.geojson"

Metric

n with access

Mean

Median

p90

Max

Reach

13,135

3,197.96

2,490.00

5,986.00

20,027.00

Gravity (exp)

13,135

2,451.64

1,864.02

4,572.85

16,457.07

Gravity (log)

13,135

2,774.99

2,110.67

5,154.10

18,397.42

KNN access

13,135

515.74

356.19

876.46

7,959.39

../_images/t2_image6.png

Gravity (exponential) per building centroid, visualized in QGIS.

About 89 % of buildings have at least one stop within a 400 m walk. The maxima belong to buildings near Harvard Square, Central Square, and Kendall Square where multiple bus and rail lines converge.

(g) Enable elevation, penalty = 4

Uphill walking accrues 4 extra cost-units per meter of rise. Swap in the 3D network (same 69,957 segments with SRTM 30 m z-values; elevation range −13 to 194 m):

una.settings.network_file = "20260703_PercLenNetwork_InnerCore_3D.geojson"
una.settings.elevation         = True
una.settings.elevation_penalty = 4

Metric

n with access

Mean

Median

p90

Max

Reach

12,897

2,967.93

2,241.00

5,548.20

19,934.00

Gravity (exp)

12,897

2,277.92

1,726.00

4,313.34

16,027.12

Gravity (log)

12,897

2,577.42

1,936.88

4,825.26

18,082.06

KNN access

12,897

513.70

356.12

878.19

7,959.39

Served buildings drop ~1.8 % and mean Reach ~7 %, driven by uphill routes near Beacon Hill, Copp’s Hill, Bunker Hill, and the terrain rises in Somerville and Cambridge. MIT_SAP (flat river bank) is essentially unchanged; Harvard_GSD’s Reach falls ~15 % as some 400 m walks now climb toward the Radcliffe Yard rise.

(h) Enable turns, threshold = 45°, penalty = 35

Any deviation from straight-ahead by more than 45° at a node adds 35 cost-units. The turn-aware engine builds a line graph internally and is slower — expect a few minutes for all 14,751 origins. The tables below use the 2D network so they compare directly to step (f):

una.settings.network_file = "20260703_PercLenNetwork_InnerCore.geojson"
una.settings.elevation = False
una.settings.turns          = True
una.settings.turn_threshold = 45
una.settings.turn_penalty   = 35

Metric

n with access

Mean

Median

p90

Max

Reach

12,723

2,831.80

2,100.00

5,296.60

19,934.00

Gravity (exp)

12,723

2,164.42

1,619.11

4,169.19

16,218.53

Gravity (log)

12,723

2,455.62

1,831.23

4,713.00

18,281.50

KNN access

12,723

513.13

353.85

872.96

7,951.62

Mean Reach falls ~11 % with turns enabled. Paths that look short on a map can become inaccessible once realistic pedestrian routing costs are applied — and turns + elevation stack additively if you also switch back to the 3D network.

(i) WalkScore-style composite with UNA_Batch.py

Steps (a)–(h) measured accessibility to one destination type at a time. A WalkScore-style composite sums accessibility across many destination categories — schools, transit, food, healthcare, parks — into one number per origin. UNA_Batch.py does this from a single pairings CSV: one row per category, each with its own destinations_file, search_radius, knn_weights, decay parameters and flags. The Boston table (Boston/Boston_KNN_pairings.csv) lists 10 categories (arts, bus, food & beverage, health, metro, parks, personal services, public institutions, retail, schools), each with batch_composite_output = TRUE and composited on knn_access.

The full UNA_Batch.py script — edit the three path constants and the analysis type at the top, everything else stays as shipped:

from urban_network_analysis import UNA

# --------------------------------------------------------------
# EDIT THESE THREE VALUES
# --------------------------------------------------------------

DATA_FOLDER   = r"Boston"
PAIRINGS_FILE = r"Boston/Boston_KNN_pairings.csv"
OUTPUT_FOLDER = r"Boston/Results"

# Which analysis to run per row — must match the metric in your
# pairings CSV:
#   "accessibility"  — for reach / gravity_* / knn_access composites
#   "flow"           — for edge_flow or node_flow composites
ANALYSIS = "accessibility"

# --------------------------------------------------------------
# Run the batch (per-row files + optional composite file)
# --------------------------------------------------------------

una = UNA()
una.settings.data_folder   = DATA_FOLDER
una.settings.output_folder = OUTPUT_FOLDER

una.RunBatch(ANALYSIS, pairing_file=PAIRINGS_FILE)

All the per-category settings — destinations_file, search_radius, knn_weights, decay parameters, calculate_* flags, batch_composite_output, batch_composite_result_column — come from the pairings CSV rows, not from the script. Run it with:

python UNA_Batch.py

The script builds the topology once per row, runs the accessibility engine per category, and — because rows set batch_composite_output = TRUE — assembles a single joint output with one row per origin building whose final composite_sum column adds the 10 category KNN scores into one WalkScore-style number. Compositing is built into UNA.RunBatch — see Project workflow — RunBatch() for the mechanics and /user_guide/settings_reference:name for per-row naming.

Note

Composites are grouped by origins layer. All 10 rows here share the same origins file, so the batch writes a single composite file. A pairing table whose rows use several different origin layers (e.g. “Homes to …” and “Jobs to …” rows) produces one composite file per origin layer, each with its own composite_sum column — see Project workflow — RunBatch().

../_images/t2_image8.png

K-Nearest-Neighbor composite (WalkScore-style) for Cambridge building centroids. Highest scores near Harvard Square and Central Square.

Adapting the pairings table is the main editorial workflow: add a row for a new category, tweak a radius or knn_weights vector, rerun — no Python editing required. With turns=TRUE and elevation=TRUE (3D network) the full 10-row run takes 30–60 minutes on a laptop; with both FALSE, 5–10 minutes.

What we covered

Reach (a–b), Gravity under two decay shapes (c–d), KNN access (e), city scale (f), elevation (g), turns (h), and a CSV-driven composite (i). The same settings combine freely — try a different radius, a sharper logistic midpoint, or knn_weights = (1.0, 1.0, 0.5) and watch each change ripple through the result columns.

Next: Tutorial 3 — Foot-traffic flow analysis in Cambridge — from accessibility scores to per-edge pedestrian flows.