Repository navigation
Conversation
Preprocessor forwarded input_features to its ColumnTransformer, which was fitted on the synthetic feature_0, feature_1, ... names of array input and so required exactly those names. That broke Pipeline.get_feature_names_out() whenever an upstream step, such as SimpleImputer, outputs arrays. Following scikit-learn's convention, Preprocessor now records feature_names_in_ only when fitted on string-labelled columns and keeps validating input_features against it. Without it, any n_features_in_ names are accepted: each step's output names are rebuilt from its fitted transformer with the given labels (on shallow copies without their recorded feature names), so they match the names a fit on named columns would produce, e.g. num_age instead of num_feature_0. Behaviour change: a Preprocessor fitted on a DataFrame or dict with string column labels now has a feature_names_in_ attribute, which also changes the fingerprint_ of newly fitted preprocessors. Fixes #65
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 #65.
After a fit on a NumPy array,
Preprocessor.get_feature_names_out(input_features)raisedinput_features is not equal to feature_names_in_, even for names of the right length. As a result,Pipeline.get_feature_names_out()failed wheneverPreprocessorfollowed a step that outputs arrays, such asSimpleImputerwith default output.Root cause
Preprocessor.fitnames array columnsfeature_0,feature_1, ... before fitting the innerColumnTransformer, andget_feature_names_outforwardedinput_featuresto that transformer unchanged. TheColumnTransformerholds the synthetic labels as itsfeature_names_in_, so it accepted only exactly those names.Preprocessoritself never setfeature_names_in_, so it could not apply scikit-learn's rule that an estimator fitted without feature names accepts anyinput_featuresof the right length.Fix
Preprocessor.fitrecordsfeature_names_in_(an object array) when the input is not a NumPy array and all its column labels are strings, and deletes it on a refit on an array.Preprocessor.get_feature_names_outchecks thatinput_featureshasn_features_in_entries and, whenfeature_names_in_is defined, that it equalsfeature_names_in_. Withoutfeature_names_in_, any names of the right length are accepted.pretab.compose.inspection.feature_names_outbuilds the output names. Forinput_featuresafter an array fit, it maps each fitted label to the given name and rebuilds every step's names from its fitted transformer with the new labels._without_feature_nameshands that call a shallow copy of the step, and of nestedPipelineandFeatureUnionsteps, withoutfeature_names_in_. scikit-learn then applies only its length check, and the fitted steps are left untouched.clean_feature_namestakes a keyword-onlyrenamemapping and reports a renamed column's step under the new name. An array fit withinput_features=["age", "income"]therefore gives the same names as a fit on those columns (num_age, notnum_feature_0). Withoutrename, its output is unchanged.Behaviour changes
Preprocessorfitted on a DataFrame or dict whose column labels are all strings now has afeature_names_in_attribute. A refit on an array removes it.get_feature_names_out(input_features)accepts anyn_features_in_names and returns names built from them.input_featuresof the wrong length raises scikit-learn'sValueError: input_features should have length equal to number of features (n), got kinstead ofinput_features is not equal to feature_names_in_.to_spec()state andfingerprint_of newly fitted preprocessors with string column labels change, because the fitted state now includesfeature_names_in_. The golden baselines intests/regression/pass unchanged. A preprocessor saved by 1.0.0 still loads; it has nofeature_names_in_and therefore accepts anyinput_featuresof the right length.Documentation
Preprocessorclass docstring, Attributes: newfeature_names_in_entry, defined only when every column label is a string.Preprocessor.get_feature_names_outdocstring:input_featuresfollows scikit-learn's convention; newRaisessection for a wrong length or names that differ fromfeature_names_in_.feature_names_outand_without_feature_names, and of therenameargument ofclean_feature_names.docs/core_concepts/outputs_and_inspection.md("Feature names"): explains the convention and showsmake_pipeline(SimpleImputer(), Preprocessor(numerical_method="minmax")).get_feature_names_out()returning['num_age', 'num_income'].Tests
test_array_fit_uses_the_given_input_features(4 cases:minmax,polynomial,bsplinewithmissing_policy="separate_state",plewithadd_missing_indicator=True, all with missing values): an array fit withinput_features=["age", "income"]gives the same names as a DataFrame fit. Fails onmain, passes here.test_array_fit_rejects_input_features_of_the_wrong_length: scikit-learn's length message. Fails onmain, passes here.test_named_fit_records_and_validates_feature_names_in: a DataFrame fit recordsfeature_names_in_, matching names are accepted, other names raise, and a refit on an array removes the attribute. Fails onmain, passes here.test_pipeline_feature_names_propagate_through_an_array_step:make_pipeline(SimpleImputer(), Preprocessor(...)).get_feature_names_out()returns["num_age", "num_income"]. Fails onmain, passes here.Reproduction
Output of the script from #65 on this branch:
Checks
just test: 1445 passed, 59 skipped, 7 xfailed; coverage 93.58% (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
main.clean_feature_namesinpretab/compose/inspection.pyaround a new_blockhelper and astep_to_columnsmapping, so the docstring and the prefix logic conflict. Take Preprocessor.fit crashes when a column name starts with '_' or contains '__' #71's version, add therename=Nonekeyword withrename = rename or {}, map each step's columns through it when buildingstep_to_columns([rename.get(str(column), column) for column in columns]), and keep the docstring sentence onrename. Resolved this way, the full suite passes (1454 passed, 59 skipped, 7 xfailed).pretab/compose/inspection.py(Standalone transformers drop DataFrame feature names, so reordered columns are silently misaligned #85 addsimport pandas as pd; CrossFittedTransformer(Preprocessor(add_missing_indicator=True)) crashes when a fold's training rows contain no missing value #89 addscastandfrom sklearn.base import clone). Keep both sides. Standalone transformers drop DataFrame feature names, so reordered columns are silently misaligned #85 recordsfeature_names_in_on the standalone transformers, which_without_feature_namesalready strips on the copies it uses.Preprocessor.fitnear the new lines; git merges it cleanly. output_format="auto" chooses sparse vs dense per transform batch, breaking dense-only estimators at predict #73 also changes the fingerprint of newly fitted preprocessors.get_feature_names_out(theinput_featuresdocstring and the method body), where it compares names by their string form. Keep this PR's side of both hunks: it already accepts the labels seen at fit. With that resolution the full suite passes (1455 passed).tests/integration/test_preprocessor.py, so expect an append/append conflict there: keep both blocks.Preprocessor._align_inputfrom Preprocessor fitted on a DataFrame rejects a same-width NumPy array at transform (and vice versa) #78 should usefeature_names_in_to tell a named fit from an array fit. See the PR for Preprocessor fitted on a DataFrame rejects a same-width NumPy array at transform (and vice versa) #78 (fix(preprocessor): match array input by position after a named fit #116).isinstance(X, np.ndarray). Preprocessor rejects polars DataFrames, so pipelines with set_output(transform="polars") crash #94 reads a list of rows like an array and names its columnsfeature_0,feature_1, .... Once it lands, the check should benot has_column_labels(X), or a fit on a list of rows recordsfeature_names_in_ = ["feature_0", ...].This change was previously part of #75, which is closed in favour of one pull request per issue.