1789 lines
59 KiB
Python
1789 lines
59 KiB
Python
|
import io
|
||
|
import re
|
||
|
import warnings
|
||
|
from itertools import product
|
||
|
|
||
|
import numpy as np
|
||
|
import pytest
|
||
|
from scipy import sparse
|
||
|
from scipy.stats import kstest
|
||
|
|
||
|
from sklearn import tree
|
||
|
from sklearn.datasets import load_diabetes
|
||
|
from sklearn.dummy import DummyRegressor
|
||
|
from sklearn.exceptions import ConvergenceWarning
|
||
|
|
||
|
# make IterativeImputer available
|
||
|
from sklearn.experimental import enable_iterative_imputer # noqa
|
||
|
from sklearn.impute import IterativeImputer, KNNImputer, MissingIndicator, SimpleImputer
|
||
|
from sklearn.impute._base import _most_frequent
|
||
|
from sklearn.linear_model import ARDRegression, BayesianRidge, RidgeCV
|
||
|
from sklearn.model_selection import GridSearchCV
|
||
|
from sklearn.pipeline import Pipeline, make_union
|
||
|
from sklearn.random_projection import _sparse_random_matrix
|
||
|
from sklearn.utils._testing import (
|
||
|
_convert_container,
|
||
|
assert_allclose,
|
||
|
assert_allclose_dense_sparse,
|
||
|
assert_array_almost_equal,
|
||
|
assert_array_equal,
|
||
|
)
|
||
|
from sklearn.utils.fixes import (
|
||
|
BSR_CONTAINERS,
|
||
|
COO_CONTAINERS,
|
||
|
CSC_CONTAINERS,
|
||
|
CSR_CONTAINERS,
|
||
|
LIL_CONTAINERS,
|
||
|
)
|
||
|
|
||
|
|
||
|
def _assert_array_equal_and_same_dtype(x, y):
|
||
|
assert_array_equal(x, y)
|
||
|
assert x.dtype == y.dtype
|
||
|
|
||
|
|
||
|
def _assert_allclose_and_same_dtype(x, y):
|
||
|
assert_allclose(x, y)
|
||
|
assert x.dtype == y.dtype
|
||
|
|
||
|
|
||
|
def _check_statistics(
|
||
|
X, X_true, strategy, statistics, missing_values, sparse_container
|
||
|
):
|
||
|
"""Utility function for testing imputation for a given strategy.
|
||
|
|
||
|
Test with dense and sparse arrays
|
||
|
|
||
|
Check that:
|
||
|
- the statistics (mean, median, mode) are correct
|
||
|
- the missing values are imputed correctly"""
|
||
|
|
||
|
err_msg = "Parameters: strategy = %s, missing_values = %s, sparse = {0}" % (
|
||
|
strategy,
|
||
|
missing_values,
|
||
|
)
|
||
|
|
||
|
assert_ae = assert_array_equal
|
||
|
|
||
|
if X.dtype.kind == "f" or X_true.dtype.kind == "f":
|
||
|
assert_ae = assert_array_almost_equal
|
||
|
|
||
|
# Normal matrix
|
||
|
imputer = SimpleImputer(missing_values=missing_values, strategy=strategy)
|
||
|
X_trans = imputer.fit(X).transform(X.copy())
|
||
|
assert_ae(imputer.statistics_, statistics, err_msg=err_msg.format(False))
|
||
|
assert_ae(X_trans, X_true, err_msg=err_msg.format(False))
|
||
|
|
||
|
# Sparse matrix
|
||
|
imputer = SimpleImputer(missing_values=missing_values, strategy=strategy)
|
||
|
imputer.fit(sparse_container(X))
|
||
|
X_trans = imputer.transform(sparse_container(X.copy()))
|
||
|
|
||
|
if sparse.issparse(X_trans):
|
||
|
X_trans = X_trans.toarray()
|
||
|
|
||
|
assert_ae(imputer.statistics_, statistics, err_msg=err_msg.format(True))
|
||
|
assert_ae(X_trans, X_true, err_msg=err_msg.format(True))
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("strategy", ["mean", "median", "most_frequent", "constant"])
|
||
|
@pytest.mark.parametrize("csr_container", CSR_CONTAINERS)
|
||
|
def test_imputation_shape(strategy, csr_container):
|
||
|
# Verify the shapes of the imputed matrix for different strategies.
|
||
|
X = np.random.randn(10, 2)
|
||
|
X[::2] = np.nan
|
||
|
|
||
|
imputer = SimpleImputer(strategy=strategy)
|
||
|
X_imputed = imputer.fit_transform(csr_container(X))
|
||
|
assert X_imputed.shape == (10, 2)
|
||
|
X_imputed = imputer.fit_transform(X)
|
||
|
assert X_imputed.shape == (10, 2)
|
||
|
|
||
|
iterative_imputer = IterativeImputer(initial_strategy=strategy)
|
||
|
X_imputed = iterative_imputer.fit_transform(X)
|
||
|
assert X_imputed.shape == (10, 2)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("strategy", ["mean", "median", "most_frequent"])
|
||
|
def test_imputation_deletion_warning(strategy):
|
||
|
X = np.ones((3, 5))
|
||
|
X[:, 0] = np.nan
|
||
|
imputer = SimpleImputer(strategy=strategy).fit(X)
|
||
|
|
||
|
with pytest.warns(UserWarning, match="Skipping"):
|
||
|
imputer.transform(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("strategy", ["mean", "median", "most_frequent"])
|
||
|
def test_imputation_deletion_warning_feature_names(strategy):
|
||
|
pd = pytest.importorskip("pandas")
|
||
|
|
||
|
missing_values = np.nan
|
||
|
feature_names = np.array(["a", "b", "c", "d"], dtype=object)
|
||
|
X = pd.DataFrame(
|
||
|
[
|
||
|
[missing_values, missing_values, 1, missing_values],
|
||
|
[4, missing_values, 2, 10],
|
||
|
],
|
||
|
columns=feature_names,
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(strategy=strategy).fit(X)
|
||
|
|
||
|
# check SimpleImputer returning feature name attribute correctly
|
||
|
assert_array_equal(imputer.feature_names_in_, feature_names)
|
||
|
|
||
|
# ensure that skipped feature warning includes feature name
|
||
|
with pytest.warns(
|
||
|
UserWarning, match=r"Skipping features without any observed values: \['b'\]"
|
||
|
):
|
||
|
imputer.transform(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("strategy", ["mean", "median", "most_frequent", "constant"])
|
||
|
@pytest.mark.parametrize("csc_container", CSC_CONTAINERS)
|
||
|
def test_imputation_error_sparse_0(strategy, csc_container):
|
||
|
# check that error are raised when missing_values = 0 and input is sparse
|
||
|
X = np.ones((3, 5))
|
||
|
X[0] = 0
|
||
|
X = csc_container(X)
|
||
|
|
||
|
imputer = SimpleImputer(strategy=strategy, missing_values=0)
|
||
|
with pytest.raises(ValueError, match="Provide a dense array"):
|
||
|
imputer.fit(X)
|
||
|
|
||
|
imputer.fit(X.toarray())
|
||
|
with pytest.raises(ValueError, match="Provide a dense array"):
|
||
|
imputer.transform(X)
|
||
|
|
||
|
|
||
|
def safe_median(arr, *args, **kwargs):
|
||
|
# np.median([]) raises a TypeError for numpy >= 1.10.1
|
||
|
length = arr.size if hasattr(arr, "size") else len(arr)
|
||
|
return np.nan if length == 0 else np.median(arr, *args, **kwargs)
|
||
|
|
||
|
|
||
|
def safe_mean(arr, *args, **kwargs):
|
||
|
# np.mean([]) raises a RuntimeWarning for numpy >= 1.10.1
|
||
|
length = arr.size if hasattr(arr, "size") else len(arr)
|
||
|
return np.nan if length == 0 else np.mean(arr, *args, **kwargs)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("csc_container", CSC_CONTAINERS)
|
||
|
def test_imputation_mean_median(csc_container):
|
||
|
# Test imputation using the mean and median strategies, when
|
||
|
# missing_values != 0.
|
||
|
rng = np.random.RandomState(0)
|
||
|
|
||
|
dim = 10
|
||
|
dec = 10
|
||
|
shape = (dim * dim, dim + dec)
|
||
|
|
||
|
zeros = np.zeros(shape[0])
|
||
|
values = np.arange(1, shape[0] + 1)
|
||
|
values[4::2] = -values[4::2]
|
||
|
|
||
|
tests = [
|
||
|
("mean", np.nan, lambda z, v, p: safe_mean(np.hstack((z, v)))),
|
||
|
("median", np.nan, lambda z, v, p: safe_median(np.hstack((z, v)))),
|
||
|
]
|
||
|
|
||
|
for strategy, test_missing_values, true_value_fun in tests:
|
||
|
X = np.empty(shape)
|
||
|
X_true = np.empty(shape)
|
||
|
true_statistics = np.empty(shape[1])
|
||
|
|
||
|
# Create a matrix X with columns
|
||
|
# - with only zeros,
|
||
|
# - with only missing values
|
||
|
# - with zeros, missing values and values
|
||
|
# And a matrix X_true containing all true values
|
||
|
for j in range(shape[1]):
|
||
|
nb_zeros = (j - dec + 1 > 0) * (j - dec + 1) * (j - dec + 1)
|
||
|
nb_missing_values = max(shape[0] + dec * dec - (j + dec) * (j + dec), 0)
|
||
|
nb_values = shape[0] - nb_zeros - nb_missing_values
|
||
|
|
||
|
z = zeros[:nb_zeros]
|
||
|
p = np.repeat(test_missing_values, nb_missing_values)
|
||
|
v = values[rng.permutation(len(values))[:nb_values]]
|
||
|
|
||
|
true_statistics[j] = true_value_fun(z, v, p)
|
||
|
|
||
|
# Create the columns
|
||
|
X[:, j] = np.hstack((v, z, p))
|
||
|
|
||
|
if 0 == test_missing_values:
|
||
|
# XXX unreached code as of v0.22
|
||
|
X_true[:, j] = np.hstack(
|
||
|
(v, np.repeat(true_statistics[j], nb_missing_values + nb_zeros))
|
||
|
)
|
||
|
else:
|
||
|
X_true[:, j] = np.hstack(
|
||
|
(v, z, np.repeat(true_statistics[j], nb_missing_values))
|
||
|
)
|
||
|
|
||
|
# Shuffle them the same way
|
||
|
np.random.RandomState(j).shuffle(X[:, j])
|
||
|
np.random.RandomState(j).shuffle(X_true[:, j])
|
||
|
|
||
|
# Mean doesn't support columns containing NaNs, median does
|
||
|
if strategy == "median":
|
||
|
cols_to_keep = ~np.isnan(X_true).any(axis=0)
|
||
|
else:
|
||
|
cols_to_keep = ~np.isnan(X_true).all(axis=0)
|
||
|
|
||
|
X_true = X_true[:, cols_to_keep]
|
||
|
|
||
|
_check_statistics(
|
||
|
X, X_true, strategy, true_statistics, test_missing_values, csc_container
|
||
|
)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("csc_container", CSC_CONTAINERS)
|
||
|
def test_imputation_median_special_cases(csc_container):
|
||
|
# Test median imputation with sparse boundary cases
|
||
|
X = np.array(
|
||
|
[
|
||
|
[0, np.nan, np.nan], # odd: implicit zero
|
||
|
[5, np.nan, np.nan], # odd: explicit nonzero
|
||
|
[0, 0, np.nan], # even: average two zeros
|
||
|
[-5, 0, np.nan], # even: avg zero and neg
|
||
|
[0, 5, np.nan], # even: avg zero and pos
|
||
|
[4, 5, np.nan], # even: avg nonzeros
|
||
|
[-4, -5, np.nan], # even: avg negatives
|
||
|
[-1, 2, np.nan], # even: crossing neg and pos
|
||
|
]
|
||
|
).transpose()
|
||
|
|
||
|
X_imputed_median = np.array(
|
||
|
[
|
||
|
[0, 0, 0],
|
||
|
[5, 5, 5],
|
||
|
[0, 0, 0],
|
||
|
[-5, 0, -2.5],
|
||
|
[0, 5, 2.5],
|
||
|
[4, 5, 4.5],
|
||
|
[-4, -5, -4.5],
|
||
|
[-1, 2, 0.5],
|
||
|
]
|
||
|
).transpose()
|
||
|
statistics_median = [0, 5, 0, -2.5, 2.5, 4.5, -4.5, 0.5]
|
||
|
|
||
|
_check_statistics(
|
||
|
X, X_imputed_median, "median", statistics_median, np.nan, csc_container
|
||
|
)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("strategy", ["mean", "median"])
|
||
|
@pytest.mark.parametrize("dtype", [None, object, str])
|
||
|
def test_imputation_mean_median_error_invalid_type(strategy, dtype):
|
||
|
X = np.array([["a", "b", 3], [4, "e", 6], ["g", "h", 9]], dtype=dtype)
|
||
|
msg = "non-numeric data:\ncould not convert string to float:"
|
||
|
with pytest.raises(ValueError, match=msg):
|
||
|
imputer = SimpleImputer(strategy=strategy)
|
||
|
imputer.fit_transform(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("strategy", ["mean", "median"])
|
||
|
@pytest.mark.parametrize("type", ["list", "dataframe"])
|
||
|
def test_imputation_mean_median_error_invalid_type_list_pandas(strategy, type):
|
||
|
X = [["a", "b", 3], [4, "e", 6], ["g", "h", 9]]
|
||
|
if type == "dataframe":
|
||
|
pd = pytest.importorskip("pandas")
|
||
|
X = pd.DataFrame(X)
|
||
|
msg = "non-numeric data:\ncould not convert string to float:"
|
||
|
with pytest.raises(ValueError, match=msg):
|
||
|
imputer = SimpleImputer(strategy=strategy)
|
||
|
imputer.fit_transform(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("strategy", ["constant", "most_frequent"])
|
||
|
@pytest.mark.parametrize("dtype", [str, np.dtype("U"), np.dtype("S")])
|
||
|
def test_imputation_const_mostf_error_invalid_types(strategy, dtype):
|
||
|
# Test imputation on non-numeric data using "most_frequent" and "constant"
|
||
|
# strategy
|
||
|
X = np.array(
|
||
|
[
|
||
|
[np.nan, np.nan, "a", "f"],
|
||
|
[np.nan, "c", np.nan, "d"],
|
||
|
[np.nan, "b", "d", np.nan],
|
||
|
[np.nan, "c", "d", "h"],
|
||
|
],
|
||
|
dtype=dtype,
|
||
|
)
|
||
|
|
||
|
err_msg = "SimpleImputer does not support data"
|
||
|
with pytest.raises(ValueError, match=err_msg):
|
||
|
imputer = SimpleImputer(strategy=strategy)
|
||
|
imputer.fit(X).transform(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("csc_container", CSC_CONTAINERS)
|
||
|
def test_imputation_most_frequent(csc_container):
|
||
|
# Test imputation using the most-frequent strategy.
|
||
|
X = np.array(
|
||
|
[
|
||
|
[-1, -1, 0, 5],
|
||
|
[-1, 2, -1, 3],
|
||
|
[-1, 1, 3, -1],
|
||
|
[-1, 2, 3, 7],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
X_true = np.array(
|
||
|
[
|
||
|
[2, 0, 5],
|
||
|
[2, 3, 3],
|
||
|
[1, 3, 3],
|
||
|
[2, 3, 7],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
# scipy.stats.mode, used in SimpleImputer, doesn't return the first most
|
||
|
# frequent as promised in the doc but the lowest most frequent. When this
|
||
|
# test will fail after an update of scipy, SimpleImputer will need to be
|
||
|
# updated to be consistent with the new (correct) behaviour
|
||
|
_check_statistics(X, X_true, "most_frequent", [np.nan, 2, 3, 3], -1, csc_container)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("marker", [None, np.nan, "NAN", "", 0])
|
||
|
def test_imputation_most_frequent_objects(marker):
|
||
|
# Test imputation using the most-frequent strategy.
|
||
|
X = np.array(
|
||
|
[
|
||
|
[marker, marker, "a", "f"],
|
||
|
[marker, "c", marker, "d"],
|
||
|
[marker, "b", "d", marker],
|
||
|
[marker, "c", "d", "h"],
|
||
|
],
|
||
|
dtype=object,
|
||
|
)
|
||
|
|
||
|
X_true = np.array(
|
||
|
[
|
||
|
["c", "a", "f"],
|
||
|
["c", "d", "d"],
|
||
|
["b", "d", "d"],
|
||
|
["c", "d", "h"],
|
||
|
],
|
||
|
dtype=object,
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(missing_values=marker, strategy="most_frequent")
|
||
|
X_trans = imputer.fit(X).transform(X)
|
||
|
|
||
|
assert_array_equal(X_trans, X_true)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("dtype", [object, "category"])
|
||
|
def test_imputation_most_frequent_pandas(dtype):
|
||
|
# Test imputation using the most frequent strategy on pandas df
|
||
|
pd = pytest.importorskip("pandas")
|
||
|
|
||
|
f = io.StringIO("Cat1,Cat2,Cat3,Cat4\n,i,x,\na,,y,\na,j,,\nb,j,x,")
|
||
|
|
||
|
df = pd.read_csv(f, dtype=dtype)
|
||
|
|
||
|
X_true = np.array(
|
||
|
[["a", "i", "x"], ["a", "j", "y"], ["a", "j", "x"], ["b", "j", "x"]],
|
||
|
dtype=object,
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(strategy="most_frequent")
|
||
|
X_trans = imputer.fit_transform(df)
|
||
|
|
||
|
assert_array_equal(X_trans, X_true)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("X_data, missing_value", [(1, 0), (1.0, np.nan)])
|
||
|
def test_imputation_constant_error_invalid_type(X_data, missing_value):
|
||
|
# Verify that exceptions are raised on invalid fill_value type
|
||
|
X = np.full((3, 5), X_data, dtype=float)
|
||
|
X[0, 0] = missing_value
|
||
|
|
||
|
fill_value = "x"
|
||
|
err_msg = f"fill_value={fill_value!r} (of type {type(fill_value)!r}) cannot be cast"
|
||
|
with pytest.raises(ValueError, match=re.escape(err_msg)):
|
||
|
imputer = SimpleImputer(
|
||
|
missing_values=missing_value, strategy="constant", fill_value=fill_value
|
||
|
)
|
||
|
imputer.fit_transform(X)
|
||
|
|
||
|
|
||
|
def test_imputation_constant_integer():
|
||
|
# Test imputation using the constant strategy on integers
|
||
|
X = np.array([[-1, 2, 3, -1], [4, -1, 5, -1], [6, 7, -1, -1], [8, 9, 0, -1]])
|
||
|
|
||
|
X_true = np.array([[0, 2, 3, 0], [4, 0, 5, 0], [6, 7, 0, 0], [8, 9, 0, 0]])
|
||
|
|
||
|
imputer = SimpleImputer(missing_values=-1, strategy="constant", fill_value=0)
|
||
|
X_trans = imputer.fit_transform(X)
|
||
|
|
||
|
assert_array_equal(X_trans, X_true)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_constructor", CSR_CONTAINERS + [np.asarray])
|
||
|
def test_imputation_constant_float(array_constructor):
|
||
|
# Test imputation using the constant strategy on floats
|
||
|
X = np.array(
|
||
|
[
|
||
|
[np.nan, 1.1, 0, np.nan],
|
||
|
[1.2, np.nan, 1.3, np.nan],
|
||
|
[0, 0, np.nan, np.nan],
|
||
|
[1.4, 1.5, 0, np.nan],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
X_true = np.array(
|
||
|
[[-1, 1.1, 0, -1], [1.2, -1, 1.3, -1], [0, 0, -1, -1], [1.4, 1.5, 0, -1]]
|
||
|
)
|
||
|
|
||
|
X = array_constructor(X)
|
||
|
|
||
|
X_true = array_constructor(X_true)
|
||
|
|
||
|
imputer = SimpleImputer(strategy="constant", fill_value=-1)
|
||
|
X_trans = imputer.fit_transform(X)
|
||
|
|
||
|
assert_allclose_dense_sparse(X_trans, X_true)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("marker", [None, np.nan, "NAN", "", 0])
|
||
|
def test_imputation_constant_object(marker):
|
||
|
# Test imputation using the constant strategy on objects
|
||
|
X = np.array(
|
||
|
[
|
||
|
[marker, "a", "b", marker],
|
||
|
["c", marker, "d", marker],
|
||
|
["e", "f", marker, marker],
|
||
|
["g", "h", "i", marker],
|
||
|
],
|
||
|
dtype=object,
|
||
|
)
|
||
|
|
||
|
X_true = np.array(
|
||
|
[
|
||
|
["missing", "a", "b", "missing"],
|
||
|
["c", "missing", "d", "missing"],
|
||
|
["e", "f", "missing", "missing"],
|
||
|
["g", "h", "i", "missing"],
|
||
|
],
|
||
|
dtype=object,
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(
|
||
|
missing_values=marker, strategy="constant", fill_value="missing"
|
||
|
)
|
||
|
X_trans = imputer.fit_transform(X)
|
||
|
|
||
|
assert_array_equal(X_trans, X_true)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("dtype", [object, "category"])
|
||
|
def test_imputation_constant_pandas(dtype):
|
||
|
# Test imputation using the constant strategy on pandas df
|
||
|
pd = pytest.importorskip("pandas")
|
||
|
|
||
|
f = io.StringIO("Cat1,Cat2,Cat3,Cat4\n,i,x,\na,,y,\na,j,,\nb,j,x,")
|
||
|
|
||
|
df = pd.read_csv(f, dtype=dtype)
|
||
|
|
||
|
X_true = np.array(
|
||
|
[
|
||
|
["missing_value", "i", "x", "missing_value"],
|
||
|
["a", "missing_value", "y", "missing_value"],
|
||
|
["a", "j", "missing_value", "missing_value"],
|
||
|
["b", "j", "x", "missing_value"],
|
||
|
],
|
||
|
dtype=object,
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(strategy="constant")
|
||
|
X_trans = imputer.fit_transform(df)
|
||
|
|
||
|
assert_array_equal(X_trans, X_true)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("X", [[[1], [2]], [[1], [np.nan]]])
|
||
|
def test_iterative_imputer_one_feature(X):
|
||
|
# check we exit early when there is a single feature
|
||
|
imputer = IterativeImputer().fit(X)
|
||
|
assert imputer.n_iter_ == 0
|
||
|
imputer = IterativeImputer()
|
||
|
imputer.fit([[1], [2]])
|
||
|
assert imputer.n_iter_ == 0
|
||
|
imputer.fit([[1], [np.nan]])
|
||
|
assert imputer.n_iter_ == 0
|
||
|
|
||
|
|
||
|
def test_imputation_pipeline_grid_search():
|
||
|
# Test imputation within a pipeline + gridsearch.
|
||
|
X = _sparse_random_matrix(100, 100, density=0.10)
|
||
|
missing_values = X.data[0]
|
||
|
|
||
|
pipeline = Pipeline(
|
||
|
[
|
||
|
("imputer", SimpleImputer(missing_values=missing_values)),
|
||
|
("tree", tree.DecisionTreeRegressor(random_state=0)),
|
||
|
]
|
||
|
)
|
||
|
|
||
|
parameters = {"imputer__strategy": ["mean", "median", "most_frequent"]}
|
||
|
|
||
|
Y = _sparse_random_matrix(100, 1, density=0.10).toarray()
|
||
|
gs = GridSearchCV(pipeline, parameters)
|
||
|
gs.fit(X, Y)
|
||
|
|
||
|
|
||
|
def test_imputation_copy():
|
||
|
# Test imputation with copy
|
||
|
X_orig = _sparse_random_matrix(5, 5, density=0.75, random_state=0)
|
||
|
|
||
|
# copy=True, dense => copy
|
||
|
X = X_orig.copy().toarray()
|
||
|
imputer = SimpleImputer(missing_values=0, strategy="mean", copy=True)
|
||
|
Xt = imputer.fit(X).transform(X)
|
||
|
Xt[0, 0] = -1
|
||
|
assert not np.all(X == Xt)
|
||
|
|
||
|
# copy=True, sparse csr => copy
|
||
|
X = X_orig.copy()
|
||
|
imputer = SimpleImputer(missing_values=X.data[0], strategy="mean", copy=True)
|
||
|
Xt = imputer.fit(X).transform(X)
|
||
|
Xt.data[0] = -1
|
||
|
assert not np.all(X.data == Xt.data)
|
||
|
|
||
|
# copy=False, dense => no copy
|
||
|
X = X_orig.copy().toarray()
|
||
|
imputer = SimpleImputer(missing_values=0, strategy="mean", copy=False)
|
||
|
Xt = imputer.fit(X).transform(X)
|
||
|
Xt[0, 0] = -1
|
||
|
assert_array_almost_equal(X, Xt)
|
||
|
|
||
|
# copy=False, sparse csc => no copy
|
||
|
X = X_orig.copy().tocsc()
|
||
|
imputer = SimpleImputer(missing_values=X.data[0], strategy="mean", copy=False)
|
||
|
Xt = imputer.fit(X).transform(X)
|
||
|
Xt.data[0] = -1
|
||
|
assert_array_almost_equal(X.data, Xt.data)
|
||
|
|
||
|
# copy=False, sparse csr => copy
|
||
|
X = X_orig.copy()
|
||
|
imputer = SimpleImputer(missing_values=X.data[0], strategy="mean", copy=False)
|
||
|
Xt = imputer.fit(X).transform(X)
|
||
|
Xt.data[0] = -1
|
||
|
assert not np.all(X.data == Xt.data)
|
||
|
|
||
|
# Note: If X is sparse and if missing_values=0, then a (dense) copy of X is
|
||
|
# made, even if copy=False.
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_zero_iters():
|
||
|
rng = np.random.RandomState(0)
|
||
|
|
||
|
n = 100
|
||
|
d = 10
|
||
|
X = _sparse_random_matrix(n, d, density=0.10, random_state=rng).toarray()
|
||
|
missing_flag = X == 0
|
||
|
X[missing_flag] = np.nan
|
||
|
|
||
|
imputer = IterativeImputer(max_iter=0)
|
||
|
X_imputed = imputer.fit_transform(X)
|
||
|
# with max_iter=0, only initial imputation is performed
|
||
|
assert_allclose(X_imputed, imputer.initial_imputer_.transform(X))
|
||
|
|
||
|
# repeat but force n_iter_ to 0
|
||
|
imputer = IterativeImputer(max_iter=5).fit(X)
|
||
|
# transformed should not be equal to initial imputation
|
||
|
assert not np.all(imputer.transform(X) == imputer.initial_imputer_.transform(X))
|
||
|
|
||
|
imputer.n_iter_ = 0
|
||
|
# now they should be equal as only initial imputation is done
|
||
|
assert_allclose(imputer.transform(X), imputer.initial_imputer_.transform(X))
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_verbose():
|
||
|
rng = np.random.RandomState(0)
|
||
|
|
||
|
n = 100
|
||
|
d = 3
|
||
|
X = _sparse_random_matrix(n, d, density=0.10, random_state=rng).toarray()
|
||
|
imputer = IterativeImputer(missing_values=0, max_iter=1, verbose=1)
|
||
|
imputer.fit(X)
|
||
|
imputer.transform(X)
|
||
|
imputer = IterativeImputer(missing_values=0, max_iter=1, verbose=2)
|
||
|
imputer.fit(X)
|
||
|
imputer.transform(X)
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_all_missing():
|
||
|
n = 100
|
||
|
d = 3
|
||
|
X = np.zeros((n, d))
|
||
|
imputer = IterativeImputer(missing_values=0, max_iter=1)
|
||
|
X_imputed = imputer.fit_transform(X)
|
||
|
assert_allclose(X_imputed, imputer.initial_imputer_.transform(X))
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"imputation_order", ["random", "roman", "ascending", "descending", "arabic"]
|
||
|
)
|
||
|
def test_iterative_imputer_imputation_order(imputation_order):
|
||
|
rng = np.random.RandomState(0)
|
||
|
n = 100
|
||
|
d = 10
|
||
|
max_iter = 2
|
||
|
X = _sparse_random_matrix(n, d, density=0.10, random_state=rng).toarray()
|
||
|
X[:, 0] = 1 # this column should not be discarded by IterativeImputer
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
missing_values=0,
|
||
|
max_iter=max_iter,
|
||
|
n_nearest_features=5,
|
||
|
sample_posterior=False,
|
||
|
skip_complete=True,
|
||
|
min_value=0,
|
||
|
max_value=1,
|
||
|
verbose=1,
|
||
|
imputation_order=imputation_order,
|
||
|
random_state=rng,
|
||
|
)
|
||
|
imputer.fit_transform(X)
|
||
|
ordered_idx = [i.feat_idx for i in imputer.imputation_sequence_]
|
||
|
|
||
|
assert len(ordered_idx) // imputer.n_iter_ == imputer.n_features_with_missing_
|
||
|
|
||
|
if imputation_order == "roman":
|
||
|
assert np.all(ordered_idx[: d - 1] == np.arange(1, d))
|
||
|
elif imputation_order == "arabic":
|
||
|
assert np.all(ordered_idx[: d - 1] == np.arange(d - 1, 0, -1))
|
||
|
elif imputation_order == "random":
|
||
|
ordered_idx_round_1 = ordered_idx[: d - 1]
|
||
|
ordered_idx_round_2 = ordered_idx[d - 1 :]
|
||
|
assert ordered_idx_round_1 != ordered_idx_round_2
|
||
|
elif "ending" in imputation_order:
|
||
|
assert len(ordered_idx) == max_iter * (d - 1)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"estimator", [None, DummyRegressor(), BayesianRidge(), ARDRegression(), RidgeCV()]
|
||
|
)
|
||
|
def test_iterative_imputer_estimators(estimator):
|
||
|
rng = np.random.RandomState(0)
|
||
|
|
||
|
n = 100
|
||
|
d = 10
|
||
|
X = _sparse_random_matrix(n, d, density=0.10, random_state=rng).toarray()
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
missing_values=0, max_iter=1, estimator=estimator, random_state=rng
|
||
|
)
|
||
|
imputer.fit_transform(X)
|
||
|
|
||
|
# check that types are correct for estimators
|
||
|
hashes = []
|
||
|
for triplet in imputer.imputation_sequence_:
|
||
|
expected_type = (
|
||
|
type(estimator) if estimator is not None else type(BayesianRidge())
|
||
|
)
|
||
|
assert isinstance(triplet.estimator, expected_type)
|
||
|
hashes.append(id(triplet.estimator))
|
||
|
|
||
|
# check that each estimator is unique
|
||
|
assert len(set(hashes)) == len(hashes)
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_clip():
|
||
|
rng = np.random.RandomState(0)
|
||
|
n = 100
|
||
|
d = 10
|
||
|
X = _sparse_random_matrix(n, d, density=0.10, random_state=rng).toarray()
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
missing_values=0, max_iter=1, min_value=0.1, max_value=0.2, random_state=rng
|
||
|
)
|
||
|
|
||
|
Xt = imputer.fit_transform(X)
|
||
|
assert_allclose(np.min(Xt[X == 0]), 0.1)
|
||
|
assert_allclose(np.max(Xt[X == 0]), 0.2)
|
||
|
assert_allclose(Xt[X != 0], X[X != 0])
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_clip_truncnorm():
|
||
|
rng = np.random.RandomState(0)
|
||
|
n = 100
|
||
|
d = 10
|
||
|
X = _sparse_random_matrix(n, d, density=0.10, random_state=rng).toarray()
|
||
|
X[:, 0] = 1
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
missing_values=0,
|
||
|
max_iter=2,
|
||
|
n_nearest_features=5,
|
||
|
sample_posterior=True,
|
||
|
min_value=0.1,
|
||
|
max_value=0.2,
|
||
|
verbose=1,
|
||
|
imputation_order="random",
|
||
|
random_state=rng,
|
||
|
)
|
||
|
Xt = imputer.fit_transform(X)
|
||
|
assert_allclose(np.min(Xt[X == 0]), 0.1)
|
||
|
assert_allclose(np.max(Xt[X == 0]), 0.2)
|
||
|
assert_allclose(Xt[X != 0], X[X != 0])
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_truncated_normal_posterior():
|
||
|
# test that the values that are imputed using `sample_posterior=True`
|
||
|
# with boundaries (`min_value` and `max_value` are not None) are drawn
|
||
|
# from a distribution that looks gaussian via the Kolmogorov Smirnov test.
|
||
|
# note that starting from the wrong random seed will make this test fail
|
||
|
# because random sampling doesn't occur at all when the imputation
|
||
|
# is outside of the (min_value, max_value) range
|
||
|
rng = np.random.RandomState(42)
|
||
|
|
||
|
X = rng.normal(size=(5, 5))
|
||
|
X[0][0] = np.nan
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
min_value=0, max_value=0.5, sample_posterior=True, random_state=rng
|
||
|
)
|
||
|
|
||
|
imputer.fit_transform(X)
|
||
|
# generate multiple imputations for the single missing value
|
||
|
imputations = np.array([imputer.transform(X)[0][0] for _ in range(100)])
|
||
|
|
||
|
assert all(imputations >= 0)
|
||
|
assert all(imputations <= 0.5)
|
||
|
|
||
|
mu, sigma = imputations.mean(), imputations.std()
|
||
|
ks_statistic, p_value = kstest((imputations - mu) / sigma, "norm")
|
||
|
if sigma == 0:
|
||
|
sigma += 1e-12
|
||
|
ks_statistic, p_value = kstest((imputations - mu) / sigma, "norm")
|
||
|
# we want to fail to reject null hypothesis
|
||
|
# null hypothesis: distributions are the same
|
||
|
assert ks_statistic < 0.2 or p_value > 0.1, "The posterior does appear to be normal"
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("strategy", ["mean", "median", "most_frequent"])
|
||
|
def test_iterative_imputer_missing_at_transform(strategy):
|
||
|
rng = np.random.RandomState(0)
|
||
|
n = 100
|
||
|
d = 10
|
||
|
X_train = rng.randint(low=0, high=3, size=(n, d))
|
||
|
X_test = rng.randint(low=0, high=3, size=(n, d))
|
||
|
|
||
|
X_train[:, 0] = 1 # definitely no missing values in 0th column
|
||
|
X_test[0, 0] = 0 # definitely missing value in 0th column
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
missing_values=0, max_iter=1, initial_strategy=strategy, random_state=rng
|
||
|
).fit(X_train)
|
||
|
initial_imputer = SimpleImputer(missing_values=0, strategy=strategy).fit(X_train)
|
||
|
|
||
|
# if there were no missing values at time of fit, then imputer will
|
||
|
# only use the initial imputer for that feature at transform
|
||
|
assert_allclose(
|
||
|
imputer.transform(X_test)[:, 0], initial_imputer.transform(X_test)[:, 0]
|
||
|
)
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_transform_stochasticity():
|
||
|
rng1 = np.random.RandomState(0)
|
||
|
rng2 = np.random.RandomState(1)
|
||
|
n = 100
|
||
|
d = 10
|
||
|
X = _sparse_random_matrix(n, d, density=0.10, random_state=rng1).toarray()
|
||
|
|
||
|
# when sample_posterior=True, two transforms shouldn't be equal
|
||
|
imputer = IterativeImputer(
|
||
|
missing_values=0, max_iter=1, sample_posterior=True, random_state=rng1
|
||
|
)
|
||
|
imputer.fit(X)
|
||
|
|
||
|
X_fitted_1 = imputer.transform(X)
|
||
|
X_fitted_2 = imputer.transform(X)
|
||
|
|
||
|
# sufficient to assert that the means are not the same
|
||
|
assert np.mean(X_fitted_1) != pytest.approx(np.mean(X_fitted_2))
|
||
|
|
||
|
# when sample_posterior=False, and n_nearest_features=None
|
||
|
# and imputation_order is not random
|
||
|
# the two transforms should be identical even if rng are different
|
||
|
imputer1 = IterativeImputer(
|
||
|
missing_values=0,
|
||
|
max_iter=1,
|
||
|
sample_posterior=False,
|
||
|
n_nearest_features=None,
|
||
|
imputation_order="ascending",
|
||
|
random_state=rng1,
|
||
|
)
|
||
|
|
||
|
imputer2 = IterativeImputer(
|
||
|
missing_values=0,
|
||
|
max_iter=1,
|
||
|
sample_posterior=False,
|
||
|
n_nearest_features=None,
|
||
|
imputation_order="ascending",
|
||
|
random_state=rng2,
|
||
|
)
|
||
|
imputer1.fit(X)
|
||
|
imputer2.fit(X)
|
||
|
|
||
|
X_fitted_1a = imputer1.transform(X)
|
||
|
X_fitted_1b = imputer1.transform(X)
|
||
|
X_fitted_2 = imputer2.transform(X)
|
||
|
|
||
|
assert_allclose(X_fitted_1a, X_fitted_1b)
|
||
|
assert_allclose(X_fitted_1a, X_fitted_2)
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_no_missing():
|
||
|
rng = np.random.RandomState(0)
|
||
|
X = rng.rand(100, 100)
|
||
|
X[:, 0] = np.nan
|
||
|
m1 = IterativeImputer(max_iter=10, random_state=rng)
|
||
|
m2 = IterativeImputer(max_iter=10, random_state=rng)
|
||
|
pred1 = m1.fit(X).transform(X)
|
||
|
pred2 = m2.fit_transform(X)
|
||
|
# should exclude the first column entirely
|
||
|
assert_allclose(X[:, 1:], pred1)
|
||
|
# fit and fit_transform should both be identical
|
||
|
assert_allclose(pred1, pred2)
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_rank_one():
|
||
|
rng = np.random.RandomState(0)
|
||
|
d = 50
|
||
|
A = rng.rand(d, 1)
|
||
|
B = rng.rand(1, d)
|
||
|
X = np.dot(A, B)
|
||
|
nan_mask = rng.rand(d, d) < 0.5
|
||
|
X_missing = X.copy()
|
||
|
X_missing[nan_mask] = np.nan
|
||
|
|
||
|
imputer = IterativeImputer(max_iter=5, verbose=1, random_state=rng)
|
||
|
X_filled = imputer.fit_transform(X_missing)
|
||
|
assert_allclose(X_filled, X, atol=0.02)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("rank", [3, 5])
|
||
|
def test_iterative_imputer_transform_recovery(rank):
|
||
|
rng = np.random.RandomState(0)
|
||
|
n = 70
|
||
|
d = 70
|
||
|
A = rng.rand(n, rank)
|
||
|
B = rng.rand(rank, d)
|
||
|
X_filled = np.dot(A, B)
|
||
|
nan_mask = rng.rand(n, d) < 0.5
|
||
|
X_missing = X_filled.copy()
|
||
|
X_missing[nan_mask] = np.nan
|
||
|
|
||
|
# split up data in half
|
||
|
n = n // 2
|
||
|
X_train = X_missing[:n]
|
||
|
X_test_filled = X_filled[n:]
|
||
|
X_test = X_missing[n:]
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
max_iter=5, imputation_order="descending", verbose=1, random_state=rng
|
||
|
).fit(X_train)
|
||
|
X_test_est = imputer.transform(X_test)
|
||
|
assert_allclose(X_test_filled, X_test_est, atol=0.1)
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_additive_matrix():
|
||
|
rng = np.random.RandomState(0)
|
||
|
n = 100
|
||
|
d = 10
|
||
|
A = rng.randn(n, d)
|
||
|
B = rng.randn(n, d)
|
||
|
X_filled = np.zeros(A.shape)
|
||
|
for i in range(d):
|
||
|
for j in range(d):
|
||
|
X_filled[:, (i + j) % d] += (A[:, i] + B[:, j]) / 2
|
||
|
# a quarter is randomly missing
|
||
|
nan_mask = rng.rand(n, d) < 0.25
|
||
|
X_missing = X_filled.copy()
|
||
|
X_missing[nan_mask] = np.nan
|
||
|
|
||
|
# split up data
|
||
|
n = n // 2
|
||
|
X_train = X_missing[:n]
|
||
|
X_test_filled = X_filled[n:]
|
||
|
X_test = X_missing[n:]
|
||
|
|
||
|
imputer = IterativeImputer(max_iter=10, verbose=1, random_state=rng).fit(X_train)
|
||
|
X_test_est = imputer.transform(X_test)
|
||
|
assert_allclose(X_test_filled, X_test_est, rtol=1e-3, atol=0.01)
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_early_stopping():
|
||
|
rng = np.random.RandomState(0)
|
||
|
n = 50
|
||
|
d = 5
|
||
|
A = rng.rand(n, 1)
|
||
|
B = rng.rand(1, d)
|
||
|
X = np.dot(A, B)
|
||
|
nan_mask = rng.rand(n, d) < 0.5
|
||
|
X_missing = X.copy()
|
||
|
X_missing[nan_mask] = np.nan
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
max_iter=100, tol=1e-2, sample_posterior=False, verbose=1, random_state=rng
|
||
|
)
|
||
|
X_filled_100 = imputer.fit_transform(X_missing)
|
||
|
assert len(imputer.imputation_sequence_) == d * imputer.n_iter_
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
max_iter=imputer.n_iter_, sample_posterior=False, verbose=1, random_state=rng
|
||
|
)
|
||
|
X_filled_early = imputer.fit_transform(X_missing)
|
||
|
assert_allclose(X_filled_100, X_filled_early, atol=1e-7)
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
max_iter=100, tol=0, sample_posterior=False, verbose=1, random_state=rng
|
||
|
)
|
||
|
imputer.fit(X_missing)
|
||
|
assert imputer.n_iter_ == imputer.max_iter
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_catch_warning():
|
||
|
# check that we catch a RuntimeWarning due to a division by zero when a
|
||
|
# feature is constant in the dataset
|
||
|
X, y = load_diabetes(return_X_y=True)
|
||
|
n_samples, n_features = X.shape
|
||
|
|
||
|
# simulate that a feature only contain one category during fit
|
||
|
X[:, 3] = 1
|
||
|
|
||
|
# add some missing values
|
||
|
rng = np.random.RandomState(0)
|
||
|
missing_rate = 0.15
|
||
|
for feat in range(n_features):
|
||
|
sample_idx = rng.choice(
|
||
|
np.arange(n_samples), size=int(n_samples * missing_rate), replace=False
|
||
|
)
|
||
|
X[sample_idx, feat] = np.nan
|
||
|
|
||
|
imputer = IterativeImputer(n_nearest_features=5, sample_posterior=True)
|
||
|
with warnings.catch_warnings():
|
||
|
warnings.simplefilter("error", RuntimeWarning)
|
||
|
X_fill = imputer.fit_transform(X, y)
|
||
|
assert not np.any(np.isnan(X_fill))
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"min_value, max_value, correct_output",
|
||
|
[
|
||
|
(0, 100, np.array([[0] * 3, [100] * 3])),
|
||
|
(None, None, np.array([[-np.inf] * 3, [np.inf] * 3])),
|
||
|
(-np.inf, np.inf, np.array([[-np.inf] * 3, [np.inf] * 3])),
|
||
|
([-5, 5, 10], [100, 200, 300], np.array([[-5, 5, 10], [100, 200, 300]])),
|
||
|
(
|
||
|
[-5, -np.inf, 10],
|
||
|
[100, 200, np.inf],
|
||
|
np.array([[-5, -np.inf, 10], [100, 200, np.inf]]),
|
||
|
),
|
||
|
],
|
||
|
ids=["scalars", "None-default", "inf", "lists", "lists-with-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.RandomState(0).randn(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_allclose(correct_output[0, :], imputer._min_value)
|
||
|
assert_allclose(correct_output[1, :], imputer._max_value)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"min_value, max_value, err_msg",
|
||
|
[
|
||
|
(100, 0, "min_value >= max_value."),
|
||
|
(np.inf, -np.inf, "min_value >= max_value."),
|
||
|
([-5, 5], [100, 200, 0], "_value' should be of shape"),
|
||
|
],
|
||
|
)
|
||
|
def test_iterative_imputer_catch_min_max_error(min_value, max_value, err_msg):
|
||
|
# 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)
|
||
|
with pytest.raises(ValueError, match=err_msg):
|
||
|
imputer.fit(X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"min_max_1, min_max_2",
|
||
|
[([None, None], [-np.inf, np.inf]), ([-10, 10], [[-10] * 4, [10] * 4])],
|
||
|
ids=["None-vs-inf", "Scalar-vs-vector"],
|
||
|
)
|
||
|
def test_iterative_imputer_min_max_array_like_imputation(min_max_1, min_max_2):
|
||
|
# Test that None/inf and scalar/vector give the same imputation
|
||
|
X_train = np.array(
|
||
|
[
|
||
|
[np.nan, 2, 2, 1],
|
||
|
[10, np.nan, np.nan, 7],
|
||
|
[3, 1, np.nan, 1],
|
||
|
[np.nan, 4, 2, np.nan],
|
||
|
]
|
||
|
)
|
||
|
X_test = np.array(
|
||
|
[[np.nan, 2, np.nan, 5], [2, 4, np.nan, np.nan], [np.nan, 1, 10, 1]]
|
||
|
)
|
||
|
imputer1 = IterativeImputer(
|
||
|
min_value=min_max_1[0], max_value=min_max_1[1], random_state=0
|
||
|
)
|
||
|
imputer2 = IterativeImputer(
|
||
|
min_value=min_max_2[0], max_value=min_max_2[1], random_state=0
|
||
|
)
|
||
|
X_test_imputed1 = imputer1.fit(X_train).transform(X_test)
|
||
|
X_test_imputed2 = imputer2.fit(X_train).transform(X_test)
|
||
|
assert_allclose(X_test_imputed1[:, 0], X_test_imputed2[:, 0])
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("skip_complete", [True, False])
|
||
|
def test_iterative_imputer_skip_non_missing(skip_complete):
|
||
|
# check the imputing strategy when missing data are present in the
|
||
|
# testing set only.
|
||
|
# taken from: https://github.com/scikit-learn/scikit-learn/issues/14383
|
||
|
rng = np.random.RandomState(0)
|
||
|
X_train = np.array([[5, 2, 2, 1], [10, 1, 2, 7], [3, 1, 1, 1], [8, 4, 2, 2]])
|
||
|
X_test = np.array([[np.nan, 2, 4, 5], [np.nan, 4, 1, 2], [np.nan, 1, 10, 1]])
|
||
|
imputer = IterativeImputer(
|
||
|
initial_strategy="mean", skip_complete=skip_complete, random_state=rng
|
||
|
)
|
||
|
X_test_est = imputer.fit(X_train).transform(X_test)
|
||
|
if skip_complete:
|
||
|
# impute with the initial strategy: 'mean'
|
||
|
assert_allclose(X_test_est[:, 0], np.mean(X_train[:, 0]))
|
||
|
else:
|
||
|
assert_allclose(X_test_est[:, 0], [11, 7, 12], rtol=1e-4)
|
||
|
|
||
|
|
||
|
@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):
|
||
|
class ZeroEstimator:
|
||
|
def __init__(self, random_state):
|
||
|
self.random_state = random_state
|
||
|
|
||
|
def fit(self, *args, **kgards):
|
||
|
return self
|
||
|
|
||
|
def predict(self, X):
|
||
|
return np.zeros(X.shape[0])
|
||
|
|
||
|
estimator = ZeroEstimator(random_state=rs_estimator)
|
||
|
imputer = IterativeImputer(random_state=rs_imputer)
|
||
|
X_train = np.zeros((10, 3))
|
||
|
imputer.fit(X_train)
|
||
|
assert estimator.random_state == rs_estimator
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"X_fit, X_trans, params, msg_err",
|
||
|
[
|
||
|
(
|
||
|
np.array([[-1, 1], [1, 2]]),
|
||
|
np.array([[-1, 1], [1, -1]]),
|
||
|
{"features": "missing-only", "sparse": "auto"},
|
||
|
"have missing values in transform but have no missing values in fit",
|
||
|
),
|
||
|
(
|
||
|
np.array([["a", "b"], ["c", "a"]], dtype=str),
|
||
|
np.array([["a", "b"], ["c", "a"]], dtype=str),
|
||
|
{},
|
||
|
"MissingIndicator does not support data with dtype",
|
||
|
),
|
||
|
],
|
||
|
)
|
||
|
def test_missing_indicator_error(X_fit, X_trans, params, msg_err):
|
||
|
indicator = MissingIndicator(missing_values=-1)
|
||
|
indicator.set_params(**params)
|
||
|
with pytest.raises(ValueError, match=msg_err):
|
||
|
indicator.fit(X_fit).transform(X_trans)
|
||
|
|
||
|
|
||
|
def _generate_missing_indicator_cases():
|
||
|
missing_values_dtypes = [(0, np.int32), (np.nan, np.float64), (-1, np.int32)]
|
||
|
arr_types = (
|
||
|
[np.array]
|
||
|
+ CSC_CONTAINERS
|
||
|
+ CSR_CONTAINERS
|
||
|
+ COO_CONTAINERS
|
||
|
+ LIL_CONTAINERS
|
||
|
+ BSR_CONTAINERS
|
||
|
)
|
||
|
return [
|
||
|
(arr_type, missing_values, dtype)
|
||
|
for arr_type, (missing_values, dtype) in product(
|
||
|
arr_types, missing_values_dtypes
|
||
|
)
|
||
|
if not (missing_values == 0 and arr_type is not np.array)
|
||
|
]
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"arr_type, missing_values, dtype", _generate_missing_indicator_cases()
|
||
|
)
|
||
|
@pytest.mark.parametrize(
|
||
|
"param_features, n_features, features_indices",
|
||
|
[("missing-only", 3, np.array([0, 1, 2])), ("all", 3, np.array([0, 1, 2]))],
|
||
|
)
|
||
|
def test_missing_indicator_new(
|
||
|
missing_values, arr_type, dtype, param_features, n_features, features_indices
|
||
|
):
|
||
|
X_fit = np.array([[missing_values, missing_values, 1], [4, 2, missing_values]])
|
||
|
X_trans = np.array([[missing_values, missing_values, 1], [4, 12, 10]])
|
||
|
X_fit_expected = np.array([[1, 1, 0], [0, 0, 1]])
|
||
|
X_trans_expected = np.array([[1, 1, 0], [0, 0, 0]])
|
||
|
|
||
|
# convert the input to the right array format and right dtype
|
||
|
X_fit = arr_type(X_fit).astype(dtype)
|
||
|
X_trans = arr_type(X_trans).astype(dtype)
|
||
|
X_fit_expected = X_fit_expected.astype(dtype)
|
||
|
X_trans_expected = X_trans_expected.astype(dtype)
|
||
|
|
||
|
indicator = MissingIndicator(
|
||
|
missing_values=missing_values, features=param_features, sparse=False
|
||
|
)
|
||
|
X_fit_mask = indicator.fit_transform(X_fit)
|
||
|
X_trans_mask = indicator.transform(X_trans)
|
||
|
|
||
|
assert X_fit_mask.shape[1] == n_features
|
||
|
assert X_trans_mask.shape[1] == n_features
|
||
|
|
||
|
assert_array_equal(indicator.features_, features_indices)
|
||
|
assert_allclose(X_fit_mask, X_fit_expected[:, features_indices])
|
||
|
assert_allclose(X_trans_mask, X_trans_expected[:, features_indices])
|
||
|
|
||
|
assert X_fit_mask.dtype == bool
|
||
|
assert X_trans_mask.dtype == bool
|
||
|
assert isinstance(X_fit_mask, np.ndarray)
|
||
|
assert isinstance(X_trans_mask, np.ndarray)
|
||
|
|
||
|
indicator.set_params(sparse=True)
|
||
|
X_fit_mask_sparse = indicator.fit_transform(X_fit)
|
||
|
X_trans_mask_sparse = indicator.transform(X_trans)
|
||
|
|
||
|
assert X_fit_mask_sparse.dtype == bool
|
||
|
assert X_trans_mask_sparse.dtype == bool
|
||
|
assert X_fit_mask_sparse.format == "csc"
|
||
|
assert X_trans_mask_sparse.format == "csc"
|
||
|
assert_allclose(X_fit_mask_sparse.toarray(), X_fit_mask)
|
||
|
assert_allclose(X_trans_mask_sparse.toarray(), X_trans_mask)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"arr_type",
|
||
|
CSC_CONTAINERS + CSR_CONTAINERS + COO_CONTAINERS + LIL_CONTAINERS + BSR_CONTAINERS,
|
||
|
)
|
||
|
def test_missing_indicator_raise_on_sparse_with_missing_0(arr_type):
|
||
|
# test for sparse input and missing_value == 0
|
||
|
|
||
|
missing_values = 0
|
||
|
X_fit = np.array([[missing_values, missing_values, 1], [4, missing_values, 2]])
|
||
|
X_trans = np.array([[missing_values, missing_values, 1], [4, 12, 10]])
|
||
|
|
||
|
# convert the input to the right array format
|
||
|
X_fit_sparse = arr_type(X_fit)
|
||
|
X_trans_sparse = arr_type(X_trans)
|
||
|
|
||
|
indicator = MissingIndicator(missing_values=missing_values)
|
||
|
|
||
|
with pytest.raises(ValueError, match="Sparse input with missing_values=0"):
|
||
|
indicator.fit_transform(X_fit_sparse)
|
||
|
|
||
|
indicator.fit_transform(X_fit)
|
||
|
with pytest.raises(ValueError, match="Sparse input with missing_values=0"):
|
||
|
indicator.transform(X_trans_sparse)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("param_sparse", [True, False, "auto"])
|
||
|
@pytest.mark.parametrize(
|
||
|
"arr_type, missing_values",
|
||
|
[(np.array, 0)]
|
||
|
+ list(
|
||
|
product(
|
||
|
CSC_CONTAINERS
|
||
|
+ CSR_CONTAINERS
|
||
|
+ COO_CONTAINERS
|
||
|
+ LIL_CONTAINERS
|
||
|
+ BSR_CONTAINERS,
|
||
|
[np.nan],
|
||
|
)
|
||
|
),
|
||
|
)
|
||
|
def test_missing_indicator_sparse_param(arr_type, missing_values, param_sparse):
|
||
|
# check the format of the output with different sparse parameter
|
||
|
X_fit = np.array([[missing_values, missing_values, 1], [4, missing_values, 2]])
|
||
|
X_trans = np.array([[missing_values, missing_values, 1], [4, 12, 10]])
|
||
|
X_fit = arr_type(X_fit).astype(np.float64)
|
||
|
X_trans = arr_type(X_trans).astype(np.float64)
|
||
|
|
||
|
indicator = MissingIndicator(missing_values=missing_values, sparse=param_sparse)
|
||
|
X_fit_mask = indicator.fit_transform(X_fit)
|
||
|
X_trans_mask = indicator.transform(X_trans)
|
||
|
|
||
|
if param_sparse is True:
|
||
|
assert X_fit_mask.format == "csc"
|
||
|
assert X_trans_mask.format == "csc"
|
||
|
elif param_sparse == "auto" and missing_values == 0:
|
||
|
assert isinstance(X_fit_mask, np.ndarray)
|
||
|
assert isinstance(X_trans_mask, np.ndarray)
|
||
|
elif param_sparse is False:
|
||
|
assert isinstance(X_fit_mask, np.ndarray)
|
||
|
assert isinstance(X_trans_mask, np.ndarray)
|
||
|
else:
|
||
|
if sparse.issparse(X_fit):
|
||
|
assert X_fit_mask.format == "csc"
|
||
|
assert X_trans_mask.format == "csc"
|
||
|
else:
|
||
|
assert isinstance(X_fit_mask, np.ndarray)
|
||
|
assert isinstance(X_trans_mask, np.ndarray)
|
||
|
|
||
|
|
||
|
def test_missing_indicator_string():
|
||
|
X = np.array([["a", "b", "c"], ["b", "c", "a"]], dtype=object)
|
||
|
indicator = MissingIndicator(missing_values="a", features="all")
|
||
|
X_trans = indicator.fit_transform(X)
|
||
|
assert_array_equal(X_trans, np.array([[True, False, False], [False, False, True]]))
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"X, missing_values, X_trans_exp",
|
||
|
[
|
||
|
(
|
||
|
np.array([["a", "b"], ["b", "a"]], dtype=object),
|
||
|
"a",
|
||
|
np.array([["b", "b", True, False], ["b", "b", False, True]], dtype=object),
|
||
|
),
|
||
|
(
|
||
|
np.array([[np.nan, 1.0], [1.0, np.nan]]),
|
||
|
np.nan,
|
||
|
np.array([[1.0, 1.0, True, False], [1.0, 1.0, False, True]]),
|
||
|
),
|
||
|
(
|
||
|
np.array([[np.nan, "b"], ["b", np.nan]], dtype=object),
|
||
|
np.nan,
|
||
|
np.array([["b", "b", True, False], ["b", "b", False, True]], dtype=object),
|
||
|
),
|
||
|
(
|
||
|
np.array([[None, "b"], ["b", None]], dtype=object),
|
||
|
None,
|
||
|
np.array([["b", "b", True, False], ["b", "b", False, True]], dtype=object),
|
||
|
),
|
||
|
],
|
||
|
)
|
||
|
def test_missing_indicator_with_imputer(X, missing_values, X_trans_exp):
|
||
|
trans = make_union(
|
||
|
SimpleImputer(missing_values=missing_values, strategy="most_frequent"),
|
||
|
MissingIndicator(missing_values=missing_values),
|
||
|
)
|
||
|
X_trans = trans.fit_transform(X)
|
||
|
assert_array_equal(X_trans, X_trans_exp)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("imputer_constructor", [SimpleImputer, IterativeImputer])
|
||
|
@pytest.mark.parametrize(
|
||
|
"imputer_missing_values, missing_value, err_msg",
|
||
|
[
|
||
|
("NaN", np.nan, "Input X contains NaN"),
|
||
|
("-1", -1, "types are expected to be both numerical."),
|
||
|
],
|
||
|
)
|
||
|
def test_inconsistent_dtype_X_missing_values(
|
||
|
imputer_constructor, imputer_missing_values, missing_value, err_msg
|
||
|
):
|
||
|
# regression test for issue #11390. Comparison between incoherent dtype
|
||
|
# for X and missing_values was not raising a proper error.
|
||
|
rng = np.random.RandomState(42)
|
||
|
X = rng.randn(10, 10)
|
||
|
X[0, 0] = missing_value
|
||
|
|
||
|
imputer = imputer_constructor(missing_values=imputer_missing_values)
|
||
|
|
||
|
with pytest.raises(ValueError, match=err_msg):
|
||
|
imputer.fit_transform(X)
|
||
|
|
||
|
|
||
|
def test_missing_indicator_no_missing():
|
||
|
# check that all features are dropped if there are no missing values when
|
||
|
# features='missing-only' (#13491)
|
||
|
X = np.array([[1, 1], [1, 1]])
|
||
|
|
||
|
mi = MissingIndicator(features="missing-only", missing_values=-1)
|
||
|
Xt = mi.fit_transform(X)
|
||
|
|
||
|
assert Xt.shape[1] == 0
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("csr_container", CSR_CONTAINERS)
|
||
|
def test_missing_indicator_sparse_no_explicit_zeros(csr_container):
|
||
|
# Check that non missing values don't become explicit zeros in the mask
|
||
|
# generated by missing indicator when X is sparse. (#13491)
|
||
|
X = csr_container([[0, 1, 2], [1, 2, 0], [2, 0, 1]])
|
||
|
|
||
|
mi = MissingIndicator(features="all", missing_values=1)
|
||
|
Xt = mi.fit_transform(X)
|
||
|
|
||
|
assert Xt.getnnz() == Xt.sum()
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("imputer_constructor", [SimpleImputer, IterativeImputer])
|
||
|
def test_imputer_without_indicator(imputer_constructor):
|
||
|
X = np.array([[1, 1], [1, 1]])
|
||
|
imputer = imputer_constructor()
|
||
|
imputer.fit(X)
|
||
|
|
||
|
assert imputer.indicator_ is None
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"arr_type",
|
||
|
CSC_CONTAINERS + CSR_CONTAINERS + COO_CONTAINERS + LIL_CONTAINERS + BSR_CONTAINERS,
|
||
|
)
|
||
|
def test_simple_imputation_add_indicator_sparse_matrix(arr_type):
|
||
|
X_sparse = arr_type([[np.nan, 1, 5], [2, np.nan, 1], [6, 3, np.nan], [1, 2, 9]])
|
||
|
X_true = np.array(
|
||
|
[
|
||
|
[3.0, 1.0, 5.0, 1.0, 0.0, 0.0],
|
||
|
[2.0, 2.0, 1.0, 0.0, 1.0, 0.0],
|
||
|
[6.0, 3.0, 5.0, 0.0, 0.0, 1.0],
|
||
|
[1.0, 2.0, 9.0, 0.0, 0.0, 0.0],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(missing_values=np.nan, add_indicator=True)
|
||
|
X_trans = imputer.fit_transform(X_sparse)
|
||
|
|
||
|
assert sparse.issparse(X_trans)
|
||
|
assert X_trans.shape == X_true.shape
|
||
|
assert_allclose(X_trans.toarray(), X_true)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"strategy, expected", [("most_frequent", "b"), ("constant", "missing_value")]
|
||
|
)
|
||
|
def test_simple_imputation_string_list(strategy, expected):
|
||
|
X = [["a", "b"], ["c", np.nan]]
|
||
|
|
||
|
X_true = np.array([["a", "b"], ["c", expected]], dtype=object)
|
||
|
|
||
|
imputer = SimpleImputer(strategy=strategy)
|
||
|
X_trans = imputer.fit_transform(X)
|
||
|
|
||
|
assert_array_equal(X_trans, X_true)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"order, idx_order",
|
||
|
[("ascending", [3, 4, 2, 0, 1]), ("descending", [1, 0, 2, 4, 3])],
|
||
|
)
|
||
|
def test_imputation_order(order, idx_order):
|
||
|
# regression test for #15393
|
||
|
rng = np.random.RandomState(42)
|
||
|
X = rng.rand(100, 5)
|
||
|
X[:50, 1] = np.nan
|
||
|
X[:30, 0] = np.nan
|
||
|
X[:20, 2] = np.nan
|
||
|
X[:10, 4] = np.nan
|
||
|
|
||
|
with pytest.warns(ConvergenceWarning):
|
||
|
trs = IterativeImputer(max_iter=1, imputation_order=order, random_state=0).fit(
|
||
|
X
|
||
|
)
|
||
|
idx = [x.feat_idx for x in trs.imputation_sequence_]
|
||
|
assert idx == idx_order
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("missing_value", [-1, np.nan])
|
||
|
def test_simple_imputation_inverse_transform(missing_value):
|
||
|
# Test inverse_transform feature for np.nan
|
||
|
X_1 = np.array(
|
||
|
[
|
||
|
[9, missing_value, 3, -1],
|
||
|
[4, -1, 5, 4],
|
||
|
[6, 7, missing_value, -1],
|
||
|
[8, 9, 0, missing_value],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
X_2 = np.array(
|
||
|
[
|
||
|
[5, 4, 2, 1],
|
||
|
[2, 1, missing_value, 3],
|
||
|
[9, missing_value, 7, 1],
|
||
|
[6, 4, 2, missing_value],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
X_3 = np.array(
|
||
|
[
|
||
|
[1, missing_value, 5, 9],
|
||
|
[missing_value, 4, missing_value, missing_value],
|
||
|
[2, missing_value, 7, missing_value],
|
||
|
[missing_value, 3, missing_value, 8],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
X_4 = np.array(
|
||
|
[
|
||
|
[1, 1, 1, 3],
|
||
|
[missing_value, 2, missing_value, 1],
|
||
|
[2, 3, 3, 4],
|
||
|
[missing_value, 4, missing_value, 2],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(
|
||
|
missing_values=missing_value, strategy="mean", add_indicator=True
|
||
|
)
|
||
|
|
||
|
X_1_trans = imputer.fit_transform(X_1)
|
||
|
X_1_inv_trans = imputer.inverse_transform(X_1_trans)
|
||
|
|
||
|
X_2_trans = imputer.transform(X_2) # test on new data
|
||
|
X_2_inv_trans = imputer.inverse_transform(X_2_trans)
|
||
|
|
||
|
assert_array_equal(X_1_inv_trans, X_1)
|
||
|
assert_array_equal(X_2_inv_trans, X_2)
|
||
|
|
||
|
for X in [X_3, X_4]:
|
||
|
X_trans = imputer.fit_transform(X)
|
||
|
X_inv_trans = imputer.inverse_transform(X_trans)
|
||
|
assert_array_equal(X_inv_trans, X)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("missing_value", [-1, np.nan])
|
||
|
def test_simple_imputation_inverse_transform_exceptions(missing_value):
|
||
|
X_1 = np.array(
|
||
|
[
|
||
|
[9, missing_value, 3, -1],
|
||
|
[4, -1, 5, 4],
|
||
|
[6, 7, missing_value, -1],
|
||
|
[8, 9, 0, missing_value],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(missing_values=missing_value, strategy="mean")
|
||
|
X_1_trans = imputer.fit_transform(X_1)
|
||
|
with pytest.raises(
|
||
|
ValueError, match=f"Got 'add_indicator={imputer.add_indicator}'"
|
||
|
):
|
||
|
imputer.inverse_transform(X_1_trans)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"expected,array,dtype,extra_value,n_repeat",
|
||
|
[
|
||
|
# array of object dtype
|
||
|
("extra_value", ["a", "b", "c"], object, "extra_value", 2),
|
||
|
(
|
||
|
"most_frequent_value",
|
||
|
["most_frequent_value", "most_frequent_value", "value"],
|
||
|
object,
|
||
|
"extra_value",
|
||
|
1,
|
||
|
),
|
||
|
("a", ["min_value", "min_valuevalue"], object, "a", 2),
|
||
|
("min_value", ["min_value", "min_value", "value"], object, "z", 2),
|
||
|
# array of numeric dtype
|
||
|
(10, [1, 2, 3], int, 10, 2),
|
||
|
(1, [1, 1, 2], int, 10, 1),
|
||
|
(10, [20, 20, 1], int, 10, 2),
|
||
|
(1, [1, 1, 20], int, 10, 2),
|
||
|
],
|
||
|
)
|
||
|
def test_most_frequent(expected, array, dtype, extra_value, n_repeat):
|
||
|
assert expected == _most_frequent(
|
||
|
np.array(array, dtype=dtype), extra_value, n_repeat
|
||
|
)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize(
|
||
|
"initial_strategy", ["mean", "median", "most_frequent", "constant"]
|
||
|
)
|
||
|
def test_iterative_imputer_keep_empty_features(initial_strategy):
|
||
|
"""Check the behaviour of the iterative imputer with different initial strategy
|
||
|
and keeping empty features (i.e. features containing only missing values).
|
||
|
"""
|
||
|
X = np.array([[1, np.nan, 2], [3, np.nan, np.nan]])
|
||
|
|
||
|
imputer = IterativeImputer(
|
||
|
initial_strategy=initial_strategy, keep_empty_features=True
|
||
|
)
|
||
|
X_imputed = imputer.fit_transform(X)
|
||
|
assert_allclose(X_imputed[:, 1], 0)
|
||
|
X_imputed = imputer.transform(X)
|
||
|
assert_allclose(X_imputed[:, 1], 0)
|
||
|
|
||
|
|
||
|
def test_iterative_imputer_constant_fill_value():
|
||
|
"""Check that we propagate properly the parameter `fill_value`."""
|
||
|
X = np.array([[-1, 2, 3, -1], [4, -1, 5, -1], [6, 7, -1, -1], [8, 9, 0, -1]])
|
||
|
|
||
|
fill_value = 100
|
||
|
imputer = IterativeImputer(
|
||
|
missing_values=-1,
|
||
|
initial_strategy="constant",
|
||
|
fill_value=fill_value,
|
||
|
max_iter=0,
|
||
|
)
|
||
|
imputer.fit_transform(X)
|
||
|
assert_array_equal(imputer.initial_imputer_.statistics_, fill_value)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("keep_empty_features", [True, False])
|
||
|
def test_knn_imputer_keep_empty_features(keep_empty_features):
|
||
|
"""Check the behaviour of `keep_empty_features` for `KNNImputer`."""
|
||
|
X = np.array([[1, np.nan, 2], [3, np.nan, np.nan]])
|
||
|
|
||
|
imputer = KNNImputer(keep_empty_features=keep_empty_features)
|
||
|
|
||
|
for method in ["fit_transform", "transform"]:
|
||
|
X_imputed = getattr(imputer, method)(X)
|
||
|
if keep_empty_features:
|
||
|
assert X_imputed.shape == X.shape
|
||
|
assert_array_equal(X_imputed[:, 1], 0)
|
||
|
else:
|
||
|
assert X_imputed.shape == (X.shape[0], X.shape[1] - 1)
|
||
|
|
||
|
|
||
|
def test_simple_impute_pd_na():
|
||
|
pd = pytest.importorskip("pandas")
|
||
|
|
||
|
# Impute pandas array of string types.
|
||
|
df = pd.DataFrame({"feature": pd.Series(["abc", None, "de"], dtype="string")})
|
||
|
imputer = SimpleImputer(missing_values=pd.NA, strategy="constant", fill_value="na")
|
||
|
_assert_array_equal_and_same_dtype(
|
||
|
imputer.fit_transform(df), np.array([["abc"], ["na"], ["de"]], dtype=object)
|
||
|
)
|
||
|
|
||
|
# Impute pandas array of string types without any missing values.
|
||
|
df = pd.DataFrame({"feature": pd.Series(["abc", "de", "fgh"], dtype="string")})
|
||
|
imputer = SimpleImputer(fill_value="ok", strategy="constant")
|
||
|
_assert_array_equal_and_same_dtype(
|
||
|
imputer.fit_transform(df), np.array([["abc"], ["de"], ["fgh"]], dtype=object)
|
||
|
)
|
||
|
|
||
|
# Impute pandas array of integer types.
|
||
|
df = pd.DataFrame({"feature": pd.Series([1, None, 3], dtype="Int64")})
|
||
|
imputer = SimpleImputer(missing_values=pd.NA, strategy="constant", fill_value=-1)
|
||
|
_assert_allclose_and_same_dtype(
|
||
|
imputer.fit_transform(df), np.array([[1], [-1], [3]], dtype="float64")
|
||
|
)
|
||
|
|
||
|
# Use `np.nan` also works.
|
||
|
imputer = SimpleImputer(missing_values=np.nan, strategy="constant", fill_value=-1)
|
||
|
_assert_allclose_and_same_dtype(
|
||
|
imputer.fit_transform(df), np.array([[1], [-1], [3]], dtype="float64")
|
||
|
)
|
||
|
|
||
|
# Impute pandas array of integer types with 'median' strategy.
|
||
|
df = pd.DataFrame({"feature": pd.Series([1, None, 2, 3], dtype="Int64")})
|
||
|
imputer = SimpleImputer(missing_values=pd.NA, strategy="median")
|
||
|
_assert_allclose_and_same_dtype(
|
||
|
imputer.fit_transform(df), np.array([[1], [2], [2], [3]], dtype="float64")
|
||
|
)
|
||
|
|
||
|
# Impute pandas array of integer types with 'mean' strategy.
|
||
|
df = pd.DataFrame({"feature": pd.Series([1, None, 2], dtype="Int64")})
|
||
|
imputer = SimpleImputer(missing_values=pd.NA, strategy="mean")
|
||
|
_assert_allclose_and_same_dtype(
|
||
|
imputer.fit_transform(df), np.array([[1], [1.5], [2]], dtype="float64")
|
||
|
)
|
||
|
|
||
|
# Impute pandas array of float types.
|
||
|
df = pd.DataFrame({"feature": pd.Series([1.0, None, 3.0], dtype="float64")})
|
||
|
imputer = SimpleImputer(missing_values=pd.NA, strategy="constant", fill_value=-2.0)
|
||
|
_assert_allclose_and_same_dtype(
|
||
|
imputer.fit_transform(df), np.array([[1.0], [-2.0], [3.0]], dtype="float64")
|
||
|
)
|
||
|
|
||
|
# Impute pandas array of float types with 'median' strategy.
|
||
|
df = pd.DataFrame({"feature": pd.Series([1.0, None, 2.0, 3.0], dtype="float64")})
|
||
|
imputer = SimpleImputer(missing_values=pd.NA, strategy="median")
|
||
|
_assert_allclose_and_same_dtype(
|
||
|
imputer.fit_transform(df),
|
||
|
np.array([[1.0], [2.0], [2.0], [3.0]], dtype="float64"),
|
||
|
)
|
||
|
|
||
|
|
||
|
def test_missing_indicator_feature_names_out():
|
||
|
"""Check that missing indicator return the feature names with a prefix."""
|
||
|
pd = pytest.importorskip("pandas")
|
||
|
|
||
|
missing_values = np.nan
|
||
|
X = pd.DataFrame(
|
||
|
[
|
||
|
[missing_values, missing_values, 1, missing_values],
|
||
|
[4, missing_values, 2, 10],
|
||
|
],
|
||
|
columns=["a", "b", "c", "d"],
|
||
|
)
|
||
|
|
||
|
indicator = MissingIndicator(missing_values=missing_values).fit(X)
|
||
|
feature_names = indicator.get_feature_names_out()
|
||
|
expected_names = ["missingindicator_a", "missingindicator_b", "missingindicator_d"]
|
||
|
assert_array_equal(expected_names, feature_names)
|
||
|
|
||
|
|
||
|
def test_imputer_lists_fit_transform():
|
||
|
"""Check transform uses object dtype when fitted on an object dtype.
|
||
|
|
||
|
Non-regression test for #19572.
|
||
|
"""
|
||
|
|
||
|
X = [["a", "b"], ["c", "b"], ["a", "a"]]
|
||
|
imp_frequent = SimpleImputer(strategy="most_frequent").fit(X)
|
||
|
X_trans = imp_frequent.transform([[np.nan, np.nan]])
|
||
|
assert X_trans.dtype == object
|
||
|
assert_array_equal(X_trans, [["a", "b"]])
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("dtype_test", [np.float32, np.float64])
|
||
|
def test_imputer_transform_preserves_numeric_dtype(dtype_test):
|
||
|
"""Check transform preserves numeric dtype independent of fit dtype."""
|
||
|
X = np.asarray(
|
||
|
[[1.2, 3.4, np.nan], [np.nan, 1.2, 1.3], [4.2, 2, 1]], dtype=np.float64
|
||
|
)
|
||
|
imp = SimpleImputer().fit(X)
|
||
|
|
||
|
X_test = np.asarray([[np.nan, np.nan, np.nan]], dtype=dtype_test)
|
||
|
X_trans = imp.transform(X_test)
|
||
|
assert X_trans.dtype == dtype_test
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_type", ["array", "sparse"])
|
||
|
@pytest.mark.parametrize("keep_empty_features", [True, False])
|
||
|
def test_simple_imputer_constant_keep_empty_features(array_type, keep_empty_features):
|
||
|
"""Check the behaviour of `keep_empty_features` with `strategy='constant'.
|
||
|
For backward compatibility, a column full of missing values will always be
|
||
|
fill and never dropped.
|
||
|
"""
|
||
|
X = np.array([[np.nan, 2], [np.nan, 3], [np.nan, 6]])
|
||
|
X = _convert_container(X, array_type)
|
||
|
fill_value = 10
|
||
|
imputer = SimpleImputer(
|
||
|
strategy="constant",
|
||
|
fill_value=fill_value,
|
||
|
keep_empty_features=keep_empty_features,
|
||
|
)
|
||
|
|
||
|
for method in ["fit_transform", "transform"]:
|
||
|
X_imputed = getattr(imputer, method)(X)
|
||
|
assert X_imputed.shape == X.shape
|
||
|
constant_feature = (
|
||
|
X_imputed[:, 0].toarray() if array_type == "sparse" else X_imputed[:, 0]
|
||
|
)
|
||
|
assert_array_equal(constant_feature, fill_value)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("array_type", ["array", "sparse"])
|
||
|
@pytest.mark.parametrize("strategy", ["mean", "median", "most_frequent"])
|
||
|
@pytest.mark.parametrize("keep_empty_features", [True, False])
|
||
|
def test_simple_imputer_keep_empty_features(strategy, array_type, keep_empty_features):
|
||
|
"""Check the behaviour of `keep_empty_features` with all strategies but
|
||
|
'constant'.
|
||
|
"""
|
||
|
X = np.array([[np.nan, 2], [np.nan, 3], [np.nan, 6]])
|
||
|
X = _convert_container(X, array_type)
|
||
|
imputer = SimpleImputer(strategy=strategy, keep_empty_features=keep_empty_features)
|
||
|
|
||
|
for method in ["fit_transform", "transform"]:
|
||
|
X_imputed = getattr(imputer, method)(X)
|
||
|
if keep_empty_features:
|
||
|
assert X_imputed.shape == X.shape
|
||
|
constant_feature = (
|
||
|
X_imputed[:, 0].toarray() if array_type == "sparse" else X_imputed[:, 0]
|
||
|
)
|
||
|
assert_array_equal(constant_feature, 0)
|
||
|
else:
|
||
|
assert X_imputed.shape == (X.shape[0], X.shape[1] - 1)
|
||
|
|
||
|
|
||
|
@pytest.mark.parametrize("csc_container", CSC_CONTAINERS)
|
||
|
def test_imputation_custom(csc_container):
|
||
|
X = np.array(
|
||
|
[
|
||
|
[1.1, 1.1, 1.1],
|
||
|
[3.9, 1.2, np.nan],
|
||
|
[np.nan, 1.3, np.nan],
|
||
|
[0.1, 1.4, 1.4],
|
||
|
[4.9, 1.5, 1.5],
|
||
|
[np.nan, 1.6, 1.6],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
X_true = np.array(
|
||
|
[
|
||
|
[1.1, 1.1, 1.1],
|
||
|
[3.9, 1.2, 1.1],
|
||
|
[0.1, 1.3, 1.1],
|
||
|
[0.1, 1.4, 1.4],
|
||
|
[4.9, 1.5, 1.5],
|
||
|
[0.1, 1.6, 1.6],
|
||
|
]
|
||
|
)
|
||
|
|
||
|
imputer = SimpleImputer(missing_values=np.nan, strategy=np.min)
|
||
|
X_trans = imputer.fit_transform(X)
|
||
|
assert_array_equal(X_trans, X_true)
|
||
|
|
||
|
# Sparse matrix
|
||
|
imputer = SimpleImputer(missing_values=np.nan, strategy=np.min)
|
||
|
X_trans = imputer.fit_transform(csc_container(X))
|
||
|
assert_array_equal(X_trans.toarray(), X_true)
|
||
|
|
||
|
|
||
|
def test_simple_imputer_constant_fill_value_casting():
|
||
|
"""Check that we raise a proper error message when we cannot cast the fill value
|
||
|
to the input data type. Otherwise, check that the casting is done properly.
|
||
|
|
||
|
Non-regression test for:
|
||
|
https://github.com/scikit-learn/scikit-learn/issues/28309
|
||
|
"""
|
||
|
# cannot cast fill_value at fit
|
||
|
fill_value = 1.5
|
||
|
X_int64 = np.array([[1, 2, 3], [2, 3, 4]], dtype=np.int64)
|
||
|
imputer = SimpleImputer(
|
||
|
strategy="constant", fill_value=fill_value, missing_values=2
|
||
|
)
|
||
|
err_msg = f"fill_value={fill_value!r} (of type {type(fill_value)!r}) cannot be cast"
|
||
|
with pytest.raises(ValueError, match=re.escape(err_msg)):
|
||
|
imputer.fit(X_int64)
|
||
|
|
||
|
# cannot cast fill_value at transform
|
||
|
X_float64 = np.array([[1, 2, 3], [2, 3, 4]], dtype=np.float64)
|
||
|
imputer.fit(X_float64)
|
||
|
err_msg = (
|
||
|
f"The dtype of the filling value (i.e. {imputer.statistics_.dtype!r}) "
|
||
|
"cannot be cast"
|
||
|
)
|
||
|
with pytest.raises(ValueError, match=re.escape(err_msg)):
|
||
|
imputer.transform(X_int64)
|
||
|
|
||
|
# check that no error is raised when having the same kind of dtype
|
||
|
fill_value_list = [np.float64(1.5), 1.5, 1]
|
||
|
X_float32 = X_float64.astype(np.float32)
|
||
|
|
||
|
for fill_value in fill_value_list:
|
||
|
imputer = SimpleImputer(
|
||
|
strategy="constant", fill_value=fill_value, missing_values=2
|
||
|
)
|
||
|
X_trans = imputer.fit_transform(X_float32)
|
||
|
assert X_trans.dtype == X_float32.dtype
|