carebundle 0.1.0__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 (79) hide show
  1. carebundle-0.1.0/.github/workflows/ci.yml +211 -0
  2. carebundle-0.1.0/.github/workflows/release.yml +134 -0
  3. carebundle-0.1.0/.gitignore +12 -0
  4. carebundle-0.1.0/BENCHMARK.md +250 -0
  5. carebundle-0.1.0/CHANGELOG.md +86 -0
  6. carebundle-0.1.0/CONFORMANCE.md +94 -0
  7. carebundle-0.1.0/FIDELITY.md +93 -0
  8. carebundle-0.1.0/LICENSE +202 -0
  9. carebundle-0.1.0/PKG-INFO +511 -0
  10. carebundle-0.1.0/README.md +477 -0
  11. carebundle-0.1.0/RELEASING.md +114 -0
  12. carebundle-0.1.0/ROADMAP.md +428 -0
  13. carebundle-0.1.0/carebundle/__init__.py +37 -0
  14. carebundle-0.1.0/carebundle/benchmark/__init__.py +23 -0
  15. carebundle-0.1.0/carebundle/benchmark/cqm.py +205 -0
  16. carebundle-0.1.0/carebundle/builders/__init__.py +0 -0
  17. carebundle-0.1.0/carebundle/builders/clinical.py +188 -0
  18. carebundle-0.1.0/carebundle/builders/orders.py +104 -0
  19. carebundle-0.1.0/carebundle/builders/people.py +64 -0
  20. carebundle-0.1.0/carebundle/calibration/__init__.py +0 -0
  21. carebundle-0.1.0/carebundle/calibration/custom.py +231 -0
  22. carebundle-0.1.0/carebundle/calibration/data/nhanes_targets.json +2445 -0
  23. carebundle-0.1.0/carebundle/calibration/nhanes.py +356 -0
  24. carebundle-0.1.0/carebundle/calibration/xpt.py +130 -0
  25. carebundle-0.1.0/carebundle/cli.py +276 -0
  26. carebundle-0.1.0/carebundle/conformance/__init__.py +0 -0
  27. carebundle-0.1.0/carebundle/conformance/validator.py +162 -0
  28. carebundle-0.1.0/carebundle/core/__init__.py +0 -0
  29. carebundle-0.1.0/carebundle/core/bundle.py +110 -0
  30. carebundle-0.1.0/carebundle/core/ids.py +41 -0
  31. carebundle-0.1.0/carebundle/core/safety.py +115 -0
  32. carebundle-0.1.0/carebundle/core/uscore.py +27 -0
  33. carebundle-0.1.0/carebundle/correlation/__init__.py +0 -0
  34. carebundle-0.1.0/carebundle/correlation/distributions.py +358 -0
  35. carebundle-0.1.0/carebundle/correlation/engine.py +84 -0
  36. carebundle-0.1.0/carebundle/correlation/relations.py +312 -0
  37. carebundle-0.1.0/carebundle/fidelity/__init__.py +0 -0
  38. carebundle-0.1.0/carebundle/fidelity/report.py +472 -0
  39. carebundle-0.1.0/carebundle/generate.py +462 -0
  40. carebundle-0.1.0/carebundle/history.py +260 -0
  41. carebundle-0.1.0/carebundle/imperfection/__init__.py +30 -0
  42. carebundle-0.1.0/carebundle/imperfection/defects.py +250 -0
  43. carebundle-0.1.0/carebundle/models/__init__.py +0 -0
  44. carebundle-0.1.0/carebundle/models/r4.py +1046 -0
  45. carebundle-0.1.0/carebundle/profiles/__init__.py +0 -0
  46. carebundle-0.1.0/carebundle/profiles/base.py +219 -0
  47. carebundle-0.1.0/carebundle/profiles/library.py +657 -0
  48. carebundle-0.1.0/carebundle/py.typed +0 -0
  49. carebundle-0.1.0/carebundle/spec/__init__.py +0 -0
  50. carebundle-0.1.0/carebundle/spec/codegen.py +307 -0
  51. carebundle-0.1.0/carebundle/terminology/__init__.py +0 -0
  52. carebundle-0.1.0/carebundle/terminology/codes.py +398 -0
  53. carebundle-0.1.0/carebundle/terminology/systems.py +28 -0
  54. carebundle-0.1.0/carebundle/terminology/verify.py +174 -0
  55. carebundle-0.1.0/pyproject.toml +85 -0
  56. carebundle-0.1.0/synthetic-fhir-generator-build-doc.md +658 -0
  57. carebundle-0.1.0/tests/conftest.py +37 -0
  58. carebundle-0.1.0/tests/golden/bundle-ckd_stage3.json +3432 -0
  59. carebundle-0.1.0/tests/golden/bundle-healthy.json +2832 -0
  60. carebundle-0.1.0/tests/golden/bundle-hypertension.json +3030 -0
  61. carebundle-0.1.0/tests/golden/bundle-type2_diabetes.json +3324 -0
  62. carebundle-0.1.0/tests/golden/patient-F.json +37 -0
  63. carebundle-0.1.0/tests/golden/patient-M.json +37 -0
  64. carebundle-0.1.0/tests/test_benchmark.py +160 -0
  65. carebundle-0.1.0/tests/test_bundle.py +297 -0
  66. carebundle-0.1.0/tests/test_calibration.py +246 -0
  67. carebundle-0.1.0/tests/test_cli.py +183 -0
  68. carebundle-0.1.0/tests/test_cohort.py +203 -0
  69. carebundle-0.1.0/tests/test_conformance.py +105 -0
  70. carebundle-0.1.0/tests/test_correlation.py +352 -0
  71. carebundle-0.1.0/tests/test_custom_calibration.py +187 -0
  72. carebundle-0.1.0/tests/test_determinism.py +77 -0
  73. carebundle-0.1.0/tests/test_fidelity.py +92 -0
  74. carebundle-0.1.0/tests/test_golden.py +67 -0
  75. carebundle-0.1.0/tests/test_history.py +131 -0
  76. carebundle-0.1.0/tests/test_imperfection.py +161 -0
  77. carebundle-0.1.0/tests/test_public_api.py +174 -0
  78. carebundle-0.1.0/tests/test_terminology.py +268 -0
  79. carebundle-0.1.0/tests/test_validator_harness.py +142 -0
@@ -0,0 +1,211 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+ schedule:
8
+ # Nightly full conformance matrix + fidelity report.
9
+ - cron: "0 6 * * *"
10
+ # So the nightly-only jobs can be exercised on demand. A job that has only ever been
11
+ # scheduled is a job nobody has watched fail.
12
+ workflow_dispatch:
13
+
14
+ env:
15
+ VALIDATOR_URL: https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar
16
+
17
+ jobs:
18
+ # Fast gate. No JVM — this is the loop contributors wait on.
19
+ unit:
20
+ runs-on: ${{ matrix.os }}
21
+ strategy:
22
+ # Every version in `requires-python`, because the README claims the whole range.
23
+ # Do not trim this to save minutes without trimming the claim to match.
24
+ #
25
+ # macOS and Windows carry one version each rather than the full grid: the package
26
+ # is pure Python, so the cross-platform risk is concentrated in the CLI's POSIX
27
+ # assumptions (SIGPIPE, os.dup2 on a real stdout fd) and in path handling, none of
28
+ # which vary by Python minor. `requires-python` names no platform, so leaving
29
+ # Windows untested would be claiming support nobody had checked.
30
+ fail-fast: false
31
+ matrix:
32
+ os: [ubuntu-latest]
33
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
34
+ include:
35
+ - os: macos-latest
36
+ python-version: "3.12"
37
+ - os: windows-latest
38
+ python-version: "3.12"
39
+ # Windows defaults to PowerShell, which quotes differently. Git Bash ships on the
40
+ # Windows runners, so pinning bash keeps all three platforms running the identical
41
+ # command rather than three dialects of it.
42
+ defaults:
43
+ run:
44
+ shell: bash
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ # Required, and not only for the matrix to mean anything: without it
48
+ # `uv pip install --system` targets the runner's system interpreter, which is
49
+ # PEP 668 externally-managed and refuses the install outright.
50
+ - uses: actions/setup-python@v5
51
+ with:
52
+ python-version: ${{ matrix.python-version }}
53
+ - uses: astral-sh/setup-uv@v5
54
+ - name: Install
55
+ run: uv pip install --system -e ".[dev]"
56
+ - name: Confirm the matrix version is the one under test
57
+ run: python -c "import sys; assert '.'.join(map(str, sys.version_info[:2])) == '${{ matrix.python-version }}', sys.version"
58
+ - name: Lint
59
+ run: ruff check carebundle tests
60
+ - name: Unit, property and determinism tests
61
+ run: pytest tests/ -q -m "not conformance and not fidelity" --cov --cov-report=term-missing
62
+
63
+ # Guards the artefacts themselves. The unit job installs from the source tree, so it
64
+ # cannot catch a packaging fault: a missing py.typed, a data file left out of the
65
+ # wheel, or metadata PyPI will reject. This installs the built wheel into a clean
66
+ # environment, outside the repo, and exercises the README's entry points there.
67
+ package:
68
+ runs-on: ubuntu-latest
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ - uses: actions/setup-python@v5
72
+ with:
73
+ python-version: "3.12"
74
+ - name: Build sdist and wheel
75
+ run: |
76
+ python -m pip install --quiet build twine
77
+ python -m build
78
+ - name: Check metadata renders on PyPI
79
+ run: twine check --strict dist/*
80
+ - name: Install the wheel into a clean environment
81
+ run: |
82
+ python -m venv /tmp/probe
83
+ /tmp/probe/bin/pip install --quiet dist/*.whl
84
+ - name: Smoke-test the installed package from outside the repo
85
+ working-directory: /tmp
86
+ run: |
87
+ /tmp/probe/bin/python -c "
88
+ import json, pathlib, carebundle
89
+ from carebundle import (generate_bundle, generate_history, generate_cohort,
90
+ to_json, Imperfection, inject_defects,
91
+ calibrate_profile, Quartiles)
92
+
93
+ bundle = json.loads(to_json(generate_bundle(profile='type2_diabetes', seed=42, sex='F')))
94
+ assert bundle['resourceType'] == 'Bundle', bundle['resourceType']
95
+ assert bundle['entry'], 'installed package generated an empty bundle'
96
+
97
+ # Every public entry point, because the source-tree job cannot catch a
98
+ # packaging fault and each of these is a separate import surface.
99
+ history = json.loads(to_json(generate_history(profile='hypertension', seed=42, visits=4)))
100
+ encounters = [e for e in history['entry'] if e['resource']['resourceType'] == 'Encounter']
101
+ assert len(encounters) == 4, encounters
102
+
103
+ _, defects = inject_defects(bundle, Imperfection(missing_field=0.3), seed=5)
104
+ assert defects, 'imperfection injected nothing'
105
+
106
+ calibrate_profile('ci_probe', base='type2_diabetes',
107
+ marginals={'hba1c': Quartiles(median=8.4, q1=7.5, q3=9.8, low=6.0, high=13.5),
108
+ 'glucose': Quartiles(median=190, q1=155, q3=240, low=90, high=420)})
109
+ assert json.loads(to_json(generate_bundle(profile='ci_probe', seed=42)))['entry']
110
+
111
+ assert len(list(generate_cohort(count=3, seed=1))) == 3
112
+
113
+ marker = pathlib.Path(carebundle.__file__).parent / 'py.typed'
114
+ assert marker.exists(), 'py.typed missing from the wheel'
115
+ data = pathlib.Path(carebundle.__file__).parent / 'calibration/data/nhanes_targets.json'
116
+ assert data.exists(), 'calibration data missing from the wheel'
117
+ print('installed package OK:', len(bundle['entry']), 'entries')
118
+ "
119
+ /tmp/probe/bin/carebundle profiles
120
+ /tmp/probe/bin/carebundle generate --profile healthy --seed 1 --out /tmp/fixtures
121
+ - name: A closed pipe must not print a traceback
122
+ working-directory: /tmp
123
+ run: |
124
+ /tmp/probe/bin/carebundle generate --profile healthy --seed 1 2>/tmp/err.txt | head -3
125
+ if [ -s /tmp/err.txt ]; then echo "::error::stderr noise on a closed pipe"; cat /tmp/err.txt; exit 1; fi
126
+ - uses: actions/upload-artifact@v4
127
+ with:
128
+ name: distributions
129
+ path: dist/
130
+
131
+ # Authoritative conformance gate. The JVM lives HERE and only here — it is never a
132
+ # runtime or install dependency of the published package (build doc Section 10).
133
+ conformance:
134
+ runs-on: ubuntu-latest
135
+ steps:
136
+ - uses: actions/checkout@v4
137
+ - uses: actions/setup-java@v4
138
+ with:
139
+ distribution: temurin
140
+ java-version: "17"
141
+ - uses: actions/setup-python@v5
142
+ with:
143
+ python-version: "3.12"
144
+ - uses: astral-sh/setup-uv@v5
145
+ - name: Install
146
+ run: uv pip install --system -e ".[dev]"
147
+ - name: Fetch HL7 validator
148
+ run: |
149
+ mkdir -p .tools
150
+ curl -sSL -o .tools/validator_cli.jar "$VALIDATOR_URL"
151
+ - name: Cache FHIR package registry
152
+ uses: actions/cache@v4
153
+ with:
154
+ path: ~/.fhir
155
+ key: fhir-packages-us-core-6.1.0
156
+ - name: Fetch R4 spec definitions
157
+ run: |
158
+ mkdir -p .tools/spec
159
+ curl -sSL -o .tools/spec/definitions.json.zip https://hl7.org/fhir/R4/definitions.json.zip
160
+ (cd .tools/spec && unzip -o -q definitions.json.zip)
161
+ - name: Verify generated models match the spec
162
+ run: |
163
+ python -m carebundle.spec.codegen
164
+ git diff --exit-code carebundle/models/r4.py \
165
+ || (echo "::error::carebundle/models/r4.py is stale — run 'python -m carebundle.spec.codegen' and commit" && exit 1)
166
+ - name: US Core conformance
167
+ # On PRs this runs a small sample to keep the loop tight; nightly runs the
168
+ # full profile x resource-type matrix.
169
+ run: |
170
+ if [ "${{ github.event_name }}" = "schedule" ]; then
171
+ pytest tests/ -q -m conformance --conformance-scale=full
172
+ else
173
+ pytest tests/ -q -m conformance --conformance-scale=sample
174
+ fi
175
+
176
+ # Re-check every shipped code against its source vocabulary. Curated subsets go
177
+ # stale (build doc Section 14), and a display that drifts is rejected outright by
178
+ # the HL7 validator. Nightly because it depends on three external APIs.
179
+ terminology:
180
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
181
+ runs-on: ubuntu-latest
182
+ steps:
183
+ - uses: actions/checkout@v4
184
+ - uses: actions/setup-python@v5
185
+ with:
186
+ python-version: "3.12"
187
+ - uses: astral-sh/setup-uv@v5
188
+ - name: Install
189
+ run: uv pip install --system -e ".[dev]"
190
+ - name: Verify codes against LOINC, RxNorm and ICD-10-CM
191
+ run: python -m carebundle.terminology.verify
192
+
193
+ # Statistical evidence for the clinical-coherence claim (build doc Section 8).
194
+ # Nightly only: N=10,000 patients per profile is too slow for a PR gate.
195
+ fidelity:
196
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
197
+ runs-on: ubuntu-latest
198
+ steps:
199
+ - uses: actions/checkout@v4
200
+ - uses: actions/setup-python@v5
201
+ with:
202
+ python-version: "3.12"
203
+ - uses: astral-sh/setup-uv@v5
204
+ - name: Install
205
+ run: uv pip install --system -e ".[dev]"
206
+ - name: Fidelity report
207
+ run: pytest tests/ -q -m fidelity
208
+ - uses: actions/upload-artifact@v4
209
+ with:
210
+ name: fidelity-report
211
+ path: FIDELITY.md
@@ -0,0 +1,134 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI when a v* tag is pushed. Uses Trusted Publishing (OIDC), so there
4
+ # is no API token in this repository's secrets to leak or rotate — PyPI is configured
5
+ # to trust this workflow in this repository instead.
6
+ #
7
+ # Before the first run, register the publisher at
8
+ # https://pypi.org/manage/account/publishing/ with:
9
+ # project: carebundle | owner: 27jackson08 | repo: fhirfaker
10
+ # workflow: release.yml | environment: pypi
11
+ #
12
+ # workflow_dispatch runs everything EXCEPT the publish job, so the pipeline can be
13
+ # rehearsed on a branch. Publishing is gated on `github.ref_type == 'tag'`, which a
14
+ # manual dispatch on a branch can never satisfy — the dry run cannot upload by
15
+ # accident. Rehearse before relying on this: the previous CI workflow was silently
16
+ # broken for months precisely because nobody had watched it run.
17
+ on:
18
+ push:
19
+ tags: ["v*"]
20
+ workflow_dispatch:
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ jobs:
26
+ # A tag that disagrees with pyproject is the classic release footgun: it publishes
27
+ # the wrong version under the right name, and PyPI will not let you reuse the number.
28
+ # Fail here rather than after upload.
29
+ verify:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.12"
36
+ - name: Tag must match the version in pyproject.toml
37
+ # Only meaningful on a tag; on a dispatch rehearsal GITHUB_REF_NAME is a branch.
38
+ if: github.ref_type == 'tag'
39
+ run: |
40
+ python - <<'PY'
41
+ import os, pathlib, re, sys, tomllib
42
+ tag = os.environ["GITHUB_REF_NAME"]
43
+ version = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"]
44
+ if tag != f"v{version}":
45
+ sys.exit(f"::error::tag {tag} does not match pyproject version {version} (expected v{version})")
46
+ changelog = pathlib.Path("CHANGELOG.md").read_text()
47
+ if not re.search(rf"^## \[{re.escape(version)}\]", changelog, re.M):
48
+ sys.exit(f"::error::CHANGELOG.md has no '## [{version}]' section")
49
+ print(f"tag {tag} matches pyproject and CHANGELOG")
50
+ PY
51
+
52
+ # Do not publish something the gates have not passed. This duplicates ci.yml
53
+ # deliberately: a tag can be pushed at a commit CI never ran on.
54
+ gates:
55
+ needs: verify
56
+ runs-on: ubuntu-latest
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+ - uses: actions/setup-java@v4
60
+ with:
61
+ distribution: temurin
62
+ java-version: "17"
63
+ # Without this, `uv pip install --system` targets the runner's PEP 668
64
+ # externally-managed system interpreter and the install fails.
65
+ - uses: actions/setup-python@v5
66
+ with:
67
+ python-version: "3.12"
68
+ - uses: astral-sh/setup-uv@v5
69
+ - name: Install
70
+ run: uv pip install --system -e ".[dev]"
71
+ - name: Lint
72
+ run: ruff check carebundle tests
73
+ - name: Unit, property and determinism tests
74
+ run: pytest tests/ -q -m "not conformance and not fidelity" --cov --cov-report=term-missing
75
+ - name: Fetch HL7 validator and R4 definitions
76
+ run: |
77
+ mkdir -p .tools/spec
78
+ curl -sSL -o .tools/validator_cli.jar \
79
+ "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
80
+ curl -sSL -o .tools/spec/definitions.json.zip https://hl7.org/fhir/R4/definitions.json.zip
81
+ (cd .tools/spec && unzip -o -q definitions.json.zip)
82
+ - name: US Core conformance (full matrix)
83
+ run: pytest tests/ -q -m conformance --conformance-scale=full
84
+
85
+ build:
86
+ needs: gates
87
+ runs-on: ubuntu-latest
88
+ steps:
89
+ - uses: actions/checkout@v4
90
+ - uses: actions/setup-python@v5
91
+ with:
92
+ python-version: "3.12"
93
+ - name: Build
94
+ run: |
95
+ python -m pip install --quiet build twine
96
+ python -m build
97
+ - name: Check metadata
98
+ run: twine check --strict dist/*
99
+ - name: Smoke-test the wheel outside the repo
100
+ run: |
101
+ python -m venv /tmp/probe
102
+ /tmp/probe/bin/pip install --quiet dist/*.whl
103
+ cd /tmp && /tmp/probe/bin/python -c "
104
+ import json, carebundle
105
+ b = json.loads(carebundle.to_json(
106
+ carebundle.generate_bundle(profile='type2_diabetes', seed=42, sex='F')))
107
+ assert b['resourceType'] == 'Bundle' and b['entry']
108
+ print('wheel OK:', len(b['entry']), 'entries')
109
+ "
110
+ - uses: actions/upload-artifact@v4
111
+ with:
112
+ name: dist
113
+ path: dist/
114
+
115
+ publish:
116
+ needs: build
117
+ # Tags only. A workflow_dispatch rehearsal on a branch stops after `build`, so the
118
+ # irreversible step cannot be reached by anything except pushing a version tag.
119
+ if: github.ref_type == 'tag'
120
+ runs-on: ubuntu-latest
121
+ # The environment gives you a manual approval gate: enable required reviewers on
122
+ # the `pypi` environment and the upload waits for a human. PyPI uploads cannot be
123
+ # undone — a deleted version number can never be reused.
124
+ environment:
125
+ name: pypi
126
+ url: https://pypi.org/p/carebundle
127
+ permissions:
128
+ id-token: write
129
+ steps:
130
+ - uses: actions/download-artifact@v4
131
+ with:
132
+ name: dist
133
+ path: dist/
134
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .coverage
7
+ build/
8
+ dist/
9
+ # Dev/CI tooling: the validator jar and FHIR spec archives are fetched, not vendored.
10
+ .tools/
11
+ # macOS Finder metadata — never wanted in a published sdist.
12
+ .DS_Store
@@ -0,0 +1,250 @@
1
+ # Clinical quality measure benchmark
2
+
3
+ Regenerate with `pytest -m fidelity tests/test_benchmark.py`. Measures are computed
4
+ from the **emitted FHIR**, not from the internal draw — a measure engine reading the
5
+ sampler's own state would be marking its own homework.
6
+
7
+ ## Why this benchmark and not another
8
+
9
+ Synthea's published validation ([Chen et al., *BMC Med Inform Decis Mak* 2019](https://pmc.ncbi.nlm.nih.gov/articles/PMC6416981/))
10
+ measured it against four CMS clinical quality measures. The result splits cleanly:
11
+
12
+ | Measure | Type | Synthea | Real (MA) | Real (US) |
13
+ |---|---|---:|---:|---:|
14
+ | Colorectal cancer screening | process | 68.7% | 77.3% | 69.8% |
15
+ | COPD 30-day mortality | outcome | 0.7% | 7.0% | 8.0% |
16
+ | Complications after hip/knee replacement | outcome | **0%** | 2.9% | 2.8% |
17
+ | Controlling high blood pressure | outcome | **0%** | 74.5% | 69.7% |
18
+
19
+ Synthea tracks reality on the process measure and collapses on every outcome measure.
20
+ The authors name the cause: synthetic generators "do not currently model for deviations
21
+ in care and the potential outcomes that may result from care deviations."
22
+
23
+ That is a statement about architecture, not a bug report. A state machine over care
24
+ pathways can decide *whether a patient was screened*. It has no representation of what
25
+ the blood pressure did after the thiazide started, so a control rate cannot emerge from
26
+ it. This project models clinical state directly, which is the machinery an outcome
27
+ measure needs.
28
+
29
+ ## Results
30
+
31
+ | Measure | Synthea | **carebundle** | Real (US) | Real (MA) |
32
+ |---|---:|---:|---:|---:|
33
+ | Controlling high blood pressure (CBP) | 0% | **71.5%** | 69.7% | 74.5% |
34
+ | Colorectal cancer screening | 68.7% | *not modelled* | 69.8% | 77.3% |
35
+ | COPD 30-day mortality | 0.7% | *not modelled* | 8.0% | 7.0% |
36
+ | Complications after hip/knee replacement | 0% | *not modelled* | 2.8% | 2.9% |
37
+
38
+ **The three "not modelled" rows are published deliberately.** A benchmark that quietly
39
+ dropped the measures it loses would be worthless as evidence. *Not modelled* is a
40
+ different claim from 0%, and the measure engine keeps them distinguishable: a zero
41
+ denominator is reported as a zero denominator, never as a rate.
42
+
43
+ **They are also not going to be modelled**, and the reason is licensing rather than
44
+ effort. All three need `Procedure` resources, and the procedures in question have no
45
+ realistic public-domain coding:
46
+
47
+ - Screening colonoscopy is **CPT 45378** in the ambulatory setting where screening
48
+ happens; CPT is AMA-licensed. Hip and knee replacement likewise.
49
+ - SNOMED CT needs an affiliate licence and bars redistribution.
50
+ - ICD-10-PCS *is* public domain and US Core accepts it, so `0DJD8ZZ` would validate —
51
+ but PCS is inpatient facility coding, and no real US ambulatory system emits it for a
52
+ screening colonoscopy. Conformant and wrong. Checked and rejected rather than used,
53
+ because winning a benchmark row with a code no real system emits would forfeit the
54
+ realism claim the row is meant to evidence.
55
+
56
+ The measures this project can reach are those defined over labs, vitals and diagnoses —
57
+ which it codes with LOINC and ICD-10-CM. That is a real and narrow boundary, and it is
58
+ better stated than discovered.
59
+
60
+ ### Controlling high blood pressure, in detail
61
+
62
+ Denominator is the HEDIS/NCQA definition — ages 18–85, coded hypertension diagnosis,
63
+ an outpatient encounter, and a recorded blood pressure. Numerator is a most-recent
64
+ reading below **140/90**, with both components required.
65
+
66
+ | Population | Denominator | CBP rate |
67
+ |---|---:|---:|
68
+ | `hypertension` profile | 1500 | 70.3% |
69
+ | `type2_diabetes` profile | 1045 | 70.8% |
70
+ | `ckd_stage3` profile | 1197 | 71.8% |
71
+ | **Mixed cohort (prevalence-drawn)** | **1822 / 4000** | **71.5%** |
72
+ | `healthy` profile | 0 | *not in denominator* |
73
+
74
+ ## How the number was produced, and why it is not tuned
75
+
76
+ This is the part that matters. A benchmark you fit to is worthless, so every input
77
+ comes from a cited source and the rate is what those inputs imply.
78
+
79
+ **Inputs (fitted to sources):**
80
+
81
+ 1. **Who is treated.** NHANES, August 2021–August 2023 ([NCHS Data Brief](https://www.ncbi.nlm.nih.gov/books/NBK612761/)):
82
+ 59.2% of US adults with hypertension are aware of it, 51.2% take medication. So
83
+ 51.2 / 59.2 = **86.5% of diagnosed hypertensives are on treatment**, and
84
+ diagnosed-and-in-care is exactly the population these profiles represent. The
85
+ per-drug prescribing probabilities are *solved* for that fraction rather than
86
+ hand-written, so the cited target and the emitted probabilities cannot drift apart.
87
+ 2. **How much treatment lowers pressure.** Law MR, Wald NJ, Morris JK, *BMJ*
88
+ 2003;326:1427 — a meta-analysis of 354 randomised placebo-controlled trials. One
89
+ standard-dose agent lowers blood pressure by **9.1 systolic / 5.5 diastolic**, and
90
+ the reduction is larger from a higher starting pressure (1.0 mmHg more systolic per
91
+ 10 mmHg higher pre-treatment). A [2025 *Lancet* meta-analysis](https://pubmed.ncbi.nlm.nih.gov/40885583/)
92
+ of 484 trials puts monotherapy at 8.7 (95% CI 8.2–9.2), bracketing the same figure.
93
+
94
+ **Output (not fitted):** the control rate. Nothing in the model was adjusted to reach
95
+ 71.5%. Change either cited input and the number moves — which is exactly what happened
96
+ when titration was added, and is documented below rather than smoothed over.
97
+
98
+ The mechanism is the same computed-identity discipline used for eGFR and Friedewald
99
+ LDL: the copula draws a *pre-treatment* pressure, the prescribing rules escalate on it
100
+ (you add an agent because the patient is uncontrolled), and the *recorded* pressure is
101
+ then computed from the regimen the patient actually received. Effects are applied per
102
+ distinct drug class sequentially, so Law's baseline-dependence produces diminishing
103
+ returns by construction rather than by a fudge factor. A bundle therefore cannot
104
+ prescribe three antihypertensives beside an untreated-looking 168/102.
105
+
106
+ ## The titration hypothesis, tested and confirmed
107
+
108
+ The first version of this benchmark measured **64.1%** against a US comparator of
109
+ 69.7% and published the 5.6-point shortfall rather than closing it, along with a
110
+ specific, falsifiable explanation:
111
+
112
+ > The most likely mechanism is dose titration: HEDIS scores the *most recent* reading
113
+ > of a year in which clinicians repeatedly re-measure and escalate until the patient
114
+ > reaches goal, whereas this models a single visit with a fixed regimen at standard
115
+ > doses.
116
+
117
+ **That prediction was then tested and it held.** Modelling titration moved the rate
118
+ from 64.1% to **71.5%**, between the US (69.7%) and Massachusetts (74.5%) comparators.
119
+
120
+ What makes this a real test rather than a fit:
121
+
122
+ - The titration effect size comes from a **different study** than the base effect —
123
+ the [2025 *Lancet* meta-analysis](https://pubmed.ncbi.nlm.nih.gov/40885583/) of 484
124
+ trials, at 1.5 mmHg systolic per dose doubling — so the correction was not
125
+ calibrated against the residual it was predicting.
126
+ - Escalation is **conditional on being above goal**, which is what titration is. A
127
+ patient already at target is never escalated, so the mechanism cannot inflate the
128
+ control rate from the wrong end by pushing controlled patients further down.
129
+ - The ceiling of two doublings is clinically motivated (beyond roughly four times
130
+ standard dose, another agent is preferred to another doubling), not fitted. Raising
131
+ it would push the rate higher; it was fixed before the rate was measured.
132
+
133
+ The direction of the original error also ruled out the obvious alternative. Law's
134
+ figures are **trial efficacy**, and real-world effectiveness is normally *lower*
135
+ because of adherence — so an adherence gap would have made the modelled rate too
136
+ **high**, not too low. Undershooting pointed at a missing treatment intensity, which
137
+ is what titration supplies.
138
+
139
+ ## Non-adherence: a prediction that failed, and was not shipped
140
+
141
+ The previous section predicted that adding non-adherence "should push the rate down".
142
+ It does — by far too much to be right.
143
+
144
+ | Model | CBP rate |
145
+ |---|---:|
146
+ | Current (no explicit adherence term) | 74.7% |
147
+ | 45% of patients take nothing ([Abegaz 2017 meta-analysis](https://pubmed.ncbi.nlm.nih.gov/28121920/)) | 50.4% |
148
+ | Effect scaled by proportion-of-days-covered (62.2%) | 49.3% |
149
+ | **Real-world comparators** | **69.7% – 74.5%** |
150
+
151
+ Both formulations land roughly 20 points **below** reality. The prediction was right
152
+ about direction and badly wrong about magnitude, and the honest conclusion is that the
153
+ naive adherence term is a worse model, not a missing one. It is therefore **not
154
+ shipped**. Two reasons it fails:
155
+
156
+ - **The population-wide adherence figures do not transfer to this denominator.** HEDIS
157
+ CBP counts patients who are diagnosed *and* have an outpatient encounter — people
158
+ engaged enough with care to show up. Adherence meta-analyses are drawn from a much
159
+ broader population. Applying the broader figure to the narrower denominator imports
160
+ a selection effect that the denominator has already excluded.
161
+ - **"Non-adherent" is not "untreated".** A patient with 62% days covered takes most of
162
+ their doses and gets most of the benefit. Modelling them as receiving nothing is a
163
+ category error, and the PDC-scaled variant fails for the related reason that response
164
+ is not linear in coverage.
165
+
166
+ Recording this matters more than the result. The rate is close to reality *without*
167
+ this term, so a naive version would have made the benchmark worse while sounding more
168
+ sophisticated — and the reason it is excluded is that it contradicts the data by 20
169
+ points, which is model falsification rather than parameter fitting. If adherence is
170
+ modelled later it needs a dose-response formulation and a denominator-matched source.
171
+
172
+ ## Why there is no second measure: CMS122, and what it taught
173
+
174
+ The obvious candidate for a second measure was **CMS122, Diabetes: HbA1c Poor Control
175
+ (>9%)**. It needs only a diagnosis and a lab result, both of which this project codes
176
+ with ICD-10-CM and LOINC, so unlike the colonoscopy measures it is not blocked by
177
+ licensing. It is blocked by something more interesting.
178
+
179
+ **The measure has no single national rate.** CMS's own
180
+ [2024 quality benchmarks](https://www.cms.gov/files/document/2024-quality-benchmarks.csv)
181
+ publish three, for the same measure in the same year:
182
+
183
+ | Collection type | Average performance rate |
184
+ |---|---:|
185
+ | Medicare Part B Claims | 11.70% |
186
+ | MIPS CQM | 27.30% |
187
+ | eCQM (CMS122v12) | 43.53% |
188
+
189
+ A four-fold spread is not measurement noise, and the numerator definition explains it:
190
+
191
+ > "Patients whose most recent HbA1c level (performed during the measurement period) is
192
+ > >9.0% **or is missing, or was not performed** during the measurement period"
193
+
194
+ CMS122 does not measure glycaemic control. It measures glycaemic control **plus
195
+ testing completeness**, summed into one number. The more a collection method depends on
196
+ complete EHR capture, the more untested patients inflate the rate — which is exactly the
197
+ ordering above.
198
+
199
+ **A generator with perfect data capture cannot reproduce that.** Every diabetic here has
200
+ an HbA1c, so this project would always score at the "everyone was tested" floor. Our
201
+ generated rate above 9.0% is 12.9%, which sits near the claims figure and near NHANES's
202
+ measured 12.9% — and that agreement would be *misleading* to publish as reproducing
203
+ CMS122, because the measure it would claim to reproduce is counting something else.
204
+
205
+ Two things follow, and both are worth more than the row would have been.
206
+
207
+ **The blocker generalises.** Any quality measure whose numerator includes "or is
208
+ missing" is partly a data-completeness measure, and synthetic data with complete capture
209
+ can only ever reproduce the clinical half. That is a structural limit on benchmarking
210
+ synthetic generators against real quality measures, and it applies to Synthea equally.
211
+
212
+ **It is also a use for the imperfection module.** `carebundle.imperfection` exists to
213
+ omit fields on purpose. A future version could model the missing-result component
214
+ explicitly and reproduce the full measure — the only route to CMS122 that would not be
215
+ quietly comparing two different quantities.
216
+
217
+ **A caveat this exposed in the CBP measure above.** HEDIS CBP has the same shape: a
218
+ member with no blood-pressure reading counts as not controlled. This implementation
219
+ instead requires a reading to enter the denominator. It makes no difference to the
220
+ number, because every generated patient has one — but it means the 71.5% figure is a
221
+ *clinical* control rate compared against real-world rates that blend control with
222
+ capture. The real-world comparators are therefore, if anything, slightly pessimistic
223
+ relative to what is being measured here.
224
+
225
+ ## Remaining caveats
226
+
227
+ - The comparators are from 2019 and reflect the plans and years measured then. The
228
+ national HEDIS figure has sat in the low-to-mid 60s in other years.
229
+ - Titration is modelled here as an equilibrium: the recorded pressure is where a
230
+ titrated patient ends up, which is the right value for a most-recent-reading measure.
231
+ The trajectory behind it is now available separately via `generate_history`, which
232
+ emits the same patient across several reviews as therapy is escalated.
233
+
234
+ ## What this does and does not establish
235
+
236
+ It establishes that a distributional model reproduces an outcome measure that a
237
+ pathway simulator scores **0%** on, from independently cited inputs, checked in CI.
238
+
239
+ It does not establish that this is a better synthetic data generator than Synthea.
240
+ Synthea covers 231 conditions, a lifetime per patient, and three of the four measures
241
+ in this very table. On breadth it is not close, and [ROADMAP.md](ROADMAP.md) does not
242
+ propose competing there.
243
+
244
+ ## Sources
245
+
246
+ - [Chen J et al., "The validity of synthetic clinical data … (Synthea) using clinical quality measures", *BMC Med Inform Decis Mak* 2019](https://pmc.ncbi.nlm.nih.gov/articles/PMC6416981/)
247
+ - [NCHS Data Brief: Hypertension Prevalence, Awareness, Treatment, and Control, US, Aug 2021–Aug 2023](https://www.ncbi.nlm.nih.gov/books/NBK612761/)
248
+ - [Law MR, Wald NJ, Morris JK, "Value of low dose combination treatment with blood pressure lowering drugs", *BMJ* 2003;326:1427](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC162261/)
249
+ - [Blood pressure-lowering efficacy of antihypertensive drugs and their combinations, *Lancet* 2025](https://pubmed.ncbi.nlm.nih.gov/40885583/)
250
+ - [NCQA HEDIS: Controlling High Blood Pressure (CBP)](https://www.ncqa.org/report-cards/health-plans/state-of-health-care-quality-report/controlling-high-blood-pressure-cbp/)