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", faithful to Forbes). 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, where a loading is the correlation between an item and a factor. 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 true correlation between the components themselves, 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, continuing while|r| >= redundancy_rcontiguously. This is Forbes's (2023) publishedChaseCorrPathsrule and the honest way to operationalize "the same construct", because two factor scores share>= redundancy_r^2variance directly. It reproduces her AMH applied example exactly."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
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.
