Skip to content

Commit 2b21a49

Browse files
authored
Merge pull request #46 from adolfo-ab/fairness-property-testing
test: additional unit tests for SPD and DIR metrics
2 parents 17bc25c + 844bad4 commit 2b21a49

File tree

6 files changed

+1980
-1442
lines changed

6 files changed

+1980
-1442
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ dependencies = [
1717
"h5py>=3.13.0,<4",
1818
"scikit-learn",
1919
"aif360",
20+
"hypothesis>=6.136.2",
21+
"pytest>=8.4.1",
2022
]
2123

2224
[project.optional-dependencies]

src/core/metrics/fairness/fairness_metrics_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
import numpy as np
33
from sklearn.metrics import confusion_matrix
44

5+
56
def filter_rows_by_inputs(data: np.ndarray, filter_func: Callable[[np.ndarray], bool]):
67
return data[np.apply_along_axis(filter_func, 1, data)]
78

9+
810
def calculate_confusion_matrix(test: np.array, truth: np.array, positive_class: int) -> dict:
911
# cast test and truth to int
1012
test = test.astype(int)
@@ -16,3 +18,14 @@ def calculate_confusion_matrix(test: np.array, truth: np.array, positive_class:
1618
fp = cm[1, 0]
1719
tn = cm[1, 1]
1820
return {"tp": tp, "tn": tn, "fp": fp, "fn": fn}
21+
22+
23+
def validate_fairness_groups(privileged: np.ndarray, unprivileged: np.ndarray) -> None:
24+
empty_groups = []
25+
if len(privileged) == 0:
26+
empty_groups.append("privileged")
27+
if len(unprivileged) == 0:
28+
empty_groups.append("unprivileged")
29+
30+
if empty_groups:
31+
raise ValueError(f"Arrays cannot be empty for the following groups: {', '.join(empty_groups)}")

src/core/metrics/fairness/group/disparate_impact_ratio.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44
import numpy as np
55
from sklearn.base import ClassifierMixin
66

7+
from src.core.metrics.fairness.fairness_metrics_utils import validate_fairness_groups
8+
9+
710
class DisparateImpactRatio:
811
"""
912
Calculate disparate impact ratio (DIR).
1013
"""
14+
1115
@staticmethod
1216
def calculate_model(
1317
samples: np.ndarray,
1418
model: ClassifierMixin,
1519
privilege_columns: List[int],
1620
privilege_values: List[int],
17-
favorable_output: np.ndarray
18-
) -> float:
21+
favorable_output: np.ndarray,
22+
) -> float:
1923
"""
2024
Calculate disparate impact ratio (DIR) for model outputs.
2125
:param samples a NumPy array of inputs to be used for testing fairness
@@ -34,17 +38,17 @@ def calculate_model(
3438

3539
@staticmethod
3640
def calculate(
37-
privileged: Union[int, np.ndarray],
38-
unprivileged: Union[int, np.ndarray],
39-
favorable_output: int
40-
) -> float:
41+
privileged: Union[int, np.ndarray], unprivileged: Union[int, np.ndarray], favorable_output: int
42+
) -> float:
4143
"""
4244
Calculate disparate impact ratio (DIR) when the labels are pre-calculated.
4345
:param privileged a NumPy array with the privileged groups
4446
:param unprivileged a NumPy array with the unprivileged groups
4547
:param favorableOutput an output that is considered favorable / desirable
4648
return DIR, between 0 and 1
4749
"""
50+
validate_fairness_groups(privileged=privileged, unprivileged=unprivileged)
51+
4852
probability_privileged = np.sum(privileged[:, -1] == favorable_output) / len(privileged)
4953
probability_unprivileged = np.sum(unprivileged[:, -1] == favorable_output) / len(unprivileged)
5054
return probability_unprivileged / probability_privileged

src/core/metrics/fairness/group/group_statistical_parity_difference.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44
import numpy as np
55
from sklearn.base import ClassifierMixin
66

7+
from src.core.metrics.fairness.fairness_metrics_utils import validate_fairness_groups
8+
9+
710
class GroupStatisticalParityDifference:
811
"""
912
Calculate group statistical parity difference (SPD).
1013
"""
14+
1115
@staticmethod
1216
def calculate_model(
1317
samples: np.ndarray,
1418
model: ClassifierMixin,
1519
privilege_columns: List[int],
1620
privilege_values: List[int],
1721
favorable_output,
18-
) -> float:
22+
) -> float:
1923
"""
2024
Calculate group statistical parity difference (SPD) for model outputs.
2125
:param samples a NumPy array of inputs to be used for testing fairness
@@ -37,14 +41,16 @@ def calculate(
3741
privileged,
3842
unprivileged,
3943
favorable_output,
40-
) -> float:
44+
) -> float:
4145
"""
4246
Calculate statistical/demographic parity difference (SPD) when the labels are pre-calculated.
4347
:param priviledged numPy array with the privileged groups
4448
:param unpriviledged numPy array with the unpriviledged groups
4549
:param favorableOutput an output that is considered favorable / desirable
46-
return SPD, between 0 and 1
50+
return SPD, between -1 and 1
4751
"""
52+
validate_fairness_groups(privileged=privileged, unprivileged=unprivileged)
53+
4854
probability_privileged = np.sum(privileged[:, -1] == favorable_output) / len(privileged)
4955
probability_unprivileged = np.sum(unprivileged[:, -1] == favorable_output) / len(unprivileged)
5056
return probability_unprivileged - probability_privileged

0 commit comments

Comments
 (0)