---
title: "Getting started with infercalcR"
author: "Raban Heller"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Getting started with infercalcR}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
resource_files:
- figures/logo.svg
- figures/logo-dark.svg
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(infercalcR)
```
infercalcR carries out statistical procedures that the analyst names in
advance. It never chooses a test from the data, for example after a normality
pretest. Every procedure returns the same kind of object: a table of finalized
rows (estimates, intervals, p-values, counts and a status), a list of metadata
(method identity, settings, population and provenance) and a method-specific
payload.
This tutorial follows that workflow on a small synthetic data set: describe the
data, specify a method, compute it and read the result. It then converts results
produced by base R and adjusts a declared family of p-values. The code runs
offline and, apart from one optional chunk, needs only the packages that ship
with R.
## Synthetic data
The data imitate a two-arm study with three continuous outcomes and one binary
characteristic. Two blood-pressure values are missing on purpose.
```{r data}
set.seed(2026)
trial <- data.frame(
id = 1:24,
arm = factor(rep(c("control", "treatment"), each = 12)),
sbp = round(c(rnorm(12, mean = 142, sd = 11), rnorm(12, mean = 132, sd = 11)), 1),
dbp = round(c(rnorm(12, mean = 88, sd = 8), rnorm(12, mean = 84, sd = 8)), 1),
heart_rate = round(c(rnorm(12, mean = 74, sd = 9), rnorm(12, mean = 73, sd = 9))),
smoker = factor(sample(c("no", "yes"), 24, replace = TRUE, prob = c(0.7, 0.3)))
)
trial$sbp[c(5, 17)] <- NA
head(trial)
```
## Describe the data
`describe_data()` summarizes variables, optionally within groups. It computes
no hypothesis tests. Continuous variables get the mean, standard deviation,
median, quartiles and range; categorical variables get counts and proportions.
```{r describe}
desc <- describe_data(
trial,
variables = c("sbp", "smoker"),
group = "arm",
labels = c(sbp = "Systolic blood pressure", smoker = "Current smoker"),
units = c(sbp = "mmHg")
)
class(desc)
desc_rows <- result_data(desc)
desc_rows[, c("group", "variable_id", "estimand", "level", "estimate",
"proportion", "n", "n.missing", "n.total")]
```
Missing values are counted in `n.missing` and kept apart from the `n` used for
each summary. Category proportions use the non-missing observations within a
group as their denominator. The result has the same structure as an inferential
result, so the same accessors work on it.
## Choose a method explicitly
`method_catalogue()` lists the method identities that `run_inference()` accepts,
the package that provides the computation and the option names each method
understands.
```{r catalogue}
catalogue <- method_catalogue()
dim(catalogue)
table(catalogue$implemented)
catalogue[c("t_means_two_independent_welch", "rank_sum_mann_whitney",
"regression_negative_binomial"), c("backend", "implemented")]
```
Most methods use only `stats` and `utils`. A few rely on an optional package,
such as MASS for negative binomial regression. Such a package is used only when
it is installed; if it is missing, the method stops with an explicit error and
no other model is fitted in its place. `?observed-methods` lists the arguments
that each method takes.
A specification fixes the method, the alternative hypothesis, the confidence
level, the missing-data policy and any method options before the data are seen.
```{r specification}
spec <- method_spec("t_means_two_independent_welch",
alternative = "two.sided", conf.level = 0.95)
str(unclass(spec))
```
Unknown options and impossible settings are rejected when the specification is
created, not silently ignored later.
```{r specification-errors}
tryCatch(
method_spec("t_means_two_independent_welch", options = list(var.equal = TRUE)),
error = conditionMessage
)
tryCatch(
method_spec("t_means_two_independent_welch", conf.level = 1),
error = conditionMessage
)
```
## Compute the result
The default missing-data policy is `"error"`. Because the blood-pressure values
contain missing entries, the Welch test refuses to run on them.
```{r missing-values}
treated <- trial$sbp[trial$arm == "treatment"]
control <- trial$sbp[trial$arm == "control"]
tryCatch(run_inference(spec, x = treated, y = control), error = conditionMessage)
```
`missing = "omit"` does not change this for the Welch test: the package does not
pick a missing-data assumption on the analyst's behalf. Here we decide to
analyze the complete observations and do so visibly in the code. A `result_id`
gives the result a stable name for later joins.
```{r compute}
x_complete <- treated[!is.na(treated)]
y_complete <- control[!is.na(control)]
welch <- run_inference(spec, x = x_complete, y = y_complete,
result_id = "sbp-welch")
welch
```
## Read the result contract
`result_data()` returns the finalized rows as a data frame. Every result has the
same mandatory columns; unavailable quantities are `NA`.
```{r rows}
rows <- result_data(welch)
rows[, c("estimand", "estimate", "conf.low", "conf.high", "conf.level",
"interval.method")]
rows[, c("statistic", "df", "p.value", "p.adjusted", "p.reported",
"alternative", "n", "n.excluded", "status", "reason")]
```
The `estimand` names the quantity, including its direction: here the mean of
`x` (treatment) minus the mean of `y` (control). The interval columns record
the level, the method and the scale. `p.value` is the raw p-value, `p.adjusted`
stays empty until a multiplicity adjustment is requested, and `p.reported` is
the value meant for display. Check `status` before using an estimate: besides
`"ok"`, a row can be `"undefined"`, `"not_applicable"`, `"unsupported"`,
`"invalid_input"`, `"numerical_failure"`, `"infeasible"`, `"reference_pending"`
or `"unresolved"`, with an explanation in `reason`.
`result_metadata()` records how the result came about.
```{r metadata}
meta <- result_metadata(welch)
meta[c("method_id", "variant_id", "result_id", "kind", "contract_version")]
str(meta$settings[c("alternative", "conf.level", "missing", "options")])
str(meta$population)
str(meta$provenance)
```
The payload keeps the complete output of the underlying computation, including
quantities that do not fit in a single row.
```{r payload}
str(welch$payload$native, give.attr = FALSE)
```
The numbers agree with a direct call to `stats::t.test()` on the same vectors.
```{r agreement}
reference <- stats::t.test(x_complete, y_complete)
stopifnot(
isTRUE(all.equal(rows$p.value, reference$p.value)),
isTRUE(all.equal(c(rows$conf.low, rows$conf.high),
as.numeric(reference$conf.int)))
)
```
## Prespecified alternatives use the same pattern
A different procedure is another specification, not a fallback chosen after
looking at the data. The Mann-Whitney rank-sum test, for example, reports a
probability of superiority as its descriptive estimate. The `exact` option is
declared in the specification.
```{r rank-sum}
rank_spec <- method_spec("rank_sum_mann_whitney", options = list(exact = FALSE))
rank_sum <- run_inference(rank_spec, x = x_complete, y = y_complete,
result_id = "sbp-rank-sum")
result_data(rank_sum)[, c("estimand", "estimate", "p.value", "n", "status")]
```
## Choose the engine
Thirteen methods, the Brunner-Munzel test among them, have two engines.
`engine = "base"`, the default, uses infercalcR's own implementation;
`engine = "reference"` computes the same procedure with the validated original
package, here brunnermunzel. The original is optional: this chunk runs only when
it is installed. Without it, the reference engine stops with an error instead
of using the base engine.
```{r engines, eval = requireNamespace("brunnermunzel", quietly = TRUE)}
bm_base <- run_inference(method_spec("brunner_munzel"), x = x_complete, y = y_complete)
bm_reference <- run_inference(method_spec("brunner_munzel", engine = "reference"),
x = x_complete, y = y_complete)
rbind(base = result_data(bm_base)[1, c("estimate", "p.value")],
reference = result_data(bm_reference)[1, c("estimate", "p.value")])
result_metadata(bm_reference)$engine
```
## Convert results computed elsewhere
`as_inference()` turns a supported `htest`, `lm` or `glm` object into the same
contract. It does not refit anything. Sample sizes are not recovered from
degrees of freedom, so they are supplied together with a description of the
analysis population.
```{r convert-htest}
imported <- as_inference(
reference,
n = c(length(x_complete), length(y_complete)),
population = "Synthetic trial, complete observations",
groups = c("treatment", "control")
)
result_data(imported)[, c("estimand", "estimate", "conf.low", "conf.high",
"p.value", "n")]
```
A fitted linear model is summarized once, with classical t intervals for each
coefficient.
```{r convert-lm}
model <- lm(sbp ~ arm, data = trial)
coefficients <- result_data(
as_inference(model, population = "Synthetic trial, complete observations")
)
print(coefficients[, c("contrast", "estimate", "conf.low", "conf.high", "p.value")],
row.names = FALSE)
```
`lm()` drops the two incomplete rows through its own `na.action`; the population
label states this explicitly.
## Adjust a declared family of p-values
Suppose the three continuous outcomes form a prespecified family of secondary
comparisons. Each is analyzed with the same specification, and the rows are
collected into one result with `new_inference_result()`.
```{r family}
secondary <- lapply(c("sbp", "dbp", "heart_rate"), function(variable) {
values <- trial[[variable]]
keep <- !is.na(values)
fit <- run_inference(spec,
x = values[keep & trial$arm == "treatment"],
y = values[keep & trial$arm == "control"])
out <- result_data(fit)[, c("estimand", "estimate", "conf.low", "conf.high",
"conf.level", "p.value", "alternative", "n")]
out$row_id <- variable
out
})
family <- new_inference_result(
do.call(rbind, secondary),
metadata = list(method_id = "t_means_two_independent_welch",
result_id = "secondary-outcomes")
)
class(validate_inference(family))
```
`adjust_inference()` adjusts the complete family once and records the family
identifier and size. The raw p-values and the confidence intervals stay
unchanged.
```{r adjust}
adjusted <- adjust_inference(family, method = "holm", family_id = "secondary")
result_data(adjusted)[, c("row_id", "estimate", "p.value", "p.adjusted",
"p.reported", "p.method", "family_id", "family_size")]
```
A second adjustment of the same rows is refused, so a reported p-value cannot
be adjusted twice by accident.
```{r adjust-twice}
tryCatch(adjust_inference(adjusted, family_id = "secondary-again"),
error = conditionMessage)
```
## Keep a record
A result is an ordinary S3 list and can be saved as RDS. Load RDS files only
from trusted sources.
```{r save}
path <- tempfile(fileext = ".rds")
saveRDS(welch, path)
restored <- readRDS(path)
stopifnot(identical(result_data(restored), result_data(welch)))
unlink(path)
```
Archive the specification and the result together with the package version
(recorded in the provenance metadata), the repository revision and the output of
`sessionInfo()`.
## Continue
The [diagnostic accuracy tutorial](classification.html) covers classifier
metrics and cutoffs; the [exact tests tutorial](exact-tests.html) covers
Boschloo's test for two-by-two tables. `?result-contract` describes the result
columns in full.
```{r acknowledgments, child = "children/_acknowledgments.Rmd"}
```
```{r session-information}
sessionInfo()
```