Download code_release/code/construction/pipeline/preprocess.py from clinical-agent-memory-bench/meme-bench: direct link, hf CLI and curl.
- Browser
- Download file 9.98 kB
-
https://huggingface.co/spaces/clinical-agent-memory-bench/meme-bench/resolve/main/code_release/code/construction/pipeline/preprocess.py
- Command line
-
hf download hf://spaces/clinical-agent-memory-bench/meme-bench/code_release/code/construction/pipeline/preprocess.py
-
curl -L -o preprocess.py https://huggingface.co/spaces/clinical-agent-memory-bench/meme-bench/resolve/main/code_release/code/construction/pipeline/preprocess.py
9.98 kB
| #!/usr/bin/env python3 | |
| """Stage A, preprocessing: one patient's synthetic timeline -> per-visit records (deterministic). | |
| export visit table -> ALL_visits.txt (term glossary applied) | |
| parse ALL_visits.txt -> visits.jsonl | |
| split visits.jsonl -> visits/v*.json + visit_manifest.json (the extraction manifest) | |
| vitals visit table -> vitals.json + vitals_timeline.json | |
| facts patient.env: the per-patient values the prompts are filled with | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import re | |
| import shlex | |
| import sys | |
| import unicodedata | |
| import pandas as pd | |
| from pipeline import common | |
| from pipeline.common import BASE, VISIT_TABLE, patient_id | |
| PATIENT = patient_id() | |
| PID, DEPT, ISO, VT = "record_id", "department", "visit_date", "visit_type" | |
| S, O, A, P = "subjective", "objective", "assessment", "plan" | |
| ICDS = ["icd_1", "icd_2", "icd_3", "icd_4", "icd_5", "icd_6"] | |
| def caseless_letters(text): | |
| """The number of letters without case (Unicode category Lo) in `text`.""" | |
| return sum(1 for ch in text if unicodedata.category(ch) == "Lo") | |
| def norm(x): | |
| """Collapse all whitespace (incl. embedded newlines) to single spaces; strip.""" | |
| return " ".join(str(x).split()) | |
| def build_raw_text(df, patient): | |
| sub = df[df[PID] == patient].sort_values([ISO, "visit_no"]) | |
| lines = [] | |
| for _, r in sub.iterrows(): | |
| icds = [str(r[c]).strip() for c in ICDS if str(r[c]).strip()] | |
| header = f"### {r[ISO]} | {norm(r[DEPT])} | {VT}:{norm(r[VT])} | ICD:[{','.join(icds)}]" | |
| lines.append(header) | |
| lines.append(f"S: {norm(r[S])}") | |
| lines.append(f"O: {norm(r[O])}") | |
| lines.append(f"A: {norm(r[A])}") | |
| lines.append(f"P: {norm(r[P])}") | |
| return "\n".join(lines) + "\n", len(sub) | |
| def export_visits(patient: int, out_path: str) -> None: | |
| """Visit table rows of one patient -> all-visits file. | |
| One block per visit (header line plus S/O/A/P); the term glossary turns every term into | |
| "normalized form (term)". | |
| """ | |
| df = pd.read_parquet(VISIT_TABLE) | |
| for c in [S, O, A, P, DEPT, VT] + ICDS: | |
| df[c] = df[c].fillna("") | |
| raw, n = build_raw_text(df, patient) | |
| g = common.load_glossary() | |
| pat = common.glossary_re(g) | |
| out = common.apply_glossary(raw, g, pat) | |
| left = caseless_letters(re.sub(r"\([^()]*\)", "", out)) | |
| open(out_path, "w", encoding="utf-8").write(out) | |
| print(f"patient={patient} visits={n} lines={out.count(chr(10))} caseless_letters_outside_parens={left}") | |
| print(f"wrote {out_path}") | |
| HEADER_RE = re.compile( | |
| r"^###\s+(\d{4}-\d{2}-\d{2})\s+\|\s+(.+?)\s+\|\s+visit_type:([^|]*?)\s+\|\s+ICD:\[(.*?)\]\s*$" | |
| ) | |
| SOAP_RE = re.compile(r"^([SOAP]):\s?(.*)$") | |
| def dept_key(dept): | |
| base = re.sub(r"\s*\(.*?\)\s*", "", dept).strip().lower() | |
| return re.sub(r"[^a-z0-9]+", "_", base).strip("_") | |
| def parse(path): | |
| visits = [] | |
| cur = None | |
| field = None | |
| for line in open(path, encoding="utf-8"): | |
| line = line.rstrip("\n") | |
| m = HEADER_RE.match(line) | |
| if m: | |
| if cur: | |
| visits.append(cur) | |
| date, dept, vt, icd = m.groups() | |
| codes = [c.strip() for c in icd.split(",") if c.strip()] | |
| cur = { | |
| "visit_number": len(visits) + 1, | |
| "date": date, | |
| "department": dept, | |
| "department_key": dept_key(dept), | |
| "visit_type": vt, | |
| "icd": codes, | |
| "soap": {"S": "", "O": "", "A": "", "P": ""}, | |
| } | |
| field = None | |
| continue | |
| sm = SOAP_RE.match(line) | |
| if sm and cur is not None: | |
| field = sm.group(1) | |
| cur["soap"][field] = sm.group(2) | |
| elif cur is not None and field: | |
| cur["soap"][field] += " " + line | |
| if cur: | |
| visits.append(cur) | |
| return visits | |
| def parse_visits(inp: str, out: str) -> None: | |
| """ALL_visits.txt -> deterministic per-visit JSONL records.""" | |
| visits = parse(inp) | |
| with open(out, "w", encoding="utf-8") as f: | |
| for v in visits: | |
| f.write(json.dumps(v, ensure_ascii=False) + "\n") | |
| depts = {} | |
| for v in visits: | |
| depts[v["department"]] = depts.get(v["department"], 0) + 1 | |
| print(f"parsed {len(visits)} visits -> {out}") | |
| print("departments:", json.dumps(depts, ensure_ascii=False)) | |
| print("first record:", json.dumps(visits[0], ensure_ascii=False)[:300]) | |
| def split() -> None: | |
| """Split visits.jsonl into one file per visit and write the extraction manifest (absolute paths).""" | |
| BASE = common.BASE.resolve() | |
| src = BASE / "visits.jsonl" | |
| if not src.exists(): | |
| raise SystemExit(f"FATAL: {src} not found; run the parse step first") | |
| rows = [json.loads(l) for l in open(src, encoding="utf-8") if l.strip()] | |
| if not rows: | |
| raise SystemExit(f"FATAL: {src} is empty") | |
| out = BASE / "visits" | |
| out.mkdir(parents=True, exist_ok=True) | |
| vn = [r["visit_number"] for r in rows] | |
| if len(set(vn)) != len(vn): | |
| raise SystemExit(f"FATAL: duplicate visit_number in {src}; numbering is the join key for the " | |
| f"whole pipeline (vitals, chain, questions all index by it)") | |
| manifest = [] | |
| for r in sorted(rows, key=lambda x: x["visit_number"]): | |
| p = out / f"v{r['visit_number']}.json" | |
| p.write_text(json.dumps(r, ensure_ascii=False, indent=1), encoding="utf-8") | |
| manifest.append({"visit_number": r["visit_number"], "path": str(p)}) | |
| (BASE / "visit_manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=1)) | |
| print(f"patient {PATIENT}: {len(manifest)} visits -> {out}/v*.json") | |
| print(f" v{min(vn)}..v{max(vn)} ({min(r['date'] for r in rows)} .. {max(r['date'] for r in rows)})") | |
| print(f"-> {BASE/'visit_manifest.json'}") | |
| COLS = {"weight": "weight_kg", "height": "height_cm", "blood_pressure": "BP", "pulse": "pulse"} | |
| def clean(x): | |
| x = str(x).strip() | |
| return x if x not in ("", "-", "nan", "None", "NaN") else None | |
| def vitals() -> None: | |
| """Structured vitals (height, weight, blood pressure, pulse) from the visit table. | |
| Visits are numbered by the same (date, visit_no) sort as visits.jsonl, and the row count is checked | |
| against visits.jsonl. Writes vitals.json and vitals_timeline.json. | |
| """ | |
| df = pd.read_parquet(VISIT_TABLE) | |
| sub = (df[df["record_id"] == PATIENT] | |
| .sort_values(["visit_date", "visit_no"]) | |
| .reset_index(drop=True)) | |
| if sub.empty: | |
| sys.exit(f"FATAL: patient {PATIENT} not in {VISIT_TABLE}") | |
| sub["vn"] = range(1, len(sub) + 1) | |
| vpath = BASE / "visits.jsonl" | |
| if vpath.exists(): | |
| visits = {json.loads(l)["visit_number"]: json.loads(l) for l in open(vpath)} | |
| if len(visits) != len(sub): | |
| sys.exit(f"FATAL: visit-count mismatch; the visit table has {len(sub)} rows, " | |
| f"{vpath} has {len(visits)}. Numbering would drift; refusing to write.") | |
| bad = [int(r["vn"]) for _, r in sub.iterrows() | |
| if visits[int(r["vn"])]["date"] != str(r["visit_date"])[:10]] | |
| if bad: | |
| sys.exit(f"FATAL: date misalignment on {len(bad)} visits (first: {bad[:5]}). " | |
| f"visit-table sort != visits.jsonl sort; refusing to write.") | |
| else: | |
| print(f" note: {vpath} absent; skipping the alignment check") | |
| vit = {} | |
| for _, r in sub.iterrows(): | |
| v = {name: clean(r[col]) for col, name in COLS.items() if clean(r[col])} | |
| if v: | |
| vit[int(r["vn"])] = v | |
| (BASE / "vitals.json").write_text( | |
| json.dumps({str(k): v for k, v in sorted(vit.items())}, ensure_ascii=False, indent=1)) | |
| timelines: dict = {} | |
| for vn in sorted(vit): | |
| date = str(sub.loc[sub["vn"] == vn, "visit_date"].iloc[0])[:10] | |
| for measure, val in vit[vn].items(): | |
| timelines.setdefault(measure, []).append( | |
| {"visit_number": vn, "date": date, "value": val}) | |
| (BASE / "vitals_timeline.json").write_text(json.dumps(timelines, ensure_ascii=False, indent=1)) | |
| print(f"patient {PATIENT}: {len(sub)} visits, {len(vit)} with >=1 structured vital") | |
| for m, series in timelines.items(): | |
| vals = [s["value"] for s in series] | |
| print(f" {m:<10} {len(series):>3} points {series[0]['date']}..{series[-1]['date']} " | |
| f"{vals[:6]}{'...' if len(vals) > 6 else ''}") | |
| if not timelines: | |
| print(" no structured vitals") | |
| print(f"-> {BASE/'vitals.json'}\n-> {BASE/'vitals_timeline.json'}") | |
| def facts(pdesc: str) -> None: | |
| """Write patient.env: visit count, first and last visit date, and the clinical descriptor.""" | |
| W = BASE | |
| v = [json.loads(l) for l in open(f"{W}/visits.jsonl")] | |
| env = {"MEMEBENCH_WORK": W, "MEMEBENCH_PATIENT": PATIENT, "MEMEBENCH_PDESC": pdesc or common.DEFAULT_PDESC, | |
| "MEMEBENCH_NVISITS": len(v), "MEMEBENCH_PFIRST": min(x["date"] for x in v), | |
| "MEMEBENCH_PLAST": max(x["date"] for x in v)} | |
| open(f"{W}/patient.env", "w").write("".join(f"export {k}={shlex.quote(str(x))}\n" for k, x in env.items())) | |
| print(f" patient.env: {len(v)} visits") | |
| def main(): | |
| ap = argparse.ArgumentParser(description="Stage A: preprocess one patient's synthetic timeline.") | |
| sp = ap.add_subparsers(dest="cmd", required=True) | |
| sp.add_parser("export").add_argument("--out", required=True) | |
| p = sp.add_parser("parse") | |
| p.add_argument("--inp", default="ALL_visits.txt") | |
| p.add_argument("--out", default="visits.jsonl") | |
| sp.add_parser("split") | |
| sp.add_parser("vitals") | |
| sp.add_parser("facts").add_argument("--pdesc", default="") | |
| a = ap.parse_args() | |
| if a.cmd == "export": | |
| export_visits(PATIENT, a.out) | |
| elif a.cmd == "parse": | |
| parse_visits(a.inp, a.out) | |
| elif a.cmd == "split": | |
| split() | |
| elif a.cmd == "vitals": | |
| vitals() | |
| else: | |
| facts(a.pdesc) | |
| if __name__ == "__main__": | |
| main() | |