1from __future__ import annotations
2
3import functools
4from typing import TYPE_CHECKING
5
6import numpy as np
7
8from pandas.compat._optional import import_optional_dependency
9
10if TYPE_CHECKING:
11 from collections.abc import Callable
12
13
14@functools.cache
15def generate_online_numba_ewma_func(
16 nogil: bool,
17 parallel: bool,
18) -> Callable[
19 [np.ndarray, np.ndarray, int, float, float, np.ndarray, bool, bool],
20 tuple[np.ndarray, np.ndarray],
21]:
22 """
23 Generate a numba jitted groupby ewma function specified by values
24 from engine_kwargs.
25
26 Parameters
27 ----------
28 nogil : bool
29 nogil to be passed into numba.jit
30 parallel : bool
31 parallel to be passed into numba.jit
32
33 Returns
34 -------
35 Numba function
36 """
37 if TYPE_CHECKING:
38 import numba
39 else:
40 numba = import_optional_dependency("numba")
41
42 @numba.jit(nogil=nogil, parallel=parallel)
43 def online_ewma(
44 values: np.ndarray,
45 deltas: np.ndarray,
46 minimum_periods: int,
47 old_wt_factor: float,
48 new_wt: float,
49 old_wt: np.ndarray,
50 adjust: bool,
51 ignore_na: bool,
52 ) -> tuple[np.ndarray, np.ndarray]:
53 """
54 Compute online exponentially weighted mean per column over 2D values.
55
56 Takes the first observation as is, then computes the subsequent
57 exponentially weighted mean accounting minimum periods.
58 """
59 result = np.empty(values.shape)
60 weighted_avg = values[0].copy()
61 nobs = (~np.isnan(weighted_avg)).astype(np.int64)
62 result[0] = np.where(nobs >= minimum_periods, weighted_avg, np.nan)
63
64 for i in range(1, len(values)):
65 cur = values[i]
66 is_observations = ~np.isnan(cur)
67 nobs += is_observations.astype(np.int64)
68 for j in numba.prange(len(cur)):
69 if not np.isnan(weighted_avg[j]):
70 if is_observations[j] or not ignore_na:
71 # note that len(deltas) = len(vals) - 1 and deltas[i] is to be
72 # used in conjunction with vals[i+1]
73 old_wt[j] *= old_wt_factor ** deltas[i - 1]
74 if is_observations[j]:
75 # avoid numerical errors on constant series
76 if weighted_avg[j] != cur[j]:
77 weighted_avg[j] = (
78 (old_wt[j] * weighted_avg[j]) + (new_wt * cur[j])
79 ) / (old_wt[j] + new_wt)
80 if adjust:
81 old_wt[j] += new_wt
82 else:
83 old_wt[j] = 1.0
84 elif is_observations[j]:
85 weighted_avg[j] = cur[j]
86
87 result[i] = np.where(nobs >= minimum_periods, weighted_avg, np.nan)
88
89 return result, old_wt
90
91 return online_ewma
92
93
94class EWMMeanState:
95 def __init__(
96 self,
97 com: float,
98 adjust: bool,
99 ignore_na: bool,
100 shape: tuple[int, ...],
101 ) -> None:
102 alpha = 1.0 / (1.0 + com)
103 self.shape = shape
104 self.adjust = adjust
105 self.ignore_na = ignore_na
106 self.new_wt = 1.0 if adjust else alpha
107 self.old_wt_factor = 1.0 - alpha
108 self.old_wt = np.ones(self.shape[-1])
109 self.last_ewm: np.ndarray | None = None
110
111 def run_ewm(
112 self,
113 weighted_avg: np.ndarray,
114 deltas: np.ndarray,
115 min_periods: int,
116 ewm_func: Callable[
117 [np.ndarray, np.ndarray, int, float, float, np.ndarray, bool, bool],
118 tuple[np.ndarray, np.ndarray],
119 ],
120 ) -> np.ndarray:
121 result, old_wt = ewm_func(
122 weighted_avg,
123 deltas,
124 min_periods,
125 self.old_wt_factor,
126 self.new_wt,
127 self.old_wt,
128 self.adjust,
129 self.ignore_na,
130 )
131 self.old_wt = old_wt
132 self.last_ewm = result[-1]
133 return result
134
135 def reset(self) -> None:
136 self.old_wt = np.ones(self.shape[-1])
137 self.last_ewm = None