Extracts factor/component solutions at levels 1 through k, then
characterises the hierarchy by computing between-level factor-score
correlations. 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"(ESEM requires raw data), and themissingandcorarguments are ignored. See the Correlation-matrix input section below.- k_max
Maximum number of factors/components to extract. Required; use
suggest_k()if uncertain. Sets the depth of the hierarchy: levels 1 throughk_maxare all extracted and retained. (Note:suggest_k()'sk_maxmeans something related but distinct – the max number of factors/components it 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; recommended for the clinical/HiTOP workflow (Kim & Eaton, 2015; Forbush et al., 2024). Requires lavaan >= 0.6-13.- cor
Correlation basis:
"pearson"(default),"spearman", or"polychoric". For PCA/EFA,"polychoric"computes a polychoric correlation matrix viapsych::polychoric()(requires psych). For ESEM, it triggers WLSMV estimation via lavaan. A cli warning is emitted when ordinal-looking columns are detected andcoris not"polychoric".- fm
Factor extraction method passed to
psych::fa(); only used whenengine = "efa". One of"minres"(default, robust OLS),"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"(robust ML).cor = "polychoric"withestimator = "ML"/"MLR"errors (lavaan itself does not support ML/MLR on ordered indicators);"WLSMV"/"ULSMV"with a continuouscoris allowed (a valid, if atypical, continuous WLS/ADF estimator). Ignored for PCA and EFA engines. The effective value (after auto-selection) is recorded inx$meta$estimator(NAfor PCA/EFA).- missing
How to handle missing item responses. One of:
"pairwise"(default) – use all available observations pairwise. For PCA/EFA this feedsstats::cor(use = "pairwise.complete.obs"). For ESEM with WLSMV/ULSMV (ordinal), lavaan usesavailable.cases, which computes polychoric thresholds and correlations from all rows that contribute to each pair – MCAR-valid and uses the full N. For ESEM with ML/MLR (continuous), lavaan uses listwise deletion internally while edges are computed from a pairwise correlation matrix; this minor inconsistency is documented in$meta. A warning is emitted when incomplete rows are detected."listwise"– only complete rows are used. Reduces 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"– Full Information Maximum Likelihood. Forengine = "esem"(withestimator = "ML"/"MLR"), 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; this requirescor = "pearson"(corFiml estimates a multivariate-normal matrix) and the route is announced via a cli message. Errors for WLSMV/ULSMV and for a non-Pearson PCA/EFA basis. Note: FIML improves estimation under missingness but does not impute item responses; score materialisation (keep_scores = TRUE) still producesNArows for incomplete observations. Seen_obsfor the fit-index sample size on the PCA/EFA path.
- n_obs
Number of observations, or (on the raw-data FIML path) a string selecting which N feeds the fit indices.
Correlation-matrix input: a positive integer. Required for
engine = "efa"(psych::fa()needs N for chi-square / RMSEA / TLI); 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): becausepsych::corFiml()estimates the correlation matrix from incomplete rows,n_obsmay be"total"(default – every row contributing to the FIML likelihood, matching the FIML convention; Enders, 2010) or"complete"(complete-case N, a conservative lower bound). Point estimates (loadings, edges) do not depend on this choice; only the EFA fit indices do, and those are approximate under this two-step (FIML matrix into normal-theory EFA) route regardless of N (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"(default, classic Goldberg – only consecutive levels) or"all"(Forbes extension – every pair of levels, including skip-level correlations).prune()recomputes its own all-pairs edges on demand regardless of this setting, 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 (its error suggests exactly this) – this typically happens when an item has a near-empty response category or when items with unequal category counts produce a sparse cross-cell. 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"– the original Goldberg (2006) method; fastest; never fails to converge; the Waller (2007) algebra is exact for components.rotation = "varimax"– varimax keeps the within-level factors mutually uncorrelated (orthogonal), 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. Matches Goldberg (2006), Kim & Eaton (2015), and Forbush et al. (2024); varimax is the only supported rotation.cor = "pearson"– 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.align_signs = TRUE– unaligned signs make the output unreadable. Anchor: m1f1 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 = FALSE– memory and privacy. Scores are O(n x Sigmak) and often sensitive; 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) the costly sample statistics
lavaan derives from the raw data – thresholds, the polychoric correlation
matrix, and the asymptotic weight matrix – 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 (identical solutions, 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 – 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"(psych needs N for chi-square / RMSEA / TLI); optional for"pca"(stored asNAif omitted; used for the N-based sampling-adequacy checks and result metadata only – PCA computes no N-dependent fit statistics).corargument: ignored – the basis is already determined by the matrix you supply. A warning is emitted if you setcorexplicitly.missingargument: ignored – 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_; 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 – 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 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; do 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 and the loadings/edges rest on a rank-deficient matrix. (For EFA the residual-based fallback inflatesTLI/RMSEA; for ESEMCFIcomes backNA.) Trim redundant items, usemissing = "listwise", or – on the polychoric basis – collapse sparse categories or setcorrect = 0.
Caution – interpret carefully. The solution exists but may be unstable:
A Heywood case (communality
> 1/ negative uniqueness) at a level – check that level's loadings make substantive sense; 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 – fine for a quick look or
suggest_k()screening, but report the final model oncor = "polychoric".
Informational – usually fine. Proceed, just be aware: 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() (Forbes-extension redundancy/artifact flagging, 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 is_primary above_cut
#> 1 m1f1 m2f1 1 2 0.707296113 TRUE TRUE
#> 2 m1f1 m2f2 1 2 0.706917399 TRUE TRUE
#> 3 m2f1 m3f1 2 3 -0.011342395 FALSE FALSE
#> 4 m2f1 m3f2 2 3 0.697092981 TRUE TRUE
#> 5 m2f1 m3f3 2 3 0.716891014 TRUE TRUE
#> 6 m2f2 m3f1 2 3 0.995593231 TRUE TRUE
#> 7 m2f2 m3f2 2 3 0.074615767 FALSE FALSE
#> 8 m2f2 m3f3 2 3 -0.056803218 FALSE FALSE
#> 9 m3f1 m4f1 3 4 -0.016458058 FALSE FALSE
#> 10 m3f1 m4f2 3 4 -0.004812835 FALSE FALSE
#> 11 m3f1 m4f3 3 4 0.663913488 TRUE TRUE
#> 12 m3f1 m4f4 3 4 0.747612767 TRUE TRUE
#> 13 m3f2 m4f1 3 4 0.940647736 TRUE TRUE
#> 14 m3f2 m4f2 3 4 0.031735215 FALSE FALSE
#> 15 m3f2 m4f3 3 4 0.262765620 FALSE FALSE
#> 16 m3f2 m4f4 3 4 -0.212435736 FALSE FALSE
#> 17 m3f3 m4f1 3 4 0.051257892 FALSE FALSE
#> 18 m3f3 m4f2 3 4 0.970778427 TRUE TRUE
#> 19 m3f3 m4f3 3 4 -0.171584098 FALSE FALSE
#> 20 m3f3 m4f4 3 4 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.
