ToolsPostgreSQL · SQL · Python
Data13.2M trips, full-year 2024
TechniquesWindow functions · CTEs · Self-joins
RoleEnd-to-end (solo)
View code & SQL on GitHub

A SQL + Python analysis of Montreal’s BIXI bike-share network over the full 2024 open dataset (13.2M trips). The goal wasn’t just to describe ridership — it was to turn a raw 2.6 GB CSV into operational recommendations a BIXI dispatch team could actually act on: when to staff, which stations to rebalance, and where to invest.

01 The problem

BIXI runs 1,100+ stations across Montreal. Bikes naturally pile up at some stations and run dry at others, forcing manual truck-based rebalancing. Three operational questions drove the analysis:

  • When does demand peak, so staffing and rebalancing can be scheduled around it?
  • Which stations are chronically overloaded and need bikes removed?
  • Which routes and areas drive the most ridership, to prioritise investment?

02 How it was built

Data modeling

Raw CSV → staging table → a normalised schema: stations (1,134 unique, deduplicated by name) and trips (13.2M valid; ~73K rows with missing station data dropped, ~0.56%). Indexes on the station and time columns keep the aggregation-heavy queries fast. One real gotcha: timestamps needed an explicit AT TIME ZONE 'America/Montreal' conversion — the initial load stored them in the server’s default zone, which would have quietly skewed the entire hourly pattern.

Analysis techniques

01

Window functions

7-day moving average of daily ridership; RANK() OVER (PARTITION BY …) for the top stations per arrondissement.

02

CTEs

Net bike flow (arrivals − departures) per station — the backbone of the dispatch recommendation.

03

Self-join

Most popular station-pair routes, joining trips back to stations twice.

04

Time-series aggregation

Hourly and seasonal ridership patterns.

The dispatch recommendation rests on one CTE query — arrivals minus departures per station. A positive net flow means bikes accumulate (remove them); negative means the station runs dry (add them):

WITH departures AS (
    SELECT start_station_id AS station_id, COUNT(*) AS total_departures
    FROM trips GROUP BY start_station_id
),
arrivals AS (
    SELECT end_station_id AS station_id, COUNT(*) AS total_arrivals
    FROM trips GROUP BY end_station_id
)
SELECT
    s.station_name,
    s.arrondissement,
    COALESCE(a.total_arrivals, 0)
      - COALESCE(d.total_departures, 0) AS net_flow
FROM stations s
LEFT JOIN departures d ON s.station_id = d.station_id
LEFT JOIN arrivals   a ON s.station_id = a.station_id
ORDER BY net_flow DESC;

03 What the data showed

A commuter pattern, not just leisure

Ridership peaks sharply at 8am and 5pm on a 24-hour cycle — classic work-commute behaviour rather than purely recreational use. This is what makes pre-peak rebalancing windows worth staffing.

Trips by hour of day — clear 8am / 5pm commuter peaks.
Trips by hour of day — clear 8am / 5pm commuter peaks.

Extreme seasonality — and a shift in rider type

Summer ridership (5.93M trips) is ~23× winter (252K). Yet average trip duration is longer in winter (20.9 min vs. 17.2) — suggesting winter riders are a smaller, more committed group of true commuters, while summer volume includes many short casual trips.

Total trips and average duration by season.
Total trips and average duration by season.

Ridership concentrated around Mont-Royal / Plateau

The busiest station-pair routes are almost all anchored around Mont-Royal metro and the Plateau — Montreal’s densest, most bike-friendly residential area.

Top 10 busiest station pairs (routes).
Top 10 busiest station pairs (routes).

Dispatch priority: Ville-Marie is chronically overloaded

Stations in Ville-Marie (downtown / Old Port) show the largest positive net flow — bikes arrive far more than they leave. Trucks should proactively remove bikes here rather than wait for stations to fill.

Top stations by net bike surplus — the dispatch shortlist.
Top stations by net bike surplus — the dispatch shortlist.

04 Recommendations

  • Staffing: concentrate rebalancing crews in the pre-peak windows (6–7am, 3–4pm) to get ahead of the 8am / 5pm surges.
  • Dispatch routing: prioritise downtown Ville-Marie stations for bike removal — a standing daily route beats reactive dispatch.
  • Seasonal operations: given the ~23× volume swing, winter fleet size could be cut substantially without hurting service, freeing bikes for maintenance.

05 Limitations

  • No membership or bike-ID data in the 2024 export, so rider-level segmentation (member vs. casual) wasn’t possible.
  • Findings reflect 2024 only; multi-year trend analysis would require reconciling BIXI’s different historical data formats.

The full write-up, all SQL, and the Python chart scripts are on GitHub.

View on GitHub