drdavidprivacy commited on
Commit
dc6e8fb
·
verified ·
1 Parent(s): 10a7381

Upload 8 files

Browse files
Files changed (8) hide show
  1. README.md +45 -7
  2. app.py +348 -0
  3. examples.csv +4 -0
  4. reachfirst_v1.pkl +3 -0
  5. reference.json +442 -0
  6. requirements.txt +7 -0
  7. sample_month.csv +0 -0
  8. space_config.json +472 -0
README.md CHANGED
@@ -1,15 +1,53 @@
1
  ---
2
- title: Reachfirst
3
- emoji: 💻
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 6.31.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  license: cc-by-4.0
12
- short_description: income eligibility prediction
 
 
 
 
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Crosswind Workforce Partnership ReachFirst
3
+ emoji: 📬
4
+ colorFrom: green
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.31.0
8
+ python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
  license: cc-by-4.0
12
+ models:
13
+ - drdavidprivacy/reachfirst-model
14
+ datasets:
15
+ - drdavidprivacy/reachfirst-data
16
+ - drdavidprivacy/practicum-case-packs
17
+ short_description: ReachFirst v1, the signed model and its dossier
18
  ---
19
 
20
+ # ReachFirst v1: Crosswind Workforce Partnership
21
+
22
+ **Governance analyst:** [YOUR NAME]
23
+
24
+ **Program:** AI Governance Practicum, Track 1, cohort 1
25
+
26
+ This page is the public home of a governed system. The model is ReachFirst v1, the version the Chief AI Officer signed in Week 6. The app has three pages: the model (its fixed weights and three example records), monitoring (upload a monthly file and read it against the training file and the tripwires), and the dossier (the six documents below).
27
+
28
+ ## The signed version
29
+
30
+ - File: `reachfirst_v1.pkl`, 4,300 bytes, SHA-256 `120234d9f9a111028a2d605d2779a9d8e151ff8d6f85df4ec0497f651595b953`
31
+ - Operating point: the top 20 percent of the ranked file
32
+ - Libraries: scikit-learn 1.9.1, fairlearn 0.15.0
33
+ - Data: the remediated working file in the program's dataset repository, loaded by link, never copied here: https://huggingface.co/datasets/drdavidprivacy/practicum-case-packs/resolve/main/crosswind_pilot_extract_remediated.csv (MD5 `ca9ed6d4b9dfd59f3f586b09da05b31f`)
34
+
35
+ - Model repository (the model card and the weights): https://huggingface.co/drdavidprivacy/reachfirst-model. Dataset repository (the datasheet and the remediation log): https://huggingface.co/datasets/drdavidprivacy/reachfirst-data
36
+
37
+ ## The dossier
38
+
39
+ The six deliverables, in course order. Each opens in the browser.
40
+ - [Use Case Assessment (Week 1)](docs/01_Use-Case-Assessment.pdf)
41
+ - [EDA Observation Log (Week 2)](docs/02_EDA-Observation-Log.pdf)
42
+ - [Dataset Datasheet (Week 3)](docs/03_Dataset-Datasheet.pdf)
43
+ - [Model Selection Rationale (Week 4)](docs/04_Model-Selection-Rationale.pdf)
44
+ - [Model Card (Week 6)](docs/05_Model-Card.pdf)
45
+ - [Go/No-Go Decision Record (Week 7)](docs/06_Go-No-Go-Decision-Record.pdf)
46
+
47
+ ## What this page is not
48
+
49
+ A score says where a household's record ranks on one list. It is not a decision about the household. The decisions, and the conditions on them, are in the Go/No-Go Decision Record above.
50
+
51
+ ## Source
52
+
53
+ Built for the AI Governance Practicum (AI Governance Collective). The organization, the people, and the program are fictional. The data is a public dataset, remediated and documented in the dossier's datasheet.
app.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The public page of a governed system: the signed model, its monitoring page, and the dossier.
2
+ Built for the AI Governance Practicum (AI Governance Collective). One app.py serves all three organizations; space_config.json says which."""
3
+ import json, os, pickle, io, warnings
4
+ import numpy as np, pandas as pd
5
+ import gradio as gr
6
+ import matplotlib
7
+ matplotlib.use("Agg")
8
+ import matplotlib.pyplot as plt
9
+ warnings.filterwarnings("ignore")
10
+
11
+ HERE = os.path.dirname(os.path.abspath(__file__))
12
+
13
+ # Hugging Face's free tier runs Gradio Spaces on ZeroGPU, which refuses to start an app that declares no GPU function.
14
+ # This app needs no GPU: the declaration below satisfies the check and is never called. On a plain CPU Space it is skipped.
15
+ try:
16
+ import spaces
17
+ @spaces.GPU
18
+ def _gpu_not_used():
19
+ return "This app runs on the CPU."
20
+ except Exception:
21
+ pass
22
+ CFG = json.load(open(os.path.join(HERE, "space_config.json")))
23
+ REF = json.load(open(os.path.join(HERE, "reference.json")))
24
+ MODEL = pickle.load(open(os.path.join(HERE, CFG["model_file"]), "rb"))
25
+ RECAL = pickle.load(open(os.path.join(HERE, CFG["recal_file"]), "rb")) if CFG.get("recal_file") else None
26
+ REASONS = pd.read_csv(os.path.join(HERE, CFG["reasons_file"])) if CFG.get("reasons_file") else None
27
+ EXAMPLES = pd.read_csv(os.path.join(HERE, "examples.csv"), keep_default_na=False)
28
+ INPUTS, KEPT, SHARE = CFG["inputs"], CFG["kept"], CFG["share"]
29
+ CAL_FLOOR = 25 # a calibration band is read only when it expects at least this many outcomes; a smaller band says nothing either way
30
+ SPACE_ID = os.environ.get("SPACE_ID", "")
31
+ WEEK6 = CFG["week6"]
32
+
33
+ # ----------------------------------------------------------------------------- scoring
34
+ def score_frame(X):
35
+ """The production score. Where the Week 6 memo ratified fitted steps (the recalibration and the odds correction), they run here."""
36
+ X = X[INPUTS]
37
+ if RECAL is not None:
38
+ pf = RECAL.predict_proba(X)[:, 1]; f = CFG["odds_factor"]
39
+ return pf * f / (pf * f + 1 - pf)
40
+ return MODEL.predict_proba(X)[:, 1]
41
+
42
+ def contributions(X):
43
+ """Each input's fixed weight times the record's value on the scaled, remover-adjusted inputs. The record's own reasons."""
44
+ Z = X[INPUTS]
45
+ for name, step in MODEL.steps[:-1]:
46
+ Z = step.transform(Z)
47
+ coef = MODEL.named_steps["model"].coef_[0]
48
+ return pd.DataFrame(np.asarray(Z) * coef, columns=KEPT, index=X.index)
49
+
50
+ def allowed_reasons(con):
51
+ """Drop the columns a reason may never cite: a medication the record marks No (Alderwyn, Week 6 memo, ruling 5)."""
52
+ drugs = CFG.get("drug_fields", [])
53
+ if not drugs: return con
54
+ keep = [c for c in con.index if not any(c == d + "_No" for d in drugs)]
55
+ return con[keep]
56
+
57
+ def allowed_reasons_frame(frame):
58
+ drugs = CFG.get("drug_fields", [])
59
+ return frame[[c for c in frame.columns if not any(c == d + "_No" for d in drugs)]] if drugs else frame
60
+
61
+ def readable(col):
62
+ if REASONS is not None:
63
+ r = REASONS.loc[(REASONS.input == col) | ((REASONS.input + "_" + REASONS.value.astype(str)) == col), "reason"]
64
+ if len(r): return r.iloc[0]
65
+ for c in CFG["categorical"]:
66
+ if col.startswith(c + "_"): return f"{c} is {col[len(c) + 1:]}"
67
+ return col
68
+
69
+ def coerce(df):
70
+ """Read a file the way the working file is read: numeric inputs as numbers, categorical inputs as text."""
71
+ df = df.copy()
72
+ for c in CFG["numeric"]:
73
+ if c in df: df[c] = pd.to_numeric(df[c], errors="coerce")
74
+ for c in CFG["categorical"]:
75
+ if c in df: df[c] = df[c].astype(str)
76
+ return df
77
+
78
+ def groups_of(frame):
79
+ g = {}
80
+ for gname, d in CFG["groups"].items():
81
+ col = d["col"]
82
+ if col not in frame: continue
83
+ if "bands" in d:
84
+ v = pd.to_numeric(frame[col], errors="coerce"); lab = pd.Series(["unrecorded"] * len(frame), index=frame.index, dtype=object)
85
+ for lo, hi, name in d["bands"]: lab[(v >= lo) & (v <= hi)] = name
86
+ g[gname] = lab
87
+ else:
88
+ s = frame[col].astype(str); g[gname] = s.map(d["map"]).fillna(s) if d.get("map") else s
89
+ return pd.DataFrame(g, index=frame.index)
90
+
91
+ def pct(x, d=1): return f"{x * 100:.{d}f} percent"
92
+
93
+ # ----------------------------------------------------------------------------- the signed version (the collapsed panel at the top)
94
+ def model_header():
95
+ a = CFG["artifacts"][CFG["model_file"]]
96
+ L = [f"**Organization:** {CFG['org']}. **Signed by:** {CFG['caio']} ({CFG['memo_ref']}). **Kept by:** {CFG['lead']}.",
97
+ f"**File:** `{CFG['model_file']}`, {a['bytes']:,} bytes, SHA-256 `{a['sha256']}`."]
98
+ if RECAL is not None:
99
+ r = CFG["artifacts"][CFG["recal_file"]]
100
+ L.append(f"**Fitted steps the memo ratified:** the isotonic recalibration (`{CFG['recal_file']}`, SHA-256 `{r['sha256']}`), then the odds correction to the {int(CFG['calibration']['book_rate']*100)} percent book rate (factor {CFG['odds_factor']}). Every score on this page has both applied.")
101
+ L.append(f"**What it is:** a logistic regression with {len(KEPT)} fitted columns from {len(INPUTS)} input fields. The weights are fixed. The same record gets the same score every time.")
102
+ L.append(f"**Operating point:** the top {int(SHARE*100)} percent of the ranked file gets {CFG['action']}. On the Week 6 test set ({WEEK6['test_rows']:,} {CFG['units']}) that was {WEEK6['flagged']:,} {CFG['units']}, of which {WEEK6['caught']:,} were {CFG['positive_label']}. Test-set ROC-AUC {WEEK6['roc_auc']:.3f}.")
103
+ L.append(f"**Libraries:** scikit-learn {CFG['versions']['sklearn']}, fairlearn {CFG['versions']['fairlearn']}. The model does not load on another version of scikit-learn, by design.")
104
+ return "\n\n".join(L)
105
+
106
+ def weights_table(n=25):
107
+ return pd.DataFrame([dict(column=c, weight=round(w, 4), meaning=readable(c)) for c, w in CFG["weights"][:n]])
108
+
109
+ # ----------------------------------------------------------------------------- tab 1: one record
110
+ CAT = [c for c in INPUTS if c in CFG["categorical"]]
111
+ NUM = [c for c in INPUTS if c in CFG["numeric"]]
112
+ FORM = CAT + NUM # the order of the form's controls
113
+
114
+ def _typical():
115
+ d = {c: REF["numeric"][c]["median"] for c in NUM}
116
+ for c in CAT:
117
+ r = REF["categorical"][c]; d[c] = max(r, key=r.get)
118
+ return d
119
+
120
+ def _case_row(i):
121
+ row = EXAMPLES.iloc[i]
122
+ return {c: (float(row[c]) if c in NUM else str(row[c])) for c in FORM}
123
+
124
+ PRESETS = {f"Typical {CFG['unit']} (training-file medians and most common values)": _typical()}
125
+ for i in range(len(EXAMPLES)):
126
+ PRESETS[f"{EXAMPLES['case'].iloc[i].capitalize()} (Week 6 case, rank {EXAMPLES['week6_rank'].iloc[i]} of {WEEK6['test_rows']:,})"] = _case_row(i)
127
+ FIRST = list(PRESETS)[0]
128
+
129
+ def apply_preset(name):
130
+ d = PRESETS[name]
131
+ return [d[c] for c in FORM]
132
+
133
+ def field_label(c):
134
+ """The input's name, with the meaning of its codes where the program's group definitions give one (SEX: 1 male, 2 female)."""
135
+ for d in CFG["groups"].values():
136
+ if d["col"] == c and d.get("map"):
137
+ return c + " (" + ", ".join(f"{k} {v}" for k, v in d["map"].items()) + ")"
138
+ return c
139
+
140
+ def choices_for(c):
141
+ ch = sorted(REF["categorical"][c], key=str)
142
+ for v in EXAMPLES[c].astype(str).unique():
143
+ if v not in ch: ch.append(v)
144
+ return ch
145
+
146
+ def score_one(*vals):
147
+ row = {c: v for c, v in zip(FORM, vals)}
148
+ X = coerce(pd.DataFrame([row]))
149
+ p = float(score_frame(X)[0]); cut = REF["cut_score"]; inside = p >= cut
150
+ verdict = (f"## Score: **{p:.1%}**\n\n"
151
+ f"### {'🔴 Above the cut' if inside else '🟢 Below the cut'} (Week 6 cut = {cut:.1%}, the score of the last {CFG['unit']} inside the top {int(SHARE*100)} percent)\n\n"
152
+ f"At the operating point the {CFG['memo_ref']} set, this {CFG['unit']} {'would' if inside else 'would not'} be {CFG['flagged_word']}: {CFG['action']}. "
153
+ f"The score says where the record ranks. It is not a decision about the {CFG['person']}.")
154
+ con = allowed_reasons(contributions(X).iloc[0])
155
+ up = con[con > 0].sort_values(ascending=False).head(6); down = con[con < 0].sort_values().head(4)
156
+ items = list(up.items()) + list(down.items())
157
+ labs = [readable(c) for c, _ in items][::-1]; v = [float(x) for _, x in items][::-1]
158
+ fig, ax = plt.subplots(figsize=(11, 0.45 * len(v) + 1.8))
159
+ ax.barh(range(len(v)), v, color=["#d62728" if x > 0 else "#2ca02c" for x in v])
160
+ ax.set_yticks(range(len(v))); ax.set_yticklabels(labs, fontsize=9); ax.axvline(0, color="black", lw=0.8)
161
+ ax.set_xlabel(f"Toward {CFG['positive_label']} (red) or away from it (green): the fixed weight times this record's value", fontsize=9)
162
+ ax.set_title(f"Why {CFG['system']} scored this {CFG['unit']} {p:.1%}: the record's own reasons", fontsize=11, loc="left")
163
+ fig.tight_layout()
164
+ return verdict, fig
165
+
166
+ # ----------------------------------------------------------------------------- tab 2: batch upload
167
+ def score_batch(file, top_n):
168
+ if file is None: return None, f"Upload a CSV of {CFG['units']} to score (the model's input columns, one row per {CFG['unit']}).", None
169
+ try: df = coerce(pd.read_csv(file.name if hasattr(file, "name") else file, low_memory=False, keep_default_na=False, na_values=[]))
170
+ except Exception as e: return None, f"Could not read the file: {e}", None
171
+ missing = [c for c in INPUTS if c not in df.columns]
172
+ if missing: return None, "The file is missing input columns: " + ", ".join(missing) + ". Nothing was scored.", None
173
+ s = score_frame(df); order = np.argsort(-s, kind="stable"); k = int(round(SHARE * len(df)))
174
+ idc = CFG.get("id_col"); ids = df[idc].values if idc and idc in df.columns else np.arange(1, len(df) + 1)
175
+ con = allowed_reasons_frame(contributions(df)); top = [readable(con.columns[i]) for i in np.argmax(con.values, axis=1)]
176
+ out = pd.DataFrame({(idc if idc and idc in df.columns else "row"): ids, "score": np.round(s, 4), "ranked_in": np.where(np.arange(len(df)) >= 0, "", ""), "top_reason": top})
177
+ out = out.iloc[order].reset_index(drop=True); out.insert(0, "rank", np.arange(1, len(df) + 1)); out["ranked_in"] = np.where(out["rank"] <= k, "yes", "no")
178
+ path = os.path.join("/tmp", "ranked_list.csv"); out.to_csv(path, index=False)
179
+ msg = (f"Scored **{len(df):,}** {CFG['units']}. The top {int(SHARE*100)} percent, the operating point, is **{k:,}** {CFG['units']}. "
180
+ f"Showing the top **{int(top_n)}** by score (the team's capacity). The download carries every record with its score, rank, whether it is ranked in, and its own top reason.")
181
+ return out.head(int(top_n)), msg, path
182
+
183
+ # ----------------------------------------------------------------------------- tab 2: monitoring
184
+ def psi(ref_shares, new_shares):
185
+ keys = set(ref_shares) | set(new_shares); eps = 1e-4; v = 0.0
186
+ for k in keys:
187
+ a, b = max(ref_shares.get(k, 0.0), eps), max(new_shares.get(k, 0.0), eps); v += (b - a) * np.log(b / a)
188
+ return float(v)
189
+
190
+ def drift_table(df):
191
+ rows = []
192
+ for c in INPUTS:
193
+ if c not in df.columns: continue
194
+ watch = c in CFG["watch_fields"]
195
+ if c in REF["numeric"]:
196
+ r = REF["numeric"][c]; v = pd.to_numeric(df[c], errors="coerce"); edges = r["edges"]
197
+ if len(edges) < 3: continue
198
+ def shares(x):
199
+ b = np.clip(np.searchsorted(edges, x, side="right") - 1, 0, len(edges) - 2); return {str(i): float((b == i).mean()) for i in range(len(edges) - 1)}
200
+ ref_sh = r["shares"]; new_sh = shares(v.dropna().values)
201
+ idx = psi(ref_sh, new_sh)
202
+ rows.append(dict(input=c, kind="number", training_file=f"mean {r['mean']:,.2f}, median {r['median']:,.2f}", this_file=f"mean {v.mean():,.2f}, median {v.median():,.2f}", missing=int(v.isna().sum()), stability_index=round(idx, 3), reading=reading(idx), watched="yes" if watch else ""))
203
+ elif c in REF["categorical"]:
204
+ r = REF["categorical"][c]; vc = df[c].astype(str).value_counts(normalize=True); new_sh = {str(k): float(x) for k, x in vc.items()}
205
+ idx = psi(r, new_sh); top_r = max(r, key=r.get); top_n = max(new_sh, key=new_sh.get) if new_sh else ""
206
+ new_levels = [k for k in new_sh if k not in r]
207
+ rows.append(dict(input=c, kind="category", training_file=f"most common {top_r} ({pct(r[top_r])})", this_file=f"most common {top_n} ({pct(new_sh.get(top_n, 0))})" + (f"; new levels: {', '.join(new_levels)}" if new_levels else ""),
208
+ missing=int((df[c].astype(str) == "nan").sum()), stability_index=round(idx, 3), reading=reading(idx), watched="yes" if watch else ""))
209
+ t = pd.DataFrame(rows)
210
+ return t.sort_values(["watched", "stability_index"], ascending=[False, False]).reset_index(drop=True) if len(t) else t
211
+
212
+ def reading(idx):
213
+ return "stable" if idx < 0.10 else ("watch" if idx < 0.25 else "SHIFTED")
214
+
215
+ def group_table(df, s, flag, y):
216
+ """Each group's rate at the cut beside the Week 6 test-set reading and the tripwire. With outcomes: the ruled check (false alarm or miss). Without: the share ranked in."""
217
+ G = groups_of(df); rows = []; check = CFG["group_check"]; line = CFG["group_tripwire"]
218
+ if y is not None:
219
+ pos, neg = y == 1, y == 0
220
+ port = (((flag == 1) & neg).sum() / max(neg.sum(), 1)) if check == "false_alarm" else (((flag == 0) & pos).sum() / max(pos.sum(), 1))
221
+ for gname in G.columns:
222
+ d = CFG["groups"][gname]; order = [b[2] for b in d["bands"]] if "bands" in d else d.get("order", [])
223
+ for lvl in sorted(G[gname].unique(), key=lambda s: (order.index(s) if s in order else len(order), str(s))):
224
+ m = (G[gname] == lvl).values; n = int(m.sum())
225
+ wk6 = REF["group_rates"].get(gname, {}).get(str(lvl), {})
226
+ covered = n >= CFG["min_group"] # the memo's group floor, applied to the month's file
227
+ if y is not None:
228
+ rate = (((flag == 1) & neg & m).sum() / max((neg & m).sum(), 1)) if check == "false_alarm" else (((flag == 0) & pos & m).sum() / max((pos & m).sum(), 1))
229
+ base = wk6.get("false_alarm" if check == "false_alarm" else "miss", np.nan)
230
+ over = rate > port * (1 + line)
231
+ result = (f"not judged (under the group floor of {CFG['min_group']:,})" if not covered else ("TRIP" if over else "inside the line"))
232
+ rows.append({"group": gname, "level": lvl, "records this file": n, ("false alarm rate" if check == "false_alarm" else "miss rate"): pct(rate), "whole file": pct(port), "line": pct(port * (1 + line)), "Week 6 test set": pct(base) if base == base else "", "watched": "yes" if str(lvl) in CFG["watch_groups"] else "", "result": result})
233
+ else:
234
+ rate = float(flag[m].mean()); base = wk6.get("flagged_share", np.nan)
235
+ rows.append({"group": gname, "level": lvl, "records this file": n, "share ranked in": pct(rate), "whole file": pct(SHARE), "Week 6 test set": pct(base) if base == base else "", "watched": "yes" if str(lvl) in CFG["watch_groups"] else "", "result": "no outcomes yet: share ranked in only" if covered else f"not judged (under the group floor of {CFG['min_group']:,})"})
236
+ return pd.DataFrame(rows)
237
+
238
+ def calibration_table(s, y):
239
+ cal = CFG["calibration"]; rows = []
240
+ for lo, hi, name in cal["bands"]:
241
+ m = (s >= lo) & (s < hi)
242
+ if m.sum() == 0: rows.append(dict(band=name, records=0, predicted="", observed="", gap_share_of_observed="", result="empty band")); continue
243
+ mp, ob = float(s[m].mean()), float(y[m].mean()); gap = abs(mp - ob); expected = mp * m.sum()
244
+ if expected < CAL_FLOOR: res = f"not read (fewer than {CAL_FLOOR} expected outcomes in the band)"
245
+ else: res = "no outcomes in band" if ob == 0 else ("pass" if gap / ob <= cal["tripwire"] else "TRIP")
246
+ rows.append(dict(band=name, records=int(m.sum()), predicted=pct(mp, 2), observed=pct(ob, 2), gap_share_of_observed=(pct(gap / ob, 1) if ob else ""), result=res))
247
+ return pd.DataFrame(rows)
248
+
249
+ def monitor(file):
250
+ empty = (None, None, None)
251
+ if file is None: return "Upload a monthly file: one row per " + CFG["unit"] + ", the model's input columns, the measurement fields, and the outcome column once it is known.", *empty
252
+ try: df = coerce(pd.read_csv(file.name if hasattr(file, "name") else file, low_memory=False, keep_default_na=False, na_values=[]))
253
+ except Exception as e: return f"Could not read the file: {e}", *empty
254
+ missing = [c for c in INPUTS if c not in df.columns]
255
+ if missing: return "The file is missing input columns: " + ", ".join(missing) + ". The job would hold this file out of the cycle (the input contract).", *empty
256
+ s = score_frame(df); order = np.argsort(-s, kind="stable"); k = int(round(SHARE * len(df))); flag = np.zeros(len(df), int); flag[order[:k]] = 1
257
+ y = (df[CFG["outcome"]].astype(str) == str(CFG["positive"])).astype(int).values if CFG["outcome"] in df.columns else None
258
+ above_cut = float((s >= REF["cut_score"]).mean())
259
+ L = [f"**File:** {len(df):,} {CFG['units']}, {df.shape[1]} columns. Outcome column `{CFG['outcome']}`: " + ("present, so the ruled checks run." if y is not None else "absent, so only the input and score readings run. Upload again when the outcomes are known."),
260
+ f"**Scores:** mean {s.mean():.4f} (training-file positive rate {pct(REF['positive_rate'])}). {pct(above_cut)} of this file scores above the Week 6 cut of {REF['cut_score']:.4f}, against {pct(SHARE, 0)} by construction on the test set. The top {int(SHARE*100)} percent of this file is {k:,} {CFG['units']}.",
261
+ f"**Inputs:** the stability index compares each input's distribution with the training file's ({REF['rows']:,} rows). Under 0.10 is stable, 0.10 to 0.25 is watch, over 0.25 is shifted. These lines are the usual working lines, provisional until the Week 8 monitoring plan sets the program's own. Watched fields: {CFG['watch_fields_label']}."]
262
+ if y is not None:
263
+ pos = y == 1
264
+ L.append(f"**At the cut:** {int((flag & pos).sum()):,} of the {int(pos.sum()):,} {CFG['positive_label']} {CFG['units']} are ranked in ({pct((flag & pos).sum() / max(pos.sum(), 1))}); the whole-file {'false alarm' if CFG['group_check'] == 'false_alarm' else 'miss'} rate is the line every group is read against, plus {int(CFG['group_tripwire']*100)} percent ({CFG['memo_ref']}).")
265
+ cal = calibration_table(s, y) if (CFG.get("calibration") and y is not None) else None
266
+ if cal is not None:
267
+ worst = [r for r in cal.result if r == "TRIP"]; read = [r for r in cal.result if r in ("TRIP", "pass")]
268
+ L.append(f"**Calibration:** predicted against observed by score band, line {int(CFG['calibration']['tripwire']*100)} percent of observed. Bands that expect fewer than {CAL_FLOOR} outcomes are not read. " + (f"{len(read)} band(s) read, every one inside the line." if not worst else f"{len(worst)} of {len(read)} band(s) read are over the line: the calibration condition (ruling 1) is not met for this file."))
269
+ return "\n\n".join(L), drift_table(df), group_table(df, s, flag, y), cal
270
+
271
+ # ----------------------------------------------------------------------------- tab 4: the dossier
272
+ def dossier_markdown():
273
+ L = [f"## The dossier: {CFG['system']} at {CFG['org']}", "", "The six deliverables of the practicum, in course order. Each opens in the browser.", ""]
274
+ for f, label in CFG["docs"]:
275
+ p = os.path.join(HERE, "docs", f)
276
+ if SPACE_ID: url = f"https://huggingface.co/spaces/{SPACE_ID}/resolve/main/docs/{f}"
277
+ else: url = f"docs/{f}"
278
+ L.append(f"- [{label}]({url})" + ("" if os.path.exists(p) else " (not uploaded yet)"))
279
+ L += ["", f"**Monitoring targets from the {CFG['memo_ref']}:**"] + [f"- {m}" for m in CFG["monitoring_lines"]]
280
+ L += ["", f"**Data:** [{CFG['data_file']}]({CFG['data_url']}) in the program's dataset repository (MD5 `{CFG['md5']}`). The data is not copied into this page."]
281
+ return "\n".join(L)
282
+
283
+ # ----------------------------------------------------------------------------- the page
284
+ INTRO = f"""
285
+ # {CFG['system']} {CFG['version']}: {CFG['org']}
286
+ **Governance analyst: [YOUR NAME]** · a teaching deployment for the *AI Governance Practicum*, Track 1
287
+
288
+ ---
289
+
290
+ {CFG['system']} estimates, for each {CFG['unit']}, the likelihood of {CFG['positive_label']}, ranks the file, and sends the top {int(SHARE*100)} percent to a person for {CFG['action']}. This page runs the signed version ({CFG['memo_ref']}). It is for **education**, not real decisions. A score says where a {CFG['person']}'s record ranks on one list. It is not a decision about the {CFG['person']}.
291
+ """
292
+
293
+ with gr.Blocks(title=f"{CFG['org']} {CFG['system']}") as demo:
294
+ gr.Markdown(INTRO)
295
+ with gr.Accordion("About the signed version: the file, its hash, the fitted steps, the weights", open=False):
296
+ gr.Markdown(model_header())
297
+ gr.Markdown("**The fixed weights.** The largest by size, with what each column means. A positive weight pushes the score up.")
298
+ gr.Dataframe(weights_table(), interactive=False, wrap=True)
299
+
300
+ with gr.Tab(f"1 · Single {CFG['unit']}"):
301
+ gr.Markdown(f"Score one {CFG['unit']}, then read the **reasons**. Load a Week 6 case to fill the form, or edit any field and score again.")
302
+ with gr.Row():
303
+ preset = gr.Dropdown(list(PRESETS), label="Load an example", value=FIRST)
304
+ d0 = PRESETS[FIRST]
305
+ widgets = {}
306
+ with gr.Accordion("Categorical inputs", open=True):
307
+ for i in range(0, len(CAT), 4):
308
+ with gr.Row():
309
+ for c in CAT[i:i + 4]:
310
+ widgets[c] = gr.Dropdown(choices_for(c), value=d0[c], label=field_label(c), allow_custom_value=True)
311
+ with gr.Accordion("Numeric inputs", open=(len(CAT) == 0)):
312
+ for i in range(0, len(NUM), 4):
313
+ with gr.Row():
314
+ for c in NUM[i:i + 4]:
315
+ widgets[c] = gr.Number(value=d0[c], label=c)
316
+ ordered = [widgets[c] for c in FORM]
317
+ score_btn = gr.Button(f"Score this {CFG['unit']}", variant="primary")
318
+ out_md = gr.Markdown(); out_plot = gr.Plot()
319
+ preset.change(apply_preset, preset, ordered)
320
+ score_btn.click(score_one, ordered, [out_md, out_plot])
321
+
322
+ with gr.Tab("2 · Batch upload"):
323
+ gr.Markdown(f"Upload a CSV of {CFG['units']} (the same columns as the working file). {CFG['system']} scores every row, ranks the file, and marks the top {int(SHARE*100)} percent, the operating point the {CFG['memo_ref']} set. Top-N is how many of the ranked list the team can work. `sample_month.csv` in this repository is a file to try.")
324
+ with gr.Row():
325
+ up = gr.File(label=f"{CFG['units'].capitalize()} CSV", file_types=[".csv"])
326
+ with gr.Column():
327
+ topn = gr.Slider(5, 100, value=20, step=5, label="Top-N by capacity (how many the team can work)")
328
+ batch_btn = gr.Button("Score & rank", variant="primary")
329
+ batch_msg = gr.Markdown(); batch_tbl = gr.Dataframe(interactive=False, wrap=True); batch_dl = gr.File(label="Download the full ranked list")
330
+ batch_btn.click(score_batch, [up, topn], [batch_tbl, batch_msg, batch_dl])
331
+
332
+ with gr.Tab("3 · Monitoring"):
333
+ gr.Markdown(f"**One month at a time.** Upload the month's file. The page reads it against the training file and the tripwires the {CFG['memo_ref']} set: the inputs' distributions, the scores, the groups at the cut, " + ("and the calibration table." if CFG.get("calibration") else "and the ruled group check.") + f" `sample_month.csv` in this repository is a stand-in drawn from the Week 6 test set, so the page can be tried before a monthly file exists.")
334
+ mf = gr.File(label="Monthly file (CSV)", file_types=[".csv"])
335
+ mon_btn = gr.Button("Read this month", variant="primary")
336
+ m_msg = gr.Markdown()
337
+ m_drift = gr.Dataframe(interactive=False, label="Inputs against the training file", wrap=True)
338
+ m_groups = gr.Dataframe(interactive=False, label="Groups at the cut, against the whole file and the line", wrap=True)
339
+ m_cal = gr.Dataframe(interactive=False, label="Calibration by score band", visible=bool(CFG.get("calibration")))
340
+ mon_btn.click(monitor, mf, [m_msg, m_drift, m_groups, m_cal])
341
+
342
+ with gr.Tab("4 · The dossier"):
343
+ gr.Markdown(dossier_markdown())
344
+
345
+ gr.Markdown(f"---\n*{CFG['system']} is a fictional model built on a public dataset for the AI Governance Practicum (AI Governance Collective). The organization, the people, and the program are fictional. Data: [{CFG['data_file']}]({CFG['data_url']}).*")
346
+
347
+ if __name__ == "__main__":
348
+ demo.launch()
examples.csv ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ case,case_note,age,workclass,education,marital-status,occupation,capital-gain,capital-loss,hours-per-week,relationship_model,capital_gain_topcoded,income,sex,race,native_country_report,week6_rank
2
+ caught,"caught (ranked in, at or below the line)",20,Private,Some-college,Never-married,Priv-house-serv,0,0,45,Own-child,0,<=50K,Female,White,United-States,975
3
+ missed,"missed (ranked out, at or below the line; the first record below the cut)",28,Private,Some-college,Never-married,Adm-clerical,0,0,40,Other-relative,0,<=50K,Male,Black,United-States,1953
4
+ false alarm,"false alarm (ranked in, above the line; the highest-scored above-the-line record)",33,Private,7th-8th,Never-married,Farming-fishing,0,0,40,Not-in-family,0,>50K,Male,White,Mexico,417
reachfirst_v1.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:120234d9f9a111028a2d605d2779a9d8e151ff8d6f85df4ec0497f651595b953
3
+ size 4300
reference.json ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "rows": 39032,
3
+ "test_rows": 9758,
4
+ "cut_score": 0.9864791972217691,
5
+ "share": 0.2,
6
+ "positive_rate": 0.7605810616929699,
7
+ "numeric": {
8
+ "age": {
9
+ "mean": 38.57839721254356,
10
+ "std": 13.709782584667774,
11
+ "median": 37.0,
12
+ "edges": [
13
+ 17.0,
14
+ 22.0,
15
+ 26.0,
16
+ 30.0,
17
+ 33.0,
18
+ 37.0,
19
+ 41.0,
20
+ 45.0,
21
+ 50.0,
22
+ 58.0,
23
+ 90.0
24
+ ],
25
+ "shares": {
26
+ "0": 0.09804775568764092,
27
+ "1": 0.10040479606476738,
28
+ "2": 0.09971305595408896,
29
+ "3": 0.07949887271981963,
30
+ "4": 0.11008915761426522,
31
+ "5": 0.10045603607296577,
32
+ "6": 0.09397417503586801,
33
+ "7": 0.10086595613855298,
34
+ "8": 0.11239495798319328,
35
+ "9": 0.10455523672883787
36
+ }
37
+ },
38
+ "capital-gain": {
39
+ "mean": 1078.3918067226891,
40
+ "std": 7454.4260044832945,
41
+ "median": 0.0,
42
+ "edges": [
43
+ 0.0,
44
+ 99999.0
45
+ ],
46
+ "shares": {
47
+ "0": 1.0
48
+ }
49
+ },
50
+ "capital-loss": {
51
+ "mean": 86.47678827628613,
52
+ "std": 401.1241413791568,
53
+ "median": 0.0,
54
+ "edges": [
55
+ 0.0,
56
+ 4356.0
57
+ ],
58
+ "shares": {
59
+ "0": 1.0
60
+ }
61
+ },
62
+ "hours-per-week": {
63
+ "mean": 40.41898954703833,
64
+ "std": 12.400096371479366,
65
+ "median": 40.0,
66
+ "edges": [
67
+ 1.0,
68
+ 24.0,
69
+ 35.0,
70
+ 40.0,
71
+ 48.0,
72
+ 55.0,
73
+ 99.0
74
+ ],
75
+ "shares": {
76
+ "0": 0.09469153515064563,
77
+ "1": 0.07765423242467719,
78
+ "2": 0.06730375076860012,
79
+ "3": 0.5458085673293708,
80
+ "4": 0.10965361754457881,
81
+ "5": 0.10488829678212748
82
+ }
83
+ }
84
+ },
85
+ "categorical": {
86
+ "workclass": {
87
+ "Private": 0.6928417708546833,
88
+ "Self-emp-not-inc": 0.07947325271572044,
89
+ "Local-gov": 0.06507481041196966,
90
+ "?": 0.05741442918630867,
91
+ "State-gov": 0.04078704652592744,
92
+ "Self-emp-inc": 0.03484320557491289,
93
+ "Federal-gov": 0.02897622463619594,
94
+ "Without-pay": 0.0004099200655872105,
95
+ "Never-worked": 0.0001793400286944046
96
+ },
97
+ "education": {
98
+ "HS-grad": 0.32232527157204344,
99
+ "Some-college": 0.2220998155359705,
100
+ "Bachelors": 0.16540274646443942,
101
+ "Masters": 0.054647468743594996,
102
+ "Assoc-voc": 0.041863086698093875,
103
+ "11th": 0.037046525927444146,
104
+ "Assoc-acdm": 0.033101045296167246,
105
+ "10th": 0.028258864521418323,
106
+ "7th-8th": 0.01916376306620209,
107
+ "Prof-school": 0.017114162738266037,
108
+ "9th": 0.015884402541504406,
109
+ "12th": 0.013783562205369953,
110
+ "Doctorate": 0.012425701988112318,
111
+ "5th-6th": 0.010401721664275465,
112
+ "1st-4th": 0.005021520803443328,
113
+ "Preschool": 0.0014603402336544375
114
+ },
115
+ "marital-status": {
116
+ "Married-civ-spouse": 0.4578038532486165,
117
+ "Never-married": 0.3324195531871285,
118
+ "Divorced": 0.13376204140192663,
119
+ "Widowed": 0.031461365033818404,
120
+ "Separated": 0.030769624923139987,
121
+ "Married-spouse-absent": 0.012963722074195532,
122
+ "Married-AF-spouse": 0.000819840131174421
123
+ },
124
+ "occupation": {
125
+ "Prof-specialty": 0.12689588030334084,
126
+ "Exec-managerial": 0.12435949989751999,
127
+ "Craft-repair": 0.12433387989342078,
128
+ "Adm-clerical": 0.11511067841770854,
129
+ "Sales": 0.11293297806927649,
130
+ "Other-service": 0.10089157614265218,
131
+ "Machine-op-inspct": 0.06212850994056159,
132
+ "?": 0.057593769215003074,
133
+ "Transport-moving": 0.04880610780897725,
134
+ "Handlers-cleaners": 0.04176060668169707,
135
+ "Farming-fishing": 0.030205984832957573,
136
+ "Tech-support": 0.029232424677187948,
137
+ "Protective-serv": 0.02034228325476532,
138
+ "Priv-house-serv": 0.00509838081574093,
139
+ "Armed-Forces": 0.00030744004919040785
140
+ },
141
+ "relationship_model": {
142
+ "Spouse": 0.45127075220332036,
143
+ "Not-in-family": 0.25586698093871696,
144
+ "Own-child": 0.15820352531256404,
145
+ "Unmarried": 0.10358167657306826,
146
+ "Other-relative": 0.031077064972330395
147
+ },
148
+ "capital_gain_topcoded": {
149
+ "0": 0.9950040992006559,
150
+ "1": 0.0049959007993441275
151
+ }
152
+ },
153
+ "groups": {
154
+ "sex": {
155
+ "Male": 26130,
156
+ "Female": 12902
157
+ },
158
+ "race": {
159
+ "White": 33372,
160
+ "Black": 3721,
161
+ "Asian-Pac-Islander": 1217,
162
+ "Amer-Indian-Eskimo": 387,
163
+ "Other": 335
164
+ },
165
+ "age band": {
166
+ "25 to 34": 10071,
167
+ "35 to 44": 9748,
168
+ "45 to 54": 6959,
169
+ "under 25": 6808,
170
+ "55 to 64": 3799,
171
+ "65 and over": 1647
172
+ },
173
+ "marital status": {
174
+ "Married-civ-spouse": 17869,
175
+ "Never-married": 12975,
176
+ "Divorced": 5221,
177
+ "Widowed": 1228,
178
+ "Separated": 1201,
179
+ "Married-spouse-absent": 506,
180
+ "Married-AF-spouse": 32
181
+ },
182
+ "country of origin": {
183
+ "United-States": 35034,
184
+ "all other": 953,
185
+ "Mexico": 745,
186
+ "unrecorded": 682,
187
+ "Philippines": 234,
188
+ "Germany": 166,
189
+ "Canada": 148,
190
+ "Puerto-Rico": 147,
191
+ "India": 125,
192
+ "El-Salvador": 122,
193
+ "Cuba": 120,
194
+ "England": 105,
195
+ "China": 101,
196
+ "South": 96,
197
+ "Italy": 95,
198
+ "Dominican-Republic": 80,
199
+ "Jamaica": 79
200
+ }
201
+ },
202
+ "portfolio": {
203
+ "false_alarm": 0.0029965753424657533,
204
+ "miss": 0.7379412557262194,
205
+ "flagged": 1952,
206
+ "caught": 1945
207
+ },
208
+ "group_rates": {
209
+ "sex": {
210
+ "Male": {
211
+ "n": 6484,
212
+ "false_alarm": 0.0025380710659898475,
213
+ "miss": 0.7780239255649092,
214
+ "flagged_share": 0.155305367057372
215
+ },
216
+ "Female": {
217
+ "n": 3274,
218
+ "false_alarm": 0.00546448087431694,
219
+ "miss": 0.6757221458046767,
220
+ "flagged_share": 0.28863775198533903
221
+ }
222
+ },
223
+ "race": {
224
+ "White": {
225
+ "n": 8342,
226
+ "false_alarm": 0.0028142589118198874,
227
+ "miss": 0.7516908212560387,
228
+ "flagged_share": 0.18556701030927836
229
+ },
230
+ "Black": {
231
+ "n": 962,
232
+ "false_alarm": 0.0,
233
+ "miss": 0.6485849056603774,
234
+ "flagged_share": 0.3097713097713098
235
+ },
236
+ "Amer-Indian-Eskimo": {
237
+ "n": 83,
238
+ "false_alarm": 0.0,
239
+ "miss": 0.6153846153846154,
240
+ "flagged_share": 0.3614457831325301
241
+ },
242
+ "Asian-Pac-Islander": {
243
+ "n": 300,
244
+ "false_alarm": 0.013157894736842105,
245
+ "miss": 0.7410714285714286,
246
+ "flagged_share": 0.19666666666666666
247
+ },
248
+ "Other": {
249
+ "n": 71,
250
+ "false_alarm": 0.0,
251
+ "miss": 0.7258064516129032,
252
+ "flagged_share": 0.23943661971830985
253
+ }
254
+ },
255
+ "age band": {
256
+ "under 25": {
257
+ "n": 1601,
258
+ "false_alarm": 0.07142857142857142,
259
+ "miss": 0.3314429741650914,
260
+ "flagged_share": 0.6633354153653966
261
+ },
262
+ "25 to 34": {
263
+ "n": 2493,
264
+ "false_alarm": 0.007537688442211055,
265
+ "miss": 0.7527446300715991,
266
+ "flagged_share": 0.2089851584436422
267
+ },
268
+ "35 to 44": {
269
+ "n": 2436,
270
+ "false_alarm": 0.0,
271
+ "miss": 0.8971393791844188,
272
+ "flagged_share": 0.069376026272578
273
+ },
274
+ "45 to 54": {
275
+ "n": 1806,
276
+ "false_alarm": 0.0,
277
+ "miss": 0.9044585987261147,
278
+ "flagged_share": 0.05813953488372093
279
+ },
280
+ "55 to 64": {
281
+ "n": 983,
282
+ "false_alarm": 0.006134969325153374,
283
+ "miss": 0.9162861491628614,
284
+ "flagged_share": 0.05798575788402848
285
+ },
286
+ "65 and over": {
287
+ "n": 439,
288
+ "false_alarm": 0.01020408163265306,
289
+ "miss": 0.8914956011730205,
290
+ "flagged_share": 0.08656036446469248
291
+ }
292
+ },
293
+ "marital status": {
294
+ "Never-married": {
295
+ "n": 3107,
296
+ "false_alarm": 0.030534351145038167,
297
+ "miss": 0.47043010752688175,
298
+ "flagged_share": 0.5085291277759897
299
+ },
300
+ "Separated": {
301
+ "n": 329,
302
+ "false_alarm": 0.0,
303
+ "miss": 0.7339743589743589,
304
+ "flagged_share": 0.25227963525835867
305
+ },
306
+ "Married-civ-spouse": {
307
+ "n": 4497,
308
+ "false_alarm": 0.0,
309
+ "miss": 0.9971887550200803,
310
+ "flagged_share": 0.00155659328441183
311
+ },
312
+ "Married-spouse-absent": {
313
+ "n": 121,
314
+ "false_alarm": 0.0,
315
+ "miss": 0.6181818181818182,
316
+ "flagged_share": 0.34710743801652894
317
+ },
318
+ "Divorced": {
319
+ "n": 1409,
320
+ "false_alarm": 0.013986013986013986,
321
+ "miss": 0.8570300157977883,
322
+ "flagged_share": 0.12987934705464868
323
+ },
324
+ "Widowed": {
325
+ "n": 290,
326
+ "false_alarm": 0.04,
327
+ "miss": 0.7886792452830189,
328
+ "flagged_share": 0.19655172413793104
329
+ },
330
+ "Married-AF-spouse": {
331
+ "n": 5,
332
+ "false_alarm": 0.0,
333
+ "miss": 1.0,
334
+ "flagged_share": 0.0
335
+ }
336
+ },
337
+ "country of origin": {
338
+ "United-States": {
339
+ "n": 8758,
340
+ "false_alarm": 0.0014012143858010276,
341
+ "miss": 0.7417258576394137,
342
+ "flagged_share": 0.1954784197305321
343
+ },
344
+ "all other": {
345
+ "n": 258,
346
+ "false_alarm": 0.0,
347
+ "miss": 0.7635467980295566,
348
+ "flagged_share": 0.18604651162790697
349
+ },
350
+ "Jamaica": {
351
+ "n": 27,
352
+ "false_alarm": 0.0,
353
+ "miss": 0.5833333333333334,
354
+ "flagged_share": 0.37037037037037035
355
+ },
356
+ "Canada": {
357
+ "n": 34,
358
+ "false_alarm": 0.0,
359
+ "miss": 1.0,
360
+ "flagged_share": 0.0
361
+ },
362
+ "unrecorded": {
363
+ "n": 174,
364
+ "false_alarm": 0.038461538461538464,
365
+ "miss": 0.7868852459016393,
366
+ "flagged_share": 0.16091954022988506
367
+ },
368
+ "Mexico": {
369
+ "n": 198,
370
+ "false_alarm": 0.07692307692307693,
371
+ "miss": 0.5621621621621622,
372
+ "flagged_share": 0.41414141414141414
373
+ },
374
+ "Germany": {
375
+ "n": 40,
376
+ "false_alarm": 0.0,
377
+ "miss": 0.8484848484848485,
378
+ "flagged_share": 0.125
379
+ },
380
+ "Puerto-Rico": {
381
+ "n": 37,
382
+ "false_alarm": 0.2,
383
+ "miss": 0.75,
384
+ "flagged_share": 0.24324324324324326
385
+ },
386
+ "Italy": {
387
+ "n": 10,
388
+ "false_alarm": 0.0,
389
+ "miss": 0.8333333333333334,
390
+ "flagged_share": 0.1
391
+ },
392
+ "Philippines": {
393
+ "n": 60,
394
+ "false_alarm": 0.0,
395
+ "miss": 0.673469387755102,
396
+ "flagged_share": 0.26666666666666666
397
+ },
398
+ "Dominican-Republic": {
399
+ "n": 23,
400
+ "false_alarm": 0.0,
401
+ "miss": 0.45454545454545453,
402
+ "flagged_share": 0.5217391304347826
403
+ },
404
+ "Cuba": {
405
+ "n": 18,
406
+ "false_alarm": 0.0,
407
+ "miss": 0.7857142857142857,
408
+ "flagged_share": 0.16666666666666666
409
+ },
410
+ "China": {
411
+ "n": 21,
412
+ "false_alarm": 0.0,
413
+ "miss": 0.9333333333333333,
414
+ "flagged_share": 0.047619047619047616
415
+ },
416
+ "India": {
417
+ "n": 26,
418
+ "false_alarm": 0.0,
419
+ "miss": 0.6875,
420
+ "flagged_share": 0.19230769230769232
421
+ },
422
+ "England": {
423
+ "n": 22,
424
+ "false_alarm": 0.0,
425
+ "miss": 0.8333333333333334,
426
+ "flagged_share": 0.09090909090909091
427
+ },
428
+ "South": {
429
+ "n": 19,
430
+ "false_alarm": 0.0,
431
+ "miss": 0.8666666666666667,
432
+ "flagged_share": 0.10526315789473684
433
+ },
434
+ "El-Salvador": {
435
+ "n": 33,
436
+ "false_alarm": 0.0,
437
+ "miss": 0.5,
438
+ "flagged_share": 0.48484848484848486
439
+ }
440
+ }
441
+ }
442
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio==6.31.0
2
+ scikit-learn==1.9.1
3
+ fairlearn==0.15.0
4
+ pandas>=2.2
5
+ numpy>=1.26
6
+ scipy>=1.11
7
+ matplotlib>=3.8
sample_month.csv ADDED
The diff for this file is too large to render. See raw diff
 
space_config.json ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "org": "Crosswind Workforce Partnership",
3
+ "system": "ReachFirst",
4
+ "version": "v1",
5
+ "short": "reachfirst",
6
+ "emoji": "\ud83d\udcec",
7
+ "model_file": "reachfirst_v1.pkl",
8
+ "recal_file": null,
9
+ "odds_factor": null,
10
+ "reasons_file": null,
11
+ "data_file": "crosswind_pilot_extract_remediated.csv",
12
+ "md5": "ca9ed6d4b9dfd59f3f586b09da05b31f",
13
+ "id_col": null,
14
+ "outcome": "income",
15
+ "positive": "<=50K",
16
+ "positive_label": "income at or below the line",
17
+ "negative_label": "income above the line",
18
+ "unit": "record",
19
+ "units": "records",
20
+ "action": "a place on the first mailer list for the measured first cycle",
21
+ "flagged_word": "ranked in",
22
+ "person": "household",
23
+ "share": 0.2,
24
+ "book_size": 15000,
25
+ "capacity": 3000,
26
+ "sample_rows": 4000,
27
+ "groups": {
28
+ "sex": {
29
+ "col": "sex"
30
+ },
31
+ "race": {
32
+ "col": "race"
33
+ },
34
+ "age band": {
35
+ "col": "age",
36
+ "bands": [
37
+ [
38
+ 0,
39
+ 24,
40
+ "under 25"
41
+ ],
42
+ [
43
+ 25,
44
+ 34,
45
+ "25 to 34"
46
+ ],
47
+ [
48
+ 35,
49
+ 44,
50
+ "35 to 44"
51
+ ],
52
+ [
53
+ 45,
54
+ 54,
55
+ "45 to 54"
56
+ ],
57
+ [
58
+ 55,
59
+ 64,
60
+ "55 to 64"
61
+ ],
62
+ [
63
+ 65,
64
+ 200,
65
+ "65 and over"
66
+ ]
67
+ ]
68
+ },
69
+ "marital status": {
70
+ "col": "marital-status"
71
+ },
72
+ "country of origin": {
73
+ "col": "native_country_report",
74
+ "map": {
75
+ "?": "unrecorded"
76
+ }
77
+ }
78
+ },
79
+ "min_group": 500,
80
+ "group_check": "miss",
81
+ "group_tripwire": 0.2,
82
+ "watch_groups": [
83
+ "Married-civ-spouse",
84
+ "35 to 44",
85
+ "45 to 54",
86
+ "55 to 64"
87
+ ],
88
+ "calibration": null,
89
+ "watch_fields": [
90
+ "marital-status",
91
+ "age",
92
+ "relationship_model",
93
+ "occupation"
94
+ ],
95
+ "watch_fields_label": "marital status, age, household role, and occupation (Week 6 memo, rulings 1 and 4)",
96
+ "week6": {
97
+ "roc_auc": 0.905,
98
+ "flagged": 1952,
99
+ "caught": 1945,
100
+ "test_rows": 9758,
101
+ "test_positives": 7422
102
+ },
103
+ "lead": "Lauren Chen, AI Governance Lead",
104
+ "caio": "Denise Whitfield, Chief AI Officer",
105
+ "memo_ref": "Week 6 memo",
106
+ "monitoring_lines": [
107
+ "The share of at-or-below records left off the list, by age band and marital status, against the file's (ruling 1).",
108
+ "The measured first cycle: the list half against the current-practice half, married at-or-below households as the comparison group (ruling 3).",
109
+ "The sex-and-race recovery check, rerun at every retrain (ruling 4)."
110
+ ],
111
+ "inputs": [
112
+ "age",
113
+ "workclass",
114
+ "education",
115
+ "marital-status",
116
+ "occupation",
117
+ "capital-gain",
118
+ "capital-loss",
119
+ "hours-per-week",
120
+ "relationship_model",
121
+ "capital_gain_topcoded"
122
+ ],
123
+ "categorical": [
124
+ "workclass",
125
+ "education",
126
+ "marital-status",
127
+ "occupation",
128
+ "relationship_model",
129
+ "capital_gain_topcoded"
130
+ ],
131
+ "numeric": [
132
+ "age",
133
+ "capital-gain",
134
+ "capital-loss",
135
+ "hours-per-week"
136
+ ],
137
+ "kept": [
138
+ "age",
139
+ "capital-gain",
140
+ "capital-loss",
141
+ "hours-per-week",
142
+ "workclass_?",
143
+ "workclass_Federal-gov",
144
+ "workclass_Local-gov",
145
+ "workclass_Never-worked",
146
+ "workclass_Private",
147
+ "workclass_Self-emp-inc",
148
+ "workclass_Self-emp-not-inc",
149
+ "workclass_State-gov",
150
+ "workclass_Without-pay",
151
+ "education_10th",
152
+ "education_11th",
153
+ "education_12th",
154
+ "education_1st-4th",
155
+ "education_5th-6th",
156
+ "education_7th-8th",
157
+ "education_9th",
158
+ "education_Assoc-acdm",
159
+ "education_Assoc-voc",
160
+ "education_Bachelors",
161
+ "education_Doctorate",
162
+ "education_HS-grad",
163
+ "education_Masters",
164
+ "education_Preschool",
165
+ "education_Prof-school",
166
+ "education_Some-college",
167
+ "marital-status_Divorced",
168
+ "marital-status_Married-AF-spouse",
169
+ "marital-status_Married-civ-spouse",
170
+ "marital-status_Married-spouse-absent",
171
+ "marital-status_Never-married",
172
+ "marital-status_Separated",
173
+ "marital-status_Widowed",
174
+ "occupation_?",
175
+ "occupation_Adm-clerical",
176
+ "occupation_Armed-Forces",
177
+ "occupation_Craft-repair",
178
+ "occupation_Exec-managerial",
179
+ "occupation_Farming-fishing",
180
+ "occupation_Handlers-cleaners",
181
+ "occupation_Machine-op-inspct",
182
+ "occupation_Other-service",
183
+ "occupation_Priv-house-serv",
184
+ "occupation_Prof-specialty",
185
+ "occupation_Protective-serv",
186
+ "occupation_Sales",
187
+ "occupation_Tech-support",
188
+ "occupation_Transport-moving",
189
+ "relationship_model_Not-in-family",
190
+ "relationship_model_Other-relative",
191
+ "relationship_model_Own-child",
192
+ "relationship_model_Spouse",
193
+ "relationship_model_Unmarried",
194
+ "capital_gain_topcoded_0",
195
+ "capital_gain_topcoded_1"
196
+ ],
197
+ "artifacts": {
198
+ "reachfirst_v1.pkl": {
199
+ "bytes": 4300,
200
+ "sha256": "120234d9f9a111028a2d605d2779a9d8e151ff8d6f85df4ec0497f651595b953"
201
+ }
202
+ },
203
+ "weights": [
204
+ [
205
+ "capital-gain",
206
+ -2.2417872489881026
207
+ ],
208
+ [
209
+ "education_Doctorate",
210
+ -1.6366401222699463
211
+ ],
212
+ [
213
+ "education_Prof-school",
214
+ -1.5886947281443564
215
+ ],
216
+ [
217
+ "marital-status_Married-civ-spouse",
218
+ -1.3318312733718085
219
+ ],
220
+ [
221
+ "education_Masters",
222
+ -1.226998809000688
223
+ ],
224
+ [
225
+ "education_7th-8th",
226
+ 1.1948745792414959
227
+ ],
228
+ [
229
+ "education_9th",
230
+ 1.0117126998478085
231
+ ],
232
+ [
233
+ "education_Bachelors",
234
+ -0.9637259660175791
235
+ ],
236
+ [
237
+ "education_5th-6th",
238
+ 0.9516051530601645
239
+ ],
240
+ [
241
+ "occupation_Farming-fishing",
242
+ 0.8772839801447048
243
+ ],
244
+ [
245
+ "occupation_Exec-managerial",
246
+ -0.8541365785148912
247
+ ],
248
+ [
249
+ "occupation_Other-service",
250
+ 0.781840452365622
251
+ ],
252
+ [
253
+ "education_10th",
254
+ 0.7805877804503274
255
+ ],
256
+ [
257
+ "education_1st-4th",
258
+ 0.7792674440291991
259
+ ],
260
+ [
261
+ "education_11th",
262
+ 0.7773197750619535
263
+ ],
264
+ [
265
+ "marital-status_Never-married",
266
+ 0.7576080167618042
267
+ ],
268
+ [
269
+ "relationship_model_Spouse",
270
+ -0.7179494340103041
271
+ ],
272
+ [
273
+ "occupation_Prof-specialty",
274
+ -0.6729447708086544
275
+ ],
276
+ [
277
+ "occupation_Handlers-cleaners",
278
+ 0.6242368981515822
279
+ ],
280
+ [
281
+ "occupation_Tech-support",
282
+ -0.6189663047020056
283
+ ],
284
+ [
285
+ "relationship_model_Own-child",
286
+ 0.6110107663731764
287
+ ],
288
+ [
289
+ "marital-status_Married-AF-spouse",
290
+ -0.6036891114878192
291
+ ],
292
+ [
293
+ "workclass_Federal-gov",
294
+ -0.5782322983438535
295
+ ],
296
+ [
297
+ "occupation_Priv-house-serv",
298
+ 0.49771933984626343
299
+ ],
300
+ [
301
+ "education_Preschool",
302
+ 0.475000514022028
303
+ ],
304
+ [
305
+ "occupation_Protective-serv",
306
+ -0.4486092827356464
307
+ ],
308
+ [
309
+ "education_12th",
310
+ 0.43801881119853986
311
+ ],
312
+ [
313
+ "relationship_model_Other-relative",
314
+ 0.4366084364105382
315
+ ],
316
+ [
317
+ "marital-status_Widowed",
318
+ 0.411442162972904
319
+ ],
320
+ [
321
+ "workclass_Self-emp-not-inc",
322
+ 0.40584831710662533
323
+ ],
324
+ [
325
+ "education_Assoc-acdm",
326
+ -0.40047630749854773
327
+ ],
328
+ [
329
+ "occupation_Sales",
330
+ -0.3691730492412525
331
+ ],
332
+ [
333
+ "marital-status_Divorced",
334
+ 0.35970764075843786
335
+ ],
336
+ [
337
+ "education_Assoc-voc",
338
+ -0.35460703817389366
339
+ ],
340
+ [
341
+ "relationship_model_Not-in-family",
342
+ -0.3468235390014372
343
+ ],
344
+ [
345
+ "hours-per-week",
346
+ -0.3376055865841321
347
+ ],
348
+ [
349
+ "workclass_Self-emp-inc",
350
+ -0.3133450413850334
351
+ ],
352
+ [
353
+ "age",
354
+ -0.3079133863771645
355
+ ],
356
+ [
357
+ "marital-status_Separated",
358
+ 0.2891974899880865
359
+ ],
360
+ [
361
+ "occupation_?",
362
+ 0.27540443434911954
363
+ ],
364
+ [
365
+ "workclass_?",
366
+ 0.27236425493903366
367
+ ],
368
+ [
369
+ "occupation_Machine-op-inspct",
370
+ 0.26824061777393843
371
+ ],
372
+ [
373
+ "education_Some-college",
374
+ -0.25756126820826947
375
+ ],
376
+ [
377
+ "capital-loss",
378
+ -0.24456817976716777
379
+ ],
380
+ [
381
+ "workclass_State-gov",
382
+ 0.22032596360113468
383
+ ],
384
+ [
385
+ "marital-status_Married-spouse-absent",
386
+ 0.19106420631129978
387
+ ],
388
+ [
389
+ "occupation_Craft-repair",
390
+ -0.12003826217984946
391
+ ],
392
+ [
393
+ "workclass_Without-pay",
394
+ 0.10573384913493386
395
+ ],
396
+ [
397
+ "occupation_Armed-Forces",
398
+ -0.10136306490673276
399
+ ],
400
+ [
401
+ "education_HS-grad",
402
+ 0.09381661433467584
403
+ ],
404
+ [
405
+ "relationship_model_Unmarried",
406
+ 0.09065290216094356
407
+ ],
408
+ [
409
+ "workclass_Private",
410
+ -0.08277029477532862
411
+ ],
412
+ [
413
+ "occupation_Adm-clerical",
414
+ -0.0779884657765726
415
+ ],
416
+ [
417
+ "capital_gain_topcoded_0",
418
+ 0.06912153062371537
419
+ ],
420
+ [
421
+ "workclass_Local-gov",
422
+ 0.04053420224531096
423
+ ],
424
+ [
425
+ "occupation_Transport-moving",
426
+ 0.011993188167275508
427
+ ],
428
+ [
429
+ "capital_gain_topcoded_1",
430
+ 0.004377601309201683
431
+ ],
432
+ [
433
+ "workclass_Never-worked",
434
+ 0.003040179410085815
435
+ ]
436
+ ],
437
+ "intercept": 2.818318687777577,
438
+ "data_url": "https://huggingface.co/datasets/drdavidprivacy/practicum-case-packs/resolve/main/crosswind_pilot_extract_remediated.csv",
439
+ "dataset_repo": "https://huggingface.co/datasets/drdavidprivacy/practicum-case-packs",
440
+ "versions": {
441
+ "sklearn": "1.9.1",
442
+ "fairlearn": "0.15.0",
443
+ "pandas": "3.0.5",
444
+ "numpy": "2.5.3"
445
+ },
446
+ "docs": [
447
+ [
448
+ "01_Use-Case-Assessment.pdf",
449
+ "Use Case Assessment (Week 1)"
450
+ ],
451
+ [
452
+ "02_EDA-Observation-Log.pdf",
453
+ "EDA Observation Log (Week 2)"
454
+ ],
455
+ [
456
+ "03_Dataset-Datasheet.pdf",
457
+ "Dataset Datasheet (Week 3)"
458
+ ],
459
+ [
460
+ "04_Model-Selection-Rationale.pdf",
461
+ "Model Selection Rationale (Week 4)"
462
+ ],
463
+ [
464
+ "05_Model-Card.pdf",
465
+ "Model Card (Week 6)"
466
+ ],
467
+ [
468
+ "06_Go-No-Go-Decision-Record.pdf",
469
+ "Go/No-Go Decision Record (Week 7)"
470
+ ]
471
+ ]
472
+ }