--- title: "Ant Colony Optimization with antColony()" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Ant Colony Optimization with antColony()} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(ShortForm) ``` ## How it works The Ant Colony Optimization (ACO) algorithm (Dorigo & Stutzle, 2004; adapted for short-form construction by [Leite, Huang, & Marcoulides, 2008](https://doi.org/10.1080/00273170802285743)) is based on the foraging behavior of ants: they search for food in a variety of directions, and the shortest paths accumulate the strongest pheromone trails, which then attract more ants. Applied to short-form construction: 1. A number of "ants" (candidate short forms) are sampled per iteration, with items selected with probability proportional to their current "pheromone" weight. 2. Each candidate model is fit, and checked against a fit-based threshold test (`fit.statistics.test`). 3. Candidates that pass the threshold have a "pheromone" strength computed from their fit (`pheromone.calculation`); the best candidate's items have pheromone added to their weight, making them more likely to be sampled again. 4. This repeats until the same solution is chosen for a set number of ants in a row (`steps`), or `maxIterations` ants have been tried in total. ## A basic example Every candidate item must already appear on its factor's line in `initialModel` -- `antColony()` derives the factor names and each factor's candidate item pool directly from that syntax, the same way `simulatedAnnealing()` and `tabuSearch()` do. ```{r basic-example} set.seed(58310) result <- 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 ``` `itemsPerFactor` sets the target number of items to keep for each factor, in the order the factors appear in `initialModel`. Since this toy example already asks to keep all 3 items per factor, the "search" has only one candidate to find. ## Inspecting the result ```{r summary} summary(result) ``` `plot()` shows several diagnostics: how the pheromone accumulates across runs, and how the mean standardized loadings/variance explained change. ```{r plot, fig.width=7, fig.height=6} plot(result) ``` You can also request a single panel: ```{r plot-single, fig.width=6, fig.height=4} plot(result, type = "pheromone") ``` ## A more realistic example The toy example above doesn't actually reduce the item bank. A more typical use case starts from a much larger item pool -- here, the bundled `exampleAntModel` (56 items on a single `Ability` factor) and `simulated_test_data`: ```{r realistic-example, eval=FALSE} data(exampleAntModel) # a character vector for a lavaan model data(simulated_test_data) abilityShortForm <- antColony( data = simulated_test_data, ants = 5, evaporation = 0.7, initialModel = exampleAntModel, itemsPerFactor = 20, steps = 3, fit.indices = c("cfi", "rmsea"), fit.statistics.test = "(cfi > 0.95)&(rmsea < 0.05)", maxIterations = 500 ) abilityShortForm ``` ## Fit indices, thresholds, and pheromone Unlike `simulatedAnnealing()`/`tabuSearch()`, which optimize a single scalar `criterion`, `antColony()` uses two related but distinct arguments: - `fit.indices` -- which `lavaan::fitmeasures()` values to compute for each candidate (e.g. `c("cfi", "tli", "rmsea")`) - `fit.statistics.test` -- a logical expression over those fit indices that a candidate must satisfy to be considered viable at all (e.g. `"(cfi > 0.95)&(tli > 0.95)&(rmsea < 0.06)"`) - `pheromone.calculation` -- how the *strength* of a viable candidate's pheromone deposit is computed: `"gamma"` (mean standardized latent regression coefficients), `"beta"` (mean standardized observed regression coefficients), `"regression"` (both), or `"variance"` (mean variance explained) A candidate that fails `fit.statistics.test` contributes no pheromone at all, regardless of `pheromone.calculation`. ## Ordered (categorical) data `antColony()` works with categorical/ordered indicators by passing the appropriate `lavaan.model.specs`: ```{r ordered-example, eval=FALSE} sim_model <- " f1 =~ x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x10 f2 =~ x11 + x12 + x13 + x14 + x15 + x16 + x17 + x18 + x19 + x20 f3 =~ x21 + x22 + x23 + x24 + x25 + x26 + x27 + x28 + x29 + x30" sim_data <- cbind( psych::sim.rasch(nvar = 10)$items, psych::sim.rasch(nvar = 10)$items, psych::sim.rasch(nvar = 10)$items ) colnames(sim_data) <- paste0("x", 1:30) # only estimator and ordered are changed -- every other lavaan.model.specs # element falls back to antColony()'s own default example <- antColony( data = sim_data, ants = 5, evaporation = 0.7, initialModel = sim_model, lavaan.model.specs = list(estimator = "wlsmv", ordered = TRUE), itemsPerFactor = c(5, 5, 5), steps = 20, fit.indices = c("cfi.scaled"), fit.statistics.test = "(cfi.scaled > 0.90)", maxIterations = 500, parallel = TRUE ) ``` `lavaan.model.specs` accepts a *partial* list -- any element you omit falls back to `antColony()`'s own default for that element, but every name you do supply must be a recognized `lavaan()` argument, or the call errors immediately (catching typos like `estmator` before a long run starts). ## Bifactor models Pass the name of the general factor as `bifactor` to have all of the retained items across the other factors also load on it: ```{r bifactor-example, eval=FALSE} bifactorModel <- " visual =~ x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9" antColony( data = lavaan::HolzingerSwineford1939, ants = 5, evaporation = 0.7, initialModel = bifactorModel, itemsPerFactor = c(6, 3, 3), bifactor = "visual", steps = 5, fit.indices = c("cfi"), fit.statistics.test = "(cfi > 0.9)", maxIterations = 100 ) ``` ## Parallelization and progress output `antColony()` evaluates the `ants` candidates within each iteration in parallel by default (`parallel = TRUE`); set `parallel = FALSE` for serial execution (as in the examples above, so output is deterministic). `verbose` controls whether per-ant progress is printed to the console -- the full per-run history is always available afterward from the returned object's `summary` and `final_solution` slots regardless of this setting.