Skip to content

fix(preprocessor): keep ColumnTransformer step names valid for any label - #111

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/71-valid-step-names
Open

ChrisW09 wants to merge 1 commit into
mainfrom
fix/71-valid-step-names

Conversation

@ChrisW09

@ChrisW09 ChrisW09 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #71.

Preprocessor.fit crashed with scikit-learn's "Estimator names must not contain __" for any column label that starts with _ or contains __, such as _score, pyarrow's __index_level_0__, or imp__age from an upstream ColumnTransformer with pandas output.

Root cause

build_column_transformer named each per-column step f"num_{label}" / f"cat_{label}". scikit-learn rejects estimator names that contain __, its nested-parameter separator, so _score became the invalid step name num__score.

Fix

  • compose.factory._step_names names the steps. A step keeps num_<label> / cat_<label> whenever scikit-learn accepts that name. Otherwise it gets a positional num_col<i> / cat_col<i> name, with a _<n> suffix if another step already uses it. build_column_transformer now builds one (kind, feature) list and names its steps with _step_names. Column selectors are unchanged.
  • Public names no longer come from the step name. compose.inspection.block_name (and the private _block) builds num_<label> / cat_<label> from the step's kind and its column label. get_output_slices (the dict-output keys) and clean_feature_names (feature names, also used for lineage) use it, so a positional step name never reaches the user.
  • Preprocessor._log_internal_decisions logs the verbose=3 fitted decisions under the block name.

Behaviour changes

  • None beyond the fix. Labels that already worked keep the same step names, block keys, feature names, lineage and output_dims_. On string, integer and float labels, and on labels ending in a single _, main and this branch give identical output under three configurations. Labels that used to crash get a positional step name inside column_transformer_ and appear as num_<label> / cat_<label> everywhere else.

Documentation

  • Preprocessor docstring, column_transformer_ attribute: explains how steps are named, and that block keys, feature names and lineage always use the column label.
  • The docstrings of _step_names, block_name, build_column_transformer, get_output_slices and clean_feature_names describe the naming.
  • No docs page changes. The README and docs/core_concepts/outputs_and_inspection.md say block keys are prefixed num_ / cat_, which still holds.

Tests

  • test_numerical_labels_with_underscores_fit[_score|income__log|__index_level_0__|trailing__]: fit succeeds, the feature name and block key are num_<label>, the block holds the min-max scaled input, and output_dims_ == {label: 1}. Fails on main, passes here.
  • test_categorical_and_expanded_labels_with_underscores_fit: one-hot _city and 3-wide PLE x__y produce the exact feature names, the same names in lineage, block keys num_x__y / cat__city, and output_dims_ keyed by label. Fails on main, passes here.
  • test_positional_step_names_do_not_leak_into_names_or_block_keys: with missing_policy="separate_state", _city runs under the internal step cat_col1. Feature names, lineage and block keys still use cat__city, including the names clean_feature_names does not collapse (cat__city__representation___city_a). Fails on main, passes here.
  • test_underscore_labels_do_not_collide_with_other_steps: _a next to a real col0 column gives the names ["num__a", "num_col0"]. Fails on main, passes here.
  • test_preprocessor_chains_after_a_pandas_column_transformer: the issue's pipeline fits, with names num_imp__age, num_imp__income. Fails on main, passes here.
  • test_verbose_3_logs_internal_decisions_under_the_block_name: verbose=3 logs num__score.total_output_dim_ and never mentions num_col0. Fails on main, passes here.

Reproduction

Output of the script from #71 on this branch. Each call prints the fitted feature names, and the last line is the categorical _city case from the issue text:

_score: ['num__score']
income__log: ['num_income__log']
pipeline: ['num_imp__age', 'num_imp__income']
_city: ['cat__city_a', 'cat__city_b']

Checks

  • just test: 1447 passed, 59 skipped, 7 xfailed; coverage 93.54% (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): 1434 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.

Preprocessor named each per-column step of its internal ColumnTransformer
f"num_{label}" / f"cat_{label}". scikit-learn rejects estimator names that
contain its "__" parameter separator, so fit crashed for any column label
starting with "_" or containing "__" (e.g. "_score", pyarrow's
"__index_level_0__", or "imp__age" from an upstream ColumnTransformer with
pandas output).

Steps now keep the num_<label> / cat_<label> name whenever scikit-learn
accepts it and otherwise get a unique positional name (num_col0, ...).
Block keys, feature names, lineage and the verbose=3 log are derived from
the step's kind and its column label, so they are unchanged for every
label that already worked and read num_<label> / cat_<label> for the new
ones.

Fixes #71

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 when a column name starts with '_' or contains '__'

1 participant