Trait Simulation Tutorial

Authors: Sarah Ji, Janet Sinsheimer, Eric Sobel, Kenneth Lange, Hua Zhou

In this notebook we show how to use the TraitSimulation.jl package we illustrate how TraitSimulation.jl can easily simulate traits from genotype data, all within the OpenMendel universe. Operating within this universe brings potential advantages over the available software(s) when needed for downstream analysis or study design.

Using just a few calls on the command line to the appropriate packages within the OpenMendel, we demonstrate in three easy examples the utilities of the TraitSimulation.jl package.

Background

There is a lack of software available to geneticists who wish to calculate power and sample sizes in designing a study on genetics data. Typically, the study power depends on assumptions about the underlying disease model. Many power calculating software tools operate as a black box and do not allow for customization. To develop custom tests, researchers can develop their own simulation procedures to carry out power calculations. One limitation with many existing methods for simulating traits conditional on genotypes is that these methods are limited to normally distributed traits and to fixed effects.

This software package, TraitSimuliation.jl addresses the need for simulated trait data in genetic analyses. This package generates data sets that will allow researchers to accurately check the validity of programs and to calculate power for their proposed studies. This package gives users the ability to easily simulate phenotypic traits under generalized linear models (GLMs) or variance component models (VCMs) conditional on PLINK formatted genotype data [3]. In addition, we include customized simulation utilities that accompany specific genetic analysis options in Open-Mendel; for example, ordered, multinomial traits. We demonstrate these simulation utilities on the example dataset described below.

Demonstration

Example Data

We use the OpenMendel package SnpArrays.jl to both read in and write out PLINK formatted files. Available in the data directory under the Example_Data section of this package, we use the file "EUR_SUBSET" for the demonstration how to simulate phenotypic traits on PLINK formatted data. For convenience we use the common assumption that the residual covariance among two relatives can be captured by the additive genetic variance times twice the kinship coefficient.

In each example the user can specify the simulation model parameters, along with the number of repitions for each simulation model as desired. By default, the simulation will return the result of a single simulation.

Double check that you are using Julia version 1.0 or higher by checking the machine information

In [1]:
versioninfo()
Julia Version 1.2.0
Commit c6da87ff4b (2019-08-20 00:03 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin18.6.0)
  CPU: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.1 (ORCJIT, skylake)
In [2]:
using Random, Plots, DataFrames, LinearAlgebra
using SnpArrays, TraitSimulation, GLM, StatsBase, OrdinalMultinomialModels
Random.seed!(1234);

Reading genotype data using SnpArrays

First use SnpArrays.jl to read in the genotype data. We use PLINK formatted data with the same prefixes for the .bim, .fam, .bed files.

SnpArrays is a very useful utility and can do a lot more than just read in the data. More information about all the functionality of SnpArrays can be found at: https://openmendel.github.io/SnpArrays.jl/latest/

As missing genotypes are often due to problems making the calls, the called genotypes at a marker with too much missing genotypes are potentially unreliable. By default, SnpArrays filters to keep only the genotypes with success rates greater than 0.98 and the minimum minor allele frequency to be 0.01. If the user wishes to change the stringency, change the number given in filter according to SnpArrays.

In [3]:
filename = "EUR_subset"
EUR = SnpArray(SnpArrays.datadir(filename * ".bed"));
In [4]:
rowmask, colmask =  SnpArrays.filter(EUR)
minor_allele_frequency = maf(EUR);
people, snps = size(EUR)
Out[4]:
(379, 54051)
In [5]:
EUR_data = SnpData(SnpArrays.datadir(filename));

Here we will use the rarest snp in our dataset, rs150018646, as the causal snp in our model for demonstration. For users who wish to identify by name, which locus to include, first subset the names of all the loci into a vector called snpid and then call the following command to store our design matrix for the model that includes sex and locus of choice.

{julia}
   bimfile = EUR_data.snp_info # store the snp_info with the snp names

   snpid  = bimfile[!, :snpid] # store the snp names in the snpid vector

   causal_snp_index = findall(x -> x == "rs150018646", snpid) # find the index of the snp of interest by snpid
In [6]:
minimum_maf, causal_snp_index = findmin(minor_allele_frequency)
Out[6]:
(0.01055408970976257, 82)

Additionally, we will control for sex, with females as the baseline group, sex = 0.0. We want to find the index of this causal locus in the snp_definition (.bim) file and then subset that locus from the genetic marker data above. Make note of julia's ternary operator '?' which allows us to make this conversion efficiently!

Using SnpArrays.jl we can then use the convert and @view commands to get the appropriate conversion from SnpArray to a computable vector of Float64.

In [7]:
locus = convert(Vector{Float64}, @view(EUR[:, causal_snp_index]))
famfile = EUR_data.person_info
sex = map(x -> strip(x) == "F" ? 0.0 : 1.0, famfile[!, :sex])
intercept = ones(length(sex))
X = [intercept sex locus]
Out[7]:
379×3 Array{Float64,2}:
 1.0  1.0  2.0
 1.0  1.0  1.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 ⋮            
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  1.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0
 1.0  1.0  2.0

Quick QC

We can take a quick moment to insure the correct value for the allele frequency of the causal snp. This will act as a quick quality control (QC) step to validate that the user has found the right snp and that it has the expected allele frequency.

In [8]:
minor_allele_frequency[causal_snp_index] == 0.01055408970976257
Out[8]:
true

Example 1: Standard Models

In this example we first demonstrate how to use the GLM.jl package to simulate a trait from unrelated individuals. We then show how to use our TraitSimulation.jl package to efficiently simulate a trait from families under the linear mixed model framework.

The notebook is organized as follows:

For the first example, we show how to simulate traits from standard models users in the genetics community will recognize.

Users can specify to simulate phenotypes from unrelateds or families with user-specified Generalized Linear Models (GLMs) or Variance Component Models (VCMs), respectively. Researchers who feel more comfortable viewing the latter as a Linear Mixed Model(LMM) an additive genetic variance component and environmental variance component, we welcome you to do so.

$$ 1 . \text{GLM}: Y_{n \times 1} \sim Poisson(g^{-1}({X \beta})) $$

$$ 2 . \text{VCM}: Y_{n \times d} \sim \text{MatrixNormal}(\mathbf{M}_{n \times d} = XB, \Omega_{nd \times nd} = \Sigma_A \otimes 2\hat{\Phi}_{GRM} + \Sigma_E \otimes I_n),$$

where we can calculate the estimated empirical kinship matrix $2*\hat{\Phi}_{GRM}$ using SnpArrays.jl.

We show in the next example, models with additional variance components can also be specified, as long as they are sensible (positive semi definite).

GLM Traits from Unrelated Individuals

$$ Y_{n \times 1} \sim Poisson(\mu_{n \times 1} = X\beta) $$

Here we specify the fixed effects and the phenotype distribution, and output for ten simulations per person.

In [9]:
β = [1; 0.2; 0.5]
dist = Poisson()
link = LogLink()
GLMmodel = GLMTrait(X, β, dist, link)
Out[9]:
Generalized Linear Model
  * response distribution: Poisson
  * link function: LogLink
  * sample size: 379
In [10]:
nsim = 10
Simulated_GLM_Traits = DataFrame(simulate(GLMmodel, nsim))
rename!(Simulated_GLM_Traits, [Symbol("Trait$i") for i in 1:nsim])
Out[10]:

379 rows × 10 columns

Trait1Trait2Trait3Trait4Trait5Trait6Trait7Trait8Trait9Trait10
Int64Int64Int64Int64Int64Int64Int64Int64Int64Int64
110115881381199
27585265383
39811628108117
491310778133118
511612131341281110
612789119814810
761312691097514
87127105537128
9712577959137
109568111261088
111065127138676
123629896799
13586610786915
1414589961011138
15101010111779101215
165813149104769
1777101012891558
18105161061091187
1959105651413118
2012965812111156
211412711115691411
22159108116581110
231441198947117
241111131261311101011
2512591310165858
264513679105158
27585912486810
28710510789949
296111076121491214
3081061199141469

In this example we show how to generate data so that the related individuals have correlated trait values even after we account for the effect of a snp, a combination of snps or other fixed effects. We simulate data under a linear mixed model so that we can model residual dependency among individuals.

$$ Y \sim \text{Normal}(\mathbf{\mu}_{n \times 1} = X\beta, \Sigma_{n \times n} = \sigma_A \times 2\hat{\Phi}_{GRM} + \sigma_E \times I_n) $$

This example is meant to simulate data in a scenario in which a number of rare mutations in a single gene can change a trait value. We model the residual variation among relatives with the additive genetic variance component and we include 20 rare variants in the mean portion of the model, defined as loci with minor allele frequencies greater than 0.002 but less than 0.02.

Specifically we are generating a single normal trait controlling for family structure with residual heritabiity of 67%, and effect sizes for the variants generated as a function of the minor allele frequencies. The rarer the variant the greater its effect size.

In practice rare variants have smaller minor allele frequencies, but we are limited in this tutorial by the relatively small size of the data set. Note also that our modeling these effects as part of the mean is not meant to imply that the best way to detect them would be a standard association analysis. Instead we recommend a burden or SKAT test.

In [11]:
GRM = grm(EUR, minmaf = 0.05);

Filtering

We first subset only the rare SNP's, then we simulate traits on the 20 of the rare SNP's. For this demo, we subset the fist k = 20 rare snps. Change the parameters and the number of SNPs for simulation to model different regions of the genome. The number 20 is arbitrary and you can use more or less than 20 if you desire by changing the final number.

In [12]:
rare_index = (0.002 .< minor_allele_frequency .≤ 0.02)
filtsnpdata = SnpArrays.filter(EUR_data, rowmask, rare_index, des = "rare_filtered_28data");
In [13]:
rare_snps = SnpArray("rare_filtered_28data.bed")
Out[13]:
379×7171 SnpArray:
 0x03  0x03  0x03  0x03  0x03  0x03  …  0x03  0x03  0x03  0x03  0x03  0x03
 0x02  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x02  0x03  0x02  0x03  0x03  0x03
 0x03  0x03  0x03  0x02  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03  …  0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x02  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03  …  0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x02  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x02  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
    ⋮                             ⋮  ⋱     ⋮                             ⋮
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x02
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03  …  0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x02  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03  …  0x03  0x03  0x02  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x02  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03
 0x03  0x03  0x03  0x03  0x03  0x03     0x03  0x03  0x03  0x03  0x03  0x03

Simulation

For demonstration purposes, we simulate effect sizes from realistic distributions using the minor allele frequency (maf), so that the rarest SNP's have the biggest effect sizes. The effect sizes are rounded to the second digit, throughout this example. Notice there is a random +1 or -1, so that there are effects that both increase and decrease the simulated trait value.

# Generating Effect Sizes 

n = length(rare_snps)
effect_sizes = zeros(n)

for i in 1:n
    effect_sizes[i] = rand([-1, 1]) .* (0.1 / sqrt.(maf_rare_snps[i] .* (1 - maf_rare_snps[i])))
end
In [14]:
sim_effectsize = simulate_effect_size(minor_allele_frequency[rare_index][1:20])
X_rare = @view(rare_snps[:, 1:20])
mu = X_rare * sim_effectsize
rare_20_snp_model = VCMTrait(mu[:, :], 4*(2*GRM) + 2*(I))
Out[14]:
Variance Component Model
  * number of traits: 1
  * number of variance components: 1
  * sample size: 379
In [15]:
Rare_SNP_Trait = DataFrame(simulate(rare_20_snp_model))
rename!(Rare_SNP_Trait, [Symbol("Trait$i") for i in 1:size(mu, 2)])
Out[15]:

379 rows × 1 columns

Trait1
Float64
18.05867
214.2267
310.5112
46.67995
513.4649
610.0289
77.70729
89.17434
96.92844
1013.0706
1112.0975
129.36559
138.22128
147.88176
1510.6868
1611.8909
1713.138
1811.1556
196.34875
207.65595
2111.4261
2210.9443
2310.5285
248.26589
256.75258
265.42014
278.51405
287.15976
2912.8667
3011.9212
In [16]:
rm("rare_filtered_28data.bed")
rm("rare_filtered_28data.bim")
rm("rare_filtered_28data.fam")

Example 2: Multiple Traits, Multiple Variance Components? Easy.

We can extend the mixed model for a single trait in the previous example to demo how to efficiently simulate multiple traits, while accounting for any number of other random effects in addition to the additive genetic and environmental variance components. In particular, we note the alternative ways users can specify simulation parameters under the VCM.

VCM Parameter Specification:

$\textbf{Option 1:}$ Say that you have the the classical setting in genetics, two variance components, one for the additive genetic variance and one for the environmental variance. For simulating $d$ correlated traits for $n$ related people under the VCM, users can specify their covariance structures as follows.

$$Y_{n \times d} \sim \text{MatrixNormal}(\mathbf{M}_{n \times d} = XB, \Omega_{nd \times nd} = \Sigma_A \otimes V_A + \Sigma_E \otimes V_E)$$

$\textbf{Option 2:}$ Say we have $m \geq 2$ variance components for $d$ correlated traits of $n$ related people under the VCM. For this model we want to specify the following:

$$Y_{n \times d} \sim \text{MatrixNormal}(\mathbf{M}_{n \times d} = XB, \Omega_{nd \times nd} = \Sigma_1 \otimes V_1 + \cdots + \Sigma_m \otimes V_m) $$

Although $\textbf{Option 1}$ can also specify $m \geq 2$ variance components, $\textbf{Option 2}$ allows the model data can be inputed under the standard VarianceComponentModels.jl framework as follows:

  • Y: n x d response matrix
  • X: n x p covariate matrix
  • V=(V1,...,Vm): a tuple of m n x n covariance matrices

and parameters are

  • B: p x d mean parameter matrix
  • Σ=(Σ1,...,Σm): a tuple of m d x d variance components.

Due to time constraints, I encourage for those interested, to look at This Example where we demonstrate the simlation of $d = 2$ traits with $m = 10$ variance components, and benchmark it against the available method using the MatrixNormal distribution in Julia package, Distributions.jl.

In [17]:
formulas = ["1 + 2.0*sex + 0.5*locus", "20 + 2.0*sex + 1.0*locus"]
I_n = Matrix{Float64}(I, size(GRM))
Σ_A = [4 1; 1 4]
Σ_E = [2.0 0.0; 0.0 2.0];
variance_formula = @vc Σ_A  2GRM + Σ_E  I_n;
genetic_model = VCMTrait(formulas, DataFrame(X, [:intercept, :sex, :locus]), variance_formula)
Out[17]:
Variance Component Model
  * number of traits: 2
  * number of variance components: 2
  * sample size: 379
In [18]:
GeneticTraits = DataFrame(simulate(genetic_model))
rename!(GeneticTraits, [Symbol("Trait$i") for i in 1:length(formulas)])
Out[18]:

379 rows × 2 columns

Trait1Trait2
Float64Float64
12.1715119.485
20.29434921.0201
34.5826325.2843
44.8521123.1231
54.3770325.3901
62.2751924.082
71.1816622.3009
85.0446325.15
93.9156524.194
103.9680223.4634
111.4114322.3104
123.0136722.5568
137.5236125.3331
146.9921426.7024
152.74124.1689
163.5068724.3052
173.121726.0634
182.6400722.5332
191.8326730.1957
203.103221.9109
215.7916129.0065
222.9989625.5038
236.6554221.3525
245.547226.7639
253.0152627.8003
269.8398525.9242
273.8694722.5092
286.7821421.7596
291.2621319.7709
309.2571120.8663

Example 3: Power Calculation

For the last example, we show how to simulate from customized simulation models that accompany specific genetic analysis options in OpenMendel; for example, ordered, multinomial traits.

This example illustrates the use of the simulations to generates data sets allowing researchers to accurately check the validity of programs and to calculate power for their proposed studies.

We illustrate this example in three digestable steps:

  • The first by simulating genotypes and covariate values representative for our study population.
  • Carry over the simulated design matrix from (1) to create the OrderedMultinomialTrait model object.
  • Simulate off the OrderedMultinomialTrait model object created in (2) and run the power analyses for the desired significance level.

Genotype Simulation:

Say our study population has a sample size of n people and we are interested in studying the effect of the causal snp with a predetermined minor allele frequency. We use the minor allele frequency of the causal variant to simulate the SnpArray under Hardy Weinberg Equillibrium (HWE), using the snparray_simulation function as follows:

Genotype Plink/SnpArray
A1,A1 0x00
missing 0x01
A1,A2 0x02
A2,A2 0x03

Given the specified minor allele frequency, maf, here maf = [0.2], this function samples from the genotype vector under HWE and returns the compressed binary format under SnpArrays. Note if you give the function a vector of minor allele frequencies, specify maf = [0.2, 0.25, 0.3], for each specified allele it will simulate a SnpArray under HWE and ouput them together.

In [19]:
n = 5000
maf_causal_snp = [0.2]
Simulated_SnpArray = snparray_simulation(maf_causal_snp, n)
Out[19]:
5000×1 SnpArray:
 0x00
 0x02
 0x00
 0x02
 0x02
 0x00
 0x00
 0x03
 0x02
 0x02
 0x02
 0x02
 0x00
    ⋮
 0x00
 0x02
 0x00
 0x00
 0x00
 0x00
 0x02
 0x00
 0x02
 0x02
 0x00
 0x02

convert

By default convert function translates genotypes according to the additive SNP model, which essentially counts the number of A2 allele (0, 1 or 2) per genotype. Other SNP models are dominant and recessive, both in terms of the A2 allele.

Genotype SnpArray model=ADDITIVE_MODEL model=DOMINANT_MODEL model=RECESSIVE_MODEL
A1,A1 0x00 0 0 0
missing 0x01 NaN NaN NaN
A1,A2 0x02 1 1 0
A2,A2 0x03 2 1 1

If desired, the user can decide to specify alternative model parameters found in the SnpArrays documentation.

In [20]:
locus = convert(Vector{Float64}, @view(Simulated_SnpArray[:, 1]))
Out[20]:
5000-element Array{Float64,1}:
 0.0
 1.0
 0.0
 1.0
 1.0
 0.0
 0.0
 2.0
 1.0
 1.0
 1.0
 1.0
 0.0
 ⋮  
 0.0
 1.0
 0.0
 0.0
 0.0
 0.0
 1.0
 0.0
 1.0
 1.0
 0.0
 1.0

Create Simulated Design Matrix

Now we concatenate the simulated causal locus, transformed to Float64, to the other covariates we want to control for in the study design.

Say we want to have a study population of 51% females, where the mean age is 45 years with a standard deviation of 8 years. For demo purposes we will standardize the age variable. Using the distributions package we can simulate the values for the non-genetic covariates and attach the causal locus as the last column in the design matrix.

In [21]:
pdf_sex = Bernoulli(0.51)
pdf_age = Normal(45, 8)

sex = rand(pdf_sex, n)
# simulate age under the specified pdf_age and standardize to be ~ N(0, 1)
age = zscore(rand(pdf_age, n))

X_new = [age sex locus]
DataFrame(X_new, [:age, :sex, :locus])
Out[21]:

5,000 rows × 3 columns

agesexlocus
Float64Float64Float64
1-0.3114951.00.0
2-0.8550660.01.0
3-1.02341.00.0
4-0.7952381.01.0
51.483971.01.0
6-1.314241.00.0
71.977431.00.0
8-0.7972530.02.0
90.1739860.01.0
101.257761.01.0
11-0.5553881.01.0
120.8685020.01.0
13-0.1316121.00.0
14-0.6426961.00.0
150.4043561.00.0
16-0.4553620.00.0
17-1.725830.00.0
18-2.065151.00.0
19-1.312121.00.0
20-0.06469320.00.0
210.2253360.00.0
220.1036090.01.0
230.5146060.00.0
240.2218911.00.0
25-1.020711.01.0
260.3341340.00.0
271.31540.00.0
280.3927490.00.0
29-1.132171.00.0
301.442711.01.0

Phenotype Simulation:

Now that we have our simulated design matrix with the desired SNP of interest, we can simulate our phenotypes on the simulated data under different TraitSimulation models. To illustrate, we demonstrate on the OrderedMultinomialTrait model object in TraitSimulation.jl.

Ordered Multinomial Trait

Recall that this phenotype is special, in that the OrdinalMultinomialModels package provides Julia utilities to fit ordered multinomial models, including proportional odds model and ordered Probit model as special cases.

In [22]:
θ = [1.0, 1.2, 1.4]
β_new = [1.0, 2.0, 10.0]
Ordinal_Model = OrderedMultinomialTrait(X_new, β_new, θ, LogitLink())
Out[22]:
Ordinal Multinomial Model
  * number of fixed effects: 3
  * number of ordinal multinomial outcome categories: 4
  * link function: LogitLink
  * sample size: 5000
In [23]:
nsim = 10
Ordinal_Trait = DataFrame(simulate(Ordinal_Model, nsim))
rename!(Ordinal_Trait, [Symbol("Trait$i") for i in 1:nsim])
Out[23]:

5,000 rows × 10 columns

Trait1Trait2Trait3Trait4Trait5Trait6Trait7Trait8Trait9Trait10
Int64Int64Int64Int64Int64Int64Int64Int64Int64Int64
11211414414
24444444444
34411441444
44444444444
54444444444
61344111141
74444444444
84444444444
94444444444
104444444444
114444444444
124444444444
133414344414
144141444411
154424431142
161411411111
171111111111
184114111114
191141214141
204111411111
211141421111
224444444444
231411111441
243414441444
254444444444
263431411314
274441141444
284121111114
294244444111
304444444444

Simulate Ordered Multinomial Logistic

Specific to the Ordered Multinomial Logistic model is the option to transform the multinomial outcome (i.e 1, 2, 3, 4) into a binary outcome for logistic regression.

Although by default is the multinomial simulation above, the user can simulate from the transformed logistic outcome for example by specifying arguments: Logistic = true and threshold = 2 the value to use as a cutoff for identifying cases and controls. (i.e if y > 2 => y_logit == 1). We note if you specify Logistic = true and do not provide a threshold value, the function will throw an error to remind you to specify one.

In [24]:
Logistic_Trait = DataFrame(simulate(Ordinal_Model, nsim, Logistic = true, threshold = 2))
rename!(Logistic_Trait, [Symbol("Trait$i") for i in 1:nsim])
Out[24]:

5,000 rows × 10 columns

Trait1Trait2Trait3Trait4Trait5Trait6Trait7Trait8Trait9Trait10
Int64Int64Int64Int64Int64Int64Int64Int64Int64Int64
10101011110
21111111111
30110101010
41111111111
51111111111
60000110100
71111111110
81111111111
91111111111
101111111111
111111111111
121111111111
131101011101
140001101111
151111110001
160100000100
170000000100
180011010000
191101011010
200100010000
210001000010
221111111111
231101000001
240111101011
251111111111
260001001000
271100011011
280100010100
290001100000
301111111111

Power Calculation:

We use the following function to generate the p-values for the simulated power example for the ordered multinomial regression model. We range effect sizes in the vector γs, which collects effect sizes from 0 to 1 in increments of 0.05. As expected, the power increases as the effect size increases.

In [25]:
γs = collect(0.0:0.05:0.5)
Out[25]:
11-element Array{Float64,1}:
 0.0 
 0.05
 0.1 
 0.15
 0.2 
 0.25
 0.3 
 0.35
 0.4 
 0.45
 0.5 

Each column of this matrix represents each of the detected effect sizes, and each row of this matrix represents each simulation for that effect size. The user feeds into the function the number of simulations, the vector of effect sizes, the TraitSimulation.jl model object, and the random seed.

For GLMTrait objects, the realistic_power_simulation function makes the appropriate calls to the GLM.jl package to get the simulation p-values obtained from testing the significance of the causal locus using the Wald Test by default. However since the GLM.jl package has its limitations, we include additional power utilities that make the appropriate function calls to the OrdinalMultinomialModels to get the p-value obtained from testing the significance of the causal locus.

In [26]:
function ordinal_multinomial_power(
    nsim::Int, γs::Vector{Float64}, traitobject::OrderedMultinomialTrait, randomseed::Int)
    #power estimate
    pvaluepolr = Array{Float64}(undef, nsim, length(γs))
    β_original = traitobject.β[end]
    Random.seed!(randomseed)

    #generate the data
    X_null = traitobject.X[:, 1:(end - 1)]
    causal_snp = traitobject.X[:, end][:, :]
    for j in eachindex(γs)
        for i in 1:nsim
            β = traitobject.β
            β[end] = γs[j]
            y = simulate(traitobject) # simulate the trait 
            #compute the power from the ordinal model
            ornull = polr(X_null, y, traitobject.link)
            pvaluepolr[i, j] = polrtest(OrdinalMultinomialScoreTest(ornull, causal_snp))
        end
    end
    traitobject.β[end] = β_original
    return pvaluepolr
end
Out[26]:
ordinal_multinomial_power (generic function with 1 method)

For each effect size in $\gamma_s,$ in each column we have the p-values obtained from testing the significance of the causal locus nsim = 100 times under the ordinal multinomial model, Ordinal_Model and the randomseed = 1234.

In [27]:
nsim = 100
randomseed = 1234
simulated_pvalues = ordinal_multinomial_power(nsim, γs, Ordinal_Model, randomseed)
rename!(DataFrame(simulated_pvalues), [Symbol("γs = $(γs[i])") for i in 1:length(γs)])
Out[27]:

100 rows × 11 columns (omitted printing of 4 columns)

γs = 0.0γs = 0.05γs = 0.1γs = 0.15γs = 0.2γs = 0.25γs = 0.3
Float64Float64Float64Float64Float64Float64Float64
10.3623090.1456020.8696123.11717e-78.12364e-60.004316727.53159e-7
20.1590650.7600980.03722890.2419830.003847178.98399e-51.52093e-5
30.4234340.00861780.4105230.008514612.65624e-78.42959e-87.93777e-7
40.03719690.003904440.05151420.0002648142.21532e-51.86846e-60.000610505
50.8216880.1185060.01397874.96686e-50.00156639.99855e-86.37073e-7
60.9253460.1860230.06189040.1026470.002943154.21476e-54.41649e-7
70.3590710.0394580.8614360.0009692974.69087e-51.16095e-63.53071e-5
80.671440.6420210.00323450.001753514.39916e-80.000620011.84705e-11
90.538240.8031580.08644820.002214580.1640120.001083721.62538e-9
100.3460910.6106440.001669890.005057545.40483e-66.52737e-92.90593e-12
110.1679430.2699260.0008163070.06197180.0001373730.001011411.08027e-8
120.1092690.0049050.00193130.001579233.83052e-63.8507e-82.02977e-7
130.3309250.4511280.02602920.02251650.0007363321.18065e-111.46156e-12
140.4017710.8571710.03236821.14005e-52.041e-51.69914e-64.88913e-12
150.5684770.06308810.1688480.01283110.0161490.01121991.46611e-6
160.8624010.06017240.03538996.43778e-62.13767e-71.01956e-52.68073e-7
170.5213710.9831350.0421852.58819e-50.3898053.36919e-78.64545e-6
180.912530.07856860.0008766410.008906390.004088060.0005976364.48763e-9
190.568170.008457870.3587310.05403540.0009893630.0003017751.23944e-8
200.07669010.04958470.005919660.04130030.001259542.96929e-88.88805e-7
210.7642940.7925785.23464e-50.02400340.0002350955.80359e-71.86832e-7
220.3629560.03621280.002788291.01053e-60.0002159683.43234e-55.98112e-8
230.4640330.4518020.7103150.01504780.005019634.86814e-63.85565e-8
240.4325440.03747050.1318350.002131590.0484324.19596e-65.77858e-9
250.626250.322150.01562140.06230550.0001239516.51728e-63.38018e-11
260.08947120.6488180.2377780.4185580.04259581.78233e-72.2979e-10
270.96580.04610590.02372510.008925642.83424e-80.001796832.20499e-9
280.5077050.9100310.2011810.01706088.53456e-91.37423e-67.58574e-8
290.1431270.2201030.1037180.05319890.0001122718.68575e-80.000795859
300.3881930.8377770.0191450.0001774151.09349e-51.38964e-60.000210648

Now we find the power of each effect size in the user-specified γs vector at the specified alpha level of significance, and plot the trajectory using the Plots.jl package.

In [28]:
α = 0.000005
power_effectsize = power(simulated_pvalues, α)
Out[28]:
11-element Array{Float64,1}:
 0.0 
 0.0 
 0.0 
 0.07
 0.19
 0.47
 0.75
 0.95
 1.0 
 1.0 
 1.0 
In [29]:
plot(γs, power_effectsize, title = "Multinomial Power", label = "maf = $maf_causal_snp, alpha = ", lw = 3) # plot power
xlabel!("Detectable Effect Size")
Out[29]:
0.0 0.1 0.2 0.3 0.4 0.5 0.00 0.25 0.50 0.75 1.00 Multinomial Power Detectable Effect Size maf = [0.2], alpha = 5.0e-6

Try it Yourself Exercises:

With the remaining time we encourage you to play with the simulation models above.

(1) If you change the significance level, α, what would happen to the plot of the power? How would the plot change with a smaller value of α ?

(2) What if you change the sample size to be smaller than n = 5000? How does the power look as a function of the effect size when n = 2500?

(3) What if we changed the minor allele frequency of the SNP of interest from maf = 0.2 to say maf = 0.3 ?

Citations:

[1] Lange K, Papp JC, Sinsheimer JS, Sripracha R, Zhou H, Sobel EM (2013) Mendel: The Swiss army knife of genetic analysis programs. Bioinformatics 29:1568-1570.`

[2] OPENMENDEL: a cooperative programming project for statistical genetics. Hum Genet. 2019 Mar 26. doi: 10.1007/s00439-019-02001-z.

[3] German, CA, Sinsheimer, JS, Klimentidis, YC, Zhou, H, Zhou, JJ. Ordered multinomial regression for genetic association analysis of ordinal phenotypes at Biobank scale. Genetic Epidemiology. 2019; 1– 13. https://doi.org/10.1002/gepi.22276