Repository: scikit-learn/scikit-learn
Pull request: FIX Raise proper error for sparse dataframe with mixed dtypes
Fixes #17945.
Currently, function check_array() in validation.py throws a confusing exception while validating:
1. A sparse DataFrame with mixed numeric types
2. A sparse DataFrame with object types.
The error happens because for this cases the DataFrame creates a coo_matrix with dtype('object'), and when
trying to create a csr_matrix from this coo_matrix scipy will raise a ValueError. 
This scipy Exception is not very informative for the scikit-learn user that gets this exception while trying to fit an
estimator with a sparse DataFrame.

The fix is to handle this two cases in check_array by looking at the coo_matrix and issue a clear Exception to the caller.
The fix includes a uninittest that handles all the intput types scenarios that can generate this issue.
The unnittest will fail if run in the version prior to this fix.


Problem statement:
TruncatedSVD fails on DataFrame with mixture of Sparse columns int and float
#### Describe the bug
TruncatedSVD doesn't work on a DataFrame that has both Sparse[float64] and Sparse[int32] columns
Whereas it is working if the DataFrame has only Sparse[float64] and also if it has only Sparse[int32]

#### Steps/Code to Reproduce

```from sklearn.decomposition import TruncatedSVD

import numpy as np
import pandas as pd

np.random.seed(123)
X1 = np.random.randn(50, 10)
df1 = pd.DataFrame({j:pd.arrays.SparseArray(X1[:,j]) for j in range(X1.shape[1])})

df1.dtypes # Sparse[float64]
# ok : dataframe with sparse float
svd = TruncatedSVD(n_components=2, random_state=123)
svd.fit(df1)  #works fine


# ok : dataframe with sparse int
X2 = np.random.randint(0,10,(50,10))
df2 = pd.DataFrame({j:pd.arrays.SparseArray(X2[:,j]) for j in range(X2.shape[1])})
df2.dtypes # Sparse[int32]

svd = TruncatedSVD(n_components=2, random_state=123)
svd.fit(df2) #also work fine

# fails : mix sparse int and sparse float
df = pd.concat((df1, df2), axis=1)
df.columns=list(range(df.shape[1]))

svd = TruncatedSVD(n_components=2, random_state=123)
svd.fit(df)
#  raises : TypeError: no supported conversion for types: (dtype('O'),)
```
Update : further investigation seems to show that the problem actually come from 'check_array'

```
from sklearn.utils import check_array
check_array(df,  accept_sparse=['csr', 'csc'], ensure_min_features=2)
```



#### Versions
System:
    python: 3.7.4 (default, Aug  9 2019, 18:34:13) [MSC v.1915 64 bit (AMD64)]
executable: C:\**\**\Anaconda3\pythonw.exe
   machine: Windows-10-10.0.***-SP0

Python dependencies:
          pip: 19.2.3
   setuptools: 41.4.0
      sklearn: 0.23.1
        numpy: 1.16.5
        scipy: 1.3.1
       Cython: 0.29.13
       pandas: 1.0.5
   matplotlib: 3.1.1
       joblib: 0.13.2
threadpoolctl: 2.1.0

Built with OpenMP: True
Windows-10-10.0.**-SP0
Python 3.7.4 (default, Aug  9 2019, 18:34:13) [MSC v.1915 64 bit (AMD64)]
NumPy 1.16.5
SciPy 1.3.1
Scikit-Learn 0.23.1

Hints from issue discussion:
Hi @LionelMassoulard. I would like to try and resolve this bug. Is it ok or are you already working on it?
Hi, no it's okay, I took a look and I think I isolated the issue to "check_array" but I didn't work on it any further.
The issues stems from `df.sparse.to_coo()` with mixed pandas sparse extension array results in a coo sparse matrix with object dtypes:

```py
import pandas as pd

df = pd.DataFrame({'float_col': pd.arrays.SparseArray([0, 1, 0],
                                                      dtype="float", fill_value=0),
                   'int_col': pd.arrays.SparseArray([1, 0, 1], dtype='int')})
df.dtypes
# float_col    Sparse[float64, 0]
# int_col        Sparse[int64, 0]

df.sparse.to_coo()
# <3x2 sparse matrix of type '<class 'numpy.object_'>'
#  	with 3 stored elements in COOrdinate format>
```
@thomasjpfan  you are right. I looked into this issue and  pandas SparseFrameAccessor.to_coo() will create a coo_matrix  with dtype np.object, If the DataFrame containd sparse columns of different types.  And a coo_matrix with np.objects cannot be used to generate a csr_matrix. When calling coo_matrix.to_csr, we get the exception described in the issue.

I propose a fix in scikit -learn,file validation.py in function check_array. Just beforre calling 
array.sparse.to_coo()
we can test if the sparse DataFrame has columns with different numeric types, and instead of calling    SparseFrameAccessor.to_coo() we calll SparseFrameAccessor.to_dense() which return a dense DataFrame.
I tested TruncatedSVD.fit() with this approach and verified we get a valid model.
Does this look like a good solution? If so I will submit a PR.
Hey @thomasjpfan, @jnothman.
The issue stems from a pandas limitation to create a sparse matrix when the DataFrame has sparse columns of different numeric types - like int and float. In this case it creates a "problematic" coo_matrix. The coo_matrix has np.object elemets, so calling tocsr() on it will fail. This is the exception we are getting.
I propose a fix where in validation.py we detect whether the sparse DataFrame has mixed numeric types and in this case we call SparseFrameAccessor.to_dense()  instead of  SparseFrameAccessor.to_coo()
Does it make sense? 
sparse matrix incompatibilty with np.object is encapsulated in the upcast function:

```
from scipy.sparse.sputils import upcast
upcast(np.dtype('f'))
Out[22]: numpy.float32

upcast(np.dtype('O'))
Traceback (most recent call last):
  File "/anaconda3/envs/sklearn-dev/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3343, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-23-2b9e4bba277d>", line 1, in <module>
    upcast(np.dtype('O'))
  File "/anaconda3/envs/sklearn-dev/lib/python3.7/site-packages/scipy/sparse/sputils.py", line 51, in upcast
    raise TypeError('no supported conversion for types: %r' % (args,))
TypeError: no supported conversion for types: (dtype('O'),)
```
> we can test if the sparse DataFrame has columns with different numeric types, and instead of calling SparseFrameAccessor.to_coo() we calll SparseFrameAccessor.to_dense() which return a dense DataFrame.

I would not recommend densifying the sparse matrix, especially when the original dataframe had a sparse presentation. I would consider raising an error when there the sparse extension arrays are not of the same type.

On a related note, the semantics of a [pandas sparse](https://pandas.pydata.org/docs/reference/api/pandas.arrays.SparseArray.html#pandas.arrays.SparseArray) is not consistent across the data types. Float extension arrays have a default fill value of `np.nan`, while int extension arrays have a default fill value of 0. This means trying to automatically combing them may not be what is expected.
OK, @thomasjpfan. I agree. I'll implement a solution that checks for different numeric types in the DataFrame and issues an error. This will be done in function check_arrray in validation.py. Will submit a PR with this solution soon
Further analysis :
In order to produce a coo_matrix, function SparseFrameAccessor.to_coo() looks at the SparseDtype of the Dataframe columns. All columns need to have the same SparseDtype so that the coo_matrix will NOT have np.dtype('object').

When comparing two SparseDtypes the code looks at their np.dtype. The point is that two different np.types can produce the same dtype. For example:
```
np.dtype('integer') == np.dtype('long'). 
np.dtype('integer').name = 'int64'
np.dtype('long').name = 'int64'
```
This means we can create a sparse DataFrame with different column np.types as long as they map to the same np.dtype.
And this DataFrame will be validated by check_array:
```
tf = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0], dtype="int"),
                                  'col2': pd.arrays.SparseArray([1, 0, 1], dtype='long')})
```
But this DataFrame will not:
```
tf = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0], dtype="int32"),
                                  'col2': pd.arrays.SparseArray([1, 0, 1], dtype='long')})
```

The full mapping between numpy types and numpy dtypes, bellow:
```
                     np_name                          np_type        dtype
dtype_name
bool                   bool_            <class 'numpy.bool_'>         bool
bool                   bool8            <class 'numpy.bool_'>         bool
bytes                string_           <class 'numpy.bytes_'>          |S0
bytes                 bytes_           <class 'numpy.bytes_'>          |S0
bytes                 bytes0           <class 'numpy.bytes_'>          |S0
bytes              character        <class 'numpy.character'>          |S0
complex128   complexfloating  <class 'numpy.complexfloating'>   complex128
complex128          complex_       <class 'numpy.complex128'>   complex128
complex128        complex128       <class 'numpy.complex128'>   complex128
complex128            cfloat       <class 'numpy.complex128'>   complex128
complex128           cdouble       <class 'numpy.complex128'>   complex128
complex256        clongfloat       <class 'numpy.complex256'>   complex256
complex256        complex256       <class 'numpy.complex256'>   complex256
complex256       longcomplex       <class 'numpy.complex256'>   complex256
complex256       clongdouble       <class 'numpy.complex256'>   complex256
complex64          complex64        <class 'numpy.complex64'>    complex64
complex64            csingle        <class 'numpy.complex64'>    complex64
complex64      singlecomplex        <class 'numpy.complex64'>    complex64
datetime64        datetime64       <class 'numpy.datetime64'>   datetime64
float128            float128         <class 'numpy.float128'>     float128
float128           longfloat         <class 'numpy.float128'>     float128
float128          longdouble         <class 'numpy.float128'>     float128
float16                 half          <class 'numpy.float16'>      float16
float16              float16          <class 'numpy.float16'>      float16
float32              float32          <class 'numpy.float32'>      float32
float32               single          <class 'numpy.float32'>      float32
float64              inexact          <class 'numpy.inexact'>      float64
float64               float_          <class 'numpy.float64'>      float64
float64             floating         <class 'numpy.floating'>      float64
float64               double          <class 'numpy.float64'>      float64
float64               number           <class 'numpy.number'>      float64
float64              float64          <class 'numpy.float64'>      float64
int16                  int16            <class 'numpy.int16'>        int16
int16                  short            <class 'numpy.int16'>        int16
int32                  int32            <class 'numpy.int32'>        int32
int32                   intc            <class 'numpy.int32'>        int32
int64                integer          <class 'numpy.integer'>        int64
int64                   int_            <class 'numpy.int64'>        int64
int64                   int0            <class 'numpy.int64'>        int64
int64               longlong         <class 'numpy.longlong'>        int64
int64                   long            <class 'numpy.int64'>        int64
int64                   intp            <class 'numpy.int64'>        int64
int64                  int64            <class 'numpy.int64'>        int64
int64          signedinteger    <class 'numpy.signedinteger'>        int64
int8                    int8             <class 'numpy.int8'>         int8
int8                    byte             <class 'numpy.int8'>         int8
object               object_          <class 'numpy.object_'>       object
object               object0          <class 'numpy.object_'>       object
str                     str_             <class 'numpy.str_'>          <U0
str                 unicode_             <class 'numpy.str_'>          <U0
str                  unicode             <class 'numpy.str_'>          <U0
str                     str0             <class 'numpy.str_'>          <U0
timedelta64      timedelta64      <class 'numpy.timedelta64'>  timedelta64
uint16                ushort           <class 'numpy.uint16'>       uint16
uint16                uint16           <class 'numpy.uint16'>       uint16
uint32                uint32           <class 'numpy.uint32'>       uint32
uint32                 uintc           <class 'numpy.uint32'>       uint32
uint64                uint64           <class 'numpy.uint64'>       uint64
uint64       unsignedinteger  <class 'numpy.unsignedinteger'>       uint64
uint64             ulonglong        <class 'numpy.ulonglong'>       uint64
uint64                 uintp           <class 'numpy.uint64'>       uint64
uint64                  uint           <class 'numpy.uint64'>       uint64
uint64                 uint0           <class 'numpy.uint64'>       uint64
uint8                  uint8            <class 'numpy.uint8'>        uint8
uint8                  ubyte            <class 'numpy.uint8'>        uint8
void                flexible         <class 'numpy.flexible'>          |V0
void                    void             <class 'numpy.void'>          |V0
void                   void0             <class 'numpy.void'>          |V0
void                 generic          <class 'numpy.generic'>          |V0
```













Hey, @thomasjpfan. I recently submitted a pull request to fix this issue.

Commit message: test that invalid behaviour is fixed starting from pandas 1.1.0  (#17945)

Patch to review:
diff --git a/doc/whats_new/v0.24.rst b/doc/whats_new/v0.24.rst
index aaf86a2f0576d..a72cca8517b51 100644
--- a/doc/whats_new/v0.24.rst
+++ b/doc/whats_new/v0.24.rst
@@ -433,6 +433,14 @@ Changelog
   :meth:`tree.DecisionTreeRegressor.fit`, and has not effect.
   :pr:`17614` by :user:`Juan Carlos Alfaro Jiménez <alfaro96>`.
 
+:mod:`sklearn.utils`
+.........................
+
+- |Fix| Raise ValueError with clear error message in :func:`check_array`
+  for sparse DataFrames with mixed types.
+  :pr:`17992` by :user:`Thomas J. Fan <thomasjpfan>` and
+  :user:`Alex Shacked <alexshacked>`.
+
 - |Fix| Allow serialized tree based models to be unpickled on a machine
   with different endianness.
   :pr:`17644` by :user:`Qi Zhang <qzhang90>`.
diff --git a/sklearn/utils/tests/test_validation.py b/sklearn/utils/tests/test_validation.py
index e59693b26d307..cd7a14a3a15ac 100644
--- a/sklearn/utils/tests/test_validation.py
+++ b/sklearn/utils/tests/test_validation.py
@@ -45,6 +45,7 @@
     _allclose_dense_sparse,
     FLOAT_DTYPES)
 from sklearn.utils.validation import _check_fit_params
+from sklearn.utils.fixes import parse_version
 
 import sklearn
 
@@ -1213,3 +1214,60 @@ def test_check_sparse_pandas_sp_format(sp_format):
     assert sp.issparse(result)
     assert result.format == sp_format
     assert_allclose_dense_sparse(sp_mat, result)
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longdouble", "float16"),
+    ("float16", "float32"),
+    ("float32", "double"),
+    ("int16", "int32"),
+    ("int32", "long"),
+    ("byte", "uint16"),
+    ("ushort", "uint32"),
+    ("uint32", "uint64"),
+    ("uint8", "int8"),
+])
+def test_check_pandas_sparse_invalid(ntype1, ntype2):
+    pd = pytest.importorskip("pandas", minversion="0.25.0")
+    df = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0],
+                                                     dtype=ntype1),
+                       'col2': pd.arrays.SparseArray([1, 0, 1],
+                                                     dtype=ntype2)})
+
+    if parse_version(pd.__version__) < parse_version('1.1'):
+        with pytest.raises(ValueError,
+                           match="Pandas DataFrame with mixed "
+                                 "sparse extension arrays"):
+            check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                               'ensure_min_features': 2})
+    else:
+        check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                           'ensure_min_features': 2})
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longfloat", "longdouble"),
+    ("float16", "half"),
+    ("single", "float32"),
+    ("double", "float64"),
+    ("int8", "byte"),
+    ("short", "int16"),
+    ("intc", "int32"),
+    ("int0", "long"),
+    ("int", "long"),
+    ("int64", "longlong"),
+    ("int_", "intp"),
+    ("ubyte", "uint8"),
+    ("uint16", "ushort"),
+    ("uintc", "uint32"),
+    ("uint", "uint64"),
+    ("uintp", "ulonglong")
+])
+def test_check_pandas_sparse_valid(ntype1, ntype2):
+    pd = pytest.importorskip("pandas", minversion="0.25.0")
+    df = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0],
+                                                     dtype=ntype1),
+                       'col2': pd.arrays.SparseArray([1, 0, 1],
+                                                     dtype=ntype2)})
+    check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                       'ensure_min_features': 2})
diff --git a/sklearn/utils/validation.py b/sklearn/utils/validation.py
index 00deee97d5a14..c933e6591d95e 100644
--- a/sklearn/utils/validation.py
+++ b/sklearn/utils/validation.py
@@ -568,6 +568,14 @@ def check_array(array, accept_sparse=False, *, accept_large_sparse=True,
     if hasattr(array, 'sparse') and array.ndim > 1:
         # DataFrame.sparse only supports `to_coo`
         array = array.sparse.to_coo()
+        if array.dtype == np.dtype('object'):
+            unique_dtypes = set([dt.subtype.name for dt in
+                                 array_orig.dtypes])
+            if len(unique_dtypes) > 1:
+                raise ValueError(
+                    "Pandas DataFrame with mixed sparse extension arrays "
+                    "generated a sparse matrix with object dtype which "
+                    "can not be converted to a scipy sparse matrix")
 
     if sp.issparse(array):
         _ensure_no_complex_data(array)


Code review comments:
diff_hunk: @@ -1213,3 +1214,60 @@ def test_check_sparse_pandas_sp_format(sp_format):
     assert sp.issparse(result)
     assert result.format == sp_format
     assert_allclose_dense_sparse(sp_mat, result)
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longdouble", "float16"),
+    ("float16", "float32"),
+    ("float32", "double"),
+    ("int16", "int32"),
+    ("int32", "long"),
+    ("byte", "uint16"),
+    ("ushort", "uint32"),
+    ("uint32", "uint64"),
+    ("uint8", "int8"),
+])
+def test_check_pandas_sparse_invalid(ntype1, ntype2):
+    pd = pytest.importorskip("pandas", minversion="0.25.0")
+    df = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0],
+                                                     dtype=ntype1),
+                       'col2': pd.arrays.SparseArray([1, 0, 1],
+                                                     dtype=ntype2)})
+
+    if parse_version(pd.__version__) < parse_version('1.1'):
+        with pytest.raises(ValueError,
+                           match="Pandas DataFrame with mixed "
+                                 "sparse extension arrays"):
+            check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                               'ensure_min_features': 2})
+    else:
+        check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                           'ensure_min_features': 2})
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longfloat", "longdouble"),
+    ("float16", "half"),
+    ("single", "float32"),
+    ("double", "float64"),
+    ("int8", "byte"),
+    ("short", "int16"),
+    ("intc", "int32"),
+    ("int0", "long"),
+    ("int", "long"),
+    ("int64", "longlong"),
+    ("int_", "intp"),
+    ("ubyte", "uint8"),
+    ("uint16", "ushort"),
+    ("uintc", "uint32"),
+    ("uint", "uint64"),
+    ("uintp", "ulonglong")
+])
path: sklearn/utils/tests/test_validation.py
text: @user2:
```suggestion
@user1.mark.parametrize(
    "ntype1, ntype2",
    [
        ("longfloat", "longdouble"),
        ("float16", "half"),
        ("single", "float32"),
        ("double", "float64"),
        ("int8", "byte"),
        ("short", "int16"),
        ("intc", "int32"),
        ("int0", "long"),
        ("int", "long"),
        ("int64", "longlong"),
        ("int_", "intp"),
        ("ubyte", "uint8"),
        ("uint16", "ushort"),
        ("uintc", "uint32"),
        ("uint", "uint64"),
        ("uintp", "ulonglong"),
    ]
)
```

diff_hunk: @@ -1213,3 +1214,60 @@ def test_check_sparse_pandas_sp_format(sp_format):
     assert sp.issparse(result)
     assert result.format == sp_format
     assert_allclose_dense_sparse(sp_mat, result)
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longdouble", "float16"),
+    ("float16", "float32"),
+    ("float32", "double"),
+    ("int16", "int32"),
+    ("int32", "long"),
+    ("byte", "uint16"),
+    ("ushort", "uint32"),
+    ("uint32", "uint64"),
+    ("uint8", "int8"),
+])
+def test_check_pandas_sparse_invalid(ntype1, ntype2):
+    pd = pytest.importorskip("pandas", minversion="0.25.0")
+    df = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0],
+                                                     dtype=ntype1),
+                       'col2': pd.arrays.SparseArray([1, 0, 1],
+                                                     dtype=ntype2)})
+
+    if parse_version(pd.__version__) < parse_version('1.1'):
+        with pytest.raises(ValueError,
+                           match="Pandas DataFrame with mixed "
+                                 "sparse extension arrays"):
+            check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                               'ensure_min_features': 2})
path: sklearn/utils/tests/test_validation.py
text: @user1:
```suggestion
        err_msg = "Pandas DataFrame with mixed sparse extension arrays"
        with pytest.raises(ValueError, match=err_msg):
            check_array(df, **{'accept_sparse': ['csr', 'csc']})
```

diff_hunk: @@ -1213,3 +1214,60 @@ def test_check_sparse_pandas_sp_format(sp_format):
     assert sp.issparse(result)
     assert result.format == sp_format
     assert_allclose_dense_sparse(sp_mat, result)
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longdouble", "float16"),
+    ("float16", "float32"),
+    ("float32", "double"),
+    ("int16", "int32"),
+    ("int32", "long"),
+    ("byte", "uint16"),
+    ("ushort", "uint32"),
+    ("uint32", "uint64"),
+    ("uint8", "int8"),
+])
+def test_check_pandas_sparse_invalid(ntype1, ntype2):
+    pd = pytest.importorskip("pandas", minversion="0.25.0")
+    df = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0],
+                                                     dtype=ntype1),
+                       'col2': pd.arrays.SparseArray([1, 0, 1],
+                                                     dtype=ntype2)})
+
+    if parse_version(pd.__version__) < parse_version('1.1'):
+        with pytest.raises(ValueError,
+                           match="Pandas DataFrame with mixed "
+                                 "sparse extension arrays"):
+            check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                               'ensure_min_features': 2})
+    else:
+        check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                           'ensure_min_features': 2})
path: sklearn/utils/tests/test_validation.py
text: @user1:
```suggestion
        check_array(df, **{'accept_sparse': ['csr', 'csc']})
```

diff_hunk: @@ -568,6 +568,14 @@ def check_array(array, accept_sparse=False, *, accept_large_sparse=True,
     if hasattr(array, 'sparse') and array.ndim > 1:
         # DataFrame.sparse only supports `to_coo`
         array = array.sparse.to_coo()
+        if array.dtype == np.dtype('object'):
+            unique_dtypes = set([dt.subtype.name for dt in
+                                 array_orig.dtypes])
+            if len(unique_dtypes) > 1:
+                raise ValueError(
+                    "Pandas DataFrame with mixed sparse extension arrays "
+                    "generated a sparse matrix with object dtype which "
+                    "can not be converted to a scipy sparse matrix")
path: sklearn/utils/validation.py
text: @user1:
```suggestion
                    "can not be converted to a scipy sparse matrix."
                )
```

@user1:
Shall we provide some hints to solve the issue? One might my want to convert manually to a single dtype if possible?

@author:
Done

diff_hunk: @@ -1213,3 +1214,60 @@ def test_check_sparse_pandas_sp_format(sp_format):
     assert sp.issparse(result)
     assert result.format == sp_format
     assert_allclose_dense_sparse(sp_mat, result)
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longdouble", "float16"),
+    ("float16", "float32"),
+    ("float32", "double"),
+    ("int16", "int32"),
+    ("int32", "long"),
+    ("byte", "uint16"),
+    ("ushort", "uint32"),
+    ("uint32", "uint64"),
+    ("uint8", "int8"),
+])
path: sklearn/utils/tests/test_validation.py
text: @user2:
```suggestion
@user1.mark.parametrize(
    "ntype1, ntype2",
    [
        ("longdouble", "float16"),
        ("float16", "float32"),
        ("float32", "double"),
        ("int16", "int32"),
        ("int32", "long"),
        ("byte", "uint16"),
        ("ushort", "uint32"),
        ("uint32", "uint64"),
        ("uint8", "int8"),
    ]
)
```

diff_hunk: @@ -568,6 +568,14 @@ def check_array(array, accept_sparse=False, *, accept_large_sparse=True,
     if hasattr(array, 'sparse') and array.ndim > 1:
         # DataFrame.sparse only supports `to_coo`
         array = array.sparse.to_coo()
+        if array.dtype == np.dtype('object'):
+            unique_dtypes = set([dt.subtype.name for dt in
+                                 array_orig.dtypes])
path: sklearn/utils/validation.py
text: @user1:
```suggestion
            unique_dtypes = set(
                [dt.subtype.name for dt in array_orig.dtypes]
            )
```

diff_hunk: @@ -1213,3 +1214,60 @@ def test_check_sparse_pandas_sp_format(sp_format):
     assert sp.issparse(result)
     assert result.format == sp_format
     assert_allclose_dense_sparse(sp_mat, result)
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longdouble", "float16"),
+    ("float16", "float32"),
+    ("float32", "double"),
+    ("int16", "int32"),
+    ("int32", "long"),
+    ("byte", "uint16"),
+    ("ushort", "uint32"),
+    ("uint32", "uint64"),
+    ("uint8", "int8"),
+])
+def test_check_pandas_sparse_invalid(ntype1, ntype2):
+    pd = pytest.importorskip("pandas", minversion="0.25.0")
+    df = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0],
+                                                     dtype=ntype1),
+                       'col2': pd.arrays.SparseArray([1, 0, 1],
+                                                     dtype=ntype2)})
+
+    if parse_version(pd.__version__) < parse_version('1.1'):
+        with pytest.raises(ValueError,
+                           match="Pandas DataFrame with mixed "
+                                 "sparse extension arrays"):
+            check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                               'ensure_min_features': 2})
+    else:
+        check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                           'ensure_min_features': 2})
+
+
+@pytest.mark.parametrize('ntype1, ntype2', [
+    ("longfloat", "longdouble"),
+    ("float16", "half"),
+    ("single", "float32"),
+    ("double", "float64"),
+    ("int8", "byte"),
+    ("short", "int16"),
+    ("intc", "int32"),
+    ("int0", "long"),
+    ("int", "long"),
+    ("int64", "longlong"),
+    ("int_", "intp"),
+    ("ubyte", "uint8"),
+    ("uint16", "ushort"),
+    ("uintc", "uint32"),
+    ("uint", "uint64"),
+    ("uintp", "ulonglong")
+])
+def test_check_pandas_sparse_valid(ntype1, ntype2):
+    pd = pytest.importorskip("pandas", minversion="0.25.0")
+    df = pd.DataFrame({'col1': pd.arrays.SparseArray([0, 1, 0],
+                                                     dtype=ntype1),
+                       'col2': pd.arrays.SparseArray([1, 0, 1],
+                                                     dtype=ntype2)})
+    check_array(df, **{'accept_sparse': ['csr', 'csc'],
+                       'ensure_min_features': 2})
path: sklearn/utils/tests/test_validation.py
text: @user1:
```suggestion
    check_array(df, **{'accept_sparse': ['csr', 'csc']})
```

@user1:
We should also check the output by checking that we get a numpy array with the expected dtype?

@user1:
Or at least that we have a single dtype

@author:
check_array returns a scipy csr_matrix. Scipy sparse matrices like numpy arrays always contain elements of the same type. The type is found in the matrix member dtype. So, one type is guaranteed.
I can test that the dtype of the csr_matrix is as expected. Will do.

@author:
Done