Skip to content

fix(preprocessor): encode boolean columns as binary categoricals - #99

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/54-boolean-columns
Open

ChrisW09 wants to merge 1 commit into
mainfrom
fix/54-boolean-columns

Conversation

@ChrisW09

@ChrisW09 ChrisW09 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #54.

Any DataFrame or NumPy array with a plain bool column made Preprocessor.fit raise ValueError: SimpleImputer does not support data with dtype bool. This happened under the default settings, every categorical_method, every preset, and the imputing missing policies.

Root cause

detect_column_types classifies bool (dtype kind "b") as categorical. The categorical pipeline starts with SimpleImputer(strategy="most_frequent") whenever imputation is enabled, which is the default, and SimpleImputer rejects the bool dtype.

Fix

  • compose.feature_detection.bool_columns_as_object casts boolean columns (bool and pandas' nullable boolean) to object with the values True / False, so they are encoded like any other binary categorical column. A missing value of the nullable boolean dtype becomes NaN, the missing marker the imputer and the encoders recognize. The function returns a shallow copy, so the caller's frame is unchanged. A frame without boolean columns is returned as is.
  • The new Preprocessor._column_transformer_input applies it. fit and transform pass their frame through it to the internal ColumnTransformer. Column-type detection still sees the original dtypes.

Behaviour changes

  • categorical_method="none" now passes a boolean column through as the Python objects True / False. The block holds those values, and the stacked output becomes an object array:
    • For plain bool, "none" only fitted on main with categorical_imputation=None or missing_policy="propagate". The column was then upcast to 1.0 / 0.0 in a float array.
    • For nullable boolean with the default imputer, main returned float 1.0 / 0.0. A pd.NA that is not imputed (categorical_imputation=None or missing_policy="propagate") made main raise. It now passes through as NaN.
    • With a configured dtype (for example dtype=np.float32), the output is still cast to 1.0 / 0.0. A frame with any string categorical column already produced an object array under "none".
  • One-hot feature names for nullable boolean columns change from cat_flag_0.0 / cat_flag_1.0 to cat_flag_False / cat_flag_True. On main, the imputer had turned the column into floats. Without imputation, the missing category is still cat_flag_nan. The values are unchanged.
  • pd.NA in a nullable boolean column is now treated as missing everywhere. With categorical_method="int" and missing_policy="propagate" or categorical_imputation=None, main raised TypeError: boolean value of NA is ambiguous. The value now gets the missing code 0.
  • Configurations that already fitted on main keep identical names and values. That covers int, one-hot and onehot_from_ordinal on plain bool with categorical_imputation=None or missing_policy="propagate" / "error", and int / onehot_from_ordinal on nullable boolean.

Documentation

  • README, "Automatic feature-type detection": boolean columns are categorical and are encoded as a binary categorical with the categories False and True.
  • Preprocessor docstring, Notes: bool and nullable boolean columns are binary categoricals, and a nullable missing value is treated as missing.
  • detect_column_types docstring: non-numeric dtypes, including bool and nullable boolean, are always categorical.
  • New bool_columns_as_object docstring.

Tests

  • test_bool_column_is_encoded_as_a_binary_categorical: 11 cases, covering every failing configuration from the issue:

    • the defaults;
    • categorical_method "one-hot", "onehot_from_ordinal" and "none";
    • presets "standard", "expanded" and "adaptive";
    • missing_policy "impute", "impute_with_indicator" and "separate_state";
    • treat_all_integers_as_numerical=True.

    For each, the test checks the exact trailing feature names, categorical_features_ == ["flag"] and the transform width. Fails on main, passes here.

  • test_bool_column_integer_codes_follow_the_values: with categorical_method="int" the codes are exactly False -> 1 and True -> 2. Fails on main, passes here.

  • test_nullable_boolean_column_with_missing_values_is_imputed: one-hot names are cat_flag_False / cat_flag_True, and the pd.NA row is imputed to True. Fails on main (names cat_flag_0.0 / cat_flag_1.0), passes here.

  • test_nullable_boolean_missing_value_gets_the_missing_code_without_imputation: under missing_policy="propagate", the codes are [2, 1, 0, 1, 2, 2] with pd.NA mapped to 0. Fails on main (TypeError), passes here.

  • test_bool_numpy_array_is_supported: a bool ndarray fits and transforms. Fails on main, passes here.

  • test_bool_columns_as_object_casts_only_boolean_columns: checks the dtypes and values after the cast (NaN for pd.NA) and that the input keeps its dtypes. Fails on main (the function does not exist), passes here.

  • test_bool_columns_as_object_returns_frame_without_bool_columns_unchanged: a frame without boolean columns is returned as the same object. Fails on main (the function does not exist), passes here.

Reproduction

Output of the script from #54 on this branch. It runs under the default settings, under each configuration the issue lists as also failing, and on a NumPy bool array. Each line shows the categorical feature names (all names for the array):

{}                                            ['cat_flag']
{'categorical_method': 'one-hot'}             ['cat_flag_False', 'cat_flag_True']
{'categorical_method': 'int'}                 ['cat_flag']
{'categorical_method': 'onehot_from_ordinal'} ['cat_flag_bin_0', 'cat_flag_bin_1']
{'categorical_method': 'none'}                ['cat_flag']
{'preset': 'standard'}                        ['cat_flag']
{'preset': 'expanded'}                        ['cat_flag_False', 'cat_flag_True']
{'preset': 'adaptive'}                        ['cat_flag']
{'missing_policy': 'impute'}                  ['cat_flag']
{'missing_policy': 'impute_with_indicator'}   ['cat_flag']
{'missing_policy': 'separate_state'}          ['cat_flag__representation__flag', 'cat_flag__missing__flag__missing']
{'treat_all_integers_as_numerical': True}     ['cat_flag']
bool ndarray                                  ['cat_feature_0']

Checks

  • just test: 1455 passed, 59 skipped, 7 xfailed; coverage 93.71% (gate 90%).
  • Minimum dependencies (CI job: Python 3.10, numpy 1.24.4, pandas 2.0.3, scipy 1.10.1, scikit-learn 1.6.0): 1442 passed, 72 skipped, 7 xfailed; scripts/quickstart.py passes.
  • just docs: strict build succeeds.
  • just check: all pre-commit and pre-push hooks pass (ruff format, ruff lint, prettier, file hygiene, pyright with 0 errors).
  • Import direction between subpackages is unchanged.

Merge notes

This change was previously part of #75, which is closed in favour of one pull request per issue.

Boolean columns are detected as categorical, but the categorical pipeline
starts with a SimpleImputer, which rejects the bool dtype, so any frame
(or NumPy array) with a plain bool column failed to fit under the default
settings, every categorical_method and every preset.

Boolean columns are now cast to object (True / False) before they reach
the internal ColumnTransformer, so they are encoded like any other binary
categorical column. A missing value of pandas' nullable boolean dtype
becomes NaN, the missing marker the imputer and the encoders recognize.
The caller's frame is left unchanged.

Fixes #54

This branch has not been deployed

No deployments
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.

Preprocessor.fit crashes on any bool column under default settings

1 participant