test: cover both unchecked get_feature_names_out call shapes, fix the CRMCleaner crash they found - #198
Merged
Conversation
… CRMCleaner crash they found The public-API contract asserted that every transformer's get_feature_names_out is as wide as its transform output, but only ever made one call: no argument, on a transformer fitted from a DataFrame. The two shapes a real pipeline uses were unchecked. ColumnTransformer and Pipeline.get_feature_names_out both pass input_features down explicitly, and an estimator fitted on a bare array has no column names to fall back on. Adding both shapes found one defect. CRMCleaner.get_feature_names_out read self.feature_names_in_ unconditionally, and scikit-learn only sets that attribute when the fitted input carried column names. After an array fit it raised AttributeError one line after the check_is_fitted guard that exists to catch this class of problem, and as an AttributeError rather than anything a caller can handle. It now falls back to x0, x1, ... exactly as WealthScreeningImputer and WealthScreeningImputerKNN already did. The input_features shape found nothing: all 13 non-exempt transformers already honour it. That is worth recording rather than leaving as an absence. Two transformers genuinely cannot fit on an unnamed array, so they are exempted from that shape only, in a new _EXEMPT_ARRAY_FIT table with a written reason each. It is kept separate from _EXEMPT deliberately: _EXEMPT skips every check, so folding them in would have silently dropped both from the width and input_features contracts they do pass. Three hygiene tests police the new table for stale entries, names listed in both tables, and missing reasons. Closes #157.
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.
Closes #157.
What the issue predicted, and what actually happened
The issue said the value would be "the transformers this finds, not the two tests," and to report it either way. It found one real defect and cleared the rest.
Found:
CRMCleaner.get_feature_names_outcrashes after an unnamed-array fit.check_is_fitted(self)passes, becausen_features_in_is set, and the next line then readsself.feature_names_in_, which scikit-learn only assigns when the fitted input carried column names. So the failure lands one line after the guard that is supposed to catch exactly this class of problem, and it surfaces as anAttributeErrorrather than anything a caller can handle.The fix is the fallback
WealthScreeningImputerandWealthScreeningImputerKNNalready had, and nothing else:Before and after, on the same fixture:
Cleared:
input_featuresis honoured everywhere it is claimed to be. All 13 non-exempt public transformers already return the right width when passedinput_features=list(X.columns). Zero failures. That is a real result and worth stating rather than leaving as an absence.The two new contracts
Both parametrize over
sorted(preprocessing.__all__)and reuse the existing_transformer_instance/_transformer_inputhelpers, so there is no second fixture table to drift against__all__.test_feature_names_out_accepts_input_features: fits on the DataFrame, then passes the real column names. This is the callColumnTransformerandPipeline.get_feature_names_outactually make, and nothing in the suite made it before.test_feature_names_out_width_after_array_fit: fits onX.to_numpy(). This is what caughtCRMCleaner.One deliberate deviation from the issue text
The issue says to add array-fit failures to
_EXEMPT. I put them in a separate_EXEMPT_ARRAY_FITinstead, because_EXEMPTskips every check: folding these two in would also have dropped them from the width andinput_featurescontracts, which they both pass. Losing two passing contracts to record one legitimate limitation is a bad trade, and it would have been invisible.Two entries, one written reason each, same discipline as
_EXEMPT:EncounterTransformermerges an encounter table on a nameddonor_idand raises a writtenValueErrornamingmerge_keywhen it is absent.MatchingGiftFeaturizerrejects a non-DataFrame outright withTypeError: X must be a pandas DataFrame.Three hygiene tests police the new table so it cannot rot: no stale entry, no name in both tables, and a reason on every entry in both.
Follow-up question, deliberately not changed here
CRMCleaner.get_feature_names_outdocumentsinput_featuresas "Ignored", andFiscalYearTransformerandShareOfWalletScorerlikewise return fixed arrays regardless of what is passed. scikit-learn's own convention is to validateinput_featuresagainstfeature_names_in_and raise on a mismatch. Ignoring it means a caller who passes the wrong names silently gets the wrong answer instead of an error.That is a behaviour change, not a bug fix, so per the issue's own instruction it is flagged rather than shipped inside this PR. Worth its own issue if you agree; defensible as-is for the two fixed-output transformers, less so for
CRMCleaner.Verification
The behavioural check above is the direct evidence for the fix: the array-fit call raised before the change and returns
['x0', 'x1']after it.