SECTION 1
Historical Context & Motivation
Every data analysis rests on a foundation of assumptions — beliefs about the structure, completeness, and measurement scale of the data being processed. When those assumptions are left unstated, analyses become fragile: a column silently parsed as character instead of numeric, missing values treated as zeros rather than unknowns, or temperatures recorded in Fahrenheit mistakenly combined with Celsius readings. The practice of stating assumptions grew out of broader movements in reproducible research and defensive programming, both of which demand that analysts document — and programmatically verify — the preconditions under which their code produces correct results.
1992 • Literate Programming Gains Traction
2002 • Sweave Connects R and LaTeX
2012 • knitr and the Reproducibility Crisis
2017 • assertr and pointblank Emerge
2023 • Data Contracts in Production
The central question this lesson addresses is deceptively simple: What do you believe to be true about your data, and how do you communicate those beliefs so that your code, your collaborators, and your future self can verify them? Answering this question systematically transforms ad hoc scripting into principled software craft.
SECTION 2
Core Principles of Stating Assumptions
Stating assumptions in R involves three interlocking domains: the data types that columns must conform to, the missingness patterns you expect (or prohibit), and the units of measurement that give numeric values their meaning. Each domain has its own failure modes, and each demands its own documentation and assertion strategies. The principles below provide a framework for thinking about all three domains coherently.
Explicitness Over Convention
stopifnot(is.numeric(df$x)).Document Before You Transform
Distinguish NA Semantics
NA can mean 'not collected', 'not applicable', or 'censored.' Your assumptions must clarify which interpretation applies, because each demands different statistical treatment.Units Are Part of the Schema
units package in R can enforce this programmatically.Assertions as Executable Documentation
stopifnot(), assertthat::assert_that(), or assertr::verify() — serve as living documentation that raises errors when violated.KEY TAKEAWAY
fn compute(x: f64) -> f64 tells the compiler and the reader exactly what is expected, an R comment block paired with stopifnot() calls serves the same dual purpose: machine-checkable and human-readable.SECTION 3
Visual Explanation — The Assumption Layer
The diagram above captures a fundamental architectural principle: assumptions are not merely comments scattered through your script — they constitute a distinct logical layer that mediates between the raw data source and every subsequent operation. When you load a CSV with read_csv(), R infers column types heuristically. Those inferred types may not match your actual expectations. By inserting an explicit assumption layer — a block of assertions and documentation immediately after data ingestion — you create a checkpoint that catches schema drift, encoding changes, and upstream data corruption before they silently propagate through your analysis.
SECTION 4
How Assumptions Work in R — Mechanisms and Patterns
Data Type Assumptions
R's type system includes six atomic types — logical, integer, double, complex, character, and raw — plus composite structures like factor, Date, and POSIXct. A type assumption declares which of these you expect each column to inhabit, and optionally constrains the domain of values within that type (e.g., non-negative integers, dates after 2020-01-01). Because R will silently coerce types during operations like c(1, "two") (yielding a character vector), explicit assertions prevent entire classes of bugs.
Assertion Pattern: Type Checking
stopifnot(is.numeric(df$price), is.character(df$name), inherits(df$date, "Date")). This single line documents three type assumptions and enforces them at runtime. If any condition evaluates to FALSE, R halts execution with an informative error.Missingness Assumptions
Missing data mechanisms are typically categorized into three regimes, originally formalized by Donald Rubin: MCAR (Missing Completely At Random), MAR (Missing At Random), and MNAR (Missing Not At Random). Your assumption about which regime governs your data determines whether simple listwise deletion is valid, whether imputation is appropriate, or whether specialized models are needed. In R, you must state these assumptions explicitly because functions like na.rm = TRUE silently drop missing values without alerting you to the statistical implications.
MISSINGNESS THRESHOLD ASSERTION
Where x is a column vector, and τ is the maximum tolerable fraction of missing values (e.g., τ = 0.05 for a 5% threshold). In R: stopifnot(mean(is.na(df$x)) <= 0.05)
Unit Assumptions
The Mars Climate Orbiter was lost in 1999 because one team used pound-force·seconds while another assumed newton·seconds — a $125 million unit mismatch. In R, numeric vectors carry no inherent unit metadata. The units package allows you to attach and enforce units programmatically: library(units); x <- set_units(9.81, "m/s^2"). Even without the package, you should document units in column names (e.g., weight_kg), in a data dictionary, or in comments immediately adjacent to data loading code. A unit assumption includes both the unit itself and whether the measurement is on an interval or ratio scale, which constrains which arithmetic operations are meaningful.
SECTION 5
A Taxonomy of Common Assumptions
| Assumption Domain | What to State | R Assertion Example | Failure Mode if Unstated |
|---|---|---|---|
| Column type | Expected R class for each column | stopifnot(is.numeric(df$age)) | Silent coercion; arithmetic on character strings yields NA with warning |
| Value range | Min/max or set of valid levels | stopifnot(all(df$age >= 0 & df$age <= 150, na.rm = TRUE)) | Impossible values (e.g., age = −3) distort summary statistics |
| NA fraction | Maximum tolerable proportion of NA | stopifnot(mean(is.na(df$income)) <= 0.10) | Downstream models trained on tiny, unrepresentative subsets |
| NA mechanism | MCAR, MAR, or MNAR | Comment + Little's MCAR test | Biased estimates from inappropriate deletion/imputation |
| Units | SI/imperial/custom + scale type | units::set_units(df$dist, "km") | Unit mismatch yields off-by-orders-of-magnitude errors |
Common assumptions and their corresponding R assertion patterns
SECTION 6
Worked Example — Documenting Assumptions for a Patient Dataset
Suppose you receive a CSV file named patients.csv containing clinical trial data. The columns are patient_id, age, weight, treatment_group, and blood_pressure. Below, we walk through a complete assumption-stating workflow, from initial documentation to executable assertions.
Stating Assumptions for patients.csv
- 1
Step 1 — Write a Prose Assumption Block
Before writing any analysis code, create a comment block at the top of your script (or a dedicated section in an R Markdown file) that lists every assumption in natural language. For this dataset:# ASSUMPTIONS: # 1. patient_id: character, unique, no NAs # 2. age: integer, range [18, 100], ≤ 2% NA (MCAR) # 3. weight: numeric, in kilograms, range [30, 250], ≤ 5% NA (MAR on age/group) # 4. treatment_group: factor with levels {"control", "drug_A", "drug_B"}, no NAs # 5. blood_pressure: numeric, in mmHg (systolic), range [60, 250], ≤ 10% NA (MAR)A human-readable contract that any collaborator can review before examining the code. - 2
Step 2 — Load Data and Assert Types
Usereadr::read_csv()with explicit column type specifications rather than relying on auto-detection:library(readr) df <- read_csv("patients.csv", col_types = cols( patient_id = col_character(), age = col_integer(), weight = col_double(), treatment_group = col_factor(levels = c("control", "drug_A", "drug_B")), blood_pressure = col_double() ))By specifyingcol_typesexplicitly, any rows that fail to parse into the expected type will generate warnings rather than silently coercing.A tibble with guaranteed column types, plus parsing warnings for any violations. - 3
Step 3 — Assert Uniqueness and Completeness
Check thatpatient_idserves as a valid primary key and that mandatory columns contain no NAs:stopifnot( !anyDuplicated(df$patient_id), # unique IDs !any(is.na(df$patient_id)), # no missing IDs !any(is.na(df$treatment_group)) # no missing group assignments )Script halts immediately if any ID is duplicated or if group assignments are missing. - 4
Step 4 — Assert Missingness Thresholds
Encode the tolerable NA fractions from your prose assumptions:stopifnot( mean(is.na(df$age)) <= 0.02, mean(is.na(df$weight)) <= 0.05, mean(is.na(df$blood_pressure)) <= 0.10 )If a new data delivery has 15% missing blood pressure readings, this assertion fires and forces you to investigate before proceeding.Missingness thresholds verified; excessive missing data triggers an immediate halt. - 5
Step 5 — Assert Value Ranges and Document Units
Combine range checks with unit documentation. Even though R cannot natively enforce units, your assertions on plausible ranges serve as a proxy:stopifnot( all(df$age >= 18 & df$age <= 100, na.rm = TRUE), all(df$weight >= 30 & df$weight <= 250, na.rm = TRUE), # kg all(df$blood_pressure >= 60 & df$blood_pressure <= 250, na.rm = TRUE) # mmHg, systolic ) # Optionally, use the units package: library(units) df$weight <- set_units(df$weight, "kg") df$blood_pressure <- set_units(df$blood_pressure, "mmHg")All value ranges verified; columns annotated with physical units for downstream safety.
SECTION 7
Assertion Tools in R — Strengths and Limitations
R offers several mechanisms for encoding assumptions as executable checks. The choice between them depends on the complexity of your validation logic, whether you need detailed error reports or fail-fast behavior, and how well the tool integrates with tidyverse-style pipelines. The table below compares the most widely used approaches, from base R's minimal stopifnot() to full-featured validation frameworks.
| Tool | Strengths | Limitations | Best For |
|---|---|---|---|
stopifnot() | Zero dependencies; concise; built into base R | Error messages are cryptic; stops at first failure; no reporting | Quick scripts, internal assertions in functions |
assertthat | Human-readable error messages; custom assertion functions | Still fails at first violation; package not actively maintained | Package development, function preconditions |
assertr | Pipe-friendly; accumulates all violations; rich predicates | Learning curve for custom predicates; limited type checking | Tidyverse data pipelines with multi-check validation |
pointblank | HTML validation reports; threshold-based pass/fail; database support | Heavier dependency; overkill for small scripts | Production pipelines, team-based data monitoring |
| Comments + data dictionary | No code overhead; accessible to non-programmers | Not executable; drifts out of sync with code; no enforcement | Supplementary documentation alongside executable assertions |
Comparison of R assertion tools for encoding data assumptions
KEY TAKEAWAY
stopifnot() for fast, critical checks that should halt execution, assertr or pointblank for comprehensive validation reports, and a human-readable data dictionary for cross-team communication. Think of it like testing in software engineering: unit tests, integration tests, and documentation each serve different purposes but together form a comprehensive quality system.SECTION 8
Connecting to Advanced Practices — Data Contracts and Schema Evolution
Stating assumptions at the script level is the first step in a continuum that extends to enterprise-scale data contracts — formal agreements between data producers (e.g., a database team) and data consumers (e.g., an analytics team) that specify schemas, freshness guarantees, and quality SLAs. In the R ecosystem, packages like dm model relational data with foreign key constraints, while integrations with tools like dbt and Great Expectations extend assumption-stating into the data engineering pipeline. The table below maps the practices introduced in this lesson to their advanced counterparts.
| This Lesson's Practice | Advanced / Production Practice |
|---|---|
| Comment block listing assumptions | Data contract YAML/JSON schema shared between teams |
stopifnot() assertions | Great Expectations / pointblank validation suites run on CI/CD |
| Column naming conventions for units | Semantic type systems (e.g., units package, Apache Arrow schemas with metadata) |
| Manual NA threshold checks | Automated data quality dashboards with alerting (e.g., Monte Carlo, Bigeye) |
| Single-file R script | R package with testthat test suite and vignettes documenting data expectations |
From script-level assumptions to production-grade data contracts
As your projects scale from exploratory notebooks to production pipelines, the discipline of stating assumptions grows in value. The habits formed here — documenting types, quantifying tolerable missingness, and annotating units — transfer directly to schema evolution workflows where upstream changes must be validated before propagating to downstream consumers. In a microservices architecture, each service's data interface is essentially a set of stated assumptions; learning to articulate them in R prepares you for this broader software engineering paradigm.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
na.rm = TRUE in mean(df$income, na.rm = TRUE) is an implicit assumption rather than just a convenience parameter. What assumption does it encode, and under what missingness mechanism would it produce biased results?Show answer
na.rm = TRUE implicitly assumes that the missing values are Missing Completely At Random (MCAR) — that is, the probability of a value being missing is unrelated to either the observed or unobserved data. Under this assumption, the complete cases form a representative subsample and the mean is unbiased. However, if the data is MNAR (Missing Not At Random) — for example, if high-income individuals disproportionately decline to report their income — then dropping NAs yields a systematically downward-biased mean. The parameter silently performs listwise deletion without alerting the analyst to the statistical consequences, which is precisely why the assumption should be stated explicitly.Question 2: BASIC
stopifnot() block that asserts the following assumptions for a data frame df: (a) the column temperature is numeric, (b) city is a character vector, (c) temperature has no more than 3% NAs, and (d) all non-NA temperature values are between −50 and 60 (Celsius).Show answer
stopifnot(
is.numeric(df$temperature),
is.character(df$city),
mean(is.na(df$temperature)) <= 0.03,
all(df$temperature >= -50 & df$temperature <= 60, na.rm = TRUE)
)
This assertion block encodes all four assumptions as executable checks. The is.numeric() check catches cases where the column was read as character (e.g., due to a stray comma in the CSV). The missingness threshold of 0.03 halts execution if more than 3% of temperatures are missing. The range check uses na.rm = TRUE to evaluate only non-NA values, and would catch data recorded in Fahrenheit (since 140°F = 60°C would be at the boundary).Question 3: INTERMEDIATE
duration contains values like "2h 30m", "45m", and "1h". Write a complete assumption documentation block (prose comments) and corresponding assertions that (a) note the raw type is character, (b) state the target type after parsing is numeric (minutes), (c) validate the parsed values fall within a sensible range, and (d) specify the unit.Show answer
# ASSUMPTIONS for 'duration' column:
# - Raw format: character, pattern "Xh Ym" or "Xh" or "Ym"
# - Target: numeric, converted to total minutes
# - Unit: minutes (min)
# - Range: [1, 480] (no session shorter than 1 min or longer than 8 hours)
# - Missingness: ≤ 1% NA; assumed MCAR
stopifnot(is.character(df$duration))
parse_duration <- function(x) {
hours <- as.numeric(stringr::str_extract(x, "\\d+(?=h)"))
mins <- as.numeric(stringr::str_extract(x, "\\d+(?=m)"))
hours[is.na(hours)] <- 0
mins[is.na(mins)] <- 0
hours * 60 + mins
}
df$duration_min <- parse_duration(df$duration)
stopifnot(
is.numeric(df$duration_min),
mean(is.na(df$duration_min)) <= 0.01,
all(df$duration_min >= 1 & df$duration_min <= 480, na.rm = TRUE)
)
This solution demonstrates the full pattern: state the raw type, document the transformation, specify the target unit, set range bounds, and encode everything as assertions. The strategy of parsing and then validating catches both format violations and semantically implausible values.Question 4: APPLIED
sales_us.csv (prices in USD, weights in pounds) and sales_eu.csv (prices in EUR, weights in kilograms). Write the assumption documentation and assertion code that ensures unit consistency before merging. Assume you will convert everything to USD and kilograms.Show answer
# ASSUMPTIONS — sales_us.csv:
# - price_usd: numeric, unit = USD, range [0.01, 100000]
# - weight_lb: numeric, unit = pounds (lb), range [0.01, 5000]
# - No NAs in price or weight
# ASSUMPTIONS — sales_eu.csv:
# - price_eur: numeric, unit = EUR, range [0.01, 100000]
# - weight_kg: numeric, unit = kilograms (kg), range [0.01, 2500]
# - No NAs in price or weight
# CONVERSION ASSUMPTIONS:
# - EUR to USD rate: 1.08 (as of 2024-01-15; source: ECB)
# - Pounds to kg: 1 lb = 0.453592 kg (exact)
stopifnot(
is.numeric(us$price_usd), is.numeric(us$weight_lb),
!any(is.na(us$price_usd)), !any(is.na(us$weight_lb)),
all(us$price_usd > 0), all(us$weight_lb > 0)
)
stopifnot(
is.numeric(eu$price_eur), is.numeric(eu$weight_kg),
!any(is.na(eu$price_eur)), !any(is.na(eu$weight_kg)),
all(eu$price_eur > 0), all(eu$weight_kg > 0)
)
eur_to_usd <- 1.08 # ASSUMPTION: exchange rate
lb_to_kg <- 0.453592
us$price_usd <- us$price_usd # already in USD
us$weight_kg <- us$weight_lb * lb_to_kg
eu$price_usd <- eu$price_eur * eur_to_usd
merged <- bind_rows(
us |> select(price_usd, weight_kg),
eu |> select(price_usd, weight_kg)
)
This solution highlights a critical practice: stating the conversion factors as named constants with explicit documentation of their source and date. Exchange rates are time-dependent, so the assumption must be versioned. The range checks serve as implicit unit verification — if US weights were accidentally recorded in kilograms, values above 2500 would trigger the US-side assertion for lb values above 5000.Question 5: CRITICAL THINKING
Show answer
mean(df$age) call produces NA with a warning that is easily overlooked in a long pipeline, and if wrapped in suppressWarnings(), it is invisible. Subsequent joins or filters on 'age' silently produce empty results or incorrect matches. Scenario 2: Unit mismatch with plausible magnitude. Two lab sites measure enzyme activity: Site A reports in μmol/min/mg and Site B in nmol/min/mg (a 1000× difference). If the pooled analysis computes a mean of, say, 523 — this looks perfectly reasonable as a measurement value in either unit. The resulting analysis would show a significant difference between sites that is entirely an artifact of unit mismatch, and the p-value would look 'correct.' Without a stated unit assumption and a range assertion calibrated to the expected unit, this bug produces a publishable but false result. These scenarios demonstrate that the most dangerous data bugs are the ones that produce reasonable-looking output — exactly the cases where explicit assumptions provide the most value.SUMMARY
Summary — Stating Assumptions in R
Summary
Every R analysis rests on assumptions about data types (which R class each column should inhabit), missingness (how much missing data is tolerable and what mechanism generated it — MCAR, MAR, or MNAR), and units of measurement (what physical or domain-specific unit gives numeric values their meaning). Making these assumptions explicit — through prose documentation and executable assertions — prevents silent failures, supports reproducibility, and transforms fragile scripts into robust, communicative analyses.
The core workflow is to document assumptions in prose immediately after data loading, then encode them as assertions using tools ranging from base R's stopifnot() to full validation frameworks like assertr and pointblank. This discipline scales from single-file scripts to production data contracts and prepares you for the broader software engineering practice of design by contract.