--- title: "Your first capsule: a published table, analysed and recorded" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Your first capsule: a published table, analysed and recorded} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") ``` Most published administrative data arrives as a table of counts by period and group. The questions are always the same: what changed, how sure can we be, is there a trend, and is this release the one we had before. This vignette answers them for one real table in twenty lines, then records the run so someone else can repeat it. ## The table Ontario publishes the number of distinct individuals held in restrictive confinement each fiscal year, by age category and gender. A tidy copy of the published figures ships with the package. ```{r} library(rmoriebricklayer) otis <- read.csv(system.file("extdata", "otis_a01_individuals.csv", package = "rmoriebricklayer")) otis ``` ## One call ```{r} a <- analyse_table(otis, value = "individuals", period = "year", by = c("table", "group")) a ``` The change table carries an exact interval for every percent change and a p-value that is exact too. Five groups are compared twice each, so the p-values are adjusted over the scan (Benjamini-Hochberg) before any row is called significant. A row that looks striking on its own is judged in the company of the other nine. ```{r} a$change[, c("table", "group", "year", "value", "previous", "pct_change", "pct_lower", "pct_upper", "p_adjusted", "significant")] ``` ## What the release withheld Published counts are often rounded. If this table had been rounded to the nearest five, each cell could have come from any count within two and a half of it, and the percent change inherits that. `rounding = 5` adds the envelope and a combined interval that is the union of the sampling interval and the envelope: the honest range for a number read off a rounded table. Suppressed cells (`"x"`, `"<5"`) are handled the same way through `suppression_limit`. ```{r} b <- analyse_table(otis, value = "individuals", period = "year", by = c("table", "group"), rounding = 5) b$change[!is.na(b$change$previous), c("group", "year", "pct_change", "pct_lower", "pct_upper", "combined_pct_lower", "combined_pct_upper")] ``` ## Trend Three fiscal years is a short series. The Mann-Kendall test is exact for it, and its smallest possible p-value with three points is one third, so no trend can be declared from three years; the table says so instead of hiding it. The Poisson rate ratio per year is still informative as a description. ```{r} a$trend ``` ## Has the data changed since last time? Keep the previous release and pass it as `prior`; every column is screened. Before trusting a drift verdict, ask how often the screens fire on data that has not changed: `drift_calibrate()` splits one release into random halves and reports the false-alarm rate per column. ```{r} drift_calibrate(otis, n = 10) ``` ## Stock and flow A count of people in confinement on a day is a stock; the people who pass through in a year are a flow. Lakner's decomposition ties them exactly: average daily population is person-days over the days in the period, average length of stay is person-days over the people served, and the identity `adp = admissions * alos / t` means a change in days must multiply out of a change in people and a change in stay. `stock_flow()` reports both sides and checks the identity. ```{r} sf <- stock_flow(days = c(21900, 23725, 20440), people = c(300, 325, 280), period = c(2022, 2023, 2024), t = 365) sf ``` A population that rose while stays shortened, or fell while stays lengthened, reads differently from one where both moved together; `adp()`, `alos()` and `admissions()` give the single measures. ## Stock and flow A count of people in confinement on a day is a stock; the people who pass through in a year are a flow. Lakner's decomposition ties them exactly: average daily population is person-days over the days in the period, average length of stay is person-days over the people served, and the identity `adp = admissions * alos / t` means a change in days must multiply out of a change in people and a change in stay. `stock_flow()` reports both sides and checks the identity. ```{r} sf <- stock_flow(days = c(21900, 23725, 20440), people = c(300, 325, 280), period = c(2022, 2023, 2024), t = 365) sf ``` A population that rose while stays shortened, or fell while stays lengthened, reads differently from one where both moved together; `adp()`, `alos()` and `admissions()` give the single measures. ## Record it `report_analysis()` writes the whole analysis as Markdown or a single HTML file, and `use_capsule_template()` writes a folder with the provenance file, an `analysis.R` that pins the source bytes and runs the steps above, and a README. The template runs as written against this table. ```{r} d <- use_capsule_template(tempfile("capsule-"), example = TRUE) list.files(d) cat(readLines(file.path(d, "analysis.R"))[1:12], sep = "\n") ``` From here, replace the source URL and the column names in `data_provenance.json`, run `Rscript analysis.R`, and copy the SHA-256 it prints back into the provenance file. Later runs refuse to proceed if the bytes change, and `manifest_recompute()` compares two runs number by number.