diff --git a/doc/source/computation.rst b/doc/source/computation.rst index 207e2796c468d..85c6b88d740da 100644 --- a/doc/source/computation.rst +++ b/doc/source/computation.rst @@ -453,15 +453,16 @@ average as y_t = (1 - \alpha) y_{t-1} + \alpha x_t One must have :math:`0 < \alpha \leq 1`, but rather than pass :math:`\alpha` -directly, it's easier to think about either the **span** or **center of mass -(com)** of an EW moment: +directly, it's easier to think about either the **span**, **center of mass +(com)** or **halflife** of an EW moment: .. math:: \alpha = \begin{cases} \frac{2}{s + 1}, s = \text{span}\\ - \frac{1}{1 + c}, c = \text{center of mass} + \frac{1}{1 + c}, c = \text{center of mass}\\ + 1 - \exp^{\frac{\log 0.5}{h}}, h = \text{half life} \end{cases} .. note:: @@ -474,11 +475,12 @@ directly, it's easier to think about either the **span** or **center of mass where :math:`\alpha' = 1 - \alpha`. -You can pass one or the other to these functions but not both. **Span** +You can pass one of the three to these functions but not more. **Span** corresponds to what is commonly called a "20-day EW moving average" for example. **Center of mass** has a more physical interpretation. For example, -**span** = 20 corresponds to **com** = 9.5. Here is the list of functions -available: +**span** = 20 corresponds to **com** = 9.5. **Halflife** is the period of +time for the exponential weight to reduce to one half. Here is the list of +functions available: .. csv-table:: :header: "Function", "Description" diff --git a/doc/source/release.rst b/doc/source/release.rst index 66c3dcd203a6a..cd1cd669152ec 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -138,6 +138,8 @@ Improvements to existing features (:issue:`4961`). - ``concat`` now gives a more informative error message when passed objects that cannot be concatenated (:issue:`4608`). + - Add ``halflife`` option to exponentially weighted moving functions (PR + :issue:`4998`) API Changes ~~~~~~~~~~~ diff --git a/pandas/stats/moments.py b/pandas/stats/moments.py index fd81bd119fe09..f3ec3880ec8b5 100644 --- a/pandas/stats/moments.py +++ b/pandas/stats/moments.py @@ -59,6 +59,8 @@ Center of mass: :math:`\alpha = 1 / (1 + com)`, span : float, optional Specify decay in terms of span, :math:`\alpha = 2 / (span + 1)` +halflife : float, optional + Specify decay in terms of halflife, :math: `\alpha = 1 - exp(log(0.5) / halflife)` min_periods : int, default 0 Number of observations in sample to require (only affects beginning) @@ -338,25 +340,29 @@ def _process_data_structure(arg, kill_inf=True): # Exponential moving moments -def _get_center_of_mass(com, span): - if span is not None: - if com is not None: - raise Exception("com and span are mutually exclusive") +def _get_center_of_mass(com, span, halflife): + valid_count = len([x for x in [com, span, halflife] if x is not None]) + if valid_count > 1: + raise Exception("com, span, and halflife are mutually exclusive") + if span is not None: # convert span to center of mass com = (span - 1) / 2. - + elif halflife is not None: + # convert halflife to center of mass + decay = 1 - np.exp(np.log(0.5) / halflife) + com = 1 / decay - 1 elif com is None: - raise Exception("Must pass either com or span") + raise Exception("Must pass one of com, span, or halflife") return float(com) @Substitution("Exponentially-weighted moving average", _unary_arg, "") @Appender(_ewm_doc) -def ewma(arg, com=None, span=None, min_periods=0, freq=None, time_rule=None, +def ewma(arg, com=None, span=None, halflife=None, min_periods=0, freq=None, time_rule=None, adjust=True): - com = _get_center_of_mass(com, span) + com = _get_center_of_mass(com, span, halflife) arg = _conv_timerule(arg, freq, time_rule) def _ewma(v): @@ -377,9 +383,9 @@ def _first_valid_index(arr): @Substitution("Exponentially-weighted moving variance", _unary_arg, _bias_doc) @Appender(_ewm_doc) -def ewmvar(arg, com=None, span=None, min_periods=0, bias=False, +def ewmvar(arg, com=None, span=None, halflife=None, min_periods=0, bias=False, freq=None, time_rule=None): - com = _get_center_of_mass(com, span) + com = _get_center_of_mass(com, span, halflife) arg = _conv_timerule(arg, freq, time_rule) moment2nd = ewma(arg * arg, com=com, min_periods=min_periods) 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, @Substitution("Exponentially-weighted moving std", _unary_arg, _bias_doc) @Appender(_ewm_doc) -def ewmstd(arg, com=None, span=None, min_periods=0, bias=False, +def ewmstd(arg, com=None, span=None, halflife=None, min_periods=0, bias=False, time_rule=None): - result = ewmvar(arg, com=com, span=span, time_rule=time_rule, + result = ewmvar(arg, com=com, span=span, halflife=halflife, time_rule=time_rule, min_periods=min_periods, bias=bias) return _zsqrt(result) @@ -404,17 +410,17 @@ def ewmstd(arg, com=None, span=None, min_periods=0, bias=False, @Substitution("Exponentially-weighted moving covariance", _binary_arg, "") @Appender(_ewm_doc) -def ewmcov(arg1, arg2, com=None, span=None, min_periods=0, bias=False, +def ewmcov(arg1, arg2, com=None, span=None, halflife=None, min_periods=0, bias=False, freq=None, time_rule=None): X, Y = _prep_binary(arg1, arg2) X = _conv_timerule(X, freq, time_rule) Y = _conv_timerule(Y, freq, time_rule) - mean = lambda x: ewma(x, com=com, span=span, min_periods=min_periods) + mean = lambda x: ewma(x, com=com, span=span, halflife=halflife, min_periods=min_periods) result = (mean(X * Y) - mean(X) * mean(Y)) - com = _get_center_of_mass(com, span) + com = _get_center_of_mass(com, span, halflife) if not bias: result *= (1.0 + 2.0 * com) / (2.0 * com) @@ -423,15 +429,15 @@ def ewmcov(arg1, arg2, com=None, span=None, min_periods=0, bias=False, @Substitution("Exponentially-weighted moving " "correlation", _binary_arg, "") @Appender(_ewm_doc) -def ewmcorr(arg1, arg2, com=None, span=None, min_periods=0, +def ewmcorr(arg1, arg2, com=None, span=None, halflife=None, min_periods=0, freq=None, time_rule=None): X, Y = _prep_binary(arg1, arg2) X = _conv_timerule(X, freq, time_rule) Y = _conv_timerule(Y, freq, time_rule) - mean = lambda x: ewma(x, com=com, span=span, min_periods=min_periods) - var = lambda x: ewmvar(x, com=com, span=span, min_periods=min_periods, + mean = lambda x: ewma(x, com=com, span=span, halflife=halflife, min_periods=min_periods) + var = lambda x: ewmvar(x, com=com, span=span, halflife=halflife, min_periods=min_periods, bias=True) return (mean(X * Y) - mean(X) * mean(Y)) / _zsqrt(var(X) * var(Y)) diff --git a/pandas/stats/tests/test_moments.py b/pandas/stats/tests/test_moments.py index 70653d9d96bef..1f7df9894a97d 100644 --- a/pandas/stats/tests/test_moments.py +++ b/pandas/stats/tests/test_moments.py @@ -535,6 +535,16 @@ def test_ewma_span_com_args(self): self.assertRaises(Exception, mom.ewma, self.arr, com=9.5, span=20) self.assertRaises(Exception, mom.ewma, self.arr) + def test_ewma_halflife_arg(self): + A = mom.ewma(self.arr, com=13.932726172912965) + B = mom.ewma(self.arr, halflife=10.0) + assert_almost_equal(A, B) + + self.assertRaises(Exception, mom.ewma, self.arr, span=20, halflife=50) + self.assertRaises(Exception, mom.ewma, self.arr, com=9.5, halflife=50) + self.assertRaises(Exception, mom.ewma, self.arr, com=9.5, span=20, halflife=50) + self.assertRaises(Exception, mom.ewma, self.arr) + def test_ew_empty_arrays(self): arr = np.array([], dtype=np.float64)