Skip to content

added halflife to exponentially weighted moving functions #4998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 29, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions doc/source/computation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand All @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions doc/source/release.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~
Expand Down
42 changes: 24 additions & 18 deletions pandas/stats/moments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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))

Expand Down
10 changes: 10 additions & 0 deletions pandas/stats/tests/test_moments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down