# Fairness Metrics Specifications
## Overview
This document provides detailed mathematical specifications for all fairness metrics implemented in ExFairness.
## Notation
- $Y$: True label
- $\hat{Y}$: Predicted label
- $A$: Sensitive attribute (e.g., race, gender)
- $X$: Feature vector
- $P(\cdot)$: Probability
- $E[\cdot]$: Expectation
## Group Fairness Metrics
### 1. Demographic Parity (Statistical Parity)
**Definition**: The probability of a positive prediction should be equal across groups.
$$P(\hat{Y} = 1 | A = 0) = P(\hat{Y} = 1 | A = 1)$$
**Disparity Measure**:
$$\Delta_{DP} = |P(\hat{Y} = 1 | A = 0) - P(\hat{Y} = 1 | A = 1)|$$
**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**:
```elixir
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
}
end
```
---
### 2. Equalized Odds
**Definition**: True positive and false positive rates should be equal across groups.
$$P(\hat{Y} = 1 | Y = 1, A = 0) = P(\hat{Y} = 1 | Y = 1, A = 1)$$
$$P(\hat{Y} = 1 | Y = 0, A = 0) = P(\hat{Y} = 1 | Y = 0, A = 1)$$
**Disparity Measures**:
$$\Delta_{TPR} = |TPR_{A=0} - TPR_{A=1}|$$
$$\Delta_{FPR} = |FPR_{A=0} - FPR_{A=1}|$$
**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**:
```elixir
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
}
end
```
---
### 3. Equal Opportunity
**Definition**: True positive rate (recall) should be equal across groups.
$$P(\hat{Y} = 1 | Y = 1, A = 0) = P(\hat{Y} = 1 | Y = 1, A = 1)$$
**Disparity Measure**:
$$\Delta_{EO} = |TPR_{A=0} - TPR_{A=1}|$$
**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**:
```elixir
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)
}
end
```
---
### 4. Predictive Parity (Outcome Test)
**Definition**: Positive predictive value (precision) should be equal across groups.
$$P(Y = 1 | \hat{Y} = 1, A = 0) = P(Y = 1 | \hat{Y} = 1, A = 1)$$
**Disparity Measure**:
$$\Delta_{PP} = |PPV_{A=0} - PPV_{A=1}|$$
**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
---
### 5. Calibration
**Definition**: For any predicted probability, actual outcomes should be equal across groups.
$$P(Y = 1 | S(X) = s, A = 0) = P(Y = 1 | S(X) = s, A = 1)$$
where $S(X)$ is the model's score function.
**Disparity Measure** (per bin):
$$\Delta_{Cal}(b) = |P(Y = 1 | S(X) \in bin_b, A = 0) - P(Y = 1 | S(X) \in bin_b, A = 1)|$$
**When to Use**:
- When probability estimates must be interpretable
- Medical risk prediction
- Weather forecasting
- Any application where probabilities guide decisions
**Implementation**:
```elixir
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
}
end
```
---
## Individual Fairness Metrics
### 6. Individual Fairness (Lipschitz Continuity)
**Definition**: Similar individuals should receive similar predictions.
$$d(\hat{Y}(x_1), \hat{Y}(x_2)) \leq L \cdot d(x_1, x_2)$$
where $L$ is the Lipschitz constant and $d$ is a distance metric.
**Measurement**:
For a set of similar pairs $(x_i, x_j)$:
$$\text{Fairness} = \frac{1}{|P|} \sum_{(i,j) \in P} \mathbb{1}[|f(x_i) - f(x_j)| \leq \epsilon]$$
**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
---
### 7. Counterfactual Fairness
**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.
$$P(\hat{Y}_{A \leftarrow a}(U) = y | X = x, A = a) = P(\hat{Y}_{A \leftarrow a'}(U) = y | X = x, A = a)$$
**When to Use**:
- When causal understanding is important
- Legal compliance (disparate treatment)
- High-stakes decisions
**Challenges**:
- Requires causal graph
- Unobserved confounders problematic
- Computationally intensive
---
## Disparate Impact Measures
### 80% Rule (4/5ths Rule)
**Definition**: The selection rate for the protected group should be at least 80% of the selection rate for the reference group.
$$\frac{P(\hat{Y} = 1 | A = 1)}{P(\hat{Y} = 1 | A = 0)} \geq 0.8$$
**Legal Context**: Used by EEOC in employment discrimination cases.
**Implementation**:
```elixir
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)
}
end
```
---
## Impossibility Theorems
### Chouldechova's Theorem
**Statement**: If base rates differ between groups ($P(Y=1|A=0) \neq P(Y=1|A=1)$), it is impossible to simultaneously satisfy:
1. Predictive parity
2. Equal false positive rates
3. Equal false negative rates
**Implication**: Must choose which fairness definition to prioritize.
### Kleinberg et al. Theorem
**Statement**: Except in degenerate cases, it is impossible to simultaneously satisfy:
1. Calibration
2. Balance for the positive class (equal TPR)
3. Balance for the negative class (equal TNR)
**Implication**: Trade-offs are necessary when base rates differ.
---
## Metric Selection Guide
```mermaid
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
```
## Intersectional Fairness
For multiple sensitive attributes $(A_1, A_2, ..., A_k)$, fairness metrics can be extended:
**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)
---
## Confidence Intervals
All metrics should include confidence intervals:
**Bootstrap Method**:
```elixir
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
```
---
## References
1. Hardt, M., Price, E., & Srebro, N. (2016). Equality of opportunity in supervised learning. *NeurIPS*.
2. Chouldechova, A. (2017). Fair prediction with disparate impact. *Big Data*, 5(2), 153-163.
3. Kleinberg, J., Mullainathan, S., & Raghavan, M. (2016). Inherent trade-offs in the fair determination of risk scores. *ITCS*.
4. Dwork, C., Hardt, M., Pitassi, T., Reingold, O., & Zemel, R. (2012). Fairness through awareness. *ITCS*.
5. Kusner, M. J., Loftus, J., Russell, C., & Silva, R. (2017). Counterfactual fairness. *NeurIPS*.