Skip to contents

Attaches nonparametric bootstrap standard errors and percentile confidence intervals to every between-level correlation (edge) of a fitted bass-ackwards hierarchy. Every edge ackwards() reports is a point estimate. This function quantifies the sampling uncertainty of that estimate. That matters most where a hard threshold consumes the estimate. Two such places are prune()'s |r| >= redundancy_r redundancy rule and the Forbes (2023) practice of interpreting the strongest all-pairs edge. A factor is a summary variable standing in for a group of items that move together, and a redundant factor is one that persists across levels without changing.

Usage

boot_edges(x, ...)

# S3 method for class 'ackwards'
boot_edges(x, data, n_boot = 1000L, conf = 0.95, seed = NULL, ...)

Arguments

x

An ackwards object fit with engine = "pca" or "efa" on a "pearson" or "spearman" basis. ESEM objects (exploratory structural equation modeling) are not supported, because refitting n_boot lavaan hierarchies needs its own performance treatment. Polychoric-basis objects are not supported either, because estimating a polychoric matrix in every resample is slow and unstable. Polychoric correlations estimate the correlation between the continuous traits assumed to underlie ordered responses. Objects fit from a correlation matrix are also unsupported, because resampling needs rows.

...

Reserved for future arguments.

data

The raw item data the model was fit on. Required, because the ackwards object deliberately does not store raw data (light core), so it must be re-supplied here. Columns are matched by name against the fit, and a warning is issued if the data do not look like the fit data.

n_boot

Number of bootstrap replicates. Default 1000L. Larger values (2000+) are advisable for published interval endpoints (Efron & Tibshirani, 1993). Each replicate refits the full hierarchy, so cost scales linearly.

conf

Confidence level for the percentile intervals. Default 0.95.

seed

Integer seed for reproducible resampling. NULL (default) uses the current RNG state.

Value

x, invisibly modified: the $boot element is populated with

edges

Data frame with one row per edge, aligned with tidy(x, what = "edges"). The identifier columns are from, to, level_from, and level_to. The estimate columns are r (the full-sample point estimate), se (bootstrap standard error), and lo and hi (percentile interval endpoints). The column n_boot_ok counts the usable replicates for that edge.

n_boot, conf, seed

The request.

After calling boot_edges(), the table tidy(x, what = "edges") gains se, lo, and hi columns. Also, print(x) and summary(x) note the interval coverage.

Details

For each of n_boot replicates, n rows are resampled with replacement. The correlation matrix is recomputed with the same basis and missing-data routine used at fit time, and the full hierarchy is refit. Each replicate level is then anchored to the full-sample solution before its edges are computed. That is, its factors are matched (greedy max-|r| with removal) and sign-oriented against the full-sample factors, on the full-sample correlation matrix. Without anchoring, factor label switching and sign flipping across replicates would corrupt the pooled edge distributions. This is the same matching machinery comparability() uses.

All resample indices are drawn upfront from seed, so results are reproducible and identical whether replicates run serially or in parallel. Replicate fits are dispatched through future.apply when it is installed and the user has set a future::plan(). Otherwise they run serially, as in the ESEM (exploratory structural equation modeling) engine of ackwards().

What the intervals do and do not fix

Per-edge intervals make sampling uncertainty visible: an edge whose interval straddles prune()'s redundancy_r threshold should not be treated as decisively above or below it. They do not correct the selection bias of scanning many edges for the strongest one. The maximum of hundreds of correlations capitalizes on chance even when every individual interval is honest. Treat the intervals as per-edge error bars, not a familywise inference.

Failed replicates

A replicate whose hierarchy fails to converge (in full or at some levels) contributes NA to the affected edges. It is dropped from their distributions, because convergence is data, not an error. The usable replicate count is reported per edge in n_boot_ok, and a message summarises any shortfall.

References

Efron, B., & Tibshirani, R. J. (1993). An introduction to the bootstrap. Chapman & Hall.

Forbes, M. K. (2023). Improving hierarchical models of individual differences: An extension of Goldberg's bass-ackward method. Psychological Methods. doi:10.1037/met0000546

See also

Use prune() for the thresholded rules the intervals contextualise. See comparability() for split-half replicability of the factors themselves, where the sample is split into two random halves and each half is analysed separately. See tidy.ackwards() for the augmented edge table.

Examples

# \donttest{
x <- ackwards(sim16, k_max = 3)
x <- boot_edges(x, sim16, n_boot = 100, seed = 1)
#>  Fitting 100 bootstrap replicates (pca, k = 1-3)...
#>  Fitting 100 bootstrap replicates (pca, k = 1-3)... [1.8s]
#> 
x$boot$edges
#>   from   to level_from level_to           r         se          lo          hi
#> 1 m1f1 m2f1          1        2  0.70729611 0.03334587  0.62894118 0.755307440
#> 2 m1f1 m2f2          1        2  0.70691740 0.03221841  0.65534452 0.777438710
#> 3 m2f1 m3f1          2        3 -0.01134239 0.02483636 -0.08601120 0.007369852
#> 4 m2f1 m3f2          2        3  0.69709298 0.20160298  0.03830045 0.734809599
#> 5 m2f1 m3f3          2        3  0.71689101 0.09283102  0.67793763 0.998542647
#> 6 m2f2 m3f1          2        3  0.99559323 0.09091406  0.69647708 0.999880140
#> 7 m2f2 m3f2          2        3  0.07461577 0.20966388 -0.01269972 0.717035306
#> 8 m2f2 m3f3          2        3 -0.05680322 0.04384198 -0.12671309 0.029286309
#>   n_boot_ok
#> 1       100
#> 2       100
#> 3       100
#> 4       100
#> 5       100
#> 6       100
#> 7       100
#> 8       100
head(tidy(x)) # now carries se / lo / hi
#>   from   to level_from level_to           r        beta is_primary above_cut
#> 1 m1f1 m2f1          1        2  0.70729611  0.70729611       TRUE      TRUE
#> 2 m1f1 m2f2          1        2  0.70691740  0.70691740       TRUE      TRUE
#> 3 m2f1 m3f1          2        3 -0.01134239 -0.01134239      FALSE     FALSE
#> 4 m2f1 m3f2          2        3  0.69709298  0.69709298       TRUE      TRUE
#> 5 m2f1 m3f3          2        3  0.71689101  0.71689101       TRUE      TRUE
#> 6 m2f2 m3f1          2        3  0.99559323  0.99559323       TRUE      TRUE
#>           se          lo          hi n_boot_ok
#> 1 0.03334587  0.62894118 0.755307440       100
#> 2 0.03221841  0.65534452 0.777438710       100
#> 3 0.02483636 -0.08601120 0.007369852       100
#> 4 0.20160298  0.03830045 0.734809599       100
#> 5 0.09283102  0.67793763 0.998542647       100
#> 6 0.09091406  0.69647708 0.999880140       100
# }