Thanks to visit codestin.com
Credit goes to github.com

Skip to content

ENH Add "ensure_non_negative" option to check_array #29540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions doc/whats_new/v1.6.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,9 @@ Changelog
.......................

- |Efficiency| Small runtime improvement of fitting
:class:`ensemble.HistGradientBoostingClassifier` and :class:`ensemble.HistGradientBoostingRegressor`
by parallelizing the initial search for bin thresholds
:class:`ensemble.HistGradientBoostingClassifier` and
:class:`ensemble.HistGradientBoostingRegressor` by parallelizing the initial search
for bin thresholds.
:pr:`28064` by :user:`Christian Lorentzen <lorentzenchr>`.

- |Enhancement| The verbosity of :class:`ensemble.HistGradientBoostingClassifier`
Expand All @@ -193,9 +194,10 @@ Changelog
:pr:`28622` by :user:`Adam Li <adam2392>` and
:user:`Sérgio Pereira <sergiormpereira>`.

- |Feature| :class:`ensemble.ExtraTreesClassifier` and :class:`ensemble.ExtraTreesRegressor` now support
missing-values in the data matrix `X`. Missing-values are handled by randomly moving all of
the samples to the left, or right child node as the tree is traversed.
- |Feature| :class:`ensemble.ExtraTreesClassifier` and
:class:`ensemble.ExtraTreesRegressor` now support missing-values in the data matrix
`X`. Missing-values are handled by randomly moving all of the samples to the left, or
right child node as the tree is traversed.
:pr:`28268` by :user:`Adam Li <adam2392>`.

:mod:`sklearn.impute`
Expand Down Expand Up @@ -249,7 +251,8 @@ Changelog
estimator without re-fitting it.
:pr:`29067` by :user:`Guillaume Lemaitre <glemaitre>`.

- |Fix| Improve error message when :func:`model_selection.RepeatedStratifiedKFold.split` is called without a `y` argument
- |Fix| Improve error message when :func:`model_selection.RepeatedStratifiedKFold.split`
is called without a `y` argument
:pr:`29402` by :user:`Anurag Varma <Anurag-Varma>`.

:mod:`sklearn.neighbors`
Expand Down Expand Up @@ -285,6 +288,11 @@ Changelog
:mod:`sklearn.utils`
....................

- |Enhancement| :func:`utils.validation.check_array` now accepts `ensure_non_negative`
to check for negative values in the passed array, until now only available through
calling :func:`utils.validation.check_non_negative`.
:pr:`29540` by :user:`Tamara Atanasoska <tamaraatanasoska>`.

- |API| the `assert_all_finite` parameter of functions :func:`utils.check_array`,
:func:`utils.check_X_y`, :func:`utils.as_float_array` is renamed into
`ensure_all_finite`. `force_all_finite` will be removed in 1.8.
Expand Down
8 changes: 5 additions & 3 deletions sklearn/decomposition/_nmf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1700,8 +1700,6 @@ def _fit_transform(self, X, y=None, W=None, H=None, update_H=True):
n_iter_ : int
Actual number of iterations.
"""
check_non_negative(X, "NMF (input X)")

# check parameters
self._check_params(X)

Expand Down Expand Up @@ -1777,7 +1775,11 @@ def transform(self, X):
"""
check_is_fitted(self)
X = self._validate_data(
X, accept_sparse=("csr", "csc"), dtype=[np.float64, np.float32], reset=False
X,
accept_sparse=("csr", "csc"),
dtype=[np.float64, np.float32],
reset=False,
ensure_non_negative=True,
)

with config_context(assume_finite=True):
Expand Down
2 changes: 1 addition & 1 deletion sklearn/ensemble/_weight_boosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def fit(self, X, y, sample_weight=None):
)

sample_weight = _check_sample_weight(
sample_weight, X, np.float64, copy=True, only_non_negative=True
sample_weight, X, np.float64, copy=True, ensure_non_negative=True
)
sample_weight /= sample_weight.sum()

Expand Down
9 changes: 4 additions & 5 deletions sklearn/kernel_approximation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from .utils.validation import (
_check_feature_names_in,
check_is_fitted,
check_non_negative,
)


Expand Down Expand Up @@ -674,8 +673,7 @@ def fit(self, X, y=None):
self : object
Returns the transformer.
"""
X = self._validate_data(X, accept_sparse="csr")
check_non_negative(X, "X in AdditiveChi2Sampler.fit")
X = self._validate_data(X, accept_sparse="csr", ensure_non_negative=True)

if self.sample_interval is None and self.sample_steps not in (1, 2, 3):
raise ValueError(
Expand All @@ -701,8 +699,9 @@ def transform(self, X):
Whether the return value is an array or sparse matrix depends on
the type of the input X.
"""
X = self._validate_data(X, accept_sparse="csr", reset=False)
check_non_negative(X, "X in AdditiveChi2Sampler.transform")
X = self._validate_data(
X, accept_sparse="csr", reset=False, ensure_non_negative=True
)
sparse = sp.issparse(X)

if self.sample_interval is None:
Expand Down
2 changes: 1 addition & 1 deletion sklearn/linear_model/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ def fit(self, X, y, sample_weight=None):
has_sw = sample_weight is not None
if has_sw:
sample_weight = _check_sample_weight(
sample_weight, X, dtype=X.dtype, only_non_negative=True
sample_weight, X, dtype=X.dtype, ensure_non_negative=True
)

# Note that neither _rescale_data nor the rest of the fit method of
Expand Down
13 changes: 8 additions & 5 deletions sklearn/neighbors/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from ..utils.fixes import parse_version, sp_base_version
from ..utils.multiclass import check_classification_targets
from ..utils.parallel import Parallel, delayed
from ..utils.validation import _to_object_array, check_is_fitted, check_non_negative
from ..utils.validation import _to_object_array, check_is_fitted
from ._ball_tree import BallTree
from ._kd_tree import KDTree

Expand Down Expand Up @@ -167,8 +167,7 @@ def _check_precomputed(X):
case only non-zero elements may be considered neighbors.
"""
if not issparse(X):
X = check_array(X)
check_non_negative(X, whom="precomputed distance matrix.")
X = check_array(X, ensure_non_negative=True, input_name="X")
return X
else:
graph = X
Expand All @@ -179,8 +178,12 @@ def _check_precomputed(X):
"its handling of explicit zeros".format(graph.format)
)
copied = graph.format != "csr"
graph = check_array(graph, accept_sparse="csr")
check_non_negative(graph, whom="precomputed distance matrix.")
graph = check_array(
graph,
accept_sparse="csr",
ensure_non_negative=True,
input_name="precomputed distance matrix",
)
graph = sort_graph_by_row_values(graph, copy=not copied, warn_when_not_sorted=True)

return graph
Expand Down
2 changes: 1 addition & 1 deletion sklearn/neighbors/_kde.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def fit(self, X, y=None, sample_weight=None):

if sample_weight is not None:
sample_weight = _check_sample_weight(
sample_weight, X, dtype=np.float64, only_non_negative=True
sample_weight, X, dtype=np.float64, ensure_non_negative=True
)

kwargs = self.metric_params
Expand Down
4 changes: 2 additions & 2 deletions sklearn/tests/test_kernel_approximation.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ def test_additive_chi2_sampler_exceptions():
transformer = AdditiveChi2Sampler()
X_neg = X.copy()
X_neg[0, 0] = -1
with pytest.raises(ValueError, match="X in AdditiveChi2Sampler.fit"):
with pytest.raises(ValueError, match="X in AdditiveChi2Sampler"):
transformer.fit(X_neg)
with pytest.raises(ValueError, match="X in AdditiveChi2Sampler.transform"):
with pytest.raises(ValueError, match="X in AdditiveChi2Sampler"):
transformer.fit(X)
transformer.transform(X_neg)

Expand Down
15 changes: 13 additions & 2 deletions sklearn/utils/tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,17 @@ def test_check_array():
result = check_array(X_no_array)
assert isinstance(result, np.ndarray)

# check negative values when ensure_non_negative=True
X_neg = check_array([[1, 2], [-3, 4]])
err_msg = "Negative values in data passed to X in RandomForestRegressor"
with pytest.raises(ValueError, match=err_msg):
check_array(
X_neg,
ensure_non_negative=True,
input_name="X",
estimator=RandomForestRegressor(),
)


@pytest.mark.parametrize(
"X",
Expand Down Expand Up @@ -1480,13 +1491,13 @@ def test_check_sample_weight():
sample_weight = _check_sample_weight(None, X, dtype=X.dtype)
assert sample_weight.dtype == np.float64

# check negative weight when only_non_negative=True
# check negative weight when ensure_non_negative=True
X = np.ones((5, 2))
sample_weight = np.ones(_num_samples(X))
sample_weight[-1] = -10
err_msg = "Negative values in data passed to `sample_weight`"
with pytest.raises(ValueError, match=err_msg):
_check_sample_weight(sample_weight, X, only_non_negative=True)
_check_sample_weight(sample_weight, X, ensure_non_negative=True)


@pytest.mark.parametrize("toarray", [np.array, sp.csr_matrix, sp.csc_matrix])
Expand Down
21 changes: 17 additions & 4 deletions sklearn/utils/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ def check_array(
force_writeable=False,
force_all_finite="deprecated",
ensure_all_finite=None,
ensure_non_negative=False,
ensure_2d=True,
allow_nd=False,
ensure_min_samples=1,
Expand Down Expand Up @@ -828,6 +829,12 @@ def check_array(
.. versionadded:: 1.6
`force_all_finite` was renamed to `ensure_all_finite`.

ensure_non_negative : bool, default=False
Make sure the array has only non-negative values. If True, an array that
contains negative values will raise a ValueError.

.. versionadded:: 1.6

ensure_2d : bool, default=True
Whether to raise a value error if array is not 2D.

Expand Down Expand Up @@ -1132,6 +1139,12 @@ def is_sparse(dtype):
% (n_features, array.shape, ensure_min_features, context)
)

if ensure_non_negative:
whom = input_name
if estimator_name:
whom += f" in {estimator_name}"
check_non_negative(array, whom)

if force_writeable:
# By default, array.copy() creates a C-ordered copy. We set order=K to
# preserve the order of the array.
Expand Down Expand Up @@ -1739,7 +1752,7 @@ def check_non_negative(X, whom):
X_min = xp.min(X)

if X_min < 0:
raise ValueError("Negative values in data passed to %s" % whom)
raise ValueError(f"Negative values in data passed to {whom}.")


def check_scalar(
Expand Down Expand Up @@ -2044,7 +2057,7 @@ def _check_psd_eigenvalues(lambdas, enable_warnings=False):


def _check_sample_weight(
sample_weight, X, dtype=None, copy=False, only_non_negative=False
sample_weight, X, dtype=None, copy=False, ensure_non_negative=False
):
"""Validate sample weights.

Expand All @@ -2061,7 +2074,7 @@ def _check_sample_weight(
X : {ndarray, list, sparse matrix}
Input data.

only_non_negative : bool, default=False,
ensure_non_negative : bool, default=False,
Whether or not the weights are expected to be non-negative.

.. versionadded:: 1.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have a versionchanged to mention that we rename the parameter. @jeremiedbb I'm scared to break code but this should be quite limited and easy to handle for downstream package.

Expand Down Expand Up @@ -2112,7 +2125,7 @@ def _check_sample_weight(
)
)

if only_non_negative:
if ensure_non_negative:
check_non_negative(sample_weight, "`sample_weight`")

return sample_weight
Expand Down