---
title: "Data structures for rasch models"
author: "Joshua A. McGrane"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Data structures for rasch models}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

```{r library}
library(rasch)
```

This vignette shows the data required by each model. The examples use plain
data frames so that the required columns are visible. Column names can differ;
name their roles in the fitting function.

## Rasch, partial credit and rating scale models

Use one row per person and one column per item. Scores begin at zero. A
dichotomous item is coded 0/1; an item with four categories is coded 0, 1, 2
or 3. Missing values may be `NA`; negative missing-value codes are also
accepted through `na_codes`.

```{r wide-data}
responses <- data.frame(
  person = c("P01", "P02", "P03", "P04"),
  group = c("control", "control", "treated", "treated"),
  I1 = c(0, 1, 1, 1),
  I2 = c(0, 1, 2, 2),
  I3 = c(1, 2, 3, NA)
)
responses
```

The identifier and person factors are not item responses. Name them
explicitly, or nominate the item columns.

```{r ordinary-fit, eval=FALSE}
fit <- rasch(
  responses,
  id = "person",
  factors = "group",
  items = c("I1", "I2", "I3"),
  model = "PCM"
)
```

Use `model = "RSM"` only when the items share one rating-scale threshold
structure. Items may have different maximum scores in the partial credit
model.

External weights are supplied after fitting. Item weights are a named numeric
vector. Set weights additionally need an item-to-set map, except for an EFRM
fit, which already contains one.

```{r person-weights, eval=FALSE}
item_weights <- c(I1 = 2, I2 = 1, I3 = 0.5)
weighted <- weighted_person_estimates(fit, item_weights)

set_of <- c(I1 = "core", I2 = "core", I3 = "extension")
set_weights <- c(core = 2, extension = 1)
weighted_person_estimates(fit, set_weights, by = "set", sets = set_of)

save_outputs(fit, "analysis", person_weights = weighted) # new or empty folder
```

Only relative weights matter. The weighted estimates are supplementary; they
do not replace the Rasch estimates used for fit, reliability, targeting or
DIF. In the application, use a CSV with `item,weight`, or
`item,set,weight` for set weights. The computed table can also be passed to
`save_outputs()`, `report_html()` or `report_document()`.

## Explanatory item and threshold models

The response data remain in the wide form above. A second data frame --- the
one uploaded as a CSV in the application --- describes the items. Its shape
depends on the level being explained, and that is the distinction to get
right before anything else.

An **item-level** design has one row per item. The predictors describe the
item as a whole, and the model explains the item's location.

A **threshold-level** design has one row per fitted *threshold*, identified by
`item` and `threshold` together. The predictors may vary within an item, and
the model explains each threshold separately.

The number of rows a threshold design needs is fixed by the data, not chosen:
an item scored 0 to *m* has *m* thresholds. For the responses above that is
one for `I1`, two for `I2` and three for `I3` --- six rows, not three.

```{r threshold-count}
items <- c("I1", "I2", "I3")
maxima <- vapply(responses[items], max, 0, na.rm = TRUE)
data.frame(item = items, max_score = maxima, thresholds = maxima,
           row.names = NULL)
sum(maxima)          # rows a threshold-level design must have
```

### One row per item

```{r item-predictors}
item_design <- data.frame(
  item = items,
  format = c("selected", "constructed", "constructed"),
  demand = c(0.2, 0.7, 1.1)
)
item_design
```

```{r explanatory-item-fit, eval=FALSE}
fit <- rasch_explanatory(
  responses,
  predictors = item_design,
  formula = ~ format + demand + format:demand,
  level = "item",
  id = "person",
  factors = "group",
  items = items
)
```

### One row per threshold

The same two predictors at threshold level. `format` is a property of the
item, so it simply repeats down that item's thresholds; `demand` now varies
between them, which is the reason to work at this level at all. An item-level
predictor is not excluded from a threshold design --- it is constant within
each item.

```{r threshold-predictors}
threshold_design <- data.frame(
  item = rep(items, maxima),
  threshold = sequence(maxima),
  format = rep(c("selected", "constructed", "constructed"), maxima),
  demand = c(0.2,
             0.5, 0.9,
             0.4, 0.8, 1.4)
)
threshold_design
nrow(threshold_design) == sum(maxima)
```

```{r explanatory-threshold-fit, eval=FALSE}
fit <- rasch_explanatory(
  responses,
  predictors = threshold_design,
  formula = ~ format + demand,
  level = "threshold",
  id = "person",
  factors = "group",
  items = items
)
```

`threshold` numbers the thresholds within an item, from 1, in order. It is
not a running count across the instrument: `I2` has thresholds 1 and 2, and
so does the first half of `I3`. `sequence(maxima)` above generates exactly
that pattern, and is worth using in place of typing the column out.

Three mistakes account for most refused designs. Each is named precisely
when it happens, so the message identifies the rows to fix:

- one row per item where the level is `"threshold"`, leaving the design short
  by every threshold after the first, and every threshold missing where the
  numbering does not match. Both report *threshold-level predictors are
  missing*, listing the item and threshold of each;
- thresholds numbered continuously across the instrument (1 to 6 here)
  rather than from 1 within each item, which reports the same missing
  combinations alongside *predictor rows do not correspond to an observed
  item threshold* for the numbers that exist nowhere;
- a repeated `item` and `threshold` pair, which reports *threshold-level
  predictors need one row per item and threshold*.

Categorical predictors should be factors, continuous predictors numeric, and
ordinal predictors ordered factors with their substantive order declared. In
the application, upload the same table as a CSV: `item` plus the predictor
columns at item level, and `item,threshold` plus the predictor columns at
threshold level. The predictor type of each column is set after the upload,
so a column read as text can still be declared ordinal with its order given.

## Multiple ratings

Long data use one row per observed rating. The required roles are person,
item, score and at least one facet. A rater, task or occasion can be a facet.
A person-group variable is instead supplied through `factors`.

```{r mfrm-long}
ratings <- data.frame(
  person = c("P01", "P01", "P02", "P02"),
  item = c("Essay1", "Essay2", "Essay1", "Essay2"),
  rater = c("R1", "R2", "R2", "R1"),
  occasion = c("first", "first", "first", "first"),
  score = c(2, 3, 1, 2)
)
ratings
```

```{r mfrm-fit, eval=FALSE}
fit <- rasch_mfrm(
  ratings,
  person = "person",
  item = "item",
  score = "score",
  facets = c("rater", "occasion")
)
```

Wide multiple-ratings data use one row per person-by-facet combination and
one score column per item.

```{r mfrm-wide}
wide_ratings <- data.frame(
  person = c("P01", "P01", "P02", "P02"),
  rater = c("R1", "R2", "R1", "R2"),
  Essay1 = c(2, 3, 1, 2),
  Essay2 = c(3, 2, 2, 2)
)
wide_ratings
```

```{r mfrm-wide-fit, eval=FALSE}
fit <- rasch_mfrm(
  wide_ratings,
  person = "person",
  facets = "rater",
  items = c("Essay1", "Essay2")
)
```

Long data are usually easier to inspect when a facet varies within items.

## Extended Frames

EFRM response data use one row per person and one column per item, together
with a person-group column. A separate named map assigns each item to exactly
one item set. Supplied person identifiers must not repeat; missing or blank
identifiers are treated as different unknown persons.

```{r efrm-structure}
frame_data <- data.frame(
  person = paste0("P", 1:6),
  group = rep(c("A", "B"), each = 3),
  S1I1 = c(0, 1, 1, 0, 1, 1),
  S1I2 = c(0, 0, 1, 0, 1, 1),
  S2I1 = c(0, 1, 1, 0, 0, 1),
  S2I2 = c(0, 1, 1, 0, 1, 1)
)
frame_data

item_sets <- list(
  set1 = c("S1I1", "S1I2"),
  set2 = c("S2I1", "S2I2")
)
item_sets
```

```{r efrm-fit, eval=FALSE}
fit <- rasch_efrm(
  frame_data,
  item_sets = item_sets,
  groups = "group",
  id = "person"
)
```

Persons must connect the item sets, and common items across person groups must
identify the group units. Crossed person-group factors can be supplied as
several column names in `groups`.

## Comparative Judgement

Use one row per comparison. `object_a` and `object_b` identify the pair. For a
dichotomous comparison, `winner` contains one of those two object names.

```{r cj-data}
comparisons <- data.frame(
  object_a = c("A", "A", "B", "A"),
  object_b = c("B", "C", "C", "C"),
  winner = c("A", "C", "B", "A"),
  judge = c("J1", "J1", "J2", "J2")
)
comparisons
```

```{r cj-fit, eval=FALSE}
fit <- btl(
  comparisons,
  object_a = "object_a",
  object_b = "object_b",
  winner = "winner",
  judge = "judge"
)
```

For ordered comparisons, replace `winner` with an ordered response column
whose categories run from preference for object B to preference for object A.
A frequency column can represent repeated identical rows. A judgement-order
column is needed to estimate exposure or carry-over dependence.

Explanatory Comparative Judgement adds one metadata row per object.

```{r cj-explanatory}
object_design <- data.frame(
  object = c("A", "B", "C"),
  genre = factor(c("essay", "essay", "report")),
  length = c(800, 950, 700)
)
object_design
```

```{r cj-explanatory-fit, eval=FALSE}
fit <- btl_explanatory(
  comparisons,
  predictors = object_design,
  formula = ~ genre + length,
  object_a = "object_a",
  object_b = "object_b",
  winner = "winner",
  judge = "judge"
)
```

## Extended Frames for Comparative Judgement

The comparison rows additionally identify each judge's panel. A named list
assigns every object to one object set. Both within-set and cross-set
comparisons are required to identify set units and origins.

```{r cj-frames}
object_sets <- list(
  set1 = c("S1A", "S1B", "S1C"),
  set2 = c("S2A", "S2B", "S2C")
)
object_sets

# the first four rows compare within a set, the last two across sets
frame_comparisons <- data.frame(
  object_a = c("S1A", "S1B", "S2A", "S2B", "S1A", "S1C"),
  object_b = c("S1B", "S1C", "S2B", "S2C", "S2A", "S2C"),
  winner   = c("S1A", "S1C", "S2A", "S2B", "S2A", "S1C"),
  judge    = c("J1", "J1", "J2", "J2", "J3", "J3"),
  panel    = c("east", "east", "west", "west", "east", "east")
)
frame_comparisons
```

```{r cj-frames-fit, eval=FALSE}
fit <- btl_efrm(
  frame_comparisons,
  object_a = "object_a",
  object_b = "object_b",
  winner = "winner",
  judge = "judge",
  panels = "panel",
  object_sets = object_sets
)
```

## Simulated data

The **Simulate** page in the Shiny application generates every structure
above, assigns its roles and retains the generating values. The corresponding
R functions are `simulate_rasch()`, `simulate_btl()`, `simulate_mfrm()`,
`simulate_efrm()` and `simulate_btl_efrm()`. Explanatory simulations add the
item or object metadata used to generate the locations. Each simulator stores
its parameter values and planted departures in `attr(data, "truth")`. The app
can download the data as a CSV or as a bundle containing the generating call,
true values and explanatory metadata.

```{r simulation-map}
d <- simulate_efrm(n_per_group = 100, items_per_set = 5, seed = 4)
names(attr(d, "truth"))
```
