Getting Started with ShortForm

Overview

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):

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.

When to use ShortForm

  • Reducing assessment length while maintaining measurement validity
  • Automating item selection for large-scale assessments
  • Building reproducible pipelines for psychometric modeling and evaluation

A shared workflow

All three algorithms follow the same general shape:

  1. Specify the full model – lavaan syntax for the item bank you’re reducing, with every candidate item already listed under its factor.
  2. Choose how many items to keep per factor, and (for simulatedAnnealing()/tabuSearch()) which fit measure or custom function to optimize.
  3. Run the algorithm – it searches candidate short forms and keeps track of the best one found.
  4. Inspect the result – every algorithm returns an S4 object with show()/summary()/plot() methods.
library(ShortForm)

Quick example: Ant Colony Optimization

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.316 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.931

Quick example: Simulated Annealing

set.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.194 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.985

Which algorithm should I use?

There’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:

  • Ant Colony Optimization evaluates many candidate short forms (“ants”) per iteration and is naturally parallel-friendly; it’s a good default when you can afford to evaluate a lot of candidate models and want a threshold-based fit criterion (e.g. “CFI > 0.95 AND RMSEA < 0.06”) rather than a single scalar to optimize.
  • Simulated Annealing explores one candidate at a time per chain but can accept a worse candidate with some probability (shrinking over time), which helps it escape local optima; running multiple parallel chains (setChains) gives several independent searches to compare.
  • Tabu Search also explores one candidate at a time, but instead of accepting worse candidates, it forbids revisiting recently-tried changes (the “tabu” list) to avoid cycling back to the same local optimum.

Outputs

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 syntax
  • summary() – the above plus the full lavaan fit output
  • plot() – a visualization of how the search progressed (fit over iterations/steps, or ACO’s pheromone/regression diagnostics)
plot(result_TS)