Skip to main content

ml-feature-check

One call tells you which columns should not go into the model: the ones that carry no signal, the ones that repeat another column, the ones that already know the answer, and the ones that are really row identifiers - each explained, ranked by severity, and removable with a single report.apply(df).

Install

pip install ml-feature-check

Parquet input needs the optional extra: pip install "ml-feature-check[parquet]".

Quickstart

import pandas as pd
from ml_feature_check import check

df = pd.DataFrame({"row_id": range(60),
                   "age": [23, 34, 45, 31, 29] * 12,
                   "age_in_years": [23, 34, 45, 31, 29] * 12,
                   "country": ["IN"] * 60,
                   "signed_up": ["2024-01-05", "2024-02-11", "2024-03-18"] * 20,
                   "churn_flag": [0, 1] * 30,
                   "churned": [0, 1] * 30})
report = check(df, target="churned")
print(report.summary())
clean = report.apply(df)          # the same frame minus report.drop_recommended
ml-feature-check: 6 feature(s), 60 rows, target 'churned'
Drop recommended (4):
  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
                suspicious_name [low]            the name contains 'row', which usually marks a row identifier or bookkeeping column rather than a feature
  age_in_years  duplicate_of [high]              identical to column 'age': keep one of the two
  country       constant [high]                  only one distinct value ('IN')
  churn_flag    leakage_suspect [high]           a 1-feature decision tree reaches 1.000 accuracy on held-out rows for target 'churned' (baseline 0.500)
Worth a look (2):
  age           zero_importance [low]            a small random forest gives it no measurable importance (+0.0000 against a 0.0000 noise level)
  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
                zero_importance [low]            a small random forest gives it no measurable importance (+0.0000 against a 0.0000 noise level)
Keep (2): age, signed_up
Notes:
  - target 'churned' was treated as classification

report.features maps every feature column to its findings, report.drop_recommended is the ordered hit list, report.keep is the rest, and report.to_dict() / report.to_markdown() give you the same findings as JSON or as a report page.

What it checks

Every finding is a Finding with a kind. Passing target= switches on the two checks that need a label; everything else runs either way.

kind severity fires when
constant high the column has one distinct value, or is entirely missing
near_constant medium more than 99% of the non-missing values are the same one
high_missing medium (high above 95%) the missing share is above missing_threshold
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
duplicate_of high the column is value-for-value identical to an earlier column
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
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
date_as_string low at least 90% of a text column parses as a date
suspicious_name low the name contains id, uuid, index, key, timestamp, row or unnamed
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

What "drop" means. A high or medium finding puts the column on drop_recommended, ordered worst first; low findings leave it on keep and in report.review, the "worth a look" list. Nothing is ever removed for you until you call apply().

Checks that cannot fire twice. A constant column is not then tested for missingness or cardinality, a column already reported as a duplicate_of another is not also reported as correlated with it, and the column a duplicate points at is kept. So one problem produces one finding, on one column.

The survivor of a redundant group is a column worth keeping. When several columns carry the same signal, the one left on keep is the one with no drop-level finding of its own and the fewest missing values, not whichever the frame happened to list first - so a 90%-missing copy never displaces the complete column beside it. Columns that tie on both criteria fall back to frame order, so two equally good copies keep whichever the frame lists first; reordering the frame can swap which of that pair is dropped, and the report stays correct either way.

Leakage without false alarms. The single-feature tree is cross-fitted on two halves and scored only on rows it did not train on, and the categorical value-to-target mapping is scored leave-one-out - each row predicted from the other rows sharing its value. An identifier column therefore scores at chance instead of scoring perfectly, which is what a plain groupby accuracy would do.

Zero variance is not a warning. Correlation on a constant column yields no finding rather than a NaN one, and no numpy warning is emitted anywhere. Two columns are compared only when they share at least 20 non-missing rows (5% of the frame once that is larger), and when the overlap is partial the finding says how many rows it rests on - correlation +1.000 with column 'a' over 46 shared non-missing rows - with the count in detail["n_shared"].

Sampling. Frames longer than sample rows (200,000 by default) are checked on a seeded random sample; the forest behind zero_importance fits on at most 20,000 of those rows. report.n_rows, report.n_rows_checked and report.notes say exactly what happened. Smaller frames are read whole, so their counts are exact.

API

check(df, target=None, *, corr_threshold=0.95, missing_threshold=0.6,
      cardinality_threshold=0.98, sample=200_000, random_state=0) -> FeatureReport

df is a pandas.DataFrame or a path to a .csv, .tsv or .parquet file. target names the label column: it is never reported as a feature, and it unlocks leakage_suspect and zero_importance. A target that is not a column raises ValueError listing the columns that do exist, and so do duplicate column names. The input frame is never modified.

FeatureReport

  • .features - dict[column -> list[Finding]], every feature column, worst finding first; an empty list means the column is clean
  • .drop_recommended - columns with a high or medium finding, most severe first
  • .keep - the remaining columns, in the original order
  • .review - kept columns that still have a low-severity finding
  • .flagged - every column with any finding; .columns_with(kind) - filtered by kind
  • .apply(df) - a copy of df without the drop_recommended columns; a frame that is missing those columns is logged as a warning on the ml_feature_check logger rather than passed over in silence
  • .iter_findings() - (column, finding) pairs, most severe first
  • .target, .n_features, .n_rows, .n_rows_checked, .params
  • .notes - what was skipped or sampled and why
  • .summary() - human text; .to_dict() - JSON-safe dict; .to_markdown() - a report page

Finding(kind, severity, message, detail)

kind is one of ml_feature_check.KINDS, severity is "high", "medium" or "low", and detail is a JSON-safe dict with the numbers behind the message - the other column, the correlation, the score, the threshold that was crossed.

FeatureChecker(*, corr_threshold=0.95, missing_threshold=0.6, cardinality_threshold=0.98,
               sample=200_000, random_state=0, near_constant_threshold=0.99,
               leakage_score_threshold=0.99, leakage_corr_threshold=0.95, min_id_rows=20,
               max_model_rows=20_000, max_model_categories=100, max_pair_columns=400)
FeatureChecker.check(df, target=None) -> FeatureReport

The same run with every threshold exposed, for reuse across many frames.

Without a target the two label-dependent checks are skipped and a note says so; everything else still runs. The library never prints - it logs through logging.getLogger("ml_feature_check").

CLI

ml-feature-check train.csv                       # print the summary
ml-feature-check train.csv --target churned      # add the leakage and importance checks
ml-feature-check train.csv --json                # print to_dict() as JSON
ml-feature-check train.csv --markdown            # print to_markdown()
ml-feature-check train.csv --output report.md    # also write it (.json/.md/.txt by suffix)
ml-feature-check train.csv --apply clean.csv     # write the frame without the dropped columns
ml-feature-check train.csv --sample 50000 --corr-threshold 0.9
ml-feature-check --help

python -m ml_feature_check train.csv works the same way. The command exits 1 with a one-line message on a bad file, a missing target or a bad threshold.

License

MIT

Metadata

Release files for ml-feature-check 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ml-feature-check 0.1.0
File Size Uploaded
ml_feature_check-0.1.0.tar.gz 42.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ml-feature-check 0.1.0
File Interpreter ABI Platform
ml_feature_check-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 75.3 kB

Release files / ml_feature_check-0.1.0.tar.gz

Download URL ml_feature_check-0.1.0.tar.gz
Size 42.2 kB
Tags Source
SHA-256 checksum
How to use checksums
d9b9f4dbbd9292874f2689f592e30478b233b949772d766c4bc02352a32e858c
BLAKE2b-256 checksum
How to use checksums
a227ba016072db8185922e0d29f904804de3763f1a8e16e4220345453287fa75
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.11

Release files / ml_feature_check-0.1.0-py3-none-any.whl

Download URL ml_feature_check-0.1.0-py3-none-any.whl
Size 33.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1c2d4b1f3fb7a1b753d9b4d9e912f616d573eea769bd5a5a3c31cda8a0849c0d
BLAKE2b-256 checksum
How to use checksums
d557cda07eb685dd3c8b7d31c88110c6264f9aaf1906e078570e1f3b3245c614
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.11

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page