feat: Implement Dynamic Ensemble Blending (DEB) algorithm with historical data management, dynamic weight calculation, and accuracy tracking.
This commit is contained in:
@@ -37,6 +37,11 @@ _history_cache = {}
|
||||
_history_mtime = 0
|
||||
|
||||
|
||||
def _is_excluded_model_name(model_name: str) -> bool:
|
||||
normalized = str(model_name or "").strip().lower().replace(" ", "").replace("_", "").replace("-", "")
|
||||
return "meteoblue" in normalized
|
||||
|
||||
|
||||
def load_history(filepath):
|
||||
global _history_cache, _history_mtime
|
||||
if not os.path.exists(filepath):
|
||||
@@ -100,6 +105,11 @@ def update_daily_record(
|
||||
if date_str not in data[city_name]:
|
||||
data[city_name][date_str] = {}
|
||||
|
||||
# 统一过滤已弃用模型,避免历史/展示残留
|
||||
forecasts = {
|
||||
k: v for k, v in (forecasts or {}).items() if not _is_excluded_model_name(k)
|
||||
}
|
||||
|
||||
compact_probs = None
|
||||
if probabilities is not None:
|
||||
# Store compact: [{"v": 25, "p": 0.8}, ...]
|
||||
@@ -124,6 +134,13 @@ def update_daily_record(
|
||||
):
|
||||
return
|
||||
|
||||
# actual_high 应该是日内最高温,理论上不应下降;防止异常写入覆盖已确认高值
|
||||
if old_actual is not None and actual_high is not None:
|
||||
try:
|
||||
actual_high = max(float(old_actual), float(actual_high))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
existing["forecasts"] = forecasts
|
||||
existing["actual_high"] = actual_high
|
||||
if deb_prediction is not None:
|
||||
@@ -155,6 +172,12 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
|
||||
history_file = os.path.join(project_root, "data", "daily_records.json")
|
||||
data = load_history(history_file)
|
||||
|
||||
current_forecasts = {
|
||||
k: v
|
||||
for k, v in (current_forecasts or {}).items()
|
||||
if not _is_excluded_model_name(k)
|
||||
}
|
||||
|
||||
if city_name not in data or not data[city_name]:
|
||||
# 没有历史数据,返回简单的平均/中位数
|
||||
valid_vals = [v for v in current_forecasts.values() if v is not None]
|
||||
|
||||
@@ -6,12 +6,14 @@ for both Telegram bot and web dashboard.
|
||||
"""
|
||||
|
||||
import math
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
|
||||
from src.analysis.deb_algorithm import (
|
||||
calculate_dynamic_weights,
|
||||
get_deb_accuracy,
|
||||
update_daily_record,
|
||||
_is_excluded_model_name,
|
||||
)
|
||||
from src.analysis.settlement_rounding import wu_round
|
||||
from src.data_collection.city_risk_profiles import get_city_risk_profile
|
||||
@@ -89,7 +91,7 @@ def analyze_weather_trend(
|
||||
|
||||
mm_forecasts = weather_data.get("multi_model", {}).get("forecasts", {})
|
||||
for m_name, m_val in mm_forecasts.items():
|
||||
if m_val is not None:
|
||||
if m_val is not None and not _is_excluded_model_name(m_name):
|
||||
current_forecasts[m_name] = _sf(m_val)
|
||||
|
||||
forecast_highs = [h for h in current_forecasts.values() if h is not None]
|
||||
@@ -100,18 +102,54 @@ def analyze_weather_trend(
|
||||
|
||||
wind_speed = metar.get("current", {}).get("wind_speed_kt", 0)
|
||||
|
||||
# === Local time ===
|
||||
local_time_full = open_meteo.get("current", {}).get("local_time", "")
|
||||
try:
|
||||
local_date_str = local_time_full.split(" ")[0]
|
||||
time_parts = local_time_full.split(" ")[1].split(":")
|
||||
local_hour = int(time_parts[0])
|
||||
local_minute = int(time_parts[1]) if len(time_parts) > 1 else 0
|
||||
except Exception:
|
||||
from datetime import datetime
|
||||
local_date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
local_hour = datetime.now().hour
|
||||
local_minute = datetime.now().minute
|
||||
# === Local time/date (do not trust cached Open-Meteo local_time for date key) ===
|
||||
utc_offset = _sf(open_meteo.get("utc_offset"))
|
||||
if utc_offset is None and city_name:
|
||||
try:
|
||||
from src.data_collection.city_registry import CITY_REGISTRY
|
||||
|
||||
city_meta = CITY_REGISTRY.get(str(city_name).lower())
|
||||
if isinstance(city_meta, dict):
|
||||
utc_offset = _sf(city_meta.get("tz_offset"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
city_now = None
|
||||
if utc_offset is not None:
|
||||
try:
|
||||
city_now = datetime.now(timezone.utc).astimezone(
|
||||
timezone(timedelta(seconds=int(utc_offset)))
|
||||
)
|
||||
except Exception:
|
||||
city_now = None
|
||||
|
||||
local_time_full = str((open_meteo.get("current") or {}).get("local_time") or "").strip()
|
||||
if city_now is not None:
|
||||
local_date_str = city_now.strftime("%Y-%m-%d")
|
||||
local_hour = city_now.hour
|
||||
local_minute = city_now.minute
|
||||
else:
|
||||
try:
|
||||
local_date_str = local_time_full.split(" ")[0]
|
||||
time_parts = local_time_full.split(" ")[1].split(":")
|
||||
local_hour = int(time_parts[0])
|
||||
local_minute = int(time_parts[1]) if len(time_parts) > 1 else 0
|
||||
except Exception:
|
||||
fallback_now = datetime.now()
|
||||
local_date_str = fallback_now.strftime("%Y-%m-%d")
|
||||
local_hour = fallback_now.hour
|
||||
local_minute = fallback_now.minute
|
||||
|
||||
# Use METAR observation date in city local time when available (reliable for actual_high date key).
|
||||
metar_obs_time_raw = str(metar.get("observation_time") or "").strip()
|
||||
if metar_obs_time_raw and utc_offset is not None:
|
||||
try:
|
||||
metar_obs_dt = datetime.fromisoformat(metar_obs_time_raw.replace("Z", "+00:00"))
|
||||
local_date_str = metar_obs_dt.astimezone(
|
||||
timezone(timedelta(seconds=int(utc_offset)))
|
||||
).strftime("%Y-%m-%d")
|
||||
except Exception:
|
||||
pass
|
||||
local_hour_frac = local_hour + local_minute / 60
|
||||
|
||||
# === DEB ===
|
||||
|
||||
Reference in New Issue
Block a user