Skip to contents

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 arm

A 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 dplyr

It 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