Add ops dashboards for training data and model coverage

This commit is contained in:
2569718930@qq.com
2026-04-03 00:57:19 +08:00
parent 37cd8b8166
commit 781c247952
24 changed files with 3634 additions and 907 deletions
@@ -5,8 +5,10 @@ import re
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional
import requests
from loguru import logger
from src.data_collection.city_registry import CITY_REGISTRY
from src.analysis.settlement_rounding import apply_city_settlement
class WundergroundSourceMixin:
@@ -502,3 +504,92 @@ class WundergroundSourceMixin:
}
self._set_settlement_cache(cache_key, payload)
return payload
def _normalize_wu_history_date_url(url: str, target_date: str) -> str:
normalized = str(url or "").strip().rstrip("/")
normalized = re.sub(r"/date/\d{4}-\d{2}-\d{2}$", "", normalized, flags=re.IGNORECASE)
return f"{normalized}/date/{target_date}"
def fetch_wunderground_historical_high(
city: str,
target_date: str,
*,
url: Optional[str] = None,
timeout: int = 15,
session: Optional[requests.Session] = None,
) -> Dict[str, Any]:
city_key = str(city or "").strip().lower()
city_meta = CITY_REGISTRY.get(city_key) or {}
history_url = _normalize_wu_history_date_url(
url or str(city_meta.get("settlement_url") or "").strip(),
target_date,
)
if not history_url:
return {"ok": False, "reason": "missing_history_url", "city": city_key, "date": target_date}
requester = session or requests.Session()
try:
response = requester.get(
history_url,
headers={
"User-Agent": "Mozilla/5.0",
"Referer": history_url,
},
timeout=timeout,
)
response.raise_for_status()
html = str(response.text or "")
except Exception as exc:
logger.warning(f"Wunderground history fetch failed city={city_key} date={target_date}: {exc}")
return {
"ok": False,
"reason": "fetch_failed",
"city": city_key,
"date": target_date,
"history_url": history_url,
"error": str(exc),
}
app_state = WundergroundSourceMixin._wu_extract_app_state(html)
if not isinstance(app_state, dict):
return {
"ok": False,
"reason": "missing_app_state",
"city": city_key,
"date": target_date,
"history_url": history_url,
}
utc_offset_seconds = int(city_meta.get("tz_offset") or 0)
obs = WundergroundSourceMixin._wu_extract_history_observations(
app_state,
utc_offset_seconds=utc_offset_seconds,
)
if not obs:
return {
"ok": False,
"reason": "missing_observations",
"city": city_key,
"date": target_date,
"history_url": history_url,
}
raw_max_temp_c = max(float(point.get("temp")) for point in obs if point.get("temp") is not None)
settled_actual_high = apply_city_settlement(city_key, raw_max_temp_c)
station_code = str(city_meta.get("settlement_station_code") or "").strip().upper() or None
station_label = str(city_meta.get("settlement_station_label") or "").strip() or None
return {
"ok": True,
"city": city_key,
"date": target_date,
"history_url": history_url,
"raw_max_temp_c": round(raw_max_temp_c, 1),
"actual_high": float(settled_actual_high),
"settlement_source": "wunderground",
"settlement_station_code": station_code,
"settlement_station_label": station_label,
"observation_count": len(obs),
"observations": obs,
}