Skip to content

Commit d47c5a2

Browse files
ohad83WillAyd
authored andcommitted
ENH: Allow map with abc mapping (pandas-dev#29788)
1 parent 27f406f commit d47c5a2

File tree

9 files changed

+81
-15
lines changed

9 files changed

+81
-15
lines changed

doc/source/whatsnew/v1.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ Other enhancements
224224
- :meth:`DataFrame.to_markdown` and :meth:`Series.to_markdown` added (:issue:`11052`)
225225
- :meth:`DataFrame.drop_duplicates` has gained ``ignore_index`` keyword to reset index (:issue:`30114`)
226226
- Added new writer for exporting Stata dta files in version 118, ``StataWriter118``. This format supports exporting strings containing Unicode characters (:issue:`23573`)
227+
- :meth:`Series.map` now accepts ``collections.abc.Mapping`` subclasses as a mapper (:issue:`29733`)
227228

228229
Build Changes
229230
^^^^^^^^^^^^^

pandas/conftest.py

+36
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import abc
12
from datetime import date, time, timedelta, timezone
23
from decimal import Decimal
34
import operator
@@ -894,3 +895,38 @@ def index_or_series(request):
894895
See GH#29725
895896
"""
896897
return request.param
898+
899+
900+
@pytest.fixture
901+
def dict_subclass():
902+
"""
903+
Fixture for a dictionary subclass.
904+
"""
905+
906+
class TestSubDict(dict):
907+
def __init__(self, *args, **kwargs):
908+
dict.__init__(self, *args, **kwargs)
909+
910+
return TestSubDict
911+
912+
913+
@pytest.fixture
914+
def non_mapping_dict_subclass():
915+
"""
916+
Fixture for a non-mapping dictionary subclass.
917+
"""
918+
919+
class TestNonDictMapping(abc.Mapping):
920+
def __init__(self, underlying_dict):
921+
self._data = underlying_dict
922+
923+
def __getitem__(self, key):
924+
return self._data.__getitem__(key)
925+
926+
def __iter__(self):
927+
return self._data.__iter__()
928+
929+
def __len__(self):
930+
return self._data.__len__()
931+
932+
return TestNonDictMapping

pandas/core/base.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
is_categorical_dtype,
2020
is_datetime64_ns_dtype,
2121
is_datetime64tz_dtype,
22+
is_dict_like,
2223
is_extension_array_dtype,
2324
is_list_like,
2425
is_object_dtype,
@@ -1107,8 +1108,8 @@ def _map_values(self, mapper, na_action=None):
11071108
# we can fastpath dict/Series to an efficient map
11081109
# as we know that we are not going to have to yield
11091110
# python types
1110-
if isinstance(mapper, dict):
1111-
if hasattr(mapper, "__missing__"):
1111+
if is_dict_like(mapper):
1112+
if isinstance(mapper, dict) and hasattr(mapper, "__missing__"):
11121113
# If a dictionary subclass defines a default value method,
11131114
# convert mapper to a lookup function (GH #15999).
11141115
dict_with_default = mapper

pandas/core/series.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class Series(base.IndexOpsMixin, generic.NDFrame):
182182
def __init__(
183183
self, data=None, index=None, dtype=None, name=None, copy=False, fastpath=False
184184
):
185+
185186
# we are called internally, so short-circuit
186187
if fastpath:
187188

@@ -250,7 +251,7 @@ def __init__(
250251
else:
251252
data = data.reindex(index, copy=copy)
252253
data = data._data
253-
elif isinstance(data, dict):
254+
elif is_dict_like(data):
254255
data, index = self._init_dict(data, index, dtype)
255256
dtype = None
256257
copy = False
@@ -3513,7 +3514,7 @@ def map(self, arg, na_action=None):
35133514
35143515
Parameters
35153516
----------
3516-
arg : function, dict, or Series
3517+
arg : function, collections.abc.Mapping subclass or Series
35173518
Mapping correspondence.
35183519
na_action : {None, 'ignore'}, default None
35193520
If 'ignore', propagate NaN values, without passing them to the

pandas/tests/frame/test_constructors.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -511,17 +511,17 @@ def test_constructor_with_embedded_frames(self):
511511
result = df2.loc[1, 0]
512512
tm.assert_frame_equal(result, df1 + 10)
513513

514-
def test_constructor_subclass_dict(self, float_frame):
514+
def test_constructor_subclass_dict(self, float_frame, dict_subclass):
515515
# Test for passing dict subclass to constructor
516516
data = {
517-
"col1": tm.TestSubDict((x, 10.0 * x) for x in range(10)),
518-
"col2": tm.TestSubDict((x, 20.0 * x) for x in range(10)),
517+
"col1": dict_subclass((x, 10.0 * x) for x in range(10)),
518+
"col2": dict_subclass((x, 20.0 * x) for x in range(10)),
519519
}
520520
df = DataFrame(data)
521521
refdf = DataFrame({col: dict(val.items()) for col, val in data.items()})
522522
tm.assert_frame_equal(refdf, df)
523523

524-
data = tm.TestSubDict(data.items())
524+
data = dict_subclass(data.items())
525525
df = DataFrame(data)
526526
tm.assert_frame_equal(refdf, df)
527527

pandas/tests/series/test_api.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ def test_constructor_dict(self):
126126
expected = Series([1, 2, np.nan, 0], index=["b", "c", "d", "a"])
127127
tm.assert_series_equal(result, expected)
128128

129-
def test_constructor_subclass_dict(self):
130-
data = tm.TestSubDict((x, 10.0 * x) for x in range(10))
129+
def test_constructor_subclass_dict(self, dict_subclass):
130+
data = dict_subclass((x, 10.0 * x) for x in range(10))
131131
series = Series(data)
132132
expected = Series(dict(data.items()))
133133
tm.assert_series_equal(series, expected)

pandas/tests/series/test_apply.py

+24
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,30 @@ class DictWithoutMissing(dict):
627627
expected = Series([np.nan, np.nan, "three"])
628628
tm.assert_series_equal(result, expected)
629629

630+
def test_map_abc_mapping(self, non_mapping_dict_subclass):
631+
# https://github.com/pandas-dev/pandas/issues/29733
632+
# Check collections.abc.Mapping support as mapper for Series.map
633+
s = Series([1, 2, 3])
634+
not_a_dictionary = non_mapping_dict_subclass({3: "three"})
635+
result = s.map(not_a_dictionary)
636+
expected = Series([np.nan, np.nan, "three"])
637+
tm.assert_series_equal(result, expected)
638+
639+
def test_map_abc_mapping_with_missing(self, non_mapping_dict_subclass):
640+
# https://github.com/pandas-dev/pandas/issues/29733
641+
# Check collections.abc.Mapping support as mapper for Series.map
642+
class NonDictMappingWithMissing(non_mapping_dict_subclass):
643+
def __missing__(self, key):
644+
return "missing"
645+
646+
s = Series([1, 2, 3])
647+
not_a_dictionary = NonDictMappingWithMissing({3: "three"})
648+
result = s.map(not_a_dictionary)
649+
# __missing__ is a dict concept, not a Mapping concept,
650+
# so it should not change the result!
651+
expected = Series([np.nan, np.nan, "three"])
652+
tm.assert_series_equal(result, expected)
653+
630654
def test_map_box(self):
631655
vals = [pd.Timestamp("2011-01-01"), pd.Timestamp("2011-01-02")]
632656
s = pd.Series(vals)

pandas/tests/series/test_constructors.py

+8
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,14 @@ def create_data(constructor):
10891089
tm.assert_series_equal(result_datetime, expected)
10901090
tm.assert_series_equal(result_Timestamp, expected)
10911091

1092+
def test_constructor_mapping(self, non_mapping_dict_subclass):
1093+
# GH 29788
1094+
ndm = non_mapping_dict_subclass({3: "three"})
1095+
result = Series(ndm)
1096+
expected = Series(["three"], index=[3])
1097+
1098+
tm.assert_series_equal(result, expected)
1099+
10921100
def test_constructor_list_of_tuples(self):
10931101
data = [(1, 1), (2, 2), (2, 3)]
10941102
s = Series(data)

pandas/util/testing.py

-5
Original file line numberDiff line numberDiff line change
@@ -2123,11 +2123,6 @@ def makeMissingDataframe(density=0.9, random_state=None):
21232123
return df
21242124

21252125

2126-
class TestSubDict(dict):
2127-
def __init__(self, *args, **kwargs):
2128-
dict.__init__(self, *args, **kwargs)
2129-
2130-
21312126
def optional_args(decorator):
21322127
"""allows a decorator to take optional positional and keyword arguments.
21332128
Assumes that taking a single, callable, positional argument means that

0 commit comments

Comments
 (0)