Repository: scikit-learn/scikit-learn
Pull request: FIX Dataframe support in KNN.predict
Fixes https://github.com/scikit-learn/scikit-learn/issues/26768

knn.predict doesn't validate X right away because it call `kneighbors` that does validate X.
The issue is that ``ArgKMin.is_usable_for`` assumes that it's a numpy array. This PR simply adds a check there. Note that it is not usable for a pandas dataframe anyway because it's not c-contiguous.

Problem statement:
AttributeError "Flags object has no attribute 'c_contiguous'" when using KNeighborsClassifier predict
### Describe the bug

I tried to build a K-nearest neighbor model. I used the default ``weights`` parameter and called the ``predict`` method with a pandas DataFrame object and got the following error:

AttributeError: 'Flags' object has no attribute 'c_contiguous'

### Steps/Code to Reproduce

```python
import pandas as pd
import numpy as np
from sklearn.neighbors import KNeighborsClassifier

X = pd.DataFrame(np.array([[1, 2], [3, 4], [5, 6], [7, 8]]), columns=['a', 'b'])
y = np.array([1, 2, 3, 4])

knn1 = KNeighborsClassifier(n_neighbors=2)

knn1.fit(X, y)

knn1.predict(X)
```

### Expected Results

I expect to get a prediction from the knn1 model. Unfortunately, I get this error

### Actual Results
```
AttributeError                            Traceback (most recent call last)
Cell In[5], line 5
      1 knn1 = KNeighborsClassifier(n_neighbors=2)
      3 knn1.fit(X, y)
----> 5 knn1.predict(X)

File ~/.local/lib/python3.10/site-packages/sklearn/neighbors/_classification.py:246, in KNeighborsClassifier.predict(self, X)
    244 check_is_fitted(self, "_fit_method")
    245 if self.weights == "uniform":
--> 246     if self._fit_method == "brute" and ArgKminClassMode.is_usable_for(
    247         X, self._fit_X, self.metric
    248     ):
    249         probabilities = self.predict_proba(X)
    250         if self.outputs_2d_:

File ~/.local/lib/python3.10/site-packages/sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py:471, in ArgKminClassMode.is_usable_for(cls, X, Y, metric)
    448 @classmethod
    449 def is_usable_for(cls, X, Y, metric) -> bool:
    450     """Return True if the dispatcher can be used for the given parameters.
    451 
    452     Parameters
   (...)
    468     True if the PairwiseDistancesReduction can be used, else False.
    469     """
    470     return (
--> 471         ArgKmin.is_usable_for(X, Y, metric)
    472         # TODO: Support CSR matrices.
    473         and not issparse(X)
    474         and not issparse(Y)
    475         # TODO: implement Euclidean specialization with GEMM.
    476         and metric not in ("euclidean", "sqeuclidean")
    477     )

File ~/.local/lib/python3.10/site-packages/sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py:115, in BaseDistancesReductionDispatcher.is_usable_for(cls, X, Y, metric)
    101 def is_valid_sparse_matrix(X):
    102     return (
    103         isspmatrix_csr(X)
    104         and
   (...)
    110         X.indices.dtype == X.indptr.dtype == np.int32
    111     )
    113 is_usable = (
    114     get_config().get("enable_cython_pairwise_dist", True)
--> 115     and (is_numpy_c_ordered(X) or is_valid_sparse_matrix(X))
    116     and (is_numpy_c_ordered(Y) or is_valid_sparse_matrix(Y))
    117     and X.dtype == Y.dtype
    118     and X.dtype in (np.float32, np.float64)
    119     and metric in cls.valid_metrics()
    120 )
    122 return is_usable

File ~/.local/lib/python3.10/site-packages/sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py:99, in BaseDistancesReductionDispatcher.is_usable_for.<locals>.is_numpy_c_ordered(X)
     98 def is_numpy_c_ordered(X):
---> 99     return hasattr(X, "flags") and X.flags.c_contiguous

AttributeError: 'Flags' object has no attribute 'c_contiguous'
```

### Versions

```shell
System:
    python: 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0]
executable: /usr/bin/python3
   machine: Linux-5.15.0-76-generic-x86_64-with-glibc2.35

Python dependencies:
      sklearn: 1.3.0
          pip: 23.1.2
   setuptools: 68.0.0
        numpy: 1.25.0
        scipy: 1.11.1
       Cython: 0.29.35
       pandas: 2.0.3
   matplotlib: 3.7.1
       joblib: 1.3.1
threadpoolctl: 3.1.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
         prefix: libopenblas
       filepath: /home/werner/.local/lib/python3.10/site-packages/numpy.libs/libopenblas64_p-r0-7a851222.3.23.so
        version: 0.3.23
threading_layer: pthreads
   architecture: Haswell
    num_threads: 20

       user_api: openmp
   internal_api: openmp
         prefix: libgomp
       filepath: /home/werner/.local/lib/python3.10/site-packages/scikit_learn.libs/libgomp-a34b3233.so.1.0.0
        version: None
    num_threads: 20

       user_api: blas
   internal_api: openblas
         prefix: libopenblas
       filepath: /home/werner/.local/lib/python3.10/site-packages/scipy.libs/libopenblasp-r0-23e5df77.3.21.dev.so
        version: 0.3.21.dev
threading_layer: pthreads
   architecture: Haswell
    num_threads: 20
```

Hints from issue discussion:
Thanks for the report @wejaeger. A fix is proposed in #26772.

Commit message: ref

Patch to review:
diff --git a/doc/whats_new/v1.3.rst b/doc/whats_new/v1.3.rst
index e3c444b3ae9ff..a559646fcb48e 100644
--- a/doc/whats_new/v1.3.rst
+++ b/doc/whats_new/v1.3.rst
@@ -2,6 +2,22 @@
 
 .. currentmodule:: sklearn
 
+.. _changes_1_3_1:
+
+Version 1.3.1
+=============
+
+**In development**
+
+Changelog
+---------
+
+:mod:`sklearn.neighbors`
+........................
+
+- |Fix| :meth:`neighbors.KNeighborsClassifier.predict` now works with dataframes.
+  :pr:`26772` by :user:`Jérémie du Boisberranger <jeremiedbb>`.
+
 .. _changes_1_3:
 
 Version 1.3.0
diff --git a/sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py b/sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py
index 42f9e38aa2265..557067b07c0f4 100644
--- a/sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py
+++ b/sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py
@@ -96,7 +96,9 @@ def is_usable_for(cls, X, Y, metric) -> bool:
         """
 
         def is_numpy_c_ordered(X):
-            return hasattr(X, "flags") and X.flags.c_contiguous
+            if not hasattr(X, "flags"):
+                return False
+            return getattr(X.flags, "c_contiguous", False)
 
         def is_valid_sparse_matrix(X):
             return (
diff --git a/sklearn/neighbors/tests/test_neighbors.py b/sklearn/neighbors/tests/test_neighbors.py
index 923c4e3e9506d..405ac3a6d0847 100644
--- a/sklearn/neighbors/tests/test_neighbors.py
+++ b/sklearn/neighbors/tests/test_neighbors.py
@@ -2193,3 +2193,17 @@ def _weights(dist):
     est = KNeighborsRegressor(n_neighbors=1, algorithm="brute", weights=_weights)
     est.fit(X, y)
     assert_allclose(est.predict([[0, 2.5]]), [6])
+
+
+def test_predict_dataframe():
+    """Check that KNN predict works with dataframes
+
+    non-regression test for issue #26768
+    """
+    pd = pytest.importorskip("pandas")
+
+    X = pd.DataFrame(np.array([[1, 2], [3, 4], [5, 6], [7, 8]]), columns=["a", "b"])
+    y = np.array([1, 2, 3, 4])
+
+    knn = neighbors.KNeighborsClassifier(n_neighbors=2).fit(X, y)
+    knn.predict(X)


Code review comments:
diff_hunk: @@ -96,7 +96,9 @@ def is_usable_for(cls, X, Y, metric) -> bool:
         """
 
         def is_numpy_c_ordered(X):
-            return hasattr(X, "flags") and X.flags.c_contiguous
+            if not hasattr(X, "flags"):
path: sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py
text: @user1:
I don't quite understand why this fixes it :( I always thought that something like `bool(expr1 and expr2)` will not evaluate `expr2` if `expr1` is `False`. From the bug report it looks like `hasattr(X, "flags")` will be `False`, but then why did it continue into evaluating `X.flags.c_contiguous`. And even more confusingly the exception is `AttributeError: 'Flags' object has no attribute 'c_contiguous'`, which makes me think that `hasattr(X, "flags")` evaluates as True when you use a DF as input. But then there is a new test and that passes. So overall I am left confused :-/

@user1:
Or is the thing that actually fixes it the `getattr(X.flags, "c_contiguous", False)`?

@author:
> which makes me think that hasattr(X, "flags") evaluates as True when you use a DF as input.

Yep, a dataframe has a `flags` attribute, but this `flags` has no `c_contiguous` attribute :)

@author:
> Or is the thing that actually fixes it the getattr(X.flags, "c_contiguous", False)?

indeed. Your understanding of the conditional evaluation of ``expr1 and expr2`` is correct

@user2:
So we could have kept a one-liner then:

```python
    return hasattr(X, "flags") and getattr(X.flags, "c_contiguous", False)
```

@user3:
hi every one so i have same prblm here 'AttributeError: 'Flags' object has no attribute 'c_contiguous''
and i dont understand the solution u given before .  any one can help ?