Repository: pandas-dev/pandas

Issue:
Converting from categorical to int ignores NaNs
#### Code Sample, a copy-pastable example if possible
```python
In [6]: s = pd.Series([1, 0, None], dtype='category')                                                                                                                                                                                            

In [7]: s                                                                                                                                                                                                                                      
Out[7]: 
0      1
1      0
2    NaN
dtype: category
Categories (2, int64): [0, 1]

In [8]: s.astype(int)                                                                                                                                                                                                                          
Out[8]: 
0                      1
1                      0
2   -9223372036854775808  # <- this is unexpected
dtype: int64
```
#### Problem description
When converting categorical series back into Int column, it converts `NaN` to incorect integer negative value.

#### Expected Output
I would expect that `NaN` in category converts to `NaN` in `IntX`(nullable integer) or `float`.

When trying to use `d.astype('Int8')`, I get an error `dtype not understood`

#### Output of ``pd.show_versions()``
```
In [147]: pd.show_versions()                                                                                                                                                                                                                   

INSTALLED VERSIONS
------------------
commit           : None
python           : 3.7.4.final.0
python-bits      : 64
OS               : Linux
OS-release       : 5.2.13-arch1-1-ARCH
machine          : x86_64
processor        : 
byteorder        : little
LC_ALL           : None
LANG             : en_US.UTF-8
LOCALE           : en_US.UTF-8

pandas           : 0.25.1
numpy            : 1.17.2
pytz             : 2019.2
dateutil         : 2.8.0
pip              : 19.2.3
setuptools       : 41.2.0
Cython           : None
pytest           : 5.1.2
hypothesis       : None
sphinx           : None
blosc            : None
feather          : 0.4.0
xlsxwriter       : None
lxml.etree       : None
html5lib         : None
pymysql          : None
psycopg2         : None
jinja2           : None
IPython          : 7.8.0
pandas_datareader: None
bs4              : None
bottleneck       : None
fastparquet      : None
gcsfs            : None
lxml.etree       : None
matplotlib       : None
numexpr          : 2.7.0
odfpy            : None
openpyxl         : None
pandas_gbq       : None
pyarrow          : 0.14.1
pytables         : None
s3fs             : None
scipy            : None
sqlalchemy       : None
tables           : 3.5.2
xarray           : None
xlrd             : None
xlwt             : None
xlsxwriter       : None
```



Discussion:
I would expect `s.atype(int)` to raise, the same as `s.astype(float).astype(int)`

To get a nullable integer, `s.astype("Int8")` should work, but let's leave that as a second issue (we may already have one for it, not sure).
> I would expect `s.atype(int)` to raise, the same as `s.astype(float).astype(int)`
> 
> To get a nullable integer, `s.astype("Int8")` should work, but let's leave that as a second issue (we may already have one for it, not sure).

It seems like the weird value gets introduced here: https://github.com/pandas-dev/pandas/blob/master/pandas/core/arrays/categorical.py#L523 . Would this be a `numpy` issue?

```
In [1]: import numpy as np                                                      

In [2]: arr = np.array([1.0, 0.0, np.nan])                                      

In [3]: np.array(arr, dtype=np.int64)                                            
Out[3]: array([                   1,                    0, -9223372036854775808])
```
> I would expect s.atype(int) to raise, the same as s.astype(float).astype(int)

Well, but that's a bit sad because it means that once you convert to categoricals, you cannot get back :-( .
> It seems like the weird value gets introduced here: https://github.com/pandas-dev/pandas/blob/master/pandas/core/arrays/categorical.py#L523 . Would this be a numpy issue?

Thanks for tracking it down. I think that's just NumPy's defined behavior.

We might want `astype_nansafe` there (should also test with `inf`).

>  Well, but that's a bit sad because it means that once you convert to categoricals, you cannot get back :-( .

What do you mean?
> What do you mean?

Well, I meant something like if you did `s = pd.Series([1, None]).astype(int).astype(category)` and now wanted to get back to `int` somehow. The workaround seems to be `s.astype('float').astype('Int8')` for example.

I understand it's tricky, especially when dealing with nullable integers. It's true that raising is probably better than trying to be somehow clever.
Sorry, I'm still not understanding. In your example,

```python
s = pd.Series([1, None]).astype(int)
```

raises. The conversion from float to int is what raises, not to or from categorical.
Sorry, I wrote it wrongly. I meant if you do:
```
In [3]: s = pd.Series([1, None], dtype='Int8').astype('category')
```
and now wanted to get back to numerics, or more specifically e.g. `Int8`. You have to do
```
In [6]: s.astype(float).astype('Int8')
```
while it would be super cool to do just `s.astype('Int8')`
> s.astype('Int8')

That should work just fine. As I said earlier

> To get a nullable integer, s.astype("Int8") should work, but let's leave that as a second issue (we may already have one for it, not sure).



Resolving patch:
diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py
--- a/pandas/core/arrays/categorical.py
+++ b/pandas/core/arrays/categorical.py
@@ -520,6 +520,9 @@ def astype(self, dtype: Dtype, copy: bool = True) -> ArrayLike:
             if dtype == self.dtype:
                 return self
             return self._set_dtype(dtype)
+        if is_integer_dtype(dtype) and self.isna().any():
+            msg = "Cannot convert float NaN to integer"
+            raise ValueError(msg)
         return np.array(self, dtype=dtype, copy=copy)
 
     @cache_readonly
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -4713,13 +4713,13 @@ def set_value(self, arr, key, value):
     @Appender(_index_shared_docs["get_indexer_non_unique"] % _index_doc_kwargs)
     def get_indexer_non_unique(self, target):
         target = ensure_index(target)
-        if is_categorical(target):
-            target = target.astype(target.dtype.categories.dtype)
         pself, ptarget = self._maybe_promote(target)
         if pself is not self or ptarget is not target:
             return pself.get_indexer_non_unique(ptarget)
 
-        if self.is_all_dates:
+        if is_categorical(target):
+            tgt_values = np.asarray(target)
+        elif self.is_all_dates:
             tgt_values = target.asi8
         else:
             tgt_values = target._ndarray_values
@@ -4731,7 +4731,7 @@ def get_indexer_for(self, target, **kwargs):
         """
         Guaranteed return of an indexer even when non-unique.
 
-        This dispatches to get_indexer or get_indexer_nonunique
+        This dispatches to get_indexer or get_indexer_non_unique
         as appropriate.
 
         Returns


Test patch:
diff --git a/pandas/tests/extension/test_categorical.py b/pandas/tests/extension/test_categorical.py
--- a/pandas/tests/extension/test_categorical.py
+++ b/pandas/tests/extension/test_categorical.py
@@ -19,7 +19,7 @@
 import pytest
 
 import pandas as pd
-from pandas import Categorical
+from pandas import Categorical, CategoricalIndex, Timestamp
 from pandas.api.types import CategoricalDtype
 from pandas.tests.extension import base
 import pandas.util.testing as tm
@@ -197,7 +197,15 @@ def test_searchsorted(self, data_for_sorting):
 
 
 class TestCasting(base.BaseCastingTests):
-    pass
+    @pytest.mark.parametrize("cls", [Categorical, CategoricalIndex])
+    @pytest.mark.parametrize("values", [[1, np.nan], [Timestamp("2000"), pd.NaT]])
+    def test_cast_nan_to_int(self, cls, values):
+        # GH 28406
+        s = cls(values)
+
+        msg = "Cannot (cast|convert)"
+        with pytest.raises((ValueError, TypeError), match=msg):
+            s.astype(int)
 
 
 class TestArithmeticOps(base.BaseArithmeticOpsTests):