Every function the course teaches, on one page, grouped by the job you want done.
Keep this open in a second browser tab while you work. A lesson takes over your R console while it runs, so you cannot look things up there, but this page is always to hand. It is also worth printing.
How to read an entry
Each line of code is written so it works on any data, not just the course’s. The highlighted words are yours: replace each one with a name from your own data, such as your table or one of its columns. Everything else, including the brackets, commas and quote marks, is typed exactly as shown. Where a line has nothing highlighted, type it as it stands.
Under each one, in grey, is an example from the lessons, and the last column says which lesson taught it, so you can go back to the full explanation whenever a one-line reminder is not enough.
Two symbols worth knowing
These two turn up everywhere and neither is guessable.
The pipe, |>, means “and then.” It
carries the result of one step into the next, so a pipeline reads top to
bottom like a recipe:
patients |>
filter(age > 70) |>
select(id, age, arm)Take patients, and then keep the older ones, and then show these three columns.
The tilde, ~, means “by”, or “explained
by.” What you are measuring goes on the left, what splits or
explains it goes on the right:
change ~ arm # change, by arm
facet_wrap(~ arm) # one panel, by armA third piece of punctuation is worth knowing even though it is not a
symbol you type often.
package::function(), with two colons,
means “this function, out of this package”:
r4clinstats::start_report() # start_report(), from r4clinstats
dplyr::filter(patients, age > 65) # filter(), from dplyrIt is a command to run in the Console. It is not a web address, and there is nothing to click.
Getting your data in
| Code | What it does | Lesson |
|---|---|---|
read_csv(here("folder", "file_name.csv"))e.g.
read_csv(here("data", "linelist.csv"))
|
Reads a CSV. here() builds the path from the top of your project, so it works on any computer. Each part of the path is its own quoted name. |
M6 |
read_excel(here("folder", "file_name.xlsx"))e.g.
read_excel(here("data", "trial-data.xlsx"))
|
Reads a spreadsheet directly, no export step. From the readxl package. |
M6 |
read_excel(path, sheet = "sheet_name")e.g.
read_excel(here("data", "trial.xlsx"), sheet = "Baseline")
|
Picks one sheet out of a workbook. The path is the whole here(...) from the line above. |
M6 |
read_excel(path, skip = number)e.g.
read_excel(here("data", "trial.xlsx"), skip = 2)
|
Ignores that many rows above the real headers, such as a title or logo. | M6 |
read_excel(path, na = c("code", "code"))e.g.
read_excel(here("data", "trial.xlsx"), na = c("", "N/A", "-"))
|
Says what should count as missing. List every code your spreadsheet uses, each in its own quotes. "" is an empty cell. |
M6 |
Read the file, never edit it. Then glimpse() the result
and check the row count and column types before going further.
Looking at your data
Start here when you meet a new table.
| Code | What it does | Lesson |
|---|---|---|
glimpse(your_data)e.g.
glimpse(patients)
|
Every column, its type, and the first few values. The best first look. | M2 |
head(your_data)e.g.
head(patients)
|
The first six rows. | M2 |
summary(your_data$column)e.g.
summary(patients$age)
|
Min, max, quartiles, median and mean of a numeric column. The $ picks one column out of a table. |
M10 |
class(your_data$column)e.g.
class(patients$sex)
|
What type a column is: chr, dbl, fct, lgl. |
M2 |
table(your_data$column)e.g.
table(patients$smoker)
|
Counts of each category, base R's version of count(). |
M2 |
nrow(your_data)e.g.
nrow(patients)
|
How many rows. | M14 |
Choosing rows and columns
| Code | What it does | Lesson |
|---|---|---|
filter(your_data, column == "value")e.g.
filter(patients, arm == "Active")
|
Keep the rows that match. Note the double ==. The quote marks are there because the value is text; for a number, leave them off: filter(your_data, column > number). |
M3 |
filter(your_data, condition, condition)e.g.
filter(patients, age > 70, sex == "Female")
|
Several conditions at once, separated by commas. A row is kept only if every one holds. Each condition is written like the ones above. | M6 |
select(your_data, column, column)e.g.
select(patients, id, age, arm)
|
Keep only these columns, in this order. List as many as you need. | M3 |
arrange(your_data, column)e.g.
arrange(patients, age)
|
Sort, smallest first. | M3 |
arrange(your_data, desc(column))e.g.
arrange(patients, desc(age))
|
Sort, largest first. desc() flips it. |
M3 |
Counting and summarising
| Code | What it does | Lesson |
|---|---|---|
count(your_data, column)e.g.
count(patients, smoker)
|
How many rows in each group. The count column is called n. |
M3 |
summarise(your_data, new_name = mean(column))e.g.
summarise(patients, mean_age = mean(age))
|
Collapse a column to one number. You choose the new_name, and it becomes the column's heading. | M4 |
mean(column)e.g.
mean(age)
|
The summary numbers themselves: median(), sd() and IQR() are written the same way. Use mean and SD for a symmetric shape, median and IQR when it is lopsided. |
M4, M10 |
The two you will reach for most are a sorted tally, and a summary per group:
patients |> # tally, most common first
count(smoker) |>
arrange(desc(n))
patients |> # one row per group
group_by(arm) |>
summarise(mean_age = mean(age))Making and changing columns
| Code | What it does | Lesson |
|---|---|---|
mutate(your_data, new_name = calculation)e.g.
mutate(patients, high_bp = sbp > 140)
|
Add a column built from the others. A comparison such as sbp > 140 gives TRUE/FALSE. |
M4 |
if_else(condition, "if_true", "if_false")e.g.
mutate(linelist, sex = if_else(tolower(sex) == "female", "Female", "Male"))
|
Picks between two values, row by row. Use it inside mutate(). With tolower(), which flattens case, it is the standard recoding pair. |
M6 |
mutate(your_data, column = as.numeric(column))e.g.
mutate(linelist, age = as.numeric(age))
|
Convert text to numbers. Anything that was never a number becomes NA, with a warning. |
M6 |
is.na(column)e.g.
is.na(weight)
|
TRUE where a value is missing. !is.na(column) is "has a value". |
M6 |
factor(your_data$column, levels = c("first", "second", "third"))e.g.
factor(patients$smoker, levels = c("Never", "Former", "Current"))
|
Fix the order of categories, instead of R's alphabetical default. This is what tidies your tables and plot axes. List every category, in the order you want. | M2 |
factor(your_data$column, levels = c("lowest", "middle", "highest"), ordered = TRUE) |
The same, plus permission to compare levels with < and >. Only needed for a real scale, like tumour stage. |
M2 |
Reshaping and joining tables
| Code | What it does | Lesson |
|---|---|---|
your_data |> pivot_wider(names_from = column, values_from = column)e.g.
labs |> filter(measure == "sbp") |> select(id, visit, value) |> pivot_wider(names_from = visit, values_from = value)
|
Long to wide: one column per visit. names_from names the column that supplies the new headings, values_from the one that fills the cells. |
M5 |
your_data |> pivot_longer(cols = c(column, column), names_to = "new_name", values_to = "new_name") |
Wide back to long, the reverse move. The two new names are in quotes because those columns do not exist yet. | M5 |
left_join(your_data, other_data, by = "column")e.g.
left_join(labs, patients, by = "id")
|
Bring columns from a second table across, matching on a column both tables share. Keeps every row of the first table. | M5 |
left_join(your_data, select(other_data, column, column), by = "column")e.g.
left_join(labs, select(patients, id, arm), by = "id")
|
Narrow the second table first when you only want a column or two. Keep the shared column in the select(). |
M5 |
If a joined column comes back all NA, the key values did
not match. That is the quiet failure to watch for.
Plotting
Every plot is three parts: the data, the aes() mapping,
and a geom_ that draws it. The layers after the first are
added with +.
| Code | What it does | Lesson |
|---|---|---|
ggplot(your_data, aes(x = column, y = column)) + geom_point()e.g.
ggplot(patients, aes(x = bmi, y = sbp)) + geom_point()
|
Scatter plot: how two numbers relate. | M7 |
aes(x = column, y = column, colour = group)e.g.
aes(x = bmi, y = sbp, colour = arm)
|
Map a third variable to colour. Inside aes() means "from the data", and ggplot adds the legend. |
M7 |
geom_point(colour = "colour_name")e.g.
geom_point(colour = "steelblue")
|
A fixed colour, outside aes(), carrying no information. |
M7 |
ggplot(your_data, aes(x = group, y = column)) + geom_boxplot()e.g.
ggplot(patients, aes(x = arm, y = sbp)) + geom_boxplot()
|
One box per group: median line, middle half in the box. | M7 |
ggplot(your_data, aes(x = column)) + geom_bar()e.g.
ggplot(patients, aes(x = arm)) + geom_bar()
|
Counts rows per category. No y needed, it counts for you. |
M7 |
ggplot(your_data, aes(x = column)) + geom_histogram(binwidth = number)e.g.
ggplot(patients, aes(x = age)) + geom_histogram(binwidth = 10)
|
The shape of one numeric column. The number is the width of each bar, in the column's own units. Aim for about ten bars, then adjust. | M10 |
ggplot(your_data, aes(x = time_column, y = column, group = id_column)) + geom_line()e.g.
ggplot(bp, aes(x = weeks, y = value, group = id)) + geom_line()
|
Joins each patient's readings into their own line. group says which rows belong to the same patient. |
M8 |
geom_line(linewidth = number)e.g.
geom_line(linewidth = 1)
|
Thicker lines, which survive being shrunk into a paper. Set outside aes(), because it is a fixed choice rather than something read from the data. |
M8 |
geom_step() |
Draws a staircase rather than a sloped line, which is the correct shape for a survival curve. | M13 |
geom_smooth(method = "lm") |
Adds a fitted straight line through the points, with a grey band for its 95% confidence interval. Add se = FALSE to leave the band off. |
M12 |
facet_wrap(~ group)e.g.
facet_wrap(~ arm)
|
One panel per group, all on the same scale. Use when a single panel is too crowded. | M8 |
labs(x = "axis_title", y = "axis_title", colour = "legend_title")e.g.
labs(x = "Weeks", y = "Systolic BP", colour = "Treatment arm")
|
Readable axis and legend titles. Any text you like, in quotes. | M7, M8 |
theme_minimal() |
A cleaner look for a paper. | M8 |
scale_colour_brewer(palette = "Dark2") |
Colours a reader with red-green colour blindness can still tell apart. Worth it from three groups up. Use scale_fill_brewer() for bars and boxes. |
M8 |
scale_colour_viridis_d() |
The same idea for an ordered variable; scale_colour_viridis_c() for a continuous one. |
M8 |
Tables and tests
| Code | What it does | Lesson |
|---|---|---|
your_data |> summary_factorlist("group", c("column", "column"))e.g.
patients |> summary_factorlist("arm", c("age", "sex"))
|
The whole of Table 1 in one line: one column per group, one row per variable. Here the names go in quotes, unlike in filter(). Add as many variables inside c() as you need. From the finalfit package. |
M9 |
your_data |> summary_factorlist("group", c("column", "column"), p = TRUE)e.g.
patients |> summary_factorlist("sex", c("age", "bmi"), p = TRUE)
|
Adds a p-value per variable. One test per variable, not per level. Leave it off a randomised baseline table. | M9 |
t.test(column ~ group, data = your_data)e.g.
t.test(change ~ arm, data = bp_change)
|
Compares the mean of a number between two groups. Read ~ as "split by". |
M11 |
chisq.test(table(your_data$column, your_data$column))e.g.
chisq.test(table(trial$arm, trial$response))
|
Tests whether two categories are associated. | M11 |
result |> tidy()e.g.
t.test(change ~ arm, data = bp_change) |> tidy()
|
Turns any test or model into a tidy data frame, with columns like estimate and p.value. The result is the whole test or model. From the broom package. |
M11 |
tidy() returns up to ten columns, and the output panel
hides the ones that do not fit behind a small arrow at the top right, so
the confidence interval and p-value are often out of sight. Keep the
ones you would report:
t.test(change ~ arm, data = bp_change) |>
tidy() |>
select(estimate, conf.low, conf.high, p.value)For a t-test, estimate is the first group’s mean minus
the second’s, with the groups in alphabetical order, so check which way
round it is before you report it. Lesson 11 explains every column.
Reading e-17 and the like
R writes very small numbers in scientific notation.
e-17 means “shift the decimal point 17 places left”, and
the exponent does all the work:
| R shows | Means | Report as |
|---|---|---|
6.78e-17 |
0.0000000000000000678 | p < 0.001 |
1.98e-03 |
0.00198 | p = 0.002 |
4.20e-02 |
0.042 | p = 0.042 |
1.98e-03 is 0.002, not 1.98. A p-value
can never exceed 1, so if yours looks bigger, you have missed the
exponent. (M12)
Models
All of them share the same formula shape: outcome, tilde, predictors.
Add a predictor with +.
| Code | What it does | Lesson |
|---|---|---|
lm(outcome ~ predictor, data = your_data)e.g.
lm(change ~ arm, data = bp_change)
|
Linear regression, for a numeric outcome. | M12 |
lm(outcome ~ predictor + predictor, data = your_data)e.g.
lm(change ~ baseline + arm, data = bp_change)
|
The same, adjusted for a second predictor. Adding predictors is what regression is for. | M12 |
glm(outcome ~ predictor, data = your_data, family = binomial)e.g.
glm(response ~ arm, data = trial, family = binomial)
|
Logistic regression, for a yes/no outcome. | M12 |
model |> tidy(exponentiate = TRUE, conf.int = TRUE)e.g.
glm(response ~ arm, data = trial, family = binomial) |> tidy(exponentiate = TRUE, conf.int = TRUE)
|
Odds ratios (or hazard ratios) with confidence intervals. Both are switches, and both are off by default. | M12 |
Surv(your_data$time_column, your_data$event_column)e.g.
Surv(trial$time, trial$status)
|
Packages follow-up time and censoring into one outcome. Inside the model functions below, name the two columns without your_data$, because data = already says which table they come from. |
M13 |
survfit(Surv(time_column, event_column) ~ group, data = your_data)e.g.
survfit(Surv(time, status) ~ arm, data = trial)
|
Kaplan-Meier estimate, one curve per group. | M13 |
survdiff(Surv(time_column, event_column) ~ group, data = your_data)e.g.
survdiff(Surv(time, status) ~ arm, data = trial)
|
The log-rank test: do the curves differ? | M13 |
coxph(Surv(time_column, event_column) ~ group, data = your_data)e.g.
coxph(Surv(time, status) ~ arm, data = trial)
|
Cox model, for the hazard ratio. | M13 |
To draw the Kaplan-Meier curves, tidy the fit and step the line:
km <- survfit(Surv(time, status) ~ arm, data = trial)
tidy(km) |>
ggplot(aes(x = time, y = estimate, colour = strata)) +
geom_step()Reporting
| Code | What it does | Lesson |
|---|---|---|
your_table |> kable()e.g.
patients |> summary_factorlist("arm", c("age", "sex")) |> kable()
|
Typesets a data frame as a proper table in a rendered document. Pipe a table into it as the last step. | M14 |
start_report() |
Puts the report template in your working directory and opens it. | M14 |
Inline code writes a number straight into a
sentence, so your text can never disagree with your data. In a Quarto
document, wrap the expression in backticks with an r at the
front:
The trial randomised `r nrow(patients)` patients.
That comes out in the finished document as “The trial randomised 100 patients.” (M14)
Course housekeeping
| Code | What it does |
|---|---|
learn() |
List the lessons. |
learn("lesson_id")e.g.
learn("m4")
|
Open a lesson. The id is the one learn() lists, in quotes. |
check_setup() |
Check your machine and install anything missing. |
use_local_library() |
Move your package library out of OneDrive, if installs keep failing. |
Which lesson taught what
| Lesson | Tools |
|---|---|
| M2 Building blocks |
glimpse, head, class,
table, factor
|
| M3 Wrangling I |
filter, select, arrange,
desc, count, the pipe |
| M4 Wrangling II |
mutate, group_by,
summarise
|
| M5 Tidy data in practice |
pivot_wider, pivot_longer,
left_join
|
| M6 Reading & cleaning |
read_csv, read_excel,
if_else, tolower, as.numeric,
is.na
|
| M7 Visualisation I |
ggplot, aes, geom_point,
geom_boxplot, geom_bar, labs
|
| M8 Visualisation II |
geom_line, facet_wrap,
theme_minimal, scale_colour_brewer
|
| M9 Summary tables | summary_factorlist |
| M10 Describing data |
summary, geom_histogram,
mean, sd, median,
IQR
|
| M11 Comparing groups |
t.test, chisq.test, tidy
|
| M12 Regression |
lm, glm, geom_smooth
|
| M13 Survival |
Surv, survfit, survdiff,
coxph
|
| M14 Reproducible reports |
kable, inline code, start_report
|