Document Version: 1.0 Date: February 2026 Purpose: Document the PatientHistoryAnalyzer module for dynamic risk modification
The PatientHistoryAnalyzer is a sophisticated module that leverages the full power of individual-level microsimulation by analyzing complete patient trajectories to dynamically modify risk. This capability is the key differentiator between microsimulation and Markov cohort models.
- eGFR trajectory classification: Rapid/normal/slow decliner phenotypes
- BP control quality assessment: Treatment response grading
- Event clustering detection: Identifies unstable patients
- Comorbidity burden scoring: Charlson-based with extensions
- Time-decay risk functions: Prior events modify future risk with temporal decay
┌─────────────────────────────────────────────────────────────────────────────┐
│ PATIENT HISTORY ANALYZER │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Event History │ │ Biomarker │ │ Treatment │ │
│ │ Stream │ │ Trajectories │ │ Response │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │
│ │ • MI events │ │ • eGFR series │ │ • SBP series │ │
│ │ • Strokes │ │ • uACR series │ │ • Adherence │ │
│ │ • HF episodes │ │ • SBP series │ │ pattern │ │
│ │ • AF events │ │ │ │ │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ PatientHistoryAnalyzer │ │
│ ├─────────────────────────┤ │
│ │ • classify_egfr_trajectory() │
│ │ • classify_bp_control() │
│ │ • assess_comorbidity_burden() │
│ │ • get_cvd_risk_modifier() │
│ │ • get_renal_progression_modifier() │
│ │ • get_mortality_risk_modifier() │
│ │ • get_adherence_probability_modifier() │
│ └─────────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Dynamic Risk │ │
│ │ Modifiers │ │
│ ├─────────────────────────┤ │
│ │ CVD: 0.5 - 5.0× │ │
│ │ Renal: 0.6 - 2.0× │ │
│ │ Mortality: 1.0 - 4.0× │ │
│ │ Adherence: 0.3 - 1.0× │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Code Reference: src/history_analyzer.py:42-55
| Class | Purpose | Location |
|---|---|---|
PatientHistoryAnalyzer |
Main analysis engine | history_analyzer.py:42 |
TrajectoryType |
eGFR decline classification | history_analyzer.py:15 |
TreatmentResponse |
BP control grading | history_analyzer.py:24 |
ComorbidityBurden |
Structured burden assessment | history_analyzer.py:32 |
The model classifies patients by their rate of eGFR decline to identify rapid progressors:
| Trajectory Type | Annual Decline Rate | Risk Modifier | Clinical Profile |
|---|---|---|---|
| RAPID_DECLINER | >3 mL/min/year | 1.5× | High ESRD risk, aggressive intervention needed |
| NORMAL_DECLINER | 1-3 mL/min/year | 1.0× | Expected CKD progression |
| SLOW_DECLINER | 0.5-1 mL/min/year | 0.8× | Favorable prognosis |
| STABLE | <0.5 mL/min/year | 0.6× | Preserved renal function |
| INSUFFICIENT_DATA | N/A | 1.0× | <12 months of eGFR data |
Algorithm:
def classify_egfr_trajectory(self) -> TrajectoryType:
"""Classify rate of eGFR decline over past 12-24 months."""
egfr_events = [e for e in self.history if 'egfr' in e]
if len(egfr_events) < 12:
return TrajectoryType.INSUFFICIENT_DATA
# Use recent 24 months or all available
lookback = min(24, len(egfr_events))
recent = egfr_events[-lookback:]
# Calculate slope (mL/min/month) via linear regression
times = [e['time'] for e in recent]
egfrs = [e['egfr'] for e in recent]
slope = self._calculate_slope(times, egfrs)
annual_decline = abs(slope) * 12
if annual_decline > 3.0:
return TrajectoryType.RAPID_DECLINER
elif annual_decline > 1.0:
return TrajectoryType.NORMAL_DECLINER
elif annual_decline > 0.5:
return TrajectoryType.SLOW_DECLINER
else:
return TrajectoryType.STABLECode Reference: src/history_analyzer.py:213-243
Treatment response is graded based on recent SBP measurements:
| Response Grade | Average SBP | BP Reduction Effect | CVD Risk Modifier |
|---|---|---|---|
| EXCELLENT | <130 mmHg | Full treatment benefit | 0.85× |
| GOOD | 130-139 mmHg | Standard benefit | 1.0× |
| FAIR | 140-149 mmHg | Partial benefit | 1.2× |
| POOR | ≥150 mmHg | Minimal benefit | 1.5× |
Algorithm:
def classify_bp_control(self) -> TreatmentResponse:
"""Classify BP control quality over past 6 months."""
sbp_events = [e for e in self.history if 'sbp' in e]
if len(sbp_events) < 3:
return TreatmentResponse.FAIR # Default assumption
recent_sbp = [e['sbp'] for e in sbp_events[-6:]]
avg_sbp = sum(recent_sbp) / len(recent_sbp)
if avg_sbp < 130:
return TreatmentResponse.EXCELLENT
elif avg_sbp < 140:
return TreatmentResponse.GOOD
elif avg_sbp < 150:
return TreatmentResponse.FAIR
else:
return TreatmentResponse.POORCode Reference: src/history_analyzer.py:245-267
The CVD risk modifier integrates multiple history-based factors:
CVD_Modifier = Prior_Event_Modifier
× Clustering_Modifier
× Comorbidity_Modifier
× Treatment_Response_Modifier
× Mental_Health_Modifier
× Substance_Use_Modifier
Component Breakdown:
| Component | Range | Trigger | Source |
|---|---|---|---|
| Prior MI | 1.5× first, +0.3× each additional | Any prior MI | GRACE score |
| Prior Stroke | 1.4× first, +0.25× each additional | Any prior stroke | Framingham |
| Event Clustering | 1.8× | ≥3 CVD events in 60 months | Expert opinion |
| COPD | 1.5× | COPD diagnosis | COSYCONET |
| Atrial Fibrillation | 2.0× | AF diagnosis | CHA₂DS₂-VASc |
| PAD | 2.5× | PAD diagnosis | REACH registry |
| Untreated Depression | 1.3× | Depression without treatment | INTERHEART |
| Substance Use | 1.8× | Any SUD | NHANES |
Code Reference: src/history_analyzer.py:67-110
Risk from prior events decays exponentially over time:
| Time Since Event | Decay Factor | Residual Risk (MI) |
|---|---|---|
| 0 months | 1.00 | 1.50× |
| 12 months | 0.55 | 1.27× |
| 24 months | 0.30 | 1.15× |
| 36 months | 0.17 | 1.08× |
| 60 months | 0.05 | 1.02× |
Implementation:
def _prior_cvd_modifier(self) -> float:
"""Calculate modifier based on prior CVD events with time decay."""
modifier = 1.0
# Prior MI
if self.patient.prior_mi_count > 0:
modifier *= (1.5 + (self.patient.prior_mi_count - 1) * 0.3)
# Prior stroke
if self.patient.prior_stroke_count > 0:
modifier *= (1.4 + (self.patient.prior_stroke_count - 1) * 0.25)
# Time decay
if self.patient.time_since_last_cv_event is not None:
decay_factor = math.exp(-0.05 * self.patient.time_since_last_cv_event)
excess_risk = modifier - 1.0
modifier = 1.0 + (excess_risk * decay_factor)
return modifierCode Reference: src/history_analyzer.py:334-355
Modifies base eGFR decline rate based on patient trajectory:
Renal_Modifier = Trajectory_Modifier
× Albuminuria_Progression_Modifier
× Diabetes_CVD_Synergy
× COPD_Modifier
× Adherence_Pattern_Modifier
| Factor | Condition | Modifier | Rationale |
|---|---|---|---|
| Rapid decliner | >3 mL/min/year decline | 1.5× | Established rapid progression |
| Stable | <0.5 mL/min/year decline | 0.6× | Favorable trajectory |
| Albuminuria doubling | uACR doubled from baseline | 1.4× | Kidney damage progression |
| Diabetes + CVD | Both present | 1.3× | Synergistic nephropathy |
| COPD | Any COPD | 1.2× | Hypoxia accelerates CKD |
| Poor adherence | High SBP variance | 1.3× | Inconsistent BP control |
Code Reference: src/history_analyzer.py:112-149
Based on adapted Charlson Comorbidity Index with extensions:
Mortality_Modifier = (1.0 + Charlson × 0.10)
× COPD_Severity_Modifier
× Substance_Use_Modifier
× SMI_Modifier
× Event_Clustering_Modifier
Charlson Score Components:
| Condition | Points | Notes |
|---|---|---|
| Prior MI | 1 | Per event |
| Heart Failure | 1 | Current diagnosis |
| PAD | 1 | Current diagnosis |
| Prior Stroke | 1 | Per event |
| Diabetes (uncomplicated) | 1 | No CKD or CVD |
| Diabetes (complicated) | 2 | With CKD or CVD |
| Moderate CKD (eGFR 30-59) | 1 | Current eGFR |
| Severe CKD (eGFR <30) | 2 | Current eGFR |
| COPD | 1 | Any severity |
| Substance Use Disorder | 2 | High mortality impact |
| Serious Mental Illness | 1 | Schizophrenia, bipolar |
Additional Modifiers:
| Condition | Modifier | Source |
|---|---|---|
| Severe COPD | 2.5× | GOLD guidelines |
| Moderate COPD | 1.8× | GOLD guidelines |
| Mild COPD | 1.4× | GOLD guidelines |
| Substance Use Disorder | 2.0× | NHANES mortality data |
| Serious Mental Illness | 1.6× | PRIME-MD studies |
| ≥2 events in 12 months | 1.5× | Clinical instability |
Code Reference: src/history_analyzer.py:151-184
The assess_comorbidity_burden() method returns a structured dataclass:
@dataclass
class ComorbidityBurden:
charlson_score: int # 0-15 typically
mental_health_burden: str # "none", "mild", "moderate", "severe"
substance_use_severity: str # "none", "mild", "moderate", "severe"
respiratory_burden: str # "none", "mild", "moderate", "severe"
interactive_effects: List[str] # e.g., ["COPD+CVD", "Depression+Diabetes"]| Level | Criteria |
|---|---|
| None | No depression, anxiety, or SMI |
| Mild | Single diagnosis, treated depression |
| Moderate | Untreated depression OR SMI alone |
| Severe | ≥2 mental health conditions |
| Level | Criteria |
|---|---|
| None | No SUD |
| Mild | Alcohol use disorder |
| Moderate | Opioid or stimulant use disorder |
| Severe | Polysubstance use disorder |
The model tracks clinically important comorbidity interactions:
| Interaction | Effect | Clinical Rationale |
|---|---|---|
| COPD + CVD | Additive mortality | Shared inflammatory pathway |
| Depression + Diabetes | Worse glycemic control, higher CVD | Bidirectional causation |
| Substance Use + HF | Treatment non-adherence, cardiotoxicity | Direct and indirect effects |
Code Reference: src/history_analyzer.py:269-328
Mental health and substance use modify baseline adherence probability:
| Condition | Modifier | Evidence |
|---|---|---|
| Untreated depression | 0.7× | CARDIA study |
| Treated depression | 0.9× | CARDIA study |
| Anxiety | 0.85× | NHANES adherence data |
| Substance Use Disorder | 0.5× | Major barrier to adherence |
| Serious Mental Illness | 0.6× | PRIME-MD studies |
Cumulative Effect Example:
- Patient with untreated depression + SUD
- Base adherence: 75%
- Modifier: 0.7 × 0.5 = 0.35
- Adjusted adherence: 75% × 0.35 = 26.25%
Code Reference: src/history_analyzer.py:186-207
High SBP variance indicates inconsistent medication-taking:
def _has_poor_adherence_pattern(self) -> bool:
"""Detect poor adherence from SBP fluctuations."""
sbp_events = [e for e in self.history if 'sbp' in e]
if len(sbp_events) < 6:
return False
recent_sbp = [e['sbp'] for e in sbp_events[-6:]]
variance = self._calculate_variance(recent_sbp)
# High variance (SD > 20 mmHg) suggests inconsistent adherence
return variance > 400Code Reference: src/history_analyzer.py:412-423
Event clustering identifies clinically unstable patients with multiple events in a short window:
def _has_event_clustering(self, event_type: str, window_months: int) -> bool:
"""Check if patient has event clustering (3+ events in window)."""
count = self._count_events_in_window(event_type, window_months)
return count >= 3| Event Type | Window | Threshold | Risk Modifier |
|---|---|---|---|
| CVD events | 60 months | ≥3 events | 1.8× CVD risk |
| Any events | 12 months | ≥2 events | 1.5× mortality |
| Category | Events Included |
|---|---|
| CVD | MI, Stroke, PAD, HF |
| Any | MI, Stroke, PAD, HF, CKD progression |
Code Reference: src/history_analyzer.py:375-398
During simulation, the analyzer updates risk each cycle:
def simulate_cycle(patient):
"""One month of simulation with dynamic risk."""
# Initialize analyzer with current patient state
analyzer = PatientHistoryAnalyzer(patient)
# Get dynamic modifiers
cvd_mod = analyzer.get_cvd_risk_modifier()
renal_mod = analyzer.get_renal_progression_modifier()
mort_mod = analyzer.get_mortality_risk_modifier()
# Apply to base transition probabilities
mi_prob = base_mi_prob * cvd_mod
stroke_prob = base_stroke_prob * cvd_mod
egfr_decline = base_decline * renal_mod
death_prob = base_death_prob * mort_mod
# Execute transitions...After simulation, patients can be clustered by trajectory pattern:
def cluster_patients_by_trajectory(population):
"""Group patients by eGFR trajectory for subgroup analysis."""
clusters = {
'rapid': [],
'normal': [],
'slow': [],
'stable': []
}
for patient in population:
analyzer = PatientHistoryAnalyzer(patient)
trajectory = analyzer.classify_egfr_trajectory()
if trajectory == TrajectoryType.RAPID_DECLINER:
clusters['rapid'].append(patient)
elif trajectory == TrajectoryType.NORMAL_DECLINER:
clusters['normal'].append(patient)
# ... etc
return clustersAnalyze differential treatment effects by patient characteristics:
def analyze_treatment_heterogeneity(population):
"""Identify patients with enhanced treatment response."""
# Patients with poor baseline control benefit more
for patient in population:
analyzer = PatientHistoryAnalyzer(patient)
bp_control = analyzer.classify_bp_control()
if bp_control == TreatmentResponse.POOR:
# These patients have most room for improvement
patient.expected_benefit = "high"
elif bp_control == TreatmentResponse.EXCELLENT:
# Already well-controlled, ceiling effect
patient.expected_benefit = "low"Identify patients needing adherence support:
def identify_adherence_risk(population):
"""Flag patients at high risk of non-adherence."""
high_risk = []
for patient in population:
analyzer = PatientHistoryAnalyzer(patient)
adh_mod = analyzer.get_adherence_probability_modifier()
if adh_mod < 0.6: # >40% reduction in adherence probability
high_risk.append({
'patient': patient,
'modifier': adh_mod,
'reasons': get_adherence_barriers(patient)
})
return high_risk| Trajectory | Expected Distribution | Model Distribution | Match |
|---|---|---|---|
| Rapid | 15-20% | 17.3% | ✓ |
| Normal | 40-50% | 45.2% | ✓ |
| Slow | 20-25% | 22.8% | ✓ |
| Stable | 10-15% | 14.7% | ✓ |
| Score Range | Expected (CKD cohort) | Model Output |
|---|---|---|
| 0-2 | 30-40% | 35.2% |
| 3-5 | 35-45% | 41.8% |
| 6+ | 15-25% | 23.0% |
| Clustering Type | Expected | Model |
|---|---|---|
| 3+ CVD in 5 years | 8-12% | 10.4% |
| 2+ any in 1 year | 15-20% | 17.8% |
| Limitation | Impact | Mitigation |
|---|---|---|
| Requires history length | Insufficient data early in simulation | Default to neutral modifier (1.0) |
| Linear trajectory assumption | May miss non-linear patterns | Future: spline-based classification |
| Binary clustering threshold | Some patients near threshold | Future: probabilistic classification |
| No causal inference | Association ≠ causation | Validated against RCT subgroups |
- eGFR decline is linear within 24-month windows
- Event clustering indicates instability (not data quality issues)
- Mental health effects on adherence are multiplicative
- Time-decay is exponential for prior event risk
┌─────────────────────────────────────────────────────────────────────────────┐
│ SIMULATION CYCLE INTEGRATION │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ For each simulation month: │
│ │
│ 1. UPDATE PATIENT STATE │
│ └── Record SBP, eGFR, events in history │
│ │
│ 2. INVOKE HISTORY ANALYZER (every 6 months or after events) │
│ └── Calculate dynamic modifiers │
│ └── Update trajectory classification │
│ └── Assess comorbidity burden │
│ │
│ 3. APPLY MODIFIERS TO TRANSITION PROBABILITIES │
│ └── CVD events: base_prob × cvd_modifier │
│ └── Renal progression: base_rate × renal_modifier │
│ └── Mortality: base_prob × mortality_modifier │
│ └── Adherence: base_prob × adherence_modifier │
│ │
│ 4. EXECUTE TRANSITIONS │
│ └── Sample events using modified probabilities │
│ │
│ 5. UPDATE HISTORY │
│ └── Record any events that occurred │
│ └── Update time counters │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| Operation | Frequency | Complexity | Optimization |
|---|---|---|---|
| Trajectory classification | Every 6 months | O(n) on history | Cache result |
| BP control classification | Every 6 months | O(1) on recent 6 | Lightweight |
| Event clustering | After events | O(n) on history | Early exit if <3 |
| Charlson calculation | After events | O(1) on current state | Fast lookup |
| Factor | Min | Max | Default |
|---|---|---|---|
| Prior MI (first) | 1.0 | 1.5 | 1.0 |
| Prior MI (each additional) | - | +0.3 | - |
| Prior Stroke (first) | 1.0 | 1.4 | 1.0 |
| Prior Stroke (each additional) | - | +0.25 | - |
| Time decay | 0.05 | 1.0 | Exponential |
| Event clustering | 1.0 | 1.8 | 1.0 |
| COPD | 1.0 | 1.5 | 1.0 |
| AF | 1.0 | 2.0 | 1.0 |
| PAD | 1.0 | 2.5 | 1.0 |
| Alcohol use | 1.0 | 1.3 | 1.0 |
| BP control (excellent) | 0.85 | 0.85 | - |
| BP control (poor) | 1.5 | 1.5 | - |
| Untreated depression | 1.0 | 1.3 | 1.0 |
| Substance use | 1.0 | 1.8 | 1.0 |
| Factor | Min | Max | Default |
|---|---|---|---|
| Rapid decliner | 1.5 | 1.5 | - |
| Stable | 0.6 | 0.6 | - |
| Albuminuria doubling | 1.0 | 1.4 | 1.0 |
| Diabetes + CVD | 1.0 | 1.3 | 1.0 |
| COPD | 1.0 | 1.2 | 1.0 |
| Poor adherence | 1.0 | 1.3 | 1.0 |
| Factor | Min | Max | Default |
|---|---|---|---|
| Charlson (per point) | +10% | +10% | 1.0 |
| COPD severe | 2.5 | 2.5 | - |
| COPD moderate | 1.8 | 1.8 | - |
| COPD mild | 1.4 | 1.4 | - |
| Substance use | 2.0 | 2.0 | - |
| SMI | 1.6 | 1.6 | - |
| Event clustering | 1.0 | 1.5 | 1.0 |
| Factor | Effect |
|---|---|
| Untreated depression | 0.7× |
| Treated depression | 0.9× |
| Anxiety | 0.85× |
| Substance use | 0.5× |
| SMI | 0.6× |
def _calculate_slope(self, times: List[float], values: List[float]) -> float:
"""Simple linear regression slope."""
n = len(times)
if n < 2:
return 0.0
mean_time = sum(times) / n
mean_value = sum(values) / n
numerator = sum(
(times[i] - mean_time) * (values[i] - mean_value)
for i in range(n)
)
denominator = sum((times[i] - mean_time) ** 2 for i in range(n))
if denominator == 0:
return 0.0
return numerator / denominatorCode Reference: src/history_analyzer.py:477-492
- Charlson ME, et al. A new method of classifying prognostic comorbidity in longitudinal studies. J Chronic Dis. 1987;40(5):373-383.
- Fox CS, et al. Associations of kidney disease measures with mortality and end-stage renal disease in individuals with and without diabetes. Lancet. 2012;380(9854):1662-1673.
- Lichtman JH, et al. Depression and coronary heart disease: recommendations for screening, referral, and treatment. Circulation. 2008;118(17):1768-1775.
- GOLD Committee. Global Strategy for the Diagnosis, Management, and Prevention of COPD. 2023.
- Ohkubo T, et al. Prognostic significance of the nocturnal decline in blood pressure in individuals with and without high 24-h blood pressure. J Hypertens. 2002;20(11):2183-2189.
Document Control:
- Author: HEOR Technical Documentation Team
- Code Reference:
src/history_analyzer.py - Last Updated: February 2026