Figure 2: Population-Level Patterns¶
Purpose¶
Show population-level disease signature patterns and their temporal evolution.
Panels Required:¶
- Panel A: Heatmap of discovered signatures with top diseases for each
- Panel B: Patient clustering based on signature weights
- Panel C: Signature prevalence across age cohorts (young vs. old)
- Panel D: Temporal evolution of signature prevalence in population
Key Message:¶
Focus on trajectory aspects, not just clusters - emphasize temporal dynamics
# 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 for publication-quality figures
sns.set_style("whitegrid")
plt.rcParams['figure.dpi'] = 300
plt.rcParams['savefig.dpi'] = 300
plt.rcParams['font.size'] = 11
plt.rcParams['axes.labelsize'] = 12
plt.rcParams['axes.titlesize'] = 14
plt.rcParams['xtick.labelsize'] = 10
plt.rcParams['ytick.labelsize'] = 10
plt.rcParams['legend.fontsize'] = 9
plt.rcParams['figure.titlesize'] = 16
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans', 'Helvetica', 'Liberation Sans']
print("Setup complete")
# Define plotting functions
from scipy.special import expit as sigmoid
def plot_single_signature(phi_np, clusters_np, disease_names, signature_idx,
age_offset=30, figsize=(10, 7), top_n_diseases=None,
plot_probability=False, prevalence_t_np=None, logit_prev_t_np=None):
"""
Plot a single signature with prettier styling for publication.
Parameters:
-----------
phi_np : np.ndarray, shape (K, D, T)
Phi values (log hazard ratios) for each signature, disease, and timepoint
clusters_np : np.ndarray, shape (D,)
Disease-to-signature assignments
disease_names : list
List of disease names
signature_idx : int
Signature index to plot
age_offset : int
Age offset (timepoint 0 = age_offset)
figsize : tuple
Figure size (width, height)
top_n_diseases : int or None
If specified, plot only top N signature-specific diseases based on max phi value
plot_probability : bool
If True, convert phi to probability using sigmoid. If False, plot log hazard ratio.
prevalence_t_np : np.ndarray or None, shape (D, T)
Smoothed prevalence for each disease and timepoint. If provided, will be overlaid.
logit_prev_t_np : np.ndarray or None, shape (D, T)
Logit prevalence for each disease and timepoint. If provided, will be overlaid as probability.
Returns:
--------
fig : matplotlib.figure.Figure
The figure object
"""
K, D, T = phi_np.shape
age_points = np.arange(T) + age_offset
# Convert to probability if requested
if plot_probability:
plot_values = sigmoid(phi_np)
y_label = 'Probability'
else:
plot_values = phi_np
y_label = 'Log Hazard Ratio'
# Create single figure
fig, ax = plt.subplots(figsize=figsize)
k = signature_idx
# Find diseases assigned to this signature
sig_disease_indices = np.where(clusters_np == k)[0]
# Find all other diseases (background)
background_disease_indices = np.where(clusters_np != k)[0]
# Plot background diseases in light grey
for d in background_disease_indices:
ax.plot(age_points, plot_values[k, d, :],
color='lightgray', alpha=0.15, linewidth=0.5, zorder=1)
# Plot signature-specific diseases in colored lines
if len(sig_disease_indices) > 0:
# Optionally select top N diseases
if top_n_diseases is not None and len(sig_disease_indices) > top_n_diseases:
# Rank by max value
max_values = np.max(plot_values[k, sig_disease_indices, :], axis=1)
top_indices = np.argsort(max_values)[-top_n_diseases:]
sig_disease_indices = sig_disease_indices[top_indices]
# Use a nice color palette
n_colors = len(sig_disease_indices)
if n_colors <= 10:
colors = plt.cm.tab10(np.linspace(0, 1, n_colors))
else:
colors = plt.cm.Set3(np.linspace(0, 1, n_colors))
for i, d in enumerate(sig_disease_indices):
color = colors[i]
disease_name = disease_names[d] if d < len(disease_names) else f"Disease_{d}"
# Plot model's learned phi
ax.plot(age_points, plot_values[k, d, :],
color=color, linewidth=2.5, label=disease_name, zorder=3, alpha=0.9)
# Overlay smoothed prevalence if available
if prevalence_t_np is not None and d < prevalence_t_np.shape[0]:
if isinstance(prevalence_t_np, torch.Tensor):
prev_values = prevalence_t_np[d, :].detach().cpu().numpy()
else:
prev_values = prevalence_t_np[d, :]
ax.plot(age_points, prev_values,
color=color, linewidth=1.8, linestyle='--',
alpha=0.5, label=f'{disease_name} (prev)', zorder=2)
# Styling
ax.set_title(f'Signature {k}', fontsize=16, fontweight='bold', pad=15)
ax.set_xlabel('Age (years)', fontsize=13, fontweight='bold')
ax.set_ylabel(y_label, fontsize=13, fontweight='bold')
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5, color='gray')
ax.set_xlim(age_points[0], age_points[-1])
# Set y-axis limits based on data range
if len(sig_disease_indices) > 0:
sig_data = plot_values[k, sig_disease_indices, :]
y_min = max(0, np.min(sig_data) * 0.95) if plot_probability else np.min(sig_data) * 1.05
y_max = np.max(sig_data) * 1.05
# Also consider prevalence if available
if prevalence_t_np is not None:
if isinstance(prevalence_t_np, torch.Tensor):
prev_data = prevalence_t_np[sig_disease_indices, :].detach().cpu().numpy()
else:
prev_data = prevalence_t_np[sig_disease_indices, :]
y_min = min(y_min, max(0, np.min(prev_data) * 0.95))
y_max = max(y_max, np.max(prev_data) * 1.05)
ax.set_ylim(y_min, y_max)
# Add legend
if len(sig_disease_indices) > 0:
ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=9,
framealpha=0.95, edgecolor='gray', fancybox=True, frameon=True)
plt.tight_layout()
return fig
def plot_signature_phi_patterns(phi_np, clusters_np, disease_names, selected_signatures=None,
age_offset=30, figsize=(8, 5), top_n_diseases=None,
plot_probability=True, prevalence_t_np=None, logit_prev_t_np=None):
"""
Plot phi values (as probability or log hazard ratio) for signature-specific diseases (colored)
vs background diseases (in grey), arranged vertically. Optionally overlay prevalence and logit prevalence.
Parameters:
-----------
phi_np : np.ndarray, shape (K, D, T)
Phi values (log hazard ratios) for each signature, disease, and timepoint
clusters_np : np.ndarray, shape (D,)
Disease-to-signature assignments
disease_names : list
List of disease names
selected_signatures : list or None
List of signature indices to plot. If None, plot all signatures.
age_offset : int
Age offset (timepoint 0 = age_offset)
figsize : tuple
Figure size per signature subplot (width, height)
top_n_diseases : int or None
If specified, plot only top N signature-specific diseases based on max phi value
plot_probability : bool
If True, convert phi to probability using sigmoid. If False, plot log hazard ratio.
prevalence_t_np : np.ndarray or None, shape (D, T)
Smoothed prevalence for each disease and timepoint. If provided, will be overlaid.
logit_prev_t_np : np.ndarray or None, shape (D, T)
Logit prevalence for each disease and timepoint. If provided, will be overlaid as probability.
"""
K, D, T = phi_np.shape
if selected_signatures is None:
selected_signatures = list(range(K))
n_sigs = len(selected_signatures)
age_points = np.arange(T) + age_offset
# Convert to probability if requested
if plot_probability:
plot_values = sigmoid(phi_np)
y_label = 'Prob (disease | sig k, age)'
else:
plot_values = phi_np
y_label = 'Log hazard ratio'
# Create subplots in a grid (3 rows x 5 cols for 15 signatures)
n_cols = 5
n_rows = (n_sigs + n_cols - 1) // n_cols # Ceiling division
fig, axes = plt.subplots(n_rows, n_cols, figsize=(figsize[0] * n_cols, figsize[1] * n_rows),
sharex=True, sharey=False)
# Convert axes to a flat array for easier indexing
if n_sigs == 1:
axes_flat = np.array([axes]).flatten()
elif n_rows == 1:
axes_flat = np.array(axes).flatten()
else:
axes_flat = axes.flatten()
for idx, k in enumerate(selected_signatures):
ax = axes_flat[idx]
# Find diseases assigned to this signature
sig_disease_indices = np.where(clusters_np == k)[0]
# Find all other diseases (background)
background_disease_indices = np.where(clusters_np != k)[0]
# Plot background diseases in grey
for d in background_disease_indices:
ax.plot(age_points, plot_values[k, d, :],
color='grey', alpha=0.2, linewidth=0.3, zorder=1)
# Plot signature-specific diseases in colored lines
if len(sig_disease_indices) > 0:
# Optionally select top N diseases
if top_n_diseases is not None and len(sig_disease_indices) > top_n_diseases:
# Rank by max value
max_values = np.max(plot_values[k, sig_disease_indices, :], axis=1)
top_indices = np.argsort(max_values)[-top_n_diseases:]
sig_disease_indices = sig_disease_indices[top_indices]
# Use different colors for signature-specific diseases
colors = plt.cm.tab10(np.linspace(0, 1, min(len(sig_disease_indices), 10)))
for i, d in enumerate(sig_disease_indices):
color = colors[i % len(colors)] if len(sig_disease_indices) <= 10 else 'red'
disease_name = disease_names[d] if d < len(disease_names) else f"Disease_{d}"
# Plot model's learned phi (as probability)
ax.plot(age_points, plot_values[k, d, :],
color=color, linewidth=2.5, label=disease_name, zorder=2)
# Overlay smoothed prevalence if available
if prevalence_t_np is not None and d < prevalence_t_np.shape[0]:
# Convert to numpy if it's a torch tensor
if isinstance(prevalence_t_np, torch.Tensor):
prev_values = prevalence_t_np[d, :].detach().cpu().numpy()
else:
prev_values = prevalence_t_np[d, :]
ax.plot(age_points, prev_values,
color=color, linewidth=1.5, linestyle='--',
alpha=0.6, label=f'{disease_name} (prev)', zorder=3)
# Overlay logit prevalence (converted to probability) if available
if logit_prev_t_np is not None and d < logit_prev_t_np.shape[0]:
# Convert to numpy if it's a torch tensor
if isinstance(logit_prev_t_np, torch.Tensor):
logit_prev_values = logit_prev_t_np[d, :].detach().cpu().numpy()
else:
logit_prev_values = logit_prev_t_np[d, :]
logit_prev_prob = sigmoid(logit_prev_values)
ax.plot(age_points, logit_prev_prob,
color=color, linewidth=1.5, linestyle=':',
alpha=0.6, label=f'{disease_name} (logit prev)', zorder=3)
ax.set_title(f'Signature {k}', fontsize=12, fontweight='bold', pad=5)
if idx >= (n_rows - 1) * n_cols: # Only label x-axis on bottom row
ax.set_xlabel('Age (yr)', fontsize=10)
if idx % n_cols == 0: # Only label y-axis on left column
ax.set_ylabel(y_label, fontsize=10)
ax.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)
ax.set_xlim(age_points[0], age_points[-1])
# Set y-axis limits based on data range for this signature
if len(sig_disease_indices) > 0:
sig_data = plot_values[k, sig_disease_indices, :]
y_min = max(0, np.min(sig_data) * 0.9) if plot_probability else np.min(sig_data) * 1.1
y_max = np.max(sig_data) * 1.1
# Also consider prevalence and logit prevalence if available
if prevalence_t_np is not None:
# Convert to numpy if it's a torch tensor
if isinstance(prevalence_t_np, torch.Tensor):
prev_data = prevalence_t_np[sig_disease_indices, :].detach().cpu().numpy()
else:
prev_data = prevalence_t_np[sig_disease_indices, :]
y_min = min(y_min, max(0, np.min(prev_data) * 0.9))
y_max = max(y_max, np.max(prev_data) * 1.1)
if logit_prev_t_np is not None:
# Convert to numpy if it's a torch tensor
if isinstance(logit_prev_t_np, torch.Tensor):
logit_prev_t_np_numpy = logit_prev_t_np.detach().cpu().numpy()
else:
logit_prev_t_np_numpy = logit_prev_t_np
logit_prev_prob_data = sigmoid(logit_prev_t_np_numpy[sig_disease_indices, :])
y_min = min(y_min, max(0, np.min(logit_prev_prob_data) * 0.9))
y_max = max(y_max, np.max(logit_prev_prob_data) * 1.1)
ax.set_ylim(y_min, y_max)
# Add legend if there are signature-specific diseases (only for first few to avoid clutter)
if len(sig_disease_indices) > 0 and len(sig_disease_indices) <= 10 and idx < 3: # Only show legend for first 3 signatures
ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=6,
framealpha=0.9, edgecolor='gray', fancybox=True)
# Hide unused subplots
for idx in range(n_sigs, n_rows * n_cols):
axes_flat[idx].set_visible(False)
plt.tight_layout(rect=[0, 0, 0.95, 1]) # Leave space for legend
return fig
print("Plotting functions defined!")
Panel A: Signature-Disease Heatmap¶
Show heatmap of signatures with top diseases for each.
Note: We can use the existing plot_disease_blocks function from utils.py which creates heatmaps showing cluster correspondence between biobanks. This shows how diseases are assigned to signatures (clusters).
# Load data
from helper_py.pathway_discovery import load_full_data
# Load UKB model checkpoint to get clusters (disease-signature assignments)
import torch
# Load model checkpoints for cross-cohort comparison
# Note: plot_disease_blocks shows cluster correspondence between biobanks
# For main paper, we may want to show UKB signature-disease assignments
ukb_checkpoint_path = '/Users/sarahurbut/Dropbox-Personal/model_with_kappa_bigam.pt'
ukb_checkpoint = torch.load(ukb_checkpoint_path, map_location='cpu')
print(f"UKB checkpoint keys: {list(ukb_checkpoint.keys())}")
print(f"Number of diseases: {len(ukb_checkpoint['disease_names'])}")
print(f"Number of signatures: {ukb_checkpoint['clusters'].max() + 1 if hasattr(ukb_checkpoint['clusters'], 'max') else 'N/A'}")
# Extract clusters (disease-to-signature assignments)
clusters = ukb_checkpoint['clusters']
disease_names = ukb_checkpoint['disease_names']
# Convert to list if needed
if isinstance(disease_names, (list, tuple)):
disease_names = list(disease_names)
elif hasattr(disease_names, 'values'):
disease_names = disease_names.values.tolist()
# Create signature-disease heatmap
# Option 1: Use existing plot_disease_blocks for cross-cohort comparison
# Option 2: Create new heatmap showing UKB signature-disease assignments
# For main paper Figure 2A, we want to show:
# - Each signature (column) with top diseases (rows)
# - Diseases clustered within each signature based on their psi/phi associations
# Load psi matrix (disease-signature associations)
if 'psi' in ukb_checkpoint:
psi = ukb_checkpoint['psi']
print(f"Psi shape: {psi.shape}")
# Create heatmap of psi values
# Rows: diseases, Columns: signatures
# This shows the strength of association between each disease and signature
# TODO: Create publication-ready heatmap
# - Show top diseases for each signature
# - Cluster diseases within signatures
# - Use appropriate color scale
# - Add labels and legend
else:
print("Psi matrix not found in checkpoint")
print("Available keys:", list(ukb_checkpoint.keys()))
# Load master checkpoint with pooled phi from corrected E
master_checkpoint = torch.load('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/data_for_running/master_for_fitting_pooled_correctedE.pt', map_location='cpu')
phi_np = master_checkpoint['model_state_dict']['phi'].numpy()
# Load clusters from old checkpoint (clusters don't change)
ukb_checkpoint_path = '/Users/sarahurbut/Dropbox-Personal/model_with_kappa_bigam.pt'
ukb_checkpoint = torch.load(ukb_checkpoint_path, map_location='cpu')
clusters_np = ukb_checkpoint['clusters']
# Load corrected prevalence
prevalence_t_np = torch.load('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/data_for_running/prevalence_t_corrected.pt', map_location='cpu')
if torch.is_tensor(prevalence_t_np):
prevalence_t_np = prevalence_t_np.numpy()
# Get disease names
disease_names = ukb_checkpoint['disease_names']
if isinstance(disease_names, (list, tuple)):
disease_names = list(disease_names)
elif hasattr(disease_names, 'values'):
disease_names = disease_names.values.tolist()
print(f"Loaded master checkpoint:")
print(f" Phi shape: {phi_np.shape}")
print(f" Clusters shape: {clusters_np.shape if hasattr(clusters_np, 'shape') else len(clusters_np)}")
print(f" Prevalence shape: {prevalence_t_np.shape}")
selected_sigs = [5, 6, 11, 14]
fig = plot_signature_phi_patterns(phi_np, clusters_np, disease_names,
selected_signatures=selected_sigs,
top_n_diseases=7, # Show top 7 signature-specific diseases
plot_probability=False, # Plot log hazard ratio (not probability)
prevalence_t_np=prevalence_t_np) # Overlay prevalence
plt.show()
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/main_paper_figures/generate_fig2_signatures.py
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/pool_aou_phi_from_batches.py
## Panel E: Cross-Cohort Comparison - Cardiovascular and Malignancy Signatures
# Load all three model checkpoints
# UKB: master checkpoint with pooled phi
ukb_master = torch.load('/Users/sarahurbut/Library/CloudStorage/Dropbox-Personal/data_for_running/master_for_fitting_pooled_correctedE.pt', map_location='cpu')
ukb_phi = ukb_master['model_state_dict']['phi'].numpy()
# AOU: initialized model
aou_checkpoint = torch.load('/Users/sarahurbut/aladynoulli2/aou_model_master_correctedE.pt', map_location='cpu')
aou_phi = aou_checkpoint['model_state_dict']['phi'].numpy()
# MGB: initialized model
mgb_checkpoint = torch.load('/Users/sarahurbut/aladynoulli2/mgb_model_initialized.pt', map_location='cpu')
mgb_phi = mgb_checkpoint['model_state_dict']['phi'].numpy()
mgb_checkpoint2 = torch.load('/Users/sarahurbut/Dropbox-Personal/model_with_kappa_bigam_MGB.pt')
# Get clusters and disease names from UKB (for reference)
ukb_checkpoint_ref = torch.load('/Users/sarahurbut/Dropbox-Personal/model_with_kappa_bigam.pt', map_location='cpu')
ukb_clusters = ukb_checkpoint_ref['clusters']
ukb_disease_names = ukb_checkpoint_ref['disease_names']
if isinstance(ukb_disease_names, (list, tuple)):
ukb_disease_names = list(ukb_disease_names)
elif hasattr(ukb_disease_names, 'values'):
ukb_disease_names = ukb_disease_names.values.tolist()
# Get AOU and MGB disease names
aou_disease_names = aou_checkpoint['disease_names']
if isinstance(aou_disease_names, (list, tuple)):
aou_disease_names = list(aou_disease_names)
elif hasattr(aou_disease_names, 'values'):
aou_disease_names = aou_disease_names.values.tolist()
mgb_disease_names = mgb_checkpoint['disease_names']
if isinstance(mgb_disease_names, (list, tuple)):
mgb_disease_names = list(mgb_disease_names)
elif hasattr(mgb_disease_names, 'values'):
mgb_disease_names = mgb_disease_names.values.tolist()
# Define signature mappings
# Cardiovascular: UKB 5, AOU 16, MGB 5
# Malignancy: UKB 6, AOU 11, MGB 11
cv_signatures = {'ukb': 5, 'aou': 16, 'mgb': 5}
malignancy_signatures = {'ukb': 6, 'aou': 11, 'mgb': 11}
# Define diseases to plot for each signature type
# Cardiovascular diseases (from UKB signature 5)
cv_diseases_ukb = [
('Coronary atherosclerosis', 111), # Need to find actual indices
('Myocardial infarction', 112),
('Other chronic ischemic heart disease, unspecified', 115),
('Unstable angina (intermediate coronary syndrome)', 113)
]
# Malignancy diseases (from UKB signature 6)
malignancy_diseases_ukb = [
('Malignant neoplasm, other', 18),
('Secondary malignancy of bone', 20),
('Secondary malignancy of lymph nodes', 21),
('Secondary malignancy of respiratory organs', 22),
('Secondary malignant neoplasm', 19),
('Secondary malignant neoplasm of digestive systems', 23),
('Secondary malignant neoplasm of liver', 24)
]
# Helper function to find disease index by name
def find_disease_index(disease_name, disease_names_list):
"""Find disease index by name (fuzzy matching)"""
for i, name in enumerate(disease_names_list):
if disease_name.lower() in str(name).lower() or str(name).lower() in disease_name.lower():
return i
return None
# Create the 3x2 plot
fig, axes = plt.subplots(3, 2, figsize=(14, 12))
# Get actual time dimension from phi (should be 51 or 52)
T = ukb_phi.shape[2]
age_points = np.arange(30, 30 + T) # Ages starting at 30
print(f"Time dimension: {T}, Age range: {age_points[0]}-{age_points[-1]}")
# Colors for diseases
cv_colors = ['green', 'orange', 'lightblue', 'purple']
malignancy_colors = ['lightblue', 'orange', 'green', 'brown', 'yellow', 'pink', 'peachpuff'] # Fixed: peachpuff instead of lightorange
# First, identify specific cardiovascular diseases in UKB signature 5 by name
# These are the diseases we want to plot: Coronary atherosclerosis, Myocardial infarction, etc.
ukb_cv_sig = 5
if isinstance(ukb_clusters, torch.Tensor):
ukb_clusters_np = ukb_clusters.numpy()
else:
ukb_clusters_np = ukb_clusters
ukb_cv_indices = np.where(ukb_clusters_np == ukb_cv_sig)[0]
ukb_cv_names = [ukb_disease_names[i] for i in ukb_cv_indices if i < len(ukb_disease_names)]
# Target cardiovascular disease names to match
target_cv_diseases = [
'Coronary atherosclerosis',
'Myocardial infarction',
'Other chronic ischemic heart disease',
'Unstable angina'
]
# Find these diseases in UKB signature 5
ukb_cv_target_names = []
ukb_cv_target_indices = []
for target_name in target_cv_diseases:
for i, ukb_name in enumerate(ukb_cv_names):
if (target_name.lower() in str(ukb_name).lower() or
str(ukb_name).lower() in target_name.lower()):
if ukb_cv_indices[i] not in ukb_cv_target_indices:
ukb_cv_target_names.append(ukb_name)
ukb_cv_target_indices.append(ukb_cv_indices[i])
break
print(f"\nUKB cardiovascular signature {ukb_cv_sig} target diseases ({len(ukb_cv_target_names)}):")
for i, (idx, name) in enumerate(zip(ukb_cv_target_indices, ukb_cv_target_names)):
print(f" {i}: {name} (idx {idx})")
# Plot Cardiovascular signatures (left column) - MATCH BY NAME
for row_idx, (cohort, sig_idx) in enumerate(cv_signatures.items()):
ax = axes[row_idx, 0]
if cohort == 'ukb':
phi_data = ukb_phi
clusters = ukb_clusters
disease_names_list = ukb_disease_names
elif cohort == 'aou':
phi_data = aou_phi
clusters = aou_checkpoint.get('clusters', None)
disease_names_list = aou_disease_names
else: # mgb
phi_data = mgb_phi
clusters = mgb_checkpoint.get('clusters', None)
disease_names_list = mgb_disease_names
# Debug: Print info
print(f"\n{cohort.upper()} - Cardiovascular signature {sig_idx}:")
print(f" Phi shape: {phi_data.shape}")
print(f" Clusters type: {type(clusters)}")
# Get diseases in this signature
if clusters is None:
print(f" WARNING: No clusters found for {cohort}!")
sig_disease_indices = []
matched_disease_indices = []
else:
if isinstance(clusters, torch.Tensor):
clusters_np = clusters.numpy()
else:
clusters_np = clusters
print(f" Clusters shape: {clusters_np.shape if hasattr(clusters_np, 'shape') else len(clusters_np)}")
print(f" Unique signatures: {np.unique(clusters_np) if hasattr(clusters_np, 'shape') else 'N/A'}")
sig_disease_indices = np.where(clusters_np == sig_idx)[0]
print(f" Diseases in signature {sig_idx}: {len(sig_disease_indices)}")
# Match diseases by name with UKB cardiovascular target diseases
matched_disease_indices = []
matched_names = []
for ukb_name in ukb_cv_target_names:
# Find this disease in the current cohort's signature
for d_idx in sig_disease_indices:
if d_idx < len(disease_names_list):
cohort_disease_name = str(disease_names_list[d_idx])
# Fuzzy matching: check if names are similar
if (ukb_name.lower() in cohort_disease_name.lower() or
cohort_disease_name.lower() in ukb_name.lower() or
ukb_name.lower().replace(' ', '') == cohort_disease_name.lower().replace(' ', '')):
if d_idx not in matched_disease_indices:
matched_disease_indices.append(d_idx)
matched_names.append(cohort_disease_name)
break
print(f" Matched {len(matched_disease_indices)} diseases with UKB:")
for i, (idx, name) in enumerate(zip(matched_disease_indices[:10], matched_names[:10])):
print(f" {i}: {name} (idx {idx})")
# Plot matched diseases (or all if no matches found)
diseases_to_plot = matched_disease_indices if len(matched_disease_indices) > 0 else sig_disease_indices[:4]
if len(diseases_to_plot) > 0:
# Get disease names and plot
for i, d_idx in enumerate(diseases_to_plot):
if d_idx < phi_data.shape[1]:
phi_values = phi_data[sig_idx, d_idx, :]
# Ensure age_points matches phi_values length
if len(phi_values) != len(age_points):
age_points_adj = np.arange(30, 30 + len(phi_values))
else:
age_points_adj = age_points
disease_name = disease_names_list[d_idx] if d_idx < len(disease_names_list) else f"Disease {d_idx}"
ax.plot(age_points_adj, phi_values, label=disease_name,
color=cv_colors[i % len(cv_colors)], linewidth=2)
else:
print(f" WARNING: No diseases found in {cohort} signature {sig_idx}!")
ax.text(0.5, 0.5, f'No diseases in signature {sig_idx}',
transform=ax.transAxes, ha='center', va='center', fontsize=12)
ax.set_xlabel('Age (yr)', fontsize=10)
ax.set_ylabel('Log hazard ratio', fontsize=10)
ax.set_title(f'{cohort.upper()}', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3)
ax.set_ylim(-12, -3)
ax.set_xlim(30, 80)
if row_idx == 0:
ax.set_title('Cardiovascular signatures', fontsize=12, fontweight='bold')
if row_idx == 2 and len(sig_disease_indices) > 0:
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8)
# First, identify diseases in UKB signature 6 (malignancy) by name
# Then match those diseases across AOU and MGB
ukb_malignancy_sig = 6
if isinstance(ukb_clusters, torch.Tensor):
ukb_clusters_np = ukb_clusters.numpy()
else:
ukb_clusters_np = ukb_clusters
ukb_malignancy_indices = np.where(ukb_clusters_np == ukb_malignancy_sig)[0]
ukb_malignancy_names = [ukb_disease_names[i] for i in ukb_malignancy_indices if i < len(ukb_disease_names)]
print(f"\nUKB malignancy signature {ukb_malignancy_sig} diseases ({len(ukb_malignancy_names)}):")
for i, name in enumerate(ukb_malignancy_names[:10]): # Print first 10
print(f" {i}: {name}")
# Plot Malignancy signatures (right column) - MATCH BY NAME
for row_idx, (cohort, sig_idx) in enumerate(malignancy_signatures.items()):
ax = axes[row_idx, 1]
if cohort == 'ukb':
phi_data = ukb_phi
clusters = ukb_clusters
disease_names_list = ukb_disease_names
elif cohort == 'aou':
phi_data = aou_phi
clusters = aou_checkpoint.get('clusters', None)
disease_names_list = aou_disease_names
else: # mgb
phi_data = mgb_phi
clusters = mgb_checkpoint.get('clusters', None)
disease_names_list = mgb_disease_names
# Debug: Print info
print(f"\n{cohort.upper()} - Malignancy signature {sig_idx}:")
print(f" Phi shape: {phi_data.shape}")
print(f" Clusters type: {type(clusters)}")
# Get diseases in this signature
if clusters is None:
print(f" WARNING: No clusters found for {cohort}!")
sig_disease_indices = []
matched_disease_indices = []
else:
if isinstance(clusters, torch.Tensor):
clusters_np = clusters.numpy()
else:
clusters_np = clusters
print(f" Clusters shape: {clusters_np.shape if hasattr(clusters_np, 'shape') else len(clusters_np)}")
print(f" Unique signatures: {np.unique(clusters_np) if hasattr(clusters_np, 'shape') else 'N/A'}")
sig_disease_indices = np.where(clusters_np == sig_idx)[0]
print(f" Diseases in signature {sig_idx}: {len(sig_disease_indices)}")
# Match diseases by name with UKB malignancy diseases
matched_disease_indices = []
matched_names = []
for ukb_name in ukb_malignancy_names:
# Find this disease in the current cohort's signature
for d_idx in sig_disease_indices:
if d_idx < len(disease_names_list):
cohort_disease_name = str(disease_names_list[d_idx])
# Fuzzy matching: check if names are similar
if (ukb_name.lower() in cohort_disease_name.lower() or
cohort_disease_name.lower() in ukb_name.lower() or
ukb_name.lower().replace(' ', '') == cohort_disease_name.lower().replace(' ', '')):
if d_idx not in matched_disease_indices:
matched_disease_indices.append(d_idx)
matched_names.append(cohort_disease_name)
break
print(f" Matched {len(matched_disease_indices)} diseases with UKB:")
for i, (idx, name) in enumerate(zip(matched_disease_indices[:10], matched_names[:10])):
print(f" {i}: {name} (idx {idx})")
# Plot matched diseases (or all if no matches found)
diseases_to_plot = matched_disease_indices if len(matched_disease_indices) > 0 else sig_disease_indices[:7]
if len(diseases_to_plot) > 0:
# Get disease names and plot
for i, d_idx in enumerate(diseases_to_plot):
if d_idx < phi_data.shape[1]:
phi_values = phi_data[sig_idx, d_idx, :]
# Ensure age_points matches phi_values length
if len(phi_values) != len(age_points):
age_points_adj = np.arange(30, 30 + len(phi_values))
else:
age_points_adj = age_points
disease_name = disease_names_list[d_idx] if d_idx < len(disease_names_list) else f"Disease {d_idx}"
ax.plot(age_points_adj, phi_values, label=disease_name,
color=malignancy_colors[i % len(malignancy_colors)], linewidth=2)
else:
print(f" WARNING: No diseases found in {cohort} signature {sig_idx}!")
ax.text(0.5, 0.5, f'No diseases in signature {sig_idx}',
transform=ax.transAxes, ha='center', va='center', fontsize=12)
ax.set_xlabel('Age (yr)', fontsize=10)
ax.set_ylabel('Log hazard ratio', fontsize=10)
ax.set_title(f'{cohort.upper()}', fontsize=12, fontweight='bold')
ax.grid(True, alpha=0.3)
ax.set_ylim(-16, -4)
ax.set_xlim(30, 80)
if row_idx == 0:
ax.set_title('Malignancy signatures', fontsize=12, fontweight='bold')
if row_idx == 2 and len(sig_disease_indices) > 0:
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8)
plt.suptitle('Panel E: Cross-Cohort Comparison', fontsize=14, fontweight='bold', y=0.995)
plt.tight_layout()
plt.show()
Calculate Empirical Hazards from MGB/AOU ICD10 Data¶
Calculate empirical hazards from raw ICD10 data to compare with model's learned phi values. This addresses the competing risk concern by showing the model captures empirical patterns.
Plot Signature Patterns for All Three Cohorts (UKB, AOU, MGB)¶
Create signature plots showing phi values for all signatures (0-14) for each cohort. Each plot shows diseases in the signature (colored) vs background diseases (grey).
# Define plotting functions
from scipy.special import expit as sigmoid
import sys
sys.path.append('/Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/main_paper_figures')
from plot_signature_patterns import plot_signature_phi_patterns
print("Plotting functions loaded!")
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/plot_ukb_sigs.py
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/plot_aou_all_signatures.py
import torch
mgb_checkpoint = torch.load('/Users/sarahurbut/aladynoulli2/mgb_model_initialized.pt', map_location='cpu')
if 'model_state_dict' in mgb_checkpoint and 'psi' in mgb_checkpoint['model_state_dict']:
psi = mgb_checkpoint['model_state_dict']['psi']
print(f"Psi range: [{psi.min():.3f}, {psi.max():.3f}]")
print(f"Psi mean: {psi.mean():.3f}")
# Check if values are in initialized range (-2 to +1) or trained range
if psi.max() > 2.0:
print("✓ Looks like TRAINED psi (values > 2)")
else:
print("⚠ Looks like INITIALIZED psi (values in -2 to +1 range)")
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/plot_mgb_all_signatures.py
Panel B & C: PSI Heatmap and Disease Prevalence by Age (Corrected E)¶
Create two plots:
- Panel B: PSI heatmap showing disease-signature associations (log odds ratios)
- Panel C: Disease prevalence by age, clustered by signature (average pi over people)
These plots demonstrate that we no longer have censoring bias with the corrected E approach.
import torch
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.special import expit as sigmoid
import glob
from pathlib import Path
# Load PSI from one batch (or average from 40 batches)
batch_pattern = '/Users/sarahurbut/Library/CloudStorage/Dropbox/censor_e_batchrun_vectorized/enrollment_model_W0.0001_batch_*_*.pt'
batch_files = sorted(glob.glob(batch_pattern))
print(f"Found {len(batch_files)} batch files")
# Option 1: Use first batch
if len(batch_files) > 0:
batch_checkpoint = torch.load(batch_files[0], map_location='cpu', weights_only=False)
psi = batch_checkpoint['model_state_dict']['psi']
if torch.is_tensor(psi):
psi = psi.detach().cpu().numpy()
print(f"Loaded PSI from first batch: {batch_files[0]}")
print(f"PSI shape: {psi.shape}")
# Option 2: Average across all batches (uncomment to use)
all_psis = []
for batch_file in batch_files:
try:
checkpoint = torch.load(batch_file, map_location='cpu', weights_only=False)
psi_batch = checkpoint['model_state_dict']['psi']
if torch.is_tensor(psi_batch):
psi_batch = psi_batch.detach().cpu().numpy()
all_psis.append(psi_batch)
except:
continue
psi = np.mean(np.stack(all_psis, axis=0), axis=0)
print(f"Averaged PSI across {len(all_psis)} batches")
else:
raise FileNotFoundError("No batch files found!")
# Load clusters and disease names
ukb_checkpoint_ref = torch.load('/Users/sarahurbut/Dropbox-Personal/model_with_kappa_bigam.pt', map_location='cpu')
clusters = ukb_checkpoint_ref['clusters']
if torch.is_tensor(clusters):
clusters = clusters.detach().cpu().numpy()
disease_names = ukb_checkpoint_ref['disease_names']
if isinstance(disease_names, (list, tuple)):
disease_names = list(disease_names)
elif hasattr(disease_names, 'values'):
disease_names = disease_names.values.tolist()
print(f"Clusters shape: {clusters.shape}")
print(f"Number of diseases: {len(disease_names)}")
print(f"Number of signatures: {psi.shape[0]}")
# Load pi tensor and average over people
pi_path = '/Users/sarahurbut/Library/CloudStorage/Dropbox/enrollment_predictions_fixedphi_correctedE_vectorized/pi_enroll_fixedphi_sex_FULL.pt'
print(f"Loading pi tensor from: {pi_path}")
pi_full = torch.load(pi_path, map_location='cpu', weights_only=False)
if torch.is_tensor(pi_full):
pi_full = pi_full.detach().cpu().numpy()
print(f"Pi tensor shape: {pi_full.shape}") # Should be (N, D, T)
# Average over people (patients) to get (D, T) - average probability for each disease at each age
pi_avg = np.mean(pi_full, axis=0) # Shape: (D, T)
print(f"Average pi shape: {pi_avg.shape}") # Should be (D, T)
# Get age points (assuming age_offset=30, T=52 timepoints)
T = pi_avg.shape[1]
age_points = np.arange(30, 30 + T)
print(f"Age range: {age_points[0]}-{age_points[-1]}")
# Create Panel B: PSI Heatmap (Disease Index vs Signatures)
# PSI shape: (K, D) where K=21 signatures, D=348 diseases
# We want to show: diseases (rows) vs signatures (columns)
# Signature labels for grouping
SIGNATURE_LABELS = {
0: 'Cardiac Arrhythmias',
1: 'Musculoskeletal',
2: 'Upper GI/Esophageal',
3: 'Mixed/General Medical',
4: 'Upper Respiratory',
5: 'Ischemic cardiovascular',
6: 'Metastatic Cancer',
7: 'Pain/Inflammation',
8: 'Gynecologic',
9: 'Spinal Disorders',
10: 'Ophthalmologic',
11: 'Cerebrovascular',
12: 'Renal/Urologic',
13: 'Male Urogenital',
14: 'Pulmonary/Smoking',
15: 'Metabolic/Diabetes',
16: 'Infectious/Critical Care',
17: 'Lower GI/Colon',
18: 'Hepatobiliary',
19: 'Dermatologic/Oncologic',
20: 'Health'
}
# Transpose PSI to get (D, K) for heatmap
psi_heatmap = psi.T # Shape: (D, K)
# Create figure for PSI heatmap only
# Increased height to accommodate all disease names
fig1, ax1 = plt.subplots(1, 1, figsize=(14, 24))
# Create heatmap
# Note: We'll show a subset of diseases (top diseases per signature) for clarity
# Or show all diseases but cluster them by signature
# Reorder diseases by signature (cluster assignment), then by max psi within each signature
# Group diseases by signature
sig_to_diseases = {}
for d in range(len(clusters)):
sig = clusters[d]
if sig not in sig_to_diseases:
sig_to_diseases[sig] = []
sig_to_diseases[sig].append(d)
# Sort diseases within each signature by their psi value in that signature (most positive first)
# Note: psi is already averaged across batches, so we use psi[sig, d] for disease d in signature sig
disease_order = []
for sig in sorted(sig_to_diseases.keys()):
diseases_in_sig = sig_to_diseases[sig]
# Get psi value for each disease in its assigned signature
sig_psi_values = [psi[sig, d] for d in diseases_in_sig]
# Sort by psi value in this signature (descending - most positive first)
sorted_indices = sorted(range(len(diseases_in_sig)),
key=lambda i: sig_psi_values[i], reverse=True)
disease_order.extend([diseases_in_sig[i] for i in sorted_indices])
# Reorder PSI and disease names
psi_ordered = psi_heatmap[disease_order, :]
disease_names_ordered = [disease_names[i] for i in disease_order]
# Create heatmap with improved aesthetics
im = ax1.imshow(psi_ordered, aspect='auto', cmap='RdBu_r',
vmin=-5, vmax=3, interpolation='nearest')
# Set labels with better formatting
ax1.set_xlabel('Signatures', fontsize=16, fontweight='bold')
ax1.set_ylabel('Disease index (psi kd)', fontsize=16, fontweight='bold')
ax1.set_title('Panel B: Disease-Signature Associations (Log Odds Ratio)',
fontsize=18, fontweight='bold', pad=25)
# Set x-axis ticks (signatures 0-20)
ax1.set_xticks(np.arange(psi.shape[0]))
ax1.set_xticklabels([f'{i}' for i in range(psi.shape[0])], fontsize=11, fontweight='bold')
# Set y-axis ticks (show all disease names - will be hard to read but comprehensive)
n_diseases = len(disease_order)
ax1.set_yticks(np.arange(n_diseases))
ax1.set_yticklabels([disease_names_ordered[i] for i in range(n_diseases)],
fontsize=6) # Small font size to fit all names
# Add colorbar with better formatting
cbar = fig1.colorbar(im, ax=ax1, label='Log odds ratio (psi)', pad=0.02, shrink=0.8)
cbar.ax.tick_params(labelsize=12)
cbar.set_label('Log odds ratio (psi)', fontsize=14, fontweight='bold')
# Add vertical lines to separate signatures
for k in range(psi.shape[0] + 1):
ax1.axvline(k - 0.5, color='white', linewidth=1.5, alpha=0.8)
# Add horizontal lines to separate signature groups (diseases grouped by signature)
# Also add signature labels as brackets on the right side
from matplotlib.patches import FancyBboxPatch
import matplotlib.patches as mpatches
current_y = 0
sig_y_positions = {} # Store y positions for each signature group
for sig in sorted(sig_to_diseases.keys()):
n_diseases_in_sig = len(sig_to_diseases[sig])
if current_y > 0:
ax1.axhline(current_y - 0.5, color='white', linewidth=1.5, alpha=0.8)
# Store the middle y position for this signature group
sig_y_positions[sig] = current_y + n_diseases_in_sig / 2 - 0.5
current_y += n_diseases_in_sig
# Add signature labels on the right side with brackets
ax2 = ax1.twinx() # Create a second y-axis for labels
ax2.set_ylim(ax1.get_ylim())
ax2.set_yticks([])
ax2.set_yticklabels([])
# Get x position for labels (just to the right of the heatmap)
x_label_pos = psi.shape[0] + 0.5
# Add brackets and labels for each signature group
for sig in sorted(sig_to_diseases.keys()):
n_diseases_in_sig = len(sig_to_diseases[sig])
sig_start_y = sum(len(sig_to_diseases[s]) for s in sorted(sig_to_diseases.keys()) if s < sig) - 0.5
sig_end_y = sig_start_y + n_diseases_in_sig
# Get label text
label_text = SIGNATURE_LABELS.get(sig, f'Signature {sig}')
# Draw bracket (curly brace)
# Use annotation with curly brace style
y_mid = (sig_start_y + sig_end_y) / 2
# Draw bracket using FancyBboxPatch or annotation
# Create a bracket shape using lines
bracket_width = 0.3
bracket_x_start = x_label_pos - bracket_width
bracket_x_end = x_label_pos
# Top of bracket
ax2.plot([bracket_x_start, bracket_x_end], [sig_start_y, sig_start_y],
'k-', linewidth=1.5, clip_on=False)
# Bottom of bracket
ax2.plot([bracket_x_start, bracket_x_end], [sig_end_y, sig_end_y],
'k-', linewidth=1.5, clip_on=False)
# Vertical line
ax2.plot([bracket_x_start, bracket_x_start], [sig_start_y, sig_end_y],
'k-', linewidth=1.5, clip_on=False)
# Add label text
ax2.text(x_label_pos + 0.1, y_mid, label_text,
fontsize=9, va='center', ha='left',
rotation=0, fontweight='bold',
clip_on=False)
# Adjust x-axis limits to accommodate labels
ax1.set_xlim(-0.5, x_label_pos + 3)
ax2.set_xlim(-0.5, x_label_pos + 3)
plt.tight_layout()
# Display the figure
from IPython.display import display
#display(fig1)
plt.show()
print("✓ Panel B: PSI heatmap created")
from generate_psi_heatmap import plot_psi_heatmap_with_arrows
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/main_paper_figures/generate_psi_heatmap.py
%run /Users/sarahurbut/aladynoulli2/pyScripts/dec_6_revision/new_notebooks/main_paper_figures/generate_pi_heatmap2.py