Repository: pandas-dev/pandas

Issue:
REGR: empty list selection resets index types
regression from 0.14.0 (which return `Index`, which is also not correct).

`iloc` DOES work correctly though

```
In [9]: df = tm.makeTimeDataFrame()

In [10]: df.index
Out[10]: 
<class 'pandas.tseries.index.DatetimeIndex'>
[2000-01-03, ..., 2000-02-11]
Length: 30, Freq: B, Timezone: None

In [11]: df.loc[[]].index
Out[11]: Float64Index([], dtype='float64')

# this is the correct result
In [4]: df.iloc[[]].index
Out[4]: 
<class 'pandas.tseries.index.DatetimeIndex'>
Length: 0, Freq: None, Timezone: None
```

```
In [7]: p = tm.makePanel()

In [8]: p.major_axis
Out[8]: 
<class 'pandas.tseries.index.DatetimeIndex'>
[2000-01-03, ..., 2000-02-11]
Length: 30, Freq: B, Timezone: None

In [9]: p.loc[:,[],:].major_axis
Out[9]: Float64Index([], dtype='float64')
```



Discussion:
cc @immerrr I don't remember changing anything specifically.....any thoughts?

I can take a look at this. 

@cpcloud gr8!

Got it running a debugger for a bit: there's a point when `ax.reindex(new_labels)` is invoked and at that point `new_labels` has gone all the way of `[] -> np.array([], dtype=float) -> Index(np.array([], dtype=float)) -> Float64Index([])`.  If you skip conversion to ndarray, you get `Index([])` which is what I see on v0.14.0:

```
In [1]: pd.Series(index=pd.date_range('2013', periods=20))
Out[1]: 
2013-07-17   NaN
2013-07-18   NaN
2013-07-19   NaN
2013-07-20   NaN
2013-07-21   NaN
2013-07-22   NaN
2013-07-23   NaN
2013-07-24   NaN
2013-07-25   NaN
2013-07-26   NaN
2013-07-27   NaN
2013-07-28   NaN
2013-07-29   NaN
2013-07-30   NaN
2013-07-31   NaN
2013-08-01   NaN
2013-08-02   NaN
2013-08-03   NaN
2013-08-04   NaN
2013-08-05   NaN
Freq: D, dtype: float64

In [2]: _1.loc[[]]
Out[2]: Series([], dtype: float64)

In [3]: _1.loc[[]].index
Out[3]: Index([], dtype='object')

In [4]: pd.__version__
Out[4]: '0.14.0'
```

May be Index.reindex should be smarter about preserving type & metadata of the original container.  Incidentally, I've recently encountered the same problem when investigating this [issue](https://github.com/pydata/pandas/issues/6552#issuecomment-48102643).

@immerrr thanks, yes I think `.reindex` should try to preserve if at all possible (might need something special for the 0-len case, but prob just a big buggy somewhere)

All magic happens [here](https://github.com/pydata/pandas/blob/master/pandas/core/indexing.py#L886-L926), I don't see anything terribly out of line.

so either `take` and/or `reindex_axis` are not preserving (dtype, maybe other attributes) on 0-len then.

bet its getting passed correctly in `take`, but then re-inferred in the constructor which then returns `Index` or (something else). prob need to explicity pass in `dtype` in `_simple_new`

It's `reindex_axis`, yes. `_asarray_tuplesafe([])` produces `array([], dtype=float)` which is then passed to `Index.reindex`, which brings us to the question above.

yep, which then takes the character of the passed in index. 

@immerrr doable for 0.15.0?

Yes, it's the same as #6552.

@immerrr ?

Will do at the weekend.


Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -1707,15 +1707,11 @@ def _reindex_axes(self, axes, level, limit, method, fill_value, copy):
             if labels is None:
                 continue
 
-            # convert to an index if we are not a multi-selection
             ax = self._get_axis(a)
-            if level is None:
-                labels = _ensure_index(labels)
-
-            axis = self._get_axis_number(a)
             new_index, indexer = ax.reindex(
                 labels, level=level, limit=limit, method=method)
 
+            axis = self._get_axis_number(a)
             obj = obj._reindex_with_indexers(
                 {axis: [new_index, indexer]}, method=method,
                 fill_value=fill_value, limit=limit, copy=copy,
diff --git a/pandas/core/index.py b/pandas/core/index.py
--- a/pandas/core/index.py
+++ b/pandas/core/index.py
@@ -1594,7 +1594,14 @@ def reindex(self, target, method=None, level=None, limit=None):
         # (i.e. neither Index nor Series).
         preserve_names = not hasattr(target, 'name')
 
-        target = _ensure_index(target)
+        # GH7774: preserve dtype/tz if target is empty and not an Index.
+        target = _ensure_has_len(target)  # target may be an iterator
+        if not isinstance(target, Index) and len(target) == 0:
+            attrs = self._get_attributes_dict()
+            attrs.pop('freq', None)  # don't preserve freq
+            target = self._simple_new(np.empty(0, dtype=self.dtype), **attrs)
+        else:
+            target = _ensure_index(target)
         if level is not None:
             if method is not None:
                 raise TypeError('Fill method not supported if level passed')
@@ -3706,7 +3713,17 @@ def reindex(self, target, method=None, level=None, limit=None):
         if level is not None:
             if method is not None:
                 raise TypeError('Fill method not supported if level passed')
-            target = _ensure_index(target)
+
+            # GH7774: preserve dtype/tz if target is empty and not an Index.
+            target = _ensure_has_len(target)  # target may be an iterator
+            if len(target) == 0 and not isinstance(target, Index):
+                idx = self.levels[level]
+                attrs = idx._get_attributes_dict()
+                attrs.pop('freq', None)  # don't preserve freq
+                target = type(idx)._simple_new(np.empty(0, dtype=idx.dtype),
+                                               **attrs)
+            else:
+                target = _ensure_index(target)
             target, indexer, _ = self._join_level(target, level, how='right',
                                                   return_indexers=True)
         else:
@@ -4566,3 +4583,13 @@ def _get_na_rep(dtype):
 def _get_na_value(dtype):
     return {np.datetime64: tslib.NaT, np.timedelta64: tslib.NaT}.get(dtype,
                                                                      np.nan)
+
+
+def _ensure_has_len(seq):
+    """If seq is an iterator, put its values into a list."""
+    try:
+        len(seq)
+    except TypeError:
+        return list(seq)
+    else:
+        return seq


Test patch:
diff --git a/pandas/tests/test_index.py b/pandas/tests/test_index.py
--- a/pandas/tests/test_index.py
+++ b/pandas/tests/test_index.py
@@ -1049,6 +1049,34 @@ def test_reindex_preserves_name_if_target_is_list_or_ndarray(self):
         self.assertEqual(idx.reindex(dt_idx.values)[0].name, 'foobar')
         self.assertEqual(idx.reindex(dt_idx.tolist())[0].name, 'foobar')
 
+    def test_reindex_preserves_type_if_target_is_empty_list_or_array(self):
+        # GH7774
+        idx = pd.Index(list('abc'))
+        def get_reindex_type(target):
+            return idx.reindex(target)[0].dtype.type
+
+        self.assertEqual(get_reindex_type([]), np.object_)
+        self.assertEqual(get_reindex_type(np.array([])), np.object_)
+        self.assertEqual(get_reindex_type(np.array([], dtype=np.int64)),
+                         np.object_)
+
+    def test_reindex_doesnt_preserve_type_if_target_is_empty_index(self):
+        # GH7774
+        idx = pd.Index(list('abc'))
+        def get_reindex_type(target):
+            return idx.reindex(target)[0].dtype.type
+
+        self.assertEqual(get_reindex_type(pd.Int64Index([])), np.int_)
+        self.assertEqual(get_reindex_type(pd.Float64Index([])), np.float_)
+        self.assertEqual(get_reindex_type(pd.DatetimeIndex([])), np.datetime64)
+
+        reindexed = idx.reindex(pd.MultiIndex([pd.Int64Index([]),
+                                               pd.Float64Index([])],
+                                              [[], []]))[0]
+        self.assertEqual(reindexed.levels[0].dtype.type, np.int64)
+        self.assertEqual(reindexed.levels[1].dtype.type, np.float64)
+
+
 
 class Numeric(Base):
 
@@ -1699,6 +1727,13 @@ def test_roundtrip_pickle_with_tz(self):
         unpickled = self.round_trip_pickle(index)
         self.assertTrue(index.equals(unpickled))
 
+    def test_reindex_preserves_tz_if_target_is_empty_list_or_array(self):
+        # GH7774
+        index = date_range('20130101', periods=3, tz='US/Eastern')
+        self.assertEqual(str(index.reindex([])[0].tz), 'US/Eastern')
+        self.assertEqual(str(index.reindex(np.array([]))[0].tz), 'US/Eastern')
+
+
 class TestPeriodIndex(Base, tm.TestCase):
     _holder = PeriodIndex
     _multiprocess_can_split_ = True
@@ -3321,6 +3356,21 @@ def test_reindex_preserves_names_when_target_is_list_or_ndarray(self):
         self.assertEqual(idx.reindex(other_dtype.tolist())[0].names, ['foo', 'bar'])
         self.assertEqual(idx.reindex(other_dtype.values)[0].names, ['foo', 'bar'])
 
+    def test_reindex_lvl_preserves_names_when_target_is_list_or_array(self):
+        # GH7774
+        idx = pd.MultiIndex.from_product([[0, 1], ['a', 'b']],
+                                         names=['foo', 'bar'])
+        self.assertEqual(idx.reindex([], level=0)[0].names, ['foo', 'bar'])
+        self.assertEqual(idx.reindex([], level=1)[0].names, ['foo', 'bar'])
+
+    def test_reindex_lvl_preserves_type_if_target_is_empty_list_or_array(self):
+        # GH7774
+        idx = pd.MultiIndex.from_product([[0, 1], ['a', 'b']])
+        self.assertEqual(idx.reindex([], level=0)[0].levels[0].dtype.type,
+                         np.int_)
+        self.assertEqual(idx.reindex([], level=1)[0].levels[1].dtype.type,
+                         np.object_)
+
 
 def test_get_combined_index():
     from pandas.core.index import _get_combined_index
diff --git a/pandas/tests/test_indexing.py b/pandas/tests/test_indexing.py
--- a/pandas/tests/test_indexing.py
+++ b/pandas/tests/test_indexing.py
@@ -3832,23 +3832,41 @@ def test_set_ix_out_of_bounds_axis_1(self):
     def test_iloc_empty_list_indexer_is_ok(self):
         from pandas.util.testing import makeCustomDataframe as mkdf
         df = mkdf(5, 2)
-        assert_frame_equal(df.iloc[:,[]], df.iloc[:, :0])  # vertical empty
-        assert_frame_equal(df.iloc[[],:], df.iloc[:0, :])  # horizontal empty
-        assert_frame_equal(df.iloc[[]], df.iloc[:0, :])  # horizontal empty
+        # vertical empty
+        assert_frame_equal(df.iloc[:, []], df.iloc[:, :0],
+                           check_index_type=True, check_column_type=True)
+        # horizontal empty
+        assert_frame_equal(df.iloc[[], :], df.iloc[:0, :],
+                           check_index_type=True, check_column_type=True)
+        # horizontal empty
+        assert_frame_equal(df.iloc[[]], df.iloc[:0, :],
+                           check_index_type=True, check_column_type=True)
 
     def test_loc_empty_list_indexer_is_ok(self):
         from pandas.util.testing import makeCustomDataframe as mkdf
         df = mkdf(5, 2)
-        assert_frame_equal(df.loc[:,[]], df.iloc[:, :0])  # vertical empty
-        assert_frame_equal(df.loc[[],:], df.iloc[:0, :])  # horizontal empty
-        assert_frame_equal(df.loc[[]], df.iloc[:0, :])  # horizontal empty
+        # vertical empty
+        assert_frame_equal(df.loc[:, []], df.iloc[:, :0],
+                           check_index_type=True, check_column_type=True)
+        # horizontal empty
+        assert_frame_equal(df.loc[[], :], df.iloc[:0, :],
+                           check_index_type=True, check_column_type=True)
+        # horizontal empty
+        assert_frame_equal(df.loc[[]], df.iloc[:0, :],
+                           check_index_type=True, check_column_type=True)
 
     def test_ix_empty_list_indexer_is_ok(self):
         from pandas.util.testing import makeCustomDataframe as mkdf
         df = mkdf(5, 2)
-        assert_frame_equal(df.ix[:,[]], df.iloc[:, :0])  # vertical empty
-        assert_frame_equal(df.ix[[],:], df.iloc[:0, :])  # horizontal empty
-        assert_frame_equal(df.ix[[]], df.iloc[:0, :])  # horizontal empty
+        # vertical empty
+        assert_frame_equal(df.ix[:, []], df.iloc[:, :0],
+                           check_index_type=True, check_column_type=True)
+        # horizontal empty
+        assert_frame_equal(df.ix[[], :], df.iloc[:0, :],
+                           check_index_type=True, check_column_type=True)
+        # horizontal empty
+        assert_frame_equal(df.ix[[]], df.iloc[:0, :],
+                           check_index_type=True, check_column_type=True)
 
     def test_deprecate_float_indexers(self):