tabaudit 0.1.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.
- tabaudit-0.1.0/LICENSE +21 -0
- tabaudit-0.1.0/PKG-INFO +229 -0
- tabaudit-0.1.0/README.md +193 -0
- tabaudit-0.1.0/pyproject.toml +65 -0
- tabaudit-0.1.0/setup.cfg +4 -0
- tabaudit-0.1.0/src/tabaudit/__init__.py +8 -0
- tabaudit-0.1.0/src/tabaudit/audit.py +102 -0
- tabaudit-0.1.0/src/tabaudit/checks/__init__.py +22 -0
- tabaudit-0.1.0/src/tabaudit/checks/duplicates.py +83 -0
- tabaudit-0.1.0/src/tabaudit/checks/imbalance.py +70 -0
- tabaudit-0.1.0/src/tabaudit/checks/label_noise.py +134 -0
- tabaudit-0.1.0/src/tabaudit/checks/leakage.py +316 -0
- tabaudit-0.1.0/src/tabaudit/checks/schema.py +119 -0
- tabaudit-0.1.0/src/tabaudit/cli.py +155 -0
- tabaudit-0.1.0/src/tabaudit/context.py +52 -0
- tabaudit-0.1.0/src/tabaudit/demo.py +111 -0
- tabaudit-0.1.0/src/tabaudit/findings.py +122 -0
- tabaudit-0.1.0/src/tabaudit/loader.py +88 -0
- tabaudit-0.1.0/src/tabaudit/report/__init__.py +4 -0
- tabaudit-0.1.0/src/tabaudit/report/console.py +155 -0
- tabaudit-0.1.0/src/tabaudit/report/html.py +35 -0
- tabaudit-0.1.0/src/tabaudit/report/template.html +139 -0
- tabaudit-0.1.0/src/tabaudit.egg-info/PKG-INFO +229 -0
- tabaudit-0.1.0/src/tabaudit.egg-info/SOURCES.txt +28 -0
- tabaudit-0.1.0/src/tabaudit.egg-info/dependency_links.txt +1 -0
- tabaudit-0.1.0/src/tabaudit.egg-info/entry_points.txt +2 -0
- tabaudit-0.1.0/src/tabaudit.egg-info/requires.txt +14 -0
- tabaudit-0.1.0/src/tabaudit.egg-info/top_level.txt +1 -0
- tabaudit-0.1.0/tests/test_checks.py +221 -0
- tabaudit-0.1.0/tests/test_cli.py +60 -0
tabaudit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sadia Samia
|
|
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.
|
tabaudit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tabaudit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Audit tabular ML datasets for leakage, duplicates, label noise and imbalance before you train.
|
|
5
|
+
Author: Sadia Samia
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sadiasamia121912/tabaudit
|
|
8
|
+
Project-URL: Issues, https://github.com/sadiasamia121912/tabaudit/issues
|
|
9
|
+
Keywords: machine-learning,data-quality,data-leakage,label-noise,cleanlab,audit
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pandas>=2.0
|
|
23
|
+
Requires-Dist: numpy>=1.24
|
|
24
|
+
Requires-Dist: scikit-learn>=1.3
|
|
25
|
+
Requires-Dist: cleanlab>=2.5
|
|
26
|
+
Requires-Dist: typer>=0.12
|
|
27
|
+
Requires-Dist: rich>=13.0
|
|
28
|
+
Requires-Dist: jinja2>=3.1
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
32
|
+
Requires-Dist: build; extra == "dev"
|
|
33
|
+
Requires-Dist: twine; extra == "dev"
|
|
34
|
+
Requires-Dist: pyarrow; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# tabaudit
|
|
38
|
+
|
|
39
|
+
**Find the problems in your dataset before your model does.**
|
|
40
|
+
|
|
41
|
+
`tabaudit` is a command-line tool that audits a tabular ML dataset for the defects that
|
|
42
|
+
quietly inflate metrics and break models in production — target leakage, train/test
|
|
43
|
+
contamination, duplicates, label noise, class imbalance and schema problems — and gives it
|
|
44
|
+
a single **Data Health Score** with concrete, prioritised fixes.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
pip install tabaudit
|
|
48
|
+
tabaudit audit train.csv --target churn --test test.csv --html report.html
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Why
|
|
56
|
+
|
|
57
|
+
A model that scores 99% on a leaked dataset is worse than useless: it looks finished and
|
|
58
|
+
fails silently in production. Most of these defects take one line of pandas to fix — the
|
|
59
|
+
hard part is *noticing* them. `tabaudit` makes the check automatic, fast, and repeatable
|
|
60
|
+
(it runs in CI with `--fail-under`).
|
|
61
|
+
|
|
62
|
+
## What it checks
|
|
63
|
+
|
|
64
|
+
| check | finds | severity |
|
|
65
|
+
| ------------- | --------------------------------------------------------------------------------------------------------- | -------- |
|
|
66
|
+
| `leakage` | single features that predict the target almost perfectly on their own (incl. *missingness* leaks), identifier columns, columns named after the target | CRITICAL / HIGH / MEDIUM |
|
|
67
|
+
| `duplicates` | exact duplicate rows, feature-identical rows with conflicting labels, **test rows that also appear in train** | CRITICAL → LOW |
|
|
68
|
+
| `label_noise` | probably-mislabeled rows via confident learning ([cleanlab](https://github.com/cleanlab/cleanlab)), ranked and tiered | HIGH → INFO |
|
|
69
|
+
| `imbalance` | class ratio, classes with < 10 examples | HIGH → LOW |
|
|
70
|
+
| `schema` | ≥ 50 % missing columns, missing labels, constant / near-constant columns, numbers stored as text, leftover index columns | HIGH → INFO |
|
|
71
|
+
|
|
72
|
+
Every finding carries a plain-English explanation of *why it matters* and *what to do*.
|
|
73
|
+
Findings are weighted into a 0–100 score and an A–F grade.
|
|
74
|
+
|
|
75
|
+
## Quick start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install tabaudit
|
|
79
|
+
|
|
80
|
+
# See every check fire on a synthetic dataset with planted defects
|
|
81
|
+
tabaudit demo
|
|
82
|
+
|
|
83
|
+
# Audit your own data
|
|
84
|
+
tabaudit audit data.csv --target label
|
|
85
|
+
tabaudit audit train.parquet -t label --test test.parquet --html report.html --json report.json
|
|
86
|
+
|
|
87
|
+
# Only some checks, sampled for speed, gate a CI pipeline
|
|
88
|
+
tabaudit audit data.csv -t label -c leakage,duplicates --max-rows 20000 --fail-under 75
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Supported inputs: CSV, TSV, Parquet, Feather, JSON-lines. Classification and regression
|
|
92
|
+
targets are inferred automatically; omit `--target` to run only the unsupervised checks.
|
|
93
|
+
|
|
94
|
+
`--html` writes a self-contained report you can send to whoever owns the data:
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
98
|
+
### Python API
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from tabaudit import run_audit
|
|
102
|
+
|
|
103
|
+
report = run_audit("train.csv", target="churn", test="test.csv")
|
|
104
|
+
print(report.score, report.grade) # 6 'F'
|
|
105
|
+
for f in report.sorted_findings():
|
|
106
|
+
print(f.severity.value, f.title, f.columns)
|
|
107
|
+
report.to_dict() # JSON-serialisable
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Example output
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
┌────────────────────────────────── Verdict ──────────────────────────────────┐
|
|
114
|
+
│ Data Health Score 6/100 F │
|
|
115
|
+
│ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
|
116
|
+
│ Do not train on this dataset as-is │
|
|
117
|
+
│ CRITICAL 2 MEDIUM 4 LOW 2 │
|
|
118
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
119
|
+
┌─ CRITICAL 105 test rows (8.2%) also appear in the training set ──────────┐
|
|
120
|
+
│ The model has already seen these rows. Any test-set score is partly │
|
|
121
|
+
│ memorisation, not generalisation. │
|
|
122
|
+
│ → Remove overlapping rows from the test set, or re-split with a │
|
|
123
|
+
│ group-aware splitter if rows belong to entities. │
|
|
124
|
+
└──────────────────────────────────────────────────────────────── duplicates ─┘
|
|
125
|
+
┌─ CRITICAL 1 feature(s) predict the target almost perfectly on their own ─┐
|
|
126
|
+
│ churn_reason (AUC=1.000) - its *missingness alone* has AUC 1.00 │
|
|
127
|
+
│ → This is target leakage: the column encodes the answer (recorded after │
|
|
128
|
+
│ the outcome, or derived from it). Remove it - any model trained with it │
|
|
129
|
+
│ will look excellent and fail in production. │
|
|
130
|
+
└─────────────────────────────────────────────────────────────────── leakage ─┘
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Results on real datasets
|
|
134
|
+
|
|
135
|
+
Ten well-known public datasets, loaded straight from OpenML and audited with **default
|
|
136
|
+
settings** — no tuning, no column dropping. Full write-up, including what the tool got
|
|
137
|
+
wrong on the first run and how it was fixed: [`docs/benchmarks.md`](https://github.com/sadiasamia121912/tabaudit/blob/main/docs/benchmarks.md).
|
|
138
|
+
|
|
139
|
+
| dataset | rows | score | headline finding |
|
|
140
|
+
|---|---:|:-:|---|
|
|
141
|
+
| titanic | 1 309 | 64 C | **HIGH** `boat` predicts survival alone (AUC 0.97 vs next-best 0.74) — target leakage |
|
|
142
|
+
| spambase | 4 601 | 75 B | **HIGH** 391 exact duplicate rows (8.5 %) |
|
|
143
|
+
| creditcard | 284 807 | 78 B | **HIGH** 578 : 1 class imbalance; 9 144 duplicate rows |
|
|
144
|
+
| telco-customer-churn | 7 043 | 80 B | `TotalCharges` is numeric but stored as text; 18 conflicting-label groups |
|
|
145
|
+
| breast-w | 699 | 82 B | **HIGH** 236 exact duplicate rows (34 %) |
|
|
146
|
+
| bank-marketing | 45 211 | 83 B | `duration` stands far above every other feature (AUC 0.81 vs 0.65) — a documented leak |
|
|
147
|
+
| adult | 48 842 | 84 B | 5 groups of rows with identical features but different labels |
|
|
148
|
+
| credit-g | 1 000 | 93 A | ~6 % of rows likely mislabeled |
|
|
149
|
+
| heart-statlog | 270 | 93 A | ~6 % of rows likely mislabeled |
|
|
150
|
+
| diabetes | 768 | 93 A | ~5 % of rows likely mislabeled |
|
|
151
|
+
|
|
152
|
+
- **4 of 10 have a CRITICAL/HIGH finding; 10 of 10 have at least one MEDIUM.** Every HIGH
|
|
153
|
+
is a documented property of the dataset (Titanic's lifeboat column, spambase and
|
|
154
|
+
breast-w duplicates, creditcard's 0.17 % fraud rate).
|
|
155
|
+
- The leakage check catches both the near-perfect leak (`boat`) and the *soft* one
|
|
156
|
+
(bank-marketing `duration`, which the dataset's own documentation says to drop), while
|
|
157
|
+
correctly reporting breast-w's six strong-but-honest features as "easy task", not leakage.
|
|
158
|
+
- Label-noise suspects were checked by hand on two datasets: of the 10 top-ranked rows,
|
|
159
|
+
5 look genuinely mislabeled, 5 are ambiguous, 0 look like false alarms
|
|
160
|
+
([review sheet](https://github.com/sadiasamia121912/tabaudit/blob/main/docs/label_noise_review.md)).
|
|
161
|
+
|
|
162
|
+
Reproduce with `python benchmarks/run_benchmarks.py` (~6 min, downloads ~50 MB).
|
|
163
|
+
|
|
164
|
+
## How the hard checks work
|
|
165
|
+
|
|
166
|
+
_Short version. Every threshold, and the reason for it, is in [`docs/checks.md`](https://github.com/sadiasamia121912/tabaudit/blob/main/docs/checks.md)._
|
|
167
|
+
|
|
168
|
+
**Leakage.** For each feature *alone*, a shallow decision tree is cross-validated against
|
|
169
|
+
the target. A single column with out-of-fold AUC ≥ 0.98 (or R² ≥ 0.98 for regression) is
|
|
170
|
+
almost never a legitimate signal — it is the answer written down after the fact. Below
|
|
171
|
+
that, a leak has to be an *outlier*: the sorted single-feature scores are split at their
|
|
172
|
+
largest gap, and only a feature that sits ≥ 0.15 above every other one is flagged (HIGH if
|
|
173
|
+
it scores ≥ 0.90, MEDIUM "soft leak" if ≥ 0.75). Several strong features bunched together
|
|
174
|
+
mean the task is easy, not leaky — that case is reported at INFO. Missing values are
|
|
175
|
+
encoded so the tree can split on *missingness itself*, which catches the common "this field
|
|
176
|
+
is only filled in for positives" leak.
|
|
177
|
+
|
|
178
|
+
**Label noise.** An out-of-fold gradient-boosting model produces class probabilities for
|
|
179
|
+
every row; cleanlab's confident-learning filter flags rows whose given label it confidently
|
|
180
|
+
contradicts. The model is deliberately regularised because an over-confident model makes
|
|
181
|
+
cleanlab over-flag. Leaky and identifier columns found by the `leakage` check are excluded
|
|
182
|
+
first — otherwise the leak makes the model agree with every wrong label and the noise is
|
|
183
|
+
invisible.
|
|
184
|
+
|
|
185
|
+
Suspects are reported in two tiers: **likely** (model gives the given label < 20 %
|
|
186
|
+
probability) and **suspected** (any confident-learning flag), ranked most-confident first.
|
|
187
|
+
|
|
188
|
+
### Measured on known ground truth
|
|
189
|
+
|
|
190
|
+
The demo generator knows exactly which labels it flipped, so the detector can be scored
|
|
191
|
+
honestly (`python examples/validate_label_noise.py <noise_rate>`; 6 000 rows, clean-data
|
|
192
|
+
AUC ≈ 0.88):
|
|
193
|
+
|
|
194
|
+
| planted noise | flipped rows | "likely" flagged | likely precision / recall | top-25 precision |
|
|
195
|
+
| ------------- | ------------ | ---------------- | ------------------------- | ---------------- |
|
|
196
|
+
| 2.2 % | 110 | 187 | 32 % / 55 % | 56 % |
|
|
197
|
+
| 6.4 % | 317 | 313 | 60 % / 59 % | 72 % |
|
|
198
|
+
| 10.3 % | 514 | 420 | 73 % / 60 % | 72 % |
|
|
199
|
+
|
|
200
|
+
Random label flips on rows the model is genuinely unsure about are indistinguishable from
|
|
201
|
+
correct labels, so no detector can reach high precision *and* recall here. The point is
|
|
202
|
+
the ranked review list, not the raw count — and the count is a useful estimate at realistic
|
|
203
|
+
noise rates.
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
git clone https://github.com/sadiasamia121912/tabaudit && cd tabaudit
|
|
209
|
+
python -m venv .venv && .venv/Scripts/activate # or source .venv/bin/activate
|
|
210
|
+
pip install -e ".[dev]"
|
|
211
|
+
pytest
|
|
212
|
+
ruff check src tests examples && ruff format --check src tests examples
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Adding a check: drop a module in `src/tabaudit/checks/` exposing
|
|
216
|
+
`run(ctx: AuditContext) -> list[Finding]` and register it in `checks/__init__.py`.
|
|
217
|
+
Checks run in registry order and may communicate through `ctx.excluded_features`.
|
|
218
|
+
|
|
219
|
+
## Roadmap
|
|
220
|
+
|
|
221
|
+
- [ ] Group / time leakage: entity IDs shared across splits, features that peek into the future
|
|
222
|
+
- [ ] Near-duplicate detection (fuzzy text, numeric tolerance)
|
|
223
|
+
- [ ] Label-noise support for regression targets
|
|
224
|
+
- [x] Audit results for popular public benchmark datasets — see [Results on real datasets](#results-on-real-datasets)
|
|
225
|
+
- [ ] `pre-commit` hook and GitHub Action
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT
|
tabaudit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# tabaudit
|
|
2
|
+
|
|
3
|
+
**Find the problems in your dataset before your model does.**
|
|
4
|
+
|
|
5
|
+
`tabaudit` is a command-line tool that audits a tabular ML dataset for the defects that
|
|
6
|
+
quietly inflate metrics and break models in production — target leakage, train/test
|
|
7
|
+
contamination, duplicates, label noise, class imbalance and schema problems — and gives it
|
|
8
|
+
a single **Data Health Score** with concrete, prioritised fixes.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install tabaudit
|
|
12
|
+
tabaudit audit train.csv --target churn --test test.csv --html report.html
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why
|
|
20
|
+
|
|
21
|
+
A model that scores 99% on a leaked dataset is worse than useless: it looks finished and
|
|
22
|
+
fails silently in production. Most of these defects take one line of pandas to fix — the
|
|
23
|
+
hard part is *noticing* them. `tabaudit` makes the check automatic, fast, and repeatable
|
|
24
|
+
(it runs in CI with `--fail-under`).
|
|
25
|
+
|
|
26
|
+
## What it checks
|
|
27
|
+
|
|
28
|
+
| check | finds | severity |
|
|
29
|
+
| ------------- | --------------------------------------------------------------------------------------------------------- | -------- |
|
|
30
|
+
| `leakage` | single features that predict the target almost perfectly on their own (incl. *missingness* leaks), identifier columns, columns named after the target | CRITICAL / HIGH / MEDIUM |
|
|
31
|
+
| `duplicates` | exact duplicate rows, feature-identical rows with conflicting labels, **test rows that also appear in train** | CRITICAL → LOW |
|
|
32
|
+
| `label_noise` | probably-mislabeled rows via confident learning ([cleanlab](https://github.com/cleanlab/cleanlab)), ranked and tiered | HIGH → INFO |
|
|
33
|
+
| `imbalance` | class ratio, classes with < 10 examples | HIGH → LOW |
|
|
34
|
+
| `schema` | ≥ 50 % missing columns, missing labels, constant / near-constant columns, numbers stored as text, leftover index columns | HIGH → INFO |
|
|
35
|
+
|
|
36
|
+
Every finding carries a plain-English explanation of *why it matters* and *what to do*.
|
|
37
|
+
Findings are weighted into a 0–100 score and an A–F grade.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install tabaudit
|
|
43
|
+
|
|
44
|
+
# See every check fire on a synthetic dataset with planted defects
|
|
45
|
+
tabaudit demo
|
|
46
|
+
|
|
47
|
+
# Audit your own data
|
|
48
|
+
tabaudit audit data.csv --target label
|
|
49
|
+
tabaudit audit train.parquet -t label --test test.parquet --html report.html --json report.json
|
|
50
|
+
|
|
51
|
+
# Only some checks, sampled for speed, gate a CI pipeline
|
|
52
|
+
tabaudit audit data.csv -t label -c leakage,duplicates --max-rows 20000 --fail-under 75
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Supported inputs: CSV, TSV, Parquet, Feather, JSON-lines. Classification and regression
|
|
56
|
+
targets are inferred automatically; omit `--target` to run only the unsupervised checks.
|
|
57
|
+
|
|
58
|
+
`--html` writes a self-contained report you can send to whoever owns the data:
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
### Python API
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from tabaudit import run_audit
|
|
66
|
+
|
|
67
|
+
report = run_audit("train.csv", target="churn", test="test.csv")
|
|
68
|
+
print(report.score, report.grade) # 6 'F'
|
|
69
|
+
for f in report.sorted_findings():
|
|
70
|
+
print(f.severity.value, f.title, f.columns)
|
|
71
|
+
report.to_dict() # JSON-serialisable
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Example output
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
┌────────────────────────────────── Verdict ──────────────────────────────────┐
|
|
78
|
+
│ Data Health Score 6/100 F │
|
|
79
|
+
│ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
|
80
|
+
│ Do not train on this dataset as-is │
|
|
81
|
+
│ CRITICAL 2 MEDIUM 4 LOW 2 │
|
|
82
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
83
|
+
┌─ CRITICAL 105 test rows (8.2%) also appear in the training set ──────────┐
|
|
84
|
+
│ The model has already seen these rows. Any test-set score is partly │
|
|
85
|
+
│ memorisation, not generalisation. │
|
|
86
|
+
│ → Remove overlapping rows from the test set, or re-split with a │
|
|
87
|
+
│ group-aware splitter if rows belong to entities. │
|
|
88
|
+
└──────────────────────────────────────────────────────────────── duplicates ─┘
|
|
89
|
+
┌─ CRITICAL 1 feature(s) predict the target almost perfectly on their own ─┐
|
|
90
|
+
│ churn_reason (AUC=1.000) - its *missingness alone* has AUC 1.00 │
|
|
91
|
+
│ → This is target leakage: the column encodes the answer (recorded after │
|
|
92
|
+
│ the outcome, or derived from it). Remove it - any model trained with it │
|
|
93
|
+
│ will look excellent and fail in production. │
|
|
94
|
+
└─────────────────────────────────────────────────────────────────── leakage ─┘
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Results on real datasets
|
|
98
|
+
|
|
99
|
+
Ten well-known public datasets, loaded straight from OpenML and audited with **default
|
|
100
|
+
settings** — no tuning, no column dropping. Full write-up, including what the tool got
|
|
101
|
+
wrong on the first run and how it was fixed: [`docs/benchmarks.md`](https://github.com/sadiasamia121912/tabaudit/blob/main/docs/benchmarks.md).
|
|
102
|
+
|
|
103
|
+
| dataset | rows | score | headline finding |
|
|
104
|
+
|---|---:|:-:|---|
|
|
105
|
+
| titanic | 1 309 | 64 C | **HIGH** `boat` predicts survival alone (AUC 0.97 vs next-best 0.74) — target leakage |
|
|
106
|
+
| spambase | 4 601 | 75 B | **HIGH** 391 exact duplicate rows (8.5 %) |
|
|
107
|
+
| creditcard | 284 807 | 78 B | **HIGH** 578 : 1 class imbalance; 9 144 duplicate rows |
|
|
108
|
+
| telco-customer-churn | 7 043 | 80 B | `TotalCharges` is numeric but stored as text; 18 conflicting-label groups |
|
|
109
|
+
| breast-w | 699 | 82 B | **HIGH** 236 exact duplicate rows (34 %) |
|
|
110
|
+
| bank-marketing | 45 211 | 83 B | `duration` stands far above every other feature (AUC 0.81 vs 0.65) — a documented leak |
|
|
111
|
+
| adult | 48 842 | 84 B | 5 groups of rows with identical features but different labels |
|
|
112
|
+
| credit-g | 1 000 | 93 A | ~6 % of rows likely mislabeled |
|
|
113
|
+
| heart-statlog | 270 | 93 A | ~6 % of rows likely mislabeled |
|
|
114
|
+
| diabetes | 768 | 93 A | ~5 % of rows likely mislabeled |
|
|
115
|
+
|
|
116
|
+
- **4 of 10 have a CRITICAL/HIGH finding; 10 of 10 have at least one MEDIUM.** Every HIGH
|
|
117
|
+
is a documented property of the dataset (Titanic's lifeboat column, spambase and
|
|
118
|
+
breast-w duplicates, creditcard's 0.17 % fraud rate).
|
|
119
|
+
- The leakage check catches both the near-perfect leak (`boat`) and the *soft* one
|
|
120
|
+
(bank-marketing `duration`, which the dataset's own documentation says to drop), while
|
|
121
|
+
correctly reporting breast-w's six strong-but-honest features as "easy task", not leakage.
|
|
122
|
+
- Label-noise suspects were checked by hand on two datasets: of the 10 top-ranked rows,
|
|
123
|
+
5 look genuinely mislabeled, 5 are ambiguous, 0 look like false alarms
|
|
124
|
+
([review sheet](https://github.com/sadiasamia121912/tabaudit/blob/main/docs/label_noise_review.md)).
|
|
125
|
+
|
|
126
|
+
Reproduce with `python benchmarks/run_benchmarks.py` (~6 min, downloads ~50 MB).
|
|
127
|
+
|
|
128
|
+
## How the hard checks work
|
|
129
|
+
|
|
130
|
+
_Short version. Every threshold, and the reason for it, is in [`docs/checks.md`](https://github.com/sadiasamia121912/tabaudit/blob/main/docs/checks.md)._
|
|
131
|
+
|
|
132
|
+
**Leakage.** For each feature *alone*, a shallow decision tree is cross-validated against
|
|
133
|
+
the target. A single column with out-of-fold AUC ≥ 0.98 (or R² ≥ 0.98 for regression) is
|
|
134
|
+
almost never a legitimate signal — it is the answer written down after the fact. Below
|
|
135
|
+
that, a leak has to be an *outlier*: the sorted single-feature scores are split at their
|
|
136
|
+
largest gap, and only a feature that sits ≥ 0.15 above every other one is flagged (HIGH if
|
|
137
|
+
it scores ≥ 0.90, MEDIUM "soft leak" if ≥ 0.75). Several strong features bunched together
|
|
138
|
+
mean the task is easy, not leaky — that case is reported at INFO. Missing values are
|
|
139
|
+
encoded so the tree can split on *missingness itself*, which catches the common "this field
|
|
140
|
+
is only filled in for positives" leak.
|
|
141
|
+
|
|
142
|
+
**Label noise.** An out-of-fold gradient-boosting model produces class probabilities for
|
|
143
|
+
every row; cleanlab's confident-learning filter flags rows whose given label it confidently
|
|
144
|
+
contradicts. The model is deliberately regularised because an over-confident model makes
|
|
145
|
+
cleanlab over-flag. Leaky and identifier columns found by the `leakage` check are excluded
|
|
146
|
+
first — otherwise the leak makes the model agree with every wrong label and the noise is
|
|
147
|
+
invisible.
|
|
148
|
+
|
|
149
|
+
Suspects are reported in two tiers: **likely** (model gives the given label < 20 %
|
|
150
|
+
probability) and **suspected** (any confident-learning flag), ranked most-confident first.
|
|
151
|
+
|
|
152
|
+
### Measured on known ground truth
|
|
153
|
+
|
|
154
|
+
The demo generator knows exactly which labels it flipped, so the detector can be scored
|
|
155
|
+
honestly (`python examples/validate_label_noise.py <noise_rate>`; 6 000 rows, clean-data
|
|
156
|
+
AUC ≈ 0.88):
|
|
157
|
+
|
|
158
|
+
| planted noise | flipped rows | "likely" flagged | likely precision / recall | top-25 precision |
|
|
159
|
+
| ------------- | ------------ | ---------------- | ------------------------- | ---------------- |
|
|
160
|
+
| 2.2 % | 110 | 187 | 32 % / 55 % | 56 % |
|
|
161
|
+
| 6.4 % | 317 | 313 | 60 % / 59 % | 72 % |
|
|
162
|
+
| 10.3 % | 514 | 420 | 73 % / 60 % | 72 % |
|
|
163
|
+
|
|
164
|
+
Random label flips on rows the model is genuinely unsure about are indistinguishable from
|
|
165
|
+
correct labels, so no detector can reach high precision *and* recall here. The point is
|
|
166
|
+
the ranked review list, not the raw count — and the count is a useful estimate at realistic
|
|
167
|
+
noise rates.
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
git clone https://github.com/sadiasamia121912/tabaudit && cd tabaudit
|
|
173
|
+
python -m venv .venv && .venv/Scripts/activate # or source .venv/bin/activate
|
|
174
|
+
pip install -e ".[dev]"
|
|
175
|
+
pytest
|
|
176
|
+
ruff check src tests examples && ruff format --check src tests examples
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Adding a check: drop a module in `src/tabaudit/checks/` exposing
|
|
180
|
+
`run(ctx: AuditContext) -> list[Finding]` and register it in `checks/__init__.py`.
|
|
181
|
+
Checks run in registry order and may communicate through `ctx.excluded_features`.
|
|
182
|
+
|
|
183
|
+
## Roadmap
|
|
184
|
+
|
|
185
|
+
- [ ] Group / time leakage: entity IDs shared across splits, features that peek into the future
|
|
186
|
+
- [ ] Near-duplicate detection (fuzzy text, numeric tolerance)
|
|
187
|
+
- [ ] Label-noise support for regression targets
|
|
188
|
+
- [x] Audit results for popular public benchmark datasets — see [Results on real datasets](#results-on-real-datasets)
|
|
189
|
+
- [ ] `pre-commit` hook and GitHub Action
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tabaudit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Audit tabular ML datasets for leakage, duplicates, label noise and imbalance before you train."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Sadia Samia" }]
|
|
14
|
+
keywords = ["machine-learning", "data-quality", "data-leakage", "label-noise", "cleanlab", "audit"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pandas>=2.0",
|
|
28
|
+
"numpy>=1.24",
|
|
29
|
+
"scikit-learn>=1.3",
|
|
30
|
+
"cleanlab>=2.5",
|
|
31
|
+
"typer>=0.12",
|
|
32
|
+
"rich>=13.0",
|
|
33
|
+
"jinja2>=3.1",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = ["pytest>=8", "ruff>=0.5", "build", "twine", "pyarrow"]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
tabaudit = "tabaudit.cli:app"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/sadiasamia121912/tabaudit"
|
|
44
|
+
Issues = "https://github.com/sadiasamia121912/tabaudit/issues"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
"tabaudit.report" = ["*.html"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py310"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I", "UP", "B", "RUF", "SIM"]
|
|
58
|
+
ignore = [
|
|
59
|
+
"B008", # typer.Option() in defaults is the documented Typer idiom
|
|
60
|
+
"E501", # formatter owns line length
|
|
61
|
+
"RUF001", "RUF002", "RUF003", # we print unicode glyphs (×, →) on purpose
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
tabaudit-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""tabaudit — audit tabular ML datasets before you train."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from tabaudit.audit import run_audit
|
|
6
|
+
from tabaudit.findings import AuditReport, Finding, Severity
|
|
7
|
+
|
|
8
|
+
__all__ = ["AuditReport", "Finding", "Severity", "__version__", "run_audit"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Orchestrates loading, task inference and running every registered check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from tabaudit import __version__
|
|
12
|
+
from tabaudit.checks import REGISTRY
|
|
13
|
+
from tabaudit.context import AuditContext
|
|
14
|
+
from tabaudit.findings import AuditReport, CheckRun, DatasetSummary, Finding
|
|
15
|
+
from tabaudit.loader import dtype_kinds, infer_task, load_table
|
|
16
|
+
|
|
17
|
+
ProgressFn = Callable[[str, str], None] # (check_name, status) -> None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_audit(
|
|
21
|
+
data: str | Path | pd.DataFrame,
|
|
22
|
+
target: str | None = None,
|
|
23
|
+
test: str | Path | pd.DataFrame | None = None,
|
|
24
|
+
checks: Iterable[str] | None = None,
|
|
25
|
+
max_rows: int = 50_000,
|
|
26
|
+
random_state: int = 42,
|
|
27
|
+
on_progress: ProgressFn | None = None,
|
|
28
|
+
) -> AuditReport:
|
|
29
|
+
"""Audit a dataset and return an :class:`AuditReport`.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
data: path to CSV/Parquet/… or a DataFrame.
|
|
34
|
+
target: name of the label column (omit for unsupervised checks only).
|
|
35
|
+
test: optional held-out set, used to detect train/test contamination.
|
|
36
|
+
checks: subset of check names to run (default: all).
|
|
37
|
+
"""
|
|
38
|
+
df, path = (
|
|
39
|
+
(data, "<DataFrame>") if isinstance(data, pd.DataFrame) else (load_table(data), str(data))
|
|
40
|
+
)
|
|
41
|
+
test_df, test_path = (None, None)
|
|
42
|
+
if test is not None:
|
|
43
|
+
test_df, test_path = (
|
|
44
|
+
(test, "<DataFrame>")
|
|
45
|
+
if isinstance(test, pd.DataFrame)
|
|
46
|
+
else (load_table(test), str(test))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if target is not None and target not in df.columns:
|
|
50
|
+
close = [c for c in df.columns if c.lower() == target.lower()]
|
|
51
|
+
hint = f" Did you mean '{close[0]}'?" if close else ""
|
|
52
|
+
raise KeyError(f"Target column '{target}' not found.{hint}")
|
|
53
|
+
|
|
54
|
+
task = infer_task(df[target]) if target else "unsupervised"
|
|
55
|
+
n_classes = (
|
|
56
|
+
int(df[target].nunique(dropna=True)) if target and task == "classification" else None
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
ctx = AuditContext(
|
|
60
|
+
df=df,
|
|
61
|
+
target=target,
|
|
62
|
+
task=task,
|
|
63
|
+
test_df=test_df,
|
|
64
|
+
max_rows=max_rows,
|
|
65
|
+
random_state=random_state,
|
|
66
|
+
)
|
|
67
|
+
selected = list(checks) if checks else list(REGISTRY)
|
|
68
|
+
unknown = [c for c in selected if c not in REGISTRY]
|
|
69
|
+
if unknown:
|
|
70
|
+
raise ValueError(f"Unknown check(s): {unknown}. Available: {list(REGISTRY)}")
|
|
71
|
+
|
|
72
|
+
findings: list[Finding] = []
|
|
73
|
+
runs: list[CheckRun] = []
|
|
74
|
+
for name in selected:
|
|
75
|
+
fn = REGISTRY[name]
|
|
76
|
+
if on_progress:
|
|
77
|
+
on_progress(name, "running")
|
|
78
|
+
t0 = time.perf_counter()
|
|
79
|
+
try:
|
|
80
|
+
out = fn(ctx)
|
|
81
|
+
findings.extend(out)
|
|
82
|
+
runs.append(CheckRun(name, "ok", time.perf_counter() - t0, len(out)))
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
runs.append(
|
|
85
|
+
CheckRun(name, "error", time.perf_counter() - t0, 0, f"{type(exc).__name__}: {exc}")
|
|
86
|
+
)
|
|
87
|
+
if on_progress:
|
|
88
|
+
on_progress(name, runs[-1].status)
|
|
89
|
+
|
|
90
|
+
summary = DatasetSummary(
|
|
91
|
+
path=path,
|
|
92
|
+
n_rows=len(df),
|
|
93
|
+
n_cols=int(df.shape[1]),
|
|
94
|
+
target=target,
|
|
95
|
+
task=task,
|
|
96
|
+
n_classes=n_classes,
|
|
97
|
+
test_path=test_path,
|
|
98
|
+
n_test_rows=len(test_df) if test_df is not None else None,
|
|
99
|
+
memory_mb=round(float(df.memory_usage(deep=True).sum()) / 1e6, 2),
|
|
100
|
+
dtypes=dtype_kinds(df),
|
|
101
|
+
)
|
|
102
|
+
return AuditReport(summary=summary, findings=findings, checks=runs, version=__version__)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Check registry. Each check is a callable (AuditContext) -> list[Finding]."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from tabaudit.checks import duplicates, imbalance, label_noise, leakage, schema
|
|
8
|
+
from tabaudit.context import AuditContext
|
|
9
|
+
from tabaudit.findings import Finding
|
|
10
|
+
|
|
11
|
+
CheckFn = Callable[[AuditContext], list[Finding]]
|
|
12
|
+
|
|
13
|
+
# Ordered: cheap structural checks first, model-based checks last.
|
|
14
|
+
REGISTRY: dict[str, CheckFn] = {
|
|
15
|
+
"schema": schema.run,
|
|
16
|
+
"duplicates": duplicates.run,
|
|
17
|
+
"imbalance": imbalance.run,
|
|
18
|
+
"leakage": leakage.run,
|
|
19
|
+
"label_noise": label_noise.run,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
__all__ = ["REGISTRY", "CheckFn"]
|