---
title: "Comparing Rasch packages/estimators"
subtitle: "Item threshold parameter estimation"
author:
name: 'Magnus Johansson'
affiliation: 'RISE Research Institutes of Sweden'
affiliation-url: 'https://ri.se/shic'
orcid: '0000-0003-1669-592X'
date: 2024-09-06
date-format: iso
google-scholar: true
citation:
type: 'webpage'
format:
html:
code-fold: true
execute:
cache: true
warning: false
message: false
editor_options:
chunk_output_type: console
bibliography: est_comp.bib
---
## Background
I'm doing comparisons of the bias in the estimators for the Partial Credit Model (PCM) implemented in Rasch R packages and thought I would share a bit of code and simulated data. There will be more extensive simulations and comparisons coming along, varying targeting and sample distribution.
For this example we have a simple setup:
- 250 datasets
- each dataset has 9 items and 4 thresholds (5 response categories)
- each dataset has 720 respondents (20 per threshold estimated, which should be plenty)
The .rds data file contains a list object with the 250 datasets, and each dataset is accompanied by a matrix of item threshold parameters and a vector of theta values used to generate the response data.
- the vector of theta values is normally distributed with a mean and SD closely matching that of the item parameters
We have four packages in the comparison:
- `eRm` - Conditional Maximum Likelihood (CML)
- `TAM` - Marginal ML (MML)
- `pairwise` - Pairwise CML (PCML)
- `mirt` - fixed quadrature expectation-maximization algorithm
Other estimation methods are available in `TAM` and `mirt` , but we'll just test these for now.
The datafile is available in the [ Github repo ](https://github.com/pgmj/pgmj.github.io/blob/main/sim_data_4t.rds) .
## Data import
```{r}
library (tidyverse)
library (eRm)
library (janitor)
library (TAM)
library (mirt)
library (pairwise)
library (ggdist)
library (doParallel)
library (tinytable)
### some commands exist in multiple packages, here we define preferred ones that are frequently used
select <- dplyr:: select
count <- dplyr:: count
recode <- car:: recode
rename <- dplyr:: rename
# read 250 simulated datasets, stored in a list() object
sim_data_4t <- readRDS ("sim_data_4t.rds" )
```
## Estimation
We'll make use of `library(doParallel)` for multicore processing.
```{r}
registerDoParallel (cores = 8 )
iterations = 250
sim_results_4t <- list ()
sim_results_4t <- foreach (i = 1 : iterations) %dopar% {
input_params <- sim_data_4t[[i]]$ input_params
input_thetas <- sim_data_4t[[i]]$ input_thetas
testData <- sim_data_4t[[i]]$ data
# check mean/centrality (item parameters should have been centered before data generation)
input_params_correction <- mean (input_params)
# eRm
erm_out <- PCM (testData)
erm_params <- thresholds (erm_out)[[3 ]][[1 ]][,- 1 ] %>%
as.data.frame () %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
erm_params_c <- erm_params - mean (erm_params) + input_params_correction
# TAM
tam_out <- tam (as.matrix (testData), irtmodel = "PCM" , verbose = FALSE )
tam_params <- tam_out$ item_irt %>%
as.data.frame () %>%
select (starts_with ("tau" )) %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
tam_params_c <- tam_params - mean (tam_params) + input_params_correction
# mirt
mirt_out <- mirt (data = testData, model = 1 , itemtype = "Rasch" , verbose = FALSE )
mirt_params <- coef (mirt_out, simplify = TRUE , IRTpars = TRUE )$ items %>%
as.data.frame () %>%
select (! a) %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
mirt_params_c <- mirt_params - mean (mirt_params) + input_params_correction
# PAIR
pair_out <- pair (testData)
pair_params <- deltapar (pair_out) %>%
as.data.frame ()
pair_params <- pair_params[,- 1 ] %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
pair_params_c <- pair_params - mean (pair_params) + input_params_correction
# combine item parameters and calculate lowest to highest threshold distances
input_params %>%
as.data.frame () %>%
rename (T1 = V1,
T2 = V2,
T3 = V3,
T4 = V4) %>%
add_column (Item = c (1 : 9 ),
Type = "Input" ) %>%
bind_rows (
erm_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "eRm" )
) %>%
bind_rows (
tam_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "TAM" )
) %>%
bind_rows (
mirt_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "mirt" )
) %>%
bind_rows (
pair_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "PAIR" )
) %>%
remove_rownames ()
}
```
## Results
```{r}
# collect item threshold parameters to a single dataframe
results_params_4t <- map_df (1 : iterations, ~ sim_results_4t[[.x]] %>%
mutate (iteration = .x))
```
### Summary table MAE
Mean Absolute Error
```{r}
# calculate absolute differences between input and estimated item thresholds per estimator, average by item
results_params_diff_4t <- results_params_4t %>%
pivot_wider (
values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" , "Item" )
) %>%
group_by (iteration, Item) %>%
summarise (
diff_eRm = sum (abs (T1_Input - T1_eRm) + abs (T2_Input - T2_eRm) + abs (T3_Input - T3_eRm) + abs (T4_Input - T4_eRm))/ 4 ,
diff_TAM = sum (abs (T1_Input - T1_TAM) + abs (T2_Input - T2_TAM) + abs (T3_Input - T3_TAM) + abs (T4_Input - T4_TAM))/ 4 ,
diff_mirt = sum (abs (T1_Input - T1_mirt) + abs (T2_Input - T2_mirt) + abs (T3_Input - T3_mirt) + abs (T4_Input - T4_mirt))/ 4 ,
diff_PAIR = sum (abs (T1_Input - T1_PAIR) + abs (T2_Input - T2_PAIR) + abs (T3_Input - T3_PAIR) + abs (T4_Input - T4_PAIR))/ 4
) %>%
ungroup () %>%
pivot_longer (
cols = c (diff_eRm, diff_TAM,diff_mirt,diff_PAIR),
names_to = c (NA , "Estimator" ),
names_sep = "_" ,
values_to = "diff"
)
# produce table with summary stats per estimator/package
results_params_diff_4t %>%
group_by (Estimator) %>%
summarise (
Median = median (diff),
MAD = mad (diff),
IQR = IQR (diff),
Mean = mean (diff),
SD = sd (diff)
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
rename (Package = Estimator) %>%
arrange (Median) %>%
tt ()
```
### Summary table RMSE
Root Mean Squared Error
```{r}
# RMSE
results_params_4t %>%
pivot_wider (
values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" , "Item" )
) %>%
group_by (iteration, Item) %>% # sqrt(mean((data$actual - data$predicted)^2))
summarise (
rmse_eRm = sqrt (mean (((T1_Input - T1_eRm)^ 2 ) + ((T2_Input - T2_eRm)^ 2 ) + ((T3_Input - T3_eRm)^ 2 ) + ((T4_Input - T4_eRm))^ 2 )),
rmse_TAM = sqrt (mean (((T1_Input - T1_TAM)^ 2 ) + ((T2_Input - T2_TAM)^ 2 ) + ((T3_Input - T3_TAM)^ 2 ) + ((T4_Input - T4_TAM))^ 2 )),
rmse_mirt = sqrt (mean (((T1_Input - T1_mirt)^ 2 ) + ((T2_Input - T2_mirt)^ 2 ) + ((T3_Input - T3_mirt)^ 2 ) + ((T4_Input - T4_mirt))^ 2 )),
rmse_PAIR = sqrt (mean (((T1_Input - T1_PAIR)^ 2 ) + ((T2_Input - T2_PAIR)^ 2 ) + ((T3_Input - T3_PAIR)^ 2 ) + ((T4_Input - T4_PAIR))^ 2 ))
) %>%
ungroup () %>%
pivot_longer (
cols = c (rmse_eRm, rmse_TAM,rmse_mirt,rmse_PAIR),
names_to = c (NA , "Estimator" ),
names_sep = "_" ,
values_to = "rmse"
) %>%
group_by (Estimator) %>%
summarise (
Median = median (rmse),
MAD = mad (rmse),
IQR = IQR (rmse),
Mean = mean (rmse),
SD = sd (rmse)
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
rename (Package = Estimator) %>%
arrange (Median) %>%
tt ()
```
### Thresholds summary MAE
```{r}
# table summarizing all thresholds and estimators
results_params_4t %>%
pivot_wider (values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" ,"Item" )
) %>%
group_by (iteration,Item) %>%
summarise (
diff_eRm.T1 = abs (T1_eRm - T1_Input),
diff_eRm.T2 = abs (T2_eRm - T2_Input),
diff_eRm.T3 = abs (T3_eRm - T3_Input),
diff_eRm.T4 = abs (T4_eRm - T4_Input),
diff_TAM.T1 = abs (T1_TAM - T1_Input),
diff_TAM.T2 = abs (T2_TAM - T2_Input),
diff_TAM.T3 = abs (T3_TAM - T3_Input),
diff_TAM.T4 = abs (T4_TAM - T4_Input),
diff_mirt.T1 = abs (T1_mirt - T1_Input),
diff_mirt.T2 = abs (T2_mirt - T2_Input),
diff_mirt.T3 = abs (T3_mirt - T3_Input),
diff_mirt.T4 = abs (T4_mirt - T4_Input),
diff_PAIR.T1 = abs (T1_PAIR - T1_Input),
diff_PAIR.T2 = abs (T2_PAIR - T2_Input),
diff_PAIR.T3 = abs (T3_PAIR - T3_Input),
diff_PAIR.T4 = abs (T4_PAIR - T4_Input)
) %>%
ungroup () %>%
pivot_longer (cols = starts_with ("diff" ),
names_to = c ("Estimator" ,"Threshold" ),
names_sep = " \\ ." ,
values_to = "diff" ) %>%
mutate (Package = gsub ("diff_" ,"" ,Estimator)) %>%
group_by (Package, Threshold) %>%
summarise (
Median = median (diff),
MAD = mad (diff),
IQR = IQR (diff),
Mean = mean (diff),
SD = sd (diff)
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
arrange (Threshold,Median) %>%
tt () %>%
style_tt (i = c (1 , 5 , 9 , 13 ), j = 2 , rowspan = 4 , alignv = "t" )
```
### Figure
```{r}
results_params_4t %>%
pivot_wider (values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" ,"Item" )
) %>%
group_by (iteration,Item) %>%
summarise (
diff_eRm.T1 = T1_eRm - T1_Input,
diff_eRm.T2 = T2_eRm - T2_Input,
diff_eRm.T3 = T3_eRm - T3_Input,
diff_eRm.T4 = T4_eRm - T4_Input,
diff_TAM.T1 = T1_TAM - T1_Input,
diff_TAM.T2 = T2_TAM - T2_Input,
diff_TAM.T3 = T3_TAM - T3_Input,
diff_TAM.T4 = T4_TAM - T4_Input,
diff_mirt.T1 = T1_mirt - T1_Input,
diff_mirt.T2 = T2_mirt - T2_Input,
diff_mirt.T3 = T3_mirt - T3_Input,
diff_mirt.T4 = T4_mirt - T4_Input,
diff_PAIR.T1 = T1_PAIR - T1_Input,
diff_PAIR.T2 = T2_PAIR - T2_Input,
diff_PAIR.T3 = T3_PAIR - T3_Input,
diff_PAIR.T4 = T4_PAIR - T4_Input
) %>%
ungroup () %>%
pivot_longer (cols = starts_with ("diff" ),
names_to = c ("Estimator" ,"Threshold" ),
names_sep = " \\ ." ,
values_to = "diff" ) %>%
mutate (Package = gsub ("diff_" ,"" ,Estimator)) %>%
mutate (Threshold = car:: recode (Threshold,"'T1'='Threshold 1';'T2'='Threshold 2';'T3'='Threshold 3';'T4'='Threshold 4';" )) %>%
ggplot (aes (x = diff, y = Package, slab_fill = after_stat (level))) +
stat_dotsinterval (quantiles = 250 , point_interval = "median_qi" ,
layout = "weave" , slab_color = NA , .width = c (.66 ,.95 )) +
labs (caption = "Point interval: median_qi (.66 and .95). Based on 250 simulated datasets with 9 items and 720 respondents each." ,
x = "Bias (logit scale)" ,
y = "Package" ,
title = "Distribution of item threshold estimation bias" ,
subtitle = "Rasch Partial Credit Model" ) +
scale_color_manual (guide = "none" , values = scales:: brewer_pal ()(3 )[- 1 ], aesthetics = "slab_fill" ) +
facet_wrap (~ Threshold, nrow = 1 ) +
geom_vline (xintercept = 0 , linetype = "dashed" ) +
theme_minimal ()
```
## Smaller sample
Since the pairwise method has been claimed to be good with small samples, at least compared to MML [ @Finch_French_2019 ] . Notably, the paper mentions using the `ltm` package for MML estimation (which is referred to as "standard MLE" in the paper), for PCM estimation, but PCM is not available in `ltm` .
I was interested to have a quick look at this. We'll use the same 250 datasets again, but randomly select 108 respondents (3 per threshold estimated) from each dataset.
```{r}
registerDoParallel (cores = 8 )
iterations = 250
samplesize = 108 # n = 3 per threshold (instead of 20/threshold)
sim_results_4t2 <- list ()
sim_results_4t2 <- foreach (i = 1 : iterations) %dopar% {
input_params <- sim_data_4t[[i]]$ input_params
testData <- sim_data_4t[[i]]$ data[sample (1 : 720 , samplesize), ]
# check mean/centrality (item parameters should have been centered before data generation)
input_params_correction <- mean (input_params)
# eRm
erm_out <- PCM (testData)
erm_params <- thresholds (erm_out)[[3 ]][[1 ]][,- 1 ] %>%
as.data.frame () %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
erm_params_c <- erm_params - mean (erm_params) + input_params_correction
# TAM
tam_out <- tam (as.matrix (testData), irtmodel = "PCM" , verbose = FALSE )
tam_params <- tam_out$ item_irt %>%
as.data.frame () %>%
select (starts_with ("tau" )) %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
tam_params_c <- tam_params - mean (tam_params) + input_params_correction
# mirt
mirt_out <- mirt (data = testData, model = 1 , itemtype = "Rasch" , verbose = FALSE )
mirt_params <- coef (mirt_out, simplify = TRUE , IRTpars = TRUE )$ items %>%
as.data.frame () %>%
select (! a) %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
mirt_params_c <- mirt_params - mean (mirt_params) + input_params_correction
# PAIR
pair_out <- pair (testData, m = 5 )
pair_params <- deltapar (pair_out) %>%
as.data.frame ()
pair_params <- pair_params[,- 1 ] %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
pair_params_c <- pair_params - mean (pair_params) + input_params_correction
# combine item parameters and calculate lowest to highest threshold distances
input_params %>%
as.data.frame () %>%
rename (T1 = V1,
T2 = V2,
T3 = V3,
T4 = V4) %>%
add_column (Item = c (1 : 9 ),
Type = "Input" ) %>%
bind_rows (
erm_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "eRm" )
) %>%
bind_rows (
tam_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "TAM" )
) %>%
bind_rows (
mirt_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "mirt" )
) %>%
bind_rows (
pair_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "PAIR" )
) %>%
remove_rownames ()
}
```
### Results
```{r}
# collect item threshold parameters to a single dataframe
results_params_4t2 <- map_df (1 : iterations, ~ sim_results_4t2[[.x]] %>%
mutate (iteration = .x))
```
#### Summary table MAE
```{r}
# calculate absolute differences between input and estimated item thresholds per estimator, average by item
results_params_diff_4t2 <- results_params_4t2 %>%
pivot_wider (
values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" , "Item" )
) %>%
group_by (iteration, Item) %>%
summarise (
diff_eRm = sum (abs (T1_Input - T1_eRm) + abs (T2_Input - T2_eRm) + abs (T3_Input - T3_eRm) + abs (T4_Input - T4_eRm))/ 4 ,
diff_TAM = sum (abs (T1_Input - T1_TAM) + abs (T2_Input - T2_TAM) + abs (T3_Input - T3_TAM) + abs (T4_Input - T4_TAM))/ 4 ,
diff_mirt = sum (abs (T1_Input - T1_mirt) + abs (T2_Input - T2_mirt) + abs (T3_Input - T3_mirt) + abs (T4_Input - T4_mirt))/ 4 ,
diff_PAIR = sum (abs (T1_Input - T1_PAIR) + abs (T2_Input - T2_PAIR) + abs (T3_Input - T3_PAIR) + abs (T4_Input - T4_PAIR))/ 4
) %>%
ungroup () %>%
pivot_longer (
cols = c (diff_eRm, diff_TAM,diff_mirt,diff_PAIR),
names_to = c (NA , "Estimator" ),
names_sep = "_" ,
values_to = "diff"
)
# produce table with summary stats per estimator/package
results_params_diff_4t2 %>%
group_by (Estimator) %>%
summarise (
Median = median (diff, na.rm = TRUE ),
MAD = mad (diff, na.rm = TRUE ),
IQR = IQR (diff, na.rm = TRUE ),
Mean = mean (diff, na.rm = TRUE ),
SD = sd (diff, na.rm = TRUE ),
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
rename (Package = Estimator) %>%
arrange (Median) %>%
tt ()
```
#### Summary table RMSE
```{r}
# RMSE
results_params_4t2 %>%
pivot_wider (
values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" , "Item" )
) %>%
group_by (iteration, Item) %>% # sqrt(mean((data$actual - data$predicted)^2))
summarise (
rmse_eRm = sqrt (mean (((T1_Input - T1_eRm)^ 2 ) + ((T2_Input - T2_eRm)^ 2 ) + ((T3_Input - T3_eRm)^ 2 ) + ((T4_Input - T4_eRm))^ 2 )),
rmse_TAM = sqrt (mean (((T1_Input - T1_TAM)^ 2 ) + ((T2_Input - T2_TAM)^ 2 ) + ((T3_Input - T3_TAM)^ 2 ) + ((T4_Input - T4_TAM))^ 2 )),
rmse_mirt = sqrt (mean (((T1_Input - T1_mirt)^ 2 ) + ((T2_Input - T2_mirt)^ 2 ) + ((T3_Input - T3_mirt)^ 2 ) + ((T4_Input - T4_mirt))^ 2 )),
rmse_PAIR = sqrt (mean (((T1_Input - T1_PAIR)^ 2 ) + ((T2_Input - T2_PAIR)^ 2 ) + ((T3_Input - T3_PAIR)^ 2 ) + ((T4_Input - T4_PAIR))^ 2 ))
) %>%
ungroup () %>%
pivot_longer (
cols = c (rmse_eRm, rmse_TAM,rmse_mirt,rmse_PAIR),
names_to = c (NA , "Estimator" ),
names_sep = "_" ,
values_to = "rmse"
) %>%
group_by (Estimator) %>%
summarise (
Median = median (rmse, na.rm = T),
MAD = mad (rmse, na.rm = T),
IQR = IQR (rmse, na.rm = T),
Mean = mean (rmse, na.rm = T),
SD = sd (rmse, na.rm = T)
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
rename (Package = Estimator) %>%
arrange (Median) %>%
tt ()
```
#### Thresholds summary MAE
```{r}
# table summarizing all thresholds and estimators
results_params_4t2 %>%
pivot_wider (values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" ,"Item" )
) %>%
group_by (iteration,Item) %>%
summarise (
diff_eRm.T1 = abs (T1_eRm - T1_Input),
diff_eRm.T2 = abs (T2_eRm - T2_Input),
diff_eRm.T3 = abs (T3_eRm - T3_Input),
diff_eRm.T4 = abs (T4_eRm - T4_Input),
diff_TAM.T1 = abs (T1_TAM - T1_Input),
diff_TAM.T2 = abs (T2_TAM - T2_Input),
diff_TAM.T3 = abs (T3_TAM - T3_Input),
diff_TAM.T4 = abs (T4_TAM - T4_Input),
diff_mirt.T1 = abs (T1_mirt - T1_Input),
diff_mirt.T2 = abs (T2_mirt - T2_Input),
diff_mirt.T3 = abs (T3_mirt - T3_Input),
diff_mirt.T4 = abs (T4_mirt - T4_Input),
diff_PAIR.T1 = abs (T1_PAIR - T1_Input),
diff_PAIR.T2 = abs (T2_PAIR - T2_Input),
diff_PAIR.T3 = abs (T3_PAIR - T3_Input),
diff_PAIR.T4 = abs (T4_PAIR - T4_Input)
) %>%
ungroup () %>%
pivot_longer (cols = starts_with ("diff" ),
names_to = c ("Estimator" ,"Threshold" ),
names_sep = " \\ ." ,
values_to = "diff" ) %>%
mutate (Package = gsub ("diff_" ,"" ,Estimator)) %>%
group_by (Package, Threshold) %>%
summarise (
Median = median (diff, na.rm = TRUE ),
MAD = mad (diff, na.rm = TRUE ),
IQR = IQR (diff, na.rm = TRUE ),
Mean = mean (diff, na.rm = TRUE ),
SD = sd (diff, na.rm = TRUE )
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
arrange (Threshold,Median) %>%
tt () %>%
style_tt (i = c (1 , 5 , 9 , 13 ), j = 2 , rowspan = 4 , alignv = "t" )
```
#### Figure
```{r}
results_params_4t2 %>%
pivot_wider (values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" ,"Item" )
) %>%
group_by (iteration,Item) %>%
summarise (
diff_eRm.T1 = T1_eRm - T1_Input,
diff_eRm.T2 = T2_eRm - T2_Input,
diff_eRm.T3 = T3_eRm - T3_Input,
diff_eRm.T4 = T4_eRm - T4_Input,
diff_TAM.T1 = T1_TAM - T1_Input,
diff_TAM.T2 = T2_TAM - T2_Input,
diff_TAM.T3 = T3_TAM - T3_Input,
diff_TAM.T4 = T4_TAM - T4_Input,
diff_mirt.T1 = T1_mirt - T1_Input,
diff_mirt.T2 = T2_mirt - T2_Input,
diff_mirt.T3 = T3_mirt - T3_Input,
diff_mirt.T4 = T4_mirt - T4_Input,
diff_PAIR.T1 = T1_PAIR - T1_Input,
diff_PAIR.T2 = T2_PAIR - T2_Input,
diff_PAIR.T3 = T3_PAIR - T3_Input,
diff_PAIR.T4 = T4_PAIR - T4_Input
) %>%
ungroup () %>%
pivot_longer (cols = starts_with ("diff" ),
names_to = c ("Estimator" ,"Threshold" ),
names_sep = " \\ ." ,
values_to = "diff" ) %>%
mutate (Package = gsub ("diff_" ,"" ,Estimator)) %>%
mutate (Threshold = car:: recode (Threshold,"'T1'='Threshold 1';'T2'='Threshold 2';'T3'='Threshold 3';'T4'='Threshold 4';" )) %>%
ggplot (aes (x = diff, y = Package, slab_fill = after_stat (level))) +
stat_dotsinterval (quantiles = 250 , point_interval = "median_qi" ,
layout = "weave" , slab_color = NA , .width = c (.66 ,.95 )) +
labs (caption = str_wrap ("Point interval: median_qi (.66 and .95). Based on 250 simulated datasets with 9 items and 108 respondents each." ),
x = "Bias (logit scale)" ,
y = "Package" ,
title = "Distribution of item threshold estimation bias" ,
subtitle = "Rasch Partial Credit Model" ) +
scale_color_manual (guide = "none" , values = scales:: brewer_pal ()(3 )[- 1 ], aesthetics = "slab_fill" ) +
facet_wrap (~ Threshold, nrow = 1 ) +
geom_vline (xintercept = 0 , linetype = "dashed" ) +
theme_minimal ()
```
## Even smaller sample
n = 54, 2 per threshold estimated.
```{r}
registerDoParallel (cores = 8 )
iterations = 250
samplesize = 54 # n = 2 per threshold (instead of 20/threshold)
sim_results_4t3 <- list ()
sim_results_4t3 <- foreach (i = 1 : iterations) %dopar% {
input_params <- sim_data_4t[[i]]$ input_params
testData <- sim_data_4t[[i]]$ data[sample (1 : 720 , samplesize), ]
# check mean/centrality (item parameters should have been centered before data generation)
input_params_correction <- mean (input_params)
# eRm
erm_out <- PCM (testData)
erm_params <- thresholds (erm_out)[[3 ]][[1 ]][,- 1 ] %>%
as.data.frame () %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
erm_params_c <- erm_params - mean (erm_params) + input_params_correction
# TAM
tam_out <- tam (as.matrix (testData), irtmodel = "PCM" , verbose = FALSE )
tam_params <- tam_out$ item_irt %>%
as.data.frame () %>%
select (starts_with ("tau" )) %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
tam_params_c <- tam_params - mean (tam_params) + input_params_correction
# mirt
mirt_out <- mirt (data = testData, model = 1 , itemtype = "Rasch" , verbose = FALSE )
mirt_params <- coef (mirt_out, simplify = TRUE , IRTpars = TRUE )$ items %>%
as.data.frame () %>%
select (! a) %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
mirt_params_c <- mirt_params - mean (mirt_params) + input_params_correction
# PAIR
pair_out <- pair (testData, m = 5 )
pair_params <- deltapar (pair_out) %>%
as.data.frame ()
pair_params <- pair_params[,- 1 ] %>%
set_names (c ("T1" ,"T2" ,"T3" ,"T4" )) %>%
as.matrix ()
pair_params_c <- pair_params - mean (pair_params) + input_params_correction
# combine item parameters and calculate lowest to highest threshold distances
input_params %>%
as.data.frame () %>%
rename (T1 = V1,
T2 = V2,
T3 = V3,
T4 = V4) %>%
add_column (Item = c (1 : 9 ),
Type = "Input" ) %>%
bind_rows (
erm_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "eRm" )
) %>%
bind_rows (
tam_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "TAM" )
) %>%
bind_rows (
mirt_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "mirt" )
) %>%
bind_rows (
pair_params_c %>%
as.data.frame () %>%
add_column (Item = c (1 : 9 ),
Type = "PAIR" )
) %>%
remove_rownames ()
}
```
### Results
```{r}
# collect item threshold parameters to a single dataframe
results_params_4t3 <- map_df (1 : iterations, ~ sim_results_4t3[[.x]] %>%
mutate (iteration = .x))
```
#### Summary table MAE
```{r}
# calculate absolute differences between input and estimated item thresholds per estimator, average by item
results_params_diff_4t3 <- results_params_4t3 %>%
pivot_wider (
values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" , "Item" )
) %>%
group_by (iteration, Item) %>%
summarise (
diff_eRm = sum (abs (T1_Input - T1_eRm) + abs (T2_Input - T2_eRm) + abs (T3_Input - T3_eRm) + abs (T4_Input - T4_eRm))/ 4 ,
diff_TAM = sum (abs (T1_Input - T1_TAM) + abs (T2_Input - T2_TAM) + abs (T3_Input - T3_TAM) + abs (T4_Input - T4_TAM))/ 4 ,
diff_mirt = sum (abs (T1_Input - T1_mirt) + abs (T2_Input - T2_mirt) + abs (T3_Input - T3_mirt) + abs (T4_Input - T4_mirt))/ 4 ,
diff_PAIR = sum (abs (T1_Input - T1_PAIR) + abs (T2_Input - T2_PAIR) + abs (T3_Input - T3_PAIR) + abs (T4_Input - T4_PAIR))/ 4
) %>%
ungroup () %>%
pivot_longer (
cols = c (diff_eRm, diff_TAM,diff_mirt,diff_PAIR),
names_to = c (NA , "Estimator" ),
names_sep = "_" ,
values_to = "diff"
)
# produce table with summary stats per estimator/package
results_params_diff_4t3 %>%
group_by (Estimator) %>%
summarise (
Median = median (diff, na.rm = TRUE ),
MAD = mad (diff, na.rm = TRUE ),
IQR = IQR (diff, na.rm = TRUE ),
Mean = mean (diff, na.rm = TRUE ),
SD = sd (diff, na.rm = TRUE ),
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
rename (Package = Estimator) %>%
arrange (Median) %>%
tt ()
```
#### Summary table RMSE
```{r}
# RMSE
results_params_4t3 %>%
pivot_wider (
values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" , "Item" )
) %>%
group_by (iteration, Item) %>% # sqrt(mean((data$actual - data$predicted)^2))
summarise (
rmse_eRm = sqrt (mean (((T1_Input - T1_eRm)^ 2 ) + ((T2_Input - T2_eRm)^ 2 ) + ((T3_Input - T3_eRm)^ 2 ) + ((T4_Input - T4_eRm))^ 2 )),
rmse_TAM = sqrt (mean (((T1_Input - T1_TAM)^ 2 ) + ((T2_Input - T2_TAM)^ 2 ) + ((T3_Input - T3_TAM)^ 2 ) + ((T4_Input - T4_TAM))^ 2 )),
rmse_mirt = sqrt (mean (((T1_Input - T1_mirt)^ 2 ) + ((T2_Input - T2_mirt)^ 2 ) + ((T3_Input - T3_mirt)^ 2 ) + ((T4_Input - T4_mirt))^ 2 )),
rmse_PAIR = sqrt (mean (((T1_Input - T1_PAIR)^ 2 ) + ((T2_Input - T2_PAIR)^ 2 ) + ((T3_Input - T3_PAIR)^ 2 ) + ((T4_Input - T4_PAIR))^ 2 ))
) %>%
ungroup () %>%
pivot_longer (
cols = c (rmse_eRm, rmse_TAM,rmse_mirt,rmse_PAIR),
names_to = c (NA , "Estimator" ),
names_sep = "_" ,
values_to = "rmse"
) %>%
group_by (Estimator) %>%
summarise (
Median = median (rmse, na.rm = T),
MAD = mad (rmse, na.rm = T),
IQR = IQR (rmse, na.rm = T),
Mean = mean (rmse, na.rm = T),
SD = sd (rmse, na.rm = T)
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
rename (Package = Estimator) %>%
arrange (Median) %>%
tt ()
```
#### Thresholds summary MAE
```{r}
# table summarizing all thresholds and estimators
results_params_4t3 %>%
pivot_wider (values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" ,"Item" )
) %>%
group_by (iteration,Item) %>%
summarise (
diff_eRm.T1 = abs (T1_eRm - T1_Input),
diff_eRm.T2 = abs (T2_eRm - T2_Input),
diff_eRm.T3 = abs (T3_eRm - T3_Input),
diff_eRm.T4 = abs (T4_eRm - T4_Input),
diff_TAM.T1 = abs (T1_TAM - T1_Input),
diff_TAM.T2 = abs (T2_TAM - T2_Input),
diff_TAM.T3 = abs (T3_TAM - T3_Input),
diff_TAM.T4 = abs (T4_TAM - T4_Input),
diff_mirt.T1 = abs (T1_mirt - T1_Input),
diff_mirt.T2 = abs (T2_mirt - T2_Input),
diff_mirt.T3 = abs (T3_mirt - T3_Input),
diff_mirt.T4 = abs (T4_mirt - T4_Input),
diff_PAIR.T1 = abs (T1_PAIR - T1_Input),
diff_PAIR.T2 = abs (T2_PAIR - T2_Input),
diff_PAIR.T3 = abs (T3_PAIR - T3_Input),
diff_PAIR.T4 = abs (T4_PAIR - T4_Input)
) %>%
ungroup () %>%
pivot_longer (cols = starts_with ("diff" ),
names_to = c ("Estimator" ,"Threshold" ),
names_sep = " \\ ." ,
values_to = "diff" ) %>%
mutate (Package = gsub ("diff_" ,"" ,Estimator)) %>%
group_by (Package, Threshold) %>%
summarise (
Median = median (diff, na.rm = TRUE ),
MAD = mad (diff, na.rm = TRUE ),
IQR = IQR (diff, na.rm = TRUE ),
Mean = mean (diff, na.rm = TRUE ),
SD = sd (diff, na.rm = TRUE )
) %>%
mutate (across (where (is.numeric), ~ round (.x,3 ))) %>%
arrange (Threshold,Median) %>%
tt () %>%
style_tt (i = c (1 , 5 , 9 , 13 ), j = 2 , rowspan = 4 , alignv = "t" )
```
#### Figure
```{r}
results_params_4t3 %>%
pivot_wider (values_from = c ("T1" , "T2" , "T3" , "T4" ),
names_from = "Type" ,
id_cols = c ("iteration" ,"Item" )
) %>%
group_by (iteration,Item) %>%
summarise (
diff_eRm.T1 = T1_eRm - T1_Input,
diff_eRm.T2 = T2_eRm - T2_Input,
diff_eRm.T3 = T3_eRm - T3_Input,
diff_eRm.T4 = T4_eRm - T4_Input,
diff_TAM.T1 = T1_TAM - T1_Input,
diff_TAM.T2 = T2_TAM - T2_Input,
diff_TAM.T3 = T3_TAM - T3_Input,
diff_TAM.T4 = T4_TAM - T4_Input,
diff_mirt.T1 = T1_mirt - T1_Input,
diff_mirt.T2 = T2_mirt - T2_Input,
diff_mirt.T3 = T3_mirt - T3_Input,
diff_mirt.T4 = T4_mirt - T4_Input,
diff_PAIR.T1 = T1_PAIR - T1_Input,
diff_PAIR.T2 = T2_PAIR - T2_Input,
diff_PAIR.T3 = T3_PAIR - T3_Input,
diff_PAIR.T4 = T4_PAIR - T4_Input
) %>%
ungroup () %>%
pivot_longer (cols = starts_with ("diff" ),
names_to = c ("Estimator" ,"Threshold" ),
names_sep = " \\ ." ,
values_to = "diff" ) %>%
mutate (Package = gsub ("diff_" ,"" ,Estimator)) %>%
mutate (Threshold = car:: recode (Threshold,"'T1'='Threshold 1';'T2'='Threshold 2';'T3'='Threshold 3';'T4'='Threshold 4';" )) %>%
ggplot (aes (x = diff, y = Package, slab_fill = after_stat (level))) +
stat_dotsinterval (quantiles = 250 , point_interval = "median_qi" ,
layout = "weave" , slab_color = NA , .width = c (.66 ,.95 )) +
labs (caption = str_wrap ("Point interval: median_qi (.66 and .95). Based on 250 simulated datasets with 9 items and 54 respondents each. X-axis omits extreme values." ),
x = "Bias (logit scale)" ,
y = "Package" ,
title = "Distribution of item threshold estimation bias" ,
subtitle = "Rasch Partial Credit Model" ) +
scale_color_manual (guide = "none" , values = scales:: brewer_pal ()(3 )[- 1 ], aesthetics = "slab_fill" ) +
facet_wrap (~ Threshold, nrow = 1 ) +
geom_vline (xintercept = 0 , linetype = "dashed" ) +
theme_minimal () +
coord_cartesian (xlim = c (- 2.5 ,2.5 ))
```
```{r}
sessionInfo ()
```
## References