新增日内偏差修正:用实况与模型小时预报偏差动态修正 DEB 和概率分布
_analyze 在小时数据组装后,对比当前实测温度与模型预报偏差, 按日内时段权重修正 DEB 高温预测和概率分布 mu。 修正量上限 5°F/3°C,上午权重轻、峰值窗口加重、峰值后最强。 同时将 max-so-far 突破 DEB 预测作为辅助趋势修正信号。
This commit is contained in:
@@ -143,6 +143,19 @@ def _is_plausible_city_temp(city: str, value: Any, unit: str = "°C") -> bool:
|
||||
return temp >= min_value
|
||||
|
||||
|
||||
def _parse_local_hour(local_time_str: Optional[str]) -> Optional[int]:
|
||||
if not local_time_str:
|
||||
return None
|
||||
try:
|
||||
parts = str(local_time_str).strip().split(":")
|
||||
hour = int(parts[0])
|
||||
if 0 <= hour <= 23:
|
||||
return hour
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _parse_utc_datetime(value: Any) -> Optional[datetime]:
|
||||
raw = str(value or "").strip()
|
||||
if not raw or "T" not in raw:
|
||||
@@ -1479,6 +1492,66 @@ def _analyze(
|
||||
today_hourly["temps"].append(h_temps[i] if i < len(h_temps) else None)
|
||||
today_hourly["radiation"].append(h_rad[i] if i < len(h_rad) else None)
|
||||
|
||||
# ── 12a-b. Intraday bias correction ──────────────────────────────────
|
||||
# Nudge the DEB high-temp forecast and probability mu using the gap
|
||||
# between the current observed temperature and the model's hourly path.
|
||||
# Weight grows as the day progresses toward the peak window.
|
||||
_live_mc = raw.get("live_mc") or {}
|
||||
_settle_cur = raw.get("settlement_current") or {}
|
||||
_sc_cur = _settle_cur.get("current", {}) if _settle_cur else {}
|
||||
_use_settle = str(raw.get("settlement_source") or "").lower() in {
|
||||
"hko", "cwa", "noaa", "wunderground",
|
||||
} and bool(_sc_cur)
|
||||
_pri_cur = _sc_cur if _use_settle else _live_mc
|
||||
_current_temp = _sf(_pri_cur.get("temp"))
|
||||
_max_so_far = _sf(_pri_cur.get("max_temp_so_far"))
|
||||
if _max_so_far is None:
|
||||
_max_so_far = _sf(_live_mc.get("max_temp_so_far"))
|
||||
_local_hour = _parse_local_hour(local_time_str)
|
||||
peak_first = int(first_peak_h or 14)
|
||||
peak_last_h = int(last_peak_h or 17)
|
||||
|
||||
if (
|
||||
deb_val is not None
|
||||
and _current_temp is not None
|
||||
and _local_hour is not None
|
||||
and 6 <= _local_hour <= 22
|
||||
):
|
||||
hourly_times_list = today_hourly.get("times") or []
|
||||
hourly_temps_list = today_hourly.get("temps") or []
|
||||
model_hourly_temp = None
|
||||
current_hour_str = f"{_local_hour:02d}:00"
|
||||
for idx, t_str in enumerate(hourly_times_list):
|
||||
if str(t_str or "").startswith(current_hour_str) and idx < len(hourly_temps_list):
|
||||
candidate = _sf(hourly_temps_list[idx])
|
||||
if candidate is not None:
|
||||
model_hourly_temp = candidate
|
||||
break
|
||||
reference_temp = model_hourly_temp if model_hourly_temp is not None else _current_temp
|
||||
if reference_temp is not None:
|
||||
hourly_bias = _current_temp - reference_temp
|
||||
|
||||
if _local_hour < peak_first:
|
||||
progress = max(0.0, (_local_hour - 6) / max(1, peak_first - 6))
|
||||
weight = 0.15 + 0.20 * progress
|
||||
elif peak_first <= _local_hour <= peak_last_h:
|
||||
progress = (_local_hour - peak_first) / max(1, peak_last_h - peak_first)
|
||||
weight = 0.40 + 0.35 * progress
|
||||
else:
|
||||
weight = 0.80
|
||||
|
||||
max_correction = 5.0 if str(sym or "").upper() == "F" else 3.0
|
||||
hourly_correction = max(-max_correction, min(max_correction, hourly_bias * weight))
|
||||
|
||||
max_so_far_excess = (_max_so_far or _current_temp) - deb_val
|
||||
max_correction_clamped = max(-max_correction, min(max_correction, max_so_far_excess * max(0.3, weight)))
|
||||
|
||||
blended_correction = hourly_correction * 0.6 + max_correction_clamped * 0.4
|
||||
deb_val = round(deb_val + blended_correction, 1)
|
||||
if mu is not None:
|
||||
mu = round(mu + blended_correction, 1)
|
||||
deb_weights = f"{deb_weights or 'DEB'} + intraday_bias({blended_correction:+.1f})"
|
||||
|
||||
# ── 12b. Next 48h hourly block for future-date analysis modal ──
|
||||
next_48h_hourly = {
|
||||
"times": [],
|
||||
|
||||
Reference in New Issue
Block a user