Skip to contents

What the correction estimates

The integrated CJ workflow explains where bootstrap bias correction fits alongside pair selection, internal reliability, score recovery, and held-out prediction. This guide focuses on the bootstrap interface. The examples require the suggested withr package and are displayed without execution when it is absent.

A Bradley–Terry (BT) score describes relative strength on a log-odds scale. With limited comparisons, an estimator may systematically place scores too far apart or too close together. A parametric bootstrap estimates this bias by simulating new judgments from the fitted scores and fitting the same estimator to each simulated dataset. It makes no provider calls.

Pair selection matters. For a random schedule chosen independently of results, the bootstrap can keep the pairs fixed. For an adaptive schedule, later pairs depend on earlier winners. Each replicate must repeat that selection process using its own simulated winners. Reusing only the observed adaptive pairs would answer a different question.

The function aligns item IDs and centers every score vector to sum to zero. For each item it calculates:

estimated bias = average bootstrap estimate - initial estimate
corrected score = initial estimate - estimated bias

Zero remains a relative reference, not a passing score. The correction can change rankings, and it does not guarantee improvement for every item or application.

A fixed random schedule

This synthetic example uses a connected starting chain and additional random pairs, all chosen before simulating any winners. Alpha adjustment is used here so the example needs no optional fitting engine; the penalty is specified in advance. Firth (engine = "brglm2") is also supported for nonadaptive schedules when that optional package is installed.

ids <- letters[1:4]
truth <- setNames(c(-0.8, -0.2, 0.2, 0.8), ids)
dat <- withr::with_seed(304, {
  extra <- replicate(21, sample(ids, 2))
  pairs <- data.frame(object1 = c(ids[1:3], extra[1, ]),
                      object2 = c(ids[2:4], extra[2, ]))
  p <- plogis(truth[pairs$object1] - truth[pairs$object2])
  pairs$result <- as.integer(runif(nrow(pairs)) < p)
  pairs
})
fit <- fit_bt_model(dat, engine = "alpha", alpha = 0.5, verbose = FALSE)
fixed <- bootstrap_bt_model(fit, mode = "fixed", n_rep = 20, seed = 1304)
fixed$theta
#> # A tibble: 4 × 7
#>   ID    theta_initial bootstrap_mean    bias theta_corrected bootstrap_sd
#>   <chr>         <dbl>          <dbl>   <dbl>           <dbl>        <dbl>
#> 1 a            -0.320         -0.138  0.182           -0.502        0.486
#> 2 b            -0.384         -0.327  0.0569          -0.441        0.367
#> 3 c             0.404          0.240 -0.165            0.569        0.483
#> 4 d             0.299          0.225 -0.0742           0.373        0.565
#> # ℹ 1 more variable: mcse_bias <dbl>
fixed$provenance[c("mode", "seed", "pairing_strategy", "refit", "versions")]
#> $mode
#> [1] "fixed"
#> 
#> $seed
#> [1] 1304
#> 
#> $pairing_strategy
#> [1] "fixed"
#> 
#> $refit
#> $refit$estimator
#> [1] "alpha"
#> 
#> $refit$args
#> $refit$args$alpha
#> [1] 0.5
#> 
#> 
#> 
#> $versions
#>           R pairwiseLLM       stats 
#>     "4.6.1"     "1.6.0"     "4.6.1"

The fitted object supplies both the generating scores and the estimator settings. Each replicate keeps the original ordered pair rows, including any repetitions, and draws fresh winners. Twenty replicates keep this example quick; that number is not a recommendation for a study.

An adaptive p50 schedule

Save the initial state before collecting any outcomes. Here the p50 strategy chooses partners with current TrueSkill win probabilities near one half. The synthetic judge below supplies winners in place of a provider.

ids <- letters[1:6]
truth <- setNames(seq(-1, 1, length.out = 6), ids)
initial <- adaptive_rank_start(ids, seed = 17,
  adaptive_config = list(pairing_strategy = "trueskill_p50"))
judge <- function(A, B, state, ...) {
  p <- plogis(truth[[A$item_id]] - truth[[B$item_id]])
  list(is_valid = TRUE, Y = as.integer(runif(1) < p))
}
# This demonstration design has no Bayesian refit within its 12-comparison budget.
schedule_config <- list(refit_pairs_target = 100L)
observed <- withr::with_seed(7304, adaptive_rank_run_live(initial, judge,
  n_steps = 12, btl_config = schedule_config, progress = "none"))
dat <- adaptive_results_history(observed)
fit <- fit_bt_model(dat, engine = "alpha", alpha = 0.5, verbose = FALSE)
adaptive <- bootstrap_bt_model(fit, mode = "adaptive", n_rep = 4, seed = 2304,
  initial_state = initial, budget = 12, btl_config = schedule_config)
adaptive$theta
#> # A tibble: 6 × 7
#>   ID    theta_initial bootstrap_mean    bias theta_corrected bootstrap_sd
#>   <chr>         <dbl>          <dbl>   <dbl>           <dbl>        <dbl>
#> 1 a            -2.61          -2.14   0.469           -3.08         0.601
#> 2 b            -0.870         -0.197  0.674           -1.54         0.571
#> 3 c            -0.715         -0.648  0.0667          -0.782        0.681
#> 4 d             0.397         -0.453 -0.851            1.25         1.17 
#> 5 e             2.44           1.71  -0.724            3.16         0.489
#> 6 f             1.36           1.73   0.366            0.993        0.395
#> # ℹ 1 more variable: mcse_bias <dbl>
adaptive$replicates[, c("replicate", "success", "n_comparisons", "schedule_digest")]
#> # A tibble: 4 × 4
#>   replicate success n_comparisons schedule_digest                 
#>       <int> <lgl>           <int> <chr>                           
#> 1         1 TRUE               12 8e73059d9ff495e86aa26ef53c6ff117
#> 2         2 TRUE               12 3e80c6940736310370db99ad7add564a
#> 3         3 TRUE               12 a45fe7069b80fe0680ab99a7654b2558
#> 4         4 TRUE               12 005c33660e3e93f0b85b2ddb91fb6208
adaptive$provenance[c("mode", "seed", "pairing_strategy", "refit", "versions")]
#> $mode
#> [1] "adaptive"
#> 
#> $seed
#> [1] 2304
#> 
#> $pairing_strategy
#> [1] "trueskill_p50"
#> 
#> $refit
#> $refit$estimator
#> [1] "alpha"
#> 
#> $refit$args
#> $refit$args$alpha
#> [1] 0.5
#> 
#> 
#> 
#> $versions
#>           R pairwiseLLM       stats 
#>     "4.6.1"     "1.6.0"     "4.6.1"

The initial connected pairs and their presentations stay fixed across replicates. After initialization, each replicate has a new deterministic selector seed and its own simulated outcomes. The selector and state updates are the same ones used by the ordinary adaptive runner. Four replicates here illustrate the workflow only; their Monte Carlo error can be substantial.

The same interface supports trueskill_pollitt, hybrid, and random states. Keep the original scheduling-refit settings when reproducing a design. Hybrid routing can change after Bayesian refits establish a sufficiently identified global scale. Those refits are retained and may require CmdStan. The final bootstrap estimator is a separate choice from the scheduling fitter. An outcome-independent predictive prior may be included in the initial state. Sparse reservoirs restrict selection to their legal pairs; their old winners are discarded and new binary outcomes are simulated.

The budget counts successful comparisons, including the initial connected structure. Statistical stopping is recorded but does not truncate this fixed-budget experiment. If legal selection is exhausted, the replicate fails. Small panels can exhaust hybrid exposure constraints before using every possible pair; the bootstrap does not relax these constraints to fill the budget.

Reading the results and managing cost

bias estimates systematic error under the fitted generating model. bootstrap_sd describes how much the replicate estimates vary. mcse_bias describes simulation error in the estimated bias and usually decreases as the replicate count increases. Neither quantity is automatically a standard error for the corrected score. The function does not provide confidence intervals or calculate corrected scale-separation reliability (SSR).

Inspect n_success, n_failed, and replicates before using a correction. By default every requested replicate must succeed. You may choose a lower min_success explicitly, but excluded failures can change the interpretation: the average then describes successful refits. An unmet threshold raises an error whose result field retains diagnostics, with missing corrected scores. No failed estimates are silently included and no other estimator is substituted.

Each replicate costs a schedule simulation and final refit. All adaptive strategies retain their scheduled native Bayesian fits. Use workers = 2 for parallel execution with the optional future and future.apply packages. Reproducible seeds and ordered aggregation give the same scientific results as serial execution. Avoid oversubscribing CPUs with both bootstrap workers and many sampler chains. Custom callbacks must honor supplied seeds and avoid mutable external state.

The default keep = "summary" discards large replicate states. Use "theta" to retain score draws, or "full" when individual schedules or detailed numerical failure diagnostics need inspection. Detailed failures can contain large matrices. Save the returned object with saveRDS() to retain inputs, settings, seeds, software versions and diagnostics. There is no checkpoint/resume interface; restarting with the same inputs and compatible software repeats the run.

Reusing an external worker pool

For many conditions, a caller-owned pool avoids starting new workers for each bootstrap call. Pass an ordered map function through executor; it receives chunks of replicate indices, the worker function, and its arguments. It must return one result per chunk in the supplied order. It must not retry failed tasks, substitute seeds, or return partial results. Transport failures propagate to the caller. The package still handles and reports ordinary replicate failures.

old_plan <- future::plan("list")
future::plan(future::multisession, workers = 2)
executor <- function(X, FUN, ...) {
  future.apply::future_lapply(X, FUN, ..., future.seed = TRUE)
}
tryCatch({
  # Repeat this call for independent conditions using the same pool.
  pooled <- bootstrap_bt_model(fit, mode = "fixed", n_rep = 24, seed = 1304,
    workers = 2, executor = executor, keep = "summary")
}, finally = future::plan(old_plan))

workers bounds the number of submitted chunks; configure the executor’s actual concurrency consistently. The package leaves pool ownership with the caller. It prepares immutable fitting designs inside each worker chunk and recomputes counts for each replicate. Summary/theta dispatch blocks hold at most eight replicates per worker; full-retention blocks hold at most two. Prepared designs are not included in returned provenance or sent over worker sockets.

An external scheduler may instead dispatch independent conditions, with each condition calling bootstrap_bt_model(..., workers = 1). This avoids nested bootstrap pools and requires no executor argument. Keep each condition’s own master seed and replicate numbering unchanged.

For native Bayesian scheduling, budget outer workers together with concurrent chains and threads per chain. Four total chains need not run simultaneously, but changing chain concurrency requires posterior and trajectory parity checks on the intended platform. A future worker normally advertises one CPU slot; assign any larger per-task allocation explicitly only after reserving that capacity across the whole pool. Do not disable the sampler’s resource checks. The bounded native benchmarking scripts in the source repository demonstrate this allocation and report parent, worker and CmdStan process memory together. At N=229, allow approximately 139 MiB per worker for the retained alpha design alone, in addition to adaptive state, posterior draws and temporary fit memory.

Citation

Mercer, S. H. (2026). Bootstrap bias correction for BT scores [R package vignette]. Comprehensive R Archive Network. https://doi.org/10.32614/CRAN.package.pairwiseLLM