upgini 1.1.266a3254.post2__tar.gz → 1.1.267a3254.post3__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.

Potentially problematic release.


This version of upgini might be problematic. Click here for more details.

Files changed (84) hide show
  1. {upgini-1.1.266a3254.post2/src/upgini.egg-info → upgini-1.1.267a3254.post3}/PKG-INFO +1 -1
  2. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/setup.py +1 -1
  3. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/date.py +22 -14
  4. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/features_enricher.py +38 -6
  5. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/resource_bundle/strings.properties +4 -2
  6. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/target_utils.py +18 -0
  7. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3/src/upgini.egg-info}/PKG-INFO +1 -1
  8. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_autofe_operands.py +3 -2
  9. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_features_enricher.py +32 -7
  10. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_metrics.py +36 -36
  11. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_target_utils.py +61 -1
  12. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/LICENSE +0 -0
  13. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/README.md +0 -0
  14. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/pyproject.toml +0 -0
  15. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/setup.cfg +0 -0
  16. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/__init__.py +0 -0
  17. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/ads.py +0 -0
  18. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/ads_management/__init__.py +0 -0
  19. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/ads_management/ads_manager.py +0 -0
  20. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/__init__.py +0 -0
  21. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/all_operands.py +0 -0
  22. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/binary.py +0 -0
  23. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/feature.py +0 -0
  24. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/groupby.py +0 -0
  25. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/operand.py +0 -0
  26. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/unary.py +0 -0
  27. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/autofe/vector.py +0 -0
  28. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/data_source/__init__.py +0 -0
  29. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/data_source/data_source_publisher.py +0 -0
  30. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/dataset.py +0 -0
  31. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/errors.py +0 -0
  32. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/http.py +0 -0
  33. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/mdc/__init__.py +0 -0
  34. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/mdc/context.py +0 -0
  35. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/metadata.py +0 -0
  36. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/metrics.py +0 -0
  37. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/normalizer/__init__.py +0 -0
  38. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/normalizer/phone_normalizer.py +0 -0
  39. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/resource_bundle/__init__.py +0 -0
  40. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/resource_bundle/exceptions.py +0 -0
  41. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/resource_bundle/strings_widget.properties +0 -0
  42. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/sampler/__init__.py +0 -0
  43. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/sampler/base.py +0 -0
  44. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/sampler/random_under_sampler.py +0 -0
  45. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/sampler/utils.py +0 -0
  46. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/search_task.py +0 -0
  47. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/spinner.py +0 -0
  48. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/__init__.py +0 -0
  49. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/base_search_key_detector.py +0 -0
  50. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/blocked_time_series.py +0 -0
  51. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/country_utils.py +0 -0
  52. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/custom_loss_utils.py +0 -0
  53. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/cv_utils.py +0 -0
  54. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/datetime_utils.py +0 -0
  55. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/deduplicate_utils.py +0 -0
  56. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/display_utils.py +0 -0
  57. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/email_utils.py +0 -0
  58. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/fallback_progress_bar.py +0 -0
  59. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/features_validator.py +0 -0
  60. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/format.py +0 -0
  61. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/ip_utils.py +0 -0
  62. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/phone_utils.py +0 -0
  63. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/postal_code_utils.py +0 -0
  64. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/progress_bar.py +0 -0
  65. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/sklearn_ext.py +0 -0
  66. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/track_info.py +0 -0
  67. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/utils/warning_counter.py +0 -0
  68. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini/version_validator.py +0 -0
  69. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini.egg-info/SOURCES.txt +0 -0
  70. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini.egg-info/dependency_links.txt +0 -0
  71. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini.egg-info/requires.txt +0 -0
  72. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/src/upgini.egg-info/top_level.txt +0 -0
  73. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_binary_dataset.py +0 -0
  74. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_blocked_time_series.py +0 -0
  75. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_categorical_dataset.py +0 -0
  76. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_continuous_dataset.py +0 -0
  77. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_country_utils.py +0 -0
  78. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_custom_loss_utils.py +0 -0
  79. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_datetime_utils.py +0 -0
  80. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_email_utils.py +0 -0
  81. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_etalon_validation.py +0 -0
  82. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_phone_utils.py +0 -0
  83. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_postal_code_utils.py +0 -0
  84. {upgini-1.1.266a3254.post2 → upgini-1.1.267a3254.post3}/tests/test_widget.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: upgini
3
- Version: 1.1.266a3254.post2
3
+ Version: 1.1.267a3254.post3
4
4
  Summary: Intelligent data search & enrichment for Machine Learning
5
5
  Home-page: https://upgini.com/
6
6
  Author: Upgini Developers
@@ -40,7 +40,7 @@ def send_log(msg: str):
40
40
 
41
41
 
42
42
  here = Path(__file__).parent.resolve()
43
- version = "1.1.266a3254-2"
43
+ version = "1.1.267a3254-3"
44
44
  try:
45
45
  send_log(f"Start setup PyLib version {version}")
46
46
  setup(
@@ -54,6 +54,9 @@ class DateDiffType2(PandasOperand, DateDiffMixin):
54
54
  return diff
55
55
 
56
56
 
57
+ _ext_aggregations = {"nunique": (lambda x: len(np.unique(x)), 0), "count": (len, 0)}
58
+
59
+
57
60
  class DateListDiff(PandasOperand, DateDiffMixin):
58
61
  is_binary = True
59
62
  has_symmetry_importance = True
@@ -72,18 +75,31 @@ class DateListDiff(PandasOperand, DateDiffMixin):
72
75
 
73
76
  def calculate_binary(self, left: pd.Series, right: pd.Series) -> pd.Series:
74
77
  left = self._convert_to_date(left, self.left_unit)
78
+ right = right.apply(lambda x: pd.arrays.DatetimeArray(self._convert_to_date(x, self.right_unit)))
79
+
80
+ return pd.Series(left - right.values).apply(lambda x: self._agg(self._diff(x)))
75
81
 
76
- return pd.Series(left.index.map(lambda i: self.reduce(self.map_diff(left.loc[i], right.loc[i]))))
82
+ def _diff(self, x):
83
+ x = x / np.timedelta64(1, self.diff_unit)
84
+ return x[x > 0]
85
+
86
+ def _agg(self, x):
87
+ method = getattr(np, self.aggregation, None)
88
+ default = np.nan
89
+ if method is None and self.aggregation in _ext_aggregations:
90
+ method, default = _ext_aggregations[self.aggregation]
91
+ elif not callable(method):
92
+ raise ValueError(f"Unsupported aggregation: {self.aggregation}")
93
+
94
+ return method(x) if len(x) > 0 else default
77
95
 
78
96
 
79
97
  class DateListDiffBounded(DateListDiff):
80
98
  lower_bound: Optional[int]
81
99
  upper_bound: Optional[int]
82
- inclusive: Optional[str]
83
100
 
84
101
  def __init__(self, **data: Any) -> None:
85
102
  if "name" not in data:
86
- inclusive = data.get("inclusive")
87
103
  lower_bound = data.get("lower_bound")
88
104
  upper_bound = data.get("upper_bound")
89
105
  components = [
@@ -92,18 +108,10 @@ class DateListDiffBounded(DateListDiff):
92
108
  str(lower_bound if lower_bound is not None else "minusinf"),
93
109
  str(upper_bound if upper_bound is not None else "plusinf"),
94
110
  ]
95
- if inclusive:
96
- components.append(inclusive)
97
111
  components.append(data.get("aggregation"))
98
112
  data["name"] = "_".join(components)
99
113
  super().__init__(**data)
100
114
 
101
- def reduce(self, diff_list: pd.Series) -> float:
102
- return diff_list[
103
- (diff_list > 0)
104
- & (
105
- diff_list.between(
106
- self.lower_bound or -np.inf, self.upper_bound or np.inf, inclusive=self.inclusive or "left"
107
- )
108
- )
109
- ].aggregate(self.aggregation)
115
+ def _agg(self, x):
116
+ x = x[(x >= (self.lower_bound or -np.inf)) & (x < (self.upper_bound or np.inf))]
117
+ return super()._agg(x)
@@ -94,7 +94,7 @@ try:
94
94
  except Exception:
95
95
  from upgini.utils.fallback_progress_bar import CustomFallbackProgressBar as ProgressBar
96
96
 
97
- from upgini.utils.target_utils import define_task
97
+ from upgini.utils.target_utils import calculate_psi, define_task
98
98
  from upgini.utils.warning_counter import WarningCounter
99
99
  from upgini.version_validator import validate_version
100
100
 
@@ -2226,14 +2226,11 @@ class FeaturesEnricher(TransformerMixin):
2226
2226
  validated_X, self.fit_search_keys, self.logger, self.bundle, self.warning_counter
2227
2227
  )
2228
2228
 
2229
- has_date = self._get_date_column(self.fit_search_keys) is not None
2229
+ maybe_date_column = self._get_date_column(self.fit_search_keys)
2230
+ has_date = maybe_date_column is not None
2230
2231
  model_task_type = self.model_task_type or define_task(validated_y, has_date, self.logger)
2231
2232
  self._validate_binary_observations(validated_y, model_task_type)
2232
2233
 
2233
- df = self.__handle_index_search_keys(df, self.fit_search_keys)
2234
-
2235
- df = self.__correct_target(df)
2236
-
2237
2234
  self.runtime_parameters = get_runtime_params_custom_loss(
2238
2235
  self.loss, model_task_type, self.runtime_parameters, self.logger
2239
2236
  )
@@ -2245,6 +2242,13 @@ class FeaturesEnricher(TransformerMixin):
2245
2242
  eval_df[EVAL_SET_INDEX] = idx + 1
2246
2243
  df = pd.concat([df, eval_df])
2247
2244
 
2245
+ df = self.__correct_target(df)
2246
+
2247
+ df = self.__handle_index_search_keys(df, self.fit_search_keys)
2248
+
2249
+ if is_numeric_dtype(df[self.TARGET_NAME]) and has_date:
2250
+ self._validate_PSI(df.sort_values(by=maybe_date_column))
2251
+
2248
2252
  if DEFAULT_INDEX in df.columns:
2249
2253
  msg = self.bundle.get("unsupported_index_column")
2250
2254
  self.logger.info(msg)
@@ -3567,6 +3571,34 @@ class FeaturesEnricher(TransformerMixin):
3567
3571
  self.logger.warning(msg)
3568
3572
  print(msg)
3569
3573
 
3574
+ def _validate_PSI(self, df: pd.DataFrame):
3575
+ if EVAL_SET_INDEX in df.columns:
3576
+ train = df.query(f"{EVAL_SET_INDEX} == 0")
3577
+ eval1 = df.query(f"{EVAL_SET_INDEX} == 1")
3578
+ else:
3579
+ train = df
3580
+ eval1 = None
3581
+
3582
+ # 1. Check train PSI
3583
+ half_train = round(len(train) / 2)
3584
+ part1 = train[:half_train]
3585
+ part2 = train[half_train:]
3586
+ train_psi = calculate_psi(part1[self.TARGET_NAME], part2[self.TARGET_NAME])
3587
+ if train_psi > 0.2:
3588
+ self.warning_counter.increment()
3589
+ msg = self.bundle.get("train_unstable_target").format(train_psi)
3590
+ print(msg)
3591
+ self.logger.warning(msg)
3592
+
3593
+ # 2. Check train-test PSI
3594
+ if eval1 is not None:
3595
+ train_test_psi = calculate_psi(train[self.TARGET_NAME], eval1[self.TARGET_NAME])
3596
+ if train_test_psi > 0.2:
3597
+ self.warning_counter.increment()
3598
+ msg = self.bundle.get("eval_unstable_target").format(train_test_psi)
3599
+ print(msg)
3600
+ self.logger.warning(msg)
3601
+
3570
3602
  def _dump_python_libs(self):
3571
3603
  try:
3572
3604
  from pip._internal.operations.freeze import freeze
@@ -111,7 +111,9 @@ x_is_empty=X is empty
111
111
  y_is_empty=y is empty
112
112
  x_contains_reserved_column_name=Column name {} is reserved. Please rename column and try again
113
113
  missing_generate_feature=\nWARNING: Feature {} specified in `generate_features` is not present in input columns: {}
114
- x_unstable_by_date=\nWARNING: Your training sample is unstable in number of rows per date. It is recommended to redesign the training sample.
114
+ x_unstable_by_date=\nWARNING: Your training sample is unstable in number of rows per date. It is recommended to redesign the training sample
115
+ train_unstable_target=\nWARNING: Your training sample contains an unstable target event, PSI = {}. This will lead to unstable scoring on deferred samples. It is recommended to redesign the training sample
116
+ eval_unstable_target=\nWARNING: Your training and evaluation samples have a difference in target distribution. PSI = {}. The results will be unstable. It is recommended to redesign the training and evaluation samples
115
117
  # eval set validation
116
118
  unsupported_type_eval_set=Unsupported type of eval_set: {}. It should be list of tuples with two elements: X and y
117
119
  eval_set_invalid_tuple_size=eval_set contains a tuple of size {}. It should contain only pairs of X and y
@@ -198,7 +200,7 @@ email_detected=Emails detected in column `{}`. It will be used as a search key\n
198
200
  email_detected_not_registered=Emails detected in column `{}`. It can be used only with api_key from profile.upgini.com\nSee docs to turn off the automatic detection: https://github.com/upgini/upgini/blob/main/README.md#turn-off-autodetection-for-search-key-columns
199
201
  phone_detected=Phone numbers detected in column `{}`. It can be used only with api_key from profile.upgini.com\nSee docs to turn off the automatic detection: https://github.com/upgini/upgini/blob/main/README.md#turn-off-autodetection-for-search-key-columns
200
202
  phone_detected_not_registered=\nWARNING: Phone numbers detected in column `{}`. It can be used only with api_key from profile.upgini.com\nSee docs to turn off the automatic detection: https://github.com/upgini/upgini/blob/main/README.md#turn-off-autodetection-for-search-key-columns
201
- target_type_detected=Detected task type: {}\n
203
+ target_type_detected=\nDetected task type: {}\n
202
204
  # all_ok_community_invite=Chat with us in Slack community:
203
205
  all_ok_community_invite=❓ Support request
204
206
  too_small_for_metrics=Your train dataset contains less than 500 rows. For such dataset Upgini will not calculate accuracy metrics. Please increase the number of rows in the training dataset to calculate accuracy metrics
@@ -177,3 +177,21 @@ def balance_undersample(
177
177
 
178
178
  logger.info(f"Shape after rebalance resampling: {resampled_data}")
179
179
  return resampled_data
180
+
181
+
182
+ def calculate_psi(expected: pd.Series, actual: pd.Series) -> float:
183
+ df = pd.concat([expected, actual])
184
+
185
+ # Define the bins for the target variable
186
+ df_min = df.min()
187
+ df_max = df.max()
188
+ bins = [df_min, (df_min + df_max) / 2, df_max]
189
+
190
+ # Calculate the base distribution
191
+ train_distribution = expected.value_counts(bins=bins, normalize=True).sort_index().values
192
+
193
+ # Calculate the target distribution
194
+ test_distribution = actual.value_counts(bins=bins, normalize=True).sort_index().values
195
+
196
+ # Calculate the PSI
197
+ return np.sum((train_distribution - test_distribution) * np.log(train_distribution / test_distribution))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: upgini
3
- Version: 1.1.266a3254.post2
3
+ Version: 1.1.267a3254.post3
4
4
  Summary: Intelligent data search & enrichment for Machine Learning
5
5
  Home-page: https://upgini.com/
6
6
  Author: Upgini Developers
@@ -1,4 +1,5 @@
1
1
  import pandas as pd
2
+ from pydantic import NoneBytes
2
3
  from upgini.autofe.date import DateDiff, DateDiffType2, DateListDiff, DateListDiffBounded
3
4
 
4
5
  from datetime import datetime
@@ -47,7 +48,7 @@ def test_date_diff_list():
47
48
  def check(aggregation, expected_name, expected_values):
48
49
  operand = DateListDiff(aggregation=aggregation)
49
50
  assert operand.name == expected_name
50
- assert_series_equal(operand.calculate_binary(df.date1, df.date2), expected_values)
51
+ assert_series_equal(operand.calculate_binary(df.date1, df.date2).rename(None), expected_values)
51
52
 
52
53
  check(aggregation="min", expected_name="date_diff_min", expected_values=pd.Series([10530, 10531, None, None]))
53
54
  check(aggregation="max", expected_name="date_diff_max", expected_values=pd.Series([10531, 10531, None, None]))
@@ -83,7 +84,7 @@ def test_date_diff_list_bounded():
83
84
  diff_unit="Y", aggregation="count", lower_bound=lower_bound, upper_bound=upper_bound
84
85
  )
85
86
  assert operand.name == expected_name
86
- assert_series_equal(operand.calculate_binary(df.date1, df.date2), expected_values)
87
+ assert_series_equal(operand.calculate_binary(df.date1, df.date2).rename(None), expected_values)
87
88
 
88
89
  check_num_by_years(0, 18, "date_diff_Y_0_18_count", pd.Series([2, 1, 0, 0, 0]))
89
90
  check_num_by_years(18, 23, "date_diff_Y_18_23_count", pd.Series([1, 2, 2, 0, 0]))
@@ -428,13 +428,13 @@ def test_saved_features_enricher(requests_mock: Mocker):
428
428
  df.drop(columns="SystemRecordId_473310000", inplace=True)
429
429
  train_df = df.head(10000)
430
430
  train_features = train_df.drop(columns="target")
431
- train_target = train_df["target"]
431
+ train_target = train_df["target"].copy()
432
432
  eval1_df = df[10000:11000].reset_index(drop=True)
433
433
  eval1_features = eval1_df.drop(columns="target")
434
- eval1_target = eval1_df["target"].reset_index(drop=True)
434
+ eval1_target = eval1_df["target"].reset_index(drop=True).copy()
435
435
  eval2_df = df[11000:12000]
436
436
  eval2_features = eval2_df.drop(columns="target")
437
- eval2_target = eval2_df["target"]
437
+ eval2_target = eval2_df["target"].copy()
438
438
 
439
439
  enricher = FeaturesEnricher(
440
440
  search_keys={"phone_num": SearchKey.PHONE, "rep_date": SearchKey.DATE},
@@ -482,6 +482,31 @@ def test_saved_features_enricher(requests_mock: Mocker):
482
482
  assert first_feature_info[feature_name_header] == "feature"
483
483
  assert first_feature_info[shap_value_header] == 10.1
484
484
 
485
+ # Check imbalanced target metrics
486
+ random = np.random.RandomState(42)
487
+ train_random_indices = random.choice(train_target.index, size=9000, replace=False)
488
+ train_target.loc[train_random_indices] = 0
489
+
490
+ metrics = enricher.calculate_metrics(
491
+ train_features,
492
+ train_target
493
+ )
494
+ expected_metrics = pd.DataFrame(
495
+ {
496
+ segment_header: [train_segment],
497
+ rows_header: [10000],
498
+ target_mean_header: [0.049],
499
+ enriched_gini: [0.000985],
500
+ }
501
+ )
502
+ print("Expected metrics: ")
503
+ print(expected_metrics)
504
+ print("Actual metrics: ")
505
+ print(metrics)
506
+
507
+ assert metrics is not None
508
+ assert_frame_equal(expected_metrics, metrics, atol=1e-6)
509
+
485
510
 
486
511
  def test_features_enricher_with_demo_key(requests_mock: Mocker):
487
512
  url = "http://fake_url2"
@@ -2498,11 +2523,11 @@ def test_diff_target_dups(requests_mock: Mocker):
2498
2523
  assert len(self.data) == 2
2499
2524
  print(self.data)
2500
2525
  assert self.data.loc[0, "date_0e8763"] == 1672531200000
2501
- assert self.data.loc[0, "feature_2ad562"] == 12
2502
- assert self.data.loc[0, "target"] == 0
2526
+ assert self.data.loc[0, "feature_2ad562"] == 13
2527
+ assert self.data.loc[0, "target"] == 1
2503
2528
  assert self.data.loc[1, "date_0e8763"] == 1672531200000
2504
- assert self.data.loc[1, "feature_2ad562"] == 13
2505
- assert self.data.loc[1, "target"] == 1
2529
+ assert self.data.loc[1, "feature_2ad562"] == 12
2530
+ assert self.data.loc[1, "target"] == 0
2506
2531
  return SearchTask("123", self, rest_client=enricher.rest_client)
2507
2532
 
2508
2533
  Dataset.search = mock_search
@@ -857,23 +857,23 @@ def test_catboost_metric_binary(requests_mock: Mocker):
857
857
  assert metrics_df.loc[0, segment_header] == train_segment
858
858
  assert metrics_df.loc[0, rows_header] == 500
859
859
  assert metrics_df.loc[0, target_mean_header] == 0.51
860
- assert metrics_df.loc[0, baseline_gini] == approx(0.023101)
861
- assert metrics_df.loc[0, enriched_gini] == approx(0.090344)
862
- assert metrics_df.loc[0, uplift] == approx(0.067243)
860
+ assert metrics_df.loc[0, baseline_gini] == approx(0.061408)
861
+ assert metrics_df.loc[0, enriched_gini] == approx(0.071498)
862
+ assert metrics_df.loc[0, uplift] == approx(0.010090)
863
863
 
864
864
  assert metrics_df.loc[1, segment_header] == eval_1_segment
865
865
  assert metrics_df.loc[1, rows_header] == 250
866
866
  assert metrics_df.loc[1, target_mean_header] == 0.452
867
- assert metrics_df.loc[1, baseline_gini] == approx(-0.016188)
868
- assert metrics_df.loc[1, enriched_gini] == approx(0.014947)
869
- assert metrics_df.loc[1, uplift] == approx(0.031135)
867
+ assert metrics_df.loc[1, baseline_gini] == approx(-0.051702)
868
+ assert metrics_df.loc[1, enriched_gini] == approx(0.023668)
869
+ assert metrics_df.loc[1, uplift] == approx(0.075370)
870
870
 
871
871
  assert metrics_df.loc[2, segment_header] == eval_2_segment
872
872
  assert metrics_df.loc[2, rows_header] == 250
873
873
  assert metrics_df.loc[2, target_mean_header] == 0.536
874
- assert metrics_df.loc[2, baseline_gini] == approx(-0.017138)
875
- assert metrics_df.loc[2, enriched_gini] == approx(0.035666)
876
- assert metrics_df.loc[2, uplift] == approx(0.052805)
874
+ assert metrics_df.loc[2, baseline_gini] == approx(0.012674)
875
+ assert metrics_df.loc[2, enriched_gini] == approx(0.022980)
876
+ assert metrics_df.loc[2, uplift] == approx(0.010306)
877
877
 
878
878
 
879
879
  def test_catboost_metric_binary_with_cat_features(requests_mock: Mocker):
@@ -984,23 +984,23 @@ def test_catboost_metric_binary_with_cat_features(requests_mock: Mocker):
984
984
  assert metrics_df.loc[0, segment_header] == train_segment
985
985
  assert metrics_df.loc[0, rows_header] == 500
986
986
  assert metrics_df.loc[0, target_mean_header] == 0.51
987
- assert metrics_df.loc[0, baseline_gini] == approx(0.102928)
988
- assert metrics_df.loc[0, enriched_gini] == approx(0.139437)
989
- assert metrics_df.loc[0, uplift] == approx(0.036508)
987
+ assert metrics_df.loc[0, baseline_gini] == approx(0.027066)
988
+ assert metrics_df.loc[0, enriched_gini] == approx(0.101601)
989
+ assert metrics_df.loc[0, uplift] == approx(0.074535)
990
990
 
991
991
  assert metrics_df.loc[1, segment_header] == eval_1_segment
992
992
  assert metrics_df.loc[1, rows_header] == 250
993
993
  assert metrics_df.loc[1, target_mean_header] == 0.452
994
- assert metrics_df.loc[1, baseline_gini] == approx(-0.074491)
995
- assert metrics_df.loc[1, enriched_gini] == approx(-0.052619)
996
- assert metrics_df.loc[1, uplift] == approx(0.021872)
994
+ assert metrics_df.loc[1, baseline_gini] == approx(-0.078548)
995
+ assert metrics_df.loc[1, enriched_gini] == approx(-0.019663)
996
+ assert metrics_df.loc[1, uplift] == approx(0.058885)
997
997
 
998
998
  assert metrics_df.loc[2, segment_header] == eval_2_segment
999
999
  assert metrics_df.loc[2, rows_header] == 250
1000
1000
  assert metrics_df.loc[2, target_mean_header] == 0.536
1001
- assert metrics_df.loc[2, baseline_gini] == approx(0.022002)
1002
- assert metrics_df.loc[2, enriched_gini] == approx(-0.010950)
1003
- assert metrics_df.loc[2, uplift] == approx(-0.032952)
1001
+ assert metrics_df.loc[2, baseline_gini] == approx(-0.066572)
1002
+ assert metrics_df.loc[2, enriched_gini] == approx(-0.116598)
1003
+ assert metrics_df.loc[2, uplift] == approx(-0.050026)
1004
1004
 
1005
1005
 
1006
1006
  @pytest.mark.skip()
@@ -1225,23 +1225,23 @@ def test_rf_metric_rmse(requests_mock: Mocker):
1225
1225
  assert metrics_df.loc[0, segment_header] == train_segment
1226
1226
  assert metrics_df.loc[0, rows_header] == 500
1227
1227
  assert metrics_df.loc[0, target_mean_header] == 0.51
1228
- assert metrics_df.loc[0, baseline_rmse] == approx(0.737054)
1229
- assert metrics_df.loc[0, enriched_rmse] == approx(0.720624)
1230
- assert metrics_df.loc[0, uplift] == approx(0.016430)
1228
+ assert metrics_df.loc[0, baseline_rmse] == approx(0.695490)
1229
+ assert metrics_df.loc[0, enriched_rmse] == approx(0.656957)
1230
+ assert metrics_df.loc[0, uplift] == approx(0.038533)
1231
1231
 
1232
1232
  assert metrics_df.loc[1, segment_header] == eval_1_segment
1233
1233
  assert metrics_df.loc[1, rows_header] == 250
1234
1234
  assert metrics_df.loc[1, target_mean_header] == 0.452
1235
- assert metrics_df.loc[1, baseline_rmse] == approx(0.704719)
1236
- assert metrics_df.loc[1, enriched_rmse] == approx(0.721444)
1237
- assert metrics_df.loc[1, uplift] == approx(-0.016725)
1235
+ assert metrics_df.loc[1, baseline_rmse] == approx(0.717178)
1236
+ assert metrics_df.loc[1, enriched_rmse] == approx(0.685107)
1237
+ assert metrics_df.loc[1, uplift] == approx(0.032071)
1238
1238
 
1239
1239
  assert metrics_df.loc[2, segment_header] == eval_2_segment
1240
1240
  assert metrics_df.loc[2, rows_header] == 250
1241
1241
  assert metrics_df.loc[2, target_mean_header] == 0.536
1242
- assert metrics_df.loc[2, baseline_rmse] == approx(0.690261)
1243
- assert metrics_df.loc[2, enriched_rmse] == approx(0.694711)
1244
- assert metrics_df.loc[2, uplift] == approx(-0.004450)
1242
+ assert metrics_df.loc[2, baseline_rmse] == approx(0.678079)
1243
+ assert metrics_df.loc[2, enriched_rmse] == approx(0.718205)
1244
+ assert metrics_df.loc[2, uplift] == approx(-0.040126)
1245
1245
 
1246
1246
 
1247
1247
  def test_default_metric_binary_with_string_feature(requests_mock: Mocker):
@@ -1341,23 +1341,23 @@ def test_default_metric_binary_with_string_feature(requests_mock: Mocker):
1341
1341
  assert metrics_df.loc[0, segment_header] == train_segment
1342
1342
  assert metrics_df.loc[0, rows_header] == 500
1343
1343
  assert metrics_df.loc[0, target_mean_header] == 0.51
1344
- assert metrics_df.loc[0, baseline_gini] == approx(0.116841)
1345
- assert metrics_df.loc[0, enriched_gini] == approx(0.076030)
1346
- assert metrics_df.loc[0, uplift] == approx(-0.040811)
1344
+ assert metrics_df.loc[0, baseline_gini] == approx(-0.034968)
1345
+ assert metrics_df.loc[0, enriched_gini] == approx(-0.090683)
1346
+ assert metrics_df.loc[0, uplift] == approx(-0.055715)
1347
1347
 
1348
1348
  assert metrics_df.loc[1, segment_header] == eval_1_segment
1349
1349
  assert metrics_df.loc[1, rows_header] == 250
1350
1350
  assert metrics_df.loc[1, target_mean_header] == 0.452
1351
- assert metrics_df.loc[1, baseline_gini] == approx(-0.078160)
1352
- assert metrics_df.loc[1, enriched_gini] == approx(-0.029288)
1353
- assert metrics_df.loc[1, uplift] == approx(0.048873)
1351
+ assert metrics_df.loc[1, baseline_gini] == approx(-0.081674)
1352
+ assert metrics_df.loc[1, enriched_gini] == approx(-0.006627)
1353
+ assert metrics_df.loc[1, uplift] == approx(0.075047)
1354
1354
 
1355
1355
  assert metrics_df.loc[2, segment_header] == eval_2_segment
1356
1356
  assert metrics_df.loc[2, rows_header] == 250
1357
1357
  assert metrics_df.loc[2, target_mean_header] == 0.536
1358
- assert metrics_df.loc[2, baseline_gini] == approx(-0.013484)
1359
- assert metrics_df.loc[2, enriched_gini] == approx(-0.017486)
1360
- assert metrics_df.loc[2, uplift] == approx(-0.004002)
1358
+ assert metrics_df.loc[2, baseline_gini] == approx(-0.039166)
1359
+ assert metrics_df.loc[2, enriched_gini] == approx(-0.016457)
1360
+ assert metrics_df.loc[2, uplift] == approx(0.022710)
1361
1361
 
1362
1362
 
1363
1363
  def approx(value: float):
@@ -4,7 +4,8 @@ import pytest
4
4
  from pandas.testing import assert_frame_equal
5
5
 
6
6
  from upgini.errors import ValidationError
7
- from upgini.metadata import SYSTEM_RECORD_ID, TARGET, ModelTaskType
7
+ from upgini.features_enricher import FeaturesEnricher
8
+ from upgini.metadata import SYSTEM_RECORD_ID, TARGET, ModelTaskType, SearchKey
8
9
  from upgini.resource_bundle import bundle
9
10
  from upgini.utils.target_utils import balance_undersample, define_task
10
11
 
@@ -132,3 +133,62 @@ def test_balance_undersaampling_multiclass():
132
133
  })
133
134
  # Get all of 25% quantile class (b) and minor classes (a) and x2 (or all if less) of major classes
134
135
  assert_frame_equal(balanced_df.sort_values(by=SYSTEM_RECORD_ID).reset_index(drop=True), expected_df)
136
+
137
+
138
+ def test_binary_psi_calculation():
139
+ df = pd.DataFrame({
140
+ "target": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]
141
+ })
142
+ df["date"] = pd.date_range("2020-01-01", "2020-01-20")
143
+ enricher = FeaturesEnricher(search_keys={"date": SearchKey.DATE})
144
+ enricher._validate_PSI(df)
145
+ assert not enricher.warning_counter.has_warnings()
146
+
147
+ df = pd.DataFrame({
148
+ "target": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1]
149
+ })
150
+ df["date"] = pd.date_range("2020-01-01", "2020-01-20")
151
+ enricher = FeaturesEnricher(search_keys={"date": SearchKey.DATE})
152
+ enricher._validate_PSI(df)
153
+ assert enricher.warning_counter._count == 1
154
+
155
+ df = pd.DataFrame({
156
+ "target": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1],
157
+ "eval_set_index": [0] * 10 + [1] * 10,
158
+ })
159
+ df["date"] = pd.date_range("2020-01-01", "2020-01-20")
160
+ enricher = FeaturesEnricher(search_keys={"date": SearchKey.DATE})
161
+ enricher._validate_PSI(df)
162
+ assert enricher.warning_counter._count == 1
163
+
164
+ df = pd.DataFrame({
165
+ "target": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
166
+ "eval_set_index": [0] * 10 + [1] * 10,
167
+ })
168
+ df["date"] = pd.date_range("2020-01-01", "2020-01-20")
169
+ enricher = FeaturesEnricher(search_keys={"date": SearchKey.DATE})
170
+ enricher._validate_PSI(df)
171
+ assert enricher.warning_counter._count == 2
172
+
173
+
174
+ def test_regression_psi_calculation():
175
+ random = np.random.RandomState(42)
176
+ df = pd.DataFrame({
177
+ "target": random.rand(20)
178
+ })
179
+ df["date"] = pd.date_range("2020-01-01", "2020-01-20")
180
+ enricher = FeaturesEnricher(search_keys={"date": SearchKey.DATE})
181
+ enricher._validate_PSI(df)
182
+ assert enricher.warning_counter._count == 1
183
+
184
+ values1 = random.rand(10)
185
+ values2 = values1.copy()
186
+ values2[0] = 0.0
187
+ values2[9] = 1.0
188
+ df = pd.DataFrame({
189
+ "target": list(values1) + list(values2)
190
+ })
191
+ df["date"] = pd.date_range("2020-01-01", "2020-01-20")
192
+ enricher = FeaturesEnricher(search_keys={"date": SearchKey.DATE})
193
+ enricher._validate_PSI(df)
194
+ assert not enricher.warning_counter.has_warnings()