Skip to content

Commit d791d17

Browse files
authored
Merge pull request #1322 from al4225/sldsc-enrichment-mwe-docs
sldsc_enrichment: document snplist vs no-snplist path; add --target-categories-label; fix MWE
2 parents 71e00e7 + d4c8063 commit d791d17

1 file changed

Lines changed: 81 additions & 11 deletions

File tree

code/enrichment/sldsc_enrichment.ipynb

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,11 @@
409409
},
410410
"source": [
411411
"##### 1.2 joint tau\n",
412-
"with more than one annotation as the input"
412+
"with more than one annotation as the input\n",
413+
"\n",
414+
"`--snp_list <file>` is **optional** and **orthogonal to single/joint**: it restricts which SNP *rows* are written to the `.l2.ldscore` output (HM3-style restriction) — it does **not** change the LD-score *values* of retained SNPs (the r²-window still uses all `.bim` SNPs), nor `.l2.M` / `.l2.M_5_50`. With `--snp_list` the step runs polyfun's `ldsc.py --print-snps` and emits `.l2.ldscore.gz` (without it: `compute_ldscores.py` → `.l2.ldscore.parquet`), and the target annot is normalized to the plink `.bim`. Downstream `get_heritability` / `postprocess` **commands are unchanged** — they just read whichever `.l2.ldscore.{gz,parquet}` exists; the regression runs on the intersection sumstats ∩ baseline ∩ weights ∩ target, so for the HM3 restriction to take full effect the baseline & weights LD scores should also be HM3-restricted.\n",
415+
"\n",
416+
"**Which polyfun script & what to align.** `--snp_list` present → step 1 runs `ldsc.py --l2 --print-snps` (output `.l2.ldscore.gz`); absent → `compute_ldscores.py` (output `.l2.ldscore.parquet`). `ldsc.py` requires the target annot to line up with the plink `.bim` — **same SNP set and same row order**. The pipeline handles this internally (`normalize_for_ldsc` re-expands the annot onto the `.bim` rows, filling 0), but if you assemble inputs by hand, keep the **plink `.bim`, `.frq`, target annot, and baseline annot all on the same reference panel with consistent SNP set/order**: target and baseline annot row counts must match (polyfun hstacks them), and the `.frq` MAF filter (`--frqfile-chr`, m50) plus the by-SNP merge in `get_heritability` both assume a consistent panel. Mixing panels (e.g. 1000G plink with an ADSP-derived annot, or a `.frq` whose row count/order differs from the annot) is the most common source of wrong `Prop_SNPs` / Enrichment.\n"
413417
]
414418
},
415419
{
@@ -493,7 +497,8 @@
493497
" --weight-name weights_chr \\\n",
494498
" --annotation-name my_analysis \\\n",
495499
" --cwd output/heritability \\\n",
496-
" --maf-cutoff 0.05"
500+
" --maf-cutoff 0.05\n",
501+
"# (allm variant: use --maf-cutoff 0 — no MAF filter; polyfun then uses --not-M-5-50)\n"
497502
]
498503
},
499504
{
@@ -508,7 +513,9 @@
508513
"(which contains the $N$ single-target subdirectories and optionally the\n",
509514
"joint subdirectory) and calls `pecotmr::sldsc_postprocessing_pipeline()`\n",
510515
"to produce per-trait standardized tables and the default random-effects\n",
511-
"meta across all traits."
516+
"meta across all traits.\n",
517+
"\n",
518+
"Use `--target-categories-label` (same order as `--target-categories`) to give the target annotations friendly names in the output — e.g. `--target-categories ANNOT_1_0 ANNOT_2_0 --target-categories-label quantile_eQTL eQTL` makes the `target` column read `quantile_eQTL` / `eQTL` instead of `ANNOT_1_0` / `ANNOT_2_0` (the original names are kept in `params$target_categories_orig`). Omit it to keep the polyfun `.results` names.\n"
512519
]
513520
},
514521
{
@@ -524,16 +531,25 @@
524531
"# pecotmr::sldsc_postprocessing_pipeline. Single and joint target subdirectories\n",
525532
"# are auto-detected from <annotation_name>_single_<i> and <annotation_name>_joint.\n",
526533
"\n",
534+
"# --target-categories: the joint-run target columns as they appear in the .results \"Category\"\n",
535+
"# column. The 2-target joint dir here has columns ANNOT_1, ANNOT_2 (compute_ldscores.py keeps\n",
536+
"# the annot col names) and polyfun appends \"_0\" (target = ref-ld file 0) -> \"ANNOT_1_0\", \"ANNOT_2_0\".\n",
537+
"# (Single-target dirs would be \"ANNOT_0\"; with --snp_list/ldsc.py and a single annot it is \"L2_0\".)\n",
538+
"# You can drop --target-categories entirely to auto-detect from the joint-run results.\n",
539+
"# --target-categories-label (optional, same order as --target-categories): friendly display\n",
540+
"# names; renames the \"target\" column in the output (originals kept in params$target_categories_orig).\n",
527541
"sos run pipeline/sldsc_enrichment.ipynb postprocess \\\n",
528542
" --traits-file data/all_traits.txt \\\n",
529-
" --heritability-cwd output/polyfun/heritability \\\n",
530-
" --target-categories anno1 anno2 \\\n",
531-
" --target-anno-dir output/polyfun/ldscores/my_analysis_joint \\\n",
543+
" --heritability-cwd output/heritability \\\n",
544+
" --target-categories ANNOT_1_0 ANNOT_2_0 \\\n",
545+
" --target-categories-label quantile_eQTL eQTL \\\n",
546+
" --target-anno-dir output/my_analysis_joint \\\n",
532547
" --frqfile-dir data/ADSP/frq \\\n",
533548
" --plink-name ADSP_chr \\\n",
534549
" --annotation-name my_analysis \\\n",
535550
" --cwd output/sldsc_postprocess \\\n",
536-
" --maf-cutoff 0.05"
551+
" --maf-cutoff 0.05\n",
552+
"# (allm variant: use --maf-cutoff 0 — no MAF filter; polyfun then uses --not-M-5-50)\n"
537553
]
538554
},
539555
{
@@ -561,7 +577,7 @@
561577
" --postprocess-rds output/sldsc_postprocess/my_analysis.sldsc_postprocess.rds \\\n",
562578
" --subset-traits-file data/neuro_traits.txt \\\n",
563579
" --subset-name neuro \\\n",
564-
" --target-categories anno1 anno2 \\\n",
580+
" --target-categories ANNOT_1_0 ANNOT_2_0 \\\n",
565581
" --annotation-name my_analysis \\\n",
566582
" --cwd output/sldsc_postprocess"
567583
]
@@ -610,7 +626,6 @@
610626
"parameter: weights_dir = path() # Directory containing LD weights (computed against our panel)\n",
611627
"parameter: baseline_name = \"baseline_chr\" # Prefix of baseline annotation files\n",
612628
"parameter: weight_name = \"weights_chr\" # Prefix of LD weights files\n",
613-
"parameter: Mref = -1 # Reference number of SNPs in our panel (MAF > maf_cutoff). If > 0, use provided value; if <= 0, auto-calculate from ldscore files\n",
614629
"parameter: n_blocks = 200\n",
615630
"\n",
616631
"# Number of threads\n",
@@ -664,6 +679,33 @@
664679
"# Run on a NEW annotation_file with the K selected annotations\n",
665680
"# -> 1 joint dir with the conditional model.\n",
666681
"\n",
682+
"#\n",
683+
"# --- snplist (--snp_list) vs no-snplist: which polyfun script, output format,\n",
684+
"# column name, and the CM requirement ---\n",
685+
"# --snp_list given -> ldsc.py --l2 --print-snps -> output .l2.ldscore.gz\n",
686+
"# --snp_list absent -> compute_ldscores.py -> output .l2.ldscore.parquet\n",
687+
"#\n",
688+
"# LD-score column name (this is what becomes the .results \"Category\" in\n",
689+
"# [get_heritability], with a \"_<ref-ld-index>\" suffix appended there):\n",
690+
"# * compute_ldscores.py ALWAYS keeps the annot column name(s):\n",
691+
"# single annot column \"ANNOT\" -> ldscore column \"ANNOT\"\n",
692+
"# joint annot columns \"ANNOT_1\",\"ANNOT_2\",... -> \"ANNOT_1\",\"ANNOT_2\",...\n",
693+
"# * ldsc.py --l2 has a quirk: with EXACTLY ONE annotation (n_annot == 1) it\n",
694+
"# HARD-CODES the ldscore column name to \"L2\" and DROPS the annot's original\n",
695+
"# column name. With >=2 annotations it uses \"<annot_name>L2\"\n",
696+
"# (\"ANNOT_1L2\",\"ANNOT_2L2\",...).\n",
697+
"# => a single-target snplist run reports \"L2_0\" in .results, while a\n",
698+
"# single-target no-snplist run reports \"ANNOT_0\". [postprocess] auto-\n",
699+
"# detects either; only matters if you pass --target-categories explicitly.\n",
700+
"#\n",
701+
"# CM column requirement for snplist: ldsc.py --l2 --print-snps requires the\n",
702+
"# target annot to (a) carry a \"CM\" (centimorgan) column and (b) line up with\n",
703+
"# the plink .bim (same SNP set, same row order). This step handles both\n",
704+
"# internally (normalize_for_ldsc: takes CM from the .bim 4th column, re-expands\n",
705+
"# the annot onto the .bim rows, filling 0). Therefore the plink .bim files MUST\n",
706+
"# carry genetic-map (cM) positions when using --ld-wind-cm (the default);\n",
707+
"# if your .bim has 0 in the cM column, switch to --ld-wind-kb instead.\n",
708+
"#\n",
667709
"parameter: compute_single = True\n",
668710
"parameter: compute_joint = True\n",
669711
"parameter: score_column = 3\n",
@@ -1067,6 +1109,25 @@
10671109
"# `target_anno_dirs` is the list produced by [make_annotation_files_ldscore]:\n",
10681110
"# typically N _single_<i> directories plus optionally one _joint directory.\n",
10691111
"\n",
1112+
"#\n",
1113+
"# --- about the \".results\" Category column and the \"_0 / _1\" suffix ---\n",
1114+
"# Each (trait, target_dir) pair is ONE polyfun call; its `ldsc.py --ref-ld-chr`\n",
1115+
"# always gets exactly two LD-score sources, in this order:\n",
1116+
"# \"<target_dir>/<target>.\" (index 0) , \"<baseline_dir>/<baseline>\" (index 1)\n",
1117+
"# With --overlap-annot, every annotation column in the .results \"Category\" is\n",
1118+
"# named <ldscore_column_name>_<ref-ld-index>:\n",
1119+
"# index 0 = the target file -> \"ANNOT_0\" (no-snplist; compute_ldscores.py keeps the annot col name)\n",
1120+
"# -> \"L2_0\" (snplist + single annot; ldsc.py hard-codes \"L2\", see below)\n",
1121+
"# -> \"ANNOT_1_0\",\"ANNOT_2_0\" (no-snplist joint dir, N>=2 annot cols)\n",
1122+
"# -> \"ANNOT_1L2_0\",\"ANNOT_2L2_0\" (snplist joint dir, N>=2 -> \"<name>L2\")\n",
1123+
"# index 1 = the baseline file -> \"base_1\",\"Coding_UCSC_1\", ... (the 97 baseline annots)\n",
1124+
"# So in this pipeline the suffix is only ever 0 (target) or 1 (baseline); it would\n",
1125+
"# continue 0,1,2,... only if you handed `ldsc.py --ref-ld-chr` more than two sources.\n",
1126+
"# (Why ANNOT_0 vs L2_0: see the [make_annotation_files_ldscore] header — ldsc.py's\n",
1127+
"# \"n_annot == 1 -> column name 'L2'\" quirk vs compute_ldscores.py keeping the annot\n",
1128+
"# column name.) [postprocess] auto-detects the target Category; if you instead pass\n",
1129+
"# --target-categories, the names must match this column exactly.\n",
1130+
"#\n",
10701131
"parameter: target_anno_dirs = paths()\n",
10711132
"parameter: all_traits = []\n",
10721133
"\n",
@@ -1173,6 +1234,10 @@
11731234
"parameter: traits_file = path() # text file: one trait sumstats filename per line\n",
11741235
"parameter: heritability_cwd = path() # parent directory of [get_heritability] outputs (contains <annotation_name>_single_<i>/ subdirs and optionally <annotation_name>_joint/)\n",
11751236
"parameter: target_categories = [] # target annotation names. Auto-detected from the joint-run results if empty.\n",
1237+
"parameter: target_categories_label = [] # optional display names, same order as target_categories;\n",
1238+
" # when given, every \"target\" column / tau*-block colname in\n",
1239+
" # the output RDS is renamed to these (params$target_categories\n",
1240+
" # holds the labels, params$target_categories_orig the originals).\n",
11761241
"parameter: target_anno_dir = path() # directory of target .annot.gz files used for sd_C and binary detection (typically the joint dir, since it carries all target columns)\n",
11771242
"\n",
11781243
"input: traits_file\n",
@@ -1184,6 +1249,7 @@
11841249
"\n",
11851250
" traits <- readLines(\"${traits_file}\")\n",
11861251
" target_cats <- c(${\",\".join('\"%s\"' % c for c in target_categories)})\n",
1252+
" target_lab <- c(${\",\".join('\"%s\"' % c for c in target_categories_label)})\n",
11871253
"\n",
11881254
" # Auto-detect single-target and joint-target output directories.\n",
11891255
" her_root <- \"${heritability_cwd}\"\n",
@@ -1218,7 +1284,8 @@
12181284
" frqfile_dir = \"${frqfile_dir}\",\n",
12191285
" plink_name = \"${plink_name}\",\n",
12201286
" maf_cutoff = ${maf_cutoff},\n",
1221-
" target_categories = if (length(target_cats) > 0) target_cats else NULL\n",
1287+
" target_categories = if (length(target_cats) > 0) target_cats else NULL,\n",
1288+
" target_labels = if (length(target_lab) > 0) target_lab else NULL\n",
12221289
" )\n",
12231290
"\n",
12241291
" saveRDS(res, \"${_output[0]}\")\n",
@@ -1241,6 +1308,9 @@
12411308
"parameter: subset_traits_file = path() # text file: one trait id per line, subset of those passed to [postprocess]\n",
12421309
"parameter: subset_name = str # label used in the output filename\n",
12431310
"parameter: target_categories = [] # target annotation names to meta on; if empty, uses all from postprocess output\n",
1311+
"# If [postprocess] was run with --target-categories-label, the cached RDS already\n",
1312+
"# carries the display names (params$target_categories = the labels), so leave\n",
1313+
"# --target-categories empty here (or pass the labels, not the original ANNOT_* names).\n",
12441314
"\n",
12451315
"input: postprocess_rds, subset_traits_file\n",
12461316
"output: f\"{cwd:a}/{annotation_name}.{subset_name}.meta.rds\"\n",
@@ -1312,4 +1382,4 @@
13121382
},
13131383
"nbformat": 4,
13141384
"nbformat_minor": 4
1315-
}
1385+
}

0 commit comments

Comments
 (0)