Predicting travel demand to help understand urban traffic patterns and alleviate congestion.
This repository tackles a travel-demand forecasting problem: given a city's historical
demand aggregated into geohash locations over 15-minute buckets, predict the
normalized demand (a value in [0, 1]) for a held-out future window.
The data is a spatio-temporal time series, not a table of independent rows — and the solution is built around that fact. The headline pipeline is a stacked ensemble of five gradient-boosted / tree models on top of rich geohash×time features.
Evaluation metric:
accuracy = max(0, 100 × R²(actual, predicted))
Demand hotspot map — brighter = higher average demand. The spatial clustering validates geohash-based features.
Spatial evolution of demand across the day — the same hotspots intensify from night to morning.
Explore demand predictions interactively — no setup required.
HuggingFace Spaces | Streamlit Cloud
Select a geohash, hour, road type, and weather condition to see the predicted demand, a 24-hour profile chart, a map pin, and location statistics. Both apps run on precomputed aggregated profiles (no raw data is exposed).
| File | Rows × Cols | Description |
|---|---|---|
train.csv |
77,299 × 11 | Historical demand with context features |
test.csv |
41,778 × 10 | Records to forecast (target hidden) |
sample_submission.csv |
5 × 2 | Submission format (Index, demand) |
Columns
| Column | Meaning |
|---|---|
Index |
Unique row identifier |
geohash |
Geocoded location (decodes to lat/lon; adjacency preserved) |
day |
Sequential day index (not a calendar date) |
timestamp |
Time of day, H:M, on a 15-minute grid |
RoadType, NumberofLanes, LargeVehicles, Landmarks |
Road context |
Temperature, Weather |
Environmental context |
demand |
Target — normalized traffic demand in [0, 1] |
Structure that matters: training spans one full reference day plus the early hours of the next; the test set continues that next day through the daytime. The same geohash locations recur across train and test, and demand follows a smooth, location-specific time-of-day profile — the dominant predictive signal.
Built jointly on train + test so every encoding is consistent.
- Spatial — geohash decoded to latitude/longitude, region prefixes, and K-means geo-clusters grouping nearby locations.
- Temporal — minutes-of-day, hour/minute, cyclical sin/cos encodings, day index, day-of-week.
- Demand profile (core signal) — leak-free K-fold target encodings of mean demand
at multiple granularities (
geohash,geohash×hour,geohash×slot, region×hour, …), a denoised reference-day time-of-day profile, and a per-geohash recent level. - Spatial spillover — mean demand of each location's k nearest geohash neighbours at the same time slot (geohash adjacency is preserved), capturing local diffusion.
- Context — road and weather attributes (numeric + categorical).
The chart above is the model's permutation importance — the reference-day time-of-day profile and geohash×time demand encodings carry the signal, confirming the spatio-temporal framing.
src/solution.py— single end-to-end pipeline: gradient-boosted trees (LightGBM, automatic fallback to scikit-learn HistGradientBoosting), trained in log space, predictions clipped to[0, 1], bagged over 3 seeds × 5 folds.notebooks/02_stacked_ensemble.ipynb— the headline model: out-of-fold stacking of LightGBM + XGBoost + CatBoost + HistGradientBoosting + ExtraTrees, combined by a Ridge meta-learner. Boosters usegeohashand other categoricals natively.notebooks/07_model_comparison.ipynb— head-to-head comparison of all five model families trained independently, showing why stacking outperforms: a prediction correlation matrix reveals the error diversity that the meta-learner exploits.
A forward holdout (train on the reference day, predict the held-out next-day records) is used for model selection — a random K-fold split is optimistic for time-series data and is reported only for reference.
| Approach | Accuracy |
|---|---|
| Persistence baseline | ~80 |
Single GBM (solution.py) |
~90 |
Stacked ensemble (02_stacked_ensemble.ipynb) |
97.85 |
Each row adds one feature group — geohash×time target encodings provide the largest single lift.
Stacking beats every individual model — the Ridge meta-learner exploits diverse error patterns across five model families.
OOF prediction correlation — lower off-diagonal values mean more diverse errors, which is what stacking exploits.
Errors peak during high-demand hours; the model slightly under-predicts demand spikes.
Validation uses a forward holdout (train on the reference day, predict the held-out
next-day records) so reported numbers reflect true forecasting performance rather than an
optimistic random split. See docs/APPROACH.md for the full
methodology, EDA, and ablations.
Each row adds one feature group — geohash×time target encodings provide the single largest lift.
Errors peak during high-demand hours; the model slightly under-predicts demand spikes.
---# 1. clone & install
git clone https://github.com/USERNAME/traffic-demand-prediction.git
cd traffic-demand-prediction
pip install -r requirements.txt # LightGBM/XGBoost/CatBoost optional; HGBR fallback works
# 2. place the data
# put train.csv, test.csv, sample_submission.csv in ./data/
# 3a. run the single-model pipeline
python src/solution.py # writes submission.csv
# 3b. or run the stacked ensemble
jupyter notebook notebooks/02_stacked_ensemble.ipynbBoth produce a submission.csv of shape (41778, 2) with columns Index, demand,
predictions clipped to [0, 1].
| Command | What it does |
|---|---|
make train |
Run the single-model pipeline → submission.csv |
make ensemble |
Execute the stacked-ensemble notebook |
make test |
CI smoke test on synthetic data |
make lint |
Run ruff + mypy |
python tests/validate_data.py |
Schema & quality checks on data/*.csv |
docker build -t traffic . && docker run traffic |
Containerized smoke test |
traffic-demand-prediction/
├── README.md
├── LICENSE
├── Makefile # one-command: make train / make test
├── Dockerfile # reproducible container
├── CHANGELOG.md # iteration history
├── CONTRIBUTING.md # how to contribute
├── requirements.txt
├── .gitignore
├── .pre-commit-config.yaml # auto-format on commit (ruff)
├── .github/
│ ├── workflows/ci.yml # CI workflow (build badge)
│ └── ISSUE_TEMPLATE/ # bug report + feature request
├── app/
│ ├── streamlit_app.py # interactive demo (glass-morphism UI)
│ ├── data/profiles_*.csv|json # precomputed profiles (no raw data)
│ └── .streamlit/config.toml # dark theme
├── src/
│ ├── solution.py # end-to-end pipeline (LightGBM → HGBR)
│ └── feature_engineering.py # shared feature builder (42 features)
├── notebooks/
│ ├── 01_eda.ipynb # exploratory data analysis
│ ├── 02_stacked_ensemble.ipynb # 5-model stacked ensemble (headline)
│ ├── 03_recursive_forecast.ipynb # autoregressive experiment
│ ├── 04_error_analysis.ipynb # residual analysis by hour/road/weather
│ ├── 05_ablation_study.ipynb # feature-group contribution study
│ └── 06_geospatial_visualization.ipynb # demand heatmaps & clusters
│ ├── 07_model_comparison.ipynb # individual models vs stacked ensemble
├── tests/
│ ├── make_synth_and_check.py # CI smoke test
│ └── validate_data.py # schema & quality checks
├── docs/
│ └── APPROACH.md # full methodology
├── assets/ # SVG diagrams + generated PNG charts
└── data/.gitkeep # local data (git-ignored)
![]() |
![]() |
| Decoded geohash grid | K-means geo-clusters (k=30) |
![]() |
![]() |
| Own vs. neighbour demand | Residual distribution |
| Layer | Technologies |
|---|---|
| Core ML | Python, pandas, NumPy, scikit-learn |
| Gradient boosting | LightGBM, XGBoost, CatBoost |
| Ensemble | Ridge meta-learner, K-Fold OOF stacking |
| Visualization | matplotlib (dark-themed spatial heatmaps) |
| Interactive demo | Streamlit (glass-morphism CSS, profile serving) |
| CI/CD | GitHub Actions |
| Containerization | Docker |
| Code quality | ruff, pre-commit hooks |
| Build automation | GNU Make |
Released under the MIT License.









