Add DEB ensemble confidence signal
This commit is contained in:
@@ -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;
|
||||
|
||||
+38
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user