riskaudit 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. riskaudit-0.1.1/.github/workflows/ci.yml +22 -0
  2. riskaudit-0.1.1/.github/workflows/publish.yml +20 -0
  3. riskaudit-0.1.1/.gitignore +36 -0
  4. riskaudit-0.1.1/.pre-commit-config.yaml +16 -0
  5. riskaudit-0.1.1/CHANGELOG.md +36 -0
  6. riskaudit-0.1.1/CITATION.cff +24 -0
  7. riskaudit-0.1.1/CLAUDE.md +85 -0
  8. riskaudit-0.1.1/LICENSE +21 -0
  9. riskaudit-0.1.1/Makefile +28 -0
  10. riskaudit-0.1.1/PKG-INFO +247 -0
  11. riskaudit-0.1.1/PLAN.md +309 -0
  12. riskaudit-0.1.1/PROTOCOL.md +236 -0
  13. riskaudit-0.1.1/README.md +202 -0
  14. riskaudit-0.1.1/data/checksums.txt +6 -0
  15. riskaudit-0.1.1/data/processed/.gitkeep +0 -0
  16. riskaudit-0.1.1/data/raw/.gitkeep +0 -0
  17. riskaudit-0.1.1/data/synthetic/.gitkeep +0 -0
  18. riskaudit-0.1.1/demo/README.md +18 -0
  19. riskaudit-0.1.1/demo/run_demo.py +64 -0
  20. riskaudit-0.1.1/docs/decisions.md +119 -0
  21. riskaudit-0.1.1/docs/methods.md +147 -0
  22. riskaudit-0.1.1/pyproject.toml +66 -0
  23. riskaudit-0.1.1/scripts/build_panel.py +6 -0
  24. riskaudit-0.1.1/scripts/download_meps.py +4 -0
  25. riskaudit-0.1.1/scripts/profile_meps.py +37 -0
  26. riskaudit-0.1.1/scripts/run_audit.py +103 -0
  27. riskaudit-0.1.1/scripts/train_models.py +6 -0
  28. riskaudit-0.1.1/src/riskaudit/__init__.py +4 -0
  29. riskaudit-0.1.1/src/riskaudit/_config.py +12 -0
  30. riskaudit-0.1.1/src/riskaudit/audit/__init__.py +24 -0
  31. riskaudit-0.1.1/src/riskaudit/audit/_common.py +107 -0
  32. riskaudit-0.1.1/src/riskaudit/audit/ablation.py +99 -0
  33. riskaudit-0.1.1/src/riskaudit/audit/capture.py +83 -0
  34. riskaudit-0.1.1/src/riskaudit/audit/curves.py +101 -0
  35. riskaudit-0.1.1/src/riskaudit/audit/lift.py +98 -0
  36. riskaudit-0.1.1/src/riskaudit/audit/reclassification.py +54 -0
  37. riskaudit-0.1.1/src/riskaudit/audit/report.py +85 -0
  38. riskaudit-0.1.1/src/riskaudit/audit/rtm.py +108 -0
  39. riskaudit-0.1.1/src/riskaudit/etl/__init__.py +0 -0
  40. riskaudit-0.1.1/src/riskaudit/etl/dictionary.yml +99 -0
  41. riskaudit-0.1.1/src/riskaudit/etl/download.py +71 -0
  42. riskaudit-0.1.1/src/riskaudit/etl/meps.py +107 -0
  43. riskaudit-0.1.1/src/riskaudit/features.py +65 -0
  44. riskaudit-0.1.1/src/riskaudit/models.py +108 -0
  45. riskaudit-0.1.1/tests/test_audit.py +182 -0
  46. riskaudit-0.1.1/tests/test_demo.py +12 -0
  47. riskaudit-0.1.1/tests/test_etl_ranges.py +36 -0
  48. riskaudit-0.1.1/tests/test_etl_schema.py +16 -0
  49. riskaudit-0.1.1/tests/test_etl_treatment.py +15 -0
  50. riskaudit-0.1.1/tests/test_features.py +55 -0
  51. riskaudit-0.1.1/tests/test_models.py +37 -0
  52. riskaudit-0.1.1/tests/test_smoke.py +18 -0
@@ -0,0 +1,22 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.11", "3.12"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python-version }}
18
+ cache: pip
19
+ - run: pip install -e ".[dev]"
20
+ - run: ruff check .
21
+ - run: ruff format --check .
22
+ - run: pytest --cov=riskaudit.audit --cov-fail-under=90
@@ -0,0 +1,20 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install build
17
+ - run: python -m build
18
+ - uses: pypa/gh-action-pypi-publish@release/v1
19
+ with:
20
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,36 @@
1
+ # Data and artifacts are never versioned (PROTOCOL §1, guardrail 5)
2
+ data/
3
+ artifacts/
4
+ *.dta
5
+ *.zip
6
+ *.parquet
7
+ demo_report.html
8
+
9
+ # Keep the data tree and the checksum manifest
10
+ !data/
11
+ !data/**/.gitkeep
12
+ !data/checksums.txt
13
+
14
+ # Python
15
+ __pycache__/
16
+ *.py[cod]
17
+ *.egg-info/
18
+ .eggs/
19
+ build/
20
+ dist/
21
+ .venv/
22
+ venv/
23
+
24
+ # Tooling caches
25
+ .pytest_cache/
26
+ .ruff_cache/
27
+ .coverage
28
+ htmlcov/
29
+ .mypy_cache/
30
+
31
+ # OS / editor
32
+ desktop.ini
33
+ Thumbs.db
34
+ .DS_Store
35
+ .idea/
36
+ .vscode/
@@ -0,0 +1,16 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.6.9
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: https://github.com/pre-commit/pre-commit-hooks
10
+ rev: v4.6.0
11
+ hooks:
12
+ - id: trailing-whitespace
13
+ - id: end-of-file-fixer
14
+ - id: check-yaml
15
+ - id: check-added-large-files
16
+ args: [--maxkb=512]
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## v0.1.1 — 2026-07-19
4
+
5
+ - Published to PyPI: `pip install riskaudit`.
6
+ - **Split dependencies:** the core auditor needs only `numpy`, `pandas`,
7
+ `scikit-learn`, `matplotlib`; the MEPS example's heavy stack (`lightgbm`, `shap`,
8
+ `pyreadstat`, …) moves to the `[meps]` extra.
9
+ - PyPI packaging metadata (classifiers, project URLs).
10
+
11
+ ## v0.1.0 — 2026-07-19
12
+
13
+ First release. The product is the **`riskaudit` auditing library**; the MEPS study
14
+ and the synthetic demo are worked examples.
15
+
16
+ ### `riskaudit.audit`
17
+ - `top_k_capture` — weighted top-k capture of need, reported against a random-score
18
+ floor (= k) and an oracle ceiling (ranking by need itself).
19
+ - `reclassification` — weighted 2×2 of who enters and leaves the top-k when the label changes.
20
+ - `label_choice_curve` — mean need across score percentiles, with a bootstrap band.
21
+ - `ablation` — **cross-fitted** Δ global performance vs Δ capture of an independent
22
+ need measure, per feature group.
23
+ - `regression_to_mean` — descriptive share of a top-k drop attributable to RTM
24
+ (weighted regression slope).
25
+ - `incremental_lift` — the contribution metric: excess future outcome, in the
26
+ deprioritized tail, of the distressed over everyone else.
27
+ - `audit_report` — self-contained HTML report.
28
+ - `SurveyDesign` — stratified cluster (VARSTR/VARPSU) bootstrap CIs; warns on
29
+ single-PSU strata. Public inputs are validated (finite, equal length). ~99% test coverage.
30
+
31
+ ### Examples
32
+ - **MEPS 2021–2023**: verified ETL, three comparable LightGBM models, full weighted
33
+ design-based audit. On this data the spend model captures need barely above chance
34
+ and under-serves the distressed on total cost; the effect does **not** appear on
35
+ non-psychiatric utilization, so that mechanism is not demonstrated (docs/methods.md §2).
36
+ - **Synthetic demo** (`demo/run_demo.py`): end-to-end, no external data, no PHI.
@@ -0,0 +1,24 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ title: "riskaudit: auditing label-choice bias in healthcare risk-stratification models"
4
+ abstract: >-
5
+ A Python library that audits any risk-stratification model for label-choice
6
+ bias: how much real need it leaves behind when trained on a proxy such as
7
+ healthcare spending. Ships with a reproducible MEPS example and a synthetic demo.
8
+ authors:
9
+ - family-names: Proromant
10
+ given-names: Conrado
11
+ version: 0.1.0
12
+ date-released: 2026-07-19
13
+ license: MIT
14
+ repository-code: "https://github.com/CProromant/risk-equity-audit"
15
+ doi: 10.5281/zenodo.21461268
16
+ identifiers:
17
+ - type: doi
18
+ value: 10.5281/zenodo.21461268
19
+ description: "Concept DOI (resolves to the latest version)"
20
+ keywords:
21
+ - algorithmic-fairness
22
+ - health-equity
23
+ - risk-stratification
24
+ - label-choice-bias
@@ -0,0 +1,85 @@
1
+ CLAUDE.md
2
+
3
+ Read PROTOCOL.md first — it is the binding spec (architecture, data sources, phases, acceptance criteria). Execute phase by phase; do not advance with acceptance criteria unmet. This file governs how the code reads. On style, this file wins.
4
+
5
+ Voice
6
+
7
+ Write like one careful researcher who codes daily, not like a code generator. The test for every line: would a senior data scientist, writing quickly but well, produce it? If a line exists to explain the obvious, impress, or hedge, delete it.
8
+
9
+ Comments
10
+
11
+
12
+ Comments explain why, never what. If the code needs a what-comment, rewrite the code instead.
13
+ No banner comments (# ===== Load data =====), no numbered-step comments, no comment restating the line below it.
14
+ No chat residue, ever: "Here we...", "Note that...", "As requested", # rest of the code, placeholder TODOs.
15
+ A file with zero comments is normal. Real TODOs are fine only when something is genuinely deferred: # TODO: two-part model (backlog, PROTOCOL §7). Never invent one.
16
+
17
+
18
+ Docstrings
19
+
20
+
21
+ Public API of riskaudit.audit: full NumPy-style docstrings with the exact math — PROTOCOL §4.4 requires it.
22
+ Everything else: one line or nothing. No Args/Returns scaffolding on trivial functions. Never open with "This function".
23
+
24
+
25
+ Structure
26
+
27
+
28
+ Simplest thing that works. Functions over classes; a class must earn its existence with state plus behavior. No abstract bases, factories, or config objects for two parameters.
29
+ Do not extract a helper before the third real use. Duplicating twice is cheaper than the wrong abstraction.
30
+ Scripts read top to bottom: constants, functions, if __name__ == "__main__":. No framework ceremony.
31
+ Let it crash. No try/except unless the except does something specific. Validate at data boundaries (ETL); trust internal calls.
32
+ No logging framework. A terse print at script level for progress is fine. No emoji, no exclamation marks.
33
+
34
+
35
+ Naming
36
+
37
+
38
+ Domain shorthand a colleague would use: wt, k6, df, topk, oof, capture. Short names in tight scopes (i, col, m).
39
+ Banned generator vocabulary: data, result, temp, process_, handle_, my_, _list/_dict suffixes, enhanced, comprehensive, robust, utils grab-bags.
40
+ Type hints on public signatures. Skip locals and obvious internals.
41
+
42
+
43
+ Pandas / analysis idiom
44
+
45
+
46
+ Moderate method chains (2–4 steps), then a named intermediate. Never a ten-step chain; never .copy() reflexively.
47
+ Vectorize. No iterrows unless unavoidable — and then one comment saying why.
48
+ Test fixtures use MEPS-plausible values (ages 18–85, K6 in 0–24, right-skewed costs), never foo/bar or tidy round numbers.
49
+
50
+
51
+ Tests
52
+
53
+
54
+ Plain pytest functions, no test classes. One behavior per test; hand-computed expected values in the audit tests. Names describe behavior: test_capture_weights_change_result, not test_1.
55
+
56
+
57
+ README and prose
58
+
59
+
60
+ Plain and factual, first person singular where natural ("I use the Panel 26 file because..."). No emoji, no "Features 🚀", no comprehensive/leverages/seamless/state-of-the-art. Badges: CI, license, DOI — nothing else.
61
+
62
+
63
+ Commits
64
+
65
+
66
+ Subject ≤ 60 chars, imperative, conventional prefix per PROTOCOL §6. Body only when the why is not obvious: one to three plain sentences. Never bullet-list what the diff already shows. One logical change per commit — mixed commits are the strongest generator tell.
67
+
68
+
69
+ Dependencies and imports
70
+
71
+
72
+ Only what is used; stdlib first. No dependency for what ten lines of stdlib can do. Remove dead imports before committing.
73
+
74
+
75
+ Hard limits — do not "humanize"
76
+
77
+
78
+ Never introduce deliberate typos, bugs, dead code, or artificial inconsistency to look human. Human-quality means fewer defects, not planted ones.
79
+ Never rewrite git history or fake timestamps or authorship.
80
+ Formatter-enforced consistency (ruff) is normal for humans too. Keep it.
81
+
82
+
83
+ Self-check before closing any file
84
+
85
+ Read it once asking: is there any line whose existence could only be explained with "the tool put it there"? If yes, delete or rewrite until every line has a reason the author can state out loud — in a code review.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Conrado Proromant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # Windows: run these under Git Bash, or invoke the underlying scripts directly.
2
+ .PHONY: download etl models audit demo all test lint format
3
+
4
+ all: download etl models audit
5
+
6
+ download:
7
+ python scripts/download_meps.py
8
+
9
+ etl:
10
+ python scripts/build_panel.py
11
+
12
+ models:
13
+ python scripts/train_models.py
14
+
15
+ audit:
16
+ python scripts/run_audit.py
17
+
18
+ demo:
19
+ python demo/run_demo.py
20
+
21
+ test:
22
+ pytest
23
+
24
+ lint:
25
+ ruff check .
26
+
27
+ format:
28
+ ruff format .
@@ -0,0 +1,247 @@
1
+ Metadata-Version: 2.4
2
+ Name: riskaudit
3
+ Version: 0.1.1
4
+ Summary: Audit any risk-stratification model for label-choice bias — how much real need it leaves behind
5
+ Project-URL: Homepage, https://github.com/CProromant/risk-equity-audit
6
+ Project-URL: Repository, https://github.com/CProromant/risk-equity-audit
7
+ Project-URL: Changelog, https://github.com/CProromant/risk-equity-audit/blob/main/CHANGELOG.md
8
+ Author: Conrado Proromant
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: algorithmic-fairness,auditing,health-equity,label-choice-bias,risk-stratification
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Healthcare Industry
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: matplotlib
23
+ Requires-Dist: numpy
24
+ Requires-Dist: pandas
25
+ Requires-Dist: scikit-learn
26
+ Provides-Extra: dev
27
+ Requires-Dist: lightgbm; extra == 'dev'
28
+ Requires-Dist: pre-commit; extra == 'dev'
29
+ Requires-Dist: pyarrow; extra == 'dev'
30
+ Requires-Dist: pyreadstat; extra == 'dev'
31
+ Requires-Dist: pytest; extra == 'dev'
32
+ Requires-Dist: pytest-cov; extra == 'dev'
33
+ Requires-Dist: pyyaml; extra == 'dev'
34
+ Requires-Dist: requests; extra == 'dev'
35
+ Requires-Dist: ruff; extra == 'dev'
36
+ Requires-Dist: shap; extra == 'dev'
37
+ Provides-Extra: meps
38
+ Requires-Dist: lightgbm; extra == 'meps'
39
+ Requires-Dist: pyarrow; extra == 'meps'
40
+ Requires-Dist: pyreadstat; extra == 'meps'
41
+ Requires-Dist: pyyaml; extra == 'meps'
42
+ Requires-Dist: requests; extra == 'meps'
43
+ Requires-Dist: shap; extra == 'meps'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # risk-equity-audit
47
+
48
+ **Auditing label-choice bias in healthcare risk-stratification models — and the mental-health blind spot it creates.**
49
+
50
+ `riskaudit` is a Python toolkit that audits *any* risk-stratification model for **label-choice bias**: the systematic error that appears when a model is trained on the wrong proxy for "need" (typically **healthcare spending**) instead of need itself. The tool is the product; a reproducible worked example on U.S. MEPS data shows it finds real bias.
51
+
52
+ [![CI](https://github.com/CProromant/risk-equity-audit/actions/workflows/ci.yml/badge.svg)](https://github.com/CProromant/risk-equity-audit/actions/workflows/ci.yml)
53
+ ![license](https://img.shields.io/badge/license-MIT-green)
54
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21461268-blue)](https://doi.org/10.5281/zenodo.21461268)
55
+
56
+ > **Project status.** **`v0.1.0` released** and archived on Zenodo (DOI above). The `riskaudit` auditing library is implemented and tested (7 functions, design-based CIs, ~99% coverage); the MEPS case study and a self-contained demo run end to end. Built phase by phase per [`PROTOCOL.md`](PROTOCOL.md) and [`PLAN.md`](PLAN.md).
57
+
58
+ *(Versión en español más abajo — [ir al español](#español).)*
59
+
60
+ ---
61
+
62
+ ## What this is
63
+
64
+ Health systems have limited budgets for expensive programs — case management, home visits, close follow-up — so a model decides **who gets prioritized**, usually by predicting who is "high risk." The catch is *how you define* high risk. Most deployed models define it as **"who will spend the most,"** because spending is recorded for everyone.
65
+
66
+ That choice has a blind spot. Consider someone in psychological distress who **does not seek care**: their observed spending is near zero, so a spend-trained model labels them *low risk* and ignores them — and the concern is that untreated distress may surface next year as other medical need (ER visits, worsened physical illness). The need was there; the label never looked for it.
67
+
68
+ This is **label-choice bias**: a well-documented failure where an algorithm trained on cost as a proxy for need under-serves people whose need hasn't yet turned into spending. This project applies that lens to **untreated mental-health need** and packages the audit as a reusable tool.
69
+
70
+ **The claim in one sentence:** a risk model trained on healthcare spending captures barely more measured need than a coin flip and systematically under-serves people in psychological distress — because the *label* (spending), not the algorithm, decides who counts as high risk. This project measures that label-choice cost on MEPS.
71
+
72
+ ---
73
+
74
+ ## What's in the box
75
+
76
+ 1. **`riskaudit`** — an installable Python package that audits label-choice bias in any risk-stratification model. **This is the product:** a reusable tool that does not depend on any particular dataset.
77
+ 2. **A worked example on MEPS 2021–2023** — a reproducible pipeline (three targets, identical models, full audit) that shows the tool finds real bias on U.S. survey data. It is a demonstration, not the point.
78
+ 3. **End-to-end demo** — a self-contained synthetic run (`demo/run_demo.py`), with **no credentials and no PHI**, so anyone can see the tool working in seconds.
79
+
80
+ ---
81
+
82
+ ## The `riskaudit` toolkit
83
+
84
+ `riskaudit` does **not** train models or make predictions. It is an **auditor**: give it the scores a model assigned and an independent measure of real need, and it quantifies how much need the model leaves behind — with population weights and confidence intervals (a plain row bootstrap, or a design-based one over survey strata/PSUs).
85
+
86
+ Because it works purely on scores and a need measure, `riskaudit` is **domain- and country-agnostic**: the same functions audit a hospital's readmission model, an insurer's cost model, or a ministry's triage algorithm. The MEPS mental-health study below is one worked example showing the tool finds real bias — an illustration, not the tool's scope.
87
+
88
+ | Function | Question it answers |
89
+ |---|---|
90
+ | `top_k_capture(scores, need, k, weights)` | Of all real need in the population, what fraction lands in the top-*k* the model prioritizes? |
91
+ | `reclassification(scores_a, scores_b, k, weights)` | If we switch the label from A to B, **who** enters and leaves the priority list? |
92
+ | `ablation(fit_fn, X, y, feature_groups, k, weights)` | If we remove a feature group and retrain, how much does *global performance* drop vs. how much does *capture of that group* collapse? |
93
+ | `label_choice_curve(scores, need, weights, bins)` | Curve of score percentile vs. observed mean need — where does high need sit on the score? |
94
+ | `regression_to_mean(y_t, y_t1, scores_t, k, weights)` | How much of the top-*k* spending drop from year *t* to *t+1* is just regression to the mean? |
95
+ | `incremental_lift(y_t1, y_pred, distress, scores, k, weights)` | **The contribution metric:** among people the model deprioritizes, do those with measured need generate *more future outcome than predicted* than the rest? Makes the argument non-circular. |
96
+ | `audit_report(results, out_html)` | Bundles everything into a self-contained HTML report. |
97
+
98
+ **What the audit finds on MEPS** (weighted, with design-based confidence intervals): the spend-trained model captures only ~15% of top-decile K6 need — barely above the ~10% a random score gets, and far below the ~41% an oracle (ranking by need itself) reaches; a need-trained model reaches ~29%. Among the people the spend model deprioritizes, those in distress run up more *total* future cost than it predicted (incremental lift +0.8 log-dollars with the mental-health features, +1.0 without — both 95% CIs exclude zero), so the bias comes from the **label, not from missing information**.
99
+
100
+ **Honest limit.** That excess does **not** appear on non-psychiatric utilization (ER + hospitalizations; lift ≈ 0, CI includes zero), so we do *not* claim the distress surfaces specifically as non-psychiatric spending — the total-spend excess may be partly mental-health spending, and a clean test needs a non-psychiatric spend target (backlog). The severe-untreated subgroup (n ≈ 40) is descriptive only.
101
+
102
+ ---
103
+
104
+ ## Install
105
+
106
+ > Requires Python ≥ 3.11. Until the first release is published, install from source:
107
+
108
+ ```bash
109
+ git clone https://github.com/<user>/risk-equity-audit.git
110
+ cd risk-equity-audit
111
+ pip install -e ".[dev]"
112
+ ```
113
+
114
+ ## Quickstart
115
+
116
+ Minimal end-to-end audit on your own model's scores:
117
+
118
+ ```python
119
+ import numpy as np
120
+ from riskaudit.audit import top_k_capture, label_choice_curve, audit_report
121
+
122
+ rng = np.random.default_rng(2026)
123
+ n = 5_000
124
+ scores = rng.random(n) # what your risk model output per person
125
+ need = rng.random(n) # an independent measure of real need
126
+ weights = rng.random(n) # survey / population weights
127
+
128
+ capture = top_k_capture(scores, need, k=0.10, weights=weights)
129
+ curve = label_choice_curve(scores, need, weights=weights, bins=20)
130
+
131
+ print(f"Top-decile capture of need: {capture.value:.1%}")
132
+ audit_report({"capture": capture, "curve": curve}, out_html="audit.html")
133
+ ```
134
+
135
+ The audit API above runs today, and a self-contained synthetic demo shows the whole flow end to end:
136
+
137
+ ```bash
138
+ python demo/run_demo.py # no real data, no PHI, a few seconds, writes demo_report.html
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Data sources
144
+
145
+ No data is stored in this repository. Only download scripts and checksums are versioned; everything under `data/` is git-ignored.
146
+
147
+ - **Core — MEPS (AHRQ, U.S.):** HC-233 (2021), HC-243 (2022), HC-251 (2023), **HC-244 (Panel 26 Longitudinal 2021–2022)**, plus **HC-231** (2021 Conditions) and **HC-229A** (2021 Prescribed Medicines) for the mental-health treatment proxy — free, no registration; downloaded with SHA-256 checksums. See [`PROTOCOL.md`](PROTOCOL.md) §3 for exact files, weights, and codebook references.
148
+ - **Synthetic (demo):** generated in-script by `demo/run_demo.py` — no external data, no real patients.
149
+
150
+ ---
151
+
152
+ ## Limitations
153
+
154
+ Honesty about limits is a feature of this project, not a footnote:
155
+
156
+ - **The severe untreated subgroup is small** (~135 people have severe distress, K6 ≥ 13, in both panel years; the untreated subset is smaller still). It is reported **descriptively, with wide confidence intervals** — never modeled. The robust finding is the population-level mechanism, not an anecdote about invisible patients.
157
+ - **"Need" is a normative choice.** Calling a model "biased" requires asserting which target is the legitimate measure of need. That judgment is stated and defended in `docs/methods.md`, not assumed.
158
+ - **Survey design is respected throughout** — final results always use sample weights and design-based variance (a stratified cluster bootstrap over VARSTR/VARPSU, with subpopulation/domain estimation), never unweighted or naïvely filtered.
159
+
160
+ ---
161
+
162
+ ## Roadmap · Hoja de ruta
163
+
164
+ Built phase by phase (see [`PLAN.md`](PLAN.md) for task-level detail and acceptance criteria):
165
+
166
+ - **Phase 0** ✅ — Scaffolding: package layout, CI, tooling.
167
+ - **Phase 1** ✅ — MEPS ETL: verified data dictionary, cleaned panel (6,741 persons).
168
+ - **Phase 2** ✅ — Models + full weighted, design-based audit on MEPS.
169
+ - **Phase 3** ✅ — `riskaudit.audit` API (~99% coverage) + self-contained synthetic demo.
170
+ - **Phase 4** ✅ — Release `v0.1.0`, archived on Zenodo ([10.5281/zenodo.21461268](https://doi.org/10.5281/zenodo.21461268)).
171
+
172
+ ## How to cite
173
+
174
+ If you use `riskaudit`, please cite it:
175
+
176
+ > Proromant, C. (2026). *riskaudit: auditing label-choice bias in healthcare risk-stratification models* (v0.1.0) [Software]. Zenodo. https://doi.org/10.5281/zenodo.21461268
177
+
178
+ Machine-readable metadata is in [`CITATION.cff`](CITATION.cff).
179
+
180
+ ## License
181
+
182
+ [MIT](LICENSE) © Conrado — MD (PUC), MSc(c) Data Science (PUC).
183
+
184
+ ---
185
+ ---
186
+
187
+ <a name="español"></a>
188
+
189
+ # risk-equity-audit (español)
190
+
191
+ **Auditoría del sesgo por elección de etiqueta en modelos de estratificación de riesgo en salud — y el punto ciego de salud mental que produce.**
192
+
193
+ `riskaudit` es una herramienta en Python que audita *cualquier* modelo de estratificación de riesgo en busca de **sesgo por elección de la etiqueta** (*label-choice bias*): el error sistemático que aparece cuando el modelo se entrena con un proxy equivocado de "necesidad" (típicamente el **gasto sanitario**) en lugar de la necesidad misma. La herramienta es el producto; un ejemplo reproducible sobre datos de MEPS (EE.UU.) muestra que encuentra sesgo real.
194
+
195
+ > **Estado del proyecto.** **`v0.1.0` publicado** y archivado en Zenodo (DOI arriba). La librería de auditoría `riskaudit` está implementada y testeada (7 funciones, IC de diseño, ~99% de cobertura); el estudio MEPS y un demo auto-contenido corren de punta a punta. Construido por fases según [`PROTOCOL.md`](PROTOCOL.md) y [`PLAN.md`](PLAN.md).
196
+
197
+ ## Qué es esto
198
+
199
+ Los sistemas de salud tienen presupuesto limitado para programas caros —gestión de caso, visitas domiciliarias, seguimiento cercano— así que un modelo decide **a quién se prioriza**, normalmente prediciendo quién es "de alto riesgo". El problema está en *cómo se define* ese alto riesgo. La mayoría de los modelos desplegados lo definen como **"quién va a gastar más"**, porque el gasto está registrado para todos.
200
+
201
+ Esa elección tiene un punto ciego. Pensemos en una persona con distrés psíquico que **no consulta**: su gasto observado es casi cero, así que un modelo entrenado con gasto la etiqueta como *bajo riesgo* y la ignora — y la inquietud es que el distrés no tratado pueda reaparecer al año siguiente como otra necesidad médica (urgencias, enfermedades físicas agravadas). La necesidad estaba; la etiqueta nunca la buscó.
202
+
203
+ Esto es el **sesgo por elección de la etiqueta**: una falla bien documentada en la que un algoritmo entrenado con el costo como proxy de necesidad sub-atiende a quienes su necesidad aún no se tradujo en gasto. Este proyecto aplica esa mirada a la **necesidad de salud mental no tratada** y empaqueta la auditoría como herramienta reutilizable.
204
+
205
+ **La idea en una frase:** un modelo de riesgo entrenado con gasto sanitario captura la necesidad medida apenas por encima del azar y sub-atiende sistemáticamente a las personas con distrés psíquico — porque es la *etiqueta* (el gasto), no el algoritmo, la que decide quién es de alto riesgo. Este proyecto mide ese costo de la elección de etiqueta con MEPS.
206
+
207
+ ## Qué incluye
208
+
209
+ 1. **`riskaudit`** — paquete Python instalable que audita el sesgo por elección de etiqueta en cualquier modelo de estratificación de riesgo. **Es el producto:** una herramienta reutilizable que no depende de ninguna base en particular.
210
+ 2. **Un ejemplo demostrativo sobre MEPS 2021–2023** — pipeline reproducible (tres targets, modelos idénticos, auditoría completa) que muestra que la herramienta encuentra sesgo real con datos de EE.UU. Es una demostración, no el objetivo.
211
+ 3. **Demo end-to-end** — una corrida auto-contenida sobre datos sintéticos (`demo/run_demo.py`), **sin credenciales ni datos sensibles**, en segundos.
212
+
213
+ ## La herramienta `riskaudit`
214
+
215
+ `riskaudit` **no** entrena modelos ni predice. Es un **auditor**: le das los puntajes que asignó un modelo y una medida independiente de necesidad real, y cuantifica cuánta necesidad el modelo deja fuera — con pesos poblacionales e intervalos de confianza (bootstrap de filas, o de diseño sobre estratos/PSU de la encuesta).
216
+
217
+ Como trabaja solo con puntajes y una medida de necesidad, `riskaudit` es **agnóstico al dominio y al país**: las mismas funciones auditan el modelo de reingresos de un hospital, el de gasto de una aseguradora o el algoritmo de priorización de un ministerio. El estudio MEPS de salud mental de abajo es un ejemplo demostrativo que muestra que la herramienta encuentra sesgo real — una ilustración, no el alcance de la herramienta.
218
+
219
+ | Función | Pregunta que responde |
220
+ |---|---|
221
+ | `top_k_capture` | De toda la necesidad real de la población, ¿qué fracción cae en el top-*k* que prioriza el modelo? |
222
+ | `reclassification` | Si cambio la etiqueta de A a B, ¿**quién** entra y sale de la lista de prioridad? |
223
+ | `ablation` | Si quito un grupo de features y reentreno, ¿cuánto baja el *desempeño global* vs. cuánto se desploma la *captura* de ese grupo? |
224
+ | `label_choice_curve` | Curva de percentil de score vs. necesidad observada media — ¿dónde cae la necesidad alta en el score? |
225
+ | `regression_to_mean` | ¿Cuánto de la caída de gasto del top-*k* entre *t* y *t+1* es solo regresión a la media? |
226
+ | `incremental_lift` | **La métrica-contribución:** entre los que el modelo deprioriza, ¿los que tienen necesidad medida generan *más desenlace futuro del predicho* que el resto? Hace no-circular el argumento. |
227
+ | `audit_report` | Empaqueta todo en un informe HTML autocontenido. |
228
+
229
+ **Lo que encuentra la auditoría en MEPS** (ponderado, con IC de diseño): el modelo de gasto captura solo ~15% de la necesidad K6 del top-decil — apenas sobre el ~10% que daría un score al azar, y muy por debajo del ~41% de un oráculo (rankear por la propia necesidad); un modelo de K6 llega a ~29%. Entre las personas que el modelo de gasto deprioriza, las que están en distrés acumulan más gasto *total* futuro del que el modelo predijo (lift incremental +0.8 log-dólares con las features de salud mental, +1.0 sin ellas — ambos IC 95% excluyen el cero), así que el sesgo viene de la **etiqueta, no de falta de información**.
230
+
231
+ **Límite honesto.** Ese exceso **no** aparece en la utilización no-psiquiátrica (urgencias + hospitalizaciones; lift ≈ 0, IC incluye el cero), así que **no** afirmamos que el distrés se manifieste específicamente como gasto no psiquiátrico — el exceso de gasto total puede ser en parte gasto en salud mental, y un test limpio necesita un target de gasto no-psiquiátrico (backlog). El subgrupo severo no tratado (n ≈ 40) es solo descriptivo.
232
+
233
+ ## Instalación y quickstart
234
+
235
+ Igual que en la sección en inglés (Python ≥ 3.11, `pip install -e ".[dev]"`). El ejemplo mínimo de la API funciona hoy, y un demo auto-contenido muestra el flujo completo: `python demo/run_demo.py` (sin datos reales, sin PHI, segundos).
236
+
237
+ ## Limitaciones
238
+
239
+ La honestidad sobre los límites es parte del proyecto, no una nota al pie:
240
+
241
+ - **El subgrupo severo no tratado es pequeño** (~135 personas con distrés severo, K6 ≥ 13, en ambos años del panel; el subgrupo *sin tratamiento* es aún menor): se reporta **descriptivamente, con IC anchos**, nunca se modela. El hallazgo robusto es el mecanismo poblacional, no la anécdota de "los invisibles".
242
+ - **"Necesidad" es una elección normativa.** Llamar "sesgado" a un modelo exige afirmar cuál es la vara legítima de necesidad. Ese juicio se declara y defiende en `docs/methods.md`.
243
+ - **El diseño muestral se respeta siempre:** los resultados finales usan pesos y varianza basada en el diseño (bootstrap de clúster estratificado sobre VARSTR/VARPSU, con estimación de subpoblación/dominio).
244
+
245
+ ## Licencia
246
+
247
+ [MIT](LICENSE) © Conrado — Médico (PUC), MSc(c) Data Science (PUC).