--- title: "Getting started with reproducr" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting started with reproducr} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = TRUE ) library(reproducr) ``` ## What is reproducr? You finish an analysis. The code runs. The numbers look right. But are they stable? Package updates change function behaviour silently. Stochastic code without a fixed seed produces different results on every run. Results certified last month may drift this month — with no error and no warning. `reproducr` makes these risks visible and trackable via a three-tier workflow: 1. **Scan & score** — parse your scripts and assess risk 2. **Baseline & drift** — certify outputs and detect changes over time 3. **Report & export** — generate human-readable audit reports It works with your existing setup. If you use `renv`, `reproducr` reads your lockfile automatically. No configuration required. --- ## Why this matters — real failure modes These are not hypothetical. Each scenario describes a class of problem that occurs routinely in research and regulated workflows, produces no error, and is invisible without explicit tooling. ### Scenario 1 — The collaborator upgrade problem You write an analysis in January using dplyr 1.0.4 and share it with a colleague who has dplyr 1.1.2. ```r results <- mtcars |> dplyr::group_by(cyl) |> dplyr::summarise(mean_mpg = mean(mpg)) # You then chain a further operation: results |> dplyr::mutate(rank = dplyr::row_number()) ``` In dplyr 1.0.x, `summarise()` retained grouping by default. In dplyr 1.1.x it drops the last grouping level. Your colleague's `mutate()` now operates on ungrouped data — the `rank` column is computed differently. No error. No warning. Different numbers. `reproducr` flags this immediately: ``` [HIGH] dplyr::summarise In dplyr 1.1.0, summarise() changed its default grouping behaviour... ``` ### Scenario 2 — The server deployment problem You develop a model locally on R 3.5.3 and deploy to a production server running R 3.6.2. ```r set.seed(42) train_idx <- base::sample(1:nrow(data), 0.8 * nrow(data)) ``` R 3.6.0 changed the default RNG algorithm for `sample()`. The same seed now produces a different train/test split. Your model is trained on different data than you validated locally. Accuracy metrics differ silently across environments. `reproducr` flags this: ``` [HIGH] stats::sample In R 3.6.0, the default RNG algorithm changed... ``` ### Scenario 3 — The renv false sense of security You use `renv` to lock your environment and restore it six months later on a new machine. Everything installs correctly but results differ. `renv` locked `readr 2.0.1`. Your original analysis was written with `readr 1.4.0`. The lockfile captured the version you were *already on* when you ran `renv::init()` — past the breaking change. You never compared against pre-2.0 output. ```r data <- readr::read_csv("clinical_data.csv") # Column "patient_id" now parses as character instead of double. # Downstream merge silently drops rows. ``` `renv` cannot detect this because it only sees versions, not behaviour. `reproducr` sees the function call and flags it: ``` [HIGH] readr::read_csv In readr 2.0.0, read_csv() switched to the vroom backend. Column type guessing changed... ``` --- ## Tier 1: Scan and score ### Auditing a script The entry point is `audit_script()`. It reads your R source files, extracts every qualified `pkg::fn` call, and resolves which version of each package is in use. ```{r audit} # Create a small example script script <- tempfile(fileext = ".R") writeLines(c( "# Example analysis", "set.seed(237)", "x <- dplyr::filter(mtcars, cyl == 4)", "y <- dplyr::summarise(x, mean_mpg = mean(mpg), n = dplyr::n())", "fit <- lm(mpg ~ wt, data = x)", "z <- stats::rnorm(nrow(y))", "out <- base::sort(unique(x$gear))" ), script) report <- audit_script(script, renv = FALSE, verbose = FALSE) print(report) ``` ```{r calls} report$calls ``` ### Scoring for risk Pass the report to `risk_score()` to run three independent checks: ```{r risk} risks <- risk_score(report) print(risks) ``` - **`"changelog"`** — checks calls against a curated database of known silent breaking changes - **`"seed_check"`** — flags stochastic functions without a nearby `set.seed()` - **`"locale_check"`** — flags functions whose output varies by system locale ```{r risk-filtered} # High-severity only high_risks <- risk_score(report, min_risk = "high") # Just the seed check seed_issues <- risk_score(report, methods = "seed_check") ``` ```{r risk-df} # As a plain data frame for downstream use as.data.frame(risks) ``` ## Tier 2: Baseline and drift detection ### Certifying outputs After running an analysis, certify the key outputs using `certify()`. ```{r certify} cert_file <- tempfile() model <- lm(mpg ~ wt, data = mtcars) certify( outputs = list( coefs = coef(model), r_squared = summary(model)$r.squared, n_obs = nrow(mtcars) ), tag = "baseline-v1", script = script, file = cert_file ) ``` ```{r list-certs} list_certs(file = cert_file) ``` ### Checking for drift After any environment change, re-run `check_drift()`: ```{r drift-ok} result <- check_drift( outputs = list( coefs = coef(model), r_squared = summary(model)$r.squared, n_obs = nrow(mtcars) ), against = "baseline-v1", file = cert_file ) ``` ```{r drift-detect} # Different model — shows drift model2 <- lm(mpg ~ hp, data = mtcars) check_drift( outputs = list(coefs = coef(model2)), against = "baseline-v1", file = cert_file ) ``` ## Tier 3: Report and export ```{r report-minimal, results='hide'} repro_report(report, risks, format = "text", style = "minimal") ``` ```{r report-academic} cat(repro_report(report, risks, format = "text", style = "academic")) ``` ```{r badge} badge <- repro_badge(report, risks, output = "markdown") cat(badge) ``` ## The full pipeline ```r library(reproducr) # Tier 1 report <- audit_script("analysis.R") risks <- risk_score(report) # Tier 2 certify( outputs = list(coefs = coef(my_model)), tag = "submission-v1" ) check_drift( outputs = list(coefs = coef(my_model)), against = "submission-v1" ) # Tier 3 repro_report(report, risks, format = "html", style = "pharma") repro_badge(report, risks, output = "README") ``` ```{r cleanup, include = FALSE} unlink(script) unlink(paste0(cert_file, ".rds")) ```