All Work
T

Halifax Transit Service Equity Analysis

Spatial & Operational AnalyticsDec 2025

Where is Halifax’s transit network leaving riders underserved?

Role
Sole analyst & developer
Type
Personal project
Status
Complete

Results

Transit records analysed
663,788

GTFS static feed across 11 normalized tables (481,610 stop-time entries).

Stops served by a single route
41%

A connectivity gap: 973 stops (of 2,380) depend on one route only.

Query time, down from 4 s+
<1s

Stop-connectivity view optimized via B-tree and GIST spatial indexes.

In Brief

663,788 GTFS records modelled in PostgreSQL + PostGIS, with a Streamlit dashboard showing where route access, stop connectivity and service thin out across Halifax.

Context

Halifax publishes its transit schedule as an open GTFS feed, but the raw feed is a set of flat files — useful for trip planning, not for asking whether the network serves riders evenly. There was no queryable, spatial view of where coverage thins out.

This project treats the feed as an analytics problem: could a normalized spatial database and dashboard reveal the parts of the network where access, connectivity, and frequency fall short — the kind of evidence a municipal transit planner would need to target improvements?

It is a personal project. The stakeholder is framed, not real, so the recommendations are written as what the analysis suggests rather than a mandate.

Data

Source
Halifax Transit GTFS static feed (public open data)
Period
Schedule snapshot, December 2025
Records
663,788 records across 11 normalized tables
Grain
2,380 stops · 80 routes · 13,666 trips · 481,610 stop-time entries

Quality & Handling

  • Foreign-key integrity checks filtered 10 invalid trips (0.07%) and 65 invalid calendar dates before load.
  • ID fields arrived inconsistently typed (VARCHAR in schema, INTEGER from CSV); all IDs were normalized to strings so joins resolved.
  • Date fields were converted from YYYYMMDD integers to PostgreSQL DATE types.
  • The feed is a scheduled snapshot — it records planned service, not real-time vehicle positions or ridership.

Approach

  1. Ingest the GTFS static feed
  2. Validate & normalize (FK checks, ID/date types)
  3. Load to PostgreSQL + PostGIS
  4. Build SQL analytical views
  5. Spatial analysis (PostGIS)
  6. Temporal analysis
  7. Streamlit dashboard

PostGIS geometry columns are populated automatically from stop coordinates via triggers, and GIST spatial indexes make distance queries fast enough for interactive use.

Output

Stacked bar chart of the top 15 Halifax Transit routes, splitting each route’s departures into morning peak (7–9 AM), evening peak (4–6 PM) and off-peak service.
Peak versus off-peak service distribution across the fifteen busiest routes — Route 1 carries roughly 50% more departures than any other.

Findings

Service is highly concentrated on a few core routes.

Route 1 (Spring Garden) runs about 180 trips per service pattern — far above the next-busiest routes — and the Barrington St hub serves 27 routes.

Why it matters. A handful of trunk routes carry the network. Their reliability disproportionately shapes the rider experience city-wide.

Route frequency, top 10 routes

Route frequency, top 10 routes
RouteAvg trips per service pattern
1180
7B119
7A118
2116
5114
4112
3102
FerD91
886
9A76
Redrawn from the project output (route_frequency.png).

41% of stops are served by a single route.

Of 2,380 stops, 973 are served by only one route, and average connectivity is 2.17 routes per stop. Only 152 stops (6%) act as hubs with five or more routes.

Why it matters. Single-route stops leave riders with no alternative when a route is delayed or cut — a structural fragility in the network.

Caveat. Based on the scheduled GTFS feed, not observed ridership; it measures service supply, not demand.

Stop connectivity distribution

Stop connectivity distribution
Routes serving a stopNumber of stops
1973
2875
3220
4150
552
615
730
818
914
10+33
Redrawn from the project output (stop_distribution.png).

Service peaks sharply at 17:00 with 33,635 departures.

Demand-shaped scheduling produces a clear morning (7–9 AM) and evening (4–6 PM) commuter pattern, with the evening rush the busiest window.

Why it matters. Off-peak and midday coverage is comparatively thin, which shapes access for non-commuter trips.

Departures by hour of day

Departures by hour of day
Hour of dayDepartures
54,100
624,200
731,200
830,800
927,000
1024,400
1124,100
1224,300
1324,300
1425,100
1528,400
1632,500
1733,635
1831,900
1923,200
2021,400
2120,800
2218,400
2315,200
Redrawn from the project output (hourly_service.png).

Sunday service runs 47% below weekday levels.

Weekday scheduled trips (4,195) fall to 2,217 on Sundays — a 47% reduction confirmed directly in the schedule data.

Why it matters. Weekend service reductions disproportionately affect riders who depend on transit for non-standard work hours.

Caveat. Reflects scheduled service; special-event or seasonal adjustments are not modelled.

Scheduled trips by day type

Scheduled trips by day type
Day typeScheduled trips
Weekday4,195
Saturday2,765
Sunday2,217
Redrawn from the project output (weekday_weekend.png).

What the Analysis Suggests

A personal project, so these are readings of the data, not delivered advice.

  1. Prioritize connectivity upgrades at high-isolation single-route stops.

    Evidence
    973 single-route stops concentrated away from the downtown hub cluster.
    Benefit
    Improves network resilience and rider access with targeted, low-cost route overlaps.

Validation

  • Data-quality tests

    A validation layer in the ETL pipeline queried referenced tables to build valid ID sets and filtered the dataframe before insertion, removing invalid trips and calendar dates.

  • Manual spot checks

    Results were checked against known ground truth — Route 1 (Spring Garden) as the highest-frequency route and Barrington St as the busiest transfer hub (27 routes).

  • Query-plan verification

    EXPLAIN ANALYZE identified sequential scans and a disk-based merge sort on the 481,610-row connectivity view; adding B-tree and GIST indexes cut execution from 4 s+ to under 1 s.

  • Reproducibility

    Schema, analytical views, and the ETL pipeline are scripted, so the database can be rebuilt from the raw feed end to end.

Limitations

  • Uses the scheduled GTFS static feed — it describes planned service, not actual vehicle performance or ridership.
  • “Equity” here means spatial service access (coverage, connectivity, isolation); demographic equity was not analysed as no demographic data was joined.
  • A single schedule snapshot; seasonal and special-service variations are out of scope.
  • Isolation is measured as straight-line distance to the nearest neighbouring stop, which is not the same as walking distance over the street network.

My Contribution

  • Designed the PostgreSQL + PostGIS schema and ETL pipeline end to end.
  • Authored the analytical SQL views and the Streamlit dashboard.
  • Diagnosed and fixed the query-performance bottleneck (4 s+ → <1 s).

Technical Notes

Query performance optimization

The stop-connectivity view joined stop_times (481,610 rows) and initially took 4+ seconds, using an external merge sort spilling ~39MB to disk. Adding B-tree indexes on the foreign keys (trip_id, stop_id, route_id, service_id) and GIST indexes on the geometry columns brought it under one second, and Streamlit result caching kept the dashboard responsive.

Analytical SQL views

Four views encapsulate the analysis: route_summary (trips, patterns, unique stops per route), route_efficiency (average trips per service pattern), stop_connectivity (routes per stop, hub identification), and busiest_stops (ranked by daily service volume).

Data model

11 normalized tables follow the GTFS specification with foreign-key constraints. PostGIS geometry columns are auto-populated from lat/lon via triggers, and spatial indexes support ST_Distance proximity queries.

Sources

Data
Halifax Transit GTFS static feed (public open data)
Period
Schedule snapshot, December 2025
Volume
663,788 records across 11 normalized tables

Tables

Tables and inputs used
stopsStop locations; PostGIS geometry auto-populated from lat/lon.2,380 rows
routesRoute identifiers and metadata.80 rows
tripsScheduled trips linking routes to service patterns.13,666 rows
stop_timesArrival/departure per stop per trip — the analytical grain.481,610 rows

Figures on this page

  • 663,788 Transit records analysedReadme.md
  • 41% Stops served by a single routeReadme.md, stop_distribution.png
  • <1 s Query time, down from 4 s+Readme.md