cohort-diff 0.1.0__py3-none-any.whl
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.
- cohort_diff/__init__.py +20 -0
- cohort_diff/cli.py +104 -0
- cohort_diff/compare.py +184 -0
- cohort_diff/definitions/aki_icd.sql +8 -0
- cohort_diff/definitions/aki_kdigo_48h.sql +20 -0
- cohort_diff/definitions/aki_kdigo_7d.sql +22 -0
- cohort_diff/definitions/dm_hba1c.sql +9 -0
- cohort_diff/definitions/dm_icd.sql +8 -0
- cohort_diff/definitions/dm_meds.sql +10 -0
- cohort_diff/definitions/readmit30_any.sql +12 -0
- cohort_diff/definitions/readmit30_excl.sql +17 -0
- cohort_diff/engine.py +80 -0
- cohort_diff/py.typed +0 -0
- cohort_diff/registry.py +56 -0
- cohort_diff/synth.py +188 -0
- cohort_diff-0.1.0.dist-info/METADATA +235 -0
- cohort_diff-0.1.0.dist-info/RECORD +19 -0
- cohort_diff-0.1.0.dist-info/WHEEL +4 -0
- cohort_diff-0.1.0.dist-info/entry_points.txt +3 -0
cohort_diff/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""cohort-diff: how different are "the same" EHR cohorts?"""
|
|
2
|
+
|
|
3
|
+
from cohort_diff.compare import bootstrap_pairs, describe, jaccard, overlap_table
|
|
4
|
+
from cohort_diff.engine import connect, run
|
|
5
|
+
from cohort_diff.registry import Definition, builtin, load
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Definition",
|
|
11
|
+
"__version__",
|
|
12
|
+
"bootstrap_pairs",
|
|
13
|
+
"builtin",
|
|
14
|
+
"connect",
|
|
15
|
+
"describe",
|
|
16
|
+
"jaccard",
|
|
17
|
+
"load",
|
|
18
|
+
"overlap_table",
|
|
19
|
+
"run",
|
|
20
|
+
]
|
cohort_diff/cli.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Command-line interface: ``cohort-diff <command> --help``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from cohort_diff import compare, engine, registry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _list(args: argparse.Namespace) -> None:
|
|
13
|
+
for d in registry.builtin().values():
|
|
14
|
+
if args.condition and d.condition != args.condition:
|
|
15
|
+
continue
|
|
16
|
+
print(f"{d.name:<16} {d.condition:<12} {d.description}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run(args: argparse.Namespace) -> None:
|
|
20
|
+
con = engine.connect(args.data)
|
|
21
|
+
ids = sorted(engine.run(con, registry.load(args.definition)))
|
|
22
|
+
out = "\n".join(["hadm_id", *map(str, ids)]) + "\n"
|
|
23
|
+
if args.out:
|
|
24
|
+
Path(args.out).write_text(out)
|
|
25
|
+
print(f"{len(ids)} admissions -> {args.out}")
|
|
26
|
+
else:
|
|
27
|
+
sys.stdout.write(out)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _compare(args: argparse.Namespace) -> None:
|
|
31
|
+
defs = [registry.load(n) for n in args.definitions]
|
|
32
|
+
if args.condition:
|
|
33
|
+
defs = [d for d in registry.builtin().values() if d.condition == args.condition]
|
|
34
|
+
if len(defs) < 2:
|
|
35
|
+
raise ValueError("compare needs at least two definitions")
|
|
36
|
+
con = engine.connect(args.data)
|
|
37
|
+
cohorts = {d.name: engine.run(con, d) for d in defs}
|
|
38
|
+
admissions = engine.admissions_frame(con)
|
|
39
|
+
uncertainty = (
|
|
40
|
+
compare.bootstrap_pairs(cohorts, admissions, n_boot=args.bootstrap, seed=args.seed)
|
|
41
|
+
if args.bootstrap > 0
|
|
42
|
+
else None
|
|
43
|
+
)
|
|
44
|
+
report = compare.to_markdown(
|
|
45
|
+
compare.overlap_table(cohorts),
|
|
46
|
+
compare.describe(cohorts, admissions),
|
|
47
|
+
title=f"cohort-diff: {', '.join(cohorts)}",
|
|
48
|
+
uncertainty=uncertainty,
|
|
49
|
+
)
|
|
50
|
+
if args.out:
|
|
51
|
+
Path(args.out).write_text(report)
|
|
52
|
+
print(report)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _synth(args: argparse.Namespace) -> None:
|
|
56
|
+
from cohort_diff.synth import SynthConfig, generate
|
|
57
|
+
|
|
58
|
+
hosp = Path(args.out) / "hosp"
|
|
59
|
+
hosp.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
for name, frame in generate(SynthConfig(n_patients=args.patients, seed=args.seed)).items():
|
|
61
|
+
frame.to_csv(hosp / f"{name}.csv.gz", index=False)
|
|
62
|
+
print(f"wrote synthetic MIMIC-shaped tables to {hosp} (synthetic data, not real patients)")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main(argv: list[str] | None = None) -> int:
|
|
66
|
+
parser = argparse.ArgumentParser(prog="cohort-diff", description=__doc__)
|
|
67
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
68
|
+
|
|
69
|
+
p = sub.add_parser("list", help="show built-in definitions")
|
|
70
|
+
p.add_argument("--condition")
|
|
71
|
+
p.set_defaults(func=_list)
|
|
72
|
+
|
|
73
|
+
p = sub.add_parser("run", help="admissions meeting one definition")
|
|
74
|
+
p.add_argument("definition", help="built-in name or path to a .sql file")
|
|
75
|
+
p.add_argument("--data", required=True)
|
|
76
|
+
p.add_argument("--out")
|
|
77
|
+
p.set_defaults(func=_run)
|
|
78
|
+
|
|
79
|
+
p = sub.add_parser("compare", help="overlap and downstream differences between definitions")
|
|
80
|
+
p.add_argument("definitions", nargs="*", help="two or more names or .sql paths")
|
|
81
|
+
p.add_argument("--condition", help="compare every built-in definition of this condition")
|
|
82
|
+
p.add_argument("--data", required=True)
|
|
83
|
+
p.add_argument("--out", help="write the markdown report here")
|
|
84
|
+
p.add_argument("--bootstrap", type=int, default=1000, help="resamples (0 = skip)")
|
|
85
|
+
p.add_argument("--seed", type=int, default=0)
|
|
86
|
+
p.set_defaults(func=_compare)
|
|
87
|
+
|
|
88
|
+
p = sub.add_parser("synth", help="write synthetic MIMIC-shaped tables")
|
|
89
|
+
p.add_argument("--out", required=True)
|
|
90
|
+
p.add_argument("--patients", type=int, default=2000)
|
|
91
|
+
p.add_argument("--seed", type=int, default=0)
|
|
92
|
+
p.set_defaults(func=_synth)
|
|
93
|
+
|
|
94
|
+
args = parser.parse_args(argv)
|
|
95
|
+
try:
|
|
96
|
+
args.func(args)
|
|
97
|
+
except (FileNotFoundError, KeyError, ValueError) as exc:
|
|
98
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
99
|
+
return 2
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
raise SystemExit(main())
|
cohort_diff/compare.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Compare cohorts: overlap, who is in them, and how a downstream result moves.
|
|
2
|
+
|
|
3
|
+
The downstream result is deliberately simple and common in papers: the odds
|
|
4
|
+
ratio between cohort membership and in-hospital mortality. If that number
|
|
5
|
+
changes a lot between definitions of "the same" condition, so would the
|
|
6
|
+
conclusions of any study built on it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from itertools import combinations
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class OddsRatio:
|
|
21
|
+
estimate: float
|
|
22
|
+
low: float
|
|
23
|
+
high: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def jaccard(a: set[int], b: set[int]) -> float:
|
|
27
|
+
"""Size of the intersection over size of the union; 1.0 when both are empty."""
|
|
28
|
+
union = a | b
|
|
29
|
+
return len(a & b) / len(union) if union else 1.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def overlap_table(cohorts: dict[str, set[int]]) -> pd.DataFrame:
|
|
33
|
+
"""One row per pair of definitions."""
|
|
34
|
+
rows = []
|
|
35
|
+
for (na, a), (nb, b) in combinations(cohorts.items(), 2):
|
|
36
|
+
rows.append(
|
|
37
|
+
{
|
|
38
|
+
"a": na,
|
|
39
|
+
"b": nb,
|
|
40
|
+
"n_a": len(a),
|
|
41
|
+
"n_b": len(b),
|
|
42
|
+
"both": len(a & b),
|
|
43
|
+
"only_a": len(a - b),
|
|
44
|
+
"only_b": len(b - a),
|
|
45
|
+
"jaccard": round(jaccard(a, b), 3),
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
return pd.DataFrame(rows)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def odds_ratio(exposed: np.ndarray, outcome: np.ndarray) -> OddsRatio:
|
|
52
|
+
"""Woolf (log) 95% interval, with a 0.5 continuity correction if any cell is 0."""
|
|
53
|
+
e, y = exposed.astype(bool), outcome.astype(bool)
|
|
54
|
+
cells = np.array([(e & y).sum(), (e & ~y).sum(), (~e & y).sum(), (~e & ~y).sum()], dtype=float)
|
|
55
|
+
if (cells == 0).any():
|
|
56
|
+
cells = cells + 0.5
|
|
57
|
+
a, b, c, d = cells
|
|
58
|
+
log_or = math.log((a * d) / (b * c))
|
|
59
|
+
se = math.sqrt(1 / a + 1 / b + 1 / c + 1 / d)
|
|
60
|
+
return OddsRatio(math.exp(log_or), math.exp(log_or - 1.96 * se), math.exp(log_or + 1.96 * se))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _weighted_or(weights: np.ndarray, exposed: np.ndarray, outcome: np.ndarray) -> float:
|
|
64
|
+
"""Odds ratio from a 2x2 table of summed weights (Haldane 0.5 if any cell is 0)."""
|
|
65
|
+
cells = np.array(
|
|
66
|
+
[
|
|
67
|
+
weights @ (exposed & outcome),
|
|
68
|
+
weights @ (exposed & ~outcome),
|
|
69
|
+
weights @ (~exposed & outcome),
|
|
70
|
+
weights @ (~exposed & ~outcome),
|
|
71
|
+
],
|
|
72
|
+
dtype=float,
|
|
73
|
+
)
|
|
74
|
+
if (cells == 0).any():
|
|
75
|
+
cells = cells + 0.5
|
|
76
|
+
a, b, c, d = cells
|
|
77
|
+
return float((a * d) / (b * c))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def bootstrap_pairs(
|
|
81
|
+
cohorts: dict[str, set[int]],
|
|
82
|
+
admissions: pd.DataFrame,
|
|
83
|
+
n_boot: int = 1000,
|
|
84
|
+
seed: int = 0,
|
|
85
|
+
) -> pd.DataFrame:
|
|
86
|
+
"""95% percentile intervals, per pair of definitions, for the Jaccard index and
|
|
87
|
+
for the ratio of their mortality odds ratios (OR of a / OR of b).
|
|
88
|
+
|
|
89
|
+
Patients are resampled, not admissions: one patient's admissions are not
|
|
90
|
+
independent. A patient drawn k times contributes each of their admissions k times.
|
|
91
|
+
A ratio of ORs whose interval excludes 1 means the two definitions give
|
|
92
|
+
different answers to "is this condition associated with mortality, and how much".
|
|
93
|
+
"""
|
|
94
|
+
patients, patient_idx = np.unique(admissions["subject_id"].to_numpy(), return_inverse=True)
|
|
95
|
+
n_patients = len(patients)
|
|
96
|
+
died = admissions["hospital_expire_flag"].to_numpy().astype(bool)
|
|
97
|
+
member = {n: admissions["hadm_id"].isin(ids).to_numpy() for n, ids in cohorts.items()}
|
|
98
|
+
pairs = list(combinations(member, 2))
|
|
99
|
+
jac = np.empty((len(pairs), n_boot))
|
|
100
|
+
ratio = np.empty((len(pairs), n_boot))
|
|
101
|
+
rng = np.random.default_rng(seed)
|
|
102
|
+
for r in range(n_boot):
|
|
103
|
+
drawn = np.bincount(rng.integers(0, n_patients, n_patients), minlength=n_patients)
|
|
104
|
+
weights = drawn[patient_idx].astype(float)
|
|
105
|
+
for k, (na, nb) in enumerate(pairs):
|
|
106
|
+
a, b = member[na], member[nb]
|
|
107
|
+
union = weights @ (a | b)
|
|
108
|
+
jac[k, r] = (weights @ (a & b)) / union if union > 0 else 1.0
|
|
109
|
+
ratio[k, r] = _weighted_or(weights, a, died) / _weighted_or(weights, b, died)
|
|
110
|
+
rows = []
|
|
111
|
+
for k, (na, nb) in enumerate(pairs):
|
|
112
|
+
a, b = member[na], member[nb]
|
|
113
|
+
point = odds_ratio(a, died).estimate / odds_ratio(b, died).estimate
|
|
114
|
+
j_low, j_high = np.percentile(jac[k], [2.5, 97.5])
|
|
115
|
+
r_low, r_high = np.percentile(ratio[k], [2.5, 97.5])
|
|
116
|
+
rows.append(
|
|
117
|
+
{
|
|
118
|
+
"a": na,
|
|
119
|
+
"b": nb,
|
|
120
|
+
"jaccard": round(jaccard(cohorts[na], cohorts[nb]), 3),
|
|
121
|
+
"jaccard_low": round(float(j_low), 3),
|
|
122
|
+
"jaccard_high": round(float(j_high), 3),
|
|
123
|
+
"or_ratio": round(point, 2),
|
|
124
|
+
"or_ratio_low": round(float(r_low), 2),
|
|
125
|
+
"or_ratio_high": round(float(r_high), 2),
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
return pd.DataFrame(rows)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def describe(cohorts: dict[str, set[int]], admissions: pd.DataFrame) -> pd.DataFrame:
|
|
132
|
+
"""Size, prevalence, demographics and the mortality odds ratio per definition."""
|
|
133
|
+
rows = []
|
|
134
|
+
n_total = len(admissions)
|
|
135
|
+
for name, ids in cohorts.items():
|
|
136
|
+
member = admissions["hadm_id"].isin(ids).to_numpy()
|
|
137
|
+
sub = admissions[member]
|
|
138
|
+
ratio = odds_ratio(member, admissions["hospital_expire_flag"].to_numpy())
|
|
139
|
+
rows.append(
|
|
140
|
+
{
|
|
141
|
+
"definition": name,
|
|
142
|
+
"n": int(member.sum()),
|
|
143
|
+
"prevalence": round(member.mean(), 4) if n_total else float("nan"),
|
|
144
|
+
"age_mean": round(float(sub["age"].mean()), 1) if len(sub) else float("nan"),
|
|
145
|
+
"female_pct": round(float((sub["gender"] == "F").mean() * 100), 1)
|
|
146
|
+
if len(sub)
|
|
147
|
+
else float("nan"),
|
|
148
|
+
"mortality_pct": round(float(sub["hospital_expire_flag"].mean() * 100), 1)
|
|
149
|
+
if len(sub)
|
|
150
|
+
else float("nan"),
|
|
151
|
+
"mortality_or": round(ratio.estimate, 2),
|
|
152
|
+
"or_low": round(ratio.low, 2),
|
|
153
|
+
"or_high": round(ratio.high, 2),
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
return pd.DataFrame(rows)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def to_markdown(
|
|
160
|
+
overlap: pd.DataFrame,
|
|
161
|
+
description: pd.DataFrame,
|
|
162
|
+
title: str,
|
|
163
|
+
uncertainty: pd.DataFrame | None = None,
|
|
164
|
+
) -> str:
|
|
165
|
+
def table(frame: pd.DataFrame) -> str:
|
|
166
|
+
header = "| " + " | ".join(frame.columns) + " |"
|
|
167
|
+
sep = "| " + " | ".join("---" for _ in frame.columns) + " |"
|
|
168
|
+
body = [
|
|
169
|
+
"| " + " | ".join(str(v) for v in row) + " |" for row in frame.itertuples(index=False)
|
|
170
|
+
]
|
|
171
|
+
return "\n".join([header, sep, *body])
|
|
172
|
+
|
|
173
|
+
text = (
|
|
174
|
+
f"# {title}\n\n## Cohorts\n\n{table(description)}\n\n"
|
|
175
|
+
f"## Pairwise overlap\n\n{table(overlap)}\n\n"
|
|
176
|
+
"Odds ratios: in-hospital mortality for members vs non-members, Woolf 95% CI.\n"
|
|
177
|
+
)
|
|
178
|
+
if uncertainty is not None:
|
|
179
|
+
text += (
|
|
180
|
+
f"\n## Uncertainty (patient-level bootstrap)\n\n{table(uncertainty)}\n\n"
|
|
181
|
+
"95% percentile intervals. or_ratio = mortality OR of a / OR of b; "
|
|
182
|
+
"an interval excluding 1 means the definitions disagree on the association.\n"
|
|
183
|
+
)
|
|
184
|
+
return text
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- name: aki_icd
|
|
2
|
+
-- condition: aki
|
|
3
|
+
-- description: Any AKI diagnosis code on the admission (ICD-10 N17.*, ICD-9 584.*).
|
|
4
|
+
-- reference: Common administrative-data definition; codes are assigned after discharge.
|
|
5
|
+
SELECT DISTINCT hadm_id
|
|
6
|
+
FROM diagnoses_icd
|
|
7
|
+
WHERE (icd_version = 10 AND icd_code LIKE 'N17%')
|
|
8
|
+
OR (icd_version = 9 AND icd_code LIKE '584%')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- name: aki_kdigo_48h
|
|
2
|
+
-- condition: aki
|
|
3
|
+
-- description: Serum creatinine rises by >= 0.3 mg/dL within 48 hours, within one admission.
|
|
4
|
+
-- reference: KDIGO 2012 AKI guideline, creatinine criterion 1 (urine-output criterion not used).
|
|
5
|
+
WITH cr AS (
|
|
6
|
+
SELECT l.hadm_id, l.charttime, l.valuenum
|
|
7
|
+
FROM labevents l
|
|
8
|
+
JOIN d_labitems d ON d.itemid = l.itemid
|
|
9
|
+
WHERE lower(d.label) = 'creatinine' AND d.fluid = 'Blood'
|
|
10
|
+
AND l.valuenum IS NOT NULL AND l.hadm_id IS NOT NULL
|
|
11
|
+
)
|
|
12
|
+
SELECT DISTINCT later.hadm_id
|
|
13
|
+
FROM cr AS later
|
|
14
|
+
JOIN cr AS earlier
|
|
15
|
+
ON earlier.hadm_id = later.hadm_id
|
|
16
|
+
AND earlier.charttime < later.charttime
|
|
17
|
+
AND earlier.charttime >= later.charttime - INTERVAL 48 HOUR
|
|
18
|
+
-- Round the difference: in floating point 1.2 - 0.9 = 0.29999999999999993,
|
|
19
|
+
-- which would silently miss a true 0.3 mg/dL rise.
|
|
20
|
+
WHERE round(later.valuenum - earlier.valuenum, 6) >= 0.3
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- name: aki_kdigo_7d
|
|
2
|
+
-- condition: aki
|
|
3
|
+
-- description: Serum creatinine >= 1.5 x the lowest value in the prior 7 days, within one admission.
|
|
4
|
+
-- reference: KDIGO 2012 AKI guideline, creatinine criterion 2 (baseline = in-admission minimum).
|
|
5
|
+
WITH cr AS (
|
|
6
|
+
SELECT l.hadm_id, l.charttime, l.valuenum
|
|
7
|
+
FROM labevents l
|
|
8
|
+
JOIN d_labitems d ON d.itemid = l.itemid
|
|
9
|
+
WHERE lower(d.label) = 'creatinine' AND d.fluid = 'Blood'
|
|
10
|
+
AND l.valuenum IS NOT NULL AND l.hadm_id IS NOT NULL
|
|
11
|
+
),
|
|
12
|
+
flagged AS (
|
|
13
|
+
SELECT later.hadm_id
|
|
14
|
+
FROM cr AS later
|
|
15
|
+
JOIN cr AS earlier
|
|
16
|
+
ON earlier.hadm_id = later.hadm_id
|
|
17
|
+
AND earlier.charttime < later.charttime
|
|
18
|
+
AND earlier.charttime >= later.charttime - INTERVAL 7 DAY
|
|
19
|
+
GROUP BY later.hadm_id, later.charttime, later.valuenum
|
|
20
|
+
HAVING round(later.valuenum - 1.5 * min(earlier.valuenum), 6) >= 0
|
|
21
|
+
)
|
|
22
|
+
SELECT DISTINCT hadm_id FROM flagged
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- name: dm_hba1c
|
|
2
|
+
-- condition: diabetes
|
|
3
|
+
-- description: Any HbA1c >= 6.5% during the admission.
|
|
4
|
+
-- reference: ADA diagnostic threshold for HbA1c (a single in-hospital value, so an approximation).
|
|
5
|
+
SELECT DISTINCT l.hadm_id
|
|
6
|
+
FROM labevents l
|
|
7
|
+
JOIN d_labitems d ON d.itemid = l.itemid
|
|
8
|
+
WHERE lower(d.label) IN ('% hemoglobin a1c', 'hemoglobin a1c')
|
|
9
|
+
AND l.valuenum >= 6.5 AND l.hadm_id IS NOT NULL
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- name: dm_icd
|
|
2
|
+
-- condition: diabetes
|
|
3
|
+
-- description: Any diabetes diagnosis code (ICD-10 E10-E14, ICD-9 250.*).
|
|
4
|
+
-- reference: Common administrative-data definition.
|
|
5
|
+
SELECT DISTINCT hadm_id
|
|
6
|
+
FROM diagnoses_icd
|
|
7
|
+
WHERE (icd_version = 10 AND substr(icd_code, 1, 3) IN ('E10', 'E11', 'E13', 'E14'))
|
|
8
|
+
OR (icd_version = 9 AND icd_code LIKE '250%')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- name: dm_meds
|
|
2
|
+
-- condition: diabetes
|
|
3
|
+
-- description: Any glucose-lowering medication ordered (insulin, metformin, sulfonylureas, DPP-4 inhibitors, SGLT2 inhibitors, GLP-1 agonists).
|
|
4
|
+
-- reference: Medication-based phenotype; insulin for stress hyperglycemia will over-capture.
|
|
5
|
+
SELECT DISTINCT hadm_id
|
|
6
|
+
FROM prescriptions
|
|
7
|
+
WHERE hadm_id IS NOT NULL AND regexp_matches(
|
|
8
|
+
lower(drug),
|
|
9
|
+
'insulin|metformin|glipizide|glyburide|glimepiride|sitagliptin|linagliptin|empagliflozin|dapagliflozin|canagliflozin|liraglutide|semaglutide|dulaglutide'
|
|
10
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- name: readmit30_any
|
|
2
|
+
-- condition: readmission
|
|
3
|
+
-- description: Index admissions followed by any admission of the same patient within 30 days of discharge.
|
|
4
|
+
-- reference: The simplest definition used in many MIMIC papers.
|
|
5
|
+
WITH ordered AS (
|
|
6
|
+
SELECT hadm_id, dischtime,
|
|
7
|
+
lead(admittime) OVER (PARTITION BY subject_id ORDER BY admittime) AS next_admit
|
|
8
|
+
FROM admissions
|
|
9
|
+
)
|
|
10
|
+
SELECT hadm_id
|
|
11
|
+
FROM ordered
|
|
12
|
+
WHERE next_admit IS NOT NULL AND next_admit <= dischtime + INTERVAL 30 DAY
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- name: readmit30_excl
|
|
2
|
+
-- condition: readmission
|
|
3
|
+
-- description: As readmit30_any, but excludes index admissions ending in death, hospice or discharge against advice, and planned (elective) readmissions.
|
|
4
|
+
-- reference: Inspired by the CMS Hospital Readmissions Reduction Program exclusions (simplified).
|
|
5
|
+
WITH ordered AS (
|
|
6
|
+
SELECT hadm_id, dischtime, hospital_expire_flag, discharge_location,
|
|
7
|
+
lead(admittime) OVER (PARTITION BY subject_id ORDER BY admittime) AS next_admit,
|
|
8
|
+
lead(admission_type) OVER (PARTITION BY subject_id ORDER BY admittime) AS next_type
|
|
9
|
+
FROM admissions
|
|
10
|
+
)
|
|
11
|
+
SELECT hadm_id
|
|
12
|
+
FROM ordered
|
|
13
|
+
WHERE next_admit IS NOT NULL
|
|
14
|
+
AND next_admit <= dischtime + INTERVAL 30 DAY
|
|
15
|
+
AND hospital_expire_flag = 0
|
|
16
|
+
AND coalesce(discharge_location, '') NOT IN ('HOSPICE', 'AGAINST ADVICE', 'DIED')
|
|
17
|
+
AND upper(coalesce(next_type, '')) NOT LIKE '%ELECTIVE%'
|
cohort_diff/engine.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Run cohort definitions with DuckDB over MIMIC-IV-shaped tables.
|
|
2
|
+
|
|
3
|
+
DuckDB reads the demo's ``.csv.gz`` files in place, so nothing has to be
|
|
4
|
+
loaded into a server first.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import duckdb
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from cohort_diff.registry import Definition
|
|
15
|
+
|
|
16
|
+
TABLES = ("patients", "admissions", "labevents", "d_labitems", "diagnoses_icd", "prescriptions")
|
|
17
|
+
OPTIONAL = {"prescriptions"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _find(root: Path, table: str) -> Path | None:
|
|
21
|
+
for candidate in (
|
|
22
|
+
root / "hosp" / f"{table}.csv.gz",
|
|
23
|
+
root / "hosp" / f"{table}.csv",
|
|
24
|
+
root / f"{table}.csv.gz",
|
|
25
|
+
root / f"{table}.csv",
|
|
26
|
+
):
|
|
27
|
+
if candidate.exists():
|
|
28
|
+
return candidate
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def connect(data: str | Path | dict[str, pd.DataFrame]) -> duckdb.DuckDBPyConnection:
|
|
33
|
+
"""An in-memory DuckDB connection with one view per MIMIC table.
|
|
34
|
+
|
|
35
|
+
``data`` is a MIMIC-IV root (containing ``hosp/``), a flat CSV directory, or a
|
|
36
|
+
dict of DataFrames (used by the tests).
|
|
37
|
+
"""
|
|
38
|
+
con = duckdb.connect()
|
|
39
|
+
if isinstance(data, dict):
|
|
40
|
+
for name, frame in data.items():
|
|
41
|
+
con.register(f"_{name}", frame)
|
|
42
|
+
con.execute(f"CREATE VIEW {name} AS SELECT * FROM _{name}")
|
|
43
|
+
return con
|
|
44
|
+
root = Path(data)
|
|
45
|
+
for table in TABLES:
|
|
46
|
+
path = _find(root, table)
|
|
47
|
+
if path is None:
|
|
48
|
+
if table in OPTIONAL:
|
|
49
|
+
con.execute(
|
|
50
|
+
f"CREATE VIEW {table} AS SELECT NULL::BIGINT AS subject_id, "
|
|
51
|
+
"NULL::BIGINT AS hadm_id, NULL::TIMESTAMP AS starttime, "
|
|
52
|
+
"NULL::TIMESTAMP AS stoptime, NULL::VARCHAR AS drug WHERE false"
|
|
53
|
+
)
|
|
54
|
+
continue
|
|
55
|
+
raise FileNotFoundError(f"{table}: expected {root}/hosp/{table}.csv.gz")
|
|
56
|
+
# Views can't take bound parameters, so quote the path as an SQL literal.
|
|
57
|
+
literal = str(path).replace("'", "''")
|
|
58
|
+
con.execute(
|
|
59
|
+
f"CREATE VIEW {table} AS SELECT * FROM read_csv_auto('{literal}', header = true)"
|
|
60
|
+
)
|
|
61
|
+
return con
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run(con: duckdb.DuckDBPyConnection, definition: Definition) -> set[int]:
|
|
65
|
+
"""The admissions (``hadm_id``) that meet a definition."""
|
|
66
|
+
rows = con.execute(definition.sql).fetchall()
|
|
67
|
+
return {int(r[0]) for r in rows if r[0] is not None}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def admissions_frame(con: duckdb.DuckDBPyConnection) -> pd.DataFrame:
|
|
71
|
+
"""One row per admission with the fields used to describe cohorts."""
|
|
72
|
+
return con.execute(
|
|
73
|
+
"""
|
|
74
|
+
SELECT a.hadm_id, a.subject_id, a.hospital_expire_flag,
|
|
75
|
+
date_diff('hour', a.admittime, a.dischtime) / 24.0 AS los_days,
|
|
76
|
+
p.anchor_age + (year(a.admittime) - p.anchor_year) AS age,
|
|
77
|
+
p.gender
|
|
78
|
+
FROM admissions a JOIN patients p ON p.subject_id = a.subject_id
|
|
79
|
+
"""
|
|
80
|
+
).df()
|
cohort_diff/py.typed
ADDED
|
File without changes
|
cohort_diff/registry.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Cohort definitions: SQL files with a small metadata header.
|
|
2
|
+
|
|
3
|
+
Each file in ``cohort_diff/definitions`` starts with ``-- key: value`` lines
|
|
4
|
+
(``name``, ``condition``, ``description``, ``reference``) followed by one query
|
|
5
|
+
that returns a single column, ``hadm_id``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from importlib import resources
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
REQUIRED_KEYS = ("name", "condition", "description", "reference")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Definition:
|
|
19
|
+
name: str
|
|
20
|
+
condition: str
|
|
21
|
+
description: str
|
|
22
|
+
reference: str
|
|
23
|
+
sql: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse(text: str) -> Definition:
|
|
27
|
+
meta: dict[str, str] = {}
|
|
28
|
+
body: list[str] = []
|
|
29
|
+
for line in text.splitlines():
|
|
30
|
+
stripped = line.strip()
|
|
31
|
+
if not body and stripped.startswith("--") and ":" in stripped:
|
|
32
|
+
key, value = stripped[2:].split(":", 1)
|
|
33
|
+
meta[key.strip()] = value.strip()
|
|
34
|
+
elif stripped or body:
|
|
35
|
+
body.append(line)
|
|
36
|
+
missing = [k for k in REQUIRED_KEYS if k not in meta]
|
|
37
|
+
if missing:
|
|
38
|
+
raise ValueError(f"definition header missing {missing}")
|
|
39
|
+
return Definition(sql="\n".join(body).strip(), **{k: meta[k] for k in REQUIRED_KEYS})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def builtin() -> dict[str, Definition]:
|
|
43
|
+
"""All definitions shipped with the package, keyed by name."""
|
|
44
|
+
folder = resources.files("cohort_diff") / "definitions"
|
|
45
|
+
defs = [parse(f.read_text()) for f in folder.iterdir() if f.name.endswith(".sql")]
|
|
46
|
+
return {d.name: d for d in sorted(defs, key=lambda d: d.name)}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load(name_or_path: str) -> Definition:
|
|
50
|
+
"""A built-in definition by name, or a user's ``.sql`` file by path."""
|
|
51
|
+
if name_or_path.endswith(".sql"):
|
|
52
|
+
return parse(Path(name_or_path).read_text())
|
|
53
|
+
defs = builtin()
|
|
54
|
+
if name_or_path not in defs:
|
|
55
|
+
raise KeyError(f"unknown definition {name_or_path!r}; known: {', '.join(defs)}")
|
|
56
|
+
return defs[name_or_path]
|
cohort_diff/synth.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Copied from ehr-asof (same author) so this repo stays self-contained.
|
|
2
|
+
"""Synthetic data shaped like MIMIC-IV's hosp module.
|
|
3
|
+
|
|
4
|
+
The generator exists so the code and tests run anywhere. It is not a model of
|
|
5
|
+
real patients: it plants the two timing problems the project studies, so
|
|
6
|
+
every number it produces must be labelled *synthetic*.
|
|
7
|
+
|
|
8
|
+
1. Lab results become available (``storetime``) after the sample is taken
|
|
9
|
+
(``charttime``). Some labs, such as cultures, take hours to days.
|
|
10
|
+
2. Diagnosis codes carry no timestamp. They are assigned at discharge, and
|
|
11
|
+
sicker patients, including those who die, receive more severe codes.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
# (itemid, label, fluid, category, mean, sd, effect of severity, median delay in hours)
|
|
22
|
+
LAB_ITEMS: list[tuple[int, str, str, str, float, float, float, float]] = [
|
|
23
|
+
(50912, "Creatinine", "Blood", "Chemistry", 1.0, 0.3, 0.6, 1.5),
|
|
24
|
+
(50971, "Potassium", "Blood", "Chemistry", 4.1, 0.4, 0.3, 1.5),
|
|
25
|
+
(50983, "Sodium", "Blood", "Chemistry", 139.0, 3.0, -2.0, 1.5),
|
|
26
|
+
(51301, "White Blood Cells", "Blood", "Hematology", 8.0, 2.5, 3.0, 1.0),
|
|
27
|
+
(51222, "Hemoglobin", "Blood", "Hematology", 12.5, 1.5, -1.0, 1.0),
|
|
28
|
+
(50813, "Lactate", "Blood", "Blood Gas", 1.4, 0.5, 1.2, 0.5),
|
|
29
|
+
(50852, "% Hemoglobin A1c", "Blood", "Chemistry", 5.7, 0.8, 0.2, 30.0),
|
|
30
|
+
(51003, "Troponin T", "Blood", "Chemistry", 0.02, 0.02, 0.15, 3.0),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# ICD-10 codes; the last group is assigned mostly to patients who die.
|
|
34
|
+
COMMON_CODES = ["I10", "E119", "E785", "N179", "J189", "I509", "K219", "F329"]
|
|
35
|
+
TERMINAL_CODES = ["R6521", "I469", "Z515"] # septic shock, cardiac arrest, palliative care
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class SynthConfig:
|
|
40
|
+
n_patients: int = 2000
|
|
41
|
+
admissions_per_patient: float = 1.4
|
|
42
|
+
seed: int = 0
|
|
43
|
+
start: str = "2150-01-01"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate(config: SynthConfig | None = None) -> dict[str, pd.DataFrame]:
|
|
47
|
+
"""Return MIMIC-IV-shaped tables: patients, admissions, labevents, d_labitems,
|
|
48
|
+
diagnoses_icd and prescriptions."""
|
|
49
|
+
cfg = config or SynthConfig()
|
|
50
|
+
rng = np.random.default_rng(cfg.seed)
|
|
51
|
+
start = pd.Timestamp(cfg.start)
|
|
52
|
+
|
|
53
|
+
subject_ids = np.arange(10_000_000, 10_000_000 + cfg.n_patients)
|
|
54
|
+
patients = pd.DataFrame(
|
|
55
|
+
{
|
|
56
|
+
"subject_id": subject_ids,
|
|
57
|
+
"gender": rng.choice(["F", "M"], size=cfg.n_patients),
|
|
58
|
+
"anchor_age": rng.integers(18, 91, size=cfg.n_patients),
|
|
59
|
+
"anchor_year": 2150,
|
|
60
|
+
"anchor_year_group": "2017 - 2019",
|
|
61
|
+
"dod": pd.NaT,
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
frailty = rng.normal(0, 1, size=cfg.n_patients)
|
|
65
|
+
|
|
66
|
+
adm_rows = []
|
|
67
|
+
hadm_id = 20_000_000
|
|
68
|
+
for i, sid in enumerate(subject_ids):
|
|
69
|
+
n_adm = 1 + rng.poisson(cfg.admissions_per_patient - 1)
|
|
70
|
+
t = start + pd.Timedelta(days=float(rng.uniform(0, 365)))
|
|
71
|
+
for _ in range(n_adm):
|
|
72
|
+
hadm_id += 1
|
|
73
|
+
severity = 0.7 * frailty[i] + rng.normal(0, 1) + 0.02 * (patients.anchor_age[i] - 60)
|
|
74
|
+
los_hours = float(
|
|
75
|
+
np.clip(rng.lognormal(np.log(96) + 0.25 * severity, 0.6), 30, 60 * 24)
|
|
76
|
+
)
|
|
77
|
+
p_death = 1 / (1 + np.exp(-(-3.6 + 1.4 * severity)))
|
|
78
|
+
died = bool(rng.random() < p_death)
|
|
79
|
+
admit = t + pd.Timedelta(minutes=int(rng.integers(0, 24 * 60)))
|
|
80
|
+
disch = admit + pd.Timedelta(hours=los_hours)
|
|
81
|
+
adm_rows.append(
|
|
82
|
+
{
|
|
83
|
+
"subject_id": sid,
|
|
84
|
+
"hadm_id": hadm_id,
|
|
85
|
+
"admittime": admit,
|
|
86
|
+
"dischtime": disch,
|
|
87
|
+
"deathtime": disch if died else pd.NaT,
|
|
88
|
+
"admission_type": rng.choice(
|
|
89
|
+
["EW EMER.", "URGENT", "ELECTIVE"], p=[0.6, 0.25, 0.15]
|
|
90
|
+
),
|
|
91
|
+
"admission_location": "EMERGENCY ROOM",
|
|
92
|
+
"discharge_location": "DIED"
|
|
93
|
+
if died
|
|
94
|
+
else rng.choice(["HOME", "SNF", "HOSPICE"], p=[0.75, 0.2, 0.05]),
|
|
95
|
+
"hospital_expire_flag": int(died),
|
|
96
|
+
"latent_severity": severity,
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
if died:
|
|
100
|
+
break # no admissions after death
|
|
101
|
+
t = disch + pd.Timedelta(days=float(rng.exponential(60)))
|
|
102
|
+
admissions = pd.DataFrame(adm_rows)
|
|
103
|
+
|
|
104
|
+
lab_rows = []
|
|
105
|
+
labevent_id = 0
|
|
106
|
+
for adm in admissions.itertuples(index=False):
|
|
107
|
+
los = (adm.dischtime - adm.admittime).total_seconds() / 3600
|
|
108
|
+
n_draws = max(1, int(los // 12))
|
|
109
|
+
for k in range(n_draws):
|
|
110
|
+
draw = adm.admittime + pd.Timedelta(hours=float(k * 12 + rng.uniform(0, 4)))
|
|
111
|
+
# severity drifts upward towards death
|
|
112
|
+
drift = 1.5 * (k / max(n_draws - 1, 1)) if adm.hospital_expire_flag else 0.0
|
|
113
|
+
for itemid, _label, _fluid, _cat, mu, sd, eff, delay in LAB_ITEMS:
|
|
114
|
+
if itemid == 50852 and k > 0:
|
|
115
|
+
continue # HbA1c once per admission
|
|
116
|
+
if rng.random() < 0.15:
|
|
117
|
+
continue
|
|
118
|
+
labevent_id += 1
|
|
119
|
+
value = mu + 0.5 * eff * (adm.latent_severity + drift) + rng.normal(0, sd)
|
|
120
|
+
value = max(value, 0.01)
|
|
121
|
+
store = draw + pd.Timedelta(hours=float(rng.lognormal(np.log(delay), 0.8)))
|
|
122
|
+
lab_rows.append(
|
|
123
|
+
{
|
|
124
|
+
"labevent_id": labevent_id,
|
|
125
|
+
"subject_id": adm.subject_id,
|
|
126
|
+
"hadm_id": adm.hadm_id,
|
|
127
|
+
"specimen_id": labevent_id,
|
|
128
|
+
"itemid": itemid,
|
|
129
|
+
"charttime": draw,
|
|
130
|
+
"storetime": store if rng.random() > 0.02 else pd.NaT,
|
|
131
|
+
"value": f"{value:.2f}",
|
|
132
|
+
"valuenum": round(value, 2),
|
|
133
|
+
"valueuom": "",
|
|
134
|
+
"flag": None,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
labevents = pd.DataFrame(lab_rows)
|
|
138
|
+
|
|
139
|
+
d_labitems = pd.DataFrame(
|
|
140
|
+
[(i, lab, fl, cat) for i, lab, fl, cat, *_ in LAB_ITEMS],
|
|
141
|
+
columns=["itemid", "label", "fluid", "category"],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
dx_rows = []
|
|
145
|
+
rx_rows = []
|
|
146
|
+
for adm in admissions.itertuples(index=False):
|
|
147
|
+
codes: list[str] = []
|
|
148
|
+
for code in COMMON_CODES:
|
|
149
|
+
p = 1 / (1 + np.exp(-(-1.8 + 0.5 * adm.latent_severity)))
|
|
150
|
+
if rng.random() < p:
|
|
151
|
+
codes.append(code)
|
|
152
|
+
if adm.hospital_expire_flag:
|
|
153
|
+
codes += [c for c in TERMINAL_CODES if rng.random() < 0.7]
|
|
154
|
+
elif rng.random() < 0.03:
|
|
155
|
+
codes.append(rng.choice(TERMINAL_CODES))
|
|
156
|
+
for seq, code in enumerate(codes, start=1):
|
|
157
|
+
dx_rows.append(
|
|
158
|
+
{
|
|
159
|
+
"subject_id": adm.subject_id,
|
|
160
|
+
"hadm_id": adm.hadm_id,
|
|
161
|
+
"seq_num": seq,
|
|
162
|
+
"icd_code": code,
|
|
163
|
+
"icd_version": 10,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
if "E119" in codes and rng.random() < 0.8:
|
|
167
|
+
rx_rows.append(
|
|
168
|
+
{
|
|
169
|
+
"subject_id": adm.subject_id,
|
|
170
|
+
"hadm_id": adm.hadm_id,
|
|
171
|
+
"starttime": adm.admittime + pd.Timedelta(hours=6),
|
|
172
|
+
"stoptime": adm.dischtime,
|
|
173
|
+
"drug": rng.choice(["Metformin", "Insulin", "Glipizide"]),
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
diagnoses_icd = pd.DataFrame(dx_rows)
|
|
177
|
+
prescriptions = pd.DataFrame(
|
|
178
|
+
rx_rows, columns=["subject_id", "hadm_id", "starttime", "stoptime", "drug"]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"patients": patients,
|
|
183
|
+
"admissions": admissions.drop(columns="latent_severity"),
|
|
184
|
+
"labevents": labevents,
|
|
185
|
+
"d_labitems": d_labitems,
|
|
186
|
+
"diagnoses_icd": diagnoses_icd,
|
|
187
|
+
"prescriptions": prescriptions,
|
|
188
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cohort-diff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: How different are 'the same' EHR cohorts? Run published definitions side by side on MIMIC-IV and diff them.
|
|
5
|
+
Author: Eric Yang
|
|
6
|
+
Author-email: Eric Yang <159638721+yangpings@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: duckdb>=1.0
|
|
9
|
+
Requires-Dist: numpy>=1.26
|
|
10
|
+
Requires-Dist: pandas>=2.1
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# cohort-diff
|
|
15
|
+
|
|
16
|
+
**Keywords:** EHR · MIMIC-IV · cohort definitions · phenotyping · acute kidney injury · diabetes · 30-day readmission · reproducibility · odds ratio · Jaccard index · bootstrap confidence intervals
|
|
17
|
+
|
|
18
|
+
**Skills:** Python · SQL · DuckDB · pandas · NumPy · patient-level bootstrap · uv packaging · PyPI (Trusted Publishing) · GitHub Actions CI · pytest · Hypothesis (property-based testing) · test oracles · mypy (strict) · ruff · AI-assisted engineering (Claude Code with CLAUDE.md, tests first, AI log)
|
|
19
|
+
|
|
20
|
+
How different are "the same" EHR cohorts? `cohort-diff` runs several definitions of one condition side by side on MIMIC-IV-shaped data and reports how much the cohorts, and the results built on them, disagree.
|
|
21
|
+
|
|
22
|
+
## Description
|
|
23
|
+
|
|
24
|
+
A cohort definition is a modelling decision, but papers usually report it in a sentence. Two studies that both say "AKI patients in MIMIC-IV" may share only a fraction of their admissions: one counts discharge diagnosis codes, another a creatinine rise within 48 hours, another a rise over 7 days. A finding such as "AKI is associated with higher in-hospital mortality" can change size with the definition. `cohort-diff` stores each definition as a versioned SQL file with its source, runs them on DuckDB directly over MIMIC's `.csv.gz` files, and reports overlap, who is in each cohort, and how a typical downstream association moves.
|
|
25
|
+
|
|
26
|
+
## Objective
|
|
27
|
+
|
|
28
|
+
1. Build a tested, pip-installable library that runs cohort definitions as reviewable SQL and checks each one against an independent implementation.
|
|
29
|
+
2. Measure how much definitions of the same condition (AKI, diabetes, 30-day readmission) disagree, in membership and in a downstream result (the odds ratio between membership and in-hospital mortality), with confidence intervals.
|
|
30
|
+
3. *(Extension, branch `cohort-diff-llm`.)* Measure how often a large language model, given a paper's methods text, drafts a cohort definition that passes the same oracle tests as the hand-written one.
|
|
31
|
+
|
|
32
|
+
## Hypotheses
|
|
33
|
+
|
|
34
|
+
- **H1.** Definitions of the same condition select substantially different admissions (pairwise Jaccard well below 1).
|
|
35
|
+
- **H2.** Code-based and lab-based definitions differ systematically: codes under-capture, and lab rules with fixed thresholds over-capture when measurements are frequent and noisy.
|
|
36
|
+
- **H3.** The mortality odds ratio reported for "the same" condition changes materially with the definition: for some pairs, the 95% interval of the ratio of their odds ratios excludes 1.
|
|
37
|
+
- **H4.** A definition limits which questions its cohort can answer (for example, readmission exclusions that remove every death).
|
|
38
|
+
- **H5.** *(Extension.)* LLM-drafted definitions often run without error but fail the oracle tests at clinically important details (thresholds, time windows, specimen type, boundary conditions), so tests, not inspection, decide whether a draft can be used.
|
|
39
|
+
|
|
40
|
+
## Samples
|
|
41
|
+
|
|
42
|
+
| Dataset | Access | Size | Role |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| Synthetic, MIMIC-shaped (`cohort-diff synth`, seed 0) | Generated | 2,000 patients, 2,738 admissions | Known-structure validation; numbers say nothing about real patients |
|
|
45
|
+
| MIMIC-IV Clinical Database Demo v2.2 | Open (ODbL) | 100 patients, 275 admissions | End-to-end run on real EHR data |
|
|
46
|
+
|
|
47
|
+
The demo shares the full MIMIC-IV schema, so the same definitions run on full MIMIC-IV (credentialed) unchanged.
|
|
48
|
+
|
|
49
|
+
## Methods
|
|
50
|
+
|
|
51
|
+
- **Definitions as data.** Each definition is one SQL file in `src/cohort_diff/definitions/` with a header (`name`, `condition`, `description`, `reference`) and returns one column, `hadm_id`. Adding a definition does not touch Python.
|
|
52
|
+
- **Built-in definitions.**
|
|
53
|
+
|
|
54
|
+
| Name | Condition | Rule |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `aki_icd` | AKI | ICD-10 N17.* or ICD-9 584.* on the admission |
|
|
57
|
+
| `aki_kdigo_48h` | AKI | Creatinine rises ≥ 0.3 mg/dL within 48 h (KDIGO 2012, criterion 1) |
|
|
58
|
+
| `aki_kdigo_7d` | AKI | Creatinine ≥ 1.5 × the lowest value in the prior 7 days (KDIGO 2012, criterion 2) |
|
|
59
|
+
| `dm_icd` | Diabetes | ICD-10 E10/E11/E13/E14 or ICD-9 250.* |
|
|
60
|
+
| `dm_hba1c` | Diabetes | Any HbA1c ≥ 6.5% during the admission |
|
|
61
|
+
| `dm_meds` | Diabetes | Any glucose-lowering medication |
|
|
62
|
+
| `readmit30_any` | Readmission | Any admission within 30 days of discharge |
|
|
63
|
+
| `readmit30_excl` | Readmission | As above, excluding deaths, hospice, discharge against advice and elective readmissions (inspired by CMS HRRP, simplified) |
|
|
64
|
+
|
|
65
|
+
- **Engine.** DuckDB, in process, reads the MIMIC tables in place (demo folder layout or flat CSVs) as views. Lab items are found by label and fluid, not by itemid.
|
|
66
|
+
- **Correctness.** Every rule-based definition is compared with an independent, deliberately slow pandas implementation (a test oracle written with loops rather than joins), on the synthetic data and on hundreds of random creatinine series generated by Hypothesis. Hand-picked regression tests pin threshold edge cases that both implementations could get wrong together.
|
|
67
|
+
- **Comparison.** For every pair of definitions: admissions in both, in only one, and the Jaccard index |A ∩ B| / |A ∪ B|. For each cohort: size, prevalence, mean age, % female, in-hospital mortality.
|
|
68
|
+
- **Downstream result.** Unadjusted odds ratio between membership and in-hospital mortality, with a Woolf 95% CI (log OR ± 1.96·√(1/a + 1/b + 1/c + 1/d)); 0.5 is added to every cell when any cell is 0 (Haldane–Anscombe).
|
|
69
|
+
- **Uncertainty.** Patient-level bootstrap (1,000 resamples; a patient drawn k times contributes all their admissions k times) gives 95% percentile intervals for each pair's Jaccard index and for the ratio of their mortality odds ratios (OR of a / OR of b). An interval for the ratio that excludes 1 means the two definitions disagree about the association, beyond sampling noise. A test checks that patients, not admissions, are resampled.
|
|
70
|
+
- **Scale check.** Wall time of `compare` for each condition, on the demo and (once credentialed) on full MIMIC-IV, on one laptop.
|
|
71
|
+
- **Extension: LLM-drafted definitions** *(planned, branch `cohort-diff-llm`)*. An LLM receives a paper's methods text and the table and column names only (never patient data) and returns a definition through JSON-schema structured output. Each draft is graded by the same oracle and property tests, and by exact agreement with the hand-written definition on synthetic data. Pass rates for 3+ models on a 20-item labeled set, with 95% intervals; every failure is categorized.
|
|
72
|
+
- **Engineering.** Strict typing, lint and tests on Python 3.11–3.13 in CI; the wheel is built in CI; releases go to PyPI by Trusted Publishing from version tags.
|
|
73
|
+
|
|
74
|
+
## Results
|
|
75
|
+
|
|
76
|
+
### Synthetic data (2,000 patients, 2,738 admissions, seed 0)
|
|
77
|
+
|
|
78
|
+
| Definition | n | Prevalence | Mortality among members | Mortality OR [95% CI] |
|
|
79
|
+
| --- | --- | --- | --- | --- |
|
|
80
|
+
| aki_icd | 400 | 14.6% | 16.8% | 3.06 [2.24, 4.19] |
|
|
81
|
+
| aki_kdigo_48h | 2,139 | 78.2% | 9.2% | 4.23 [2.44, 7.33] |
|
|
82
|
+
| aki_kdigo_7d | 2,033 | 74.3% | 8.4% | 1.48 [1.04, 2.10] |
|
|
83
|
+
| dm_icd | 438 | 16.0% | 15.8% | 2.84 [2.09, 3.87] |
|
|
84
|
+
| dm_hba1c | 374 | 13.7% | 10.4% | 1.48 [1.03, 2.14] |
|
|
85
|
+
| dm_meds | 355 | 13.0% | 16.1% | 2.77 [2.00, 3.84] |
|
|
86
|
+
| readmit30_any | 296 | 10.8% | 0.0% | not meaningful (no deaths by construction) |
|
|
87
|
+
| readmit30_excl | 242 | 8.8% | 0.0% | not meaningful (no deaths by construction) |
|
|
88
|
+
|
|
89
|
+
Pairwise overlap and disagreement in the mortality association (patient-level bootstrap, 1,000 resamples, 95% intervals):
|
|
90
|
+
|
|
91
|
+
| Pair | Jaccard [95% CI] | Ratio of mortality ORs [95% CI] |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| aki_icd vs aki_kdigo_48h | 0.160 [0.145, 0.176] | 0.72 [0.33, 1.26] |
|
|
94
|
+
| aki_icd vs aki_kdigo_7d | 0.142 [0.127, 0.157] | **2.08 [1.29, 3.37]** |
|
|
95
|
+
| aki_kdigo_48h vs aki_kdigo_7d | 0.883 [0.869, 0.896] | **2.87 [1.87, 5.15]** |
|
|
96
|
+
| dm_hba1c vs dm_icd | 0.099 [0.078, 0.121] | **0.52 [0.31, 0.83]** |
|
|
97
|
+
| dm_hba1c vs dm_meds | 0.093 [0.072, 0.116] | **0.54 [0.32, 0.86]** |
|
|
98
|
+
| dm_icd vs dm_meds | 0.811 [0.776, 0.847] | 1.03 [0.88, 1.21] |
|
|
99
|
+
| readmit30_any vs readmit30_excl | 0.818 [0.771, 0.860] | not meaningful (no deaths in either cohort) |
|
|
100
|
+
|
|
101
|
+
Bold: the interval excludes 1, so the two definitions disagree about the mortality association beyond sampling noise.
|
|
102
|
+
|
|
103
|
+
- **Code vs lab definitions barely overlap** (Jaccard 0.09–0.16), while two lab rules (KDIGO 48 h vs 7 d) or two code-like rules (ICD vs medications) overlap 81–88%.
|
|
104
|
+
- **High overlap does not mean the same answer.** The two KDIGO definitions share 88% of admissions, yet their mortality ORs differ about threefold (ratio 2.87 [1.87, 5.15]).
|
|
105
|
+
- **Different-looking ORs can be compatible.** `aki_icd` (3.06) and `aki_kdigo_48h` (4.23) look different, but the interval for their ratio includes 1.
|
|
106
|
+
- **KDIGO-48h flags 78% of synthetic admissions**: the generator draws creatinine about every 12 h with noise (SD 0.3 mg/dL), and a 0.3 mg/dL rule fires on noise when labs are frequent. Whether real data shows the same is what the demo and full MIMIC-IV can answer.
|
|
107
|
+
- **Readmission cohorts contain no deaths by construction** (an admission followed by another one cannot end in death), so their mortality ORs come only from the 0.5 zero-cell correction. The definition limits which questions the cohort can answer.
|
|
108
|
+
|
|
109
|
+
### MIMIC-IV demo (real data: 100 patients, 275 admissions)
|
|
110
|
+
|
|
111
|
+
| Definition | n | Prevalence | Mortality among members | Mortality OR [95% CI] |
|
|
112
|
+
| --- | --- | --- | --- | --- |
|
|
113
|
+
| aki_icd | 70 | 25.5% | 15.7% | 9.37 [2.88, 30.51] |
|
|
114
|
+
| aki_kdigo_48h | 69 | 25.1% | 15.9% | 9.58 [2.94, 31.20] |
|
|
115
|
+
| aki_kdigo_7d | 53 | 19.3% | 20.8% | 14.27 [4.34, 46.97] |
|
|
116
|
+
| dm_icd | 111 | 40.4% | 4.5% | 0.73 [0.24, 2.19] |
|
|
117
|
+
| dm_hba1c | 17 | 6.2% | 0.0% | 0.45 [0.03, 7.82] (no deaths; zero-cell correction) |
|
|
118
|
+
| dm_meds | 135 | 49.1% | 5.9% | 1.20 [0.42, 3.40] |
|
|
119
|
+
| readmit30_any | 53 | 19.3% | 0.0% | not meaningful (no deaths by construction) |
|
|
120
|
+
| readmit30_excl | 49 | 17.8% | 0.0% | not meaningful (no deaths by construction) |
|
|
121
|
+
|
|
122
|
+
| Pair | Jaccard [95% CI] | Ratio of mortality ORs [95% CI] |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| aki_icd vs aki_kdigo_48h | 0.376 [0.263, 0.494] | 0.98 [0.25, 3.23] |
|
|
125
|
+
| aki_icd vs aki_kdigo_7d | 0.309 [0.189, 0.440] | 0.66 [0.17, 2.48] |
|
|
126
|
+
| aki_kdigo_48h vs aki_kdigo_7d | 0.564 [0.447, 0.679] | 0.67 [0.20, 2.35] |
|
|
127
|
+
| dm_hba1c vs dm_icd | 0.113 [0.071, 0.171] | 0.62 [0.17, 5.56] (zero-cell) |
|
|
128
|
+
| dm_hba1c vs dm_meds | 0.126 [0.079, 0.176] | 0.38 [0.09, 1.91] (zero-cell) |
|
|
129
|
+
| dm_icd vs dm_meds | 0.608 [0.461, 0.715] | 0.61 [0.15, 1.51] |
|
|
130
|
+
| readmit30_any vs readmit30_excl | 0.925 [0.852, 1.000] | not meaningful |
|
|
131
|
+
|
|
132
|
+
- **Real definitions also select different patients.** The AKI definitions have similar prevalence (19–25%) but share only 31–56% of their combined admissions. Diabetes by HbA1c ≥ 6.5% (6% of admissions) overlaps the code and medication definitions at Jaccard 0.11–0.13.
|
|
133
|
+
- **The medication phenotype captures the most** (49% of admissions vs 40% by codes), consistent with insulin given for stress hyperglycemia; every HbA1c-defined admission is also in the medication cohort.
|
|
134
|
+
- **KDIGO-48h flags 25% of demo admissions, not the 78% seen on synthetic data**, so the synthetic over-capture came from the generator's frequent, noisy creatinine, not from the rule itself.
|
|
135
|
+
- **No disagreement in the mortality association can be shown at this size.** The AKI ORs range from 9.4 to 14.3 and the diabetes ORs from 0.45 to 1.20, but every ratio interval includes 1 and some are 20-fold wide.
|
|
136
|
+
|
|
137
|
+
With 100 patients, cohorts are small and intervals wide; these numbers show the tool runs on real data, not how much definitions disagree in practice.
|
|
138
|
+
|
|
139
|
+
### Scale check
|
|
140
|
+
|
|
141
|
+
Full MIMIC-IV (about 546,000 admissions) will be timed once credentialed.
|
|
142
|
+
|
|
143
|
+
| Data | Condition | Admissions | Wall time (s) | Machine |
|
|
144
|
+
| --- | --- | --- | --- | --- |
|
|
145
|
+
| MIMIC-IV demo | aki / diabetes / readmission | 275 | 1.6 / 1.5 / 1.3 (includes 1,000 bootstrap resamples and Python start-up) | Intel i7-12800HX, 64 GB RAM, Windows |
|
|
146
|
+
|
|
147
|
+
### Extension: LLM-drafted definitions (planned)
|
|
148
|
+
|
|
149
|
+
Not started. Planned on branch `cohort-diff-llm` after v0.1.0: pass rates of LLM-drafted definitions on the oracle tests for 3+ models on a 20-item labeled set, with 95% intervals and every failure categorized.
|
|
150
|
+
|
|
151
|
+
### Correctness
|
|
152
|
+
|
|
153
|
+
27 tests pass with 99% line coverage on Python 3.11–3.13 (CI). Two deliberate breaks show what each kind of test catches:
|
|
154
|
+
|
|
155
|
+
| Deliberate break | Caught by | Not caught by |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| Remove the rounding from the KDIGO 0.3 mg/dL comparison in both the SQL and the oracle | The hand-picked boundary test (0.9 → 1.2 and 1.1 → 1.4 missed) | Every SQL-vs-oracle test, because both shared the bug |
|
|
158
|
+
| KDIGO window 48 h → 24 h | SQL vs oracle on synthetic data, and the Hypothesis property test (shrunk to creatinine 1.0 then 2.0 mg/dL, 25 hours apart) | (nothing missed) |
|
|
159
|
+
|
|
160
|
+
## Discussion
|
|
161
|
+
|
|
162
|
+
On synthetic data, where the structure is known, definitions of the same condition overlap little when they use different sources (codes vs labs, Jaccard 0.09–0.16; H1), and four of six pairs disagree about the mortality association beyond sampling noise (ratio intervals excluding 1; H3). High overlap does not guarantee agreement: the two KDIGO rules share 88% of admissions but their ORs differ about threefold.
|
|
163
|
+
|
|
164
|
+
The open demo shows the same pattern in membership on real data: AKI definitions with almost identical prevalence share at most 56% of their admissions, and diabetes by HbA1c overlaps the other definitions at about 0.12 (H1). The medication phenotype captures the most admissions, as expected (H2). For AKI, H2 was not supported on the demo: the lab rule flagged the same share as the codes (25%), and the synthetic data's 78% came from the generator. Whether the mortality association itself changes with the definition (H3) cannot be tested with 100 patients: every interval for the ratio of ORs includes 1. Readmission definitions exclude deaths by construction, so they cannot answer a mortality question at all (H4).
|
|
165
|
+
|
|
166
|
+
**Strengths.** Definitions are versioned, cited SQL rather than prose; each is checked against an independent implementation and by property tests; known-structure validation on synthetic data; installable with `pip`; reproducible by anyone from open data, a lock file and CI on Python 3.11–3.13.
|
|
167
|
+
|
|
168
|
+
## Limitations
|
|
169
|
+
|
|
170
|
+
- **No real-world estimate of disagreement.** The demo has 100 patients; its cohorts and intervals are too small to say how much definitions disagree in practice.
|
|
171
|
+
- **Synthetic structure is planted.** Overlaps on synthetic data reflect the generator's assumptions, not real coding or lab practice.
|
|
172
|
+
- **KDIGO is creatinine-only** (no urine output) with an in-admission baseline; many studies use pre-admission baselines.
|
|
173
|
+
- **Readmission definitions are simplified**; the CMS planned-readmission algorithm is far more detailed.
|
|
174
|
+
- **The odds ratio is unadjusted.** It shows how a reported association can move with the definition, not a causal effect.
|
|
175
|
+
|
|
176
|
+
## Trial and error
|
|
177
|
+
|
|
178
|
+
What went wrong while building this, and what changed as a result. Analysis problems come first because they affect results; engineering problems follow because they affect reproducibility.
|
|
179
|
+
|
|
180
|
+
### Analysis and data
|
|
181
|
+
|
|
182
|
+
| Problem | Why it matters | How it was handled |
|
|
183
|
+
| --- | --- | --- |
|
|
184
|
+
| A creatinine rise of exactly 0.3 mg/dL can be missed: in floating point, 1.2 − 0.9 = 0.29999999999999993 | KDIGO's 0.3 mg/dL threshold is clinically exact; a naive `>= 0.3` silently drops true AKI cases | The SQL and the oracle round the difference to 6 decimals. Removing the rounding from *both* left every SQL-vs-oracle test green (shared bug); only the hand-picked boundary test failed, on 0.9 → 1.2 and 1.1 → 1.4 (0.7 → 1.0 happens to round correctly) |
|
|
185
|
+
| KDIGO-48h flagged 78% of synthetic admissions but 25% of demo admissions | A planted generator artifact (creatinine every 12 h with SD 0.3 mg/dL noise) looked like a property of the rule | Reported both; the synthetic over-capture is attributed to the generator, and synthetic overlaps are used only to validate the tool |
|
|
186
|
+
| `dm_hba1c` has no deaths on the demo (17 admissions) | Its odds ratio and every ratio involving it come only from the 0.5 zero-cell correction | Flagged as zero-cell in the Results tables and not interpreted |
|
|
187
|
+
| A wrong time window (24 h instead of KDIGO's 48 h) still produces a plausible-looking cohort | Window edges are where cohort SQL is most often wrong, and a smaller cohort doesn't look broken | Deliberately changing the window to 24 h failed two tests: SQL vs oracle on synthetic data (admissions missing from the SQL cohort), and the Hypothesis property test, which shrank the failure to the minimal case: creatinine 1.0 then 2.0 mg/dL, 25 hours apart |
|
|
188
|
+
|
|
189
|
+
### Engineering
|
|
190
|
+
|
|
191
|
+
| Part | What went wrong | Cause | Fix and lesson |
|
|
192
|
+
| --- | --- | --- | --- |
|
|
193
|
+
| Setup | The parent lab repository saw the whole Level 1 folder (both projects and the MIMIC-IV demo data) as untracked files | The parent's `.gitignore` had no rule for that folder, although earlier notes said it did | Added the folder to the parent's `.gitignore` before any commit; nothing had been committed. Lesson: verify ignore rules with `git check-ignore -v` and `git status`, not from notes |
|
|
194
|
+
| CLI | The Part 5 commit had 2 files instead of 3: the CI workflow still lacked the release job | One of three files to replace was skipped; the commit itself succeeded, so nothing looked wrong | Caught by `git show --stat HEAD` before pushing; replaced the workflow and folded it in with `git commit --amend`. Lesson: compare the commit's file list with the list you expected before `git push` |
|
|
195
|
+
|
|
196
|
+
## Conclusions
|
|
197
|
+
|
|
198
|
+
"The same" cohort is often not the same patients: on real MIMIC-IV demo data, AKI definitions agree on at most 56% of their combined admissions and diabetes definitions on as little as 11%. `cohort-diff` makes that difference measurable, with each definition as tested, cited SQL and paired, patient-level intervals for overlap and for the downstream association. Whether those differences change published associations on real data is the question for full MIMIC-IV, where the same code runs unchanged.
|
|
199
|
+
|
|
200
|
+
## Repository layout
|
|
201
|
+
|
|
202
|
+
| Path | What it does |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `src/cohort_diff/definitions/` | One SQL file per cohort definition, with a metadata header |
|
|
205
|
+
| `src/cohort_diff/registry.py` | Finds and parses definitions (built-in or a path) |
|
|
206
|
+
| `src/cohort_diff/engine.py` | DuckDB views over MIMIC tables (folder or DataFrames) |
|
|
207
|
+
| `src/cohort_diff/compare.py` | Jaccard, overlap table, cohort description, mortality odds ratio, patient-level bootstrap, markdown report |
|
|
208
|
+
| `src/cohort_diff/synth.py` | MIMIC-shaped synthetic data |
|
|
209
|
+
| `src/cohort_diff/cli.py` | `list`, `run`, `synth`, `compare` |
|
|
210
|
+
| `tests/` | Unit, oracle and property-based (Hypothesis) tests |
|
|
211
|
+
| `CLAUDE.md` | Project rules for AI coding agents (commands, testing, data safety) |
|
|
212
|
+
| `docs/AI_LOG.md` | What was delegated to AI tools, what they got wrong, and how it was caught |
|
|
213
|
+
|
|
214
|
+
## Reproduce
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
uv sync
|
|
218
|
+
uv run cohort-diff synth --out data/synth --patients 2000 --seed 0
|
|
219
|
+
uv run cohort-diff list
|
|
220
|
+
uv run cohort-diff compare --condition aki --data data/synth --out reports/aki.md
|
|
221
|
+
uv run cohort-diff compare --condition aki --data path/to/mimic-iv-clinical-database-demo-2.2
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Development: `uv run ruff format --check src tests`, `uv run ruff check src tests`, `uv run mypy src` (strict), `uv run pytest`, `uv build`.
|
|
225
|
+
|
|
226
|
+
## Citations
|
|
227
|
+
|
|
228
|
+
- Johnson, A., Bulgarelli, L., Pollard, T., Horng, S., Celi, L. A., & Mark, R. (2023). MIMIC-IV Clinical Database Demo (version 2.2). PhysioNet. RRID:SCR_007345. https://doi.org/10.13026/dp1f-ex47
|
|
229
|
+
- Johnson, A. E. W., Bulgarelli, L., Shen, L., et al. (2023). MIMIC-IV, a freely accessible electronic health record dataset. *Scientific Data*, 10, 1. https://doi.org/10.1038/s41597-022-01899-x
|
|
230
|
+
- Pollard, T., Moody, B. E., Lehman, L., Gow, B., Fernandes, C., Xie, C., Johnson, A., Mark, R. G., & Heldt, T. (2026). PhysioNet as a global platform for biomedical research. *Nature Health*. https://doi.org/10.1038/s44360-026-00096-z
|
|
231
|
+
- Kidney Disease: Improving Global Outcomes (KDIGO) Acute Kidney Injury Work Group (2012). KDIGO Clinical Practice Guideline for Acute Kidney Injury. *Kidney International Supplements*, 2(1), 1–138.
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
Code: MIT. The MIMIC-IV demo data is distributed by PhysioNet under the Open Data Commons Open Database License v1.0 and is not included in this repository.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
cohort_diff/__init__.py,sha256=8Yl71OOa_VT0iHnDTQArsiPzQ54CQrzLmKAlE5PDl6I,450
|
|
2
|
+
cohort_diff/cli.py,sha256=--abpuaLO7WzSr2LpFk0txQx6x_LnaVVMu9kixuYKW8,3789
|
|
3
|
+
cohort_diff/compare.py,sha256=DlQov-ybxoTgPFr8TZvMkotZcIEi_vpJO5kwJpsg-Ys,7074
|
|
4
|
+
cohort_diff/definitions/aki_icd.sql,sha256=CWR_rDPlBpBtSAqT4M7-o1R2FeUa1vS8wfTy74ZyGEI,351
|
|
5
|
+
cohort_diff/definitions/aki_kdigo_48h.sql,sha256=fxKiEPrK1r8onBLYTQRam4XXnLPzIc0p78XGPYkRbBY,871
|
|
6
|
+
cohort_diff/definitions/aki_kdigo_7d.sql,sha256=i6D1HUE0udUeWgC6rzPmJ9-gfmyD-wI9ooxR9tTQXWU,890
|
|
7
|
+
cohort_diff/definitions/dm_hba1c.sql,sha256=JhQiPuQrmr_cAyt5Gqjbi2HBRfaVeIkhET5y3BNWzfk,394
|
|
8
|
+
cohort_diff/definitions/dm_icd.sql,sha256=zqa-SKdzQwcnoGN689yFIF-K6H1JezHzW6XlZ3DUS24,343
|
|
9
|
+
cohort_diff/definitions/dm_meds.sql,sha256=hrfzDDH_zMYqsncmqkJhY9kMVDLcROaYShDZw7huy_c,545
|
|
10
|
+
cohort_diff/definitions/readmit30_any.sql,sha256=7sPIGDZAXPBnyaxUBO2l9L6kqcuusWXFS-P_qATFFbw,487
|
|
11
|
+
cohort_diff/definitions/readmit30_excl.sql,sha256=L-QrwG4NcmeAR3C4V8fLl-Jfc1MZqyi45-SXICzND5o,883
|
|
12
|
+
cohort_diff/engine.py,sha256=niEgEN9LskyowVuzvyLm7tLl3sckvkbrpYatDwRp0VM,2854
|
|
13
|
+
cohort_diff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
cohort_diff/registry.py,sha256=XnNeJxFj8fJnryVScxQncijieM7KKfDiRzcarOZg0cs,1920
|
|
15
|
+
cohort_diff/synth.py,sha256=lEaU-zgpNiCvPYxuk3s_J1wm7kUx9iE7QEro12Gc8OI,7843
|
|
16
|
+
cohort_diff-0.1.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
|
|
17
|
+
cohort_diff-0.1.0.dist-info/entry_points.txt,sha256=sxn9Wc8qX3S86s8GeFcvO9EcEgMufnuSakFWrX5Zu9c,54
|
|
18
|
+
cohort_diff-0.1.0.dist-info/METADATA,sha256=1KC8v-z8-yJl7yI8v2AMy2_C5f6khpnTGzhP1_pLJvs,20781
|
|
19
|
+
cohort_diff-0.1.0.dist-info/RECORD,,
|