Add rounded model-vote baseline to probability distribution
This commit is contained in:
@@ -1001,6 +1001,87 @@
|
||||
border: 1px solid rgba(251, 113, 133, 0.26);
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: 8px;
|
||||
background: rgba(15, 23, 42, 0.34);
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-head) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-head span) {
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-head em) {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-grid) {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-row) {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-row > span) {
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-track) {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-fill) {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(90deg, rgba(20, 184, 166, 0.62), rgba(34, 211, 238, 0.88));
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote-track strong) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-primary);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
text-shadow: 0 1px 2px rgba(2, 6, 23, 0.85);
|
||||
}
|
||||
|
||||
.root :global(.prob-model-vote p) {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ── Model Bars ── */
|
||||
.root :global(.model-bars) {
|
||||
display: flex;
|
||||
@@ -1125,13 +1206,6 @@
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(90deg, rgba(14, 165, 233, 0.72), rgba(34, 211, 238, 0.92));
|
||||
transition: width 0.6s ease-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.root :global(.model-bar-fill.deb) {
|
||||
@@ -1139,6 +1213,28 @@
|
||||
box-shadow: 0 0 8px rgba(34, 211, 238, 0.3);
|
||||
}
|
||||
|
||||
.root :global(.model-bar-value) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
z-index: 3;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-primary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
max-width: calc(100% - 12px);
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
text-align: right;
|
||||
text-shadow: 0 1px 2px rgba(2, 6, 23, 0.9);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.root :global(.model-bar-value.deb) {
|
||||
color: #ecfeff;
|
||||
}
|
||||
|
||||
.root :global(.model-deb-line) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -174,6 +174,92 @@ function formatModelMetaLine(
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
function normalizeModelNameForVote(name: string) {
|
||||
return String(name || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_/-]/g, "");
|
||||
}
|
||||
|
||||
function getModelVoteFamily(name: string) {
|
||||
const normalized = normalizeModelNameForVote(name);
|
||||
if (["icon", "iconeu", "icond2"].includes(normalized)) return "dwd_icon";
|
||||
if (["gem", "gdps", "rdps", "hrdps"].includes(normalized)) return "eccc_gem";
|
||||
if (["ecmwfaifs", "aifs"].includes(normalized)) return "ecmwf_aifs";
|
||||
if (normalized === "ecmwf") return "ecmwf_ifs";
|
||||
return normalized || name;
|
||||
}
|
||||
|
||||
function getModelVotePriority(name: string) {
|
||||
const normalized = normalizeModelNameForVote(name);
|
||||
return (
|
||||
{
|
||||
icond2: 40,
|
||||
iconeu: 30,
|
||||
icon: 20,
|
||||
hrdps: 40,
|
||||
rdps: 35,
|
||||
gdps: 30,
|
||||
gem: 20,
|
||||
ecmwfaifs: 30,
|
||||
ecmwf: 30,
|
||||
gfs: 30,
|
||||
jma: 30,
|
||||
mgm: 45,
|
||||
nws: 45,
|
||||
openmeteo: 15,
|
||||
}[normalized] || 10
|
||||
);
|
||||
}
|
||||
|
||||
function getRoundedModelVoteDistribution(
|
||||
detail: CityDetail,
|
||||
targetDate?: string | null,
|
||||
) {
|
||||
const view = getModelView(detail, targetDate);
|
||||
const representatives = new Map<
|
||||
string,
|
||||
{ name: string; priority: number; value: number }
|
||||
>();
|
||||
|
||||
Object.entries(view.models || {}).forEach(([name, rawValue]) => {
|
||||
const normalized = normalizeModelNameForVote(name);
|
||||
if (normalized === "lgbm" || normalized.includes("meteoblue")) return;
|
||||
const value = Number(rawValue);
|
||||
if (!Number.isFinite(value)) return;
|
||||
const family = getModelVoteFamily(name);
|
||||
const priority = getModelVotePriority(name);
|
||||
const current = representatives.get(family);
|
||||
if (!current || priority > current.priority) {
|
||||
representatives.set(family, { name, priority, value });
|
||||
}
|
||||
});
|
||||
|
||||
const bucketMap = new Map<number, { count: number; models: string[] }>();
|
||||
representatives.forEach(({ name, value }) => {
|
||||
const rounded = Math.round(value);
|
||||
const row = bucketMap.get(rounded) || { count: 0, models: [] };
|
||||
row.count += 1;
|
||||
row.models.push(name);
|
||||
bucketMap.set(rounded, row);
|
||||
});
|
||||
|
||||
const total = representatives.size;
|
||||
const rows = Array.from(bucketMap.entries())
|
||||
.map(([value, row]) => ({
|
||||
count: row.count,
|
||||
models: row.models,
|
||||
percent: total > 0 ? row.count / total : 0,
|
||||
value,
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count || b.value - a.value);
|
||||
|
||||
return {
|
||||
rows,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
function getMarketNoPrice(scan?: MarketScan | null) {
|
||||
if (scan?.no_buy != null) {
|
||||
const direct = Number(scan.no_buy);
|
||||
@@ -494,6 +580,10 @@ export function ProbabilityDistribution({
|
||||
const marketYesText = toPercent(marketYesPrice);
|
||||
const marketNoText = toPercent(marketNoPrice);
|
||||
const isToday = !targetDate || targetDate === detail.local_date;
|
||||
const modelVoteView = useMemo(
|
||||
() => getRoundedModelVoteDistribution(detail, targetDate),
|
||||
[detail, targetDate],
|
||||
);
|
||||
const marketTopBuckets = isToday ? getMarketTopBuckets(marketScan) : [];
|
||||
const sortedMarketTopBuckets = useMemo(() => {
|
||||
const sorted = [...marketTopBuckets].sort(
|
||||
@@ -549,6 +639,46 @@ export function ProbabilityDistribution({
|
||||
: `市场概率(该温度桶): ${marketYesText}`}
|
||||
</div>
|
||||
)}
|
||||
{modelVoteView.rows.length > 0 && (
|
||||
<div className="prob-model-vote">
|
||||
<div className="prob-model-vote-head">
|
||||
<span>
|
||||
{locale === "en-US"
|
||||
? "Rounded model-vote baseline"
|
||||
: "模型四舍五入票数基线"}
|
||||
</span>
|
||||
<em>
|
||||
{locale === "en-US"
|
||||
? `${modelVoteView.total} sources after family dedup`
|
||||
: `家族去重后 ${modelVoteView.total} 个来源`}
|
||||
</em>
|
||||
</div>
|
||||
<div className="prob-model-vote-grid">
|
||||
{modelVoteView.rows.slice(0, 4).map((row) => (
|
||||
<div key={row.value} className="prob-model-vote-row">
|
||||
<span>
|
||||
{row.value}
|
||||
{detail.temp_symbol}
|
||||
</span>
|
||||
<div className="prob-model-vote-track">
|
||||
<div
|
||||
className="prob-model-vote-fill"
|
||||
style={{ width: `${Math.max(row.percent * 100, 8)}%` }}
|
||||
/>
|
||||
<strong>
|
||||
{row.count}/{modelVoteView.total}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p>
|
||||
{locale === "en-US"
|
||||
? "This is a transparent model-count baseline, not the calibrated settlement probability."
|
||||
: "这是透明模型票数基线,不等同于经过实测下限与历史 MAE 校准后的结算概率。"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{useMarketTopBuckets ? (
|
||||
sortedMarketTopBuckets.map((bucket, index) => {
|
||||
const probability = Math.round(
|
||||
|
||||
Reference in New Issue
Block a user