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.
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.
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.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.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.
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.
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.
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.
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 |
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.
# 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)readr::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 specifying col_types explicitly, any rows that fail to parse into the expected type will generate warnings rather than silently coercing.patient_id serves 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
)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.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")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 |
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.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 |
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.
Practice Problems
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?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).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.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.Summary — Stating Assumptions in R
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.