Repository navigation
Conversation
… fit A column missing on every fit row (e.g. an optional field that is empty in a training window or a cross-validation fold) was dropped by the SimpleImputer, scikit-learn's default for such a column. The rest of its per-column pipeline then got no input: numerical methods and one-hot failed with "Found array with 0 feature(s)", while "int" and "none" silently removed the column from the output, output_dims_, the blocks and the lineage, so get_feature_info() and verbose=2 fits raised an IndexError. The imputers now keep such a column (keep_empty_features=True) and fill it with 0, or fill_value for strategy="constant", so every feature keeps its block and later data where it is observed transforms with the fitted width. fit emits a DataWarning naming the column, and the constant policy treats an empty numerical column as constant. A method that cannot be fitted on such a column (the splines and Box-Cox, or with imputation disabled every method except the scalers, "quantile" and "none") raises a PretabDataError naming it, chained to the method's own error. The column is only blamed when its block fits a stand-in column of observed values with the same y, so an unrelated failure (e.g. a y of the wrong length) keeps its own error. get_feature_info no longer raises on a 0-width "int" block. Fixes #95
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 #95.
A column that is missing on every
fitrow, such as an optional field that is empty in a training window or a cross-validation fold, broke thePreprocessor. Numerical columns and"one-hot"crashed with scikit-learn's "Found array with 0 feature(s)". With"int"and"none", the column silently disappeared from the output while the metadata still listed it, soget_feature_info()andverbose=2fits raisedIndexError.Root cause
Each per-column pipeline starts with a
SimpleImputerbuilt with scikit-learn's defaultkeep_empty_features=False, which drops a feature without any observed value. A pipeline holds exactly one column, so the next step received an(n, 0)array. The scalers, the numerical methods andOneHotEncoderreject it."int"and"none"accept it and return a 0-width block.build_feature_infothen indexedmapping_of an ordinal encoder that had been fitted on no column.Fix
pretab/compose/factory.py: the new_imputer()helper builds the leading imputer withkeep_empty_features=True.get_numerical_transformer_stepsandget_categorical_transformer_stepsuse it. An empty column is filled with0, or withfill_valueforstrategy="constant", and keeps its block.imputer_kwargscan still override the setting.Preprocessor.fitfinds the columns with no observed value and emits aDataWarningnaming them. The constant policy sees an empty numerical column as the constant it becomes once imputed.ValueErrorother than aPretabConfigErrorand an empty column exists, the newPreprocessor._raise_for_empty_featuresrefits each empty column's block on its own. If the block fails, but fits a stand-in column of observed values with the samey, it raises aPretabDataErrornaming the column and its method, chained to the method's error. Otherwise the original error propagates, so an unrelated failure (for example ayof the wrong length) is not blamed on the empty column.build_feature_inforeports an"int"block fitted on no column as dimension 0 with no categories instead of raisingIndexError.Behaviour changes
fitemits aDataWarningnaming each column with no observed value. scikit-learn's "Skipping features without any observed values"UserWarningno longer appears."int"and"one-hot"keep a block for it, and later data where the column is observed transforms with the fitted width.PretabDataError("Column 'x' has no observed (non-missing) value at fit, and its 'bspline' representation cannot be fitted on such a column ...") instead of scikit-learn's "0 feature(s)" error. With imputation disabled, the column stays missing: the scalers,"quantile"and"none"keep its block as missing values, and every other method raises the same error.policy={"constant": "error"}/"warn"now also fires for an all-missing numerical column.get_numerical_transformer_steps/get_categorical_transformer_stepsdefault tokeep_empty_features=True. Passingimputer_kwargs={"keep_empty_features": False}restores scikit-learn's default.Documentation
docs/core_concepts/missing_values.md: a note that a column with no observed value atfitkeeps its block, how the imputers fill it, and theDataWarning.docs/representations/choosing_a_method.md(edge-case behaviour): a new "Column with no observed value at fit" entry. It lists which methods fit such a column with and without imputation, which ones raise the typed error naming the column, and thatpolicy={"constant": "error"}rejects it. This backs the page's statement that "PreTab is explicit about degenerate inputs rather than failing silently", which the issue quotes.Preprocessorclass docstring;Raises(PretabDataError) andWarns(DataWarning) sections inPreprocessor.fit;_imputerand_raise_for_empty_features.Tests
test_imputer_keeps_a_column_without_observed_values: an all-NaN column keeps width 1 through the numerical ("minmax") and"one-hot"step pipelines, andimputer_kwargscan restorekeep_empty_features=False. Fails onmain, passes here.test_build_feature_info_reports_a_zero_width_block: an"int"block whose encoder received no column reports dimension 0. Fails onmain(IndexError), passes here.test_verbose_2_lists_a_column_without_observed_values: averbose=2fit warns and lists the empty column in the feature table. Fails onmain(IndexError), passes here.test_empty_numerical_column_keeps_its_block(27 cases: 14 methods, target-aware on and off):fitwarns, the column keeps a block, andoutput_dims_, feature names, lineage, dict blocks andget_feature_infoagree with the output. Data where the column is observed transforms with the fitted width. Fails onmain, passes here.test_empty_numerical_column_names_the_column_when_the_method_needs_spread(B/M/I, P, natural and cubic regression splines, Box-Cox):PretabDataErrornaming the column and the method, chained to the method's error. Fails onmain, passes here.test_empty_column_is_not_blamed_for_an_unrelated_fit_failure(3 cases): with aythat is too short or holds strings, the original error is raised, not the empty-column one. Fails onmain, passes here.test_empty_numerical_column_under_missing_value_settings(8 settings) andtest_empty_categorical_column_keeps_its_block("int","one-hot","none"under 7 settings): the same metadata checks across everymissing_policy, with a missing indicator, without imputation, and with constant imputation. Fail onmain, pass here.test_empty_numerical_column_counts_as_constant_for_the_policy:policy={"constant": "error"}rejects an empty numerical column. Fails onmain, passes here.test_column_empty_in_a_cross_fitting_fold_keeps_the_width: underCrossFittedTransformer, a column that is observed only in the last fold gives out-of-fold and refit outputs of the same width. Fails onmain, passes here.Reproduction
Output of the script from #95 on this branch. The script was adapted to print each call that now succeeds and the warnings
fitemits, where the original hid all warnings:Checks
just test: 1509 passed, 59 skipped, 7 xfailed; coverage 93.95% (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.self.column_transformer_.fit(X, y)line inPreprocessor.fitthat this PR wraps intry/except, so each conflicts there. Resolve it toX_ct = self._column_transformer_input(X)(Preprocessor.fit crashes on any bool column under default settings #54, Preprocessor routes integer column labels positionally, silently swapping columns #55), Preprocessor with one-hot encoding or preset="expanded" fails under set_config(transform_output="pandas") #76'sset_output(transform="default")line, and output_format="auto" chooses sparse vs dense per transform batch, breaking dense-only estimators at predict #73'straining_output = self.column_transformer_.fit_transform(X_ct, y)inside this PR'stry, withself._raise_for_empty_features(X_ct, y, empty_features, config)in theexceptandresolve_output_formatafter it. Preprocessor.fit crashes on any bool column under default settings #54 also conflicts in thePreprocessorclass docstring, where both add a paragraph after the alias list: keep both. Preprocessor routes integer column labels positionally, silently swapping columns #55 conflicts in theRaisessection offit: keep bothPretabDataErrorconditions._raise_for_empty_featuresmust then look up the empty feature by that string form (empty = {str(feature): feature for feature in empty_features},feature = empty.get(columns[0])). Otherwise an empty column with a non-string label is not matched, and the method's own error surfaces instead of the one naming the column.factory.pyand the feature-info code ininspection.py; git merges the two cleanly. Follow-up once add_missing_indicator / impute_with_indicator feeds the indicator through the scaler and basis; lineage crashes #62 lands:_assert_metadata_matches_outputintests/integration/test_edge_case_contract.pycurrently skips theget_feature_infodimension check for blocks with the imputer's missing indicator. Onmainthat indicator runs through the representation, andget_feature_infomiscounts it for"int"and"none". On a trial merge with add_missing_indicator / impute_with_indicator feeds the indicator through the scaler and basis; lineage crashes #62's branch, the dimensions agree for every indicator setting these tests cover, so the skip should then be removed.tests/compose/test_inspection.py, where both add a test aftertest_build_feature_info_does_not_infer_kind_from_cat_in_feature_name: keep both. The PR for A 2-D y (multi-output target) crashes the B/M/I splines even when unsupervised, and target-aware methods with unrelated errors #88 (fix(splines): ignore y when unsupervised and reject a multi-output target #123) conflicts intests/integration/test_edge_case_contract.py, in the import lines (keepCrossFittedTransformerandIncompatibleParamsError) and in the section both append at the end (keep both).main,CrossFittedTransformerconverts the frame withnp.asarray, sotest_column_empty_in_a_cross_fitting_fold_keeps_the_widthfits every column as categorical. After the PR 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) it runs on the DataFrame. The test needs no change.This change was previously part of #97, which is closed in favour of one pull request per issue.