Skip to content

Commit 4e59100

Browse files
authored
Merge pull request #24 from fidelity/probabilistic_membership
Probabilistic membership
2 parents 47848d3 + 4536a33 commit 4e59100

26 files changed

Lines changed: 2966 additions & 625 deletions

CHANGELOG.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22
CHANGELOG
33
=========
44

5+
-------------------------------------------------------------------------------
6+
Sep 09, 2022 2.0.0
7+
-------------------------------------------------------------------------------
8+
9+
- Probabilistic fairness metrics are added based on membership likelihoods and surrogates --thanks to @mthielbar
10+
- Algorithm based on Surrogate Membership for Inferred Metrics in Fairness Evaluation (LION 2023)
11+
512
-------------------------------------------------------------------------------
613
August 1, 2023 1.3.4
714
-------------------------------------------------------------------------------
815

916
- Added False Omission Rate Difference to Binary Fairness Metrics.
1017

18+
1119
-------------------------------------------------------------------------------
1220
April 21, 2023 1.3.3
1321
-------------------------------------------------------------------------------

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# These owners will be the default owners for everything in the repo.
2-
* @bkleyn @skadio
2+
* @bkleyn @skadio @mthielbar

README.md

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
# Jurity: Fairness & Evaluation Library
55

6-
Jurity is a research library that provides fairness metrics, recommender system evaluations, classification metrics and bias mitigation techniques. The library adheres to PEP-8 standards and is tested heavily.
6+
Jurity ([LION'23](), [ICMLA'21](https://ieeexplore.ieee.org/document/9680169)) is a research library
7+
that provides fairness metrics, recommender system evaluations, classification metrics and bias mitigation techniques.
8+
The library adheres to PEP-8 standards and is tested heavily.
79

8-
Jurity is developed by the Artificial Intelligence Center of Excellence at Fidelity Investments. Documentation is available at [fidelity.github.io/jurity](https://fidelity.github.io/jurity).
10+
Jurity is developed by the Artificial Intelligence Center of Excellence at Fidelity Investments.
11+
Documentation is available at [fidelity.github.io/jurity](https://fidelity.github.io/jurity).
912

1013
## Fairness Metrics
1114
* [Average Odds](https://fidelity.github.io/jurity/about_fairness.html#average-odds)
@@ -51,7 +54,7 @@ from jurity.fairness import BinaryFairnessMetrics, MultiClassFairnessMetrics
5154
binary_predictions = [1, 1, 0, 1, 0, 0]
5255
multi_class_predictions = ["a", "b", "c", "b", "a", "a"]
5356
multi_class_multi_label_predictions = [["a", "b"], ["b", "c"], ["b"], ["a", "b"], ["c", "a"], ["c"]]
54-
is_member = [0, 0, 0, 1, 1, 1]
57+
memberships = [0, 0, 0, 1, 1, 1]
5558
classes = ["a", "b", "c"]
5659

5760
# Metrics (see also other available metrics)
@@ -63,11 +66,41 @@ print("Metric:", metric.description)
6366
print("Lower Bound: ", metric.lower_bound)
6467
print("Upper Bound: ", metric.upper_bound)
6568
print("Ideal Value: ", metric.ideal_value)
66-
print("Binary Fairness score: ", metric.get_score(binary_predictions, is_member))
67-
print("Multi-class Fairness scores: ", multi_metric.get_scores(multi_class_predictions, is_member))
68-
print("Multi-class multi-label Fairness scores: ", multi_metric.get_scores(multi_class_multi_label_predictions, is_member))
69+
print("Binary Fairness score: ", metric.get_score(binary_predictions, memberships))
70+
print("Multi-class Fairness scores: ", multi_metric.get_scores(multi_class_predictions, memberships))
71+
print("Multi-class multi-label Fairness scores: ", multi_metric.get_scores(multi_class_multi_label_predictions, memberships))
6972
```
7073

74+
## Quick Start: Probabilistic Fairness Evaluation
75+
76+
What if we do not know the protected membership attribute of each sample? This is a practical scenario that we refer to as _probabilistic_ fairness evaluation.
77+
78+
At a high-level, instead of strict 0/1 deterministic membership at individual level, consider the probability of membership to protected classes for each sample.
79+
80+
An easy baseline is to convert these probabilities back to the deterministic setting by taking the maximum likelihood as the protected membership. This is problematic as the goal is not to predict membership but to evaluate fairness.
81+
82+
Taking this a step further, while we do not have membership information at the individual level, consider access to _surrogate membership_ at _group level_. We can then infer the fairness metrics directly.
83+
84+
Jurity offers both options to address the case where membership data is missing. We provide an in-depth study and formal treatment in [Surrogate Membership for Inferred Metrics in Fairness Evaluation (LION 2023)]().
85+
86+
```python
87+
from jurity.fairness import BinaryFairnessMetrics
88+
89+
# Instead of 0/1 deterministic membership at individual level
90+
# consider likelihoods of membership to protected classes for each sample
91+
binary_predictions = [1, 1, 0, 1]
92+
memberships = [[0.2, 0.8], [0.4, 0.6], [0.2, 0.8], [0.9, 0.1]]
93+
94+
# Metric
95+
metric = BinaryFairnessMetrics.StatisticalParity()
96+
print("Binary Fairness score: ", metric.get_score(binary_predictions, memberships))
97+
98+
# Surrogate membership: consider access to surrogate membership at the group level.
99+
surrogates = [0, 2, 0, 1]
100+
print("Binary Fairness score: ", metric.get_score(binary_predictions, memberships, surrogates))
101+
```
102+
103+
71104
## Quick Start: Bias Mitigation
72105

73106
```python
@@ -154,6 +187,32 @@ print('F1 score is', f1_score.get_score(predictions, labels))
154187

155188
Jurity requires **Python 3.7+** and can be installed from PyPI using ``pip install jurity`` or by building from source as shown in [installation instructions](https://fidelity.github.io/jurity/install.html).
156189

190+
## Citation
191+
192+
If you use MABWiser in a publication, please cite it as:
193+
194+
```bibtex
195+
@article{DBLP:conf/lion/Melinda23,
196+
author = {Melinda Thielbar, Serdar Kadioglu, Chenhui Zhang, Rick Pack, and Lukas Dannull},
197+
title = {Surrogate Membership for Inferred Metrics in Fairness Evaluation},
198+
booktitle = {The 17th Learning and Intelligent Optimization Conference (LION)},
199+
publisher = {{LION}},
200+
year = {2023}
201+
}
202+
203+
@inproceedings{DBLP:conf/icmla/MichalskyK21,
204+
author = {Filip Michalsk{\'{y}} and Serdar Kadioglu},
205+
title = {Surrogate Ground Truth Generation to Enhance Binary Fairness Evaluation in Uplift Modeling},
206+
booktitle = {20th {IEEE} International Conference on Machine Learning and Applications,
207+
{ICMLA} 2021, Pasadena, CA, USA, December 13-16, 2021},
208+
pages = {1654--1659},
209+
publisher = {{IEEE}},
210+
year = {2021},
211+
url = {https://doi.org/10.1109/ICMLA52953.2021.00264},
212+
doi = {10.1109/ICMLA52953.2021.00264},
213+
}
214+
```
215+
157216
## Support
158217
Please submit bug reports and feature requests as [Issues](https://github.com/fidelity/jurity/issues).
159218

jurity/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# Copyright FMR LLC <opensource@fidelity.com>
33
# SPDX-License-Identifier: Apache-2.0
44

5-
__version__ = "1.3.4"
5+
__version__ = "2.0.0"

jurity/constants.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import NamedTuple
2+
import numpy as np
3+
4+
5+
class Constants(NamedTuple):
6+
"""
7+
Constant values used by the modules.
8+
"""
9+
10+
default_seed = 1
11+
float_null = np.float64(0.0)
12+
bootstrap_trials = 100
13+
14+
TPR = "TPR"
15+
TNR = "TNR"
16+
FPR = "FPR"
17+
FNR = "FNR"
18+
PPV = "PPV"
19+
NPV = "NPV"
20+
FDR = "FDR"
21+
FOR = "FOR"
22+
ACC = "ACC"
23+
PRED_RATE = "Prediction Rate"
24+
25+
user_id = "user_id"
26+
item_id = "item_id"
27+
estimate = "estimate"
28+
inverse_propensity = "inverse_propensity"
29+
ips_correction = "ips_correction"
30+
propensity = "propensity"
31+
32+
true_positive_ratio = "true_positive_ratio"
33+
true_negative_ratio = "true_negative_ratio"
34+
false_positive_ratio = "false_positive_ratio"
35+
false_negative_ratio = "false_negative_ratio"
36+
prediction_ratio = "prediction_ratio"
37+
class_col_name = "class"
38+
weight_col_name = "count"
39+
no_label_metrics = ["StatisticalParity", "DisparateImpact"]
40+
probabilistic_metrics = ["AverageOdds", "EqualOpportunity",
41+
"FNRDifference", "StatisticalParity", "PredictiveEquality"]

jurity/fairness/__init__.py

Lines changed: 97 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
import inspect
6-
from typing import List, Union
6+
from typing import List, Union, Optional
77
from typing import NamedTuple
88

99
import numpy as np
1010
import pandas as pd
1111

1212
from jurity.fairness.base import _BaseBinaryFairness
1313
from jurity.fairness.base import _BaseMultiClassMetric
14-
from jurity.utils import check_inputs_validity
14+
from jurity.utils import is_one_dimensional
15+
from jurity.utils_proba import get_argmax_memberships
16+
from jurity.utils_proba import get_bootstrap_results
1517
from .average_odds import AverageOdds
1618
from .disparate_impact import BinaryDisparateImpact, MultiDisparateImpact
1719
from .equal_opportunity import EqualOpportunity
@@ -41,57 +43,121 @@ class BinaryFairnessMetrics(NamedTuple):
4143
@staticmethod
4244
def get_all_scores(labels: Union[List, np.ndarray, pd.Series],
4345
predictions: Union[List, np.ndarray, pd.Series],
44-
is_member: Union[List, np.ndarray, pd.Series],
45-
membership_label: Union[str, float, int] = 1) -> pd.DataFrame:
46+
memberships: Union[List, np.ndarray, pd.Series],
47+
surrogates: Union[List, np.ndarray, pd.Series] = None,
48+
membership_labels: Union[str, float, int, List, np.array] = 1,
49+
bootstrap_results: Optional[pd.DataFrame] = None) -> pd.DataFrame:
4650
"""
47-
Calculates and tabulates all of the fairness metric scores.
48-
51+
Calculates and tabulates all fairness metric scores.
4952
Parameters
5053
----------
5154
labels: Union[List, np.ndarray, pd.Series]
52-
Binary ground truth labels for the provided dataset (0/1).
55+
Binary ground truth labels for each sample.
5356
predictions: Union[List, np.ndarray, pd.Series]
54-
Binary predictions from some black-box classifier (0/1).
55-
is_member: Union[List, np.ndarray, pd.Series]
56-
Binary membership labels (0/1).
57-
membership_label: Union[str, float, int]
58-
Value indicating group membership.
59-
Default value is 1.
60-
57+
Binary prediction for each sample from a black-box classifier binary (0/1).
58+
memberships: Union[List, np.ndarray, pd.Series, List[List], pd.DataFrame]
59+
Membership attribute for each sample.
60+
If deterministic, it is the binary label for each sample [0, 1, 0, ..., 1]
61+
If probabilistic, it is the likelihoods array of membership labels
62+
for each sample, i.e., a two-dim array [[0.6, 0.2, 0.2], ..., [..]]
63+
surrogates: Union[List, np.ndarray, pd.Series]
64+
Surrogate class attribute for each sample.
65+
If the membership is deterministic, surrogates are not needed.
66+
If the membership is probabilistic,
67+
- if surrogates are given, inferred metrics are used
68+
to calculate the fairness metric as proposed in [1]_.
69+
- when surrogates are not given, the arg max likelihood is used as the membership for each sample.
70+
Default is None.
71+
membership_labels: Union[int, float, str, List[int],np.array[int]]
72+
Labels indicating group membership.
73+
If the membership is deterministic, a single str/int is expected, e.g., 1.
74+
If the membership is probabilistic, a list or np.array of int is expected,
75+
with the index of the protected groups in the memberships array,
76+
e.g, [1, 2, 3], if 1-2-3 indexes are protected.
77+
Default value is 1 for deterministic case or [1] for probabilistic case.
78+
bootstrap_results: Optional[pd.DataFrame]
79+
A Pandas dataframe with inferred scores based surrogate class memberships.
80+
Default value is None.
81+
When given, other parameters will be discarded and bootstrap results will be used.
6182
Returns
6283
----------
6384
Pandas data frame with all implemented binary fairness metrics.
6485
"""
65-
# Logic to check input types
66-
check_inputs_validity(labels=labels, predictions=predictions, is_member=is_member, optional_labels=False)
67-
68-
fairness_funcs = inspect.getmembers(BinaryFairnessMetrics, predicate=inspect.isclass)[:-1]
6986

87+
# if memberships is given as likelihoods WITHOUT any surrogates, then revise it to deterministic case
88+
is_memberships_1d = is_one_dimensional(memberships)
89+
if not is_memberships_1d and surrogates is None and bootstrap_results is None:
90+
# Subtle point: membership_labels need to be an array when membership is 2d
91+
# if the user didn't specify, which defaults to 1, convert 1 -> [1] automatically
92+
# BUT do not overwrite membership_labels, we are still in "deterministic" mode via argmax
93+
# In deterministic mode, we need a single primitive label like 1
94+
memberships = get_argmax_memberships(memberships, [1] if membership_labels == 1 else membership_labels)
95+
# We now converted 2d likelihoods memberships into deterministic 1d membership, set flag to true
96+
is_memberships_1d = True
97+
98+
# Probabilistic version
99+
if not is_memberships_1d or bootstrap_results is not None:
100+
if membership_labels == 1:
101+
membership_labels = [1]
102+
103+
if bootstrap_results is None:
104+
bootstrap_results = get_bootstrap_results(predictions, memberships, surrogates,
105+
membership_labels, labels)
106+
107+
# Output df
70108
df = pd.DataFrame(columns=["Metric", "Value", "Ideal Value", "Lower Bound", "Upper Bound"])
109+
110+
fairness_funcs = inspect.getmembers(BinaryFairnessMetrics, predicate=inspect.isclass)[:-1]
71111
for fairness_func in fairness_funcs:
72112

113+
# Get metric
73114
name = fairness_func[0]
74115
class_ = getattr(BinaryFairnessMetrics, name) # grab a class which is a property of BinaryFairnessMetrics
75-
instance = class_() # dynamically instantiate such class
116+
metric = class_() # dynamically instantiate such class
76117

77-
if name in ["DisparateImpact", "StatisticalParity"]:
78-
score = instance.get_score(predictions, is_member, membership_label)
79-
elif name in ["GeneralizedEntropyIndex", "TheilIndex"]:
80-
score = instance.get_score(labels, predictions)
81-
else:
82-
score = instance.get_score(labels, predictions, is_member, membership_label)
118+
# Get score
119+
score = BinaryFairnessMetrics._get_score_logic(metric, name,
120+
labels, predictions, memberships, surrogates,
121+
membership_labels, bootstrap_results)
83122

84-
if score is None:
85-
score = np.nan
86-
score = np.round(score, 3)
87-
df = pd.concat([df, pd.DataFrame(
88-
[[instance.name, score, instance.ideal_value, instance.lower_bound, instance.upper_bound]],
89-
columns=df.columns)], axis=0, ignore_index=True)
123+
# Add score
124+
df = pd.concat([df,
125+
pd.DataFrame([[metric.name, score, metric.ideal_value,
126+
metric.lower_bound, metric.upper_bound]], columns=df.columns)],
127+
axis=0, ignore_index=True)
90128

91129
df = df.set_index("Metric")
92130

93131
return df
94132

133+
@staticmethod
134+
def _get_score_logic(metric, name,
135+
labels, predictions,
136+
memberships, surrogates,
137+
membership_labels, bootstrap_results):
138+
139+
# Standard deterministic calculation
140+
if bootstrap_results is None:
141+
if name in ["DisparateImpact", "StatisticalParity"]:
142+
score = metric.get_score(predictions, memberships, membership_labels)
143+
elif name in ["GeneralizedEntropyIndex", "TheilIndex"]:
144+
score = metric.get_score(labels, predictions)
145+
else:
146+
score = metric.get_score(labels, predictions, memberships, membership_labels)
147+
else:
148+
if name == "StatisticalParity":
149+
score = metric.get_score(predictions, memberships, surrogates, membership_labels, bootstrap_results)
150+
elif name in ["AverageOdds", "EqualOpportunity", "FNRDifference", "PredictiveEquality"]:
151+
score = metric.get_score(labels, predictions, memberships, surrogates,
152+
membership_labels, bootstrap_results)
153+
else:
154+
score = None
155+
156+
# pretty score
157+
score = np.nan if score is None else np.round(score, 3)
158+
159+
return score
160+
95161

96162
class MultiClassFairnessMetrics(NamedTuple):
97163
"""

0 commit comments

Comments
 (0)