Repository navigation
Conversation
CrossFittedTransformer converted X with np.asarray and sliced folds from that array, so a mixed-dtype DataFrame reached the wrapped estimator as an object array without column names. A wrapped Preprocessor then treated every column as categorical (every out-of-fold value of a numeric feature became the 'unseen category' code), and name-based estimators such as a ColumnTransformer selecting columns by label raised. DataFrames are now passed through unchanged, with each fold taken as a row subset by position; other input is still converted to a 2D array. Fixes #58
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 #58.
CrossFittedTransformerconvertedXto a NumPy array before fitting the wrapped estimator, so a mixed-dtype DataFrame reached it as an object array without column names. A wrappedPreprocessorthen treated every column as categorical and silently produced meaningless out-of-fold features, and estimators that select columns by name raised.Root cause
_fit_fullandtransformcallednp.asarray(X), andfit_transformsliced the folds from that array (X_arr[train_idx],X_arr[test_idx]). A frame with a float and a string column becomes an object array;to_dataframewraps it as-is anddetect_column_typesclassifies every object column as categorical.Fix
_as_2d(X)inpretab/core/supervised.py: a 2-D pandas DataFrame is returned unchanged; any other input is still converted withnp.asarray, with 1-D input reshaped to one column._fit_fullandtransformuse it._take_rows(X, indices): selects rows by position (.ilocfor a DataFrame). Thefit_transformfold loop takes each fold's training and held-out rows with it.yis still converted withnp.asarray(y).ravel().Behaviour changes
fit,transformand on every fold as a DataFrame that keeps its column names, dtypes and index labels (a fold is a row subset, its index is not reset). Before, it was an ndarray, so a wrappedPreprocessornamed its outputs afterfeature_0,feature_1, ...; it now uses the column names.to_dataframestill wraps an object ndarray withoutinfer_objects()(the optional second suggestion in the issue), because that would change type detection for every object-array input.Documentation
CrossFittedTransformerdocstring,transformerparameter: it may also be an estimator such as aPreprocessororPipelinebuilt from PreTab transformers, and a pandas DataFrameXis passed to it unchanged, each fold as a row subset._as_2dand_take_rows.docs/core_concepts/target_awareness.mdand the README show NumPy examples only and do not describe the input conversion.Tests
In
tests/core/test_cross_fitted.py, with amixed_framefixture (a float and a string column, shuffled index):test_wrapped_preprocessor_keeps_dataframe_column_types: the wrappedPreprocessordetectsnumas numerical andcityas categorical, its output width and feature names equal a direct fit, the out-of-fold PLE column holds more than one distinct value (a supervised encoding, not the "unseen category" code), andtransformhas the same shape. Fails onmain, passes here.test_wrapped_column_transformer_selects_by_column_name: a wrappedColumnTransformerthat selects["num"]by name cross-fits. Fails onmain("Specifying the columns using strings is only supported for dataframes"), passes here.test_wrapped_preprocessor_accepts_feature_preprocessing_by_column_name: withfeature_preprocessing={"num": "ple"}andnumerical_method="minmax", the outputs arenum_num_ple0...num_num_ple3andcat_city. Fails onmain(InvalidParamError ... Valid values: ['feature_0', 'feature_1']), passes here.test_one_dimensional_input_is_still_a_single_column: 1-D input is still reshaped to one column. Guard test: passes on both by design.Reproduction
Output of the script from #58 on this branch, with the two further effects the issue lists (
feature_preprocessing={"num": "ple"}and aColumnTransformerselecting["num"]) added as separate calls:Checks
just test: 1442 passed, 59 skipped, 7 xfailed; coverage 93.73% (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.fit_transformfold loop and append tests at the end oftests/core/test_cross_fitted.py, so expect textual conflicts. Keep every test section, and take the fold rows with_take_rows:fold = self._fit_fold(_take_rows(X_arr, train_idx), y_arr[train_idx])andfold.transform(_take_rows(X_arr, test_idx)).Preprocessor._cross_fit_foldthen receives DataFrame folds with their original index; it appliesto_dataframeand_safe_indexing, so it needs no change._stack_folds(blocks, test_indices, X_arr), which then receives the DataFrame and keeps its index for scikit-learnset_outputframes. Whichever of this PR and the PR for CrossFittedTransformer.fit_transform forces a dense float64 array: string and sparse outputs crash #90 (fix(cross-fitting): return transform's output type from fit_transform #125) lands second can add a test for that index (mixed_framewith a name-basedColumnTransformerandset_output(transform="pandas")).CrossFittedTransformer._fit_full: keep itsvalidate_task(self.task, type(self).__name__)together withX_arr = _as_2d(X).if is_polars_frame(X): return polars_to_pandas(X)to_as_2d, so a polars frame is converted once, before the fold rows are taken. That PR lists the cross-fitting tests to add.Preprocessorfitted on a frame recordsfeature_names_in_(Preprocessor.get_feature_names_out(input_features) fails after fitting on an ndarray, breaking Pipeline.get_feature_names_out() #65),CrossFittedTransformer.get_representation_spec()without names asks the wrappedPreprocessorfor the outputs ofx0,x1, ..., which it rejects. Whichever of CrossFittedTransformer converts DataFrames to ndarray; a wrapped Preprocessor treats every column as categorical #58, Preprocessor.get_feature_names_out(input_features) fails after fitting on an ndarray, breaking Pipeline.get_feature_names_out() #65 and Standalone transformers drop DataFrame feature names, so reordered columns are silently misaligned #85 lands last defaultsinput_featurestogetattr(self.estimator_, "feature_names_in_", None)there, with tests.This change was previously part of #75, which is closed in favour of one pull request per issue.