NCSU Parking
Campus parking forecast: public occupancy + events into Postgres, a strong seasonal baseline, and per-lot XGBoost only when it beats that baseline.
Built
A personal forecasting stack for NC State lots. A collector polls the public occupancy API (delta snapshots) and campus events into Postgres; features include 5-minute grids, lags, rolling stats, haversine event radii, and academic-calendar flags. An hour-of-week median is always fit; per-lot XGBoost is kept only if it beats that baseline on validation MAE, across eight horizons from 15 minutes to 24 hours. Training is a separate file-based job so the Streamlit app never loads XGBoost. Private repository — architecture only.
A forecast that can lose
NCSU Parking is a personal stack for campus lots. The public occupancy API and campus events go into Postgres. Features are boring on purpose: 5-minute grids, lags, rolling stats, haversine event radii, academic-calendar flags. Eight horizons, 15 minutes to 24 hours.
Private repository — architecture only. I am not publishing lot-level error tables I cannot stand behind in public.
Baseline first
An hour-of-week median is always fit. Per-lot XGBoost is kept only if it beats that baseline on validation MAE. Most “ML parking” demos skip this gate and then look clever on a Tuesday at 10:00, when the median already knew.
Training is a separate file-based job. The Streamlit app never loads XGBoost. That is not a performance trick. It is how I keep a dashboard from importing a model that failed the gate.
What the events actually add
A football Saturday is not a residual you invent after the fact. Events are collected with the occupancy, given a radius, and become features. If they do not beat the seasonal median, they do not get to stay either.
The lesson I wanted on the page: a model that is not allowed to lose is not a forecast.