Repository navigation
Conversation
Preprocessor passed each column label to scikit-learn's ColumnTransformer as the column selector. ColumnTransformer reads an integer selector as a position, so integer labels that differ from their positions (after dropping, reordering or selecting columns of pd.DataFrame(array) or read_csv(header=None)) silently swapped columns: data, per-feature methods from feature_preprocessing, names and lineage referred to different columns. Non-contiguous labels or swapped columns of different types crashed. The ColumnTransformer is now fitted and applied on a frame whose labels are strings (a shallow relabelled copy only when needed, so the caller's frame is unchanged), and every step selects its column by that string label. Labels that collide as strings, such as 1 and "1", raise PretabDataError. output_dims_ and get_feature_info stay keyed by the original labels. A NumPy array passed to transform after a fit on a frame with non-string labels is still matched by position, as before. Fixes #55
This was referenced Oct 16, 2026
Remove a trailing blank line at the end of the column-label tests.
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 #55.
When a DataFrame's integer column labels did not match their positions,
Preprocessorsilently swapped columns. Data, per-feature methods fromfeature_preprocessing, names and lineage then referred to different columns. Non-contiguous labels, and swapped columns of different types, crashed. This hitspd.DataFrame(array)andread_csv(header=None)frames after a column is dropped, reordered or selected.Root cause
build_column_transformerpassed each column label to scikit-learn'sColumnTransformeras the column selector ([feature]). PreTab identifies columns by label everywhere else, butColumnTransformerreads anintselector as a position.Fix
compose.feature_detection.with_string_labelsconverts every column label to itsstrform. It returnsXitself when all labels are already strings, and otherwise a shallow relabelled copy, so the caller's frame is never modified. Labels that share a string form (1and"1") raisePretabDataError.build_column_transformerselects each column bystr(feature). Step names are unchanged.Preprocessor.fitandPreprocessor.transformpassself._column_transformer_input(X), which applieswith_string_labels, to the internalColumnTransformer.Preprocessor._input_labelsmaps the string labels back to the input labels, sooutput_dims_andget_feature_infostay keyed by the original labels.Preprocessor._label_array_by_positionkeeps array input matched by position after a fit on non-string labels: a NumPy array passed totransformtakes the fitted labels by position. Without it, thefeature_0,feature_1, ... names thatto_dataframegives an array would fail with "columns are missing".Behaviour changes
pd.DataFrame(array)with labels in position order now givesnum_0instead ofnum_0__x0. Lineagesource_featuresare unchanged (("0",)).column_transformer_is fitted on the string labels. Its step columns are["0"]instead of[0], and it now has afeature_names_in_attribute (["0", "1"]), which it lacked onmain.transformmust carry the fitted labels, in any order. Onmain, any frame of the same width was read by position. A frame with other labels now raises scikit-learn'sValueError("columns are missing"). A NumPy array is still matched by position.get_feature_names_out(input_features)compares each name by its string form, so the labels seen at fit are accepted as given ([0, 1]) or as strings (["0", "1"]), and give the same names asget_feature_names_out(). Other names now raiseValueError: input_features is not equal to feature_names_in_, as scikit-learn does when it knows the input names; onmainthey renamed the inputs. 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) restores renaming.int/strlabels used to fail inside scikit-learn ("Feature names are only supported if all input features have string names"). They now work.1and"1") raisePretabDataErrorwith a hint to rename the columns, instead of that scikit-learnTypeError.Documentation
Preprocessor.fit: new Raises section.PretabDataErroris raised for a duplicate label (already the case onmain) or for two labels with the same string form.Preprocessor.transform: theXdescription says that DataFrame and dict columns are matched by label, integer labels included. It also says that an array is matched by position after a fit on non-string labels.Preprocessordocstring,column_transformer_attribute: notes that it is fitted on the string form of every label.with_string_labels(with a Raises section) andbuild_column_transformerexplain why selectors are strings.ColumnTransformerselectors.Tests
test_integer_labels_route_each_column_to_its_own_block: on the issue's[1, 0]frame, each block holds its own column, and the caller's frame keeps its labels. Fails onmain, passes here.test_feature_preprocessing_with_integer_keys_targets_the_labelled_column:{1: "minmax", 0: "none"}scales column1and passes column0through. Fails onmain, passes here.test_non_contiguous_integer_labels_fit_and_transform: the issue's.drop(columns=[0])frame fits, gives namesnum_1,num_2, and its first output column is the min-max of label1. Fails onmain, passes here.test_integer_labels_of_different_column_types_reach_their_own_pipeline:{1: list("abcabc"), 0: [1.0, ..., 6.0]}fits with explicit methods and with the defaults. The test checks exact min-max values and ordinal codes, and that column0is detected as numerical and1as categorical. Fails onmain, passes here.test_integer_labels_are_matched_by_label_at_transform: a reordered frame gives the samenum_1block. Fails onmain, passes here.test_array_input_after_an_integer_labelled_fit_is_matched_by_position: an ndarray passed after a fit on[1, 0]labels is matched by position, and each block holds the right column. Fails onmain, passes here.test_integer_label_metadata_uses_the_input_labels: checks namesnum_1,num_0,output_dims_ == {1: 1, 0: 1},get_feature_infokeyed[1, 0], and the lineage pairs. Fails onmain, passes here.test_labels_colliding_as_strings_are_rejected:1and"1"raisePretabDataError. Fails onmain, passes here.test_with_string_labels_relabels_without_touching_the_input:[1, "b"]becomes["1", "b"], the input is left untouched, and an all-string frame is returned as is. Fails onmain(the function does not exist), passes here.test_get_feature_names_out_accepts_the_integer_labels_seen_at_fit:[0, 1]gives the same names as noinput_features. Fails onmain(where the names differ,num_0__0vs.num_0__x0), passes here.Reproduction
Output of the script from #55 on this branch. The failing
fit(A)now prints its feature names. The last line is the mixed-type frame from the issue text, fitted with the defaults:Checks
just test: 1448 passed, 59 skipped, 7 xfailed; coverage 93.70% (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.fit crashes when a column name starts with '_' or contains '__' #71 (fix(preprocessor): keep ColumnTransformer step names valid for any label #111) edits the same lines, and the two conflict in three places:
build_column_transformer: keep Preprocessor.fit crashes when a column name starts with '_' or contains '__' #71's loop over_step_names, use this PR'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 this PR's fixture, helpers and imports, plus Preprocessor.fit crashes when a column name starts with '_' or contains '__' #71's scikit-learn imports and tests, with the module docstrings merged.With the conflicts resolved this way, the full test suite passes (1456 passed, 59 skipped, 7 xfailed).
The PR for Preprocessor.fit crashes on any bool column under default settings #54 (fix(preprocessor): encode boolean columns as binary categoricals #99) adds the same
Preprocessor._column_transformer_inputhelper in the same place, and both PRs routefitandtransformthrough it. Conflicts:return with_string_labels(bool_columns_as_object(X))and merge both docstring sentences.feature_detectionimport line inpreprocessor.py, andfeature_detection.__all__.to_dataframe.tests/compose/test_feature_detection.py.Keep both sides in each. With the conflicts resolved this way, the full test suite passes (1464 passed, 59 skipped, 7 xfailed).
When 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) lands, its
_align_inputreplaces_label_array_by_position. Thetransformlines and the helper conflict: keep_align_input,_column_transformer_inputand_input_labels, drop_label_array_by_position, and take Preprocessor fitted on a DataFrame rejects a same-width NumPy array at transform (and vice versa) #78's description ofXintransform._align_inputreads the fitted labels fromcolumn_transformer_.feature_names_in_, which are the string labels after this change. With the conflicts resolved this way, the full test suite passes (1450 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) rewrites
get_feature_names_out(input_features), so the two conflict there (theinput_featuresdocstring and the body of the method). Take Preprocessor.get_feature_names_out(input_features) fails after fitting on an ndarray, breaking Pipeline.get_feature_names_out() #65's side of both hunks: it accepts the fitted labels and new names on its own. With that resolution the full suite passes (1455 passed), and after an integer-label fit[0, 1]givesnum_0,num_1and["a", "b"]givesnum_a,num_b.The PRs for output_format="auto" chooses sparse vs dense per transform batch, breaking dense-only estimators at predict #73 (fix(preprocessor): resolve output_format='auto' once per fit #113), Preprocessor with one-hot encoding or preset="expanded" fails under set_config(transform_output="pandas") #76 (fix(preprocessor): keep the internal ColumnTransformer on default output #114) and Preprocessor crashes on an all-missing column at fit (numerical, one-hot) or silently drops it (int) #95 (fix(preprocessor): keep a block for columns with no observed value at fit #130) change the
self.column_transformer_.fit(X, y)line infit. Keep their change and passself._column_transformer_input(X)to theColumnTransformer. Preprocessor crashes on an all-missing column at fit (numerical, one-hot) or silently drops it (int) #95 also adds aPretabDataErrorcondition to thefitRaises section: list both conditions under the one entry and keep its Warns section.The PR for Preprocessor.transform does not check n_features_in_ for ndarray input; extra columns are silently misaligned #72 (fix(preprocessor): reject NumPy input of the wrong width at transform #112) adds a width check where this PR adds
is_array = ...intransform. Keep both, with the check first.The PR for Preprocessor rejects polars DataFrames, so pipelines with set_output(transform="polars") crash #94 (fix(preprocessor): accept polars DataFrames as input #129) adds
has_column_labelsnext towith_string_labelsinfeature_detection(in__all__and as a new function), and changes theXtype line intransform. Keep both, i.e. Preprocessor rejects polars DataFrames, so pipelines with set_output(transform="polars") crash #94's type line with this PR's description.Follow-up with the PR for Preprocessor crashes on an all-missing column at fit (numerical, one-hot) or silently drops it (int) #95 (fix(preprocessor): keep a block for columns with no observed value at fit #130): its
Preprocessor._raise_for_empty_featurescompares theColumnTransformer's selectors with the input labels and indexesXwith them. After this change, an integer-labelled frame skips its message. In a trial merge, labels[0, 1]with an all-missing Box-Cox column raise scikit-learn'sValueError: Data must not be constant.instead of Preprocessor crashes on an all-missing column at fit (numerical, one-hot) or silently drops it (int) #95'sPretabDataError. Mapping the selector back withself._input_labels()and indexingself._column_transformer_input(X)restores the message. With that change, the full test suite passes on the trial merge (1518 passed, 59 skipped, 7 xfailed).Follow-up with the PRs for CrossFittedTransformer converts DataFrames to ndarray; a wrapped Preprocessor treats every column as categorical #58 (fix(cross-fitting): pass DataFrames to the wrapped transformer unchanged #103) and 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): CrossFittedTransformer(Preprocessor) relearns column types and category codes per fold, unlike transform #79 adds
Preprocessor._cross_fit_fold, which picks each target-aware block's columns out of the fold by theColumnTransformer's selectors. After this change those selectors are strings. Once CrossFittedTransformer converts DataFrames to ndarray; a wrapped Preprocessor treats every column as categorical #58 letsCrossFittedTransformerpass a DataFrame through, an integer-labelled frame would reach_cross_fit_foldand raise aKeyError._cross_fit_foldshould take its input throughself._column_transformer_input(X), asfitandtransformdo.This change was previously part of #75, which is closed in favour of one pull request per issue.