Skip to content

Commit 6acec1c

Browse files
committed
REF: simplify .map method for datetime-likes
1 parent a7b9c56 commit 6acec1c

File tree

8 files changed

+36
-55
lines changed

8 files changed

+36
-55
lines changed

doc/source/whatsnew/v2.1.0.rst

+3
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,14 @@ Categorical
133133
Datetimelike
134134
^^^^^^^^^^^^
135135
- Bug in :meth:`Timestamp.round` with values close to the implementation bounds returning incorrect results instead of raising ``OutOfBoundsDatetime`` (:issue:`51494`)
136+
- :meth:`arrays.DatetimeArray.map` can now take a ``na_action`` argument. :meth:`DatetimeIndex.map` with ``na_action="ignore"`` now works as expected. (:issue:`51644`)
136137
-
137138

138139
Timedelta
139140
^^^^^^^^^
140141
- Bug in :meth:`Timedelta.round` with values close to the implementation bounds returning incorrect results instead of raising ``OutOfBoundsTimedelta`` (:issue:`51494`)
141142
- Bug in :class:`TimedeltaIndex` division or multiplication leading to ``.freq`` of "0 Days" instead of ``None`` (:issue:`51575`)
143+
- :meth:`arrays.TimedeltaArray.map` can now take a ``na_action`` argument. :meth:`TimedeltaIndex.map` with ``na_action="ignore"`` now works as expected. (:issue:`51644`)
142144
-
143145

144146
Timezones
@@ -190,6 +192,7 @@ Period
190192
^^^^^^
191193
- Bug in :class:`PeriodDtype` constructor failing to raise ``TypeError`` when no argument is passed or when ``None`` is passed (:issue:`27388`)
192194
- Bug in :class:`PeriodDtype` constructor raising ``ValueError`` instead of ``TypeError`` when an invalid type is passed (:issue:`51790`)
195+
- :meth:`arrays.PeriodArray.map` can now take a ``na_action`` argument. :meth:`PeriodIndex.map` with ``na_action="ignore"`` now works as expected. (:issue:`51644`)
193196
-
194197

195198
Plotting

pandas/core/algorithms.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1750,4 +1750,4 @@ def map_array(
17501750
if na_action is None:
17511751
return lib.map_infer(values, mapper)
17521752
else:
1753-
return lib.map_infer_mask(values, mapper, isna(values).view(np.uint8))
1753+
return lib.map_infer_mask(values, mapper, mask=isna(values).view(np.uint8))

pandas/core/arrays/datetimelike.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
from pandas.core.algorithms import (
120120
checked_add_with_arr,
121121
isin,
122+
map_array,
122123
unique1d,
123124
)
124125
from pandas.core.array_algos import datetimelike_accumulations
@@ -751,17 +752,34 @@ def _unbox(self, other) -> np.int64 | np.datetime64 | np.timedelta64 | np.ndarra
751752

752753
@ravel_compat
753754
def map(self, mapper, na_action=None):
754-
if na_action is not None:
755-
raise NotImplementedError
756-
757755
# TODO(GH-23179): Add ExtensionArray.map
758756
# Need to figure out if we want ExtensionArray.map first.
759757
# If so, then we can refactor IndexOpsMixin._map_values to
760758
# a standalone function and call from here..
761759
# Else, just rewrite _map_infer_values to do the right thing.
762760
from pandas import Index
763761

764-
return Index(self).map(mapper).array
762+
idx = Index(self)
763+
764+
try:
765+
if na_action is not None:
766+
raise ValueError("calling mapper directly only na_action=None")
767+
result = mapper(idx)
768+
769+
# Try to use this result if we can
770+
if isinstance(result, np.ndarray):
771+
result = Index(result)
772+
773+
if not isinstance(result, Index):
774+
raise TypeError("The map function must return an Index object")
775+
except Exception:
776+
result = map_array(self, mapper, na_action=na_action)
777+
result = Index(result)
778+
779+
if isinstance(result, ABCMultiIndex):
780+
return result.to_numpy()
781+
else:
782+
return result.array
765783

766784
def isin(self, values) -> npt.NDArray[np.bool_]:
767785
"""

pandas/core/base.py

+3-8
Original file line numberDiff line numberDiff line change
@@ -881,14 +881,9 @@ def _map_values(self, mapper, na_action=None):
881881
"""
882882
arr = extract_array(self, extract_numpy=True, extract_range=True)
883883

884-
if is_extension_array_dtype(arr.dtype):
885-
# Item "IndexOpsMixin" of "Union[IndexOpsMixin, ExtensionArray,
886-
# ndarray[Any, Any]]" has no attribute "map"
887-
return arr.map(mapper, na_action=na_action) # type: ignore[union-attr]
888-
889-
# Argument 1 to "map_array" has incompatible type
890-
# "Union[IndexOpsMixin, ExtensionArray, ndarray[Any, Any]]";
891-
# expected "Union[ExtensionArray, ndarray[Any, Any]]"
884+
if isinstance(arr, ExtensionArray):
885+
return arr.map(mapper, na_action=na_action)
886+
892887
return algorithms.map_array(
893888
arr, mapper, na_action=na_action # type: ignore[arg-type]
894889
)

pandas/core/indexes/extension.py

+3-23
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,15 @@
99
TypeVar,
1010
)
1111

12-
import numpy as np
13-
14-
from pandas.util._decorators import (
15-
cache_readonly,
16-
doc,
17-
)
12+
from pandas.util._decorators import cache_readonly
1813

1914
from pandas.core.dtypes.generic import ABCDataFrame
2015

2116
from pandas.core.indexes.base import Index
2217

2318
if TYPE_CHECKING:
19+
import numpy as np
20+
2421
from pandas._typing import (
2522
ArrayLike,
2623
npt,
@@ -154,23 +151,6 @@ def _validate_fill_value(self, value):
154151
"""
155152
return self._data._validate_setitem_value(value)
156153

157-
@doc(Index.map)
158-
def map(self, mapper, na_action=None):
159-
# Try to run function on index first, and then on elements of index
160-
# Especially important for group-by functionality
161-
try:
162-
result = mapper(self)
163-
164-
# Try to use this result if we can
165-
if isinstance(result, np.ndarray):
166-
result = Index(result)
167-
168-
if not isinstance(result, Index):
169-
raise TypeError("The map function must return an Index object")
170-
return result
171-
except Exception:
172-
return self.astype(object).map(mapper)
173-
174154
@cache_readonly
175155
def _isnan(self) -> npt.NDArray[np.bool_]:
176156
# error: Incompatible return value type (got "ExtensionArray", expected

pandas/tests/apply/test_invalid_arg.py

-7
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,6 @@ def test_map_categorical_na_action():
8383
s.map(lambda x: x, na_action="ignore")
8484

8585

86-
def test_map_datetimetz_na_action():
87-
values = date_range("2011-01-01", "2011-01-02", freq="H").tz_localize("Asia/Tokyo")
88-
s = Series(values, name="XX")
89-
with pytest.raises(NotImplementedError, match=tm.EMPTY_STRING_PATTERN):
90-
s.map(lambda x: x, na_action="ignore")
91-
92-
9386
@pytest.mark.parametrize("method", ["apply", "agg", "transform"])
9487
@pytest.mark.parametrize("func", [{"A": {"B": "sum"}}, {"A": {"B": ["sum"]}}])
9588
def test_nested_renamer(frame_or_series, method, func):

pandas/tests/extension/test_datetime.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,8 @@ def test_combine_add(self, data_repeated):
118118

119119
@pytest.mark.parametrize("na_action", [None, "ignore"])
120120
def test_map(self, data, na_action):
121-
if na_action is not None:
122-
with pytest.raises(NotImplementedError, match=""):
123-
data.map(lambda x: x, na_action=na_action)
124-
else:
125-
result = data.map(lambda x: x, na_action=na_action)
126-
self.assert_extension_array_equal(result, data)
121+
result = data.map(lambda x: x, na_action=na_action)
122+
self.assert_extension_array_equal(result, data)
127123

128124

129125
class TestInterface(BaseDatetimeTests, base.BaseInterfaceTests):

pandas/tests/extension/test_period.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,8 @@ def test_diff(self, data, periods):
107107

108108
@pytest.mark.parametrize("na_action", [None, "ignore"])
109109
def test_map(self, data, na_action):
110-
if na_action is not None:
111-
with pytest.raises(NotImplementedError, match=""):
112-
data.map(lambda x: x, na_action=na_action)
113-
else:
114-
result = data.map(lambda x: x, na_action=na_action)
115-
self.assert_extension_array_equal(result, data)
110+
result = data.map(lambda x: x, na_action=na_action)
111+
self.assert_extension_array_equal(result, data)
116112

117113

118114
class TestInterface(BasePeriodTests, base.BaseInterfaceTests):

0 commit comments

Comments
 (0)