Skip to content

Commit 4dcd372

Browse files
chvvkumarclaude
andcommitted
feat: show samples needed per class before a determination
Expose min_samples and samples_needed in the confidence-stats summary, make the recommendation text state how many more samples are required, and surface a 'N to go / ready' marker plus a determination progress bar per class in the insights panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cb9ef0d commit 4dcd372

4 files changed

Lines changed: 65 additions & 2 deletions

File tree

alpaca/confidence_stats.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ def summary(self) -> dict:
177177
"max": e["max_conf"] if e["max_conf"] is not None else 0.0,
178178
"buckets": list(e["buckets"]),
179179
"bucket_pct": bucket_pct,
180+
"min_samples": MIN_SAMPLES,
181+
"samples_needed": max(0, MIN_SAMPLES - count),
180182
"recommendation": rec,
181183
})
182184
if rec["severity"] in ("warn", "info"):
@@ -194,11 +196,16 @@ def summary(self) -> dict:
194196
def _recommend(count: int, mean: float, buckets: list) -> dict:
195197
"""First matching condition wins."""
196198
if count == 0:
197-
return {"severity": "none", "text": "Not yet observed."}
199+
return {
200+
"severity": "none",
201+
"text": f"Not yet observed; {MIN_SAMPLES} samples needed before a determination can be made.",
202+
}
198203
if count < MIN_SAMPLES:
204+
remaining = MIN_SAMPLES - count
205+
sample_word = "sample" if remaining == 1 else "samples"
199206
return {
200207
"severity": "info",
201-
"text": "Too few samples to judge; needs more observations.",
208+
"text": f"Too few samples to judge; {remaining} more {sample_word} needed before a determination can be made.",
202209
}
203210
if mean < LOW_MEAN_PCT:
204211
return {

docs/EXTERNAL_API.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ Confidence values are percentages (0-100). The histogram divides confidence into
304304
"max": 71.2,
305305
"buckets": [0, 0, 1, 4, 12, 9, 6, 3, 2, 0],
306306
"bucket_pct": [0.0, 0.0, 2.7, 10.8, 32.4, 24.3, 16.2, 8.1, 5.4, 0.0],
307+
"min_samples": 50,
308+
"samples_needed": 13,
307309
"recommendation": {
308310
"severity": "warn",
309311
"text": "Low average confidence; collect and label more images for this class."
@@ -330,6 +332,8 @@ Confidence values are percentages (0-100). The histogram divides confidence into
330332
| `classes[].max` | number | Highest recorded confidence percentage |
331333
| `classes[].buckets` | array | 10 raw counts, one per 10-point confidence band |
332334
| `classes[].bucket_pct` | array | The same distribution as percentages of `count` |
335+
| `classes[].min_samples` | number | Sample count required before a determination is made (currently 50) |
336+
| `classes[].samples_needed` | number | Remaining samples to reach `min_samples` (0 once reached) |
333337
| `classes[].recommendation.severity` | string | `none`, `info`, `warn`, or `ok` |
334338
| `classes[].recommendation.text` | string | Suggested action for this class |
335339
| `advice` | array | Text recommendations for classes flagged `warn` or `info` |

templates/setup.html

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,6 +1771,8 @@ <h3 style="color: rgb(248, 113, 113); font-size: 14px; font-weight: 600; margin-
17711771
var color = ciSeverityColor(sev);
17721772
var hasData = count > 0;
17731773
var meanWidth = hasData ? Math.max(0, Math.min(100, mean)) : 0;
1774+
var minSamples = (cls && typeof cls.min_samples === 'number') ? cls.min_samples : 0;
1775+
var needed = (cls && typeof cls.samples_needed === 'number') ? cls.samples_needed : 0;
17741776

17751777
var wrap = document.createElement('div');
17761778
wrap.style.cssText = 'padding: 10px 12px; background: rgba(30, 41, 59, 0.4); border-radius: 6px; margin-bottom: 8px; border: 1px solid rgba(71, 85, 105, 0.5);';
@@ -1814,6 +1816,19 @@ <h3 style="color: rgb(248, 113, 113); font-size: 14px; font-weight: 600; margin-
18141816
countEl.textContent = '(' + count + ')';
18151817
summary.appendChild(countEl);
18161818

1819+
var detEl = document.createElement('div');
1820+
detEl.style.cssText = 'min-width: 64px; text-align: right; font-size: 10px; font-weight: 600; font-family: \'JetBrains Mono\', monospace;';
1821+
if (needed > 0) {
1822+
detEl.textContent = needed + ' to go';
1823+
detEl.style.color = 'rgb(251, 191, 36)';
1824+
detEl.title = 'Needs ' + needed + ' more sample(s) to reach ' + minSamples + ' before a determination can be made';
1825+
} else {
1826+
detEl.textContent = '✓ ready';
1827+
detEl.style.color = 'rgb(52, 211, 153)';
1828+
detEl.title = 'Reached ' + minSamples + ' samples; a determination can be made';
1829+
}
1830+
summary.appendChild(detEl);
1831+
18171832
wrap.appendChild(summary);
18181833

18191834
if (rec.text) {
@@ -1827,6 +1842,23 @@ <h3 style="color: rgb(248, 113, 113); font-size: 14px; font-weight: 600; margin-
18271842
detail.id = 'ci-detail-' + idx;
18281843
detail.style.cssText = 'display: none; margin-top: 12px; margin-left: 16px;';
18291844

1845+
// Determination progress toward the minimum sample count.
1846+
var detWrap = document.createElement('div');
1847+
detWrap.style.cssText = 'margin-bottom: 12px;';
1848+
var detLbl = document.createElement('div');
1849+
detLbl.style.cssText = 'font-size: 11px; color: rgb(148, 163, 184); margin-bottom: 4px; font-family: \'JetBrains Mono\', monospace;';
1850+
detLbl.textContent = 'Samples toward determination: ' + Math.min(count, minSamples) + ' / ' + minSamples +
1851+
(needed > 0 ? ' (' + needed + ' more needed)' : ' (reached)');
1852+
var detTrack = document.createElement('div');
1853+
detTrack.style.cssText = 'height: 12px; background: rgba(30, 41, 59, 0.6); border-radius: 2px; overflow: hidden; border: 1px solid rgba(71, 85, 105, 0.5);';
1854+
var detFill = document.createElement('div');
1855+
var detPct = minSamples > 0 ? Math.max(0, Math.min(100, (count / minSamples) * 100)) : 100;
1856+
detFill.style.cssText = 'height: 100%; width: ' + detPct + '%; background: ' + (needed > 0 ? 'rgb(251, 191, 36)' : 'rgb(52, 211, 153)') + '; opacity: 0.75;';
1857+
detTrack.appendChild(detFill);
1858+
detWrap.appendChild(detLbl);
1859+
detWrap.appendChild(detTrack);
1860+
detail.appendChild(detWrap);
1861+
18301862
if (hasData) {
18311863
var stats = document.createElement('div');
18321864
stats.style.cssText = 'font-size: 11px; color: rgb(148, 163, 184); margin-bottom: 10px; font-family: \'JetBrains Mono\', monospace;';

tests/test_confidence_stats.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,26 @@ def test_recommendation_too_few_samples(stats):
121121
assert rain["recommendation"]["severity"] == "info"
122122

123123

124+
def test_samples_needed_counts_down_to_zero(stats):
125+
# Unobserved class needs the full MIN_SAMPLES.
126+
clear = next(c for c in stats.summary()["classes"] if c["name"] == "Clear")
127+
assert clear["min_samples"] == MIN_SAMPLES
128+
assert clear["samples_needed"] == MIN_SAMPLES
129+
130+
# After some samples, the remaining count decreases.
131+
for _ in range(10):
132+
stats.record("Rain", 50.0)
133+
rain = next(c for c in stats.summary()["classes"] if c["name"] == "Rain")
134+
assert rain["samples_needed"] == MIN_SAMPLES - 10
135+
assert str(MIN_SAMPLES - 10) in rain["recommendation"]["text"]
136+
137+
# Once the threshold is reached, no more samples are needed.
138+
for _ in range(MIN_SAMPLES):
139+
stats.record("Snow", 95.0)
140+
snow = next(c for c in stats.summary()["classes"] if c["name"] == "Snow")
141+
assert snow["samples_needed"] == 0
142+
143+
124144
def test_recommendation_healthy(stats):
125145
for _ in range(MIN_SAMPLES + 10):
126146
stats.record("Clear", 95.0)

0 commit comments

Comments
 (0)