XGBoost Forecaster API
XGBoost Substation Forecaster
This package implements an XGBoost-based model to forecast power flows at NGED primary substations
using numerical weather prediction (NWP) forecasts. It implements the BaseForecaster interface
defined in ml_core.
How it works
One xgb.Booster is trained per time_series_id, so each substation's model can learn its own
relationship between weather and power. Features are passed via the AllFeatures schema (see
contracts), which joins NWP variables, power lag and rolling features, and static metadata. A
feature is one input column offered to the model: a lag feature is the power measured a fixed
interval before the target time, and a rolling feature is a statistic over a recent window of that
power. Categorical and string columns are encoded as integer codes before being handed to XGBoost;
all features are cast to Float32, and missing values are left as NaN so XGBoost handles them
natively. ECMWF publishes ~51 members of the same weather forecast — one unperturbed control
member plus ~50 perturbed members — and an ensemble forecast is one power prediction per member.
The model is deterministic, and an ensemble forecast still comes out of that deterministic model
— the XGBoostForecaster class below says how.
Both train() and predict() collect their input once, so keeping that collect bounded is the
caller's job. The dominant cost is the multi-tens-of-GB NWP scan, which has to be pruned at the
inputs and streamed. Feature engineering joins and upsamples the raw inputs into AllFeatures,
and plans that work lazily, so filtering the engineered output cannot prune the NWP scan. The
train and predict docstrings below give the mechanics. For the dataset sizes and the table of
which predicates actually prune the NWP scan, see Bounding feature-engineering
memory.
Save format
XGBoostForecaster.save(path) writes:
{time_series_id}.ubj— one XGBoost native binary model per trained substationmeta.json— the fullXGBoostConfigserialised via Pydantic, the trainedtime_series_idpopulation, and the model class, soload()is completely self-contained
Configuration
XGBoostConfig extends BaseForecasterConfig with XGBoost hyperparameters (n_estimators,
learning_rate, max_depth, etc.). BaseForecasterConfig contributes selected_features,
random_seed (threaded into XGBoost's own seed parameter for deterministic training), the
experiment-identity fields experiment_name and ml_flow_experiment_id, and the leaderboard tag
fields weather_source and training_strategy, which label a run so the leaderboard can compare
like with like. Model-family identity — MODEL_NAME ("xgboost") and MODEL_VERSION —
lives on the XGBoostForecaster class itself, not in the config; both a config's experiment
identity and the class's model-family identity are stamped onto every row of the PowerForecast
output, so the Delta Lake table is self-describing. XGBoostConfig inherits extra="forbid" from
BaseForecasterConfig, so a key that names neither an XGBoostConfig field nor an inherited
BaseForecasterConfig field raises ValidationError rather than being silently ignored.
xgboost_forecaster.forecaster
XGBoost-based power forecasting model.
Classes
XGBoostConfig
Bases: BaseForecasterConfig
Configuration for XGBoostForecaster.
Inherits the universal fields (selected_features, MLflow experiment id) from BaseForecasterConfig and adds XGBoost-specific hyperparameters.
Source code in packages/xgboost_forecaster/src/xgboost_forecaster/forecaster.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | |
Attributes
n_estimators = 1000
class-attribute
instance-attribute
learning_rate = 0.05
class-attribute
instance-attribute
max_depth = 6
class-attribute
instance-attribute
min_child_weight = 1
class-attribute
instance-attribute
subsample = 0.8
class-attribute
instance-attribute
colsample_bytree = 0.8
class-attribute
instance-attribute
device = 'cpu'
class-attribute
instance-attribute
objective = 'reg:squarederror'
class-attribute
instance-attribute
model_config = ConfigDict(extra='forbid')
class-attribute
instance-attribute
selected_features
instance-attribute
ml_flow_experiment_id = None
class-attribute
instance-attribute
experiment_name = ''
class-attribute
instance-attribute
weather_source = ''
class-attribute
instance-attribute
training_strategy = ''
class-attribute
instance-attribute
random_seed = 0
class-attribute
instance-attribute
Methods:
to_xgb_params()
Return the params dict accepted by xgb.train().
Source code in packages/xgboost_forecaster/src/xgboost_forecaster/forecaster.py
52 53 54 55 56 57 58 59 60 61 62 63 | |
XGBoostForecaster
Bases: BaseForecaster
Trains and serves one XGBoost Booster per time_series_id.
All lead times for a given time_series_id are handled by a single Booster. The model is
deterministic; ensemble forecasts arise because each NWP ensemble member's weather is a
separate row through the relevant Booster. predict scores every ensemble member present
in its input in one call. predict groups by time_series_id and dispatches each group
to its Booster.
Save layout: a directory holding one {time_series_id}.ubj file per trained Booster, plus
a meta.json. meta.json stores the full XGBoostConfig, the trained time_series_id
population, and the model class, so that load() is self-contained.
Source code in packages/xgboost_forecaster/src/xgboost_forecaster/forecaster.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | |
Attributes
MODEL_NAME = 'xgboost'
class-attribute
instance-attribute
MODEL_VERSION = 1
class-attribute
instance-attribute
CONFIG_CLASS = XGBoostConfig
class-attribute
model_params
instance-attribute
trained_time_series_ids
property
The sorted time_series_ids this forecaster will serve a predict for.
For XGBoostForecaster, the population is exactly the set of series it holds a trained
Booster for, one Booster per time_series_id. predict ignores rows for any other
series. save() records this set in meta.json, which is what load() reads it
back from. A model scores exactly the series it was trained on, no more and no fewer:
that rule is the train==predict population invariant. See
BaseForecaster.trained_time_series_ids for the model-agnostic contract this
implements.
feature_engineer = TabularFeatureEngineer()
class-attribute
The feature pipeline this forecaster's data is engineered through.
the forecaster references a feature engineer rather than
implementing feature engineering. A forecaster can therefore swap the whole pipeline by
overriding this attribute with a different FeatureEngineer. The default produces the
tabular AllFeatures
frame that train/predict consume.
Methods:
__init__(model_params)
Start with no trained boosters; train populates one per time_series_id.
Source code in packages/xgboost_forecaster/src/xgboost_forecaster/forecaster.py
86 87 88 89 | |
train(data, time_series_ids)
Fit one Booster per time_series_id in time_series_ids.
data is collected once and grouped in memory by time_series_id. Each group's rows
feed an xgb.QuantileDMatrix, which compresses them to 8-bit quantile bins rather than
holding an uncompressed Float32 copy. Keeping the collect bounded is the caller's job.
The NWP scan must be pruned at the inputs: the control member, the relevant H3 cells, and
the window's init_time partitions. An ECMWF ensemble run holds one unperturbed
control member plus ~50 perturbed members, and training uses the control member alone.
Filtering the engineered output cannot prune the upstream join/upsample. See
load_engineering_inputs and "Bounding feature-engineering memory: prune the inputs,
not the output" in
https://openclimatefix.github.io/nged-substation-forecast/architecture/performance/#bounding-feature-engineering-memory-prune-the-inputs-not-the-output.
Only the requested time_series_ids are trained. A requested series with no non-null
power rows does not appear, and gets no Booster. A series can lack those rows because
none fall in the training window.
Source code in packages/xgboost_forecaster/src/xgboost_forecaster/forecaster.py
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
predict(data, *, fold_id='live')
Generate one power_fcst per row, dispatching by time_series_id to the right Booster.
data is collected once and grouped in memory by time_series_id. This model
ignores rows for any time_series_id it was not trained on, and only scores its own
trained population (see trained_time_series_ids). Keeping the collect bounded is the
caller's job. Training reads the control member alone, but at validation the full
~51-member NWP ensemble is present, so the caller predicts one init_time chunk at a
time. The caller appends each chunk to Delta as it goes. init_time is one of the NWP
table's two partition columns, and it is the axis that fans the output out across runs.
Chunking on init_time therefore bounds each iteration's forecast frame, and still
reads every partition exactly once. Looping per H3 cell instead runs out of memory on the
busiest cell. See cv_power_forecasts and
https://openclimatefix.github.io/nged-substation-forecast/architecture/performance/#bounding-feature-engineering-memory-prune-the-inputs-not-the-output.
fold_id is stamped onto every output row (the model has no inherent fold; the caller
supplies it). Defaults to the "live" production sentinel.
Source code in packages/xgboost_forecaster/src/xgboost_forecaster/forecaster.py
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
save(path)
Save all Boosters as .ubj files plus a meta.json with the full config.
Clears path first. The dropped series' boosters can then never be left behind on
disk, even when re-saving a model trained on fewer series over a directory that already
holds the .ubj files of a model trained on more (issue #197). The same
replace-don't-merge property holds through MLflow, because
BaseForecaster.save_to_mlflow uploads this directory as a single archive artifact.
Source code in packages/xgboost_forecaster/src/xgboost_forecaster/forecaster.py
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | |
load(path)
classmethod
Reconstruct an XGBoostForecaster from a saved directory.
meta.json's trained_time_series_ids decides the population, not whatever .ubj
files happen to be in the directory. A model's population is the model's own frozen
record, not a directory listing (issue #197). A directory can hold files this model did
not write. ml_core.base_forecaster.save_to_mlflow adds a
time_series_metadata.parquet. ml_core.production_helpers.fetch_model_artifacts
adds a promotion.json. A hand-assembled directory can hold any file at all. Globbing
would let one of those files enlarge the population. The model would then silently score
a series it was never trained for, breaking the train==predict invariant (see
BaseForecaster.trained_time_series_ids).
Source code in packages/xgboost_forecaster/src/xgboost_forecaster/forecaster.py
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | |
save_to_mlflow(run_id, *, time_series_metadata)
Upload this trained model to the given MLflow run, as one replaceable archive.
Writes the model to a temporary directory via save (the subclass's own format), then
adds the frozen metadata copy (write_trained_metadata). Packs that directory into a
single model.tar.gz, and logs that one file to the run's artifact root. Logging one
archive rather than a directory of files is what makes a re-upload replace the previous
model instead of merging with it — see _MLFLOW_MODEL_ARTIFACT. The caller is responsible
for setting the tracking URI (mlflow.set_tracking_uri) beforehand.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run_id
|
str
|
The MLflow run to attach the artifact to. |
required |
time_series_metadata
|
DataFrame[TimeSeriesMetadata]
|
The roster rows this model was engineered against. Required,
because a model uploaded without them cannot be promoted — see
|
required |
Source code in packages/ml_core/src/ml_core/base_forecaster.py
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | |
load_from_mlflow(run_id)
classmethod
Download a trained model's archive from an MLflow run, unpack it and load it.
Downloads into a temporary directory and loads from there. There is deliberately no
local-disk cache. A CV fold run is reused across re-materialisations:
get_or_create_fold_run resolves the same run for every re-run of a fold's partition, and
each re-run overwrites that run's single model archive. One run_id therefore names
different model bytes at different times, so a cache keyed by run_id would not be unique
for its contents. Production inference makes no MLflow call at all either. Full rationale:
https://openclimatefix.github.io/nged-substation-forecast/architecture/ml-orchestration/#why-there-is-no-local-cache.
The caller is responsible for setting the tracking URI (mlflow.set_tracking_uri)
beforehand.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run_id
|
str
|
The MLflow run the model was saved under. |
required |
Returns:
| Type | Description |
|---|---|
Self
|
The reconstructed, trained forecaster. |
Source code in packages/ml_core/src/ml_core/base_forecaster.py
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | |