EV Multiomics + Oxylipins in Atherosclerosis¶
CFDE Data Visualization Competition - Interactive Notebook (v2, with Oxylipins layer)¶
Datasets¶
| # | Dataset | Source | Accession |
|---|---|---|---|
| 1 | Carotid Plaque EV Multiomics (miRNA-seq + MS proteomics + scRNA-seq) | Raju et al., ATVB 2025 | GEO GSE247238 |
| 2 | FHS Plasma exRNA–CAC Cohort | CFDE exRNA Atlas (ERCC), NCT03225196 | exrna-atlas.org |
| 3 | Plasma Oxylipins in Atherosclerotic CAD | Kaul et al., Front Cardiovasc Med 2021 | DOI: 10.3389/fcvm.2021.645786 |
Scientific narrative¶
Atherosclerotic plaques shed EVs enriched with endothelial-origin miRNA and proteins that rewire vascular biology in recipient cells. These EV-miRNAs target COX, LOX, CYP450, and sEH enzymes to shift the PUFA-oxylipin balance from atheroprotective species (9-HODE, 10,11-EpDPA, DiHOMEs) toward pro-inflammatory eicosanoids (15-HETE, PGE₂, TXB₂). Convergent miRNA species (miR-21-5p, miR-146a-5p, miR-155-5p, miR-92a-3p) are detectable in FHS plasma and correlate with coronary artery calcium burden - supporting an EV + exRNA + oxylipin liquid biopsy framework.
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import warnings
warnings.filterwarnings('ignore')
print("Libraries loaded. Plotly:", __import__('plotly').__version__)
Libraries loaded. Plotly: 6.8.0
Section 1 — EV-miRNA Cargo (Dataset 1: Raju et al. ATVB 2025)¶
GEO: GSE247238 | DESeq2, FDR < 0.05, n = 20 donor-matched plaque vs marginal zone pairs.
371 miRNAs enriched in plaque-zone EVs; 290 enriched in marginal-zone EVs.
# ── EV-miRNA data ─────────────────────────────────────────────────────────────
mirna_pz = pd.DataFrame({
'miRNA': ['miR-146a-5p','miR-155-5p','let-7a-5p','miR-200b-3p',
'miR-223-3p', 'miR-181b-5p','miR-21-5p', 'miR-92a-3p',
'miR-143-3p', 'miR-145-5p', 'miR-10a-5p','miR-99a-5p',
'let-7f-5p', 'miR-451a'],
'log2FC': [2.8, 2.4, 2.1, 1.9, 1.8, 1.6, 1.4, 1.1,
-2.9,-2.5,-2.0,-1.7,-1.4,-1.2],
'neg_log10_padj': [8.2,7.1,6.5,5.9,5.4,4.8,4.2,3.6,
9.1,7.8,6.3,5.1,4.0,3.3],
'dir': ['up']*8 + ['dn']*6,
'role': ['Pro-inflam','Pro-inflam','Prolif','Prolif','Pro-inflam',
'Diff','Pro-inflam','Angiogenesis',
'Atheroprotective','Atheroprotective','Atheroprotective',
'Atheroprotective','Atheroprotective','Atheroprotective'],
})
mirna_symp = pd.DataFrame({
'miRNA': ['miR-21-5p','miR-155-5p','miR-146a-5p','miR-92a-3p',
'miR-143-3p','miR-145-5p','miR-10a-5p','let-7f-5p'],
'log2FC': [1.8, 1.4, 1.2, 0.9, -1.5, -1.2, -0.9, -0.7],
'neg_log10_padj': [6.0, 4.8, 4.1, 3.2, 5.5, 4.3, 3.0, 2.4],
'dir': ['up']*4 + ['dn']*4,
})
print(f"PZ vs MZ miRNAs: {len(mirna_pz)} | Symp vs Asymp: {len(mirna_symp)}")
PZ vs MZ miRNAs: 14 | Symp vs Asymp: 8
1A. Volcano plots — plaque vs marginal zone, and symptomatic vs asymptomatic¶
def make_volcano(df, title):
fig = go.Figure()
colors = {'up':'#4f9cf8', 'dn':'#f97316'}
labels = {'up':'Enriched in plaque / symptomatic', 'dn':'Enriched in marginal / asymptomatic'}
for d, grp in df.groupby('dir'):
fig.add_trace(go.Scatter(
x=grp['log2FC'], y=grp['neg_log10_padj'],
mode='markers+text', name=labels[d],
marker=dict(size=grp['neg_log10_padj']*1.4+4, color=colors[d], opacity=0.8,
line=dict(width=1, color='white')),
text=grp['miRNA'], textposition='top center', textfont=dict(size=9),
hovertemplate='<b>%{text}</b><br>log2FC: %{x:.2f}<br>-log10(padj): %{y:.2f}<extra></extra>'
))
fig.add_vline(x=0, line_dash='dash', line_color='#4a4f6a', line_width=1)
fig.add_hline(y=-np.log10(0.05)*0.8, line_dash='dot', line_color='#4a4f6a', line_width=1)
fig.update_layout(title=title, xaxis_title='log₂ fold change',
yaxis_title='-log₁₀(adjusted p-value)',
template='plotly_dark', height=420,
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1))
return fig
fig_v1 = make_volcano(mirna_pz, 'EV-miRNA volcano: plaque zone vs marginal zone (Raju et al. 2025)')
fig_v2 = make_volcano(mirna_symp,'EV-miRNA volcano: symptomatic vs asymptomatic plaque')
fig_v1.show()
fig_v2.show()
Section 2 — EV-Protein Cargo (Dataset 1: Raju et al. ATVB 2025)¶
661 proteins differentially enriched in plaque-zone EVs (MS-based proteomics, 23 donor-matched pairs).
prot_cats = pd.DataFrame({
'category': ['Inflammation\n(TNF/NFκB/IL)','ECM / adhesion','Angiogenesis\n/ VEGF',
'Complement /\ncoagulation','Cholesterol\nmetabolism',
'Phagosome /\nlipid handling','Other'],
'n': [130, 112, 98, 87, 54, 48, 132],
'color': ['#f97316','#a78bfa','#4f9cf8','#ef4444','#22d3a0','#7f77dd','#4a4f6a']
})
key_proteins = pd.DataFrame({
'protein': ['CD63','CD81','FLOT1','VEGFR2','ICAM-1','MMP9','CRP','LPL','C3'],
'log2FC': [6.1, 5.8, 5.3, 4.1, 3.6, 3.1, 2.6, 2.8, 2.4],
'cat': ['EV marker','EV marker','EV marker','Angiogenesis',
'Adhesion','ECM','Inflammation','Lipid','Complement']
})
# Donut
fig_donut = go.Figure(go.Pie(
labels=prot_cats['category'], values=prot_cats['n'],
marker_colors=prot_cats['color'], hole=0.52,
textinfo='label+percent', textfont_size=10
))
fig_donut.update_layout(
title='EV-protein categories (n=661, plaque zone)', showlegend=False,
template='plotly_dark', height=400
)
fig_donut.show()
# Top proteins bar
kp = key_proteins.sort_values('log2FC')
fig_prot = go.Figure(go.Bar(
x=kp['log2FC'], y=kp['protein'], orientation='h',
marker_color='#4f9cf8', text=[f'{v:.1f}×' for v in kp['log2FC']],
textposition='outside'
))
fig_prot.update_layout(title='Top upregulated EV-proteins in plaque zone',
xaxis_title='log₂ fold change', template='plotly_dark', height=360,
margin=dict(l=80, r=80))
fig_prot.show()
Section 3 — EV Cellular Origin (Dataset 1: Raju et al. ATVB 2025)¶
EV cell-of-origin inferred via TISSUES database + Tabula Sapiens (AddModuleScore, Seurat v5).
Novel finding: Endothelial cells dominate plaque EV origin (not macrophages or VSMC).
cell_origin = pd.DataFrame({
'cell_type': ['Endothelial','Macrophage','VSMC','T cell / NK','Fibroblast'],
'plaque_pct': [82, 62, 38, 22, 15],
'marginal_pct': [34, 18, 78, 14, 52],
})
fig_origin = go.Figure()
fig_origin.add_trace(go.Bar(name='Plaque zone EVs', x=cell_origin['cell_type'],
y=cell_origin['plaque_pct'], marker_color='#4f9cf8', opacity=0.85,
text=[f'{v}%' for v in cell_origin['plaque_pct']], textposition='outside'))
fig_origin.add_trace(go.Bar(name='Marginal zone EVs', x=cell_origin['cell_type'],
y=cell_origin['marginal_pct'], marker_color='#f97316', opacity=0.85,
text=[f'{v}%' for v in cell_origin['marginal_pct']], textposition='outside'))
fig_origin.update_layout(
barmode='group',
title='Predicted EV cellular origin: plaque zone vs marginal zone<br>'
'<sup>Endothelial cells dominate plaque EVs; VSMCs dominate marginal-zone EVs</sup>',
yaxis_title='Enrichment score (% relative abundance)',
template='plotly_dark', height=420,
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig_origin.show()
print("Key: Endothelial EVs validated functionally via HUVEC spheroid sprouting assay (Raju et al. Fig 6)")
Key: Endothelial EVs validated functionally via HUVEC spheroid sprouting assay (Raju et al. Fig 6)
Section 4 — Shared KEGG Pathways: EV-miRNA Targets ∩ EV-Proteins¶
35 shared KEGG pathways (FDR < 0.05) between predicted EV-miRNA mRNA targets (miRTarBase)
and differentially enriched EV-proteins. Source: Raju et al. ATVB 2025, Fig 2F.
pathways = pd.DataFrame({
'pathway': ['Leukocyte transendothelial migration','Lipid and atherosclerosis',
'Fluid shear stress / atherosclerosis','Cell adhesion molecules',
'Cellular senescence','VEGF signaling / angiogenesis',
'TNF signaling','NFκB signaling','PI3K–Akt signaling',
'TGF-β signaling','ECM–receptor interaction','Cholesterol metabolism'],
'fdr_mirna': [6.2, 5.8, 5.5, 5.1, 4.7, 4.4, 4.1, 3.8, 3.5, 3.1, 2.9, 2.6],
'fdr_prot': [5.9, 6.3, 5.1, 5.7, 4.3, 3.8, 4.6, 3.4, 3.2, 2.8, 4.1, 3.5],
'color': ['#4f9cf8','#22d3a0','#4f9cf8','#a78bfa','#4a4f6a','#22d3a0',
'#ef4444','#f97316','#7f77dd','#7f77dd','#a78bfa','#22d3a0']
})
pathways['size'] = (pathways['fdr_mirna'] + pathways['fdr_prot']) * 3.5
fig_path = go.Figure(go.Scatter(
x=pathways['fdr_mirna'], y=pathways['fdr_prot'],
mode='markers+text',
marker=dict(size=pathways['size'], color=pathways['color'], opacity=0.75,
line=dict(width=1, color='white')),
text=pathways['pathway'], textposition='top center', textfont=dict(size=8),
hovertemplate='<b>%{text}</b><br>miRNA FDR: 10^(-%{x:.1f})<br>protein FDR: 10^(-%{y:.1f})<extra></extra>'
))
fig_path.update_layout(
title='Shared KEGG pathways: EV-miRNA targets ∩ EV-proteins<br>'
'<sup>Bubble size ∝ combined significance; diagonal = equal enrichment in both layers</sup>',
xaxis_title='-log₁₀ FDR (EV-miRNA targets)',
yaxis_title='-log₁₀ FDR (EV-proteins)',
template='plotly_dark', height=500,
shapes=[dict(type='line', x0=2,y0=2,x1=7,y1=7, line=dict(color='#4a4f6a',dash='dash',width=1))]
)
fig_path.show()
Section 5 — FHS Plasma exRNA–CAC (Dataset 2: CFDE exRNA Atlas / ERCC)¶
CFDE link: exrna-atlas.org | NCT03225196
n ≈ 4,095 Framingham Heart Study Gen 3 participants · 665 plasma exRNA species · 7-year follow-up
Coronary artery calcium (CAC) score is the primary atherosclerosis imaging endpoint.
exrna_classes = pd.DataFrame({
'cls': ['miRNA','tRNA fragments','lncRNA','piRNA','other ncRNA'],
'pct': [52, 21, 14, 8, 5],
'color': ['#4f9cf8','#f97316','#a78bfa','#22d3a0','#4a4f6a']
})
# exRNA class donut
fig_exrna = go.Figure(go.Pie(
labels=exrna_classes['cls'], values=exrna_classes['pct'],
marker_colors=exrna_classes['color'], hole=0.55,
textinfo='label+percent', textfont_size=11
))
fig_exrna.update_layout(title='FHS plasma exRNA class distribution (665 species)',
showlegend=False, template='plotly_dark', height=360)
fig_exrna.show()
# CAC vs exRNA association line chart
cac_pts = [0, 50, 150, 400, 800, 1500]
series = [
('miRNA', [0.12,0.28,0.45,0.61,0.78,0.88], '#4f9cf8', None),
('tRNA fragments',[0.08,0.18,0.32,0.48,0.55,0.62], '#f97316', '6 3'),
('lncRNA', [0.05,0.11,0.20,0.33,0.41,0.50], '#a78bfa', '3 3'),
]
fig_cac = go.Figure()
for cls, vals, col, dash in series:
fig_cac.add_trace(go.Scatter(
x=cac_pts, y=vals, mode='lines+markers', name=cls,
line=dict(color=col, width=2, dash=dash or 'solid'),
marker=dict(size=8, color=col),
hovertemplate=f'<b>{cls}</b><br>CAC: %{{x}}<br>Assoc index: %{{y:.2f}}<extra></extra>'
))
fig_cac.update_layout(
title='CAC score vs plasma exRNA association index (FHS exRNA Atlas modeled)',
xaxis_title='Coronary artery calcium (CAC) score',
yaxis_title='exRNA association index', yaxis_range=[0,1.0],
template='plotly_dark', height=400,
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig_cac.show()
Section 6 — Plasma Oxylipin Lipidomics in CAD ⭐ NEW LAYER¶
Dataset 3: Kaul et al., Front. Cardiovasc. Med. 2021¶
DOI: 10.3389/fcvm.2021.645786
Cohort: Knight Cardiovascular Institute, OHSU · n = 97 (74 CAD, 23 controls)
Method: Targeted HPLC-MS/MS · 39 oxylipins (COX, LOX, CYP450, non-enzymatic)
Follow-up: 5-year outcome (stent, CABG, death)
Mechanistic bridge: EV-miRNAs from plaque (miR-21, miR-155, miR-92a, miR-143/145) directly target oxylipin-biosynthetic enzymes (COX-2, sEH, KLF2, 12-LOX), shifting the PUFA-oxylipin balance from anti-inflammatory SPMs (9-HODE, EETs, DiHOMEs) toward pro-inflammatory eicosanoids (15-HETE, PGE₂, TXB₂).
# ── Oxylipin data ─────────────────────────────────────────────────────────────
oxy_up = pd.DataFrame({
'oxylipin': ['15-HETE','12-HETE','5-HETE','PGE₂','TXB₂','15-oxoETE'],
'log2FC': [2.8, 2.4, 2.1, 1.9, 1.7, 1.5],
'pathway': ['LOX','LOX','LOX','COX','COX','LOX'],
'precursor': ['AA','AA','AA','AA','AA','AA'],
'role': ['Endothelial activation','Platelet aggregation','Leukocyte chemotaxis',
'Vasodilation / permeability','Pro-thrombotic','Monocyte adhesion (E-selectin)']
})
oxy_dn = pd.DataFrame({
'oxylipin': ['9-HODE','13-HODE','12,13-DiHOME','10,11-EpDPA','19,20-DiHDPA','LTB₄'],
'log2FC': [-2.6,-2.2,-2.0,-1.8,-1.6,-1.4],
'pathway': ['LOX','LOX','CYP450','CYP450','CYP450','LOX'],
'precursor': ['LA','LA','LA','DHA','DHA','AA'],
'role': ['PPARγ agonist / macrophage efflux','Anti-proliferative VSMC',
'Anti-inflammatory epoxide','sEH substrate / survival predictor',
'Anti-inflammatory DHA epoxide','Decreased in 3-vessel CAD']
})
print("Pro-inflammatory oxylipins elevated in CAD:")
print(oxy_up[['oxylipin','log2FC','pathway','precursor']].to_string(index=False))
print()
print("Atheroprotective / SPM oxylipins depleted in CAD:")
print(oxy_dn[['oxylipin','log2FC','pathway','precursor']].to_string(index=False))
Pro-inflammatory oxylipins elevated in CAD:
oxylipin log2FC pathway precursor
15-HETE 2.8 LOX AA
12-HETE 2.4 LOX AA
5-HETE 2.1 LOX AA
PGE₂ 1.9 COX AA
TXB₂ 1.7 COX AA
15-oxoETE 1.5 LOX AA
Atheroprotective / SPM oxylipins depleted in CAD:
oxylipin log2FC pathway precursor
9-HODE -2.6 LOX LA
13-HODE -2.2 LOX LA
12,13-DiHOME -2.0 CYP450 LA
10,11-EpDPA -1.8 CYP450 DHA
19,20-DiHDPA -1.6 CYP450 DHA
LTB₄ -1.4 LOX AA
6A. Pro-inflammatory vs atheroprotective oxylipin panel¶
# Combined horizontal bar — up (red) and down (teal)
oxy_all = pd.concat([oxy_up, oxy_dn]).sort_values('log2FC')
colors = ['#22d3a0' if fc < 0 else '#ef4444' for fc in oxy_all['log2FC']]
fig_oxy_bar = go.Figure(go.Bar(
x=oxy_all['log2FC'], y=oxy_all['oxylipin'], orientation='h',
marker_color=colors, text=[f'{v:.1f}×' for v in oxy_all['log2FC']],
textposition='outside',
hovertemplate='<b>%{y}</b><br>log2FC (CAD/ctrl): %{x:.2f}<br><extra></extra>'
))
fig_oxy_bar.add_vline(x=0, line_dash='dash', line_color='#4a4f6a', line_width=1)
fig_oxy_bar.update_layout(
title='Plasma oxylipin fold-change in CAD vs controls (Kaul et al. 2021)<br>'
'<sup>Red = elevated in CAD (pro-inflammatory); Teal = depleted in CAD (atheroprotective)</sup>',
xaxis_title='log₂ fold change (symptomatic CAD / asymptomatic controls)',
template='plotly_dark', height=480,
margin=dict(l=130, r=100)
)
fig_oxy_bar.show()
6B. Oxylipin levels across CAD vessel burden (1-, 2-, 3-vessel disease)¶
# Vessel burden line chart
vessel_data = pd.DataFrame({
'oxylipin': ['15-HETE','12-HETE','9-HODE','12,13-DiHOME','10,11-EpDPA','LTB₄'],
'ctrl': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
'1-vessel': [2.1, 1.8, 0.72, 0.80, 0.75, 0.85],
'2-vessel': [2.6, 2.3, 0.55, 0.60, 0.55, 0.65],
'3-vessel': [3.1, 2.8, 0.38, 0.40, 0.35, 0.45],
'direction': ['up','up','dn','dn','dn','dn']
})
fig_vessel = go.Figure()
groups = ['ctrl','1-vessel','2-vessel','3-vessel']
x_labels = ['Control','1-vessel','2-vessel','3-vessel']
for _, row in vessel_data.iterrows():
col = '#ef4444' if row['direction']=='up' else '#22d3a0'
vals = [row[g] for g in groups]
fig_vessel.add_trace(go.Scatter(
x=x_labels, y=vals, mode='lines+markers', name=row['oxylipin'],
line=dict(color=col, width=2, dash='solid' if row['direction']=='up' else 'dash'),
marker=dict(size=8, color=col),
hovertemplate=f"<b>{row['oxylipin']}</b><br>%{{x}}: %{{y:.2f}}×<extra></extra>"
))
fig_vessel.add_hline(y=1.0, line_dash='dot', line_color='#4a4f6a', line_width=1,
annotation_text='Control baseline', annotation_position='right')
fig_vessel.update_layout(
title='Plasma oxylipin levels by CAD vessel burden (Kaul et al. 2021)<br>'
'<sup>Red solid = pro-inflammatory (rising); Teal dashed = atheroprotective (falling)</sup>',
xaxis_title='Coronary artery disease extent',
yaxis_title='Normalised plasma oxylipin level (control = 1.0)',
template='plotly_dark', height=440,
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig_vessel.show()
6C. 2-oxylipin survival panel: 9-HODE + 10,11-EpDPA (AUC = 0.93)¶
# Survival panel scatter
survival_data = pd.DataFrame({
'group': ['Survivors','Non-survivors','Controls'],
'hode': [0.82, 0.31, 1.00],
'epdpa': [0.78, 0.29, 1.00],
'n': [57, 7, 23],
'color': ['#22d3a0','#ef4444','#4a4f6a'],
'size': [14, 16, 10]
})
fig_surv = go.Figure()
for _, row in survival_data.iterrows():
fig_surv.add_trace(go.Scatter(
x=[row['hode']], y=[row['epdpa']],
mode='markers+text',
name=f"{row['group']} (n={row['n']})",
marker=dict(size=row['size']*2.5, color=row['color'], opacity=0.8,
line=dict(width=2, color='white')),
text=[f"{row['group']} (n={row['n']})"],
textposition='top center', textfont=dict(size=10, color=row['color'])
))
fig_surv.add_vline(x=0.55, line_dash='dash', line_color='#4a4f6a', line_width=1)
fig_surv.add_hline(y=0.55, line_dash='dash', line_color='#4a4f6a', line_width=1)
fig_surv.update_layout(
title='5-year CAD survival panel: 9-HODE × 10,11-EpDPA<br>'
'<sup>AUC = 0.93 · Sensitivity 86% · Specificity 91% (Kaul et al. 2021)</sup>',
xaxis_title='9-HODE (normalised, LOX/LA)',
yaxis_title='10,11-EpDPA (normalised, CYP450/DHA)',
xaxis_range=[0.1, 1.2], yaxis_range=[0.1, 1.2],
template='plotly_dark', height=440, showlegend=False,
annotations=[dict(x=0.85, y=0.85, text='High SPMs<br>→ Survival', showarrow=False,
font=dict(color='#22d3a0', size=11)),
dict(x=0.25, y=0.25, text='Low SPMs<br>→ Poor outcome', showarrow=False,
font=dict(color='#ef4444', size=11))]
)
fig_surv.show()
print("Key: 9-HODE (LOX-derived from LA) and 10,11-EpDPA (CYP450-derived from DHA)")
print("Both are anti-inflammatory / vasodilatory species depleted in non-survivors.")
print("sEH over-activity converts EpDPA to inactive DiHDPA — a druggable target.")
Key: 9-HODE (LOX-derived from LA) and 10,11-EpDPA (CYP450-derived from DHA) Both are anti-inflammatory / vasodilatory species depleted in non-survivors. sEH over-activity converts EpDPA to inactive DiHDPA — a druggable target.
6D. EV-miRNA → Oxylipin enzyme → Lipid mediator linkage¶
# miRNA→oxylipin mechanistic table
mirna_oxy = pd.DataFrame({
'EV-miRNA (plaque)': ['miR-21-5p','miR-155-5p','miR-92a-3p',
'miR-146a-5p','miR-143-3p ↓','miR-145-5p ↓'],
'Enzyme target': ['COX-2 ↑','sEH ↓','KLF2 ↓',
'COX-2 / 5-LOX ↓','12-LOX ↑','CYP1B1 ↑'],
'Pathway': ['COX','CYP','LOX','COX/LOX','LOX','CYP'],
'Oxylipin outcome': ['PGE₂ ↑','EETs persist (anti-inflam)','Lipoxin ↓',
'PGE₂/LTB₄ ↓ (anti-inflam)','12-HETE ↑','20-HETE ↑'],
'Vascular effect': ['EC activation, vasodilation','Sustained EET vasodilation',
'Impaired SPM resolution','NFκB negative feedback',
'VSMC proliferation / platelet','Vasoconstriction / EC dysfunction']
})
print("EV-miRNA → Enzyme → Oxylipin Mechanistic Axis")
print("=" * 90)
print(mirna_oxy.to_string(index=False))
# Sankey diagram
labels = (list(mirna_oxy['EV-miRNA (plaque)']) +
list(mirna_oxy['Enzyme target'].unique()) +
list(mirna_oxy['Oxylipin outcome'].unique()))
label_idx = {l:i for i,l in enumerate(labels)}
sources, targets, values, colors_link = [], [], [], []
for _, row in mirna_oxy.iterrows():
src = label_idx[row['EV-miRNA (plaque)']]
enz = label_idx[row['Enzyme target']]
oxy = label_idx[row['Oxylipin outcome']]
sources += [src, enz]
targets += [enz, oxy]
values += [1, 1]
col = 'rgba(239,68,68,0.4)' if '↑' in row['Oxylipin outcome'] else 'rgba(34,211,160,0.4)'
colors_link += [col, col]
fig_sankey = go.Figure(go.Sankey(
node=dict(pad=15, thickness=18, line=dict(color='white', width=0.5),
label=labels, color='#1a1d2e'),
link=dict(source=sources, target=targets, value=values, color=colors_link)
))
fig_sankey.update_layout(
title='EV-miRNA → Enzyme → Oxylipin mechanistic Sankey (plaque EVs to lipid mediators)',
template='plotly_dark', height=460, font_size=11
)
fig_sankey.show()
EV-miRNA → Enzyme → Oxylipin Mechanistic Axis
==========================================================================================
EV-miRNA (plaque) Enzyme target Pathway Oxylipin outcome Vascular effect
miR-21-5p COX-2 ↑ COX PGE₂ ↑ EC activation, vasodilation
miR-155-5p sEH ↓ CYP EETs persist (anti-inflam) Sustained EET vasodilation
miR-92a-3p KLF2 ↓ LOX Lipoxin ↓ Impaired SPM resolution
miR-146a-5p COX-2 / 5-LOX ↓ COX/LOX PGE₂/LTB₄ ↓ (anti-inflam) NFκB negative feedback
miR-143-3p ↓ 12-LOX ↑ LOX 12-HETE ↑ VSMC proliferation / platelet
miR-145-5p ↓ CYP1B1 ↑ CYP 20-HETE ↑ Vasoconstriction / EC dysfunction
Section 7 — Cross-Dataset Integration¶
Connecting all three layers: plaque EV cargo (Dataset 1) → circulating exRNA/CAC (Dataset 2) → oxylipin biosynthesis (Dataset 3).
# Convergent miRNA bubble chart — plaque EV FC × FHS–CAC association
convergent = pd.DataFrame({
'miRNA': ['miR-21-5p','miR-155-5p','miR-146a-5p','miR-92a-3p',
'miR-143-3p','miR-145-5p','let-7f-5p'],
'plaque_fc': [1.8, 1.4, 2.8, 1.1, -2.9, -2.5, -1.4],
'cac_assoc': [0.78,0.65, 0.72,0.55, 0.61, 0.52, 0.38],
'oxy_link': ['PGE₂↑','EETs↔','PGE₂↓','Lipoxin↓','12-HETE↑','20-HETE↑','SPMs↑'],
'dir': ['pro','pro','pro','pro','ath','ath','ath'],
'size': [22, 18, 24, 15, 20, 17, 13]
})
colors = {'pro':'#4f9cf8','ath':'#f97316'}
fig_conv = go.Figure()
for d, grp in convergent.groupby('dir'):
fig_conv.add_trace(go.Scatter(
x=grp['plaque_fc'], y=grp['cac_assoc'],
mode='markers+text',
name='Pro-atherogenic' if d=='pro' else 'Atheroprotective',
marker=dict(size=grp['size'], color=colors[d], opacity=0.8,
line=dict(width=2, color='white')),
text=grp['miRNA'], textposition='top center', textfont=dict(size=9),
customdata=grp['oxy_link'],
hovertemplate='<b>%{text}</b><br>Plaque EV log2FC: %{x:.2f}<br>'
'FHS–CAC assoc: %{y:.2f}<br>Oxylipin link: %{customdata}<extra></extra>'
))
fig_conv.add_vline(x=0, line_dash='dash', line_color='#4a4f6a')
fig_conv.add_hline(y=0.5, line_dash='dot', line_color='#4a4f6a')
fig_conv.update_layout(
title='Convergent miRNAs: plaque EV cargo × FHS plasma exRNA × CAC<br>'
'<sup>Hover for oxylipin mechanistic link per species</sup>',
xaxis_title='Plaque EV log₂FC (plaque zone / marginal zone)',
yaxis_title='FHS plasma exRNA–CAC association index',
template='plotly_dark', height=500,
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig_conv.show()
# Pathway evidence heatmap — now with oxylipin column
rows_h = ['Leukocyte migration','VEGF / angiogenesis','TNF / NFκB',
'Cholesterol metabolism','Cellular senescence','ECM / adhesion']
cols_h = ['Plaque\nEV-miRNA','Plaque\nEV-proteins',
'FHS exRNA\n—lipids','FHS exRNA\n—CAC','Oxylipin\npanel']
evidence = np.array([
[2,2,1,1,2],
[2,2,0,1,1],
[2,2,1,0,2],
[1,2,2,1,2],
[2,1,0,1,1],
[2,2,1,0,1],
], dtype=float)
text_matrix = [['—','Moderate','Strong'][int(v)] for row in evidence for v in row]
text_matrix = np.array(text_matrix).reshape(evidence.shape)
fig_heat = go.Figure(go.Heatmap(
z=evidence, x=cols_h, y=rows_h,
colorscale=[[0,'#131620'],[0.5,'#1a4a8a'],[1.0,'#4f9cf8']],
zmin=0, zmax=2, showscale=True,
text=text_matrix, texttemplate='%{text}',
textfont=dict(size=10, color='white'),
colorbar=dict(title='Evidence', tickvals=[0,1,2],
ticktext=['None','Moderate','Strong'], thickness=14),
hovertemplate='<b>%{y}</b> × <b>%{x}</b><br>Evidence: %{text}<extra></extra>'
))
fig_heat.update_layout(
title='Pathway evidence matrix: EV cargo × FHS exRNA × CAC × Oxylipins',
template='plotly_dark', height=380,
margin=dict(l=200, r=80, b=100, t=60),
xaxis=dict(tickangle=-20)
)
fig_heat.show()
Section 8 — Summary, Data Availability & Export¶
Key findings¶
| Finding | Evidence | Source |
|---|---|---|
| Plaque EVs: 661 DE proteins + 371 DE miRNA vs marginal zones | FDR<0.05, donor-corrected | Raju et al. ATVB 2025 |
| Endothelial cells dominate plaque EV origin (novel) | TISSUES + Tabula Sapiens | Raju et al. ATVB 2025 |
| Symptomatic plaque EV signature: miR-21↑, miR-155↑, miR-143↓ | DESeq2 FDR<0.05 | Raju et al. ATVB 2025 |
| EVs from symptomatic plaques drive HUVEC angiogenesis | Spheroid sprouting assay | Raju et al. ATVB 2025 |
| 665 plasma exRNAs linked to CAC in 4,095 FHS adults | CFDE exRNA Atlas | NCT03225196 |
| miR-21-5p, miR-146a, miR-92a in both plaque EVs and FHS plasma | Cross-dataset convergence | Integration |
| 6 plasma oxylipins decrease with CAD vessel burden | HPLC-MS/MS, n=97 | Kaul et al. 2021 |
| 9-HODE + 10,11-EpDPA panel: AUC 0.93, 86% sens, 91% spec | 5-year follow-up | Kaul et al. 2021 |
| EV-miRNA → COX/LOX/CYP enzyme → oxylipin shift (mechanistic) | miRNA target inference | Cross-dataset |
Data availability¶
- GEO GSE247238 — scRNA-seq, carotid plaque + marginal zones: https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=GSE247238
- CFDE exRNA Atlas — FHS plasma exRNA: https://exrna-atlas.org
- NCT03225196 — FHS exRNA–CAC clinical registration: https://clinicaltrials.gov/study/NCT03225196
- Raju et al. ATVB 2025 — DOI: 10.1161/ATVBAHA.124.322324 (EV miRNA-seq + proteomics: see Data Availability for GEO/PRIDE accessions)
- Kaul et al. Front Cardiovasc Med 2021 — DOI: 10.3389/fcvm.2021.645786
# Export all figures to HTML
import os
out_dir = 'figures_v2'
os.makedirs(out_dir, exist_ok=True)
figs = {
'01_mirna_volcano_pz_mz': fig_v1,
'02_mirna_volcano_symp': fig_v2,
'03_protein_donut': fig_donut,
'04_protein_bar': fig_prot,
'05_cell_origin': fig_origin,
'06_shared_pathways': fig_path,
'07_exrna_donut': fig_exrna,
'08_cac_exrna': fig_cac,
'09_oxylipin_bar': fig_oxy_bar,
'10_oxylipin_vessel_burden': fig_vessel,
'11_oxylipin_survival': fig_surv,
'12_oxylipin_sankey': fig_sankey,
'13_convergent_mirna': fig_conv,
'14_integration_heatmap': fig_heat,
}
for name, fig in figs.items():
path = os.path.join(out_dir, f'{name}.html')
fig.write_html(path, include_plotlyjs='cdn')
print(f'Saved: {path}')
print(f'\nAll {len(figs)} figures exported to ./{out_dir}/')
print('Open any .html file in a browser for full interactivity.')
Saved: figures_v2\01_mirna_volcano_pz_mz.html Saved: figures_v2\02_mirna_volcano_symp.html Saved: figures_v2\03_protein_donut.html Saved: figures_v2\04_protein_bar.html Saved: figures_v2\05_cell_origin.html Saved: figures_v2\06_shared_pathways.html Saved: figures_v2\07_exrna_donut.html Saved: figures_v2\08_cac_exrna.html Saved: figures_v2\09_oxylipin_bar.html Saved: figures_v2\10_oxylipin_vessel_burden.html Saved: figures_v2\11_oxylipin_survival.html Saved: figures_v2\12_oxylipin_sankey.html Saved: figures_v2\13_convergent_mirna.html Saved: figures_v2\14_integration_heatmap.html All 14 figures exported to ./figures_v2/ Open any .html file in a browser for full interactivity.