paramtest
packageThis is an old script I used to benchmark some power test algorithms, including the usage of the paramtest
package and its parallelism capabilities. You will notice I don’t use data.table
and fixest
here, up to you to adapt the following code, :-)
library(dplyr)
library(sandwich)
library(paramtest)
library(microbenchmark)
library(ggplot2)
Load data
data <- read.csv("Data/escolas.csv")
# Clean up the data. Never forget it!!
data <- data %>%
filter(!is.na(logico)) %>%
mutate(escola = if_else(escola == "S/escola", "99", escola)) %>%
mutate(escola = as.integer(escola))
We’ll consider a fine grid of treatment effects
grid = seq(0, 0.5, 0.05)
pt_func <- function(simNum, eff) {
#Drawing a sample, with replacement, of schools from our data. The idea here is that our sample well approximates the population distribution.
#and that the sampling scheme is close to random sampling from the population of interest.
sample.draw = sample(list.schools, replace = TRUE)
#Construct the artificial data from a draw. Note that repeated schools should be treated as distinct so as to mimic random sampling.
list.new.data = lapply(1:length(sample.draw), function(x){
extract = data[data$escola == sample.draw[x],]
return(extract)
})
#Concatenating data on lists
# data.artificial ends up with lower number of observations that original
# data
data.artificial = do.call(rbind, list.new.data)
#Next, we select that half of the schools will be treated
treat.draw = sample(unique(data.artificial$escola),size = length(unique(data.artificial$escola))/2, replace = F)
data.artificial$treat = 1*(data.artificial$escola %in% treat.draw)
#Create outcome
data.artificial$y = data.artificial$logico + data.artificial$treat*eff
#Running models and storing whether we reject the null that effect is 0
model1 = lm(y~treat, data = data.artificial)
se = sqrt(diag(sandwich::vcovCL(model1, cluster = data.artificial$escola)))
rej.col.1 = 1*(abs(model1$coefficients["treat"]/se["treat"]) > qnorm(0.975) )
model2 = lm(y~treat+mulher, data = data.artificial)
se = sqrt(diag(sandwich::vcovCL(model2, cluster = data.artificial$escola)))
rej.col.2 = 1*(abs(model2$coefficients["treat"]/se["treat"]) > qnorm(0.975) )
model3 = lm(y~treat+mulher+idade, data = data.artificial)
se = sqrt(diag(sandwich::vcovCL(model3, cluster = data.artificial$escola)))
rej.col.3 = 1*(abs(model3$coefficients["treat"]/se["treat"]) > qnorm(0.975) )
return(c(M1 = rej.col.1, M2 = rej.col.2, M3 = rej.col.3))
}
Passing data frame along
pt_func2 <- function(simNum, df, eff) {
list.schools <- unique(df$escola)
#Drawing a sample, with replacement, of schools from our data. The idea here is that our sample well approximates the population distribution.
#and that the sampling scheme is close to random sampling from the population of interest.
sample.draw = sample(list.schools, replace = T)
#Construct the artificial data from a draw. Note that repeated schools should be treated as distinct so as to mimic random sampling.
list.new.data = lapply(1:length(sample.draw), function(x){
extract = df[df$escola == sample.draw[x],]
#extract$escola = x
return(extract)
})
#Concatenating data on lists
# data.artificial ends up with lower number of observations that original
# data
data.artificial = do.call(rbind, list.new.data)
#Next, we select that half of the schools will be treated
treat.draw = sample(unique(data.artificial$escola),
size = length(unique(data.artificial$escola))/2,
replace = F)
data.artificial$treat = 1*(data.artificial$escola %in% treat.draw)
#Create outcome
data.artificial$y = data.artificial$logico + data.artificial$treat*eff
#Running models and storing whether we reject the null that effect is 0
model1 = lm(y~treat, data = data.artificial)
se = sqrt(diag(sandwich::vcovCL(model1, cluster = data.artificial$escola)))
rej.col.1 = 1*(abs(model1$coefficients["treat"]/se["treat"]) > qnorm(0.975) )
model2 = lm(y~treat+mulher, data = data.artificial)
se = sqrt(diag(sandwich::vcovCL(model2, cluster = data.artificial$escola)))
rej.col.2 = 1*(abs(model2$coefficients["treat"]/se["treat"]) > qnorm(0.975) )
model3 = lm(y~treat+mulher+idade, data = data.artificial)
se = sqrt(diag(sandwich::vcovCL(model3, cluster = data.artificial$escola)))
rej.col.3 = 1*(abs(model3$coefficients["treat"]/se["treat"]) > qnorm(0.975) )
return(c(M1 = rej.col.1, M2 = rej.col.2, M3 = rej.col.3))
}
Testing paramtest
First let as global variables: data and list.schools
list.schools <- unique(data$escola)
pt_power <- run_test(pt_func, n.iter = 1000, output = "data.frame",
eff = 0.5)
## Running 1,000 tests...
Test power results for one effect level only
colMeans(results(pt_power))[2:4]
## M1.treat M2.treat M3.treat
## 0.663 0.664 0.720
Now test passing data to pt_func2
pt_power2 <- run_test(pt_func2, n.iter = 1000, output = "data.frame",
df = data, eff = 0.5)
## Running 1,000 tests...
Test power results
colMeans(results(pt_power2))[2:4]
## M1.treat M2.treat M3.treat
## 0.683 0.679 0.724
WARNING: Benchmarking may take a long time to run depending on your number of simulations and number of replication in the benchmark (parameter “times”)
Benchmarking. Looks like there is no harm in passing the data.frame to the function.
pt_bench <- microbenchmark(
pt1 = run_test(pt_func, n.iter = 1000, output = "data.frame", eff = 0.5),
pt2 = run_test(pt_func2, n.iter = 1000, output = "data.frame", df = data,
eff = 0.5),
times = 5
)
## Running 1,000 tests...
## Running 1,000 tests...
## Running 1,000 tests...
## Running 1,000 tests...
## Running 1,000 tests...
## Running 1,000 tests...
## Running 1,000 tests...
## Running 1,000 tests...
## Running 1,000 tests...
## Running 1,000 tests...
autoplot(pt_bench)
## Coordinate system already present. Adding new coordinate system, which will replace the existing one.
Now let’s vary the eff argument, to capture the Minimum Detectable Effect
pt_pwr2 <- grid_search(pt_func2, n.iter = 1000, output = "data.frame",
df = data, params = list(eff = grid))
## Running 11,000 tests...
pwr2_df <- results(pt_pwr2) %>%
group_by(eff.test) %>%
summarise(across(contains("treat"), mean))
pwr2_df
## # A tibble: 11 x 4
## eff.test M1.treat M2.treat M3.treat
## <dbl> <dbl> <dbl> <dbl>
## 1 0 0.11 0.111 0.102
## 2 0.05 0.13 0.127 0.117
## 3 0.1 0.143 0.143 0.138
## 4 0.15 0.181 0.18 0.188
## 5 0.2 0.22 0.221 0.232
## 6 0.25 0.275 0.28 0.306
## 7 0.3 0.318 0.329 0.373
## 8 0.35 0.435 0.441 0.487
## 9 0.4 0.512 0.511 0.564
## 10 0.45 0.564 0.568 0.613
## 11 0.5 0.65 0.648 0.709
pt_par <- grid_search(pt_func2, n.iter = 1000, output = "data.frame",
df = data, params = list(eff = grid),
parallel = "multicore", ncpus = 4)
## Running 11,000 tests...
par_df <- results(pt_par) %>%
group_by(eff.test) %>%
summarise(across(contains("treat"), mean))
par_df
## # A tibble: 11 x 4
## eff.test M1.treat M2.treat M3.treat
## <dbl> <dbl> <dbl> <dbl>
## 1 0 0.112 0.109 0.102
## 2 0.05 0.137 0.135 0.131
## 3 0.1 0.115 0.117 0.109
## 4 0.15 0.199 0.193 0.205
## 5 0.2 0.214 0.217 0.239
## 6 0.25 0.267 0.273 0.294
## 7 0.3 0.357 0.362 0.39
## 8 0.35 0.445 0.448 0.49
## 9 0.4 0.521 0.526 0.584
## 10 0.45 0.578 0.585 0.625
## 11 0.5 0.662 0.67 0.712
Is it worth to use parallelization?
Benchmark it! Parallelized simulation is almost twice as fast.
par_bench <- microbenchmark(
no_par = grid_search(pt_func2, n.iter = 1000, output = "data.frame",
df = data, params = list(eff = grid)),
par = grid_search(pt_func2, n.iter = 1000, output = "data.frame",
df = data, params = list(eff = grid),
parallel = "multicore", ncpus = 4),
times = 5
)
## Running 11,000 tests...
## Running 11,000 tests...
## Running 11,000 tests...
## Running 11,000 tests...
## Running 11,000 tests...
## Running 11,000 tests...
## Running 11,000 tests...
## Running 11,000 tests...
## Running 11,000 tests...
## Running 11,000 tests...
par_bench
## Unit: seconds
## expr min lq mean median uq max neval
## no_par 174.20182 174.2672 187.46722 174.35237 174.42401 240.0907 5
## par 86.00311 86.5560 92.83074 86.60456 87.20923 117.7808 5