This document provides detailed mathematical specifications for all fairness metrics implemented in ExFairness.
-
$Y$ : True label -
$\hat{Y}$ : Predicted label -
$A$ : Sensitive attribute (e.g., race, gender) -
$X$ : Feature vector -
$P(\cdot)$ : Probability -
$E[\cdot]$ : Expectation
Definition: The probability of a positive prediction should be equal across groups.
Disparity Measure:
When to Use:
- When equal representation in positive outcomes is required
- Advertising, content recommendation
- When base rates can differ between groups
Limitations:
- Ignores base rate differences in actual outcomes
- May conflict with accuracy if base rates differ
- Can be satisfied by a random classifier
Implementation:
def demographic_parity(predictions, sensitive_attr, opts \\ []) do
threshold = Keyword.get(opts, :threshold, 0.1)
group_a_mask = Nx.equal(sensitive_attr, 0)
group_b_mask = Nx.equal(sensitive_attr, 1)
rate_a = positive_rate(predictions, group_a_mask)
rate_b = positive_rate(predictions, group_b_mask)
disparity = Nx.abs(Nx.subtract(rate_a, rate_b)) |> Nx.to_number()
%{
group_a_rate: Nx.to_number(rate_a),
group_b_rate: Nx.to_number(rate_b),
disparity: disparity,
passes: disparity <= threshold,
threshold: threshold
}
endDefinition: True positive and false positive rates should be equal across groups.
Disparity Measures:
When to Use:
- When both false positives and false negatives matter
- Criminal justice (both wrongful conviction and wrongful acquittal are serious)
- Medical diagnosis (both missed diagnoses and false alarms matter)
Limitations:
- Requires ground truth labels
- Can be impossible to achieve with demographic parity when base rates differ
- May reduce overall accuracy
Implementation:
def equalized_odds(predictions, labels, sensitive_attr, opts \\ []) do
threshold = Keyword.get(opts, :threshold, 0.1)
group_a_mask = Nx.equal(sensitive_attr, 0)
group_b_mask = Nx.equal(sensitive_attr, 1)
# Group A
tpr_a = true_positive_rate(predictions, labels, group_a_mask)
fpr_a = false_positive_rate(predictions, labels, group_a_mask)
# Group B
tpr_b = true_positive_rate(predictions, labels, group_b_mask)
fpr_b = false_positive_rate(predictions, labels, group_b_mask)
tpr_disparity = Nx.abs(Nx.subtract(tpr_a, tpr_b)) |> Nx.to_number()
fpr_disparity = Nx.abs(Nx.subtract(fpr_a, fpr_b)) |> Nx.to_number()
%{
group_a_tpr: Nx.to_number(tpr_a),
group_b_tpr: Nx.to_number(tpr_b),
group_a_fpr: Nx.to_number(fpr_a),
group_b_fpr: Nx.to_number(fpr_b),
tpr_disparity: tpr_disparity,
fpr_disparity: fpr_disparity,
passes: tpr_disparity <= threshold and fpr_disparity <= threshold
}
endDefinition: True positive rate (recall) should be equal across groups.
Disparity Measure:
When to Use:
- When the cost of false negatives varies by group
- Hiring (missing qualified candidates)
- College admissions
- Opportunity allocation
Limitations:
- Only considers true positive rate, ignores false positive rate
- Requires ground truth labels
- May allow different false positive rates
Implementation:
def equal_opportunity(predictions, labels, sensitive_attr, opts \\ []) do
threshold = Keyword.get(opts, :threshold, 0.1)
group_a_mask = Nx.equal(sensitive_attr, 0)
group_b_mask = Nx.equal(sensitive_attr, 1)
tpr_a = true_positive_rate(predictions, labels, group_a_mask)
tpr_b = true_positive_rate(predictions, labels, group_b_mask)
disparity = Nx.abs(Nx.subtract(tpr_a, tpr_b)) |> Nx.to_number()
%{
group_a_tpr: Nx.to_number(tpr_a),
group_b_tpr: Nx.to_number(tpr_b),
disparity: disparity,
passes: disparity <= threshold,
interpretation: interpret_equal_opportunity(disparity, threshold)
}
endDefinition: Positive predictive value (precision) should be equal across groups.
Disparity Measure:
When to Use:
- When the meaning of a positive prediction should be consistent
- Risk assessment tools
- Credit scoring
Limitations:
- Can be incompatible with equalized odds when base rates differ
- Requires ground truth labels
- May allow different selection rates
Definition: For any predicted probability, actual outcomes should be equal across groups.
where
Disparity Measure (per bin):
When to Use:
- When probability estimates must be interpretable
- Medical risk prediction
- Weather forecasting
- Any application where probabilities guide decisions
Implementation:
def calibration(probabilities, labels, sensitive_attr, opts \\ []) do
bins = Keyword.get(opts, :bins, 10)
threshold = Keyword.get(opts, :threshold, 0.1)
group_a_mask = Nx.equal(sensitive_attr, 0)
group_b_mask = Nx.equal(sensitive_attr, 1)
calibration_a = compute_calibration_curve(probabilities, labels, group_a_mask, bins)
calibration_b = compute_calibration_curve(probabilities, labels, group_b_mask, bins)
disparities = Enum.zip(calibration_a, calibration_b)
|> Enum.map(fn {a, b} -> abs(a - b) end)
max_disparity = Enum.max(disparities)
%{
calibration_a: calibration_a,
calibration_b: calibration_b,
disparities: disparities,
max_disparity: max_disparity,
passes: max_disparity <= threshold
}
endDefinition: Similar individuals should receive similar predictions.
where
Measurement:
For a set of similar pairs
When to Use:
- When individual treatment is important
- Personalized recommendations
- Custom pricing
Challenges:
- Requires defining similarity metric
- Computationally expensive for large datasets
- Similarity metric may be domain-specific
Definition: A prediction is counterfactually fair if it is the same in the actual world and in a counterfactual world where the sensitive attribute is different.
When to Use:
- When causal understanding is important
- Legal compliance (disparate treatment)
- High-stakes decisions
Challenges:
- Requires causal graph
- Unobserved confounders problematic
- Computationally intensive
Definition: The selection rate for the protected group should be at least 80% of the selection rate for the reference group.
Legal Context: Used by EEOC in employment discrimination cases.
Implementation:
def disparate_impact(predictions, sensitive_attr) do
group_a_rate = positive_rate(predictions, Nx.equal(sensitive_attr, 0))
group_b_rate = positive_rate(predictions, Nx.equal(sensitive_attr, 1))
ratio = Nx.divide(group_b_rate, group_a_rate) |> Nx.to_number()
%{
ratio: ratio,
passes_80_percent_rule: ratio >= 0.8,
interpretation: interpret_disparate_impact(ratio)
}
endStatement: If base rates differ between groups ($P(Y=1|A=0) \neq P(Y=1|A=1)$), it is impossible to simultaneously satisfy:
- Predictive parity
- Equal false positive rates
- Equal false negative rates
Implication: Must choose which fairness definition to prioritize.
Statement: Except in degenerate cases, it is impossible to simultaneously satisfy:
- Calibration
- Balance for the positive class (equal TPR)
- Balance for the negative class (equal TNR)
Implication: Trade-offs are necessary when base rates differ.
graph TD
A[Start] --> B{Ground truth available?}
B -->|No| C[Demographic Parity]
B -->|Yes| D{What matters most?}
D -->|Equal opportunity| E[Equal Opportunity]
D -->|Both error types| F[Equalized Odds]
D -->|Prediction meaning| G[Predictive Parity]
D -->|Probability interpretation| H[Calibration]
D -->|Individual treatment| I[Individual Fairness]
D -->|Causal reasoning| J[Counterfactual Fairness]
C --> K{Legal compliance?}
K -->|Yes| L[Add 80% rule check]
K -->|No| M[Use demographic parity]
E --> N[Check impossibility theorems]
F --> N
G --> N
H --> N
For multiple sensitive attributes
Additive Intersectionality: Measure fairness for each attribute independently.
Multiplicative Intersectionality: Measure fairness for all combinations of attributes.
Example: Gender × Race creates 4 groups: (Male, White), (Male, Black), (Female, White), (Female, Black)
All metrics should include confidence intervals:
Bootstrap Method:
def demographic_parity_with_ci(predictions, sensitive_attr, opts \\ []) do
result = demographic_parity(predictions, sensitive_attr, opts)
# Bootstrap CI
bootstrap_samples = Keyword.get(opts, :bootstrap_samples, 1000)
ci_level = Keyword.get(opts, :confidence_level, 0.95)
bootstrap_disparities = bootstrap(predictions, sensitive_attr, bootstrap_samples, fn p, s ->
demographic_parity(p, s).disparity
end)
ci = percentile_ci(bootstrap_disparities, ci_level)
Map.put(result, :confidence_interval, ci)
end- Hardt, M., Price, E., & Srebro, N. (2016). Equality of opportunity in supervised learning. NeurIPS.
- Chouldechova, A. (2017). Fair prediction with disparate impact. Big Data, 5(2), 153-163.
- Kleinberg, J., Mullainathan, S., & Raghavan, M. (2016). Inherent trade-offs in the fair determination of risk scores. ITCS.
- Dwork, C., Hardt, M., Pitassi, T., Reingold, O., & Zemel, R. (2012). Fairness through awareness. ITCS.
- Kusner, M. J., Loftus, J., Russell, C., & Silva, R. (2017). Counterfactual fairness. NeurIPS.