Oblique rotation as an option, with partialled edge coefficients and
within-level factor correlations in tidy(). Also fixes to the ESEM engine,
and documentation rewritten in plain English.
cor = "spearman" and missing = "fiml" now stops with an
error. Before this fix, the fit ran, but x$r held lavaan's Pearson FIML
matrix while the object reported cor = "spearman". All edges were Pearson
under a Spearman label. FIML estimates a multivariate-normal (Pearson)
matrix on every engine, so it now requires cor = "pearson" for ESEM, as
it already did for PCA and EFA. To keep a Spearman basis, use
missing = "pairwise" or "listwise". The warning for ESEM with
cor = "spearman" now says that lavaan fits Pearson covariances of the raw
data. Before, it said "Pearson-ML model", which was wrong under WLSMV and
ULSMV.Oblique rotation as an option. ackwards() gains a rotation
argument. The default stays "varimax", and default numerical output does
not change (PCA and EFA factor_cor gain row and column names, see
below). With engine = "pca" or "efa", it also accepts "oblimin" and
"promax". The GPArotation package (now in Suggests) is needed for
"oblimin" with these engines, and for "promax" with engine = "efa". With
engine = "esem", it also accepts "oblimin" and "geomin". Under an
oblique rotation, each level stores the engine's factor correlation. The
scores keep that correlation: EFA and ESEM use the oblique form of the ten
Berge weights, and PCA uses the oblique components. The variance values
follow psych's convention for correlated factors. Each edge r is then a
total correlation. Primary parents, sign alignment, and the diagram still
use r, and beta in tidy() gives the partialled coefficient. The fit
prints a message that says so, and print(), summary(), and autoplot()
label it. prune() warns that its thresholds were set for varimax.
boot_edges() refits each replicate with the object's rotation. If that
rotation needs GPArotation, it checks for the package first. A PCA level whose
oblique rotation fails, or raises an error, ends the hierarchy at the level
before it, because PCA rotates once. EFA rotates from several random
starts, so its non-convergence warnings are shown and the level is kept.
ESEM counts its failed starts, as the first entry under Bug fixes
describes. An EFA level whose final rotation step failed still ends the
hierarchy. If the installed psych rotates from one start, any EFA rotation
warning ends it too. Set seed to reproduce an oblique EFA fit or any ESEM
fit exactly. A new test checks the oblique results against Forbes's
reference implementation on her three simulation studies.
Within-level factor correlations are carried and shown. Every engine
now stores the factor correlation it reports for each level. That matrix
is permuted and sign-flipped in step with the loadings, in place of a
fixed identity matrix. The new table tidy(x, what = "factor_cor") has
one row per pair of factors within a level. Its columns are level,
factor_a, factor_b, and cor. The summary() output prints a
"Within-level factor correlations" block only when some pair is
correlated, which means a within-level correlation above 1e-8 in size.
Under the default varimax rotation every within-level correlation is 0, so
no number changes and the block is absent.
Partialled edge columns. The edge table from tidy() gains beta
beside r, and the variance table gains r2. The beta column is the
standardized regression weight of the child factor on all factors of the
parent level together. It removes the part of r that the other factors
at the parent level share. The r2 column is the share of a factor's
score variance that all factors of the level just above account for
together. It is NA at level 1. Under varimax beta equals r, and
r2 equals the sum of that factor's squared r values from the level
above. The two pairs of quantities come apart only when the factors within
a level are correlated. If a level's within-level score correlation
cannot be inverted, both are NA, with a warning.
tidy(what = "edges") now warns when the within-level score correlation
of a level is nearly singular, meaning its smallest eigenvalue is below
1e-2. The factors at that level are then close to collinear, so the
partialled beta for edges from that level is unstable. beta is still
reported, and the warning names the level. The package chose this cutoff
as a numerical guard. It is not a published rule. tidy(what = "variance")
and summary() do not raise this warning.
PCA and EFA factor_cor matrices now carry the level's factor labels as
row and column names, as the ESEM ones already did.
ESEM now counts failed rotation starts. At each rotated level, lavaan keeps the best of 30 random starts (its default). In 0.2.0, ESEM showed no rotation warning, and it kept the level even when no start converged. Now a warning gives the count of starts that did not converge (for example, "3 of 30"), and the level is kept. If no start converged, the kept rotation did not converge either. The hierarchy then ends at the level before it with a warning. This holds under every ESEM rotation, varimax included. Fits where at least one start converges keep the same levels and values as before.
ESEM with ULSMV on continuous items now fits under the default
missing = "pairwise". Before this fix, ackwards() stopped at level 1
with a lavaan error, complete data included. lavaan 0.7.2 efa() fails for
ULSMV on continuous items when it uses all available cases. That setting
now fits the complete rows, as ML and MLR do, and the edges still come
from the pairwise correlation matrix. ULSMV on cor = "polychoric" and
WLSMV are unchanged. The help for missing in ackwards() now says which
estimators use all available cases. It no longer calls WLSMV and ULSMV
ordinal-only, and it names the $meta fields that record the setting. The
estimator table in the engines article no longer calls them ordinal-only
either.
Which matrix an edge is exact for. Each between-level edge is implied
by the correlation matrix the fit uses, and it is exact for that matrix.
The package description, the README, the help for ackwards(), tidy(),
and prune(), and five articles now say so. The five articles are the
introduction, engines, ordinal, recommended-workflow, and Forbes articles.
Scores computed from the observed items do not in general reproduce the
edges when that matrix is polychoric or Spearman. The same holds when the
matrix comes from incomplete data, by pairwise deletion or by
full-information maximum likelihood (FIML). The ackwards() help names
the two ESEM settings in which lavaan fits a matrix other than the stored
x$r. With cor = "spearman" and missing = "pairwise" or
"listwise", lavaan fits Pearson covariances of the raw data. With
cor = "pearson" and missing = "pairwise" on data with missing values,
ML and MLR fit the complete rows, and WLSMV fits the pairwise covariances.
The package description no longer says that edges can come from
materialized scores, because no exported function computes them that way.
It credits Waller (2007) with the closed-form result for principal
components only. No code or result changed.
Where the redundancy chase stops, in the prune() help. Under the
default redundancy_criterion = "direct", the chase stops at the first
ancestor level that fails the criterion. A level further up that passes
again does not restart it. The help now says so, and says that this stop
follows Forbes's (2023) reference code, ChaseCorrPaths. To bridge such a
gap by your own judgment, pass the factor to drop to manual. A
factor that no rule flagged then gets prune_reason = "manual", and manual adds no row to
x$prune$chains. A new test shows this on component m7f2 of the
forbes2023 example. The behavior and results of prune() did not
change.
Varimax and oblimin side by side in the engines article. The section on
orthogonal and oblique rotation now fits EFA to the BFI-25 twice, with
varimax and with oblimin, in place of the simulated example. It shows the
oblimin factor correlations within each level. It matches each oblimin
factor to a varimax factor by Tucker's congruence of loadings. After the
match, the two primary-parent trees are the same. It lists
the secondary edges that reach the display cutoff under oblimin, with r
beside beta, and draws both diagrams. No package function or its output
changed.
Plain-English documentation. The README, the package description, the help pages, and all nine articles were revised so that a reader outside the field can follow them on one read. Sentences are shorter, and each statistical term is explained in plain words where it is first used. No function, argument, default, or example changed. The code in the articles and the results they show did not change either. The settings that reproduce Forbes (2023), the Pearson default with its ordinal-data warning, and the descriptive reading of the hierarchy are stated as before.
New publication-figure controls for autoplot(), secondary correlation edges
in the pruned view, a near-redundant band in prune(), and cleaner layouts for
deep hierarchies. Also fixes an error on degenerate hierarchies, and a crash in
the package's own test suite — both of which surfaced only on particular
numerical platforms after 0.1.1 was published.
Publication-figure controls for autoplot(). Three additions make
hand-tuned figures easier. show_items = TRUE lists the salient items (top
n_items by |loading|, via the same extraction as top_items()) beneath
each deepest-level factor box, so a figure shows what each most-granular factor
is made of. node_width / node_height now also accept a named vector
keyed by factor ID, sizing individual boxes to fit long manual labels. And
order = (also on ba_layout()) fixes the deepest level's left-to-right order
by hand — every factor above stays centred over its primary children, so any
arrangement of the hierarchy is reachable from the leaf order. All are opt-in;
defaults are unchanged. The visualization vignette demonstrates each.
autoplot(drop_pruned = TRUE) can now draw secondary correlation edges.
A new show_secondary = TRUE adds the between-level correlations the pruned
view otherwise hides — every kept cross-level factor pair with
|r| >= cut_show that is not the single strongest-ancestor primary edge,
including a factor's weaker second parents and direct skip-level
correlations (a skip-level |r| is its own non-transitive fact). They render
in a channel deliberately distinct from the primary edges — dimmed and thinner,
drawn beneath them — while still inheriting the sign encoding (sign_by), so
the sign color/linetype is never conflated with the secondary channel. Default
FALSE leaves the pruned view unchanged. The visualization vignette
illustrates the new argument.
prune(x, "artifact") now reports a near-redundant band. A new
x$prune$near_redundant table flags cross-level factor pairs that sit just
below the redundancy thresholds — where prune(x, "redundant") drops full
redundancy (|r| >= redundancy_r), the band surfaces the messier candidates a
hair under it (e.g. |r| = 0.89 / phi = 0.94), which Forbes (2023) treats as
the main use of the artifact flags. A pair is flagged when its direct
(skip-level) |r| or its Tucker phi falls within the new near_margin
argument (default 0.1) below the corresponding threshold, and the pair is not
itself fully redundant. The band is report-only — no factor is dropped on its
basis. Under EFA/ESEM, redundancy_phi now auto-resolves in artifact mode too
(announced via cli), so the phi band is active; under PCA only the |r| band
applies. The Forbes-extension vignette's artifact example now illustrates a
genuinely near-redundant pair rather than one redundancy already drops.
Cleaner deep-hierarchy diagrams (k >= 10). ba_layout() now orders each
level by a traversal of the primary-parent forest, laying every subtree out as
a contiguous block. This drives primary-tree edge crossings to zero in deep
hierarchies (the "bent levels" the previous single-pass ordering left behind —
e.g. 3 crossings down to 0 on the 155-variable AMH example at k_max = 10),
with no change to shallow layouts and the primary-child x-placement unchanged.
Separately, autoplot(show_r = TRUE) now dodges overlapping edge-correlation
labels apart so dense diagrams stay legible. The layout stays fully
deterministic.
print() for suggest_k() now renders an aligned criteria table. The
per-criterion evidence prints as a column-aligned grid (one row per k, one
column per requested criterion) with a header row and a glyph legend, instead
of the previous per-k concatenated lines. Numeric columns are right-aligned so
the optimal-k star never shifts the decimals, and the "retained" tick is drawn
in text presentation so terminals that render it as a wide emoji keep the
columns aligned. The returned object and all criteria values are unchanged;
only the printed layout is new.
Clarified documentation (no behavior change). The ?prune help and the
bass-ackward vignette now explain the default redundancy_criterion = "direct"
as a star anchored on the chain's deepest factor — each ancestor correlates
directly with that leaf — distinct from adjacent-hop chaining and from an
all-pairs screen. The ?ackwards rationale for varimax now states the reason
correctly: the between-level edge algebra is exact for any linear scoring, so
orthogonality is an interpretive choice (it keeps within-level factors
uncorrelated, so between-level edges are not confounded by within-level factor
intercorrelation), not a numerical necessity. The artifact-mode discussion now
frames automated flags as removing investigator degrees of freedom, leaving
only the substantive drop decision to the researcher.
Fixed an error when a level of the hierarchy is degenerate. If an
extraction produced a factor whose scores carry no variance — reachable when
k_max exceeds what the data can identify — ackwards() aborted with an
opaque invalid subscript type 'list' message instead of handling it. Such a
level is now recorded, warned about, and skipped, with every level above it
kept, which is how a non-converged level has always been treated. The failure
was platform-dependent: the same fit is merely ill-conditioned under most
numerical libraries and fully degenerate under others, so it surfaced only on
some check platforms (ATLAS and no-long-double builds). A hierarchy left with
fewer than two usable levels now reports that directly.
The package's own tests no longer require two identically seeded
boot_edges() runs to agree bit-for-bit. A multi-threaded linear-algebra
library may reassociate its reductions between runs, which moves aggregated
bootstrap quantities in the last unit of precision (~1e-16, seen on ATLAS
builds); the test now allows that while still holding the seed to a far
tighter standard than any real reproducibility failure would pass. Nothing
user-facing changed.
Fixed a crash in the package's own test suite on macOS. Two tests set a
forking parallel plan (future::multicore) to verify that parallel and
serial fits agree exactly. Forking does not mix with the multi-threaded
numerical libraries R's linear algebra runs through — ?mclapply warns against
the combination — and R segfaulted inside the forked worker on one of CRAN's
macOS check flavours. Both tests now skip macOS and continue to run everywhere
else. Nothing user-facing changed: ackwards() and boot_edges() never set a
parallel plan themselves — the default plan is sequential, and choosing one is
the caller's business.
CRAN resubmission of the first release, addressing reviewer feedback on the 0.1.0 submission; also picks up everything added since that submission.
Fixed compatibility with lavaan 0.7. lavaan 0.7 renamed its
sample-statistics slot argument (breaking the ESEM engine's multi-level
reuse of anchor-level sample statistics — every level beyond k = 1 failed
to build) and now requires an explicit ordered = FALSE to use WLSMV/ULSMV
with continuous data. The ESEM engine detects the installed lavaan's
argument vocabulary and works with both lavaan >= 0.7 and >= 0.6-13.
New vignette: "Reproducing Forbes (2023): The AMH Applied Example"
(vignette("ackwards-forbes2023")). A full worked reproduction of the
paper's 155-variable applied example on the bundled forbes2023 dataset:
the 10-level hierarchy, skip-level correlations, the redundancy chase
(including where the default direct criterion and the adjacent opt-in
disagree), and the pruned-factor diagram in the paper's publication style.
Corrected the Forbes (2023) article title in the forbes2023 help page
("bass-ackward method", per the published title).
Corrected the n_obs advisory for PCA on correlation-matrix input. The
message (and the n_obs help text) claimed supplying n_obs would enable
chi-square/RMSEA/TLI, but the PCA engine's level fit is eigenvalue-based and
never computes them; n_obs is recorded in the result metadata and feeds
the N-based sampling-adequacy checks only. The message now says so. No
behavior changes.
Completed the Goldberg (2006) reference to its full published title ("Doing
it all Bass-Ackwards: The development of hierarchical factor structures from
the top down") in the ackwards() help page and four vignettes.
Corrected the historical citations for comparability()'s split-half
benchmarks. The .90 replication threshold traces to Everett (1983) and
to its use in Goldberg's lexical research program by Saucier (1997) and
Saucier, Georgiades, Tsaousis, and Goldberg (2005) — not to Goldberg
(1990), which contains no split-half analyses and is no longer cited for
this purpose. The .95 reference line is now sourced to Lorenzo-Seva and
ten Berge (2006). Affects the roxygen help page, print()/autoplot()
footer text, the README, and the replicability-workflow vignette; no
behavior changes.
Sourced suggest_k()'s k-selection guidance. The consensus-range stance
now cites Lim and Jahng (2019) with Achim's (2021) counterpoint, and the
"PA-PC tends to overextract" note cites Saucier (1997) alongside Forbes
(2023), in both the help page and the suggest-k vignette; no behavior changes.
label_template() now returns its scaffold visibly (behavior change).
The named character vector carries class "ackwards_labels", and the
editable c(...) literal is rendered by its print() method instead of
being written to the console unconditionally. A top-level call looks the
same as before; assigning the result (labs <- label_template(x)) or
passing it inline (e.g. inside autoplot()) is now silent, per CRAN
policy on console output.
DESCRIPTION now spells out principal component analysis (PCA), exploratory factor analysis (EFA), and exploratory structural equation modeling (ESEM) per CRAN feedback.
New bundled dataset forbes2023. The 155-variable "Assessing Mental Health"
Spearman correlation matrix that forms Forbes's (2023) applied example is now
exported, so ackwards(forbes2023, k_max = 10) reproduces her worked hierarchy
directly. It joins bfi25 and sim16. The matrix is redistributed under
CC-BY 4.0 with attribution to M. K. Forbes (see LICENSE.note).
prune("redundant") gains redundancy_criterion, defaulting to "direct"
(behavior change). Redundancy chains are now traced by the direct
(skip-level) correlation between a factor and each ancestor level — Forbes's
(2023) actual ChaseCorrPaths rule — rather than the previous adjacent-hop
walk. Because correlation is non-transitive, the two can differ in deep
(many-level) hierarchies: on shallow ones (e.g. the bundled sim16) results
are unchanged, but a factor can now be flagged redundant with an ancestor it
correlates with directly even if an intermediate step is weak (and vice
versa). Pass redundancy_criterion = "adjacent" for the old behavior. This
makes prune("redundant") reproduce Forbes's published applied example
exactly. print() and summary() name the active criterion. Note that under
"direct", a chain's r_to_prev column reports the adjacent-level
correlation and can sit below redundancy_r (membership is set by the direct
skip-level link; see ?prune).
Validation. The Forbes (2023) fidelity suite now also reproduces her
155-variable "Assessing Mental Health" applied example (k_max = 10), not just
the three simulation studies: between-level correlations match her reference
implementation to 1.3e-14 across all 45 level-pairs, loading congruences agree
within her two-decimal rounding, and her redundancy chase is reproduced for all
54 components. The published matrix ships as a test fixture under CC-BY 4.0
(see LICENSE.note).
suggest_k() now reads Comparison Data (CD) results from EFAtools >= 0.8.0,
which restructured CD()'s return value (the per-iteration RMSE matrix moved
from the top-level RMSE_eigenvalues field into results[[1]]$rmse_eigenvalues).
Without this, the CD criterion and its autoplot() panel silently dropped out
when a current EFAtools was installed. Older EFAtools versions still work.
Console output consistency. summary()'s per-level fit-index pass/fail
mark is now the same terminal-adaptive cli glyph as print()'s convergence
mark (a tick/cross that degrades to v/x in a non-UTF-8 console), replacing
a hard-coded Unicode ✔/✘. And print()'s cumulative-variance percentages
now carry a fixed single decimal (e.g. 20.0%, where a whole-number value
previously printed as 20%), matching summary(). Cosmetic only; the
reported values are unchanged.
First public release. ackwards implements Goldberg's (2006) bass-ackwards
method and its modern extensions: it fits factor solutions at every level from 1
to k and characterizes the hierarchy through the between-level factor-score
correlations that connect them. Initial features, roughly in order of importance:
ackwards() — fit the hierarchy with a PCA, EFA, or ESEM engine;
between-level edges from Waller's (2007) exact W′RW algebra.suggest_k() — bracket a plausible depth range from five retention
criteria (parallel analysis, MAP, VSS, Comparison Data).comparability() — gate hierarchy depth on split-half replicability
(Everett 1983; Goldberg 1990).factorability() — screen a dataset (or correlation matrix) before you
fit: Kaiser-Meyer-Olkin sampling adequacy (overall and per item), Bartlett's
test of sphericity, the N:p ratio, and the Ledermann bound on identifiable
factors, reported as numbers-and-bands rather than pass/fail. ackwards()
runs a light version internally and warns only at the consequential extreme
(k_max above the Ledermann bound for EFA/ESEM, or poor sampling adequacy).pairs = "all" skip-level edges, prune() for
redundant/artifactual factors, and boot_edges() bootstrap edge CIs.cor = "polychoric" (WLSMV for ESEM), check_items()
pre-analysis screening, and near-singularity diagnostics.top_items() for reading factors;
label_template()/set_factor_labels()/factor_labels() to attach persistent
substantive names shown across print(), summary(), autoplot(), tidy(),
and top_items(); augment()/predict() for factor scores, in and out of
sample.autoplot() hierarchy diagrams and tidy()/glance()/summary().bfi25 (ordinal Big Five) and sim16 (continuous), plus
eight vignettes.Beyond base R, psych is the only hard dependency; lavaan, ggplot2, and
others are optional.