Skip to content

Commit 859a787

Browse files
authored
PERF: EWMA with times (#40072)
1 parent bc46620 commit 859a787

File tree

4 files changed

+24
-79
lines changed

4 files changed

+24
-79
lines changed

asv_bench/benchmarks/rolling.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def time_ewm(self, constructor, window, dtype, method):
114114
getattr(self.ewm, method)()
115115

116116
def time_ewm_times(self, constructor, window, dtype, method):
117-
self.ewm.mean()
117+
self.ewm_times.mean()
118118

119119

120120
class VariableWindowMethods(Methods):

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ Performance improvements
270270
- Performance improvement in :func:`unique` for object data type (:issue:`37615`)
271271
- Performance improvement in :class:`core.window.rolling.ExpandingGroupby` aggregation methods (:issue:`39664`)
272272
- Performance improvement in :class:`Styler` where render times are more than 50% reduced (:issue:`39972` :issue:`39952`)
273+
- Performance improvement in :meth:`core.window.ewm.ExponentialMovingWindow.mean` with ``times`` (:issue:`39784`)
273274

274275
.. ---------------------------------------------------------------------------
275276

pandas/_libs/window/aggregations.pyx

+7-65
Original file line numberDiff line numberDiff line change
@@ -1473,66 +1473,9 @@ def roll_weighted_var(const float64_t[:] values, const float64_t[:] weights,
14731473
# ----------------------------------------------------------------------
14741474
# Exponentially weighted moving average
14751475

1476-
def ewma_time(const float64_t[:] vals, int64_t[:] start, int64_t[:] end,
1477-
int minp, ndarray[int64_t] times, int64_t halflife):
1478-
"""
1479-
Compute exponentially-weighted moving average using halflife and time
1480-
distances.
1481-
1482-
Parameters
1483-
----------
1484-
vals : ndarray[float_64]
1485-
start: ndarray[int_64]
1486-
end: ndarray[int_64]
1487-
minp : int
1488-
times : ndarray[int64]
1489-
halflife : int64
1490-
1491-
Returns
1492-
-------
1493-
ndarray
1494-
"""
1495-
cdef:
1496-
Py_ssize_t i, j, num_not_nan = 0, N = len(vals)
1497-
bint is_not_nan
1498-
float64_t last_result, weights_dot, weights_sum, weight, halflife_float
1499-
float64_t[:] times_float
1500-
float64_t[:] observations = np.zeros(N, dtype=float)
1501-
float64_t[:] times_masked = np.zeros(N, dtype=float)
1502-
ndarray[float64_t] output = np.empty(N, dtype=float)
1503-
1504-
if N == 0:
1505-
return output
1506-
1507-
halflife_float = <float64_t>halflife
1508-
times_float = times.astype(float)
1509-
last_result = vals[0]
1510-
1511-
with nogil:
1512-
for i in range(N):
1513-
is_not_nan = vals[i] == vals[i]
1514-
num_not_nan += is_not_nan
1515-
if is_not_nan:
1516-
times_masked[num_not_nan-1] = times_float[i]
1517-
observations[num_not_nan-1] = vals[i]
1518-
1519-
weights_sum = 0
1520-
weights_dot = 0
1521-
for j in range(num_not_nan):
1522-
weight = 0.5 ** (
1523-
(times_float[i] - times_masked[j]) / halflife_float)
1524-
weights_sum += weight
1525-
weights_dot += weight * observations[j]
1526-
1527-
last_result = weights_dot / weights_sum
1528-
1529-
output[i] = last_result if num_not_nan >= minp else NaN
1530-
1531-
return output
1532-
1533-
15341476
def ewma(float64_t[:] vals, int64_t[:] start, int64_t[:] end, int minp,
1535-
float64_t com, bint adjust, bint ignore_na):
1477+
float64_t com, bint adjust, bint ignore_na, float64_t[:] times,
1478+
float64_t halflife):
15361479
"""
15371480
Compute exponentially-weighted moving average using center-of-mass.
15381481
@@ -1555,13 +1498,15 @@ def ewma(float64_t[:] vals, int64_t[:] start, int64_t[:] end, int minp,
15551498
Py_ssize_t i, j, s, e, nobs, win_size, N = len(vals), M = len(start)
15561499
float64_t[:] sub_vals
15571500
ndarray[float64_t] sub_output, output = np.empty(N, dtype=float)
1558-
float64_t alpha, old_wt_factor, new_wt, weighted_avg, old_wt, cur
1501+
float64_t alpha, old_wt_factor, new_wt, weighted_avg, old_wt, cur, delta
15591502
bint is_observation
15601503

15611504
if N == 0:
15621505
return output
15631506

15641507
alpha = 1. / (1. + com)
1508+
old_wt_factor = 1. - alpha
1509+
new_wt = 1. if adjust else alpha
15651510

15661511
for j in range(M):
15671512
s = start[j]
@@ -1570,9 +1515,6 @@ def ewma(float64_t[:] vals, int64_t[:] start, int64_t[:] end, int minp,
15701515
win_size = len(sub_vals)
15711516
sub_output = np.empty(win_size, dtype=float)
15721517

1573-
old_wt_factor = 1. - alpha
1574-
new_wt = 1. if adjust else alpha
1575-
15761518
weighted_avg = sub_vals[0]
15771519
is_observation = weighted_avg == weighted_avg
15781520
nobs = int(is_observation)
@@ -1587,8 +1529,8 @@ def ewma(float64_t[:] vals, int64_t[:] start, int64_t[:] end, int minp,
15871529
if weighted_avg == weighted_avg:
15881530

15891531
if is_observation or not ignore_na:
1590-
1591-
old_wt *= old_wt_factor
1532+
delta = times[i] - times[i - 1]
1533+
old_wt *= old_wt_factor ** (delta / halflife)
15921534
if is_observation:
15931535

15941536
# avoid numerical errors on constant series

pandas/core/window/ewm.py

+15-13
Original file line numberDiff line numberDiff line change
@@ -335,20 +335,22 @@ def aggregate(self, func, *args, **kwargs):
335335
def mean(self, *args, **kwargs):
336336
nv.validate_window_func("mean", args, kwargs)
337337
if self.times is not None:
338-
window_func = window_aggregations.ewma_time
339-
window_func = partial(
340-
window_func,
341-
times=self.times,
342-
halflife=self.halflife,
343-
)
338+
com = 1.0
339+
times = self.times.astype(np.float64)
340+
halflife = float(self.halflife)
344341
else:
345-
window_func = window_aggregations.ewma
346-
window_func = partial(
347-
window_func,
348-
com=self.com,
349-
adjust=self.adjust,
350-
ignore_na=self.ignore_na,
351-
)
342+
com = self.com
343+
times = np.arange(len(self.obj), dtype=np.float64)
344+
halflife = 1.0
345+
window_func = window_aggregations.ewma
346+
window_func = partial(
347+
window_func,
348+
com=com,
349+
adjust=self.adjust,
350+
ignore_na=self.ignore_na,
351+
times=times,
352+
halflife=halflife,
353+
)
352354
return self._apply(window_func)
353355

354356
@doc(

0 commit comments

Comments
 (0)