| """Synthetic patient generation.""" |
|
|
| from __future__ import annotations |
|
|
| import random |
|
|
| from app.common.enums import Difficulty, DoseBucket |
| from app.common.types import LabSummary, Medication, PatientProfile |
|
|
| _DRUG_POOL = [ |
| ("warfarin_like", "anticoagulant"), |
| ("benzodiazepine_like", "sedative"), |
| ("metformin_like", "glucose_lowering"), |
| ("statin_like", "lipid_lowering"), |
| ("ace_inhibitor_like", "antihypertensive"), |
| ("nsaid_like", "analgesic"), |
| ("opioid_like", "analgesic"), |
| ("ssri_like", "antidepressant"), |
| ("ppi_like", "gastro"), |
| ("beta_blocker_like", "antihypertensive"), |
| ] |
|
|
|
|
| def generate_patient_profile(seed: int, difficulty: Difficulty, patient_id: str | None = None) -> PatientProfile: |
| random.seed(seed) |
| med_count = {Difficulty.EASY: 5, Difficulty.MEDIUM: 8, Difficulty.HARD: 10}[difficulty] |
| selected = random.sample(_DRUG_POOL, k=med_count) |
| medications = [ |
| Medication( |
| drug=drug, |
| class_name=cls, |
| dose_bucket=random.choice([DoseBucket.LOW, DoseBucket.MEDIUM, DoseBucket.HIGH]), |
| indication=f"indication_{idx}", |
| requires_taper=drug in {"benzodiazepine_like", "opioid_like"}, |
| ) |
| for idx, (drug, cls) in enumerate(selected) |
| ] |
| return PatientProfile( |
| patient_id=patient_id or f"patient_{seed}", |
| age=random.randint(55, 90), |
| sex=random.choice(["F", "M"]), |
| comorbidities=random.sample( |
| ["htn", "dm2", "afib", "ckd", "copd", "depression", "fall_risk"], k=3 |
| ), |
| medications=medications, |
| labs=LabSummary( |
| egfr=round(random.uniform(20, 95), 1), |
| ast=round(random.uniform(10, 120), 1), |
| alt=round(random.uniform(10, 120), 1), |
| inr=round(random.uniform(1.0, 4.0), 2), |
| glucose=round(random.uniform(70, 280), 1), |
| ), |
| vitals={ |
| "sbp": random.randint(100, 180), |
| "dbp": random.randint(60, 105), |
| "hr": random.randint(50, 120), |
| "egfr_trend": round(random.uniform(-8.0, 3.0), 2), |
| "inr_trend": round(random.uniform(-0.5, 0.7), 2), |
| "glucose_trend": round(random.uniform(-35.0, 45.0), 2), |
| }, |
| specialist_conflicts=[ |
| "duplicate_analgesic_strategy", |
| "cardio_vs_pain_med_conflict", |
| ] |
| if difficulty != Difficulty.EASY |
| else [], |
| prior_ade_history=["fall_event", "sedation_event"] if difficulty == Difficulty.HARD else [], |
| frailty_score=round(random.uniform(0.1, 0.9), 2), |
| adherence_estimate=round(random.uniform(0.4, 0.95), 2), |
| latent_confounders={ |
| "metabolism_variability": round(random.uniform(0.1, 0.9), 3), |
| "social_support_risk": round(random.uniform(0.0, 1.0), 3), |
| "polyprovider_fragmentation": round(random.uniform(0.1, 0.95), 3), |
| }, |
| monitoring_gaps=["no_recent_inr", "missing_liver_panel"] if difficulty == Difficulty.HARD else ["missing_followup_bp"], |
| ) |
|
|