6 Tidy Data
The verbs in the last chapter assume your data arrives in a sensible shape. Real data often does not. This chapter is about what “tidy” data means, why the tidyverse cares so much about it, and how to clean up a dataset that shows up messy.
Learning Objectives
By the end of this chapter you should be able to:
- Explain what “tidy data” means and why it matters.
- Recognize common ways real datasets violate tidiness, and describe the fix.
- Clean up awkward column names.
- Combine several verbs into a single readable pipeline that answers a real question.
6.1 Why Shape Matters
When you meet a new dataset in the wild, it is almost never in exactly the form you need. Column names are weird. Units are inconsistent. Dates are stored as strings. Some rows belong to a different year. The things you actually want to compute are combinations of things that are in the data. Data transformation is the work of getting from the dataset as it exists to the dataset you can actually analyze.
In Excel, this work happens in a mix of formulas, filters, and manual edits. In R, it happens in a script — which means it is reproducible, auditable, and easy to re-run. This is one of R’s biggest advantages.
The tidyverse package dplyr provides a small set of verbs for data transformation that are powerful enough to express most of what you will ever need. The key insight of dplyr is that most data manipulation is a sequence of simple operations, and if you have the right small vocabulary, complex transformations become readable.
6.2 Tidy Data
Before we dive into verbs, a philosophical detour.
Hadley Wickham (one of the creators of the tidyverse) has written extensively about what he calls tidy data (Wickham et al. 2023). The idea is simple: a dataset is “tidy” if:
- Every variable has its own column.
- Every observation has its own row.
- Every value has its own cell.
That sounds obvious, but a huge fraction of real datasets violate it. Common problems:
- Column headers are values, not variable names. For example, a table with columns
2020,2021,2022,2023where each column contains yields. The column header should be a variable calledyear; the values should be in a singleyieldcolumn. - Multiple variables in one column. For example, a
region_varietycolumn that contains"South_InVigor". These should be two separate columns. - Observations split across rows. For example, one row for price and another for quantity of the same product. These should be columns.
You can do analysis on untidy data, but it is much harder. Almost every tool in the tidyverse assumes tidy input. If your data is not tidy, your first step should usually be to make it tidy using tools like pivot_longer and pivot_wider (which you will meet in Module 5, or whenever it comes up naturally).
For now the point is: when you are reading in a dataset, look at the shape. Is each row an observation? Is each column a variable? If not, fixing that is the first step.
6.3 Combining It All
Let’s do something realistic. Suppose we want to answer: “For each region, what was the mean yield (in t/ha) of fields larger than 100 acres, in 2025, for the top three varieties by total production?”
yields |>
filter(year == 2025, acres > 100) |>
mutate(yield_t_ha = yield_bu_acre * 0.0560,
total_tonnes = yield_t_ha * acres * 0.4047) |>
group_by(variety) |>
mutate(variety_total = sum(total_tonnes, na.rm = TRUE)) |>
ungroup() |>
filter(dense_rank(desc(variety_total)) <= 3) |>
group_by(region, variety) |>
summarise(mean_yield_t_ha = mean(yield_t_ha, na.rm = TRUE),
n_fields = n(),
.groups = "drop") |>
arrange(region, desc(mean_yield_t_ha))This is a lot. Don’t panic. Read it top to bottom and notice how each step does one thing:
- Filter to 2025 and fields over 100 acres.
- Compute metric yield and total production for each field.
- For each variety, compute its total production across all its fields. Here we use
group_by(variety)followed bymutate(notsummarise) on purpose:summarisewould collapse each variety down to a single row, butmutatekeeps all the rows and just adds the group total as a new column on each one. (After a grouped operation, callungroup()to remove the grouping so later steps behave normally.) - Keep the top three varieties by total production.
desc(variety_total)sorts largest-first;dense_rank(...)assigns rank 1 to the biggest, 2 to the next, and so on; and<= 3keeps ranks 1, 2, and 3. - Regroup by region and variety and
summarisedown to one row per group, computing the mean yield and the number of fields (n()counts the rows in each group). The.groups = "drop"argument just tellssummariseto return an ordinary, ungrouped data frame afterward (without it, R prints a chatty message about how it left the grouping). - Sort the final table.
Do not worry about writing something this long yet — the point is that each line is one small, readable step. Writing it as a pipeline makes the logic visible. Compare with how you would do this in Excel — probably three PivotTables, a manual filter, and a lot of copy-pasting between sheets.
6.4 Renaming and Cleaning Column Names
Real-world datasets often have terrible column names: Yield (bu/ac), REGION_NAME, X1. These will work in R but are annoying to type and easy to mistype. Clean them up early:
yields |> rename(yield_bu_ac = "Yield (bu/ac)",
region = REGION_NAME)For wholesale renaming, janitor::clean_names() is invaluable. (This is from the janitor package, not the tidyverse. Install it once with install.packages("janitor").)
library(janitor)
yields <- read_csv("messy_data.csv") |> clean_names()clean_names converts everything to lowercase with underscores, strips special characters, and generally makes your column names nice. I use it on almost every dataset I read in.
6.5 Test Bank Sample
- (Concept.) Give two reasons why we use R in addition to Excel. Give one situation where Excel is still the right choice.
- (Syntax.) What does the
<-operator do in R? What doesc()do? - (Reading data.) You have a file called
yields.csvin your working directory. Write one line of R that reads it into a data frame calledyields. - (Summary.) Write R code that computes the mean, median, and standard deviation of the
yieldcolumn of theyieldsdata frame, ignoring any missing values. - (Concept.) What does “tidy data” mean? Give an example of untidy data and how to fix it.
- (dplyr.) Write a
dplyrpipeline that filtersyieldsto fields in the South region, computes yield in t/ha, and sorts descending by yield. - (group_by.) Write a pipeline that computes the mean and standard deviation of yield for each variety, for fields larger than 50 acres.
- (case_when.) A dataset has a
yieldcolumn and aunitscolumn. Write code to create ayield_t_hacolumn that converts as needed. - (Missing values.) Why is it dangerous to blindly drop missing values? Under what conditions is it safe?
- (Scripting.) Explain why the following is a reproducibility problem: > “I changed the CSV file in Excel, then re-ran my R script.”
- (AI.) Describe one situation where using an AI coding assistant would help you, and one where it could lead you astray.
6.6 Practice Exercises
Use the field_yields.csv dataset for these.
- Install R and Positron. Run the script from Section 4.4.
- Create a vector of the heights (in cm) of five people. Compute the mean, median, and standard deviation.
- Read in
field_yields.csvand reproduce a table of summary statistics (mean, median, and standard deviation of yield) usingdplyrverbs. - Filter to one region and one year, and find the variety with the highest mean yield.
- Create a new column that flags whether each field is “above average” in yield for its region.
- The dataset has a few fields with a missing (
NA) yield. Find them withfilter(is.na(yield_bu_acre)), then compute the mean yield with and withoutna.rm = TRUEand explain the difference. - Add a
yield_t_hacolumn convertingyield_bu_acreto tonnes per hectare (× 0.0560), then sort the fields from highest to lowest metric yield. - Rewrite your Module 1 worked example (canola yields) as an R script. Compare the result with what you got in Excel.
- Break your script intentionally (misspell a column name) and read the error message. Can you fix it?