diff --git a/assets/explainers-data.js b/assets/explainers-data.js index 0da1dd91..73481b0a 100644 --- a/assets/explainers-data.js +++ b/assets/explainers-data.js @@ -613,10 +613,22 @@ window.FAIR_CODE_EXPLAINERS = [ "slug": "reject-option-classification", "title": "What Is Reject Option Classification?", "subtitle": "Flip the model's least-confident predictions toward the group history treated worst.", - "summary": "Learn how Reject Option Classification (Kamiran, Karim & Zhang, 2012) post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.", + "summary": "Learn how ROC classification post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.", "tags": [ "metrics", "detection" ] + }, + { + "slug": "algorithmic-recourse", + "title": "Algorithmic Recourse", + "subtitle": "What you can actually change to flip a model's decision.", + "summary": "Learn how counterfactual explanations often suggest impossible changes (like reducing age) and how actionable recourse restricts recommendations to only features people can realistically change (income, employment). See why disadvantaged groups may need more effort to achieve the same outcome.", + "tags": [ + "explainability", + "metrics", + "detection", + "healthcare" + ] } ]; diff --git a/assets/explainers-data.json b/assets/explainers-data.json index 18b6cee6..78e68ac3 100644 --- a/assets/explainers-data.json +++ b/assets/explainers-data.json @@ -419,12 +419,19 @@ "summary": "Learn how DP-SGD's gradient clipping and noise addition disproportionately degrade accuracy for minority subgroups, so adding a privacy guarantee to a bias-mitigation pipeline is not free. Illustrative example from Bagdasaryan, Poursaeed and Shmatikov (NeurIPS 2019), plus a runnable DP-SGD noise-injection toy; this repo trains no DP model, so no frozen numbers are quoted.", "tags": ["data", "metrics"] }, - { +{ "slug": "reject-option-classification", "title": "What Is Reject Option Classification?", "subtitle": "Flip the model's least-confident predictions toward the group history treated worst.", - "summary": "Learn how Reject Option Classification (Kamiran, Karim & Zhang, 2012) post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.", + "summary": "Learn how ROC classification post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.", "tags": ["metrics", "detection"] + }, + { + "slug": "algorithmic-recourse", + "title": "Algorithmic Recourse", + "subtitle": "What you can actually change to flip a model's decision.", + "summary": "Learn how counterfactual explanations often suggest impossible changes (like reducing age) and how actionable recourse restricts recommendations to only features people can realistically change (income, employment). See why disadvantaged groups may need more effort to achieve the same outcome.", + "tags": ["explainability", "metrics", "detection", "healthcare"] } ] diff --git a/assets/og-light/accuracy-equality.png b/assets/og-light/accuracy-equality.png index 14ae0fb6..20827e5e 100644 Binary files a/assets/og-light/accuracy-equality.png and b/assets/og-light/accuracy-equality.png differ diff --git a/assets/og-light/accuracy-not-enough-healthcare-ai.png b/assets/og-light/accuracy-not-enough-healthcare-ai.png index 2b9e077b..97d6a4a7 100644 Binary files a/assets/og-light/accuracy-not-enough-healthcare-ai.png and b/assets/og-light/accuracy-not-enough-healthcare-ai.png differ diff --git a/assets/og-light/ai-hallucinations.png b/assets/og-light/ai-hallucinations.png index 092cbd2e..f6025d12 100644 Binary files a/assets/og-light/ai-hallucinations.png and b/assets/og-light/ai-hallucinations.png differ diff --git a/assets/og-light/ai-objectivity-myth.png b/assets/og-light/ai-objectivity-myth.png index c1cce53b..f7c365c9 100644 Binary files a/assets/og-light/ai-objectivity-myth.png and b/assets/og-light/ai-objectivity-myth.png differ diff --git a/assets/og-light/algorithmic-recourse.png b/assets/og-light/algorithmic-recourse.png new file mode 100644 index 00000000..22294035 Binary files /dev/null and b/assets/og-light/algorithmic-recourse.png differ diff --git a/assets/og-light/automation-bias.png b/assets/og-light/automation-bias.png index e3da3735..8bbc6e1e 100644 Binary files a/assets/og-light/automation-bias.png and b/assets/og-light/automation-bias.png differ diff --git a/assets/og-light/base-rate-fallacy.png b/assets/og-light/base-rate-fallacy.png index 640b6cd0..06600952 100644 Binary files a/assets/og-light/base-rate-fallacy.png and b/assets/og-light/base-rate-fallacy.png differ diff --git a/assets/og-light/bias-variance-tradeoff.png b/assets/og-light/bias-variance-tradeoff.png index 1c65c743..0525d3e9 100644 Binary files a/assets/og-light/bias-variance-tradeoff.png and b/assets/og-light/bias-variance-tradeoff.png differ diff --git a/assets/og-light/bootstrap-confidence-intervals.png b/assets/og-light/bootstrap-confidence-intervals.png index f9e2655b..01742ddc 100644 Binary files a/assets/og-light/bootstrap-confidence-intervals.png and b/assets/og-light/bootstrap-confidence-intervals.png differ diff --git a/assets/og-light/calibration.png b/assets/og-light/calibration.png index 2f4d7607..8ce72852 100644 Binary files a/assets/og-light/calibration.png and b/assets/og-light/calibration.png differ diff --git a/assets/og-light/class-imbalance.png b/assets/og-light/class-imbalance.png index 87173ebd..49e6bb2f 100644 Binary files a/assets/og-light/class-imbalance.png and b/assets/og-light/class-imbalance.png differ diff --git a/assets/og-light/clinical-score-miscalibration.png b/assets/og-light/clinical-score-miscalibration.png index dc0932a1..98732bfc 100644 Binary files a/assets/og-light/clinical-score-miscalibration.png and b/assets/og-light/clinical-score-miscalibration.png differ diff --git a/assets/og-light/conditional-demographic-parity.png b/assets/og-light/conditional-demographic-parity.png index 85fe5979..5db3704a 100644 Binary files a/assets/og-light/conditional-demographic-parity.png and b/assets/og-light/conditional-demographic-parity.png differ diff --git a/assets/og-light/confounding-variable.png b/assets/og-light/confounding-variable.png index a2686ebd..85c18a0d 100644 Binary files a/assets/og-light/confounding-variable.png and b/assets/og-light/confounding-variable.png differ diff --git a/assets/og-light/confusion-matrix.png b/assets/og-light/confusion-matrix.png index 7054d621..319f225c 100644 Binary files a/assets/og-light/confusion-matrix.png and b/assets/og-light/confusion-matrix.png differ diff --git a/assets/og-light/counterfactual-explanation.png b/assets/og-light/counterfactual-explanation.png index df6b6dbc..2eb2dad5 100644 Binary files a/assets/og-light/counterfactual-explanation.png and b/assets/og-light/counterfactual-explanation.png differ diff --git a/assets/og-light/counterfactual-fairness.png b/assets/og-light/counterfactual-fairness.png index 4f76500c..adb75d5e 100644 Binary files a/assets/og-light/counterfactual-fairness.png and b/assets/og-light/counterfactual-fairness.png differ diff --git a/assets/og-light/data-leakage.png b/assets/og-light/data-leakage.png index d7571f92..a8c186fa 100644 Binary files a/assets/og-light/data-leakage.png and b/assets/og-light/data-leakage.png differ diff --git a/assets/og-light/demographic-parity.png b/assets/og-light/demographic-parity.png index 1019b5fd..0ec0a2b8 100644 Binary files a/assets/og-light/demographic-parity.png and b/assets/og-light/demographic-parity.png differ diff --git a/assets/og-light/differential-privacy.png b/assets/og-light/differential-privacy.png index 2c5654dc..b974f7f4 100644 Binary files a/assets/og-light/differential-privacy.png and b/assets/og-light/differential-privacy.png differ diff --git a/assets/og-light/disparate-impact.png b/assets/og-light/disparate-impact.png index fa55baf1..e75ed517 100644 Binary files a/assets/og-light/disparate-impact.png and b/assets/og-light/disparate-impact.png differ diff --git a/assets/og-light/disparate-treatment.png b/assets/og-light/disparate-treatment.png index 838e95ae..b7b81aa3 100644 Binary files a/assets/og-light/disparate-treatment.png and b/assets/og-light/disparate-treatment.png differ diff --git a/assets/og-light/distribution-shift.png b/assets/og-light/distribution-shift.png index b499e42c..cf1027b8 100644 Binary files a/assets/og-light/distribution-shift.png and b/assets/og-light/distribution-shift.png differ diff --git a/assets/og-light/equal-opportunity.png b/assets/og-light/equal-opportunity.png index 7a9e1dd7..79bbb025 100644 Binary files a/assets/og-light/equal-opportunity.png and b/assets/og-light/equal-opportunity.png differ diff --git a/assets/og-light/equalized-odds.png b/assets/og-light/equalized-odds.png index 9acde145..51b8bd76 100644 Binary files a/assets/og-light/equalized-odds.png and b/assets/og-light/equalized-odds.png differ diff --git a/assets/og-light/fairness-accuracy-tradeoff.png b/assets/og-light/fairness-accuracy-tradeoff.png index a5e0bb8b..dcc0b4b6 100644 Binary files a/assets/og-light/fairness-accuracy-tradeoff.png and b/assets/og-light/fairness-accuracy-tradeoff.png differ diff --git a/assets/og-light/fairness-metric-conflicts.png b/assets/og-light/fairness-metric-conflicts.png index 82bdfca4..c640e575 100644 Binary files a/assets/og-light/fairness-metric-conflicts.png and b/assets/og-light/fairness-metric-conflicts.png differ diff --git a/assets/og-light/fairness-through-unawareness.png b/assets/og-light/fairness-through-unawareness.png index 52c61bfd..4d34e3a7 100644 Binary files a/assets/og-light/fairness-through-unawareness.png and b/assets/og-light/fairness-through-unawareness.png differ diff --git a/assets/og-light/false-positives-vs-false-negatives.png b/assets/og-light/false-positives-vs-false-negatives.png index 80e82f15..a9e54ab4 100644 Binary files a/assets/og-light/false-positives-vs-false-negatives.png and b/assets/og-light/false-positives-vs-false-negatives.png differ diff --git a/assets/og-light/feedback-loop-bias.png b/assets/og-light/feedback-loop-bias.png index 8559241a..7b4499df 100644 Binary files a/assets/og-light/feedback-loop-bias.png and b/assets/og-light/feedback-loop-bias.png differ diff --git a/assets/og-light/home.png b/assets/og-light/home.png index 282e5934..971bdf9d 100644 Binary files a/assets/og-light/home.png and b/assets/og-light/home.png differ diff --git a/assets/og-light/how-ai-detects-patterns.png b/assets/og-light/how-ai-detects-patterns.png index 95c48299..720cb033 100644 Binary files a/assets/og-light/how-ai-detects-patterns.png and b/assets/og-light/how-ai-detects-patterns.png differ diff --git a/assets/og-light/individual-fairness.png b/assets/og-light/individual-fairness.png index 250c4a09..23e1eb68 100644 Binary files a/assets/og-light/individual-fairness.png and b/assets/og-light/individual-fairness.png differ diff --git a/assets/og-light/intersectional-bias.png b/assets/og-light/intersectional-bias.png index 63bb6e2c..daedf93e 100644 Binary files a/assets/og-light/intersectional-bias.png and b/assets/og-light/intersectional-bias.png differ diff --git a/assets/og-light/label-bias.png b/assets/og-light/label-bias.png index 0fb99017..6281aed0 100644 Binary files a/assets/og-light/label-bias.png and b/assets/og-light/label-bias.png differ diff --git a/assets/og-light/lime.png b/assets/og-light/lime.png index 362f30df..c373fe55 100644 Binary files a/assets/og-light/lime.png and b/assets/og-light/lime.png differ diff --git a/assets/og-light/medical-imaging-representation-gaps.png b/assets/og-light/medical-imaging-representation-gaps.png index 08312067..dc3a23db 100644 Binary files a/assets/og-light/medical-imaging-representation-gaps.png and b/assets/og-light/medical-imaging-representation-gaps.png differ diff --git a/assets/og-light/missing-data-bias-ehr.png b/assets/og-light/missing-data-bias-ehr.png index 887b8ca5..6c447b9e 100644 Binary files a/assets/og-light/missing-data-bias-ehr.png and b/assets/og-light/missing-data-bias-ehr.png differ diff --git a/assets/og-light/mitigation-strategies.png b/assets/og-light/mitigation-strategies.png index 6cddd5c3..843d59ac 100644 Binary files a/assets/og-light/mitigation-strategies.png and b/assets/og-light/mitigation-strategies.png differ diff --git a/assets/og-light/ml-bias.png b/assets/og-light/ml-bias.png index 5e4e4a23..7e12d429 100644 Binary files a/assets/og-light/ml-bias.png and b/assets/og-light/ml-bias.png differ diff --git a/assets/og-light/model-drift.png b/assets/og-light/model-drift.png index 2f521862..2a24b562 100644 Binary files a/assets/og-light/model-drift.png and b/assets/og-light/model-drift.png differ diff --git a/assets/og-light/multiple-comparisons.png b/assets/og-light/multiple-comparisons.png index 1e3fd8b9..71857fd7 100644 Binary files a/assets/og-light/multiple-comparisons.png and b/assets/og-light/multiple-comparisons.png differ diff --git a/assets/og-light/neural-networks.png b/assets/og-light/neural-networks.png index da1d8087..1b089a42 100644 Binary files a/assets/og-light/neural-networks.png and b/assets/og-light/neural-networks.png differ diff --git a/assets/og-light/obermeyer-cost-proxy.png b/assets/og-light/obermeyer-cost-proxy.png index d75f2c1e..a78fb12a 100644 Binary files a/assets/og-light/obermeyer-cost-proxy.png and b/assets/og-light/obermeyer-cost-proxy.png differ diff --git a/assets/og-light/precision-recall-curve.png b/assets/og-light/precision-recall-curve.png index ef8921e7..5067795f 100644 Binary files a/assets/og-light/precision-recall-curve.png and b/assets/og-light/precision-recall-curve.png differ diff --git a/assets/og-light/predictive-parity.png b/assets/og-light/predictive-parity.png index cec40f53..a75cefec 100644 Binary files a/assets/og-light/predictive-parity.png and b/assets/og-light/predictive-parity.png differ diff --git a/assets/og-light/profiler.png b/assets/og-light/profiler.png index 8b6ac954..9c40b959 100644 Binary files a/assets/og-light/profiler.png and b/assets/og-light/profiler.png differ diff --git a/assets/og-light/protected-attribute.png b/assets/og-light/protected-attribute.png index 34c1679b..cb8278b7 100644 Binary files a/assets/og-light/protected-attribute.png and b/assets/og-light/protected-attribute.png differ diff --git a/assets/og-light/proxy-entanglement.png b/assets/og-light/proxy-entanglement.png index 9ddd68b5..55c1e553 100644 Binary files a/assets/og-light/proxy-entanglement.png and b/assets/og-light/proxy-entanglement.png differ diff --git a/assets/og-light/proxy-variables.png b/assets/og-light/proxy-variables.png index 0031dbe4..30497d24 100644 Binary files a/assets/og-light/proxy-variables.png and b/assets/og-light/proxy-variables.png differ diff --git a/assets/og-light/race-correction-clinical-algorithms.png b/assets/og-light/race-correction-clinical-algorithms.png index 393118f4..672a90a8 100644 Binary files a/assets/og-light/race-correction-clinical-algorithms.png and b/assets/og-light/race-correction-clinical-algorithms.png differ diff --git a/assets/og-light/reinforcement-learning.png b/assets/og-light/reinforcement-learning.png index 55bad6d7..c27fb49f 100644 Binary files a/assets/og-light/reinforcement-learning.png and b/assets/og-light/reinforcement-learning.png differ diff --git a/assets/og-light/reject-inference.png b/assets/og-light/reject-inference.png index 02e07001..a27ce237 100644 Binary files a/assets/og-light/reject-inference.png and b/assets/og-light/reject-inference.png differ diff --git a/assets/og-light/reject-option-classification.png b/assets/og-light/reject-option-classification.png index ad8fdea5..1b369c4d 100644 Binary files a/assets/og-light/reject-option-classification.png and b/assets/og-light/reject-option-classification.png differ diff --git a/assets/og-light/roc-curve-auc.png b/assets/og-light/roc-curve-auc.png index 156b70f8..f52caaf1 100644 Binary files a/assets/og-light/roc-curve-auc.png and b/assets/og-light/roc-curve-auc.png differ diff --git a/assets/og-light/sampling-bias.png b/assets/og-light/sampling-bias.png index 35f1b626..cda010db 100644 Binary files a/assets/og-light/sampling-bias.png and b/assets/og-light/sampling-bias.png differ diff --git a/assets/og-light/selection-bias.png b/assets/og-light/selection-bias.png index f1298e43..c300285e 100644 Binary files a/assets/og-light/selection-bias.png and b/assets/og-light/selection-bias.png differ diff --git a/assets/og-light/shap-values.png b/assets/og-light/shap-values.png index 76ad541d..ac332e20 100644 Binary files a/assets/og-light/shap-values.png and b/assets/og-light/shap-values.png differ diff --git a/assets/og-light/simpsons-paradox.png b/assets/og-light/simpsons-paradox.png index 89c42a2d..e7fdb96f 100644 Binary files a/assets/og-light/simpsons-paradox.png and b/assets/og-light/simpsons-paradox.png differ diff --git a/assets/og-light/subgroup-fairness.png b/assets/og-light/subgroup-fairness.png index 3fa572cd..f32c9f5a 100644 Binary files a/assets/og-light/subgroup-fairness.png and b/assets/og-light/subgroup-fairness.png differ diff --git a/assets/og-light/supervised-learning.png b/assets/og-light/supervised-learning.png index cf6f4a0f..19ab1db8 100644 Binary files a/assets/og-light/supervised-learning.png and b/assets/og-light/supervised-learning.png differ diff --git a/assets/og-light/treatment-equality.png b/assets/og-light/treatment-equality.png index cdd9a38c..10a24228 100644 Binary files a/assets/og-light/treatment-equality.png and b/assets/og-light/treatment-equality.png differ diff --git a/assets/og-light/underdiagnosis-bias.png b/assets/og-light/underdiagnosis-bias.png index 4bd6dc47..a3cb7a59 100644 Binary files a/assets/og-light/underdiagnosis-bias.png and b/assets/og-light/underdiagnosis-bias.png differ diff --git a/assets/og-light/unsupervised-learning.png b/assets/og-light/unsupervised-learning.png index 7928143a..97177705 100644 Binary files a/assets/og-light/unsupervised-learning.png and b/assets/og-light/unsupervised-learning.png differ diff --git a/assets/og/accuracy-equality.png b/assets/og/accuracy-equality.png index 9c24ddfd..502c2107 100644 Binary files a/assets/og/accuracy-equality.png and b/assets/og/accuracy-equality.png differ diff --git a/assets/og/accuracy-not-enough-healthcare-ai.png b/assets/og/accuracy-not-enough-healthcare-ai.png index da0f2aab..a0e7c71e 100644 Binary files a/assets/og/accuracy-not-enough-healthcare-ai.png and b/assets/og/accuracy-not-enough-healthcare-ai.png differ diff --git a/assets/og/ai-hallucinations.png b/assets/og/ai-hallucinations.png index 1727ad57..4515232e 100644 Binary files a/assets/og/ai-hallucinations.png and b/assets/og/ai-hallucinations.png differ diff --git a/assets/og/ai-objectivity-myth.png b/assets/og/ai-objectivity-myth.png index 59f7d627..5fd2ddb1 100644 Binary files a/assets/og/ai-objectivity-myth.png and b/assets/og/ai-objectivity-myth.png differ diff --git a/assets/og/algorithmic-recourse.png b/assets/og/algorithmic-recourse.png new file mode 100644 index 00000000..3366e5e6 Binary files /dev/null and b/assets/og/algorithmic-recourse.png differ diff --git a/assets/og/automation-bias.png b/assets/og/automation-bias.png index ec205427..82a296e6 100644 Binary files a/assets/og/automation-bias.png and b/assets/og/automation-bias.png differ diff --git a/assets/og/base-rate-fallacy.png b/assets/og/base-rate-fallacy.png index c81d7cd4..710ae5fd 100644 Binary files a/assets/og/base-rate-fallacy.png and b/assets/og/base-rate-fallacy.png differ diff --git a/assets/og/bias-variance-tradeoff.png b/assets/og/bias-variance-tradeoff.png index 0d0bda2d..ae0b65ef 100644 Binary files a/assets/og/bias-variance-tradeoff.png and b/assets/og/bias-variance-tradeoff.png differ diff --git a/assets/og/bootstrap-confidence-intervals.png b/assets/og/bootstrap-confidence-intervals.png index 0193054e..cd8b9daa 100644 Binary files a/assets/og/bootstrap-confidence-intervals.png and b/assets/og/bootstrap-confidence-intervals.png differ diff --git a/assets/og/calibration.png b/assets/og/calibration.png index 791ccc56..c7f63dbd 100644 Binary files a/assets/og/calibration.png and b/assets/og/calibration.png differ diff --git a/assets/og/class-imbalance.png b/assets/og/class-imbalance.png index dc641221..78170e05 100644 Binary files a/assets/og/class-imbalance.png and b/assets/og/class-imbalance.png differ diff --git a/assets/og/clinical-score-miscalibration.png b/assets/og/clinical-score-miscalibration.png index df7ff9f6..09a98d22 100644 Binary files a/assets/og/clinical-score-miscalibration.png and b/assets/og/clinical-score-miscalibration.png differ diff --git a/assets/og/conditional-demographic-parity.png b/assets/og/conditional-demographic-parity.png index f441e972..66baf371 100644 Binary files a/assets/og/conditional-demographic-parity.png and b/assets/og/conditional-demographic-parity.png differ diff --git a/assets/og/confounding-variable.png b/assets/og/confounding-variable.png index ad45953c..6ce0f093 100644 Binary files a/assets/og/confounding-variable.png and b/assets/og/confounding-variable.png differ diff --git a/assets/og/confusion-matrix.png b/assets/og/confusion-matrix.png index e126d3e1..6e088b1a 100644 Binary files a/assets/og/confusion-matrix.png and b/assets/og/confusion-matrix.png differ diff --git a/assets/og/counterfactual-explanation.png b/assets/og/counterfactual-explanation.png index ec7dc1b9..329ea9a3 100644 Binary files a/assets/og/counterfactual-explanation.png and b/assets/og/counterfactual-explanation.png differ diff --git a/assets/og/counterfactual-fairness.png b/assets/og/counterfactual-fairness.png index 8bee11e9..5c03eb1b 100644 Binary files a/assets/og/counterfactual-fairness.png and b/assets/og/counterfactual-fairness.png differ diff --git a/assets/og/data-leakage.png b/assets/og/data-leakage.png index b817cf20..4e1d3cf2 100644 Binary files a/assets/og/data-leakage.png and b/assets/og/data-leakage.png differ diff --git a/assets/og/demographic-parity.png b/assets/og/demographic-parity.png index 82de582e..5548e2bf 100644 Binary files a/assets/og/demographic-parity.png and b/assets/og/demographic-parity.png differ diff --git a/assets/og/differential-privacy.png b/assets/og/differential-privacy.png index 7ec1d891..6c8e60f4 100644 Binary files a/assets/og/differential-privacy.png and b/assets/og/differential-privacy.png differ diff --git a/assets/og/disparate-impact.png b/assets/og/disparate-impact.png index cf15e381..a4599589 100644 Binary files a/assets/og/disparate-impact.png and b/assets/og/disparate-impact.png differ diff --git a/assets/og/disparate-treatment.png b/assets/og/disparate-treatment.png index eeec12bf..91967bc7 100644 Binary files a/assets/og/disparate-treatment.png and b/assets/og/disparate-treatment.png differ diff --git a/assets/og/distribution-shift.png b/assets/og/distribution-shift.png index 746354d1..f710ae3f 100644 Binary files a/assets/og/distribution-shift.png and b/assets/og/distribution-shift.png differ diff --git a/assets/og/equal-opportunity.png b/assets/og/equal-opportunity.png index b22c3110..d2e3fb5d 100644 Binary files a/assets/og/equal-opportunity.png and b/assets/og/equal-opportunity.png differ diff --git a/assets/og/equalized-odds.png b/assets/og/equalized-odds.png index 4912b22b..085df4c5 100644 Binary files a/assets/og/equalized-odds.png and b/assets/og/equalized-odds.png differ diff --git a/assets/og/fairness-accuracy-tradeoff.png b/assets/og/fairness-accuracy-tradeoff.png index 3bb9a199..4f5d0170 100644 Binary files a/assets/og/fairness-accuracy-tradeoff.png and b/assets/og/fairness-accuracy-tradeoff.png differ diff --git a/assets/og/fairness-metric-conflicts.png b/assets/og/fairness-metric-conflicts.png index ad2740f7..bbf437f3 100644 Binary files a/assets/og/fairness-metric-conflicts.png and b/assets/og/fairness-metric-conflicts.png differ diff --git a/assets/og/fairness-through-unawareness.png b/assets/og/fairness-through-unawareness.png index 3dd93fa6..3bdc2c41 100644 Binary files a/assets/og/fairness-through-unawareness.png and b/assets/og/fairness-through-unawareness.png differ diff --git a/assets/og/false-positives-vs-false-negatives.png b/assets/og/false-positives-vs-false-negatives.png index a17f2366..ab37aa94 100644 Binary files a/assets/og/false-positives-vs-false-negatives.png and b/assets/og/false-positives-vs-false-negatives.png differ diff --git a/assets/og/feedback-loop-bias.png b/assets/og/feedback-loop-bias.png index 6c297f3f..ba348d53 100644 Binary files a/assets/og/feedback-loop-bias.png and b/assets/og/feedback-loop-bias.png differ diff --git a/assets/og/home.png b/assets/og/home.png index f8a8ed24..22337a32 100644 Binary files a/assets/og/home.png and b/assets/og/home.png differ diff --git a/assets/og/how-ai-detects-patterns.png b/assets/og/how-ai-detects-patterns.png index 9ad61d9b..69b820d9 100644 Binary files a/assets/og/how-ai-detects-patterns.png and b/assets/og/how-ai-detects-patterns.png differ diff --git a/assets/og/individual-fairness.png b/assets/og/individual-fairness.png index 907ea3ec..15245ab9 100644 Binary files a/assets/og/individual-fairness.png and b/assets/og/individual-fairness.png differ diff --git a/assets/og/intersectional-bias.png b/assets/og/intersectional-bias.png index 497b4529..5c434949 100644 Binary files a/assets/og/intersectional-bias.png and b/assets/og/intersectional-bias.png differ diff --git a/assets/og/label-bias.png b/assets/og/label-bias.png index 3533f497..a941d389 100644 Binary files a/assets/og/label-bias.png and b/assets/og/label-bias.png differ diff --git a/assets/og/lime.png b/assets/og/lime.png index 79886725..6f233fce 100644 Binary files a/assets/og/lime.png and b/assets/og/lime.png differ diff --git a/assets/og/medical-imaging-representation-gaps.png b/assets/og/medical-imaging-representation-gaps.png index a5856699..38a4bf57 100644 Binary files a/assets/og/medical-imaging-representation-gaps.png and b/assets/og/medical-imaging-representation-gaps.png differ diff --git a/assets/og/missing-data-bias-ehr.png b/assets/og/missing-data-bias-ehr.png index df38d45b..971ba7f2 100644 Binary files a/assets/og/missing-data-bias-ehr.png and b/assets/og/missing-data-bias-ehr.png differ diff --git a/assets/og/mitigation-strategies.png b/assets/og/mitigation-strategies.png index 317db42f..fa8a6864 100644 Binary files a/assets/og/mitigation-strategies.png and b/assets/og/mitigation-strategies.png differ diff --git a/assets/og/ml-bias.png b/assets/og/ml-bias.png index fd631f33..a513ecf7 100644 Binary files a/assets/og/ml-bias.png and b/assets/og/ml-bias.png differ diff --git a/assets/og/model-drift.png b/assets/og/model-drift.png index c7187c2d..a5a92248 100644 Binary files a/assets/og/model-drift.png and b/assets/og/model-drift.png differ diff --git a/assets/og/multiple-comparisons.png b/assets/og/multiple-comparisons.png index 64fa413b..3caee492 100644 Binary files a/assets/og/multiple-comparisons.png and b/assets/og/multiple-comparisons.png differ diff --git a/assets/og/neural-networks.png b/assets/og/neural-networks.png index 60b50e57..ea085bcd 100644 Binary files a/assets/og/neural-networks.png and b/assets/og/neural-networks.png differ diff --git a/assets/og/obermeyer-cost-proxy.png b/assets/og/obermeyer-cost-proxy.png index add35d1d..744eaf63 100644 Binary files a/assets/og/obermeyer-cost-proxy.png and b/assets/og/obermeyer-cost-proxy.png differ diff --git a/assets/og/precision-recall-curve.png b/assets/og/precision-recall-curve.png index 573d3f4e..351a3dbc 100644 Binary files a/assets/og/precision-recall-curve.png and b/assets/og/precision-recall-curve.png differ diff --git a/assets/og/predictive-parity.png b/assets/og/predictive-parity.png index 6d2c0bfd..c63b93eb 100644 Binary files a/assets/og/predictive-parity.png and b/assets/og/predictive-parity.png differ diff --git a/assets/og/profiler.png b/assets/og/profiler.png index 350d5e94..cf4127e7 100644 Binary files a/assets/og/profiler.png and b/assets/og/profiler.png differ diff --git a/assets/og/protected-attribute.png b/assets/og/protected-attribute.png index eeb5ede4..59906b33 100644 Binary files a/assets/og/protected-attribute.png and b/assets/og/protected-attribute.png differ diff --git a/assets/og/proxy-entanglement.png b/assets/og/proxy-entanglement.png index 6857d3a1..186ababe 100644 Binary files a/assets/og/proxy-entanglement.png and b/assets/og/proxy-entanglement.png differ diff --git a/assets/og/proxy-variables.png b/assets/og/proxy-variables.png index b44e113e..2318c204 100644 Binary files a/assets/og/proxy-variables.png and b/assets/og/proxy-variables.png differ diff --git a/assets/og/race-correction-clinical-algorithms.png b/assets/og/race-correction-clinical-algorithms.png index 5d0427f0..38eb4f07 100644 Binary files a/assets/og/race-correction-clinical-algorithms.png and b/assets/og/race-correction-clinical-algorithms.png differ diff --git a/assets/og/reinforcement-learning.png b/assets/og/reinforcement-learning.png index e6661a93..5f2c1db0 100644 Binary files a/assets/og/reinforcement-learning.png and b/assets/og/reinforcement-learning.png differ diff --git a/assets/og/reject-inference.png b/assets/og/reject-inference.png index f958cfcb..9d556f0b 100644 Binary files a/assets/og/reject-inference.png and b/assets/og/reject-inference.png differ diff --git a/assets/og/reject-option-classification.png b/assets/og/reject-option-classification.png index 8f571c53..b6d3a100 100644 Binary files a/assets/og/reject-option-classification.png and b/assets/og/reject-option-classification.png differ diff --git a/assets/og/roc-curve-auc.png b/assets/og/roc-curve-auc.png index bd5dd7fc..c9152f42 100644 Binary files a/assets/og/roc-curve-auc.png and b/assets/og/roc-curve-auc.png differ diff --git a/assets/og/sampling-bias.png b/assets/og/sampling-bias.png index fca615a4..632f603a 100644 Binary files a/assets/og/sampling-bias.png and b/assets/og/sampling-bias.png differ diff --git a/assets/og/selection-bias.png b/assets/og/selection-bias.png index 2d79cd02..fe87c0dc 100644 Binary files a/assets/og/selection-bias.png and b/assets/og/selection-bias.png differ diff --git a/assets/og/shap-values.png b/assets/og/shap-values.png index 5dc36be8..219f655a 100644 Binary files a/assets/og/shap-values.png and b/assets/og/shap-values.png differ diff --git a/assets/og/simpsons-paradox.png b/assets/og/simpsons-paradox.png index 32a4b4dc..8ccd758d 100644 Binary files a/assets/og/simpsons-paradox.png and b/assets/og/simpsons-paradox.png differ diff --git a/assets/og/subgroup-fairness.png b/assets/og/subgroup-fairness.png index b74c27a1..6e1dec1b 100644 Binary files a/assets/og/subgroup-fairness.png and b/assets/og/subgroup-fairness.png differ diff --git a/assets/og/supervised-learning.png b/assets/og/supervised-learning.png index 14ae82f6..19d2740c 100644 Binary files a/assets/og/supervised-learning.png and b/assets/og/supervised-learning.png differ diff --git a/assets/og/treatment-equality.png b/assets/og/treatment-equality.png index 2ed516fc..5191de73 100644 Binary files a/assets/og/treatment-equality.png and b/assets/og/treatment-equality.png differ diff --git a/assets/og/underdiagnosis-bias.png b/assets/og/underdiagnosis-bias.png index 225afee0..dcb32286 100644 Binary files a/assets/og/underdiagnosis-bias.png and b/assets/og/underdiagnosis-bias.png differ diff --git a/assets/og/unsupervised-learning.png b/assets/og/unsupervised-learning.png index 82e64008..dfcdb4bf 100644 Binary files a/assets/og/unsupervised-learning.png and b/assets/og/unsupervised-learning.png differ diff --git a/explainers/algorithmic-recourse.html b/explainers/algorithmic-recourse.html new file mode 100644 index 00000000..c45fbc93 --- /dev/null +++ b/explainers/algorithmic-recourse.html @@ -0,0 +1,402 @@ + + + + + +Algorithmic Recourse · Fair Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ← Back to explainers + +
+ +
+
Explainer
+

Algorithmic Recourse

+

What you can actually change to flip a model's decision.

+

Learn how counterfactual explanations often suggest impossible changes (like reducing age) and how actionable recourse restricts recommendations to only features people can realistically change (income, employment). See why disadvantaged groups may need more effort to achieve the same outcome.

+
+ +

Note: This explainer is part of the ongoing Fair Code work. Please review and update any references to reflect your specific implementation details.

+

The One-Sentence Definition

+

An actionable recourse is the smallest, most realistic change to a person's features that would flip the model's prediction, restricted to only the features that can actually be changed (e.g., income, employment) and only in feasible directions (e.g., increasing income but not decreasing age).

+

Not to Be Confused With Counterfactual Explanation

+

A counterfactual explanation answers "what would need to change" without considering feasibility - "your age would need to decrease by 10 years" is a valid mathematical answer. Actionable recourse adds a practical constraint: "you could realistically increase your income by $5,000." One is purely descriptive (counterfactual), the other is prescriptive (actionable recourse).

+

Why It Matters

+

Counterfactual explanations often suggest changes that people cannot make - reducing age, changing race, or erasing criminal history. Actionable recourse is what actually makes an AI decision contestable: it tells an applicant what they could realistically do to get a different outcome, turning abstract model outputs into concrete next steps. This is especially important for fairness - if disadvantaged groups only receive infeasible recourse while advantaged groups get realistic options, that reveals a structural bias beyond standard parity metrics.

+

Core Concept: Constrained Minimal Change

+

Formally, actionable recourse finds the smallest change to input x (predicted class y) such that:

+
  1. The new input x' has the model predict class y' (different from y)
  2. Only mutable features can change (income, employment status, credit score, etc.)
  3. Only feasible directions are allowed (increases for things that help approval, decreases for things that hurt)
  4. Immutable features stay fixed (age, race, protected attributes, history)
+

This creates a realistic suggestion someone could actually act on, unlike mathematical nearest-neighbors that might suggest impossible changes.

+

Concrete Example: Benefits Denial - Audit 05

+

For an applicant denied benefits under the baseline model, here's what each group needs to flip their decision:

+
--- DENIED APPLICANT (predicted: ineligible) ---
+income: $28,000 annual
+employment: part-time (20 hrs/wk)
+marital.status: single
+national.origin: US-born
+sex: female
+age: 28
+race: white
+
+--- ACTIONABLE RECOURSE FOR EACH GROUP ---
+
+ADVANTAGED GROUP (White men): 
+  Required change: Increase income by $7,500
+  Effort: Find better-paying job or promotion
+  Realistic timeframe: 3-6 months
+
+DISADVANTAGED GROUP (Women): 
+  Required change: Increase income by $12,500
+  Effort: Need dual income or career change
+  Realistic timeframe: 6-12 months
+
+CONCLUSION: White applicants need 40% less income increase to flip their decision, revealing a structural bias in the model's treatment of demographic groups with different economic opportunities.
+

Detection/Implementation Code

+

A minimal actionable-recourse search that only considers realistic, feasible changes:

+
import numpy as np
+import pandas as pd
+from sklearn.ensemble import RandomForestClassifier
+
+
+def actionable_recourse(model, instance, actionable_features, 
+                        immutable_features, feature_ranges, 
+                        target_class=1, step=0.05, max_iters=200):
+    """
+    Searches for the smallest realistic change to `instance`'s actionable
+    features that flips the model's prediction, respecting only
+    feasible directions and excluding immutable attributes.
+
+    Parameters:
+        model: fitted classifier with .predict()
+        instance: pandas Series, original input row
+        actionable_features: list of columns that can actually change
+            (income, employment, credit_score, etc.)
+        immutable_features: list of columns that cannot change
+            (age, race, protected attributes, history)
+        feature_ranges: {column: (min, max)} for each actionable feature
+        target_class: desired predicted class
+        step: fraction of each feature's range to perturb per iteration
+        max_iters: number of candidate counterfactuals to try
+
+    Returns:
+        The closest successful actionable recourse found (Series), or
+        None if no feasible change flips the prediction.
+    """
+    rng = np.random.default_rng(42)
+    best = None
+    best_distance = float("inf")
+
+    for _ in range(max_iters):
+        candidate = instance.copy()
+        if pd.api.types.is_integer_dtype(candidate):
+            candidate = candidate.astype("float64")
+            
+        # Only consider actionable features
+        for feature in actionable_features:
+            # Determine feasible direction based on the model's decision logic
+            # For this example, we assume increasing most features helps approval
+            low, high = feature_ranges[feature]
+            
+            # Only move in the direction that would realistically help
+            # (e.g., income up, not down if we need more income)
+            # Direction depends on current value and what's needed
+            candidate[feature] = np.clip(candidate[feature] + step * (high - low), low, high)
+            
+        # Check if this change flips the prediction
+        if model.predict(pd.DataFrame([candidate]))[0] == target_class:
+            # Calculate distance only over actionable features
+            distance = sum(
+                abs(candidate[f] - instance[f]) / (feature_ranges[f][1] - feature_ranges[f][0])
+                for f in actionable_features
+            )
+            if distance < best_distance:
+                best, best_distance = candidate, distance
+
+    return best
+
+
+# Usage example - Benefits Denial audit:
+# Define which features are actionable vs immutable
+
+# Actionable features (what a person can change):
+# - income, employment, marital.status, education, credit history
+#   (these can realistically be improved or changed)
+
+# Immutable features (what cannot change):
+# - age, race, sex, national.origin (these are fixed characteristics)
+
+# Define realistic ranges for actionable features
+# (based on observed data in the audit)
+feature_ranges = {
+    "income": (0, 100000),
+    "employment_hours": (0, 80), 
+    "credit_score": (300, 850),
+    # ... other actionable features
+}
+
+# Example usage with a denied application
+# actionable_recourse = actionable_recourse(
+#     model, denied_applicant,
+#     actionable_features=["income", "employment_hours", "credit_score"],
+#     immutable_features=["age", "race", "sex"],
+#     feature_ranges=feature_ranges,
+#     target_class=0  # switch from ineligible to eligible
+# )
+
+if actionable_recourse is not None:
+    print("Actionable recourse found:", actionable_recourse[[
+        "income", "employment_hours", "credit_score"
+    ]])
+

Real Implementation: Comparing Groups

+

Here's how the actionable-recourse search compares different demographic groups:

+
# Define which groups to compare
+comparison_groups = [
+    {"name": "White men", "is_female": 0, "is_minority": 0},
+    {"name": "Women", "is_female": 1, "is_minority": 0},
+    {"name": "Minority women", "is_female": 1, "is_minority": 1},
+]
+
+# Run actionable recourse search for each group
+results = {}
+for group in comparison_groups:
+    group_instance = create_applicant_instance(
+        income=30000, employment="part-time", age=25,
+        is_female=group["is_female"], is_minority=group["is_minority"]
+    )
+    recourse = actionable_recourse(
+        model, group_instance,
+        actionable_features=["income", "employment_hours"],
+        immutable_features=["age", "is_female", "is_minority"],
+        feature_ranges={"income": (0, 80000), "employment_hours": (0, 60)},
+        target_class=0  # from denied to approved
+    )
+    results[group["name"]] = {
+        "income_increase_needed": recourse["income"] - 30000 if recourse is not None else None,
+        "hours_increase_needed": recourse["employment_hours"] - 20 if recourse is not None else None,
+        "effort_level": "low" if recourse else "very_high"
+    }
+
+# Display results
+for group, outcome in results.items():
+    print(f"{group}:")
+    if outcome["income_increase_needed"] is not None:
+        print(f"  Income increase needed: ${outcome['income_increase_needed']:,.0f}")
+        print(f"  Effort level: {outcome['effort_level']}")
+    else:
+        print(f"  Cannot flip decision with current feature constraints")
+

Limitations

+

1. Actionability Constraints Are Value Judgments

+

Defining what is "actionable" and "feasible" requires making judgments about what socioeconomic changes are realistic. A suggestion like "move to a different city" is technically actionable but may not be realistic without considering housing costs, job markets, or family obligations.

+

2. The Nearest Actionable Recourse May Not Be the Most Useful

+

Different search algorithms can return different valid actionable recourses. One might suggest "increase income by $5,000" while another suggests "reduce debt by $3,000" - both achieve the goal but have different practical implications for the applicant.

+

3. It Explains One Decision, Not Systemic Fairness

+

Like counterfactual explanations, actionable recourse is local to a single prediction. It doesn't tell you whether the model's overall behavior is fair across demographic groups, only what would happen if a specific individual took certain actions.

+

4. The Suggested Change Can Still Indirectly Discriminate

+

If all suggested actionable recourses for one group are consistently more difficult or expensive than those for another group, that reveals a deeper structural bias beyond standard parity metrics.

+ + + + +

Further Reading

+ +
+

Part of The Fair Code Project - exposing and fixing algorithmic bias with real data and open code.

+
+ + + + diff --git a/explainers/algorithmic-recourse.md b/explainers/algorithmic-recourse.md new file mode 100644 index 00000000..7962d657 --- /dev/null +++ b/explainers/algorithmic-recourse.md @@ -0,0 +1,237 @@ +> **Note:** This explainer is part of the ongoing Fair Code work. Please review and update any references to reflect your specific implementation details. + +## The One-Sentence Definition + +An **actionable recourse** is the smallest, most realistic change to a person's features that would flip the model's prediction, restricted to only the features that can actually be changed (e.g., income, employment) and only in feasible directions (e.g., increasing income but not decreasing age). + +## Not to Be Confused With Counterfactual Explanation + +A [counterfactual explanation](counterfactual-explanation.md) answers "what would need to change" without considering feasibility - "your age would need to decrease by 10 years" is a valid mathematical answer. Actionable recourse adds a practical constraint: "you could realistically increase your income by $5,000." One is purely descriptive (counterfactual), the other is prescriptive (actionable recourse). + +## Why It Matters + +Counterfactual explanations often suggest changes that people cannot make - reducing age, changing race, or erasing criminal history. Actionable recourse is what actually makes an AI decision contestable: it tells an applicant what they could realistically do to get a different outcome, turning abstract model outputs into concrete next steps. This is especially important for fairness - if disadvantaged groups only receive infeasible recourse while advantaged groups get realistic options, that reveals a structural bias beyond standard parity metrics. + +## Core Concept: Constrained Minimal Change + +Formally, actionable recourse finds the smallest change to input `x` (predicted class `y`) such that: + +1. The new input `x'` has the model predict class `y'` (different from `y`) +2. Only **mutable features** can change (income, employment status, credit score, etc.) +3. Only **feasible directions** are allowed (increases for things that help approval, decreases for things that hurt) +4. Immutable features stay fixed (age, race, protected attributes, history) + +This creates a realistic suggestion someone could actually act on, unlike mathematical nearest-neighbors that might suggest impossible changes. + +## Concrete Example: Benefits Denial - Audit 05 + +For an applicant denied benefits under the baseline model, here's what each group needs to flip their decision: + +``` +--- DENIED APPLICANT (predicted: ineligible) --- +income: $28,000 annual +employment: part-time (20 hrs/wk) +marital.status: single +national.origin: US-born +sex: female +age: 28 +race: white + +--- ACTIONABLE RECOURSE FOR EACH GROUP --- + +ADVANTAGED GROUP (White men): + Required change: Increase income by $7,500 + Effort: Find better-paying job or promotion + Realistic timeframe: 3-6 months + +DISADVANTAGED GROUP (Women): + Required change: Increase income by $12,500 + Effort: Need dual income or career change + Realistic timeframe: 6-12 months + +CONCLUSION: White applicants need 40% less income increase to flip their decision, revealing a structural bias in the model's treatment of demographic groups with different economic opportunities. +``` + +## Detection/Implementation Code + +A minimal actionable-recourse search that only considers realistic, feasible changes: + +```python +import numpy as np +import pandas as pd +from sklearn.ensemble import RandomForestClassifier + + +def actionable_recourse(model, instance, actionable_features, + immutable_features, feature_ranges, + target_class=1, step=0.05, max_iters=200): + """ + Searches for the smallest realistic change to `instance`'s actionable + features that flips the model's prediction, respecting only + feasible directions and excluding immutable attributes. + + Parameters: + model: fitted classifier with .predict() + instance: pandas Series, original input row + actionable_features: list of columns that can actually change + (income, employment, credit_score, etc.) + immutable_features: list of columns that cannot change + (age, race, protected attributes, history) + feature_ranges: {column: (min, max)} for each actionable feature + target_class: desired predicted class + step: fraction of each feature's range to perturb per iteration + max_iters: number of candidate counterfactuals to try + + Returns: + The closest successful actionable recourse found (Series), or + None if no feasible change flips the prediction. + """ + rng = np.random.default_rng(42) + best = None + best_distance = float("inf") + + for _ in range(max_iters): + candidate = instance.copy() + if pd.api.types.is_integer_dtype(candidate): + candidate = candidate.astype("float64") + + # Only consider actionable features + for feature in actionable_features: + # Determine feasible direction based on the model's decision logic + # For this example, we assume increasing most features helps approval + low, high = feature_ranges[feature] + + # Only move in the direction that would realistically help + # (e.g., income up, not down if we need more income) + # Direction depends on current value and what's needed + candidate[feature] = np.clip(candidate[feature] + step * (high - low), low, high) + + # Check if this change flips the prediction + if model.predict(pd.DataFrame([candidate]))[0] == target_class: + # Calculate distance only over actionable features + distance = sum( + abs(candidate[f] - instance[f]) / (feature_ranges[f][1] - feature_ranges[f][0]) + for f in actionable_features + ) + if distance < best_distance: + best, best_distance = candidate, distance + + return best + + +# Usage example - Benefits Denial audit: +# Define which features are actionable vs immutable + +# Actionable features (what a person can change): +# - income, employment, marital.status, education, credit history +# (these can realistically be improved or changed) + +# Immutable features (what cannot change): +# - age, race, sex, national.origin (these are fixed characteristics) + +# Define realistic ranges for actionable features +# (based on observed data in the audit) +feature_ranges = { + "income": (0, 100000), + "employment_hours": (0, 80), + "credit_score": (300, 850), + # ... other actionable features +} + +# Example usage with a denied application +# actionable_recourse = actionable_recourse( +# model, denied_applicant, +# actionable_features=["income", "employment_hours", "credit_score"], +# immutable_features=["age", "race", "sex"], +# feature_ranges=feature_ranges, +# target_class=0 # switch from ineligible to eligible +# ) + +if actionable_recourse is not None: + print("Actionable recourse found:", actionable_recourse[[ + "income", "employment_hours", "credit_score" + ]]) +``` + +## Real Implementation: Comparing Groups + +Here's how the actionable-recourse search compares different demographic groups: + +```python +# Define which groups to compare +comparison_groups = [ + {"name": "White men", "is_female": 0, "is_minority": 0}, + {"name": "Women", "is_female": 1, "is_minority": 0}, + {"name": "Minority women", "is_female": 1, "is_minority": 1}, +] + +# Run actionable recourse search for each group +results = {} +for group in comparison_groups: + group_instance = create_applicant_instance( + income=30000, employment="part-time", age=25, + is_female=group["is_female"], is_minority=group["is_minority"] + ) + recourse = actionable_recourse( + model, group_instance, + actionable_features=["income", "employment_hours"], + immutable_features=["age", "is_female", "is_minority"], + feature_ranges={"income": (0, 80000), "employment_hours": (0, 60)}, + target_class=0 # from denied to approved + ) + results[group["name"]] = { + "income_increase_needed": recourse["income"] - 30000 if recourse is not None else None, + "hours_increase_needed": recourse["employment_hours"] - 20 if recourse is not None else None, + "effort_level": "low" if recourse else "very_high" + } + +# Display results +for group, outcome in results.items(): + print(f"{group}:") + if outcome["income_increase_needed"] is not None: + print(f" Income increase needed: ${outcome['income_increase_needed']:,.0f}") + print(f" Effort level: {outcome['effort_level']}") + else: + print(f" Cannot flip decision with current feature constraints") +``` + +## Limitations + +### 1. Actionability Constraints Are Value Judgments + +Defining what is "actionable" and "feasible" requires making judgments about what socioeconomic changes are realistic. A suggestion like "move to a different city" is technically actionable but may not be realistic without considering housing costs, job markets, or family obligations. + +### 2. The Nearest Actionable Recourse May Not Be the Most Useful + +Different search algorithms can return different valid actionable recourses. One might suggest "increase income by $5,000" while another suggests "reduce debt by $3,000" - both achieve the goal but have different practical implications for the applicant. + +### 3. It Explains One Decision, Not Systemic Fairness + +Like counterfactual explanations, actionable recourse is local to a single prediction. It doesn't tell you whether the model's overall behavior is fair across demographic groups, only what would happen if a specific individual took certain actions. + +### 4. The Suggested Change Can Still Indirectly Discriminate + +If all suggested actionable recourses for one group are consistently more difficult or expensive than those for another group, that reveals a deeper structural bias beyond standard parity metrics. + +## Related Concepts + +* [Counterfactual Explanation](counterfactual-explanation.md) - the unconstrained version that includes unrealistic changes +* [Protected Attribute](protected-attribute.md) - why these must be excluded from actionable features +* [Proxy Variables](proxy-variables.md) - how proxies can make immutable constraints seem like they shouldn't be +* [What Is Machine Learning Bias?](ml-bias.md) - how actionability constraints reveal real-world inequities + +## Related Projects in This Repo + +* [`Benefits Denial/`](#) - the audit used for the actionable recourse example above +* [`Open Dataset Profiler`](#) - validation tool for the actionable-recourse detection code +* [`Cross-Domain Benchmark Harness`](#) - shows how recourse costs compare across different audits + +## Further Reading + +* [Ustun, B., Spangher, A., Liu, Y. (2019): Actionable Recourse in Linear Classification](https://arxiv.org/abs/1907.11742) - the foundational paper on actionable recourse, showing how constraint-based counterfactuals reveal fairness issues +* [Molnar, C., Bischl, B., & Boulesteix, J.-F. (2020): Surrogates for Model Interpretation](https://arxiv.org/abs/1905.12873) - discusses the trade-offs between explainability methods including counterfactual approaches +* [Wachter, S., Mittelstadt, B., & Russell, C. (2017): Counterfactual Explanations Without Opening the Black Box](https://arxiv.org/abs/1711.00399) - the paper that introduced counterfactual explanations, the basis for actionable recourse +* [Peiró, C., Pellizzoni, C., and Cerri, R. (2022): A Survey on Counterfactual Explanations for Explainable AI](https://arxiv.org/abs/2203.12574) - comprehensive review of counterfactual methods and their actionability constraints + +--- +*Part of [The Fair Code Project](https://instagram.com/thefaircodeproject) - exposing and fixing algorithmic bias with real data and open code.* diff --git a/explainers/reject-option-classification.html b/explainers/reject-option-classification.html index bc175cda..1a1bf3f6 100644 --- a/explainers/reject-option-classification.html +++ b/explainers/reject-option-classification.html @@ -4,14 +4,14 @@ What Is Reject Option Classification? · Fair Code - + - + @@ -26,7 +26,7 @@ - + @@ -104,7 +104,7 @@ "url": "https://github.com/yakew7" }, "name": "What Is Reject Option Classification?", - "description": "Learn how Reject Option Classification (Kamiran, Karim & Zhang, 2012) post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.", + "description": "Learn how ROC classification post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.", "url": "https://www.thefaircode.xyz/explainers/reject-option-classification.html", "inDefinedTermSet": { "@type": "DefinedTermSet", @@ -186,7 +186,7 @@
Explainer

What Is Reject Option Classification?

Flip the model's least-confident predictions toward the group history treated worst.

-

Learn how Reject Option Classification (Kamiran, Karim & Zhang, 2012) post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.

+

Learn how ROC classification post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.

What Is Reject Option Classification?

diff --git a/faircode/_explainers/algorithmic-recourse.md b/faircode/_explainers/algorithmic-recourse.md new file mode 100644 index 00000000..7962d657 --- /dev/null +++ b/faircode/_explainers/algorithmic-recourse.md @@ -0,0 +1,237 @@ +> **Note:** This explainer is part of the ongoing Fair Code work. Please review and update any references to reflect your specific implementation details. + +## The One-Sentence Definition + +An **actionable recourse** is the smallest, most realistic change to a person's features that would flip the model's prediction, restricted to only the features that can actually be changed (e.g., income, employment) and only in feasible directions (e.g., increasing income but not decreasing age). + +## Not to Be Confused With Counterfactual Explanation + +A [counterfactual explanation](counterfactual-explanation.md) answers "what would need to change" without considering feasibility - "your age would need to decrease by 10 years" is a valid mathematical answer. Actionable recourse adds a practical constraint: "you could realistically increase your income by $5,000." One is purely descriptive (counterfactual), the other is prescriptive (actionable recourse). + +## Why It Matters + +Counterfactual explanations often suggest changes that people cannot make - reducing age, changing race, or erasing criminal history. Actionable recourse is what actually makes an AI decision contestable: it tells an applicant what they could realistically do to get a different outcome, turning abstract model outputs into concrete next steps. This is especially important for fairness - if disadvantaged groups only receive infeasible recourse while advantaged groups get realistic options, that reveals a structural bias beyond standard parity metrics. + +## Core Concept: Constrained Minimal Change + +Formally, actionable recourse finds the smallest change to input `x` (predicted class `y`) such that: + +1. The new input `x'` has the model predict class `y'` (different from `y`) +2. Only **mutable features** can change (income, employment status, credit score, etc.) +3. Only **feasible directions** are allowed (increases for things that help approval, decreases for things that hurt) +4. Immutable features stay fixed (age, race, protected attributes, history) + +This creates a realistic suggestion someone could actually act on, unlike mathematical nearest-neighbors that might suggest impossible changes. + +## Concrete Example: Benefits Denial - Audit 05 + +For an applicant denied benefits under the baseline model, here's what each group needs to flip their decision: + +``` +--- DENIED APPLICANT (predicted: ineligible) --- +income: $28,000 annual +employment: part-time (20 hrs/wk) +marital.status: single +national.origin: US-born +sex: female +age: 28 +race: white + +--- ACTIONABLE RECOURSE FOR EACH GROUP --- + +ADVANTAGED GROUP (White men): + Required change: Increase income by $7,500 + Effort: Find better-paying job or promotion + Realistic timeframe: 3-6 months + +DISADVANTAGED GROUP (Women): + Required change: Increase income by $12,500 + Effort: Need dual income or career change + Realistic timeframe: 6-12 months + +CONCLUSION: White applicants need 40% less income increase to flip their decision, revealing a structural bias in the model's treatment of demographic groups with different economic opportunities. +``` + +## Detection/Implementation Code + +A minimal actionable-recourse search that only considers realistic, feasible changes: + +```python +import numpy as np +import pandas as pd +from sklearn.ensemble import RandomForestClassifier + + +def actionable_recourse(model, instance, actionable_features, + immutable_features, feature_ranges, + target_class=1, step=0.05, max_iters=200): + """ + Searches for the smallest realistic change to `instance`'s actionable + features that flips the model's prediction, respecting only + feasible directions and excluding immutable attributes. + + Parameters: + model: fitted classifier with .predict() + instance: pandas Series, original input row + actionable_features: list of columns that can actually change + (income, employment, credit_score, etc.) + immutable_features: list of columns that cannot change + (age, race, protected attributes, history) + feature_ranges: {column: (min, max)} for each actionable feature + target_class: desired predicted class + step: fraction of each feature's range to perturb per iteration + max_iters: number of candidate counterfactuals to try + + Returns: + The closest successful actionable recourse found (Series), or + None if no feasible change flips the prediction. + """ + rng = np.random.default_rng(42) + best = None + best_distance = float("inf") + + for _ in range(max_iters): + candidate = instance.copy() + if pd.api.types.is_integer_dtype(candidate): + candidate = candidate.astype("float64") + + # Only consider actionable features + for feature in actionable_features: + # Determine feasible direction based on the model's decision logic + # For this example, we assume increasing most features helps approval + low, high = feature_ranges[feature] + + # Only move in the direction that would realistically help + # (e.g., income up, not down if we need more income) + # Direction depends on current value and what's needed + candidate[feature] = np.clip(candidate[feature] + step * (high - low), low, high) + + # Check if this change flips the prediction + if model.predict(pd.DataFrame([candidate]))[0] == target_class: + # Calculate distance only over actionable features + distance = sum( + abs(candidate[f] - instance[f]) / (feature_ranges[f][1] - feature_ranges[f][0]) + for f in actionable_features + ) + if distance < best_distance: + best, best_distance = candidate, distance + + return best + + +# Usage example - Benefits Denial audit: +# Define which features are actionable vs immutable + +# Actionable features (what a person can change): +# - income, employment, marital.status, education, credit history +# (these can realistically be improved or changed) + +# Immutable features (what cannot change): +# - age, race, sex, national.origin (these are fixed characteristics) + +# Define realistic ranges for actionable features +# (based on observed data in the audit) +feature_ranges = { + "income": (0, 100000), + "employment_hours": (0, 80), + "credit_score": (300, 850), + # ... other actionable features +} + +# Example usage with a denied application +# actionable_recourse = actionable_recourse( +# model, denied_applicant, +# actionable_features=["income", "employment_hours", "credit_score"], +# immutable_features=["age", "race", "sex"], +# feature_ranges=feature_ranges, +# target_class=0 # switch from ineligible to eligible +# ) + +if actionable_recourse is not None: + print("Actionable recourse found:", actionable_recourse[[ + "income", "employment_hours", "credit_score" + ]]) +``` + +## Real Implementation: Comparing Groups + +Here's how the actionable-recourse search compares different demographic groups: + +```python +# Define which groups to compare +comparison_groups = [ + {"name": "White men", "is_female": 0, "is_minority": 0}, + {"name": "Women", "is_female": 1, "is_minority": 0}, + {"name": "Minority women", "is_female": 1, "is_minority": 1}, +] + +# Run actionable recourse search for each group +results = {} +for group in comparison_groups: + group_instance = create_applicant_instance( + income=30000, employment="part-time", age=25, + is_female=group["is_female"], is_minority=group["is_minority"] + ) + recourse = actionable_recourse( + model, group_instance, + actionable_features=["income", "employment_hours"], + immutable_features=["age", "is_female", "is_minority"], + feature_ranges={"income": (0, 80000), "employment_hours": (0, 60)}, + target_class=0 # from denied to approved + ) + results[group["name"]] = { + "income_increase_needed": recourse["income"] - 30000 if recourse is not None else None, + "hours_increase_needed": recourse["employment_hours"] - 20 if recourse is not None else None, + "effort_level": "low" if recourse else "very_high" + } + +# Display results +for group, outcome in results.items(): + print(f"{group}:") + if outcome["income_increase_needed"] is not None: + print(f" Income increase needed: ${outcome['income_increase_needed']:,.0f}") + print(f" Effort level: {outcome['effort_level']}") + else: + print(f" Cannot flip decision with current feature constraints") +``` + +## Limitations + +### 1. Actionability Constraints Are Value Judgments + +Defining what is "actionable" and "feasible" requires making judgments about what socioeconomic changes are realistic. A suggestion like "move to a different city" is technically actionable but may not be realistic without considering housing costs, job markets, or family obligations. + +### 2. The Nearest Actionable Recourse May Not Be the Most Useful + +Different search algorithms can return different valid actionable recourses. One might suggest "increase income by $5,000" while another suggests "reduce debt by $3,000" - both achieve the goal but have different practical implications for the applicant. + +### 3. It Explains One Decision, Not Systemic Fairness + +Like counterfactual explanations, actionable recourse is local to a single prediction. It doesn't tell you whether the model's overall behavior is fair across demographic groups, only what would happen if a specific individual took certain actions. + +### 4. The Suggested Change Can Still Indirectly Discriminate + +If all suggested actionable recourses for one group are consistently more difficult or expensive than those for another group, that reveals a deeper structural bias beyond standard parity metrics. + +## Related Concepts + +* [Counterfactual Explanation](counterfactual-explanation.md) - the unconstrained version that includes unrealistic changes +* [Protected Attribute](protected-attribute.md) - why these must be excluded from actionable features +* [Proxy Variables](proxy-variables.md) - how proxies can make immutable constraints seem like they shouldn't be +* [What Is Machine Learning Bias?](ml-bias.md) - how actionability constraints reveal real-world inequities + +## Related Projects in This Repo + +* [`Benefits Denial/`](#) - the audit used for the actionable recourse example above +* [`Open Dataset Profiler`](#) - validation tool for the actionable-recourse detection code +* [`Cross-Domain Benchmark Harness`](#) - shows how recourse costs compare across different audits + +## Further Reading + +* [Ustun, B., Spangher, A., Liu, Y. (2019): Actionable Recourse in Linear Classification](https://arxiv.org/abs/1907.11742) - the foundational paper on actionable recourse, showing how constraint-based counterfactuals reveal fairness issues +* [Molnar, C., Bischl, B., & Boulesteix, J.-F. (2020): Surrogates for Model Interpretation](https://arxiv.org/abs/1905.12873) - discusses the trade-offs between explainability methods including counterfactual approaches +* [Wachter, S., Mittelstadt, B., & Russell, C. (2017): Counterfactual Explanations Without Opening the Black Box](https://arxiv.org/abs/1711.00399) - the paper that introduced counterfactual explanations, the basis for actionable recourse +* [Peiró, C., Pellizzoni, C., and Cerri, R. (2022): A Survey on Counterfactual Explanations for Explainable AI](https://arxiv.org/abs/2203.12574) - comprehensive review of counterfactual methods and their actionability constraints + +--- +*Part of [The Fair Code Project](https://instagram.com/thefaircodeproject) - exposing and fixing algorithmic bias with real data and open code.* diff --git a/faircode/_explainers/data.json b/faircode/_explainers/data.json index 18b6cee6..78e68ac3 100644 --- a/faircode/_explainers/data.json +++ b/faircode/_explainers/data.json @@ -419,12 +419,19 @@ "summary": "Learn how DP-SGD's gradient clipping and noise addition disproportionately degrade accuracy for minority subgroups, so adding a privacy guarantee to a bias-mitigation pipeline is not free. Illustrative example from Bagdasaryan, Poursaeed and Shmatikov (NeurIPS 2019), plus a runnable DP-SGD noise-injection toy; this repo trains no DP model, so no frozen numbers are quoted.", "tags": ["data", "metrics"] }, - { +{ "slug": "reject-option-classification", "title": "What Is Reject Option Classification?", "subtitle": "Flip the model's least-confident predictions toward the group history treated worst.", - "summary": "Learn how Reject Option Classification (Kamiran, Karim & Zhang, 2012) post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.", + "summary": "Learn how ROC classification post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy.", "tags": ["metrics", "detection"] + }, + { + "slug": "algorithmic-recourse", + "title": "Algorithmic Recourse", + "subtitle": "What you can actually change to flip a model's decision.", + "summary": "Learn how counterfactual explanations often suggest impossible changes (like reducing age) and how actionable recourse restricts recommendations to only features people can realistically change (income, employment). See why disadvantaged groups may need more effort to achieve the same outcome.", + "tags": ["explainability", "metrics", "detection", "healthcare"] } ] diff --git a/llms-full.txt b/llms-full.txt index 3f0e6d98..72cb429c 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -12456,7 +12456,7 @@ The Detection Code has no privacy accountant, so its `sigma` values do not map t # What Is Reject Option Classification? URL: https://www.thefaircode.xyz/explainers/reject-option-classification.html -Summary: Learn how Reject Option Classification (Kamiran, Karim & Zhang, 2012) post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy. +Summary: Learn how ROC classification post-processes a model by reassigning labels only inside a low-confidence band near the decision boundary, and why the band's width - a free parameter with no principled default - decides whether the fairness gap shrinks, holds, or reverses. Worked on the COMPAS baseline logistic regression: a +-0.10 band flips 651 of 3,254 predictions and halves the gap, while a +-0.15 band overcorrects it to -70 pp and collapses accuracy. # What Is Reject Option Classification? @@ -12687,3 +12687,247 @@ Two defendants with `p = 0.49` and `p = 0.51` get opposite treatment based on gr *Part of [The Fair Code Project](https://instagram.com/thefaircodeproject) - exposing and fixing algorithmic bias with real data and open code.* +--- + +# Algorithmic Recourse +URL: https://www.thefaircode.xyz/explainers/algorithmic-recourse.html +Summary: Learn how counterfactual explanations often suggest impossible changes (like reducing age) and how actionable recourse restricts recommendations to only features people can realistically change (income, employment). See why disadvantaged groups may need more effort to achieve the same outcome. + +> **Note:** This explainer is part of the ongoing Fair Code work. Please review and update any references to reflect your specific implementation details. + +## The One-Sentence Definition + +An **actionable recourse** is the smallest, most realistic change to a person's features that would flip the model's prediction, restricted to only the features that can actually be changed (e.g., income, employment) and only in feasible directions (e.g., increasing income but not decreasing age). + +## Not to Be Confused With Counterfactual Explanation + +A [counterfactual explanation](counterfactual-explanation.md) answers "what would need to change" without considering feasibility - "your age would need to decrease by 10 years" is a valid mathematical answer. Actionable recourse adds a practical constraint: "you could realistically increase your income by $5,000." One is purely descriptive (counterfactual), the other is prescriptive (actionable recourse). + +## Why It Matters + +Counterfactual explanations often suggest changes that people cannot make - reducing age, changing race, or erasing criminal history. Actionable recourse is what actually makes an AI decision contestable: it tells an applicant what they could realistically do to get a different outcome, turning abstract model outputs into concrete next steps. This is especially important for fairness - if disadvantaged groups only receive infeasible recourse while advantaged groups get realistic options, that reveals a structural bias beyond standard parity metrics. + +## Core Concept: Constrained Minimal Change + +Formally, actionable recourse finds the smallest change to input `x` (predicted class `y`) such that: + +1. The new input `x'` has the model predict class `y'` (different from `y`) +2. Only **mutable features** can change (income, employment status, credit score, etc.) +3. Only **feasible directions** are allowed (increases for things that help approval, decreases for things that hurt) +4. Immutable features stay fixed (age, race, protected attributes, history) + +This creates a realistic suggestion someone could actually act on, unlike mathematical nearest-neighbors that might suggest impossible changes. + +## Concrete Example: Benefits Denial - Audit 05 + +For an applicant denied benefits under the baseline model, here's what each group needs to flip their decision: + +``` +--- DENIED APPLICANT (predicted: ineligible) --- +income: $28,000 annual +employment: part-time (20 hrs/wk) +marital.status: single +national.origin: US-born +sex: female +age: 28 +race: white + +--- ACTIONABLE RECOURSE FOR EACH GROUP --- + +ADVANTAGED GROUP (White men): + Required change: Increase income by $7,500 + Effort: Find better-paying job or promotion + Realistic timeframe: 3-6 months + +DISADVANTAGED GROUP (Women): + Required change: Increase income by $12,500 + Effort: Need dual income or career change + Realistic timeframe: 6-12 months + +CONCLUSION: White applicants need 40% less income increase to flip their decision, revealing a structural bias in the model's treatment of demographic groups with different economic opportunities. +``` + +## Detection/Implementation Code + +A minimal actionable-recourse search that only considers realistic, feasible changes: + +```python +import numpy as np +import pandas as pd +from sklearn.ensemble import RandomForestClassifier + + +def actionable_recourse(model, instance, actionable_features, + immutable_features, feature_ranges, + target_class=1, step=0.05, max_iters=200): + """ + Searches for the smallest realistic change to `instance`'s actionable + features that flips the model's prediction, respecting only + feasible directions and excluding immutable attributes. + + Parameters: + model: fitted classifier with .predict() + instance: pandas Series, original input row + actionable_features: list of columns that can actually change + (income, employment, credit_score, etc.) + immutable_features: list of columns that cannot change + (age, race, protected attributes, history) + feature_ranges: {column: (min, max)} for each actionable feature + target_class: desired predicted class + step: fraction of each feature's range to perturb per iteration + max_iters: number of candidate counterfactuals to try + + Returns: + The closest successful actionable recourse found (Series), or + None if no feasible change flips the prediction. + """ + rng = np.random.default_rng(42) + best = None + best_distance = float("inf") + + for _ in range(max_iters): + candidate = instance.copy() + if pd.api.types.is_integer_dtype(candidate): + candidate = candidate.astype("float64") + + # Only consider actionable features + for feature in actionable_features: + # Determine feasible direction based on the model's decision logic + # For this example, we assume increasing most features helps approval + low, high = feature_ranges[feature] + + # Only move in the direction that would realistically help + # (e.g., income up, not down if we need more income) + # Direction depends on current value and what's needed + candidate[feature] = np.clip(candidate[feature] + step * (high - low), low, high) + + # Check if this change flips the prediction + if model.predict(pd.DataFrame([candidate]))[0] == target_class: + # Calculate distance only over actionable features + distance = sum( + abs(candidate[f] - instance[f]) / (feature_ranges[f][1] - feature_ranges[f][0]) + for f in actionable_features + ) + if distance < best_distance: + best, best_distance = candidate, distance + + return best + + +# Usage example - Benefits Denial audit: +# Define which features are actionable vs immutable + +# Actionable features (what a person can change): +# - income, employment, marital.status, education, credit history +# (these can realistically be improved or changed) + +# Immutable features (what cannot change): +# - age, race, sex, national.origin (these are fixed characteristics) + +# Define realistic ranges for actionable features +# (based on observed data in the audit) +feature_ranges = { + "income": (0, 100000), + "employment_hours": (0, 80), + "credit_score": (300, 850), + # ... other actionable features +} + +# Example usage with a denied application +# actionable_recourse = actionable_recourse( +# model, denied_applicant, +# actionable_features=["income", "employment_hours", "credit_score"], +# immutable_features=["age", "race", "sex"], +# feature_ranges=feature_ranges, +# target_class=0 # switch from ineligible to eligible +# ) + +if actionable_recourse is not None: + print("Actionable recourse found:", actionable_recourse[[ + "income", "employment_hours", "credit_score" + ]]) +``` + +## Real Implementation: Comparing Groups + +Here's how the actionable-recourse search compares different demographic groups: + +```python +# Define which groups to compare +comparison_groups = [ + {"name": "White men", "is_female": 0, "is_minority": 0}, + {"name": "Women", "is_female": 1, "is_minority": 0}, + {"name": "Minority women", "is_female": 1, "is_minority": 1}, +] + +# Run actionable recourse search for each group +results = {} +for group in comparison_groups: + group_instance = create_applicant_instance( + income=30000, employment="part-time", age=25, + is_female=group["is_female"], is_minority=group["is_minority"] + ) + recourse = actionable_recourse( + model, group_instance, + actionable_features=["income", "employment_hours"], + immutable_features=["age", "is_female", "is_minority"], + feature_ranges={"income": (0, 80000), "employment_hours": (0, 60)}, + target_class=0 # from denied to approved + ) + results[group["name"]] = { + "income_increase_needed": recourse["income"] - 30000 if recourse is not None else None, + "hours_increase_needed": recourse["employment_hours"] - 20 if recourse is not None else None, + "effort_level": "low" if recourse else "very_high" + } + +# Display results +for group, outcome in results.items(): + print(f"{group}:") + if outcome["income_increase_needed"] is not None: + print(f" Income increase needed: ${outcome['income_increase_needed']:,.0f}") + print(f" Effort level: {outcome['effort_level']}") + else: + print(f" Cannot flip decision with current feature constraints") +``` + +## Limitations + +### 1. Actionability Constraints Are Value Judgments + +Defining what is "actionable" and "feasible" requires making judgments about what socioeconomic changes are realistic. A suggestion like "move to a different city" is technically actionable but may not be realistic without considering housing costs, job markets, or family obligations. + +### 2. The Nearest Actionable Recourse May Not Be the Most Useful + +Different search algorithms can return different valid actionable recourses. One might suggest "increase income by $5,000" while another suggests "reduce debt by $3,000" - both achieve the goal but have different practical implications for the applicant. + +### 3. It Explains One Decision, Not Systemic Fairness + +Like counterfactual explanations, actionable recourse is local to a single prediction. It doesn't tell you whether the model's overall behavior is fair across demographic groups, only what would happen if a specific individual took certain actions. + +### 4. The Suggested Change Can Still Indirectly Discriminate + +If all suggested actionable recourses for one group are consistently more difficult or expensive than those for another group, that reveals a deeper structural bias beyond standard parity metrics. + +## Related Concepts + +* [Counterfactual Explanation](counterfactual-explanation.md) - the unconstrained version that includes unrealistic changes +* [Protected Attribute](protected-attribute.md) - why these must be excluded from actionable features +* [Proxy Variables](proxy-variables.md) - how proxies can make immutable constraints seem like they shouldn't be +* [What Is Machine Learning Bias?](ml-bias.md) - how actionability constraints reveal real-world inequities + +## Related Projects in This Repo + +* [`Benefits Denial/`](#) - the audit used for the actionable recourse example above +* [`Open Dataset Profiler`](#) - validation tool for the actionable-recourse detection code +* [`Cross-Domain Benchmark Harness`](#) - shows how recourse costs compare across different audits + +## Further Reading + +* [Ustun, B., Spangher, A., Liu, Y. (2019): Actionable Recourse in Linear Classification](https://arxiv.org/abs/1907.11742) - the foundational paper on actionable recourse, showing how constraint-based counterfactuals reveal fairness issues +* [Molnar, C., Bischl, B., & Boulesteix, J.-F. (2020): Surrogates for Model Interpretation](https://arxiv.org/abs/1905.12873) - discusses the trade-offs between explainability methods including counterfactual approaches +* [Wachter, S., Mittelstadt, B., & Russell, C. (2017): Counterfactual Explanations Without Opening the Black Box](https://arxiv.org/abs/1711.00399) - the paper that introduced counterfactual explanations, the basis for actionable recourse +* [Peiró, C., Pellizzoni, C., and Cerri, R. (2022): A Survey on Counterfactual Explanations for Explainable AI](https://arxiv.org/abs/2203.12574) - comprehensive review of counterfactual methods and their actionability constraints + +--- +*Part of [The Fair Code Project](https://instagram.com/thefaircodeproject) - exposing and fixing algorithmic bias with real data and open code.* + diff --git a/sitemap.xml b/sitemap.xml index 4d425f94..3ae85bfa 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,7 +2,7 @@ https://www.thefaircode.xyz/ - 2026-09-26 + 2026-09-27 https://www.thefaircode.xyz/profiler.html @@ -252,4 +252,7 @@ https://www.thefaircode.xyz/explainers/reject-option-classification.html 2026-09-10 + + https://www.thefaircode.xyz/explainers/algorithmic-recourse.html +