Skip to content

Repository files navigation

RidePulse — Predicting Citi Bike demand surges

Will this bike station get slammed in the next hour? RidePulse predicts, for every active Citi Bike station in New York City, whether the next hour will bring an unusual surge in ride demand — so an operator could rebalance bikes before a station empties out.

End-to-end machine learning on real, public data: the model trains on millions of actual Citi Bike trips, and the live demo scores every station off the real-time station feed and the current weather forecast.

  • Task: binary classification (surge / no-surge), evaluated honestly on a forward-in-time hold-out.
  • Result: PyTorch MLP at ROC-AUC 0.795 on unseen days, clearly beating baselines.
  • Live: a Streamlit map + FastAPI endpoint scoring all ~1,986 active stations for the upcoming hour.

Table of contents

  1. Problem
  2. Data
  3. Methodology
  4. Results
  5. Project structure
  6. Quickstart
  7. Live serving
  8. Reproducibility
  9. Limitations
  10. License

The problem

Bike-share systems live and die on availability. A station that runs dry at rush hour is a rider who walks — and a rebalancing truck dispatched too late. The operational question is a forecasting one: which stations are about to spike in demand?

We frame it as binary classification:

For a given station and hour, will the number of rides starting in that hour meet or exceed the station's own busy-hour threshold (its 75th-percentile hourly demand)?

Defining "surge" relative to each station (not "big station vs small station") makes the signal useful: it flags a station about to be busier than usual for that station. At serve time you simply run it for the upcoming hour.

The data (all real, all public)

SourceWhatUse
Citi Bike system dataOfficial monthly trip archives (every real ride: start/end station, timestamps)Training — Jan 2024, ~1.89M rides
GBFS real-time feedstation_information (capacity, coords) + station_status (live bikes/docks)Station metadata + live demo
Open-MeteoHourly temperature, precipitation, wind (historical + forecast)Weather features

No synthetic data anywhere.

Data dictionary (processed station_hours.parquet)

ColumnTypeDescription
start_station_namestrStation (join key)
hour_tsdatetimeHour bucket (local NY time)
splitstrtrain (Jan 1–25) / test (Jan 26–31)
ridesintRide-starts in this station-hour (label source)
thrintStation's 75th-pct hourly demand (train only, floor 1)
surge_next_hourintTarget — 1 if rides >= thr
hour, dow, is_weekend, monthintCalendar features
capacity, lat, lonfloatStation metadata (from GBFS)
hist_mean_station_hourfloatTrain-only mean rides for this station × hour-of-week
temp_c, precip_mm, wind_kphfloatWeather

Methodology

  1. Ingest & aggregate (prepare_data.py) — parse trips, floor starts to the hour, count ride-starts per (station, hour), and build a complete station×hour grid so hours with zero demand are modeled, not dropped.
  2. Label without leakage — the surge threshold and the historical-demand profile are computed on the training split only; every feature is knowable before the hour it describes and is identically computable at serve time (no train/serve skew).
  3. Baselines first, then a neural net (train.py) — majority-class, logistic regression, and gradient boosting before a PyTorch MLP, so the neural net has to earn its place.
  4. Evaluate honestly — surges are the minority class, so accuracy alone would mislead; we lead with ROC-AUC and PR-AUC, report F1/precision/recall/Brier, and plot a calibration curve, always against the majority baseline.

Results

Trained on real Jan 2024 trips (1.89M rides → 1.52M station-hours), evaluated on a time-based hold-out (Jan 26–31, never seen in training). Surge base rate: 31%. Full numbers in reports/metrics.json; plots in reports/figures/.

ModelROC-AUCPR-AUCF1
Majority baseline0.5000.3130.000
Logistic regression0.7650.6090.484
Gradient boosting0.7900.6530.601
PyTorch MLP0.7950.6360.588

The PyTorch MLP has the best ranking performance (ROC-AUC 0.795); gradient boosting is marginally better on the imbalance-sensitive PR-AUC and F1. Both far exceed the majority baseline, which never flags a surge. Honest read: the neural net and a well-tuned tree are effectively tied on this tabular problem — itself a useful finding. See the model card.

ROC

Project structure

ridepulse/
├── LICENSE
├── Makefile # make setup | data | features | train | serve | api
├── README.md
├── requirements.txt
├── pyproject.toml
├── data/ # (git-ignored)
│ ├── raw/ # downloaded trips + live GBFS snapshots
│ ├── interim/
│ └── processed/ # station_hours.parquet
├── docs/
│ └── model_card.md
├── models/ # mlp.pt, scaler.json, serve artifacts
├── notebooks/
├── reports/
│ ├── metrics.json
│ └── figures/ # roc.png, pr.png, calibration.png
└── src/
├── config.py # paths, data sources, task definition
├── download_data.py # fetch real data -> data/raw/
├── prepare_data.py # trips -> station-hour features + labels
├── train.py # baselines + PyTorch MLP + metrics/plots
├── export_serve.py # compact serving artifacts
├── serve.py # live scoring (weather forecast + model)
├── api.py # FastAPI /predict
└── app.py # Streamlit live map

Quickstart

make setup # create .venv and install dependencies
make data # download real Citi Bike + GBFS data (~350 MB) -> data/raw/
make features # build station-hour features + labels -> data/processed/
make train # train baselines + PyTorch MLP -> models/, reports/
make serve # launch the live Streamlit map

Or run the steps directly with .venv/bin/python src/<script>.py. make help lists all targets.

Live serving

The trained model scores the upcoming hour using the real weather forecast — no current-hour ride data needed (by design).

make serve # Streamlit map, stations colored by P(surge)# or the API:
make api # then: curl "localhost:8000/predict?top=25"

Both are stateless and deploy free on Streamlit Community Cloud or Hugging Face Spaces — no Kubernetes or cloud infrastructure (production deployment is a separate project).

Reproducibility

  • Python 3.12+ (developed on 3.13), dependencies pinned in requirements.txt.
  • Deterministic training seed; time-based split defined in config.py.
  • make data && make features && make train regenerates everything from scratch.
  • The committed models/ + reports/ let the demo run without re-downloading the raw data.

Limitations

  • Single winter month → no seasonality; retrain across months for production.
  • "Surge" is relative to each station's own distribution, not an absolute ride count, and predicts demand, not live dock occupancy.
  • See the model card for the full list.

License

MIT (code) — see LICENSE. Trip data © Citi Bike/Lyft under the Citi Bike Data License; weather © Open-Meteo (CC-BY 4.0).

About

Predicting NYC Citi Bike demand surges — end-to-end ML on real data (PyTorch + sklearn, honest eval, live demo)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages