Live app: https://greengrid-uk-4hou3jvgi9rebmisrvspmh.streamlit.app/
The same kilowatt-hour can cost 5× more CO₂ depending on when you draw it from the GB grid: a windy night might run at ~50 gCO₂/kWh while a calm winter evening peak runs at 300+. GreenGrid predicts the grid's carbon intensity up to 48 hours ahead and tells you the cleanest time to charge an EV, run a washing machine, or cook dinner — turning the UK's net-zero transition into a decision anyone can act on tonight.
This matters at scale: shifting one EV charge into the cleanest window saves kilograms of CO₂ per charge, the behaviour smart tariffs (Octopus Agile, Intelligent Go) already pay for — so the green slot is usually the cheap slot too.
NESO Historic GB Generation Mix — the National Energy System Operator's own published record of the grid:
- 305,711 half-hourly rows, January 2009 → present (refreshed continuously)
- generation by fuel (gas, coal, nuclear, wind, solar, …), absolute and % share
- computed carbon intensity in gCO₂/kWh — our prediction target
The live app additionally pulls recent actuals from the official Carbon Intensity API to build its lag features at prediction time.
Cleaning decisions (full audit in notebook 01)
| Issue checked | Found | Decision |
|---|---|---|
| Nulls / duplicate timestamps | 0 / 0 | nothing to impute |
| Out-of-order rows | 2 (a Dec-2025 append glitch) | sort by DATETIME |
| Gaps in the half-hourly series | 0 after sorting | no imputation needed |
| Implausible target values | 0 (range 19–644 is physically real) | no clipping — trimming "outliers" would erase the real coal era |
| Regime change | coal fell 45% → 0% of generation | train on 2017+ only — a model that learns coal-era physics predicts a grid that no longer exists |
These checks are encoded as assertions in src/data.py, so a future NESO
refresh that breaks any guarantee fails loudly instead of silently corrupting the model.
- The grid decarbonised 3.6× since 2009 (445 → ~125 gCO₂/kWh yearly average) — wind, solar and biomass replaced coal entirely and half of gas.
- The daily cycle is the product: overnight trough (~02:00–05:00) and midday solar dip vs a dirty evening peak (17:00–20:00). Median within-day spread in 2024–26 is ~90 gCO₂/kWh — worth ~4.6 kg CO₂ per 50 kWh EV charge.
- Weekends and summer run cleaner (lower industrial demand; solar) — calendar features carry real signal.
- Wind is the #1 lever: wind share correlates −0.75 with carbon intensity, and wind regimes persist across days — which is why lagged wind share helps at a 48h horizon.
Leakage rule: the app forecasts up to 48h ahead, so every feature must be knowable 48h before the target — all lags look back ≥48h (3h-smoothed; see below for why).
Engineered features: cyclical hour/month encodings, weekend/UK-bank-holiday flags,
3h-smoothed carbon-intensity lags (48h, 1 week), a 24h rolling "grid regime" mean, and
lagged wind & solar shares. Business rationale for each is in
src/features.py.
Split by year (never shuffled): train 2017–2024, validation 2025, test 2026.
| Model (test 2026) | MAE ↓ | RMSE ↓ | green-window hit rate ↑ | green-window regret ↓ |
|---|---|---|---|---|
| persistence (48h ago) | 55.3 | 69.0 | 0.38 | 26.1 |
| climatology (seasonal clock) | 56.9 | 70.6 | 0.61 | 17.3 |
| Ridge | 48.5 | 59.1 | 0.46 | 22.2 |
| LightGBM | 49.2 | 61.0 | 0.43 | 23.4 |
| GreenBlend (shipped) | 50.5 | 62.6 | 0.54 | 19.4 |
MAE/RMSE (in gCO₂/kWh, the units users see) measure curve accuracy. But the product's actual job is picking the right hours, so we also measure:
- green-window hit rate — does the predicted greenest 3h window of each day overlap the actual one?
- green-window regret — how many extra gCO₂/kWh you pay following our window instead of the hindsight-perfect one (the true product cost).
The accuracy metric and the business metric disagreed about which model to ship. The lag-driven models win on MAE but inject 48h-old shape noise into the daily curve, while the humble seasonal average nails the shape (when the trough falls) and misses the level. Shipping on MAE alone would have shipped the worse product. The resolution is GreenBlend = 0.6·climatology + 0.4·LightGBM, with the weight chosen on the 2025 validation year against regret — never on test data. It beats both baselines on MAE and recovers most of climatology's window-picking skill; pure climatology still edges the window metrics, which we report openly — the fix is weather-forecast features (see Production notes).
Streamlit app (app.py): pick an appliance, how long it runs, and how long you can
wait — the model forecasts the next 48h, highlights your cleanest slot, and quantifies the
CO₂ saved vs starting now, alongside the live grid mix. If the live API is down, the app
degrades to seasonal-average forecasts with a warning instead of crashing.
├── app.py # Streamlit app
├── requirements.txt
├── data/ # raw data not committed — see data/README.md
├── models/ # trained artifacts + metrics.json (small, committed)
├── notebooks/
│ ├── 01_eda_cleaning.ipynb # data audit, cleaning decisions, EDA insights
│ └── 02_features_modeling.ipynb # features, models, metric story, error analysis
└── src/
├── data.py # download + clean (with assertions)
├── features.py # feature builder shared by training AND the app
├── live.py # Carbon Intensity API client
└── train.py # full training pipeline -> models/ + metrics.json
git clone https://github.com/jadstrike/greengrid-uk && cd greengrid-uk
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python src/train.py # downloads the NESO data (~60MB) and reproduces models/ + metrics
streamlit run app.py # local appDeployed free on Streamlit Community Cloud:
https://greengrid-uk-4hou3jvgi9rebmisrvspmh.streamlit.app/
(connect the repo at share.streamlit.io, main file app.py).
The committed models/ artifacts mean the cloud app needs no training step.
- Schema/regime drift: NESO column changes are caught by
src/data.pyassertions, but a grid regime change (new interconnectors, mass storage) would silently age the model — needs scheduled retraining (the pipeline is one command) and monitoring of live MAE. - Single upstream dependency: the Carbon Intensity API is unauthenticated with no SLA; the app already degrades to climatology, but a product would cache recent actuals server-side.
- The known gap: without wind/solar forecasts the model cannot beat the seasonal clock at ranking hours within a day. NESO publishes day-ahead wind forecasts — the single highest-value next feature.
- Honest-metric risk: optimising MAE alone would regress the product (see above) — any retraining must gate on regret, not just accuracy.
Contains data from the National Energy System Operator (NESO) and the GB Carbon Intensity API (NESO/Oxford), used under their open data licences. Not affiliated with NESO.