Skip to content

fix(imputer): keep all-NaN columns instead of crashing (#124) - #257

Open
jaideeppyne wants to merge 3 commits into
nubank:masterfrom
jaideeppyne:fix/imputer-all-na-column
Open

fix(imputer): keep all-NaN columns instead of crashing (#124)#257
jaideeppyne wants to merge 3 commits into
nubank:masterfrom
jaideeppyne:fix/imputer-all-na-column

Conversation

@jaideeppyne

Copy link
Copy Markdown

What

Fixes #124. imputer with the default placeholder_value=None crashes whenever any column in columns_to_impute is entirely NaN in the training data.

Why

scikit-learn's SimpleImputer silently drops all-NaN features during fit. So at transform time imp.transform(...) returns fewer columns than columns_imputable, and:

new_cols = pd.DataFrame(data=new_data, columns=columns_imputable).to_dict("list")

raises ValueError: Shape of passed values is (N, k-1), indices imply (N, k).

Minimal repro on current master:

import pandas as pd
from fklearn.training.imputation import imputer

df = pd.DataFrame({"col1": [10, 13, 10], "col2": [None, None, None]})
imputer(df, ["col1", "col2"], "mean")   # ValueError: Shape of passed values is (3, 1), indices imply (3, 2)

The existing placeholder_value mechanism only rescues all-NaN columns when a placeholder is explicitly supplied; the default code path routes every column (including all-NaN ones) into columns_imputable and breaks.

How

Pass keep_empty_features=True to SimpleImputer (available since scikit-learn 1.2, within the pinned >=1.5,<1.8 range). This keeps all-NaN features in the output and imputes them with a default value (0), matching the behaviour requested in the issue. The explicit placeholder_value path is unaffected — all-NaN columns are split off into columns_to_fill there, so the flag is a no-op for it.

-    imp = SimpleImputer(strategy=impute_strategy)
+    imp = SimpleImputer(strategy=impute_strategy, keep_empty_features=True)

Tests

  • New regression test test_imputer_all_na_column_without_placeholder — asserts an all-NaN column with the default placeholder_value=None is imputed to 0.0 (both in the training output and on new data) instead of raising.
  • Existing tests/training/test_imputation.py still passes (test_imputer_with_fill_value confirms the placeholder_value path is unchanged).
tests/training/test_imputation.py ....  [4 passed]

Disclosure: this change was prepared with AI assistance and reviewed/verified by me before submission.

`imputer` with the default `placeholder_value=None` crashed whenever any
column in `columns_to_impute` was entirely NaN in the training data.
scikit-learn's SimpleImputer silently drops all-NaN features on `fit`, so at
transform time the output had fewer columns than `columns_imputable` and
`pd.DataFrame(data=new_data, columns=columns_imputable)` raised
`ValueError: Shape of passed values ... indices imply ...`.

Passing `keep_empty_features=True` (scikit-learn >= 1.2, within the pinned
range) keeps those columns and imputes a default value (0), matching the
behaviour requested in the issue. The explicit `placeholder_value` path is
unaffected, since all-NaN columns are split off into `columns_to_fill` there.

Adds a regression test covering the default-placeholder all-NaN case.
Copilot AI lite review requested due to automatic review settings August 19, 2026 01:53
@jaideeppyne
jaideeppyne requested a review from a team as a code owner August 19, 2026 01:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR addresses a regression where imputer crashed when columns_to_impute contained an entirely-NaN column and placeholder_value=None, by ensuring empty features are preserved during imputation.

Changes:

  • Added a regression test covering all-NA columns with default placeholder_value=None.
  • Updated SimpleImputer initialization to keep empty features instead of dropping them.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
tests/training/test_imputation.py Adds regression test to verify all-NA columns are retained and imputed (defaulting to 0.0).
src/fklearn/training/imputation.py Configures SimpleImputer to retain empty/all-NA features during fit/transform.
Suppressed comments (1)

src/fklearn/training/imputation.py:42

  • With keep_empty_features=True, the behavior for entirely-NA columns changes even when placeholder_value=None (they are no longer dropped and will be imputed with a default value per scikit-learn behavior). The docstring currently implies only placeholder_value controls the behavior for all-NA features; please update this docstring to reflect the new default behavior and how callers can control it (e.g., using placeholder_value / strategy='constant').
    placeholder_value : Any, (default=None)
        if not None, use this as default value when some features only contains
        NA values on training. For transformation, NA values on those features
        will be replaced by `fill_value`.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/fklearn/training/imputation.py
Comment thread tests/training/test_imputation.py Outdated
@jaideeppyne

Copy link
Copy Markdown
Author

Thanks for the review. Addressing both points:

  1. sklearn versionkeep_empty_features has been available since scikit-learn 1.2, and fklearn already pins scikit-learn>=1.5,<1.8 (see pyproject.toml), so the argument is always supported across the project's supported range — no fallback or version bump is needed.

  2. Strategy coverage — good call. I've parametrized the regression test over mean, median, and most_frequent; keep_empty_features=True imputes fully-missing columns with 0 for every non-constant strategy, so the test now asserts that for all three.

…rt true per-strategy behavior

Address Copilot review on nubank#257:

1. keep_empty_features requires scikit-learn>=1.2; fklearn's declared floor
   is scikit-learn>=1.5, so it is always available and no version guard /
   TypeError fallback is needed. Documented this at the call site. (A silent
   fallback constructing SimpleImputer without keep_empty_features would just
   reintroduce the nubank#124 crash on unsupported sklearn.)

2. Correct the parametrized all-NaN regression test. keep_empty_features
   affects each strategy's handling of fully-missing columns differently: an
   all-None column is object-dtyped, so mean/median fill the empty feature
   with 0.0 while most_frequent (no modal value) keeps it as a NaN/None
   sentinel. The previous test asserted a uniform 0.0 and failed for
   most_frequent. Assert the concrete per-strategy behavior; the nubank#124 fix
   (column preserved, no crash) holds for all three.
@jaideeppyne

Copy link
Copy Markdown
Author

Thanks @copilot — both addressed in 1af7688:

1. keep_empty_features vs. older scikit-learn. keep_empty_features was added to SimpleImputer in scikit-learn 1.2, and fklearn's declared floor is scikit-learn>=1.5 (pyproject.toml), so the argument is guaranteed available in every supported environment and no TypeError can occur. I opted for the pin-guarantee path over a try/except fallback: a silent fallback that constructs the imputer without keep_empty_features would just reintroduce the original #124 crash on any sub-1.2 sklearn, so it wouldn't be a graceful degrade. Added a comment at the call site documenting the minimum-version guarantee.

2. Cover median and most_frequent. Good catch — parametrizing surfaced a real asymmetry. Because an all-None column is object-dtyped, keep_empty_features fills the empty feature with 0.0 under mean/median but keeps it as a NaN/None sentinel under most_frequent (there's no modal value for an empty column). The earlier test asserted a uniform 0.0 and would fail for most_frequent; I've updated it to assert each strategy's concrete behavior. The core #124 fix — the column is preserved instead of dropped, so imputer no longer crashes — holds for all three. All three parametrizations now pass, along with ruff and mypy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing features removal with SimpleImputer

2 participants