first commit

This commit is contained in:
Carla Floricel
2022-08-02 09:52:52 -04:00
parent 417ea8660b
commit 05e52aa52b
10444 changed files with 2300232 additions and 0 deletions

View File

@@ -0,0 +1,333 @@
import itertools
import pickle
import copy
import numpy as np
from numpy.testing import assert_array_almost_equal
import pytest
import scipy.sparse as sp
from scipy.spatial.distance import cdist
from sklearn.metrics import DistanceMetric
from sklearn.metrics._dist_metrics import BOOL_METRICS
from sklearn.utils import check_random_state
from sklearn.utils._testing import create_memmap_backed_data
from sklearn.utils.fixes import sp_version, parse_version
def dist_func(x1, x2, p):
return np.sum((x1 - x2) ** p) ** (1.0 / p)
rng = check_random_state(0)
d = 4
n1 = 20
n2 = 25
X1 = rng.random_sample((n1, d)).astype("float64", copy=False)
X2 = rng.random_sample((n2, d)).astype("float64", copy=False)
[X1_mmap, X2_mmap] = create_memmap_backed_data([X1, X2])
# make boolean arrays: ones and zeros
X1_bool = X1.round(0)
X2_bool = X2.round(0)
[X1_bool_mmap, X2_bool_mmap] = create_memmap_backed_data([X1_bool, X2_bool])
V = rng.random_sample((d, d))
VI = np.dot(V, V.T)
METRICS_DEFAULT_PARAMS = [
("euclidean", {}),
("cityblock", {}),
("minkowski", dict(p=(1, 1.5, 2, 3))),
("chebyshev", {}),
("seuclidean", dict(V=(rng.random_sample(d),))),
("mahalanobis", dict(VI=(VI,))),
("hamming", {}),
("canberra", {}),
("braycurtis", {}),
]
if sp_version >= parse_version("1.8.0.dev0"):
# Starting from scipy 1.8.0.dev0, minkowski now accepts w, the weighting
# parameter directly and using it is preferred over using wminkowski.
METRICS_DEFAULT_PARAMS.append(
("minkowski", dict(p=(1, 1.5, 3), w=(rng.random_sample(d),))),
)
else:
# For previous versions of scipy, this was possible through a dedicated
# metric (deprecated in 1.6 and removed in 1.8).
METRICS_DEFAULT_PARAMS.append(
("wminkowski", dict(p=(1, 1.5, 3), w=(rng.random_sample(d),))),
)
def check_cdist(metric, kwargs, X1, X2):
if metric == "wminkowski":
# wminkoski is deprecated in SciPy 1.6.0 and removed in 1.8.0
WarningToExpect = None
if sp_version >= parse_version("1.6.0"):
WarningToExpect = DeprecationWarning
with pytest.warns(WarningToExpect):
D_scipy_cdist = cdist(X1, X2, metric, **kwargs)
else:
D_scipy_cdist = cdist(X1, X2, metric, **kwargs)
dm = DistanceMetric.get_metric(metric, **kwargs)
D_sklearn = dm.pairwise(X1, X2)
assert_array_almost_equal(D_sklearn, D_scipy_cdist)
# TODO: Remove filterwarnings in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
@pytest.mark.parametrize("metric_param_grid", METRICS_DEFAULT_PARAMS)
@pytest.mark.parametrize("X1, X2", [(X1, X2), (X1_mmap, X2_mmap)])
def test_cdist(metric_param_grid, X1, X2):
metric, param_grid = metric_param_grid
keys = param_grid.keys()
for vals in itertools.product(*param_grid.values()):
kwargs = dict(zip(keys, vals))
if metric == "mahalanobis":
# See: https://github.com/scipy/scipy/issues/13861
# Possibly caused by: https://github.com/joblib/joblib/issues/563
pytest.xfail(
"scipy#13861: cdist with 'mahalanobis' fails on joblib memmap data"
)
check_cdist(metric, kwargs, X1, X2)
@pytest.mark.parametrize("metric", BOOL_METRICS)
@pytest.mark.parametrize(
"X1_bool, X2_bool", [(X1_bool, X2_bool), (X1_bool_mmap, X2_bool_mmap)]
)
def test_cdist_bool_metric(metric, X1_bool, X2_bool):
D_true = cdist(X1_bool, X2_bool, metric)
check_cdist_bool(metric, D_true)
def check_cdist_bool(metric, D_true):
dm = DistanceMetric.get_metric(metric)
D12 = dm.pairwise(X1_bool, X2_bool)
assert_array_almost_equal(D12, D_true)
# TODO: Remove filterwarnings in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
@pytest.mark.parametrize("metric_param_grid", METRICS_DEFAULT_PARAMS)
@pytest.mark.parametrize("X1, X2", [(X1, X2), (X1_mmap, X2_mmap)])
def test_pdist(metric_param_grid, X1, X2):
metric, param_grid = metric_param_grid
keys = param_grid.keys()
for vals in itertools.product(*param_grid.values()):
kwargs = dict(zip(keys, vals))
if metric == "mahalanobis":
# See: https://github.com/scipy/scipy/issues/13861
pytest.xfail("scipy#13861: pdist with 'mahalanobis' fails onmemmap data")
elif metric == "wminkowski":
if sp_version >= parse_version("1.8.0"):
pytest.skip("wminkowski will be removed in SciPy 1.8.0")
# wminkoski is deprecated in SciPy 1.6.0 and removed in 1.8.0
ExceptionToAssert = None
if sp_version >= parse_version("1.6.0"):
ExceptionToAssert = DeprecationWarning
with pytest.warns(ExceptionToAssert):
D_true = cdist(X1, X1, metric, **kwargs)
else:
D_true = cdist(X1, X1, metric, **kwargs)
check_pdist(metric, kwargs, D_true)
@pytest.mark.parametrize("metric", BOOL_METRICS)
@pytest.mark.parametrize("X1_bool", [X1_bool, X1_bool_mmap])
def test_pdist_bool_metrics(metric, X1_bool):
D_true = cdist(X1_bool, X1_bool, metric)
check_pdist_bool(metric, D_true)
def check_pdist(metric, kwargs, D_true):
dm = DistanceMetric.get_metric(metric, **kwargs)
D12 = dm.pairwise(X1)
assert_array_almost_equal(D12, D_true)
def check_pdist_bool(metric, D_true):
dm = DistanceMetric.get_metric(metric)
D12 = dm.pairwise(X1_bool)
# Based on https://github.com/scipy/scipy/pull/7373
# When comparing two all-zero vectors, scipy>=1.2.0 jaccard metric
# was changed to return 0, instead of nan.
if metric == "jaccard" and sp_version < parse_version("1.2.0"):
D_true[np.isnan(D_true)] = 0
assert_array_almost_equal(D12, D_true)
# TODO: Remove filterwarnings in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
@pytest.mark.parametrize("writable_kwargs", [True, False])
@pytest.mark.parametrize("metric_param_grid", METRICS_DEFAULT_PARAMS)
def test_pickle(writable_kwargs, metric_param_grid):
metric, param_grid = metric_param_grid
keys = param_grid.keys()
for vals in itertools.product(*param_grid.values()):
if any(isinstance(val, np.ndarray) for val in vals):
vals = copy.deepcopy(vals)
for val in vals:
if isinstance(val, np.ndarray):
val.setflags(write=writable_kwargs)
kwargs = dict(zip(keys, vals))
check_pickle(metric, kwargs)
# TODO: Remove filterwarnings in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
@pytest.mark.parametrize("metric", BOOL_METRICS)
@pytest.mark.parametrize("X1_bool", [X1_bool, X1_bool_mmap])
def test_pickle_bool_metrics(metric, X1_bool):
dm = DistanceMetric.get_metric(metric)
D1 = dm.pairwise(X1_bool)
dm2 = pickle.loads(pickle.dumps(dm))
D2 = dm2.pairwise(X1_bool)
assert_array_almost_equal(D1, D2)
def check_pickle(metric, kwargs):
dm = DistanceMetric.get_metric(metric, **kwargs)
D1 = dm.pairwise(X1)
dm2 = pickle.loads(pickle.dumps(dm))
D2 = dm2.pairwise(X1)
assert_array_almost_equal(D1, D2)
def test_haversine_metric():
def haversine_slow(x1, x2):
return 2 * np.arcsin(
np.sqrt(
np.sin(0.5 * (x1[0] - x2[0])) ** 2
+ np.cos(x1[0]) * np.cos(x2[0]) * np.sin(0.5 * (x1[1] - x2[1])) ** 2
)
)
X = np.random.random((10, 2))
haversine = DistanceMetric.get_metric("haversine")
D1 = haversine.pairwise(X)
D2 = np.zeros_like(D1)
for i, x1 in enumerate(X):
for j, x2 in enumerate(X):
D2[i, j] = haversine_slow(x1, x2)
assert_array_almost_equal(D1, D2)
assert_array_almost_equal(haversine.dist_to_rdist(D1), np.sin(0.5 * D2) ** 2)
def test_pyfunc_metric():
X = np.random.random((10, 3))
euclidean = DistanceMetric.get_metric("euclidean")
pyfunc = DistanceMetric.get_metric("pyfunc", func=dist_func, p=2)
# Check if both callable metric and predefined metric initialized
# DistanceMetric object is picklable
euclidean_pkl = pickle.loads(pickle.dumps(euclidean))
pyfunc_pkl = pickle.loads(pickle.dumps(pyfunc))
D1 = euclidean.pairwise(X)
D2 = pyfunc.pairwise(X)
D1_pkl = euclidean_pkl.pairwise(X)
D2_pkl = pyfunc_pkl.pairwise(X)
assert_array_almost_equal(D1, D2)
assert_array_almost_equal(D1_pkl, D2_pkl)
def test_input_data_size():
# Regression test for #6288
# Previously, a metric requiring a particular input dimension would fail
def custom_metric(x, y):
assert x.shape[0] == 3
return np.sum((x - y) ** 2)
rng = check_random_state(0)
X = rng.rand(10, 3)
pyfunc = DistanceMetric.get_metric("pyfunc", func=custom_metric)
eucl = DistanceMetric.get_metric("euclidean")
assert_array_almost_equal(pyfunc.pairwise(X), eucl.pairwise(X) ** 2)
# TODO: Remove filterwarnings in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
def test_readonly_kwargs():
# Non-regression test for:
# https://github.com/scikit-learn/scikit-learn/issues/21685
rng = check_random_state(0)
weights = rng.rand(100)
VI = rng.rand(10, 10)
weights.setflags(write=False)
VI.setflags(write=False)
# Those distances metrics have to support readonly buffers.
DistanceMetric.get_metric("seuclidean", V=weights)
DistanceMetric.get_metric("wminkowski", p=1, w=weights)
DistanceMetric.get_metric("mahalanobis", VI=VI)
@pytest.mark.parametrize(
"w, err_type, err_msg",
[
(np.array([1, 1.5, -13]), ValueError, "w cannot contain negative weights"),
(np.array([1, 1.5, np.nan]), ValueError, "w contains NaN"),
(
sp.csr_matrix([1, 1.5, 1]),
TypeError,
"A sparse matrix was passed, but dense data is required",
),
(np.array(["a", "b", "c"]), ValueError, "could not convert string to float"),
(np.array([]), ValueError, "a minimum of 1 is required"),
],
)
def test_minkowski_metric_validate_weights_values(w, err_type, err_msg):
with pytest.raises(err_type, match=err_msg):
DistanceMetric.get_metric("minkowski", p=3, w=w)
def test_minkowski_metric_validate_weights_size():
w2 = rng.random_sample(d + 1)
dm = DistanceMetric.get_metric("minkowski", p=3, w=w2)
msg = (
"MinkowskiDistance: the size of w must match "
f"the number of features \\({X1.shape[1]}\\). "
f"Currently len\\(w\\)={w2.shape[0]}."
)
with pytest.raises(ValueError, match=msg):
dm.pairwise(X1, X2)
# TODO: Remove in 1.3 when wminkowski is removed
def test_wminkowski_deprecated():
w = rng.random_sample(d)
msg = "WMinkowskiDistance is deprecated in version 1.1"
with pytest.warns(FutureWarning, match=msg):
DistanceMetric.get_metric("wminkowski", p=3, w=w)
# TODO: Remove in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
@pytest.mark.parametrize("p", [1, 1.5, 3])
def test_wminkowski_minkowski_equivalence(p):
w = rng.random_sample(d)
# Weights are rescaled for consistency w.r.t scipy 1.8 refactoring of 'minkowski'
dm_wmks = DistanceMetric.get_metric("wminkowski", p=p, w=(w) ** (1 / p))
dm_mks = DistanceMetric.get_metric("minkowski", p=p, w=w)
D_wmks = dm_wmks.pairwise(X1, X2)
D_mks = dm_mks.pairwise(X1, X2)
assert_array_almost_equal(D_wmks, D_mks)

View File

@@ -0,0 +1,551 @@
import numpy as np
import pytest
import threadpoolctl
from numpy.testing import assert_array_equal, assert_allclose
from scipy.sparse import csr_matrix
from scipy.spatial.distance import cdist
from sklearn.metrics._pairwise_distances_reduction import (
PairwiseDistancesReduction,
PairwiseDistancesArgKmin,
PairwiseDistancesRadiusNeighborhood,
_sqeuclidean_row_norms,
)
from sklearn.metrics import euclidean_distances
from sklearn.utils.fixes import sp_version, parse_version
# Common supported metric between scipy.spatial.distance.cdist
# and PairwiseDistancesReduction.
# This allows constructing tests to check consistency of results
# of concrete PairwiseDistancesReduction on some metrics using APIs
# from scipy and numpy.
CDIST_PAIRWISE_DISTANCES_REDUCTION_COMMON_METRICS = [
"braycurtis",
"canberra",
"chebyshev",
"cityblock",
"euclidean",
"minkowski",
"seuclidean",
]
def _get_metric_params_list(metric: str, n_features: int, seed: int = 1):
"""Return list of dummy DistanceMetric kwargs for tests."""
# Distinguishing on cases not to compute unneeded datastructures.
rng = np.random.RandomState(seed)
if metric == "minkowski":
minkowski_kwargs = [dict(p=1.5), dict(p=2), dict(p=3), dict(p=np.inf)]
if sp_version >= parse_version("1.8.0.dev0"):
# TODO: remove the test once we no longer support scipy < 1.8.0.
# Recent scipy versions accept weights in the Minkowski metric directly:
# type: ignore
minkowski_kwargs.append(dict(p=3, w=rng.rand(n_features)))
return minkowski_kwargs
# TODO: remove this case for "wminkowski" once we no longer support scipy < 1.8.0.
if metric == "wminkowski":
weights = rng.random_sample(n_features)
weights /= weights.sum()
wminkowski_kwargs = [dict(p=1.5, w=weights)]
if sp_version < parse_version("1.8.0.dev0"):
# wminkowski was removed in scipy 1.8.0 but should work for previous
# versions.
wminkowski_kwargs.append(dict(p=3, w=rng.rand(n_features)))
return wminkowski_kwargs
if metric == "seuclidean":
return [dict(V=rng.rand(n_features))]
# Case of: "euclidean", "manhattan", "chebyshev", "haversine" or any other metric.
# In those cases, no kwargs is needed.
return [{}]
def assert_argkmin_results_equality(ref_dist, dist, ref_indices, indices):
assert_array_equal(
ref_indices,
indices,
err_msg="Query vectors have different neighbors' indices",
)
assert_allclose(
ref_dist,
dist,
err_msg="Query vectors have different neighbors' distances",
rtol=1e-7,
)
def assert_radius_neighborhood_results_equality(ref_dist, dist, ref_indices, indices):
# We get arrays of arrays and we need to check for individual pairs
for i in range(ref_dist.shape[0]):
assert_array_equal(
ref_indices[i],
indices[i],
err_msg=f"Query vector #{i} has different neighbors' indices",
)
assert_allclose(
ref_dist[i],
dist[i],
err_msg=f"Query vector #{i} has different neighbors' distances",
rtol=1e-7,
)
ASSERT_RESULT = {
PairwiseDistancesArgKmin: assert_argkmin_results_equality,
PairwiseDistancesRadiusNeighborhood: assert_radius_neighborhood_results_equality,
}
def test_pairwise_distances_reduction_is_usable_for():
rng = np.random.RandomState(0)
X = rng.rand(100, 10)
Y = rng.rand(100, 10)
metric = "euclidean"
assert PairwiseDistancesReduction.is_usable_for(X, Y, metric)
assert not PairwiseDistancesReduction.is_usable_for(
X.astype(np.int64), Y.astype(np.int64), metric
)
assert not PairwiseDistancesReduction.is_usable_for(X, Y, metric="pyfunc")
# TODO: remove once 32 bits datasets are supported
assert not PairwiseDistancesReduction.is_usable_for(X.astype(np.float32), Y, metric)
assert not PairwiseDistancesReduction.is_usable_for(X, Y.astype(np.int32), metric)
# TODO: remove once sparse matrices are supported
assert not PairwiseDistancesReduction.is_usable_for(csr_matrix(X), Y, metric)
assert not PairwiseDistancesReduction.is_usable_for(X, csr_matrix(Y), metric)
def test_argkmin_factory_method_wrong_usages():
rng = np.random.RandomState(1)
X = rng.rand(100, 10)
Y = rng.rand(100, 10)
k = 5
metric = "euclidean"
msg = (
"Only 64bit float datasets are supported at this time, "
"got: X.dtype=float32 and Y.dtype=float64"
)
with pytest.raises(ValueError, match=msg):
PairwiseDistancesArgKmin.compute(
X=X.astype(np.float32), Y=Y, k=k, metric=metric
)
msg = (
"Only 64bit float datasets are supported at this time, "
"got: X.dtype=float64 and Y.dtype=int32"
)
with pytest.raises(ValueError, match=msg):
PairwiseDistancesArgKmin.compute(X=X, Y=Y.astype(np.int32), k=k, metric=metric)
with pytest.raises(ValueError, match="k == -1, must be >= 1."):
PairwiseDistancesArgKmin.compute(X=X, Y=Y, k=-1, metric=metric)
with pytest.raises(ValueError, match="k == 0, must be >= 1."):
PairwiseDistancesArgKmin.compute(X=X, Y=Y, k=0, metric=metric)
with pytest.raises(ValueError, match="Unrecognized metric"):
PairwiseDistancesArgKmin.compute(X=X, Y=Y, k=k, metric="wrong metric")
with pytest.raises(
ValueError, match=r"Buffer has wrong number of dimensions \(expected 2, got 1\)"
):
PairwiseDistancesArgKmin.compute(
X=np.array([1.0, 2.0]), Y=Y, k=k, metric=metric
)
with pytest.raises(ValueError, match="ndarray is not C-contiguous"):
PairwiseDistancesArgKmin.compute(
X=np.asfortranarray(X), Y=Y, k=k, metric=metric
)
unused_metric_kwargs = {"p": 3}
message = (
r"Some metric_kwargs have been passed \({'p': 3}\) but aren't usable for this"
r" case \("
r"FastEuclideanPairwiseDistancesArgKmin\) and will be ignored."
)
with pytest.warns(UserWarning, match=message):
PairwiseDistancesArgKmin.compute(
X=X, Y=Y, k=k, metric=metric, metric_kwargs=unused_metric_kwargs
)
def test_radius_neighborhood_factory_method_wrong_usages():
rng = np.random.RandomState(1)
X = rng.rand(100, 10)
Y = rng.rand(100, 10)
radius = 5
metric = "euclidean"
with pytest.raises(
ValueError,
match=(
"Only 64bit float datasets are supported at this time, "
"got: X.dtype=float32 and Y.dtype=float64"
),
):
PairwiseDistancesRadiusNeighborhood.compute(
X=X.astype(np.float32), Y=Y, radius=radius, metric=metric
)
with pytest.raises(
ValueError,
match=(
"Only 64bit float datasets are supported at this time, "
"got: X.dtype=float64 and Y.dtype=int32"
),
):
PairwiseDistancesRadiusNeighborhood.compute(
X=X, Y=Y.astype(np.int32), radius=radius, metric=metric
)
with pytest.raises(ValueError, match="radius == -1.0, must be >= 0."):
PairwiseDistancesRadiusNeighborhood.compute(X=X, Y=Y, radius=-1, metric=metric)
with pytest.raises(ValueError, match="Unrecognized metric"):
PairwiseDistancesRadiusNeighborhood.compute(
X=X, Y=Y, radius=radius, metric="wrong metric"
)
with pytest.raises(
ValueError, match=r"Buffer has wrong number of dimensions \(expected 2, got 1\)"
):
PairwiseDistancesRadiusNeighborhood.compute(
X=np.array([1.0, 2.0]), Y=Y, radius=radius, metric=metric
)
with pytest.raises(ValueError, match="ndarray is not C-contiguous"):
PairwiseDistancesRadiusNeighborhood.compute(
X=np.asfortranarray(X), Y=Y, radius=radius, metric=metric
)
unused_metric_kwargs = {"p": 3}
message = (
r"Some metric_kwargs have been passed \({'p': 3}\) but aren't usable for this"
r" case \(FastEuclideanPairwiseDistancesRadiusNeighborhood\) and will be"
r" ignored."
)
with pytest.warns(UserWarning, match=message):
PairwiseDistancesRadiusNeighborhood.compute(
X=X, Y=Y, radius=radius, metric=metric, metric_kwargs=unused_metric_kwargs
)
@pytest.mark.parametrize("n_samples", [100, 1000])
@pytest.mark.parametrize("chunk_size", [50, 512, 1024])
@pytest.mark.parametrize(
"PairwiseDistancesReduction",
[PairwiseDistancesArgKmin, PairwiseDistancesRadiusNeighborhood],
)
def test_chunk_size_agnosticism(
global_random_seed,
PairwiseDistancesReduction,
n_samples,
chunk_size,
n_features=100,
dtype=np.float64,
):
# Results should not depend on the chunk size
rng = np.random.RandomState(global_random_seed)
spread = 100
X = rng.rand(n_samples, n_features).astype(dtype) * spread
Y = rng.rand(n_samples, n_features).astype(dtype) * spread
parameter = (
10
if PairwiseDistancesReduction is PairwiseDistancesArgKmin
# Scaling the radius slightly with the numbers of dimensions
else 10 ** np.log(n_features)
)
ref_dist, ref_indices = PairwiseDistancesReduction.compute(
X,
Y,
parameter,
return_distance=True,
)
dist, indices = PairwiseDistancesReduction.compute(
X,
Y,
parameter,
chunk_size=chunk_size,
return_distance=True,
)
ASSERT_RESULT[PairwiseDistancesReduction](ref_dist, dist, ref_indices, indices)
@pytest.mark.parametrize("n_samples", [100, 1000])
@pytest.mark.parametrize("chunk_size", [50, 512, 1024])
@pytest.mark.parametrize(
"PairwiseDistancesReduction",
[PairwiseDistancesArgKmin, PairwiseDistancesRadiusNeighborhood],
)
def test_n_threads_agnosticism(
global_random_seed,
PairwiseDistancesReduction,
n_samples,
chunk_size,
n_features=100,
dtype=np.float64,
):
# Results should not depend on the number of threads
rng = np.random.RandomState(global_random_seed)
spread = 100
X = rng.rand(n_samples, n_features).astype(dtype) * spread
Y = rng.rand(n_samples, n_features).astype(dtype) * spread
parameter = (
10
if PairwiseDistancesReduction is PairwiseDistancesArgKmin
# Scaling the radius slightly with the numbers of dimensions
else 10 ** np.log(n_features)
)
ref_dist, ref_indices = PairwiseDistancesReduction.compute(
X,
Y,
parameter,
return_distance=True,
)
with threadpoolctl.threadpool_limits(limits=1, user_api="openmp"):
dist, indices = PairwiseDistancesReduction.compute(
X, Y, parameter, return_distance=True
)
ASSERT_RESULT[PairwiseDistancesReduction](ref_dist, dist, ref_indices, indices)
# TODO: Remove filterwarnings in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
@pytest.mark.parametrize("n_samples", [100, 1000])
@pytest.mark.parametrize("metric", PairwiseDistancesReduction.valid_metrics())
@pytest.mark.parametrize(
"PairwiseDistancesReduction",
[PairwiseDistancesArgKmin, PairwiseDistancesRadiusNeighborhood],
)
def test_strategies_consistency(
global_random_seed,
PairwiseDistancesReduction,
metric,
n_samples,
n_features=10,
dtype=np.float64,
):
rng = np.random.RandomState(global_random_seed)
spread = 100
X = rng.rand(n_samples, n_features).astype(dtype) * spread
Y = rng.rand(n_samples, n_features).astype(dtype) * spread
# Haversine distance only accepts 2D data
if metric == "haversine":
X = np.ascontiguousarray(X[:, :2])
Y = np.ascontiguousarray(Y[:, :2])
parameter = (
10
if PairwiseDistancesReduction is PairwiseDistancesArgKmin
# Scaling the radius slightly with the numbers of dimensions
else 10 ** np.log(n_features)
)
dist_par_X, indices_par_X = PairwiseDistancesReduction.compute(
X,
Y,
parameter,
metric=metric,
# Taking the first
metric_kwargs=_get_metric_params_list(
metric, n_features, seed=global_random_seed
)[0],
# To be sure to use parallelization
chunk_size=n_samples // 4,
strategy="parallel_on_X",
return_distance=True,
)
dist_par_Y, indices_par_Y = PairwiseDistancesReduction.compute(
X,
Y,
parameter,
metric=metric,
# Taking the first
metric_kwargs=_get_metric_params_list(
metric, n_features, seed=global_random_seed
)[0],
# To be sure to use parallelization
chunk_size=n_samples // 4,
strategy="parallel_on_Y",
return_distance=True,
)
ASSERT_RESULT[PairwiseDistancesReduction](
dist_par_X,
dist_par_Y,
indices_par_X,
indices_par_Y,
)
# "Concrete PairwiseDistancesReductions"-specific tests
# TODO: Remove filterwarnings in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
@pytest.mark.parametrize("n_features", [50, 500])
@pytest.mark.parametrize("translation", [0, 1e6])
@pytest.mark.parametrize("metric", CDIST_PAIRWISE_DISTANCES_REDUCTION_COMMON_METRICS)
@pytest.mark.parametrize("strategy", ("parallel_on_X", "parallel_on_Y"))
def test_pairwise_distances_argkmin(
global_random_seed,
n_features,
translation,
metric,
strategy,
n_samples=100,
k=10,
dtype=np.float64,
):
rng = np.random.RandomState(global_random_seed)
spread = 1000
X = translation + rng.rand(n_samples, n_features).astype(dtype) * spread
Y = translation + rng.rand(n_samples, n_features).astype(dtype) * spread
# Haversine distance only accepts 2D data
if metric == "haversine":
X = np.ascontiguousarray(X[:, :2])
Y = np.ascontiguousarray(Y[:, :2])
metric_kwargs = _get_metric_params_list(metric, n_features)[0]
# Reference for argkmin results
if metric == "euclidean":
# Compare to scikit-learn GEMM optimized implementation
dist_matrix = euclidean_distances(X, Y)
else:
dist_matrix = cdist(X, Y, metric=metric, **metric_kwargs)
# Taking argkmin (indices of the k smallest values)
argkmin_indices_ref = np.argsort(dist_matrix, axis=1)[:, :k]
# Getting the associated distances
argkmin_distances_ref = np.zeros(argkmin_indices_ref.shape, dtype=np.float64)
for row_idx in range(argkmin_indices_ref.shape[0]):
argkmin_distances_ref[row_idx] = dist_matrix[
row_idx, argkmin_indices_ref[row_idx]
]
argkmin_distances, argkmin_indices = PairwiseDistancesArgKmin.compute(
X,
Y,
k,
metric=metric,
metric_kwargs=metric_kwargs,
return_distance=True,
# So as to have more than a chunk, forcing parallelism.
chunk_size=n_samples // 4,
strategy=strategy,
)
ASSERT_RESULT[PairwiseDistancesArgKmin](
argkmin_distances, argkmin_distances_ref, argkmin_indices, argkmin_indices_ref
)
# TODO: Remove filterwarnings in 1.3 when wminkowski is removed
@pytest.mark.filterwarnings("ignore:WMinkowskiDistance:FutureWarning:sklearn")
@pytest.mark.parametrize("n_features", [50, 500])
@pytest.mark.parametrize("translation", [0, 1e6])
@pytest.mark.parametrize("metric", CDIST_PAIRWISE_DISTANCES_REDUCTION_COMMON_METRICS)
@pytest.mark.parametrize("strategy", ("parallel_on_X", "parallel_on_Y"))
def test_pairwise_distances_radius_neighbors(
global_random_seed,
n_features,
translation,
metric,
strategy,
n_samples=100,
dtype=np.float64,
):
rng = np.random.RandomState(global_random_seed)
spread = 1000
radius = spread * np.log(n_features)
X = translation + rng.rand(n_samples, n_features).astype(dtype) * spread
Y = translation + rng.rand(n_samples, n_features).astype(dtype) * spread
metric_kwargs = _get_metric_params_list(
metric, n_features, seed=global_random_seed
)[0]
# Reference for argkmin results
if metric == "euclidean":
# Compare to scikit-learn GEMM optimized implementation
dist_matrix = euclidean_distances(X, Y)
else:
dist_matrix = cdist(X, Y, metric=metric, **metric_kwargs)
# Getting the neighbors for a given radius
neigh_indices_ref = []
neigh_distances_ref = []
for row in dist_matrix:
ind = np.arange(row.shape[0])[row <= radius]
dist = row[ind]
sort = np.argsort(dist)
ind, dist = ind[sort], dist[sort]
neigh_indices_ref.append(ind)
neigh_distances_ref.append(dist)
neigh_indices_ref = np.array(neigh_indices_ref)
neigh_distances_ref = np.array(neigh_distances_ref)
neigh_distances, neigh_indices = PairwiseDistancesRadiusNeighborhood.compute(
X,
Y,
radius,
metric=metric,
metric_kwargs=metric_kwargs,
return_distance=True,
# So as to have more than a chunk, forcing parallelism.
chunk_size=n_samples // 4,
strategy=strategy,
sort_results=True,
)
ASSERT_RESULT[PairwiseDistancesRadiusNeighborhood](
neigh_distances, neigh_distances_ref, neigh_indices, neigh_indices_ref
)
@pytest.mark.parametrize("n_samples", [100, 1000])
@pytest.mark.parametrize("n_features", [5, 10, 100])
@pytest.mark.parametrize("num_threads", [1, 2, 8])
def test_sqeuclidean_row_norms(
global_random_seed,
n_samples,
n_features,
num_threads,
dtype=np.float64,
):
rng = np.random.RandomState(global_random_seed)
spread = 100
X = rng.rand(n_samples, n_features).astype(dtype) * spread
sq_row_norm_reference = np.linalg.norm(X, axis=1) ** 2
sq_row_norm = np.asarray(_sqeuclidean_row_norms(X, num_threads=num_threads))
assert_allclose(sq_row_norm_reference, sq_row_norm)

View File

@@ -0,0 +1,615 @@
import numpy as np
from scipy import optimize
from numpy.testing import assert_allclose
from scipy.special import factorial, xlogy
from itertools import product
import pytest
from sklearn.utils._testing import assert_almost_equal
from sklearn.utils._testing import assert_array_equal
from sklearn.utils._testing import assert_array_almost_equal
from sklearn.dummy import DummyRegressor
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import explained_variance_score
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_squared_log_error
from sklearn.metrics import median_absolute_error
from sklearn.metrics import mean_absolute_percentage_error
from sklearn.metrics import max_error
from sklearn.metrics import mean_pinball_loss
from sklearn.metrics import r2_score
from sklearn.metrics import mean_tweedie_deviance
from sklearn.metrics import d2_tweedie_score
from sklearn.metrics import d2_pinball_score
from sklearn.metrics import d2_absolute_error_score
from sklearn.metrics import make_scorer
from sklearn.metrics._regression import _check_reg_targets
from sklearn.exceptions import UndefinedMetricWarning
def test_regression_metrics(n_samples=50):
y_true = np.arange(n_samples)
y_pred = y_true + 1
y_pred_2 = y_true - 1
assert_almost_equal(mean_squared_error(y_true, y_pred), 1.0)
assert_almost_equal(
mean_squared_log_error(y_true, y_pred),
mean_squared_error(np.log(1 + y_true), np.log(1 + y_pred)),
)
assert_almost_equal(mean_absolute_error(y_true, y_pred), 1.0)
assert_almost_equal(mean_pinball_loss(y_true, y_pred), 0.5)
assert_almost_equal(mean_pinball_loss(y_true, y_pred_2), 0.5)
assert_almost_equal(mean_pinball_loss(y_true, y_pred, alpha=0.4), 0.6)
assert_almost_equal(mean_pinball_loss(y_true, y_pred_2, alpha=0.4), 0.4)
assert_almost_equal(median_absolute_error(y_true, y_pred), 1.0)
mape = mean_absolute_percentage_error(y_true, y_pred)
assert np.isfinite(mape)
assert mape > 1e6
assert_almost_equal(max_error(y_true, y_pred), 1.0)
assert_almost_equal(r2_score(y_true, y_pred), 0.995, 2)
assert_almost_equal(r2_score(y_true, y_pred, force_finite=False), 0.995, 2)
assert_almost_equal(explained_variance_score(y_true, y_pred), 1.0)
assert_almost_equal(
explained_variance_score(y_true, y_pred, force_finite=False), 1.0
)
assert_almost_equal(
mean_tweedie_deviance(y_true, y_pred, power=0),
mean_squared_error(y_true, y_pred),
)
assert_almost_equal(
d2_tweedie_score(y_true, y_pred, power=0), r2_score(y_true, y_pred)
)
dev_median = np.abs(y_true - np.median(y_true)).sum()
assert_array_almost_equal(
d2_absolute_error_score(y_true, y_pred),
1 - np.abs(y_true - y_pred).sum() / dev_median,
)
alpha = 0.2
pinball_loss = lambda y_true, y_pred, alpha: alpha * np.maximum(
y_true - y_pred, 0
) + (1 - alpha) * np.maximum(y_pred - y_true, 0)
y_quantile = np.percentile(y_true, q=alpha * 100)
assert_almost_equal(
d2_pinball_score(y_true, y_pred, alpha=alpha),
1
- pinball_loss(y_true, y_pred, alpha).sum()
/ pinball_loss(y_true, y_quantile, alpha).sum(),
)
assert_almost_equal(
d2_absolute_error_score(y_true, y_pred),
d2_pinball_score(y_true, y_pred, alpha=0.5),
)
# Tweedie deviance needs positive y_pred, except for p=0,
# p>=2 needs positive y_true
# results evaluated by sympy
y_true = np.arange(1, 1 + n_samples)
y_pred = 2 * y_true
n = n_samples
assert_almost_equal(
mean_tweedie_deviance(y_true, y_pred, power=-1),
5 / 12 * n * (n**2 + 2 * n + 1),
)
assert_almost_equal(
mean_tweedie_deviance(y_true, y_pred, power=1), (n + 1) * (1 - np.log(2))
)
assert_almost_equal(
mean_tweedie_deviance(y_true, y_pred, power=2), 2 * np.log(2) - 1
)
assert_almost_equal(
mean_tweedie_deviance(y_true, y_pred, power=3 / 2),
((6 * np.sqrt(2) - 8) / n) * np.sqrt(y_true).sum(),
)
assert_almost_equal(
mean_tweedie_deviance(y_true, y_pred, power=3), np.sum(1 / y_true) / (4 * n)
)
dev_mean = 2 * np.mean(xlogy(y_true, 2 * y_true / (n + 1)))
assert_almost_equal(
d2_tweedie_score(y_true, y_pred, power=1),
1 - (n + 1) * (1 - np.log(2)) / dev_mean,
)
dev_mean = 2 * np.log((n + 1) / 2) - 2 / n * np.log(factorial(n))
assert_almost_equal(
d2_tweedie_score(y_true, y_pred, power=2), 1 - (2 * np.log(2) - 1) / dev_mean
)
def test_mean_squared_error_multioutput_raw_value_squared():
# non-regression test for
# https://github.com/scikit-learn/scikit-learn/pull/16323
mse1 = mean_squared_error([[1]], [[10]], multioutput="raw_values", squared=True)
mse2 = mean_squared_error([[1]], [[10]], multioutput="raw_values", squared=False)
assert np.sqrt(mse1) == pytest.approx(mse2)
def test_multioutput_regression():
y_true = np.array([[1, 0, 0, 1], [0, 1, 1, 1], [1, 1, 0, 1]])
y_pred = np.array([[0, 0, 0, 1], [1, 0, 1, 1], [0, 0, 0, 1]])
error = mean_squared_error(y_true, y_pred)
assert_almost_equal(error, (1.0 / 3 + 2.0 / 3 + 2.0 / 3) / 4.0)
error = mean_squared_error(y_true, y_pred, squared=False)
assert_almost_equal(error, 0.454, decimal=2)
error = mean_squared_log_error(y_true, y_pred)
assert_almost_equal(error, 0.200, decimal=2)
# mean_absolute_error and mean_squared_error are equal because
# it is a binary problem.
error = mean_absolute_error(y_true, y_pred)
assert_almost_equal(error, (1.0 + 2.0 / 3) / 4.0)
error = mean_pinball_loss(y_true, y_pred)
assert_almost_equal(error, (1.0 + 2.0 / 3) / 8.0)
error = np.around(mean_absolute_percentage_error(y_true, y_pred), decimals=2)
assert np.isfinite(error)
assert error > 1e6
error = median_absolute_error(y_true, y_pred)
assert_almost_equal(error, (1.0 + 1.0) / 4.0)
error = r2_score(y_true, y_pred, multioutput="variance_weighted")
assert_almost_equal(error, 1.0 - 5.0 / 2)
error = r2_score(y_true, y_pred, multioutput="uniform_average")
assert_almost_equal(error, -0.875)
score = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput="raw_values")
raw_expected_score = [
1
- np.abs(y_true[:, i] - y_pred[:, i]).sum()
/ np.abs(y_true[:, i] - np.median(y_true[:, i])).sum()
for i in range(y_true.shape[1])
]
# in the last case, the denominator vanishes and hence we get nan,
# but since the numerator vanishes as well the expected score is 1.0
raw_expected_score = np.where(np.isnan(raw_expected_score), 1, raw_expected_score)
assert_array_almost_equal(score, raw_expected_score)
score = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput="uniform_average")
assert_almost_equal(score, raw_expected_score.mean())
# constant `y_true` with force_finite=True leads to 1. or 0.
yc = [5.0, 5.0]
error = r2_score(yc, [5.0, 5.0], multioutput="variance_weighted")
assert_almost_equal(error, 1.0)
error = r2_score(yc, [5.0, 5.1], multioutput="variance_weighted")
assert_almost_equal(error, 0.0)
# Setting force_finite=False results in the nan for 4th output propagating
error = r2_score(
y_true, y_pred, multioutput="variance_weighted", force_finite=False
)
assert_almost_equal(error, np.nan)
error = r2_score(y_true, y_pred, multioutput="uniform_average", force_finite=False)
assert_almost_equal(error, np.nan)
# Dropping the 4th output to check `force_finite=False` for nominal
y_true = y_true[:, :-1]
y_pred = y_pred[:, :-1]
error = r2_score(y_true, y_pred, multioutput="variance_weighted")
error2 = r2_score(
y_true, y_pred, multioutput="variance_weighted", force_finite=False
)
assert_almost_equal(error, error2)
error = r2_score(y_true, y_pred, multioutput="uniform_average")
error2 = r2_score(y_true, y_pred, multioutput="uniform_average", force_finite=False)
assert_almost_equal(error, error2)
# constant `y_true` with force_finite=False leads to NaN or -Inf.
error = r2_score(
yc, [5.0, 5.0], multioutput="variance_weighted", force_finite=False
)
assert_almost_equal(error, np.nan)
error = r2_score(
yc, [5.0, 6.0], multioutput="variance_weighted", force_finite=False
)
assert_almost_equal(error, -np.inf)
def test_regression_metrics_at_limits():
# Single-sample case
# Note: for r2 and d2_tweedie see also test_regression_single_sample
assert_almost_equal(mean_squared_error([0.0], [0.0]), 0.0)
assert_almost_equal(mean_squared_error([0.0], [0.0], squared=False), 0.0)
assert_almost_equal(mean_squared_log_error([0.0], [0.0]), 0.0)
assert_almost_equal(mean_absolute_error([0.0], [0.0]), 0.0)
assert_almost_equal(mean_pinball_loss([0.0], [0.0]), 0.0)
assert_almost_equal(mean_absolute_percentage_error([0.0], [0.0]), 0.0)
assert_almost_equal(median_absolute_error([0.0], [0.0]), 0.0)
assert_almost_equal(max_error([0.0], [0.0]), 0.0)
assert_almost_equal(explained_variance_score([0.0], [0.0]), 1.0)
# Perfect cases
assert_almost_equal(r2_score([0.0, 1], [0.0, 1]), 1.0)
assert_almost_equal(d2_pinball_score([0.0, 1], [0.0, 1]), 1.0)
# Non-finite cases
# R² and explained variance have a fix by default for non-finite cases
for s in (r2_score, explained_variance_score):
assert_almost_equal(s([0, 0], [1, -1]), 0.0)
assert_almost_equal(s([0, 0], [1, -1], force_finite=False), -np.inf)
assert_almost_equal(s([1, 1], [1, 1]), 1.0)
assert_almost_equal(s([1, 1], [1, 1], force_finite=False), np.nan)
msg = (
"Mean Squared Logarithmic Error cannot be used when targets "
"contain negative values."
)
with pytest.raises(ValueError, match=msg):
mean_squared_log_error([-1.0], [-1.0])
msg = (
"Mean Squared Logarithmic Error cannot be used when targets "
"contain negative values."
)
with pytest.raises(ValueError, match=msg):
mean_squared_log_error([1.0, 2.0, 3.0], [1.0, -2.0, 3.0])
msg = (
"Mean Squared Logarithmic Error cannot be used when targets "
"contain negative values."
)
with pytest.raises(ValueError, match=msg):
mean_squared_log_error([1.0, -2.0, 3.0], [1.0, 2.0, 3.0])
# Tweedie deviance error
power = -1.2
assert_allclose(
mean_tweedie_deviance([0], [1.0], power=power), 2 / (2 - power), rtol=1e-3
)
msg = "can only be used on strictly positive y_pred."
with pytest.raises(ValueError, match=msg):
mean_tweedie_deviance([0.0], [0.0], power=power)
with pytest.raises(ValueError, match=msg):
d2_tweedie_score([0.0] * 2, [0.0] * 2, power=power)
assert_almost_equal(mean_tweedie_deviance([0.0], [0.0], power=0), 0.0, 2)
power = 1.0
msg = "only be used on non-negative y and strictly positive y_pred."
with pytest.raises(ValueError, match=msg):
mean_tweedie_deviance([0.0], [0.0], power=power)
with pytest.raises(ValueError, match=msg):
d2_tweedie_score([0.0] * 2, [0.0] * 2, power=power)
power = 1.5
assert_allclose(mean_tweedie_deviance([0.0], [1.0], power=power), 2 / (2 - power))
msg = "only be used on non-negative y and strictly positive y_pred."
with pytest.raises(ValueError, match=msg):
mean_tweedie_deviance([0.0], [0.0], power=power)
with pytest.raises(ValueError, match=msg):
d2_tweedie_score([0.0] * 2, [0.0] * 2, power=power)
power = 2.0
assert_allclose(mean_tweedie_deviance([1.0], [1.0], power=power), 0.00, atol=1e-8)
msg = "can only be used on strictly positive y and y_pred."
with pytest.raises(ValueError, match=msg):
mean_tweedie_deviance([0.0], [0.0], power=power)
with pytest.raises(ValueError, match=msg):
d2_tweedie_score([0.0] * 2, [0.0] * 2, power=power)
power = 3.0
assert_allclose(mean_tweedie_deviance([1.0], [1.0], power=power), 0.00, atol=1e-8)
msg = "can only be used on strictly positive y and y_pred."
with pytest.raises(ValueError, match=msg):
mean_tweedie_deviance([0.0], [0.0], power=power)
with pytest.raises(ValueError, match=msg):
d2_tweedie_score([0.0] * 2, [0.0] * 2, power=power)
power = 0.5
with pytest.raises(ValueError, match="is only defined for power<=0 and power>=1"):
mean_tweedie_deviance([0.0], [0.0], power=power)
with pytest.raises(ValueError, match="is only defined for power<=0 and power>=1"):
d2_tweedie_score([0.0] * 2, [0.0] * 2, power=power)
def test__check_reg_targets():
# All of length 3
EXAMPLES = [
("continuous", [1, 2, 3], 1),
("continuous", [[1], [2], [3]], 1),
("continuous-multioutput", [[1, 1], [2, 2], [3, 1]], 2),
("continuous-multioutput", [[5, 1], [4, 2], [3, 1]], 2),
("continuous-multioutput", [[1, 3, 4], [2, 2, 2], [3, 1, 1]], 3),
]
for (type1, y1, n_out1), (type2, y2, n_out2) in product(EXAMPLES, repeat=2):
if type1 == type2 and n_out1 == n_out2:
y_type, y_check1, y_check2, multioutput = _check_reg_targets(y1, y2, None)
assert type1 == y_type
if type1 == "continuous":
assert_array_equal(y_check1, np.reshape(y1, (-1, 1)))
assert_array_equal(y_check2, np.reshape(y2, (-1, 1)))
else:
assert_array_equal(y_check1, y1)
assert_array_equal(y_check2, y2)
else:
with pytest.raises(ValueError):
_check_reg_targets(y1, y2, None)
def test__check_reg_targets_exception():
invalid_multioutput = "this_value_is_not_valid"
expected_message = (
"Allowed 'multioutput' string values are.+You provided multioutput={!r}".format(
invalid_multioutput
)
)
with pytest.raises(ValueError, match=expected_message):
_check_reg_targets([1, 2, 3], [[1], [2], [3]], invalid_multioutput)
def test_regression_multioutput_array():
y_true = [[1, 2], [2.5, -1], [4.5, 3], [5, 7]]
y_pred = [[1, 1], [2, -1], [5, 4], [5, 6.5]]
mse = mean_squared_error(y_true, y_pred, multioutput="raw_values")
mae = mean_absolute_error(y_true, y_pred, multioutput="raw_values")
err_msg = (
"multioutput is expected to be 'raw_values' "
"or 'uniform_average' but we got 'variance_weighted' instead."
)
with pytest.raises(ValueError, match=err_msg):
mean_pinball_loss(y_true, y_pred, multioutput="variance_weighted")
with pytest.raises(ValueError, match=err_msg):
d2_pinball_score(y_true, y_pred, multioutput="variance_weighted")
pbl = mean_pinball_loss(y_true, y_pred, multioutput="raw_values")
mape = mean_absolute_percentage_error(y_true, y_pred, multioutput="raw_values")
r = r2_score(y_true, y_pred, multioutput="raw_values")
evs = explained_variance_score(y_true, y_pred, multioutput="raw_values")
d2ps = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput="raw_values")
evs2 = explained_variance_score(
y_true, y_pred, multioutput="raw_values", force_finite=False
)
assert_array_almost_equal(mse, [0.125, 0.5625], decimal=2)
assert_array_almost_equal(mae, [0.25, 0.625], decimal=2)
assert_array_almost_equal(pbl, [0.25 / 2, 0.625 / 2], decimal=2)
assert_array_almost_equal(mape, [0.0778, 0.2262], decimal=2)
assert_array_almost_equal(r, [0.95, 0.93], decimal=2)
assert_array_almost_equal(evs, [0.95, 0.93], decimal=2)
assert_array_almost_equal(d2ps, [0.833, 0.722], decimal=2)
assert_array_almost_equal(evs2, [0.95, 0.93], decimal=2)
# mean_absolute_error and mean_squared_error are equal because
# it is a binary problem.
y_true = [[0, 0]] * 4
y_pred = [[1, 1]] * 4
mse = mean_squared_error(y_true, y_pred, multioutput="raw_values")
mae = mean_absolute_error(y_true, y_pred, multioutput="raw_values")
pbl = mean_pinball_loss(y_true, y_pred, multioutput="raw_values")
r = r2_score(y_true, y_pred, multioutput="raw_values")
d2ps = d2_pinball_score(y_true, y_pred, multioutput="raw_values")
assert_array_almost_equal(mse, [1.0, 1.0], decimal=2)
assert_array_almost_equal(mae, [1.0, 1.0], decimal=2)
assert_array_almost_equal(pbl, [0.5, 0.5], decimal=2)
assert_array_almost_equal(r, [0.0, 0.0], decimal=2)
assert_array_almost_equal(d2ps, [0.0, 0.0], decimal=2)
r = r2_score([[0, -1], [0, 1]], [[2, 2], [1, 1]], multioutput="raw_values")
assert_array_almost_equal(r, [0, -3.5], decimal=2)
assert np.mean(r) == r2_score(
[[0, -1], [0, 1]], [[2, 2], [1, 1]], multioutput="uniform_average"
)
evs = explained_variance_score(
[[0, -1], [0, 1]], [[2, 2], [1, 1]], multioutput="raw_values"
)
assert_array_almost_equal(evs, [0, -1.25], decimal=2)
evs2 = explained_variance_score(
[[0, -1], [0, 1]],
[[2, 2], [1, 1]],
multioutput="raw_values",
force_finite=False,
)
assert_array_almost_equal(evs2, [-np.inf, -1.25], decimal=2)
# Checking for the condition in which both numerator and denominator is
# zero.
y_true = [[1, 3], [1, 2]]
y_pred = [[1, 4], [1, 1]]
r2 = r2_score(y_true, y_pred, multioutput="raw_values")
assert_array_almost_equal(r2, [1.0, -3.0], decimal=2)
assert np.mean(r2) == r2_score(y_true, y_pred, multioutput="uniform_average")
r22 = r2_score(y_true, y_pred, multioutput="raw_values", force_finite=False)
assert_array_almost_equal(r22, [np.nan, -3.0], decimal=2)
assert_almost_equal(
np.mean(r22),
r2_score(y_true, y_pred, multioutput="uniform_average", force_finite=False),
)
evs = explained_variance_score(y_true, y_pred, multioutput="raw_values")
assert_array_almost_equal(evs, [1.0, -3.0], decimal=2)
assert np.mean(evs) == explained_variance_score(y_true, y_pred)
d2ps = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput="raw_values")
assert_array_almost_equal(d2ps, [1.0, -1.0], decimal=2)
evs2 = explained_variance_score(
y_true, y_pred, multioutput="raw_values", force_finite=False
)
assert_array_almost_equal(evs2, [np.nan, -3.0], decimal=2)
assert_almost_equal(
np.mean(evs2), explained_variance_score(y_true, y_pred, force_finite=False)
)
# Handling msle separately as it does not accept negative inputs.
y_true = np.array([[0.5, 1], [1, 2], [7, 6]])
y_pred = np.array([[0.5, 2], [1, 2.5], [8, 8]])
msle = mean_squared_log_error(y_true, y_pred, multioutput="raw_values")
msle2 = mean_squared_error(
np.log(1 + y_true), np.log(1 + y_pred), multioutput="raw_values"
)
assert_array_almost_equal(msle, msle2, decimal=2)
def test_regression_custom_weights():
y_true = [[1, 2], [2.5, -1], [4.5, 3], [5, 7]]
y_pred = [[1, 1], [2, -1], [5, 4], [5, 6.5]]
msew = mean_squared_error(y_true, y_pred, multioutput=[0.4, 0.6])
rmsew = mean_squared_error(y_true, y_pred, multioutput=[0.4, 0.6], squared=False)
maew = mean_absolute_error(y_true, y_pred, multioutput=[0.4, 0.6])
mapew = mean_absolute_percentage_error(y_true, y_pred, multioutput=[0.4, 0.6])
rw = r2_score(y_true, y_pred, multioutput=[0.4, 0.6])
evsw = explained_variance_score(y_true, y_pred, multioutput=[0.4, 0.6])
d2psw = d2_pinball_score(y_true, y_pred, alpha=0.5, multioutput=[0.4, 0.6])
evsw2 = explained_variance_score(
y_true, y_pred, multioutput=[0.4, 0.6], force_finite=False
)
assert_almost_equal(msew, 0.39, decimal=2)
assert_almost_equal(rmsew, 0.59, decimal=2)
assert_almost_equal(maew, 0.475, decimal=3)
assert_almost_equal(mapew, 0.1668, decimal=2)
assert_almost_equal(rw, 0.94, decimal=2)
assert_almost_equal(evsw, 0.94, decimal=2)
assert_almost_equal(d2psw, 0.766, decimal=2)
assert_almost_equal(evsw2, 0.94, decimal=2)
# Handling msle separately as it does not accept negative inputs.
y_true = np.array([[0.5, 1], [1, 2], [7, 6]])
y_pred = np.array([[0.5, 2], [1, 2.5], [8, 8]])
msle = mean_squared_log_error(y_true, y_pred, multioutput=[0.3, 0.7])
msle2 = mean_squared_error(
np.log(1 + y_true), np.log(1 + y_pred), multioutput=[0.3, 0.7]
)
assert_almost_equal(msle, msle2, decimal=2)
@pytest.mark.parametrize("metric", [r2_score, d2_tweedie_score, d2_pinball_score])
def test_regression_single_sample(metric):
y_true = [0]
y_pred = [1]
warning_msg = "not well-defined with less than two samples."
# Trigger the warning
with pytest.warns(UndefinedMetricWarning, match=warning_msg):
score = metric(y_true, y_pred)
assert np.isnan(score)
def test_tweedie_deviance_continuity():
n_samples = 100
y_true = np.random.RandomState(0).rand(n_samples) + 0.1
y_pred = np.random.RandomState(1).rand(n_samples) + 0.1
assert_allclose(
mean_tweedie_deviance(y_true, y_pred, power=0 - 1e-10),
mean_tweedie_deviance(y_true, y_pred, power=0),
)
# Ws we get closer to the limit, with 1e-12 difference the absolute
# tolerance to pass the below check increases. There are likely
# numerical precision issues on the edges of different definition
# regions.
assert_allclose(
mean_tweedie_deviance(y_true, y_pred, power=1 + 1e-10),
mean_tweedie_deviance(y_true, y_pred, power=1),
atol=1e-6,
)
assert_allclose(
mean_tweedie_deviance(y_true, y_pred, power=2 - 1e-10),
mean_tweedie_deviance(y_true, y_pred, power=2),
atol=1e-6,
)
assert_allclose(
mean_tweedie_deviance(y_true, y_pred, power=2 + 1e-10),
mean_tweedie_deviance(y_true, y_pred, power=2),
atol=1e-6,
)
def test_mean_absolute_percentage_error():
random_number_generator = np.random.RandomState(42)
y_true = random_number_generator.exponential(size=100)
y_pred = 1.2 * y_true
assert mean_absolute_percentage_error(y_true, y_pred) == pytest.approx(0.2)
@pytest.mark.parametrize(
"distribution", ["normal", "lognormal", "exponential", "uniform"]
)
@pytest.mark.parametrize("target_quantile", [0.05, 0.5, 0.75])
def test_mean_pinball_loss_on_constant_predictions(distribution, target_quantile):
if not hasattr(np, "quantile"):
pytest.skip(
"This test requires a more recent version of numpy "
"with support for np.quantile."
)
# Check that the pinball loss is minimized by the empirical quantile.
n_samples = 3000
rng = np.random.RandomState(42)
data = getattr(rng, distribution)(size=n_samples)
# Compute the best possible pinball loss for any constant predictor:
best_pred = np.quantile(data, target_quantile)
best_constant_pred = np.full(n_samples, fill_value=best_pred)
best_pbl = mean_pinball_loss(data, best_constant_pred, alpha=target_quantile)
# Evaluate the loss on a grid of quantiles
candidate_predictions = np.quantile(data, np.linspace(0, 1, 100))
for pred in candidate_predictions:
# Compute the pinball loss of a constant predictor:
constant_pred = np.full(n_samples, fill_value=pred)
pbl = mean_pinball_loss(data, constant_pred, alpha=target_quantile)
# Check that the loss of this constant predictor is greater or equal
# than the loss of using the optimal quantile (up to machine
# precision):
assert pbl >= best_pbl - np.finfo(best_pbl.dtype).eps
# Check that the value of the pinball loss matches the analytical
# formula.
expected_pbl = (pred - data[data < pred]).sum() * (1 - target_quantile) + (
data[data >= pred] - pred
).sum() * target_quantile
expected_pbl /= n_samples
assert_almost_equal(expected_pbl, pbl)
# Check that we can actually recover the target_quantile by minimizing the
# pinball loss w.r.t. the constant prediction quantile.
def objective_func(x):
constant_pred = np.full(n_samples, fill_value=x)
return mean_pinball_loss(data, constant_pred, alpha=target_quantile)
result = optimize.minimize(objective_func, data.mean(), method="Nelder-Mead")
assert result.success
# The minimum is not unique with limited data, hence the large tolerance.
assert result.x == pytest.approx(best_pred, rel=1e-2)
assert result.fun == pytest.approx(best_pbl)
def test_dummy_quantile_parameter_tuning():
# Integration test to check that it is possible to use the pinball loss to
# tune the hyperparameter of a quantile regressor. This is conceptually
# similar to the previous test but using the scikit-learn estimator and
# scoring API instead.
n_samples = 1000
rng = np.random.RandomState(0)
X = rng.normal(size=(n_samples, 5)) # Ignored
y = rng.exponential(size=n_samples)
all_quantiles = [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95]
for alpha in all_quantiles:
neg_mean_pinball_loss = make_scorer(
mean_pinball_loss,
alpha=alpha,
greater_is_better=False,
)
regressor = DummyRegressor(strategy="quantile", quantile=0.25)
grid_search = GridSearchCV(
regressor,
param_grid=dict(quantile=all_quantiles),
scoring=neg_mean_pinball_loss,
).fit(X, y)
assert grid_search.best_params_["quantile"] == pytest.approx(alpha)