HSCI 410 · Lesson 1

A Structured Approach to Data Analysis

Exploratory Data Analysis For Epidemiology

Learning objectives for this lesson:

  • Construct a causal diagram before beginning data analysis
  • Establish a system for managing data-collection sheets, files, and variables
  • Apply best practices for data coding, entry, and verification
  • Process outcome and predictor variables appropriately for analysis
  • Evaluate unconditional associations between variables
  • Set up a systematic approach for keeping track of analyses

This course was developed by Dr. Kiffer G. Card, Faculty of Health Sciences, Simon Fraser University based on Dohoo, I. R., Martin, S. W., & Stryhn, H. (2012). Methods in Epidemiologic Research. VER Inc.

Reference

Glossary: Key Terms, People & Concepts

📚 Reference page, available throughout the lesson

This glossary collects the key concepts, people, and ideas you will meet in this lesson. Use it as a reference while you work through the material, or as a review before assessments. Type in the search box to filter entries.

Key Concepts & Ideas
Research Question A focused, answerable statement that frames an analysis. Usually structured around four elements: the population studied, the exposure (or determinant, or intervention), the comparator group, and the outcome. The initials of these elements give the acronym PECO (population, exposure, comparator, outcome), or PICO when the exposure is an intervention. A clear research question drives the entire analytic plan.
Analytic Plan A pre-specified roadmap that links the research question to data collection, variables, statistical models, and decision rules. Reduces ad hoc decisions and protects against fishing expeditions.
Hypothesis A testable prediction about the relationship between variables. The null hypothesis (H₀) typically states no association; the alternative (H₁) states an association exists.
Exposure The factor whose effect on the outcome is of primary interest (e.g., a treatment, behaviour, or environmental agent). Sometimes called the predictor or independent variable.
Outcome The health state or event being predicted or explained (e.g., disease, recovery, death). Also called the dependent variable or response.
Covariate Any variable other than the primary exposure that may influence the outcome. May be a confounder, mediator, effect modifier, or simply a precision variable.
Confounder A variable that affects both the exposure and the outcome and is not on the causal pathway between them. Failing to adjust for confounders biases effect estimates.
DAG (Directed Acyclic Graph) A diagram of variables (nodes) and directed causal arrows (edges) with no cycles. Used to encode assumed causal structure and identify which variables to adjust for.
Data Dictionary / Codebook A document listing every variable in a dataset with its name, definition, type, allowable values, units, and coding rules. Essential for reproducibility and collaboration.
Reproducibility The ability of others (or future-you) to re-run an analysis on the same data and obtain the same results, given the code and documentation.
Data Verification Checking entered data against source records to confirm accuracy. Includes double-entry, range checks, consistency checks, and cross-tabulation against expected patterns.
Multilevel Data Observations nested within higher-level units (e.g., patients within clinics, students within schools). Requires methods that account for clustering and within-unit correlation.
Methods & Statistical Concepts
MCAR (Missing Completely at Random) Missingness is unrelated to any observed or unobserved variable. Complete-case analysis is unbiased but loses efficiency.
MAR (Missing at Random) Missingness depends only on observed variables. Multiple imputation and likelihood-based methods can give unbiased estimates.
MNAR (Missing Not at Random) Missingness depends on the unobserved value itself. Requires sensitivity analyses or explicit modeling assumptions; cannot be fixed by standard imputation.
Unconditional Association The crude (unadjusted) relationship between two variables, ignoring other covariates. Useful as a first look at the data before multivariable modeling; the choice of confounders to adjust for comes from the causal diagram.
Program File / Script A saved file containing analysis code (e.g., a .R script) that can be re-executed to reproduce results. Preferred over interactive (point-and-click) processing.
Data Coding Translating raw responses (e.g., “Yes”, “No”, “Don't know”) into numeric or categorical values suitable for analysis, following pre-specified rules.
Mediator (Intervening Variable) A variable on the causal pathway from the exposure to the outcome, so that part of the exposure’s effect passes through it. Adjusting for a mediator removes that part of the effect from the estimate.
Adjusting For (Conditioning On) Comparing people who share the same value of a third variable, so that the variable cannot explain the difference being studied. It is done by adding the variable to a regression model, by stratifying on it, or by restricting the sample to one of its values.
Replication Repeating a study with new participants and new data to see whether the original finding holds. Reproducibility, by comparison, means obtaining the same results from the same data and code.
Regression Coefficient A number estimated by a regression model that gives the average change in the outcome for a one-unit increase in a predictor, with the other predictors in the model held fixed.
Residual The difference between a person’s observed outcome and the value the model predicts for that person (observed minus predicted).
Variance A measure of spread equal to the average squared distance of the values from their mean (R divides by n − 1). Its square root is the standard deviation.
Standard Error The estimated amount by which a statistic, such as a mean or a regression coefficient, would vary from one sample to the next. A smaller standard error indicates a more precise estimate.
Confidence Interval (CI) A range of values, calculated from the data, that shows how precisely a quantity has been estimated. A 95% confidence interval comes from a method that captures the true value in 95% of repeated samples.
p-Value The probability of a result at least as far from “no effect” as the one observed, if there were truly no effect. A small p-value indicates that the data are hard to reconcile with no effect; the size and importance of the effect are judged from the estimate and its confidence interval.
Correlation (r) A number between −1 and +1 that describes how closely two continuous variables follow a straight-line pattern. Values near 0 indicate no linear relationship, and values near −1 or +1 indicate a tight one.
Collinearity A situation in which two or more predictors are so highly correlated that a model cannot separate their individual effects.
Censoring In time-to-event data, the situation in which follow-up ends before the event is observed for a person, so the event time is known only to be later than the last contact.
Complete-Case Analysis An analysis that uses only the records with no missing values on the variables involved. It is simple, and it can bias results when the people with missing values differ from the rest.
NA (R’s Missing-Value Code) The code R uses for a missing value (“not available”). Most R functions return NA when asked to summarise data that contain missing values unless told to remove them, for example with na.rm = TRUE.
Factor (in R) R’s variable type for categorical data. A factor stores each value as one of a fixed set of levels, each with a text label, and the first level serves as the reference category in regression models.
Pipe Operator The R symbol |> (or %>% in the tidyverse), which passes the result of one step to the next, so that a sequence of data operations reads from top to bottom.
No matching entries. Try a different search term.
Section 1

Introduction & Data Collection

⏱ Estimated time: 15 minutes
Lesson 1 · HSCI 410

A Structured Approach to Data Analysis

From reading evidence and designing studies to analysing the data those studies produce.

Why it matters now

Reproducibility as a professional expectation

An analysis is only as trustworthy as the steps that came before it.Lesson 1 throughline

Concerns about replication, raised by Ioannidis (2005) and confirmed by large replication projects in the 2010s, led to a manifesto for reproducible science (Munafò et al., 2017) and raised the bar. Structured, scripted, documented workflows are now the expected standard in published public-health research.

Section 1 of 4

Introduction & Data Collection

A structured, iterative workflow and the causal diagram as your first analytic move.

The core principle

Analysis is iterative, not linear

Jumping straight to the sophisticated model almost always produces wrong results.

Tukey distinguished exploratory work (getting to know the data) from confirmatory work (formally testing a stated claim). A structured template makes every iteration cheaper and more defensible.

Causal diagram Clean & verify Unconditional checks Multivariable model iterate
The essential first step

Three DAG structures

Fork

C X Y

C is a confounder. Adjust for it.

Chain

X M Y

M is a mediator. Leave it out of the model when estimating the total effect.

Collider

X Y Z

Do not adjust for Z. Adjusting for a collider creates a false association between X and Y.

DAG to regression

Mediation: education → income → health

Baron & Kenny decomposition

indirect effect (a × b) + direct effect (c′) = total effect (c)

True values chosen for the simulation: 0.30 + 0.30 = 0.60. Estimates from the simulated data: 0.2969 + 0.3506 = 0.6475.

a effect of exposure on mediator b effect of mediator on outcome c' direct effect (ADE, average direct effect) c total effect

About half of the total effect of education on health flows through income (proportion mediated: 0.50 by design of the simulation, 0.46 estimated). The indirect effect a × b is reported as the ACME (average causal mediation effect).

Key caution: the indirect estimate is only as credible as the DAG. An unmeasured confounder on the income→health path biases the result even when the regressions look clean.

Project setup

A reproducible project skeleton

.
+-- R/                  numbered scripts: 01_load.R, 02_clean.R
+-- data/raw/          read-only; never overwritten
+-- data/processed/    regenerated entirely from R/
+-- output/figures/     numbered figures for the paper
+-- project.Rproj       one click reopens the environment

Pipelines read top to bottom with the pipe operator; here() keeps file paths portable across machines.

Data collection

Managing data-collection sheets

Protect originals

Never remove originals from the file. Ship photocopies only. The original is the irreplaceable record.

Track progress

Record insertion of each form so you know how many remain to be collected before further work begins.

Scan for completeness

Once all forms are in, check every sheet for omissions before any other step. Act on gaps promptly.

Carry forward

What to take into the next section

  • Draw the DAG first. It fixes what you are estimating and what goes in the model.
  • Expect iteration. Backing up several steps is normal and necessary.
  • Protect originals and scan for completeness before any analysis begins.

Introduction and Overview

Earlier courses in this series covered how to read epidemiological evidence (230) and how to design and conduct epidemiologic studies (341). This course addresses the next link in the chain: how to analyse the data those studies produce. It uses R to apply the main statistical methods of modern public-health analysis, including linear, logistic, multinomial, ordinal, and count regression; mixed models for clustered and longitudinal data; and causal-inference tools for observational data. Each of these models is introduced in a later lesson, and none of them is needed to follow this lesson. Time-to-event (survival) models are beyond the scope of this course; HSCI 341 Lesson 8, Time-to-Event Data, introduces them. This first lesson sets the foundation: a disciplined workflow for taking raw data from collection through to analysis-ready files. The structured approach has become especially important since the wider scientific community recognised a replication crisis in published research (Ioannidis, 2005; Open Science Collaboration, 2015) and called for a manifesto of reproducible practice (Munafò et al., 2017). Across four content sections we walk through this in order: introduction and data collection (this section), data coding, entry, and file management (a later section), program files, editing, and verification (a later section), and data processing plus the first unconditional associations (a later section).

Two terms: replication and reproducibility

Replication means repeating a study with new participants and new data to see whether the original finding holds. Reproducibility means re-running the original analysis on the original data, with the original code, and obtaining the same numbers. The crisis was named for failed replications, which Ioannidis warned about in 2005 and which large projects documented in the 2010s. The habits taught in this lesson target reproducibility, because a result that cannot be reproduced from its own data and code cannot be checked, corrected, or built upon.

Learning Objectives

  • Explain why a structured, iterative analytic workflow outperforms diving straight into modelling.
  • Sketch a causal diagram that distinguishes outcomes, predictors, confounders, and intervening variables.
  • Set up a storage and tracking system for original data-collection sheets.
  • Recognise where this section sits in the larger pipeline that runs through later sections.

Why a Structured Approach?

When starting the analysis of a complex dataset, it is very helpful to have a structured approach in mind. For most people, there is a strong tendency to jump straight into the sophisticated analysis that will provide the ultimate answer. This rarely works out, because the results will be wrong when important preliminary steps were skipped.

Key Principle

Data analysis is an iterative process which often requires that you back up several steps as you gain more insight into your data, an idea Tukey developed when he distinguished exploratory from confirmatory work (Peng, 2011). A structured template, while not the only approach, will be applicable in most situations and will serve to guide your initial efforts; tidy-data conventions (Wickham, 2014) make each iteration cheaper.

Two terms in this box recur throughout the course. Exploratory analysis means getting to know the data by summarising and plotting it and looking for patterns, errors, and surprises; confirmatory analysis means formally testing a claim that was stated before the data were examined. Tidy data means a table in which each variable has its own column, each observation (for example, each participant) has its own row, and each cell holds a single value.

Start with a Causal Diagram

Before you start any work with your data, it is essential to construct a plausible causal diagram of the problem you are about to investigate. This will help identify:

  • Which variables are important outcomes and predictors
  • Which are potential confounders
  • Which might be intervening variables between your main predictors and outcomes

Practical Tip

Keep this causal diagram in mind throughout the entire data-analysis process. With large datasets, it will not be possible to include all predictors as separate entities. This can be handled by including blocks of variables (e.g., demographic characteristics) in the diagram instead of listing each variable.

Quick refresher: DAGs from an earlier course

When we say “causal diagram” in 410, we mean a directed acyclic graph (DAG): nodes for variables, directed arrows for direct causal effects, no cycles. You met these in an earlier course, and the three structural pieces still do all the work:

  • Fork (X ← C → Y): C is a confounder. Adjust for it.
  • Chain (X → M → Y): M is a mediator. Do not adjust if you want the total effect.
  • Collider (X → Z ← Y): Do not adjust, and watch for it in selection.

The DAG fixes your estimand (what causal quantity you are estimating) and your adjustment set (what goes on the right-hand side of the regression) before you fit anything. In 410 we use it as the bridge from a research question to a regression model.

What “adjusting for” a variable means in practice

To adjust for a variable (also called controlling for it or conditioning on it) means to compare exposed and unexposed people who have the same value of that variable, so that the variable cannot explain the difference between them. In practice an analyst adjusts in one of three ways. The most common is to add the variable to the right-hand side of a regression model, for example lm(outcome ~ exposure + age), which estimates the exposure effect among people of the same age. A second is to stratify, which means analysing each level of the variable separately (for example, women and men) and then combining the results. A third is to restrict the sample to one level of the variable, for example by studying only non-smokers or only hospital patients.

Restriction is the form that is easiest to overlook. A study that recruits only hospital patients has conditioned on hospital admission before any model is fitted, and if admission is a collider, that sampling decision alone can create a false association. The collider example later in this section shows the size of the distortion with simulated numbers.

Worked Example: From a research question to a DAG and an adjustment set

The smoking cohort used later in this section (cohort.csv) records whether each participant smokes and whether an event occurred during follow-up. Suppose the event is a cardiovascular (CVD) event, such as a heart attack or stroke. The six steps below turn that study into a causal diagram.

StepWhat the analyst doesResult for this example
1. State the questionThe question names the population, exposure, comparator, and outcome (PECO).Among adults aged 20 to 79, does smoking, compared with not smoking, increase the risk of a CVD event during follow-up?
2. Add common causesThe analyst lists variables that plausibly cause both the exposure and the outcome, using subject-matter knowledge and earlier studies.Age and sex both influence whether a person smokes and their CVD risk, so each gets an arrow into smoking and an arrow into the CVD event.
3. Add pathway variablesThe analyst lists variables through which the exposure might act on the outcome.Smoking raises blood pressure, and high blood pressure raises CVD risk, so blood pressure sits on a chain from smoking to the event.
4. Add common effectsThe analyst lists variables that both the exposure and the outcome cause, including any variable that determined who entered the data.Smoking-related illness and CVD events both lead to hospital admission, so admission receives arrows from both and is a collider.
5. Classify each variableEach variable is labelled using the three structures: fork, chain, or collider.Age and sex are confounders, blood pressure is a mediator, and hospital admission is a collider.
6. Read off the adjustment setThe analyst chooses the estimand and lists the variables the model must include and exclude.For the total effect of smoking, the model adjusts for age and sex, leaves blood pressure out, and does not restrict the sample to hospital patients.
Causal diagram for the smoking and cardiovascular event example Age (confounder) Sex (confounder) Smoking (exposure) CVD event (outcome) Blood pressure (mediator) Hospital admission (collider)
Causal diagram for the worked example. Age and sex are confounders, blood pressure is a mediator, and hospital admission is a collider. For the total effect of smoking on cardiovascular (CVD) events, the adjustment set is age and sex.
Learn to do this in R

Narrated R walkthrough: Getting Started with R and RStudio

If you have not used R before, start with this walkthrough. It installs R and RStudio, shows how the RStudio window is laid out, introduces objects, functions and packages, loads the course survey data, and shows how to read a help page, with every line of code explained as it runs.

Open the Getting Started with R and RStudio walkthrough
R Getting started: R, RStudio, and the packages for this lesson

The R boxes in this course assume a working installation of two free programs. R is the statistics language that does the calculations, and RStudio is the program in which R code is written and run. Both are available from the Posit download page; R is installed first and RStudio second.

  1. Create an RStudio project for the course by choosing File, then New Project, then New Directory, then New Project, and naming the folder (for example, hsci410). A project is a folder that RStudio remembers; opening its .Rproj file later reopens the same folder with the same settings, and file paths in the code are read relative to it.
  2. Install the packages used in this lesson by typing the line below into the Console pane and pressing Enter. A package is a free add-on that supplies extra functions. Installation needs an internet connection, takes a few minutes, and is done once per computer.
  3. Load the packages at the top of each script with library(). Loading has to be repeated in every new R session.
  4. Open a new script with File, New File, R Script, and save it inside the project. Code typed into a script is saved with the project; code typed only into the Console is lost when RStudio closes.
# Run once per computer, in the Console
install.packages(c("tidyverse", "here", "mediation", "dagitty"))

# Run at the top of every script that uses them
library(tidyverse)   # data handling, graphics, reading and writing files
library(here)        # builds file paths from the project folder

Two ways of running code appear in every lesson. Running a line sends only the current line, or the highlighted lines, to the Console; in RStudio this is the Run button or Ctrl+Enter (Cmd+Return on a Mac). Sourcing a script runs the whole file from top to bottom; this is the Source button or Ctrl+Shift+S (Cmd+Shift+S on a Mac). Running line by line suits exploration, and sourcing confirms that the complete script works from a fresh start.

Any text after a # symbol on a line is a comment. R ignores comments, so they are used to explain what each step does and why. The code boxes in this course use comments in this way, and the answer-key scripts follow the same practice.

About tidyverse. The line library(tidyverse) loads several packages at once, including dplyr (data manipulation), readr (reading and writing files), tidyr (tidying data) and ggplot2 (graphics). If the tidyverse installation fails, the four packages can be installed separately with install.packages(c("dplyr", "readr", "tidyr", "ggplot2")) and loaded with four library() lines in place of library(tidyverse). Together they supply every tidyverse function used in this lesson.

R Finding the adjustment set with dagitty

The dagitty package reads a DAG written as text and reports which variables must be adjusted for to estimate a chosen effect. The same tool is available as a free web page, dagitty.net, where the diagram is drawn with the mouse; the R version keeps the diagram in the script beside the analysis. The code below encodes the diagram from the worked example, with cvd standing for the CVD event and bp for blood pressure.

library(dagitty)

# Each "A -> B" is one arrow: A causes B
g <- dagitty("dag {
  age -> smoker ; age -> cvd
  sex -> smoker ; sex -> cvd
  smoker -> bp ; bp -> cvd
  smoker -> cvd
  smoker -> hospital ; cvd -> hospital
}")

# Variables to adjust for to estimate the TOTAL effect of smoking on cvd
adjustmentSets(g, exposure = "smoker", outcome = "cvd", effect = "total")

# Variables to adjust for to estimate the DIRECT effect (not through bp)
adjustmentSets(g, exposure = "smoker", outcome = "cvd", effect = "direct")
Console output
{ age, sex } { age, bp, sex }

Reading the output. The first line answers the first call (the total effect), and the second line answers the second call (the direct effect). Each is a sufficient adjustment set: if the DAG is correct, including these variables in the model removes confounding of that effect. For the total effect of smoking, the set is age and sex. Blood pressure is left out because it is a mediator, and hospital admission is left out because it is a collider. For the direct effect, blood pressure joins the set, because the direct effect is defined as the effect that remains when the mediator is held fixed. The answer is only as good as the arrows typed in: dagitty checks the logic of the diagram, and only subject-matter knowledge can check whether the diagram matches reality.

R Worked Example: a collider created by studying only hospital patients

The simulation below creates a population of 10,000 people in which smoking and diabetes are unrelated by construction. Each condition raises the chance of a hospital admission, which makes admission a collider (smoking → admission ← diabetes). The code compares the percentage with diabetes among smokers and non-smokers, first in the whole population and then among hospital patients only. The function rbinom(n, 1, p) draws n values of 0 or 1 with probability p of a 1; table() counts each combination of values; prop.table(..., margin = 1) turns the counts into proportions within each row; and subset() keeps the rows that meet a condition.

set.seed(2026)
n        <- 10000
smoker   <- rbinom(n, 1, 0.30)   # 1 = smokes (30% of people)
diabetes <- rbinom(n, 1, 0.10)   # 1 = diabetes (10%), unrelated to smoking
# Each condition raises the chance of a hospital admission (the collider)
p_admit  <- 0.05 + 0.30 * smoker + 0.30 * diabetes
hospital <- rbinom(n, 1, p_admit)
pop      <- data.frame(smoker, diabetes, hospital)

# Whole population: % with diabetes among non-smokers (0) and smokers (1)
round(100 * prop.table(table(smoker = pop$smoker, diabetes = pop$diabetes),
                       margin = 1), 1)
Console output
diabetes smoker 0 1 0 90.0 10.0 1 90.2 9.8
# Hospital patients only: this restriction conditions on the collider
inpatients <- subset(pop, hospital == 1)
nrow(inpatients)
round(100 * prop.table(table(smoker = inpatients$smoker,
                             diabetes = inpatients$diabetes), margin = 1), 1)
Console output
[1] 1688 diabetes smoker 0 1 0 56 44 1 85 15

Reading the output. Rows are smoking status (0 = does not smoke, 1 = smokes) and columns are diabetes status, so the right-hand column is the percentage with diabetes. In the whole population, about 10% of both groups have diabetes (10.0% and 9.8%), which matches the way the data were built. Among the 1,688 hospital patients, 44% of non-smokers and only 15% of smokers have diabetes, so smoking now appears to protect against diabetes. The association is produced entirely by the sampling: a non-smoker in hospital is likely to be there because of diabetes, whereas a smoker in hospital is often there because of smoking. A study that recruited only inpatients would report this false association even with flawless data entry and modelling.

From DAG to Regression: A Mediation Example

One of the clearest places to see this bridge is mediation. A DAG of the form X → M → Y with a residual direct path X → Y is the qualitative claim; the Baron & Kenny (1986) procedure introduced in 341 puts numbers on the direct and indirect components. Intuitively, the indirect effect is the part of education's benefit that appears only because more education tends to raise income, and higher income in turn improves health; the direct effect is whatever is left once that income pathway is set aside. Below we do that fitting in R, on simulated data so you can verify the answer against the truth.

A short primer on regression for the mediation example

Regression is taught properly in a later lesson, and three ideas are enough to follow this example. First, a regression model such as lm(health ~ education) fits the straight line that best describes how the outcome (health) changes as the predictor (education) increases. The model’s regression coefficient for education is the slope of that line: the average change in health for each one-unit increase in education. When a second predictor is added, as in lm(health ~ education + income), the coefficient for education becomes the average change in health per unit of education among people with the same income, which is what adjusting for income means.

Second, the data are simulated. The code invents 800 people and generates their education, income, and health from rules chosen in advance, so the true effects are known exactly: each unit of education raises income by 0.6 (the true path a), each unit of income raises health by 0.5 (the true path b), and each unit of education raises health directly by 0.3 (the true direct effect c′). The true total effect is therefore 0.3 + 0.6 × 0.5 = 0.60. Because the truth is known, the estimates can be checked against it, which is impossible with real data.

Third, the variables have no real-world units. Education is drawn with rnorm(), which produces values centred on 0 with a standard deviation of 1, so one unit of education is one standard deviation, and income and health are built from education on the same arbitrary scale. A coefficient of 0.30 therefore means that health rises by 0.30 of a unit for each one-unit (one standard deviation) increase in education, on a scale with no direct translation into years of schooling or dollars.

Mediation path diagram: education affects health directly and indirectly through income Education Income Health a b c′ (direct)
The indirect effect is the product a × b; the direct effect is the path that does not pass through income. Their sum is the total effect.
R Fitting a mediation model in R (Baron & Kenny + the mediation package)

The DAG: education → income → health, with education → health directly. We will (i) run Baron & Kenny’s three regressions by hand, then (ii) replicate the result with the mediation package, which gives proper bootstrap confidence intervals for the indirect effect.

# install.packages(c("mediation", "dagitty"))
library(mediation)

# 1. Simulate data that match the DAG: education -> income -> health,
#    with a smaller direct path education -> health.
set.seed(410)
n         <- 800
education <- rnorm(n)
income    <- 0.6 * education + rnorm(n)              # path a
health    <- 0.3 * education + 0.5 * income + rnorm(n)  # direct + path b
dat       <- data.frame(education, income, health)

# 2. Baron & Kenny by hand --------------------------------------------------
#    Step 1: total effect c  (health on education)
coef(lm(health ~ education, data = dat))["education"]

#    Step 2: a (income on education)
fit_M  <- lm(income ~ education, data = dat)

#    Step 3: direct c' (education) and b (income), from health on both
fit_Y  <- lm(health ~ education + income, data = dat)
coef(fit_Y)                     # c' on education, b on income

# Indirect effect = a * b  (or equivalently c - c')
a <- coef(fit_M)["education"]
b <- coef(fit_Y)["income"]
a * b

# 3. Same answer, with bootstrap CIs, via the mediation package -------------
med <- mediate(fit_M, fit_Y,
                treat    = "education",
                mediator = "income",
                boot     = TRUE, sims = 1000)
summary(med)

What each line does.

CodeWhat it does
library(mediation)This line loads the mediation package, which supplies the mediate() function used at the end.
set.seed(410)This line fixes R’s random-number generator, so the simulated data, and therefore every number in the output, are identical on every computer that runs the code.
n <- 800This line stores the number of simulated people. The arrow <- assigns the value on its right to the name on its left.
education <- rnorm(n)This line draws 800 values from a normal (bell-shaped) distribution with mean 0 and standard deviation 1.
income <- 0.6 * education + rnorm(n)This line builds income so that each unit of education adds 0.6 (the true path a), plus random variation.
health <- 0.3 * education + 0.5 * income + rnorm(n)This line builds health from a direct education effect of 0.3 (the true c′) and an income effect of 0.5 (the true b), plus random variation.
dat <- data.frame(...)This line combines the three variables into one table with 800 rows and three columns.
lm(health ~ education, data = dat)This call fits a linear regression of health on education. The tilde ~ separates the outcome (left) from the predictors (right).
coef(...)["education"]This call extracts the fitted coefficients and keeps the one named education, which in the first model is the total effect c.
fit_M <- lm(income ~ education, ...)This line fits the mediator model; its education coefficient is a.
fit_Y <- lm(health ~ education + income, ...)This line fits the outcome model with both predictors; its education coefficient is c′ and its income coefficient is b.
a * bThis line multiplies the two path coefficients to give the indirect effect.
mediate(fit_M, fit_Y, treat, mediator, boot = TRUE, sims = 1000)This call combines the two models to estimate the indirect, direct, and total effects. treat names the exposure, mediator names the mediator, and boot = TRUE with sims = 1000 requests 1,000 bootstrap resamples to build the confidence intervals.
summary(med)This call prints the table of estimates, confidence intervals, and p-values.

Console output, with the four paths marked. The first three printed results come from the by-hand part of the code. The highlighted numbers are c (total effect, first result), c′ and b (the education and income coefficients in fit_Y), and a × b (the indirect effect, printed under the name education because a carries that label). The (Intercept) is the predicted health score when every predictor equals zero and is not needed here.

Console output: the by-hand results
education 0.6474746 (Intercept) education income 0.01002411 0.35055962 0.44774286 education 0.296915

The value of a is computed by the code without being printed. Printing the coefficients of the mediator model shows it:

coef(fit_M)    # the education coefficient is path a
Console output
(Intercept) education 0.02401282 0.66313739
PathWhere it comes fromEstimateTrue value
c (total effect)The education coefficient in lm(health ~ education).0.6470.60
aThe education coefficient in fit_M (income on education).0.6630.60
bThe income coefficient in fit_Y (health on education and income).0.4480.50
c′ (direct effect)The education coefficient in fit_Y.0.3510.30
a × b (indirect effect)The product of a and b; it also equals c − c′ = 0.6475 − 0.3506 = 0.2969.0.2970.30
Console output: summary(med)
Running nonparametric bootstrap Causal Mediation Analysis Nonparametric Bootstrap Confidence Intervals with the Percentile Method Estimate 95% CI Lower 95% CI Upper p-value ACME 0.29692 0.24316 0.34977 < 2.2e-16 *** ADE 0.35056 0.28202 0.43229 < 2.2e-16 *** Total Effect 0.64747 0.57748 0.72300 < 2.2e-16 *** Prop. Mediated 0.45857 0.38062 0.53752 < 2.2e-16 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Sample Size Used: 800 Simulations: 1000

Reading the summary(med) table. Each row is one effect. ACME stands for average causal mediation effect, the indirect effect through income (a × b). ADE stands for average direct effect, the effect of education that does not pass through income (c′). Total Effect is c, and Prop. Mediated is the share of the total effect that travels through income: 0.297 ÷ 0.647 = 0.459. The Estimate column repeats the by-hand values, because the bootstrap adds uncertainty intervals without changing the estimates.

The 95% confidence interval. For the ACME, the interval runs from 0.243 to 0.350. A confidence interval shows how precisely an effect has been estimated: the values inside it are those most compatible with the data, and the method that produces it captures the true value in 95% of repeated samples. In this simulation the true indirect effect, 0.30, lies inside the interval. A narrow interval indicates a precise estimate, and an interval that includes 0 indicates that the data cannot rule out the absence of an indirect effect.

The p-value. The p-value answers the question of how often an estimate at least this far from zero would arise by chance if the true effect were zero. R prints < 2.2e-16 for any p-value below 0.00000000000000022. Here the p-value comes from the 1,000 bootstrap resamples, none of which fell on the other side of zero, so the package records it as zero. With 1,000 resamples the smallest p-value distinguishable from zero is 0.002, so the result is reported as p < 0.002. A p-value this small means the data are very hard to reconcile with an effect of zero. The stars repeat the same information using the thresholds listed on the Signif. codes line. The p-value carries no information about the size or importance of an effect; the estimate and its confidence interval carry that information.

A model reporting sentence. “In simulated data (n = 800), the estimated indirect effect of education on health through income was 0.30 units of the simulated health score per standard deviation of education (95% CI 0.24 to 0.35), the direct effect was 0.35 (95% CI 0.28 to 0.43), and the total effect was 0.65 (95% CI 0.58 to 0.72); income carried an estimated 46% of the total effect (95% CI 38% to 54%).”

Two cautions. (1) The estimate of the indirect effect is only as credible as the DAG. If an unmeasured variable confounds income and health, the indirect estimate is biased even though every regression runs cleanly. (2) Both the Baron & Kenny arithmetic and mediate() as called here assume that the effect of income on health is the same at every level of education, which is called the assumption of no exposure-mediator interaction. To relax it, the outcome model is fitted as lm(health ~ education + income + education:income, data = dat), where education:income is the interaction term; mediate() then reports separate indirect effects, labelled ACME (control) and ACME (treated), for education values of 0 and 1.

R Reflect on what you just ran

Use the questions below to interpret the output shown above, which matches what the code produces when it is run.

1. Compare the total effect c from lm(health ~ education) with the direct effect c' (the education coefficient in fit_Y). Which is larger, and what does the difference imply about the role of income?

Model answerWith set.seed(410), the total effect c from lm(health ~ education) is 0.647, while the direct effect c' from fit_Y is 0.351, so the total is larger. The difference (0.297) is the indirect effect operating through income; income carries a little under half (46%) of education's effect on health. This is the classical Baron & Kenny mediation signal: a substantial drop from c to c' when the mediator is added to the model.

2. Multiply a * b by hand. How close is this product to the bootstrapped ACME from summary(med)? Does the 95% CI for ACME exclude zero?

Model answera = 0.663 (income on education) and b = 0.448 (the income coefficient in fit_Y, the model of health on education and income); a×b = 0.297, close to the simulated truth of 0.6 × 0.5 = 0.30. The bootstrapped ACME from summary(med) is reported as 0.297 with 95% CI (0.243, 0.350), the same value as the product (the bootstrap only adds the interval), with the CI clearly excluding zero. The agreement validates that the by-hand and package estimates match; the CI confirms statistical significance.

3. The output reports a Prop. Mediated of about 0.46. Translate that into a sentence about education, income, and health. What would change about your interpretation if the 95% CI for ACME crossed zero?

Model answerProp. Mediated = 0.459 (95% CI 0.381, 0.538): a little under half of the total effect of education on health flows through income, while the rest is a direct effect through other mechanisms, such as health-information access, health-system navigation, and health-promoting environments. If the 95% CI for ACME crossed zero, the indirect path through income would not be statistically significant; the interpretation would shift to "we cannot rule out that income contributes nothing to the education-health link in this dataset." Substantive caution: ACME's credibility is bounded by the no-unmeasured-confounding assumption on the M→Y path.
Saved.
R A reproducible project skeleton in RStudio

The structured approach starts with structured files. The convention below, with one RStudio project, one folder per stage, and one numbered script per task, scales from a homework assignment to a journal-ready paper. The steps are as follows.

  1. Open the RStudio project created in the setup box (File, Open Project), so that R works inside the project folder.
  2. Run the four dir.create() lines below once, in the Console. They create the folders data/raw, data/processed, R, and output/figures inside the project.
  3. Download cohort.csv from the link above and move it into the new data/raw folder. The file describes a small smoking cohort: 600 participants, 36 of whom have no recorded outcome because they were lost to follow-up.
  4. Create a new script, save it as R/01_load_clean.R, paste in the code from library(tidyverse) onward, and run it line by line.
  5. Compare the console with the output shown below the code.
# Create directories from R (or by hand). Run once at project start.
dir.create("data/raw",        recursive = TRUE)
dir.create("data/processed",  recursive = TRUE)
dir.create("R");  dir.create("output/figures", recursive = TRUE)

# tidyverse: dplyr (manipulation), ggplot2 (graphics), readr (file IO),
# tidyr (reshape), stringr (text). Install once.
# install.packages(c("tidyverse", "here"))
library(tidyverse)
library(here)                                # builds file paths from the project folder

# A canonical pipeline: read -> clean -> save -> analyse
raw <- read_csv(here("data/raw/cohort.csv"))
clean <- raw |>
  filter(!is.na(outcome)) |>
  mutate(age_grp = cut(age, c(0, 30, 50, 70, Inf)),
         smoker  = factor(smoker, levels = c("No", "Yes")))
write_csv(clean, here("data/processed/cohort_clean.csv"))

# Sketch a DAG to anchor the analysis (see the earlier DAG course)
# library(dagitty)
# g <- dagitty("dag { smoker -> outcome ; age -> smoker ; age -> outcome }")
Conventions worth defending
. +-- R/ <- analysis scripts (numbered: 01_load.R, 02_clean.R) +-- data/raw/ <- never overwritten; treated as read-only +-- data/processed/ <- generated, fully reproducible from R/ +-- output/figures/ <- numbered .png/.pdf for the paper +-- project.Rproj <- one click reopens the whole environment

Checking the data before and after cleaning. When read_csv() runs, it prints a short report of what it found. chr marks text (character) columns and dbl marks numeric columns (“double” is R’s name for a number that can have decimals). The message is informational, and adding show_col_types = FALSE to read_csv() switches it off.

Console output when read_csv() runs
Rows: 600 Columns: 6 ── Column specification ──────────────────────────────────────────────────────── Delimiter: "," chr (3): id, sex, smoker dbl (3): age, followup_years, outcome ℹ Use `spec()` to retrieve the full column specification for this data. ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Before any cleaning, glimpse() shows every variable on one line, with its type and its first few values:

glimpse(raw)    # one line per variable: name, type, first values
Console output
Rows: 600 Columns: 6 $ id <chr> "C001", "C002", "C003", "C004", "C005", "C006", "C007",… $ age <dbl> 62, 76, 36, 76, 47, 51, 30, 38, 21, 60, 51, 79, 69, 73,… $ sex <chr> "Female", "Female", "Male", "Male", "Female", "Male", "… $ smoker <chr> "No", "Yes", "No", "No", "No", "No", "Yes", "Yes", "No"… $ followup_years <dbl> 3.8, 4.2, 4.2, 8.9, 7.4, 4.8, 1.1, 3.5, 3.8, 3.4, 5.5, … $ outcome <dbl> 0, 0, NA, 1, 1, 0, 1, NA, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0,…

The output can be read against a short codebook for the file:

VariableType in RMeaning and codes
idtext (chr)This is the participant identifier, running from C001 to C600.
agenumber (dbl)This is age in years, from 20 to 79.
sextext (chr)Sex is recorded as Female or Male.
smokertext, converted to a factor by the pipelineSmoking status is recorded as No or Yes.
followup_yearsnumber (dbl)This is the length of follow-up in years, from 1.0 to 10.0.
outcomenumber (dbl)The value is 1 if the event occurred during follow-up and 0 if it did not; it is NA (a blank cell in the file) for participants lost to follow-up.

Counting rows before and after the filter confirms what the pipeline did, and count() shows how many participants fall in each new age band:

nrow(raw)       # rows before filtering
nrow(clean)     # rows after dropping participants with no outcome
count(clean, age_grp)   # participants in each age band
Console output
[1] 600 [1] 564 # A tibble: 4 × 2 age_grp n <fct> <int> 1 (0,30] 100 2 (30,50] 176 3 (50,70] 202 4 (70,Inf] 86

Reading the output. The raw file has 600 rows and the cleaned file 564, so 36 participants were removed. In filter(!is.na(outcome)), is.na() asks whether each outcome is missing and ! means “not”, so the filter keeps the rows whose outcome is known. In the age bands, (30,50] means older than 30 and up to and including 50; a round bracket excludes the boundary and a square bracket includes it.

Dropping missing outcomes is a decision to record. Keeping only participants with a known outcome is a complete-case decision: the analysis will describe only the people who were followed to the end. If those lost to follow-up differ from the rest (for example, if sicker participants were more likely to drop out), the remaining 564 no longer represent the cohort, and the results may be biased. The decision therefore belongs in the file log, with the number of rows removed and the reason, as in the two lines below. The date in the log will be the date on which the code is run.

# Record the complete-case decision in the file log
cat(format(Sys.Date()), "cohort_clean.csv:", nrow(raw) - nrow(clean),
    "participants with no recorded outcome excluded;", nrow(clean), "remain\n",
    file = here("data/file_log.txt"), append = TRUE)
readLines(here("data/file_log.txt"))
Console output
[1] "2026-10-01 cohort_clean.csv: 36 participants with no recorded outcome excluded; 564 remain"

The pipe operator |> (or %>%) is the workhorse of the tidyverse: it chains verb -> verb -> verb so analysis reads top to bottom. Combined with here() for paths, your project is movable, shareable, and version-control-friendly out of the box.

R Reflect on what you just ran

Use the questions below to interpret the output shown above, which matches what the code produces when it is run.

1. After running dir.create() four times, what folder structure now exists in your project? Why is keeping data/raw/ separate from data/processed/ a defensible choice?

Model answerThe four dir.create() calls create: data/raw/, data/processed/, R/, and output/figures/. Keeping data/raw/ separate from data/processed/ is defensible because raw data is the irreplaceable artefact, the source of truth that should never be modified. Processed data are derived from raw; if a cleaning bug is discovered, you can re-derive from raw, but if you overwrote the raw file, the bug is permanent. The separation enforces the rule "raw is read-only; processed is regenerable."

2. Trace the pipeline raw |> filter(...) |> mutate(...). One brand-new column is added by mutate() and one existing column is re-encoded in place. Which is which?

Model answerOnly one brand-new column appears in clean: age_grp, a factor with age bands (0–30, 30–50, 50–70, 70+) built by cutting continuous age. The mutate() call also rewrites smoker, but that column already existed in raw; factor(smoker, levels = c("No", "Yes")) re-encodes it in place so the model's reference level is "No". So the pipeline adds one column (age_grp) and transforms one (smoker); filter() only drops rows and creates no columns.

3. Why does the script use here("data/raw/cohort.csv") instead of an absolute path like "C:/Users/.../cohort.csv"? Give one practical scenario where this matters.

Model answerhere() resolves paths relative to the project root, so the same script works on any machine, in any user account, in any operating system, as long as the project structure is unchanged. Absolute paths break instantly when the project is moved, shared with a collaborator on a different operating system, or run on another computer such as a university server. Concrete scenario: you send your code to a co-author for review. They unzip the project on their Mac at ~/projects/this_study/; your hardcoded C:/Users/.../ would fail immediately, while here("data/raw/cohort.csv") just works.
Saved.

Managing Data-Collection Sheets

It is important to establish a permanent storage system for all original data-collection sheets (survey forms, data-collection forms, etc.) that makes it easy to retrieve individual sheets if they are needed during the analysis.

Protect your originals

Do not remove originals from your file. If you need a specific sheet for use at another location, make a photocopy. Never ship the original to another location without first making copies of all forms.

Track collection progress

Set up a system for recording the insertion of data-collection sheets into the file so that you know how many remain to be collected before further work begins.

Scan for completeness

Once all forms have been collected, scan through all sheets for their completeness before doing anything else. If there are omissions, returning to the data source to complete the data will be more likely to succeed if done soon after collection rather than weeks or months later.

Knowledge check: this section

1. What should you construct before beginning any work with your data?

Before working with your data, you should construct a plausible causal diagram. This identifies which variables are important outcomes and predictors, which are potential confounders, and which might be intervening variables.

2. Why is data analysis described as an “iterative process”?

Data analysis is iterative because as you gain more insight into your data, you often need to revisit earlier steps, revise your approach, and re-examine your variables and models.

3. What should you do if you find omissions in data-collection sheets?

Returning to the data source to complete missing data will more likely be successful if done soon after the data were initially collected, rather than weeks or months later when the analysis has begun.

Reflection

Think of a research question you are interested in. A causal diagram shows the outcome, the main predictors (exposures) of interest, the confounders (variables that affect both a predictor and the outcome and are not on the causal pathway between them), and any intervening variables (variables that lie on the causal pathway from a predictor to the outcome). Sketch out (describe) such a diagram for your question. How does the diagram help you plan your analysis, for example in deciding which variables to adjust for and which to leave out?

Model answerPick a question (e.g., does air-pollution exposure during pregnancy lower birth weight?). DAG: prenatal PM2.5 exposure → birth weight, with confounders maternal age, SES, smoking, prenatal-care utilisation, neighbourhood environment, and pre-pregnancy BMI all pointing into both exposure and outcome. Intervening variables: placental function and maternal hypertension on the causal path. The DAG helps plan the analysis by (a) identifying the minimal sufficient adjustment set (using dagitty: the set of confounders to control for to identify the total effect of PM2.5, as in the dagitty example in this section); (b) flagging mediators that must NOT be adjusted for if the total effect is the question (placental function); (c) flagging potential colliders (e.g., gestational age) that must be considered carefully; (d) supporting a mediation analysis if the indirect path through placental function is the question. The DAG turns a list of "things to control for" into a structured causal claim.
Reflection saved!
* Complete the quiz and reflection to continue.
Section 2

Data Coding, Entry & File Management

⏱ Estimated time: 20 minutes
Section 2 of 4

Data Coding, Entry & File Management

Encoding responses, organising project files, and building a variable codebook.

Coding conventions

Three rules that prevent cascading errors

Missing values

Assign a code that is impossible as a real response (e.g., −999). Never use a value that could be legitimate data.

Numeric codes

Use numbers for all variables, including those originally recorded as text. In R, the cleaning script then converts each code to a labelled factor.

No compound codes

One variable, one piece of information. A code that bundles sex and ethnicity (1 = male Caucasian, 2 = female Caucasian) breaks the moment you want to analyse either alone.

Data entry

Double-entry and the spreadsheet hazard

Double-entry

Two independent entries, then a programmatic comparison. The most effective way to reduce transcription error.

Spreadsheet caution

Sorting a single column destroys row alignment across the whole dataset. Custom software removes this risk.

Save originals immediately. Store a backup in a second location. Convert to your statistical software's format promptly.

File management

Versioning and the file log

bp01.csv   (28/09) : original; one row per participant; 1092 obs, 8 vars
bp02.csv   (30/09) : 45 missing-value records dropped; 1047 obs, 8 vars
bp03.csv   (02/10) : age_ct, age_ctsq, age_c3, htn added; 1047 obs, 12 vars

Two-digit suffix: 99 versions sort correctly. Never overwrite. The log is your audit trail.

Variable codebook

Naming conventions and the master list

age       Original age in years
age_ct     Age centred by subtracting the mean
age_ctsq   Quadratic term (age_ct squared)
age_c2     Age categorised into 2 groups
age_c3     Age categorised into 3 groups

Group related variables by prefix; shorten long names by removing vowels (wtr_cstrn). One-line description per variable is the minimum standard.

Carry forward

What to take into the next section

  • Coding decisions made in the first hour shape every analysis that follows.
  • Double-entry, versioned files, and a log are the practical backbone of reproducibility.
  • A variable codebook captures what was measured, which is often more useful than the code itself.

Introduction and Overview

An earlier section set up the conceptual workflow and the discipline of starting from a causal diagram before you touch any data. This section turns to the practical side: how do you encode raw responses, enter them into a workable file, organise files across a project, and keep track of what every variable in your dataset means? These are unglamorous tasks, but they determine whether the analysis you eventually run is reproducible. Organising data so that each variable is a column and each observation a row (the tidy data convention) makes downstream analysis dramatically easier (Wickham, 2014).

Learning Objectives

  • Apply coding conventions for missing values, numeric codes, and avoiding compound codes.
  • Plan a data-entry workflow that minimises transcription error.
  • Lay out a project folder structure that distinguishes raw, processed, and analysis files.
  • Build and maintain a variable codebook that any collaborator could open and use.

Data Coding

Before entering data into a computer, careful coding is essential. Good coding practices prevent errors that can cascade throughout an entire analysis.

Missing ValuesClick to explore
Numeric CodesClick to explore
No Compound CodesClick to explore
R Worked Example: how a −999 code distorts a mean, and the fix

The rule about missing-value codes matters because software treats a code such as −999 as a real number until it is told otherwise. The five-person example below shows what happens to the mean age when one person’s unknown age is stored as −999. The function tibble() builds a small table, and c() combines values into a vector.

library(tidyverse)

# Five participants; the third did not report age, coded -999
ages <- tibble(id = 1:5, age = c(34, 51, -999, 47, 62))
mean(ages$age)                       # -999 is treated as a real age
Console output
[1] -161

R has averaged the code with the real ages: (34 + 51 − 999 + 47 + 62) ÷ 5 = −805 ÷ 5 = −161 years, an impossible value. A code of 999 would have produced an equally wrong mean of 238.6 years, and a code hidden among many real values distorts the mean by less, which makes it harder to notice. Converting the code to NA fixes the problem:

# Fix 1: convert the code to NA, R's missing-value code
ages_fixed <- ages |> mutate(age = na_if(age, -999))
ages_fixed$age
mean(ages_fixed$age)                 # NA: R will not average an unknown value
mean(ages_fixed$age, na.rm = TRUE)   # drop the NA, then average the rest
Console output
[1] 34 51 NA 47 62 [1] NA [1] 48.5

na_if(age, -999) replaces every −999 in age with NA. A plain mean() then returns NA, because the average of a set that includes an unknown value is itself unknown; the argument na.rm = TRUE (“NA remove”) tells R to drop the missing value and average the rest: (34 + 51 + 47 + 62) ÷ 4 = 194 ÷ 4 = 48.5 years. The same conversion can be made when the file is first read, by listing every value that should count as missing:

# Fix 2: declare the code as missing when the file is read
demo_file <- tempfile(fileext = ".csv")   # a throwaway file for the demo
write_csv(ages, demo_file)
ages_read <- read_csv(demo_file, na = c("", "NA", "-999"),
                      show_col_types = FALSE)
ages_read$age
Console output
[1] 34 51 NA 47 62

In practice. For a real file, the second fix is written as read_csv("data/raw/file.csv", na = c("", "NA", "-999")). The empty string "" covers blank cells and "NA" covers cells that already contain the letters NA. Every missing-value code used in the study belongs in this list and in the codebook.

R Numeric codes and R factors

The rule to use numbers for every variable comes from software in which text values were awkward to analyse. In R, the usual practice is to keep the numeric codes in the raw file, documented in the codebook, and to convert each coded variable into a factor in the cleaning script. A factor is R’s type for a categorical variable: it stores the codes, attaches a text label to each one, and fixes the order of the categories, so that tables and models display Female and Male in place of 1 and 2. A raw file that already stores text, such as the Yes and No values in cohort.csv, is also acceptable provided every value is spelled consistently, and the same factor() step then fixes the order of the categories.

# The raw file stores sex as 1 = Female, 2 = Male (see the codebook)
codes <- tibble(id = 1:4, sex = c(1, 2, 2, 1))
codes <- codes |>
  mutate(sex_f = factor(sex, levels = c(1, 2), labels = c("Female", "Male")))
codes
table(codes$sex_f)
Console output
# A tibble: 4 × 3 id sex sex_f <int> <dbl> <fct> 1 1 1 Female 2 2 2 Male 3 3 2 Male 4 4 1 Female Female Male 2 2

Reading the code. In factor(sex, levels = c(1, 2), labels = c("Female", "Male")), levels lists the codes in the order wanted and labels gives the text for each code in the same order. In the printed table, <dbl> marks the original numeric column and <fct> marks the new factor. Keeping both columns side by side makes the conversion easy to check. The first level (here Female) becomes the reference category when the factor is used in a regression model.

Data Entry

Some important issues to consider when entering your data into a computer file:

Double-data entry

Double-data entry, followed by comparison of the 2 files to detect any inconsistencies, is preferable to single-data entry. This dramatically reduces the error rate in your dataset.

Caution with spreadsheets

Spreadsheets are a convenient tool for initial data entry, but they must be used with extreme caution. It is possible to sort individual columns, which could destroy your entire dataset with one inappropriate “sort” command. Custom data-entry software provides a greater margin of safety.

Save and back up immediately

As soon as the data-entry process has been completed, save the original data files in a safe location. In large, expensive trials, keep a copy of all originals stored in another location. Convert your data to the format your statistical software uses as soon as possible.

R Worked Example: comparing two independent entries in R

Double data entry produces two files that should be identical, and the comparison step takes only a few lines of code. In the example below, two people entered the same five forms (sbp is systolic blood pressure in mmHg). The function anti_join(x, y, by = ...) returns the rows of x that have no exact match in y on the listed columns, so running it in both directions lists every record on which the two entries disagree.

# The same five forms, entered independently by two people
entry_a <- tibble(id = 1:5, age = c(34, 51, 29, 47, 62),
                  sbp = c(128, 141, 117, 135, 150))
entry_b <- tibble(id = 1:5, age = c(34, 15, 29, 47, 62),
                  sbp = c(128, 141, 117, 153, 150))

# Rows of entry_a with no exact match in entry_b, then the reverse
anti_join(entry_a, entry_b, by = c("id", "age", "sbp"))
anti_join(entry_b, entry_a, by = c("id", "age", "sbp"))
Console output
# A tibble: 2 × 3 id age sbp <int> <dbl> <dbl> 1 2 51 141 2 4 47 135 # A tibble: 2 × 3 id age sbp <int> <dbl> <dbl> 1 2 15 141 2 4 47 153

Reading the output. Records 2 and 4 disagree. In record 2 the first person typed an age of 51 and the second typed 15, a pair of swapped digits; in record 4 the systolic pressure is 135 in one entry and 153 in the other. The analyst checks each disagreement against the original paper form and corrects the wrong entry. An error made identically by both people cannot be detected this way, so double entry reduces errors substantially without removing every one.

Keeping Track of Files

It is important to have a system for keeping track of all your files. Key recommendations:

  • Assign a logical name with a 2-digit numerical suffix (e.g., brazil01). A 2-digit suffix allows you to have 99 versions that still sort correctly when listed alphabetically.
  • When data manipulations are carried out, save the file with a new name (the next available number). Do not change data and then overwrite the file.
  • Keep a simple log of files created with information about the contents (e.g., number of observations and variables).
Example: File Log for a Blood Pressure Study

bp01.csv (28/09): Original blood pressure study data as received; one record per participant (each participant was measured once). 1092 obs, 8 vars.

bp02.csv (30/09): 45 records with a missing age, systolic, or diastolic value dropped. 1047 obs, 8 vars.

bp03.csv (02/10): Derived variables age_ct, age_ctsq, age_c3, and htn added. 1047 obs, 12 vars.

Dates are written as day/month. The textbook version of this log used .dta files, the format of the Stata statistics package. This course stores every version as .csv (comma-separated values), a plain-text table that R, Stata, SPSS, and spreadsheet programs can all open.

R A reproducible recoding pipeline (no overwrites, ever)

The "save a new version, don't overwrite" rule is automatic if your transformations live in a script. The script, together with the log line it writes, becomes the file log. Download bp01.csv (the blood pressure study from the file log above: 1092 records, one per participant, 8 variables, 45 records incomplete) and save it as data/raw/bp01.csv in the project skeleton built earlier. For brevity, the script carries out the second and third steps of the log in one pass, so the file it saves as bp02.csv already contains the four derived variables (1047 observations and 12 variables); in a larger project each step would get its own numbered file, as in the log.

library(tidyverse)

# Read raw, never modify in place
bp_raw <- read_csv("data/raw/bp01.csv")

# Tidy: drop incomplete rows, build derived variables, lock factors
bp_clean <- bp_raw |>
  drop_na(systolic, diastolic, age) |>
  mutate(
    age_ct    = age - mean(age),                              # centred
    age_ctsq  = age_ct^2,                                       # quadratic term
    age_c3    = cut(age, c(0, 35, 55, Inf),
                    labels = c("young", "middle", "older")),
    htn       = factor(systolic >= 140 | diastolic >= 90,
                       levels = c(FALSE, TRUE),
                       labels = c("normotensive", "hypertensive"))
  )

# Persist as a new versioned file - and a small log line
write_csv(bp_clean, "data/processed/bp02.csv")
cat("bp02.csv", format(Sys.Date()), nrow(bp_clean), "obs",
    "\n", file = "data/file_log.txt", append = TRUE)

## At any time you can rebuild bp02 from bp01 by re-running this script.

What each function does.

CodeWhat it does
read_csv("data/raw/bp01.csv")This call reads the raw file into R as a table (a tibble) called bp_raw; the file on disk is never changed.
drop_na(systolic, diastolic, age)This step removes every row in which any of the three named variables is missing (NA), which drops the 45 incomplete records.
mutate(...)This step creates new columns, or changes existing ones, from calculations on the existing columns.
age - mean(age)This calculation subtracts the mean age (48.05 years among the 1047 participants) from each person’s age, which centres age at zero.
age_ct^2This calculation squares the centred age; the squared term lets a later model fit a curve.
cut(age, c(0, 35, 55, Inf), labels = ...)This call splits age into bands: above 0 and up to 35 (young), above 35 and up to 55 (middle), and above 55 (older). Inf stands for infinity, so the last band has no upper limit.
systolic >= 140 | diastolic >= 90This comparison returns TRUE for each person whose systolic pressure is at least 140 mmHg or (the | symbol) whose diastolic pressure is at least 90 mmHg, and FALSE otherwise.
factor(..., levels = c(FALSE, TRUE), labels = ...)This call turns the TRUE/FALSE result into a factor with readable labels, so FALSE becomes normotensive and TRUE becomes hypertensive.
write_csv(bp_clean, "data/processed/bp02.csv")This call saves the cleaned table as a new file in the processed folder.
cat(..., file = "data/file_log.txt", append = TRUE)This call writes one line of text to the log file; append = TRUE adds the line at the end so that earlier entries are kept.
format(Sys.Date())Sys.Date() returns today’s date and format() turns it into text such as 2026-10-01.
nrow(bp_clean)This call counts the rows (participants) in the cleaned table.

What htn means. The variable htn (short for hypertension, or high blood pressure) classifies each participant as hypertensive if the systolic pressure is 140 mmHg or higher or the diastolic pressure is 90 mmHg or higher, and as normotensive (normal blood pressure) otherwise. Systolic pressure is the higher number in a reading such as 140/90 and diastolic the lower. The 140/90 threshold is a widely used clinical definition of hypertension; some guidelines use lower thresholds, which is one reason the activity below asks what happens when the cut-off moves. This simple definition ignores on_treatment, so a participant whose blood pressure is controlled by medication is classified as normotensive. A real analysis would decide how to classify treated participants and record that decision in the codebook.

Checking the result. The lines below confirm the row and column counts, show the derived variables for the first three participants, and produce the percentages quoted in the activity answers.

nrow(bp_raw)           # rows before cleaning
nrow(bp_clean)         # rows after drop_na()
ncol(bp_clean)         # 8 original variables + 4 derived
mean(bp_clean$age)     # the value subtracted to make age_ct
Console output
[1] 1092 [1] 1047 [1] 12 [1] 48.04871
head(bp_clean[, c("id", "age", "age_ct", "age_ctsq", "age_c3")], 3)
Console output
# A tibble: 3 × 5 id age age_ct age_ctsq age_c3 <chr> <dbl> <dbl> <dbl> <fct> 1 BP0001 49 0.951 0.905 middle 2 BP0002 43 -5.05 25.5 middle 3 BP0003 82 34.0 1153. older
# Prevalence of hypertension under the 140/90 rule
count(bp_clean, htn) |> mutate(percent = round(100 * n / sum(n), 1))

# The same count with the systolic cut-off raised to 150
bp_clean |>
  mutate(htn150 = systolic >= 150 | diastolic >= 90) |>
  count(htn150) |>
  mutate(percent = round(100 * n / sum(n), 1))
Console output
# A tibble: 2 × 3 htn n percent <fct> <int> <dbl> 1 normotensive 734 70.1 2 hypertensive 313 29.9 # A tibble: 2 × 3 htn150 n percent <lgl> <int> <dbl> 1 FALSE 796 76 2 TRUE 251 24

Reading the output. The 45 incomplete records have gone (1092 to 1047 rows) and four variables have been added (12 columns). count() counts the rows in each category, and the added percent column divides each count by the total: 313 of 1047 participants (29.9%) are hypertensive under the 140/90 rule, and 251 (24.0%, printed as 24) under a 150/90 rule. Centring is easiest to see in the first rows. The mean age is 48.05 years, so participant BP0001, aged 49, has age_ct = 49 − 48.05 = 0.95, and BP0002, aged 43, has age_ct = 43 − 48.05 = −5.05. A centred value of 0 identifies a participant of exactly average age, so in a regression model that uses age_ct, the intercept is the predicted outcome for a participant of average age in this sample. The squared term grows quickly away from the mean: BP0003, aged 82, has age_ct = 33.95 and age_ctsq = 33.95 × 33.95 ≈ 1153.

Two scatterplots of the same simulated data in which the outcome is lowest in middle age. Panel A shows a flat straight line that misses the curve; panel B shows a U-shaped fitted curve that follows the data.
Why a squared term is built in advance. Simulated data in which the outcome is lowest in middle age. A. A model with centred age alone can only draw a straight line, which misses the curve. B. Adding the squared term (age_ctsq) lets the fitted line bend, so it follows the data. In bp01.csv the relationship between age and systolic pressure is close to a straight line, so the squared term adds little there; having it ready makes the check quick to run.

SPSS is a statistics package that is often used through menus. A workflow built only by clicking through menus, in SPSS or any similar program, is difficult to re-derive unless the underlying commands are also saved (SPSS calls them syntax). A script can be re-run six months from now, by a colleague, on a different computer.

R Reflect on what you just ran

Use the questions below to interpret the output shown above, which matches what the code produces when it is run.

1. The mutate() call creates four new variables (age_ct, age_ctsq, age_c3, htn). For each, state in one phrase what kind of variable it is (continuous, categorical, derived) and why a future analyst would want it pre-built.

Model answerage_ct is a continuous variable (mean-centred age); centring is useful because the intercept of a regression model that uses it is the predicted outcome for a participant of average age (48 years in bp02). age_ctsq is a derived continuous variable (centred age squared) that allows the model to fit non-linear age effects without high collinearity with linear age. age_c3 is a categorical (factor) variable (three age groups), convenient for stratified summaries and clinical-grouping interpretation. htn is a derived categorical (binary) variable that allows easy contingency analyses and clinically intuitive subgroup reports. Pre-building these in the cleaning script means downstream analysis code references them by name instead of duplicating the recoding logic.

2. The threshold for htn is systolic >= 140 | diastolic >= 90. If you raised the systolic cutoff to 150, would the prevalence of "hypertensive" go up or down? What does this tell you about the sensitivity of categorical recodes to threshold choice?

Model answerRaising the systolic cutoff to 150 would lower the prevalence of hypertensive classification (fewer people would meet the threshold). This illustrates how sensitive categorical recodes are to threshold choice: in bp02 the 140/90 rule classifies 29.9% of the 1047 participants as hypertensive, and the 150/90 rule 24.0%, so a 10 mmHg shift moves the prevalence by about 6 percentage points. The reproducibility lesson: any categorical cut-point should be defended by reference to clinical guidelines (or explicit alternative cut-points checked in sensitivity analyses), and the analysis script should expose the threshold as a named constant rather than buried in a formula.

3. The script writes bp02.csv rather than overwriting bp01.csv. Describe one error this rule would prevent that a point-and-click workflow which saves changes over the working file would not.

Model answerWriting bp02.csv instead of overwriting bp01.csv preserves an audit trail of derivations. Point-and-click workflows in menu-driven packages such as SPSS make it easy to save changes over the working dataset unless the commands (syntax) are saved as well, so a bug discovered three steps later cannot be undone, because the original derived state is gone. With versioned outputs, the analyst can re-run from any intermediate point, compare one version with another, and verify the consequence of a single cleaning decision. Concrete bug-prevention: a labelling error in bp02 (e.g., reversed levels) is recoverable because bp01 remains intact to be re-derived.
Saved.

Keeping Track of Variables

Even a relatively focused study can give rise to a large number of variables once transformed and recoded variables have been created. Recommendations include:

Use short but informative names and have all related variables start with the same name. Long names can be shortened by removing vowels (e.g., wtr_cstrn for “water cistern”). If your statistics program is case sensitive, use ONLY lower-case letters. At some point, prepare a master list of all variables.

VariableDescription
ageOriginal data (in years)
age_ctAge after centring by subtraction of the mean
age_ctsqQuadratic term (age_ct squared)
age_c2Age categorised into 2 categories (young vs old)
age_c3Age categorised into 3 categories

A Complete Codebook for the Blood Pressure Study

A codebook (also called a data dictionary) lists every variable with its meaning, type, units, allowed values, and number of missing values, so that a collaborator can use the file without asking the original analyst. The table below is the codebook for bp01.csv, extended with the four variables derived in the cleaning script. The ranges and missing counts come from the verification checks in the next section.

VariableDescriptionType and unitsValues or rangeMissing
idParticipant identifiertextBP0001 to BP1092, one per participant0
ageAgenumber, years18 to 8511
sexSextext (factor in analysis)Female, Male0
smokerSmoking statustext (factor in analysis)No, Yes0
bmiBody mass indexnumber, kg/m²16.0 to 41.20
systolicSystolic blood pressurenumber, mmHg85 to 18618
diastolicDiastolic blood pressurenumber, mmHg42 to 11919
on_treatmentTakes blood-pressure medicationtext (factor in analysis)No, Yes0
age_ctAge minus the mean age of 48.05 (derived)number, years−30.05 to 36.950
age_ctsqage_ct squared (derived)number0 to about 13650
age_c3Age band (derived)factoryoung (18 to 35), middle (36 to 55), older (56 to 85)0
htnHypertension by the 140/90 rule (derived)factornormotensive, hypertensive0

The missing counts refer to bp01.csv; in the cleaned file every variable is complete because the 45 incomplete records were removed. A codebook is updated each time a variable is added or redefined, and saving it as a .csv file beside the data (as shown in the next section) keeps the two together.

Knowledge check: this section

1. Why should you never use compound codes?

Only code one piece of information in a single variable. Compound codes (e.g., 1=male Caucasian, 2=female Caucasian) make it extremely difficult to separate and analyse each characteristic independently.

2. What is the advantage of double-data entry?

Double-data entry, followed by comparison of the 2 files to detect any inconsistencies, is preferable to single-data entry because it dramatically reduces the entry error rate.

3. When data manipulations are carried out, what should you do with the file?

Save the file with a new name (the next available number in your naming convention) so you always have a record of all versions and can trace back to the original data if needed.

Reflection

Describe a file-naming and version-control system you would use for a dataset in your own research area. How would you organise the variable names for a study with demographic, clinical, and outcome variables?

Model answerFile naming: YYYYMMDD_studyname_dataset_version.ext (e.g., 20260516_smoking_cohort_clean_v03.csv). Numbered file versions with a one-line log entry for each, plus weekly copies to a second location such as university cloud storage. Scripts can also be tracked with Git, a version-control program that records every saved change, although numbered files and a log achieve the same goal at this stage. Variable naming: lower-case words joined by underscores throughout; demographic prefix dem_ (e.g., dem_age, dem_sex, dem_education); clinical prefix cli_ (cli_bp_systolic, cli_hba1c); outcome prefix out_ (out_mi_5y, out_cvd_death). Avoid spaces and special characters; version suffix for revised derived variables (out_mi_5y_v2); a codebook (a table saved as .csv beside the data) accompanies the dataset documenting type, valid range, missing-data codes, and derivation logic. This is the difference between a dataset a stranger can reproduce in 6 months and one only the original analyst can navigate.
Reflection saved!
* Complete the quiz and reflection to continue.
Section 3

Program Files, Data Editing & Verification

⏱ Estimated time: 15 minutes
Section 3 of 4

Program Files, Data Editing & Verification

Program-mode scripts, systematic data editing, and verification before analysis.

Program vs. interactive

Why the script is the record

Interactive mode

In RStudio: typing in the Console. Useful for exploration. Produces no durable record of steps. Cannot reconstruct what was done.

Program mode

In RStudio: a saved .R script. Commands compiled into a script that can be saved, re-run, and shared. The audit trail is automatic.

All analyses, including descriptive statistics, should live in the script. Mixed spreadsheet-and-program workflows break the audit trail.

Data editing

Three components of scripted editing

Label variables

Attach a short description to each variable name so output is self-explanatory without the codebook open.

Label categories

Attach text labels to numeric codes: sex coded 0/1 displays as "male" and "female", never bare numbers.

Missing-value codes

Standardise every missing indicator (999, blank, full stop) to the single convention the software recognises as missing (NA in R).

Verification

Check before you trust

Continuous variables

Count valid observations and missing values. Check 5 smallest and 5 largest for implausibility. Plot a histogram to assess distribution shape.

Categorical variables

Count valid observations and missing values. Frequency distribution across all categories. Confirm no unexpected categories have appeared.

Carry forward

What to take into the next section

  • Program mode makes every analytic step reproducible by default.
  • Systematic editing belongs in the cleaning script, not scattered across analysis files.
  • Verification catches data problems that sophisticated modelling would only hide, not fix.

Introduction and Overview

An earlier section organised your files and variables. This section turns to the analyses themselves: working in program mode rather than interactively (so the work is reproducible), editing data systematically rather than ad hoc, and verifying that the dataset behaves as expected before you draw any inferences from it. The discipline you build here is what separates a defensible analysis from a fragile one, and it has been argued that reproducibility is now the minimum standard for evaluating computational research (Peng, 2011).

Learning Objectives

  • Contrast interactive and program-mode workflows and justify why programs are required for reproducible work.
  • Edit data through scripted, documented steps rather than ad hoc fixes to the raw file.
  • Run systematic verification checks on ranges, types, and internal consistency.
  • Decide when verification is sufficient to move on to substantive analysis.

Program Mode vs. Interactive Processing

Statistical programs can be used in an interactive mode (selecting items from menus or typing in a command) or in program mode (compiling a series of commands into a program and then running it).

Interactive mode is very useful for exploring your data and trying out analyses. However, it should not be used for any of the “real” processing and/or analysis because it is very difficult to keep a clear record of steps taken. Consequently, it is difficult or impossible to reconstruct the analyses you have completed.

Program mode is the recommended approach. You compile the commands into a program and then run it. These program files can be saved and used to reconstruct any analyses you have carried out. Key tips: name files logically, structure the program to be easy to follow, use sequential indents, and document the file thoroughly with comments.

In RStudio, interactive mode corresponds to typing commands directly into the Console pane, or to clicking through menus such as Import Dataset, and program mode corresponds to writing the commands in a script file (.R) and running it. A practical habit combines the two: commands are tried out in the Console, and each command that is kept is copied into the script with a comment, so that the script alone can recreate every result. In R, a comment is any text that follows a # symbol on a line, and R ignores it when the code runs.

Critical Rule

Do all of the analyses in your statistical program. Don’t start doing basic statistics in a spreadsheet. You are going to need the statistical program eventually, and it will be much easier to keep track of all your analyses if they are all done there.

Data Editing

Before beginning any analyses, spend time editing your data. The most important components are:

Labelling VariablesClick to explore
Labelling CategoriesClick to explore
Missing Value CodesClick to explore

Data Verification

Before you start any analyses, you must verify that your data are correct. This can be combined with data processing and involves going through all of your variables, one-by-one.

For continuous variables
  • Determine the number of valid observations and the number of missing values
  • Check the maximum and minimum values (or the 5 smallest and 5 largest) to make sure they are reasonable; if they are not, find the error, correct it, and repeat the process
  • Prepare a histogram of the data to get an idea of the distribution and see if it looks reasonable
For categorical variables
  • Determine the number of valid observations and the number of missing values
  • Obtain a frequency distribution to see if the counts in each category look reasonable (and to make sure there are no unexpected categories)
R Verifying bp01.csv in R

The verification steps listed above translate into a handful of R functions. The code below runs them on the raw blood pressure file from the previous section, one check at a time, and the output under each piece of code is what it produces. The table after the code explains how to read each check and what a problem would look like.

library(tidyverse)
bp_raw <- read_csv("data/raw/bp01.csv", show_col_types = FALSE)

# 1. Range, quartiles, mean and number missing for each continuous variable
summary(bp_raw[, c("age", "bmi", "systolic", "diastolic")])
Console output
age bmi systolic diastolic Min. :18.00 Min. :16.00 Min. : 85.0 Min. : 42.00 1st Qu.:38.00 1st Qu.:23.90 1st Qu.:116.0 1st Qu.: 71.00 Median :49.00 Median :27.00 Median :128.0 Median : 80.00 Mean :48.05 Mean :26.84 Mean :127.4 Mean : 79.97 3rd Qu.:58.00 3rd Qu.:29.73 3rd Qu.:139.0 3rd Qu.: 88.00 Max. :85.00 Max. :41.20 Max. :186.0 Max. :119.00 NA's :11 NA's :18 NA's :19
# 2. Number of missing values (NA) in every variable
colSums(is.na(bp_raw))
Console output
id age sex smoker bmi systolic 0 11 0 0 0 18 diastolic on_treatment 19 0
# 3. The five smallest and five largest values
head(sort(bp_raw$systolic), 5)
tail(sort(bp_raw$systolic), 5)
head(sort(bp_raw$age), 5)
tail(sort(bp_raw$age), 5)
Console output
[1] 85 85 85 85 85 [1] 172 172 181 186 186 [1] 18 18 18 18 18 [1] 85 85 85 85 85
# 4. Histogram: does the shape look reasonable?
hist(bp_raw$systolic, breaks = 30, xlab = "Systolic BP (mmHg)", main = "")

The histogram appears in the Plots pane; panel A of the figure below shows it.

# 5. Frequency table for each categorical variable, showing NA if any
table(bp_raw$sex, useNA = "ifany")
table(bp_raw$smoker, useNA = "ifany")
table(bp_raw$on_treatment, useNA = "ifany")
Console output
Female Male 554 538 No Yes 844 248 No Yes 759 333
# 6. Consistency check: diastolic pressure should never exceed systolic
bp_raw |> filter(diastolic > systolic)

# 7. Every identifier should appear exactly once (compare with nrow)
n_distinct(bp_raw$id)
Console output
# A tibble: 0 × 8 # ℹ 8 variables: id <chr>, age <dbl>, sex <chr>, smoker <chr>, bmi <dbl>, # systolic <dbl>, diastolic <dbl>, on_treatment <chr> [1] 1092

How to read each check.

CheckWhat the output shows for bp01.csvWhat would signal a problem
1. summary()Min. and Max. are the smallest and largest values; 1st Qu., Median, and 3rd Qu. split the data into quarters; and NA's counts missing values. Ages run from 18 to 85, BMI from 16.0 to 41.2, systolic pressure from 85 to 186 mmHg, and diastolic pressure from 42 to 119 mmHg, all of which are possible in adults.A minimum or maximum that cannot be true, such as an age of 999 or a blood pressure of 0, or a mean far from the median, would call for a closer look.
2. colSums(is.na())is.na() marks each missing value as TRUE and colSums() counts the TRUE values in each column: 11 ages, 18 systolic values, and 19 diastolic values are missing, and the other variables are complete.Missing counts that differ from the study records, or missing values in a variable that should be complete, would need explaining.
3. head(sort()), tail(sort())sort() puts the values in order, head(..., 5) shows the first five, and tail(..., 5) the last five. The lowest systolic value (85 mmHg) and the highest age (85 years) fill all five places shown. A count with sum(bp_raw$systolic == 85, na.rm = TRUE) finds 15 participants at 85 mmHg, and the same count for age finds 6 participants aged 85.An impossible value would be an error. A pile-up of identical values at the edge, as here, can also mean that values beyond a limit were recorded as the limit, which the study protocol should confirm.
4. hist()The histogram of systolic pressure is a single hump centred near 128 mmHg. The small bars above 180 mmHg hold three high but possible readings (181, 186, and 186 mmHg).An isolated bar far from the rest, or a shape that contradicts what is known about the variable, would need investigation.
5. table(..., useNA = "ifany")Each category is listed with its count. The argument useNA = "ifany" adds an NA column only when values are missing, and none are missing here.An unexpected category, such as a misspelling, or more missing values than expected would need correcting.
6. filter(diastolic > systolic)filter() keeps the rows that meet the condition. The result has 0 rows, so no participant has a diastolic reading above the systolic reading.Any returned row identifies a record to check against the original form.
7. n_distinct(id)n_distinct() counts the different values. There are 1092 identifiers for 1092 rows, which confirms one row per participant.Fewer identifiers than rows would mean duplicated records or repeated measurements that the codebook should explain.
Three histograms side by side. A: systolic blood pressure with a single hump between 85 and 186 mmHg. B: age on an axis stretching to 1000, with the real ages squeezed at the left and a tiny isolated bar at 999. C: simulated length of stay with most values below 10 days and a long tail to the right.
Three histograms of the kind hist() produces. A. Systolic blood pressure in bp01.csv: a single hump of plausible values. B. A copy of bp01.csv in which three missing ages were stored as 999 (planted for illustration): the real ages are squeezed against the left edge, and an isolated bar appears at 999 (highlighted in red here). C. Simulated hospital length of stay: most values are small and a long tail stretches to the right. A skewed shape like C can be correct for the variable; it still matters for later choices, such as the log transformation discussed in the next section.
R Worked Example: finding and fixing planted errors

The raw file passes every check above, so the example below plants three typical errors on a copy of the data, leaving the raw file untouched, and shows how the same checks reveal them and how the cleaning script fixes them.

bp_bad <- bp_raw                       # a copy; bp_raw is untouched
bp_bad$age[c(5, 120, 640)] <- 999      # planted: unknown age typed as 999
bp_bad$sex[c(10, 302)] <- "F"          # planted: abbreviation
bp_bad$sex[450] <- "male"              # planted: lower-case spelling

summary(bp_bad$age)
tail(sort(bp_bad$age), 5)
table(bp_bad$sex, useNA = "ifany")
Console output
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's 18.00 38.00 49.00 50.69 58.00 999.00 11 [1] 85 85 999 999 999 F Female male Male 2 552 1 537

The maximum age of 999 and a mean of 50.69 (up from 48.05) reveal the planted code, and the frequency table shows four categories where the codebook allows two. Both problems are fixed in the cleaning script:

# The fix belongs in the cleaning script, never in the raw file
bp_fixed <- bp_bad |>
  mutate(age = na_if(age, 999),
         sex = case_when(sex %in% c("Female", "F") ~ "Female",
                         sex %in% c("Male", "male") ~ "Male"))
summary(bp_fixed$age)
table(bp_fixed$sex, useNA = "ifany")
Console output
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's 18.00 38.00 49.00 48.05 58.00 85.00 14 Female Male 554 538

Reading the fix. na_if(age, 999) turns the code into NA. case_when() checks each condition in turn: %in% asks whether the value is one of the listed spellings, and the value after ~ is assigned when it is. Any value that matches no condition becomes NA, so a spelling that the analyst did not anticipate appears as missing in the next frequency table and is never silently relabelled. The fixed counts, 554 women and 538 men, match the raw file. After the fix, age has 14 missing values (11 original and 3 converted); where possible, the three ages are looked up on the original forms and entered through the script.

R Keeping a codebook in R

Variable labels can be stored in R in several ways. The simplest, and the one that works with every package, is a codebook table saved beside the data. The function tribble() builds a small table row by row: the first line names the columns, each name starting with ~, and every following line describes one variable. Category labels are attached separately with factor(), as shown in an earlier section.

codebook <- tribble(
  ~variable,      ~label,                             ~units_or_codes,
  "id",           "Participant identifier",           "BP0001 to BP1092",
  "age",          "Age",                              "years",
  "sex",          "Sex",                              "Female, Male",
  "smoker",       "Smoking status",                   "No, Yes",
  "bmi",          "Body mass index",                  "kg/m2",
  "systolic",     "Systolic blood pressure",          "mmHg",
  "diastolic",    "Diastolic blood pressure",         "mmHg",
  "on_treatment", "Takes blood-pressure medication",  "No, Yes"
)
codebook
write_csv(codebook, "data/codebook_bp01.csv")   # saved beside the data
Console output
# A tibble: 8 × 3 variable label units_or_codes <chr> <chr> <chr> 1 id Participant identifier BP0001 to BP1092 2 age Age years 3 sex Sex Female, Male 4 smoker Smoking status No, Yes 5 bmi Body mass index kg/m2 6 systolic Systolic blood pressure mmHg 7 diastolic Diastolic blood pressure mmHg 8 on_treatment Takes blood-pressure medication No, Yes

Saving the codebook as a .csv file in the data folder keeps it with the data, and any collaborator can open it in R or in a spreadsheet. When a variable is added or redefined in the cleaning script, the codebook is updated in the same session.

Verification checklist: when to move on

Verification can be considered complete when every statement below is true, and each fix has been made in the cleaning script and recorded in the log.

  • Every variable in the file appears in the codebook, with its meaning, type, units, and allowed values.
  • The number of rows matches the number of participants (or records) expected from the study records, and every identifier is unique.
  • For each continuous variable, the number of missing values is known, and the smallest and largest values are possible.
  • Each continuous variable’s histogram shows no isolated bar at a code such as 999 and no shape that contradicts what is known about the variable.
  • For each categorical variable, the frequency table shows only the categories listed in the codebook.
  • Every missing-value code has been converted to NA.
  • Cross-variable checks, such as a search for diastolic readings above the systolic reading or for pregnancies recorded among male participants, return no rows, or every returned row has been checked against the original form.
  • The cleaning script runs from top to bottom on the raw file and reproduces the cleaned file exactly.
Knowledge check: this section

1. Why should program mode be preferred over interactive mode for “real” data analysis?

Program mode compiles commands into a program file that can be saved and reused, making it possible to reconstruct and reproduce all analyses. Interactive mode makes it difficult to keep a clear record of steps taken.

2. When verifying continuous variables, what should you examine first?

For continuous variables, the first verification step is to determine the number of valid observations and missing values, then check the minimum and maximum values (or the 5 smallest and 5 largest) to make sure they are reasonable.

3. What is the purpose of attaching labels to categorical variable values?

Categorical variables should have meaningful labels attached to each category (e.g., sex coded as 0 or 1 should have labels “male” and “female” attached) so that output is immediately interpretable.

Reflection

Imagine you receive a dataset where a colleague entered data interactively in a spreadsheet with no documentation. What steps would you take to clean, verify, and prepare the data for analysis? What problems might you encounter?

Model answerSteps: (1) Inventory: list all variables, their declared types, missing-value patterns, and value ranges; flag any variable with no obvious purpose. (2) Range checks: identify implausible values (age = 999, blood pressure = 0) and recode to NA with documentation. (3) Consistency checks: cross-variable plausibility (pregnancy in men, dates of birth after dates of death). (4) De-duplication: check for duplicated participant IDs and resolve. (5) Recoding: standardise binary and categorical variables to consistent encodings. (6) Code-up an audit trail: every cleaning decision logged in a script that re-creates the cleaned dataset from raw. (7) Documentation: build a codebook from scratch. Problems likely encountered: free-text entries in numeric fields ("unknown", "N/A", "-"), mixed date formats, automatic conversion by the spreadsheet program (for example, an entry such as 1-2 turned into a date), inconsistent missing-data codes (.,-,99,999), and variables that cannot be interpreted. The audit trail is the single most valuable artefact: it makes the cleaning reproducible and reviewable.
Reflection saved!
* Complete the quiz and reflection to continue.
Section 4

Data Processing & Unconditional Associations

⏱ Estimated time: 20 minutes
Section 4 of 4

Data Processing & Unconditional Associations

Processing outcomes and predictors, multilevel structure, and first associations before multivariable modelling.

Outcome processing

Matching the variable to the model class

Categorical

Sparse categories may need collapsing before multinomial regression.

Continuous

Approximately normal? If not, explore transformations. Normality of residuals is what ultimately matters.

Count / rate

Poisson assumption
\[ \color{#0B7B6B}{\mathbb{E}[Y]} \approx \color{#C2410C}{\operatorname{Var}(Y)} \]
E[Y] mean of the count outcome Var(Y) variance of the count outcome

Overdispersion signals negative binomial.

Time-to-event

Check proportion censored. Plot the empirical hazard before choosing a parametric or Cox model.

Predictor processing

Missing values, distributions, and sparse categories

Missing values

Drop the predictor, run parallel analyses, or use multiple imputation. The choice must be documented and defended.

Continuous spread

Reasonable variation across the range? If most values cluster near a boundary, consider transformation or categorisation.

Sparse categories

Combine thin cells. Sparse categories distort estimates and cause convergence failures in logistic models.

Multilevel structure

Characterise the hierarchy before modelling

Clinical centres Individuals Individuals Individuals Measurements Measurements Measurements

Ignoring clustering produces standard errors that are too small. Mixed models address this in later lessons.

Unconditional associations

One predictor at a time, before the multivariable model

Variable types Method What to examine
Two continuousCorrelation, scatterplot, simple linear regressionDirection, strength, linearity
Continuous + categoricalOne-way ANOVA, simple linear or logistic regressionGroup differences, effect size
Two categoricalCross-tabulation, chi-squaredCell counts, unexpected categories
Carry forward

The analytic log and the road to modelling

Analyse in blocks

Univariate summaries, then bivariate associations, then multivariable models. Each block in a labelled script section.

Log every decision

Date-stamp outputs. Record what each run produced and what it decided. This log makes the analysis defensible under review.

The structured workflow ends here. The next lesson continues with data cleaning and descriptive analysis, and modelling begins in a later lesson, on a dataset that is clean, documented, and understood.

Introduction and Overview

Earlier sections produced a verified, well-documented dataset. This section finally takes the step every student is impatient to make: the actual analysis. We start with how to process outcome and predictor variables for analysis, handle multilevel data structure, and run the “unconditional associations” (single-predictor descriptions) that are the necessary first look at any dataset before you touch a multivariable model.

Learning Objectives

  • Process outcome variables to fit the planned analysis (categorical, continuous, count, or rate).
  • Process predictor variables, including categorisation, scaling, and recoding decisions.
  • Recognise multilevel structure in your dataset and the implications for later modelling.
  • Run and interpret unconditional (single-predictor) associations as the first analytic step.
  • Keep an analytic log that allows you to reconstruct every decision you made.

Processing the Outcome Variable(s)

While verifying data, you can also start processing your outcome variable(s). Review the stated goals of the study to determine the format(s) which best suits the goal(s). Consider the following based on outcome type:

Categorical outcomes

Is the distribution of outcomes across categories acceptable? For example, if you planned a multinomial regression with a 3-category outcome, but very few observations fall in one of the categories, you might want to recode it to a 2-category variable.

Continuous outcomes

Does the variable have the characteristics necessary for the planned analysis? If linear regression is planned, is the distribution approximately normal? If not, explore transformations. Note: It is the normality of the residuals which is ultimately important, but if the original variable is far from normal and there are no strong predictors, the residuals are unlikely to be normal.

Count / rate outcomes

If Poisson regression is planned, are the mean and variance of the distribution approximately equal? The Poisson model assumes they are; when the variance runs well above the mean (a pattern called overdispersion), the fitted standard errors come out too small and the p-values look stronger than the data warrant. If that happens, consider negative binomial regression or alternative analytic approaches.

Time-to-event outcomes

What proportion of the observations are censored? You might also want to generate a simple graph of the empirical hazard function to get an idea what shape it has.

Outcome Types at a Glance

The table below gathers the outcome types from the slide and the accordion above, with an example of each, the model usually used, the check to make now, and where the model is taught. The checks in the fourth column belong to this lesson.

Outcome typeExampleUsual modelWhat to check nowWhere it is taught
Binary (two categories)Hypertensive or normotensive (htn)Logistic regressionBoth categories should contain a reasonable number of people, because a rare category gives unstable estimates.It is taught in a later lesson, Linear and Logistic Regression.
Categorical with three or more categoriesSmoking status recorded as never, former, or currentMultinomial regression (unordered) or ordinal regression (ordered)The count in each category should be checked, and categories that hold only a handful of people can be combined.It is taught in a later lesson, Generalized Linear Models.
ContinuousSystolic blood pressure in mmHgLinear regressionA histogram should show no strong skew; if it does, a transformation such as the logarithm can be tried, and the residuals are checked after the model is fitted.It is taught in a later lesson, Linear and Logistic Regression.
Count or rateNumber of clinic visits in a yearPoisson regression, or negative binomial regression when the variance is much larger than the meanThe mean and the variance of the counts should be compared, and the check is repeated after the model is fitted, because the Poisson assumption applies to people who share the same values of the predictors.It is taught in a later lesson, Generalized Linear Models.
Time to eventYears from enrolment to a cardiovascular eventSurvival models, such as the Cox modelThe proportion of censored observations should be counted, and the event rate over time can be plotted.These models are beyond the scope of this course; HSCI 341 Lesson 8, Time-to-Event Data, introduces them.

Four terms in this section need plain definitions.

TermPlain-language meaning
CensoringIn time-to-event data, a record is censored when follow-up ends before the event has happened, for example because the study closed or the person moved away. The analyst knows only that the event time is later than the last contact, and survival methods use that partial information.
VarianceVariance measures spread. It is the average of the squared distances between each value and the mean (R divides by n − 1), and its square root is the standard deviation.
ResidualA residual is the difference between a person’s observed value and the value the model predicts for that person (observed minus predicted). Linear regression assumes that the residuals are roughly bell-shaped around zero.
OverdispersionA count outcome is overdispersed when its variance is clearly larger than its mean. The Poisson model assumes the two are about equal, so overdispersion makes its standard errors too small and its p-values too optimistic.
R Worked Example: comparing the variance with the mean for counts

The check for a Poisson model is to compare the mean of the counts with their variance. The invented example below records clinic visits in one year for ten patients at each of two clinics; var() computes the variance.

# Clinic visits in one year for ten patients at each of two clinics
visits_a <- c(0, 2, 4, 3, 2, 6, 1, 3, 2, 3)
visits_b <- c(0, 1, 1, 0, 9, 2, 0, 12, 1, 0)
mean(visits_a)
var(visits_a)
mean(visits_b)
var(visits_b)
Console output
[1] 2.6 [1] 2.711111 [1] 2.6 [1] 18.26667

Reading the output. Both clinics average 2.6 visits per patient. For clinic A, the squared distances from 2.6 are 6.76, 0.36, 1.96, 0.16, 0.36, 11.56, 2.56, 0.16, 0.36, and 0.16; they sum to 24.4, and 24.4 ÷ 9 = 2.71, because var() divides by n − 1 = 9. A variance this close to the mean is what a Poisson model expects. In clinic B, most patients have no visits or one visit and two have 9 and 12, so the variance (18.27) is about seven times the mean. That pattern is overdispersion, and a negative binomial model would be the safer starting point for these counts.

Residuals and Transformations

A residual is easiest to understand in a picture. Panel A of the figure below fits a straight line for systolic pressure against age in the cleaned blood pressure data and draws, for 25 randomly chosen participants, the vertical distance between each observed value and the line. Panel B collects the residuals of all 1047 participants in a histogram; a roughly bell-shaped histogram centred on zero is what linear regression assumes. Residuals are checked after a model is fitted, which a later lesson covers in detail. At the processing stage the analyst looks at the outcome itself, because a strongly skewed outcome rarely produces bell-shaped residuals.

Left: scatterplot of 25 participants with a fitted line and red vertical segments from each point to the line. Right: histogram of 1047 residuals, roughly bell-shaped and centred on zero.
Residuals from a linear regression of systolic blood pressure on age (bp01.csv after cleaning). A. Each red segment is one participant’s residual, the observed value minus the value on the fitted line; points above the line have positive residuals. B. Histogram of all 1047 residuals, centred on zero (dashed line) and roughly bell-shaped, which is the pattern linear regression assumes.
R Worked Example: before and after a log transformation

When an outcome has a long right tail, taking the natural logarithm of each value, log() in R, often makes the distribution close to symmetric, because the logarithm shrinks large values much more than small ones. The simulated example below uses hospital length of stay in days. The function rlnorm() draws right-skewed values for the simulation, and round(..., 1) keeps one decimal place.

# Simulated hospital length of stay (days) for 1000 patients
set.seed(4104)
los <- round(rlnorm(1000, meanlog = 1.2, sdlog = 0.8), 1)
summary(los)        # mean well above median: a sign of right skew
summary(log(los))   # after the log transformation
hist(los, breaks = 40)          # before: long right tail
hist(log(los), breaks = 30)     # after: roughly symmetric
Console output
Min. 1st Qu. Median Mean 3rd Qu. Max. 0.300 1.900 3.400 4.568 5.625 40.700 Min. 1st Qu. Median Mean 3rd Qu. Max. -1.2040 0.6419 1.2238 1.1902 1.7272 3.7062
Left: histogram of length of stay in days with a tall peak near 2 days and a long tail to 40 days. Right: histogram of the logarithm of length of stay, roughly symmetric and bell-shaped.
The two histograms produced by the code. A. Length of stay in days (simulated): most stays are short and a long tail stretches to 40 days. B. The same values after log(): the tail is pulled in and the shape is roughly symmetric.

Reading the output. Before the transformation the mean (4.57 days) is well above the median (3.40 days), because the few very long stays pull the mean upwards; this gap is a quick numerical sign of right skew. After the transformation the mean (1.19) and median (1.22) are close, matching the symmetric histogram. In a real analysis the new variable is created in the cleaning script, for example mutate(log_los = log(los)), and the transformation is recorded in the codebook. The logarithm is defined only for values above zero, so an outcome that contains zeros needs adjustment first; the next lesson returns to transformations.

Processing Predictor Variables

It is important to go through all predictor variables to determine how they will be handled:

  • Missing values: Are there many? If so, you might need to abandon plans to use that predictor, or conduct 2 analyses (one on the subset where the predictor is present and one on the full dataset ignoring the predictor). A third option, multiple imputation, fills each missing value several times with plausible values predicted from the other variables and combines the results; the next lesson introduces it. Whichever option is chosen is recorded in the analysis log with its reason.
  • Distribution: For continuous variables, is there a reasonable representation over the whole range of values? If not, it might be necessary to categorise the variable.
  • Categorical variables: Are all categories reasonably well represented? If not, you might have to combine categories.
R Tabulating missing values and logging the decision

The first question about any predictor is how much of it is missing. The two lines below give the count and the percentage of missing values for every variable in the raw blood pressure file. colMeans(is.na()) gives the proportion missing, because R counts each TRUE as 1 and each FALSE as 0.

colSums(is.na(bp_raw))                    # number missing per variable
round(100 * colMeans(is.na(bp_raw)), 1)   # percentage missing per variable
Console output
id age sex smoker bmi systolic 0 11 0 0 0 18 diastolic on_treatment 19 0 id age sex smoker bmi systolic 0.0 1.0 0.0 0.0 0.0 1.6 diastolic on_treatment 1.7 0.0

The decision that follows belongs in the analysis log in plain words. An entry for this file might read as follows.

Example analysis-log entry (written by the analyst)
2026-10-02 bp02.csv Missing data: age 11 (1.0%), systolic 18 (1.6%), diastolic 19 (1.7%); all other variables complete. Decision: complete-case analysis; 45 participants (4.1%) excluded, because each variable is missing for under 2% of participants. Revisit if a later model adds a variable with more missing values.

The analyst types the log entry by hand, using the counts from the output above; 45 of 1092 participants is 4.1%.

R Worked Example: combining a sparse category

The cross-tabulation below splits the cleaned blood pressure data into ten-year age bands and counts normotensive and hypertensive participants in each. The breaks c(17, 29, 39, ...) work because ages are whole numbers, so the band above 17 and up to 29 holds ages 18 to 29.

# bp_clean comes from the recoding pipeline in the section on data coding
bp_dec <- bp_clean |>
  mutate(age_dec = cut(age, c(17, 29, 39, 49, 59, 69, 79, Inf),
                       labels = c("18-29", "30-39", "40-49", "50-59",
                                  "60-69", "70-79", "80+")))
table(bp_dec$age_dec, bp_dec$htn)
Console output: before
normotensive hypertensive 18-29 110 4 30-39 146 32 40-49 190 68 50-59 187 81 60-69 77 79 70-79 21 35 80+ 3 14
# Merge the two oldest bands into 70+
bp_dec <- bp_dec |>
  mutate(age_dec6 = cut(age, c(17, 29, 39, 49, 59, 69, Inf),
                        labels = c("18-29", "30-39", "40-49", "50-59",
                                   "60-69", "70+")))
table(bp_dec$age_dec6, bp_dec$htn)
Console output: after
normotensive hypertensive 18-29 110 4 30-39 146 32 40-49 190 68 50-59 187 81 60-69 77 79 70+ 24 49

Reading the output. Before merging, the 80+ band holds only 17 participants, and only 3 of them are normotensive. A logistic model that compares each band with a reference band would estimate the 80+ effect from those 3 people, which gives a very wide confidence interval: with 50–59 as the reference band, the odds of hypertension in the 80+ band are estimated at about 11 times the odds in the reference band, with a 95% confidence interval from about 3 to more than 38. A band with no normotensive participants at all would prevent the model from converging (the fitting procedure cannot settle on an answer). Merging 70–79 and 80+ into 70+ gives 24 normotensive and 49 hypertensive participants, enough for a stable estimate. The 18–29 band has only 4 hypertensive participants and might likewise be merged with 30–39. Each merge is a judgement made before the outcome model is fitted, and it is recorded in the log.

Multilevel Data

If your data are multilevel (e.g., blood pressure measurements within individuals within centres), evaluate the hierarchical structure:

Key Questions for Multilevel Data

What is the average (and range) number of observations at one level in each higher-level unit? Are individuals uniquely identified within a hierarchical level? It is often useful to create one unique identifier for each observation in the dataset.

Why this matters: observations from the same person, or the same centre, resemble one another more than observations drawn at random, so a model that ignores the clustering behaves as if it holds more independent information than it really does. The result is standard errors that are too small and confidence intervals that are too narrow. Mixed models, which arrive later in the course, are built for exactly this structure.

R A long-format multilevel table in R

Multilevel data are usually stored in long format, with one row per measurement and columns that identify the person and the centre. The small table below, typed into R for illustration with tribble(), holds ten blood pressure measurements on five people in two centres.

# Long format: one row per measurement (illustrative data typed into R)
bp_long <- tribble(
  ~centre, ~person, ~visit, ~sbp,
  "A",     "A01",   1,      132,
  "A",     "A01",   2,      128,
  "A",     "A02",   1,      141,
  "A",     "A02",   2,      139,
  "A",     "A02",   3,      137,
  "B",     "B01",   1,      118,
  "B",     "B01",   2,      121,
  "B",     "B02",   1,      150,
  "B",     "B03",   1,      126,
  "B",     "B03",   2,      124
)

Three counts characterise the hierarchy. count(bp_long, centre, person) counts the rows for each combination of centre and person, which is the number of measurements per person; distinct() keeps one row per person, so counting those rows by centre gives the number of people per centre; and n_distinct() counts the different person identifiers.

count(bp_long, centre, person)                        # measurements per person
bp_long |> distinct(centre, person) |> count(centre)  # people per centre
n_distinct(bp_long$person)                            # people overall
Console output
# A tibble: 5 × 3 centre person n <chr> <chr> <int> 1 A A01 2 2 A A02 3 3 B B01 2 4 B B02 1 5 B B03 2 # A tibble: 2 × 2 centre n <chr> <int> 1 A 2 2 B 3 [1] 5

Reading the output. People have between one and three measurements, centre A has two people and centre B three, and there are five people in total. Here every person identifier begins with the centre letter, so each person is uniquely identified across centres. If two centres had both numbered their patients 01, 02, 03, the analyst would build a unique identifier first, for example with mutate(uid = paste(centre, person)), so that no merging step could confuse two people.

Unconditional Associations

Before proceeding with any multivariable analyses, it is important to evaluate unconditional associations within the data, that is, the crude one-predictor-at-a-time relationships examined with nothing else held constant. These serve as the foundation for building more complex models.

Variable TypesAnalytical Approach
Two continuous variablesCorrelation coefficient, scatterplot, simple linear regression
One continuous + one categoricalOne-way ANOVA, simple linear or logistic regression
Two categorical variablesCross-tabulation and χ² test

In the middle row of the table, the choice depends on which variable is the outcome. When the continuous variable is the outcome, as when systolic pressure is compared across smoking groups, one-way ANOVA or a simple linear regression compares the group means, and the two methods give the same overall p-value for any number of groups. When the categorical variable is the outcome and has two categories, as when hypertension is related to age, simple logistic regression is used; it is taught in a later lesson. The worked examples below run one example of each row on the cleaned blood pressure data (bp_clean from the recoding pipeline in the section on data coding; in a new R session, that script is run first).

R Worked Example: two continuous variables (age and systolic pressure)

The correlation coefficient r summarises how closely two continuous variables follow a straight line. cor() computes it, and cor.test() adds a 95% confidence interval and a p-value.

cor(bp_clean$age, bp_clean$systolic)        # correlation coefficient r
cor.test(bp_clean$age, bp_clean$systolic)   # adds a 95% CI and a p-value
Console output
[1] 0.4759709 Pearson's product-moment correlation data: bp_clean$age and bp_clean$systolic t = 17.495, df = 1045, p-value < 2.2e-16 alternative hypothesis: true correlation is not equal to 0 95 percent confidence interval: 0.4277199 0.5215171 sample estimates: cor 0.4759709

The scatterplot shows the shape of the relationship, and a simple linear regression puts a number on the slope. confint() gives 95% confidence intervals for the coefficients.

plot(systolic ~ age, data = bp_clean)         # scatterplot
fit1 <- lm(systolic ~ age, data = bp_clean)   # simple linear regression
coef(fit1)
confint(fit1)                                 # 95% CIs for the coefficients
Console output
(Intercept) age 99.3509023 0.5851592 2.5 % 97.5 % (Intercept) 96.0561027 102.6457019 age 0.5195291 0.6507894

Reading the output. The correlation is r = 0.48: systolic pressure tends to be higher in older participants, with considerable scatter. As a rough guide, values of |r| below about 0.3 are described as weak, values from about 0.3 to 0.7 as moderate, and values above about 0.7 as strong; the labels are conventions, and the scatterplot should always be inspected as well. In the cor.test() output, the 95% confidence interval for r runs from 0.43 to 0.52, and the p-value (< 2.2e-16) means that a correlation this large would almost never arise by chance if age and systolic pressure were unrelated; t and df are the test statistic and its degrees of freedom. The regression slope for age is 0.585: on average, systolic pressure is 0.59 mmHg higher for each additional year of age (95% CI 0.52 to 0.65). The intercept, 99.4 mmHg, is the predicted value at age 0, which lies far outside the data and has no practical meaning; centred age would give an intercept for a participant of average age.

A model reporting sentence. “Systolic blood pressure was moderately correlated with age (r = 0.48, 95% CI 0.43 to 0.52); in a simple linear regression, each additional year of age was associated with a 0.59 mmHg higher systolic pressure (95% CI 0.52 to 0.65), before adjustment for other variables.”

Three scatterplots of systolic blood pressure. A: against BMI, a wide cloud with a shallow line, r = 0.21. B: against age, an upward trend with much scatter, r = 0.48. C: against diastolic pressure, a tight band around a steep line, r = 0.84.
Weak, moderate, and strong correlations in the cleaned blood pressure data, each with its fitted straight line. A. BMI: a wide cloud and a shallow line (r = 0.21). B. Age: a clear upward trend with much scatter (r = 0.48). C. Diastolic pressure: points packed tightly around the line (r = 0.84). A scatterplot also shows whether a relationship is straight or curved, which r alone cannot show.
R Worked Example: one continuous and one categorical variable (systolic pressure by group)

For two groups, the group means and a simple linear regression answer the question together. tapply(x, group, mean) applies mean() to x separately within each group.

table(bp_clean$smoker)                             # group sizes
tapply(bp_clean$systolic, bp_clean$smoker, mean)   # mean systolic by group
fit2 <- lm(systolic ~ smoker, data = bp_clean)
coef(fit2)
confint(fit2)
Console output
No Yes 811 236 No Yes 126.3070 131.4534 (Intercept) smokerYes 126.307028 5.146361 2.5 % 97.5 % (Intercept) 125.082524 127.531532 smokerYes 2.567206 7.725517

For three or more groups, one-way ANOVA tests whether the group means differ. The example uses the three age bands created earlier.

# Three or more groups: one-way ANOVA across the age bands
tapply(bp_clean$systolic, bp_clean$age_c3, mean)
summary(aov(systolic ~ age_c3, data = bp_clean))
Console output
young middle older 115.5571 126.7930 136.8133 Df Sum Sq Mean Sq F value Pr(>F) age_c3 2 58901 29450 111.4 <2e-16 *** Residuals 1044 275972 264 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Reading the output. Mean systolic pressure is 126.3 mmHg among the 811 non-smokers and 131.5 mmHg among the 236 smokers. In the regression, the intercept (126.3) is the mean for the reference group, non-smokers, because No is the first level of smoker, and the coefficient smokerYes (5.15) is the difference between the two group means, with a 95% CI of 2.57 to 7.73 mmHg that excludes zero. In the ANOVA table, Df gives the degrees of freedom (number of groups minus 1, and number of people minus number of groups), Sum Sq and Mean Sq measure the variation between and within the groups, F value is the ratio of between-group to within-group variation, and Pr(>F) is the p-value. An F of 111.4 with p < 2e-16 indicates that mean systolic pressure differs across the age bands; ANOVA does not identify which bands differ, so the group means (115.6, 126.8, and 136.8 mmHg) are reported alongside it.

A model reporting sentence. “Mean systolic pressure was 131.5 mmHg among smokers and 126.3 mmHg among non-smokers, an unadjusted difference of 5.1 mmHg (95% CI 2.6 to 7.7). Mean systolic pressure also differed across age bands (one-way ANOVA, F = 111.4 on 2 and 1044 degrees of freedom, p < 0.001), rising from 115.6 mmHg in participants aged 35 or younger to 136.8 mmHg in those older than 55.”

R Worked Example: two categorical variables (smoking and hypertension)

A cross-tabulation counts the participants in every combination of two categorical variables, and row percentages make the groups comparable. prop.table(tab, margin = 1) divides each count by its row total.

tab <- table(smoker = bp_clean$smoker, htn = bp_clean$htn)
tab                                            # counts
round(100 * prop.table(tab, margin = 1), 1)    # row percentages
Console output
htn smoker normotensive hypertensive No 581 230 Yes 153 83 htn smoker normotensive hypertensive No 71.6 28.4 Yes 64.8 35.2

The chi-squared test asks whether the pattern of counts differs from what would be expected if the two variables were unrelated. The expected counts are worth printing, because the test is unreliable when any expected count is small.

chisq.test(tab)                                # chi-squared test
chisq.test(tab)$expected                       # expected counts if unrelated
Console output
Pearson's Chi-squared test with Yates' continuity correction data: tab X-squared = 3.7261, df = 1, p-value = 0.05357 htn smoker normotensive hypertensive No 568.5521 242.44795 Yes 165.4479 70.55205

Reading the output. Hypertension is more common among smokers (35.2%, 83 of 236) than among non-smokers (28.4%, 230 of 811). The chi-squared statistic is 3.73 on 1 degree of freedom with p = 0.054. R applies a small correction (Yates’ continuity correction) automatically for a table with two rows and two columns. A p-value of 0.054 indicates weak evidence against no association: the data are somewhat unusual under no association, and a difference at least this large would arise in about 5 of every 100 samples of this size if there were no association. The crude difference will also change once age, a likely confounder, is taken into account. All expected counts are far above 5, so the test is appropriate here; when any expected count falls below 5, Fisher’s exact test, fisher.test(tab), is used instead.

A model reporting sentence. “Hypertension was more common among smokers (35.2%) than among non-smokers (28.4%), although the evidence for an unadjusted association was weak (chi-squared test, p = 0.054).”

When evaluating unconditional associations, pay attention to:

  • Associations between predictors and outcome: Determine if there is any association at all; determine the functional form (is it linear?); get a simple picture of the strength and direction.
  • Associations between pairs of predictors: Look for potential collinearity problems (highly correlated predictors).
  • Confounding variables: Evaluate associations between the confounders identified in the causal diagram and the key predictors of interest and the outcome. These associations show how strongly each confounder could distort the crude comparison; the decision to adjust for a confounder rests on the diagram.
R Checking predictors for collinearity with a correlation matrix

A correlation matrix shows the correlation between every pair of continuous variables at once. cor() applied to several columns returns the matrix, and round(..., 2) keeps two decimal places.

round(cor(bp_clean[, c("age", "bmi", "systolic", "diastolic")]), 2)
Console output
age bmi systolic diastolic age 1.00 0.00 0.48 0.41 bmi 0.00 1.00 0.21 0.15 systolic 0.48 0.21 1.00 0.84 diastolic 0.41 0.15 0.84 1.00

Reading the output. Each cell is the correlation between the row variable and the column variable, and the diagonal is always 1. A common rule of thumb flags pairs with |r| above about 0.8 as possible collinearity problems. Systolic and diastolic pressure (r = 0.84) cross that line, which is expected because both measure blood pressure; a model would normally include one of them, or a combined measure, as a predictor. The correlation matrix helps detect collinearity, whereas the choice of confounders to include in a model comes from the causal diagram.

What to do next. The unconditional analyses feed directly into the plan for the multivariable model.

FindingWhat to do next
A confounder from the DAG shows little crude association with the outcome.The confounder stays in the planned model, because the decision to adjust rests on the causal diagram.
A scatterplot shows a curve.The analyst plans a squared term (such as age_ctsq) or a categorised version (such as age_c3) for that predictor.
Two predictors have |r| above about 0.8.The analyst treats them as candidates for collinearity and keeps the one that best matches the research question, or combines them.
A cross-tabulation has an expected count below 5.The analyst uses Fisher’s exact test or combines sparse categories before modelling.
An association is far stronger or weaker than subject-matter knowledge suggests.The analyst rechecks the coding and verification of both variables before interpreting it.

Keeping Track of Your Analyses

Before starting the more substantial analysis, set up a system for keeping track of your results:

Analyse in BlocksClick to explore
Keep Log FilesClick to explore
Label & DateClick to explore
Knowledge check: this section

1. Why should you evaluate unconditional associations before multivariable analyses?

Evaluating unconditional associations before multivariable analyses helps you understand the basic relationships in your data, identify collinearity, detect confounding, and determine the functional form of relationships, all of which inform the complex models you will subsequently build.

2. If a continuous outcome variable is far from normally distributed, what should you do?

If the continuous outcome is not approximately normally distributed, you should explore transformations which might normalise the distribution. It is ultimately the normality of the residuals that is important, but a far-from-normal variable with no strong predictors will produce non-normal residuals.

3. What is the appropriate analytical approach for evaluating the association between two categorical variables?

For associations between two categorical variables, cross-tabulation and the chi-squared (χ²) test are the appropriate analytical approaches. The cross-tabulation shows the count in every cell, which helps identify sparse or unexpected categories; the chi-squared test asks whether the pattern of counts differs from what would be expected if the two variables were unrelated.

Reflection

Consider a dataset with 15 predictor variables and one continuous outcome. Unconditional analyses examine each predictor on its own (its distribution, missing values, and its association with the outcome one predictor at a time) before any multivariable model is fitted, so that data problems and effect sizes are visible before adjustment hides them. Describe the sequence of unconditional analyses you would carry out before fitting any multivariable models. How would you handle a predictor that has 30% missing values?

Model answerFor 15 predictors and a continuous outcome, the unconditional analyses before multivariable modelling would be: (1) a summary of each predictor (mean, SD, range, number missing) and of the outcome (histogram); (2) the association of each predictor with the outcome, using a scatterplot and correlation for continuous predictors and group means with a one-way ANOVA or simple regression for categorical predictors, reporting each estimate with its 95% CI; (3) a correlation matrix of the predictors to detect collinearity; and (4) the associations of the DAG’s confounders with the main exposure and with the outcome. For the predictor with 30% missing values: (a) tabulate the missingness and compare people with and without the value on other variables, to judge whether the values are missing completely at random (MCAR), missing in a way related to observed variables (MAR), or missing in a way related to the missing values themselves (MNAR); (b) choose among the options in this section, which are to abandon the predictor, to run two analyses (one on the subset with the predictor and one on the full dataset without it), or to use multiple imputation, which fills each missing value several times from the other variables and is introduced in the next lesson; and (c) record the choice and the reason in the analysis log. Dropping the predictor is a legitimate choice when it is documented; dropping it silently, or replacing the missing values with the mean, hides the problem and can bias the result.
Reflection saved!
* Complete the quiz and reflection to continue.
Final Assessment

A Structured Approach to Data Analysis: Final Assessment

15 questions • 100% required to pass

Bringing It All Together

This lesson laid out a structured workflow for taking a dataset from collection to first analysis. The first section insisted that you start with a causal diagram and a clear separation of outcomes, predictors, confounders, and intervening variables, before you touch the data. The section on coding, entry, and file management turned that intent into discipline at the level of files: thoughtful coding, a project folder you could hand to a collaborator, and a codebook that captures every variable. The section on program files and verification made the workflow reproducible by moving you out of point-and-click interactive mode into program-mode scripts, with systematic editing and verification. The section on data processing closed the loop by processing outcomes and predictors, surfacing multilevel structure, and producing the unconditional associations that should always precede a multivariable model.

The thread running through all four sections is that an analysis is only as trustworthy as the steps that came before it. Every later lesson in this course, including linear regression, model building, logistic regression, count data, and mixed models, assumes you arrive at the modelling step with a clean, documented, and well-understood dataset. The structured approach is what makes the rest of the course possible. It also positions you to interpret p-values, confidence intervals, and effect sizes responsibly when you eventually report them (Wasserstein & Lazar, 2016; Greenland et al., 2016), and to avoid the analytic flexibility that drives false-positive results (Simmons, Nelson, & Simonsohn, 2011). A first plain-language reading of a p-value and a confidence interval accompanies the mediation example in the first section, and the worked examples of unconditional associations apply both; later lessons return to them with every model. The historical roots of exploratory data analysis trace to John Tukey.

Key Takeaways from this lesson

  • Always start with a causal diagram: it forces you to declare your outcome, predictors, confounders, and intervening variables before the data can mislead you.
  • Data analysis is iterative; expect to back up several steps as you learn more about your data.
  • Coding and file-management decisions made in the first hour shape every analysis you run afterward; treat them as part of the analysis, not pre-work.
  • Use program mode, not interactive clicks: a script you can re-run is the only honest record of what you did.
  • Verify the dataset (ranges, types, consistency) before you trust any descriptive or inferential output.
  • Run unconditional associations first; they reveal data problems and effect sizes that a multivariable model will hide.

Reflection

This lesson set out a structured approach to data analysis in four steps: (1) start with a causal diagram that separates outcomes, predictors, confounders, and intervening variables before touching the data; (2) plan data coding, file management, and a codebook so that the project could be handed to a collaborator; (3) work in program mode with scripts rather than interactive point-and-click processing, and edit and verify the data systematically; and (4) process the outcome and predictor variables, identify any multilevel structure, and examine unconditional associations before fitting a multivariable model. Which of these steps do you consider the most important, and why? How would you apply the structured approach to a dataset you are currently working with or plan to work with in the future?

Model answerThe most important step is the causal-question specification before any modelling: deciding what relationship is to be estimated (total, direct, or mediated effect), drawing the DAG that encodes the assumed structure, and identifying the adjustment set before looking at associations with the outcome. Everything downstream (variable cleaning, transformation, model choice, reporting) depends on this decision. Applied to a current dataset, the sequence would be to write the question as a single sentence, draw the DAG (by hand, with the dagitty package in R, or on the dagitty.net web page), compute the adjustment set with adjustmentSets(), and write the analysis plan down, with a date, before modelling begins. Verification and the unconditional analyses then proceed as described in this lesson; they examine the outcome’s distribution and the crude associations, and any change they prompt to the plan, such as a transformation or a merged category, is recorded in the log with its reason. Writing the plan down in advance, which some studies do formally by registering it publicly (pre-registration), is what separates confirmatory from exploratory analysis. Both are valuable, and labelling each clearly is what makes a causal claim defensible.
Reflection saved!

Final Knowledge Assessment

Final assessment: the 15 questions

1. What is the first step recommended before beginning any data analysis?

The first step is to construct a plausible causal diagram of the problem, identifying outcomes, predictors, confounders, and intervening variables.

2. Why should you avoid starting analyses in a spreadsheet?

Doing all analyses in the statistical program makes it easier to keep track of all analyses and simplifies tracking modifications to the data.

3. What coding value should NOT be assigned to missing data?

The specific number assigned to missing values must not be a legitimate value for any of the responses. Common conventions include large negative numbers like −999.

4. Why is a 2-digit numerical suffix recommended for file names?

A 2-digit suffix allows you to have 99 versions of a file that will sort correctly when listed alphabetically (e.g., brazil01, brazil02, ... brazil99).

5. What is the danger of using the “sort” command in a spreadsheet for data entry?

In spreadsheets, it is possible to sort individual columns independently, which can destroy your entire dataset with one inappropriate “sort” command by misaligning records across columns.

6. What is the primary purpose of evaluating unconditional associations between pairs of predictors?

Associations between pairs of predictors are evaluated to detect potential collinearity problems, where highly correlated predictors can cause instability in multivariable models.

7. When processing a categorical outcome with 3 categories, when might you recode it to 2 categories?

If you planned a multinomial regression with a 3-category outcome, but there are very few observations in 1 of the 3 categories, you might want to recode it to a 2-category variable.

8. What approach should be used to document what a program file does?

All statistical programs allow you to add comments to the program files. These should document what the program does and, in some cases, record key results within the file itself.

9. For verifying a continuous variable, what visual tool is recommended?

For continuous variables, preparing a histogram gives you an idea of the distribution and allows you to see if it looks reasonable before proceeding with further analysis.

10. What is the appropriate analysis for the association between one continuous and one categorical variable?

For the association between one continuous and one categorical variable, one-way ANOVA, simple linear regression, or logistic regression are appropriate analytical approaches.

11. If a predictor variable has many missing values, what options are available?

If many values are missing, you might abandon plans to use that predictor, or conduct 2 analyses: one on the subset in which the predictor is present and one on the full dataset ignoring the predictor.

12. What should you do with log files from your analyses?

Give log files the same name as the program file (except with a different extension) so that it is easy to match the program that generated a particular set of results.

13. Why is interactive mode still useful despite its limitations?

Interactive mode is very useful for exploring your data and trying out analyses. However, the “real” processing and analysis should be done in program mode for reproducibility.

14. When you evaluate the confounders identified in the causal diagram, which ones can distort the crude comparison the most?

A confounder can distort the crude comparison most when it is strongly associated with both the key predictor and the outcome, and these associations deserve special attention. The decision to adjust for a confounder rests on the causal diagram, which means that a confounder from the diagram stays in the planned model even when its crude associations are weak.

15. What does the chapter suggest you should do if a count/rate outcome’s mean and variance are not approximately equal?

If the mean and variance of a count/rate outcome are not approximately equal, Poisson regression assumptions may be violated. Consider negative binomial regression or alternative analytic approaches.

✦ Before submitting: pass every section knowledge check (100%) and complete every reflection.

Lesson Complete!

You have successfully completed A Structured Approach to Data Analysis. Your responses have been downloaded.

A later lesson, Data Cleaning and Descriptive Analyses, takes the next concrete step. With data collected and organized, the next job is detecting outliers, addressing missing values, and producing the descriptive summaries (Table 1) that every analysis report eventually needs. The structured workflow you built here makes that work tractable.