ml-feature-check 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.
- ml_feature_check-0.1.0/LICENSE +21 -0
- ml_feature_check-0.1.0/PKG-INFO +203 -0
- ml_feature_check-0.1.0/README.md +176 -0
- ml_feature_check-0.1.0/pyproject.toml +60 -0
- ml_feature_check-0.1.0/src/ml_feature_check/__init__.py +21 -0
- ml_feature_check-0.1.0/src/ml_feature_check/__main__.py +6 -0
- ml_feature_check-0.1.0/src/ml_feature_check/_columns.py +236 -0
- ml_feature_check-0.1.0/src/ml_feature_check/_io.py +70 -0
- ml_feature_check-0.1.0/src/ml_feature_check/_models.py +112 -0
- ml_feature_check-0.1.0/src/ml_feature_check/_stats.py +229 -0
- ml_feature_check-0.1.0/src/ml_feature_check/checker.py +933 -0
- ml_feature_check-0.1.0/src/ml_feature_check/cli.py +139 -0
- ml_feature_check-0.1.0/src/ml_feature_check/report.py +276 -0
- ml_feature_check-0.1.0/tests/conftest.py +46 -0
- ml_feature_check-0.1.0/tests/test_checks.py +585 -0
- ml_feature_check-0.1.0/tests/test_cli.py +169 -0
- ml_feature_check-0.1.0/tests/test_edge_cases.py +460 -0
- ml_feature_check-0.1.0/tests/test_report.py +255 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pranay Mahendrakar
|
|
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,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ml-feature-check
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Catch useless, redundant, leaking and suspicious features before you train on them
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/ml-feature-check/
|
|
6
|
+
Project-URL: Author, https://pypi.org/user/pranaymahendrakar/
|
|
7
|
+
Author: Pranay Mahendrakar
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: data-leakage,data-quality,feature-selection,machine-learning,pandas,preprocessing,tabular
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: numpy>=1.23
|
|
20
|
+
Requires-Dist: pandas>=1.5
|
|
21
|
+
Requires-Dist: scikit-learn>=1.1
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
24
|
+
Provides-Extra: parquet
|
|
25
|
+
Requires-Dist: pyarrow>=12; extra == 'parquet'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# ml-feature-check
|
|
29
|
+
|
|
30
|
+
One call tells you which columns should not go into the model: the ones that
|
|
31
|
+
carry no signal, the ones that repeat another column, the ones that already know
|
|
32
|
+
the answer, and the ones that are really row identifiers - each explained, ranked
|
|
33
|
+
by severity, and removable with a single `report.apply(df)`.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
pip install ml-feature-check
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Parquet input needs the optional extra: `pip install "ml-feature-check[parquet]"`.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import pandas as pd
|
|
47
|
+
from ml_feature_check import check
|
|
48
|
+
|
|
49
|
+
df = pd.DataFrame({"row_id": range(60),
|
|
50
|
+
"age": [23, 34, 45, 31, 29] * 12,
|
|
51
|
+
"age_in_years": [23, 34, 45, 31, 29] * 12,
|
|
52
|
+
"country": ["IN"] * 60,
|
|
53
|
+
"signed_up": ["2024-01-05", "2024-02-11", "2024-03-18"] * 20,
|
|
54
|
+
"churn_flag": [0, 1] * 30,
|
|
55
|
+
"churned": [0, 1] * 30})
|
|
56
|
+
report = check(df, target="churned")
|
|
57
|
+
print(report.summary())
|
|
58
|
+
clean = report.apply(df) # the same frame minus report.drop_recommended
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
ml-feature-check: 6 feature(s), 60 rows, target 'churned'
|
|
63
|
+
Drop recommended (4):
|
|
64
|
+
row_id id_like [high] 100.0% of the 60 values are distinct and they run consecutively: looks like a row identifier, not a feature
|
|
65
|
+
suspicious_name [low] the name contains 'row', which usually marks a row identifier or bookkeeping column rather than a feature
|
|
66
|
+
age_in_years duplicate_of [high] identical to column 'age': keep one of the two
|
|
67
|
+
country constant [high] only one distinct value ('IN')
|
|
68
|
+
churn_flag leakage_suspect [high] a 1-feature decision tree reaches 1.000 accuracy on held-out rows for target 'churned' (baseline 0.500)
|
|
69
|
+
Worth a look (2):
|
|
70
|
+
age zero_importance [low] a small random forest gives it no measurable importance (+0.0000 against a 0.0000 noise level)
|
|
71
|
+
signed_up date_as_string [low] text that parses as dates (for example '2024-01-05'): convert with pandas.to_datetime and feed the parts (year, month, weekday) to the model
|
|
72
|
+
zero_importance [low] a small random forest gives it no measurable importance (+0.0000 against a 0.0000 noise level)
|
|
73
|
+
Keep (2): age, signed_up
|
|
74
|
+
Notes:
|
|
75
|
+
- target 'churned' was treated as classification
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`report.features` maps every feature column to its findings, `report.drop_recommended`
|
|
79
|
+
is the ordered hit list, `report.keep` is the rest, and `report.to_dict()` /
|
|
80
|
+
`report.to_markdown()` give you the same findings as JSON or as a report page.
|
|
81
|
+
|
|
82
|
+
## What it checks
|
|
83
|
+
|
|
84
|
+
Every finding is a `Finding` with a `kind`. Passing `target=` switches on the two
|
|
85
|
+
checks that need a label; everything else runs either way.
|
|
86
|
+
|
|
87
|
+
| kind | severity | fires when |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `constant` | high | the column has one distinct value, or is entirely missing |
|
|
90
|
+
| `near_constant` | medium | more than 99% of the non-missing values are the same one |
|
|
91
|
+
| `high_missing` | medium (high above 95%) | the missing share is above `missing_threshold` |
|
|
92
|
+
| `id_like` | high (medium for timestamps) | more than `cardinality_threshold` of the values are distinct, and the column is text, integer or datetime rather than a continuous measurement |
|
|
93
|
+
| `duplicate_of` | high | the column is value-for-value identical to an earlier column |
|
|
94
|
+
| `highly_correlated_with` | medium | numeric: `abs(pearson)` above `corr_threshold` with a column kept earlier; categorical: bias-corrected Cramer's V above the same threshold |
|
|
95
|
+
| `leakage_suspect` | high | with a target: `abs(corr)` above 0.95 for a numeric target, a cross-fitted 1-feature decision tree above 0.99 accuracy / R2, or a categorical column whose values map onto the target almost one-to-one |
|
|
96
|
+
| `date_as_string` | low | at least 90% of a text column parses as a date |
|
|
97
|
+
| `suspicious_name` | low | the name contains `id`, `uuid`, `index`, `key`, `timestamp`, `row` or `unnamed` |
|
|
98
|
+
| `zero_importance` | low | with a target: a small random forest gives the column no more holdout permutation importance than a shuffled copy of a real column |
|
|
99
|
+
|
|
100
|
+
**What "drop" means.** A `high` or `medium` finding puts the column on
|
|
101
|
+
`drop_recommended`, ordered worst first; `low` findings leave it on `keep` and in
|
|
102
|
+
`report.review`, the "worth a look" list. Nothing is ever removed for you until
|
|
103
|
+
you call `apply()`.
|
|
104
|
+
|
|
105
|
+
**Checks that cannot fire twice.** A constant column is not then tested for
|
|
106
|
+
missingness or cardinality, a column already reported as a `duplicate_of` another
|
|
107
|
+
is not also reported as correlated with it, and the column a duplicate points at
|
|
108
|
+
is kept. So one problem produces one finding, on one column.
|
|
109
|
+
|
|
110
|
+
**The survivor of a redundant group is a column worth keeping.** When several
|
|
111
|
+
columns carry the same signal, the one left on `keep` is the one with no
|
|
112
|
+
drop-level finding of its own and the fewest missing values, not whichever the
|
|
113
|
+
frame happened to list first - so a 90%-missing copy never displaces the complete
|
|
114
|
+
column beside it. Columns that tie on both criteria fall back to frame order, so
|
|
115
|
+
two equally good copies keep whichever the frame lists first; reordering the
|
|
116
|
+
frame can swap which of that pair is dropped, and the report stays correct
|
|
117
|
+
either way.
|
|
118
|
+
|
|
119
|
+
**Leakage without false alarms.** The single-feature tree is cross-fitted on two
|
|
120
|
+
halves and scored only on rows it did not train on, and the categorical
|
|
121
|
+
value-to-target mapping is scored leave-one-out - each row predicted from the
|
|
122
|
+
*other* rows sharing its value. An identifier column therefore scores at chance
|
|
123
|
+
instead of scoring perfectly, which is what a plain `groupby` accuracy would do.
|
|
124
|
+
|
|
125
|
+
**Zero variance is not a warning.** Correlation on a constant column yields no
|
|
126
|
+
finding rather than a NaN one, and no numpy warning is emitted anywhere. Two
|
|
127
|
+
columns are compared only when they share at least 20 non-missing rows (5% of the
|
|
128
|
+
frame once that is larger), and when the overlap is partial the finding says how
|
|
129
|
+
many rows it rests on - `correlation +1.000 with column 'a' over 46 shared
|
|
130
|
+
non-missing rows` - with the count in `detail["n_shared"]`.
|
|
131
|
+
|
|
132
|
+
**Sampling.** Frames longer than `sample` rows (200,000 by default) are checked on
|
|
133
|
+
a seeded random sample; the forest behind `zero_importance` fits on at most 20,000
|
|
134
|
+
of those rows. `report.n_rows`, `report.n_rows_checked` and `report.notes` say
|
|
135
|
+
exactly what happened. Smaller frames are read whole, so their counts are exact.
|
|
136
|
+
|
|
137
|
+
## API
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
check(df, target=None, *, corr_threshold=0.95, missing_threshold=0.6,
|
|
141
|
+
cardinality_threshold=0.98, sample=200_000, random_state=0) -> FeatureReport
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`df` is a `pandas.DataFrame` or a path to a `.csv`, `.tsv` or `.parquet` file.
|
|
145
|
+
`target` names the label column: it is never reported as a feature, and it
|
|
146
|
+
unlocks `leakage_suspect` and `zero_importance`. A target that is not a column
|
|
147
|
+
raises `ValueError` listing the columns that do exist, and so do duplicate column
|
|
148
|
+
names. The input frame is never modified.
|
|
149
|
+
|
|
150
|
+
`FeatureReport`
|
|
151
|
+
|
|
152
|
+
- `.features` - `dict[column -> list[Finding]]`, every feature column, worst finding first; an empty list means the column is clean
|
|
153
|
+
- `.drop_recommended` - columns with a high or medium finding, most severe first
|
|
154
|
+
- `.keep` - the remaining columns, in the original order
|
|
155
|
+
- `.review` - kept columns that still have a low-severity finding
|
|
156
|
+
- `.flagged` - every column with any finding; `.columns_with(kind)` - filtered by kind
|
|
157
|
+
- `.apply(df)` - a copy of `df` without the `drop_recommended` columns; a frame
|
|
158
|
+
that is missing those columns is logged as a warning on the `ml_feature_check`
|
|
159
|
+
logger rather than passed over in silence
|
|
160
|
+
- `.iter_findings()` - `(column, finding)` pairs, most severe first
|
|
161
|
+
- `.target`, `.n_features`, `.n_rows`, `.n_rows_checked`, `.params`
|
|
162
|
+
- `.notes` - what was skipped or sampled and why
|
|
163
|
+
- `.summary()` - human text; `.to_dict()` - JSON-safe dict; `.to_markdown()` - a report page
|
|
164
|
+
|
|
165
|
+
`Finding(kind, severity, message, detail)`
|
|
166
|
+
|
|
167
|
+
`kind` is one of `ml_feature_check.KINDS`, `severity` is `"high"`, `"medium"` or
|
|
168
|
+
`"low"`, and `detail` is a JSON-safe dict with the numbers behind the message -
|
|
169
|
+
the other column, the correlation, the score, the threshold that was crossed.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
FeatureChecker(*, corr_threshold=0.95, missing_threshold=0.6, cardinality_threshold=0.98,
|
|
173
|
+
sample=200_000, random_state=0, near_constant_threshold=0.99,
|
|
174
|
+
leakage_score_threshold=0.99, leakage_corr_threshold=0.95, min_id_rows=20,
|
|
175
|
+
max_model_rows=20_000, max_model_categories=100, max_pair_columns=400)
|
|
176
|
+
FeatureChecker.check(df, target=None) -> FeatureReport
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The same run with every threshold exposed, for reuse across many frames.
|
|
180
|
+
|
|
181
|
+
Without a target the two label-dependent checks are skipped and a note says so;
|
|
182
|
+
everything else still runs. The library never prints - it logs through
|
|
183
|
+
`logging.getLogger("ml_feature_check")`.
|
|
184
|
+
|
|
185
|
+
## CLI
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
ml-feature-check train.csv # print the summary
|
|
189
|
+
ml-feature-check train.csv --target churned # add the leakage and importance checks
|
|
190
|
+
ml-feature-check train.csv --json # print to_dict() as JSON
|
|
191
|
+
ml-feature-check train.csv --markdown # print to_markdown()
|
|
192
|
+
ml-feature-check train.csv --output report.md # also write it (.json/.md/.txt by suffix)
|
|
193
|
+
ml-feature-check train.csv --apply clean.csv # write the frame without the dropped columns
|
|
194
|
+
ml-feature-check train.csv --sample 50000 --corr-threshold 0.9
|
|
195
|
+
ml-feature-check --help
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`python -m ml_feature_check train.csv` works the same way. The command exits 1
|
|
199
|
+
with a one-line message on a bad file, a missing target or a bad threshold.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# ml-feature-check
|
|
2
|
+
|
|
3
|
+
One call tells you which columns should not go into the model: the ones that
|
|
4
|
+
carry no signal, the ones that repeat another column, the ones that already know
|
|
5
|
+
the answer, and the ones that are really row identifiers - each explained, ranked
|
|
6
|
+
by severity, and removable with a single `report.apply(df)`.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install ml-feature-check
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Parquet input needs the optional extra: `pip install "ml-feature-check[parquet]"`.
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import pandas as pd
|
|
20
|
+
from ml_feature_check import check
|
|
21
|
+
|
|
22
|
+
df = pd.DataFrame({"row_id": range(60),
|
|
23
|
+
"age": [23, 34, 45, 31, 29] * 12,
|
|
24
|
+
"age_in_years": [23, 34, 45, 31, 29] * 12,
|
|
25
|
+
"country": ["IN"] * 60,
|
|
26
|
+
"signed_up": ["2024-01-05", "2024-02-11", "2024-03-18"] * 20,
|
|
27
|
+
"churn_flag": [0, 1] * 30,
|
|
28
|
+
"churned": [0, 1] * 30})
|
|
29
|
+
report = check(df, target="churned")
|
|
30
|
+
print(report.summary())
|
|
31
|
+
clean = report.apply(df) # the same frame minus report.drop_recommended
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
ml-feature-check: 6 feature(s), 60 rows, target 'churned'
|
|
36
|
+
Drop recommended (4):
|
|
37
|
+
row_id id_like [high] 100.0% of the 60 values are distinct and they run consecutively: looks like a row identifier, not a feature
|
|
38
|
+
suspicious_name [low] the name contains 'row', which usually marks a row identifier or bookkeeping column rather than a feature
|
|
39
|
+
age_in_years duplicate_of [high] identical to column 'age': keep one of the two
|
|
40
|
+
country constant [high] only one distinct value ('IN')
|
|
41
|
+
churn_flag leakage_suspect [high] a 1-feature decision tree reaches 1.000 accuracy on held-out rows for target 'churned' (baseline 0.500)
|
|
42
|
+
Worth a look (2):
|
|
43
|
+
age zero_importance [low] a small random forest gives it no measurable importance (+0.0000 against a 0.0000 noise level)
|
|
44
|
+
signed_up date_as_string [low] text that parses as dates (for example '2024-01-05'): convert with pandas.to_datetime and feed the parts (year, month, weekday) to the model
|
|
45
|
+
zero_importance [low] a small random forest gives it no measurable importance (+0.0000 against a 0.0000 noise level)
|
|
46
|
+
Keep (2): age, signed_up
|
|
47
|
+
Notes:
|
|
48
|
+
- target 'churned' was treated as classification
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`report.features` maps every feature column to its findings, `report.drop_recommended`
|
|
52
|
+
is the ordered hit list, `report.keep` is the rest, and `report.to_dict()` /
|
|
53
|
+
`report.to_markdown()` give you the same findings as JSON or as a report page.
|
|
54
|
+
|
|
55
|
+
## What it checks
|
|
56
|
+
|
|
57
|
+
Every finding is a `Finding` with a `kind`. Passing `target=` switches on the two
|
|
58
|
+
checks that need a label; everything else runs either way.
|
|
59
|
+
|
|
60
|
+
| kind | severity | fires when |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `constant` | high | the column has one distinct value, or is entirely missing |
|
|
63
|
+
| `near_constant` | medium | more than 99% of the non-missing values are the same one |
|
|
64
|
+
| `high_missing` | medium (high above 95%) | the missing share is above `missing_threshold` |
|
|
65
|
+
| `id_like` | high (medium for timestamps) | more than `cardinality_threshold` of the values are distinct, and the column is text, integer or datetime rather than a continuous measurement |
|
|
66
|
+
| `duplicate_of` | high | the column is value-for-value identical to an earlier column |
|
|
67
|
+
| `highly_correlated_with` | medium | numeric: `abs(pearson)` above `corr_threshold` with a column kept earlier; categorical: bias-corrected Cramer's V above the same threshold |
|
|
68
|
+
| `leakage_suspect` | high | with a target: `abs(corr)` above 0.95 for a numeric target, a cross-fitted 1-feature decision tree above 0.99 accuracy / R2, or a categorical column whose values map onto the target almost one-to-one |
|
|
69
|
+
| `date_as_string` | low | at least 90% of a text column parses as a date |
|
|
70
|
+
| `suspicious_name` | low | the name contains `id`, `uuid`, `index`, `key`, `timestamp`, `row` or `unnamed` |
|
|
71
|
+
| `zero_importance` | low | with a target: a small random forest gives the column no more holdout permutation importance than a shuffled copy of a real column |
|
|
72
|
+
|
|
73
|
+
**What "drop" means.** A `high` or `medium` finding puts the column on
|
|
74
|
+
`drop_recommended`, ordered worst first; `low` findings leave it on `keep` and in
|
|
75
|
+
`report.review`, the "worth a look" list. Nothing is ever removed for you until
|
|
76
|
+
you call `apply()`.
|
|
77
|
+
|
|
78
|
+
**Checks that cannot fire twice.** A constant column is not then tested for
|
|
79
|
+
missingness or cardinality, a column already reported as a `duplicate_of` another
|
|
80
|
+
is not also reported as correlated with it, and the column a duplicate points at
|
|
81
|
+
is kept. So one problem produces one finding, on one column.
|
|
82
|
+
|
|
83
|
+
**The survivor of a redundant group is a column worth keeping.** When several
|
|
84
|
+
columns carry the same signal, the one left on `keep` is the one with no
|
|
85
|
+
drop-level finding of its own and the fewest missing values, not whichever the
|
|
86
|
+
frame happened to list first - so a 90%-missing copy never displaces the complete
|
|
87
|
+
column beside it. Columns that tie on both criteria fall back to frame order, so
|
|
88
|
+
two equally good copies keep whichever the frame lists first; reordering the
|
|
89
|
+
frame can swap which of that pair is dropped, and the report stays correct
|
|
90
|
+
either way.
|
|
91
|
+
|
|
92
|
+
**Leakage without false alarms.** The single-feature tree is cross-fitted on two
|
|
93
|
+
halves and scored only on rows it did not train on, and the categorical
|
|
94
|
+
value-to-target mapping is scored leave-one-out - each row predicted from the
|
|
95
|
+
*other* rows sharing its value. An identifier column therefore scores at chance
|
|
96
|
+
instead of scoring perfectly, which is what a plain `groupby` accuracy would do.
|
|
97
|
+
|
|
98
|
+
**Zero variance is not a warning.** Correlation on a constant column yields no
|
|
99
|
+
finding rather than a NaN one, and no numpy warning is emitted anywhere. Two
|
|
100
|
+
columns are compared only when they share at least 20 non-missing rows (5% of the
|
|
101
|
+
frame once that is larger), and when the overlap is partial the finding says how
|
|
102
|
+
many rows it rests on - `correlation +1.000 with column 'a' over 46 shared
|
|
103
|
+
non-missing rows` - with the count in `detail["n_shared"]`.
|
|
104
|
+
|
|
105
|
+
**Sampling.** Frames longer than `sample` rows (200,000 by default) are checked on
|
|
106
|
+
a seeded random sample; the forest behind `zero_importance` fits on at most 20,000
|
|
107
|
+
of those rows. `report.n_rows`, `report.n_rows_checked` and `report.notes` say
|
|
108
|
+
exactly what happened. Smaller frames are read whole, so their counts are exact.
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
check(df, target=None, *, corr_threshold=0.95, missing_threshold=0.6,
|
|
114
|
+
cardinality_threshold=0.98, sample=200_000, random_state=0) -> FeatureReport
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`df` is a `pandas.DataFrame` or a path to a `.csv`, `.tsv` or `.parquet` file.
|
|
118
|
+
`target` names the label column: it is never reported as a feature, and it
|
|
119
|
+
unlocks `leakage_suspect` and `zero_importance`. A target that is not a column
|
|
120
|
+
raises `ValueError` listing the columns that do exist, and so do duplicate column
|
|
121
|
+
names. The input frame is never modified.
|
|
122
|
+
|
|
123
|
+
`FeatureReport`
|
|
124
|
+
|
|
125
|
+
- `.features` - `dict[column -> list[Finding]]`, every feature column, worst finding first; an empty list means the column is clean
|
|
126
|
+
- `.drop_recommended` - columns with a high or medium finding, most severe first
|
|
127
|
+
- `.keep` - the remaining columns, in the original order
|
|
128
|
+
- `.review` - kept columns that still have a low-severity finding
|
|
129
|
+
- `.flagged` - every column with any finding; `.columns_with(kind)` - filtered by kind
|
|
130
|
+
- `.apply(df)` - a copy of `df` without the `drop_recommended` columns; a frame
|
|
131
|
+
that is missing those columns is logged as a warning on the `ml_feature_check`
|
|
132
|
+
logger rather than passed over in silence
|
|
133
|
+
- `.iter_findings()` - `(column, finding)` pairs, most severe first
|
|
134
|
+
- `.target`, `.n_features`, `.n_rows`, `.n_rows_checked`, `.params`
|
|
135
|
+
- `.notes` - what was skipped or sampled and why
|
|
136
|
+
- `.summary()` - human text; `.to_dict()` - JSON-safe dict; `.to_markdown()` - a report page
|
|
137
|
+
|
|
138
|
+
`Finding(kind, severity, message, detail)`
|
|
139
|
+
|
|
140
|
+
`kind` is one of `ml_feature_check.KINDS`, `severity` is `"high"`, `"medium"` or
|
|
141
|
+
`"low"`, and `detail` is a JSON-safe dict with the numbers behind the message -
|
|
142
|
+
the other column, the correlation, the score, the threshold that was crossed.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
FeatureChecker(*, corr_threshold=0.95, missing_threshold=0.6, cardinality_threshold=0.98,
|
|
146
|
+
sample=200_000, random_state=0, near_constant_threshold=0.99,
|
|
147
|
+
leakage_score_threshold=0.99, leakage_corr_threshold=0.95, min_id_rows=20,
|
|
148
|
+
max_model_rows=20_000, max_model_categories=100, max_pair_columns=400)
|
|
149
|
+
FeatureChecker.check(df, target=None) -> FeatureReport
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The same run with every threshold exposed, for reuse across many frames.
|
|
153
|
+
|
|
154
|
+
Without a target the two label-dependent checks are skipped and a note says so;
|
|
155
|
+
everything else still runs. The library never prints - it logs through
|
|
156
|
+
`logging.getLogger("ml_feature_check")`.
|
|
157
|
+
|
|
158
|
+
## CLI
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
ml-feature-check train.csv # print the summary
|
|
162
|
+
ml-feature-check train.csv --target churned # add the leakage and importance checks
|
|
163
|
+
ml-feature-check train.csv --json # print to_dict() as JSON
|
|
164
|
+
ml-feature-check train.csv --markdown # print to_markdown()
|
|
165
|
+
ml-feature-check train.csv --output report.md # also write it (.json/.md/.txt by suffix)
|
|
166
|
+
ml-feature-check train.csv --apply clean.csv # write the frame without the dropped columns
|
|
167
|
+
ml-feature-check train.csv --sample 50000 --corr-threshold 0.9
|
|
168
|
+
ml-feature-check --help
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`python -m ml_feature_check train.csv` works the same way. The command exits 1
|
|
172
|
+
with a one-line message on a bad file, a missing target or a bad threshold.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ml-feature-check"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Catch useless, redundant, leaking and suspicious features before you train on them"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Pranay Mahendrakar" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"machine-learning",
|
|
16
|
+
"feature-selection",
|
|
17
|
+
"data-leakage",
|
|
18
|
+
"data-quality",
|
|
19
|
+
"pandas",
|
|
20
|
+
"preprocessing",
|
|
21
|
+
"tabular",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"pandas>=1.5",
|
|
34
|
+
"numpy>=1.23",
|
|
35
|
+
"scikit-learn>=1.1",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
parquet = ["pyarrow>=12"]
|
|
40
|
+
dev = ["pytest>=7"]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
ml-feature-check = "ml_feature_check.cli:main"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://pypi.org/project/ml-feature-check/"
|
|
47
|
+
Author = "https://pypi.org/user/pranaymahendrakar/"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/ml_feature_check"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
filterwarnings = [
|
|
55
|
+
"error::RuntimeWarning",
|
|
56
|
+
"error::FutureWarning",
|
|
57
|
+
"error::DeprecationWarning",
|
|
58
|
+
"error::PendingDeprecationWarning",
|
|
59
|
+
"error::UserWarning",
|
|
60
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Catch useless, redundant, leaking and suspicious features before you train on them.
|
|
2
|
+
|
|
3
|
+
>>> from ml_feature_check import check
|
|
4
|
+
>>> report = check(df, target="label")
|
|
5
|
+
>>> print(report.summary())
|
|
6
|
+
>>> clean = report.apply(df)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .checker import KINDS, FeatureChecker, check
|
|
10
|
+
from .report import FeatureReport, Finding
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"check",
|
|
16
|
+
"FeatureChecker",
|
|
17
|
+
"FeatureReport",
|
|
18
|
+
"Finding",
|
|
19
|
+
"KINDS",
|
|
20
|
+
"__version__",
|
|
21
|
+
]
|