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.
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/PKG-INFO +2 -2
- upgini-1.2.158a4180.dev2/src/upgini/__about__.py +1 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/date.py +204 -46
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/delta.py +12 -20
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/lag.py +4 -9
- upgini-1.2.158a4180.dev2/src/upgini/autofe/timeseries/numpy_kernels.py +259 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/roll.py +14 -7
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/volatility.py +46 -28
- upgini-1.2.157.dev4/src/upgini/__about__.py +0 -1
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/.gitignore +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/LICENSE +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/README.md +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/pyproject.toml +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/ads.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/binary.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/operand.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/unary.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/dataset.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/errors.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/features_enricher.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/http.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/metadata.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/metrics.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/strings.properties +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/search_task.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/spinner.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/display_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/psi.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/track_info.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/warning_counter.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: upgini
|
|
3
|
-
Version: 1.2.
|
|
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:
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
#
|
|
9
|
+
# Names kept for compatibility; computation uses numpy ROLL_AGGS.
|
|
9
10
|
roll_aggregations = {
|
|
10
|
-
"norm_mean":
|
|
11
|
-
"q25":
|
|
12
|
-
"q75":
|
|
13
|
-
"iqr":
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
)
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/data_source/data_source_publisher.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/resource_bundle/strings.properties
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{upgini-1.2.157.dev4 → upgini-1.2.158a4180.dev2}/src/upgini/utils/base_search_key_detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|