Figure 3: Individual Trajectories¶
Purpose¶
Demonstrate individual-specific disease trajectories and how signatures evolve over time.
Panels Required:¶
- Panel A: Case studies of 2-3 individuals showing signature evolution
- Panel B: Multimorbidity patterns - how theta changes before vs. after diagnosis
- Panel C: Signature response to new diagnoses (real-time updating)
- Panel D: Comparison of trajectories between subtypes of same disease
Key Message:¶
Show personalized trajectories and dynamic updating capabilities
In [ ]:
# Setup
import sys
import os
sys.path.append('/Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/')
import torch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
# Set style
sns.set_style("whitegrid")
plt.rcParams['figure.dpi'] = 300
print("Setup complete")
In [ ]:
# ============================================================================
# Plot Patient Timeline (Panel A/B/C Style)
# ============================================================================
import torch
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
# Load data
initial_clusters = torch.load('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/data_for_running/initial_clusters_400k.pt')
if torch.is_tensor(initial_clusters):
initial_clusters = initial_clusters.numpy()
K = int(initial_clusters.max() + 1)
checkpoint_path = '/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized/enrollment_model_W0.0001_batch_0_10000.pt'
pi_path = '/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized/pi_fullmode_batch_0_10000.pt'
ckpt = torch.load(checkpoint_path, map_location='cpu', weights_only=False)
lambda_ = ckpt['model_state_dict']['lambda_']
theta = torch.softmax(lambda_, dim=1).numpy() # (N, K, T)
Y = ckpt['Y']
if torch.is_tensor(Y):
Y_np = Y.numpy()
else:
Y_np = Y
pi_full = torch.load(pi_path, map_location='cpu', weights_only=False)
if torch.is_tensor(pi_full):
pi_np = pi_full.numpy()
else:
pi_np = pi_full
# Load disease names if available
#disease_names = ckpt.get('disease_names', [f'Disease {i}' for i in range(Y_np.shape[1])])
disease_names=pd.read_csv('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/disease_names.csv')['x'].tolist()
# Find patient with signature spike
N, K_total, T = theta.shape
patient_idx = None
for p_idx in range(N):
max_theta = theta[p_idx, :, :].max()
if max_theta > 0.4 and Y_np[p_idx, :, :].sum() > 0:
patient_idx = p_idx
break
if patient_idx is None:
patient_idx = 0
print(f"Selected patient: {patient_idx}")
# Get patient data
patient_theta = theta[patient_idx, :, :] # (K, T)
patient_pi = pi_np[patient_idx, :, :] # (D, T)
patient_Y = Y_np[patient_idx, :, :] # (D, T)
# Find diagnoses
diagnosis_times = {}
for d in range(patient_Y.shape[0]):
event_times = np.where(patient_Y[d, :] == 1)[0]
if len(event_times) > 0:
diagnosis_times[d] = event_times.tolist()
# Convert timepoints to ages
ages = np.arange(30, 30 + T)
# ============================================================================
# Create Plot (Panel A/B/C Style)
# ============================================================================
fig = plt.figure(figsize=(14, 10))
gs = plt.GridSpec(3, 1, height_ratios=[2, 0.8, 1.2], hspace=0.4)
# Panel 1: Signature loadings (θ) vs Age
ax1 = fig.add_subplot(gs[0])
colors = sns.color_palette("tab20", K_total)
for k in range(K_total):
ax1.plot(ages, patient_theta[k, :],
label=f'Signature {k}', linewidth=2, color=colors[k], alpha=0.8)
# Mark diagnosis times
for d, times in diagnosis_times.items():
for t in times:
age_at_diag = 30 + t
ax1.axvline(x=age_at_diag, color='gray', linestyle='--', alpha=0.5, linewidth=1)
ax1.set_ylabel('Signature loadings (θ)', fontsize=12)
ax1.set_title(f'Patient {patient_idx}: Signature Trajectories', fontsize=14, fontweight='bold')
ax1.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=9, ncol=2)
ax1.grid(True, alpha=0.3)
ax1.set_xlim([30, 81])
ax1.set_ylim([0, None])
# Panel 2: Static model summary (Average loading bars)
ax2 = fig.add_subplot(gs[1])
avg_theta = patient_theta.mean(axis=1) # Average across time
bars = ax2.barh(range(K_total), avg_theta, color=colors)
ax2.set_xlabel('Average loading (θ)', fontsize=11)
ax2.set_ylabel('Signature', fontsize=11)
ax2.set_yticks(range(K_total))
ax2.set_yticklabels([f'Sig {k}' for k in range(K_total)], fontsize=9)
ax2.set_xlim([0, 1])
ax2.grid(True, alpha=0.3, axis='x')
ax2.set_title('Static model summary', fontsize=11)
# Panel 3: Disease timeline + Top pi values
ax3 = fig.add_subplot(gs[2])
# Top diseases by max pi
max_pi_per_disease = patient_pi.max(axis=1)
top_diseases = np.argsort(max_pi_per_disease)[::-1][:15] # Top 15
# Plot pi for top diseases
for i, d in enumerate(top_diseases):
sig_for_disease = initial_clusters[d] if d < len(initial_clusters) else -1
color = colors[sig_for_disease] if sig_for_disease < K_total else 'gray'
ax3.plot(ages, patient_pi[d, :],
linewidth=1.5, color=color, alpha=0.6, label=f'D{d}')
# Mark diagnoses
for d, times in diagnosis_times.items():
if d in top_diseases:
sig_for_disease = initial_clusters[d] if d < len(initial_clusters) else -1
color = colors[sig_for_disease] if sig_for_disease < K_total else 'black'
for t in times:
age_at_diag = 30 + t
ax3.scatter(age_at_diag, patient_pi[d, t],
s=100, color=color, edgecolors='black',
linewidth=1.5, zorder=10, marker='o')
ax3.set_xlabel('Age (yr)', fontsize=12)
ax3.set_ylabel('Disease Probability (π)', fontsize=11)
ax3.set_title('Top Disease Probabilities', fontsize=11)
ax3.grid(True, alpha=0.3)
ax3.set_xlim([30, 81])
# Add disease list on the right
disease_list_text = "Diseases:\n"
for d in top_diseases[:10]:
sig = initial_clusters[d] if d < len(initial_clusters) else -1
d_name = disease_names[d] if d < len(disease_names) else f'Disease {d}'
disease_list_text += f"D{d} (sig{sig}): {d_name[:40]}\n"
ax3.text(1.02, 0.5, disease_list_text, transform=ax3.transAxes,
fontsize=8, verticalalignment='center',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.3))
plt.suptitle(f'Patient {patient_idx} Timeline', fontsize=16, fontweight='bold', y=0.995)
plt.tight_layout()
plt.savefig(f'/Users/sarahurbut/aladynoulli2/patient_{patient_idx}_timeline_panel_style.pdf',
dpi=300, bbox_inches='tight')
print(f"\n✓ Saved plot to: patient_{patient_idx}_timeline_panel_style.pdf")
plt.show()
In [ ]:
# ============================================================================
# Plot Patient Timeline - Updated Layout
# ============================================================================
import torch
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
# Load data
initial_clusters = torch.load('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/data_for_running/initial_clusters_400k.pt')
if torch.is_tensor(initial_clusters):
initial_clusters = initial_clusters.numpy()
K = int(initial_clusters.max() + 1)
checkpoint_path = '/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized/enrollment_model_W0.0001_batch_0_10000.pt'
pi_path = '/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized/pi_fullmode_batch_0_10000.pt'
ckpt = torch.load(checkpoint_path, map_location='cpu', weights_only=False)
lambda_ = ckpt['model_state_dict']['lambda_']
theta = torch.softmax(lambda_, dim=1).numpy() # (N, K, T)
Y = ckpt['Y']
if torch.is_tensor(Y):
Y_np = Y.numpy()
else:
Y_np = Y
pi_full = torch.load(pi_path, map_location='cpu', weights_only=False)
if torch.is_tensor(pi_full):
pi_np = pi_full.numpy()
else:
pi_np = pi_full
# Load disease names
disease_names = pd.read_csv('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/disease_names.csv')['x'].tolist()
# Find patient with signature spike
# Find patient with multiple diseases at different times
N, K_total, T = theta.shape
patient_idx = None
best_score = 0
for p_idx in range(N):
# Count number of diseases with events
patient_Y_single = Y_np[p_idx, :, :]
diseases_with_events = np.where(patient_Y_single.sum(axis=1) > 0)[0]
n_diseases = len(diseases_with_events)
if n_diseases < 5: # Need at least 5 diseases
continue
# Get diagnosis times for all diseases
diagnosis_times_list = []
for d in diseases_with_events:
event_times = np.where(patient_Y_single[d, :] == 1)[0]
if len(event_times) > 0:
diagnosis_times_list.extend(event_times.tolist())
if len(diagnosis_times_list) == 0:
continue
# Calculate spread of diagnosis times (want diagnoses spread across time)
diagnosis_times_array = np.array(diagnosis_times_list)
time_spread = diagnosis_times_array.max() - diagnosis_times_array.min()
# Calculate diversity: want diagnoses at different timepoints
unique_times = len(np.unique(diagnosis_times_array))
# Score: prioritize more diseases, more spread, more unique timepoints
score = n_diseases * 10 + time_spread * 2 + unique_times * 5
if score > best_score:
best_score = score
patient_idx = p_idx
if patient_idx is None:
# Fallback: find any patient with multiple diseases
for p_idx in range(N):
patient_Y_single = Y_np[p_idx, :, :]
n_diseases = np.where(patient_Y_single.sum(axis=1) > 0)[0].shape[0]
if n_diseases >= 3:
patient_idx = p_idx
break
if patient_idx is None:
patient_idx = 0
print(f"Selected patient: {patient_idx}")
# Get patient data
patient_theta = theta[patient_idx, :, :] # (K, T)
patient_pi = pi_np[patient_idx, :, :] # (D, T)
patient_Y = Y_np[patient_idx, :, :] # (D, T)
# Find diagnoses
diagnosis_times = {}
for d in range(patient_Y.shape[0]):
event_times = np.where(patient_Y[d, :] == 1)[0]
if len(event_times) > 0:
diagnosis_times[d] = event_times.tolist()
# Print summary
n_diseases = len(diagnosis_times)
all_times = []
for times in diagnosis_times.values():
all_times.extend(times)
time_range = (min(all_times), max(all_times)) if all_times else (0, 0)
print(f" Number of diseases: {n_diseases}")
print(f" Diagnosis timepoints: {sorted(set(all_times))}")
print(f" Time range: {time_range[0]} to {time_range[1]} (ages {30+time_range[0]} to {30+time_range[1]})")
# Get patient data
patient_theta = theta[patient_idx, :, :] # (K, T)
patient_pi = pi_np[patient_idx, :, :] # (D, T)
patient_Y = Y_np[patient_idx, :, :] # (D, T)
# Calculate average theta (K vector) - average across time
avg_theta = patient_theta.mean(axis=1) # Shape: (K,)
# Find diseases with events
diseases_with_events = []
diagnosis_times = {}
for d in range(patient_Y.shape[0]):
event_times = np.where(patient_Y[d, :] == 1)[0]
if len(event_times) > 0:
diseases_with_events.append(d)
diagnosis_times[d] = event_times.tolist()
# Convert timepoints to ages
ages = np.arange(30, 30 + T)
# ============================================================================
# Create Plot
# ============================================================================
fig = plt.figure(figsize=(14, 10))
gs = plt.GridSpec(3, 1, height_ratios=[2, 1, 1.2], hspace=0.4)
colors = sns.color_palette("tab20", K_total)
sig_colors = sns.color_palette("tab20", K_total)
# Panel 1: Signature loadings (θ) vs Age
ax1 = fig.add_subplot(gs[0])
colors = sns.color_palette("tab20", K_total)
for k in range(K_total):
ax1.plot(ages, patient_theta[k, :],
label=f'Signature {k}', linewidth=2, color=colors[k], alpha=0.8)
# Add horizontal lines at diagnosis times
for d, times in diagnosis_times.items():
for t in times:
age_at_diag = 30 + t
# Get the signature for this disease
sig_for_disease = initial_clusters[d] if d < len(initial_clusters) else -1
if sig_for_disease < K_total:
# Draw horizontal line at the theta value for this signature at diagnosis time
theta_at_diag = patient_theta[sig_for_disease, t]
ax1.axhline(y=theta_at_diag, xmin=(age_at_diag - 30) / (81 - 30),
xmax=(age_at_diag - 30 + 1) / (81 - 30),
color=colors[sig_for_disease], linestyle='--',
alpha=0.6, linewidth=1.5)
# Add thin stacked bar showing average theta (single bar, stacked)
# Position it at the top right
ax1_bar = ax1.inset_axes([0.7, 0.7, 0.25, 0.15]) # [x, y, width, height] in axes coordinates
# Sort signatures by average theta (largest first) for better visualization
sorted_indices = np.argsort(avg_theta)[::-1]
sorted_avg_theta = avg_theta[sorted_indices]
sorted_colors = [colors[i] for i in sorted_indices]
# Create stacked bar (single bar)
bottom = 0
for i, (val, color) in enumerate(zip(sorted_avg_theta, sorted_colors)):
if val > 0.01: # Only show if > 1% to avoid clutter
ax1_bar.barh(0, val, left=bottom, color=color, height=0.5, alpha=0.8)
bottom += val
ax1_bar.set_xlim([0, 1])
ax1_bar.set_ylim([-0.5, 0.5])
ax1_bar.set_xticks([0, 0.5, 1.0])
ax1_bar.set_xticklabels(['0', '0.5', '1'], fontsize=7)
ax1_bar.set_yticks([])
ax1_bar.set_title('Avg θ (stacked)', fontsize=8)
ax1_bar.spines['top'].set_visible(False)
ax1_bar.spines['right'].set_visible(False)
ax1_bar.spines['left'].set_visible(False)
ax1.set_ylabel('Signature loadings (θ)', fontsize=12)
ax1.set_title(f'Patient {patient_idx}: Signature Trajectories', fontsize=14, fontweight='bold')
ax1.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=9, ncol=2)
ax1.grid(True, alpha=0.3)
ax1.set_xlim([30, 81])
ax1.set_ylim([0, None])
# Panel 2: Disease timeline (scatter plot)
ax2 = fig.add_subplot(gs[1], sharex=ax1)
if len(diagnosis_times) > 0:
# Sort diagnoses by time
diag_order = sorted([(d, times[0]) for d, times in diagnosis_times.items()],
key=lambda x: x[1])
disease_rows = {d: i for i, (d, _) in enumerate(diag_order)}
for d, t_diag in diag_order:
sig_for_disease = initial_clusters[d] if d < len(initial_clusters) else -1
color = sig_colors[sig_for_disease] if sig_for_disease < K_total else 'gray'
age_at_diag = 30 + t_diag
y = disease_rows[d]
disease_name = disease_names[d] if d < len(disease_names) else f'Disease {d}'
ax2.scatter(age_at_diag, y, s=100, color=color, alpha=0.7, zorder=10,
edgecolors='black', linewidths=1)
ax2.text(age_at_diag + 1, y, f'{disease_name[:30]} (sig{sig_for_disease})',
fontsize=8, verticalalignment='center')
ax2.set_yticks(range(len(diag_order)))
# Label diseases by chronological order (1, 2, 3, ...)
ax2.set_yticklabels([f'{i+1}' for i in range(len(diag_order))], fontsize=8)
else:
ax2.text(0.5, 0.5, 'No diagnoses', transform=ax2.transAxes,
ha='center', va='center', fontsize=12)
ax2.set_ylabel('Disease', fontsize=11)
ax2.set_title('Disease timeline', fontsize=11)
ax2.grid(True, alpha=0.3, axis='x')
ax2.set_xlim([30, 81])
# Panel 3: Disease probabilities (π) - stop after diagnosis
ax3 = fig.add_subplot(gs[2], sharex=ax1)
# Plot diseases with events, colored by signature, stopping after diagnosis
for d in diseases_with_events:
disease_name = disease_names[d] if d < len(disease_names) else f'Disease {d}'
sig_for_disease = initial_clusters[d] if d < len(initial_clusters) else -1
color = sig_colors[sig_for_disease] if sig_for_disease < K_total else 'gray'
# Get diagnosis timepoint
if d in diagnosis_times:
first_diag_t = min(diagnosis_times[d])
# Only plot up to and including diagnosis timepoint
plot_ages = ages[:first_diag_t + 1]
plot_pi = patient_pi[d, :first_diag_t + 1]
else:
plot_ages = ages
plot_pi = patient_pi[d, :]
# Plot probability curve (stops after diagnosis)
ax3.plot(plot_ages, plot_pi,
label=f"{disease_name} (Sig {sig_for_disease})",
color=color, linewidth=2, alpha=0.7)
# Mark diagnosis timepoint
if d in diagnosis_times:
for t in diagnosis_times[d]:
age_at_diag = 30 + t
ax3.scatter(age_at_diag, patient_pi[d, t],
color=color, s=100, zorder=10, marker='o',
edgecolors='black', linewidths=1.5)
ax3.set_xlabel('Age (yr)', fontsize=12)
ax3.set_ylabel('Disease Probability (π)', fontsize=11)
ax3.set_title('Disease Probabilities Over Time (colored by primary signature)', fontsize=11)
ax3.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=8)
ax3.grid(True, alpha=0.3)
ax3.set_xlim([30, 81])
plt.suptitle(f'Patient {patient_idx} Timeline', fontsize=16, fontweight='bold', y=0.995)
plt.tight_layout()
plt.savefig(f'/Users/sarahurbut/aladynoulli2/patient_{patient_idx}_timeline_panel_style.pdf',
dpi=300, bbox_inches='tight')
print(f"\n✓ Saved plot to: patient_{patient_idx}_timeline_panel_style.pdf")
plt.show()
In [ ]:
# ============================================================================
# Plot Patient Timeline - Updated Layout
# ============================================================================
import torch
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from pathlib import Path
# Load data
initial_clusters = torch.load('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/data_for_running/initial_clusters_400k.pt')
if torch.is_tensor(initial_clusters):
initial_clusters = initial_clusters.numpy()
K = int(initial_clusters.max() + 1)
# Load theta from the specified file
theta_path = '/Users/sarahurbut/aladynoulli2/pyScripts/new_thetas_with_pcs_retrospective_correctE.pt'
theta_full = torch.load(theta_path, map_location='cpu', weights_only=False)
# Check structure of theta file
if isinstance(theta_full, dict):
# If it's a dict, try common keys
if 'theta' in theta_full:
theta = theta_full['theta']
elif 'thetas' in theta_full:
theta = theta_full['thetas']
elif 'lambda_' in theta_full:
theta = torch.softmax(theta_full['lambda_'], dim=1)
else:
# Try first tensor value
theta = list(theta_full.values())[0]
if torch.is_tensor(theta) and theta.dim() == 3:
theta = torch.softmax(theta, dim=1)
else:
theta = theta_full
# Convert to numpy if needed
if torch.is_tensor(theta):
theta = theta.numpy()
elif isinstance(theta, list):
theta = np.array(theta)
# Load Y and pi from checkpoint (for patient 148746)
# Load Y and pi from checkpoint (for patient 148746)
checkpoint_path = '/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized/enrollment_model_W0.0001_batch_0_10000.pt'
pi_path = '/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized/pi_fullmode_400k.pt'
ckpt = torch.load(checkpoint_path, map_location='cpu', weights_only=False)
Y = torch.load('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/data_for_running/Y_tensor.pt')
if torch.is_tensor(Y):
Y_np = Y.numpy()
else:
Y_np = Y
pi_full = torch.load(pi_path, map_location='cpu', weights_only=False)
if torch.is_tensor(pi_full):
pi_np = pi_full.numpy()
else:
pi_np = pi_full
# Load disease names
disease_names = pd.read_csv('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/disease_names.csv')['x'].tolist()
# Set patient index to 148746
patient_idx = 148745
# Check dimensions
N_theta, K_total, T_theta = theta.shape
N_y, D, T_y = Y_np.shape
N_pi, D_pi, T_pi = pi_np.shape
print(f"Theta shape: ({N_theta}, {K_total}, {T_theta})")
print(f"Y shape: ({N_y}, {D}, {T_y})")
print(f"Pi shape: ({N_pi}, {D_pi}, {T_pi})")
print(f"\nRequested patient index: {patient_idx}")
# Verify patient index is valid
if patient_idx >= N_theta:
print(f"WARNING: Patient {patient_idx} not in theta array (max index: {N_theta-1})")
patient_idx = min(patient_idx, N_theta - 1)
print(f"Using patient index: {patient_idx}")
if patient_idx >= N_y:
print(f"WARNING: Patient {patient_idx} not in Y array (max index: {N_y-1})")
# We'll handle this below
if patient_idx >= N_pi:
print(f"WARNING: Patient {patient_idx} not in pi array (max index: {N_pi-1})")
# We'll handle this below
# Get patient data
patient_theta = theta[patient_idx, :, :] # (K, T)
patient_Y = Y_np[patient_idx, :, :] if patient_idx < N_y else np.zeros((D, T_y)) # (D, T)
patient_pi = pi_np[patient_idx, :, :] if patient_idx < N_pi else np.zeros((D_pi, T_pi)) # (D, T)
# Ensure T dimensions match (use minimum)
T = min(T_theta, T_y, T_pi)
if T_theta != T:
patient_theta = patient_theta[:, :T]
if T_y != T:
patient_Y = patient_Y[:, :T]
if T_pi != T:
patient_pi = patient_pi[:, :T]
print(f"\nUsing T = {T} (aligned across all arrays)")
# Find diagnoses
diagnosis_times = {}
for d in range(patient_Y.shape[0]):
event_times = np.where(patient_Y[d, :] == 1)[0]
if len(event_times) > 0:
diagnosis_times[d] = event_times.tolist()
# Print summary
n_diseases = len(diagnosis_times)
all_times = []
for times in diagnosis_times.values():
all_times.extend(times)
time_range = (min(all_times), max(all_times)) if all_times else (0, 0)
print(f"\nPatient {patient_idx} Summary:")
print(f" Number of diseases: {n_diseases}")
print(f" Diagnosis timepoints: {sorted(set(all_times))}")
print(f" Time range: {time_range[0]} to {time_range[1]} (ages {30+time_range[0]} to {30+time_range[1]})")
# Calculate average theta (K vector) - average across time
avg_theta = patient_theta.mean(axis=1) # Shape: (K,)
# Find diseases with events
diseases_with_events = []
for d in range(patient_Y.shape[0]):
event_times = np.where(patient_Y[d, :] == 1)[0]
if len(event_times) > 0:
diseases_with_events.append(d)
if d not in diagnosis_times:
diagnosis_times[d] = event_times.tolist()
# Convert timepoints to ages
ages = np.arange(30, 30 + T)
# ============================================================================
# Create Plot
# ============================================================================
fig = plt.figure(figsize=(14, 10))
gs = plt.GridSpec(3, 1, height_ratios=[2, 1, 1.2], hspace=0.4)
colors = sns.color_palette("tab20", K_total)
sig_colors = sns.color_palette("tab20", K_total)
# Panel 1: Signature loadings (θ) vs Age
ax1 = fig.add_subplot(gs[0])
for k in range(K_total):
ax1.plot(ages, patient_theta[k, :],
label=f'Signature {k}', linewidth=2, color=colors[k], alpha=0.8)
# Add horizontal lines at diagnosis times
for d, times in diagnosis_times.items():
for t in times:
if t >= T:
continue
age_at_diag = 30 + t
# Get the signature for this disease
sig_for_disease = initial_clusters[d] if d < len(initial_clusters) else -1
if sig_for_disease < K_total:
# Draw horizontal line at the theta value for this signature at diagnosis time
theta_at_diag = patient_theta[sig_for_disease, t]
ax1.axhline(y=theta_at_diag, xmin=(age_at_diag - 30) / (81 - 30),
xmax=(age_at_diag - 30 + 1) / (81 - 30),
color=colors[sig_for_disease], linestyle='--',
alpha=0.6, linewidth=1.5)
# Add thin stacked bar showing average theta (single bar, stacked)
# Position it at the top right
ax1_bar = ax1.inset_axes([0.7, 0.7, 0.25, 0.15]) # [x, y, width, height] in axes coordinates
# Sort signatures by average theta (largest first) for better visualization
sorted_indices = np.argsort(avg_theta)[::-1]
sorted_avg_theta = avg_theta[sorted_indices]
sorted_colors = [colors[i] for i in sorted_indices]
# Create stacked bar (single bar)
bottom = 0
for i, (val, color) in enumerate(zip(sorted_avg_theta, sorted_colors)):
if val > 0.01: # Only show if > 1% to avoid clutter
ax1_bar.barh(0, val, left=bottom, color=color, height=0.5, alpha=0.8)
bottom += val
ax1_bar.set_xlim([0, 1])
ax1_bar.set_ylim([-0.5, 0.5])
ax1_bar.set_xticks([0, 0.5, 1.0])
ax1_bar.set_xticklabels(['0', '0.5', '1'], fontsize=7)
ax1_bar.set_yticks([])
ax1_bar.set_title('Avg θ (stacked)', fontsize=8)
ax1_bar.spines['top'].set_visible(False)
ax1_bar.spines['right'].set_visible(False)
ax1_bar.spines['left'].set_visible(False)
ax1.set_ylabel('Signature loadings (θ)', fontsize=12)
ax1.set_title(f'Patient {patient_idx}: Signature Trajectories', fontsize=14, fontweight='bold')
ax1.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=9, ncol=2)
ax1.grid(True, alpha=0.3)
ax1.set_xlim([30, 81])
ax1.set_ylim([0, None])
# Panel 2: Disease timeline (scatter plot)
ax2 = fig.add_subplot(gs[1], sharex=ax1)
if len(diagnosis_times) > 0:
# Sort diagnoses by time
diag_order = sorted([(d, times[0]) for d, times in diagnosis_times.items()],
key=lambda x: x[1])
disease_rows = {d: i for i, (d, _) in enumerate(diag_order)}
for d, t_diag in diag_order:
if t_diag >= T:
continue
sig_for_disease = initial_clusters[d] if d < len(initial_clusters) else -1
color = sig_colors[sig_for_disease] if sig_for_disease < K_total else 'gray'
age_at_diag = 30 + t_diag
y = disease_rows[d]
disease_name = disease_names[d] if d < len(disease_names) else f'Disease {d}'
ax2.scatter(age_at_diag, y, s=100, color=color, alpha=0.7, zorder=10,
edgecolors='black', linewidths=1)
ax2.text(age_at_diag + 1, y, f'{disease_name[:30]} (sig{sig_for_disease})',
fontsize=8, verticalalignment='center')
ax2.set_yticks(range(len(diag_order)))
# Label diseases by chronological order (1, 2, 3, ...)
ax2.set_yticklabels([f'{i+1}' for i in range(len(diag_order))], fontsize=8)
else:
ax2.text(0.5, 0.5, 'No diagnoses', transform=ax2.transAxes,
ha='center', va='center', fontsize=12)
ax2.set_ylabel('Disease', fontsize=11)
ax2.set_title('Disease timeline', fontsize=11)
ax2.grid(True, alpha=0.3, axis='x')
ax2.set_xlim([30, 81])
# Panel 3: Disease probabilities (π) - stop after diagnosis
ax3 = fig.add_subplot(gs[2], sharex=ax1)
# Plot diseases with events, colored by signature, stopping after diagnosis
for d in diseases_with_events:
disease_name = disease_names[d] if d < len(disease_names) else f'Disease {d}'
sig_for_disease = initial_clusters[d] if d < len(initial_clusters) else -1
color = sig_colors[sig_for_disease] if sig_for_disease < K_total else 'gray'
# Get diagnosis timepoint
if d in diagnosis_times:
first_diag_t = min(diagnosis_times[d])
if first_diag_t >= T:
first_diag_t = T - 1
# Only plot up to and including diagnosis timepoint
plot_ages = ages[:first_diag_t + 1]
plot_pi = patient_pi[d, :first_diag_t + 1]
else:
plot_ages = ages
plot_pi = patient_pi[d, :]
# Plot probability curve (stops after diagnosis)
ax3.plot(plot_ages, plot_pi,
label=f"{disease_name} (Sig {sig_for_disease})",
color=color, linewidth=2, alpha=0.7)
# Mark diagnosis timepoint
if d in diagnosis_times:
for t in diagnosis_times[d]:
if t >= T:
continue
age_at_diag = 30 + t
ax3.scatter(age_at_diag, patient_pi[d, t],
color=color, s=100, zorder=10, marker='o',
edgecolors='black', linewidths=1.5)
ax3.set_xlabel('Age (yr)', fontsize=12)
ax3.set_ylabel('Disease Probability (π)', fontsize=11)
ax3.set_title('Disease Probabilities Over Time (colored by primary signature)', fontsize=11)
ax3.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=8)
ax3.grid(True, alpha=0.3)
ax3.set_xlim([30, 81])
plt.suptitle(f'Patient {patient_idx} Timeline', fontsize=16, fontweight='bold', y=0.995)
plt.tight_layout()
plt.savefig(f'/Users/sarahurbut/aladynoulli2/patient_{patient_idx}_timeline_panel_style.pdf',
dpi=300, bbox_inches='tight')
print(f"\n✓ Saved plot to: patient_{patient_idx}_timeline_panel_style.pdf")
plt.show()
In [ ]:
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/main_paper_figures/plot_patient_timeline.py
In [ ]:
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/main_paper_figures/generate_prs_signature_plots.py --batch_dir "/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized_noPCS/" --pattern "enrollment_model_VECTORIZED_W0.0001_batch_*_*.pt" --output_dir "/Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/results/paper_figs/prs_signatures_nopPCS_E" --n_top 30
In [ ]:
from analyze_age_onset import analyze_age_onset_patterns
early_indices, late_indices, stats = analyze_age_onset_patterns(
results_base_dir='/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized',
disease_index=113, # MI
early_threshold=55,
late_threshold=70,
output_path='/Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/results/paper_figs/fig3/mi_onset_patterns_all_batches.pdf', # optional
return_stats=True
)