Expose DEB quality guidance
This commit is contained in:
@@ -1213,6 +1213,7 @@ export function LiveTemperatureThresholdChart({
|
||||
observedHighRunway={observedHighRunway}
|
||||
wundergroundDailyHigh={wundergroundDailyHigh}
|
||||
debVal={debVal}
|
||||
debQuality={chartHourly?.debQuality || null}
|
||||
modelMin={modelMin}
|
||||
modelMax={modelMax}
|
||||
spread={spread}
|
||||
|
||||
@@ -38,6 +38,50 @@ function highLabel(label: string, isEn: boolean) {
|
||||
return isEn ? (HIGH_LABEL_EN[label] || label) : label;
|
||||
}
|
||||
|
||||
type DebQuality = {
|
||||
quality_tier?: string | null;
|
||||
recommendation?: string | null;
|
||||
recent_hit_rate?: number | null;
|
||||
recent_samples?: number | null;
|
||||
};
|
||||
|
||||
function debQualityLabel(quality: DebQuality | null | undefined, isEn: boolean) {
|
||||
const recommendation = quality?.recommendation;
|
||||
if (recommendation === "primary") return isEn ? "Primary" : "主用";
|
||||
if (recommendation === "supporting") return isEn ? "Support" : "辅助";
|
||||
if (recommendation === "context_only") return isEn ? "Context" : "参考";
|
||||
if (recommendation === "insufficient") return isEn ? "Thin" : "样本少";
|
||||
return "";
|
||||
}
|
||||
|
||||
function debQualityClass(quality: DebQuality | null | undefined) {
|
||||
const tier = quality?.quality_tier;
|
||||
if (tier === "high") return "border-emerald-200 bg-emerald-50 text-emerald-700";
|
||||
if (tier === "medium") return "border-amber-200 bg-amber-50 text-amber-700";
|
||||
if (tier === "low") return "border-rose-200 bg-rose-50 text-rose-700";
|
||||
return "border-slate-200 bg-slate-50 text-slate-500";
|
||||
}
|
||||
|
||||
function DebQualityBadge({ quality, isEn }: { quality?: DebQuality | null; isEn: boolean }) {
|
||||
const label = debQualityLabel(quality, isEn);
|
||||
if (!label) return null;
|
||||
const hitRate = quality?.recent_hit_rate;
|
||||
const samples = quality?.recent_samples;
|
||||
const titleParts = [
|
||||
isEn ? `DEB recommendation: ${label}` : `DEB 建议:${label}`,
|
||||
hitRate == null ? null : `${hitRate.toFixed(0)}%`,
|
||||
samples == null ? null : `n=${samples}`,
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<span
|
||||
className={clsx("ml-1.5 inline-flex items-center rounded border px-1.5 py-0.5 text-[9px] font-black uppercase leading-none", debQualityClass(quality))}
|
||||
title={titleParts.join(" · ")}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function buildStatsLabels({
|
||||
isEn,
|
||||
isShenzhen,
|
||||
@@ -82,6 +126,7 @@ export function TemperatureStatsBars({
|
||||
observedHighRunway,
|
||||
wundergroundDailyHigh,
|
||||
debVal,
|
||||
debQuality,
|
||||
modelMin,
|
||||
modelMax,
|
||||
spread,
|
||||
@@ -104,6 +149,7 @@ export function TemperatureStatsBars({
|
||||
observedHighRunway: number | null;
|
||||
wundergroundDailyHigh: number | null;
|
||||
debVal: number | null;
|
||||
debQuality?: DebQuality | null;
|
||||
modelMin: number | null;
|
||||
modelMax: number | null;
|
||||
spread: number | null;
|
||||
@@ -139,6 +185,7 @@ export function TemperatureStatsBars({
|
||||
<div className="flex items-center gap-4 text-[11px]">
|
||||
<span className="font-semibold text-slate-500">
|
||||
DEB: <strong className="text-orange-600 font-mono">{temp(debVal, tempSymbol)}</strong>
|
||||
<DebQualityBadge quality={debQuality} isEn={isEn} />
|
||||
</span>
|
||||
{modelMin !== null && modelMax !== null && (
|
||||
<>
|
||||
@@ -187,6 +234,7 @@ export function TemperatureStatsBars({
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[11px] font-semibold text-slate-500 uppercase tracking-wider">
|
||||
DEB Max
|
||||
<DebQualityBadge quality={debQuality} isEn={isEn} />
|
||||
</span>
|
||||
<span className="text-2xl font-bold font-mono text-orange-600 mt-1">
|
||||
{temp(debVal, tempSymbol)}
|
||||
@@ -237,6 +285,7 @@ export function TemperatureStatsBars({
|
||||
</span>
|
||||
<strong className="text-blue-600 font-bold">
|
||||
{temp(debVal, tempSymbol)}
|
||||
<DebQualityBadge quality={debQuality} isEn={isEn} />
|
||||
</strong>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
@@ -263,3 +312,4 @@ export function TemperatureStatsBars({
|
||||
}
|
||||
|
||||
export const __buildTemperatureStatsLabelsForTest = buildStatsLabels;
|
||||
export const __buildDebQualityLabelForTest = debQualityLabel;
|
||||
|
||||
+9
-1
@@ -1,4 +1,4 @@
|
||||
import { __buildTemperatureStatsLabelsForTest } from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
|
||||
import { __buildDebQualityLabelForTest, __buildTemperatureStatsLabelsForTest } from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
|
||||
import { temp } from "@/components/dashboard/scan-terminal/utils";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
@@ -63,4 +63,12 @@ export function runTests() {
|
||||
assert(temp(null, "°C") === "--", "empty temperature values should not render as 0.0°C while city detail is loading");
|
||||
assert(temp(undefined, "°C") === "--", "undefined temperature values should not render as 0.0°C while city detail is loading");
|
||||
assert(temp("", "°C") === "--", "blank temperature values should not render as 0.0°C while city detail is loading");
|
||||
assert(
|
||||
__buildDebQualityLabelForTest({ recommendation: "context_only" }, true) === "Context",
|
||||
"low-confidence DEB should render as context-only guidance in English",
|
||||
);
|
||||
assert(
|
||||
__buildDebQualityLabelForTest({ recommendation: "insufficient" }, false) === "样本少",
|
||||
"thin-sample DEB should render a Chinese low-sample label",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ScanOpportunityRow,
|
||||
ForecastDay,
|
||||
DailyModelForecast,
|
||||
DebForecast,
|
||||
DebHourlyPath,
|
||||
ProbabilityBucket,
|
||||
} from "@/lib/dashboard-types";
|
||||
@@ -940,6 +941,7 @@ function runwayPatchPointsFromRunwayObs(runwayObs: any) {
|
||||
type HourlyForecast = {
|
||||
forecastTodayHigh?: number | null;
|
||||
debPrediction?: number | null;
|
||||
debQuality?: Pick<DebForecast, "quality_tier" | "recommendation" | "recent_hit_rate" | "recent_samples" | "recent_hits" | "recent_mae"> | null;
|
||||
debHourlyPath?: DebHourlyPath | null;
|
||||
localDate?: string | null;
|
||||
localTime?: string | null;
|
||||
@@ -967,6 +969,7 @@ function seedHourlyForecastFromRow(row: ScanOpportunityRow | null): HourlyForeca
|
||||
return {
|
||||
forecastTodayHigh: null,
|
||||
debPrediction: validNumber(row.deb_prediction),
|
||||
debQuality: null,
|
||||
debHourlyPath: null,
|
||||
localDate: row.local_date || null,
|
||||
localTime: row.local_time || null,
|
||||
@@ -1029,6 +1032,14 @@ function parseHourlyForecastFromCityDetail(json: CityDetail | null): HourlyForec
|
||||
return {
|
||||
forecastTodayHigh: json.forecast?.today_high ?? null,
|
||||
debPrediction: json.deb?.prediction ?? (json as any)?.overview?.deb_prediction ?? null,
|
||||
debQuality: json.deb ? {
|
||||
quality_tier: json.deb.quality_tier,
|
||||
recommendation: json.deb.recommendation,
|
||||
recent_hit_rate: json.deb.recent_hit_rate,
|
||||
recent_samples: json.deb.recent_samples,
|
||||
recent_hits: json.deb.recent_hits,
|
||||
recent_mae: json.deb.recent_mae,
|
||||
} : null,
|
||||
debHourlyPath: json.deb?.hourly_path || null,
|
||||
localDate: json.local_date || (json as any)?.overview?.local_date || null,
|
||||
localTime: json.local_time || null,
|
||||
@@ -1366,6 +1377,7 @@ function mergePatchIntoHourly(
|
||||
...(prev || {
|
||||
forecastTodayHigh: null,
|
||||
debPrediction: null,
|
||||
debQuality: null,
|
||||
localDate: null,
|
||||
localTime: null,
|
||||
times: [],
|
||||
|
||||
@@ -228,6 +228,12 @@ export interface DebForecast {
|
||||
weights_info?: string | null;
|
||||
bias_adjustment?: number | null;
|
||||
bias_samples?: number | null;
|
||||
quality_tier?: string | null;
|
||||
recommendation?: string | null;
|
||||
recent_hit_rate?: number | null;
|
||||
recent_samples?: number | null;
|
||||
recent_hits?: number | null;
|
||||
recent_mae?: number | null;
|
||||
intraday_adjustment?: number | null;
|
||||
hourly_path?: DebHourlyPath | null;
|
||||
hourly_correction?: Record<string, unknown> | null;
|
||||
@@ -328,6 +334,12 @@ export interface DailyModelForecast {
|
||||
models?: Record<string, number | null>;
|
||||
deb?: {
|
||||
prediction?: number | null;
|
||||
quality_tier?: string | null;
|
||||
recommendation?: string | null;
|
||||
recent_hit_rate?: number | null;
|
||||
recent_samples?: number | null;
|
||||
recent_hits?: number | null;
|
||||
recent_mae?: number | null;
|
||||
};
|
||||
probabilities?: ProbabilityBucket[];
|
||||
probabilities_all?: ProbabilityBucket[];
|
||||
|
||||
@@ -1249,6 +1249,79 @@ def calculate_dynamic_weight_components(
|
||||
}
|
||||
|
||||
|
||||
def _assess_recent_deb_quality(
|
||||
city_name,
|
||||
history_rows,
|
||||
*,
|
||||
adjustment=0.0,
|
||||
lookback_days=30,
|
||||
):
|
||||
city_key = str(city_name or "").strip().lower()
|
||||
rows = [
|
||||
row
|
||||
for row in (history_rows or [])
|
||||
if str(row.get("city") or "").strip().lower() == city_key
|
||||
]
|
||||
rows.sort(key=lambda row: str(row.get("target_date") or ""), reverse=True)
|
||||
recent = rows[: max(int(lookback_days or 0), 1)]
|
||||
hits = 0
|
||||
samples = 0
|
||||
errors = []
|
||||
for row in recent:
|
||||
prediction = _sf(row.get("prediction", row.get("deb_prediction")))
|
||||
actual = _sf(row.get("actual", row.get("actual_high")))
|
||||
if prediction is None or actual is None:
|
||||
continue
|
||||
effective_prediction = prediction + float(adjustment or 0.0)
|
||||
try:
|
||||
pred_bucket = apply_city_settlement(city_key, effective_prediction)
|
||||
actual_bucket = apply_city_settlement(city_key, actual)
|
||||
except Exception:
|
||||
continue
|
||||
if pred_bucket is None or actual_bucket is None:
|
||||
continue
|
||||
samples += 1
|
||||
if pred_bucket == actual_bucket:
|
||||
hits += 1
|
||||
errors.append(abs(effective_prediction - actual))
|
||||
|
||||
hit_rate = (hits / samples * 100.0) if samples else None
|
||||
mae = (sum(errors) / len(errors)) if errors else None
|
||||
if samples < 3 or hit_rate is None or mae is None:
|
||||
tier = "insufficient"
|
||||
recommendation = "insufficient"
|
||||
elif hit_rate >= 67.0 and mae <= 1.25:
|
||||
tier = "high"
|
||||
recommendation = "primary"
|
||||
elif hit_rate >= 34.0 and mae <= 1.75:
|
||||
tier = "medium"
|
||||
recommendation = "supporting"
|
||||
else:
|
||||
tier = "low"
|
||||
recommendation = "context_only"
|
||||
|
||||
return {
|
||||
"quality_tier": tier,
|
||||
"recommendation": recommendation,
|
||||
"recent_hit_rate": round(hit_rate, 1) if hit_rate is not None else None,
|
||||
"recent_samples": samples,
|
||||
"recent_hits": hits,
|
||||
"recent_mae": round(mae, 2) if mae is not None else None,
|
||||
}
|
||||
|
||||
|
||||
def _append_deb_quality_note(weights_info, quality):
|
||||
tier = (quality or {}).get("quality_tier")
|
||||
if tier not in {"low", "insufficient"}:
|
||||
return weights_info
|
||||
note = f"quality:{tier}"
|
||||
if weights_info:
|
||||
if note in weights_info:
|
||||
return weights_info
|
||||
return f"{weights_info} | {note}"
|
||||
return note
|
||||
|
||||
|
||||
def calculate_deb_prediction(
|
||||
city_name,
|
||||
current_forecasts,
|
||||
@@ -1283,13 +1356,22 @@ def calculate_deb_prediction(
|
||||
decay_factor=decay_factor,
|
||||
)
|
||||
if raw_prediction is None:
|
||||
quality = {
|
||||
"quality_tier": "insufficient",
|
||||
"recommendation": "insufficient",
|
||||
"recent_hit_rate": None,
|
||||
"recent_samples": 0,
|
||||
"recent_hits": 0,
|
||||
"recent_mae": None,
|
||||
}
|
||||
return {
|
||||
"prediction": None,
|
||||
"raw_prediction": None,
|
||||
"version": DEB_RAW_VERSION,
|
||||
"weights_info": weights_info,
|
||||
"weights_info": _append_deb_quality_note(weights_info, quality),
|
||||
"bias_adjustment": 0.0,
|
||||
"bias_samples": 0,
|
||||
**quality,
|
||||
}
|
||||
|
||||
data = load_history(_get_history_file_path())
|
||||
@@ -1309,14 +1391,21 @@ def calculate_deb_prediction(
|
||||
|
||||
bias_adjustment = float(corrected.get("bias_adjustment") or 0.0)
|
||||
bias_samples = int(corrected.get("samples") or 0)
|
||||
quality = _assess_recent_deb_quality(
|
||||
city_name,
|
||||
history_rows,
|
||||
adjustment=bias_adjustment,
|
||||
lookback_days=bias_lookback_days,
|
||||
)
|
||||
if bias_samples <= 0:
|
||||
return {
|
||||
"prediction": raw_prediction,
|
||||
"raw_prediction": raw_prediction,
|
||||
"version": DEB_RAW_VERSION,
|
||||
"weights_info": weights_info,
|
||||
"weights_info": _append_deb_quality_note(weights_info, quality),
|
||||
"bias_adjustment": 0.0,
|
||||
"bias_samples": 0,
|
||||
**quality,
|
||||
}
|
||||
|
||||
next_weights_info = weights_info
|
||||
@@ -1330,6 +1419,7 @@ def calculate_deb_prediction(
|
||||
f"{weights_info or 'DEB'} | "
|
||||
f"{correction_label}({bias_adjustment:+.1f},n={bias_samples})"
|
||||
)
|
||||
next_weights_info = _append_deb_quality_note(next_weights_info, quality)
|
||||
return {
|
||||
"prediction": corrected["corrected_prediction"],
|
||||
"raw_prediction": corrected["raw_prediction"],
|
||||
@@ -1337,6 +1427,7 @@ def calculate_deb_prediction(
|
||||
"weights_info": next_weights_info,
|
||||
"bias_adjustment": bias_adjustment,
|
||||
"bias_samples": bias_samples,
|
||||
**quality,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -449,6 +449,7 @@ def analyze_weather_trend(
|
||||
deb_bias_adjustment = 0.0
|
||||
deb_bias_samples = 0
|
||||
deb_weights = ""
|
||||
deb_quality = {}
|
||||
if city_name and current_forecasts:
|
||||
deb_result = calculate_deb_prediction(
|
||||
city_name,
|
||||
@@ -462,6 +463,14 @@ def analyze_weather_trend(
|
||||
deb_bias_adjustment = deb_result.get("bias_adjustment") or 0.0
|
||||
deb_bias_samples = deb_result.get("bias_samples") or 0
|
||||
deb_weights = deb_result.get("weights_info") or ""
|
||||
deb_quality = {
|
||||
"quality_tier": deb_result.get("quality_tier"),
|
||||
"recommendation": deb_result.get("recommendation"),
|
||||
"recent_hit_rate": deb_result.get("recent_hit_rate"),
|
||||
"recent_samples": deb_result.get("recent_samples"),
|
||||
"recent_hits": deb_result.get("recent_hits"),
|
||||
"recent_mae": deb_result.get("recent_mae"),
|
||||
}
|
||||
insights.insert(
|
||||
0,
|
||||
f"🧬 <b>DEB 融合预测</b>:<b>{deb_prediction}{temp_symbol}</b> ({deb_weights})",
|
||||
@@ -992,6 +1001,7 @@ def analyze_weather_trend(
|
||||
"deb_bias_adjustment": deb_bias_adjustment,
|
||||
"deb_bias_samples": deb_bias_samples,
|
||||
"deb_weights": deb_weights,
|
||||
"deb_quality": deb_quality,
|
||||
"current_forecasts": current_forecasts,
|
||||
"ens_data": ens_data,
|
||||
"forecast_miss_deg": forecast_miss_deg,
|
||||
|
||||
@@ -174,6 +174,52 @@ def test_calculate_deb_prediction_prefers_bucket_calibration_when_enough_samples
|
||||
assert result["bias_samples"] == 5
|
||||
|
||||
|
||||
def test_calculate_deb_prediction_reports_recent_quality_for_effective_deb(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"src.analysis.deb_algorithm.load_history",
|
||||
lambda _: {
|
||||
"high": {
|
||||
"2026-04-11": {"actual_high": 21.0, "deb_prediction": 21.0},
|
||||
"2026-04-12": {"actual_high": 22.0, "deb_prediction": 22.0},
|
||||
"2026-04-13": {"actual_high": 23.0, "deb_prediction": 23.0},
|
||||
"2026-04-14": {"actual_high": 24.0, "deb_prediction": 24.0},
|
||||
"2026-04-15": {"actual_high": 25.0, "deb_prediction": 25.0},
|
||||
},
|
||||
"low": {
|
||||
"2026-04-11": {"actual_high": 20.0, "deb_prediction": 16.0},
|
||||
"2026-04-12": {"actual_high": 21.0, "deb_prediction": 26.0},
|
||||
"2026-04-13": {"actual_high": 22.0, "deb_prediction": 18.0},
|
||||
"2026-04-14": {"actual_high": 23.0, "deb_prediction": 28.0},
|
||||
"2026-04-15": {"actual_high": 24.0, "deb_prediction": 20.0},
|
||||
},
|
||||
"thin": {
|
||||
"2026-04-14": {"actual_high": 23.0, "deb_prediction": 23.0},
|
||||
"2026-04-15": {"actual_high": 24.0, "deb_prediction": 24.0},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def raw(_city, _forecasts, **_kwargs):
|
||||
return 25.0, "raw"
|
||||
|
||||
high = calculate_deb_prediction("high", {"ECMWF": 25.0}, raw_calculator=raw)
|
||||
low = calculate_deb_prediction("low", {"ECMWF": 25.0}, raw_calculator=raw)
|
||||
thin = calculate_deb_prediction("thin", {"ECMWF": 25.0}, raw_calculator=raw)
|
||||
|
||||
assert high["quality_tier"] == "high"
|
||||
assert high["recommendation"] == "primary"
|
||||
assert high["recent_hit_rate"] == 100.0
|
||||
|
||||
assert low["quality_tier"] == "low"
|
||||
assert low["recommendation"] == "context_only"
|
||||
assert low["recent_hit_rate"] == 0.0
|
||||
assert "quality:low" in low["weights_info"]
|
||||
|
||||
assert thin["quality_tier"] == "insufficient"
|
||||
assert thin["recommendation"] == "insufficient"
|
||||
assert thin["recent_samples"] == 2
|
||||
|
||||
|
||||
def test_compute_hourly_model_errors_basic():
|
||||
from src.analysis.deb_algorithm import compute_hourly_model_errors
|
||||
|
||||
|
||||
@@ -1198,6 +1198,7 @@ def _analyze(
|
||||
deb_bias_adjustment, deb_bias_samples = 0.0, 0
|
||||
deb_intraday_adjustment = 0.0
|
||||
deb_hourly_consensus = None
|
||||
deb_quality = {}
|
||||
if current_forecasts:
|
||||
deb_result = calculate_deb_prediction(city, current_forecasts)
|
||||
if deb_result.get("prediction") is not None:
|
||||
@@ -1207,6 +1208,14 @@ def _analyze(
|
||||
deb_bias_adjustment = deb_result.get("bias_adjustment") or 0.0
|
||||
deb_bias_samples = deb_result.get("bias_samples") or 0
|
||||
deb_weights = deb_result.get("weights_info") or ""
|
||||
deb_quality = {
|
||||
"quality_tier": deb_result.get("quality_tier"),
|
||||
"recommendation": deb_result.get("recommendation"),
|
||||
"recent_hit_rate": deb_result.get("recent_hit_rate"),
|
||||
"recent_samples": deb_result.get("recent_samples"),
|
||||
"recent_hits": deb_result.get("recent_hits"),
|
||||
"recent_mae": deb_result.get("recent_mae"),
|
||||
}
|
||||
deb_hourly_consensus = build_deb_hourly_consensus_path(
|
||||
city=city,
|
||||
hourly_times=mm.get("hourly_times") or [],
|
||||
@@ -1366,6 +1375,7 @@ def _analyze(
|
||||
deb_bias_adjustment = sd.get("deb_bias_adjustment") or 0.0
|
||||
deb_bias_samples = sd.get("deb_bias_samples") or 0
|
||||
deb_weights = sd.get("deb_weights", "")
|
||||
deb_quality = sd.get("deb_quality") or deb_quality
|
||||
if deb_hourly_consensus is None and sd.get("deb_hourly_consensus"):
|
||||
deb_hourly_consensus = sd.get("deb_hourly_consensus")
|
||||
|
||||
@@ -1675,6 +1685,7 @@ def _analyze(
|
||||
d_val, d_winfo = None, ""
|
||||
d_raw_val, d_version = None, None
|
||||
d_bias_adjustment, d_bias_samples = 0.0, 0
|
||||
d_quality = {}
|
||||
d_probs = []
|
||||
d_probs_all = []
|
||||
if day_m:
|
||||
@@ -1688,6 +1699,14 @@ def _analyze(
|
||||
d_bias_adjustment = deb_result.get("bias_adjustment") or 0.0
|
||||
d_bias_samples = deb_result.get("bias_samples") or 0
|
||||
d_winfo = deb_result.get("weights_info") or ""
|
||||
d_quality = {
|
||||
"quality_tier": deb_result.get("quality_tier"),
|
||||
"recommendation": deb_result.get("recommendation"),
|
||||
"recent_hit_rate": deb_result.get("recent_hit_rate"),
|
||||
"recent_samples": deb_result.get("recent_samples"),
|
||||
"recent_hits": deb_result.get("recent_hits"),
|
||||
"recent_mae": deb_result.get("recent_mae"),
|
||||
}
|
||||
|
||||
# Calculate future probability based on model divergence
|
||||
m_vals = [v for v in day_m.values() if v is not None]
|
||||
@@ -1714,6 +1733,7 @@ def _analyze(
|
||||
"weights_info": d_winfo,
|
||||
"bias_adjustment": d_bias_adjustment,
|
||||
"bias_samples": d_bias_samples,
|
||||
**d_quality,
|
||||
},
|
||||
"probabilities": d_probs if i > 0 else probabilities, # Use today's real prob for today
|
||||
"probabilities_all": d_probs_all if i > 0 else probabilities_all,
|
||||
@@ -1880,6 +1900,7 @@ def _analyze(
|
||||
"hourly_consensus": deb_hourly_consensus,
|
||||
"hourly_path": deb_hourly_path,
|
||||
"hourly_correction": (deb_hourly_path or {}).get("correction") if isinstance(deb_hourly_path, dict) else None,
|
||||
**deb_quality,
|
||||
},
|
||||
"deviation_monitor": deviation_monitor,
|
||||
"ensemble": ens_data,
|
||||
@@ -2201,6 +2222,7 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
deb_version = None
|
||||
deb_bias_adjustment = 0.0
|
||||
deb_bias_samples = 0
|
||||
deb_quality = {}
|
||||
if current_forecasts:
|
||||
deb_result = calculate_deb_prediction(city, current_forecasts)
|
||||
if deb_result.get("prediction") is not None:
|
||||
@@ -2209,6 +2231,14 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
deb_version = deb_result.get("version")
|
||||
deb_bias_adjustment = deb_result.get("bias_adjustment") or 0.0
|
||||
deb_bias_samples = deb_result.get("bias_samples") or 0
|
||||
deb_quality = {
|
||||
"quality_tier": deb_result.get("quality_tier"),
|
||||
"recommendation": deb_result.get("recommendation"),
|
||||
"recent_hit_rate": deb_result.get("recent_hit_rate"),
|
||||
"recent_samples": deb_result.get("recent_samples"),
|
||||
"recent_hits": deb_result.get("recent_hits"),
|
||||
"recent_mae": deb_result.get("recent_mae"),
|
||||
}
|
||||
if deb_val is None:
|
||||
deb_val = om_today
|
||||
|
||||
@@ -2285,6 +2315,7 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
"version": deb_version,
|
||||
"bias_adjustment": deb_bias_adjustment,
|
||||
"bias_samples": deb_bias_samples,
|
||||
**deb_quality,
|
||||
},
|
||||
"deviation_monitor": deviation_monitor or {},
|
||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||
|
||||
Reference in New Issue
Block a user