[ENH] defer trailing underscore attribute assignment in fit() for imputers and discretisers - #917
Conversation
|
Before the test cases were passing locally but the CI checks were failing. the 3 CI chesks that were failing were:-
As |
|
After the fix, since no trailing underscore attributes are set until the very end, a failed |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #917 +/- ##
==========================================
- Coverage 98.27% 98.25% -0.02%
==========================================
Files 116 116
Lines 4978 5036 +58
Branches 795 797 +2
==========================================
+ Hits 4892 4948 +56
- Misses 55 56 +1
- Partials 31 32 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
solegalli
left a comment
There was a problem hiding this comment.
These changes are neat! Thank you so much!
Could you finish updating all discretisers and all imputers so we can merge?
For the next PR, could you please make 1 PR per module? So, for example, 1 PR for encoders, a different PR for creation and so on?
Thanks a lot! I look forward to the changes.
|
|
||
| return X, variables_ | ||
|
|
||
| def fit(self, X: pd.DataFrame) -> pd.DataFrame: |
There was a problem hiding this comment.
I can see that the logic that is here and we were calling with super().fit(), you are now not using and passing it on to the transformer, which makes sense for the requested change.
So i think we should remove the fit method from base numerical altogether. Like this, we ensure we don't use it as legacy anywhere else in the source code.
|
Hey @solegalli ,thank you for the kind words and the feedback! |
|
I'll update this PR shortly. Thanks again for the guidance |
Changes made
2 Deferred Trailing Underscore Attribute Assignment - Updated the
Files changed:-
tests |
There was a problem hiding this comment.
Thanks a lot for the quick turnaround!
I wasn't sure if you finished working on this PR, but I had a look anyways.
It's looking great.
We need to add tests for all the modules included in this PR. Currently, we need to add the test to the following:
- Add test for the transformers in the creation module (like this, we ensure all transformers in creation are now OK)
- Add test for the transformers in the transformation module
- Add test for the scaler
I also updated the issue, to keep track of which modules have been updated up to now.
Thank you!
|
HI, @solegalli made the 3 tests i was asked to , would appreciate you review |
|
Thank you so much @direkkakkar319-ops The only thing missing for merging would be to update 2 transformers from the creation module: DecisionTreeFeatures and GeoDistanceTransformers. Would you mind updating those 2 files as well? That would be it and we can go ahead and merge this PR! Thanks a lot! |
|
hi, @solegalli , the changes are done and the checks are passing would appreciate a merge. |
…eeds Several fit() methods assigned trailing-underscore attributes (variables_, imputer_dict_, mean_, range_, etc.) as soon as they were computed, rather than only once the rest of fit()'s logic had completed successfully. If fit() later raised partway through, those attributes were already set, so sklearn's check_is_fitted() would incorrectly treat the transformer as fitted, and transform() would run instead of raising NotFittedError. Two confirmed concrete cases this fixes: - MeanNormalisationScaler: mean_/range_ were set before the constant-column check that raises ValueError. - RandomSampleImputer: variables_/X_ were set before validating random_state, so an invalid random_state would raise but leave the imputer looking fitted. Fix: compute everything into local variables during fit(), and only assign self.*_ attributes at the very end, immediately before self._get_feature_names_in(X). This matches the pattern already used in imputation/base_imputer.py, encoding/base_encoder.py and selection/base_selector.py; this change extends it to _base_transformers/base_numerical.py (BaseNumericalTransformer.fit() is replaced by _fit_setup(), which returns (X, variables_) instead of assigning to self, so every subclass now assigns variables_ itself) and _base_transformers/mixins.py (FitFromDictMixin._fit_from_dict() likewise) and applied to every discretisation, transformation, creation and scaling class built on them, plus the remaining imputation classes. Also fixed the same issue in CyclicalFeatures.fit()'s dict-based branch, which previously called _fit_from_dict() and discarded its result, then relied on it having set self.variables_ as a side effect - broken now that _fit_from_dict() no longer does that. Tests: added check_raises_non_fitted_error_when_fit_fails(estimator, X) to tests/estimator_checks/non_fitted_error_checks.py, a shared helper (clone -> fit raises -> transform raises NotFittedError) called once per estimator from the affected test_check_estimator_*.py files (creation, discretisation, imputation, scaling, transformation), with a genuine mid-fit failure trigger per class where one exists (e.g. the two cases above, plus CategoricalImputer's "multiple frequent categories" and the negative/out-of-range value checks in BoxCoxTransformer, LogTransformer, ArcsinTransformer, ReciprocalTransformer) and an early input-validation trigger for classes with no reachable failure point later in fit. Note: sklearn's own check_estimator suite does not cover this scenario. check_fit_check_is_fitted only tests "never fitted" and "successfully fitted with well-behaved data", never "fit raises on malformed input" - which is estimator-specific and needs these hand-written tests. Co-authored-by: Soledad Galli <solegalli@protonmail.com>
5aee954 to
19d3e58
Compare








Description
This PR ensures that all trailing underscore attributes (
variables_,imputer_dict_,binner_dict_) in imputation and discretisation transformers are only assigned after all fit logic has successfully completed, following sklearn convention.Problem
Previously, attributes like
variables_were set early infit(), before the remaining logic ran. If an error occurred midway through fitting, the transformer was left in a partially fitted state — meaningtransform()would not raiseNotFittedErroras expected.For example:
Tests
Added
test_raises_non_fitted_error_when_error_during_fittotests/test_imputation/test_check_estimator_imputers.py, following the same pattern already established intests/test_encoding/test_check_estimator_encoders.py.First attempt — 4 test cases failed
When the test was first added, it used a numerical-only DataFrame as the failure trigger for all estimators. This worked for
MeanMedianImputer,EndTailImputer, andArbitraryNumberImputer, but 4 cases did not raise becauseCategoricalImputer(ignore_format=True),AddMissingIndicator,RandomSampleImputer, andDropMissingDataaccept all variable types and fitted successfully on that DataFrame.Fix — different triggers per estimator
The test was updated to use the correct failure trigger per estimator type:
MeanMedianImputer,EndTailImputer,ArbitraryNumberImputerfind_numerical_variables()raisesTypeErrorCategoricalImputerignore_format=False+ numerical-only df → raisesTypeErrorAddMissingIndicator,RandomSampleImputer,DropMissingDatacheck_X()raisesValueErrorType of Change
All the test cases are passing locally
