HSCI 410 · Lesson 2

Data Cleaning &
Descriptive Analyses

Exploratory Data Analysis For Epidemiology

Learning objectives for this lesson:

  • Identify common types of data errors and their sources in the data pipeline
  • Distinguish between missing data mechanisms (MCAR, MAR, MNAR) and their analytic implications
  • Apply systematic data cleaning strategies including range checks, outlier detection, and consistency verification
  • Select and implement appropriate methods for handling missing data
  • Calculate and interpret measures of central tendency, spread, and shape
  • Construct and interpret descriptive summaries appropriate for epidemiologic research
  • Assess distributional assumptions and select appropriate visualization techniques

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
Tidy Data A data layout where each variable is a column, each observation is a row, and each cell holds a single value. Coined by Hadley Wickham; the foundation of the tidyverse workflow.
Data Pipeline The full sequence of steps that takes raw data through cleaning, transformation, and analysis to a final result. Errors can enter at any stage; tracking them is essential.
Codebook / Data Dictionary Documentation that describes every variable in a dataset (name, type, allowable values, units, coding, source). Required for reproducible analysis.
Outlier An observation that lies far from the rest of the distribution. May reflect a genuine extreme value, a measurement error, or a data-entry mistake; investigate before removing.
Range Check Verifying that values for a variable fall within plausible biological or logical limits (e.g., human age between 0 and 120). A first-pass tool for spotting impossible entries.
Frequency Table A tabular summary showing the count (and often the percentage) of each level of a categorical variable. The simplest descriptive tool for nominal/ordinal data.
Missingness Patterns of missing values in a dataset. Classified by mechanism (MCAR/MAR/MNAR); the mechanism governs whether complete-case analysis or imputation is appropriate.
Type Conversion (Coercion) Changing a variable's data type (e.g., character to numeric, factor to integer). Mistyped variables are a common source of analysis errors in R.
Derived Variable A new variable computed from existing ones (e.g., BMI from height and weight, age categories from age). Document the formula in the data dictionary.
Methods & Statistical Concepts
Mean The arithmetic average. Sensitive to outliers; appropriate for symmetric continuous data.
Median The 50th percentile, the value that splits the distribution in half. Robust to outliers and skew; preferred for non-normal continuous data.
Standard Deviation (SD) A measure of spread equal to the square root of the variance. Reported alongside the mean for symmetric data.
Interquartile Range (IQR) The difference between the 75th and 25th percentiles (Q3 − Q1). A robust measure of spread; reported with the median.
Q-Q Plot A quantile-quantile plot comparing observed data to a theoretical distribution (often the normal). Points falling on the diagonal suggest the distribution fits.
Histogram A bar chart of binned counts that displays the empirical distribution of a continuous variable. Useful for spotting skew, modality, and outliers.
Box Plot A graphical summary showing the median, the IQR (box), whiskers that reach the most extreme values within 1.5×IQR of the box, and points beyond the whiskers. Compact for comparing groups.
Skewness Asymmetry of a distribution. Positive (right) skew has a long right tail; negative (left) skew has a long left tail. Influences choice of mean vs. median.
Multiple Imputation A principled method for handling missing data: generate multiple plausible values per missing cell, analyse each completed dataset, and pool results using Rubin's rules.
Odds and Odds Ratio Odds compare the number of people with a characteristic with the number without it. An odds ratio divides the odds in one group by the odds in another; a value of 1 means no association.
Bias A systematic difference between an estimate and the true value, so that repeated studies would be wrong in the same direction on average.
Precision and Standard Error Precision describes how much an estimate would vary from sample to sample. The standard error measures that variation; for a mean it is the SD divided by the square root of the sample size.
Confidence Interval A range around an estimate, roughly the estimate plus or minus two standard errors for a 95% interval, that shows the values compatible with the data.
Null Hypothesis and p-Value The null hypothesis is the starting assumption of no difference or no effect. The p-value is the probability of a result at least as extreme as the one observed if the null hypothesis were true.
Standardised Difference The difference between two groups divided by a pooled standard deviation, used in Table 1 to describe the size of an imbalance. Values below about 0.1 are usually read as negligible.
Confounder A variable related to both the exposure and the outcome that can distort the apparent association between them.
Complete-Case Analysis Analysing only the rows with no missing value on any variable used; also called listwise deletion. Defensible when little is missing and MCAR is plausible.
Parametric and Rank-Based Methods Parametric methods assume the data follow a particular distribution, usually the normal. Rank-based (non-parametric) methods work with the ranks of the values and make fewer assumptions.
Geometric Mean The back-transformed mean of logged values, exp(mean(log(x))); a typical value for right-skewed data such as biomarker concentrations.
Regular Expression A short text pattern, such as ^vanc for text that starts with vanc, used to find or replace variants in text fields.
Eigenvalue In factor analysis, the amount of shared variation among the items that one factor accounts for. Parallel analysis keeps factors whose eigenvalues exceed those from random data.
Factor Loading The correlation between an item and a factor. This lesson treats loadings of 0.40 or higher as meaningful.
psych (R package) An R package for survey and psychological measurement. This lesson uses its describe() function for detailed descriptive statistics and its alpha() and fa.parallel() functions for scale checks.
alpha() The psych function that calculates Cronbach's alpha for a set of items and shows what alpha would be if each item were dropped.
fa.parallel() The psych function for parallel analysis, which compares the eigenvalues of the real items with those of random data to suggest how many factors to keep.
factanal() The base R function for factor analysis. It reports each item's loading on each factor and needs rows without missing values.
mice (R package) An R package for multiple imputation by chained equations: mice() creates completed datasets, with() analyses each one, and pool() combines the results with Rubin's rules.
Key People
John Tukey (1915–2000) American statistician who coined the term “exploratory data analysis” (EDA) and invented the box plot, stem-and-leaf display, and many tools for visual data inspection.
Hadley Wickham (1979– ) New Zealand statistician and chief scientist at Posit (RStudio). Created ggplot2, dplyr, tidyr, and the broader tidyverse; defined “tidy data”.
No matching entries. Try a different search term.
Section 1

Data Quality & Types of Errors

⏱ Estimated time: 35 minutes
Lesson 2 · HSCI 410

Data Cleaning & Descriptive Analyses

Every regression coefficient in later lessons rests on data someone had to clean first.

The R activities

Code blocks: logic before syntax

Three R code blocks run through the full pipeline on a simulated survey dataset called phaa_survey.csv.

  • Block 1: read the raw file, fix variable types, and apply range checks.
  • Block 2: calculate descriptives and produce standard visualisations.
  • Block 3: assess internal consistency and build derived scale scores.

The narration describes the logic. The written lesson has the syntax, a primer on reading R code, and annotated output.

Section 1 of 3

Data Quality & Types of Errors

The error taxonomy, the data pipeline, and the three missing-data mechanisms that govern your analytic choices.

The pipeline

Where errors enter

Collection Entry Storage Analysis errors can enter at every stage

Data quality is multidimensional: accuracy, completeness, timeliness, and consistency can all break down at any point (Wang & Strong, 1996).

Error types

Five things that go wrong

Transcription

Digits transposed, decimal-point shifts during copying.

Coding

Incorrect code assignment; category labels reversed.

Logical inconsistency

Cross-field contradictions (e.g., diagnosis before birth).

Out-of-range

Values outside biological or logical plausibility limits.

Duplicates

The same observation entered more than once.

Van den Broeck et al. (2005): cleaning = screening → diagnosing → editing, planned into the study design.

Missing data

Three mechanisms (Rubin, 1976)

MCAR

Missingness unrelated to any data value, observed or not. Complete-case analysis is unbiased (but less precise).

MAR

Missingness depends on observed variables only. Multiple imputation produces valid estimates if the imputation model includes the variables that predict missingness.

MNAR

Missingness depends on the unobserved value itself. No standard method fully corrects this; sensitivity analyses are essential.

The mechanism cannot be verified from observed data alone. Always run sensitivity analyses.

Before you clean

Codebooks and variable types

Why a data dictionary?

Without it you cannot define what “out-of-range” means for each variable. It also makes cleaning reproducible.

Include: name, type, valid range, coding, and source for every variable.

Continuous Categorical Binary Count

Variable type determines which checks apply and which statistical operations are valid.

Carry forward

Three things to take into the next section

  • Errors enter at every stage: collection, entry, storage, and analysis all need checks.
  • Mechanism before method: decide which mechanism (MCAR, MAR or MNAR) is plausible, state it as an assumption, and then choose how to handle missingness.
  • No codebook, no cleaning: the dictionary defines what “valid” means for every variable.

Introduction and Overview

An earlier lesson set up the structured workflow that takes raw data from collection to analysis-ready files. This lesson zooms into two specific phases of that pipeline that deserve their own treatment: cleaning the data (detecting errors, handling missing values, deciding how to deal with outliers) and producing descriptive analyses (the numerical summaries and visualisations that every analysis report eventually needs). The three content sections walk through this in order: data quality and types of error (this section), the cleaning workflow itself (a later section), and descriptive statistics and Table 1 (a later section).

Learning Objectives

  • Trace where errors enter the collection → entry → storage → analysis pipeline.
  • Distinguish between common error types (random vs. systematic, structural vs. value-level).
  • Classify missing-data mechanisms (MCAR, MAR, MNAR) and explain why the distinction matters.
  • Describe variable types in epidemiologic data and the operations each type supports.

The Data Pipeline

Before any statistical analysis can begin, data must travel through a pipeline: collection → entry → storage → analysis. Errors can be introduced at every stage, and understanding where problems arise is the first step toward ensuring high-quality data for epidemiologic research.

Key Principle

The quality of your analysis can never exceed the quality of your data. Careful attention to data quality at each stage of the pipeline is a prerequisite for valid epidemiologic inference. Data quality has several dimensions. Wang and Strong (1996) asked data users what quality meant to them and found many dimensions beyond accuracy. Four of them matter at every cleaning step and appear in the walkthrough above: accuracy (the values are correct), completeness (the values are present), timeliness (the values are current enough for the question) and consistency (the values are recorded in the same format and agree with each other). A fifth, relevance, asks whether the data suit the research question at all.

At the collection stage, errors may arise from poorly worded survey items, interviewer bias, instrument malfunction, or participant misunderstanding. During data entry, transcription mistakes and coding errors are common. Storage introduces risks of data corruption, version control problems, and format incompatibilities. Finally, at the analysis stage, incorrect variable coding, inappropriate transformations, and software errors can compromise results.

Types of Data Errors

Errors in epidemiologic datasets take many forms. Van den Broeck, Argeseanu Cunningham, Eeckels, & Herbst (2005) describe data cleaning as a three-stage process, screening, diagnosing, and editing, that should be planned into study design rather than improvised after collection. Click each card below to learn about common error types.

Screening means looking systematically for values that seem odd, such as an age of 220 or an empty cell where an answer was expected. Diagnosing means deciding what each odd value is: an error, a true but unusual value, or a missing-data code. Editing means acting on the diagnosis by correcting the value from the source document, setting it to missing, or leaving it unchanged, and recording the decision. Planning these steps before data collection allows the study team to build some of the checks into the data-entry system itself.

Transcription ErrorsClick to explore
Coding ErrorsClick to explore
Logical InconsistenciesClick to explore
Out-of-Range ValuesClick to explore
Duplicate RecordsClick to explore

Two Further Ways to Classify Errors

The five error types above describe what an error looks like in the file. Two further distinctions describe how an error behaves, and both affect what the analyst can do about it.

Random versus systematic error. A random error is unpredictable in direction: a reader misjudges a blood pressure gauge by a few mmHg, sometimes upward and sometimes downward. Random errors add noise, which widens the spread of the data and makes estimates less precise. Their effect on a mean tends to cancel out, but random error in an exposure weakens estimated associations toward no effect, and random error in a measurement compared with a cut-off, such as 140 mmHg, changes the proportion above that cut-off. A systematic error pushes values in the same direction every time, as with a miscalibrated scale that adds 2 kg to every weight. Systematic errors do not cancel out, so they produce bias, meaning an estimate that is consistently too high or too low. Range checks catch some random errors, such as an impossible value, whereas a systematic error usually leaves values that look plausible and can be found only by comparison with an outside source, such as a recalibrated instrument.

Structural versus value-level error. A structural error affects the layout of the file: a variable stored as text when it should be a number, two variables merged into one column, a record that appears twice, or a missing-data code such as -9 mixed in with real values. A value-level error affects a single cell, such as one blood pressure entered as 1,200. Structural problems are usually fixed first, because value-level checks such as range checks give misleading answers when a column has the wrong type or contains codes.

Missing Data

Missing data are ubiquitous in epidemiologic research. The mechanism by which data become missing has profound implications for the validity of subsequent analyses. Rubin (1976) identified three missing data mechanisms:

MCARClick to explore
MARClick to explore
MNARClick to explore

Important Warning

The missing data mechanism is an assumption that cannot be verified from the observed data alone. You should always conduct sensitivity analyses to assess how your results might change under different assumptions about the missing data mechanism.

Data Dictionaries and Codebooks

A data dictionary (or codebook) is a structured document that describes every variable in a dataset: its name, label, type, valid range, coding scheme, and source. Maintaining a comprehensive data dictionary is essential for:

  • Ensuring consistency across data entry operators
  • Facilitating communication among research team members
  • Supporting reproducibility when analyses are revisited months or years later
  • Identifying and resolving coding discrepancies

Variable Types in Epidemiologic Data

Correctly classifying variables is fundamental to choosing appropriate cleaning strategies and analytic methods.

ContinuousClick to explore
CategoricalClick to explore
BinaryClick to explore
CountClick to explore

The Course Dataset and Its Codebook

The R activities in this lesson use phaa_survey.csv, a simulated cross-sectional survey of 800 adults that later lessons also use. The table below is a codebook excerpt for the variables this lesson works with most. A hard limit marks values that cannot be correct for this survey; values beyond it are treated as errors. A soft limit marks values that are possible but unusual; values between a soft and a hard limit are flagged for investigation, checked against the source where possible, and kept unless they are shown to be wrong. The limits are the rules applied in this lesson.

VariableType and unitsValid values (hard limits)Soft limits (flag and check)Missing code
idText identifierP0001 to P0800, each appearing onceNoneNever missing
ageContinuous; whole years18 to 100 (only adults were eligible)Above 90Blank cell
systolic_bpContinuous; mmHg, whole numbers80 to 220Below 90 or above 180Blank cell
bmiContinuous; kg/m², one decimal13 to 60Below 16 or above 50Blank cell
smokerBinary; text"Yes" (current smoker) or "No"NoneBlank cell
dep1 to dep7Ordinal questionnaire items1 (Never) to 5 (Always); a higher value means a more frequent depressive symptomNoneBlank cell
incomeOrdered categories; text"<25k", "25-50k", "50-75k", "75-100k", ">100k"NoneBlank cell

In this file a missing value is an empty cell, which R reads as NA (not available). Many real files instead store codes such as -9 (refused) or 999 (not asked); the codebook is where such codes are declared, and the next section shows how to convert them before any analysis. A full codebook would also record each variable's source, such as the questionnaire item or the measurement protocol; in this simulated file every variable comes from a single survey record per participant.

Getting Ready to Run R

Setup Before the First R Box

Each R box in this lesson can be run in RStudio or any R console. Three preparations are needed once, before the first box.

  1. Download phaa_survey.csv (the link appears in each R box) and save it in a folder for this course. R looks for files in its working directory, the folder it is currently pointed at. In RStudio the menu Session > Set Working Directory > Choose Directory points R at the folder, and getwd() prints the folder R is using. If read.csv() reports that it cannot open the file, the file is not in the working directory.
  2. Install the two add-on packages used in this lesson by running install.packages("psych") and install.packages("mice") once. A package is a bundle of extra functions; installing copies it to the computer, and library(psych) loads it at the start of each new R session.
  3. Open a new R script (File > New File > R Script), copy the code from each box into it in the order the boxes appear, and save the script in the same folder. The order matters, because later boxes use objects created by earlier ones.

A line of code runs when the cursor is on it and Ctrl+Enter (Cmd+Return on a Mac) is pressed, and the result appears in the Console pane. Selecting several lines and pressing the same keys runs them together, and the Source button runs the whole script from top to bottom. Running one line at a time allows each result to be compared with the annotated output on this page.

The code shown on this page is the starter code for every activity and can be copied directly. The answer-key script linked in each activity box contains the same code together with worked answers, and it becomes available after the responses to that box are saved.

Learn to do this in R

Narrated R walkthrough: Data Cleaning and Descriptive Statistics in R

This walkthrough applies the steps of this lesson to the Canadian Social Connection Survey, one line at a time. It keeps the rows and columns an analysis needs, explores each variable before changing it, recodes and checks every recode, and then describes the data with summary statistics, plots, stratified tables and a Table 1.

Open the Data Cleaning and Descriptive Statistics in R walkthrough

Reading R Code: A Short Primer

R code is a sequence of instructions. Most lines either create an object (a named result stored in memory) or ask R to display something. The symbols in the table below appear throughout this lesson.

Symbol or termExampleWhat it means
<-x <- c(4, 7, 9)The arrow stores the result on its right under the name on its left. The example creates an object called x that holds three numbers.
c()c("age", "bmi")The function c() combines values into a vector, an ordered list of values of the same type.
Functions and argumentsmean(x, na.rm = TRUE)A function is a named command followed by parentheses. The values inside the parentheses are its arguments, and name = value sets an option.
$phaa$ageThe dollar sign picks one column out of a data frame (R's name for a table of rows and columns). phaa$age is the age column of the data frame phaa.
[ ]phaa$age[3]
phaa[1:5, c("id", "age")]
Square brackets select elements. For a single column, [3] picks the third value. For a data frame, the part before the comma picks rows and the part after the comma picks columns, so the second example shows the ID and age of the first five people.
== < > <= >= !=phaa$age < 18Comparisons return TRUE or FALSE for each value. A double equals sign tests whether two values are equal, and != tests whether they differ.
| and &phaa$age < 18 | phaa$age > 100The vertical bar means "or" and the ampersand means "and". The example is TRUE for any age below 18 or above 100.
!!is.na(phaa$bmi)The exclamation mark turns TRUE into FALSE and FALSE into TRUE, so the example is TRUE wherever BMI is present.
NAphaa$bmi[phaa$bmi > 60] <- NANA is R's code for a missing value. The example replaces every BMI above 60 with a missing value.
na.rm = TRUEmean(phaa$age, na.rm = TRUE)Most summary functions return NA when any value is missing. The argument na.rm = TRUE (remove NAs) tells the function to leave missing values out.
~boxplot(systolic_bp ~ smoker, data = phaa)The tilde reads as "by" or "depending on". The example draws systolic blood pressure separately for each smoking group.
## a note for readersEverything after # on a line is a comment for human readers, and R ignores it.
library()library(psych)The function loads an installed package so that its functions can be used in the current session.

A First Look at the Raw File

R Worked code: a first look at phaa_survey.csv
File for this example phaa_survey.csvdataset

Screening starts by reading the file and comparing what arrived with the codebook. Four commands do most of this work: dim() reports the number of rows and columns, str() (structure) lists each variable with its type and first few values, summary() gives the minimum, quartiles, mean and maximum of numeric variables, and colSums(is.na()) counts the missing values in each column (is.na() marks each missing cell as TRUE and colSums() adds up the TRUEs in each column).

# Read the raw file. Blank cells and the text "NA" become missing values (NA).
phaa <- read.csv("phaa_survey.csv", stringsAsFactors = FALSE,
                 na.strings = c("", "NA"))

dim(phaa)                 # number of rows (people) and columns (variables)
str(phaa)                 # each variable's type and its first few values
summary(phaa[, c("age", "systolic_bp", "bmi", "dep1")])
table(phaa$smoker, useNA = "ifany")   # counts for a text (character) variable
colSums(is.na(phaa))      # how many values are missing in each column
Console output
> phaa <- read.csv("phaa_survey.csv", stringsAsFactors = FALSE, + na.strings = c("", "NA")) > dim(phaa) [1] 800 27 > str(phaa) 'data.frame': 800 obs. of 27 variables: $ id : chr "P0001" "P0002" "P0003" "P0004" ... $ age : int 49 46 40 55 56 43 49 39 59 64 ... $ gender : chr "Woman" "Man" "Woman" "Man" ... $ ethnicity : chr "White" "White" "East Asian" "White" ... $ education : chr "Some college" "High school" "Some college" "High school" ... $ income : chr "25-50k" "50-75k" "50-75k" "50-75k" ... $ region : chr "Surrey" "Other" "Surrey" "Richmond" ... $ smoker : chr "No" "No" "No" "No" ... $ phys_act_min : int 196 256 0 253 130 221 355 180 84 236 ... $ discrimination_score: int 7 3 4 5 0 8 3 12 4 5 ... $ social_support_score: int 30 19 32 20 32 27 31 19 31 22 ... ... (16 more lines not shown) > summary(phaa[, c("age", "systolic_bp", "bmi", "dep1")]) age systolic_bp bmi dep1 Min. : -3.00 Min. : 60.0 Min. :15.00 Min. :1 1st Qu.: 36.00 1st Qu.:100.0 1st Qu.:18.50 1st Qu.:2 Median : 45.00 Median :109.0 Median :20.70 Median :3 Mean : 45.31 Mean :108.4 Mean :20.86 Mean :3 3rd Qu.: 54.00 3rd Qu.:116.0 3rd Qu.:23.20 3rd Qu.:4 Max. :220.00 Max. :300.0 Max. :95.00 Max. :5 > table(phaa$smoker, useNA = "ifany") No Yes 664 136 > colSums(is.na(phaa)) id age gender 0 0 0 ethnicity education income 0 0 35 region smoker phys_act_min 0 0 28 discrimination_score social_support_score dep1 0 20 0 dep2 dep3 dep4 0 0 15 dep5 dep6 dep7 0 0 0 anx1 anx2 anx3 0 15 0 anx4 anx5 bmi 0 0 0 systolic_bp diastolic_bp hypertension 0 0 0
OutputWhat it showsWhat to do with it
dim(): 800 27The file has 800 rows, one per participant, and 27 columns, one per variable.The counts match the codebook. A different count would suggest an incomplete download or a different version of the file.
str(): chr, int, numThe label chr means text (character), int means whole numbers and num means numbers that can have decimals.Every variable has the type the codebook leads one to expect. A numeric variable listed as chr would signal text, such as "unknown", hiding in the column.
summary(): Min. and Max.Age runs from -3 to 220, systolic BP from 60 to 300 and BMI up to 95. The middle of each distribution (quartiles and median) looks ordinary.These five values break the hard limits in the codebook. The next section diagnoses them and sets them to missing.
table()Smoking status takes only the two codes in the codebook, "No" (664) and "Yes" (136), with no missing values.No recoding is needed. Variants such as "yes" or "Y" would appear here as extra categories.
colSums(is.na())Five variables have missing values: income (35), physical activity (28), social support (20), and the items dep4 (15) and anx2 (15).These counts are the starting point for the missing-data decisions in the next section.
Knowledge check: this section

1. A researcher discovers that a participant’s recorded date of diagnosis is earlier than their date of birth. What type of error is this?

This is a logical inconsistency because two fields within the same record contradict each other; a diagnosis cannot precede birth. While it may have been caused by a transcription error, the error type is classified by what is observed in the data.

2. Under which missing data mechanism can complete-case analysis still produce unbiased estimates?

Under MCAR, the probability of missingness is unrelated to any data values, so the observed cases are a random subset of the full sample. Complete-case analysis is unbiased but less precise due to reduced sample size.

3. Which of the following is NOT a primary purpose of a data dictionary?

A data dictionary documents variable names, labels, types, valid ranges, and coding schemes. It supports data quality and reproducibility but does not play a role in determining statistical significance.

● Complete the knowledge check to continue.

Section 2

Data Cleaning Strategies

⏱ Estimated time: 50 minutes
Section 2 of 3

Data Cleaning Strategies

Systematic verification, outlier triage, missing-data methods, transformations, and the audit trail that ties it all together.

Verification

Three types of systematic checks

Range checks

Hard limits (impossible) and soft limits (unusual but possible). Define from the codebook before you look at the data.

Consistency checks

Cross-field logic: discharge after admission, age matches date of birth, skip patterns respected.

Cross-validation

Compare across data sources. Establish a reliability hierarchy when sources disagree.

Outliers

Three ways to find them; one rule for all of them

IQR method
\[ \color{#0B7B6B}{\text{Low}} = \color{#C2410C}{Q_1} - 1.5 \times IQR \quad \color{#1D4ED8}{\text{High}} = \color{#BE185D}{Q_3} + 1.5 \times IQR \]
Q1 25th percentile Q3 75th percentile Low / High outlier fences
Z-score
\[ \color{#0B7B6B}{z} = \frac{\color{#C2410C}{x} - \color{#6D28D9}{\bar{x}}}{\color{#1D4ED8}{s}} \quad \text{flag if } |z| > 3 \]
z standardized score x observed value x̄ sample mean s standard deviation

Visual inspection (box plots, histograms, scatter plots) should accompany any quantitative rule.

The rule: investigate before you delete. An extreme value may be a genuine clinical finding or a data-entry shift of one decimal place.

Tukey (1977): look at data before you model it.

Missing data

Methods ranked by how well they handle uncertainty

Listwise deletion

Simple but biased unless MCAR. Large cumulative losses in multivariable models.

Pairwise deletion

Uses all available cases per calculation. Preserves data but results may not fit together coherently.

Single imputation

Mean / median / mode. Underestimates variance; standard errors too narrow.

Multiple imputation

Widely used under MAR (Sterne et al., 2009). Generates 5–20 plausible datasets; pools results with Rubin’s rules.

Transformations

Addressing skew without discarding data

TransformationFormulaWhen to use
Log (natural)\(\ln(x)\) or \(\ln(x+1)\)Right-skewed; multiplicative relationships
Square root\(\sqrt{x}\)Count data; moderate skew
Reciprocal\(1/x\)Strongly skewed rates and times

Back-transform all results for reporting. A coefficient from a log-transformed outcome is multiplicative, not additive.

Recoding & strings

New variables from old; order from messy text

Recoding

Collapse categories (current + former → ever smoker); band continuous variables (BMI groups); derive quantities (person-years, age at onset). Every recode goes in the dictionary.

String cleaning

“Vancouver”, “vancouver”, “VANCOOUVER” → one standard value. Consistent case, trimmed whitespace, pattern rules, lookup tables, fuzzy matching.

Documentation

The audit trail is non-negotiable

Every cleaning decision needs to record:

  • What changed (original value and replacement)
  • Why (biological implausibility, logical contradiction, etc.)
  • Who and when

Reproducibility means another analyst can rebuild the cleaned dataset from the raw file using only your documentation.

Do not edit values by hand in a spreadsheet. Write a script. The script is the audit trail.Course guidance, paraphrasing the reproducibility principle in Wilkinson et al., 1999

Introduction and Overview

An earlier section named the kinds of errors that can enter a dataset. This section provides the cleaning workflow that addresses them: systematic verification, outlier identification, missing-data handling, transformations, recoding, and string cleaning. Each step has its own techniques and its own decisions to document.

Learning Objectives

  • Run range, type, and consistency checks as a first systematic verification pass.
  • Identify outliers and decide whether to keep, transform, or exclude them, with reasons recorded.
  • Choose an appropriate missing-data strategy (complete-case, single imputation, multiple imputation) given the plausible mechanism and the amount missing.
  • Apply transformations and derive new variables without losing the audit trail back to the raw data.
  • Document every cleaning decision so that another analyst could reproduce your final dataset.

Systematic Verification

Data cleaning should follow a systematic, documented process. The goal is not to “fix” data arbitrarily but to identify and resolve genuine errors while preserving the integrity of valid observations.

Range Checks

Range checks verify that values fall within predefined valid or plausible bounds. For example, gestational age should be between approximately 20 and 44 weeks; human body temperature typically ranges from 35°C to 42°C.

Define both hard limits (biologically impossible values that should be flagged as errors) and soft limits (unusual but possible values that should be reviewed).

The two kinds of limit lead to different actions. A value beyond a hard limit is an error: it is corrected from the source document if possible and otherwise set to missing. A value between a soft limit and a hard limit is flagged for investigation: the analyst lists it, checks it against the source where possible, and keeps it unless it is shown to be wrong. The codebook excerpt in the previous section gives both kinds of limit for the course data.

Consistency Checks

Consistency checks examine whether values across multiple fields are logically compatible. Examples include verifying that discharge dates occur after admission dates, that a participant’s age is consistent with their reported date of birth, and that skip patterns in questionnaires are respected.

These checks often require domain knowledge about the relationships between variables.

Cross-Validation

Cross-validation involves comparing data from multiple sources to verify accuracy. For example, comparing self-reported medication use with pharmacy dispensing records, or verifying diagnoses against medical chart reviews.

When discrepancies arise, establishing a hierarchy of data source reliability is essential for deciding which value to retain.

Identifying Outliers

Outliers are observations that lie far from the bulk of the data. They may represent genuine extreme values, data errors, or observations from a different population. The visual tools used to spot them, including box plots, stem-and-leaf displays, and the broader practice of looking at data before modelling, trace back to Tukey (1977). Several methods are used to identify them:

The interquartile range (IQR) method defines outliers as values falling below Q1 − 1.5×IQR or above Q3 + 1.5×IQR, where Q1 and Q3 are the 25th and 75th percentiles, respectively.

IQR outlier bounds
\[ \color{#0B7B6B}{\text{Lower}} = \color{#C2410C}{Q_1} - 1.5 \times \color{#6D28D9}{IQR} \qquad \color{#1D4ED8}{\text{Upper}} = \color{#BE185D}{Q_3} + 1.5 \times \color{#6D28D9}{IQR} \]
An observation is an outlier if it falls below the lower bound (the 25th percentile minus 1.5 times the interquartile range) or above the upper bound (the 75th percentile plus 1.5 times that same range).

This method is robust to extreme values because it is based on percentiles rather than the mean and standard deviation. Values beyond 3×IQR from the quartiles are sometimes termed “extreme outliers.”

Worked Example: IQR Fences for Eleven Readings

Eleven systolic blood pressure readings (mmHg), sorted from lowest to highest, are 96, 102, 106, 110, 112, 115, 118, 121, 125, 131 and 184.

StepCalculationResult
Median: the middle (6th) value96, 102, 106, 110, 112, 115, 118, 121, 125, 131, 184115
Q1: halfway between the 3rd and 4th values(106 + 110) / 2108
Q3: halfway between the 8th and 9th values(121 + 125) / 2123
IQR123 − 10815
Lower fence108 − 1.5 × 15 = 108 − 22.585.5
Upper fence123 + 1.5 × 15 = 123 + 22.5145.5
Values outside the fences184 is above 145.5184 is flagged

The reading of 184 mmHg is flagged for investigation. It also lies beyond Q3 + 3 × IQR = 168, so some texts would call it an extreme outlier. A systolic pressure of 184 mmHg is high but physiologically possible, so the next step is to check the source record, and the value is kept if it is confirmed. Textbooks differ slightly in how they place Q1 and Q3 for some sample sizes; for these eleven values the method above gives the same quartiles as R's quantile() and boxplot() functions, as the R box later in this section shows.

The z-score method standardizes each observation as z = (x − mean) / SD. Values with |z| > 3 are commonly flagged as potential outliers.

Z-score
\[ \color{#0B7B6B}{z} = \frac{\color{#C2410C}{x} - \color{#6D28D9}{\bar{x}}}{\color{#1D4ED8}{s}} \]
The z-score is the distance of a value from the mean, in units of the standard deviation. Values with an absolute z-score above 3 are commonly flagged.

This approach assumes an approximately normal distribution and can be misleading for heavily skewed data because the mean and SD are themselves influenced by outliers (the masking effect).

Worked Example: A z-Score

The standard deviation (SD) measures the typical distance of values from their mean; a later section explains how it is calculated. Suppose systolic blood pressure in a population has a mean of 120 mmHg and an SD of 15 mmHg. A reading of 180 mmHg has z = (180 − 120) / 15 = 60 / 15 = 4, so it lies four SDs above the mean. The absolute value |z|, the size of z ignoring its sign, is 4, which is above the usual cut-off of 3, so the reading is flagged. A reading of 135 mmHg has z = (135 − 120) / 15 = 1, one SD above the mean, which is ordinary. In a bell-shaped (normal) distribution about 99.7% of values lie within three SDs of the mean, which is why |z| > 3 is the conventional flag. In R, scale() converts every value of a variable to its z-score in one step.

Visual inspection using boxplots, histograms, and scatterplots is often the most informative first step in outlier detection.

  • Boxplots display the median, IQR, and individual outliers beyond the whiskers
  • Histograms reveal gaps in the distribution that may indicate erroneous values
  • Scatterplots can reveal bivariate outliers that appear normal when each variable is examined individually

Visual methods should complement, not replace, quantitative rules. Always investigate outliers before deciding to exclude them.

Practical Tip

Never delete an outlier simply because it is extreme. Investigate whether it represents a data error, a genuinely unusual observation, or a member of a different subpopulation. Document every decision to retain or remove an outlier in your audit trail.

Box plot of systolic blood pressure with the IQR outlier fences marked; five points fall beyond the fences and are flagged as outliers.
The box spans the interquartile range; the dashed lines are the lower and upper fences (Q1 − 1.5×IQR and Q3 + 1.5×IQR). Points beyond them, shown in orange, are flagged for investigation, not automatic deletion.

What the Visual Checks Reveal

Two kinds of problem are easier to see in a plot than in a table of numbers. A histogram (a bar chart of how many values fall in each range) shows gaps and separate clusters. When a decimal point is shifted during data entry, the shifted values form a small cluster at ten times the typical value, far from the main distribution, as in panel B below.

Two histograms of simulated systolic blood pressure. Panel A shows one continuous cluster from about 75 to 170 mmHg. Panel B shows the same data with eight readings multiplied by ten, which appear as a small separate cluster between 1,200 and 1,450 mmHg, far to the right of the main bar.
Simulated data. A: a single continuous cluster, as expected for blood pressure. B: eight readings with a shifted decimal point (for example 1,280 entered for 128) form a separate cluster near 1,200 mmHg. The y-axis in panel B is cut at 25 so that the small cluster is visible.

A scatter plot shows each person as a point with two measurements. A bivariate outlier is a combination of values that is implausible even though each value is ordinary on its own. In the simulated example below, a height of 191 cm and a weight of 46 kg each lie inside the boxplot whiskers, but together they imply a BMI of 12.6, which would be life-threatening. Only the scatter plot reveals the problem.

Panel A shows separate boxplots of height and weight; the highlighted person, at 191 cm and 46 kg, lies inside the whiskers of both. Panel B is a scatter plot of weight against height in which most points rise from lower left to upper right, and the highlighted point sits far below the cloud on the right.
Simulated data. A: each value of the highlighted person lies within the boxplot whiskers. B: the combination of 191 cm and 46 kg (BMI 12.6) sits far below the cloud of other people and is flagged for checking.

Outlier Rules in R

R Worked code: outlier rules and soft limits in R
File for this example phaa_survey.csvdataset

The code below repeats the eleven-reading example, then applies the IQR rule and z-scores to BMI in the raw course file, and finally counts the values that fall in the soft-limit zones from the codebook. It continues from the first-look box in the previous section, so phaa still holds the raw data. quantile(x, probs = c(0.25, 0.75)) returns Q1 and Q3, IQR() returns their difference, boxplot.stats(x)$out lists the values beyond the fences that a boxplot would draw as points, and which() returns the row numbers where a condition is TRUE, so that the IDs can be listed.

# Worked example: the eleven readings from the text
x <- c(96, 102, 106, 110, 112, 115, 118, 121, 125, 131, 184)
quantile(x, probs = c(0.25, 0.75))     # Q1 and Q3
IQR(x)                                  # Q3 minus Q1
boxplot.stats(x)$out                    # values beyond the fences

# The same steps on BMI in the course file (before any cleaning)
q     <- quantile(phaa$bmi, probs = c(0.25, 0.75), na.rm = TRUE)
iqr   <- IQR(phaa$bmi, na.rm = TRUE)
lower <- q[1] - 1.5 * iqr
upper <- q[2] + 1.5 * iqr
c(lower, upper)
flag <- which(phaa$bmi < lower | phaa$bmi > upper)
length(flag)                            # how many rows are flagged
phaa[flag, c("id", "bmi")]              # who they are, and their values

# z-scores: scale() computes (x - mean) / SD for every value
z <- as.numeric(scale(phaa$bmi))
phaa$id[which(abs(z) > 3)]              # IDs with |z| greater than 3
round(z[which(phaa$bmi == 31.6)], 2)   # z for the BMI of 31.6

# Masking: repeat without the impossible BMI of 95
bmi_ok <- phaa$bmi[phaa$bmi < 60]
round(c(mean = mean(phaa$bmi), sd = sd(phaa$bmi)), 2)
round(c(mean = mean(bmi_ok), sd = sd(bmi_ok)), 2)
round((31.6 - mean(bmi_ok)) / sd(bmi_ok), 2)

# Soft limits: count values that deserve a second look (nothing is changed)
sum(phaa$systolic_bp < 90 | phaa$systolic_bp > 180)
sum(phaa$bmi < 16 | phaa$bmi > 50)
Console output
> x <- c(96, 102, 106, 110, 112, 115, 118, 121, 125, 131, 184) > quantile(x, probs = c(0.25, 0.75)) 25% 75% 108 123 > IQR(x) [1] 15 > boxplot.stats(x)$out [1] 184 > q <- quantile(phaa$bmi, probs = c(0.25, 0.75), na.rm = TRUE) > iqr <- IQR(phaa$bmi, na.rm = TRUE) > lower <- q[1] - 1.5 * iqr > upper <- q[2] + 1.5 * iqr > c(lower, upper) 25% 75% 11.45 30.25 > flag <- which(phaa$bmi < lower | phaa$bmi > upper) > length(flag) [1] 5 > phaa[flag, c("id", "bmi")] id bmi 78 P0078 95.0 151 P0151 31.6 228 P0228 30.3 320 P0320 30.6 542 P0542 30.8 > z <- as.numeric(scale(phaa$bmi)) > phaa$id[which(abs(z) > 3)] [1] "P0078" > round(z[which(phaa$bmi == 31.6)], 2) [1] 2.58 > bmi_ok <- phaa$bmi[phaa$bmi < 60] > round(c(mean = mean(phaa$bmi), sd = sd(phaa$bmi)), 2) mean sd 20.86 4.17 > round(c(mean = mean(bmi_ok), sd = sd(bmi_ok)), 2) mean sd 20.77 3.24 > round((31.6 - mean(bmi_ok)) / sd(bmi_ok), 2) [1] 3.34 > sum(phaa$systolic_bp < 90 | phaa$systolic_bp > 180) [1] 48 > sum(phaa$bmi < 16 | phaa$bmi > 50) [1] 67
OutputWhat it shows
Eleven readingsR reproduces the hand calculation: Q1 = 108, Q3 = 123, IQR = 15, and only 184 lies beyond the fences.
BMI fences 11.45 and 30.25Five BMI values are flagged. One (95.0, ID P0078) is impossible; the other four (30.3 to 31.6) are ordinary values for adults with obesity. The IQR rule finds what is unusual in this sample, and it cannot tell an error from a real value, so the four plausible values are kept and the 95.0 is handled by the hard limit in the activity below.
z-scoresOnly P0078 has |z| > 3. The BMI of 31.6 has z = 2.58 because the single value of 95 pulls the mean up and inflates the SD from 3.24 to 4.17. With the impossible value removed, the same reading has z = 3.34, and the readings of 30.6 and 30.8 also exceed 3 (z = 3.03 and 3.09). One extreme error hid three unusual values from the z rule, which is called masking.
Soft-limit counts48 systolic readings lie below 90 or above 180 mmHg. Two of them (60 and 300) are beyond the hard limits; the other 46 lie between 85 and 89 mmHg, which is low but possible, so they are flagged and kept. Likewise 67 BMI values lie below 16 or above 50; one is the impossible 95, and the other 66 lie between 15.0 and 15.9 and are kept.

These lines only display results and leave phaa unchanged; the activity box below reads the raw file again and makes the replacements.

Range Checks in Practice

The activity below applies the hard limits from the codebook to the course file. It is the editing stage of the screening, diagnosing and editing sequence: the values were screened in the first-look box, diagnosed as impossible in the outlier box, and are now replaced, with each change listed first.

R Activity: read, fix classes, and clean impossible values

This block uses the course dataset phaa_survey.csv, the simulated survey of 800 adults introduced in the previous section; later lessons use the same participants. It reads the raw file again, so it can be run on its own. Step 3a prints how many values break each hard limit from the codebook and lists their IDs before step 3b replaces them, which leaves a record of exactly what changed. Values in the soft-limit zones (counted in the previous box) are left as they are. All the code needed is shown here; the answer-key script linked above contains the same code with worked answers and becomes available after the responses are saved.

# 1. Read the raw file. Treat blank cells as missing (na.strings).
phaa <- read.csv(file = "phaa_survey.csv",
                 stringsAsFactors = FALSE,
                 na.strings = c("", "NA"))
summary(phaa$age)                 # before the conversion in step 2

# 2. Make sure age and bmi are numeric (in a real export they often arrive
#    as character because a cell holds text such as "unknown"; here every
#    cell is a number, so no NAs are created at this step).
phaa$age <- as.numeric(phaa$age)
phaa$bmi <- as.numeric(phaa$bmi)
summary(phaa$age)                 # after the conversion

# 3a. Count and list the rows that break each hard limit BEFORE changing them
sum(phaa$age < 18 | phaa$age > 100, na.rm = TRUE)
phaa$id[which(phaa$age < 18 | phaa$age > 100)]
sum(phaa$systolic_bp < 80 | phaa$systolic_bp > 220, na.rm = TRUE)
phaa$id[which(phaa$systolic_bp < 80 | phaa$systolic_bp > 220)]
sum(phaa$bmi < 13 | phaa$bmi > 60, na.rm = TRUE)
phaa$id[which(phaa$bmi < 13 | phaa$bmi > 60)]

# 3b. Range checks: replace the impossible values with NA
phaa$age[phaa$age < 18 | phaa$age > 100] <- NA
phaa$systolic_bp[phaa$systolic_bp < 80 | phaa$systolic_bp > 220] <- NA
phaa$bmi[phaa$bmi < 13 | phaa$bmi > 60] <- NA
summary(phaa$age)                 # after the range check

# 4. Put the education levels in order (lowest to highest) so that
#    comparisons such as <= are meaningful
phaa$education <- factor(phaa$education,
  levels = c("Less than high school", "High school", "Some college",
             "Bachelor's", "Graduate degree"),
  ordered = TRUE)
levels(phaa$education)

Why never overwrite the raw file? Range-check rules will change as you learn more about the data. Keeping phaa_survey.csv untouched lets you replay the cleaning from a known starting state.

R Reflect on what you just ran

Use the questions below to interpret the output you produced. Look at your console / plot before answering.

1. Look at summary(phaa$age) before and after the line phaa$age <- as.numeric(phaa$age). How many NAs appear, and what does that count tell you about the raw data quality?

Model answerIn this file every age cell holds a number (the two impossible entries are -3 and 220), so read.csv() already stores age as integer and summary() reports the quartiles before and after as.numeric() with 0 NAs both times; the conversion changes nothing here. The NAs appear at step 3, where the 18 to 100 range check turns those two impossible ages into NA (summary(phaa$age) then shows 2 NAs). Had the raw file contained text such as "unknown" or "30s", the column would have arrived as character and as.numeric() would have produced one NA per unparseable cell (with a warning). Either way the NA count is a direct measure of data-quality flaws in the raw entry: each NA is a value the original data collection accepted but that cannot be analysed quantitatively.

2. After the range-check on systolic_bp, how many rows were set to NA? Why is replacing impossible values with NA preferable to deleting those rows entirely?

Model answerThe 80 to 220 mmHg range check flags 2 rows (P0017 with 300 mmHg and P0245 with 60 mmHg) and sets them to NA. Replacing with NA is preferable to deletion because (a) it preserves the row for the other 26 variables (you don't lose two whole observations to delete one bad blood pressure); (b) it documents in the data itself that this value was flagged; (c) the missingness then enters multiple-imputation pipelines like any other NA, allowing principled handling. Deleting rows loses information silently and can introduce selection bias if implausible values cluster non-randomly.

3. After making education an ordered factor, what does levels(phaa$education) show? Why does the ordering matter for any analysis that asks "as education goes up, does X change?"

Model answerlevels(phaa$education) shows the five levels in the specified order: "Less than high school" < "High school" < "Some college" < "Bachelor's" < "Graduate degree". Ordering matters because an ordered factor lets R interpret "higher education" as a meaningful direction in regression and tests, so linear contrasts become interpretable, and later analyses that look for a trend across education levels treat the categories in the right order. Without the ordering R defaults to alphabetical, which makes "Bachelor's" < "Graduate degree" < "High school" < "Less than high school" < "Some college", which is nonsensical for inferential purposes.
Saved.

Handling Missing Data

The choice of method for handling missing data depends on the assumed missing data mechanism and the proportion of missingness. Practical guidance on implementing multiple imputation via chained equations, including how to specify the imputation model, how many imputations to draw, and how to diagnose problems, is given by White, Royston, & Wood (2011).

Five Terms Used in This Section

Complete-case analysis (also called listwise deletion) analyses only the rows that have no missing value on any variable used in the analysis.

Bias is a systematic difference between an estimate and the true value, so that repeating the study many times would give answers that are too high, or too low, on average.

Precision describes how much an estimate would vary from sample to sample; a precise estimate changes little when the study is repeated.

The standard error (SE) measures that sample-to-sample variation. For a mean it equals the standard deviation divided by the square root of the number of observations, so it shrinks as the sample grows.

A confidence interval (CI) is a range around an estimate, roughly the estimate plus or minus two standard errors for a 95% interval, that shows the values compatible with the data.

Missing data can damage both properties. Losing rows lowers precision (larger standard errors and wider confidence intervals), and missingness that is related to the values being studied, either directly (MNAR) or through observed variables linked to them (MAR), can bias a complete-case analysis.

Listwise Deletion (Complete-Case Analysis)

Excludes any observation with one or more missing values. Simple to implement, but can dramatically reduce sample size and introduces bias unless data are MCAR. In multivariable models with many variables, even low rates of missingness on individual variables can lead to large cumulative losses.

Pairwise Deletion (Available-Case Analysis)

Uses all available data for each analysis. For example, when computing a correlation matrix, each pair of variables uses all cases with non-missing values for that pair. This preserves more data than listwise deletion, but can produce inconsistent results. For example, correlations computed on different subsets of people can form a set that could not all have come from one group of people (technically, a correlation matrix that is not positive semi-definite), which some later methods cannot use.

Single Imputation (Mean, Median, Mode)

Mean imputation replaces missing values with the variable mean. While simple, it underestimates variance and distorts relationships between variables. Median imputation is more robust for skewed distributions. Mode imputation is used for categorical variables.

All single imputation methods treat the imputed value as if it were known with certainty, leading to underestimated standard errors and overly narrow confidence intervals.

Multiple Imputation (Introduction)

Multiple imputation creates several (typically 5–20) plausible imputed datasets, analyzes each separately, and pools the results using Rubin’s rules. This approach correctly accounts for the uncertainty due to missing data and produces valid inference under the MAR assumption.

Rubin’s rules average the estimates from the completed datasets and combine two sources of variation in the standard error: the ordinary variation within each dataset and the variation between datasets, which reflects how uncertain the imputed values are.

Multiple imputation is widely used for handling missing data in epidemiologic research when the proportion of missingness is non-trivial. It needs care: the imputation model should include the outcome and the variables that predict missingness (Sterne et al., 2009).

Missing Data in R: How Much, and Who Is Missing?

R Worked code: counting and comparing missing values
File for this example phaa_survey.csvdataset

The lines below continue from the activity above, so phaa now holds the data after the range checks. complete.cases() returns TRUE for each row with no missing value, na.omit() returns a copy of the data with every incomplete row removed, and tapply(y, group, mean) calculates the mean of y separately for each value of group.

# How much is missing, and where? (after the range checks above)
n_miss <- colSums(is.na(phaa))
n_miss[n_miss > 0]                       # only the columns with gaps
round(100 * n_miss[n_miss > 0] / nrow(phaa), 1)   # the same, as percentages
sum(complete.cases(phaa))                # rows with no gap in any column
nrow(na.omit(phaa))                      # na.omit() keeps only those rows

# Do people with and without a recorded income differ on what was observed?
inc_miss <- is.na(phaa$income)           # TRUE = income missing
table(inc_miss)
round(tapply(phaa$age, inc_miss, mean, na.rm = TRUE), 1)
round(tapply(phaa$systolic_bp, inc_miss, mean, na.rm = TRUE), 1)
round(prop.table(table(inc_miss, phaa$smoker), margin = 1) * 100, 1)
Console output
> n_miss <- colSums(is.na(phaa)) > n_miss[n_miss > 0] age income phys_act_min 2 35 28 social_support_score dep4 anx2 20 15 15 bmi systolic_bp 1 2 > round(100 * n_miss[n_miss > 0] / nrow(phaa), 1) age income phys_act_min 0.2 4.4 3.5 social_support_score dep4 anx2 2.5 1.9 1.9 bmi systolic_bp 0.1 0.2 > sum(complete.cases(phaa)) [1] 687 > nrow(na.omit(phaa)) [1] 687 > inc_miss <- is.na(phaa$income) > table(inc_miss) inc_miss FALSE TRUE 765 35 > round(tapply(phaa$age, inc_miss, mean, na.rm = TRUE), 1) FALSE TRUE 45.3 42.6 > round(tapply(phaa$systolic_bp, inc_miss, mean, na.rm = TRUE), 1) FALSE TRUE 108.4 105.3 > round(prop.table(table(inc_miss, phaa$smoker), margin = 1) * 100, 1) inc_miss No Yes FALSE 82.6 17.4 TRUE 91.4 8.6
OutputWhat it shows
Missing values by columnEight columns now have gaps: the five from the raw file plus age, BMI and systolic BP, which gained NAs from the range checks. The largest share is income, at 4.4%.
complete.cases() and na.omit()Only 687 of 800 rows (85.9%) are complete. No variable is missing more than 4.4% of its values, yet a complete-case analysis that used every column would lose 113 people (14%), because the gaps fall on different people. An analysis that uses only a few variables loses far fewer, so na.omit() should be applied only to the columns an analysis needs, for example na.omit(phaa[, c("age", "systolic_bp")]).
Comparison by income missingnessThe 35 people without an income value are a little younger (42.6 versus 45.3 years), have slightly lower systolic BP (105.3 versus 108.4 mmHg) and smoke less often (8.6% versus 17.4%, which is 3 of the 35 people). Differences like these, if real, would argue against MCAR and suggest that missingness depends on observed variables (MAR).

Because the course file is simulated, its true mechanism is known: the income values were deleted completely at random. The differences in the comparison therefore arose by chance in a group of only 35 people. Comparisons of this kind can suggest a mechanism, and small groups can produce sizeable differences by chance alone, so they cannot establish one.

Reasoning About the Mechanism

The observed data can show that missingness is related to observed variables, which is evidence against MCAR. They cannot show whether missingness depends on the missing values themselves, because those values were never recorded. Whether MNAR is plausible is therefore judged from subject knowledge. Income is the standard example: refusal to answer income questions is often reported to be more common among people with high incomes, so an analyst should treat MNAR as a real possibility for income even when a comparison like the one above looks reassuring. By contrast, a blood pressure reading lost because a machine failed at random times is a case where MCAR is plausible.

Because the mechanism cannot be confirmed from the data, it is stated as an assumption in the methods section of a report. An example sentence is: "Income was missing for 35 of 800 participants (4.4%). We assumed that income was missing at random given age, gender, education and region, and handled it with multiple imputation; a sensitivity analysis that assumed non-responders had higher incomes than similar responders gave similar results." The sentence reports the amount missing, the assumed mechanism, the method used, and how the conclusion was checked.

What Little’s MCAR Test Can and Cannot Show

Little’s test (Little, 1988) compares the means of the observed variables across groups of people with different patterns of missing values. A small p-value (a result that would be unlikely if the data were MCAR) is evidence that the data are not MCAR. A large p-value is weak reassurance: the test has little power (ability to detect a departure from MCAR) when few values are missing, and because it uses only observed values it cannot detect MNAR at all. The test is available in add-on R packages (for example as mcar_test() in the naniar package) and is not needed in this course; the group comparison above examines the same question in a form that is easier to read.

Worked Example: Missing High Earners Bias the Mean

Ten people have annual incomes (in thousands of dollars) of 22, 28, 31, 35, 40, 46, 52, 60, 85 and 120. Their total is 519, so the true mean is 519 / 10 = 51.9.

ScenarioCalculation from the eight observed valuesMeanError
Two people chosen at random (31 and 52) do not answer(519 − 31 − 52) / 8 = 436 / 854.5+2.6
The two highest earners (85 and 120) do not answer(519 − 85 − 120) / 8 = 314 / 839.25−12.65

When the missing people are chosen at random (MCAR), the error can go in either direction and averages out to zero over many repetitions; the only cost is a smaller sample. When the highest values are the ones that go missing (MNAR), every complete-case mean is pulled downward, and collecting more data of the same kind does not remove the error. The simulation below repeats the comparison with 10,000 simulated incomes.

R Worked code: a simulation of MCAR and MNAR missingness

The code simulates 10,000 incomes with a long right tail. set.seed() fixes the random numbers so that the results repeat exactly, rlnorm() draws right-skewed values, and runif(10000) < 0.30 is TRUE for a random 30% of people.

set.seed(410)
income <- round(rlnorm(10000, meanlog = log(55), sdlog = 0.6))  # simulated, $1000s
mean(income)                            # the true mean in this population

# MCAR: everyone has the same 30% chance of leaving the question blank
inc_mcar <- income
inc_mcar[runif(10000) < 0.30] <- NA
mean(inc_mcar, na.rm = TRUE)

# MNAR: the top quarter of earners leave it blank far more often
p_miss <- ifelse(income > quantile(income, 0.75), 0.70, 0.15)
inc_mnar <- income
inc_mnar[runif(10000) < p_miss] <- NA
mean(inc_mnar, na.rm = TRUE)

# Standard error of a mean = SD / square root of n (smaller = more precise)
sd(income) / sqrt(10000)
sd(inc_mcar, na.rm = TRUE) / sqrt(sum(!is.na(inc_mcar)))
Console output
> set.seed(410) > income <- round(rlnorm(10000, meanlog = log(55), sdlog = 0.6)) > mean(income) [1] 65.8979 > inc_mcar <- income > inc_mcar[runif(10000) < 0.30] <- NA > mean(inc_mcar, na.rm = TRUE) [1] 65.34256 > p_miss <- ifelse(income > quantile(income, 0.75), 0.70, 0.15) > inc_mnar <- income > inc_mnar[runif(10000) < p_miss] <- NA > mean(inc_mnar, na.rm = TRUE) [1] 54.36383 > sd(income) / sqrt(10000) [1] 0.437095 > sd(inc_mcar, na.rm = TRUE) / sqrt(sum(!is.na(inc_mcar))) [1] 0.509361

The true mean is 65.9. With MCAR missingness the complete-case mean, 65.3, is close to the truth, and the standard error rises from 0.437 to 0.509 because fewer values remain: no bias, lower precision. With MNAR missingness the complete-case mean is 54.4, about 11.5 thousand dollars too low, and no method that uses only the observed incomes can recover the missing information without further assumptions.

How Much Missing Data Is Too Much?

No single threshold applies to every study. As a rough guide, many applied texts suggest that when fewer than about 5% of the rows needed for an analysis have missing values and MCAR is plausible, complete-case analysis is unlikely to change the conclusions much. When more is missing, when missingness is related to observed variables, or when MNAR is plausible, multiple imputation under a stated MAR assumption, together with a sensitivity analysis, is the safer choice. The proportion that matters is the share of rows lost from the specific analysis, which can be much larger than the share missing on any one variable, as the 687 complete rows above show.

This guide also explains where complete-case analysis fits in this lesson. It is defensible when little is missing and MCAR is plausible, which is the situation in the scale activity in the next section, where na.omit() drops the 15 people (about 2%) who skipped one depression item before a factor analysis. It becomes hard to defend when many rows are lost or when the people with missing values differ from the rest.

Missing-Data Codes Such as -9 and 999

Many data systems cannot leave a cell empty, so they store a code such as -9 (refused) or 999 (not asked) instead. R treats these codes as real numbers. A single age of 999 among the 800 ages in the course file would raise the mean by about 1.2 years, and in a small sample, as in the example below, one code can move a mean by decades. The codebook lists the codes for each variable, and the analyst converts them to NA before any summary is calculated. The course file uses empty cells only, so the example below uses a short made-up vector. The operator %in% asks, for each value on its left, whether it appears in the list on its right.

R Worked code: converting missing-data codes to NA
age_raw <- c(34, -9, 51, 999, 47)    # -9 = refused, 999 = not asked
mean(age_raw)                        # wrong: the codes are averaged as if they were ages
age_raw[age_raw %in% c(-9, 999)] <- NA
age_raw
mean(age_raw, na.rm = TRUE)          # na.rm = TRUE leaves the NAs out
# The codes can also be converted while reading a file:
# mydata <- read.csv("mydata.csv", na.strings = c("", "NA", "-9", "999"))
Console output
> age_raw <- c(34, -9, 51, 999, 47) > mean(age_raw) [1] 224.4 > age_raw[age_raw %in% c(-9, 999)] <- NA > age_raw [1] 34 NA 51 NA 47 > mean(age_raw, na.rm = TRUE) [1] 44

The first mean, 224.4, is meaningless because the codes were averaged as if they were ages; after the conversion the mean of the three real ages is 44. The na.strings argument converts codes while the file is read, but it applies to every column, so it is safe only when the code cannot be a real value in any column. A value of 999 could be a real number of minutes of activity per week, for example. Converting codes column by column with %in%, guided by the codebook, avoids that problem.

Going further: multiple imputation in R with the mice package

The mice package (multivariate imputation by chained equations) fills each missing value with a draw from a model built from the other variables, repeats this to create several completed datasets, analyses each dataset, and pools the results with Rubin’s rules. The minimal example below imputes the 28 missing physical-activity values using age, gender, smoking status, BMI and social support, and estimates the mean weekly activity. It continues from the boxes above, so phaa holds the cleaned data.

R Worked code: a minimal multiple imputation
File for this example phaa_survey.csvdataset
library(mice)
mi_vars <- phaa[, c("age", "gender", "smoker", "bmi", "phys_act_min",
                    "social_support_score")]
mi_vars$gender <- factor(mi_vars$gender)
mi_vars$smoker <- factor(mi_vars$smoker)
imp <- mice(mi_vars, m = 5, seed = 2026, printFlag = FALSE)  # 5 completed datasets
fit <- with(imp, lm(phys_act_min ~ 1))   # lm(y ~ 1) estimates the mean of y
summary(pool(fit), conf.int = TRUE)      # pooled with Rubin's rules

# Complete-case estimate and its standard error, for comparison
mean(phaa$phys_act_min, na.rm = TRUE)
sd(phaa$phys_act_min, na.rm = TRUE) / sqrt(sum(!is.na(phaa$phys_act_min)))
Console output
> summary(pool(fit), conf.int = TRUE) term estimate std.error statistic df p.value 1 (Intercept) 175.732 2.938265 59.80808 748.7907 2.121669e-287 2.5 % 97.5 % conf.low conf.high 1 169.9638 181.5002 169.9638 181.5002 > mean(phaa$phys_act_min, na.rm = TRUE) [1] 176.0518 > sd(phaa$phys_act_min, na.rm = TRUE) / sqrt(sum(!is.na(phaa$phys_act_min))) [1] 2.964489
Code or outputWhat it means
mice(mi_vars, m = 5, seed = 2026, printFlag = FALSE)The function creates five completed datasets; seed makes the random draws repeatable and printFlag = FALSE hides the progress messages. When the package loads, R prints messages about functions it masks, which can be ignored.
with(imp, lm(phys_act_min ~ 1))The same analysis is run on each completed dataset. lm(y ~ 1) is a regression with no predictors, whose single coefficient (the intercept) is the mean of y; the next lesson introduces regression fully.
estimate, std.error, 2.5 % and 97.5 %The pooled mean is 175.7 minutes per week with a standard error of 2.94 and a 95% confidence interval from 170.0 to 181.5. These are the numbers to report.
Complete-case mean and standard errorThe complete-case mean is 176.1 (SE 2.96). The two answers agree closely, as expected, because the values in this simulated file were removed completely at random. With MAR missingness in real data the two can differ noticeably, and the imputed estimate is the one that corrects the bias when the MAR assumption holds.

Data Transformations

When continuous variables are heavily skewed, transformations can make the distribution more symmetric, stabilize variance, and improve the validity of parametric methods.

Parametric methods, such as the t-test and linear regression, assume that the data (or the errors of a model) follow a particular distribution, usually the bell-shaped normal distribution, which is fully described by its mean and standard deviation. When that assumption is badly wrong, their standard errors and confidence intervals can be misleading. Rank-based (non-parametric) methods, described in a later section, make fewer assumptions.

Two histograms: a right-skewed biomarker distribution on the left, and the same data after a natural-log transform on the right, now approximately symmetric.
A natural-log transform pulls a right-skewed variable toward symmetry, which often improves the fit of methods that assume approximate normality. Interpretation then shifts to the multiplicative scale.
TransformationFormulaWhen to Use
Log (natural)ln(x) or ln(x + 1)Right-skewed data, multiplicative relationships (e.g., biomarker concentrations, income)
Square root√xCount data, moderately right-skewed distributions
Inverse (reciprocal)1/xStrongly right-skewed data; commonly used for rates and times

The log of zero does not exist (R returns -Inf, minus infinity), so a variable that contains zeros, such as the number of clinic visits, cannot be logged directly. The usual remedy is to add 1 to every value first, ln(x + 1), which turns 0 into ln(1) = 0 and keeps all values in the same order. The constant 1 is a convention; when most values are small, the choice of constant affects the results, and the report should state it. R has a dedicated function, log1p(), for ln(x + 1), and expm1() reverses it.

The reciprocal, 1/x, reverses the order of values: the largest value becomes the smallest. Analysts often use −1/x instead, so that larger original values still have larger transformed values and the direction of any association stays easy to read.

Worked Example: Back-Transforming a Log Mean

Nine C-reactive protein (CRP) values, a blood marker of inflammation, are 0.4, 0.8, 1.1, 1.5, 2.3, 3.0, 4.8, 9.5 and 21.0 mg/L. Their arithmetic mean is 4.93 mg/L, pulled upward by the single value of 21.0, and their median is 2.3 mg/L. The mean of the natural logs is 0.906. Back-transforming with the exponential function gives exp(0.906) = 2.48 mg/L. This back-transformed value is the geometric mean, a typical value on the original scale that is much less affected by the long right tail. A report would give the geometric mean of 2.48 mg/L, because the log-scale figure of 0.906 has no direct meaning for most readers. The same logic applies to regression coefficients for a log-transformed outcome, which a later lesson back-transforms into percentage differences.

R Worked code: log(), log1p(), exp() and the reciprocal
crp <- c(0.4, 0.8, 1.1, 1.5, 2.3, 3.0, 4.8, 9.5, 21.0)  # a right-skewed biomarker (mg/L)
mean(crp)                  # arithmetic mean, pulled up by the 21.0
median(crp)
log_crp <- log(crp)        # natural log of each value
round(log_crp, 2)
mean(log_crp)              # the mean on the log scale
exp(mean(log_crp))         # back-transformed: the geometric mean, in mg/L

visits <- c(0, 0, 1, 2, 5, 12)
log(visits)                # log(0) is -Inf, which cannot be analysed
log1p(visits)              # log1p(x) = log(x + 1), and log1p(0) = 0
expm1(log1p(visits))       # expm1() undoes log1p()

1 / c(2, 5, 10)            # the reciprocal reverses the order
-1 / c(2, 5, 10)           # a minus sign restores it
Console output
> crp <- c(0.4, 0.8, 1.1, 1.5, 2.3, 3.0, 4.8, 9.5, 21.0) > mean(crp) [1] 4.933333 > median(crp) [1] 2.3 > log_crp <- log(crp) > round(log_crp, 2) [1] -0.92 -0.22 0.10 0.41 0.83 1.10 1.57 2.25 3.04 > mean(log_crp) [1] 0.9063658 > exp(mean(log_crp)) [1] 2.47531 > visits <- c(0, 0, 1, 2, 5, 12) > log(visits) [1] -Inf -Inf 0.0000000 0.6931472 1.6094379 2.4849066 > log1p(visits) [1] 0.0000000 0.0000000 0.6931472 1.0986123 1.7917595 2.5649494 > expm1(log1p(visits)) [1] 0 0 1 2 5 12 > 1 / c(2, 5, 10) [1] 0.5 0.2 0.1 > -1 / c(2, 5, 10) [1] -0.5 -0.2 -0.1

log() is the natural log, exp() reverses it, log1p() and expm1() are the matching pair for ln(x + 1), and the last two lines show that the reciprocal reverses the order of 2, 5 and 10 while −1/x restores it.

Remember

When you transform a variable, all subsequent interpretations must account for the transformation. For example, a regression coefficient from a log-transformed outcome represents a multiplicative (rather than additive) change. Always back-transform results for reporting.

Recoding and Derived Variables

Recoding involves creating new variables from existing ones. Common examples include collapsing categories (e.g., combining “current smoker” and “former smoker” into “ever smoker”), categorizing continuous variables into clinically meaningful groups (e.g., BMI categories), and computing derived variables such as person-years of follow-up or age at onset.

In R, ifelse(condition, value_if_true, value_if_false) creates a new variable from a condition, and cut() turns a continuous variable into bands. In cut(), the breaks argument gives the cut-points, right = FALSE places each cut-point in the band above it (so a BMI of exactly 25.0 falls in the band 25 to 29.9), and labels names the bands. A cross-tabulation of the old and new variables, as in the box below, is the standard check that a recode did what was intended.

String Cleaning and Standardization

Free-text fields often contain inconsistencies: “Vancouver,” “vancouver,” “VANCOUVER,” and “Vancoouver” may all refer to the same city. Standardization strategies include converting to consistent case, trimming whitespace, applying regular expressions for pattern matching, and using lookup tables or fuzzy matching algorithms.

A regular expression is a short pattern that describes text. The pattern ^vanc means "text that starts with vanc" (the symbol ^ marks the start), and [0-9]+ means "one or more digits". The function grepl() asks whether a pattern occurs in each value, and gsub() replaces it. A lookup table is a two-column list that maps every known variant to its standard spelling. Fuzzy matching finds strings that differ by only a letter or two, which helps when the variants cannot all be listed in advance; its suggested matches should be checked by a person. The function duplicated() returns TRUE for the second and later copies of a value, so sum(duplicated(phaa$id)) counts repeated IDs.

R Worked code: cleaning text, recoding, and checking for duplicates
city <- c("Vancouver", "vancouver ", "VANCOUVER", "Vancoouver", " Burnaby")
city <- trimws(tolower(city))    # remove stray spaces, then make lower case
city
city[grepl("^vanc", city)] <- "vancouver"   # regular expression: starts with "vanc"
table(city)

# Recoding with ifelse() and cut()
smoker01 <- ifelse(phaa$smoker == "Yes", 1, 0)   # 1 = smoker, 0 = non-smoker
table(phaa$smoker, smoker01)
bmi_group <- cut(phaa$bmi, breaks = c(0, 18.5, 25, 30, Inf), right = FALSE,
                 labels = c("Under 18.5", "18.5 to 24.9", "25 to 29.9", "30 or more"))
table(bmi_group, useNA = "ifany")

# Duplicates: is any ID present more than once?
sum(duplicated(phaa$id))
Console output
> city <- c("Vancouver", "vancouver ", "VANCOUVER", "Vancoouver", " Burnaby") > city <- trimws(tolower(city)) > city [1] "vancouver" "vancouver" "vancouver" "vancoouver" "burnaby" > city[grepl("^vanc", city)] <- "vancouver" > table(city) city burnaby vancouver 1 4 > smoker01 <- ifelse(phaa$smoker == "Yes", 1, 0) > table(phaa$smoker, smoker01) smoker01 0 1 No 664 0 Yes 0 136 > bmi_group <- cut(phaa$bmi, breaks = c(0, 18.5, 25, 30, Inf), right = FALSE, + labels = c("Under 18.5", "18.5 to 24.9", "25 to 29.9", "30 or more")) > table(bmi_group, useNA = "ifany") bmi_group Under 18.5 18.5 to 24.9 25 to 29.9 30 or more <NA> 199 521 75 4 1 > sum(duplicated(phaa$id)) [1] 0
OutputWhat it shows
trimws(tolower(city))Making the text lower case and removing stray spaces merges three of the four spellings of Vancouver. The misspelling "vancoouver" remains.
grepl("^vanc", city)The pattern catches the misspelling, and the table now shows four values of "vancouver" and one of "burnaby". A pattern this broad would also catch any other place name starting with "vanc", so the table should be checked after every replacement.
table(phaa$smoker, smoker01)All 664 "No" values became 0 and all 136 "Yes" values became 1, so the recode worked.
table(bmi_group, useNA = "ifany")The bands hold 199, 521, 75 and 4 people, and one person has no band because that BMI was set to missing by the range check. useNA = "ifany" makes the missing value visible in the table.
sum(duplicated(phaa$id))The result is 0, so no ID appears twice. Records entered twice under different IDs can be found with sum(duplicated(phaa[, names(phaa) != "id"])), which compares whole rows after setting the ID column aside and flags a record only when every other value matches.

These examples store their results in separate objects (city, smoker01, bmi_group) instead of adding columns to phaa, so the cleaned file saved at the end of the lesson keeps the same columns as the course version used in later lessons.

Documenting the Cleaning Process

Every data cleaning decision should be recorded in an audit trail. This includes what was changed, why, by whom, and when. Reproducibility requires that another analyst could replicate the cleaning process from the original raw data using your documentation, an expectation echoed in the Statistical Methods in Psychology Journals guidelines (Wilkinson & the Task Force on Statistical Inference, 1999).

In practice the audit trail has two parts. The cleaning script records every change as code, with a comment giving the reason, and it is always run from the untouched raw file. A cleaning log, a small table with one row per changed value, records the date, the analyst, the ID, the variable, the old and new values and the reason, in a form that a reviewer can read without running any code. The cleaned data are then saved under a new file name so that the raw file is never overwritten; in this lesson that happens in the last line of the scale activity in the next section, which writes phaa_survey_clean.csv.

R Worked code: the header of a cleaning script and a cleaning log
# ---------------------------------------------------------------
# clean_phaa.R   Cleaning script for phaa_survey.csv
# Raw file: phaa_survey.csv (never edited)
# Output:   phaa_survey_clean.csv (written at the end of the script)
# ---------------------------------------------------------------
cleaning_log <- data.frame(
  date     = "2026-09-30",
  analyst  = "AB",
  id       = c("P0033", "P0612", "P0017", "P0245", "P0078"),
  variable = c("age", "age", "systolic_bp", "systolic_bp", "bmi"),
  old      = c(-3, 220, 300, 60, 95),
  new      = NA,
  reason   = c("below 18: impossible", "above 100: impossible",
               "above 220: implausible", "below 80: implausible",
               "above 60: implausible")
)
cleaning_log
write.csv(cleaning_log, "cleaning_log.csv", row.names = FALSE)
Console output
> cleaning_log date analyst id variable old new reason 1 2026-09-30 AB P0033 age -3 NA below 18: impossible 2 2026-09-30 AB P0612 age 220 NA above 100: impossible 3 2026-09-30 AB P0017 systolic_bp 300 NA above 220: implausible 4 2026-09-30 AB P0245 systolic_bp 60 NA below 80: implausible 5 2026-09-30 AB P0078 bmi 95 NA above 60: implausible

Each row of the log matches one value changed in the activity above. Saving the log with write.csv() creates cleaning_log.csv in the working directory, where it sits beside the raw file, the script and, later, the cleaned file.

Applied Example: Audit Trail

A cohort study of cardiovascular disease collected blood pressure measurements on 2,400 participants. During cleaning, 15 systolic values exceeded 300 mmHg. The analyst traced 12 of these to a decimal-point shift (e.g., 1,200 instead of 120.0) and corrected them using the original paper forms. Three could not be verified and were set to missing with a note: “Original form illegible; value implausible.” All decisions were logged in a cleaning script with date stamps.

Reflection

Think about a dataset you have worked with (or imagine a large epidemiologic study). What data cleaning challenges would you expect, and how would you prioritize your cleaning steps? What would your audit trail include?

Model answerFor a large epidemiologic dataset (e.g., a multi-site cohort), expected challenges: heterogeneous data entry across sites, free-text encoded in numeric fields, inconsistent missing-data codes (-9, 999, blank), dates entered in different formats at different sites (03/04/2026 can mean 3 April or 4 March), and text fields typed differently at each site. Priority order: (1) inventory and codebook the raw data; (2) range and consistency checks per variable; (3) cross-variable plausibility (impossible combinations); (4) standardise dates, units, and missing codes; (5) de-duplication; (6) derive analytic variables; (7) document. Audit trail: every step logged in a versioned script, intermediate datasets saved with version numbers, decision log (e.g., "impossible BMI > 60 set to NA on 2026-05-16, n = 12 rows affected"), and the final cleaned dataset saved under a new name and accompanied by a short summary of the checks and an updated codebook.
Reflection saved.
Knowledge check: this section

1. What is the primary disadvantage of mean imputation for handling missing data?

Mean imputation replaces missing values with the sample mean, which artificially reduces variability in the data. It also attenuates correlations between variables because the imputed values carry no information about their relationship with other variables.

2. Which outlier detection method is most robust for skewed distributions?

The IQR method is based on percentiles (Q1 and Q3), which are resistant to extreme values. The z-score method relies on the mean and standard deviation, both of which are influenced by outliers and skewness, making it less robust for non-normal distributions.

3. A log transformation is most appropriate for which type of distribution?

The natural log transformation compresses the right tail and stretches the left, making it ideal for right-skewed distributions commonly seen with biomarker concentrations, income, and other variables with multiplicative processes.

● Complete the reflection and knowledge check to continue.

Section 3

Descriptive Analyses

⏱ Estimated time: 55 minutes
Section 3 of 3

Descriptive Analyses

Measures of central tendency, spread, and shape; visualisations; scale scores; Table 1; normality assessment.

Central tendency

Three measures; one rule for choosing

Arithmetic mean
\[ \color{#0B7B6B}{\bar{x}} = \frac{1}{n} \sum_{i=1}^{n} \color{#1D4ED8}{x_i} \]
x̄ mean x_i each observed value n number of observations

Mean

Uses all data; sensitive to skew and outliers. Report with SD for normal data.

Median

Resistant to extremes. Report with IQR for skewed data or when outliers are present.

Mode is appropriate for nominal categorical data where neither mean nor median is meaningful.

Spread and shape

How far and how symmetric

Sample standard deviation
\[ \color{#0B7B6B}{s} = \sqrt{\frac{\sum_{i=1}^{n}(\color{#1D4ED8}{x_i} - \color{#6D28D9}{\bar{x}})^2}{\color{#C2410C}{n}-1}} \quad \color{#047857}{\text{IQR}} = Q_3 - Q_1 \]
s standard deviation x_i each value x̄ mean n sample size IQR interquartile range
Right-skewed median mean Symmetric (normal) mean = median
Categorical data

Cross-tabulations and the odds ratio

Odds ratio from a 2×2 table
\[ \color{#0B7B6B}{OR} = \frac{\color{#C2410C}{a}\,\color{#6D28D9}{d}}{\color{#1D4ED8}{b}\,\color{#BE185D}{c}} \]
OR odds ratio a exposed cases d unexposed non-cases b exposed non-cases c unexposed cases
CasesControls
Exposed45 (a)30 (b)
Unexposed15 (c)60 (d)

OR = (45 × 60) / (30 × 15) = 6.0. A strong association, visible from the descriptive table alone.

Visualisation

Match the plot to the question

Histograms

Distribution of a continuous variable: shape, modality, gaps, outliers.

Boxplots

Median + IQR + outlier points. Best for comparing groups side by side.

Bar charts

Frequencies for categorical variables. Not for continuous data.

Scatter / line

Continuous relationships and trends over time.

Building scales

From questionnaire items to a score

Internal consistency

Cronbach's α: ≥ 0.70 acceptable, ≥ 0.80 good. The seven depression items in the course data reach α = 0.904.

Dimensionality

Exploratory factor analysis + parallel analysis. Loadings ≥ ~0.40 contribute meaningfully; depression and anxiety items load on separate factors.

Both checks pass → combine the items into a derived score (dep_score) and carry it into later lessons.

Table 1

The publication-ready summary of your cohort

CharacteristicExposedUnexposedp
Age, mean ± SD52.3 ± 11.449.8 ± 12.10.02
Female, n (%)110 (55%)162 (54%)0.82
BMI, median (IQR)27.1 (24.0–31.5)25.8 (23.2–29.4)0.004

Normal variables: mean ± SD. Skewed variables: median (IQR). Categorical: n (%). Missing-data counts belong in the table too.

Normality

Is it close enough to normal?

Visual methods

Q-Q plot: points along the diagonal suggest normality; systematic departures signal skew or heavy tails. Histogram with normal overlay for a quick read.

Formal tests

Shapiro-Wilk (preferred for n < 5,000); Kolmogorov-Smirnov as an alternative. In very large samples they flag trivial departures, so pair them with a plot.

Approximately normal → t-tests, ANOVA, Pearson. Otherwise → transform, or use rank-based tests (Wilcoxon rank-sum, Kruskal-Wallis, Spearman).

Carry forward

What this lesson set up

  • Choose central tendency and spread based on variable type and distributional shape.
  • A well-stratified Table 1 is often the most informative single output of an analysis.
  • Normality assessment should combine visual tools (Q-Q plot) with formal tests; neither alone is sufficient.
  • The clean, described dataset is the foundation on which every regression in later lessons rests.

Introduction and Overview

Earlier sections produced a clean, well-documented dataset. This section turns that dataset into the descriptive summaries that any analysis report opens with: means and medians, spreads and shapes, and frequency distributions, “Table 1” characteristics by exposure group, and the visualisations that go with them. Doing this carefully is what lets readers understand what your data look like before they trust your inferential models.

Learning Objectives

  • Choose the appropriate measure of central tendency, spread, and shape for each variable type and distribution.
  • Build frequency distributions and cross-tabulations to summarise categorical data.
  • Select visualisations that match the variable type and the question you are asking.
  • Construct a publication-ready “Table 1” of participant characteristics, including stratified comparisons.
  • Assess normality and decide when a transformation or non-parametric approach is warranted.

Measures of Central Tendency

Measures of central tendency summarize the “typical” value in a distribution. The choice depends on the variable type and distributional shape.

The arithmetic mean (x̄) is the sum of all values divided by the number of observations. It uses all data points and is the basis of many parametric statistical methods. Parametric methods are those that assume the data follow a particular distribution, usually the bell-shaped normal distribution, which is fully described by its mean and standard deviation.

Arithmetic mean
\[ \color{#0B7B6B}{\bar{x}} = \frac{1}{\color{#C2410C}{n}} \sum_{i=1}^{\color{#C2410C}{n}} \color{#1D4ED8}{x_i} \]
The mean is the sum of all observed values divided by the number of observations.

When to use: Symmetric or approximately normal distributions. Avoid for heavily skewed data or when outliers are present, as the mean is pulled toward extreme values.

The median is the middle value when observations are ordered from smallest to largest. For an even number of observations, it is the average of the two middle values.

When to use: Skewed distributions, ordinal data, or when outliers are present. The median is robust to extreme values; unlike the mean, adding or removing an outlier has minimal effect on the median.

The mode is the most frequently occurring value. A distribution may be unimodal, bimodal, or multimodal.

When to use: Categorical (nominal) data, where neither the mean nor the median is meaningful. It is also useful for identifying common response values in continuous data (e.g., digit preference).

Worked Example: An Extreme Income Moves the Mean

Five people earn 30, 35, 40, 45 and 50 thousand dollars a year. The mean is (30 + 35 + 40 + 45 + 50) / 5 = 200 / 5 = 40, and the median, the middle value, is also 40. A sixth person who earns 500 thousand dollars joins the group. The mean becomes 700 / 6 = 116.7, far above what five of the six people earn, while the median moves only to 42.5, halfway between 40 and 45, the two middle values of the six. The median still describes a typical member of the group, whereas the mean now lies far from five of the six incomes. This is why skewed variables such as income are summarised with the median.

Measures of Spread

Spread (or dispersion) describes how much variability exists in the data around the central value.

MeasureFormula / DefinitionProperties
RangeMaximum − MinimumSensitive to outliers; uses only 2 data points
Variance (s²)Σ(xi − x̄)² / (n − 1)Average squared deviation; units are squared
Standard Deviation (s)√s²Same units as original variable; most commonly reported
IQRQ3 − Q1Robust to outliers; covers middle 50% of data

The variance divides by n − 1 instead of n. The deviations are measured from the sample mean, which sits in the middle of the sample by construction, so they are slightly smaller on average than deviations from the true population mean would be. Dividing by n − 1, a slightly smaller number, corrects for this. The standard deviation is the square root of the variance, which brings the value back from squared units (squared mmHg, for instance) to the original units so that it can be reported beside the mean.

Worked Example: A Standard Deviation Step by Step

Five systolic readings are 110, 116, 120, 124 and 130 mmHg. Their mean is 600 / 5 = 120 mmHg.

Value xDeviation x − 120Squared deviation
110−10100
116−416
12000
124416
13010100
Sum0232

The deviations always sum to zero, which is why they are squared before being averaged. The variance is 232 / (5 − 1) = 58 squared mmHg, and the standard deviation is √58 = 7.62 mmHg: a typical reading lies about 7.6 mmHg from the mean. The R box below repeats both worked examples.

R Worked code: mean, median and standard deviation
income5 <- c(30, 35, 40, 45, 50)        # five incomes, $1000s per year
mean(income5)
median(income5)
income6 <- c(income5, 500)              # add one very high earner
mean(income6)
median(income6)

# Standard deviation, step by step
sbp <- c(110, 116, 120, 124, 130)
dev <- sbp - mean(sbp)                  # distance of each value from the mean
dev
dev^2                                   # squared distances
sum(dev^2) / (length(sbp) - 1)          # the variance
sqrt(sum(dev^2) / (length(sbp) - 1))    # the standard deviation
sd(sbp)                                 # R's built-in function gives the same
Console output
> income5 <- c(30, 35, 40, 45, 50) > mean(income5) [1] 40 > median(income5) [1] 40 > income6 <- c(income5, 500) > mean(income6) [1] 116.6667 > median(income6) [1] 42.5 > sbp <- c(110, 116, 120, 124, 130) > dev <- sbp - mean(sbp) > dev [1] -10 -4 0 4 10 > dev^2 [1] 100 16 0 16 100 > sum(dev^2) / (length(sbp) - 1) [1] 58 > sqrt(sum(dev^2) / (length(sbp) - 1)) [1] 7.615773 > sd(sbp) [1] 7.615773

Reporting Convention

For normally distributed variables, report mean ± SD. For skewed distributions, report median (IQR) or median (Q1, Q3). This convention is standard in epidemiologic publications and should be followed in “Table 1”.

Measures of Shape

Beyond central tendency and spread, the shape of a distribution provides critical information for choosing analytical methods.

Skewness

Skewness measures the asymmetry of a distribution. A skewness of 0 indicates perfect symmetry.

  • Positive (right) skew: Long right tail; mean > median. Common in variables like income, hospital length of stay, and biomarker concentrations.
  • Negative (left) skew: Long left tail; mean < median. Less common but seen in variables like age at death in developed countries.

As a rule of thumb, |skewness| > 1 indicates substantial skew, though this depends on sample size.

Kurtosis

Kurtosis describes the “tailedness” of a distribution relative to a normal distribution (which has a kurtosis of 3, or excess kurtosis of 0).

  • Leptokurtic (excess kurtosis > 0): Heavier tails and a sharper peak; more extreme values than expected under normality.
  • Platykurtic (excess kurtosis < 0): Lighter tails and a flatter peak; fewer extreme values than expected.

High kurtosis can indicate the presence of outliers or a mixture of subpopulations.

The describe() function in the psych package, used below, reports excess kurtosis. A value near 0 therefore means tails like those of a normal distribution, a positive value means heavier tails, and a negative value means lighter tails.

Four histograms of simulated data. A, symmetric, has skew 0.12 and excess kurtosis 0.08, with the mean and median lines on top of each other. B, right-skewed, has skew 1.21 and excess kurtosis 1.85, with a long right tail and the mean to the right of the median. C, left-skewed, has skew -1.60 and excess kurtosis 4.28, with a long left tail and the mean to the left of the median. D, heavy-tailed, has skew -0.19 and excess kurtosis 2.05, with a narrow peak and long tails on both sides.
Simulated data (2,000 values per panel). Skew and excess kurtosis were calculated with the same functions that psych::describe() uses. The solid red line is the mean and the dashed line the median: they coincide for symmetric shapes, and the mean is pulled toward the long tail for skewed ones. Panel D has almost no skew but positive excess kurtosis, because both tails are longer than a normal curve would produce.

Reading summary() and describe() Output

Two functions produce most numerical summaries in this course. Base R's summary() gives the minimum, first quartile, median, mean, third quartile and maximum, plus a count of missing values when there are any. The describe() function from the psych package gives a longer row for each variable, including the standard deviation, skew and excess kurtosis. The box below applies both to three variables that the activity below does not use.

R Worked code: annotated summary() and describe() output
File for this example phaa_survey.csvdataset
summary(phaa$diastolic_bp)
library(psych)
describe(phaa[, c("diastolic_bp", "discrimination_score", "social_support_score")])
Console output
> summary(phaa$diastolic_bp) Min. 1st Qu. Median Mean 3rd Qu. Max. 46.00 64.00 70.00 70.15 76.00 99.00 > library(psych) > describe(phaa[, c("diastolic_bp", "discrimination_score", "social_support_score")]) vars n mean sd median trimmed mad min max diastolic_bp 1 800 70.15 9.12 70 70.12 8.90 46 99 discrimination_score 2 800 7.02 4.03 7 6.99 4.45 0 23 social_support_score 3 780 25.92 6.01 26 25.93 5.93 5 40 range skew kurtosis se diastolic_bp 53 0.07 0.05 0.32 discrimination_score 23 0.13 -0.44 0.14 social_support_score 35 -0.04 -0.29 0.22
ColumnWhat it means
Min., 1st Qu., Median, Mean, 3rd Qu., Max.For diastolic BP the middle half of readings lies between 64 and 76 mmHg (IQR = 12), and the mean (70.15) and median (70) almost coincide, a sign of a symmetric distribution. A seventh column, NA's, appears when values are missing.
nThe number of non-missing values. Social support has 780, so 20 values are missing.
mean, sd, medianThe centre and spread that are reported in a results table.
trimmed, madtrimmed is the mean after dropping the lowest and highest 10% of values, and mad (median absolute deviation) is the median distance of the values from their median, multiplied by 1.4826 so that it equals the SD for normally distributed data. When they are close to the mean and SD, extreme values have little influence.
skew, kurtosisSkewness and excess kurtosis. All three variables have skew between −0.04 and 0.13 and kurtosis between −0.44 and 0.05, so none is noticeably skewed or heavy-tailed.
seThe standard error of the mean, the SD divided by the square root of n. It describes the precision of the mean; the SD is the measure of spread between people.

What to report: because the three variables are close to symmetric, each is summarised as mean (SD): diastolic BP 70.2 (9.1) mmHg, discrimination score 7.0 (4.0) and social support score 25.9 (6.0) with 20 values missing. If |skew| were above about 1, or the histogram showed a long tail, the median and IQR from summary() would be reported instead.

Frequency Distributions and Cross-Tabulations

For categorical variables, frequency distributions display the count and percentage of observations in each category. Cross-tabulations (contingency tables) display the joint distribution of two categorical variables and are the basis for computing measures of association such as odds ratios and risk ratios. Some readers will have calculated odds ratios in an earlier course; the worked example after the table builds them from the start.

Example: Cross-Tabulation in an Outbreak Investigation
Cases (n)Controls (n)Total
Exposed453075
Unexposed156075
Total6090150

From this 2×2 table, the odds ratio = (45 × 60) / (30 × 15) = 2,700 / 450 = 6.0, suggesting a strong association between exposure and disease.

Worked Example: From Counts to Odds to an Odds Ratio

The odds of a characteristic compare the number of people who have it with the number who do not: odds = number with / number without. For example, 3 people with for every 1 without gives odds of 3, which corresponds to a probability of 3 / 4 = 0.75.

In the outbreak table above the four cells are labelled a = 45 (exposed cases), b = 30 (exposed controls), c = 15 (unexposed cases) and d = 60 (unexposed controls).

GroupExposedUnexposedOdds of exposure
Casesa = 45c = 1545 / 15 = 3.0
Controlsb = 30d = 6030 / 60 = 0.5

Among cases, 3 people were exposed for every 1 who was not; among controls, 1 person was exposed for every 2 who were not. The odds ratio divides one odds by the other: OR = 3.0 / 0.5 = 6.0. Rearranging gives the cross-product formula, OR = (a × d) / (b × c) = (45 × 60) / (30 × 15) = 6.0.

An odds ratio of 1 means the odds are the same in both groups, so there is no association. A value above 1 means the exposure is more common (in odds terms) among cases, and a value below 1 means it is less common. An OR of 6 is a strong association. A descriptive odds ratio makes no allowance for chance or for other differences between the groups; later lessons add confidence intervals and adjust for other variables.

R Worked code: odds and odds ratios from a table
File for this example phaa_survey.csvdataset

matrix() builds a table from four counts, filling the first column and then the second; dimnames labels the rows and columns. For the course data, table() builds the same kind of table from two variables, and tab["Yes", "No"] picks the cell in the row labelled "Yes" and the column labelled "No".

# The outbreak table from the text
outbreak <- matrix(c(45, 15, 30, 60), nrow = 2,
                   dimnames = list(Exposure = c("Exposed", "Unexposed"),
                                   Group    = c("Case", "Control")))
outbreak
odds_cases    <- outbreak["Exposed", "Case"] / outbreak["Unexposed", "Case"]
odds_controls <- outbreak["Exposed", "Control"] / outbreak["Unexposed", "Control"]
odds_cases
odds_controls
odds_cases / odds_controls              # the odds ratio

# The same steps with the course data: hypertension by smoking status
tab <- table(Smoker = phaa$smoker, Hypertension = phaa$hypertension)
tab
odds_smokers     <- tab["Yes", "Yes"] / tab["Yes", "No"]
odds_non_smokers <- tab["No", "Yes"] / tab["No", "No"]
round(c(smokers = odds_smokers, non_smokers = odds_non_smokers,
        OR = odds_smokers / odds_non_smokers), 3)
Console output
> outbreak <- matrix(c(45, 15, 30, 60), nrow = 2, + dimnames = list(Exposure = c("Exposed", "Unexposed"), + Group = c("Case", "Control"))) > outbreak Group Exposure Case Control Exposed 45 30 Unexposed 15 60 > odds_cases <- outbreak["Exposed", "Case"] / outbreak["Unexposed", "Case"] > odds_controls <- outbreak["Exposed", "Control"] / outbreak["Unexposed", "Control"] > odds_cases [1] 3 > odds_controls [1] 0.5 > odds_cases / odds_controls [1] 6 > tab <- table(Smoker = phaa$smoker, Hypertension = phaa$hypertension) > tab Hypertension Smoker No Yes No 649 15 Yes 128 8 > odds_smokers <- tab["Yes", "Yes"] / tab["Yes", "No"] > odds_non_smokers <- tab["No", "Yes"] / tab["No", "No"] > round(c(smokers = odds_smokers, non_smokers = odds_non_smokers, + OR = odds_smokers / odds_non_smokers), 3) smokers non_smokers OR 0.062 0.023 2.704

The outbreak calculation reproduces the odds of 3 and 0.5 and the odds ratio of 6. In the course data, 8 of 136 smokers and 15 of 664 non-smokers have hypertension, giving odds of 0.062 and 0.023 and an odds ratio of 2.70: the odds of hypertension are about 2.7 times as high among smokers. With only 23 people with hypertension this estimate is imprecise; the logistic regression in the next lesson adds a confidence interval and adjusts for age and other variables.

Visualization Techniques

Appropriate visualization depends on the variable type and the question being asked. Exploratory data analysis emphasises looking at data first so structure, gaps, and anomalies are visible before any model is fit.

Histograms display the distribution of a continuous variable by dividing values into bins and counting observations per bin. They reveal distributional shape, modality, gaps, and potential outliers. The choice of bin width affects interpretation: too few bins obscure patterns, too many create noise.

The figure below shows the same 850 simulated readings, drawn from two groups with different average blood pressures, using 5, 20 and 100 bins. In R the number of bins is suggested with the breaks argument, for example hist(phaa$systolic_bp, breaks = 20); R treats the number as a suggestion and chooses round cut-points near it.

Three histograms of the same simulated systolic blood pressure data. With 5 bins the distribution looks like a single lumpy shape. With 20 bins two peaks appear, near 115 and 150 mmHg. With 100 bins the bars are jagged and the two peaks are hard to separate from random variation.
Simulated data. A: with 5 bins the two groups merge into one shape. B: with 20 bins the two peaks near 115 and 150 mmHg are clear. C: with 100 bins each bar holds only a few people and the outline is mostly noise. Trying two or three bin numbers before interpreting a histogram is good practice.

Boxplots display the median, the IQR (the box), whiskers that extend to the most extreme observation lying within 1.5×IQR of the box, and individual points beyond the whiskers. They are ideal for comparing distributions across groups (e.g., blood pressure by treatment arm) and for quickly identifying asymmetry and outliers.

Annotated boxplot of systolic blood pressure in the course data. The box runs from Q1 = 100 to Q3 = 116 mmHg with the median at 109. The upper whisker ends at 140 and the lower whisker at 85. Two points, 142 and 145 mmHg, lie above the upper whisker.
Course data after the range checks (n = 798). The whiskers stop at the most extreme readings inside the fences (Q1 − 1.5 × IQR = 76 and Q3 + 1.5 × IQR = 140), which here are 85 and 140 mmHg. Readings beyond the fences are drawn as individual points.

Bar charts display frequencies or proportions for categorical variables. Use them for comparing counts across groups. Avoid using bar charts for continuous data (use histograms instead) and avoid 3D bar charts, which distort proportions.

Scatter plots display the relationship between two continuous variables and can reveal linear or nonlinear associations, clusters, and outliers. Line graphs are appropriate for displaying trends over time (e.g., incidence rates by year). Both are essential in epidemiologic exploratory analysis.

Scatter plot of systolic blood pressure against age in the course data. The cloud of points rises from lower left to upper right.
Course data. Each point is one participant. The upward drift of the cloud shows that older participants tend to have higher systolic BP. The correlation, a measure of linear association that runs from −1 to 1, is 0.54 and is introduced fully in the next lesson. No point sits far from the cloud, so there is no sign of a bivariate outlier.
R Activity: descriptives and standard plots

This block continues from the cleaning activity in the previous section, so phaa must already hold the cleaned data. It calculates descriptive statistics and draws four standard plots: a histogram, a boxplot by smoking status, a bar chart and a scatter plot. Base R is enough; psych::describe() (the describe() function from the psych package) is convenient for many variables at once. The stratified Table 1 is built in its own box under the next heading.

# Categorical descriptives ---------------------------------------------------
table(phaa$gender)                            # frequencies
prop.table(table(phaa$gender))                # proportions
round(prop.table(table(phaa$gender)) * 100, 1)  # %

# Cross-tab: gender by smoker, row percentages
round(prop.table(table(phaa$gender, phaa$smoker), margin = 1) * 100, 1)

# Numeric descriptives --------------------------------------------------------
summary(phaa$age)
sd(phaa$age, na.rm = TRUE)
IQR(phaa$age, na.rm = TRUE)

# psych::describe() summarises many variables at once
library(psych)
describe(phaa[, c("age", "bmi", "systolic_bp", "phys_act_min")])

# Plots: histogram, boxplot stratified by exposure, bar chart, scatter plot ---
hist(phaa$systolic_bp, main = "Systolic BP", xlab = "mmHg")

boxplot(systolic_bp ~ smoker, data = phaa,
        main = "Systolic BP by smoking status", ylab = "mmHg")

barplot(table(phaa$gender), main = "Gender distribution")

plot(phaa$age, phaa$systolic_bp, main = "Systolic BP by age",
     xlab = "Age (years)", ylab = "Systolic BP (mmHg)")

Tip: always plot before you fit a regression. A boxplot of systolic_bp by smoker tells you in a second whether the comparison you are about to do in a later lesson will be driven by a few extreme values or by a real shift in the centre of the distribution.

R Reflect on what you just ran

Use the questions below to interpret the output you produced. Look at your console / plot before answering.

1. From round(prop.table(table(phaa$gender, phaa$smoker), margin = 1) * 100, 1), which gender category has the highest percentage of current smokers? What is the magnitude of the gender gap in smoking?

Model answerFrom the row-proportions table, women have the highest percentage of current smokers in this sample (17.9%, 74 of 414), slightly above men (16.4%, 59 of 359); non-binary participants are lowest at 11.1%, but that row rests on only 27 people (3 smokers), so it is unstable. The gender gap between women and men is 1.5 percentage points, small enough to be chance, whereas national surveys usually show men a few points higher. Direction matters: this is a row-percentage table (margin=1), so each row sums to 100% within gender; reading across, you see smoking prevalence within each gender.

2. From describe(phaa[, c("age", "bmi", "systolic_bp", "phys_act_min")]), which variable has the largest skew? Which is most clearly approximately normal? How would you justify reporting median (IQR) vs mean (SD) for each in a Table 1?

Model answerdescribe() from psych returns skew values for each variable. In this dataset all four are close to symmetric: BMI has the largest skew (0.17), with physical-activity minutes just behind (0.16) and age at 0.12, while systolic BP (skew 0.05, kurtosis -0.31) is the most clearly normal. For Table 1: report mean (SD) for all four here, because none has the long tail that pulls a mean away from the median (mean and median agree within 1 unit for each). Reserve median (IQR) for variables whose skew is well above 1 or whose histogram shows a long right tail (real physical-activity or income data often look like that), because the median is less pulled by extremes.

3. Look at the boxplot(systolic_bp ~ smoker). Does the median for smokers appear higher, lower, or similar to non-smokers? Are there any visible outliers, and would you expect them to bias a t-test of the difference in means?

Model answerThe boxplot shows the smoker median higher than the non-smoker median (114 versus 108 mmHg, a 6 mmHg gap; means 113.5 versus 107.2). Only the non-smoker group has outliers beyond the upper whisker (three values of 140 to 142 mmHg); the smoker group has none. Outliers above the upper whisker can bias a t-test of means toward statistical significance if they cluster in one group; the practical fix is to use a Wilcoxon rank-sum (which is rank-based) as a sensitivity test, or to inspect the outliers for data-entry errors. If outliers are real clinical values (genuine hypertensives), they belong in the analysis; report both means and medians side by side. Here the three outliers sit in the larger, lower-BP group, and a t-test, which compares the two means, and the rank-based Wilcoxon rank-sum test (both described in the normality section at the end of this page) both give p < 0.001, so the outliers do not drive the result.
Saved.

Descriptive Statistics for Epidemiologic Data: “Table 1”

In epidemiologic publications, “Table 1” typically presents the baseline characteristics of the study population, often stratified by exposure or outcome status. How baseline characteristics and group comparisons are reported has been the subject of methodological scrutiny. Pocock, Assmann, Enos, & Kasten (2002) reviewed published trial reports and catalogued common problems with baseline comparisons, subgroup analyses and covariate adjustment. A well-constructed Table 1 includes:

  • Demographics (age, sex, ethnicity) with appropriate summary statistics
  • Key clinical or exposure variables
  • Missing data counts for each variable
  • A comparison column between groups, preferably standardised differences (explained below)
Example: Table 1 Structure
CharacteristicExposed (n=200)Unexposed (n=300)p-value
Age, mean ± SD52.3 ± 11.449.8 ± 12.10.02
Female, n (%)110 (55.0%)162 (54.0%)0.82
BMI, median (IQR)27.1 (24.0–31.5)25.8 (23.2–29.4)0.004
Current smoker, n (%)48 (24.0%)51 (17.0%)0.05
Diabetes, n (%)34 (17.0%)30 (10.0%)0.02

Note how continuous variables with normal distributions use mean ± SD, while skewed variables (BMI) use median (IQR). Categorical variables are presented as n (%).

In this hypothetical study BMI was right-skewed, so its median (IQR) was reported. In the course data BMI is close to symmetric (skew 0.17 in the descriptive activity above), so the course-data table below reports it as mean (SD). The choice follows the distribution of each variable in the data at hand. The example also follows a layout common in published papers, with a p-value column and without an overall column or counts of missing values; the course-data table below shows the layout this lesson recommends.

Null Hypotheses, p-Values and Standardised Differences

A null hypothesis is the starting assumption of no difference: here, that exposed and unexposed people have the same average age in the population the sample came from. A p-value is the probability of seeing a difference at least as large as the one observed if the null hypothesis were true. A small p-value (conventionally below 0.05) means the observed difference would be surprising if there were truly no difference between the groups.

A standardised difference (also called the standardised mean difference, SMD) expresses the gap between two groups in standard-deviation units. For a continuous variable it is the difference in means divided by the square root of the average of the two groups' variances; for a proportion, the difference in proportions is divided by a standard deviation calculated from the two proportions. An SMD of 0.5 means the groups differ by half a standard deviation. Values below about 0.1 are commonly read as a negligible imbalance, although this cut-off is a convention.

Recommendation for this course. Table 1 describes the sample, so this course follows the STROBE reporting guideline for observational studies, which advises that significance tests be avoided in descriptive tables (Vandenbroucke et al., 2007). A p-value in Table 1 depends heavily on sample size and describes chance variation; whether an imbalance is large enough to matter as a confounder (a variable that can distort the association between exposure and outcome, defined more fully under the next heading) is a separate question, which the standardised difference addresses more directly. Where a comparison column is wanted, report the standardised difference. When a journal requires p-values, they should be read as descriptions of chance variation.

R Worked code: a Table 1 for the course data, stratified by smoking status
File for this example phaa_survey.csvdataset

Base R can build Table 1 with three small helper functions. function(v) { ... } defines a reusable recipe, tapply(v, grp, f) applies the recipe f separately to each smoking group, and sprintf() formats numbers as text: %.1f means one decimal place, %d a whole number and %% a percent sign. The comparison phaa$education >= "Bachelor's" works because education was made an ordered factor in the cleaning activity; it is TRUE for a bachelor's or graduate degree.

# Three small helper functions. function(v) { ... } defines a reusable recipe
# that is applied to the whole sample and then to each smoking group.
grp <- phaa$smoker                       # the grouping variable ("No", "Yes")

mean_sd <- function(v) {                 # "mean (SD)" and the standardised difference
  f <- function(x) sprintf("%.1f (%.1f)", mean(x, na.rm = TRUE), sd(x, na.rm = TRUE))
  m <- tapply(v, grp, mean, na.rm = TRUE)
  s <- tapply(v, grp, sd, na.rm = TRUE)
  d <- (m["Yes"] - m["No"]) / sqrt((s["Yes"]^2 + s["No"]^2) / 2)
  c(Overall = f(v), tapply(v, grp, f), SMD = sprintf("%.2f", d))
}
n_pct <- function(v, level) {            # "n (%)" and the standardised difference
  f <- function(x) sprintf("%d (%.1f%%)", sum(x == level, na.rm = TRUE),
                           100 * mean(x == level, na.rm = TRUE))
  p <- tapply(v == level, grp, mean, na.rm = TRUE)
  d <- (p["Yes"] - p["No"]) / sqrt((p["Yes"] * (1 - p["Yes"]) + p["No"] * (1 - p["No"])) / 2)
  c(Overall = f(v), tapply(v, grp, f), SMD = sprintf("%.2f", d))
}
n_miss <- function(v) {                  # number of missing values
  c(Overall = sum(is.na(v)), tapply(is.na(v), grp, sum), SMD = "")
}

table1 <- rbind(
  "N"                     = c(Overall = length(grp), table(grp), SMD = ""),
  "Age, mean (SD)"        = mean_sd(phaa$age),
  "  Age missing, n"      = n_miss(phaa$age),
  "Woman, n (%)"          = n_pct(phaa$gender, "Woman"),
  "Degree, n (%)"         = n_pct(phaa$education >= "Bachelor's", TRUE),
  "BMI, mean (SD)"        = mean_sd(phaa$bmi),
  "SBP, mean (SD)"        = mean_sd(phaa$systolic_bp),
  "Activity, mean (SD)"   = mean_sd(phaa$phys_act_min),
  "  Activity missing, n" = n_miss(phaa$phys_act_min),
  "Income missing, n (%)" = n_pct(is.na(phaa$income), TRUE)
)
noquote(table1)
Console output
> noquote(table1) Overall No Yes SMD N 800 664 136 Age, mean (SD) 45.1 (13.6) 45.1 (13.4) 45.3 (14.9) 0.02 Age missing, n 2 1 1 Woman, n (%) 414 (51.7%) 340 (51.2%) 74 (54.4%) 0.06 Degree, n (%) 360 (45.0%) 304 (45.8%) 56 (41.2%) -0.09 BMI, mean (SD) 20.8 (3.2) 21.0 (3.2) 19.5 (2.9) -0.50 SBP, mean (SD) 108.2 (11.3) 107.2 (11.0) 113.5 (11.6) 0.56 Activity, mean (SD) 176.1 (82.4) 176.0 (83.4) 176.1 (77.1) 0.00 Activity missing, n 28 20 8 Income missing, n (%) 35 (4.4%) 32 (4.8%) 3 (2.2%) -0.14

The output has one column for the whole sample, one for non-smokers ("No"), one for smokers ("Yes") and the standardised difference (smokers minus non-smokers). Smokers and non-smokers have nearly the same age (SMD 0.02), proportion of women (0.06), education (−0.09) and physical activity (0.00). Smokers have a lower mean BMI (19.5 versus 21.0 kg/m², SMD −0.50) and a higher mean systolic BP (113.5 versus 107.2 mmHg, SMD 0.56). BMI differs between the groups, but smoking can itself lower body weight, so BMI may be a mediator, a variable on the causal path from smoking to blood pressure. In the simulated course data BMI was generated to depend on smoking status. Adjusting for a mediator removes part of the effect being estimated, so whether BMI enters a model is decided from subject knowledge about causal order, which the regression lessons will need to consider. The missing-value rows show that missing activity data are spread across both groups (20 and 8).

Turning the R output into a report table involves relabelling the columns (with group sizes in the headers), writing units and full names into the row labels, removing the percent signs that the row label already states, and adding a footnote that defines abbreviations. The numbers themselves are copied unchanged.

CharacteristicOverall (n = 800)Non-smokers (n = 664)Smokers (n = 136)Std. diff.
Age (years), mean (SD)45.1 (13.6)45.1 (13.4)45.3 (14.9)0.02
  Missing, n211
Women, n (%)414 (51.7)340 (51.2)74 (54.4)0.06
Bachelor's degree or higher, n (%)360 (45.0)304 (45.8)56 (41.2)−0.09
BMI (kg/m²), mean (SD)20.8 (3.2)21.0 (3.2)19.5 (2.9)−0.50
Systolic BP (mmHg), mean (SD)108.2 (11.3)107.2 (11.0)113.5 (11.6)0.56
Physical activity (min/week), mean (SD)176.1 (82.4)176.0 (83.4)176.1 (77.1)0.00
  Missing, n28208
Income not reported, n (%)35 (4.4)32 (4.8)3 (2.2)−0.14

Table note for a report: "Std. diff.: standardised difference, smokers minus non-smokers. SD: standard deviation. BMI: body mass index. One BMI value and two systolic BP values were set to missing by range checks."

Stratified Descriptive Analyses

Stratifying descriptive statistics by key exposure or covariate groups allows you to assess the distribution of potential confounders across exposure categories. This is a critical step before proceeding to multivariable modelling, as it helps identify imbalances that may require adjustment.

A confounder is a third variable that is related to both the exposure and the outcome and can therefore make the two appear more, or less, strongly associated than they truly are. Age is a common example, because it is linked to many exposures and to blood pressure. A large standardised difference in Table 1 marks a variable that differs between the exposure groups; whether it also affects the outcome is judged from subject knowledge and the later regression models. A variable that is itself affected by the exposure is a mediator and does not meet the definition of a confounder.

Building Scales: Internal Consistency and Factor Analysis

Many public-health surveys ask several items that together measure a latent construct (e.g., depression, anxiety, social support). Two questions need to be answered before you can use these items as a single scale variable in a regression: (1) internal consistency (do the items hang together?), assessed with Cronbach’s α; and (2) dimensionality (how many underlying factors do the items measure?), assessed with exploratory factor analysis (EFA). Once both checks pass, the items can be summed (or averaged) into a derived scale variable that you carry forward into later lessons.

Each depression item asks how often a symptom occurred, on a five-point response scale from 1 (Never) to 5 (Always), so a higher number always means more symptoms. Some questionnaires also include reverse-coded items, worded in the opposite direction (for example, "I felt hopeful about the future"), where a high answer means fewer symptoms. Such items must be flipped before alpha is calculated or a score is built; on a 1 to 5 scale the flipped value is 6 minus the original, so 5 becomes 1 and 1 becomes 5. The alpha() function prints a warning when an item correlates negatively with the rest of the scale, which is the usual sign of an item that has not been reversed. None of the course items is reverse-coded.

Cronbach's alpha can be understood as a measure of how well the items agree. If people who score high on one item tend to score high on the others, the items share a common source, the construct, and alpha is high. Alpha rises when the items are more strongly correlated with each other and when there are more of them. Under the assumptions behind it, an alpha of 0.90 indicates that about 90% of the variation in total scores reflects what the items have in common and about 10% reflects item-specific noise. Values of about 0.70 or higher are conventionally acceptable and 0.80 or higher good; values above about 0.95 can mean that some items are near-duplicates.

Two terms from factor analysis appear in the output. An eigenvalue measures how much of the shared variation among the items one factor accounts for; a large first eigenvalue followed by small ones suggests a single underlying factor. Parallel analysis compares each eigenvalue from the real data with the eigenvalue that random data of the same size would produce, and keeps factors, counting from the first, for as long as the real eigenvalue is larger. A factor loading is the correlation between an item and a factor; loadings of 0.40 or higher are conventionally treated as meaningful, and this lesson uses that threshold throughout.

R Activity: Cronbach’s α and exploratory factor analysis

The course dataset contains seven depression items (dep1 … dep7) and five anxiety items (anx1 … anx5). Below we (1) confirm internal consistency, (2) decide on the number of factors, and (3) build the derived scale scores we use for the rest of the course.

# 1. Internal consistency for each candidate scale ----------------------------
library(psych)
dep_items <- phaa[, c("dep1","dep2","dep3","dep4","dep5","dep6","dep7")]
anx_items <- phaa[, c("anx1","anx2","anx3","anx4","anx5")]

alpha(dep_items)             # raw_alpha >= 0.70 is acceptable, >= 0.80 is good
alpha(anx_items)
dep_alpha <- alpha(dep_items)            # store the result to read it at 3 decimals
round(dep_alpha$total$raw_alpha, 3)
round(dep_alpha$alpha.drop[, "raw_alpha", drop = FALSE], 3)  # alpha if an item is dropped

# 2. How many underlying factors? ------------------------------------------
fa.parallel(dep_items, fa = "fa")   # parallel analysis for factors

# 3. Inspect a one-factor solution for the depression items ----------------
factanal(x = na.omit(dep_items), factors = 1)
# na.omit(): factanal() needs complete rows and dep4 has 15 NAs, so these 15
# people are left out of this check (a complete-case analysis).
# With a single factor there is nothing to rotate, so no rotation is requested.
# Loadings >= 0.40 contribute meaningfully to the factor.

# 4. Two-factor solution combining dep + anx items -------------------------
combined <- na.omit(cbind(dep_items, anx_items))
factanal(x = combined, factors = 2, rotation = "varimax")
# dep1-7 should load on one factor; anx1-5 on the other.

# 5. Build derived scale variables for use in later lessons ------------------
# Score = mean of the answered items x number of items, calculated only when
# at most one item is missing (otherwise NA). A missing item is never
# counted as 0, which would understate the person's score.
n_dep <- rowSums(!is.na(dep_items))     # items answered by each person
n_anx <- rowSums(!is.na(anx_items))
phaa$dep_score <- ifelse(n_dep >= 6, rowMeans(dep_items, na.rm = TRUE) * 7, NA)
phaa$anx_score <- ifelse(n_anx >= 4, rowMeans(anx_items, na.rm = TRUE) * 5, NA)
summary(phaa$dep_score)

# 6. Save the cleaned, scale-augmented file under a new name -----------------
write.csv(phaa, "phaa_survey_clean.csv", row.names = FALSE)
Console output (depression items; the anxiety and two-factor output are left for the questions)
> alpha(dep_items) Reliability analysis Call: alpha(x = dep_items) raw_alpha std.alpha G6(smc) average_r S/N ase mean sd median_r 0.9 0.9 0.89 0.57 9.4 0.0052 3 1.1 0.58 95% confidence boundaries lower alpha upper Feldt 0.89 0.9 0.91 Duhachek 0.89 0.9 0.91 Reliability if an item is dropped: raw_alpha std.alpha G6(smc) average_r S/N alpha se var.r med.r dep1 0.89 0.89 0.87 0.57 8.0 0.0061 0.00111 0.57 dep2 0.89 0.89 0.87 0.56 7.8 0.0062 0.00118 0.57 dep3 0.89 0.89 0.87 0.57 7.9 0.0062 0.00157 0.57 dep4 0.89 0.89 0.87 0.57 7.8 0.0062 0.00132 0.57 dep5 0.89 0.89 0.87 0.58 8.3 0.0059 0.00126 0.59 dep6 0.89 0.89 0.87 0.57 8.0 0.0061 0.00129 0.57 dep7 0.90 0.90 0.88 0.59 8.6 0.0057 0.00042 0.59 Item statistics n raw.r std.r r.cor r.drop mean sd dep1 800 0.80 0.80 0.76 0.72 3 1.4 dep2 800 0.82 0.82 0.78 0.74 3 1.4 dep3 800 0.81 0.81 0.77 0.73 3 1.4 dep4 785 0.81 0.81 0.78 0.74 3 1.4 dep5 800 0.78 0.78 0.73 0.69 3 1.4 dep6 800 0.80 0.80 0.75 0.72 3 1.4 dep7 800 0.75 0.75 0.69 0.65 3 1.4 ... (10 more lines not shown) > round(dep_alpha$total$raw_alpha, 3) [1] 0.904 > round(dep_alpha$alpha.drop[, "raw_alpha", drop = FALSE], 3) raw_alpha dep1 0.889 dep2 0.886 dep3 0.888 dep4 0.887 dep5 0.892 dep6 0.889 dep7 0.896 > fa.parallel(dep_items, fa = "fa") Parallel analysis suggests that the number of factors = 1 and the number of components = NA > factanal(x = na.omit(dep_items), factors = 1) Call: factanal(x = na.omit(dep_items), factors = 1) Uniquenesses: dep1 dep2 dep3 dep4 dep5 dep6 dep7 0.405 0.368 0.401 0.391 0.465 0.417 0.523 Loadings: Factor1 dep1 0.772 dep2 0.795 dep3 0.774 dep4 0.781 dep5 0.731 dep6 0.763 dep7 0.691 Factor1 SS loadings 4.030 Proportion Var 0.576 Test of the hypothesis that 1 factor is sufficient. The chi square statistic is 12.7 on 14 degrees of freedom. The p-value is 0.551
OutputWhat it shows and what to report
raw_alphaCronbach's alpha, printed to two decimals as 0.9. The line round(dep_alpha$total$raw_alpha, 3) gives 0.904, which is the value to report (α = 0.904).
std.alpha, average_rAlpha calculated from standardised items (similar here because all items share the same response scale), and the average correlation between pairs of items (0.57).
95% confidence boundariesThe range of alpha values compatible with the data, 0.89 to 0.91.
Reliability if an item is droppedEach row gives alpha with that item removed. A value above the overall 0.904 would mean the scale is better without the item. Every value here is lower (0.886 to 0.896 at three decimals), so every item contributes; dep7 contributes least, because removing it would lower alpha only to 0.896.
r.drop (item statistics)The correlation of each item with the total of the other items, from 0.65 (dep7) to 0.74. Values below about 0.30 would mark an item that does not fit the scale.
fa.parallel()The message reports one factor (the number of components is NA because fa = "fa" requests factors only). The annotated plot below shows why.
factanal() LoadingsAll seven loadings lie between 0.691 (dep7) and 0.795 (dep2), well above 0.40. Uniquenesses are the share of each item that the factor does not explain (1 minus the squared loading).
Proportion Var and the chi-square testThe single factor explains 57.6% of the item variance. The test asks whether one factor is enough; its large p-value (0.551) gives no evidence that more factors are needed.
Parallel analysis plot for the seven depression items. The blue line with triangles shows the eigenvalues from the real data: 4.02 for factor 1, then values near zero for factors 2 to 7. Two red lines show eigenvalues from random data of the same size, starting near 0.5 and falling toward zero. Factor 1 lies far above the red lines and factor 2 falls below them, so one factor is kept.
Course data. The blue line (FA Actual Data) shows the eigenvalues of the real items; the red dotted and dashed lines (FA Simulated Data and FA Resampled Data) show the eigenvalues that random data of the same size produce. Counting from factor 1, factors are kept until the first blue point that falls below the red lines. That happens at factor 2, so one factor is kept, and the near-zero eigenvalues of later factors play no part. The red lines vary slightly from run to run because they come from random data.

How to read this. An α of 0.904 indicates that the seven depression items measure essentially the same construct. The single factor explains 57.6% of the item variance and every loading is well above the 0.40 threshold (the lowest is 0.691, for dep7), so combining the items into dep_score is justified. Step 5 builds the score as the mean of the answered items multiplied by the number of items (a prorated sum), and only for people who answered at least six of the seven depression items (four of the five anxiety items). Using rowSums(dep_items, na.rm = TRUE) instead would count a skipped item as 0 and understate the scores of the 15 people who skipped dep4, by between 1.8 and 4.5 points (2.8 on average). The score is used as a continuous predictor in a later lesson and as a covariate in the logistic model for hypertension.

R Reflect on what you just ran

Use the questions below to interpret the output you produced. Look at your console / plot before answering.

1. What is the raw alpha for the depression items? For the anxiety items? Which scale (if either) clears the conventional 0.80 threshold for "good" internal consistency, and which item (look at the "Reliability if an item is dropped" table) contributes least to the depression scale?

Model answerRaw α for the seven depression items is 0.904 (good internal consistency), matching the console output above; the anxiety scale is a little lower at 0.855. Both scales clear the 0.80 threshold. In the "reliability if an item is dropped" table, the item that contributes least is the one whose alpha-if-dropped value is highest, equivalently the one with the lowest item-total correlation: dep7. Removing it would lower alpha the least (from 0.904 to 0.896), and its r.drop of 0.65 is the lowest (0.69 to 0.74 for the other six). Because every alpha-if-dropped value is below 0.904, every item, dep7 included, adds to the scale.

2. From fa.parallel(dep_items), how many factors have eigenvalues above the parallel-analysis line? Does the one-factor solution from factanal() show all seven loadings >= 0.40?

Model answerfa.parallel() shows exactly one factor above the parallel-analysis line for the depression items (first eigenvalue 4.02 against about 0.5 for random data, a value that varies slightly from run to run; the second is 0.05) and prints "the number of factors = 1", supporting a unidimensional structure. The one-factor solution from factanal() has all seven loadings ≥ 0.40 (they run from 0.691 for dep7 to 0.795 for dep2, and the factor explains 57.6% of the item variance), confirming that all items track the same latent construct.

3. In the two-factor combined solution, do the depression items load on a different factor than the anxiety items? Cite one specific loading from your output that supports your answer.

Model answerIn the two-factor combined solution, the depression and anxiety items load on different factors. A specific example: depression item dep3 loads 0.748 on Factor 1 and 0.205 on Factor 2, while anxiety item anx2 loads 0.124 on Factor 1 and 0.714 on Factor 2. The cross-loadings (no item loads above 0.21 on the other factor) and the loadings on each item's own factor (all at least 0.68, well above the 0.40 threshold) together support the interpretation that the two scales measure distinct, though related, latent constructs.
Saved.

Normality Assessment

Assessing whether a variable follows a normal distribution informs the choice between parametric and non-parametric methods.

Visual Methods

Q-Q (quantile-quantile) plots compare observed quantiles to theoretical normal quantiles. Points falling along the diagonal line suggest normality; systematic departures indicate non-normality. Histograms with a normal curve overlay also provide quick visual assessment.

In plain terms, a Q-Q plot sorts the data from smallest to largest and plots each value against the z-score that a standard normal distribution would place at the same position. These z-scores form the horizontal axis, labelled theoretical quantiles, which runs from about −3 to 3. If the data are normal, the points fall along a straight line. Points that curve away from the line at one end indicate skew, and points that bend away at both ends in an S shape indicate heavy tails, meaning more extreme values than a normal distribution would produce. In R, qqnorm(x) draws the points and qqline(x) adds the reference line.

Formal Tests

The Shapiro-Wilk test is generally preferred for samples under 5,000. The Kolmogorov-Smirnov test (with Lilliefors correction) is an alternative. Both test the null hypothesis that data come from a normal distribution.

Because the null hypothesis is normality, a small p-value (below 0.05 by convention) is evidence of a departure from normality, and a large p-value means that no departure was detected. The p-value also depends on sample size: the same small departure gives a large p-value in a sample of 50 and a very small one in a sample of 50,000, because a test with many observations has the power to detect even trivial departures. R's shapiro.test() accepts between 3 and 5,000 values.

Caveat: With very large samples, these tests will reject normality even for trivial departures. Visual assessment should always accompany formal tests.

Three histograms with their normal Q-Q plots beneath. A, normal: the Q-Q points follow the red line. B, right-skewed: the points curve above the line at the right end and sit above it at the left end. C, heavy-tailed: the points lie on the line in the middle and bend away at both ends.
Simulated data (300 values per panel). A: a normal distribution, whose points follow the line. B: a right-skewed distribution, whose points curve away at the right end. C: a heavy-tailed distribution, whose points bend away at both ends (low values below the line, high values above it).
R Worked code: a Q-Q plot and the Shapiro-Wilk test
File for this example phaa_survey.csvdataset
qqnorm(phaa$systolic_bp, main = "Systolic BP: normal Q-Q plot")
qqline(phaa$systolic_bp)          # the reference line
shapiro.test(phaa$systolic_bp)
shapiro.test(phaa$diastolic_bp)
Console output
> qqnorm(phaa$systolic_bp, main = "Systolic BP: normal Q-Q plot") > qqline(phaa$systolic_bp) > shapiro.test(phaa$systolic_bp) Shapiro-Wilk normality test data: phaa$systolic_bp W = 0.99258, p-value = 0.0005134 > shapiro.test(phaa$diastolic_bp) Shapiro-Wilk normality test data: phaa$diastolic_bp W = 0.99672, p-value = 0.09998
OutputWhat it shows
WThe test statistic, which is close to 1 when the data look normal. Both values (0.993 and 0.997) are very close to 1.
Systolic BP: p-value = 0.0005The small p-value means a departure from normality was detected. In the Q-Q plot the points follow the line closely except at the lower end, where the 18 lowest readings are all exactly 85 mmHg and form a short flat run. With 798 readings the test detects this small departure, while the skew of 0.05 shows that the distribution is close to symmetric.
Diastolic BP: p-value = 0.10No departure from normality was detected.

From the Checks to a Decision

In practice the analyst combines the plot, the skew value and the purpose of the analysis. If the Q-Q plot is close to a straight line and |skew| is below about 1, the variable is summarised as mean (SD) and parametric methods are reasonable, even when a large sample makes the Shapiro-Wilk p-value small, as for systolic BP here. If the plot curves clearly or |skew| is above about 1, the variable is summarised as median (IQR), and the analyst either transforms it (as in an earlier section) or uses a rank-based method. Rank-based methods replace each value by its rank (1 for the smallest, 2 for the next, and so on), so a few extreme values cannot dominate the result. The table below pairs the common options.

QuestionParametric option (assumes approximate normality)Rank-based option (fewer assumptions)
Do two groups differ?The t-test compares the two means: t.test(y ~ group). R uses the Welch version by default, which allows the two groups to have different spreads.The Wilcoxon rank-sum test compares the ranks of the two groups: wilcox.test(y ~ group).
Do three or more groups differ?One-way analysis of variance (ANOVA) compares the means: summary(aov(y ~ group)).The Kruskal-Wallis test compares the ranks: kruskal.test(y ~ group).
Are two continuous variables associated?Pearson correlation measures linear association: cor.test(x, y).Spearman correlation is Pearson correlation calculated on the ranks: cor.test(x, y, method = "spearman").

Implications for Analysis Choice

If a variable is approximately normal, parametric tests (t-tests, ANOVA, Pearson correlation) are appropriate. For non-normal distributions, consider non-parametric alternatives (Wilcoxon rank-sum, Kruskal-Wallis, Spearman correlation) or data transformations. The Central Limit Theorem provides some protection for large samples, but descriptive summaries should still reflect the actual distribution.

Reflection

Consider how you would construct a “Table 1” for an epidemiologic study examining the relationship between physical activity and cardiovascular disease. What variables would you include, how would you summarize each, and what stratification would you use?

Model answerTable 1 for a cohort studying physical activity and cardiovascular disease (CVD): rows are baseline characteristics, and columns are the exposure groups (for example low, moderate and high physical activity) plus an overall column. Variables: demographic (age, sex, ethnicity, education, income), clinical (BMI, systolic and diastolic BP, total cholesterol, HbA1c as a measure of average blood glucose, smoking, alcohol, family history of CVD), and exposure-related (weekly minutes of moderate-to-vigorous activity, sedentary time). Summaries: mean (SD) for approximately normal continuous variables, median (IQR) for skewed ones (activity minutes are often right-skewed), n (%) for categorical variables, and the number missing for each variable. Comparison column: standardised differences between the activity groups instead of p-values, because Table 1 describes the sample and the size of an imbalance matters more than its statistical significance; variables with large standardised differences, such as age, are candidate confounders for the models in later lessons. Stratification by age group and sex can be shown as a supplementary table.
Reflection saved.
Knowledge check: this section

1. For a heavily right-skewed variable, which summary statistics should be reported?

For skewed distributions, the median is a more representative measure of central tendency than the mean, and the IQR is a more robust measure of spread than the standard deviation. This is the standard reporting convention in epidemiologic research.

2. A distribution has excess kurtosis greater than 0. What does this indicate?

Positive excess kurtosis (leptokurtic) indicates heavier tails and a sharper peak compared to a normal distribution, meaning more extreme values are present than would be expected under normality. This may reflect outliers or a mixture of subpopulations.

3. What is the primary purpose of stratifying descriptive statistics by exposure group in epidemiologic research?

Stratifying by exposure group reveals whether potential confounders are unevenly distributed between groups. This information is essential for planning multivariable analyses and identifying variables that may require adjustment.

4. A Shapiro-Wilk test yields p < 0.001 with a sample of 50,000. Which interpretation is most appropriate?

With very large samples, formal normality tests have excessive power and will reject the null hypothesis for negligible departures from normality. Power is the ability of a test to detect a departure, and with 50,000 values a test can detect departures far too small to matter. Visual assessment (Q-Q plots, histograms) should always accompany formal tests to evaluate practical significance of any departure.

● Complete the reflection and knowledge check to continue.

Section 4

Final Assessment

⏱ Estimated time: 15 minutes

Bringing It All Together

This lesson followed a single dataset from raw to descriptive. The section on data quality named the errors (collection, entry, storage, and analysis-stage problems) and unpacked the missing-data mechanisms (MCAR, MAR, MNAR) that determine which cleaning approaches are valid. The section on cleaning strategies turned that vocabulary into a workflow: systematic range, type, and consistency checks; outlier triage; principled missing-data handling; transformations and derived variables; and a documentation trail that a peer reviewer could follow. The section on descriptive analyses closed the loop with the layer that every paper opens with: central tendency, spread, shape, frequency tables, visualisations, scale checks, and a Table 1 that stratifies on the primary exposure or outcome.

Descriptive analysis is a core stage of any analysis. It is the layer where data problems surface, where assumptions get tested, and where readers calibrate how much trust to place in the inferential models that follow. From here we can move into the regression machinery of the rest of the course knowing that the dataset under it has been examined and understood.

Key Takeaways from this lesson

  • Errors enter the data pipeline at every stage; cleaning is about locating them, not arbitrarily fixing values.
  • The missing-data mechanism (MCAR, MAR, MNAR) determines which handling strategies remain valid; complete-case analysis is defensible when little is missing and MCAR is plausible, and multiple imputation under a stated MAR assumption is preferred otherwise.
  • Outliers should be investigated, not deleted by reflex; document keep/exclude/transform decisions.
  • Choose central tendency, spread, and shape statistics based on variable type and distributional shape.
  • A well-built Table 1 stratified by exposure or outcome is often the most informative single output of an analysis.
  • Cleaning and descriptive work must leave a documented trail; reproducibility is not optional.

Reflection

Reflecting on the entire lesson, describe a systematic workflow you would follow when receiving a new epidemiologic dataset, from initial data quality assessment through to producing a complete descriptive summary. What tools, checks, and documentation would you use at each step?

Model answerA defensible workflow: (1) Receipt: save the raw file unchanged in its own folder (for example data/raw/) and never edit it. (2) Inventory: read the file and check its structure with dim(), str(), summary() and colSums(is.na()), comparing each variable with the codebook. (3) Variable-by-variable cleaning: convert missing-data codes to NA, list the IDs that break each hard limit with sum() and which() before setting them to NA, flag soft-limit values for checking, check categories with table() and IDs with duplicated(). (4) Missing data: count missing values per variable and complete cases, compare people with and without missing values, state the assumed mechanism, and choose complete-case analysis or multiple imputation accordingly. (5) Derivation: build analytic variables (age groups, BP categories, scale scores after checking alpha and dimensionality). (6) Descriptive summary: histograms, boxplots, summary() and psych::describe() for each variable, normality checks for the main continuous variables, and a Table 1 by exposure or outcome. (7) Documentation: one R script, always run from the raw file, with a comment for every decision; a cleaning log saved as a CSV file; the cleaned data saved under a new name; and an updated codebook that includes every derived variable and its formula. Documentation at every step: the decision log explains every NA, every recode, every dropped row.
Reflection saved.

Final Knowledge Assessment

You must answer all 15 questions correctly (100%) and complete the final reflection to finish this lesson.

Final assessment: the 15 questions

1. Which stage of the data pipeline is most susceptible to transcription errors?

Transcription errors occur when data are transferred from one medium to another, which is the defining activity of data entry. Double data entry is a key strategy for detecting such errors.

2. In a dataset where missingness of income depends on the actual income value itself (high earners are less likely to report), the missing data mechanism is:

When the probability of missingness depends on the unobserved (missing) values themselves, the mechanism is MNAR. In this case, the actual income value influences whether it is reported, regardless of other observed characteristics.

3. Which method for handling missing data correctly accounts for imputation uncertainty?

Multiple imputation creates several plausible imputed datasets and pools results using Rubin’s rules, which correctly incorporates the additional uncertainty due to missing data into standard errors and confidence intervals.

4. Using the IQR method, a value is classified as an outlier if it falls:

The IQR method defines outliers using the first and third quartiles. Values falling below Q1 − 1.5×IQR or above Q3 + 1.5×IQR are flagged as potential outliers. This is the same criterion used in standard boxplots.

5. What is the primary limitation of listwise deletion?

Listwise deletion excludes any observation with missing data on any variable in the analysis, which can substantially reduce the effective sample size. It only produces unbiased estimates under the MCAR assumption.

6. For a normally distributed continuous variable, the conventional summary statistics to report are:

For normally distributed variables, the mean and standard deviation are the most informative summary statistics because the normal distribution is fully characterized by these two parameters. This is the standard reporting convention in epidemiologic research.

7. A variable measuring “number of emergency department visits in the past year” is best classified as:

This variable represents discrete events (visits) counted over a time period, making it a count variable. Count variables are non-negative whole numbers (0, 1, 2, and so on), and a later lesson introduces regression models designed for them.

8. Which visualization is most appropriate for comparing the distribution of a continuous variable across three treatment groups?

Boxplots allow direct comparison of medians, IQRs, and outliers across groups. Side-by-side boxplots are ideal for comparing the distribution of a continuous variable across multiple categorical groups.

9. In a cross-tabulation of exposure and disease, the odds ratio is computed as:

The odds ratio from a 2×2 table is calculated as the cross-product ratio: (a × d) / (b × c), where cells are arranged with exposed cases (a), exposed controls (b), unexposed cases (c), and unexposed controls (d).

10. Positive skewness indicates that:

Positive (right) skewness means the distribution has a longer or fatter right tail. The mean is pulled toward the extreme values, so it exceeds the median. This pattern is common in epidemiologic data such as hospital length of stay or healthcare costs.

11. What is the purpose of a Q-Q plot?

A Q-Q plot compares the quantiles of the observed data against the quantiles of a theoretical distribution. If data follow the specified distribution, points will fall approximately along the diagonal reference line.

12. When applying a natural log transformation to a variable that contains zero values, the appropriate approach is to:

Since ln(0) is undefined, a common practice is to add a small constant (typically 1) to all values before applying the log transformation. This preserves the zero observations in the analysis while allowing the transformation to be applied.

13. In a “Table 1” for an epidemiologic study, categorical variables are typically summarized as:

Categorical variables in Table 1 are reported as the count (n) and percentage (%) within each category. This provides both the absolute number and the relative frequency, allowing readers to assess both the distribution and sample size.

14. Which statement about the z-score method for outlier detection is correct?

The z-score method relies on the mean and standard deviation, both of which are influenced by extreme values. In skewed distributions, outliers pull the mean and inflate the SD, which can mask other outliers (the masking effect) or fail to flag truly unusual values.

15. An audit trail for data cleaning should include all of the following EXCEPT:

An audit trail documents data cleaning decisions: what was changed, why, by whom, when, and what the original values were. Regression model coefficients are analysis outputs, not data cleaning documentation.

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

Lesson Complete

You have successfully completed Data Cleaning and Descriptive Analyses. Your responses have been downloaded.

The next lesson, Linear and Logistic Regression, takes the step from description to inference. The continuous outcomes summarised in this lesson become dependent variables, and the predictors cleaned and processed here enter as independent variables. The regression framework that begins in the next lesson reappears, in different forms, throughout the remainder of this course.