Output files
bikescore-bna has no run store. score_city runs each stage into a
subdirectory of a workdir you choose and returns a ScoreResult:
result = score_city(inputs, build_config("default"), workdir="runs/aspen-2026")
result.stage_dirs # {stage_name: Path} — one output directory per stage that ran
result.workdir # the run root you passed (persistent, not a discarded temp dir)
result.output("stress", "stress.parquet") # -> Path to a specific output file
# Later — reuse those outputs without recomputing (e.g. to export):
from bikescore_bna import ScoreResult
result = ScoreResult.from_dir("runs/aspen-2026")
Omit workdir and outputs land in a fresh timestamped folder under ./bikescore-bna-runs/
in the current directory — still persistent and reusable, never a temp dir that is
silently thrown away.
Each stage owns its directory and writes one or more files into it. The filenames and
schemas below are stable, and columns match brokenspoke-analyzer so the two tools stay
interchangeable for downstream consumers.
Geometry columns are written by GeoParquet where the file is a GeoDataFrame; the
destination cluster geometries are stored as EWKB-hex strings (geom_pt, geom_poly)
so plain pandas.to_parquet can round-trip them with their source CRS.
To turn any of these into GeoJSON / Shapefile / CSV for GIS tools or the PeopleForBikes
platform, use the export framework below.
Files by stage
parse/
| File |
Contents |
Key |
ways_raw.parquet |
Raw OSM ways with parsed tags (pre-segmentation) |
osm_id |
nodes.parquet |
OSM nodes with intersection attributes (signalized, stop, rrfb, island, lon, lat) |
node_id |
poi_raw.pkl |
Pickled list[RawPOI] — raw points/polygons of interest for the destinations stage |
— |
census/
| File |
Contents |
Key |
census_blocks.parquet |
Census blocks (GeoParquet) with population/housing and the clipped city geometry |
geoid20 |
excluded_census_blocks.parquet |
Blocks dropped from scoring — water-only or mostly-outside the boundary — with a block_class code (water / outside) plus geoid20, pop20, aland20, awater20. Written only when some block is excluded; inert (never scored), so it just makes the drops distinguishable from missing data |
geoid20 |
jobs/
| File |
Contents |
Key |
jobs.parquet |
LODES employment counts per census block (US cities only; empty otherwise) |
blockid20 |
attributes/
| File |
Contents |
Key |
ways_classified.parquet |
Ways with the resolved attribute layer applied (functional_class, speed/lane/width defaults, bike-infra, derived flags) |
osm_id |
segment/
| File |
Contents |
Key |
segments.parquet |
Topology-split road segments (GeoParquet) with start_node_id / end_node_id |
road_id |
trails.parquet |
Off-network paths (the network_path pseudo-destination) with path_length / bbox_length |
— |
stress/
| File |
Contents |
Key |
stress.parquet |
Segments with LTS added: ft_seg_stress, tf_seg_stress, ft_int_stress, tf_int_stress |
road_id |
graph/
| File |
Contents |
Key |
graph_bundle.pkl |
Pickled GraphBundle — the CSR routing graphs (G_high, G_low) and block↔vertex association tables used by connectivity/scores |
— |
graph.parquet |
Directed link table: source_vert, target_vert, link_cost, link_stress (parallel edges allowed) |
— |
nodes.parquet |
Vertex↔road map: vert_id, road_id |
vert_id |
blocks_with_roads.parquet |
Census blocks enriched with the road IDs and projected geometry needed for source-vertex selection |
geoid20 |
connectivity/
| File |
Contents |
Key |
connectivity.parquet |
Reachability between every block pair: source_blockid20, target_blockid20, low_stress, low_stress_cost, high_stress, high_stress_cost |
(source, target) |
connectivity.csv |
Same table with booleans as PostgreSQL t/f, matching brokenspoke-analyzer |
— |
destinations/
| File |
Contents |
Key |
dest_{type}.parquet |
One clustered destination table per type (schools, parks, supermarkets, …); id, blockid20 (array), geom_pt/geom_poly (EWKB-hex), and pop_* placeholder columns filled later by the scoring stage |
id |
destination_summary.parquet |
Cluster count per destination type: dest_type, cluster_count |
dest_type |
scores/
| File |
Contents |
Key |
scores.parquet |
Per-block access scoring — every {category}_low_stress / {category}_high_stress / {category}_score, plus opportunity_score, core_services_score, recreation_score, overall_score |
geoid20 |
neighborhood/
| File |
Contents |
Key |
neighborhood.parquet |
The 23 headline scores: score_id, score_original, score_normalized, human_explanation |
score_id |
score_inputs.parquet |
The 132 intermediate scores (medians, percentiles, shed scores) with use_* contribution flags |
id |
mileage.parquet |
Miles of each bike-infrastructure type within the boundary: feature_type, total_mileage |
feature_type |
Export
The stage outputs above are parquet/pickle — the native format for hashing and reuse. To
hand them to GIS tools or the PeopleForBikes platform, bikescore_bna.export writes them to
GeoJSON, Shapefile, or CSV, reprojecting geometry to WGS84 (EPSG:4326). It works
directly off a ScoreResult — no run store, no database. Because ScoreResult.from_dir
rebuilds that result from a prior run's workdir, export can reuse existing outputs
instead of recomputing the pipeline.
from bikescore_bna import score_city, export_target, export_bundle, load_city
city = load_city(city_dir)
config = build_config("default")
result = score_city(inputs, config)
# one target → one file (road-segment LTS as GeoJSON)
export_target(result, city, config, "stress", "out/", file_format="geojson", inputs=inputs)
# the whole brokenspoke-analyzer deliverable set
export_bundle(result, city, config, "out/", bundle="bna", inputs=inputs)
A target is one named, exportable output; export_target / export_bundle build a
ExportContext over the result's stage_dirs and write the requested formats.
inputs is the same dict passed to score_city — some targets (e.g. boundary) read a
raw dataset directly. list_export_targets() / list_export_bundles() enumerate what is
available, and the CLI's export-list prints the table.
Targets
| Target |
Owner stage |
Default formats |
Notes |
stress |
stress |
geojson, shapefile, csv |
road segments with LTS — the headline network layer |
ways_raw, nodes |
parse |
geojson, shapefile, csv |
raw ways / nodes |
intersections |
parse |
geojson, shapefile, csv |
intersection points with legs, signalized, stops, … |
boundary |
parse |
geojson, shapefile, csv |
the original fetched city boundary (reads the raw boundary input) |
analysis_boundary |
parse |
geojson |
the transformed analysis boundary; emitted only when a boundary transform changed it |
census_blocks |
census |
geojson, shapefile, csv |
plain census blocks |
excluded_blocks |
census |
geojson, shapefile |
blocks dropped from scoring, carrying block_class (water / outside); skipped cleanly when nothing was excluded |
neighborhood_census_blocks |
census |
geojson, shapefile, csv |
blocks joined to their scores |
ways_classified |
attributes |
geojson, shapefile, csv |
the resolved attribute layer |
segments, trails |
segment |
geojson, shapefile, csv |
split segments / off-network paths |
blocks_with_roads |
graph |
geojson, shapefile, csv |
blocks with associated road IDs |
destinations |
destinations |
geojson, csv |
one file per destination type (fan-out) |
connectivity |
connectivity |
csv |
block-pair reachability (t/f booleans) |
scores |
scores |
csv |
per-block access scores |
neighborhood_scores, score_inputs, mileage |
neighborhood |
csv |
headline / intermediate scores, mileage |
speed_limits |
config |
csv |
residential speed limits from city FIPS + config |
The bna bundle
export_bundle(..., bundle="bna") writes the full brokenspoke-analyzer deliverable set
into one directory, each target under its platform filename (neighborhood_ways.geojson,
neighborhood_census_blocks.geojson, neighborhood_connected_census_blocks.csv,
residential_speed_limit.csv, …) plus a self-describing README.md. neighborhood_census_blocks
and stress are required (a missing input raises); optional targets whose inputs are
absent are skipped with a warning. The neighborhood_analysis_boundary and
neighborhood_excluded_blocks layers ride along too — each skipped cleanly when a run has
no transformed boundary / no excluded blocks, so default bundles are unchanged.