Analysis of bicycle connectivity and Level of Traffic Stress (LTS) for the Greater Boston area, built by the DS4CG Boston Cyclists Union Team.
The project builds a routable street network graph from OpenStreetMap in which each edge's- or road segment's- travel "cost" reflects how difficult it is to bike (edge length scaled by a stress rating), attaches census population, and models travel demand to the network. The future goal is to utilize this graph representation to identify which road segments would be most beneficial to improve.
The analysis is organized in stages, each as a sub-package under src/bcu_analysis/. Each step depends on the previous, so they should normally be run in order.
-
graph_builder/— Download OpenStreetMap data for the region (Boston, Cambridge, Somerville, Brookline, or All four cities), determine a Level of Traffic Stress (LTS) rating for every edge, and build a routable "cost" graph wherecost = length × LTS stress multiplier(the LTS stress multipliers are modifiable for different scenarios). Higher-stress edges are penalized so routing prefers low-stress streets. The graph is then simplified for faster routing. -
destination_csvs/— Query OSM (via the Overpass API) over the region of choice (Boston, Brookline, Cambridge, Somerville, or All four cities) for destination points of interest including schools, healthcare facilities, transit stations, stores, and greenspaces and write these locations of interest to coordinate CSVs. Multiple separate CSVs are created for each type of destination (ex. schools, transit stations, etc.), which are then combined into a master CSV containing all destinations for the given region. -
census/— Assign census-tract population to graph nodes using deterministic, area-weighted Voronoi allocation.build_census_tracts.pyjoins the population value of a tract to the associated tract geometry;assignment.pyperforms the allocation of population to nodes (or intersections) on the graph; andrun_census_assignment.pyis the runner. -
od_generation/— Generate travel demand as a set of Origin Destination (OD) pairs over the graph. Two generators feed a single combined demand file:- LODES commutes (
lodes_io.py,lodes_pairs.py,lodes_sampling.py): home→work trips built from Census LODES data, sampled to favor shorter, more bikeable trips. - Population-weighted POI trips (
build_poi_od_pairs.py,poi_destination_choice.py): Homes (nodes on the graph) are drawn in proportion to assigned population so higher-population points are more likely to be chosen (see census module). Destination points (from thedestination_csvs/module) are snapped to the closest node on the graph. Homes are then paired to destination POIs, or points of interest, by specific rules that vary by category.
generate_od_demand.pycombines both the Population-weighted POI trips and the LODES commutes for a demand scenario defined inod_generation/config/demand_parameters.csv, writing columnsorigin_node, destination_node, category, count. - LODES commutes (
-
road_usage/— Generates the least-costly path on the network for each origin-destination pair, and assigns the graph edges (road segments) 3 new metrics:path_count: Measures traffic demand on the networkusage_stress(path_count*max_lts) : Highlights high-stress and high-use road segmentspotential_Dbenefit(path_count* (cost-distance)) : Quantifies the potential improvement if the given road segment were to be improved (similar tousage_stressas high-stress & high-use roads would have a greater impact compared to less-popular or less-stressful road segments, however potential improvement is more directly effected by the length of the given road segment)
-
one_way_evaluation/— For the set of origin-destination pairs, maps the most efficient path for trips in both directions (ex. home→school and school→home) and highlights one-way road segments that cause disproportionately large detours in one direction compared to the other. The one-way road segments are then scored based on the severity of the detour and the frequency of affected trips -
corridor_analysis/— Isolates connected low-stress (LTS 1 & 2) "safe zones" into discrete islands and computes the highest-ROI (highest return on investment) missing link corridors to bridge them. Generates interactive visualizations (PyDeck/Streamlit), an updated scenario of the network if the corridor road segments were improved (GraphML format), and GeoPackage (.gpkg) files for QGIS integration. -
node_accessibility/— Each node (or intersection) in the graph is given a rating corresponding to the ratio of the current connectivity in the surrounding road network (considering the stress level ratings of the surrounding edges) and the potential connectivity (if all road segments had a stress level rating of LTS 1). These ratings are also compared with demographic information to investigate if there is a correlation between the two.
-
Set up a Conda environent:
conda create -n bcu_graph_analysis python=3.12 conda activate bcu_graph_analysis -
Install the project:
python -m pip install -e '.[test,dev]'
- Run:
python src/bcu_analysis/graph_builder/build_cost_graph.py COST_SCENARIO_ID REGION --data-dir FOLDERCOST_SCENARIO_IDshould be replaced with the specific ID number for the cost scenario of interest (please seesrc/bcu_analysis/graph_builder/config/cost_parameters.csvto view cost scenarios)REGIONshould be replaced with the region of interest (boston, brookline, cambridge, somerville, greater_boston)FOLDERshould be replaced with the path to the folder where the data will be stored
This will build the cost graph (with the specified cost scenario).
- Run:
followed by:
python src/bcu_analysis/destination_csvs/csv_maker.py FOLDER REGIONpython src/bcu_analysis/destination_csvs/combining_csvs.py FOLDER REGIONREGIONshould be replaced with the region of interest (Boston, Brookline, Cambridge, Somerville, or All). Note that thecsv_maker.pyscript only processes 1 city at a time, so to generate destinations for all of Greater Boston, run the first command 4 times (once for each city) and then run the second command with region as All.FOLDERshould be replaced with the path to the folder where the data is stored
This will generate a list of all destination points and their locations.
- Run:
python bcu_analysis.census.run_census_assignment --region REGION --graph-path GRAPH_PATH --tract-path TRACT_PATH --output-directory OUTPUT_PATH --output-prefix OUTPUT_PREFIXREGIONshould be replaced with the region of interest (boston, brookline, cambridge, somerville, or greater-boston)GRAPH_PATHshould be your root directory +/output/cost_scenarios/cost_scenario_#/REGION_cost_scenario_#_simplified.graphmlwhere the # is replaced with the ID number of the cost scenario of interest and REGION is replaced with the region of interest (boston, brookline, cambridge, somerville, or greater_boston)TRACT_PATHshould be your root directory +/ma_tracts_population.geojsonOUTPUT_PATHshould be your root directory +/census_resultsOUTPUT_PREFIXshould beREGION_cost_scenario_#where the # is replaced with the ID number of the cost scenario of interest and REGION is replaced with the region of interest (boston, brookline, cambridge, somerville, or greater_boston)
This will assign population counts to each node in the cost graph (which represent origin points, or points where people are coming from)
- Run:
python src/bcu_analysis/od_generation/generate_od_demand.py COST_SCENARIO_ID REGION --demand-scenario DEMAND_SCENARIO_ID --data-dir FOLDER --pop-geojson-path PATHCOST_SCENARIO_IDshould be replaced with the ID number for the cost scenario of interestREGIONshould be replaced with the region of interest (boston, brookline, cambridge, somerville, or greater_boston)DEMAND_SCENARIO_IDshould be replaced with the ID number for the demand scenario of interest (please seesrc/bcu_analysis/od_generation/config/demand_parameters.csvto view demand scenarios)FOLDERshould be replaced with the path to the folder where the data is storedPATHshould be your root directory (FOLDER) +/census_results/REGION_cost_scenario_#_nodes_with_population_web.geojsonwhere the # is replaced with the ID number of the cost scenario of interest and REGION is replaced with the region of interest (boston, brookline, cambridge, somerville, or greater_boston)
This will generate a list of origin-destination pairs that estimate starting and ending points of popular trips taken on the network.
- Run:
python src/bcu_analysis/road_usage/path_count.py FOLDER DEMAND_SCENARIO_ID COST_SCENARIO_ID REGIONFOLDERshould be replaced with the path to the folder where the data is storedDEMAND_SCENARIO_IDshould be replaced with the ID number for the demand scenario of interestCOST_SCENARIO_IDshould be replaced with the ID number for the cost scenario of interestREGIONshould be replaced with the region of interest (Boston, Brookline, Cambridge, Somerville, or All)
This will generate the least-cost route for each origin-destination pair and add a new attribute called path_count to the edges of the cost graph. path_count is the
number of least-cost paths that cross through a given edge.
- Run:
python src/bcu_analysis/road_usage/metrics.py FOLDER DEMAND_SCENARIO_ID COST_SCENARIO_ID REGIONFOLDERshould be replaced with the path to the folder where the data is storedDEMAND_SCENARIO_IDshould be replaced with the ID number for the demand scenario of interestCOST_SCENARIO_IDshould be replaced with the ID number for the cost scenario of interestREGIONshould be replaced with the region of interest (Boston, Brookline, Cambridge, Somerville, or All)
This will add two attributes to the edges of the cost graph: usage_stress and potential_Dbenefit (please see the /road_usage/ section under Modules for
further clarification).
- (Optional) Run:
python src/bcu_analysis/road_usage/Distributions.py FOLDER REGION DEMAND_SCENARIO_ID COST_SCENARIO_IDFOLDERshould be replaced with the path to the folder where the data is storedREGIONshould be replaced with the region of interest (Boston, Brookline, Cambridge, Somerville, or All)DEMAND_SCENARIO_IDshould be replaced with the ID number for the demand scenario of interestCOST_SCENARIO_IDshould be replaced with the ID number for the cost scenario of interest
This will report the 5-number summary (minimum, 1st quartile, median, 3rd quartile, and maximum) for the following edge attributes of the cost graph: usage_stress,
potential_Dbenefit, path_count, distance, max_lts, and cost. Optional flags to add to the command include:
--no_path_count: If written, will not report on thepath_countattribute--no_distance: If written, will not report on thedistanceattribute--no_max_lts: If written, will not report on themax_ltsattribute--no_cost: If written, will not report on thecostattribute--no_usage_stress: If written, will not report on theusage_stressattribute--no_potential_Dbenefit: If written, will not report on thepotential_Dbenefitattribute
- Run:
python src/bcu_analysis/road_usage/svgs/HeatmapLog.py FOLDER REGION DEMAND_SCENARIO_ID COST_SCENARIO_ID ATTRIBUTE LOWER_THRESHOLD UPPER_THRESHOLDFOLDERshould be replaced with the path to the folder where the data is storedREGIONshould be replaced with the region of interest (Boston, Brookline, Cambridge, Somerville, or All)DEMAND_SCENARIO_IDshould be replaced with the ID number for the demand scenario of interestCOST_SCENARIO_IDshould be replaced with the ID number for the cost scenario of interestATTRIBUTEshould be replaced with the edge attribute of interest. The script will generate a heatmap of the road network using the edges' values of the specified attribute (usage,usage_stress, orpotential_improvement)LOWER_THRESHOLDshould be replaced with the desired lower bound for the log-color-scale. All edges with an attribute value below this number (but greater than 0) will be colored in yellow.UPPER_THRESHOLDshould be replaced with the desired upper bound for the log-color-scale. All edges with an attribute value above this number will be colored in red.
This will generate a heatmap of the road network where each road segment (or edge) is a different color. For each edge, the color is determined by its value of the specified attribute, and the color follows a log-color-scale with an upper and lower bound (all edges above the upper bound are colored red and all below are colored in yellow). An optional flag to include in the command is:
--onlyLTS3and4: The heatmap will only color road segments that have a traffic stress-level rating of LTS 3 or LTS 4
Run:
python src/bcu_analysis/one_way_evaluation/run_route_asymmetry.py COST_SCENARIO_ID REGION --demand-scenario DEMAND_SCENARIO_ID --data-dir FOLDER --workers WORKERS
COST_SCENARIO_IDshould be replaced with the ID number for the cost scenario of interestREGIONshould be replaced with the region of interest (boston, brookline, cambridge, somerville, or greater_boston)DEMAND_SCENARIO_IDshould be replaced with the ID number for the demand scenario of interestFOLDERshould be replaced with the path to the folder where the data is stored (root directory)WORKERSshould be replaced with the number of desired routing processes (default: all CPU cores)
This will run the entire analysis, which scores one-way road segments based on the severity of the detours they cause and the frequency of affected trips. These scores are stored and outputted in a csv file, and could be visualized with a heatmap.
Run:
python src/bcu_analysis/corridor_analysis/run_corridors.py REGION --graph-dir GRAPH_PATH --poi-dir POI_PATH --output-dir OUTPUT_PATH --demand-scenario DEMAND_SCENARIO_ID --cost-scenario COST_SCENARIO_ID
REGIONshould be replaced with the region of interest (boston, brookline, cambridge, somerville, or greater_boston)GRAPH_PATHshould be your root directory +/processed/road_usage_analysisPOI_PATHshould be your root directory +processed/osmOUTPUT_PATHshould be your root directory +/corridor_analysisDEMAND_SCENARIO_IDshould be replaced with the ID number for the demand scenario of interestCOST_SCENARIO_IDshould be replaced with the ID number for the cost scenario of interest
This will run the entire analysis, computing missing link corridors between isolated low-stress islands in the network, and generate a series of output files (visualizations, graph scenarios with potential improvements on the network, and geo-based files). Note, please use the --help flag to adjust parameters like --min-island-size and --link-complexity.
(Optional) To view the streamlit dashboard:
streamlit run src/bcu_analysis/corridor_analysis/dashboard.py
Run:
python bcu_analysis.node_accessibility.run_all_nodes --graph-path GRAPH_PATH --progress-every PROGRESS --cost-field COST --cutoff-miles MILES --output-path OUTPUT_PATH --failure-path FAILURE_PATH
GRAPH_PATHshould be your root directory +/output/cost_scenarios/cost_scenario_#/REGION_cost_scenario_#_simplified.graphmlwhere the # is replaced with the ID number of the cost scenario of interest and REGION is replaced with the region of interest (boston, brookline, cambridge, somerville, or greater_boston)PROGRESSshould be replaced with an integer x, so that for every x nodes the algorithm is run for, a progress statement is printedCOSTis the name of thecostedge attribute (this should always be "cost")MILESshould be replaced with the maximum distance (in miles) that connectivity is measured for each nodeOUTPUT_PATHshould be your root directory +/greater_boston_cost_scenario_#_node_accessibility_MILESmi.csvwhere the # is replaced with the ID number of the cost scenario of interest and MILES is replaced with yourMILESparameterFAILURE_PATHshould be replaced with your root directory +/greater_boston_cost_scenario_#_node_accessibility_MILESmi_failures.csvwhere the # is replaced with the ID number of the cost scenario of interest and MILES is replaced with yourMILESparameter
This will run the entire analysis, scoring each node on its relative accessibility (a ratio of the current accessibility considering cost & stress level and the ideal accessibility just considering distance) and generating a series of visualizations.
.
├── src
│ └── bcu_analysis # The importable Python package
│ ├── graph_builder # Build the LTS-weighted cost graph from OSM
│ │ ├── config # LTS tables and OSM-tag parsing rules (yml)
│ │ └── query # Overpass query for the region
│ ├── destination_csvs # Overpass queries for destination POIs
│ ├── census # Assign census-tract population to graph nodes
│ ├── od_generation # Origin–destination travel demand
│ │ └── config # Demand scenario parameters
│ ├── corridor_analysis # Low-stress islands and missing-link corridors
│ ├── node_accessibility # Per-node accessibility, sharding, and maps
│ ├── one_way_evaluation # Route asymmetry from one-way streets
│ └── road_usage # Path counts and usage metrics
│ └── svgs # Standalone plotting scripts
├── docs # Documentation + Sphinx auto-doc setup
├── jobs # Slurm job definitions for cluster runs
├── scripts # Shell wrappers for running each analysis
├── tests # Unit tests
└── .github # Issue templates and CI workflows