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.
- Problem
- Data
- Methodology
- Results
- Project structure
- Quickstart
- Live serving
- Reproducibility
- Limitations
- License
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.
| Source | What | Use |
|---|---|---|
| Citi Bike system data | Official monthly trip archives (every real ride: start/end station, timestamps) | Training — Jan 2024, ~1.89M rides |
| GBFS real-time feed | station_information (capacity, coords) + station_status (live bikes/docks) | Station metadata + live demo |
| Open-Meteo | Hourly temperature, precipitation, wind (historical + forecast) | Weather features |
No synthetic data anywhere.
| Column | Type | Description |
|---|---|---|
start_station_name | str | Station (join key) |
hour_ts | datetime | Hour bucket (local NY time) |
split | str | train (Jan 1–25) / test (Jan 26–31) |
rides | int | Ride-starts in this station-hour (label source) |
thr | int | Station's 75th-pct hourly demand (train only, floor 1) |
surge_next_hour | int | Target — 1 if rides >= thr |
hour, dow, is_weekend, month | int | Calendar features |
capacity, lat, lon | float | Station metadata (from GBFS) |
hist_mean_station_hour | float | Train-only mean rides for this station × hour-of-week |
temp_c, precip_mm, wind_kph | float | Weather |
- 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. - 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).
- 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. - 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.
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/.
| Model | ROC-AUC | PR-AUC | F1 |
|---|---|---|---|
| Majority baseline | 0.500 | 0.313 | 0.000 |
| Logistic regression | 0.765 | 0.609 | 0.484 |
| Gradient boosting | 0.790 | 0.653 | 0.601 |
| PyTorch MLP | 0.795 | 0.636 | 0.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.
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
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 mapOr run the steps directly with .venv/bin/python src/<script>.py. make help lists all targets.
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).
- 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 trainregenerates everything from scratch.- The committed
models/+reports/let the demo run without re-downloading the raw data.
- 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.
MIT (code) — see LICENSE. Trip data © Citi Bike/Lyft under the Citi Bike Data License; weather © Open-Meteo (CC-BY 4.0).
