This tutorial illustrates how to do power analysis for multilevel models using the “simr” package in R. The examples follow the presentation in Chapter 10, Statistical Power for Intenstive Longitudinal Designs, of Bolger & Laurenceau (2013).
This script covers …
A. Power analysis for a simple multilevel model (growth model)
B. Power analysis for a multilevel process model
C. Power analysis for a categorical outcome multilevel model
Some other useful overviews on doing power analysis in R can be found at
http://www.alexanderdemos.org/Mixed9.html#simulate_observed_power_directly and
http://environmentalcomputing.net/power-analysis/.
Loading libraries used in this script.
library(psych) #for describing data
library(lme4) # multilevel models
library(simr) #for power analysis simualtions
library(ggplot2) # for data visualization
library(plyr) # for data manipulation
This example makes use of the Intimacy data set from B & L Chapter 4.
Loading the Intimacy data (N = 50, T = 16).
#set filepath for data file
filepath <- "https://quantdev.ssri.psu.edu/sites/qdev/files/BLdata_time.csv"
#read in the .csv file using the url() function
data_intimacy <- read.csv(file=url(filepath),header=TRUE)
Describe the data.
#describing the data
describe(data_intimacy)
## vars n mean sd median trimmed mad min max range skew
## id 1 800 25.50 14.44 25.50 25.50 18.53 1 50.00 49.00 0.00
## time 2 800 7.50 4.61 7.50 7.50 5.93 0 15.00 15.00 0.00
## time01 3 800 0.50 0.31 0.50 0.50 0.40 0 1.00 1.00 0.00
## intimacy 4 800 3.47 1.63 3.33 3.43 1.61 0 9.41 9.41 0.28
## treatment 5 800 0.50 0.50 0.50 0.50 0.74 0 1.00 1.00 0.00
## kurtosis se
## id -1.21 0.51
## time -1.21 0.16
## time01 -1.21 0.01
## intimacy -0.06 0.06
## treatment -2.00 0.02
#number of persons
length(unique(data_intimacy$id))
## [1] 50
#number of occasions
length(unique(data_intimacy$time))
## [1] 16
#crossing of persons and occasions
xtabs(~id+time, data=data_intimacy)
## time
## id 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
## 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 4 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 5 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 7 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 9 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 10 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 11 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 12 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 13 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 15 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 16 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 17 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 18 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 19 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 20 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 21 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 22 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 23 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 24 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 25 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 26 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 27 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 28 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 29 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 30 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 31 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 32 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 33 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 34 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 35 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 36 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 37 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 38 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 39 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 41 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 42 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 43 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 44 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 45 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 46 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 47 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 48 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 49 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 50 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
We see that there are 50 persons with 16 occasions each (coded 0 to 15). The data are fully complete.
We use the lmer() function in the lme4 library, as the simr power analysis package is built to work with lmer().
Running multilevel using lmer().
#Run linear growth model with AR(1) errors
fit_lgmodel <- lmer(intimacy ~ 1 + time01 + treatment + time01:treatment +
(1 + time01 | id),
data=data_intimacy,
na.action = na.exclude)
#examine model summary
summary(fit_lgmodel)
## Linear mixed model fit by REML ['lmerMod']
## Formula:
## intimacy ~ 1 + time01 + treatment + time01:treatment + (1 + time01 |
## id)
## Data: data_intimacy
##
## REML criterion at convergence: 2834.4
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -2.6243 -0.6798 -0.0190 0.6434 2.4463
##
## Random effects:
## Groups Name Variance Std.Dev. Corr
## id (Intercept) 0.6858 0.8281
## time01 1.8938 1.3762 -0.45
## Residual 1.6925 1.3010
## Number of obs: 800, groups: id, 50
##
## Fixed effects:
## Estimate Std. Error t value
## (Intercept) 2.89897 0.20703 14.002
## time01 0.73520 0.34721 2.117
## treatment -0.05644 0.29279 -0.193
## time01:treatment 0.92143 0.49103 1.877
##
## Correlation of Fixed Effects:
## (Intr) time01 trtmnt
## time01 -0.599
## treatment -0.707 0.423
## tm01:trtmnt 0.423 -0.707 -0.599
The results are the very similar to those in the book even through the model does not include the auto-correlation of residuals. lmer() does not provide for alternative error structures.
The object obtaned from lmer(), formally referred to as a merMod object is used in the power analysis.
We can simualte power directly from the fitted model using the powerSim() function. We supply the function with the fitted model object (fit_lgmodel), the specific parameter we would like to examine (“time01”) and test to use (“lr”), how many simulations we would like (nsim=100) and what alpha level to use in the evaluations of significance (alpha=.05).
Typically, power ananlysis simulations use 1000 or more simulations. For time savings here, we only use 100.
We evaluate all three parameters in the model.
Simulating power directly from fitted model for the time effect.
#Power for the effect of interest
SimPower_Direct<-powerSim(fit_lgmodel,test=fixed("time01","lr"),
seed=1234, #set for replication
nsim=100, alpha=.05, progress=TRUE) #options
## Simulating: | |Simulating: |= |Simulating: |== |Simulating: |=== |Simulating: |==== |Simulating: |===== |Simulating: |====== |Simulating: |======= |Simulating: |======== |Simulating: |========= |Simulating: |========== |Simulating: |=========== |Simulating: |============ |Simulating: |============= |Simulating: |============== |Simulating: |=============== |Simulating: |================ |Simulating: |================= |Simulating: |================== |Simulating: |=================== |Simulating: |==================== |Simulating: |===================== |Simulating: |====================== |Simulating: |======================= |Simulating: |======================== |Simulating: |========================= |Simulating: |========================== |Simulating: |=========================== |Simulating: |============================ |Simulating: |============================= |Simulating: |============================== |Simulating: |=============================== |Simulating: |================================ |Simulating: |================================= |Simulating: |================================== |Simulating: |=================================== |Simulating: |==================================== |Simulating: |===================================== |Simulating: |====================================== |Simulating: |======================================= |Simulating: |======================================== |Simulating: |========================================= |Simulating: |========================================== |Simulating: |=========================================== |Simulating: |============================================ |Simulating: |============================================= |Simulating: |============================================== |Simulating: |=============================================== |Simulating: |================================================ |Simulating: |================================================= |Simulating: |================================================== |Simulating: |=================================================== |Simulating: |==================================================== |Simulating: |===================================================== |Simulating: |====================================================== |Simulating: |======================================================= |Simulating: |======================================================== |Simulating: |========================================================= |Simulating: |========================================================== |Simulating: |=========================================================== |Simulating: |============================================================ |Simulating: |=============================================================|
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examine post-hoc power
SimPower_Direct
## Power for predictor 'time01', (95% confidence interval):
## 62.00% (51.75, 71.52)
##
## Test: Likelihood ratio
## Effect size for time01 is 0.74
##
## Based on 100 simulations, (100 warnings, 0 errors)
## alpha = 0.05, nrow = 800
##
## Time elapsed: 0 h 0 m 12 s
##
## nb: result might be an observed power calculation
We see that with an effect size of 0.74, power for the time01 predictor is 62%, less than ideal.
Simulating power directly from fitted model for the treatment effect.
#Power for the effect of interest
SimPower_Direct<-powerSim(fit_lgmodel,test=fixed("treatment","lr"),
seed=1234, #set for replication
nsim=100, alpha=.05, progress=FALSE) #options
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examine post-hoc power
SimPower_Direct
## Power for predictor 'treatment', (95% confidence interval):
## 5.00% ( 1.64, 11.28)
##
## Test: Likelihood ratio
## Effect size for treatment is -0.056
##
## Based on 100 simulations, (100 warnings, 0 errors)
## alpha = 0.05, nrow = 800
##
## Time elapsed: 0 h 0 m 11 s
##
## nb: result might be an observed power calculation
We see that with an effect size of -0.06, power for the treatment predictor is 5%, very low.
Simulating power directly from fitted model for the time01:treatment interaction effect.
#Power for the effect of interest
SimPower_Direct<-powerSim(fit_lgmodel,test=fixed("time01:treatment","lr"),
seed=1234, #set for replication
nsim=100, alpha=.05, progress=FALSE) #options
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examine post-hoc power
SimPower_Direct
## Power for predictor 'time01:treatment', (95% confidence interval):
## 45.00% (35.03, 55.27)
##
## Test: Likelihood ratio
## Effect size for time01:treatment is 0.92
##
## Based on 100 simulations, (20 warnings, 0 errors)
## alpha = 0.05, nrow = 800
##
## Time elapsed: 0 h 0 m 11 s
##
## nb: result might be an observed power calculation
We see that with an effect size of 0.92, power for the time01:treatment interaction is 45%, lower than ideal.
All three of these evaluations are very similar to what was obtained using Mplus and presented in the book.
Given low power, we would like to evaluate what is the power to detect the interaction at different sample sizes - obtained by either adding more persons or adding more occasions within person.
To evaluate sample sizes that are larger than the original data, we first “extend” the data by creating a new lmer() object. Specifically, we use the extend() function in the simr package.
We do that by extending the data that is inside the lmer() model object to include more participants or more occasions.
Look at size of the data within the lmer() object.
#xtabs(~id+time, data=getData(fit_lgmodel))
Extending the number of persons from 50 to 200.
#extending number of persons
fit_lgmodelpersons <- extend(fit_lgmodel, along="id", n=200)
#checking length of data, number of persons
length(unique(getData(fit_lgmodelpersons)$id))
## [1] 200
#checking length of data, number of times
length(unique(getData(fit_lgmodelpersons)$time))
## [1] 16
Extending the number of occasions from 16 to 60.
#extending number of occasions
fit_lgmodeltimes <- extend(fit_lgmodel, along="time", n=60)
#checking length of data, number of persons
length(unique(getData(fit_lgmodeltimes)$id))
## [1] 50
#checking length of data, number of times
length(unique(getData(fit_lgmodeltimes)$time))
## [1] 60
Using the new model objects with extented data, we can examine what power looks like all the way up to N = 200.
Running power curve analysis for a range of sample sizes - persons (each with T = 16 occasions). Specific sample sizes chosen so that the total number of observations, N**T*, is near 500, 1000, 1500, 2000, 2500, and 3000.
#running power curve
powerCurve_persons <-powerCurve(fit_lgmodelpersons,
fixed("time01:treatment", "lr"),
along = "id",
breaks = c(32,63,94,125,157,188),
nsim=100,alpha=.05, progress=FALSE)
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examining power estiamtes
summary(powerCurve_persons)
## nrow nlevels successes trials mean lower upper
## 1 512 32 28 100 0.28 0.1947936 0.3786670
## 2 1008 63 54 100 0.54 0.4374116 0.6401566
## 3 1504 94 76 100 0.76 0.6642645 0.8397754
## 4 2000 125 87 100 0.87 0.7879593 0.9289270
## 5 2512 157 92 100 0.92 0.8484424 0.9648284
## 6 3008 188 98 100 0.98 0.9296161 0.9975687
#plot(powerCurve_persons)
We see the power estimates as each sample size, and how power increases as number of persons increases.
Using the new model objects with extented data, we can also examine what power looks like all the way up to T = 60.
Running power curve analysis for a range of sample sizes - occasions (increasing number of occasions within person). Specific sample sizes chosen so that the total number of observations, N**T*, is near 500, 1000, 1500, 2000, 2500, and 3000.
#running power curve
powerCurve_times <-powerCurve(fit_lgmodeltimes,
fixed("time01:treatment", "lr"),
within = "id",
breaks = c(10,20,30,40,50,60),
nsim=100,alpha=.05, progress=FALSE)
## boundary (singular) fit: see ?isSingular
## boundary (singular) fit: see ?isSingular
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examining power estiamtes
summary(powerCurve_times)
## nrow nlevels successes trials mean lower upper
## 1 500 10 29 100 0.29 0.2035742 0.3892660
## 2 1000 20 49 100 0.49 0.3886442 0.5919637
## 3 1500 30 51 100 0.51 0.4080363 0.6113558
## 4 2000 40 54 100 0.54 0.4374116 0.6401566
## 5 2500 50 59 100 0.59 0.4871442 0.6873800
## 6 3000 60 60 100 0.60 0.4972092 0.6967052
#plot(powerCurve_times)
We see the power estimates as each sample size, and how power does not increase very much as the number of occasions increases.
Of course, power curves are often useful in visual form. It takes a few steps to get them into a form useful for ggplot().
Prepare the summary outputs for plotting (Data Management).
#preparing persons power curve data
powerdata_persons <- as.data.frame(summary(powerCurve_persons))
powerdata_persons$persons <- powerdata_persons$nlevels
powerdata_persons$times <- 16
powerdata_persons$design <- "persons"
#preparing times power curve data
powerdata_times <- as.data.frame(summary(powerCurve_times))
powerdata_times$persons <-50
powerdata_times$times <- powerdata_times$nlevels
powerdata_times$design <- "times"
#merging together
powerdata_all <- rbind(powerdata_persons, powerdata_times)
Plotting power curves
#Plotting curves
ggplot(data = powerdata_all,
aes(y = mean, x=nrow, group=factor(design))) +
#power goal
geom_hline(yintercept=0.8, color="gray40",lty = 2) +
#persons curve
geom_line(color="black") +
geom_errorbar(ymin=powerdata_all$lower,ymax=powerdata_all$upper, width=50, color="black") +
geom_point(aes(shape=factor(powerdata_all$design)),size=3, color="black", show.legend = FALSE) +
scale_shape_manual(values=c(15,17)) +
scale_x_continuous(limits=c(500,3100),
breaks=c(500,1000,1500,2000,2500,3000),
name="Number of Total Observations") +
scale_y_continuous(limits=c(0,1.0),
breaks=c(0,.2,.4,.6,.8,1.0),
name="Power") +
theme_classic() +
#theme(axis.title=element_text(size=14),
# axis.text=element_text(size=14),
# plot.title=element_text(size=14, hjust=.5)) +
ggtitle("Power Curves for time01:treatment interaction")
Nice! We can see that power increases with larger sample size (squares), but not so much with additional occasions (triangles). This is the same as the Figure 10.1 in B & L. Readers are referred to the book for more in-depth discussion of the implications, but the general notion is that more level-2 units (here, persons) is better.
This example makes use of the Process data set from B & L Chapter 5.
Loading the Process data (N = 66, T = 28).
#set filepath for data file
filepath <- "https://quantdev.ssri.psu.edu/sites/qdev/files/BLdata_process.csv"
#read in the .csv file using the url() function
data_process <- read.csv(file=url(filepath),header=TRUE)
Describe the data.
#describing the data
describe(data_process)
## vars n mean sd median trimmed mad min max range
## id 1 1848 33.50 19.06 33.50 33.50 24.46 1.00 66.00 65.00
## time 2 1848 13.50 8.08 13.50 13.50 10.38 0.00 27.00 27.00
## time7c 3 1848 0.00 1.15 0.00 0.00 1.48 -1.93 1.93 3.86
## intimacy 4 1848 4.82 2.28 4.84 4.83 2.34 0.00 10.00 10.00
## conflict 5 1848 0.22 0.42 0.00 0.15 0.00 0.00 1.00 1.00
## confc 6 1848 0.00 0.42 -0.22 -0.07 0.00 -0.22 0.78 1.00
## confcb 7 1848 0.00 0.16 -0.04 -0.02 0.16 -0.22 0.42 0.64
## confcw 8 1848 0.00 0.38 -0.11 -0.04 0.16 -0.64 0.96 1.61
## relqual 9 1848 0.61 0.49 1.00 0.63 0.00 0.00 1.00 1.00
## skew kurtosis se
## id 0.00 -1.20 0.44
## time 0.00 -1.21 0.19
## time7c 0.00 -1.21 0.03
## intimacy -0.02 -0.50 0.05
## conflict 1.34 -0.20 0.01
## confc 1.34 -0.20 0.01
## confcb 0.75 -0.35 0.00
## confcw 1.04 0.04 0.01
## relqual -0.43 -1.81 0.01
#number of persons
length(unique(data_process$id))
## [1] 66
#number of occasions
length(unique(data_process$time))
## [1] 28
#crossing of persons and occasions
#xtabs(~id+time, data=data_process)
We see that there are 66 persons with 28 occasions each (coded 0 to 27). The data are fully complete.
We use the lmer() function in the lme4 library, as the simr power analysis package is built to work with lmer().
Running multilevel using lmer().
#Run multilevel model
fit_cpmodel <- lmer(intimacy ~ 1 + relqual + confcb + relqual:confcb +
confcw + confcw:relqual +
time7c +
(1 + confcw | id),
data=data_process,
na.action = na.exclude)
#examine model summary
summary(fit_cpmodel)
## Linear mixed model fit by REML ['lmerMod']
## Formula:
## intimacy ~ 1 + relqual + confcb + relqual:confcb + confcw + confcw:relqual +
## time7c + (1 + confcw | id)
## Data: data_process
##
## REML criterion at convergence: 7817.9
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -3.0095 -0.6767 -0.0045 0.6714 2.9160
##
## Random effects:
## Groups Name Variance Std.Dev. Corr
## id (Intercept) 0.8005 0.8947
## confcw 2.6993 1.6430 0.27
## Residual 3.5908 1.8949
## Number of obs: 1848, groups: id, 66
##
## Fixed effects:
## Estimate Std. Error t value
## (Intercept) 4.53208 0.22129 20.480
## relqual 0.64735 0.28209 2.295
## confcb -0.84163 1.10421 -0.762
## confcw -2.02444 0.37063 -5.462
## time7c -0.02820 0.03865 -0.730
## relqual:confcb 2.52633 1.68176 1.502
## relqual:confcw 1.03382 0.49235 2.100
##
## Correlation of Fixed Effects:
## (Intr) relqul confcb confcw time7c rlql:cnfcb
## relqual -0.784
## confcb -0.520 0.408
## confcw 0.166 -0.131 0.036
## time7c 0.001 -0.001 -0.001 -0.008
## relql:cnfcb 0.341 -0.038 -0.657 -0.024 -0.002
## relql:cnfcw -0.125 0.178 -0.027 -0.753 0.005 0.040
The results are the very similar to those in the book.
As before, we can simulate power directly from the fitted model using the powerSim() function. We supply the function with the fitted model object (fit_cpmodel), the specific parameter we would like to examine (“relqual:confcw”) and test to use (“lr”), how many simulations we would like (nsim=100) and what alpha level to use in the evaluations of significance (alpha=.05).
Typically, power ananlysis simulations use 1000 or more simulations. For time savings here, we only use 100.
This time, we just evaluate the main parameter of interest.
Simulating power directly from fitted model for the relqual:confcw interaction effect.
#Power for the effect of interest
SimPower_Direct<-powerSim(fit_cpmodel,test=fixed("relqual:confcw","lr"),
seed=1234, #set for replication
nsim=100, alpha=.05, progress=FALSE) #options
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examine post-hoc power
SimPower_Direct
## Power for predictor 'relqual:confcw', (95% confidence interval):
## 58.00% (47.71, 67.80)
##
## Test: Likelihood ratio
## Effect size for relqual:confcw is 1.0
##
## Based on 100 simulations, (20 warnings, 0 errors)
## alpha = 0.05, nrow = 1848
##
## Time elapsed: 0 h 0 m 20 s
##
## nb: result might be an observed power calculation
We see that with an effect size of 1.0, power for the relqual:confcw interaction is 58%, just as was found in the book.
Given low power, we would like to evaluate what is the power to detect the interaction at different sample sizes - obtained by either adding more persons or adding more occasions within person.
To evaluate sample sizes that are larger than the original data, we first “extend” the data by creating a new lmer() object. Specifically, we use the extend() function in the simr package.
We do that by extending the data that is inside the lmer() model object to include more participants or more occasions.
Extending the number of persons from 66 to 200.
#extending number of persons
fit_cpmodelpersons <- extend(fit_cpmodel, along="id", n=200)
#checking length of data, number of persons
length(unique(getData(fit_cpmodelpersons)$id))
## [1] 200
#checking length of data, number of times
length(unique(getData(fit_cpmodelpersons)$time))
## [1] 28
Extending the number of occasions from 28 to 70.
#extending number of occasions
fit_cpmodeltimes <- extend(fit_cpmodel, along="time", n=70)
#checking length of data, number of persons
length(unique(getData(fit_cpmodeltimes)$id))
## [1] 66
#checking length of data, number of times
length(unique(getData(fit_cpmodeltimes)$time))
## [1] 70
Using the new model objects with extented data, we can examine what power looks like all the way up to N = 165.
Running power curve analysis for a range of sample sizes - persons (each with T = 16 occasions).
#running power curve
powerCurve_persons <-powerCurve(fit_cpmodelpersons,
fixed("relqual:confcw", "lr"),
along = "id",
breaks = c(35,66,83,95,118,142,165),
nsim=100,alpha=.05, progress=FALSE)
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examining power estiamtes
summary(powerCurve_persons)
## nrow nlevels successes trials mean lower upper
## 1 980 35 31 100 0.31 0.2212888 0.4103146
## 2 1848 66 40 100 0.40 0.3032948 0.5027908
## 3 2324 83 52 100 0.52 0.4177898 0.6209945
## 4 2660 95 57 100 0.57 0.4671337 0.6686090
## 5 3304 118 69 100 0.69 0.5896854 0.7787112
## 6 3976 142 73 100 0.73 0.6319837 0.8139336
## 7 4620 165 84 100 0.84 0.7532124 0.9056897
#plot(powerCurve_persons)
We see the power estimates as each sample size, and how power increases as number of persons increases.
Using the new model objects with extented data, we can also examine what power looks like all the way up to T = 60.
Running power curve analysis for a range of sample sizes - occasions (increasing number of occasions within person). Specific sample sizes chosen so that the total number of observations, N**T*, is near 500, 1000, 1500, 2000, 2500, and 3000.
#running power curve
powerCurve_times <-powerCurve(fit_cpmodeltimes,
fixed("relqual:confcw", "lr"),
within = "id",
breaks = c(15,28,35,40,50,60,70),
nsim=100,alpha=.05, progress=FALSE)
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examining power estiamtes
summary(powerCurve_times)
## nrow nlevels successes trials mean lower upper
## 1 990 15 53 100 0.53 0.4275815 0.6305948
## 2 1848 28 57 100 0.57 0.4671337 0.6686090
## 3 2310 35 59 100 0.59 0.4871442 0.6873800
## 4 2640 40 58 100 0.58 0.4771192 0.6780145
## 5 3300 50 62 100 0.62 0.5174607 0.7152325
## 6 3960 60 65 100 0.65 0.5481506 0.7427062
## 7 4620 70 65 100 0.65 0.5481506 0.7427062
#plot(powerCurve_times)
We see the power estimates as each sample size, and how power does not increase very much as the number of occasions increases.
Of course, power curves are often useful in visual form. It takes a few steps to get them into a form useful for ggplot().
Prepare the summary outputs for plotting (Data Management).
#preparing persons power curve data
powerdata_persons <- as.data.frame(summary(powerCurve_persons))
powerdata_persons$persons <- powerdata_persons$nlevels
powerdata_persons$times <- 28
powerdata_persons$design <- "persons"
#preparing times power curve data
powerdata_times <- as.data.frame(summary(powerCurve_times))
powerdata_times$persons <-66
powerdata_times$times <- powerdata_times$nlevels
powerdata_times$design <- "times"
#merging together
powerdata_all <- rbind(powerdata_persons, powerdata_times)
Plotting power curves
#Plotting curves
ggplot(data = powerdata_all,
aes(y = mean, x=nrow, group=factor(design))) +
#power goal
geom_hline(yintercept=0.8, color="gray40",lty = 2) +
#persons curve
geom_line(color="black") +
geom_errorbar(ymin=powerdata_all$lower,ymax=powerdata_all$upper, width=50, color="black") +
geom_point(aes(shape=factor(powerdata_all$design)),size=3, color="black", show.legend = FALSE) +
scale_shape_manual(values=c(15,17)) +
scale_x_continuous(limits=c(950,5000),
breaks=c(990,1848,2320,2640,3300,3960,4620),
name="Number of Total Observations") +
scale_y_continuous(limits=c(0,1.0),
breaks=c(0,.2,.4,.6,.8,1.0),
name="Power") +
theme_classic() +
#theme(axis.title=element_text(size=14),
# axis.text=element_text(size=14),
# plot.title=element_text(size=14, hjust=.5)) +
ggtitle("Power Curves for relqual:confcw interaction")
Nice! We can see that power increases with larger sample size (squares), but not so much with additional occasions (triangles). This is the same as the Figure 10.2 in B & L. Readers are referred to the book for more in-depth discussion of the implications, but the general notion is that more level-2 units (here, persons) is better.
This example makes use of the Categorical data set from B & L Chapter 6.
Loading the Categorical data (N = 61, T = 27).
#set filepath for data file
filepath <- "https://quantdev.ssri.psu.edu/sites/qdev/files/BLdata_categorical.csv"
#read in the .csv file using the url() function
data_categorical <- read.csv(file=url(filepath),header=TRUE)
Describe the data.
#describing the data
describe(data_categorical)
## vars n mean sd median trimmed mad min max range
## id 1 1345 45.61 30.19 38.00 44.44 37.06 1.00 102.00 101.00
## time 2 1345 14.66 7.76 14.00 14.60 10.38 2.00 28.00 26.00
## time7c 3 1345 0.00 1.15 -0.10 -0.01 1.54 -1.88 1.98 3.85
## pconf 4 1345 0.14 0.35 0.00 0.06 0.00 0.00 1.00 1.00
## lpconf 5 1345 0.16 0.36 0.00 0.07 0.00 0.00 1.00 1.00
## lpconfc 6 1345 0.00 0.36 -0.16 -0.09 0.00 -0.16 0.84 1.00
## amang 7 1345 0.49 1.11 0.00 0.22 0.00 0.00 10.00 10.00
## amangc 8 1345 0.00 1.11 -0.49 -0.27 0.00 -0.49 9.51 10.00
## amangcb 9 1345 0.00 0.49 -0.18 -0.10 0.27 -0.47 1.61 2.08
## amangcw 10 1345 0.00 1.00 -0.17 -0.15 0.27 -2.10 9.40 11.50
## skew kurtosis se
## id 0.28 -1.29 0.82
## time 0.06 -1.18 0.21
## time7c 0.06 -1.18 0.03
## pconf 2.02 2.09 0.01
## lpconf 1.88 1.55 0.01
## lpconfc 1.88 1.55 0.01
## amang 3.96 21.05 0.03
## amangc 3.96 21.05 0.03
## amangcb 1.72 2.32 0.01
## amangcw 3.78 22.51 0.03
#number of persons
length(unique(data_categorical$id))
## [1] 61
#number of occasions
length(unique(data_categorical$time))
## [1] 27
#crossing of persons and occasions
head(xtabs(~id+time, data=data_categorical),10) #just top of output
## time
## id 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
## 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 5 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
## 6 0 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 9 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 10 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 11 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1
## 12 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 13 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1
## 14 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
## time
## id 28
## 1 1
## 3 1
## 5 0
## 6 1
## 9 1
## 10 1
## 11 1
## 12 1
## 13 1
## 14 0
We see that there are 61 persons with 27 occasions (coded 2 to 28) - and that the data are not fully complete.
Examine how many observations per person.
#calculating intraindividual stats (counts)
istats <- ddply(data_categorical, "id", summarize,
icount_pconf = sum(!is.na(pconf)), #count of observations
icount_amang = sum(!is.na(amang)))
describe(istats)
## vars n mean sd median trimmed mad min max range skew
## id 1 61 46.30 29.86 42 45.24 40.03 1 102 101 0.26
## icount_pconf 2 61 22.05 6.52 25 23.35 2.97 5 27 22 -1.43
## icount_amang 3 61 22.05 6.52 25 23.35 2.97 5 27 22 -1.43
## kurtosis se
## id -1.32 3.82
## icount_pconf 0.88 0.84
## icount_amang 0.88 0.84
xtabs(~icount_pconf, data=istats)
## icount_pconf
## 5 6 11 12 13 14 15 17 19 20 21 22 23 24 25 26 27
## 2 3 1 1 2 1 1 1 2 1 2 2 7 2 10 2 21
We see that on average individual completed 22 occasions.
Running the multilevel model with the available data.
We use the lmer() function in the lme4 library, as the simr power analysis package is built to work with lmer().
Running multilevel using lmer().
#Run multilevel model
fit_catmodel <- glmer(pconf ~ 1 + amangcw + amangcb + lpconfc + time7c +
(1 | id),
family=binomial,
data=data_categorical,
na.action = na.exclude)
#examine model summary
summary(fit_catmodel)
## Generalized linear mixed model fit by maximum likelihood (Laplace
## Approximation) [glmerMod]
## Family: binomial ( logit )
## Formula: pconf ~ 1 + amangcw + amangcb + lpconfc + time7c + (1 | id)
## Data: data_categorical
##
## AIC BIC logLik deviance df.resid
## 1084.8 1116.0 -536.4 1072.8 1339
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -0.7981 -0.4224 -0.3533 -0.2893 4.5619
##
## Random effects:
## Groups Name Variance Std.Dev.
## id (Intercept) 0.2475 0.4975
## Number of obs: 1345, groups: id, 61
##
## Fixed effects:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -1.90161 0.10894 -17.456 < 2e-16 ***
## amangcw 0.21565 0.06738 3.200 0.00137 **
## amangcb -0.22437 0.22398 -1.002 0.31649
## lpconfc 0.31826 0.20892 1.523 0.12767
## time7c -0.19222 0.07073 -2.718 0.00657 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Correlation of Fixed Effects:
## (Intr) amngcw amngcb lpcnfc
## amangcw -0.108
## amangcb 0.050 -0.103
## lpconfc 0.006 -0.083 0.052
## time7c 0.132 -0.004 -0.027 0.102
The results are the very similar to the Mplus results reported in the book.
As before, we can simulate power directly from the fitted model using the powerSim() function. We supply the function with the fitted model object (fit_catmodel), the specific parameter we would like to examine (“amangcw”) and test to use (“lr”), how many simulations we would like (nsim=100) and what alpha level to use in the evaluations of significance (alpha=.05).
Typically, power ananlysis simulations use 1000 or more simulations. For time savings here, we only use 100.
This time, we just evaluate the main parameter of interest.
Simulating power directly from fitted model for the amangcw effect.
#Power for the effect of interest
SimPower_Direct<-powerSim(fit_catmodel,test=fixed("amangcw","lr"),
seed=1234, #set for replication
nsim=100, alpha=.05, progress=FALSE) #options
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examine post-hoc power
SimPower_Direct
## Power for predictor 'amangcw', (95% confidence interval):
## 79.00% (69.71, 86.51)
##
## Test: Likelihood ratio
## Effect size for amangcw is 0.22
##
## Based on 100 simulations, (0 warnings, 0 errors)
## alpha = 0.05, nrow = 1345
##
## Time elapsed: 0 h 0 m 55 s
##
## nb: result might be an observed power calculation
We see that with an effect size of 0.22, power for the amangcw effect is 79%. Note that since the data object we are using has the missing data in it, we are already doing the power anaysis with, on average, 22 occcasions (i.e., we do not need to reduce to get that analysis, as B & L do in Chapter 10.5).
As is done in the book, we can also test power for the variance term for the intercept. We use the compare function and specify the model to compare against, which in this case is a model without any random effects. Simulating power directly from fitted model for the random effect.
#Power for the single random effect
SimPower_variance<-powerSim(fit_catmodel,test=random(), #empty becasue only one
seed=1234, #set for replication
nsim=100, alpha=.05, progress=FALSE) #options
## Warning in observedPowerWarning(sim): This appears to be an "observed
## power" calculation
#examine post-hoc power of random effect
SimPower_variance
## Power for a single random effect, (95% confidence interval):
## 0.00% ( 0.00, 3.62)
##
## Test: Exact restricted LRT (package RLRsim)
##
## Based on 100 simulations, (0 warnings, 100 errors)
## alpha = 0.05, nrow = 1345
##
## Time elapsed: 0 h 0 m 33 s
##
## nb: result might be an observed power calculation
Note that we get something a bit different from the book here. We are not yet sure why, but are exciterd about the prospects of also being able to examine power for random effects as this is where more occasions may be relevant.
This tutorial covered some basics for conducting power analysis for multilevel model in R using the simr package. We fit models using “pilot” data and then extended those data in order to examine power across a range of sample sizes - persons and occasions. There is also facility for construting a model and power analysis from scratch (prior to having data for analysis). We shall look to cover that in a future tutorial.
Thanks for powering through with us!