Repository: pandas-dev/pandas

Issue:
ValueError in Categorical Constructor with empty data and boolean categories
This works,

```python
In [15]: pd.Categorical([], categories=['a', 'b'])
Out[15]: [], Categories (2, object): [a, b]
```

This doesn't

```pytb
In [16]: pd.Categorical([], categories=[True, False])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-16-8e79cd310199> in <module>()
----> 1 pd.Categorical([], categories=[True, False])

~/sandbox/pandas/pandas/core/arrays/categorical.py in __init__(self, values, categories, ordered, dtype, fastpath)
    426
    427         else:
--> 428             codes = _get_codes_for_values(values, dtype.categories)
    429
    430         if null_mask.any():

~/sandbox/pandas/pandas/core/arrays/categorical.py in _get_codes_for_values(values, categories)
   2449     (_, _), cats = _get_data_algo(categories, _hashtables)
   2450     t = hash_klass(len(cats))
-> 2451     t.map_locations(cats)
   2452     return coerce_indexer_dtype(t.lookup(vals), cats)
   2453

~/sandbox/pandas/pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.map_locations()
   1330             raise KeyError(key)
   1331
-> 1332     def map_locations(self, ndarray[object] values):
   1333         cdef:
   1334             Py_ssize_t i, n = len(values)

ValueError: Buffer dtype mismatch, expected 'Python object' but got 'unsigned long'
```

the `values` there is `array([], dtype=object)`. It should be int dtype by this point.


Discussion:
I haven't bisected on this to check exactly when it first started manifesting, but this issue is not present in version `0.20.3`, but it is present in version `0.21.0`.
Turns out the first bad commit is [7818486859d1aba53ce359b93cfc772e688958e5](https://github.com/pandas-dev/pandas/commit/7818486859d1aba53ce359b93cfc772e688958e5).
Ok, so the problem is that in [this function](https://github.com/pandas-dev/pandas/blob/master/pandas/core/arrays/categorical.py#L2444), you have:

```python
    if not is_dtype_equal(values.dtype, categories.dtype):
        values = ensure_object(values)
        categories = ensure_object(categories)

    (hash_klass, vec_klass), vals = _get_data_algo(values, _hashtables)
    (_, _), cats = _get_data_algo(categories, _hashtables)
```

For boolean categories, the category's dtype is already `object` I guess, so the `if not is_dtype_equal(values.dtype, categories.dtype)` check is `True` and the no coercion to object takes place, but inside `_get_data_algo`, there is a call to [`_ensure_data`](https://github.com/pandas-dev/pandas/blob/master/pandas/core/algorithms.py#L48), which detects that `categories` is all booleans and converts it to `uint64`, which invalidates the assumption that `values.dtype` and `categories.dtype` are the same.

You would think that this would be a problem for `pd.CategoricalIndex([], categories=[1, 2])` as well, since integers should *also* be coerced, but evidently `_ensure_data` treats `Index` and `ndarray` differently, and `_ensure_object` gives an `ndarray` while `categories` is an `Index`. PR incoming.

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
@@ -2439,9 +2439,13 @@ def _get_codes_for_values(values, categories):
     """
     utility routine to turn values into codes given the specified categories
     """
-
     from pandas.core.algorithms import _get_data_algo, _hashtables
-    if not is_dtype_equal(values.dtype, categories.dtype):
+    if is_dtype_equal(values.dtype, categories.dtype):
+        # To prevent erroneous dtype coercion in _get_data_algo, retrieve
+        # the underlying numpy array. gh-22702
+        values = getattr(values, 'values', values)
+        categories = getattr(categories, 'values', categories)
+    else:
         values = ensure_object(values)
         categories = ensure_object(categories)
 


Test patch:
diff --git a/pandas/tests/arrays/categorical/test_constructors.py b/pandas/tests/arrays/categorical/test_constructors.py
--- a/pandas/tests/arrays/categorical/test_constructors.py
+++ b/pandas/tests/arrays/categorical/test_constructors.py
@@ -42,6 +42,12 @@ def test_constructor_empty(self):
         expected = pd.Int64Index([1, 2, 3])
         tm.assert_index_equal(c.categories, expected)
 
+    def test_constructor_empty_boolean(self):
+        # see gh-22702
+        cat = pd.Categorical([], categories=[True, False])
+        categories = sorted(cat.categories.tolist())
+        assert categories == [False, True]
+
     def test_constructor_tuples(self):
         values = np.array([(1,), (1, 2), (1,), (1, 2)], dtype=object)
         result = Categorical(values)
diff --git a/pandas/tests/indexes/test_category.py b/pandas/tests/indexes/test_category.py
--- a/pandas/tests/indexes/test_category.py
+++ b/pandas/tests/indexes/test_category.py
@@ -136,6 +136,12 @@ def test_construction_with_dtype(self):
         result = CategoricalIndex(idx, categories=idx, ordered=True)
         tm.assert_index_equal(result, expected, exact=True)
 
+    def test_construction_empty_with_bool_categories(self):
+        # see gh-22702
+        cat = pd.CategoricalIndex([], categories=[True, False])
+        categories = sorted(cat.categories.tolist())
+        assert categories == [False, True]
+
     def test_construction_with_categorical_dtype(self):
         # construction with CategoricalDtype
         # GH18109