From ec84243fb91684607938bfe5c3a636fe46e41c6e Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 8 Jun 2026 12:27:20 +0800 Subject: [PATCH] Show usable recent DEB accuracy --- .../scan-terminal/TrainingDashboard.tsx | 21 +++++++- .../ops/training/TrainingPageClient.tsx | 20 +++++++- frontend/lib/ops-api.ts | 12 +++++ tests/test_ops_training_accuracy.py | 41 +++++++++++++++ web/services/ops_api.py | 50 +++++++++++++++++++ 5 files changed, 142 insertions(+), 2 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/TrainingDashboard.tsx b/frontend/components/dashboard/scan-terminal/TrainingDashboard.tsx index 5ac83dff..8a882d6b 100644 --- a/frontend/components/dashboard/scan-terminal/TrainingDashboard.tsx +++ b/frontend/components/dashboard/scan-terminal/TrainingDashboard.tsx @@ -40,6 +40,19 @@ type DebHistoricalSummary = { hits?: number; }; +type DebUsableRecentSummary = { + window?: "recent_7d" | "recent_14d" | string; + city_count?: number; + samples?: number; + hits?: number; + hit_rate?: number | null; + avg_mae?: number | null; + recommendations?: { + primary?: number; + supporting?: number; + }; +}; + type DebVersionSummary = { version?: string; samples?: number; @@ -51,6 +64,7 @@ type DebVersionSummary = { type DebSummaryPayload = { historical?: DebHistoricalSummary; + usable_recent?: DebUsableRecentSummary; recent_7d?: DebWindowSummary; recent_14d?: DebWindowSummary; versions?: Record; @@ -178,6 +192,7 @@ export function TrainingDashboard({ isEn }: { isEn: boolean }) { cities: debSummary?.historical?.city_count ?? debSorted.length, sampleDays: debSummary?.historical?.sample_days, weightedHit: debSummary?.historical?.weighted_hit_rate, + usableRecent: debSummary?.usable_recent, }; }, [debSorted, debSummary]); @@ -218,6 +233,10 @@ export function TrainingDashboard({ isEn }: { isEn: boolean }) { const formatPct = (value: number | null | undefined) => value == null ? "--" : `${value.toFixed(1)}%`; const formatMaybeDeg = (value: number | null | undefined, digits = 1) => value == null ? "--" : `${value.toFixed(digits)}°`; + const usableWindowLabel = (window?: string) => { + if (window === "recent_14d") return isEn ? "Usable 14d" : "可用近14天"; + return isEn ? "Usable 7d" : "可用近7天"; + }; return (
@@ -242,7 +261,7 @@ export function TrainingDashboard({ isEn }: { isEn: boolean }) {
{[ { icon: Hash, label: isEn ? "Cities" : "城市数", value: debStats.cities, tone: "blue" }, - { icon: Target, label: isEn ? "Historical Avg" : "历史平均", value: `${debStats.avgHit.toFixed(1)}%`, tone: "emerald" }, + { icon: Target, label: usableWindowLabel(debStats.usableRecent?.window), value: formatPct(debStats.usableRecent?.hit_rate), tone: "emerald" }, { icon: TrendingUp, label: isEn ? "Recent 7d" : "近7天", value: formatPct(debSummary?.recent_7d?.hit_rate), tone: "emerald" }, { icon: TrendingUp, label: isEn ? "Recent 14d" : "近14天", value: formatPct(debSummary?.recent_14d?.hit_rate), tone: "purple" }, { icon: Thermometer, label: isEn ? "Avg Error" : "平均误差", value: `${debStats.avgMae.toFixed(1)}°`, tone: "amber" }, diff --git a/frontend/components/ops/training/TrainingPageClient.tsx b/frontend/components/ops/training/TrainingPageClient.tsx index 348d2564..1fd6ffb8 100644 --- a/frontend/components/ops/training/TrainingPageClient.tsx +++ b/frontend/components/ops/training/TrainingPageClient.tsx @@ -89,6 +89,18 @@ interface DebSummary { sample_days?: number; city_count?: number; }; + usable_recent?: { + window?: string; + city_count?: number; + samples?: number; + hits?: number; + hit_rate?: number | null; + avg_mae?: number | null; + recommendations?: { + primary?: number; + supporting?: number; + }; + }; recent_7d?: { hit_rate?: number | null; mae?: number | null; @@ -160,6 +172,7 @@ export function TrainingPageClient() { return { avgHit: debSummary?.historical?.avg_hit_rate ?? avgHit, avgMae: debSummary?.historical?.avg_mae ?? avgMae, + usableRecent: debSummary?.usable_recent, recent7Hit: debSummary?.recent_7d?.hit_rate, recent14Hit: debSummary?.recent_14d?.hit_rate, best, @@ -167,6 +180,9 @@ export function TrainingPageClient() { }; }, [accuracy, debSummary]); + const usableWindowLabel = (window?: string) => + window === "recent_14d" ? "DEB 可用近 14 天命中" : "DEB 可用近 7 天命中"; + const debChartData = useMemo(() => { if (!accuracy?.length) return []; return accuracy @@ -247,7 +263,9 @@ export function TrainingPageClient() {
Dict[s } +def _build_deb_usable_recent_summary( + accuracy_data: List[Dict[str, Any]], +) -> Dict[str, Any]: + usable_rows = [] + recommendations = {"primary": 0, "supporting": 0} + for row in accuracy_data: + recent = row.get("deb_recent") if isinstance(row.get("deb_recent"), dict) else None + if not recent: + continue + recommendation = str(recent.get("recommendation") or "").strip().lower() + if recommendation not in {"primary", "supporting"}: + continue + usable_rows.append(row) + recommendations[recommendation] += 1 + + def build_window(window_key: str) -> Dict[str, Any]: + samples = 0 + hits = 0 + weighted_mae = 0.0 + city_count = 0 + for row in usable_rows: + recent = row.get("deb_recent") if isinstance(row.get("deb_recent"), dict) else {} + metrics = recent.get(window_key) if isinstance(recent.get(window_key), dict) else {} + row_samples = int(metrics.get("samples") or 0) + if row_samples <= 0: + continue + row_hits = int(metrics.get("hits") or 0) + row_mae = _sf(metrics.get("mae")) + samples += row_samples + hits += row_hits + city_count += 1 + if row_mae is not None: + weighted_mae += row_mae * row_samples + return { + "window": window_key, + "city_count": city_count, + "samples": samples, + "hits": hits, + "hit_rate": _round_metric((hits / samples * 100) if samples else None, 1), + "avg_mae": _round_metric((weighted_mae / samples) if samples else None, 2), + "recommendations": dict(recommendations), + } + + recent_7d = build_window("recent_7d") + if int(recent_7d.get("samples") or 0) > 0: + return recent_7d + return build_window("recent_14d") + + def _flatten_training_history(history: Dict[str, Dict[str, Dict[str, Any]]], today_str: str) -> List[Dict[str, Any]]: rows: List[Dict[str, Any]] = [] for city, city_rows in (history or {}).items(): @@ -2545,6 +2594,7 @@ def _build_training_accuracy_payload( "accuracy": accuracy_data, "deb_summary": { "historical": _build_deb_historical_summary(accuracy_data), + "usable_recent": _build_deb_usable_recent_summary(accuracy_data), "recent_7d": _evaluate_deb_records( all_rows, start_date=recent_7_start,