foldaudit 0.2.5__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anas Riaz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ graft examples
2
+ include README.md
3
+ include LICENSE
@@ -0,0 +1,437 @@
1
+ Metadata-Version: 2.4
2
+ Name: foldaudit
3
+ Version: 0.2.5
4
+ Summary: Check that your cross-validation folds are fair and usable before you trust the score.
5
+ Author: Anas Riaz
6
+ License-Expression: MIT
7
+ Keywords: cross-validation,machine-learning,evaluation,scikit-learn
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # foldaudit
17
+
18
+ **Name:** foldaudit
19
+ **One sentence:** It checks that your cross-validation folds are fair
20
+ and usable before you trust the score.
21
+
22
+ ## The problem
23
+
24
+ When you run cross-validation, you get scores. You do not get any
25
+ information about the folds those scores came from. A fold can be
26
+ broken in ways that make its score meaningless: it contains only one
27
+ class, it is far too small, its class balance is nothing like your
28
+ full dataset, or, in a time-series split, training data comes from
29
+ after the test data. Scikit-learn's own issue tracker documents
30
+ degenerate folds of exactly this kind, for example StratifiedGroupKFold
31
+ producing empty or unusable splits, while `cross_val_score` only
32
+ returns scores and never inspects the folds themselves. Almost nobody
33
+ inspects every fold's indices by hand. foldaudit does that inspection
34
+ for you, before you train anything.
35
+
36
+ ## What it is
37
+
38
+ A small, pure-Python library, version 0.2.5. No third-party
39
+ dependencies, no model training, no plots, no GPU, no paid APIs.
40
+ It works with scikit-learn splitters and without them: give it a
41
+ splitter with a `.split()` method, or a plain list of
42
+ `(train_idx, test_idx)` pairs.
43
+
44
+ ## Installation (once published)
45
+
46
+ ```bash
47
+ pip install foldaudit
48
+ ```
49
+
50
+
51
+ Local use from this folder, without installing:
52
+
53
+ ```bash
54
+ PYTHONPATH=src python -c "from foldaudit import audit; print('ok')"
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ```python
60
+ from foldaudit import audit
61
+
62
+ # With a splitter (KFold, StratifiedKFold, GroupKFold,
63
+ # TimeSeriesSplit, or any object with .split())
64
+ report = audit(cv, X, y, groups=groups, time=dates)
65
+
66
+ # Without scikit-learn: plain index pairs
67
+ report = audit([(train_idx, test_idx), ...], y=y)
68
+
69
+ print(report.text) # pasteable plain-text report
70
+ print(report.to_json()) # JSON, for saving or for CI
71
+ report.raise_if_fail() # raises ValueError on a FAIL verdict
72
+ # report.raise_if_fail(fail_on_warning=True) # also fails on WARNING
73
+ ```
74
+
75
+ ## What it checks (version 0.2.5)
76
+
77
+ 1. **Per-fold table.** Train/test sizes, class counts and rates,
78
+ group counts, and time min/max per fold.
79
+ 2. **Validity flags.** Empty train/test, single-class train/test,
80
+ a class missing from a test fold, a test fold smaller than
81
+ the minimum size, a row appearing in both train and test of the
82
+ same fold (FAIL, since 0.2.1), a duplicated index inside
83
+ one side of a fold (WARNING, since 0.2.1), and a group
84
+ appearing on both sides of a fold (FAIL, since 0.2.2).
85
+ 3. **Stratification and balance.** A fold's class rate compared
86
+ with the overall rate, and a fold's test size compared with the
87
+ median test size. Thresholds are configurable; the report always
88
+ quotes the actual numbers.
89
+ 4. **Coverage.** How many times each row is tested, rows never
90
+ tested, and overlapping test folds (reported, not failed, since
91
+ repeated or shuffled splitters overlap by design).
92
+ 5. **Report export.** Plain text and JSON, with an overall verdict
93
+ of OK, WARNING, or FAIL, and an optional fail-on-warning hook
94
+ for CI.
95
+
96
+ Additional checks: time order (train times must precede test
97
+ times, otherwise FAIL), group counts per fold, and group
98
+ overlap (a group on both sides of a fold is group leakage, FAIL,
99
+ since 0.2.2). Version 0.2.0
100
+ adds the rare-class ratio check: a fold's class rate more than 2x
101
+ (or less than 1/2x) the overall rate warns, even when the absolute
102
+ gap is under 20 points, provided the gap is at least 5 points and
103
+ the class appears in the test fold.
104
+
105
+ ## Thresholds (defaults, all configurable)
106
+
107
+ | Setting | Default | Meaning |
108
+ | --- | --- | --- |
109
+ | `class_rate_deviation` | 0.20 | Warn when a fold's class rate differs from the overall rate by more than 20 percentage points. |
110
+ | `class_rate_ratio` | 2.0 | Warn when a fold's class rate is more than this many times the overall rate, or less than 1/this of it (rare-class check; 5-point absolute floor applies). |
111
+ | `min_test_size` | 10 | Warn when a test fold has fewer rows than this. |
112
+ | `fold_size_deviation` | 0.50 | Warn when a test fold's size differs from the median test size by more than 50%. |
113
+
114
+ ```python
115
+ from foldaudit import Thresholds, audit
116
+
117
+ report = audit(cv, X, y, thresholds=Thresholds(
118
+ class_rate_deviation=0.10,
119
+ min_test_size=20,
120
+ ))
121
+ ```
122
+
123
+ These are documented heuristics, not universal truths. Change them
124
+ to fit your problem.
125
+
126
+ ## Example output
127
+
128
+ A degenerate split, where each test fold contains only one class:
129
+
130
+ ```
131
+ foldaudit verdict: FAIL
132
+
133
+ Fold Train Test Test class rates Verdict
134
+ 0 50 50 0=100% FAIL
135
+ 1 50 50 1=100% FAIL
136
+
137
+ Flags:
138
+ [FAIL] fold 0: Train set has a single class: 1.
139
+ [FAIL] fold 0: Test set has a single class: 0.
140
+ ...
141
+
142
+ Since 0.2.0 the missing-class and rate-deviation warnings for a
143
+ single-class fold are suppressed: they were the same fact reported
144
+ three times. One bad fold now reports two flags, not five.
145
+ ```
146
+
147
+ A healthy balanced split reports `foldaudit verdict: OK` with no flags.
148
+
149
+ ## What it deliberately will NOT do
150
+
151
+ - No model training or scores.
152
+ - No new splitters, and no purged or embargoed splits.
153
+ - No semantic train/test leakage detection beyond group labels
154
+ (duplicated rows, feature or target leakage — that is a
155
+ separate tool's job). A literal index appearing in both train
156
+ and test of one fold IS flagged, since 0.2.1, and a group label
157
+ appearing on both sides IS flagged, since 0.2.2: both are basic
158
+ fold validity, not semantic leakage analysis.
159
+ - No plots, drift detection, slice or fairness metrics, and no
160
+ experiment tracking.
161
+ - No automatic "fixing" of folds. It tells you what is wrong so
162
+ you can choose a better splitter yourself.
163
+
164
+ ## Project layout
165
+
166
+ ```
167
+ fold-audit/
168
+ pyproject.toml
169
+ README.md
170
+ src/
171
+ foldaudit/
172
+ __init__.py # public API: audit, Thresholds, Report, ...
173
+ core.py # audit logic, flags, coverage, time, groups
174
+ report.py # FoldResult, Flag, Report, text/JSON rendering
175
+ tests/
176
+ test_foldaudit.py # 72 tests, standard library only
177
+ ```
178
+
179
+ ## Tests and demo
180
+
181
+ ```bash
182
+ python -m pytest tests -q
183
+ PYTHONPATH=src python examples/demo.py
184
+ ```
185
+
186
+ 72 tests pass. They use fake splitters made of plain index lists,
187
+ so they need no scikit-learn, pandas, or any other dependency.
188
+ Ruff check and ruff format check are clean. A wheel builds as
189
+ `foldaudit-0.2.5-py3-none-any.whl` and installs and imports as
190
+ version 0.2.5.
191
+
192
+ ## What changed in 0.2.0 (fixes for the 2 November 2026 problems report)
193
+
194
+ All five reported problems are fixed, with a regression test each:
195
+
196
+ 1. **Rare-class blind spot:** new `class_rate_ratio_deviation` check
197
+ (see above). The report's own example — 2% overall, 15% in a fold —
198
+ now warns at 7.5x.
199
+ 2. **Out-of-range indices:** a bad index now raises
200
+ `ValueError: fold 0 train set contains index 3, but only 3 samples
201
+ were provided`, instead of a bare `IndexError` from inside the
202
+ counting loop. Negative and non-integer indices are rejected too.
203
+ 3. **Report noise:** a single-class test fold now reports only the
204
+ single-class FAILs. The missing-class and rate-deviation warnings
205
+ for that fold were consequences of the same fact, so they are
206
+ suppressed. The degenerate demo fold went from 5 flags to 2.
207
+ 4. **Small-fold check without labels:** `small_test` no longer lives
208
+ inside the labels block, so auditing without `y` still warns on a
209
+ 1-row test fold. An empty test fold gets `empty_test` only.
210
+ 5. **TimeSeriesSplit verdict:** with a `time` column supplied,
211
+ `never_tested` is INFO, not WARNING, because the earliest rows
212
+ are train-only by design. Without `time`, it stays WARNING.
213
+
214
+ ## What changed in 0.2.1 (fix for the 0.2.0 problems report)
215
+
216
+ Problem 6, the only open item from that report, is fixed:
217
+
218
+ - **Train/test overlap within a fold (FAIL):** a row in both sides
219
+ of one fold now raises `train_test_overlap`, quoting the count,
220
+ e.g. 10 rows in the report's example. Previously that split
221
+ reported only an unrelated `never_tested` warning.
222
+ - **Duplicate index inside one side (WARNING):** a test fold like
223
+ `[2, 2, 3]` is flagged as `duplicate_indices` instead of being
224
+ silently counted twice. Indices are counted as given, not
225
+ silently deduplicated.
226
+ - **Polish:** the sdist now includes `examples/` via
227
+ `MANIFEST.in`. No `[project.urls]` Homepage/Repository was
228
+ added, because no GitHub repo exists yet — adding a guessed
229
+ URL would be worse than none.
230
+
231
+ ## What changed in 0.2.2 (complete fix list, F1-F8)
232
+
233
+ Only the items in that list were changed:
234
+
235
+ - **F1 group overlap (FAIL):** a group label on both sides of a
236
+ fold now raises `group_overlap`. Counts are unchanged. No other
237
+ leakage detection was added.
238
+ - **F2 Thresholds validation:** invalid values now raise
239
+ `ValueError` naming the field (`class_rate_deviation` outside
240
+ [0, 1], `class_rate_ratio` below 1.0, `min_test_size` below 0,
241
+ `fold_size_deviation` below 0). Defaults and field names are
242
+ unchanged.
243
+ - **F3 boolean index:** a boolean index now raises the same
244
+ non-integer `ValueError` as 2.5. Ordinary ints still pass.
245
+ - **F4 ratio formatting:** ratios now print with two significant
246
+ digits, so 0.25 prints as "(0.25x)" and 7.5 still as "(7.5x)".
247
+ - **F5 text table:** the rates column is now as wide as the
248
+ longest rates string (minimum 40, plus one gap character), so a
249
+ 12-class report no longer collides with its verdict. The demo
250
+ output is byte-identical to 0.2.1.
251
+ - **F6 README:** the PyPI-name block was removed; the dated check
252
+ is recorded once, in Blunt status below.
253
+ - **F7 sdist:** verified to list `examples/demo.py`; the
254
+ `MANIFEST.in` that already provided it is unchanged.
255
+ - **F8 [project.urls]:** skipped, because no GitHub repo exists
256
+ yet. No URL was invented or guessed.
257
+
258
+
259
+ ## What changed in 0.2.4 (audit FA-001 to FA-030)
260
+
261
+ All 30 issues from the 0.2.2 audit are fixed in this version, with
262
+ a regression test each (30 new tests, 66 in total). This is a fresh
263
+ build on 0.2.2; it is not 0.2.3, and no 0.2.3 code was reused.
264
+
265
+ - **FA-001/002/003 splitter routing:** splitter arguments are now
266
+ dispatched by parameter name. A parameter named `groups` gets
267
+ groups, `time`/`times` gets time, keyword-only parameters are
268
+ passed by keyword, and a `*args` wrapper or an unavailable
269
+ signature falls back to `split(X, y, groups)`. An unknown
270
+ required parameter raises a clear error instead of receiving
271
+ the wrong object.
272
+ - **FA-004 missing/infinite labels:** rejected with ValueError
273
+ naming the position, as scikit-learn does. NaN labels no longer
274
+ become one phantom class per row.
275
+ - **FA-005 missing groups:** normalised to one missing group and
276
+ warned about as `missing_group_ids`, with the count. All-missing
277
+ groups therefore also trigger `group_overlap`, instead of
278
+ silently passing.
279
+ - **FA-006 missing times:** warned about as `missing_time` and
280
+ excluded from min/max, so the time-order verdict no longer
281
+ depends on index order.
282
+ - **FA-007 Thresholds:** frozen dataclass; NaN, infinite and
283
+ non-numeric values are rejected naming the field. `audit()`
284
+ also rejects a non-Thresholds thresholds object.
285
+ - **FA-008 input validation:** 2-D, mapping, string and unhashable
286
+ y/groups/time values raise ValueError naming the argument and
287
+ the required 1-D hashable shape.
288
+ - **FA-009/010 sample sizes:** sample count uses `shape[0]`
289
+ first, so sparse X works, and X length is checked against
290
+ y/groups/time and raises ValueError on mismatch.
291
+ - **FA-011 fold iterables:** any iterable of pairs is accepted,
292
+ a single `(train, test)` pair is one fold, strings raise
293
+ TypeError, and malformed folds raise ValueError naming the fold.
294
+ - **FA-012/013 JSON:** class tables in JSON are now records
295
+ (`label`, `repr`, `count`/`rate`), so labels `1` and `"1"` or
296
+ `None` and `"null"` cannot collide, numpy scalars, tuples,
297
+ datetimes, bytes and Decimal labels cannot crash export, and
298
+ `to_json()` uses `allow_nan=False` so no bare NaN/Infinity
299
+ token is ever emitted. This changes `to_dict()`/`to_json()`
300
+ class-table shape from dicts to lists; the Python
301
+ `FoldResult` attributes are unchanged dicts.
302
+ - **FA-014 datetime64 times:** numpy datetime64 ints are
303
+ converted to ISO strings (NaT becomes missing). Month/year
304
+ datetime64 units are left as-is.
305
+ - **FA-015 continuous targets:** a float target with more than
306
+ half its values unique warns as `continuous_target` and skips
307
+ class checks, instead of flooding the report. This detection
308
+ is float-only, as specified in the registry.
309
+ - **FA-016 text table:** every column width is computed from
310
+ the data, so large sizes and fold numbers cannot run together.
311
+ - **FA-017 median:** fold-size imbalance now uses the true
312
+ median (`statistics.median`), not the upper middle element.
313
+ - **FA-018 boundary ties (author decision):** equal train-max
314
+ and test-min timestamps warn as `time_tie_at_boundary`
315
+ (WARNING), not FAIL and not a silent pass. A tie may be one
316
+ batch, but it may also hide leakage, so it must be visible.
317
+ - **FA-019 missing train classes:** mirrored as
318
+ `missing_class_train`, suppressed only when the train side
319
+ itself is single-class (the same P3 rule as for test).
320
+ - **FA-020 single hold-out:** `never_tested` is INFO, not
321
+ WARNING, for a single fold, as it already was with time.
322
+ - **FA-021 group examples:** distinct overlapping groups are
323
+ shown, deduplicated before slicing to three.
324
+ - **FA-022 message formatting:** rates under 1% print with
325
+ two decimal places (never "0%"), and ratios never print in
326
+ scientific notation.
327
+ - **FA-023 exact thresholds:** class-rate decisions use exact
328
+ Fraction arithmetic from the decimal threshold, so exactly
329
+ 20 points does not warn and exactly 5 points at 2.5x does.
330
+ - **FA-024 performance:** folds are streamed when sample data
331
+ is given (materialisation only remains when the sample count
332
+ must itself be inferred from the folds), and pairwise test
333
+ overlap uses bitmasks for up to 250,000 rows instead of
334
+ Python set intersections. Pairwise checking is still O(k^2)
335
+ in the number of folds, in C bit operations rather than
336
+ Python set operations; it is not claimed to be linear.
337
+ - **FA-025 per-row test counts:** `Report.test_counts` now
338
+ exposes the count for every row, including 0 for never-tested
339
+ rows, matching what the README coverage section promised.
340
+ - **FA-026 pooled schemes:** 1-row test folds, and test folds
341
+ that are exactly one group absent from train, downgrade
342
+ `single_class_test` and `small_test` to INFO instead of FAIL.
343
+ - **FA-027 string times:** rejected with ValueError; string
344
+ times are never compared lexicographically.
345
+ - **FA-028 packaging:** SPDX `license = "MIT"` with
346
+ license-files, and a `py.typed` marker is shipped. The old
347
+ `License :: OSI Approved :: MIT License` classifier was
348
+ removed because modern setuptools (PEP 639) forbids it
349
+ alongside an SPDX license expression; this is metadata
350
+ only, no code change.
351
+ - **FA-029 scope:** every text report ends with a Scope
352
+ footer stating what is checked and what is not checked,
353
+ and `to_dict()`/`to_json()` carry the same `scope` field.
354
+ - **FA-030 test suite:** 30 new regression tests, one per
355
+ bug, all standard-library only. The auditor's 28
356
+ sklearn/numpy/pandas regression tests are not copied into
357
+ the shipped suite (it must run without dependencies), but
358
+ equivalent stdlib tests cover the same behaviours; the
359
+ mutation score was not re-measured in this build.
360
+
361
+ Not fixed / still limited: no project is claimed
362
+ production-ready or leakage-free. Semantic leakage (duplicate
363
+ rows, feature/target leakage, preprocessing leakage) is still
364
+ out of scope by design and is now stated in every report.
365
+
366
+ ## What changed in 0.2.5 (problems report P-001 to P-006)
367
+
368
+ All six problems from the 0.2.4 problems report are fixed, with a
369
+ regression test each (6 new tests, 72 in total). No new
370
+ dependencies, no API or parameter renames, no removed features.
371
+
372
+ - **P-001 tuple labels:** hashable tuple labels are now accepted
373
+ as atomic class labels. Unhashable nested values (lists, dicts,
374
+ sets, or tuples containing unhashable values) are still rejected.
375
+ - **P-002 generator time:** a generator `time` input is
376
+ materialised once at the start of `audit()` and reused for
377
+ validation and auditing, instead of being consumed by the
378
+ string-time validation pass.
379
+ - **P-003 small rates:** the text table now uses the adaptive
380
+ percentage formatter, so a 0.5% test class rate prints as
381
+ "0.50%", never as "0%".
382
+ - **P-004 fold verdict:** the per-fold table verdict is derived
383
+ from severity, FAIL > WARNING > INFO > OK, so an INFO-only
384
+ fold (e.g. a pooled leave-one-out fold) displays INFO, not
385
+ WARNING. The overall verdict rule is unchanged.
386
+ - **P-005 splitter routing:** positional slots already consumed
387
+ are tracked, and recognised named arguments after a skipped
388
+ defaulted slot are passed by keyword, so e.g.
389
+ `split(X, unused=None, groups=None)` no longer receives
390
+ groups in `unused`.
391
+ - **P-006 numpy pair:** a single `(train, test)` pair given as
392
+ two 1-D numpy integer arrays is now recognised as one fold,
393
+ like the list/tuple pair form. The shipped tests use a
394
+ stdlib fake array with the same shape/tolist interface, so
395
+ the suite still runs without numpy.
396
+
397
+ ## Blunt status: done vs not done
398
+
399
+ Done: the core audit, the version 0.1 checks plus the 0.2.0
400
+ rare-class ratio check, the 0.2.1 overlap/duplicate checks, the
401
+ 0.2.2 group-overlap check, and the 0.2.4 FA-001 to FA-030 fixes, the 0.2.5 P-001 to P-006 fixes,
402
+ text and JSON reports, configurable thresholds, 72 tests, a
403
+ runnable demo, a clean ruff check and ruff format check, and a
404
+ buildable wheel and sdist that pass twine check.
405
+ Not done: not published on PyPI, not proven to be wanted by
406
+ anyone, and no GitHub repo exists yet, so no Homepage link is
407
+ set. The PyPI name "foldaudit" was checked free on PyPI on
408
+ 2 November 2026 and remains unpublished. Real scikit-learn
409
+ integration has now been tested successfully: by the auditor's
410
+ regression suite (StratifiedKFold OK, degenerate KFold FAIL,
411
+ GroupKFold counts correct), and in a real-data test on NYC
412
+ Yellow Taxi, January 2024 (first 1,500,000 rows of 2,964,624
413
+ trips; label payment_type, groups 252 pickup zones, time
414
+ pickup timestamp) — StratifiedKFold(5) OK in 8.9s, KFold(5)
415
+ with zones as groups FAIL in 12.6s (233–237 of 252 zones
416
+ leaking into train and test in every fold), GroupKFold(5)
417
+ by zone OK in 12.2s with no group or class-balance false
418
+ alarms, and KFold (no shuffle) on time-sorted data with
419
+ time FAIL in 12.2s, including a 2002-12-31 timestamp flagged
420
+ in January 2024 data. That real-data test was reported by
421
+ Anas on 2 November 2026 and was not run in this environment,
422
+ where scikit-learn is still not installed; the shipped test
423
+ suite itself remains standard-library only by design.
424
+
425
+ ## Risks and honest limits
426
+
427
+ - Demand is indirect: scikit-learn issues document degenerate
428
+ folds, but nobody was found asking for a "fold audit library"
429
+ by name, so popularity is unproven.
430
+ - The library must earn its existence through the full checklist,
431
+ severity levels, and report, not just a counts loop.
432
+ - Scikit-learn could one day add fold warnings itself; foldaudit
433
+ stays splitter-agnostic to survive that.
434
+
435
+ ## License
436
+
437
+ MIT.