From 0e4e7aebb251571e0d1b1bc89438981c20390219 Mon Sep 17 00:00:00 2001
From: "2569718930@qq.com" <2569718930@qq.com>
Date: Mon, 18 May 2026 22:43:34 +0800
Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E4=BD=93=E6=A3=80=E4=BF=AE?=
=?UTF-8?q?=E5=A4=8D=EF=BC=9A=E5=88=A0=E9=99=A4=E7=A0=B4=E6=8D=9F=E7=9A=84?=
=?UTF-8?q?=20LGBM=20=E5=AF=BC=E5=85=A5=E3=80=818=E4=B8=AA=E6=AD=BB?=
=?UTF-8?q?=E6=B5=8B=E8=AF=95=E3=80=812=E4=B8=AA=E6=AD=BB=E8=84=9A?=
=?UTF-8?q?=E6=9C=AC=E3=80=81=E7=A7=BB=E9=99=A4=20pytz/lightgbm=20?=
=?UTF-8?q?=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
requirements.txt | 2 -
scripts/backfill_daily_record_model_gaps.py | 145 ------------
.../backfill_probability_shadow_history.py | 153 -------------
src/analysis/trend_engine.py | 28 +--
...st_auto_retrain_probability_calibration.py | 42 ----
tests/test_lgbm_daily_high.py | 93 --------
tests/test_lgbm_features.py | 93 --------
tests/test_probability_calibration.py | 209 ------------------
tests/test_probability_rollout.py | 65 ------
tests/test_probability_shadow_report.py | 61 -----
tests/test_probability_snapshot_archive.py | 183 ---------------
tests/test_probability_training_dataset.py | 61 -----
12 files changed, 1 insertion(+), 1134 deletions(-)
delete mode 100644 scripts/backfill_daily_record_model_gaps.py
delete mode 100644 scripts/backfill_probability_shadow_history.py
delete mode 100644 tests/test_auto_retrain_probability_calibration.py
delete mode 100644 tests/test_lgbm_daily_high.py
delete mode 100644 tests/test_lgbm_features.py
delete mode 100644 tests/test_probability_calibration.py
delete mode 100644 tests/test_probability_rollout.py
delete mode 100644 tests/test_probability_shadow_report.py
delete mode 100644 tests/test_probability_snapshot_archive.py
delete mode 100644 tests/test_probability_training_dataset.py
diff --git a/requirements.txt b/requirements.txt
index 9a79aed8..8534c0f0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,9 +4,7 @@ loguru
pyTelegramBotAPI
python-dotenv
netCDF4
-pytz
numpy
-lightgbm
web3
fastapi
uvicorn
diff --git a/scripts/backfill_daily_record_model_gaps.py b/scripts/backfill_daily_record_model_gaps.py
deleted file mode 100644
index 4668e5a4..00000000
--- a/scripts/backfill_daily_record_model_gaps.py
+++ /dev/null
@@ -1,145 +0,0 @@
-from __future__ import annotations
-
-import argparse
-import json
-import sys
-from collections import Counter
-from pathlib import Path
-from typing import Any, Dict
-
-PROJECT_ROOT = Path(__file__).resolve().parents[1]
-if str(PROJECT_ROOT) not in sys.path:
- sys.path.insert(0, str(PROJECT_ROOT))
-
-from src.analysis.deb_algorithm import load_history, save_history # noqa: E402
-from src.analysis.probability_snapshot_archive import ( # noqa: E402
- load_snapshot_rows_for_day,
-)
-from src.database.runtime_state import STATE_STORAGE_FILE, get_state_storage_mode # noqa: E402
-from scripts.fit_probability_calibration import _default_history_arg # noqa: E402
-
-
-def _load_daily_records(path: Path) -> Dict[str, Dict[str, Dict[str, Any]]]:
- data = load_history(str(path))
- return data if isinstance(data, dict) else {}
-
-
-def _pick_model_value_from_snapshots(
- city: str,
- target_date: str,
- model_name: str,
-) -> float | None:
- rows = load_snapshot_rows_for_day(city, target_date)
- values = []
- for row in rows:
- mm = row.get("multi_model") or {}
- if not isinstance(mm, dict):
- continue
- value = mm.get(model_name)
- if value is None:
- continue
- try:
- values.append(float(value))
- except Exception:
- continue
- if not values:
- return None
- counts = Counter(values)
- return counts.most_common(1)[0][0]
-
-
-def main() -> int:
- parser = argparse.ArgumentParser(
- description="Backfill missing model forecasts in daily_records from archived probability snapshots."
- )
- parser.add_argument(
- "--history-file",
- default=_default_history_arg(),
- help="Optional legacy daily_records.json path. In sqlite mode this defaults to the runtime database.",
- )
- parser.add_argument("--city", help="Optional city filter, e.g. ankara")
- parser.add_argument("--date", help="Optional YYYY-MM-DD filter")
- parser.add_argument(
- "--model",
- default="MGM",
- help="Model name to backfill from snapshot multi_model payloads",
- )
- parser.add_argument(
- "--write",
- action="store_true",
- help="Write recovered values back to history file / runtime state",
- )
- args = parser.parse_args()
-
- history_path = Path(args.history_file) if args.history_file else None
- data = _load_daily_records(history_path or Path())
- model_name = str(args.model or "").strip()
- city_filter = str(args.city or "").strip().lower() or None
- date_filter = str(args.date or "").strip() or None
-
- recovered = []
- missing = []
- changed = False
-
- for city, city_rows in sorted(data.items()):
- if city_filter and city != city_filter:
- continue
- if not isinstance(city_rows, dict):
- continue
- for target_date, record in sorted(city_rows.items()):
- if date_filter and target_date != date_filter:
- continue
- if not isinstance(record, dict):
- continue
- forecasts = record.get("forecasts") or {}
- if not isinstance(forecasts, dict):
- forecasts = {}
- if forecasts.get(model_name) is not None:
- continue
-
- recovered_value = _pick_model_value_from_snapshots(city, target_date, model_name)
- if recovered_value is None:
- missing.append((city, target_date))
- continue
-
- recovered.append((city, target_date, recovered_value))
- if args.write:
- next_forecasts = dict(forecasts)
- next_forecasts[model_name] = recovered_value
- record["forecasts"] = next_forecasts
- changed = True
-
- print(
- json.dumps(
- {
- "model": model_name,
- "recovered_count": len(recovered),
- "missing_count": len(missing),
- "recovered": [
- {"city": city, "date": date_str, "value": value}
- for city, date_str, value in recovered
- ],
- "missing": [
- {"city": city, "date": date_str}
- for city, date_str in missing
- ],
- "write_requested": bool(args.write),
- "storage_mode": get_state_storage_mode(),
- },
- ensure_ascii=False,
- indent=2,
- )
- )
-
- if changed:
- previous_mode = get_state_storage_mode()
- # Reuse existing save path semantics. In sqlite-only mode, save_history would skip file write.
- save_history(str(history_path or ""), data)
- if previous_mode == STATE_STORAGE_FILE and (history_path is None or not history_path.exists()):
- raise FileNotFoundError(history_path)
-
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/scripts/backfill_probability_shadow_history.py b/scripts/backfill_probability_shadow_history.py
deleted file mode 100644
index eac88583..00000000
--- a/scripts/backfill_probability_shadow_history.py
+++ /dev/null
@@ -1,153 +0,0 @@
-import argparse
-import json
-import os
-import sys
-
-PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-if PROJECT_ROOT not in sys.path:
- sys.path.insert(0, PROJECT_ROOT)
-
-from src.analysis.deb_algorithm import load_history, save_history # noqa: E402
-from src.analysis.probability_calibration import ( # noqa: E402
- ENGINE_MODE_EMOS_SHADOW,
- apply_probability_calibration,
- build_probability_features,
-)
-from scripts.fit_probability_calibration import _default_history_arg # noqa: E402
-
-
-def _sample_to_features(sample):
- peak_flag = sample.get("peak_flag")
- if peak_flag == 1.0:
- peak_status = "past"
- elif peak_flag == 0.5:
- peak_status = "in_window"
- else:
- peak_status = "before"
- return build_probability_features(
- city_name=sample.get("city") or "",
- raw_mu=sample.get("raw_mu"),
- raw_sigma=sample.get("raw_sigma"),
- deb_prediction=sample.get("deb_prediction"),
- ens_data={
- "median": sample.get("ens_median"),
- "p10": None,
- "p90": None,
- },
- current_forecasts={},
- max_so_far=None,
- peak_status=peak_status,
- local_hour_frac=None,
- )
-
-
-def main():
- parser = argparse.ArgumentParser(description="Backfill shadow probability snapshots into daily records.")
- parser.add_argument(
- "--history-file",
- default=_default_history_arg(),
- )
- parser.add_argument(
- "--training-samples",
- default=os.path.join(
- PROJECT_ROOT,
- "artifacts",
- "probability_calibration",
- "training_samples.json",
- ),
- )
- parser.add_argument(
- "--calibration-file",
- default=os.path.join(
- PROJECT_ROOT,
- "artifacts",
- "probability_calibration",
- "default.json",
- ),
- )
- args = parser.parse_args()
-
- history = load_history(args.history_file)
- with open(args.training_samples, "r", encoding="utf-8") as fh:
- training_payload = json.load(fh)
-
- updated = 0
- touched = 0
-
- for sample in training_payload.get("samples") or []:
- city = str(sample.get("city") or "").strip().lower()
- date_str = str(sample.get("date") or "").strip()
- if not city or not date_str:
- continue
- record = ((history.get(city) or {}).get(date_str) or {})
- if not isinstance(record, dict) or not record:
- continue
-
- legacy_distribution = [
- {"value": row.get("v"), "probability": row.get("p")}
- for row in (record.get("prob_snapshot") or [])
- if isinstance(row, dict) and row.get("v") is not None
- ]
- if not legacy_distribution:
- continue
-
- calibration = apply_probability_calibration(
- city_name=city,
- temp_symbol="°F" if city in {"atlanta", "chicago", "dallas", "miami", "new york", "seattle"} else "°C",
- raw_mu=sample.get("raw_mu"),
- raw_sigma=sample.get("raw_sigma"),
- max_so_far=None,
- legacy_distribution=legacy_distribution,
- features=_sample_to_features(sample),
- calibration_path=args.calibration_file,
- mode=ENGINE_MODE_EMOS_SHADOW,
- )
-
- shadow_distribution = calibration.get("shadow_distribution") or []
- compact_shadow = [
- {
- "v": int(row.get("value")),
- "p": round(float(row.get("probability") or 0.0), 3),
- }
- for row in shadow_distribution[:4]
- if row.get("value") is not None
- ]
- compact_calibration = {
- "mode": calibration.get("mode"),
- "engine": calibration.get("engine"),
- "version": calibration.get("calibration_version"),
- "source": calibration.get("calibration_source"),
- "raw_mu": calibration.get("raw_mu"),
- "raw_sigma": calibration.get("raw_sigma"),
- "calibrated_mu": calibration.get("calibrated_mu"),
- "calibrated_sigma": calibration.get("calibrated_sigma"),
- }
-
- touched += 1
- if (
- record.get("shadow_prob_snapshot") == compact_shadow
- and record.get("probability_calibration") == compact_calibration
- ):
- continue
-
- record["shadow_prob_snapshot"] = compact_shadow
- record["probability_calibration"] = compact_calibration
- history[city][date_str] = record
- updated += 1
-
- save_history(args.history_file, history)
- print(
- json.dumps(
- {
- "samples_seen": len(training_payload.get("samples") or []),
- "records_considered": touched,
- "records_updated": updated,
- },
- ensure_ascii=False,
- indent=2,
- )
- )
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/analysis/trend_engine.py b/src/analysis/trend_engine.py
index 7f10e515..9f7f3a49 100644
--- a/src/analysis/trend_engine.py
+++ b/src/analysis/trend_engine.py
@@ -18,7 +18,6 @@ from src.analysis.deb_algorithm import (
from src.analysis.settlement_rounding import apply_city_settlement, is_exact_settlement_city
from src.data_collection.city_registry import CITY_REGISTRY
from src.data_collection.city_risk_profiles import get_city_risk_profile
-from src.models.lgbm_daily_high import predict_lgbm_daily_high
SETTLEMENT_SOURCE_LABELS = {
"metar": "METAR",
@@ -415,32 +414,7 @@ def analyze_weather_trend(
peak_status = "before"
if city_name and current_forecasts and deb_prediction is not None:
- lgbm_prediction, _ = predict_lgbm_daily_high(
- city_name=city_name,
- current_forecasts=current_forecasts,
- deb_prediction=deb_prediction,
- current_temp=cur_temp,
- max_so_far=max_so_far,
- humidity=_sf(primary_current.get("humidity")),
- wind_speed_kt=_sf(primary_current.get("wind_speed_kt")),
- visibility_mi=_sf(primary_current.get("visibility_mi")),
- local_hour=local_hour,
- local_date=local_date_str,
- peak_status=peak_status,
- )
- if lgbm_prediction is not None:
- current_forecasts["LGBM"] = lgbm_prediction
- blended_high, weight_info = calculate_dynamic_weights(
- city_name, current_forecasts
- )
- if blended_high is not None:
- deb_prediction = blended_high
- deb_weights = weight_info
- _deb_to_save = blended_high
- if insights and "DEB 融合预测" in insights[0]:
- insights[0] = (
- f"🧬 DEB 融合预测:{blended_high}{temp_symbol} ({weight_info})"
- )
+ # DEB blending uses the already-computed set of model forecasts
if ai_features and "DEB系统已通过历史偏差矫正算出期待点是" in ai_features[0]:
ai_features[0] = (
f"🧬 DEB系统已通过历史偏差矫正算出期待点是: {blended_high}{temp_symbol}。"
diff --git a/tests/test_auto_retrain_probability_calibration.py b/tests/test_auto_retrain_probability_calibration.py
deleted file mode 100644
index 17e43a38..00000000
--- a/tests/test_auto_retrain_probability_calibration.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from scripts.auto_retrain_probability_calibration import judge_candidate
-
-
-def _report(sample_count=80, crps=-0.1, mae=0.0, hit=0.0):
- return {
- "summary": {
- "sample_count": sample_count,
- "delta": {
- "crps": crps,
- "mae": mae,
- "bucket_hit_rate": hit,
- },
- }
- }
-
-
-def test_candidate_gate_promotes_when_metrics_pass():
- decision = judge_candidate(
- _report(),
- min_samples=50,
- max_delta_crps=0.0,
- max_delta_mae=0.05,
- min_delta_bucket_hit_rate=-0.05,
- )
-
- assert decision["decision"] == "promote"
- assert decision["ready_for_promotion"] is True
- assert decision["blocking_reasons"] == []
-
-
-def test_candidate_gate_holds_when_metrics_regress():
- decision = judge_candidate(
- _report(sample_count=40, crps=0.1, mae=0.2, hit=-0.2),
- min_samples=50,
- max_delta_crps=0.0,
- max_delta_mae=0.05,
- min_delta_bucket_hit_rate=-0.05,
- )
-
- assert decision["decision"] == "hold"
- assert decision["ready_for_promotion"] is False
- assert len(decision["blocking_reasons"]) == 4
diff --git a/tests/test_lgbm_daily_high.py b/tests/test_lgbm_daily_high.py
deleted file mode 100644
index a5a0db82..00000000
--- a/tests/test_lgbm_daily_high.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import src.models.lgbm_daily_high as runtime
-
-
-class _FakeBooster:
- best_iteration = 7
-
- def predict(self, rows, num_iteration=None):
- assert len(rows) == 1
- return [14.36]
-
-
-def test_predict_lgbm_daily_high_skips_when_disabled(monkeypatch):
- monkeypatch.setenv("POLYWEATHER_LGBM_ENABLED", "false")
- prediction, meta = runtime.predict_lgbm_daily_high(
- city_name="ankara",
- current_forecasts={"Open-Meteo": 12.4},
- deb_prediction=12.3,
- current_temp=11.0,
- max_so_far=11.4,
- humidity=62.0,
- wind_speed_kt=8.0,
- visibility_mi=6.0,
- local_hour=10,
- local_date="2026-03-24",
- peak_status="before",
- history_data={},
- )
- assert prediction is None
- assert meta["reason"] == "disabled"
-
-
-def test_predict_lgbm_daily_high_returns_prediction(monkeypatch):
- monkeypatch.setenv("POLYWEATHER_LGBM_ENABLED", "true")
- monkeypatch.setenv("POLYWEATHER_LGBM_MIN_HISTORY_POINTS", "3")
- monkeypatch.setattr(runtime, "_load_schema", lambda path: {"feature_names": runtime.FEATURE_NAMES})
- monkeypatch.setattr(runtime, "_load_booster", lambda path: _FakeBooster())
-
- history_data = {
- "ankara": {
- "2026-03-20": {"actual_high": 10.0},
- "2026-03-21": {"actual_high": 11.0},
- "2026-03-22": {"actual_high": 13.0},
- "2026-03-23": {"actual_high": 12.0},
- }
- }
- prediction, meta = runtime.predict_lgbm_daily_high(
- city_name="ankara",
- current_forecasts={"Open-Meteo": 12.4, "ECMWF": 12.1, "GFS": 11.9},
- deb_prediction=12.3,
- current_temp=11.0,
- max_so_far=11.4,
- humidity=62.0,
- wind_speed_kt=8.0,
- visibility_mi=6.0,
- local_hour=10,
- local_date="2026-03-24",
- peak_status="before",
- history_data=history_data,
- )
- assert prediction == 14.4
- assert meta["reason"] == "ok"
- assert meta["history_count"] == 4
-
-
-def test_predict_lgbm_daily_high_requires_min_history(monkeypatch):
- monkeypatch.setenv("POLYWEATHER_LGBM_ENABLED", "true")
- monkeypatch.setenv("POLYWEATHER_LGBM_MIN_HISTORY_POINTS", "5")
- monkeypatch.setattr(runtime, "_load_schema", lambda path: {"feature_names": runtime.FEATURE_NAMES})
- monkeypatch.setattr(runtime, "_load_booster", lambda path: _FakeBooster())
-
- history_data = {
- "ankara": {
- "2026-03-21": {"actual_high": 11.0},
- "2026-03-22": {"actual_high": 13.0},
- "2026-03-23": {"actual_high": 12.0},
- }
- }
- prediction, meta = runtime.predict_lgbm_daily_high(
- city_name="ankara",
- current_forecasts={"Open-Meteo": 12.4},
- deb_prediction=12.3,
- current_temp=11.0,
- max_so_far=11.4,
- humidity=62.0,
- wind_speed_kt=8.0,
- visibility_mi=6.0,
- local_hour=10,
- local_date="2026-03-24",
- peak_status="before",
- history_data=history_data,
- )
- assert prediction is None
- assert meta["reason"] == "insufficient_history"
diff --git a/tests/test_lgbm_features.py b/tests/test_lgbm_features.py
deleted file mode 100644
index e3e64698..00000000
--- a/tests/test_lgbm_features.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from src.models.lgbm_features import build_runtime_feature_map, build_training_samples
-
-
-def test_build_runtime_feature_map_derives_history_and_model_summary():
- history_data = {
- "ankara": {
- "2026-03-20": {"actual_high": 10.0},
- "2026-03-21": {"actual_high": 11.0},
- "2026-03-22": {"actual_high": 13.0},
- "2026-03-23": {"actual_high": 12.0},
- }
- }
-
- feature_map, meta = build_runtime_feature_map(
- city_name="ankara",
- current_forecasts={
- "Open-Meteo": 12.4,
- "ECMWF": 12.1,
- "GFS": 11.9,
- "GEM": 12.8,
- },
- deb_prediction=12.3,
- current_temp=11.0,
- max_so_far=11.4,
- humidity=62.0,
- wind_speed_kt=8.0,
- visibility_mi=6.0,
- local_hour=10,
- local_date="2026-03-24",
- peak_status="before",
- history_data=history_data,
- )
-
- assert meta["reason"] == "ok"
- assert meta["history_count"] == 4
- assert feature_map["actual_high_lag_1"] == 12.0
- assert feature_map["actual_high_lag_2"] == 13.0
- assert feature_map["actual_high_trend_3"] == 1.0
- assert feature_map["model_median"] == 12.4
- assert round(feature_map["model_spread"], 3) == 0.9
- assert feature_map["peak_status_code"] == 0.0
-
-
-def test_build_runtime_feature_map_returns_none_without_history():
- feature_map, meta = build_runtime_feature_map(
- city_name="unknown-city",
- current_forecasts={"Open-Meteo": 12.4},
- deb_prediction=12.3,
- current_temp=11.0,
- max_so_far=11.4,
- humidity=62.0,
- wind_speed_kt=8.0,
- visibility_mi=6.0,
- local_hour=10,
- local_date="2026-03-24",
- peak_status="before",
- history_data={},
- )
-
- assert feature_map is None
- assert meta["reason"] == "no_history"
-
-
-def test_build_training_samples_prefers_truth_history_for_target():
- history_data = {
- "ankara": {
- "2026-03-20": {"actual_high": 10.0},
- "2026-03-21": {"actual_high": 11.0},
- "2026-03-22": {"actual_high": 13.0},
- "2026-03-23": {
- "actual_high": 12.0,
- "deb_prediction": 12.3,
- "forecasts": {"Open-Meteo": 12.4, "ECMWF": 12.1},
- },
- }
- }
- snapshot_index = {
- ("ankara", "2026-03-23"): {
- "city": "ankara",
- "date": "2026-03-23",
- "timestamp": "2026-03-23T10:00:00+03:00",
- "raw_mu": 12.2,
- "deb_prediction": 12.3,
- "max_so_far": 11.8,
- "peak_status": "before",
- "multi_model": {"Open-Meteo": 12.4, "ECMWF": 12.1},
- "observation": {"current_temp": 11.5, "humidity": 60.0, "wind_speed_kt": 8.0, "local_hour": 10},
- }
- }
-
- samples = build_training_samples(history_data=history_data, snapshot_index=snapshot_index)
- assert len(samples) == 1
- assert samples[0]["sample_source"] == "snapshot"
diff --git a/tests/test_probability_calibration.py b/tests/test_probability_calibration.py
deleted file mode 100644
index 4ada3779..00000000
--- a/tests/test_probability_calibration.py
+++ /dev/null
@@ -1,209 +0,0 @@
-import json
-from pathlib import Path
-
-from src.analysis.probability_calibration import (
- ENGINE_MODE_EMOS_PRIMARY,
- ENGINE_MODE_EMOS_SHADOW,
- ENGINE_MODE_LEGACY,
- apply_probability_calibration,
- build_probability_features,
- fit_calibration,
- resolve_probability_engine_mode,
-)
-
-
-def _write_calibration(tmp_path: Path):
- payload = {
- "version": "test-emos-v1",
- "source": "tmp/test-emos-v1.json",
- "global": {
- "mu": {
- "intercept": 0.0,
- "raw_mu_coef": 0.0,
- "deb_coef": 1.0,
- "ens_median_coef": 0.0,
- "max_so_far_gap_coef": 0.0,
- },
- "sigma": {
- "intercept": 0.0,
- "raw_sigma_coef": 1.0,
- "spread_coef": 0.0,
- "peak_flag_coef": 0.0,
- "max_so_far_gap_coef": 0.0,
- },
- },
- "sigma_constraints": {
- "min_ratio": 0.85,
- "max_ratio": 1.2,
- "absolute_min": 0.25,
- "absolute_max": 2.0,
- },
- "cities": {
- "ankara": {
- "mu_bias": 0.5,
- "sigma_scale": 2.0,
- "confidence": 1.0,
- }
- },
- "metrics": {"sample_count": 10, "mean_crps": 0.4},
- }
- path = tmp_path / "calibration.json"
- path.write_text(json.dumps(payload), encoding="utf-8")
- return path
-
-
-def test_default_probability_engine_is_emos_primary(monkeypatch):
- monkeypatch.delenv("POLYWEATHER_PROBABILITY_ENGINE", raising=False)
-
- assert resolve_probability_engine_mode() == ENGINE_MODE_EMOS_PRIMARY
- assert resolve_probability_engine_mode("unknown-mode") == ENGINE_MODE_EMOS_PRIMARY
-
- monkeypatch.setenv("POLYWEATHER_PROBABILITY_ENGINE", ENGINE_MODE_EMOS_SHADOW)
-
- assert resolve_probability_engine_mode() == ENGINE_MODE_EMOS_SHADOW
-
-
-def test_shadow_mode_keeps_legacy_distribution(tmp_path):
- calibration_path = _write_calibration(tmp_path)
- features = build_probability_features(
- city_name="ankara",
- raw_mu=9.0,
- raw_sigma=1.0,
- deb_prediction=10.0,
- ens_data={"median": 9.5, "p10": 8.0, "p90": 11.0},
- current_forecasts={"Open-Meteo": 9.0, "MGM": 10.0},
- max_so_far=8.8,
- peak_status="before",
- local_hour_frac=11.0,
- )
- legacy_distribution = [{"value": 9, "range": "[8.5~9.5)", "probability": 0.7}]
-
- result = apply_probability_calibration(
- city_name="ankara",
- temp_symbol="°C",
- raw_mu=9.0,
- raw_sigma=1.0,
- max_so_far=8.8,
- legacy_distribution=legacy_distribution,
- features=features,
- calibration_path=str(calibration_path),
- mode=ENGINE_MODE_EMOS_SHADOW,
- )
-
- assert result["mode"] == ENGINE_MODE_EMOS_SHADOW
- assert result["engine"] == ENGINE_MODE_LEGACY
- assert result["distribution"] == legacy_distribution
- assert result["shadow_distribution"]
- assert result["calibrated_mu"] == 10.5
- assert result["calibrated_sigma"] == 1.2
- assert len(result["shadow_distribution_all"]) >= len(result["shadow_distribution"])
-
-
-def test_primary_mode_switches_to_calibrated_distribution(tmp_path):
- calibration_path = _write_calibration(tmp_path)
- features = build_probability_features(
- city_name="ankara",
- raw_mu=9.0,
- raw_sigma=1.0,
- deb_prediction=10.0,
- ens_data={"median": 9.5, "p10": 8.0, "p90": 11.0},
- current_forecasts={"Open-Meteo": 9.0, "MGM": 10.0},
- max_so_far=8.8,
- peak_status="before",
- local_hour_frac=11.0,
- )
-
- result = apply_probability_calibration(
- city_name="ankara",
- temp_symbol="°C",
- raw_mu=9.0,
- raw_sigma=1.0,
- max_so_far=8.8,
- legacy_distribution=[{"value": 9, "range": "[8.5~9.5)", "probability": 0.7}],
- features=features,
- calibration_path=str(calibration_path),
- mode=ENGINE_MODE_EMOS_PRIMARY,
- )
-
- assert result["mode"] == ENGINE_MODE_EMOS_PRIMARY
- assert result["engine"] == "emos"
- assert result["calibrated_mu"] == 10.5
- assert result["calibrated_sigma"] == 1.2
- assert result["distribution"]
- assert len(result["distribution_all"]) >= len(result["distribution"])
- assert result["distribution"][0]["value"] >= 10
-
-
-def test_primary_mode_respects_observed_max_floor(tmp_path):
- calibration_path = _write_calibration(tmp_path)
- features = build_probability_features(
- city_name="ankara",
- raw_mu=32.0,
- raw_sigma=1.0,
- deb_prediction=32.0,
- ens_data={"median": 31.5, "p10": 30.0, "p90": 34.0},
- current_forecasts={"Open-Meteo": 32.0, "MGM": 31.8},
- max_so_far=33.0,
- peak_status="in_window",
- local_hour_frac=14.0,
- )
-
- result = apply_probability_calibration(
- city_name="ankara",
- temp_symbol="°C",
- raw_mu=32.0,
- raw_sigma=1.0,
- max_so_far=33.0,
- legacy_distribution=[{"value": 33, "range": "[32.5~33.5)", "probability": 0.7}],
- features=features,
- calibration_path=str(calibration_path),
- mode=ENGINE_MODE_EMOS_PRIMARY,
- )
-
- assert result["engine"] == "emos"
- assert result["calibrated_mu"] >= 33.0
- assert all(row["value"] >= 33 for row in result["distribution"])
-
-
-def test_fit_calibration_returns_metrics():
- samples = [
- {
- "city": "ankara",
- "actual_high": 11.0,
- "raw_mu": 10.2,
- "raw_sigma": 1.0,
- "deb_prediction": 10.5,
- "ens_median": 10.6,
- "ensemble_spread": 0.9,
- "max_so_far_gap": 0.5,
- "peak_flag": 0.0,
- },
- {
- "city": "ankara",
- "actual_high": 12.0,
- "raw_mu": 11.1,
- "raw_sigma": 1.0,
- "deb_prediction": 11.3,
- "ens_median": 11.2,
- "ensemble_spread": 1.0,
- "max_so_far_gap": 0.4,
- "peak_flag": 0.5,
- },
- {
- "city": "new york",
- "actual_high": 19.0,
- "raw_mu": 18.2,
- "raw_sigma": 1.4,
- "deb_prediction": 18.4,
- "ens_median": 18.3,
- "ensemble_spread": 1.2,
- "max_so_far_gap": 0.6,
- "peak_flag": 1.0,
- },
- ]
-
- result = fit_calibration(samples, version="unit-test-v1")
-
- assert result["version"] == "unit-test-v1"
- assert result["metrics"]["sample_count"] == 3
- assert "mean_crps" in result["metrics"]
diff --git a/tests/test_probability_rollout.py b/tests/test_probability_rollout.py
deleted file mode 100644
index 73de299b..00000000
--- a/tests/test_probability_rollout.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from src.analysis.probability_rollout import judge_probability_rollout
-
-
-def test_judge_probability_rollout_holds_on_shadow_brier_regression():
- evaluation_report = {
- "summary": {
- "sample_count": 105,
- "delta": {
- "crps": -0.09,
- "mae": 0.0,
- "bucket_hit_rate": 0.0,
- },
- }
- }
- shadow_report = {
- "summary": {
- "samples": 103,
- "delta_mae": 0.01,
- "delta_bucket_hit_rate": 0.01,
- "delta_bucket_brier": 0.29,
- },
- "by_city": {
- "miami": {
- "samples": 4,
- "delta_mae": 0.24,
- "delta_bucket_hit_rate": -0.5,
- "delta_bucket_brier": 0.47,
- }
- },
- }
-
- payload = judge_probability_rollout(evaluation_report, shadow_report)
-
- assert payload["decision"] == "hold"
- assert payload["ready_for_primary"] is False
- assert payload["blocking_reasons"]
- assert payload["worst_shadow_regressions"][0]["city"] == "miami"
-
-
-def test_judge_probability_rollout_promotes_on_clean_metrics():
- evaluation_report = {
- "summary": {
- "sample_count": 120,
- "delta": {
- "crps": -0.08,
- "mae": 0.0,
- "bucket_hit_rate": 0.02,
- },
- }
- }
- shadow_report = {
- "summary": {
- "samples": 110,
- "delta_mae": 0.0,
- "delta_bucket_hit_rate": 0.01,
- "delta_bucket_brier": 0.01,
- },
- "by_city": {},
- }
-
- payload = judge_probability_rollout(evaluation_report, shadow_report)
-
- assert payload["decision"] == "promote"
- assert payload["ready_for_primary"] is True
- assert payload["blocking_reasons"] == []
diff --git a/tests/test_probability_shadow_report.py b/tests/test_probability_shadow_report.py
deleted file mode 100644
index dd2acf5b..00000000
--- a/tests/test_probability_shadow_report.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import json
-from pathlib import Path
-
-from scripts.build_probability_shadow_report import main
-
-
-def test_shadow_report_builds_summary(monkeypatch, tmp_path: Path):
- history_file = tmp_path / "daily_records.json"
- output_file = tmp_path / "shadow_report.json"
- history_file.write_text(
- json.dumps(
- {
- "ankara": {
- "2026-03-18": {
- "actual_high": 10.0,
- "mu": 9.6,
- "prob_snapshot": [
- {"v": 10, "p": 0.48},
- {"v": 9, "p": 0.41},
- ],
- "shadow_prob_snapshot": [
- {"v": 10, "p": 0.55},
- {"v": 9, "p": 0.28},
- ],
- "probability_calibration": {
- "mode": "emos_shadow",
- "engine": "legacy",
- "version": "emos-test",
- "raw_mu": 9.6,
- "calibrated_mu": 9.9,
- },
- }
- }
- },
- ensure_ascii=False,
- indent=2,
- ),
- encoding="utf-8",
- )
-
- monkeypatch.setattr(
- "sys.argv",
- [
- "build_probability_shadow_report.py",
- "--history-file",
- str(history_file),
- "--output",
- str(output_file),
- ],
- )
-
- main()
-
- payload = json.loads(output_file.read_text(encoding="utf-8"))
- assert payload["summary"]["samples"] == 1
- assert payload["summary"]["legacy_mean_mae"] == 0.4
- assert payload["summary"]["shadow_mean_mae"] == 0.1
- assert payload["summary"]["delta_mae"] == -0.3
- assert payload["summary"]["legacy_bucket_hit_rate"] == 1.0
- assert payload["summary"]["shadow_bucket_hit_rate"] == 1.0
- assert payload["recent_observations"][0]["calibration_version"] == "emos-test"
diff --git a/tests/test_probability_snapshot_archive.py b/tests/test_probability_snapshot_archive.py
deleted file mode 100644
index 4534e085..00000000
--- a/tests/test_probability_snapshot_archive.py
+++ /dev/null
@@ -1,183 +0,0 @@
-import json
-from pathlib import Path
-import pytest
-
-import src.analysis.probability_snapshot_archive as snapshot_archive
-from src.database.runtime_state import RuntimeStateDB, TrainingFeatureRecordRepository
-
-
-@pytest.fixture(autouse=True)
-def _force_file_mode(monkeypatch):
- monkeypatch.setenv("POLYWEATHER_STATE_STORAGE_MODE", "file")
-
-
-def test_append_probability_snapshot_writes_jsonl(tmp_path: Path, monkeypatch):
- archive_path = tmp_path / "probability_training_snapshots.jsonl"
-
- db = RuntimeStateDB(str(tmp_path / "polyweather.db"))
- monkeypatch.setattr(
- snapshot_archive,
- "_training_feature_repo",
- TrainingFeatureRecordRepository(db),
- )
- monkeypatch.setattr(
- snapshot_archive,
- "_snapshot_repo",
- snapshot_archive.ProbabilitySnapshotRepository(db),
- )
-
- snapshot_archive.append_probability_snapshot(
- city_name="ankara",
- local_date="2026-03-20",
- observation_time="2026-03-20T12:00:00+03:00",
- temp_symbol="°C",
- raw_mu=15.2,
- raw_sigma=1.2,
- deb_prediction=15.4,
- ens_data={"p10": 14.8, "median": 15.8, "p90": 17.9},
- current_forecasts={"ECMWF": 15.8, "GFS": 14.1},
- max_so_far=15.0,
- peak_status="before",
- probabilities=[{"value": 15, "probability": 0.552}],
- shadow_probabilities=[{"value": 15, "probability": 0.324}],
- calibration_summary={
- "engine": "legacy",
- "mode": "emos_shadow",
- "calibration_version": "emos-test",
- "calibration_source": "artifacts/probability_calibration/default.json",
- "calibrated_mu": 15.1,
- "calibrated_sigma": 1.25,
- },
- archive_path=str(archive_path),
- )
-
- lines = archive_path.read_text(encoding="utf-8").strip().splitlines()
- assert len(lines) == 1
- payload = json.loads(lines[0])
- assert payload["city"] == "ankara"
- assert payload["date"] == "2026-03-20"
- assert payload["raw_mu"] == 15.2
- assert payload["ensemble"]["median"] == 15.8
- assert payload["prob_snapshot"][0]["v"] == 15
- assert payload["shadow_prob_snapshot"][0]["v"] == 15
- assert payload["calibration_version"] == "emos-test"
-
-
-def test_append_probability_snapshot_skips_near_duplicate(tmp_path: Path):
- archive_path = tmp_path / "probability_training_snapshots.jsonl"
- kwargs = dict(
- city_name="ankara",
- local_date="2026-03-20",
- observation_time="2026-03-20T12:00:00+03:00",
- temp_symbol="°C",
- raw_mu=15.2,
- raw_sigma=1.2,
- deb_prediction=15.4,
- ens_data={"p10": 14.8, "median": 15.8, "p90": 17.9},
- current_forecasts={"ECMWF": 15.8, "GFS": 14.1},
- max_so_far=15.0,
- peak_status="before",
- probabilities=[{"value": 15, "probability": 0.552}],
- shadow_probabilities=[{"value": 15, "probability": 0.324}],
- calibration_summary={
- "engine": "legacy",
- "mode": "emos_shadow",
- "calibration_version": "emos-test",
- "calibration_source": "artifacts/probability_calibration/default.json",
- "calibrated_mu": 15.1,
- "calibrated_sigma": 1.25,
- },
- archive_path=str(archive_path),
- )
-
- snapshot_archive.append_probability_snapshot(**kwargs)
- snapshot_archive.append_probability_snapshot(**kwargs)
-
- lines = archive_path.read_text(encoding="utf-8").strip().splitlines()
- assert len(lines) == 1
-
-
-def test_append_probability_snapshot_writes_on_bucket_change(tmp_path: Path):
- archive_path = tmp_path / "probability_training_snapshots.jsonl"
- base_kwargs = dict(
- city_name="ankara",
- local_date="2026-03-20",
- observation_time="2026-03-20T12:00:00+03:00",
- temp_symbol="°C",
- raw_mu=15.2,
- raw_sigma=1.2,
- deb_prediction=15.4,
- ens_data={"p10": 14.8, "median": 15.8, "p90": 17.9},
- current_forecasts={"ECMWF": 15.8, "GFS": 14.1},
- max_so_far=15.0,
- peak_status="before",
- shadow_probabilities=[{"value": 15, "probability": 0.324}],
- calibration_summary={
- "engine": "legacy",
- "mode": "emos_shadow",
- "calibration_version": "emos-test",
- "calibration_source": "artifacts/probability_calibration/default.json",
- "calibrated_mu": 15.1,
- "calibrated_sigma": 1.25,
- },
- archive_path=str(archive_path),
- )
-
- snapshot_archive.append_probability_snapshot(
- probabilities=[{"value": 15, "probability": 0.552}],
- **base_kwargs,
- )
- snapshot_archive.append_probability_snapshot(
- probabilities=[{"value": 16, "probability": 0.552}],
- **base_kwargs,
- )
-
- lines = archive_path.read_text(encoding="utf-8").strip().splitlines()
- assert len(lines) == 2
-
-
-def test_append_probability_snapshot_dual_writes_training_feature_store(tmp_path: Path, monkeypatch):
- monkeypatch.setenv("POLYWEATHER_STATE_STORAGE_MODE", "sqlite")
- monkeypatch.setenv("POLYWEATHER_DB_PATH", str(tmp_path / "polyweather.db"))
- db = RuntimeStateDB(str(tmp_path / "polyweather.db"))
-
- monkeypatch.setattr(
- snapshot_archive,
- "_training_feature_repo",
- TrainingFeatureRecordRepository(db),
- )
- monkeypatch.setattr(
- snapshot_archive,
- "_snapshot_repo",
- snapshot_archive.ProbabilitySnapshotRepository(db),
- )
-
- snapshot_archive.append_probability_snapshot(
- city_name="ankara",
- local_date="2026-03-20",
- observation_time="2026-03-20T12:00:00+03:00",
- temp_symbol="°C",
- raw_mu=15.2,
- raw_sigma=1.2,
- deb_prediction=15.4,
- ens_data={"p10": 14.8, "median": 15.8, "p90": 17.9},
- current_forecasts={"ECMWF": 15.8, "GFS": 14.1},
- max_so_far=15.0,
- peak_status="before",
- probabilities=[{"value": 15, "probability": 0.552}],
- shadow_probabilities=[{"value": 15, "probability": 0.324}],
- calibration_summary={
- "engine": "legacy",
- "mode": "emos_shadow",
- "calibration_version": "emos-test",
- "calibration_source": "artifacts/probability_calibration/default.json",
- "calibrated_mu": 15.1,
- "calibrated_sigma": 1.25,
- },
- )
-
- payload = TrainingFeatureRecordRepository(db).get_record("ankara", "2026-03-20")
- assert payload is not None
- assert payload["mu"] == 15.2
- assert payload["forecasts"]["ECMWF"] == 15.8
- assert payload["probability_features"]["ens_median"] == 15.8
diff --git a/tests/test_probability_training_dataset.py b/tests/test_probability_training_dataset.py
deleted file mode 100644
index c5bffb8d..00000000
--- a/tests/test_probability_training_dataset.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from scripts.fit_probability_calibration import _extract_samples
-
-
-def test_extract_samples_prefers_snapshot_rows_for_same_city_day():
- history = {
- "ankara": {
- "2026-03-19": {
- "actual_high": 11.0,
- "mu": 10.8,
- "deb_prediction": 10.9,
- "forecasts": {"ECMWF": 10.5, "GFS": 11.2},
- "probability_features": {
- "ens_median": 10.7,
- "ensemble_spread": 0.8,
- "peak_status": "before",
- },
- }
- }
- }
- snapshot_rows = [
- {
- "city": "ankara",
- "date": "2026-03-19",
- "timestamp": "2026-03-19T12:00:00+03:00",
- "raw_mu": 11.2,
- "raw_sigma": 1.1,
- "deb_prediction": 11.0,
- "ensemble": {"p10": 10.0, "median": 11.1, "p90": 12.2},
- "multi_model": {"ECMWF": 10.5, "GFS": 11.2},
- "max_so_far": 10.9,
- "peak_status": "in_window",
- }
- ]
- truth_history = {
- "ankara": {
- "2026-03-19": {
- "actual_high": 11.0,
- "settlement_source": "metar",
- "settlement_station_code": "LTAC",
- "truth_version": "v1",
- "updated_by": "test",
- "truth_updated_at": 123.0,
- }
- }
- }
-
- samples, filled = _extract_samples(
- history,
- truth_history=truth_history,
- settlement_history={},
- snapshot_rows=snapshot_rows,
- )
-
- assert filled == 0
- assert len(samples) == 1
- assert samples[0]["sample_source"] == "snapshot"
- assert samples[0]["raw_mu"] == 11.2
- assert samples[0]["peak_flag"] == 0.5
- assert samples[0]["settlement_source"] == "metar"
- assert samples[0]["settlement_station_code"] == "LTAC"
- assert samples[0]["truth_version"] == "v1"