
Exploratory Factor Analysis Optimizer with Optional LLM Support
efa_optimizer.RdAutomatically refines an Exploratory Factor Analysis (EFA) solution by combining global fit (scaled RMSEA), item-level loading quality, and explicit detection of Heywood/near-Heywood cases. At each iteration the routine re-fits the model, removes the worst offending item (by structural fault or RMSEA improvement), and stops when predefined fit/structure criteria are met or no admissible improvement remains. Optionally, it can call a Large Language Model (LLM) to generate plain-language justifications for removing/keeping items, with language control and detail level.
Usage
efa_optimizer(
data,
name_items,
item_range = NULL,
n_factors = 3,
exclude_items = NULL,
# Thresholds (includes Heywood tolerances)
thresholds = list(
rmsea = 0.08,
loading = 0.30,
min_items_per_factor = 3,
heywood_tol = 1e-6,
near_heywood = 0.015
),
# Model configuration
model_config = list(
estimator = "WLSMV",
rotation = "oblimin"
),
# AI (optional)
use_ai_analysis = FALSE,
ai_config = list(
api_key = NULL,
generate_names = FALSE,
only_removed = TRUE,
gpt_model = "gpt-3.5-turbo",
language = "english",
analysis_detail = "detailed",
domain_name = "Default Domain",
scale_title = "Default Scale Title",
construct_definition = "",
model_name = "EFA Model",
item_definitions = NULL
),
verbose = TRUE, ...
)Arguments
- data
A
data.framecontaining observed variables.- name_items
Common item prefix (e.g.,
"DP"or"PPTQ").- item_range
Integer vector of length 2 with the first/last item indices; if
NULL, items are discovered by the prefix followed by a trailing integer.- n_factors
Number of factors to extract.
- exclude_items
Character vector of items to exclude at the start.
- thresholds
List of decision thresholds:
- rmsea
Maximum acceptable scaled RMSEA.
- loading
Minimum salient absolute loading.
- min_items_per_factor
Minimum items retained per factor (based on primary loading).
- heywood_tol
Tolerance for detecting negative uniqueness (\(\psi < -\)
heywood\_tol) or impossible loadings.- near_heywood
Band around zero uniqueness treated as near-Heywood.
- model_config
List of EFA fitting options:
- estimator
Estimator (e.g.,
"WLSMV").- rotation
Rotation method (e.g.,
"oblimin").
- use_ai_analysis
Logical; if
TRUE, requests LLM justifications without affecting the optimization path.- ai_config
List with LLM settings:
- api_key
API key (character).
- generate_names
If
TRUE, may also request short factor names (if implemented).- only_removed
If
TRUE, justify only removed items; otherwise also justify retained items.- gpt_model
LLM model identifier.
- language
Language for justifications; supports at least
"english"and"spanish".- analysis_detail
Verbosity level for the LLM answer; one of
"brief","standard","detailed".domain_name,scale_title,construct_definition,model_nameContext strings injected into prompts.
- item_definitions
Named list mapping item IDs to plain-language item stems/descriptions used by the LLM.
- verbose
Logical; if
TRUE, prints progress, current structure, counts per factor, and a progress bar during LLM analysis.- ...
Additional arguments forwarded to the package's internal EFA engine.
Details
Iteration logic.
Fit an EFA using the package's internal EFA engine on the current item set (oblique rotation by default).
Extract the loading matrix \(L\) and the factor correlation matrix \(\Phi\) (identity if unavailable).
Derive communalities \(h^2 = \mathrm{diag}(L \Phi L')\) and uniquenesses \(\psi = 1 - h^2\). Flag:
Heywood (\(\psi < -\)
heywood\_tolor impossible loadings),near-Heywood (\(\psi\) within
[-near\_heywood, near\_heywood]),no salient loading (all \(|loading| <\)
loading),cross-loadings (2+ salient loadings; severity scored by the gap between the two largest absolute loadings).
Enforce
min_items_per_factorusing the primary (largest absolute) loading; rows with no salient loading do not count toward any factor.Removal policy (priority order):
Remove the most severe Heywood item.
Else remove the worst near-Heywood item.
Else, if RMSEA exceeds
thresholds$rmsea, simulate single-item removals (that preservemin\_items\_per\_factor) and drop the candidate yielding the best RMSEA improvement.Else remove the structurally worst item (by diagnostic score).
Stop when RMSEA \(\le\) threshold and all factors meet
min\_items\_per\_factor, or when no admissible removal improves fit/structure, or when the maximum number of steps is reached.
LLM justifications (optional).
If use_ai_analysis = TRUE and item_definitions are provided, the function can call an LLM via httr to draft plain-language justifications. The language (ai_config$language) and verbosity (ai_config$analysis_detail) control the style and length. The request includes, when available, item-level technical statistics (primary loading, \(h^2\), reason for removal, and RMSEA at the removal step). Transient server errors (e.g., HTTP 502/503/504) and rate limits (HTTP 429) are handled with up to three attempts and exponential backoff. This analysis is purely descriptive and does not influence the optimization path.
Dependencies. If LLM is used, httr and jsonlite are required; the function attempts to install them if missing.
Value
A list with:
- final_structure
Data frame with the final, thresholded loadings and item labels.
- removed_items
Character vector of removed items, in order.
- steps_log
Data frame with
step,removed_item,reason,rmsea(value at that step).- iterations
Number of iterations executed.
- final_rmsea
Scaled RMSEA at termination.
- bondades_original
Fit indices returned by
EFA_modern.- specifications
Model specifications returned by
EFA_modern.- inter_factor_correlation
Estimated \(\Phi\) (identity if unavailable or
n_factors == 1).- last_h2
Vector of communalities from the last iteration.
- last_psi
Vector of uniquenesses from the last iteration.
- last_flags
List with logical vectors
heywoodandnearfor the last iteration.- conceptual_analysis
If LLM used, a list with:
- removed
Named list of justifications for removed items (or
NULL).- kept
Named list of justifications for retained items if
only_removed = FALSE(orNULL).- item_stats
Named list of per-item statistics passed to the LLM (primary loading, \(h^2\), removal reason, RMSEA at removal).
- config_used
Echo of
thresholds,model_config,use_ai_analysis, andai_config(includinglanguageandanalysis_detail).
Examples
# \donttest{
# Run without LLM support
data(Data_Personality)
res <- efa_optimizer(
data = Data_Personality,
name_items = "PPTQ",
n_factors = 3,
thresholds = list(
rmsea = 0.08, loading = 0.30, min_items_per_factor = 3,
heywood_tol = 1e-6, near_heywood = 0.015
),
model_config = list(estimator = "WLSMV", rotation = "oblimin"),
verbose = FALSE
)
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= 8.540726e-18)
#> is close to zero. This may be a symptom that the model is not identified.
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= 1.397705e-17)
#> is close to zero. This may be a symptom that the model is not identified.
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= 1.268967e-17)
#> is close to zero. This may be a symptom that the model is not identified.
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= -1.842400e-17)
#> is smaller than zero. This may be a symptom that the model is not
#> identified.
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= 2.161542e-17)
#> is close to zero. This may be a symptom that the model is not identified.
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= 5.176050e-17)
#> is close to zero. This may be a symptom that the model is not identified.
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= -4.983443e-17)
#> is smaller than zero. This may be a symptom that the model is not
#> identified.
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= 2.201994e-17)
#> is close to zero. This may be a symptom that the model is not identified.
#> Warning: lavaan->lav_model_vcov():
#> The variance-covariance matrix of the estimated parameters (vcov) does not
#> appear to be positive definite! The smallest eigenvalue (= 5.931251e-17)
#> is close to zero. This may be a symptom that the model is not identified.
res$final_structure
#> Items f1 f2 f3
#> 1 PPTQ6 -0.6713424 0.0000000 0.0000000
#> 2 PPTQ12 0.6447529 0.0000000 0.0000000
#> 3 PPTQ2 0.6403550 0.0000000 0.0000000
#> 4 PPTQ9 0.0000000 0.6593586 0.0000000
#> 5 PPTQ4 0.0000000 0.6071600 0.0000000
#> 6 PPTQ14 0.3643084 -0.5578913 0.0000000
#> 7 PPTQ5 0.0000000 0.0000000 0.7788308
#> 8 PPTQ10 0.0000000 0.0000000 0.6900500
#> 9 PPTQ15 0.0000000 0.0000000 0.6706673
#> 10 PPTQ3 0.0000000 0.0000000 0.5421446
#> 11 PPTQ1 0.0000000 0.0000000 0.5200078
#> 12 PPTQ8 0.0000000 0.0000000 0.3547691
#> 13 PPTQ11 0.0000000 0.0000000 0.3328500
res$steps_log
#> step removed_item reason rmsea
#> 1 1 PPTQ7 Cross-loading (priority) 0.056
#> 2 2 PPTQ13 No loading 0.061
res$final_rmsea
#> [1] 0.077
# }
if (FALSE) { # \dontrun{
# LLM justification (requires a valid API key and item definitions)
res_ai <- efa_optimizer(
data = Data_Personality,
name_items = "PPTQ",
n_factors = 3,
use_ai_analysis = TRUE,
ai_config = list(
api_key = Sys.getenv("OPENAI_API_KEY"),
item_definitions = as.list(stats::setNames(
paste("Item", 1:15, "content"), paste0("PPTQ", 1:15))),
language = "spanish",
analysis_detail = "standard",
only_removed = TRUE
),
verbose = TRUE
)
str(res_ai$conceptual_analysis, max.level = 1)
} # }