From a1416d1324db9cbe6e3c34eee40dd501ef82126b Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 5 Mar 2026 04:28:35 +0800 Subject: [PATCH] feat: introduce PolyWeather application with an interactive map, detailed city weather, trend analysis, and a supporting API. --- src/analysis/trend_engine.py | 80 ++++++++++++++++++++++++++---------- web/app.py | 18 +++++++- web/static/app.js | 16 ++++++-- 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/src/analysis/trend_engine.py b/src/analysis/trend_engine.py index fdbe83aa..02675e24 100644 --- a/src/analysis/trend_engine.py +++ b/src/analysis/trend_engine.py @@ -329,23 +329,15 @@ def analyze_weather_trend( f"实测最高 {max_so_far}{temp_symbol},偏差 {forecast_miss_deg}°。当前趋势: {_trend_dir}。" ) - # Gaussian CDF - def _norm_cdf(x, m, s): - return 0.5 * (1 + math.erf((x - m) / (s * math.sqrt(2)))) - - min_possible_wu = round(max_so_far) if max_so_far is not None else -999 - probs = {} - for n in range(round(mu) - 2, round(mu) + 3): - 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()) - 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) + # Probability Engine + probs_result = calculate_prob_distribution( + mu, sigma, max_so_far, temp_symbol + ) + mu = probs_result.get("mu", mu) + probabilities = probs_result.get("probabilities", []) + sorted_probs = probs_result.get("sorted_probs", []) + + if sorted_probs: prob_parts = [ f"{int(t)}{temp_symbol} [{t - 0.5}~{t + 0.5}) {p * 100:.0f}%" for t, p in sorted_probs[:4] @@ -354,10 +346,6 @@ def analyze_weather_trend( prob_str = " | ".join(prob_parts) insights.append(f"🎲 结算概率 (μ={mu:.1f}):{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: 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, "wu_settle": 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 + + +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 + } diff --git a/web/app.py b/web/app.py index 39c3032b..64bb644a 100644 --- a/web/app.py +++ b/web/app.py @@ -284,7 +284,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: # ── 10. Shared analysis (probability, trend, AI) via trend_engine ── # This single call replaces the duplicate probability engine, dead market # 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 probabilities = [] @@ -434,19 +434,33 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: day_m["MGM"] = _sf(mgm_daily[d_str]) d_val, d_winfo = None, "" + d_probs = [] if day_m: try: blended, winfo = calculate_dynamic_weights(city, day_m) if blended is not None: d_val = blended 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: pass if day_m: multi_model_daily[d_str] = { "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 ── diff --git a/web/static/app.js b/web/static/app.js index 009637b6..94b0f120 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -818,8 +818,18 @@ function renderChart(data) { function renderProbabilities(data) { const container = document.getElementById("probBars"); - const probs = data.probabilities?.distribution || []; - const mu = data.probabilities?.mu; + const targetDate = selectedForecastDate || data.local_date; + + 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) { container.innerHTML = @@ -834,7 +844,6 @@ function renderProbabilities(data) { probs.forEach((p, i) => { const pct = Math.round(p.probability * 100); - const width = Math.max(pct, 8); html += `
${p.value}${data.temp_symbol}
@@ -962,6 +971,7 @@ function switchForecastDate(cityName, dateStr) { const data = cityDataCache[cityName]; if (data) { renderModels(data); + renderProbabilities(data); renderForecast(data); } }