Repository: pandas-dev/pandas

Issue:
BUG: Series.map({}) should retain the original dtype
https://github.com/pandas-dev/pandas/pull/18491#issuecomment-347050600

```
In [1]: pd.Series(pd.date_range("2012-01-01", periods=3)).map({})
Out[1]: 
0   NaN
1   NaN
2   NaN
dtype: float64

In [2]: pd.date_range("2012-01-01", periods=3).map({})
Out[2]: DatetimeIndex(['NaT', 'NaT', 'NaT'], dtype='datetime64[ns]', freq=None)
```

[1] should infer to a datetime64 as well

@jschendel comments

On master:
```python
In [2]: pd.__version__
Out[2]: '0.22.0.dev0+241.gf745e52'

In [3]: pd.date_range('20170101', periods=4).map({})
Out[3]: DatetimeIndex(['NaT', 'NaT', 'NaT', 'NaT'], dtype='datetime64[ns]', freq=None)
```

`IntervalIndex`, `CategoricalIndex`, and `Index` with `object` dtype get coerced to `Float64Index`:

```python
In [4]: pd.interval_range(0, 5).map({})
Out[4]: Float64Index([nan, nan, nan, nan, nan], dtype='float64')

In [5]: pd.CategoricalIndex(list('abca')).map({})
Out[5]: Float64Index([nan, nan, nan, nan], dtype='float64')

In [6]: pd.Index(list('abca')).map({})
Out[6]: Float64Index([nan, nan, nan, nan], dtype='float64')
```

`PeriodIndex` and `TimedeltaIndex` get coerced `DatetimeIndex`:
```python
In [7]: pd.period_range('2017Q1', periods=4, freq='Q').map({})
Out[7]: DatetimeIndex(['NaT', 'NaT', 'NaT', 'NaT'], dtype='datetime64[ns]', freq=None)

In [8]: pd.timedelta_range(1, periods=4).map({})
Out[8]: DatetimeIndex(['NaT', 'NaT', 'NaT', 'NaT'], dtype='datetime64[ns]', freq=None)
```


Discussion:
@jschendel yeah [7], and [8] 'work' but should be the correct type (bug I introduced). The others are not well tested (cat / interval), so not surprising they are wrong. [6] also not tested well.
Repeating my comment from https://github.com/pandas-dev/pandas/pull/18491 here:

I am not sure I find this special casing a good idea. I would rather keep `.map` 'dummy' and simply create a new index/series from the newly created values, without trying to infer anything more than `pd.Index(..)` / `pd.Series(..)` already does. 

Do you have a specific use case for this? (for wanting to be smart?) 
I think the user can always change the dtype after doing the map if it wants something specific or to preserve the index.
To further clarify: I think we are just trying to be too smart here, introducing a lot of corner cases. I would say: just let the user handle this (if he/she wants to retain the index type, he/she can simply wrap the map call in the appropriate index type). 

So what is the rationale of trying to keep the index type in case of all-NaNs ? Because you think a user typically uses `map` to create a new index of the same type but with different values? And it such a case that your DatetimeIndex becoming a FloatIndex is suprising? 
In my personal use cases of `map`, I am typically not interested in preserving the index type, as my functions typically return something completely different (in terms of dtype). In that case, this "trying to preserve the dtype" can also give unexpected results the other way around. Assume I have a function that returns floats, but for some reason for the index I have, only returns np.nans:

```
In [44]: pd.date_range("2010-01-01", periods=3, freq='A').map(lambda x: np.nan if x.year > 2010 else 1)
Out[44]: Float64Index([1.0, nan, nan], dtype='float64')

In [45]: pd.date_range("2011-01-01", periods=3, freq='A').map(lambda x: np.nan if x.year > 2010 else 1)
Out[45]: DatetimeIndex(['NaT', 'NaT', 'NaT'], dtype='datetime64[ns]', freq=None)
```

In that case I, unexpectedly, get a DatetimeIndex instead of a FloatIndex.

To put it differently, we wouldn't distinguish a NaN and NaT return:

```
In [39]: pd.date_range("2012-01-01", periods=3).map(lambda x: pd.NaT)
Out[39]: DatetimeIndex(['NaT', 'NaT', 'NaT'], dtype='datetime64[ns]', freq=None)

In [40]: pd.date_range("2012-01-01", periods=3).map(lambda x: np.nan)
Out[40]: DatetimeIndex(['NaT', 'NaT', 'NaT'], dtype='datetime64[ns]', freq=None)
```

Also, you added a special case of trying to preserve uint64 data type for Uint64Index. Shouldn't we then do this for Series with different dtypes as well?

And about [1] (first example at the top) being a bug: this has always been the behaviour, so if we change this I would see it as a API breaking change rather than a bug fix.

Resolving patch:
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -2897,25 +2897,9 @@ def map(self, mapper, na_action=None):
                                           names=names)
 
         attributes['copy'] = False
-
-        # we want to try to return our original dtype
-        # ints infer to integer, but if we have
-        # uints, would prefer to return these
-        if is_unsigned_integer_dtype(self.dtype):
-            inferred = lib.infer_dtype(new_values)
-            if inferred == 'integer':
-                attributes['dtype'] = self.dtype
-
-        elif not new_values.size:
+        if not new_values.size:
             # empty
             attributes['dtype'] = self.dtype
-        elif isna(new_values).all():
-            # all nan
-            inferred = lib.infer_dtype(self)
-            if inferred in ['datetime', 'datetime64',
-                            'timedelta', 'timedelta64',
-                            'period']:
-                new_values = [libts.NaT] * len(new_values)
 
         return Index(new_values, **attributes)
 


Test patch:
diff --git a/pandas/tests/indexes/common.py b/pandas/tests/indexes/common.py
--- a/pandas/tests/indexes/common.py
+++ b/pandas/tests/indexes/common.py
@@ -1009,7 +1009,13 @@ def test_searchsorted_monotonic(self, indices):
     def test_map(self):
         # callable
         index = self.create_index()
-        expected = index
+
+        # we don't infer UInt64
+        if isinstance(index, pd.UInt64Index):
+            expected = index.astype('int64')
+        else:
+            expected = index
+
         result = index.map(lambda x: x)
         tm.assert_index_equal(result, expected)
 
@@ -1024,9 +1030,14 @@ def test_map_dictlike(self, mapper):
         if isinstance(index, (pd.CategoricalIndex, pd.IntervalIndex)):
             pytest.skip("skipping tests for {}".format(type(index)))
 
-        expected = index
-
         identity = mapper(index.values, index)
+
+        # we don't infer to UInt64 for a dict
+        if isinstance(index, pd.UInt64Index) and isinstance(identity, dict):
+            expected = index.astype('int64')
+        else:
+            expected = index
+
         result = index.map(identity)
         tm.assert_index_equal(result, expected)
 
diff --git a/pandas/tests/indexes/datetimelike.py b/pandas/tests/indexes/datetimelike.py
--- a/pandas/tests/indexes/datetimelike.py
+++ b/pandas/tests/indexes/datetimelike.py
@@ -1,5 +1,6 @@
 """ generic datetimelike tests """
 import pytest
+import numpy as np
 import pandas as pd
 from .common import Base
 import pandas.util.testing as tm
@@ -72,6 +73,6 @@ def test_map_dictlike(self, mapper):
 
         # empty map; these map to np.nan because we cannot know
         # to re-infer things
-        expected = pd.Index([pd.NaT] * len(self.index))
+        expected = pd.Index([np.nan] * len(self.index))
         result = self.index.map(mapper([], []))
         tm.assert_index_equal(result, expected)
diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py
--- a/pandas/tests/indexes/test_base.py
+++ b/pandas/tests/indexes/test_base.py
@@ -885,9 +885,7 @@ def test_map_dictlike(self, mapper):
             expected = Index(np.arange(len(index), 0, -1))
 
             # to match proper result coercion for uints
-            if name == 'uintIndex':
-                expected = expected.astype('uint64')
-            elif name == 'empty':
+            if name == 'empty':
                 expected = Index([])
 
             result = index.map(mapper(expected, index))
diff --git a/pandas/tests/series/test_apply.py b/pandas/tests/series/test_apply.py
--- a/pandas/tests/series/test_apply.py
+++ b/pandas/tests/series/test_apply.py
@@ -377,6 +377,14 @@ def test_map(self):
         exp = Series([np.nan, 'B', 'C', 'D'])
         tm.assert_series_equal(a.map(c), exp)
 
+    @pytest.mark.parametrize("index", tm.all_index_generator(10))
+    def test_map_empty(self, index):
+        s = Series(index)
+        result = s.map({})
+
+        expected = pd.Series(np.nan, index=s.index)
+        tm.assert_series_equal(result, expected)
+
     def test_map_compat(self):
         # related GH 8024
         s = Series([True, True, False], index=[1, 2, 3])
diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py
--- a/pandas/tests/test_resample.py
+++ b/pandas/tests/test_resample.py
@@ -816,21 +816,23 @@ def test_resample_empty_dataframe(self):
 
             # test size for GH13212 (currently stays as df)
 
-    def test_resample_empty_dtypes(self):
+    @pytest.mark.parametrize("index", tm.all_timeseries_index_generator(0))
+    @pytest.mark.parametrize(
+        "dtype",
+        [np.float, np.int, np.object, 'datetime64[ns]'])
+    def test_resample_empty_dtypes(self, index, dtype):
 
         # Empty series were sometimes causing a segfault (for the functions
         # with Cython bounds-checking disabled) or an IndexError.  We just run
         # them to ensure they no longer do.  (GH #10228)
-        for index in tm.all_timeseries_index_generator(0):
-            for dtype in (np.float, np.int, np.object, 'datetime64[ns]'):
-                for how in downsample_methods + upsample_methods:
-                    empty_series = Series([], index, dtype)
-                    try:
-                        getattr(empty_series.resample('d'), how)()
-                    except DataError:
-                        # Ignore these since some combinations are invalid
-                        # (ex: doing mean with dtype of np.object)
-                        pass
+        for how in downsample_methods + upsample_methods:
+            empty_series = Series([], index, dtype)
+            try:
+                getattr(empty_series.resample('d'), how)()
+            except DataError:
+                # Ignore these since some combinations are invalid
+                # (ex: doing mean with dtype of np.object)
+                pass
 
     def test_resample_loffset_arg_type(self):
         # GH 13218, 15002
diff --git a/pandas/util/testing.py b/pandas/util/testing.py
--- a/pandas/util/testing.py
+++ b/pandas/util/testing.py
@@ -1695,7 +1695,8 @@ def all_index_generator(k=10):
     """
     all_make_index_funcs = [makeIntIndex, makeFloatIndex, makeStringIndex,
                             makeUnicodeIndex, makeDateIndex, makePeriodIndex,
-                            makeTimedeltaIndex, makeBoolIndex,
+                            makeTimedeltaIndex, makeBoolIndex, makeRangeIndex,
+                            makeIntervalIndex,
                             makeCategoricalIndex]
     for make_index_func in all_make_index_funcs:
         yield make_index_func(k=k)