Migrating from FACETS to mfrmr

This vignette walks FACETS users through the closest mfrmr workflow: preparing data, fitting an RSM/PCM many-facet Rasch-family model with FACETS-oriented settings, generating related diagnostic and reporting tables, and reviewing the output-contract boundary between the two systems. Bounded GPCM can be fit in mfrmr, but its slope-aware score semantics are intentionally outside the score-side FACETS output-contract route.

The software reference target for this migration boundary is FACETS 64-bit 4.5.1 (July 2026). A cited manual may retain its published 4.5.0 edition; software version and documentation edition are recorded separately. The coverage described here is not an external numerical-parity result.

Mental model

The two stacks share the same psychometric framework but differ in operating model.

Before treating a legacy workflow as covered, inspect the public coverage boundary:

facets_feature_coverage()
facets_feature_coverage("not_implemented")

This matrix describes the availability of package-native output surfaces. implemented does not by itself mean that the two programs use the same estimand, conditioning, extreme-score rule, degrees of freedom, or numerical contract.

Concept FACETS (Linacre 2026) mfrmr
Input Specification file plus data file data.frame in long format
Estimation JMLE by default MML by default; JML is the closest estimation route for a JMLE-oriented comparison
Fit-statistic basis Residuals at JMLE estimates Residuals at EAP person measures under MML (shrunken toward the mean); refit with method = "JML" for a JMLE-style residual basis
Models Multiple model statements, rating scales, partial credit, and other response families can coexist One response-model family per fit: RSM, PCM, or bounded GPCM
Output Tables 0-30 plus graphic files Returned R objects with summary() and plot() methods
Anchoring Element/group anchors, rating-scale calibration, and reusable starting values Element and group anchors; no general threshold/scale anchors or fixed-calibration starting-value bundle
Repeated cells Multiple observations may be represented within a design cell Exact Person-by-facet duplicates are retained but force Data review; distinguish legitimate repeats with an event/occasion facet
Bias / interaction Table 14 estimate_bias() and bias_interaction_report()
Wright map / variable map Graphic variable-map output plot(fit, type = "wright") and plot_wright_unified()
Fair average Table 7 fair-M average fair_average_table()
Reproducibility Specification, input data, FACETS version, and recorded run environment/settings build_mfrm_manifest() plus build_mfrm_replay_script()

A one-shot legacy-compatible call

If the goal is to translate a FACETS-style script with minimal R-side plumbing, use run_mfrm_facets() (alias mfrmRFacets()):

library(mfrmr)
data("mfrmr_example_operational", package = "mfrmr")

run <- run_mfrm_facets(
  data = mfrmr_example_operational,
  person = "Person",
  facets = c("Rater", "Criterion"),
  score = "Score",
  model = "RSM",
  method = "JML"
)

names(run)

The wrapper returns the same fit_mfrm() and diagnose_mfrm() objects that a step-by-step pipeline produces, plus the iteration log, fair-average table, and rating-scale table:

jml_status <- summary(run$fit, profile = "fit", detail = "brief")
jml_status$overview[, c(
  "Model", "Method", "Converged", "InferenceReady",
  "ConvergenceSeverity"
)]
jml_status$readiness
head(run$fair_average)

method = "JML" is shown here for a JMLE-oriented migration comparison. Do not infer readiness from Converged alone: require InferenceReady = TRUE for the numerical gate and review the terminal-gradient guidance when severity is "review" or "fail". Numerical readiness does not override a Data, Design, or Stability hold; use the readiness table before interpreting or reporting the fit. For new analysis scripts, prefer fit_mfrm(method = "MML") directly. MML integrates over the person distribution under an N(0, 1) prior and exposes per-person posterior SEs that JML cannot produce.

Translating the specification file

The mapping below covers the most common FACETS specification keywords.

FACETS and labels

Facets = 3
Models = ?,?,?,R5
Labels =
  1, Examinee
    1 = P01
    ...
  2, Rater
    1 = R1
    ...
  3, Criterion
    1 = Content
    ...

translates to:

fit_mfrm(
  data = examinee_long,
  person = "Examinee",
  facets = c("Rater", "Criterion"),
  score = "Score",
  rating_min = 1,
  rating_max = 5,
  model = "RSM"
)

Models = ?,?,?,R5 becomes model = "RSM" and the R5 rating-scale declaration becomes rating_min = 1, rating_max = 5. For a partial-credit specification, pass model = "PCM" and identify the facet that carries the step thresholds with step_facet = "Rater" (or the appropriate facet name).

Anchoring

A FACETS D = 2, A = block:

D = 2
A = 1, 0.0
    2, 0.5

becomes an anchors data frame:

anchors <- data.frame(
  facet = "Rater",
  level = c("R1", "R2"),
  estimate = c(0.0, 0.5),
  stringsAsFactors = FALSE
)
fit <- fit_mfrm(..., anchors = anchors)

review_mfrm_anchors() validates and reports on the anchor block before the fit runs, surfacing connectivity, overlap, and minimum-sample issues.

Bias and interaction

For FACETS Table 14 bias output between Rater and Criterion, the closest mfrmr screening route is:

diag <- diagnose_mfrm(fit)
bias <- estimate_bias(fit, diag,
                      facet_a = "Rater", facet_b = "Criterion")
summary(bias)

estimate_all_bias() enumerates every non-person facet pair in one call.

Wright map / variable map

For a shared-logit visual display of persons, facet levels, and step thresholds, first create the FACETS-organized summary and retain its result object:

review <- summary(fit, profile = "facets", detail = "brief")
res <- review$results

# Primary final-scale figure: all locations and available facet uncertainty.
plot(res, type = "wright", renderer = "native", show_ci = TRUE,
     top_n = Inf, preset = "publication")

plot_wright_unified() is the corresponding explicit helper when the Wright map is the main figure. For readers who expect the FACETS Table 6-style asterisk ruler and horizontal, rubric-labelled category transitions, define one label for every retained original score:

rubric_labels <- setNames(
  your_rubric_labels,
  fit$prep$score_map$OriginalScore
)
plot(res, type = "wright", renderer = "facets", show_ci = FALSE,
     category_labels = rubric_labels, preset = "publication")

show_ci = FALSE is the closest FACETS-style visual grammar. Setting show_ci = TRUE deliberately creates a hybrid display: the ruler is FACETS-style, but the intervals are mfrmr uncertainty estimates. Neither renderer implies that FACETS performed the estimation or that the two programs are numerically equivalent.

For the Bond-and-Fox-style follow-up requested by many FACETS users, put Infit on the horizontal axis and the measure on the vertical axis. Person rows remain opt-in:

plot(res, type = "fit_pathway", fit_stat = "Infit",
     include_person = TRUE, top_n_person = 12,
     person_labels = "none", facet_labels = "flagged")

Use draw = FALSE or plot_data(fit, type = "wright") when you need the underlying coordinates for a custom ggplot2, base-R, or Quarto graphic.

Fit df and ZSTD review

FACETS users often compare Infit/Outfit MnSq together with ZStd columns. In mfrmr, treat MnSq as the primary fit statistic and use the df/ZSTD columns to explain how the same MnSq values were standardized. The direct review path is:

diag <- diagnose_mfrm(fit, residual_pca = "none", fit_df_method = "both")
fm <- fit_measures_table(fit, diagnostics = diag,
                         facet = "Rater", fit_df_method = "both")

fm$facets_table
fm$df_sensitive
plot(fm, type = "df_sensitivity")

df_sensitivity reports the engine-vs-FACETS-style df comparison row by row; df_sensitive keeps only rows where the df convention changes the |ZSTD| flag or materially changes the ZSTD interpretation. The same status taxonomy is used by facets_fit_review(), so a table-oriented review and an external FACETS comparison use the same language.

Group anchoring and DFF

FACETS D = ..., G = group-anchor blocks for differential facet functioning translate to the group_anchors argument and the analyze_dff() follow-up:

group_anchors <- data.frame(
  facet = "Criterion",
  level = "Content",
  group = c("Native", "Non-native"),
  estimate = c(0.0, 0.0),
  stringsAsFactors = FALSE
)
fit_g <- fit_mfrm(..., group_anchors = group_anchors)
dff <- analyze_dff(fit_g, diag, facet = "Criterion",
                   group = "FirstLanguage", method = "refit")

Reviewing output contracts and fit tables

When migrating an existing study, facets_output_contract_review() checks whether the package-generated report components satisfy the FACETS-style output contract encoded in the package:

contract_review <- facets_output_contract_review(
  fit,
  diagnostics = diag,
  branch = "facets"
)
summary(contract_review)
contract_review$missing_preview
contract_review$metric_checks

The resulting object reviews column coverage and package-native metric checks. It is not a claim that mfrmr has reproduced FACETS estimates numerically. For external numerical comparison, use an exported FACETS fit table and facets_fit_review().

When that comparison involves an MML fit, remember that mfrmr evaluates residual-based fit statistics at shrunken EAP person measures while FACETS uses JMLE estimates, so MnSq differences can reflect the residual basis rather than a fit-computation difference; refit with method = "JML" before attributing such gaps. See facets_fit_df_guide() for this boundary and for the separate df/ZSTD standardization conventions.

If you already have a FACETS fit table on disk, read it first and then run the fit review. This does not run FACETS; it consumes an exported or otherwise harmonized table.

facets_fit <- read_facets_fit_table(
  "score.2.txt",
  facet_map = c("1" = "Person", "2" = "Rater", "3" = "Criterion")
)
review <- facets_fit_review(
  fit,
  diagnostics = diag,
  facets_fit = facets_fit,
  external_zstd_tolerance = 0.05
)

review$df_sensitivity
review$df_sensitive
review$external_table_quality
review$external_comparison
plot(review, type = "df_sensitivity")

Use external_comparison for the supplied FACETS table and df_sensitivity for the engine-vs-FACETS-style df convention check. This separation keeps external numerical differences distinct from ZSTD differences caused by df standardization. external_table_quality is the first place to look if the FACETS export only contains ZStd and T.Count columns, or if duplicate Facet x Level rows were supplied.

Producing FACETS-style output files

For traceability or downstream tools that expect FACETS output files, facets_output_file_bundle() writes a parallel set of fixed-width or CSV exports:

files <- facets_output_file_bundle(
  fit,
  diagnostics = diag,
  out_dir = tempdir(),
  include = c("graph", "score")
)

For RSM and PCM the score-side helpers are available. Under bounded GPCM the score-side bundle is intentionally restricted; see ?gpcm_capability_matrix and the mfrmr-gpcm-scope vignette for the documented limitation.