v4.1 — Quantile Loss + Weather Interactions
Date: March 2, 2026
What Changed
Two improvements merged into a single release: a loss function change that corrects systematic underprediction, and 15 new features that encode physical relationships between weather and electricity price drivers. All 4 forecasting approaches (hourly, expanded, pure15, hybrid15) were retrained and backtested.
Quantile Loss (q=0.55)
Spanish electricity prices are right-skewed: bounded near zero, with occasional spikes above 200 EUR/MWh. Standard loss functions have a structural problem:
- MSE targets the conditional mean — sits below the median on skewed data
- MAE targets the conditional median — closer, but still below the mean
Quantile loss at q=0.55 targets the 55th percentile, slightly above the median. This directly corrects the underprediction bias without distorting the price forecast shape.
| Model | Loss Configuration |
|---|---|
| HistGBT | loss="quantile", quantile=0.55 |
| LightGBM | objective="quantile", alpha=0.55 |
| XGBoost | objective="reg:quantileerror", quantile_alpha=0.55 |
The optimal quantile was set through cross-validation over [0.50, 0.52, 0.54, 0.56, 0.58].
Weather Interaction Features
Raw weather variables (temperature, wind speed, cloud cover) have no inherent price meaning. A 30 km/h wind tells the model nothing about prices unless it knows how much of the generation mix depends on wind. These 15 interaction features encode the physical relationships that matter:
Origin-Time Features (at forecast time)
| Feature | Formula | Why It Matters |
|---|---|---|
weather_cold_x_demand | max(0, 15-temp) x demand | Cold weather + high demand = expensive heating load |
weather_temp_deviation | temp - 15 | Distance from mild baseline |
weather_heating_degree_days | max(0, 18-temp) | Standard heating load proxy |
weather_cooling_degree_days | max(0, temp-24) | Cooling load (summer AC) |
weather_temp_deviation_sq | (temp-15)^2 | Non-linear: extremes cost more than mild deviations |
weather_wind_x_wind_share | wind_speed x wind_share | Wind blowing fast AND turbines are a large generation share |
weather_precip_x_hydro | precip x hydro/demand | Rain boosting hydro reservoir output |
weather_cloud_x_solar | cloud_cover x solar_share | Clouds suppressing solar when solar is a large share |
weather_sunshine_x_solar | sunshine x solar_share | Clear skies amplifying solar output |
weather_ghi | direct + diffuse radiation | Total solar energy reaching the surface |
Target-Time Features (at predicted hour)
| Feature | Formula | Why It Matters |
|---|---|---|
target_weather_temp_deviation | temp - 15 | Expected temperature comfort deviation |
target_weather_heating_dd | max(0, 18-temp) | Expected heating load |
target_weather_cooling_dd | max(0, temp-24) | Expected cooling load |
target_weather_wind_x_wind_share | wind x wind_share | Expected wind displacement of expensive generators |
target_weather_cloud_x_solar | cloud x solar_share | Expected solar suppression |
target_weather_ghi_x_solar | radiation x solar_share | Expected solar generation value |
These features give the model pre-computed domain knowledge that would otherwise require many tree splits to approximate. For example, weather_wind_x_wind_share directly encodes “wind is blowing fast AND wind is a large share of generation” — something a tree model would need to learn by splitting on both variables independently.
Configuration
| Parameter | Value |
|---|---|
| Loss function | Quantile 0.55 |
| Ensemble | 3 models (HistGBT, LightGBM, XGBoost) |
| Features | 76–78 (57 base + 15 weather interactions + weather raw) |
| Feature selection | No |
| Peak-split | No |
Multi-Approach Training
All 4 forecasting approaches were retrained from scratch with quantile loss + weather features:
| Approach | Models Trained | Training Data |
|---|---|---|
| Hourly | 6 | 3+ years hourly (32K rows) |
| Expanded | Uses hourly models | Hourly models expanded to 15-min |
| Pure15 | 6 | 5 months genuine 15-min (14K rows) |
| Hybrid15 | 6 | 3+ years expanded + 5 months genuine (146K rows) |
Total: 18 models (3 model types x 2 run modes x 3 approaches requiring training).
Bug Fixes
- LazySection spacing: Lazy-loaded dashboard sections now use
flex+gap: 1.5rem, matching parent dashboard spacing - Batch evaluation tag injection:
/evaluation/summary/batchand/evaluation/timeline/batchnow correctly translate tagged model names (e.g.,ensemble_backtest+tag=v4.1->ensemble_v4.1_backtest) - Backtest
--approach all: New flag runs all 4 approaches in a single command
Results
149-day backtest (Oct 2025 – Feb 2026), ensemble models at hourly resolution:
| Approach | Run Mode | Baseline MAE | v4.1 MAE | Change | Baseline Bias | v4.1 Bias |
|---|---|---|---|---|---|---|
| Hourly | Dayahead | 16.40 | 13.42 | -18.2% | -13.77 | -8.68 |
| Hourly | Strategic | 20.42 | 20.47 | +0.2% | -17.38 | -16.93 |
| Expanded | Dayahead | 17.28 | 14.00 | -19.0% | -14.34 | -8.96 |
| Expanded | Strategic | 21.25 | 20.99 | -1.2% | -17.78 | -17.22 |
| Hybrid15 | Dayahead | 16.63 | 15.04 | -9.6% | -14.19 | -10.24 |
| Hybrid15 | Strategic | 21.18 | 23.27 | +9.9% | -17.68 | -20.37 |
| Pure15 | Dayahead | 14.09 | 15.00 | +6.5% | -13.46 | -9.85 |
| Pure15 | Strategic | 26.31 | 28.87 | +9.7% | -25.24 | -28.43 |
Summary: Dayahead MAE improved 18-19% for the main hourly and expanded approaches, with bias reduced by 37%. Hourly and expanded strategic forecasts (D+2-D+7) are essentially flat — expected, since weather interaction features encode local conditions that lose predictive value over multi-day horizons. Pure15 and hybrid15 strategic regressed (~+10% MAE) due to limited training data combined with 15 additional features increasing overfitting risk. Pure15 dayahead also regressed slightly (+6.5%) for the same reason.
Implementation Notes
- Quantile target configured as
QUANTILE_TARGET = 0.55 - Weather interaction features computed during both training and prediction
- Backtest supports
--approach allto run all 4 approaches in a single command