required <- c("knitr", "jsonlite", "curl")
ready <- vapply(required, requireNamespace, quietly = TRUE, FUN.VALUE = logical(1))
if (!all(ready)) stop("Install these packages first: ", paste(required[!ready], collapse = ", "))
options(timeout = 180)
set.seed(2381)
knitr::opts_chunk$set(dpi = 120)
year <- as.integer(params$year)
earlier_year <- as.integer(params$earlier_year)
stopifnot(earlier_year < year)
dir.create("hss2381_cache", showWarnings = FALSE)
show_table <- function(x, digits = 2) knitr::kable(x, digits = digits, row.names = FALSE)
pretty_p <- function(x) format.pval(x, digits = 3, eps = .001)
get_json <- function(url) {
last_error <- "No response"
for (attempt in 1:3) {
result <- tryCatch({
h <- curl::new_handle(connecttimeout = 30, timeout = 180,
useragent = "HSS2381 teaching workbook")
response <- curl::curl_fetch_memory(url, handle = h)
if (response$status_code != 200) stop("HTTP ", response$status_code)
jsonlite::fromJSON(rawToChar(response$content), simplifyVector = FALSE)
}, error = function(e) e)
if (!inherits(result, "error")) return(result)
last_error <- conditionMessage(result)
if (attempt < 3) Sys.sleep(attempt)
}
stop("World Bank download failed: ", last_error,
". Check the connection or use a previously downloaded valid cache.")
}
get_pages <- function(endpoint, query = "") {
rows <- list()
urls <- character()
page <- 1L
repeat {
url <- paste0("https://api.worldbank.org/v2/", endpoint,
"?format=json&per_page=10000&page=", page, query)
z <- get_json(url)
if (length(z) != 2 || is.null(z[[1]]$pages) || !is.list(z[[2]])) {
stop("The API returned an unexpected response for ", endpoint)
}
rows <- c(rows, z[[2]])
urls <- c(urls, url)
if (page >= as.integer(z[[1]]$pages)) break
page <- page + 1L
}
list(rows = rows, urls = urls)
}
value <- function(x) {
if (is.null(x) || !length(x)) NA_character_ else as.character(x[[1]])
}
indicators <- c(life = "SP.DYN.LE00.IN", spending = "SH.XPD.CHEX.PC.CD",
gdp = "NY.GDP.PCAP.CD", urban = "SP.URB.TOTL.IN.ZS")
cache <- file.path("hss2381_cache", paste0("gentle_v1_", earlier_year, "_", year, ".rds"))
if (file.exists(cache) && !isTRUE(params$refresh_data)) {
bundle <- readRDS(cache)
} else {
meta <- get_pages("country")
countries <- do.call(rbind, lapply(meta$rows, function(z) {
data.frame(id = value(z$id), country = value(z$name),
region = value(z$region$value), region_id = value(z$region$id),
income_id = value(z$incomeLevel$id))
}))
countries <- countries[!is.na(countries$region_id) &
countries$region_id != "NA" &
countries$region != "Aggregates", ]
urls <- meta$urls
series <- list()
for (nm in names(indicators)) {
z <- get_pages(paste0("country/all/indicator/", indicators[[nm]]),
paste0("&source=2&date=", earlier_year, ":", year))
series[[nm]] <- do.call(rbind, lapply(z$rows, function(r) {
data.frame(id = value(r$countryiso3code), year = as.integer(value(r$date)),
measurement = as.numeric(value(r$value)))
}))
names(series[[nm]])[3] <- nm
urls <- c(urls, z$urls)
}
panel <- Reduce(function(a, b) merge(a, b, by = c("id", "year"), all = TRUE), series)
panel <- merge(panel, countries, by = "id")
bundle <- list(panel = panel, indicators = indicators, year = year,
earlier_year = earlier_year, urls = urls,
retrieved = format(Sys.time(), tz = "UTC", usetz = TRUE))
saveRDS(bundle, cache)
}
stopifnot(identical(bundle$indicators, indicators), bundle$year == year,
bundle$earlier_year == earlier_year)
panel <- bundle$panel
stopifnot(!anyDuplicated(panel[c("id", "year")]))
data <- panel[panel$year == year, ]
data$income <- factor(data$income_id, levels = c("LIC", "LMC", "UMC", "HIC"),
labels = c("Low", "Lower middle", "Upper middle", "High"))
stopifnot(nrow(data) > 0, !anyDuplicated(data$id))
stopifnot(all(is.na(data$life) | (data$life > 0 & data$life < 130)),
all(is.na(data$urban) | (data$urban >= 0 & data$urban <= 100)),
all(is.na(data$spending) | data$spending >= 0),
all(is.na(data$gdp) | data$gdp >= 0))
life_data <- data[is.finite(data$life), ]
groups <- droplevels(data[complete.cases(data[c("life", "income")]), ])
two_groups <- droplevels(groups[groups$income %in% c("Low", "High"), ])
two_groups$income <- factor(two_groups$income, levels = c("Low", "High"))
low <- two_groups$life[two_groups$income == "Low"]
high <- two_groups$life[two_groups$income == "High"]
relation <- data[complete.cases(data[c("life", "spending")]) &
!is.na(data$spending) & data$spending > 0, ]
relation$spending100 <- relation$spending / 100
relation$log_spending <- log2(relation$spending)
old <- panel[panel$year == earlier_year, c("id", "life")]
new <- panel[panel$year == year, c("id", "country", "life")]
names(old)[2] <- "before"
names(new)[3] <- "after"
pairs <- merge(old, new, by = "id")
pairs <- pairs[complete.cases(pairs[c("before", "after")]), ]
pairs$change <- pairs$after - pairs$before
stopifnot(nrow(life_data) >= 20, nrow(relation) >= 20, nrow(pairs) >= 10,
length(low) >= 3, length(high) >= 3,
nlevels(groups$income) == 4, all(table(groups$income) >= 3))
rownames(data) <- rownames(life_data) <- rownames(relation) <- NULLHSS 2381: A Gentle Guide to Quantitative Health Research
Eight weeks of plain-language explanations, real World Bank data and small steps in R
Start here
You do not need to be good at programming to begin this course. We will ask a health question, look at real data, run a few lines of R, and explain the answer in ordinary words. You do not need to memorise every command. Learning what an answer means matters more than typing quickly.
This workbook follows the eight-week syllabus supplied by your instructor. It is a teaching companion, not a replacement for lectures or an official examination paper. The review questions below are original practice questions, not predictions of what will appear in an examination.
How each lesson works
Each worked example follows the same path:
- The question: what do we want to find out?
- The idea: what does the method do?
- A small piece of R: copy it, run it and look at the result.
- Read the result: turn the output into a useful sentence.
- Your turn: change one thing or explain one point.
- Check your understanding: open the answer only after trying.
The code is visible, but technical data-download machinery is tucked away. Most examples use only a few lines. Later examples reuse earlier objects, so work through the document in order. If you feel lost, return to the question before returning to the code.
Your eight-week map
| Week | Topic | What you will learn to do |
|---|---|---|
| 1 | Introduction to Quantitative Health Research and Types of Variables | Ask a clear question and recognise the data you need |
| 2 | Descriptive Statistics and Confidence Intervals | Describe values and explain uncertainty |
| 3 | Hypothesis Testing | Compare evidence with a stated starting claim |
| Midterm Examination 1: Lectures 1–3 | Review Weeks 1–3 | |
| 4 | Correlation and Association Between Continuous Variables | Describe how two numerical variables move together |
| 5 | Simple Linear Regression | Fit and explain a straight-line relationship |
| 6 | Analysis of Variance (ANOVA) | Compare means across more than two groups |
| 7 | Non-Parametric Methods for Continuous and Ordinal Outcomes | Use ranks when they match the question |
| 8 | Statistical Computing, Interpretation, and Reporting in Health Sciences | Complete a small, clear and reproducible report |
| Midterm Examination 2: Lectures 4–8 | Review Weeks 4–8 | |
| Final Examination: cumulative | Connect all eight weeks |
The real data we will use
We will use country and economy indicators from the World Bank. To keep the wording light, the lessons usually say country. Some World Bank entries are economies or territories rather than sovereign states. Regional totals and other aggregate entries are excluded.
Our main outcome is life expectancy at birth, measured in years. It summarises mortality conditions; it is not a list of actual ages at death. Our other numerical variables are health expenditure per person, GDP per person and the percentage of the population living in urban areas. Income group gives us categories for comparisons.
Keep one important boundary in mind: a row describes a country, not a patient. A relationship between country averages does not tell us what will happen to an individual patient. These are observational data, so we will say associated with, not caused by.
We use a fixed year, 2023 by default, rather than mixing each country’s latest available year. The earlier year, 2010, is used only for a matched life-expectancy comparison. Income labels come from metadata at download time; they are not claimed to be historical 2023 classifications.
Getting the workbook ready
Install R, RStudio and Quarto. Save this QMD in a project folder and click Render in RStudio. The first successful render needs internet access and the packages below. The data are then saved locally for later renders. The finished HTML embeds its visual resources, but this QMD does not contain an offline World Bank data snapshot.
Run this once in the R console if the packages are not installed:
install.packages(c("knitr", "jsonlite", "curl"))Students can use a rendered copy without reading the technical preparation below. An instructor can render once and share the HTML, together with the cached data and source when appropriate.
Validation note: the file’s structure and code delimiters were checked during preparation. R and Quarto execution were not available in the preparation environment. The document has not been fully executed or render-tested here. Numerical results are generated from the actual download when you render, not invented in the text.
This is plumbing, not a student learning objective. Students do not need to reproduce these functions in an examination. The downloader checks pages, retries failed requests and keeps a local cache. It stops if a required dataset is unavailable rather than making up observations.
These countries are not a random sample of patients, and the available countries are not necessarily a probability sample of countries. Confidence intervals and p-values here are teaching examples under a model that treats countries as approximately independent observations from a broader country-level process. Neighbouring countries and countries with shared histories may not be independent. The intervals also do not include every source of uncertainty in the original indicators. You can learn the calculations while being honest about these limits.
Week 1 · Introduction to Quantitative Health Research and Types of Variables
This week’s small goals
By the end of the week, you should be able to identify a research question, a row, a variable and a unit. You should also be able to run a short command without feeling that you need to understand all of R.
Quantitative research uses numerical information to describe a situation, compare groups or study relationships. A good question tells us what is measured, who or what is measured, and when. We begin with description before trying to explain why something happens.
A population is the full set we want to understand. A sample is the set observed for a study. A parameter is a number describing the population or a model. A statistic is a number calculated from the observed data. An official dataset can cover many countries and still have missing measurements or uncertainty.
1.1 · Meet one row
The question. What does a row in our dataset describe?
Think of a spreadsheet. A row is one record. A column is one variable. Here, a row is one country in the selected year. We start by looking at just six rows, not the whole dataset.
head(data[c("country", "year", "life", "spending")]) country year life spending
1 Aruba 2023 76.353 NA
2 Afghanistan 2023 66.035 59.06771
3 Angola 2023 64.617 76.07118
4 Albania 2023 79.602 590.62799
5 Andorra 2023 84.041 3605.19800
6 United Arab Emirates 2023 82.909 2401.52026
Read the result. country gives the name, year gives the time, life gives life expectancy in years and spending gives health expenditure per person in current US dollars. head() means “show me the first few rows”. It does not calculate a statistic.
Your turn. Explain what a life expectancy value of 75 would mean. This is a hypothetical reading example, not a reported result.
It means the country’s life expectancy indicator is 75 years under the mortality conditions represented by that indicator. It does not mean every resident dies at 75, or that we observed one patient aged 75.
1.2 · Recognise variable types
The question. Which columns are numerical and which are categories?
Life expectancy and expenditure are continuous measurements. Country name is a nominal label: there is no natural numerical order. Income group is ordinal: Low comes before Lower middle, but the gaps between categories are not equal numerical distances. A population count would be discrete because it counts people. A percentage, such as urban share, is numerical and bounded between 0 and 100.
str(data[c("country", "life", "spending", "urban", "income")])'data.frame': 217 obs. of 5 variables:
$ country : chr "Aruba" "Afghanistan" "Angola" "Albania" ...
$ life : num 76.4 66 64.6 79.6 84 ...
$ spending: num NA 59.1 76.1 590.6 3605.2 ...
$ urban : num 61.8 25.5 69.9 58.2 88.8 ...
$ income : Factor w/ 4 levels "Low","Lower middle",..: 4 1 2 3 4 4 3 3 4 4 ...
Read the result. num means numerical. chr means text. Factor means categories. Our income factor is stored as a categorical variable for group comparisons. Its labels have a meaningful order, but we do not assume equal distances or average the category codes.
Your turn. Would it make sense to calculate the mean of country names? What about the mean of life expectancy?
Country names are labels, so their mean has no meaning. A mean life expectancy across countries is meaningful if we clearly say that each country receives equal weight. It is not automatically the life expectancy of the world’s population.
1.3 · Read R as a sentence
The question. How do I ask R for one simple answer?
Read mean(data$life, na.rm = TRUE) as “find the mean of the life column in data, leaving out missing entries”. The dollar sign chooses a column. A function name is followed by parentheses. TRUE switches an option on. You can use a command before memorising its punctuation.
mean(data$life, na.rm = TRUE)[1] 73.79094
median(data$life, na.rm = TRUE)[1] 74.552
Read the result. The first answer is the arithmetic mean. The second is the median, or middle ordered value. R is doing the arithmetic; your job is to explain what the two answers describe. NA means unavailable, not zero.
Your turn. Change life to urban in the first command. What unit should you use in your answer?
The mean urban share is expressed as a percentage of population. It is an average of country percentages, not necessarily the percentage of all people worldwide who live in urban areas.
1.4 · Save an answer with a name
The question. How can I use an answer again?
An object is a named place to keep something. The arrow <- means “save this as”. A clear name is a reminder, not an extra statistical idea. A line beginning with # is a comment for the reader; R does not run it.
# Save the mean so we can use it again.
average_life <- mean(data$life, na.rm = TRUE)
round(average_life, 1)[1] 73.8
Read the result. round(..., 1) shows one decimal place. It changes the displayed precision, not the underlying indicator’s quality. Do not confuse a neatly rounded number with an exact measurement.
Your turn. Save the median as middle_life and display it to one decimal place.
Use middle_life <- median(data$life, na.rm = TRUE) followed by round(middle_life, 1). Names are case-sensitive: middle_life and Middle_life are different.
1.5 · Missing does not mean zero
The question. How many life expectancy measurements are available?
A missing value is a gap in information. Replacing it with zero would pretend that we observed a life expectancy of zero. We count the gaps before calculating summaries.
nrow(data)[1] 217
sum(is.na(data$life))[1] 0
sum(!is.na(data$life))[1] 217
Read the result. nrow() counts rows. is.na() asks whether each entry is missing. ! means “not”. The three answers are total rows, missing life expectancy values and observed life expectancy values. The last two should add to the first.
Your turn. Check missing expenditure values. Why might its observed count differ from life expectancy?
Use sum(is.na(data$spending)). Different indicators can have different reporting coverage. A country may have life expectancy but no expenditure estimate in that year. A missingness pattern can affect which countries enter an analysis.
1.6 · Turn a broad interest into a question
The question. Is “money and health” a research question?
It is a topic, but it is too broad for an analysis. A clearer question is: “In the selected year, how is health expenditure per person associated with country-level life expectancy?” The outcome is life expectancy. The predictor is expenditure. The unit is the country. The design is observational and cross-sectional: we compare countries at one time.
show_table(data.frame(
outcome = "Life expectancy in years",
predictor = "Health expenditure per person",
unit = "Country or economy",
selected_year = year
))| outcome | predictor | unit | selected_year |
|---|---|---|---|
| Life expectancy in years | Health expenditure per person | Country or economy | 2023 |
Read the result. This small table is a plan, not a statistical result. Planning the question prevents us from choosing a method just because we recognise its name.
Your turn. Would the question “Does increasing a patient’s spending make that patient live longer?” be answered by these data?
No. We have country indicators, not patient records or a randomised spending intervention. Moving from country averages to individual effects is an ecological inference error.
Week 1 · Pause and explain
Without using R, explain: What is one row? What is one continuous variable? Why is income group not a continuous measurement? What does NA mean? Why is association not the same as causation?
A useful sentence: “Our dataset describes countries in one selected year; it does not follow individual patients.”
Week 2 · Descriptive Statistics and Confidence Intervals
This week’s small goals
Describe a typical value, describe how spread out values are, choose a simple graph and explain what a confidence interval is trying to tell us. A summary is a short description, not a judgement about whether a country is good or bad.
The mean uses every value. The median is the middle ordered value. The standard deviation, or SD, describes how spread out observations are around their mean. The interquartile range, or IQR, spans the middle half of observations. A standard error, or SE, concerns uncertainty in an estimated quantity. SD and SE answer different questions.
2.1 · Find a typical value
The question. What is a typical country-level life expectancy?
We use both the mean and median first. If they are similar, the distribution may be reasonably balanced. If they differ, a graph can help explain why. Neither number describes every country.
mean(life_data$life)[1] 73.79094
median(life_data$life)[1] 74.552
summary(life_data$life) Min. 1st Qu. Median Mean 3rd Qu. Max.
54.46 68.48 74.55 73.79 79.59 86.37
Read the result. summary() adds the minimum, quartiles and maximum. The first quartile marks the 25th percentile; the third marks the 75th percentile. A percentile is a position in an ordered distribution, not a percentage change.
Your turn. Which number would be less affected by one extremely large observation?
The median is usually less affected because it depends on ordered position rather than the size of every value. This does not make the mean wrong; it means the summaries emphasise different features.
2.2 · Describe spread
The question. Do all countries have similar life expectancy?
Two datasets can have the same mean and very different spreads. SD keeps the original unit, years. Variance is SD squared and therefore has squared units. The range uses only the smallest and largest values, so it can be sensitive to extremes.
sd(life_data$life)[1] 7.12287
IQR(life_data$life)[1] 11.11
range(life_data$life)[1] 54.462 86.372
Read the result. The SD describes variation across the observed countries. The IQR describes the width of the middle half. The range shows the endpoints. Do not describe the SD as the uncertainty around the mean; that is the SE’s role.
Your turn. If SD were 6 years in a hypothetical dataset, would that mean every country is exactly 6 years from the mean?
No. SD summarises spread; it is not a fixed distance shared by every observation. A graph shows the actual distribution more clearly.
2.3 · See the distribution
The question. What can a histogram show that a mean cannot?
A histogram groups numerical values into intervals and counts how many fall in each interval. We look for a balanced shape, a long tail, gaps or unusual values. The choice of intervals affects appearance, so avoid treating every small bump as a scientific finding.
hist(life_data$life,
main = "Life expectancy across countries",
xlab = "Life expectancy (years)",
ylab = "Number of countries")Read the result. The horizontal axis is life expectancy. The vertical axis counts countries. A tall bar means many observed countries fall in that interval. It does not mean the people in those countries all have the same lifespan.
Your turn. Change life_data$life to the observed expenditure values in data$spending. Also change the title and horizontal-axis label.
Use hist(data$spending, main = "Health expenditure across countries", xlab = "Current US dollars per person"). The distribution may have a long right tail; inspect the actual output before describing it.
2.4 · Use a boxplot without deleting points
The question. What is an unusual value?
A boxplot gives a compact view of the median and middle half of values. Points beyond the usual whisker rule are flagged for attention. A flag is not proof of a data error. A real country can genuinely differ from the others.
boxplot(life_data$life,
main = "Life expectancy: a compact summary",
ylab = "Life expectancy (years)")Read the result. The line in the box is the median. The box spans the first to third quartiles. The usual whisker rule uses 1.5 times the IQR. Before removing anything, check the source, the country context and the analysis question.
Your turn. Give one reason to retain an unusual value and one reason to investigate it.
Retain it if it is a valid measurement relevant to the question. Investigate it if the unit, country identifier or source appears wrong. “It makes my p-value larger” is not a valid reason for deletion.
2.5 · Compare country summaries by group
The question. How can we summarise life expectancy within each income group?
A group summary answers a more specific question than an overall mean. We use the same numerical outcome within each category. aggregate() means “calculate this summary separately for each group”. The tilde ~ separates the outcome from the grouping variable.
aggregate(life ~ income, data = groups, FUN = mean) income life
1 Low 64.20236
2 Lower middle 67.90955
3 Upper middle 73.76977
4 High 79.80710
aggregate(life ~ income, data = groups, FUN = sd) income life
1 Low 4.600719
2 Lower middle 4.896255
3 Upper middle 3.778299
4 High 4.177049
table(groups$income)
Low Lower middle Upper middle High
25 47 59 86
Read the result. The first table gives means, the second SDs and the third counts. Read them together. A group with a small count and large spread will generally have a less precise mean under the independent-observation model. Income labels are current metadata labels.
Your turn. Calculate group medians instead of means.
Change FUN = mean to FUN = median. You do not need a new dataset or a new grouping variable.
2.6 · Separate SD from SE
The question. How precisely have we estimated a mean?
Imagine repeatedly observing comparable independent countries under a model. The estimated mean would vary from one set to another. The SE describes that variation. The familiar formula is SD divided by the square root of the number of observations. It is not a formula for removing bias.
n <- nrow(life_data)
life_sd <- sd(life_data$life)
life_se <- life_sd / sqrt(n)
show_table(data.frame(n = n, SD_years = life_sd, SE_years = life_se))| n | SD_years | SE_years |
|---|---|---|
| 217 | 7.12 | 0.48 |
Read the result. The SD is about differences between observed countries. The SE is about uncertainty in the mean under the model. A larger sample can reduce SE without reducing SD. Dependence between countries can make this simple SE misleading.
Your turn. If sample size became four times as large with the same SD and independent observations, what would happen to the SE?
It would be halved because the square root of four is two. This is a mathematical comparison, not a claim that we can create additional independent countries.
2.7 · Build and read a confidence interval
The question. What range is compatible with our estimated mean?
A confidence interval combines an estimate with uncertainty. For a mean, R can calculate a t-based interval. We use t.test() here only as a convenient calculator for the interval; the hypothesis-test interpretation comes next week. Look at conf.int, not the p-value yet.
mean_result <- t.test(life_data$life)
mean_result$estimatemean of x
73.79094
mean_result$conf.int[1] 72.83790 74.74399
attr(,"conf.level")
[1] 0.95
Read the result. The interval concerns a modelled mean across comparable country-level observations. It is not an interval containing 95% of countries or patients. In repeated use under the assumptions, 95% intervals from this procedure would cover the fixed target mean about 95% of the time.
Your turn. Calculate a 99% interval. Should it be narrower or wider?
Use t.test(life_data$life, conf.level = 0.99)$conf.int. It is wider because greater coverage requires a wider range. It is not automatically a better answer; the confidence level should match the analysis plan.
2.8 · Make a small descriptive table
The question. How can we put the main summaries in one readable place?
A table should save the reader effort. Use clear labels and units, show the number observed, and round only for presentation. We keep this table small instead of printing every possible statistic.
description <- data.frame(
Countries = nrow(life_data),
Mean_years = mean(life_data$life),
SD_years = sd(life_data$life),
Median_years = median(life_data$life),
IQR_years = IQR(life_data$life)
)
show_table(description)| Countries | Mean_years | SD_years | Median_years | IQR_years |
|---|---|---|---|---|
| 217 | 73.79 | 7.12 | 74.55 | 11.11 |
Read the result. This is a country-weighted description: every country contributes one observation. It is not a population-weighted world life expectancy. A simple average of national life expectancies does not reconstruct a global life table.
Your turn. Write one sentence containing the mean, SD, year and number of countries. Use your rendered output.
A suitable structure is: “In [year], mean life expectancy among [n] observed countries was [mean] years (SD [SD]).” The brackets are writing prompts; replace them with actual output rather than guessing.
Week 2 · Pause and explain
Explain the difference between mean and median, SD and SE, and a confidence interval and the spread of individual observations. If you can explain those three distinctions without looking at the code, you have learned the most important part of this week.
Week 3 · Hypothesis Testing
This week’s small goals
State a starting claim, choose a comparison that matches the design, read a p-value correctly and report the size of a difference. We introduce one-sample, independent-group and paired t-tests as practical examples of hypothesis testing.
A null hypothesis is the starting claim tested by a method. An alternative hypothesis is the competing statement. A two-sided alternative allows a difference in either direction. Choose the direction and the significance level before inspecting the result.
A p-value asks: if the null model and its assumptions were correct, how unusual would a result at least this extreme be? It does not give the probability that the null is true. It does not measure the size or importance of a difference.
A Type I error is rejecting a true null. A Type II error is failing to reject a false null. Power is the probability of detecting a specified difference when that alternative is true. These are properties of a procedure under stated conditions, not guarantees for one study.
3.1 · Test one mean against a stated reference
The question. Is the mean different from a hypothetical 70-year reference?
The value 70 is a teaching choice, not a World Bank world mean or an official health target. The null says the modelled mean is 70 years. The alternative says it is different from 70. The one-sample t-test compares the observed difference with its estimated SE.
one_test <- t.test(life_data$life, mu = 70)
one_test
One Sample t-test
data: life_data$life
t = 7.8401, df = 216, p-value = 2.036e-13
alternative hypothesis: true mean is not equal to 70
95 percent confidence interval:
72.83790 74.74399
sample estimates:
mean of x
73.79094
Read the result. Read the estimated mean, the interval and then the p-value. The interval printed here is for the mean itself. To express the interval as a difference from 70, subtract 70 from both endpoints. The exact small-sample t reference assumes independent normal observations; extreme values and dependence deserve attention.
Your turn. Change the reference to 75. Does the observed mean change?
No. The data have not changed. The difference from the reference, test statistic and p-value change. A benchmark should be chosen for a scientific reason before testing, not changed until a preferred answer appears.
3.2 · Extract only the pieces we need
The question. How can I avoid being overwhelmed by a full test output?
R stores the result as a named object. We can ask for one part at a time. The p-value is only one part of the answer, so we keep the mean and interval alongside it.
one_test$estimatemean of x
73.79094
one_test$conf.int[1] 72.83790 74.74399
attr(,"conf.level")
[1] 0.95
one_test$p.value[1] 2.036479e-13
mean(life_data$life) - 70[1] 3.790943
Read the result. The last line is the difference from the reference, in years. A positive difference means above 70; a negative difference means below 70. A small p-value does not say whether that difference is important for health policy.
Your turn. If a hypothetical test had p = 0.03, would it mean a 3% chance that the null is true?
No. It is a probability about results under the null model, not a probability assigned to the null hypothesis. This distinction matters even when the arithmetic is correct.
3.3 · Compare two different groups
The question. Do low-income and high-income countries have different means?
These groups contain different countries, so the observations are not paired. We use Welch’s t-test, which does not require equal group variances. The contrast is explicitly Low minus High. The labels describe current metadata classifications, not a randomised intervention.
welch_test <- t.test(low, high)
mean(low) - mean(high)[1] -15.60474
welch_test$conf.int[1] -17.68188 -13.52759
attr(,"conf.level")
[1] 0.95
welch_test$p.value[1] 2.417591e-17
Read the result. The difference and interval are in years. A negative contrast means the observed low-income mean is lower. The test still needs a defensible independent-observation framework. Welch’s method handles unequal variances; it does not remove confounding or country dependence.
Your turn. What would happen to the sign of the contrast if you ran t.test(high, low)?
The sign would reverse and the interval endpoints would reverse direction. The two-sided p-value would stay the same. Always name the contrast so the reader knows what a positive or negative number means.
3.4 · Compare the same countries at two times
The question. Has life expectancy changed within matched countries?
Now the same country appears twice, once in the earlier year and once in the selected year. The paired test works with each country’s change. We matched by country identifier during preparation, not by row order. The direction is later minus earlier.
paired_test <- t.test(pairs$after, pairs$before, paired = TRUE)
mean(pairs$change)[1] 3.015802
paired_test$conf.int[1] 2.633461 3.398144
attr(,"conf.level")
[1] 0.95
paired_test$p.value[1] 4.459023e-37
Read the result. The interval describes mean within-country change. The normality assumption for the paired t-test concerns the differences, not a requirement that both years separately look normal. Different pairs should be independent under the model. A temporal change is not automatically a programme effect.
Your turn. Why would t.test(pairs$after, pairs$before) without paired = TRUE be the wrong design here?
It would treat the two sets as independent and ignore that each earlier value belongs to the same country as a later value. The paired analysis uses the matching information and the SD of changes.
3.5 · Look at the changes before trusting a test
The question. Are the paired differences reasonably well behaved?
A test command cannot tell us whether the data were matched correctly. We inspect the differences and look for strong skewness or unusual changes. A graph supports judgement; it does not prove every assumption.
hist(pairs$change,
main = "Change within matched countries",
xlab = "Later minus earlier life expectancy (years)")
abline(v = 0, lty = 2)Read the result. Zero means no change. Positive values mean higher life expectancy in the later year. A very unusual change should prompt a source and context check, not automatic deletion. A signed-rank alternative is introduced in Week 7.
Your turn. Could a paired test show a mean increase even if some countries decreased?
Yes. The test concerns the average change. Individual countries can move in different directions. Reporting only the average can hide important variation, so keep the graph and spread in mind.
3.6 · Keep practical importance separate
The question. Is a small p-value enough for a useful conclusion?
No. A result can be precisely estimated but small in practical terms. A wide interval can include both little difference and a meaningful difference. We report the estimated difference and its uncertainty before discussing a threshold such as 0.05.
show_table(data.frame(
Contrast = "Low minus High",
Difference_years = mean(low) - mean(high),
Lower_95 = welch_test$conf.int[1],
Upper_95 = welch_test$conf.int[2],
p_value = pretty_p(welch_test$p.value)
))| Contrast | Difference_years | Lower_95 | Upper_95 | p_value |
|---|---|---|---|---|
| Low minus High | -15.6 | -17.68 | -13.53 | <0.001 |
Read the result. A confidence interval that includes zero is compatible with no mean difference under the model, but it may also include differences that matter. “Not statistically significant” does not prove equality. A p-value should not replace a health interpretation.
Your turn. Why should you not try many references and report only the smallest p-value?
That hides the number of opportunities to find a small p-value and can exaggerate evidence. Plan the main question, report exploratory work honestly and address multiple comparisons when appropriate.
Midterm Examination 1 · Review of Lectures 1–3
A short practice paper
- Identify the unit of observation and give one continuous, one nominal and one ordinal variable.
- Explain why replacing missing expenditure with zero is misleading.
- Choose mean/SD or median/IQR for a strongly right-skewed distribution and explain your choice.
- Explain why SD and SE are not interchangeable.
- Explain a 95% confidence interval without saying that it contains 95% of countries.
- State the null and alternative for a mean compared with a pre-specified reference.
- Choose an independent or paired test for each: two different income groups; the same countries at two times.
- Correct this sentence: “p = 0.02 means there is a 2% probability that the null is true.”
- Explain why a non-significant result does not prove equality.
- Write a two-sentence result using the actual independent-group output.
- A country in the selected year; life expectancy is continuous, country name nominal and income category ordinal.
- Zero is a measurement, not a missingness label.
- Median/IQR can give a less extreme-value-sensitive description; also inspect the graph and report the scientific context.
- SD describes observational spread; SE describes estimator uncertainty under assumptions.
- The procedure has repeated-use coverage under its model; the interval concerns a mean, not individual countries.
- Null: the target mean equals the stated reference. Two-sided alternative: it differs.
- Different groups: independent Welch test. Same countries over time: paired test.
- The p-value concerns how unusual the results are under the null model, not the probability that the null is true.
- The interval may be wide and include important differences.
- Name the groups, year, direction, sample sizes, mean difference, CI and the country-level limitation. Do not invent numerical answers.
Week 4 · Correlation and Association Between Continuous Variables
This week’s small goals
Draw a scatterplot, recognise the direction and shape of an association, calculate Pearson and Spearman correlations and explain why correlation is not causation.
Two measurements belong together when they come from the same country. Correlation uses those pairs. It is different from the paired t-test: the paired t-test asks about a mean change, while correlation asks how two variables vary together.
4.1 · Look before calculating
The question. Do higher-spending countries tend to have higher life expectancy?
A scatterplot places one country at each point. The horizontal position is expenditure and the vertical position is life expectancy. Before asking for a coefficient, look for direction, curvature, clusters and unusual points.
plot(relation$spending, relation$life,
xlab = "Health expenditure (current US dollars per person)",
ylab = "Life expectancy (years)",
main = "Two measurements for each country", pch = 16)Read the result. An upward pattern suggests a positive association; a downward pattern suggests a negative association. A curve can be important even if a straight-line summary is weak. The graph does not identify why the relationship exists.
Your turn. Would swapping the axes change which countries are included?
No, if you use the same complete pairs. It changes how the graph is read, but not the set of observations. Correlation itself is symmetric; regression prediction is not.
4.2 · Measure a linear relationship
The question. What does Pearson correlation tell us?
Pearson’s r summarises the direction and strength of a linear association. It lies between -1 and 1 and has no measurement unit. A value near zero means little linear association, not necessarily no relationship of any kind.
pearson <- cor.test(relation$spending, relation$life, method = "pearson")
pearson$estimate cor
0.6264554
pearson$conf.int[1] 0.5317740 0.7056698
attr(,"conf.level")
[1] 0.95
pearson$p.value[1] 3.247429e-22
Read the result. Read the coefficient before the p-value. The interval concerns the modelled correlation. The usual exact small-sample test uses an independent-pair bivariate-normal framework. Strong outliers and curvature can make a simple Pearson summary misleading.
Your turn. If r were 0.60 in a hypothetical study, would that mean expenditure explains 60% of life expectancy?
No. r is a correlation coefficient, not a percentage explained. In a simple linear regression with an intercept on the same rows, R-squared equals r squared. Even then, “explained variation” is not a causal fraction.
4.3 · Describe a monotonic relationship
The question. What if the relationship rises but is not a straight line?
A monotonic relationship tends to move in one direction even if the slope changes. Spearman correlation works with ranks rather than the original distances. It can be useful when a monotonic pattern is more appropriate than a linear one.
spearman <- cor.test(relation$spending, relation$life,
method = "spearman", exact = FALSE)
spearman$estimate rho
0.8409345
spearman$p.value[1] 2.707007e-52
Read the result. Spearman’s coefficient is also between -1 and 1. We request an approximate p-value explicitly. Rank methods are not assumption-free: independence and a suitable inferential question still matter. Ties can affect the calculation.
Your turn. Why might Pearson and Spearman give different values for the same countries?
Pearson uses numerical distances and linearity; Spearman uses ordered positions and monotonicity. Curvature and extreme values can therefore affect them differently. A difference is a reason to inspect the graph, not to select whichever p-value is smaller.
4.4 · Keep the denominator visible
The question. How many countries actually enter a correlation?
Correlation requires both measurements. The number with life expectancy alone may be larger than the number with both life expectancy and expenditure. We use the same complete-pair dataset for the two correlations so the comparison is fair.
nrow(data)[1] 217
nrow(life_data)[1] 217
nrow(relation)[1] 191
Read the result. The counts refer to all selected-year countries, countries with observed life expectancy, and countries with observed life expectancy plus positive expenditure. They need not match. A complete-case analysis can represent a different mix of countries.
Your turn. Can a large number of countries remove bias caused by missing data?
No. More observations can improve precision under a model, but they do not automatically fix selection bias. Report missingness and consider which countries may be absent.
4.5 · Use a log scale gently
The question. Why do some graphs use log expenditure?
Expenditure can span a very wide range. A base-2 log scale puts equal doublings at equal distances: 100 to 200 and 1,000 to 2,000 each become a one-unit increase. Those are hypothetical scale examples, not claimed country measurements. Only positive expenditure can be logged.
plot(relation$log_spending, relation$life,
xlab = "Log2 expenditure: one step is a doubling",
ylab = "Life expectancy (years)", pch = 16)cor(relation$log_spending, relation$life)[1] 0.8277551
Read the result. This is a different predictor scale. It can make a pattern easier to see, but it does not guarantee a straight line, normal errors or causality. Label the transformed axis so the reader knows what changed.
Your turn. If expenditure quadruples, how many base-2 log units does it increase?
Two. Quadrupling is two doublings. We will use this interpretation again in the optional simple-regression example next week.
Week 4 · Pause and explain
Explain the difference between a positive association and a causal effect. Then explain why an r near zero does not rule out a curved relationship. Finally, describe why we looked at the graph before reading the p-value.
Week 5 · Simple Linear Regression
This week’s small goals
Understand a fitted line, interpret an intercept and slope, describe residuals, read R-squared and distinguish a confidence interval for a mean from a prediction interval.
A simple regression has one predictor and one continuous outcome. In words: predicted outcome = intercept + slope × predictor. The line is a summary of a relationship, not a rule that every country must follow. We keep multiple regression and complex modelling outside the core workbook because they are not listed in this syllabus.
5.1 · Fit the first line
The question. How can we describe life expectancy using one expenditure predictor?
To keep the coefficient readable, we express expenditure in hundreds of dollars. Dividing by 100 changes the predictor’s unit; it does not change the underlying observations. lm() means linear model. Read life ~ spending100 as “life expectancy modelled using expenditure in hundreds”.
model <- lm(life ~ spending100, data = relation)
coef(model)(Intercept) spending100
70.3130597 0.1935423
Read the result. The intercept is the fitted life expectancy when expenditure is zero. The slope is the fitted difference in life expectancy associated with an extra 100 current US dollars per person. The intercept may be outside the useful data range and need not have a realistic health interpretation.
Your turn. If a hypothetical slope were 0.2, what would it mean?
An extra 100 dollars per person would be associated with 0.2 additional years in the fitted country-level mean. This is a hypothetical coefficient example, not our downloaded result, and it is not a causal intervention estimate.
5.2 · Put the line on the graph
The question. Does a straight line look like a reasonable summary?
A fitted line is easier to understand when you can see the points around it. Ordinary least squares chooses the line that minimises the sum of squared vertical residuals. It does not try to pass through every point.
plot(relation$spending100, relation$life,
xlab = "Health expenditure (hundreds of current US dollars per person)",
ylab = "Life expectancy (years)", pch = 16)
abline(model, lwd = 2)Read the result. Inspect whether the points curve away from the line or become more spread out as expenditure rises. If the line is a poor summary, a precise coefficient does not make the model appropriate. Keep the health question and the graph together.
Your turn. What does a point above the line tell you?
That country’s observed life expectancy is higher than the fitted value at its expenditure. The difference is a positive residual. It does not prove that the country has a better health system.
5.3 · Read only the important output first
The question. Which parts of summary(model) matter for a beginner?
Start with the slope estimate, its SE, its interval and the number of countries. The full output includes more information, but you do not need to read every symbol at once. A coefficient interval describes uncertainty in a model parameter, not the spread of countries.
summary(model)$coefficients Estimate Std. Error t value Pr(>|t|)
(Intercept) 70.3130597 0.47805376 147.08191 7.327283e-197
spending100 0.1935423 0.01751649 11.04915 3.247429e-22
confint(model) 2.5 % 97.5 %
(Intercept) 69.3700533 71.2560662
spending100 0.1589894 0.2280953
nobs(model)[1] 191
Read the result. The slope’s t-test asks whether its modelled value is zero. The conventional interval and test assume the linear model is appropriately specified, with independent errors and constant variance; normal errors support exact small-sample inference. The predictor itself does not need to be normally distributed.
Your turn. Does a slope interval that excludes zero prove that expenditure causes longer life?
No. GDP, living conditions, education, reverse causation and other factors may help explain the relationship. This is an unadjusted country-level association from an observational design.
5.4 · Understand a residual
The question. How far is each observed value from the line?
A fitted value is the model’s prediction for an observed country. A residual is observed minus fitted. Residuals help us see what the line misses. They are not the same thing as measurement error in the original data.
first_five <- data.frame(
Country = relation$country[1:5],
Observed = relation$life[1:5],
Fitted = fitted(model)[1:5],
Residual = residuals(model)[1:5]
)
show_table(first_five)| Country | Observed | Fitted | Residual |
|---|---|---|---|
| Afghanistan | 66.03 | 70.43 | -4.39 |
| Angola | 64.62 | 70.46 | -5.84 |
| Albania | 79.60 | 71.46 | 8.15 |
| Andorra | 84.04 | 77.29 | 6.75 |
| United Arab Emirates | 82.91 | 74.96 | 7.95 |
Read the result. A positive residual means observed is above fitted; a negative residual means below fitted. A large residual deserves investigation, but it is not automatically a mistake. A model can miss a valid observation.
Your turn. Check that observed minus fitted equals residual for one row.
Subtract the displayed fitted value from the observed value. Small differences can occur because the table rounds values for display. R keeps more precision internally.
5.5 · Inspect the model without a long checklist
The question. What patterns in residuals should make us pause?
A residual plot should not show a strong systematic curve if a straight-line mean is appropriate. A fan shape suggests changing spread. A Q-Q plot compares residuals with a normal reference. Neither graph can verify independence between countries.
plot(fitted(model), residuals(model),
xlab = "Fitted life expectancy", ylab = "Residual", pch = 16)
abline(h = 0, lty = 2)qqnorm(residuals(model))
qqline(residuals(model))Read the result. Look for clear patterns rather than demanding a perfect picture. Do not switch methods solely because a normality test crosses 0.05. A country with an unusual predictor value can have high leverage; a country that strongly changes the fit is influential. Those are not identical to having a large residual.
Your turn. Would a normal-looking predictor histogram prove the regression assumptions?
No. The normal-error assumption concerns errors conditional on the predictor, not the predictor’s marginal distribution. Correct design and independence cannot be read from a histogram.
5.6 · Explain R-squared carefully
The question. How much observed variation does the fitted line account for?
R-squared compares the line’s squared residuals with those from predicting the same overall mean for every country. It summarises fit in this dataset. It is not a percentage of life expectancy caused by expenditure.
summary(model)$r.squared[1] 0.3924464
summary(model)$sigma[1] 5.621559
cor(relation$spending100, relation$life)^2[1] 0.3924464
Read the result. For simple regression with an intercept on the same rows, R-squared equals Pearson r squared. sigma is the residual standard error in years. A high R-squared does not guarantee valid assumptions or accurate prediction for new countries.
Your turn. Why should we not call the residual standard error a causal effect?
It describes typical model discrepancy in outcome units. It is not a change caused by the predictor. Fit statistics and effect estimates answer different questions.
5.7 · Make a prediction within the observed range
The question. What does the model predict at a typical observed expenditure?
We use the median observed expenditure rather than inventing an extreme value. A confidence interval concerns the fitted mean at that predictor value. A prediction interval concerns a new country-level observation under the model and includes additional residual variation.
profile <- data.frame(spending100 = median(relation$spending100))
predict(model, newdata = profile, interval = "confidence") fit lwr upr
1 71.22349 70.3548 72.09218
predict(model, newdata = profile, interval = "prediction") fit lwr upr
1 71.22349 60.10046 82.34652
Read the result. The prediction interval is wider because individual country outcomes vary around the fitted mean. It is not an interval for a patient’s lifespan. Both intervals rely on the model assumptions and can be misleading when the mean or variance model is poor.
Your turn. Why is predicting far beyond the observed expenditure range risky?
That is extrapolation. The observed pattern may not continue outside the data range. A mathematical line can produce a number where the data provide little scientific support.
5.8 · Optional: interpret a doubling
The question. Can we fit a simple model using log expenditure?
This remains simple regression because it still has one predictor. The predictor is now log base 2 expenditure. The slope is a fitted difference in life expectancy per doubling of expenditure, rather than per extra 100 dollars. Compare the graph and residuals, not only the p-value.
log_model <- lm(life ~ log_spending, data = relation)
coef(log_model) (Intercept) log_spending
51.437499 2.472954
confint(log_model) 2.5 % 97.5 %
(Intercept) 49.253966 53.621032
log_spending 2.232432 2.713475
Read the result. The outcome remains in years. A one-unit increase in log_spending is a doubling. A quadrupling corresponds to two slope units in this model. The transformation changes the assumed relationship; it is not a trick for making a result significant.
Your turn. Is the intercept here the predicted outcome at zero dollars?
No. A log2 predictor of zero corresponds to one dollar because log2(1) = 0. That may be outside the observed useful range. Do not give the intercept a health interpretation without checking the data.
Week 5 · Pause and explain
Explain a slope in its correct units. Explain why a residual is not a causal effect. Explain why R-squared is not a percentage caused. Then tell a classmate why a prediction interval is wider than a fitted-mean interval.
Week 6 · Analysis of Variance (ANOVA)
This week’s small goals
Compare more than two group means, explain an overall test, distinguish classical and Welch ANOVA and understand why follow-up comparisons need care.
ANOVA stands for analysis of variance, but the main question here concerns means. It compares variation between group means with variation within groups. The null says all group means are equal. The alternative says at least one differs; it does not say every pair differs.
6.1 · Look at all four groups
The question. What do the group distributions look like?
Before testing, compare centres, spreads and counts. A boxplot is a starting picture. The points add information about individual countries and unequal group sizes. We use current income labels consistently throughout the workbook.
boxplot(life ~ income, data = groups,
xlab = "Current income group", ylab = "Life expectancy (years)",
names = c("Low", "Lower middle", "Upper middle", "High"),
outline = FALSE)
stripchart(life ~ income, data = groups, vertical = TRUE,
method = "jitter", add = TRUE, pch = 16, cex = 0.5)Read the result. The graph can show overlap even when means differ. It can also show different spreads. The small horizontal jitter only separates overlapping dots visually; it does not alter the measured life expectancy values.
Your turn. Can two groups overlap substantially and still have different estimated means?
Yes. Overlap among individual observations is not the same as uncertainty in a mean difference. Look at the estimated contrast and its interval, not just whether boxes overlap.
6.2 · Run classical one-way ANOVA
The question. Are all four modelled means equal?
The classical method uses a common within-group variance. Its F statistic is a ratio of between-group to within-group mean squares. Larger ratios can be less compatible with equal means under the model. The exact reference assumes independent observations, normal errors within groups and equal variances.
anova_model <- aov(life ~ income, data = groups)
summary(anova_model) Df Sum Sq Mean Sq F value Pr(>F)
income 3 7037 2345.7 127.4 <2e-16 ***
Residuals 213 3922 18.4
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Read the result. The row for income is the overall group comparison. The residual row describes variation left within groups. The p-value is not the probability that all means are equal. An overall rejection does not identify which pairs differ.
Your turn. Why not simply run six unadjusted t-tests and report any p-value below 0.05?
Repeated opportunities for a small p-value increase false-positive risk for the family of comparisons. Use a planned comparison strategy and an appropriate multiplicity method.
6.3 · Use Welch ANOVA when variances differ
The question. What if group spreads and sizes are unequal?
Welch ANOVA compares means without imposing a common group variance. It extends the idea of Welch’s two-group t-test. It still relies on a suitable independent-unit model and does not solve confounding or country dependence.
welch_anova <- oneway.test(life ~ income, data = groups, var.equal = FALSE)
welch_anova
One-way analysis of means (not assuming equal variances)
data: life and income
F = 115.17, num df = 3.000, denom df = 81.929, p-value < 2.2e-16
Read the result. Read this as an overall unequal-variance comparison of means. It is not a rank test. Classical and Welch results can differ because they use different variance assumptions. Do not select whichever gives the smaller p-value.
Your turn. Would a non-significant test of equal variances prove the variances are equal?
No. A test can lack power. Use the design, group sizes, spread and substantive judgement rather than treating a preliminary variance p-value as a mechanical gatekeeper.
6.4 · Follow up a classical ANOVA
The question. Which pairs differ under the classical model?
Tukey’s method provides pairwise mean differences and simultaneous intervals for the family of comparisons under the classical ANOVA framework. Read the direction in each row label. A negative difference is meaningful only when you know which group is subtracted from which.
TukeyHSD(anova_model) Tukey multiple comparisons of means
95% family-wise confidence level
Fit: aov(formula = life ~ income, data = groups)
$income
diff lwr upr p adj
Lower middle-Low 3.707189 0.9567336 6.457643 0.0032663
Upper middle-Low 9.567413 6.9158571 12.218970 0.0000000
High-Low 15.604736 13.0800963 18.129377 0.0000000
Upper middle-Lower middle 5.860225 3.6878478 8.032602 0.0000000
High-Lower middle 11.897548 9.8820382 13.913058 0.0000000
High-Upper middle 6.037323 4.1590185 7.915627 0.0000000
Read the result. diff is a mean difference, lwr and upr are simultaneous interval limits and p adj is an adjusted p-value. Tukey’s classical variance assumptions do not disappear if you first ran Welch ANOVA. This is the follow-up for the classical framework.
Your turn. If an interval includes zero, does that prove the two groups have equal means?
No. It indicates that zero is among the values compatible with that interval procedure. The interval may also include meaningful positive or negative differences.
6.5 · Follow up an unequal-variance comparison
The question. How can we compare pairs without pooling variances?
We can use pairwise Welch t-tests and adjust their p-values using Holm’s method. Holm addresses family-wise false-positive risk for the specified comparison family. It does not correct poor data quality or make an observational contrast causal.
pairwise.t.test(groups$life, groups$income,
pool.sd = FALSE, p.adjust.method = "holm")
Pairwise comparisons using t tests with non-pooled SD
data: groups$life and groups$income
Low Lower middle Upper middle
Lower middle 0.0025 - -
Upper middle 9.8e-11 3.3e-09 -
High < 2e-16 < 2e-16 6.2e-15
P value adjustment method: holm
Read the result. The table contains adjusted p-values, not mean differences or confidence intervals. Use group summaries and a clearly named individual contrast to describe magnitude. Do not call an ordinary pairwise 95% interval a simultaneous interval just because its p-value was adjusted elsewhere.
Your turn. Why is the size of a difference still needed after adjustment?
An adjusted p-value says nothing about the number of years separating means. Health interpretation needs the raw difference, uncertainty, group sizes and context.
6.6 · Optional: describe the amount of group-associated variation
The question. Can we describe more than a p-value?
Eta squared is the between-group sum of squares divided by the total sum of squares in the classical one-way model. It is a descriptive proportion of variation associated with the grouping in this analysis. It is not a causal proportion determined by income.
anova_table <- summary(anova_model)[[1]]
eta_squared <- anova_table[1, "Sum Sq"] / sum(anova_table[, "Sum Sq"])
eta_squared[1] 0.6421321
Read the result. The result lies between zero and one for this model. It can be influenced by group separation and within-group spread. Raw mean differences in years remain easier to connect to the health question.
Your turn. Can eta squared tell us what would happen if a country changed income group?
No. It summarises observed group-associated variation, not the effect of changing a country’s income category. A causal question requires a different design and stronger assumptions.
Week 6 · Pause and explain
Explain why ANOVA compares means despite its name. Explain why an overall result is not a list of pairwise results. Then describe the difference between Tukey follow-ups under the classical model and pairwise Welch tests with Holm adjustment.
Week 7 · Non-Parametric Methods for Continuous and Ordinal Outcomes
This week’s small goals
Understand ranks, choose a rank method that matches independent or paired data, interpret ordinal outcomes carefully and avoid calling every rank test a test of medians.
A rank is a position in an ordered list. Rank methods often use less information about numerical distances than mean-based methods. They can be useful for ordinal measurements or for a rank-based question about continuous measurements. They are not assumption-free replacements for every t-test or ANOVA.
An ordinal outcome has ordered categories but does not guarantee equal spacing. A patient-reported rating such as mild, moderate and severe is a conceptual example; we do not fabricate patient data for this workbook. To practise with real observations, we will later derive clearly labelled ordered bands from observed life expectancy. Those bands are teaching categories, not a validated health scale.
7.1 · See what a rank does
The question. How can we replace values with ordered positions?
We take a few real observed values and order them. R assigns average ranks when values tie. Ranking preserves order but discards the original distance between measurements. That trade-off changes what the analysis emphasises.
example_values <- life_data$life[1:6]
show_table(data.frame(Value_years = example_values, Rank = rank(example_values)))| Value_years | Rank |
|---|---|
| 76.35 | 3 |
| 66.03 | 2 |
| 64.62 | 1 |
| 79.60 | 4 |
| 84.04 | 6 |
| 82.91 | 5 |
Read the result. A larger rank means a larger value within this small set. Rank 6 is not six years. Two values far apart and two values close together can be adjacent ranks. This is why a rank analysis is not simply the same mean analysis with a different name.
Your turn. If two values tie, should one arbitrarily receive the higher rank?
No. R normally assigns their average rank. Ties affect the reference calculation, so we explicitly use approximate Wilcoxon and Spearman tests in this workbook.
7.2 · Compare two independent groups with ranks
The question. Do low-income and high-income countries differ in rank location?
The Wilcoxon rank-sum test is also called the Mann–Whitney test. It compares independent groups using ranks. Under an appropriate common-shape location-shift model, it can support a location interpretation. Without that extra structure, it is not simply a universal test of medians.
rank_test <- wilcox.test(low, high, exact = FALSE)
rank_test
Wilcoxon rank sum test with continuity correction
data: low and high
W = 27, p-value = 1.418e-13
alternative hypothesis: true location shift is not equal to 0
median(low)[1] 63.651
median(high)[1] 81.1335
Read the result. The medians describe the groups, while the test uses the ranks of all observations. Different shapes or spreads can complicate a median interpretation. The groups remain observational country groups; the test does not identify an income effect.
Your turn. Why should we not write “the medians differ” solely because the rank-sum p-value is small?
The rank test’s interpretation depends on distributional assumptions. If shapes or spreads differ, a rank difference is not automatically a difference in medians. Describe the distributions and state the location-shift assumption when using that interpretation.
7.3 · Analyse paired changes with signed ranks
The question. Can we use ranks while keeping the matching?
The Wilcoxon signed-rank test uses the signs and ranks of within-country differences. It is different from the independent rank-sum test. A symmetry assumption for the difference distribution supports its usual location interpretation.
signed_test <- wilcox.test(pairs$after, pairs$before,
paired = TRUE, exact = FALSE)
signed_test
Wilcoxon signed rank test with continuity correction
data: pairs$after and pairs$before
V = 23203, p-value < 2.2e-16
alternative hypothesis: true location shift is not equal to 0
median(pairs$change)[1] 2.447
Read the result. The paired option keeps each country’s two measurements together. The median change is a useful description, but the signed-rank test is not assumption-free and does not always target the ordinary median without further conditions.
Your turn. Which is the relevant distribution to inspect: each year separately or the within-country differences?
The within-country differences. Pairing changes the analysis from two unrelated groups to one set of matched changes. Independence is required between pairs under the model.
7.4 · Optional: use only the direction of change
The question. What if we want a simpler sign-based question?
A sign test asks whether positive and negative non-zero changes are equally likely under its null. It discards the size of changes. With an appropriate continuous difference distribution, it can address a zero median without the signed-rank symmetry condition. We exclude exact zero changes explicitly.
nonzero <- pairs$change[pairs$change != 0]
positive <- sum(nonzero > 0)
binom.test(positive, length(nonzero), p = 0.5)
Exact binomial test
data: positive and length(nonzero)
number of successes = 207, number of trials = 217, p-value < 2.2e-16
alternative hypothesis: true probability of success is not equal to 0.5
95 percent confidence interval:
0.9168867 0.9776833
sample estimates:
probability of success
0.9539171
Read the result. This binomial calculation is being used for a sign test of paired continuous changes. It does not tell us the average number of years changed. A tiny and a large positive change each count as one positive sign.
Your turn. Why might a sign test lose information compared with a paired t-test?
It ignores magnitude. That can make it less sensitive to some alternatives, although it answers a simpler question with different assumptions. Method choice should follow the estimand and design, not a search for significance.
7.5 · Compare more than two groups with ranks
The question. What is the rank-based counterpart to a multi-group comparison?
Kruskal–Wallis compares rank locations across independent groups. It is an overall test, just as ANOVA is overall, but its target and assumptions differ. A location or median interpretation needs suitable common-shape assumptions.
kruskal_result <- kruskal.test(life ~ income, data = groups)
kruskal_result
Kruskal-Wallis rank sum test
data: life by income
Kruskal-Wallis chi-squared = 141.68, df = 3, p-value < 2.2e-16
Read the result. A small p-value suggests the observed rank pattern is difficult to reconcile with the null framework. It does not identify every differing pair. Different group spreads and shapes must be considered before describing the result as a median difference.
Your turn. Can we use Kruskal–Wallis for the same countries measured at three times as if the rows were independent?
No. That would ignore repeated measurements. A repeated-rank method such as Friedman addresses a different matched design. Repeated-measures modelling is outside this workbook’s core syllabus.
7.6 · Follow up a rank comparison
The question. Which pairwise rank comparisons are supported?
We use pairwise Wilcoxon rank-sum comparisons with Holm-adjusted p-values. This is a clearly named follow-up family. It is not Dunn’s test, and we should not label it as such.
pairwise.wilcox.test(groups$life, groups$income,
exact = FALSE, p.adjust.method = "holm")
Pairwise comparisons using Wilcoxon rank sum test with continuity correction
data: groups$life and groups$income
Low Lower middle Upper middle
Lower middle 0.0032 - -
Upper middle 6.5e-10 1.4e-08 -
High 5.7e-13 < 2e-16 2.1e-13
P value adjustment method: holm
Read the result. Read the adjusted p-values alongside group medians, IQRs and graphs. Adjustment addresses the family of pairwise tests, not the scientific interpretation of a rank difference. It does not turn the results into mean differences in years.
Your turn. What descriptive summaries would you put beside this table?
Group counts, medians and IQRs, plus a graph showing distribution shape. If you also report means, label them clearly and do not imply that the rank test directly tests those means.
7.7 · Practise an ordinal outcome without inventing patients
The question. How can we work with ordered categories using real observations?
We create three bands from observed life expectancy: below 65, 65 to below 75, and 75 or above. These cut-offs are arbitrary teaching choices, not clinical thresholds or official World Bank categories. Categorising a continuous outcome loses information, so we keep the original variable for the main analyses.
ordinal_data <- relation
ordinal_data$life_band <- cut(ordinal_data$life,
breaks = c(-Inf, 65, 75, Inf), right = FALSE, ordered_result = TRUE,
labels = c("Below 65", "65 to below 75", "75 or above"))
table(ordinal_data$life_band)
Below 65 65 to below 75 75 or above
28 80 83
cor.test(ordinal_data$spending, as.integer(ordinal_data$life_band),
method = "spearman", exact = FALSE)
Spearman's rank correlation rho
data: ordinal_data$spending and as.integer(ordinal_data$life_band)
S = 273469, p-value < 2.2e-16
alternative hypothesis: true rho is not equal to 0
sample estimates:
rho
0.7645106
Read the result. The integers encode ordered positions, not equal year distances. Spearman uses their ranks, so it does not require treating the bands as equally spaced measurements. The many ties reduce detail. The derived bands are an ordinal teaching outcome, not new observed measurements.
Your turn. Why would ordinary regression on category codes 1, 2 and 3 require extra caution?
It treats numerical steps as meaningful equal distances in the outcome. Ordinal models can handle ordered categories differently, but they are beyond this introductory syllabus. For our main health question, retaining continuous life expectancy avoids this unnecessary information loss.
7.8 · Revisit Spearman with a clearer purpose
The question. When should I choose Pearson or Spearman?
Choose Pearson for a linear numerical association when its framework is appropriate. Choose Spearman for a rank-based monotonic association, including suitable ordinal measurements. Start with the question and graph. Do not choose the method by whichever p-value is smaller.
show_table(data.frame(
Method = c("Pearson: original expenditure", "Spearman: expenditure ranks"),
Coefficient = c(unname(pearson$estimate), unname(spearman$estimate)),
Countries = nrow(relation)
))| Method | Coefficient | Countries |
|---|---|---|
| Pearson: original expenditure | 0.63 | 191 |
| Spearman: expenditure ranks | 0.84 | 191 |
Read the result. Both coefficients use the same countries, but they summarise different aspects of the relationship. Neither is an adjusted causal effect. A method can be correctly computed and still answer the wrong scientific question.
Your turn. Explain one reason to prefer a graph plus a coefficient over a coefficient alone.
The graph can reveal curvature, clusters, changing spread and influential observations that a single number hides. A coefficient is a summary, not a substitute for seeing the data.
Week 7 · Pause and explain
Explain the difference between rank-sum and signed-rank tests. Explain why non-parametric does not mean assumption-free. Then explain why converting life expectancy into bands is useful for one teaching example but not desirable for our main analysis.
Week 8 · Statistical Computing, Interpretation, and Reporting in Health Sciences
This week’s small goals
Run an analysis from start to finish, recognise common R errors, keep the denominator visible and write a short report that another reader can understand. This week is about making the earlier methods usable, not adding a large collection of new tests.
A reproducible analysis keeps the question, source, data preparation, code and interpretation together. Quarto helps because the written explanation and executable code live in one document. Reproducibility does not guarantee that the research design is valid, but it makes the work easier to inspect.
8.1 · Make a simple analysis plan
The question. What should I decide before running a test?
Write the outcome, comparison, unit, year, missing-data rule and intended summary first. This makes the method a consequence of the question rather than a guess. The example below plans the two-group comparison already studied.
plan <- data.frame(
Question = "Compare mean life expectancy: Low minus High",
Unit = "Country or economy",
Year = year,
Method = "Welch independent-samples t-test",
Missing_rule = "Observed life expectancy and classified income group"
)
show_table(plan)| Question | Unit | Year | Method | Missing_rule |
|---|---|---|---|---|
| Compare mean life expectancy: Low minus High | Country or economy | 2023 | Welch independent-samples t-test | Observed life expectancy and classified income group |
Read the result. This is a transparent plan, not proof that the country-independence assumption is correct. It also reminds the reader that income classification is from current metadata. A plan should be dated and preserved for a real project.
Your turn. What would change in the plan for a within-country change question?
The design would become paired, the data rule would require both years for each country, the contrast would be later minus earlier and the method would use matched changes. The unit of the pair would remain the country.
8.2 · Keep an analysis count table
The question. Why do different analyses have different sample sizes?
Each method needs specific measurements. The descriptive mean needs life expectancy. Correlation needs life expectancy and expenditure. A paired analysis needs both years. We make those requirements visible instead of hiding exclusions.
counts <- data.frame(
Analysis = c("Selected-year data", "Life expectancy description",
"Low versus High", "Correlation and regression", "Paired years"),
Countries = c(nrow(data), nrow(life_data), nrow(two_groups),
nrow(relation), nrow(pairs))
)
show_table(counts)| Analysis | Countries |
|---|---|
| Selected-year data | 217 |
| Life expectancy description | 217 |
| Low versus High | 111 |
| Correlation and regression | 191 |
| Paired years | 217 |
Read the result. These are not contradictory counts. They refer to different analysis requirements. If the composition changes, interpretation can change too. Complete-case analysis does not automatically make missingness harmless.
Your turn. Why should a regression report not simply copy the descriptive sample size?
Regression may exclude countries missing its predictor even when their outcome is observed. Report the number actually used by the model, for example with nobs(model).
8.3 · Build a small results table
The question. Can I show the main comparison without printing the whole console?
Yes. Select the quantities the reader needs and label their units. Keep p-values readable without rounding a small value to zero. We use the saved test object rather than manually retyping results.
results <- data.frame(
Contrast = "Low minus High",
Low_n = length(low), High_n = length(high),
Difference_years = mean(low) - mean(high),
Lower_95 = welch_test$conf.int[1],
Upper_95 = welch_test$conf.int[2],
p = pretty_p(welch_test$p.value)
)
show_table(results)| Contrast | Low_n | High_n | Difference_years | Lower_95 | Upper_95 | p |
|---|---|---|---|---|---|---|
| Low minus High | 25 | 86 | -15.6 | -17.68 | -13.53 | <0.001 |
Read the result. The table contains direction, sample sizes, magnitude and uncertainty. The p-value is supplementary, not the whole result. An interval in years is usually easier to interpret than an unlabelled test statistic.
Your turn. Why should you not type the p-value as 0.000?
That falsely suggests a probability of exactly zero. Use an appropriate format such as p < 0.001 when the value is smaller than the chosen reporting threshold. The helper uses R’s p-value formatting function.
8.4 · Turn output into a plain sentence
The question. How do I write a result without exaggerating it?
Use the actual values from the analysis. Name the unit and comparison. The following code generates a sentence so the numbers stay linked to the data. It is a writing aid, not a replacement for checking the result.
p_text <- if (welch_test$p.value < 0.001) "p < 0.001" else
paste("p =", pretty_p(welch_test$p.value))
cat(sprintf(
paste0("In %d, the Low-minus-High mean life expectancy difference was ",
"%.2f years (95%% model-based CI %.2f to %.2f; %s). ",
"The comparison included %d low-income and %d high-income countries.\n"),
year, mean(low) - mean(high), welch_test$conf.int[1],
welch_test$conf.int[2], p_text, length(low), length(high)
))In 2023, the Low-minus-High mean life expectancy difference was -15.60 years (95% model-based CI -17.68 to -13.53; p < 0.001). The comparison included 25 low-income and 86 high-income countries.
Read the result. Add a limitation in your own words: these are observational country-level comparisons using current metadata classifications, not causal effects of income. The code formats very small p-values as p < 0.001 rather than as zero.
Your turn. Correct this claim: “Being in the high-income group makes a person live longer.”
A safer statement is: “Country-level life expectancy differed between the observed income groups.” Add the actual direction and estimate. The data do not identify a patient-level causal effect.
8.5 · Save a table and a figure
The question. How can I keep the results for a report?
A project folder helps keep the source and outputs together. We save a CSV table and a PNG graph. The original downloaded bundle remains in the cache folder. Keep it if you need to reproduce a particular data version.
dir.create("hss2381_outputs", showWarnings = FALSE)
write.csv(results, "hss2381_outputs/two_group_results.csv", row.names = FALSE)
png("hss2381_outputs/life_expectancy_histogram.png", width = 1000, height = 650)
hist(life_data$life, main = "Life expectancy across countries",
xlab = "Life expectancy (years)", ylab = "Number of countries")
dev.off()quartz_off_screen
2
Read the result. write.csv() saves the table. png() opens an image file for plotting; dev.off() closes it. These commands overwrite files with the same names, so archive a project before a new analysis if you need to preserve versions.
Your turn. What else should you keep besides a finished graph?
Keep the QMD source, downloaded data bundle, parameter settings, retrieval date and software session information. A graph alone cannot show exactly how the result was produced.
8.6 · Leave a short reproducibility note
The question. How will another reader know where the data came from?
We record the retrieval time, selected years and software versions. Historical estimates can be revised, so a future download may not be identical. Saving the original bundle matters even when the source is public.
cat("World Bank retrieval time (UTC):", bundle$retrieved, "\n")World Bank retrieval time (UTC): 2026-10-17 13:45:53 UTC
cat("Selected year:", year, "| Earlier year:", earlier_year, "\n")Selected year: 2023 | Earlier year: 2010
writeLines(capture.output(sessionInfo()),
"hss2381_outputs/session_info.txt")
writeLines(c(paste("Retrieved:", bundle$retrieved),
"Income classifications: current metadata at retrieval.",
bundle$urls), "hss2381_outputs/data_sources.txt")Read the result. The note supports transparency. It does not establish that every original estimate is error-free. The source pages identify contributing organisations and indicator definitions; preserve that attribution when sharing data.
Your turn. Would changing the year at the top of the document always preserve every sample size and conclusion?
No. Coverage, classifications and values can differ. The document requests the exact selected year and stops if required teaching datasets are too small. Recheck missingness, graphs and assumptions after any change.
Friendly troubleshooting: the message is a clue, not a judgement
| What you see | What it usually means | First thing to try |
|---|---|---|
object not found |
The named object has not been created in this session, or its spelling differs | Run earlier chunks and check capitals |
unexpected symbol |
R could not read the punctuation | Check commas, quotation marks and parentheses |
could not find function |
A function name is misspelt or its package is not available | Check the name and setup instructions |
NA as a mean |
Missing entries were included without an explicit rule | Inspect missingness, then use na.rm = TRUE when appropriate |
| A download error | The API or network request failed | Check the connection or use a previously saved valid cache |
| Different results after downloading again | Data or metadata may have been revised | Compare the cached bundle and retrieval dates |
Run one small piece at a time when learning. Read the first error, not the last ten messages. Copy a working example and change only one thing. You are learning a language; a punctuation error is not a failure to understand statistics.
A short reporting template
Question
State the outcome, predictor or grouping variable, unit and selected year.
Data and preparation
Name the World Bank indicators, retrieval date and exact year. Explain exclusions and missingness. Say that income classifications come from current metadata. State that the data are country-level and observational.
Description
Report the number observed, suitable centre and spread, and a labelled graph. Do not substitute a p-value for a description.
Main analysis
Name the method and why it matches the question. Report the estimate in its correct units, interval and p-value when relevant. State the contrast direction and analysis-specific count.
Interpretation
Explain what the result suggests about the observed country-level relationship. Keep practical importance separate from statistical evidence.
Limitations
Mention missing data, possible confounding, country dependence, indicator uncertainty and the inability to infer patient-level effects. Include any important diagnostic concern rather than hiding it.
A small end-of-course project
Choose one main question, not every method in the workbook:
- Compare life expectancy between two current income groups.
- Compare life expectancy across all four current income groups.
- Describe the association between expenditure and life expectancy.
- Describe matched life expectancy change between the two selected years.
Submit a short report with a question, one descriptive table, one or two useful graphs, one main analysis, a plain-language interpretation and a limitations paragraph. Include the QMD and source information. A clear, appropriate analysis is better than a long list of tests.
Suggested self-check
- Can a reader identify the unit and year?
- Are the variable units and group labels clear?
- Is the sample size the one actually used?
- Does the method match independent or paired data?
- Are the estimate and interval reported, not only the p-value?
- Does the conclusion avoid causal and patient-level overclaims?
- Can the source and code be found again?
Midterm Examination 2 · Review of Lectures 4–8
A short practice paper
- Describe what a scatterplot can reveal that a correlation coefficient cannot.
- Explain the difference between Pearson and Spearman correlation.
- Interpret the simple-regression slope when expenditure is measured in hundreds of dollars.
- Explain a residual and give its sign for a point above the fitted line.
- Explain why R-squared is not a percentage caused by the predictor.
- Distinguish a fitted-mean confidence interval from a prediction interval.
- State the null for a four-group ANOVA.
- Explain why an overall ANOVA result does not identify all differing pairs.
- Choose a follow-up strategy for classical ANOVA and one for unequal-variance mean comparisons.
- Distinguish rank-sum, signed-rank and Kruskal–Wallis designs.
- Explain one caution when interpreting a rank test as a median test.
- List the minimum information needed in a reproducible results paragraph.
- Curvature, clusters, changing spread and unusual points.
- Pearson summarises linear numerical association; Spearman summarises rank-based monotonic association.
- The fitted difference in country-level life expectancy per additional 100 dollars per person, not a causal effect.
- Observed minus fitted; a point above the line has a positive residual.
- It describes fitted variation in the observed outcome, not causal attribution.
- The first concerns a modelled mean; the second includes variation in a new country-level observation.
- All modelled group means are equal.
- It is an omnibus question; pairwise contrasts ask more specific questions.
- Tukey under the classical framework; pairwise Welch tests with Holm adjustment for the unequal-variance comparison family.
- Two independent groups; matched changes; more than two independent groups.
- Location or median interpretations require additional distributional conditions; shapes and spreads matter.
- Source, year, unit, sample size, method, contrast, estimate, uncertainty, relevant p-value and limitations.
Final Examination · Cumulative Review
Connect the ideas rather than memorising commands
The full workflow is: question → design → variables → missingness → description → graph → method → estimate and uncertainty → interpretation → limitations.
The same command can be useful or inappropriate depending on the question. For example, t.test() can compare independent groups or matched measurements, but the paired setting changes the design. A technically successful command is not enough: the explanation must match the unit and target quantity.
Choose a method: a calm decision guide
| What do you want to know? | Design | Starting method |
|---|---|---|
| What is a typical value? | One numerical variable | Mean/SD and/or median/IQR, with a graph |
| How uncertain is a mean? | One numerical variable under a stated model | t-based mean confidence interval |
| Does a mean differ from a stated reference? | One group | One-sample t-test |
| Do two means differ? | Different countries in two groups | Welch independent-samples t-test |
| Has the same unit changed? | Matched countries at two times | Paired t-test |
| Do two numerical variables move together linearly? | Complete measurement pairs | Pearson correlation |
| Is the association monotonic or rank-based? | Numerical or suitable ordinal pairs | Spearman correlation |
| How does a continuous outcome relate to one predictor? | Country-level observational pairs | Simple linear regression |
| Do more than two means differ? | Independent groups | Welch ANOVA or justified classical ANOVA |
| Do two independent groups differ in ranks/location? | Independent groups | Wilcoxon rank-sum, with interpretation conditions |
| Do paired changes differ in signed-rank location? | Matched measurements | Wilcoxon signed-rank, with symmetry conditions |
| Do more than two independent groups differ in ranks/location? | Independent groups | Kruskal–Wallis, with interpretation conditions |
This guide does not replace assumptions. Do not use it as “normality p below 0.05 means always use ranks”. Start with the design and the quantity you want to describe.
Ten cumulative scenarios
- A report gives the mean but no SD, graph or sample size. What is missing?
- A student replaces every unavailable expenditure value with zero. What changes scientifically?
- A histogram is skewed. Does that alone invalidate every t-test?
- A paired analysis was matched by spreadsheet row number. What should be checked?
- A correlation is near zero, but the graph is strongly curved. Is there no association?
- A regression slope is statistically significant, but the residual plot has a strong curve. What should be reported?
- A four-group overall test is significant. Can the report say every group differs from every other group?
- A rank-sum test is significant while the group shapes differ. Can the report automatically claim a median difference?
- A confidence interval includes zero and a large positive difference. Does that establish no meaningful difference?
- A country-level association is used to advise an individual patient. What boundary has been crossed?
- Spread, distribution shape, denominator, units and uncertainty where relevant.
- Missing information is falsely turned into observed zero expenditure, distorting summaries and associations.
- No. Consider the estimand, design, sample size, extremes and relevant sampling or error distribution. For paired tests, inspect differences.
- Match by stable country identifier and verify one observation per country per year.
- No. Pearson correlation measures linear association and can miss a curved pattern.
- The model may be misspecified. Report the concern; a small p-value does not rescue a poor mean model.
- No. The overall test says at least one mean differs under the model.
- No. Rank differences are not automatically median differences without additional conditions.
- No. The result may be imprecise and compatible with a meaningful difference.
- The ecological boundary: country averages do not identify individual patient effects.
A compact command card
You do not need to memorise this card. Use it to recognise a familiar pattern and return to the worked example.
# Describe one variable.
mean(data$life, na.rm = TRUE)
median(data$life, na.rm = TRUE)
sd(data$life, na.rm = TRUE)
IQR(data$life, na.rm = TRUE)
# Estimate a mean and compare it with a stated reference.
t.test(life_data$life, mu = 70)
# Compare independent groups or matched years.
t.test(low, high)
t.test(pairs$after, pairs$before, paired = TRUE)
# Study association and a simple fitted line.
cor.test(relation$spending, relation$life)
cor.test(relation$spending, relation$life, method = "spearman", exact = FALSE)
lm(life ~ spending100, data = relation)
# Compare more than two means.
oneway.test(life ~ income, data = groups)
aov(life ~ income, data = groups)
# Use ranks with the correct design.
wilcox.test(low, high, exact = FALSE)
wilcox.test(pairs$after, pairs$before, paired = TRUE, exact = FALSE)
kruskal.test(life ~ income, data = groups)Plain-language glossary
| Word | Meaning in this workbook |
|---|---|
| Association | Two measurements vary together; this alone does not show causation |
| Bias | A systematic problem that can pull an estimate away from its intended target |
| Confidence interval | An uncertainty interval from a procedure with stated repeated-use coverage under assumptions |
| Confounding | Another factor helps explain an observed relationship |
| Continuous variable | A numerical measurement that can take values along a continuum |
| Effect size | The magnitude of a contrast or association, with its scale stated |
| Estimate | A number calculated to describe a target quantity |
| Independent groups | Different units in each group, with no matching between groups |
| Intercept | The fitted outcome when the predictor equals zero on its modelled scale |
| IQR | The distance between the first and third quartiles |
| Mean | The sum divided by the number of observed values |
| Median | The middle ordered value |
| Missing value | Information not available; not an observed zero |
| Model | A simplified mathematical description of a relationship |
| Null hypothesis | The starting claim assessed by a test |
| Ordinal variable | Ordered categories without guaranteed equal numerical gaps |
| Paired data | Two measurements linked to the same unit or a deliberate match |
| Parameter | A quantity describing a population or model |
| p-value | A tail probability for results under the null model and its assumptions |
| Rank | A position in an ordered set of values |
| Residual | Observed outcome minus fitted outcome |
| SD | Spread among observations |
| SE | Uncertainty in an estimator under a model or sampling design |
| Slope | Fitted outcome difference per one unit of the modelled predictor |
| Statistic | A quantity calculated from observed data |
Sources and instructor notes
Indicator dictionary
| Lesson name | World Bank indicator | Unit |
|---|---|---|
life |
SP.DYN.LE00.IN |
Life expectancy at birth, total, years |
spending |
SH.XPD.CHEX.PC.CD |
Current health expenditure per capita, current US dollars |
gdp |
NY.GDP.PCAP.CD |
GDP per capita, current US dollars |
urban |
SP.URB.TOTL.IN.ZS |
Urban population, percentage of total |
income |
Country metadata income classification | Current metadata category at retrieval |
GDP is available for optional descriptive practice; multiple regression is intentionally not part of the core lessons. Expenditure and GDP are in current dollars, not inflation-adjusted or purchasing-power-parity units. We do not interpret nominal cross-year expenditure growth in this workbook.
The API preparation downloads a year range so the two endpoint years can be matched. The student cross-sectional analyses use only the selected year. Missing selected-year values are not replaced with values from another year. The downloader excludes aggregate entries using region metadata, checks country-year duplicates and performs broad plausibility checks. Such checks are not proof of measurement accuracy.
The original contributing organisations and licensing details are on each indicator page. The health expenditure page identifies the WHO Global Health Expenditure Database; the life expectancy page identifies contributing demographic sources. Preserve attribution when redistributing data.
Official documentation
- World Bank: Indicators API documentation.
- World Bank: country metadata queries.
- World Bank: life expectancy indicator and source details.
- World Bank: health expenditure indicator and source details.
- World Bank: GDP per capita indicator.
- World Bank: urban population indicator.
- R: t-tests, confidence intervals and paired comparisons.
- R: correlation tests.
- R: linear models.
- R: Welch and classical one-way mean tests.
- R: Wilcoxon rank-sum and signed-rank tests.
- R: Kruskal–Wallis test.
- R: p-value adjustment.
- Quarto: executable R documents.
The explanations, practice questions and teaching sequence in this workbook are original. The eight-week structure and examination coverage follow the syllabus supplied for this revision. Optional items are marked and kept short. Bootstrap, permutation, multiple regression, robust covariance estimation and advanced repeated-measures modelling from the broader earlier version are intentionally omitted from the core path to reduce overload.
sessionInfo()R version 4.5.2 (2025-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Tahoe 26.6.2
Matrix products: default
BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: America/Toronto
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
loaded via a namespace (and not attached):
[1] htmlwidgets_1.6.4 compiler_4.5.2 fastmap_1.2.0 cli_3.6.5
[5] tools_4.5.2 htmltools_0.5.9 otel_0.2.0 rstudioapi_0.17.1
[9] curl_7.0.0 yaml_2.3.12 rmarkdown_2.30 knitr_1.51
[13] jsonlite_2.0.0 xfun_0.55 digest_0.6.39 rlang_1.3.0
[17] evaluate_1.0.5