2018-07-10 17:48:03 +08:00
|
|
|
|
import pytest
|
2015-06-02 04:08:31 +08:00
|
|
|
|
import numpy as np
|
2017-10-26 04:49:28 +08:00
|
|
|
|
from scipy import sparse
|
2015-06-02 04:08:31 +08:00
|
|
|
|
|
2015-08-04 04:08:45 +08:00
|
|
|
|
from sklearn.preprocessing import FunctionTransformer
|
2019-10-29 00:28:56 +08:00
|
|
|
|
from sklearn.utils._testing import assert_array_equal, assert_allclose_dense_sparse
|
2015-06-02 04:08:31 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_func(args_store, kwargs_store, func=lambda X, *a, **k: X):
|
|
|
|
|
|
def _func(X, *args, **kwargs):
|
|
|
|
|
|
args_store.append(X)
|
|
|
|
|
|
args_store.extend(args)
|
|
|
|
|
|
kwargs_store.update(kwargs)
|
|
|
|
|
|
return func(X)
|
|
|
|
|
|
|
|
|
|
|
|
return _func
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delegate_to_func():
|
|
|
|
|
|
# (args|kwargs)_store will hold the positional and keyword arguments
|
2015-07-31 00:48:04 +08:00
|
|
|
|
# passed to the function inside the FunctionTransformer.
|
2015-06-02 04:08:31 +08:00
|
|
|
|
args_store = []
|
|
|
|
|
|
kwargs_store = {}
|
|
|
|
|
|
X = np.arange(10).reshape((5, 2))
|
2016-10-08 00:46:52 +08:00
|
|
|
|
assert_array_equal(
|
2019-06-14 00:06:01 +08:00
|
|
|
|
FunctionTransformer(_make_func(args_store, kwargs_store)).transform(X),
|
2017-06-23 05:24:12 +08:00
|
|
|
|
X,
|
|
|
|
|
|
"transform should have returned X unchanged",
|
2015-06-02 04:08:31 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2015-12-08 02:13:40 +08:00
|
|
|
|
# The function should only have received X.
|
2019-07-02 12:53:51 +08:00
|
|
|
|
assert args_store == [
|
2021-06-18 02:21:09 +08:00
|
|
|
|
X
|
2019-07-02 12:53:51 +08:00
|
|
|
|
], "Incorrect positional arguments passed to func: {args}".format(args=args_store)
|
|
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
|
not kwargs_store
|
|
|
|
|
|
), "Unexpected keyword arguments passed to func: {args}".format(args=kwargs_store)
|
2015-06-02 04:08:31 +08:00
|
|
|
|
|
2015-07-31 00:48:04 +08:00
|
|
|
|
# reset the argument stores.
|
2019-02-02 22:01:13 +08:00
|
|
|
|
args_store[:] = []
|
2015-07-31 00:48:04 +08:00
|
|
|
|
kwargs_store.clear()
|
2018-10-12 02:56:37 +08:00
|
|
|
|
transformed = FunctionTransformer(
|
|
|
|
|
|
_make_func(args_store, kwargs_store),
|
2019-06-14 00:06:01 +08:00
|
|
|
|
).transform(X)
|
2017-06-23 05:24:12 +08:00
|
|
|
|
|
|
|
|
|
|
assert_array_equal(
|
|
|
|
|
|
transformed, X, err_msg="transform should have returned X unchanged"
|
|
|
|
|
|
)
|
2015-06-02 04:08:31 +08:00
|
|
|
|
|
2018-10-12 02:56:37 +08:00
|
|
|
|
# The function should have received X
|
2019-07-02 12:53:51 +08:00
|
|
|
|
assert args_store == [
|
2021-06-18 02:21:09 +08:00
|
|
|
|
X
|
2019-07-02 12:53:51 +08:00
|
|
|
|
], "Incorrect positional arguments passed to func: {args}".format(args=args_store)
|
|
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
|
not kwargs_store
|
|
|
|
|
|
), "Unexpected keyword arguments passed to func: {args}".format(args=kwargs_store)
|
2015-07-31 00:48:04 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_np_log():
|
|
|
|
|
|
X = np.arange(10).reshape((5, 2))
|
|
|
|
|
|
|
|
|
|
|
|
# Test that the numpy.log example still works.
|
2016-10-08 00:46:52 +08:00
|
|
|
|
assert_array_equal(
|
2015-08-04 04:08:45 +08:00
|
|
|
|
FunctionTransformer(np.log1p).transform(X),
|
|
|
|
|
|
np.log1p(X),
|
2015-07-31 00:48:04 +08:00
|
|
|
|
)
|
2015-11-24 22:05:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_kw_arg():
|
|
|
|
|
|
X = np.linspace(0, 1, num=10).reshape((5, 2))
|
|
|
|
|
|
|
|
|
|
|
|
F = FunctionTransformer(np.around, kw_args=dict(decimals=3))
|
|
|
|
|
|
|
|
|
|
|
|
# Test that rounding is correct
|
2016-10-08 00:46:52 +08:00
|
|
|
|
assert_array_equal(F.transform(X), np.around(X, decimals=3))
|
2015-11-24 22:05:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_kw_arg_update():
|
|
|
|
|
|
X = np.linspace(0, 1, num=10).reshape((5, 2))
|
|
|
|
|
|
|
|
|
|
|
|
F = FunctionTransformer(np.around, kw_args=dict(decimals=3))
|
|
|
|
|
|
|
|
|
|
|
|
F.kw_args["decimals"] = 1
|
|
|
|
|
|
|
|
|
|
|
|
# Test that rounding is correct
|
2016-10-08 00:46:52 +08:00
|
|
|
|
assert_array_equal(F.transform(X), np.around(X, decimals=1))
|
2015-11-24 22:05:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_kw_arg_reset():
|
|
|
|
|
|
X = np.linspace(0, 1, num=10).reshape((5, 2))
|
|
|
|
|
|
|
|
|
|
|
|
F = FunctionTransformer(np.around, kw_args=dict(decimals=3))
|
|
|
|
|
|
|
|
|
|
|
|
F.kw_args = dict(decimals=1)
|
|
|
|
|
|
|
|
|
|
|
|
# Test that rounding is correct
|
2016-10-08 00:46:52 +08:00
|
|
|
|
assert_array_equal(F.transform(X), np.around(X, decimals=1))
|
2016-04-12 13:26:30 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_inverse_transform():
|
|
|
|
|
|
X = np.array([1, 4, 9, 16]).reshape((2, 2))
|
|
|
|
|
|
|
|
|
|
|
|
# Test that inverse_transform works correctly
|
|
|
|
|
|
F = FunctionTransformer(
|
2016-10-08 00:46:52 +08:00
|
|
|
|
func=np.sqrt,
|
|
|
|
|
|
inverse_func=np.around,
|
|
|
|
|
|
inv_kw_args=dict(decimals=3),
|
|
|
|
|
|
)
|
|
|
|
|
|
assert_array_equal(
|
|
|
|
|
|
F.inverse_transform(F.transform(X)),
|
|
|
|
|
|
np.around(np.sqrt(X), decimals=3),
|
|
|
|
|
|
)
|
2017-10-26 04:49:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_check_inverse():
|
|
|
|
|
|
X_dense = np.array([1, 4, 9, 16], dtype=np.float64).reshape((2, 2))
|
|
|
|
|
|
|
|
|
|
|
|
X_list = [X_dense, sparse.csr_matrix(X_dense), sparse.csc_matrix(X_dense)]
|
|
|
|
|
|
|
|
|
|
|
|
for X in X_list:
|
|
|
|
|
|
if sparse.issparse(X):
|
|
|
|
|
|
accept_sparse = True
|
|
|
|
|
|
else:
|
|
|
|
|
|
accept_sparse = False
|
|
|
|
|
|
trans = FunctionTransformer(
|
|
|
|
|
|
func=np.sqrt,
|
|
|
|
|
|
inverse_func=np.around,
|
|
|
|
|
|
accept_sparse=accept_sparse,
|
2018-07-10 17:48:03 +08:00
|
|
|
|
check_inverse=True,
|
|
|
|
|
|
validate=True,
|
|
|
|
|
|
)
|
2021-03-20 22:07:09 +08:00
|
|
|
|
warning_message = (
|
|
|
|
|
|
"The provided functions are not strictly"
|
|
|
|
|
|
" inverse of each other. If you are sure you"
|
|
|
|
|
|
" want to proceed regardless, set"
|
|
|
|
|
|
" 'check_inverse=False'."
|
|
|
|
|
|
)
|
|
|
|
|
|
with pytest.warns(UserWarning, match=warning_message):
|
|
|
|
|
|
trans.fit(X)
|
2017-10-26 04:49:28 +08:00
|
|
|
|
|
|
|
|
|
|
trans = FunctionTransformer(
|
|
|
|
|
|
func=np.expm1,
|
|
|
|
|
|
inverse_func=np.log1p,
|
|
|
|
|
|
accept_sparse=accept_sparse,
|
2018-07-10 17:48:03 +08:00
|
|
|
|
check_inverse=True,
|
|
|
|
|
|
validate=True,
|
|
|
|
|
|
)
|
2021-03-20 22:07:09 +08:00
|
|
|
|
with pytest.warns(None) as record:
|
|
|
|
|
|
Xt = trans.fit_transform(X)
|
|
|
|
|
|
assert len(record) == 0
|
2017-10-26 04:49:28 +08:00
|
|
|
|
assert_allclose_dense_sparse(X, trans.inverse_transform(Xt))
|
|
|
|
|
|
|
|
|
|
|
|
# check that we don't check inverse when one of the func or inverse is not
|
|
|
|
|
|
# provided.
|
|
|
|
|
|
trans = FunctionTransformer(
|
2018-07-10 17:48:03 +08:00
|
|
|
|
func=np.expm1, inverse_func=None, check_inverse=True, validate=True
|
|
|
|
|
|
)
|
2021-03-20 22:07:09 +08:00
|
|
|
|
with pytest.warns(None) as record:
|
|
|
|
|
|
trans.fit(X_dense)
|
|
|
|
|
|
assert len(record) == 0
|
2017-10-26 04:49:28 +08:00
|
|
|
|
trans = FunctionTransformer(
|
2018-07-10 17:48:03 +08:00
|
|
|
|
func=None, inverse_func=np.expm1, check_inverse=True, validate=True
|
|
|
|
|
|
)
|
2021-03-20 22:07:09 +08:00
|
|
|
|
with pytest.warns(None) as record:
|
|
|
|
|
|
trans.fit(X_dense)
|
|
|
|
|
|
assert len(record) == 0
|
2018-07-10 17:48:03 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_function_transformer_frame():
|
|
|
|
|
|
pd = pytest.importorskip("pandas")
|
|
|
|
|
|
X_df = pd.DataFrame(np.random.randn(100, 10))
|
2019-06-14 00:06:01 +08:00
|
|
|
|
transformer = FunctionTransformer()
|
2018-07-10 17:48:03 +08:00
|
|
|
|
X_df_trans = transformer.fit_transform(X_df)
|
|
|
|
|
|
assert hasattr(X_df_trans, "loc")
|
2021-09-08 17:01:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
2021-12-01 02:31:17 +08:00
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
"X, feature_names_out, input_features, expected",
|
|
|
|
|
|
[
|
|
|
|
|
|
(
|
|
|
|
|
|
# NumPy inputs, default behavior: generate names
|
|
|
|
|
|
np.random.rand(100, 3),
|
|
|
|
|
|
"one-to-one",
|
|
|
|
|
|
None,
|
|
|
|
|
|
("x0", "x1", "x2"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# Pandas input, default behavior: use input feature names
|
|
|
|
|
|
{"a": np.random.rand(100), "b": np.random.rand(100)},
|
|
|
|
|
|
"one-to-one",
|
|
|
|
|
|
None,
|
|
|
|
|
|
("a", "b"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# NumPy input, feature_names_out=callable
|
|
|
|
|
|
np.random.rand(100, 3),
|
|
|
|
|
|
lambda transformer, input_features: ("a", "b"),
|
|
|
|
|
|
None,
|
|
|
|
|
|
("a", "b"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# Pandas input, feature_names_out=callable
|
|
|
|
|
|
{"a": np.random.rand(100), "b": np.random.rand(100)},
|
|
|
|
|
|
lambda transformer, input_features: ("c", "d", "e"),
|
|
|
|
|
|
None,
|
|
|
|
|
|
("c", "d", "e"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# NumPy input, feature_names_out=callable – default input_features
|
|
|
|
|
|
np.random.rand(100, 3),
|
|
|
|
|
|
lambda transformer, input_features: tuple(input_features) + ("a",),
|
|
|
|
|
|
None,
|
|
|
|
|
|
("x0", "x1", "x2", "a"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# Pandas input, feature_names_out=callable – default input_features
|
|
|
|
|
|
{"a": np.random.rand(100), "b": np.random.rand(100)},
|
|
|
|
|
|
lambda transformer, input_features: tuple(input_features) + ("c",),
|
|
|
|
|
|
None,
|
|
|
|
|
|
("a", "b", "c"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# NumPy input, input_features=list of names
|
|
|
|
|
|
np.random.rand(100, 3),
|
|
|
|
|
|
"one-to-one",
|
|
|
|
|
|
("a", "b", "c"),
|
|
|
|
|
|
("a", "b", "c"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# Pandas input, input_features=list of names
|
|
|
|
|
|
{"a": np.random.rand(100), "b": np.random.rand(100)},
|
|
|
|
|
|
"one-to-one",
|
|
|
|
|
|
("a", "b"), # must match feature_names_in_
|
|
|
|
|
|
("a", "b"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# NumPy input, feature_names_out=callable, input_features=list
|
|
|
|
|
|
np.random.rand(100, 3),
|
|
|
|
|
|
lambda transformer, input_features: tuple(input_features) + ("d",),
|
|
|
|
|
|
("a", "b", "c"),
|
|
|
|
|
|
("a", "b", "c", "d"),
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
# Pandas input, feature_names_out=callable, input_features=list
|
|
|
|
|
|
{"a": np.random.rand(100), "b": np.random.rand(100)},
|
|
|
|
|
|
lambda transformer, input_features: tuple(input_features) + ("c",),
|
|
|
|
|
|
("a", "b"), # must match feature_names_in_
|
|
|
|
|
|
("a", "b", "c"),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
def test_function_transformer_get_feature_names_out(
|
|
|
|
|
|
X, feature_names_out, input_features, expected
|
|
|
|
|
|
):
|
|
|
|
|
|
if isinstance(X, dict):
|
|
|
|
|
|
pd = pytest.importorskip("pandas")
|
|
|
|
|
|
X = pd.DataFrame(X)
|
|
|
|
|
|
|
|
|
|
|
|
transformer = FunctionTransformer(
|
|
|
|
|
|
feature_names_out=feature_names_out, validate=True
|
|
|
|
|
|
)
|
|
|
|
|
|
transformer.fit_transform(X)
|
|
|
|
|
|
names = transformer.get_feature_names_out(input_features)
|
|
|
|
|
|
assert isinstance(names, np.ndarray)
|
|
|
|
|
|
assert names.dtype == object
|
|
|
|
|
|
assert_array_equal(names, expected)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_function_transformer_get_feature_names_out_without_validation():
|
|
|
|
|
|
transformer = FunctionTransformer(feature_names_out="one-to-one", validate=False)
|
|
|
|
|
|
X = np.random.rand(100, 2)
|
|
|
|
|
|
transformer.fit_transform(X)
|
|
|
|
|
|
|
|
|
|
|
|
msg = "When 'feature_names_out' is 'one-to-one', either"
|
|
|
|
|
|
with pytest.raises(ValueError, match=msg):
|
|
|
|
|
|
transformer.get_feature_names_out()
|
|
|
|
|
|
|
|
|
|
|
|
names = transformer.get_feature_names_out(("a", "b"))
|
|
|
|
|
|
assert isinstance(names, np.ndarray)
|
|
|
|
|
|
assert names.dtype == object
|
|
|
|
|
|
assert_array_equal(names, ("a", "b"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("feature_names_out", ["x0", ["x0"], ("x0",)])
|
|
|
|
|
|
def test_function_transformer_feature_names_out_string(feature_names_out):
|
|
|
|
|
|
transformer = FunctionTransformer(feature_names_out=feature_names_out)
|
|
|
|
|
|
X = np.random.rand(100, 2)
|
|
|
|
|
|
transformer.fit_transform(X)
|
|
|
|
|
|
|
|
|
|
|
|
msg = """must either be "one-to-one" or a callable"""
|
|
|
|
|
|
with pytest.raises(ValueError, match=msg):
|
|
|
|
|
|
transformer.get_feature_names_out()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_function_transformer_feature_names_out_is_None():
|
|
|
|
|
|
transformer = FunctionTransformer()
|
|
|
|
|
|
X = np.random.rand(100, 2)
|
|
|
|
|
|
transformer.fit_transform(X)
|
|
|
|
|
|
|
|
|
|
|
|
msg = "This 'FunctionTransformer' has no attribute 'get_feature_names_out'"
|
|
|
|
|
|
with pytest.raises(AttributeError, match=msg):
|
|
|
|
|
|
transformer.get_feature_names_out()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_function_transformer_feature_names_out_uses_estimator():
|
|
|
|
|
|
def add_n_random_features(X, n):
|
|
|
|
|
|
return np.concatenate([X, np.random.rand(len(X), n)], axis=1)
|
|
|
|
|
|
|
|
|
|
|
|
def feature_names_out(transformer, input_features):
|
|
|
|
|
|
n = transformer.kw_args["n"]
|
|
|
|
|
|
return list(input_features) + [f"rnd{i}" for i in range(n)]
|
|
|
|
|
|
|
|
|
|
|
|
transformer = FunctionTransformer(
|
|
|
|
|
|
func=add_n_random_features,
|
|
|
|
|
|
feature_names_out=feature_names_out,
|
|
|
|
|
|
kw_args=dict(n=3),
|
|
|
|
|
|
validate=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
pd = pytest.importorskip("pandas")
|
|
|
|
|
|
df = pd.DataFrame({"a": np.random.rand(100), "b": np.random.rand(100)})
|
|
|
|
|
|
transformer.fit_transform(df)
|
|
|
|
|
|
names = transformer.get_feature_names_out()
|
|
|
|
|
|
|
|
|
|
|
|
assert isinstance(names, np.ndarray)
|
|
|
|
|
|
assert names.dtype == object
|
|
|
|
|
|
assert_array_equal(names, ("a", "b", "rnd0", "rnd1", "rnd2"))
|
|
|
|
|
|
|
|
|
|
|
|
|
2021-09-08 17:01:24 +08:00
|
|
|
|
def test_function_transformer_validate_inverse():
|
|
|
|
|
|
"""Test that function transformer does not reset estimator in
|
|
|
|
|
|
`inverse_transform`."""
|
|
|
|
|
|
|
|
|
|
|
|
def add_constant_feature(X):
|
|
|
|
|
|
X_one = np.ones((X.shape[0], 1))
|
|
|
|
|
|
return np.concatenate((X, X_one), axis=1)
|
|
|
|
|
|
|
|
|
|
|
|
def inverse_add_constant(X):
|
|
|
|
|
|
return X[:, :-1]
|
|
|
|
|
|
|
|
|
|
|
|
X = np.array([[1, 2], [3, 4], [3, 4]])
|
|
|
|
|
|
trans = FunctionTransformer(
|
|
|
|
|
|
func=add_constant_feature,
|
|
|
|
|
|
inverse_func=inverse_add_constant,
|
|
|
|
|
|
validate=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
X_trans = trans.fit_transform(X)
|
|
|
|
|
|
assert trans.n_features_in_ == X.shape[1]
|
|
|
|
|
|
|
|
|
|
|
|
trans.inverse_transform(X_trans)
|
|
|
|
|
|
assert trans.n_features_in_ == X.shape[1]
|