selection-fragility 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- selection_fragility-1.0.0/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
- selection_fragility-1.0.0/.github/workflows/selection-fragility-test.yml +46 -0
- selection_fragility-1.0.0/CHANGELOG.md +438 -0
- selection_fragility-1.0.0/CITATION.cff +102 -0
- selection_fragility-1.0.0/CONTRIBUTING.md +62 -0
- selection_fragility-1.0.0/EVALUATION_CARD.md +148 -0
- selection_fragility-1.0.0/LICENSE +21 -0
- selection_fragility-1.0.0/MANIFEST.in +36 -0
- selection_fragility-1.0.0/PKG-INFO +449 -0
- selection_fragility-1.0.0/README.md +419 -0
- selection_fragility-1.0.0/pyproject.toml +89 -0
- selection_fragility-1.0.0/requirements-lock.txt +20 -0
- selection_fragility-1.0.0/setup.cfg +4 -0
- selection_fragility-1.0.0/src/selection_fragility/__init__.py +73 -0
- selection_fragility-1.0.0/src/selection_fragility/__main__.py +39 -0
- selection_fragility-1.0.0/src/selection_fragility/_internals.py +52 -0
- selection_fragility-1.0.0/src/selection_fragility/compare.py +276 -0
- selection_fragility-1.0.0/src/selection_fragility/fragility.py +885 -0
- selection_fragility-1.0.0/src/selection_fragility/identify.py +312 -0
- selection_fragility-1.0.0/src/selection_fragility/mcs.py +258 -0
- selection_fragility-1.0.0/src/selection_fragility/panel.py +794 -0
- selection_fragility-1.0.0/src/selection_fragility/pivot.py +154 -0
- selection_fragility-1.0.0/src/selection_fragility/prop22.py +90 -0
- selection_fragility-1.0.0/src/selection_fragility/report.py +253 -0
- selection_fragility-1.0.0/src/selection_fragility/resolution.py +435 -0
- selection_fragility-1.0.0/src/selection_fragility.egg-info/PKG-INFO +449 -0
- selection_fragility-1.0.0/src/selection_fragility.egg-info/SOURCES.txt +73 -0
- selection_fragility-1.0.0/src/selection_fragility.egg-info/dependency_links.txt +1 -0
- selection_fragility-1.0.0/src/selection_fragility.egg-info/requires.txt +11 -0
- selection_fragility-1.0.0/src/selection_fragility.egg-info/top_level.txt +1 -0
- selection_fragility-1.0.0/tests/README.md +71 -0
- selection_fragility-1.0.0/tests/conftest.py +176 -0
- selection_fragility-1.0.0/tests/test_ab_testing_domain_review.py +94 -0
- selection_fragility-1.0.0/tests/test_core_api_losspanel_acceptance.py +112 -0
- selection_fragility-1.0.0/tests/test_crosscutting.py +184 -0
- selection_fragility-1.0.0/tests/test_extensibility_review.py +143 -0
- selection_fragility-1.0.0/tests/test_packaging.py +108 -0
- selection_fragility-1.0.0/tests/test_probabilistic_forecasting_review.py +110 -0
- selection_fragility-1.0.0/tests/test_property_based.py +297 -0
- selection_fragility-1.0.0/tests/test_reproducibility.py +100 -0
- selection_fragility-1.0.0/tests/test_round3_reviews.py +308 -0
- selection_fragility-1.0.0/tests/test_round5_competition_integrity.py +139 -0
- selection_fragility-1.0.0/tests/test_round6_clinical_biostatistics.py +70 -0
- selection_fragility-1.0.0/tests/test_round6_financial_adversarial.py +158 -0
- selection_fragility-1.0.0/tests/test_round6_foundation_model_researcher.py +121 -0
- selection_fragility-1.0.0/tests/test_round6_production_scale.py +83 -0
- selection_fragility-1.0.0/tests/test_round6_recsys_cross_domain.py +110 -0
- selection_fragility-1.0.0/tests/test_round7_ab_testing_realistic_data.py +168 -0
- selection_fragility-1.0.0/tests/test_round7_clinical_realistic_data.py +210 -0
- selection_fragility-1.0.0/tests/test_round7_government_policy_realistic_data.py +207 -0
- selection_fragility-1.0.0/tests/test_round7_llm_leaderboard_realistic_data.py +179 -0
- selection_fragility-1.0.0/tests/test_round8_concurrency_safety.py +129 -0
- selection_fragility-1.0.0/tests/test_round8_energy_grid_realistic_data.py +169 -0
- selection_fragility-1.0.0/tests/test_round8_epidemiology_realistic_data.py +188 -0
- selection_fragility-1.0.0/tests/test_round8_formula_rederivation.py +94 -0
- selection_fragility-1.0.0/tests/test_round8_insurance_actuarial_realistic_data.py +179 -0
- selection_fragility-1.0.0/tests/test_round8_losspanel_completeness.py +68 -0
- selection_fragility-1.0.0/tests/test_round8_manufacturing_realistic_data.py +177 -0
- selection_fragility-1.0.0/tests/test_round8_marketing_attribution_realistic_data.py +119 -0
- selection_fragility-1.0.0/tests/test_round8_sports_analytics_realistic_data.py +135 -0
- selection_fragility-1.0.0/tests/test_round8_type_annotations.py +25 -0
- selection_fragility-1.0.0/tests/test_round9_agriculture_realistic_data.py +143 -0
- selection_fragility-1.0.0/tests/test_round9_climate_weather_realistic_data.py +123 -0
- selection_fragility-1.0.0/tests/test_round9_election_polling_realistic_data.py +135 -0
- selection_fragility-1.0.0/tests/test_round9_fraud_detection_realistic_data.py +198 -0
- selection_fragility-1.0.0/tests/test_round9_real_estate_realistic_data.py +175 -0
- selection_fragility-1.0.0/tests/test_round9_subgroup_heterogeneity_realistic_data.py +165 -0
- selection_fragility-1.0.0/tests/test_security_adversarial.py +452 -0
- selection_fragility-1.0.0/tests/test_stage0_losspanel.py +706 -0
- selection_fragility-1.0.0/tests/test_stage1_identified.py +342 -0
- selection_fragility-1.0.0/tests/test_stage2_resolution.py +282 -0
- selection_fragility-1.0.0/tests/test_stage3_kstar_prop22.py +1065 -0
- selection_fragility-1.0.0/tests/test_stage4_pivot.py +357 -0
- selection_fragility-1.0.0/tests/test_stage5_compare.py +167 -0
- selection_fragility-1.0.0/tests/test_stage6_report.py +350 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: selection-fragility bug report
|
|
3
|
+
about: Report a bug in the selection-fragility package
|
|
4
|
+
title: "[selection-fragility] "
|
|
5
|
+
labels: bug
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- ADDED 2026-09-07 (round-4 8-lens PyPI-preflight audit, CONFIRMED LOW, found alongside the same-
|
|
9
|
+
class CI-workflow gap fixed the same day -- see .github/workflows/selection-fragility-test.yml's own
|
|
10
|
+
header comment for the full story). CONTRIBUTING.md points readers at
|
|
11
|
+
`.github/ISSUE_TEMPLATE/bug_report.md`, but the only template that existed lived in the private
|
|
12
|
+
monorepo's own `.github/ISSUE_TEMPLATE/selection_fragility_bug_report.md` -- a dead reference from
|
|
13
|
+
both a PyPI download and this package's own eventual standalone repo, the same way the CI workflow
|
|
14
|
+
was. This is a repo-root-adapted copy (renamed to match CONTRIBUTING.md's stated filename exactly,
|
|
15
|
+
and with the `about:` line's release/selection-fragility/-relative path removed, since once this is
|
|
16
|
+
its own repo there is no such subdirectory). -->
|
|
17
|
+
|
|
18
|
+
**Environment**
|
|
19
|
+
- `selection-fragility` version (`pip show selection-fragility`):
|
|
20
|
+
- Python version (`python --version`):
|
|
21
|
+
- OS (Linux / Windows / macOS, and version):
|
|
22
|
+
|
|
23
|
+
**Minimal reproducible example**
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# A plain Python snippet that fails -- not a description of the failure in prose.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Expected behavior**
|
|
30
|
+
|
|
31
|
+
**Actual behavior** (full traceback if there is one)
|
|
32
|
+
|
|
33
|
+
**Have you checked whether a sibling function has the same issue?**
|
|
34
|
+
Several real bugs in this project's history affected one function but not its closest siblings (or vice versa)
|
|
35
|
+
— see CONTRIBUTING.md's "known code shapes" section. If you've checked, say what you found; if not, no need to.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: selection-fragility test
|
|
2
|
+
|
|
3
|
+
# ADDED 2026-09-07 (round-4 8-lens PyPI-preflight audit, CONFIRMED HIGH). This is the STANDALONE-REPO
|
|
4
|
+
# copy of the CI matrix -- a real gap the audit found: the only version of this workflow that
|
|
5
|
+
# actually runs today lives at the PRIVATE monorepo's own root
|
|
6
|
+
# (Paper3_UIClaims_Forecasting/.github/workflows/selection-fragility-test.yml, moved there
|
|
7
|
+
# 2026-08-27 because GitHub only discovers workflow files at <repo-root>/.github/workflows/, and
|
|
8
|
+
# this package's directory is a SUBDIRECTORY of that monorepo, not its own repo -- see that file's
|
|
9
|
+
# own comment for the full story). But `release/selection-fragility/` is also meant to become its
|
|
10
|
+
# OWN standalone public repo (pyproject.toml's Homepage/Repository point at
|
|
11
|
+
# github.com/sudhanshurai360/selection-fragility) -- and in THAT repo, this directory IS the repo
|
|
12
|
+
# root, so the monorepo's own workflow (with its release/selection-fragility/-scoped `paths:` filter
|
|
13
|
+
# and `working-directory:` default) would never trigger and would fail immediately even if it did.
|
|
14
|
+
# No automated staging script was found anywhere in the monorepo to copy/adapt the workflow when
|
|
15
|
+
# release/selection-fragility/ gets pushed out standalone -- committing this pre-adapted copy here,
|
|
16
|
+
# in the exact location GitHub will scan once this directory becomes a repo root, closes that gap
|
|
17
|
+
# without needing to build that automation. EVALUATION_CARD.md's own CI-matrix reference was
|
|
18
|
+
# corrected to point at this file (a plain repo-root-relative path, not the monorepo's `../../`).
|
|
19
|
+
#
|
|
20
|
+
# DIFFERENCES FROM THE MONOREPO COPY, BOTH INTENTIONAL: no `paths:` filter (this whole repo IS the
|
|
21
|
+
# package once standalone, so every push is in scope -- the monorepo's filter existed only to avoid
|
|
22
|
+
# running this job on unrelated changes elsewhere in a much bigger repo); no
|
|
23
|
+
# `working-directory: release/selection-fragility` default (the checkout root is already the
|
|
24
|
+
# package root here). The matrix/install/test steps themselves are unchanged and should be kept in
|
|
25
|
+
# sync with the monorepo copy by hand until real staging automation exists.
|
|
26
|
+
|
|
27
|
+
on:
|
|
28
|
+
push: {}
|
|
29
|
+
pull_request: {}
|
|
30
|
+
workflow_dispatch: {}
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
install-and-test:
|
|
34
|
+
strategy:
|
|
35
|
+
fail-fast: false
|
|
36
|
+
matrix:
|
|
37
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
38
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
39
|
+
runs-on: ${{ matrix.os }}
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
- uses: actions/setup-python@v5
|
|
43
|
+
with:
|
|
44
|
+
python-version: ${{ matrix.python-version }}
|
|
45
|
+
- run: pip install -e ".[test]"
|
|
46
|
+
- run: pytest tests/ -v
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `selection-fragility` are recorded here. Versions follow
|
|
4
|
+
[semantic versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [1.0.0] — unreleased
|
|
7
|
+
|
|
8
|
+
First public release, accompanying the paper *The Decision Breakdown Point: How Fragile Is "The Best Forecasting
|
|
9
|
+
Model," and What Does That Fragility Cost?*
|
|
10
|
+
|
|
11
|
+
### Added — the v1.0 diagnostic surface (`LossPanel`, identification, resolution, pivot, compare, report)
|
|
12
|
+
|
|
13
|
+
A full redesign of the reporting layer around one validated entry point, replacing the retired `fragile`/`screen`
|
|
14
|
+
verdict (above) with statistics whose calibration was independently verified before shipping, and a discipline of
|
|
15
|
+
refusing to answer rather than printing a confident number the data cannot support (R1/R2, in `resolution_report`).
|
|
16
|
+
|
|
17
|
+
- **`LossPanel`** — the single validated entry point: `.from_losses()` (wide `DataFrame` / 2-D `ndarray` / dict of
|
|
18
|
+
per-model arrays) and `.from_forecasts()` (long forecast-vs-actual frame, computes the loss). Rejects fewer than
|
|
19
|
+
two models or two periods, ragged or non-finite arrays, NaN/inf, duplicate columns (including labels that only
|
|
20
|
+
collide after `str()` coercion), stray metadata columns misread as models, boolean flag columns, mismatched
|
|
21
|
+
weight/label lengths, and loss magnitudes beyond any real per-period metric (`>1e100`, well below where float64
|
|
22
|
+
arithmetic starts to silently overflow). `.save()`/`.load()` round-trip through JSON with the exact same
|
|
23
|
+
validation as building a panel fresh — a saved file is not a shortcut around any of the above.
|
|
24
|
+
- **`identified()` / `mcs_size()`** — the Model Confidence Set via `arch.bootstrap.MCS` (`bootstrap='circular'`
|
|
25
|
+
explicitly, since `arch`'s own default is `'stationary'`), column order and dict insertion order independent by
|
|
26
|
+
construction. Refuses non-uniform weights outright rather than silently computing an unweighted MCS while the
|
|
27
|
+
reported champion is weighted.
|
|
28
|
+
- **`resolution_report()`, `minimum_detectable_edge()`, `mcb_bound()`, `significance_boundary()`,
|
|
29
|
+
`selection_regret()`** — R1 (is the observed edge against the closest rival actually resolvable at this sample
|
|
30
|
+
size, tested at the correct significance boundary — not the stricter minimum-detectable-edge, a common
|
|
31
|
+
conflation that silently halves the claimed statistical power) and R2 (is the champion point-identified before
|
|
32
|
+
any fragility-adjacent read is answered). `mcb_bound` corrects for having picked the closest rival post-hoc out
|
|
33
|
+
of several candidates (Bonferroni on the critical value). `selection_regret` is a deterministic,
|
|
34
|
+
leave-one-period-out empirical cost of the picking rule.
|
|
35
|
+
- **`decision_breakdown()` / `breakdown_number()`** carried over from the original k\* design (below), now
|
|
36
|
+
wired into `prop22_certifies()` / `certified_tied_subset()` — a zero-simulation lower bound on the tied set,
|
|
37
|
+
derived by inverting Proposition 2.2's significance bound directly (no bootstrap at all), certifying
|
|
38
|
+
non-significance only, never significance.
|
|
39
|
+
- **`concentration_share()` / `pivot_agreement()`** — replacements for the original `concentration` field
|
|
40
|
+
(rejected after measurement: near-zero power exactly where fragility matters most) and an unshipped
|
|
41
|
+
`pivot_sharpness` candidate (rejected for a structural degeneracy at near-ties). Both were accepted only after
|
|
42
|
+
matched-false-positive-rate testing against real shock configurations, not an AUC comparison.
|
|
43
|
+
- **`compare(previous, current)`** — the most-requested missing feature from practitioner review: did the pooled
|
|
44
|
+
champion change since the last run, and is that signal or noise? Identifies genuinely new periods by label (not
|
|
45
|
+
position), refuses to guess when two panels share no periods at all, and reports whether the old champion
|
|
46
|
+
actually left the Model Confidence Set (a real separation) versus still being statistically tied. `.act` is the
|
|
47
|
+
single boolean a CI/pipeline promotion step should gate on.
|
|
48
|
+
- **`report(panel)`** — the one-screen summary (VERDICT / LEADERBOARD / RESOLUTION / PIVOT) tying all of the above
|
|
49
|
+
together, with R1/R2 refusal applied consistently so no two sections of the same report can contradict each
|
|
50
|
+
other.
|
|
51
|
+
- A CLI (`python -m selection_fragility compare <previous.json> <current.json>`) for gating a promotion step
|
|
52
|
+
without writing Python.
|
|
53
|
+
|
|
54
|
+
### Fixed after implementation, before shipping
|
|
55
|
+
|
|
56
|
+
Extensive independent adversarial review (multiple review rounds, several distinct lenses — statistical
|
|
57
|
+
correctness, reproducibility, hostile/malformed input, numerical extremes, mutation/aliasing, cross-fix
|
|
58
|
+
interaction, statistical calibration) found and fixed real defects across the surface above, including: a churn
|
|
59
|
+
statistic that was measuring close to the opposite of its own definition; report sections that could contradict
|
|
60
|
+
each other on the same undetermined decision; `LossPanel` silently aliasing (not copying) caller-owned arrays, so
|
|
61
|
+
mutating your own data after building a panel could silently change the panel; `LossPanel.load()` bypassing the
|
|
62
|
+
same validation `.from_losses()` enforces (accepting negative or all-zero weights, mismatched label lengths, and
|
|
63
|
+
non-standard JSON `NaN`/`Infinity` tokens); several statistics whose tie-breaking depended on Python dict
|
|
64
|
+
insertion order rather than model identity; and the resolution report's headline "resolved" field claiming a power
|
|
65
|
+
target it did not actually deliver (comparing the observed edge against the wrong critical value delivered roughly
|
|
66
|
+
50% power at the stated minimum detectable edge, not the intended 80%). None of these affected the retired
|
|
67
|
+
`fragility()`/`model_confidence_set()` surface below, which predates this redesign and was not part of it. See the
|
|
68
|
+
test suite for the specific regression each fix locks in.
|
|
69
|
+
|
|
70
|
+
### Fixed in a later, wider round (personal line-by-line review of every source file, not delegated)
|
|
71
|
+
|
|
72
|
+
Several more rounds of fresh adversarial review, then a personal file-by-file read of the entire package with no
|
|
73
|
+
method commitment ("never assume anything is fine"), found real defects that had survived every prior round,
|
|
74
|
+
including in files reviewed many times before:
|
|
75
|
+
|
|
76
|
+
- **`resolution.py`**: the SE estimator used `ddof=0` (population variance) against its own documented intent of
|
|
77
|
+
"sample variance," understating SE worst at small T; `minimum_detectable_edge`/`significance_boundary`/
|
|
78
|
+
`mcb_bound` used a normal (z) critical value when SE was estimated from the same small sample — textbook grounds
|
|
79
|
+
for Student-t (df=T−1) — silently inflating confidence in the `resolved` verdict exactly at small T, the regime
|
|
80
|
+
the tool is meant to serve best; `mcb_bound`'s docstring/`report()` text/README all said "the champion may be up
|
|
81
|
+
to X% *worse*" when the formula computes an upper bound on how much *better* the champion's edge could be —
|
|
82
|
+
math unchanged (already coverage-tested), wording corrected; no T≥2 guard (silent NaN on a 1-period call) and no
|
|
83
|
+
weight-magnitude cap (silent overflow) on the raw-array entry points.
|
|
84
|
+
- **`identify.py`**: the exact-tie-breaking jitter's scale was computed panel-wide, so one model with a
|
|
85
|
+
legitimately large-but-valid magnitude (a plausible mixed-units mistake — raw-dollar loss alongside a normalized
|
|
86
|
+
metric) silently corrupted the tie-break for two *unrelated*, decisively-separated small-scale models sharing the
|
|
87
|
+
same panel.
|
|
88
|
+
- **`mcs.py`** (the legacy, independently-hand-rolled MCS implementation): no magnitude cap at all — an
|
|
89
|
+
astronomically worse model was silently crowned the *sole* MCS survivor, the opposite of correct, from only a
|
|
90
|
+
`RuntimeWarning`.
|
|
91
|
+
- **`panel.py`**: `from_forecasts()` let a model missing a forecast for *some* (not all) rows within a period-group
|
|
92
|
+
silently average over only the rows it did have — comparing models on different, non-comparable denominators
|
|
93
|
+
with no warning; a `(T, 0)`-shaped ndarray crashed with a bare `StopIteration` instead of the clear `ValueError`
|
|
94
|
+
every sibling zero-model path already raises.
|
|
95
|
+
- **`report.py`**: two genuinely distinct sub-monthly responsible periods could both truncate to the same `%Y-%m`
|
|
96
|
+
label, printing e.g. `(2020-04, 2020-04)` and reading as a duplicate. Labels are now disambiguated (day-level,
|
|
97
|
+
or an index suffix as a last resort) only within a report whose own responsible-period set actually collides —
|
|
98
|
+
a report with no collision looks exactly as before.
|
|
99
|
+
- **`__main__.py`**: every CLI error path (bad file, malformed panel, mismatched models) exited with the same code
|
|
100
|
+
(1) as `--exit-code`'s intentional "block the promotion" signal — indistinguishable to a calling CI pipeline.
|
|
101
|
+
Errors now exit 2.
|
|
102
|
+
- **`__init__.py`**: the package's own top-level docstring — the first thing `help()` shows — no longer opens with
|
|
103
|
+
an internal recovery note; it now states what the package does and how to start, matching every other public
|
|
104
|
+
docstring's convention.
|
|
105
|
+
|
|
106
|
+
### Fixed in a third round (round-2 "does it deliver on its promises" review, 2026-08-26)
|
|
107
|
+
|
|
108
|
+
The prior round's own CHANGELOG entries above were re-verified against the shipped source rather than trusted, and
|
|
109
|
+
three of them were found to describe an intended fix that had never actually been applied — the same defect class
|
|
110
|
+
this round exists to catch, recurring inside its own changelog:
|
|
111
|
+
|
|
112
|
+
- **`__main__.py`**: the "errors now exit 2" claim above was false — no `try`/`except` existed anywhere in the CLI
|
|
113
|
+
dispatch, so a missing/malformed file surfaced as a raw, unhandled Python traceback at exit 1, indistinguishable
|
|
114
|
+
from an actual internal bug. Now genuinely wraps `LossPanel.load()`/`compare()` in a try/except scoped to the
|
|
115
|
+
user-input-class exceptions those functions document (a missing file, corrupt/non-`LossPanel` JSON, a real data
|
|
116
|
+
problem such as a mismatched model set) and exits 2 with a one-line message; anything else still propagates
|
|
117
|
+
uncaught, so a genuine programming error is never mistaken for bad input.
|
|
118
|
+
- **`report.py`**: the same-month disambiguation claim above was also false — `_fmt_label` was still a bare
|
|
119
|
+
`strftime("%Y-%m")` with no collision handling anywhere in the file; `k*=2 of 5 periods (2020-04, 2020-04)` was
|
|
120
|
+
reproduced verbatim. `_disambiguate_labels()` now does this for real.
|
|
121
|
+
- **`panel.py`**: `from_forecasts()`'s partial-row-NaN guard above was also never shipped — a model with 7 valid
|
|
122
|
+
rows and 3 NaN rows in a period where a sibling model had all 10 built a "clean" panel with no warning, since
|
|
123
|
+
`groupby().mean()` skips NaN by default and the existing NaN/inf check only ever sees the already-averaged
|
|
124
|
+
(NaN-free) result. Now compares each model's non-null row count against the period's true total row count and
|
|
125
|
+
warns, naming the model and the under-covered period(s), before the caller ever gets an "already validated"
|
|
126
|
+
panel with a silently different effective sample size per model.
|
|
127
|
+
|
|
128
|
+
### Fixed in round 3 (5-persona adoption stress test, 2026-08-26)
|
|
129
|
+
|
|
130
|
+
Five isolated reviewers, each required to actually install and run the package against a scenario realistic to
|
|
131
|
+
their role (an M-competition organizer, an applied forecasting practitioner, a strict IJF referee, an
|
|
132
|
+
AMIP-literature-aware researcher, a government/policy adopter) — find only, then a second pass fixed everything
|
|
133
|
+
actionable:
|
|
134
|
+
|
|
135
|
+
- **`EVALUATION_CARD.md`** claimed results were "verified with `python code/gates/frozen_manifest.py --check`"
|
|
136
|
+
against SHA-256-pinned inputs in `FROZEN_INPUTS.md` — neither file was ever built for this package. Corrected to
|
|
137
|
+
honestly describe what verification actually exists (the test suite, the `arch` cross-check, the CI matrix)
|
|
138
|
+
instead of fabricating matching files to make the old claim true. A separate unqualified reference to
|
|
139
|
+
`.zenodo.json`, read as though a Zenodo deposit already existed, was corrected the same way.
|
|
140
|
+
- The package's **own docs** (README, CITATION.cff, EVALUATION_CARD.md) had zero engagement with the closest
|
|
141
|
+
prior art (Broderick, Giordano & Meager's AMIP) even though the accompanying paper already handled this
|
|
142
|
+
correctly — a referee or adopter evaluating the standalone package would never see the paper. Ported the
|
|
143
|
+
already-vetted positioning (k\* is exact on AMIP's linear-functional case; the non-reducible contribution is the
|
|
144
|
+
calibrated null plus transport to dependent-data model selection) into the package's own citable docs.
|
|
145
|
+
- **`resolution_report()`** didn't accept a `LossPanel` the way `report()`/`compare()` do — passing one gave a
|
|
146
|
+
confusing internal `TypeError` from inside `_validate_losses` instead of a clean top-level message. Fixed for
|
|
147
|
+
API consistency across all three primary entry points.
|
|
148
|
+
- Two real, cheap documentation gaps closed: the README quickstart never demonstrated the `labels=` parameter
|
|
149
|
+
(PIVOT printed meaningless period indices instead of real dates — the feature worked, it was just undocumented),
|
|
150
|
+
and never showed how to reshape long/tidy-format data into the Nixtla-style wide shape `from_forecasts()`
|
|
151
|
+
expects.
|
|
152
|
+
|
|
153
|
+
### Fixed in round 4 (automated fuzzing + zero-context walkthrough, 2026-08-26)
|
|
154
|
+
|
|
155
|
+
Property-based fuzzing (`hypothesis`, 11 properties, 40–200 examples each) found zero crashes anywhere across the
|
|
156
|
+
full public API on valid input, and one real bug:
|
|
157
|
+
|
|
158
|
+
- **`fragility.py`**: `decision_breakdown`'s k\* was not invariant to positive-scalar rescaling of the weight
|
|
159
|
+
vector, contrary to its documented guarantee ("only relative weights matter"). Root cause:
|
|
160
|
+
`breakdown_number`'s degenerate-margin floor compared a weight-scaled running margin against a threshold
|
|
161
|
+
computed only from the raw loss values, never rescaled by `w` — scaling `w` down far enough could trip the
|
|
162
|
+
floor one period early, understating k\* by 1 (the safer failure direction). Fixed by making the floor
|
|
163
|
+
weight-aware; byte-identical to prior behavior at `w=1` (every pre-existing test).
|
|
164
|
+
- The fuzzer's own panel-generator could occasionally draw a column that legitimately triggers `panel.py`'s
|
|
165
|
+
deliberate metadata-column guard — a fuzzer/guard collision, not a product bug — filtered via `hypothesis`'s
|
|
166
|
+
`assume()`.
|
|
167
|
+
- Verified release-readiness mechanically for the first time: builds cleanly (sdist+wheel), `twine check` passes,
|
|
168
|
+
installs and imports correctly from a fresh venv, zero known CVEs in dependencies (`pip-audit`).
|
|
169
|
+
|
|
170
|
+
### Fixed in round 5 (security, reproducibility, extensibility, competition-integrity, R-interop, 2026-08-26)
|
|
171
|
+
|
|
172
|
+
- **`mcs.py`**: `model_confidence_set`'s bootstrap count `B` had no upper bound — `B=1,000,000` ran unbounded for
|
|
173
|
+
25+ seconds. Fixed with a 100,000 ceiling and a clear error.
|
|
174
|
+
- **`pyproject.toml`**: declared dependency floors (`numpy>=1.20`/`pandas>=1.3`/`scipy>=1.7`) predated the
|
|
175
|
+
package's own `>=3.10` Python floor and didn't even build on 3.13. Bumped to the real floors
|
|
176
|
+
(`numpy>=2.1.0`/`pandas>=2.2.3`/`scipy>=1.14.1`), verified by installing at exactly those versions and running
|
|
177
|
+
the full suite unchanged. Added `requirements-lock.txt` pinning the exact currently-tested versions.
|
|
178
|
+
- **`__init__.py`**: `from .X import X` re-export pattern silently shadows **four** submodules (`mcs`,
|
|
179
|
+
`fragility`, `compare`, `report`) at their own dotted path — `import selection_fragility.mcs as m;
|
|
180
|
+
m._block_idx` silently returned the wrong object with a confusing `AttributeError`. Fixed with a new,
|
|
181
|
+
documented `_internals.py` giving contributors a stable, non-shadowed path to real submodules without touching
|
|
182
|
+
the existing public API.
|
|
183
|
+
- Two real, confirmed (not hypothetical) competition-integrity risks documented, not fixed as code (they're
|
|
184
|
+
correct arithmetic, not bugs): a model with a genuinely worse mean loss can survive MCS elimination up to 100%
|
|
185
|
+
of the time simply by carrying high per-period noise; a participant-controlled weight vector can flip both the
|
|
186
|
+
pooled winner and k\* entirely. See README's "Adversarial use / competition settings" section.
|
|
187
|
+
- The package's MCS defaults (`alpha=0.10`, `B=2000`, fixed `block=3`) diverge from R's `MCS::MCSprocedure`
|
|
188
|
+
defaults on all three tunable parameters — previously undocumented. Added a "Using from R" README section with
|
|
189
|
+
a `reticulate` example and an explicit default-mapping table.
|
|
190
|
+
|
|
191
|
+
### Fixed in round 6 (8-persona forecasting-research + cross-domain review, 2026-08-26)
|
|
192
|
+
|
|
193
|
+
The headline finding of this round, independently reproduced by 6 of 8 reviewers: **`pooled_winner()`** was not
|
|
194
|
+
invariant to positive-scalar rescaling of the weight vector on a near/exact-tied panel — the same
|
|
195
|
+
float64-residue-near-a-tie defect class already fixed twice in round 4, in a third independent location. Fixed
|
|
196
|
+
with the same relative-floor discipline; verified 0/200,000 mismatches on the exact repro (was 101/200,000).
|
|
197
|
+
|
|
198
|
+
- **`decision_breakdown`/`pooled_winner`/`winner_stability`/`fragility()`** (the "raw-array tier") gave confusing
|
|
199
|
+
internal errors when passed a `LossPanel` instead of a dict — the same class of gap round 3 found for
|
|
200
|
+
`resolution_report()`. Fixed the same way, via the shared `_unwrap_panel()` helper.
|
|
201
|
+
- `report()`'s internal MCS path (`identify.py`, wraps `arch`, default `reps=500`) and calling
|
|
202
|
+
`model_confidence_set()` directly (`mcs.py`'s own implementation, default `B=2000`) can give contradicting
|
|
203
|
+
identification verdicts on the identical panel for highly-correlated candidates (0.998+ correlation). Confirmed
|
|
204
|
+
this is a deliberate cross-check independence, not an accidental mismatch — fixed via explicit
|
|
205
|
+
cross-referencing warnings in both docstrings rather than forcing the defaults to match.
|
|
206
|
+
- **The most statistically consequential finding of the whole project**: the package's own guidance to increase
|
|
207
|
+
MCS block length for more persistent/dependent data is actively counterproductive under realistic serial
|
|
208
|
+
dependence. Real simulation (K=5, T=30, alpha=0.10, rho=0.7): P(true best wrongly excluded) is 11.4% at the
|
|
209
|
+
default `block=3`, already above nominal, and 16.4% at `block=12` — the larger block the docstring recommended.
|
|
210
|
+
The counterproductive advice in `mcs.py`/README/EVALUATION_CARD.md was replaced with an honest disclosure
|
|
211
|
+
citing the real numbers, not guidance that doesn't hold up.
|
|
212
|
+
- One sentence added distinguishing k\* (descriptive, no multiple-comparisons correction needed) from
|
|
213
|
+
`mcb_bound` (a real corrected confidence bound for the same post-hoc rival-selection structure); an inline
|
|
214
|
+
hint added after VERDICT whenever a result is unresolved, so a reader can't stop at "VERDICT: identified"
|
|
215
|
+
before reaching the power caveat.
|
|
216
|
+
- Cross-domain findings disclosed, not code bugs: MCS becomes entirely unavailable under realistic
|
|
217
|
+
traffic-weighted A/B panels (`arch` has no native non-uniform-weight support — the tool correctly refuses
|
|
218
|
+
rather than computing something silently wrong); no native multi-metric reconciliation for recommender-system
|
|
219
|
+
evaluation; Walsh's clinical Fragility Index relationship independently re-verified as related in philosophy
|
|
220
|
+
but not a strict mathematical generalization.
|
|
221
|
+
|
|
222
|
+
### Fixed in round 7 (integrity audit + realistic long-tenure domain deep-dives, 2026-08-27)
|
|
223
|
+
|
|
224
|
+
Explicitly requested to answer a direct concern about whether the test suite's "all green" was genuinely real.
|
|
225
|
+
A fresh clone from GitHub into an independent venv reproduced the exact claimed test result and independently
|
|
226
|
+
re-executed 5 major fixes against that clean install. A hollow-test hunter deliberately reverted two major fixes
|
|
227
|
+
and watched tests fail across 8 different files, proving they're load-bearing; found exactly 1 genuinely hollow
|
|
228
|
+
test (fixed) and one `xfail`-strictness gap (closed, `xfail_strict = true` now set). Mutation testing (8
|
|
229
|
+
controlled, fully-reverted mutations) found 6 of 8 caught immediately by specific tests, and pinpointed 2 real,
|
|
230
|
+
previously-unprotected regression gaps — `mcs.py`'s own overflow guard and `panel.py`'s dict-input metadata-column
|
|
231
|
+
guard could both have been silently reintroduced-broken with zero test failures; both now have real regression
|
|
232
|
+
tests.
|
|
233
|
+
|
|
234
|
+
- **`resolution_report()`** crashed raw and uncaught on non-uniform weights (only visible with realistic
|
|
235
|
+
enrollment-ramp weighted data, not a smaller uniform example) — fixed internally, preserving real computed
|
|
236
|
+
values rather than falling back to an external NaN pattern.
|
|
237
|
+
- **`breakdown_number`**'s "responsible periods" diagnostic finds periods propping up the *current winner's*
|
|
238
|
+
margin specifically — a disruption that temporarily favors the challenger is nearly invisible in that output
|
|
239
|
+
even though something real happened. Not a bug; now stated explicitly in the docstring.
|
|
240
|
+
- Domain deep-dives (clinical trials, A/B testing, government/policy at up to 50-year horizons, LLM benchmark
|
|
241
|
+
leaderboard evaluation) found no other bugs.
|
|
242
|
+
|
|
243
|
+
### Fixed in round 7.5 (first real cross-platform CI run, 2026-08-27)
|
|
244
|
+
|
|
245
|
+
A CI matrix (Linux/Windows/macOS × Python 3.10–3.13) had existed since round 2 but had never actually triggered
|
|
246
|
+
on GitHub — it lived at the wrong path (`.github/workflows/` is only discovered at the repository root, not
|
|
247
|
+
inside this package's own subdirectory). Moved to the real root; the first real run found genuine
|
|
248
|
+
cross-platform bugs no amount of single-machine testing could have caught:
|
|
249
|
+
|
|
250
|
+
- The packaging test suite imported the tomllib standard-library module unconditionally, which failed outright on
|
|
251
|
+
Python 3.10 (that module only entered the standard library in 3.11). Added a fallback import and the matching
|
|
252
|
+
conditional test dependency — a CI-environment-level fact verified by the cross-platform matrix itself running
|
|
253
|
+
green, not a single unit test.
|
|
254
|
+
- Reading README.md raised a decode error on Windows specifically: any file read without an explicit encoding
|
|
255
|
+
uses the OS default (`cp1252` on Windows, not UTF-8), and README.md contains a real UTF-8 character outside
|
|
256
|
+
that range. Swept the whole tree for unencoded `open()`/`read_text()` calls — 8 sites fixed, including one in
|
|
257
|
+
real library code (`panel.py`), not just tests — again verified by the cross-platform CI matrix itself, not a
|
|
258
|
+
single named function.
|
|
259
|
+
- A malformed path (a hardcoded `.rsplit("/tests/", 1)` assuming forward-slash paths, silently wrong on
|
|
260
|
+
Windows' backslash separators) fixed with `pathlib` in two files.
|
|
261
|
+
- A CLI stderr-matching test compared a full Windows path string against differently-escaped subprocess output —
|
|
262
|
+
fixed to assert on the filename only.
|
|
263
|
+
- A performance-regression guard's 20-second ceiling (calibrated on one fast local machine) was too tight for
|
|
264
|
+
GitHub's shared, slower CI runners — hit on both Linux (20.5s) and Windows (24.2s). Raised to 60 seconds, still
|
|
265
|
+
tight enough to catch a genuine algorithmic regression by a wide margin.
|
|
266
|
+
|
|
267
|
+
All 12 jobs (3 OSes × 4 Python versions) now pass, confirmed on a real run.
|
|
268
|
+
|
|
269
|
+
### Fixed in round 8 (10-agent code-validation + 6-domain stress test, 2026-08-27)
|
|
270
|
+
|
|
271
|
+
The most valuable finding of the whole project came from a holistic, line-by-line fresh-eyes read of the current
|
|
272
|
+
codebase (distinct from every prior theme-targeted review): **six public, prominently-exported functions**
|
|
273
|
+
(`minimum_detectable_edge`, `significance_boundary`, `mcb_bound`, `selection_regret`, `concentration_share`,
|
|
274
|
+
`pivot_agreement`) plus `certified_tied_subset` had never received the `LossPanel`-acceptance fix their sibling
|
|
275
|
+
functions got in rounds 3 and 6 — a blind spot no theme-targeted review could structurally catch, since each
|
|
276
|
+
prior round's fix was scoped to whichever specific function that round's reviewer happened to test. Fixed via
|
|
277
|
+
the existing `_unwrap_panel()` pattern, applied consistently this time. Also corrected a false claim made in a
|
|
278
|
+
round-7 fix comment (that `compare()` already degraded gracefully on non-uniform weights) that this same read
|
|
279
|
+
caught.
|
|
280
|
+
|
|
281
|
+
- **`panel.py`**: `LossPanel.save()`/`.load()` was not atomic under concurrent access — a real stress test
|
|
282
|
+
reproduced ~300 corrupted reads per 800 attempts. Fixed with the standard write-to-temp-then-`os.replace()`
|
|
283
|
+
pattern (atomic on both POSIX and Windows). Multiprocessing and threading were both independently confirmed
|
|
284
|
+
safe via real adversarial stress tests otherwise.
|
|
285
|
+
- **`compare.py`**: crashed raw and uncaught on non-uniform weights. Fixed to degrade gracefully like its
|
|
286
|
+
documented peers, with a deliberately conservative failure mode — on an MCS computation error it reports
|
|
287
|
+
"undetermined" rather than a default, and forces its action flag to `False`, so an automated pipeline can
|
|
288
|
+
never mistake "couldn't compute" for "genuinely safe to act."
|
|
289
|
+
- Four functions that legitimately return `None` per their own accurate docstrings (`condorcet_winner`,
|
|
290
|
+
`prop22_certifies`, `concentration_share`, `pivot_agreement`) had signatures that didn't say so — fixed.
|
|
291
|
+
- An exhaustive independent mathematical re-derivation of every formula in the package against its cited
|
|
292
|
+
literature source (Hansen-Lunde-Nason 2011, Kish 1965, Politis-Romano 1992, Bonferroni-corrected MCB bounds)
|
|
293
|
+
found zero discrepancies.
|
|
294
|
+
- Six new-domain deep-dives (epidemiology, energy grid, insurance, manufacturing, marketing, sports analytics)
|
|
295
|
+
found zero code bugs and three genuine, now-documented non-bug findings: a pooled `report()` can mask a large
|
|
296
|
+
regime-driven reversal (see README's domain-specific pitfalls section); a single catastrophic observation can
|
|
297
|
+
dominate the whole comparison in a way the leaderboard view alone won't reveal; right-censored data is silently
|
|
298
|
+
accepted with no warning. Epidemiology separately found that evaluating against premature vs. settled ground
|
|
299
|
+
truth can flip the winner in any backfill-revised domain — not a tool defect, permanently regression-locked.
|
|
300
|
+
- A formatting defect (squished single-line docstrings in 4 files) and an efficiency issue (`fragility()` ran an
|
|
301
|
+
expensive bootstrap before checking a cheap condition that would discard the result anyway) also fixed.
|
|
302
|
+
|
|
303
|
+
430 passed, 1 xfailed at the close of round 8 (up from 259 at the close of the initial 6-agent review) — see the
|
|
304
|
+
test suite itself for the specific regression each entry above locks in.
|
|
305
|
+
|
|
306
|
+
### Fixed in round 9 (Phase-2 holistic review of the paper repo, 2026-08-27/28)
|
|
307
|
+
|
|
308
|
+
- **`fragility.py`**: `concentration`/`conc_by_opp` divided a near-zero-but-technically-positive weighted margin
|
|
309
|
+
`M` through to a huge, meaningless ratio instead of reporting the pair as an effective tie — the same
|
|
310
|
+
degenerate-margin floor `breakdown_number()` already applies (`_MARGIN_REL_FLOOR`) had never been extended to
|
|
311
|
+
these two fields. First fix reused the pre-existing `degenerate` flag's whole-panel scale (`_loss_scale(L)`);
|
|
312
|
+
an independent review found a concrete counterexample where an unrelated, never-winning, large-magnitude model
|
|
313
|
+
elsewhere in the panel inflates that whole-panel scale enough to falsely mark a genuinely decisive margin as
|
|
314
|
+
degenerate — inconsistent with `breakdown_number()`'s own per-pair convention, the function that actually
|
|
315
|
+
computes `k_star`. Corrected to a per-pair scale (`_pair_scale()`, matching `breakdown_number()`'s own formula
|
|
316
|
+
exactly), recomputed independently for each opponent in `conc_by_opp` so one wide-magnitude co-binding
|
|
317
|
+
opponent can't contaminate another's degeneracy read. The pre-existing `degenerate` field was updated to the
|
|
318
|
+
same per-pair scale so it and `concentration` can no longer disagree about whether the same margin is
|
|
319
|
+
degenerate (their documented mutual-consistency contract). A second, fresh-context review independently
|
|
320
|
+
reproduced both the original bug (negative control) and the fix, and confirmed the corrected formula matches
|
|
321
|
+
`breakdown_number()`'s character-for-character.
|
|
322
|
+
|
|
323
|
+
464 passed, 1 xfailed at the close of round 9.
|
|
324
|
+
|
|
325
|
+
### Fixed in round 10 (Phase-2 holistic review of the paper repo, 2026-08-28)
|
|
326
|
+
|
|
327
|
+
- **`fragility.py`**: `breakdown_number()`'s greedy removal order, `np.argsort(c)[::-1]`, does not guarantee
|
|
328
|
+
stability for equal margin contributions (default `kind='quicksort'`), and the trailing `[::-1]` reverses the
|
|
329
|
+
whole result -- so two periods with the exact same contribution could be removed in an order that is an
|
|
330
|
+
accidental byproduct of the sort algorithm rather than a stated convention. k* (the removal count) is
|
|
331
|
+
unaffected -- deleting either member of a tied pair removes the same amount -- but which periods land in
|
|
332
|
+
`removed_period_indices` is not, when the greedy loop stops partway through a tied group. Fixed via
|
|
333
|
+
`np.argsort(-c, kind="stable")`: identical to the old code whenever no two contributions tie exactly, and for
|
|
334
|
+
a genuine tie, deterministically prefers the earlier period (ascending original index). A fresh-context review
|
|
335
|
+
independently reproduced the old-vs-new behavior on a constructed tie-at-the-boundary example, fuzz-tested
|
|
336
|
+
200,000 all-distinct arrays (0 mismatches between the old and new sort) and 50,000 forced-tie arrays (0
|
|
337
|
+
tie-break violations), and confirmed "earliest period" is a more principled convention than a sort-algorithm
|
|
338
|
+
accident given periods (unlike model names) already carry a real chronological order.
|
|
339
|
+
|
|
340
|
+
465 passed, 1 xfailed at the close of round 10.
|
|
341
|
+
|
|
342
|
+
### Fixed in rounds 11-13 (11-angle deep review + two further 10-agent audits, 2026-08-29 -- 2026-09-05)
|
|
343
|
+
|
|
344
|
+
**CORRECTED 2026-09-07** (round-4 8-lens PyPI-preflight audit): this file previously jumped straight
|
|
345
|
+
from round 10 to nothing, even though substantial work happened in between (test files carrying
|
|
346
|
+
mtimes and in-file comments through 2026-09-05) -- EVALUATION_CARD.md's own test-count claim ("379")
|
|
347
|
+
and this file's own last recorded count (465) had both drifted from the real, current figure with no
|
|
348
|
+
record of what moved the number. Filling the gap at commit-level detail (full line-item history for
|
|
349
|
+
these rounds lives in the paper monorepo's own commit messages and session records, not duplicated
|
|
350
|
+
here) rather than leaving it unrecorded:
|
|
351
|
+
|
|
352
|
+
- **`7be69eb`** (2026-08-29) — closed 6 code/data issues found by an 11-angle deep review, touching
|
|
353
|
+
`test_stage3_kstar_prop22.py` and `test_stage4_pivot.py`.
|
|
354
|
+
- **`2d52972`** (2026-09-02) — a 10-agent code review across the paper and this tool together; added
|
|
355
|
+
`test_stage5_compare.py`/`test_stage6_report.py` coverage among 13 files changed.
|
|
356
|
+
- **`54669ea`** (2026-09-02, round-2 phase 1) — fixed six confirmed tool bugs, one CRITICAL
|
|
357
|
+
(`pooled_winner()`'s whole-panel-vs-per-pair tie-tolerance defect, see `code/instrument/`'s own
|
|
358
|
+
changelog in the paper repo for the full account); added `test_security_adversarial.py` and more
|
|
359
|
+
of `test_stage3_kstar_prop22.py`.
|
|
360
|
+
- **`8a1b0c5`** (2026-09-05, round 3) — a 10-agent audit of round 2's own work, closing findings
|
|
361
|
+
including a `d_max_over_median` mixed-sign fix in `surprise_concentration()` and a hollow-test fix
|
|
362
|
+
in `test_stage3_kstar_prop22.py`'s tie-strength monotonicity check.
|
|
363
|
+
|
|
364
|
+
### Fixed in round 14 (8-lens external pre-PyPI-publish audit, 2026-09-07)
|
|
365
|
+
|
|
366
|
+
The first review round to simulate a stranger about to `pip install` this package cold, rather than
|
|
367
|
+
auditing it as an insider. Found and fixed real defects none of rounds 1-13 had surfaced, despite
|
|
368
|
+
their combined depth -- being correct internally and being safe to hand to an external user turned
|
|
369
|
+
out to be different properties:
|
|
370
|
+
|
|
371
|
+
- **`panel.py`, `from_forecasts()` (BLOCKING)**: the `models=None` auto-inference branch silently
|
|
372
|
+
EXCLUDED any genuinely numeric model column stored as object/string dtype (e.g. from a CSV read
|
|
373
|
+
without explicit dtype control) -- no warning, no error. Reproduced: the actual best model,
|
|
374
|
+
stored as numeric strings, vanished entirely and `report()` printed a confident verdict over the
|
|
375
|
+
remaining models as if nothing were missing. Fixed via scoped `pd.to_numeric` coercion on
|
|
376
|
+
object-dtype columns (explicitly excluding datetime64, which "succeeds" under naive coercion by
|
|
377
|
+
reinterpreting itself as nanosecond-epoch integers).
|
|
378
|
+
- **`panel.py`, `from_forecasts()` (HIGH)**: `group` was validated as a column but never used as
|
|
379
|
+
part of the aggregation key, so a duplicated `(group, period)` row (a re-appended CV fold, a
|
|
380
|
+
fan-out join) was silently pooled into that period's mean -- reproduced flipping a verdict with
|
|
381
|
+
zero warning. Fixed with a `ValueError` when `(period, group)` isn't unique, scoped to
|
|
382
|
+
`group`-not-`None` only (a broader version broke a legitimate, intentionally-designed test proving
|
|
383
|
+
multiple rows per period with no group column is a real, supported shape).
|
|
384
|
+
- **LossPanel-acceptance gap (HIGH)**: following the README's own Stage 0 -> Stage 1 example
|
|
385
|
+
literally raised a raw internal `TypeError`. `identified()`/`mcs_size()` (identify.py),
|
|
386
|
+
`per_period_winner`/`condorcet_winner`/`condorcet_status`/`surprise_concentration` (fragility.py),
|
|
387
|
+
and `model_confidence_set` (mcs.py) all now accept a `LossPanel` directly via the same shared
|
|
388
|
+
`_unwrap_panel` helper every other panel-accepting function already used -- a completeness sweep
|
|
389
|
+
confirmed every applicable public function now accepts one, with the one correct, documented
|
|
390
|
+
exception (`mcs()`, the raw T x K matrix engine).
|
|
391
|
+
- **`report.py` (HIGH)**: `report()` called `identified()`, `mcs_size()`, AND `_run_mcs()`
|
|
392
|
+
separately -- three independent calls each re-running the same `arch` bootstrap MCS computation
|
|
393
|
+
from scratch. Deduped to one call (~3x measured speedup at K=200/T=50: 9.3s -> 3.1s), verified as
|
|
394
|
+
a pure speedup with no behavior change. Added a K-scaling warning to `LossPanel` construction
|
|
395
|
+
(K>100) since no guidance existed anywhere despite `mcs()` having explicit ceilings on other
|
|
396
|
+
parameters.
|
|
397
|
+
- **Documentation**: `surprise_concentration()` and the `python -m selection_fragility compare` CLI
|
|
398
|
+
were both real, working, exported/implemented features with zero README mentions -- documented,
|
|
399
|
+
every claim independently verified against real execution.
|
|
400
|
+
- **Packaging**: `MANIFEST.in` was missing `requirements-lock.txt` and `tests/*.py` (including
|
|
401
|
+
`conftest.py` -- a downloaded sdist's own test suite gave 140 fixture errors, not a clean pass);
|
|
402
|
+
the CI matrix referenced from EVALUATION_CARD.md only ever lived in the private monorepo, so the
|
|
403
|
+
standalone public repo would have shipped with zero registered CI -- a repo-root-adapted copy now
|
|
404
|
+
ships inside this package itself.
|
|
405
|
+
|
|
406
|
+
541 passed, 1 xfailed at the close of round 14.
|
|
407
|
+
|
|
408
|
+
### Fixed in round 15 (final pre-publish real `pip install` + clean-venv test run, 2026-09-07)
|
|
409
|
+
|
|
410
|
+
Round 14 audited the package's *contents*; this round actually did the thing a first user does --
|
|
411
|
+
built fresh sdist/wheel artifacts, installed each into a genuinely clean virtualenv (not the
|
|
412
|
+
long-lived dev environment every prior round ran tests in), and ran the real test suite against the
|
|
413
|
+
installed copy rather than the source tree. That surfaced one thing internal testing never would,
|
|
414
|
+
because the dev environment's own pinned `pandas==2.3.3` (see `requirements-lock.txt`) never
|
|
415
|
+
exercises it:
|
|
416
|
+
|
|
417
|
+
- **`panel.py`, `from_forecasts()` (BLOCKING, pandas 3.x)**: round 14's own numeric-string-column
|
|
418
|
+
fix (above) scoped its `pd.to_numeric` coercion to `is_object_dtype` columns only. pandas 3.0 --
|
|
419
|
+
the version any `pip install selection-fragility` resolves to today, since `pyproject.toml` pins
|
|
420
|
+
no upper bound -- made its dedicated string dtype the DEFAULT representation for string data,
|
|
421
|
+
including a plain `pd.Series(["1.5", "2.3"])`; such a column is no longer `is_object_dtype` at
|
|
422
|
+
all. Reproduced directly against a clean pandas-3.0.5 install: the round-14 fix's own worked
|
|
423
|
+
example (a numeric-string `nbeats` column, the best model) vanished from `model_cols` again,
|
|
424
|
+
silently, the identical failure mode round 14 closed for pandas 2.x. Fixed by also matching
|
|
425
|
+
`isinstance(dtype, pd.StringDtype)`, which correctly catches the new default on 3.x, an explicit
|
|
426
|
+
opt-in `dtype="string"` column on 2.x, and -- confirmed directly -- never a datetime64 column
|
|
427
|
+
(native or object-boxed), so it cannot reopen the datetime-swept-in-as-numeric bug the
|
|
428
|
+
object-only scoping was written to prevent in the first place. Verified against a genuinely clean
|
|
429
|
+
install on both pandas 2.3.3 and 3.0.5: 541/541 and 529/529 (12 tests skip cleanly in the minimal
|
|
430
|
+
sdist-only venv, each for the same reason -- the private golden-master data file those tests read
|
|
431
|
+
isn't shipped with the package or present outside the project's own dev environment, exactly as
|
|
432
|
+
intended) respectively, both from the built sdist, not the source tree. A second, independent
|
|
433
|
+
review then found the fix above still missed a third string-dtype shape (`pd.ArrowDtype`); widened
|
|
434
|
+
to `pd.api.types.is_string_dtype`, confirmed to cover all three shapes and still correctly exclude
|
|
435
|
+
datetime64 -- see the code comment for the full account.
|
|
436
|
+
- The one test whose own setup asserted `dtype == object` (`test_numeric_string_column_is_recovered
|
|
437
|
+
_as_a_model`) had the identical pandas-generation assumption baked into its precondition; loosened
|
|
438
|
+
to accept either representation of "a column of number-looking strings," matching the fix above.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite both the software and the accompanying paper."
|
|
3
|
+
title: "selection-fragility: the decision breakdown point (k*) and ranking-fragility diagnostics for forecast model selection"
|
|
4
|
+
abstract: >-
|
|
5
|
+
Decision-oriented diagnostics for forecast model selection: the Model Confidence Set
|
|
6
|
+
(which candidate models are statistically indistinguishable from the best), the decision
|
|
7
|
+
breakdown point k* (the fewest evaluation periods whose deletion flips the pooled winner),
|
|
8
|
+
a resolution report answering whether the observed edge is actually resolvable at the
|
|
9
|
+
available sample size (with a refusal discipline for reads the data cannot support), a
|
|
10
|
+
zero-simulation significance certificate for k*, run-over-run champion-change comparison,
|
|
11
|
+
and selection-as-election diagnostics including score, plurality and Condorcet winners,
|
|
12
|
+
genuine intransitive cycles versus pairwise ties, margin concentration, and a
|
|
13
|
+
block-bootstrap winner-stability null.
|
|
14
|
+
type: software
|
|
15
|
+
authors:
|
|
16
|
+
- family-names: Rai
|
|
17
|
+
given-names: Sudhanshu
|
|
18
|
+
orcid: "https://orcid.org/0009-0001-2447-0336"
|
|
19
|
+
affiliation: "Independent Researcher"
|
|
20
|
+
email: me@sudhanshurai.org
|
|
21
|
+
# CONFIRMED REGRESSION (personal full-file review, 2026-08-17): this file previously asserted
|
|
22
|
+
# `date-released: "2026-07-26"` for version 1.0.0, directly contradicted by this same repo's own
|
|
23
|
+
# CHANGELOG.md header ("## [1.0.0] -- unreleased") -- the release genuinely has not happened (bugs
|
|
24
|
+
# were still being found and fixed the day this comment was written). Same premature-fact risk the
|
|
25
|
+
# DOI comment below already correctly avoids; omit until the actual release date is known, add it
|
|
26
|
+
# then rather than guess or backdate.
|
|
27
|
+
# ADDED 2026-09-08: the release happened -- GitHub release v1.0.0 published, Zenodo deposit
|
|
28
|
+
# confirmed live at the DOI below (fetched directly, not assumed from the GitHub-Zenodo widget).
|
|
29
|
+
date-released: "2026-09-08"
|
|
30
|
+
version: 1.0.0
|
|
31
|
+
license: MIT
|
|
32
|
+
repository-code: "https://github.com/sudhanshurai360/selection-fragility"
|
|
33
|
+
keywords:
|
|
34
|
+
- forecasting
|
|
35
|
+
- model selection
|
|
36
|
+
- model confidence set
|
|
37
|
+
- robustness
|
|
38
|
+
- breakdown point
|
|
39
|
+
- computational social choice
|
|
40
|
+
- real-time data
|
|
41
|
+
- official statistics
|
|
42
|
+
- reproducibility
|
|
43
|
+
|
|
44
|
+
# ADDED 2026-09-08: the version DOI below was read directly from the live Zenodo record
|
|
45
|
+
# (zenodo.org/records/22652327), not copied from the GitHub-Zenodo integration widget or assumed.
|
|
46
|
+
# That record does not yet surface a separate "concept DOI" (the one that always resolves to the
|
|
47
|
+
# latest version) -- Zenodo generally only makes that distinction visible in the UI once a second
|
|
48
|
+
# version exists. This is deliberately the VERSION-specific DOI, paired with version: 1.0.0 above;
|
|
49
|
+
# revisit when a v1.1.0/v2.0.0 deposit exists and the concept DOI becomes unambiguous.
|
|
50
|
+
identifiers:
|
|
51
|
+
- type: doi
|
|
52
|
+
value: 10.5281/zenodo.22652327
|
|
53
|
+
description: "The version-specific DOI for v1.0.0, minted by Zenodo on GitHub release."
|
|
54
|
+
|
|
55
|
+
preferred-citation:
|
|
56
|
+
type: unpublished
|
|
57
|
+
title: >-
|
|
58
|
+
The Decision Breakdown Point: How Fragile Is "The Best Forecasting Model," and What Does
|
|
59
|
+
That Fragility Cost?
|
|
60
|
+
authors:
|
|
61
|
+
- family-names: Rai
|
|
62
|
+
given-names: Sudhanshu
|
|
63
|
+
orcid: "https://orcid.org/0009-0001-2447-0336"
|
|
64
|
+
affiliation: "Independent Researcher"
|
|
65
|
+
year: 2026
|
|
66
|
+
|
|
67
|
+
# Closest prior art -- see README.md's "Related work" section for the full, precise relationship
|
|
68
|
+
# (k* is exact on AMIP's linear-functional case; the non-reducible contribution is the calibrated
|
|
69
|
+
# null AMIP lacks, transported to a model-selection decision on dependent, vintaged data).
|
|
70
|
+
references:
|
|
71
|
+
- type: article
|
|
72
|
+
title: >-
|
|
73
|
+
An automatic finite-sample robustness metric: when can dropping a little data change
|
|
74
|
+
conclusions? Part I: definitions and experiments
|
|
75
|
+
authors:
|
|
76
|
+
- family-names: Giordano
|
|
77
|
+
given-names: Ryan
|
|
78
|
+
- family-names: Meager
|
|
79
|
+
given-names: Rachael
|
|
80
|
+
- family-names: Broderick
|
|
81
|
+
given-names: Tamara
|
|
82
|
+
journal: "Philosophical Transactions of the Royal Society A"
|
|
83
|
+
volume: "384"
|
|
84
|
+
issue: "2321"
|
|
85
|
+
year: 2026
|
|
86
|
+
notes: "Preprint: arXiv:2011.14999 (2020). AMIP -- the closest prior art to k*."
|
|
87
|
+
- type: generic
|
|
88
|
+
title: >-
|
|
89
|
+
Dropping Just a Handful of Preferences Can Change Top Large Language Model Rankings
|
|
90
|
+
authors:
|
|
91
|
+
- family-names: Huang
|
|
92
|
+
given-names: "Jenny Y."
|
|
93
|
+
- family-names: Shen
|
|
94
|
+
given-names: Yunyi
|
|
95
|
+
- family-names: Wei
|
|
96
|
+
given-names: Dennis
|
|
97
|
+
- family-names: Broderick
|
|
98
|
+
given-names: Tamara
|
|
99
|
+
year: 2025
|
|
100
|
+
notes: >-
|
|
101
|
+
arXiv:2508.11847. Drop-to-flip applied to Bradley-Terry LLM preference rankings; a related,
|
|
102
|
+
non-competing instance in a different domain -- see README.md.
|