prune() never removes anything from an ackwards object. It only
annotates factors with pruned and prune_reason flags in x$prune$nodes.
A factor is a summary variable standing in for a group of items that move
together. A redundant factor is one that persists across levels without
changing. Pruning is flag-only and never removes, so the object keeps every
level. Because it is a
separate, cheap step from extraction, you can re-prune with new thresholds
without re-running ackwards().
x <- ackwards(bfi25, k_max = 6, engine = "esem") # expensive
x |> prune("redundant") # cheap, repeatable
x |> prune("redundant", redundancy_r = 0.95) # no re-extractionThe function prune() is an S3 generic (rather than a plain function) so
it coexists
with the prune generics already defined by recursive-partitioning
packages (e.g. rpart::prune) regardless of package load order.
Usage
prune(x, ...)
# S3 method for class 'ackwards'
prune(
x,
rules = "none",
manual = NULL,
redundancy_r = 0.9,
redundancy_phi = NULL,
redundancy_criterion = c("direct", "adjacent"),
min_items = 3L,
orphan_r = 0.5,
near_margin = 0.1,
...
)Arguments
- x
An
ackwardsobject.- ...
Reserved for future methods/arguments.
- rules
Character vector controlling which auto-rules run. Default
"none", which runs no auto rule. Combine it withmanualfor pure manual pruning, or callprune(x)with no arguments to clear any existing pruning. Options:"redundant": identify chains of factors connected by factor-score correlations|r| >= redundancy_r(and optionallyphi > redundancy_phi), usingredundancy_criterion(default"direct", which follows Forbes's reference code). A factor score is each person's estimated standing on a factor. Applies Forbes's (2023) retention rule: keep the bottom node when the chain reaches levelk_max(most specific), and keep the top node otherwise. Flagged nodes getpruned = TRUEandprune_reason = "redundant"inx$prune$nodes."artifact"(or the alias"artefact", normalized to"artifact"): compute Tucker's congruence coefficient (phi) for all cross-level factor pairs and store it inx$prune$phi. Congruence is a 0 to 1 index of how similar two loading patterns are. A loading is the correlation between an item and a factor under varimax, and the pattern coefficient under an oblique rotation. This mode also stores structural signals (few_items,orphan, andsplit_merge) inx$prune$structural, and the near-redundant band (near_margin, see Details) inx$prune$near_redundant. No factors are auto-flagged, because artifact identification requires judgment (Forbes, 2023, Wicherts et al., 2016).
- manual
Character vector of factor labels (e.g.
c("m4f3", "m4f4")) to flag directly, in addition to or instead of an auto rule. Standalone manual pruning is supported:prune(x, manual = c("m4f3")). Unknown labels error. A node already flagged by an auto rule keeps thatprune_reason, and only otherwise-unflagged manual nodes getprune_reason = "manual".- redundancy_r
Scalar in
(0, 1]. Score-correlation|r|threshold for redundancy chains. Default0.9(Forbes, 2023).- redundancy_phi
Scalar in
(0, 1],NULL(default, auto), orNA(explicit opt-out). WhenNULL:x$engine == "pca": no phi filter. PCA is principal component analysis, and a component is a weighted sum of the items. Component scores are determinate, that is, exact linear functions of the data, with no factor-score indeterminacy. So the score correlation|r|is the correlation between the components themselves under the correlation matrix the fit uses, and phi adds nothing that|r|does not already capture.x$engineis"efa"or"esem": automatically set to0.95(Lorenzo-Seva & ten Berge, 2006). EFA is exploratory factor analysis and ESEM is exploratory structural equation modeling. Factor-score indeterminacy off-PCA means|r|alone is liberal, so the conjunctive phi criterion is the conservative default. A cli message announces the resolved value. PassNAto disable phi filtering regardless of engine. Pass a numeric value to override on any engine.
- redundancy_criterion
How redundancy chains are traced. One of:
"direct"(default): chase upward via the direct (skip-level) correlation between a factor and each ancestor level. At each level it takes the factor with the largest direct|r|. It continues while|r| >= redundancy_r, and while phi is aboveredundancy_phiwhenredundancy_phiis set (by you or automatically). The chase stops at the first level that fails. A level further up that passes again does not restart it. This contiguous stop follows Forbes's (2023) reference code,ChaseCorrPaths, and the chase reproduces her AMH applied example exactly. The direct correlation is the honest way to operationalize "the same construct", because two factor scores share>= redundancy_r^2variance directly. To bridge such a gap, judge the case yourself, including which factor of the bridged chain to drop, and pass that factor tomanual. A factor that no rule flagged then getsprune_reason = "manual", and a factor already flagged as redundant keeps that reason.manualadds no row tox$prune$chains."adjacent": trace adjacent primary-parent links only, so each consecutive level must meet|r| >= redundancy_r. This was the pre-M53 default. Because correlation is non-transitive it can both over- and under-flag versus"direct"in deep (many-level) hierarchies, so it is retained only as an opt-in. On shallow or transitive hierarchies the two agree.
- min_items
Minimum number of items for which a factor must be the primary loader (highest
|loading|). Factors with fewer thanmin_itemsprimary items are flaggedfew_items = TRUEinx$prune$structural. Only used whenrulesincludes"artifact". Default3L, because a factor defined by one or two items is under-identified and frequently an extraction artifact rather than a replicable construct (the classic "three-indicator rule", Forbes, 2023, Fig. 2).- orphan_r
Threshold for the
orphanstructural signal. A factor whose maximum adjacent-level|r|(to the immediately shallower and deeper levels) falls beloworphan_ris flaggedorphan = TRUEinx$prune$structural. Such a factor does not connect to the neighbouring solutions and so does not replicate across the hierarchy. Only used whenrulesincludes"artifact". Default0.5, a moderate correlation, because a factor that shares less than a quarter of its variance with every neighbour is a structural outlier worth inspecting.- near_margin
Scalar in
(0, 1]. Width of the near-redundant band reported by"artifact"mode (see Details). A cross-level pair is flagged near-redundant when its direct|r|sits in[redundancy_r - near_margin, redundancy_r)or its Tuckerphisits in[redundancy_phi - near_margin, redundancy_phi). That is, it sits withinnear_marginbelow a redundancy threshold, while the pair is not itself fully redundant. Only used whenrulesincludes"artifact". Default0.1.
Details
Oblique objects. When ackwards() was fit with an oblique rotation,
each edge r is a total correlation, which includes overlap through
correlated factors at the same level. Every rule still runs unchanged: the
edge criteria on r, and the congruence (phi) checks and item-assignment
signals on the loadings, which are then pattern coefficients. A warning names the rules that run and
says that they assume orthogonal levels. Their default thresholds, such as
redundancy_r = 0.9, were calibrated on varimax edges.
The "direct" criterion is a star anchored on the leaf, not a walk.
Take a three-level chain candidate with deepest leaf m3f1 and shallower
factors m2f1 (level 2) and m1f1 (level 1). Under the default "direct"
criterion the chain m1f1 -> m2f1 -> m3f1 forms when both direct-to-leaf
correlations |r(m1f1, m3f1)| and |r(m2f1, m3f1)| meet redundancy_r.
Every member is judged by its own direct correlation to the same deepest
factor, a star centred on the leaf. It does not require the adjacent hop
|r(m1f1, m2f1)| to meet the threshold (that is the "adjacent"
criterion). It also does not screen every ancestor pair against every
other, so there is no all-pairs test. An ancestor can join on a strong
direct link to the
leaf even where its adjacent hop to the next chain member is weak. That is
exactly why r_to_prev (below) can sit under redundancy_r.
Reading x$prune$chains under redundancy_criterion = "direct". The
r_to_prev and phi_to_prev columns report the adjacent-level
correlation and congruence between consecutive chain members, for continuity
of display. But chain membership is decided by the direct (skip-level)
correlation to the chain's deepest factor. A direct chain can therefore
legitimately contain a link whose r_to_prev is below redundancy_r,
because the stronger direct link is what justified it. The endpoint_r
column gives the
direct root-to-leaf correlation as an at-a-glance cross-check. Under
redundancy_criterion = "adjacent", r_to_prev is the criterion and always
meets redundancy_r.
The near-redundant band ("artifact" mode). prune("redundant") drops
full redundancy, meaning pairs at or above the thresholds. Forbes (2023)
uses the artifact flags mainly for the messier band just below them. Say a
pair correlates at |r| = 0.89 and shares a loading pattern at
phi = 0.93. It is not quite redundant, but it may be the same factor
re-rotated (re-oriented without changing fit), so it is worth a second
look. Artifact
mode surfaces this as x$prune$near_redundant. That data frame holds every
cross-level pair that is not itself fully redundant. Such a pair has its
direct (skip-level) |r| or its Tucker phi within near_margin
below the corresponding threshold (redundancy_r or redundancy_phi).
Its columns
are from, to, level_from, level_to, r (direct score correlation),
phi (loading congruence), and the logical band flags near_r and
near_phi. Under
the PCA engine redundancy_phi is NULL (component scores are determinate),
so only the |r| band applies. Under EFA and ESEM redundancy_phi
auto-resolves
to 0.95 (announced via cli) and the phi band is active too. Like phi and
the structural signals, the band is report-only. It never drops or
mutates the kept node set.
References
Forbes, M. K. (2023). Improving hierarchical models of individual differences: An extension of Goldberg's bass-ackward method. Psychological Methods. doi:10.1037/met0000546
Lorenzo-Seva, U., & ten Berge, J. M. F. (2006). Tucker's congruence coefficient as a meaningful index of factor similarity. Methodology, 2(2), 57–64. doi:10.1027/1614-2241.2.2.57
See also
ackwards(), tidy.ackwards() (what = "nodes"),
autoplot.ackwards() (drop_pruned)
Examples
# sim16 has a planted redundant chain + overextraction artifact at k = 5,
# so the prune rules always have a finding to show (and no ordinal warning).
x <- ackwards(sim16, k_max = 5)
xp <- prune(x, "redundant")
#> ℹ Redundancy pruning (direct criterion, |r| ≥ 0.9) flagged 7 nodes.
#> ℹ Nodes are retained in the object; inspect with `x$prune$nodes` and
#> `x$prune$chains`.
xp$prune$nodes
#> id level pruned prune_reason
#> 1 m1f1 1 FALSE <NA>
#> 2 m2f1 2 FALSE <NA>
#> 3 m2f2 2 FALSE <NA>
#> 4 m3f1 3 TRUE redundant
#> 5 m3f2 3 TRUE redundant
#> 6 m3f3 3 TRUE redundant
#> 7 m4f1 4 TRUE redundant
#> 8 m4f2 4 TRUE redundant
#> 9 m4f3 4 TRUE redundant
#> 10 m4f4 4 TRUE redundant
#> 11 m5f1 5 FALSE <NA>
#> 12 m5f2 5 FALSE <NA>
#> 13 m5f3 5 FALSE <NA>
#> 14 m5f4 5 FALSE <NA>
#> 15 m5f5 5 FALSE <NA>
# Re-prune with a new threshold -- no re-extraction needed
prune(x, "redundant", redundancy_r = 0.95)
#> ℹ Redundancy pruning (direct criterion, |r| ≥ 0.95) flagged 6 nodes.
#> ℹ Nodes are retained in the object; inspect with `x$prune$nodes` and
#> `x$prune$chains`.
#>
#> ── Bass-Ackwards Analysis (ackwards) ───────────────────────────────────────────
#> Engine: pca
#> Rotation: varimax
#> Basis: pearson
#> n: 1,000
#> k (max): 5
#>
#> ── 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
#> ✔ k = 5: 5 factors, 70.8% variance
#>
#> ── Edges ──
#>
#> 13 of 40 edges have |r| ≥ 0.3
#>
#> ── Pruning ──
#>
#> Redundancy (direct, |r| ≥ 0.95): 6 nodes flagged
#> ────────────────────────────────────────────────────────────────────────────────
#> Note: Pruning is interpretive relabeling, not re-estimation. Flagged nodes
#> remain in the object; all edges are preserved. Inspect with `x$prune$nodes` and
#> `tidy(x, what = "nodes")`.
#> 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.
# Manual pruning: standalone, or mixed with an auto rule
prune(x, manual = "m4f2")
#>
#> ── Bass-Ackwards Analysis (ackwards) ───────────────────────────────────────────
#> Engine: pca
#> Rotation: varimax
#> Basis: pearson
#> n: 1,000
#> k (max): 5
#>
#> ── 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
#> ✔ k = 5: 5 factors, 70.8% variance
#>
#> ── Edges ──
#>
#> 13 of 40 edges have |r| ≥ 0.3
#>
#> ── Pruning ──
#>
#> Manual: 1 node explicitly flagged (m4f2)
#> ────────────────────────────────────────────────────────────────────────────────
#> Note: Pruning is interpretive relabeling, not re-estimation. Flagged nodes
#> remain in the object; all edges are preserved. Inspect with `x$prune$nodes` and
#> `tidy(x, what = "nodes")`.
#> 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.
prune(x, "redundant", manual = "m4f2")
#> ℹ Redundancy pruning (direct criterion, |r| ≥ 0.9) flagged 7 nodes.
#> ℹ Nodes are retained in the object; inspect with `x$prune$nodes` and
#> `x$prune$chains`.
#>
#> ── Bass-Ackwards Analysis (ackwards) ───────────────────────────────────────────
#> Engine: pca
#> Rotation: varimax
#> Basis: pearson
#> n: 1,000
#> k (max): 5
#>
#> ── 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
#> ✔ k = 5: 5 factors, 70.8% variance
#>
#> ── Edges ──
#>
#> 13 of 40 edges have |r| ≥ 0.3
#>
#> ── Pruning ──
#>
#> Redundancy (direct, |r| ≥ 0.9): 7 nodes flagged
#> Manual: 1 node explicitly flagged (m4f2)
#> ────────────────────────────────────────────────────────────────────────────────
#> Note: Pruning is interpretive relabeling, not re-estimation. Flagged nodes
#> remain in the object; all edges are preserved. Inspect with `x$prune$nodes` and
#> `tidy(x, what = "nodes")`.
#> 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.
