Add DEB ensemble confidence signal

This commit is contained in:
2569718930@qq.com
2026-06-25 02:49:12 +08:00
parent 578f547fa7
commit ac565c4a00
7 changed files with 308 additions and 7 deletions
@@ -43,6 +43,19 @@ type DebQuality = {
recommendation?: string | null;
recent_hit_rate?: number | null;
recent_samples?: number | null;
ensemble_signal?: DebEnsembleSignal | null;
};
type DebEnsembleSignal = {
available?: boolean;
stance?: string | null;
label_zh?: string | null;
label_en?: string | null;
reason_zh?: string | null;
reason_en?: string | null;
spread?: number | null;
deb_distance?: number | null;
confidence_delta?: number | null;
};
function debQualityLabel(quality: DebQuality | null | undefined, isEn: boolean) {
@@ -55,6 +68,9 @@ function debQualityLabel(quality: DebQuality | null | undefined, isEn: boolean)
}
function debQualityClass(quality: DebQuality | null | undefined) {
const stance = quality?.ensemble_signal?.available ? quality.ensemble_signal.stance : null;
if (stance === "caution") return "border-amber-300 bg-amber-50 text-amber-700";
if (stance === "supporting") return "border-emerald-200 bg-emerald-50 text-emerald-700";
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";
@@ -62,22 +78,46 @@ function debQualityClass(quality: DebQuality | null | undefined) {
return "border-slate-200 bg-slate-50 text-slate-500";
}
function DebQualityBadge({ quality, isEn }: { quality?: DebQuality | null; isEn: boolean }) {
function debEnsembleShortLabel(signal: DebEnsembleSignal | null | undefined, isEn: boolean) {
if (!signal?.available) return "";
if (signal.stance === "supporting") return isEn ? "Ens+" : "集+";
if (signal.stance === "caution") return isEn ? "Ens!" : "集警";
return "";
}
function debQualityTitle(quality: DebQuality | null | undefined, isEn: boolean) {
const label = debQualityLabel(quality, isEn);
if (!label) return null;
const hitRate = quality?.recent_hit_rate;
const samples = quality?.recent_samples;
const ensemble = quality?.ensemble_signal;
const titleParts = [
isEn ? `DEB recommendation: ${label}` : `DEB 建议:${label}`,
label ? (isEn ? `DEB recommendation: ${label}` : `DEB 建议:${label}`) : null,
hitRate == null ? null : `${hitRate.toFixed(0)}%`,
samples == null ? null : `n=${samples}`,
ensemble?.available
? `${isEn ? ensemble.label_en || "Ensemble" : ensemble.label_zh || "集合"}: ${
isEn ? ensemble.reason_en || "" : ensemble.reason_zh || ""
}`
: null,
].filter(Boolean);
return titleParts.join(" · ");
}
function DebQualityBadge({ quality, isEn }: { quality?: DebQuality | null; isEn: boolean }) {
const label = debQualityLabel(quality, isEn);
const ensembleLabel = debEnsembleShortLabel(quality?.ensemble_signal, isEn);
if (!label && !ensembleLabel) return null;
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(" · ")}
title={debQualityTitle(quality, isEn)}
>
{label}
{label || "DEB"}
{ensembleLabel && (
<span className="ml-1 border-l border-current/30 pl-1">
{ensembleLabel}
</span>
)}
</span>
);
}
@@ -313,3 +353,6 @@ export function TemperatureStatsBars({
export const __buildTemperatureStatsLabelsForTest = buildStatsLabels;
export const __buildDebQualityLabelForTest = debQualityLabel;
export const __buildDebQualityClassForTest = debQualityClass;
export const __buildDebQualityTitleForTest = debQualityTitle;
export const __buildDebEnsembleLabelForTest = debEnsembleShortLabel;
@@ -1,4 +1,10 @@
import { __buildDebQualityLabelForTest, __buildTemperatureStatsLabelsForTest } from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
import {
__buildDebEnsembleLabelForTest,
__buildDebQualityClassForTest,
__buildDebQualityLabelForTest,
__buildDebQualityTitleForTest,
__buildTemperatureStatsLabelsForTest,
} from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
import { temp } from "@/components/dashboard/scan-terminal/utils";
function assert(condition: unknown, message: string) {
@@ -71,4 +77,35 @@ export function runTests() {
__buildDebQualityLabelForTest({ recommendation: "insufficient" }, false) === "样本少",
"thin-sample DEB should render a Chinese low-sample label",
);
const supportingSignal = {
available: true,
stance: "supporting",
label_zh: "集合支撑",
label_en: "Ensemble support",
reason_zh: "集合区间较窄",
reason_en: "Ensemble spread is tight",
};
assert(
__buildDebEnsembleLabelForTest(supportingSignal, false) === "集+",
"supporting ensemble signal should render a compact Chinese marker",
);
assert(
__buildDebEnsembleLabelForTest({ ...supportingSignal, stance: "caution" }, true) === "Ens!",
"caution ensemble signal should render a compact English marker",
);
assert(
__buildDebQualityClassForTest({
recommendation: "primary",
quality_tier: "high",
ensemble_signal: { ...supportingSignal, stance: "caution" },
}).includes("amber"),
"ensemble caution should override the DEB quality badge color",
);
assert(
__buildDebQualityTitleForTest(
{ recommendation: "primary", ensemble_signal: supportingSignal },
false,
).includes("集合支撑"),
"DEB badge title should include the ensemble signal reason",
);
}
@@ -1605,7 +1605,16 @@ function seedRunwayPlateHistoryFromRow(
type ChartRenderState = {
forecastTodayHigh?: number | null;
debPrediction?: number | null;
debQuality?: Pick<DebForecast, "quality_tier" | "recommendation" | "recent_hit_rate" | "recent_samples" | "recent_hits" | "recent_mae"> | null;
debQuality?: Pick<
DebForecast,
| "quality_tier"
| "recommendation"
| "recent_hit_rate"
| "recent_samples"
| "recent_hits"
| "recent_mae"
| "ensemble_signal"
> | null;
debHourlyPath?: DebHourlyPath | null;
localDate?: string | null;
localTime?: string | null;
@@ -1995,6 +2004,7 @@ function parseFullChartDetailFromCityDetail(json: CityDetail | null): FullChartD
recent_samples: json.deb.recent_samples,
recent_hits: json.deb.recent_hits,
recent_mae: json.deb.recent_mae,
ensemble_signal: json.deb.ensemble_signal,
} : null,
debHourlyPath: json.deb?.hourly_path || null,
localDate: json.local_date || (json as any)?.overview?.local_date || null,