upgini 1.2.157.dev4__tar.gz → 1.2.158a4180.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/PKG-INFO +2 -2
  2. upgini-1.2.158a4180.dev2/src/upgini/__about__.py +1 -0
  3. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/date.py +204 -46
  4. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/delta.py +12 -20
  5. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/lag.py +4 -9
  6. upgini-1.2.158a4180.dev2/src/upgini/autofe/timeseries/numpy_kernels.py +259 -0
  7. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/roll.py +14 -7
  8. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/volatility.py +46 -28
  9. upgini-1.2.157.dev4/src/upgini/__about__.py +0 -1
  10. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/.gitignore +0 -0
  11. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/LICENSE +0 -0
  12. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/README.md +0 -0
  13. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/pyproject.toml +0 -0
  14. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/__init__.py +0 -0
  15. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/ads.py +0 -0
  16. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/ads_management/__init__.py +0 -0
  17. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/ads_management/ads_manager.py +0 -0
  18. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/__init__.py +0 -0
  19. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/all_operators.py +0 -0
  20. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/binary.py +0 -0
  21. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/feature.py +0 -0
  22. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/groupby.py +0 -0
  23. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/operand.py +0 -0
  24. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/operator.py +0 -0
  25. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/__init__.py +0 -0
  26. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/base.py +0 -0
  27. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/cross.py +0 -0
  28. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/trend.py +0 -0
  29. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/unary.py +0 -0
  30. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/utils.py +0 -0
  31. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/vector.py +0 -0
  32. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/data_source/__init__.py +0 -0
  33. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/data_source/data_source_publisher.py +0 -0
  34. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/dataset.py +0 -0
  35. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/errors.py +0 -0
  36. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/features_enricher.py +0 -0
  37. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/http.py +0 -0
  38. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/mdc/__init__.py +0 -0
  39. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/mdc/context.py +0 -0
  40. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/metadata.py +0 -0
  41. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/metrics.py +0 -0
  42. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/normalizer/__init__.py +0 -0
  43. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/normalizer/normalize_utils.py +0 -0
  44. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/__init__.py +0 -0
  45. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/exceptions.py +0 -0
  46. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/strings.properties +0 -0
  47. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  48. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/sampler/__init__.py +0 -0
  49. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/sampler/base.py +0 -0
  50. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/sampler/random_under_sampler.py +0 -0
  51. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/sampler/utils.py +0 -0
  52. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/search_task.py +0 -0
  53. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/spinner.py +0 -0
  54. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  55. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/__init__.py +0 -0
  56. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/base_search_key_detector.py +0 -0
  57. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/blocked_time_series.py +0 -0
  58. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/config.py +0 -0
  59. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/country_utils.py +0 -0
  60. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/custom_loss_utils.py +0 -0
  61. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/cv_utils.py +0 -0
  62. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/datetime_utils.py +0 -0
  63. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/deduplicate_utils.py +0 -0
  64. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/display_utils.py +0 -0
  65. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/email_utils.py +0 -0
  66. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/fallback_progress_bar.py +0 -0
  67. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/feature_info.py +0 -0
  68. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/features_validator.py +0 -0
  69. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/format.py +0 -0
  70. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/hash_utils.py +0 -0
  71. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/ip_utils.py +0 -0
  72. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/mstats.py +0 -0
  73. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/one_hot_encoder.py +0 -0
  74. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/phone_utils.py +0 -0
  75. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/postal_code_utils.py +0 -0
  76. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/progress_bar.py +0 -0
  77. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/psi.py +0 -0
  78. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/sample_utils.py +0 -0
  79. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/sklearn_ext.py +0 -0
  80. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/sort.py +0 -0
  81. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/target_utils.py +0 -0
  82. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/track_info.py +0 -0
  83. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/ts_utils.py +0 -0
  84. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/warning_counter.py +0 -0
  85. {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/version_validator.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: upgini
3
- Version: 1.2.157.dev4
3
+ Version: 1.2.158a4180.dev2
4
4
  Summary: Intelligent data search & enrichment for Machine Learning
5
5
  Project-URL: Bug Reports, https://github.com/upgini/upgini/issues
6
6
  Project-URL: Homepage, https://upgini.com/
@@ -0,0 +1 @@
1
+ __version__ = "1.2.158.a4180.dev2"
@@ -1,13 +1,13 @@
1
1
  import abc
2
2
  import json
3
- from typing import Dict, List, Optional, Union
3
+ from typing import Dict, List, NamedTuple, Optional, Union
4
4
 
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
  from pandas.core.arrays.timedeltas import TimedeltaArray
8
8
  from pydantic import BaseModel, __version__ as pydantic_version
9
9
 
10
- from upgini.autofe.operand import OperandValue
10
+ from upgini.autofe.operand import OperandKind, OperandValue
11
11
  from upgini.autofe.operator import PandasOperator, ParametrizedOperator
12
12
  from upgini.autofe.utils import bin_index, bin_index_many, bin_index_vectorized, pydantic_validator
13
13
 
@@ -114,8 +114,57 @@ class DateDiffType2(PandasOperator, DateDiffMixin):
114
114
  return diff
115
115
 
116
116
 
117
- _ext_aggregations = {"nunique": (lambda x: len(np.unique(x)), 0), "count": (len, 0)}
117
+ _ext_aggregations = {"nunique": (lambda x: float(np.unique(x).size), 0), "count": (len, 0)}
118
118
  _count_aggregations = ["nunique", "count"]
119
+ _DATE_DIFF_LISTS_LENGTH_COL = 0
120
+ _NS_PER_DAY = np.float64(86400 * 10**9)
121
+ _NS_PER_YEAR = np.float64(365 * 86400 * 10**9)
122
+ _MATRIX_AGGREGATIONS = {
123
+ "nunique": "_matrix_agg_nunique",
124
+ "count": "_matrix_agg_count",
125
+ "sum": "_matrix_agg_sum",
126
+ "mean": "_matrix_agg_mean",
127
+ "min": "_matrix_agg_min",
128
+ "max": "_matrix_agg_max",
129
+ }
130
+
131
+
132
+ class _MatrixAggContext(NamedTuple):
133
+ lengths: np.ndarray
134
+ masked_values: np.ndarray
135
+ valid_mask: np.ndarray
136
+ missing: np.ndarray
137
+ empty: np.ndarray
138
+ agg_source: np.ndarray
139
+ count_source: np.ndarray
140
+ has_bounds: bool
141
+ results: np.ndarray
142
+
143
+
144
+ def _timedelta_ns_to_diff_unit(delta_ns: np.ndarray, diff_unit: str) -> np.ndarray:
145
+ if diff_unit == "D":
146
+ return delta_ns / _NS_PER_DAY
147
+ if diff_unit == "Y":
148
+ return (delta_ns / _NS_PER_YEAR).astype(np.int64).astype(np.float64)
149
+ raise ValueError(f"Unsupported difference unit: {diff_unit}")
150
+
151
+
152
+ def _group_cumcount(group_keys: np.ndarray) -> np.ndarray:
153
+ n = len(group_keys)
154
+ if n == 0:
155
+ return np.zeros(0, dtype=np.intp)
156
+ order = np.argsort(group_keys, kind="stable")
157
+ sorted_keys = group_keys[order]
158
+ group_change = np.empty(n, dtype=bool)
159
+ group_change[0] = True
160
+ if n > 1:
161
+ group_change[1:] = sorted_keys[1:] != sorted_keys[:-1]
162
+ group_ids = np.cumsum(group_change) - 1
163
+ group_start_idx = np.flatnonzero(group_change)
164
+ sorted_cumcount = np.arange(n, dtype=np.intp) - group_start_idx[group_ids]
165
+ cumcount = np.empty(n, dtype=np.intp)
166
+ cumcount[order] = sorted_cumcount
167
+ return cumcount
119
168
 
120
169
 
121
170
  def _aggregate_diffs(values: np.ndarray, aggregation: str) -> float:
@@ -163,54 +212,71 @@ class DateListDiffLists(PandasOperator, DateDiffMixin, ParametrizedOperator):
163
212
  return cls(diff_unit=diff_unit)
164
213
  return None
165
214
 
166
- @staticmethod
167
- def _non_empty_list_mask(right: pd.Series) -> pd.Series:
168
- values = right.to_numpy()
169
- mask = np.empty(len(values), dtype=bool)
170
- for i, value in enumerate(values):
171
- if value is None or (isinstance(value, float) and np.isnan(value)):
172
- mask[i] = False
173
- elif isinstance(value, (list, tuple, np.ndarray)):
174
- mask[i] = len(value) > 0
175
- else:
176
- mask[i] = False
177
- return pd.Series(mask, index=right.index)
178
-
179
- def _convert_date_lists(self, lists: pd.Series) -> pd.Series:
180
- exploded = lists.explode()
181
- converted = pd.to_datetime(exploded, unit=self.right_unit, errors="coerce")
182
- return pd.Series(
183
- {
184
- idx: pd.arrays.DatetimeArray(values.to_numpy())
185
- for idx, values in converted.groupby(converted.index, sort=False)
186
- }
187
- )
215
+ def _non_empty_list_mask(self, right: pd.Series) -> pd.Series:
216
+ return right.map(lambda value: isinstance(value, (list, tuple, np.ndarray)) and len(value) > 0).fillna(False)
217
+
218
+ def _build_matrix(self, left: pd.Series, right: pd.Series) -> np.ndarray:
219
+ n = len(left)
220
+ if n == 0:
221
+ return np.empty((0, 1), dtype=np.float64)
222
+
223
+ left_dates = pd.to_datetime(left, unit=self.left_unit, errors="coerce")
224
+ date_unit = self.right_unit
225
+ right_mask = self._non_empty_list_mask(right).to_numpy()
226
+ right_notna = right.notna().to_numpy()
227
+ left_notna = left_dates.notna().to_numpy()
228
+
229
+ compute_mask = left_notna & right_notna & right_mask
230
+ empty_right = right_notna & ~right_mask
231
+
232
+ lengths = np.full(n, np.nan, dtype=np.float64)
233
+ lengths[empty_right] = 0.0
234
+
235
+ compute_idx = np.flatnonzero(compute_mask)
236
+ if len(compute_idx) == 0:
237
+ return lengths.reshape(n, 1)
238
+
239
+ exploded = right.iloc[compute_idx].explode()
240
+ row_indices_arr = right.index.get_indexer(exploded.index).astype(np.intp)
241
+ raw_dates = exploded.to_numpy()
242
+ pos_in_row_arr = _group_cumcount(row_indices_arr)
243
+ converted = pd.to_datetime(pd.Series(raw_dates), unit=date_unit, errors="coerce")
244
+ left_ns = left_dates.iloc[row_indices_arr].astype(np.int64).to_numpy()
245
+ right_ns = converted.astype(np.int64).to_numpy()
246
+ diffs = np.full(len(row_indices_arr), np.nan, dtype=np.float64)
247
+ valid_ts = converted.notna().to_numpy()
248
+ if valid_ts.any():
249
+ diffs[valid_ts] = _timedelta_ns_to_diff_unit(left_ns[valid_ts] - right_ns[valid_ts], self.diff_unit)
188
250
 
189
- def _row_diffs(self, left_date, right_dates: pd.arrays.DatetimeArray) -> List[float]:
190
- diffs = self._convert_diff_to_unit(left_date - right_dates)
191
251
  if self.replace_negative:
192
- diffs = diffs[diffs > 0]
193
- return np.atleast_1d(np.asarray(diffs, dtype=np.float64)).tolist()
194
-
195
- def calculate_binary(self, left: OperandValue, right: OperandValue) -> pd.Series:
252
+ keep = diffs > 0
253
+ row_indices_arr = row_indices_arr[keep]
254
+ diffs = diffs[keep]
255
+ lengths[compute_mask] = 0.0
256
+ if len(row_indices_arr):
257
+ pos_in_row_arr = _group_cumcount(row_indices_arr)
258
+ row_lengths = np.bincount(row_indices_arr, minlength=n).astype(np.float64)
259
+ positive_rows = np.flatnonzero(row_lengths > 0)
260
+ lengths[positive_rows] = row_lengths[positive_rows]
261
+ else:
262
+ row_lengths = np.bincount(row_indices_arr, minlength=n).astype(np.float64)
263
+ lengths[compute_mask] = row_lengths[compute_mask]
264
+
265
+ finite_lengths = lengths[np.isfinite(lengths)]
266
+ k_max = int(finite_lengths.max()) if finite_lengths.size else 0
267
+ matrix = np.full((n, 1 + k_max), np.nan, dtype=np.float64)
268
+ matrix[:, _DATE_DIFF_LISTS_LENGTH_COL] = lengths
269
+ if k_max > 0 and len(row_indices_arr):
270
+ matrix[row_indices_arr, pos_in_row_arr + 1] = diffs
271
+ return matrix
272
+
273
+ def calculate_binary(self, left: OperandValue, right: OperandValue) -> np.ndarray:
196
274
  left = left.as_series()
197
275
  right = right.as_series()
198
276
  if left.isna().all() or right.isna().all():
199
- return pd.Series([None] * len(left), index=left.index, dtype=object)
200
-
201
- left = self._convert_to_date(left, self.left_unit)
202
- right_mask = self._non_empty_list_mask(right)
203
- mask = left.notna() & right.notna() & right_mask
277
+ return np.full((len(left), 1), np.nan, dtype=np.float64)
204
278
 
205
- results = pd.Series([None] * len(left), index=left.index, dtype=object)
206
- if not mask.any():
207
- return results
208
-
209
- masked_left = left[mask]
210
- converted_lists = self._convert_date_lists(right[mask])
211
- for idx, left_date in masked_left.items():
212
- results.loc[idx] = self._row_diffs(left_date, converted_lists[idx])
213
- return results
279
+ return self._build_matrix(left, right)
214
280
 
215
281
 
216
282
  class DateListDiffAggWithinBounds(PandasOperator, ParametrizedOperator):
@@ -263,6 +329,95 @@ class DateListDiffAggWithinBounds(PandasOperator, ParametrizedOperator):
263
329
  normalize=normalize,
264
330
  )
265
331
 
332
+ def _masked_values(self, matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
333
+ lengths = matrix[:, _DATE_DIFF_LISTS_LENGTH_COL]
334
+ values = matrix[:, _DATE_DIFF_LISTS_LENGTH_COL + 1 :]
335
+ missing = np.isnan(lengths)
336
+ empty = (~missing) & (lengths == 0)
337
+ if values.shape[1] == 0:
338
+ valid_mask = np.zeros((len(lengths), 0), dtype=bool)
339
+ else:
340
+ valid_mask = np.arange(values.shape[1])[None, :] < lengths[:, None]
341
+ masked_values = np.where(valid_mask, values, np.nan)
342
+ return lengths, masked_values, valid_mask, missing, empty
343
+
344
+ def _matrix_agg_nunique(self, ctx: _MatrixAggContext) -> None:
345
+ ctx.results[ctx.empty] = 0.0
346
+ active = ~ctx.missing & ~ctx.empty
347
+ if ctx.has_bounds:
348
+ select_mask = ctx.count_source & active[:, None]
349
+ else:
350
+ select_mask = ctx.valid_mask & active[:, None]
351
+ rows, _ = np.nonzero(select_mask)
352
+ ctx.results[active] = 0.0
353
+ if rows.size:
354
+ vals = ctx.masked_values[select_mask]
355
+ counts = pd.Series(vals).groupby(rows, sort=False).nunique(dropna=False)
356
+ ctx.results[counts.index.to_numpy(dtype=np.intp)] = counts.to_numpy(dtype=np.float64)
357
+
358
+ def _matrix_agg_count(self, ctx: _MatrixAggContext) -> None:
359
+ ctx.results[~ctx.missing] = ctx.count_source[~ctx.missing].sum(axis=1).astype(np.float64)
360
+
361
+ def _matrix_agg_sum(self, ctx: _MatrixAggContext) -> None:
362
+ agg_rows = ~ctx.missing & ~ctx.empty
363
+ if agg_rows.any():
364
+ with np.errstate(all="ignore"):
365
+ ctx.results[agg_rows] = np.nansum(ctx.agg_source[agg_rows], axis=1)
366
+
367
+ def _matrix_agg_nanaxis(self, ctx: _MatrixAggContext, reducer) -> None:
368
+ agg_rows = ~ctx.missing & ~ctx.empty
369
+ if not agg_rows.any():
370
+ return
371
+ has_finite = np.any(np.isfinite(ctx.agg_source[agg_rows]), axis=1)
372
+ finite_rows = np.flatnonzero(agg_rows)[has_finite]
373
+ with np.errstate(all="ignore"):
374
+ ctx.results[finite_rows] = reducer(ctx.agg_source[finite_rows], axis=1)
375
+
376
+ def _matrix_agg_mean(self, ctx: _MatrixAggContext) -> None:
377
+ self._matrix_agg_nanaxis(ctx, np.nanmean)
378
+
379
+ def _matrix_agg_min(self, ctx: _MatrixAggContext) -> None:
380
+ self._matrix_agg_nanaxis(ctx, np.nanmin)
381
+
382
+ def _matrix_agg_max(self, ctx: _MatrixAggContext) -> None:
383
+ self._matrix_agg_nanaxis(ctx, np.nanmax)
384
+
385
+ def _calculate_unary_matrix(self, matrix: np.ndarray, index: pd.Index) -> pd.Series:
386
+ lengths, masked_values, valid_mask, missing, empty = self._masked_values(matrix)
387
+ results = np.full(len(lengths), np.nan, dtype=np.float64)
388
+ has_bounds = self.lower_bound is not None or self.upper_bound is not None
389
+
390
+ if has_bounds:
391
+ lower = self.lower_bound if self.lower_bound is not None else -np.inf
392
+ upper = self.upper_bound if self.upper_bound is not None else np.inf
393
+ in_bounds = (masked_values >= lower) & (masked_values < upper)
394
+ agg_source = np.where(in_bounds & valid_mask, masked_values, np.nan)
395
+ count_source = in_bounds & valid_mask
396
+ else:
397
+ agg_source = masked_values
398
+ count_source = valid_mask
399
+
400
+ ctx = _MatrixAggContext(
401
+ lengths=lengths,
402
+ masked_values=masked_values,
403
+ valid_mask=valid_mask,
404
+ missing=missing,
405
+ empty=empty,
406
+ agg_source=agg_source,
407
+ count_source=count_source,
408
+ has_bounds=has_bounds,
409
+ results=results,
410
+ )
411
+ method_name = _MATRIX_AGGREGATIONS.get(self.aggregation)
412
+ if method_name is None:
413
+ raise ValueError(f"Unsupported aggregation: {self.aggregation}")
414
+ getattr(self, method_name)(ctx)
415
+
416
+ if self.normalize:
417
+ normalize_mask = ~missing & ~empty & (lengths > 0)
418
+ results[normalize_mask] = results[normalize_mask] / lengths[normalize_mask]
419
+ return pd.Series(results, index=index, dtype=np.float64)
420
+
266
421
  def _aggregate_row(self, diffs) -> float:
267
422
  if diffs is None or (isinstance(diffs, float) and np.isnan(diffs)):
268
423
  return np.nan
@@ -279,6 +434,9 @@ class DateListDiffAggWithinBounds(PandasOperator, ParametrizedOperator):
279
434
  return agg_res
280
435
 
281
436
  def calculate_unary(self, data: OperandValue) -> pd.Series:
437
+ if data.kind == OperandKind.MATRIX:
438
+ return self._calculate_unary_matrix(data.as_matrix(), data.index)
439
+
282
440
  data = data.as_series()
283
441
  results = np.empty(len(data), dtype=np.float64)
284
442
  results[:] = np.nan
@@ -339,7 +497,7 @@ class DateListDiff(PandasOperator, DateDiffMixin, ParametrizedOperator):
339
497
  if left.isna().all() or right.isna().all():
340
498
  return pd.Series([None] * len(left), index=left.index, dtype=np.float64)
341
499
 
342
- right_mask = DateListDiffLists._non_empty_list_mask(right)
500
+ right_mask = self._lists_op()._non_empty_list_mask(right)
343
501
  diff_lists = self._lists_op().calculate(left=left, right=right)
344
502
  result = self._agg_op().calculate(data=diff_lists).as_series()
345
503
  if self.aggregation in _count_aggregations:
@@ -1,9 +1,9 @@
1
1
  import pandas as pd
2
- from typing import Dict, Optional, Union
2
+ from typing import Dict, Optional
3
3
 
4
4
  from upgini.autofe.operator import ParametrizedOperator
5
5
  from upgini.autofe.timeseries.base import TimeSeriesBase
6
- from upgini.autofe.timeseries.lag import Lag
6
+ from upgini.autofe.timeseries.numpy_kernels import apply_grouped_kernel, delta2_values, delta_values
7
7
 
8
8
 
9
9
  class DeltaBase(TimeSeriesBase):
@@ -20,15 +20,6 @@ class DeltaBase(TimeSeriesBase):
20
20
  )
21
21
  return res
22
22
 
23
- def _calculate_delta(self, x: Union[pd.DataFrame, pd.Series]) -> Union[pd.DataFrame, pd.Series]:
24
- return_series = isinstance(x, pd.Series)
25
- x = pd.DataFrame(x)
26
- value_col = x.columns[-1]
27
- x[value_col] = pd.to_numeric(x[value_col], errors="coerce").astype("float64")
28
- lag = Lag(lag_size=self.delta_size, lag_unit=self.delta_unit)
29
- x.iloc[:, -1] = x.iloc[:, -1] - lag._aggregate(x.iloc[:, -1])
30
- return x.iloc[:, -1] if return_series else x
31
-
32
23
 
33
24
  class Delta(DeltaBase, ParametrizedOperator):
34
25
  def to_formula(self) -> str:
@@ -69,7 +60,11 @@ class Delta(DeltaBase, ParametrizedOperator):
69
60
  return cls(**params)
70
61
 
71
62
  def _aggregate(self, ts: pd.DataFrame) -> pd.DataFrame:
72
- return ts.apply(self._calculate_delta).iloc[:, [-1]]
63
+ delta_size = self.delta_size
64
+ delta_unit = self.delta_unit
65
+ return apply_grouped_kernel(
66
+ ts, lambda times, values: delta_values(times, values, delta_size, delta_unit)
67
+ )
73
68
 
74
69
 
75
70
  class Delta2(DeltaBase, ParametrizedOperator):
@@ -111,11 +106,8 @@ class Delta2(DeltaBase, ParametrizedOperator):
111
106
  return cls(**params)
112
107
 
113
108
  def _aggregate(self, ts: pd.DataFrame) -> pd.DataFrame:
114
- return ts.apply(self._calculate_delta2).iloc[:, [-1]]
115
-
116
- def _calculate_delta2(self, x):
117
- # Calculate first delta
118
- first_delta = self._calculate_delta(x)
119
-
120
- # Calculate delta of delta (second derivative)
121
- return self._calculate_delta(first_delta)
109
+ delta_size = self.delta_size
110
+ delta_unit = self.delta_unit
111
+ return apply_grouped_kernel(
112
+ ts, lambda times, values: delta2_values(times, values, delta_size, delta_unit)
113
+ )
@@ -1,9 +1,9 @@
1
- import numpy as np
2
1
  import pandas as pd
3
2
  from typing import Dict, Optional
4
3
 
5
4
  from upgini.autofe.operator import ParametrizedOperator
6
5
  from upgini.autofe.timeseries.base import TimeSeriesBase
6
+ from upgini.autofe.timeseries.numpy_kernels import apply_grouped_kernel, lag_values
7
7
 
8
8
 
9
9
  class Lag(TimeSeriesBase, ParametrizedOperator):
@@ -58,11 +58,6 @@ class Lag(TimeSeriesBase, ParametrizedOperator):
58
58
  return res
59
59
 
60
60
  def _aggregate(self, ts: pd.DataFrame) -> pd.DataFrame:
61
- lag_window = self.lag_size + 1
62
- return ts.rolling(f"{lag_window}{self.lag_unit}", min_periods=1).agg(self._lag)
63
-
64
- def _lag(self, x):
65
- if x.index.min() > (x.index.max() - pd.Timedelta(self.lag_size, self.lag_unit)):
66
- return np.nan
67
- else:
68
- return x[0]
61
+ lag_size = self.lag_size
62
+ lag_unit = self.lag_unit
63
+ return apply_grouped_kernel(ts, lambda times, values: lag_values(times, values, lag_size, lag_unit))
@@ -0,0 +1,259 @@
1
+ """Numpy kernels for time-series AutoFE operators.
2
+
3
+ Window semantics match pandas time-based rolling with a left-open interval
4
+ ``(t - window, t]`` (see ``Lag`` / ``Roll`` operators and their tests).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Callable, Union
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ # Aggregation applied to values in a time window (may be length 1+).
15
+ WindowAgg = Callable[[np.ndarray], float]
16
+
17
+
18
+ def _timedelta_ns(size: int, unit: str) -> int:
19
+ return int(pd.Timedelta(size, unit).to_timedelta64().astype("timedelta64[ns]").astype(np.int64))
20
+
21
+
22
+ def window_left_indices(times_ns: np.ndarray, window_ns: int) -> np.ndarray:
23
+ """For each t_i, first index j with times[j] > t_i - window (left-open)."""
24
+ times_ns = np.asarray(times_ns, dtype=np.int64)
25
+ return np.searchsorted(times_ns, times_ns - window_ns, side="right")
26
+
27
+
28
+ def lag_values(times_ns: np.ndarray, values: np.ndarray, lag_size: int, lag_unit: str) -> np.ndarray:
29
+ """Lag: oldest value in ``(t - (lag+1)·unit, t]`` if span covers ``lag``, else NaN."""
30
+ times_ns = np.asarray(times_ns, dtype=np.int64)
31
+ values = np.asarray(values, dtype=np.float64)
32
+ n = len(times_ns)
33
+ if n == 0:
34
+ return np.array([], dtype=np.float64)
35
+
36
+ lag_ns = _timedelta_ns(lag_size, lag_unit)
37
+ window_ns = _timedelta_ns(lag_size + 1, lag_unit)
38
+ lefts = window_left_indices(times_ns, window_ns)
39
+
40
+ out = np.full(n, np.nan, dtype=np.float64)
41
+ # Gate: oldest in window is at or before t - lag <=> times[left] <= t - lag
42
+ gate = times_ns[lefts] <= times_ns - lag_ns
43
+ out[gate] = values[lefts[gate]]
44
+ return out
45
+
46
+
47
+ def _rolling_mean(window: np.ndarray) -> float:
48
+ return float(np.mean(window))
49
+
50
+
51
+ def _rolling_min(window: np.ndarray) -> float:
52
+ return float(np.min(window))
53
+
54
+
55
+ def _rolling_max(window: np.ndarray) -> float:
56
+ return float(np.max(window))
57
+
58
+
59
+ def _rolling_median(window: np.ndarray) -> float:
60
+ return float(np.median(window))
61
+
62
+
63
+ def _rolling_std(window: np.ndarray) -> float:
64
+ # pandas rolling.std default ddof=1
65
+ if len(window) < 2:
66
+ return np.nan
67
+ return float(np.std(window, ddof=1))
68
+
69
+
70
+ def _rolling_norm_mean(window: np.ndarray) -> float:
71
+ mean = np.mean(window)
72
+ return float(window[-1] / mean)
73
+
74
+
75
+ def _rolling_q25(window: np.ndarray) -> float:
76
+ return float(np.quantile(window, 0.25, method="linear"))
77
+
78
+
79
+ def _rolling_q75(window: np.ndarray) -> float:
80
+ return float(np.quantile(window, 0.75, method="linear"))
81
+
82
+
83
+ def _rolling_iqr(window: np.ndarray) -> float:
84
+ q75, q25 = np.quantile(window, [0.75, 0.25], method="linear")
85
+ return float(q75 - q25)
86
+
87
+
88
+ ROLL_AGGS: dict[str, WindowAgg] = {
89
+ "mean": _rolling_mean,
90
+ "min": _rolling_min,
91
+ "max": _rolling_max,
92
+ "median": _rolling_median,
93
+ "std": _rolling_std,
94
+ "norm_mean": _rolling_norm_mean,
95
+ "q25": _rolling_q25,
96
+ "q75": _rolling_q75,
97
+ "iqr": _rolling_iqr,
98
+ }
99
+
100
+
101
+ def roll_values(
102
+ times_ns: np.ndarray,
103
+ values: np.ndarray,
104
+ window_size: int,
105
+ window_unit: str,
106
+ aggregation: str,
107
+ ) -> np.ndarray:
108
+ """Time-based rolling aggregation with ``min_periods=1`` semantics."""
109
+ times_ns = np.asarray(times_ns, dtype=np.int64)
110
+ values = np.asarray(values, dtype=np.float64)
111
+ n = len(times_ns)
112
+ if n == 0:
113
+ return np.array([], dtype=np.float64)
114
+
115
+ if aggregation not in ROLL_AGGS:
116
+ raise ValueError(f"Unsupported roll aggregation for numpy path: {aggregation}")
117
+
118
+ window_ns = _timedelta_ns(window_size, window_unit)
119
+ lefts = window_left_indices(times_ns, window_ns)
120
+ counts = np.arange(n, dtype=np.int64) - lefts + 1
121
+
122
+ if aggregation in {"mean", "norm_mean", "std"}:
123
+ csum = np.empty(n + 1, dtype=np.float64)
124
+ csum[0] = 0.0
125
+ np.cumsum(values, out=csum[1:])
126
+ window_sum = csum[1:] - csum[lefts]
127
+ means = window_sum / counts
128
+ if aggregation == "mean":
129
+ return means
130
+ if aggregation == "norm_mean":
131
+ return values / means
132
+ # std, ddof=1
133
+ csum2 = np.empty(n + 1, dtype=np.float64)
134
+ csum2[0] = 0.0
135
+ np.cumsum(values * values, out=csum2[1:])
136
+ window_sum2 = csum2[1:] - csum2[lefts]
137
+ out = np.full(n, np.nan, dtype=np.float64)
138
+ multi = counts >= 2
139
+ # sample variance: (sumsq - sum^2/n) / (n-1)
140
+ var = (window_sum2[multi] - window_sum[multi] * window_sum[multi] / counts[multi]) / (counts[multi] - 1)
141
+ # numerical noise can be slightly negative
142
+ out[multi] = np.sqrt(np.maximum(var, 0.0))
143
+ return out
144
+
145
+ if aggregation == "min":
146
+ # Falling window minima via brute force is fine for correctness; still O(n·w) worst case.
147
+ # Use a simple loop — windows are typically small calendar spans.
148
+ out = np.empty(n, dtype=np.float64)
149
+ for i in range(n):
150
+ out[i] = np.min(values[lefts[i] : i + 1])
151
+ return out
152
+
153
+ if aggregation == "max":
154
+ out = np.empty(n, dtype=np.float64)
155
+ for i in range(n):
156
+ out[i] = np.max(values[lefts[i] : i + 1])
157
+ return out
158
+
159
+ agg = ROLL_AGGS[aggregation]
160
+ out = np.empty(n, dtype=np.float64)
161
+ for i in range(n):
162
+ out[i] = agg(values[lefts[i] : i + 1])
163
+ return out
164
+
165
+
166
+ def freq_pct_change(times_ns: np.ndarray, values: np.ndarray, step_size: int, step_unit: str) -> np.ndarray:
167
+ """Match ``Series.pct_change(freq=..., fill_method='pad').fillna(0)`` on sorted unique dates."""
168
+ times_ns = np.asarray(times_ns, dtype=np.int64)
169
+ values = np.asarray(values, dtype=np.float64)
170
+ n = len(times_ns)
171
+ if n == 0:
172
+ return np.array([], dtype=np.float64)
173
+
174
+ # pandas pct_change default fill_method='pad' forward-fills before differencing
175
+ filled = values.copy()
176
+ is_valid = np.isfinite(filled)
177
+ if not is_valid.all():
178
+ idx = np.where(is_valid, np.arange(n), 0)
179
+ np.maximum.accumulate(idx, out=idx)
180
+ # keep leading NaNs as NaN
181
+ first_valid = int(np.argmax(is_valid)) if is_valid.any() else n
182
+ filled = filled[idx]
183
+ filled[:first_valid] = np.nan
184
+
185
+ step_ns = _timedelta_ns(step_size, step_unit)
186
+ targets = times_ns - step_ns
187
+ idx = np.searchsorted(times_ns, targets, side="left")
188
+ out = np.zeros(n, dtype=np.float64)
189
+ in_range = idx < n
190
+ matches = np.zeros(n, dtype=bool)
191
+ matches[in_range] = times_ns[idx[in_range]] == targets[in_range]
192
+ prev = filled[idx[matches]]
193
+ cur = filled[matches]
194
+ with np.errstate(divide="ignore", invalid="ignore"):
195
+ pct = cur / prev - 1.0
196
+ out[matches] = pct
197
+ out[~np.isfinite(out)] = 0.0
198
+ return out
199
+
200
+
201
+ def delta_values(times_ns: np.ndarray, values: np.ndarray, delta_size: int, delta_unit: str) -> np.ndarray:
202
+ values = np.asarray(values, dtype=np.float64)
203
+ return values - lag_values(times_ns, values, delta_size, delta_unit)
204
+
205
+
206
+ def delta2_values(times_ns: np.ndarray, values: np.ndarray, delta_size: int, delta_unit: str) -> np.ndarray:
207
+ first = delta_values(times_ns, values, delta_size, delta_unit)
208
+ return delta_values(times_ns, first, delta_size, delta_unit)
209
+
210
+
211
+ def rolling_volatility_values(
212
+ times_ns: np.ndarray,
213
+ values: np.ndarray,
214
+ step_size: int,
215
+ step_unit: str,
216
+ window_size: int,
217
+ window_unit: str,
218
+ abs_returns: bool = False,
219
+ ) -> np.ndarray:
220
+ returns = freq_pct_change(times_ns, values, step_size, step_unit)
221
+ if abs_returns:
222
+ returns = np.abs(returns)
223
+ return roll_values(times_ns, returns, window_size, window_unit, "std")
224
+
225
+
226
+ def _kernel_on_frame(
227
+ frame: pd.DataFrame,
228
+ kernel: Callable[[np.ndarray, np.ndarray], np.ndarray],
229
+ ) -> pd.DataFrame:
230
+ if frame.empty:
231
+ return frame.iloc[:, [-1]].astype(np.float64) if len(frame.columns) else frame
232
+
233
+ value_col = frame.columns[-1]
234
+ series = pd.to_numeric(frame[value_col], errors="coerce").astype(np.float64)
235
+ index = frame.index
236
+ if isinstance(index, pd.MultiIndex):
237
+ times_ns = index.get_level_values(-1).asi8
238
+ else:
239
+ times_ns = index.asi8
240
+ out = kernel(times_ns, series.to_numpy(dtype=np.float64, copy=False))
241
+ return pd.DataFrame({value_col: out}, index=frame.index, dtype=np.float64)
242
+
243
+
244
+ def apply_grouped_kernel(
245
+ ts: Union[pd.DataFrame, pd.core.groupby.generic.DataFrameGroupBy],
246
+ kernel: Callable[[np.ndarray, np.ndarray], np.ndarray],
247
+ ) -> pd.DataFrame:
248
+ """Apply ``kernel(times_ns, values) -> values`` on a DatetimeIndex frame or GroupBy."""
249
+ if isinstance(ts, pd.DataFrame):
250
+ return _kernel_on_frame(ts, kernel)
251
+
252
+ try:
253
+ applied = ts.apply(lambda g: _kernel_on_frame(g, kernel), include_groups=False)
254
+ except TypeError:
255
+ # pandas without include_groups
256
+ applied = ts.apply(lambda g: _kernel_on_frame(g, kernel))
257
+ if isinstance(applied, pd.Series):
258
+ return applied.to_frame()
259
+ return applied.iloc[:, [-1]] if getattr(applied, "shape", (0, 0))[1] > 1 else applied
@@ -3,14 +3,15 @@ from typing import Dict, Optional
3
3
 
4
4
  from upgini.autofe.operator import ParametrizedOperator
5
5
  from upgini.autofe.timeseries.base import TimeSeriesBase
6
+ from upgini.autofe.timeseries.numpy_kernels import ROLL_AGGS, apply_grouped_kernel, roll_values
6
7
  from upgini.autofe.utils import pydantic_validator
7
8
 
8
- # Roll aggregation functions
9
+ # Names kept for compatibility; computation uses numpy ROLL_AGGS.
9
10
  roll_aggregations = {
10
- "norm_mean": lambda x: x[-1] / x.mean(),
11
- "q25": lambda x: x.quantile(0.25),
12
- "q75": lambda x: x.quantile(0.75),
13
- "iqr": lambda x: x.quantile(0.75) - x.quantile(0.25),
11
+ "norm_mean": ROLL_AGGS["norm_mean"],
12
+ "q25": ROLL_AGGS["q25"],
13
+ "q75": ROLL_AGGS["q75"],
14
+ "iqr": ROLL_AGGS["iqr"],
14
15
  }
15
16
 
16
17
 
@@ -82,6 +83,12 @@ class Roll(TimeSeriesBase, ParametrizedOperator):
82
83
  return res
83
84
 
84
85
  def _aggregate(self, ts: pd.DataFrame) -> pd.DataFrame:
85
- return ts.rolling(f"{self.window_size}{self.window_unit}", min_periods=1).agg(
86
- roll_aggregations.get(self.aggregation, self.aggregation)
86
+ window_size = self.window_size
87
+ window_unit = self.window_unit
88
+ aggregation = self.aggregation
89
+ if aggregation not in ROLL_AGGS:
90
+ return ts.rolling(f"{window_size}{window_unit}", min_periods=1).agg(aggregation)
91
+ return apply_grouped_kernel(
92
+ ts,
93
+ lambda times, values: roll_values(times, values, window_size, window_unit, aggregation),
87
94
  )
@@ -1,9 +1,10 @@
1
- from typing import Dict, Optional, Union
1
+ from typing import Dict, Optional
2
2
 
3
3
  import numpy as np
4
4
  import pandas as pd
5
5
  from upgini.autofe.operator import ParametrizedOperator
6
6
  from upgini.autofe.timeseries.base import TimeSeriesBase
7
+ from upgini.autofe.timeseries.numpy_kernels import apply_grouped_kernel, rolling_volatility_values
7
8
 
8
9
 
9
10
  class VolatilityBase(TimeSeriesBase):
@@ -90,18 +91,16 @@ class RollingVolBase(VolatilityBase):
90
91
  )
91
92
  return res
92
93
 
93
- def _rolling_vol(
94
- self, x: Union[pd.DataFrame, pd.Series], window_size: int, window_unit: str, abs_returns: bool = False
95
- ) -> Union[pd.DataFrame, pd.Series]:
96
- return_series = isinstance(x, pd.Series)
97
- x = pd.DataFrame(x)
98
- value_col = x.columns[-1]
99
- x[value_col] = pd.to_numeric(x[value_col], errors="coerce").astype("float64")
100
- returns = self._get_returns(x.iloc[:, -1], f"{self.step_size}{self.step_unit}")
101
- if abs_returns:
102
- returns = returns.abs()
103
- x.iloc[:, -1] = returns.rolling(f"{window_size}{window_unit}", min_periods=1).std()
104
- return x.iloc[:, -1] if return_series else x
94
+ def _rolling_vol_kernel(self, window_size: int, window_unit: str, abs_returns: bool = False):
95
+ step_size = self.step_size
96
+ step_unit = self.step_unit
97
+
98
+ def kernel(times_ns: np.ndarray, values: np.ndarray) -> np.ndarray:
99
+ return rolling_volatility_values(
100
+ times_ns, values, step_size, step_unit, window_size, window_unit, abs_returns=abs_returns
101
+ )
102
+
103
+ return kernel
105
104
 
106
105
 
107
106
  class RollingVolatility(RollingVolBase, ParametrizedOperator):
@@ -141,9 +140,9 @@ class RollingVolatility(RollingVolBase, ParametrizedOperator):
141
140
  return cls(**params)
142
141
 
143
142
  def _aggregate(self, ts: pd.DataFrame) -> pd.DataFrame:
144
- return ts.apply(
145
- self._rolling_vol, window_size=self.window_size, window_unit=self.window_unit, abs_returns=self.abs_returns
146
- ).iloc[:, [-1]]
143
+ return apply_grouped_kernel(
144
+ ts, self._rolling_vol_kernel(self.window_size, self.window_unit, abs_returns=self.abs_returns)
145
+ )
147
146
 
148
147
 
149
148
  class RollingVolatility2(RollingVolBase, ParametrizedOperator):
@@ -185,12 +184,20 @@ class RollingVolatility2(RollingVolBase, ParametrizedOperator):
185
184
  return cls(**params)
186
185
 
187
186
  def _aggregate(self, ts: pd.DataFrame) -> pd.DataFrame:
188
- return ts.apply(self._vol_on_vol).iloc[:, [-1]]
187
+ window_size = self.window_size
188
+ window_unit = self.window_unit
189
+ step_size = self.step_size
190
+ step_unit = self.step_unit
189
191
 
190
- def _vol_on_vol(self, x: Union[pd.DataFrame, pd.Series]) -> Union[pd.DataFrame, pd.Series]:
191
- vol1 = self._rolling_vol(x, self.window_size, self.window_unit, abs_returns=True)
192
- vol2 = self._rolling_vol(vol1, self.window_size, self.window_unit, abs_returns=False)
193
- return vol2
192
+ def kernel(times_ns: np.ndarray, values: np.ndarray) -> np.ndarray:
193
+ vol1 = rolling_volatility_values(
194
+ times_ns, values, step_size, step_unit, window_size, window_unit, abs_returns=True
195
+ )
196
+ return rolling_volatility_values(
197
+ times_ns, vol1, step_size, step_unit, window_size, window_unit, abs_returns=False
198
+ )
199
+
200
+ return apply_grouped_kernel(ts, kernel)
194
201
 
195
202
 
196
203
  class VolatilityRatio(RollingVolBase, ParametrizedOperator):
@@ -252,13 +259,24 @@ class VolatilityRatio(RollingVolBase, ParametrizedOperator):
252
259
  return res
253
260
 
254
261
  def _aggregate(self, ts: pd.DataFrame) -> pd.DataFrame:
255
- return ts.apply(self._vol_ratio).iloc[:, [-1]]
256
-
257
- def _vol_ratio(self, x: Union[pd.DataFrame, pd.Series]) -> Union[pd.DataFrame, pd.Series]:
258
- short_vol = self._rolling_vol(x, self.short_window_size, self.short_window_unit)
259
- long_vol = self._rolling_vol(x, self.window_size, self.window_unit)
260
- ratio = VolatilityRatio._handle_div_errors(short_vol / long_vol)
261
- return ratio
262
+ short_window_size = self.short_window_size
263
+ short_window_unit = self.short_window_unit
264
+ window_size = self.window_size
265
+ window_unit = self.window_unit
266
+ step_size = self.step_size
267
+ step_unit = self.step_unit
268
+
269
+ def kernel(times_ns: np.ndarray, values: np.ndarray) -> np.ndarray:
270
+ short_vol = rolling_volatility_values(
271
+ times_ns, values, step_size, step_unit, short_window_size, short_window_unit
272
+ )
273
+ long_vol = rolling_volatility_values(times_ns, values, step_size, step_unit, window_size, window_unit)
274
+ with np.errstate(divide="ignore", invalid="ignore"):
275
+ ratio = short_vol / long_vol
276
+ ratio[~np.isfinite(ratio)] = np.nan
277
+ return np.where(np.isnan(ratio), 1.0, ratio)
278
+
279
+ return apply_grouped_kernel(ts, kernel)
262
280
 
263
281
  @staticmethod
264
282
  def _handle_div_errors(x: pd.Series) -> pd.Series:
@@ -1 +0,0 @@
1
- __version__ = "1.2.157.dev4"