Jurity (ACM'24, ArXiv'24, LION'23, ICMLA'21) 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.
Jurity is developed by the Artificial Intelligence Center of Excellence at Fidelity Investments. Documentation is available at fidelity.github.io/jurity.
- Average Odds
- Disparate Impact
- Equal Opportunity
- False Negative Rate (FNR) Difference
- False Omission Rate (FOR) Difference
- Generalized Entropy Index
- Predictive Equality
- Statistical Parity
- Theil Index
- AUC: Area Under the Curve
- CTR: Click-through rate
- DR: Doubly robust estimation
- IPS: Inverse propensity scoring
- MAP@K: Mean Average Precision
- NDCG: Normalized discounted cumulative gain
- Precision@K
- Recall@K
- Inter-List Diversity@K
- Intra-List Diversity@K
# Import binary and multi-class fairness metrics
from jurity.fairness import BinaryFairnessMetrics, MultiClassFairnessMetrics
# Data
binary_predictions = [1, 1, 0, 1, 0, 0]
multi_class_predictions = ["a", "b", "c", "b", "a", "a"]
multi_class_multi_label_predictions = [["a", "b"], ["b", "c"], ["b"], ["a", "b"], ["c", "a"], ["c"]]
memberships = [0, 0, 0, 1, 1, 1]
classes = ["a", "b", "c"]
# Metrics (see also other available metrics)
metric = BinaryFairnessMetrics.StatisticalParity()
multi_metric = MultiClassFairnessMetrics.StatisticalParity(classes)
# Scores
print("Metric:", metric.description)
print("Lower Bound: ", metric.lower_bound)
print("Upper Bound: ", metric.upper_bound)
print("Ideal Value: ", metric.ideal_value)
print("Binary Fairness score: ", metric.get_score(binary_predictions, memberships))
print("Multi-class Fairness scores: ", multi_metric.get_scores(multi_class_predictions, memberships))
print("Multi-class multi-label Fairness scores: ", multi_metric.get_scores(multi_class_multi_label_predictions, memberships))
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.
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.
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.
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.
Jurity offers both options to address the case where membership data is missing. We provide an in-depth study and formal treatment in ACM'24 and LION'23. For the analogous scenario of the lack of ground truth data, see ArXiv'24 and ICMLA'21.
from jurity.fairness import BinaryFairnessMetrics
# Instead of 0/1 deterministic membership at individual level
# consider likelihoods of membership to protected classes for each sample
binary_predictions = [1, 1, 0, 1]
memberships = [[0.2, 0.8], [0.4, 0.6], [0.2, 0.8], [0.9, 0.1]]
# Metric
metric = BinaryFairnessMetrics.StatisticalParity()
print("Binary Fairness score: ", metric.get_score(binary_predictions, memberships))
# Surrogate membership: consider access to surrogate membership at the group level.
surrogates = [0, 2, 0, 1]
print("Binary Fairness score: ", metric.get_score(binary_predictions, memberships, surrogates))
# Import binary fairness and binary bias mitigation
from jurity.mitigation import BinaryMitigation
from jurity.fairness import BinaryFairnessMetrics
# Data
labels = [1, 1, 0, 1, 0, 0, 1, 0]
predictions = [0, 0, 0, 1, 1, 1, 1, 0]
likelihoods = [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.1]
is_member = [0, 0, 0, 0, 1, 1, 1, 1]
# Bias Mitigation
mitigation = BinaryMitigation.EqualizedOdds()
# Training: Learn mixing rates from the labeled data
mitigation.fit(labels, predictions, likelihoods, is_member)
# Testing: Mitigate bias in predictions
fair_predictions, fair_likelihoods = mitigation.transform(predictions, likelihoods, is_member)
# Scores: Fairness before and after
print("Fairness Metrics Before:", BinaryFairnessMetrics().get_all_scores(labels, predictions, is_member), '\n'+30*'-')
print("Fairness Metrics After:", BinaryFairnessMetrics().get_all_scores(labels, fair_predictions, is_member))
# Import recommenders metrics
from jurity.recommenders import BinaryRecoMetrics, RankingRecoMetrics, DiversityRecoMetrics
import pandas as pd
# Data
actual = pd.DataFrame({"user_id": [1, 2, 3, 4], "item_id": [1, 2, 0, 3], "clicks": [0, 1, 0, 0]})
predicted = pd.DataFrame({"user_id": [1, 2, 3, 4], "item_id": [1, 2, 2, 3], "clicks": [0.8, 0.7, 0.8, 0.7]})
item_features = pd.DataFrame({"item_id": [0, 1, 2, 3], "feature1": [1, 2, 2, 1], "feature2": [0.8, 0.7, 0.8, 0.7]})
# Metrics
auc = BinaryRecoMetrics.AUC(click_column="clicks")
ctr = BinaryRecoMetrics.CTR(click_column="clicks")
dr = BinaryRecoMetrics.CTR(click_column="clicks", estimation='dr')
ips = BinaryRecoMetrics.CTR(click_column="clicks", estimation='ips')
map_k = RankingRecoMetrics.MAP(click_column="clicks", k=2)
ncdg_k = RankingRecoMetrics.NDCG(click_column="clicks", k=3)
precision_k = RankingRecoMetrics.Precision(click_column="clicks", k=2)
recall_k = RankingRecoMetrics.Recall(click_column="clicks", k=2)
interlist_diversity_k = DiversityRecoMetrics.InterListDiversity(click_column="clicks", k=2)
intralist_diversity_k = DiversityRecoMetrics.IntraListDiversity(item_features, click_column="clicks", k=2)
# Scores
print("AUC:", auc.get_score(actual, predicted))
print("CTR:", ctr.get_score(actual, predicted))
print("Doubly Robust:", dr.get_score(actual, predicted))
print("IPS:", ips.get_score(actual, predicted))
print("MAP@K:", map_k.get_score(actual, predicted))
print("NCDG:", ncdg_k.get_score(actual, predicted))
print("Precision@K:", precision_k.get_score(actual, predicted))
print("Recall@K:", recall_k.get_score(actual, predicted))
print("Inter-List Diversity@K:", interlist_diversity_k.get_score(actual, predicted))
print("Intra-List Diversity@K:", intralist_diversity_k.get_score(actual, predicted))
# Import classification metrics
from jurity.classification import BinaryClassificationMetrics
# Data
labels = [1, 1, 0, 1, 0, 0, 1, 0]
predictions = [0, 0, 0, 1, 1, 1, 1, 0]
# Available: Accuracy, F1, Precision, Recall, and AUC
f1_score = BinaryClassificationMetrics.F1()
print('F1 score is', f1_score.get_score(predictions, labels))
Jurity requires Python 3.8+ and can be installed from PyPI using pip install jurity
or by building from source as shown in installation instructions.
If you use Jurity in a publication, please cite it as:
@article{10.1145/3700145,
author = {Kad\i{}o\u{g}lu, Serdar and Thielbar, Melinda},
title = {Surrogate Modeling to Address the Absence of Protected Membership Attributes in Fairness Evaluation},
year = {2024},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
url = {https://doi.org/10.1145/3700145},
doi = {10.1145/3700145},
journal = {ACM Trans. Evol. Learn. Optim.},
}
@article{DBLP:conf/lion/Melinda23,
author = {Melinda Thielbar, Serdar Kadioglu, Chenhui Zhang, Rick Pack, and Lukas Dannull},
title = {Surrogate Membership for Inferred Metrics in Fairness Evaluation},
booktitle = {The 17th Learning and Intelligent Optimization Conference (LION)},
publisher = {{LION}},
year = {2023}
}
@inproceedings{DBLP:conf/icmla/MichalskyK21,
author = {Filip Michalsk{\'{y}} and Serdar Kadioglu},
title = {Surrogate Ground Truth Generation to Enhance Binary Fairness Evaluation in Uplift Modeling},
booktitle = {20th {IEEE} International Conference on Machine Learning and Applications,
{ICMLA} 2021, Pasadena, CA, USA, December 13-16, 2021},
pages = {1654--1659},
publisher = {{IEEE}},
year = {2021},
url = {https://doi.org/10.1109/ICMLA52953.2021.00264},
doi = {10.1109/ICMLA52953.2021.00264},
}
Please submit bug reports and feature requests as Issues.
Jurity is licensed under the Apache License 2.0.