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.