Repository: pandas-dev/pandas
Pull request: BUG: Avoid casting Int to object in Categorical.from_codes
- [x] closes #31779
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Problem statement:
Categorical.from_codes fails for the (new nullable) Int64 dtype
#### Code Sample, a copy-pastable example if possible

```python
>>> import pandas as pd
>>> codes = pd.Series([1, 0], dtype="Int64")
>>> pd.Categorical.from_codes(codes, categories=["foo", "bar"])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../lib/python3.7/site-packages/pandas/core/arrays/categorical.py", line 649, in from_codes
    raise ValueError("codes need to be array-like integers")
ValueError: codes need to be array-like integers
```
#### Problem description

`Categories.from_codes` works with Series with the Numpy `"int64"` dtype. 

```python
>>> codes = pd.Series([1, 0])
>>> pd.Categorical.from_codes(codes, categories=["foo", "bar"])
[bar, foo]
Categories (2, object): [foo, bar]
```

I would expect that it will work with the new Pandas `"Int64"` dtype.

#### Expected Output

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit           : None
python           : 3.7.3.final.0
python-bits      : 64
OS               : Darwin
OS-release       : 18.7.0
machine          : x86_64
processor        : i386
byteorder        : little
LC_ALL           : None
LANG             : en_US.UTF-8
LOCALE           : en_US.UTF-8

pandas           : 1.0.1
numpy            : 1.18.1
pytz             : 2019.3
dateutil         : 2.8.1
pip              : 20.0.2
setuptools       : 45.1.0
Cython           : None
pytest           : None
hypothesis       : None
sphinx           : None
blosc            : None
feather          : None
xlsxwriter       : None
lxml.etree       : None
html5lib         : None
pymysql          : None
psycopg2         : None
jinja2           : None
IPython          : None
pandas_datareader: None
bs4              : None
bottleneck       : None
fastparquet      : None
gcsfs            : None
lxml.etree       : None
matplotlib       : None
numexpr          : None
odfpy            : None
openpyxl         : None
pandas_gbq       : None
pyarrow          : None
pytables         : None
pytest           : None
pyxlsb           : None
s3fs             : None
scipy            : None
sqlalchemy       : None
tables           : None
tabulate         : None
xarray           : None
xlrd             : None
xlwt             : None
xlsxwriter       : None
numba            : None

</details>

Hints from issue discussion:

Commit message: Merge branch 'master' into cat-from-int-codes

Patch to review:
diff --git a/doc/source/whatsnew/v1.0.2.rst b/doc/source/whatsnew/v1.0.2.rst
index 94dc1e0c007ca..25de7330f9f8b 100644
--- a/doc/source/whatsnew/v1.0.2.rst
+++ b/doc/source/whatsnew/v1.0.2.rst
@@ -25,7 +25,7 @@ Fixed regressions
 Bug fixes
 ~~~~~~~~~
 
--
+- Fixed bug where :meth:`Categorical.from_codes` improperly raised a ``ValueError`` when passed nullable integer codes. (:issue:`31779`)
 -
 
 .. ---------------------------------------------------------------------------
diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py
index d26ff7490e714..5b06b4b7f08bf 100644
--- a/pandas/core/arrays/categorical.py
+++ b/pandas/core/arrays/categorical.py
@@ -644,7 +644,11 @@ def from_codes(cls, codes, categories=None, ordered=None, dtype=None):
             )
             raise ValueError(msg)
 
-        codes = np.asarray(codes)  # #21767
+        if is_extension_array_dtype(codes) and is_integer_dtype(codes):
+            # Avoid the implicit conversion of Int to object
+            codes = codes.to_numpy(dtype=np.int64, na_value=np.nan)
+        else:
+            codes = np.asarray(codes)  # #21767
         if len(codes) and not is_integer_dtype(codes):
             raise ValueError("codes need to be array-like integers")
 
diff --git a/pandas/tests/arrays/categorical/test_constructors.py b/pandas/tests/arrays/categorical/test_constructors.py
index 70e1421c8dcf4..1d82d368d9a24 100644
--- a/pandas/tests/arrays/categorical/test_constructors.py
+++ b/pandas/tests/arrays/categorical/test_constructors.py
@@ -560,6 +560,15 @@ def test_from_codes_neither(self):
         with pytest.raises(ValueError, match=msg):
             Categorical.from_codes([0, 1])
 
+    def test_from_codes_with_nullable_int(self):
+        codes = pd.array([0, 1], dtype="Int64")
+        categories = ["a", "b"]
+
+        result = Categorical.from_codes(codes, categories=categories)
+        expected = Categorical.from_codes(codes.astype(int), categories=categories)
+
+        tm.assert_categorical_equal(result, expected)
+
     @pytest.mark.parametrize("dtype", [None, "category"])
     def test_from_inferred_categories(self, dtype):
         cats = ["a", "b"]


Code review comments:
diff_hunk: @@ -560,6 +560,15 @@ def test_from_codes_neither(self):
         with pytest.raises(ValueError, match=msg):
             Categorical.from_codes([0, 1])
 
+    def test_from_codes_with_nullable_int(self):
+        codes = pd.array([0, 1], dtype="Int64")
+        categories = ["a", "b"]
+
+        result = Categorical.from_codes(codes, categories=categories)
+        expected = Categorical.from_codes(codes.astype(int), categories=categories)
path: pandas/tests/arrays/categorical/test_constructors.py
text: @user1:
```suggestion
        expected = Categorical.from_codes(codes.to_numpy(int), categories=categories)
```

(in case in the near future astype will return an extension array)

diff_hunk: @@ -644,7 +644,11 @@ def from_codes(cls, codes, categories=None, ordered=None, dtype=None):
             )
             raise ValueError(msg)
 
-        codes = np.asarray(codes)  # #21767
+        if is_extension_array_dtype(codes) and is_integer_dtype(codes):
+            # Avoid the implicit conversion of Int to object
+            codes = codes.to_numpy(dtype=np.int64, na_value=np.nan)
path: pandas/core/arrays/categorical.py
text: @user1:
Codes shouldn't contain missing values (and the `na_value=np.nan` will not work anyway for a int64 dtype)