Repository: pandas-dev/pandas

Issue:
BUG: method .nunique on categorical series in v0.21 with only NaNs gives ValueError
#### Code Sample, a copy-pastable example if possible

```python
>>> ser = pd.Series(pd.Categorical([np.nan]))
>>> ser.nunique()
ValueError: buffer source array is read-only
```
#### Problem description

The above code gave 0 in v20.3 and is expected to give 0 also in v0.21. The problem is independent of if I set some categories.

**EDIT**: Actually this doesn't give error if I set categories. so this only happens if no categories are set. The use case for no categories in my case is programmatically reading in data, where some columns are empty and of dtype categorical.

#### Expected Output

0 (zero)

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: 81372093f1fdc0c07e4b45ba0f47b0360fabd405
python: 3.5.4.final.0
python-bits: 32
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.21.0
pytest: 3.2.3
pip: 9.0.1
setuptools: 36.5.0.post20170922
Cython: None
numpy: 1.13.3
scipy: None
pyarrow: None
xarray: None
IPython: 6.1.0
sphinx: None
patsy: None
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: None
openpyxl: 2.4.8
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None


</details>

BUG: method .nunique on categorical series in v0.21 with only NaNs gives ValueError
#### Code Sample, a copy-pastable example if possible

```python
>>> ser = pd.Series(pd.Categorical([np.nan]))
>>> ser.nunique()
ValueError: buffer source array is read-only
```
#### Problem description

The above code gave 0 in v20.3 and is expected to give 0 also in v0.21. The problem is independent of if I set some categories.

**EDIT**: Actually this doesn't give error if I set categories. so this only happens if no categories are set. The use case for no categories in my case is programmatically reading in data, where some columns are empty and of dtype categorical.

#### Expected Output

0 (zero)

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: 81372093f1fdc0c07e4b45ba0f47b0360fabd405
python: 3.5.4.final.0
python-bits: 32
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.21.0
pytest: 3.2.3
pip: 9.0.1
setuptools: 36.5.0.post20170922
Cython: None
numpy: 1.13.3
scipy: None
pyarrow: None
xarray: None
IPython: 6.1.0
sphinx: None
patsy: None
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: None
openpyxl: 2.4.8
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None


</details>



Discussion:
this is a variation of https://github.com/pandas-dev/pandas/issues/10043, fixed by #10070 
and duplicated by #17192, though this is example is simple, so should fix.

PR's welcome!
This individual issue can be fixed with a simple ``ìf not len(self.cat.categories): return 0`` but that feels like bypassing the issue in the ``.unique`` method. Is that ok is something more involved required?

If this is something in cython or requires larger refactoring, this will be beyond my ability, I'm sorry.
no, this should be fixed in cython.
Tthis is a regression from v0.20.3. Maybe this has something to do with the new CategoricalDtype, @TomAugspurger ?
I think it's more likely to be the changes to `take_nd`, but I may be wrong.
@topper-123  @TomAugspurger @jreback please review  #18279 
this is a variation of https://github.com/pandas-dev/pandas/issues/10043, fixed by #10070 
and duplicated by #17192, though this is example is simple, so should fix.

PR's welcome!
This individual issue can be fixed with a simple ``ìf not len(self.cat.categories): return 0`` but that feels like bypassing the issue in the ``.unique`` method. Is that ok is something more involved required?

If this is something in cython or requires larger refactoring, this will be beyond my ability, I'm sorry.
no, this should be fixed in cython.
Tthis is a regression from v0.20.3. Maybe this has something to do with the new CategoricalDtype, @TomAugspurger ?
I think it's more likely to be the changes to `take_nd`, but I may be wrong.
@topper-123  @TomAugspurger @jreback please review  #18279 

Resolving patch:
diff --git a/pandas/core/categorical.py b/pandas/core/categorical.py
--- a/pandas/core/categorical.py
+++ b/pandas/core/categorical.py
@@ -2276,7 +2276,7 @@ def _recode_for_categories(codes, old_categories, new_categories):
 
     if len(old_categories) == 0:
         # All null anyway, so just retain the nulls
-        return codes
+        return codes.copy()
     indexer = coerce_indexer_dtype(new_categories.get_indexer(old_categories),
                                    new_categories)
     new_codes = take_1d(indexer, codes.copy(), fill_value=-1)


Test patch:
diff --git a/pandas/tests/series/test_analytics.py b/pandas/tests/series/test_analytics.py
--- a/pandas/tests/series/test_analytics.py
+++ b/pandas/tests/series/test_analytics.py
@@ -848,6 +848,12 @@ def test_value_counts_nunique(self):
         result = series.nunique()
         assert result == 11
 
+        # GH 18051
+        s = pd.Series(pd.Categorical([]))
+        assert s.nunique() == 0
+        s = pd.Series(pd.Categorical([np.nan]))
+        assert s.nunique() == 0
+
     def test_unique(self):
 
         # 714 also, dtype=float
@@ -873,6 +879,14 @@ def test_unique(self):
         expected = np.array([1, 2, 3, None], dtype=object)
         tm.assert_numpy_array_equal(result, expected)
 
+        # GH 18051
+        s = pd.Series(pd.Categorical([]))
+        tm.assert_categorical_equal(s.unique(), pd.Categorical([]),
+                                    check_dtype=False)
+        s = pd.Series(pd.Categorical([np.nan]))
+        tm.assert_categorical_equal(s.unique(), pd.Categorical([np.nan]),
+                                    check_dtype=False)
+
     @pytest.mark.parametrize(
         "tc1, tc2",
         [