Conversation
The existing examples measure hand-written toy rows and synthetic normal draws. Both are fine for showing the API, but neither shows the library doing the thing it exists for: deciding whether the gap between two models survives resampling. examples/census_income.ipynb downloads the Census Income set from OpenML, trains a gradient-boosted tree and a deliberately handicapped logistic regression, and runs the full pipeline over both. Average precision comes out at [0.816, 0.835] against [0.634, 0.660], and at a precision floor of 0.80 the recall intervals are [0.593, 0.639] against [0.227, 0.309]. Neither pair overlaps, so the notebook ends on a decision rather than on two bare numbers. Real data was the point. Iris and breast_cancer are both bundled and offline, but logistic regression is near-perfect on either -- measured AP band widths of 0.000 and 0.015 -- so the band collapses and the figure argues against the library. Census Income is genuinely hard, and the bands have something to say. The notebook is committed with its outputs and executed in CI next to the quickstart, so a change that moves any printed number fails the build. That costs the notebook job a network dependency on OpenML; a fetch failure there is a flake rather than a defect, and the comment in test.yml says so. Two portability details: the AUC cell selects its columns before `groupby.apply` rather than passing `include_groups=False`, which would have required pandas 2.2 while the install line in the notebook asks only for `replicas[pandas,plot]`; and the `notebook` extra declares scikit-learn>=1.4, the floor for HistGradientBoostingClassifier(categorical_features="from_dtype"). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four models now instead of two: gradient boosting, a random forest, a linear SVM, and the numeric-only logistic regression. LinearSVC has no predict_proba, so it scores through decision_function on a -3 to 12 scale, which makes the point that a prediction column only has to be monotone -- replicas ranks by it and never reads it as a probability. All four average-precision intervals came out disjoint: 5th percentile of each model clears the 95th of the next. That includes the three-point gap between the forest and the SVM that the plotted curves cannot separate by eye. An earlier draft of the prose guessed the opposite and had to be corrected against the executed output. The new sections carry race and sex through the prediction frame and re-run the same pipeline grouped by them, first col="sex" and then row="sex", col="race". The grid is the argument: White men hold 8,582 test rows and 2,717 positives, while women recorded as Other hold 46 rows and 3 positives, so the bands go from tight to panel-wide and every model becomes indistinguishable from every other. Those columns stay out of bootstrap's `by`. Stratifying on them would fix each subgroup's size across replicas and understate exactly the uncertainty these sections are about. The operating-point table needed its commentary rewritten too. Resolution is not monotone in subgroup size -- with three positives a single true positive at the top of the ranking gives precision 1.0 and clears the target trivially, while the group with twenty positives resolves least often of all. The notebook now explains that inversion rather than implying smaller is simply worse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drops race from the disaggregated analysis and keeps sex, as asked. The row/column grid stays: it is now row="sex", col="age band", with the band cut at 25/35/45/55. Age turns out to make the point better than race did. The under-25 cells are not small -- 1,165 women and 1,351 men -- but almost nobody in them earns above $50K, so they hold 6 and 14 positives. A precision-recall curve is built out of positives, so those panels are a mess of overlapping bands while the 45-54 cells, with hundreds of positives, separate the models in the same order the aggregate did. The lesson lands on the right variable: it is the positive count, not the row count, that decides whether a subgroup can be measured at all. The sizes table now sorts by positives rather than rows, so the cells the prose points at are the ones at the top, with the row count sitting beside them for contrast. The resolution commentary is rewritten against the new output. The inversion still holds and still needs explaining: the under-25 women, with six positives, resolve 92 times in 100 because one true positive at the top of the ranking gives precision 1.0, while the under-25 men, with fourteen, resolve only 70 times. race remains a column in the dataset and therefore in the raw preview and the models' feature set; it is only gone from the analysis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Transposes the grid to row="age band", col="sex", so the two sexes sit side by side within each age row and the figure reads down the age axis. Shape goes from 2x5 to 5x2; height and aspect adjusted to suit. Fixes a real ordering bug while here. The age band labels are strings, and "under 25" sorts after "55+", so that panel was rendering last -- while the prose told the reader to start from it. Renaming the band to "17-24" (17 is the minimum age in the data) makes the labels sort chronologically on their own, in the plot and in the two tables. Verified the grid comes out as rows 17-24, 25-34, 35-44, 45-54, 55+ against columns Female, Male. Prose follows the new orientation: read top to bottom, the 17-24 row rather than the under-25 column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Adds
examples/census_income.ipynb— self-contained, committed with its outputs, and executed in CI like the quickstart.What it shows
The existing examples measure hand-written toy rows and synthetic normal draws. They demonstrate the API, but not the thing the library exists for: deciding whether the gap between two models survives resampling.
This one downloads the Census Income ("Adult") set from OpenML, trains a gradient-boosted tree and a deliberately handicapped logistic regression, and runs the full pipeline over both.
No pair overlaps, so the notebook ends on a decision instead of two bare numbers. 48,842 rows, 23.9% positive, 14,653 held out, 100 replicas.
Why this dataset
Measured the alternatives against the same pipeline before choosing:
Iris and breast_cancer are bundled and need no network, which is why they were the obvious candidates — but logistic regression is near-perfect on both, the band collapses, and the resulting figure argues against the library. Census Income is hard enough that the intervals carry information.
CI
The notebook job gains a step and now guards both notebooks in the working tree. The checker compares
text/plainpayloads, so every printed number is pinned — a change that moves a metric fails the build, which is the regression signal you asked for.Verified reproducible: executed, then re-executed through
scripts/check_example_notebook.py, matching. Both models are bitwise-deterministic across runs on fixed seeds, and the housefloat_format = "{:.3f}"absorbs floating-point jitter across machines.This costs the notebook job a network dependency on OpenML. A fetch failure there is a flake, not a package defect; the comment in
test.ymlsays so. Runtime is about 10 seconds — the wholereplicaspipeline over 2.96M bootstrapped rows takes ~4.5s.Portability details
groupby.applyrather than passinginclude_groups=False, which needs pandas 2.2 — the notebook's own install line asks only forreplicas[pandas,plot].notebookextra declaresscikit-learn>=1.4, the floor forHistGradientBoostingClassifier(categorical_features="from_dtype").pr_bandimport in the first draft; the notebook is linted like any other file now that Move the reference design notebook into docs/ #10 split the per-file ignores.Note on the dataset
Census Income has documented baggage as a fairness benchmark (Ding et al., Retiring Adult, 2021). The notebook says so in prose, uses it only as a hard imbalanced classification problem, and does not facet any result by the
raceorsexcolumns.Verification
ruff check,ruff format --check, 135 tests, and the notebook checker all pass.🤖 Generated with Claude Code