Repository navigation
ENH Do not pass random_state to estimator in IterativeImputer - #15636
Conversation
rth
left a comment
There was a problem hiding this comment.
Thanks! It would be good to have a unit test for this behaviour.
One concern is that this is not fully backward compatible (because the same random state will now produce different output) -- let's wait for more feedback to see if this is fine. I think it might be if we consider this a "bug fix".
|
|
||
| if hasattr(self._estimator, 'random_state'): | ||
| self._estimator.random_state = self.random_state_ | ||
| if self._estimator.random_state is None: |
There was a problem hiding this comment.
Shouldn't this be
| if self._estimator.random_state is None: | |
| if self._estimator.random_state is None and self.random_state_ is not None: |
?
There was a problem hiding this comment.
I don't think so, because self.random_state_ (with underscore) is returned from function check_random_state through self.random_state (no underscore), so it cannot be missing (L541).
| if hasattr(self._estimator, 'random_state'): | ||
| self._estimator.random_state = self.random_state_ | ||
| if self._estimator.random_state is None: | ||
| self._estimator.random_state = self.random_state_.randint(np.iinfo(np.int32).max) |
There was a problem hiding this comment.
Linting is failing because the line is too long
There was a problem hiding this comment.
also this should probably be using set_params. we might special-case the way we handle random_state in the future as discussed in #14042
|
I've not checked this out yet, but backwards incompatibility is not an
issue as IterativeImputer is experimental
|
|
Changed to |
|
shouldn't iterative imputer pass the given random state to the underlying estimator in general? We shouldn't try to convert it to an integer, we just pass it along (the original I think I'm missing something here though. |
That means that by default (i.e. with random_state=None), the IterativeImputer isn't compatible with third party estimators that only support ints. Is it really part of our convention that estimators must support RandomState and None? |
That's precisely the point of the PR: Estimators from outside scikit-learn which are scikit-learn-compatible oftentimes don't work with NumPy's |
|
If the user passes |
|
Updated. Now it will only set the
EDIT: fixed mixed use of spaces and tabs now. EDIT2: corrected yet again for linting. |
4ee0ff2 to
290d7ac
Compare
That's not the semantic we've been using so far. None doesn't mean "no random state", it mean "use the numpy singleton". |
|
could you please avoid force pushing to the branch? makes it hard to keep track of the changes |
|
This also needs tests |
|
Added tests for the cases in which it should and shouldn't set a seed in the estimator, verifying that the seed is an integer. |
| random_state = self.random_state_.randint( | ||
| np.iinfo(np.int32).max) | ||
| self._estimator.set_params(random_state=random_state) |
There was a problem hiding this comment.
This should simply be:
self._estimator.set_params(random_state=self.random_state)If the given estimator doesn't support int or rng or None, then user shouldn't pass that as the random state.
|
Modified to pass to the estimator the same |
adrinjalali
left a comment
There was a problem hiding this comment.
this also needs a whats_new entry, otherwise looks good to me.
|
|
||
| def predict(self, X): | ||
| return np.zeros(X.shape[0]) | ||
| X = np.ones((10, 3), dtype=float) |
|
Added the extra line and comment on the changes in What's New. |
| :mod:`sklearn.impute` | ||
| ............................... | ||
|
|
||
| - |Enhancement| :class:`impute.IterativeImputer` Will not share the internal |
There was a problem hiding this comment.
I think this is too much detail and is unclear in telling the user what they need to know. Perhaps "IterativeImputer now provides a random seed to its regressor instances, rather than a RandomState object." or "Fixes a bug where ..."?
| `RandomState` object with the estimator, but instead will set its attribute | ||
| `random_state` if it was passed to the imputer and not to the estimator, | ||
| allowing to use estimators which do not support `RandomState` objects. | ||
| :pr: `15636` by :user:`David Cortes` <david-cortes>. |
There was a problem hiding this comment.
| :pr: `15636` by :user:`David Cortes` <david-cortes>. | |
| :pr: `15636` by :user:`David Cortes <david-cortes>`. |
There was a problem hiding this comment.
Good catch! I see some other entries in there are also not entered this way and don’t render correctly (PR’s 15503 and 15918) - may I attempt to correct them too?
| and self._estimator.random_state is None | ||
| and self.random_state is not None | ||
| ): | ||
| self._estimator.set_params(random_state=self.random_state) |
There was a problem hiding this comment.
this is not necessarily an integer. should we instead be generating a random seed and passing that?
There was a problem hiding this comment.
It was like that before, and I asked to change it. I don't think we should get a seed and pass around. If the estimator doesn't support rng, the user shouldn't pass an rng. I don't see why
we should workaround the API limitations of third party libraries.
There was a problem hiding this comment.
I am happy with the current approach, where the int should be used if self._estimator only supports random_state ints.
There was a problem hiding this comment.
Sorry for bringing this point late. I am wondering if IterativeImputer should be setting the random_state in self._estimator at all. In the example the user can do:
imputer = IterativeImputer(LGBMRegressor(random_state=42), random_state = 456)In this version, IterativeImputer would not set self._estimator.random_state.
What do you think @david-cortes ?
There was a problem hiding this comment.
I guess it's also a very valid approach (not setting the estimator's random_state), although it might cause some confusion when one has a seed/state set while the other doesn't. However in that snippet (setting the random_state in the estimator before passing it to the imputer), with the changes in this PR, it wouldn't set the estimator's random_state either, because it isn't None.
There was a problem hiding this comment.
When looking at metaestimators such as RegressorChain of ClassifierChain, it has a random_state parameter, but does not set the random_state of the base_estimator. I think not setting random_state would be more consistent with the rest of the library.
|
|
||
| def fit(self, X, y): | ||
| if self.rs_expect == "RandomState": | ||
| assert isinstance(self.random_state, np.random.RandomState) |
There was a problem hiding this comment.
can't you check the identity of the RandomState?
There was a problem hiding this comment.
It's not possible unfortunately as they contain information about memory address. So something like this would return False:
import numpy as np
np.random.RandomState(100) == np.random.RandomState(100)
>>> FalseThere was a problem hiding this comment.
We can try to check for the state explicitly:
import numpy as np
from numpy.testing import assert_array_equal
rng1 = np.random.RandomState(100)
rng2 = np.random.RandomState(100)
rng1_state = rng1.get_state()
rng2_state = rng2.get_state()
assert rng1_state[0] == rng2_state[0]
assert_array_equal(rng1_state[1], rng2_state[1])
assert_array_equal(rng1_state[2:], rng2_state[2:])| (None, "string", "string"), (408, "string", "string"), | ||
| (np.random.RandomState(seed=409), "string", "string")] | ||
| ) | ||
| def test_iterative_imputer_set_estimator_random_state( |
There was a problem hiding this comment.
I am finding it hard to read this test. I think naming and semantics of rs_expect could be improved at least.
|
Renamed the parameters in the test and added more line breaks for readability now. Also shortened the description in what's new. |
NicolasHug
left a comment
There was a problem hiding this comment.
Do we still need self.random_state_ now?
| ) | ||
| def test_iterative_imputer_set_estimator_random_state( | ||
| rs_imputer, rs_estimator, expect_in_fit | ||
| ): |
There was a problem hiding this comment.
Please add a short comment of what this test does
| - |Enhancement| :class:`impute.IterativeImputer` Will set the estimator's | ||
| `random_state` to the same `random_state` that was passed to the imputer. |
There was a problem hiding this comment.
in contrast to what? what does this enable?
Yes, it's still needed because the imputer itself also makes random choices such as the column order, so it needs an internal |
|
Added a better description to the test, the what's new, and also to the documentation just in case. The merge conflict now is because of this other PR: #16091 which fixed spacing in the previous entry in what's new. Not sure how to fix it in this pull request (other than force-pushing). Tips appreciated. |
|
Hello @david-cortes , if you are still interested in working on this, do you mind fixing conflicts? This will bring more attention from the reviewer side. Thanks for your work! |
… whitespace to its test
|
Solved merge conflicts. |
| :mod:`sklearn.impute` | ||
| ............................... | ||
|
|
||
| - |Enhancement| :class:`impute.IterativeImputer` Will set the estimator's |
There was a problem hiding this comment.
| - |Enhancement| :class:`impute.IterativeImputer` Will set the estimator's | |
| - |Enhancement| :class:`impute.IterativeImputer` will set the estimator's |
| ``sample_posterior`` is True. If the estimator has a `random_state`, | ||
| but it's set to None, will set it to this value too. Use an integer |
There was a problem hiding this comment.
| ``sample_posterior`` is True. If the estimator has a `random_state`, | |
| but it's set to None, will set it to this value too. Use an integer | |
| ``sample_posterior`` is True. If the estimator has a `random_state`, | |
| and it is set to None, then it's `random_state` will set it to this value | |
| too. Use an integer for determinism. |
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "rs_imputer, rs_estimator, expect_in_fit", |
There was a problem hiding this comment.
| "rs_imputer, rs_estimator, expect_in_fit", | |
| "rs_imputer, rs_estimator, rs_expect", |
And the rest of the test funcion.
|
Incorporated suggestions. |
|
Changed the logic of the PR to avoid setting the estimator's |
thomasjpfan
left a comment
There was a problem hiding this comment.
Thank you for the updates @david-cortes !
@adrinjalali What do you think of the recent changes here?
adrinjalali
left a comment
There was a problem hiding this comment.
Strangely enough it seems this change doesn't require a change in the random_state's description. Seems like a fix to me.
Still feels odd not to have a test for this change. We could test that the IterativeImputer does not set the random_state of a child estimator, otherwise LGTM.
I haven't read the whole conversation, is @NicolasHug happy with the change?
NicolasHug
left a comment
There was a problem hiding this comment.
thanks for your patience @david-cortes
| with `add_indicator=True`. | ||
| :pr:`17612` by :user:`Srimukh Sripada <d3b0unce>` | ||
|
|
||
| - |Enhancement| :class:`impute.IterativeImputer` will not attempt to set the |
There was a problem hiding this comment.
As noted by Adrin this should probably be a |Fix| entry since the behaviour might change a little
|
Changed the what's new entry to "Fix" and added a test asserting that the imputer doesn't set the random_state of the estimator. |
| @pytest.mark.parametrize( | ||
| "rs_imputer, rs_estimator", | ||
| [ | ||
| (None, None), | ||
| (1, None), | ||
| (np.random.RandomState(seed=1), None), | ||
| (None, 1), | ||
| (1, 1), | ||
| (np.random.RandomState(seed=1), 1), | ||
| (None, np.random.RandomState(seed=1)), | ||
| (1, np.random.RandomState(seed=1)), | ||
| (np.random.RandomState(seed=1), np.random.RandomState(seed=1)) | ||
| ] | ||
| ) | ||
| def test_iterative_imputer_dont_set_random_state(rs_imputer, rs_estimator): |
There was a problem hiding this comment.
We can parameterize as follows: (May require adjusting the whitespace to pass the linter)
@pytest.mark.parametrize("rs_imputer",
[None, 1, np.random.RandomState(seed=1)])
@pytest.mark.parametrize("rs_estimator",
[None, 1, np.random.RandomState(seed=1)])
def test_iterative_imputer_dont_set_random_state(rs_imputer, rs_estimator):|
Split the pytest parametrization across two parts to make it shorter. |
|
It seems to me that the codecov failure is not related to the PR. @thomasjpfan, two approvals: time to merge? :) Thanks! |
Trying to fix issue: #15611
Lots of algorithms that rely on randomization offer an argument
random_statethrough which it's possible to pass them RNG seeds that would ensure reproducible results. TheIterativeImputerclass will take some regressor/classifier and set their attributerandom_stateto a NumPy MT19937 object class if the object has such attribute, in such a way that this object is modified implicitly once the regressor/classifier to which it was assigned produces some random number.This is fine for SciKit-Learn’s own classes, but other libraries which provide different regressors/classifiers which are SciKit-Learn-compatible might not be able to use such
RandomStateobjects (e.g. if they generate random numbers in C++ with some method other than MT19937).This PR addresses the issue by setting the
random_stateof the estimator toa random integer, which is drawn from the imputer'sthe same value that was passed to the imputer, but does so only if it's not already set in the estimator. This way:RandomStateobjectrandom_statewhich is passed to the constructor of the estimator used for imputation (if it was passed).IterativeImputer. Example: