Skip to contents

This guide shows how to put two separately ranked sets of writing samples on a common scale using comparisons you already have. It uses small, invented data and makes no provider requests. You need only pairwiseLLM for the runnable examples.

The reference set is the hub and the other set is the spoke. Their original, separate rankings are called Phase A. Linking them is called Phase B. Read the linking overview for help choosing inputs, or the design guide for the statistical models.

Start with the samples and their comparisons

Each set has two samples. The IDs below identify samples within their own set. Add unique global_item_id values if you will later reuse a rubric calibration.

library(pairwiseLLM)
hub <- list(set_id = "hub", items = data.frame(item_id = c("h1", "h2")))
spoke <- list(set_id = "spoke", items = data.frame(item_id = c("s1", "s2")))
cross <- data.frame(
  observation_id = c("cross-1", "cross-2"),
  A_set = "hub", A_item = c("h1", "h2"),
  B_set = "spoke", B_item = c("s1", "s2"), y_A = c(0L, 1L)
)

Each row describes one judgment: y_A = 1 means sample A won and y_A = 0 means sample B won. Retain the order in which samples were presented. Give a new judgment a new ID even when the same pair is compared again. Keep any comparisons reserved for checking accuracy outside this table.

For this example, assume there is no presentation-order bias or random lapse. For an analysis, use the shared judge settings estimated in Phase A; keep them fixed when linking.

judge <- list(beta = 0, epsilon = 0, model_variant = "btl",
  link = "logit", source = "invented example: no bias or lapses")

Choose a method and fit the first comparison

There is no default method. We start with E1 (fixed_shape_offset) to illustrate the workflow. It keeps the original within-set score differences fixed and estimates how much to shift the spoke scores.

phase_a <- list(
  hub = list(points = c(h1 = -1, h2 = 1)),
  spoke = list(points = c(s1 = -.5, s2 = .5))
)
input <- prepare_link_input("fixed_shape_offset", hub, spoke, phase_a,
  cross[1, ], judge)
state <- start_link_session(input)
print(state)
## Link hub -> spoke: fixed_shape_offset v1 [active]
##   Offset: 3.47948 (SD 3.36821); cross_set; valid: TRUE
##   Uncertainty: offset_only_conditional_on_fixed_shapes
##   Phase A: points/points payloads 2/2; raw rows used 0/0; cross rows 1
## # A tibble: 4 × 12
##   set_id item_id global_item_id theta_link_mean theta_link_sd theta_link_lower
##   <chr>  <chr>   <chr>                    <dbl>         <dbl>            <dbl>
## 1 hub    h1      NA                       -1             0               -1   
## 2 hub    h2      NA                        1             0                1   
## 3 spoke  s1      NA                        2.98          3.37            -2.48
## 4 spoke  s2      NA                        3.98          3.37            -1.48
## # ℹ 6 more variables: theta_link_upper <dbl>, theta_link_eap <dbl>,
## #   estimator_id <chr>, uncertainty_scope <chr>, rank_link <dbl>,
## #   link_spoke_id <chr>

Higher theta_link_mean values mean stronger estimated writing on the chosen trait. theta_link_sd describes uncertainty; it is not a rubric grade. For E1, this uncertainty concerns only the shift between sets. The earlier scores are treated as fixed, so the hub’s zero uncertainty here does not mean its original scores were known perfectly.

theta_link_eap is another name for theta_link_mean, retained for compatibility. E1 reports posterior averages. E2 and the usual E3 fit report the most probable joint scores, with approximate uncertainty. See the design guide before comparing uncertainty across methods.

Save your work and add the next comparison

path <- tempfile(fileext = ".rds")
save_link_session(state, path)
restored <- load_link_session(path, input = input)
stopifnot(identical(state, restored))

next_input <- prepare_link_input("fixed_shape_offset", hub, spoke, phase_a,
  cross, judge)
continued <- resume_link_session(restored, next_input)
summary(continued)[c("estimator_id", "delta_spoke_mean", "delta_spoke_sd",
  "phase_b_active_edges_used", "uncertainty_scope", "fit_valid", "status")]
## # A tibble: 1 × 7
##   estimator_id       delta_spoke_mean delta_spoke_sd phase_b_active_edges_used
##   <chr>                         <dbl>          <dbl>                     <int>
## 1 fixed_shape_offset        -4.71e-18           1.68                         2
## # ℹ 3 more variables: uncertainty_scope <chr>, fit_valid <lgl>, status <chr>
unlink(path)

delta_spoke_mean is the estimated shift from hub to spoke; positive values put the spoke higher. delta_spoke_sd describes uncertainty in that shift. phase_b_active_edges_used counts judgments used for fitting, including legitimate repeated judgments. Check fit_valid and the uncertainty description before interpreting the scores.

When adding data, supply all previous rows in their original order followed by the new rows. Keep the same Phase A inputs, method and settings. Reusing unchanged input returns the saved fit without fitting again. Start a new session if you need to change those inputs or settings. adaptive_get_logs(continued) provides the record of the fits made along the way.

Use saved uncertainty with E2

E2 (gaussian_posterior_bridge) uses joint posterior draws from each Phase A fit. Each row is one plausible set of scores; columns identify samples. The tiny matrices below are invented to demonstrate the required format, not recommended sample sizes.

hub_draws <- cbind(h1 = c(-1, -2, -.5, -1.5), h2 = c(1, 2, .5, 1.5))
spoke_draws <- cbind(s1 = c(-.3, -.7, -.4, -.6), s2 = c(.3, .7, .4, .6))
e2_input <- prepare_link_input("gaussian_posterior_bridge", hub, spoke,
  phase_a = list(hub = list(draws = hub_draws), spoke = list(draws = spoke_draws)),
  cross = cross, judge = judge)
e2_fit <- fit_link(e2_input)
e2_fit$offset
## $delta_mean
## [1] -1.562499e-13
## 
## $delta_sd
## [1] 1.517156
## 
## $delta_lower
## [1] -2.973571
## 
## $delta_upper
## [1] 2.973571
## 
## $identification
## [1] "cross_set"

E2 can update both sets’ scores and the shift between them. It needs the joint draws to retain relationships among uncertain scores. Separate standard errors cannot replace them. It does not fit the original Phase A comparisons a second time.

Return to the original comparisons with E3

E3 (joint_offset) fits the original within-set comparisons together with the between-set comparisons, using each judgment once. It requires compatibility with the original Phase A model, including its prior assumptions. Saved scores alone are insufficient. These invented within-set rows use the same format as cross.

within_hub <- data.frame(observation_id = c("hub-1", "hub-2"),
  A_set = "hub", A_item = "h1", B_set = "hub", B_item = "h2", y_A = c(0L, 1L))
within_spoke <- data.frame(observation_id = c("spoke-1", "spoke-2"),
  A_set = "spoke", A_item = "s1", B_set = "spoke", B_item = "s2", y_A = c(0L, 0L))
e3_input <- prepare_link_input("joint_offset", hub, spoke,
  phase_a = list(hub = list(observations = within_hub),
    spoke = list(observations = within_spoke)), cross = cross, judge = judge)
e3_fit <- fit_link(e3_input)
e3_fit$offset
## $delta_mean
## [1] 0
## 
## $delta_sd
## [1] 1.360828
## 
## $delta_lower
## [1] -2.667173
## 
## $delta_upper
## [1] 2.667173
## 
## $identification
## [1] "cross_set"

The usual E3 fit needs no sampler. An optional MCMC engine provides a more expensive statistical audit and requires CmdStan. It must be requested explicitly; see ?fit_link and the design guide. It is not selected automatically when a fit fails.

The three examples illustrate input formats. Their invented Phase A summaries were not estimated from the same comparisons, so their numerical results should not be used to judge which method works best.

For rubric scoring, the hub can be an import-ready adaptive Phase A result or a standalone Bayesian ranking saved with prepare_linked_rubric_reference(). The rubric guide shows how to prepare and save the latter, including rankings based on pooled historical comparisons.

A prepared reference provides the matching hub inputs for each method:

Method Hub input
E1 list(points = reference$points, source = reference$source)
E2 list(draws = reference$posterior_draws, source = reference$source)
E3, including its MCMC engine list(observations = reference$evidence, source = reference$source)

Always carry reference$source with the hub input. It identifies the frozen reference across methods. The package also checks the actual hub inputs before applying the saved rubric calibration; copying an identifier onto different scores or comparisons will not work.

Here is an E2 example using a prepared reference and an existing target Phase A artifact. active_cross must use the set and sample IDs in these inputs.

standalone_input <- prepare_link_input(
  "gaussian_posterior_bridge",
  hub = reference$hub,
  spoke = list(set_id = target_phase_a$set_id,
    items = target_phase_a$items[c("item_id", "global_item_id")]),
  phase_a = list(
    hub = list(draws = reference$posterior_draws, source = reference$source),
    spoke = list(artifact = target_phase_a)
  ),
  cross = active_cross, judge = reference$judge
)
standalone_link <- fit_link(standalone_input)
target_scores <- predict(standalone_calibration, standalone_link)

Use compatible model settings for the target ranking. reference$judge keeps the shared bias and lapse settings from the reference fit. E3 also requires compatible original prior assumptions, as described in the design guide. You do not need target rubric labels for linking or prediction. E2 and E3 may update hub scores during linking, but the saved rubric calibration stays attached to the original reference. Prediction restores that reference’s score origin; it does not adjust the scores to match the new cohort’s distribution.

Predict a comparison and check the result

To predict a winner, provide sample identities in their presentation order, without an outcome column:

pairs_to_predict <- cross[, c("observation_id", "A_set", "A_item", "B_set", "B_item")]
predict_link(e3_fit, pairs_to_predict)
## [1] 0.5 0.5
e3_fit$diagnostics[c("fit_valid", "failure_code", "uncertainty_scope")]
## $fit_valid
## [1] TRUE
## 
## $failure_code
## [1] NA
## 
## $uncertainty_scope
## [1] "joint_shapes_and_offset"

Predictions are probabilities that A wins. They average over the method’s estimated uncertainty. Keep observed outcomes for checking those predictions in a separate table. An offset fitted without between-set comparisons reflects only its starting assumption and does not establish a link. Failed fits or unavailable uncertainty need investigation before you use the results.

Use saved Phase A results or several spokes

Instead of supplying points, draws or raw observations directly, you can supply list(artifact = artifact) for each set’s Phase A input. Read saved files with readRDS() first. The artifact must contain what the selected method needs and must describe the original Phase A fit. Compatible older Phase A results remain usable; linked Phase B results cannot substitute for them.

For several spokes, prepare one input per spoke and pass the list to start_link_session(). All must share the same hub information, judge settings and estimator. Reports include link_spoke_id: keep the separate hub estimates associated with their spokes. The package does not combine them into one hub fit.

The optional status labels active, probe and frozen help record your workflow. They do not decide when enough comparisons have been collected. A frozen spoke must be explicitly reactivated before adding observations. Automatic Phase B selection and stopping remain unavailable pending validation.

The rubric guide explains how to apply a saved hub calibration to linked spoke scores. Old anchored-joint Phase B sessions must be restarted from compatible Phase A results; they cannot be resumed as E1, E2 or E3.

Dependency upgrades and saved hashes

The package requires rlang >= 1.3.0. That release changed the hash algorithm; hashes created with earlier versions cannot be verified with the new engine. When recorded provenance establishes this incompatibility, loading or validating raises pairwiseLLM_link_hash_incompatibility, identifying the saved scheme, saved rlang version, and current version. A tampered current artifact remains an integrity error. An older artifact without engine provenance cannot establish which version produced its hashes; failed verification does not prove that a dependency upgrade caused it.

Keep the original artifacts and their hashes. To build a new input, supply the original saved arguments, judgments, and compatible Phase A points or draws:

args <- readRDS("original-link-input-arguments.rds")
new_input <- do.call(prepare_link_input, args)
saveRDS(new_input, "rebuilt-link-input.rds")

Preparing this input makes no LLM requests and performs no sampling. It does not repair or resume the old session. Reuse saved original outcomes; new judgments are unnecessary. Do not replace the old input’s recorded hashes with new ones. Old normalized inputs are not a substitute for original Phase A payloads and their recorded centering/source information.

A standalone reference can be prepared again from a saved completed fit and its exact evidence only if the fit-time evidence identity still verifies. That identity also uses rlang hashing, so some older completed fits cannot be reused this way. Investigate with the original producing environment before separately deciding whether to refit. A new E3 result refits the saved outcomes, and choosing E3-MCMC can require expensive sampling. Loading, validation, and input preparation never initiate that work automatically. A rebuilt reference has a new identity; dependent calibration/linking artifacts must be prepared explicitly as well.

Citation

Mercer, S. H. (2026). Guide: Linking Saved Comparisons. In pairwiseLLM [R package vignette]. Comprehensive R Archive Network. https://doi.org/10.32614/CRAN.package.pairwiseLLM