Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions explainers/demographic-parity.html
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,10 @@ <h3 id="the-formal-definition">The Formal Definition</h3>
<h3 id="real-world-proof-hiring-bias">Real-World Proof: Hiring Bias</h3>
<p>The AI Fair Recruitment audit in this repo is a direct illustration of a demographic parity violation - and its fix.</p>
<p>A model trained with gender and age as features assigned hire recommendations at sharply different rates:</p>
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Group</th><th>Hire Rate</th></tr></thead><tbody><tr><td>Male applicants</td><td>~71%</td></tr><tr><td>Female applicants</td><td>~50%</td></tr><tr><td><strong>Fairness Gap</strong></td><td><strong>~20.9 percentage points</strong></td></tr></tbody></table></div>
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Group</th><th>Hire Rate</th></tr></thead><tbody><tr><td>Male applicants</td><td>21.62%</td></tr><tr><td>Female applicants</td><td>17.10%</td></tr><tr><td><strong>Fairness Gap</strong></td><td><strong>4.51 percentage points</strong></td></tr></tbody></table></div>
<p>(The disparate-impact ratio here is 17.10 / 21.62 = 0.79, below the 0.80 four-fifths threshold.)</p>
<p>The model was not told to discriminate. It learned to - by treating age as a proxy for gender, because women in the dataset more often had career gaps. Age was correlated with gender, so including it smuggled the gender signal back in even without an explicit gender rule.</p>
<p>After dropping gender and age (the protected attribute and its proxy):</p>
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Group</th><th>Hire Rate</th></tr></thead><tbody><tr><td>Male applicants</td><td>~67%</td></tr><tr><td>Female applicants</td><td>~67%</td></tr><tr><td><strong>New Fairness Gap</strong></td><td><strong>~0.12 percentage points</strong></td></tr></tbody></table></div>
<p><strong>97.3% reduction.</strong> The gap wasn&#x27;t in the underlying merit of candidates - it was in which features the model was permitted to see.</p>
<p>After dropping gender and age (the protected attribute and its proxy), the gap closes to <strong>0.12 percentage points</strong> - a <strong>97.3% reduction</strong>. The gap wasn&#x27;t in the underlying merit of candidates - it was in which features the model was permitted to see.</p>
<hr>
<h3 id="detection-code">Detection Code</h3>
<h4 id="measure-demographic-parity-gap">Measure demographic parity gap</h4>
Expand Down
18 changes: 6 additions & 12 deletions explainers/demographic-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,15 @@ A model trained with gender and age as features assigned hire recommendations at

| Group | Hire Rate |
|---|---|
| Male applicants | ~71% |
| Female applicants | ~50% |
| **Fairness Gap** | **~20.9 percentage points** |
| Male applicants | 21.62% |
| Female applicants | 17.10% |
| **Fairness Gap** | **4.51 percentage points** |

The model was not told to discriminate. It learned to - by treating age as a proxy for gender, because women in the dataset more often had career gaps. Age was correlated with gender, so including it smuggled the gender signal back in even without an explicit gender rule.

After dropping gender and age (the protected attribute and its proxy):
(The disparate-impact ratio here is 17.10 / 21.62 = 0.79, below the 0.80 four-fifths threshold.)

| Group | Hire Rate |
|---|---|
| Male applicants | ~67% |
| Female applicants | ~67% |
| **New Fairness Gap** | **~0.12 percentage points** |
The model was not told to discriminate. It learned to - by treating age as a proxy for gender, because women in the dataset more often had career gaps. Age was correlated with gender, so including it smuggled the gender signal back in even without an explicit gender rule.

**97.3% reduction.** The gap wasn't in the underlying merit of candidates - it was in which features the model was permitted to see.
After dropping gender and age (the protected attribute and its proxy), the gap closes to **0.12 percentage points** - a **97.3% reduction**. The gap wasn't in the underlying merit of candidates - it was in which features the model was permitted to see.

---

Expand Down
2 changes: 1 addition & 1 deletion explainers/disparate-treatment.html
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ <h4 id="ai-fair-recruitment-unfair-py">AI Fair Recruitment - <code>unfair.py</co
<pre><code class="language-python"># DISPARATE TREATMENT: Gender and Age are direct model features
features = [&#x27;Gender&#x27;, &#x27;Age&#x27;, &#x27;Experience_Years&#x27;, &#x27;Technical_Test_Score&#x27;,
&#x27;Education_Level&#x27;, &#x27;Previous_Companies&#x27;, &#x27;Distance_from_Company&#x27;]</code></pre>
<p><code>Gender</code> is a direct input. <code>Age</code> is both an input and a proxy for gender (women in the dataset more often have career gaps, so age encodes gender signal twice over - once directly, once through correlation). The model was <em>designed</em> to see these attributes. The 20.9pp hire rate gap is the disparate impact.</p>
<p><code>Gender</code> is a direct input. <code>Age</code> is both an input and a proxy for gender (women in the dataset more often have career gaps, so age encodes gender signal twice over - once directly, once through correlation). The model was <em>designed</em> to see these attributes. The 4.51pp hire rate gap (21.62% vs 17.10%) is the disparate impact.</p>
<h4 id="the-fix-what-removing-disparate-treatment-looks-like">The Fix - What Removing Disparate Treatment Looks Like</h4>
<pre><code class="language-python"># fair.py: protected attribute and its proxy removed
features = [&#x27;Experience_Years&#x27;, &#x27;Technical_Test_Score&#x27;]
Expand Down
2 changes: 1 addition & 1 deletion explainers/disparate-treatment.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ features = ['Gender', 'Age', 'Experience_Years', 'Technical_Test_Score',
'Education_Level', 'Previous_Companies', 'Distance_from_Company']
```

`Gender` is a direct input. `Age` is both an input and a proxy for gender (women in the dataset more often have career gaps, so age encodes gender signal twice over - once directly, once through correlation). The model was *designed* to see these attributes. The 20.9pp hire rate gap is the disparate impact.
`Gender` is a direct input. `Age` is both an input and a proxy for gender (women in the dataset more often have career gaps, so age encodes gender signal twice over - once directly, once through correlation). The model was *designed* to see these attributes. The 4.51pp hire rate gap (21.62% vs 17.10%) is the disparate impact.

### The Fix - What Removing Disparate Treatment Looks Like

Expand Down
7 changes: 3 additions & 4 deletions explainers/neural-networks.html
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ <h4 id="what-we-did">What We Did</h4>
&#x27;communication_score&#x27;
]</code></pre>
<p><strong>Results:</strong></p>
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Group</th><th>Hire Rate</th></tr></thead><tbody><tr><td>Male candidates</td><td>61.2%</td></tr><tr><td>Female candidates</td><td>40.3%</td></tr><tr><td><strong>Fairness gap</strong></td><td><strong>20.9%</strong></td></tr></tbody></table></div>
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Group</th><th>Hire Rate</th></tr></thead><tbody><tr><td>Male candidates</td><td>21.62%</td></tr><tr><td>Female candidates</td><td>17.10%</td></tr><tr><td><strong>Fairness gap</strong></td><td><strong>4.51 percentage points</strong></td></tr></tbody></table></div>
<p>The network didn&#x27;t contain a rule that said &quot;prefer men.&quot; It learned from historical hiring data in which men were hired more. The weights encoded that pattern. The bias was invisible - buried in floating-point numbers across hidden layers.</p>
<hr>
<p><strong>Step 2 - Remove gender + proxy (our fix):</strong></p>
Expand All @@ -296,10 +296,9 @@ <h4 id="what-we-did">What We Did</h4>
&#x27;technical_score&#x27;,
&#x27;communication_score&#x27;
]</code></pre>
<p><strong>Results:</strong></p>
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Group</th><th>Hire Rate</th></tr></thead><tbody><tr><td>Male candidates</td><td>54.1%</td></tr><tr><td>Female candidates</td><td>54.0%</td></tr><tr><td><strong>Fairness gap</strong></td><td><strong>0.1%</strong></td></tr></tbody></table></div>
<p><strong>Result:</strong> the fairness gap closes to <strong>0.12 percentage points</strong>.</p>
<h4 id="summary">Summary</h4>
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Approach</th><th>Fairness Gap</th><th>Reduction</th></tr></thead><tbody><tr><td>Biased model</td><td>20.9%</td><td>-</td></tr><tr><td>Remove gender only</td><td>~18%</td><td>Minimal</td></tr><tr><td>Remove gender + proxy</td><td>0.1%</td><td><strong>99.5%</strong></td></tr></tbody></table></div>
<div class="explainer-table-wrap"><table class="explainer-table"><thead><tr><th>Approach</th><th>Fairness Gap</th><th>Reduction</th></tr></thead><tbody><tr><td>Biased model</td><td>4.51%</td><td>-</td></tr><tr><td>Remove gender only</td><td>barely moves (age still proxies it)</td><td>Minimal</td></tr><tr><td>Remove gender + proxy</td><td>0.12%</td><td><strong>97.3%</strong></td></tr></tbody></table></div>
<p><strong>The network&#x27;s architecture didn&#x27;t change. The training procedure didn&#x27;t change. Only the inputs changed - and the bias disappeared.</strong></p>
<hr>
<h3 id="how-to-inspect-what-a-network-learned">How to Inspect What a Network Learned</h3>
Expand Down
20 changes: 7 additions & 13 deletions explainers/neural-networks.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ features = [

| Group | Hire Rate |
|---|---|
| Male candidates | 61.2% |
| Female candidates | 40.3% |
| **Fairness gap** | **20.9%** |
| Male candidates | 21.62% |
| Female candidates | 17.10% |
| **Fairness gap** | **4.51 percentage points** |

The network didn't contain a rule that said "prefer men." It learned from historical hiring data in which men were hired more. The weights encoded that pattern. The bias was invisible - buried in floating-point numbers across hidden layers.

Expand All @@ -172,21 +172,15 @@ features = [
]
```

**Results:**

| Group | Hire Rate |
|---|---|
| Male candidates | 54.1% |
| Female candidates | 54.0% |
| **Fairness gap** | **0.1%** |
**Result:** the fairness gap closes to **0.12 percentage points**.

### Summary

| Approach | Fairness Gap | Reduction |
|---|---|---|
| Biased model | 20.9% | - |
| Remove gender only | ~18% | Minimal |
| Remove gender + proxy | 0.1% | **99.5%** |
| Biased model | 4.51% | - |
| Remove gender only | barely moves (age still proxies it) | Minimal |
| Remove gender + proxy | 0.12% | **97.3%** |

**The network's architecture didn't change. The training procedure didn't change. Only the inputs changed - and the bias disappeared.**

Expand Down
18 changes: 6 additions & 12 deletions faircode/_explainers/demographic-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,15 @@ A model trained with gender and age as features assigned hire recommendations at

| Group | Hire Rate |
|---|---|
| Male applicants | ~71% |
| Female applicants | ~50% |
| **Fairness Gap** | **~20.9 percentage points** |
| Male applicants | 21.62% |
| Female applicants | 17.10% |
| **Fairness Gap** | **4.51 percentage points** |

The model was not told to discriminate. It learned to - by treating age as a proxy for gender, because women in the dataset more often had career gaps. Age was correlated with gender, so including it smuggled the gender signal back in even without an explicit gender rule.

After dropping gender and age (the protected attribute and its proxy):
(The disparate-impact ratio here is 17.10 / 21.62 = 0.79, below the 0.80 four-fifths threshold.)

| Group | Hire Rate |
|---|---|
| Male applicants | ~67% |
| Female applicants | ~67% |
| **New Fairness Gap** | **~0.12 percentage points** |
The model was not told to discriminate. It learned to - by treating age as a proxy for gender, because women in the dataset more often had career gaps. Age was correlated with gender, so including it smuggled the gender signal back in even without an explicit gender rule.

**97.3% reduction.** The gap wasn't in the underlying merit of candidates - it was in which features the model was permitted to see.
After dropping gender and age (the protected attribute and its proxy), the gap closes to **0.12 percentage points** - a **97.3% reduction**. The gap wasn't in the underlying merit of candidates - it was in which features the model was permitted to see.

---

Expand Down
2 changes: 1 addition & 1 deletion faircode/_explainers/disparate-treatment.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ features = ['Gender', 'Age', 'Experience_Years', 'Technical_Test_Score',
'Education_Level', 'Previous_Companies', 'Distance_from_Company']
```

`Gender` is a direct input. `Age` is both an input and a proxy for gender (women in the dataset more often have career gaps, so age encodes gender signal twice over - once directly, once through correlation). The model was *designed* to see these attributes. The 20.9pp hire rate gap is the disparate impact.
`Gender` is a direct input. `Age` is both an input and a proxy for gender (women in the dataset more often have career gaps, so age encodes gender signal twice over - once directly, once through correlation). The model was *designed* to see these attributes. The 4.51pp hire rate gap (21.62% vs 17.10%) is the disparate impact.

### The Fix - What Removing Disparate Treatment Looks Like

Expand Down
20 changes: 7 additions & 13 deletions faircode/_explainers/neural-networks.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ features = [

| Group | Hire Rate |
|---|---|
| Male candidates | 61.2% |
| Female candidates | 40.3% |
| **Fairness gap** | **20.9%** |
| Male candidates | 21.62% |
| Female candidates | 17.10% |
| **Fairness gap** | **4.51 percentage points** |

The network didn't contain a rule that said "prefer men." It learned from historical hiring data in which men were hired more. The weights encoded that pattern. The bias was invisible - buried in floating-point numbers across hidden layers.

Expand All @@ -172,21 +172,15 @@ features = [
]
```

**Results:**

| Group | Hire Rate |
|---|---|
| Male candidates | 54.1% |
| Female candidates | 54.0% |
| **Fairness gap** | **0.1%** |
**Result:** the fairness gap closes to **0.12 percentage points**.

### Summary

| Approach | Fairness Gap | Reduction |
|---|---|---|
| Biased model | 20.9% | - |
| Remove gender only | ~18% | Minimal |
| Remove gender + proxy | 0.1% | **99.5%** |
| Biased model | 4.51% | - |
| Remove gender only | barely moves (age still proxies it) | Minimal |
| Remove gender + proxy | 0.12% | **97.3%** |

**The network's architecture didn't change. The training procedure didn't change. Only the inputs changed - and the bias disappeared.**

Expand Down
40 changes: 14 additions & 26 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1207,7 +1207,7 @@ features = ['Gender', 'Age', 'Experience_Years', 'Technical_Test_Score',
'Education_Level', 'Previous_Companies', 'Distance_from_Company']
```

`Gender` is a direct input. `Age` is both an input and a proxy for gender (women in the dataset more often have career gaps, so age encodes gender signal twice over - once directly, once through correlation). The model was *designed* to see these attributes. The 20.9pp hire rate gap is the disparate impact.
`Gender` is a direct input. `Age` is both an input and a proxy for gender (women in the dataset more often have career gaps, so age encodes gender signal twice over - once directly, once through correlation). The model was *designed* to see these attributes. The 4.51pp hire rate gap (21.62% vs 17.10%) is the disparate impact.

### The Fix - What Removing Disparate Treatment Looks Like

Expand Down Expand Up @@ -1992,21 +1992,15 @@ A model trained with gender and age as features assigned hire recommendations at

| Group | Hire Rate |
|---|---|
| Male applicants | ~71% |
| Female applicants | ~50% |
| **Fairness Gap** | **~20.9 percentage points** |
| Male applicants | 21.62% |
| Female applicants | 17.10% |
| **Fairness Gap** | **4.51 percentage points** |

The model was not told to discriminate. It learned to - by treating age as a proxy for gender, because women in the dataset more often had career gaps. Age was correlated with gender, so including it smuggled the gender signal back in even without an explicit gender rule.

After dropping gender and age (the protected attribute and its proxy):
(The disparate-impact ratio here is 17.10 / 21.62 = 0.79, below the 0.80 four-fifths threshold.)

| Group | Hire Rate |
|---|---|
| Male applicants | ~67% |
| Female applicants | ~67% |
| **New Fairness Gap** | **~0.12 percentage points** |
The model was not told to discriminate. It learned to - by treating age as a proxy for gender, because women in the dataset more often had career gaps. Age was correlated with gender, so including it smuggled the gender signal back in even without an explicit gender rule.

**97.3% reduction.** The gap wasn't in the underlying merit of candidates - it was in which features the model was permitted to see.
After dropping gender and age (the protected attribute and its proxy), the gap closes to **0.12 percentage points** - a **97.3% reduction**. The gap wasn't in the underlying merit of candidates - it was in which features the model was permitted to see.

---

Expand Down Expand Up @@ -3728,9 +3722,9 @@ features = [

| Group | Hire Rate |
|---|---|
| Male candidates | 61.2% |
| Female candidates | 40.3% |
| **Fairness gap** | **20.9%** |
| Male candidates | 21.62% |
| Female candidates | 17.10% |
| **Fairness gap** | **4.51 percentage points** |

The network didn't contain a rule that said "prefer men." It learned from historical hiring data in which men were hired more. The weights encoded that pattern. The bias was invisible - buried in floating-point numbers across hidden layers.

Expand All @@ -3748,21 +3742,15 @@ features = [
]
```

**Results:**

| Group | Hire Rate |
|---|---|
| Male candidates | 54.1% |
| Female candidates | 54.0% |
| **Fairness gap** | **0.1%** |
**Result:** the fairness gap closes to **0.12 percentage points**.

### Summary

| Approach | Fairness Gap | Reduction |
|---|---|---|
| Biased model | 20.9% | - |
| Remove gender only | ~18% | Minimal |
| Remove gender + proxy | 0.1% | **99.5%** |
| Biased model | 4.51% | - |
| Remove gender only | barely moves (age still proxies it) | Minimal |
| Remove gender + proxy | 0.12% | **97.3%** |

**The network's architecture didn't change. The training procedure didn't change. Only the inputs changed - and the bias disappeared.**

Expand Down