Nothing
# LTA -----
# ==== Reactive Data LTA ====
server_lta <- function(input, output, session) {
library(mirt)
library(plotly)
library(psych)
library(shinycssloaders)
data_user <- reactive({
req(input$dimension, input$data_source_lta)
if (input$dimension == "uni" && input$data_source_lta == "diko") {
a <- matrix(runif(15, 0.8, 2), ncol = 1) # discrimination 1 dimension
d <- matrix(rnorm(15, 0, 1), ncol = 1) # difficulty
theta <- matrix(rnorm(750), ncol = 1) # ability 1 dimension
resp <- simdata(a = a, d = d, itemtype = "2PL", Theta = theta)
df <- as.data.frame(resp) %>% tidyr::drop_na()
df$Group <- sample(c("Group_A", "Group_B"), nrow(df), replace = TRUE)
} else if (input$dimension == "uni" && input$data_source_lta == "poli") {
a <- matrix(rlnorm(20, .2, .3))
diffs <- t(apply(matrix(runif(20*4, .3, 1), 20), 1, cumsum))
diffs <- -(diffs - rowMeans(diffs))
d <- diffs + rnorm(20)
resp <- simdata(a, d, 300, itemtype = 'graded')
df <- as.data.frame(resp) %>% tidyr::drop_na()
df$Group <- sample(c("Group_A", "Group_B"), nrow(df), replace = TRUE)
} else if (input$dimension == "multi" && input$data_source_lta == "diko") {
N <- 750
n_items <- 15
n_factors <- 2
a <- matrix(0, nrow = n_items, ncol = n_factors)
a[1:8,1] <- runif(8, 0.5, 1.5) # loading ke F1
a[9:15,2] <- runif(7, 0.5, 1.5) # loading ke F2
d <- matrix(rnorm(n_items, 0, 1), ncol = 1)
sigma <- matrix(c(1,0.3,0.3,1), nrow = 2)
resp <- simdata(a = a, d = d, N = N, itemtype = '2PL', sigma = sigma)
df <- as.data.frame(resp) %>% tidyr::drop_na()
df$Group <- sample(c("Group_A", "Group_B"), nrow(df), replace = TRUE)
} else if (input$dimension == "multi" && input$data_source_lta == "poli") {
df <- as.data.frame(psych::bfi) %>% dplyr::select(A1:E2, gender) %>% tidyr::drop_na()
df$gender <- factor(df$gender, levels = c(1, 2), labels = c("Male", "Female"))
colnames(df)[ncol(df)] <- "Gender"
} else {
req(input$datafile_lta)
ext <- tolower(tools::file_ext(input$datafile_lta$name))
showModal(modalDialog(title = NULL, "Reading Your File, Please wait...", footer = NULL, easyClose = FALSE))
df <- switch(
ext,
"csv" = data.table::fread(input$datafile_lta$datapath,data.table = FALSE),
"xls" = readxl::read_excel(input$datafile_lta$datapath),
"xlsx" = readxl::read_excel(input$datafile_lta$datapath),
"sav" = haven::read_sav(input$datafile_lta$datapath),
"rds" = readRDS(input$datafile_lta$datapath),
stop("Unsupported file type. Please upload CSV, Excel, SPSS (.sav), or RDS file.")
)
removeModal()
df <- df %>% mutate(across(everything(), ~ifelse(.x == "", NA, .x)),
id_auto = paste0("id_", sprintf("%04d", 1:n())))
}
return(df)
})
output$id_select_ui <- renderUI({
req(data_user(), input$dimension, input$data_source_lta)
selectizeInput("id_cols", "ID columns (optional)", choices = names(data_user()), multiple = TRUE,
selected = names(data_user())[grepl("id", names(data_user()), ignore.case = TRUE)])
})
output$var_select_ui <- renderUI({
req(data_user(), input$dimension, input$data_source_lta)
all_vars <- names(data_user())
id_cols <- input$id_cols
choices <- setdiff(all_vars, id_cols)
selectInput("selected_vars", "Select Variables (used for LTA):", choices = choices, multiple = TRUE, selected = choices)
})
data_lta <- reactive({
req(input$dimension, input$data_source_lta, data_user(), input$selected_vars, input$fit_stats)
df <- data_user() %>% dplyr::select(all_of(input$selected_vars))
# Exclude non-numeric columns for IRT model fitting
df %>% dplyr::select(where(is.numeric))
})
# ==== Preview Data ====
output$data_preview_lta <- DT::renderDT({
req(input$dimension, input$data_source_lta, data_user(), input$selected_vars, input$fit_stats,data_lta())
df <- data_lta()
numeric_cols <- which(sapply(df, is.numeric))
DT::datatable(df, extensions = 'Buttons',
options = list(dom='Brtp',scrollX = TRUE, pageLength = 25,
buttons = list(
list(extend = 'csv',
text = 'Export CSV',
filename = 'Data LTA'
),
list(extend = 'excel',
text = 'Export Excel',
filename = 'Data LTA'
))),
rownames = TRUE) %>%
formatRound(columns = numeric_cols, digits = 0)
}, server = FALSE)
output$itemtype_ui <- renderUI({
req(input$datatype, input$data_source_lta)
source_type <- if(input$data_source_lta == "upload") {
input$datatype
} else {
input$data_source_lta
}
choices <- switch(source_type,
"diko" = c("Rasch" = "Rasch",
"2-Parameter Logistic (2-PL)" = "2PL",
"3-Parameter Logistic (3-PL)" = "3PL"),
"poli" = c("Partial Credit Model (PCM)" = "Rasch",
"Graded Response Model (GRM)" = "graded",
"Generalized PCM (GPCM)" = "gpcm")
)
div(
class = "select-large", # styling selector
selectInput(
inputId = "itemtype",
label = NULL,
choices = choices,
selected = choices[[1]],
width = "100%"
)
)
})
# ==== Reset all stored LTA results when data_user changes ====
observeEvent(data_user(), {
# Reset stored models and current selection
lta_fit_list(list())
lta_fit_compare(NULL)
lta_current_id(NULL)
})
# ===== LTA / MIRT MODEL =====
output$lta_model_ui <- renderUI({
req(input$data_source_lta, input$dimension=="multi")
model_text <- switch(input$data_source_lta,
# === 1. Multidimensional - Dichotomous ===
"diko" = "
# Example 1: Multidimensional - Dichotomous (2 dimensions)
model <- '
F1 = 1-5
F2 = 6-10
'
",
# === 2. Multidimensional - Polytomous ===
"poli" = "
# Example 2: Multidimensional - Polytomous (2 dimensions)
model <- '
F1 = 1-5
F2 = 6-10
'
"
)
tags$textarea(
id = "lta_model_text",
rows = 10,
style = "width:100%; font-family: monospace; font-size:11px;",
model_text
)
})
# ==== LTA storage ====
lta_fit_list <- reactiveVal(list())
lta_fit_compare <- reactiveVal(NULL)
lta_current_id <- reactiveVal(NULL)
# ==== Fungsi untuk menjalankan model ====
run_mirt_model <- function(data, model_def, itemtype) {
tryCatch({
mirt(data, model = model_def, itemtype = itemtype, verbose = FALSE)
}, error = function(e) {
showNotification(paste("Model", itemtype, "error:", e$message), type = "error")
return(NULL)
})
}
# ==== LTA Main Run ====
observeEvent(input$run_lta, {
req(data_lta(), input$selected_vars, input$fit_stats)
# Tambahkan kondisi khusus untuk upload
if (input$data_source_lta == "upload") {
req(input$datatype)
}
updateTabsetPanel(session, "main_tab_lta", selected = "summary_tab")
df <- data_lta()
# Model
source_type <- if(input$data_source_lta == "upload") {
req(input$datatype) # pastikan user sudah pilih
input$datatype
} else {
input$data_source_lta
}
itemtypes <- switch(source_type,
"diko" = c("Rasch","2PL","3PL"),
"poli" = c("Rasch","graded","gpcm"))
# Define Model Syntax
model_def <- if (input$dimension == "multi") {
mirt.model(input$lta_model_text,itemnames = df)
} else {1}
showModal(modalDialog("Running Latent Trait Analysis ...", footer = NULL))
fit_results <- list()
for (item in itemtypes) {
mod <- run_mirt_model(df, model_def, item)
if (is.null(mod)) next
measures <- tryCatch({
as.data.frame(anova(mod))
}, error = function(e) data.frame())
N_itemfit <- tryCatch({
itemfit_df <- as.data.frame(mirt::itemfit(mod, fit_stats=input$fit_stats))
p_cols <- grep("^p\\.", names(itemfit_df), value = TRUE)
sum(itemfit_df[[p_cols]] >= 0.05, na.rm = TRUE)
}, error = function(e) 0)
LD <- tryCatch({
Q3_mat <- residuals(mod, type = "Q3", verbose = FALSE)
# ambil item names
items <- colnames(Q3_mat)
# ekstrak upper triangle
idx <- which(upper.tri(Q3_mat), arr.ind = TRUE)
q3_long <- data.frame(
item = items[idx[,2]],
item2 = items[idx[,1]],
Q3 = Q3_mat[upper.tri(Q3_mat)]
)
violations <- q3_long %>%
dplyr::filter(abs(Q3) > 0.2)
unique_items <- unique(c(violations$item, violations$item2))
N_LD <- length(unique_items)
list(N_LocalDependency = N_LD,
item_LocalDep = unique_items
)
}, error = function(e) {
list(N_violations = NULL, itemLD = NA)}
)
fit_results[[item]] <- list(
id = item,
model_type = item,
fit = mod,
measures = measures,
N_itemfit = N_itemfit,
LD = LD,
time = Sys.time()
)
}
# ---- Model comparison (if possible) ----
models_for_anova <- lapply(fit_results, function(x) x$fit)
models_for_anova <- models_for_anova[!sapply(models_for_anova, is.null)]
lta_fit_compare(
switch(source_type,
"diko" = as.data.frame(anova(models_for_anova[['Rasch']],
models_for_anova[['2PL']],
models_for_anova[['3PL']])),
"poli" = rbind(
as.data.frame(anova(models_for_anova[['Rasch']],
models_for_anova[['graded']]
)),
as.data.frame(anova(models_for_anova[['Rasch']],
models_for_anova[['gpcm']]))[2,]
)
))
removeModal()
cur <- lta_fit_list()
cur <- modifyList(cur, fit_results)
best_name <- input$itemtype
all_model_types <- sapply(cur, function(x) x$model_type)
match_idx <- which(tolower(all_model_types) == tolower(best_name))
if (length(match_idx) == 0) {
best_id <- names(cur)[1]
} else {
best_id <- names(cur)[match_idx[1]]
}
lta_fit_list(cur)
lta_current_id(best_id)
showNotification(paste0("LTA finished. Selected model: ", best_name), type = "message")
})
output$fit_comparison <- renderUI({
req(data_lta(), input$selected_vars, input$fit_stats, lta_fit_list())
source_type <- if(input$data_source_lta == "upload") {
input$datatype
} else {
input$data_source_lta
}
all_measures <- do.call(rbind, lapply(lta_fit_list(), function(x) x$measures)) %>%
tibble::rownames_to_column("Model") %>%
dplyr::mutate(
Model = dplyr::case_when(
source_type == 'poli' & Model == 'Rasch' ~ 'PCM',
source_type == 'poli' & Model == 'graded' ~ 'GRM',
source_type == 'poli' & Model == 'gpcm' ~ 'GPCM',
TRUE ~ Model
)
) %>%
dplyr::select(Model)
N_itemfit <- do.call(rbind, lapply(lta_fit_list(), function(x) x$N_itemfit))
N_itemLD <- do.call(rbind, lapply(lta_fit_list(), function(x) x$LD$N_LocalDependency))
item_LocalDep <- do.call(rbind, lapply(lta_fit_list(), function(x) x$LD$item_LocalDep))
comparison <- cbind(all_measures,lta_fit_compare(),
N_itemfit, N_itemLD) %>% dplyr::rename(`p (χ²)`=p, 'χ²'=X2)
comparison <- comparison %>%
dplyr::mutate(
AIC = round(AIC, 2),
SABIC = round(SABIC, 2),
BIC = round(BIC, 2),
HQ = round(HQ, 2),
logLik = round(logLik,2),
`χ²` = round(`χ²`,2),
df=df,
`p (χ²)` = case_when(
is.na(p (`p (χ²)`)) ~ NA_character_,
`p (χ²)` < 0.01 ~ "p <.01",
`p (χ²)` < 0.05 ~ "p <.05",
TRUE ~ "p >.05"
),
N_itemfit = N_itemfit,
N_itemLD = N_itemLD
)
min_AIC <- min(comparison$AIC, na.rm = TRUE)
min_SABIC <- min(comparison$SABIC, na.rm = TRUE)
min_BIC <- min(comparison$BIC, na.rm = TRUE)
min_HQ <- min(comparison$HQ, na.rm = TRUE)
max_Nfit <- max(comparison$N_itemfit , na.rm = TRUE)
minLD <- min(comparison$N_itemLD, na.rm = TRUE)
column(12,
DT::datatable(comparison,extensions = 'Buttons',
options = list(scrollX = TRUE, pageLength = 10,
dom = 'B',
buttons = list(
list(
extend = 'csv',
text = 'Export CSV',
filename = 'Fit Comparison' # <-- nama file CSV
),
list(
extend = 'excel',
text = 'Export Excel',
filename = 'Fit Comparison' # <-- nama file Excel
))),
rownames = FALSE) %>%
# === Highlight ===
formatStyle(
columns = 'AIC',
color = styleEqual(min_AIC, 'black'),
backgroundColor = styleEqual(min_AIC, 'lightgreen'),
fontWeight = styleEqual(min_AIC, 'bold')
) %>%
formatStyle(
columns = 'BIC',
color = styleEqual(min_BIC, 'black'),
backgroundColor = styleEqual(min_BIC, 'lightgreen'),
fontWeight = styleEqual(min_BIC, 'bold')
) %>%
formatStyle(
columns = 'SABIC',
color = styleEqual(min_SABIC, 'black'),
backgroundColor = styleEqual(min_SABIC, 'lightgreen'),
fontWeight = styleEqual(min_SABIC, 'bold')
) %>%
formatStyle(
columns = 'HQ',
color = styleEqual(min_HQ, 'black'),
backgroundColor = styleEqual(min_HQ, 'lightgreen'),
fontWeight = styleEqual(min_HQ, 'bold')
) %>%
# === Highlight untuk kolom N_itemfit (nilai terbesar) ===
formatStyle(
columns = 'N_itemfit',
color = styleEqual(max_Nfit, 'black'),
backgroundColor = styleEqual(max_Nfit, 'lightgreen'),
fontWeight = styleEqual(max_Nfit, 'bold')
) %>%
formatStyle(
columns = 'N_itemLD',
color = styleEqual(minLD, 'black'),
backgroundColor = styleEqual(minLD, 'lightgreen'),
fontWeight = styleEqual(minLD, 'bold')
) %>%
formatStyle(
columns = 'p (χ²)',
backgroundColor = styleEqual(
c('p <.01', 'p <.05'),
c('lightgreen', 'lightgreen')
)
),
tags$div(
style = "margin-top: 0px; font-size: 12px; color: #6c757d;",
tags$b("Interpretation Guidelines:"),
tags$ul(
tags$li(tags$span(style = "color: green;", "Best Model:"),"Smaller AIC/BIC/SABIC/HQ/N_ItemLD; Higher N_itemfit"),
tags$li(tags$span(style = "color: red;", "ItemLD (Violation of Local Independency Assumption):"), "|Q3| > 0.2 (Yen, 1984)"),
tags$li(tags$span(style = "color: green;", "p.value <.05 / p.value <.01:"),
"The more complex model fits better than the simpler model (Desjardin & Bulut, 2018)")
),
tags$hr()
)
)
})
# === Helper: Get the currently selected model based on input$itemtype ===
selected_fit <- reactive({
req(lta_fit_list(), input$itemtype)
all_models <- lta_fit_list()
match_idx <- which(
tolower(sapply(all_models, function(x) x$model_type)) == tolower(input$itemtype)
)
if (length(match_idx) == 0) {
showNotification(paste0("Model '", input$itemtype, "' not found."), type = "warning")
return(NULL)
}
all_models[[match_idx[1]]]$fit
})
# === Item Summary (reactive to selected model) ===
output$item_summary <- renderUI({
req(selected_fit())
itemparam <- as.data.frame(mirt::coef(selected_fit(), IRTpars = TRUE, simplify = TRUE)$items)
itemfit <- as.data.frame(mirt::itemfit(selected_fit(), fit_stats = input$fit_stats)) %>%
dplyr::select (paste0(input$fit_stats), paste0('p.',input$fit_stats)) %>%
dplyr::rename(`χ²_itemfit` = paste0(input$fit_stats), `p(χ²)_itemfit` = paste0('p.',input$fit_stats))
item_summary <- cbind(itemparam,itemfit) %>% tibble::rownames_to_column("item")
numeric_cols <- which(sapply(item_summary, is.numeric))
DT::datatable(item_summary,extensions = 'Buttons',
options = list(scrollX = TRUE, dom = 'Brtp',
buttons = list(
list(
extend = 'csv',
text = 'Export CSV',
filename = paste0('Item Summary-',input$itemtype)
),
list(
extend = 'excel',
text = 'Export Excel',
filename = paste0('Item Summary-',input$itemtype)
))),
rownames = FALSE) %>%
formatRound(columns = numeric_cols, digits = 2) %>%
DT::formatStyle(columns = "p(χ²)_itemfit",
fontWeight = 'bold',
color = styleInterval(0.04999999, c('red', 'green'))
)
})
output$icc_info <- renderUI({
req(input$dimension, selected_fit() )
nfactors <- selected_fit()@Model$nfact
if (input$dimension =='uni')
{
column(12,
column(6,
plotOutput('icc'),
uiOutput('select_icc'),
),
column(6, plotOutput('infose')))
} else if (input$dimension =='multi' && nfactors == 2 ) {
column(12,
plotOutput('icc'),
uiOutput('select_icc'),
uiOutput("info_ui_multi")
)
}
else if (input$dimension =='multi' && nfactors >= 3 )
{
column(12,
uiOutput("info_ui_multi")
)
}
else{return(NULL)}
})
output$select_icc <- renderUI({
req(data_lta())
choices <- c('SELECT ALL',names(data_lta()))
div(class = "select-mini",
selectInput("item_select_icc", NULL, choices = choices, multiple = TRUE, selected = 'SELECT ALL', width = '100%'))
})
observeEvent(input$item_select_icc, {
req(input$item_select_icc)
selected <- input$item_select_icc
# Jika "All_Item" dipilih bersama dengan yang lain, sisakan hanya "All_Item"
if ("SELECT ALL" %in% selected && length(selected) > 1) {
# Jika "All_Item" baru saja diklik, hapus item lain
if (tail(selected, 1) == "SELECT ALL") {
updateSelectInput(session, "item_select_icc", selected = "SELECT ALL")
} else {
# Jika item lain baru diklik saat All_Item aktif, hapus All_Item
updateSelectInput(session, "item_select_icc", selected = setdiff(selected, "SELECT ALL"))
}
}
})
# === ICC Plot (reactive to itemtype) ===
output$icc <- renderPlot({
req(selected_fit())
req(input$item_select_icc)
all_names <- names(data_lta()) # urutan nama sesuai choices
if ("SELECT ALL" %in% input$item_select_icc) {
which_idx <- seq_along(all_names)
} else {
# ambil indeks dari nama yang dipilih (preserve urutan as in all_names)
which_idx <- which(all_names %in% input$item_select_icc)
# optional: jika pengguna memilih nama yang tidak ada (safety), bail out
if (length(which_idx) == 0) return()
}
# plot mirt: gunakan which.items untuk menyebutkan indeks item
mirt::plot(selected_fit(), type = "trace", which.items = which_idx,
main = paste("ICC -", input$itemtype))
})
# === IIF Plot (reactive to itemtype, using custom function) ===
output$infose <- renderPlot({
req(selected_fit(), input$dimension == "uni")
plot_info_se(selected_fit())
})
observeEvent(input$dimension, {
output$info_ui_multi <- renderUI({
if (input$dimension == "multi") {
infoVisUI("info1")
} else {
NULL
}
})
})
# Jalankan modul server hanya jika dimensi = multi
observeEvent(input$dimension, {
req(input$dimension == "multi")
infoVisServer("info1", model_reactive = reactive({ selected_fit() }))
})
# ==== Factor Score ====
fscore_data <- reactive({
req(selected_fit())
tryCatch({
as.data.frame(fscores(selected_fit(), method = "EAP", full.scores = TRUE))
}, error = function(e) data.frame())
})
output$fscoreLTA <- renderUI({
df <- fscore_data()
req(nrow(df) > 0)
numeric_cols <- which(sapply(df, is.numeric))
tagList(
column(
6,
DTOutput("fscore_table")
),
column(
6,
br(),
numericInput("n_kat", "Number of Factor Score Category:", value = 3, min = 2, max = 10),
uiOutput("cutoff_inputs"),
lapply(numeric_cols, function(i) {
plotname <- paste0("fscore_donut_", i)
div(
style = "margin-bottom: 20px;", # jarak antar-plot
plotOutput(plotname, height = 280)
)
})
)
)
})
output$fscore_table <- DT::renderDT({
df <- fscore_data()
req(nrow(df) > 0)
numeric_cols <- which(sapply(df, is.numeric))
DT::datatable(
df,
extensions = "Buttons",
options = list(
scrollX = TRUE,
pageLength = nrow(df),
dom = "Brtp",
buttons = list(
list(
extend = "csv",
text = "Export CSV",
filename = paste0("Factor Scores-", input$itemtype),
exportOptions = list(modifier = list(page = "all"))
),
list(
extend = "excel",
text = "Export Excel",
filename = paste0("Factor Scores-", input$itemtype),
exportOptions = list(modifier = list(page = "all"))
)
)
),
rownames = TRUE
) %>%
formatRound(columns = numeric_cols, digits = 2) %>%
DT::formatStyle(
columns = numeric_cols,
fontWeight = "bold" )
}, server = FALSE)
output$cutoff_inputs <- renderUI({
req(input$n_kat)
n_cut <- input$n_kat - 1
tagList(
lapply(1:n_cut, function(i) {
div(
style = "display: inline-block; margin-right: 10px;",
numericInput(
paste0("cut_", i),
label = paste("Cut-off", i),
value = NA,
step = 0.01,
width = "70px" # small width
)
)
})
)
})
observe({
df <- fscore_data()
req(nrow(df) > 0)
numeric_cols <- which(sapply(df, is.numeric))
n_kat <- input$n_kat # default 3 kategori
lapply(seq_along(numeric_cols), function(idx) {
local({
col_index <- numeric_cols[idx]
plotname <- paste0("fscore_donut_", col_index)
output[[plotname]] <- renderPlot({
scores <- df[[col_index]]
n_kat <- input$n_kat
n_cut <- n_kat - 1
# Ambil cut-off manual
cutoffs <- numeric(0)
for (i in 1:n_cut) {
val <- input[[paste0("cut_", i)]]
if (!is.null(val) && !is.na(val)) cutoffs <- c(cutoffs, val)
}
# Jika cut-off belum lengkap → fallback quantile
if (length(cutoffs) != n_cut) {
cuts <- quantile(scores, probs = seq(0, 1, length.out = n_kat + 1))
} else {
cuts <- c(min(scores), sort(cutoffs), max(scores))
}
# Kategorisasi
kategori <- cut(scores, breaks = cuts, include.lowest = TRUE, labels = FALSE)
# Frekuensi
freq <- table(kategori)
df_plot <- data.frame(
kategori = factor(names(freq)),
freq = as.numeric(freq)
)
df_plot$percent <- df_plot$freq / sum(df_plot$freq) * 100
df_plot$label <- paste0(df_plot$freq, " (", round(df_plot$percent), "%)")
# ===== Legend label (θ ranges) =====
legend_labels <- c()
for (i in 1:n_kat) {
low <- cuts[i]
high <- cuts[i+1]
if (i == 1) {
legend_labels[i] <- paste0("θ ≤ ", round(high, 2))
} else if (i == n_kat) {
legend_labels[i] <- paste0("θ > ", round(low, 2))
} else {
legend_labels[i] <- paste0(round(low, 2), " < θ ≤ ", round(high, 2))
}
}
# Warna kategori
colors <- scales::hue_pal()(n_kat)
# ====== PLOT DONUT ======
ggplot(df_plot, aes(x = 2, y = freq, fill = kategori)) +
# Donut bar
geom_col(width = 1, color = "white") +
# ===== LABEL DI LUAR DONUT =====
geom_text(
aes(
x = 2.35, # posisi label lebih keluar dari donut
label = label
),
position = position_stack(vjust = 0.5),
size = 4,
fontface = "bold"
) +
coord_polar(theta = "y") +
scale_fill_manual(
values = colors,
labels = legend_labels,
name = "θ Range"
) +
# ===== LABEL DI TENGAH DONUT =====
annotate(
"text",
x = 0.5,
y = 0,
label = paste0("F", which(numeric_cols == col_index), "-Score"),
size = 6,
fontface = "bold"
) +
# Maintain the strcuture
xlim(0.5, 2.5) +
theme_void() +
ggtitle(paste0("Factor Score : ", colnames(df)[col_index])) +
theme(
plot.title = element_text(hjust = 0.5),
legend.position = "bottom"
)
})
})
})
})
# ====== INFORMATION & RELIABILITY TAB ======
output$info_rel_ui <- renderUI({
req(selected_fit())
mod <- selected_fit()
# Calculate Marginal Reliability
marg_rel <- tryCatch({
mirt::marginal_rxx(mod)
}, error = function(e) NA)
# Generate TIF table
theta_seq <- matrix(seq(-4, 4, by = 0.5))
tif_vals <- tryCatch({
mirt::testinfo(mod, theta_seq)
}, error = function(e) rep(NA, length(theta_seq)))
tif_df <- data.frame(
Theta = as.numeric(theta_seq),
Information = tif_vals
)
# Render
tagList(
h4(icon("check-circle"), "Marginal Reliability"),
tags$p("Marginal reliability estimate(s) for the factors in this model:"),
verbatimTextOutput("marg_rel_out"),
tags$hr(),
h4(icon("table"), "Test Information Function (TIF) Table"),
tags$p("Test information values across different levels of the latent trait (\u03B8):"),
DTOutput("tif_table")
)
})
output$marg_rel_out <- renderPrint({
req(selected_fit())
mod <- selected_fit()
mirt::marginal_rxx(mod)
})
output$tif_table <- renderDT({
req(selected_fit())
mod <- selected_fit()
theta_seq <- matrix(seq(-4, 4, by = 0.5))
tif_vals <- mirt::testinfo(mod, theta_seq)
df <- data.frame(
Theta = as.numeric(theta_seq),
Information = round(tif_vals, 3)
)
datatable(df, options = list(pageLength = 10, dom = 'rtip'))
})
# ====== DIF ANALYSIS TAB ======
output$dif_ui <- renderUI({
req(data_user())
# Find categorical variables for grouping
df <- data_user()
cat_vars <- names(df)[sapply(df, function(x) is.factor(x) || is.character(x) || length(unique(x)) <= 5)]
if (length(cat_vars) == 0) {
return(tags$p("No suitable grouping variable found in the dataset for DIF analysis. Please ensure your dataset includes a categorical column (e.g., Gender, Region)."))
}
tagList(
fluidRow(
column(4,
h4(icon("cogs"), "DIF Settings"),
selectInput("dif_group_var", "Select Grouping Variable:", choices = cat_vars),
selectInput("dif_anchor_items", "Anchor Items (optional):",
choices = c("None", input$lta_vars), selected = "None", multiple = TRUE),
actionButton("run_dif", "Run DIF Analysis", class = "btn-primary", icon = icon("play"))
),
column(8,
h4(icon("table"), "DIF Results (mirt)"),
withSpinner(DTOutput("dif_results_table"))
)
)
)
})
dif_results <- eventReactive(input$run_dif, {
req(selected_fit(), input$dif_group_var, data_user())
df <- data_user()
group_var <- df[[input$dif_group_var]]
if (length(unique(na.omit(group_var))) < 2) {
showNotification("Grouping variable must have at least 2 categories.", type = "error")
return(NULL)
}
# The selected_fit() model provides the variable names
mod <- selected_fit()
vars <- mirt::extract.mirt(mod, "itemnames")
mod_syntax <- NULL # If custom, wait we don't have model_syntax easily here. Let's just fit standard multiple group.
# We need to fit a multiple group model for DIF
# Use mirt::multipleGroup
item_data <- df[, vars, drop = FALSE]
# Simplified approach: If we can't easily refit, we might just use Wald test or likelihood ratio.
# To use mirt::DIF, we first fit a multipleGroup model where all items are constrained to be equal across groups.
showNotification("Fitting multiple group model... this may take a while.", id = "dif_msg", duration = NULL)
tryCatch({
# Base constrained model
if (is.null(mod_syntax) || mod_syntax == "") {
mg_model <- mirt::multipleGroup(item_data, 1, group = as.character(group_var), invariance = c('free_means', 'free_var', 'slopes', 'intercepts'))
} else {
model_def <- mirt::mirt.model(mod_syntax)
mg_model <- mirt::multipleGroup(item_data, model_def, group = as.character(group_var), invariance = c('free_means', 'free_var', 'slopes', 'intercepts'))
}
items2test <- vars
if (!is.null(input$dif_anchor_items) && !("None" %in% input$dif_anchor_items)) {
items2test <- setdiff(vars, input$dif_anchor_items)
}
dif_res <- mirt::DIF(mg_model, which.par = c('a1', 'd'), items2test = items2test, p.adjust = "fdr")
removeNotification("dif_msg")
return(dif_res)
}, error = function(e) {
removeNotification("dif_msg")
showNotification(paste("DIF Error:", e$message), type = "error")
return(NULL)
})
})
output$dif_results_table <- renderDT({
req(dif_results())
res <- dif_results()
if (is.null(res)) return(NULL)
df_res <- as.data.frame(res)
df_res$Item <- rownames(df_res)
df_res <- df_res[, c("Item", setdiff(names(df_res), "Item"))]
datatable(df_res, options = list(pageLength = 15, scrollX = TRUE)) %>%
formatRound(columns = 2:ncol(df_res), digits = 3) %>%
formatStyle(
'p',
color = styleInterval(0.05, c('red', 'black')),
fontWeight = styleInterval(0.05, c('bold', 'normal'))
)
})
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.