Add full probability distributions to dashboard

This commit is contained in:
2569718930@qq.com
2026-04-22 01:43:13 +08:00
parent f9eff36aae
commit 974b55e34f
7 changed files with 65 additions and 14 deletions
@@ -858,6 +858,10 @@ export function ProbabilityDistribution({
? formatBucketDisplayLabel(topProbability, detail) ? formatBucketDisplayLabel(topProbability, detail)
: null; : null;
const topProbabilityTemp = topProbability ? getBucketTemp(topProbability) : null; const topProbabilityTemp = topProbability ? getBucketTemp(topProbability) : null;
const probabilitiesForMarketContracts =
view.probabilitiesAll?.length > 0
? view.probabilitiesAll
: view.probabilities || [];
const marketContractRows = useMemo<ProbabilityDisplayRow[]>(() => { const marketContractRows = useMemo<ProbabilityDisplayRow[]>(() => {
if (!isToday || !marketScan?.available || marketAllBuckets.length === 0) { if (!isToday || !marketScan?.available || marketAllBuckets.length === 0) {
return []; return [];
@@ -867,7 +871,7 @@ export function ProbabilityDistribution({
const seenKeys = new Set<string>(); const seenKeys = new Set<string>();
for (const marketBucket of marketAllBuckets) { for (const marketBucket of marketAllBuckets) {
const probability = getAggregatedModelProbabilityForMarketBucket( const probability = getAggregatedModelProbabilityForMarketBucket(
view.probabilities || [], probabilitiesForMarketContracts,
marketBucket, marketBucket,
detail, detail,
); );
@@ -886,7 +890,13 @@ export function ProbabilityDistribution({
}); });
} }
return rows; return rows;
}, [detail, isToday, marketAllBuckets, marketScan?.available, view.probabilities]); }, [
detail,
isToday,
marketAllBuckets,
marketScan?.available,
probabilitiesForMarketContracts,
]);
const modelProbabilityRows = useMemo<ProbabilityDisplayRow[]>( const modelProbabilityRows = useMemo<ProbabilityDisplayRow[]>(
() => () =>
(view.probabilities || []).slice(0, 6).map((bucket, index) => { (view.probabilities || []).slice(0, 6).map((bucket, index) => {
@@ -938,7 +948,7 @@ export function ProbabilityDistribution({
topProbabilityLabel || topProbabilityLabel ||
null; null;
const aggregatedMarketProbability = getAggregatedModelProbabilityForMarketBucket( const aggregatedMarketProbability = getAggregatedModelProbabilityForMarketBucket(
view.probabilities || [], probabilitiesForMarketContracts,
linkedMarketBucket, linkedMarketBucket,
detail, detail,
); );
+2
View File
@@ -488,6 +488,7 @@ export interface CityDetail {
probabilities?: { probabilities?: {
mu?: number | null; mu?: number | null;
distribution?: ProbabilityBucket[]; distribution?: ProbabilityBucket[];
distribution_all?: ProbabilityBucket[];
engine?: string | null; engine?: string | null;
calibration_mode?: string | null; calibration_mode?: string | null;
calibration_version?: string | null; calibration_version?: string | null;
@@ -496,6 +497,7 @@ export interface CityDetail {
calibrated_mu?: number | null; calibrated_mu?: number | null;
calibrated_sigma?: number | null; calibrated_sigma?: number | null;
shadow_distribution?: ProbabilityBucket[]; shadow_distribution?: ProbabilityBucket[];
shadow_distribution_all?: ProbabilityBucket[];
}; };
hourly?: { hourly?: {
times?: string[]; times?: string[];
+10
View File
@@ -1212,7 +1212,15 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
engine: detail.probabilities?.engine ?? null, engine: detail.probabilities?.engine ?? null,
mu: detail.probabilities?.mu ?? null, mu: detail.probabilities?.mu ?? null,
probabilities: detail.probabilities?.distribution || [], probabilities: detail.probabilities?.distribution || [],
probabilitiesAll:
detail.probabilities?.distribution_all ||
detail.probabilities?.distribution ||
[],
shadowProbabilities: detail.probabilities?.shadow_distribution || [], shadowProbabilities: detail.probabilities?.shadow_distribution || [],
shadowProbabilitiesAll:
detail.probabilities?.shadow_distribution_all ||
detail.probabilities?.shadow_distribution ||
[],
}; };
} }
@@ -1223,7 +1231,9 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
engine: null, engine: null,
mu: daily?.deb?.prediction ?? null, mu: daily?.deb?.prediction ?? null,
probabilities: daily?.probabilities || [], probabilities: daily?.probabilities || [],
probabilitiesAll: daily?.probabilities || [],
shadowProbabilities: [], shadowProbabilities: [],
shadowProbabilitiesAll: [],
}; };
} }
+19 -9
View File
@@ -169,7 +169,7 @@ def _bucket_probabilities(
sigma: float, sigma: float,
max_so_far: Optional[float], max_so_far: Optional[float],
city_name: str, city_name: str,
) -> Tuple[List[Dict[str, Any]], List[Tuple[int, float]]]: ) -> Tuple[List[Dict[str, Any]], List[Tuple[int, float]], List[Dict[str, Any]]]:
sigma = max(0.1, float(sigma)) sigma = max(0.1, float(sigma))
min_possible = ( min_possible = (
apply_city_settlement(city_name, max_so_far) if max_so_far is not None else -999 apply_city_settlement(city_name, max_so_far) if max_so_far is not None else -999
@@ -193,24 +193,24 @@ def _bucket_probabilities(
total = sum(probs.values()) total = sum(probs.values())
if total <= 0: if total <= 0:
return [], [] return [], [], []
normalized = {key: val / total for key, val in probs.items()} normalized = {key: val / total for key, val in probs.items()}
sorted_probs = sorted(normalized.items(), key=lambda item: item[1], reverse=True) sorted_probs = sorted(normalized.items(), key=lambda item: item[1], reverse=True)
distribution = [] full_distribution = []
for value, prob in sorted_probs[:4]: for value, prob in sorted_probs:
if is_exact: if is_exact:
bucket_range = "[{0}.0~{1}.0)".format(value, value + 1) bucket_range = "[{0}.0~{1}.0)".format(value, value + 1)
else: else:
bucket_range = "[{0}~{1})".format(value - 0.5, value + 0.5) bucket_range = "[{0}~{1})".format(value - 0.5, value + 0.5)
distribution.append( full_distribution.append(
{ {
"value": int(value), "value": int(value),
"range": bucket_range, "range": bucket_range,
"probability": round(prob, 3), "probability": round(prob, 3),
} }
) )
return distribution, sorted_probs return full_distribution[:4], sorted_probs, full_distribution
def _top_bucket_value(distribution: Optional[List[Dict[str, Any]]]) -> Optional[int]: def _top_bucket_value(distribution: Optional[List[Dict[str, Any]]]) -> Optional[int]:
@@ -299,7 +299,9 @@ def apply_probability_calibration(
"mode": selected_mode, "mode": selected_mode,
"engine": ENGINE_MODE_LEGACY, "engine": ENGINE_MODE_LEGACY,
"distribution": legacy_distribution or [], "distribution": legacy_distribution or [],
"distribution_all": legacy_distribution or [],
"shadow_distribution": [], "shadow_distribution": [],
"shadow_distribution_all": [],
"raw_mu": raw_mu, "raw_mu": raw_mu,
"raw_sigma": raw_sigma, "raw_sigma": raw_sigma,
"calibrated_mu": None, "calibrated_mu": None,
@@ -314,7 +316,9 @@ def apply_probability_calibration(
"mode": selected_mode, "mode": selected_mode,
"engine": ENGINE_MODE_LEGACY, "engine": ENGINE_MODE_LEGACY,
"distribution": legacy_distribution or [], "distribution": legacy_distribution or [],
"distribution_all": legacy_distribution or [],
"shadow_distribution": [], "shadow_distribution": [],
"shadow_distribution_all": [],
"raw_mu": raw_mu, "raw_mu": raw_mu,
"raw_sigma": raw_sigma, "raw_sigma": raw_sigma,
"calibrated_mu": None, "calibrated_mu": None,
@@ -384,7 +388,7 @@ def apply_probability_calibration(
calibrated_mu = observed_floor calibrated_mu = observed_floor
calibrated_sigma = max(0.1, _blend_value(raw_sigma, calibrated_sigma, blend_alpha_sigma)) calibrated_sigma = max(0.1, _blend_value(raw_sigma, calibrated_sigma, blend_alpha_sigma))
calibrated_sigma = _clamp_sigma(raw_sigma, calibrated_sigma, sigma_constraints) calibrated_sigma = _clamp_sigma(raw_sigma, calibrated_sigma, sigma_constraints)
calibrated_distribution, calibrated_sorted = _bucket_probabilities( calibrated_distribution, calibrated_sorted, calibrated_distribution_all = _bucket_probabilities(
calibrated_mu, calibrated_mu,
calibrated_sigma, calibrated_sigma,
max_so_far=max_so_far, max_so_far=max_so_far,
@@ -393,23 +397,29 @@ def apply_probability_calibration(
engine = ENGINE_MODE_LEGACY engine = ENGINE_MODE_LEGACY
selected_distribution = legacy_distribution or [] selected_distribution = legacy_distribution or []
selected_distribution_all = legacy_distribution or []
selected_sorted: List[Tuple[int, float]] = [] selected_sorted: List[Tuple[int, float]] = []
shadow_distribution: List[Dict[str, Any]] = [] shadow_distribution: List[Dict[str, Any]] = []
shadow_distribution_all: List[Dict[str, Any]] = []
shadow_sorted: List[Tuple[int, float]] = [] shadow_sorted: List[Tuple[int, float]] = []
if selected_mode == ENGINE_MODE_EMOS_PRIMARY: if selected_mode == ENGINE_MODE_EMOS_PRIMARY:
engine = "emos" engine = "emos"
selected_distribution = calibrated_distribution selected_distribution = calibrated_distribution
selected_distribution_all = calibrated_distribution_all
selected_sorted = calibrated_sorted selected_sorted = calibrated_sorted
elif selected_mode == ENGINE_MODE_EMOS_SHADOW: elif selected_mode == ENGINE_MODE_EMOS_SHADOW:
shadow_distribution = calibrated_distribution shadow_distribution = calibrated_distribution
shadow_distribution_all = calibrated_distribution_all
shadow_sorted = calibrated_sorted shadow_sorted = calibrated_sorted
return { return {
"mode": selected_mode, "mode": selected_mode,
"engine": engine, "engine": engine,
"distribution": selected_distribution, "distribution": selected_distribution,
"distribution_all": selected_distribution_all,
"selected_sorted_probs": selected_sorted, "selected_sorted_probs": selected_sorted,
"shadow_distribution": shadow_distribution, "shadow_distribution": shadow_distribution,
"shadow_distribution_all": shadow_distribution_all,
"shadow_sorted_probs": shadow_sorted, "shadow_sorted_probs": shadow_sorted,
"raw_mu": raw_mu, "raw_mu": raw_mu,
"raw_sigma": raw_sigma, "raw_sigma": raw_sigma,
@@ -551,7 +561,7 @@ def fit_calibration(
== apply_city_settlement(city, actual_high) == apply_city_settlement(city, actual_high)
else 0.0 else 0.0
) )
legacy_distribution, _ = _bucket_probabilities( legacy_distribution, _, _ = _bucket_probabilities(
legacy_mu, legacy_mu,
legacy_sigma, legacy_sigma,
max_so_far=None, max_so_far=None,
@@ -627,7 +637,7 @@ def fit_calibration(
city = row["city"] city = row["city"]
crps_values.append(_gaussian_crps(actual_high, mu_hat, sigma_hat)) crps_values.append(_gaussian_crps(actual_high, mu_hat, sigma_hat))
mae_values.append(abs(mu_hat - actual_high)) mae_values.append(abs(mu_hat - actual_high))
distribution, _ = _bucket_probabilities( distribution, _, _ = _bucket_probabilities(
mu_hat, mu_hat,
sigma_hat, sigma_hat,
max_so_far=None, max_so_far=None,
+13 -2
View File
@@ -594,7 +594,9 @@ def analyze_weather_trend(
# === Probability Engine === # === Probability Engine ===
probabilities: List[Dict[str, Any]] = [] probabilities: List[Dict[str, Any]] = []
probabilities_all: List[Dict[str, Any]] = []
shadow_probabilities: List[Dict[str, Any]] = [] shadow_probabilities: List[Dict[str, Any]] = []
shadow_probabilities_all: List[Dict[str, Any]] = []
forecast_miss_deg = 0.0 forecast_miss_deg = 0.0
probability_features = None probability_features = None
calibration_summary = { calibration_summary = {
@@ -621,6 +623,7 @@ def analyze_weather_trend(
probabilities = [ probabilities = [
{"value": settled_wu, "range": f"[{settled_wu-0.5}~{settled_wu+0.5})", "probability": 1.0} {"value": settled_wu, "range": f"[{settled_wu-0.5}~{settled_wu+0.5})", "probability": 1.0}
] ]
probabilities_all = probabilities
elif (ens_p10 is not None and ens_p90 is not None) or fallback_sigma: elif (ens_p10 is not None and ens_p90 is not None) or fallback_sigma:
# Forecast miss magnitude # Forecast miss magnitude
if max_so_far is not None and forecast_median is not None: if max_so_far is not None and forecast_median is not None:
@@ -665,6 +668,7 @@ def analyze_weather_trend(
) )
mu = probs_result.get("mu", mu) mu = probs_result.get("mu", mu)
probabilities = probs_result.get("probabilities", []) probabilities = probs_result.get("probabilities", [])
probabilities_all = probs_result.get("probabilities_all", probabilities)
sorted_probs = probs_result.get("sorted_probs", []) sorted_probs = probs_result.get("sorted_probs", [])
probability_features = build_probability_features( probability_features = build_probability_features(
@@ -698,10 +702,12 @@ def analyze_weather_trend(
"calibration_source": calibration_result.get("calibration_source"), "calibration_source": calibration_result.get("calibration_source"),
} }
shadow_probabilities = calibration_result.get("shadow_distribution") or [] shadow_probabilities = calibration_result.get("shadow_distribution") or []
shadow_probabilities_all = calibration_result.get("shadow_distribution_all") or shadow_probabilities
if calibration_result.get("engine") == "emos": if calibration_result.get("engine") == "emos":
mu = calibration_result.get("calibrated_mu", mu) mu = calibration_result.get("calibrated_mu", mu)
sigma = calibration_result.get("calibrated_sigma", sigma) sigma = calibration_result.get("calibrated_sigma", sigma)
probabilities = calibration_result.get("distribution") or probabilities probabilities = calibration_result.get("distribution") or probabilities
probabilities_all = calibration_result.get("distribution_all") or probabilities_all or probabilities
sorted_probs = calibration_result.get("selected_sorted_probs") or sorted_probs sorted_probs = calibration_result.get("selected_sorted_probs") or sorted_probs
if sorted_probs: if sorted_probs:
@@ -926,7 +932,9 @@ def analyze_weather_trend(
structured = { structured = {
"mu": mu, "mu": mu,
"probabilities": probabilities, "probabilities": probabilities,
"probabilities_all": probabilities_all or probabilities,
"shadow_probabilities": shadow_probabilities, "shadow_probabilities": shadow_probabilities,
"shadow_probabilities_all": shadow_probabilities_all or shadow_probabilities,
"probability_engine": calibration_summary["engine"], "probability_engine": calibration_summary["engine"],
"probability_calibration_mode": calibration_summary["mode"], "probability_calibration_mode": calibration_summary["mode"],
"probability_calibration_version": calibration_summary["calibration_version"], "probability_calibration_version": calibration_summary["calibration_version"],
@@ -999,21 +1007,24 @@ def calculate_prob_distribution(
total_p = sum(probs.values()) total_p = sum(probs.values())
sorted_probs = [] sorted_probs = []
probabilities = [] probabilities = []
probabilities_all = []
if total_p > 0: if total_p > 0:
norm_probs = {k: v / total_p for k, v in probs.items()} 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) sorted_probs = sorted(norm_probs.items(), key=lambda x: x[1], reverse=True)
for t, p in sorted_probs[:4]: for t, p in sorted_probs:
rng_str = f"[{t}.0~{t+1}.0)" if is_exact else f"[{t-0.5}~{t+0.5})" rng_str = f"[{t}.0~{t+1}.0)" if is_exact else f"[{t-0.5}~{t+0.5})"
probabilities.append({ probabilities_all.append({
"value": int(t), "value": int(t),
"range": rng_str, "range": rng_str,
"probability": round(p, 3) "probability": round(p, 3)
}) })
probabilities = probabilities_all[:4]
return { return {
"mu": mu, "mu": mu,
"sigma": sigma, "sigma": sigma,
"probabilities": probabilities, "probabilities": probabilities,
"probabilities_all": probabilities_all,
"sorted_probs": sorted_probs "sorted_probs": sorted_probs
} }
+2
View File
@@ -96,6 +96,7 @@ def test_shadow_mode_keeps_legacy_distribution(tmp_path):
assert result["shadow_distribution"] assert result["shadow_distribution"]
assert result["calibrated_mu"] == 10.5 assert result["calibrated_mu"] == 10.5
assert result["calibrated_sigma"] == 1.2 assert result["calibrated_sigma"] == 1.2
assert len(result["shadow_distribution_all"]) >= len(result["shadow_distribution"])
def test_primary_mode_switches_to_calibrated_distribution(tmp_path): def test_primary_mode_switches_to_calibrated_distribution(tmp_path):
@@ -129,6 +130,7 @@ def test_primary_mode_switches_to_calibrated_distribution(tmp_path):
assert result["calibrated_mu"] == 10.5 assert result["calibrated_mu"] == 10.5
assert result["calibrated_sigma"] == 1.2 assert result["calibrated_sigma"] == 1.2
assert result["distribution"] assert result["distribution"]
assert len(result["distribution_all"]) >= len(result["distribution"])
assert result["distribution"][0]["value"] >= 10 assert result["distribution"][0]["value"] >= 10
+6
View File
@@ -2006,7 +2006,9 @@ def _analyze(
from src.analysis.trend_engine import analyze_weather_trend as _trend_analyze, calculate_prob_distribution from src.analysis.trend_engine import analyze_weather_trend as _trend_analyze, calculate_prob_distribution
probabilities = [] probabilities = []
probabilities_all = []
shadow_probabilities = [] shadow_probabilities = []
shadow_probabilities_all = []
mu = None mu = None
probability_engine = "legacy" probability_engine = "legacy"
probability_calibration_mode = "legacy" probability_calibration_mode = "legacy"
@@ -2022,7 +2024,9 @@ def _analyze(
# Use structured data from shared engine # Use structured data from shared engine
mu = sd.get("mu") mu = sd.get("mu")
probabilities = sd.get("probabilities", []) probabilities = sd.get("probabilities", [])
probabilities_all = sd.get("probabilities_all", probabilities)
shadow_probabilities = sd.get("shadow_probabilities", []) shadow_probabilities = sd.get("shadow_probabilities", [])
shadow_probabilities_all = sd.get("shadow_probabilities_all", shadow_probabilities)
probability_engine = sd.get("probability_engine", "legacy") probability_engine = sd.get("probability_engine", "legacy")
probability_calibration_mode = sd.get("probability_calibration_mode", "legacy") probability_calibration_mode = sd.get("probability_calibration_mode", "legacy")
probability_calibration_version = sd.get("probability_calibration_version") probability_calibration_version = sd.get("probability_calibration_version")
@@ -2406,6 +2410,7 @@ def _analyze(
"probabilities": { "probabilities": {
"mu": round(mu, 1) if mu is not None else None, "mu": round(mu, 1) if mu is not None else None,
"distribution": probabilities, "distribution": probabilities,
"distribution_all": probabilities_all or probabilities,
"engine": probability_engine, "engine": probability_engine,
"calibration_mode": probability_calibration_mode, "calibration_mode": probability_calibration_mode,
"calibration_version": probability_calibration_version, "calibration_version": probability_calibration_version,
@@ -2414,6 +2419,7 @@ def _analyze(
"calibrated_mu": probability_calibrated_mu, "calibrated_mu": probability_calibrated_mu,
"calibrated_sigma": probability_calibrated_sigma, "calibrated_sigma": probability_calibrated_sigma,
"shadow_distribution": shadow_probabilities, "shadow_distribution": shadow_probabilities,
"shadow_distribution_all": shadow_probabilities_all or shadow_probabilities,
}, },
"trend": trend_info, "trend": trend_info,
"peak": { "peak": {