Skip to content

Commit bd405e8

Browse files
authored
ENH: Allow adjust=False when times is provided (#59142)
* add adjust parameter to the ewma variable times test. Add tests for disallowed decay-specification parameters when times is specified and adjust=False * allow adjust=False when times is provided * re-calculate alpha each iteration for irregular-spaced time series * whatsnew entry for allowing adjust=False with times * pre-commit style fixes * reduce line lengths to comply with pre-commit * reduce line lengths and apply ruff-reformat changes
1 parent 6090042 commit bd405e8

File tree

5 files changed

+72
-10
lines changed

5 files changed

+72
-10
lines changed

doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Other enhancements
4242
- :func:`DataFrame.to_excel` argument ``merge_cells`` now accepts a value of ``"columns"`` to only merge :class:`MultiIndex` column header header cells (:issue:`35384`)
4343
- :meth:`DataFrame.corrwith` now accepts ``min_periods`` as optional arguments, as in :meth:`DataFrame.corr` and :meth:`Series.corr` (:issue:`9490`)
4444
- :meth:`DataFrame.cummin`, :meth:`DataFrame.cummax`, :meth:`DataFrame.cumprod` and :meth:`DataFrame.cumsum` methods now have a ``numeric_only`` parameter (:issue:`53072`)
45+
- :meth:`DataFrame.ewm` now allows ``adjust=False`` when ``times`` is provided (:issue:`54328`)
4546
- :meth:`DataFrame.fillna` and :meth:`Series.fillna` can now accept ``value=None``; for non-object dtype the corresponding NA value will be used (:issue:`57723`)
4647
- :meth:`DataFrame.pivot_table` and :func:`pivot_table` now allow the passing of keyword arguments to ``aggfunc`` through ``**kwargs`` (:issue:`57884`)
4748
- :meth:`Series.cummin` and :meth:`Series.cummax` now supports :class:`CategoricalDtype` (:issue:`52335`)

pandas/_libs/window/aggregations.pyx

+3
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,9 @@ def ewm(const float64_t[:] vals, const int64_t[:] start, const int64_t[:] end,
18131813
if normalize:
18141814
# avoid numerical errors on constant series
18151815
if weighted != cur:
1816+
if not adjust and com == 1:
1817+
# update in case of irregular-interval series
1818+
new_wt = 1. - old_wt
18161819
weighted = old_wt * weighted + new_wt * cur
18171820
weighted /= (old_wt + new_wt)
18181821
if adjust:

pandas/core/window/ewm.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,10 @@ class ExponentialMovingWindow(BaseWindow):
134134
Provide exponentially weighted (EW) calculations.
135135
136136
Exactly one of ``com``, ``span``, ``halflife``, or ``alpha`` must be
137-
provided if ``times`` is not provided. If ``times`` is provided,
137+
provided if ``times`` is not provided. If ``times`` is provided and ``adjust=True``,
138138
``halflife`` and one of ``com``, ``span`` or ``alpha`` may be provided.
139+
If ``times`` is provided and ``adjust=False``, ``halflife`` must be the only
140+
provided decay-specification parameter.
139141
140142
Parameters
141143
----------
@@ -358,8 +360,6 @@ def __init__(
358360
self.ignore_na = ignore_na
359361
self.times = times
360362
if self.times is not None:
361-
if not self.adjust:
362-
raise NotImplementedError("times is not supported with adjust=False.")
363363
times_dtype = getattr(self.times, "dtype", None)
364364
if not (
365365
is_datetime64_dtype(times_dtype)
@@ -376,6 +376,11 @@ def __init__(
376376
# Halflife is no longer applicable when calculating COM
377377
# But allow COM to still be calculated if the user passes other decay args
378378
if common.count_not_none(self.com, self.span, self.alpha) > 0:
379+
if not self.adjust:
380+
raise NotImplementedError(
381+
"None of com, span, or alpha can be specified if "
382+
"times is provided and adjust=False"
383+
)
379384
self._com = get_center_of_mass(self.com, self.span, None, self.alpha)
380385
else:
381386
self._com = 1.0

pandas/core/window/numba_.py

+6
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ def ewm(
149149
# note that len(deltas) = len(vals) - 1 and deltas[i]
150150
# is to be used in conjunction with vals[i+1]
151151
old_wt *= old_wt_factor ** deltas[start + j - 1]
152+
if not adjust and com == 1:
153+
# update in case of irregular-interval time series
154+
new_wt = 1.0 - old_wt
152155
else:
153156
weighted = old_wt_factor * weighted
154157
if is_observation:
@@ -324,6 +327,9 @@ def ewm_table(
324327
# note that len(deltas) = len(vals) - 1 and deltas[i]
325328
# is to be used in conjunction with vals[i+1]
326329
old_wt[j] *= old_wt_factor ** deltas[i - 1]
330+
if not adjust and com == 1:
331+
# update in case of irregular-interval time series
332+
new_wt = 1.0 - old_wt[j]
327333
else:
328334
weighted[j] = old_wt_factor * weighted[j]
329335
if is_observations[j]:

pandas/tests/window/test_ewm.py

+54-7
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ def test_ewma_with_times_equal_spacing(halflife_with_times, times, min_periods):
102102
tm.assert_frame_equal(result, expected)
103103

104104

105-
def test_ewma_with_times_variable_spacing(tz_aware_fixture, unit):
105+
def test_ewma_with_times_variable_spacing(tz_aware_fixture, unit, adjust):
106+
# GH 54328
106107
tz = tz_aware_fixture
107108
halflife = "23 days"
108109
times = (
@@ -112,8 +113,11 @@ def test_ewma_with_times_variable_spacing(tz_aware_fixture, unit):
112113
)
113114
data = np.arange(3)
114115
df = DataFrame(data)
115-
result = df.ewm(halflife=halflife, times=times).mean()
116-
expected = DataFrame([0.0, 0.5674161888241773, 1.545239952073459])
116+
result = df.ewm(halflife=halflife, times=times, adjust=adjust).mean()
117+
if adjust:
118+
expected = DataFrame([0.0, 0.5674161888241773, 1.545239952073459])
119+
else:
120+
expected = DataFrame([0.0, 0.23762518642226227, 1.534926369128742])
117121
tm.assert_frame_equal(result, expected)
118122

119123

@@ -148,13 +152,56 @@ def test_ewm_getitem_attributes_retained(arg, adjust, ignore_na):
148152
assert result == expected
149153

150154

151-
def test_ewma_times_adjust_false_raises():
152-
# GH 40098
155+
def test_ewma_times_adjust_false_with_disallowed_com():
156+
# GH 54328
157+
with pytest.raises(
158+
NotImplementedError,
159+
match=(
160+
"None of com, span, or alpha can be specified "
161+
"if times is provided and adjust=False"
162+
),
163+
):
164+
Series(range(1)).ewm(
165+
0.1,
166+
adjust=False,
167+
times=date_range("2000", freq="D", periods=1),
168+
halflife="1D",
169+
)
170+
171+
172+
def test_ewma_times_adjust_false_with_disallowed_alpha():
173+
# GH 54328
153174
with pytest.raises(
154-
NotImplementedError, match="times is not supported with adjust=False."
175+
NotImplementedError,
176+
match=(
177+
"None of com, span, or alpha can be specified "
178+
"if times is provided and adjust=False"
179+
),
180+
):
181+
Series(range(1)).ewm(
182+
0.1,
183+
adjust=False,
184+
times=date_range("2000", freq="D", periods=1),
185+
alpha=0.5,
186+
halflife="1D",
187+
)
188+
189+
190+
def test_ewma_times_adjust_false_with_disallowed_span():
191+
# GH 54328
192+
with pytest.raises(
193+
NotImplementedError,
194+
match=(
195+
"None of com, span, or alpha can be specified "
196+
"if times is provided and adjust=False"
197+
),
155198
):
156199
Series(range(1)).ewm(
157-
0.1, adjust=False, times=date_range("2000", freq="D", periods=1)
200+
0.1,
201+
adjust=False,
202+
times=date_range("2000", freq="D", periods=1),
203+
span=10,
204+
halflife="1D",
158205
)
159206

160207

0 commit comments

Comments
 (0)