Extracts factor or component solutions at levels 1 through k. A factor
(or component) is a summary variable that stands in for a group of items
that move together. It then characterises the hierarchy by computing the
correlations between the factor scores of different levels. A factor score
is each person's estimated standing on a factor. The "hierarchy" is
descriptive: edges are score correlations, not a fitted higher-order SEM.
Usage
ackwards(
data,
k_max,
engine = "pca",
cor = "pearson",
fm = "minres",
estimator = NULL,
missing = "pairwise",
n_obs = NULL,
align_signs = TRUE,
keep_scores = FALSE,
keep_fits = FALSE,
seed = NULL,
pairs = "adjacent",
cut_show = 0.3,
correct = 0.5,
...
)Arguments
- data
A data frame or numeric matrix of observed variables (items in columns, observations in rows). Alternatively, a pre-computed correlation matrix may be supplied (a square, symmetric, numeric matrix with unit diagonal). When a correlation matrix is supplied,
enginemust be"pca"or"efa", because ESEM (exploratory structural equation modeling) needs raw data. Themissingandcorarguments are then ignored. See the Correlation-matrix input section below.- k_max
Maximum number of factors or components to extract. It is required, so use
suggest_k()if uncertain. It sets the depth of the hierarchy: levels 1 throughk_maxare all extracted and retained. (Thek_maxofsuggest_k()means something related but different. It is the largest number of factors that function evaluates when recommending a depth, not a depth itself. See that function's docs.)- engine
Extraction engine:
"pca"(default),"efa", or"esem"."esem"useslavaan::efa()with rotation-aware SEs and per-level fit indices. ESEM is exploratory structural equation modeling, and it is recommended for the clinical or HiTOP workflow (Kim & Eaton, 2015, Forbush et al., 2024). It requires lavaan >= 0.6-13.- cor
Correlation basis:
"pearson"(default),"spearman", or"polychoric". For PCA and EFA,"polychoric"computes a polychoric correlation matrix viapsych::polychoric()(requires psych). Polychoric correlations estimate the correlation between the continuous traits assumed to underlie ordered responses. For ESEM, it triggers WLSMV estimation via lavaan. A cli warning is emitted when ordinal-looking columns are detected andcoris not"polychoric". Ordinal items have a few ordered categories, such as a 1 to 5 rating.- fm
Factor extraction method passed to
psych::fa(), and used only whenengine = "efa". One of"minres"(the default, an ordinary least squares fit that converges reliably),"ml"(maximum likelihood, gives chi-square fit but converges less reliably at deep levels), or"pa"(principal axis). Ignored forengine = "pca".- estimator
Estimation method for the ESEM engine.
NULL(default) auto-selects:"WLSMV"whencor = "polychoric","ML"otherwise. Pass explicitly to override:"ULSMV"(unweighted WLS),"MLR"(maximum likelihood with standard errors that tolerate non-normal data).cor = "polychoric"withestimator = "ML"/"MLR"errors, because lavaan itself does not support ML or MLR on ordered indicators."WLSMV"/"ULSMV"with a continuouscoris allowed, and gives a valid, if atypical, continuous WLS or ADF estimator. Ignored for PCA and EFA engines. The effective value (after auto-selection) is recorded inx$meta$estimator(NAfor PCA and EFA).- missing
How to handle missing item responses. One of:
"pairwise"(the default) uses all available observations pairwise. For PCA and EFA this feedsstats::cor(use = "pairwise.complete.obs"). For ESEM with WLSMV or ULSMV (ordinal), lavaan usesavailable.cases, which computes polychoric thresholds and correlations from all rows that contribute to each pair. That is valid under MCAR and uses the full N. For ESEM with ML or MLR (continuous), lavaan uses listwise deletion internally while edges are computed from a pairwise correlation matrix. This small inconsistency is documented in$meta. A warning is emitted when incomplete rows are detected."listwise"uses only complete rows. It reduces the data tostats::complete.cases()before fitting, so the correlation matrix, the engine fit, and the edges are all consistent.n_obsin the result reflects the reduced N."fiml"is full information maximum likelihood. Forengine = "esem"(withestimator = "ML"/"MLR"), it passesmissing = "fiml"tolavaan::efa()and derives edge correlations from lavaan's FIML-estimated saturated model. Forengine = "pca"/"efa"(M38), the correlation matrix is estimated viapsych::corFiml()and fed to the usualW'RWalgebra. That route requirescor = "pearson", because corFiml estimates a multivariate-normal matrix, and the route is announced via a cli message. It errors for WLSMV and ULSMV, and for a non-Pearson PCA or EFA basis. FIML improves estimation under missingness but does not impute item responses. So score materialisation (keep_scores = TRUE) still producesNArows for incomplete observations. Seen_obsfor the fit-index sample size on the PCA and EFA path.
- n_obs
Number of observations. On the raw-data FIML path it may instead be a string selecting which N feeds the fit indices. FIML is full information maximum likelihood, which uses every observed value without dropping incomplete rows.
Correlation-matrix input: a positive integer. It is required for
engine = "efa", becausepsych::fa()needs N for chi-square, RMSEA, and TLI. It is optional for"pca"(stored asNA_integer_if omitted). For PCA it is recorded in the result metadata and feeds the N-based sampling-adequacy checks only. PCA level fit is eigenvalue-based and computes no N-dependent fit statistics.Raw data: N is normally taken from
nrow(data)and a numericn_obsis ignored (with a warning). The exception ismissing = "fiml"withengine = "pca"/"efa"(M38).psych::corFiml()estimates the correlation matrix from incomplete rows, son_obsmay be"total"or"complete". The default is"total", meaning every row that contributes to the FIML likelihood, which matches the FIML convention (Enders, 2010)."complete"is the complete-case N, a conservative lower bound. Point estimates do not depend on this choice. Those are the loadings (the correlation between each item and a factor) and the edges. Only the fit indices of EFA (exploratory factor analysis) do. Those indices are approximate whatever N you pick, because of this two-step route: a FIML matrix fed into normal-theory EFA (Zhang & Savalei, 2020). A stringn_obsis accepted only on this path.
- align_signs
Logical. Sign-align factors to primary-parent lineage? Default
TRUE.- keep_scores
Logical. Store factor scores in the result? Default
FALSE(recomputable viaaugment.ackwards()). WhenTRUE, per-observation scores are stored inx$scoresas a named list ofn x k_jmatrices, one per level, standardized by real score SDs (seeaugment.ackwards()).- keep_fits
Logical. Store raw engine fit objects? Default
FALSE. WhenTRUE, the per-level fit objects (psych or lavaan) are stored inx$fitsas a named list indexed by level.- seed
Integer seed for stochastic engines (not used by PCA but captured for reproducibility metadata). Default
NULL.- pairs
Which level pairs to compute edges for.
"adjacent"is the default and is the classic Goldberg choice of consecutive levels only."all"is the Forbes extension, which takes every pair of levels, including skip-level correlations. Theprune()function recomputes its own all-pairs edges on demand whatever this setting is, so pruning does not requirepairs = "all"here.- cut_show
Edges with
|r| >= cut_showare flaggedabove_cutintidy()output. Default0.3.- correct
Continuity correction passed to
psych::polychoric()on the PCA/EFA polychoric path (engine = "pca"/"efa"withcor = "polychoric"). Default0.5(psych's own default), which adds that value to zero cells before estimating thresholds. Setcorrect = 0ifpsych::polychoric()fails on your data, and its error suggests exactly this. That failure typically happens when an item has a near-empty response category, or when items with unequal category counts produce a sparse cross-cell. It is ignored on other paths: ESEM computes its own polychoric correlations inside lavaan, and the Pearson/Spearman bases do not use it.- ...
Reserved for future arguments.
Value
An object of class "ackwards". See print.ackwards(),
tidy.ackwards(), glance.ackwards(), and augment.ackwards() for
output methods.
Defaults and why
engine = "pca"is the original Goldberg (2006) method. PCA (principal component analysis) is the fastest engine and never fails to converge, and the Waller (2007) algebra is exact for components.rotation = "varimax"keeps the within-level factors mutually uncorrelated (orthogonal). A rotation re-orients the factors without changing how well they fit, and varimax pushes each item toward one factor. So each between-level edge reflects only the cross-level relationship. An oblique rotation's correlated within-level factors would leak into the between-level edges and confound the between-level signal that is the method's core output. The closed-formW'RWedge algebra is itself exact for any fixed linear scoring, orthogonal or not, so the choice is interpretive, not a numerical necessity. It matches Goldberg (2006), Kim & Eaton (2015), and Forbush et al. (2024). Varimax is the only supported rotation.cor = "pearson"means no silent basis switching. If your items look ordinal (<= 7 distinct integer values), a cli warning will suggestcor = "polychoric", which is available for all three engines. Ordinal items have a few ordered categories, such as a 1 to 5 rating. Polychoric correlations estimate the correlation between the continuous traits assumed to underlie those ordered responses.align_signs = TRUEbecause unaligned signs make the output unreadable. The anchor is m1f1, which is oriented toward the positive manifold. Each subsequent factor is flipped so its edge to its primary parent is positive.keep_scores = FALSE/keep_fits = FALSEfor memory and privacy. Scores are O(n x Sigmak) and often sensitive, and raw engine fits can be large. Both are recomputable from the storedrmatrix.
Performance (ESEM, large item sets)
The ESEM engine fits a separate lavaan model at every level 1..k_max.
For ordinal data (cor = "polychoric", WLSMV) lavaan derives costly sample
statistics from the raw data. Those are the thresholds, the polychoric
correlation matrix, and the asymptotic weight matrix. They depend only on
the data, not on the number of factors. So they are computed once at
the first level and reused for every deeper level, which gives
identical solutions for much less work. This
matters most when you have many items (hundreds), where recomputing those
statistics at each level dominated the run time.
The per-level model fits are mutually independent and are dispatched through
the future framework when future.apply is installed. By default
the plan is sequential (no behaviour change). To run the levels in parallel,
set a plan once before calling ackwards():
future::plan(future::multisession, workers = 4) # or multicore on Unix
x <- ackwards(items, k_max = 8, engine = "esem", cor = "polychoric")Parallelism pays off when the per-level fits are heavy (large p, several
levels). For small problems the worker startup cost can outweigh it. Results
are reproducible across plans when seed is supplied. PCA and EFA already
compute their correlation matrix once and are unaffected.
Correlation-matrix input
When data is a pre-computed correlation matrix (square, symmetric, unit
diagonal), ackwards() runs entirely from that matrix using the W'RW
algebra, so no raw item responses are needed. This is useful when you have
a published correlation table or a polychoric matrix computed externally.
Constraints and behaviour when a correlation matrix is supplied:
Engine: only
"pca"and"efa"are supported."esem"requires raw data (for lavaan's own polychoric computation, WLSMV estimation, and per-level fit indices) and will error clearly.n_obs: required for"efa", because psych needs N for chi-square, RMSEA, and TLI. It is optional for"pca", where it is stored asNAif omitted and is used only for the N-based sampling-adequacy checks and the result metadata. PCA computes no N-dependent fit statistics.corargument: ignored, because the basis is already determined by the matrix you supply. A warning is emitted if you setcorexplicitly.missingargument: ignored, because missingness was handled when computing the matrix. A warning is emitted if you setmissingexplicitly.Factor scores:
keep_scores = TRUEwill error.augment()andtidy(what = "scores")will also error because individual-level scores require row-level item responses.$corfield: stored asNA_character_, and printed as"(user-supplied matrix)".
When to trust the result
ackwards() raises diagnostics as it fits. They fall into three tiers by
what they mean for whether you should trust and report the solution:
Fatal, so fix before trusting. The result is undefined or rests on a broken correlation matrix:
A constant item (no variance) errors. Drop it (see
check_items()).psych::polychoric()fails, usually because of a near-empty response category. Setcorrect = 0or collapse rare categories.A level fails to converge. The hierarchy is truncated to the deepest level that did converge, and you should not interpret beyond it.
A near-singular correlation matrix (smallest eigenvalue
< 1e-4, recorded inmeta$near_singular/meta$min_eigenvalueand re-surfaced byprint()/summary()) means per-level fit indices and factor scores are unreliable. The loadings and edges then rest on a rank-deficient matrix. (For EFA the residual-based fallback inflatesTLIandRMSEA, and for ESEMCFIcomes backNA.) Trim redundant items, or usemissing = "listwise". On the polychoric basis you can instead collapse sparse categories or setcorrect = 0.
Caution, so interpret carefully. The solution exists but may be unstable:
A Heywood case at a level, meaning a communality
> 1or a negative uniqueness. Check that level's loadings make substantive sense, and consider fewer factors.A near-constant item (one response category dominates) can drive a meaningless factor. Inspect it with
check_items().Ordinal data on a Pearson basis attenuates correlations. That is fine for a quick look or
suggest_k()screening, but report the final model oncor = "polychoric".
Informational, and usually fine. Proceed, just be aware of the pairwise-missing note, a merely sparse (rare-but-present) response category, and the ordinal-detection warning when you did intend Pearson.
References
Goldberg, L. R. (2006). Doing it all Bass-Ackwards: The development of hierarchical factor structures from the top down. Journal of Research in Personality, 40(4), 347–358. doi:10.1016/j.jrp.2006.01.001
Waller, N. G. (2007). A general method for computing hierarchical component structures by Goldberg's bass-ackwards method. Journal of Research in Personality, 41(4), 745–752. doi:10.1016/j.jrp.2006.08.005
Forbes, M. K. (2023). Improving hierarchical models of individual differences: An extension of Goldberg's bass-ackward method. Psychological Methods. doi:10.1037/met0000546
Enders, C. K. (2010). Applied Missing Data Analysis. Guilford Press.
Zhang, X., & Savalei, V. (2020). Examining the effect of missing data on RMSEA and CFI under normal theory full-information maximum likelihood. Structural Equation Modeling, 27(2), 219–239. doi:10.1080/10705511.2019.1642111
Kaiser, H. F. (1958). The varimax criterion for analytic rotation in factor analysis. Psychometrika, 23(3), 187–200. doi:10.1007/BF02289233
Grice, J. W. (2001). Computing and evaluating factor scores. Psychological Methods, 6(4), 430–450. doi:10.1037/1082-989X.6.4.430
Beauducel, A., Hilger, N., & Kuhl, T. (2024). The trade-off between factor score determinacy and the preservation of inter-factor correlations. Educational and Psychological Measurement, 84(2), 289–313. doi:10.1177/00131644231171137
Williams, A. L., Conway, C. C., Olino, T. M., Revelle, W., Zinbarg, R. E., & HiTOP Utility Workgroup. (2025). Testing criterion validity in hierarchical models of psychopathology: Comparison of latent-variable and factor-score approaches. Clinical Psychological Science, 13(1), 128–145. doi:10.1177/21677026231225414
See also
print.ackwards(), tidy.ackwards(), glance.ackwards(),
prune(), which is the Forbes-extension flagging of redundancy (one
factor adding nothing over another) and of artifacts, piped off the
result of this function
Examples
# sim16 is continuous with a known 1 -> 2 -> 4 hierarchy, so the default
# pearson basis is appropriate. For ordinal items (e.g. bfi25), fit on the
# polychoric basis instead -- see `cor = "polychoric"` and the ordinal
# vignette.
x <- ackwards(sim16, k_max = 4)
print(x)
#>
#> ── Bass-Ackwards Analysis (ackwards) ───────────────────────────────────────────
#> Engine: pca
#> Rotation: varimax
#> Basis: pearson
#> n: 1,000
#> k (max): 4
#>
#> ── Levels ──
#>
#> ✔ k = 1: 1 factor, 28.2% variance
#> ✔ k = 2: 2 factors, 46.5% variance
#> ✔ k = 3: 3 factors, 57.5% variance
#> ✔ k = 4: 4 factors, 67.7% variance
#>
#> ── Edges ──
#>
#> 9 of 20 edges have |r| ≥ 0.3
#> ────────────────────────────────────────────────────────────────────────────────
#> Note: This is a series of linked solutions, not a fitted hierarchical model.
#> Cross-level edges are descriptive score correlations. Per-level fit indices
#> (EFA/ESEM) describe how well a k-factor model fits the items at that level --
#> they do not validate the edges or the hierarchy itself.
tidy(x)
#> from to level_from level_to r beta is_primary above_cut
#> 1 m1f1 m2f1 1 2 0.707296113 0.707296113 TRUE TRUE
#> 2 m1f1 m2f2 1 2 0.706917399 0.706917399 TRUE TRUE
#> 3 m2f1 m3f1 2 3 -0.011342395 -0.011342395 FALSE FALSE
#> 4 m2f1 m3f2 2 3 0.697092981 0.697092981 TRUE TRUE
#> 5 m2f1 m3f3 2 3 0.716891014 0.716891014 TRUE TRUE
#> 6 m2f2 m3f1 2 3 0.995593231 0.995593231 TRUE TRUE
#> 7 m2f2 m3f2 2 3 0.074615767 0.074615767 FALSE FALSE
#> 8 m2f2 m3f3 2 3 -0.056803218 -0.056803218 FALSE FALSE
#> 9 m3f1 m4f1 3 4 -0.016458058 -0.016458058 FALSE FALSE
#> 10 m3f1 m4f2 3 4 -0.004812835 -0.004812835 FALSE FALSE
#> 11 m3f1 m4f3 3 4 0.663913488 0.663913488 TRUE TRUE
#> 12 m3f1 m4f4 3 4 0.747612767 0.747612767 TRUE TRUE
#> 13 m3f2 m4f1 3 4 0.940647736 0.940647736 TRUE TRUE
#> 14 m3f2 m4f2 3 4 0.031735215 0.031735215 FALSE FALSE
#> 15 m3f2 m4f3 3 4 0.262765620 0.262765620 FALSE FALSE
#> 16 m3f2 m4f4 3 4 -0.212435736 -0.212435736 FALSE FALSE
#> 17 m3f3 m4f1 3 4 0.051257892 0.051257892 FALSE FALSE
#> 18 m3f3 m4f2 3 4 0.970778427 0.970778427 TRUE TRUE
#> 19 m3f3 m4f3 3 4 -0.171584098 -0.171584098 FALSE FALSE
#> 20 m3f3 m4f4 3 4 0.159752219 0.159752219 FALSE FALSE
glance(x)
#> engine rotation cor k_max n_obs deepest_converged n_edges CFI TLI RMSEA
#> 1 pca varimax pearson 4 1000 4 20 NA NA NA
#> SRMR BIC
#> 1 NA NA
# Correlation-matrix input (PCA engine; n_obs optional)
R <- cor(sim16)
x_R <- ackwards(R, k_max = 4)
#> ℹ `n_obs` not supplied; stored as `NA`.
#> ℹ PCA level fit is eigenvalue-based and does not use N; supplying `n_obs = <N>`
#> records it in the result metadata and enables the N-based sampling-adequacy
#> checks.
print(x_R)
#>
#> ── Bass-Ackwards Analysis (ackwards) ───────────────────────────────────────────
#> Engine: pca
#> Rotation: varimax
#> Basis: (user-supplied matrix)
#> n: NA
#> k (max): 4
#>
#> ── Levels ──
#>
#> ✔ k = 1: 1 factor, 28.2% variance
#> ✔ k = 2: 2 factors, 46.5% variance
#> ✔ k = 3: 3 factors, 57.5% variance
#> ✔ k = 4: 4 factors, 67.7% variance
#>
#> ── Edges ──
#>
#> 9 of 20 edges have |r| ≥ 0.3
#> ────────────────────────────────────────────────────────────────────────────────
#> Note: This is a series of linked solutions, not a fitted hierarchical model.
#> Cross-level edges are descriptive score correlations. Per-level fit indices
#> (EFA/ESEM) describe how well a k-factor model fits the items at that level --
#> they do not validate the edges or the hierarchy itself.
