Skip to content

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.

ModelLoss Configuration
HistGBTloss="quantile", quantile=0.55
LightGBMobjective="quantile", alpha=0.55
XGBoostobjective="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)

FeatureFormulaWhy It Matters
weather_cold_x_demandmax(0, 15-temp) x demandCold weather + high demand = expensive heating load
weather_temp_deviationtemp - 15Distance from mild baseline
weather_heating_degree_daysmax(0, 18-temp)Standard heating load proxy
weather_cooling_degree_daysmax(0, temp-24)Cooling load (summer AC)
weather_temp_deviation_sq(temp-15)^2Non-linear: extremes cost more than mild deviations
weather_wind_x_wind_sharewind_speed x wind_shareWind blowing fast AND turbines are a large generation share
weather_precip_x_hydroprecip x hydro/demandRain boosting hydro reservoir output
weather_cloud_x_solarcloud_cover x solar_shareClouds suppressing solar when solar is a large share
weather_sunshine_x_solarsunshine x solar_shareClear skies amplifying solar output
weather_ghidirect + diffuse radiationTotal solar energy reaching the surface

Target-Time Features (at predicted hour)

FeatureFormulaWhy It Matters
target_weather_temp_deviationtemp - 15Expected temperature comfort deviation
target_weather_heating_ddmax(0, 18-temp)Expected heating load
target_weather_cooling_ddmax(0, temp-24)Expected cooling load
target_weather_wind_x_wind_sharewind x wind_shareExpected wind displacement of expensive generators
target_weather_cloud_x_solarcloud x solar_shareExpected solar suppression
target_weather_ghi_x_solarradiation x solar_shareExpected 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

ParameterValue
Loss functionQuantile 0.55
Ensemble3 models (HistGBT, LightGBM, XGBoost)
Features76–78 (57 base + 15 weather interactions + weather raw)
Feature selectionNo
Peak-splitNo

Multi-Approach Training

All 4 forecasting approaches were retrained from scratch with quantile loss + weather features:

ApproachModels TrainedTraining Data
Hourly63+ years hourly (32K rows)
ExpandedUses hourly modelsHourly models expanded to 15-min
Pure1565 months genuine 15-min (14K rows)
Hybrid1563+ 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/batch and /evaluation/timeline/batch now 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:

ApproachRun ModeBaseline MAEv4.1 MAEChangeBaseline Biasv4.1 Bias
HourlyDayahead16.4013.42-18.2%-13.77-8.68
HourlyStrategic20.4220.47+0.2%-17.38-16.93
ExpandedDayahead17.2814.00-19.0%-14.34-8.96
ExpandedStrategic21.2520.99-1.2%-17.78-17.22
Hybrid15Dayahead16.6315.04-9.6%-14.19-10.24
Hybrid15Strategic21.1823.27+9.9%-17.68-20.37
Pure15Dayahead14.0915.00+6.5%-13.46-9.85
Pure15Strategic26.3128.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 all to run all 4 approaches in a single command