fix(outliers): zero-IQR fallback fences; never clip declared id/target columns - #429
Merged
Merged
Conversation
A non-constant numeric column whose quartiles coincide (for example 95 zeros plus spikes of 1000, 5000 and -800) got no Tukey fences, so outlier_method "iqr" and "auto" silently flagged, clipped, capped and removed nothing, while "zscore" still flagged the 5000. When Q1 == Q3, the IQR is now replaced by its normal-consistent equivalent from the mean absolute deviation around the median (which equals the quartiles here): 1.6907 x MeanAD, i.e. sqrt(pi/2) x MeanAD as the sigma estimate times 1.349. The fallback only applies when it flags at most 5% of the non-missing values. Above that, the off-center values are a second mode rather than rare outliers, and the column stays untouched, as a constant column does. Report descriptions note "IQR is zero, so fences use 1.69 x mean absolute deviation from the median". "auto" measures skewness untrimmed for such columns, as before, so method selection is unchanged. Columns with a non-zero IQR get identical fences and output. The FreshCore kernel applies the same rule; Polars, DuckDB and Spark still skip zero-IQR columns (documented).
…allback capital_loss in the cached adult_income sample is about 95% zeros, so its IQR is zero and the balanced default used to skip it. With the zero-IQR fallback the default flags its 100 non-zero values (5.0% of the column, exactly the fallback's share limit), with the "IQR is zero" note in the description. cols_after goes 19 -> 20 (capital_loss_outlier), and outliers_handled goes 594 -> 694. No other action changed. The summary line was appended by --update-golden.
outliers="clip" rewrote values in columns declared through id_columns and target_column: a legitimate customer_id of 10,000,000 was clipped, and a declared target's maximum fell from 5000 to 138.1. That breaks the documented guarantee that targets and identifiers are never modified. The explicit step never consulted the declared roles; only the decision engine did. Clipping now skips declared id_columns and target_column, matched exactly or by snake-case name after renaming. When such a column has values outside the fences, the report records "skipped: identifier column" or "skipped: target column". Roles inferred only from names are not skipped. Flagging still reports these columns and never changes their values. Context-protected (mutable=False / policy) columns are skipped under clip too, where they used to raise ProtectedColumnError.
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
FreshData benchmark report —
|
| fixture | n_rows | n_cols | p50 s | p95 s | peak MB | repair % | false-repair % | preserve % | trust | monotonic | export % |
|---|
Authored-code reduction (Metric 6)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR fixes two cases where
outliers=did the wrong thing.outlier_method="iqr"and"auto"flagged, clipped, capped and removed nothing, even though"zscore"still flagged the 5000.outliers="clip"rewrote values in columns declared throughid_columnsandtarget_column. A legitimatecustomer_idof 10,000,000 was clipped, and a declared target's maximum fell from 5000 to 138.1. That breaks the documented guarantee that identifiers and targets are never modified.Root cause
Q1 == Q3the fences collapse to a single value. The step treated that like a constant column and skipped it.Fix
Zero-IQR fallback (
steps/outliers.py,engine/outliers.py, FreshCore kernel)Q1 == Q3, the IQR is replaced by1.6907 × MeanAD, where MeanAD is the mean absolute deviation around the median. The constant issqrt(pi/2)(MeanAD as a σ estimate) times 1.349. The fences aremedian ± factor × 1.6907 × MeanAD.auto. Method selection is unchanged.docs/backends.md, anddocs/cleaning-engine.mddescribes the rule.Clip respects declared roles (
steps/outliers.py)id_columnsandtarget_column, matched exactly or by snake-case name after renaming.mutable=Falseor policy) are skipped under clip too. They used to raiseProtectedColumnError.Default-output changes
Yes.
fd.clean(outlier_method="auto"/"iqr") can now flag outliers on zero-IQR columns where it found none before. Under the default flag action, this adds<col>_outliercolumns and outlier actions or counts to the report.adult_income.balancedreport is refreshed and gains one flagged column.clipwith declared roles. Declaredid_columnsandtarget_columnare no longer modified, and the report gains "skipped: …" entries.clipwith context-protected columns. These columns are skipped instead of raising.Tests
tests/test_outliers.py:tests/test_execution/test_freshcore_native_parity.py:adult_income.balanced.report.jsonandgolden_diff_summary.jsonl.Verification
ruff check .passes.test_freshcore_native_parity.py,test_outliers.py,test_engine_outliers.pyandtest_golden.pypass:not online and not largelanes on the rebased branch: py3.12 / pandas 2.3.3 6396 passed, 14 skipped; py3.9 / pandas 1.5.3 6367 passed, 18 skipped.mainconflicted intest_freshcore_native_parity.py: both sides appended tests there. I resolved it by keeping both, and the tests above were rerun after the rebase.