--- title: "Tabu Search with tabuSearch()" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Tabu Search with tabuSearch()} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(ShortForm) ``` ## How it works Tabu search is based on [Marcoulides & Falk (2018)](https://doi.org/10.1080/10705511.2017.1409074), extended here for short-form construction. At each iteration: 1. Every "neighbor" reachable by a single one-item swap (one item currently on a factor for one item not currently on it) is generated and fit. 2. Among the neighbors that both converged and aren't on the recent "tabu" list (a short memory of recently-visited changes, sized by `tabu.size`), the best one becomes the new current model. 3. If that model is also better than the best one found so far, it becomes the new best, and the tabu list is cleared. Forbidding recently-tried changes (rather than accepting worse moves with some probability, as `simulatedAnnealing()` does) is what keeps Tabu search from immediately cycling back to a local optimum it just left. `tabuSearch()` is the short-form-oriented, higher-level function; the package also provides the lower-level `tabu.sem()` for searching over an arbitrary set of free/fixed parameter changes (given an already-fit model and a candidate parameter table from `search.prep()`), which `tabuSearch()` is itself built on top of internally. ## A basic example As with `antColony()`/`simulatedAnnealing()`, every candidate item must already appear on its factor's line in `initialModel`. ```{r basic-example} set.seed(58310) shortAntModel <- " Ability =~ Item1 + Item2 + Item3 + Item4 + Item5 + Item6 + Item7 + Item8 Ability ~ Outcome " result <- tabuSearch( initialModel = shortAntModel, originalData = simulated_test_data, itemsPerFactor = 7, maxIterations = 3, tabu.size = 3, parallel = FALSE ) result ``` `itemsPerFactor` sets the target item count per factor; `items` (omitted here) is the flat candidate item pool, defaulting to all column names in `originalData`. ## Inspecting the result ```{r summary} summary(result) ``` `plot()` shows the criterion value across iterations, labeled to show whether it's being maximized or minimized: ```{r plot, fig.width=6, fig.height=4} plot(result) ``` ## The criterion `criterion` accepts either a `character` fit-measure name recognized by `lavaan::fitmeasures()` (the default is `"cfi"`, maximized), or an arbitrary function that takes a fitted `lavaan` object and returns a single numeric value -- useful for measures `lavaan::fitmeasures()` doesn't provide directly, like AIC, or for custom scoring: ```{r criterion-function} set.seed(58310) tabuCriterion <- function(fit) { tryCatch(lavaan::fitmeasures(fit, "chisq"), error = function(e) Inf) } result_chisq <- tabuSearch( initialModel = shortAntModel, originalData = simulated_test_data, itemsPerFactor = 7, criterion = tabuCriterion, # smaller chisq is better, so this is minimized directly # (unlike the default cfi criterion, which is maximized) negateCriterion = FALSE, maxIterations = 3, tabu.size = 3, parallel = FALSE ) result_chisq ``` `negateCriterion` controls the search direction: `TRUE` (the default, matching the default `"cfi"` criterion) looks for the *largest* value of `criterion`; `FALSE` looks for the *smallest*. Set it to match whichever direction is "better" for your chosen criterion. ## A larger example The examples above are deliberately small so they run quickly. A more realistic search, over a larger item bank with a custom criterion: ```{r larger-example, eval=FALSE} # four correlated-ish factors, 12 candidate items each tabuModel <- " Trait1 =~ Item1 + Item2 + Item3 + Item4 + Item5 + Item6 + Item7 + Item8 + Item9 + Item10 + Item11 + Item12 Trait2 =~ Item13 + Item14 + Item15 + Item16 + Item17 + Item18 + Item19 + Item20 + Item21 + Item22 + Item23 + Item24 Trait3 =~ Item25 + Item26 + Item27 + Item28 + Item29 + Item30 + Item31 + Item32 + Item33 + Item34 + Item35 + Item36 Trait4 =~ Item37 + Item38 + Item39 + Item40 + Item41 + Item42 + Item43 + Item44 + Item45 + Item46 + Item47 + Item48 " # NOTE: each factor must be on a single line, or the algorithm # will not parse the model syntax correctly. tabuShort <- tabuSearch( initialModel = tabuModel, originalData = tabuData, # your data here itemsPerFactor = c(3, 3, 3, 3), criterion = tabuCriterion, negateCriterion = FALSE, maxIterations = 20, tabu.size = 10 ) ``` ## 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" tabuSearch( initialModel = bifactorModel, originalData = lavaan::HolzingerSwineford1939, itemsPerFactor = c(6, 3, 3), bifactor = "visual", maxIterations = 20, tabu.size = 5 ) ``` ## The lower-level tabu.sem() `tabu.sem()` searches directly over a candidate parameter table (from `search.prep()`) rather than item swaps -- useful when you want to search over an arbitrary set of free/fixed parameter changes rather than a short-form-specific item-swap search: ```{r tabu-sem} holzingerModel <- " visual =~ x1 + x2 + x3 textual =~ x4 + x5 + x6 speed =~ x7 + x8 + x9" init.model <- lavaan::lavaan( model = holzingerModel, data = lavaan::HolzingerSwineford1939, auto.var = TRUE, auto.fix.first = TRUE, std.lv = FALSE, auto.cov.lv.x = TRUE ) ptab <- search.prep(fitted.model = init.model, loadings = TRUE, fcov = TRUE, errors = FALSE) trial <- suppressWarnings( tabu.sem(init.model = init.model, ptab = ptab, criterion = AIC, niter = 2, tabu.size = 5) ) trial ``` Like `tabuSearch()`, `tabu.sem()`'s `criterion` accepts either a character fit-measure name or a function (here, base R's `AIC()`), and its `negateCriterion` defaults to `FALSE` (minimizing) rather than `tabuSearch()`'s `TRUE`, since a plain objective like AIC is typically minimized directly.