Repository navigation
Conversation
CrossFittedTransformer(Preprocessor(add_missing_indicator=True)) and missing_policy="impute_with_indicator" raised a width-mismatch error whenever a fold's training rows held no missing value for a column with missing values (always for a column with a single NaN). The indicator comes from the block's SimpleImputer(add_indicator=True), which only emits a column for features that had missing values at fit, and each fold refit the whole target-aware block, imputer included. A target-aware block now keeps the steps before its representation (the imputer with its missing indicator, scalers) from the all-data fit and refits only the representation on the fold's training rows, transformed by those kept steps; a separate-state union also keeps its missing branch. The out-of-fold features therefore have the columns of transform, and out-of-fold rows still never see their own target. The width-mismatch message now describes the general cause instead of adaptive sizing only. Fixes #89
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 #89.
Cross-fitting a
Preprocessorwithadd_missing_indicator=Trueormissing_policy="impute_with_indicator"raised a width-mismatch error whenever a fold's training rows held no missing value for a column that has missing values, which always happens for a column with a single missing value. On this branch alone, target-aware splines (bspline,ispline,mspline,cubicspline,naturalspline) with a missing indicator still raise, pinned by a strict xfail test, becausemainstill feeds the indicator into the representation; the PR for #62 (#106) removes that, and with #62, #79 and this PR combined every method works with no further code change (see Merge notes).Root cause
The indicator comes from the block's
SimpleImputer(add_indicator=True), which only emits a column for features that had missing values in the rows it was fit on. Onmainthe wholePreprocessoris refit per fold, so every method failed. With the PR for #79 (#117) (the base of this branch), blocks that do not useyare shared with the all-data fit, but a target-aware block is still refit as a whole, imputer included, so PLE, the feature maps, the target-aware splines and registered target-using encoders still lost the indicator column on such a fold.Fix
refit_block_representation(transformer, X, y)(new,pretab/compose/inspection.py) returns a copy of a fitted block in which only the representation step and the steps after it are refit. The steps before it (the imputer with its indicator, scalers) keep their all-data fit and transform the fold rows for the refit. For a separate-state union it keeps themissingbranch and recurses intorepresentation. A block that is not a pipeline, or whose first step is the representation, is cloned and refit as before. The fitted block itself is not modified._locate_block_representation(new) also returns the representation step's index;_resolve_block_representationnow wraps it.Preprocessor._cross_fit_foldrefits each target-aware block withrefit_block_representation.Behaviour changes
Preprocessor, the imputer (its median / most-frequent statistics and its indicator) and any scaler now come from the all-data fit on every fold; only the representation is refit on the fold's training rows. This also applies to separate-state blocks and to blocks without an indicator. These steps never usey, so nothing leaks, and a held-out missing value is now imputed with the all-data statistic, astransformimputes it. On a PLE column without missing values, the out-of-fold values are the same as with the whole-block refit up to float32 rounding.IncompatibleParamsErrorhas a new message (same type): "... The wrapped transformer's width depends on the rows it is fit on (e.g. adaptive sizing, or category / indicator columns learned from the data); configure it to produce the same columns on every fold."bspline,ispline,mspline,cubicspline,naturalsplinewith the defaulttarget_aware=True) with a missing indicator and a fold without missing values now raisePretabDataError: Feature at index 1 is constant ...instead of the width error. Onmain's block structure the imputer feeds its indicator to the spline as a second input (add_missing_indicator / impute_with_indicator feeds the indicator through the scaler and basis; lineage crashes #62); the all-data indicator is constant on such a fold, and a spline basis cannot be built on a constant input. This cannot be fixed here without moving the indicator out of the representation, which is the fix for add_missing_indicator / impute_with_indicator feeds the indicator through the scaler and basis; lineage crashes #62.Documentation
docs/core_concepts/target_awareness.md: the cross-fitting paragraph added by CrossFittedTransformer(Preprocessor) relearns column types and category codes per fold, unlike transform #79 now lists imputers with their missing-value indicators and scalers among the parts shared with the all-data fit.CrossFittedTransformerdocstring,transformerparameter: only the target-aware representations of aPreprocessorare refit per fold; imputers with their indicators and scalers come from the all-data fit.refit_block_representation,_locate_block_representationand the privatePreprocessor._cross_fit_fold.Tests
Every new and changed test below fails on
mainand on the base branch (#79's), and passes here, except the strict xfail as noted.tests/core/test_cross_fitted.py, with anarray_with_one_missing_valuefixture (one NaN, so one fold trains on no missing value):test_wrapped_preprocessor_keeps_its_missing_indicator_columns[ple-missing0|ple-missing1|rbf-missing2|relu-missing3|sigmoid-missing4|tanh-missing5]: PLE withadd_missing_indicator=Trueand withmissing_policy="impute_with_indicator", and rbf, relu, sigmoid and tanh withadd_missing_indicator=True. The output hastransform's shape, the indicator columns are kept, and the value's representation columns equal the out-of-fold output withmissing_policy="impute". The base fails with the width error ("expected 12, got 8" for PLE).test_wrapped_preprocessor_refits_only_the_representation_after_the_imputer: exact values. The PLE block's 8 columns (value and indicator, 4 PLE columns each) equal the all-data imputer and scaler followed by a PLE refit on each fold. This test assertsmain's block structure (imputer,minmax,ple).test_wrapped_preprocessor_keeps_the_indicator_next_to_a_target_aware_spline:xfail(strict=True, raises=PretabDataError), with a reason pointing to add_missing_indicator / impute_with_indicator feeds the indicator through the scaler and basis; lineage crashes #62; it cross-fits a target-awarebsplinewith an indicator and pins the remaining limitation. On the base it fails with the width error instead, so it is reported as a failure there.test_wrapped_preprocessor_keeps_the_separate_state_missing_branch: withmissing_policy="separate_state", the representation branch equals the all-data imputer and scaler plus a per-fold PLE (exact), and the__missingcolumn equalsisnan. On the base the fold's imputer filled the held-out NaN with the fold median.test_wrapped_preprocessor_target_aware_blocks_stay_out_of_fold(changed from CrossFittedTransformer(Preprocessor) relearns column types and category codes per fold, unlike transform #79): now expects the all-data imputer and scaler plus a per-fold PLE, with exact equality. On the base it differs by float32 rounding.test_width_mismatch_names_the_general_cause(renamed from CrossFittedTransformer(Preprocessor) relearns column types and category codes per fold, unlike transform #79'stest_width_mismatch_message_does_not_blame_adaptive_sizing_only): matches "same columns on every fold".tests/extension/test_registered_cross_fitting.py:test_registered_target_encoder_keeps_its_missing_indicator: a registeredTargetEncoderwith an indicator keeps both output columns and is refit per fold. The base fails with the width error ("expected 2, got 1").Reproduction
Output of the script from #89 on this branch:
The same data with
add_missing_indicator=True,output_dim=6and other numerical methods, on this branch. The target-aware splines are the remaining limitation described above; on the base branch every method exceptminmaxfails with the width error:With the PR for #62 (#106) added, every row of this table, the splines included, gives the same shape for
fit_transformandtransform((200, 13)for all butminmax).Checks
just test: 1461 passed, 59 skipped, 8 xfailed (the additional xfail is the strict spline case); coverage 93.76% (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
fix/79-crossfit-preprocessor-encoding, because it changes the per-block refit (Preprocessor._cross_fit_fold,block_uses_target) that 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) introduces. GitHub shows only this issue's commit, and the PR must be retargeted tomainafter 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) merges.main, this branch needs two test changes and no code change:xfailmarker fromtest_wrapped_preprocessor_keeps_the_indicator_next_to_a_target_aware_spline, which then passes (a strict XPASS fails the suite).test_wrapped_preprocessor_refits_only_the_representation_after_the_imputer, which assertsmain's pipeline structure. After add_missing_indicator / impute_with_indicator feeds the indicator through the scaler and basis; lineage crashes #62 the block is aFeatureUnion: compute the expectation fromdict(block.transformer_list)["representation"]with_refit_representation_per_fold, astest_wrapped_preprocessor_keeps_the_separate_state_missing_branchdoes, and check that the indicator is one raw column equal tonp.isnan(X[:, 0]).refit_block_representationrecognises add_missing_indicator / impute_with_indicator feeds the indicator through the scaler and basis; lineage crashes #62's union through_separate_state_branches, keeps its fittedmissingbranch and refits only the representation step inside it. Only the import lists inpretab/compose/inspection.pyandpretab/preprocessor.pyconflict (keep both names). The full test suite then has exactly these two failures (1481 passed, 59 skipped, 7 xfailed otherwise).main's width-mismatch message; when both land, take this PR's message. With CrossFittedTransformer converts DataFrames to ndarray; a wrapped Preprocessor treats every column as categorical #58, CrossFittedTransformer(Preprocessor) relearns column types and category codes per fold, unlike transform #79, CrossFittedTransformer(Preprocessor(add_missing_indicator=True)) crashes when a fold's training rows contain no missing value #89 and CrossFittedTransformer.fit_transform forces a dense float64 array: string and sparse outputs crash #90 combined, the full test suite passes (1473 passed, 59 skipped, 8 xfailed).__all__at the top ofpretab/compose/inspection.py(Preprocessor.get_feature_names_out(input_features) fails after fitting on an ndarray, breaking Pipeline.get_feature_names_out() #65 addscopy,AnyandPipelineas well; Preprocessor.fit crashes when a column name starts with '_' or contains '__' #71 also addsblock_nameto the import list inpretab/preprocessor.py). Keep the union of the imports and names.This change was previously part of #97, which is closed in favour of one pull request per issue.