Ninth review round: one finding.
print() distinguishes the three saved-fit dependence
schemas by their df field: current fits label the statistic
t; 1.11.4 transitional fits (z name,
df present, t-based p) print as t; older fits
(z, no df, normal-reference p) KEEP the
z label – relabelling them t misrepresented
how their p-values were computed (a legacy z = 3.17 with normal p = .002
is not the same claim as t = 3.17 with p = .016 at five clusters). The
regression test now reconstructs both legacy schemas faithfully.Eighth review round: label and migration polish.
print() reads the dependence statistic from either the
current t column or the pre-1.11.5 z column,
so fits saved before the rename print completely; the printed label and
the rank-deficiency note both say t, matching the t(G - 1)
reference. No duplicate z alias column is added: the
package has never had a released version with the old name, the rename
is documented here, and old saved fits are read transparently.Seventh review round: three completions of round-six fixes, one label.
margin= gets the same explicit-ordering rule as
response=: an ORDERED factor (smallest to largest margin)
or a numeric magnitude; plain factors AND character columns are refused,
since alphabetical order could silently reverse which margin counts as
the big win.second_dim$items, and
a vector-valued rho.t, matching its t(G - 1) reference (at infinite df it is
the familiar z). The reviewer’s independent 400-run null simulation of
the cluster-t correction found 5.25% rejection at nominal 5%.Sixth review round: four findings, all verified and closed.
as.numeric(factor) returns level codes, so a factor score
of “1.9” slipped past as the integer 3 and was then truncated.
Non-numeric and non-finite scores now also error at every entry point
instead of silently becoming missing.btl() requires an ORDERED response factor: a
plain factor’s alphabetical level order silently defined – and could
reverse – the response scale (worse < same < better read as better
< same < worse). Integer scores 0..m remain the alternative.simulate_rasch() validates the second-dimension
specification: rho outside [-1, 1] and unknown item
names/indices are errors (rho = 1.2 used to produce an all-missing
second dimension that looked like a successful simulation), and the
item-set resolver now range-checks every nominated item list
centrally.Fifth review round: seven findings, all verified and closed.
pcml(),
pcml_pc(), and rasch_mfrm() truncated 1.9 to 1
silently, and graded btl() rounded it; only
rasch() rejected. One shared guard serves all entry
points.item_moments() is overflow-stable (log-sum-exp): a
31-category item, or wide categories with a large discrimination,
overflowed the uncentred exponent and turned every
person_wle() location NA.btl_efrm() rejects a judge assigned to more than one
panel (a panel is a judge attribute; the judge bootstrap would otherwise
silently reclassify rows).simulate_rasch()’s secondary trait keeps the requested
mean and sd (combining two mean-mu components had shifted the mean by
the factor rho + sqrt(1 - rho^2)) and is returned in the truth
metadata.Edge cases from the fourth review round, verified and closed:
as.integer(0.5) is a zero binomial size – every reference
draw degenerated to 0). The per-replicate leading strengths are exposed
as $reference$draws.btl_efrm(se_method = "judge_bootstrap") refuses a panel
with a single judge (resampling one judge returns the same data every
time, so its SEs would be a spurious zero) and notes panels with fewer
than five judges as rough.btl() notes when there are no more judge clusters than
parameters: the clustered covariance is then rank-deficient by
construction and no scalar (CR1) correction repairs it – the SEs likely
understate.The follow-up review’s residual findings, all verified and closed:
btl_efrm(se_method = "judge_bootstrap"): judges
resampled with replacement within panels and the whole pipeline
refitted, so the errors carry within-judge dependence as well as
stage-one uncertainty – closing the documented gap in the parametric
bootstrap. On model-true data it agrees with the parametric bootstrap;
estimates are unchanged by construction.Statistical validity, from a second external review. Every finding was re-derived or reproduced by simulation before fixing; the fixes are verified by new calibration tests (test-statistical-validity.R).
pcml()
recentres thresholds so the mean item location is zero, but the
covariance stayed under the sum-of-thresholds constraint – identical
only when every item has the same maximum score. With mixed
1/2/3-threshold items, per-item empSD/SE ran 0.90-1.21 by item type; the
linear transform (I - 1a') cov (I - a1') restores
0.96-1.09. Affects threshold and item SEs, separation, and equating
weights on mixed designs.disc[(R-E) + mu3/(2V)] – the
discrimination cancels; a stray disc factor in the
correction biased WLEs by up to 0.26 logits at disc = 0.5 (exact at disc
= 1, so ordinary analyses were unaffected; the vector-disc EFRM path was
already correct).adjust_N proportional-scaling semantics are now documented
explicitly.[(I - 1u') Sigma (I - u1')]_ii with Sigma the sum of the
two calibrations’ location covariances (a bank contributes diag(se^2)).
Null calibration: 4.6% rejection at nominal 5%, t-SD 0.97.btl_dif() now tests at the judge level: residuals
aggregate to one weighted mean per judge (per opponent band) and enter a
split-plot ANOVA with the judge as the error unit. The comparison-level
ANOVA pseudo-replicated – 6 of 10 null datasets with judge heterogeneity
and arbitrary groups falsely flagged uniform DIF; the judge-level design
flags 0 of 10 while still detecting a planted 1-logit effect. Band
breaks moved to count-weighted quantiles, making aggregated and expanded
data exactly equivalent. Power now scales with judges per group – the
honest unit.rasch()
and btl() (it was only visible in print);
btl()’s convergence flag is scale-free (gradient per
comparison).sim_replicate() recommended where exact calibration
matters; the BTL pair chi-squares are marked descriptive under judge
clustering; the next-pair priority is documented as a ranking heuristic;
the BTL dimensionality reference’s no-refit design is stated.Input honesty, from an external code review (five findings, all confirmed and fixed):
id, factors, or
items column names in rasch() are now errors
naming the missing columns; they were silently ignored (one misspelled
factor silently dropped ALL factors), producing valid-looking analyses
of the wrong data. Numeric item indices still work.as.integer() used to truncate them silently, altering
response data. Integer-valued doubles (“2.0”) still pass.paste() used to coerce NA
to the literal string “NA”, silently pooling unrelated rows under a
phantom level.equate_tests() excludes common items whose location or
SE is unavailable (e.g. weakly determined items with honest NA SEs) from
the precision-weighted shift and drift tests, notes them, and keeps
their rows with NA test columns – one such item used to turn every
statistic NA. plot_equate() handles the excluded rows.report_html() escapes all data-derived text (title,
notes, item and facet names) – crafted column names could inject markup
into reports; table cells were already escaped.compare_fits() gains cl_aic and
cl_bic (Varin & Vidoni 2005; Gao & Song 2010):
-2 cl penalised by the effective parameter count
tr(H^-1 J) from the Godambe matrices – the same quantity
whose eigenvalues calibrate lr_test() – which absorbs the
pairwise over-counting a nominal AIC/BIC would ignore (empirically
3.5-5x the nominal count). Verified by plant-and-detect: the criteria
select RSM on rating-structured data, PCM under varying threshold
spreads, and a planted BTL position effect – and reject the richer model
when the effect is absent. n for CL-BIC counts independent
units: persons contributing an informative pair, or judges
(count-weighted comparisons when unclustered). MFRM/EFRM fits report NA
with the reason noted.compare_fits() now accepts btl fits
(all-BTL comparisons: free vs principal-component thresholds,
with/without a position effect or within-judge dependence), reporting
judges/objects/comparisons and OSI. btl() fits carry their
composite-likelihood ingredients in $cl; anchored
pcml() and pcml_pc() now return their Godambe
matrices. The app’s Compare page shows the criteria and accepts
paired-comparison fits.pcml(), pcml_pc(), and everything built on
them now stop with an error naming the disconnected blocks. Anchors
rescue a block (disjoint-form equating): with an anchored item in every
block the fit proceeds; with any block unanchored it stops and names
it.thr$weak), their SEs and the item-location SE
reported as NA, and a note names the item, the category, and its count,
pointing to pc_components or category collapsing. Estimates
are still returned; categories with zero responses are rescored away as
before.inst/casestudies/party_blocs_crisis.R
applies btl_efrm() to the Tuebingen 2009 party-preference
data (via psychotools), with ideological blocs as object
sets and economic-crisis concern as judge panels. Running the model
against real data surfaced three defects, all fixed below.btl().?btl_efrm.btl_efrm() estimates object locations when the unit of the
scale differs across judge-panel by object-set frames – the package’s
extension of Humphry’s persons-by-items EFRM to the Bradley-Terry-Luce
family. Panel units (phi, a panel’s discriminating power) are identified
conditionally wherever two panels judge a common set; set units and
origins (alpha, kappa) are identified from cross-set comparisons, which
are themselves conditional outcomes – so, unlike the persons-by-items
case, the linking makes no distributional assumption about the objects.
Everything is estimated by the staged conditional estimator: the
within-frame calibrations are invariant to the linking data (the
frame-of-reference property), a deliberate trade of efficiency for
invariance. The cross-frame discrimination convention (cross-set
contests judged at exactly phi) is stated plainly in the documentation
as an assumption, with the per-frame fit residuals as its check.btl_efrm() defaults to a parametric
bootstrap of the whole two-stage pipeline: simulation showed the fast
conditional errors cover at a third of nominal on chain-linked designs
(they omit the stage-one uncertainty that flows into the linking), while
the bootstrap restores 12/12 coverage and nominal false-flag rates at
both small and large designs. The conditional errors remain available as
se_method = "conditional" for quick inspection, labelled as
understating. plot_btl_units() draws the unit caterpillar;
simulate_btl_efrm() generates frame-structured data with
planted units for the plant-to-detect loop.Three further paired-comparison analogues of the Rasch tool set, and CRAN housekeeping.
btl(anchors = c(Object = value, ...)) holds the named
objects at fixed locations and estimates the rest on that scale (no
sum-zero constraint; anchored objects report se 0) – the equating
workflow’s second half, mirroring rasch(anchors=). An
anchored object at a response boundary is an error rather than a silent
removal.btl(position = TRUE)
estimates the pure positional (first/left-presented) advantage of the
Davidson & Beaver
btl_equate() compares two
paired-comparison calibrations (or a calibration against a bank) through
their common objects – precision-weighted origin shift, per-object drift
t-tests with multiplicity adjustment, equated locations – with
plot_btl_equate(); the standards-maintenance workflow of
comparative judgement across panels or years.btl_information()
(per-comparison Fisher information and per-object design information,
with the naive 1/sqrt(information) SE beside the judge-clustered
sandwich), plot_btl_targeting(), and
btl_next_pairs() – the greedy most-informative-next-pair
step of adaptive comparative judgement (Pollitt 2012), with the Bramley
(2015) reliability-inflation caution documented.skip_on_cran() (they run locally and on CI), and
cran-comments.md drafted from a full --as-cran
check (about 3 minutes; slowest example 1.4s).btl(count=)
inflated every sandwich standard error by about the square root of the
count (the meat weighted aggregated rows twice; point estimates were
always correct). Count- weighted fits now reproduce the expanded-data
standard errors exactly; OSI and all z/p values follow. Additionally
from that round: btl_equate()’s shift standard error now
uses the stored covariance of each calibration (it treated the
common-object differences as independent, overstating the SE
several-fold; 95 per cent coverage verified) and documents the
majority-drift limitation of single-shift equating;
btl_next_pairs() ranks by the exact one-step reduction in
total location variance (a Sherman-Morrison update on the fit’s
covariance), verified to beat low-priority pairs empirically; and
btl_information()’s se_naive is documented
honestly as a single-parameter lower bound rather than an independence
benchmark.A statistical audit of the new simulation and paired-comparison independence code (23 findings across two adversarial rounds, all fixed), plus a vignette and a logo.
Misfit layers now compose correctly in
simulate_rasch(): the response-dependence and
response-style regeneration steps previously rebuilt their target items
from the base structure, silently erasing any DIF or second-dimension
loading planted on those items (and leaking a DIF-like signal from a
DIF-carrying source item into its dependent partner). Every regeneration
now honours the item’s own generating structure – trait, group-shifted
thresholds and slope, guessing.
btl_dimensionality() now simulates its noise
reference sequentially WITH the fitted within-judge dependence effects
when the fit carries them: real order effects previously inflated the
false-positive rate to ~36 per cent on one-dimensional data (now ~0-5
per cent). The documented price is conservatism, since carry-over and a
judge-camp second attribute are partially confounded.
Doc examples pass id = "id" so they no longer emit
spurious dropped-column notes.
simulate_rasch(): model = "PCM" now
draws each item’s threshold spacings and span afresh, as the partial
credit model allows (previously PCM and RSM generated identical
rating-scale-structured data); dif without
n_groups >= 2 is an error instead of a silent no-op;
polytomous guessing warns and is ignored rather than
falsely recorded as planted; the response-dependence term now carries
the guessing asymptote of the pair’s first item (no more spurious
easiness on the dependent item); disordered thresholds work with 3
categories and no longer shift the item’s location; careless responders
are no longer double-counted as response-style persons in the recorded
truth.
simulate_mfrm(): halo raters are capped at the
eligible pool (no NA raters when erratic raters shrink it); the example
runs.
sim_recovery(): person abilities are mean-centred as
documented (an asymmetric difficulty range no longer masquerades as
person bias); the many-facet branch reads the item margins
(item_effects), restoring the documented item-difficulty
recovery for MFRM fits.
btl_dimensionality(): null calibration verified for
graded fits (mildly conservative, never anticonservative) and
documented; docs also clarify the leading-bimension reference columns
and the no-ties precondition of Kendall’s zeta.
Shiny app (Simulate page): the parameter-recovery card only renders when the current fit was estimated on the currently loaded simulation (a stale fit can no longer be compared against new truth); the reproducible call now carries every argument the generator received (MFRM person/item SDs, EFRM person SD, graded categories); non-integer seeds are rounded safely.
New vignette: “Planting misfit and watching the diagnostics fire”
— the plant-to-detect loop end to end, closing with a small Monte Carlo
power estimate via sim_replicate().
A hex logo (an item characteristic curve at its location),
reproducible from tools/logo.R.
judge_surprise() and plot_btl_judge_map():
the paired-comparison counterpart of the kidmap. A judge has no ability
to condition on, so the reference is the consensus object scale; each
object a judge met gets a standardised residual saying how much more or
less that judge favoured it than its location predicts. A strong object
the judge under-rated, or a weak object over-rated, is an unexpected
judgement. It catches systematic bias – a distinct failure mode from the
noise that judge fit and judge consistency flag. In the Shiny app the
Persons > Judge fit panel is now a master-detail: the judges table
drives the selected judge’s map, mirroring person -> kidmap.btl_transitivity() counts circular triads (preference
loops: A beats B, B beats C, C beats A) against the chance rate, reports
Kendall’s coefficient of consistency for complete designs, and gives
each judge’s own consistency (Kendall & Babington Smith 1940) – the
single-dimension check, and a judge-fit analogue.btl_dimensionality() decomposes the skew-symmetric
object-by-object residual preference matrix into Gower (1977) bimensions
– the paired-comparison counterpart of residual PCA – and judges the
leading bimension against a model-simulated noise reference, so a
coherent residual “swirl” (a second attribute steering some contests) is
distinguished from noise.plot_btl_scree(), plot_btl_dim_map(), and
plot_btl_transitivity() display them; the Shiny app’s
Independence > Trait tab now carries these for a BTL fit (it
previously hid for paired comparisons).rmt). The name rmt
belongs to an unrelated package on CRAN, and rasch says
plainly what this is – and mirrors mirt::mirt() with
rasch::rasch(). No functions, arguments, or results change;
only the package and library name. The auxiliary result classes were
unified under the rasch_ prefix (rasch_btl,
rasch_dif, …) to match the fit classes.A full statistical audit of the independence, invariance, and paired- comparison procedures, with fixes throughout.
btl() now interrogates within-judge
dependence – dependence_data holds every comparison’s
exposure and carry-over covariates (count-weighted),
plot_btl_dependence() displays an effect as a
partial-residual curve, and a separated effect (one-sided informative
comparisons) is set aside with a note instead of reported as a runaway
estimate. The graded engine’s threshold-by-dependence Hessian block is
corrected (free-threshold fits with five or more categories and an order
column now converge), rows with a missing judge are dropped with a note,
category checks re-run after boundary objects are removed, and the
dichotomous path now routes through the one (vectorised) estimator.btl_dif() holds fitted dependence effects fixed in its
residual moments and resolution refits (dependence is no longer absorbed
as spurious judge-group DIF), weights count-aggregated comparisons
exactly as expanded rows, restricts supersession to group terms, and
notes every significant term it does not resolve.dif_anova() keeps each ANOVA term in its own error
stratum, so a mixed (within-subject) design with missing responses no
longer un-flags real DIF; factors named like internals (ci,
f1) are safe; a term’s own class-interval crossing no
longer supersedes it (so resolve_dif() again splits items
with both uniform and non-uniform DIF); mixed designs say plainly that
Tukey comparisons are unavailable; and dif_size() notes
when a within-person factor makes its standard errors conservative.plot_scree()’s parallel-analysis reference is now
model-simulated (responses drawn from the calibrated model, persons
re-estimated), so model-true data sits at the reference instead of
always “showing structure” (Raiche 2005; Chou & Wang 2010).plot_pca_biplot() draws the item loadings on the
first two residual principal components – the pair that usually carries
any interpretable contrast – on equal axes, coloured by the sign of the
PC1 loading.
residual_correlations() now also returns the
adjusted-Q3 star_matrix (each Q3 less the average
off-diagonal), and plot_resid_cor() gains a
stat argument to colour either the raw Q3 or the adjusted
Q3*, drawing the lower triangle only.
Shiny app: the trait- and local-dependence pages pair each table with its plot (loadings with a PC1-PC2 biplot; Q3 and Q3* matrices with heatmaps), the raw and adjusted correlations are each flagged by their own rule (|Q3| and Q3*), and every data-restructuring action (subtest, item split, automatic DIF resolution) offers an in-place reset to the original data.
dif_anova() is now the single DIF
analysis-of-variance function. One factor is analysed one-way; several
factors are modelled jointly – the statistically correct treatment –
with main effects by default and factor-by-factor interactions optional
(effects = "factorial"). It handles within-subject factors
(repeated-measures / mixed ANOVA) and returns a classed object with a
summary, the full terms table, and Tukey
comparisons. The separate dif_anova_factorial() is
removed.
resolve_dif() resolves DIF iteratively by item
splitting, largest effect first, to clear artificial DIF.
First stable release. The package delivers a complete Rasch Measurement Theory workflow built entirely from published measurement theory, with a pairwise conditional estimation core and a Shiny interface that exposes every analysis with reproducing R code attached to every output.
rasch(): dichotomous and polytomous (partial credit and
rating scale) Rasch models by pairwise conditional maximum likelihood
(Andrich & Luo 2003; Zwinderman 1995), with Godambe sandwich
standard errors, sum-zero identification, Warm (1989) weighted
likelihood person estimation, and the principal-component threshold
parameterisation as an estimation option.rasch_mfrm(): many-facet models with additive facet
severities or item-by-facet interactions, accepting wide (one column per
item) or long data.rasch_efrm(): the extended frame-of-reference model
(Humphry) with frame units estimated by within-frame pairwise
conditional likelihood.btl(): paired comparisons as the conditional form of
the dichotomous Rasch model (Bradley & Terry 1952; Andrich 1978),
including the adjacent-categories graded extension (Tutz 1986; Agresti
1992) with symmetric thresholds – the Davidson (1970) ties model is its
three-category case – winner-plus-margin data entry, judge-clustered
errors, and within-judge exposure and carry-over effects estimated
jointly with the locations.btl_dif() tests object-by-judge-group
interaction by the same two routes.fit_summary_table() and targeting_table()
return the headline statistics as tidy tables;
save_outputs() writes every table and plot to disk;
report_html() produces a single-file HTML report.run_app() launches the Shiny interface: model-adaptive
navigation, reproducing R code beneath every output, and one-click
export.