Skip to main content

Wrangle Penguins with dplyr

Filter rows, chain steps with pipes, reshape tables, and join metadata

In Coding 5 you built figures with ggplot2. Today you will reshape tables with dplyr and tidyr: filter rows with conditions, add columns, chain steps with pipes, and join metadata. The two tables below are a reference for the whole activity and for your assignments.

The first time you click Run Code, your browser downloads R once (a few seconds). After that it is quick.

Function What it does Example
glimpse() Shows rows, columns, and column types glimpse(penguins)
filter() Keeps rows that meet a condition filter(penguins, species == "Adelie")
select() Keeps only the columns you name select(penguins, species, body_mass_g)
mutate() Adds a new column computed from existing ones mutate(penguins, body_mass_kg = body_mass_g / 1000)
arrange() Sorts rows by a column; wrap in desc() for largest first arrange(penguins, desc(body_mass_g))
summarise() Collapses many rows into one summary number summarise(penguins, avg = mean(body_mass_g, na.rm = TRUE))
group_by() Makes the next summary happen once per group penguins |> group_by(species) |> summarise(n = n())
count() Counts rows per group in one step count(penguins, species)
pull() Takes one column out as a plain vector pull(penguins, body_mass_g)
distinct() Lists each unique value once distinct(penguins, island)
pivot_longer() Turns wide column headings into tidy rows (from tidyr) pivot_longer(island_wide, cols = everything(), names_to = "island", values_to = "elev")
left_join() Adds columns from a second table by a shared key left_join(penguins, island_tidy, by = "island")
|> Passes the result of one step into the next penguins |> filter(species == "Adelie") |> count(island)

Posit, the company that makes RStudio, publishes a one-page summary of all of these: dplyr cheat sheet (web page) or PDF. Keep it open while you work on assignments.

Write Meaning Example inside filter()
== is equal to species == "Adelie"
!= is not equal to species != "Gentoo"
> and >= greater than, greater than or equal to body_mass_g >= 4500
< and <= less than, less than or equal to flipper_length_mm < 190
%in% is one of these values species %in% c("Adelie", "Chinstrap")
is.na() is missing is.na(body_mass_g)
! not; flips TRUE and FALSE !is.na(body_mass_g)
& or a comma both must be true species == "Gentoo", body_mass_g >= 4500
| either can be true island == "Dream" | island == "Biscoe"

Text goes in quotes ("Adelie"); numbers do not (4500). One = assigns a value; two == compares. Each condition produces TRUE or FALSE for every row, and filter() keeps the TRUE rows.

1. Load the tidyverse

Coding Assignment 2 starts the same way. Run library(tidyverse) to load dplyr, tidyr, and friends. A conflicts message afterward is expected, not an error.

NoteHint

From ?library: library(package, ...) attaches a package so its functions become available. Replace package with the name of the collection you need (dplyr, tidyr, and others ship together in one bundle).

Try ?library in R for the full help page.

TipSolution
library(tidyverse)

On DataHub and in your graded assignment you will type exactly this. The tidyverse also offers an older pipe, %>%; in this activity we use R’s native pipe, |>.

2. Glimpse the penguins

glimpse() is a tidyverse way to inspect a dataset: rows, columns, and column types. Run it on penguins from the palmerpenguins package.

NoteHint

From ?glimpse: glimpse(x) — print a compact summary of a data frame (rows, columns, types).

Example with a built-in dataset:

glimpse(mtcars)

Apply the same function to the dataset named in the exercise prompt.

TipSolution
glimpse(penguins)

You should see 344 rows and 8 columns. species and island are <fct> (categories); measurements like body_mass_g are <dbl> (numbers).

From the glimpse() output:

  1. How many rows are there?
  2. What type is body_mass_g?
  3. Which column tells you which island each penguin was sampled on?

Answers: 344 rows; body_mass_g is <dbl>; island.

3. Chain steps with a pipe

A pipe passes the result of one step into the next. R’s native pipe is |>. Rewrite the nested version below as a pipe chain: start with penguins, keep only Adelie penguins with filter(), then glimpse() the result.

Note== means “equals”

Inside filter(), == checks whether two things match. For example, species == "Adelie" keeps only rows where the species column is exactly "Adelie".

Do not confuse == with a single =. A single = assigns a value to a name (x <- 5 or x = 5). Double equals == compares — “is this equal to that?”

Nested (hard to read):

glimpse(filter(penguins, species == "Adelie"))

Your version should use |> twice.

NoteHint

From ?filter: keep rows where conditions are TRUE. Chain steps with |>:

mtcars |>
  filter(cyl == 4) |>
  glimpse()

Use the same shape on penguins: start with the dataset, filter() to one group, then glimpse().

TipSolution
penguins |>
  filter(species == "Adelie") |>
  glimpse()

Read it top to bottom: “Take penguins, filter to Adelie, then glimpse.” The pipe replaces nesting functions inside functions.

4. Filter out missing values

Two penguins have missing body_mass_g. Use filter() and !is.na() to drop those rows. Save the result as penguins_clean.

Note! means “not”

The exclamation mark ! flips a yes/no test backwards. The function is.na(body_mass_g) is TRUE when a value is missing. So !is.na(body_mass_g) means “body mass is not missing” — keep those rows.

NoteHint

From ?filter: filter(.data, ...) keeps matching rows. From ?is.na: is.na(x) is TRUE for missing values; prefix with ! to mean “not.”

Example — drop rows with missing mpg:

mtcars_clean <- mtcars |>
  filter(!is.na(mpg))

Use the same pattern on penguins, the column named in the prompt, and save as penguins_clean.

TipSolution
penguins_clean <- penguins |>
  filter(!is.na(body_mass_g))

You should have 342 rows — two fewer than the original 344.

5. Mean with a pipe

Compute the mean body mass of penguins_clean, skipping any remaining NAs with na.rm = TRUE. Use a pipe starting from penguins_clean.

NoteHint

From ?mean: mean(x, na.rm = FALSE) — set na.rm = TRUE to skip missing values. From ?pull: pull(.data, var) extracts one column.

Example pipe:

mtcars |>
  pull(mpg) |>
  mean(na.rm = TRUE)

Start from penguins_clean and pipe to mean() on the body-mass column.

TipSolution
penguins_clean |>
  pull(body_mass_g) |>
  mean(na.rm = TRUE)

You should see about 4,202 g — the average weight across penguins with a recorded body mass.

6. Add a column with mutate(), then filter() with two conditions

mutate() adds a new column computed from existing ones. Body mass is recorded in grams; add body_mass_kg by dividing by 1000. Then filter() to the heavy Gentoo penguins: body_mass_kg >= 4.5 and species == "Gentoo". Save the result as heavy_gentoo and print its row count. You should get 107.

NoteHint

From ?mutate: mutate(.data, new_name = expression) creates a column from a calculation. From ?filter: separate two conditions with a comma (both must be true). The conditions table at the top of the page lists >= and friends.

Example:

mtcars |>
  mutate(wt_kg = wt * 453.6) |>
  filter(wt_kg >= 1500, cyl == 8) |>
  nrow()

Apply the same shape: a unit conversion in mutate(), then a numeric condition and a text condition in filter().

TipSolution
heavy_gentoo <- penguins_clean |>
  mutate(body_mass_kg = body_mass_g / 1000) |>
  filter(body_mass_kg >= 4.5, species == "Gentoo")
nrow(heavy_gentoo)

107 rows. The comma inside filter() means “and”: a row must pass both tests. mutate() comes first so that filter() can use the new column.

  1. What would change if you wrote > instead of >=?
  2. How would you keep heavy penguins of every species?
  3. How would you keep Adelie and Chinstrap penguins but not Gentoo?

Answers: (1) Penguins weighing exactly 4.5 kg would be dropped; here that changes nothing, but it often matters. (2) Remove the species condition: 118 rows. (3) species != "Gentoo", or species %in% c("Adelie", "Chinstrap"): 219 rows either way.

7. Meet a wide table

On Coding Assignment 2 you will read a wide elevation table from a CSV file. Here we create the same kind of object directly — no file to read.

A wide table stores category names as column headings. Below is a tiny lookup table with fictional camp elevations (meters) for each study island:

island_wide <- data.frame(
  Biscoe = 120,
  Dream = 85,
  Torgersen = 45
)

Notice: Biscoe, Dream, and Torgersen are column names, not values sitting in a row. That layout is easy to glance at in a spreadsheet but awkward for joining to penguins — we will reshape it in the next step.

On your graded assignment, the same wide layout arrives as a CSV; you will use read.csv() to load it. Today we build the object in memory so you can focus on reshaping and joining.

Run this to create island_wide:

8. Reshape with pivot_longer()

Use pivot_longer() to turn the wide table into a tidy table: one row per island, with columns island and camp_elevation_m. Save as island_tidy and print it.

NoteHint

From ?pivot_longer: pivot_longer(data, cols, names_to, values_to, ...)

Turn column names into values — example with a wide toy table:

wide_mpg <- data.frame(
  four_cyl = 26.7,
  six_cyl = 20.1,
  eight_cyl = 15.1
)

wide_mpg |>
  pivot_longer(
    cols = everything(),
    names_to = "cyl_group",
    values_to = "avg_mpg"
  )

Apply the same arguments to island_wide, with the column names specified in the exercise.

TipSolution
island_tidy <- island_wide |>
  pivot_longer(
    cols = everything(),
    names_to = "island",
    values_to = "camp_elevation_m"
  )
island_tidy

Three rows — one per island — with elevations in a column where R can match them to penguins$island.

Wide vs. tidy: In Coding Assignment 2, population names sit in column headings of the elevation file — same problem, same fix with pivot_longer().

9. Join elevation onto penguins

Use left_join() to add camp_elevation_m to penguins_clean. Both tables share an island column — that is the key. Save the result as penguins_joined. Row count should stay 342.

NoteHint

From ?left_join: left_join(x, y, by = NULL, ...) — add columns from y to x wherever a shared key matches.

Example:

cyl_lookup <- data.frame(
  cyl = c(4, 6, 8),
  cyl_label = c("four", "six", "eight")
)

mtcars |>
  left_join(cyl_lookup, by = "cyl")

Join your cleaned penguin table to island_tidy on the column both tables share.

TipSolution
penguins_joined <- penguins_clean |>
  left_join(island_tidy, by = "island")
nrow(penguins_joined)

342 rows, one new column — every penguin keeps its measurements and gains its island’s camp elevation.

10. Recap — functions you practiced

Function Role
library(tidyverse) Loads dplyr, tidyr, and related packages
glimpse() Quick look at rows, columns, and types
|> Passes data from one step to the next
filter() Keeps rows that match a condition (==, >=, !is.na(), or several separated by commas)
mean(..., na.rm = TRUE) Average of a numeric column, skipping missing values
mutate() Adds a new column computed from existing ones (grams to kilograms)
data.frame() Builds a wide table in memory — island names as column headings
pivot_longer() Turns wide columns into tidy rows
left_join() Adds columns from a second table by a shared key

On Coding Assignment 2 you will also use read.csv() to load the hemoglobin and elevation files from disk — same workflow, but the wide table arrives as a file instead of a data.frame() you type yourself.

If you can explain each row in your own words, you have covered the wrangling core of Coding Assignment 2.

11. Transfer to Coding Assignment 2

On Canvas you will repeat this workflow with hemoglobin measurements and population elevation data:

  1. library(tidyverse) and read HumanHemoglobinElevation.csv
  2. glimpse(), filter(!is.na(...)), and mean(..., na.rm = TRUE)
  3. Read PopulationElevation.csv (wide layout), pivot_longer(), then left_join()

Same functions, different biological story — altitude adaptation instead of penguin islands. You already practiced every wrangling step here.

The assignment also asks you to build a boxplot with reorder() so groups sort by elevation. You learned ggplot layering in Coding Activity 5; on Canvas you combine that with the joined table you prepare here.

Keep playing

No grading here. Try filtering to one species, chaining another summary through a pipe, or predicting how many rows a filter will leave before you run it.

Try this with a partner. One of you predicts the average Gentoo body mass; the other runs the pipe. Swap roles. Saying your prediction out loud first is one of the fastest ways to learn.