fix(track): a calibration band is a bin sized by what it can judge - #91
Merged
Conversation
adityak74
force-pushed
the
cal/adaptive-bins
branch
from
August 22, 2026 14:54
50cb093 to
35e0e3c
Compare
adityak74
changed the base branch from
cal/confidence-grid
to
cal/band-evidence
August 22, 2026 14:54
adityak74
force-pushed
the
cal/band-evidence
branch
from
August 22, 2026 21:34
49ca84e to
3d55448
Compare
required_band_n arrived in the branch under this one and is right, but nothing had asked what it does to a forecaster that writes free-form confidences. A registry run against stealth/ox-alpha produced 35 scored forecasts on six confidences between 0.93 and 0.99, the thickest holding eleven. One band per distinct stated confidence made every one of them BandTooThin, so the report concluded nothing at any tolerance, and reaching a conclusion that way would have taken roughly five hundred usable attempts. The rows were not the problem. Pooled, they say a stated 0.968 against an observed 0.886, a forecaster overconfident by about eight points, which is a finding. CalibrationReport now bins. Rows arrive ascending by stated confidence, adjacent rows accumulate until the bin holds what required_band_n asks of its own mean, and then it closes. A band's confidence is the mean of what its forecasts stated and never a bin edge, because the comparison has to be against what was actually claimed. CalibrationBand gained parts, one entry per distinct stated confidence pooled into the bin, so everything behind that mean stays on the page. Fixed bins were tried first and are worse. Cutting at 0.7, 0.85 and 0.95 splits that run into three and thirty two, and the bin of three is BandTooThin at every tolerance forever, so one stray forecast at 0.93 holds the verdict hostage for the life of the record. A grid fixed in advance is a bet on the forecaster's habits. This is a loosening and should be read as one. A go that no tolerance could reach is now reachable. What it removes is silence rather than a finding, since BandTooThin is the report saying it has not measured anything. A group carrying the rows its own requirement asks for still closes its own bin, so a miss is never pooled away by a neighbour that could have stood alone. What pooling can hide is a bin holding one group that over-covers and one that under-covers, because the pooled gap is the row-weighted mean of the two. Nineteen rows at a stated 0.50 all covered, pooled with twenty at a stated 0.90 covering 0.60, read as a gap of 0.09 where the second group alone is a miss. Same-signed gaps cannot cancel, which is what matters here: uniform overconfidence is the failure this report exists to catch. Bins are the narrowest the requirement allows, so only a group that arithmetically cannot be judged alone is ever absorbed, and parts puts every absorbed group back on the page with its own n and covered. Three things are not negotiable and each has a test. bin_boundaries is handed the stated confidences and never the outcomes, so no boundary can be fitted to the answer; a test flips every outcome and asserts the bins do not move. Every scored row lands in exactly one bin; a test sums the bands' n against the report's n. And the binner closes on exactly the predicate verdict judges by, through the same required_band_n over the same mean, so a bin the binner called finished can never come back as BandTooThin. verdict itself is untouched. MIN_CALIBRATION_N, MIN_BAND_N, required_band_n, thinness checked before tolerance, NaN checked before comparison, and the empty report answering NotEnoughEvidence all still hold with their tests. A report too small to fill one bin is one band that verdict calls BandTooThin, exactly as before. Worth knowing: n >= ceil(1 / (1 - mean)) is just sum(1 - c_i) >= 1, so a bin is thick enough when a correct forecaster expects one miss in it. That form only rises as rows are added, which is why merging a leftover tail into a closed bin can never reopen it. Verified: 47 calibration tests and 251 in zorp-track, the research feature suite on zorp-agent, cargo fmt clean, CI's clippy line clean, and zorp-track's own clippy showing only the two lints that were already there. Four mutations checked: closing on MIN_BAND_N alone fails two tests, dropping the leftover tail fails six, reporting the bin's low edge instead of the mean fails one, and letting the outcomes reorder the rows fails three. Stacked on cal/confidence-grid, which asks for confidence on a grid, and under that cal/band-evidence, which is where required_band_n comes from. Claude-Session: https://claude.ai/code/session_01KGPVQ8wUG7h36zashWYCp4
adityak74
force-pushed
the
cal/adaptive-bins
branch
from
August 22, 2026 21:45
35e0e3c to
db048fa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on
cal/confidence-grid(#90), which is stacked oncal/band-evidence(#89). Both must merge first. Targetscal/confidence-grid.The problem
required_band_nis right, but nothing had asked what it does to a forecaster that writes free-form confidences. One band per distinct stated confidence shatters the sample. A registry run againststealth/ox-alpha, 35 scored forecasts:Nothing concluded, at any tolerance. The rows were not the problem: pooled they say a stated 0.968 against an observed 0.886, a forecaster overconfident by about eight points, which is a finding.
Fixed bins were checked and are worse. Cutting at 0.7, 0.85 and 0.95 gives three and thirty two, and the bin of three blocks the go at every tolerance forever, so one stray forecast at 0.93 holds the verdict hostage for the life of the record. A grid fixed in advance is a bet on the forecaster's habits.
The binning rule, and why
Rows already arrive ascending by stated confidence. Adjacent rows accumulate until the bin holds what
required_band_nasks of its own mean, and then the bin closes. Two rules on top of that:BandTooThin. Standing alone is what made fixed bins fail.CalibrationBand::confidenceis now the mean of the stated confidences in the bin, never an edge, because the comparison has to be against what was claimed. When a bin holds one distinct stated confidence,mean_statedreturns that number itself rather than dividing a float sum, becauserequired_band_nis a step function and one ulp can move what a band is asked for by a whole row.CalibrationBandgainedparts: Vec<BandPart>, one entry per distinct stated confidence pooled into the bin, ascending, with its ownnandcovered.parts.first()andparts.last()are the bin's span, and the rest is what makes a pass auditable rather than asserted. The four calibration printers now print it.Worth knowing. Written out,
n >= ceil(n / (n - S))for a bin ofnrows whose stated confidences sum toSis exactlyn - S >= 1: a bin is thick enough when a correct forecaster expects at least one miss in it. That form only rises as rows are added, andMIN_BAND_Nis a count, which only rises too. That is the proof that merging a tail into a closed bin can never reopen it, and it is why a greedy left to right pass is well defined.On the real run the rule closes one bin of 31 at the 0.98 group (mean 0.9648, needs 29), then the four rows at 0.99 cannot close and merge back: one band, n=35, mean stated 0.9677, observed 0.8857, gap 0.082. At a tolerance of 0.20 the only remaining reason is
NotEnoughEvidence(35 < 50). At 0.05 it is aBandOutOfTolerancewith that gap, which is the overconfidence the data really shows.Does this make GO easier?
Yes, and it should be read as a loosening. A go that no tolerance could reach is now reachable. Three things convinced me it is the honest kind.
BandTooThin, which is the report saying it has not measured anything. Nothing that was a demonstrated miss became a pass by binning alone.a_stated_confidence_that_can_stand_alone_is_not_pooledpins that, anda_bin_thick_enough_to_show_a_miss_still_shows_onepins that a real miss still reports as one.MIN_CALIBRATION_N,MIN_BAND_N,required_band_n, thinness checked before tolerance, NaN checked before comparison, a stated confidence outside (0, 1) beingUnjudgeable, and the empty report answeringNotEnoughEvidence. A report too small to fill one bin is one band thatverdictcallsBandTooThin.What this can hide, and what limits it
Pooling averages. The pooled gap is the row-weighted mean of the gaps that went into the bin, so:
What limits it:
partscarries every pooled stated confidence with its ownnandcovered, so the cancellation above is visible on the page and re-derivable by hand.Not negotiable, and tested
bin_boundaries(&[f64])takes the stated confidences and nothing else, so a boundary cannot be fitted to the answer. The signature is what makes it true;the_bins_do_not_move_when_every_outcome_flipsis what keeps it true.every_scored_forecast_lands_in_exactly_one_binsums the bands'nandcoveredagainst the report's, over four shapes, andthe_bins_are_a_partition_of_every_scored_rowchecks the boundaries directly.verdictjudges by, through the samerequired_band_nover the samemean_stated. A bin the binner called finished can never come back asBandTooThin.Tests added
the_six_band_run_collapses_into_a_band_that_can_be_judgedNotEnoughEvidence; at 0.05 the pooled overconfidence is a findingevery_scored_forecast_lands_in_exactly_one_binnandcoveredsum to the report's, over four shapesa_report_too_small_for_one_bin_is_still_too_thinBandTooThin, notGothe_bins_do_not_move_when_every_outcome_flipsa_stated_confidence_that_can_stand_alone_is_not_pooleda_bin_thick_enough_to_show_a_miss_still_shows_oneBandOutOfTolerance, gap 0.35a_tail_that_cannot_close_never_reopens_the_bin_it_joinsevery_closed_bin_satisfies_the_rule_the_verdict_usesrequired_band_n(band.confidence)and no closed bin comes back thinreaching_the_flat_floor_is_not_enough_for_a_confident_binMIN_BAND_N, needs 50, keeps fillingthe_bins_are_a_partition_of_every_scored_rowthe_curve_is_one_point_per_band_in_ascending_orderVerification
cargo test -p zorp-track --lib calibrationcargo test -p zorp-trackcargo test -p zorp-agent --features research#[ignore]d and were not run)cargo fmt --all --checkcargo clippy --workspace --exclude zorp-track --all-targets --locked -- -D warningscargo clippy -p zorp-track --all-targetsprereg.rs:170too many arguments,track.rs:344contains()overiter().any()rustup updateMutations run against my own guards
n >= MIN_BAND_Ninstead ofrequired_band_n(mean)reaching_the_flat_floor_is_not_enough_for_a_confident_bin,every_closed_bin_satisfies_the_rule_the_verdict_usesevery_scored_forecast_lands_in_exactly_one_binandthe_bins_are_a_partition_of_every_scored_rowthe_six_band_run_collapses_into_a_band_that_can_be_judgedthe_bins_do_not_move_when_every_outcome_flipsThe first pass of the outcome-independence test did not catch the fourth mutation, because the six band run's fully-covered groups make a flip degenerate. The test now runs a second plan that mixes hits and misses inside each stated confidence, and it fails as it should.
Note on the machine
The disk hit 100% mid-run and every build failed with ENOSPC. I freed
zorp/target/debug/incrementalin the main checkout (pure cache, no process using it, 11G) and two duplicatelibduckdb-sysbuild directories inside this agent's own worktree target. I did not touchtarget/releaseanywhere, the calibration run on.claude/worktrees/aryabhatta/target/release, port 7777, or~/.config/zorp/. There is still roughly 350G of stale per-worktree debug caches on that volume and it will wedge again.https://claude.ai/code/session_01KGPVQ8wUG7h36zashWYCp4