How to teach (90 minutes)
- 00–10 · Diagnose: two predictions from prior work, paths and R setup.
- 10–25 · Concepts and data dictionary: variable meaning and units.
- 25–45 · Live R: predict, run, explain output.
- 45–65 · Independent student TODO tasks; instructor offers hints.
- 65–80 · Debugging, inference limits and counterexample.
- 80–90 · Three-question exit ticket and next homework.
Before each session
- Working directory: package root containing data/ for Weeks 1–4; Weeks 5–6 scripts are standalone.
- Required packages: base R; any dplyr/ggplot2 mapping to original Moodle work is additional.
- Student attempts TODO starter first; instructor keeps the worked script separate.
- Use RStudio Run line by line; the final PASS line indicates only local assertions.
- Revisit distribution versus association versus causality, and sample versus population.
- Use your browser's Print function to save this self-contained page as a PDF.
R basics and data frames
Students distinguish Console and script and explain objects, vectors, indexing, CSV import and data-frame operations.
Expected output and interpretation
Reference checks: 7+5=12; 3×(4+2)=18; x has length 4, mean 9, second element 8, and x>8 selects 10 and 12. The fictional dataset has 12 rows; mean age ≈21.17.
Live demonstration
Ask for predictions of x[2] and x[x>8] before running. Then demonstrate the distinction between p and p$age.
R script: line-by-line explanation
The table shows executable lines from the openly published, original solution. Continuation lines of multi-line calls are explained separately.
| R code | What exactly happens? |
|---|---|
01print(7+5) | print() displays the evaluated expression in the Console; 7+5=12. |
02print(3*(4+2)) | The parentheses evaluate 4+2 before multiplication, giving 18. |
03x <- c(6,8,10,12) | c() combines four numbers into one numeric vector; <- assigns it to x. |
04print(length(x)) | length() returns the number of elements, which is 4 here, not their sum. |
05print(mean(x)) | mean() computes the arithmetic average; (6+8+10+12)/4 = 9. |
06print(x[2]) | Square brackets select by position; R's second element x[2] is 8. |
07print(x[x>8]) | x>8 creates a TRUE/FALSE mask, which selects only 10 and 12. |
08p <- read.csv("data/participants.csv") | Loads the CSV into data frame p. The working directory must be the parent of data/. |
09print(head(p)) | head(p) displays the first six rows by default; it does not verify all column types. |
10str(p) | str(p) inspects dimensions and the R class of each column. |
11print(nrow(p)) | nrow(p) counts rows/observations; the expected value is 12 fictional records. |
12print(mean(p$age)) | $age extracts the age column from p and mean() averages that vector. |
13print(aggregate(age~intervention,p,mean)) | age~intervention groups ages by intervention for descriptive mean summaries only. |
14plot(seq_len(nrow(p)),p$age,pch=19,xlab="Fictional row",ylab="Age",main="Synthetic participants") | seq_len(nrow(p)) makes row indices 1–12; pch=19 draws solid points, and xlab/ylab name axes. |
15stopifnot(length(x)==4L,mean(x)==9,nrow(p)==12L) | stopifnot() asserts all conditions are TRUE and stops with an error if one fails. |
16cat("W1 PASS: 12 synthetic participants, no external packages\n") | cat() prints a status message; the message is not validation of real-world research. |
Likely misconception
R indexing starts at 1; p$age is a column vector; plotting row order does not imply an experiment or causality.
Exit check
Exit questions: What does read.csv() return? What does str() inspect? Can 12 convenience records support population claims?
Data wrangling and paired change
Perform a two-key join, isolate pre/post occasions, match individuals and summarize within-person change.
Expected output and interpretation
There are 22 measurement rows and 12 participant records. Ten have both occasions, yielding 10 matched pairs. Mean paired AHI change is +2.9 points; group A +3 and B +2.75 (entirely fictional).
Live demonstration
Ask why joining by id alone is not universally safe; have the student verify occasion codes 0/1 before analysis.
R script: line-by-line explanation
The table shows executable lines from the openly published, original solution. Continuation lines of multi-line calls are explained separately.
| R code | What exactly happens? |
|---|---|
01participants <- read.csv("data/participants.csv") | Reads participant identifiers and group labels; uniqueness of id alone should not be assumed. |
02scores <- read.csv("data/scores.csv") | Reads repeated observations: each row can represent a person–occasion record. |
03stopifnot(all(scores$occasion %in% c(0,1))) | %in% checks that occasion values are only 0 or 1, as defined in this fabricated dataset. |
04combined <- merge(scores,participants,by=c("id","intervention")) | merge(...,by=c('id','intervention')) requires both keys to match, preventing cross-group joins. |
05before <- combined[combined$occasion==0,c("id","intervention","ahiTotal")] | Selects occasion==0 rows and three columns; before the comma filters rows, after it selects columns. |
06after <- combined[combined$occasion==1,c("id","intervention","ahiTotal")] | Separates the occasion==1 post observations into a data frame. |
07names(before)[3] <- "pre" | Renames the third column from ahiTotal to pre without changing its values. |
08names(after)[3] <- "post" | Renames the post measurement column to post, avoiding ambiguous names after merging. |
09paired <- merge(before,after,by=c("id","intervention")) | Retains only id+intervention keys with both pre and post values; missing follow-up records are excluded. |
10paired$delta <- paired$post-paired$pre | post-pre is within-person change; a positive difference indicates an increase, not a causal effect. |
11print(paired) | Prints the matched table so the instructor can inspect whether each row corresponds to a single person. |
12print(nrow(paired)) | nrow(paired) counts complete pairs, here 10 rather than all participants. |
13print(mean(paired$delta)) | Averages within-person differences; the fictional result is +2.9. |
14print(aggregate(delta~intervention,paired,mean)) | Summarizes average change by group; these labels are not randomized conditions. |
15stopifnot(nrow(paired)==10L,nrow(combined)==22L) | Asserts 22 raw observation rows and 10 matched pairs; the test must change if input data change. |
16cat("W2 PASS: matched 10/12 synthetic records\n") | cat() prints a synthetic-data check message, not evidence of research validity. |
Likely misconception
The original source codebook says occasion 0=pre, 1=post, 2=follow-up, despite conflicting practical hints. Difference of two unpaired means is not the same analysis as paired differences; no causal claim follows.
Exit check
Why did we use two join keys? Why did only 10 of 12 records form pairs? Could missing follow-up data bias the comparison?
AQ-like scoring and summaries
Reshape wide responses to long format, join item-format and scoring lookups, then summarize binary item scores.
Expected output and interpretation
The toy data contain 8 × 10 = 80 responses. By ID S01–S08 the totals are 6, 4, 5, 4, 6, 3, 7, 4; mean = 4.875, sample SD ≈ 1.356. These are programming outputs, not clinical classifications.
Live demonstration
How does scoring differ for F and R item keys? Hand-score two responses before automating reshape and joins.
R script: line-by-line explanation
The table shows executable lines from the openly published, original solution. Continuation lines of multi-line calls are explained separately.
| R code | What exactly happens? |
|---|---|
01aq <- read.csv("data/aq10.csv",check.names=FALSE) | check.names=FALSE preserves Q1…Q10 names; in wide format each row is a participant. |
02formats <- read.csv("data/aq_formats.csv") | Reads the lookup that assigns F/R scoring directions to each item. |
03scoring <- read.csv("data/aq_scoring.csv") | Reads the second lookup mapping F/R plus Agree/Disagree to binary 0/1 scores. |
04items <- paste0("Q",1:10) | paste0('Q',1:10) constructs the column names Q1 through Q10. |
05long <- reshape(aq,varying=items,v.names="response", | reshape() begins converting ten question columns into one long response column while retaining identifiers. |
06timevar="question",times=items,direction="long") | timevar/times populate a question label Q1…Q10; direction='long' increases the row count. |
07rownames(long) <- NULL | Removes auto-generated row names; it does not erase participant IDs. |
08long <- merge(long,formats,by="question") | Joins by question to attach each item's F/R scoring direction to every response. |
09long$agrees <- grepl("Agree",long$response,fixed=TRUE) | grepl('Agree',response,fixed=TRUE) flags labels containing Agree; it assumes a controlled vocabulary. |
10long$response_kind <- ifelse(long$agrees,"Agree","Disagree") | ifelse() transforms the Boolean flag into the labels Agree and Disagree. |
11long <- merge(long,scoring,by=c("format","response_kind")) | The second merge attaches score using format + response_kind; inspect unmatched cases before trusting totals. |
12totals <- aggregate(score~id,long,sum) | aggregate(score~id,long,sum) adds the ten binary item scores per participant. |
13print(totals) | Prints IDs and calculated totals without assigning a clinical label. |
14print(mean(totals$score)) | Calculates the mean of the eight fictional totals. |
15print(sd(totals$score)) | sd() computes the sample standard deviation of the eight totals (n−1 denominator). |
16stopifnot(nrow(totals)==8L,all(totals$score>=0),all(totals$score<=10)) | Checks eight participants and totals within 0–10; also discuss validating 80 long rows and ten items per ID. |
17cat("W3 PASS: 8 original fictional AQ-like screening totals\n") | cat() prints a code-check message; it does not validate diagnostic accuracy. |
Likely misconception
grepl('Agree', ...) works only for the provided controlled response labels, not arbitrary free text. An educational AQ-like total is not a diagnosis.
Exit check
Why join the question format before the scoring table? If only 79 rather than 80 rows remain, which join needs checking?
Data visualisation and graphical reasoning
Choose suitable plot types, label axes and describe distributions or associations without causal claims.
Expected output and interpretation
Twelve fictional ATSQ rows join with background by id. Group mean score: Coding=Yes 2.38, Coding=No ≈3.114. Histograms show one-variable distributions; scatterplots show associations; boxplots show medians/quartiles, not these group means.
Live demonstration
Ask 'Which question should this chart answer?' before plotting. Check the student knows a standard boxplot depicts the median and IQR.
R script: line-by-line explanation
The table shows executable lines from the openly published, original solution. Continuation lines of multi-line calls are explained separately.
| R code | What exactly happens? |
|---|---|
01atsq <- read.csv("data/atsq.csv") | Reads fictional score totals; this file alone lacks background covariates. |
02background <- read.csv("data/background.csv") | Reads age and Coding background variables from a separate CSV. |
03d <- merge(atsq,background,by="id") | An inner join by id retains only participants present in both files. |
04print(head(d)) | head(d) quickly inspects the first six joined records. |
05hist(d$atsqTotal,breaks=5,main="Synthetic ATSQ totals", | hist() shows a single numeric distribution; breaks=5 requests an approximate bin arrangement. |
06xlab="Illustrative score") | xlab names the axis; do not invent a unit when it is not established. |
07plot(d$age,d$atsqTotal,pch=19,xlab="Age",ylab="Synthetic ATSQ score") | plot(x,y) places two quantitative variables on axes; pch=19 draws solid dots. |
08boxplot(atsqTotal~Coding,data=d,xlab="Prior coding", | atsqTotal~Coding groups the score distribution by categorical Coding values. |
09ylab="Synthetic ATSQ score",main="Distribution by coding") | ylab and main supply the axis label and chart title. |
10print(aggregate(atsqTotal~Coding,d,mean)) | aggregate() reports group means; this does NOT mean the boxplot box represents a mean. |
11stopifnot(nrow(d)==12L,all(is.finite(d$atsqTotal))) | Checks 12 joined rows and that all scores are finite numeric values. |
12cat("W4 PASS: 12 synthetic matched records\n") | Prints a code-check message; it does not validate the plot interpretation. |
Likely misconception
A boxplot is not mean±SD. A scatterplot is not causal evidence; the invented ATSQ variable has no verified clinical scale interpretation.
Exit check
Why use a scatterplot for age versus score? What does a box represent? What is the n=12 limitation?
Sampling, SD, SE and simulation — provisional
Distinguish individual spread (SD) from sampling variability of the mean (SE) and implement a reproducible resampling simulation.
Expected output and interpretation
For 12 invented response times, the mean is 606.75 ms, sample SD ≈10.332 ms and SE ≈2.983 ms. Resample sizes 4 and 10 can be compared; seeded simulation values are reproducible in the same R environment.
Live demonstration
Ask which measure reflects differences among people versus uncertainty of a mean. Plot side by side and compare variability as resample size increases.
R script: line-by-line explanation
The table shows executable lines from the openly published, original solution. Continuation lines of multi-line calls are explained separately.
| R code | What exactly happens? |
|---|---|
01rt_ms <- c(620,610,590,605,615,600,625,595,608,612,598,603) | c() creates a vector of 12 fabricated response times in milliseconds. |
02n <- length(rt_ms) | length() returns the number of observations: n=12. |
03mean_rt <- mean(rt_ms) | mean() computes mean response time: 606.75 ms. |
04sd_rt <- sd(rt_ms) | sd() calculates sample standard deviation using n−1: approximately 10.332 ms. |
05se_rt <- sd_rt / sqrt(n) | sd/sqrt(n) estimates the standard error of the mean under independent, identically distributed sampling: about 2.983 ms. |
06print(c(n=n, mean_ms=mean_rt, sd_ms=sd_rt, se_ms=se_rt)) | c() creates a named summary vector; print() displays it. |
07stopifnot(n==12L, is.finite(se_rt), se_rt>0) | stopifnot() asserts n=12 and that SE is finite and positive. |
08set.seed(46) | set.seed(46) makes the pseudo-random simulation reproducible. |
09means_n4 <- replicate(1000, mean(sample(rt_ms, 4, replace=TRUE))) | replicate(1000,...) samples four values with replacement and stores the mean 1,000 times. |
10means_n10 <- replicate(1000, mean(sample(rt_ms, 10, replace=TRUE))) | Repeats sampling with replacement using size 10; mean estimates tend to vary less. |
11par(mfrow=c(1,2)) | par(mfrow=c(1,2)) arranges plots in one row and two columns. |
12hist(means_n4, main="Resample size = 4", xlab="Mean RT (ms)") | Plots resampled means at n=4, NOT a histogram of individual reaction times. |
13hist(means_n10, main="Resample size = 10", xlab="Mean RT (ms)") | Plots simulated sample means at n=10; the x-axis remains in milliseconds. |
14par(mfrow=c(1,1)) | par(mfrow=c(1,1)) restores a single-plot layout. |
15print(c(sd_n4=sd(means_n4),sd_n10=sd(means_n10))) | Compares simulation-based spread of sample means at n=4 versus n=10. |
16means_n12 <- replicate(1000, mean(sample(rt_ms, n, replace=TRUE))) | Generates 1,000 size-12 bootstrap sample means to match the original sample size. |
17print(quantile(means_n12,c(.025,.975))) | quantile(...,.025,.975) gives a percentile interval from size-12 bootstrap means; its validity depends on sampling assumptions. |
18stopifnot(length(means_n4)==1000L, all(is.finite(means_n12))) | Checks that simulation vectors have the expected length and finite values. |
19cat("WEEK 5 WORKED EXAMPLE PASS\n") | cat() prints a test banner, not a statistical finding. |
Likely misconception
set.seed() guarantees reproducibility, not representativeness. Resample means at n=10 do not form a standard bootstrap percentile CI for the observed n=12 mean. Thus we separately resample n=12; even that cannot validate population inference from a fabricated sample.
Exit check
What units do SD and SE use? What does replace=TRUE do? Why cannot bootstrapping rescue a nonrepresentative sample?
Hypothesis testing, p-values and intervals — provisional
Define H0: μ=600 ms against a two-sided alternative; run a one-sample t-test and interpret p-value and CI with assumptions.
Expected output and interpretation
Fictional t-test: n=12, mean 606.75 ms, df=11, t≈2.263, two-sided p≈0.0448, and a 95% CI of roughly [600.185, 613.315] ms. This does not establish a meaningful population effect or representative sample.
Live demonstration
Have the student avoid interpreting p≈0.045 as the probability H0 is false. Discuss how the 95% CI excludes 600 and what assumptions this requires.
R script: line-by-line explanation
The table shows executable lines from the openly published, original solution. Continuation lines of multi-line calls are explained separately.
| R code | What exactly happens? |
|---|---|
01rt_ms <- c(620,610,590,605,615,600,625,595,608,612,598,603) | Uses the same 12 fictional response times as Week 5 for continuity. |
02reference_mu <- 600 # arbitrary teaching benchmark, not a research claim | 600 ms is an arbitrary teaching reference, not a hypothesis derived from an actual study. |
03result <- t.test(rt_ms, mu=reference_mu, alternative="two.sided") | t.test(x,mu=600,alternative='two.sided') performs a two-sided one-sample t-test of H0: μ=600 and returns a CI. |
04print(result) | print(result) displays t statistic, df, p-value, 95% CI and estimated mean. |
05print(c(estimate_ms=unname(result$estimate), t=unname(result$statistic), p=result$p.value)) | $estimate is the sample mean, $statistic is t, and $p.value is the p-value; unname() removes attached names. |
06print(result$conf.int) | $conf.int returns the confidence limits in milliseconds; it does not give a 95% posterior probability for this one interval. |
07stopifnot(result$parameter == length(rt_ms)-1, result$p.value>=0, | Degrees of freedom should be n−1; the p-value must lie between 0 and 1. |
08result$p.value<=1, length(result$conf.int)==2L) | Continuation of stopifnot(): checks valid p and two confidence interval endpoints. |
09cat("WEEK 6 WORKED EXAMPLE PASS\n") | cat() prints a code-check banner; substantive inference still depends on design and assumptions. |
Likely misconception
Independence and assumptions for the mean matter; n=12 is small. A p-value below .05 is not, by itself, scientific relevance or causal evidence.
Exit check
State H0/H1. The p-value is the probability of what? Explain 95% confidence in repeated sampling.
Verification and assessment
Suggested instructor rubric: question/data understanding (2), correct R (3), debugging and join/NA checks (2), statistical interpretation and limits (2), reproducibility (1). Total 10, not an official UCL mark scheme.
Explanations are aligned with the repository's independently authored worked scripts. Key numerical values were cross-checked independently; an actual R interpreter execution test has not been performed in this environment.
UCL Week 5 Moodle section: section.php?id=1502829. Authenticated content could not be read; Week 5–6 topics remain subject to revision.