Rolaficus's picture
Sync from GitHub Actions
47d42b1 verified
Raw History Blame Contribute Delete
5.63 kB
"""Model training and evaluation utilities for train_xgboost."""
from __future__ import annotations
import logging
import numpy as np
import pandas as pd
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from xgboost import XGBRegressor
LOGGER = logging.getLogger("static_xgboost")
def train_and_evaluate(
x: pd.DataFrame,
y: pd.Series,
timestamps: pd.Series,
test_year: int,
) -> tuple[XGBRegressor, dict[str, float], pd.DataFrame]:
"""Train using pre-test-year rows and evaluate on rows in ``test_year``."""
years = pd.to_datetime(timestamps, errors="coerce", utc=True).dt.year
test_mask = years == test_year
train_mask = years < test_year
if int(test_mask.sum()) == 0:
raise ValueError(f"No rows found for test_year={test_year}.")
if int(train_mask.sum()) == 0:
raise ValueError(f"No train rows found before test_year={test_year}.")
x_train, x_test = x.loc[train_mask], x.loc[test_mask]
y_train, y_test = y.loc[train_mask], y.loc[test_mask]
model = XGBRegressor(
objective="reg:squarederror",
n_estimators=500,
max_depth=6,
learning_rate=0.05,
subsample=0.9,
colsample_bytree=0.9,
random_state=42,
n_jobs=-1,
)
model.fit(x_train, y_train)
pred_test = model.predict(x_test)
metrics = {
"train_rows": int(len(x_train)),
"test_rows": int(len(x_test)),
"test_year": int(test_year),
"mae": float(mean_absolute_error(y_test, pred_test)),
"rmse": float(np.sqrt(mean_squared_error(y_test, pred_test))),
"r2": float(r2_score(y_test, pred_test)),
}
pred_df = pd.DataFrame(
{
"actual": y_test.to_numpy(),
"predicted": pred_test,
"residual": y_test.to_numpy() - pred_test,
}
)
return model, metrics, pred_df
def train_and_evaluate_explicit_split(
x_train: pd.DataFrame,
y_train: pd.Series,
x_test: pd.DataFrame,
y_test: pd.Series,
test_descriptor: str,
) -> tuple[XGBRegressor, dict[str, float], pd.DataFrame]:
"""Train on explicit train split and evaluate on explicit test split."""
model = XGBRegressor(
objective="reg:squarederror",
n_estimators=500,
max_depth=6,
learning_rate=0.05,
subsample=0.9,
colsample_bytree=0.9,
random_state=42,
n_jobs=-1,
)
model.fit(x_train, y_train)
pred_test = model.predict(x_test)
metrics = {
"train_rows": int(len(x_train)),
"test_rows": int(len(x_test)),
"test_descriptor": test_descriptor,
"mae": float(mean_absolute_error(y_test, pred_test)),
"rmse": float(np.sqrt(mean_squared_error(y_test, pred_test))),
"r2": float(r2_score(y_test, pred_test)),
}
pred_df = pd.DataFrame(
{
"actual": y_test.to_numpy(),
"predicted": pred_test,
"residual": y_test.to_numpy() - pred_test,
}
)
return model, metrics, pred_df
def rolling_backtest(
x: pd.DataFrame,
y: pd.Series,
train_size: int,
test_size: int,
step_size: int,
max_folds: int,
) -> tuple[pd.DataFrame, dict[str, float]]:
"""Run fixed-window rolling backtest and return fold metrics + summary."""
total_rows = len(x)
if total_rows < train_size + test_size:
raise ValueError(
f"Not enough rows for rolling backtest: rows={total_rows}, "
f"train_size={train_size}, test_size={test_size}."
)
rows: list[dict[str, float | int]] = []
fold = 0
train_start = 0
while fold < max_folds:
train_end = train_start + train_size
test_end = train_end + test_size
if test_end > total_rows:
break
x_train = x.iloc[train_start:train_end]
y_train = y.iloc[train_start:train_end]
x_test = x.iloc[train_end:test_end]
y_test = y.iloc[train_end:test_end]
model = XGBRegressor(
objective="reg:squarederror",
n_estimators=500,
max_depth=6,
learning_rate=0.05,
subsample=0.9,
colsample_bytree=0.9,
random_state=42,
n_jobs=-1,
)
model.fit(x_train, y_train)
preds = model.predict(x_test)
rows.append(
{
"fold": fold + 1,
"train_start": int(train_start),
"train_end": int(train_end - 1),
"test_start": int(train_end),
"test_end": int(test_end - 1),
"train_rows": int(len(x_train)),
"test_rows": int(len(x_test)),
"mae": float(mean_absolute_error(y_test, preds)),
"rmse": float(np.sqrt(mean_squared_error(y_test, preds))),
"r2": float(r2_score(y_test, preds)),
}
)
fold += 1
train_start += step_size
if not rows:
raise RuntimeError("Rolling backtest produced no folds.")
folds_df = pd.DataFrame(rows)
summary = {
"folds": int(len(folds_df)),
"train_size": int(train_size),
"test_size": int(test_size),
"step_size": int(step_size),
"mae_mean": float(folds_df["mae"].mean()),
"mae_std": float(folds_df["mae"].std(ddof=0)),
"rmse_mean": float(folds_df["rmse"].mean()),
"rmse_std": float(folds_df["rmse"].std(ddof=0)),
"r2_mean": float(folds_df["r2"].mean()),
"r2_std": float(folds_df["r2"].std(ddof=0)),
}
return folds_df, summary