feat: Implement the PolyWeather dashboard including frontend components, data collection, analysis, and API endpoints.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from src.analysis.settlement_rounding import wu_round
|
||||
|
||||
# Cross-platform file locking
|
||||
import sys
|
||||
@@ -99,27 +100,39 @@ def update_daily_record(
|
||||
if date_str not in data[city_name]:
|
||||
data[city_name][date_str] = {}
|
||||
|
||||
# 避免无意义的频繁磁盘写入
|
||||
old_actual = data[city_name][date_str].get("actual_high")
|
||||
if (
|
||||
old_actual == actual_high
|
||||
and data[city_name][date_str].get("forecasts") == forecasts
|
||||
):
|
||||
return
|
||||
|
||||
data[city_name][date_str]["forecasts"] = forecasts
|
||||
data[city_name][date_str]["actual_high"] = actual_high
|
||||
if deb_prediction is not None:
|
||||
data[city_name][date_str]["deb_prediction"] = deb_prediction
|
||||
if mu is not None:
|
||||
data[city_name][date_str]["mu"] = round(mu, 2)
|
||||
compact_probs = None
|
||||
if probabilities is not None:
|
||||
# Store compact: [{"v": 25, "p": 0.8}, ...]
|
||||
data[city_name][date_str]["prob_snapshot"] = [
|
||||
compact_probs = [
|
||||
{"v": p["value"], "p": p["probability"]}
|
||||
for p in probabilities[:4]
|
||||
]
|
||||
|
||||
# 避免无意义的频繁磁盘写入
|
||||
existing = data[city_name][date_str]
|
||||
old_actual = existing.get("actual_high")
|
||||
old_deb = existing.get("deb_prediction")
|
||||
old_mu = existing.get("mu")
|
||||
old_probs = existing.get("prob_snapshot")
|
||||
next_mu = round(mu, 2) if mu is not None else None
|
||||
if (
|
||||
old_actual == actual_high
|
||||
and existing.get("forecasts") == forecasts
|
||||
and (deb_prediction is None or old_deb == deb_prediction)
|
||||
and (mu is None or old_mu == next_mu)
|
||||
and (compact_probs is None or old_probs == compact_probs)
|
||||
):
|
||||
return
|
||||
|
||||
existing["forecasts"] = forecasts
|
||||
existing["actual_high"] = actual_high
|
||||
if deb_prediction is not None:
|
||||
existing["deb_prediction"] = deb_prediction
|
||||
if mu is not None:
|
||||
existing["mu"] = next_mu
|
||||
if probabilities is not None:
|
||||
existing["prob_snapshot"] = compact_probs
|
||||
|
||||
# 自动清理:只保留最近 14 天的记录(DEB 只用 7 天,14 天留足余量)
|
||||
cutoff = (datetime.now() - timedelta(days=14)).strftime("%Y-%m-%d")
|
||||
for city in list(data.keys()):
|
||||
@@ -173,7 +186,12 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
|
||||
|
||||
for model in current_forecasts.keys():
|
||||
if model in past_forecasts and past_forecasts[model] is not None:
|
||||
errors[model].append(abs(past_forecasts[model] - actual))
|
||||
try:
|
||||
pv = float(past_forecasts[model])
|
||||
av = float(actual)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
errors[model].append(abs(pv - av))
|
||||
|
||||
days_used += 1
|
||||
if days_used >= lookback_days:
|
||||
@@ -182,6 +200,8 @@ def calculate_dynamic_weights(city_name, current_forecasts, lookback_days=7):
|
||||
# 如果有效历史天数 < 2 天,还是使用等权
|
||||
if days_used < 2:
|
||||
valid_vals = [v for v in current_forecasts.values() if v is not None]
|
||||
if not valid_vals:
|
||||
return None, f"暂无有效模型数据(由于仅{days_used}天历史)"
|
||||
avg = sum(valid_vals) / len(valid_vals)
|
||||
return round(avg, 1), f"等权平均(由于仅{days_used}天历史)"
|
||||
|
||||
@@ -263,8 +283,8 @@ def get_deb_accuracy(city_name):
|
||||
continue
|
||||
|
||||
total += 1
|
||||
deb_wu = round(deb_pred)
|
||||
actual_wu = round(actual)
|
||||
deb_wu = wu_round(deb_pred)
|
||||
actual_wu = wu_round(actual)
|
||||
if deb_wu == actual_wu:
|
||||
hits += 1
|
||||
errors.append(abs(deb_pred - actual))
|
||||
@@ -328,13 +348,13 @@ def get_mu_accuracy(city_name):
|
||||
|
||||
total += 1
|
||||
mu_errors.append(abs(mu_val - actual))
|
||||
if round(mu_val) == round(actual):
|
||||
if wu_round(mu_val) == wu_round(actual):
|
||||
mu_hits += 1
|
||||
|
||||
# Brier Score from probability snapshot
|
||||
prob_snap = record.get("prob_snapshot", [])
|
||||
if prob_snap:
|
||||
actual_wu = round(actual)
|
||||
actual_wu = wu_round(actual)
|
||||
bs = 0.0
|
||||
for entry in prob_snap:
|
||||
predicted_p = entry.get("p", 0)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import math
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
Number = Union[int, float]
|
||||
|
||||
|
||||
def wu_round(value: Optional[Number]) -> Optional[int]:
|
||||
"""
|
||||
WU 结算口径四舍五入(0.5 一律进位):
|
||||
- 正数: floor(x + 0.5)
|
||||
- 负数: ceil(x - 0.5)
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
x = float(value)
|
||||
if x >= 0:
|
||||
return int(math.floor(x + 0.5))
|
||||
return int(math.ceil(x - 0.5))
|
||||
|
||||
@@ -13,6 +13,7 @@ from src.analysis.deb_algorithm import (
|
||||
get_deb_accuracy,
|
||||
update_daily_record,
|
||||
)
|
||||
from src.analysis.settlement_rounding import wu_round
|
||||
from src.data_collection.city_risk_profiles import get_city_risk_profile
|
||||
|
||||
|
||||
@@ -348,7 +349,7 @@ def analyze_weather_trend(
|
||||
ai_features.append(f"🎲 数学概率分布:{prob_str}")
|
||||
|
||||
elif is_dead_market:
|
||||
settled_wu = round(max_so_far) if max_so_far is not None else 0
|
||||
settled_wu = wu_round(max_so_far) if max_so_far is not None else 0
|
||||
dead_msg = f"🎲 <b>结算预测</b>:已锁定 {settled_wu}{temp_symbol} (死盘确认)"
|
||||
insights.append(dead_msg)
|
||||
ai_features.append("🎲 状态: 确认死盘,结算已无悬念。")
|
||||
@@ -375,7 +376,7 @@ def analyze_weather_trend(
|
||||
|
||||
# === Settlement boundary ===
|
||||
if max_so_far is not None:
|
||||
settled = round(max_so_far)
|
||||
settled = wu_round(max_so_far)
|
||||
fractional = max_so_far - int(max_so_far)
|
||||
dist_to_boundary = abs(fractional - 0.5)
|
||||
if dist_to_boundary <= 0.3:
|
||||
@@ -437,7 +438,7 @@ def analyze_weather_trend(
|
||||
ai_features.append(f"🌡️ 当前实测温度: {cur_temp}{temp_symbol}。")
|
||||
if max_so_far is not None:
|
||||
ai_features.append(
|
||||
f"🏔️ 今日实测最高温: {max_so_far}{temp_symbol} (WU结算={round(max_so_far)}{temp_symbol})。"
|
||||
f"🏔️ 今日实测最高温: {max_so_far}{temp_symbol} (WU结算={wu_round(max_so_far)}{temp_symbol})。"
|
||||
)
|
||||
if city_name:
|
||||
_profile = get_city_risk_profile(city_name)
|
||||
@@ -486,7 +487,7 @@ def analyze_weather_trend(
|
||||
for t, p in sorted_probs[:4]
|
||||
]
|
||||
elif is_dead_market and max_so_far is not None:
|
||||
_prob_list = [{"value": round(max_so_far), "probability": 1.0}]
|
||||
_prob_list = [{"value": wu_round(max_so_far), "probability": 1.0}]
|
||||
|
||||
update_daily_record(
|
||||
city_name,
|
||||
@@ -524,7 +525,7 @@ def analyze_weather_trend(
|
||||
"forecast_miss_deg": forecast_miss_deg,
|
||||
"max_so_far": max_so_far,
|
||||
"cur_temp": cur_temp,
|
||||
"wu_settle": round(max_so_far) if max_so_far is not None else None,
|
||||
"wu_settle": wu_round(max_so_far) if max_so_far is not None else None,
|
||||
}
|
||||
display_str = "\n".join(insights) if insights else ""
|
||||
return display_str, "\n".join(ai_features), structured
|
||||
@@ -543,12 +544,12 @@ def calculate_prob_distribution(
|
||||
# 0.5 * (1 + erf( (x-m)/(s*sqrt(2)) ))
|
||||
return 0.5 * (1 + math.erf((x - m) / (sigma * math.sqrt(2))))
|
||||
|
||||
min_possible_wu = round(max_so_far) if max_so_far is not None else -999
|
||||
min_possible_wu = wu_round(max_so_far) if max_so_far is not None else -999
|
||||
probs = {}
|
||||
|
||||
# Range: mu +/- 3 sigma or at least +/- 2 degrees
|
||||
search_range = max(2, int(sigma * 2.5))
|
||||
target_mu = round(mu)
|
||||
target_mu = wu_round(mu)
|
||||
|
||||
for n in range(target_mu - search_range, target_mu + search_range + 1):
|
||||
if n < min_possible_wu:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,19 @@ class WeatherDataCollector:
|
||||
"munich": ["EDDM", "EDMO", "EDJA"],
|
||||
}
|
||||
|
||||
# Meteoblue 仅在增益最大的城市启用(减少配额消耗与冗余请求)
|
||||
METEOBLUE_PRIORITY_CITIES = {
|
||||
"london",
|
||||
"paris",
|
||||
"seoul",
|
||||
"toronto",
|
||||
"buenos aires",
|
||||
"wellington",
|
||||
"lucknow",
|
||||
"sao paulo",
|
||||
"munich",
|
||||
}
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
weather_cfg = config.get("weather", {})
|
||||
@@ -1503,14 +1516,15 @@ class WeatherDataCollector:
|
||||
# 获取时区偏移以过滤 METAR
|
||||
utc_offset = open_meteo.get("utc_offset", 0)
|
||||
|
||||
mb_data = self.fetch_from_meteoblue(
|
||||
lat,
|
||||
lon,
|
||||
timezone_name=open_meteo.get("timezone", "UTC"),
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if mb_data:
|
||||
results["meteoblue"] = mb_data
|
||||
if city_lower in self.METEOBLUE_PRIORITY_CITIES:
|
||||
mb_data = self.fetch_from_meteoblue(
|
||||
lat,
|
||||
lon,
|
||||
timezone_name=open_meteo.get("timezone", "UTC"),
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if mb_data:
|
||||
results["meteoblue"] = mb_data
|
||||
|
||||
# 对美国城市,额外获取 NWS 高精预报
|
||||
if use_fahrenheit:
|
||||
|
||||
Reference in New Issue
Block a user