1from __future__ import annotations
2
3import datetime
4from functools import partial
5from typing import (
6 TYPE_CHECKING,
7 cast,
8)
9
10import numpy as np
11
12from pandas._libs.tslibs import Timedelta
13import pandas._libs.window.aggregations as window_aggregations
14from pandas.util._decorators import set_module
15
16from pandas.core.dtypes.common import (
17 is_datetime64_dtype,
18 is_numeric_dtype,
19)
20from pandas.core.dtypes.dtypes import DatetimeTZDtype
21from pandas.core.dtypes.generic import ABCSeries
22from pandas.core.dtypes.missing import isna
23
24from pandas.core import common
25from pandas.core.arrays.datetimelike import dtype_to_unit
26from pandas.core.indexers.objects import (
27 BaseIndexer,
28 ExponentialMovingWindowIndexer,
29 GroupByIndexer,
30)
31from pandas.core.util.numba_ import (
32 get_jit_arguments,
33 maybe_use_numba,
34)
35from pandas.core.window.common import zsqrt
36from pandas.core.window.numba_ import (
37 generate_numba_ewm_func,
38 generate_numba_ewm_table_func,
39)
40from pandas.core.window.online import (
41 EWMMeanState,
42 generate_online_numba_ewma_func,
43)
44from pandas.core.window.rolling import (
45 BaseWindow,
46 BaseWindowGroupBy,
47)
48
49if TYPE_CHECKING:
50 from pandas._typing import (
51 TimedeltaConvertibleTypes,
52 TimeUnit,
53 npt,
54 )
55
56 from pandas import (
57 DataFrame,
58 Series,
59 )
60 from pandas.core.generic import NDFrame
61
62
63def get_center_of_mass(
64 comass: float | None,
65 span: float | None,
66 halflife: float | None,
67 alpha: float | None,
68) -> float:
69 valid_count = common.count_not_none(comass, span, halflife, alpha)
70 if valid_count > 1:
71 raise ValueError("comass, span, halflife, and alpha are mutually exclusive")
72
73 # Convert to center of mass; domain checks ensure 0 < alpha <= 1
74 if comass is not None:
75 if comass < 0:
76 raise ValueError("comass must satisfy: comass >= 0")
77 elif span is not None:
78 if span < 1:
79 raise ValueError("span must satisfy: span >= 1")
80 comass = (span - 1) / 2
81 elif halflife is not None:
82 if halflife <= 0:
83 raise ValueError("halflife must satisfy: halflife > 0")
84 decay = 1 - np.exp(np.log(0.5) / halflife)
85 comass = 1 / decay - 1
86 elif alpha is not None:
87 if alpha <= 0 or alpha > 1:
88 raise ValueError("alpha must satisfy: 0 < alpha <= 1")
89 comass = (1 - alpha) / alpha
90 else:
91 raise ValueError("Must pass one of comass, span, halflife, or alpha")
92
93 return float(comass)
94
95
96def _calculate_deltas(
97 times: np.ndarray | NDFrame,
98 halflife: float | TimedeltaConvertibleTypes | None,
99) -> npt.NDArray[np.float64]:
100 """
101 Return the diff of the times divided by the half-life. These values are used in
102 the calculation of the ewm mean.
103
104 Parameters
105 ----------
106 times : np.ndarray, Series
107 Times corresponding to the observations. Must be monotonically increasing
108 and ``datetime64[ns]`` dtype.
109 halflife : float, str, timedelta, optional
110 Half-life specifying the decay
111
112 Returns
113 -------
114 np.ndarray
115 Diff of the times divided by the half-life
116 """
117 unit = dtype_to_unit(times.dtype)
118 unit = cast("TimeUnit", unit)
119 if isinstance(times, ABCSeries):
120 times = times._values
121 _times = np.asarray(times.view(np.int64), dtype=np.float64)
122 _halflife = float(Timedelta(halflife).as_unit(unit)._value)
123 return np.diff(_times) / _halflife
124
125
126@set_module("pandas.api.typing")
127class ExponentialMovingWindow(BaseWindow):
128 r"""
129 Provide exponentially weighted (EW) calculations.
130
131 Exactly one of ``com``, ``span``, ``halflife``, or ``alpha`` must be
132 provided if ``times`` is not provided. If ``times`` is provided and ``adjust=True``,
133 ``halflife`` and one of ``com``, ``span`` or ``alpha`` may be provided.
134 If ``times`` is provided and ``adjust=False``, ``halflife`` must be the only
135 provided decay-specification parameter.
136
137 Parameters
138 ----------
139 com : float, optional
140 Specify decay in terms of center of mass
141
142 :math:`\alpha = 1 / (1 + com)`, for :math:`com \geq 0`.
143
144 span : float, optional
145 Specify decay in terms of span
146
147 :math:`\alpha = 2 / (span + 1)`, for :math:`span \geq 1`.
148
149 halflife : float, str, timedelta, optional
150 Specify decay in terms of half-life
151
152 :math:`\alpha = 1 - \exp\left(-\ln(2) / halflife\right)`, for
153 :math:`halflife > 0`.
154
155 If ``times`` is specified, a timedelta convertible unit over which an
156 observation decays to half its value. Only applicable to ``mean()``,
157 and halflife value will not apply to the other functions.
158
159 alpha : float, optional
160 Specify smoothing factor :math:`\alpha` directly
161
162 :math:`0 < \alpha \leq 1`.
163
164 min_periods : int, default 0
165 Minimum number of observations in window required to have a value;
166 otherwise, result is ``np.nan``.
167
168 adjust : bool, default True
169 Divide by decaying adjustment factor in beginning periods to account
170 for imbalance in relative weightings (viewing EWMA as a moving average).
171
172 - When ``adjust=True`` (default), the EW function is calculated using weights
173 :math:`w_i = (1 - \alpha)^i`. For example, the EW moving average of the series
174 [:math:`x_0, x_1, ..., x_t`] would be:
175
176 .. math::
177 y_t = \frac{x_t + (1 - \alpha)x_{t-1} + (1 - \alpha)^2 x_{t-2} + ... + (1 -
178 \alpha)^t x_0}{1 + (1 - \alpha) + (1 - \alpha)^2 + ... + (1 - \alpha)^t}
179
180 - When ``adjust=False``, the exponentially weighted function is calculated
181 recursively:
182
183 .. math::
184 \begin{split}
185 y_0 &= x_0\\
186 y_t &= (1 - \alpha) y_{t-1} + \alpha x_t,
187 \end{split}
188 ignore_na : bool, default False
189 Ignore missing values when calculating weights.
190
191 - When ``ignore_na=False`` (default), weights are based on absolute positions.
192 For example, the weights of :math:`x_0` and :math:`x_2` used in calculating
193 the final weighted average of [:math:`x_0`, None, :math:`x_2`] are
194 :math:`(1-\alpha)^2` and :math:`1` if ``adjust=True``, and
195 :math:`(1-\alpha)^2` and :math:`\alpha` if ``adjust=False``.
196
197 - When ``ignore_na=True``, weights are based
198 on relative positions. For example, the weights of :math:`x_0` and :math:`x_2`
199 used in calculating the final weighted average of
200 [:math:`x_0`, None, :math:`x_2`] are :math:`1-\alpha` and :math:`1` if
201 ``adjust=True``, and :math:`1-\alpha` and :math:`\alpha` if ``adjust=False``.
202
203 times : np.ndarray, Series, default None
204
205 Only applicable to ``mean()``.
206
207 Times corresponding to the observations. Must be monotonically increasing and
208 ``datetime64[ns]`` dtype.
209
210 If 1-D array like, a sequence with the same shape as the observations.
211
212 method : str {'single', 'table'}, default 'single'
213 Execute the rolling operation per single column or row (``'single'``)
214 or over the entire object (``'table'``).
215
216 This argument is only implemented when specifying ``engine='numba'``
217 in the method call.
218
219 Only applicable to ``mean()``
220
221 Returns
222 -------
223 pandas.api.typing.ExponentialMovingWindow
224 An instance of ExponentialMovingWindow for further exponentially weighted (EW)
225 calculations, e.g. using the ``mean`` method.
226
227 See Also
228 --------
229 rolling : Provides rolling window calculations.
230 expanding : Provides expanding transformations.
231
232 Notes
233 -----
234 See :ref:`Windowing Operations <window.exponentially_weighted>`
235 for further usage details and examples.
236
237 Examples
238 --------
239 >>> df = pd.DataFrame({'B': [0, 1, 2, np.nan, 4]})
240 >>> df
241 B
242 0 0.0
243 1 1.0
244 2 2.0
245 3 NaN
246 4 4.0
247
248 >>> df.ewm(com=0.5).mean()
249 B
250 0 0.000000
251 1 0.750000
252 2 1.615385
253 3 1.615385
254 4 3.670213
255 >>> df.ewm(alpha=2 / 3).mean()
256 B
257 0 0.000000
258 1 0.750000
259 2 1.615385
260 3 1.615385
261 4 3.670213
262
263 **adjust**
264
265 >>> df.ewm(com=0.5, adjust=True).mean()
266 B
267 0 0.000000
268 1 0.750000
269 2 1.615385
270 3 1.615385
271 4 3.670213
272 >>> df.ewm(com=0.5, adjust=False).mean()
273 B
274 0 0.000000
275 1 0.666667
276 2 1.555556
277 3 1.555556
278 4 3.650794
279
280 **ignore_na**
281
282 >>> df.ewm(com=0.5, ignore_na=True).mean()
283 B
284 0 0.000000
285 1 0.750000
286 2 1.615385
287 3 1.615385
288 4 3.225000
289 >>> df.ewm(com=0.5, ignore_na=False).mean()
290 B
291 0 0.000000
292 1 0.750000
293 2 1.615385
294 3 1.615385
295 4 3.670213
296
297 **times**
298
299 Exponentially weighted mean with weights calculated with a timedelta ``halflife``
300 relative to ``times``.
301
302 >>> times = ['2020-01-01', '2020-01-03', '2020-01-10', '2020-01-15', '2020-01-17']
303 >>> df.ewm(halflife='4 days', times=pd.DatetimeIndex(times)).mean()
304 B
305 0 0.000000
306 1 0.585786
307 2 1.523889
308 3 1.523889
309 4 3.233686
310 """
311
312 _attributes = [
313 "com",
314 "span",
315 "halflife",
316 "alpha",
317 "min_periods",
318 "adjust",
319 "ignore_na",
320 "times",
321 "method",
322 ]
323
324 def __init__(
325 self,
326 obj: NDFrame,
327 com: float | None = None,
328 span: float | None = None,
329 halflife: float | TimedeltaConvertibleTypes | None = None,
330 alpha: float | None = None,
331 min_periods: int | None = 0,
332 adjust: bool = True,
333 ignore_na: bool = False,
334 times: np.ndarray | NDFrame | None = None,
335 method: str = "single",
336 *,
337 selection=None,
338 ) -> None:
339 super().__init__(
340 obj=obj,
341 min_periods=1 if min_periods is None else max(int(min_periods), 1),
342 on=None,
343 center=False,
344 closed=None,
345 method=method,
346 selection=selection,
347 )
348 self.com = com
349 self.span = span
350 self.halflife = halflife
351 self.alpha = alpha
352 self.adjust = adjust
353 self.ignore_na = ignore_na
354 self.times = times
355 if self.times is not None:
356 times_dtype = getattr(self.times, "dtype", None)
357 if not (
358 is_datetime64_dtype(times_dtype)
359 or isinstance(times_dtype, DatetimeTZDtype)
360 ):
361 raise ValueError("times must be datetime64 dtype.")
362 if len(self.times) != len(obj):
363 raise ValueError("times must be the same length as the object.")
364 if not isinstance(self.halflife, (str, datetime.timedelta, np.timedelta64)):
365 raise ValueError("halflife must be a timedelta convertible object")
366 if isna(self.times).any():
367 raise ValueError("Cannot convert NaT values to integer")
368 self._deltas = _calculate_deltas(self.times, self.halflife)
369 # Halflife is no longer applicable when calculating COM
370 # But allow COM to still be calculated if the user passes other decay args
371 if common.count_not_none(self.com, self.span, self.alpha) > 0:
372 if not self.adjust:
373 raise NotImplementedError(
374 "None of com, span, or alpha can be specified if "
375 "times is provided and adjust=False"
376 )
377 self._com = get_center_of_mass(self.com, self.span, None, self.alpha)
378 else:
379 self._com = 1.0
380 else:
381 if self.halflife is not None and isinstance(
382 self.halflife, (str, datetime.timedelta, np.timedelta64)
383 ):
384 raise ValueError(
385 "halflife can only be a timedelta convertible argument if "
386 "times is not None."
387 )
388 # Without times, points are equally spaced
389 self._deltas = np.ones(max(self.obj.shape[0] - 1, 0), dtype=np.float64)
390 self._com = get_center_of_mass(
391 # error: Argument 3 to "get_center_of_mass" has incompatible type
392 # "Union[float, Any, None, timedelta64, signedinteger[_64Bit]]";
393 # expected "Optional[float]"
394 self.com,
395 self.span,
396 self.halflife, # type: ignore[arg-type]
397 self.alpha,
398 )
399
400 def _check_window_bounds(
401 self, start: np.ndarray, end: np.ndarray, num_vals: int
402 ) -> None:
403 # emw algorithms are iterative with each point
404 # ExponentialMovingWindowIndexer "bounds" are the entire window
405 pass
406
407 def _get_window_indexer(self) -> BaseIndexer:
408 """
409 Return an indexer class that will compute the window start and end bounds
410 """
411 return ExponentialMovingWindowIndexer()
412
413 def online(
414 self, engine: str = "numba", engine_kwargs=None
415 ) -> OnlineExponentialMovingWindow:
416 """
417 Return an ``OnlineExponentialMovingWindow`` object to calculate
418 exponentially moving window aggregations in an online method.
419
420 Parameters
421 ----------
422 engine: str, default ``'numba'``
423 Execution engine to calculate online aggregations.
424 Applies to all supported aggregation methods.
425
426 engine_kwargs : dict, default None
427 Applies to all supported aggregation methods.
428
429 * For ``'numba'`` engine, the engine can accept ``nogil``
430 and ``parallel`` dictionary keys. The values must either be ``True`` or
431 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is
432 ``{'nogil': False, 'parallel': False}`` and will be
433 applied to the function
434
435 Returns
436 -------
437 OnlineExponentialMovingWindow
438 """
439 return OnlineExponentialMovingWindow(
440 obj=self.obj,
441 com=self.com,
442 span=self.span,
443 halflife=self.halflife,
444 alpha=self.alpha,
445 min_periods=self.min_periods,
446 adjust=self.adjust,
447 ignore_na=self.ignore_na,
448 times=self.times,
449 engine=engine,
450 engine_kwargs=engine_kwargs,
451 selection=self._selection,
452 )
453
454 def aggregate(self, func=None, *args, **kwargs):
455 """
456 Aggregate using one or more operations over the specified axis.
457
458 Parameters
459 ----------
460 func : function, str, list or dict
461 Function to use for aggregating the data. If a function, must either
462 work when passed a Series/Dataframe or when passed to
463 Series/Dataframe.apply.
464
465 Accepted combinations are:
466
467 - function
468 - string function name
469 - list of functions and/or function names, e.g. ``[np.sum, 'mean']``
470 - dict of axis labels -> functions, function names or list of such.
471 *args
472 Positional arguments to pass to `func`.
473 **kwargs
474 Keyword arguments to pass to `func`.
475
476 Returns
477 -------
478 scalar, Series or DataFrame
479
480 The return can be:
481
482 * scalar : when Series.agg is called with single function
483 * Series : when DataFrame.agg is called with a single function
484 * DataFrame : when DataFrame.agg is called with several functions
485
486 See Also
487 --------
488 pandas.DataFrame.rolling.aggregate
489
490 Notes
491 -----
492 The aggregation operations are always performed over an axis, either the
493 index (default) or the column axis. This behavior is different from
494 `numpy` aggregation functions (`mean`, `median`, `prod`, `sum`, `std`,
495 `var`), where the default is to compute the aggregation of the flattened
496 array, e.g., ``numpy.mean(arr_2d)`` as opposed to
497 ``numpy.mean(arr_2d, axis=0)``.
498
499 `agg` is an alias for `aggregate`. Use the alias.
500
501 Functions that mutate the passed object can produce unexpected
502 behavior or errors and are not supported. See :ref:`gotchas.udf-mutation`
503 for more details.
504
505 A passed user-defined-function will be passed a Series for evaluation.
506
507 If ``func`` defines an index relabeling, ``axis`` must be ``0`` or ``index``.
508
509 Examples
510 --------
511 >>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]})
512 >>> df
513 A B C
514 0 1 4 7
515 1 2 5 8
516 2 3 6 9
517
518 >>> df.ewm(alpha=0.5).mean()
519 A B C
520 0 1.000000 4.000000 7.000000
521 1 1.666667 4.666667 7.666667
522 2 2.428571 5.428571 8.428571
523 """
524 if callable(func):
525 # GH#41700 BaseWindow.aggregate falls back to ``self.apply(...)``,
526 # which ExponentialMovingWindow does not implement.
527 raise NotImplementedError(
528 f"{type(self).__name__}.aggregate does not support arbitrary "
529 "callables; supported aggregations are mean, sum, std, var, "
530 "cov, corr."
531 )
532 return super().aggregate(func, *args, **kwargs)
533
534 agg = aggregate
535
536 def mean(
537 self,
538 numeric_only: bool = False,
539 engine=None,
540 engine_kwargs=None,
541 ):
542 """
543 Calculate the ewm (exponential weighted moment) mean.
544
545 The weighting is controlled by the ``com``, ``span``, ``halflife``, or
546 ``alpha`` parameter specified when calling :meth:`DataFrame.ewm` or
547 :meth:`Series.ewm`.
548
549 Parameters
550 ----------
551 numeric_only : bool, default False
552 Include only float, int, boolean columns.
553
554 engine : str, default None
555 * ``'cython'`` : Runs the operation through C-extensions from cython.
556 * ``'numba'`` : Runs the operation through JIT compiled code from numba.
557 * ``None`` : Defaults to ``'cython'`` or globally setting
558 ``compute.use_numba``
559
560 engine_kwargs : dict, default None
561 * For ``'cython'`` engine, there are no accepted ``engine_kwargs``
562 * For ``'numba'`` engine, the engine can accept ``nogil``
563 and ``parallel`` dictionary keys. The values must either be ``True`` or
564 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is
565 ``{'nogil': False, 'parallel': False}``
566
567 Returns
568 -------
569 Series or DataFrame
570 Return type is the same as the original object with ``np.float64`` dtype.
571
572 See Also
573 --------
574 Series.ewm : Calling ewm with Series data.
575 DataFrame.ewm : Calling ewm with DataFrames.
576 Series.mean : Aggregating mean for Series.
577 DataFrame.mean : Aggregating mean for DataFrame.
578
579 Notes
580 -----
581 See :ref:`window.numba_engine` and :ref:`enhancingperf.numba` for
582 extended documentation and performance considerations for the Numba engine.
583
584 Examples
585 --------
586 >>> ser = pd.Series([1, 2, 3, 4])
587 >>> ser.ewm(alpha=0.2).mean()
588 0 1.000000
589 1 1.555556
590 2 2.147541
591 3 2.775068
592 dtype: float64
593 """
594 if maybe_use_numba(engine):
595 if self.method == "single":
596 func = generate_numba_ewm_func
597 else:
598 func = generate_numba_ewm_table_func
599 ewm_func = func(
600 **get_jit_arguments(engine_kwargs),
601 com=self._com,
602 adjust=self.adjust,
603 ignore_na=self.ignore_na,
604 deltas=tuple(self._deltas),
605 normalize=True,
606 )
607 return self._apply(ewm_func, name="mean")
608 elif engine in ("cython", None):
609 if engine_kwargs is not None:
610 raise ValueError("cython engine does not accept engine_kwargs")
611
612 deltas = None if self.times is None else self._deltas
613 window_func = partial(
614 window_aggregations.ewm,
615 com=self._com,
616 adjust=self.adjust,
617 ignore_na=self.ignore_na,
618 deltas=deltas,
619 normalize=True,
620 )
621 return self._apply(window_func, name="mean", numeric_only=numeric_only)
622 else:
623 raise ValueError("engine must be either 'numba' or 'cython'")
624
625 def sum(
626 self,
627 numeric_only: bool = False,
628 engine=None,
629 engine_kwargs=None,
630 ):
631 """
632 Calculate the ewm (exponential weighted moment) sum.
633
634 The weighting is controlled by the ``com``, ``span``, ``halflife``, or
635 ``alpha`` parameter specified when calling :meth:`DataFrame.ewm` or
636 :meth:`Series.ewm`.
637
638 Parameters
639 ----------
640 numeric_only : bool, default False
641 Include only float, int, boolean columns.
642 engine : str, default None
643 * ``'cython'`` : Runs the operation through C-extensions from cython.
644 * ``'numba'`` : Runs the operation through JIT compiled code from numba.
645 * ``None`` : Defaults to ``'cython'`` or globally setting
646 ``compute.use_numba``
647 engine_kwargs : dict, default None
648 * For ``'cython'`` engine, there are no accepted ``engine_kwargs``
649 * For ``'numba'`` engine, the engine can accept ``nogil``
650 and ``parallel`` dictionary keys. The values must either be ``True`` or
651 ``False``. The default ``engine_kwargs`` for the ``'numba'`` engine is
652 ``{'nogil': False, 'parallel': False}``
653
654 Returns
655 -------
656 Series or DataFrame
657 Return type is the same as the original object with ``np.float64`` dtype.
658
659 See Also
660 --------
661 Series.ewm : Calling ewm with Series data.
662 DataFrame.ewm : Calling ewm with DataFrames.
663 Series.sum : Aggregating sum for Series.
664 DataFrame.sum : Aggregating sum for DataFrame.
665
666 Notes
667 -----
668 See :ref:`window.numba_engine` and :ref:`enhancingperf.numba` for extended
669 documentation and performance considerations for the Numba engine.
670
671 Examples
672 --------
673 >>> ser = pd.Series([1, 2, 3, 4])
674 >>> ser.ewm(alpha=0.2).sum()
675 0 1.000
676 1 2.800
677 2 5.240
678 3 8.192
679 dtype: float64
680 """
681 if not self.adjust:
682 raise NotImplementedError("sum is not implemented with adjust=False")
683 if self.times is not None:
684 raise NotImplementedError("sum is not implemented with times")
685 if maybe_use_numba(engine):
686 if self.method == "single":
687 func = generate_numba_ewm_func
688 else:
689 func = generate_numba_ewm_table_func
690 ewm_func = func(
691 **get_jit_arguments(engine_kwargs),
692 com=self._com,
693 adjust=self.adjust,
694 ignore_na=self.ignore_na,
695 deltas=tuple(self._deltas),
696 normalize=False,
697 )
698 return self._apply(ewm_func, name="sum")
699 elif engine in ("cython", None):
700 if engine_kwargs is not None:
701 raise ValueError("cython engine does not accept engine_kwargs")
702
703 deltas = None if self.times is None else self._deltas
704 window_func = partial(
705 window_aggregations.ewm,
706 com=self._com,
707 adjust=self.adjust,
708 ignore_na=self.ignore_na,
709 deltas=deltas,
710 normalize=False,
711 )
712 return self._apply(window_func, name="sum", numeric_only=numeric_only)
713 else:
714 raise ValueError("engine must be either 'numba' or 'cython'")
715
716 def std(self, bias: bool = False, numeric_only: bool = False):
717 """
718 Calculate the ewm (exponential weighted moment) standard deviation.
719
720 The weighting is controlled by the ``com``, ``span``, ``halflife``, or
721 ``alpha`` parameter specified when calling :meth:`DataFrame.ewm` or
722 :meth:`Series.ewm`.
723
724 Parameters
725 ----------
726 bias : bool, default False
727 Use a standard estimation bias correction.
728 numeric_only : bool, default False
729 Include only float, int, boolean columns.
730
731 Returns
732 -------
733 Series or DataFrame
734 Return type is the same as the original object with ``np.float64`` dtype.
735
736 See Also
737 --------
738 Series.ewm : Calling ewm with Series data.
739 DataFrame.ewm : Calling ewm with DataFrames.
740 Series.std : Aggregating std for Series.
741 DataFrame.std : Aggregating std for DataFrame.
742
743 Examples
744 --------
745 >>> ser = pd.Series([1, 2, 3, 4])
746 >>> ser.ewm(alpha=0.2).std()
747 0 NaN
748 1 0.707107
749 2 0.995893
750 3 1.277320
751 dtype: float64
752 """
753 if (
754 numeric_only
755 and self._selected_obj.ndim == 1
756 and not is_numeric_dtype(self._selected_obj.dtype)
757 ):
758 # Raise directly so error message says std instead of var
759 raise NotImplementedError(
760 f"{type(self).__name__}.std does not implement numeric_only"
761 )
762 if self.times is not None:
763 raise NotImplementedError("std is not implemented with times")
764 return zsqrt(self.var(bias=bias, numeric_only=numeric_only))
765
766 def var(self, bias: bool = False, numeric_only: bool = False):
767 """
768 Calculate the ewm (exponential weighted moment) variance.
769
770 The weighting is controlled by the ``com``, ``span``, ``halflife``, or
771 ``alpha`` parameter specified when calling :meth:`DataFrame.ewm` or
772 :meth:`Series.ewm`.
773
774 Parameters
775 ----------
776 bias : bool, default False
777 Use a standard estimation bias correction.
778 numeric_only : bool, default False
779 Include only float, int, boolean columns.
780
781 Returns
782 -------
783 Series or DataFrame
784 Return type is the same as the original object with ``np.float64`` dtype.
785
786 See Also
787 --------
788 Series.ewm : Calling ewm with Series data.
789 DataFrame.ewm : Calling ewm with DataFrames.
790 Series.var : Aggregating var for Series.
791 DataFrame.var : Aggregating var for DataFrame.
792
793 Examples
794 --------
795 >>> ser = pd.Series([1, 2, 3, 4])
796 >>> ser.ewm(alpha=0.2).var()
797 0 NaN
798 1 0.500000
799 2 0.991803
800 3 1.631547
801 dtype: float64
802 """
803 if self.times is not None:
804 raise NotImplementedError("var is not implemented with times")
805 window_func = window_aggregations.ewmcov
806 wfunc = partial(
807 window_func,
808 com=self._com,
809 adjust=self.adjust,
810 ignore_na=self.ignore_na,
811 bias=bias,
812 )
813
814 def var_func(values, begin, end, min_periods):
815 return wfunc(values, begin, end, min_periods, values)
816
817 return self._apply(var_func, name="var", numeric_only=numeric_only)
818
819 def cov(
820 self,
821 other: DataFrame | Series | None = None,
822 pairwise: bool | None = None,
823 bias: bool = False,
824 numeric_only: bool = False,
825 ):
826 """
827 Calculate the ewm (exponential weighted moment) sample covariance.
828
829 The weighting is controlled by the ``com``, ``span``, ``halflife``, or
830 ``alpha`` parameter specified when calling :meth:`DataFrame.ewm` or
831 :meth:`Series.ewm`.
832
833 Parameters
834 ----------
835 other : Series or DataFrame , optional
836 If not supplied then will default to self and produce pairwise
837 output.
838 pairwise : bool, default None
839 If False then only matching columns between self and other will be
840 used and the output will be a DataFrame.
841 If True then all pairwise combinations will be calculated and the
842 output will be a MultiIndex DataFrame in the case of DataFrame
843 inputs. In the case of missing elements, only complete pairwise
844 observations will be used.
845 bias : bool, default False
846 Use a standard estimation bias correction.
847 numeric_only : bool, default False
848 Include only float, int, boolean columns.
849
850 Returns
851 -------
852 Series or DataFrame
853 Return type is the same as the original object with ``np.float64`` dtype.
854
855 See Also
856 --------
857 Series.ewm : Calling ewm with Series data.
858 DataFrame.ewm : Calling ewm with DataFrames.
859 Series.cov : Aggregating cov for Series.
860 DataFrame.cov : Aggregating cov for DataFrame.
861
862 Examples
863 --------
864 >>> ser1 = pd.Series([1, 2, 3, 4])
865 >>> ser2 = pd.Series([10, 11, 13, 16])
866 >>> ser1.ewm(alpha=0.2).cov(ser2)
867 0 NaN
868 1 0.500000
869 2 1.524590
870 3 3.408836
871 dtype: float64
872 """
873 if self.times is not None:
874 raise NotImplementedError("cov is not implemented with times")
875
876 from pandas import Series
877
878 self._validate_numeric_only("cov", numeric_only)
879
880 def cov_func(x, y):
881 x_array = self._prep_values(x)
882 y_array = self._prep_values(y)
883 window_indexer = self._get_window_indexer()
884 min_periods = (
885 self.min_periods
886 if self.min_periods is not None
887 else window_indexer.window_size
888 )
889 start, end = window_indexer.get_window_bounds(
890 num_values=len(x_array),
891 min_periods=min_periods,
892 center=self.center,
893 closed=self.closed,
894 step=self.step,
895 )
896 result = window_aggregations.ewmcov(
897 x_array,
898 start,
899 end,
900 # error: Argument 4 to "ewmcov" has incompatible type
901 # "Optional[int]"; expected "int"
902 self.min_periods, # type: ignore[arg-type]
903 y_array,
904 self._com,
905 self.adjust,
906 self.ignore_na,
907 bias,
908 )
909 return Series(result, index=x.index, name=x.name, copy=False)
910
911 return self._apply_pairwise(
912 self._selected_obj, other, pairwise, cov_func, numeric_only
913 )
914
915 def corr(
916 self,
917 other: DataFrame | Series | None = None,
918 pairwise: bool | None = None,
919 numeric_only: bool = False,
920 ):
921 """
922 Calculate the ewm (exponential weighted moment) sample correlation.
923
924 The weighting is controlled by the ``com``, ``span``, ``halflife``, or
925 ``alpha`` parameter specified when calling :meth:`DataFrame.ewm` or
926 :meth:`Series.ewm`.
927
928 Parameters
929 ----------
930 other : Series or DataFrame, optional
931 If not supplied then will default to self and produce pairwise
932 output.
933 pairwise : bool, default None
934 If False then only matching columns between self and other will be
935 used and the output will be a DataFrame.
936 If True then all pairwise combinations will be calculated and the
937 output will be a MultiIndex DataFrame in the case of DataFrame
938 inputs. In the case of missing elements, only complete pairwise
939 observations will be used.
940 numeric_only : bool, default False
941 Include only float, int, boolean columns.
942
943 Returns
944 -------
945 Series or DataFrame
946 Return type is the same as the original object with ``np.float64`` dtype.
947
948 See Also
949 --------
950 Series.ewm : Calling ewm with Series data.
951 DataFrame.ewm : Calling ewm with DataFrames.
952 Series.corr : Aggregating corr for Series.
953 DataFrame.corr : Aggregating corr for DataFrame.
954
955 Examples
956 --------
957 >>> ser1 = pd.Series([1, 2, 3, 4])
958 >>> ser2 = pd.Series([10, 11, 13, 16])
959 >>> ser1.ewm(alpha=0.2).corr(ser2)
960 0 NaN
961 1 1.000000
962 2 0.982821
963 3 0.977802
964 dtype: float64
965 """
966 if self.times is not None:
967 raise NotImplementedError("corr is not implemented with times")
968
969 from pandas import Series
970
971 self._validate_numeric_only("corr", numeric_only)
972
973 def cov_func(x, y):
974 x_array = self._prep_values(x)
975 y_array = self._prep_values(y)
976 window_indexer = self._get_window_indexer()
977 min_periods = (
978 self.min_periods
979 if self.min_periods is not None
980 else window_indexer.window_size
981 )
982 start, end = window_indexer.get_window_bounds(
983 num_values=len(x_array),
984 min_periods=min_periods,
985 center=self.center,
986 closed=self.closed,
987 step=self.step,
988 )
989
990 def _cov(X, Y):
991 return window_aggregations.ewmcov(
992 X,
993 start,
994 end,
995 min_periods,
996 Y,
997 self._com,
998 self.adjust,
999 self.ignore_na,
1000 True,
1001 )
1002
1003 with np.errstate(all="ignore"):
1004 cov = _cov(x_array, y_array)
1005 x_var = _cov(x_array, x_array)
1006 y_var = _cov(y_array, y_array)
1007 result = cov / zsqrt(x_var * y_var)
1008 return Series(result, index=x.index, name=x.name, copy=False)
1009
1010 return self._apply_pairwise(
1011 self._selected_obj, other, pairwise, cov_func, numeric_only
1012 )
1013
1014
1015@set_module("pandas.api.typing")
1016class ExponentialMovingWindowGroupBy(BaseWindowGroupBy, ExponentialMovingWindow):
1017 """
1018 Provide an exponential moving window groupby implementation.
1019 """
1020
1021 _attributes = ExponentialMovingWindow._attributes + BaseWindowGroupBy._attributes
1022
1023 def __init__(self, obj, *args, _grouper=None, **kwargs) -> None:
1024 super().__init__(obj, *args, _grouper=_grouper, **kwargs)
1025
1026 if not obj.empty and self.times is not None:
1027 # sort the times and recalculate the deltas according to the groups
1028 groupby_order = np.concatenate(list(self._grouper.indices.values()))
1029 self._deltas = _calculate_deltas(
1030 self.times.take(groupby_order),
1031 self.halflife,
1032 )
1033
1034 def _get_window_indexer(self) -> GroupByIndexer:
1035 """
1036 Return an indexer class that will compute the window start and end bounds
1037
1038 Returns
1039 -------
1040 GroupByIndexer
1041 """
1042 window_indexer = GroupByIndexer(
1043 groupby_indices=self._grouper.indices,
1044 window_indexer=ExponentialMovingWindowIndexer,
1045 )
1046 return window_indexer
1047
1048
1049class OnlineExponentialMovingWindow(ExponentialMovingWindow):
1050 def __init__(
1051 self,
1052 obj: NDFrame,
1053 com: float | None = None,
1054 span: float | None = None,
1055 halflife: float | TimedeltaConvertibleTypes | None = None,
1056 alpha: float | None = None,
1057 min_periods: int | None = 0,
1058 adjust: bool = True,
1059 ignore_na: bool = False,
1060 times: np.ndarray | NDFrame | None = None,
1061 engine: str = "numba",
1062 engine_kwargs: dict[str, bool] | None = None,
1063 *,
1064 selection=None,
1065 ) -> None:
1066 if times is not None:
1067 raise NotImplementedError(
1068 "times is not implemented with online operations."
1069 )
1070 super().__init__(
1071 obj=obj,
1072 com=com,
1073 span=span,
1074 halflife=halflife,
1075 alpha=alpha,
1076 min_periods=min_periods,
1077 adjust=adjust,
1078 ignore_na=ignore_na,
1079 times=times,
1080 selection=selection,
1081 )
1082 self._mean = EWMMeanState(self._com, self.adjust, self.ignore_na, obj.shape)
1083 if maybe_use_numba(engine):
1084 self.engine = engine
1085 self.engine_kwargs = engine_kwargs
1086 else:
1087 raise ValueError("'numba' is the only supported engine")
1088
1089 def reset(self) -> None:
1090 """
1091 Reset the state captured by `update` calls.
1092 """
1093 self._mean.reset()
1094
1095 def aggregate(self, func=None, *args, **kwargs):
1096 raise NotImplementedError("aggregate is not implemented.")
1097
1098 def std(self, bias: bool = False, *args, **kwargs):
1099 raise NotImplementedError("std is not implemented.")
1100
1101 def corr(
1102 self,
1103 other: DataFrame | Series | None = None,
1104 pairwise: bool | None = None,
1105 numeric_only: bool = False,
1106 ):
1107 raise NotImplementedError("corr is not implemented.")
1108
1109 def cov(
1110 self,
1111 other: DataFrame | Series | None = None,
1112 pairwise: bool | None = None,
1113 bias: bool = False,
1114 numeric_only: bool = False,
1115 ):
1116 raise NotImplementedError("cov is not implemented.")
1117
1118 def var(self, bias: bool = False, numeric_only: bool = False):
1119 raise NotImplementedError("var is not implemented.")
1120
1121 def mean(self, *args, update=None, update_times=None, **kwargs):
1122 """
1123 Calculate an online exponentially weighted mean.
1124
1125 Parameters
1126 ----------
1127 update: DataFrame or Series, default None
1128 New values to continue calculating the
1129 exponentially weighted mean from the last values and weights.
1130 Values should be float64 dtype.
1131
1132 ``update`` needs to be ``None`` the first time the
1133 exponentially weighted mean is calculated.
1134
1135 update_times: Series or 1-D np.ndarray, default None
1136 New times to continue calculating the
1137 exponentially weighted mean from the last values and weights.
1138 If ``None``, values are assumed to be evenly spaced
1139 in time.
1140 This feature is currently unsupported.
1141
1142 Returns
1143 -------
1144 DataFrame or Series
1145
1146 Examples
1147 --------
1148 >>> df = pd.DataFrame({"a": range(5), "b": range(5, 10)})
1149 >>> online_ewm = df.head(2).ewm(0.5).online()
1150 >>> online_ewm.mean()
1151 a b
1152 0 0.00 5.00
1153 1 0.75 5.75
1154 >>> online_ewm.mean(update=df.tail(3))
1155 a b
1156 2 1.615385 6.615385
1157 3 2.550000 7.550000
1158 4 3.520661 8.520661
1159 >>> online_ewm.reset()
1160 >>> online_ewm.mean()
1161 a b
1162 0 0.00 5.00
1163 1 0.75 5.75
1164 """
1165 result_kwargs = {}
1166 is_frame = self._selected_obj.ndim == 2
1167 if update_times is not None:
1168 raise NotImplementedError("update_times is not implemented.")
1169 if update is not None:
1170 if self._mean.last_ewm is None:
1171 raise ValueError(
1172 "Must call mean with update=None first before passing update"
1173 )
1174 result_from = 1
1175 result_kwargs["index"] = update.index
1176 if is_frame:
1177 last_value = self._mean.last_ewm[np.newaxis, :]
1178 result_kwargs["columns"] = update.columns
1179 else:
1180 last_value = self._mean.last_ewm
1181 result_kwargs["name"] = update.name
1182 np_array = np.concatenate((last_value, update.to_numpy()))
1183 else:
1184 result_from = 0
1185 result_kwargs["index"] = self._selected_obj.index
1186 if is_frame:
1187 result_kwargs["columns"] = self._selected_obj.columns
1188 else:
1189 result_kwargs["name"] = self._selected_obj.name
1190 np_array = self._selected_obj.astype(np.float64).to_numpy()
1191 update_deltas = np.ones(max(len(np_array) - 1, 0), dtype=np.float64)
1192 ewma_func = generate_online_numba_ewma_func(
1193 **get_jit_arguments(self.engine_kwargs)
1194 )
1195 result = self._mean.run_ewm(
1196 np_array if is_frame else np_array[:, np.newaxis],
1197 update_deltas,
1198 self.min_periods, # type: ignore[arg-type]
1199 ewma_func,
1200 )
1201 if not is_frame:
1202 result = result.squeeze()
1203 result = result[result_from:]
1204 result = self._selected_obj._constructor(result, **result_kwargs)
1205 return result