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

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)
#> Package 'ShortForm' version 1.0.0

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

Outputs

Every algorithm returns an S4 object with:

plot(result_TS)