---
title: "Introduction to Leunbach test equating"
author:
  - name: "Magnus Johansson, PhD"
    email: "pgmj@pm.me"
    affiliation: "Karolinska Institutet, Department of Clinical Neuroscience"
    orcid: "0000-0003-1669-592X"
date: 2026-01-27
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Introduction to Leunbach test equating}
  %\VignetteEncoding{UTF-8}
  %\VignetteEngine{knitr::rmarkdown}
editor_options: 
  chunk_output_type: console
---

This document provides an overview of how to do direct and indirect equating using the `leunbachR` package for R, which is based on the DIGRAM implementation of the Leunbach method.

Direct equating is made using the common person link between tests, meaning that the same individuals have taken two different tests that do not share any items.

Indirect equating connects test A and C through test B, where some respondents have taken both tests A and B and some have taken both tests B and C, but none have taken tests A and C, thus the indirect connection between A and C through B.

A basic assumption of the Leunbach method is that the observed sum score is a sufficient metric for the latent score, meaning that the items underlying the sum score fulfill the psychometric requirements of a Rasch model.

## Setup

First, we load the package and set our seed for reproducibility of bootstrap results.

```{r}
library(leunbachR)
set.seed(1234) # for reproducibility of bootstrap results
```

## Direct equating

Looking at our data, we can see that it has two variables containing sum scores from the two tests that we want to equate. This is the type of input expected by the functions in `leunbachR`. Data needs to be in the form/class of either a data.frame or matrix.

```{r}
d3a_path <- system.file("extdata", "data3a.csv", package = "leunbachR")
d3a <- read.delim(d3a_path, sep = ";")
head(d3a)
```

First, we estimate the model.

```{r}
fit <- leunbach_ipf(d3a, verbose = FALSE)
```

For most functions in this package, you can use `print()`, `summary()`, and `plot()` to investigate the results. All functions have documentation that you can access using for instance `?leunbach_ipf` in the R console.

```{r}
summary(fit)
```

```{r}
print(fit)
```


### Analyze orbits

```{r}
orb <- analyze_orbits(fit)
summary(orb)
print(orb)
```

```{r}
plot(orb)
plot(orb, type = "significant")
```

Specific total scores can also be analyzed.

```{r}
get_orbit(orb, total_score = 5)
```

### Equating

```{r}
leunbach_equate(fit, direction = "1to2")
```

### Bootstrap

If you have installed the package `mirai` and have a computer with multiple CPU cores, you can significantly reduce the time needed for bootstrap. Note that not all cores are equal. For instance, on a modern Mac, you should only use the "performance cores", not the "efficiency cores". Setting the option `verbose = TRUE` outputs a progress bar.

For the purposes of this vignette, we only use 100 bootstrap iterations. It is recommended to use at least 1000.

```{r}
boot <- leunbach_bootstrap(fit, n_cores = 4, verbose = TRUE, nsim = 100)
```

```{r}
print(boot)
```

You can also get the equating table as a separate object.

```{r}
get_equating_table(boot)
```

Write the table to a CSV file (this vignette writes to a temporary
directory; in your own work, replace `tempdir()` with the path you want).

```{r}
write.csv(get_equating_table(boot),
          file = file.path(tempdir(), "eqtable.csv"),
          row.names = FALSE)
```


## Indirect equating

For this, we read a dataset with three tests.

```{r}
d1_path <- system.file("extdata", "data1.csv", package = "leunbachR")
d1 <- read.delim(d1_path, sep = ";")
head(d1)
```

We will estimate two models before the indirect equating procedure. Test A with Test B; and Test B with Test C. Then the results will be used to indirectly equate Test A with Test C

The code below uses base R methods to select the columns in the dataframe, first 1 and 2, second 2 and 3.

```{r}
fit_ab <- leunbach_ipf(d1[,c(1,2)])
fit_bc <- leunbach_ipf(d1[,c(2,3)])
```

You can of course use the objects `fit_ab` and `fit_bc` to analyze orbits too, but we'll skip that step here.

```{r}
indirect1 <- leunbach_indirect_equate(fit_ab, fit_bc,
                                     direction_ab = "1to2",
                                     direction_bc = "1to2")
print(indirect1)
```

To get the table as a dataframe:

```{r}
id1table <- indirect1[["equating_table"]]
```


### Bootstrap

Again, only using 100 simulations for demonstration purposes.

```{r}
boot_indirect1 <- leunbach_indirect_bootstrap(fit_ab, fit_bc,
                                             direction_ab = "1to2",
                                             direction_bc = "1to2",
                                             nsim = 100,
                                             verbose = TRUE, n_cores = 4)
```

```{r}
print(boot_indirect1)
summary(boot_indirect1)
```

Get a clean table and write to a CSV file (again writing to a temporary
directory so the vignette does not touch the user's filespace).

```{r}
indirect_table <- get_indirect_equating_table(boot_indirect1)
indirect_table
write.csv(indirect_table,
          file = file.path(tempdir(), "indirect_table.csv"),
          row.names = FALSE)
```


```{r}
plot(boot_indirect1, type = "equating")
```


```{r}
plot(boot_indirect1, type = "see")
```

