feat: add versioned DEB bias backtesting
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from src.analysis.deb_evaluation import (
|
||||
DEB_RAW_VERSION,
|
||||
DEB_RECENT_BIAS_CORRECTED_VERSION,
|
||||
backtest_deb_versions,
|
||||
build_recent_bias_corrector,
|
||||
evaluate_prediction_records,
|
||||
write_backtest_report,
|
||||
)
|
||||
from src.database.runtime_state import DailyRecordRepository, RuntimeStateDB
|
||||
|
||||
|
||||
def test_deb_evaluation_reports_mae_rmse_bias_and_bucket_hits():
|
||||
records = [
|
||||
{"city": "ankara", "target_date": "2026-05-20", "prediction": 20.0, "actual": 21.0},
|
||||
{"city": "ankara", "target_date": "2026-05-21", "prediction": 22.0, "actual": 21.0},
|
||||
{"city": "ankara", "target_date": "2026-05-22", "prediction": 23.0, "actual": 23.0},
|
||||
]
|
||||
|
||||
metrics = evaluate_prediction_records(records, version=DEB_RAW_VERSION)
|
||||
|
||||
assert metrics["version"] == DEB_RAW_VERSION
|
||||
assert metrics["samples"] == 3
|
||||
assert metrics["mae"] == 0.667
|
||||
assert metrics["rmse"] == 0.816
|
||||
assert metrics["bias"] == 0.0
|
||||
assert metrics["bucket_hit_rate"] == 0.333
|
||||
|
||||
|
||||
def test_recent_bias_corrector_uses_signed_error_without_rewriting_raw_deb():
|
||||
history = [
|
||||
{"city": "ankara", "target_date": "2026-05-20", "deb_prediction": 20.0, "actual_high": 22.0},
|
||||
{"city": "ankara", "target_date": "2026-05-21", "deb_prediction": 21.0, "actual_high": 23.0},
|
||||
{"city": "ankara", "target_date": "2026-05-22", "deb_prediction": 24.0, "actual_high": 25.0},
|
||||
]
|
||||
|
||||
corrector = build_recent_bias_corrector(history, lookback_days=30, min_samples=2)
|
||||
corrected = corrector.apply("ankara", raw_prediction=24.0)
|
||||
|
||||
assert corrected["version"] == DEB_RECENT_BIAS_CORRECTED_VERSION
|
||||
assert corrected["raw_prediction"] == 24.0
|
||||
assert corrected["corrected_prediction"] > corrected["raw_prediction"]
|
||||
assert corrected["bias_adjustment"] == 1.0
|
||||
assert corrected["samples"] == 3
|
||||
|
||||
|
||||
def test_backtest_deb_versions_compares_raw_and_bias_corrected_versions():
|
||||
history = [
|
||||
{"city": "ankara", "target_date": "2026-05-20", "deb_prediction": 20.0, "actual_high": 22.0},
|
||||
{"city": "ankara", "target_date": "2026-05-21", "deb_prediction": 21.0, "actual_high": 23.0},
|
||||
{"city": "ankara", "target_date": "2026-05-22", "deb_prediction": 24.0, "actual_high": 25.0},
|
||||
{"city": "ankara", "target_date": "2026-05-23", "deb_prediction": 24.0, "actual_high": 26.0},
|
||||
]
|
||||
|
||||
report = backtest_deb_versions(history, train_lookback_days=30)
|
||||
|
||||
assert report["schema_version"] == "deb_backtest_report.v1"
|
||||
assert report["versions"][DEB_RAW_VERSION]["samples"] == 2
|
||||
assert report["versions"][DEB_RECENT_BIAS_CORRECTED_VERSION]["samples"] == 2
|
||||
assert (
|
||||
report["versions"][DEB_RECENT_BIAS_CORRECTED_VERSION]["mae"]
|
||||
< report["versions"][DEB_RAW_VERSION]["mae"]
|
||||
)
|
||||
assert report["rows"][0]["versions"][DEB_RAW_VERSION]["prediction"] == 24.0
|
||||
assert report["rows"][0]["versions"][DEB_RECENT_BIAS_CORRECTED_VERSION]["prediction"] == 24.8
|
||||
|
||||
|
||||
def test_write_backtest_report_persists_versioned_json_and_csv(tmp_path):
|
||||
history = [
|
||||
{"city": "ankara", "target_date": "2026-05-20", "deb_prediction": 20.0, "actual_high": 22.0},
|
||||
{"city": "ankara", "target_date": "2026-05-21", "deb_prediction": 21.0, "actual_high": 23.0},
|
||||
{"city": "ankara", "target_date": "2026-05-22", "deb_prediction": 24.0, "actual_high": 25.0},
|
||||
]
|
||||
report = backtest_deb_versions(history)
|
||||
json_path = tmp_path / "deb-backtest.json"
|
||||
csv_path = tmp_path / "deb-backtest.csv"
|
||||
|
||||
write_backtest_report(report, json_path=json_path, csv_path=csv_path)
|
||||
|
||||
assert json_path.read_text(encoding="utf-8").startswith("{\n \"schema_version\": \"deb_backtest_report.v1\"")
|
||||
csv_text = csv_path.read_text(encoding="utf-8")
|
||||
assert "deb_v1_raw_prediction" in csv_text
|
||||
assert "deb_v1_recent_bias_corrected_prediction" in csv_text
|
||||
|
||||
|
||||
def test_backtest_deb_versions_cli_reads_sqlite_and_writes_outputs(tmp_path):
|
||||
db_path = tmp_path / "polyweather.db"
|
||||
db = RuntimeStateDB(str(db_path))
|
||||
repo = DailyRecordRepository(db)
|
||||
repo.upsert_record("ankara", "2026-05-20", {"deb_prediction": 20.0, "actual_high": 22.0})
|
||||
repo.upsert_record("ankara", "2026-05-21", {"deb_prediction": 21.0, "actual_high": 23.0})
|
||||
repo.upsert_record("ankara", "2026-05-22", {"deb_prediction": 24.0, "actual_high": 25.0})
|
||||
json_path = tmp_path / "report.json"
|
||||
csv_path = tmp_path / "report.csv"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(Path("scripts") / "backtest_deb_versions.py"),
|
||||
"--db",
|
||||
str(db_path),
|
||||
"--output-json",
|
||||
str(json_path),
|
||||
"--output-csv",
|
||||
str(csv_path),
|
||||
],
|
||||
cwd=Path(__file__).resolve().parents[1],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
payload = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
assert payload["schema_version"] == "deb_backtest_report.v1"
|
||||
assert payload["versions"][DEB_RAW_VERSION]["samples"] == 1
|
||||
assert csv_path.exists()
|
||||
@@ -1,5 +1,6 @@
|
||||
from src.analysis.deb_algorithm import (
|
||||
_collapse_forecasts_for_deb,
|
||||
calculate_deb_prediction,
|
||||
calculate_dynamic_weights,
|
||||
)
|
||||
|
||||
@@ -91,6 +92,42 @@ def test_deb_weighted_path_uses_deduped_family_values(monkeypatch):
|
||||
assert "家族去重" in info
|
||||
|
||||
|
||||
def test_calculate_deb_prediction_keeps_raw_and_adds_versioned_bias_correction(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"src.analysis.deb_algorithm.load_history",
|
||||
lambda _: {
|
||||
"ankara": {
|
||||
"2026-04-14": {
|
||||
"actual_high": 22.0,
|
||||
"deb_prediction": 20.0,
|
||||
"forecasts": {"ECMWF": 20.0, "GFS": 20.0},
|
||||
},
|
||||
"2026-04-15": {
|
||||
"actual_high": 23.0,
|
||||
"deb_prediction": 21.0,
|
||||
"forecasts": {"ECMWF": 21.0, "GFS": 21.0},
|
||||
},
|
||||
"2026-04-16": {
|
||||
"actual_high": 25.0,
|
||||
"deb_prediction": 24.0,
|
||||
"forecasts": {"ECMWF": 24.0, "GFS": 24.0},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = calculate_deb_prediction(
|
||||
"ankara",
|
||||
{"ECMWF": 24.0, "GFS": 24.0},
|
||||
)
|
||||
|
||||
assert result["raw_prediction"] == 24.0
|
||||
assert result["prediction"] == 25.0
|
||||
assert result["version"] == "deb_v1_recent_bias_corrected"
|
||||
assert result["bias_adjustment"] == 1.0
|
||||
assert result["bias_samples"] == 3
|
||||
|
||||
|
||||
def test_compute_hourly_model_errors_basic():
|
||||
from src.analysis.deb_algorithm import compute_hourly_model_errors
|
||||
|
||||
|
||||
Reference in New Issue
Block a user