upgini 1.2.156.dev3__tar.gz → 1.2.156.dev5__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 (83) hide show
  1. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/PKG-INFO +1 -1
  2. upgini-1.2.156.dev5/src/upgini/__about__.py +1 -0
  3. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/binary.py +113 -16
  4. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/date.py +172 -45
  5. upgini-1.2.156.dev3/src/upgini/__about__.py +0 -1
  6. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/.gitignore +0 -0
  7. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/LICENSE +0 -0
  8. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/README.md +0 -0
  9. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/pyproject.toml +0 -0
  10. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/__init__.py +0 -0
  11. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/ads.py +0 -0
  12. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/ads_management/__init__.py +0 -0
  13. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/ads_management/ads_manager.py +0 -0
  14. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/__init__.py +0 -0
  15. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/all_operators.py +0 -0
  16. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/feature.py +0 -0
  17. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/groupby.py +0 -0
  18. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/operator.py +0 -0
  19. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/timeseries/__init__.py +0 -0
  20. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/timeseries/base.py +0 -0
  21. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/timeseries/cross.py +0 -0
  22. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/timeseries/delta.py +0 -0
  23. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/timeseries/lag.py +0 -0
  24. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/timeseries/roll.py +0 -0
  25. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/timeseries/trend.py +0 -0
  26. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/timeseries/volatility.py +0 -0
  27. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/unary.py +0 -0
  28. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/utils.py +0 -0
  29. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/autofe/vector.py +0 -0
  30. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/data_source/__init__.py +0 -0
  31. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/data_source/data_source_publisher.py +0 -0
  32. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/dataset.py +0 -0
  33. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/errors.py +0 -0
  34. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/features_enricher.py +0 -0
  35. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/http.py +0 -0
  36. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/mdc/__init__.py +0 -0
  37. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/mdc/context.py +0 -0
  38. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/metadata.py +0 -0
  39. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/metrics.py +0 -0
  40. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/normalizer/__init__.py +0 -0
  41. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/normalizer/normalize_utils.py +0 -0
  42. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/resource_bundle/__init__.py +0 -0
  43. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/resource_bundle/exceptions.py +0 -0
  44. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/resource_bundle/strings.properties +0 -0
  45. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  46. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/sampler/__init__.py +0 -0
  47. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/sampler/base.py +0 -0
  48. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/sampler/random_under_sampler.py +0 -0
  49. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/sampler/utils.py +0 -0
  50. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/search_task.py +0 -0
  51. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/spinner.py +0 -0
  52. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  53. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/__init__.py +0 -0
  54. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/base_search_key_detector.py +0 -0
  55. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/blocked_time_series.py +0 -0
  56. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/config.py +0 -0
  57. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/country_utils.py +0 -0
  58. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/custom_loss_utils.py +0 -0
  59. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/cv_utils.py +0 -0
  60. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/datetime_utils.py +0 -0
  61. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/deduplicate_utils.py +0 -0
  62. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/display_utils.py +0 -0
  63. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/email_utils.py +0 -0
  64. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/fallback_progress_bar.py +0 -0
  65. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/feature_info.py +0 -0
  66. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/features_validator.py +0 -0
  67. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/format.py +0 -0
  68. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/hash_utils.py +0 -0
  69. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/ip_utils.py +0 -0
  70. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/mstats.py +0 -0
  71. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/one_hot_encoder.py +0 -0
  72. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/phone_utils.py +0 -0
  73. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/postal_code_utils.py +0 -0
  74. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/progress_bar.py +0 -0
  75. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/psi.py +0 -0
  76. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/sample_utils.py +0 -0
  77. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/sklearn_ext.py +0 -0
  78. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/sort.py +0 -0
  79. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/target_utils.py +0 -0
  80. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/track_info.py +0 -0
  81. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/ts_utils.py +0 -0
  82. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/utils/warning_counter.py +0 -0
  83. {upgini-1.2.156.dev3 → upgini-1.2.156.dev5}/src/upgini/version_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: upgini
3
- Version: 1.2.156.dev3
3
+ Version: 1.2.156.dev5
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.156.dev5"
@@ -140,23 +140,120 @@ class Distance(PandasOperator):
140
140
  is_symmetrical: bool = True
141
141
  has_symmetry_importance: bool = True
142
142
 
143
+ def _to_float_vector(self, value) -> Optional[np.ndarray]:
144
+ if value is None or (isinstance(value, float) and np.isnan(value)):
145
+ return None
146
+ return np.atleast_1d(np.asarray(value, dtype=np.float64))
147
+
148
+ def _stack_vectors(self, arr: np.ndarray, indices: np.ndarray) -> Optional[np.ndarray]:
149
+ try:
150
+ stacked = np.vstack(arr[indices])
151
+ except (ValueError, TypeError):
152
+ return None
153
+ if stacked.ndim != 2 or stacked.shape[1] == 0:
154
+ return None
155
+ if stacked.dtype != np.float64:
156
+ stacked = stacked.astype(np.float64, copy=False)
157
+ return stacked
158
+
159
+ def _cosine_distance_stacked(self, left_mat: np.ndarray, right_mat: np.ndarray) -> np.ndarray:
160
+ if left_mat.shape != right_mat.shape:
161
+ raise ValueError("Left and right matrices must have the same shape")
162
+ dots = np.sum(left_mat * right_mat, axis=1)
163
+ denom = np.sqrt(np.sum(left_mat * left_mat, axis=1)) * np.sqrt(np.sum(right_mat * right_mat, axis=1))
164
+ results = np.full(len(left_mat), np.nan, dtype=np.float64)
165
+ valid = denom > 0
166
+ results[valid] = 1 - dots[valid] / denom[valid]
167
+ return results
168
+
169
+ def _try_fast_distance(
170
+ self, left_arr: np.ndarray, right_arr: np.ndarray, row_present: np.ndarray, n: int
171
+ ) -> Optional[np.ndarray]:
172
+ valid_idx = np.flatnonzero(row_present)
173
+ if len(valid_idx) == 0:
174
+ return None
175
+
176
+ left_mat = self._stack_vectors(left_arr, valid_idx)
177
+ right_mat = self._stack_vectors(right_arr, valid_idx)
178
+ if left_mat is None or right_mat is None:
179
+ return None
180
+
181
+ stacked_results = self._cosine_distance_stacked(left_mat, right_mat)
182
+ if row_present.all():
183
+ return stacked_results
184
+
185
+ results = np.full(n, np.nan, dtype=np.float64)
186
+ results[valid_idx] = stacked_results
187
+ return results
188
+
189
+ def _parse_vectors(self, left_arr: np.ndarray, right_arr: np.ndarray) -> tuple[list, list, np.ndarray]:
190
+ n = len(left_arr)
191
+ left_vecs: list[Optional[np.ndarray]] = [None] * n
192
+ right_vecs: list[Optional[np.ndarray]] = [None] * n
193
+ row_valid = np.zeros(n, dtype=bool)
194
+ for i, (left_value, right_value) in enumerate(zip(left_arr, right_arr)):
195
+ left_vec = self._to_float_vector(left_value)
196
+ right_vec = self._to_float_vector(right_value)
197
+ left_vecs[i] = left_vec
198
+ right_vecs[i] = right_vec
199
+ if left_vec is not None and right_vec is not None and left_vec.size > 0 and right_vec.size > 0:
200
+ row_valid[i] = True
201
+ return left_vecs, right_vecs, row_valid
202
+
203
+ def _masked_cosine_distance(
204
+ self, left_mat: np.ndarray, right_mat: np.ndarray, row_valid: np.ndarray
205
+ ) -> np.ndarray:
206
+ results = self._cosine_distance_stacked(left_mat, right_mat)
207
+ if len(results) == len(row_valid):
208
+ results[~row_valid] = np.nan
209
+ return results
210
+
211
+ def _calculate_rowwise(
212
+ self, results: np.ndarray, indices: np.ndarray, left_vecs: list, right_vecs: list
213
+ ) -> None:
214
+ for index in indices:
215
+ left_vec = left_vecs[index]
216
+ right_vec = right_vecs[index]
217
+ dot = np.sum(left_vec * right_vec)
218
+ denom = np.sqrt(np.sum(left_vec * left_vec)) * np.sqrt(np.sum(right_vec * right_vec))
219
+ if denom != 0:
220
+ results[index] = 1 - dot / denom
221
+
222
+ def _calculate_fallback_distance(
223
+ self, left_arr: np.ndarray, right_arr: np.ndarray, n: int, index: pd.Index
224
+ ) -> pd.Series:
225
+ results = np.full(n, np.nan, dtype=np.float64)
226
+ left_vecs, right_vecs, row_valid = self._parse_vectors(left_arr, right_arr)
227
+ if not row_valid.any():
228
+ return pd.Series(results, index=index, dtype=np.float64)
229
+
230
+ valid_idx = np.flatnonzero(row_valid)
231
+ dim = left_vecs[valid_idx[0]].size
232
+ if not all(left_vecs[i].size == dim and right_vecs[i].size == dim for i in valid_idx):
233
+ self._calculate_rowwise(results, valid_idx, left_vecs, right_vecs)
234
+ return pd.Series(results, index=index, dtype=np.float64)
235
+
236
+ left_mat = np.zeros((n, dim), dtype=np.float64)
237
+ right_mat = np.zeros((n, dim), dtype=np.float64)
238
+ left_mat[valid_idx] = np.stack([left_vecs[i] for i in valid_idx])
239
+ right_mat[valid_idx] = np.stack([right_vecs[i] for i in valid_idx])
240
+
241
+ return pd.Series(self._masked_cosine_distance(left_mat, right_mat, row_valid), index=index, dtype=np.float64)
242
+
143
243
  def calculate_binary(self, left: pd.Series, right: pd.Series) -> pd.Series:
144
- return pd.Series(
145
- 1 - self.__dot(left, right) / (self.__norm(left) * self.__norm(right)), index=left.index
146
- ).astype(np.float64)
147
-
148
- # row-wise dot product, handling None values
149
- def __dot(self, left: pd.Series, right: pd.Series) -> pd.Series:
150
- left = left.apply(lambda x: np.array(x).astype(np.float64))
151
- right = right.apply(lambda x: np.array(x).astype(np.float64))
152
- res = (left.dropna() * right.dropna()).apply(np.sum)
153
- res = res.reindex(left.index.union(right.index))
154
- return res
155
-
156
- # Calculate the norm of a vector, handling None values
157
- def __norm(self, vector: pd.Series) -> pd.Series:
158
- vector = vector.fillna(np.nan)
159
- return np.sqrt(self.__dot(vector, vector))
244
+ n = len(left)
245
+ if n == 0:
246
+ return pd.Series(np.full(n, np.nan), index=left.index, dtype=np.float64)
247
+
248
+ row_present = left.notna().to_numpy() & right.notna().to_numpy()
249
+ left_arr = left.to_numpy()
250
+ right_arr = right.to_numpy()
251
+
252
+ fast_results = self._try_fast_distance(left_arr, right_arr, row_present, n)
253
+ if fast_results is not None:
254
+ return pd.Series(fast_results, index=left.index, dtype=np.float64)
255
+
256
+ return self._calculate_fallback_distance(left_arr, right_arr, n, left.index)
160
257
 
161
258
 
162
259
  # Left for backward compatibility
@@ -113,18 +113,30 @@ _ext_aggregations = {"nunique": (lambda x: len(np.unique(x)), 0), "count": (len,
113
113
  _count_aggregations = ["nunique", "count"]
114
114
 
115
115
 
116
- class DateListDiff(PandasOperator, DateDiffMixin, ParametrizedOperator):
116
+ def _aggregate_diffs(values: np.ndarray, aggregation: str) -> float:
117
+ values = np.atleast_1d(np.asarray(values, dtype=np.float64))
118
+ method = getattr(np, aggregation, None)
119
+ default = np.nan
120
+ if method is None and aggregation in _ext_aggregations:
121
+ method, default = _ext_aggregations[aggregation]
122
+ elif not callable(method):
123
+ raise ValueError(f"Unsupported aggregation: {aggregation}")
124
+
125
+ return method(values) if len(values) > 0 else default
126
+
127
+
128
+ class DateListDiffLists(PandasOperator, DateDiffMixin, ParametrizedOperator):
129
+ name: str = "date_diff_lists"
117
130
  is_binary: bool = True
118
131
  has_symmetry_importance: bool = True
132
+ output_type: Optional[str] = "vector"
119
133
 
120
- aggregation: str
121
134
  replace_negative: bool = False
122
135
 
123
136
  def get_params(self) -> Dict[str, Optional[str]]:
124
137
  res = super().get_params()
125
138
  res.update(
126
139
  {
127
- "aggregation": self.aggregation,
128
140
  "diff_unit": self.diff_unit,
129
141
  "left_unit": self.left_unit,
130
142
  "right_unit": self.right_unit,
@@ -134,16 +146,17 @@ class DateListDiff(PandasOperator, DateDiffMixin, ParametrizedOperator):
134
146
  return res
135
147
 
136
148
  def to_formula(self) -> str:
137
- return f"date_diff_{self.aggregation}"
149
+ return f"date_diff_lists_{self.diff_unit}"
138
150
 
139
151
  @classmethod
140
- def from_formula(cls, formula: str) -> Optional["DateListDiff"]:
141
- if not formula.startswith("date_diff_") or formula.startswith("date_diff_type"):
142
- return None
143
- aggregation = formula.replace("date_diff_", "")
144
- if "_" in aggregation:
145
- return None
146
- return cls(aggregation=aggregation)
152
+ def from_formula(cls, formula: str) -> Optional["DateListDiffLists"]:
153
+ if formula == "date_diff_lists":
154
+ return cls()
155
+ if formula.startswith("date_diff_lists_"):
156
+ diff_unit = formula.replace("date_diff_lists_", "")
157
+ if diff_unit in {"D", "Y"}:
158
+ return cls(diff_unit=diff_unit)
159
+ return None
147
160
 
148
161
  @staticmethod
149
162
  def _non_empty_list_mask(right: pd.Series) -> pd.Series:
@@ -168,48 +181,165 @@ class DateListDiff(PandasOperator, DateDiffMixin, ParametrizedOperator):
168
181
  }
169
182
  )
170
183
 
184
+ def _row_diffs(self, left_date, right_dates: pd.arrays.DatetimeArray) -> List[float]:
185
+ diffs = self._convert_diff_to_unit(left_date - right_dates)
186
+ if self.replace_negative:
187
+ diffs = diffs[diffs > 0]
188
+ return np.atleast_1d(np.asarray(diffs, dtype=np.float64)).tolist()
189
+
171
190
  def calculate_binary(self, left: pd.Series, right: pd.Series) -> pd.Series:
172
191
  if left.isna().all() or right.isna().all():
173
- return pd.Series([None] * len(left), index=left.index, dtype=np.float64)
192
+ return pd.Series([None] * len(left), index=left.index, dtype=object)
174
193
 
175
194
  left = self._convert_to_date(left, self.left_unit)
176
195
  right_mask = self._non_empty_list_mask(right)
177
196
  mask = left.notna() & right.notna() & right_mask
178
197
 
198
+ results = pd.Series([None] * len(left), index=left.index, dtype=object)
179
199
  if not mask.any():
180
- res = pd.Series(np.nan, index=left.index, dtype=np.float64)
181
- if self.aggregation in _count_aggregations:
182
- res[~right_mask] = 0.0
183
- return res
200
+ return results
184
201
 
185
202
  masked_left = left[mask]
186
203
  converted_lists = self._convert_date_lists(right[mask])
187
- results = np.empty(len(masked_left), dtype=np.float64)
204
+ for idx, left_date in masked_left.items():
205
+ results.loc[idx] = self._row_diffs(left_date, converted_lists[idx])
206
+ return results
207
+
208
+
209
+ class DateListDiffAggWithinBounds(PandasOperator, ParametrizedOperator):
210
+ name: str = "date_diff_list_agg"
211
+ is_unary: bool = True
212
+ output_type: Optional[str] = "float"
213
+
214
+ lower_bound: Optional[int] = None
215
+ upper_bound: Optional[int] = None
216
+ aggregation: str
217
+ normalize: Optional[bool] = None
218
+
219
+ def get_params(self) -> Dict[str, Optional[str]]:
220
+ res = super().get_params()
221
+ res.update(
222
+ {
223
+ "aggregation": self.aggregation,
224
+ "lower_bound": str(self.lower_bound) if self.lower_bound is not None else None,
225
+ "upper_bound": str(self.upper_bound) if self.upper_bound is not None else None,
226
+ "normalize": str(self.normalize) if self.normalize is not None else None,
227
+ }
228
+ )
229
+ return res
230
+
231
+ def to_formula(self) -> str:
232
+ lower_bound = "minusinf" if self.lower_bound is None else self.lower_bound
233
+ upper_bound = "plusinf" if self.upper_bound is None else self.upper_bound
234
+ norm = "_norm" if self.normalize else ""
235
+ return f"date_diff_list_agg_{lower_bound}_{upper_bound}_{self.aggregation}{norm}"
236
+
237
+ @classmethod
238
+ def from_formula(cls, formula: str) -> Optional["DateListDiffAggWithinBounds"]:
239
+ import re
240
+
241
+ normalize = formula.endswith("_norm")
242
+ formula = formula.replace("_norm", "")
243
+
244
+ pattern = r"^date_diff_list_agg_((minusinf|\d+))_((plusinf|\d+))_(\w+)$"
245
+ match = re.match(pattern, formula)
246
+ if not match:
247
+ return None
248
+
249
+ lower_bound = None if match.group(1) == "minusinf" else int(match.group(1))
250
+ upper_bound = None if match.group(3) == "plusinf" else int(match.group(3))
251
+ aggregation = match.group(5)
252
+ return cls(
253
+ lower_bound=lower_bound,
254
+ upper_bound=upper_bound,
255
+ aggregation=aggregation,
256
+ normalize=normalize,
257
+ )
258
+
259
+ def _aggregate_row(self, diffs) -> float:
260
+ if diffs is None or (isinstance(diffs, float) and np.isnan(diffs)):
261
+ return np.nan
262
+
263
+ diffs = np.atleast_1d(np.asarray(diffs, dtype=np.float64))
264
+ orig_len = len(diffs)
265
+ if self.lower_bound is not None or self.upper_bound is not None:
266
+ lower = self.lower_bound if self.lower_bound is not None else -np.inf
267
+ upper = self.upper_bound if self.upper_bound is not None else np.inf
268
+ diffs = diffs[(diffs >= lower) & (diffs < upper)]
269
+ agg_res = _aggregate_diffs(diffs, self.aggregation)
270
+ if self.normalize and orig_len > 0:
271
+ return agg_res / orig_len
272
+ return agg_res
273
+
274
+ def calculate_unary(self, data: pd.Series) -> pd.Series:
275
+ results = np.empty(len(data), dtype=np.float64)
188
276
  results[:] = np.nan
189
- for i, (idx, left_date) in enumerate(masked_left.items()):
190
- results[i] = self._agg(self._diff(left_date - converted_lists[idx]))
277
+ for i, diffs in enumerate(data.to_numpy()):
278
+ results[i] = self._aggregate_row(diffs)
279
+ return pd.Series(results, index=data.index, dtype=np.float64)
191
280
 
192
- res_masked = pd.Series(results, index=masked_left.index)
193
- res = res_masked.reindex(left.index.union(right.index))
194
- if self.aggregation in _count_aggregations:
195
- res[~right_mask] = 0.0
196
- res = res.astype(np.float64)
197
281
 
282
+ class DateListDiff(PandasOperator, DateDiffMixin, ParametrizedOperator):
283
+ is_binary: bool = True
284
+ has_symmetry_importance: bool = True
285
+
286
+ aggregation: str
287
+ replace_negative: bool = False
288
+
289
+ def get_params(self) -> Dict[str, Optional[str]]:
290
+ res = super().get_params()
291
+ res.update(
292
+ {
293
+ "aggregation": self.aggregation,
294
+ "diff_unit": self.diff_unit,
295
+ "left_unit": self.left_unit,
296
+ "right_unit": self.right_unit,
297
+ "replace_negative": self.replace_negative,
298
+ }
299
+ )
198
300
  return res
199
301
 
200
- def _diff(self, x: TimedeltaArray):
201
- x = self._convert_diff_to_unit(x)
202
- return x[x > 0] if self.replace_negative else x
302
+ def to_formula(self) -> str:
303
+ return f"date_diff_{self.aggregation}"
203
304
 
204
- def _agg(self, x):
205
- method = getattr(np, self.aggregation, None)
206
- default = np.nan
207
- if method is None and self.aggregation in _ext_aggregations:
208
- method, default = _ext_aggregations[self.aggregation]
209
- elif not callable(method):
210
- raise ValueError(f"Unsupported aggregation: {self.aggregation}")
305
+ @classmethod
306
+ def from_formula(cls, formula: str) -> Optional["DateListDiff"]:
307
+ if not formula.startswith("date_diff_") or formula.startswith("date_diff_type"):
308
+ return None
309
+ aggregation = formula.replace("date_diff_", "")
310
+ if "_" in aggregation:
311
+ return None
312
+ return cls(aggregation=aggregation)
211
313
 
212
- return method(x) if len(x) > 0 else default
314
+ def _lists_op(self) -> DateListDiffLists:
315
+ return DateListDiffLists(
316
+ diff_unit=self.diff_unit,
317
+ left_unit=self.left_unit,
318
+ right_unit=self.right_unit,
319
+ replace_negative=self.replace_negative,
320
+ )
321
+
322
+ def _agg_op(self) -> DateListDiffAggWithinBounds:
323
+ return DateListDiffAggWithinBounds(
324
+ lower_bound=None,
325
+ upper_bound=None,
326
+ aggregation=self.aggregation,
327
+ normalize=False,
328
+ )
329
+
330
+ def _compose_list_diff(self, left: pd.Series, right: pd.Series) -> pd.Series:
331
+ if left.isna().all() or right.isna().all():
332
+ return pd.Series([None] * len(left), index=left.index, dtype=np.float64)
333
+
334
+ right_mask = DateListDiffLists._non_empty_list_mask(right)
335
+ diff_lists = self._lists_op().calculate_binary(left, right)
336
+ result = self._agg_op().calculate_unary(diff_lists)
337
+ if self.aggregation in _count_aggregations:
338
+ result[~right_mask] = 0.0
339
+ return result.astype(np.float64)
340
+
341
+ def calculate_binary(self, left: pd.Series, right: pd.Series) -> pd.Series:
342
+ return self._compose_list_diff(left, right)
213
343
 
214
344
 
215
345
  class DateListDiffBounded(DateListDiff, ParametrizedOperator):
@@ -258,16 +388,13 @@ class DateListDiffBounded(DateListDiff, ParametrizedOperator):
258
388
  res["normalize"] = str(self.normalize)
259
389
  return res
260
390
 
261
- def _agg(self, x):
262
- orig_len = len(x)
263
- x = x[
264
- (x >= (self.lower_bound if self.lower_bound is not None else -np.inf))
265
- & (x < (self.upper_bound if self.upper_bound is not None else np.inf))
266
- ]
267
- agg_res = super()._agg(x)
268
- if self.normalize and orig_len > 0:
269
- return agg_res / orig_len
270
- return agg_res
391
+ def _agg_op(self) -> DateListDiffAggWithinBounds:
392
+ return DateListDiffAggWithinBounds(
393
+ lower_bound=self.lower_bound,
394
+ upper_bound=self.upper_bound,
395
+ aggregation=self.aggregation,
396
+ normalize=self.normalize or False,
397
+ )
271
398
 
272
399
 
273
400
  class DatePercentileBase(PandasOperator, abc.ABC):
@@ -1 +0,0 @@
1
- __version__ = "1.2.156.dev3"
File without changes
File without changes
File without changes