Clarify calibrated probability read with LGBM context

This commit is contained in:
2569718930@qq.com
2026-04-17 20:53:37 +08:00
parent bc09a3753c
commit f5e61e96ff
6 changed files with 154 additions and 71 deletions
@@ -923,6 +923,49 @@
gap: 8px;
}
.root :global(.prob-calibration-head) {
display: grid;
gap: 6px;
margin-bottom: 4px;
padding: 10px;
border: 1px solid rgba(34, 211, 238, 0.16);
border-radius: 8px;
background: rgba(15, 23, 42, 0.26);
}
.root :global(.prob-calibration-head > div) {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.root :global(.prob-calibration-head strong) {
color: var(--text-primary);
font-size: 13px;
font-weight: 800;
}
.root :global(.prob-calibration-head p) {
margin: 0;
color: var(--text-muted);
font-size: 11px;
line-height: 1.45;
}
.root :global(.prob-source-chip) {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 3px 8px;
border: 1px solid rgba(34, 211, 238, 0.28);
border-radius: 8px;
color: #67e8f9;
background: rgba(34, 211, 238, 0.08);
font-size: 11px;
font-weight: 900;
}
.root :global(.prob-row) {
display: flex;
align-items: center;
@@ -742,6 +742,13 @@ export function FutureForecastModal() {
[dateStr, detail],
);
const modelView = useMemo(() => getModelView(detail, dateStr), [dateStr, detail]);
const hasLgbmProbability = useMemo(
() =>
Object.keys(modelView?.models || {}).some((name) =>
String(name || "").toLowerCase().replace(/[\s_/-]/g, "").includes("lgbm"),
),
[modelView],
);
const topProbabilityBucket = useMemo(() => {
const buckets = Array.isArray(probabilityView?.probabilities)
? probabilityView.probabilities
@@ -914,9 +921,14 @@ export function FutureForecastModal() {
}
const bucketLabel = formatBucketLabel(topProbabilityBucket);
const bucketProb = formatMarketPercent(topProbabilityBucket.probability);
if (hasLgbmProbability) {
return locale === "en-US"
? `LGBM-calibrated read puts the leading bucket at ${bucketLabel} (${bucketProb}). Treat this as the base case, not the final settlement.`
: `LGBM 校准后领先温度桶为 ${bucketLabel}${bucketProb})。可作为基准情形,但不要直接等同于最终结算。`;
}
return locale === "en-US"
? `Highest current hit probability is ${bucketLabel} at ${bucketProb}. Treat this as the base case, not the final settlement.`
: `当前命中概率最高的是 ${bucketLabel}${bucketProb},可把它当作基准情形,但不要直接等同于最终结算。`;
? `Calibrated model probability puts the leading bucket at ${bucketLabel} (${bucketProb}). Treat this as the base case, not the final settlement.`
: `校准模型概率显示领先温度桶为 ${bucketLabel}${bucketProb}。可作为基准情形,但不要直接等同于最终结算。`;
})();
const modelSummary = (() => {
if (!modelSpreadView) {
@@ -1823,10 +1835,16 @@ export function FutureForecastModal() {
<section className="future-modal-section">
<div className="modal-section-heading">
<div className="modal-section-kicker">
{locale === "en-US" ? "Auxiliary probability" : "辅助概率"}
{locale === "en-US" ? "Probability read" : "概率判断"}
</div>
<h3>
{locale === "en-US" ? "Model & Market Reference" : "模型与市场参考"}
{hasLgbmProbability
? locale === "en-US"
? "LGBM-Calibrated Probability"
: "LGBM 校准概率"
: locale === "en-US"
? "Calibrated Model Probability"
: "校准模型概率"}
</h3>
</div>
<div className="future-text-block" style={{ marginBottom: "12px" }}>
+69 -63
View File
@@ -310,6 +310,31 @@ function getMarketTopBucketKey(bucket: MarketTopBucket) {
return `s:${String(bucket?.slug || bucket?.question || bucket?.label || "")}`;
}
function hasLgbmModel(detail: CityDetail, targetDate?: string | null) {
const view = getModelView(detail, targetDate);
return Object.keys(view.models || {}).some((name) =>
normalizeModelNameForVote(name).includes("lgbm"),
);
}
function formatProbabilityEngineLabel(
detail: CityDetail,
targetDate: string | null | undefined,
locale: string,
) {
const view = getProbabilityView(detail, targetDate);
if (hasLgbmModel(detail, targetDate)) {
return locale === "en-US"
? "LGBM-calibrated probability"
: "LGBM 校准概率";
}
const engine = String(view.engine || "").trim().toLowerCase();
if (engine === "emos") {
return locale === "en-US" ? "EMOS-calibrated probability" : "EMOS 校准概率";
}
return locale === "en-US" ? "Model probability" : "模型概率";
}
export function HeroSummary() {
const { data } = useCityData();
const { locale } = useI18n();
@@ -580,6 +605,12 @@ export function ProbabilityDistribution({
const marketYesText = toPercent(marketYesPrice);
const marketNoText = toPercent(marketNoPrice);
const isToday = !targetDate || targetDate === detail.local_date;
const probabilityEngineLabel = formatProbabilityEngineLabel(
detail,
targetDate,
locale,
);
const hasLgbmProbability = hasLgbmModel(detail, targetDate);
const modelVoteView = useMemo(
() => getRoundedModelVoteDistribution(detail, targetDate),
[detail, targetDate],
@@ -610,11 +641,41 @@ export function ProbabilityDistribution({
const useMarketTopBuckets =
marketScan?.available && sortedMarketTopBuckets.length >= 2;
const topMarketBucketText = toPercent(sortedMarketTopBuckets[0]?.probability);
const topProbability = [...(view.probabilities || [])].sort(
(a, b) => Number(b.probability || 0) - Number(a.probability || 0),
)[0];
const topProbabilityText = toPercent(topProbability?.probability);
const topProbabilityLabel = topProbability
? topProbability.label || `${topProbability.value}${detail.temp_symbol}`
: null;
return (
<section className="prob-section">
{!hideTitle && <h3>{t("section.probability")}</h3>}
<div className="prob-bars">
<div className="prob-calibration-head">
<div>
<span className="prob-source-chip">{probabilityEngineLabel}</span>
<strong>
{topProbability && topProbabilityText
? locale === "en-US"
? `${topProbabilityLabel} leads at ${topProbabilityText}`
: `${topProbabilityLabel} 当前最高,${topProbabilityText}`
: locale === "en-US"
? "Awaiting calibrated buckets"
: "等待校准概率桶"}
</strong>
</div>
<p>
{hasLgbmProbability
? locale === "en-US"
? "LGBM is the learned intraday adjustment; model consensus below remains an explanation layer."
: "LGBM 作为日内学习校准项;下方模型共识只保留为解释层。"
: locale === "en-US"
? "Using the calibrated model distribution; model consensus below is for explanation only."
: "使用校准后的模型分布;下方模型共识仅用于解释。"}
</p>
</div>
{view.mu != null && (
<div
style={{
@@ -639,82 +700,27 @@ export function ProbabilityDistribution({
>
{useMarketTopBuckets
? locale === "en-US"
? `Market top-4 buckets (top): ${topMarketBucketText}`
: `市场概率(前4温度桶):最高 ${topMarketBucketText}`
? `Market reference only: top traded bucket ${topMarketBucketText}`
: `市场仅作参考:最高交易温度桶 ${topMarketBucketText}`
: locale === "en-US"
? `Market probability (this bucket): ${marketYesText}`
: `市场概率(该温度桶: ${marketYesText}`}
? `Market reference only: this bucket ${marketYesText}`
: `市场仅作参考:该温度桶 ${marketYesText}`}
</div>
)}
{modelVoteHint && (
<div className="prob-model-hint">
<span>
{locale === "en-US" ? "Model vote reference" : "模型投票参考"}
{locale === "en-US" ? "Model consensus" : "模型共识参考"}
</span>
<strong>{modelVoteHint}</strong>
<em>
{locale === "en-US"
? "shown for explanation only"
: "仅用于解释,不作为结算概率"}
? "explains clustering, not calibrated probability"
: "解释模型聚集,不等同于校准概率"}
</em>
</div>
)}
{useMarketTopBuckets ? (
sortedMarketTopBuckets.map((bucket, index) => {
const probability = Math.round(
Number(bucket.probability || 0) * 100,
);
let bucketLabel =
bucket.label ||
(bucket.value != null
? `${bucket.value}${detail.temp_symbol}`
: `${bucket.temp ?? "--"}${detail.temp_symbol}`);
if (bucketLabel) {
let str = String(bucketLabel).toUpperCase().replace(/\s+/g, "");
str = str.replace(/°?C($|\+|-)/g, "℃$1");
if (!str.includes("℃") && /[0-9]/.test(str)) {
str += "℃";
}
bucketLabel = str;
}
const buyYesText = toPriceCents(
bucket.yes_buy ?? bucket.market_price ?? bucket.probability,
);
const buyNoText = toPriceCents(bucket.no_buy);
const marketTag = buyYesText
? locale === "en-US"
? `Market ref: ${buyYesText}`
: `市场参考: ${buyYesText}`
: buyNoText
? locale === "en-US"
? `Market hedge: ${buyNoText}`
: `市场反向: ${buyNoText}`
: null;
return (
<div
key={`${bucket.slug || bucket.label || index}`}
className="prob-row"
>
<div className="prob-label">{bucketLabel}</div>
<div className="prob-bar-track">
<div
className={clsx("prob-bar-fill", `rank-${index}`)}
style={{ width: `${Math.max(probability, 8)}%` }}
>
{probability}%
</div>
</div>
{marketTag && (
<div className={clsx("prob-market-inline", "yes")}>
{marketTag}
</div>
)}
</div>
);
})
) : view.probabilities.length === 0 ? (
{view.probabilities.length === 0 ? (
<EmptyState text={t("section.noProb")} />
) : (
view.probabilities.slice(0, 6).map((bucket, index) => {
+8
View File
@@ -429,6 +429,14 @@ export interface CityDetail {
probabilities?: {
mu?: number | null;
distribution?: ProbabilityBucket[];
engine?: string | null;
calibration_mode?: string | null;
calibration_version?: string | null;
raw_mu?: number | null;
raw_sigma?: number | null;
calibrated_mu?: number | null;
calibrated_sigma?: number | null;
shadow_distribution?: ProbabilityBucket[];
};
hourly?: {
times?: string[];
+8
View File
@@ -1195,15 +1195,23 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
const date = targetDate || detail.local_date;
if (date === detail.local_date) {
return {
calibrationMode: detail.probabilities?.calibration_mode ?? null,
calibrationVersion: detail.probabilities?.calibration_version ?? null,
engine: detail.probabilities?.engine ?? null,
mu: detail.probabilities?.mu ?? null,
probabilities: detail.probabilities?.distribution || [],
shadowProbabilities: detail.probabilities?.shadow_distribution || [],
};
}
const daily = detail.multi_model_daily?.[date];
return {
calibrationMode: null,
calibrationVersion: null,
engine: null,
mu: daily?.deb?.prediction ?? null,
probabilities: daily?.probabilities || [],
shadowProbabilities: [],
};
}
+4 -4
View File
@@ -88,7 +88,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"future.score": "趋势评分",
"future.todayTempTrend": "今日温度走势",
"future.targetTempTrend": "目标日小时走势",
"future.probability": "模型结算概率分布",
"future.probability": "校准模型概率",
"future.models": "多模型预报",
"future.structureToday": "今日日内结构信号",
"future.structureDate": "未来 6-48 小时趋势",
@@ -108,7 +108,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"section.todayTempTrend": "今日温度走势",
"section.chartEmpty": "暂无小时级数据",
"section.probability": "模型结算概率分布",
"section.probability": "校准模型概率",
"section.mu": "动态分布中心 μ = {value}{unit}",
"section.noProb": "暂无概率数据",
"section.models": "多模型预报",
@@ -253,7 +253,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"future.score": "Trend Score",
"future.todayTempTrend": "Today's Temperature Trend",
"future.targetTempTrend": "Target-day Hourly Trend",
"future.probability": "Model Settlement Probabilities",
"future.probability": "Calibrated Model Probability",
"future.models": "Multi-model Forecast",
"future.structureToday": "Intraday Structural Signal",
"future.structureDate": "6-48h Structural Trend",
@@ -275,7 +275,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"section.todayTempTrend": "Today's Temperature Trend",
"section.chartEmpty": "No hourly data available",
"section.probability": "Model Settlement Probabilities",
"section.probability": "Calibrated Model Probability",
"section.mu": "Dynamic center μ = {value}{unit}",
"section.noProb": "No probability data available",
"section.models": "Multi-model Forecast",