目录

Gene-set scoring in the human atherosclerotic plaque atlas (Traeuble et al. 2025)

Code, figures and manuscript for this project: https://gitlink.org.cn/yanhaidong/vein-graft-atherosclerosis-atlas

Scores an arbitrary gene list (e.g. a 104-gene signature) across every cell type of the integrated human plaque atlas, on all 259,116 cells, on a laptop with ~8 GB RAM.

Dataset

field value
object plaque atlas (CELLxGENE Discover)
paper Traeuble et al., Nat Commun 2025, doi 10.1038/s41467-025-63202-x
collection db70986c-7d91-49fe-a399-a4730be394ac
file https://datasets.cellxgene.cziscience.com/ad9f52c0-054b-4dc5-944c-46c376a544cf.h5ad (3.06 GB)
matrix X = log1p(CP10k)-normalised counts, CSR (X/indptr, X/indices, X/data)
shape 259,116 cells x 25,379 genes
labels cell_type_level1 (13 types), cell_type_level2 (23 types, author-level), cell_type

cell_type_level1 keeps Fibromyocyte separate from Smooth Muscle Cell, which matters for SMC-derived plaque phenotypes.

Why this pipeline

SeuratDisk / hdf5r are not installable here, and converting a 3 GB h5ad would need

20 GB RAM. Instead the CSR matrix is streamed in cell blocks with rhdf5 (no full matrix in memory) and only per-gene / per-cell accumulators are kept.

Two passes:

  1. pass 1 – per-gene mean/SD over 12 blocks x 5,000 cells (--subsample cells total) needed for z-scores and for the AddModuleScore control bins;
  2. pass 2 – stream all cells, accumulate the scores below.

Runtime on this machine: ~2 min for all 259k cells (peak RSS ≈ 600 MB).

Scores (per cell)

column definition
score_mean mean log-normalised expression of the gene set
score_am AddModuleScore-style: mean(feature expression) − mean(expression of control genes drawn from the same expression bin, 24 bins) — numerically comparable to Seurat::AddModuleScore
score_z mean per-gene z-score ((x - mean_g) / sd_g), library-size/level independent
n_genes_det how many of the gene set are detected (>0) in the cell
lib_size total log-normalised counts of the cell (QC covariate)

score_am is the primary ranking score; score_z is the most robust to genes with very different detection rates.

Usage

1. Get the gene list out of WGCNA

R objects live only inside the session that made them, so export the vector once (write.csv(data.frame(gene = candidate_genes_final), "candidate_genes_final.csv", row.names = FALSE)) or save the workspace. scripts/02_genes_from_wgcna.R converts either form to the one-symbol-per-line list the scorer wants, and pre-checks every symbol against the atlas var before the 2-min scoring run:

Rscript scripts/02_genes_from_wgcna.R --csv  candidate_genes_final.csv
Rscript scripts/02_genes_from_wgcna.R --rdata wgcna.RData --object candidate_genes_final
Rscript scripts/02_genes_from_wgcna.R --csv  module_turquoise.csv --rownames   # genes in rownames
Rscript scripts/02_genes_from_wgcna.R --csv  hub_genes.csv --column Gene       # pick the column

It prints matched in atlas: N / M, writes data/genes.txt with the matched symbols only, and drops the misses into data/genes_not_in_atlas.txt (aliases, lncRNAs or probe IDs that the atlas calls differently).

2. Score the list

# one gene per line; '#' starts a comment
bash run.sh data/genes.txt                     # cell_type_level1
bash run.sh data/genes.txt cell_type_level2    # finer author labels

or directly:

Rscript scripts/01_score_genes.R \
  --h5ad data/plaque_atlas.h5ad \
  --genes data/genes.txt \
  --label cell_type_level2 \
  --outdir out/myset__cell_type_level2 \
  --name MYSET

Useful flags: --chunk 5000 (cells per block), --subsample 60000 (cells for pass 1), --nbin 24 (AddModuleScore bins), --no-plots, --max-cells N (smoke test on the first N cells), --seed 1.

Outputs (per run, out/<tag>/)

file content
cell_scores.tsv.gz per-cell table: barcode, cell type, all scores, lib_size, n_genes_det
gene_mean_by_celltype.tsv mean log-normalised expression, gene x cell type
gene_pct_by_celltype.tsv percent of cells expressing, gene x cell type
score_summary_by_celltype.tsv per cell type: n_cells, mean/median/SD of each score, pct of cells with score > 0, mean genes detected, rank, share of the top-1% scoring cells, enrichment
score_summary_console.txt pretty-printed summary + Kruskal-Wallis test
genes_used.txt, genes_not_in_atlas.txt gene-list QC (symbols matched against var/feature_name)
figures/violin_score_by_celltype.{png,pdf} score distribution per cell type
figures/dotplot_genes_by_celltype.png per-gene mean expression (colour) and % expressing (size)
figures/heatmap_genes_by_celltype.png per-gene mean expression, z-scored across cell types
figures/umap_score.png, figures/umap_celltype.png score on the atlas UMAP (colour clipped to 1st–99th pct)
sessionInfo.txt reproducibility

Sanity check with canonical markers

data/genes_demo_markers.txt (29 lineage markers) was used to validate the pipeline. It recovers exactly the expected biology (full atlas, cell_type_level1, score_am):

rank cell type n_cells mean score_am % cells score>0 enrichment in top 1%
1 Fibroblast 10,077 0.277 95.5 18.6x
2 Fibromyocyte 8,255 0.232 95.6 6.8x
3 Smooth Muscle Cell 16,124 0.165 91.7 0.60x
4 Macrophage 37,851 0.059 69.7 0.13x
5 EC 11,950 0.002 50.2 0.06x
6 Monocyte 13,370 -0.071 20.7 0
7 Neutrophil 3,867 -0.079 15.3 0
8 NK cell 12,483 -0.091 7.0 0
9 T cell 122,190 -0.128 4.7 0
10 Dendritic cell 7,259 -0.174 5.1 0
11 B cell 10,241 -0.183 1.0 0
12 Mast cell 2,965 -0.211 1.8 0
13 Plasma cell 2,484 -0.214 4.1 0

Per-gene dotplot confirms the drivers (SMC: ACTA2/MYH11/CNN1/TAGLN, fibroblast: COL1A1/COL3A1/COL5A1/DCN/LUM/POSTN, macrophage: CD68/C1QA/CD163/APOE, EC: PECAM1/VWF/CDH5/KDR/ACKR1), so the extraction and the CSR indexing are correct.

Result: the 104-gene WGCNA module

Input: data/candidate_genes_final.csv (104 symbols, NF-kB / cytokine / ER-stress / apoptosis / lipid-handling programme). 103 of 104 are present in the atlas var; SOD2 is genuinely absent (the atlas carries SOD1, SOD2-OT1, SOD3), so it is listed in data/genes_not_in_atlas.txt and the run uses 103 genes (out/genes__cell_type_level1).

cell_type_level1 (13 author labels, all 259,116 cells)

rank cell type n_cells mean score_am median % cells >0 top-1% enrichment mean score_z
1 Neutrophil 3,867 0.153 0.152 97.4 20.7x 0.110
2 Monocyte 13,370 0.085 0.088 82.5 6.2x 0.172
3 Macrophage 37,851 0.049 0.042 73.3 2.4x 0.134
4 Plasma cell 2,484 0.037 0.034 73.9 0.28x 0.054
5 Mast cell 2,965 0.022 0.015 61.5 0.20x 0.022
6 Dendritic cell 7,259 0.005 -0.002 48.7 0.23x 0.070
7 EC 11,950 -0.002 -0.006 44.3 0.03x 0.035
8 T cell 122,190 -0.010 -0.012 40.7 0.01x -0.024
9 B cell 10,241 -0.018 -0.022 33.6 0.11x -0.001
10 NK cell 12,483 -0.020 -0.022 32.3 0 -0.053
11 Fibroblast 10,077 -0.025 -0.028 26.8 0.01x -0.032
12 Smooth Muscle Cell 16,124 -0.051 -0.056 10.6 0 -0.057
13 Fibromyocyte 8,255 -0.057 -0.059 5.7 0 -0.061

Kruskal-Wallis across cell types: chi2 = 65,230, df = 12, p < 2.2e-16 (score_summary_console.txt). score_am and score_z agree on the top 3; SMC and fibromyocyte are the lowest of all 13 types, i.e. the module is not a smooth-muscle / fibroblast programme.

cell_type_level2 (23 labels, out/genes__cell_type_level2)

cell type n_cells mean score_am % cells >0 top-1% enrichment mean score_z
Neutrophil 3,867 0.153 97.4 20.7x 0.110
PLIN2+/TREM1+ Macrophage 1,755 0.128 97.4 12.8x 0.287
Monocyte 13,370 0.085 82.5 6.2x 0.172
Other Macrophage 20,036 0.051 72.9 2.9x 0.123
TREM2+/Foamy Macrophage 7,871 0.043 75.2 0.81x 0.154
Inflammatory Macrophage 3,732 0.040 73.4 0.59x 0.132
HMOX1+ Macrophage 4,457 0.024 62.3 0.56x 0.089
EndoMT EC 4,324 0.009 52.3 0.07x 0.069
… Fibroblast / SMC / Fibromyocyte 10,077 / 16,124 / 8,255 -0.025 / -0.051 / -0.057 26.8 / 10.6 / 5.7 ~0 -0.032 / -0.057 / -0.061

Within the monocyte-macrophage compartment the lipid-loaded / inflammatory PLIN2+/TREM1+ subset is the highest scorer and has the largest score_z of any label, while TREM2+/Foamy and HMOX1+ macrophages are close to the macrophage average — the module tracks the inflammatory rather than the foamy/efferocytotic phenotype.

Per-gene drivers (full atlas means, gene_mean_by_celltype.tsv)

  • Neutrophil: CXCL8 4.71 (93% of cells), NFKBIA 2.75 (86%), FOS 2.22 (71%), IL1B 1.91 (59%), NFE2L2 1.41 (56%), then ICAM1, HSP90AA1, MAP2K3.
  • Monocyte: NFKBIA 2.30, CXCL8 2.05, FOS 2.01, IL1B 1.90, CXCL2 1.48.
  • Macrophage: FOS 1.89, HSP90AA1 1.60, NFKBIA 1.48, CALM1 1.40, HSP90AB1 1.27.
  • SMC: mostly housekeeping/chaperone signal (CALM1 1.76, HSP90AB1 1.65, HSP90AA1 1.43), i.e. no module-specific activation.

Controls (both in out/)

gene set Neutrophil Monocyte Macrophage
module (103) 0.153 0.085 0.049
module minus CXCL8/NFKBIA/FOS/IL1B/NFE2L2 (_module_minus5) 0.019 0.018 0.014
expression-matched random 103 genes (_null_random103) -0.007 -0.015 -0.020

All 13 labels side by side: out/validation_summary.tsv.

  • The random, expression-matched set scores ~0 (+/- 0.02) in every cell type, so the neutrophil/monocyte signal is not an artifact of low RNA content: neutrophils detect only 10.4 of the 103 module genes per cell vs 23.5 in monocytes.
  • cor(score_am, n_genes_detected) = 0.044 and cor(score_am, log10 lib_size) = -0.018 across all cells, and -0.02 / 0.01 / 0.05 within neutrophil / monocyte / macrophage — the control bins removed the depth effect.
  • The five genes above carry most of the neutrophil signal (0.153 -> 0.019 when removed); they are the canonical neutrophil activation genes of the plaque (CXCL8/IL1B/NFKBIA/FOS/NFE2L2), so this is biology, not a single-gene outlier — but it does mean the neutrophil ranking is driven by the immediate-early / NF-kB arm of the module.
  • Neutrophils are spread over 20+ donors (largest: 8_Slysz_femoral n=1122, mean 0.167; 4_Slysz n=791, 0.196; 15_Bashore n=448, 0.122; 6_Slysz_femoral n=271, 0.147) and all sizeable donors are positive, so the hit is not one dataset.

Bottom line

The module is an inflammatory myeloid (NF-kB / cytokine / ER-stress) programme. It is highest in neutrophils (1.5% of the atlas, 97% of cells positive, 20.7x over-representation in the global top 1%), then in monocytes and macrophages, with the PLIN2+/TREM1+ inflammatory macrophage as the top-scoring myeloid subset. It is lowest in SMC / fibromyocyte / fibroblast, so it does not support an SMC- or fibroblast-derived origin for this WGCNA module.

MR candidate genes, Figure 3, and pathway enrichment

1. mr_candidate_genes.csv — 18 genes for MR / target follow-up

scripts/04_mr_candidates.R combines three sources:

  • per-gene myeloid enrichment from out/genes__cell_type_level2 (myeloid mean vs non-myeloid mean, log2FC, % of myeloid cells positive);
  • Druggable Genome / clinical-actionability annotation from the DGIdb GraphQL API (data/dgidb_annotation.tsv; 101/103 genes annotated, 78/103 are in the DRUGGABLE GENOME category);
  • a simple, stated selection rule: the five anchor genes the module is read on (CXCL8, IL1B, NFKBIA, FOS, NFE2L2) + the five strongest myeloid-enriched drivers + Druggable Genome genes with myeloid log2FC >= 1, >= 10% positive cells and mean >= 0.05, until 18 genes.
# gene tier neut mono mac (best) % myeloid+ DGIdb druggable role
1 CXCL8 anchor 4.71 2.05 2.25 65 yes CXCR1/2 chemokine
2 IL1B anchor 1.91 1.90 1.35 52 yes IL-1β (IL-1R1)
3 NFKBIA anchor 2.75 2.31 2.42 87 yes IκBα, NF-κB feedback
4 FOS anchor 2.22 2.01 2.21 84 yes AP-1 immediate-early TF
5 NFE2L2 anchor 1.41 0.66 0.63 61 no NRF2 (DMF-pathway target)
6 CXCL3 driver 0.13 1.19 2.07 46 yes CXCR2 chemokine
7 CXCL2 driver 0.63 1.48 1.76 49 yes CXCR2 chemokine
8 OLR1 driver 0.32 0.59 0.63 37 yes LOX-1 oxLDL receptor
9 CYBB driver 0.11 0.87 0.86 54 yes NOX2 oxidative burst
10 MMP9 driver 0.23 0.07 0.89 14 yes gelatinase B
11 CXCL1 fill 0.68 0.12 0.29 13 yes CXCR2 chemokine
12 CD36 fill 0.01 0.40 0.92 32 yes oxLDL scavenger receptor
13 LY96 fill 0.37 0.40 0.78 53 yes MD-2, TLR4 co-receptor
14 CCL2 fill 0.02 0.24 1.49 29 yes CCR2 chemokine
15 IL18 fill 0.02 0.20 0.49 38 yes IL-18
16 ICAM1 fill 0.92 0.35 0.25 32 yes ICAM-1 adhesion
17 ERO1A fill 0.21 0.17 0.59 31 yes ER oxidoreductase
18 PPARG fill 0.00 0.05 0.27 14 yes PPARγ (TZD target)

Expression columns are mean log1p(CP10k) in neutrophils / monocytes / the best macrophage subset (from cell_type_level2). representative_agents in the CSV lists approved or clinical-stage agents per gene (canakinumab for IL1B, dimethyl fumarate for NFE2L2, TZDs for PPARG, …).

Notes:

  • SOD2 is not in the atlas, so it cannot appear in any expression-filtered list (data/genes_not_in_atlas.txt).
  • NFE2L2 is not annotated DRUGGABLE GENOME by DGIdb (no direct small molecule; dimethyl fumarate/bardoxolone act on the NRF2 pathway), so its druggable_genome flag is FALSE although its representative_agents is populated.
  • Strongly druggable but not myeloid-enriched in this atlas (log2FC < 1, so not selected): JAK2 (0.39), CHUK (0.48), IKBKG, TBK1 (0.34), IRAK4, MAPK13 (1.51 but only 14% of cells), CASP3, BCL2, HSP90AA1.
  • The full ranking of all 103 genes (score, log2FC, % positive, DGIdb categories) is in out/mr_candidates_all_genes.tsv, so the cut-offs can be re-tuned with --n-max, --min-pct, --min-mean, --min-lfc.
Rscript scripts/04_mr_candidates.R                 # writes ./mr_candidate_genes.csv
Rscript scripts/04_mr_candidates.R --n-max 20 --min-pct 5

These 18 genes are what the cis-MR below uses; the actual MR estimates are in Cis-MR: do the candidate genes causally affect CAD?.

2. Figure 3 — core genes at cell_type_level2

scripts/03_core_gene_figures.R streams X once (same chunked CSR reader as 01_score_genes.R) and keeps only the 18 candidate columns, so the whole figure costs ~40 MB of RAM and one pass over the 3 GB file.

Rscript scripts/03_core_gene_figures.R \
  --h5ad data/plaque_atlas.h5ad --genes mr_candidate_genes.csv \
  --label cell_type_level2 --outdir out/core_genes__cell_type_level2

Outputs in out/core_genes__cell_type_level2/figures/:

file content
fig3A_violin_core_genes.png/.pdf violins per gene x cell type (7 myeloid + 5 highest non-myeloid labels, <=3000 cells per group)
fig3B_umap_core_genes.png one UMAP panel per gene, expression scaled to each gene’s 99th percentile, grey background = all cells
fig3C_umap_celltype_reference.png which cluster is which: myeloid subsets coloured and labelled at their median position
fig3_core_genes_combined.png A over B (patchwork), ready for assembly

Data behind the panels: core_gene_expression.tsv.gz (all 259,116 cells x 18 genes), core_gene_mean_by_celltype.tsv, core_gene_pct_by_celltype.tsv, core_gene_overall_summary.tsv (mean, % positive, top cell type, myeloid log2FC).

3. GO / KEGG enrichment — scripts/05_enrichment.py

gseapy enrichr with the atlas’ own gene universe as background (data/atlas_background_genes.txt, 25,377 symbols), so the Fisher exact test is comparable to clusterProfiler::enricher rather than Enrichr’s generic background. Libraries: GO BP/MF/CC (2021), KEGG 2021, Reactome 2022, WikiPathways 2024, MSigDB Hallmark 2020. The seven GMT files are committed under data/gmt/ and read locally, so the test runs offline and gives identical P/FDR values on every machine.

python3 scripts/05_enrichment.py        # offline; needs gseapy + pandas + matplotlib

Key pathways (best term per family, KEGG 2021 unless stated):

pathway overlap FDR
Lipid and atherosclerosis 103/194 5.6e-231
TNF signalling 37/111 3.4e-61
Toll-like receptor signalling 34/90 2.7e-58
NOD-like receptor signalling 37/153 2.0e-55
IL-17 signalling 31/88 4.8e-52
Apoptosis 33/136 2.4e-49
NF-κB signalling 23/101 1.1e-33
Chemokine signalling 22/184 9.9e-26
Neutrophil extracellular trap formation 15/168 6.9e-16
Cytokine-cytokine receptor interaction 14/260 6.3e-12
TNF-α signalling via NF-κB (Hallmark) 17/198 1.9e-16
Cytokine-mediated signalling (GO BP) 40/588 2.8e-35

Outputs: out/enrichment/enrichment_all.tsv (every term, every library), enrichment_key_pathways.tsv, fig_enrichment_barplot.png/.pdf (A: top 5 terms per library, B: NF-κB / TNF / cytokine-receptor axis).

Interpretation caveat. The 104-gene module was itself assembled from NF-κB, cytokine, ER-stress, apoptosis and lipid programmes, so these enrichments confirm what the module is — they are not independent evidence for the phenotype. The independent evidence is the cell-type scoring in the result section above. Two further points worth knowing: all 103 genes fall inside KEGG “Lipid and atherosclerosis” (a 194-gene term in this background), i.e. the module is essentially a subset of that pathway; and terms named after infectious diseases (Hepatitis B, measles, Kaposi) are driven by the same shared cytokine genes, so they are hidden from the figure (still present in enrichment_all.tsv).

Cis-MR: do the candidate genes causally affect CAD?

scripts/06_fetch_mr_inputs.sh (data) + scripts/07_run_mr.R (TwoSampleMR) run a two-sample cis-MR of the 18 candidate genes on coronary artery disease. Both summary-statistic sources are streamed and cut down to the rows we need, so the whole analysis runs locally from public files and nothing genome-wide has to be kept on disk.

role dataset what is used
exposure eQTLGen phase I cis-eQTLs, FDR<0.05 (2019-12-11-cis-eQTLsFDR0.05-ProbeLevel-...txt.gz) 12,494 SNP-gene rows for 15 of the 18 genes
allele frequencies eQTLGen 1000G SNP AF converts the published Z-scores to beta/SE
exposure (fallback) GTEx v8 single-tissue cis-eQTLs (GTEx_Analysis_v8_eQTL.tar, tissues Whole_Blood, Artery_Coronary, Artery_Tibial) 1,378 rows for CXCL2, CXCL3, CYBB
outcome CARDIoGRAMplusC4D 1000G, GCST003116 (cad.add.160614.website.txt) 11,800 of 12,454 instrument SNPs (60,801 cases / 123,504 controls)

Two details that matter and are easy to get wrong:

  • eQTLGen publishes Z-scores only. They are converted with the eQTLGen 1000G allele frequencies, beta = Z / sqrt(2p(1-p)(N+Z^2)), se = 1 / sqrt(2p(1-p)(N+Z^2)).
  • eQTLGen uses Ensembl-v71 (2013) gene symbols, so CXCL8 is filed as IL8 and ERO1A as ERO1L. scripts/06_fetch_mr_inputs.sh maps the old names and writes the current HGNC symbol back into the table. Without this, two perfectly good genes look like they have no cis-eQTL.
# 1. fetch + filter (streams ~1.3 GB; the GTEx tar and the 820 MB variant
#    lookup table are only needed if you want the GTEx fallback)
bash scripts/06_fetch_mr_inputs.sh --gtex
# 2. MR, diagnostics, plots, report
Rscript scripts/07_run_mr.R
# or both as part of the main run:
MR=1 bash run.sh mr_candidate_genes.csv

Instruments are cis SNPs with p < 5e-8 (GTEx: each gene’s own permutation threshold), distance-pruned at 250 kb — LD clumping is switched on only when a local PLINK binary and 1000G --bfile are supplied (--plink, --bfile), so the default path is offline. All instruments have F > 16, i.e. no weak-instrument problem.

Results, per 1-SD higher genetically predicted expression

gene method SNPs OR (95% CI) P
NFKBIA IVW 5 0.93 (0.89-0.97) 4.7e-4
CXCL8 (IL8) IVW 5 1.07 (1.02-1.12) 9.2e-3
CXCL3 Wald 1 1.10 (0.98-1.24) 0.11
IL1B Wald 1 1.24 (0.92-1.66) 0.16
OLR1 Wald 1 1.25 (0.90-1.72) 0.18
PPARG IVW 3 1.06 (0.97-1.15) 0.19
CCL2 Wald 1 1.15 (0.83-1.60) 0.41
NFE2L2 Wald 1 0.87 (0.61-1.23) 0.42
CXCL2 Wald 1 1.04 (0.94-1.14) 0.44
ICAM1 IVW 2 0.91 (0.69-1.21) 0.53
IL18 IVW 3 1.02 (0.95-1.09) 0.62
MMP9 Wald 1 1.03 (0.93-1.14) 0.63
FOS Wald 1 1.05 (0.83-1.33) 0.70
ERO1A IVW 5 1.01 (0.97-1.04) 0.73
CD36 IVW 4 1.00 (0.96-1.06) 0.84
LY96 IVW 3 1.00 (0.94-1.06) 0.89
CXCL1 IVW 5 1.00 (0.89-1.12) 0.99
CYBB — 0 not testable —

CYBB is X-linked and GCST003116 contains autosomes only, so it has no outcome data. Diagnostics are in out/mr/mr_report.md: MR-Egger intercepts are all non-significant (no directional pleiotropy), Steiger directionality is correct for every gene, and heterogeneity is limited to CXCL1 (Q p = 0.046) and ICAM1 (p = 0.014), both with 5 and 2 SNPs.

Reading of the two hits: higher NFKBIA (IκBα) expression is associated with lower CAD risk and higher CXCL8 (IL-8) expression with higher risk. Both are consistent with the atlas result (the module is NF-κB/cytokine driven) and survive the multiple-testing look (17 genes, Bonferroni 0.05/17 = 2.9e-3) only for NFKBIA — CXCL8 at p = 9.2e-3 does not. The weighted-median estimate for CXCL8 agrees (OR 1.06, p = 0.03), so the signal is not driven by a single SNP.

Variant-level follow-up: the Colocalisation section asks whether the eQTL and CAD signals at these two loci share a causal variant. Neither locus carries a genome-wide significant CAD association (min P = 2.7e-4 and 7.2e-4), so coloc is underpowered there (PP.H4 = 0.03 and 0.20) — MR can still be significant because it aggregates small per-SNP effects.

Outputs: out/mr/mr_report.md (tables + embedded figures), mr_results_main.tsv, mr_results_all_methods.tsv, mr_instruments_summary.tsv, mr_harmonised_all.tsv, mr_leaveoneout.tsv, mr_singlesnp.tsv, mr_heterogeneity.tsv, mr_pleiotropy.tsv, mr_directionality.tsv (the last five only for genes with

= 2 instruments), and plots/ with scatter, leave-one-out and funnel plots per gene plus mr_forest_all_genes.png/.pdf.

Colocalisation: do the eQTL and CAD signals share a variant?

cis-MR says whether the transcript matters; it cannot say which variant carries the effect. scripts/08_coloc.R (coloc 5.2.3) therefore asks, for the two MR hits, whether the cis-eQTL and the CAD association in the same window are consistent with one shared causal variant (PP.H4) or with two distinct causal variants (PP.H3).

role dataset what is used
exposure eQTLGen phase I, full cis-eQTL file (2019-12-11-cis-eQTLsFDR-ProbeLevel-...txt.gz, 3.9 GB) every SNP within ±500 kb of the TSS (13,263 rows for the four genes, 5,394 for the two targets)
allele frequencies eQTLGen 1000G SNP AF Z-scores → beta/SE for the regional SNPs
outcome CARDIoGRAMplusC4D 1000G (already local in data/mr/_raw/) every SNP in the same windows
controls LPL (8p21.3) and SORT1/CELSR2 (1p13.3) known CAD loci, run through exactly the same code

Why the MR input tables cannot be reused

coloc.abf() is only valid on complete regional summary statistics. data/mr/eqtlgen_cis.tsv keeps FDR<0.05 pairs only and data/mr/cad_subset.tsv keeps the 12,454 instrument SNPs only; running coloc on those truncated tables inflates the eQTL likelihood and makes PP.H4 meaningless. scripts/06_fetch_mr_inputs.sh --coloc therefore streams the full eQTLGen file (cached under data/coloc/_raw/, 3.9 GB, git-ignored, safe to delete) and extracts every SNP in the window from both sources.

# 1. regional, complete summary statistics (~5 min from the cached file)
bash scripts/06_fetch_mr_inputs.sh --coloc \
  --coloc-genes "CXCL8 NFKBIA LPL:8:19791998 SORT1:1:109896382"
# 2. coloc.abf + prior sensitivity + LocusZoom-style plots + report
Rscript scripts/08_coloc.R --genes CXCL8 NFKBIA LPL SORT1
# or as part of the main run:
COLOC=1 MR=1 bash run.sh mr_candidate_genes.csv

Both traits are harmonised on rsID: every eQTLGen Z-score is converted to beta/SE with beta = Z / sqrt(2·MAF·(1-MAF)·(N + Z^2)) (Vosa et al. 2021), both betas are put on the CAD effect allele (complementing strand-flipped pairs), palindromic SNPs with MAF > 0.42 are dropped, and each gene is tested with coloc.abf(p12 = 1e-5) plus coloc::sensitivity() over a grid of priors. The eQTL side is a quantitative trait (N = per-gene eQTLGen sample size, sdY = 1), the CAD side is case-control (s = 60,801/184,305 = 0.33).

Results

gene role chr SNPs PP.H1 PP.H3 PP.H4 reading
CXCL8 (IL8) target 4 2,102 0.858 0.110 0.033 eQTL only
NFKBIA target 14 2,608 0.696 0.105 0.199 inconclusive
LPL control 8 4,061 0.007 0.134 0.859 shared variant
SORT1 control 1 2,671 3.6e-17 1.000 8.8e-16 two variants

Reading of the results

  • Neither target locus carries a genome-wide significant CAD signal in CARDIoGRAMplusC4D: the strongest SNP in the CXCL8 window is P = 2.7e-4 and in the NFKBIA window P = 7.2e-4. With no outcome association to attribute, PP.H4 cannot become large — coloc is underpowered here, and PP.H1 (“eQTL only”) dominates for CXCL8. This is a power statement, not a refutation of the MR: MR aggregates many small per-SNP effects and can be significant while no single variant reaches 5e-8 (the NFKBIA IVW P = 4.7e-4 comes from exactly such small, concordant effects).
  • The code is not the explanation. The same script, on the same data sources, returns PP.H4 = 0.86 at LPL (a CAD locus whose blood eQTL and CAD signal share variants) and PP.H3 ≈ 1 at SORT1 (where the blood eQTL and the CAD signal are two different variants — the CAD signal at 1p13 is hepatic, not blood). The pipeline therefore discriminates shared from distinct signals on real data.
  • The regional plots show why: at CXCL8 the eQTL peak (rs1951703, 74.572 Mb) and the nominal CAD peak (rs74361175, 74.742 Mb) are 170 kb apart; at NFKBIA the two peaks are 17 kb apart but neither reaches significance and the posteriors remain split (PP.H4 0.20, PP.H3 0.11, PP.H1 0.70).
  • These are whole-blood posteriors. The plasma pQTL replication and the (power-limited) GTEx artery check are in Protein and tissue replication; they explain why a low blood PP.H4 is a tissue-specific lower bound rather than a refutation of the cis-MR.

Outputs: out/coloc/coloc_report.md (tables, per-gene numbers, embedded figures), coloc_results.tsv (PP.H0-H4, lead SNPs, effect sizes, interpretation), coloc_sensitivity.tsv (PP.H4 over the p12 grid), coloc_region_<gene>.tsv (harmonised regional data) and plots/coloc_locus_<gene>.png|.pdf plus plots/coloc_pp_summary.png|.pdf.

Protein and tissue replication: is the blood eQTL the right proxy?

Both steps above rest on one exposure, a whole-blood cis-eQTL. Two questions follow: does the protein behave the same way, and does vascular tissue reproduce the RNA signal strongly enough to re-run either analysis?

1. Plasma pQTL (public GWAS Catalog)

UKB-PPP (Synapse account) and deCODE (download request form) cannot be fetched unattended, but the GWAS Catalog hosts harmonised public pQTL summary statistics for both proteins, with complete regional statistics and no registration:

gene GWAS Catalog trait n used for
CXCL8 (IL-8) GCST90428420, PMID 39794498 Interleukin-8 levels 70,016 MR + coloc
NFKBIA (IκBα) GCST90423455, PMID 39528825 NF-kappa-B inhibitor alpha levels 2,721 MR + coloc

These files are GRCh38 + rsIDs and already carry beta, SE and the effect-allele frequency, so scripts/09_fetch_pqtl.sh only has to cut the ±500 kb windows: there is no Z → beta step and no external allele-frequency file. The CAD GWAS is hg19, but every join is on rsID, never on position.

bash scripts/09_fetch_pqtl.sh                       # ~630 MB, 8 parallel ranges
Rscript scripts/07_run_mr.R --exposure-schema pqtl  # -> out/mr_pqtl
Rscript scripts/08_coloc.R  --eqtl-schema pqtl      # -> out/coloc_pqtl
# or: PQTL=1 bash run.sh mr_candidate_genes.csv

Cis-MR, per 1 SD of plasma protein. The CXCL8 pQTL is strong (rs79656070, β = −0.134, P = 1.7e-18, F = 77) and points the same way as the blood eQTL:

exposure gene instruments OR (95% CI) P
eQTLGen blood cis-eQTL CXCL8 5 1.07 (1.02–1.12) 0.009
plasma IL-8 pQTL CXCL8 1 1.29 (0.99–1.67) 0.055
eQTLGen blood cis-eQTL NFKBIA 5 0.93 (0.89–0.97) 4.7e-4
plasma IκBα pQTL NFKBIA 0 not estimable —

NFKBIA cannot be tested at the protein level with public data: its only GWAS Catalog pQTL has n = 2,721 and its best cis-SNP reaches P = 9.8e-3, nowhere near the 5e-8 needed for an instrument. That is a data-availability limit, not a negative result.

Coloc, pQTL vs CAD — same code, same windows, same priors as above:

gene SNPs PP.H1 PP.H3 PP.H4 reading
CXCL8 2,001 0.839 0.105 0.057 protein only
NFKBIA 1,913 0.087 0.010 0.006 H0 = 0.80 (neither trait)

The CXCL8 pQTL is enormous (P = 1.7e-18) and still gives PP.H4 = 0.06, because the CAD side of the same window contains nothing significant to colocalise with (min CAD P = 2.7e-4). Moving from mRNA to protein shifts PP.H4 from 0.033 to 0.057: the ceiling is set by the outcome, not by the exposure. Protein-level data therefore reinforce the direction of the MR finding and leave the coloc null where it was.

2. GTEx v8 artery: can vascular tissue carry the analysis?

scripts/10_gtex_artery_check.R reads the GTEx v8 single-tissue eGenes and significant variant-gene pairs already in data/mr/_gtex/ (Artery_Aorta, Artery_Coronary, Artery_Tibial, Whole_Blood), recovers each tissue’s sample size from minor_allele_count / (2·MAF), and looks every lead SNP up in the CAD GWAS:

gene tissue n eGene q lead SNP β (alt) CAD P at lead F instrument
CXCL8 Aorta 387 no 0.13 rs16849080 −0.193 0.10 13.5 none
CXCL8 Coronary 213 no 0.44 rs6532120 +0.230 0.49 8.8 none
CXCL8 Tibial 584 yes 0.018 rs16850659 −0.818 absent 18.1 lead SNP not in CAD
CXCL8 Whole blood 670 yes 3.0e-6 rs4694633 +0.202 0.073 38.9 usable
NFKBIA Aorta 387 no 0.14 rs28758606 +0.188 0.21 13.7 none
NFKBIA Coronary 213 no 0.40 rs77319708 +0.858 0.45 10.4 none
NFKBIA Tibial 584 no 0.21 rs8017740 +0.440 0.72 10.6 none
NFKBIA Whole blood 670 no 0.19 rs150976671 −0.362 0.064 11.7 none

No artery instrument exists for either gene (0 of 6 gene–tissue pairs).

  • In the disease-relevant coronary artery (n = 213) neither gene is an eGene: q = 0.44 (CXCL8), q = 0.40 (NFKBIA), with lead SNPs ~1 Mb from the gene — the signature of no cis signal at all rather than a weak one.
  • Aorta (n = 387) is the same: q = 0.13 and q = 0.14.
  • CXCL8 is an eGene in tibial artery (q = 0.018), but only through two low-frequency variants — rs16850659 (MAF 1.5%, β = −0.82, the lead) and a second SNP 244 kb away with the opposite sign (β = +0.62) — and the lead SNP is absent from the CAD GWAS. No Wald ratio is estimable, and the pair could not form a coherent instrument in any case.
  • NFKBIA is not an eGene in any artery, nor in GTEx whole blood at n = 670.

So for these two genes GTEx artery can support neither MR nor coloc; the full table is out/tissue/gtex_tissue_eqtl.tsv. Coloc is blocked a second time by the data structure: the v8 archive distributes only the significant variant-gene pairs, and coloc.abf() needs complete regional statistics. Where the tissue does have power the same code works: at the GTEx whole-blood CXCL8 lead SNP the Wald ratio is OR = 1.14 (P = 0.07 at n = 670), the same direction as eQTLGen (1.07) and the plasma pQTL (1.29).

3. Why a blood eQTL can under-report a vascular target

Tissue specificity is a genuine limitation of the coloc step above and belongs in the interpretation of the PP.H4 values:

组织特异性会削弱全血 eQTL 的共定位信号。 A whole-blood cis-eQTL is only a proxy for expression in the cells that build the plaque. If the CAD-relevant regulatory variant acts on CXCL8 or NFKBIA in plaque macrophages, endothelial cells or smooth-muscle cells rather than in circulating leukocytes, the blood eQTL and the CAD signal can tag the same locus through different causal variants (high PP.H3), or the blood eQTL can be too weak to support any confident posterior (PP.H1 dominant, PP.H4 low) even when the gene is genuinely causal in the vessel wall. A low PP.H4 measured in blood is therefore a tissue-specific estimate and a lower bound on the evidence available in the artery, not a refutation of the cis-MR. The converse also holds: a high PP.H4 in blood implicates a shared regulatory variant, but not necessarily the cell type in which that variant matters.

The GTEx check above shows that public artery data cannot currently settle the question — at n = 213–584 these two genes are not eGenes in coronary artery or aorta, so the missing artery-based replication reflects power and data availability, not absence of a vessel-wall effect. What would settle it: a well-powered artery eQTL (e.g. STARNET or GTEx v10 arteries), a plaque single-cell/pseudobulk eQTL, or the pQTL route above, which measures the circulating protein that a drug would actually engage.

Figures, manuscript and end-to-end reproduction

The figures in the manuscript are assembled from the tables in out/, not from screenshots, so every panel is a vector PDF that can be regenerated:

Rscript scripts/11_bulk_gse310013.R      # Figure 1 source: DEGs + KEGG intersection
Rscript scripts/12_wgcna_modules.R       # Figure 2A source: module-trait heatmap
Rscript scripts/13_assemble_figures.R    # all panels + composites -> out/final_figures/

11_bulk_gse310013.R downloads the GSE310013 raw counts once into data/bulk/ (git-ignored) and runs a paired DESeq2 model (~ donor + day). With the default filter (>= 10 counts in >= 4 libraries, padj < 0.05, no LFC cut-off) this gives 11,650 DEGs, of which 117 fall in KEGG hsa05417, capturing 101 of the 103 scorable signature genes. An earlier, unarchived run of the same comparison reported 10,848 DEGs / 111 overlapping genes; the difference is the expressed-gene filter and the LFC cut-off, and out/bulk/venn_counts.tsv is the authoritative table for the current run. The 215-gene KEGG set is cached in data/atherosclerosis_kegg.txt so the figure does not depend on network access to Enrichr.

12_wgcna_modules.R rebuilds the module structure on the 5,048 most variable DEGs (plus all expressed signature genes): signed hybrid network, soft power 16, modules renumbered by |r| with culture day so that ME1 is the module most anti-correlated with Day 7 (r = -0.99, P = 9.3e-10). 101 signature genes fall in ME1/ME2 and 76 have kME > 0.8.

20_ambient_sensitivity.R is the reviewer-facing control for the cell-type ranking. It estimates the ambient profile from the 10% lowest-complexity cells, unmixes every cell at contamination fractions phi = 0-0.30 (c’ = max(0, c - phiTa), renormalised to the atlas scale T2 recovered per cell from X = log1p(c/T2*1e4)) and rescores all 259,116 cells. At phi = 0 the recomputed score_mean reproduces out/genes__cell_type_level2/cell_scores.tsv.gz exactly, which is the built-in check that the implementation is correct. Outputs (ambient_profile.tsv, scores_by_celltype_by_phi.tsv, summary.tsv, the phi-sweep figure and ambient_sensitivity.md) go to out/ambient_sensitivity/.

13_assemble_figures.R writes:

out/final_figures/
├── figure1/   Figure1.pdf   fig1A_volcano.pdf   fig1B_venn.pdf
├── figure2/   Figure2.pdf   fig2A_module_trait.pdf   fig2B_enrichment.pdf
├── figure3/   Figure3.pdf   fig3A_umap_celltypes.pdf   fig3B_umap_score.pdf
│                            fig3C_violin_celltypes.pdf
├── figure4/   Figure4.pdf   fig4A_mr_forest.pdf   fig4B_mr_scatter.pdf
│                            fig4C_coloc_posteriors.pdf   fig4D_drug_targets.pdf
├── supplementary/  FigureS.pdf   figS1_leave_one_out.pdf   figS2_funnel.pdf
│                   figS3_coloc_prior_sensitivity.pdf   figS4_gtex_tissue.pdf
│                   figS5_soft_threshold.pdf   figS6_ambient_sensitivity.pdf
├── captions.md      # panel-level legends with the exact statistics of the run
└── MANIFEST.tsv     # file inventory

Composite figures are 180 mm wide (single column 90 mm, double column 180 mm), so they drop into a manuscript template without rescaling; each panel is also saved on its own for journals that want panels uploaded separately. figure3/fig3A subsamples the UMAP to ~70,000 cells (keeping every PLIN2+/TREM1+ macrophage) so the PDF stays a few MB; fig3B bins the embedding instead of drawing 259k points, and the violin uses all cells.

The manuscript is manuscript/manuscript.md (Abstract, Introduction, Results, Discussion, Limitations, Methods, availability statements, references, legends). Numbers quoted in the text are the ones written by the scripts above; if a script is re-run with different thresholds, the text has to be updated with the new venn_counts.tsv / module_trait.tsv / mr_results_main.tsv values.

Caveats

  • X is used as stored (log-normalised). No re-normalisation, no scaling, no imputation — this mirrors what Seurat::AddModuleScore/PercentageFeatureSet would see after NormalizeData.
  • Gene symbols are matched against var/feature_name; anything unmatched is written to data/genes_not_in_atlas.txt (alias problems: e.g. use current HGNC symbols).
  • score_am control bins come from a 60k-cell subsample, so values are stable to ~1e-3 but not bit-identical to a Seurat run on the full object.
  • Cell-type labels are the atlas’ own (cell_type_level1/2), i.e. author annotations, not re-clustered here. cell_type (level 0) merges SMC + fibromyocyte.
  • The atlas is disease-mixed (atherosclerosis + controls); obs/disease, obs/dataset, obs/donor_id are available for stratified follow-up runs.

Files

plaque-atlas-genescore/
├── run.sh                     # end-to-end wrapper
├── mr_candidate_genes.csv     # 18-gene MR / target short list (script 04)
├── data/
│   ├── plaque_atlas.h5ad      # 3.06 GB CELLxGENE download
│   ├── candidate_genes_final.csv   # the 104-gene WGCNA module as exported
│   ├── genes.txt              # 103 symbols actually scored (SOD2 missing)
│   ├── genes_not_in_atlas.txt # symbols the atlas does not carry
│   ├── genes_demo_markers.txt # 29-marker sanity set
│   ├── atlas_background_genes.txt  # 25,377 var symbols = enrichment universe
│   ├── dgidb_annotation.tsv   # DGIdb categories / drugs for the module genes
│   ├── _null_random103.txt    # expression-matched random control set
│   ├── _module_minus5.txt     # module minus the 5 neutrophil-driver genes
│   ├── mr/                    # cis-MR inputs (row-filtered from public sumstats)
│       ├── eqtlgen_cis.tsv        # eQTLGen cis-eQTLs for the 18 genes
│       ├── eqtlgen_snp_af.tsv     # 1000G allele frequencies (Z -> beta)
│       ├── gtex_cis.tsv           # GTEx v8 fallback (CXCL2, CXCL3, CYBB)
│       ├── cad_subset.tsv         # CAD GWAS rows for the instrument SNPs
│       ├── pqtl_cis.tsv           # plasma pQTL instruments (CXCL8)
│       ├── cad_subset_pqtl.tsv    # CAD rows for the pQTL instruments
│       ├── SOURCES.tsv            # URL / date / row counts
│       └── _raw/                  # optional local CAD file (git-ignored, 0.8 GB)
│   └── coloc/                 # coloc inputs: complete regional statistics
│       ├── eqtlgen_region.tsv     # every cis-eQTL SNP within +/-500 kb of each gene
│       ├── eqtlgen_region_af.tsv  # 1000G allele frequencies for those SNPs
│       ├── cad_region.tsv         # every CAD SNP in the same windows
│       ├── SOURCES.coloc.tsv      # URL / date / row counts
│       ├── pqtl_region.tsv        # every pQTL SNP within +/-500 kb (GRCh38)
│       ├── SOURCES.pqtl.tsv       # GWAS Catalog accessions / PMID / N
│       └── _raw/                  # cached 3.9 GB eQTLGen + pQTL files (git-ignored)
├── scripts/00_inspect_h5ad.R  # structure / label tables
├── scripts/01_score_genes.R   # streaming scorer + figures
├── scripts/02_genes_from_wgcna.R  # WGCNA/DE table -> data/genes.txt + symbol check
├── scripts/03_core_gene_figures.R # Figure 3: violins + per-gene UMAPs
├── scripts/04_mr_candidates.R # MR short list + DGIdb druggability annotation
├── scripts/05_enrichment.py   # gseapy GO/KEGG/Reactome enrichment + bar chart
├── scripts/06_fetch_mr_inputs.sh  # eQTLGen + GTEx + CAD -> data/mr/ (streaming, row-filtered)
├── scripts/07_run_mr.R        # cis-MR per gene: TwoSampleMR, diagnostics, plots, report
├── scripts/08_coloc.R         # coloc.abf per gene: posteriors, sensitivity, LocusZoom plots
├── scripts/09_fetch_pqtl.sh   # plasma pQTL (GWAS Catalog) -> MR/coloc inputs
├── scripts/10_gtex_artery_check.R # GTEx artery vs blood: is there an instrument at all?
├── scripts/11_bulk_gse310013.R # Figure 1: GSE310013 DEGs + KEGG intersection
├── scripts/12_wgcna_modules.R  # Figure 2A: WGCNA modules + module-trait heatmap
├── scripts/13_assemble_figures.R # all manuscript panels -> out/final_figures/
├── manuscript/manuscript.md    # Abstract / Intro / Results / Discussion / Methods
├── out/
│   ├── genes__cell_type_level1/   # main result (13 labels)
│   ├── genes__cell_type_level2/   # main result (23 labels)
│   ├── _null_random103__cell_type_level1/   # control
│   ├── _module_minus5__cell_type_level1/    # control
│   ├── core_genes__cell_type_level2/        # Figure 3 + per-cell expression
│   ├── enrichment/                # enrichment tables + bar chart
│   ├── mr/                        # cis-MR: report, result tables, per-gene plots
│   ├── coloc/                     # colocalisation: report, posteriors, regional plots
│   ├── mr_pqtl/                   # plasma pQTL cis-MR (same layout as out/mr/)
│   ├── coloc_pqtl/                # plasma pQTL coloc (same layout as out/coloc/)
│   ├── tissue/                    # GTEx artery vs whole blood eQTL check
│   ├── bulk/                      # GSE310013 DEGs, venn counts, summary
│   ├── wgcna/                     # module assignments, module-trait table, kME
│   ├── final_figures/             # Figure 1-4 + supplementary (PDF + PNG) + captions
│   ├── mr_candidates_all_genes.tsv          # ranking of all 103 genes
│   ├── validation_summary.tsv     # module vs the two controls
│   ├── demo__cell_type_level1/
│   └── demo__cell_type_level2/
└── logs/
关于
49.7 MB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802047560号