Rubric calibration turns writing-quality scores from pairwise comparisons into ordered levels such as “developing”, “proficient” and “advanced”. Comparing samples two at a time is called comparative judgment (CJ). The ordinal methods use human rubric scores to learn where the levels belong on the CJ scale. Percentile scoring instead assigns levels according to proportions you choose.
You need: a completed Bayesian CJ analysis and, for ordinal calibration, human rubric labels for some of its items. You get: a rubric label for each eligible item, full category probabilities for ordinal methods, and diagnostics. The frequentist BT/Elo fits in Getting Started are not accepted calibration inputs.
For example, you might have compared essays for organization and want to report “developing”, “proficient”, or “advanced”. Human scores teach the ordinal model how that trait’s CJ scale relates to those labels. If you have no human scores, percentile levels describe relative standing under your chosen distribution; they do not establish mastery of rubric criteria.
| Available evidence | Method and design | Interpretation |
|---|---|---|
| Completed CJ, no human rubric labels | percentile, same_set |
Levels describe relative standing in this set |
| Human labels for some samples in one completed CJ analysis | ordinal_linear, same_set |
The default calibration learns from those human scores |
| A human-scored reference and new samples | ordinal_linear, linked_anchors |
Reuse the saved calibration after linking the sets |
| A curved relationship between CJ scores and rubric levels | ordinal_monotone, either ordinal design |
A more flexible curve that must be checked on separate human scores |
same_set means that samples with and without human
scores participated in one CJ analysis. Calibration
uses its completed scores; it does not change the pairwise analysis.
Both ordinal methods assume the effect of CJ quality is shared across
rubric boundaries. Allowing a curve does not remove that assumption. You
choose the method; the package does not choose one automatically.
Mercer (2026) compared direct LLM rubric scoring with percentile scoring and human-score calibration using pairwiseLLM. The study assessed 1,714 essays from students in Grades 8–12 on six writing traits. Without human-scored references, percentile scoring placed roughly one fifth of essays in each of five levels. Compared with direct scoring without examples, it better preserved relative differences among essays, but individual scores were farther from human scores. Calibrated CJ scores were also closer to human scores than percentile scores were.
With human-score calibration, CJ agreement changed little between 20 and 80 reference essays. Direct scoring improved with more examples until overall agreement was similar, although the approaches retained different strengths and results varied by writing trait. Calibrated CJ may be more useful when relative differences matter—for example, when examining correlations with external measures. Direct scoring may be more useful when the aim is to place each essay accurately on the rubric, especially when its levels describe specific skills or standards that students should meet (criterion-referenced scores).
These findings concern the study’s particular tasks and model.
Reference and target essays participated in one comparison analysis
(same_set); the study does not establish performance for
separately linked sets or make 20 references a general recommendation.
Equal-sized percentile groups describe relative standing; they do not
establish that essays meet rubric criteria.
Mercer, S. H. (2026). Comparative judgment versus direct rubric scoring with large language models: Effects of human-scored reference samples [Preprint]. SSRN. https://doi.org/10.2139/ssrn.7549240
Use a completed fit_bayes_btl_mcmc() result, a completed
within-set adaptive_rank() result (or its state), or an
import-ready Phase A artifact. Supply the completed result, not just a
table of scores or an unfinished adaptive run. If a fixed-pair analysis
was fitted several times, calibration uses its last fit. All four
Bayesian variants, btl, btl_e,
btl_b, and btl_e_b, use the same calibration
functions. Whether fixed comparisons were collected live or in batches
does not change calibration.
Before calibration, check that the samples are connected through comparisons and that the CJ estimates and diagnostics are adequate for your purpose. Rubric calibration cannot repair a weak ranking. A saved Phase A result being ready to import does not, by itself, show that its original run finished successfully.
For constructing these inputs, see Standalone Bayesian BTL, Guide: Adaptive Pairing, and Guide: Adaptive Linking. Rubric functions do not generate pairs, collect judgments, or control adaptive stopping.
The code below starts from your completed objects and label tables. Those objects are not bundled example data, so dependent chunks are not evaluated during normal documentation builds. No provider calls or CmdStan sampling occur in the rubric examples. The downstream snippets are tested with deterministic completed-result fixtures in the package’s test suite.
pairwiseLLM requires R >= 4.4. Percentile scoring
needs no optional modeling backend. Linear fitting requires
ordinal; saved linear prediction does not. Monotone fitting
requires mgcv >= 1.9-4 and withr; saved
monotone prediction requires mgcv. Install the optional
packages only when you use those methods:
completed_cj below is the completed
organization-specific CJ result. training_labels and
evaluation_labels are disjoint data frames with columns
item_id and rubric_score, optionally
trait. Each ID must occur in that CJ result. Use global IDs
when the adaptive result or artifact provides them; otherwise use the
original fixed-fit IDs. Alignment is by ID, never row position. Omitted
items or NA rubric scores are unlabeled.
A label table has this shape (illustrative IDs only; use IDs from your completed analysis and enough labels to support fitting and validation):
| item_id | rubric_score |
|---|---|
| essay_01 | developing |
| essay_07 | proficient |
| essay_12 | advanced |
training_labels <- read.csv("training_labels.csv", colClasses = "character")
evaluation_labels <- read.csv("evaluation_labels.csv", colClasses = "character")List levels from lowest to highest quality. For text labels, supply
levels explicitly. Numeric scores are ordered from low to
high; ordered factors keep their declared order. Use at least three
levels, commonly three to six. Every intended level must appear in the
training labels. The package stops if a level is missing rather than
silently combining levels. Higher CJ and rubric scores must both mean
better performance.
Choose calibration responses that cover the CJ range, including its extremes, and check category counts. There is no universal sufficient number of labels. Keep evaluation labels separate before fitting: evaluation on training labels measures apparent performance, not held-out accuracy.
if (requireNamespace("ordinal", quietly = TRUE)) {
calibration <- fit_rubric_calibration(
completed_cj, training_labels,
trait = "organization", levels = rubric_levels
)
scores <- predict(calibration)
scores[, c("item_id", "rubric_score", "extrapolated")]
calibration$category_counts
calibration$calibration_range
calibration$transformation
}The model learns from samples with training labels and saves their
score scale for future predictions. Higher CJ scores should favor higher
rubric levels. A warning about a zero or negative slope means this
relationship needs review; the package does not reverse your scoring
direction automatically. Also inspect warnings about sparse categories
or an unstable fit. The formal model and scaling rules are documented in
?fit_rubric_calibration.
predict(calibration) scores every original sample,
including those without human labels. If you supply newdata
for a same-set calibration, it must be the original completed result;
changing row order is fine, but changing the samples or their fit is
not. Use the linked-reference workflow below for a separate set.
if (requireNamespace("ordinal", quietly = TRUE)) {
probability_matrix <- do.call(rbind, scores$probabilities)
head(probability_matrix)
scores[, c("median_category", "modal_category", "expected_level")]
modal_scores <- predict(calibration, hard_score = "mode")
}Each probabilities entry gives the probability of every
rubric level, named by its label. The default single score is the
median category: starting from the lowest level, add
probabilities until the total reaches 0.5. If it equals 0.5 exactly, use
that lower category. With hard_score = "mode", use the most
probable category instead; ties go to the lower category. Both summaries
and all probabilities remain available whichever score you report.
expected_level is an average of category positions (1,
2, 3, and so on), not an average of your numeric labels. It is a summary
rather than an assigned rubric level. extrapolated flags
scores outside the range covered by training labels, even for samples in
the original analysis.
For an illustrative probability vector
(developing = 0.20, proficient = 0.55, advanced = 0.25),
cumulative probability first reaches 0.5 at “proficient”, so that is the
default median-category score. The probabilities also show substantial
uncertainty; reporting the category alone discards it. These numbers
illustrate the decision rule and are not fitted results.
For a report, retain item_id, rubric_score,
probabilities, and extrapolated for ordinal
predictions. Explain an extrapolation flag as a score outside the
labeled calibration range. Percentile predictions instead require
requested and achieved category proportions. Keep the method, trait, and
validation results with either kind of output.
if (requireNamespace("ordinal", quietly = TRUE)) {
assessment <- evaluate_rubric_predictions(
calibration, evaluation_labels, diagnostics = TRUE
)
assessment$metrics
assessment$calibration
assessment$metadata$n_training_label_overlap
assessment$diagnostics$common_effect
}Start with ranked probability score (rps): lower values
indicate better predictions, accounting for how far a prediction falls
from the observed level. Log loss is another probability-based check.
For the assigned categories, inspect exact agreement, agreement within
one level, average category error and weighted kappa (agreement adjusted
for chance). Distances count category steps. If a metric cannot be
calculated, the output explains why.
Evaluation matches samples by ID, counts missing labels and reports
any overlap with training labels. Unlike training, an evaluation set
need not contain every category. assessment$calibration
groups similar predictions and compares them with observed human scores,
helping you see where the model is too optimistic or too
pessimistic.
Diagnostics are off by default. For the linear method, they check the assumption that CJ quality has a shared effect across rubric boundaries. For the curved method, they provide descriptive boundary checks. These checks use training data; the separate evaluation labels do not change your fitted calibration. If a check fails to run, it is marked unavailable and the original fit is preserved.
For repeated validation, keep the completed CJ analysis fixed. Repeatedly set aside some human labels, fit a fresh calibration using only the remaining labels, and evaluate the labels you set aside. Every training group must still include all rubric levels. If you use those results to choose a model or its settings, reserve another independent evaluation set for your final report.
if (requireNamespace("mgcv", quietly = TRUE) &&
packageVersion("mgcv") >= "1.9.4" && requireNamespace("withr", quietly = TRUE)) {
monotone_calibration <- fit_rubric_calibration(
completed_cj, training_labels, method = "ordinal_monotone",
trait = "organization", levels = rubric_levels, k = 6
)
monotone_scores <- predict(monotone_calibration)
monotone_assessment <- evaluate_rubric_predictions(
monotone_calibration, evaluation_labels
)
monotone_assessment$metrics
}This fits a smooth curve that can bend while never decreasing as CJ
quality increases. k controls how much flexibility is
available; its default is 6 and it must be at least 5. The package may
reduce it with a warning when too few distinct training scores are
available. At least five distinct scores are needed to fit this curve,
but that minimum does not guarantee enough data for useful calibration.
See the function help for smoothing controls.
Check fit diagnostics and performance on separate human labels before preferring the more flexible method. A flat part of the curve may make a category cutpoint unavailable; predictions still use category probabilities. Scores outside the training range are flagged because the curve is being extended beyond the scores used to learn it.
percentile_calibration <- fit_rubric_calibration(
completed_cj, method = "percentile", trait = "organization",
levels = rubric_levels, target_distribution = c(0.2, 0.5, 0.3)
)
percentile_scores <- predict(percentile_calibration)
percentile_calibration$backend$requested_proportions
percentile_calibration$backend$achieved_proportions
percentile_calibration$backend$cutpoints
percentile_calibration$backend$cutpoint_tie_countsThe example asks for 20% of samples in the lowest level, 50% in the
middle and 30% in the highest. Omit target_distribution for
equal proportions. You can supply the number of levels with
K instead of naming them.
Tied scores stay together. This means the achieved proportions may differ from what you requested, and some categories may be empty. A score exactly on a boundary enters the higher category; if all scores are identical, this rule puts every sample in the highest category. Always inspect the achieved proportions.
These levels describe relative standing within this
set. Matching a historical rubric distribution does not
establish agreement with human raters for individual samples. Percentile
scoring reuses only the original analysis; it cannot transport levels to
a separate cohort through linked_anchors. It returns
categories without category probabilities, so evaluation uses only
category-based metrics.
See What the research shows for a study comparing percentile scoring, human-score calibration, and direct LLM rubric scoring.
You can use a human-scored reference set to score new writing samples after linking both sets onto a common scale. The reference is the hub and the new set is the spoke. Phase A means ranking each set separately; Phase B links them using comparisons between sets. A hub sample is a reference for comparison, not a sample whose writing quality is known without uncertainty.
The following example assumes reference_phase_a contains
the original reference artifact (the saved Phase A result) and
reference_labels contains its human scores. Read a saved
artifact with readRDS() first; supply the artifact itself,
not a filename or the entire collection of saved-run metadata.
if (requireNamespace("ordinal", quietly = TRUE)) {
linked_calibration <- fit_rubric_calibration(
reference_phase_a, reference_labels,
calibration_design = "linked_anchors",
trait = "organization", levels = rubric_levels
)
reference_scores <- predict(linked_calibration)
}An import-ready adaptive Phase A result is one supported reference source. You can also use a completed standalone Bayesian ranking, including one fitted from pooled historical comparisons of reference samples. Freeze this reference before evaluating the new samples.
Keep the exact comparison table supplied to
fit_bayes_btl_mcmc(). If you fitted several subsets, keep
the rows used for the final fit. The constructor checks these rows
against the record saved when the fit was made. Older standalone results
without that record must be refitted from their original comparisons;
they remain usable for ordinary same-set calibration.
reference <- prepare_linked_rubric_reference(
completed_reference, reference_comparisons,
set_id = "reference", items = reference_items, trait = "organization"
)
standalone_calibration <- fit_rubric_calibration(
reference, reference_labels, calibration_design = "linked_anchors",
levels = rubric_levels
)
saveRDS(reference, "rubric-reference.rds")
saveRDS(standalone_calibration, "rubric-calibration.rds")reference_items maps the fit’s item_id
values to unique global_item_id values. Use those global
IDs in reference_labels. Omit items when the
original IDs already uniquely identify your samples across sets.
The linking
sessions guide shows which saved reference inputs to use with E1,
E2, and E3. All three retain the same reference identifier. The saved
reference and calibration can be read with readRDS() later;
the original sampler does not need to stay open.
Use the same BTL model variant for the reference and target rankings.
The linking sessions guide explains
the required input tables and how to save and resume work. Here we
explicitly choose E2, which needs saved joint posterior draws in both
artifacts. active_cross holds the comparisons to use for
fitting; frozen_judge holds the shared bias and lapse
settings from Phase A. Keep comparisons reserved for checking accuracy
in a separate table.
link_input <- prepare_link_input(
estimator = "gaussian_posterior_bridge",
hub = list(set_id = "1",
items = reference_phase_a$items[c("item_id", "global_item_id")]),
spoke = list(set_id = "2",
items = target_phase_a$items[c("item_id", "global_item_id")]),
phase_a = list(hub = list(artifact = reference_phase_a),
spoke = list(artifact = target_phase_a)),
cross = active_cross, judge = frozen_judge
)
completed_phase_b <- start_link_session(link_input)For several target sets, prepare one linking input per set and pass
the list to start_link_session(). All use the same hub,
judge settings and chosen method. E2 and E3 can update hub scores during
linking, but the rubric calibration stays attached to the original
reference result. You do not need target rubric labels to link the sets
or apply the saved calibration.
if (requireNamespace("ordinal", quietly = TRUE)) {
target_scores <- predict(linked_calibration, newdata = completed_phase_b)
target_scores[, c("global_item_id", "set_id", "rubric_score", "theta_sd", "extrapolated")]
attr(target_scores, "linking")$diagnostics
}The prediction contains only the target samples. Use
newdata = NULL to predict scores for the original reference
samples. Every target set needs a valid link supported by between-set
comparisons; a fit based only on starting assumptions is
insufficient.
Linking scores are named theta_link_mean (also available
as theta_link_eap). E1 and E3-MCMC report posterior
averages; E2 and the usual E3 fit report the most probable joint scores
with approximate uncertainty. E1 treats the earlier rankings as fixed.
E2 and E3 include uncertainty in those rankings as well as the shift
between sets. The design
guide explains these differences. Rubric prediction uses the linked
point scores; it retains score uncertainty for inspection but does not
propagate it through the rubric model.
Prediction checks that you are using the original reference samples, trait, scoring direction and compatible model settings. It restores the original reference’s score origin before applying the saved calibration. Do not standardize the target set separately: doing so would change the meaning of its scores relative to the reference. The output includes information about the linking method, comparisons used and fit diagnostics.
Old anchored-joint Phase B sessions cannot be used with this workflow. Restart linking from compatible original Phase A results using E1, E2 or E3.
Reference holdout performance assesses calibration on the reference scale. To assess transport, use external human labels for linked targets:
if (requireNamespace("ordinal", quietly = TRUE)) {
target_assessment <- evaluate_rubric_predictions(
linked_calibration, target_labels, newdata = completed_phase_b
)
target_assessment$metrics
target_assessment$metadata$linking
}Phase B diagnostics alone do not demonstrate rubric accuracy, and reference validation alone does not measure the additional error from linking.
Organization and evidence require separate trait-specific comparisons, CJ fits, labels, and calibrations even when item IDs overlap. Each may have different levels or a different Bayesian variant. Compatibility must hold within each trait’s reference/target linking workflow. One holistic CJ scale cannot supply multiple analytic trait calibrations.
All three methods condition on accepted CJ point locations. Retained CJ SDs, posterior draws, or linking uncertainty are not propagated into rubric category probabilities. Ordinal parameter uncertainty is conditional on those estimated predictors; monotone threshold uncertainty is not supplied by the wrapper. There is no posterior/bootstrap propagation engine or joint likelihood here.
Report the trait, BTL variant, method/design, ordered levels, labeled category counts and range, held-out metrics, extrapolation, and CJ/calibration/linking diagnostics. Distinguish numerical feasibility from adequate measurement and external rubric validity. Small deterministic examples and package recovery tests do not establish a universal calibration sample size or validity for a new population.
Mercer, S. H. (2026). Guide: Rubric Calibration [R package vignette]. Comprehensive R Archive Network. https://doi.org/10.32614/CRAN.package.pairwiseLLM