尚未检测到更新。GitHub 发布版本后会显示在这里。
Next-day temperature forecasts for Lucerne with calibrated uncertainty. End-to-end MLOps on GitHub Actions: daily forecasts, monitoring, automatic retraining.
翻译准备中
正在加载完整 README。
查看完整 README ↗Next-day temperature forecasts for Lucerne that are 19% more accurate than the ECMWF global weather model, with a calibrated uncertainty range. A model learns ECMWF's local errors from MeteoSwiss measurements, and GitHub Actions runs everything: new data, forecasts and scoring every day, retraining when needed.
Live dashboard: tomorrow's forecast with its 10–90% range, the last 14 days of forecasts against measurements and raw ECMWF, and how accurate the model has been so far. It updates after every daily run.
An end-to-end MLOps pipeline built from scratch, in about 800 lines of Python with four dependencies and no framework hiding the steps:
Stack: Python · LightGBM · pandas · GitHub Actions · pytest · uv
I built it to learn MLOps on real data rather than a synthetic dataset.
On average the corrected forecast is off by 1.22 °C, against 1.50 °C for raw ECMWF. Its 10–90% range contains the measured temperature 81% of the time, close to the 80% it should.
Rolling-origin backtest, March 2025 to October 2026 (14,176 hours, every month predicted by a model trained only on earlier data):
| MAE (°C) | Pinball loss | 10–90% interval holds | |
|---|---|---|---|
| Raw ECMWF forecast | 1.50 | 0.751 | – |
| ECMWF + typical error for that hour | 1.42 | 0.454 | 79% |
| ECMWF + LightGBM correction | 1.22 | 0.387 | 81% |
The correction lowers the error by 19% and beats the raw forecast in 17 of 20 months. The gains are largest from November to April, especially in March and April (about 0.6 °C). Details: reports/backtest.md. Live scores: reports/monitoring.md, and as a chart on the dashboard.
Why correct ECMWF? ECMWF runs one of the best global weather models. Its free open data comes on a 0.25° grid, about 28 km north–south and 19 km east–west at Lucerne (ECMWF, Open-Meteo). A grid cell that size averages lake, town and mountain slopes, so it can't see what makes Lucerne's weather local: the lake, cold air collecting in the basin, or winter fog. Those errors repeat in similar weather, so a model can learn them from past forecasts and measurements.
flowchart LR
M[MeteoSwiss<br/>station LUZ] --> D
E[Open-Meteo<br/>ECMWF forecasts] --> D
subgraph "daily, 09:15 UTC"
D[update data] --> F[forecast tomorrow] --> S[score past forecasts] --> W[publish dashboard]
end
S -- degraded --> R
subgraph "monthly, or when degraded"
R[backtest + train] --> G{better than<br/>baselines and<br/>current model?}
G -- yes --> P[promote]
G -- no --> K[keep current model,<br/>job fails]
end
The model doesn't predict the temperature, but how wrong ECMWF will be: observed minus
forecast. It does that for the 10%, 50% and 90% quantiles: the error is expected to fall
below each in 10%, 50% and 90% of hours, so the 10–90% range should hold 80% of them.
Its inputs are the ECMWF forecast (temperature, dew point, cloud, wind, pressure, rain),
the hour and day of year, and recent_error.
recent_error is how far ECMWF's day-ahead forecasts were off, on average, over the
24 hours before the forecast is made. Errors like that tend to persist: if fog or cold air in
the basin made ECMWF 2 °C too warm today, it will probably be too warm tomorrow as well.
It only uses measurements published by then, so the model never sees the future.
Everything runs in GitHub Actions and is stored in the repo: data as monthly Parquet files, models as LightGBM text files, reports as Markdown. That means no server, no cloud account and no secrets, and every change to data, model and score is versioned.
| Path | Content |
|---|---|
data/observations/ | Measured temperature at LUZ, hourly |
data/archive/ | ECMWF forecasts as issued about a day ahead |
data/forecasts/ | Our published forecasts, for scoring |
forecasts/latest.json | Tomorrow's forecast, at a stable URL |
model/ | Current model and its card (training data, backtest scores) |
You need uv. No API keys. The data and the current model are in the repo, so you can forecast right after cloning:
git clone https://github.com/jonastheiler/lucerne_temp.git
cd lucerne_temp
uv run python -m lucerne_temp.forecast # update the last 3 days of data, forecast tomorrow
uv run python -m lucerne_temp.monitor # score past forecasts
uv run python -m lucerne_temp.site # build the dashboard in site/index.html
uv run python -m lucerne_temp.train # backtest, train, promote if it passes the gate
uv run pytest
To rebuild all data from scratch (~2 min), run
uv run python -m lucerne_temp.data 2024-03-01. Without an end date it goes up to today.
These commands write to data/, forecasts/, reports/ and, if a new model
passes the gate, model/. To undo that:
git restore . && git clean -fd data forecasts reports model.
In the order the code runs:
以上为 README 的开头部分。其余内容请阅读原文。 查看完整 README ↗