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.
Files changed (75) hide show
  1. selection_fragility-1.0.0/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
  2. selection_fragility-1.0.0/.github/workflows/selection-fragility-test.yml +46 -0
  3. selection_fragility-1.0.0/CHANGELOG.md +438 -0
  4. selection_fragility-1.0.0/CITATION.cff +102 -0
  5. selection_fragility-1.0.0/CONTRIBUTING.md +62 -0
  6. selection_fragility-1.0.0/EVALUATION_CARD.md +148 -0
  7. selection_fragility-1.0.0/LICENSE +21 -0
  8. selection_fragility-1.0.0/MANIFEST.in +36 -0
  9. selection_fragility-1.0.0/PKG-INFO +449 -0
  10. selection_fragility-1.0.0/README.md +419 -0
  11. selection_fragility-1.0.0/pyproject.toml +89 -0
  12. selection_fragility-1.0.0/requirements-lock.txt +20 -0
  13. selection_fragility-1.0.0/setup.cfg +4 -0
  14. selection_fragility-1.0.0/src/selection_fragility/__init__.py +73 -0
  15. selection_fragility-1.0.0/src/selection_fragility/__main__.py +39 -0
  16. selection_fragility-1.0.0/src/selection_fragility/_internals.py +52 -0
  17. selection_fragility-1.0.0/src/selection_fragility/compare.py +276 -0
  18. selection_fragility-1.0.0/src/selection_fragility/fragility.py +885 -0
  19. selection_fragility-1.0.0/src/selection_fragility/identify.py +312 -0
  20. selection_fragility-1.0.0/src/selection_fragility/mcs.py +258 -0
  21. selection_fragility-1.0.0/src/selection_fragility/panel.py +794 -0
  22. selection_fragility-1.0.0/src/selection_fragility/pivot.py +154 -0
  23. selection_fragility-1.0.0/src/selection_fragility/prop22.py +90 -0
  24. selection_fragility-1.0.0/src/selection_fragility/report.py +253 -0
  25. selection_fragility-1.0.0/src/selection_fragility/resolution.py +435 -0
  26. selection_fragility-1.0.0/src/selection_fragility.egg-info/PKG-INFO +449 -0
  27. selection_fragility-1.0.0/src/selection_fragility.egg-info/SOURCES.txt +73 -0
  28. selection_fragility-1.0.0/src/selection_fragility.egg-info/dependency_links.txt +1 -0
  29. selection_fragility-1.0.0/src/selection_fragility.egg-info/requires.txt +11 -0
  30. selection_fragility-1.0.0/src/selection_fragility.egg-info/top_level.txt +1 -0
  31. selection_fragility-1.0.0/tests/README.md +71 -0
  32. selection_fragility-1.0.0/tests/conftest.py +176 -0
  33. selection_fragility-1.0.0/tests/test_ab_testing_domain_review.py +94 -0
  34. selection_fragility-1.0.0/tests/test_core_api_losspanel_acceptance.py +112 -0
  35. selection_fragility-1.0.0/tests/test_crosscutting.py +184 -0
  36. selection_fragility-1.0.0/tests/test_extensibility_review.py +143 -0
  37. selection_fragility-1.0.0/tests/test_packaging.py +108 -0
  38. selection_fragility-1.0.0/tests/test_probabilistic_forecasting_review.py +110 -0
  39. selection_fragility-1.0.0/tests/test_property_based.py +297 -0
  40. selection_fragility-1.0.0/tests/test_reproducibility.py +100 -0
  41. selection_fragility-1.0.0/tests/test_round3_reviews.py +308 -0
  42. selection_fragility-1.0.0/tests/test_round5_competition_integrity.py +139 -0
  43. selection_fragility-1.0.0/tests/test_round6_clinical_biostatistics.py +70 -0
  44. selection_fragility-1.0.0/tests/test_round6_financial_adversarial.py +158 -0
  45. selection_fragility-1.0.0/tests/test_round6_foundation_model_researcher.py +121 -0
  46. selection_fragility-1.0.0/tests/test_round6_production_scale.py +83 -0
  47. selection_fragility-1.0.0/tests/test_round6_recsys_cross_domain.py +110 -0
  48. selection_fragility-1.0.0/tests/test_round7_ab_testing_realistic_data.py +168 -0
  49. selection_fragility-1.0.0/tests/test_round7_clinical_realistic_data.py +210 -0
  50. selection_fragility-1.0.0/tests/test_round7_government_policy_realistic_data.py +207 -0
  51. selection_fragility-1.0.0/tests/test_round7_llm_leaderboard_realistic_data.py +179 -0
  52. selection_fragility-1.0.0/tests/test_round8_concurrency_safety.py +129 -0
  53. selection_fragility-1.0.0/tests/test_round8_energy_grid_realistic_data.py +169 -0
  54. selection_fragility-1.0.0/tests/test_round8_epidemiology_realistic_data.py +188 -0
  55. selection_fragility-1.0.0/tests/test_round8_formula_rederivation.py +94 -0
  56. selection_fragility-1.0.0/tests/test_round8_insurance_actuarial_realistic_data.py +179 -0
  57. selection_fragility-1.0.0/tests/test_round8_losspanel_completeness.py +68 -0
  58. selection_fragility-1.0.0/tests/test_round8_manufacturing_realistic_data.py +177 -0
  59. selection_fragility-1.0.0/tests/test_round8_marketing_attribution_realistic_data.py +119 -0
  60. selection_fragility-1.0.0/tests/test_round8_sports_analytics_realistic_data.py +135 -0
  61. selection_fragility-1.0.0/tests/test_round8_type_annotations.py +25 -0
  62. selection_fragility-1.0.0/tests/test_round9_agriculture_realistic_data.py +143 -0
  63. selection_fragility-1.0.0/tests/test_round9_climate_weather_realistic_data.py +123 -0
  64. selection_fragility-1.0.0/tests/test_round9_election_polling_realistic_data.py +135 -0
  65. selection_fragility-1.0.0/tests/test_round9_fraud_detection_realistic_data.py +198 -0
  66. selection_fragility-1.0.0/tests/test_round9_real_estate_realistic_data.py +175 -0
  67. selection_fragility-1.0.0/tests/test_round9_subgroup_heterogeneity_realistic_data.py +165 -0
  68. selection_fragility-1.0.0/tests/test_security_adversarial.py +452 -0
  69. selection_fragility-1.0.0/tests/test_stage0_losspanel.py +706 -0
  70. selection_fragility-1.0.0/tests/test_stage1_identified.py +342 -0
  71. selection_fragility-1.0.0/tests/test_stage2_resolution.py +282 -0
  72. selection_fragility-1.0.0/tests/test_stage3_kstar_prop22.py +1065 -0
  73. selection_fragility-1.0.0/tests/test_stage4_pivot.py +357 -0
  74. selection_fragility-1.0.0/tests/test_stage5_compare.py +167 -0
  75. 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.