Add DEB city trust tiers

This commit is contained in:
2569718930@qq.com
2026-06-07 23:32:23 +08:00
parent c3f9b974c3
commit 4389d4a40d
5 changed files with 280 additions and 10 deletions
@@ -56,10 +56,20 @@ type DebSummaryPayload = {
versions?: Record<string, DebVersionSummary>;
};
type DebRecentStrategy = {
recent_7d?: DebWindowSummary;
recent_14d?: DebWindowSummary;
trust_tier?: "high" | "medium" | "low" | "insufficient" | string;
recommendation?: "primary" | "supporting" | "context_only" | "insufficient" | string;
bias_direction?: "under" | "over" | "neutral" | "unknown" | string;
reason?: string;
};
type TrainingCity = {
city_id: string;
name: string;
deb?: MetricPayload;
deb_recent?: DebRecentStrategy | null;
mu?: MetricPayload;
};
@@ -87,6 +97,27 @@ function barColor(hr: number) {
return "#dc2626";
}
function trustBadgeClass(tier?: string) {
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 trustLabel(tier: string | undefined, isEn: boolean) {
if (tier === "high") return isEn ? "High" : "高";
if (tier === "medium") return isEn ? "Medium" : "中";
if (tier === "low") return isEn ? "Low" : "低";
return isEn ? "Thin" : "少";
}
function recommendationLabel(value: string | undefined, isEn: boolean) {
if (value === "primary") return isEn ? "Primary" : "主用";
if (value === "supporting") return isEn ? "Support" : "辅助";
if (value === "context_only") return isEn ? "Context" : "参考";
return isEn ? "Insufficient" : "样本少";
}
const TRAINING_CACHE_KEY = "polyweather_training_accuracy_v1";
const TRAINING_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -322,12 +353,15 @@ export function TrainingDashboard({ isEn }: { isEn: boolean }) {
)}
{/* ── Combined Table ── */}
<div className="rounded-lg border border-slate-200 bg-white overflow-hidden">
<table className="w-full text-[12px] border-collapse">
<div className="overflow-x-auto rounded-lg border border-slate-200 bg-white">
<table className="min-w-[960px] w-full text-[12px] border-collapse">
<thead>
<tr className="border-b border-slate-200 bg-[#f8f9fa] text-left">
<th className="w-10 px-3 py-2 text-center text-[11px] font-black text-slate-400">#</th>
<th className="px-3 py-2 text-[11px] font-black uppercase text-slate-500">{isEn ? "City" : "城市"}</th>
<th className="px-2 py-2 text-left text-[11px] font-black uppercase text-slate-500">{isEn ? "DEB Trust" : "DEB 信任"}</th>
<th className="px-2 py-2 text-right text-[11px] font-black uppercase text-slate-500">{isEn ? "7d" : "7天"}</th>
<th className="px-2 py-2 text-right text-[11px] font-black uppercase text-slate-500">{isEn ? "14d" : "14天"}</th>
<th className="px-2 py-2 text-right text-[11px] font-black uppercase text-slate-500">{isEn ? "DEB Hit" : "DEB 命中"}</th>
<th className="px-2 py-2 text-right text-[11px] font-black uppercase text-slate-500">{isEn ? "DEB Error" : "DEB 误差"}</th>
<th className="px-2 py-2 text-right text-[11px] font-black uppercase text-slate-500">{isEn ? "μ Hit" : "μ 命中"}</th>
@@ -338,12 +372,12 @@ export function TrainingDashboard({ isEn }: { isEn: boolean }) {
<tbody className="divide-y divide-slate-100">
{debSorted.length || muSorted.length ? (
(() => {
const cities = new Map<string, { deb?: MetricPayload; mu?: MetricPayload; name: string }>();
for (const c of debSorted) cities.set(c.city_id, { deb: c.deb, mu: c.mu, name: c.name });
const cities = new Map<string, { deb?: MetricPayload; debRecent?: DebRecentStrategy | null; mu?: MetricPayload; name: string }>();
for (const c of debSorted) cities.set(c.city_id, { deb: c.deb, debRecent: c.deb_recent, mu: c.mu, name: c.name });
for (const c of muSorted) {
const existing = cities.get(c.city_id);
if (existing) existing.mu = c.mu;
else cities.set(c.city_id, { deb: c.deb, mu: c.mu, name: c.name });
else cities.set(c.city_id, { deb: c.deb, debRecent: c.deb_recent, mu: c.mu, name: c.name });
}
const merged = [...cities.entries()]
.sort((a, b) => {
@@ -352,14 +386,30 @@ export function TrainingDashboard({ isEn }: { isEn: boolean }) {
return bMax - aMax;
})
.slice(0, 30);
return merged.map(([cityId, { deb, mu, name }], i) => {
return merged.map(([cityId, { deb, debRecent, mu, name }], i) => {
const debHit = deb?.hit_rate ?? 0;
const muHit = mu?.hit_rate ?? 0;
const brier = mu?.brier_score;
const recent7 = debRecent?.recent_7d?.hit_rate;
const recent14 = debRecent?.recent_14d?.hit_rate;
return (
<tr key={cityId} className="hover:bg-slate-50 transition-colors">
<td className="px-3 py-2 text-center text-[12px] font-mono text-slate-400">{i + 1}</td>
<td className="px-3 py-2 font-semibold text-slate-800 capitalize">{name}</td>
<td className="px-2 py-2">
{debRecent ? (
<span
className={`inline-flex min-w-[4rem] items-center justify-center rounded border px-2 py-0.5 text-[10px] font-black uppercase ${trustBadgeClass(debRecent.trust_tier)}`}
title={debRecent.reason}
>
{trustLabel(debRecent.trust_tier, isEn)} · {recommendationLabel(debRecent.recommendation, isEn)}
</span>
) : (
<span className="text-slate-400">--</span>
)}
</td>
<td className="px-2 py-2 text-right font-mono text-slate-600">{recent7 == null ? "--" : `${recent7.toFixed(0)}%`}</td>
<td className="px-2 py-2 text-right font-mono text-slate-600">{recent14 == null ? "--" : `${recent14.toFixed(0)}%`}</td>
<td className="px-2 py-2 text-right font-mono font-bold" style={{ color: barColor(debHit) }}>
{deb ? `${debHit.toFixed(0)}%` : "--"}
</td>
@@ -375,7 +425,7 @@ export function TrainingDashboard({ isEn }: { isEn: boolean }) {
})()
) : (
<tr>
<td colSpan={7} className="px-4 py-12 text-center text-slate-400">
<td colSpan={10} className="px-4 py-12 text-center text-slate-400">
{data === null ? (isEn ? "Loading..." : "加载中...") : (isEn ? "No training data" : "暂无训练数据")}
</td>
</tr>
@@ -56,6 +56,22 @@ interface CityAccuracy {
total_days: number;
details_str: string;
} | null;
deb_recent?: {
recent_7d?: {
hit_rate?: number | null;
samples?: number;
mae?: number | null;
};
recent_14d?: {
hit_rate?: number | null;
samples?: number;
mae?: number | null;
};
trust_tier?: string;
recommendation?: string;
bias_direction?: string;
reason?: string;
} | null;
mu?: {
mae: number;
hit_rate: number;
@@ -87,6 +103,31 @@ interface DebSummary {
};
}
function debTrustBadgeClass(tier?: string) {
if (tier === "high") return "bg-emerald-500/15 text-emerald-300 border-emerald-500/30";
if (tier === "medium") return "bg-amber-500/15 text-amber-300 border-amber-500/30";
if (tier === "low") return "bg-rose-500/15 text-rose-300 border-rose-500/30";
return "bg-slate-500/15 text-slate-300 border-slate-500/30";
}
function debTrustLabel(tier?: string) {
if (tier === "high") return "高可信";
if (tier === "medium") return "中可信";
if (tier === "low") return "低可信";
return "样本少";
}
function debRecommendationLabel(recommendation?: string) {
if (recommendation === "primary") return "主用";
if (recommendation === "supporting") return "辅助";
if (recommendation === "context_only") return "仅参考";
return "不足";
}
function formatPct(value: number | null | undefined) {
return value == null ? "—" : `${value.toFixed(0)}%`;
}
export function TrainingPageClient() {
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState<SystemStatusPayload | null>(null);
@@ -279,6 +320,8 @@ export function TrainingPageClient() {
<thead className="text-xs uppercase bg-slate-800/50 text-slate-400">
<tr>
<th scope="col" className="px-4 py-3"></th>
<th scope="col" className="px-4 py-3 text-center">DEB </th>
<th scope="col" className="px-4 py-3 text-center"> 7 / 14 </th>
<th scope="col" className="px-4 py-3 text-center">DEB </th>
<th scope="col" className="px-4 py-3 text-center">DEB MAE</th>
<th scope="col" className="px-4 py-3 text-center">DEB </th>
@@ -296,6 +339,29 @@ export function TrainingPageClient() {
{row.name}
<span className="text-xs text-slate-500 block font-mono">{row.city_id}</span>
</td>
<td className="px-4 py-3 text-center">
{row.deb_recent ? (
<span
title={row.deb_recent.reason}
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold ${debTrustBadgeClass(row.deb_recent.trust_tier)}`}
>
{debTrustLabel(row.deb_recent.trust_tier)} · {debRecommendationLabel(row.deb_recent.recommendation)}
</span>
) : (
<span className="text-slate-600"></span>
)}
</td>
<td className="px-4 py-3 text-center font-mono text-xs text-slate-300">
{row.deb_recent ? (
<span>
{formatPct(row.deb_recent.recent_7d?.hit_rate)}
<span className="mx-1 text-slate-600">/</span>
{formatPct(row.deb_recent.recent_14d?.hit_rate)}
</span>
) : (
"—"
)}
</td>
<td className="px-4 py-3 text-center">
{row.deb ? (
<span className={`px-2 py-0.5 rounded-full text-xs font-semibold ${
@@ -357,7 +423,7 @@ export function TrainingPageClient() {
))
) : (
<tr>
<td colSpan={8} className="px-4 py-8 text-center text-slate-500">
<td colSpan={10} className="px-4 py-8 text-center text-slate-500">
</td>
</tr>