1from __future__ import annotations
2
3import functools
4from typing import (
5 TYPE_CHECKING,
6 Any,
7)
8
9import numpy as np
10
11from pandas.compat._optional import import_optional_dependency
12
13from pandas.core.util.numba_ import jit_user_function
14
15if TYPE_CHECKING:
16 from collections.abc import Callable
17
18 from pandas._typing import Scalar
19
20
21@functools.cache
22def generate_numba_apply_func(
23 func: Callable[..., Scalar],
24 nogil: bool,
25 parallel: bool,
26):
27 """
28 Generate a numba jitted apply function specified by values from engine_kwargs.
29
30 1. jit the user's function
31 2. Return a rolling apply function with the jitted function inline
32
33 Configurations specified in engine_kwargs apply to both the user's
34 function _AND_ the rolling apply function.
35
36 Parameters
37 ----------
38 func : function
39 function to be applied to each window and will be JITed
40 nogil : bool
41 nogil to be passed into numba.jit
42 parallel : bool
43 parallel to be passed into numba.jit
44
45 Returns
46 -------
47 Numba function
48 """
49 numba_func = jit_user_function(func)
50 if TYPE_CHECKING:
51 import numba
52 else:
53 numba = import_optional_dependency("numba")
54
55 @numba.jit(nogil=nogil, parallel=parallel)
56 def roll_apply(
57 values: np.ndarray,
58 begin: np.ndarray,
59 end: np.ndarray,
60 minimum_periods: int,
61 *args: Any,
62 ) -> np.ndarray:
63 result = np.empty(len(begin))
64 for i in numba.prange(len(result)):
65 start = begin[i]
66 stop = end[i]
67 window = values[start:stop]
68 count_nan = np.sum(np.isnan(window))
69 if len(window) - count_nan >= minimum_periods:
70 result[i] = numba_func(window, *args)
71 else:
72 result[i] = np.nan
73 return result
74
75 return roll_apply
76
77
78@functools.cache
79def generate_numba_ewm_func(
80 nogil: bool,
81 parallel: bool,
82 com: float,
83 adjust: bool,
84 ignore_na: bool,
85 deltas: tuple[float, ...],
86 normalize: bool,
87):
88 """
89 Generate a numba jitted ewm mean or sum function specified by values
90 from engine_kwargs.
91
92 Parameters
93 ----------
94 nogil : bool
95 nogil to be passed into numba.jit
96 parallel : bool
97 parallel to be passed into numba.jit
98 com : float
99 adjust : bool
100 ignore_na : bool
101 deltas : tuple
102 normalize : bool
103
104 Returns
105 -------
106 Numba function
107 """
108 if TYPE_CHECKING:
109 import numba
110 else:
111 numba = import_optional_dependency("numba")
112
113 @numba.jit(nogil=nogil, parallel=parallel)
114 def ewm(
115 values: np.ndarray,
116 begin: np.ndarray,
117 end: np.ndarray,
118 minimum_periods: int,
119 ) -> np.ndarray:
120 result = np.empty(len(values))
121 alpha = 1.0 / (1.0 + com)
122 old_wt_factor = 1.0 - alpha
123 new_wt = 1.0 if adjust else alpha
124
125 for i in numba.prange(len(begin)):
126 start = begin[i]
127 stop = end[i]
128 window = values[start:stop]
129 sub_result = np.empty(len(window))
130
131 weighted = window[0]
132 nobs = int(not np.isnan(weighted))
133 sub_result[0] = weighted if nobs >= minimum_periods else np.nan
134 old_wt = 1.0
135
136 for j in range(1, len(window)):
137 cur = window[j]
138 is_observation = not np.isnan(cur)
139 nobs += is_observation
140 if not np.isnan(weighted):
141 if is_observation or not ignore_na:
142 if normalize:
143 # note that len(deltas) = len(vals) - 1 and deltas[i]
144 # is to be used in conjunction with vals[i+1]
145 old_wt *= old_wt_factor ** deltas[start + j - 1]
146 if not adjust and com == 1:
147 # update in case of irregular-interval time series
148 new_wt = 1.0 - old_wt
149 else:
150 weighted = old_wt_factor * weighted
151 if is_observation:
152 if normalize:
153 # avoid numerical errors on constant series
154 if weighted != cur:
155 weighted = old_wt * weighted + new_wt * cur
156 if normalize:
157 weighted = weighted / (old_wt + new_wt)
158 if adjust:
159 old_wt += new_wt
160 else:
161 old_wt = 1.0
162 else:
163 weighted += cur
164 elif is_observation:
165 weighted = cur
166
167 sub_result[j] = weighted if nobs >= minimum_periods else np.nan
168
169 result[start:stop] = sub_result
170
171 return result
172
173 return ewm
174
175
176@functools.cache
177def generate_numba_table_func(
178 func: Callable[..., np.ndarray],
179 nogil: bool,
180 parallel: bool,
181):
182 """
183 Generate a numba jitted function to apply window calculations table-wise.
184
185 Func will be passed an M window size x N number of columns array, and
186 must return a 1 x N number of columns array.
187
188 1. jit the user's function
189 2. Return a rolling apply function with the jitted function inline
190
191 Parameters
192 ----------
193 func : function
194 function to be applied to each window and will be JITed
195 nogil : bool
196 nogil to be passed into numba.jit
197 parallel : bool
198 parallel to be passed into numba.jit
199
200 Returns
201 -------
202 Numba function
203 """
204 numba_func = jit_user_function(func)
205 if TYPE_CHECKING:
206 import numba
207 else:
208 numba = import_optional_dependency("numba")
209
210 @numba.jit(nogil=nogil, parallel=parallel)
211 def roll_table(
212 values: np.ndarray,
213 begin: np.ndarray,
214 end: np.ndarray,
215 minimum_periods: int,
216 *args: Any,
217 ):
218 result = np.empty((len(begin), values.shape[1]))
219 min_periods_mask = np.empty(result.shape)
220 for i in numba.prange(len(result)):
221 start = begin[i]
222 stop = end[i]
223 window = values[start:stop]
224 count_nan = np.sum(np.isnan(window), axis=0)
225 nan_mask = len(window) - count_nan >= minimum_periods
226 if nan_mask.any():
227 result[i, :] = numba_func(window, *args)
228 min_periods_mask[i, :] = nan_mask
229 result = np.where(min_periods_mask, result, np.nan)
230 return result
231
232 return roll_table
233
234
235# This function will no longer be needed once numba supports
236# axis for all np.nan* agg functions
237# https://github.com/numba/numba/issues/1269
238@functools.cache
239def generate_manual_numpy_nan_agg_with_axis(nan_func):
240 if TYPE_CHECKING:
241 import numba
242 else:
243 numba = import_optional_dependency("numba")
244
245 # Not jitted directly: this is passed through jit_user_function in
246 # generate_numba_table_func, which wraps it with register_jitable so it
247 # inlines into roll_table's outer jit. A standalone @jit(parallel=True)
248 # here nests parallel regions and crashes the workqueue threading layer
249 # on platforms without tbb/openmp (GH#40454).
250 def nan_agg_with_axis(table):
251 result = np.empty(table.shape[1])
252 for i in numba.prange(table.shape[1]):
253 partition = table[:, i]
254 result[i] = nan_func(partition)
255 return result
256
257 return nan_agg_with_axis
258
259
260@functools.cache
261def generate_numba_ewm_table_func(
262 nogil: bool,
263 parallel: bool,
264 com: float,
265 adjust: bool,
266 ignore_na: bool,
267 deltas: tuple[float, ...],
268 normalize: bool,
269):
270 """
271 Generate a numba jitted ewm mean or sum function applied table wise specified
272 by values from engine_kwargs.
273
274 Parameters
275 ----------
276 nogil : bool
277 nogil to be passed into numba.jit
278 parallel : bool
279 parallel to be passed into numba.jit
280 com : float
281 adjust : bool
282 ignore_na : bool
283 deltas : tuple
284 normalize: bool
285
286 Returns
287 -------
288 Numba function
289 """
290 if TYPE_CHECKING:
291 import numba
292 else:
293 numba = import_optional_dependency("numba")
294
295 @numba.jit(nogil=nogil, parallel=parallel)
296 def ewm_table(
297 values: np.ndarray,
298 begin: np.ndarray,
299 end: np.ndarray,
300 minimum_periods: int,
301 ) -> np.ndarray:
302 alpha = 1.0 / (1.0 + com)
303 old_wt_factor = 1.0 - alpha
304 new_wt = 1.0 if adjust else alpha
305 old_wt = np.ones(values.shape[1])
306
307 result = np.empty(values.shape)
308 weighted = values[0].copy()
309 nobs = (~np.isnan(weighted)).astype(np.int64)
310 result[0] = np.where(nobs >= minimum_periods, weighted, np.nan)
311 for i in range(1, len(values)):
312 cur = values[i]
313 is_observations = ~np.isnan(cur)
314 nobs += is_observations.astype(np.int64)
315 for j in numba.prange(len(cur)):
316 if not np.isnan(weighted[j]):
317 if is_observations[j] or not ignore_na:
318 if normalize:
319 # note that len(deltas) = len(vals) - 1 and deltas[i]
320 # is to be used in conjunction with vals[i+1]
321 old_wt[j] *= old_wt_factor ** deltas[i - 1]
322 if not adjust and com == 1:
323 # update in case of irregular-interval time series
324 new_wt = 1.0 - old_wt[j]
325 else:
326 weighted[j] = old_wt_factor * weighted[j]
327 if is_observations[j]:
328 if normalize:
329 # avoid numerical errors on constant series
330 if weighted[j] != cur[j]:
331 weighted[j] = (
332 old_wt[j] * weighted[j] + new_wt * cur[j]
333 )
334 if normalize:
335 weighted[j] = weighted[j] / (old_wt[j] + new_wt)
336 if adjust:
337 old_wt[j] += new_wt
338 else:
339 old_wt[j] = 1.0
340 else:
341 weighted[j] += cur[j]
342 elif is_observations[j]:
343 weighted[j] = cur[j]
344
345 result[i] = np.where(nobs >= minimum_periods, weighted, np.nan)
346
347 return result
348
349 return ewm_table