finalizare 1.0
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
from .validation import check_random_state
|
||||
|
||||
|
||||
def _init_arpack_v0(size, random_state):
|
||||
"""Initialize the starting vector for iteration in ARPACK functions.
|
||||
|
||||
Initialize a ndarray with values sampled from the uniform distribution on
|
||||
[-1, 1]. This initialization model has been chosen to be consistent with
|
||||
the ARPACK one as another initialization can lead to convergence issues.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size : int
|
||||
The size of the eigenvalue vector to be initialized.
|
||||
|
||||
random_state : int, RandomState instance or None, default=None
|
||||
The seed of the pseudo random number generator used to generate a
|
||||
uniform distribution. If int, random_state is the seed used by the
|
||||
random number generator; If RandomState instance, random_state is the
|
||||
random number generator; If None, the random number generator is the
|
||||
RandomState instance used by `np.random`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
v0 : ndarray of shape (size,)
|
||||
The initialized vector.
|
||||
"""
|
||||
random_state = check_random_state(random_state)
|
||||
v0 = random_state.uniform(-1, 1, size)
|
||||
return v0
|
||||
@@ -0,0 +1,575 @@
|
||||
"""Tools to support array_api."""
|
||||
import itertools
|
||||
import math
|
||||
from functools import wraps
|
||||
|
||||
import numpy
|
||||
import scipy.special as special
|
||||
|
||||
from .._config import get_config
|
||||
from .fixes import parse_version
|
||||
|
||||
|
||||
def yield_namespace_device_dtype_combinations():
|
||||
"""Yield supported namespace, device, dtype tuples for testing.
|
||||
|
||||
Use this to test that an estimator works with all combinations.
|
||||
|
||||
Returns
|
||||
-------
|
||||
array_namespace : str
|
||||
The name of the Array API namespace.
|
||||
|
||||
device : str
|
||||
The name of the device on which to allocate the arrays. Can be None to
|
||||
indicate that the default value should be used.
|
||||
|
||||
dtype_name : str
|
||||
The name of the data type to use for arrays. Can be None to indicate
|
||||
that the default value should be used.
|
||||
"""
|
||||
for array_namespace in [
|
||||
# The following is used to test the array_api_compat wrapper when
|
||||
# array_api_dispatch is enabled: in particular, the arrays used in the
|
||||
# tests are regular numpy arrays without any "device" attribute.
|
||||
"numpy",
|
||||
# Stricter NumPy-based Array API implementation. The
|
||||
# numpy.array_api.Array instances always a dummy "device" attribute.
|
||||
"numpy.array_api",
|
||||
"cupy",
|
||||
"cupy.array_api",
|
||||
"torch",
|
||||
]:
|
||||
if array_namespace == "torch":
|
||||
for device, dtype in itertools.product(
|
||||
("cpu", "cuda"), ("float64", "float32")
|
||||
):
|
||||
yield array_namespace, device, dtype
|
||||
yield array_namespace, "mps", "float32"
|
||||
else:
|
||||
yield array_namespace, None, None
|
||||
|
||||
|
||||
def _check_array_api_dispatch(array_api_dispatch):
|
||||
"""Check that array_api_compat is installed and NumPy version is compatible.
|
||||
|
||||
array_api_compat follows NEP29, which has a higher minimum NumPy version than
|
||||
scikit-learn.
|
||||
"""
|
||||
if array_api_dispatch:
|
||||
try:
|
||||
import array_api_compat # noqa
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"array_api_compat is required to dispatch arrays using the API"
|
||||
" specification"
|
||||
)
|
||||
|
||||
numpy_version = parse_version(numpy.__version__)
|
||||
min_numpy_version = "1.21"
|
||||
if numpy_version < parse_version(min_numpy_version):
|
||||
raise ImportError(
|
||||
f"NumPy must be {min_numpy_version} or newer to dispatch array using"
|
||||
" the API specification"
|
||||
)
|
||||
|
||||
|
||||
def device(x):
|
||||
"""Hardware device the array data resides on.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : array
|
||||
Array instance from NumPy or an array API compatible library.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : device
|
||||
`device` object (see the "Device Support" section of the array API spec).
|
||||
"""
|
||||
if isinstance(x, (numpy.ndarray, numpy.generic)):
|
||||
return "cpu"
|
||||
return x.device
|
||||
|
||||
|
||||
def size(x):
|
||||
"""Return the total number of elements of x.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : array
|
||||
Array instance from NumPy or an array API compatible library.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : int
|
||||
Total number of elements.
|
||||
"""
|
||||
return math.prod(x.shape)
|
||||
|
||||
|
||||
def _is_numpy_namespace(xp):
|
||||
"""Return True if xp is backed by NumPy."""
|
||||
return xp.__name__ in {"numpy", "array_api_compat.numpy", "numpy.array_api"}
|
||||
|
||||
|
||||
def _union1d(a, b, xp):
|
||||
if _is_numpy_namespace(xp):
|
||||
return xp.asarray(numpy.union1d(a, b))
|
||||
assert a.ndim == b.ndim == 1
|
||||
return xp.unique_values(xp.concat([xp.unique_values(a), xp.unique_values(b)]))
|
||||
|
||||
|
||||
def isdtype(dtype, kind, *, xp):
|
||||
"""Returns a boolean indicating whether a provided dtype is of type "kind".
|
||||
|
||||
Included in the v2022.12 of the Array API spec.
|
||||
https://data-apis.org/array-api/latest/API_specification/generated/array_api.isdtype.html
|
||||
"""
|
||||
if isinstance(kind, tuple):
|
||||
return any(_isdtype_single(dtype, k, xp=xp) for k in kind)
|
||||
else:
|
||||
return _isdtype_single(dtype, kind, xp=xp)
|
||||
|
||||
|
||||
def _isdtype_single(dtype, kind, *, xp):
|
||||
if isinstance(kind, str):
|
||||
if kind == "bool":
|
||||
return dtype == xp.bool
|
||||
elif kind == "signed integer":
|
||||
return dtype in {xp.int8, xp.int16, xp.int32, xp.int64}
|
||||
elif kind == "unsigned integer":
|
||||
return dtype in {xp.uint8, xp.uint16, xp.uint32, xp.uint64}
|
||||
elif kind == "integral":
|
||||
return any(
|
||||
_isdtype_single(dtype, k, xp=xp)
|
||||
for k in ("signed integer", "unsigned integer")
|
||||
)
|
||||
elif kind == "real floating":
|
||||
return dtype in supported_float_dtypes(xp)
|
||||
elif kind == "complex floating":
|
||||
# Some name spaces do not have complex, such as cupy.array_api
|
||||
# and numpy.array_api
|
||||
complex_dtypes = set()
|
||||
if hasattr(xp, "complex64"):
|
||||
complex_dtypes.add(xp.complex64)
|
||||
if hasattr(xp, "complex128"):
|
||||
complex_dtypes.add(xp.complex128)
|
||||
return dtype in complex_dtypes
|
||||
elif kind == "numeric":
|
||||
return any(
|
||||
_isdtype_single(dtype, k, xp=xp)
|
||||
for k in ("integral", "real floating", "complex floating")
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unrecognized data type kind: {kind!r}")
|
||||
else:
|
||||
return dtype == kind
|
||||
|
||||
|
||||
def supported_float_dtypes(xp):
|
||||
"""Supported floating point types for the namespace
|
||||
|
||||
Note: float16 is not officially part of the Array API spec at the
|
||||
time of writing but scikit-learn estimators and functions can choose
|
||||
to accept it when xp.float16 is defined.
|
||||
|
||||
https://data-apis.org/array-api/latest/API_specification/data_types.html
|
||||
"""
|
||||
if hasattr(xp, "float16"):
|
||||
return (xp.float64, xp.float32, xp.float16)
|
||||
else:
|
||||
return (xp.float64, xp.float32)
|
||||
|
||||
|
||||
class _ArrayAPIWrapper:
|
||||
"""sklearn specific Array API compatibility wrapper
|
||||
|
||||
This wrapper makes it possible for scikit-learn maintainers to
|
||||
deal with discrepancies between different implementations of the
|
||||
Python Array API standard and its evolution over time.
|
||||
|
||||
The Python Array API standard specification:
|
||||
https://data-apis.org/array-api/latest/
|
||||
|
||||
Documentation of the NumPy implementation:
|
||||
https://numpy.org/neps/nep-0047-array-api-standard.html
|
||||
"""
|
||||
|
||||
def __init__(self, array_namespace):
|
||||
self._namespace = array_namespace
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._namespace, name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._namespace == other._namespace
|
||||
|
||||
def isdtype(self, dtype, kind):
|
||||
return isdtype(dtype, kind, xp=self._namespace)
|
||||
|
||||
|
||||
def _check_device_cpu(device): # noqa
|
||||
if device not in {"cpu", None}:
|
||||
raise ValueError(f"Unsupported device for NumPy: {device!r}")
|
||||
|
||||
|
||||
def _accept_device_cpu(func):
|
||||
@wraps(func)
|
||||
def wrapped_func(*args, **kwargs):
|
||||
_check_device_cpu(kwargs.pop("device", None))
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped_func
|
||||
|
||||
|
||||
class _NumPyAPIWrapper:
|
||||
"""Array API compat wrapper for any numpy version
|
||||
|
||||
NumPy < 1.22 does not expose the numpy.array_api namespace. This
|
||||
wrapper makes it possible to write code that uses the standard
|
||||
Array API while working with any version of NumPy supported by
|
||||
scikit-learn.
|
||||
|
||||
See the `get_namespace()` public function for more details.
|
||||
"""
|
||||
|
||||
# Creation functions in spec:
|
||||
# https://data-apis.org/array-api/latest/API_specification/creation_functions.html
|
||||
_CREATION_FUNCS = {
|
||||
"arange",
|
||||
"empty",
|
||||
"empty_like",
|
||||
"eye",
|
||||
"full",
|
||||
"full_like",
|
||||
"linspace",
|
||||
"ones",
|
||||
"ones_like",
|
||||
"zeros",
|
||||
"zeros_like",
|
||||
}
|
||||
# Data types in spec
|
||||
# https://data-apis.org/array-api/latest/API_specification/data_types.html
|
||||
_DTYPES = {
|
||||
"int8",
|
||||
"int16",
|
||||
"int32",
|
||||
"int64",
|
||||
"uint8",
|
||||
"uint16",
|
||||
"uint32",
|
||||
"uint64",
|
||||
# XXX: float16 is not part of the Array API spec but exposed by
|
||||
# some namespaces.
|
||||
"float16",
|
||||
"float32",
|
||||
"float64",
|
||||
"complex64",
|
||||
"complex128",
|
||||
}
|
||||
|
||||
def __getattr__(self, name):
|
||||
attr = getattr(numpy, name)
|
||||
|
||||
# Support device kwargs and make sure they are on the CPU
|
||||
if name in self._CREATION_FUNCS:
|
||||
return _accept_device_cpu(attr)
|
||||
|
||||
# Convert to dtype objects
|
||||
if name in self._DTYPES:
|
||||
return numpy.dtype(attr)
|
||||
return attr
|
||||
|
||||
@property
|
||||
def bool(self):
|
||||
return numpy.bool_
|
||||
|
||||
def astype(self, x, dtype, *, copy=True, casting="unsafe"):
|
||||
# astype is not defined in the top level NumPy namespace
|
||||
return x.astype(dtype, copy=copy, casting=casting)
|
||||
|
||||
def asarray(self, x, *, dtype=None, device=None, copy=None): # noqa
|
||||
_check_device_cpu(device)
|
||||
# Support copy in NumPy namespace
|
||||
if copy is True:
|
||||
return numpy.array(x, copy=True, dtype=dtype)
|
||||
else:
|
||||
return numpy.asarray(x, dtype=dtype)
|
||||
|
||||
def unique_inverse(self, x):
|
||||
return numpy.unique(x, return_inverse=True)
|
||||
|
||||
def unique_counts(self, x):
|
||||
return numpy.unique(x, return_counts=True)
|
||||
|
||||
def unique_values(self, x):
|
||||
return numpy.unique(x)
|
||||
|
||||
def concat(self, arrays, *, axis=None):
|
||||
return numpy.concatenate(arrays, axis=axis)
|
||||
|
||||
def reshape(self, x, shape, *, copy=None):
|
||||
"""Gives a new shape to an array without changing its data.
|
||||
|
||||
The Array API specification requires shape to be a tuple.
|
||||
https://data-apis.org/array-api/latest/API_specification/generated/array_api.reshape.html
|
||||
"""
|
||||
if not isinstance(shape, tuple):
|
||||
raise TypeError(
|
||||
f"shape must be a tuple, got {shape!r} of type {type(shape)}"
|
||||
)
|
||||
|
||||
if copy is True:
|
||||
x = x.copy()
|
||||
return numpy.reshape(x, shape)
|
||||
|
||||
def isdtype(self, dtype, kind):
|
||||
return isdtype(dtype, kind, xp=self)
|
||||
|
||||
|
||||
_NUMPY_API_WRAPPER_INSTANCE = _NumPyAPIWrapper()
|
||||
|
||||
|
||||
def get_namespace(*arrays):
|
||||
"""Get namespace of arrays.
|
||||
|
||||
Introspect `arrays` arguments and return their common Array API
|
||||
compatible namespace object, if any. NumPy 1.22 and later can
|
||||
construct such containers using the `numpy.array_api` namespace
|
||||
for instance.
|
||||
|
||||
See: https://numpy.org/neps/nep-0047-array-api-standard.html
|
||||
|
||||
If `arrays` are regular numpy arrays, an instance of the
|
||||
`_NumPyAPIWrapper` compatibility wrapper is returned instead.
|
||||
|
||||
Namespace support is not enabled by default. To enabled it
|
||||
call:
|
||||
|
||||
sklearn.set_config(array_api_dispatch=True)
|
||||
|
||||
or:
|
||||
|
||||
with sklearn.config_context(array_api_dispatch=True):
|
||||
# your code here
|
||||
|
||||
Otherwise an instance of the `_NumPyAPIWrapper`
|
||||
compatibility wrapper is always returned irrespective of
|
||||
the fact that arrays implement the `__array_namespace__`
|
||||
protocol or not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*arrays : array objects
|
||||
Array objects.
|
||||
|
||||
Returns
|
||||
-------
|
||||
namespace : module
|
||||
Namespace shared by array objects. If any of the `arrays` are not arrays,
|
||||
the namespace defaults to NumPy.
|
||||
|
||||
is_array_api_compliant : bool
|
||||
True if the arrays are containers that implement the Array API spec.
|
||||
Always False when array_api_dispatch=False.
|
||||
"""
|
||||
array_api_dispatch = get_config()["array_api_dispatch"]
|
||||
if not array_api_dispatch:
|
||||
return _NUMPY_API_WRAPPER_INSTANCE, False
|
||||
|
||||
_check_array_api_dispatch(array_api_dispatch)
|
||||
|
||||
# array-api-compat is a required dependency of scikit-learn only when
|
||||
# configuring `array_api_dispatch=True`. Its import should therefore be
|
||||
# protected by _check_array_api_dispatch to display an informative error
|
||||
# message in case it is missing.
|
||||
import array_api_compat
|
||||
|
||||
namespace, is_array_api_compliant = array_api_compat.get_namespace(*arrays), True
|
||||
|
||||
# These namespaces need additional wrapping to smooth out small differences
|
||||
# between implementations
|
||||
if namespace.__name__ in {"numpy.array_api", "cupy.array_api"}:
|
||||
namespace = _ArrayAPIWrapper(namespace)
|
||||
|
||||
return namespace, is_array_api_compliant
|
||||
|
||||
|
||||
def _expit(X):
|
||||
xp, _ = get_namespace(X)
|
||||
if _is_numpy_namespace(xp):
|
||||
return xp.asarray(special.expit(numpy.asarray(X)))
|
||||
|
||||
return 1.0 / (1.0 + xp.exp(-X))
|
||||
|
||||
|
||||
def _add_to_diagonal(array, value, xp):
|
||||
# Workaround for the lack of support for xp.reshape(a, shape, copy=False) in
|
||||
# numpy.array_api: https://github.com/numpy/numpy/issues/23410
|
||||
value = xp.asarray(value, dtype=array.dtype)
|
||||
if _is_numpy_namespace(xp):
|
||||
array_np = numpy.asarray(array)
|
||||
array_np.flat[:: array.shape[0] + 1] += value
|
||||
return xp.asarray(array_np)
|
||||
elif value.ndim == 1:
|
||||
for i in range(array.shape[0]):
|
||||
array[i, i] += value[i]
|
||||
else:
|
||||
# scalar value
|
||||
for i in range(array.shape[0]):
|
||||
array[i, i] += value
|
||||
|
||||
|
||||
def _weighted_sum(sample_score, sample_weight, normalize=False, xp=None):
|
||||
# XXX: this function accepts Array API input but returns a Python scalar
|
||||
# float. The call to float() is convenient because it removes the need to
|
||||
# move back results from device to host memory (e.g. calling `.cpu()` on a
|
||||
# torch tensor). However, this might interact in unexpected ways (break?)
|
||||
# with lazy Array API implementations. See:
|
||||
# https://github.com/data-apis/array-api/issues/642
|
||||
if xp is None:
|
||||
xp, _ = get_namespace(sample_score)
|
||||
if normalize and _is_numpy_namespace(xp):
|
||||
sample_score_np = numpy.asarray(sample_score)
|
||||
if sample_weight is not None:
|
||||
sample_weight_np = numpy.asarray(sample_weight)
|
||||
else:
|
||||
sample_weight_np = None
|
||||
return float(numpy.average(sample_score_np, weights=sample_weight_np))
|
||||
|
||||
if not xp.isdtype(sample_score.dtype, "real floating"):
|
||||
# We move to cpu device ahead of time since certain devices may not support
|
||||
# float64, but we want the same precision for all devices and namespaces.
|
||||
sample_score = xp.astype(xp.asarray(sample_score, device="cpu"), xp.float64)
|
||||
|
||||
if sample_weight is not None:
|
||||
sample_weight = xp.asarray(
|
||||
sample_weight, dtype=sample_score.dtype, device=device(sample_score)
|
||||
)
|
||||
if not xp.isdtype(sample_weight.dtype, "real floating"):
|
||||
sample_weight = xp.astype(sample_weight, xp.float64)
|
||||
|
||||
if normalize:
|
||||
if sample_weight is not None:
|
||||
scale = xp.sum(sample_weight)
|
||||
else:
|
||||
scale = sample_score.shape[0]
|
||||
if scale != 0:
|
||||
sample_score = sample_score / scale
|
||||
|
||||
if sample_weight is not None:
|
||||
return float(sample_score @ sample_weight)
|
||||
else:
|
||||
return float(xp.sum(sample_score))
|
||||
|
||||
|
||||
def _nanmin(X, axis=None):
|
||||
# TODO: refactor once nan-aware reductions are standardized:
|
||||
# https://github.com/data-apis/array-api/issues/621
|
||||
xp, _ = get_namespace(X)
|
||||
if _is_numpy_namespace(xp):
|
||||
return xp.asarray(numpy.nanmin(X, axis=axis))
|
||||
|
||||
else:
|
||||
mask = xp.isnan(X)
|
||||
X = xp.min(xp.where(mask, xp.asarray(+xp.inf, device=device(X)), X), axis=axis)
|
||||
# Replace Infs from all NaN slices with NaN again
|
||||
mask = xp.all(mask, axis=axis)
|
||||
if xp.any(mask):
|
||||
X = xp.where(mask, xp.asarray(xp.nan), X)
|
||||
return X
|
||||
|
||||
|
||||
def _nanmax(X, axis=None):
|
||||
# TODO: refactor once nan-aware reductions are standardized:
|
||||
# https://github.com/data-apis/array-api/issues/621
|
||||
xp, _ = get_namespace(X)
|
||||
if _is_numpy_namespace(xp):
|
||||
return xp.asarray(numpy.nanmax(X, axis=axis))
|
||||
|
||||
else:
|
||||
mask = xp.isnan(X)
|
||||
X = xp.max(xp.where(mask, xp.asarray(-xp.inf, device=device(X)), X), axis=axis)
|
||||
# Replace Infs from all NaN slices with NaN again
|
||||
mask = xp.all(mask, axis=axis)
|
||||
if xp.any(mask):
|
||||
X = xp.where(mask, xp.asarray(xp.nan), X)
|
||||
return X
|
||||
|
||||
|
||||
def _asarray_with_order(array, dtype=None, order=None, copy=None, *, xp=None):
|
||||
"""Helper to support the order kwarg only for NumPy-backed arrays
|
||||
|
||||
Memory layout parameter `order` is not exposed in the Array API standard,
|
||||
however some input validation code in scikit-learn needs to work both
|
||||
for classes and functions that will leverage Array API only operations
|
||||
and for code that inherently relies on NumPy backed data containers with
|
||||
specific memory layout constraints (e.g. our own Cython code). The
|
||||
purpose of this helper is to make it possible to share code for data
|
||||
container validation without memory copies for both downstream use cases:
|
||||
the `order` parameter is only enforced if the input array implementation
|
||||
is NumPy based, otherwise `order` is just silently ignored.
|
||||
"""
|
||||
if xp is None:
|
||||
xp, _ = get_namespace(array)
|
||||
if _is_numpy_namespace(xp):
|
||||
# Use NumPy API to support order
|
||||
if copy is True:
|
||||
array = numpy.array(array, order=order, dtype=dtype)
|
||||
else:
|
||||
array = numpy.asarray(array, order=order, dtype=dtype)
|
||||
|
||||
# At this point array is a NumPy ndarray. We convert it to an array
|
||||
# container that is consistent with the input's namespace.
|
||||
return xp.asarray(array)
|
||||
else:
|
||||
return xp.asarray(array, dtype=dtype, copy=copy)
|
||||
|
||||
|
||||
def _convert_to_numpy(array, xp):
|
||||
"""Convert X into a NumPy ndarray on the CPU."""
|
||||
xp_name = xp.__name__
|
||||
|
||||
if xp_name in {"array_api_compat.torch", "torch"}:
|
||||
return array.cpu().numpy()
|
||||
elif xp_name == "cupy.array_api":
|
||||
return array._array.get()
|
||||
elif xp_name in {"array_api_compat.cupy", "cupy"}: # pragma: nocover
|
||||
return array.get()
|
||||
|
||||
return numpy.asarray(array)
|
||||
|
||||
|
||||
def _estimator_with_converted_arrays(estimator, converter):
|
||||
"""Create new estimator which converting all attributes that are arrays.
|
||||
|
||||
The converter is called on all NumPy arrays and arrays that support the
|
||||
`DLPack interface <https://dmlc.github.io/dlpack/latest/>`__.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : Estimator
|
||||
Estimator to convert
|
||||
|
||||
converter : callable
|
||||
Callable that takes an array attribute and returns the converted array.
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_estimator : Estimator
|
||||
Convert estimator
|
||||
"""
|
||||
from sklearn.base import clone
|
||||
|
||||
new_estimator = clone(estimator)
|
||||
for key, attribute in vars(estimator).items():
|
||||
if hasattr(attribute, "__dlpack__") or isinstance(attribute, numpy.ndarray):
|
||||
attribute = converter(attribute)
|
||||
setattr(new_estimator, key, attribute)
|
||||
return new_estimator
|
||||
|
||||
|
||||
def _atol_for_type(dtype):
|
||||
"""Return the absolute tolerance for a given dtype."""
|
||||
return numpy.finfo(dtype).eps * 100
|
||||
@@ -0,0 +1,93 @@
|
||||
from functools import update_wrapper, wraps
|
||||
from types import MethodType
|
||||
|
||||
|
||||
class _AvailableIfDescriptor:
|
||||
"""Implements a conditional property using the descriptor protocol.
|
||||
|
||||
Using this class to create a decorator will raise an ``AttributeError``
|
||||
if check(self) returns a falsey value. Note that if check raises an error
|
||||
this will also result in hasattr returning false.
|
||||
|
||||
See https://docs.python.org/3/howto/descriptor.html for an explanation of
|
||||
descriptors.
|
||||
"""
|
||||
|
||||
def __init__(self, fn, check, attribute_name):
|
||||
self.fn = fn
|
||||
self.check = check
|
||||
self.attribute_name = attribute_name
|
||||
|
||||
# update the docstring of the descriptor
|
||||
update_wrapper(self, fn)
|
||||
|
||||
def _check(self, obj, owner):
|
||||
attr_err_msg = (
|
||||
f"This {repr(owner.__name__)} has no attribute {repr(self.attribute_name)}"
|
||||
)
|
||||
try:
|
||||
check_result = self.check(obj)
|
||||
except Exception as e:
|
||||
raise AttributeError(attr_err_msg) from e
|
||||
|
||||
if not check_result:
|
||||
raise AttributeError(attr_err_msg)
|
||||
|
||||
def __get__(self, obj, owner=None):
|
||||
if obj is not None:
|
||||
# delegate only on instances, not the classes.
|
||||
# this is to allow access to the docstrings.
|
||||
self._check(obj, owner=owner)
|
||||
out = MethodType(self.fn, obj)
|
||||
|
||||
else:
|
||||
# This makes it possible to use the decorated method as an unbound method,
|
||||
# for instance when monkeypatching.
|
||||
@wraps(self.fn)
|
||||
def out(*args, **kwargs):
|
||||
self._check(args[0], owner=owner)
|
||||
return self.fn(*args, **kwargs)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def available_if(check):
|
||||
"""An attribute that is available only if check returns a truthy value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
check : callable
|
||||
When passed the object with the decorated method, this should return
|
||||
a truthy value if the attribute is available, and either return False
|
||||
or raise an AttributeError if not available.
|
||||
|
||||
Returns
|
||||
-------
|
||||
callable
|
||||
Callable makes the decorated method available if `check` returns
|
||||
a truthy value, otherwise the decorated method is unavailable.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils.metaestimators import available_if
|
||||
>>> class HelloIfEven:
|
||||
... def __init__(self, x):
|
||||
... self.x = x
|
||||
...
|
||||
... def _x_is_even(self):
|
||||
... return self.x % 2 == 0
|
||||
...
|
||||
... @available_if(_x_is_even)
|
||||
... def say_hello(self):
|
||||
... print("Hello")
|
||||
...
|
||||
>>> obj = HelloIfEven(1)
|
||||
>>> hasattr(obj, "say_hello")
|
||||
False
|
||||
>>> obj.x = 2
|
||||
>>> hasattr(obj, "say_hello")
|
||||
True
|
||||
>>> obj.say_hello()
|
||||
Hello
|
||||
"""
|
||||
return lambda fn: _AvailableIfDescriptor(fn, check, attribute_name=fn.__name__)
|
||||
@@ -0,0 +1,67 @@
|
||||
import warnings
|
||||
|
||||
|
||||
class Bunch(dict):
|
||||
"""Container object exposing keys as attributes.
|
||||
|
||||
Bunch objects are sometimes used as an output for functions and methods.
|
||||
They extend dictionaries by enabling values to be accessed by key,
|
||||
`bunch["value_key"]`, or by an attribute, `bunch.value_key`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import Bunch
|
||||
>>> b = Bunch(a=1, b=2)
|
||||
>>> b['b']
|
||||
2
|
||||
>>> b.b
|
||||
2
|
||||
>>> b.a = 3
|
||||
>>> b['a']
|
||||
3
|
||||
>>> b.c = 6
|
||||
>>> b['c']
|
||||
6
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(kwargs)
|
||||
|
||||
# Map from deprecated key to warning message
|
||||
self.__dict__["_deprecated_key_to_warnings"] = {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self.__dict__.get("_deprecated_key_to_warnings", {}):
|
||||
warnings.warn(
|
||||
self._deprecated_key_to_warnings[key],
|
||||
FutureWarning,
|
||||
)
|
||||
return super().__getitem__(key)
|
||||
|
||||
def _set_deprecated(self, value, *, new_key, deprecated_key, warning_message):
|
||||
"""Set key in dictionary to be deprecated with its warning message."""
|
||||
self.__dict__["_deprecated_key_to_warnings"][deprecated_key] = warning_message
|
||||
self[new_key] = self[deprecated_key] = value
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self[key] = value
|
||||
|
||||
def __dir__(self):
|
||||
return self.keys()
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
raise AttributeError(key)
|
||||
|
||||
def __setstate__(self, state):
|
||||
# Bunch pickles generated with scikit-learn 0.16.* have an non
|
||||
# empty __dict__. This causes a surprising behaviour when
|
||||
# loading these pickles scikit-learn 0.17: reading bunch.key
|
||||
# uses __dict__ but assigning to bunch.key use __setattr__ and
|
||||
# only changes bunch['key']. More details can be found at:
|
||||
# https://github.com/scikit-learn/scikit-learn/issues/6196.
|
||||
# Overriding __setstate__ to be a noop has the effect of
|
||||
# ignoring the pickled __dict__
|
||||
pass
|
||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
from cython cimport floating
|
||||
|
||||
|
||||
cpdef enum BLAS_Order:
|
||||
RowMajor # C contiguous
|
||||
ColMajor # Fortran contiguous
|
||||
|
||||
|
||||
cpdef enum BLAS_Trans:
|
||||
NoTrans = 110 # correspond to 'n'
|
||||
Trans = 116 # correspond to 't'
|
||||
|
||||
|
||||
# BLAS Level 1 ################################################################
|
||||
cdef floating _dot(int, const floating*, int, const floating*, int) noexcept nogil
|
||||
|
||||
cdef floating _asum(int, const floating*, int) noexcept nogil
|
||||
|
||||
cdef void _axpy(int, floating, const floating*, int, floating*, int) noexcept nogil
|
||||
|
||||
cdef floating _nrm2(int, const floating*, int) noexcept nogil
|
||||
|
||||
cdef void _copy(int, const floating*, int, const floating*, int) noexcept nogil
|
||||
|
||||
cdef void _scal(int, floating, const floating*, int) noexcept nogil
|
||||
|
||||
cdef void _rotg(floating*, floating*, floating*, floating*) noexcept nogil
|
||||
|
||||
cdef void _rot(int, floating*, int, floating*, int, floating, floating) noexcept nogil
|
||||
|
||||
# BLAS Level 2 ################################################################
|
||||
cdef void _gemv(BLAS_Order, BLAS_Trans, int, int, floating, const floating*, int,
|
||||
const floating*, int, floating, floating*, int) noexcept nogil
|
||||
|
||||
cdef void _ger(BLAS_Order, int, int, floating, const floating*, int, const floating*,
|
||||
int, floating*, int) noexcept nogil
|
||||
|
||||
# BLASLevel 3 ################################################################
|
||||
cdef void _gemm(BLAS_Order, BLAS_Trans, BLAS_Trans, int, int, int, floating,
|
||||
const floating*, int, const floating*, int, floating, floating*,
|
||||
int) noexcept nogil
|
||||
@@ -0,0 +1,367 @@
|
||||
from collections import Counter
|
||||
from contextlib import suppress
|
||||
from typing import NamedTuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import is_scalar_nan
|
||||
|
||||
|
||||
def _unique(values, *, return_inverse=False, return_counts=False):
|
||||
"""Helper function to find unique values with support for python objects.
|
||||
|
||||
Uses pure python method for object dtype, and numpy method for
|
||||
all other dtypes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
values : ndarray
|
||||
Values to check for unknowns.
|
||||
|
||||
return_inverse : bool, default=False
|
||||
If True, also return the indices of the unique values.
|
||||
|
||||
return_counts : bool, default=False
|
||||
If True, also return the number of times each unique item appears in
|
||||
values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
unique : ndarray
|
||||
The sorted unique values.
|
||||
|
||||
unique_inverse : ndarray
|
||||
The indices to reconstruct the original array from the unique array.
|
||||
Only provided if `return_inverse` is True.
|
||||
|
||||
unique_counts : ndarray
|
||||
The number of times each of the unique values comes up in the original
|
||||
array. Only provided if `return_counts` is True.
|
||||
"""
|
||||
if values.dtype == object:
|
||||
return _unique_python(
|
||||
values, return_inverse=return_inverse, return_counts=return_counts
|
||||
)
|
||||
# numerical
|
||||
return _unique_np(
|
||||
values, return_inverse=return_inverse, return_counts=return_counts
|
||||
)
|
||||
|
||||
|
||||
def _unique_np(values, return_inverse=False, return_counts=False):
|
||||
"""Helper function to find unique values for numpy arrays that correctly
|
||||
accounts for nans. See `_unique` documentation for details."""
|
||||
uniques = np.unique(
|
||||
values, return_inverse=return_inverse, return_counts=return_counts
|
||||
)
|
||||
|
||||
inverse, counts = None, None
|
||||
|
||||
if return_counts:
|
||||
*uniques, counts = uniques
|
||||
|
||||
if return_inverse:
|
||||
*uniques, inverse = uniques
|
||||
|
||||
if return_counts or return_inverse:
|
||||
uniques = uniques[0]
|
||||
|
||||
# np.unique will have duplicate missing values at the end of `uniques`
|
||||
# here we clip the nans and remove it from uniques
|
||||
if uniques.size and is_scalar_nan(uniques[-1]):
|
||||
nan_idx = np.searchsorted(uniques, np.nan)
|
||||
uniques = uniques[: nan_idx + 1]
|
||||
if return_inverse:
|
||||
inverse[inverse > nan_idx] = nan_idx
|
||||
|
||||
if return_counts:
|
||||
counts[nan_idx] = np.sum(counts[nan_idx:])
|
||||
counts = counts[: nan_idx + 1]
|
||||
|
||||
ret = (uniques,)
|
||||
|
||||
if return_inverse:
|
||||
ret += (inverse,)
|
||||
|
||||
if return_counts:
|
||||
ret += (counts,)
|
||||
|
||||
return ret[0] if len(ret) == 1 else ret
|
||||
|
||||
|
||||
class MissingValues(NamedTuple):
|
||||
"""Data class for missing data information"""
|
||||
|
||||
nan: bool
|
||||
none: bool
|
||||
|
||||
def to_list(self):
|
||||
"""Convert tuple to a list where None is always first."""
|
||||
output = []
|
||||
if self.none:
|
||||
output.append(None)
|
||||
if self.nan:
|
||||
output.append(np.nan)
|
||||
return output
|
||||
|
||||
|
||||
def _extract_missing(values):
|
||||
"""Extract missing values from `values`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
values: set
|
||||
Set of values to extract missing from.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output: set
|
||||
Set with missing values extracted.
|
||||
|
||||
missing_values: MissingValues
|
||||
Object with missing value information.
|
||||
"""
|
||||
missing_values_set = {
|
||||
value for value in values if value is None or is_scalar_nan(value)
|
||||
}
|
||||
|
||||
if not missing_values_set:
|
||||
return values, MissingValues(nan=False, none=False)
|
||||
|
||||
if None in missing_values_set:
|
||||
if len(missing_values_set) == 1:
|
||||
output_missing_values = MissingValues(nan=False, none=True)
|
||||
else:
|
||||
# If there is more than one missing value, then it has to be
|
||||
# float('nan') or np.nan
|
||||
output_missing_values = MissingValues(nan=True, none=True)
|
||||
else:
|
||||
output_missing_values = MissingValues(nan=True, none=False)
|
||||
|
||||
# create set without the missing values
|
||||
output = values - missing_values_set
|
||||
return output, output_missing_values
|
||||
|
||||
|
||||
class _nandict(dict):
|
||||
"""Dictionary with support for nans."""
|
||||
|
||||
def __init__(self, mapping):
|
||||
super().__init__(mapping)
|
||||
for key, value in mapping.items():
|
||||
if is_scalar_nan(key):
|
||||
self.nan_value = value
|
||||
break
|
||||
|
||||
def __missing__(self, key):
|
||||
if hasattr(self, "nan_value") and is_scalar_nan(key):
|
||||
return self.nan_value
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def _map_to_integer(values, uniques):
|
||||
"""Map values based on its position in uniques."""
|
||||
table = _nandict({val: i for i, val in enumerate(uniques)})
|
||||
return np.array([table[v] for v in values])
|
||||
|
||||
|
||||
def _unique_python(values, *, return_inverse, return_counts):
|
||||
# Only used in `_uniques`, see docstring there for details
|
||||
try:
|
||||
uniques_set = set(values)
|
||||
uniques_set, missing_values = _extract_missing(uniques_set)
|
||||
|
||||
uniques = sorted(uniques_set)
|
||||
uniques.extend(missing_values.to_list())
|
||||
uniques = np.array(uniques, dtype=values.dtype)
|
||||
except TypeError:
|
||||
types = sorted(t.__qualname__ for t in set(type(v) for v in values))
|
||||
raise TypeError(
|
||||
"Encoders require their input argument must be uniformly "
|
||||
f"strings or numbers. Got {types}"
|
||||
)
|
||||
ret = (uniques,)
|
||||
|
||||
if return_inverse:
|
||||
ret += (_map_to_integer(values, uniques),)
|
||||
|
||||
if return_counts:
|
||||
ret += (_get_counts(values, uniques),)
|
||||
|
||||
return ret[0] if len(ret) == 1 else ret
|
||||
|
||||
|
||||
def _encode(values, *, uniques, check_unknown=True):
|
||||
"""Helper function to encode values into [0, n_uniques - 1].
|
||||
|
||||
Uses pure python method for object dtype, and numpy method for
|
||||
all other dtypes.
|
||||
The numpy method has the limitation that the `uniques` need to
|
||||
be sorted. Importantly, this is not checked but assumed to already be
|
||||
the case. The calling method needs to ensure this for all non-object
|
||||
values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
values : ndarray
|
||||
Values to encode.
|
||||
uniques : ndarray
|
||||
The unique values in `values`. If the dtype is not object, then
|
||||
`uniques` needs to be sorted.
|
||||
check_unknown : bool, default=True
|
||||
If True, check for values in `values` that are not in `unique`
|
||||
and raise an error. This is ignored for object dtype, and treated as
|
||||
True in this case. This parameter is useful for
|
||||
_BaseEncoder._transform() to avoid calling _check_unknown()
|
||||
twice.
|
||||
|
||||
Returns
|
||||
-------
|
||||
encoded : ndarray
|
||||
Encoded values
|
||||
"""
|
||||
if values.dtype.kind in "OUS":
|
||||
try:
|
||||
return _map_to_integer(values, uniques)
|
||||
except KeyError as e:
|
||||
raise ValueError(f"y contains previously unseen labels: {str(e)}")
|
||||
else:
|
||||
if check_unknown:
|
||||
diff = _check_unknown(values, uniques)
|
||||
if diff:
|
||||
raise ValueError(f"y contains previously unseen labels: {str(diff)}")
|
||||
return np.searchsorted(uniques, values)
|
||||
|
||||
|
||||
def _check_unknown(values, known_values, return_mask=False):
|
||||
"""
|
||||
Helper function to check for unknowns in values to be encoded.
|
||||
|
||||
Uses pure python method for object dtype, and numpy method for
|
||||
all other dtypes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
values : array
|
||||
Values to check for unknowns.
|
||||
known_values : array
|
||||
Known values. Must be unique.
|
||||
return_mask : bool, default=False
|
||||
If True, return a mask of the same shape as `values` indicating
|
||||
the valid values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
diff : list
|
||||
The unique values present in `values` and not in `know_values`.
|
||||
valid_mask : boolean array
|
||||
Additionally returned if ``return_mask=True``.
|
||||
|
||||
"""
|
||||
valid_mask = None
|
||||
|
||||
if values.dtype.kind in "OUS":
|
||||
values_set = set(values)
|
||||
values_set, missing_in_values = _extract_missing(values_set)
|
||||
|
||||
uniques_set = set(known_values)
|
||||
uniques_set, missing_in_uniques = _extract_missing(uniques_set)
|
||||
diff = values_set - uniques_set
|
||||
|
||||
nan_in_diff = missing_in_values.nan and not missing_in_uniques.nan
|
||||
none_in_diff = missing_in_values.none and not missing_in_uniques.none
|
||||
|
||||
def is_valid(value):
|
||||
return (
|
||||
value in uniques_set
|
||||
or missing_in_uniques.none
|
||||
and value is None
|
||||
or missing_in_uniques.nan
|
||||
and is_scalar_nan(value)
|
||||
)
|
||||
|
||||
if return_mask:
|
||||
if diff or nan_in_diff or none_in_diff:
|
||||
valid_mask = np.array([is_valid(value) for value in values])
|
||||
else:
|
||||
valid_mask = np.ones(len(values), dtype=bool)
|
||||
|
||||
diff = list(diff)
|
||||
if none_in_diff:
|
||||
diff.append(None)
|
||||
if nan_in_diff:
|
||||
diff.append(np.nan)
|
||||
else:
|
||||
unique_values = np.unique(values)
|
||||
diff = np.setdiff1d(unique_values, known_values, assume_unique=True)
|
||||
if return_mask:
|
||||
if diff.size:
|
||||
valid_mask = np.isin(values, known_values)
|
||||
else:
|
||||
valid_mask = np.ones(len(values), dtype=bool)
|
||||
|
||||
# check for nans in the known_values
|
||||
if np.isnan(known_values).any():
|
||||
diff_is_nan = np.isnan(diff)
|
||||
if diff_is_nan.any():
|
||||
# removes nan from valid_mask
|
||||
if diff.size and return_mask:
|
||||
is_nan = np.isnan(values)
|
||||
valid_mask[is_nan] = 1
|
||||
|
||||
# remove nan from diff
|
||||
diff = diff[~diff_is_nan]
|
||||
diff = list(diff)
|
||||
|
||||
if return_mask:
|
||||
return diff, valid_mask
|
||||
return diff
|
||||
|
||||
|
||||
class _NaNCounter(Counter):
|
||||
"""Counter with support for nan values."""
|
||||
|
||||
def __init__(self, items):
|
||||
super().__init__(self._generate_items(items))
|
||||
|
||||
def _generate_items(self, items):
|
||||
"""Generate items without nans. Stores the nan counts separately."""
|
||||
for item in items:
|
||||
if not is_scalar_nan(item):
|
||||
yield item
|
||||
continue
|
||||
if not hasattr(self, "nan_count"):
|
||||
self.nan_count = 0
|
||||
self.nan_count += 1
|
||||
|
||||
def __missing__(self, key):
|
||||
if hasattr(self, "nan_count") and is_scalar_nan(key):
|
||||
return self.nan_count
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def _get_counts(values, uniques):
|
||||
"""Get the count of each of the `uniques` in `values`.
|
||||
|
||||
The counts will use the order passed in by `uniques`. For non-object dtypes,
|
||||
`uniques` is assumed to be sorted and `np.nan` is at the end.
|
||||
"""
|
||||
if values.dtype.kind in "OU":
|
||||
counter = _NaNCounter(values)
|
||||
output = np.zeros(len(uniques), dtype=np.int64)
|
||||
for i, item in enumerate(uniques):
|
||||
with suppress(KeyError):
|
||||
output[i] = counter[item]
|
||||
return output
|
||||
|
||||
unique_values, counts = _unique_np(values, return_counts=True)
|
||||
|
||||
# Recorder unique_values based on input: `uniques`
|
||||
uniques_in_values = np.isin(uniques, unique_values, assume_unique=True)
|
||||
if np.isnan(unique_values[-1]) and np.isnan(uniques[-1]):
|
||||
uniques_in_values[-1] = True
|
||||
|
||||
unique_valid_indices = np.searchsorted(unique_values, uniques[uniques_in_values])
|
||||
output = np.zeros_like(uniques, dtype=np.int64)
|
||||
output[uniques_in_values] = counts[unique_valid_indices]
|
||||
return output
|
||||
@@ -0,0 +1,404 @@
|
||||
#$id {
|
||||
/* Definition of color scheme common for light and dark mode */
|
||||
--sklearn-color-text: black;
|
||||
--sklearn-color-line: gray;
|
||||
/* Definition of color scheme for unfitted estimators */
|
||||
--sklearn-color-unfitted-level-0: #fff5e6;
|
||||
--sklearn-color-unfitted-level-1: #f6e4d2;
|
||||
--sklearn-color-unfitted-level-2: #ffe0b3;
|
||||
--sklearn-color-unfitted-level-3: chocolate;
|
||||
/* Definition of color scheme for fitted estimators */
|
||||
--sklearn-color-fitted-level-0: #f0f8ff;
|
||||
--sklearn-color-fitted-level-1: #d4ebff;
|
||||
--sklearn-color-fitted-level-2: #b3dbfd;
|
||||
--sklearn-color-fitted-level-3: cornflowerblue;
|
||||
|
||||
/* Specific color for light theme */
|
||||
--sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));
|
||||
--sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));
|
||||
--sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));
|
||||
--sklearn-color-icon: #696969;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* Redefinition of color scheme for dark theme */
|
||||
--sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));
|
||||
--sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));
|
||||
--sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));
|
||||
--sklearn-color-icon: #878787;
|
||||
}
|
||||
}
|
||||
|
||||
#$id {
|
||||
color: var(--sklearn-color-text);
|
||||
}
|
||||
|
||||
#$id pre {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#$id input.sk-hidden--visually {
|
||||
border: 0;
|
||||
clip: rect(1px 1px 1px 1px);
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
#$id div.sk-dashed-wrapped {
|
||||
border: 1px dashed var(--sklearn-color-line);
|
||||
margin: 0 0.4em 0.5em 0.4em;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 0.4em;
|
||||
background-color: var(--sklearn-color-background);
|
||||
}
|
||||
|
||||
#$id div.sk-container {
|
||||
/* jupyter's `normalize.less` sets `[hidden] { display: none; }`
|
||||
but bootstrap.min.css set `[hidden] { display: none !important; }`
|
||||
so we also need the `!important` here to be able to override the
|
||||
default hidden behavior on the sphinx rendered scikit-learn.org.
|
||||
See: https://github.com/scikit-learn/scikit-learn/issues/21755 */
|
||||
display: inline-block !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#$id div.sk-text-repr-fallback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.sk-parallel-item,
|
||||
div.sk-serial,
|
||||
div.sk-item {
|
||||
/* draw centered vertical line to link estimators */
|
||||
background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));
|
||||
background-size: 2px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
/* Parallel-specific style estimator block */
|
||||
|
||||
#$id div.sk-parallel-item::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
border-bottom: 2px solid var(--sklearn-color-text-on-default-background);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#$id div.sk-parallel {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
background-color: var(--sklearn-color-background);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#$id div.sk-parallel-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#$id div.sk-parallel-item:first-child::after {
|
||||
align-self: flex-end;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#$id div.sk-parallel-item:last-child::after {
|
||||
align-self: flex-start;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#$id div.sk-parallel-item:only-child::after {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* Serial-specific style estimator block */
|
||||
|
||||
#$id div.sk-serial {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--sklearn-color-background);
|
||||
padding-right: 1em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is
|
||||
clickable and can be expanded/collapsed.
|
||||
- Pipeline and ColumnTransformer use this feature and define the default style
|
||||
- Estimators will overwrite some part of the style using the `sk-estimator` class
|
||||
*/
|
||||
|
||||
/* Pipeline and ColumnTransformer style (default) */
|
||||
|
||||
#$id div.sk-toggleable {
|
||||
/* Default theme specific background. It is overwritten whether we have a
|
||||
specific estimator or a Pipeline/ColumnTransformer */
|
||||
background-color: var(--sklearn-color-background);
|
||||
}
|
||||
|
||||
/* Toggleable label */
|
||||
#$id label.sk-toggleable__label {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
padding: 0.5em;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#$id label.sk-toggleable__label-arrow:before {
|
||||
/* Arrow on the left of the label */
|
||||
content: "▸";
|
||||
float: left;
|
||||
margin-right: 0.25em;
|
||||
color: var(--sklearn-color-icon);
|
||||
}
|
||||
|
||||
#$id label.sk-toggleable__label-arrow:hover:before {
|
||||
color: var(--sklearn-color-text);
|
||||
}
|
||||
|
||||
/* Toggleable content - dropdown */
|
||||
|
||||
#$id div.sk-toggleable__content {
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
/* unfitted */
|
||||
background-color: var(--sklearn-color-unfitted-level-0);
|
||||
}
|
||||
|
||||
#$id div.sk-toggleable__content.fitted {
|
||||
/* fitted */
|
||||
background-color: var(--sklearn-color-fitted-level-0);
|
||||
}
|
||||
|
||||
#$id div.sk-toggleable__content pre {
|
||||
margin: 0.2em;
|
||||
border-radius: 0.25em;
|
||||
color: var(--sklearn-color-text);
|
||||
/* unfitted */
|
||||
background-color: var(--sklearn-color-unfitted-level-0);
|
||||
}
|
||||
|
||||
#$id div.sk-toggleable__content.fitted pre {
|
||||
/* unfitted */
|
||||
background-color: var(--sklearn-color-fitted-level-0);
|
||||
}
|
||||
|
||||
#$id input.sk-toggleable__control:checked~div.sk-toggleable__content {
|
||||
/* Expand drop-down */
|
||||
max-height: 200px;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#$id input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {
|
||||
content: "▾";
|
||||
}
|
||||
|
||||
/* Pipeline/ColumnTransformer-specific style */
|
||||
|
||||
#$id div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {
|
||||
color: var(--sklearn-color-text);
|
||||
background-color: var(--sklearn-color-unfitted-level-2);
|
||||
}
|
||||
|
||||
#$id div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {
|
||||
background-color: var(--sklearn-color-fitted-level-2);
|
||||
}
|
||||
|
||||
/* Estimator-specific style */
|
||||
|
||||
/* Colorize estimator box */
|
||||
#$id div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {
|
||||
/* unfitted */
|
||||
background-color: var(--sklearn-color-unfitted-level-2);
|
||||
}
|
||||
|
||||
#$id div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {
|
||||
/* fitted */
|
||||
background-color: var(--sklearn-color-fitted-level-2);
|
||||
}
|
||||
|
||||
#$id div.sk-label label.sk-toggleable__label,
|
||||
#$id div.sk-label label {
|
||||
/* The background is the default theme color */
|
||||
color: var(--sklearn-color-text-on-default-background);
|
||||
}
|
||||
|
||||
/* On hover, darken the color of the background */
|
||||
#$id div.sk-label:hover label.sk-toggleable__label {
|
||||
color: var(--sklearn-color-text);
|
||||
background-color: var(--sklearn-color-unfitted-level-2);
|
||||
}
|
||||
|
||||
/* Label box, darken color on hover, fitted */
|
||||
#$id div.sk-label.fitted:hover label.sk-toggleable__label.fitted {
|
||||
color: var(--sklearn-color-text);
|
||||
background-color: var(--sklearn-color-fitted-level-2);
|
||||
}
|
||||
|
||||
/* Estimator label */
|
||||
|
||||
#$id div.sk-label label {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
#$id div.sk-label-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Estimator-specific */
|
||||
#$id div.sk-estimator {
|
||||
font-family: monospace;
|
||||
border: 1px dotted var(--sklearn-color-border-box);
|
||||
border-radius: 0.25em;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0.5em;
|
||||
/* unfitted */
|
||||
background-color: var(--sklearn-color-unfitted-level-0);
|
||||
}
|
||||
|
||||
#$id div.sk-estimator.fitted {
|
||||
/* fitted */
|
||||
background-color: var(--sklearn-color-fitted-level-0);
|
||||
}
|
||||
|
||||
/* on hover */
|
||||
#$id div.sk-estimator:hover {
|
||||
/* unfitted */
|
||||
background-color: var(--sklearn-color-unfitted-level-2);
|
||||
}
|
||||
|
||||
#$id div.sk-estimator.fitted:hover {
|
||||
/* fitted */
|
||||
background-color: var(--sklearn-color-fitted-level-2);
|
||||
}
|
||||
|
||||
/* Specification for estimator info (e.g. "i" and "?") */
|
||||
|
||||
/* Common style for "i" and "?" */
|
||||
|
||||
.sk-estimator-doc-link,
|
||||
a:link.sk-estimator-doc-link,
|
||||
a:visited.sk-estimator-doc-link {
|
||||
float: right;
|
||||
font-size: smaller;
|
||||
line-height: 1em;
|
||||
font-family: monospace;
|
||||
background-color: var(--sklearn-color-background);
|
||||
border-radius: 1em;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
text-decoration: none !important;
|
||||
margin-left: 1ex;
|
||||
/* unfitted */
|
||||
border: var(--sklearn-color-unfitted-level-1) 1pt solid;
|
||||
color: var(--sklearn-color-unfitted-level-1);
|
||||
}
|
||||
|
||||
.sk-estimator-doc-link.fitted,
|
||||
a:link.sk-estimator-doc-link.fitted,
|
||||
a:visited.sk-estimator-doc-link.fitted {
|
||||
/* fitted */
|
||||
border: var(--sklearn-color-fitted-level-1) 1pt solid;
|
||||
color: var(--sklearn-color-fitted-level-1);
|
||||
}
|
||||
|
||||
/* On hover */
|
||||
div.sk-estimator:hover .sk-estimator-doc-link:hover,
|
||||
.sk-estimator-doc-link:hover,
|
||||
div.sk-label-container:hover .sk-estimator-doc-link:hover,
|
||||
.sk-estimator-doc-link:hover {
|
||||
/* unfitted */
|
||||
background-color: var(--sklearn-color-unfitted-level-3);
|
||||
color: var(--sklearn-color-background);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,
|
||||
.sk-estimator-doc-link.fitted:hover,
|
||||
div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,
|
||||
.sk-estimator-doc-link.fitted:hover {
|
||||
/* fitted */
|
||||
background-color: var(--sklearn-color-fitted-level-3);
|
||||
color: var(--sklearn-color-background);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Span, style for the box shown on hovering the info icon */
|
||||
.sk-estimator-doc-link span {
|
||||
display: none;
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
right: .2ex;
|
||||
padding: .5ex;
|
||||
margin: .5ex;
|
||||
width: min-content;
|
||||
min-width: 20ex;
|
||||
max-width: 50ex;
|
||||
color: var(--sklearn-color-text);
|
||||
box-shadow: 2pt 2pt 4pt #999;
|
||||
/* unfitted */
|
||||
background: var(--sklearn-color-unfitted-level-0);
|
||||
border: .5pt solid var(--sklearn-color-unfitted-level-3);
|
||||
}
|
||||
|
||||
.sk-estimator-doc-link.fitted span {
|
||||
/* fitted */
|
||||
background: var(--sklearn-color-fitted-level-0);
|
||||
border: var(--sklearn-color-fitted-level-3);
|
||||
}
|
||||
|
||||
.sk-estimator-doc-link:hover span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* "?"-specific style due to the `<a>` HTML tag */
|
||||
|
||||
#$id a.estimator_doc_link {
|
||||
float: right;
|
||||
font-size: 1rem;
|
||||
line-height: 1em;
|
||||
font-family: monospace;
|
||||
background-color: var(--sklearn-color-background);
|
||||
border-radius: 1rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
text-decoration: none;
|
||||
/* unfitted */
|
||||
color: var(--sklearn-color-unfitted-level-1);
|
||||
border: var(--sklearn-color-unfitted-level-1) 1pt solid;
|
||||
}
|
||||
|
||||
#$id a.estimator_doc_link.fitted {
|
||||
/* fitted */
|
||||
border: var(--sklearn-color-fitted-level-1) 1pt solid;
|
||||
color: var(--sklearn-color-fitted-level-1);
|
||||
}
|
||||
|
||||
/* On hover */
|
||||
#$id a.estimator_doc_link:hover {
|
||||
/* unfitted */
|
||||
background-color: var(--sklearn-color-unfitted-level-3);
|
||||
color: var(--sklearn-color-background);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#$id a.estimator_doc_link.fitted:hover {
|
||||
/* fitted */
|
||||
background-color: var(--sklearn-color-fitted-level-3);
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
import html
|
||||
import itertools
|
||||
from contextlib import closing
|
||||
from inspect import isclass
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
|
||||
from .. import __version__, config_context
|
||||
from .fixes import parse_version
|
||||
|
||||
|
||||
class _IDCounter:
|
||||
"""Generate sequential ids with a prefix."""
|
||||
|
||||
def __init__(self, prefix):
|
||||
self.prefix = prefix
|
||||
self.count = 0
|
||||
|
||||
def get_id(self):
|
||||
self.count += 1
|
||||
return f"{self.prefix}-{self.count}"
|
||||
|
||||
|
||||
def _get_css_style():
|
||||
return Path(__file__).with_suffix(".css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
_CONTAINER_ID_COUNTER = _IDCounter("sk-container-id")
|
||||
_ESTIMATOR_ID_COUNTER = _IDCounter("sk-estimator-id")
|
||||
_CSS_STYLE = _get_css_style()
|
||||
|
||||
|
||||
class _VisualBlock:
|
||||
"""HTML Representation of Estimator
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kind : {'serial', 'parallel', 'single'}
|
||||
kind of HTML block
|
||||
|
||||
estimators : list of estimators or `_VisualBlock`s or a single estimator
|
||||
If kind != 'single', then `estimators` is a list of
|
||||
estimators.
|
||||
If kind == 'single', then `estimators` is a single estimator.
|
||||
|
||||
names : list of str, default=None
|
||||
If kind != 'single', then `names` corresponds to estimators.
|
||||
If kind == 'single', then `names` is a single string corresponding to
|
||||
the single estimator.
|
||||
|
||||
name_details : list of str, str, or None, default=None
|
||||
If kind != 'single', then `name_details` corresponds to `names`.
|
||||
If kind == 'single', then `name_details` is a single string
|
||||
corresponding to the single estimator.
|
||||
|
||||
dash_wrapped : bool, default=True
|
||||
If true, wrapped HTML element will be wrapped with a dashed border.
|
||||
Only active when kind != 'single'.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, kind, estimators, *, names=None, name_details=None, dash_wrapped=True
|
||||
):
|
||||
self.kind = kind
|
||||
self.estimators = estimators
|
||||
self.dash_wrapped = dash_wrapped
|
||||
|
||||
if self.kind in ("parallel", "serial"):
|
||||
if names is None:
|
||||
names = (None,) * len(estimators)
|
||||
if name_details is None:
|
||||
name_details = (None,) * len(estimators)
|
||||
|
||||
self.names = names
|
||||
self.name_details = name_details
|
||||
|
||||
def _sk_visual_block_(self):
|
||||
return self
|
||||
|
||||
|
||||
def _write_label_html(
|
||||
out,
|
||||
name,
|
||||
name_details,
|
||||
outer_class="sk-label-container",
|
||||
inner_class="sk-label",
|
||||
checked=False,
|
||||
doc_link="",
|
||||
is_fitted_css_class="",
|
||||
is_fitted_icon="",
|
||||
):
|
||||
"""Write labeled html with or without a dropdown with named details.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : file-like object
|
||||
The file to write the HTML representation to.
|
||||
name : str
|
||||
The label for the estimator. It corresponds either to the estimator class name
|
||||
for a simple estimator or in the case of a `Pipeline` and `ColumnTransformer`,
|
||||
it corresponds to the name of the step.
|
||||
name_details : str
|
||||
The details to show as content in the dropdown part of the toggleable label. It
|
||||
can contain information such as non-default parameters or column information for
|
||||
`ColumnTransformer`.
|
||||
outer_class : {"sk-label-container", "sk-item"}, default="sk-label-container"
|
||||
The CSS class for the outer container.
|
||||
inner_class : {"sk-label", "sk-estimator"}, default="sk-label"
|
||||
The CSS class for the inner container.
|
||||
checked : bool, default=False
|
||||
Whether the dropdown is folded or not. With a single estimator, we intend to
|
||||
unfold the content.
|
||||
doc_link : str, default=""
|
||||
The link to the documentation for the estimator. If an empty string, no link is
|
||||
added to the diagram. This can be generated for an estimator if it uses the
|
||||
`_HTMLDocumentationLinkMixin`.
|
||||
is_fitted_css_class : {"", "fitted"}
|
||||
The CSS class to indicate whether or not the estimator is fitted. The
|
||||
empty string means that the estimator is not fitted and "fitted" means that the
|
||||
estimator is fitted.
|
||||
is_fitted_icon : str, default=""
|
||||
The HTML representation to show the fitted information in the diagram. An empty
|
||||
string means that no information is shown.
|
||||
"""
|
||||
# we need to add some padding to the left of the label to be sure it is centered
|
||||
padding_label = " " if is_fitted_icon else "" # add padding for the "i" char
|
||||
|
||||
out.write(
|
||||
f'<div class="{outer_class}"><div'
|
||||
f' class="{inner_class} {is_fitted_css_class} sk-toggleable">'
|
||||
)
|
||||
name = html.escape(name)
|
||||
|
||||
if name_details is not None:
|
||||
name_details = html.escape(str(name_details))
|
||||
label_class = (
|
||||
f"sk-toggleable__label {is_fitted_css_class} sk-toggleable__label-arrow"
|
||||
)
|
||||
|
||||
checked_str = "checked" if checked else ""
|
||||
est_id = _ESTIMATOR_ID_COUNTER.get_id()
|
||||
|
||||
if doc_link:
|
||||
doc_label = "<span>Online documentation</span>"
|
||||
if name is not None:
|
||||
doc_label = f"<span>Documentation for {name}</span>"
|
||||
doc_link = (
|
||||
f'<a class="sk-estimator-doc-link {is_fitted_css_class}"'
|
||||
f' rel="noreferrer" target="_blank" href="{doc_link}">?{doc_label}</a>'
|
||||
)
|
||||
padding_label += " " # add additional padding for the "?" char
|
||||
|
||||
fmt_str = (
|
||||
'<input class="sk-toggleable__control sk-hidden--visually"'
|
||||
f' id="{est_id}" '
|
||||
f'type="checkbox" {checked_str}><label for="{est_id}" '
|
||||
f'class="{label_class} {is_fitted_css_class}">{padding_label}{name}'
|
||||
f"{doc_link}{is_fitted_icon}</label><div "
|
||||
f'class="sk-toggleable__content {is_fitted_css_class}">'
|
||||
f"<pre>{name_details}</pre></div> "
|
||||
)
|
||||
out.write(fmt_str)
|
||||
else:
|
||||
out.write(f"<label>{name}</label>")
|
||||
out.write("</div></div>") # outer_class inner_class
|
||||
|
||||
|
||||
def _get_visual_block(estimator):
|
||||
"""Generate information about how to display an estimator."""
|
||||
if hasattr(estimator, "_sk_visual_block_"):
|
||||
try:
|
||||
return estimator._sk_visual_block_()
|
||||
except Exception:
|
||||
return _VisualBlock(
|
||||
"single",
|
||||
estimator,
|
||||
names=estimator.__class__.__name__,
|
||||
name_details=str(estimator),
|
||||
)
|
||||
|
||||
if isinstance(estimator, str):
|
||||
return _VisualBlock(
|
||||
"single", estimator, names=estimator, name_details=estimator
|
||||
)
|
||||
elif estimator is None:
|
||||
return _VisualBlock("single", estimator, names="None", name_details="None")
|
||||
|
||||
# check if estimator looks like a meta estimator (wraps estimators)
|
||||
if hasattr(estimator, "get_params") and not isclass(estimator):
|
||||
estimators = [
|
||||
(key, est)
|
||||
for key, est in estimator.get_params(deep=False).items()
|
||||
if hasattr(est, "get_params") and hasattr(est, "fit") and not isclass(est)
|
||||
]
|
||||
if estimators:
|
||||
return _VisualBlock(
|
||||
"parallel",
|
||||
[est for _, est in estimators],
|
||||
names=[f"{key}: {est.__class__.__name__}" for key, est in estimators],
|
||||
name_details=[str(est) for _, est in estimators],
|
||||
)
|
||||
|
||||
return _VisualBlock(
|
||||
"single",
|
||||
estimator,
|
||||
names=estimator.__class__.__name__,
|
||||
name_details=str(estimator),
|
||||
)
|
||||
|
||||
|
||||
def _write_estimator_html(
|
||||
out,
|
||||
estimator,
|
||||
estimator_label,
|
||||
estimator_label_details,
|
||||
is_fitted_css_class,
|
||||
is_fitted_icon="",
|
||||
first_call=False,
|
||||
):
|
||||
"""Write estimator to html in serial, parallel, or by itself (single).
|
||||
|
||||
For multiple estimators, this function is called recursively.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : file-like object
|
||||
The file to write the HTML representation to.
|
||||
estimator : estimator object
|
||||
The estimator to visualize.
|
||||
estimator_label : str
|
||||
The label for the estimator. It corresponds either to the estimator class name
|
||||
for simple estimator or in the case of `Pipeline` and `ColumnTransformer`, it
|
||||
corresponds to the name of the step.
|
||||
estimator_label_details : str
|
||||
The details to show as content in the dropdown part of the toggleable label.
|
||||
It can contain information as non-default parameters or column information for
|
||||
`ColumnTransformer`.
|
||||
is_fitted_css_class : {"", "fitted"}
|
||||
The CSS class to indicate whether or not the estimator is fitted or not. The
|
||||
empty string means that the estimator is not fitted and "fitted" means that the
|
||||
estimator is fitted.
|
||||
is_fitted_icon : str, default=""
|
||||
The HTML representation to show the fitted information in the diagram. An empty
|
||||
string means that no information is shown. If the estimator to be shown is not
|
||||
the first estimator (i.e. `first_call=False`), `is_fitted_icon` is always an
|
||||
empty string.
|
||||
first_call : bool, default=False
|
||||
Whether this is the first time this function is called.
|
||||
"""
|
||||
if first_call:
|
||||
est_block = _get_visual_block(estimator)
|
||||
else:
|
||||
is_fitted_icon = ""
|
||||
with config_context(print_changed_only=True):
|
||||
est_block = _get_visual_block(estimator)
|
||||
# `estimator` can also be an instance of `_VisualBlock`
|
||||
if hasattr(estimator, "_get_doc_link"):
|
||||
doc_link = estimator._get_doc_link()
|
||||
else:
|
||||
doc_link = ""
|
||||
if est_block.kind in ("serial", "parallel"):
|
||||
dashed_wrapped = first_call or est_block.dash_wrapped
|
||||
dash_cls = " sk-dashed-wrapped" if dashed_wrapped else ""
|
||||
out.write(f'<div class="sk-item{dash_cls}">')
|
||||
|
||||
if estimator_label:
|
||||
_write_label_html(
|
||||
out,
|
||||
estimator_label,
|
||||
estimator_label_details,
|
||||
doc_link=doc_link,
|
||||
is_fitted_css_class=is_fitted_css_class,
|
||||
is_fitted_icon=is_fitted_icon,
|
||||
)
|
||||
|
||||
kind = est_block.kind
|
||||
out.write(f'<div class="sk-{kind}">')
|
||||
est_infos = zip(est_block.estimators, est_block.names, est_block.name_details)
|
||||
|
||||
for est, name, name_details in est_infos:
|
||||
if kind == "serial":
|
||||
_write_estimator_html(
|
||||
out,
|
||||
est,
|
||||
name,
|
||||
name_details,
|
||||
is_fitted_css_class=is_fitted_css_class,
|
||||
)
|
||||
else: # parallel
|
||||
out.write('<div class="sk-parallel-item">')
|
||||
# wrap element in a serial visualblock
|
||||
serial_block = _VisualBlock("serial", [est], dash_wrapped=False)
|
||||
_write_estimator_html(
|
||||
out,
|
||||
serial_block,
|
||||
name,
|
||||
name_details,
|
||||
is_fitted_css_class=is_fitted_css_class,
|
||||
)
|
||||
out.write("</div>") # sk-parallel-item
|
||||
|
||||
out.write("</div></div>")
|
||||
elif est_block.kind == "single":
|
||||
_write_label_html(
|
||||
out,
|
||||
est_block.names,
|
||||
est_block.name_details,
|
||||
outer_class="sk-item",
|
||||
inner_class="sk-estimator",
|
||||
checked=first_call,
|
||||
doc_link=doc_link,
|
||||
is_fitted_css_class=is_fitted_css_class,
|
||||
is_fitted_icon=is_fitted_icon,
|
||||
)
|
||||
|
||||
|
||||
def estimator_html_repr(estimator):
|
||||
"""Build a HTML representation of an estimator.
|
||||
|
||||
Read more in the :ref:`User Guide <visualizing_composite_estimators>`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : estimator object
|
||||
The estimator to visualize.
|
||||
|
||||
Returns
|
||||
-------
|
||||
html: str
|
||||
HTML representation of estimator.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils._estimator_html_repr import estimator_html_repr
|
||||
>>> from sklearn.linear_model import LogisticRegression
|
||||
>>> estimator_html_repr(LogisticRegression())
|
||||
'<style>...</div>'
|
||||
"""
|
||||
from sklearn.exceptions import NotFittedError
|
||||
from sklearn.utils.validation import check_is_fitted
|
||||
|
||||
if not hasattr(estimator, "fit"):
|
||||
status_label = "<span>Not fitted</span>"
|
||||
is_fitted_css_class = ""
|
||||
else:
|
||||
try:
|
||||
check_is_fitted(estimator)
|
||||
status_label = "<span>Fitted</span>"
|
||||
is_fitted_css_class = "fitted"
|
||||
except NotFittedError:
|
||||
status_label = "<span>Not fitted</span>"
|
||||
is_fitted_css_class = ""
|
||||
|
||||
is_fitted_icon = (
|
||||
f'<span class="sk-estimator-doc-link {is_fitted_css_class}">'
|
||||
f"i{status_label}</span>"
|
||||
)
|
||||
with closing(StringIO()) as out:
|
||||
container_id = _CONTAINER_ID_COUNTER.get_id()
|
||||
style_template = Template(_CSS_STYLE)
|
||||
style_with_id = style_template.substitute(id=container_id)
|
||||
estimator_str = str(estimator)
|
||||
|
||||
# The fallback message is shown by default and loading the CSS sets
|
||||
# div.sk-text-repr-fallback to display: none to hide the fallback message.
|
||||
#
|
||||
# If the notebook is trusted, the CSS is loaded which hides the fallback
|
||||
# message. If the notebook is not trusted, then the CSS is not loaded and the
|
||||
# fallback message is shown by default.
|
||||
#
|
||||
# The reverse logic applies to HTML repr div.sk-container.
|
||||
# div.sk-container is hidden by default and the loading the CSS displays it.
|
||||
fallback_msg = (
|
||||
"In a Jupyter environment, please rerun this cell to show the HTML"
|
||||
" representation or trust the notebook. <br />On GitHub, the"
|
||||
" HTML representation is unable to render, please try loading this page"
|
||||
" with nbviewer.org."
|
||||
)
|
||||
html_template = (
|
||||
f"<style>{style_with_id}</style>"
|
||||
f'<div id="{container_id}" class="sk-top-container">'
|
||||
'<div class="sk-text-repr-fallback">'
|
||||
f"<pre>{html.escape(estimator_str)}</pre><b>{fallback_msg}</b>"
|
||||
"</div>"
|
||||
'<div class="sk-container" hidden>'
|
||||
)
|
||||
|
||||
out.write(html_template)
|
||||
|
||||
_write_estimator_html(
|
||||
out,
|
||||
estimator,
|
||||
estimator.__class__.__name__,
|
||||
estimator_str,
|
||||
first_call=True,
|
||||
is_fitted_css_class=is_fitted_css_class,
|
||||
is_fitted_icon=is_fitted_icon,
|
||||
)
|
||||
out.write("</div></div>")
|
||||
|
||||
html_output = out.getvalue()
|
||||
return html_output
|
||||
|
||||
|
||||
class _HTMLDocumentationLinkMixin:
|
||||
"""Mixin class allowing to generate a link to the API documentation.
|
||||
|
||||
This mixin relies on three attributes:
|
||||
- `_doc_link_module`: it corresponds to the root module (e.g. `sklearn`). Using this
|
||||
mixin, the default value is `sklearn`.
|
||||
- `_doc_link_template`: it corresponds to the template used to generate the
|
||||
link to the API documentation. Using this mixin, the default value is
|
||||
`"https://scikit-learn.org/{version_url}/modules/generated/
|
||||
{estimator_module}.{estimator_name}.html"`.
|
||||
- `_doc_link_url_param_generator`: it corresponds to a function that generates the
|
||||
parameters to be used in the template when the estimator module and name are not
|
||||
sufficient.
|
||||
|
||||
The method :meth:`_get_doc_link` generates the link to the API documentation for a
|
||||
given estimator.
|
||||
|
||||
This useful provides all the necessary states for
|
||||
:func:`sklearn.utils.estimator_html_repr` to generate a link to the API
|
||||
documentation for the estimator HTML diagram.
|
||||
|
||||
Examples
|
||||
--------
|
||||
If the default values for `_doc_link_module`, `_doc_link_template` are not suitable,
|
||||
then you can override them:
|
||||
>>> from sklearn.base import BaseEstimator
|
||||
>>> estimator = BaseEstimator()
|
||||
>>> estimator._doc_link_template = "https://website.com/{single_param}.html"
|
||||
>>> def url_param_generator(estimator):
|
||||
... return {"single_param": estimator.__class__.__name__}
|
||||
>>> estimator._doc_link_url_param_generator = url_param_generator
|
||||
>>> estimator._get_doc_link()
|
||||
'https://website.com/BaseEstimator.html'
|
||||
"""
|
||||
|
||||
_doc_link_module = "sklearn"
|
||||
_doc_link_url_param_generator = None
|
||||
|
||||
@property
|
||||
def _doc_link_template(self):
|
||||
sklearn_version = parse_version(__version__)
|
||||
if sklearn_version.dev is None:
|
||||
version_url = f"{sklearn_version.major}.{sklearn_version.minor}"
|
||||
else:
|
||||
version_url = "dev"
|
||||
return getattr(
|
||||
self,
|
||||
"__doc_link_template",
|
||||
(
|
||||
f"https://scikit-learn.org/{version_url}/modules/generated/"
|
||||
"{estimator_module}.{estimator_name}.html"
|
||||
),
|
||||
)
|
||||
|
||||
@_doc_link_template.setter
|
||||
def _doc_link_template(self, value):
|
||||
setattr(self, "__doc_link_template", value)
|
||||
|
||||
def _get_doc_link(self):
|
||||
"""Generates a link to the API documentation for a given estimator.
|
||||
|
||||
This method generates the link to the estimator's documentation page
|
||||
by using the template defined by the attribute `_doc_link_template`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
url : str
|
||||
The URL to the API documentation for this estimator. If the estimator does
|
||||
not belong to module `_doc_link_module`, the empty string (i.e. `""`) is
|
||||
returned.
|
||||
"""
|
||||
if self.__class__.__module__.split(".")[0] != self._doc_link_module:
|
||||
return ""
|
||||
|
||||
if self._doc_link_url_param_generator is None:
|
||||
estimator_name = self.__class__.__name__
|
||||
# Construct the estimator's module name, up to the first private submodule.
|
||||
# This works because in scikit-learn all public estimators are exposed at
|
||||
# that level, even if they actually live in a private sub-module.
|
||||
estimator_module = ".".join(
|
||||
itertools.takewhile(
|
||||
lambda part: not part.startswith("_"),
|
||||
self.__class__.__module__.split("."),
|
||||
)
|
||||
)
|
||||
return self._doc_link_template.format(
|
||||
estimator_module=estimator_module, estimator_name=estimator_name
|
||||
)
|
||||
return self._doc_link_template.format(
|
||||
**self._doc_link_url_param_generator(self)
|
||||
)
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
# Author: Gael Varoquaux
|
||||
# License: BSD
|
||||
"""
|
||||
Uses C++ map containers for fast dict-like behavior with keys being
|
||||
integers, and values float.
|
||||
"""
|
||||
|
||||
from libcpp.map cimport map as cpp_map
|
||||
|
||||
from ._typedefs cimport float64_t, intp_t
|
||||
|
||||
|
||||
###############################################################################
|
||||
# An object to be used in Python
|
||||
|
||||
cdef class IntFloatDict:
|
||||
cdef cpp_map[intp_t, float64_t] my_map
|
||||
cdef _to_arrays(self, intp_t [:] keys, float64_t [:] values)
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
# Heap routines, used in various Cython implementations.
|
||||
|
||||
from cython cimport floating
|
||||
|
||||
from ._typedefs cimport intp_t
|
||||
|
||||
|
||||
cdef int heap_push(
|
||||
floating* values,
|
||||
intp_t* indices,
|
||||
intp_t size,
|
||||
floating val,
|
||||
intp_t val_idx,
|
||||
) noexcept nogil
|
||||
Binary file not shown.
@@ -0,0 +1,38 @@
|
||||
import warnings as _warnings
|
||||
|
||||
with _warnings.catch_warnings():
|
||||
_warnings.simplefilter("ignore")
|
||||
# joblib imports may raise DeprecationWarning on certain Python
|
||||
# versions
|
||||
import joblib
|
||||
from joblib import (
|
||||
Memory,
|
||||
Parallel,
|
||||
__version__,
|
||||
cpu_count,
|
||||
delayed,
|
||||
dump,
|
||||
effective_n_jobs,
|
||||
hash,
|
||||
load,
|
||||
logger,
|
||||
parallel_backend,
|
||||
register_parallel_backend,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"parallel_backend",
|
||||
"register_parallel_backend",
|
||||
"cpu_count",
|
||||
"Parallel",
|
||||
"Memory",
|
||||
"delayed",
|
||||
"effective_n_jobs",
|
||||
"hash",
|
||||
"logger",
|
||||
"dump",
|
||||
"load",
|
||||
"joblib",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
from contextlib import suppress
|
||||
|
||||
import numpy as np
|
||||
from scipy import sparse as sp
|
||||
|
||||
from . import is_scalar_nan
|
||||
from .fixes import _object_dtype_isnan
|
||||
|
||||
|
||||
def _get_dense_mask(X, value_to_mask):
|
||||
with suppress(ImportError, AttributeError):
|
||||
# We also suppress `AttributeError` because older versions of pandas do
|
||||
# not have `NA`.
|
||||
import pandas
|
||||
|
||||
if value_to_mask is pandas.NA:
|
||||
return pandas.isna(X)
|
||||
|
||||
if is_scalar_nan(value_to_mask):
|
||||
if X.dtype.kind == "f":
|
||||
Xt = np.isnan(X)
|
||||
elif X.dtype.kind in ("i", "u"):
|
||||
# can't have NaNs in integer array.
|
||||
Xt = np.zeros(X.shape, dtype=bool)
|
||||
else:
|
||||
# np.isnan does not work on object dtypes.
|
||||
Xt = _object_dtype_isnan(X)
|
||||
else:
|
||||
Xt = X == value_to_mask
|
||||
|
||||
return Xt
|
||||
|
||||
|
||||
def _get_mask(X, value_to_mask):
|
||||
"""Compute the boolean mask X == value_to_mask.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : {ndarray, sparse matrix} of shape (n_samples, n_features)
|
||||
Input data, where ``n_samples`` is the number of samples and
|
||||
``n_features`` is the number of features.
|
||||
|
||||
value_to_mask : {int, float}
|
||||
The value which is to be masked in X.
|
||||
|
||||
Returns
|
||||
-------
|
||||
X_mask : {ndarray, sparse matrix} of shape (n_samples, n_features)
|
||||
Missing mask.
|
||||
"""
|
||||
if not sp.issparse(X):
|
||||
# For all cases apart of a sparse input where we need to reconstruct
|
||||
# a sparse output
|
||||
return _get_dense_mask(X, value_to_mask)
|
||||
|
||||
Xt = _get_dense_mask(X.data, value_to_mask)
|
||||
|
||||
sparse_constructor = sp.csr_matrix if X.format == "csr" else sp.csc_matrix
|
||||
Xt_sparse = sparse_constructor(
|
||||
(Xt, X.indices.copy(), X.indptr.copy()), shape=X.shape, dtype=bool
|
||||
)
|
||||
|
||||
return Xt_sparse
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,400 @@
|
||||
import numpy as np
|
||||
|
||||
from ..base import BaseEstimator, ClassifierMixin
|
||||
from ..utils._metadata_requests import RequestMethod
|
||||
from .metaestimators import available_if
|
||||
from .validation import _check_sample_weight, _num_samples, check_array, check_is_fitted
|
||||
|
||||
|
||||
class ArraySlicingWrapper:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
array
|
||||
"""
|
||||
|
||||
def __init__(self, array):
|
||||
self.array = array
|
||||
|
||||
def __getitem__(self, aslice):
|
||||
return MockDataFrame(self.array[aslice])
|
||||
|
||||
|
||||
class MockDataFrame:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
array
|
||||
"""
|
||||
|
||||
# have shape and length but don't support indexing.
|
||||
|
||||
def __init__(self, array):
|
||||
self.array = array
|
||||
self.values = array
|
||||
self.shape = array.shape
|
||||
self.ndim = array.ndim
|
||||
# ugly hack to make iloc work.
|
||||
self.iloc = ArraySlicingWrapper(array)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.array)
|
||||
|
||||
def __array__(self, dtype=None):
|
||||
# Pandas data frames also are array-like: we want to make sure that
|
||||
# input validation in cross-validation does not try to call that
|
||||
# method.
|
||||
return self.array
|
||||
|
||||
def __eq__(self, other):
|
||||
return MockDataFrame(self.array == other.array)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def take(self, indices, axis=0):
|
||||
return MockDataFrame(self.array.take(indices, axis=axis))
|
||||
|
||||
|
||||
class CheckingClassifier(ClassifierMixin, BaseEstimator):
|
||||
"""Dummy classifier to test pipelining and meta-estimators.
|
||||
|
||||
Checks some property of `X` and `y`in fit / predict.
|
||||
This allows testing whether pipelines / cross-validation or metaestimators
|
||||
changed the input.
|
||||
|
||||
Can also be used to check if `fit_params` are passed correctly, and
|
||||
to force a certain score to be returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
check_y, check_X : callable, default=None
|
||||
The callable used to validate `X` and `y`. These callable should return
|
||||
a bool where `False` will trigger an `AssertionError`. If `None`, the
|
||||
data is not validated. Default is `None`.
|
||||
|
||||
check_y_params, check_X_params : dict, default=None
|
||||
The optional parameters to pass to `check_X` and `check_y`. If `None`,
|
||||
then no parameters are passed in.
|
||||
|
||||
methods_to_check : "all" or list of str, default="all"
|
||||
The methods in which the checks should be applied. By default,
|
||||
all checks will be done on all methods (`fit`, `predict`,
|
||||
`predict_proba`, `decision_function` and `score`).
|
||||
|
||||
foo_param : int, default=0
|
||||
A `foo` param. When `foo > 1`, the output of :meth:`score` will be 1
|
||||
otherwise it is 0.
|
||||
|
||||
expected_sample_weight : bool, default=False
|
||||
Whether to check if a valid `sample_weight` was passed to `fit`.
|
||||
|
||||
expected_fit_params : list of str, default=None
|
||||
A list of the expected parameters given when calling `fit`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
classes_ : int
|
||||
The classes seen during `fit`.
|
||||
|
||||
n_features_in_ : int
|
||||
The number of features seen during `fit`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils._mocking import CheckingClassifier
|
||||
|
||||
This helper allow to assert to specificities regarding `X` or `y`. In this
|
||||
case we expect `check_X` or `check_y` to return a boolean.
|
||||
|
||||
>>> from sklearn.datasets import load_iris
|
||||
>>> X, y = load_iris(return_X_y=True)
|
||||
>>> clf = CheckingClassifier(check_X=lambda x: x.shape == (150, 4))
|
||||
>>> clf.fit(X, y)
|
||||
CheckingClassifier(...)
|
||||
|
||||
We can also provide a check which might raise an error. In this case, we
|
||||
expect `check_X` to return `X` and `check_y` to return `y`.
|
||||
|
||||
>>> from sklearn.utils import check_array
|
||||
>>> clf = CheckingClassifier(check_X=check_array)
|
||||
>>> clf.fit(X, y)
|
||||
CheckingClassifier(...)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
check_y=None,
|
||||
check_y_params=None,
|
||||
check_X=None,
|
||||
check_X_params=None,
|
||||
methods_to_check="all",
|
||||
foo_param=0,
|
||||
expected_sample_weight=None,
|
||||
expected_fit_params=None,
|
||||
):
|
||||
self.check_y = check_y
|
||||
self.check_y_params = check_y_params
|
||||
self.check_X = check_X
|
||||
self.check_X_params = check_X_params
|
||||
self.methods_to_check = methods_to_check
|
||||
self.foo_param = foo_param
|
||||
self.expected_sample_weight = expected_sample_weight
|
||||
self.expected_fit_params = expected_fit_params
|
||||
|
||||
def _check_X_y(self, X, y=None, should_be_fitted=True):
|
||||
"""Validate X and y and make extra check.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : array-like of shape (n_samples, n_features)
|
||||
The data set.
|
||||
`X` is checked only if `check_X` is not `None` (default is None).
|
||||
y : array-like of shape (n_samples), default=None
|
||||
The corresponding target, by default `None`.
|
||||
`y` is checked only if `check_y` is not `None` (default is None).
|
||||
should_be_fitted : bool, default=True
|
||||
Whether or not the classifier should be already fitted.
|
||||
By default True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
X, y
|
||||
"""
|
||||
if should_be_fitted:
|
||||
check_is_fitted(self)
|
||||
if self.check_X is not None:
|
||||
params = {} if self.check_X_params is None else self.check_X_params
|
||||
checked_X = self.check_X(X, **params)
|
||||
if isinstance(checked_X, (bool, np.bool_)):
|
||||
assert checked_X
|
||||
else:
|
||||
X = checked_X
|
||||
if y is not None and self.check_y is not None:
|
||||
params = {} if self.check_y_params is None else self.check_y_params
|
||||
checked_y = self.check_y(y, **params)
|
||||
if isinstance(checked_y, (bool, np.bool_)):
|
||||
assert checked_y
|
||||
else:
|
||||
y = checked_y
|
||||
return X, y
|
||||
|
||||
def fit(self, X, y, sample_weight=None, **fit_params):
|
||||
"""Fit classifier.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : array-like of shape (n_samples, n_features)
|
||||
Training vector, where `n_samples` is the number of samples and
|
||||
`n_features` is the number of features.
|
||||
|
||||
y : array-like of shape (n_samples, n_outputs) or (n_samples,), \
|
||||
default=None
|
||||
Target relative to X for classification or regression;
|
||||
None for unsupervised learning.
|
||||
|
||||
sample_weight : array-like of shape (n_samples,), default=None
|
||||
Sample weights. If None, then samples are equally weighted.
|
||||
|
||||
**fit_params : dict of string -> object
|
||||
Parameters passed to the ``fit`` method of the estimator
|
||||
|
||||
Returns
|
||||
-------
|
||||
self
|
||||
"""
|
||||
assert _num_samples(X) == _num_samples(y)
|
||||
if self.methods_to_check == "all" or "fit" in self.methods_to_check:
|
||||
X, y = self._check_X_y(X, y, should_be_fitted=False)
|
||||
self.n_features_in_ = np.shape(X)[1]
|
||||
self.classes_ = np.unique(check_array(y, ensure_2d=False, allow_nd=True))
|
||||
if self.expected_fit_params:
|
||||
missing = set(self.expected_fit_params) - set(fit_params)
|
||||
if missing:
|
||||
raise AssertionError(
|
||||
f"Expected fit parameter(s) {list(missing)} not seen."
|
||||
)
|
||||
for key, value in fit_params.items():
|
||||
if _num_samples(value) != _num_samples(X):
|
||||
raise AssertionError(
|
||||
f"Fit parameter {key} has length {_num_samples(value)}"
|
||||
f"; expected {_num_samples(X)}."
|
||||
)
|
||||
if self.expected_sample_weight:
|
||||
if sample_weight is None:
|
||||
raise AssertionError("Expected sample_weight to be passed")
|
||||
_check_sample_weight(sample_weight, X)
|
||||
|
||||
return self
|
||||
|
||||
def predict(self, X):
|
||||
"""Predict the first class seen in `classes_`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : array-like of shape (n_samples, n_features)
|
||||
The input data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
preds : ndarray of shape (n_samples,)
|
||||
Predictions of the first class seens in `classes_`.
|
||||
"""
|
||||
if self.methods_to_check == "all" or "predict" in self.methods_to_check:
|
||||
X, y = self._check_X_y(X)
|
||||
return self.classes_[np.zeros(_num_samples(X), dtype=int)]
|
||||
|
||||
def predict_proba(self, X):
|
||||
"""Predict probabilities for each class.
|
||||
|
||||
Here, the dummy classifier will provide a probability of 1 for the
|
||||
first class of `classes_` and 0 otherwise.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : array-like of shape (n_samples, n_features)
|
||||
The input data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
proba : ndarray of shape (n_samples, n_classes)
|
||||
The probabilities for each sample and class.
|
||||
"""
|
||||
if self.methods_to_check == "all" or "predict_proba" in self.methods_to_check:
|
||||
X, y = self._check_X_y(X)
|
||||
proba = np.zeros((_num_samples(X), len(self.classes_)))
|
||||
proba[:, 0] = 1
|
||||
return proba
|
||||
|
||||
def decision_function(self, X):
|
||||
"""Confidence score.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : array-like of shape (n_samples, n_features)
|
||||
The input data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
decision : ndarray of shape (n_samples,) if n_classes == 2\
|
||||
else (n_samples, n_classes)
|
||||
Confidence score.
|
||||
"""
|
||||
if (
|
||||
self.methods_to_check == "all"
|
||||
or "decision_function" in self.methods_to_check
|
||||
):
|
||||
X, y = self._check_X_y(X)
|
||||
if len(self.classes_) == 2:
|
||||
# for binary classifier, the confidence score is related to
|
||||
# classes_[1] and therefore should be null.
|
||||
return np.zeros(_num_samples(X))
|
||||
else:
|
||||
decision = np.zeros((_num_samples(X), len(self.classes_)))
|
||||
decision[:, 0] = 1
|
||||
return decision
|
||||
|
||||
def score(self, X=None, Y=None):
|
||||
"""Fake score.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : array-like of shape (n_samples, n_features)
|
||||
Input data, where `n_samples` is the number of samples and
|
||||
`n_features` is the number of features.
|
||||
|
||||
Y : array-like of shape (n_samples, n_output) or (n_samples,)
|
||||
Target relative to X for classification or regression;
|
||||
None for unsupervised learning.
|
||||
|
||||
Returns
|
||||
-------
|
||||
score : float
|
||||
Either 0 or 1 depending of `foo_param` (i.e. `foo_param > 1 =>
|
||||
score=1` otherwise `score=0`).
|
||||
"""
|
||||
if self.methods_to_check == "all" or "score" in self.methods_to_check:
|
||||
self._check_X_y(X, Y)
|
||||
if self.foo_param > 1:
|
||||
score = 1.0
|
||||
else:
|
||||
score = 0.0
|
||||
return score
|
||||
|
||||
def _more_tags(self):
|
||||
return {"_skip_test": True, "X_types": ["1dlabel"]}
|
||||
|
||||
|
||||
# Deactivate key validation for CheckingClassifier because we want to be able to
|
||||
# call fit with arbitrary fit_params and record them. Without this change, we
|
||||
# would get an error because those arbitrary params are not expected.
|
||||
CheckingClassifier.set_fit_request = RequestMethod( # type: ignore
|
||||
name="fit", keys=[], validate_keys=False
|
||||
)
|
||||
|
||||
|
||||
class NoSampleWeightWrapper(BaseEstimator):
|
||||
"""Wrap estimator which will not expose `sample_weight`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
est : estimator, default=None
|
||||
The estimator to wrap.
|
||||
"""
|
||||
|
||||
def __init__(self, est=None):
|
||||
self.est = est
|
||||
|
||||
def fit(self, X, y):
|
||||
return self.est.fit(X, y)
|
||||
|
||||
def predict(self, X):
|
||||
return self.est.predict(X)
|
||||
|
||||
def predict_proba(self, X):
|
||||
return self.est.predict_proba(X)
|
||||
|
||||
def _more_tags(self):
|
||||
return {"_skip_test": True}
|
||||
|
||||
|
||||
def _check_response(method):
|
||||
def check(self):
|
||||
return self.response_methods is not None and method in self.response_methods
|
||||
|
||||
return check
|
||||
|
||||
|
||||
class _MockEstimatorOnOffPrediction(BaseEstimator):
|
||||
"""Estimator for which we can turn on/off the prediction methods.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
response_methods: list of \
|
||||
{"predict", "predict_proba", "decision_function"}, default=None
|
||||
List containing the response implemented by the estimator. When, the
|
||||
response is in the list, it will return the name of the response method
|
||||
when called. Otherwise, an `AttributeError` is raised. It allows to
|
||||
use `getattr` as any conventional estimator. By default, no response
|
||||
methods are mocked.
|
||||
"""
|
||||
|
||||
def __init__(self, response_methods=None):
|
||||
self.response_methods = response_methods
|
||||
|
||||
def fit(self, X, y):
|
||||
self.classes_ = np.unique(y)
|
||||
return self
|
||||
|
||||
@available_if(_check_response("predict"))
|
||||
def predict(self, X):
|
||||
return "predict"
|
||||
|
||||
@available_if(_check_response("predict_proba"))
|
||||
def predict_proba(self, X):
|
||||
return "predict_proba"
|
||||
|
||||
@available_if(_check_response("decision_function"))
|
||||
def decision_function(self, X):
|
||||
return "decision_function"
|
||||
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
# Helpers to safely access OpenMP routines
|
||||
#
|
||||
# no-op implementations are provided for the case where OpenMP is not available.
|
||||
#
|
||||
# All calls to OpenMP routines should be cimported from this module.
|
||||
|
||||
cdef extern from *:
|
||||
"""
|
||||
#ifdef _OPENMP
|
||||
#include <omp.h>
|
||||
#define SKLEARN_OPENMP_PARALLELISM_ENABLED 1
|
||||
#else
|
||||
#define SKLEARN_OPENMP_PARALLELISM_ENABLED 0
|
||||
#define omp_lock_t int
|
||||
#define omp_init_lock(l) (void)0
|
||||
#define omp_destroy_lock(l) (void)0
|
||||
#define omp_set_lock(l) (void)0
|
||||
#define omp_unset_lock(l) (void)0
|
||||
#define omp_get_thread_num() 0
|
||||
#define omp_get_max_threads() 1
|
||||
#endif
|
||||
"""
|
||||
bint SKLEARN_OPENMP_PARALLELISM_ENABLED
|
||||
|
||||
ctypedef struct omp_lock_t:
|
||||
pass
|
||||
|
||||
void omp_init_lock(omp_lock_t*) noexcept nogil
|
||||
void omp_destroy_lock(omp_lock_t*) noexcept nogil
|
||||
void omp_set_lock(omp_lock_t*) noexcept nogil
|
||||
void omp_unset_lock(omp_lock_t*) noexcept nogil
|
||||
int omp_get_thread_num() noexcept nogil
|
||||
int omp_get_max_threads() noexcept nogil
|
||||
@@ -0,0 +1,905 @@
|
||||
import functools
|
||||
import math
|
||||
import operator
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterable
|
||||
from inspect import signature
|
||||
from numbers import Integral, Real
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import csr_matrix, issparse
|
||||
|
||||
from .._config import config_context, get_config
|
||||
from .validation import _is_arraylike_not_scalar
|
||||
|
||||
|
||||
class InvalidParameterError(ValueError, TypeError):
|
||||
"""Custom exception to be raised when the parameter of a class/method/function
|
||||
does not have a valid type or value.
|
||||
"""
|
||||
|
||||
# Inherits from ValueError and TypeError to keep backward compatibility.
|
||||
|
||||
|
||||
def validate_parameter_constraints(parameter_constraints, params, caller_name):
|
||||
"""Validate types and values of given parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parameter_constraints : dict or {"no_validation"}
|
||||
If "no_validation", validation is skipped for this parameter.
|
||||
|
||||
If a dict, it must be a dictionary `param_name: list of constraints`.
|
||||
A parameter is valid if it satisfies one of the constraints from the list.
|
||||
Constraints can be:
|
||||
- an Interval object, representing a continuous or discrete range of numbers
|
||||
- the string "array-like"
|
||||
- the string "sparse matrix"
|
||||
- the string "random_state"
|
||||
- callable
|
||||
- None, meaning that None is a valid value for the parameter
|
||||
- any type, meaning that any instance of this type is valid
|
||||
- an Options object, representing a set of elements of a given type
|
||||
- a StrOptions object, representing a set of strings
|
||||
- the string "boolean"
|
||||
- the string "verbose"
|
||||
- the string "cv_object"
|
||||
- the string "nan"
|
||||
- a MissingValues object representing markers for missing values
|
||||
- a HasMethods object, representing method(s) an object must have
|
||||
- a Hidden object, representing a constraint not meant to be exposed to the user
|
||||
|
||||
params : dict
|
||||
A dictionary `param_name: param_value`. The parameters to validate against the
|
||||
constraints.
|
||||
|
||||
caller_name : str
|
||||
The name of the estimator or function or method that called this function.
|
||||
"""
|
||||
for param_name, param_val in params.items():
|
||||
# We allow parameters to not have a constraint so that third party estimators
|
||||
# can inherit from sklearn estimators without having to necessarily use the
|
||||
# validation tools.
|
||||
if param_name not in parameter_constraints:
|
||||
continue
|
||||
|
||||
constraints = parameter_constraints[param_name]
|
||||
|
||||
if constraints == "no_validation":
|
||||
continue
|
||||
|
||||
constraints = [make_constraint(constraint) for constraint in constraints]
|
||||
|
||||
for constraint in constraints:
|
||||
if constraint.is_satisfied_by(param_val):
|
||||
# this constraint is satisfied, no need to check further.
|
||||
break
|
||||
else:
|
||||
# No constraint is satisfied, raise with an informative message.
|
||||
|
||||
# Ignore constraints that we don't want to expose in the error message,
|
||||
# i.e. options that are for internal purpose or not officially supported.
|
||||
constraints = [
|
||||
constraint for constraint in constraints if not constraint.hidden
|
||||
]
|
||||
|
||||
if len(constraints) == 1:
|
||||
constraints_str = f"{constraints[0]}"
|
||||
else:
|
||||
constraints_str = (
|
||||
f"{', '.join([str(c) for c in constraints[:-1]])} or"
|
||||
f" {constraints[-1]}"
|
||||
)
|
||||
|
||||
raise InvalidParameterError(
|
||||
f"The {param_name!r} parameter of {caller_name} must be"
|
||||
f" {constraints_str}. Got {param_val!r} instead."
|
||||
)
|
||||
|
||||
|
||||
def make_constraint(constraint):
|
||||
"""Convert the constraint into the appropriate Constraint object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
constraint : object
|
||||
The constraint to convert.
|
||||
|
||||
Returns
|
||||
-------
|
||||
constraint : instance of _Constraint
|
||||
The converted constraint.
|
||||
"""
|
||||
if isinstance(constraint, str) and constraint == "array-like":
|
||||
return _ArrayLikes()
|
||||
if isinstance(constraint, str) and constraint == "sparse matrix":
|
||||
return _SparseMatrices()
|
||||
if isinstance(constraint, str) and constraint == "random_state":
|
||||
return _RandomStates()
|
||||
if constraint is callable:
|
||||
return _Callables()
|
||||
if constraint is None:
|
||||
return _NoneConstraint()
|
||||
if isinstance(constraint, type):
|
||||
return _InstancesOf(constraint)
|
||||
if isinstance(
|
||||
constraint, (Interval, StrOptions, Options, HasMethods, MissingValues)
|
||||
):
|
||||
return constraint
|
||||
if isinstance(constraint, str) and constraint == "boolean":
|
||||
return _Booleans()
|
||||
if isinstance(constraint, str) and constraint == "verbose":
|
||||
return _VerboseHelper()
|
||||
if isinstance(constraint, str) and constraint == "cv_object":
|
||||
return _CVObjects()
|
||||
if isinstance(constraint, Hidden):
|
||||
constraint = make_constraint(constraint.constraint)
|
||||
constraint.hidden = True
|
||||
return constraint
|
||||
if isinstance(constraint, str) and constraint == "nan":
|
||||
return _NanConstraint()
|
||||
raise ValueError(f"Unknown constraint type: {constraint}")
|
||||
|
||||
|
||||
def validate_params(parameter_constraints, *, prefer_skip_nested_validation):
|
||||
"""Decorator to validate types and values of functions and methods.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parameter_constraints : dict
|
||||
A dictionary `param_name: list of constraints`. See the docstring of
|
||||
`validate_parameter_constraints` for a description of the accepted constraints.
|
||||
|
||||
Note that the *args and **kwargs parameters are not validated and must not be
|
||||
present in the parameter_constraints dictionary.
|
||||
|
||||
prefer_skip_nested_validation : bool
|
||||
If True, the validation of parameters of inner estimators or functions
|
||||
called by the decorated function will be skipped.
|
||||
|
||||
This is useful to avoid validating many times the parameters passed by the
|
||||
user from the public facing API. It's also useful to avoid validating
|
||||
parameters that we pass internally to inner functions that are guaranteed to
|
||||
be valid by the test suite.
|
||||
|
||||
It should be set to True for most functions, except for those that receive
|
||||
non-validated objects as parameters or that are just wrappers around classes
|
||||
because they only perform a partial validation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
decorated_function : function or method
|
||||
The decorated function.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
# The dict of parameter constraints is set as an attribute of the function
|
||||
# to make it possible to dynamically introspect the constraints for
|
||||
# automatic testing.
|
||||
setattr(func, "_skl_parameter_constraints", parameter_constraints)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
global_skip_validation = get_config()["skip_parameter_validation"]
|
||||
if global_skip_validation:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
func_sig = signature(func)
|
||||
|
||||
# Map *args/**kwargs to the function signature
|
||||
params = func_sig.bind(*args, **kwargs)
|
||||
params.apply_defaults()
|
||||
|
||||
# ignore self/cls and positional/keyword markers
|
||||
to_ignore = [
|
||||
p.name
|
||||
for p in func_sig.parameters.values()
|
||||
if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
|
||||
]
|
||||
to_ignore += ["self", "cls"]
|
||||
params = {k: v for k, v in params.arguments.items() if k not in to_ignore}
|
||||
|
||||
validate_parameter_constraints(
|
||||
parameter_constraints, params, caller_name=func.__qualname__
|
||||
)
|
||||
|
||||
try:
|
||||
with config_context(
|
||||
skip_parameter_validation=(
|
||||
prefer_skip_nested_validation or global_skip_validation
|
||||
)
|
||||
):
|
||||
return func(*args, **kwargs)
|
||||
except InvalidParameterError as e:
|
||||
# When the function is just a wrapper around an estimator, we allow
|
||||
# the function to delegate validation to the estimator, but we replace
|
||||
# the name of the estimator by the name of the function in the error
|
||||
# message to avoid confusion.
|
||||
msg = re.sub(
|
||||
r"parameter of \w+ must be",
|
||||
f"parameter of {func.__qualname__} must be",
|
||||
str(e),
|
||||
)
|
||||
raise InvalidParameterError(msg) from e
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class RealNotInt(Real):
|
||||
"""A type that represents reals that are not instances of int.
|
||||
|
||||
Behaves like float, but also works with values extracted from numpy arrays.
|
||||
isintance(1, RealNotInt) -> False
|
||||
isinstance(1.0, RealNotInt) -> True
|
||||
"""
|
||||
|
||||
|
||||
RealNotInt.register(float)
|
||||
|
||||
|
||||
def _type_name(t):
|
||||
"""Convert type into human readable string."""
|
||||
module = t.__module__
|
||||
qualname = t.__qualname__
|
||||
if module == "builtins":
|
||||
return qualname
|
||||
elif t == Real:
|
||||
return "float"
|
||||
elif t == Integral:
|
||||
return "int"
|
||||
return f"{module}.{qualname}"
|
||||
|
||||
|
||||
class _Constraint(ABC):
|
||||
"""Base class for the constraint objects."""
|
||||
|
||||
def __init__(self):
|
||||
self.hidden = False
|
||||
|
||||
@abstractmethod
|
||||
def is_satisfied_by(self, val):
|
||||
"""Whether or not a value satisfies the constraint.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
val : object
|
||||
The value to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_satisfied : bool
|
||||
Whether or not the constraint is satisfied by this value.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
"""A human readable representational string of the constraint."""
|
||||
|
||||
|
||||
class _InstancesOf(_Constraint):
|
||||
"""Constraint representing instances of a given type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
type : type
|
||||
The valid type.
|
||||
"""
|
||||
|
||||
def __init__(self, type):
|
||||
super().__init__()
|
||||
self.type = type
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return isinstance(val, self.type)
|
||||
|
||||
def __str__(self):
|
||||
return f"an instance of {_type_name(self.type)!r}"
|
||||
|
||||
|
||||
class _NoneConstraint(_Constraint):
|
||||
"""Constraint representing the None singleton."""
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return val is None
|
||||
|
||||
def __str__(self):
|
||||
return "None"
|
||||
|
||||
|
||||
class _NanConstraint(_Constraint):
|
||||
"""Constraint representing the indicator `np.nan`."""
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return (
|
||||
not isinstance(val, Integral) and isinstance(val, Real) and math.isnan(val)
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "numpy.nan"
|
||||
|
||||
|
||||
class _PandasNAConstraint(_Constraint):
|
||||
"""Constraint representing the indicator `pd.NA`."""
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
try:
|
||||
import pandas as pd
|
||||
|
||||
return isinstance(val, type(pd.NA)) and pd.isna(val)
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return "pandas.NA"
|
||||
|
||||
|
||||
class Options(_Constraint):
|
||||
"""Constraint representing a finite set of instances of a given type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
type : type
|
||||
|
||||
options : set
|
||||
The set of valid scalars.
|
||||
|
||||
deprecated : set or None, default=None
|
||||
A subset of the `options` to mark as deprecated in the string
|
||||
representation of the constraint.
|
||||
"""
|
||||
|
||||
def __init__(self, type, options, *, deprecated=None):
|
||||
super().__init__()
|
||||
self.type = type
|
||||
self.options = options
|
||||
self.deprecated = deprecated or set()
|
||||
|
||||
if self.deprecated - self.options:
|
||||
raise ValueError("The deprecated options must be a subset of the options.")
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return isinstance(val, self.type) and val in self.options
|
||||
|
||||
def _mark_if_deprecated(self, option):
|
||||
"""Add a deprecated mark to an option if needed."""
|
||||
option_str = f"{option!r}"
|
||||
if option in self.deprecated:
|
||||
option_str = f"{option_str} (deprecated)"
|
||||
return option_str
|
||||
|
||||
def __str__(self):
|
||||
options_str = (
|
||||
f"{', '.join([self._mark_if_deprecated(o) for o in self.options])}"
|
||||
)
|
||||
return f"a {_type_name(self.type)} among {{{options_str}}}"
|
||||
|
||||
|
||||
class StrOptions(Options):
|
||||
"""Constraint representing a finite set of strings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
options : set of str
|
||||
The set of valid strings.
|
||||
|
||||
deprecated : set of str or None, default=None
|
||||
A subset of the `options` to mark as deprecated in the string
|
||||
representation of the constraint.
|
||||
"""
|
||||
|
||||
def __init__(self, options, *, deprecated=None):
|
||||
super().__init__(type=str, options=options, deprecated=deprecated)
|
||||
|
||||
|
||||
class Interval(_Constraint):
|
||||
"""Constraint representing a typed interval.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
type : {numbers.Integral, numbers.Real, RealNotInt}
|
||||
The set of numbers in which to set the interval.
|
||||
|
||||
If RealNotInt, only reals that don't have the integer type
|
||||
are allowed. For example 1.0 is allowed but 1 is not.
|
||||
|
||||
left : float or int or None
|
||||
The left bound of the interval. None means left bound is -∞.
|
||||
|
||||
right : float, int or None
|
||||
The right bound of the interval. None means right bound is +∞.
|
||||
|
||||
closed : {"left", "right", "both", "neither"}
|
||||
Whether the interval is open or closed. Possible choices are:
|
||||
|
||||
- `"left"`: the interval is closed on the left and open on the right.
|
||||
It is equivalent to the interval `[ left, right )`.
|
||||
- `"right"`: the interval is closed on the right and open on the left.
|
||||
It is equivalent to the interval `( left, right ]`.
|
||||
- `"both"`: the interval is closed.
|
||||
It is equivalent to the interval `[ left, right ]`.
|
||||
- `"neither"`: the interval is open.
|
||||
It is equivalent to the interval `( left, right )`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Setting a bound to `None` and setting the interval closed is valid. For instance,
|
||||
strictly speaking, `Interval(Real, 0, None, closed="both")` corresponds to
|
||||
`[0, +∞) U {+∞}`.
|
||||
"""
|
||||
|
||||
def __init__(self, type, left, right, *, closed):
|
||||
super().__init__()
|
||||
self.type = type
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.closed = closed
|
||||
|
||||
self._check_params()
|
||||
|
||||
def _check_params(self):
|
||||
if self.type not in (Integral, Real, RealNotInt):
|
||||
raise ValueError(
|
||||
"type must be either numbers.Integral, numbers.Real or RealNotInt."
|
||||
f" Got {self.type} instead."
|
||||
)
|
||||
|
||||
if self.closed not in ("left", "right", "both", "neither"):
|
||||
raise ValueError(
|
||||
"closed must be either 'left', 'right', 'both' or 'neither'. "
|
||||
f"Got {self.closed} instead."
|
||||
)
|
||||
|
||||
if self.type is Integral:
|
||||
suffix = "for an interval over the integers."
|
||||
if self.left is not None and not isinstance(self.left, Integral):
|
||||
raise TypeError(f"Expecting left to be an int {suffix}")
|
||||
if self.right is not None and not isinstance(self.right, Integral):
|
||||
raise TypeError(f"Expecting right to be an int {suffix}")
|
||||
if self.left is None and self.closed in ("left", "both"):
|
||||
raise ValueError(
|
||||
f"left can't be None when closed == {self.closed} {suffix}"
|
||||
)
|
||||
if self.right is None and self.closed in ("right", "both"):
|
||||
raise ValueError(
|
||||
f"right can't be None when closed == {self.closed} {suffix}"
|
||||
)
|
||||
else:
|
||||
if self.left is not None and not isinstance(self.left, Real):
|
||||
raise TypeError("Expecting left to be a real number.")
|
||||
if self.right is not None and not isinstance(self.right, Real):
|
||||
raise TypeError("Expecting right to be a real number.")
|
||||
|
||||
if self.right is not None and self.left is not None and self.right <= self.left:
|
||||
raise ValueError(
|
||||
f"right can't be less than left. Got left={self.left} and "
|
||||
f"right={self.right}"
|
||||
)
|
||||
|
||||
def __contains__(self, val):
|
||||
if not isinstance(val, Integral) and np.isnan(val):
|
||||
return False
|
||||
|
||||
left_cmp = operator.lt if self.closed in ("left", "both") else operator.le
|
||||
right_cmp = operator.gt if self.closed in ("right", "both") else operator.ge
|
||||
|
||||
left = -np.inf if self.left is None else self.left
|
||||
right = np.inf if self.right is None else self.right
|
||||
|
||||
if left_cmp(val, left):
|
||||
return False
|
||||
if right_cmp(val, right):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
if not isinstance(val, self.type):
|
||||
return False
|
||||
|
||||
return val in self
|
||||
|
||||
def __str__(self):
|
||||
type_str = "an int" if self.type is Integral else "a float"
|
||||
left_bracket = "[" if self.closed in ("left", "both") else "("
|
||||
left_bound = "-inf" if self.left is None else self.left
|
||||
right_bound = "inf" if self.right is None else self.right
|
||||
right_bracket = "]" if self.closed in ("right", "both") else ")"
|
||||
|
||||
# better repr if the bounds were given as integers
|
||||
if not self.type == Integral and isinstance(self.left, Real):
|
||||
left_bound = float(left_bound)
|
||||
if not self.type == Integral and isinstance(self.right, Real):
|
||||
right_bound = float(right_bound)
|
||||
|
||||
return (
|
||||
f"{type_str} in the range "
|
||||
f"{left_bracket}{left_bound}, {right_bound}{right_bracket}"
|
||||
)
|
||||
|
||||
|
||||
class _ArrayLikes(_Constraint):
|
||||
"""Constraint representing array-likes"""
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return _is_arraylike_not_scalar(val)
|
||||
|
||||
def __str__(self):
|
||||
return "an array-like"
|
||||
|
||||
|
||||
class _SparseMatrices(_Constraint):
|
||||
"""Constraint representing sparse matrices."""
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return issparse(val)
|
||||
|
||||
def __str__(self):
|
||||
return "a sparse matrix"
|
||||
|
||||
|
||||
class _Callables(_Constraint):
|
||||
"""Constraint representing callables."""
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return callable(val)
|
||||
|
||||
def __str__(self):
|
||||
return "a callable"
|
||||
|
||||
|
||||
class _RandomStates(_Constraint):
|
||||
"""Constraint representing random states.
|
||||
|
||||
Convenience class for
|
||||
[Interval(Integral, 0, 2**32 - 1, closed="both"), np.random.RandomState, None]
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._constraints = [
|
||||
Interval(Integral, 0, 2**32 - 1, closed="both"),
|
||||
_InstancesOf(np.random.RandomState),
|
||||
_NoneConstraint(),
|
||||
]
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return any(c.is_satisfied_by(val) for c in self._constraints)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
|
||||
f" {self._constraints[-1]}"
|
||||
)
|
||||
|
||||
|
||||
class _Booleans(_Constraint):
|
||||
"""Constraint representing boolean likes.
|
||||
|
||||
Convenience class for
|
||||
[bool, np.bool_, Integral (deprecated)]
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._constraints = [
|
||||
_InstancesOf(bool),
|
||||
_InstancesOf(np.bool_),
|
||||
]
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return any(c.is_satisfied_by(val) for c in self._constraints)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
|
||||
f" {self._constraints[-1]}"
|
||||
)
|
||||
|
||||
|
||||
class _VerboseHelper(_Constraint):
|
||||
"""Helper constraint for the verbose parameter.
|
||||
|
||||
Convenience class for
|
||||
[Interval(Integral, 0, None, closed="left"), bool, numpy.bool_]
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._constraints = [
|
||||
Interval(Integral, 0, None, closed="left"),
|
||||
_InstancesOf(bool),
|
||||
_InstancesOf(np.bool_),
|
||||
]
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return any(c.is_satisfied_by(val) for c in self._constraints)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
|
||||
f" {self._constraints[-1]}"
|
||||
)
|
||||
|
||||
|
||||
class MissingValues(_Constraint):
|
||||
"""Helper constraint for the `missing_values` parameters.
|
||||
|
||||
Convenience for
|
||||
[
|
||||
Integral,
|
||||
Interval(Real, None, None, closed="both"),
|
||||
str, # when numeric_only is False
|
||||
None, # when numeric_only is False
|
||||
_NanConstraint(),
|
||||
_PandasNAConstraint(),
|
||||
]
|
||||
|
||||
Parameters
|
||||
----------
|
||||
numeric_only : bool, default=False
|
||||
Whether to consider only numeric missing value markers.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, numeric_only=False):
|
||||
super().__init__()
|
||||
|
||||
self.numeric_only = numeric_only
|
||||
|
||||
self._constraints = [
|
||||
_InstancesOf(Integral),
|
||||
# we use an interval of Real to ignore np.nan that has its own constraint
|
||||
Interval(Real, None, None, closed="both"),
|
||||
_NanConstraint(),
|
||||
_PandasNAConstraint(),
|
||||
]
|
||||
if not self.numeric_only:
|
||||
self._constraints.extend([_InstancesOf(str), _NoneConstraint()])
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return any(c.is_satisfied_by(val) for c in self._constraints)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
|
||||
f" {self._constraints[-1]}"
|
||||
)
|
||||
|
||||
|
||||
class HasMethods(_Constraint):
|
||||
"""Constraint representing objects that expose specific methods.
|
||||
|
||||
It is useful for parameters following a protocol and where we don't want to impose
|
||||
an affiliation to a specific module or class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
methods : str or list of str
|
||||
The method(s) that the object is expected to expose.
|
||||
"""
|
||||
|
||||
@validate_params(
|
||||
{"methods": [str, list]},
|
||||
prefer_skip_nested_validation=True,
|
||||
)
|
||||
def __init__(self, methods):
|
||||
super().__init__()
|
||||
if isinstance(methods, str):
|
||||
methods = [methods]
|
||||
self.methods = methods
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return all(callable(getattr(val, method, None)) for method in self.methods)
|
||||
|
||||
def __str__(self):
|
||||
if len(self.methods) == 1:
|
||||
methods = f"{self.methods[0]!r}"
|
||||
else:
|
||||
methods = (
|
||||
f"{', '.join([repr(m) for m in self.methods[:-1]])} and"
|
||||
f" {self.methods[-1]!r}"
|
||||
)
|
||||
return f"an object implementing {methods}"
|
||||
|
||||
|
||||
class _IterablesNotString(_Constraint):
|
||||
"""Constraint representing iterables that are not strings."""
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return isinstance(val, Iterable) and not isinstance(val, str)
|
||||
|
||||
def __str__(self):
|
||||
return "an iterable"
|
||||
|
||||
|
||||
class _CVObjects(_Constraint):
|
||||
"""Constraint representing cv objects.
|
||||
|
||||
Convenient class for
|
||||
[
|
||||
Interval(Integral, 2, None, closed="left"),
|
||||
HasMethods(["split", "get_n_splits"]),
|
||||
_IterablesNotString(),
|
||||
None,
|
||||
]
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._constraints = [
|
||||
Interval(Integral, 2, None, closed="left"),
|
||||
HasMethods(["split", "get_n_splits"]),
|
||||
_IterablesNotString(),
|
||||
_NoneConstraint(),
|
||||
]
|
||||
|
||||
def is_satisfied_by(self, val):
|
||||
return any(c.is_satisfied_by(val) for c in self._constraints)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{', '.join([str(c) for c in self._constraints[:-1]])} or"
|
||||
f" {self._constraints[-1]}"
|
||||
)
|
||||
|
||||
|
||||
class Hidden:
|
||||
"""Class encapsulating a constraint not meant to be exposed to the user.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
constraint : str or _Constraint instance
|
||||
The constraint to be used internally.
|
||||
"""
|
||||
|
||||
def __init__(self, constraint):
|
||||
self.constraint = constraint
|
||||
|
||||
|
||||
def generate_invalid_param_val(constraint):
|
||||
"""Return a value that does not satisfy the constraint.
|
||||
|
||||
Raises a NotImplementedError if there exists no invalid value for this constraint.
|
||||
|
||||
This is only useful for testing purpose.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
constraint : _Constraint instance
|
||||
The constraint to generate a value for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
val : object
|
||||
A value that does not satisfy the constraint.
|
||||
"""
|
||||
if isinstance(constraint, StrOptions):
|
||||
return f"not {' or '.join(constraint.options)}"
|
||||
|
||||
if isinstance(constraint, MissingValues):
|
||||
return np.array([1, 2, 3])
|
||||
|
||||
if isinstance(constraint, _VerboseHelper):
|
||||
return -1
|
||||
|
||||
if isinstance(constraint, HasMethods):
|
||||
return type("HasNotMethods", (), {})()
|
||||
|
||||
if isinstance(constraint, _IterablesNotString):
|
||||
return "a string"
|
||||
|
||||
if isinstance(constraint, _CVObjects):
|
||||
return "not a cv object"
|
||||
|
||||
if isinstance(constraint, Interval) and constraint.type is Integral:
|
||||
if constraint.left is not None:
|
||||
return constraint.left - 1
|
||||
if constraint.right is not None:
|
||||
return constraint.right + 1
|
||||
|
||||
# There's no integer outside (-inf, +inf)
|
||||
raise NotImplementedError
|
||||
|
||||
if isinstance(constraint, Interval) and constraint.type in (Real, RealNotInt):
|
||||
if constraint.left is not None:
|
||||
return constraint.left - 1e-6
|
||||
if constraint.right is not None:
|
||||
return constraint.right + 1e-6
|
||||
|
||||
# bounds are -inf, +inf
|
||||
if constraint.closed in ("right", "neither"):
|
||||
return -np.inf
|
||||
if constraint.closed in ("left", "neither"):
|
||||
return np.inf
|
||||
|
||||
# interval is [-inf, +inf]
|
||||
return np.nan
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def generate_valid_param(constraint):
|
||||
"""Return a value that does satisfy a constraint.
|
||||
|
||||
This is only useful for testing purpose.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
constraint : Constraint instance
|
||||
The constraint to generate a value for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
val : object
|
||||
A value that does satisfy the constraint.
|
||||
"""
|
||||
if isinstance(constraint, _ArrayLikes):
|
||||
return np.array([1, 2, 3])
|
||||
|
||||
if isinstance(constraint, _SparseMatrices):
|
||||
return csr_matrix([[0, 1], [1, 0]])
|
||||
|
||||
if isinstance(constraint, _RandomStates):
|
||||
return np.random.RandomState(42)
|
||||
|
||||
if isinstance(constraint, _Callables):
|
||||
return lambda x: x
|
||||
|
||||
if isinstance(constraint, _NoneConstraint):
|
||||
return None
|
||||
|
||||
if isinstance(constraint, _InstancesOf):
|
||||
if constraint.type is np.ndarray:
|
||||
# special case for ndarray since it can't be instantiated without arguments
|
||||
return np.array([1, 2, 3])
|
||||
|
||||
if constraint.type in (Integral, Real):
|
||||
# special case for Integral and Real since they are abstract classes
|
||||
return 1
|
||||
|
||||
return constraint.type()
|
||||
|
||||
if isinstance(constraint, _Booleans):
|
||||
return True
|
||||
|
||||
if isinstance(constraint, _VerboseHelper):
|
||||
return 1
|
||||
|
||||
if isinstance(constraint, MissingValues) and constraint.numeric_only:
|
||||
return np.nan
|
||||
|
||||
if isinstance(constraint, MissingValues) and not constraint.numeric_only:
|
||||
return "missing"
|
||||
|
||||
if isinstance(constraint, HasMethods):
|
||||
return type(
|
||||
"ValidHasMethods", (), {m: lambda self: None for m in constraint.methods}
|
||||
)()
|
||||
|
||||
if isinstance(constraint, _IterablesNotString):
|
||||
return [1, 2, 3]
|
||||
|
||||
if isinstance(constraint, _CVObjects):
|
||||
return 5
|
||||
|
||||
if isinstance(constraint, Options): # includes StrOptions
|
||||
for option in constraint.options:
|
||||
return option
|
||||
|
||||
if isinstance(constraint, Interval):
|
||||
interval = constraint
|
||||
if interval.left is None and interval.right is None:
|
||||
return 0
|
||||
elif interval.left is None:
|
||||
return interval.right - 1
|
||||
elif interval.right is None:
|
||||
return interval.left + 1
|
||||
else:
|
||||
if interval.type is Real:
|
||||
return (interval.left + interval.right) / 2
|
||||
else:
|
||||
return interval.left + 1
|
||||
|
||||
raise ValueError(f"Unknown constraint type: {constraint}")
|
||||
@@ -0,0 +1,98 @@
|
||||
import numpy as np
|
||||
|
||||
from . import check_consistent_length, check_matplotlib_support
|
||||
from ._response import _get_response_values_binary
|
||||
from .multiclass import type_of_target
|
||||
from .validation import _check_pos_label_consistency
|
||||
|
||||
|
||||
class _BinaryClassifierCurveDisplayMixin:
|
||||
"""Mixin class to be used in Displays requiring a binary classifier.
|
||||
|
||||
The aim of this class is to centralize some validations regarding the estimator and
|
||||
the target and gather the response of the estimator.
|
||||
"""
|
||||
|
||||
def _validate_plot_params(self, *, ax=None, name=None):
|
||||
check_matplotlib_support(f"{self.__class__.__name__}.plot")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
if ax is None:
|
||||
_, ax = plt.subplots()
|
||||
|
||||
name = self.estimator_name if name is None else name
|
||||
return ax, ax.figure, name
|
||||
|
||||
@classmethod
|
||||
def _validate_and_get_response_values(
|
||||
cls, estimator, X, y, *, response_method="auto", pos_label=None, name=None
|
||||
):
|
||||
check_matplotlib_support(f"{cls.__name__}.from_estimator")
|
||||
|
||||
name = estimator.__class__.__name__ if name is None else name
|
||||
|
||||
y_pred, pos_label = _get_response_values_binary(
|
||||
estimator,
|
||||
X,
|
||||
response_method=response_method,
|
||||
pos_label=pos_label,
|
||||
)
|
||||
|
||||
return y_pred, pos_label, name
|
||||
|
||||
@classmethod
|
||||
def _validate_from_predictions_params(
|
||||
cls, y_true, y_pred, *, sample_weight=None, pos_label=None, name=None
|
||||
):
|
||||
check_matplotlib_support(f"{cls.__name__}.from_predictions")
|
||||
|
||||
if type_of_target(y_true) != "binary":
|
||||
raise ValueError(
|
||||
f"The target y is not binary. Got {type_of_target(y_true)} type of"
|
||||
" target."
|
||||
)
|
||||
|
||||
check_consistent_length(y_true, y_pred, sample_weight)
|
||||
pos_label = _check_pos_label_consistency(pos_label, y_true)
|
||||
|
||||
name = name if name is not None else "Classifier"
|
||||
|
||||
return pos_label, name
|
||||
|
||||
|
||||
def _validate_score_name(score_name, scoring, negate_score):
|
||||
"""Validate the `score_name` parameter.
|
||||
|
||||
If `score_name` is provided, we just return it as-is.
|
||||
If `score_name` is `None`, we use `Score` if `negate_score` is `False` and
|
||||
`Negative score` otherwise.
|
||||
If `score_name` is a string or a callable, we infer the name. We replace `_` by
|
||||
spaces and capitalize the first letter. We remove `neg_` and replace it by
|
||||
`"Negative"` if `negate_score` is `False` or just remove it otherwise.
|
||||
"""
|
||||
if score_name is not None:
|
||||
return score_name
|
||||
elif scoring is None:
|
||||
return "Negative score" if negate_score else "Score"
|
||||
else:
|
||||
score_name = scoring.__name__ if callable(scoring) else scoring
|
||||
if negate_score:
|
||||
if score_name.startswith("neg_"):
|
||||
score_name = score_name[4:]
|
||||
else:
|
||||
score_name = f"Negative {score_name}"
|
||||
elif score_name.startswith("neg_"):
|
||||
score_name = f"Negative {score_name[4:]}"
|
||||
score_name = score_name.replace("_", " ")
|
||||
return score_name.capitalize()
|
||||
|
||||
|
||||
def _interval_max_min_ratio(data):
|
||||
"""Compute the ratio between the largest and smallest inter-point distances.
|
||||
|
||||
A value larger than 5 typically indicates that the parameter range would
|
||||
better be displayed with a log scale while a linear scale would be more
|
||||
suitable otherwise.
|
||||
"""
|
||||
diff = np.diff(np.sort(data))
|
||||
return diff.max() / diff.min()
|
||||
@@ -0,0 +1,463 @@
|
||||
"""This module contains the _EstimatorPrettyPrinter class used in
|
||||
BaseEstimator.__repr__ for pretty-printing estimators"""
|
||||
|
||||
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
# 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation;
|
||||
# All Rights Reserved
|
||||
|
||||
# Authors: Fred L. Drake, Jr. <fdrake@acm.org> (built-in CPython pprint module)
|
||||
# Nicolas Hug (scikit-learn specific changes)
|
||||
|
||||
# License: PSF License version 2 (see below)
|
||||
|
||||
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
# --------------------------------------------
|
||||
|
||||
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"),
|
||||
# and the Individual or Organization ("Licensee") accessing and otherwise
|
||||
# using this software ("Python") in source or binary form and its associated
|
||||
# documentation.
|
||||
|
||||
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
# grants Licensee a nonexclusive, royalty-free, world-wide license to
|
||||
# reproduce, analyze, test, perform and/or display publicly, prepare
|
||||
# derivative works, distribute, and otherwise use Python alone or in any
|
||||
# derivative version, provided, however, that PSF's License Agreement and
|
||||
# PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004,
|
||||
# 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016,
|
||||
# 2017, 2018 Python Software Foundation; All Rights Reserved" are retained in
|
||||
# Python alone or in any derivative version prepared by Licensee.
|
||||
|
||||
# 3. In the event Licensee prepares a derivative work that is based on or
|
||||
# incorporates Python or any part thereof, and wants to make the derivative
|
||||
# work available to others as provided herein, then Licensee hereby agrees to
|
||||
# include in any such work a brief summary of the changes made to Python.
|
||||
|
||||
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES
|
||||
# NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT
|
||||
# NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF
|
||||
# MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF
|
||||
# PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY
|
||||
# INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF
|
||||
# MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE
|
||||
# THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
# 6. This License Agreement will automatically terminate upon a material
|
||||
# breach of its terms and conditions.
|
||||
|
||||
# 7. Nothing in this License Agreement shall be deemed to create any
|
||||
# relationship of agency, partnership, or joint venture between PSF and
|
||||
# Licensee. This License Agreement does not grant permission to use PSF
|
||||
# trademarks or trade name in a trademark sense to endorse or promote products
|
||||
# or services of Licensee, or any third party.
|
||||
|
||||
# 8. By copying, installing or otherwise using Python, Licensee agrees to be
|
||||
# bound by the terms and conditions of this License Agreement.
|
||||
|
||||
|
||||
# Brief summary of changes to original code:
|
||||
# - "compact" parameter is supported for dicts, not just lists or tuples
|
||||
# - estimators have a custom handler, they're not just treated as objects
|
||||
# - long sequences (lists, tuples, dict items) with more than N elements are
|
||||
# shortened using ellipsis (', ...') at the end.
|
||||
|
||||
import inspect
|
||||
import pprint
|
||||
from collections import OrderedDict
|
||||
|
||||
from .._config import get_config
|
||||
from ..base import BaseEstimator
|
||||
from . import is_scalar_nan
|
||||
|
||||
|
||||
class KeyValTuple(tuple):
|
||||
"""Dummy class for correctly rendering key-value tuples from dicts."""
|
||||
|
||||
def __repr__(self):
|
||||
# needed for _dispatch[tuple.__repr__] not to be overridden
|
||||
return super().__repr__()
|
||||
|
||||
|
||||
class KeyValTupleParam(KeyValTuple):
|
||||
"""Dummy class for correctly rendering key-value tuples from parameters."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _changed_params(estimator):
|
||||
"""Return dict (param_name: value) of parameters that were given to
|
||||
estimator with non-default values."""
|
||||
|
||||
params = estimator.get_params(deep=False)
|
||||
init_func = getattr(estimator.__init__, "deprecated_original", estimator.__init__)
|
||||
init_params = inspect.signature(init_func).parameters
|
||||
init_params = {name: param.default for name, param in init_params.items()}
|
||||
|
||||
def has_changed(k, v):
|
||||
if k not in init_params: # happens if k is part of a **kwargs
|
||||
return True
|
||||
if init_params[k] == inspect._empty: # k has no default value
|
||||
return True
|
||||
# try to avoid calling repr on nested estimators
|
||||
if isinstance(v, BaseEstimator) and v.__class__ != init_params[k].__class__:
|
||||
return True
|
||||
# Use repr as a last resort. It may be expensive.
|
||||
if repr(v) != repr(init_params[k]) and not (
|
||||
is_scalar_nan(init_params[k]) and is_scalar_nan(v)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
return {k: v for k, v in params.items() if has_changed(k, v)}
|
||||
|
||||
|
||||
class _EstimatorPrettyPrinter(pprint.PrettyPrinter):
|
||||
"""Pretty Printer class for estimator objects.
|
||||
|
||||
This extends the pprint.PrettyPrinter class, because:
|
||||
- we need estimators to be printed with their parameters, e.g.
|
||||
Estimator(param1=value1, ...) which is not supported by default.
|
||||
- the 'compact' parameter of PrettyPrinter is ignored for dicts, which
|
||||
may lead to very long representations that we want to avoid.
|
||||
|
||||
Quick overview of pprint.PrettyPrinter (see also
|
||||
https://stackoverflow.com/questions/49565047/pprint-with-hex-numbers):
|
||||
|
||||
- the entry point is the _format() method which calls format() (overridden
|
||||
here)
|
||||
- format() directly calls _safe_repr() for a first try at rendering the
|
||||
object
|
||||
- _safe_repr formats the whole object recursively, only calling itself,
|
||||
not caring about line length or anything
|
||||
- back to _format(), if the output string is too long, _format() then calls
|
||||
the appropriate _pprint_TYPE() method (e.g. _pprint_list()) depending on
|
||||
the type of the object. This where the line length and the compact
|
||||
parameters are taken into account.
|
||||
- those _pprint_TYPE() methods will internally use the format() method for
|
||||
rendering the nested objects of an object (e.g. the elements of a list)
|
||||
|
||||
In the end, everything has to be implemented twice: in _safe_repr and in
|
||||
the custom _pprint_TYPE methods. Unfortunately PrettyPrinter is really not
|
||||
straightforward to extend (especially when we want a compact output), so
|
||||
the code is a bit convoluted.
|
||||
|
||||
This class overrides:
|
||||
- format() to support the changed_only parameter
|
||||
- _safe_repr to support printing of estimators (for when they fit on a
|
||||
single line)
|
||||
- _format_dict_items so that dict are correctly 'compacted'
|
||||
- _format_items so that ellipsis is used on long lists and tuples
|
||||
|
||||
When estimators cannot be printed on a single line, the builtin _format()
|
||||
will call _pprint_estimator() because it was registered to do so (see
|
||||
_dispatch[BaseEstimator.__repr__] = _pprint_estimator).
|
||||
|
||||
both _format_dict_items() and _pprint_estimator() use the
|
||||
_format_params_or_dict_items() method that will format parameters and
|
||||
key-value pairs respecting the compact parameter. This method needs another
|
||||
subroutine _pprint_key_val_tuple() used when a parameter or a key-value
|
||||
pair is too long to fit on a single line. This subroutine is called in
|
||||
_format() and is registered as well in the _dispatch dict (just like
|
||||
_pprint_estimator). We had to create the two classes KeyValTuple and
|
||||
KeyValTupleParam for this.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
indent=1,
|
||||
width=80,
|
||||
depth=None,
|
||||
stream=None,
|
||||
*,
|
||||
compact=False,
|
||||
indent_at_name=True,
|
||||
n_max_elements_to_show=None,
|
||||
):
|
||||
super().__init__(indent, width, depth, stream, compact=compact)
|
||||
self._indent_at_name = indent_at_name
|
||||
if self._indent_at_name:
|
||||
self._indent_per_level = 1 # ignore indent param
|
||||
self._changed_only = get_config()["print_changed_only"]
|
||||
# Max number of elements in a list, dict, tuple until we start using
|
||||
# ellipsis. This also affects the number of arguments of an estimators
|
||||
# (they are treated as dicts)
|
||||
self.n_max_elements_to_show = n_max_elements_to_show
|
||||
|
||||
def format(self, object, context, maxlevels, level):
|
||||
return _safe_repr(
|
||||
object, context, maxlevels, level, changed_only=self._changed_only
|
||||
)
|
||||
|
||||
def _pprint_estimator(self, object, stream, indent, allowance, context, level):
|
||||
stream.write(object.__class__.__name__ + "(")
|
||||
if self._indent_at_name:
|
||||
indent += len(object.__class__.__name__)
|
||||
|
||||
if self._changed_only:
|
||||
params = _changed_params(object)
|
||||
else:
|
||||
params = object.get_params(deep=False)
|
||||
|
||||
params = OrderedDict((name, val) for (name, val) in sorted(params.items()))
|
||||
|
||||
self._format_params(
|
||||
params.items(), stream, indent, allowance + 1, context, level
|
||||
)
|
||||
stream.write(")")
|
||||
|
||||
def _format_dict_items(self, items, stream, indent, allowance, context, level):
|
||||
return self._format_params_or_dict_items(
|
||||
items, stream, indent, allowance, context, level, is_dict=True
|
||||
)
|
||||
|
||||
def _format_params(self, items, stream, indent, allowance, context, level):
|
||||
return self._format_params_or_dict_items(
|
||||
items, stream, indent, allowance, context, level, is_dict=False
|
||||
)
|
||||
|
||||
def _format_params_or_dict_items(
|
||||
self, object, stream, indent, allowance, context, level, is_dict
|
||||
):
|
||||
"""Format dict items or parameters respecting the compact=True
|
||||
parameter. For some reason, the builtin rendering of dict items doesn't
|
||||
respect compact=True and will use one line per key-value if all cannot
|
||||
fit in a single line.
|
||||
Dict items will be rendered as <'key': value> while params will be
|
||||
rendered as <key=value>. The implementation is mostly copy/pasting from
|
||||
the builtin _format_items().
|
||||
This also adds ellipsis if the number of items is greater than
|
||||
self.n_max_elements_to_show.
|
||||
"""
|
||||
write = stream.write
|
||||
indent += self._indent_per_level
|
||||
delimnl = ",\n" + " " * indent
|
||||
delim = ""
|
||||
width = max_width = self._width - indent + 1
|
||||
it = iter(object)
|
||||
try:
|
||||
next_ent = next(it)
|
||||
except StopIteration:
|
||||
return
|
||||
last = False
|
||||
n_items = 0
|
||||
while not last:
|
||||
if n_items == self.n_max_elements_to_show:
|
||||
write(", ...")
|
||||
break
|
||||
n_items += 1
|
||||
ent = next_ent
|
||||
try:
|
||||
next_ent = next(it)
|
||||
except StopIteration:
|
||||
last = True
|
||||
max_width -= allowance
|
||||
width -= allowance
|
||||
if self._compact:
|
||||
k, v = ent
|
||||
krepr = self._repr(k, context, level)
|
||||
vrepr = self._repr(v, context, level)
|
||||
if not is_dict:
|
||||
krepr = krepr.strip("'")
|
||||
middle = ": " if is_dict else "="
|
||||
rep = krepr + middle + vrepr
|
||||
w = len(rep) + 2
|
||||
if width < w:
|
||||
width = max_width
|
||||
if delim:
|
||||
delim = delimnl
|
||||
if width >= w:
|
||||
width -= w
|
||||
write(delim)
|
||||
delim = ", "
|
||||
write(rep)
|
||||
continue
|
||||
write(delim)
|
||||
delim = delimnl
|
||||
class_ = KeyValTuple if is_dict else KeyValTupleParam
|
||||
self._format(
|
||||
class_(ent), stream, indent, allowance if last else 1, context, level
|
||||
)
|
||||
|
||||
def _format_items(self, items, stream, indent, allowance, context, level):
|
||||
"""Format the items of an iterable (list, tuple...). Same as the
|
||||
built-in _format_items, with support for ellipsis if the number of
|
||||
elements is greater than self.n_max_elements_to_show.
|
||||
"""
|
||||
write = stream.write
|
||||
indent += self._indent_per_level
|
||||
if self._indent_per_level > 1:
|
||||
write((self._indent_per_level - 1) * " ")
|
||||
delimnl = ",\n" + " " * indent
|
||||
delim = ""
|
||||
width = max_width = self._width - indent + 1
|
||||
it = iter(items)
|
||||
try:
|
||||
next_ent = next(it)
|
||||
except StopIteration:
|
||||
return
|
||||
last = False
|
||||
n_items = 0
|
||||
while not last:
|
||||
if n_items == self.n_max_elements_to_show:
|
||||
write(", ...")
|
||||
break
|
||||
n_items += 1
|
||||
ent = next_ent
|
||||
try:
|
||||
next_ent = next(it)
|
||||
except StopIteration:
|
||||
last = True
|
||||
max_width -= allowance
|
||||
width -= allowance
|
||||
if self._compact:
|
||||
rep = self._repr(ent, context, level)
|
||||
w = len(rep) + 2
|
||||
if width < w:
|
||||
width = max_width
|
||||
if delim:
|
||||
delim = delimnl
|
||||
if width >= w:
|
||||
width -= w
|
||||
write(delim)
|
||||
delim = ", "
|
||||
write(rep)
|
||||
continue
|
||||
write(delim)
|
||||
delim = delimnl
|
||||
self._format(ent, stream, indent, allowance if last else 1, context, level)
|
||||
|
||||
def _pprint_key_val_tuple(self, object, stream, indent, allowance, context, level):
|
||||
"""Pretty printing for key-value tuples from dict or parameters."""
|
||||
k, v = object
|
||||
rep = self._repr(k, context, level)
|
||||
if isinstance(object, KeyValTupleParam):
|
||||
rep = rep.strip("'")
|
||||
middle = "="
|
||||
else:
|
||||
middle = ": "
|
||||
stream.write(rep)
|
||||
stream.write(middle)
|
||||
self._format(
|
||||
v, stream, indent + len(rep) + len(middle), allowance, context, level
|
||||
)
|
||||
|
||||
# Note: need to copy _dispatch to prevent instances of the builtin
|
||||
# PrettyPrinter class to call methods of _EstimatorPrettyPrinter (see issue
|
||||
# 12906)
|
||||
# mypy error: "Type[PrettyPrinter]" has no attribute "_dispatch"
|
||||
_dispatch = pprint.PrettyPrinter._dispatch.copy() # type: ignore
|
||||
_dispatch[BaseEstimator.__repr__] = _pprint_estimator
|
||||
_dispatch[KeyValTuple.__repr__] = _pprint_key_val_tuple
|
||||
|
||||
|
||||
def _safe_repr(object, context, maxlevels, level, changed_only=False):
|
||||
"""Same as the builtin _safe_repr, with added support for Estimator
|
||||
objects."""
|
||||
typ = type(object)
|
||||
|
||||
if typ in pprint._builtin_scalars:
|
||||
return repr(object), True, False
|
||||
|
||||
r = getattr(typ, "__repr__", None)
|
||||
if issubclass(typ, dict) and r is dict.__repr__:
|
||||
if not object:
|
||||
return "{}", True, False
|
||||
objid = id(object)
|
||||
if maxlevels and level >= maxlevels:
|
||||
return "{...}", False, objid in context
|
||||
if objid in context:
|
||||
return pprint._recursion(object), False, True
|
||||
context[objid] = 1
|
||||
readable = True
|
||||
recursive = False
|
||||
components = []
|
||||
append = components.append
|
||||
level += 1
|
||||
saferepr = _safe_repr
|
||||
items = sorted(object.items(), key=pprint._safe_tuple)
|
||||
for k, v in items:
|
||||
krepr, kreadable, krecur = saferepr(
|
||||
k, context, maxlevels, level, changed_only=changed_only
|
||||
)
|
||||
vrepr, vreadable, vrecur = saferepr(
|
||||
v, context, maxlevels, level, changed_only=changed_only
|
||||
)
|
||||
append("%s: %s" % (krepr, vrepr))
|
||||
readable = readable and kreadable and vreadable
|
||||
if krecur or vrecur:
|
||||
recursive = True
|
||||
del context[objid]
|
||||
return "{%s}" % ", ".join(components), readable, recursive
|
||||
|
||||
if (issubclass(typ, list) and r is list.__repr__) or (
|
||||
issubclass(typ, tuple) and r is tuple.__repr__
|
||||
):
|
||||
if issubclass(typ, list):
|
||||
if not object:
|
||||
return "[]", True, False
|
||||
format = "[%s]"
|
||||
elif len(object) == 1:
|
||||
format = "(%s,)"
|
||||
else:
|
||||
if not object:
|
||||
return "()", True, False
|
||||
format = "(%s)"
|
||||
objid = id(object)
|
||||
if maxlevels and level >= maxlevels:
|
||||
return format % "...", False, objid in context
|
||||
if objid in context:
|
||||
return pprint._recursion(object), False, True
|
||||
context[objid] = 1
|
||||
readable = True
|
||||
recursive = False
|
||||
components = []
|
||||
append = components.append
|
||||
level += 1
|
||||
for o in object:
|
||||
orepr, oreadable, orecur = _safe_repr(
|
||||
o, context, maxlevels, level, changed_only=changed_only
|
||||
)
|
||||
append(orepr)
|
||||
if not oreadable:
|
||||
readable = False
|
||||
if orecur:
|
||||
recursive = True
|
||||
del context[objid]
|
||||
return format % ", ".join(components), readable, recursive
|
||||
|
||||
if issubclass(typ, BaseEstimator):
|
||||
objid = id(object)
|
||||
if maxlevels and level >= maxlevels:
|
||||
return "{...}", False, objid in context
|
||||
if objid in context:
|
||||
return pprint._recursion(object), False, True
|
||||
context[objid] = 1
|
||||
readable = True
|
||||
recursive = False
|
||||
if changed_only:
|
||||
params = _changed_params(object)
|
||||
else:
|
||||
params = object.get_params(deep=False)
|
||||
components = []
|
||||
append = components.append
|
||||
level += 1
|
||||
saferepr = _safe_repr
|
||||
items = sorted(params.items(), key=pprint._safe_tuple)
|
||||
for k, v in items:
|
||||
krepr, kreadable, krecur = saferepr(
|
||||
k, context, maxlevels, level, changed_only=changed_only
|
||||
)
|
||||
vrepr, vreadable, vrecur = saferepr(
|
||||
v, context, maxlevels, level, changed_only=changed_only
|
||||
)
|
||||
append("%s=%s" % (krepr.strip("'"), vrepr))
|
||||
readable = readable and kreadable and vreadable
|
||||
if krecur or vrecur:
|
||||
recursive = True
|
||||
del context[objid]
|
||||
return ("%s(%s)" % (typ.__name__, ", ".join(components)), readable, recursive)
|
||||
|
||||
rep = repr(object)
|
||||
return rep, (rep and not rep.startswith("<")), False
|
||||
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
# Authors: Arnaud Joly
|
||||
#
|
||||
# License: BSD 3 clause
|
||||
|
||||
|
||||
cimport numpy as cnp
|
||||
ctypedef cnp.npy_uint32 UINT32_t
|
||||
|
||||
cdef inline UINT32_t DEFAULT_SEED = 1
|
||||
|
||||
cdef enum:
|
||||
# Max value for our rand_r replacement (near the bottom).
|
||||
# We don't use RAND_MAX because it's different across platforms and
|
||||
# particularly tiny on Windows/MSVC.
|
||||
# It corresponds to the maximum representable value for
|
||||
# 32-bit signed integers (i.e. 2^31 - 1).
|
||||
RAND_R_MAX = 2147483647
|
||||
|
||||
|
||||
# rand_r replacement using a 32bit XorShift generator
|
||||
# See http://www.jstatsoft.org/v08/i14/paper for details
|
||||
cdef inline UINT32_t our_rand_r(UINT32_t* seed) nogil:
|
||||
"""Generate a pseudo-random np.uint32 from a np.uint32 seed"""
|
||||
# seed shouldn't ever be 0.
|
||||
if (seed[0] == 0):
|
||||
seed[0] = DEFAULT_SEED
|
||||
|
||||
seed[0] ^= <UINT32_t>(seed[0] << 13)
|
||||
seed[0] ^= <UINT32_t>(seed[0] >> 17)
|
||||
seed[0] ^= <UINT32_t>(seed[0] << 5)
|
||||
|
||||
# Use the modulo to make sure that we don't return a values greater than the
|
||||
# maximum representable value for signed 32bit integers (i.e. 2^31 - 1).
|
||||
# Note that the parenthesis are needed to avoid overflow: here
|
||||
# RAND_R_MAX is cast to UINT32_t before 1 is added.
|
||||
return seed[0] % ((<UINT32_t>RAND_R_MAX) + 1)
|
||||
@@ -0,0 +1,298 @@
|
||||
"""Utilities to get the response values of a classifier or a regressor.
|
||||
|
||||
It allows to make uniform checks and validation.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from ..base import is_classifier
|
||||
from .multiclass import type_of_target
|
||||
from .validation import _check_response_method, check_is_fitted
|
||||
|
||||
|
||||
def _process_predict_proba(*, y_pred, target_type, classes, pos_label):
|
||||
"""Get the response values when the response method is `predict_proba`.
|
||||
|
||||
This function process the `y_pred` array in the binary and multi-label cases.
|
||||
In the binary case, it selects the column corresponding to the positive
|
||||
class. In the multi-label case, it stacks the predictions if they are not
|
||||
in the "compressed" format `(n_samples, n_outputs)`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
y_pred : ndarray
|
||||
Output of `estimator.predict_proba`. The shape depends on the target type:
|
||||
|
||||
- for binary classification, it is a 2d array of shape `(n_samples, 2)`;
|
||||
- for multiclass classification, it is a 2d array of shape
|
||||
`(n_samples, n_classes)`;
|
||||
- for multilabel classification, it is either a list of 2d arrays of shape
|
||||
`(n_samples, 2)` (e.g. `RandomForestClassifier` or `KNeighborsClassifier`) or
|
||||
an array of shape `(n_samples, n_outputs)` (e.g. `MLPClassifier` or
|
||||
`RidgeClassifier`).
|
||||
|
||||
target_type : {"binary", "multiclass", "multilabel-indicator"}
|
||||
Type of the target.
|
||||
|
||||
classes : ndarray of shape (n_classes,) or list of such arrays
|
||||
Class labels as reported by `estimator.classes_`.
|
||||
|
||||
pos_label : int, float, bool or str
|
||||
Only used with binary and multiclass targets.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y_pred : ndarray of shape (n_samples,), (n_samples, n_classes) or \
|
||||
(n_samples, n_output)
|
||||
Compressed predictions format as requested by the metrics.
|
||||
"""
|
||||
if target_type == "binary" and y_pred.shape[1] < 2:
|
||||
# We don't handle classifiers trained on a single class.
|
||||
raise ValueError(
|
||||
f"Got predict_proba of shape {y_pred.shape}, but need "
|
||||
"classifier with two classes."
|
||||
)
|
||||
|
||||
if target_type == "binary":
|
||||
col_idx = np.flatnonzero(classes == pos_label)[0]
|
||||
return y_pred[:, col_idx]
|
||||
elif target_type == "multilabel-indicator":
|
||||
# Use a compress format of shape `(n_samples, n_output)`.
|
||||
# Only `MLPClassifier` and `RidgeClassifier` return an array of shape
|
||||
# `(n_samples, n_outputs)`.
|
||||
if isinstance(y_pred, list):
|
||||
# list of arrays of shape `(n_samples, 2)`
|
||||
return np.vstack([p[:, -1] for p in y_pred]).T
|
||||
else:
|
||||
# array of shape `(n_samples, n_outputs)`
|
||||
return y_pred
|
||||
|
||||
return y_pred
|
||||
|
||||
|
||||
def _process_decision_function(*, y_pred, target_type, classes, pos_label):
|
||||
"""Get the response values when the response method is `decision_function`.
|
||||
|
||||
This function process the `y_pred` array in the binary and multi-label cases.
|
||||
In the binary case, it inverts the sign of the score if the positive label
|
||||
is not `classes[1]`. In the multi-label case, it stacks the predictions if
|
||||
they are not in the "compressed" format `(n_samples, n_outputs)`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
y_pred : ndarray
|
||||
Output of `estimator.predict_proba`. The shape depends on the target type:
|
||||
|
||||
- for binary classification, it is a 1d array of shape `(n_samples,)` where the
|
||||
sign is assuming that `classes[1]` is the positive class;
|
||||
- for multiclass classification, it is a 2d array of shape
|
||||
`(n_samples, n_classes)`;
|
||||
- for multilabel classification, it is a 2d array of shape `(n_samples,
|
||||
n_outputs)`.
|
||||
|
||||
target_type : {"binary", "multiclass", "multilabel-indicator"}
|
||||
Type of the target.
|
||||
|
||||
classes : ndarray of shape (n_classes,) or list of such arrays
|
||||
Class labels as reported by `estimator.classes_`.
|
||||
|
||||
pos_label : int, float, bool or str
|
||||
Only used with binary and multiclass targets.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y_pred : ndarray of shape (n_samples,), (n_samples, n_classes) or \
|
||||
(n_samples, n_output)
|
||||
Compressed predictions format as requested by the metrics.
|
||||
"""
|
||||
if target_type == "binary" and pos_label == classes[0]:
|
||||
return -1 * y_pred
|
||||
return y_pred
|
||||
|
||||
|
||||
def _get_response_values(
|
||||
estimator,
|
||||
X,
|
||||
response_method,
|
||||
pos_label=None,
|
||||
return_response_method_used=False,
|
||||
):
|
||||
"""Compute the response values of a classifier, an outlier detector, or a regressor.
|
||||
|
||||
The response values are predictions such that it follows the following shape:
|
||||
|
||||
- for binary classification, it is a 1d array of shape `(n_samples,)`;
|
||||
- for multiclass classification, it is a 2d array of shape `(n_samples, n_classes)`;
|
||||
- for multilabel classification, it is a 2d array of shape `(n_samples, n_outputs)`;
|
||||
- for outlier detection, it is a 1d array of shape `(n_samples,)`;
|
||||
- for regression, it is a 1d array of shape `(n_samples,)`.
|
||||
|
||||
If `estimator` is a binary classifier, also return the label for the
|
||||
effective positive class.
|
||||
|
||||
This utility is used primarily in the displays and the scikit-learn scorers.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : estimator instance
|
||||
Fitted classifier, outlier detector, or regressor or a
|
||||
fitted :class:`~sklearn.pipeline.Pipeline` in which the last estimator is a
|
||||
classifier, an outlier detector, or a regressor.
|
||||
|
||||
X : {array-like, sparse matrix} of shape (n_samples, n_features)
|
||||
Input values.
|
||||
|
||||
response_method : {"predict_proba", "predict_log_proba", "decision_function", \
|
||||
"predict"} or list of such str
|
||||
Specifies the response method to use get prediction from an estimator
|
||||
(i.e. :term:`predict_proba`, :term:`predict_log_proba`,
|
||||
:term:`decision_function` or :term:`predict`). Possible choices are:
|
||||
|
||||
- if `str`, it corresponds to the name to the method to return;
|
||||
- if a list of `str`, it provides the method names in order of
|
||||
preference. The method returned corresponds to the first method in
|
||||
the list and which is implemented by `estimator`.
|
||||
|
||||
pos_label : int, float, bool or str, default=None
|
||||
The class considered as the positive class when computing
|
||||
the metrics. If `None` and target is 'binary', `estimators.classes_[1]` is
|
||||
considered as the positive class.
|
||||
|
||||
return_response_method_used : bool, default=False
|
||||
Whether to return the response method used to compute the response
|
||||
values.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
Returns
|
||||
-------
|
||||
y_pred : ndarray of shape (n_samples,), (n_samples, n_classes) or \
|
||||
(n_samples, n_outputs)
|
||||
Target scores calculated from the provided `response_method`
|
||||
and `pos_label`.
|
||||
|
||||
pos_label : int, float, bool, str or None
|
||||
The class considered as the positive class when computing
|
||||
the metrics. Returns `None` if `estimator` is a regressor or an outlier
|
||||
detector.
|
||||
|
||||
response_method_used : str
|
||||
The response method used to compute the response values. Only returned
|
||||
if `return_response_method_used` is `True`.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If `pos_label` is not a valid label.
|
||||
If the shape of `y_pred` is not consistent for binary classifier.
|
||||
If the response method can be applied to a classifier only and
|
||||
`estimator` is a regressor.
|
||||
"""
|
||||
from sklearn.base import is_classifier, is_outlier_detector # noqa
|
||||
|
||||
if is_classifier(estimator):
|
||||
prediction_method = _check_response_method(estimator, response_method)
|
||||
classes = estimator.classes_
|
||||
target_type = type_of_target(classes)
|
||||
|
||||
if target_type in ("binary", "multiclass"):
|
||||
if pos_label is not None and pos_label not in classes.tolist():
|
||||
raise ValueError(
|
||||
f"pos_label={pos_label} is not a valid label: It should be "
|
||||
f"one of {classes}"
|
||||
)
|
||||
elif pos_label is None and target_type == "binary":
|
||||
pos_label = classes[-1]
|
||||
|
||||
y_pred = prediction_method(X)
|
||||
|
||||
if prediction_method.__name__ in ("predict_proba", "predict_log_proba"):
|
||||
y_pred = _process_predict_proba(
|
||||
y_pred=y_pred,
|
||||
target_type=target_type,
|
||||
classes=classes,
|
||||
pos_label=pos_label,
|
||||
)
|
||||
elif prediction_method.__name__ == "decision_function":
|
||||
y_pred = _process_decision_function(
|
||||
y_pred=y_pred,
|
||||
target_type=target_type,
|
||||
classes=classes,
|
||||
pos_label=pos_label,
|
||||
)
|
||||
elif is_outlier_detector(estimator):
|
||||
prediction_method = _check_response_method(estimator, response_method)
|
||||
y_pred, pos_label = prediction_method(X), None
|
||||
else: # estimator is a regressor
|
||||
if response_method != "predict":
|
||||
raise ValueError(
|
||||
f"{estimator.__class__.__name__} should either be a classifier to be "
|
||||
f"used with response_method={response_method} or the response_method "
|
||||
"should be 'predict'. Got a regressor with response_method="
|
||||
f"{response_method} instead."
|
||||
)
|
||||
prediction_method = estimator.predict
|
||||
y_pred, pos_label = prediction_method(X), None
|
||||
|
||||
if return_response_method_used:
|
||||
return y_pred, pos_label, prediction_method.__name__
|
||||
return y_pred, pos_label
|
||||
|
||||
|
||||
def _get_response_values_binary(estimator, X, response_method, pos_label=None):
|
||||
"""Compute the response values of a binary classifier.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : estimator instance
|
||||
Fitted classifier or a fitted :class:`~sklearn.pipeline.Pipeline`
|
||||
in which the last estimator is a binary classifier.
|
||||
|
||||
X : {array-like, sparse matrix} of shape (n_samples, n_features)
|
||||
Input values.
|
||||
|
||||
response_method : {'auto', 'predict_proba', 'decision_function'}
|
||||
Specifies whether to use :term:`predict_proba` or
|
||||
:term:`decision_function` as the target response. If set to 'auto',
|
||||
:term:`predict_proba` is tried first and if it does not exist
|
||||
:term:`decision_function` is tried next.
|
||||
|
||||
pos_label : int, float, bool or str, default=None
|
||||
The class considered as the positive class when computing
|
||||
the metrics. By default, `estimators.classes_[1]` is
|
||||
considered as the positive class.
|
||||
|
||||
Returns
|
||||
-------
|
||||
y_pred : ndarray of shape (n_samples,)
|
||||
Target scores calculated from the provided response_method
|
||||
and pos_label.
|
||||
|
||||
pos_label : int, float, bool or str
|
||||
The class considered as the positive class when computing
|
||||
the metrics.
|
||||
"""
|
||||
classification_error = "Expected 'estimator' to be a binary classifier."
|
||||
|
||||
check_is_fitted(estimator)
|
||||
if not is_classifier(estimator):
|
||||
raise ValueError(
|
||||
classification_error + f" Got {estimator.__class__.__name__} instead."
|
||||
)
|
||||
elif len(estimator.classes_) != 2:
|
||||
raise ValueError(
|
||||
classification_error + f" Got {len(estimator.classes_)} classes instead."
|
||||
)
|
||||
|
||||
if response_method == "auto":
|
||||
response_method = ["predict_proba", "decision_function"]
|
||||
|
||||
return _get_response_values(
|
||||
estimator,
|
||||
X,
|
||||
response_method,
|
||||
pos_label=pos_label,
|
||||
)
|
||||
Binary file not shown.
@@ -0,0 +1,104 @@
|
||||
# WARNING: Do not edit this file directly.
|
||||
# It is automatically generated from 'sklearn\\utils\\_seq_dataset.pxd.tp'.
|
||||
# Changes must be made there.
|
||||
|
||||
"""Dataset abstractions for sequential data access."""
|
||||
|
||||
cimport numpy as cnp
|
||||
|
||||
# SequentialDataset and its two concrete subclasses are (optionally randomized)
|
||||
# iterators over the rows of a matrix X and corresponding target values y.
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
cdef class SequentialDataset64:
|
||||
cdef int current_index
|
||||
cdef int[::1] index
|
||||
cdef int *index_data_ptr
|
||||
cdef Py_ssize_t n_samples
|
||||
cdef cnp.uint32_t seed
|
||||
|
||||
cdef void shuffle(self, cnp.uint32_t seed) noexcept nogil
|
||||
cdef int _get_next_index(self) noexcept nogil
|
||||
cdef int _get_random_index(self) noexcept nogil
|
||||
|
||||
cdef void _sample(self, double **x_data_ptr, int **x_ind_ptr,
|
||||
int *nnz, double *y, double *sample_weight,
|
||||
int current_index) noexcept nogil
|
||||
cdef void next(self, double **x_data_ptr, int **x_ind_ptr,
|
||||
int *nnz, double *y, double *sample_weight) noexcept nogil
|
||||
cdef int random(self, double **x_data_ptr, int **x_ind_ptr,
|
||||
int *nnz, double *y, double *sample_weight) noexcept nogil
|
||||
|
||||
|
||||
cdef class ArrayDataset64(SequentialDataset64):
|
||||
cdef const double[:, ::1] X
|
||||
cdef const double[::1] Y
|
||||
cdef const double[::1] sample_weights
|
||||
cdef Py_ssize_t n_features
|
||||
cdef cnp.npy_intp X_stride
|
||||
cdef double *X_data_ptr
|
||||
cdef double *Y_data_ptr
|
||||
cdef const int[::1] feature_indices
|
||||
cdef int *feature_indices_ptr
|
||||
cdef double *sample_weight_data
|
||||
|
||||
|
||||
cdef class CSRDataset64(SequentialDataset64):
|
||||
cdef const double[::1] X_data
|
||||
cdef const int[::1] X_indptr
|
||||
cdef const int[::1] X_indices
|
||||
cdef const double[::1] Y
|
||||
cdef const double[::1] sample_weights
|
||||
cdef double *X_data_ptr
|
||||
cdef int *X_indptr_ptr
|
||||
cdef int *X_indices_ptr
|
||||
cdef double *Y_data_ptr
|
||||
cdef double *sample_weight_data
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
cdef class SequentialDataset32:
|
||||
cdef int current_index
|
||||
cdef int[::1] index
|
||||
cdef int *index_data_ptr
|
||||
cdef Py_ssize_t n_samples
|
||||
cdef cnp.uint32_t seed
|
||||
|
||||
cdef void shuffle(self, cnp.uint32_t seed) noexcept nogil
|
||||
cdef int _get_next_index(self) noexcept nogil
|
||||
cdef int _get_random_index(self) noexcept nogil
|
||||
|
||||
cdef void _sample(self, float **x_data_ptr, int **x_ind_ptr,
|
||||
int *nnz, float *y, float *sample_weight,
|
||||
int current_index) noexcept nogil
|
||||
cdef void next(self, float **x_data_ptr, int **x_ind_ptr,
|
||||
int *nnz, float *y, float *sample_weight) noexcept nogil
|
||||
cdef int random(self, float **x_data_ptr, int **x_ind_ptr,
|
||||
int *nnz, float *y, float *sample_weight) noexcept nogil
|
||||
|
||||
|
||||
cdef class ArrayDataset32(SequentialDataset32):
|
||||
cdef const float[:, ::1] X
|
||||
cdef const float[::1] Y
|
||||
cdef const float[::1] sample_weights
|
||||
cdef Py_ssize_t n_features
|
||||
cdef cnp.npy_intp X_stride
|
||||
cdef float *X_data_ptr
|
||||
cdef float *Y_data_ptr
|
||||
cdef const int[::1] feature_indices
|
||||
cdef int *feature_indices_ptr
|
||||
cdef float *sample_weight_data
|
||||
|
||||
|
||||
cdef class CSRDataset32(SequentialDataset32):
|
||||
cdef const float[::1] X_data
|
||||
cdef const int[::1] X_indptr
|
||||
cdef const int[::1] X_indices
|
||||
cdef const float[::1] Y
|
||||
cdef const float[::1] sample_weights
|
||||
cdef float *X_data_ptr
|
||||
cdef int *X_indptr_ptr
|
||||
cdef int *X_indices_ptr
|
||||
cdef float *Y_data_ptr
|
||||
cdef float *sample_weight_data
|
||||
@@ -0,0 +1,441 @@
|
||||
import importlib
|
||||
from functools import wraps
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import issparse
|
||||
|
||||
from .._config import get_config
|
||||
from ._available_if import available_if
|
||||
|
||||
|
||||
def check_library_installed(library):
|
||||
"""Check library is installed."""
|
||||
try:
|
||||
return importlib.import_module(library)
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
f"Setting output container to '{library}' requires {library} to be"
|
||||
" installed"
|
||||
) from exc
|
||||
|
||||
|
||||
def get_columns(columns):
|
||||
if callable(columns):
|
||||
try:
|
||||
return columns()
|
||||
except Exception:
|
||||
return None
|
||||
return columns
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ContainerAdapterProtocol(Protocol):
|
||||
container_lib: str
|
||||
|
||||
def create_container(self, X_output, X_original, columns, inplace=False):
|
||||
"""Create container from `X_output` with additional metadata.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X_output : {ndarray, dataframe}
|
||||
Data to wrap.
|
||||
|
||||
X_original : {ndarray, dataframe}
|
||||
Original input dataframe. This is used to extract the metadata that should
|
||||
be passed to `X_output`, e.g. pandas row index.
|
||||
|
||||
columns : callable, ndarray, or None
|
||||
The column names or a callable that returns the column names. The
|
||||
callable is useful if the column names require some computation. If `None`,
|
||||
then no columns are passed to the container's constructor.
|
||||
|
||||
inplace : bool, default=False
|
||||
Whether or not we intend to modify `X_output` in-place. However, it does
|
||||
not guarantee that we return the same object if the in-place operation
|
||||
is not possible.
|
||||
|
||||
Returns
|
||||
-------
|
||||
wrapped_output : container_type
|
||||
`X_output` wrapped into the container type.
|
||||
"""
|
||||
|
||||
def is_supported_container(self, X):
|
||||
"""Return True if X is a supported container.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
Xs: container
|
||||
Containers to be checked.
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_supported_container : bool
|
||||
True if X is a supported container.
|
||||
"""
|
||||
|
||||
def rename_columns(self, X, columns):
|
||||
"""Rename columns in `X`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : container
|
||||
Container which columns is updated.
|
||||
|
||||
columns : ndarray of str
|
||||
Columns to update the `X`'s columns with.
|
||||
|
||||
Returns
|
||||
-------
|
||||
updated_container : container
|
||||
Container with new names.
|
||||
"""
|
||||
|
||||
def hstack(self, Xs):
|
||||
"""Stack containers horizontally (column-wise).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
Xs : list of containers
|
||||
List of containers to stack.
|
||||
|
||||
Returns
|
||||
-------
|
||||
stacked_Xs : container
|
||||
Stacked containers.
|
||||
"""
|
||||
|
||||
|
||||
class PandasAdapter:
|
||||
container_lib = "pandas"
|
||||
|
||||
def create_container(self, X_output, X_original, columns, inplace=True):
|
||||
pd = check_library_installed("pandas")
|
||||
columns = get_columns(columns)
|
||||
|
||||
if not inplace or not isinstance(X_output, pd.DataFrame):
|
||||
# In all these cases, we need to create a new DataFrame
|
||||
|
||||
# Unfortunately, we cannot use `getattr(container, "index")`
|
||||
# because `list` exposes an `index` attribute.
|
||||
if isinstance(X_output, pd.DataFrame):
|
||||
index = X_output.index
|
||||
elif isinstance(X_original, pd.DataFrame):
|
||||
index = X_original.index
|
||||
else:
|
||||
index = None
|
||||
|
||||
# We don't pass columns here because it would intend columns selection
|
||||
# instead of renaming.
|
||||
X_output = pd.DataFrame(X_output, index=index, copy=not inplace)
|
||||
|
||||
if columns is not None:
|
||||
return self.rename_columns(X_output, columns)
|
||||
return X_output
|
||||
|
||||
def is_supported_container(self, X):
|
||||
pd = check_library_installed("pandas")
|
||||
return isinstance(X, pd.DataFrame)
|
||||
|
||||
def rename_columns(self, X, columns):
|
||||
# we cannot use `rename` since it takes a dictionary and at this stage we have
|
||||
# potentially duplicate column names in `X`
|
||||
X.columns = columns
|
||||
return X
|
||||
|
||||
def hstack(self, Xs):
|
||||
pd = check_library_installed("pandas")
|
||||
return pd.concat(Xs, axis=1)
|
||||
|
||||
|
||||
class PolarsAdapter:
|
||||
container_lib = "polars"
|
||||
|
||||
def create_container(self, X_output, X_original, columns, inplace=True):
|
||||
pl = check_library_installed("polars")
|
||||
columns = get_columns(columns)
|
||||
columns = columns.tolist() if isinstance(columns, np.ndarray) else columns
|
||||
|
||||
if not inplace or not isinstance(X_output, pl.DataFrame):
|
||||
# In all these cases, we need to create a new DataFrame
|
||||
return pl.DataFrame(X_output, schema=columns, orient="row")
|
||||
|
||||
if columns is not None:
|
||||
return self.rename_columns(X_output, columns)
|
||||
return X_output
|
||||
|
||||
def is_supported_container(self, X):
|
||||
pl = check_library_installed("polars")
|
||||
return isinstance(X, pl.DataFrame)
|
||||
|
||||
def rename_columns(self, X, columns):
|
||||
# we cannot use `rename` since it takes a dictionary and at this stage we have
|
||||
# potentially duplicate column names in `X`
|
||||
X.columns = columns
|
||||
return X
|
||||
|
||||
def hstack(self, Xs):
|
||||
pl = check_library_installed("polars")
|
||||
return pl.concat(Xs, how="horizontal")
|
||||
|
||||
|
||||
class ContainerAdaptersManager:
|
||||
def __init__(self):
|
||||
self.adapters = {}
|
||||
|
||||
@property
|
||||
def supported_outputs(self):
|
||||
return {"default"} | set(self.adapters)
|
||||
|
||||
def register(self, adapter):
|
||||
self.adapters[adapter.container_lib] = adapter
|
||||
|
||||
|
||||
ADAPTERS_MANAGER = ContainerAdaptersManager()
|
||||
ADAPTERS_MANAGER.register(PandasAdapter())
|
||||
ADAPTERS_MANAGER.register(PolarsAdapter())
|
||||
|
||||
|
||||
def _get_container_adapter(method, estimator=None):
|
||||
"""Get container adapter."""
|
||||
dense_config = _get_output_config(method, estimator)["dense"]
|
||||
try:
|
||||
return ADAPTERS_MANAGER.adapters[dense_config]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def _get_output_config(method, estimator=None):
|
||||
"""Get output config based on estimator and global configuration.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method : {"transform"}
|
||||
Estimator's method for which the output container is looked up.
|
||||
|
||||
estimator : estimator instance or None
|
||||
Estimator to get the output configuration from. If `None`, check global
|
||||
configuration is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
config : dict
|
||||
Dictionary with keys:
|
||||
|
||||
- "dense": specifies the dense container for `method`. This can be
|
||||
`"default"` or `"pandas"`.
|
||||
"""
|
||||
est_sklearn_output_config = getattr(estimator, "_sklearn_output_config", {})
|
||||
if method in est_sklearn_output_config:
|
||||
dense_config = est_sklearn_output_config[method]
|
||||
else:
|
||||
dense_config = get_config()[f"{method}_output"]
|
||||
|
||||
supported_outputs = ADAPTERS_MANAGER.supported_outputs
|
||||
if dense_config not in supported_outputs:
|
||||
raise ValueError(
|
||||
f"output config must be in {sorted(supported_outputs)}, got {dense_config}"
|
||||
)
|
||||
|
||||
return {"dense": dense_config}
|
||||
|
||||
|
||||
def _wrap_data_with_container(method, data_to_wrap, original_input, estimator):
|
||||
"""Wrap output with container based on an estimator's or global config.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method : {"transform"}
|
||||
Estimator's method to get container output for.
|
||||
|
||||
data_to_wrap : {ndarray, dataframe}
|
||||
Data to wrap with container.
|
||||
|
||||
original_input : {ndarray, dataframe}
|
||||
Original input of function.
|
||||
|
||||
estimator : estimator instance
|
||||
Estimator with to get the output configuration from.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output : {ndarray, dataframe}
|
||||
If the output config is "default" or the estimator is not configured
|
||||
for wrapping return `data_to_wrap` unchanged.
|
||||
If the output config is "pandas", return `data_to_wrap` as a pandas
|
||||
DataFrame.
|
||||
"""
|
||||
output_config = _get_output_config(method, estimator)
|
||||
|
||||
if output_config["dense"] == "default" or not _auto_wrap_is_configured(estimator):
|
||||
return data_to_wrap
|
||||
|
||||
dense_config = output_config["dense"]
|
||||
if issparse(data_to_wrap):
|
||||
raise ValueError(
|
||||
"The transformer outputs a scipy sparse matrix. "
|
||||
"Try to set the transformer output to a dense array or disable "
|
||||
f"{dense_config.capitalize()} output with set_output(transform='default')."
|
||||
)
|
||||
|
||||
adapter = ADAPTERS_MANAGER.adapters[dense_config]
|
||||
return adapter.create_container(
|
||||
data_to_wrap,
|
||||
original_input,
|
||||
columns=estimator.get_feature_names_out,
|
||||
)
|
||||
|
||||
|
||||
def _wrap_method_output(f, method):
|
||||
"""Wrapper used by `_SetOutputMixin` to automatically wrap methods."""
|
||||
|
||||
@wraps(f)
|
||||
def wrapped(self, X, *args, **kwargs):
|
||||
data_to_wrap = f(self, X, *args, **kwargs)
|
||||
if isinstance(data_to_wrap, tuple):
|
||||
# only wrap the first output for cross decomposition
|
||||
return_tuple = (
|
||||
_wrap_data_with_container(method, data_to_wrap[0], X, self),
|
||||
*data_to_wrap[1:],
|
||||
)
|
||||
# Support for namedtuples `_make` is a documented API for namedtuples:
|
||||
# https://docs.python.org/3/library/collections.html#collections.somenamedtuple._make
|
||||
if hasattr(type(data_to_wrap), "_make"):
|
||||
return type(data_to_wrap)._make(return_tuple)
|
||||
return return_tuple
|
||||
|
||||
return _wrap_data_with_container(method, data_to_wrap, X, self)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def _auto_wrap_is_configured(estimator):
|
||||
"""Return True if estimator is configured for auto-wrapping the transform method.
|
||||
|
||||
`_SetOutputMixin` sets `_sklearn_auto_wrap_output_keys` to `set()` if auto wrapping
|
||||
is manually disabled.
|
||||
"""
|
||||
auto_wrap_output_keys = getattr(estimator, "_sklearn_auto_wrap_output_keys", set())
|
||||
return (
|
||||
hasattr(estimator, "get_feature_names_out")
|
||||
and "transform" in auto_wrap_output_keys
|
||||
)
|
||||
|
||||
|
||||
class _SetOutputMixin:
|
||||
"""Mixin that dynamically wraps methods to return container based on config.
|
||||
|
||||
Currently `_SetOutputMixin` wraps `transform` and `fit_transform` and configures
|
||||
it based on `set_output` of the global configuration.
|
||||
|
||||
`set_output` is only defined if `get_feature_names_out` is defined and
|
||||
`auto_wrap_output_keys` is the default value.
|
||||
"""
|
||||
|
||||
def __init_subclass__(cls, auto_wrap_output_keys=("transform",), **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
# Dynamically wraps `transform` and `fit_transform` and configure it's
|
||||
# output based on `set_output`.
|
||||
if not (
|
||||
isinstance(auto_wrap_output_keys, tuple) or auto_wrap_output_keys is None
|
||||
):
|
||||
raise ValueError("auto_wrap_output_keys must be None or a tuple of keys.")
|
||||
|
||||
if auto_wrap_output_keys is None:
|
||||
cls._sklearn_auto_wrap_output_keys = set()
|
||||
return
|
||||
|
||||
# Mapping from method to key in configurations
|
||||
method_to_key = {
|
||||
"transform": "transform",
|
||||
"fit_transform": "transform",
|
||||
}
|
||||
cls._sklearn_auto_wrap_output_keys = set()
|
||||
|
||||
for method, key in method_to_key.items():
|
||||
if not hasattr(cls, method) or key not in auto_wrap_output_keys:
|
||||
continue
|
||||
cls._sklearn_auto_wrap_output_keys.add(key)
|
||||
|
||||
# Only wrap methods defined by cls itself
|
||||
if method not in cls.__dict__:
|
||||
continue
|
||||
wrapped_method = _wrap_method_output(getattr(cls, method), key)
|
||||
setattr(cls, method, wrapped_method)
|
||||
|
||||
@available_if(_auto_wrap_is_configured)
|
||||
def set_output(self, *, transform=None):
|
||||
"""Set output container.
|
||||
|
||||
See :ref:`sphx_glr_auto_examples_miscellaneous_plot_set_output.py`
|
||||
for an example on how to use the API.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
transform : {"default", "pandas"}, default=None
|
||||
Configure output of `transform` and `fit_transform`.
|
||||
|
||||
- `"default"`: Default output format of a transformer
|
||||
- `"pandas"`: DataFrame output
|
||||
- `"polars"`: Polars output
|
||||
- `None`: Transform configuration is unchanged
|
||||
|
||||
.. versionadded:: 1.4
|
||||
`"polars"` option was added.
|
||||
|
||||
Returns
|
||||
-------
|
||||
self : estimator instance
|
||||
Estimator instance.
|
||||
"""
|
||||
if transform is None:
|
||||
return self
|
||||
|
||||
if not hasattr(self, "_sklearn_output_config"):
|
||||
self._sklearn_output_config = {}
|
||||
|
||||
self._sklearn_output_config["transform"] = transform
|
||||
return self
|
||||
|
||||
|
||||
def _safe_set_output(estimator, *, transform=None):
|
||||
"""Safely call estimator.set_output and error if it not available.
|
||||
|
||||
This is used by meta-estimators to set the output for child estimators.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : estimator instance
|
||||
Estimator instance.
|
||||
|
||||
transform : {"default", "pandas"}, default=None
|
||||
Configure output of the following estimator's methods:
|
||||
|
||||
- `"transform"`
|
||||
- `"fit_transform"`
|
||||
|
||||
If `None`, this operation is a no-op.
|
||||
|
||||
Returns
|
||||
-------
|
||||
estimator : estimator instance
|
||||
Estimator instance.
|
||||
"""
|
||||
set_output_for_transform = (
|
||||
hasattr(estimator, "transform")
|
||||
or hasattr(estimator, "fit_transform")
|
||||
and transform is not None
|
||||
)
|
||||
if not set_output_for_transform:
|
||||
# If estimator can not transform, then `set_output` does not need to be
|
||||
# called.
|
||||
return
|
||||
|
||||
if not hasattr(estimator, "set_output"):
|
||||
raise ValueError(
|
||||
f"Unable to configure output for {estimator} because `set_output` "
|
||||
"is not available."
|
||||
)
|
||||
return estimator.set_output(transform=transform)
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Utility methods to print system info for debugging
|
||||
|
||||
adapted from :func:`pandas.show_versions`
|
||||
"""
|
||||
# License: BSD 3 clause
|
||||
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from .. import __version__
|
||||
from ..utils.fixes import threadpool_info
|
||||
from ._openmp_helpers import _openmp_parallelism_enabled
|
||||
|
||||
|
||||
def _get_sys_info():
|
||||
"""System information
|
||||
|
||||
Returns
|
||||
-------
|
||||
sys_info : dict
|
||||
system and Python version information
|
||||
|
||||
"""
|
||||
python = sys.version.replace("\n", " ")
|
||||
|
||||
blob = [
|
||||
("python", python),
|
||||
("executable", sys.executable),
|
||||
("machine", platform.platform()),
|
||||
]
|
||||
|
||||
return dict(blob)
|
||||
|
||||
|
||||
def _get_deps_info():
|
||||
"""Overview of the installed version of main dependencies
|
||||
|
||||
This function does not import the modules to collect the version numbers
|
||||
but instead relies on standard Python package metadata.
|
||||
|
||||
Returns
|
||||
-------
|
||||
deps_info: dict
|
||||
version information on relevant Python libraries
|
||||
|
||||
"""
|
||||
deps = [
|
||||
"pip",
|
||||
"setuptools",
|
||||
"numpy",
|
||||
"scipy",
|
||||
"Cython",
|
||||
"pandas",
|
||||
"matplotlib",
|
||||
"joblib",
|
||||
"threadpoolctl",
|
||||
]
|
||||
|
||||
deps_info = {
|
||||
"sklearn": __version__,
|
||||
}
|
||||
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
for modname in deps:
|
||||
try:
|
||||
deps_info[modname] = version(modname)
|
||||
except PackageNotFoundError:
|
||||
deps_info[modname] = None
|
||||
return deps_info
|
||||
|
||||
|
||||
def show_versions():
|
||||
"""Print useful debugging information"
|
||||
|
||||
.. versionadded:: 0.20
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn import show_versions
|
||||
>>> show_versions() # doctest: +SKIP
|
||||
"""
|
||||
|
||||
sys_info = _get_sys_info()
|
||||
deps_info = _get_deps_info()
|
||||
|
||||
print("\nSystem:")
|
||||
for k, stat in sys_info.items():
|
||||
print("{k:>10}: {stat}".format(k=k, stat=stat))
|
||||
|
||||
print("\nPython dependencies:")
|
||||
for k, stat in deps_info.items():
|
||||
print("{k:>13}: {stat}".format(k=k, stat=stat))
|
||||
|
||||
print(
|
||||
"\n{k}: {stat}".format(
|
||||
k="Built with OpenMP", stat=_openmp_parallelism_enabled()
|
||||
)
|
||||
)
|
||||
|
||||
# show threadpoolctl results
|
||||
threadpool_results = threadpool_info()
|
||||
if threadpool_results:
|
||||
print()
|
||||
print("threadpoolctl info:")
|
||||
|
||||
for i, result in enumerate(threadpool_results):
|
||||
for key, val in result.items():
|
||||
print(f"{key:>15}: {val}")
|
||||
if i != len(threadpool_results) - 1:
|
||||
print()
|
||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
from ._typedefs cimport intp_t
|
||||
|
||||
from cython cimport floating
|
||||
|
||||
cdef int simultaneous_sort(
|
||||
floating *dist,
|
||||
intp_t *idx,
|
||||
intp_t size,
|
||||
) noexcept nogil
|
||||
@@ -0,0 +1,68 @@
|
||||
import numpy as np
|
||||
|
||||
_DEFAULT_TAGS = {
|
||||
"array_api_support": False,
|
||||
"non_deterministic": False,
|
||||
"requires_positive_X": False,
|
||||
"requires_positive_y": False,
|
||||
"X_types": ["2darray"],
|
||||
"poor_score": False,
|
||||
"no_validation": False,
|
||||
"multioutput": False,
|
||||
"allow_nan": False,
|
||||
"stateless": False,
|
||||
"multilabel": False,
|
||||
"_skip_test": False,
|
||||
"_xfail_checks": False,
|
||||
"multioutput_only": False,
|
||||
"binary_only": False,
|
||||
"requires_fit": True,
|
||||
"preserves_dtype": [np.float64],
|
||||
"requires_y": False,
|
||||
"pairwise": False,
|
||||
}
|
||||
|
||||
|
||||
def _safe_tags(estimator, key=None):
|
||||
"""Safely get estimator tags.
|
||||
|
||||
:class:`~sklearn.BaseEstimator` provides the estimator tags machinery.
|
||||
However, if an estimator does not inherit from this base class, we should
|
||||
fall-back to the default tags.
|
||||
|
||||
For scikit-learn built-in estimators, we should still rely on
|
||||
`self._get_tags()`. `_safe_tags(est)` should be used when we are not sure
|
||||
where `est` comes from: typically `_safe_tags(self.base_estimator)` where
|
||||
`self` is a meta-estimator, or in the common checks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : estimator object
|
||||
The estimator from which to get the tag.
|
||||
|
||||
key : str, default=None
|
||||
Tag name to get. By default (`None`), all tags are returned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tags : dict or tag value
|
||||
The estimator tags. A single value is returned if `key` is not None.
|
||||
"""
|
||||
if hasattr(estimator, "_get_tags"):
|
||||
tags_provider = "_get_tags()"
|
||||
tags = estimator._get_tags()
|
||||
elif hasattr(estimator, "_more_tags"):
|
||||
tags_provider = "_more_tags()"
|
||||
tags = {**_DEFAULT_TAGS, **estimator._more_tags()}
|
||||
else:
|
||||
tags_provider = "_DEFAULT_TAGS"
|
||||
tags = _DEFAULT_TAGS
|
||||
|
||||
if key is not None:
|
||||
if key not in tags:
|
||||
raise ValueError(
|
||||
f"The key {key} is not defined in {tags_provider} for the "
|
||||
f"class {estimator.__class__.__name__}."
|
||||
)
|
||||
return tags[key]
|
||||
return tags
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
# Commonly used types
|
||||
# These are redefinitions of the ones defined by numpy in
|
||||
# https://github.com/numpy/numpy/blob/main/numpy/__init__.pxd
|
||||
# and exposed by cython in
|
||||
# https://github.com/cython/cython/blob/master/Cython/Includes/numpy/__init__.pxd.
|
||||
# It will eventually avoid having to always include the numpy headers even when we
|
||||
# would only use it for the types.
|
||||
#
|
||||
# When used to declare variables that will receive values from numpy arrays, it
|
||||
# should match the dtype of the array. For example, to declare a variable that will
|
||||
# receive values from a numpy array of dtype np.float64, the type float64_t must be
|
||||
# used.
|
||||
#
|
||||
# TODO: Stop defining custom types locally or globally like DTYPE_t and friends and
|
||||
# use these consistently throughout the codebase.
|
||||
# NOTE: Extend this list as needed when converting more cython extensions.
|
||||
ctypedef unsigned char uint8_t
|
||||
ctypedef unsigned int uint32_t
|
||||
ctypedef unsigned long long uint64_t
|
||||
ctypedef Py_ssize_t intp_t
|
||||
ctypedef float float32_t
|
||||
ctypedef double float64_t
|
||||
# Sparse matrices indices and indices' pointers arrays must use int32_t over
|
||||
# intp_t because intp_t is platform dependent.
|
||||
# When large sparse matrices are supported, indexing must use int64_t.
|
||||
# See https://github.com/scikit-learn/scikit-learn/issues/23653 which tracks the
|
||||
# ongoing work to support large sparse matrices.
|
||||
ctypedef signed int int32_t
|
||||
ctypedef signed long long int64_t
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
cimport numpy as cnp
|
||||
|
||||
from libcpp.vector cimport vector
|
||||
from ..utils._typedefs cimport intp_t, float64_t, int32_t, int64_t
|
||||
|
||||
ctypedef fused vector_typed:
|
||||
vector[float64_t]
|
||||
vector[intp_t]
|
||||
vector[int32_t]
|
||||
vector[int64_t]
|
||||
|
||||
cdef cnp.ndarray vector_to_nd_array(vector_typed * vect_ptr)
|
||||
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
# WARNING: Do not edit this file directly.
|
||||
# It is automatically generated from 'sklearn\\utils\\_weight_vector.pxd.tp'.
|
||||
# Changes must be made there.
|
||||
|
||||
|
||||
cdef class WeightVector64(object):
|
||||
cdef readonly double[::1] w
|
||||
cdef readonly double[::1] aw
|
||||
cdef double *w_data_ptr
|
||||
cdef double *aw_data_ptr
|
||||
|
||||
cdef double wscale
|
||||
cdef double average_a
|
||||
cdef double average_b
|
||||
cdef int n_features
|
||||
cdef double sq_norm
|
||||
|
||||
cdef void add(self, double *x_data_ptr, int *x_ind_ptr,
|
||||
int xnnz, double c) noexcept nogil
|
||||
cdef void add_average(self, double *x_data_ptr, int *x_ind_ptr,
|
||||
int xnnz, double c, double num_iter) noexcept nogil
|
||||
cdef double dot(self, double *x_data_ptr, int *x_ind_ptr,
|
||||
int xnnz) noexcept nogil
|
||||
cdef void scale(self, double c) noexcept nogil
|
||||
cdef void reset_wscale(self) noexcept nogil
|
||||
cdef double norm(self) noexcept nogil
|
||||
|
||||
cdef class WeightVector32(object):
|
||||
cdef readonly float[::1] w
|
||||
cdef readonly float[::1] aw
|
||||
cdef float *w_data_ptr
|
||||
cdef float *aw_data_ptr
|
||||
|
||||
cdef double wscale
|
||||
cdef double average_a
|
||||
cdef double average_b
|
||||
cdef int n_features
|
||||
cdef double sq_norm
|
||||
|
||||
cdef void add(self, float *x_data_ptr, int *x_ind_ptr,
|
||||
int xnnz, float c) noexcept nogil
|
||||
cdef void add_average(self, float *x_data_ptr, int *x_ind_ptr,
|
||||
int xnnz, float c, float num_iter) noexcept nogil
|
||||
cdef float dot(self, float *x_data_ptr, int *x_ind_ptr,
|
||||
int xnnz) noexcept nogil
|
||||
cdef void scale(self, float c) noexcept nogil
|
||||
cdef void reset_wscale(self) noexcept nogil
|
||||
cdef float norm(self) noexcept nogil
|
||||
Binary file not shown.
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
The :mod:`sklearn.utils.class_weight` module includes utilities for handling
|
||||
weights based on class labels.
|
||||
"""
|
||||
|
||||
# Authors: Andreas Mueller
|
||||
# Manoj Kumar
|
||||
# License: BSD 3 clause
|
||||
|
||||
import numpy as np
|
||||
from scipy import sparse
|
||||
|
||||
from ._param_validation import StrOptions, validate_params
|
||||
|
||||
|
||||
@validate_params(
|
||||
{
|
||||
"class_weight": [dict, StrOptions({"balanced"}), None],
|
||||
"classes": [np.ndarray],
|
||||
"y": ["array-like"],
|
||||
},
|
||||
prefer_skip_nested_validation=True,
|
||||
)
|
||||
def compute_class_weight(class_weight, *, classes, y):
|
||||
"""Estimate class weights for unbalanced datasets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
class_weight : dict, "balanced" or None
|
||||
If "balanced", class weights will be given by
|
||||
`n_samples / (n_classes * np.bincount(y))`.
|
||||
If a dictionary is given, keys are classes and values are corresponding class
|
||||
weights.
|
||||
If `None` is given, the class weights will be uniform.
|
||||
|
||||
classes : ndarray
|
||||
Array of the classes occurring in the data, as given by
|
||||
`np.unique(y_org)` with `y_org` the original class labels.
|
||||
|
||||
y : array-like of shape (n_samples,)
|
||||
Array of original class labels per sample.
|
||||
|
||||
Returns
|
||||
-------
|
||||
class_weight_vect : ndarray of shape (n_classes,)
|
||||
Array with `class_weight_vect[i]` the weight for i-th class.
|
||||
|
||||
References
|
||||
----------
|
||||
The "balanced" heuristic is inspired by
|
||||
Logistic Regression in Rare Events Data, King, Zen, 2001.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from sklearn.utils.class_weight import compute_class_weight
|
||||
>>> y = [1, 1, 1, 1, 0, 0]
|
||||
>>> compute_class_weight(class_weight="balanced", classes=np.unique(y), y=y)
|
||||
array([1.5 , 0.75])
|
||||
"""
|
||||
# Import error caused by circular imports.
|
||||
from ..preprocessing import LabelEncoder
|
||||
|
||||
if set(y) - set(classes):
|
||||
raise ValueError("classes should include all valid labels that can be in y")
|
||||
if class_weight is None or len(class_weight) == 0:
|
||||
# uniform class weights
|
||||
weight = np.ones(classes.shape[0], dtype=np.float64, order="C")
|
||||
elif class_weight == "balanced":
|
||||
# Find the weight of each class as present in y.
|
||||
le = LabelEncoder()
|
||||
y_ind = le.fit_transform(y)
|
||||
if not all(np.isin(classes, le.classes_)):
|
||||
raise ValueError("classes should have valid labels that are in y")
|
||||
|
||||
recip_freq = len(y) / (len(le.classes_) * np.bincount(y_ind).astype(np.float64))
|
||||
weight = recip_freq[le.transform(classes)]
|
||||
else:
|
||||
# user-defined dictionary
|
||||
weight = np.ones(classes.shape[0], dtype=np.float64, order="C")
|
||||
unweighted_classes = []
|
||||
for i, c in enumerate(classes):
|
||||
if c in class_weight:
|
||||
weight[i] = class_weight[c]
|
||||
else:
|
||||
unweighted_classes.append(c)
|
||||
|
||||
n_weighted_classes = len(classes) - len(unweighted_classes)
|
||||
if unweighted_classes and n_weighted_classes != len(class_weight):
|
||||
unweighted_classes_user_friendly_str = np.array(unweighted_classes).tolist()
|
||||
raise ValueError(
|
||||
f"The classes, {unweighted_classes_user_friendly_str}, are not in"
|
||||
" class_weight"
|
||||
)
|
||||
|
||||
return weight
|
||||
|
||||
|
||||
@validate_params(
|
||||
{
|
||||
"class_weight": [dict, list, StrOptions({"balanced"}), None],
|
||||
"y": ["array-like", "sparse matrix"],
|
||||
"indices": ["array-like", None],
|
||||
},
|
||||
prefer_skip_nested_validation=True,
|
||||
)
|
||||
def compute_sample_weight(class_weight, y, *, indices=None):
|
||||
"""Estimate sample weights by class for unbalanced datasets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
class_weight : dict, list of dicts, "balanced", or None
|
||||
Weights associated with classes in the form `{class_label: weight}`.
|
||||
If not given, all classes are supposed to have weight one. For
|
||||
multi-output problems, a list of dicts can be provided in the same
|
||||
order as the columns of y.
|
||||
|
||||
Note that for multioutput (including multilabel) weights should be
|
||||
defined for each class of every column in its own dict. For example,
|
||||
for four-class multilabel classification weights should be
|
||||
`[{0: 1, 1: 1}, {0: 1, 1: 5}, {0: 1, 1: 1}, {0: 1, 1: 1}]` instead of
|
||||
`[{1:1}, {2:5}, {3:1}, {4:1}]`.
|
||||
|
||||
The `"balanced"` mode uses the values of y to automatically adjust
|
||||
weights inversely proportional to class frequencies in the input data:
|
||||
`n_samples / (n_classes * np.bincount(y))`.
|
||||
|
||||
For multi-output, the weights of each column of y will be multiplied.
|
||||
|
||||
y : {array-like, sparse matrix} of shape (n_samples,) or (n_samples, n_outputs)
|
||||
Array of original class labels per sample.
|
||||
|
||||
indices : array-like of shape (n_subsample,), default=None
|
||||
Array of indices to be used in a subsample. Can be of length less than
|
||||
`n_samples` in the case of a subsample, or equal to `n_samples` in the
|
||||
case of a bootstrap subsample with repeated indices. If `None`, the
|
||||
sample weight will be calculated over the full sample. Only `"balanced"`
|
||||
is supported for `class_weight` if this is provided.
|
||||
|
||||
Returns
|
||||
-------
|
||||
sample_weight_vect : ndarray of shape (n_samples,)
|
||||
Array with sample weights as applied to the original `y`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils.class_weight import compute_sample_weight
|
||||
>>> y = [1, 1, 1, 1, 0, 0]
|
||||
>>> compute_sample_weight(class_weight="balanced", y=y)
|
||||
array([0.75, 0.75, 0.75, 0.75, 1.5 , 1.5 ])
|
||||
"""
|
||||
|
||||
# Ensure y is 2D. Sparse matrices are already 2D.
|
||||
if not sparse.issparse(y):
|
||||
y = np.atleast_1d(y)
|
||||
if y.ndim == 1:
|
||||
y = np.reshape(y, (-1, 1))
|
||||
n_outputs = y.shape[1]
|
||||
|
||||
if indices is not None and class_weight != "balanced":
|
||||
raise ValueError(
|
||||
"The only valid class_weight for subsampling is 'balanced'. "
|
||||
f"Given {class_weight}."
|
||||
)
|
||||
elif n_outputs > 1:
|
||||
if class_weight is None or isinstance(class_weight, dict):
|
||||
raise ValueError(
|
||||
"For multi-output, class_weight should be a list of dicts, or the "
|
||||
"string 'balanced'."
|
||||
)
|
||||
elif isinstance(class_weight, list) and len(class_weight) != n_outputs:
|
||||
raise ValueError(
|
||||
"For multi-output, number of elements in class_weight should match "
|
||||
f"number of outputs. Got {len(class_weight)} element(s) while having "
|
||||
f"{n_outputs} outputs."
|
||||
)
|
||||
|
||||
expanded_class_weight = []
|
||||
for k in range(n_outputs):
|
||||
if sparse.issparse(y):
|
||||
# Ok to densify a single column at a time
|
||||
y_full = y[:, [k]].toarray().flatten()
|
||||
else:
|
||||
y_full = y[:, k]
|
||||
classes_full = np.unique(y_full)
|
||||
classes_missing = None
|
||||
|
||||
if class_weight == "balanced" or n_outputs == 1:
|
||||
class_weight_k = class_weight
|
||||
else:
|
||||
class_weight_k = class_weight[k]
|
||||
|
||||
if indices is not None:
|
||||
# Get class weights for the subsample, covering all classes in
|
||||
# case some labels that were present in the original data are
|
||||
# missing from the sample.
|
||||
y_subsample = y_full[indices]
|
||||
classes_subsample = np.unique(y_subsample)
|
||||
|
||||
weight_k = np.take(
|
||||
compute_class_weight(
|
||||
class_weight_k, classes=classes_subsample, y=y_subsample
|
||||
),
|
||||
np.searchsorted(classes_subsample, classes_full),
|
||||
mode="clip",
|
||||
)
|
||||
|
||||
classes_missing = set(classes_full) - set(classes_subsample)
|
||||
else:
|
||||
weight_k = compute_class_weight(
|
||||
class_weight_k, classes=classes_full, y=y_full
|
||||
)
|
||||
|
||||
weight_k = weight_k[np.searchsorted(classes_full, y_full)]
|
||||
|
||||
if classes_missing:
|
||||
# Make missing classes' weight zero
|
||||
weight_k[np.isin(y_full, list(classes_missing))] = 0.0
|
||||
|
||||
expanded_class_weight.append(weight_k)
|
||||
|
||||
expanded_class_weight = np.prod(expanded_class_weight, axis=0, dtype=np.float64)
|
||||
|
||||
return expanded_class_weight
|
||||
@@ -0,0 +1,116 @@
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
__all__ = ["deprecated"]
|
||||
|
||||
|
||||
class deprecated:
|
||||
"""Decorator to mark a function or class as deprecated.
|
||||
|
||||
Issue a warning when the function is called/the class is instantiated and
|
||||
adds a warning to the docstring.
|
||||
|
||||
The optional extra argument will be appended to the deprecation message
|
||||
and the docstring. Note: to use this with the default value for extra, put
|
||||
in an empty of parentheses:
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import deprecated
|
||||
>>> deprecated()
|
||||
<sklearn.utils.deprecation.deprecated object at ...>
|
||||
>>> @deprecated()
|
||||
... def some_function(): pass
|
||||
|
||||
Parameters
|
||||
----------
|
||||
extra : str, default=''
|
||||
To be added to the deprecation messages.
|
||||
"""
|
||||
|
||||
# Adapted from https://wiki.python.org/moin/PythonDecoratorLibrary,
|
||||
# but with many changes.
|
||||
|
||||
def __init__(self, extra=""):
|
||||
self.extra = extra
|
||||
|
||||
def __call__(self, obj):
|
||||
"""Call method
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : object
|
||||
"""
|
||||
if isinstance(obj, type):
|
||||
return self._decorate_class(obj)
|
||||
elif isinstance(obj, property):
|
||||
# Note that this is only triggered properly if the `property`
|
||||
# decorator comes before the `deprecated` decorator, like so:
|
||||
#
|
||||
# @deprecated(msg)
|
||||
# @property
|
||||
# def deprecated_attribute_(self):
|
||||
# ...
|
||||
return self._decorate_property(obj)
|
||||
else:
|
||||
return self._decorate_fun(obj)
|
||||
|
||||
def _decorate_class(self, cls):
|
||||
msg = "Class %s is deprecated" % cls.__name__
|
||||
if self.extra:
|
||||
msg += "; %s" % self.extra
|
||||
|
||||
new = cls.__new__
|
||||
|
||||
def wrapped(cls, *args, **kwargs):
|
||||
warnings.warn(msg, category=FutureWarning)
|
||||
if new is object.__new__:
|
||||
return object.__new__(cls)
|
||||
return new(cls, *args, **kwargs)
|
||||
|
||||
cls.__new__ = wrapped
|
||||
|
||||
wrapped.__name__ = "__new__"
|
||||
wrapped.deprecated_original = new
|
||||
|
||||
return cls
|
||||
|
||||
def _decorate_fun(self, fun):
|
||||
"""Decorate function fun"""
|
||||
|
||||
msg = "Function %s is deprecated" % fun.__name__
|
||||
if self.extra:
|
||||
msg += "; %s" % self.extra
|
||||
|
||||
@functools.wraps(fun)
|
||||
def wrapped(*args, **kwargs):
|
||||
warnings.warn(msg, category=FutureWarning)
|
||||
return fun(*args, **kwargs)
|
||||
|
||||
# Add a reference to the wrapped function so that we can introspect
|
||||
# on function arguments in Python 2 (already works in Python 3)
|
||||
wrapped.__wrapped__ = fun
|
||||
|
||||
return wrapped
|
||||
|
||||
def _decorate_property(self, prop):
|
||||
msg = self.extra
|
||||
|
||||
@property
|
||||
@functools.wraps(prop)
|
||||
def wrapped(*args, **kwargs):
|
||||
warnings.warn(msg, category=FutureWarning)
|
||||
return prop.fget(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def _is_deprecated(func):
|
||||
"""Helper to check if func is wrapped by our deprecated decorator"""
|
||||
closures = getattr(func, "__closure__", [])
|
||||
if closures is None:
|
||||
closures = []
|
||||
is_deprecated = "deprecated" in "".join(
|
||||
[c.cell_contents for c in closures if isinstance(c.cell_contents, str)]
|
||||
)
|
||||
return is_deprecated
|
||||
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
The :mod:`sklearn.utils.discovery` module includes utilities to discover
|
||||
objects (i.e. estimators, displays, functions) from the `sklearn` package.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import pkgutil
|
||||
from importlib import import_module
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
|
||||
_MODULE_TO_IGNORE = {
|
||||
"tests",
|
||||
"externals",
|
||||
"setup",
|
||||
"conftest",
|
||||
"experimental",
|
||||
"estimator_checks",
|
||||
}
|
||||
|
||||
|
||||
def all_estimators(type_filter=None):
|
||||
"""Get a list of all estimators from `sklearn`.
|
||||
|
||||
This function crawls the module and gets all classes that inherit
|
||||
from BaseEstimator. Classes that are defined in test-modules are not
|
||||
included.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
type_filter : {"classifier", "regressor", "cluster", "transformer"} \
|
||||
or list of such str, default=None
|
||||
Which kind of estimators should be returned. If None, no filter is
|
||||
applied and all estimators are returned. Possible values are
|
||||
'classifier', 'regressor', 'cluster' and 'transformer' to get
|
||||
estimators only of these specific types, or a list of these to
|
||||
get the estimators that fit at least one of the types.
|
||||
|
||||
Returns
|
||||
-------
|
||||
estimators : list of tuples
|
||||
List of (name, class), where ``name`` is the class name as string
|
||||
and ``class`` is the actual type of the class.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils.discovery import all_estimators
|
||||
>>> estimators = all_estimators()
|
||||
>>> type(estimators)
|
||||
<class 'list'>
|
||||
>>> type(estimators[0])
|
||||
<class 'tuple'>
|
||||
>>> estimators[:2]
|
||||
[('ARDRegression', <class 'sklearn.linear_model._bayes.ARDRegression'>),
|
||||
('AdaBoostClassifier',
|
||||
<class 'sklearn.ensemble._weight_boosting.AdaBoostClassifier'>)]
|
||||
>>> classifiers = all_estimators(type_filter="classifier")
|
||||
>>> classifiers[:2]
|
||||
[('AdaBoostClassifier',
|
||||
<class 'sklearn.ensemble._weight_boosting.AdaBoostClassifier'>),
|
||||
('BaggingClassifier', <class 'sklearn.ensemble._bagging.BaggingClassifier'>)]
|
||||
>>> regressors = all_estimators(type_filter="regressor")
|
||||
>>> regressors[:2]
|
||||
[('ARDRegression', <class 'sklearn.linear_model._bayes.ARDRegression'>),
|
||||
('AdaBoostRegressor',
|
||||
<class 'sklearn.ensemble._weight_boosting.AdaBoostRegressor'>)]
|
||||
>>> both = all_estimators(type_filter=["classifier", "regressor"])
|
||||
>>> both[:2]
|
||||
[('ARDRegression', <class 'sklearn.linear_model._bayes.ARDRegression'>),
|
||||
('AdaBoostClassifier',
|
||||
<class 'sklearn.ensemble._weight_boosting.AdaBoostClassifier'>)]
|
||||
"""
|
||||
# lazy import to avoid circular imports from sklearn.base
|
||||
from ..base import (
|
||||
BaseEstimator,
|
||||
ClassifierMixin,
|
||||
ClusterMixin,
|
||||
RegressorMixin,
|
||||
TransformerMixin,
|
||||
)
|
||||
from . import IS_PYPY
|
||||
from ._testing import ignore_warnings
|
||||
|
||||
def is_abstract(c):
|
||||
if not (hasattr(c, "__abstractmethods__")):
|
||||
return False
|
||||
if not len(c.__abstractmethods__):
|
||||
return False
|
||||
return True
|
||||
|
||||
all_classes = []
|
||||
root = str(Path(__file__).parent.parent) # sklearn package
|
||||
# Ignore deprecation warnings triggered at import time and from walking
|
||||
# packages
|
||||
with ignore_warnings(category=FutureWarning):
|
||||
for _, module_name, _ in pkgutil.walk_packages(path=[root], prefix="sklearn."):
|
||||
module_parts = module_name.split(".")
|
||||
if (
|
||||
any(part in _MODULE_TO_IGNORE for part in module_parts)
|
||||
or "._" in module_name
|
||||
):
|
||||
continue
|
||||
module = import_module(module_name)
|
||||
classes = inspect.getmembers(module, inspect.isclass)
|
||||
classes = [
|
||||
(name, est_cls) for name, est_cls in classes if not name.startswith("_")
|
||||
]
|
||||
|
||||
# TODO: Remove when FeatureHasher is implemented in PYPY
|
||||
# Skips FeatureHasher for PYPY
|
||||
if IS_PYPY and "feature_extraction" in module_name:
|
||||
classes = [
|
||||
(name, est_cls)
|
||||
for name, est_cls in classes
|
||||
if name == "FeatureHasher"
|
||||
]
|
||||
|
||||
all_classes.extend(classes)
|
||||
|
||||
all_classes = set(all_classes)
|
||||
|
||||
estimators = [
|
||||
c
|
||||
for c in all_classes
|
||||
if (issubclass(c[1], BaseEstimator) and c[0] != "BaseEstimator")
|
||||
]
|
||||
# get rid of abstract base classes
|
||||
estimators = [c for c in estimators if not is_abstract(c[1])]
|
||||
|
||||
if type_filter is not None:
|
||||
if not isinstance(type_filter, list):
|
||||
type_filter = [type_filter]
|
||||
else:
|
||||
type_filter = list(type_filter) # copy
|
||||
filtered_estimators = []
|
||||
filters = {
|
||||
"classifier": ClassifierMixin,
|
||||
"regressor": RegressorMixin,
|
||||
"transformer": TransformerMixin,
|
||||
"cluster": ClusterMixin,
|
||||
}
|
||||
for name, mixin in filters.items():
|
||||
if name in type_filter:
|
||||
type_filter.remove(name)
|
||||
filtered_estimators.extend(
|
||||
[est for est in estimators if issubclass(est[1], mixin)]
|
||||
)
|
||||
estimators = filtered_estimators
|
||||
if type_filter:
|
||||
raise ValueError(
|
||||
"Parameter type_filter must be 'classifier', "
|
||||
"'regressor', 'transformer', 'cluster' or "
|
||||
"None, got"
|
||||
f" {repr(type_filter)}."
|
||||
)
|
||||
|
||||
# drop duplicates, sort for reproducibility
|
||||
# itemgetter is used to ensure the sort does not extend to the 2nd item of
|
||||
# the tuple
|
||||
return sorted(set(estimators), key=itemgetter(0))
|
||||
|
||||
|
||||
def all_displays():
|
||||
"""Get a list of all displays from `sklearn`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
displays : list of tuples
|
||||
List of (name, class), where ``name`` is the display class name as
|
||||
string and ``class`` is the actual type of the class.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils.discovery import all_displays
|
||||
>>> displays = all_displays()
|
||||
>>> displays[0]
|
||||
('CalibrationDisplay', <class 'sklearn.calibration.CalibrationDisplay'>)
|
||||
"""
|
||||
# lazy import to avoid circular imports from sklearn.base
|
||||
from ._testing import ignore_warnings
|
||||
|
||||
all_classes = []
|
||||
root = str(Path(__file__).parent.parent) # sklearn package
|
||||
# Ignore deprecation warnings triggered at import time and from walking
|
||||
# packages
|
||||
with ignore_warnings(category=FutureWarning):
|
||||
for _, module_name, _ in pkgutil.walk_packages(path=[root], prefix="sklearn."):
|
||||
module_parts = module_name.split(".")
|
||||
if (
|
||||
any(part in _MODULE_TO_IGNORE for part in module_parts)
|
||||
or "._" in module_name
|
||||
):
|
||||
continue
|
||||
module = import_module(module_name)
|
||||
classes = inspect.getmembers(module, inspect.isclass)
|
||||
classes = [
|
||||
(name, display_class)
|
||||
for name, display_class in classes
|
||||
if not name.startswith("_") and name.endswith("Display")
|
||||
]
|
||||
all_classes.extend(classes)
|
||||
|
||||
return sorted(set(all_classes), key=itemgetter(0))
|
||||
|
||||
|
||||
def _is_checked_function(item):
|
||||
if not inspect.isfunction(item):
|
||||
return False
|
||||
|
||||
if item.__name__.startswith("_"):
|
||||
return False
|
||||
|
||||
mod = item.__module__
|
||||
if not mod.startswith("sklearn.") or mod.endswith("estimator_checks"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def all_functions():
|
||||
"""Get a list of all functions from `sklearn`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
functions : list of tuples
|
||||
List of (name, function), where ``name`` is the function name as
|
||||
string and ``function`` is the actual function.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils.discovery import all_functions
|
||||
>>> functions = all_functions()
|
||||
>>> name, function = functions[0]
|
||||
>>> name
|
||||
'accuracy_score'
|
||||
"""
|
||||
# lazy import to avoid circular imports from sklearn.base
|
||||
from ._testing import ignore_warnings
|
||||
|
||||
all_functions = []
|
||||
root = str(Path(__file__).parent.parent) # sklearn package
|
||||
# Ignore deprecation warnings triggered at import time and from walking
|
||||
# packages
|
||||
with ignore_warnings(category=FutureWarning):
|
||||
for _, module_name, _ in pkgutil.walk_packages(path=[root], prefix="sklearn."):
|
||||
module_parts = module_name.split(".")
|
||||
if (
|
||||
any(part in _MODULE_TO_IGNORE for part in module_parts)
|
||||
or "._" in module_name
|
||||
):
|
||||
continue
|
||||
|
||||
module = import_module(module_name)
|
||||
functions = inspect.getmembers(module, _is_checked_function)
|
||||
functions = [
|
||||
(func.__name__, func)
|
||||
for name, func in functions
|
||||
if not name.startswith("_")
|
||||
]
|
||||
all_functions.extend(functions)
|
||||
|
||||
# drop duplicates, sort for reproducibility
|
||||
# itemgetter is used to ensure the sort does not extend to the 2nd item of
|
||||
# the tuple
|
||||
return sorted(set(all_functions), key=itemgetter(0))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,394 @@
|
||||
"""Compatibility fixes for older version of python, numpy and scipy
|
||||
|
||||
If you add content to this file, please give the version of the package
|
||||
at which the fix is no longer needed.
|
||||
"""
|
||||
# Authors: Emmanuelle Gouillart <emmanuelle.gouillart@normalesup.org>
|
||||
# Gael Varoquaux <gael.varoquaux@normalesup.org>
|
||||
# Fabian Pedregosa <fpedregosa@acm.org>
|
||||
# Lars Buitinck
|
||||
#
|
||||
# License: BSD 3 clause
|
||||
|
||||
|
||||
import numpy as np
|
||||
import scipy
|
||||
import scipy.sparse.linalg
|
||||
import scipy.stats
|
||||
import threadpoolctl
|
||||
|
||||
import sklearn
|
||||
|
||||
from ..externals._packaging.version import parse as parse_version
|
||||
from .deprecation import deprecated
|
||||
|
||||
np_version = parse_version(np.__version__)
|
||||
np_base_version = parse_version(np_version.base_version)
|
||||
sp_version = parse_version(scipy.__version__)
|
||||
sp_base_version = parse_version(sp_version.base_version)
|
||||
|
||||
# TODO: We can consider removing the containers and importing
|
||||
# directly from SciPy when sparse matrices will be deprecated.
|
||||
CSR_CONTAINERS = [scipy.sparse.csr_matrix]
|
||||
CSC_CONTAINERS = [scipy.sparse.csc_matrix]
|
||||
COO_CONTAINERS = [scipy.sparse.coo_matrix]
|
||||
LIL_CONTAINERS = [scipy.sparse.lil_matrix]
|
||||
DOK_CONTAINERS = [scipy.sparse.dok_matrix]
|
||||
BSR_CONTAINERS = [scipy.sparse.bsr_matrix]
|
||||
DIA_CONTAINERS = [scipy.sparse.dia_matrix]
|
||||
|
||||
if parse_version(scipy.__version__) >= parse_version("1.8"):
|
||||
# Sparse Arrays have been added in SciPy 1.8
|
||||
# TODO: When SciPy 1.8 is the minimum supported version,
|
||||
# those list can be created directly without this condition.
|
||||
# See: https://github.com/scikit-learn/scikit-learn/issues/27090
|
||||
CSR_CONTAINERS.append(scipy.sparse.csr_array)
|
||||
CSC_CONTAINERS.append(scipy.sparse.csc_array)
|
||||
COO_CONTAINERS.append(scipy.sparse.coo_array)
|
||||
LIL_CONTAINERS.append(scipy.sparse.lil_array)
|
||||
DOK_CONTAINERS.append(scipy.sparse.dok_array)
|
||||
BSR_CONTAINERS.append(scipy.sparse.bsr_array)
|
||||
DIA_CONTAINERS.append(scipy.sparse.dia_array)
|
||||
|
||||
try:
|
||||
from scipy.optimize._linesearch import line_search_wolfe1, line_search_wolfe2
|
||||
except ImportError: # SciPy < 1.8
|
||||
from scipy.optimize.linesearch import line_search_wolfe2, line_search_wolfe1 # type: ignore # noqa
|
||||
|
||||
|
||||
def _object_dtype_isnan(X):
|
||||
return X != X
|
||||
|
||||
|
||||
# Rename the `method` kwarg to `interpolation` for NumPy < 1.22, because
|
||||
# `interpolation` kwarg was deprecated in favor of `method` in NumPy >= 1.22.
|
||||
def _percentile(a, q, *, method="linear", **kwargs):
|
||||
return np.percentile(a, q, interpolation=method, **kwargs)
|
||||
|
||||
|
||||
if np_version < parse_version("1.22"):
|
||||
percentile = _percentile
|
||||
else: # >= 1.22
|
||||
from numpy import percentile # type: ignore # noqa
|
||||
|
||||
|
||||
# compatibility fix for threadpoolctl >= 3.0.0
|
||||
# since version 3 it's possible to setup a global threadpool controller to avoid
|
||||
# looping through all loaded shared libraries each time.
|
||||
# the global controller is created during the first call to threadpoolctl.
|
||||
def _get_threadpool_controller():
|
||||
if not hasattr(threadpoolctl, "ThreadpoolController"):
|
||||
return None
|
||||
|
||||
if not hasattr(sklearn, "_sklearn_threadpool_controller"):
|
||||
sklearn._sklearn_threadpool_controller = threadpoolctl.ThreadpoolController()
|
||||
|
||||
return sklearn._sklearn_threadpool_controller
|
||||
|
||||
|
||||
def threadpool_limits(limits=None, user_api=None):
|
||||
controller = _get_threadpool_controller()
|
||||
if controller is not None:
|
||||
return controller.limit(limits=limits, user_api=user_api)
|
||||
else:
|
||||
return threadpoolctl.threadpool_limits(limits=limits, user_api=user_api)
|
||||
|
||||
|
||||
threadpool_limits.__doc__ = threadpoolctl.threadpool_limits.__doc__
|
||||
|
||||
|
||||
def threadpool_info():
|
||||
controller = _get_threadpool_controller()
|
||||
if controller is not None:
|
||||
return controller.info()
|
||||
else:
|
||||
return threadpoolctl.threadpool_info()
|
||||
|
||||
|
||||
threadpool_info.__doc__ = threadpoolctl.threadpool_info.__doc__
|
||||
|
||||
|
||||
@deprecated(
|
||||
"The function `delayed` has been moved from `sklearn.utils.fixes` to "
|
||||
"`sklearn.utils.parallel`. This import path will be removed in 1.5."
|
||||
)
|
||||
def delayed(function):
|
||||
from sklearn.utils.parallel import delayed
|
||||
|
||||
return delayed(function)
|
||||
|
||||
|
||||
# TODO: Remove when SciPy 1.11 is the minimum supported version
|
||||
def _mode(a, axis=0):
|
||||
if sp_version >= parse_version("1.9.0"):
|
||||
mode = scipy.stats.mode(a, axis=axis, keepdims=True)
|
||||
if sp_version >= parse_version("1.10.999"):
|
||||
# scipy.stats.mode has changed returned array shape with axis=None
|
||||
# and keepdims=True, see https://github.com/scipy/scipy/pull/17561
|
||||
if axis is None:
|
||||
mode = np.ravel(mode)
|
||||
return mode
|
||||
return scipy.stats.mode(a, axis=axis)
|
||||
|
||||
|
||||
# TODO: Remove when Scipy 1.12 is the minimum supported version
|
||||
if sp_base_version >= parse_version("1.12.0"):
|
||||
_sparse_linalg_cg = scipy.sparse.linalg.cg
|
||||
else:
|
||||
|
||||
def _sparse_linalg_cg(A, b, **kwargs):
|
||||
if "rtol" in kwargs:
|
||||
kwargs["tol"] = kwargs.pop("rtol")
|
||||
if "atol" not in kwargs:
|
||||
kwargs["atol"] = "legacy"
|
||||
return scipy.sparse.linalg.cg(A, b, **kwargs)
|
||||
|
||||
|
||||
# TODO: Fuse the modern implementations of _sparse_min_max and _sparse_nan_min_max
|
||||
# into the public min_max_axis function when Scipy 1.11 is the minimum supported
|
||||
# version and delete the backport in the else branch below.
|
||||
if sp_base_version >= parse_version("1.11.0"):
|
||||
|
||||
def _sparse_min_max(X, axis):
|
||||
the_min = X.min(axis=axis)
|
||||
the_max = X.max(axis=axis)
|
||||
|
||||
if axis is not None:
|
||||
the_min = the_min.toarray().ravel()
|
||||
the_max = the_max.toarray().ravel()
|
||||
|
||||
return the_min, the_max
|
||||
|
||||
def _sparse_nan_min_max(X, axis):
|
||||
the_min = X.nanmin(axis=axis)
|
||||
the_max = X.nanmax(axis=axis)
|
||||
|
||||
if axis is not None:
|
||||
the_min = the_min.toarray().ravel()
|
||||
the_max = the_max.toarray().ravel()
|
||||
|
||||
return the_min, the_max
|
||||
|
||||
else:
|
||||
# This code is mostly taken from scipy 0.14 and extended to handle nans, see
|
||||
# https://github.com/scikit-learn/scikit-learn/pull/11196
|
||||
def _minor_reduce(X, ufunc):
|
||||
major_index = np.flatnonzero(np.diff(X.indptr))
|
||||
|
||||
# reduceat tries casts X.indptr to intp, which errors
|
||||
# if it is int64 on a 32 bit system.
|
||||
# Reinitializing prevents this where possible, see #13737
|
||||
X = type(X)((X.data, X.indices, X.indptr), shape=X.shape)
|
||||
value = ufunc.reduceat(X.data, X.indptr[major_index])
|
||||
return major_index, value
|
||||
|
||||
def _min_or_max_axis(X, axis, min_or_max):
|
||||
N = X.shape[axis]
|
||||
if N == 0:
|
||||
raise ValueError("zero-size array to reduction operation")
|
||||
M = X.shape[1 - axis]
|
||||
mat = X.tocsc() if axis == 0 else X.tocsr()
|
||||
mat.sum_duplicates()
|
||||
major_index, value = _minor_reduce(mat, min_or_max)
|
||||
not_full = np.diff(mat.indptr)[major_index] < N
|
||||
value[not_full] = min_or_max(value[not_full], 0)
|
||||
mask = value != 0
|
||||
major_index = np.compress(mask, major_index)
|
||||
value = np.compress(mask, value)
|
||||
|
||||
if axis == 0:
|
||||
res = scipy.sparse.coo_matrix(
|
||||
(value, (np.zeros(len(value)), major_index)),
|
||||
dtype=X.dtype,
|
||||
shape=(1, M),
|
||||
)
|
||||
else:
|
||||
res = scipy.sparse.coo_matrix(
|
||||
(value, (major_index, np.zeros(len(value)))),
|
||||
dtype=X.dtype,
|
||||
shape=(M, 1),
|
||||
)
|
||||
return res.A.ravel()
|
||||
|
||||
def _sparse_min_or_max(X, axis, min_or_max):
|
||||
if axis is None:
|
||||
if 0 in X.shape:
|
||||
raise ValueError("zero-size array to reduction operation")
|
||||
zero = X.dtype.type(0)
|
||||
if X.nnz == 0:
|
||||
return zero
|
||||
m = min_or_max.reduce(X.data.ravel())
|
||||
if X.nnz != np.prod(X.shape):
|
||||
m = min_or_max(zero, m)
|
||||
return m
|
||||
if axis < 0:
|
||||
axis += 2
|
||||
if (axis == 0) or (axis == 1):
|
||||
return _min_or_max_axis(X, axis, min_or_max)
|
||||
else:
|
||||
raise ValueError("invalid axis, use 0 for rows, or 1 for columns")
|
||||
|
||||
def _sparse_min_max(X, axis):
|
||||
return (
|
||||
_sparse_min_or_max(X, axis, np.minimum),
|
||||
_sparse_min_or_max(X, axis, np.maximum),
|
||||
)
|
||||
|
||||
def _sparse_nan_min_max(X, axis):
|
||||
return (
|
||||
_sparse_min_or_max(X, axis, np.fmin),
|
||||
_sparse_min_or_max(X, axis, np.fmax),
|
||||
)
|
||||
|
||||
|
||||
# For +1.25 NumPy versions exceptions and warnings are being moved
|
||||
# to a dedicated submodule.
|
||||
if np_version >= parse_version("1.25.0"):
|
||||
from numpy.exceptions import ComplexWarning, VisibleDeprecationWarning
|
||||
else:
|
||||
from numpy import ComplexWarning, VisibleDeprecationWarning # type: ignore # noqa
|
||||
|
||||
|
||||
# TODO: Remove when Scipy 1.6 is the minimum supported version
|
||||
try:
|
||||
from scipy.integrate import trapezoid # type: ignore # noqa
|
||||
except ImportError:
|
||||
from scipy.integrate import trapz as trapezoid # type: ignore # noqa
|
||||
|
||||
|
||||
# TODO: Adapt when Pandas > 2.2 is the minimum supported version
|
||||
def pd_fillna(pd, frame):
|
||||
pd_version = parse_version(pd.__version__).base_version
|
||||
if parse_version(pd_version) < parse_version("2.2"):
|
||||
frame = frame.fillna(value=np.nan)
|
||||
else:
|
||||
infer_objects_kwargs = (
|
||||
{} if parse_version(pd_version) >= parse_version("3") else {"copy": False}
|
||||
)
|
||||
with pd.option_context("future.no_silent_downcasting", True):
|
||||
frame = frame.fillna(value=np.nan).infer_objects(**infer_objects_kwargs)
|
||||
return frame
|
||||
|
||||
|
||||
# TODO: remove when SciPy 1.12 is the minimum supported version
|
||||
def _preserve_dia_indices_dtype(
|
||||
sparse_container, original_container_format, requested_sparse_format
|
||||
):
|
||||
"""Preserve indices dtype for SciPy < 1.12 when converting from DIA to CSR/CSC.
|
||||
|
||||
For SciPy < 1.12, DIA arrays indices are upcasted to `np.int64` that is
|
||||
inconsistent with DIA matrices. We downcast the indices dtype to `np.int32` to
|
||||
be consistent with DIA matrices.
|
||||
|
||||
The converted indices arrays are affected back inplace to the sparse container.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sparse_container : sparse container
|
||||
Sparse container to be checked.
|
||||
requested_sparse_format : str or bool
|
||||
The type of format of `sparse_container`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
See https://github.com/scipy/scipy/issues/19245 for more details.
|
||||
"""
|
||||
if original_container_format == "dia_array" and requested_sparse_format in (
|
||||
"csr",
|
||||
"coo",
|
||||
):
|
||||
if requested_sparse_format == "csr":
|
||||
index_dtype = _smallest_admissible_index_dtype(
|
||||
arrays=(sparse_container.indptr, sparse_container.indices),
|
||||
maxval=max(sparse_container.nnz, sparse_container.shape[1]),
|
||||
check_contents=True,
|
||||
)
|
||||
sparse_container.indices = sparse_container.indices.astype(
|
||||
index_dtype, copy=False
|
||||
)
|
||||
sparse_container.indptr = sparse_container.indptr.astype(
|
||||
index_dtype, copy=False
|
||||
)
|
||||
else: # requested_sparse_format == "coo"
|
||||
index_dtype = _smallest_admissible_index_dtype(
|
||||
maxval=max(sparse_container.shape)
|
||||
)
|
||||
sparse_container.row = sparse_container.row.astype(index_dtype, copy=False)
|
||||
sparse_container.col = sparse_container.col.astype(index_dtype, copy=False)
|
||||
|
||||
|
||||
# TODO: remove when SciPy 1.12 is the minimum supported version
|
||||
def _smallest_admissible_index_dtype(arrays=(), maxval=None, check_contents=False):
|
||||
"""Based on input (integer) arrays `a`, determine a suitable index data
|
||||
type that can hold the data in the arrays.
|
||||
|
||||
This function returns `np.int64` if it either required by `maxval` or based on the
|
||||
largest precision of the dtype of the arrays passed as argument, or by the their
|
||||
contents (when `check_contents is True`). If none of the condition requires
|
||||
`np.int64` then this function returns `np.int32`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arrays : ndarray or tuple of ndarrays, default=()
|
||||
Input arrays whose types/contents to check.
|
||||
|
||||
maxval : float, default=None
|
||||
Maximum value needed.
|
||||
|
||||
check_contents : bool, default=False
|
||||
Whether to check the values in the arrays and not just their types.
|
||||
By default, check only the types.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dtype : {np.int32, np.int64}
|
||||
Suitable index data type (int32 or int64).
|
||||
"""
|
||||
|
||||
int32min = np.int32(np.iinfo(np.int32).min)
|
||||
int32max = np.int32(np.iinfo(np.int32).max)
|
||||
|
||||
if maxval is not None:
|
||||
if maxval > np.iinfo(np.int64).max:
|
||||
raise ValueError(
|
||||
f"maxval={maxval} is to large to be represented as np.int64."
|
||||
)
|
||||
if maxval > int32max:
|
||||
return np.int64
|
||||
|
||||
if isinstance(arrays, np.ndarray):
|
||||
arrays = (arrays,)
|
||||
|
||||
for arr in arrays:
|
||||
if not isinstance(arr, np.ndarray):
|
||||
raise TypeError(
|
||||
f"Arrays should be of type np.ndarray, got {type(arr)} instead."
|
||||
)
|
||||
if not np.issubdtype(arr.dtype, np.integer):
|
||||
raise ValueError(
|
||||
f"Array dtype {arr.dtype} is not supported for index dtype. We expect "
|
||||
"integral values."
|
||||
)
|
||||
if not np.can_cast(arr.dtype, np.int32):
|
||||
if not check_contents:
|
||||
# when `check_contents` is False, we stay on the safe side and return
|
||||
# np.int64.
|
||||
return np.int64
|
||||
if arr.size == 0:
|
||||
# a bigger type not needed yet, let's look at the next array
|
||||
continue
|
||||
else:
|
||||
maxval = arr.max()
|
||||
minval = arr.min()
|
||||
if minval < int32min or maxval > int32max:
|
||||
# a big index type is actually needed
|
||||
return np.int64
|
||||
|
||||
return np.int32
|
||||
|
||||
|
||||
# TODO: Remove when Scipy 1.12 is the minimum supported version
|
||||
if sp_version < parse_version("1.12"):
|
||||
from ..externals._scipy.sparse.csgraph import laplacian # type: ignore # noqa
|
||||
else:
|
||||
from scipy.sparse.csgraph import laplacian # type: ignore # noqa # pragma: no cover
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
The :mod:`sklearn.utils.graph` module includes graph utilities and algorithms.
|
||||
"""
|
||||
|
||||
# Authors: Aric Hagberg <hagberg@lanl.gov>
|
||||
# Gael Varoquaux <gael.varoquaux@normalesup.org>
|
||||
# Jake Vanderplas <vanderplas@astro.washington.edu>
|
||||
# License: BSD 3 clause
|
||||
|
||||
import numpy as np
|
||||
from scipy import sparse
|
||||
|
||||
from ..metrics.pairwise import pairwise_distances
|
||||
from ._param_validation import Integral, Interval, validate_params
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Path and connected component analysis.
|
||||
# Code adapted from networkx
|
||||
@validate_params(
|
||||
{
|
||||
"graph": ["array-like", "sparse matrix"],
|
||||
"source": [Interval(Integral, 0, None, closed="left")],
|
||||
"cutoff": [Interval(Integral, 0, None, closed="left"), None],
|
||||
},
|
||||
prefer_skip_nested_validation=True,
|
||||
)
|
||||
def single_source_shortest_path_length(graph, source, *, cutoff=None):
|
||||
"""Return the length of the shortest path from source to all reachable nodes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
graph : {array-like, sparse matrix} of shape (n_nodes, n_nodes)
|
||||
Adjacency matrix of the graph. Sparse matrix of format LIL is
|
||||
preferred.
|
||||
|
||||
source : int
|
||||
Start node for path.
|
||||
|
||||
cutoff : int, default=None
|
||||
Depth to stop the search - only paths of length <= cutoff are returned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
paths : dict
|
||||
Reachable end nodes mapped to length of path from source,
|
||||
i.e. `{end: path_length}`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils.graph import single_source_shortest_path_length
|
||||
>>> import numpy as np
|
||||
>>> graph = np.array([[ 0, 1, 0, 0],
|
||||
... [ 1, 0, 1, 0],
|
||||
... [ 0, 1, 0, 0],
|
||||
... [ 0, 0, 0, 0]])
|
||||
>>> single_source_shortest_path_length(graph, 0)
|
||||
{0: 0, 1: 1, 2: 2}
|
||||
>>> graph = np.ones((6, 6))
|
||||
>>> sorted(single_source_shortest_path_length(graph, 2).items())
|
||||
[(0, 1), (1, 1), (2, 0), (3, 1), (4, 1), (5, 1)]
|
||||
"""
|
||||
if sparse.issparse(graph):
|
||||
graph = graph.tolil()
|
||||
else:
|
||||
graph = sparse.lil_matrix(graph)
|
||||
seen = {} # level (number of hops) when seen in BFS
|
||||
level = 0 # the current level
|
||||
next_level = [source] # dict of nodes to check at next level
|
||||
while next_level:
|
||||
this_level = next_level # advance to next level
|
||||
next_level = set() # and start a new list (fringe)
|
||||
for v in this_level:
|
||||
if v not in seen:
|
||||
seen[v] = level # set the level of vertex v
|
||||
next_level.update(graph.rows[v])
|
||||
if cutoff is not None and cutoff <= level:
|
||||
break
|
||||
level += 1
|
||||
return seen # return all path lengths as dictionary
|
||||
|
||||
|
||||
def _fix_connected_components(
|
||||
X,
|
||||
graph,
|
||||
n_connected_components,
|
||||
component_labels,
|
||||
mode="distance",
|
||||
metric="euclidean",
|
||||
**kwargs,
|
||||
):
|
||||
"""Add connections to sparse graph to connect unconnected components.
|
||||
|
||||
For each pair of unconnected components, compute all pairwise distances
|
||||
from one component to the other, and add a connection on the closest pair
|
||||
of samples. This is a hacky way to get a graph with a single connected
|
||||
component, which is necessary for example to compute a shortest path
|
||||
between all pairs of samples in the graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : array of shape (n_samples, n_features) or (n_samples, n_samples)
|
||||
Features to compute the pairwise distances. If `metric =
|
||||
"precomputed"`, X is the matrix of pairwise distances.
|
||||
|
||||
graph : sparse matrix of shape (n_samples, n_samples)
|
||||
Graph of connection between samples.
|
||||
|
||||
n_connected_components : int
|
||||
Number of connected components, as computed by
|
||||
`scipy.sparse.csgraph.connected_components`.
|
||||
|
||||
component_labels : array of shape (n_samples)
|
||||
Labels of connected components, as computed by
|
||||
`scipy.sparse.csgraph.connected_components`.
|
||||
|
||||
mode : {'connectivity', 'distance'}, default='distance'
|
||||
Type of graph matrix: 'connectivity' corresponds to the connectivity
|
||||
matrix with ones and zeros, and 'distance' corresponds to the distances
|
||||
between neighbors according to the given metric.
|
||||
|
||||
metric : str
|
||||
Metric used in `sklearn.metrics.pairwise.pairwise_distances`.
|
||||
|
||||
kwargs : kwargs
|
||||
Keyword arguments passed to
|
||||
`sklearn.metrics.pairwise.pairwise_distances`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
graph : sparse matrix of shape (n_samples, n_samples)
|
||||
Graph of connection between samples, with a single connected component.
|
||||
"""
|
||||
if metric == "precomputed" and sparse.issparse(X):
|
||||
raise RuntimeError(
|
||||
"_fix_connected_components with metric='precomputed' requires the "
|
||||
"full distance matrix in X, and does not work with a sparse "
|
||||
"neighbors graph."
|
||||
)
|
||||
|
||||
for i in range(n_connected_components):
|
||||
idx_i = np.flatnonzero(component_labels == i)
|
||||
Xi = X[idx_i]
|
||||
for j in range(i):
|
||||
idx_j = np.flatnonzero(component_labels == j)
|
||||
Xj = X[idx_j]
|
||||
|
||||
if metric == "precomputed":
|
||||
D = X[np.ix_(idx_i, idx_j)]
|
||||
else:
|
||||
D = pairwise_distances(Xi, Xj, metric=metric, **kwargs)
|
||||
|
||||
ii, jj = np.unravel_index(D.argmin(axis=None), D.shape)
|
||||
if mode == "connectivity":
|
||||
graph[idx_i[ii], idx_j[jj]] = 1
|
||||
graph[idx_j[jj], idx_i[ii]] = 1
|
||||
elif mode == "distance":
|
||||
graph[idx_i[ii], idx_j[jj]] = D[ii, jj]
|
||||
graph[idx_j[jj], idx_i[ii]] = D[ii, jj]
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unknown mode=%r, should be one of ['connectivity', 'distance']."
|
||||
% mode
|
||||
)
|
||||
|
||||
return graph
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
The :mod:`sklearn.utils.metadata_routing` module includes utilities to route
|
||||
metadata within scikit-learn estimators.
|
||||
"""
|
||||
|
||||
# This module is not a separate sub-folder since that would result in a circular
|
||||
# import issue.
|
||||
#
|
||||
# Author: Adrin Jalali <adrin.jalali@gmail.com>
|
||||
# License: BSD 3 clause
|
||||
|
||||
from ._metadata_requests import WARN, UNUSED, UNCHANGED # noqa
|
||||
from ._metadata_requests import get_routing_for_object # noqa
|
||||
from ._metadata_requests import MetadataRouter # noqa
|
||||
from ._metadata_requests import MetadataRequest # noqa
|
||||
from ._metadata_requests import MethodMapping # noqa
|
||||
from ._metadata_requests import process_routing # noqa
|
||||
from ._metadata_requests import _MetadataRequester # noqa
|
||||
from ._metadata_requests import _routing_enabled # noqa
|
||||
from ._metadata_requests import _raise_for_params # noqa
|
||||
from ._metadata_requests import _RoutingNotSupportedMixin # noqa
|
||||
from ._metadata_requests import _raise_for_unsupported_routing # noqa
|
||||
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
The :mod:`sklearn.utils.metaestimators` module includes utilities for meta-estimators.
|
||||
"""
|
||||
|
||||
# Author: Joel Nothman
|
||||
# Andreas Mueller
|
||||
# License: BSD
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from contextlib import suppress
|
||||
from typing import Any, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..base import BaseEstimator
|
||||
from ..utils import _safe_indexing
|
||||
from ..utils._tags import _safe_tags
|
||||
from ._available_if import available_if
|
||||
|
||||
__all__ = ["available_if"]
|
||||
|
||||
|
||||
class _BaseComposition(BaseEstimator, metaclass=ABCMeta):
|
||||
"""Handles parameter management for classifiers composed of named estimators."""
|
||||
|
||||
steps: List[Any]
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _get_params(self, attr, deep=True):
|
||||
out = super().get_params(deep=deep)
|
||||
if not deep:
|
||||
return out
|
||||
|
||||
estimators = getattr(self, attr)
|
||||
try:
|
||||
out.update(estimators)
|
||||
except (TypeError, ValueError):
|
||||
# Ignore TypeError for cases where estimators is not a list of
|
||||
# (name, estimator) and ignore ValueError when the list is not
|
||||
# formatted correctly. This is to prevent errors when calling
|
||||
# `set_params`. `BaseEstimator.set_params` calls `get_params` which
|
||||
# can error for invalid values for `estimators`.
|
||||
return out
|
||||
|
||||
for name, estimator in estimators:
|
||||
if hasattr(estimator, "get_params"):
|
||||
for key, value in estimator.get_params(deep=True).items():
|
||||
out["%s__%s" % (name, key)] = value
|
||||
return out
|
||||
|
||||
def _set_params(self, attr, **params):
|
||||
# Ensure strict ordering of parameter setting:
|
||||
# 1. All steps
|
||||
if attr in params:
|
||||
setattr(self, attr, params.pop(attr))
|
||||
# 2. Replace items with estimators in params
|
||||
items = getattr(self, attr)
|
||||
if isinstance(items, list) and items:
|
||||
# Get item names used to identify valid names in params
|
||||
# `zip` raises a TypeError when `items` does not contains
|
||||
# elements of length 2
|
||||
with suppress(TypeError):
|
||||
item_names, _ = zip(*items)
|
||||
for name in list(params.keys()):
|
||||
if "__" not in name and name in item_names:
|
||||
self._replace_estimator(attr, name, params.pop(name))
|
||||
|
||||
# 3. Step parameters and other initialisation arguments
|
||||
super().set_params(**params)
|
||||
return self
|
||||
|
||||
def _replace_estimator(self, attr, name, new_val):
|
||||
# assumes `name` is a valid estimator name
|
||||
new_estimators = list(getattr(self, attr))
|
||||
for i, (estimator_name, _) in enumerate(new_estimators):
|
||||
if estimator_name == name:
|
||||
new_estimators[i] = (name, new_val)
|
||||
break
|
||||
setattr(self, attr, new_estimators)
|
||||
|
||||
def _validate_names(self, names):
|
||||
if len(set(names)) != len(names):
|
||||
raise ValueError("Names provided are not unique: {0!r}".format(list(names)))
|
||||
invalid_names = set(names).intersection(self.get_params(deep=False))
|
||||
if invalid_names:
|
||||
raise ValueError(
|
||||
"Estimator names conflict with constructor arguments: {0!r}".format(
|
||||
sorted(invalid_names)
|
||||
)
|
||||
)
|
||||
invalid_names = [name for name in names if "__" in name]
|
||||
if invalid_names:
|
||||
raise ValueError(
|
||||
"Estimator names must not contain __: got {0!r}".format(invalid_names)
|
||||
)
|
||||
|
||||
|
||||
def _safe_split(estimator, X, y, indices, train_indices=None):
|
||||
"""Create subset of dataset and properly handle kernels.
|
||||
|
||||
Slice X, y according to indices for cross-validation, but take care of
|
||||
precomputed kernel-matrices or pairwise affinities / distances.
|
||||
|
||||
If ``estimator._pairwise is True``, X needs to be square and
|
||||
we slice rows and columns. If ``train_indices`` is not None,
|
||||
we slice rows using ``indices`` (assumed the test set) and columns
|
||||
using ``train_indices``, indicating the training set.
|
||||
|
||||
Labels y will always be indexed only along the first axis.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : object
|
||||
Estimator to determine whether we should slice only rows or rows and
|
||||
columns.
|
||||
|
||||
X : array-like, sparse matrix or iterable
|
||||
Data to be indexed. If ``estimator._pairwise is True``,
|
||||
this needs to be a square array-like or sparse matrix.
|
||||
|
||||
y : array-like, sparse matrix or iterable
|
||||
Targets to be indexed.
|
||||
|
||||
indices : array of int
|
||||
Rows to select from X and y.
|
||||
If ``estimator._pairwise is True`` and ``train_indices is None``
|
||||
then ``indices`` will also be used to slice columns.
|
||||
|
||||
train_indices : array of int or None, default=None
|
||||
If ``estimator._pairwise is True`` and ``train_indices is not None``,
|
||||
then ``train_indices`` will be use to slice the columns of X.
|
||||
|
||||
Returns
|
||||
-------
|
||||
X_subset : array-like, sparse matrix or list
|
||||
Indexed data.
|
||||
|
||||
y_subset : array-like, sparse matrix or list
|
||||
Indexed targets.
|
||||
|
||||
"""
|
||||
if _safe_tags(estimator, key="pairwise"):
|
||||
if not hasattr(X, "shape"):
|
||||
raise ValueError(
|
||||
"Precomputed kernels or affinity matrices have "
|
||||
"to be passed as arrays or sparse matrices."
|
||||
)
|
||||
# X is a precomputed square kernel matrix
|
||||
if X.shape[0] != X.shape[1]:
|
||||
raise ValueError("X should be a square kernel matrix")
|
||||
if train_indices is None:
|
||||
X_subset = X[np.ix_(indices, indices)]
|
||||
else:
|
||||
X_subset = X[np.ix_(indices, train_indices)]
|
||||
else:
|
||||
X_subset = _safe_indexing(X, indices)
|
||||
|
||||
if y is not None:
|
||||
y_subset = _safe_indexing(y, indices)
|
||||
else:
|
||||
y_subset = None
|
||||
|
||||
return X_subset, y_subset
|
||||
@@ -0,0 +1,553 @@
|
||||
"""
|
||||
The :mod:`sklearn.utils.multiclass` module includes utilities to handle
|
||||
multiclass/multioutput target in classifiers.
|
||||
"""
|
||||
|
||||
# Author: Arnaud Joly, Joel Nothman, Hamzeh Alsalhi
|
||||
#
|
||||
# License: BSD 3 clause
|
||||
import warnings
|
||||
from collections.abc import Sequence
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import issparse
|
||||
|
||||
from ..utils._array_api import get_namespace
|
||||
from ..utils.fixes import VisibleDeprecationWarning
|
||||
from .validation import _assert_all_finite, check_array
|
||||
|
||||
|
||||
def _unique_multiclass(y):
|
||||
xp, is_array_api_compliant = get_namespace(y)
|
||||
if hasattr(y, "__array__") or is_array_api_compliant:
|
||||
return xp.unique_values(xp.asarray(y))
|
||||
else:
|
||||
return set(y)
|
||||
|
||||
|
||||
def _unique_indicator(y):
|
||||
xp, _ = get_namespace(y)
|
||||
return xp.arange(
|
||||
check_array(y, input_name="y", accept_sparse=["csr", "csc", "coo"]).shape[1]
|
||||
)
|
||||
|
||||
|
||||
_FN_UNIQUE_LABELS = {
|
||||
"binary": _unique_multiclass,
|
||||
"multiclass": _unique_multiclass,
|
||||
"multilabel-indicator": _unique_indicator,
|
||||
}
|
||||
|
||||
|
||||
def unique_labels(*ys):
|
||||
"""Extract an ordered array of unique labels.
|
||||
|
||||
We don't allow:
|
||||
- mix of multilabel and multiclass (single label) targets
|
||||
- mix of label indicator matrix and anything else,
|
||||
because there are no explicit labels)
|
||||
- mix of label indicator matrices of different sizes
|
||||
- mix of string and integer labels
|
||||
|
||||
At the moment, we also don't allow "multiclass-multioutput" input type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*ys : array-likes
|
||||
Label values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : ndarray of shape (n_unique_labels,)
|
||||
An ordered array of unique labels.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils.multiclass import unique_labels
|
||||
>>> unique_labels([3, 5, 5, 5, 7, 7])
|
||||
array([3, 5, 7])
|
||||
>>> unique_labels([1, 2, 3, 4], [2, 2, 3, 4])
|
||||
array([1, 2, 3, 4])
|
||||
>>> unique_labels([1, 2, 10], [5, 11])
|
||||
array([ 1, 2, 5, 10, 11])
|
||||
"""
|
||||
xp, is_array_api_compliant = get_namespace(*ys)
|
||||
if not ys:
|
||||
raise ValueError("No argument has been passed.")
|
||||
# Check that we don't mix label format
|
||||
|
||||
ys_types = set(type_of_target(x) for x in ys)
|
||||
if ys_types == {"binary", "multiclass"}:
|
||||
ys_types = {"multiclass"}
|
||||
|
||||
if len(ys_types) > 1:
|
||||
raise ValueError("Mix type of y not allowed, got types %s" % ys_types)
|
||||
|
||||
label_type = ys_types.pop()
|
||||
|
||||
# Check consistency for the indicator format
|
||||
if (
|
||||
label_type == "multilabel-indicator"
|
||||
and len(
|
||||
set(
|
||||
check_array(y, accept_sparse=["csr", "csc", "coo"]).shape[1] for y in ys
|
||||
)
|
||||
)
|
||||
> 1
|
||||
):
|
||||
raise ValueError(
|
||||
"Multi-label binary indicator input with different numbers of labels"
|
||||
)
|
||||
|
||||
# Get the unique set of labels
|
||||
_unique_labels = _FN_UNIQUE_LABELS.get(label_type, None)
|
||||
if not _unique_labels:
|
||||
raise ValueError("Unknown label type: %s" % repr(ys))
|
||||
|
||||
if is_array_api_compliant:
|
||||
# array_api does not allow for mixed dtypes
|
||||
unique_ys = xp.concat([_unique_labels(y) for y in ys])
|
||||
return xp.unique_values(unique_ys)
|
||||
|
||||
ys_labels = set(chain.from_iterable((i for i in _unique_labels(y)) for y in ys))
|
||||
# Check that we don't mix string type with number type
|
||||
if len(set(isinstance(label, str) for label in ys_labels)) > 1:
|
||||
raise ValueError("Mix of label input types (string and number)")
|
||||
|
||||
return xp.asarray(sorted(ys_labels))
|
||||
|
||||
|
||||
def _is_integral_float(y):
|
||||
xp, is_array_api_compliant = get_namespace(y)
|
||||
return xp.isdtype(y.dtype, "real floating") and bool(
|
||||
xp.all(xp.astype((xp.astype(y, xp.int64)), y.dtype) == y)
|
||||
)
|
||||
|
||||
|
||||
def is_multilabel(y):
|
||||
"""Check if ``y`` is in a multilabel format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
y : ndarray of shape (n_samples,)
|
||||
Target values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : bool
|
||||
Return ``True``, if ``y`` is in a multilabel format, else ```False``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from sklearn.utils.multiclass import is_multilabel
|
||||
>>> is_multilabel([0, 1, 0, 1])
|
||||
False
|
||||
>>> is_multilabel([[1], [0, 2], []])
|
||||
False
|
||||
>>> is_multilabel(np.array([[1, 0], [0, 0]]))
|
||||
True
|
||||
>>> is_multilabel(np.array([[1], [0], [0]]))
|
||||
False
|
||||
>>> is_multilabel(np.array([[1, 0, 0]]))
|
||||
True
|
||||
"""
|
||||
xp, is_array_api_compliant = get_namespace(y)
|
||||
if hasattr(y, "__array__") or isinstance(y, Sequence) or is_array_api_compliant:
|
||||
# DeprecationWarning will be replaced by ValueError, see NEP 34
|
||||
# https://numpy.org/neps/nep-0034-infer-dtype-is-object.html
|
||||
check_y_kwargs = dict(
|
||||
accept_sparse=True,
|
||||
allow_nd=True,
|
||||
force_all_finite=False,
|
||||
ensure_2d=False,
|
||||
ensure_min_samples=0,
|
||||
ensure_min_features=0,
|
||||
)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error", VisibleDeprecationWarning)
|
||||
try:
|
||||
y = check_array(y, dtype=None, **check_y_kwargs)
|
||||
except (VisibleDeprecationWarning, ValueError) as e:
|
||||
if str(e).startswith("Complex data not supported"):
|
||||
raise
|
||||
|
||||
# dtype=object should be provided explicitly for ragged arrays,
|
||||
# see NEP 34
|
||||
y = check_array(y, dtype=object, **check_y_kwargs)
|
||||
|
||||
if not (hasattr(y, "shape") and y.ndim == 2 and y.shape[1] > 1):
|
||||
return False
|
||||
|
||||
if issparse(y):
|
||||
if y.format in ("dok", "lil"):
|
||||
y = y.tocsr()
|
||||
labels = xp.unique_values(y.data)
|
||||
return (
|
||||
len(y.data) == 0
|
||||
or (labels.size == 1 or (labels.size == 2) and (0 in labels))
|
||||
and (y.dtype.kind in "biu" or _is_integral_float(labels)) # bool, int, uint
|
||||
)
|
||||
else:
|
||||
labels = xp.unique_values(y)
|
||||
|
||||
return labels.shape[0] < 3 and (
|
||||
xp.isdtype(y.dtype, ("bool", "signed integer", "unsigned integer"))
|
||||
or _is_integral_float(labels)
|
||||
)
|
||||
|
||||
|
||||
def check_classification_targets(y):
|
||||
"""Ensure that target y is of a non-regression type.
|
||||
|
||||
Only the following target types (as defined in type_of_target) are allowed:
|
||||
'binary', 'multiclass', 'multiclass-multioutput',
|
||||
'multilabel-indicator', 'multilabel-sequences'
|
||||
|
||||
Parameters
|
||||
----------
|
||||
y : array-like
|
||||
Target values.
|
||||
"""
|
||||
y_type = type_of_target(y, input_name="y")
|
||||
if y_type not in [
|
||||
"binary",
|
||||
"multiclass",
|
||||
"multiclass-multioutput",
|
||||
"multilabel-indicator",
|
||||
"multilabel-sequences",
|
||||
]:
|
||||
raise ValueError(
|
||||
f"Unknown label type: {y_type}. Maybe you are trying to fit a "
|
||||
"classifier, which expects discrete classes on a "
|
||||
"regression target with continuous values."
|
||||
)
|
||||
|
||||
|
||||
def type_of_target(y, input_name=""):
|
||||
"""Determine the type of data indicated by the target.
|
||||
|
||||
Note that this type is the most specific type that can be inferred.
|
||||
For example:
|
||||
|
||||
* ``binary`` is more specific but compatible with ``multiclass``.
|
||||
* ``multiclass`` of integers is more specific but compatible with
|
||||
``continuous``.
|
||||
* ``multilabel-indicator`` is more specific but compatible with
|
||||
``multiclass-multioutput``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
y : {array-like, sparse matrix}
|
||||
Target values. If a sparse matrix, `y` is expected to be a
|
||||
CSR/CSC matrix.
|
||||
|
||||
input_name : str, default=""
|
||||
The data name used to construct the error message.
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
target_type : str
|
||||
One of:
|
||||
|
||||
* 'continuous': `y` is an array-like of floats that are not all
|
||||
integers, and is 1d or a column vector.
|
||||
* 'continuous-multioutput': `y` is a 2d array of floats that are
|
||||
not all integers, and both dimensions are of size > 1.
|
||||
* 'binary': `y` contains <= 2 discrete values and is 1d or a column
|
||||
vector.
|
||||
* 'multiclass': `y` contains more than two discrete values, is not a
|
||||
sequence of sequences, and is 1d or a column vector.
|
||||
* 'multiclass-multioutput': `y` is a 2d array that contains more
|
||||
than two discrete values, is not a sequence of sequences, and both
|
||||
dimensions are of size > 1.
|
||||
* 'multilabel-indicator': `y` is a label indicator matrix, an array
|
||||
of two dimensions with at least two columns, and at most 2 unique
|
||||
values.
|
||||
* 'unknown': `y` is array-like but none of the above, such as a 3d
|
||||
array, sequence of sequences, or an array of non-sequence objects.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils.multiclass import type_of_target
|
||||
>>> import numpy as np
|
||||
>>> type_of_target([0.1, 0.6])
|
||||
'continuous'
|
||||
>>> type_of_target([1, -1, -1, 1])
|
||||
'binary'
|
||||
>>> type_of_target(['a', 'b', 'a'])
|
||||
'binary'
|
||||
>>> type_of_target([1.0, 2.0])
|
||||
'binary'
|
||||
>>> type_of_target([1, 0, 2])
|
||||
'multiclass'
|
||||
>>> type_of_target([1.0, 0.0, 3.0])
|
||||
'multiclass'
|
||||
>>> type_of_target(['a', 'b', 'c'])
|
||||
'multiclass'
|
||||
>>> type_of_target(np.array([[1, 2], [3, 1]]))
|
||||
'multiclass-multioutput'
|
||||
>>> type_of_target([[1, 2]])
|
||||
'multilabel-indicator'
|
||||
>>> type_of_target(np.array([[1.5, 2.0], [3.0, 1.6]]))
|
||||
'continuous-multioutput'
|
||||
>>> type_of_target(np.array([[0, 1], [1, 1]]))
|
||||
'multilabel-indicator'
|
||||
"""
|
||||
xp, is_array_api_compliant = get_namespace(y)
|
||||
valid = (
|
||||
(isinstance(y, Sequence) or issparse(y) or hasattr(y, "__array__"))
|
||||
and not isinstance(y, str)
|
||||
or is_array_api_compliant
|
||||
)
|
||||
|
||||
if not valid:
|
||||
raise ValueError(
|
||||
"Expected array-like (array or non-string sequence), got %r" % y
|
||||
)
|
||||
|
||||
sparse_pandas = y.__class__.__name__ in ["SparseSeries", "SparseArray"]
|
||||
if sparse_pandas:
|
||||
raise ValueError("y cannot be class 'SparseSeries' or 'SparseArray'")
|
||||
|
||||
if is_multilabel(y):
|
||||
return "multilabel-indicator"
|
||||
|
||||
# DeprecationWarning will be replaced by ValueError, see NEP 34
|
||||
# https://numpy.org/neps/nep-0034-infer-dtype-is-object.html
|
||||
# We therefore catch both deprecation (NumPy < 1.24) warning and
|
||||
# value error (NumPy >= 1.24).
|
||||
check_y_kwargs = dict(
|
||||
accept_sparse=True,
|
||||
allow_nd=True,
|
||||
force_all_finite=False,
|
||||
ensure_2d=False,
|
||||
ensure_min_samples=0,
|
||||
ensure_min_features=0,
|
||||
)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error", VisibleDeprecationWarning)
|
||||
if not issparse(y):
|
||||
try:
|
||||
y = check_array(y, dtype=None, **check_y_kwargs)
|
||||
except (VisibleDeprecationWarning, ValueError) as e:
|
||||
if str(e).startswith("Complex data not supported"):
|
||||
raise
|
||||
|
||||
# dtype=object should be provided explicitly for ragged arrays,
|
||||
# see NEP 34
|
||||
y = check_array(y, dtype=object, **check_y_kwargs)
|
||||
|
||||
# The old sequence of sequences format
|
||||
try:
|
||||
first_row = y[[0], :] if issparse(y) else y[0]
|
||||
if (
|
||||
not hasattr(first_row, "__array__")
|
||||
and isinstance(first_row, Sequence)
|
||||
and not isinstance(first_row, str)
|
||||
):
|
||||
raise ValueError(
|
||||
"You appear to be using a legacy multi-label data"
|
||||
" representation. Sequence of sequences are no"
|
||||
" longer supported; use a binary array or sparse"
|
||||
" matrix instead - the MultiLabelBinarizer"
|
||||
" transformer can convert to this format."
|
||||
)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
# Invalid inputs
|
||||
if y.ndim not in (1, 2):
|
||||
# Number of dimension greater than 2: [[[1, 2]]]
|
||||
return "unknown"
|
||||
if not min(y.shape):
|
||||
# Empty ndarray: []/[[]]
|
||||
if y.ndim == 1:
|
||||
# 1-D empty array: []
|
||||
return "binary" # []
|
||||
# 2-D empty array: [[]]
|
||||
return "unknown"
|
||||
if not issparse(y) and y.dtype == object and not isinstance(y.flat[0], str):
|
||||
# [obj_1] and not ["label_1"]
|
||||
return "unknown"
|
||||
|
||||
# Check if multioutput
|
||||
if y.ndim == 2 and y.shape[1] > 1:
|
||||
suffix = "-multioutput" # [[1, 2], [1, 2]]
|
||||
else:
|
||||
suffix = "" # [1, 2, 3] or [[1], [2], [3]]
|
||||
|
||||
# Check float and contains non-integer float values
|
||||
if xp.isdtype(y.dtype, "real floating"):
|
||||
# [.1, .2, 3] or [[.1, .2, 3]] or [[1., .2]] and not [1., 2., 3.]
|
||||
data = y.data if issparse(y) else y
|
||||
if xp.any(data != xp.astype(data, int)):
|
||||
_assert_all_finite(data, input_name=input_name)
|
||||
return "continuous" + suffix
|
||||
|
||||
# Check multiclass
|
||||
if issparse(first_row):
|
||||
first_row = first_row.data
|
||||
if xp.unique_values(y).shape[0] > 2 or (y.ndim == 2 and len(first_row) > 1):
|
||||
# [1, 2, 3] or [[1., 2., 3]] or [[1, 2]]
|
||||
return "multiclass" + suffix
|
||||
else:
|
||||
return "binary" # [1, 2] or [["a"], ["b"]]
|
||||
|
||||
|
||||
def _check_partial_fit_first_call(clf, classes=None):
|
||||
"""Private helper function for factorizing common classes param logic.
|
||||
|
||||
Estimators that implement the ``partial_fit`` API need to be provided with
|
||||
the list of possible classes at the first call to partial_fit.
|
||||
|
||||
Subsequent calls to partial_fit should check that ``classes`` is still
|
||||
consistent with a previous value of ``clf.classes_`` when provided.
|
||||
|
||||
This function returns True if it detects that this was the first call to
|
||||
``partial_fit`` on ``clf``. In that case the ``classes_`` attribute is also
|
||||
set on ``clf``.
|
||||
|
||||
"""
|
||||
if getattr(clf, "classes_", None) is None and classes is None:
|
||||
raise ValueError("classes must be passed on the first call to partial_fit.")
|
||||
|
||||
elif classes is not None:
|
||||
if getattr(clf, "classes_", None) is not None:
|
||||
if not np.array_equal(clf.classes_, unique_labels(classes)):
|
||||
raise ValueError(
|
||||
"`classes=%r` is not the same as on last call "
|
||||
"to partial_fit, was: %r" % (classes, clf.classes_)
|
||||
)
|
||||
|
||||
else:
|
||||
# This is the first call to partial_fit
|
||||
clf.classes_ = unique_labels(classes)
|
||||
return True
|
||||
|
||||
# classes is None and clf.classes_ has already previously been set:
|
||||
# nothing to do
|
||||
return False
|
||||
|
||||
|
||||
def class_distribution(y, sample_weight=None):
|
||||
"""Compute class priors from multioutput-multiclass target data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
y : {array-like, sparse matrix} of size (n_samples, n_outputs)
|
||||
The labels for each example.
|
||||
|
||||
sample_weight : array-like of shape (n_samples,), default=None
|
||||
Sample weights.
|
||||
|
||||
Returns
|
||||
-------
|
||||
classes : list of size n_outputs of ndarray of size (n_classes,)
|
||||
List of classes for each column.
|
||||
|
||||
n_classes : list of int of size n_outputs
|
||||
Number of classes in each column.
|
||||
|
||||
class_prior : list of size n_outputs of ndarray of size (n_classes,)
|
||||
Class distribution of each column.
|
||||
"""
|
||||
classes = []
|
||||
n_classes = []
|
||||
class_prior = []
|
||||
|
||||
n_samples, n_outputs = y.shape
|
||||
if sample_weight is not None:
|
||||
sample_weight = np.asarray(sample_weight)
|
||||
|
||||
if issparse(y):
|
||||
y = y.tocsc()
|
||||
y_nnz = np.diff(y.indptr)
|
||||
|
||||
for k in range(n_outputs):
|
||||
col_nonzero = y.indices[y.indptr[k] : y.indptr[k + 1]]
|
||||
# separate sample weights for zero and non-zero elements
|
||||
if sample_weight is not None:
|
||||
nz_samp_weight = sample_weight[col_nonzero]
|
||||
zeros_samp_weight_sum = np.sum(sample_weight) - np.sum(nz_samp_weight)
|
||||
else:
|
||||
nz_samp_weight = None
|
||||
zeros_samp_weight_sum = y.shape[0] - y_nnz[k]
|
||||
|
||||
classes_k, y_k = np.unique(
|
||||
y.data[y.indptr[k] : y.indptr[k + 1]], return_inverse=True
|
||||
)
|
||||
class_prior_k = np.bincount(y_k, weights=nz_samp_weight)
|
||||
|
||||
# An explicit zero was found, combine its weight with the weight
|
||||
# of the implicit zeros
|
||||
if 0 in classes_k:
|
||||
class_prior_k[classes_k == 0] += zeros_samp_weight_sum
|
||||
|
||||
# If an there is an implicit zero and it is not in classes and
|
||||
# class_prior, make an entry for it
|
||||
if 0 not in classes_k and y_nnz[k] < y.shape[0]:
|
||||
classes_k = np.insert(classes_k, 0, 0)
|
||||
class_prior_k = np.insert(class_prior_k, 0, zeros_samp_weight_sum)
|
||||
|
||||
classes.append(classes_k)
|
||||
n_classes.append(classes_k.shape[0])
|
||||
class_prior.append(class_prior_k / class_prior_k.sum())
|
||||
else:
|
||||
for k in range(n_outputs):
|
||||
classes_k, y_k = np.unique(y[:, k], return_inverse=True)
|
||||
classes.append(classes_k)
|
||||
n_classes.append(classes_k.shape[0])
|
||||
class_prior_k = np.bincount(y_k, weights=sample_weight)
|
||||
class_prior.append(class_prior_k / class_prior_k.sum())
|
||||
|
||||
return (classes, n_classes, class_prior)
|
||||
|
||||
|
||||
def _ovr_decision_function(predictions, confidences, n_classes):
|
||||
"""Compute a continuous, tie-breaking OvR decision function from OvO.
|
||||
|
||||
It is important to include a continuous value, not only votes,
|
||||
to make computing AUC or calibration meaningful.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
predictions : array-like of shape (n_samples, n_classifiers)
|
||||
Predicted classes for each binary classifier.
|
||||
|
||||
confidences : array-like of shape (n_samples, n_classifiers)
|
||||
Decision functions or predicted probabilities for positive class
|
||||
for each binary classifier.
|
||||
|
||||
n_classes : int
|
||||
Number of classes. n_classifiers must be
|
||||
``n_classes * (n_classes - 1 ) / 2``.
|
||||
"""
|
||||
n_samples = predictions.shape[0]
|
||||
votes = np.zeros((n_samples, n_classes))
|
||||
sum_of_confidences = np.zeros((n_samples, n_classes))
|
||||
|
||||
k = 0
|
||||
for i in range(n_classes):
|
||||
for j in range(i + 1, n_classes):
|
||||
sum_of_confidences[:, i] -= confidences[:, k]
|
||||
sum_of_confidences[:, j] += confidences[:, k]
|
||||
votes[predictions[:, k] == 0, i] += 1
|
||||
votes[predictions[:, k] == 1, j] += 1
|
||||
k += 1
|
||||
|
||||
# Monotonically transform the sum_of_confidences to (-1/3, 1/3)
|
||||
# and add it with votes. The monotonic transformation is
|
||||
# f: x -> x / (3 * (|x| + 1)), it uses 1/3 instead of 1/2
|
||||
# to ensure that we won't reach the limits and change vote order.
|
||||
# The motivation is to use confidence levels as a way to break ties in
|
||||
# the votes without switching any decision made based on a difference
|
||||
# of 1 vote.
|
||||
transformed_confidences = sum_of_confidences / (
|
||||
3 * (np.abs(sum_of_confidences) + 1)
|
||||
)
|
||||
return votes + transformed_confidences
|
||||
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
"""Export fast murmurhash C/C++ routines + cython wrappers"""
|
||||
|
||||
cimport numpy as cnp
|
||||
|
||||
# The C API is disabled for now, since it requires -I flags to get
|
||||
# compilation to work even when these functions are not used.
|
||||
# cdef extern from "MurmurHash3.h":
|
||||
# void MurmurHash3_x86_32(void* key, int len, unsigned int seed,
|
||||
# void* out)
|
||||
#
|
||||
# void MurmurHash3_x86_128(void* key, int len, unsigned int seed,
|
||||
# void* out)
|
||||
#
|
||||
# void MurmurHash3_x64_128(void* key, int len, unsigned int seed,
|
||||
# void* out)
|
||||
|
||||
|
||||
cpdef cnp.uint32_t murmurhash3_int_u32(int key, unsigned int seed)
|
||||
cpdef cnp.int32_t murmurhash3_int_s32(int key, unsigned int seed)
|
||||
cpdef cnp.uint32_t murmurhash3_bytes_u32(bytes key, unsigned int seed)
|
||||
cpdef cnp.int32_t murmurhash3_bytes_s32(bytes key, unsigned int seed)
|
||||
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
Our own implementation of the Newton algorithm
|
||||
|
||||
Unlike the scipy.optimize version, this version of the Newton conjugate
|
||||
gradient solver uses only one function call to retrieve the
|
||||
func value, the gradient value and a callable for the Hessian matvec
|
||||
product. If the function call is very expensive (e.g. for logistic
|
||||
regression with large design matrix), this approach gives very
|
||||
significant speedups.
|
||||
"""
|
||||
# This is a modified file from scipy.optimize
|
||||
# Original authors: Travis Oliphant, Eric Jones
|
||||
# Modifications by Gael Varoquaux, Mathieu Blondel and Tom Dupre la Tour
|
||||
# License: BSD
|
||||
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
import scipy
|
||||
|
||||
from ..exceptions import ConvergenceWarning
|
||||
from .fixes import line_search_wolfe1, line_search_wolfe2
|
||||
|
||||
|
||||
class _LineSearchError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _line_search_wolfe12(f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwargs):
|
||||
"""
|
||||
Same as line_search_wolfe1, but fall back to line_search_wolfe2 if
|
||||
suitable step length is not found, and raise an exception if a
|
||||
suitable step length is not found.
|
||||
|
||||
Raises
|
||||
------
|
||||
_LineSearchError
|
||||
If no suitable step size is found.
|
||||
|
||||
"""
|
||||
ret = line_search_wolfe1(f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwargs)
|
||||
|
||||
if ret[0] is None:
|
||||
# Have a look at the line_search method of our NewtonSolver class. We borrow
|
||||
# the logic from there
|
||||
# Deal with relative loss differences around machine precision.
|
||||
args = kwargs.get("args", tuple())
|
||||
fval = f(xk + pk, *args)
|
||||
eps = 16 * np.finfo(np.asarray(old_fval).dtype).eps
|
||||
tiny_loss = np.abs(old_fval * eps)
|
||||
loss_improvement = fval - old_fval
|
||||
check = np.abs(loss_improvement) <= tiny_loss
|
||||
if check:
|
||||
# 2.1 Check sum of absolute gradients as alternative condition.
|
||||
sum_abs_grad_old = scipy.linalg.norm(gfk, ord=1)
|
||||
grad = fprime(xk + pk, *args)
|
||||
sum_abs_grad = scipy.linalg.norm(grad, ord=1)
|
||||
check = sum_abs_grad < sum_abs_grad_old
|
||||
if check:
|
||||
ret = (
|
||||
1.0, # step size
|
||||
ret[1] + 1, # number of function evaluations
|
||||
ret[2] + 1, # number of gradient evaluations
|
||||
fval,
|
||||
old_fval,
|
||||
grad,
|
||||
)
|
||||
|
||||
if ret[0] is None:
|
||||
# line search failed: try different one.
|
||||
# TODO: It seems that the new check for the sum of absolute gradients above
|
||||
# catches all cases that, earlier, ended up here. In fact, our tests never
|
||||
# trigger this "if branch" here and we can consider to remove it.
|
||||
ret = line_search_wolfe2(
|
||||
f, fprime, xk, pk, gfk, old_fval, old_old_fval, **kwargs
|
||||
)
|
||||
|
||||
if ret[0] is None:
|
||||
raise _LineSearchError()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def _cg(fhess_p, fgrad, maxiter, tol):
|
||||
"""
|
||||
Solve iteratively the linear system 'fhess_p . xsupi = fgrad'
|
||||
with a conjugate gradient descent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fhess_p : callable
|
||||
Function that takes the gradient as a parameter and returns the
|
||||
matrix product of the Hessian and gradient.
|
||||
|
||||
fgrad : ndarray of shape (n_features,) or (n_features + 1,)
|
||||
Gradient vector.
|
||||
|
||||
maxiter : int
|
||||
Number of CG iterations.
|
||||
|
||||
tol : float
|
||||
Stopping criterion.
|
||||
|
||||
Returns
|
||||
-------
|
||||
xsupi : ndarray of shape (n_features,) or (n_features + 1,)
|
||||
Estimated solution.
|
||||
"""
|
||||
xsupi = np.zeros(len(fgrad), dtype=fgrad.dtype)
|
||||
ri = np.copy(fgrad)
|
||||
psupi = -ri
|
||||
i = 0
|
||||
dri0 = np.dot(ri, ri)
|
||||
# We also track of |p_i|^2.
|
||||
psupi_norm2 = dri0
|
||||
|
||||
while i <= maxiter:
|
||||
if np.sum(np.abs(ri)) <= tol:
|
||||
break
|
||||
|
||||
Ap = fhess_p(psupi)
|
||||
# check curvature
|
||||
curv = np.dot(psupi, Ap)
|
||||
if 0 <= curv <= 16 * np.finfo(np.float64).eps * psupi_norm2:
|
||||
# See https://arxiv.org/abs/1803.02924, Algo 1 Capped Conjugate Gradient.
|
||||
break
|
||||
elif curv < 0:
|
||||
if i > 0:
|
||||
break
|
||||
else:
|
||||
# fall back to steepest descent direction
|
||||
xsupi += dri0 / curv * psupi
|
||||
break
|
||||
alphai = dri0 / curv
|
||||
xsupi += alphai * psupi
|
||||
ri += alphai * Ap
|
||||
dri1 = np.dot(ri, ri)
|
||||
betai = dri1 / dri0
|
||||
psupi = -ri + betai * psupi
|
||||
# We use |p_i|^2 = |r_i|^2 + beta_i^2 |p_{i-1}|^2
|
||||
psupi_norm2 = dri1 + betai**2 * psupi_norm2
|
||||
i = i + 1
|
||||
dri0 = dri1 # update np.dot(ri,ri) for next time.
|
||||
|
||||
return xsupi
|
||||
|
||||
|
||||
def _newton_cg(
|
||||
grad_hess,
|
||||
func,
|
||||
grad,
|
||||
x0,
|
||||
args=(),
|
||||
tol=1e-4,
|
||||
maxiter=100,
|
||||
maxinner=200,
|
||||
line_search=True,
|
||||
warn=True,
|
||||
):
|
||||
"""
|
||||
Minimization of scalar function of one or more variables using the
|
||||
Newton-CG algorithm.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
grad_hess : callable
|
||||
Should return the gradient and a callable returning the matvec product
|
||||
of the Hessian.
|
||||
|
||||
func : callable
|
||||
Should return the value of the function.
|
||||
|
||||
grad : callable
|
||||
Should return the function value and the gradient. This is used
|
||||
by the linesearch functions.
|
||||
|
||||
x0 : array of float
|
||||
Initial guess.
|
||||
|
||||
args : tuple, default=()
|
||||
Arguments passed to func_grad_hess, func and grad.
|
||||
|
||||
tol : float, default=1e-4
|
||||
Stopping criterion. The iteration will stop when
|
||||
``max{|g_i | i = 1, ..., n} <= tol``
|
||||
where ``g_i`` is the i-th component of the gradient.
|
||||
|
||||
maxiter : int, default=100
|
||||
Number of Newton iterations.
|
||||
|
||||
maxinner : int, default=200
|
||||
Number of CG iterations.
|
||||
|
||||
line_search : bool, default=True
|
||||
Whether to use a line search or not.
|
||||
|
||||
warn : bool, default=True
|
||||
Whether to warn when didn't converge.
|
||||
|
||||
Returns
|
||||
-------
|
||||
xk : ndarray of float
|
||||
Estimated minimum.
|
||||
"""
|
||||
x0 = np.asarray(x0).flatten()
|
||||
xk = np.copy(x0)
|
||||
k = 0
|
||||
|
||||
if line_search:
|
||||
old_fval = func(x0, *args)
|
||||
old_old_fval = None
|
||||
|
||||
# Outer loop: our Newton iteration
|
||||
while k < maxiter:
|
||||
# Compute a search direction pk by applying the CG method to
|
||||
# del2 f(xk) p = - fgrad f(xk) starting from 0.
|
||||
fgrad, fhess_p = grad_hess(xk, *args)
|
||||
|
||||
absgrad = np.abs(fgrad)
|
||||
if np.max(absgrad) <= tol:
|
||||
break
|
||||
|
||||
maggrad = np.sum(absgrad)
|
||||
eta = min([0.5, np.sqrt(maggrad)])
|
||||
termcond = eta * maggrad
|
||||
|
||||
# Inner loop: solve the Newton update by conjugate gradient, to
|
||||
# avoid inverting the Hessian
|
||||
xsupi = _cg(fhess_p, fgrad, maxiter=maxinner, tol=termcond)
|
||||
|
||||
alphak = 1.0
|
||||
|
||||
if line_search:
|
||||
try:
|
||||
alphak, fc, gc, old_fval, old_old_fval, gfkp1 = _line_search_wolfe12(
|
||||
func, grad, xk, xsupi, fgrad, old_fval, old_old_fval, args=args
|
||||
)
|
||||
except _LineSearchError:
|
||||
warnings.warn("Line Search failed")
|
||||
break
|
||||
|
||||
xk += alphak * xsupi # upcast if necessary
|
||||
k += 1
|
||||
|
||||
if warn and k >= maxiter:
|
||||
warnings.warn(
|
||||
"newton-cg failed to converge. Increase the number of iterations.",
|
||||
ConvergenceWarning,
|
||||
)
|
||||
return xk, k
|
||||
|
||||
|
||||
def _check_optimize_result(solver, result, max_iter=None, extra_warning_msg=None):
|
||||
"""Check the OptimizeResult for successful convergence
|
||||
|
||||
Parameters
|
||||
----------
|
||||
solver : str
|
||||
Solver name. Currently only `lbfgs` is supported.
|
||||
|
||||
result : OptimizeResult
|
||||
Result of the scipy.optimize.minimize function.
|
||||
|
||||
max_iter : int, default=None
|
||||
Expected maximum number of iterations.
|
||||
|
||||
extra_warning_msg : str, default=None
|
||||
Extra warning message.
|
||||
|
||||
Returns
|
||||
-------
|
||||
n_iter : int
|
||||
Number of iterations.
|
||||
"""
|
||||
# handle both scipy and scikit-learn solver names
|
||||
if solver == "lbfgs":
|
||||
if result.status != 0:
|
||||
try:
|
||||
# The message is already decoded in scipy>=1.6.0
|
||||
result_message = result.message.decode("latin1")
|
||||
except AttributeError:
|
||||
result_message = result.message
|
||||
warning_msg = (
|
||||
"{} failed to converge (status={}):\n{}.\n\n"
|
||||
"Increase the number of iterations (max_iter) "
|
||||
"or scale the data as shown in:\n"
|
||||
" https://scikit-learn.org/stable/modules/"
|
||||
"preprocessing.html"
|
||||
).format(solver, result.status, result_message)
|
||||
if extra_warning_msg is not None:
|
||||
warning_msg += "\n" + extra_warning_msg
|
||||
warnings.warn(warning_msg, ConvergenceWarning, stacklevel=2)
|
||||
if max_iter is not None:
|
||||
# In scipy <= 1.0.0, nit may exceed maxiter for lbfgs.
|
||||
# See https://github.com/scipy/scipy/issues/7854
|
||||
n_iter_i = min(result.nit, max_iter)
|
||||
else:
|
||||
n_iter_i = result.nit
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return n_iter_i
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
The :mod:`sklearn.utils.parallel` customizes `joblib` tools for scikit-learn usage.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import warnings
|
||||
from functools import update_wrapper
|
||||
|
||||
import joblib
|
||||
|
||||
from .._config import config_context, get_config
|
||||
|
||||
|
||||
def _with_config(delayed_func, config):
|
||||
"""Helper function that intends to attach a config to a delayed function."""
|
||||
if hasattr(delayed_func, "with_config"):
|
||||
return delayed_func.with_config(config)
|
||||
else:
|
||||
warnings.warn(
|
||||
(
|
||||
"`sklearn.utils.parallel.Parallel` needs to be used in "
|
||||
"conjunction with `sklearn.utils.parallel.delayed` instead of "
|
||||
"`joblib.delayed` to correctly propagate the scikit-learn "
|
||||
"configuration to the joblib workers."
|
||||
),
|
||||
UserWarning,
|
||||
)
|
||||
return delayed_func
|
||||
|
||||
|
||||
class Parallel(joblib.Parallel):
|
||||
"""Tweak of :class:`joblib.Parallel` that propagates the scikit-learn configuration.
|
||||
|
||||
This subclass of :class:`joblib.Parallel` ensures that the active configuration
|
||||
(thread-local) of scikit-learn is propagated to the parallel workers for the
|
||||
duration of the execution of the parallel tasks.
|
||||
|
||||
The API does not change and you can refer to :class:`joblib.Parallel`
|
||||
documentation for more details.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
|
||||
def __call__(self, iterable):
|
||||
"""Dispatch the tasks and return the results.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterable : iterable
|
||||
Iterable containing tuples of (delayed_function, args, kwargs) that should
|
||||
be consumed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
results : list
|
||||
List of results of the tasks.
|
||||
"""
|
||||
# Capture the thread-local scikit-learn configuration at the time
|
||||
# Parallel.__call__ is issued since the tasks can be dispatched
|
||||
# in a different thread depending on the backend and on the value of
|
||||
# pre_dispatch and n_jobs.
|
||||
config = get_config()
|
||||
iterable_with_config = (
|
||||
(_with_config(delayed_func, config), args, kwargs)
|
||||
for delayed_func, args, kwargs in iterable
|
||||
)
|
||||
return super().__call__(iterable_with_config)
|
||||
|
||||
|
||||
# remove when https://github.com/joblib/joblib/issues/1071 is fixed
|
||||
def delayed(function):
|
||||
"""Decorator used to capture the arguments of a function.
|
||||
|
||||
This alternative to `joblib.delayed` is meant to be used in conjunction
|
||||
with `sklearn.utils.parallel.Parallel`. The latter captures the scikit-
|
||||
learn configuration by calling `sklearn.get_config()` in the current
|
||||
thread, prior to dispatching the first task. The captured configuration is
|
||||
then propagated and enabled for the duration of the execution of the
|
||||
delayed function in the joblib workers.
|
||||
|
||||
.. versionchanged:: 1.3
|
||||
`delayed` was moved from `sklearn.utils.fixes` to `sklearn.utils.parallel`
|
||||
in scikit-learn 1.3.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
function : callable
|
||||
The function to be delayed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
output: tuple
|
||||
Tuple containing the delayed function, the positional arguments, and the
|
||||
keyword arguments.
|
||||
"""
|
||||
|
||||
@functools.wraps(function)
|
||||
def delayed_function(*args, **kwargs):
|
||||
return _FuncWrapper(function), args, kwargs
|
||||
|
||||
return delayed_function
|
||||
|
||||
|
||||
class _FuncWrapper:
|
||||
"""Load the global configuration before calling the function."""
|
||||
|
||||
def __init__(self, function):
|
||||
self.function = function
|
||||
update_wrapper(self, self.function)
|
||||
|
||||
def with_config(self, config):
|
||||
self.config = config
|
||||
return self
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
config = getattr(self, "config", None)
|
||||
if config is None:
|
||||
warnings.warn(
|
||||
(
|
||||
"`sklearn.utils.parallel.delayed` should be used with"
|
||||
" `sklearn.utils.parallel.Parallel` to make it possible to"
|
||||
" propagate the scikit-learn configuration of the current thread to"
|
||||
" the joblib workers."
|
||||
),
|
||||
UserWarning,
|
||||
)
|
||||
config = {}
|
||||
with config_context(**config):
|
||||
return self.function(*args, **kwargs)
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
The mod:`sklearn.utils.random` module includes utilities for random sampling.
|
||||
"""
|
||||
|
||||
# Author: Hamzeh Alsalhi <ha258@cornell.edu>
|
||||
#
|
||||
# License: BSD 3 clause
|
||||
import array
|
||||
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
|
||||
from . import check_random_state
|
||||
from ._random import sample_without_replacement
|
||||
|
||||
__all__ = ["sample_without_replacement"]
|
||||
|
||||
|
||||
def _random_choice_csc(n_samples, classes, class_probability=None, random_state=None):
|
||||
"""Generate a sparse random matrix given column class distributions
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_samples : int,
|
||||
Number of samples to draw in each column.
|
||||
|
||||
classes : list of size n_outputs of arrays of size (n_classes,)
|
||||
List of classes for each column.
|
||||
|
||||
class_probability : list of size n_outputs of arrays of \
|
||||
shape (n_classes,), default=None
|
||||
Class distribution of each column. If None, uniform distribution is
|
||||
assumed.
|
||||
|
||||
random_state : int, RandomState instance or None, default=None
|
||||
Controls the randomness of the sampled classes.
|
||||
See :term:`Glossary <random_state>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
random_matrix : sparse csc matrix of size (n_samples, n_outputs)
|
||||
|
||||
"""
|
||||
data = array.array("i")
|
||||
indices = array.array("i")
|
||||
indptr = array.array("i", [0])
|
||||
|
||||
for j in range(len(classes)):
|
||||
classes[j] = np.asarray(classes[j])
|
||||
if classes[j].dtype.kind != "i":
|
||||
raise ValueError("class dtype %s is not supported" % classes[j].dtype)
|
||||
classes[j] = classes[j].astype(np.int64, copy=False)
|
||||
|
||||
# use uniform distribution if no class_probability is given
|
||||
if class_probability is None:
|
||||
class_prob_j = np.empty(shape=classes[j].shape[0])
|
||||
class_prob_j.fill(1 / classes[j].shape[0])
|
||||
else:
|
||||
class_prob_j = np.asarray(class_probability[j])
|
||||
|
||||
if not np.isclose(np.sum(class_prob_j), 1.0):
|
||||
raise ValueError(
|
||||
"Probability array at index {0} does not sum to one".format(j)
|
||||
)
|
||||
|
||||
if class_prob_j.shape[0] != classes[j].shape[0]:
|
||||
raise ValueError(
|
||||
"classes[{0}] (length {1}) and "
|
||||
"class_probability[{0}] (length {2}) have "
|
||||
"different length.".format(
|
||||
j, classes[j].shape[0], class_prob_j.shape[0]
|
||||
)
|
||||
)
|
||||
|
||||
# If 0 is not present in the classes insert it with a probability 0.0
|
||||
if 0 not in classes[j]:
|
||||
classes[j] = np.insert(classes[j], 0, 0)
|
||||
class_prob_j = np.insert(class_prob_j, 0, 0.0)
|
||||
|
||||
# If there are nonzero classes choose randomly using class_probability
|
||||
rng = check_random_state(random_state)
|
||||
if classes[j].shape[0] > 1:
|
||||
index_class_0 = np.flatnonzero(classes[j] == 0).item()
|
||||
p_nonzero = 1 - class_prob_j[index_class_0]
|
||||
nnz = int(n_samples * p_nonzero)
|
||||
ind_sample = sample_without_replacement(
|
||||
n_population=n_samples, n_samples=nnz, random_state=random_state
|
||||
)
|
||||
indices.extend(ind_sample)
|
||||
|
||||
# Normalize probabilities for the nonzero elements
|
||||
classes_j_nonzero = classes[j] != 0
|
||||
class_probability_nz = class_prob_j[classes_j_nonzero]
|
||||
class_probability_nz_norm = class_probability_nz / np.sum(
|
||||
class_probability_nz
|
||||
)
|
||||
classes_ind = np.searchsorted(
|
||||
class_probability_nz_norm.cumsum(), rng.uniform(size=nnz)
|
||||
)
|
||||
data.extend(classes[j][classes_j_nonzero][classes_ind])
|
||||
indptr.append(len(indices))
|
||||
|
||||
return sp.csc_matrix((data, indices, indptr), (n_samples, len(classes)), dtype=int)
|
||||
@@ -0,0 +1,745 @@
|
||||
"""
|
||||
The :mod:`sklearn.utils.sparsefuncs` module includes a collection of utilities to
|
||||
work with sparse matrices and arrays.
|
||||
"""
|
||||
|
||||
# Authors: Manoj Kumar
|
||||
# Thomas Unterthiner
|
||||
# Giorgio Patrini
|
||||
#
|
||||
# License: BSD 3 clause
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
from scipy.sparse.linalg import LinearOperator
|
||||
|
||||
from ..utils.fixes import _sparse_min_max, _sparse_nan_min_max
|
||||
from ..utils.validation import _check_sample_weight
|
||||
from .sparsefuncs_fast import (
|
||||
csc_mean_variance_axis0 as _csc_mean_var_axis0,
|
||||
)
|
||||
from .sparsefuncs_fast import (
|
||||
csr_mean_variance_axis0 as _csr_mean_var_axis0,
|
||||
)
|
||||
from .sparsefuncs_fast import (
|
||||
incr_mean_variance_axis0 as _incr_mean_var_axis0,
|
||||
)
|
||||
|
||||
|
||||
def _raise_typeerror(X):
|
||||
"""Raises a TypeError if X is not a CSR or CSC matrix"""
|
||||
input_type = X.format if sp.issparse(X) else type(X)
|
||||
err = "Expected a CSR or CSC sparse matrix, got %s." % input_type
|
||||
raise TypeError(err)
|
||||
|
||||
|
||||
def _raise_error_wrong_axis(axis):
|
||||
if axis not in (0, 1):
|
||||
raise ValueError(
|
||||
"Unknown axis value: %d. Use 0 for rows, or 1 for columns" % axis
|
||||
)
|
||||
|
||||
|
||||
def inplace_csr_column_scale(X, scale):
|
||||
"""Inplace column scaling of a CSR matrix.
|
||||
|
||||
Scale each feature of the data matrix by multiplying with specific scale
|
||||
provided by the caller assuming a (n_samples, n_features) shape.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Matrix to normalize using the variance of the features.
|
||||
It should be of CSR format.
|
||||
|
||||
scale : ndarray of shape (n_features,), dtype={np.float32, np.float64}
|
||||
Array of precomputed feature-wise values to use for scaling.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import sparsefuncs
|
||||
>>> from scipy import sparse
|
||||
>>> import numpy as np
|
||||
>>> indptr = np.array([0, 3, 4, 4, 4])
|
||||
>>> indices = np.array([0, 1, 2, 2])
|
||||
>>> data = np.array([8, 1, 2, 5])
|
||||
>>> scale = np.array([2, 3, 2])
|
||||
>>> csr = sparse.csr_matrix((data, indices, indptr))
|
||||
>>> csr.todense()
|
||||
matrix([[8, 1, 2],
|
||||
[0, 0, 5],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
>>> sparsefuncs.inplace_csr_column_scale(csr, scale)
|
||||
>>> csr.todense()
|
||||
matrix([[16, 3, 4],
|
||||
[ 0, 0, 10],
|
||||
[ 0, 0, 0],
|
||||
[ 0, 0, 0]])
|
||||
"""
|
||||
assert scale.shape[0] == X.shape[1]
|
||||
X.data *= scale.take(X.indices, mode="clip")
|
||||
|
||||
|
||||
def inplace_csr_row_scale(X, scale):
|
||||
"""Inplace row scaling of a CSR matrix.
|
||||
|
||||
Scale each sample of the data matrix by multiplying with specific scale
|
||||
provided by the caller assuming a (n_samples, n_features) shape.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Matrix to be scaled. It should be of CSR format.
|
||||
|
||||
scale : ndarray of float of shape (n_samples,)
|
||||
Array of precomputed sample-wise values to use for scaling.
|
||||
"""
|
||||
assert scale.shape[0] == X.shape[0]
|
||||
X.data *= np.repeat(scale, np.diff(X.indptr))
|
||||
|
||||
|
||||
def mean_variance_axis(X, axis, weights=None, return_sum_weights=False):
|
||||
"""Compute mean and variance along an axis on a CSR or CSC matrix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Input data. It can be of CSR or CSC format.
|
||||
|
||||
axis : {0, 1}
|
||||
Axis along which the axis should be computed.
|
||||
|
||||
weights : ndarray of shape (n_samples,) or (n_features,), default=None
|
||||
If axis is set to 0 shape is (n_samples,) or
|
||||
if axis is set to 1 shape is (n_features,).
|
||||
If it is set to None, then samples are equally weighted.
|
||||
|
||||
.. versionadded:: 0.24
|
||||
|
||||
return_sum_weights : bool, default=False
|
||||
If True, returns the sum of weights seen for each feature
|
||||
if `axis=0` or each sample if `axis=1`.
|
||||
|
||||
.. versionadded:: 0.24
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
means : ndarray of shape (n_features,), dtype=floating
|
||||
Feature-wise means.
|
||||
|
||||
variances : ndarray of shape (n_features,), dtype=floating
|
||||
Feature-wise variances.
|
||||
|
||||
sum_weights : ndarray of shape (n_features,), dtype=floating
|
||||
Returned if `return_sum_weights` is `True`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import sparsefuncs
|
||||
>>> from scipy import sparse
|
||||
>>> import numpy as np
|
||||
>>> indptr = np.array([0, 3, 4, 4, 4])
|
||||
>>> indices = np.array([0, 1, 2, 2])
|
||||
>>> data = np.array([8, 1, 2, 5])
|
||||
>>> scale = np.array([2, 3, 2])
|
||||
>>> csr = sparse.csr_matrix((data, indices, indptr))
|
||||
>>> csr.todense()
|
||||
matrix([[8, 1, 2],
|
||||
[0, 0, 5],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
>>> sparsefuncs.mean_variance_axis(csr, axis=0)
|
||||
(array([2. , 0.25, 1.75]), array([12. , 0.1875, 4.1875]))
|
||||
"""
|
||||
_raise_error_wrong_axis(axis)
|
||||
|
||||
if sp.issparse(X) and X.format == "csr":
|
||||
if axis == 0:
|
||||
return _csr_mean_var_axis0(
|
||||
X, weights=weights, return_sum_weights=return_sum_weights
|
||||
)
|
||||
else:
|
||||
return _csc_mean_var_axis0(
|
||||
X.T, weights=weights, return_sum_weights=return_sum_weights
|
||||
)
|
||||
elif sp.issparse(X) and X.format == "csc":
|
||||
if axis == 0:
|
||||
return _csc_mean_var_axis0(
|
||||
X, weights=weights, return_sum_weights=return_sum_weights
|
||||
)
|
||||
else:
|
||||
return _csr_mean_var_axis0(
|
||||
X.T, weights=weights, return_sum_weights=return_sum_weights
|
||||
)
|
||||
else:
|
||||
_raise_typeerror(X)
|
||||
|
||||
|
||||
def incr_mean_variance_axis(X, *, axis, last_mean, last_var, last_n, weights=None):
|
||||
"""Compute incremental mean and variance along an axis on a CSR or CSC matrix.
|
||||
|
||||
last_mean, last_var are the statistics computed at the last step by this
|
||||
function. Both must be initialized to 0-arrays of the proper size, i.e.
|
||||
the number of features in X. last_n is the number of samples encountered
|
||||
until now.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : CSR or CSC sparse matrix of shape (n_samples, n_features)
|
||||
Input data.
|
||||
|
||||
axis : {0, 1}
|
||||
Axis along which the axis should be computed.
|
||||
|
||||
last_mean : ndarray of shape (n_features,) or (n_samples,), dtype=floating
|
||||
Array of means to update with the new data X.
|
||||
Should be of shape (n_features,) if axis=0 or (n_samples,) if axis=1.
|
||||
|
||||
last_var : ndarray of shape (n_features,) or (n_samples,), dtype=floating
|
||||
Array of variances to update with the new data X.
|
||||
Should be of shape (n_features,) if axis=0 or (n_samples,) if axis=1.
|
||||
|
||||
last_n : float or ndarray of shape (n_features,) or (n_samples,), \
|
||||
dtype=floating
|
||||
Sum of the weights seen so far, excluding the current weights
|
||||
If not float, it should be of shape (n_features,) if
|
||||
axis=0 or (n_samples,) if axis=1. If float it corresponds to
|
||||
having same weights for all samples (or features).
|
||||
|
||||
weights : ndarray of shape (n_samples,) or (n_features,), default=None
|
||||
If axis is set to 0 shape is (n_samples,) or
|
||||
if axis is set to 1 shape is (n_features,).
|
||||
If it is set to None, then samples are equally weighted.
|
||||
|
||||
.. versionadded:: 0.24
|
||||
|
||||
Returns
|
||||
-------
|
||||
means : ndarray of shape (n_features,) or (n_samples,), dtype=floating
|
||||
Updated feature-wise means if axis = 0 or
|
||||
sample-wise means if axis = 1.
|
||||
|
||||
variances : ndarray of shape (n_features,) or (n_samples,), dtype=floating
|
||||
Updated feature-wise variances if axis = 0 or
|
||||
sample-wise variances if axis = 1.
|
||||
|
||||
n : ndarray of shape (n_features,) or (n_samples,), dtype=integral
|
||||
Updated number of seen samples per feature if axis=0
|
||||
or number of seen features per sample if axis=1.
|
||||
|
||||
If weights is not None, n is a sum of the weights of the seen
|
||||
samples or features instead of the actual number of seen
|
||||
samples or features.
|
||||
|
||||
Notes
|
||||
-----
|
||||
NaNs are ignored in the algorithm.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import sparsefuncs
|
||||
>>> from scipy import sparse
|
||||
>>> import numpy as np
|
||||
>>> indptr = np.array([0, 3, 4, 4, 4])
|
||||
>>> indices = np.array([0, 1, 2, 2])
|
||||
>>> data = np.array([8, 1, 2, 5])
|
||||
>>> scale = np.array([2, 3, 2])
|
||||
>>> csr = sparse.csr_matrix((data, indices, indptr))
|
||||
>>> csr.todense()
|
||||
matrix([[8, 1, 2],
|
||||
[0, 0, 5],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
>>> sparsefuncs.incr_mean_variance_axis(
|
||||
... csr, axis=0, last_mean=np.zeros(3), last_var=np.zeros(3), last_n=2
|
||||
... )
|
||||
(array([1.3..., 0.1..., 1.1...]), array([8.8..., 0.1..., 3.4...]),
|
||||
array([6., 6., 6.]))
|
||||
"""
|
||||
_raise_error_wrong_axis(axis)
|
||||
|
||||
if not (sp.issparse(X) and X.format in ("csc", "csr")):
|
||||
_raise_typeerror(X)
|
||||
|
||||
if np.size(last_n) == 1:
|
||||
last_n = np.full(last_mean.shape, last_n, dtype=last_mean.dtype)
|
||||
|
||||
if not (np.size(last_mean) == np.size(last_var) == np.size(last_n)):
|
||||
raise ValueError("last_mean, last_var, last_n do not have the same shapes.")
|
||||
|
||||
if axis == 1:
|
||||
if np.size(last_mean) != X.shape[0]:
|
||||
raise ValueError(
|
||||
"If axis=1, then last_mean, last_n, last_var should be of "
|
||||
f"size n_samples {X.shape[0]} (Got {np.size(last_mean)})."
|
||||
)
|
||||
else: # axis == 0
|
||||
if np.size(last_mean) != X.shape[1]:
|
||||
raise ValueError(
|
||||
"If axis=0, then last_mean, last_n, last_var should be of "
|
||||
f"size n_features {X.shape[1]} (Got {np.size(last_mean)})."
|
||||
)
|
||||
|
||||
X = X.T if axis == 1 else X
|
||||
|
||||
if weights is not None:
|
||||
weights = _check_sample_weight(weights, X, dtype=X.dtype)
|
||||
|
||||
return _incr_mean_var_axis0(
|
||||
X, last_mean=last_mean, last_var=last_var, last_n=last_n, weights=weights
|
||||
)
|
||||
|
||||
|
||||
def inplace_column_scale(X, scale):
|
||||
"""Inplace column scaling of a CSC/CSR matrix.
|
||||
|
||||
Scale each feature of the data matrix by multiplying with specific scale
|
||||
provided by the caller assuming a (n_samples, n_features) shape.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Matrix to normalize using the variance of the features. It should be
|
||||
of CSC or CSR format.
|
||||
|
||||
scale : ndarray of shape (n_features,), dtype={np.float32, np.float64}
|
||||
Array of precomputed feature-wise values to use for scaling.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import sparsefuncs
|
||||
>>> from scipy import sparse
|
||||
>>> import numpy as np
|
||||
>>> indptr = np.array([0, 3, 4, 4, 4])
|
||||
>>> indices = np.array([0, 1, 2, 2])
|
||||
>>> data = np.array([8, 1, 2, 5])
|
||||
>>> scale = np.array([2, 3, 2])
|
||||
>>> csr = sparse.csr_matrix((data, indices, indptr))
|
||||
>>> csr.todense()
|
||||
matrix([[8, 1, 2],
|
||||
[0, 0, 5],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
>>> sparsefuncs.inplace_column_scale(csr, scale)
|
||||
>>> csr.todense()
|
||||
matrix([[16, 3, 4],
|
||||
[ 0, 0, 10],
|
||||
[ 0, 0, 0],
|
||||
[ 0, 0, 0]])
|
||||
"""
|
||||
if sp.issparse(X) and X.format == "csc":
|
||||
inplace_csr_row_scale(X.T, scale)
|
||||
elif sp.issparse(X) and X.format == "csr":
|
||||
inplace_csr_column_scale(X, scale)
|
||||
else:
|
||||
_raise_typeerror(X)
|
||||
|
||||
|
||||
def inplace_row_scale(X, scale):
|
||||
"""Inplace row scaling of a CSR or CSC matrix.
|
||||
|
||||
Scale each row of the data matrix by multiplying with specific scale
|
||||
provided by the caller assuming a (n_samples, n_features) shape.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Matrix to be scaled. It should be of CSR or CSC format.
|
||||
|
||||
scale : ndarray of shape (n_features,), dtype={np.float32, np.float64}
|
||||
Array of precomputed sample-wise values to use for scaling.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import sparsefuncs
|
||||
>>> from scipy import sparse
|
||||
>>> import numpy as np
|
||||
>>> indptr = np.array([0, 2, 3, 4, 5])
|
||||
>>> indices = np.array([0, 1, 2, 3, 3])
|
||||
>>> data = np.array([8, 1, 2, 5, 6])
|
||||
>>> scale = np.array([2, 3, 4, 5])
|
||||
>>> csr = sparse.csr_matrix((data, indices, indptr))
|
||||
>>> csr.todense()
|
||||
matrix([[8, 1, 0, 0],
|
||||
[0, 0, 2, 0],
|
||||
[0, 0, 0, 5],
|
||||
[0, 0, 0, 6]])
|
||||
>>> sparsefuncs.inplace_row_scale(csr, scale)
|
||||
>>> csr.todense()
|
||||
matrix([[16, 2, 0, 0],
|
||||
[ 0, 0, 6, 0],
|
||||
[ 0, 0, 0, 20],
|
||||
[ 0, 0, 0, 30]])
|
||||
"""
|
||||
if sp.issparse(X) and X.format == "csc":
|
||||
inplace_csr_column_scale(X.T, scale)
|
||||
elif sp.issparse(X) and X.format == "csr":
|
||||
inplace_csr_row_scale(X, scale)
|
||||
else:
|
||||
_raise_typeerror(X)
|
||||
|
||||
|
||||
def inplace_swap_row_csc(X, m, n):
|
||||
"""Swap two rows of a CSC matrix in-place.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Matrix whose two rows are to be swapped. It should be of
|
||||
CSC format.
|
||||
|
||||
m : int
|
||||
Index of the row of X to be swapped.
|
||||
|
||||
n : int
|
||||
Index of the row of X to be swapped.
|
||||
"""
|
||||
for t in [m, n]:
|
||||
if isinstance(t, np.ndarray):
|
||||
raise TypeError("m and n should be valid integers")
|
||||
|
||||
if m < 0:
|
||||
m += X.shape[0]
|
||||
if n < 0:
|
||||
n += X.shape[0]
|
||||
|
||||
m_mask = X.indices == m
|
||||
X.indices[X.indices == n] = m
|
||||
X.indices[m_mask] = n
|
||||
|
||||
|
||||
def inplace_swap_row_csr(X, m, n):
|
||||
"""Swap two rows of a CSR matrix in-place.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Matrix whose two rows are to be swapped. It should be of
|
||||
CSR format.
|
||||
|
||||
m : int
|
||||
Index of the row of X to be swapped.
|
||||
|
||||
n : int
|
||||
Index of the row of X to be swapped.
|
||||
"""
|
||||
for t in [m, n]:
|
||||
if isinstance(t, np.ndarray):
|
||||
raise TypeError("m and n should be valid integers")
|
||||
|
||||
if m < 0:
|
||||
m += X.shape[0]
|
||||
if n < 0:
|
||||
n += X.shape[0]
|
||||
|
||||
# The following swapping makes life easier since m is assumed to be the
|
||||
# smaller integer below.
|
||||
if m > n:
|
||||
m, n = n, m
|
||||
|
||||
indptr = X.indptr
|
||||
m_start = indptr[m]
|
||||
m_stop = indptr[m + 1]
|
||||
n_start = indptr[n]
|
||||
n_stop = indptr[n + 1]
|
||||
nz_m = m_stop - m_start
|
||||
nz_n = n_stop - n_start
|
||||
|
||||
if nz_m != nz_n:
|
||||
# Modify indptr first
|
||||
X.indptr[m + 2 : n] += nz_n - nz_m
|
||||
X.indptr[m + 1] = m_start + nz_n
|
||||
X.indptr[n] = n_stop - nz_m
|
||||
|
||||
X.indices = np.concatenate(
|
||||
[
|
||||
X.indices[:m_start],
|
||||
X.indices[n_start:n_stop],
|
||||
X.indices[m_stop:n_start],
|
||||
X.indices[m_start:m_stop],
|
||||
X.indices[n_stop:],
|
||||
]
|
||||
)
|
||||
X.data = np.concatenate(
|
||||
[
|
||||
X.data[:m_start],
|
||||
X.data[n_start:n_stop],
|
||||
X.data[m_stop:n_start],
|
||||
X.data[m_start:m_stop],
|
||||
X.data[n_stop:],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def inplace_swap_row(X, m, n):
|
||||
"""
|
||||
Swap two rows of a CSC/CSR matrix in-place.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Matrix whose two rows are to be swapped. It should be of CSR or
|
||||
CSC format.
|
||||
|
||||
m : int
|
||||
Index of the row of X to be swapped.
|
||||
|
||||
n : int
|
||||
Index of the row of X to be swapped.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import sparsefuncs
|
||||
>>> from scipy import sparse
|
||||
>>> import numpy as np
|
||||
>>> indptr = np.array([0, 2, 3, 3, 3])
|
||||
>>> indices = np.array([0, 2, 2])
|
||||
>>> data = np.array([8, 2, 5])
|
||||
>>> csr = sparse.csr_matrix((data, indices, indptr))
|
||||
>>> csr.todense()
|
||||
matrix([[8, 0, 2],
|
||||
[0, 0, 5],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
>>> sparsefuncs.inplace_swap_row(csr, 0, 1)
|
||||
>>> csr.todense()
|
||||
matrix([[0, 0, 5],
|
||||
[8, 0, 2],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
"""
|
||||
if sp.issparse(X) and X.format == "csc":
|
||||
inplace_swap_row_csc(X, m, n)
|
||||
elif sp.issparse(X) and X.format == "csr":
|
||||
inplace_swap_row_csr(X, m, n)
|
||||
else:
|
||||
_raise_typeerror(X)
|
||||
|
||||
|
||||
def inplace_swap_column(X, m, n):
|
||||
"""
|
||||
Swap two columns of a CSC/CSR matrix in-place.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Matrix whose two columns are to be swapped. It should be of
|
||||
CSR or CSC format.
|
||||
|
||||
m : int
|
||||
Index of the column of X to be swapped.
|
||||
|
||||
n : int
|
||||
Index of the column of X to be swapped.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from sklearn.utils import sparsefuncs
|
||||
>>> from scipy import sparse
|
||||
>>> import numpy as np
|
||||
>>> indptr = np.array([0, 2, 3, 3, 3])
|
||||
>>> indices = np.array([0, 2, 2])
|
||||
>>> data = np.array([8, 2, 5])
|
||||
>>> csr = sparse.csr_matrix((data, indices, indptr))
|
||||
>>> csr.todense()
|
||||
matrix([[8, 0, 2],
|
||||
[0, 0, 5],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
>>> sparsefuncs.inplace_swap_column(csr, 0, 1)
|
||||
>>> csr.todense()
|
||||
matrix([[0, 8, 2],
|
||||
[0, 0, 5],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
"""
|
||||
if m < 0:
|
||||
m += X.shape[1]
|
||||
if n < 0:
|
||||
n += X.shape[1]
|
||||
if sp.issparse(X) and X.format == "csc":
|
||||
inplace_swap_row_csr(X, m, n)
|
||||
elif sp.issparse(X) and X.format == "csr":
|
||||
inplace_swap_row_csc(X, m, n)
|
||||
else:
|
||||
_raise_typeerror(X)
|
||||
|
||||
|
||||
def min_max_axis(X, axis, ignore_nan=False):
|
||||
"""Compute minimum and maximum along an axis on a CSR or CSC matrix.
|
||||
|
||||
Optionally ignore NaN values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Input data. It should be of CSR or CSC format.
|
||||
|
||||
axis : {0, 1}
|
||||
Axis along which the axis should be computed.
|
||||
|
||||
ignore_nan : bool, default=False
|
||||
Ignore or passing through NaN values.
|
||||
|
||||
.. versionadded:: 0.20
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
mins : ndarray of shape (n_features,), dtype={np.float32, np.float64}
|
||||
Feature-wise minima.
|
||||
|
||||
maxs : ndarray of shape (n_features,), dtype={np.float32, np.float64}
|
||||
Feature-wise maxima.
|
||||
"""
|
||||
if sp.issparse(X) and X.format in ("csr", "csc"):
|
||||
if ignore_nan:
|
||||
return _sparse_nan_min_max(X, axis=axis)
|
||||
else:
|
||||
return _sparse_min_max(X, axis=axis)
|
||||
else:
|
||||
_raise_typeerror(X)
|
||||
|
||||
|
||||
def count_nonzero(X, axis=None, sample_weight=None):
|
||||
"""A variant of X.getnnz() with extension to weighting on axis 0.
|
||||
|
||||
Useful in efficiently calculating multilabel metrics.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_labels)
|
||||
Input data. It should be of CSR format.
|
||||
|
||||
axis : {0, 1}, default=None
|
||||
The axis on which the data is aggregated.
|
||||
|
||||
sample_weight : array-like of shape (n_samples,), default=None
|
||||
Weight for each row of X.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nnz : int, float, ndarray of shape (n_samples,) or ndarray of shape (n_features,)
|
||||
Number of non-zero values in the array along a given axis. Otherwise,
|
||||
the total number of non-zero values in the array is returned.
|
||||
"""
|
||||
if axis == -1:
|
||||
axis = 1
|
||||
elif axis == -2:
|
||||
axis = 0
|
||||
elif X.format != "csr":
|
||||
raise TypeError("Expected CSR sparse format, got {0}".format(X.format))
|
||||
|
||||
# We rely here on the fact that np.diff(Y.indptr) for a CSR
|
||||
# will return the number of nonzero entries in each row.
|
||||
# A bincount over Y.indices will return the number of nonzeros
|
||||
# in each column. See ``csr_matrix.getnnz`` in scipy >= 0.14.
|
||||
if axis is None:
|
||||
if sample_weight is None:
|
||||
return X.nnz
|
||||
else:
|
||||
return np.dot(np.diff(X.indptr), sample_weight)
|
||||
elif axis == 1:
|
||||
out = np.diff(X.indptr)
|
||||
if sample_weight is None:
|
||||
# astype here is for consistency with axis=0 dtype
|
||||
return out.astype("intp")
|
||||
return out * sample_weight
|
||||
elif axis == 0:
|
||||
if sample_weight is None:
|
||||
return np.bincount(X.indices, minlength=X.shape[1])
|
||||
else:
|
||||
weights = np.repeat(sample_weight, np.diff(X.indptr))
|
||||
return np.bincount(X.indices, minlength=X.shape[1], weights=weights)
|
||||
else:
|
||||
raise ValueError("Unsupported axis: {0}".format(axis))
|
||||
|
||||
|
||||
def _get_median(data, n_zeros):
|
||||
"""Compute the median of data with n_zeros additional zeros.
|
||||
|
||||
This function is used to support sparse matrices; it modifies data
|
||||
in-place.
|
||||
"""
|
||||
n_elems = len(data) + n_zeros
|
||||
if not n_elems:
|
||||
return np.nan
|
||||
n_negative = np.count_nonzero(data < 0)
|
||||
middle, is_odd = divmod(n_elems, 2)
|
||||
data.sort()
|
||||
|
||||
if is_odd:
|
||||
return _get_elem_at_rank(middle, data, n_negative, n_zeros)
|
||||
|
||||
return (
|
||||
_get_elem_at_rank(middle - 1, data, n_negative, n_zeros)
|
||||
+ _get_elem_at_rank(middle, data, n_negative, n_zeros)
|
||||
) / 2.0
|
||||
|
||||
|
||||
def _get_elem_at_rank(rank, data, n_negative, n_zeros):
|
||||
"""Find the value in data augmented with n_zeros for the given rank"""
|
||||
if rank < n_negative:
|
||||
return data[rank]
|
||||
if rank - n_negative < n_zeros:
|
||||
return 0
|
||||
return data[rank - n_zeros]
|
||||
|
||||
|
||||
def csc_median_axis_0(X):
|
||||
"""Find the median across axis 0 of a CSC matrix.
|
||||
|
||||
It is equivalent to doing np.median(X, axis=0).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
Input data. It should be of CSC format.
|
||||
|
||||
Returns
|
||||
-------
|
||||
median : ndarray of shape (n_features,)
|
||||
Median.
|
||||
"""
|
||||
if not (sp.issparse(X) and X.format == "csc"):
|
||||
raise TypeError("Expected matrix of CSC format, got %s" % X.format)
|
||||
|
||||
indptr = X.indptr
|
||||
n_samples, n_features = X.shape
|
||||
median = np.zeros(n_features)
|
||||
|
||||
for f_ind, (start, end) in enumerate(zip(indptr[:-1], indptr[1:])):
|
||||
# Prevent modifying X in place
|
||||
data = np.copy(X.data[start:end])
|
||||
nz = n_samples - data.size
|
||||
median[f_ind] = _get_median(data, nz)
|
||||
|
||||
return median
|
||||
|
||||
|
||||
def _implicit_column_offset(X, offset):
|
||||
"""Create an implicitly offset linear operator.
|
||||
|
||||
This is used by PCA on sparse data to avoid densifying the whole data
|
||||
matrix.
|
||||
|
||||
Params
|
||||
------
|
||||
X : sparse matrix of shape (n_samples, n_features)
|
||||
offset : ndarray of shape (n_features,)
|
||||
|
||||
Returns
|
||||
-------
|
||||
centered : LinearOperator
|
||||
"""
|
||||
offset = offset[None, :]
|
||||
XT = X.T
|
||||
return LinearOperator(
|
||||
matvec=lambda x: X @ x - offset @ x,
|
||||
matmat=lambda x: X @ x - offset @ x,
|
||||
rmatvec=lambda x: XT @ x - (offset * x.sum()),
|
||||
rmatmat=lambda x: XT @ x - offset.T @ x.sum(axis=0)[None, :],
|
||||
dtype=X.dtype,
|
||||
shape=X.shape,
|
||||
)
|
||||
Binary file not shown.
@@ -0,0 +1,69 @@
|
||||
import numpy as np
|
||||
|
||||
from .extmath import stable_cumsum
|
||||
|
||||
|
||||
def _weighted_percentile(array, sample_weight, percentile=50):
|
||||
"""Compute weighted percentile
|
||||
|
||||
Computes lower weighted percentile. If `array` is a 2D array, the
|
||||
`percentile` is computed along the axis 0.
|
||||
|
||||
.. versionchanged:: 0.24
|
||||
Accepts 2D `array`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
array : 1D or 2D array
|
||||
Values to take the weighted percentile of.
|
||||
|
||||
sample_weight: 1D or 2D array
|
||||
Weights for each value in `array`. Must be same shape as `array` or
|
||||
of shape `(array.shape[0],)`.
|
||||
|
||||
percentile: int or float, default=50
|
||||
Percentile to compute. Must be value between 0 and 100.
|
||||
|
||||
Returns
|
||||
-------
|
||||
percentile : int if `array` 1D, ndarray if `array` 2D
|
||||
Weighted percentile.
|
||||
"""
|
||||
n_dim = array.ndim
|
||||
if n_dim == 0:
|
||||
return array[()]
|
||||
if array.ndim == 1:
|
||||
array = array.reshape((-1, 1))
|
||||
# When sample_weight 1D, repeat for each array.shape[1]
|
||||
if array.shape != sample_weight.shape and array.shape[0] == sample_weight.shape[0]:
|
||||
sample_weight = np.tile(sample_weight, (array.shape[1], 1)).T
|
||||
sorted_idx = np.argsort(array, axis=0)
|
||||
sorted_weights = np.take_along_axis(sample_weight, sorted_idx, axis=0)
|
||||
|
||||
# Find index of median prediction for each sample
|
||||
weight_cdf = stable_cumsum(sorted_weights, axis=0)
|
||||
adjusted_percentile = percentile / 100 * weight_cdf[-1]
|
||||
|
||||
# For percentile=0, ignore leading observations with sample_weight=0. GH20528
|
||||
mask = adjusted_percentile == 0
|
||||
adjusted_percentile[mask] = np.nextafter(
|
||||
adjusted_percentile[mask], adjusted_percentile[mask] + 1
|
||||
)
|
||||
|
||||
percentile_idx = np.array(
|
||||
[
|
||||
np.searchsorted(weight_cdf[:, i], adjusted_percentile[i])
|
||||
for i in range(weight_cdf.shape[1])
|
||||
]
|
||||
)
|
||||
percentile_idx = np.array(percentile_idx)
|
||||
# In rare cases, percentile_idx equals to sorted_idx.shape[0]
|
||||
max_idx = sorted_idx.shape[0] - 1
|
||||
percentile_idx = np.apply_along_axis(
|
||||
lambda x: np.clip(x, 0, max_idx), axis=0, arr=percentile_idx
|
||||
)
|
||||
|
||||
col_index = np.arange(array.shape[1])
|
||||
percentile_in_sorted = sorted_idx[percentile_idx, col_index]
|
||||
percentile = array[percentile_in_sorted, col_index]
|
||||
return percentile[0] if n_dim == 1 else percentile
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user