Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repos:

- id: ruff
name: ruff (unused imports, undefined names, redefinitions)
entry: ruff check faircode scripts tests
entry: ruff check faircode scripts tests explainers/code
language: system
pass_filenames: false
always_run: true
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ lint: ## Enforce the em-dash-free rule + check for broken doc links + issue num
$(PY) scripts/check_explainer_structure.py
$(PY) scripts/check_traction.py
$(PY) scripts/check_contributors.py
ruff check faircode scripts tests explainers/code
ruff check faircode scripts tests

audits: ## Run every audit's unfair.py/fair.py + the CLI smoke tests (mirrors audits.yml's required run-audits job, plus profiler/benchmark-harness's CLI steps)
Expand Down
1 change: 0 additions & 1 deletion explainers/aggregation-bias.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ Two cautions. This model has only three features, so its absolute accuracy is lo
Run from the repo root (`PYTHONPATH=. python3 explainers/code/aggregation-bias.py`; that file is generated from the block below).

```python
import numpy as np
from sklearn.linear_model import LogisticRegression
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness

Expand Down
1 change: 0 additions & 1 deletion explainers/code/aggregation-bias.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Generated from explainers/aggregation-bias.md by scripts/build_explainers.py. Edit the markdown, not this file.
import numpy as np
from sklearn.linear_model import LogisticRegression
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness

Expand Down
1 change: 0 additions & 1 deletion explainers/code/delayed-feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def monitored_gap(mean_delay_group, mean_delay_others, today=90):
delay = np.minimum(delay, 29.9)
seen_positive = (y == 1) & (discharge + delay <= today)
window_closed = discharge + 30 <= today
known_negative = (y == 0) & window_closed
# a naive monitor counts every discharged patient and treats every outcome it has not seen as "no"
naive = {g: seen_positive[group == g].mean() for g in (True, False)}
# a careful monitor only counts patients whose 30-day window is closed
Expand Down
2 changes: 1 addition & 1 deletion explainers/code/fairness-in-ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def report(score, name):
print(f"{name}\n female share of the top {K}: {female[top].mean():.3f} (pool {female.mean():.3f})"
f"\n exposure ratio female/male, whole list: {whole[female == 1].mean() / whole[female == 0].mean():.3f}"
f"\n exposure ratio female/male, top {K} only: {cut[female == 1].mean() / cut[female == 0].mean():.3f}"
f"\n exposure per unit of hire rate, female/male: "
"\n exposure per unit of hire rate, female/male: "
f"{(cut[female == 1].mean() / hired[female == 1].mean()) / (cut[female == 0].mean() / hired[female == 0].mean()):.3f}")

report(scores(X_biased), "baseline (Gender is a feature)")
Expand Down
1 change: 0 additions & 1 deletion explainers/code/feature-importance-by-group.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Generated from explainers/feature-importance-by-group.md by scripts/build_explainers.py. Edit the markdown, not this file.
import numpy as np
from sklearn.inspection import permutation_importance
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness
from faircode.models import build_model
Expand Down
1 change: 0 additions & 1 deletion explainers/code/hiring-pipeline-adverse-impact.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
def ratio_with_interval(passed, reached, n_boot=500):
"""Women's selection rate divided by everyone else's, among the people who reached the stage, with a bootstrap interval."""
w, m = reached & (women == 1), reached & (women == 0)
rate = lambda mask, rows: passed[rows][mask[rows]].mean()
point = passed[w].mean() / passed[m].mean()
idx_w, idx_m = np.flatnonzero(w), np.flatnonzero(m)
boots = [passed[rng.choice(idx_w, len(idx_w))].mean() / passed[rng.choice(idx_m, len(idx_m))].mean() for _ in range(n_boot)]
Expand Down
1 change: 0 additions & 1 deletion explainers/code/leave-one-group-out-evaluation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Generated from explainers/leave-one-group-out-evaluation.md by scripts/build_explainers.py. Edit the markdown, not this file.
import numpy as np
import pandas as pd
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness
from faircode.models import build_model
Expand Down
2 changes: 1 addition & 1 deletion explainers/code/monitoring-fairness-in-production.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def batches_until_alert(batch, window, drift, drift_at=20, n_batches=60, z=3):
return b - drift_at + 1 if b >= drift_at else -1 # -1 = a false alarm before the drift
return None

print(f"\nalert rule: the gap over the last W batches is more than 3 standard errors from the launch value")
print("\nalert rule: the gap over the last W batches is more than 3 standard errors from the launch value")
print(f"{'batch size':>10} {'window':>7} {'drift':>6} {'median batches to alert':>25} {'never alerted':>14} {'false alarms before the drift':>31}")
for batch, window in ((200, 3), (500, 3), (1000, 3), (2000, 3), (1000, 1), (1000, 6)):
for drift in (0.03, 0.06):
Expand Down
2 changes: 1 addition & 1 deletion explainers/code/small-group-shrinkage.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def shrink(successes, counts):
small_raw.append(np.sqrt(np.mean((raw[small] - truth[small]) ** 2))); small_shrunk.append(np.sqrt(np.mean((shrunk[small] - truth[small]) ** 2)))
estimates.append((raw[worst], shrunk[worst]))

print(f"\nerror (RMSE against the true cell accuracy) from a 600-row audit, averaged over 300 audits")
print("\nerror (RMSE against the true cell accuracy) from a 600-row audit, averaged over 300 audits")
print(f" all {len(cells)} cells: raw {np.mean(raw_err):.3f} | shrunk {np.mean(shrunk_err):.3f}")
print(f" the smallest {small.sum()} cells: raw {np.mean(small_raw):.3f} | shrunk {np.mean(small_shrunk):.3f}")
mean_raw, mean_shrunk = np.mean(estimates, axis=0)
Expand Down
1 change: 0 additions & 1 deletion explainers/code/survivorship-bias.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Generated from explainers/survivorship-bias.md by scripts/build_explainers.py. Edit the markdown, not this file.
import numpy as np
from sklearn.linear_model import LogisticRegression
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness

Expand Down
1 change: 0 additions & 1 deletion explainers/code/utilization-as-a-label.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Generated from explainers/utilization-as-a-label.md by scripts/build_explainers.py. Edit the markdown, not this file.
import numpy as np
import pandas as pd
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness

Expand Down
1 change: 0 additions & 1 deletion explainers/delayed-feedback.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ def monitored_gap(mean_delay_group, mean_delay_others, today=90):
delay = np.minimum(delay, 29.9)
seen_positive = (y == 1) & (discharge + delay <= today)
window_closed = discharge + 30 <= today
known_negative = (y == 0) & window_closed
# a naive monitor counts every discharged patient and treats every outcome it has not seen as "no"
naive = {g: seen_positive[group == g].mean() for g in (True, False)}
# a careful monitor only counts patients whose 30-day window is closed
Expand Down
2 changes: 1 addition & 1 deletion explainers/fairness-in-ranking.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def report(score, name):
print(f"{name}\n female share of the top {K}: {female[top].mean():.3f} (pool {female.mean():.3f})"
f"\n exposure ratio female/male, whole list: {whole[female == 1].mean() / whole[female == 0].mean():.3f}"
f"\n exposure ratio female/male, top {K} only: {cut[female == 1].mean() / cut[female == 0].mean():.3f}"
f"\n exposure per unit of hire rate, female/male: "
"\n exposure per unit of hire rate, female/male: "
f"{(cut[female == 1].mean() / hired[female == 1].mean()) / (cut[female == 0].mean() / hired[female == 0].mean()):.3f}")

report(scores(X_biased), "baseline (Gender is a feature)")
Expand Down
1 change: 0 additions & 1 deletion explainers/feature-importance-by-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ Part of the gap is mechanical. Women are a minority of the positive class in thi
Run from the repo root (`PYTHONPATH=. python3 explainers/code/feature-importance-by-group.py`; that file is generated from the block below).

```python
import numpy as np
from sklearn.inspection import permutation_importance
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness
from faircode.models import build_model
Expand Down
1 change: 0 additions & 1 deletion explainers/hiring-pipeline-adverse-impact.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ alive = np.ones(len(df), dtype=bool)
def ratio_with_interval(passed, reached, n_boot=500):
"""Women's selection rate divided by everyone else's, among the people who reached the stage, with a bootstrap interval."""
w, m = reached & (women == 1), reached & (women == 0)
rate = lambda mask, rows: passed[rows][mask[rows]].mean()
point = passed[w].mean() / passed[m].mean()
idx_w, idx_m = np.flatnonzero(w), np.flatnonzero(m)
boots = [passed[rng.choice(idx_w, len(idx_w))].mean() / passed[rng.choice(idx_m, len(idx_m))].mean() for _ in range(n_boot)]
Expand Down
1 change: 0 additions & 1 deletion explainers/leave-one-group-out-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ The overall reading: on this data a forest generalizes to a held-out group about
Run from the repo root (`PYTHONPATH=. python3 explainers/code/leave-one-group-out-evaluation.py`; that file is generated from the block below).

```python
import numpy as np
import pandas as pd
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness
from faircode.models import build_model
Expand Down
2 changes: 1 addition & 1 deletion explainers/monitoring-fairness-in-production.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def batches_until_alert(batch, window, drift, drift_at=20, n_batches=60, z=3):
return b - drift_at + 1 if b >= drift_at else -1 # -1 = a false alarm before the drift
return None

print(f"\nalert rule: the gap over the last W batches is more than 3 standard errors from the launch value")
print("\nalert rule: the gap over the last W batches is more than 3 standard errors from the launch value")
print(f"{'batch size':>10} {'window':>7} {'drift':>6} {'median batches to alert':>25} {'never alerted':>14} {'false alarms before the drift':>31}")
for batch, window in ((200, 3), (500, 3), (1000, 3), (2000, 3), (1000, 1), (1000, 6)):
for drift in (0.03, 0.06):
Expand Down
2 changes: 1 addition & 1 deletion explainers/small-group-shrinkage.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ for _ in range(300):
small_raw.append(np.sqrt(np.mean((raw[small] - truth[small]) ** 2))); small_shrunk.append(np.sqrt(np.mean((shrunk[small] - truth[small]) ** 2)))
estimates.append((raw[worst], shrunk[worst]))

print(f"\nerror (RMSE against the true cell accuracy) from a 600-row audit, averaged over 300 audits")
print("\nerror (RMSE against the true cell accuracy) from a 600-row audit, averaged over 300 audits")
print(f" all {len(cells)} cells: raw {np.mean(raw_err):.3f} | shrunk {np.mean(shrunk_err):.3f}")
print(f" the smallest {small.sum()} cells: raw {np.mean(small_raw):.3f} | shrunk {np.mean(small_shrunk):.3f}")
mean_raw, mean_shrunk = np.mean(estimates, axis=0)
Expand Down
1 change: 0 additions & 1 deletion explainers/survivorship-bias.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ The size of the distortion depends on how hard the gate is. A gate that keeps on
Run from the repo root (`PYTHONPATH=. python3 explainers/code/survivorship-bias.py`; that file is generated from the block below). The audit has the outcome for everyone, so we can compare against it; with real survivorship data you cannot, and the check is to ask what the gate was and model it.

```python
import numpy as np
from sklearn.linear_model import LogisticRegression
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness

Expand Down
1 change: 0 additions & 1 deletion explainers/utilization-as-a-label.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ That is a finding. It says that for this label the Obermeyer mechanism is not vi
Run from the repo root (`PYTHONPATH=. python3 explainers/code/utilization-as-a-label.py`; that file is generated from the block below). `data.df` holds the raw rows, so columns the harness does not encode can still be used to define strata.

```python
import numpy as np
import pandas as pd
from faircode.benchmark import load_audit # same loader, encoding and split as the benchmark harness

Expand Down
Loading