Clarify calibrated probability read with LGBM context
This commit is contained in:
@@ -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" }}>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user