fix: reality-anchored probability engine and forecast bust AI detection

This commit is contained in:
2569718930@qq.com
2026-03-04 01:52:21 +08:00
parent 894010f17f
commit fd24d369aa
2 changed files with 57 additions and 13 deletions
+11 -5
View File
@@ -33,7 +33,12 @@ def get_ai_analysis(weather_insights: str, city_name: str, temp_symbol: str) ->
【分析优先级】(按此顺序逐项检查,前置项可推翻后置项的结论)
P1 **实况节奏**(最高优先级):
P0 **预报崩盘检测**(最高优先级):
- 如果数据中出现"🚨 预报崩盘"标记,说明所有模型集体严重高估。
- 此时你必须:① 直接宣布预报失准/崩盘;② 以实测最高温为结算锚点;③ 分析可能导致崩盘的原因(冷平流、降水、云量压制等);④ 禁止再讨论"冲击"预报值。
- 如果峰值窗口已过且实测远低于预报,结算已基本确定,应直接给出结算值。
P1 **实况节奏**
- 近2-4条METAR的温度走势:连涨/持平/回落?
- 触发阈值:连续2报创新高 → 升温未止;连续2报未创新高且已过峰值窗 → 偏死盘。
- 低辐射时段(<100W/m²)仍在升温 → 暖平流驱动,预报往往低估。
@@ -44,11 +49,12 @@ P2 **阻碍因子**
- 若阻碍条件未满足,不可凭"多云"单因子就判断"升温受限"
P3 **数学概率**
- 参考我提供的结算概率分布,但不可用概率去压过 P1 实况节奏
- 参考我提供的结算概率分布,但不可用概率去压过 P0/P1 实况。
- 概率是辅助判据,实测趋势是主判据。
P4 **预报背景**(最低优先级):
- DEB融合值和预报可用于判断上沿空间和回落节奏,但当实测已超预报时,不可作为"难以升温"的主论据
- DEB融合值和预报可用于判断上沿空间和回落节奏。
- 但当实测已远低于预报时,预报值已失去参考意义,不可继续引用。
- 结算边界:温度处于 X.5 进位线附近时需特别预警。
【输出要求】
@@ -56,8 +62,8 @@ P4 **预报背景**(最低优先级):
2. 严格按照以下 HTML 格式输出:
🤖 <b>Groq AI 决策</b>
- 🎲 盘口: [必须明确指出最热时段(如:预计最热在 14:00-16:00以及博弈焦点。区分"已确认WU底线 X°C""仍有概率冲击 Y°C"。禁止在升温未止时使用"锁定"等封顶措辞。若符合死盘条件,请直接给出死盘结论并说明理由。]
- 💡 逻辑: [用 3-5 句话深度分析实测与预报的博弈。分析当前动力条件对冲击下一个 WU 整数的支持程度。需包含具体数值。]
- 🎲 盘口: [若预报崩盘,直接给出"预报集体失准,WU结算锁定X°C"。否则:明确指出最热时段以及博弈焦点。区分"已确认WU底线 X°C""仍有概率冲击 Y°C"。禁止在升温未止时使用"锁定"等封顶措辞。若符合死盘条件,请直接给出死盘结论并说明理由。]
- 💡 逻辑: [用 3-5 句话深度分析。如果预报崩盘,重点分析崩盘原因和实测偏差。如果正常,分析实测与预报的博弈和动力条件。需包含具体数值。]
- 🎯 置信度: [1-10]/10
"""
+46 -8
View File
@@ -271,6 +271,7 @@ def _analyze(city: str) -> Dict[str, Any]:
# ── 10. Probability distribution ──
probabilities = []
mu = None
forecast_miss_deg = 0 # How far actual is below forecasts
if (
ens_data["p10"] is not None
and ens_data["p90"] is not None
@@ -314,20 +315,43 @@ def _analyze(city: str) -> Dict[str, Any]:
elif first_peak_h <= local_hour_frac <= last_peak_h:
sigma *= 0.7
# Mu calculation
# Mu calculation — reality-anchored
forecast_highs = [h for h in current_forecasts.values() if h is not None]
forecast_median = (
sorted(forecast_highs)[len(forecast_highs) // 2]
if forecast_highs
else ens_data["median"]
)
mu = (
forecast_median * 0.7 + ens_data["median"] * 0.3
if forecast_median is not None
else ens_data["median"]
)
if max_so_far is not None and max_so_far > mu:
mu = max_so_far + (0.3 if not trend_info["is_cooling"] else 0.0)
# Compute forecast miss magnitude
if max_so_far is not None and forecast_median is not None:
forecast_miss_deg = round(forecast_median - max_so_far, 1)
# --- Key fix: Reality-anchored μ ---
# If we are past or in the peak window AND actual max is significantly
# below forecasts, anchor μ on max_so_far, not on forecast_median.
if (
max_so_far is not None
and forecast_median is not None
and peak_status in ("past", "in_window")
and max_so_far < forecast_median - 2.0
):
# Forecast bust: μ anchors on actual max, not failed predictions
# Allow small upward margin only if still warming
if trend_info["is_cooling"] or peak_status == "past":
mu = max_so_far
else:
# Still in window and warming — small margin
mu = max_so_far + 0.5
else:
# Normal case: blend forecast and ensemble
mu = (
forecast_median * 0.7 + ens_data["median"] * 0.3
if forecast_median is not None
else ens_data["median"]
)
if max_so_far is not None and max_so_far > mu:
mu = max_so_far + (0.3 if not trend_info["is_cooling"] else 0.0)
def _norm_cdf(x, m, s):
return 0.5 * (1 + math.erf((x - m) / (s * math.sqrt(2))))
@@ -478,6 +502,20 @@ def _analyze(city: str) -> Dict[str, Any]:
)
ai_parts.append(f"模型分歧: {mm_str}")
# --- Forecast bust detection for AI ---
if forecast_miss_deg > 2.0 and peak_status in ("past", "in_window"):
min_forecast = min(
(v for v in current_forecasts.values() if v is not None), default=None
)
ai_parts.append(
f"🚨 预报崩盘: 所有模型集体高估!最低预报 {min_forecast}{sym} vs 实测最高 {max_so_far}{sym}"
f"偏差 {forecast_miss_deg}°。已进入/过了峰值窗口,温度严重不达预期。"
)
elif forecast_miss_deg > 4.0 and peak_status == "before":
ai_parts.append(
f"⚠️ 预报差距: 距峰值窗口尚有时间,但实测已落后预报 {forecast_miss_deg}°。"
)
ai_context = "\n".join(ai_parts)
ai_text = get_ai_analysis(ai_context, city, sym)
except Exception as e: