Skip to content

Commit 3a5e022

Browse files
mihowclaude
andcommitted
feat(ui): add Wilson CI + Cohen's kappa bars to stats panel
Two new horizontal bars below the existing verified / agreement-rate bars: - 'Agreement 95% CI (Wilson)' — RangeBar showing the Wilson CI as a filled segment between low and high (wide bar = shaky number, narrow bar = tight). Value reads '87–97%'. '—' when no verified-with-pred set. - 'Cohen's κ (beyond chance)' — SignedBar over [-1, 1] with the zero midpoint marked. Positive fills right, negative fills left. Value reads '0.41'. '—' when undefined (empty or single-category set). Hook type extended with the five new fields (agreed_*_ci_low/high + cohens_kappa). Loading skeleton bumped to 4 placeholders. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1241967 commit 3a5e022

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

ui/src/data-services/hooks/occurrences/stats/useModelAgreement.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@ interface ModelAgreementResponse {
1010
no_prediction_count: number
1111
agreed_exact_count: number
1212
agreed_exact_pct: number
13+
agreed_exact_ci_low: number | null
14+
agreed_exact_ci_high: number | null
1315
agreed_any_rank_count: number
1416
agreed_any_rank_pct: number
17+
agreed_any_rank_ci_low: number | null
18+
agreed_any_rank_ci_high: number | null
19+
// Cohen's kappa (exact-taxon) — agreement beyond chance. Range [-1, 1];
20+
// null when denominator is 0 or expected agreement is 1.0.
21+
cohens_kappa: number | null
1522
// Only populated when the caller passes ?agreement_coarsest_rank=<RANK>.
1623
agreement_coarsest_rank: string | null
1724
agreed_coarser_rank_count: number | null

ui/src/pages/occurrences/occurrence-stats.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,86 @@ const StatBar = ({
4545
)
4646
}
4747

48+
// Horizontal range bar for a Wilson confidence interval. Draws a filled
49+
// segment between `low` and `high` over a 0–100% track, so a wide CI reads as
50+
// a wide bar (= shaky number) and a tight CI as a narrow one. Null bounds →
51+
// "—".
52+
const RangeBar = ({
53+
label,
54+
low,
55+
high,
56+
}: {
57+
label: string
58+
low: number | null
59+
high: number | null
60+
}) => {
61+
const hasData = low !== null && high !== null
62+
const lowPct = hasData ? Math.round(Math.min(Math.max(low, 0), 1) * 100) : 0
63+
const highPct = hasData ? Math.round(Math.min(Math.max(high, 0), 1) * 100) : 0
64+
65+
return (
66+
<div className="space-y-2">
67+
<span className="body-overline font-bold text-muted-foreground">
68+
{label}
69+
</span>
70+
<div className="flex items-center gap-3">
71+
<div className="h-2 flex-1 rounded-full bg-muted relative">
72+
{hasData ? (
73+
<div
74+
className="absolute h-2 rounded-full bg-primary transition-all"
75+
style={{
76+
left: `${lowPct}%`,
77+
width: `${Math.max(highPct - lowPct, 1)}%`,
78+
}}
79+
/>
80+
) : null}
81+
</div>
82+
<span className="body-base tabular-nums">
83+
{hasData ? `${lowPct}${highPct}%` : '—'}
84+
</span>
85+
</div>
86+
</div>
87+
)
88+
}
89+
90+
// Signed bar for a value in [-1, 1] (Cohen's kappa). 0 sits at the visual
91+
// midpoint; positive values fill rightward, negative fill leftward. Null →
92+
// "—" (kappa is undefined for empty or single-category sets).
93+
const SignedBar = ({
94+
label,
95+
value,
96+
}: {
97+
label: string
98+
value: number | null
99+
}) => {
100+
const v = value === null ? null : Math.min(Math.max(value, -1), 1)
101+
const widthPct = v === null ? 0 : Math.abs(v) * 50
102+
const leftPct = v === null ? 50 : v >= 0 ? 50 : 50 - widthPct
103+
104+
return (
105+
<div className="space-y-2">
106+
<span className="body-overline font-bold text-muted-foreground">
107+
{label}
108+
</span>
109+
<div className="flex items-center gap-3">
110+
<div className="h-2 flex-1 rounded-full bg-muted relative">
111+
{/* zero marker */}
112+
<div className="absolute h-2 w-px bg-foreground/40 left-1/2" />
113+
{v !== null ? (
114+
<div
115+
className="absolute h-2 rounded-full bg-primary transition-all"
116+
style={{ left: `${leftPct}%`, width: `${widthPct}%` }}
117+
/>
118+
) : null}
119+
</div>
120+
<span className="body-base tabular-nums">
121+
{v === null ? '—' : v.toFixed(2)}
122+
</span>
123+
</div>
124+
</div>
125+
)
126+
}
127+
48128
// Live verified / agreement stats for the occurrence list. Threads the same
49129
// filter array the list view sends so the numbers always match the result set.
50130
export const OccurrenceStats = ({
@@ -75,6 +155,8 @@ export const OccurrenceStats = ({
75155
<>
76156
<div className="h-12 animate-pulse rounded-md bg-muted" />
77157
<div className="h-12 animate-pulse rounded-md bg-muted" />
158+
<div className="h-12 animate-pulse rounded-md bg-muted" />
159+
<div className="h-12 animate-pulse rounded-md bg-muted" />
78160
</>
79161
) : (
80162
<>
@@ -87,6 +169,15 @@ export const OccurrenceStats = ({
87169
label="Human-model agreement rate"
88170
value={data.agreed_any_rank_pct}
89171
/>
172+
<RangeBar
173+
label="Agreement 95% CI (Wilson)"
174+
low={data.agreed_any_rank_ci_low}
175+
high={data.agreed_any_rank_ci_high}
176+
/>
177+
<SignedBar
178+
label="Cohen's κ (beyond chance)"
179+
value={data.cohens_kappa}
180+
/>
90181
</>
91182
)}
92183
</div>

0 commit comments

Comments
 (0)