|
21 | 21 | from attrs import define, field |
22 | 22 |
|
23 | 23 | from rectools import Columns |
24 | | -from rectools.metrics.base import MetricAtK, outer_merge_reco |
| 24 | +from rectools.metrics.base import outer_merge_reco |
| 25 | +from rectools.metrics.debias import DebiasableMetrikAtK, calc_debiased_fit_task, debias_interactions |
25 | 26 |
|
26 | 27 |
|
27 | 28 | class InsufficientHandling(str, Enum): |
@@ -58,7 +59,7 @@ class AUCFitted: |
58 | 59 |
|
59 | 60 |
|
60 | 61 | @define |
61 | | -class _AUCMetric(MetricAtK): |
| 62 | +class _AUCMetric(DebiasableMetrikAtK): |
62 | 63 | """ |
63 | 64 | ROC AUC based metric base class. |
64 | 65 |
|
@@ -88,6 +89,8 @@ class _AUCMetric(MetricAtK): |
88 | 89 | until the model has non-zero scores for the item in item-item similarity matrix. So with |
89 | 90 | small `K` for neighbours in ItemKNN and big `K` for `recommend` and AUC based metric you |
90 | 91 | will still get an error when `insufficient_handling` is set to `raise`. |
| 92 | + debias_config : DebiasConfig, optional, default None |
| 93 | + Config with debias method parameters (iqr_coef, random_state). |
91 | 94 | """ |
92 | 95 |
|
93 | 96 | insufficient_handling: str = field(default="ignore") |
@@ -217,36 +220,45 @@ def calc_per_user(self, reco: pd.DataFrame, interactions: pd.DataFrame) -> pd.Se |
217 | 220 | pd.Series |
218 | 221 | Values of metric (index - user id, values - metric value for every user). |
219 | 222 | """ |
| 223 | + is_debiased = False |
| 224 | + if self.debias_config is not None: |
| 225 | + interactions = debias_interactions(interactions, self.debias_config) |
| 226 | + is_debiased = True |
| 227 | + |
220 | 228 | self._check(reco, interactions=interactions) |
221 | 229 | insufficient_handling_needed = self.insufficient_handling != InsufficientHandling.IGNORE |
222 | 230 | fitted = self.fit(reco, interactions, self.k, insufficient_handling_needed) |
223 | | - return self.calc_per_user_from_fitted(fitted) |
| 231 | + return self.calc_per_user_from_fitted(fitted, is_debiased) |
224 | 232 |
|
225 | | - def calc_from_fitted(self, fitted: AUCFitted) -> float: |
| 233 | + def calc_from_fitted(self, fitted: AUCFitted, is_debiased: bool = False) -> float: |
226 | 234 | """ |
227 | 235 | Calculate metric value from fitted data. |
228 | 236 |
|
229 | 237 | Parameters |
230 | 238 | ---------- |
231 | 239 | fitted : AUCFitted |
232 | 240 | Meta data that got from `.fit` method. |
| 241 | + is_debiased : bool, default False |
| 242 | + An indicator of whether the debias transformation has been applied before or not. |
233 | 243 |
|
234 | 244 | Returns |
235 | 245 | ------- |
236 | 246 | float |
237 | 247 | Value of metric (average between users). |
238 | 248 | """ |
239 | | - per_user = self.calc_per_user_from_fitted(fitted) |
| 249 | + per_user = self.calc_per_user_from_fitted(fitted, is_debiased) |
240 | 250 | return per_user.mean() |
241 | 251 |
|
242 | | - def calc_per_user_from_fitted(self, fitted: AUCFitted) -> pd.Series: |
| 252 | + def calc_per_user_from_fitted(self, fitted: AUCFitted, is_debiased: bool = False) -> pd.Series: |
243 | 253 | """ |
244 | 254 | Calculate metric values for all users from from fitted data. |
245 | 255 |
|
246 | 256 | Parameters |
247 | 257 | ---------- |
248 | 258 | fitted : AUCFitted |
249 | 259 | Meta data that got from `.fit` method. |
| 260 | + is_debiased : bool, default False |
| 261 | + An indicator of whether the debias transformation has been applied before or not. |
250 | 262 |
|
251 | 263 | Returns |
252 | 264 | ------- |
@@ -307,6 +319,8 @@ class PartialAUC(_AUCMetric): |
307 | 319 | until the model has non-zero scores for the item in item-item similarity matrix. So with |
308 | 320 | small `K` for neighbours in ItemKNN and big `K` for `recommend` and AUC based metric you |
309 | 321 | will still get an error when `insufficient_handling` is set to `raise`. |
| 322 | + debias_config : DebiasConfig, optional, default None |
| 323 | + Config with debias method parameters (iqr_coef, random_state). |
310 | 324 |
|
311 | 325 | Examples |
312 | 326 | -------- |
@@ -339,25 +353,26 @@ def _get_sufficient_reco_explanation(self) -> str: |
339 | 353 | not too high. |
340 | 354 | """ |
341 | 355 |
|
342 | | - def calc_per_user_from_fitted(self, fitted: AUCFitted) -> pd.Series: |
| 356 | + def calc_per_user_from_fitted(self, fitted: AUCFitted, is_debiased: bool = False) -> pd.Series: |
343 | 357 | """ |
344 | 358 | Calculate metric values for all users from from fitted data. |
345 | 359 |
|
346 | 360 | Parameters |
347 | 361 | ---------- |
348 | 362 | fitted : AUCFitted |
349 | 363 | Meta data that got from `.fit` method. |
| 364 | + is_debiased : bool, default False |
| 365 | + An indicator of whether the debias transformation has been applied before or not. |
350 | 366 |
|
351 | 367 | Returns |
352 | 368 | ------- |
353 | 369 | pd.Series |
354 | 370 | Values of metric (index - user id, values - metric value for every user). |
355 | 371 | """ |
| 372 | + self._check_debias(is_debiased, obj_name="AUCFitted") |
356 | 373 | outer_merged = fitted.outer_merged_enriched |
357 | | - |
358 | 374 | # Keep k first false positives for roc auc computation, keep all predicted test positives |
359 | 375 | cropped = outer_merged[(outer_merged["__fp_cumsum"] < self.k) & (~outer_merged[Columns.Rank].isna())] |
360 | | - |
361 | 376 | cropped_suf, n_pos_suf = self._handle_insufficient_cases( |
362 | 377 | outer_merged=cropped, n_pos=fitted.n_pos, n_fp_insufficient=fitted.n_fp_insufficient |
363 | 378 | ) |
@@ -415,6 +430,8 @@ class PAP(_AUCMetric): |
415 | 430 | until the model has non-zero scores for the item in item-item similarity matrix. So with |
416 | 431 | small `K` for neighbours in ItemKNN and big `K` for `recommend` and AUC based metric you |
417 | 432 | will still get an error when `insufficient_handling` is set to `raise`. |
| 433 | + debias_config : DebiasConfig, optional, default None |
| 434 | + Config with debias method parameters (iqr_coef, random_state). |
418 | 435 |
|
419 | 436 | Examples |
420 | 437 | -------- |
@@ -447,22 +464,24 @@ def _get_sufficient_reco_explanation(self) -> str: |
447 | 464 | for all users. |
448 | 465 | """ |
449 | 466 |
|
450 | | - def calc_per_user_from_fitted(self, fitted: AUCFitted) -> pd.Series: |
| 467 | + def calc_per_user_from_fitted(self, fitted: AUCFitted, is_debiased: bool = False) -> pd.Series: |
451 | 468 | """ |
452 | 469 | Calculate metric values for all users from outer merged recommendations. |
453 | 470 |
|
454 | 471 | Parameters |
455 | 472 | ---------- |
456 | 473 | fitted : AUCFitted |
457 | 474 | Meta data that got from `.fit` method. |
| 475 | + is_debiased : bool, default False |
| 476 | + An indicator of whether the debias transformation has been applied before or not. |
458 | 477 |
|
459 | 478 | Returns |
460 | 479 | ------- |
461 | 480 | pd.Series |
462 | 481 | Values of metric (index - user id, values - metric value for every user). |
463 | 482 | """ |
| 483 | + self._check_debias(is_debiased, obj_name="AUCFitted") |
464 | 484 | outer_merged = fitted.outer_merged_enriched |
465 | | - |
466 | 485 | # Keep k first false positives and k first predicted test positives for roc auc computation |
467 | 486 | cropped = outer_merged[ |
468 | 487 | (outer_merged["__test_pos_cumsum"] <= self.k) |
@@ -513,12 +532,22 @@ def calc_auc_metrics( |
513 | 532 | """ |
514 | 533 | results = {} |
515 | 534 |
|
516 | | - k_max = max(metric.k for metric in metrics.values()) |
517 | 535 | insufficient_handling_needed = any( |
518 | 536 | metric.insufficient_handling != InsufficientHandling.IGNORE for metric in metrics.values() |
519 | 537 | ) |
520 | | - fitted = _AUCMetric.fit(reco, interactions, k_max, insufficient_handling_needed) |
| 538 | + |
| 539 | + debiased_fit_task = calc_debiased_fit_task(metrics.values(), interactions) |
| 540 | + fitted_debiased = {} |
| 541 | + for debias_config_name, (k_max_d, interactions_d) in debiased_fit_task.items(): |
| 542 | + fitted_debiased[debias_config_name] = _AUCMetric.fit( |
| 543 | + reco, interactions_d, k_max_d, insufficient_handling_needed |
| 544 | + ) |
| 545 | + |
521 | 546 | for name, metric in metrics.items(): |
522 | | - results[name] = metric.calc_from_fitted(fitted) |
| 547 | + is_debiased = metric.debias_config is not None |
| 548 | + results[name] = metric.calc_from_fitted( |
| 549 | + fitted=fitted_debiased[metric.debias_config], |
| 550 | + is_debiased=is_debiased, |
| 551 | + ) |
523 | 552 |
|
524 | 553 | return results |
0 commit comments