Repository: pandas-dev/pandas
Pull request: BUG: Categorical[dt64tz].to_numpy() losing tz
- [x] closes #26406
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Problem statement:
BUG: to_numpy on categorical data with tz aware datetime categories returns datetime64
```
In [1]: s = pd.Series(pd.Categorical(pd.date_range("2012", periods=3, tz='Europe/Brussels')))

In [2]: s.to_numpy() 
Out[2]: 
array(['2011-12-31T23:00:00.000000000', '2012-01-01T23:00:00.000000000',
       '2012-01-02T23:00:00.000000000'], dtype='datetime64[ns]')

In [3]: s = pd.Series(pd.date_range("2012", periods=3, tz='Europe/Brussels'))

In [4]: s.to_numpy() 
Out[4]: 
array([Timestamp('2012-01-01 00:00:00+0100', tz='Europe/Brussels', freq='D'),
       Timestamp('2012-01-02 00:00:00+0100', tz='Europe/Brussels', freq='D'),
       Timestamp('2012-01-03 00:00:00+0100', tz='Europe/Brussels', freq='D')],
      dtype=object)
```

We opted for the default behaviour of `to_numpy` to be as much preserving the values, so for normal datetimetz series, this is object dtype. So probably we should do the same for Categorical?

@TomAugspurger since `to_numpy` is still new, I suppose we can see this is as a bug fix and not change in behaviour (to not go through deprecation cycle)?

Hints from issue discussion:
> we can see this is as a bug fix

Agreed. Tagging for 0.25.0 though, so it doesn't sit for too long.

Commit message: BUG: Categorical[dt64tz].to_numpy() losing tz

Patch to review:
diff --git a/pandas/conftest.py b/pandas/conftest.py
index a0ec6f96042fc..c368cdb07d834 100644
--- a/pandas/conftest.py
+++ b/pandas/conftest.py
@@ -321,6 +321,16 @@ def index_or_series(request):
 index_or_series2 = index_or_series
 
 
+@pytest.fixture(
+    params=[pd.Index, pd.Series, pd.array], ids=["index", "series", "array"]
+)
+def index_or_series_or_array(request):
+    """
+    Fixture to parametrize over Index, Series, and ExtensionArray
+    """
+    return request.param
+
+
 @pytest.fixture
 def dict_subclass():
     """
diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py
index fe66aae23f510..666b16f974971 100644
--- a/pandas/core/arrays/categorical.py
+++ b/pandas/core/arrays/categorical.py
@@ -1269,15 +1269,13 @@ def __array__(self, dtype=None) -> np.ndarray:
             if dtype==None (default), the same dtype as
             categorical.categories.dtype.
         """
-        ret = take_1d(self.categories.values, self._codes)
+        ret = take_1d(self.categories._values, self._codes)
         if dtype and not is_dtype_equal(dtype, self.categories.dtype):
             return np.asarray(ret, dtype)
-        if is_extension_array_dtype(ret):
-            # When we're a Categorical[ExtensionArray], like Interval,
-            # we need to ensure __array__ get's all the way to an
-            # ndarray.
-            ret = np.asarray(ret)
-        return ret
+        # When we're a Categorical[ExtensionArray], like Interval,
+        # we need to ensure __array__ get's all the way to an
+        # ndarray.
+        return np.asarray(ret)
 
     def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
         # for binary ops, use our custom dunder methods
diff --git a/pandas/tests/base/test_conversion.py b/pandas/tests/base/test_conversion.py
index a6fdb82e48197..668954a3f4a0b 100644
--- a/pandas/tests/base/test_conversion.py
+++ b/pandas/tests/base/test_conversion.py
@@ -316,18 +316,34 @@ def test_array_multiindex_raises():
             TimedeltaArray(np.array([0, 3600000000000], dtype="i8"), freq="H"),
             np.array([0, 3600000000000], dtype="m8[ns]"),
         ),
+        # GH#26406 tz is preserved in Categorical[dt64tz]
+        (
+            pd.Categorical(pd.date_range("2016-01-01", periods=2, tz="US/Pacific")),
+            np.array(
+                [
+                    Timestamp("2016-01-01", tz="US/Pacific"),
+                    Timestamp("2016-01-02", tz="US/Pacific"),
+                ]
+            ),
+        ),
     ],
 )
-def test_to_numpy(array, expected, index_or_series):
-    box = index_or_series
+def test_to_numpy(array, expected, index_or_series_or_array):
+    box = index_or_series_or_array
     thing = box(array)
 
     if array.dtype.name in ("Int64", "Sparse[int64, 0]") and box is pd.Index:
         pytest.skip(f"No index type for {array.dtype}")
 
+    if array.dtype.name == "int64" and box is pd.array:
+        pytest.xfail("thing is Int64 and to_numpy() returns object")
+
     result = thing.to_numpy()
     tm.assert_numpy_array_equal(result, expected)
 
+    result = np.asarray(thing)
+    tm.assert_numpy_array_equal(result, expected)
+
 
 @pytest.mark.parametrize("as_series", [True, False])
 @pytest.mark.parametrize(


Code review comments:
diff_hunk: @@ -1269,15 +1269,13 @@ def __array__(self, dtype=None) -> np.ndarray:
             if dtype==None (default), the same dtype as
             categorical.categories.dtype.
         """
-        ret = take_1d(self.categories.values, self._codes)
+        ret = take_1d(self.categories._values, self._codes)
         if dtype and not is_dtype_equal(dtype, self.categories.dtype):
             return np.asarray(ret, dtype)
-        if is_extension_array_dtype(ret):
-            # When we're a Categorical[ExtensionArray], like Interval,
-            # we need to ensure __array__ get's all the way to an
-            # ndarray.
-            ret = np.asarray(ret)
-        return ret
+        # When we're a Categorical[ExtensionArray], like Interval,
+        # we need to ensure __array__ get's all the way to an
path: pandas/core/arrays/categorical.py
text: @user1:
```suggestion
        # we need to ensure __array__ gets all the way to an
```