print.specification_search() documents its return value
(\value).algorithm
run on parallel workers in algorithm_stability() no longer
refers to the global environment: it copies the objects the function
needs into a new private environment and never modifies the user’s
workspace.efa_boosting() help page.algorithm_stability(),
cross_validate_cfa(), item_stability(),
simulate_cfa_recovery() and
simulate_recovery() no longer fix a seed by default
(seed = NULL).bifactor_indices() and
cross_validate_cfa() gain verbose; console
output of non-print functions can be silenced.export_conceptual_analysis() no longer writes to the
working directory by default: file must be supplied.optimal_cfa_with_ai(),
optimal_efa_with_ai(), print_cfa_results() and
print_efa_results() are now exported.\dontrun{}.report_efa_results() failed on data frames
(n partially matched na.print);
optimal_cfa_with_ai() failed with the ML estimator (it
requested rmsea.scaled).The package offers several purification routines, and on a given scale they disagree. Two functions put them on the same footing so the choice can be argued rather than asserted.
theory_recovery() measures how much
of the theoretical key a solution keeps. A solution departs from the
theory by dropping items or by moving them, and a single index confounds
the two, so it reports retention, accuracy (matching factors to
dimensions by the exhaustive optimum, not greedily), recovery (their
product), the Adjusted Rand Index and, when loadings are available,
Tucker congruence per dimension. It reads the output of any routine in
the package, a plain partition or a loading matrix. Accuracy alone is
misleading for confirmatory routines: cfa_boosting() can
only drop items, so its accuracy is 1 by construction.
algorithm_stability() runs any
item-selection algorithm on random halves of the data and fits the
structure it chooses on the other half. It reports whether the decision
replicates (Jaccard overlap with the full-sample solution, per-item
retention rates) and whether it holds out of sample (fit and largest
interfactor correlation on the validation half), and, given a theory,
the recovery on every split. Unlike item_stability() it is
not tied to efa_boosting(), and unlike
cross_validate_cfa() it is not limited to one factor.
Splits run in parallel with n_cores. A timeout
(default 300 s) counts a split whose algorithm does not finish as failed
instead of stalling the run: on small derivation halves an iterative
search can loop indefinitely.
bifactor_indices() returned
NA for omega, omega_H, PUC and every specific ECV when some
items load on the general factor only, as in a bifactor S-1 model (Eid
et al., 2017) or after dropping a specific factor that collapsed. It
also reported an inadmissible solution without complaint (an omega of
2.4 in one case); it now warns when a standardized loading reaches 1 or
a residual variance is not positive.
discriminant_boosting() stopped
with “arguments imply differing number of columns” when a structure in
its ladder did not converge: the row for a non-converged model had two
columns instead of all of them. It now carries every column, filled with
NA.
A model can fit well and still be uninterpretable. Four factors correlating .97 reproduce the covariance matrix as well as one factor does, so CFI and RMSEA say nothing about whether the factors exist. These two functions target what the fit indices miss.
discriminant_boosting() rescues the
discriminant validity of a multidimensional scale whose interfactor
correlations are above the threshold — or above 1, which makes the
solution inadmissible — with the smallest possible departure from the
theoretical structure. It walks a ladder of specifications ordered by
distance from the original (theoretical model, plus a method factor,
bifactor, and every partition of the factors into fewer blocks), prunes
each level guided by the interfactor correlation, then by CFI, and keeps
the model that meets every criterion with the most factors.
Three design decisions come from what does not work. Pruning by fit leaves the interfactor correlation untouched, so the search is guided by it instead. Letting the data assign items surfaces the wording polarity of reverse items as if it were a dimension, so item-to-factor assignment stays theoretical and only whole factors are merged. Optimising fit and correlation at once lets fit dominate, so they are optimised in sequence.
local_fit_search() searches for a
specification of a one-factor model that reaches the fit targets,
combining item removal with freed residual covariances. Where
redundancy_short_form() prunes near-duplicate items, this
one also weighs keeping the pair and modelling its covariance, and
decides by the effect on fit.
It encodes three steps normally taken by hand: items below the loading floor are removed without requiring that fit improve (an item that does not load cannot stay, and dropping it often worsens RMSEA at first); an item taking part in two or more covariances is dropped rather than kept, since the item is the problem and not the relations; and every retained covariance is tested for removal at the end, because each freed parameter is a debt.
It does not judge whether a covariance makes substantive sense. It returns the estimate, its interval, its sign and the wording of both items. A negative residual covariance between items at opposite poles of a construct is often the trace of a dimension the unidimensional model absorbed, and is worth reading before treating it as noise.
Both routines treat omega_min and
min_loading as adaptive: a model already below a floor is
not frozen, its effective bar becomes its current value. A floor set
above the starting value rejects every candidate and stalls the search
on the first iteration — a failure mode indistinguishable, from the
outside, from a genuine negative result.
specification_search_theory():
only_within_factor is documented, including why a residual
correlation between items of different factors absorbs covariance that
belongs at the factor level and makes discriminant validity look better
than it is.PsyMetricTools. Every exploratory model was fitted
through PsyMetricTools::EFA_modern(), a package that is not
on CRAN, which blocked OptimalFactor’s own submission. The engine (model
syntax → lavaan fit → fit measures → standardized pattern matrix) is now
internal (R/efa_modern_internal.R) and calls
lavaan::cfa() with exactly the same arguments
(mimic = "Mplus", ordered = TRUE), so results
are reproduced: on Data_Personality the largest loading
difference against the old engine is 1.6e-05, which is smaller than the
1.9e-05 that lavaan itself produces between two identical runs. The
internal version drops the purrr and magrittr
usage of the original and returns a plain data.frame
instead of a tibble.item_stability(): runs the whole
efa_boosting() pipeline over bootstrap or subsample
resamples and reports, per item, the retention and removal rates plus
the stability of its factor assignment. Factor labels are aligned to a
full-sample reference before agreement is computed, since rotation names
factors arbitrarily. This is the empirical answer to the
capitalization-on-chance objection (MacCallum, Roznowski & Necowitz,
1992): an item dropped in 97 of 100 resamples is a defensible decision,
one dropped in 55 is not. Ships with a print method that
flags decisions taken in 25–75 % of resamples as unstable. Resampling
defaults to method = "subsample": drawing with replacement
duplicates cases, which distorts the polychoric correlations ordinal
estimation relies on, and on a sample of 100 leaves as few as 63
distinct cases, enough to send a WLSMV fit grinding for minutes.
Measured on Data_Personality with R = 40 over
8 cores, bootstrap took 5m46s with 3 replications hitting the cap
against 2m31s with none under subsampling.
simulate_recovery(): Monte Carlo
evidence on when the pipeline recovers a known structure. Simulates
ordinal data from a population model with good, cross-loading and weak
items, crosses conditions over sample size, loading size and items per
factor, and reports recovery rate, sensitivity (contaminated items
removed) and specificity (good items kept). Replications are fitted in
parallel through n_cores, which is what makes a crossed
design feasible; the datasets are always drawn sequentially from
seed, so results do not depend on how many cores ran them.
Ships print and plot methods.
plot() draws the recovery curve against sample size, one
line per population loading and one panel per number of items per
factor, which is the figure a validation paper reports.
simulate_cfa_recovery(): the
confirmatory counterpart, running the same population model through
cfa_boosting(). The question is not the same one: the
structure is imposed rather than discovered, so the assignment cannot be
wrong and what is measured is the item selection. Reports
exact_rate (the removed set equals the contaminated set
item for item), sensitivity, specificity and
mean_covs_added, the residual covariances introduced where
the population has none, which turns the MacCallum et al. (1992)
objection into a number.
cfa_boosting()
enforces its own loading floorThe confirmatory simulation was built to evaluate the pipeline and
immediately found a defect in it. The optimisation loop tested the fit
targets first and broke as soon as they were met, so the low-loading
check further down was unreachable: an item the user’s own
thresholds$loading declares inadmissible survived untouched
whenever global fit happened to be acceptable. And global fit is
acceptable far more often than one would hope. In simulation, a
population item loading .20 sat inside a model with RMSEA = .03, and
adding cross-loading items barely moved the index at all: with six items
cross-loading at .60, RMSEA stayed at .053 while CFI rose to
.997, because omitted cross-loadings are absorbed by the interfactor
correlation instead of degrading fit.
thresholds$enforce_loading (default
TRUE): an item below the loading floor is now removed
regardless of the fit indices, weakest first and one per iteration since
the loadings are re-estimated after each refit. This is an admissibility
criterion, not a fit optimisation, so the removal is unconditional
rather than contingent on improving the loss. Items whose removal would
breach min_items_per_factor are protected.
FALSE restores the previous behaviour.Data_Personality the previous behaviour removed one item
and bought its fit with a residual covariance (RMSEA .083); enforcing
the floor removes the weak item as well, needs no covariance at all, and
ends at RMSEA .091. Slightly worse global fit, a defensible retained
set, and one less parameter that the population may not have.thresholds$enforce_simple_structure
(default TRUE) and
thresholds$cross_loading (default 0.30):
the floor catches the item that measures nothing, not the one that
measures two things. A cross-loading item still loads acceptably on its
own factor, so the floor never sees it. Detection is by the modification
index of the absent loading, but never by significance alone: the
standardized EPC must also reach the threshold, since a significant
index with a trivial EPC is the capitalization on chance the whole
exercise is meant to avoid. Adding this took the confirmatory simulation
from sensitivity .667/.583 to .958/.958 and exact
recovery from .250/.167 to .917/.917, with specificity
rising to 1.000 at both sample sizes, fewer spurious
residual covariances (.17 to .08 and .08 to .00) and better final fit
(RMSEA .031 to .013 and .035 to .004). Nothing was traded away for
it.stop_reason is now returned, with
values all_targets_met,
targets_met_loading_protected,
no_improving_action and max_iterations, so the
caller no longer has to infer it from the log.n_factors_fittedEvery condition in a recovery design hands the pipeline the true
number of factors, and in practice nobody knows it.
simulate_recovery() gains n_factors_fitted,
crossed like the other conditions, so the population keeps its own
dimensionality while the algorithm is told something else.
The answer is asymmetric, and the asymmetry is the practical finding. Against a three-factor population with 12 good items, one cross-loading item and one weak one (12 replications):
| fitted | recovery | sensitivity | specificity | retained |
|---|---|---|---|---|
| 2 | .000 | .750 / .542 | .729 / .653 | 9.25 / 8.75 |
| 3 | 1.000 | .875 | 1.000 | 12.25 |
| 4 | .667 / .556 | .000 | .986 / 1.000 | 13.83 / 14.00 |
Extracting too few factors is destructive: specificity collapses to
.65-.73, so between three and four good items are discarded to force the
data into a structure that cannot hold them. Extracting too many makes
the pipeline inert instead: sensitivity falls to zero, no contaminated
item is removed at all, and 18 of 24 replications stop at
min_items_per_factor_protected, because with four factors
and fourteen items almost any removal breaches the minimum. The scale
survives untouched, which is the harmless failure.
The practical reading: when the dimensionality is uncertain, erring towards more factors leaves the algorithm conservative, while erring towards fewer leaves it removing good items.
simulate_recovery() and
simulate_cfa_recovery() used to place every cross-loading
item on the same two factors. With more than one such item that makes
them clones: they correlate strongly, form a coherent cluster, and the
rotation reports the cluster as a factor of its own, each item showing a
single clean loading near .80. No item-level criterion can call that a
cross-loading, because in the estimated solution it no longer is one,
and the good items get displaced to make room for it. They are now
spread over different pairs.
The correction matters because it inverts the verdict. Under three
cross-loading items at .55 plus two weak ones, the old generator made
efa_boosting() look defective (sensitivity .617/.700,
recovery falling to .75 at N = 600, contaminated items left in the
pool). With the population correctly specified the same conditions give
sensitivity .983/1.000, recovery 1.000 and specificity 1.000, retaining
12.08 and 12.00 items against the 12 good ones in the population. The
apparent defect was the measuring instrument.
Resampling exposed three things that made these functions unusable in practice rather than merely slow.
item_stability()
now print a bar with elapsed time and an estimate of what is left,
announce the cluster starting up, and say in advance whether the bar
will advance per round or jump at the end. Status lines go through
flush.console(), without which the R console holds them
until the computation ends.timeout (default 120 s per
replication) in item_stability(),
simulate_recovery() and
simulate_cfa_recovery(). A bootstrap draw of N = 100 can
leave 63 distinct cases, and a WLSMV fit on the near singular matrix
that follows runs for minutes: measured, one such replication passed 180
s without finishing, while the same one capped returns in 69 s. One
replication like that stalls an entire run. Capped replications are
counted and reported rather than hidden..libPaths() is pushed to them before loading the package,
so a session that added a library at runtime (RStudio project libraries,
renv) does not produce workers that cannot find
OptimalFactor. That failure previously surfaced as an opaque
checkForRemoteErrors() message, because the guard tested
for an error while requireNamespace(quietly = TRUE) merely
returns FALSE.performance$fit_target_only (default
TRUE) in efa_boosting(). Evaluating a
candidate reads only the n_factors solution, yet the engine
fitted the 1..k-1 solutions as well and discarded them. Verified
identical on Data_Personality and
Data_Expectativas (structure, removed items, stop reason,
RMSEA, omega and the fit table), with 38 % fewer lavaan fits and a 1.43x
speedup where the greedy loop evaluates candidates. FALSE
restores the previous behaviour.parLapplyLB:
replication times are heavy tailed, so load balancing matters more than
it would with uniform tasks.fits[[r]] <- NULL deleted the slot instead of
filling it, and a failed replication returns exactly NULL.
Any condition where a replication failed aborted with a subscript error;
the same idiom in item_stability() kept its progress
counter permanently at “0 failed”.OptimalFactor::efa_boosting even when running in process,
which fails under devtools::load_all() because nothing is
exported yet. The error was swallowed and every replication was recorded
as a silent non-convergence, a result that looked like evidence.performance through ... to the
resampling functions raised “formal argument matched by multiple actual
arguments” instead of overriding the defaults. It is now merged.tests/testthat for the first time:
170 checks over the population model, the ordinal simulation, factor
alignment, recovery scoring for both pipelines, the progress helpers and
the timeout translation, with regression tests for each of the fixes
above.thresholds$min_omega in
efa_boosting() and min_omega
in redundancy_short_form(): an item is removed only if
every factor keeps McDonald’s omega at or above the floor. It is
deliberately a constraint and not a term in the loss — reliability is
not interpretable in the ill-fitting intermediate models the loss
visits, so omega vetoes individual removals instead of trading against
the fit indices. A factor already below the floor is not frozen: its
effective bar becomes its current omega, so removals that do not reduce
reliability remain available. Heywood cases are exempt, because an
inadmissible solution must be fixed regardless. NULL
(default) reproduces the previous behaviour exactly.min_omega_protected.efa_boosting() now always returns
omega_final (omega per factor) and
omega_check, whether or not the floor is active.redundancy_short_form() gained an omega
column in its trajectory plus stop_reason and
omega_blocked fields.R CMD check --as-cran now passes with 0 errors and 0
warnings (apart from the expected note about PsyMetricTools
not yet being on CRAN): non-ASCII characters in R code were escaped to
\uXXXX, runtime calls to install.packages() /
devtools::install_github() were replaced by informative
errors, library() calls inside functions were removed, and
the Remotes field was dropped from
DESCRIPTION.imprimir_items_eliminados() is now exported.cfa_boosting() and
efa_boosting(), examples updated to the current
cfa_boosting(data, model) signature, duplicated alias for
print.specification_search removed, and the new
print argument of report_efa_results()
documented.parallel and stats are now declared in
Imports.efa_boosting(),
efa_optimizer(), optimal_efa_with_ai() and
optimal_cfa_with_ai()) now honour the
OPENAI_BASE_URL environment variable, so they can be
redirected to any OpenAI-compatible provider — e.g. the Hugging Face
router (https://router.huggingface.co/v1) to use models
such as Qwen/Qwen2.5-72B-Instruct with an hf_
token. Default behaviour (variable unset) is unchanged.run_efa_boosting() and
run_efa_boosting_wizard() (plus
inst/shiny-apps/ and the two app vignettes) were removed:
the interactive OptimalFactor Wizard is now distributed
separately as a web application on Posit Connect Cloud. The package
keeps the analysis engine only, which also trims the suggested
dependencies (shiny, bslib, DT, officer, flextable, readxl, readr,
commonmark, later, future, promises are no longer suggested).theory_weight parameter grades how much theory counts
relative to fit (0 reproduces the fit-only search).specification_search_theory() and now emits a deprecation
warning on every call. It is kept for backward compatibility.run_efa_boosting() studio.
Provides a guided 5-phase flow (Data → Parallel diagnostic → EFA
boosting → Reliability → External and convergent/discriminant validity),
each with a “proposed action / what will happen” panel.efa_boosting() plus the
print_conceptual_analysis() output so the entire
item-purification process is auditable in a single pane. Downloadable as
.txt.gpt-4.1 default) to drive the wizard step by step.
Includes a chat panel with Markdown rendering of replies (via
commonmark).semTools::compRelSEM, ord.scale = TRUE) and
Cronbach’s alpha per factor, reported alongside CFI/TLI/RMSEA/SRMR of
the EFA-derived CFA model.DERS_AC1, DERS_OB1 →
multidimensional instrument with two subscales).response_format = "json_object".convergencia fuerte / moderada /
débil / no significativa for convergent
expectations; discrimina / dudosa /
NO discrimina for discriminant expectations.geom_tile) with
asterisks for p < .05.{{TABLE_LOADINGS}}, {{TABLE_FIT}},
{{TABLE_OMEGA}}, {{TABLE_EXTERNAL}},
{{TABLE_CONVERGENT}}). The AI never invents numbers — it
only writes prose around the embedded tables. Requires an OpenAI API
key.← Back, ⏸ Pause autopilot and
▶ Resume AI in a persistent toolbar. The user can rewind to
any previous phase (even from the “wizard completed” state) without
losing previously computed results.efa_boosting()performance$max_candidates_eval (default 12) and
performance$smart_pruning = TRUE: ranks candidate items by
their maximum factor loading and evaluates only the top-K, giving large
speed-ups on long instruments (e.g. 32 items: >30 min → ~2–3
min).stop_reason with canonical values
(all_criteria_met,
min_items_per_factor_protected,
max_iterations, not_enough_items,
efa_convergence_failed, fit_target_reached,
fit_zero_no_structural_problem, timeout).
Allows downstream code to give precise messages without inferring why
the loop stopped.$text field suitable for embedding in manuscripts or
printing to console.Data_Expectativas: Example dataset for expectation
scaleData_Personality: Example personality assessment
dataset