diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index 4a27bf54de6952..a8b191189f10e1 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -208,5 +208,6 @@ Other - Improved error message when attempting to use a Python keyword as an identifier in a numexpr query (:issue:`18221`) - Fixed a bug where creating a Series from an array that contains both tz-naive and tz-aware values will result in a Series whose dtype is tz-aware instead of object (:issue:`16406`) +- Fixed construction of :class:`Series` from ``dict`` containing ``NaN`` as key (:issue:`18480`) - Adding a ``Period`` object to a ``datetime`` or ``Timestamp`` object will now correctly raise a ``TypeError`` (:issue:`17983`) - diff --git a/pandas/core/base.py b/pandas/core/base.py index ae92b62ce1d119..72acd0052202b9 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -874,9 +874,8 @@ def _map_values(self, mapper, na_action=None): # convert to an Series for efficiency. # we specify the keys here to handle the # possibility that they are tuples - from pandas import Series, Index - index = Index(mapper, tupleize_cols=False) - mapper = Series(mapper, index=index) + from pandas import Series + mapper = Series(mapper) if isinstance(mapper, ABCSeries): # Since values were input this means we came from either diff --git a/pandas/core/series.py b/pandas/core/series.py index bff7c21ad69b15..8574c3a098c9e0 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -42,7 +42,6 @@ _default_index, _asarray_tuplesafe, _values_from_object, - _try_sort, _maybe_match_name, SettingWithCopyError, _maybe_box_datetimelike, @@ -198,18 +197,9 @@ def __init__(self, data=None, index=None, dtype=None, name=None, data = data.reindex(index, copy=copy) data = data._data elif isinstance(data, dict): - if index is None: - if isinstance(data, OrderedDict): - index = Index(data) - else: - index = Index(_try_sort(data)) - - try: - data = index._get_values_from_dict(data) - except TypeError: - data = ([data.get(i, np.nan) for i in index] - if data else np.nan) - + data, index = self._init_dict(data, index, dtype) + dtype = None + copy = False elif isinstance(data, SingleBlockManager): if index is None: index = data.index @@ -257,6 +247,43 @@ def __init__(self, data=None, index=None, dtype=None, name=None, self.name = name self._set_axis(0, index, fastpath=True) + def _init_dict(self, data, index=None, dtype=None): + """ + Derive the "_data" and "index" attributes of a new Series from a + dictionary input. + + Parameters + ---------- + data : dict or dict-like + Data used to populate the new Series + index : Index or index-like, default None + index for the new Series: if None, use dict keys + dtype : dtype, default None + dtype for the new Series: if None, infer from data + + Returns + ------- + _data : BlockManager for the new Series + index : index for the new Series + """ + # Looking for NaN in dict doesn't work ({np.nan : 1}[float('nan')] + # raises KeyError), so we iterate the entire dict, and align + if data: + keys, values = zip(*compat.iteritems(data)) + else: + keys, values = [], [] + # Input is now list-like, so rely on "standard" construction: + s = Series(values, index=keys, dtype=dtype) + # Now we just make sure the order is respected, if any + if index is not None and not index.identical(keys): + s = s.reindex(index) + elif not isinstance(data, OrderedDict): + try: + s = s.sort_index() + except TypeError: + pass + return s._data, s.index + @classmethod def from_array(cls, arr, index=None, name=None, dtype=None, copy=False, fastpath=False): diff --git a/pandas/tests/series/test_apply.py b/pandas/tests/series/test_apply.py index fe21ba569ae991..cafe6a34720bee 100644 --- a/pandas/tests/series/test_apply.py +++ b/pandas/tests/series/test_apply.py @@ -422,6 +422,7 @@ def test_map_dict_with_tuple_keys(self): converted to a multi-index, preventing tuple values from being mapped properly. """ + # GH 18496 df = pd.DataFrame({'a': [(1, ), (2, ), (3, 4), (5, 6)]}) label_mappings = {(1, ): 'A', (2, ): 'B', (3, 4): 'A', (5, 6): 'B'} diff --git a/pandas/tests/series/test_combine_concat.py b/pandas/tests/series/test_combine_concat.py index 71ac00975af03d..6cf60e818c845a 100644 --- a/pandas/tests/series/test_combine_concat.py +++ b/pandas/tests/series/test_combine_concat.py @@ -181,7 +181,8 @@ def test_concat_empty_series_dtypes(self): # categorical assert pd.concat([Series(dtype='category'), Series(dtype='category')]).dtype == 'category' - assert pd.concat([Series(dtype='category'), + # GH 18515 + assert pd.concat([Series(np.array([]), dtype='category'), Series(dtype='float64')]).dtype == 'float64' assert pd.concat([Series(dtype='category'), Series(dtype='object')]).dtype == 'object' diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py index ccc04da3299fef..5462464da0e0b4 100644 --- a/pandas/tests/series/test_constructors.py +++ b/pandas/tests/series/test_constructors.py @@ -625,6 +625,21 @@ def test_constructor_dict(self): expected.iloc[1] = 1 assert_series_equal(result, expected) + @pytest.mark.parametrize("value", [2, np.nan, None, float('nan')]) + def test_constructor_dict_nan_key(self, value): + # GH 18480 + d = {1: 'a', value: 'b', float('nan'): 'c', 4: 'd'} + result = Series(d).sort_values() + expected = Series(['a', 'b', 'c', 'd'], index=[1, value, np.nan, 4]) + assert_series_equal(result, expected) + + # MultiIndex: + d = {(1, 1): 'a', (2, np.nan): 'b', (3, value): 'c'} + result = Series(d).sort_values() + expected = Series(['a', 'b', 'c'], + index=Index([(1, 1), (2, np.nan), (3, value)])) + assert_series_equal(result, expected) + def test_constructor_dict_datetime64_index(self): # GH 9456 @@ -658,8 +673,6 @@ def test_constructor_tuple_of_tuples(self): s = Series(data) assert tuple(s) == data - @pytest.mark.xfail(reason='GH 18480 (Series initialization from dict with ' - 'NaN keys') def test_constructor_dict_of_tuples(self): data = {(1, 2): 3, (None, 5): 6}