Skip to content

Commit 73a3eab

Browse files
cakedev0ogriselAnneBeyerOmarManzoor
authored andcommitted
Fix: Array-API - avoid failing for numpy fit + predict with sparse or array-like X (#34144)
Co-authored-by: Olivier Grisel <olivier.grisel@ensta.org> Co-authored-by: Anne Beyer <anne.beyer@mailbox.org> Co-authored-by: Omar Salman <omar.salman@arbisoft.com>
1 parent 8839aae commit 73a3eab

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- Fixed a bug where NumPy-fitted estimators could raise an error with
2+
``config_context(array_api_dispatch=True)`` when making predictions with
3+
array-like or SciPy sparse inputs, or when a fitted attribute was sparse,
4+
such as after calling :meth:`linear_model.LogisticRegression.sparsify`.
5+
By :user:`Arthur Lacote <cakedev0>`.

sklearn/utils/_array_api.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,22 @@ def check_same_namespace(X, estimator, *, attribute, method):
11351135
if X_xp == a_xp and X_device == a_device:
11361136
return
11371137

1138-
if X_xp != a_xp:
1138+
if _is_numpy_namespace(a_xp) and _is_numpy_namespace(X_xp):
1139+
# this condition is reached when either:
1140+
# - `X` is array-like or sparse-matrix and the estimator was fitted with numpy
1141+
# - `attr` is a sparse matrix and `X` is a numpy array
1142+
# in which case devices are different (None vs "cpu") but
1143+
# `check_same_namespace` should not raise
1144+
return
1145+
1146+
if X_device is None:
1147+
type_name = "sparse array" if sp.issparse(X) else "array-like"
1148+
msg = (
1149+
f"Array namespace used during fit ({a_xp.__name__}) "
1150+
f"is not compatible with the {type_name} input passed to {method}. "
1151+
f"Only the NumPy namespace is compatible with {type_name} inputs."
1152+
)
1153+
elif X_xp != a_xp:
11391154
msg = (
11401155
f"Array namespaces used during fit ({a_xp.__name__}) "
11411156
f"and {method} ({X_xp.__name__}) differ."
@@ -1148,7 +1163,8 @@ def check_same_namespace(X, estimator, *, attribute, method):
11481163
"must use the same namespace and the same device as those passed to fit(). "
11491164
f"{msg} "
11501165
"You can move the estimator to the same namespace and device as X with: "
1151-
"'from sklearn.utils._array_api import move_estimator_to; "
1166+
"'from sklearn.utils._array_api import get_namespace_and_device, "
1167+
"move_estimator_to; "
11521168
"xp, _, device = get_namespace_and_device(X); "
11531169
"estimator = move_estimator_to(estimator, xp, device)'"
11541170
)

sklearn/utils/tests/test_array_api.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,9 @@ def predict(self, X):
503503
check_same_namespace(X, self, attribute="X_", method="predict")
504504
return X
505505

506+
def sparsify(self):
507+
self.X_ = sp.csr_matrix(self.X_)
508+
506509

507510
class SimpleEstimatorCustomLogic(BaseEstimator):
508511
def fit(self, X, y=None):
@@ -607,6 +610,31 @@ def test_check_fitted_attribute():
607610
est.predict(numpy.asarray([0]))
608611

609612

613+
@skip_if_array_api_compat_not_configured
614+
@pytest.mark.parametrize("X", [[[1.3, 4.5]], sp.csr_array([[1.3, 4.5]])])
615+
def test_check_fitted_attribute_with_non_array_input(X):
616+
"""Check validation of non-array input against fitted attribute ``X_``.
617+
618+
``SimpleEstimator.predict`` calls ``check_same_namespace`` with
619+
``attribute="X_"`` to compare the input with the fitted data.
620+
"""
621+
xp = pytest.importorskip("array_api_strict")
622+
623+
with config_context(array_api_dispatch=True):
624+
est = SimpleEstimator().fit(numpy.asarray([[1.3, 4.5]]))
625+
# shouldn't raise:
626+
est.predict(X)
627+
628+
est.sparsify()
629+
# shouldn't raise either:
630+
est.predict(X)
631+
est.predict(numpy.asarray([[1.3, 4.5]]))
632+
633+
est = SimpleEstimator().fit(xp.asarray([[1.3, 4.5]]))
634+
with pytest.raises(ValueError, match="Array namespace.*not compatible"):
635+
est.predict(X)
636+
637+
610638
@pytest.mark.parametrize(
611639
"namespace, device_name, dtype_name",
612640
yield_namespace_device_dtype_combinations(),

0 commit comments

Comments
 (0)