Skip to content

Commit 93f93ab

Browse files
committed
Regenerate explainer pages after the simpsons-paradox/CDP/subgroup-fairness content fixes
make build-explainers after the 3 preceding content-fix commits. Also picks up first-commit datePublished/dateModified for all 4 explainers added in #541 (git-log-derived; couldn't resolve until those files had a real commit history, the same one-time chicken-and-egg noted in check_generated_files_current.py's own docstring).
1 parent 3f57130 commit 93f93ab

17 files changed

Lines changed: 55 additions & 43 deletions
1.33 KB
Loading
1.62 KB
Loading
1.58 KB
Loading
1.3 KB
Loading
1.13 KB
Loading

assets/og/differential-privacy.png

1.38 KB
Loading

assets/og/simpsons-paradox.png

1.28 KB
Loading

assets/og/subgroup-fairness.png

1.17 KB
Loading

explainers/conditional-demographic-parity.html

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@
110110
"@type": "DefinedTermSet",
111111
"name": "Fair Code Explainers",
112112
"url": "https://www.thefaircode.xyz/index.html#explainers"
113-
}
113+
},
114+
"datePublished": "2026-09-09",
115+
"dateModified": "2026-09-09"
114116
},
115117
{
116118
"@type": "FAQPage",
@@ -204,17 +206,17 @@ <h3 id="how-it-works">How It Works</h3>
204206
<p>The aggregate rate is a weighted average of within-stratum rates, weighted by how group <code>g</code> is distributed across the strata of <code>L</code>. Subtracting the two groups&#x27; aggregate rates gives a gap with two sources: different within-stratum rates (<code>P(Y_hat = 1 | G = g, L = l)</code> differs by <code>g</code>), and different stratum weights (<code>P(L = l | G = g)</code> differs by <code>g</code>).</p>
205207
<p>Conditional demographic parity zeroes out the second source by construction: it only compares groups at the same value of <code>L</code>. What is left is the within-stratum gap. A sample-size-weighted average of the within-stratum gaps is the part of the aggregate gap that stratifying on <code>L</code> does <em>not</em> remove.</p>
206208
<p>The catch is entirely in the choice of <code>L</code>. If <code>L</code> is a genuine exogenous factor, the within-stratum gap is the disparity net of a real confounder. If <code>L</code> is a <a href="proxy-variables.html">proxy</a> for the protected attribute, conditioning on it absorbs the discrimination into the &quot;legitimate&quot; term and the within-stratum gap understates the real harm. CDP gives you a knob; it does not tell you where to set it.</p>
207-
<h3 id="concrete-example-benefits-denial-audit-04">Concrete Example: Benefits Denial - Audit 04</h3>
209+
<h3 id="concrete-example-benefits-denial-audit-05">Concrete Example: Benefits Denial - Audit 05</h3>
208210
<p><code>Benefits Denial/</code> audits the Adult Census Income dataset (<code>adult.csv</code>, 32,561 rows). The target is <code>income == &#x27;&gt;50K&#x27;</code> and <code>sex</code> is a declared protected attribute. Using the dataset&#x27;s own outcome rates (the base rate a label-level demographic parity check compares):</p>
209211
<p><strong>Unconditional:</strong></p>
210-
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Group</th><th>n</th><th>P(income &gt; 50K)</th></tr></thead><tbody><tr><td>Male</td><td>21,790</td><td>30.6%</td></tr><tr><td>Female</td><td>10,771</td><td>11.0%</td></tr><tr><td>Gap (M - F)</td><td></td><td><strong>+19.6 pp</strong></td></tr></tbody></table></div>
212+
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Group</th><th>n</th><th>P(income &gt; 50K)</th></tr></thead><tbody><tr><td>Male</td><td>21,790</td><td>30.6%</td></tr><tr><td>Female</td><td>10,771</td><td>10.9%</td></tr><tr><td>Gap (M - F)</td><td></td><td><strong>+19.6 pp</strong></td></tr></tbody></table></div>
211213
<p>Now apply CDP with two different choices of legitimate factor <code>L</code>.</p>
212-
<h4 id="l-education-level-education-num-15-levels">L = education level (<code>education.num</code>, 15 levels)</h4>
213-
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>education.num</th><th>n</th><th>Gap (M - F)</th></tr></thead><tbody><tr><td>9 (HS grad)</td><td>10,501</td><td>+13.7 pp</td></tr><tr><td>10 (some college)</td><td>7,291</td><td>+19.5 pp</td></tr><tr><td>13 (Bachelors)</td><td>5,355</td><td>+29.4 pp</td></tr><tr><td>14 (Masters)</td><td>1,723</td><td>+32.3 pp</td></tr><tr><td>15 (Prof-school)</td><td>576</td><td>+33.1 pp</td></tr><tr><td>16 (Doctorate)</td><td>413</td><td>+20.2 pp</td></tr></tbody></table></div>
214+
<h4 id="l-education-level-education-num-16-levels">L = education level (<code>education.num</code>, 16 levels)</h4>
215+
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>education.num</th><th>n</th><th>Gap (M - F)</th></tr></thead><tbody><tr><td>9 (HS grad)</td><td>10,501</td><td>+13.7 pp</td></tr><tr><td>10 (some college)</td><td>7,291</td><td>+19.5 pp</td></tr><tr><td>13 (Bachelors)</td><td>5,355</td><td>+29.4 pp</td></tr><tr><td>14 (Masters)</td><td>1,723</td><td>+32.3 pp</td></tr><tr><td>15 (Prof-school)</td><td>576</td><td>+33.1 pp</td></tr><tr><td>16 (Doctorate)</td><td>413</td><td>+20.1 pp</td></tr></tbody></table></div>
214216
<p>Sample-weighted within-stratum gap: <strong>+18.4 pp</strong>. Conditioning on education removes almost none of the aggregate gap - within every education level men are markedly more likely to be high earners, and at the top three levels the gap is <em>larger</em> than the unconditional +19.6. CDP conditioned on education says: the disparity is not an artifact of women being less educated in this dataset.</p>
215217
<h4 id="l-marital-status-marital-status">L = marital status (<code>marital.status</code>)</h4>
216218
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>marital.status</th><th>n</th><th>Gap (M - F)</th></tr></thead><tbody><tr><td>Married-civ-spouse</td><td>14,976</td><td>-0.9 pp</td></tr><tr><td>Never-married</td><td>10,683</td><td>+2.0 pp</td></tr><tr><td>Divorced</td><td>4,443</td><td>+9.3 pp</td></tr><tr><td>Widowed</td><td>993</td><td>+17.6 pp</td></tr></tbody></table></div>
217-
<p>Sample-weighted within-stratum gap: roughly <strong>+3 pp</strong>, and inside the single largest stratum the sign reverses (women 0.9 points ahead). Conditioning on marital status makes most of the aggregate gap disappear.</p>
219+
<p>Sample-weighted within-stratum gap: roughly <strong>+2 pp</strong>, and inside the single largest stratum the sign reverses (women 0.9 points ahead). Conditioning on marital status makes most of the aggregate gap disappear.</p>
218220
<h4 id="the-two-answers-disagree-and-that-is-the-point">The two answers disagree, and that is the point</h4>
219221
<p>Same protected attribute, same dataset, same +19.6 pp aggregate. Condition on education and the gap stands. Condition on marital status and it nearly vanishes. CDP did not resolve the fairness question - it moved it to a new one: <em>is marital status a legitimate basis for a 20-point income difference between men and women, or is it a proxy for a sex-role division of unpaid labor that the outcome variable should not be conditioned on?</em> That is a normative and legal question, not a statistical one, and CDP&#x27;s output is only as defensible as the answer to it. (This is the same aggregate-vs-stratified divergence covered from the other direction in <a href="simpsons-paradox.html">Simpson&#x27;s Paradox</a>.)</p>
220222
<pre><code class="language-python">import pandas as pd
@@ -241,7 +243,7 @@ <h4 id="the-two-answers-disagree-and-that-is-the-point">The two answers disagree
241243
_, w = within_stratum_gap(df, col)
242244
print(f&quot;condition on {col:16s}: weighted within-stratum gap = {w:+.4f}&quot;)
243245
# condition on education.num : weighted within-stratum gap = +0.1844
244-
# condition on marital.status : weighted within-stratum gap = +0.0305</code></pre>
246+
# condition on marital.status : weighted within-stratum gap = +0.0243</code></pre>
245247
<h3 id="detection-code">Detection Code</h3>
246248
<p>The following module computes the unconditional demographic parity gap, the conditional (within-stratum) gap for a chosen legitimate factor, and the fraction of the unconditional gap that conditioning explains away.</p>
247249
<pre><code class="language-python">import numpy as np
@@ -333,7 +335,7 @@ <h3 id="detection-code">Detection Code</h3>
333335
# print_cdp_report(conditional_demographic_parity(
334336
# df, &quot;high_income&quot;, &quot;sex&quot;, &quot;Male&quot;, &quot;Female&quot;, [&quot;marital.status&quot;]),
335337
# &quot;marital.status&quot;)</code></pre>
336-
<p>Against <code>Benefits Denial/adult.csv</code> this reports an <code>explained_fraction</code> near <code>0.06</code> for <code>education.num</code> (conditioning removes almost nothing) and near <code>0.84</code> for <code>marital.status</code> (conditioning removes most of the gap) - the same +19.6 pp aggregate, two incompatible readings.</p>
338+
<p>Against <code>Benefits Denial/adult.csv</code> this reports an <code>explained_fraction</code> near <code>0.06</code> for <code>education.num</code> (conditioning removes almost nothing) and near <code>0.88</code> for <code>marital.status</code> (conditioning removes most of the gap) - the same +19.6 pp aggregate, two incompatible readings.</p>
337339
<h3 id="limitations-and-trade-offs">Limitations and Trade-offs</h3>
338340
<h4 id="1-the-conditioning-set-is-a-value-judgment-not-a-statistical-choice">1. The conditioning set is a value judgment, not a statistical choice</h4>
339341
<p>Everything CDP reports is downstream of which factors you declared &quot;legitimate&quot;. There is no test that tells you whether marital status, or occupation, or prior loan history, is a fair thing to condition on. Pick a factor that is genuinely a proxy for the protected attribute and CDP will confidently report that the disparity is &quot;explained&quot;, which is exactly the <a href="proxy-variables.html">proxy-variable</a> trap.</p>

explainers/differential-privacy.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@
110110
"@type": "DefinedTermSet",
111111
"name": "Fair Code Explainers",
112112
"url": "https://www.thefaircode.xyz/index.html#explainers"
113-
}
113+
},
114+
"datePublished": "2026-09-09",
115+
"dateModified": "2026-09-09"
114116
},
115117
{
116118
"@type": "FAQPage",

0 commit comments

Comments
 (0)