Repository: scikit-learn/scikit-learn
Pull request: ENH add the possibility to pass array-like for max and min in IterativeImputer
#### Reference Issues/PRs
Resolves #16349 

#### What does this implement/fix? 
The current implementation of `impute.IterativeImputer` only allows the specification of a single pair of (min, max) values to which all imputed features will be clipped. However, in many practical applications, different features might vary over different ranges. This PR adds functionality to the `IterativeImputer` to allow the specification of a different pair of min and max values for each feature to be imputed.

#### Any other comments?
These changes allow the following:
1. Leave the default `min_value` and `max_value` as `None` (which is the current default). In this case min and max values for all features will be set to `-np.inf` and `np.inf` respectively.
2. Allow the user to specify a single pair of numbers as min and max values for all features. In this case, the scalar `min_value` and `max_value` will be broadcast to float arrays of shape = number of features.
3. Allow the user to specify a list/array of min values and another list of max values, one value for each feature being imputed. The user can leave the min and max values of some features as `None` in these arrays in which case those values will be changed to `-np.inf` and `np.inf` while being cast as float arrays.

Since these changes do not modify the default behavior of `IterativeImputer`, I do not expect them to break any tests. However, I have no experience with testing while contributing to sklearn, so let me know if there is some work that I need to do wrt testing. 

<!--
Please be aware that we are a loose team of volunteers so patience is
necessary; assistance handling other issues is very welcome. We value
all user contributions, no matter how minor they are. If we are slow to
review, either the pull request needs some benchmarking, tinkering,
convincing, etc. or more likely the reviewers are simply busy. In either
case, we ask for your understanding during the review process.
For more information, see our FAQ on this topic:
http://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->


Problem statement:
Provide different max and min values for each feature with IterativeImputer
Currently, `impute.IterativeImputer` only allows the specification of a single set of min and max values to be used to set limits on the imputed values of all features. In many practical problems, however, different features can have very different ranges. I am proposing to modify `IterativeImputer` to accept a list of max and min values, one for each feature.

Proposed solution:
1. `max_value` and `min_value` will be list/array-like, with one value for each feature to be imputed
2. We can leave the default values as `None`, in which case the `max_value` list will be filled by `np.inf` and the `min_value` list will be filled by `-np.inf`

Is modifying `_iterative.py` the way to go ahead with this change? I wanted to consult people before putting in a pull request.

Hints from issue discussion:
Is scaling features before imputation good enough?

@jnothman 
I tried doing z-scaling (subtract mean, divide by SD) but using a Bayesian ridge regression model, I would still sometimes get feature values which didn't make sense. A substantial number of my features are percentages and are bound to [0, 100], some are positive-only or negative-only while others can vary over the entire [-inf, +inf] range.

Apart from this range problem with the feature values, the Bayesian regression model works remarkably well in my problem scenario. I actually tested a quick hack on `IterativeImputer` with different ranges for the features, and the results are really good for me.

Were you thinking of any other way of scaling features than z-scaling? 
Basically, we could imagine using a `ColumnTransfomer` allowing to apply different scaler for different columns. I am scared that it will be tedious to give arrays for `min` and `max` for all features.
@glemaitre 
Thanks for bringing up the `ColumnTransformer` - my limited understanding was that the `ColumnTransformer` can be used as a pre-processing step, to compose different types of transformations before the features are fed into whatever model one is using.

The thing that seems to be different with `IterativeImputer` is that it runs several iterations of imputations (and every feature is imputed in a round-robin fashion on each iteration). The feature "clipping" (if the values went outside the permitted range) happens at the end of each iteration, and those "clipped and imputed" features are then used at the next iteration of imputation. Here's where it happens:
https://github.com/scikit-learn/scikit-learn/blob/b194674c4/sklearn/impute/_iterative.py#L343

I am not sure that the `ColumnTransformer` can achieve this effect of clipping feature values at the end of every iteration of imputation because:
1. The feature values are being clipped at the end of each iteration, and not being pre-processed before being fed into the imputer.
2. The `ColumnTransformer` can only transform columns once, before they are fed into a model (here the `IterativeImputer`), not at the end of every iteration that the model runs.

As I said, I have a limited understanding of the `ColumnTransformer`, so I might be missing something - but that was why I didn't think it was appropriate to use it with the `IterativeImputer` 
What I meant was something like this:

```python
preprocessor = make_pipeline(
    make_column_transformer(
        (StandardScaler(), some_cols),
        (MinMaxScaler(), some_other_cols),
    ),
    iterativeImputer()
)
```

So basically, the `IterativeImputer` will be fed with a dataset with features which will be scale in different way. However, the `ColumnTransformer` will not intervene inside the `IterativeImputer` fitting.
@glemaitre 
I see, thanks for the example and for your help! :) 

I guess I am still confused about my basic question - with the preprocessor being set up that way, as you said, the `ColumnTransformer` will not intervene inside the `IterativeImputer` fitting. Does that mean the intermediate iterations of the `IterativeImputer` will still be free to have say, negative values for a feature bound to [0, 100] and those negative values will only get "clipped off" at the very end of the `IterativeImputer` fitting, once all iterations of imputing are complete? If that is true, will that not create a problem with the iterative process of imputation itself?
Hi everyone,

I looked into and thought about this a bit more, and I think that I might not have been clear about my specific problem statement here. I do not think that the `preprocessor` setup suggested by @glemaitre will work for my situation because my data, on its own, doesn't have feature values that are outside their desired ranges. Instead, it has missing values, which is why I am imputing those values using the `IterativeImputer` - and that's where the problem happens: the imputer fills in those missing values (sometimes) by values that fall way outside the feature's desired range.

I next use the "imputed" dataset (which now has feature values going outside their defined ranges) in a downstream classification model (lgbm). I could, in that scenario, use the `ColumnTransformer` with min-max scaling on the imputed dataset that goes into the lgbm model - but that will again be incorrect in my opinion. What I have are a subset of feature values that go out of range - but these are outliers (which happen in the imputer's prediction step), and changing other feature values (which fall into the correct range and hence are correctly imputed predictions) because of these outliers will be wrong.

In that scenario, my option is to just clip these outlier values to their range limits at the end of the imputation, but I do not want to do that precisely because the feature clipping is being restricted to the very end of the imputation process. The success and robustness of the `IterativeImputer` comes from the fact that it performs several rounds/iterations of imputation, and uses the feature values from the last imputation step in the next one. Feature value clipping, then, needs to be done at the end of every iteration of imputation, so that the wrongly imputed feature values at each step do not affect the next iteration of imputation. This is precisely what the current implementation of `IterativeImputer` does - except that it doesn't let you specify different min and max values for the different features being imputed. And that last bit of missing functionality is what I propose to address here.

I do not think that having the min and max values be specified as arrays/lists will add additional assumption or usage overhead to the current `IterativeImputer` because:
1. The current imputer already kinda assumes that the features being imputed are numeric, and fixes the min and max imputed values for all features at -inf and +inf respectively.
2. By default, the implementation I am proposing will continue fixing -inf and +inf as min and max values for all features. If the user wants to specify say 0 as the lower limit and 100 as the upper limit for all features, they can just pass in 0 and 100 as the max and min value and we can internally broadcast those scalars to the number of features. It is only when the user wants very different feature ranges for every single feature will they have to do significant additional work to fill in these lists - and that is the desired behavior because it can be very useful (almost essential) to have the ability to do so in many applications (like mine).

Let me know what you guys think, and I can put in a pull request accordingly. I really appreciate all the time and effort you put into this project :)   
@jnothman @glemaitre Just wondering if you guys have any thoughts on my response above? Thanks in advance for your time :)
I'd be happy to see your solution implemented. Would you like to submit a
pull request?

@jnothman @glemaitre I put in a pull request with my changes - have a look and let me know how to proceed.

Commit message: Added description to what's new

Patch to review:
diff --git a/doc/whats_new/v0.23.rst b/doc/whats_new/v0.23.rst
index 45219b8346b35..ea5ef8e230545 100644
--- a/doc/whats_new/v0.23.rst
+++ b/doc/whats_new/v0.23.rst
@@ -51,16 +51,16 @@ Changelog
   more memory efficient implementation of single linkage clustering.
   :pr:`11514` by :user:`Leland McInnes <lmcinnes>`.
 
-- |Efficiency| :class:`cluster.Birch` implementation of the predict method 
-  avoids high memory footprint by calculating the distances matrix using 
+- |Efficiency| :class:`cluster.Birch` implementation of the predict method
+  avoids high memory footprint by calculating the distances matrix using
   a chunked scheme.
-  :pr:`16149` by :user:`Jeremie du Boisberranger <jeremiedbb>` and 
+  :pr:`16149` by :user:`Jeremie du Boisberranger <jeremiedbb>` and
   :user:`Alex Shacked <alexshacked>`.
 
 :mod:`sklearn.datasets`
 .......................
 
-- |Enhancement| Added ``return_centers`` parameter  in  
+- |Enhancement| Added ``return_centers`` parameter  in
   :func:`datasets.make_blobs`, which can be used to return
   centers for each cluster.
   :pr:`15709` by :user:`<shivamgargsya>` and
@@ -116,7 +116,13 @@ Changelog
 
 - |Enhancement| :func:`gaussian_process.kernels.Matern` returns the RBF kernel when ``nu=np.inf``.
   :pr:`15503` by :user:`Sam Dixon <sam-dixon>`.
-  
+
+:mod:`sklearn.impute`
+.....................
+
+- |Enhancement| :class:`impute.IterativeImputer` accepts both scalar and array-like inputs for
+  ``max_value`` and ``min_value``. :pr:`16403` by :user:`Narendra Mukherjee <narendramukherjee>`.
+
 :mod:`sklearn.linear_model`
 ...........................
 
@@ -165,10 +171,10 @@ Changelog
 :mod:`sklearn.neighbors`
 ..............................
 
-- |Fix| Fix a bug which converted a list of arrays into a 2-D object 
+- |Fix| Fix a bug which converted a list of arrays into a 2-D object
   array instead of a 1-D array containing NumPy arrays. This bug
   was affecting :meth:`neighbors.NearestNeighbors.radius_neighbors`.
-  :pr:`16076` by :user:`Guillaume Lemaitre <glemaitre>` and  
+  :pr:`16076` by :user:`Guillaume Lemaitre <glemaitre>` and
   :user:`Alex Shacked <alexshacked>`.
 
 :mod:`sklearn.neural_network`
diff --git a/sklearn/impute/_iterative.py b/sklearn/impute/_iterative.py
index fb3d4470c8e6e..f939e82223d1b 100644
--- a/sklearn/impute/_iterative.py
+++ b/sklearn/impute/_iterative.py
@@ -11,9 +11,9 @@
 from ..base import clone
 from ..exceptions import ConvergenceWarning
 from ..preprocessing import normalize
-from ..utils import check_array, check_random_state, _safe_indexing
+from ..utils import (check_array, check_random_state, _safe_indexing,
+                     as_float_array, is_scalar_nan)
 from ..utils.validation import FLOAT_DTYPES, check_is_fitted
-from ..utils import is_scalar_nan
 from ..utils._mask import _get_mask
 
 from ._base import _BaseImputer
@@ -111,13 +111,15 @@ class IterativeImputer(_BaseImputer):
         many features with no missing values at both ``fit`` and ``transform``
         time to save compute.
 
-    min_value : float, default=None
-        Minimum possible imputed value. Default of ``None`` will set minimum
-        to negative infinity.
+    min_value : float or array-like of shape (n_features,), default=None.
+        Minimum possible imputed value. Broadcast to shape (n_features,) if
+        scalar. Expects shape (n_features,) if array-like, one min value for
+        each feature. Default of None is converted to -np.inf.
 
-    max_value : float, default=None
-        Maximum possible imputed value. Default of ``None`` will set maximum
-        to positive infinity.
+    max_value : float or array-like of shape (n_features,), default=None.
+        Maximum possible imputed value. Broadcast to shape (n_features,) if
+        scalar. Expects shape (n_features,) if array-like, one max value for
+        each feature.Default of None is converted to np.inf.
 
     verbose : int, default=0
         Verbosity flag, controls the debug messages that are issued
@@ -174,7 +176,7 @@ class IterativeImputer(_BaseImputer):
     Examples
     --------
     >>> import numpy as np
-    >>> from sklearn.experimental import enable_iterative_imputer  
+    >>> from sklearn.experimental import enable_iterative_imputer
     >>> from sklearn.impute import IterativeImputer
     >>> imp_mean = IterativeImputer(random_state=0)
     >>> imp_mean.fit([[7, 2, 3], [4, np.nan, 6], [10, 5, 9]])
@@ -296,9 +298,9 @@ def _impute_one_feature(self,
         missing_row_mask = mask_missing_values[:, feat_idx]
         if fit_mode:
             X_train = _safe_indexing(X_filled[:, neighbor_feat_idx],
-                                    ~missing_row_mask)
+                                     ~missing_row_mask)
             y_train = _safe_indexing(X_filled[:, feat_idx],
-                                    ~missing_row_mask)
+                                     ~missing_row_mask)
             estimator.fit(X_train, y_train)
 
         # if no missing values, don't predict
@@ -316,16 +318,16 @@ def _impute_one_feature(self,
             # (results in inf sample)
             positive_sigmas = sigmas > 0
             imputed_values[~positive_sigmas] = mus[~positive_sigmas]
-            mus_too_low = mus < self._min_value
-            imputed_values[mus_too_low] = self._min_value
-            mus_too_high = mus > self._max_value
-            imputed_values[mus_too_high] = self._max_value
+            mus_too_low = mus < self._min_value[feat_idx]
+            imputed_values[mus_too_low] = self._min_value[feat_idx]
+            mus_too_high = mus > self._max_value[feat_idx]
+            imputed_values[mus_too_high] = self._max_value[feat_idx]
             # the rest can be sampled without statistical issues
             inrange_mask = positive_sigmas & ~mus_too_low & ~mus_too_high
             mus = mus[inrange_mask]
             sigmas = sigmas[inrange_mask]
-            a = (self._min_value - mus) / sigmas
-            b = (self._max_value - mus) / sigmas
+            a = (self._min_value[feat_idx] - mus) / sigmas
+            b = (self._max_value[feat_idx] - mus) / sigmas
 
             if scipy.__version__ < LooseVersion('0.18'):
                 # bug with vector-valued `a` in old scipy
@@ -343,8 +345,8 @@ def _impute_one_feature(self,
         else:
             imputed_values = estimator.predict(X_test)
             imputed_values = np.clip(imputed_values,
-                                     self._min_value,
-                                     self._max_value)
+                                     self._min_value[feat_idx],
+                                     self._max_value[feat_idx])
 
         # update the feature
         X_filled[missing_row_mask, feat_idx] = imputed_values
@@ -565,9 +567,6 @@ def fit_transform(self, X, y=None):
 
         self.imputation_sequence_ = []
 
-        self._min_value = -np.inf if self.min_value is None else self.min_value
-        self._max_value = np.inf if self.max_value is None else self.max_value
-
         self.initial_imputer_ = None
         super()._fit_indicator(X)
         X_indicator = super()._transform_indicator(X)
@@ -581,6 +580,37 @@ def fit_transform(self, X, y=None):
             self.n_iter_ = 0
             return super()._concatenate_indicator(Xt, X_indicator)
 
+        # If min or max value are (scalar) None, convert them to - or + inf
+        self._min_value = -np.inf if self.min_value is None else self.min_value
+        self._max_value = np.inf if self.max_value is None else self.max_value
+        # Convert min and max values to shape=(n_features,) if they are scalars
+        if np.isscalar(self._min_value):
+            self._min_value = np.full(X.shape[1], self._min_value)
+        if np.isscalar(self._max_value):
+            self._max_value = np.full(X.shape[1], self._max_value)
+        # If there were any None values in above arrays, convert them to np.nan
+        self._min_value = as_float_array(self._min_value,
+                                         force_all_finite=False)
+        self._max_value = as_float_array(self._max_value,
+                                         force_all_finite=False)
+        # And finally, convert all np.nan to - or + inf
+        self._min_value[np.isnan(self._min_value)] = -np.inf
+        self._max_value[np.isnan(self._max_value)] = np.inf
+
+        if not (self._min_value.shape[0] == X.shape[1]):
+            raise ValueError(
+                f"'min_value' should be of shape ({X.shape[1]},)"
+                "when an array-like is provided"
+                f"Got {self._min_value.shape}, instead.")
+        if not (self._max_value.shape[0] == X.shape[1]):
+            raise ValueError(
+                f"'max_value' should be of shape ({X.shape[1]},)"
+                "when an array-like is provided"
+                f"Got {self._max_value.shape}, instead.")
+        if not np.all(np.greater(self._max_value, self._min_value)):
+            raise ValueError(
+                "One (or more) features have min_value >= max_value.")
+
         # order in which to impute
         # note this is probably too slow for large feature data (d > 100000)
         # and a better way would be good.
diff --git a/sklearn/impute/tests/test_impute.py b/sklearn/impute/tests/test_impute.py
index 052a99908c569..bd5c0f6dd8350 100644
--- a/sklearn/impute/tests/test_impute.py
+++ b/sklearn/impute/tests/test_impute.py
@@ -971,6 +971,58 @@ def test_iterative_imputer_catch_warning():
     assert not np.any(np.isnan(X_fill))
 
 
+@pytest.mark.parametrize("min_value, max_value, correct_output",
+                         [(0,  # scalars cast to the correct arrays?
+                           100,
+                           np.array([[0] * 3, [100] * 3])),
+                          ([0, 0, 0],  # vectors cast to the same
+                           [100, 100, 100],  # correct arrays?
+                           np.array([[0] * 3, [100] * 3])),
+                          (None,  # None cast to vector of inf?
+                           None,
+                           np.array([[-np.inf] * 3, [np.inf] * 3])),
+                          (-np.inf,  # inf cast to vector of inf?
+                           np.inf,
+                           np.array([[-np.inf] * 3, [np.inf] * 3])),
+                          ([-5, 5, 10],  # vectors processed properly?
+                           [100, 200, 300],
+                           np.array([[-5, 5, 10], [100, 200, 300]])),
+                          ([-5, None, 10],  # vectors of numbers+None
+                           [100, 200, None],  # cast correctly?
+                           np.array([[-5, -np.inf, 10], [100, 200, np.inf]]))])
+def test_iterative_imputer_min_max_array_like(min_value,
+                                              max_value,
+                                              correct_output):
+    # check that passing scalar or array-like
+    # for min_value and max_value in IterativeImputer works
+    X = np.random.random((10, 3))
+    imputer = IterativeImputer(min_value=min_value, max_value=max_value)
+    imputer.fit(X)
+
+    assert (isinstance(imputer._min_value, np.ndarray) and
+            isinstance(imputer._max_value, np.ndarray))
+    assert ((imputer._min_value.shape[0] == X.shape[1]) and
+            (imputer._max_value.shape[0] == X.shape[1]))
+
+    assert_array_equal(correct_output[0, :], imputer._min_value)
+    assert_array_equal(correct_output[1, :], imputer._max_value)
+
+
+@pytest.mark.parametrize("min_value, max_value",
+                         [(100, 0), (np.inf, -np.inf),
+                          ([-5, 5], [100, 200, 0])])
+def test_iterative_imputer_catch_min_max_error(min_value, max_value):
+    # check that passing scalar or array-like
+    # for min_value and max_value in IterativeImputer works
+    # Test checks 1.) if an exception is raised if min_value >= max_value
+    # and 2.) an exception is raised if shape of min_value or max_value is
+    # not equal to the number of features
+    X = np.random.random((10, 3))
+    imputer = IterativeImputer(min_value=min_value, max_value=max_value)
+    with pytest.raises(ValueError):
+        imputer.fit(X)
+
+
 @pytest.mark.parametrize(
     "skip_complete", [True, False]
 )


Code review comments:
diff_hunk: @@ -581,6 +580,37 @@ def fit_transform(self, X, y=None):
             self.n_iter_ = 0
             return super()._concatenate_indicator(Xt, X_indicator)
 
+        # If min or max value are (scalar) None, convert them to - or + inf
path: sklearn/impute/_iterative.py
text: @user1:
Perhaps pull this out into a separate _validate_limits method?

@user2:
I agree with Joel. Also remove all comments. The code is self-explanatory. We try to limit comment for aspect which is not trivial by just reading the code. If the operation is not obvious, it might mean that we need to change the code then :)

@user2:
To be more explicit, I think that `_validate_limit` should only validate a single limit. We have the exact same sequence of operation for min and max. The differences should be triggered by the parameter passed to this method.

@user2:
So the function should be:

```python
@user3
    def _validate_limit(limit, limit_type, n_features):
        limit_bound = np.inf if limit_type == "max" else -np.inf
        limit = limit_bound if limit is None else limit
        if np.isscalar(limit):
            limit = np.full(n_features, limit)
        limit = check_array(
            limit, force_all_finite=False, copy=False, ensure_2d=False
        )
        if not limit.shape[0] == n_features:
            raise ValueError(
            f"'{limit_type}_value' should be of shape ({n_features},) when "
            f"an array-like is provided. Got {limit.shape}, instead."
        )
        return limit
```

@author:
Done - thank you for fleshing out the method :)

diff_hunk: @@ -971,6 +971,58 @@ def test_iterative_imputer_catch_warning():
     assert not np.any(np.isnan(X_fill))
 
 
+@pytest.mark.parametrize("min_value, max_value, correct_output",
+                         [(0,  # scalars cast to the correct arrays?
+                           100,
+                           np.array([[0] * 3, [100] * 3])),
+                          ([0, 0, 0],  # vectors cast to the same
+                           [100, 100, 100],  # correct arrays?
+                           np.array([[0] * 3, [100] * 3])),
+                          (None,  # None cast to vector of inf?
+                           None,
+                           np.array([[-np.inf] * 3, [np.inf] * 3])),
+                          (-np.inf,  # inf cast to vector of inf?
+                           np.inf,
+                           np.array([[-np.inf] * 3, [np.inf] * 3])),
+                          ([-5, 5, 10],  # vectors processed properly?
+                           [100, 200, 300],
+                           np.array([[-5, 5, 10], [100, 200, 300]])),
+                          ([-5, None, 10],  # vectors of numbers+None
path: sklearn/impute/tests/test_impute.py
text: @user1:
We should not support the case that we have `None` when this is an array.

@author:
Done!

@author:
Done!

diff_hunk: @@ -971,6 +971,58 @@ def test_iterative_imputer_catch_warning():
     assert not np.any(np.isnan(X_fill))
 
 
+@pytest.mark.parametrize("min_value, max_value, correct_output",
+                         [(0,  # scalars cast to the correct arrays?
path: sklearn/impute/tests/test_impute.py
text: @user2:
instead of putting a comment here we can pass a parameter `ids` to name the different try. It will be easier when running the tests:

```python
@user1.mark.parametrize(
    "min_value, max_value, correct_output",
    ...,
    ids=["scalars", "list", "None-default", "inf", ...]
)
```

@author:
Done!

diff_hunk: @@ -111,13 +111,15 @@ class IterativeImputer(_BaseImputer):
         many features with no missing values at both ``fit`` and ``transform``
         time to save compute.
 
-    min_value : float, default=None
-        Minimum possible imputed value. Default of ``None`` will set minimum
-        to negative infinity.
+    min_value : float or array-like of shape (n_features,), default=None.
+        Minimum possible imputed value. Broadcast to shape (n_features,) if
+        scalar. Expects shape (n_features,) if array-like, one min value for
path: sklearn/impute/_iterative.py
text: @user1:
I would turn the sentence around `If array-like, the array should be of shape ...

@author:
Done!

diff_hunk: @@ -581,6 +580,37 @@ def fit_transform(self, X, y=None):
             self.n_iter_ = 0
             return super()._concatenate_indicator(Xt, X_indicator)
 
+        # If min or max value are (scalar) None, convert them to - or + inf
+        self._min_value = -np.inf if self.min_value is None else self.min_value
+        self._max_value = np.inf if self.max_value is None else self.max_value
+        # Convert min and max values to shape=(n_features,) if they are scalars
+        if np.isscalar(self._min_value):
+            self._min_value = np.full(X.shape[1], self._min_value)
+        if np.isscalar(self._max_value):
+            self._max_value = np.full(X.shape[1], self._max_value)
+        # If there were any None values in above arrays, convert them to np.nan
+        self._min_value = as_float_array(self._min_value,
path: sklearn/impute/_iterative.py
text: @user1:
do not use `as_float_array`. Use `check_array` instead.

@author:
All these pulled into the `_validate_limit` method as you suggested :)

diff_hunk: @@ -971,6 +971,58 @@ def test_iterative_imputer_catch_warning():
     assert not np.any(np.isnan(X_fill))
 
 
+@pytest.mark.parametrize("min_value, max_value, correct_output",
+                         [(0,  # scalars cast to the correct arrays?
+                           100,
+                           np.array([[0] * 3, [100] * 3])),
+                          ([0, 0, 0],  # vectors cast to the same
+                           [100, 100, 100],  # correct arrays?
+                           np.array([[0] * 3, [100] * 3])),
+                          (None,  # None cast to vector of inf?
+                           None,
+                           np.array([[-np.inf] * 3, [np.inf] * 3])),
+                          (-np.inf,  # inf cast to vector of inf?
+                           np.inf,
+                           np.array([[-np.inf] * 3, [np.inf] * 3])),
+                          ([-5, 5, 10],  # vectors processed properly?
+                           [100, 200, 300],
+                           np.array([[-5, 5, 10], [100, 200, 300]])),
+                          ([-5, None, 10],  # vectors of numbers+None
+                           [100, 200, None],  # cast correctly?
+                           np.array([[-5, -np.inf, 10], [100, 200, np.inf]]))])
+def test_iterative_imputer_min_max_array_like(min_value,
+                                              max_value,
+                                              correct_output):
+    # check that passing scalar or array-like
+    # for min_value and max_value in IterativeImputer works
+    X = np.random.random((10, 3))
+    imputer = IterativeImputer(min_value=min_value, max_value=max_value)
+    imputer.fit(X)
+
+    assert (isinstance(imputer._min_value, np.ndarray) and
+            isinstance(imputer._max_value, np.ndarray))
+    assert ((imputer._min_value.shape[0] == X.shape[1]) and
+            (imputer._max_value.shape[0] == X.shape[1]))
+
+    assert_array_equal(correct_output[0, :], imputer._min_value)
+    assert_array_equal(correct_output[1, :], imputer._max_value)
+
+
+@pytest.mark.parametrize("min_value, max_value",
+                         [(100, 0), (np.inf, -np.inf),
+                          ([-5, 5], [100, 200, 0])])
+def test_iterative_imputer_catch_min_max_error(min_value, max_value):
+    # check that passing scalar or array-like
+    # for min_value and max_value in IterativeImputer works
path: sklearn/impute/tests/test_impute.py
text: @user1:
We don't need comments as well. For instance here we cannot know the the case that you are testing in practise because you are not matching the error message.

Thus if you are doing `pytest.raises(ValueError, match="err_msg")`, you can pass the expected (partial error messgae). `min_value should be of shape` or `One (or more) ...`. Then the test will be self explenatory.

@author:
Done!