Downloads · 30 days
283
41% of all-time downloads
Elpriser/nordic-price-forecast
nordic-price-forecast is a tabular regression model from Elpriser. Use it for the tabular regression task on the model card, and read the license before you ship it in a product. It is set up for lightgbm. The card lists the license as cc-by-4.0.
Day-ahead electricity price forecasts 2 to 9 days out, hourly, in EUR/MWh, for every bidding zone in Denmark, Norway, Sweden, Finland and the Netherlands:
Downloads · 30 days
283
41% of all-time downloads
All-time downloads
696
Public
Repo size
—
Likes
0
Public
Click a slice to open those files.
.txt17.9 MB · 100%
From the Hugging Face model README
Day-ahead electricity price forecasts 2 to 9 days out, hourly, in EUR/MWh, for every bidding zone in Denmark, Norway, Sweden, Finland and the Netherlands:
DK1, DK2 · NO1, NO2, NO3, NO4, NO5 · SE1, SE2, SE3, SE4 · FI · NL
Retrained and re-uploaded daily, so the weights here are what production runs. A companion model specialised to Denmark alone lives at Elpriser/denmark-price-forecast.
A single LightGBM quantile model covers all 13 zones, with zone as a
categorical feature. The zones share nearly all their structure — the same
calendar effects, the same weather physics, the same lag behaviour — so
pooling lets a thin zone borrow strength from a busy one, and leaves one
thing to maintain instead of thirteen.
Pooling only works because the model can identify a zone's price level
before it starts explaining deviations. NO4 averages 22 EUR/MWh and NL 90; a
pooled model on raw prices would otherwise spend its capacity rediscovering
that. Three features carry it: the zone id, last_day_mean, and level_30d
(the zone's own trailing 30-day average, measured to the last known day).
This is the single most important thing to understand about this model.
Percentage MAE punishes cheap zones. NO4's mean absolute error is 14.1 EUR/MWh — lower than DK1's 25.6 — but it reads as 64% because NO4's mean price is only 22 EUR/MWh. Nothing is wrong with the forecast there; the denominator is small.
Min-hour hit-rate measures the daily price spread, not forecast skill. Hit-rate tracks how far apart a day's cheapest and dearest hours are:
| Zone | Median daily spread | Hit-rate |
|---|---|---|
| NL | 128 EUR/MWh | 77% |
| DK1 | 116 | 70% |
| NO3 | 27 | 28% |
| NO4 | 12 (28% of days essentially flat) | 24% |
When the day's hours differ by 12 EUR/MWh, ranking them is close to random — and getting it wrong costs almost nothing.
Use regret instead. Regret is what following the forecast's three cheapest hours costs versus perfect timing. It stays meaningful whatever the zone's price level or spread.
Rolling-origin backtest, monthly retrain, tested April 2025 → July 2026.
| Zone | Mean price | MAE | Regret | Regret, seasonal baseline | Captured |
|---|---|---|---|---|---|
| DK1 | 86.4 | 25.6 | 6.0 | 7.0 | 89% |
| DK2 | 87.8 | 26.9 | 7.2 | 8.3 | 87% |
| NL | 90.2 | 23.5 | 7.8 | 8.6 | 88% |
| SE4 | 70.6 | 27.0 | 7.4 | 8.5 | 83% |
| SE3 | 55.6 | 22.8 | 6.6 | 7.1 | 79% |
| NO2 | 80.4 | 19.0 | 6.6 | 7.2 | 79% |
| NO1 | 75.3 | 20.5 | 7.9 | 8.5 | 73% |
| FI | 49.1 | 31.8 | 10.7 | 12.4 | 65% |
| NO5 | 68.5 | 17.2 | 7.0 | 7.7 | 60% |
| SE1 | 30.2 | 21.3 | 7.0 | 7.5 | 55% |
| SE2 | 30.4 | 22.1 | 7.2 | 7.9 | 54% |
| NO3 | 46.8 | 16.6 | 7.8 | 8.3 | 46% |
| NO4 | 22.2 | 14.1 | 6.0 | 6.5 | 46% |
"Captured" is the share of the available timing saving the forecast captures versus running at a random hour.
Two things to take from this. The model beats a seasonal baseline in all 13 zones on regret. And absolute regret is strikingly uniform — 6 to 8 EUR/MWh almost everywhere, FI's 10.7 the only outlier — so in terms of what a user actually loses by following it, the model performs comparably across a market where mean prices differ fourfold. The lower "captured" figures in NO3, NO4, SE1 and SE2 reflect there being less to capture, not worse forecasts.
Pooled MAE 22.2 EUR/MWh, against 25.7 for the seasonal baseline and 28.2 for persistence. MAE by horizon rises gently from 20.1 (h=2) to 23.6 (h=9).
30 per row, at horizon h (2-9):
zone (categorical), h, hour, weekday, month, is_weekend, doy_sin, doy_coslag1/2/3/7/14/21/28, filled only when known at issue time (k >= h-1), seasonal4w, last_day_mean, level_30dwind, rad, temp, precip, precip_24hreg_wind, reg_temp, hydro_precip, hydro_precip_7dreservoir_pct, reservoir_anom, nordic_reservoir_anomWeather comes from Open-Meteo's previous-runs archive: the forecast as issued N days before delivery, so training rows carry the same information quality the live model gets. Precipitation is included because NO and SE prices are set by reservoir hydrology, where rainfall is a genuine driver in a way it is not in Denmark.
The first backtest had the model losing to a plain seasonal average in NO1 (+1.3%) and NO2 (+8.1%) — the signature of a missing level driver. Nordic prices follow water value: producers hold water back when reservoirs are low and release when full. Adding weekly reservoir levels (ENTSO-E A72, lagged 14 days, expressed as share-of-maximum and as deviation from the zone's median for that week of year) turned NO1 to −1.7% and NO2 to −0.3%. The Nordic aggregate goes to every zone including DK and NL, because the market is coupled and a dry Norwegian year lifts prices well beyond Norway.
Shape blend. The model's daily level is kept, its intraday shape blended 50/50 with a 4-week seasonal profile. Carried over from the Danish model, where that was measured to beat both parents on both accuracy and hour-ranking.
Bands are additive empirical residual quantiles, not scaled model quantiles. Scaling was tried first, as the Danish model does it, and broke down: DK, NL and SE3/SE4 needed a sensible ~1.8-2.0x, but NO4 needed 8.0x and still missed 80% coverage, NO5 up to 7.5x, SE1/SE2 up to ~5x. A band inflated eightfold is not measuring uncertainty, it is being stretched to pass a check. Taking the 10th and 90th percentiles of actual residuals per zone and horizon gives correct coverage by construction.
Every price series was refetched after a parser bug was found. ENTSO-E publishes day-ahead prices as curveType A03 — variable-sized blocks, where a point holds until the next published position — and the parser read only the explicit positions. Each hour whose price repeated the hour before it was therefore dropped: 2,569 hours, and not at random, because the loss scaled with how flat a zone is (2.8% of NO4 against 0.34% of DK1). Expanding the blocks also corrected the hourly mean under the 15-minute market design introduced in October 2025, where the old code weighted each published block equally regardless of its length; that moved 4,656 further hours by 0.27 EUR/MWh on average.
The correction was verified against a source outside this code path: Denmark's correlation with Energinet's independent EDS feed rose from 0.99999 to 1.00000, so the restored hours match an external record rather than being interpolated.
It did not make the model more accurate. Retrained and backtested on the corrected data, MAE rose 1.6% while regret — the cost of following the forecast's three cheapest hours — improved from 5.28 to 5.26 EUR/MWh and the hit-rate fell from 51.7% to 51.3%. The training is deterministic, so these are real differences rather than run-to-run noise; they are simply small. The value of the fix is that the data and the published prices are correct, not that the forecast improved.
Consumer and household decision support — when in the coming week to run flexible load — and research. Not validated for trading or hedging.
CC BY 4.0. Prices and reservoir levels: ENTSO-E Transparency Platform.
Weather: Open-Meteo. Code: elpriser.org on GitHub,
scripts/forecast_model/nordic/.