feat: introduce PolyWeather application with an interactive map, detailed city weather, trend analysis, and a supporting API.
This commit is contained in:
@@ -329,23 +329,15 @@ def analyze_weather_trend(
|
|||||||
f"实测最高 {max_so_far}{temp_symbol},偏差 {forecast_miss_deg}°。当前趋势: {_trend_dir}。"
|
f"实测最高 {max_so_far}{temp_symbol},偏差 {forecast_miss_deg}°。当前趋势: {_trend_dir}。"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Gaussian CDF
|
# Probability Engine
|
||||||
def _norm_cdf(x, m, s):
|
probs_result = calculate_prob_distribution(
|
||||||
return 0.5 * (1 + math.erf((x - m) / (s * math.sqrt(2))))
|
mu, sigma, max_so_far, temp_symbol
|
||||||
|
)
|
||||||
min_possible_wu = round(max_so_far) if max_so_far is not None else -999
|
mu = probs_result.get("mu", mu)
|
||||||
probs = {}
|
probabilities = probs_result.get("probabilities", [])
|
||||||
for n in range(round(mu) - 2, round(mu) + 3):
|
sorted_probs = probs_result.get("sorted_probs", [])
|
||||||
if n < min_possible_wu:
|
|
||||||
continue
|
if sorted_probs:
|
||||||
p = _norm_cdf(n + 0.5, mu, sigma) - _norm_cdf(n - 0.5, mu, sigma)
|
|
||||||
if p > 0.01:
|
|
||||||
probs[n] = p
|
|
||||||
|
|
||||||
total_p = sum(probs.values())
|
|
||||||
if total_p > 0:
|
|
||||||
probs = {k: v / total_p for k, v in probs.items()}
|
|
||||||
sorted_probs = sorted(probs.items(), key=lambda x: x[1], reverse=True)
|
|
||||||
prob_parts = [
|
prob_parts = [
|
||||||
f"{int(t)}{temp_symbol} [{t - 0.5}~{t + 0.5}) {p * 100:.0f}%"
|
f"{int(t)}{temp_symbol} [{t - 0.5}~{t + 0.5}) {p * 100:.0f}%"
|
||||||
for t, p in sorted_probs[:4]
|
for t, p in sorted_probs[:4]
|
||||||
@@ -354,10 +346,6 @@ def analyze_weather_trend(
|
|||||||
prob_str = " | ".join(prob_parts)
|
prob_str = " | ".join(prob_parts)
|
||||||
insights.append(f"🎲 <b>结算概率</b> (μ={mu:.1f}):{prob_str}")
|
insights.append(f"🎲 <b>结算概率</b> (μ={mu:.1f}):{prob_str}")
|
||||||
ai_features.append(f"🎲 数学概率分布:{prob_str}")
|
ai_features.append(f"🎲 数学概率分布:{prob_str}")
|
||||||
for t, p in sorted_probs[:4]:
|
|
||||||
probabilities.append(
|
|
||||||
{"value": int(t), "range": f"[{t-0.5}~{t+0.5})", "probability": round(p, 3)}
|
|
||||||
)
|
|
||||||
|
|
||||||
elif is_dead_market:
|
elif is_dead_market:
|
||||||
settled_wu = round(max_so_far) if max_so_far is not None else 0
|
settled_wu = round(max_so_far) if max_so_far is not None else 0
|
||||||
@@ -538,6 +526,54 @@ def analyze_weather_trend(
|
|||||||
"cur_temp": cur_temp,
|
"cur_temp": cur_temp,
|
||||||
"wu_settle": round(max_so_far) if max_so_far is not None else None,
|
"wu_settle": round(max_so_far) if max_so_far is not None else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
display_str = "\n".join(insights) if insights else ""
|
display_str = "\n".join(insights) if insights else ""
|
||||||
return display_str, "\n".join(ai_features), structured
|
return display_str, "\n".join(ai_features), structured
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_prob_distribution(
|
||||||
|
mu: float, sigma: float, max_so_far: Optional[float], temp_symbol: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Generalized Gaussian probability distribution calculation.
|
||||||
|
"""
|
||||||
|
if mu is None or sigma is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _norm_cdf(x, m, s):
|
||||||
|
# 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
|
||||||
|
probs = {}
|
||||||
|
|
||||||
|
# Range: mu +/- 3 sigma or at least +/- 2 degrees
|
||||||
|
search_range = max(2, int(sigma * 2.5))
|
||||||
|
target_mu = round(mu)
|
||||||
|
|
||||||
|
for n in range(target_mu - search_range, target_mu + search_range + 1):
|
||||||
|
if n < min_possible_wu:
|
||||||
|
continue
|
||||||
|
p = _norm_cdf(n + 0.5, mu, sigma) - _norm_cdf(n - 0.5, mu, sigma)
|
||||||
|
if p > 0.01:
|
||||||
|
probs[n] = p
|
||||||
|
|
||||||
|
total_p = sum(probs.values())
|
||||||
|
sorted_probs = []
|
||||||
|
probabilities = []
|
||||||
|
|
||||||
|
if total_p > 0:
|
||||||
|
norm_probs = {k: v / total_p for k, v in probs.items()}
|
||||||
|
sorted_probs = sorted(norm_probs.items(), key=lambda x: x[1], reverse=True)
|
||||||
|
for t, p in sorted_probs[:4]:
|
||||||
|
probabilities.append({
|
||||||
|
"value": int(t),
|
||||||
|
"range": f"[{t-0.5}~{t+0.5})",
|
||||||
|
"probability": round(p, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mu": mu,
|
||||||
|
"sigma": sigma,
|
||||||
|
"probabilities": probabilities,
|
||||||
|
"sorted_probs": sorted_probs
|
||||||
|
}
|
||||||
|
|||||||
+16
-2
@@ -284,7 +284,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
# ── 10. Shared analysis (probability, trend, AI) via trend_engine ──
|
# ── 10. Shared analysis (probability, trend, AI) via trend_engine ──
|
||||||
# This single call replaces the duplicate probability engine, dead market
|
# This single call replaces the duplicate probability engine, dead market
|
||||||
# detection, forecast bust grading, and AI context building.
|
# detection, forecast bust grading, and AI context building.
|
||||||
from src.analysis.trend_engine import analyze_weather_trend as _trend_analyze
|
from src.analysis.trend_engine import analyze_weather_trend as _trend_analyze, calculate_prob_distribution
|
||||||
from src.analysis.ai_analyzer import get_ai_analysis
|
from src.analysis.ai_analyzer import get_ai_analysis
|
||||||
|
|
||||||
probabilities = []
|
probabilities = []
|
||||||
@@ -434,19 +434,33 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
day_m["MGM"] = _sf(mgm_daily[d_str])
|
day_m["MGM"] = _sf(mgm_daily[d_str])
|
||||||
|
|
||||||
d_val, d_winfo = None, ""
|
d_val, d_winfo = None, ""
|
||||||
|
d_probs = []
|
||||||
if day_m:
|
if day_m:
|
||||||
try:
|
try:
|
||||||
blended, winfo = calculate_dynamic_weights(city, day_m)
|
blended, winfo = calculate_dynamic_weights(city, day_m)
|
||||||
if blended is not None:
|
if blended is not None:
|
||||||
d_val = blended
|
d_val = blended
|
||||||
d_winfo = winfo
|
d_winfo = winfo
|
||||||
|
|
||||||
|
# Calculate future probability based on model divergence
|
||||||
|
m_vals = [v for v in day_m.values() if v is not None]
|
||||||
|
if len(m_vals) > 1:
|
||||||
|
# Use spread as a proxy for sigma.
|
||||||
|
# sigma = (max-min)/2 with a floor of 0.6
|
||||||
|
d_sigma = max(0.6, (max(m_vals) - min(m_vals)) / 2.0)
|
||||||
|
else:
|
||||||
|
d_sigma = 1.0
|
||||||
|
|
||||||
|
prob_obj = calculate_prob_distribution(d_val, d_sigma, None, sym)
|
||||||
|
d_probs = prob_obj.get("probabilities", [])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if day_m:
|
if day_m:
|
||||||
multi_model_daily[d_str] = {
|
multi_model_daily[d_str] = {
|
||||||
"models": day_m,
|
"models": day_m,
|
||||||
"deb": {"prediction": d_val, "weights_info": d_winfo}
|
"deb": {"prediction": d_val, "weights_info": d_winfo},
|
||||||
|
"probabilities": d_probs if i > 0 else probabilities # Use today's real prob for today
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Assemble result ──
|
# ── Assemble result ──
|
||||||
|
|||||||
+13
-3
@@ -818,8 +818,18 @@ function renderChart(data) {
|
|||||||
|
|
||||||
function renderProbabilities(data) {
|
function renderProbabilities(data) {
|
||||||
const container = document.getElementById("probBars");
|
const container = document.getElementById("probBars");
|
||||||
const probs = data.probabilities?.distribution || [];
|
const targetDate = selectedForecastDate || data.local_date;
|
||||||
const mu = data.probabilities?.mu;
|
|
||||||
|
let probs = [];
|
||||||
|
let mu = null;
|
||||||
|
|
||||||
|
if (targetDate === data.local_date) {
|
||||||
|
probs = data.probabilities?.distribution || [];
|
||||||
|
mu = data.probabilities?.mu;
|
||||||
|
} else if (data.multi_model_daily && data.multi_model_daily[targetDate]) {
|
||||||
|
probs = data.multi_model_daily[targetDate].probabilities || [];
|
||||||
|
mu = data.multi_model_daily[targetDate].deb?.prediction;
|
||||||
|
}
|
||||||
|
|
||||||
if (probs.length === 0) {
|
if (probs.length === 0) {
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
@@ -834,7 +844,6 @@ function renderProbabilities(data) {
|
|||||||
|
|
||||||
probs.forEach((p, i) => {
|
probs.forEach((p, i) => {
|
||||||
const pct = Math.round(p.probability * 100);
|
const pct = Math.round(p.probability * 100);
|
||||||
const width = Math.max(pct, 8);
|
|
||||||
html += `
|
html += `
|
||||||
<div class="prob-row">
|
<div class="prob-row">
|
||||||
<div class="prob-label">${p.value}${data.temp_symbol}</div>
|
<div class="prob-label">${p.value}${data.temp_symbol}</div>
|
||||||
@@ -962,6 +971,7 @@ function switchForecastDate(cityName, dateStr) {
|
|||||||
const data = cityDataCache[cityName];
|
const data = cityDataCache[cityName];
|
||||||
if (data) {
|
if (data) {
|
||||||
renderModels(data);
|
renderModels(data);
|
||||||
|
renderProbabilities(data);
|
||||||
renderForecast(data);
|
renderForecast(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user