Skip to content

Commit 166d857

Browse files
committed
Merge pull request #4998 from mortada/halflife
added halflife to exponentially weighted moving functions
2 parents 0bbae3b + 452d827 commit 166d857

File tree

4 files changed

+44
-24
lines changed

4 files changed

+44
-24
lines changed

doc/source/computation.rst

+8-6
Original file line numberDiff line numberDiff line change
@@ -453,15 +453,16 @@ average as
453453
y_t = (1 - \alpha) y_{t-1} + \alpha x_t
454454
455455
One must have :math:`0 < \alpha \leq 1`, but rather than pass :math:`\alpha`
456-
directly, it's easier to think about either the **span** or **center of mass
457-
(com)** of an EW moment:
456+
directly, it's easier to think about either the **span**, **center of mass
457+
(com)** or **halflife** of an EW moment:
458458

459459
.. math::
460460
461461
\alpha =
462462
\begin{cases}
463463
\frac{2}{s + 1}, s = \text{span}\\
464-
\frac{1}{1 + c}, c = \text{center of mass}
464+
\frac{1}{1 + c}, c = \text{center of mass}\\
465+
1 - \exp^{\frac{\log 0.5}{h}}, h = \text{half life}
465466
\end{cases}
466467
467468
.. note::
@@ -474,11 +475,12 @@ directly, it's easier to think about either the **span** or **center of mass
474475
475476
where :math:`\alpha' = 1 - \alpha`.
476477

477-
You can pass one or the other to these functions but not both. **Span**
478+
You can pass one of the three to these functions but not more. **Span**
478479
corresponds to what is commonly called a "20-day EW moving average" for
479480
example. **Center of mass** has a more physical interpretation. For example,
480-
**span** = 20 corresponds to **com** = 9.5. Here is the list of functions
481-
available:
481+
**span** = 20 corresponds to **com** = 9.5. **Halflife** is the period of
482+
time for the exponential weight to reduce to one half. Here is the list of
483+
functions available:
482484

483485
.. csv-table::
484486
:header: "Function", "Description"

doc/source/release.rst

+2
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ Improvements to existing features
153153
(:issue:`4961`).
154154
- ``concat`` now gives a more informative error message when passed objects
155155
that cannot be concatenated (:issue:`4608`).
156+
- Add ``halflife`` option to exponentially weighted moving functions (PR
157+
:issue:`4998`)
156158

157159
API Changes
158160
~~~~~~~~~~~

pandas/stats/moments.py

+24-18
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
Center of mass: :math:`\alpha = 1 / (1 + com)`,
6060
span : float, optional
6161
Specify decay in terms of span, :math:`\alpha = 2 / (span + 1)`
62+
halflife : float, optional
63+
Specify decay in terms of halflife, :math: `\alpha = 1 - exp(log(0.5) / halflife)`
6264
min_periods : int, default 0
6365
Number of observations in sample to require (only affects
6466
beginning)
@@ -338,25 +340,29 @@ def _process_data_structure(arg, kill_inf=True):
338340
# Exponential moving moments
339341

340342

341-
def _get_center_of_mass(com, span):
342-
if span is not None:
343-
if com is not None:
344-
raise Exception("com and span are mutually exclusive")
343+
def _get_center_of_mass(com, span, halflife):
344+
valid_count = len([x for x in [com, span, halflife] if x is not None])
345+
if valid_count > 1:
346+
raise Exception("com, span, and halflife are mutually exclusive")
345347

348+
if span is not None:
346349
# convert span to center of mass
347350
com = (span - 1) / 2.
348-
351+
elif halflife is not None:
352+
# convert halflife to center of mass
353+
decay = 1 - np.exp(np.log(0.5) / halflife)
354+
com = 1 / decay - 1
349355
elif com is None:
350-
raise Exception("Must pass either com or span")
356+
raise Exception("Must pass one of com, span, or halflife")
351357

352358
return float(com)
353359

354360

355361
@Substitution("Exponentially-weighted moving average", _unary_arg, "")
356362
@Appender(_ewm_doc)
357-
def ewma(arg, com=None, span=None, min_periods=0, freq=None, time_rule=None,
363+
def ewma(arg, com=None, span=None, halflife=None, min_periods=0, freq=None, time_rule=None,
358364
adjust=True):
359-
com = _get_center_of_mass(com, span)
365+
com = _get_center_of_mass(com, span, halflife)
360366
arg = _conv_timerule(arg, freq, time_rule)
361367

362368
def _ewma(v):
@@ -377,9 +383,9 @@ def _first_valid_index(arr):
377383

378384
@Substitution("Exponentially-weighted moving variance", _unary_arg, _bias_doc)
379385
@Appender(_ewm_doc)
380-
def ewmvar(arg, com=None, span=None, min_periods=0, bias=False,
386+
def ewmvar(arg, com=None, span=None, halflife=None, min_periods=0, bias=False,
381387
freq=None, time_rule=None):
382-
com = _get_center_of_mass(com, span)
388+
com = _get_center_of_mass(com, span, halflife)
383389
arg = _conv_timerule(arg, freq, time_rule)
384390
moment2nd = ewma(arg * arg, com=com, min_periods=min_periods)
385391
moment1st = ewma(arg, com=com, min_periods=min_periods)
@@ -393,9 +399,9 @@ def ewmvar(arg, com=None, span=None, min_periods=0, bias=False,
393399

394400
@Substitution("Exponentially-weighted moving std", _unary_arg, _bias_doc)
395401
@Appender(_ewm_doc)
396-
def ewmstd(arg, com=None, span=None, min_periods=0, bias=False,
402+
def ewmstd(arg, com=None, span=None, halflife=None, min_periods=0, bias=False,
397403
time_rule=None):
398-
result = ewmvar(arg, com=com, span=span, time_rule=time_rule,
404+
result = ewmvar(arg, com=com, span=span, halflife=halflife, time_rule=time_rule,
399405
min_periods=min_periods, bias=bias)
400406
return _zsqrt(result)
401407

@@ -404,17 +410,17 @@ def ewmstd(arg, com=None, span=None, min_periods=0, bias=False,
404410

405411
@Substitution("Exponentially-weighted moving covariance", _binary_arg, "")
406412
@Appender(_ewm_doc)
407-
def ewmcov(arg1, arg2, com=None, span=None, min_periods=0, bias=False,
413+
def ewmcov(arg1, arg2, com=None, span=None, halflife=None, min_periods=0, bias=False,
408414
freq=None, time_rule=None):
409415
X, Y = _prep_binary(arg1, arg2)
410416

411417
X = _conv_timerule(X, freq, time_rule)
412418
Y = _conv_timerule(Y, freq, time_rule)
413419

414-
mean = lambda x: ewma(x, com=com, span=span, min_periods=min_periods)
420+
mean = lambda x: ewma(x, com=com, span=span, halflife=halflife, min_periods=min_periods)
415421

416422
result = (mean(X * Y) - mean(X) * mean(Y))
417-
com = _get_center_of_mass(com, span)
423+
com = _get_center_of_mass(com, span, halflife)
418424
if not bias:
419425
result *= (1.0 + 2.0 * com) / (2.0 * com)
420426

@@ -423,15 +429,15 @@ def ewmcov(arg1, arg2, com=None, span=None, min_periods=0, bias=False,
423429

424430
@Substitution("Exponentially-weighted moving " "correlation", _binary_arg, "")
425431
@Appender(_ewm_doc)
426-
def ewmcorr(arg1, arg2, com=None, span=None, min_periods=0,
432+
def ewmcorr(arg1, arg2, com=None, span=None, halflife=None, min_periods=0,
427433
freq=None, time_rule=None):
428434
X, Y = _prep_binary(arg1, arg2)
429435

430436
X = _conv_timerule(X, freq, time_rule)
431437
Y = _conv_timerule(Y, freq, time_rule)
432438

433-
mean = lambda x: ewma(x, com=com, span=span, min_periods=min_periods)
434-
var = lambda x: ewmvar(x, com=com, span=span, min_periods=min_periods,
439+
mean = lambda x: ewma(x, com=com, span=span, halflife=halflife, min_periods=min_periods)
440+
var = lambda x: ewmvar(x, com=com, span=span, halflife=halflife, min_periods=min_periods,
435441
bias=True)
436442
return (mean(X * Y) - mean(X) * mean(Y)) / _zsqrt(var(X) * var(Y))
437443

pandas/stats/tests/test_moments.py

+10
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,16 @@ def test_ewma_span_com_args(self):
535535
self.assertRaises(Exception, mom.ewma, self.arr, com=9.5, span=20)
536536
self.assertRaises(Exception, mom.ewma, self.arr)
537537

538+
def test_ewma_halflife_arg(self):
539+
A = mom.ewma(self.arr, com=13.932726172912965)
540+
B = mom.ewma(self.arr, halflife=10.0)
541+
assert_almost_equal(A, B)
542+
543+
self.assertRaises(Exception, mom.ewma, self.arr, span=20, halflife=50)
544+
self.assertRaises(Exception, mom.ewma, self.arr, com=9.5, halflife=50)
545+
self.assertRaises(Exception, mom.ewma, self.arr, com=9.5, span=20, halflife=50)
546+
self.assertRaises(Exception, mom.ewma, self.arr)
547+
538548
def test_ew_empty_arrays(self):
539549
arr = np.array([], dtype=np.float64)
540550

0 commit comments

Comments
 (0)