---
title: "Reproducible analytical summaries"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Reproducible analytical summaries}
%\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(hyperannotatR)
```
This example follows persisted annotations through masks, raw pixel records,
ROI spectra, image summaries, and agreement tables. It uses the small synthetic
reference bundle in the source repository at
`tests/testthat/fixtures/analytical/`. Its NumPy generator computes the numerical
reference independently of hyperannotatR. The workflow never reads the expected JSON
to calculate its results; the regression test uses that JSON only for comparison.
The example functions are installed at `examples/reproducible-analysis.R`.
They are reusable source code, not exported package APIs. The fixture bundle is
kept in the repository tests rather than duplicated in the installed package.
To reproduce this vignette, use a source checkout with the reviewed package code
installed, or use `pkgload::load_all()` from that checkout first. Run the following
code from the repository root (the vignette build uses the equivalent relative
path from its own directory).
```{r load-example}
source(system.file("examples", "reproducible-analysis.R", package = "hyperannotatR"))
```
```{r root-path, eval=FALSE}
fixture_dir <- normalizePath("tests/testthat/fixtures/analytical")
```
```{r vignette-path, include=FALSE}
fixture_dir <- normalizePath("../tests/testthat/fixtures/analytical", mustWork = TRUE)
```
Create the input once, then reopen it for every analysis. The synthetic entry ID
is `analytical-image-001`; the two imported ROI IDs are preserved. The settings
record `at_annotation_revision(project)`, a SHA-256 digest of the annotation
content (`"sha256:..."`): rerunning on the same persisted annotations records the
same revision, and any change to the annotations records a different one. For an
adapted study, retain its actual entry IDs and analyse the persisted project
rather than reimporting annotations into a fresh project for every run.
```{r persist}
work <- tempfile("analytical-example-")
dir.create(work)
project_path <- file.path(work, "annotations.rds")
prepare_analytical_example(fixture_dir, project_path)
first <- run_analytical_example(project_path, fixture_dir, file.path(work, "first"))
second <- run_analytical_example(project_path, fixture_dir, file.path(work, "second"))
stopifnot(identical(first, second))
files <- list.files(file.path(work, "first"))
stopifnot(identical(
unname(tools::md5sum(file.path(work, "first", files))),
unname(tools::md5sum(file.path(work, "second", files)))
))
```
Both functions refuse to overwrite existing outputs. The analysis does not save
or mutate its input project. The ten CSV tables and `settings.json` are
deterministic for the same persisted input and software environment. Source
paths, signatures, input hashes and software versions deliberately remain in
the settings; relocating files or changing software can change provenance.
No run timestamp or runtime cache/read-generation identity enters these outputs.
The saved project's own historical timestamps remain in that input file and
therefore in its checksum. Optional execution timestamps belong in a separate
run log, not in a result table or its deterministic configuration.
## Counts and estimands
Rectangles A and B each select nine pixels on the 4 by 5 grid. They share two
pixels. The bitfield mask assigns A=1, B=2, overlap=3 and background=0.
Membership is determined at polygon pixel centres at level zero with
`touches=FALSE`, `engine="stars"` and `overlap="bitor"`.
```{r counts}
first$tables$counts
first$tables$mask
first$tables$mask_codebook
```
| Table | Row or count unit | Overlap treatment |
|---|---|---|
| `pixel_memberships` | One ROI-pixel-band record: 54 rows | Shared cells appear once per ROI |
| `unique_pixels` | One image-pixel-band record: 48 rows | Deduplicate `(x,y,band)` within this one image |
| `roi_statistics` | One ROI-band-statistic: 42 rows | Each ROI is summarized separately |
| `spectra` | One ROI-band finite mean: 6 rows | Each ROI has its own spectrum |
| `image_statistics` | One image-band summary: 3 rows | Each selected cell contributes once |
| `counts` | One image: 16 unique cells, 18 ROI memberships, 2 ROIs | Two overlap cells counted explicitly |
The `x` and `y` columns are one-based column/row indices, whereas ROI geometry
uses zero-based image coordinates. Band identity and spectral coordinates are
recorded in settings. `unit="nm"` describes the spectral coordinate, not the
intensity values. No intensity unit or physical calibration is inferred: this
cube declares pixel calibration `(1,1)` in `px`. Settings explicitly mark its
physical calibration unknown, so no physical-area result is produced.
The image summary below estimates finite-only statistics across the **unique
selected pixels within this image**. Its band-one sum is 1937, with 15 finite
contributors out of 16 selected cells, so its mean is `1937/15`. Pooling repeated
ROI records would use 17 finite contributors and sum 2193. Averaging ROI means
would weight the two ROIs equally. Those are different estimands and neither is
silently substituted for the unique-pixel image summary.
```{r image-summary}
first$tables$image_statistics
stopifnot(first$tables$image_statistics$sum[1] == 1937)
stopifnot(first$tables$image_statistics$mean[1] == 1937 / 15)
```
## Nonfinite values and spectra
`at_extract(..., stat="all", nonfinite="omit")` and
`at_extract_spectrum(..., nonfinite="omit")` choose finite-only statistics
explicitly. `n_px` and the statistic `n` still mean selected support. `n_valid`
counts finite contributors; `n_invalid` counts selected NA, NaN and infinite
values. Sample SD uses denominator `n_valid - 1`; it is undefined for a singleton.
All-invalid summaries, including sum, are NA. Empty selections have zero
contributors; the example retains an image-band row with NA statistics.
```{r spectra}
first$tables$spectra[c("label", "band", "wavelength", "unit", "value",
"n_px", "n_valid", "n_invalid")]
```
The raw pixel API retains nonfinite samples. There are four unique invalid
pixel-band values but five invalid ROI-pixel-band records, because the positive
infinity in the overlap belongs to both ROIs. The raw tables include
`sample_status` (`finite`, `NA`, `NaN`, `+Inf`, `-Inf`) to preserve distinctions
that CSV missing-value serialization could collapse. The four unselected
background cells are a spatial exclusion, separate from invalid selected
samples. `n_excluded_rois` counts ROIs with no selected cells; it is zero here.
The example applies no ROI or label filters.
This workflow produces numerical spectra and tables. It generates no image
preview. Display normalization for the mixed NaN/Inf RGB fixture remains outside
this qualification; no RGB or native ImageMagick success is claimed.
## Agreement with explicit alignment
The separate 3 by 4 categorical and bitfield NPY cases exercise agreement; these
are not prediction masks for the 4 by 5 cube. The independent fixture generator
declares their common row/column grids. Since arrays alone cannot prove source
registration, this example uses `alignment="assert"` and records that basis.
For study masks with durable common source/grid metadata, use
`alignment="check"` to verify it. Equal dimensions alone are not evidence of
registration, and assertion never resamples mismatched grids.
`at_read_npy()` receives explicit encoding, background and label codebooks.
Ordinary comparisons use `code_alignment="strict"`. The deliberately swapped
categorical codes use `code_alignment="by-label"`, preserving original codebooks
in settings. An absent class remains a table row with undefined Dice/IoU, and
macro means exclude such undefined rows.
```{r agreement}
first$tables$agreement
first$tables$agreement_overall
```
Categorical accuracy is 3/4, Cohen's kappa is 5/8, and A/B Dice and IoU are 3/4
and 3/5. Explicit remapping reproduces them. Bitfield A has Dice/IoU 1; B has
Dice 3/5 and IoU 3/7. Its overall accuracy of 2/3 is exact membership-set
agreement, including background; categorical kappa is undefined for bitfields.
Each overall agreement row counts all 12 cells, including background. Its
nonfinite policy is `error`.
## Traceability and scope
`settings.json` accompanies the tables with the persisted entry/ROI IDs, the
annotation revision digest, durable source descriptor and reader options, MD5 hashes of the
project and all eight data inputs (including the ENVI payload), calibration,
band coordinates/units, extraction and rasterization settings, mask codebook,
both sides' validated agreement metadata/codebooks, estimands, and count
definitions. It records the actual R and dependency versions and sf external
library versions. The ENVI backend is implemented by hyperannotatR, whose version is
recorded; separately unavailable plugin-version metadata is explicitly
`"unknown"`. MD5 checks exact-byte integrity, not authenticity. The source tree
or build artifact used for a study should also be archived with these outputs.
```{r settings}
first$settings$sampling_units
first$settings$extraction
first$settings$versions
```
This is a descriptive example for one image and two overlapping ROIs. Pixels,
bands and ROI means are not independent image replicates. Inferential uncertainty
requires the study's independent sampling unit (for example a donor, specimen or
image, as justified by its design), with nesting and overlap handled explicitly.
There is no resampling here and the recorded seed is null. Any added resampling
must declare its seed and independent unit. No model training or clinical claim
is part of this example.
The implementation retains existing extraction memory bounds: one bounding-box
tile per ROI, a `max_px=1e6` raw extraction guard, and in-memory deduplication and
tables. It is a small synthetic workflow, not a streaming analysis engine or
whole-slide resource qualification.
```{r cleanup, include=FALSE}
unlink(work, recursive = TRUE)
```