Repository: pandas-dev/pandas

Issue:
Concat Series[datetimetz] & Series[category] loses timezone
```python

In [8]: a = pd.Series(pd.date_range('20170101', periods=4, tz='US/Pacific'))

In [9]: b = pd.Series(['a', 'b'], dtype='category')

In [10]: pd.concat([a, b], ignore_index=True)[0]
Out[10]: Timestamp('2017-01-01 08:00:00')

```

Out[10]  should have a tz like the following

```python
In [11]: pd.concat([a, b.astype(object)], ignore_index=True)[0]
Out[11]: Timestamp('2017-01-01 00:00:00-0800', tz='US/Pacific', freq='D')
```


Discussion:
Hi,

Can my team and I contribute to this issue? 

Thanks. 
Sure, you can give it a shot.

I'm not sure where the best place is for the fix yet. Somewhere in `core/dtypes/concat.py`, but it'll be a bit tricky to find the best time to convert to objects.
That's the offset from the timezone ("US/Pacific").

Looking more closely, I think the issue is at https://github.com/pandas-dev/pandas/blob/dc8d35aa53d496b651f5e1ab4cb2604e9f7236c7/pandas/core/dtypes/concat.py#L194.

We call `np.asarray(x)` on non-categorical, like our datetimes. I wonder if we could rewrite that as


```python
to_concat = [np.asarray(x.astype(object)) for x in to_concat]
```

We know we need object dtype anyway, so there's no avoiding that. We need the `asarray(...)` for things like `SparseArray`, since `SparseArray.astype(object)` is still sparse.

Maybe give that change a shot and see if the tests still pass.
Okay I'll take a look at the suggested change. Thanks! 

Resolving patch:
diff --git a/pandas/core/dtypes/concat.py b/pandas/core/dtypes/concat.py
--- a/pandas/core/dtypes/concat.py
+++ b/pandas/core/dtypes/concat.py
@@ -191,15 +191,6 @@ def _concat_categorical(to_concat, axis=0):
         A single array, preserving the combined dtypes
     """
 
-    def _concat_asobject(to_concat):
-        to_concat = [x.get_values() if is_categorical_dtype(x.dtype)
-                     else np.asarray(x).ravel() for x in to_concat]
-        res = _concat_compat(to_concat)
-        if axis == 1:
-            return res.reshape(1, len(res))
-        else:
-            return res
-
     # we could have object blocks and categoricals here
     # if we only have a single categoricals then combine everything
     # else its a non-compat categorical
@@ -214,7 +205,14 @@ def _concat_asobject(to_concat):
         if all(first.is_dtype_equal(other) for other in to_concat[1:]):
             return union_categoricals(categoricals)
 
-    return _concat_asobject(to_concat)
+    # extract the categoricals & coerce to object if needed
+    to_concat = [x.get_values() if is_categorical_dtype(x.dtype)
+                 else np.asarray(x).ravel() if not is_datetime64tz_dtype(x)
+                 else np.asarray(x.astype(object)) for x in to_concat]
+    result = _concat_compat(to_concat)
+    if axis == 1:
+        result = result.reshape(1, len(result))
+    return result
 
 
 def union_categoricals(to_union, sort_categories=False, ignore_order=False):


Test patch:
diff --git a/pandas/tests/reshape/test_concat.py b/pandas/tests/reshape/test_concat.py
--- a/pandas/tests/reshape/test_concat.py
+++ b/pandas/tests/reshape/test_concat.py
@@ -2552,3 +2552,16 @@ def test_concat_series_name_npscalar_tuple(s1name, s2name):
     result = pd.concat([s1, s2])
     expected = pd.Series({'a': 1, 'b': 2, 'c': 5, 'd': 6})
     tm.assert_series_equal(result, expected)
+
+
+def test_concat_categorical_tz():
+    # GH-23816
+    a = pd.Series(pd.date_range('2017-01-01', periods=2, tz='US/Pacific'))
+    b = pd.Series(['a', 'b'], dtype='category')
+    result = pd.concat([a, b], ignore_index=True)
+    expected = pd.Series([
+        pd.Timestamp('2017-01-01', tz="US/Pacific"),
+        pd.Timestamp('2017-01-02', tz="US/Pacific"),
+        'a', 'b'
+    ])
+    tm.assert_series_equal(result, expected)