R PROGRAMMING • SOFTWARE CRAFT AND COMMUNICATION

Stating Assumptions — State assumptions about data types, missingness, and units

Making implicit expectations about your data explicit ensures reproducible, trustworthy analyses in R.

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
Donald Knuth's literate programming paradigm popularizes the idea that code should explain its reasoning, including the assumptions it depends on. This plants seeds for documentation-first analysis culture.
2002
Sweave Connects R and LaTeX
Friedrich Leisch's Sweave system lets R users embed code in documents, making it practical to state assumptions in prose right next to the assertions that check them.
2012
knitr and the Reproducibility Crisis
Yihui Xie's knitr package and high-profile replication failures across the social sciences push the R community to treat assumption documentation as a first-class engineering concern, not an afterthought.
2017
assertr and pointblank Emerge
R packages like assertr and pointblank formalize data validation pipelines, enabling analysts to encode assumptions about types, missingness, and value ranges as executable tests that halt pipelines on violation.
2023
Data Contracts in Production
The concept of data contracts — schema-level agreements between data producers and consumers — enters the R ecosystem through tools like dm and dbt integrations, elevating assumption-stating to a cross-team practice.

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.

1

Explicitness Over Convention

Never rely on R's automatic type coercion or default behavior. If you expect a column to be numeric, say so in prose and assert it in code with stopifnot(is.numeric(df$x)).
2

Document Before You Transform

State your assumptions about the raw data before any cleaning or transformation. This creates a contract: if the raw data violates these assumptions, the pipeline should fail loudly rather than produce subtly wrong results.
3

Distinguish NA Semantics

R's NA can mean 'not collected', 'not applicable', or 'censored.' Your assumptions must clarify which interpretation applies, because each demands different statistical treatment.
4

Units Are Part of the Schema

A bare numeric vector is ambiguous. State whether values are in meters, kilograms, milliseconds, or percentages. The units package in R can enforce this programmatically.
5

Assertions as Executable Documentation

Comments drift out of sync with code. Executable assertions — via stopifnot(), assertthat::assert_that(), or assertr::verify() — serve as living documentation that raises errors when violated.
KEY TAKEAWAY
Think of stating assumptions like writing a type signature in a statically typed language such as Haskell or Rust. In those languages, the compiler enforces your contract; in R, you must enforce it yourself through explicit assertions and documentation. Just as a Rust function signature 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 illustrates how the Assumption Layer (dashed violet box) sits between raw data ingestion and all downstream processing. It feeds into assertions (code-level checks), cleaning logic, and documentation artifacts. Every pipeline stage references these assumptions.

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.

💡 Assertion Pattern: Type Checking
A minimal assertion block after loading data might look like: 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
fraction_missing(x) = count(NA in x) / length(x) ≤ τ
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.

A Taxonomy of Common Assumptions

This taxonomy diagram organizes the three assumption domains — data types, missingness, and units — each subdivided into mechanism/category, tolerance/scale type, and handling/encoding strategy.
Common assumptions and their corresponding R assertion patterns
Assumption DomainWhat to StateR Assertion ExampleFailure Mode if Unstated
Column typeExpected R class for each columnstopifnot(is.numeric(df$age))Silent coercion; arithmetic on character strings yields NA with warning
Value rangeMin/max or set of valid levelsstopifnot(all(df$age >= 0 & df$age <= 150, na.rm = TRUE))Impossible values (e.g., age = −3) distort summary statistics
NA fractionMaximum tolerable proportion of NAstopifnot(mean(is.na(df$income)) <= 0.10)Downstream models trained on tiny, unrepresentative subsets
NA mechanismMCAR, MAR, or MNARComment + Little's MCAR testBiased estimates from inappropriate deletion/imputation
UnitsSI/imperial/custom + scale typeunits::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.

Stating Assumptions for patients.csv
1
Step 1 — Write a Prose Assumption BlockBefore 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 TypesUse 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.
A tibble with guaranteed column types, plus parsing warnings for any violations.
3
Step 3 — Assert Uniqueness and CompletenessCheck that 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 )
Script halts immediately if any ID is duplicated or if group assignments are missing.
4
Step 4 — Assert Missingness ThresholdsEncode 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 UnitsCombine 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.

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.

Comparison of R assertion tools for encoding data assumptions
ToolStrengthsLimitationsBest For
stopifnot()Zero dependencies; concise; built into base RError messages are cryptic; stops at first failure; no reportingQuick scripts, internal assertions in functions
assertthatHuman-readable error messages; custom assertion functionsStill fails at first violation; package not actively maintainedPackage development, function preconditions
assertrPipe-friendly; accumulates all violations; rich predicatesLearning curve for custom predicates; limited type checkingTidyverse data pipelines with multi-check validation
pointblankHTML validation reports; threshold-based pass/fail; database supportHeavier dependency; overkill for small scriptsProduction pipelines, team-based data monitoring
Comments + data dictionaryNo code overhead; accessible to non-programmersNot executable; drifts out of sync with code; no enforcementSupplementary documentation alongside executable assertions
KEY TAKEAWAY
No single tool covers all needs. The most robust strategy is a layered approach: use 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.

From script-level assumptions to production-grade data contracts
This Lesson's PracticeAdvanced / Production Practice
Comment block listing assumptionsData contract YAML/JSON schema shared between teams
stopifnot() assertionsGreat Expectations / pointblank validation suites run on CI/CD
Column naming conventions for unitsSemantic type systems (e.g., units package, Apache Arrow schemas with metadata)
Manual NA threshold checksAutomated data quality dashboards with alerting (e.g., Monte Carlo, Bigeye)
Single-file R scriptR 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

PROBLEM 1CONCEPTUAL
Explain why using 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?
PROBLEM 2BASIC
Write a 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).
PROBLEM 3INTERMEDIATE
You receive a dataset where the column 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.
PROBLEM 4APPLIED
You are building an R pipeline that merges two datasets: 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.
PROBLEM 5CRITICAL THINKING
A colleague argues that stating assumptions is unnecessary overhead: 'If the data is wrong, my analysis will produce obviously wrong results, and I will catch it then.' Construct a detailed counterargument with at least two concrete scenarios where unstated assumptions lead to plausible but incorrect results that would not be caught by casual inspection of the output.

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.

Varsity Tutors • R Programming • Stating Assumptions — State assumptions about data types, missingness, and units