ShortForm is an R package for constructing short-form assessments from larger item banks using reproducible, optimization-based workflows.
It provides implementations of three metaheuristic search algorithms to automate item selection while preserving prespecified psychometric properties (such as model fit):
antColony())
– adapted from Leite, Huang, &
Marcoulides (2008)simulatedAnnealing()) – following Kirkpatrick et
al. (1983)tabuSearch(), and the
lower-level tabu.sem()) – based on Marcoulides &
Falk (2018)All three search over candidate short forms of a lavaan
model, evaluating each candidate’s fit and keeping the best one found.
This vignette gives a quick tour of all three; see the dedicated
vignettes (vignette("antColony"),
vignette("simulatedAnnealing"),
vignette("tabuSearch")) for a deeper look at each one.
set.seed(58310)
result_ACO <- antColony(
data = lavaan::HolzingerSwineford1939,
ants = 2, evaporation = 0.7,
initialModel = " visual =~ x1 + x2 + x3
textual =~ x4 + x5 + x6
speed =~ x7 + x8 + x9 ",
itemsPerFactor = c(3, 3, 3),
steps = 2, fit.indices = c("cfi"), fit.statistics.test = "(cfi > 0.6)",
maxIterations = 2, parallel = FALSE, verbose = FALSE
)
result_ACO
#> Algorithm: Ant Colony Optimization
#> Total Run Time: 0.181 secs
#>
#> Function call:
#> antColony(data = lavaan::HolzingerSwineford1939, ants = 2, evaporation = 0.7,
#> initialModel = " visual =~ x1 + x2 + x3\n textual =~ x4 + x5 + x6\n speed
#> =~ x7 + x8 + x9 ", itemsPerFactor = c(3, 3, 3), steps = 2, fit.indices =
#> c("cfi"), fit.statistics.test = "(cfi > 0.6)", maxIterations = 2, parallel =
#> FALSE, verbose = FALSE, sample.cov = NULL, sample.nobs = NULL, items = NULL,
#> bifactor = NULL, lavaan.model.specs = list(model.type = "cfa", estimator
#> = "default", ordered = NULL, int.ov.free = TRUE, int.lv.free = FALSE,
#> auto.fix.first = TRUE, auto.fix.single = TRUE, auto.var = TRUE, auto.cov.lv.x
#> = TRUE, auto.th = TRUE, auto.delta = TRUE, auto.cov.y = TRUE, std.lv = FALSE,
#> group = NULL, group.label = NULL, group.equal = "loadings", group.partial =
#> NULL, group.w.free = FALSE), pheromone.calculation = "gamma")
#>
#> Final Model Syntax:
#> visual =~ x1 + x2 + x3
#> textual =~ x4 + x5 + x6
#> speed =~ x7 + x8 + x9
#>
#> Fit Indices: cfi
#> Fit Test: (cfi > 0.6)
#> Final Model Values: cfi = 0.931set.seed(58310)
result_SA <- suppressWarnings(simulatedAnnealing(
initialModel = " visual =~ x1 + x2 + x3
textual =~ x4 + x5 + x6
speed =~ x7 + x8 + x9 ",
originalData = lavaan::HolzingerSwineford1939,
maxIterations = 3,
criterion = "cfi", negateCriterion = TRUE,
itemsPerFactor = c(2, 2, 2),
items = paste0("x", 1:9)
))
#> Initializing short form creation.
#> The initial short form is:
#> visual =~ x2 + x1
#> textual =~ x6 + x4
#> speed =~ x8 + x7
#>
#> Using the short form randomNeighbor function.
#> Finished initializing short form options.
#> Current Progress:
#> Old Fit: 0.97 New Fit: 0.985 Current Step = 2 of a maximum 3. Current Step = 3 of a maximum 3.
result_SA
#> Algorithm: Simulated Annealing
#> Total Run Time: 0.061 secs using 1 chains.
#>
#> Function call:
#> simulatedAnnealing(initialModel = " visual =~ x1 + x2 + x3\n textual =~ x4 + x5
#> + x6\n speed =~ x7 + x8 + x9 ", originalData = lavaan::HolzingerSwineford1939,
#> maxIterations = 3, criterion = "cfi", negateCriterion = TRUE, itemsPerFactor
#> = c(2, 2, 2), items = paste0("x", 1:9), temperature = "linear", Kirkpatrick
#> = TRUE, randomNeighbor = TRUE, lavaan.model.specs = list(model.type = "cfa",
#> auto.var = TRUE, estimator = "default", ordered = NULL, int.ov.free = TRUE,
#> int.lv.free = FALSE, std.lv = TRUE, auto.fix.first = FALSE, auto.fix.single
#> = TRUE, auto.cov.lv.x = TRUE, auto.th = TRUE, auto.delta = TRUE, auto.cov.y =
#> TRUE), maxChanges = 5, restartCriteria = "consecutive", maximumConsecutive =
#> 25, bifactor = NULL, setChains = 1, shortForm = T)
#>
#> Final Model Syntax:
#> visual =~ x3 + x1
#> textual =~ x5 + x4
#> speed =~ x8 + x9
#>
#>
#>
#> Criterion: "cfi" (maximized)
#> Final Model Value: 0.985set.seed(58310)
shortAntModel <- "
Ability =~ Item1 + Item2 + Item3 + Item4 + Item5 + Item6 + Item7 + Item8
Ability ~ Outcome
"
result_TS <- tabuSearch(
initialModel = shortAntModel,
originalData = simulated_test_data, itemsPerFactor = 7,
maxIterations = 3, tabu.size = 3, parallel = FALSE
)
#> Running iteration 1 of 3. Running iteration 2 of 3. Running iteration 3 of 3.
result_TS
#> Algorithm: Tabu Search
#> Total Run Time: 0.464 secs
#>
#> Function call:
#> tabuSearch(originalData = simulated_test_data, initialModel = shortAntModel,
#> itemsPerFactor = 7, maxIterations = 3, tabu.size = 3, parallel = FALSE,
#> items = NULL, criterion = "cfi", negateCriterion = TRUE, lavaan.model.specs =
#> list(int.ov.free = TRUE, int.lv.free = FALSE, std.lv = TRUE, auto.fix.first =
#> FALSE, auto.fix.single = TRUE, auto.var = TRUE, auto.cov.lv.x = TRUE, auto.th
#> = TRUE, auto.delta = TRUE, auto.cov.y = TRUE, ordered = NULL, model.type =
#> "cfa", estimator = "default"), bifactor = NULL, verbose = FALSE)
#>
#> Final Model Syntax:
#> Ability =~ Item1 + Item6 + Item3 + Item4 + Item5 + Item7 + Item8
#> Ability ~ Outcome
#>
#> Criterion: "cfi" (maximized)
#> Final Model Value: 1There’s no universally “best” choice – all three are heuristic searches, so it’s reasonable to try more than one and compare results. A few practical differences:
setChains) gives several
independent searches to compare.Every algorithm returns an S4 object with:
show()/print() – a compact summary: run
time, the selected criterion and its final-model value, the function
call, and the final model syntaxsummary() – the above plus the full lavaan
fit outputplot() – a visualization of how the search progressed
(fit over iterations/steps, or ACO’s pheromone/regression
diagnostics)