NullSweep
Diagnose missing data before you impute it. NullSweep reads the shape and the mechanism of the holes in your DataFrame, then gives you a single interface to fill them with a method the evidence actually justifies.
Works with pandas and Polars — both installed by default, no extras to remember.
Why
df.fillna(df.mean()) is a guess. Two questions, answerable straight from the data, turn it into a decision:
| Question | What it tells you | API |
|---|---|---|
| Where are the gaps? (pattern) | Which methods are computationally applicable | detect_global_pattern |
| Why are they there? (mechanism) | Whether a fill will be honest or biased | detect_feature_pattern |
| What now? | The smallest tool the evidence supports | impute_nulls |
Pattern is about tractability; mechanism is about correctness. You need both.
Installation
pip install nullsweep
A single batteries-included install: pandas and Polars support, the KNN/MICE/regression imputers, the MAR/MCAR statistical tests, and the visualizations all come with it.
Quickstart
import nullsweep as ns
shape, details = ns.detect_global_pattern(df) # "univariate" | "monotone" | "non-monotone"
mech, details = ns.detect_feature_pattern(df, "income") # "MAR_evidence" | "MCAR_consistent" | "undetermined"
ns.plot_missing_values(df, "matrix") # look before you test
df = ns.impute_nulls(df, column="income", strategy="mice") # strategy chosen from the evidence
impute_nulls returns a new frame by default — your original is never mutated.
API
| Function | Purpose |
|---|---|
detect_global_pattern(df) |
Classify the dataset's missingness arrangement |
detect_feature_pattern(df, column, ...) |
Classify one column's missingness mechanism (MAR screen + Little's MCAR test) |
impute_nulls(df, column, strategy, ...) |
One-shot imputation across all strategies |
NullSweepImputer(...) |
Stateful fit/transform transformer for train/test workflows |
plot_missing_values(df, plot_type, ...) |
Nine views of the missingness, returns a Matplotlib figure |
Detecting patterns (where)
pattern, details = ns.detect_global_pattern(df)
| Pattern | Structure | What it unlocks |
|---|---|---|
univariate |
One column missing, the rest complete | A single prediction problem — one model, done |
monotone |
Nested, orderable holes (study drop-out) | A sequence of ordinary regressions — one deterministic pass |
non-monotone |
Scattered, circular dependencies | Iterative methods — mice, knn |
For monotone, details["matrix"] shows the nesting: each cell answers "whenever the row variable is missing, is the column variable also missing?"
A B C
A False True True # A missing ⇒ B and C missing
B False False True # A ⊆ B ⊆ C — that nesting is monotonicity
C False False False
For univariate, details["column"] names the offending column.
Detecting mechanisms (why)
label, details = ns.detect_feature_pattern(df, "target")
The column's missingness indicator is screened against every other column with logistic regression. If nothing predicts it, Little's (1988) MCAR test runs on the numeric columns to separate "looks random" from "we can't tell."
| Label | What it means | What to do |
|---|---|---|
MAR_evidence |
An observed column predicts the missingness | Impute with a model that conditions on it — regression, mice, knn |
MCAR_consistent |
No predictor, and MCAR could not be rejected | Simple fills or deletion are safe; nothing fancy required |
undetermined |
No predictor, and MCAR was rejected or untestable | Don't assume — suspect MNAR; flag it, or run a sensitivity analysis |
MCAR_consistent means failed to reject, not proven. details carries mar_predictors (a per-predictor evidence map) and, when it ran, mcar_test (statistic, degrees of freedom, p-value, and a plain-language message).
Useful options:
# Catch confounded MAR with a single joint model instead of marginal screens
ns.detect_feature_pattern(df, "target", multivariate=True)
# The pseudo R-squared gate (default 0.2) is deliberately strong and can hide
# weak-but-real signals. Lower it to increase sensitivity:
ns.detect_feature_pattern(df, "target", pseudo_r_squared_threshold=0.05)
# Numeric predictors only / skip the MCAR step
ns.detect_feature_pattern(df, "target", include_categorical=False, run_mcar_test=False)
Little's test is also available directly: from nullsweep.patterns.mcar.little import little_mcar_test.
Imputing (what)
df = ns.impute_nulls(df, column="age", strategy="mean")
df = ns.impute_nulls(df, column=["age", "income"], strategy="mice")
df = ns.impute_nulls(df) # strategy="auto" on every column with gaps
df = ns.impute_nulls(df, strategy="listwise", threshold=2) # drop rows with >= 2 missing values
df = ns.impute_nulls(df, column="city", strategy="constant", fill_value="Unknown")
Strategy families
| Family | Strategies | For | Best when |
|---|---|---|---|
| Statistical | mean, median |
numeric | MCAR — a central value is unbiased |
| Directional | interpolate, forwardfill, backfill |
ordered numeric/categorical | time series, sorted data |
| Model-based | knn, mice, regression |
numeric | MAR — condition on the other columns |
| Frequency | most_frequent, least_frequent, constant |
categorical | filling labels |
| Structural | flag, delete_column, listwise |
any | mark or remove instead of inventing |
| Automatic | auto |
any | let each column's type and shape pick |
Notes worth knowing:
knn/mice/regressionoperate on numeric columns and reject non-numeric targets. When a target column is given, complete numeric predictors are still used as context but only the target is written back. Encode categoricals first, or use a frequency strategy.flagadds a<column>_missingindicator per column. Withcolumn=Noneit flags only columns that have gaps;include_all_columns=Trueflags every column.delete_column/listwisetake athreshold: floats are missing-value proportions, integers are counts, and equality is deleted.- Directional fills leave edge gaps —
backfillcannot fill a trailing NaN. Check the result rather than assuming it is complete. autouses interpolation for continuous columns only when the index is datetime/timedelta (override withstrategy_params={"allow_ordered_interpolation": True}), and treats integer/boolean-coded numeric columns as categorical (disable with{"detect_numeric_categorical": False}).
Parameters
| Parameter | Type | Description |
|---|---|---|
df |
pd.DataFrame | pl.DataFrame |
Input frame. Must not be empty. |
column |
str | Iterable[str] | None |
Target column(s). None selects every column with missing values. |
strategy |
str |
One of the strategies above. Defaults to "auto". |
fill_value |
Any |
Constant used when strategy="constant". |
strategy_params |
dict | None |
Strategy configuration, e.g. {"method": "linear", "order": 2} for interpolate. |
in_place |
bool |
Mutate the input instead of returning a copy (pandas only; Polars warns and returns a new frame). Defaults to False. |
**kwargs |
Any |
Handler-specific options, e.g. n_neighbors for knn, threshold for listwise. |
Train/test workflows
impute_nulls recomputes its fill values from whatever frame it is handed — applying it to a test set leaks. Use the transformer instead:
from nullsweep import NullSweepImputer
imputer = NullSweepImputer(column="income", strategy="mean").fit(train)
train_imputed = imputer.transform(train)
test_imputed = imputer.transform(test) # filled with TRAIN's statistics
It learns means, medians, modes, the KNN/MICE/regression models, and the per-column choice made by auto at fit time, then applies them unchanged. Directional strategies carry no cross-frame state; listwise learns a per-row mask, so use fit_transform for it.
Visualizing
fig = ns.plot_missing_values(df, "heatmap", figsize=(8, 4), cmap="magma")
| Question | Plot types |
|---|---|
| Where are the gaps? | heatmap, matrix |
| How much is missing? | percentage, histogram, wordcloud |
| Do the gaps travel together? | correlation, dendrogram, upset_plot |
| Are the incomplete rows different? | pair |
Each returns a Matplotlib figure and accepts the underlying plot function's keyword arguments. None of them prove anything — they point your eye at the structure so you know which test to run first.
pandas and Polars
Every public function accepts either a pandas or a Polars DataFrame, and the imputers return the same type they were given. Polars frames are immutable, so in_place=True warns and returns a new frame.
Contributing
Contributions are welcome. Please submit pull requests, open issues, or suggest improvements.
License
Release files for nullsweep 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nullsweep-1.0.0.tar.gz | 61.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nullsweep-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 143.3 kB
Release files / nullsweep-1.0.0.tar.gz
| Download URL | nullsweep-1.0.0.tar.gz |
|---|---|
| Size | 61.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e4775975809f189109b0abb47257347c2b753a659fbfdb1a674c766b8f10f51c
|
|
BLAKE2b-256 checksum How to use checksums |
f3709dba6828630f02acd7244014da78f3663de7c9799866c33ff077ec16e758
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 3, 2026.
Transparency logRelease files / nullsweep-1.0.0-py3-none-any.whl
| Download URL | nullsweep-1.0.0-py3-none-any.whl |
|---|---|
| Size | 82.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9707ddc614069ab71dd49e6985448c629bb57a542042e45f7e05bfe4c4d84c24
|
|
BLAKE2b-256 checksum How to use checksums |
7550fed434653bee9bc619c3840c25457f5dd13cc71efa2c28824d3ee5440e5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 3, 2026.
Transparency log