empulse 0.4.2__tar.gz → 0.4.4__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 (56) hide show
  1. {empulse-0.4.2 → empulse-0.4.4}/PKG-INFO +2 -1
  2. {empulse-0.4.2 → empulse-0.4.4}/README.md +1 -0
  3. empulse-0.4.4/empulse/__init__.py +1 -0
  4. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/cost.py +3 -3
  5. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/emp.py +12 -6
  6. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/mp.py +10 -0
  7. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/savings.py +168 -95
  8. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/bias_mitigation/bias_relabeling.py +35 -30
  9. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/bias_mitigation/bias_resampling.py +37 -32
  10. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/bias_mitigation/bias_reweighing.py +44 -39
  11. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/b2boost.py +85 -60
  12. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/csboost.py +84 -0
  13. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/cslogit.py +95 -5
  14. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/robust_cs.py +83 -6
  15. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/proflogit.py +15 -24
  16. {empulse-0.4.2 → empulse-0.4.4}/empulse/samplers/__init__.py +1 -0
  17. {empulse-0.4.2 → empulse-0.4.4}/empulse/samplers/bias_relabler.py +88 -7
  18. {empulse-0.4.2 → empulse-0.4.4}/empulse/samplers/bias_resampler.py +85 -2
  19. empulse-0.4.4/empulse/samplers/cost_sampler.py +174 -0
  20. {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/PKG-INFO +2 -1
  21. {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/SOURCES.txt +2 -1
  22. {empulse-0.4.2 → empulse-0.4.4}/pyproject.toml +1 -1
  23. empulse-0.4.2/empulse/__init__.py +0 -1
  24. {empulse-0.4.2 → empulse-0.4.4}/LICENSE +0 -0
  25. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/__init__.py +0 -0
  26. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/_convex_hull.py +0 -0
  27. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/_validation.py +0 -0
  28. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/__init__.py +0 -0
  29. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/_validation.py +0 -0
  30. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/cost.py +0 -0
  31. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/deterministic.py +0 -0
  32. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/stochastic.py +0 -0
  33. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/__init__.py +0 -0
  34. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/_validation.py +0 -0
  35. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/deterministic.py +0 -0
  36. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/stochastic.py +0 -0
  37. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/common.py +0 -0
  38. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/credit_scoring/__init__.py +0 -0
  39. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/credit_scoring/_validation.py +0 -0
  40. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/credit_scoring/deterministic.py +0 -0
  41. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/credit_scoring/stochastic.py +0 -0
  42. {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/lift.py +0 -0
  43. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/__init__.py +0 -0
  44. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/_base/__init__.py +0 -0
  45. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/_base/boost.py +0 -0
  46. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/_base/logit.py +0 -0
  47. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/bias_mitigation/__init__.py +0 -0
  48. {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/__init__.py +0 -0
  49. {empulse-0.4.2 → empulse-0.4.4}/empulse/optimizers/__init__.py +0 -0
  50. {empulse-0.4.2 → empulse-0.4.4}/empulse/optimizers/generation.py +0 -0
  51. {empulse-0.4.2 → empulse-0.4.4}/empulse/samplers/_strategies.py +0 -0
  52. {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/dependency_links.txt +0 -0
  53. {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/requires.txt +0 -0
  54. {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/top_level.txt +0 -0
  55. {empulse-0.4.2 → empulse-0.4.4}/setup.cfg +0 -0
  56. {empulse-0.4.2 → empulse-0.4.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: empulse
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Value-driven and cost-sensitive tools for scikit-learn
5
5
  Author-email: Shimanto Rahman <shimanto.rahman@ugent.be>
6
6
  License: MIT License
@@ -146,6 +146,7 @@ Provides-Extra: dev
146
146
  Requires-Dist: empulse[docs,test]; extra == "dev"
147
147
  Requires-Dist: twine>=4.0.2; extra == "dev"
148
148
 
149
+ [![PyPI Downloads](https://static.pepy.tech/badge/empulse)](https://pepy.tech/projects/empulse)
149
150
  [![Python Version](https://img.shields.io/pypi/v/empulse)](https://pypi.org/project/empulse/)
150
151
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ShimantoRahman/empulse)
151
152
  ![](https://img.shields.io/pypi/pyversions/empulse)
@@ -1,3 +1,4 @@
1
+ [![PyPI Downloads](https://static.pepy.tech/badge/empulse)](https://pepy.tech/projects/empulse)
1
2
  [![Python Version](https://img.shields.io/pypi/v/empulse)](https://pypi.org/project/empulse/)
2
3
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ShimantoRahman/empulse)
3
4
  ![](https://img.shields.io/pypi/pyversions/empulse)
@@ -0,0 +1 @@
1
+ __version__ = "0.4.4"
@@ -136,6 +136,9 @@ def mpc_cost_score(
136
136
  Only a fraction of churners accepts the incentive offer.
137
137
  For detailed information, consult the paper [1]_.
138
138
 
139
+ .. seealso::
140
+ :class:`~empulse.models.B2BoostClassifier` : Uses the instance-specific cost function as objective function.
141
+
139
142
  Parameters
140
143
  ----------
141
144
 
@@ -190,9 +193,6 @@ def mpc_cost_score(
190
193
  clf.fit(X_train, y_train)
191
194
  y_pred = clf.predict(X_test)
192
195
 
193
- .. seealso::
194
- :class:`~empulse.models.B2BoostClassifier` : Uses the instance-specific cost function as objective function.
195
-
196
196
  References
197
197
  ----------
198
198
  .. [1] Janssens, B., Bogaert, M., Bagué, A., & Van den Poel, D. (2022).
@@ -22,6 +22,11 @@ def emp_score(
22
22
  """
23
23
  :func:`~empulse.metrics.emp()` but only returning the EMP score
24
24
 
25
+ .. seealso::
26
+ :func:`~empulse.metrics.emp` : To also return the threshold at which the EMP score is achieved.
27
+
28
+ :func:`~empulse.metrics.mp_score` : For a deterministic version of the EMP score.
29
+
25
30
  Parameters
26
31
  ----------
27
32
  y_true : 1D array-like, shape=(n_samples,)
@@ -164,6 +169,11 @@ def emp(
164
169
  """
165
170
  Expected Maximum Profit Measure (EMP)
166
171
 
172
+ .. seealso::
173
+ :func:`~empulse.metrics.emp_score` : To only return the EMP score.
174
+
175
+ :func:`~empulse.metrics.mp` : For a deterministic version of the EMP.
176
+
167
177
  Parameters
168
178
  ----------
169
179
  y_true : 1D array-like, shape=(n_samples,)
@@ -230,14 +240,10 @@ def emp(
230
240
  Returns
231
241
  -------
232
242
  emp : float
233
- Expected Maximum Profit
243
+ Expected Maximum Profit.
234
244
 
235
245
  threshold : float
236
- Threshold at which the expected maximum profit is achieved
237
-
238
- .. seealso::
239
- :func:`~empulse.metrics.emp_score` : Convenience function around :func:`~empulse.metrics.emp()`
240
- only returning EMP score
246
+ Threshold at which the expected maximum profit is achieved.
241
247
 
242
248
  Notes
243
249
  -----
@@ -16,6 +16,11 @@ def mp_score(
16
16
  """
17
17
  :func:`~empulse.metrics.mp()` but only returning the MP score
18
18
 
19
+ .. seealso::
20
+ :func:`~empulse.metrics.mp` : To also return the threshold at which the maximum profit is achieved.
21
+
22
+ :func:`~empulse.metrics.emp` : For a stochastic version of the maximum profit.
23
+
19
24
  Parameters
20
25
  ----------
21
26
  y_true : 1D array-like, shape=(n_samples,)
@@ -93,6 +98,11 @@ def mp(
93
98
  """
94
99
  Maximum Profit Measure (MP)
95
100
 
101
+ .. seealso::
102
+ :func:`~empulse.metrics.mp_score` : To only return the maximum profit score.
103
+
104
+ :func:`~empulse.metrics.emp` : For a stochastic version of the maximum profit.
105
+
96
106
  Parameters
97
107
  ----------
98
108
  y_true : 1D array-like, shape=(n_samples,)
@@ -95,21 +95,6 @@ def cost_loss(
95
95
  the cost of a false negative (not detecting a fraudulent transaction) is higher than
96
96
  the cost of a false positive (flagging a non-fraudulent transaction as fraudulent).
97
97
 
98
- The cost of each instance :math:`C_i` is calculated as [3]_:
99
-
100
- .. math:: C_i = y_i \\cdot (\\hat y_i \\cdot C_i(1|1) + (1 - \\hat y_i) \\cdot C_i(0|1)) + (1 - \\hat y_i) \\cdot (\\hat y_i \\cdot C_i(1|0) + (1 - \\hat y_i) \\cdot C_i(0|0))
101
-
102
- where
103
-
104
- - :math:`y_i` is the true label,
105
- - :math:`\\hat y_i` is the predicted label,
106
- - :math:`C_i(1|1)` is the cost of a true positive ``tp_cost``,
107
- - :math:`C_i(0|1)` is the cost of a false positive ``fp_cost``,
108
- - :math:`C_i(1|0)` is the cost of a false negative ``fn_cost``, and
109
- - :math:`C_i(0|0)` is the cost of a true negative ``tn_cost``.
110
-
111
- Code modified from `costcla.metrics.cost_loss`.
112
-
113
98
  .. seealso::
114
99
 
115
100
  :func:`~empulse.metrics.expected_cost_loss` : Expected cost of a classifier.
@@ -160,6 +145,23 @@ def cost_loss(
160
145
  cost_loss : float
161
146
  Cost of a classifier.
162
147
 
148
+ Notes
149
+ -----
150
+ The cost of each instance :math:`C_i` is calculated as [3]_:
151
+
152
+ .. math:: C_i = y_i \\cdot (\\hat y_i \\cdot C_i(1|1) + (1 - \\hat y_i) \\cdot C_i(0|1)) + (1 - \\hat y_i) \\cdot (\\hat y_i \\cdot C_i(1|0) + (1 - \\hat y_i) \\cdot C_i(0|0))
153
+
154
+ where
155
+
156
+ - :math:`y_i` is the true label,
157
+ - :math:`\\hat y_i` is the predicted label,
158
+ - :math:`C_i(1|1)` is the cost of a true positive ``tp_cost``,
159
+ - :math:`C_i(0|1)` is the cost of a false positive ``fp_cost``,
160
+ - :math:`C_i(1|0)` is the cost of a false negative ``fn_cost``, and
161
+ - :math:`C_i(0|0)` is the cost of a true negative ``tn_cost``.
162
+
163
+ Code modified from `costcla.metrics.cost_loss`.
164
+
163
165
  References
164
166
  ----------
165
167
  .. [1] C. Elkan, "The foundations of Cost-Sensitive Learning",
@@ -225,21 +227,6 @@ def expected_cost_loss(
225
227
  the cost of a false negative (not detecting a fraudulent transaction) is higher than
226
228
  the cost of a false positive (flagging a non-fraudulent transaction as fraudulent).
227
229
 
228
- The expected cost of each instance :math:`\\mathbb{E}[C_i]` is calculated as [3]_:
229
-
230
- .. math:: \\mathbb{E}[C_i] = y_i \\cdot (s_i \\cdot C_i(1|1) + (1 - s_i) \\cdot C_i(0|1)) + (1 - s_i) \\cdot (s_i \\cdot C_i(1|0) + (1 - s_i) \\cdot C_i(0|0))
231
-
232
- where
233
-
234
- - :math:`y_i` is the true label,
235
- - :math:`s_i` is the predicted probability,
236
- - :math:`C_i(1|1)` is the cost of a true positive ``tp_cost``,
237
- - :math:`C_i(0|1)` is the cost of a false positive ``fp_cost``,
238
- - :math:`C_i(1|0)` is the cost of a false negative ``fn_cost``, and
239
- - :math:`C_i(0|0)` is the cost of a true negative ``tn_cost``.
240
-
241
- Code modified from `costcla.metrics.cost_loss`.
242
-
243
230
  .. seealso::
244
231
 
245
232
  :func:`~empulse.metrics.cost_loss` : Cost of a classifier.
@@ -283,6 +270,23 @@ def expected_cost_loss(
283
270
  cost_loss : float
284
271
  Cost of a classifier.
285
272
 
273
+ Notes
274
+ -----
275
+ The expected cost of each instance :math:`\\mathbb{E}[C_i]` is calculated as [3]_:
276
+
277
+ .. math:: \\mathbb{E}[C_i] = y_i \\cdot (s_i \\cdot C_i(1|1) + (1 - s_i) \\cdot C_i(0|1)) + (1 - s_i) \\cdot (s_i \\cdot C_i(1|0) + (1 - s_i) \\cdot C_i(0|0))
278
+
279
+ where
280
+
281
+ - :math:`y_i` is the true label,
282
+ - :math:`s_i` is the predicted probability,
283
+ - :math:`C_i(1|1)` is the cost of a true positive ``tp_cost``,
284
+ - :math:`C_i(0|1)` is the cost of a false positive ``fp_cost``,
285
+ - :math:`C_i(1|0)` is the cost of a false negative ``fn_cost``, and
286
+ - :math:`C_i(0|0)` is the cost of a true negative ``tn_cost``.
287
+
288
+ Code modified from `costcla.metrics.cost_loss`.
289
+
286
290
  References
287
291
  ----------
288
292
  .. [1] C. Elkan, "The foundations of Cost-Sensitive Learning",
@@ -341,22 +345,6 @@ def expected_log_cost_loss(
341
345
  the cost of a false negative (not detecting a fraudulent transaction) is higher than
342
346
  the cost of a false positive (flagging a non-fraudulent transaction as fraudulent).
343
347
 
344
- The expected log cost of each instance :math:`\\mathbb{E}[C^l_i]` is calculated as [3]_:
345
-
346
- .. math:: \\mathbb{E}[C^l_i] = y_i \\cdot (\\log(s_i) \\cdot C_i(1|1) + \\log(1 - s_i) \\cdot C_i(0|1)) + (1 - s_i) \\cdot (\\log(s_i) \\cdot C_i(1|0) + \\log(1 - s_i) \\cdot C_i(0|0))
347
-
348
- where
349
-
350
- - :math:`y_i` is the true label,
351
- - :math:`s_i` is the predicted probability,
352
- - :math:`C_i(1|1)` is the cost of a true positive ``tp_cost``,
353
- - :math:`C_i(0|1)` is the cost of a false positive ``fp_cost``,
354
- - :math:`C_i(1|0)` is the cost of a false negative ``fn_cost``, and
355
- - :math:`C_i(0|0)` is the cost of a true negative ``tn_cost``.
356
-
357
- When ``tp_cost`` and ``tn_cost`` equal -1, and `fp_cost`` and ``tn_cost`` equal 0,
358
- the expected log cost is equivalent to the log loss :func:`sklearn:sklearn.metrics.log_loss`.
359
-
360
348
  .. seealso::
361
349
 
362
350
  :func:`~empulse.metrics.expected_cost_loss` : Expected cost of a classifier.
@@ -396,6 +384,37 @@ def expected_log_cost_loss(
396
384
  -------
397
385
  log_expected_cost : float
398
386
  Log expected cost.
387
+
388
+ Notes
389
+ -----
390
+ The expected log cost of each instance :math:`\\mathbb{E}[C^l_i]` is calculated as:
391
+
392
+ .. math:: \\mathbb{E}[C^l_i] = y_i \\cdot (\\log(s_i) \\cdot C_i(1|1) + \\log(1 - s_i) \\cdot C_i(0|1)) + (1 - s_i) \\cdot (\\log(s_i) \\cdot C_i(1|0) + \\log(1 - s_i) \\cdot C_i(0|0))
393
+
394
+ where
395
+
396
+ - :math:`y_i` is the true label,
397
+ - :math:`s_i` is the predicted probability,
398
+ - :math:`C_i(1|1)` is the cost of a true positive ``tp_cost``,
399
+ - :math:`C_i(0|1)` is the cost of a false positive ``fp_cost``,
400
+ - :math:`C_i(1|0)` is the cost of a false negative ``fn_cost``, and
401
+ - :math:`C_i(0|0)` is the cost of a true negative ``tn_cost``.
402
+
403
+ When ``tp_cost`` and ``tn_cost`` equal -1, and `fp_cost`` and ``tn_cost`` equal 0,
404
+ the expected log cost is equivalent to the log loss :func:`sklearn:sklearn.metrics.log_loss`.
405
+
406
+ Examples
407
+ --------
408
+
409
+ .. code-block:: python
410
+
411
+ import numpy as np
412
+ from empulse.metrics import expected_log_cost_loss
413
+ y_pred = [0.1, 0.9, 0.8, 0.2]
414
+ y_true = [0, 1, 1, 0]
415
+ fp_cost = np.array([4, 1, 2, 2])
416
+ fn_cost = np.array([1, 3, 3, 1])
417
+ expected_log_cost_loss(y_true, y_pred, fp_cost=fp_cost, fn_cost=fn_cost)
399
418
  """
400
419
  y_true, y_pred, tp_cost, fp_cost, tn_cost, fn_cost = _validate_input(
401
420
  y_true, y_pred, tp_cost, fp_cost, tn_cost, fn_cost, check_input
@@ -417,18 +436,18 @@ def savings_score(
417
436
  check_input: bool = True,
418
437
  ) -> float:
419
438
  """
420
- Cost savings of a classifier compared to using no algorithm at all.
439
+ Cost savings of a classifier compared to using a naive algorithm.
421
440
 
422
- This function calculates the savings cost of using y_pred on y_true with a
423
- cost-matrix, as the difference of y_pred and the cost_loss of a naive
424
- classification model.
441
+ The cost savings of a classifiers is the cost the classifier saved over a naive classification model
442
+ (predicting all ones or zeros whichever is better).
443
+ With 1 being the perfect model, 0 being not better than the naive model.
425
444
 
426
445
  Modified from `costcla.metrics.savings_score`.
427
446
 
428
447
  .. seealso::
429
448
 
430
449
  :func:`~empulse.metrics.expected_savings_score` : Expected savings of a classifier
431
- compared to using no algorithm at all.
450
+ compared to using a naive algorithm.
432
451
 
433
452
  :func:`~empulse.metrics.cost_loss` : Cost of a classifier.
434
453
 
@@ -477,7 +496,40 @@ def savings_score(
477
496
  score : float
478
497
  Cost savings of a classifier compared to using no algorithm at all.
479
498
 
480
- The best performance is 1.
499
+ Notes
500
+ -----
501
+ The cost of each instance :math:`C_i` is calculated as [1]_:
502
+
503
+ .. math:: C_i(s_i) = y_i \\cdot (\\hat y_i \\cdot C_i(1|1) + (1 - \\hat y_i) \\cdot C_i(0|1)) + (1 - \\hat y_i) \\cdot (\\hat y_i \\cdot C_i(1|0) + (1 - \\hat y_i) \\cdot C_i(0|0))
504
+
505
+ The savings over a naive model is calculated as:
506
+
507
+ .. math:: \\text{Savings} = 1 - \\frac{\\sum_{i=1}^N C_i(s_i)}{\\min(\\sum_{i=1}^N C_i(0), \\sum_{i=1}^N C_i(1))}
508
+
509
+ where
510
+
511
+ - :math:`y_i` is the true label,
512
+ - :math:`\\hat y_i` is the predicted label,
513
+ - :math:`C_i(1|1)` is the cost of a true positive ``tp_cost``,
514
+ - :math:`C_i(0|1)` is the cost of a false positive ``fp_cost``,
515
+ - :math:`C_i(1|0)` is the cost of a false negative ``fn_cost``, and
516
+ - :math:`C_i(0|0)` is the cost of a true negative ``tn_cost``.
517
+ - :math:`N` is the number of samples.
518
+
519
+ Code modified from `costcla.metrics.cost_loss`.
520
+
521
+ Examples
522
+ --------
523
+
524
+ .. code-block:: python
525
+
526
+ import numpy as np
527
+ from empulse.metrics import savings_score
528
+ y_pred = [0, 1, 0, 0]
529
+ y_true = [0, 1, 1, 0]
530
+ fp_cost = np.array([4, 1, 2, 2])
531
+ fn_cost = np.array([1, 3, 3, 1])
532
+ savings_score(y_true, y_pred, fp_cost=fp_cost, fn_cost=fn_cost)
481
533
 
482
534
  References
483
535
  ----------
@@ -489,21 +541,6 @@ def savings_score(
489
541
  .. [2] Höppner, S., Baesens, B., Verbeke, W., & Verdonck, T. (2022).
490
542
  Instance-dependent cost-sensitive learning for detecting transfer fraud.
491
543
  European Journal of Operational Research, 297(1), 291-300.
492
-
493
- See also
494
- --------
495
- cost_loss
496
-
497
- Examples
498
- --------
499
- >>> import numpy as np
500
- >>> from empulse.metrics import savings_score
501
- >>> y_pred = [0, 1, 0, 0]
502
- >>> y_true = [0, 1, 1, 0]
503
- >>> fp_cost = np.array([4, 1, 2, 2])
504
- >>> fn_cost = np.array([1, 3, 3, 1])
505
- >>> savings_score(y_true, y_pred, fp_cost=fp_cost, fn_cost=fn_cost)
506
- 0.5
507
544
  """
508
545
 
509
546
  y_true, y_pred, tp_cost, fp_cost, tn_cost, fn_cost = _validate_input(
@@ -534,15 +571,15 @@ def expected_savings_score(
534
571
  check_input: bool = True,
535
572
  ) -> float:
536
573
  """
537
- Expected savings of a classifier compared to using no algorithm at all.
574
+ Expected savings of a classifier compared to using a naive algorithm.
538
575
 
539
- This function calculates the expected savings cost of using y_pred on y_true with a
540
- cost-matrix, as the difference of y_pred and the cost_loss of a naive
541
- classification model.
576
+ The expected cost savings of a classifiers is the expected cost the classifier saved
577
+ over a naive classification model (predicting all ones or zeros whichever is better).
578
+ With 1 being the perfect model, 0 being not better than the naive model.
542
579
 
543
580
  .. seealso::
544
581
 
545
- :func:`~empulse.metrics.savings_score` : Cost savings of a classifier compared to using no algorithm at all.
582
+ :func:`~empulse.metrics.savings_score` : Cost savings of a classifier compared to using a naive algorithm.
546
583
 
547
584
  :func:`~empulse.metrics.expected_cost_loss` : Expected cost of a classifier.
548
585
 
@@ -578,29 +615,44 @@ def expected_savings_score(
578
615
  score : float
579
616
  Expected savings of a classifier compared to using no algorithm at all.
580
617
 
581
- The best performance is 1.
618
+ Notes
619
+ -----
620
+ The expected cost of each instance :math:`\\mathbb{E}[C_i]` is calculated as [2]_:
582
621
 
583
- References
584
- ----------
622
+ .. math:: \\mathbb{E}[C_i(s_i)] = y_i \\cdot (s_i \\cdot C_i(1|1) + (1 - s_i) \\cdot C_i(0|1)) + (1 - s_i) \\cdot (s_i \\cdot C_i(1|0) + (1 - s_i) \\cdot C_i(0|0))
585
623
 
586
- .. [1] Höppner, S., Baesens, B., Verbeke, W., & Verdonck, T. (2022).
587
- Instance-dependent cost-sensitive learning for detecting transfer fraud.
588
- European Journal of Operational Research, 297(1), 291-300.
624
+ The expected savings over a naive model is calculated as:
589
625
 
590
- See also
591
- --------
592
- savings_score
626
+ .. math:: \\text{Expected Savings} = 1 - \\frac{\\sum_{i=1}^N \\mathbb{E}[C_i(s_i)]}{\\min(\\sum_{i=1}^N C_i(0), \\sum_{i=1}^N C_i(1))}
627
+
628
+ where
629
+
630
+ - :math:`y_i` is the true label,
631
+ - :math:`\\hat y_i` is the predicted label,
632
+ - :math:`C_i(1|1)` is the cost of a true positive ``tp_cost``,
633
+ - :math:`C_i(0|1)` is the cost of a false positive ``fp_cost``,
634
+ - :math:`C_i(1|0)` is the cost of a false negative ``fn_cost``, and
635
+ - :math:`C_i(0|0)` is the cost of a true negative ``tn_cost``.
636
+ - :math:`N` is the number of samples.
593
637
 
594
638
  Examples
595
639
  --------
596
- >>> import numpy as np
597
- >>> from empulse.metrics import expected_savings_score
598
- >>> y_pred = [0.4, 0.8, 0.75, 0.1]
599
- >>> y_true = [0, 1, 1, 0]
600
- >>> fp_cost = np.array([4, 1, 2, 2])
601
- >>> fn_cost = np.array([1, 3, 3, 1])
602
- >>> expected_savings_score(y_true, y_pred, fp_cost=fp_cost, fn_cost=fn_cost)
603
- 0.475
640
+
641
+ .. code-block:: python
642
+
643
+ import numpy as np
644
+ from empulse.metrics import expected_savings_score
645
+ y_pred = [0.4, 0.8, 0.75, 0.1]
646
+ y_true = [0, 1, 1, 0]
647
+ fp_cost = np.array([4, 1, 2, 2])
648
+ fn_cost = np.array([1, 3, 3, 1])
649
+ expected_savings_score(y_true, y_pred, fp_cost=fp_cost, fn_cost=fn_cost)
650
+
651
+ References
652
+ ----------
653
+ .. [1] Höppner, S., Baesens, B., Verbeke, W., & Verdonck, T. (2022).
654
+ Instance-dependent cost-sensitive learning for detecting transfer fraud.
655
+ European Journal of Operational Research, 297(1), 291-300.
604
656
  """
605
657
  y_true, y_pred, tp_cost, fp_cost, tn_cost, fn_cost = _validate_input(
606
658
  y_true, y_pred, tp_cost, fp_cost, tn_cost, fn_cost, check_input
@@ -675,11 +727,14 @@ def make_objective_aec(
675
727
 
676
728
  import xgboost as xgb
677
729
  from empulse.metrics import make_objective_aec
730
+ from sklearn.datasets import make_classification
731
+
732
+ X, y = make_classification()
733
+ objective = make_objective_aec('xgboost', fp_cost=1, fn_cost=1)
678
734
 
679
- objective = make_objective_aec()
680
735
  clf = xgb.XGBClassifier(objective=objective, n_estimators=100, max_depth=3)
681
- clf.fit(X_train, y_train)
682
- y_pred = clf.predict(X_test)
736
+ clf.fit(X, y)
737
+ y_pred = clf.predict(X)
683
738
 
684
739
  References
685
740
  ----------
@@ -692,8 +747,15 @@ def make_objective_aec(
692
747
  objective = partial(_objective_xgboost, tp_cost=tp_cost, tn_cost=tn_cost, fn_cost=fn_cost, fp_cost=fp_cost)
693
748
  update_wrapper(objective, _objective_xgboost)
694
749
  elif model == 'lightgbm':
695
- objective = partial(_objective_lightgbm, tp_cost=tp_cost, tn_cost=tn_cost, fn_cost=fn_cost, fp_cost=fp_cost)
696
- update_wrapper(objective, _objective_lightgbm)
750
+ def objective(y_pred, train_data):
751
+ return _objective_lightgbm(
752
+ y_pred,
753
+ train_data,
754
+ tp_cost=tp_cost,
755
+ tn_cost=tn_cost,
756
+ fn_cost=fn_cost,
757
+ fp_cost=fp_cost
758
+ )
697
759
  elif model == 'catboost':
698
760
  objective = partial(_objective_catboost, tp_cost=tp_cost, tn_cost=tn_cost, fn_cost=fn_cost, fp_cost=fp_cost)
699
761
  update_wrapper(objective, _objective_catboost)
@@ -793,7 +855,18 @@ def _objective_lightgbm(
793
855
  fn_cost=0.0,
794
856
  fp_cost=0.0,
795
857
  ) -> tuple[np.ndarray, np.ndarray]:
796
- raise NotImplementedError("Objective function for LightGBM is not implemented yet.")
858
+ if hasattr(train_data, 'get_label'):
859
+ y_true = train_data.get_label()
860
+ elif isinstance(train_data, np.ndarray):
861
+ y_true = train_data
862
+ else:
863
+ raise TypeError(f"Invalid train data type, got {type(train_data)}.")
864
+
865
+ y_pred = expit(y_pred)
866
+ cost = y_true * (tp_cost - fn_cost) + (1 - y_true) * (fp_cost - tn_cost)
867
+ gradient = y_pred * (1 - y_pred) * cost
868
+ hessian = np.abs((1 - 2 * y_pred) * gradient)
869
+ return gradient, hessian
797
870
 
798
871
 
799
872
  def _objective_catboost(
@@ -20,18 +20,18 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
20
20
  Base estimator which is used for fitting and predicting.
21
21
  strategy : {'statistical parity', 'demographic parity'} or Callable, default='statistical parity'
22
22
  Determines how the group weights are computed.
23
- Group weights determine how many instances to relabel for each combination of target and protected attribute.
23
+ Group weights determine how many instances to relabel for each combination of target and sensitive feature.
24
24
 
25
25
  - ``'statistical_parity'`` or ``'demographic parity'``: \
26
- probability of positive predictions are equal between subgroups of protected attribute.
26
+ probability of positive predictions are equal between subgroups of sensitive feature.
27
27
 
28
- - ``Callable``: function which computes the group weights based on the target and protected attribute. \
29
- Callable accepts two arguments: y_true and protected_attr and returns the group weights. \
28
+ - ``Callable``: function which computes the group weights based on the target and sensitive feature. \
29
+ Callable accepts two arguments: y_true and sensitive_feature and returns the group weights. \
30
30
  Group weights are a 2x2 matrix where the rows represent the target variable and the columns represent the \
31
- protected attribute. \
32
- The element at position (i, j) is the weight for the pair (y_true == i, protected_attr == j).
33
- transform_attr : Optional[Callable], default=None
34
- Function which transforms protected attribute before resampling the training data.
31
+ sensitive feature. \
32
+ The element at position (i, j) is the weight for the pair (y_true == i, sensitive_feature == j).
33
+ transform_feature : Optional[Callable], default=None
34
+ Function which transforms sensitive feature before resampling the training data.
35
35
 
36
36
  Attributes
37
37
  ----------
@@ -55,7 +55,7 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
55
55
  high_clv = np.random.randint(0, 2, size=X.shape[0])
56
56
 
57
57
  model = BiasRelabelingClassifier(estimator=LogisticRegression())
58
- model.fit(X, y, protected_attr=high_clv)
58
+ model.fit(X, y, sensitive_feature=high_clv)
59
59
 
60
60
  2. Converting a continuous attribute to a binary attribute:
61
61
 
@@ -70,9 +70,9 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
70
70
 
71
71
  model = BiasRelabelingClassifier(
72
72
  estimator=LogisticRegression(),
73
- transform_attr=lambda clv: (clv > np.quantile(clv, 0.8)).astype(int)
73
+ transform_feature=lambda clv: (clv > np.quantile(clv, 0.8)).astype(int)
74
74
  )
75
- model.fit(X, y, protected_attr=clv)
75
+ model.fit(X, y, sensitive_feature=clv)
76
76
 
77
77
  3. Using a custom strategy function:
78
78
 
@@ -85,8 +85,8 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
85
85
  X, y = make_classification()
86
86
  high_clv = np.random.randint(0, 2, size=X.shape[0])
87
87
 
88
- # Simple strategy to double the weight for the protected attribute
89
- def strategy(y_true, protected_attr):
88
+ # Simple strategy to double the weight for the sensitive feature
89
+ def strategy(y_true, sensitive_feature):
90
90
  return np.array([
91
91
  [1, 2],
92
92
  [1, 2]
@@ -96,9 +96,9 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
96
96
  estimator=LogisticRegression(),
97
97
  strategy=strategy
98
98
  )
99
- model.fit(X, y, protected_attr=high_clv)
99
+ model.fit(X, y, sensitive_feature=high_clv)
100
100
 
101
- 4. Passing the protected attribute in a cross-validation grid search:
101
+ 4. Passing the sensitive feature in a cross-validation grid search:
102
102
 
103
103
  .. code-block:: python
104
104
 
@@ -120,9 +120,9 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
120
120
  ('model', BiasRelabelingClassifier(LogisticRegression()))
121
121
  ])
122
122
  search = GridSearchCV(pipeline, param_grid)
123
- search.fit(X, y, model__protected_attr=high_clv)
123
+ search.fit(X, y, model__sensitive_feature=high_clv)
124
124
 
125
- 5. Passing the protected attribute through metadata routing in a cross-validation grid search:
125
+ 5. Passing the sensitive feature through metadata routing in a cross-validation grid search:
126
126
 
127
127
  .. code-block:: python
128
128
 
@@ -141,24 +141,29 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
141
141
  param_grid = {'model__estimator__C': [0.1, 1, 10]}
142
142
  pipeline = Pipeline([
143
143
  ('scaler', StandardScaler()),
144
- ('model', BiasRelabelingClassifier(LogisticRegression()).set_fit_request(protected_attr=True))
144
+ ('model', BiasRelabelingClassifier(LogisticRegression()).set_fit_request(sensitive_feature=True))
145
145
  ])
146
146
  search = GridSearchCV(pipeline, param_grid)
147
- search.fit(X, y, protected_attr=high_clv)
148
- """
147
+ search.fit(X, y, sensitive_feature=high_clv)
148
+
149
+ References
150
+ ----------
149
151
 
150
- __metadata_request__fit = {'protected_attr': True}
152
+ .. [1] Rahman, S., Janssens, B., & Bogaert, M. (2025).
153
+ Profit-driven pre-processing in B2B customer churn modeling using fairness techniques.
154
+ Journal of Business Research, 189, 115159. doi:10.1016/j.jbusres.2024.115159
155
+ """
151
156
 
152
157
  def __init__(
153
158
  self,
154
159
  estimator,
155
160
  *,
156
161
  strategy: Union[StrategyFn, Strategy] = 'statistical parity',
157
- transform_attr: Optional[Callable[[np.ndarray], np.ndarray]] = None
162
+ transform_feature: Optional[Callable[[np.ndarray], np.ndarray]] = None
158
163
  ):
159
164
  self.estimator = estimator
160
165
  self.strategy = strategy
161
- self.transform_attr = transform_attr
166
+ self.transform_feature = transform_feature
162
167
 
163
168
  def __sklearn_tags__(self):
164
169
  tags = super().__sklearn_tags__()
@@ -171,7 +176,7 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
171
176
  X: ArrayLike,
172
177
  y: ArrayLike,
173
178
  *,
174
- protected_attr: Optional[ArrayLike] = None,
179
+ sensitive_feature: Optional[ArrayLike] = None,
175
180
  **fit_params
176
181
  ) -> 'BiasRelabelingClassifier':
177
182
  """
@@ -183,8 +188,8 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
183
188
  Training data.
184
189
  y : 1D array-like, shape=(n_samples,)
185
190
  Target values.
186
- protected_attr : 1D array-like, shape=(n_samples,), default = None
187
- Protected attribute used to determine the sample group weights.
191
+ sensitive_feature : 1D array-like, shape=(n_samples,), default = None
192
+ Sensitive feature used to determine the sample group weights.
188
193
  fit_params : dict
189
194
  Additional parameters passed to the estimator's `fit` method.
190
195
 
@@ -202,18 +207,18 @@ class BiasRelabelingClassifier(ClassifierMixin, BaseEstimator):
202
207
  self.classes_ = np.unique(y)
203
208
  if len(self.classes_) == 1:
204
209
  raise ValueError("Classifier can't train when only one class is present.")
205
- if protected_attr is None:
210
+ if sensitive_feature is None:
206
211
  self.estimator_ = clone(self.estimator)
207
212
  self.estimator_.fit(X, y, **fit_params)
208
213
  return self
209
- protected_attr = np.asarray(protected_attr)
214
+ sensitive_feature = np.asarray(sensitive_feature)
210
215
 
211
216
  sampler = BiasRelabler(
212
217
  estimator=self.estimator,
213
218
  strategy=self.strategy,
214
- transform_attr=self.transform_attr
219
+ transform_feature=self.transform_feature
215
220
  )
216
- X, y = sampler.fit_resample(X, y, sensitive_feature=protected_attr)
221
+ X, y = sampler.fit_resample(X, y, sensitive_feature=sensitive_feature)
217
222
  self.estimator_ = clone(self.estimator)
218
223
  self.estimator_.fit(X, y, **fit_params)
219
224