Skip to content

Commit 617067c

Browse files
committed
Add DIF for dichotomous items
1 parent 3cb3ecf commit 617067c

10 files changed

+158
-28
lines changed

R/itemresponsetheorycommon.R

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,3 +904,63 @@
904904
result <- list(model = model, data = data, pars = pars)
905905
return(result)
906906
}
907+
908+
.irtDifAnalysisTable <- function(dataset, options, jaspResults, ready, position) {
909+
if (options[["explanatoryText"]] && options[["tableDifAnalysis"]]) {
910+
text <- createJaspHtml(gettext("<h3>Explanatory Text: Differential Item Functioning (DIF)</h3> The table below presents the results of a Differential Item Functioning (DIF) analysis. A DIF-analysis is conducted to assess whether specific items perform differently across groups while controlling for overall ability or trait levels. This ensures that the items are fair and not biased toward any particular group."))
911+
text$position <- position
912+
text$dependOn(options = c("explanatoryText", "tableDifAnalysis"))
913+
jaspResults[["tableDifAnalysisText"]] <- text
914+
}
915+
if (!is.null(jaspResults[["tableDifAnalysis"]]) || !options[["tableDifAnalysis"]]) {
916+
return()
917+
}
918+
tb <- createJaspTable(title = gettext("Differential Item Functioning (DIF)"))
919+
tb$position <- position + 1
920+
tb$addColumnInfo(name = "item", title = gettext("Item"), type = "string")
921+
tb$addColumnInfo(name = "aic", title = gettext("AIC"), type = "number")
922+
tb$addColumnInfo(name = "sabic", title = gettext("SABIC"), type = "number")
923+
tb$addColumnInfo(name = "hq", title = gettext("HQ"), type = "number")
924+
tb$addColumnInfo(name = "bic", title = gettext("BIC"), type = "number")
925+
tb$addColumnInfo(name = "x2", title = gettext("X2"), type = "number")
926+
tb$addColumnInfo(name = "df", title = gettext("df"), type = "number")
927+
tb$addColumnInfo(name = "p", title = gettext("p"), type = "pvalue")
928+
tb$dependOn(options = c(.irtCommonDeps(type = "irt"), "tableDifAnalysis", "groupingVariable", "tableDifAnalysisDifficulty", "tableDifAnalysisDiscrimination", "tableDifAnalysisGuess", "tableDifAnalysisSlip"))
929+
tb$addFootnote(gettext("For each item, the null hypothesis specifies that there is no DIF between the groups."))
930+
tb$addFootnote(gettext("p-values are not adjusted for multiple comparisons."), colName = "p")
931+
if (length(options[["covariates"]]) > 0) {
932+
tb$addFootnote(gettext("The latent regressions present in the ungrouped model are not included in this analysis."))
933+
}
934+
jaspResults[["tableDifAnalysis"]] <- tb
935+
if (!ready || options[["groupingVariable"]] == "") {
936+
return()
937+
}
938+
parameters <- character()
939+
if (options[["tableDifAnalysisDifficulty"]]) {
940+
parameters <- c(parameters, "d")
941+
}
942+
if (options[["tableDifAnalysisDiscrimination"]] && options[["model"]] %in% c("2PL", "3PL", "4PL")) {
943+
parameters <- c(parameters, "a1")
944+
}
945+
if (options[["tableDifAnalysisGuess"]] && options[["model"]] %in% c("3PL", "4PL")) {
946+
parameters <- c(parameters, "d")
947+
}
948+
if (options[["tableDifAnalysisSlip"]] && options[["model"]] == "4PL") {
949+
parameters <- c(parameters, "u")
950+
}
951+
if (length(parameters) == 0) {
952+
tb$setError(gettext("DIf-analysis not possible: Select at least one parameter to test."))
953+
return()
954+
}
955+
state <- .irtIRTStateBayesian(dataset, options, jaspResults)
956+
fit <- mirt::multipleGroup(data = state[["items"]], model = 1, itemtype = options[["model"]], group = dataset[[options[["groupingVariable"]]]], SE = FALSE, verbose = FALSE, TOL = options[["emTolerance"]], technical = list(NCYCLES = options[["emIterations"]], set.seed = options[["seed"]]))
957+
dif <- mirt::DIF(fit, which.par = parameters)
958+
tb[["item"]] <- options[["items"]]
959+
tb[["aic"]] <- dif[["AIC"]]
960+
tb[["sabic"]] <- dif[["SABIC"]]
961+
tb[["hq"]] <- dif[["HQ"]]
962+
tb[["bic"]] <- dif[["BIC"]]
963+
tb[["x2"]] <- dif[["X2"]]
964+
tb[["df"]] <- dif[["df"]]
965+
tb[["p"]] <- dif[["p"]]
966+
}

R/itemresponstheorydichotomous.R

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,20 @@ itemResponseTheoryDichotomous <- function(jaspResults, dataset, options, ...) {
3030
# Create the item fit statistics table
3131
.irtIRTItemFitStatisticsTable(dataset, options, jaspResults, ready, position = 5)
3232

33+
# Create the DIF-analysis table
34+
.irtDifAnalysisTable(dataset, options, jaspResults, ready, position = 7)
35+
3336
# Create the histogram of latent ability
34-
.irtIRTHistogram(dataset, options, jaspResults, ready, position = 7)
37+
.irtIRTHistogram(dataset, options, jaspResults, ready, position = 9)
3538

3639
# Create the test information function
37-
.irtIRTTestInfoCurve(dataset, options, jaspResults, ready, position = 9)
40+
.irtIRTTestInfoCurve(dataset, options, jaspResults, ready, position = 11)
3841

3942
# Create the item information curves
40-
.irtIRTItemInfoCurve(dataset, options, jaspResults, ready, position = 11)
43+
.irtIRTItemInfoCurve(dataset, options, jaspResults, ready, position = 13)
4144

4245
# Create the item information curves
43-
.irtIRTItemCharCurve(dataset, options, jaspResults, ready, position = 13)
46+
.irtIRTItemCharCurve(dataset, options, jaspResults, ready, position = 15)
4447
}
4548

4649
.irtIRTState <- function(dataset, options, jaspResults) {
@@ -55,7 +58,6 @@ itemResponseTheoryDichotomous <- function(jaspResults, dataset, options, ...) {
5558
} else {
5659
covariates <- NULL
5760
}
58-
# Model fit (DIF analysis possible using multipleGroup model)
5961
fit <- mirt::mirt(data = items, model = 1, itemtype = options[["model"]], covdata = covariates, formula = ~., SE = FALSE, verbose = FALSE, TOL = options[["emTolerance"]], technical = list(NCYCLES = options[["emIterations"]], set.seed = options[["seed"]]))
6062
if (options[["model"]] == "grsm") {
6163
thetaRange <- seq(-10, 10, by = 0.1)

R/itemresponstheorypolytomous.R

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ itemResponseTheoryPolytomous <- function(jaspResults, dataset, options, ...) {
3030
# Create the item fit statistics table
3131
.irtIRTItemFitStatisticsTable(dataset, options, jaspResults, ready, position = 5)
3232

33+
# Create the DIF-analysis table
34+
.irtDifAnalysisTable(dataset, options, jaspResults, ready, position = 7)
35+
3336
# Create the histogram of latent ability
34-
.irtIRTHistogram(dataset, options, jaspResults, ready, position = 7)
37+
.irtIRTHistogram(dataset, options, jaspResults, ready, position = 9)
3538

3639
# Create the test information function
37-
.irtIRTTestInfoCurve(dataset, options, jaspResults, ready, position = 9)
40+
.irtIRTTestInfoCurve(dataset, options, jaspResults, ready, position = 11)
3841

3942
# Create the item information curves
40-
.irtIRTItemInfoCurve(dataset, options, jaspResults, ready, position = 11)
43+
.irtIRTItemInfoCurve(dataset, options, jaspResults, ready, position = 13)
4144

4245
# Create the item information curves
43-
.irtIRTItemCharCurve(dataset, options, jaspResults, ready, position = 13)
46+
.irtIRTItemCharCurve(dataset, options, jaspResults, ready, position = 15)
4447
}

inst/qml/common/IrtOutput.qml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Column
2525
{
2626
property bool bayesian: false
2727
property bool dichotomous: true
28+
property string modeltype: "Rasch"
2829

2930
spacing: 20 * preferencesModel.uiScale
3031

@@ -73,6 +74,59 @@ Column
7374
visible: bayesian
7475
info: qsTr("Generate a table of all parameters that are estimated.")
7576
}
77+
78+
CheckBox
79+
{
80+
name: "tableDifAnalysis"
81+
text: qsTr("Differential Item Functioning (DIF)")
82+
visible: !bayesian && dichotomous
83+
info: qsTr("Generate a table showing the output of a likelihood-ratio test for Differential Item Functioning (DIF).")
84+
85+
DropDown
86+
{
87+
name: "groupingVariable"
88+
label: qsTr("Grouping variable")
89+
showVariableTypeIcon: true
90+
addEmptyValue: true
91+
source: [ { model: columnsModel, use: "type=nominal"} ]
92+
}
93+
94+
Group
95+
{
96+
title: qsTr("Parameters")
97+
98+
CheckBox
99+
{
100+
name: "tableDifAnalysisDiscrimination"
101+
text: qsTr("Discrimination")
102+
enabled: dichotomous ? (modeltype == "2PL" || modeltype == "3PL" || modeltype == "4PL") : false // TODO
103+
checked: true
104+
}
105+
106+
CheckBox
107+
{
108+
name: "tableDifAnalysisDifficulty"
109+
text: qsTr("Difficulty")
110+
checked: true
111+
}
112+
113+
CheckBox
114+
{
115+
name: "tableDifAnalysisGuess"
116+
text: qsTr("Guessing")
117+
enabled: dichotomous ? (modeltype == "3PL" || modeltype == "4PL") : false // TODO
118+
checked: true
119+
}
120+
121+
CheckBox
122+
{
123+
name: "tableDifAnalysisSlip"
124+
text: qsTr("Slip")
125+
enabled: dichotomous ? modeltype == "4PL" : false // TODO
126+
checked: true
127+
}
128+
}
129+
}
76130
}
77131

78132
Group

inst/qml/common/bayesian/AdvancedOptions.qml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,7 @@ Section
810810
min: 0.001
811811
enabled: modeltype != "Rasch" && modeltype != "rsm"
812812
info: qsTr("Specify the scaling constant for the model. Default is 1.702 (Camilli, 1994).")
813+
fieldWidth: 60 * preferencesModel.uiScale
813814
}
814815
}
815816

@@ -825,6 +826,7 @@ Section
825826
defaultValue: 2000
826827
min: 500
827828
info: qsTr("Specify the number of samples to be collected by each chain in the MCMC process after the burn-in phase. A larger number improves the precision of parameter estimates.")
829+
fieldWidth: 60 * preferencesModel.uiScale
828830
}
829831

830832
IntegerField
@@ -835,6 +837,7 @@ Section
835837
min: 200
836838
max: samples.value - 300
837839
info: qsTr("Specify the number of warmup iterations in each chain of the MCMC process. These iterations are discarded to ensure that the chain reaches the target distribution.")
840+
fieldWidth: 60 * preferencesModel.uiScale
838841
}
839842

840843
IntegerField
@@ -844,6 +847,7 @@ Section
844847
defaultValue: 4
845848
min: 1
846849
info: qsTr("Specify the number of independent MCMC chains to run in parallel. Running multiple chains helps assess convergence and improves results.")
850+
fieldWidth: 60 * preferencesModel.uiScale
847851
}
848852
}
849853

@@ -859,6 +863,7 @@ Section
859863
min: -999999
860864
max: 999999
861865
info: qsTr("Specify the random seed for reproducibility.")
866+
fieldWidth: 60 * preferencesModel.uiScale
862867
}
863868

864869
DoubleField
@@ -870,6 +875,7 @@ Section
870875
max: 0.999
871876
decimals: 3
872877
info: qsTr("Specify is the target average proposal acceptance probability during Stan's adaptation period.")
878+
fieldWidth: 60 * preferencesModel.uiScale
873879
}
874880

875881
IntegerField
@@ -879,6 +885,7 @@ Section
879885
defaultValue: 10
880886
min: 5
881887
info: qsTr("Specify the max value, in exponents of 2, of what the binary tree size in the NUTS algorithm should have.")
888+
fieldWidth: 60 * preferencesModel.uiScale
882889
}
883890
}
884891
}

inst/qml/common/classical/AdvancedOptions.qml

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ Section
2727

2828
DoubleField
2929
{
30-
name: "priorScaling"
31-
text: qsTr("Scaling constant")
32-
defaultValue: 1.702
33-
decimals: 3
34-
min: 0.001
35-
enabled: false
36-
info: qsTr("Specify the scaling constant for the model. Default is 1.702 (Camilli, 1994).")
30+
name: "priorScaling"
31+
text: qsTr("Scaling constant")
32+
defaultValue: 1.702
33+
decimals: 3
34+
min: 0.001
35+
enabled: false
36+
info: qsTr("Specify the scaling constant for the model. Default is 1.702 (Camilli, 1994).")
37+
fieldWidth: 60 * preferencesModel.uiScale
3738
}
3839

3940
Group
@@ -42,12 +43,13 @@ Section
4243

4344
IntegerField
4445
{
45-
name: "seed"
46-
text: qsTr("Seed")
47-
defaultValue: Math.floor(Math.random() * 1000000) // Init with random integer in [1,...,999999]
48-
min: -999999
49-
max: 999999
50-
info: qsTr("Specify the random seed for reproducibility.")
46+
name: "seed"
47+
text: qsTr("Seed")
48+
defaultValue: Math.floor(Math.random() * 1000000) // Init with random integer in [1,...,999999]
49+
min: -999999
50+
max: 999999
51+
info: qsTr("Specify the random seed for reproducibility.")
52+
fieldWidth: 60 * preferencesModel.uiScale
5153
}
5254

5355
IntegerField
@@ -57,6 +59,7 @@ Section
5759
defaultValue: 2000
5860
min: 500
5961
info: qsTr("Specify the maximum number of iterations of the EM algoritm.")
62+
fieldWidth: 60 * preferencesModel.uiScale
6063
}
6164

6265
DoubleField
@@ -67,6 +70,7 @@ Section
6770
decimals: 6
6871
min: 0.000001
6972
info: qsTr("Specify the tolerance for the EM algoritm.")
73+
fieldWidth: 60 * preferencesModel.uiScale
7074
}
7175
}
7276
}

inst/qml/itemResponseTheoryDichotomous.qml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ Form
3232
CheckBox { name: "bayesian"; checked: false; visible: false } // Invisible option
3333
CheckBox { name: "dichotomous"; checked: true; visible: false } // Invisible option
3434
DICH.VariablesListDichotomous { }
35-
DICH.IrtInputDichotomous { }
36-
COMMON.IrtOutput { bayesian: false; dichotomous: true }
35+
DICH.IrtInputDichotomous { id: model }
36+
COMMON.IrtOutput { bayesian: false; dichotomous: true; modeltype: model.value }
3737
COMMON.ItemInfoCurves { }
3838
COMMON.ItemCharCurves { dichotomous: true }
3939
CLASSICAL.AdvancedOptions { }

inst/qml/itemResponseTheoryDichotomousBayesian.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Form
3333
CheckBox { name: "dichotomous"; checked: true; visible: false } // Invisible option
3434
DICH.VariablesListDichotomous { id: vars }
3535
DICH.IrtInputDichotomous { id: model }
36-
COMMON.IrtOutput { bayesian: true; dichotomous: true }
36+
COMMON.IrtOutput { bayesian: true; dichotomous: true; modeltype: model.value }
3737
COMMON.ItemInfoCurves { }
3838
COMMON.ItemCharCurves { dichotomous: true }
3939
BAYES.AdvancedOptions { ncovs: vars.ncovs; modeltype: model.value }

inst/qml/itemResponseTheoryPolytomous.qml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ Form
3232
CheckBox { name: "bayesian"; checked: false; visible: false } // Invisible option
3333
CheckBox { name: "dichotomous"; checked: false; visible: false } // Invisible option
3434
POLY.VariablesListPolytomous { }
35-
POLY.IrtInputPolytomous { }
36-
COMMON.IrtOutput { bayesian: false; dichotomous: false }
35+
POLY.IrtInputPolytomous { id: model }
36+
COMMON.IrtOutput { bayesian: false; dichotomous: false; modeltype: model.value }
3737
COMMON.ItemInfoCurves { }
3838
COMMON.ItemCharCurves { dichotomous: false }
3939
CLASSICAL.AdvancedOptions { }

inst/qml/itemResponseTheoryPolytomousBayesian.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Form
3333
CheckBox { name: "dichotomous"; checked: false; visible: false } // Invisible option
3434
POLY.VariablesListPolytomous { id: vars }
3535
POLY.IrtInputPolytomous { id: model }
36-
COMMON.IrtOutput { bayesian: true; dichotomous: false }
36+
COMMON.IrtOutput { bayesian: true; dichotomous: false; modeltype: model.value }
3737
COMMON.ItemInfoCurves { }
3838
COMMON.ItemCharCurves { dichotomous: false }
3939
BAYES.AdvancedOptions { ncovs: vars.ncovs; modeltype: model.value }

0 commit comments

Comments
 (0)