Repository navigation
Conversation
With imputation enabled, add_missing_indicator=True and missing_policy='impute_with_indicator' used SimpleImputer(add_indicator=True) as the first pipeline step, so every later step saw the indicator as a second input feature: it was scaled, expanded into basis columns or crossed into polynomial terms, categorical integer codes turned it into 1/2, and get_feature_lineage() crashed while get_feature_info() reported no dimension. The imputer now runs without its indicator, and scikit-learn's MissingIndicator (the one SimpleImputer used internally) runs on the raw input in a parallel FeatureUnion branch, mirroring the separate_state construction. The indicator stays a raw 0/1 column with its previous name, added only for features with missing values at fit; the representation columns are the same as with plain imputation, and lineage reads the indicator width from the fitted branch. Such blocks are now a FeatureUnion, so the representation summary (to_spec() and reproducibility_report()) and the verbose=3 log, which read each block's last step, would have skipped them. A representation_leaf() helper returns the last step of the union's representation branch and both use it; this also lists separate_state blocks, which the summary never did. The fingerprint does not hash the summary. Specs and pickles written by 1.0.0 with add_missing_indicator or impute_with_indicator keep their old block layout and still load, transform, and give the same to_spec(), fingerprint_ and reproducibility_report(). Newly fitted indicator blocks have a different fitted state, so their fingerprint differs from 1.0.0. Fixes #62
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 #62.
With imputation enabled,
add_missing_indicator=Trueandmissing_policy="impute_with_indicator"fed the missing indicator through the scaler and the numerical or categorical method as a second input feature. Users got a standardized, basis-expanded, polynomial-crossed or 1/2-coded indicator instead of one 0/1 column,get_feature_lineage()raised for every spline and feature-map method, andget_feature_info()reported no dimension.Root cause
PreprocessorConfig.imputation_plansetsadd_indicatorwhenever the imputer runs with an indicator, andcreate_transformerpassed it on asadd_missing_indicatortoget_numerical_transformer_steps/get_categorical_transformer_steps. These putSimpleImputer(add_indicator=True)first in the per-columnPipeline, so the imputer returned two columns (imputed value and indicator) and every later step was fitted on both. The inspection code assumes one input column per block: lineage passed a single column name to a leaf fitted on two inputs, andbuild_feature_infoprobed it with a(1, 1)array. Only theseparate_statepath built its indicator outside the representation, in a parallelFeatureUnionbranch.Fix
create_transformer(pretab/compose/factory.py) no longer passesadd_missing_indicatorto the step builders, so the imputer runs without its indicator. Whenplan["add_indicator"]is set, it returnsFeatureUnion([("representation", pipeline), ("missing", MissingIndicator(error_on_new=False))], verbose_feature_names_out=False), the same layout asseparate_state.MissingIndicatoris whatSimpleImputer(add_indicator=True)uses internally, so the indicator keeps its semantics (a column only for features with missing values at fit) and its name (num_a__missingindicator_a).build_feature_lineage(pretab/compose/inspection.py) reads the width of the missing branch from the fitted indicator'sget_feature_names_out, which may be zero columns, and assigns the rest of the block to the representation.FeatureUnion, so_representation_summary(to_spec()["representations"],reproducibility_report()["representations"]) andPreprocessor._log_internal_decisions(theverbose=3log), which read each block's last step, would have skipped them. The new helperrepresentation_leaf()inpretab/compose/inspection.py(added to__all__) returns the last step of a union's"representation"branch, or of a plain pipeline, and both callers use it.Behaviour changes
missing_policy="impute"), so outputs of basis methods get narrower: in the issue's example,bsplinewithoutput_dim=5gives 6 columns instead of 10, andpolynomialhas no indicator terms.get_feature_lineage()works for these blocks and labels indicator columns with familymissing_state.get_feature_info()reports the block width, and itspreprocessingstring readsrepresentation(imputer -> ...) + missing, as forseparate_state.to_spec()["representations"]andreproducibility_report()["representations"]now also listseparate_stateblocks, which they never did. The fingerprint does not hash this summary.fingerprint_differs from 1.0.0.add_missing_indicator=Trueormissing_policy="impute_with_indicator"keep their old block layout and still load. Checked forbspline,ple,standardizationwithone-hot, andrbfwithint, each from a pickle and from a JSON spec: the output is bit-for-bit identical to 1.0.0, andfingerprint_,to_spec()["representations"]andreproducibility_report()are unchanged.get_feature_lineage()on such a loaded object still raises for basis methods (bspline,rbf), as it did on 1.0.0, because the stored block still has the two-input leaf. Refitting fixes it.Documentation
Preprocessordocstring:add_missing_indicatornow describes a 0/1 column for each imputed feature with missing values atfit, computed on the raw input next to the representation and never passed through the scaler or basis. The"impute_with_indicator"bullet ofmissing_policyrefers to it.docs/core_concepts/missing_values.md: new paragraph saying that with imputation enabled the indicator is a raw 0/1 column computed before imputation, placed next to the representation, never scaled or expanded, and added only for features with missing values atfit.PreprocessorConfig.imputation_planand_representation_summary, and of the newrepresentation_leaf.Tests
test_imputer_indicator_is_a_raw_binary_column(withadd_missing_indicator=Trueand withmissing_policy="impute_with_indicator"): withstandardization, the indicator column equalsisna()as 0/1. Fails onmain, passes here.test_imputer_indicator_adds_exactly_one_column_per_feature_with_missing[bspline, rbf, pspline, fourier, cubicspline, polynomial]: exactly one indicator per feature, the other names equal those of plain imputation, andoutput_dims_is the plain width plus one. Fails onmain, passes here.test_imputer_indicator_lineage_and_feature_info[bspline, rbf, pspline, fourier, cubicspline]: lineage covers every output column, the two indicators have familymissing_state, and theget_feature_infodimensions equaloutput_dims_. Fails onmain, passes here.test_imputer_indicator_on_categorical_integer_codes:intcodes keep one 0/1 indicator. Fails onmain, passes here.test_missing_state_blocks_report_their_representation(withadd_missing_indicator=True,missing_policy="impute_with_indicator"andmissing_policy="separate_state"): the summary listsbsplinefor both columns and equals that of plain imputation. Fails onmain, passes here.test_verbose_3_logs_internal_decisions_of_missing_state_blocks(withadd_missing_indicator=Trueandmissing_policy="separate_state"): the PLE thresholds of these blocks are logged atverbose=3. Theseparate_statecase fails onmain, passes here. Theadd_missing_indicatorcase passes onmain, where that block is still a pipeline; it pins that the new union is unwrapped.test_imputer_indicator_only_marks_features_with_missing_values_at_fit: no indicator column for a feature without missing values at fit, as withSimpleImputer(add_indicator=True). Guard test: passes on both by design.test_block_with_the_imputer_built_in_indicator_still_loads_and_reports: rebuilds the 1.0.0 block layout (imputer withadd_indicator=Truein front of the basis) and checks that it transforms, round-trips through a JSON spec with the same output and fingerprint, and lists its representations. Guard test: passes on both by design.Reproduction
Output of the script from #62 on this branch (the
get_feature_lineage()call is wrapped and prints the number of records and the last record):Checks
just test: 1459 passed, 59 skipped, 7 xfailed; coverage 93.92% (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.Preprocessor._log_internal_decisionsand intests/integration/test_verbosity.py, where both add a test aftertest_verbose_3_logs_internal_decisions. Keep Preprocessor.fit crashes when a column name starts with '_' or contains '__' #71's loop header andname = block_name(step_name, columns)together with this PR'slast_step = representation_leaf(transformer), and keep both tests. With this branch, Preprocessor.fit crashes when a column name starts with '_' or contains '__' #71's and Preprocessor crashes on an all-missing column at fit (numerical, one-hot) or silently drops it (int) #95's combined this way, the full test suite passes (1539 passed, 59 skipped, 7 xfailed).keep_empty_features=Trueon the imputers) changes the same imputer construction infactory.pyand the feature-info code ininspection.py; git merges the two cleanly.MissingIndicatorstill emits a column for an all-missing feature, so the two fixes fit together._representation_summary(it passes each block's column names and no longer swallows errors). Whichever of the two PRs lands second gets a conflict inserialize.pyand has to keepleaf = representation_leaf(transformer)inside Standalone transformers drop DataFrame feature names, so reordered columns are silently misaligned #85's rewritten loop.Preprocessorwith missing indicators) builds on this change and on CrossFittedTransformer(Preprocessor) relearns column types and category codes per fold, unlike transform #79's per-fold refit: each fold refits only the representation branch of an indicator block and keeps the all-data indicator. Its PR needs this one first.None/pd.NAnot treated as missing) now also decides which valuesMissingIndicatormarks.This change was previously part of #75, which is closed in favour of one pull request per issue.