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.

In [1]:
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.

In [2]:
# ── 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¶

In [3]:
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).

In [4]:
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).

In [5]:
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.

In [6]:
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.

In [7]:
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₂).

In [8]:
# ── 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¶

In [9]:
# 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)¶

In [10]:
# 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)¶

In [11]:
# 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¶

In [12]:
# 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).

In [13]:
# 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()
In [14]:
# 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
In [15]:
# 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.