upgini 1.2.156.dev5__tar.gz → 1.2.157a1.dev1__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.dev5 → upgini-1.2.157a1.dev1}/PKG-INFO +1 -1
  2. upgini-1.2.157a1.dev1/src/upgini/__about__.py +1 -0
  3. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/binary.py +16 -113
  4. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/date.py +63 -227
  5. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/unary.py +9 -6
  6. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/utils.py +1 -43
  7. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/search_task.py +37 -6
  8. upgini-1.2.156.dev5/src/upgini/__about__.py +0 -1
  9. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/.gitignore +0 -0
  10. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/LICENSE +0 -0
  11. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/README.md +0 -0
  12. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/pyproject.toml +0 -0
  13. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/__init__.py +0 -0
  14. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/ads.py +0 -0
  15. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/ads_management/__init__.py +0 -0
  16. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/ads_management/ads_manager.py +0 -0
  17. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/__init__.py +0 -0
  18. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/all_operators.py +0 -0
  19. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/feature.py +0 -0
  20. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/groupby.py +0 -0
  21. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/operator.py +0 -0
  22. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/__init__.py +0 -0
  23. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/base.py +0 -0
  24. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/cross.py +0 -0
  25. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/delta.py +0 -0
  26. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/lag.py +0 -0
  27. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/roll.py +0 -0
  28. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/trend.py +0 -0
  29. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/volatility.py +0 -0
  30. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/vector.py +0 -0
  31. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/data_source/__init__.py +0 -0
  32. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/data_source/data_source_publisher.py +0 -0
  33. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/dataset.py +0 -0
  34. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/errors.py +0 -0
  35. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/features_enricher.py +0 -0
  36. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/http.py +0 -0
  37. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/mdc/__init__.py +0 -0
  38. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/mdc/context.py +0 -0
  39. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/metadata.py +0 -0
  40. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/metrics.py +0 -0
  41. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/normalizer/__init__.py +0 -0
  42. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/normalizer/normalize_utils.py +0 -0
  43. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/__init__.py +0 -0
  44. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/exceptions.py +0 -0
  45. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/strings.properties +0 -0
  46. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  47. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/sampler/__init__.py +0 -0
  48. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/sampler/base.py +0 -0
  49. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/sampler/random_under_sampler.py +0 -0
  50. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/sampler/utils.py +0 -0
  51. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/spinner.py +0 -0
  52. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/Roboto-Regular.ttf +0 -0
  53. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/__init__.py +0 -0
  54. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/base_search_key_detector.py +0 -0
  55. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/blocked_time_series.py +0 -0
  56. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/config.py +0 -0
  57. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/country_utils.py +0 -0
  58. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/custom_loss_utils.py +0 -0
  59. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/cv_utils.py +0 -0
  60. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/datetime_utils.py +0 -0
  61. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/deduplicate_utils.py +0 -0
  62. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/display_utils.py +0 -0
  63. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/email_utils.py +0 -0
  64. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/fallback_progress_bar.py +0 -0
  65. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/feature_info.py +0 -0
  66. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/features_validator.py +0 -0
  67. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/format.py +0 -0
  68. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/hash_utils.py +0 -0
  69. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/ip_utils.py +0 -0
  70. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/mstats.py +0 -0
  71. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/one_hot_encoder.py +0 -0
  72. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/phone_utils.py +0 -0
  73. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/postal_code_utils.py +0 -0
  74. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/progress_bar.py +0 -0
  75. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/psi.py +0 -0
  76. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/sample_utils.py +0 -0
  77. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/sklearn_ext.py +0 -0
  78. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/sort.py +0 -0
  79. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/target_utils.py +0 -0
  80. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/track_info.py +0 -0
  81. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/ts_utils.py +0 -0
  82. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/warning_counter.py +0 -0
  83. {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/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.dev5
3
+ Version: 1.2.157a1.dev1
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.157.a1.dev1"
@@ -140,120 +140,23 @@ 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
-
243
143
  def calculate_binary(self, left: pd.Series, right: pd.Series) -> pd.Series:
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)
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))
257
160
 
258
161
 
259
162
  # Left for backward compatibility
@@ -8,7 +8,7 @@ from pandas.core.arrays.timedeltas import TimedeltaArray
8
8
  from pydantic import BaseModel, __version__ as pydantic_version
9
9
 
10
10
  from upgini.autofe.operator import PandasOperator, ParametrizedOperator
11
- from upgini.autofe.utils import bin_index, bin_index_many, bin_index_vectorized, pydantic_validator
11
+ from upgini.autofe.utils import pydantic_validator
12
12
 
13
13
 
14
14
  def get_pydantic_version():
@@ -113,172 +113,6 @@ _ext_aggregations = {"nunique": (lambda x: len(np.unique(x)), 0), "count": (len,
113
113
  _count_aggregations = ["nunique", "count"]
114
114
 
115
115
 
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"
130
- is_binary: bool = True
131
- has_symmetry_importance: bool = True
132
- output_type: Optional[str] = "vector"
133
-
134
- replace_negative: bool = False
135
-
136
- def get_params(self) -> Dict[str, Optional[str]]:
137
- res = super().get_params()
138
- res.update(
139
- {
140
- "diff_unit": self.diff_unit,
141
- "left_unit": self.left_unit,
142
- "right_unit": self.right_unit,
143
- "replace_negative": self.replace_negative,
144
- }
145
- )
146
- return res
147
-
148
- def to_formula(self) -> str:
149
- return f"date_diff_lists_{self.diff_unit}"
150
-
151
- @classmethod
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
160
-
161
- @staticmethod
162
- def _non_empty_list_mask(right: pd.Series) -> pd.Series:
163
- values = right.to_numpy()
164
- mask = np.empty(len(values), dtype=bool)
165
- for i, value in enumerate(values):
166
- if value is None or (isinstance(value, float) and np.isnan(value)):
167
- mask[i] = False
168
- elif isinstance(value, (list, tuple, np.ndarray)):
169
- mask[i] = len(value) > 0
170
- else:
171
- mask[i] = False
172
- return pd.Series(mask, index=right.index)
173
-
174
- def _convert_date_lists(self, lists: pd.Series) -> pd.Series:
175
- exploded = lists.explode()
176
- converted = pd.to_datetime(exploded, unit=self.right_unit, errors="coerce")
177
- return pd.Series(
178
- {
179
- idx: pd.arrays.DatetimeArray(values.to_numpy())
180
- for idx, values in converted.groupby(converted.index, sort=False)
181
- }
182
- )
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
-
190
- def calculate_binary(self, left: pd.Series, right: pd.Series) -> pd.Series:
191
- if left.isna().all() or right.isna().all():
192
- return pd.Series([None] * len(left), index=left.index, dtype=object)
193
-
194
- left = self._convert_to_date(left, self.left_unit)
195
- right_mask = self._non_empty_list_mask(right)
196
- mask = left.notna() & right.notna() & right_mask
197
-
198
- results = pd.Series([None] * len(left), index=left.index, dtype=object)
199
- if not mask.any():
200
- return results
201
-
202
- masked_left = left[mask]
203
- converted_lists = self._convert_date_lists(right[mask])
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)
276
- results[:] = np.nan
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)
280
-
281
-
282
116
  class DateListDiff(PandasOperator, DateDiffMixin, ParametrizedOperator):
283
117
  is_binary: bool = True
284
118
  has_symmetry_importance: bool = True
@@ -311,35 +145,43 @@ class DateListDiff(PandasOperator, DateDiffMixin, ParametrizedOperator):
311
145
  return None
312
146
  return cls(aggregation=aggregation)
313
147
 
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:
148
+ def calculate_binary(self, left: pd.Series, right: pd.Series) -> pd.Series:
331
149
  if left.isna().all() or right.isna().all():
332
150
  return pd.Series([None] * len(left), index=left.index, dtype=np.float64)
333
151
 
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)
152
+ left = self._convert_to_date(left, self.left_unit)
153
+ right_mask = right.apply(lambda x: len(x) > 0)
154
+ mask = left.notna() & right.notna() & right_mask
155
+ right_masked = right[mask].apply(lambda x: pd.arrays.DatetimeArray(self._convert_to_date(x, self.right_unit)))
156
+
157
+ if len(right_masked) == 0:
158
+ diff = []
159
+ elif len(right_masked) < 2:
160
+ diff = [left[mask].iloc[0] - right_masked.iloc[0]]
161
+ else:
162
+ diff = left[mask] - right_masked.values
163
+
164
+ res_masked = pd.Series(diff, index=left[mask].index).apply(lambda x: self._agg(self._diff(x)))
165
+ res = res_masked.reindex(left.index.union(right.index))
337
166
  if self.aggregation in _count_aggregations:
338
- result[~right_mask] = 0.0
339
- return result.astype(np.float64)
167
+ res[~right_mask] = 0.0
168
+ res = res.astype(np.float64)
340
169
 
341
- def calculate_binary(self, left: pd.Series, right: pd.Series) -> pd.Series:
342
- return self._compose_list_diff(left, right)
170
+ return res
171
+
172
+ def _diff(self, x: TimedeltaArray):
173
+ x = self._convert_diff_to_unit(x)
174
+ return x[x > 0] if self.replace_negative else x
175
+
176
+ def _agg(self, x):
177
+ method = getattr(np, self.aggregation, None)
178
+ default = np.nan
179
+ if method is None and self.aggregation in _ext_aggregations:
180
+ method, default = _ext_aggregations[self.aggregation]
181
+ elif not callable(method):
182
+ raise ValueError(f"Unsupported aggregation: {self.aggregation}")
183
+
184
+ return method(x) if len(x) > 0 else default
343
185
 
344
186
 
345
187
  class DateListDiffBounded(DateListDiff, ParametrizedOperator):
@@ -388,13 +230,16 @@ class DateListDiffBounded(DateListDiff, ParametrizedOperator):
388
230
  res["normalize"] = str(self.normalize)
389
231
  return res
390
232
 
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
- )
233
+ def _agg(self, x):
234
+ orig_len = len(x)
235
+ x = x[
236
+ (x >= (self.lower_bound if self.lower_bound is not None else -np.inf))
237
+ & (x < (self.upper_bound if self.upper_bound is not None else np.inf))
238
+ ]
239
+ agg_res = super()._agg(x)
240
+ if self.normalize and orig_len > 0:
241
+ return agg_res / orig_len
242
+ return agg_res
398
243
 
399
244
 
400
245
  class DatePercentileBase(PandasOperator, abc.ABC):
@@ -409,33 +254,26 @@ class DatePercentileBase(PandasOperator, abc.ABC):
409
254
  left = pd.to_datetime(left, unit=self.date_unit)
410
255
 
411
256
  bounds = self._get_bounds(left)
412
- values = pd.to_numeric(right, errors="coerce").to_numpy(dtype=np.float64, copy=False)
413
- bounds_list = bounds.tolist()
414
- result = np.full(len(values), np.nan)
415
-
416
- if not bounds_list:
417
- return pd.Series(result, index=right.index).astype(pd.Int64Dtype()).astype("category")
418
-
419
- bounds_lengths = {len(b) for b in bounds_list if isinstance(b, (list, np.ndarray))}
420
- if len(bounds_lengths) == 1 and all(isinstance(b, (list, np.ndarray)) for b in bounds_list):
421
- bounds_2d = np.asarray(bounds_list, dtype=np.float64)
422
- if bounds_2d.ndim == 1:
423
- result = bin_index_vectorized(values, bounds_2d)
424
- else:
425
- result = bin_index_many(values, bounds_2d)
426
- else:
427
- for i, row_bounds in enumerate(bounds_list):
428
- if isinstance(row_bounds, (list, np.ndarray)) and len(row_bounds) > 0:
429
- result[i] = bin_index(values[i], row_bounds)
430
257
 
431
- return pd.Series(result, index=right.index).astype(pd.Int64Dtype()).astype("category")
258
+ return (
259
+ right.index.to_series()
260
+ .apply(lambda i: self._perc(right[i], bounds[i]))
261
+ .astype(pd.Int64Dtype())
262
+ .astype("category")
263
+ )
432
264
 
433
265
  @abc.abstractmethod
434
266
  def _get_bounds(self, date_col: pd.Series) -> pd.Series:
435
267
  pass
436
268
 
437
269
  def _perc(self, f, bounds):
438
- return bin_index(f, bounds)
270
+ if f is None or np.isnan(f):
271
+ return np.nan
272
+ hit = np.where(f >= np.array(bounds))[0]
273
+ if hit.size > 0:
274
+ return np.max(hit) + 1
275
+ else:
276
+ return np.nan
439
277
 
440
278
  def get_params(self) -> Dict[str, Optional[str]]:
441
279
  res = super().get_params()
@@ -475,15 +313,13 @@ class DatePercentile(DatePercentileBase):
475
313
  return value
476
314
 
477
315
  def _get_bounds(self, date_col: pd.Series) -> pd.Series:
478
- zero_bounds = self.zero_bounds if self.zero_bounds is not None else []
479
- if not zero_bounds:
480
- return pd.Series([[] for _ in range(len(date_col))], index=date_col.index)
481
-
482
- month_diffs = (
483
- 12 * (date_col.dt.year - (self.zero_year or 0)) + (date_col.dt.month - (self.zero_month or 0))
484
- ).to_numpy()
485
- bounds_2d = np.asarray(zero_bounds, dtype=np.float64) + month_diffs[:, None] * self.step
486
- return pd.Series(list(bounds_2d), index=date_col.index)
316
+ months = date_col.dt.month
317
+ years = date_col.dt.year
318
+
319
+ month_diffs = 12 * (years - (self.zero_year or 0)) + (months - (self.zero_month or 0))
320
+ return month_diffs.apply(
321
+ lambda d: np.array(self.zero_bounds if self.zero_bounds is not None else []) + d * self.step
322
+ )
487
323
 
488
324
 
489
325
  class DatePercentileMethod2(DatePercentileBase):
@@ -4,7 +4,7 @@ import numpy as np
4
4
  import pandas as pd
5
5
 
6
6
  from upgini.autofe.operator import PandasOperator, ParametrizedOperator, VectorizableMixin
7
- from upgini.autofe.utils import bin_index, bin_index_vectorized, pydantic_validator
7
+ from upgini.autofe.utils import pydantic_validator
8
8
 
9
9
 
10
10
  class Abs(PandasOperator, VectorizableMixin):
@@ -163,13 +163,16 @@ class Bin(PandasOperator):
163
163
  is_categorical: bool = True
164
164
 
165
165
  def calculate_unary(self, data: pd.Series) -> pd.Series:
166
- bounds_arr = np.asarray(self.bin_bounds, dtype=np.float64)
167
- values = pd.to_numeric(data, errors="coerce").to_numpy(dtype=np.float64, copy=False)
168
- result = bin_index_vectorized(values, bounds_arr)
169
- return pd.Series(result, index=data.index).fillna(-1).astype(int).astype("category")
166
+ return data.apply(self._bin, bounds=self.bin_bounds).fillna(-1).astype(int).astype("category")
170
167
 
171
168
  def _bin(self, f, bounds):
172
- return bin_index(f, bounds)
169
+ if f is None or np.isnan(f):
170
+ return np.nan
171
+ hit = np.where(f >= np.array(bounds))[0]
172
+ if hit.size > 0:
173
+ return np.max(hit) + 1
174
+ else:
175
+ return np.nan
173
176
 
174
177
  def get_params(self) -> Dict[str, Optional[str]]:
175
178
  res = super().get_params()
@@ -3,9 +3,8 @@ Utility functions for autofe module.
3
3
  """
4
4
 
5
5
  import functools
6
- from typing import Callable, Union
6
+ from typing import Callable
7
7
 
8
- import numpy as np
9
8
  from pydantic import BaseModel
10
9
 
11
10
 
@@ -112,44 +111,3 @@ def pydantic_copy_method(obj):
112
111
  return obj.model_copy
113
112
  else:
114
113
  return obj.copy
115
-
116
-
117
- def bin_index(value: Union[float, int, None], bounds) -> float:
118
- if value is None or (isinstance(value, float) and np.isnan(value)):
119
- return np.nan
120
- bounds_arr = np.asarray(bounds, dtype=np.float64)
121
- if bounds_arr.size == 0 or value < bounds_arr[0]:
122
- return np.nan
123
- return np.searchsorted(bounds_arr, value, side="right")
124
-
125
-
126
- def bin_index_vectorized(values: np.ndarray, bounds: np.ndarray) -> np.ndarray:
127
- n = len(values)
128
- result = np.full(n, np.nan)
129
- bounds_arr = np.asarray(bounds, dtype=np.float64)
130
- if bounds_arr.size == 0:
131
- return result
132
- valid = ~np.isnan(values)
133
- if not valid.any():
134
- return result
135
- valid_values = values[valid]
136
- idx = np.searchsorted(bounds_arr, valid_values, side="right").astype(np.float64)
137
- below = valid_values < bounds_arr[0]
138
- if below.any():
139
- idx[below] = np.nan
140
- result[valid] = idx
141
- return result
142
-
143
-
144
- def bin_index_many(values: np.ndarray, bounds_2d: np.ndarray) -> np.ndarray:
145
- n = len(values)
146
- result = np.full(n, np.nan)
147
- for i in range(n):
148
- v = values[i]
149
- if np.isnan(v):
150
- continue
151
- bounds_row = bounds_2d[i]
152
- if bounds_row.size == 0 or v < bounds_row[0]:
153
- continue
154
- result[i] = np.searchsorted(bounds_row, v, side="right")
155
- return result
@@ -1,3 +1,4 @@
1
+ import io
1
2
  import logging
2
3
  import tempfile
3
4
  import time
@@ -438,10 +439,40 @@ def _download_features_file(
438
439
  return _read_parquet(file_content)
439
440
 
440
441
 
442
+ def _is_pyarrow_binary_incompatibility(exc: BaseException) -> bool:
443
+ message = str(exc)
444
+ return "binary incompatibility" in message or "IpcReadOptions size changed" in message
445
+
446
+
447
+ def _read_parquet_with_pyarrow(file_content: bytes) -> pd.DataFrame:
448
+ import pyarrow as pa
449
+ import pyarrow.parquet as pq
450
+
451
+ # pd.read_parquet(engine="pyarrow") routes through pq.read_table -> ParquetDataset,
452
+ # which imports pyarrow.dataset and can fail in Colab with ABI mismatch errors.
453
+ return pq.ParquetFile(pa.BufferReader(file_content)).read().to_pandas()
454
+
455
+
456
+ def _read_parquet_with_fastparquet(file_content: bytes) -> pd.DataFrame:
457
+ import fastparquet
458
+
459
+ return fastparquet.ParquetFile(io.BytesIO(file_content)).to_pandas()
460
+
461
+
441
462
  def _read_parquet(file_content: bytes, file_name: str = "features.parquet"):
442
- with tempfile.TemporaryDirectory() as tmp_dir:
443
- tmp_file_name = f"{tmp_dir}/{file_name}"
444
- with open(tmp_file_name, "wb") as gzip_file:
445
- gzip_file.write(file_content)
446
- # Note: MLB writes files using pyarrow, so reading with fastparquet may cause errors.
447
- return pd.read_parquet(tmp_file_name, engine="pyarrow")
463
+ try:
464
+ return _read_parquet_with_pyarrow(file_content)
465
+ except Exception as pyarrow_error:
466
+ if not _is_pyarrow_binary_incompatibility(pyarrow_error):
467
+ raise
468
+
469
+ try:
470
+ # MLB writes files using pyarrow; fastparquet is a fallback for broken pyarrow installs.
471
+ return _read_parquet_with_fastparquet(file_content)
472
+ except Exception as fastparquet_error:
473
+ raise RuntimeError(
474
+ "Failed to read parquet features file due to PyArrow binary incompatibility. "
475
+ "If you are using Google Colab, reinstall PyArrow and restart the runtime:\n"
476
+ " %pip install --force-reinstall 'pyarrow==23.0.1'\n"
477
+ "Then use Runtime → Restart session before calling enricher.fit()."
478
+ ) from fastparquet_error
@@ -1 +0,0 @@
1
- __version__ = "1.2.156.dev5"
File without changes
File without changes