Repository navigation
Conversation
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 was referenced Oct 16, 2026
This branch has not been deployed
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.
Fixes #71.
Preprocessor.fitcrashed 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__, orimp__agefrom an upstreamColumnTransformerwith pandas output.Root cause
build_column_transformernamed each per-column stepf"num_{label}"/f"cat_{label}". scikit-learn rejects estimator names that contain__, its nested-parameter separator, so_scorebecame the invalid step namenum__score.Fix
compose.factory._step_namesnames the steps. A step keepsnum_<label>/cat_<label>whenever scikit-learn accepts that name. Otherwise it gets a positionalnum_col<i>/cat_col<i>name, with a_<n>suffix if another step already uses it.build_column_transformernow builds one(kind, feature)list and names its steps with_step_names. Column selectors are unchanged.compose.inspection.block_name(and the private_block) buildsnum_<label>/cat_<label>from the step's kind and its column label.get_output_slices(the dict-output keys) andclean_feature_names(feature names, also used for lineage) use it, so a positional step name never reaches the user.Preprocessor._log_internal_decisionslogs the verbose=3 fitted decisions under the block name.Behaviour changes
output_dims_. On string, integer and float labels, and on labels ending in a single_,mainand this branch give identical output under three configurations. Labels that used to crash get a positional step name insidecolumn_transformer_and appear asnum_<label>/cat_<label>everywhere else.Documentation
Preprocessordocstring,column_transformer_attribute: explains how steps are named, and that block keys, feature names and lineage always use the column label._step_names,block_name,build_column_transformer,get_output_slicesandclean_feature_namesdescribe the naming.docs/core_concepts/outputs_and_inspection.mdsay block keys are prefixednum_/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 arenum_<label>, the block holds the min-max scaled input, andoutput_dims_ == {label: 1}. Fails onmain, passes here.test_categorical_and_expanded_labels_with_underscores_fit: one-hot_cityand 3-wide PLEx__yproduce the exact feature names, the same names in lineage, block keysnum_x__y/cat__city, andoutput_dims_keyed by label. Fails onmain, passes here.test_positional_step_names_do_not_leak_into_names_or_block_keys: withmissing_policy="separate_state",_cityruns under the internal stepcat_col1. Feature names, lineage and block keys still usecat__city, including the namesclean_feature_namesdoes not collapse (cat__city__representation___city_a). Fails onmain, passes here.test_underscore_labels_do_not_collide_with_other_steps:_anext to a realcol0column gives the names["num__a", "num_col0"]. Fails onmain, passes here.test_preprocessor_chains_after_a_pandas_column_transformer: the issue's pipeline fits, with namesnum_imp__age,num_imp__income. Fails onmain, passes here.test_verbose_3_logs_internal_decisions_under_the_block_name: verbose=3 logsnum__score.total_output_dim_and never mentionsnum_col0. Fails onmain, 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
_citycase from the issue text:Checks
just test: 1447 passed, 59 skipped, 7 xfailed; coverage 93.54% (gate 90%).scripts/quickstart.pypasses.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).Merge notes
Base:
main.The PR for Preprocessor routes integer column labels positionally, silently swapping columns #55 (fix(preprocessor): route columns to the ColumnTransformer by label #100) edits the same lines, and the two conflict in three places:
build_column_transformer: keep this PR's loop over_step_names, use Preprocessor routes integer column labels positionally, silently swapping columns #55's[str(feature)]as the selector, and keep both docstring paragraphs.Preprocessordocstring,column_transformer_attribute: keep both paragraphs.tests/integration/test_column_labels.py, added by both (add/add): keep the union. That is Preprocessor routes integer column labels positionally, silently swapping columns #55's fixture, helpers and imports, plus this PR's scikit-learn imports and tests, with the module docstrings merged._blockalready reads the label asstr(columns[0]), so it works unchanged with Preprocessor routes integer column labels positionally, silently swapping columns #55's string selectors. With the conflicts resolved this way, the full test suite passes (1456 passed, 59 skipped, 7 xfailed).The PR for Preprocessor.get_feature_names_out(input_features) fails after fitting on an ndarray, breaking Pipeline.get_feature_names_out() #65 (fix(preprocessor): accept input_features after fitting on a NumPy array #108) also rewrites
clean_feature_names(it adds arenamemapping forinput_features). Keep this PR's block-based prefix and apply Preprocessor.get_feature_names_out(input_features) fails after fitting on an ndarray, breaking Pipeline.get_feature_names_out() #65'srenameto the label, in both branches of the_blockcheck:feature = rename.get(feature, feature), thenpublic_name = f"{kind_prefix}_{feature}"for a block. Also keep both docstrings. With that resolution, the full test suite passes (1454 passed, 59 skipped, 7 xfailed).The PR for add_missing_indicator / impute_with_indicator feeds the indicator through the scaler and basis; lineage crashes #62 (fix(preprocessor): keep the missing indicator out of the representation #106) rewrites the same loop header in
Preprocessor._log_internal_decisions. Combine the two asname = block_name(step_name, columns)andlast_step = representation_leaf(transformer). Both PRs add a verbose=3 test at the same place intests/integration/test_verbosity.py: keep both. With that resolution, the full test suite passes (1468 passed, 59 skipped, 7 xfailed).The PR for CrossFittedTransformer(Preprocessor) relearns column types and category codes per fold, unlike transform #79 (fix(cross-fitting): refit only target-aware Preprocessor blocks per fold #117) (and CrossFittedTransformer(Preprocessor(add_missing_indicator=True)) crashes when a fold's training rows contain no missing value #89, which is stacked on it) adds
block_uses_targetnext toblock_nameincompose.inspection.__all__and in thepreprocessor.pyimport list: keep both names.This change was previously part of #75, which is closed in favour of one pull request per issue.