Section 1 of 8

1 Before you start

Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.

Pre-test

A categorical variable such as treatment arm has no mean. How should you summarise it?

Pre-test

Which line gives a count and proportion for each category of the sex column in a data frame called patients?

Pre-test

After tabyl(smoker), which adorn helper turns the raw proportions into readable percentages?

Pre-test

You want a table of sex down the rows and smoking status across the columns. Which call builds that cross-tabulation?

Pre-test

Your call mean(hba1c) returns NA because the column has one missing value. What is R telling you?

Pre-test

Your call mean(hba1c) returns NA because the column has one missing value. Which line reports the mean of the values you do have?

Pre-test

Which expression reports how many values in the hba1c column are missing?

Pre-test

You set na.rm = TRUE and report the median. Why should you also report n missing beside it?

Pre-confidence

I can summarise a categorical variable with tabyl() as counts and proportions, and format the proportions as readable percentages.

Not at all confident
Fully confident
Pre-confidence

I can cross-tabulate two categorical variables with tabyl() and add totals and row percentages to compare the groups.

Not at all confident
Fully confident
Pre-confidence

I can explain why mean() returns NA when a value is missing, fix it with na.rm = TRUE, and count and report the missing values with sum(is.na(column)) so my summary stays honest.

Not at all confident
Fully confident
Section 2 of 8

2 Introduction

In Part I you summarised a numeric column with the right centre-and-spread pair — mean (SD) for a symmetric variable, median (IQR) for a skewed one. But many of the columns your reader cares about are not numbers at all: sex, treatment arm, smoking status. And every real clinical dataset has holes — a patient skips a blood test and the cell is blank. This part gives you the tools for both: summarising categories, and summarising honestly when values are missing.

This part of the module covers two skills:

  • Categorical summaries — counts and proportions for each group with tabyl(), and a cross-tabulation of two categorical variables to ask whether they are related.
  • Honest summaries — handling missing values with na.rm = TRUE, and counting and reporting how many patients you dropped so the holes never hide.

By the end of this part you will be able to tabulate a categorical variable as counts and proportions, cross-tabulate two categories and add totals and row percentages, explain why mean() returns NA when a value is missing, fix it with na.rm = TRUE, and count missing values with sum(is.na(column)) so you never silently hide missing data.

Try every snippet in the R Scratchpad on the right. The datasets patients.csv and diabetes_clinic.csv are already loaded — the first for the categorical tables, the second (which contains missing HbA1c values) for the honest-summary examples.

Section 3 of 8

3 Summarising categorical variables

Location and spread describe numbers. A categorical variable — sex, treatment arm, smoking status — has no average. You summarise it with a count (how many patients in each group) and a proportion (what fraction of the total each group is). Counts and proportions are to categories what the mean and SD are to numbers.

The cleanest way to build a count table is tabyl() from the janitor package. Give it a data frame and a column, and it returns the count and the proportion in one tidy table.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(readr)
library(janitor)
patients <- read_csv("patients.csv")
patients |> tabyl(sex)
A categorical variable has no mean, so you summarise it with a count and a proportion per group, which tabyl() produces and adorn_pct_formatting() renders as percentages.
A categorical variable has no mean, so you summarise it with a count and a proportion per group, which tabyl() produces and adorn_pct_formatting() renders as percentages.

The adorn_*() helpers then dress it up — adorn_pct_formatting() turns the raw proportions into readable percentages.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(readr)
patients <- read_csv("patients.csv")
library(janitor)
patients |>
  tabyl(smoker) |>
  adorn_pct_formatting()
tabyl() builds a frequency table by counting how many rows fall into each category and storing the share as a proportion, which adorn_pct_formatting() then rescales into a readable percentage.
tabyl() builds a frequency table by counting how many rows fall into each category and storing the share as a proportion, which adorn_pct_formatting() then rescales into a readable percentage.
Section 3.1 of 8

3.1 Cross-tabulating two categories

To ask whether two categories are related — does smoking differ by sex? — you build a cross-tabulation: a table with one variable down the rows and the other across the columns, counting patients in each cell. Pass tabyl() two column names to get one.

The adorn_*() helpers add the finishing touches a reader expects: adorn_totals() adds row and column totals, and adorn_percentages() converts the counts to proportions within each row.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(readr)
patients <- read_csv("patients.csv")
library(janitor)
patients |>
  tabyl(sex, smoker) |>
  adorn_totals(where = "row")
A cross-tabulation counts how two categorical variables overlap (sex by smoking status), and adorning it with totals and row percentages lets you compare groups (here, 20% of women smoke versus 40% of men) to judge whether the two categories are related.
A cross-tabulation counts how two categorical variables overlap (sex by smoking status), and adorning it with totals and row percentages lets you compare groups (here, 20% of women smoke versus 40% of men) to judge whether the two categories are related.
Section 4 of 8

4 Honest summaries: handling missing values

Real clinical data has holes. A patient skips a blood test and that HbA1c cell is NA — R's marker for a missing value, carried over from Module 1. How you treat those holes decides whether your summary is honest.

By default, R is cautious. If even one value in a column is NA, mean() and sd() return NA, not a number. R is telling you: I cannot average values I do not have. This is a feature, not a bug — it stops a silent wrong answer.

To summarise the values you do have, add the argument na.rm = TRUE — read it as "remove the NAs first". Then mean() averages the non-missing values. The same argument works for sd(), median(), IQR(), and friends.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
median(c(5, 7, NA, 9), na.rm = TRUE)
By default any NA makes mean() and its friends return NA, and na.rm = TRUE tells R to drop the missing values first and summarise only the data you actually have.
By default any NA makes mean() and its friends return NA, and na.rm = TRUE tells R to drop the missing values first and summarise only the data you actually have.

The first trap is forgetting na.rm = TRUE, so your summary silently returns NA and you report a blank where a number belonged. If a summary comes back as NA, suspect a missing value and add na.rm = TRUE.

Debug & fix

The code below is broken. Type a fixed version into the editor, then click Run & check. Success means your code runs without errors. Use Show hint only if you get stuck.

Broken code (do not copy verbatim)
mean(c(7.2, 8.1, NA, 9.4))
In R, a single NA propagates through summary functions like mean() so the result is NA, and na.rm = TRUE tells the function to skip missing values and compute over the values you actually have.
In R, a single NA propagates through summary functions like mean() so the result is NA, and na.rm = TRUE tells the function to skip missing values and compute over the values you actually have.

The second trap is quieter and worse: dropping incomplete rows without reporting how many you lost. Setting na.rm = TRUE hides the holes; an honest summary states them. Always count the missing values and report n missing alongside your centre and spread.

Count missing values by combining two functions: is.na() marks each value TRUE if it is missing, and sum() adds up those TRUEs (because R treats TRUE as 1). So sum(is.na(column)) is the number of missing values.

Try it out

Try this snippet in the R Scratchpad on the right.

Try this snippet
library(readr)
library(dplyr)
clinic <- read_csv("diabetes_clinic.csv")
clinic |>
  summarise(
    n_missing = sum(is.na(hba1c)),
    median_hba1c = median(hba1c, na.rm = TRUE),
    iqr_hba1c = IQR(hba1c, na.rm = TRUE)
  )
Missing values must be counted with sum(is.na(column)) and reported as n_missing beside the median and IQR, because na.rm = TRUE drops the gaps silently instead of telling you they were ever there.
Missing values must be counted with sum(is.na(column)) and reported as n_missing beside the median and IQR, because na.rm = TRUE drops the gaps silently instead of telling you they were ever there.
Section 5 of 8

5 Put it together

Let’s try this Parsons problem for practice

This Parsons problem gives you the right lines in the wrong order, plus a few lines that do not belong. Drag the correct lines into order and leave the wrong ones in the bank.

Parsons problem · An honest median summary with missing data

All the lines you need are in the Line bank on the left — some may be distractors you should leave behind. Drag the lines you need into the Your solution column on the right, in the correct order, then click Check.

Task: Read the clinic file, then in one summarise() report the number of missing HbA1c values, the median, and the IQR — all missing-aware.

Line bank
  • library(dplyr)
  • clinic <- read_csv("diabetes_clinic.csv")
  • summarise(
  • med = median(hba1c, na.rm = TRUE),
  • )
  • iqr = IQR(hba1c, na.rm = TRUE)
  • clinic |> summarise(med = median(hba1c))
  • n_missing = sum(is.na(hba1c)),
  • clinic |>
  • n_missing <- sum(NA)
  • clinic |> mean(hba1c, na.rm = TRUE)
Your solution
  • Drop lines here, in order.

The worked example below fades the support as you go: first you study a full solution, then you fill the gap, then you solve a fresh one on your own. It walks through the whole decision — check for missing values, compare mean and median, and report the right pair.

Worked example · Summarise a right-skewed lab honestly

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: You have eight CRP results (mg/L), one of them missing: 4, 5, 6, 7, 9, 12, 45, and an NA. Count the missing value, then compare the mean and median to decide which centre to report.

Stage 1 · Study the solved example
Fully solved solution
crp <- c(4, 5, 6, 7, 9, 12, 45, NA)
sum(is.na(crp))
mean(crp, na.rm = TRUE)
median(crp, na.rm = TRUE)
Walk-through
  1. sum(is.na(crp)) counts the missing values so you can report n missing
  2. mean(crp, na.rm = TRUE) averages the seven values you have
  3. median(crp, na.rm = TRUE) finds the middle value, resistant to the high 45
  4. the mean lands well above the median, so this lab is right-skewed — report median (IQR)
When a lab value is right-skewed, the mean is pulled upward by the high outlier while the median stays in the middle, so the honest summary to report is the median with its IQR rather than the mean.
When a lab value is right-skewed, the mean is pulled upward by the high outlier while the median stays in the middle, so the honest summary to report is the median with its IQR rather than the mean.
Section 6 of 8

6 Check your understanding

You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.

Post-test

A categorical variable such as treatment arm has no mean. How should you summarise it?

Post-test

Which line gives a count and proportion for each category of the sex column in a data frame called patients?

Post-test

After tabyl(smoker), which adorn helper turns the raw proportions into readable percentages?

Post-test

You want a table of sex down the rows and smoking status across the columns. Which call builds that cross-tabulation?

Post-test

Your call mean(hba1c) returns NA because the column has one missing value. What is R telling you?

Post-test

Your call mean(hba1c) returns NA because the column has one missing value. Which line reports the mean of the values you do have?

Post-test

Which expression reports how many values in the hba1c column are missing?

Post-test

You set na.rm = TRUE and report the median. Why should you also report n missing beside it?

Post-confidence

I can summarise a categorical variable with tabyl() as counts and proportions, and format the proportions as readable percentages.

Not at all confident
Fully confident
Post-confidence

I can cross-tabulate two categorical variables with tabyl() and add totals and row percentages to compare the groups.

Not at all confident
Fully confident
Post-confidence

I can explain why mean() returns NA when a value is missing, fix it with na.rm = TRUE, and count and report the missing values with sum(is.na(column)) so my summary stays honest.

Not at all confident
Fully confident
Section 7 of 8

7 Your results

Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.

Your score

Submit the post-test to see your results.

Muddiest point

What is the one thing from this module that is still unclear to you?

Rate this module

Overall, how would you rate this module?

How likely are you to recommend this module to a peer? (0 = not at all, 10 = extremely likely)