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.
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/PKG-INFO +1 -1
- upgini-1.2.157a1.dev1/src/upgini/__about__.py +1 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/binary.py +16 -113
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/date.py +63 -227
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/unary.py +9 -6
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/utils.py +1 -43
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/search_task.py +37 -6
- upgini-1.2.156.dev5/src/upgini/__about__.py +0 -1
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/.gitignore +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/LICENSE +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/README.md +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/pyproject.toml +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/ads.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/ads_management/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/ads_management/ads_manager.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/all_operators.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/feature.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/groupby.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/operator.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/base.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/cross.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/delta.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/lag.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/roll.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/trend.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/timeseries/volatility.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/autofe/vector.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/data_source/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/data_source/data_source_publisher.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/dataset.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/errors.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/features_enricher.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/http.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/mdc/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/mdc/context.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/metadata.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/metrics.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/normalizer/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/normalizer/normalize_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/exceptions.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/strings.properties +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/strings_widget.properties +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/sampler/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/sampler/base.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/sampler/random_under_sampler.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/sampler/utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/spinner.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/Roboto-Regular.ttf +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/__init__.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/base_search_key_detector.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/blocked_time_series.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/config.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/country_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/custom_loss_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/cv_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/datetime_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/deduplicate_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/display_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/email_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/fallback_progress_bar.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/feature_info.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/features_validator.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/format.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/hash_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/ip_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/mstats.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/one_hot_encoder.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/phone_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/postal_code_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/progress_bar.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/psi.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/sample_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/sklearn_ext.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/sort.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/target_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/track_info.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/ts_utils.py +0 -0
- {upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/utils/warning_counter.py +0 -0
- {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.
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
339
|
-
|
|
167
|
+
res[~right_mask] = 0.0
|
|
168
|
+
res = res.astype(np.float64)
|
|
340
169
|
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
month_diffs
|
|
483
|
-
|
|
484
|
-
)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
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.156.dev5 → upgini-1.2.157a1.dev1}/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
|
|
File without changes
|
{upgini-1.2.156.dev5 → upgini-1.2.157a1.dev1}/src/upgini/resource_bundle/strings_widget.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
|
|
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
|