Repository navigation
feat(repo): repo compare intervals invert the sign-flip test (#292) - #409
Conversation
A repo compare row took its 95% interval from a percentile bootstrap
(gate, score) or a t interval (the other measures) and its p from the
sign-flip test, and near 0.05 the two disagreed: a 26-task
metric:n_findings row had a t interval across 0 against p 0.0497, a
6-task score row a bootstrap interval clear of 0 against p 0.25.
Every measure's interval is now the set of shifts theta the row's
sign-flip test on d - theta does not reject at the uncorrected 5%
level, each end bisected between the mean and a far bound
(dokimi/repos/compare_interval.py). The test counts every task under
a shift, ties included, so both ends are bounded from 6 tasks on and
open (null) at 5 or fewer. It is exact up to 16 non-zero shifted
deltas, with one cached 2^k x k sign matrix per k; Monte Carlo above,
with one sign matrix drawn once from the seed and reused at every
theta. Its columns for the changed tasks are the draws sign_flip_p
makes, so printed p does not change. The row's own p decides whether
0 is inside, so the interval excludes 0 exactly when p < 0.05; no
mid-p, the ends are step points of p(theta).
Each row's delta names its method ("sign_flip_inversion").
interval_p_disagree now fires only where Holm lifts a gate or score
row across 0.05, and its note says the interval is uncorrected and
p_holm decides the row. An open end prints as -inf / +inf in
repo compare and in the --ledger text. mde, p_holm_floor and the
floor note are unchanged.
Settles issue intel#292 as its author's comment on it specifies.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…el#292) A decision record for the interval method of issue intel#292: repo compare takes it in the change before this one, and the arms track (dokimi/eval/stats.py) follows in an issue of its own. The rationale covers the two disagreeing rows of the issue, Holm, ties under a shift, the exact test without mid-p, no flag for the old method, and the measured cost. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 12 selected item(s). CI at This PR has reached its review limit, so only a re-request starts a review. Review details
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #409 +/- ##
=======================================
Coverage 94.75% 94.76%
=======================================
Files 352 353 +1
Lines 46507 46479 -28
Branches 7970 7956 -14
=======================================
- Hits 44070 44046 -24
+ Misses 1544 1542 -2
+ Partials 893 891 -2
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
3 open issues
What changed in this PR
Aligns repo compare confidence intervals with the sign-flip p-value by inverting the sign-flip test to produce a consistent 95% interval across all measures, addressing interval/p disagreements near α=0.05 (issue #292).
Changes:
- Introduces
compare_interval.pyimplementing sign-flip test inversion (exact up to 16 non-zero shifted deltas, Monte Carlo above) and shared formatting/helpers. - Updates
compare.pyand ledger formatting to use the new interval everywhere, including open-ended intervals (None→±infin text). - Adds/updates tests and documentation to reflect the new interval semantics and note wording.
| File | Description |
|---|---|
| dokimi/dokimi/repos/compare_interval.py | New implementation of sign-flip inversion interval, formatting, and helpers. |
| dokimi/dokimi/repos/compare.py | Switches all measures to the new interval; updates Holm/disagreement logic and output formatting. |
| dokimi/dokimi/repos/ledger.py | Uses shared interval formatter to safely print open-ended intervals. |
| dokimi/tests/test_repo_compare_interval.py | New targeted tests for inversion behavior, determinism, rounding, and JSON formatting. |
| dokimi/tests/test_repo_compare.py | Updates tests to reflect new interval method and output. |
| dokimi/tests/test_repo_compare_reading.py | Updates note/disagreement expectations and interval endpoints. |
| dokimi/tests/test_repo_ledger.py | Adds coverage for open intervals in ledger text and JSON null ends. |
| dokimi/docs/repo-evaluation-contract.md | Documents the new interval definition and open-end behavior. |
| dokimi/docs/architecture.md | Updates module map to include the new interval module. |
| dokimi/research/records/decision/D018.md | Records the decision and rationale for switching interval methodology. |
| dokimi/changelog.d/20261105_120000_antonwolfy_compare-sign-flip-interval.md | Changelog entry describing behavioral changes and compatibility notes. |
Review effort: Lite
A review of intel#409 found that sign_flip_interval([1e16] * 6, ...) printed an open upper end: the far bound was the extreme + 1.0, and 1e16 + 1.0 rounds back onto 1e16, so every shifted delta tied there, p(far) was None and the end read as unbounded. The same held for the lower end at -1e16. Finite measures go up to SCORE_BOUND (1e150), so such deltas are valid input. The far bound is now 1 past the extreme below 1 and twice the extreme from 1 on, which is always representably past every delta by at least 1. A new test pins both ends at +-1e16 and +-1e150 and a mixed 1e16/2e16 row; it also reaches the bisection's stop where no float is left between the two bounds, a line codecov reported as uncovered. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
A review of intel#409 noted that delta.mean, now rounded to 4 places in paired_summary, turns a small negative mean such as -3.3e-6 into -0.0: "-0.0" in --json and "-0.000" in the text, while the interval ends next to it already drop the sign of a zero. Adding 0.0 after the rounding maps -0.0 to 0.0, as _rounded does for the ends; a test pins the --json value and the printed "+0.000". Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
codecov reported two changed lines of compare_interval.py no test ran on intel#409. The bisection's stop with no float left between the bounds is reached by the large-delta test of the previous commit. This one covers the other: ShiftTest.p returns None when every shifted delta is a tie, as sign_flip_p has no p then. The test checks that at 0 on an all-tied row (equal to sign_flip_p there) and at a shift onto a row of equal deltas. Without that return, p is 1.0 there. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Review point 4480981025: an interval whose ends lie strictly between 0 and 0.0005 printed as [+0.000, +0.000] in repo compare and the ledger text, and in a Holm family of two the disagreement note then said that displayed interval excludes 0. Six score deltas of +0.0002 per treatment, two treatments, show it. format_interval keeps 3 decimals, and prints an end that is non-zero but would read 0.000 at 2 significant digits, so the row above reads [+0.0002, +0.0002] in the table and in its note. An end of exactly 0 still prints +0.000. The contract doc and the changelog fragment say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
Review at 7013897. The change does what it says: on every row the interval now excludes 0 exactly when p is below 0.05, and the printed p, p_holm, mde and p_holm_floor stay as they were. The far bound fix (9592980) and the -0.0 fix (c1f2923) are right. What is left is small: the tie tolerance at large values and a few leftovers, in the line comments.
Agreed with @exai-bot on ends below 0.0005 printed as +0.000; on the [1e-8] * 6 ends I disagree and replied in its thread.
What I ran, at 7013897:
make ci-local --offline: 0 failed, 19 passed, 13 skipped (network, Docker and other-Python jobs).- Coverage of the 95% interval for the true mean delta, 300 simulated rows each, against the old bootstrap and t intervals: gate +0.3/-0.1 over 12 tasks 0.957 (bootstrap 0.890, t 0.970); gate +0.5 over 16 tasks 0.907 (0.977, 0.907); skewed continuous 0.937 (0.907, 0.933); normal 0.973 (0.953, 0.970). So the new interval covers about as well as the old ones did.
- Large values:
[±1e16] * 6and[1e16, 2e16, 3e16] * 2now give bounded ends; the interval against the same test withsign_flip_p's sums, 150 random rows per scale, differs from 3e6 on.
Checked, no issues
- 0 and p: the row's p places 0 in
_end, and_roundedkeeps an end off 0, sointerval_p_disagreefires only where Holm lifts p, as the docs say. - Monte Carlo draws: the first columns of
_drawsaresign_flip_p's draws for the same seed, and the tests pinp(0)against it, ties nearEPSincluded. - Open ends: at 5 or fewer tasks the floor
2/32is above 0.05, so both ends arenulland print as±infinrepo compareand the ledger text. - Docs and changelog: the contract, the architecture page, D018 and the fragment say the same thing; the fragment says the ends can now be
nullfrom 2 to 5 tasks.
Review point: from deltas of a few million on, the exact branch of ShiftTest.p summed the observed deltas in Python but each sign pattern in the matrix product, and the two rounded apart by more than EPS. The observed pattern and its mirror then fell below their own threshold, p dropped by 2 / 2**n at those shifts and the bisection went to another step point: on a 9-task row scaled by 1e7 the ends moved from about 666667 / 7566667 to 667388 / 7566582. The threshold is now the least of the Python sum and the product's sums for the observed signs and their mirror, less EPS, so the observed pattern always counts and no other sums merge. A relative tolerance (EPS times the sum of |delta|) would merge distinct sums: against a test summed per pattern as sign_flip_p sums, it moved ends by up to 0.06 at 1e7, where this change keeps them within 1e-7 and leaves [1e16] * 5 + [2e16] at (1e16, 2e16). Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Review point: min_detectable said its quantiles are t "as the interval of mean_ci is", and the mde test said "the interval is a t interval at df 3", but no repo compare row uses mean_ci now: every row's interval inverts the sign-flip test. Both now say the t quantiles are those of the planning figure, and the test's name drops "of the interval". Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Review point: the ledger calls compare(..., exact=True), and with it sign_flip_interval returned the raw bisection points, so a rule's --json delta read lo 0.5000000223517418, hi 0.9999999925494194 for gate deltas [1] * 7 + [0] where main gave 4 places. Those digits are not exact: an end is only known within TOL (1e-7). The ledger's delta also dropped method. sign_flip_interval now rounds its ends to 4 places by default, keeping an end off 0 as before; exact still gives p and the mean unrounded. Only a caller that asks for rounded=False gets the bisection points, which the step-point tests use. ledger._entry copies delta.method, and a rule result with no pairs has method null. The changelog fragment names the ledger's method. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Review point: the Tests (py3.11) check failed on test_an_end_is_bounded_at_deltas_too_large_to_move_by_one, with the upper end of [1e16] * 5 + [2e16] at 2e16 + 4 instead of 2e16. Python 3.12 made sum() of floats compensated; 3.11 adds left to right. At the shift 2e16 + 4 the left-to-right observed sum rounded below the product's sum for the observed signs, so the min() of 02bf4c6 let the pattern 8 below it count too: p went from 2/64 to 4/64, above 0.05. ShiftTest.p now takes the observed sum from math.fsum, which rounds exactly on every Python, and sign_flip_interval starts the bisection from math.fsum's mean. On 238 random rows of 6 to 24 tasks at scales 1 to 1e100, the ends at the PR's head moved with the summation in 67 rows; now they move in none, and they equal the head's ends under 3.12's sum() in every row. The test runs under the builtin sum and under left-to-right addition, and a new test pins identical unrounded ends under the builtin, a compensated and a left-to-right sum on 40 rows at 1e6 to 1e100. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
Review rounds done at
Declined, with the reason in each thread:
@vlad-perevezentsev: the rounds above answer your points; please take another look. |
napetrov
left a comment
There was a problem hiding this comment.
COMMENT at 4e871a3: the inversion does what #292 asks; earlier threads (T1-T9) are resolved on head. Two nits and one question inline, none blocking.
| # | Finding | Provenance |
|---|---|---|
| 1 | mean outside own interval, tiny deltas | changed (probed) |
| 2 | p(0) vs sign_flip_p at ~1e9 scale |
introduced (probed) |
| 3 | ledger -0.000 |
pre-existing (read) |
Coverage: plain + edge pass with their probes; not run: full pytest (no scipy here), end-to-end repo compare/repo ledger. CI still running at review time.
| ci = format_interval(d["lo"], d["hi"]) # an end is open (None) at 5 or fewer paired cases | ||
| label = "mean score" if e["measure"] == "score" else "pass rate" # a score is not a success rate | ||
| lines.append(f" paired {e['paired']} of {e['linked']} linked: {label} {_num(e['baseline_mean'])} -> " | ||
| f"{_num(e['treatment_mean'])}, delta {d['mean']:+.3f} {ci}, p {_p(e['p'])}, " |
There was a problem hiding this comment.
question: ledger text can still print delta -0.000
The ledger calls compare(..., exact=True), and compare.py:570 only drops -0.0 on the rounded path, so a mean like -1e-5 reaches {d['mean']:+.3f} → -0.000. Same on base (read, not run). In scope for the -0.000 fix, or leave it?
There was a problem hiding this comment.
I would leave it. With exact=True the mean is not rounded, so -1e-5 is a real negative value, not -0.0; -0.000 then keeps its sign, as repo compare prints -0.0004 since c1f2923. The fix there was only for a mean rounded onto -0.0.
There was a problem hiding this comment.
The ledger calls compare(..., exact=True), where delta.mean is the unrounded mean its verdicts read (compare.py:181). A mean of -1e-5 there is a real negative value, not a -0.0 produced by rounding, so -0.000 keeps its sign. repo compare prints -0.0004 the same way. c1f2923 only maps a mean rounded onto -0.0 to 0.0. No change.
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
Review at 4e871a3, unchanged since my last round. Everything I raised is fixed, and the change does what #292 asks; one nit from another reviewer is still open.
Agreed with @napetrov on the mean printed outside its own interval for tiny deltas; on the p(0) mismatch at 1e9 I could not reproduce it and asked for a row in its thread; on the ledger's -0.000 I would leave it and replied in its thread.
What I ran, at 4e871a3:
- The three new points: six deltas of 4e-5 give
mean 0.0andlo = hi = 0.0001; 900 random 6-10 task rows at 1e9, in three shapes, giveShiftTest(d).p(0.0) == sign_flip_p(d, exact=True)every time. make ci-local --offlinefrom my last round still holds: the head did not change.
Review point 4483192510: _rounded keeps a non-zero end that 4 places would put at 0 at +-0.0001, so it keeps its side of 0. The mean next to it was rounded on its own: six deltas of 4e-5 gave delta.mean 0.0 with lo = hi = 0.0001 in repo compare --json, and the mirror for -4e-5. paired_summary now holds the rounded mean inside the rounded ends (compare_interval.within, an open end unbounded), so that row reads mean 0.0001. A mean whose interval holds 0 is still rounded as before. The exact path (the ledger's) keeps its full-precision mean. The contract doc and the changelog fragment say so; a test pins both signs and a row whose interval holds 0. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Review point 4483566762: ShiftTest's docstring said p at 0 is sign_flip_p of the deltas, and the module docstring that the Monte Carlo p(0) is the printed p. From deltas of about 1e6 on that holds up to rounding only: where magnitudes repeat (1e9 + 0.1, 2e9 + 0.2), sign patterns whose sums tie the observed one in exact arithmetic round to one side of it in the matrix product and to the other in sum(). Seven deltas of that kind give p(0) 0.046875 against sign_flip_p's 0.0625. Both docstrings now say so, and that the row's own p, not p(0), places 0. A test pins that row: its p is 0.0625 and its interval reaches 0. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
@napetrov (#409 (review)): Finding 1 (mean outside its own interval) is fixed in cd41468. Finding 2 (p(0) against |
@vlad-perevezentsev (#409 (review)): The open nit, the mean printed outside its interval, is fixed in cd41468. The p(0) mismatch reproduces only when magnitudes repeat: the example row and the counts are in its thread, and the docstrings are corrected in 05cb808. The ledger |
The py3.11 Tests leg failed on test_the_row_p_places_zero_where_large_deltas_round_p_at_zero_apart, added in 05cb808: it pinned the row's p at 0.0625, but sign_flip_p sums with sum(), compensated from Python 3.12 on and left to right before, and on those 1e9 deltas 3.11's order counts one tie pattern fewer: p 0.046875 and a lower end of 0.0001. The row still agrees with its p there; only the pin was version-bound. The test now patches compare's sum with math.fsum and with left-to- right addition, as the interval tests already do for compare_interval, and pins each order's p and lower end (0.0625 and 0.0; 0.046875 and 0.0001), so both cases run, and hold, on every Python. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>


Summary
Settles #292, where
repo comparetakes a row's 95% interval from one method and its p from another, and thetwo disagree near 0.05 (issue). The open questions (Holm, ties
under a shift, no mid-p, no flag, speed) are settled in its author's comment on the issue, and this PR follows
that comment.
On
main, gate and score rows get a percentile bootstrap (bootstrap_ci), the other rows get a t interval(
mean_ci), and p always comes from the sign-flip test. Two rows from a replay of a dpnp PR (26review_replaytasks):mainmetric:n_findings, 26 tasksThe replay's own
n_findingsdeltas can't be recovered, so the first row uses 26 integer deltas that reproduce it(the issue's row read +0.89, t [-0.091, +1.860], p 0.0497). The score row is the issue's own (deltas 0, 0, 0.333,
0.167, 0, 0.333). On
mainthe second row carriesinterval_p_disagreeand the "two methods" note. Here it readsas inconclusive, with the floor note (
with 3 changed tasks the sign-flip p cannot go below 0.25).Changes
dokimi/dokimi/repos/compare_interval.py:ShiftTest.p(theta)is the sign-flip p ofd - thetaover every task. A shifted delta withinEPSof 0is a tie, so the tasks tied at 0 count once theta moves off 0. At theta = 0 it equals
sign_flip_p, exactand Monte Carlo,
Noneincluded when every delta ties, up to rounding at large deltas where magnitudesrepeat (below); the row's own p places 0 either way. Exact, the observed sum it compares
against is the least of its
math.fsumand the product's sums for the observed signs and their mirror, sothe observed pattern counts itself however large the deltas.
math.fsum, notsum. Python'ssumof floats is compensated from 3.12 on and adds left to right on 3.11;
math.fsumrounds exactly on everyversion, so a row's ends are the same on every Python.
EXACT_MAX(16) non-zero shifted deltas the test is exact: one cached2^k x ksign matrix per k,and one matrix-vector product per theta.
20000 x nsign matrix, drawn once and reused at every theta (commonrandom numbers). Its columns for the changed tasks are the very draws
sign_flip_pmakes: numpy'sMT19937 is seeded with
random.Random(seed)'s state, so it gives the same doubles. So the printed pdoes not change.
sign_flip_intervalbisects each end to 1e-7 and returns the last shift not rejected. The bracket runsfrom the mean to a far bound past every delta and 0. The far bound is 1 past the extreme below 1, and
twice the extreme from 1 on, so a delta of 1e16 or more cannot round the bound back onto itself. The
ends are step points of p(theta); there is no mid-p.
None(open) when p at the far bound is still >= 0.05, which is the case for every row of 5or fewer tasks. From 6 tasks on both ends are bounded, whatever the ties and however large the deltas
(up to
SCORE_BOUND, 1e150).exactly when p < 0.05. That includes a row of more than 16 tasks with 16 or fewer changed, where p is
exact and the interval is Monte Carlo.
exact=Truetoo (theledger's call), since bisection only knows them within 1e-7;
rounded=Falsereturns the bisection points.EXACT_MAX,N_RESAMPLESandEPSmove here.compare.pyimports them, socompare.EXACT_MAXand theledger's
from .compare import EPSstill work.repos/compare.py(774 to 765 lines, kept small and local):paired_summaryuses the new interval for every measure; it no longer callsbootstrap_ci/mean_ci.deltagains"method": "sign_flip_inversion".delta.meanis rounded to 4 places as before, and a meanthat rounds onto 0 from below is now
0.0, not-0.0. Rounded, it stays inside the row's rounded ends(
compare_interval.within); withexact=True(the ledger's call) it keeps full precision._holmand_inconclusive_notetreat aNoneend as open. The inconclusive note keeps its "2+ tasks"rule through
n_tasks.the interval [...] excludes 0 but p_holm X is ≥ 0.05: the interval is uncorrected; p_holm decides this row.interval_p_disagreecan now be true only on a gate or score rowwhose p was lifted across 0.05 by Holm's correction.
format_compareprints an open end as-inf/+inf, so a one-task row now reads[-inf, +inf], not-.mde,p_holm_floorand the floor note are unchanged.repos/ledger.py:format_ledgerprints the interval with the same formatter, so anullend (5 or fewerpaired cases) prints as open and cannot crash.
_entrycopiesdelta.methodinto each rule result (nullfor a result with no pairs). No schema changes: there is no
repo_compareschema, andrepo_ledger.v1.jsondescribes the ledger file, notdelta.[1e16] * 6open ((1e16, None)):1e16 + 1.0 == 1e16, so every shifted delta tied there. The lower end of[-1e16] * 6was open thesame way. Fixed in 9592980.
delta.meanno longer reads-0.0in--jsonand-0.000in the table.[+0.000, +0.000], and the Holmnote then said that interval excludes 0. In 89e2d21 the interval formatter keeps 3 decimals, but a
non-zero end that would read
0.000prints at 2 significant digits ([+0.0002, +0.0002]), inrepo compareand the ledger text. An end of exactly 0 still prints+0.000; the mean column stays at3 decimals.
ShiftTest.psummed the observed deltas in Python and eachpattern in the matrix product, and the two rounded apart by more than
EPS: the observed pattern stoppedcounting and the bisection went to another step point. In 02bf4c6 the observed sum is the least of the
Python sum and the product's sums for the observed signs and their mirror.
compare(..., exact=True), which returned the raw bisection points as ends(
0.5000000223517418), and its rule results droppeddelta.method. In 7a667bd the ends are rounded to 4places in both cases, as on
main, anddelta.methodis copied.min_detectablesaid its t quantiles are those ofmean_ci's interval; in 00a5894 it says they are thoseof the planning figure.
Tests (py3.11)check failed: the upper end of[1e16] * 5 + [2e16]was2e16 + 4, not2e16.02bf4c6's threshold took the Python sum, and 3.11's left-to-right
sumrounded the observed sum at theshift
2e16 + 4below the product's, so a pattern 8 below it counted too and p rose from 2/64 to 4/64.In 4e871a3 the observed sum and the mean come from
math.fsum.4e-5gavedelta.mean0.0withlo = hi = 0.0001: the ends are kept off 0 at±0.0001, and the mean was rounded on its own. In cd41468 the rounded mean is held inside the roundedends, so that row reads mean
0.0001(-0.0001for-4e-5). A mean whose interval holds 0 rounds asbefore. The text's mean column stays at 3 decimals (
+0.000).ShiftTestdocstring said p at 0 issign_flip_p, and the module docstring that the Monte Carlop(0) is the printed p. At large deltas whose magnitudes repeat (
1e9 + 0.1,2e9 + 0.2), sign patternsthat tie the observed sum in exact arithmetic round to one side of it in the matrix product and to the
other in
sum(), so p(0) can differ. In 05cb808 both docstrings say so, and that the row's own p, notp(0), places 0.
Tests (py3.11)check failed on 05cb808's 1e9 row: the test pinned its printed p at 0.0625, butsign_flip_psums withsum(), and on those deltas 3.11's left-to-rightsumcounts one tie patternfewer, so p reads 0.046875 and the lower end 0.0001. The row agrees with its p on both Pythons; only the
pin depended on the version. In fc93796 the test runs that row under a compensated sum and under
left-to-right addition, and pins each one's p and lower end.
tests/test_repo_compare_interval.pycovers:sign_flip_p;builtin
sumand under left-to-right addition (3.11's);sum, a compensated sum andleft-to-right addition;
p(0) == sign_flip_p: exact, Monte Carlo, a near-tie at the 16/17 boundary, andNonewhen everydelta ties;
its interval reaches 0, under left-to-right addition (3.11's) its p is 0.046875 and its lower end 0.0001;
fewer changed,
exact=Trueand rounded;cleared;
±4e-5held inside ends of±0.0001;method/nullin JSON.test_repo_compare.pyandtest_repo_compare_reading.pythat pinned bootstrap/tends or the old note wording now pin the new numbers and wording.
test_repo_compare_reading.pyalsopins a Holm family of two rows with ends of +0.0002, in the table and in the note.
test_repo_ledger.pypins[+1.000, +1.000]and[-inf, +inf]in the ledger text, and a rule'sdeltaof{mean 0.875, lo 0.5, hi 1.0, method sign_flip_inversion}.docs/repo-evaluation-contract.md§9, therepo comparebullet: the method, exact/Monte Carlo, tiesunder a shift and the open end, step points and no mid-p, how a small non-zero end prints, the reworded
Holm note,
delta.method.docs/architecture.md: the module map.compare.pymodule andpaired_summarydocstrings, and thecompare_interval.pymodule docstring.research/records/decision/D018.md(separate commit). It records the method for bothtracks. The arms track (
dokimi/eval/stats.py) is not changed here; it follows in its own issue.Risk
repo compareinterval changes value: gate and score rows lose the bootstrap, the other rows the tinterval. Rows of 2 to 5 tasks had bounded ends; now they have
nullends in--jsonand print[-inf, +inf].--jsonthat formatsdelta.lo/delta.hias numbers whenevern_tasks > 1must handlenull. Inside the package those readers areformat_compare,_reading_notes,_inconclusive_note,_holmand
format_ledger, all handled here. Each row names its method, so output from before and after thischange can be told apart. The release before this change still computes the old intervals.
+0.333] above.
0.000now prints at 2 significant digits(
+0.0002), so its text is wider than 3 decimals.--jsonis unchanged.--jsonrule results gaindelta.method(nullwithout pairs); their ends stay at 4 places.p_holm,mdeandp_holm_floordo not change.delta.meanchanges in two cases only: a meanthat rounds onto 0 from below was
-0.0onmain(itsround) and is0.0here; and inrepo compare, anon-zero mean that rounds onto 0 next to an end kept off 0 was
0.0and is that end,±0.0001.repos/compare.py. Thisdiff there is local: the imports, the module docstring's interval sentence and third note,
_holm's oneline,
paired_summary, theformat_compareinterval cell,_reading_notes' first branch and_inconclusive_note's guard.paired_summaryrow, median of 5 runs. The interval itself took 4 ms at 16 changed tasks (2^16patterns), 2 ms at 26 tasks (20 changed, Monte Carlo) and 13 ms at 200 tasks. The whole row took 52, 22 and
161 ms, most of it in
sign_flip_p, which is unchanged. Sign matrices: up to about 17 MB cached for theexact range, and 32 MB per Monte Carlo matrix at 200 tasks, with two kept. One interval makes about 52
p(theta)calls. Summing the moved deltas as a Python list withmath.fsumtakes under 1% of the interval'stime.
Testing
On a Linux review host, at
fc93796(onmain8d54c93):make ci-local: 30 passed, 0 failed, 7 skipped. The skips: external-catalog-smoke needs docker;test-latest, docs deploy and pip-audit report have job conditions that are false on a PR; cli-surface
"Package the result" has an unknown expression; conda-forge-deps has no micromamba; semgrep secrets is
not on PATH. 15681 tests passed, 64 skipped. Changed-line coverage was 100% (88 lines measured).
The two issue rows: on
mainthe 26-task row's t interval includes 0 against p 0.0498, and the score row'sbootstrap interval excludes 0 against p 0.25. Here both agree with p (table above). They are pinned in
test_the_issue_n_findings_row_now_excludes_zero_as_its_p_saysandtest_the_issue_score_row_now_includes_zero_as_its_p_says.The review follow-ups, at
e609c31and here:e609c31sign_flip_interval([1e16] * 6, 0.03125)(1e16, None)(1e16, 1e16)sign_flip_interval([-1e16] * 6, 0.03125)(None, -1e16)(-1e16, -1e16)[1e16] * 5 + [2e16](1e16, None)(1e16, 2e16)delta.meanof deltas[-1e-5, 0, 0]-0.0(prints-0.000)0.0(prints+0.000)test_an_end_is_bounded_at_deltas_too_large_to_move_by_oneandtest_a_mean_rounded_to_zero_is_zero_not_negative_zerofail ate609c31and pass here.The small-end follow-up: six score deltas of +0.0002 for each of two treatments (p 0.03125, p_holm 0.0625)
printed
[+0.000, +0.000]at7013897, in the table and in the Holm note that says it excludes 0. Here theyprint
[+0.0002, +0.0002].test_a_small_interval_against_p_holm_prints_its_ends_off_zeroandtest_an_end_three_decimals_would_print_as_zero_keeps_its_sign_and_valuefail at7013897and pass here.The first also fails on
main, which prints the same[+0.000, +0.000].Large deltas, at
89e2d21and here: from deltas of about 1e6 on, the product's sum for the observed signsfell below the Python-summed observed sum minus
EPSat 136 to 622 of 3000 random shifts per scale. Againsta test that sums each pattern with Python's
sum, assign_flip_pdoes, on 6-10 task rows at 1, 1e6, 3e6,1e7 and 1e9: p now matches at all 2000 random shifts, and every end is within 1.4e-6 of that test's. A
relative tolerance,
EPS * max(1, sum|x|), was tried and not taken: it moved ends by up to 0.06 at 1e7.A tolerance of the product's worst-case rounding was tried too: it put the lower end of
[1e16] * 5 + [2e16]at 9999999999999994.test_large_deltas_keep_the_ends_and_the_p_of_the_shifted_deltasfails at
89e2d21: the 9-task row scaled by 1e7 has ends 667388 and 7566582, about 720 off.The ledger: a rule over gate deltas
[1] * 7 + [0]readlo 0.5000000223517418, hi 0.9999999925494194andno
methodat89e2d21; herelo 0.5, hi 1.0, method sign_flip_inversion.test_a_rule_delta_has_ends_at_four_places_and_names_its_methodfails at89e2d21and passes here.Python 3.11's
sum, at7a667bdand here: withcompare_interval'ssumreplaced by left-to-rightaddition (
functools.reduce(operator.add, xs, 0.0)),[1e16] * 5 + [2e16]read(1e16, 2.0000000000000004e16)at
7a667bd, the failing check's value; here it reads(1e16, 2e16). On 238 random rows of 6 to 24 tasks atscales 1 to 1e100, the rounded ends at
7a667bdchanged with the summation in 67 rows, and here in none.Here they equal
7a667bd's ends under 3.13'ssumin every row, and p at 0 equalssign_flip_pin every rowunder both summations.
test_an_end_is_bounded_at_deltas_too_large_to_move_by_one[left_to_right]andtest_the_ends_are_the_same_whether_python_sums_compensated_or_left_to_rightfail at7a667bdand pass here.The tiny-mean follow-up, at
4e871a3and here: six deltas of4e-5readdelta.mean 0.0, lo 0.0001, hi 0.0001at4e871a3(-4e-5the mirror); here the mean is0.0001(-0.0001). Five of4e-5and one of-1e-5, whose interval holds 0, keep mean0.0.test_a_tiny_mean_stays_inside_its_interval_kept_off_zerofails at
4e871a3and passes here;mainhas nocompare_intervalto run it on.p(0) at 1e9, at
4e871a3: on 6-10 task rows, 500 per shape, p(0) equalssign_flip_pon every row ofuniform deltas, quarter fractions or tenths, and differs on 169 to 296 rows drawn from a few repeated
magnitudes (
1e9 + 0.1,2e9 + 0.2,3e9 + 0.3,1e9 + 1/3), under 3.13'ssumand left-to-rightaddition. 281 of 300 Monte Carlo rows of 17-22 such deltas differ too. The printed p and the side of 0 do not
depend on it: on the first three such rows drawn where p(0) and
sign_flip_pfall on either side of 0.05, the intervalfollows the printed p.
test_the_row_p_places_zero_where_large_deltas_round_p_at_zero_apartpins one.The 1e9 row on Python 3.11, at
05cb808and here: withcompare'ssumreplaced by left-to-right addition,the row reads p 0.046875 and ends
(0.0001, 1500000000.2667), the failing check's p; under 3.13'ssumitreads p 0.0625 and
(0.0, 1500000000.2667).interval_p_disagreeis false in both. The test now pins bothreadings on every Python:
[compensated]and[left_to_right]pass here.A finer bisection tolerance, below
EPS, was tried and not taken. It moves rounded ends into the tie zonearound 0: the issue's score row would go from
(0.0, 0.333)to(-0.0001, 0.333), and six tied deltasfrom
(0.0, 0.0)to(-0.0001, 0.0001).Mutants, each on a copy of the commit, with the tests that pin it run there. All fail but one:
sign_flip_p;-, incompare_intervaland in the ledger;delta.methoddropped;interval_p_disagree;ShiftTest.preturning 1.0 instead ofNoneon an all-tied row, and with that guard removed;+ 0.0ondelta.meanremoved;digits;
exact=True;delta.methodnot copied into the ledger's rule results;sum(both the left-to-right leg and the three-summation test fail);sum(the three-summation test fails, one end off by an ulp);delta.meanno longer held inside its ends, and held by only its lower or only its upper end (thetiny-mean test fails for both signs, then for -4e-5, then for +4e-5);
ShiftTest.p(0)instead of the row's p (the 1e9 row's lower end reads 0.0001, not 0.0);The one test that passes under a mutant is
test_the_ends_are_step_points_of_pwith exact enumerationdisabled, because Monte Carlo ends are step points too. That mutant is caught by
test_p_at_zero_is_the_sign_flip_p_exact_and_monte_carlo.Not run: the matrix legs on Python 3.11, 3.12 and 3.14 (
make ci-localruns one Python). 3.11's summationis simulated in the tests above, not run on a 3.11 interpreter.
Revert
git revertof the PR's commits restores the bootstrap/t intervals. No stored artifact or schema versionchanges. JSON written in between carries
delta.method, which the old code neither writes nor reads.Checklist
pytestpassesruff,mypy, and schema checks passAGENTS.md.github/.licenserc.yaml)