diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b4ac4..031450d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to Fair Code are documented here. --- +## [1.2.1] - 6 Jul 2026 +### Added +- Explainer: Predictive Parity - `predictive-parity.md` created (contributed by @propcgamer20-png), added to `index.html`, `README.md`, and `CONTRIBUTING.md` + - Full explainer covering predictive parity as a sufficiency metric: Positive Predictive Value equal across groups, and why that is a fundamentally different fairness check than error-rate parity + - Comparison table across Demographic Parity, Equalized Odds, and Predictive Parity by what each conditions on + - Real-world proof anchored to Audit 01 (COMPAS): the 2016 ProPublica vs Northpointe dispute, where ProPublica's error-rate reading found a roughly 2x false-positive gap for Black defendants while Northpointe's predictive-parity reading found PPV close across race, and Chouldechova's proof for why both readings can be correct at once + - Detection code: `predictive_parity_gap()` and `base_rate_gap()` - pandas group-wise PPV and base-rate computation, paired to surface the Chouldechova trade-off signature + - Four numbered limitations (impossibility with Equalized Odds under unequal base rates, uneven harm despite equal PPV, small-subgroup PPV noise, threshold-tuning blind spot), cross-links to equalized-odds, demographic-parity, disparate-impact, and fairness-metric-conflicts, and three further reading citations (Angwin et al. 2016, Chouldechova 2017, Kleinberg et al. 2017) + - Nav dropdown (desktop + mobile), ticker strips, and roadmap updated on website +### Changed +- `README.md`: `predictive-parity.md` added to explainers table, repository structure tree, and What's Next checklist +- `CONTRIBUTING.md`: `predictive-parity.md` added to existing explainers table + +--- + ## [1.2.0] - "Open Tools & Causal Foundations" - 30 Jun 2026 First release since **v1.1.0** (9 Jun 2026). The headline is the **Open Dataset Profiler** - Fair Code's first interactive, bring-your-own-data tool, turning the project from a showcase of six fixed audits into something visitors run on their own CSVs. This release also bundles the six explainers shipped between v1.1.0 and now, which deepen the causal and statistical foundations behind the audits. @@ -286,4 +301,4 @@ First release since **v1.1.0** (9 Jun 2026). The headline is the **Open Dataset - `fair.py`, `unfair.py`, dataset, proof images - Interactive website (`index.html`) deployed at fair-code-five.vercel.app - Light mode, mobile-responsive layout -- README with project overview, results table, and methodology \ No newline at end of file +- README with project overview, results table, and methodology diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 407c205..e3b796f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -273,6 +273,7 @@ Explainers live in `explainers/` and should make one fairness concept easy to un | `distribution-shift.md` | Why a model that passes a fairness audit can become biased again as the population it serves changes | | `ai-objectivity-myth.md` | Why "it's just math" isn't a defense - models trained on biased history reproduce that bias | | `confounding-variable.md` | How a hidden third variable creates spurious correlations that persist after protected-attribute removal - and how to detect and adjust for it | +| `predictive-parity.md` | Why an equally trustworthy positive prediction across groups can still hide an unequal false-positive burden | ### A good explainer should include @@ -412,4 +413,4 @@ Include in the PR description: --- -All datasets used in this project are publicly available. Fair Code is for educational and awareness purposes. \ No newline at end of file +All datasets used in this project are publicly available. Fair Code is for educational and awareness purposes. diff --git a/README.md b/README.md index 7cbf7b2..dad28f8 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,8 @@ Fair-Code/ │ ├── how-ai-detects-patterns.md │ ├── distribution-shift.md │ ├── ai-objectivity-myth.md -│ └── confounding-variable.md +│ ├── confounding-variable.md +│ └── predictive-parity.md │ ├── CHANGELOG.md ├── CITATION.cff @@ -567,6 +568,7 @@ features = [ | [What Is Distribution Shift?](explainers/distribution-shift.md) | Why a model that passes a bias audit at deployment can become biased again as the population it serves changes | | [The Biggest Myth About AI Objectivity](explainers/ai-objectivity-myth.md) | Why "it's just math" isn't a defense - models trained on biased history produce biased outcomes, and the math just makes them harder to challenge | | [What Is a Confounding Variable?](explainers/confounding-variable.md) | How a hidden third variable creates spurious correlations between a feature and an outcome - and why removing the protected attribute leaves the bias intact until the confounder is removed too | +| [What Is Predictive Parity?](explainers/predictive-parity.md) | Why the ProPublica vs Northpointe COMPAS dispute was really two correct fairness checks that cannot both hold when base rates differ | --- @@ -715,6 +717,7 @@ directly. Run the tests with `pytest tests/`. - [x] Explainer: What Is Distribution Shift - [x] Explainer: The Biggest Myth About AI Objectivity - [x] Explainer: What Is a Confounding Variable? +- [x] Explainer: What Is Predictive Parity? - [ ] Explainer: Why Accuracy Is Not Enough in Healthcare AI - [ ] Explainer: False Positives and False Negatives in Medical Risk Models - [ ] Facial recognition accuracy gaps (MIT Gender Shades methodology) @@ -771,4 +774,4 @@ Data. Code. Accountability. One post at a time. --- -*All datasets used in this project are publicly available. Fair Code is for educational and awareness purposes.* \ No newline at end of file +*All datasets used in this project are publicly available. Fair Code is for educational and awareness purposes.* diff --git a/assets/explainers-data.js b/assets/explainers-data.js index fba68b7..2bcf359 100644 --- a/assets/explainers-data.js +++ b/assets/explainers-data.js @@ -160,4 +160,11 @@ window.FAIR_CODE_EXPLAINERS = [ summary: 'Learn how a third variable that independently causes both a feature and an outcome creates spurious correlations that survive protected-attribute removal. See why COMPAS stayed biased after race was dropped - until the confounder was removed too.', tags: ['detection', 'data'], }, + { + slug: 'predictive-parity', + title: 'Predictive Parity', + subtitle: 'Equally trustworthy is not the same as equally fair', + summary: 'Learn why Positive Predictive Value equal across groups is a real fairness property, and why the 2016 ProPublica vs Northpointe COMPAS dispute shows it can hold while one group absorbs a much higher false-positive rate.', + tags: ['metrics', 'detection'], + }, ]; diff --git a/explainers/predictive-parity.md b/explainers/predictive-parity.md new file mode 100644 index 0000000..92558ae --- /dev/null +++ b/explainers/predictive-parity.md @@ -0,0 +1,146 @@ +# What Is Predictive Parity? + +> *COMPAS's defenders and its critics ran fairness checks on the same defendants and reached opposite verdicts - because predictive parity and equalized odds are both legitimate, and when base rates differ, they mathematically cannot both hold.* + +## The One-Sentence Definition + +**Predictive parity** is satisfied when the Positive Predictive Value, the share of positively-flagged cases that are actually positive, is equal across groups, so a "high risk" label carries the same real-world meaning no matter who receives it. + +## Why It Matters + +Algorithmic risk scores decide who gets held before trial, whose insurance claim gets flagged for review, and whose loan application gets extra scrutiny. Over 1M+ people are assessed by COMPAS-style recidivism tools every year, and 46 US states have used one in criminal sentencing. When a vendor says a "high risk" flag is trustworthy, predictive parity is usually the claim being made: given the flag, the predicted outcome holds up at the same rate for every group. + +The non-obvious part is that "trustworthy for everyone" is not the same fairness property as "fair to everyone." A score can satisfy predictive parity perfectly and still send one group to jail on false pretenses far more often than another. Which group bears that cost depends on the false positive rate, a completely different metric, and the two cannot both be balanced unless the underlying base rates already match. + +## Predictive Parity vs Equalized Odds + +Predictive parity is a **sufficiency** metric: it conditions on the *prediction* and asks whether that prediction means the same thing across groups. Equalized Odds (see [explainers/equalized-odds.md](equalized-odds.md)) is a **separation** metric: it conditions on the *true outcome* and asks whether error rates match. + +| Metric | Conditions on | Question it asks | +|---|---|---| +| Demographic Parity | nothing (prior) | Are positive predictions handed out at the same rate? | +| Equalized Odds | true label | Given the actual outcome, are error rates equal? | +| Predictive Parity | predicted label | Given a positive prediction, is it equally trustworthy? | + +``` +PPV(group) = TP(group) / (TP(group) + FP(group)) + +Predictive Parity holds when: PPV(group a) ≈ PPV(group b) +``` + +Chouldechova (2017) proved the mechanism algebraically: if predictive parity holds but the base rate of the outcome differs between two groups, the false positive rate and false negative rate cannot be equal across those same groups. It is not a modeling flaw. It is an identity that follows directly from the definitions once base rates diverge. + +## Concrete Example: COMPAS - Audit 01 + +The [COMPAS](../COMPAS/) audit in this repo uses the same ProPublica recidivism dataset at the center of the 2016 "Machine Bias" investigation, where race is the protected attribute and the target is two-year re-offense. + +ProPublica and Northpointe (COMPAS's vendor) checked different fairness criteria on that dataset and both were internally correct. ProPublica's reading, an error-rate check, found the false positive rate for Black defendants was roughly double that of white defendants: Black defendants who did not reoffend were flagged high-risk far more often. Northpointe's reading, a predictive-parity check, found PPV was close across race: a "high risk" label corresponded to actual reoffending at a similar rate for both groups. Northpointe presented that second result as proof the tool was not biased. + +```python +# Approximating each side's check on a COMPAS-style dataframe +compas["flagged_high_risk"] = (compas["decile_score"] >= 7).astype(int) + +ppv_by_race, gap = predictive_parity_gap( + compas, y_true="two_year_recid", y_pred="flagged_high_risk", group_col="race" +) +``` + +Both readings are correct and both cannot hold simultaneously once the two racial groups in the dataset have different recidivism base rates, which they do. Satisfying predictive parity here did nothing to close the false-positive gap ProPublica measured; the two metrics were trading off against each other the entire time, exactly as Chouldechova's proof predicts. + +## Detection Code + +Compute Positive Predictive Value per group, then pair it with a base-rate check, since a small PPV gap next to a large base-rate gap is the signature Chouldechova identified. + +```python +import pandas as pd + + +def predictive_parity_gap(df: pd.DataFrame, y_true: str, y_pred: str, group_col: str): + """ + Computes Positive Predictive Value (PPV) per group and the resulting + predictive-parity gap. + + Parameters: + df: dataframe containing predictions and outcomes + y_true: column name of the ground-truth binary label + y_pred: column name of the model's binary prediction + group_col: column name of the protected attribute + + Returns: + (dict of PPV by group, float gap between the max and min PPV) + """ + ppv_by_group = {} + for group, sub in df.groupby(group_col): + predicted_positive = sub[sub[y_pred] == 1] + if len(predicted_positive) == 0: + ppv_by_group[group] = float("nan") + continue + true_positives = (predicted_positive[y_true] == 1).sum() + ppv_by_group[group] = true_positives / len(predicted_positive) + + gap = max(ppv_by_group.values()) - min(ppv_by_group.values()) + + print("PPV by group:") + for g, v in ppv_by_group.items(): + print(f" {g}: {v:.2%}") + print(f"Predictive Parity Gap: {gap:.2%}") + + return ppv_by_group, gap + + +def base_rate_gap(df: pd.DataFrame, y_true: str, group_col: str): + """ + Computes the true prevalence of the positive outcome per group. + A large gap here next to a small PPV gap indicates the Chouldechova + trade-off is active: predictive parity is being bought at the cost + of unequal error rates. + """ + rates = df.groupby(group_col)[y_true].mean() + print("Base rate (true prevalence) by group:") + print(rates) + return rates.max() - rates.min() + + +# Usage example +# predictive_parity_gap(compas_df, y_true="two_year_recid", y_pred="flagged_high_risk", group_col="race") +# base_rate_gap(compas_df, y_true="two_year_recid", group_col="race") +``` + +## Limitations and Trade-offs + +### 1. It cannot be satisfied alongside Equalized Odds when base rates differ + +This is not a modeling choice, it is Chouldechova's impossibility result. If your domain has meaningfully different base rates across groups, you have to pick which metric to optimize for and be explicit about why. + +### 2. A close PPV does not mean the harm is evenly distributed + +As the COMPAS case shows, predictive parity can hold while one group absorbs a much larger false-positive rate. Reporting PPV alone, without the accompanying false-positive and false-negative rates, can make a system look fairer than the people affected by it actually experience. + +### 3. Small subgroups produce noisy PPV estimates + +PPV is computed only over the positively-predicted subset. If a group is small or rarely flagged positive, the denominator shrinks and the estimate becomes unstable. A large-looking gap in a small subgroup may be sampling noise rather than a real disparity, and a small gap can hide real disparity in a subgroup too small to measure reliably. + +### 4. It says nothing about where the threshold is set + +Predictive parity is a property of the decision threshold that produced the predictions, not a property of the underlying score. Moving the threshold up or down can shift PPV without changing whether the underlying risk score itself is fair, so a system can be re-tuned to pass a predictive-parity check without addressing the base-rate disparity driving it. + +## Related Concepts + +* [What is Equalized Odds?](equalized-odds.md) - the error-rate metric predictive parity trades off against once base rates diverge. +* [What is Demographic Parity?](demographic-parity.md) - the simpler, prior-only metric that ignores ground truth entirely, unlike predictive parity's reliance on it. +* [Why Fairness Metrics Conflict](fairness-metric-conflicts.md) - the full impossibility-theorem write-up that this explainer's COMPAS case is a worked instance of. +* [What is Disparate Impact?](disparate-impact.md) - a rate-based legal threshold that, like predictive parity, can be satisfied while a different fairness property is violated. + +## Related Projects in This Repo + +* [`COMPAS/`](../COMPAS/) - the audit built on the same ProPublica dataset behind the real-world predictive-parity dispute described above. + +## Further Reading + +* [Angwin, J. et al. (2016): Machine Bias](https://www.propublica.org/article/machine-bias-risk-assessments-in-criminal-sentencing) - the original ProPublica investigation that surfaced the COMPAS false-positive disparity. +* [Chouldechova, A. (2017): Fair Prediction with Disparate Impact](https://arxiv.org/abs/1610.07524) - the algebraic proof that predictive parity and error-rate balance conflict when base rates differ. +* Kleinberg, J., Mullainathan, S., Raghavan, M. (2017): Inherent Trade-Offs in the Fair Determination of Risk Scores, ITCS 2017 - an independent impossibility result reaching the same conclusion via a different route. + +--- + +*Part of [The Fair Code Project](https://instagram.com/thefaircodeproject) - exposing and fixing algorithmic bias with real data and open code.* diff --git a/index.html b/index.html index 31ef958..e1e9b1d 100644 --- a/index.html +++ b/index.html @@ -2438,24 +2438,31 @@

The roadmap

-
+
-
Facial Recognition Gaps
-
MIT Gender Shades methodology · coming soon
+
Explainer: What Is Predictive Parity?
+
Sufficiency vs separation · PPV equality · COMPAS ProPublica vs Northpointe dispute · Chouldechova impossibility proof · base-rate trade-off
+
+
+
+
+
+
Fairness Dashboard
+
Interactive web app for live bias analysis
-
HMDA Mortgage Lending
-
Federal lending data · racial bias in loan approvals
+
Facial Recognition Gaps
+
MIT Gender Shades methodology · coming soon
-
Fairness Dashboard
-
Interactive web app for live bias analysis
+
HMDA Mortgage Lending
+
Federal lending data · racial bias in loan approvals
@@ -3035,4 +3042,4 @@

The roadmap

- \ No newline at end of file +