Repository: pandas-dev/pandas

Issue:
maybe_cast_to_integer_array fails when the input is all booleans
#### Code Sample, a copy-pastable example if possible

```python
In [1]: import pandas as pd, numpy as np

# expected behaviour with ordinary dtype
In [2]: pd.Series([True, False], dtype=int)
Out[2]:
0    1
1    0
dtype: int64

# broken
In [3]: pd.Series([True, False], dtype=pd.Int64Dtype())
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/internals/construction.py in _try_cast(arr, take_fast_path, dtype, copy, raise_cast_failure)
    694         if is_integer_dtype(dtype):
--> 695             subarr = maybe_cast_to_integer_array(arr, dtype)
    696

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/dtypes/cast.py in maybe_cast_to_integer_array(arr, dtype, copy)
   1304         if not hasattr(arr, "astype"):
-> 1305             casted = np.array(arr, dtype=dtype, copy=copy)
   1306         else:

TypeError: data type not understood

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-3-b747cfcdf17f> in <module>
----> 1 pd.Series([True, False], dtype=pd.Int64Dtype())

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/series.py in __init__(self, data, index, dtype, name, copy, fastpath)
    260             else:
    261                 data = sanitize_array(data, index, dtype, copy,
--> 262                                       raise_cast_failure=True)
    263
    264                 data = SingleBlockManager(data, index, fastpath=True)

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/internals/construction.py in sanitize_array(data, index, dtype, copy, raise_cast_failure)
    605             try:
    606                 subarr = _try_cast(data, False, dtype, copy,
--> 607                                    raise_cast_failure)
    608             except Exception:
    609                 if raise_cast_failure:  # pragma: no cover

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/internals/construction.py in _try_cast(arr, take_fast_path, dtype, copy, raise_cast_failure)
    714             # create an extension array from its dtype
    715             array_type = dtype.construct_array_type()._from_sequence
--> 716             subarr = array_type(arr, dtype=dtype, copy=copy)
    717         elif dtype is not None and raise_cast_failure:
    718             raise

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/arrays/integer.py in _from_sequence(cls, scalars, dtype, copy)
    301     @classmethod
    302     def _from_sequence(cls, scalars, dtype=None, copy=False):
--> 303         return integer_array(scalars, dtype=dtype, copy=copy)
    304
    305     @classmethod

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/arrays/integer.py in integer_array(values, dtype, copy)
    109     TypeError if incompatible types
    110     """
--> 111     values, mask = coerce_to_array(values, dtype=dtype, copy=copy)
    112     return IntegerArray(values, mask)
    113

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/arrays/integer.py in coerce_to_array(values, dtype, mask, copy)
    190     elif not (is_integer_dtype(values) or is_float_dtype(values)):
    191         raise TypeError("{} cannot be converted to an IntegerDtype".format(
--> 192             values.dtype))
    193
    194     if mask is None:

TypeError: bool cannot be converted to an IntegerDtype
```
#### Problem description

Pandas is unable to convert array of bools to IntegerDtype, while conversion to int is supported. What's interesting, if an arrays contains NaNs, then conversion goes as expected.
```python
In [4]: pd.Series([True, False, np.nan], dtype=pd.Int64Dtype())
Out[4]:
0      1
1      0
2    NaN
dtype: Int64
```
#### Expected Output
```python
In [5]: pd.Series([True, False], dtype=pd.Int64Dtype())
Out[4]:
0      1
1      0
dtype: Int64
```
#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None

pandas: 0.24.1
pytest: 4.2.0
pip: 19.0.1
setuptools: 40.7.3
Cython: 0.29.4
numpy: 1.15.4
scipy: 1.2.0
pyarrow: None
xarray: None
IPython: 7.2.0
sphinx: 1.8.4
patsy: None
dateutil: 2.7.5
pytz: 2018.9
blosc: None
bottleneck: 1.2.1
tables: None
numexpr: None
feather: None
matplotlib: 3.0.2
openpyxl: 2.5.14
xlrd: 1.2.0
xlwt: 1.3.0
xlsxwriter: 1.1.2
lxml.etree: 4.3.0
bs4: 4.7.1
html5lib: 1.0.1
sqlalchemy: 1.2.17
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
gcsfs: None
</details>

maybe_cast_to_integer_array fails when the input is all booleans
#### Code Sample, a copy-pastable example if possible

```python
In [1]: import pandas as pd, numpy as np

# expected behaviour with ordinary dtype
In [2]: pd.Series([True, False], dtype=int)
Out[2]:
0    1
1    0
dtype: int64

# broken
In [3]: pd.Series([True, False], dtype=pd.Int64Dtype())
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/internals/construction.py in _try_cast(arr, take_fast_path, dtype, copy, raise_cast_failure)
    694         if is_integer_dtype(dtype):
--> 695             subarr = maybe_cast_to_integer_array(arr, dtype)
    696

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/dtypes/cast.py in maybe_cast_to_integer_array(arr, dtype, copy)
   1304         if not hasattr(arr, "astype"):
-> 1305             casted = np.array(arr, dtype=dtype, copy=copy)
   1306         else:

TypeError: data type not understood

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-3-b747cfcdf17f> in <module>
----> 1 pd.Series([True, False], dtype=pd.Int64Dtype())

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/series.py in __init__(self, data, index, dtype, name, copy, fastpath)
    260             else:
    261                 data = sanitize_array(data, index, dtype, copy,
--> 262                                       raise_cast_failure=True)
    263
    264                 data = SingleBlockManager(data, index, fastpath=True)

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/internals/construction.py in sanitize_array(data, index, dtype, copy, raise_cast_failure)
    605             try:
    606                 subarr = _try_cast(data, False, dtype, copy,
--> 607                                    raise_cast_failure)
    608             except Exception:
    609                 if raise_cast_failure:  # pragma: no cover

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/internals/construction.py in _try_cast(arr, take_fast_path, dtype, copy, raise_cast_failure)
    714             # create an extension array from its dtype
    715             array_type = dtype.construct_array_type()._from_sequence
--> 716             subarr = array_type(arr, dtype=dtype, copy=copy)
    717         elif dtype is not None and raise_cast_failure:
    718             raise

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/arrays/integer.py in _from_sequence(cls, scalars, dtype, copy)
    301     @classmethod
    302     def _from_sequence(cls, scalars, dtype=None, copy=False):
--> 303         return integer_array(scalars, dtype=dtype, copy=copy)
    304
    305     @classmethod

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/arrays/integer.py in integer_array(values, dtype, copy)
    109     TypeError if incompatible types
    110     """
--> 111     values, mask = coerce_to_array(values, dtype=dtype, copy=copy)
    112     return IntegerArray(values, mask)
    113

/usr/local/anaconda3/lib/python3.7/site-packages/pandas/core/arrays/integer.py in coerce_to_array(values, dtype, mask, copy)
    190     elif not (is_integer_dtype(values) or is_float_dtype(values)):
    191         raise TypeError("{} cannot be converted to an IntegerDtype".format(
--> 192             values.dtype))
    193
    194     if mask is None:

TypeError: bool cannot be converted to an IntegerDtype
```
#### Problem description

Pandas is unable to convert array of bools to IntegerDtype, while conversion to int is supported. What's interesting, if an arrays contains NaNs, then conversion goes as expected.
```python
In [4]: pd.Series([True, False, np.nan], dtype=pd.Int64Dtype())
Out[4]:
0      1
1      0
2    NaN
dtype: Int64
```
#### Expected Output
```python
In [5]: pd.Series([True, False], dtype=pd.Int64Dtype())
Out[4]:
0      1
1      0
dtype: Int64
```
#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None

pandas: 0.24.1
pytest: 4.2.0
pip: 19.0.1
setuptools: 40.7.3
Cython: 0.29.4
numpy: 1.15.4
scipy: 1.2.0
pyarrow: None
xarray: None
IPython: 7.2.0
sphinx: 1.8.4
patsy: None
dateutil: 2.7.5
pytz: 2018.9
blosc: None
bottleneck: 1.2.1
tables: None
numexpr: None
feather: None
matplotlib: 3.0.2
openpyxl: 2.5.14
xlrd: 1.2.0
xlwt: 1.3.0
xlsxwriter: 1.1.2
lxml.etree: 4.3.0
bs4: 4.7.1
html5lib: 1.0.1
sqlalchemy: 1.2.17
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
gcsfs: None
</details>



Discussion:
Thanks for the report! That does seem strange that NA is required here. 

Investigation and PRs are always welcome
I've found the source of the discrepancy between the arrays with and without NA, it's in `pandas.core.arrays.integer.coerce_to_array`:
https://github.com/pandas-dev/pandas/blob/0eddba88332e9b7f41b838e503c155f7ebe4d9a1/pandas/core/arrays/integer.py#L179
https://github.com/pandas-dev/pandas/blob/0eddba88332e9b7f41b838e503c155f7ebe4d9a1/pandas/core/arrays/integer.py#L190-L192
When `np.array` is called on an array with NAs it casts the arrays to float:
```python
In [1]: import numpy as np

In [2]: np.array([False, True]).dtype
Out[2]: dtype('bool')

In [3]: np.array([False, True, np.nan]).dtype
Out[3]: dtype('float64')
```
Though I'm not sure how to handle this properly, should we cast boolean arrays to numpy floats first?
> should we cast boolean arrays to numpy floats first?

I would prefer casting to `int` actually.  This approach seems a little special-casey, but trying to modify EA logic is by no means straightforward and has its own landmines.  Give this a shot!
Thanks for the report! That does seem strange that NA is required here. 

Investigation and PRs are always welcome
I've found the source of the discrepancy between the arrays with and without NA, it's in `pandas.core.arrays.integer.coerce_to_array`:
https://github.com/pandas-dev/pandas/blob/0eddba88332e9b7f41b838e503c155f7ebe4d9a1/pandas/core/arrays/integer.py#L179
https://github.com/pandas-dev/pandas/blob/0eddba88332e9b7f41b838e503c155f7ebe4d9a1/pandas/core/arrays/integer.py#L190-L192
When `np.array` is called on an array with NAs it casts the arrays to float:
```python
In [1]: import numpy as np

In [2]: np.array([False, True]).dtype
Out[2]: dtype('bool')

In [3]: np.array([False, True, np.nan]).dtype
Out[3]: dtype('float64')
```
Though I'm not sure how to handle this properly, should we cast boolean arrays to numpy floats first?
> should we cast boolean arrays to numpy floats first?

I would prefer casting to `int` actually.  This approach seems a little special-casey, but trying to modify EA logic is by no means straightforward and has its own landmines.  Give this a shot!

Resolving patch:
diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py
--- a/pandas/core/arrays/integer.py
+++ b/pandas/core/arrays/integer.py
@@ -188,6 +188,9 @@ def coerce_to_array(values, dtype, mask=None, copy=False):
             raise TypeError("{} cannot be converted to an IntegerDtype".format(
                 values.dtype))
 
+    elif is_bool_dtype(values) and is_integer_dtype(dtype):
+        values = np.array(values, dtype=int, copy=copy)
+
     elif not (is_integer_dtype(values) or is_float_dtype(values)):
         raise TypeError("{} cannot be converted to an IntegerDtype".format(
             values.dtype))
diff --git a/pandas/core/series.py b/pandas/core/series.py
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -2670,7 +2670,6 @@ def combine(self, other, func, fill_value=None):
                 # do in _values_from_sequence. We still want ops to work
                 # though, so we catch any regular Exception.
                 pass
-
         return self._constructor(new_values, index=new_index, name=new_name)
 
     def combine_first(self, other):


Test patch:
diff --git a/pandas/tests/arrays/test_integer.py b/pandas/tests/arrays/test_integer.py
--- a/pandas/tests/arrays/test_integer.py
+++ b/pandas/tests/arrays/test_integer.py
@@ -613,6 +613,19 @@ def test_to_integer_array_float():
     assert result.dtype == Int64Dtype()
 
 
+@pytest.mark.parametrize(
+    'bool_values, int_values, target_dtype, expected_dtype',
+    [([False, True], [0, 1], Int64Dtype(), Int64Dtype()),
+     ([False, True], [0, 1], 'Int64', Int64Dtype()),
+     ([False, True, np.nan], [0, 1, np.nan], Int64Dtype(), Int64Dtype())])
+def test_to_integer_array_bool(bool_values, int_values, target_dtype,
+                               expected_dtype):
+    result = integer_array(bool_values, dtype=target_dtype)
+    assert result.dtype == expected_dtype
+    expected = integer_array(int_values, dtype=target_dtype)
+    tm.assert_extension_array_equal(result, expected)
+
+
 @pytest.mark.parametrize(
     'values, to_dtype, result_dtype',
     [