1
+ import datetime
1
2
from functools import partial
2
3
from textwrap import dedent
3
4
from typing import Optional , Union
4
5
5
6
import numpy as np
6
7
8
+ from pandas ._libs .tslibs import Timedelta
7
9
import pandas ._libs .window .aggregations as window_aggregations
8
- from pandas ._typing import FrameOrSeries
10
+ from pandas ._typing import FrameOrSeries , TimedeltaConvertibleTypes
9
11
from pandas .compat .numpy import function as nv
10
12
from pandas .util ._decorators import Appender , Substitution , doc
11
13
14
+ from pandas .core .dtypes .common import is_datetime64_ns_dtype
12
15
from pandas .core .dtypes .generic import ABCDataFrame
13
16
14
17
from pandas .core .base import DataError
15
- import pandas .core .common as com
18
+ import pandas .core .common as common
16
19
from pandas .core .window .common import _doc_template , _shared_docs , zsqrt
17
20
from pandas .core .window .rolling import _flex_binary_moment , _Rolling
18
21
@@ -32,7 +35,7 @@ def get_center_of_mass(
32
35
halflife : Optional [float ],
33
36
alpha : Optional [float ],
34
37
) -> float :
35
- valid_count = com .count_not_none (comass , span , halflife , alpha )
38
+ valid_count = common .count_not_none (comass , span , halflife , alpha )
36
39
if valid_count > 1 :
37
40
raise ValueError ("comass, span, halflife, and alpha are mutually exclusive" )
38
41
@@ -76,10 +79,17 @@ class ExponentialMovingWindow(_Rolling):
76
79
span : float, optional
77
80
Specify decay in terms of span,
78
81
:math:`\alpha = 2 / (span + 1)`, for :math:`span \geq 1`.
79
- halflife : float, optional
82
+ halflife : float, str, timedelta, optional
80
83
Specify decay in terms of half-life,
81
84
:math:`\alpha = 1 - \exp\left(-\ln(2) / halflife\right)`, for
82
85
:math:`halflife > 0`.
86
+
87
+ If ``times`` is specified, the time unit (str or timedelta) over which an
88
+ observation decays to half its value. Only applicable to ``mean()``
89
+ and halflife value will not apply to the other functions.
90
+
91
+ .. versionadded:: 1.1.0
92
+
83
93
alpha : float, optional
84
94
Specify smoothing factor :math:`\alpha` directly,
85
95
:math:`0 < \alpha \leq 1`.
@@ -124,6 +134,18 @@ class ExponentialMovingWindow(_Rolling):
124
134
axis : {0, 1}, default 0
125
135
The axis to use. The value 0 identifies the rows, and 1
126
136
identifies the columns.
137
+ times : str, np.ndarray, Series, default None
138
+
139
+ .. versionadded:: 1.1.0
140
+
141
+ Times corresponding to the observations. Must be monotonically increasing and
142
+ ``datetime64[ns]`` dtype.
143
+
144
+ If str, the name of the column in the DataFrame representing the times.
145
+
146
+ If 1-D array like, a sequence with the same shape as the observations.
147
+
148
+ Only applicable to ``mean()``.
127
149
128
150
Returns
129
151
-------
@@ -159,6 +181,17 @@ class ExponentialMovingWindow(_Rolling):
159
181
2 1.615385
160
182
3 1.615385
161
183
4 3.670213
184
+
185
+ Specifying ``times`` with a timedelta ``halflife`` when computing mean.
186
+
187
+ >>> times = ['2020-01-01', '2020-01-03', '2020-01-10', '2020-01-15', '2020-01-17']
188
+ >>> df.ewm(halflife='4 days', times=pd.DatetimeIndex(times)).mean()
189
+ B
190
+ 0 0.000000
191
+ 1 0.585786
192
+ 2 1.523889
193
+ 3 1.523889
194
+ 4 3.233686
162
195
"""
163
196
164
197
_attributes = ["com" , "min_periods" , "adjust" , "ignore_na" , "axis" ]
@@ -168,20 +201,49 @@ def __init__(
168
201
obj ,
169
202
com : Optional [float ] = None ,
170
203
span : Optional [float ] = None ,
171
- halflife : Optional [float ] = None ,
204
+ halflife : Optional [Union [ float , TimedeltaConvertibleTypes ] ] = None ,
172
205
alpha : Optional [float ] = None ,
173
206
min_periods : int = 0 ,
174
207
adjust : bool = True ,
175
208
ignore_na : bool = False ,
176
209
axis : int = 0 ,
210
+ times : Optional [Union [str , np .ndarray , FrameOrSeries ]] = None ,
177
211
):
212
+ self .com : Optional [float ]
178
213
self .obj = obj
179
- self .com = get_center_of_mass (com , span , halflife , alpha )
180
214
self .min_periods = max (int (min_periods ), 1 )
181
215
self .adjust = adjust
182
216
self .ignore_na = ignore_na
183
217
self .axis = axis
184
218
self .on = None
219
+ if times is not None :
220
+ if isinstance (times , str ):
221
+ times = self ._selected_obj [times ]
222
+ if not is_datetime64_ns_dtype (times ):
223
+ raise ValueError ("times must be datetime64[ns] dtype." )
224
+ if len (times ) != len (obj ):
225
+ raise ValueError ("times must be the same length as the object." )
226
+ if not isinstance (halflife , (str , datetime .timedelta )):
227
+ raise ValueError (
228
+ "halflife must be a string or datetime.timedelta object"
229
+ )
230
+ self .times = np .asarray (times .astype (np .int64 ))
231
+ self .halflife = Timedelta (halflife ).value
232
+ # Halflife is no longer applicable when calculating COM
233
+ # But allow COM to still be calculated if the user passes other decay args
234
+ if common .count_not_none (com , span , alpha ) > 0 :
235
+ self .com = get_center_of_mass (com , span , None , alpha )
236
+ else :
237
+ self .com = None
238
+ else :
239
+ if halflife is not None and isinstance (halflife , (str , datetime .timedelta )):
240
+ raise ValueError (
241
+ "halflife can only be a timedelta convertible argument if "
242
+ "times is not None."
243
+ )
244
+ self .times = None
245
+ self .halflife = None
246
+ self .com = get_center_of_mass (com , span , halflife , alpha )
185
247
186
248
@property
187
249
def _constructor (self ):
@@ -277,14 +339,23 @@ def mean(self, *args, **kwargs):
277
339
Arguments and keyword arguments to be passed into func.
278
340
"""
279
341
nv .validate_window_func ("mean" , args , kwargs )
280
- window_func = self ._get_roll_func ("ewma" )
281
- window_func = partial (
282
- window_func ,
283
- com = self .com ,
284
- adjust = self .adjust ,
285
- ignore_na = self .ignore_na ,
286
- minp = self .min_periods ,
287
- )
342
+ if self .times is not None :
343
+ window_func = self ._get_roll_func ("ewma_time" )
344
+ window_func = partial (
345
+ window_func ,
346
+ minp = self .min_periods ,
347
+ times = self .times ,
348
+ halflife = self .halflife ,
349
+ )
350
+ else :
351
+ window_func = self ._get_roll_func ("ewma" )
352
+ window_func = partial (
353
+ window_func ,
354
+ com = self .com ,
355
+ adjust = self .adjust ,
356
+ ignore_na = self .ignore_na ,
357
+ minp = self .min_periods ,
358
+ )
288
359
return self ._apply (window_func )
289
360
290
361
@Substitution (name = "ewm" , func_name = "std" )
0 commit comments