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.
- {empulse-0.4.2 → empulse-0.4.4}/PKG-INFO +2 -1
- {empulse-0.4.2 → empulse-0.4.4}/README.md +1 -0
- empulse-0.4.4/empulse/__init__.py +1 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/cost.py +3 -3
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/emp.py +12 -6
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/mp.py +10 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/savings.py +168 -95
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/bias_mitigation/bias_relabeling.py +35 -30
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/bias_mitigation/bias_resampling.py +37 -32
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/bias_mitigation/bias_reweighing.py +44 -39
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/b2boost.py +85 -60
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/csboost.py +84 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/cslogit.py +95 -5
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/robust_cs.py +83 -6
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/proflogit.py +15 -24
- {empulse-0.4.2 → empulse-0.4.4}/empulse/samplers/__init__.py +1 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/samplers/bias_relabler.py +88 -7
- {empulse-0.4.2 → empulse-0.4.4}/empulse/samplers/bias_resampler.py +85 -2
- empulse-0.4.4/empulse/samplers/cost_sampler.py +174 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/PKG-INFO +2 -1
- {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/SOURCES.txt +2 -1
- {empulse-0.4.2 → empulse-0.4.4}/pyproject.toml +1 -1
- empulse-0.4.2/empulse/__init__.py +0 -1
- {empulse-0.4.2 → empulse-0.4.4}/LICENSE +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/_convex_hull.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/_validation.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/_validation.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/cost.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/deterministic.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/acquisition/stochastic.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/_validation.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/deterministic.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/churn/stochastic.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/common.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/credit_scoring/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/credit_scoring/_validation.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/credit_scoring/deterministic.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/credit_scoring/stochastic.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/metrics/lift.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/_base/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/_base/boost.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/_base/logit.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/bias_mitigation/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/models/cost_sensitive/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/optimizers/__init__.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/optimizers/generation.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse/samplers/_strategies.py +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/dependency_links.txt +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/requires.txt +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/empulse.egg-info/top_level.txt +0 -0
- {empulse-0.4.2 → empulse-0.4.4}/setup.cfg +0 -0
- {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.
|
|
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
|
+
[](https://pepy.tech/projects/empulse)
|
|
149
150
|
[](https://pypi.org/project/empulse/)
|
|
150
151
|
[](https://github.com/ShimantoRahman/empulse)
|
|
151
152
|

|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
[](https://pepy.tech/projects/empulse)
|
|
1
2
|
[](https://pypi.org/project/empulse/)
|
|
2
3
|
[](https://github.com/ShimantoRahman/empulse)
|
|
3
4
|

|
|
@@ -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
|
|
439
|
+
Cost savings of a classifier compared to using a naive algorithm.
|
|
421
440
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
574
|
+
Expected savings of a classifier compared to using a naive algorithm.
|
|
538
575
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
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
|
-
|
|
618
|
+
Notes
|
|
619
|
+
-----
|
|
620
|
+
The expected cost of each instance :math:`\\mathbb{E}[C_i]` is calculated as [2]_:
|
|
582
621
|
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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(
|
|
682
|
-
y_pred = clf.predict(
|
|
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
|
|
696
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
29
|
-
Callable accepts two arguments: y_true and
|
|
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
|
-
|
|
32
|
-
The element at position (i, j) is the weight for the pair (y_true == i,
|
|
33
|
-
|
|
34
|
-
Function which transforms
|
|
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,
|
|
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
|
-
|
|
73
|
+
transform_feature=lambda clv: (clv > np.quantile(clv, 0.8)).astype(int)
|
|
74
74
|
)
|
|
75
|
-
model.fit(X, y,
|
|
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
|
|
89
|
-
def strategy(y_true,
|
|
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,
|
|
99
|
+
model.fit(X, y, sensitive_feature=high_clv)
|
|
100
100
|
|
|
101
|
-
4. Passing the
|
|
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,
|
|
123
|
+
search.fit(X, y, model__sensitive_feature=high_clv)
|
|
124
124
|
|
|
125
|
-
5. Passing the
|
|
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(
|
|
144
|
+
('model', BiasRelabelingClassifier(LogisticRegression()).set_fit_request(sensitive_feature=True))
|
|
145
145
|
])
|
|
146
146
|
search = GridSearchCV(pipeline, param_grid)
|
|
147
|
-
search.fit(X, y,
|
|
148
|
-
|
|
147
|
+
search.fit(X, y, sensitive_feature=high_clv)
|
|
148
|
+
|
|
149
|
+
References
|
|
150
|
+
----------
|
|
149
151
|
|
|
150
|
-
|
|
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
|
-
|
|
162
|
+
transform_feature: Optional[Callable[[np.ndarray], np.ndarray]] = None
|
|
158
163
|
):
|
|
159
164
|
self.estimator = estimator
|
|
160
165
|
self.strategy = strategy
|
|
161
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
|
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
|
-
|
|
214
|
+
sensitive_feature = np.asarray(sensitive_feature)
|
|
210
215
|
|
|
211
216
|
sampler = BiasRelabler(
|
|
212
217
|
estimator=self.estimator,
|
|
213
218
|
strategy=self.strategy,
|
|
214
|
-
|
|
219
|
+
transform_feature=self.transform_feature
|
|
215
220
|
)
|
|
216
|
-
X, y = sampler.fit_resample(X, y, sensitive_feature=
|
|
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
|
|