OSCR

Genome-wide association studies of infant and toddler temperament in European and multi-ancestry populations.

Code ↔ Paper

3 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 3 matches
  1. [1] § Methods › FUMA ↔ sumstats.py, lines 281–333 · score 0.70 · LD r2, LD blocks, lead SNPs, closer, FUMA, kb
  2. [2] § Methods › FUMA ↔ sumstats.py, lines 281–333 · score 0.59 · candidate SNPs, maximum distance, FUMA, kb
  3. [3] § Methods › Quality control of genetic data ↔ sumstats.py, lines 110–173 · score 0.57 · quality control, minor allele frequency, outside, filtering, imputation, variants

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 2,299 lines · 128 KB · GPL-3.0 · 3 matches

  1. #!/usr/bin/env python
  2. '''
  3. (c) 2016-2018 Oleksandr Frei and Alexey A. Shadrin
  4. Various utilities for GWAS summary statistics.
  5. '''
  6. from __future__ import print_function
  7. import pandas as pd
  8. import numpy as np
  9. from scipy import stats
  10. import scipy.io as sio
  11. import scipy.sparse
  12. import os
  13. import time, sys, traceback
  14. import argparse
  15. import six
  16. from sumstats_utils import *
  17. import collections
  18. import re
  19. from shutil import copyfile, rmtree
  20. import zipfile
  21. import glob
  22. import socket
  23. import getpass
  24. import subprocess
  25. import tarfile
  26. __version__ = '1.0.0'
  27. MASTHEAD = "***********************************************************************\n"
  28. MASTHEAD += "* sumstats.py: utilities for GWAS summary statistics\n"
  29. MASTHEAD += "* Version {V}\n".format(V=__version__)
  30. MASTHEAD += "* (C) 2016-2018 Oleksandr Frei and Alexey A. Shadrin\n"
  31. MASTHEAD += "* Norwegian Centre for Mental Disorders Research / University of Oslo\n"
  32. MASTHEAD += "* GNU General Public License v3\n"
  33. MASTHEAD += "***********************************************************************\n"
  34. def parse_args(args):
  35. parser = argparse.ArgumentParser(description="A collection of various utilities for GWAS summary statistics.")
  36. parent_parser = argparse.ArgumentParser(add_help=False)
  37. parent_parser.add_argument("--log", type=str, default=None, help="filename for the log file. Default is <out>.log")
  38. parent_parser.add_argument("--log-append", action="store_true", default=False, help="append to existing log file. Default is to erase previous log file if it exists.")
  39. subparsers = parser.add_subparsers()
  40. # 'csv' utility : load raw summary statistics file and convert it into a standardized format
  41. parser_csv = subparsers.add_parser("csv", parents=[parent_parser],
  42. help='Load raw summary statistics file and convert it into a standardized format: '
  43. 'tab-separated file with standard column names, standard chromosome labels, NA label for missing data, etc. '
  44. 'The conversion does not change the number of lines in the input files (e.g. no filtering is done on markers). '
  45. 'Unrecognized columns are removed from the summary statistics file. '
  46. 'The remaining utilities in sumstats.py work with summary statistics files in the standardized format.')
  47. parser_csv.add_argument("--sumstats", type=str, default='-',
  48. help="Raw input file with summary statistics. "
  49. "Default is '-', e.i. to read from sys.stdin (input pipe).")
  50. parser_csv.add_argument("--out", type=str, default='-',
  51. help="File to output the result. "
  52. "Default is '-', e.i. to write to sys.stdout (output pipe).")
  53. parser_csv.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  54. # Generate parameters from describe_cname.
  55. # Keys in describe_cname will be used as standard columns names for the resulting csv file.
  56. for cname in sorted(cols._asdict()):
  57. parser_csv.add_argument("--{}".format(cname.lower()), default=None, type=str, help=describe_cname[cname])
  58. parser_csv.add_argument("--auto", action="store_true", default=False,
  59. help="Auto-detect column types based on a set of standard column names.")
  60. parser_csv.add_argument("--ignore", type=str, nargs='+',
  61. help="List of column names in the original file to ignore during auto-detection")
  62. parser_csv.add_argument("--chunksize", default=100000, type=int,
  63. help="Size of chunk to read the file.")
  64. parser_csv.add_argument("--head", default=0, type=int,
  65. help="How many header lines of the file to print out for visual inspection (0 to disable)")
  66. parser_csv.add_argument("--preview", default=0, type=int,
  67. help="How many chunks to output into the output (debug option to preview large files that take long time to parse)")
  68. parser_csv.add_argument("--skip-validation", action="store_true", default=False,
  69. help="Skip validation of the resulting csv file")
  70. parser_csv.add_argument("--sep", default='\s+', type=str, choices=[',', ';', '\t', ' '],
  71. help="Delimiter to use (',' ';' $' ' or $'\\t'). By default uses delim_whitespace option in pandas.read_csv.")
  72. parser_csv.add_argument("--na-values", type=str, nargs='+',
  73. help="Additional strings to recognize as NA/NaN.")
  74. parser_csv.add_argument("--all-snp-info-23-and-me", default=None, type=str,
  75. help="all_snp_info file for summary stats in 23-and-me format")
  76. parser_csv.add_argument("--qc-23-and-me", action="store_true", default=False,
  77. help="QC 23andMe summary stats (exclude SNPs with 'N' in 'pass' column")
  78. parser_csv.add_argument("--n-val", default=None, type=float,
  79. help="Sample size. If this option is not set, will try to infer the sample "
  80. "size from the input file. If the input file contains a sample size "
  81. "column, and this flag is set, the argument to this flag has priority.")
  82. parser_csv.add_argument("--ncase-val", default=None, type=float,
  83. help="Number of cases. If this option is not set, will try to infer the number "
  84. "of cases from the input file. If the input file contains a number of cases "
  85. "column, and this flag is set, the argument to this flag has priority.")
  86. parser_csv.add_argument("--ncontrol-val", default=None, type=float,
  87. help="Number of controls. If this option is not set, will try to infer the number "
  88. "of controls from the input file. If the input file contains a number of controls "
  89. "column, and this flag is set, the argument to this flag has priority.")
  90. parser_csv.add_argument("--header", default=None, type=str,
  91. help="Whitespace-delimited list of column names. "
  92. "This could be used for input files without column names.")
  93. parser_csv.add_argument("--keep-cols", nargs='*', default=[],
  94. help="List of non-standard column names from --sumstats file to keep in --out file. Columns names will UPPERCASEed.")
  95. parser_csv.add_argument("--keep-all-cols", action="store_true", default=False,
  96. help="Keep all non-standard column names from --sumstats file to keep in --out file. Columns names will UPPERCASEed.")
  97. parser_csv.add_argument("--output-cleansumstats-meta", action="store_true", default=False,
  98. help="Instead of converting the file output meta-information file for https://github.com/BioPsyk/cleansumstats/")
  99. parser_csv.set_defaults(func=make_csv)
  100. # 'variantid' utility : load raw summary statistics file and convert it into a standardized format
  101. parser_variantid = subparsers.add_parser("variantid", parents=[parent_parser],
  102. help='Add VARIANT_ID column, with CHR:BP:A1:A2, where A1 and A2 codes are taking from the reference ')
  103. parser_variantid.add_argument("--sumstats", type=str, default='-',
  104. help="Raw input file with summary statistics. "
  105. "Default is '-', e.i. to read from sys.stdin (input pipe).")
  106. parser_variantid.add_argument("--out", type=str, default='-',
  107. help="File to output the result. "
  108. "Default is '-', e.i. to write to sys.stdout (output pipe).")
  109. parser_variantid.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  110. parser_variantid.add_argument("--ref", type=str, help="[required] Tab-separated file with list of referense SNPs.")
  111. parser_variantid.set_defaults(func=make_variantid)
  112. # 'qc' utility: miscellaneous quality control and filtering procedures
  113. parser_qc = subparsers.add_parser("qc", parents=[parent_parser],
  114. help="Miscellaneous quality control and filtering procedures")
  115. parser_qc.add_argument("--sumstats", type=str, default='-',
  116. help="Raw input file with summary statistics. "
  117. "Default is '-', e.i. to read from sys.stdin (input pipe).")
  118. parser_qc.add_argument("--out", type=str, default='-',
  119. help="[required] File to output the result. "
  120. "Default is '-', e.i. to write to sys.stdout (output pipe).")
  121. parser_qc.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  122. parser_qc.add_argument("--exclude-ranges", type=str, nargs='+',
  123. help='Exclude SNPs in ranges of base pair position, for example MHC. '
  124. 'The syntax is chr:from-to, for example 6:25000000-35000000. Multiple regions can be excluded. Require CHR and BP columns in sumstats file. ')
  125. parser_qc.add_argument("--dropna-cols", type=str, nargs='+',
  126. help='List of column names. SNPs with missing values in either of the columns will be excluded.')
  127. parser_qc.add_argument("--fix-dtype-cols", type=str, nargs='+',
  128. help='List of column names. Ensure appropriate data type for the columns (CHR, BP - int, PVAL - float, etc)')
  129. parser_qc.add_argument("--require-cols", type=str, nargs='+',
  130. help='List of column names to require in the input. '
  131. 'Adding "EFFECT"" to the list will have a special meaning: at least one of BETA, OR, LOGODDS, Z columns must be present in the input.')
  132. parser_qc.add_argument("--maf", type=float, default=None,
  133. help='filters out all variants with minor allele frequency below the provided threshold'
  134. 'This parameter is ignored when FRQ column is not present in the sumstats file. ')
  135. parser_qc.add_argument("--info", type=float, default=None,
  136. help='filters out all variants with imputation INFO score below the provided threshold'
  137. 'This parameter is ignored when INFO column is not present in the sumstats file. ')
  138. parser_qc.add_argument("--qc-substudies", default=False, action="store_true",
  139. help='filters out variants that pass QC and imputation is less than half of all studies in the meta-analysis '
  140. '(i.e. "?" was seen in more than 1/2*N_studies in the METAL "direction" column)'
  141. 'This parameter is ignored when DIRECTION column is not present in the sumstats file. ')
  142. parser_qc.add_argument("--max-or", type=float, default=None,
  143. help='Filter SNPs with OR exceeding threshold. Also applies to OR smaller than the inverse value of the threshold, '
  144. 'e.i. for --max-or 25 this QC procedure will exclude SNPs with OR above 25 and below 1/25. '
  145. '--max-or values below 1 are also acceptable (they will be inverted). '
  146. 'This parameter is ignored when OR column is not present in the sumstats file. ')
  147. parser_qc.add_argument("--min-pval", type=float, default=None,
  148. help='Filter SNPs with p-value below given threshold (for example, to exclude genome-wide significant SNPs from analysis')
  149. parser_qc.add_argument("--update-z-col-from-beta-and-se", action="store_true", default=False,
  150. help='Create or update Z score column from BETA and SE columns. This parameter is ignored when BETA or SE columns are not present in the sumstats file. ')
  151. parser_qc.add_argument("--snps-only", action="store_true", default=False,
  152. help="excludes all variants with one or more multi-character allele codes. Require A1 and A2 columns in sumstats file. ")
  153. parser_qc.add_argument("--just-acgt", action="store_true",
  154. help="similar to --snps-only, but variants with single-character allele codes outside of {'A', 'C', 'G', 'T' } are also excluded. Require A1 and A2 columns in sumstats file. ")
  155. parser_qc.add_argument("--drop-strand-ambiguous-snps", action="store_true", default=False,
  156. help="excludes strand ambiguous SNPs (AT, CG). Require A1 and A2 columns in sumstats file. ")
  157. parser_qc.add_argument("--just-rs-variants", action="store_true", default=False,
  158. help="keeps only variants with an RS number. Require SNP column in sumstats file. ")
  159. parser_qc.set_defaults(func=make_qc)
  160. # 'zscore' utility: calculate z-score from p-value column and effect size column
  161. parser_zscore = subparsers.add_parser("zscore", parents=[parent_parser],
  162. help="Calculate z-score from p-value column and effect size column")
  163. parser_zscore.add_argument("--sumstats", type=str, help="[REQUIRED] Raw input file with summary statistics. ")
  164. parser_zscore.add_argument("--out", type=str, default='-',
  165. help="[required] File to output the result. "
  166. "Default is '-', e.i. to write to sys.stdout (output pipe).")
  167. parser_zscore.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  168. parser_zscore.add_argument("--effect", default=None, type=str, choices=['BETA', 'OR', 'Z', 'LOGODDS'],
  169. help="Effect column. Default is to auto-detect. In case if multiple effect columns are present in the input file"
  170. " a warning will be shown and the first column is taken according to the priority list: "
  171. "Z (highest priority), BETA, OR, LOGODDS (lowest priority).")
  172. parser_zscore.add_argument("--a1-inc", action="store_true", default=False,
  173. help='A1 is the increasing risk (effect) allele.')
  174. parser_zscore.add_argument("--chunksize", default=100000, type=int,
  175. help="Size of chunk to read the file.")
  176. parser_zscore.set_defaults(func=make_zscore)
  177. # 'pvalue' utility: calculate p-value column from 'z'' or 'beta'/'se' columns
  178. parser_pvalue = subparsers.add_parser("pvalue", parents=[parent_parser],
  179. help="Calculate p-value column from 'z'' or 'beta'/'se' columns")
  180. parser_pvalue.add_argument("--sumstats", type=str, help="[REQUIRED] Raw input file with summary statistics. ")
  181. parser_pvalue.add_argument("--out", type=str, default='-',
  182. help="[required] File to output the result. "
  183. "Default is '-', e.i. to write to sys.stdout (output pipe).")
  184. parser_pvalue.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  185. parser_pvalue.add_argument("--chunksize", default=100000, type=int,
  186. help="Size of chunk to read the file.")
  187. parser_pvalue.set_defaults(func=make_pvalue)
  188. # 'beta' utility: calculate BETA column from OR and LOGODDS columns
  189. parser_beta = subparsers.add_parser("beta", parents=[parent_parser],
  190. help="Calculate BETA column from OR and LOGODDS columns")
  191. parser_beta.add_argument("--sumstats", type=str, help="[REQUIRED] Raw input file with summary statistics. ")
  192. parser_beta.add_argument("--out", type=str, default='-',
  193. help="[required] File to output the result. "
  194. "Default is '-', e.i. to write to sys.stdout (output pipe).")
  195. parser_beta.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  196. parser_beta.set_defaults(func=make_beta)
  197. # 'mat' utility: load summary statistics into matlab format
  198. parser_mat = subparsers.add_parser("mat", parents=[parent_parser], help="Create mat files that can "
  199. "be used as an input for cond/conj FDR and for CM3 model. "
  200. "Takes csv files (created with the csv task of this script). "
  201. "Require columns: SNP, P, and one of the signed summary statistics columns (BETA, OR, Z, LOGODDS). "
  202. "Creates corresponding mat files which can be used as an input for the conditional fdr model. "
  203. "Only SNPs from the reference file are considered. Zscores of strand ambiguous SNPs are set to NA. "
  204. "To use CHR:POS for merging summary statistics with reference file consider 'rs' utility "
  205. "which auguments summary statistics with SNP column (first run 'sumstats.py rs ...', "
  206. "then feed the resulting file into sumstats.py mat ...)")
  207. parser_mat.add_argument("--sumstats", type=str, default='-',
  208. help="Raw input file with summary statistics. "
  209. "Default is '-', e.i. to read from sys.stdin (input pipe).")
  210. parser_mat.add_argument("--ref", type=str, help="[required] Tab-separated file with list of referense SNPs.")
  211. parser_mat.add_argument("--out", type=str, help="[required] File to output the result. File should end with .mat extension.")
  212. parser_mat.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  213. parser_mat.add_argument("--keep-cols", nargs='*', choices=cols._fields,
  214. default=[], type=lambda col: col.upper(), metavar='COLUMN_NAME',
  215. help="Columns from csv file to keep in mat file")
  216. parser_mat.add_argument("--keep-all-cols", action="store_true",
  217. default=False, help="Keep all columns from cvs file in mat file, except SNP, CHR, BP, A1 and A2")
  218. parser_mat.add_argument("--trait", type=str, default='',
  219. help="Trait name that will be used in mat file. Can be kept empty, in this case the variables will be named 'logpvec', 'zvec' and 'nvec'")
  220. parser_mat.add_argument("--ignore-alleles", action="store_true", default=False,
  221. help="Load summary stats file ignoring alleles (only 'logpvec' is created in this case, entire 'zvec' is set to nan).")
  222. parser_mat.add_argument("--without-n", action="store_true", default=False,
  223. help="Proceed without sample size (N or NCASE/NCONTROL)")
  224. parser_mat.add_argument("--chunksize", default=100000, type=int,
  225. help="Size of chunk to read the file.")
  226. parser_mat.set_defaults(func=make_mat)
  227. # 'lift' utility: lift RS numbers to a newer version of SNPdb, and/or liftover chr:pos to another genomic build using UCSC chain files
  228. parser_lift = subparsers.add_parser("lift", parents=[parent_parser],
  229. help="Lift RS numbers to a newer version of SNPdb, "
  230. "and/or liftover chr:pos to another genomic build using UCSC chain files. "
  231. "WARNING: this utility may use excessive amount of memory (up and beyong 32 GB of RAM).")
  232. parser_lift.add_argument("--sumstats", type=str, default='-',
  233. help="Raw input file with summary statistics. "
  234. "Default is '-', e.i. to read from sys.stdin (input pipe).")
  235. parser_lift.add_argument("--out", type=str, default='-',
  236. help="File to output the result. "
  237. "Default is '-', e.i. to write to sys.stdout (output pipe).")
  238. parser_lift.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  239. parser_lift.add_argument("--chain-file", default=None, type=str,
  240. help="Chain file to use for CHR:BP conversion between genomic builds")
  241. parser_lift.add_argument("--snp-chrpos", default=None, type=str,
  242. help="NCBI SNPChrPosOnRef file.")
  243. parser_lift.add_argument("--snp-history", default=None, type=str,
  244. help="NCBI SNPHistory file.")
  245. parser_lift.add_argument("--rs-merge-arch", default=None, type=str,
  246. help="NCBI RsMergeArch file.")
  247. parser_lift.add_argument("--keep-bad-snps", action="store_true", default=False,
  248. help="Keep SNPs with undefined rs# number or CHR:POS location in the output file.")
  249. parser_lift.add_argument("--na-rep", default='NA', type=str, choices=['NA', ''],
  250. help="Missing data representation.")
  251. parser_lift.add_argument("--gzip", action="store_true", default=False,
  252. help="A flag indicating whether to compress the resulting file with gzip.")
  253. parser_lift.set_defaults(func=make_lift)
  254. # 'clump' utility: clump summary stats, produce lead SNP report, produce candidate SNP report
  255. parser_clump = subparsers.add_parser("clump", parents=[parent_parser],
  256. help="""Perform LD-based clumping of summary stats. This works similar to FUMA snp2gene functionality (http://fuma.ctglab.nl/tutorial#snp2gene).
  257. Step 1. Re-save summary stats into one file for each chromosome.
  258. Step 2a Use 'plink --clump' to find independent significant SNPs (default r2=0.6)
  259. Step 2b Use 'plink --clump' to find lead SNPs, by clumping independent significant SNPs (default r2=0.1)
  260. Step 3. Use 'plink --ld' to find genomic loci around each independent significant SNP (default r2=0.6)
  261. Step 4. Merge together genomic loci which are closer than certain threshold (250 KB)
  262. Step 5. Merge together genomic loci that fall into exclusion regions, such as MHC
  263. Step 6. Output genomic loci report, indicating lead SNPs for each loci
  264. Step 7. Output candidate SNP report""")
  265. parser_clump.add_argument("--sumstats", type=str, help="Input file with summary statistics")
  266. parser_clump.add_argument("--out", type=str, help="[required] File to output the result.")
  267. parser_clump.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  268. parser_clump.add_argument("--chr-labels", type=str, nargs='+',
  269. help="List of chromosome labels to substitute for @, default to 1..22")
  270. parser_clump.add_argument("--clump-field", type=str, default='PVAL', help="Column to clump on.")
  271. parser_clump.add_argument("--clump-snp-field", type=str, default='SNP', help="Column with marker name.")
  272. parser_clump.add_argument("--chr", type=str, default='CHR', help="Column name with chromosome labels. ")
  273. parser_clump.add_argument("--indep-r2", type=float, default=0.6, help="LD r2 threshold for clumping independent significant SNPs.")
  274. parser_clump.add_argument("--lead-r2", type=float, default=0.1, help="LD r2 threshold for clumping lead SNPs.")
  275. parser_clump.add_argument("--clump-p1", type=float, default=5e-8, help="p-value threshold for independent significant SNPs.")
  276. parser_clump.add_argument("--bfile-chr", type=str,
  277. help="prefix for plink .bed/.bim/.fam file. Will automatically concatenate .bed/.bim/.fam files split across 22 chromosomes. "
  278. "If the filename prefix contains the symbol @, sumstats.py will replace the @ symbol with chromosome numbers. "
  279. "Otherwise, sumstats.py will append chromosome numbers to the end of the filename prefix. ")
  280. parser_clump.add_argument("--ld-window-kb", type=float, default=10000, help="Window size in KB to search for clumped SNPs. ")
  281. parser_clump.add_argument("--loci-merge-kb", type=float, default=250, help="Maximum distance in KB of LD blocks to merge. ")
  282. parser_clump.add_argument("--exclude-ranges", type=str, nargs='+',
  283. help='Exclude SNPs in ranges of base pair position, for example MHC. '
  284. 'The syntax is chr:from-to, for example 6:25000000-35000000. Multiple regions can be excluded.')
  285. parser_clump.add_argument("--plink", type=str, default='plink', help="Path to plink executable.")
  286. parser_clump.add_argument("--sumstats-chr", type=str, help="Input file with summary statistics, one file per chromosome")
  287. parser_clump.set_defaults(func=make_clump)
  288. # 'rs' utility: augument summary statistic file with SNP RS number from reference file
  289. parser_rs = subparsers.add_parser("rs", parents=[parent_parser],
  290. help="Augument summary statistic file with SNP RS number from reference file. "
  291. "Merging is done on chromosome and position. If SNP column already exists in --sumstats file, it will be overwritten.")
  292. parser_rs.add_argument("--sumstats", type=str, help="[required] Input file with summary statistics in standardized format")
  293. parser_rs.add_argument("--ref", type=str, help="[required] Tab-separated file with list of referense SNPs.")
  294. parser_rs.add_argument("--out", type=str, help="[required] File to output the result.")
  295. parser_rs.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  296. parser_rs.add_argument("--a1a2", action="store_true", default=False,
  297. help="Add A1 and A2 columns from the reference file. "
  298. "Existing A1 and/or A2 columns in --sumstats file will be overwritten.")
  299. parser_rs.add_argument("--chunksize", default=100000, type=int,
  300. help="Size of chunk to read the file.")
  301. parser_rs.set_defaults(func=make_rs)
  302. # 'ls' utility: display information about columns of a standardized summary statistics file
  303. parser_ls = subparsers.add_parser("ls", parents=[parent_parser],
  304. help="Report information about standard sumstat files, "
  305. "including the set of columns available, number of SNPs, etc.")
  306. parser_ls.add_argument("--path", type=str, help="[required] File or regular expresion of the files to include in the report.")
  307. parser_ls.add_argument("--out", type=str, help="[required] File to output the result.")
  308. parser_ls.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  309. parser_ls.set_defaults(func=make_ls)
  310. # 'mat-to-csv' utility: convert matlab .mat file with logpvec and zvec into CSV files
  311. parser_mattocsv = subparsers.add_parser("mat-to-csv", parents=[parent_parser],
  312. help="Convert matlab .mat file with logpvec, zvec and (optionally) nvec into CSV files.")
  313. parser_mattocsv.add_argument("--mat", type=str, help="[required] Input mat file.")
  314. parser_mattocsv.add_argument("--ref", type=str, help="[required] Tab-separated file with list of referense SNPs.")
  315. parser_mattocsv.add_argument("--out", type=str, help="[required] File to output the result.")
  316. parser_mattocsv.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  317. parser_mattocsv.add_argument("--na-rep", default='NA', type=str, choices=['NA', ''],
  318. help="Missing data representation.")
  319. parser_mattocsv.add_argument("--gzip", action="store_true", default=False,
  320. help="A flag indicating whether to compress the resulting file with gzip.")
  321. parser_mattocsv.set_defaults(func=mat_to_csv)
  322. # 'ldsc-to-mat' utility: convert data from LD score regression formats to .mat files
  323. parser_ldsctomat = subparsers.add_parser("ldsc-to-mat", parents=[parent_parser],
  324. help="Convert .sumstats, .ldscore, .M, .M_5_50 and "
  325. "binary .annot files from LD score regression to .mat files.")
  326. parser_ldsctomat.add_argument("--ref", type=str, help="[required] Tab-separated file with list of referense SNPs.")
  327. parser_ldsctomat.add_argument("--out", type=str, help="[required] File to output the result.")
  328. parser_ldsctomat.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  329. parser_ldsctomat.add_argument("--sumstats", type=str, default=None,
  330. help="Name of .sumstats.gz file")
  331. parser_ldsctomat.add_argument("--ldscore", type=str, default=None,
  332. help="Name of .ldscore.gz files, where symbol @ indicates chromosome index. Example: baseline.@.l2.ldscore.gz")
  333. parser_ldsctomat.add_argument("--annot", type=str, default=None,
  334. help="Name of .annot.gz files, where symbol @ indicates chromosome index. Example: baseline.@.annot.gz")
  335. parser_ldsctomat.add_argument("--M", type=str, default=None,
  336. help="Name of .M files, where symbol @ indicates chromosome index. Example: baseline.@.l2.M")
  337. parser_ldsctomat.add_argument("--M-5-50", type=str, default=None,
  338. help="Name of .M_5_50 files, where symbol @ indicates chromosome index. Example: baseline.@.l2.M_5_50")
  339. parser_ldsctomat.add_argument("--chr-labels", type=str, nargs='+',
  340. help="List of chromosome labels to substitute for @, default to 1..22")
  341. parser_ldsctomat.set_defaults(func=ldsc_to_mat)
  342. # 'frq-to-mat' utility: convert allele frequency from FRQ plink format to .mat files
  343. parser_frqtomat = subparsers.add_parser("frq-to-mat", parents=[parent_parser],
  344. help="Convert .frq files plink from .mat files.")
  345. parser_frqtomat.add_argument("--ref", type=str, help="Tab-separated file with list of referense SNPs.")
  346. parser_frqtomat.add_argument("--out", type=str, help="[required] File to output the result.")
  347. parser_frqtomat.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  348. parser_frqtomat.add_argument("--frq", type=str, default=None,
  349. help="Name of .frq files, where symbol @ indicates chromosome index. Example: 1000G.EUR.QC.@.frq")
  350. parser_frqtomat.add_argument("--afreq", type=str, default=None,
  351. help="Name of .afreq files, where symbol @ indicates chromosome index. Example: 1000G.EUR.QC.@.afreq")
  352. parser_frqtomat.add_argument("--chr-labels", type=str, nargs='+',
  353. help="List of chromosome labels to substitute for @, default to 1..22")
  354. parser_frqtomat.set_defaults(func=frq_to_mat)
  355. # 'ref-to-mat' utility: convert reference files to .mat files
  356. parser_reftomat = subparsers.add_parser("ref-to-mat", parents=[parent_parser],
  357. help="Convert reference files to .mat files.")
  358. parser_reftomat.add_argument("--ref", type=str, help="Tab-separated file with list of referense SNPs.")
  359. parser_reftomat.add_argument("--out", type=str, help="[required] File to output the result.")
  360. parser_reftomat.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  361. parser_reftomat.add_argument("--numeric-only", action="store_true", default=False, help="Save only numeric data (CHR, BP, GP), skip all other column (A1, A2, SNP).")
  362. parser_reftomat.set_defaults(func=ref_to_mat)
  363. # 'ldsum' utility: convert plink .ld.gz files (pairwise ld r2) to ld scores
  364. parser_ldsum = subparsers.add_parser("ldsum", parents=[parent_parser],
  365. help="convert plink .ld.gz files (pairwise ld r2) to ld scores")
  366. parser_ldsum.add_argument("--bim", type=str, help="[required] plink bim file")
  367. parser_ldsum.add_argument("--ld", type=str, help="[required] plink .ld file")
  368. parser_ldsum.add_argument("--out", type=str, help="[required] File to output the result.")
  369. parser_ldsum.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  370. parser_ldsum.add_argument('--r2-min', default=None, type=float, nargs='+',
  371. help='Lower bound (exclusive) of r2 to consider in ld score estimation. '
  372. 'Should be used in conjunction with --r2-max. '
  373. 'Intended usage of this parameter is to create a binned histogram of l2 or l4 values, for example: '
  374. '"--r2-min 0.00 0.25 0.50 0.75 --r2-max 0.25 0.50 0.75 1.00". '
  375. 'Normally --r2-min and --r2-max should cover the range from 0 to 1. '
  376. 'In case of --per-allele flag, --r2-min and --r2-max thresholds apply to the product of allelic correlation and heterozigosity, '
  377. 'e.i. to r2_{jk} * 2*p_k*(1-p_k), where p_k denotes the MAF of SNP k. '
  378. 'To produce a complete histogram in case of --per-allele flag one must use --r2-min and --r2-max that cover the range from 0 to 0.5. ')
  379. parser_ldsum.add_argument('--r2-max', default=None, type=float, nargs='+',
  380. help='Upper bound (inclusive) of r2 to consider in ld score estimation. '
  381. 'See description of --r2-min option for additional details. ')
  382. parser_ldsum.add_argument('--per-allele', default=False, action='store_true',
  383. help='Setting this flag causes sumstats.py to compute per-allele LD Scores, '
  384. 'i.e., '
  385. '\ell2_j := \sum_k 2*p_k(1-p_k) r^2_{jk}, and '
  386. '\ell4_j := \sum_k (2*p_k(1-p_k))^2 r^4_{jk}, '
  387. 'where p_k denotes the MAF of SNP k. '
  388. 'Require --frq parameter to be specified. ')
  389. parser_ldsum.add_argument("--frq", type=str, default=None, help="Name of the .frq file.")
  390. parser_ldsum.add_argument('--not-diag', default=False, action='store_true',
  391. help='sumstats.py assume that plink-generated --ld file does not have diagonal elements, '
  392. 'e.i. does not contain LD r2 entries of 1.0 for variant r2 with itself. '
  393. 'By default sumstats.py adds such diagonal entries to the LD score. '
  394. 'Setting --not-diag flag causes sumstats.py to NOT to add the diagonal elements. ')
  395. parser_ldsum.add_argument("--chunksize", default=10000000, type=int,
  396. help="Size of chunk to read the --ld file.")
  397. parser_ldsum.set_defaults(func=ldsum)
  398. # 'diff-mat' utility: compare two .mat files with logpvec, zvec and nvec, and report the differences
  399. parser_diffmat = subparsers.add_parser("diff-mat", parents=[parent_parser],
  400. help="Compare two .mat files with logpvec, zvec and nvec, "
  401. "and report the differences.")
  402. parser_diffmat.add_argument("--mat1", type=str, default=None, help="[required] Name of the first .mat file")
  403. parser_diffmat.add_argument("--mat2", type=str, default=None, help="[required] Name of the second .mat file")
  404. parser_diffmat.add_argument("--ref", type=str, help="[required] Tab-separated file with list of referense SNPs.")
  405. parser_diffmat.add_argument("--out", type=str, help="[required] File to output the result.")
  406. parser_diffmat.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  407. parser_diffmat.add_argument("--sumstats", type=str, default=None,
  408. help="Optionally, the name of the source summary statistics file in standardized .csv format. "
  409. "Assuming that both .mat files originate from this file diff-mat will produce output file "
  410. "to help investigate where the differences came from.")
  411. parser_diffmat.set_defaults(func=diff_mat)
  412. # 'neff' utility: generate N column from NCASE and NCONTROL
  413. parser_neff = subparsers.add_parser("neff", parents=[parent_parser],
  414. help="generate N column from NCASE and NCONTROL, as 4 / (1 / NCASE + 1 / NCONTROL)")
  415. parser_neff.add_argument("--sumstats", type=str, default='-',
  416. help="Raw input file with summary statistics. "
  417. "Default is '-', e.i. to read from sys.stdin (input pipe).")
  418. parser_neff.add_argument("--out", type=str, default='-',
  419. help="[required] File to output the result. "
  420. "Default is '-', e.i. to write to sys.stdout (output pipe).")
  421. parser_neff.add_argument("--force", action="store_true", default=False, help="Allow sumstats.py to overwrite output file if it exists.")
  422. parser_neff.add_argument("--drop", action="store_true", default=False, help="Drop NCASE and NCONTROL columns.")
  423. parser_neff.add_argument("--factor", default=4, type=float,
  424. help="Factor in the numerator of the NEFF formula. Default to 4. Sometimes you may want FACTOR=2. Set FACTOR=0 if you want NEFF = NCASE + NCONTROL.")
  425. parser_neff.set_defaults(func=make_neff)
  426. return parser.parse_args(args)
  427. ### =================================================================================
  428. ### Implementation for parser_csv
  429. ### =================================================================================
  430. def set_clean_args_cnames(args):
  431. """
  432. Inspect column names in user args, and clean them according to sumstats_utils.clean_header()
  433. Raises an exception if either before or after cleaning some column names are duplicated.
  434. """
  435. cnames = [x.lower() for x in cols._asdict()]
  436. args_cnames = [args[x] for x in cnames if args[x] is not None]
  437. if len(args_cnames) != len(set(args_cnames)):
  438. raise(ValueError('Duplicated argument: {}'.format(find_duplicates(args_cnames))))
  439. for cname in cnames:
  440. if args[cname] is not None:
  441. args[cname] = clean_header(args[cname])
  442. args_clean_cnames = [args[x] for x in cnames if args[x] is not None]
  443. if len(args_clean_cnames) != len(set(args_clean_cnames)):
  444. raise(ValueError('Cleaning rules yield duplicated argument: {}'.format(find_duplicates(args_clean_cnames))))
  445. def set_clean_file_cnames(df):
  446. """
  447. Inspect column names in pd.DataFrame, and clean them according to sumstats_utils.clean_header()
  448. Raises an exception if either before or after cleaning some column names are duplicated.
  449. """
  450. file_cnames = df.columns
  451. if len(file_cnames) != len(set(file_cnames)):
  452. raise(ValueError('Unable to process input file due to duplicated column names'))
  453. clean_file_cnames = [clean_header(x) for x in file_cnames]
  454. if len(clean_file_cnames) != len(set(clean_file_cnames)):
  455. raise(ValueError('Cleaning column names resulted in duplicated column names: {}'.format(clean_file_cnames)))
  456. df.columns = clean_file_cnames
  457. def find_duplicates(values):
  458. return [item for item, count in collections.Counter(values).items() if count > 1]
  459. def find_auto_cnames(args, clean_file_cnames):
  460. """
  461. Auto-detect column using a set of default columns names
  462. """
  463. cnames = [x.lower() for x in cols._asdict()]
  464. user_args = [args[x] for x in cnames if args[x] is not None]
  465. for default_cname, cname in default_cnames.items():
  466. # Ignore default cname if it is explicitly provided by the user
  467. if (cname.lower() not in args) or args[cname.lower()]:
  468. continue
  469. # Ignore default cname if it is not present among file columns
  470. if clean_header(default_cname) not in clean_file_cnames:
  471. continue
  472. # Ignore default cname if user took the column for something else
  473. if clean_header(default_cname) in user_args:
  474. continue
  475. # Ignore default cname if user explicitly asked to ignore it
  476. if args['ignore'] and (clean_header(default_cname) in [clean_header(x) for x in args['ignore']]):
  477. continue
  478. args[cname.lower()] = clean_header(default_cname)
  479. def check_input_file(file):
  480. if file == '-':
  481. raise ValueError("sys.stdin is not supported as input file")
  482. if (file != sys.stdin) and not os.path.isfile(file):
  483. raise ValueError("Input file does not exist: {f}".format(f=file))
  484. def check_output_file(file, force=False):
  485. # Delete target file if user specifies --force option
  486. if file == '-':
  487. raise ValueError("sys.stdout is not supported as output file")
  488. if file == sys.stdout:
  489. return
  490. if force:
  491. try:
  492. os.remove(file)
  493. except OSError:
  494. pass
  495. # Otherwise raise an error if target file already exists
  496. if os.path.isfile(file) and not force:
  497. raise ValueError("Output file already exists: {f}".format(f=file))
  498. # Create target folder if it doesn't exist
  499. output_dir = os.path.dirname(file)
  500. if output_dir and not os.path.isdir(output_dir): os.makedirs(output_dir) # ensure that output folder exists
  501. def update_cleansumstats_cols(cleansumstats_cols, cname, original):
  502. if cname == 'CHRPOSA1A2':
  503. cleansumstats_cols.append(('col_CHR', original))
  504. cleansumstats_cols.append(('col_POS', original))
  505. cleansumstats_cols.append(('col_EffectAllele', original))
  506. cleansumstats_cols.append(('col_OtherAllele', original))
  507. return
  508. if cname == 'CHRPOS':
  509. cleansumstats_cols.append(('col_CHR', original))
  510. cleansumstats_cols.append(('col_POS', original))
  511. return
  512. if cname == 'A1A2':
  513. raise(ValueError('--a1a2 not supported for --output-cleansumstats-meta'))
  514. if cname in cname_to_cleansumstats_map:
  515. cleansumstats_cols.append((cname_to_cleansumstats_map[cname], original))
  516. else:
  517. log.log('Warning: {} not supported by --output-cleansumstats-meta'.format(cname))
  518. def get_meta_template():
  519. return '''cleansumstats_metafile_date: '2020-12-31'
  520. cleansumstats_metafile_user: username
  521. cleansumstats_version: 1.0.0-alpha
  522. path_sumStats: {path_sumStats}
  523. {cols_definition}
  524. stats_GCMethod: none
  525. stats_Model: {linear_or_logistic}
  526. stats_Notes: 'dummy description'
  527. stats_TraitType: quantitative
  528. stats_neglog10P: false
  529. study_AccessDate: '2020-12-31'
  530. study_Ancestry: EUR
  531. study_Array: meta
  532. study_FilePortal: http://website.org/dummydata
  533. study_FileURL: http://website.org/dummydata/file.txt.gz
  534. study_Gender: mixed
  535. study_ImputePanel: HapMap
  536. study_ImputeSoftware: meta
  537. study_PMID: 666
  538. study_PhasePanel: meta
  539. study_PhaseSoftware: meta
  540. study_PhenoCode:
  541. - EFO:0000000
  542. study_PhenoDesc: 'phenotype description'
  543. study_Title: dummy_title
  544. study_Use: open
  545. study_Year: 2020
  546. '''
  547. def make_csv(args, log):
  548. """
  549. Based on file with summary statistics creates a tab-delimited csv file with standard columns.
  550. """
  551. if args.sumstats == '-': args.sumstats = sys.stdin
  552. if args.out == '-': args.out = sys.stdout
  553. check_input_file(args.sumstats)
  554. if args.all_snp_info_23_and_me: check_input_file(args.all_snp_info_23_and_me)
  555. set_clean_args_cnames(vars(args))
  556. check_output_file(args.out, args.force)
  557. if args.keep_all_cols and args.keep_cols:
  558. err_msg = ("Misleading input arguments! Use either '--keep-cols' or "
  559. "'--keep-all-cols' option, but not both at a time.")
  560. raise(ValueError(err_msg))
  561. if args.keep_cols is None: args.keep_cols = []
  562. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  563. if (args.head > 0) and (args.sumstats != sys.stdin):
  564. log.log('File header:')
  565. header = get_header(args.sumstats, lines=args.head)
  566. for line in header: log.log(line)
  567. if args.header is None:
  568. reader = pd.read_csv(args.sumstats, dtype=str, sep=args.sep, chunksize=args.chunksize, na_values=args.na_values)
  569. else:
  570. reader = pd.read_csv(args.sumstats, dtype=str, sep=args.sep, chunksize=args.chunksize, na_values=args.na_values, header=None, names=args.header.split())
  571. reader_23_and_me = pd.read_csv(args.all_snp_info_23_and_me, dtype=str, sep=args.sep, chunksize=args.chunksize) if args.all_snp_info_23_and_me else None
  572. n_snps = 0
  573. max_n_val = np.nan; max_ncase_val = np.nan; max_ncontrol_val = np.nan
  574. with (open(args.out, 'a') if args.out != sys.stdout else sys.stdout) as out_f:
  575. for chunk_index, chunk in enumerate(reader):
  576. if reader_23_and_me:
  577. chunk = pd.concat([chunk, next(reader_23_and_me)], axis=1)
  578. chunk = chunk.loc[:, ~chunk.columns.duplicated()]
  579. if args.qc_23_and_me:
  580. chunk = chunk[chunk['pass'] != 'N'].copy()
  581. original_file_cname = chunk.columns
  582. set_clean_file_cnames(chunk)
  583. if chunk_index == 0: # First chunk => analyze column names
  584. if args.auto: find_auto_cnames(vars(args), chunk.columns)
  585. # Find the map from (cleaned) column name to a standard cname (as it will appear in the resulting file)
  586. cnames = [x.lower() for x in cols._asdict()]
  587. cname_map = {vars(args)[cname] : cname.upper() for cname in cnames if vars(args)[cname] is not None}
  588. # Report any missing columns that user requested to put in the resulting file
  589. cname_missing = [x for x in cname_map if x not in chunk.columns]
  590. if cname_missing: raise(ValueError('Columns {} are missing in the input file'.format(cname_missing)))
  591. # Describe the mapping between old and new column names that we are going to perform
  592. log.log('Interpret column names as follows:')
  593. cleansumstats_cols = []
  594. for original in original_file_cname:
  595. cname = cname_map.get(clean_header(original))
  596. if cname and args.output_cleansumstats_meta:
  597. update_cleansumstats_cols(cleansumstats_cols, cname, original)
  598. # note that in --output-cleansumstats-meta mode we do take N/NCASE/NCONTROL columns
  599. # but also allow to specify --n-val / --ncontrol-val / --ncase-val as a meta-data
  600. # this is difference from when we actually produce a .csv file - in this case
  601. # --n-val / --ncontrol-val / --ncase-val OVERWRITES the original sample size column(s)
  602. if (args.ncase_val is not None) and (cname==cols.NCASE):
  603. cleansumstats_cols.append(('stats_CaseN', int(args.ncase_val)))
  604. if not args.output_cleansumstats_meta: cname=None
  605. if (args.ncontrol_val is not None) and (cname==cols.NCONTROL):
  606. cleansumstats_cols.append(('stats_ControlN', int(args.ncontrol_val)))
  607. if not args.output_cleansumstats_meta: cname=None
  608. if (args.n_val is not None) and (cname==cols.N):
  609. cleansumstats_cols.append(('stats_TotalN', int(args.n_val)))
  610. if not args.output_cleansumstats_meta: cname=None
  611. if cname: column_status = describe_cname[cname]
  612. elif args.keep_all_cols or (original.upper() in args.keep_cols): column_status = "Will be kept as unrecognized column"
  613. else: column_status = "Will be deleted"
  614. log.log("\t{o} : {d} ({e})".format(o=original, d=cname, e=column_status))
  615. if not cname_map: raise(ValueError('Arguments imply to delete all columns from the input file. Did you forget --auto flag?'))
  616. final_cols = set(cname_map.values()) # final list of columns in the resulting file
  617. if (cols.CHRPOS not in final_cols) and (cols.CHRPOSA1A2 not in final_cols) and (cols.CHR not in final_cols): log.log('Warning: CHR column ({}) is not found'.format(describe_cname[cols.CHR]))
  618. if (cols.CHRPOS not in final_cols) and (cols.CHRPOSA1A2 not in final_cols) and (cols.BP not in final_cols): log.log('Warning: BP column ({}) is not found'.format(describe_cname[cols.BP]))
  619. if cols.SNP not in final_cols: log.log('Warning: SNP column ({}) is not found'.format(describe_cname[cols.SNP]))
  620. if cols.PVAL not in final_cols: log.log('Warning: PVAL column ({}) is not found'.format(describe_cname[cols.PVAL]))
  621. if (cols.A1 not in final_cols) and (cols.A1A2 not in final_cols) and (cols.CHRPOSA1A2 not in final_cols): log.log('Warning: A1 column ({}) is not found'.format(describe_cname[cols.A1]))
  622. if (cols.A2 not in final_cols) and (cols.A1A2 not in final_cols) and (cols.CHRPOSA1A2 not in final_cols): log.log('Warning: A2 column ({}) is not found'.format(describe_cname[cols.A2]))
  623. effect_size_column_count = int(cols.Z in final_cols) + int(cols.OR in final_cols) + int(cols.BETA in final_cols) + int(cols.LOGODDS in final_cols)
  624. if effect_size_column_count == 0: log.log('Warning: None of the columns indicate effect direction: typically either BETA, OR, LOGODDS or Z column is expected')
  625. if effect_size_column_count > 1: log.log('Warning: Multiple columns indicate effect direction: typically only one of BETA, OR, LOGODDS and Z columns is expected')
  626. if args.output_cleansumstats_meta:
  627. # add a dummy stats_TotalN
  628. if 'stats_TotalN' not in dict(cleansumstats_cols): cleansumstats_cols.append(('stats_TotalN', '1'))
  629. out_f.write(get_meta_template().format(
  630. path_sumStats=os.path.basename(args.sumstats),
  631. cols_definition = '\n'.join(['{}: {}'.format(cname, original) for cname, original in cleansumstats_cols]),
  632. linear_or_logistic = 'logistic' if (cols.OR in final_cols) else 'linear'
  633. ))
  634. break
  635. if not args.keep_all_cols:
  636. chunk.drop([x for x in chunk.columns if ((x not in cname_map) and (x not in args.keep_cols))], axis=1, inplace=True)
  637. chunk.rename(columns=cname_map, inplace=True)
  638. # Split CHR:POS column into two
  639. if cols.CHRPOS in chunk.columns:
  640. chunk[cols.CHR], chunk[cols.BP] = chunk[cols.CHRPOS].str.replace('_', ':').str.split(':', 1).str
  641. chunk.drop(cols.CHRPOS, axis=1, inplace=True)
  642. # Split A1/A2 column into two
  643. if cols.A1A2 in chunk.columns:
  644. chunk[cols.A1], chunk[cols.A2] = chunk[cols.A1A2].str.split('/', 1).str
  645. chunk.drop(cols.A1A2, axis=1, inplace=True)
  646. # Split CHR:POS:A1:A2 column into four
  647. if cols.CHRPOSA1A2 in chunk.columns:
  648. chunk[cols.CHR], chunk[cols.BP], chunk[cols.A1], chunk[cols.A2] = chunk[cols.CHRPOSA1A2].str.replace('_', ':').str.split(':', 3).str
  649. chunk.drop(cols.CHRPOSA1A2, axis=1, inplace=True)
  650. # Validate that EA has the same values as A1, and drop EA:
  651. if cols.EA in chunk.columns:
  652. if np.any(chunk[cols.EA] != chunk[cols.A1]):
  653. raise('EA column does not match A1 column, unable to read summary stats')
  654. chunk.drop(cols.EA, axis=1, inplace=True)
  655. # Ensure standard labels in CHR column
  656. if cols.CHR in chunk.columns:
  657. chunk[cols.CHR].fillna(-9, inplace=True)
  658. chunk[cols.CHR] = format_chr(chunk[cols.CHR])
  659. # Ensure that alleles are coded as capital letters
  660. if cols.A1 in chunk.columns: chunk[cols.A1] = chunk[cols.A1].str.upper().str.strip()
  661. if cols.A2 in chunk.columns: chunk[cols.A2] = chunk[cols.A2].str.upper().str.strip()
  662. # Populate sample size columns (NCASE, NCONTROL, N)
  663. if args.ncase_val is not None: chunk[cols.NCASE] = args.ncase_val
  664. if args.ncontrol_val is not None: chunk[cols.NCONTROL] = args.ncontrol_val
  665. if args.n_val is not None: chunk[cols.N] = args.n_val
  666. # Keep track of the largest NCASE, NCONTROL and N values
  667. if cols.NCASE in chunk: max_ncase_val = np.nanmax([max_ncase_val, chunk[cols.NCASE].astype(float).max()])
  668. if cols.NCONTROL in chunk: max_ncontrol_val = np.nanmax([max_ncontrol_val, chunk[cols.NCONTROL].astype(float).max()])
  669. if cols.N in chunk: max_n_val = np.nanmax([max_n_val, chunk[cols.N].astype(float).max()])
  670. # Swap A1 and A2 for 23andMe, because in 23andMe summary stats effect size is about A2
  671. if args.all_snp_info_23_and_me:
  672. chunk.columns = [(cols.A1 if x==cols.A2 else cols.A2 if x==cols.A1 else x) for x in chunk.columns]
  673. chunk = chunk.sort_index(axis=1)
  674. fix_columns_order(chunk).to_csv(out_f, index=False, header=(chunk_index==0), sep='\t', na_rep='NA')
  675. n_snps += len(chunk)
  676. eprint("{f}: {n} lines processed".format(f=args.sumstats, n=(chunk_index+1)*args.chunksize))
  677. if args.preview and (chunk_index+1) >= args.preview:
  678. log.log('Abort reading input file due to --preview flag.')
  679. break
  680. if not args.output_cleansumstats_meta:
  681. if n_snps == 0: raise(ValueError('Input summary stats file appears to be empty.'))
  682. log.log("Done. {n} SNPs saved to {f}".format(n=n_snps, f=args.out))
  683. log.log('Sample size: N={} NCASE={} NCONTROL={}'.format(max_n_val, max_ncase_val, max_ncontrol_val))
  684. if (not args.skip_validation) and (not args.output_cleansumstats_meta) and (args.out != sys.stdout):
  685. log.log('Validate the resulting file...')
  686. reader = pd.read_csv(args.out, sep='\t', chunksize=args.chunksize)
  687. n_snps = 0
  688. for chunk_index, chunk in enumerate(reader):
  689. if chunk_index==0:
  690. log.log('Column types: ' + ', '.join([column + ':' + str(dtype) for (column, dtype) in zip(chunk.columns, chunk.dtypes)]))
  691. n_snps += len(chunk)
  692. log.log("Done. {n} SNPs read from {f}".format(n=n_snps, f=args.out))
  693. ### =================================================================================
  694. ### Implementation for parser_qc
  695. ### =================================================================================
  696. def describe_sample_size(sumstats, log):
  697. log.log('Sample size N={} NCASE={} NCONTROL={}'.format(
  698. sumstats[cols.N].max() if cols.N in sumstats else np.nan,
  699. sumstats[cols.NCASE].max() if cols.NCASE in sumstats else np.nan,
  700. sumstats[cols.NCONTROL].max() if cols.NCONTROL in sumstats else np.nan))
  701. def drop_sumstats(sumstats, log, reason, drop_labels=None, dropna_subset=None):
  702. '''Drop labels from sumstats, and log the number of excluded rows.'''
  703. sumstats_len = len(sumstats)
  704. if drop_labels is not None:
  705. sumstats.drop(drop_labels, inplace=True)
  706. if dropna_subset is not None:
  707. sumstats.dropna(subset=dropna_subset, inplace=True)
  708. log.log('Drop {} markers ({})'.format(sumstats_len - len(sumstats), reason))
  709. def make_qc(args, log):
  710. if args.sumstats == '-': args.sumstats = sys.stdin
  711. if args.out == '-': args.out = sys.stdout
  712. check_input_file(args.sumstats)
  713. check_output_file(args.out, args.force)
  714. if (args.max_or is not None) and (args.max_or <= 0): raise(ValueError('--max-or value must not be negative'))
  715. if (args.maf is not None) and ((args.maf < 0) or (args.maf > 1)): raise(ValueError('--maf value must be between 0 and 1'))
  716. if (args.info is not None) and (args.info < 0): raise(ValueError('--info value must not be negative'))
  717. if (args.min_pval is not None) and ((args.min_pval < 0) or (args.min_pval > 1)): raise(ValueError('--min-pval value be between 0 and 1'))
  718. if (args.max_or is not None) and (args.max_or < 1):
  719. log.log('--max-or was changed from {} to {}'.format(args.max_or, 1/args.max_or))
  720. args.max_or = 1 / args.max_or
  721. if args.dropna_cols is None: args.dropna_cols = []
  722. if args.fix_dtype_cols is None: args.fix_dtype_cols = []
  723. if args.require_cols is None: args.require_cols = []
  724. # Read summary sumstats file...
  725. log.log('Reading sumstats file {}...'.format(args.sumstats))
  726. sumstats = pd.read_csv(args.sumstats, sep='\t', dtype=str)
  727. exclude_ranges = make_ranges(args.exclude_ranges, log)
  728. log.log("Sumstats file contains {d} markers.".format(d=len(sumstats)))
  729. if (args.exclude_ranges is not None) and (('BP' not in sumstats) or ('CHR' not in sumstats)):
  730. log.log('Warning: skip --exclude-ranges ("BP" and/or "CHR" columns not found in {})'.format(args.sumstats))
  731. args.exclude_ranges = None
  732. missing_dropna_cols = [x for x in args.dropna_cols if x not in sumstats]
  733. missing_fix_dtype_cols = [x for x in args.fix_dtype_cols if x not in sumstats]
  734. if missing_dropna_cols:
  735. log.log('Warning: can not apply --dropna-cols to {}; columns are missing'.format(', '.join(missing_dropna_cols)))
  736. args.dropna_cols = [x for x in args.dropna_cols if x not in missing_dropna_cols]
  737. if missing_fix_dtype_cols:
  738. log.log('Warning: can not apply --fix-dtype-cols to {}; columns are missing'.format(', '.join(missing_fix_dtype_cols)))
  739. args.fix_dtype_cols = [x for x in args.fix_dtype_cols if x not in missing_fix_dtype_cols]
  740. if args.exclude_ranges is not None:
  741. args.dropna_cols.extend(['CHR', 'BP'])
  742. args.fix_dtype_cols.extend(['CHR', 'BP'])
  743. if args.fix_dtype_cols is not None:
  744. for col in args.fix_dtype_cols:
  745. if cols_type_map[col] == int:
  746. args.dropna_cols.append(col)
  747. # Check all required columns
  748. args.require_cols = [col.upper() for col in args.require_cols]
  749. missing_cols = []
  750. for col in args.require_cols:
  751. if col == 'EFFECT':
  752. if not any(col in sumstats for col in ['BETA', 'OR', 'LOGODDS', 'Z']):
  753. missing_cols.append('"EFFECT" (e.i. BETA, OR, LOGODDS or Z)')
  754. elif col not in sumstats:
  755. missing_cols.append('"{}"'.format(col))
  756. if missing_cols:
  757. raise(ValueError('--require-cols detected that columns {} are not available in the sumstats file'.format(', '.join(missing_cols))))
  758. # Adjust optional parameters (those that can be ignored if certain columns are missing)
  759. if (args.max_or is not None) and ('OR' not in sumstats):
  760. log.log('Warning: skip --max-or ("OR" column not found in {})'.format(args.sumstats))
  761. args.max_or = None
  762. if (args.maf is not None) and ('FRQ' not in sumstats):
  763. log.log('Warning: skip --maf ("FRQ" column not found in {})'.format(args.sumstats))
  764. args.maf = None
  765. if (args.info is not None) and ('INFO' not in sumstats):
  766. log.log('Warning: skip --info ("INFO" column not found in {})'.format(args.sumstats))
  767. args.info = None
  768. if (args.min_pval is not None) and ('PVAL' not in sumstats):
  769. log.log('Warning: skip --min-pval ("PVAL" column not found in {})'.format(args.sumstats))
  770. args.min_pval = None
  771. if args.update_z_col_from_beta_and_se and (('BETA' not in sumstats) or ('SE' not in sumstats)):
  772. log.log('Warning: can not apply --update-z-col-from-beta-and-se ("OR" column not found in {})'.format(args.sumstats))
  773. args.update_z_col_from_beta_and_se = False
  774. if args.qc_substudies and ('DIRECTION' not in sumstats):
  775. log.log('Warning: can not apply --qc-substudies ("DIRECTION" column not found in {})'.format(args.sumstats))
  776. args.qc_substudies = False
  777. nstudies = None
  778. if args.qc_substudies:
  779. nstudies = np.min(sumstats['DIRECTION'].str.len())
  780. max_nstudies = np.max(sumstats['DIRECTION'].str.len())
  781. if max_nstudies != nstudies:
  782. raise(ValueError('Problem with DIRECTION column: number of studies vary between {} and {}'.format(nstudies, max_nstudies)))
  783. log.log('DIRECTION column indicates a meta-analysis of {} sub-studies. --qc-substudies will exclude variants with ? in {} or more substudies.'.format(nstudies, 1+int(nstudies/2)))
  784. if args.max_or is not None: args.fix_dtype_cols.append('OR')
  785. if args.maf is not None: args.fix_dtype_cols.append('FRQ')
  786. if args.info is not None: args.fix_dtype_cols.append('INFO')
  787. if args.update_z_col_from_beta_and_se: args.fix_dtype_cols.extend(['BETA', 'SE'])
  788. if args.min_pval is not None: args.fix_dtype_cols.append('PVAL')
  789. # Validate that all required columns are present
  790. if (args.just_acgt or args.snps_only or args.drop_strand_ambiguous_snps) and (('A1' not in sumstats) or ('A2' not in sumstats)):
  791. raise(ValueError('A1 and A2 columns are required for --just-acgt, --snps-only, --drop-strand-ambiguous-snps'))
  792. if (args.just_rs_variants) and ('SNP' not in sumstats):
  793. raise(ValueError('SNP column is required --just-rs-variants'))
  794. # Perform QC procedures
  795. if len(args.dropna_cols) > 0:
  796. drop_sumstats(sumstats, log, "missing values in either of '{}' columns".format(args.dropna_cols), dropna_subset=args.dropna_cols)
  797. for col in args.fix_dtype_cols:
  798. if col in sumstats:
  799. log.log('Set column {} dtype to {}'.format(col, cols_type_map[col]))
  800. if cols_type_map[col] in [float, np.float64, int]:
  801. sumstats[col] = pd.to_numeric(sumstats[col], errors='coerce')
  802. if col in args.dropna_cols:
  803. drop_sumstats(sumstats, log, "dtype conversion in {} column".format(col), dropna_subset=[col])
  804. if cols_type_map[col] == int:
  805. sumstats[col] = sumstats[col].astype(int)
  806. else:
  807. sumstats[col] = sumstats[col].astype(cols_type_map[col])
  808. for range in exclude_ranges:
  809. idx = sumstats.index[(sumstats[cols.CHR] == range.chr) & (sumstats[cols.BP] >= range.from_bp) & (sumstats[cols.BP] < range.to_bp)]
  810. drop_sumstats(sumstats, log, 'exclude range {}:{}-{}'.format(range.chr, range.from_bp, range.to_bp), drop_labels=idx)
  811. if args.max_or is not None:
  812. drop_sumstats(sumstats, log, 'OR exceeded threshold {}'.format(args.max_or),
  813. drop_labels=sumstats.index[(sumstats.OR > args.max_or) | (sumstats.OR < (1/args.max_or))])
  814. if args.maf is not None:
  815. drop_sumstats(sumstats, log, 'MAF below threshold {}'.format(args.maf),
  816. drop_labels=sumstats.index[(sumstats.FRQ < args.maf) | (sumstats.FRQ>(1-args.maf))])
  817. if args.info is not None:
  818. drop_sumstats(sumstats, log, 'INFO below threshold {}'.format(args.info),
  819. drop_labels=sumstats.index[sumstats.INFO < args.info])
  820. if args.min_pval is not None:
  821. drop_sumstats(sumstats, log, 'PVAL below threshold {} or above 1.0'.format(args.min_pval),
  822. drop_labels=sumstats.index[(sumstats.PVAL < args.min_pval) | (sumstats.PVAL > 1.0)])
  823. if args.update_z_col_from_beta_and_se:
  824. sumstats['Z'] = np.divide(sumstats.BETA.values, sumstats.SE.values)
  825. if args.snps_only:
  826. drop_sumstats(sumstats, log, '--snps-only',
  827. drop_labels=sumstats.index[(sumstats['A1'].str.len() != 1) | (sumstats['A2'].str.len() != 1)])
  828. if args.just_acgt:
  829. drop_sumstats(sumstats, log, '--just-acgt',
  830. drop_labels=sumstats.index[np.logical_not(sumstats['A1'].isin(BASES)) | np.logical_not(sumstats['A2'].isin(BASES))])
  831. if args.drop_strand_ambiguous_snps:
  832. drop_sumstats(sumstats, log, '--drop-strand-ambiguous-snps',
  833. drop_labels=sumstats.index[(sumstats['A1'].map(str) + sumstats['A2']).isin(['AT', 'TA', 'CG', 'GC']) & (sumstats['A1'].str.len() == 1)])
  834. if args.just_rs_variants:
  835. drop_sumstats(sumstats, log, '--just-rs-variants',
  836. drop_labels=sumstats.index[np.logical_not(sumstats['SNP'].str.match('^rs\d+$'))])
  837. if nstudies is not None:
  838. drop_sumstats(sumstats, log, '--qc-substudies',
  839. drop_labels=sumstats.index[sumstats['DIRECTION'].str.count('\?') > int(nstudies/2)])
  840. fix_columns_order(sumstats).to_csv(args.out, index=False, header=True, sep='\t', na_rep='NA')
  841. log.log("{n} SNPs saved to {f}".format(n=len(sumstats), f=args.out))
  842. describe_sample_size(sumstats, log)
  843. ### =================================================================================
  844. ### Implementation for parser_zscore
  845. ### =================================================================================
  846. def get_str_list_sign(str_list):
  847. return np.array([-1 if e[0]=='-' else 1 for e in list(str_list)], dtype=np.int)
  848. def make_zscore(args, log):
  849. """
  850. Calculate z-score from p-value column and effect size column
  851. """
  852. """
  853. Takes csv files (created with the csv task of this script).
  854. Require columns: SNP, P, and one of the signed summary statistics columns (BETA, OR, Z, LOGODDS).
  855. Creates corresponding mat files which can be used as an input for the conditional fdr model.
  856. Only SNPs from the reference file are considered. Zscores of strand ambiguous SNPs are set to NA.
  857. """
  858. if args.out == '-': args.out = sys.stdout
  859. check_input_file(args.sumstats)
  860. check_output_file(args.out, args.force)
  861. columns = list(pd.read_csv(args.sumstats, sep='\t', nrows=0).columns)
  862. log.log('Columns in {}: {}'.format(args.sumstats, columns))
  863. if (args.effect is None) and (not args.a1_inc):
  864. if cols.Z in columns: args.effect = cols.Z
  865. elif cols.BETA in columns: args.effect = cols.BETA
  866. elif cols.OR in columns: args.effect = cols.OR
  867. elif cols.LOGODDS in columns: args.effect = cols.LOGODDS
  868. else: raise(ValueError('Warning: signed effect column is not detected in {}. Enable --a1-inc'.format(args.sumstats)))
  869. effect_size_column_count = np.sum([int(c in columns) for c in [cols.Z, cols.BETA, cols.OR, cols.LOGODDS]])
  870. if effect_size_column_count > 1: log.log('Warning: Multiple columns indicate effect direction')
  871. if effect_size_column_count == 1: log.log('Use {} column as effect direction.'.format(args.effect))
  872. missing_columns = [c for c in [cols.PVAL, args.effect] if (c != None) and (c not in columns)]
  873. if missing_columns: raise(ValueError('{} columns are missing'.format(missing_columns)))
  874. if args.a1_inc:
  875. signed_effect = None
  876. effect_col_dtype_map = {}
  877. else:
  878. # if signed_effect is true, take effect column as string to handle correctly
  879. # case of truncated numbers, e.g.: 0.00 and -0.00 should have different sign
  880. signed_effect = False if args.effect == cols.OR else True
  881. effect_col_dtype_map = {args.effect: (str if signed_effect else float)}
  882. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  883. reader = pd.read_csv(args.sumstats, sep='\t', chunksize=args.chunksize,
  884. dtype=effect_col_dtype_map, float_precision='high')
  885. n_snps = 0
  886. with (open(args.out, 'a') if args.out != sys.stdout else sys.stdout) as out_f:
  887. for chunk_index, chunk in enumerate(reader):
  888. if chunk_index==0: log.log('Column types: ' + ', '.join([column + ':' + str(dtype) for (column, dtype) in zip(chunk.columns, chunk.dtypes)]))
  889. if args.a1_inc:
  890. effect_sign = np.ones(len(chunk))
  891. elif signed_effect:
  892. # effect column has str type
  893. # -1 if effect starts with '-' else 1
  894. effect_sign = get_str_list_sign(chunk[args.effect].astype(str))
  895. else:
  896. # effect column has np.float type
  897. # 1 if effect >=1 else -1
  898. if (chunk[args.effect] < 0).any():
  899. raise ValueError("OR column contains negative values")
  900. effect_sign = np.sign(chunk[args.effect].values - 1)
  901. effect_sign[effect_sign == 0] = 1
  902. chunk[cols.PVAL] = pd.to_numeric(chunk[cols.PVAL], errors='coerce')
  903. chunk[cols.Z] = -stats.norm.ppf(chunk[cols.PVAL].values*0.5)*effect_sign.astype(np.float64)
  904. chunk.to_csv(out_f, index=False, header=(chunk_index==0), sep='\t', na_rep='NA')
  905. n_snps += len(chunk)
  906. eprint("{f}: {n} lines processed".format(f=args.sumstats, n=(chunk_index+1)*args.chunksize))
  907. log.log("{n} SNPs saved to {f}".format(n=n_snps, f=args.out))
  908. ### =================================================================================
  909. ### Implementation for parser_pvalue
  910. ### =================================================================================
  911. def make_pvalue(args, log):
  912. """
  913. Calculate p-value column from 'z'' or 'beta'/'se' columns
  914. """
  915. if args.out == '-': args.out = sys.stdout
  916. check_input_file(args.sumstats)
  917. check_output_file(args.out, args.force)
  918. columns = list(pd.read_csv(args.sumstats, sep='\t', nrows=0).columns)
  919. log.log('Columns in {}: {}'.format(args.sumstats, columns))
  920. if 'PVAL' in columns:
  921. log.log('PVAL already exists, nothing to be done.')
  922. return
  923. if ('Z' not in columns) and (('BETA' not in columns) or ('SE' not in columns)):
  924. raise(ValueError("Neither Z nor BETA/SE are available in the input summary stats file"))
  925. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  926. reader = pd.read_csv(args.sumstats, sep='\t', chunksize=args.chunksize)
  927. n_snps = 0
  928. with (open(args.out, 'a') if args.out != sys.stdout else sys.stdout) as out_f:
  929. for chunk_index, chunk in enumerate(reader):
  930. if chunk_index==0: log.log('Column types: ' + ', '.join([column + ':' + str(dtype) for (column, dtype) in zip(chunk.columns, chunk.dtypes)]))
  931. if 'Z' in columns:
  932. chunk[cols.PVAL] = scipy.stats.norm.sf(abs(chunk['Z'].values))*2
  933. elif ('BETA'in columns) and ('SE' in columns):
  934. chunk[cols.PVAL] = scipy.stats.norm.sf(abs(np.divide(chunk['BETA'].values, chunk['SE'].values)))*2
  935. chunk.to_csv(out_f, index=False, header=(chunk_index==0), sep='\t', na_rep='NA')
  936. n_snps += len(chunk)
  937. eprint("{f}: {n} lines processed".format(f=args.sumstats, n=(chunk_index+1)*args.chunksize))
  938. log.log("{n} SNPs saved to {f}".format(n=n_snps, f=args.out))
  939. ### =================================================================================
  940. ### Implementation for parser_beta
  941. ### =================================================================================
  942. def make_beta(args, log):
  943. """
  944. Calculate beta column from OR or LOGODDS
  945. """
  946. if args.out == '-': args.out = sys.stdout
  947. check_input_file(args.sumstats)
  948. check_output_file(args.out, args.force)
  949. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  950. df = pd.read_csv(args.sumstats, sep='\t')
  951. log.log('Done, {} markers found'.format(len(df)))
  952. if 'BETA' in df:
  953. log.log('WARNING: nothing to be done, BETA column is already present')
  954. elif 'LOGODDS' in df:
  955. df.rename(columns={'LOGODDS':'BETA'}, inplace=True)
  956. log.log('Rename LOGODDS column to BETA')
  957. elif 'OR' in df:
  958. df['BETA'] = np.log(df['OR'].values)
  959. df.drop(labels=['OR'], axis=1, inplace=True)
  960. log.log('Calculate BETA=log(OR), and drop the OR column')
  961. fix_columns_order(df).to_csv(args.out, index=False, header=True, sep='\t', na_rep='NA')
  962. log.log("{n} SNPs saved to {f}".format(n=len(df), f=args.out))
  963. ### =================================================================================
  964. ### Implementation for parser_mat
  965. ### =================================================================================
  966. _base_complement = {"A":"T", "C":"G", "G":"C", "T":"A"}
  967. def _complement(seq):
  968. return "".join([_base_complement[b] for b in seq])
  969. def _reverse_complement_variant(variant):
  970. # variant should be a 2-elemet sequence with upper case string elements
  971. return ("".join([_base_complement[b] for b in variant[0][::-1]]),
  972. "".join([_base_complement[b] for b in variant[1][::-1]]))
  973. def _is_alleles_match(variant_x, variant_y):
  974. if variant_x == variant_y: return True
  975. if variant_x == variant_y[::-1]: return True
  976. if variant_x == _reverse_complement_variant(variant_y): return True
  977. if variant_x == _reverse_complement_variant(variant_y)[::-1]: return True
  978. return False
  979. def make_mat(args, log):
  980. """
  981. Takes csv files (created with the csv task of this script).
  982. Require columns: SNP, P, and one of the signed summary statistics columns (BETA, OR, Z, LOGODDS).
  983. Creates corresponding mat files which can be used as an input for the conditional fdr model.
  984. Only SNPs from the reference file are considered. Zscores of strand ambiguous SNPs are set to NA.
  985. """
  986. if args.sumstats == '-': args.sumstats = sys.stdin
  987. check_input_file(args.ref)
  988. check_input_file(args.sumstats)
  989. check_output_file(args.out, args.force)
  990. # very special handling of cases where input file has no alleles information
  991. if args.ignore_alleles:
  992. log.log('Ignore A1/A2 alleles from the input files')
  993. log.log('Reading reference file {}...'.format(args.ref))
  994. df_ref = pd.read_csv(args.ref, sep='\t', usecols=[cols.SNP])
  995. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  996. df_sumstats = pd.read_csv(args.sumstats, sep='\t', float_precision='high', usecols=[cols.SNP, cols.PVAL])
  997. log.log('Merging with reference file...')
  998. df_sumstats.drop_duplicates(subset=[cols.SNP], keep='first', inplace=True)
  999. df_result = pd.merge(df_ref, df_sumstats, how='left', on='SNP')
  1000. num_matches = df_result[cols.PVAL].notnull().sum()
  1001. if num_matches == 0: raise(ValueError("No SNPs match after joining with reference data"))
  1002. log.log("{f}: {n} SNPs matched with reference file".format(f=args.sumstats, n=num_matches))
  1003. sio.savemat(args.out, {'logpvec'+args.trait: -np.log10(df_result[cols.PVAL].values)}, format='5', do_compression=False, oned_as='column', appendmat=False)
  1004. log.log("%s created" % args.out)
  1005. return
  1006. reader = pd.read_csv(args.sumstats, sep='\t', chunksize=args.chunksize, float_precision='high')
  1007. df_out = None
  1008. for chunk_index, ss_chunk in enumerate(reader):
  1009. # (BEGIN) special handling of the first chunk
  1010. if chunk_index==0:
  1011. columns = list(ss_chunk.columns)
  1012. # check whether arguments are correct
  1013. if args.keep_all_cols and args.keep_cols:
  1014. err_msg = ("Misleading input arguments! Use either '--keep-cols' or "
  1015. "'--keep-all-cols' option, but not both at a time.")
  1016. raise(ValueError(err_msg))
  1017. not_in_csv_keep_cols = set(args.keep_cols) - set(columns)
  1018. if not_in_csv_keep_cols:
  1019. log.log("Warning: '--keep-cols' contains names which are absent in csv "
  1020. "file: %s. They will be ignored." % ', '.join(not_in_csv_keep_cols))
  1021. if cols.Z not in columns:
  1022. raise(RuntimeError('Z column is not present in the input file. Use ``sumstats.py zscore`` to enrich summary stats with z-score column.'))
  1023. # cols2ignore: columns from sumstats file which are dropped anyway
  1024. cols2ignore = ["SNP", "CHR", "BP", "A1", "A2", "Z"]
  1025. if (set(cols2ignore) - set(columns)):
  1026. # If this happens, probably standard format of csv file has changed.
  1027. absent_cols = set(cols2ignore) - set(columns)
  1028. err_msg = ("Columns required in standard csv file: {} are missing in "
  1029. "input csv file {}.").format(', '.join(absent_cols), args.sumstats)
  1030. raise(RuntimeError(err_msg))
  1031. if "DIRECTION" in columns: cols2ignore.append("DIRECTION")
  1032. # cols2keep: columns from sumstats file which are kept in mat file
  1033. cols2keep = cols._fields if args.keep_all_cols else args.keep_cols
  1034. # cols2keep: columns from sumstats file which are not saved to mat file
  1035. cols2drop = (set(columns) - set(cols2keep)) | set(cols2ignore)
  1036. n_col = cols.N if cols.N in columns else None
  1037. ncase_col = cols.NCASE if cols.NCASE in columns else None
  1038. ncontrol_col = cols.NCONTROL if cols.NCONTROL in columns else None
  1039. if (not args.without_n) and ((n_col is None) and ((ncase_col is None) or (ncontrol_col is None))):
  1040. raise(ValueError('Sample size column is not detected in {}. Expact either N or NCASE, NCONTROL column.'.format(args.sumstats)))
  1041. missing_columns = [c for c in [cols.A1, cols.A2, cols.SNP, cols.PVAL] if (c != None) and (c not in columns)]
  1042. if missing_columns: raise(ValueError('{} columns are missing'.format(missing_columns)))
  1043. log.log('Reading reference file {}...'.format(args.ref))
  1044. usecols = [cols.SNP, cols.A1, cols.A2]
  1045. ref_reader = pd.read_csv(args.ref, sep='\t', usecols=usecols,
  1046. chunksize=args.chunksize)
  1047. ref_dict = {}
  1048. for ref_chunk in ref_reader:
  1049. ref_chunk.drop(ref_chunk.index[np.logical_not(ref_chunk['A1'].str.upper().str.match('^[ACTG]*$')) | np.logical_not(ref_chunk['A2'].str.upper().str.match('^[ACTG]*$'))], inplace=True)
  1050. if ref_chunk.empty: continue
  1051. gtypes = zip(ref_chunk[cols.A1].apply(str.upper),ref_chunk[cols.A2].apply(str.upper))
  1052. #TODO?: add check whether some id is already in ref_dict
  1053. ref_dict.update(dict(zip(ref_chunk[cols.SNP], gtypes)))
  1054. ref_dict = {i: (variant, _reverse_complement_variant(variant),
  1055. variant[::-1], _reverse_complement_variant(variant[::-1]))
  1056. for i, variant in ref_dict.items()}
  1057. ref_snps = pd.read_csv(args.ref, sep='\t', usecols=[cols.SNP], squeeze=True)
  1058. #TODO?: add check whether ref_snps contains duplicates
  1059. log.log("Reference dict contains {d} snps.".format(d=len(ref_dict)))
  1060. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  1061. log.log('Column types: ' + ', '.join([column + ':' + str(dtype) for (column, dtype) in zip(ss_chunk.columns, ss_chunk.dtypes)]))
  1062. # (END) special handling of the first chunk
  1063. ss_chunk = ss_chunk.loc[ss_chunk[cols.SNP].isin(ref_dict),:]
  1064. if ss_chunk.empty: continue
  1065. gtypes = list(zip(ss_chunk[cols.A1].apply(str.upper),ss_chunk[cols.A2].apply(str.upper)))
  1066. # index of SNPs that have the same alleles as indicated in reference
  1067. ind = [gt in ref_dict[sid] for sid, gt in zip(ss_chunk[cols.SNP], gtypes)]
  1068. ss_chunk = ss_chunk.loc[ind,:]
  1069. gtypes = [gt for gt, j in zip(gtypes, ind) if j]
  1070. log10pv = -np.log10(ss_chunk[cols.PVAL].values)
  1071. # not_ref_effect = [
  1072. # 1 if effect allele in data == other allele in reference
  1073. # -1 if effect allele in data == effect allele in reference ]
  1074. # So zscores with positive effects will be positive and zscores with
  1075. # negative effects will stay negative, since
  1076. # stats.norm.ppf(ss_chunk[cols.PVAL]*0.5) is always negetive (see zvect
  1077. # calculation below).
  1078. not_ref_effect = np.array([1 if gt in ref_dict[sid][:2] else -1
  1079. for sid, gt in zip(ss_chunk[cols.SNP], gtypes)])
  1080. #TODO: check proportion of positive and negative effects
  1081. zvect = ss_chunk[cols.Z].values*not_ref_effect
  1082. ind_ambiguous = [j for j,gt in enumerate(gtypes) if gt == _reverse_complement_variant(gt)[::-1]]
  1083. # set zscore of ambiguous SNPs to nan
  1084. zvect[ind_ambiguous] = np.nan
  1085. #TODO: check whether output df contains duplicated rs-ids (warn)
  1086. # reindex by SNP, add required columns and drop unnecessary columns
  1087. ss_chunk.index = ss_chunk[cols.SNP]
  1088. # add required columns
  1089. ss_chunk["logpvec"] = log10pv
  1090. ss_chunk["zvec"] = zvect
  1091. if not args.without_n:
  1092. if n_col is None:
  1093. nvec = 4./(1./ss_chunk[ncase_col] + 1./ss_chunk[ncontrol_col])
  1094. else:
  1095. nvec = ss_chunk[n_col].values
  1096. ss_chunk["nvec"] = nvec
  1097. ss_chunk.drop(cols2drop, axis=1, inplace=True)
  1098. if df_out is None:
  1099. df_out = ss_chunk.copy()
  1100. else:
  1101. df_out = df_out.append(ss_chunk)
  1102. eprint("{f}: {n} lines processed, {m} SNPs matched with reference file".format(f=args.sumstats, n=(chunk_index+1)*args.chunksize, m=len(df_out)))
  1103. if df_out.empty: raise(ValueError("No SNPs match after joining with reference data"))
  1104. dup_index = df_out.index.duplicated(keep=False)
  1105. if dup_index.any():
  1106. log.log("Duplicated SNP ids detected:")
  1107. log.log(df_out[dup_index])
  1108. log.log("Keeping only the first occurance.")
  1109. df_out = df_out[~df_out.index.duplicated(keep='first')]
  1110. # allign index accordind order of SNPs in ref, insert NaN rows for SNPs that
  1111. # present in ref but absent in sumstats file
  1112. df_out = df_out.reindex(ref_snps)
  1113. log.log('Writing .mat file...')
  1114. #TODO: check column type, transform into matrix if not numeric
  1115. save_dict = {c+args.trait: df_out[c].astype(np.float64).values for c in df_out.columns}
  1116. sio.savemat(args.out, save_dict, format='5', do_compression=False,
  1117. oned_as='column', appendmat=False)
  1118. log.log("%s created" % args.out)
  1119. ### =================================================================================
  1120. ### Implementation for parser_variantid
  1121. ### =================================================================================
  1122. def make_variantid(args, log):
  1123. if args.sumstats == '-': args.sumstats = sys.stdin
  1124. if args.out == '-': args.out = sys.stdout
  1125. check_input_file(args.sumstats)
  1126. check_input_file(args.ref)
  1127. check_output_file(args.out, args.force)
  1128. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  1129. df = pd.read_csv(args.sumstats, sep='\t', dtype={"CHR":str})
  1130. log.log('Done, {} markers found'.format(len(df)))
  1131. log.log('Reading reference file {}...'.format(args.ref))
  1132. ref = pd.read_csv(args.ref, sep='\t', usecols=[cols.CHR, cols.BP, cols.A1, cols.A2], dtype={cols.CHR:str})
  1133. ref.rename(columns={'A1': 'A1_ref', 'A2': 'A2_ref'}, inplace=True)
  1134. log.log("Reference dict contains {d} snps.".format(d=len(ref)))
  1135. log.log('Merging summary statistics file with the reference...')
  1136. ref['DUP']=ref.duplicated(subset=['CHR', 'BP'], keep=False)
  1137. df['index'] = df.index
  1138. df_nodups = pd.merge(df[['index', 'CHR', 'BP']], ref[['CHR', 'BP', 'A1_ref', 'A2_ref']][~ref['DUP']], on=['CHR', 'BP'], how='inner')
  1139. df_variant_id = df_nodups
  1140. if ('A1' in df) and ('A2' in df):
  1141. df_dups = pd.merge(df[['index', 'CHR', 'BP', 'A1', 'A2']], ref[['CHR', 'BP', 'A1_ref', 'A2_ref']][ref['DUP']], on=['CHR', 'BP'], how='inner')
  1142. if not df_dups.empty: df_dups = df_dups[[_is_alleles_match((row.A1, row.A2), (row.A1_ref, row.A2_ref)) for _, row in df_dups.iterrows()]].copy()
  1143. if not df_dups.empty: df_dups.drop(labels=['A1', 'A2'], axis=1, inplace=True)
  1144. if not df_dups.empty: df_variant_id = pd.concat([df_nodups, df_dups]).copy()
  1145. else:
  1146. log.log("WARNING: sumstats file has no allele codes")
  1147. df_variant_id['VARIANT_ID']=df_variant_id['CHR'].astype(str)+':'+df_variant_id['BP'].astype(str)+':'+df_variant_id['A1_ref']+':'+df_variant_id['A2_ref']
  1148. df_variant_id.drop_duplicates(subset=['VARIANT_ID'], keep=False, inplace=True)
  1149. df_variant_id.drop_duplicates(subset=['index'], keep=False, inplace=True)
  1150. df = pd.merge(df, df_variant_id[['index', 'VARIANT_ID']], how='left', on='index')
  1151. df.drop(labels=['index'], axis=1, inplace=True)
  1152. df['VARIANT_ID'].fillna('.', inplace=True)
  1153. log.log("Merging complete, {n} out of {m} SNPs receive VARIANT_ID".format(n=(df['VARIANT_ID'] != '.').sum(), m=len(df)))
  1154. fix_columns_order(df).to_csv(args.out, index=False, header=True, sep='\t', na_rep='NA')
  1155. log.log("{n} SNPs saved to {f}".format(n=len(df), f=args.out))
  1156. ### =================================================================================
  1157. ### Implementation for parser_lift
  1158. ### =================================================================================
  1159. def make_lift(args, log):
  1160. """
  1161. Lift RS numbers to a newer version of SNPdb, and/or
  1162. liftover chr:pos to another genomic build using NCBI chain files.
  1163. """
  1164. from pyliftover import LiftOver
  1165. from lift_rs_numbers import LiftRsNumbers
  1166. if args.sumstats == '-': args.sumstats = sys.stdin
  1167. if args.out == '-': args.out = sys.stdout
  1168. check_input_file(args.sumstats)
  1169. check_output_file(args.out, args.force)
  1170. if args.chain_file is not None: check_input_file(args.chain_file)
  1171. if args.snp_chrpos is not None: check_input_file(args.snp_chrpos)
  1172. if args.snp_history is not None: check_input_file(args.snp_history)
  1173. if args.rs_merge_arch is not None: check_input_file(args.rs_merge_arch)
  1174. if (args.snp_history is not None) != (args.rs_merge_arch is not None):
  1175. raise(ValueError('--snp-history and --rs-merge-arch must be used together'))
  1176. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  1177. df = pd.read_csv(args.sumstats, sep='\t')
  1178. log.log('Done, {} markers found'.format(len(df)))
  1179. lift_bp = None; lift_rs = None; snp_chrpos = None
  1180. if (args.chain_file is not None) and (cols.CHR in df) and (cols.BP in df):
  1181. log.log('Reading {}...'.format(args.chain_file))
  1182. lift_bp = LiftOver(args.chain_file)
  1183. if (args.snp_history is not None) and (cols.SNP in df):
  1184. lift_rs = LiftRsNumbers(hist_file=args.snp_history, merge_file=args.rs_merge_arch)
  1185. if args.snp_chrpos is not None:
  1186. log.log('Reading {}...'.format(args.snp_chrpos))
  1187. snp_chrpos = pd.read_csv(args.snp_chrpos, sep='\t', header=None, usecols=[0,1,2])
  1188. snp_chrpos.columns=['snp_id','chr','pos'] #,'orien','neighbor_snp_list','isPAR']
  1189. snp_chrpos.dropna(subset=['pos'],inplace=True) # drop NA positions
  1190. snp_chrpos['pos']=snp_chrpos['pos'].astype(np.int) + 1 # convert to integer unity-based positions
  1191. snp_chrpos['chr'].replace({'X':23, 'Y':24, 'PAR': 25, 'MT': 26}, inplace=True) # standarize chr labels
  1192. snp_chrpos['chr']=snp_chrpos['chr'].astype(np.int)
  1193. log.log('Done, found {} entries'.format(len(snp_chrpos)))
  1194. # if snp_chrpos.duplicated(subset=['snp_id'], keep=False).sum() != 0:
  1195. # raise('Duplicated snp_id found in SNPChrPosOnRef file')
  1196. indices_with_old_chrpos = range(len(df)) # indices with original chr:pos
  1197. fixes = []
  1198. if (cols.SNP in df) and (lift_rs is not None):
  1199. # Fix1 brings forward SNP rs# numbers and set SNP rs# to None for SNPs found in SNPHistory table
  1200. df[cols.SNP], stats = lift_rs.lift(df[cols.SNP])
  1201. fixes.append('{} rs# numbers has changed based on RsMergeArch table'.format(stats['lifted']))
  1202. log.log(stats)
  1203. if (cols.SNP in df) and (snp_chrpos is not None):
  1204. # Fix2 set chr:pos based on SNPChrPosOnRef table (only applies to SNPs with rs# number, not to other markers)
  1205. df['SNP_LIFTED'] = [(int(x[2:]) if (x and x.startswith('rs') and x[2:].isdigit()) else -1) for x in df[cols.SNP]]
  1206. df = pd.merge(df, snp_chrpos, how='left', left_on='SNP_LIFTED', right_on='snp_id')
  1207. log.log('Warning: there are {} SNPs with a valid RS number, but not found in SNPChrPosOnRef'.format(((df['SNP_LIFTED'] != -1) & df['snp_id'].isnull()).sum()))
  1208. if cols.CHR in df and cols.BP in df:
  1209. idx = ((df['pos'] != df[cols.BP]) | (df['chr'] != df[cols.CHR])) & ~df['pos'].isnull() & ~df['chr'].isnull()
  1210. df.loc[idx, cols.BP] = df.loc[idx, 'pos'].astype(int)
  1211. df.loc[idx, cols.CHR] = df.loc[idx, 'chr'].astype(int)
  1212. fixes.append('{} markers receive new CHR:POS based on SNPChrPosOnRef table'.format(idx.sum()))
  1213. else:
  1214. idx = ~df['pos'].isnull() & ~df['chr'].isnull()
  1215. df[cols.BP] = np.nan; df[cols.CHR] = np.nan
  1216. df.loc[idx, cols.BP] = df.loc[idx, 'pos'].astype(int)
  1217. df.loc[idx, cols.CHR] = df.loc[idx, 'chr'].astype(int)
  1218. fixes.append('{} markers receive CHR:POS based on SNPChrPosOnRef table'.format(idx.sum()))
  1219. indices_with_old_chrpos = [i for (i, b) in enumerate(df['pos'].isnull() | df['chr'].isnull()) if b]
  1220. df.drop(['SNP_LIFTED', 'snp_id', 'chr', 'pos'], axis=1, inplace=True)
  1221. if (cols.CHR in df) and (cols.BP in df) and (cols.SNP not in df) and (snp_chrpos is not None):
  1222. # Fix3 set SNP rs# based on SNPChrPosOnRef table based on CHR:POS
  1223. # It is a fairly rough guess about SNP rs#, because we do not take allele codes into account.
  1224. # Therefore it is important that this step applies only when SNP column is missing.
  1225. df = pd.merge(df, snp_chrpos, how='left', left_on=[cols.CHR, cols.BP], right_on=['chr', 'pos'])
  1226. df['snp_id'][df['snp_id'].isnull()] = -1
  1227. df['snp_id'] = 'rs' + df['snp_id'].astype(int).astype(str)
  1228. df['snp_id'][df['snp_id'] == 'rs-1'] = None
  1229. df['SNP'] = df['snp_id']
  1230. fixes.append('{} markers receive SNP rs# based on SNPChrPosOnRef table'.format((~df[cols.SNP].isnull()).sum()))
  1231. df.drop(['snp_id', 'chr', 'pos'], axis=1, inplace=True)
  1232. if lift_bp is not None:
  1233. # Fix4 lifts chr:pos to another genomic build. This step is OFF by default, only applies if user provided chain file.
  1234. # Note that lifting with pyliftover is rather slow, so we apply this step only to markers that are not in SNPChrPosOnRef table.
  1235. log.log('Lift CHR:POS for {} SNPs to another genomic build...'.format(len(indices_with_old_chrpos)))
  1236. unique = 0; multi = 0; failed = 0
  1237. for i, index in enumerate(indices_with_old_chrpos):
  1238. if (i+1) % 100 == 0: eprint('Finish {} SNPs'.format(i+1))
  1239. chri = int(df.loc[index, cols.CHR]); bp = int(df.loc[index, cols.BP]); snp = df.loc[index, cols.SNP]
  1240. lifted = lift_bp.convert_coordinate('chr{}'.format(chri), bp)
  1241. if (lifted is None) or (len(lifted) == 0):
  1242. #log.log('Unable to lift SNP {} at chr{}:{}, delete'.format(snp, chri, bp))
  1243. df.loc[index, cols.CHR] = None
  1244. df.loc[index, cols.BP] = None
  1245. failed += 1
  1246. continue
  1247. if len(lifted) > 1:
  1248. log.log('Warning: SNP {} at chr{}:{} lifts to multiple position, use first.'.format(snp, chri, bp))
  1249. multi += 1
  1250. if len(lifted) == 1:
  1251. unique += 1
  1252. df.loc[index, cols.CHR] = int(lifted[0][0][3:])
  1253. df.loc[index, cols.BP] = lifted[0][1]
  1254. log.log('Done, {} failed, {} unique, {} multi'.format(failed, unique, multi))
  1255. fixes.append('{} markers receive new CHR:POS based on liftover chain files'.format(unique + multi))
  1256. if cols.SNP in df:
  1257. df[cols.SNP].fillna('.', inplace=True)
  1258. num_variants_without_rs_number = (df[cols.SNP]=='.').sum()
  1259. if num_variants_without_rs_number > 0:
  1260. log.log('{} variants have missing SNP rs#'.format(num_variants_without_rs_number))
  1261. if not args.keep_bad_snps:
  1262. if (cols.CHR in df) and (cols.BP in df):
  1263. df_len = len(df)
  1264. df.dropna(subset=[cols.CHR, cols.BP], inplace=True) # Fix6, due to failed liftover across genomic builds
  1265. if len(df) < df_len:
  1266. fixes.append("{n} markers were dropped due to missing CHR:POS information or due to failed CHR:POS lift".format(n = df_len - len(df)))
  1267. df[cols.CHR] = df[cols.CHR].astype(int)
  1268. df[cols.BP] = df[cols.BP].astype(int)
  1269. fix_columns_order(df).to_csv(args.out + ('.gz' if args.gzip else ''),
  1270. index=False, header=True, sep='\t', na_rep=args.na_rep,
  1271. compression='gzip' if args.gzip else None)
  1272. log.log("{n} SNPs saved to {f}".format(n=len(df), f=args.out))
  1273. describe_sample_size(df, log)
  1274. log.log('Summary: \n\t{}'.format("\n\t".join(fixes)))
  1275. ### =================================================================================
  1276. ### Implementation for parser_clump
  1277. ### =================================================================================
  1278. def touch(fname, times=None):
  1279. with open(fname, 'a'):
  1280. os.utime(fname, times)
  1281. def tar_filter(tarinfo):
  1282. if os.path.basename(tarinfo.name).startswith('sumstats'): return None
  1283. return tarinfo
  1284. def clump_cleanup(args, log):
  1285. temp_out = args.out + '.temp'
  1286. log.log('Saving intermediate files to {out}.temp.tar.gz'.format(out=args.out))
  1287. with tarfile.open('{out}.temp.tar.gz'.format(out=args.out), "w:gz") as tar:
  1288. tar.add(temp_out, arcname=os.path.basename(temp_out), filter=tar_filter)
  1289. rmtree(temp_out)
  1290. def make_clump(args, log):
  1291. """
  1292. Clump summary stats, produce lead SNP report, produce candidate SNP report
  1293. TBD: refine output tables
  1294. TBD: in snps table, do an outer merge - e.i, include SNPs that pass p-value threshold (some without locus number), and SNPs without p-value (e.i. from reference genotypes)
  1295. """
  1296. #check_output_file(args.out, args.force)
  1297. #for chri in range(1, 23):
  1298. # check_input_file(sub_chr(args.bfile_chr, chri) + '.bed')
  1299. exclude_ranges = make_ranges(args.exclude_ranges, log)
  1300. temp_out = args.out + '.temp'
  1301. if not os.path.exists(temp_out):
  1302. os.makedirs(temp_out)
  1303. if (not args.sumstats) and (not args.sumstats_chr):
  1304. raise ValueError('At least one of --sumstats or --sumstats-chr must be specified')
  1305. if args.chr_labels is None:
  1306. args.chr_labels = list(range(1, 23))
  1307. if args.sumstats_chr:
  1308. args.sumstats_chr = [sub_chr(args.sumstats_chr, chri) for chri in args.chr_labels]
  1309. else:
  1310. args.sumstats_chr = ['{}/sumstats.chr{}.csv'.format(temp_out, chri) for chri in args.chr_labels]
  1311. def validate_columns(df):
  1312. for cname in [args.clump_field, args.clump_snp_field, args.chr]:
  1313. if cname not in df.columns:
  1314. raise ValueError('{} column not found in {}; available columns: '.format(cname, args.sumstats, df.columns))
  1315. if args.sumstats:
  1316. check_input_file(args.sumstats)
  1317. log.log('Reading {}...'.format(args.sumstats))
  1318. df_sumstats = pd.read_csv(args.sumstats, delim_whitespace=True)
  1319. log.log('Read {} SNPs from --sumstats file'.format(len(df_sumstats)))
  1320. validate_columns(df_sumstats)
  1321. for chri, df_chr_file in zip(args.chr_labels, args.sumstats_chr):
  1322. df_sumstats[df_sumstats[args.chr] == int(chri)].to_csv(df_chr_file, sep='\t',index=False)
  1323. else:
  1324. for df_chr_file in args.sumstats_chr:
  1325. check_input_file(df_chr_file)
  1326. log.log('Reading {}...'.format(args.sumstats_chr))
  1327. df_sumstats = pd.concat([pd.read_csv(df_chr_file, delim_whitespace=True) for df_chr_file in args.sumstats_chr])
  1328. log.log('Read {} SNPs'.format(len(df_sumstats)))
  1329. for chri, df_chr_file in zip(reversed(args.chr_labels), reversed(args.sumstats_chr)):
  1330. # Step1 - find independent significant SNPs
  1331. execute_command(
  1332. "{} ".format(args.plink) +
  1333. "--bfile {} ".format(sub_chr(args.bfile_chr, chri)) +
  1334. "--clump {} ".format(df_chr_file) +
  1335. "--clump-p1 {} --clump-p2 1 ".format(args.clump_p1) +
  1336. "--clump-r2 {} --clump-kb 1e9 ".format(args.indep_r2) +
  1337. "--clump-snp-field {} --clump-field {} ".format(args.clump_snp_field, args.clump_field) +
  1338. "--out {}/indep.chr{} ".format(temp_out, chri),
  1339. log)
  1340. if not os.path.isfile('{}/indep.chr{}.clumped'.format(temp_out, chri)):
  1341. log.log('On CHR {} no variants pass significance threshold'.format(chri))
  1342. continue
  1343. # Step 2 - find lead SNPs by clumping together independent significant SNPs
  1344. execute_command(
  1345. "{} ".format(args.plink) +
  1346. "--bfile {} ".format(sub_chr(args.bfile_chr, chri)) +
  1347. "--clump {} ".format('{}/indep.chr{}.clumped'.format(temp_out, chri)) +
  1348. "--clump-p1 {} --clump-p2 1 ".format(args.clump_p1) +
  1349. "--clump-r2 {} --clump-kb 1e9 ".format(args.lead_r2) +
  1350. "--clump-snp-field SNP --clump-field P " +
  1351. "--out {}/lead.chr{} ".format(temp_out, chri),
  1352. log)
  1353. # Step 3 - find loci around independent significant SNPs
  1354. pd.read_csv('{}/indep.chr{}.clumped'.format(temp_out, chri), delim_whitespace=True)['SNP'].to_csv('{}/indep.chr{}.clumped.snps'.format(temp_out, chri), index=False, header=False)
  1355. execute_command(
  1356. "{} ".format(args.plink) +
  1357. "--bfile {} ".format(sub_chr(args.bfile_chr, chri)) +
  1358. "--r2 --ld-window {} --ld-window-r2 {} ".format(args.ld_window_kb, args.indep_r2) +
  1359. "--ld-snp-list {out}/indep.chr{chri}.clumped.snps ".format(out=temp_out, chri=chri) +
  1360. "--out {out}/indep.chr{chri} ".format(out=temp_out, chri=chri),
  1361. log)
  1362. # find indep to lead SNP mapping (a data frame with columns 'LEAD' and 'INDEP')
  1363. files = ["{}/lead.chr{}.clumped".format(temp_out, chri) for chri in args.chr_labels]
  1364. files = [file for file in files if os.path.isfile(file)]
  1365. if not files:
  1366. log.log('WARNING: No .clumped files found - could it be that no variants pass significance threshold?')
  1367. clump_cleanup(args, log)
  1368. return
  1369. lead_to_indep = []
  1370. for file in files:
  1371. df=pd.read_csv(file,delim_whitespace=True)
  1372. lead_to_indep.append(pd.concat([pd.DataFrame(data=[(lead, indep_snp.split('(')[0]) for indep_snp in indep_snps.split(',') if indep_snp != 'NONE'] + [(lead, lead)], columns=['LEAD', 'INDEP']) for lead, indep_snps in zip(df['SNP'].values, df['SP2'].values)]))
  1373. lead_to_indep = pd.concat(lead_to_indep).reset_index(drop=True)
  1374. if lead_to_indep.duplicated(subset=['INDEP'], keep=False).any():
  1375. raise ValueError('Some independent significant SNP belongs to to lead SNPs; this is an internal error in sumstats.py logic - please report this bug.')
  1376. log.log('{} independent significant SNPs, {} lead SNPs'.format(len(set(lead_to_indep['INDEP'])), len(set(lead_to_indep['LEAD']))))
  1377. # group loci together:
  1378. # SNP_A is an indepependent significant SNP
  1379. # SNP_B is a candidate SNP
  1380. # LEAD_SNP is lead SNP
  1381. # R2 is always correlation between candidate and independent significant SNP
  1382. files = ["{out}/indep.chr{chri}.ld".format(out=temp_out, chri=chri) for chri in args.chr_labels]
  1383. files = [file for file in files if os.path.isfile(file)]
  1384. if not files: raise ValueError('No .ld files found')
  1385. df_cand=pd.concat([pd.read_csv(file, delim_whitespace=True) for file in files])
  1386. df_cand=pd.merge(df_cand, lead_to_indep.rename(columns={'INDEP':'SNP_A', 'LEAD':'LEAD_SNP'}), how='left', on='SNP_A')
  1387. df_sumstats.drop_duplicates(subset=args.clump_snp_field, inplace=True)
  1388. df_cand = pd.merge(df_cand, df_sumstats.rename(columns={args.clump_snp_field: 'SNP_B'}), how='left', on='SNP_B')
  1389. df_lead=df_cand.groupby(['LEAD_SNP', 'CHR_A']).agg({'BP_B':['min', 'max']})
  1390. df_lead.reset_index(inplace=True)
  1391. df_lead.columns=['LEAD_SNP', 'CHR_A', 'MinBP', 'MaxBP']
  1392. df_lead=df_lead.sort_values(['CHR_A', 'MinBP']).reset_index(drop=True)
  1393. df_lead['locusnum'] = df_lead.index + 1
  1394. df_lead = pd.merge(df_lead, df_sumstats[[args.clump_snp_field, args.clump_field]].rename(columns={args.clump_snp_field: 'LEAD_SNP'}), how='left', on='LEAD_SNP')
  1395. df_lead['MaxBP_locus'] = df_lead['MaxBP']
  1396. while True:
  1397. has_changes = False
  1398. for i in range(1, len(df_lead)):
  1399. if df_lead['CHR_A'][i] != df_lead['CHR_A'][i-1]: continue
  1400. merge_to_previous = False
  1401. if (df_lead['MinBP'][i] - df_lead['MaxBP_locus'][i-1]) < (1000 * 250):
  1402. merge_to_previous = True
  1403. for exrange in exclude_ranges:
  1404. if df_lead['CHR_A'][i] != exrange.chr: continue
  1405. if (df_lead['MaxBP_locus'][i-1] >= exrange.from_bp) and (df_lead['MinBP'][i] <= exrange.to_bp):
  1406. merge_to_previous = True
  1407. if merge_to_previous and (df_lead['locusnum'][i] != df_lead['locusnum'][i-1]):
  1408. log.log('Merge locus {} to {}'.format(df_lead['locusnum'][i], df_lead['locusnum'][i-1]))
  1409. df_lead['locusnum'][i] = df_lead['locusnum'][i-1]
  1410. df_lead['MaxBP_locus'][i] = np.max([df_lead['MaxBP_locus'][i-1], df_lead['MaxBP_locus'][i]])
  1411. has_changes = True
  1412. break
  1413. if not has_changes:
  1414. df_lead.drop(['MaxBP_locus'], axis=1, inplace=True)
  1415. break # exit from "while True:" loop
  1416. df_lead['locusnum'] = df_lead['locusnum'].map({l:i+1 for (i, l) in enumerate(df_lead['locusnum'].unique())})
  1417. df_lead['is_locus_lead'] = (df_lead[args.clump_field] == df_lead.groupby(['locusnum'])[args.clump_field].transform(min))
  1418. df_lead = pd.merge(df_lead, df_cand[['SNP_B', 'BP_B']].drop_duplicates().rename(columns={'SNP_B':'LEAD_SNP', 'BP_B':'LEAD_BP'}), how='left', on='LEAD_SNP')
  1419. cols = list(df_lead)
  1420. cols.insert(0, cols.pop(cols.index('LEAD_BP')))
  1421. cols.insert(0, cols.pop(cols.index('LEAD_SNP')))
  1422. cols.insert(0, cols.pop(cols.index('CHR_A')))
  1423. cols.insert(0, cols.pop(cols.index('locusnum')))
  1424. df_lead[cols].rename(columns={'CHR_A':'CHR'}).to_csv('{}.lead.csv'.format(args.out), sep='\t', index=False)
  1425. log.log('{} lead SNPs reported to {}.lead.csv'.format(len(df_lead), args.out))
  1426. df_loci=df_lead.groupby(['locusnum']).agg({'MinBP':'min', 'MaxBP':'max', 'CHR_A':'min', args.clump_field:'min'})
  1427. df_loci.reset_index(inplace=True)
  1428. df_loci=pd.merge(df_loci, df_lead[df_lead['is_locus_lead'].values][['locusnum', 'LEAD_SNP', 'LEAD_BP']], on='locusnum', how='left')
  1429. df_loci.rename(columns={'CHR_A':'CHR'})[['locusnum', 'CHR', 'LEAD_SNP', 'LEAD_BP', 'MinBP', 'MaxBP', args.clump_field]].to_csv('{}.loci.csv'.format(args.out), sep='\t', index=False)
  1430. log.log('{} loci reported to {}.loci.csv'.format(len(df_loci), args.out))
  1431. if 'BP' in df_cand: del df_cand['BP']
  1432. if 'CHR' in df_cand: del df_cand['CHR']
  1433. df_cand = pd.merge(df_cand, df_lead[['LEAD_SNP', 'locusnum']], how='left', on='LEAD_SNP')
  1434. cols = list(df_cand); cols.insert(0, cols.pop(cols.index('locusnum')))
  1435. df_cand = df_cand[cols].drop(['CHR_B'], axis=1).rename(columns={'CHR_A':'CHR', 'BP_A':'INDEP_BP', 'SNP_A':'INDEP_SNP', 'BP_B':'CAND_BP', 'SNP_B':'CAND_SNP'}).copy()
  1436. df_cand.to_csv('{}.snps.csv'.format(args.out), sep='\t', index=False)
  1437. log.log('{} candidate SNPs reported to {}.snps.csv'.format(len(df_cand), args.out))
  1438. df_indep=df_cand[df_cand['CAND_SNP'] == df_cand['INDEP_SNP']].copy()
  1439. df_indep.drop(['CAND_BP','CAND_SNP', 'R2'], axis=1, inplace=True)
  1440. df_indep.to_csv('{}.indep.csv'.format(args.out), sep='\t', index=False)
  1441. log.log('{} independent significant SNPs reported to {}.snps.csv'.format(len(df_indep), args.out))
  1442. clump_cleanup(args, log)
  1443. ### =================================================================================
  1444. ### Implementation for parser_rs
  1445. ### =================================================================================
  1446. def make_rs(args, log):
  1447. """
  1448. Augument summary statistic file with SNP RS number from reference file.
  1449. Merging is done on chromosome and position.
  1450. """
  1451. check_input_file(args.ref)
  1452. check_input_file(args.sumstats)
  1453. check_output_file(args.out, force=args.force)
  1454. log.log('Reading reference file {}...'.format(args.ref))
  1455. usecols = [cols.SNP, cols.CHR, cols.BP]
  1456. if args.a1a2: usecols = usecols + ['A1', 'A2']
  1457. ref_file = pd.read_csv(args.ref, sep='\t', usecols=usecols)
  1458. log.log("Reference dict contains {d} snps.".format(d=len(ref_file)))
  1459. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  1460. reader = pd.read_csv(args.sumstats, dtype=str, sep='\t', chunksize=args.chunksize)
  1461. n_snps = 0
  1462. with open(args.out, 'a') as out_f:
  1463. for chunk_index, chunk in enumerate(reader):
  1464. if cols.SNP in chunk: chunk.drop(cols.SNP, axis=1, inplace=True)
  1465. if args.a1a2:
  1466. if (cols.A1 in chunk): chunk.drop(cols.A1, axis=1, inplace=True)
  1467. if (cols.A2 in chunk): chunk.drop(cols.A2, axis=1, inplace=True)
  1468. chunk.BP = chunk.BP.astype(int)
  1469. chunk.CHR = chunk.CHR.astype(int)
  1470. chunk = pd.merge(chunk, ref_file, how='left', on=[cols.CHR, cols.BP])
  1471. chunk = chunk.sort_index(axis=1)
  1472. chunk.to_csv(out_f, index=False, header=(chunk_index==0), sep='\t', na_rep='NA')
  1473. n_snps += len(chunk)
  1474. eprint("{f}: {n} lines processed".format(f=args.sumstats, n=(chunk_index+1)*args.chunksize))
  1475. log.log("{n} SNPs saved to {f}".format(n=n_snps, f=args.out))
  1476. ### =================================================================================
  1477. ### Implementation for make_ls
  1478. ### =================================================================================
  1479. def make_ls(args, log):
  1480. ml = max([len(os.path.basename(file).replace('.csv.gz', '')) for file in glob.glob(args.path)])
  1481. cols_list = [x for x in cols._asdict() if x not in ['A1A2', 'CHRPOS', 'CHRPOSA1A2', 'SNP', 'CHR', 'BP', 'PVAL', 'A1', 'A2']]
  1482. log.log('{f}\t{n}\t{c}'.format(f='file'.ljust(ml),n='#snp'.ljust(9),c='\t'.join([x.replace('NCONTROL', 'NCONT.') for x in cols_list])))
  1483. for file in glob.glob(args.path):
  1484. if not os.path.isfile(file): continue
  1485. if '_noMHC' in file: continue
  1486. num_snps = np.nan; n = np.nan; ncase = np.nan; ncontrol = np.nan
  1487. try:
  1488. file_log = os.path.splitext(file)[0] + '.log'
  1489. if os.path.isfile(file_log):
  1490. lines = open(file_log, 'r').readlines()
  1491. num_snps = [int(x.group(1)) for x in [re.search('([0-9]+) SNPs saved to', line.strip()) for line in lines] if x][0]
  1492. n = [float(x.group(1)) for x in [re.search(r'Sample size.* N=([^ ]+) ', line.strip()) for line in lines] if x][0]
  1493. ncase = [float(x.group(1)) for x in [re.search(r'Sample size.* NCASE=([^ ]+) ', line.strip()) for line in lines] if x][0]
  1494. ncontrol = [float(x.group(1)) for x in [re.search(r'Sample size.* NCONTROL=([^ ]+)', line.strip()) for line in lines] if x][0]
  1495. except:
  1496. pass
  1497. num_snps = 'n/a' if np.isnan(num_snps) else str(num_snps)
  1498. n = 'n/a' if np.isnan(n) else str(int(n))
  1499. ncase = 'n/a' if np.isnan(ncase) else str(int(ncase))
  1500. ncontrol = 'n/a' if np.isnan(ncontrol) else str(int(ncontrol))
  1501. for chunk in pd.read_csv(file, sep='\t', chunksize=1):
  1502. yes_no_or_sample_size = [( n if (x == 'N') else
  1503. ncase if (x == 'NCASE') else
  1504. ncontrol if (x == 'NCONTROL') else
  1505. 'YES' if x in chunk
  1506. else '-') for x in cols_list]
  1507. log.log('{f}\t{n}\t{c}'.format(f=os.path.basename(file).replace('.csv.gz', '').ljust(ml), c='\t'.join(yes_no_or_sample_size),n=str(num_snps).ljust(9)))
  1508. break
  1509. log.log('Columns description:')
  1510. for cname in sorted(cols._asdict()):
  1511. log.log('{c}\t{d}'.format(c=cname, d=describe_cname[cname]))
  1512. ### =================================================================================
  1513. ### Implementation for mat_to_csv
  1514. ### =================================================================================
  1515. def mat_to_csv(args, log):
  1516. check_input_file(args.ref)
  1517. check_input_file(args.mat)
  1518. check_output_file(args.out, args.force)
  1519. log.log('Reading reference file {}...'.format(args.ref))
  1520. ref_file = pd.read_csv(args.ref, sep='\t', usecols=[cols.SNP, cols.A1, cols.A2])
  1521. log.log("Reference dict contains {d} snps.".format(d=len(ref_file)))
  1522. log.log('Reading .mat file {}...'.format(args.mat))
  1523. df = ref_file.copy()
  1524. sumstats = sio.loadmat(args.mat)
  1525. for key in sumstats.keys():
  1526. if key.lower().startswith('zvec'):
  1527. df['Z'] = sumstats[key]
  1528. log.log('Found zvec, {} non-nan values.'.format(np.sum(~np.isnan(sumstats[key]))))
  1529. if key.lower().startswith('logpvec'):
  1530. df['PVAL'] = np.power(10, -sumstats[key])
  1531. log.log('Found logpvec, {} non-nan values.'.format(np.sum(~np.isnan(sumstats[key]))))
  1532. if key.lower().startswith('nvec'):
  1533. df['N'] = sumstats[key]
  1534. log.log('Found nvec, {} non-nan values.'.format(np.sum(~np.isnan(sumstats[key]))))
  1535. df.to_csv(args.out,
  1536. index=False, header=True, sep='\t', na_rep=args.na_rep,
  1537. compression='gzip' if args.gzip else None)
  1538. log.log('Result is written into {}'.format(args.out))
  1539. ### =================================================================================
  1540. ### Implementation for ldsc_to_mat
  1541. ### =================================================================================
  1542. def ldsc_to_mat(args, log):
  1543. check_input_file(args.ref)
  1544. if args.sumstats: check_input_file(args.sumstats)
  1545. if args.chr_labels is None:
  1546. args.chr_labels = list(range(1, 23))
  1547. for chri in args.chr_labels:
  1548. if args.ldscore: check_input_file(args.ldscore.replace('@', str(chri)))
  1549. if args.annot: check_input_file(args.annot.replace('@', str(chri)))
  1550. if args.M: check_input_file(args.M.replace('@', str(chri)))
  1551. if args.M_5_50: check_input_file(args.M_5_50.replace('@', str(chri)))
  1552. check_output_file(args.out, args.force)
  1553. log.log('Reading reference file {}...'.format(args.ref))
  1554. ref_file = pd.read_csv(args.ref, sep='\t', usecols=[cols.SNP, cols.A1, cols.A2])
  1555. log.log("Reference dict contains {d} snps.".format(d=len(ref_file)))
  1556. save_dict = {}
  1557. if args.sumstats:
  1558. df_sumstats = pd.read_csv(args.sumstats, sep='\t')
  1559. df_len = len(df_sumstats)
  1560. log.log('Read {} SNPs from --sumstats file'.format(df_len))
  1561. df_sumstats = pd.merge(ref_file, df_sumstats, how='left', on=cols.SNP)
  1562. df_sumstats = df_sumstats.dropna(how='any')
  1563. log.log('Removed {} SNPs not in --ref file'.format(df_len - len(df_sumstats)))
  1564. df_len = len(df_sumstats)
  1565. df_sumstats['ALLELES'] = df_sumstats.A1_x + df_sumstats.A2_x + df_sumstats.A1_y + df_sumstats.A2_y
  1566. df_sumstats = df_sumstats[filter_alleles(df_sumstats['ALLELES'])].copy()
  1567. log.log('Removed {} SNPs with alleles not matching --ref file'.format(df_len - len(df_sumstats)))
  1568. log.log('{} SNPs remain'.format(len(df_sumstats)))
  1569. df_z = df_sumstats.Z.copy()
  1570. df_sumstats.Z = align_alleles(df_sumstats.Z, df_sumstats['ALLELES'])
  1571. log.log('Flip z score for {} SNPs'.format((df_sumstats.Z != df_z).sum()))
  1572. df_sumstats.drop(['A1_x', 'A2_x', 'A1_y', 'A2_y','ALLELES'], axis=1, inplace=True)
  1573. df_sumstats = pd.merge(ref_file, df_sumstats, how='left', on=cols.SNP)
  1574. save_dict['zvec'] = df_sumstats["Z"].values
  1575. save_dict['nvec'] = df_sumstats["N"].values
  1576. if args.ldscore:
  1577. df_ldscore = pd.concat([pd.read_csv(args.ldscore.replace('@', str(chri)), delim_whitespace=True) for chri in args.chr_labels])
  1578. df_ldscore.drop([x for x in ['CHR', 'BP', 'CM', 'MAF'] if x in df_ldscore], inplace=True, axis=1)
  1579. log.log('Shape of ldscore file: {shape}'.format(shape=df_ldscore.shape))
  1580. df_ldscore = pd.merge(ref_file[['SNP']], df_ldscore, how='left', on='SNP')
  1581. del df_ldscore['SNP']
  1582. log.log('Shape of ldscore file after merge: {shape}'.format(shape=df_ldscore.shape))
  1583. save_dict['annonames'] = list(df_ldscore.columns)
  1584. save_dict['annomat'] = df_ldscore.values
  1585. if args.annot:
  1586. df_annot = pd.concat([pd.read_csv(args.annot.replace('@', str(chri)), delim_whitespace=True) for chri in args.chr_labels])
  1587. df_annot.drop([x for x in ['CHR', 'BP', 'CM', 'MAF'] if x in df_annot], inplace=True, axis=1)
  1588. log.log('Shape of annots file: {shape}'.format(shape=df_annot.shape))
  1589. df_annot = pd.merge(ref_file[['SNP']], df_annot, how='left', on='SNP')
  1590. del df_annot['SNP']
  1591. log.log('Shape of annots file after merge: {shape}'.format(shape=df_annot.shape))
  1592. save_dict['annonames_bin'] = list(df_annot.columns)
  1593. save_dict['annomat_bin'] = df_annot.values
  1594. if args.M_5_50:
  1595. m_5_50 = pd.concat([pd.read_csv(args.M_5_50.replace('@', str(chri)), delim_whitespace=True, header=None) for chri in args.chr_labels])
  1596. m_5_50 = np.atleast_2d(m_5_50.sum().values)
  1597. log.log('M_5_50={}'.format(m_5_50))
  1598. save_dict['M_5_50']=m_5_50
  1599. if args.M:
  1600. m = pd.concat([pd.read_csv(args.M.replace('@', str(chri)), delim_whitespace=True, header=None) for chri in args.chr_labels])
  1601. m = np.atleast_2d(m.sum().values)
  1602. log.log('M={}'.format(m))
  1603. save_dict['M']=m
  1604. sio.savemat(args.out, save_dict, format='5', do_compression=False, oned_as='column', appendmat=False)
  1605. log.log('Result written to {f}'.format(f=args.out))
  1606. ### =================================================================================
  1607. ### Implementation for frq_to_mat
  1608. ### =================================================================================
  1609. def frq_to_mat(args, log):
  1610. if args.ref:
  1611. check_input_file(args.ref)
  1612. if args.chr_labels is None:
  1613. args.chr_labels = list(range(1, 23))
  1614. if args.frq and args.afreq:
  1615. raise ValueError('--frq and --afreq must not be used together')
  1616. frq = args.frq if args.frq else args.afreq
  1617. snp_col = 'SNP' if args.frq else 'ID'
  1618. maf_col = 'MAF' if args.frq else 'ALT_FREQS'
  1619. for chri in args.chr_labels:
  1620. check_input_file(frq.replace('@', str(chri)))
  1621. check_output_file(args.out, args.force)
  1622. if args.ref:
  1623. log.log('Reading reference file {}...'.format(args.ref))
  1624. ref_file = pd.read_csv(args.ref, sep='\t', usecols=[cols.SNP, cols.A1, cols.A2])
  1625. log.log("Reference dict contains {d} snps.".format(d=len(ref_file)))
  1626. save_dict = {}
  1627. df_frq = pd.concat([pd.read_csv(frq.replace('@', str(chri)), delim_whitespace=True) for chri in args.chr_labels])
  1628. log.log('Input file contains {d} snps'.format(d=len(df_frq)))
  1629. if args.ref:
  1630. df_frq.drop_duplicates(subset='SNP', inplace=True)
  1631. log.log('After drop duplicates, file contains {d} snps'.format(d=len(df_frq)))
  1632. df_frq = pd.merge(ref_file[['SNP']], df_frq, how='left', left_on='SNP', right_on=snp_col)
  1633. log.log('{d} non-null values after merging with ref file'.format(d=df_frq[maf_col].notnull().sum()))
  1634. save_dict['mafvec'] = df_frq[maf_col].values
  1635. sio.savemat(args.out, save_dict, format='5', do_compression=False, oned_as='column', appendmat=False)
  1636. log.log('Result written to {f}'.format(f=args.out))
  1637. ### =================================================================================
  1638. ### Implementation for ref_to_mat
  1639. ### =================================================================================
  1640. def ref_to_mat(args, log):
  1641. check_input_file(args.ref)
  1642. check_output_file(args.out, args.force)
  1643. log.log('Reading reference file {}...'.format(args.ref))
  1644. ref_file = pd.read_csv(args.ref, sep='\t')
  1645. log.log("Reference dict contains {d} snps.".format(d=len(ref_file)))
  1646. save_dict = {}
  1647. for col in ref_file.columns:
  1648. # skip fields SNP, A1, A2 because they are saved as cell array
  1649. if args.numeric_only and ref_file[col].dtype == object:
  1650. continue
  1651. save_dict[col] = ref_file[col].values
  1652. sio.savemat(args.out, save_dict, format='5', do_compression=False, oned_as='column', appendmat=False)
  1653. log.log('Result written to {f}'.format(f=args.out))
  1654. ### =================================================================================
  1655. ### Implementation for ldsum
  1656. ### =================================================================================
  1657. def ldsum(args, log):
  1658. # Adjust r2_min and r2_max thresholds.
  1659. # They must be vectors (e.g. [None] instead of None).
  1660. # Values of 0.0 and 1.0 must be replaced with None to avoid filtering.
  1661. # Mathematicaly all r2 values are within [0, 1], but due to float-point precision
  1662. # some values may exceed 1 my small amount, and we don't want them to be filtered.
  1663. if args.r2_min is None: args.r2_min = [0.0]
  1664. if args.r2_max is None: args.r2_max = [1.0]
  1665. args.r2_min = [None if __isclose__(x, 0.0) else x for x in args.r2_min]
  1666. args.r2_max = [None if __isclose__(x, 1.0) else x for x in args.r2_max]
  1667. if len(args.r2_min) != len(args.r2_max):
  1668. raise(ValueError('--r2-min and --r2-max arguments must have equal length'))
  1669. if args.per_allele and not args.frq:
  1670. raise(ValueError('--frq argument is required for --per-allele'))
  1671. check_input_file(args.bim)
  1672. check_input_file(args.ld)
  1673. if args.frq and args.per_allele: check_input_file(args.frq)
  1674. check_output_file(args.out + ".l2.ldscore.gz", args.force)
  1675. check_output_file(args.out + ".l4.ldscore.gz", args.force)
  1676. log.log('Reading {}...'.format(args.bim))
  1677. ref = pd.read_csv(args.bim, delim_whitespace=True, header=None, names=['CHR', 'SNP', 'GP', 'BP', 'A1', 'A2'])
  1678. ref['INDEX_A'] = ref.index
  1679. ref['INDEX_B'] = ref.index
  1680. ref['SNP_A'] = ref['SNP']
  1681. ref['SNP_B'] = ref['SNP']
  1682. if len(ref.drop_duplicates(subset=['SNP'], keep='first', inplace=False)) != len(ref):
  1683. raise(ValueError('--bim file contains duplicated markers, which is not allowed'))
  1684. log.log('Done, {} markers found'.format(len(ref)))
  1685. if args.frq and args.per_allele:
  1686. log.log('Reading {}...'.format(args.frq))
  1687. frq = pd.read_csv(args.frq, delim_whitespace=True)
  1688. if len(frq) != len(ref):
  1689. raise(ValueError('--frq file is not consistent with --ref file'))
  1690. log.log('Done, {} markers found'.format(len(frq)))
  1691. ref['HVEC'] = 2 * np.multiply(frq['MAF'].values, 1.0 - frq['MAF'].values)
  1692. else:
  1693. ref['HVEC'] = 1
  1694. l2 = ref[['CHR', 'SNP', 'BP']].copy()
  1695. l4 = ref[['CHR', 'SNP', 'BP']].copy()
  1696. for r2_bin, (r2_min, r2_max) in enumerate(zip(args.r2_min, args.r2_max)):
  1697. suffix = '.{}'.format(r2_bin) if len(args.r2_min) > 0 else ''
  1698. l2['L2' + suffix] = 0.0
  1699. l4['L4' + suffix] = 0.0
  1700. log.log('Reading {} in chunks of {} lines at a time...'.format(args.ld, args.chunksize))
  1701. n_snps = 0
  1702. for chunk_index, ld in enumerate(pd.read_csv(args.ld, delim_whitespace=True, chunksize=args.chunksize, usecols=['SNP_A', 'SNP_B', 'R2'])):
  1703. n_snps += len(ld)
  1704. ld_t = ld.copy()
  1705. ld_t['SNP_A'] = ld['SNP_B']
  1706. ld_t['SNP_B'] = ld['SNP_A']
  1707. ld = pd.concat([ld, ld_t])
  1708. incl_diag = ''
  1709. if (not args.not_diag) and (chunk_index==0):
  1710. ld_diag = ref[['SNP', 'SNP']].copy()
  1711. ld_diag.columns = ['SNP_A', 'SNP_B']
  1712. ld_diag['R2'] = 1.0
  1713. ld = pd.concat([ld, ld_diag])
  1714. incl_diag = ' (including diagonal)'
  1715. ld = pd.merge(ld, ref[['SNP_A','INDEX_A']], how='left', on='SNP_A')
  1716. ld = pd.merge(ld, ref[['SNP_B','INDEX_B', 'HVEC']], how='left', on='SNP_B')
  1717. # Set r2 to the product of H2 and HVEC (the later could be 1.0, when one runs without --per-allele)
  1718. ld['R2'] = np.multiply(ld['R2'].values, ld['HVEC'].values)
  1719. r2_per_bin = np.zeros(len(args.r2_min))
  1720. for r2_bin, (r2_min, r2_max) in enumerate(zip(args.r2_min, args.r2_max)):
  1721. suffix = '.{}'.format(r2_bin) if len(args.r2_min) > 0 else ''
  1722. ld['idx'] = True
  1723. if (r2_min is not None): ld['idx'] = ld['idx'] & (ld['R2'] > r2_min)
  1724. if (r2_max is not None): ld['idx'] = ld['idx'] & (ld['R2'] <= r2_max)
  1725. idx = ld['idx'].values
  1726. vals = ld['R2'][idx].values
  1727. rows = ld['INDEX_A'][idx].values
  1728. cols = ld['INDEX_B'][idx].values
  1729. csr = scipy.sparse.coo_matrix((vals, (rows, cols)), shape=(len(ref), len(ref))).tocsr()
  1730. csr_sqr = scipy.sparse.coo_matrix((np.power(vals, 2), (rows, cols)), shape=(len(ref), len(ref))).tocsr()
  1731. r2_per_bin[r2_bin] = csr.nnz
  1732. l2['L2' + suffix] += csr.dot(np.ones((len(ref), 1))).reshape(len(ref))
  1733. l4['L4' + suffix] += csr_sqr.dot(np.ones((len(ref), 1))).reshape(len(ref))
  1734. eprint("{f}: {n} lines finished, number of r2 in the last --l2 chunk: {r}{d}".format(f=args.ld, n=n_snps,r=', '.join([str(int(x)) for x in r2_per_bin]), d=incl_diag))
  1735. log.log('Writting {}...'.format(args.out + ".l2.ldscore.gz"))
  1736. l2.to_csv(args.out + ".l2.ldscore.gz", sep='\t', index=False, compression='gzip')
  1737. log.log('Writting {}...'.format(args.out + ".l4.ldscore.gz"))
  1738. l4.to_csv(args.out + ".l4.ldscore.gz", sep='\t', index=False, compression='gzip')
  1739. log.log('Done.')
  1740. ### =================================================================================
  1741. ### Implementation for diff_mat
  1742. ### =================================================================================
  1743. def diff_mat(args, log):
  1744. check_input_file(args.mat1)
  1745. check_input_file(args.mat2)
  1746. check_input_file(args.ref)
  1747. if args.sumstats: check_input_file(args.sumstats)
  1748. check_output_file(args.out, args.force)
  1749. def read_mat_file(filename, log):
  1750. log.log('Reading .mat file {}...'.format(filename))
  1751. mat = sio.loadmat(filename)
  1752. log.log('Found variables: {}'.format([x for x in mat.keys() if x not in ['__version__', '__header__', '__globals__']]))
  1753. nvec = None; zvec = None; logpvec = None
  1754. for key in mat.keys():
  1755. if key.lower().startswith('zvec'): zvec = mat[key]
  1756. if key.lower().startswith('logpvec'): logpvec = mat[key]
  1757. if key.lower().startswith('nvec'): nvec = mat[key]
  1758. return (zvec, logpvec, nvec)
  1759. def compare_vectors(v1, v2, log):
  1760. s1 = v1 is not None
  1761. s2 = v2 is not None
  1762. log.log('\tIs present in the first file? {}'.format('YES' if s1 else 'NO'))
  1763. log.log('\tIs present in the second file? {}'.format('YES' if s2 else 'NO'))
  1764. if not s1 or not s2: return (None, None)
  1765. s1 = len(v1); s2 = len(v2)
  1766. log.log('\tHave equal length in both files? {}'.format('YES, {}'.format(s1) if s1==s2 else 'NO, {} vs {}'.format(s1, s2)))
  1767. if s1 != s2: return (None, None)
  1768. s = all(np.isnan(v1) == np.isnan(v2))
  1769. log.log('\tHave equal pattern of defined values? {}'.format('YES' if s else 'NO'))
  1770. if not s:
  1771. s1 = np.sum(np.isnan(v1)); s2 = np.sum(np.isnan(v2))
  1772. log.log('\t\tHave equal number of undefined values? {}'.format('YES, {}'.format(s1) if s1==s2 else 'NO, {} vs {} - difference is {}'.format(s1, s2, abs(s1-s2))))
  1773. s1 = np.sum(~np.isnan(v1) & np.isnan(v2))
  1774. s2 = np.sum(np.isnan(v1) & ~np.isnan(v2))
  1775. log.log('\t\tHow many values are defined in the first file but not in the second file? {}'.format(s1))
  1776. log.log('\t\tHow many values are defined in the second file but not in the first file? {}'.format(s2))
  1777. # Select values defined in both vectors
  1778. idx_def = ~np.isnan(v1) & ~np.isnan(v2)
  1779. def_v1 = v1[idx_def]
  1780. def_v2 = v2[idx_def]
  1781. idx_diff_sign = ((def_v1 <= 0) & (def_v2 > 0)) | ((def_v1 > 0) & (def_v2 <= 0))
  1782. idx_diff_value = np.greater(abs(def_v1 - def_v2), 1e-5)
  1783. log.log('\tAll defined values are equal in both files? {}'.format('YES' if all(def_v1 == def_v2) else 'NO, {} values are different'.format(np.sum(def_v1 != def_v2))))
  1784. if not all(def_v1 == def_v2):
  1785. s = np.sum(idx_diff_sign)
  1786. log.log('\t\tMean(Std) for the first file? {} ({})'.format(np.mean(def_v1), np.std(def_v1)))
  1787. log.log('\t\tMean(Std) for the second file? {} ({})'.format(np.mean(def_v2), np.std(def_v2)))
  1788. log.log('\t\tAll values have equal sign? {}'.format('YES' if s == 0 else 'NO, {} values have different sign'.format(s)))
  1789. log.log('\t\tMaximum difference of absolute values? {}'.format(max(abs(abs(def_v1) - abs(def_v2)))))
  1790. log.log('\t\tNumber of values with absolute difference above 1e-5? {}'.format(sum(idx_diff_value)))
  1791. # Return index of SNPs that we consider to be different
  1792. # First vector, idx_diff_nan_or_sign, indicate where nan pattern is different or sign is different
  1793. # Second vector, idx_diff_nan_or_sign_or_value, indicates where nan pattern is different or sign is different or value is different
  1794. idx_diff_nan_or_sign = (np.isnan(v1) != np.isnan(v2))
  1795. idx_diff_nan_or_sign[idx_def] = idx_diff_sign
  1796. idx_diff_nan_or_sign_or_value = (np.isnan(v1) != np.isnan(v2))
  1797. idx_diff_nan_or_sign_or_value[idx_def] = idx_diff_value | idx_diff_sign
  1798. return (idx_diff_nan_or_sign, idx_diff_nan_or_sign_or_value)
  1799. [zvec1, logpvec1, nvec1] = read_mat_file(args.mat1, log)
  1800. [zvec2, logpvec2, nvec2] = read_mat_file(args.mat2, log)
  1801. log.log('{}:'.format('zvec'))
  1802. (zvec_diff, _) = compare_vectors(zvec1, zvec2, log)
  1803. log.log('{}:'.format('logpvec'))
  1804. (_, logpvec_diff) = compare_vectors(logpvec1, logpvec2, log)
  1805. log.log('{}:'.format('nvec'))
  1806. (_, nvec_diff) = compare_vectors(nvec1, nvec2, log)
  1807. diff = np.logical_or.reduce([diff for diff in [zvec_diff, logpvec_diff, nvec_diff] if diff is not None])
  1808. log.log('Overall, {} markers appears to be different.'.format(np.sum(diff)))
  1809. if np.sum(diff) == 0:
  1810. open(args.out, 'a').close()
  1811. return
  1812. log.log('Reading reference file {}...'.format(args.ref))
  1813. ref = pd.read_csv(args.ref, sep='\t', usecols=[cols.SNP, cols.CHR, cols.BP])
  1814. log.log("Reference dict contains {d} snps.".format(d=len(ref)))
  1815. # Insert not-null vectors into the data frame
  1816. vectors = {'zvec1':zvec1, 'zvec2':zvec2, 'logpvec1':logpvec1, 'logpvec2':logpvec2, 'nvec1':nvec1, 'nvec2':nvec2}
  1817. for k in vectors:
  1818. if (vectors[k] is not None) and (len(vectors[k]) == len(ref)):
  1819. ref[k] = vectors[k]
  1820. ref = ref.loc[[i for i, x in enumerate(diff) if x]]
  1821. if args.sumstats:
  1822. log.log('Reading sumstats file {}...'.format(args.sumstats))
  1823. sumstats = pd.read_csv(args.sumstats, sep='\t')
  1824. log.log("Sumstats file contains {d} markers.".format(d=len(sumstats)))
  1825. sumstats_cols = sumstats.columns
  1826. if cols.SNP in sumstats_cols:
  1827. sumstats.columns = [(x + '_onSNP' if x != 'SNP' else x) for x in sumstats_cols]
  1828. ref = pd.merge(ref, sumstats, how='left', on='SNP')
  1829. if cols.CHR in sumstats_cols and cols.BP in sumstats_cols:
  1830. sumstats.columns = [(x + '_onCHRPOS' if x not in ['CHR', 'BP'] else x) for x in sumstats_cols]
  1831. ref = pd.merge(ref, sumstats, how='left', on=['CHR', 'BP'])
  1832. ref.to_csv(args.out, sep='\t', index=False, na_rep='NA')
  1833. log.log('Result is written into {}'.format(args.out))
  1834. ### =================================================================================
  1835. ### Implementation for parser_neff
  1836. ### =================================================================================
  1837. def make_neff(args, log):
  1838. """
  1839. Generate N column from NCASE and NCONTROL
  1840. """
  1841. if args.sumstats == '-': args.sumstats = sys.stdin
  1842. if args.out == '-': args.out = sys.stdout
  1843. check_input_file(args.sumstats)
  1844. check_output_file(args.out, args.force)
  1845. log.log('Reading summary statistics file {}...'.format(args.sumstats))
  1846. df = pd.read_csv(args.sumstats, delim_whitespace=True)
  1847. if 'N' in df.columns:
  1848. if (('NCASE' not in df.columns) or ('NCONTROL' not in df.columns)):
  1849. log.log('WARNING: N column is alredy present, NCASE/NCONTROL columns are not available. Nothing to be done.')
  1850. df.to_csv(args.out, sep='\t', index=False, na_rep='NA')
  1851. log.log("{n} SNPs saved to {f}".format(n=len(df), f=args.out))
  1852. return
  1853. log.log('WARNING: N column is already present and will be overwritten.')
  1854. if args.factor > 0:
  1855. df['N'] = np.divide(args.factor, 1./df['NCASE'] + 1./df['NCONTROL'])
  1856. else:
  1857. df['N'] = df['NCASE'] + df['NCONTROL']
  1858. if args.drop: df.drop(['NCASE', 'NCONTROL'], axis=1, inplace=True)
  1859. df.to_csv(args.out, sep='\t', index=False, na_rep='NA')
  1860. log.log("{n} SNPs saved to {f}".format(n=len(df), f=args.out))
  1861. ### =================================================================================
  1862. ### Misc stuff and helpers
  1863. ### =================================================================================
  1864. def sec_to_str(t):
  1865. '''Convert seconds to days:hours:minutes:seconds'''
  1866. [d, h, m, s, n] = six.moves.reduce(lambda ll, b : divmod(ll[0], b) + ll[1:], [(t, 1), 60, 60, 24])
  1867. f = ''
  1868. if d > 0:
  1869. f += '{D}d:'.format(D=d)
  1870. if h > 0:
  1871. f += '{H}h:'.format(H=h)
  1872. if m > 0:
  1873. f += '{M}m:'.format(M=m)
  1874. f += '{S}s'.format(S=s)
  1875. return f
  1876. def eprint(*args, **kwargs):
  1877. print(*args, file=sys.stderr, **kwargs)
  1878. class Logger(object):
  1879. '''
  1880. Lightweight logging.
  1881. '''
  1882. def __init__(self, fh, mode):
  1883. self.fh = fh
  1884. self.log_fh = open(fh, mode) if (fh is not None) else None
  1885. # remove error file from previous run if it exists
  1886. try:
  1887. if fh is not None: os.remove(fh + '.error')
  1888. except OSError:
  1889. pass
  1890. def log(self, msg):
  1891. '''
  1892. Print to log file and stdout with a single command.
  1893. '''
  1894. eprint(msg)
  1895. if self.log_fh:
  1896. self.log_fh.write(str(msg).rstrip() + '\n')
  1897. self.log_fh.flush()
  1898. def error(self, msg):
  1899. '''
  1900. Print to log file, error file and stdout with a single command.
  1901. '''
  1902. eprint(msg)
  1903. if self.log_fh:
  1904. self.log_fh.write(str(msg).rstrip() + '\n')
  1905. with open(self.fh + '.error', 'w') as error_fh:
  1906. error_fh.write(str(msg).rstrip() + '\n')
  1907. def wait_for_system_memory(log):
  1908. # safety guard to ensure that at least 25% of system memory is available
  1909. try:
  1910. import psutil
  1911. if psutil.virtual_memory().percent < 80:
  1912. return
  1913. log.log('Waiting for at least 20% of system memory to be available...')
  1914. while psutil.virtual_memory().percent > 80:
  1915. time.sleep(5)
  1916. except ImportError:
  1917. pass
  1918. def sub_chr(s, chr):
  1919. '''Substitute chr for @, else append chr to the end of str.'''
  1920. if '@' not in s:
  1921. s += '@'
  1922. return s.replace('@', str(chr))
  1923. def execute_command(command, log):
  1924. log.log("Execute command: {}".format(command))
  1925. exit_code = subprocess.call(command.split())
  1926. log.log('Done. Exit code: {}'.format(exit_code))
  1927. return exit_code
  1928. def make_ranges(args_exclude_ranges, log):
  1929. # Interpret --exclude-ranges input
  1930. ChromosomeRange = collections.namedtuple('ChromosomeRange', ['chr', 'from_bp', 'to_bp'])
  1931. exclude_ranges = []
  1932. if args_exclude_ranges is not None:
  1933. for exclude_range in args_exclude_ranges:
  1934. try:
  1935. range = ChromosomeRange._make([int(x) for x in exclude_range.replace(':', ' ').replace('-', ' ').split()[:3]])
  1936. except Exception as e:
  1937. raise(ValueError('Unable to interpret exclude range "{}", chr:from-to format is expected.'.format(exclude_range)))
  1938. exclude_ranges.append(range)
  1939. log.log('Exclude range: chromosome {} from BP {} to {}'.format(range.chr, range.from_bp, range.to_bp))
  1940. return exclude_ranges
  1941. def __isclose__(a, b, rel_tol=1e-09, abs_tol=0.0):
  1942. return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
  1943. def fix_columns_order(sumstats):
  1944. # Ensure that all standard columns go first (in the order define dby namedtuple 'Cols'), following by non-standard columns
  1945. cols_std = [c for c in Cols._fields if c in sumstats.columns]
  1946. cols_other = [c for c in sumstats.columns if c not in Cols._fields]
  1947. return sumstats[cols_std + cols_other]
  1948. ### =================================================================================
  1949. ### Main section
  1950. ### =================================================================================
  1951. if __name__ == "__main__":
  1952. args = parse_args(sys.argv[1:])
  1953. if args.out is None:
  1954. raise ValueError('--out is required.')
  1955. log = Logger(args.log if args.log else (args.out + '.log' if (args.out != '-') else None), 'a' if args.log_append else 'w')
  1956. start_time = time.time()
  1957. try:
  1958. defaults = vars(parse_args([sys.argv[1]]))
  1959. opts = vars(args)
  1960. non_defaults = [x for x in opts.keys() if opts[x] != defaults[x]]
  1961. header = MASTHEAD
  1962. header += "Call: \n"
  1963. header += './sumstats.py {} \\\n'.format(sys.argv[1])
  1964. options = ['\t--'+x.replace('_','-')+' '+str(opts[x]).replace('\t', '\\t')+' \\' for x in non_defaults]
  1965. header += '\n'.join(options).replace('True','').replace('False','')
  1966. header = header[0:-1]+'\n'
  1967. log.log(header)
  1968. wait_for_system_memory(log)
  1969. log.log('Beginning analysis at {T} by {U}, host {H}'.format(T=time.ctime(), U=getpass.getuser(), H=socket.gethostname()))
  1970. # run the analysis
  1971. args.func(args, log)
  1972. except Exception:
  1973. log.error( traceback.format_exc() )
  1974. raise
  1975. finally:
  1976. log.log('Analysis finished at {T}'.format(T=time.ctime()) )
  1977. time_elapsed = round(time.time()-start_time,2)
  1978. log.log('Total time elapsed: {T}'.format(T=sec_to_str(time_elapsed)))

sumstats.py at commit e46ebdf, under GPL-3.0 · at the source

Overview

Authors: Anja Hollowell1,2,3, Anna Gui1,4, Emilie Wigdor5, Morgan J. Morgan2, Laurie J. Hannigan6,7,8, Elizabeth C. Corfield6,7,9, René Pool10,11, Susanne Bruins10, Helga Ask7,12,13, Christel M. Middeldorp14,15,16,17,18, Beate St Pourcain9,19,20, Meike Bartels10, Dorret I. Boomsma21,22, Catharina A. Hartman23, Aoi Noda24,25, Ippei Takahashi25, Mami Ishikuro24,25, Taku Obara24,25, Shinichi Kuriyama24,25, Mary S. Mufford26
and 15 other authorsMarilyn T. Lake27,28, Dan J. Stein27,29,30, Heather J. Zar28, Nadia Hoffman27,30, Elise B. Robinson31, Anders D. Børglum32,33,34, Xinhe Zhang35, Varun Warrier35,36, Autism Spectrum Disorder Working Group of the Psychiatric Genomics Consortium, Tomoki Arichi37,38, Mark H. Johnson1,36, Frank Dudbridge39, Stephan J. Sanders5,40, Alexandra Havdahl6,7,13, Angelica Ronald2
40 affiliations
  1. Centre for Brain and Cognitive Development, Department of Psychological Sciences, Birkbeck University of London,London, UK
  2. School of Psychology, Faculty of Health and Medical Sciences, University of Surrey, Guildford,Surrey, UK
  3. Division of Psychiatry, University College London,London, UK
  4. Department of Systems Medicine, University of Rome Tor Vergata,Rome, Italy
  5. Institute of Developmental and Regenerative Medicine, Department of Paediatrics, University of Oxford,Oxford, UK
  6. Research Department, Lovisenberg Diaconal Hospital,Oslo, Norway
  7. PsychGen Centre for Genetic Epidemiology and Mental Health, Norwegian Institute of Public Health,Oslo, Norway
  8. Population Health Sciences, Bristol Medical School, University of Bristol,Bristol, UK
  9. MRC Integrative Epidemiology Unit, Bristol Medical School, University of Bristol,Bristol, UK
  10. Department of Biological Psychology, Vrije Universiteit Amsterdam,Amsterdam, the Netherlands
  11. Amsterdam Public Health Research Institute,Amsterdam, the Netherlands
  12. Department of Child Health and Development, Norwegian Institute of Public Health,Oslo, Norway
  13. PROMENTA Research Centre, Department of Psychology, University of Oslo,Oslo, Norway
  14. Department of Child and Adolescent Psychiatry and Psychology, Amsterdam Reproduction and Development Research Institute, Amsterdam Public Health Research Institute, Amsterdam UMC,Amsterdam, the Netherlands
  15. Arkin Mental Health Care,Amsterdam, the Netherlands
  16. Levvel, Academic Center for Child and Adolescent Psychiatry,Amsterdam, the Netherlands
  17. Child Health Research Centre, University of Queensland,Brisbane, Queensland Australia
  18. Child and Youth Mental Health Service, Children’s Health Queensland Hospital and Health Service,Brisbane, Queensland Australia
  19. Max Planck Institute for Psycholinguistics,Nijmegen, the Netherlands
  20. Donders Institute for Brain, Cognition and Behaviour, Radboud University,Nijmegen, the Netherlands
  21. Department of Complex Trait Genetics, Center for Neurogenomics and Cognitive Research, Amsterdam, Vrije Universiteit,Amsterdam, the Netherlands
  22. Amsterdam Reproduction and Development Research Institute, Amsterdam Public Health Research Institute, Amsterdam UMC,Amsterdam, the Netherlands
  23. University Medical Center Psychopathology and Emotion Regulation, Department of Psychiatry, University Medical Center Groningen, University of Groningen,Groningen, the Netherlands
  24. Division of Molecular Epidemiology, Tohoku Medical Megabank Organization, Tohoku University,Sendai, Japan
  25. Division of Molecular Epidemiology, Environment and Genome Research Center, Graduate School of Medicine, Tohoku University,Sendai, Japan
  26. Division of Human Genetics, National Health Laboratory Service and School of Pathology, Faculty of Health Sciences, University of the Witwatersrand,Johannesburg, South Africa
  27. Department of Psychiatry and Mental Health, Faculty of Health Sciences, University of Cape Town,Rondebosch, South Africa
  28. Department of Paediatrics and Child Health, and SAMRC Unit on Child and Adolescent Health, University of Cape Town,Cape Town, South Africa
  29. SAMRC Unit on Risk & Resilience in Mental Disorders, Department of Psychiatry, University of Cape Town,Cape Town, South Africa
  30. Neuroscience Institute, University of Cape Town,Cape Town, South Africa
  31. Broad Institute of MIT and Harvard,Cambridge, MA USA
  32. Department of Biomedicine—Human Genetics, Aarhus University,Aarhus, Denmark
  33. Lundbeck Foundation Initiative for Integrative Psychiatric Research, iPSYCH,Aarhus, Denmark
  34. Center for Genomics and Personalized Medicine, Aarhus, Denmark
  35. Department of Psychiatry, University of Cambridge,Cambridge, UK
  36. Department of Psychology, University of Cambridge,Cambridge, UK
  37. Research Department of Early Life Imaging, School of Biomedical Engineering and Imaging Sciences, King’s College London,London, UK
  38. MRC Centre for Neurodevelopmental Disorders, King’s College London,London, UK
  39. Division of Public Health & Epidemiology, School of Medical Sciences, University of Leicester,Leicester, UK
  40. Department of Psychiatry and Behavioral Sciences, UCSF Weill Institute for Neurosciences, University of California, San Francisco,San Francisco, CA USA
Journal: Nature human behaviour, volume 10, issue 9, pages 1821-1840
Dates: received 3 July 2025; accepted 24 April 2026; published online 1 July 2026; in print 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41562-026-02486-5 · PMID 42386913 · PMCID PMC13590410 · OpenAlex W7166836354
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), human (organism), developmental (subfield)
Methods: Statistics, Smoothing, state filtering, decompositions, Machine learning
Keywords: Behavioural genetics, Human behaviour
MeSH: Genome-Wide Association Study*, Temperament*, Child, Preschool, Emotions, Female, Humans, Infant, Male, Polymorphism, Single Nucleotide, Shyness, White People (* major topic)
Topic: Genetic Associations and Epidemiology (Genetics, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: Economic and Social Research Council (ES/P000592/1); University of Surrey (n/a); HOD | Helse Sør-Øst RHF (sorost) (2022083 and 2019097, 2021045, 2020022); Norges Forskningsråd (Forskningsrådet) (274611, 336085); Max Planck Society; NWO VICI grant (VI.C.211.054); Koninklijke Nederlandse Akademie van Wetenschappen (Academy Professor Award (PAH/6635)); Harry Crossley Foundation (the Harry Crossley Clinical Research Fellowship); South African Medical Research Council (n/a); Bill and Melinda Gates Foundation (n/a); National Institute of Mental Health (NIH/NIMH 1R01MH124851-01, R01MH129751 and U01MH122681); Simons Foundation (n/a, 724306); Wellcome Trust (Wellcome) (214322\Z\18\Z and 226392/Z/22/Z); EC | Horizon 2020 Framework Programme (EU Framework Programme for Research and Innovation H2020) (101057385, FAMILY #101057529); UKRI (10063472); UK Medical Research Council (MR/Y009665/1, MR/N026063/1, G0701484, MR/K021389/1, MR/T003057/1); HDR UK QQ2 Molecules to Health Records Driver Programme; Birkbeck Research Innovation Fund
Citations: not cited yet (Europe PMC); 141 references in the paper

Abstract

Early temperament, such as socio-emotional development and activity level, varies widely, yet its underlying biological associations are not understood. We identified genetic variation associated with infant and toddler temperament using genome-wide association meta-analyses. We studied parent-rated emotionality, activity, shyness and sociability (n = 43,963–72,663) in the second and third postnatal years and a cross-age average. Cross-age single nucleotide polymorphism heritabilities for emotionality, activity, shyness and sociability were 6.79% (95% confidence interval (CI), (4.71%, 8.87%)), 9.55% (95% CI, (7.04%, 12.06%)), 15.26% (95% CI, (12.24%, 18.28%)) and 3.42% (95% CI, (1.30%, 5.54%)), respectively. Ten genome-wide significant loci were discovered. Two loci colocalized with expression quantitative trait loci in the adult cortex: RHEBL1 (posterior probability, 0.93; associated with activity) and MR1 (posterior probability, 0.99; with emotionality). Genetic correlations were observed between early temperament and later outcomes, such as emotionality and adult neuroticism, activity and attention deficit/hyperactivity disorder (ADHD), sociability and autism, and shyness and adult extraversion. Multi-ancestry (n = 56,083–78,894) and European-ancestry analyses gave similar results. Infant and toddler temperament is associated with genetic variation and shows genetic continuity with later outcomes.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 3 matches between paragraphs and lines of code.

precimed/python_convert

License: GPL-3.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: e46ebdfafd495c1420c7f8a4740a0da75c94d84d, 14 May 2024
Languages: Python (31), Jupyter (1)
Size: 56 files, 32 scripts
Software Heritage: archived
Found in: the text, “MiXeR”
Holds: README, license file, tests, 1 notebook
Not found: CITATION.cff, environment file, continuous integration, documentation
Tools: pandas (23 files), NumPy (19 files), SciPy (12 files), Matplotlib (6 files), Biopython (1 file), seaborn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
34 files

PerlineDemange/GeneticNurtureNonCog

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: f54b5e58faa29703904ce2c2da07083b5034578e, 25 May 2022
Languages: R (13), Shell (6)
Size: 36 files, 19 scripts
Software Heritage: not archived
Found in: the text, “Within- and between-family PGS analysis”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: data.table (9 files), tidyverse (9 files), ggplot2 (8 files), psych (7 files), reshape2 (3 files), nlme (2 files), metafor (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
20 files

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 51 scripts, each with its path and the digest of its content;
  • 3 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data availability

The summary statistics of the genome-wide association study of infant temperament traits are available via Figshare at https://doi.org/10.6084/m9.figshare.30041575 (ref. 141). Data from MoBa and the Medical Birth Registry of Norway used in this study are managed by the National Health Register Holders in Norway (Norwegian Institute of Public Health) and can be made available to researchers, with approval from the Regional Committees for Medical and Health Research Ethics, compliance with the EU General Data Protection Regulation and approval from the data owners. The consent given by the participants is not open to the storage of data on an individual level in repositories or journals. Researchers who want access to datasets for replication should apply through https://helsedata.no/. Access to datasets requires approval from the Regional Committee for Medical and Health Research Ethics in Norway and an agreement with MoBa. Data from NTR are available upon request by researchers. Information is available at https://ntr-data-request.psy.vu.nl. Lifelines data may be obtained from a third party and are not publicly available. Researchers can apply to use the Lifelines data used in this study. More information about how to request Lifelines data and the conditions of use can be found on their website at https://www.lifelines-biobank.com/researchers/working-with-us. The informed consent obtained from ALSPAC participants does not allow the data to be made available through any third-party-maintained public repository. Supporting data are available from ALSPAC on request under the approved proposal number, B3360. Full instructions for applying for data access can be found here: http://www.bristol.ac.uk/alspac/researchers/access/ (https://eur02.safelinks.protection.outlook.com/?url=http://www.bristol.ac.uk/alspac/researchers/access/&data=05|02||c0af1d04c6f74d27ed0708ddb7ec7c46|6b902693107440aa9e21d89446a2ebb5|0|0|638868948506251929|Unknown|TWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ==|0|||&sdata=wayHOiZi88xx8rePIvdEXncLLCy9389saqoQB5NmqPk=&reserved=0). The ALSPAC study website contains details of all the available data (http://www.bristol.ac.uk/alspac/researchers/our-data/). The TEDS data are accessible to researchers following the procedure outlined in their data access policy (https://www.teds.ac.uk/researchers/teds-data-access-policy/). The data that support the findings of this study are available from the Tohoku Medical Megabank project, but restrictions apply to the availability of these data, which were used under licence for the current study and so are not publicly available. The data are, however, available upon request and with the permission of the Tohoku Medical Megabank project. The DCHS is committed to the principle of data sharing. De-identified data will be made available to requesting researchers as appropriate. Requests for collaborations to undertake data analysis are welcome. More information can be found on their website (http://www.paediatrics.uct.ac.za/scah/dclhs). eQTL results for the ROSMAP, Mayo TCX, Mayo CER and cortical meta-analysis from Sieberts et al.65 are available through the AMP-AD Knowledge Portal at https://www.synapse.org/#!Synapse:syn17015233 (https://eur02.safelinks.protection.outlook.com/?url=https://www.synapse.org/#!Synapse:syn17015233&data=05|02||9e5b136f3efa4f7fd34508ddba6a2dff|6b902693107440aa9e21d89446a2ebb5|0|0|638871687406494356|Unknown|TWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ==|0|||&sdata=+marAlKaIZovNyJBuDUBG8Q+yvq01k3uU6wf/xPMCYo=&reserved=0).

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Publisher: n/a → Nature Portfolio

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 35 authors, 2 keywords, 11 MeSH terms, 18 funders, 123 references.

Cite

This paper

Hollowell, A., Gui, A., Wigdor, E., Morgan, M. J., Hannigan, L. J., Corfield, E. C., Pool, R., Bruins, S., Ask, H., Middeldorp, C. M., Pourcain, B. S., Bartels, M., Boomsma, D. I., Hartman, C. A., Noda, A., Takahashi, I., Ishikuro, M., Obara, T., Kuriyama, S., . . . Ronald, A. (2026). Genome-wide association studies of infant and toddler temperament in European and multi-ancestry populations. Nature human behaviour, 10(9), 1821-1840. https://doi.org/10.1038/s41562-026-02486-5

BibTeX

@article{hollowell2026genome,
author = {Hollowell, Anja and Gui, Anna and Wigdor, Emilie and Morgan, Morgan J. and Hannigan, Laurie J. and Corfield, Elizabeth C. and Pool, René and Bruins, Susanne and Ask, Helga and Middeldorp, Christel M. and Pourcain, Beate St and Bartels, Meike and Boomsma, Dorret I. and Hartman, Catharina A. and Noda, Aoi and Takahashi, Ippei and Ishikuro, Mami and Obara, Taku and Kuriyama, Shinichi and Mufford, Mary S. and Lake, Marilyn T. and Stein, Dan J. and Zar, Heather J. and Hoffman, Nadia and Robinson, Elise B. and Børglum, Anders D. and Zhang, Xinhe and Warrier, Varun and {Autism Spectrum Disorder Working Group of the Psychiatric Genomics Consortium} and Arichi, Tomoki and Johnson, Mark H. and Dudbridge, Frank and Sanders, Stephan J. and Havdahl, Alexandra and Ronald, Angelica},
title = {{Genome-wide association studies of infant and toddler temperament in European and multi-ancestry populations}},
journal = {Nature human behaviour},
year = {2026},
month = jul,
volume = {10},
number = {9},
pages = {1821--1840},
publisher = {Nature Portfolio},
issn = {2397-3374},
doi = {10.1038/s41562-026-02486-5},
url = {https://doi.org/10.1038/s41562-026-02486-5},
pmid = {42386913},
pmcid = {PMC13590410}
}

RIS

TY - JOUR
AU - Hollowell, Anja
AU - Gui, Anna
AU - Wigdor, Emilie
AU - Morgan, Morgan J.
AU - Hannigan, Laurie J.
AU - Corfield, Elizabeth C.
AU - Pool, René
AU - Bruins, Susanne
AU - Ask, Helga
AU - Middeldorp, Christel M.
AU - Pourcain, Beate St
AU - Bartels, Meike
AU - Boomsma, Dorret I.
AU - Hartman, Catharina A.
AU - Noda, Aoi
AU - Takahashi, Ippei
AU - Ishikuro, Mami
AU - Obara, Taku
AU - Kuriyama, Shinichi
AU - Mufford, Mary S.
AU - Lake, Marilyn T.
AU - Stein, Dan J.
AU - Zar, Heather J.
AU - Hoffman, Nadia
AU - Robinson, Elise B.
AU - Børglum, Anders D.
AU - Zhang, Xinhe
AU - Warrier, Varun
AU - Autism Spectrum Disorder Working Group of the Psychiatric Genomics Consortium
AU - Arichi, Tomoki
AU - Johnson, Mark H.
AU - Dudbridge, Frank
AU - Sanders, Stephan J.
AU - Havdahl, Alexandra
AU - Ronald, Angelica
TI - Genome-wide association studies of infant and toddler temperament in European and multi-ancestry populations
T2 - Nature human behaviour
J2 - Nat Hum Behav
PY - 2026
DA - 2026/07/01
VL - 10
IS - 9
SP - 1821
EP - 1840
SN - 2397-3374
PB - Nature Portfolio
DO - 10.1038/s41562-026-02486-5
UR - https://doi.org/10.1038/s41562-026-02486-5
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41562-026-02486-5",
"type": "article-journal",
"title": "Genome-wide association studies of infant and toddler temperament in European and multi-ancestry populations",
"container-title": "Nature human behaviour",
"author": [
{
"family": "Hollowell",
"given": "Anja"
},
{
"family": "Gui",
"given": "Anna"
},
{
"family": "Wigdor",
"given": "Emilie"
},
{
"family": "Morgan",
"given": "Morgan J."
},
{
"family": "Hannigan",
"given": "Laurie J."
},
{
"family": "Corfield",
"given": "Elizabeth C."
},
{
"family": "Pool",
"given": "René"
},
{
"family": "Bruins",
"given": "Susanne"
},
{
"family": "Ask",
"given": "Helga"
},
{
"family": "Middeldorp",
"given": "Christel M."
},
{
"family": "Pourcain",
"given": "Beate St"
},
{
"family": "Bartels",
"given": "Meike"
},
{
"family": "Boomsma",
"given": "Dorret I."
},
{
"family": "Hartman",
"given": "Catharina A."
},
{
"family": "Noda",
"given": "Aoi"
},
{
"family": "Takahashi",
"given": "Ippei"
},
{
"family": "Ishikuro",
"given": "Mami"
},
{
"family": "Obara",
"given": "Taku"
},
{
"family": "Kuriyama",
"given": "Shinichi"
},
{
"family": "Mufford",
"given": "Mary S."
},
{
"family": "Lake",
"given": "Marilyn T."
},
{
"family": "Stein",
"given": "Dan J."
},
{
"family": "Zar",
"given": "Heather J."
},
{
"family": "Hoffman",
"given": "Nadia"
},
{
"family": "Robinson",
"given": "Elise B."
},
{
"family": "Børglum",
"given": "Anders D."
},
{
"family": "Zhang",
"given": "Xinhe"
},
{
"family": "Warrier",
"given": "Varun"
},
{
"literal": "Autism Spectrum Disorder Working Group of the Psychiatric Genomics Consortium"
},
{
"family": "Arichi",
"given": "Tomoki"
},
{
"family": "Johnson",
"given": "Mark H."
},
{
"family": "Dudbridge",
"given": "Frank"
},
{
"family": "Sanders",
"given": "Stephan J."
},
{
"family": "Havdahl",
"given": "Alexandra"
},
{
"family": "Ronald",
"given": "Angelica"
}
],
"container-title-short": "Nat Hum Behav",
"volume": "10",
"issue": "9",
"page": "1821-1840",
"DOI": "10.1038/s41562-026-02486-5",
"PMID": "42386913",
"PMCID": "PMC13590410",
"ISSN": "2397-3374",
"publisher": "Nature Portfolio",
"URL": "https://doi.org/10.1038/s41562-026-02486-5",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41562-026-02476-7 [code]
Genome-wide meta-analysis of quantitatively measured generalized anxiety symptoms in individuals of European ancestry.
Journal: Nature human behaviour
In common: psych, data.table, tidyverse, genetics / omics, 14 references, 5 authors
[2] doi:10.1038/s41467-026-73714-9 [code]
The genetic architecture of cortical similarity networks.
Journal: Nature communications
In common: ggplot2, tidyverse, pandas, 3 other tools, genetics / omics, 9 references, author Varun Warrier
[3] doi:10.1038/s43856-026-01510-z [code]
Mapping genetic convergence across brain structure, mental health, and cardiometabolic disease.
Journal: Communications medicine
In common: psych, data.table, ggplot2, 5 other tools, developmental, genetics / omics, 9 references
[4] doi:10.1038/s41467-026-73996-z [code]
Genetic architecture of white matter microstructure captured by unsupervised deep representation learning of fractional anisotropy maps.
Journal: Nature communications
In common: ggplot2, seaborn, tidyverse, 4 other tools, genetics / omics, 11 references
[5] doi:10.1038/s41467-026-73428-y [code]
Regional heterogeneity in phenotypic and genetic associations between bone and brain in humans.
Journal: Nature communications
In common: psych, data.table, ggplot2, 4 other tools, genetics / omics, 10 references
[6] doi:10.1073/pnas.2609814123 [code]
Genetic architectures of brain-related traits are shaped by strong selective constraints.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: data.table, ggplot2, tidyverse, 4 other tools, genetics / omics, 9 references
[7] doi:10.1038/s41467-026-71682-8 [code]
GWAS meta-analysis of cerebrospinal fluid Alzheimer's biomarkers reveals loci regulating lipids, brain volume and autophagy.
Journal: Nature communications
In common: reshape2, data.table, ggplot2, 4 other tools, genetics / omics, 9 references
[8] doi:10.1038/s41467-026-72164-7 [code]
Multivariate genetic analysis reveals three distinct pathological dimensions in musculoskeletal disorders.
Journal: Nature communications
In common: psych, data.table, ggplot2, 4 other tools, genetics / omics, 8 references
[9] doi:10.1186/s13195-026-02036-1 [code]
Genetic drivers of progression in Alzheimer's disease are distinct from disease risk.
Journal: Alzheimer's research & therapy
In common: metafor, nlme, data.table, 2 other tools, genetics / omics, 7 references
[10] doi:10.1038/s41588-026-02646-3 [code]
Co-expression-based models improve eQTL predictions for transcriptome-wide association studies and highlight new schizophrenia-associated genes.
Journal: Nature genetics
In common: metafor, reshape2, data.table, 4 other tools, genetics / omics, 6 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.