tormenta / src /update.py
aliza-ayaz's picture
Weekly retrain tormenta_v20261106_1027
31abf8d verified
Raw History Blame Contribute Delete
4.41 kB
"""Slow loop: fetch fresh data → rebuild the hourly table → retrain the frozen configuration → gate → promote.
Run weekly by .github/workflows/retrain.yml, or by hand:
python -m src.update # full run: may write models/ and data/f107_history.csv
python -m src.update --dry-run # everything up to the gate decision, writes nothing tracked by git
What is retrained: the FROZEN final configuration (same 66 features, same HistGB settings, same conformal
margin as models/current.joblib). Nothing is re-tuned and the 2020+ test set is not re-scored: the new model
is trained on those years, so its only fair check is the gate holdout (last 6 months of definitive Kp).
Leakage safeguards (from the plan):
- the processed table is rebuilt from scratch each run (never committed);
- the candidate is trained only up to the start of the holdout;
- candidate and current model are both scored on the same holdout, in this run, and
gate_decision asserts the current model never saw it.
"""
import argparse
import os
from datetime import datetime, timezone
import pandas as pd
from src import config, train
from src.ingest.fetch_gfz import download_gfz, load_gfz_daily
from src.ingest.fetch_omni import load_omni_spdf
from src.predict import load_bundle
from src.preprocessing import build_f107_history, build_hourly, check, save_processed
FIRST_YEAR = 1998
def refresh_data(write_f107: bool = True):
"""Download GFZ + every SPDF yearly OMNI file and rebuild data/processed/ from scratch."""
download_gfz()
gfz = load_gfz_daily()
omni = load_omni_spdf(FIRST_YEAR, datetime.now(timezone.utc).year)
df = build_hourly(omni, gfz)
check(df)
save_processed(df)
if write_f107:
build_f107_history(gfz)
print(f"OMNI {omni.index.min()} → {omni.index.max()} | hourly table ends {df.index.max()}")
return df
def run(dry_run: bool = False) -> dict:
refresh_data(write_f107=not dry_run)
X, y, _ = train.load_xy()
incumbent = load_bundle()
assert list(X.columns) == incumbent["features"], "feature set changed: retrain needs a new tuning round"
hold_start = y.index[train._holdout_mask(y)].min()
print(f"definitive labels with features end {y.index.max()} | gate holdout starts {hold_start}")
print(f"current model {incumbent['version']} trained until {incumbent['trained_until']}")
# The candidate would train on hours before hold_start; if that adds nothing, retraining is pointless
if hold_start <= pd.Timestamp(incumbent["trained_until"]) + pd.Timedelta("1h"):
print("No new definitive data since the current model was trained → nothing to do.")
return {"status": "no_new_data", "promoted": False}
params = {f"model__{k}": v for k, v in incumbent["params"].items()}
margin = incumbent.get("interval_margin", 0.0)
candidate, gate = train.fit_production(params, X, y, margin=margin)
promote, incumbent_gate = train.gate_decision(gate, X, y)
print(f"candidate gate RMSE {gate['rmse']:.4f} vs current {incumbent_gate['rmse']:.4f} "
f"(tolerance ×{config.GATE_TOLERANCE}) → promote: {promote}")
result = {"status": "promoted" if promote else "rejected", "promoted": promote,
"candidate_rmse": gate["rmse"], "current_rmse": incumbent_gate["rmse"]}
if dry_run:
print("Dry run: nothing saved.")
return {**result, "status": f"dry_run_{result['status']}", "promoted": False}
result["version"] = train.save_bundle(candidate, {}, gate, promote, incumbent_gate, keep_rejected=False,
note="weekly retrain (frozen configuration; test set not re-scored)")
print(f"logged {result['version']} in metrics_history.json (promoted={promote})")
return result
def _github_output(result: dict) -> None:
"""Expose the outcome to later workflow steps (no-op outside GitHub Actions)."""
path = os.environ.get("GITHUB_OUTPUT")
if path:
with open(path, "a", encoding="utf-8") as fh:
fh.write(f"status={result['status']}\npromoted={str(result['promoted']).lower()}\n"
f"version={result.get('version', '')}\n")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--dry-run", action="store_true", help="stop after the gate decision; save nothing")
_github_output(run(ap.parse_args().dry_run))