EDS 221 Day 7 PM

Reshaping data


August 18th, 2026

Tidy data


A data frame is tidy when:

  • Every variable is a column
  • Every observation is a row
  • Every value is a cell

The same values can be arranged into many different table shapes — only one of them is tidy. Reshaping moves values between rows and columns without changing what they mean.

Meet the data: penguins, summarized


Average body mass by species and sex — one row per group. This is long data: each combination of species and sex gets its own row.

library(tidyverse)
library(palmerpenguins)

mass_by_sex <- penguins |>
  filter(!is.na(sex)) |>
  summarize(mean_mass = mean(body_mass_g, na.rm = TRUE), .by = c(species, sex))

mass_by_sex
# A tibble: 6 × 3
  species   sex    mean_mass
  <fct>     <fct>      <dbl>
1 Adelie    male       4043.
2 Adelie    female     3369.
3 Gentoo    female     4680.
4 Gentoo    male       5485.
5 Chinstrap female     3527.
6 Chinstrap male       3939.

pivot_wider(): making data wider


pivot_wider() spreads one column’s values into new columns. names_from supplies the new column names; values_from supplies the cells.

mass_wide <- mass_by_sex |>
  pivot_wider(names_from = sex, values_from = mean_mass)

mass_wide
# A tibble: 3 × 3
  species    male female
  <fct>     <dbl>  <dbl>
1 Adelie    4043.  3369.
2 Gentoo    5485.  4680.
3 Chinstrap 3939.  3527.

pivot_wider(): why reshape?


Wide data makes it easy to compare values that were in different rows — now they’re in different columns of the same row.

mass_wide |>
  mutate(diff = male - female)
# A tibble: 3 × 4
  species    male female  diff
  <fct>     <dbl>  <dbl> <dbl>
1 Adelie    4043.  3369.  675.
2 Gentoo    5485.  4680.  805.
3 Chinstrap 3939.  3527.  412.

This comparison would need a join if male and female were still in separate rows.

pivot_longer(): making data longer


pivot_longer() is the reverse: it gathers columns into rows. cols says which columns to gather; names_to/values_to name where they land.

mass_wide |>
  pivot_longer(
    cols = c(female, male),
    names_to = "sex",
    values_to = "mean_mass"
  )
# A tibble: 6 × 3
  species   sex    mean_mass
  <fct>     <chr>      <dbl>
1 Adelie    female     3369.
2 Adelie    male       4043.
3 Gentoo    female     4680.
4 Gentoo    male       5485.
5 Chinstrap female     3527.
6 Chinstrap male       3939.

pivot_longer(): a messier example


billboard tracks each song’s chart rank in a separate column per week — wk1 through wk76. That’s 76 columns encoding one variable: week.

billboard |>
  select(artist, track, wk1, wk2, wk3)
# A tibble: 317 × 5
   artist         track                     wk1   wk2   wk3
   <chr>          <chr>                   <dbl> <dbl> <dbl>
 1 2 Pac          Baby Don't Cry (Keep...    87    82    72
 2 2Ge+her        The Hardest Part Of ...    91    87    92
 3 3 Doors Down   Kryptonite                 81    70    68
 4 3 Doors Down   Loser                      76    76    72
 5 504 Boyz       Wobble Wobble              57    34    25
 6 98^0           Give Me Just One Nig...    51    39    34
 7 A*Teens        Dancing Queen              97    97    96
 8 Aaliyah        I Don't Wanna              84    62    51
 9 Aaliyah        Try Again                  59    53    38
10 Adams, Yolanda Open My Heart              76    76    74
# ℹ 307 more rows

pivot_longer(): selecting with starts_with()


cols accepts the same tidyselect helpers as select().

billboard |>
  pivot_longer(
    cols = starts_with("wk"),
    names_to = "week",
    values_to = "rank"
  )
# A tibble: 24,092 × 5
   artist track                   date.entered week   rank
   <chr>  <chr>                   <date>       <chr> <dbl>
 1 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk1      87
 2 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk2      82
 3 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk3      72
 4 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk4      77
 5 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk5      87
 6 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk6      94
 7 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk7      99
 8 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk8      NA
 9 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk9      NA
10 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk10     NA
# ℹ 24,082 more rows

Selecting columns to pivot


cols can be written several ways — pick whichever most clearly states your intent.

  • c(female, male) — name them explicitly
  • female:male — a range of adjacent columns
  • starts_with("wk") — match a naming pattern
  • !year — everything except a column

What to memorize


The same values can live in a wide or a long table — reshaping picks whichever shape fits the next step of your analysis.

Wider

  • pivot_wider()
  • names_from, values_from
  • Good for comparing values across a row

Longer

  • pivot_longer()
  • cols, names_to, values_to
  • Good for plotting and grouped summaries