Wrangle and Visualize Penguins
Filter rows, reshape tables, join metadata, and build layered ggplot2 figures — all in your browser
Welcome back. Everything on this page runs real R in your browser — same setup as Coding Activity 1. You already know objects and functions; today you will reshape tables with dplyr and tidyr, then build figures in layers with ggplot2.
The first time you click Run Code, your browser downloads R once (a few seconds). After that it is quick.
Every ggplot2 figure has three essential pieces:
- data — the dataset (for example
penguins) - aes — aesthetic mappings: which column goes on which axis, or which color
- geom — the geometric object: points, boxes, bars, histogram bins
You add layers with +. We will practice all three today.
1. Load the tidyverse
Coding Assignment 2 starts the same way. Run library(tidyverse) to load dplyr, ggplot2, tidyr, and friends. A conflicts message afterward is expected, not an error.
From ?library: library(package, ...) attaches a package so its functions become available. Replace package with the name of the collection you need (dplyr, ggplot2, tidyr, and others ship together in one bundle).
Try ?library in R for the full help page.
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 (loaded automatically with tidyverse in webR).
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.
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:
- How many rows are there?
- What type is
body_mass_g? - 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.
== 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.
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().
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.
! 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.
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.
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.
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.
penguins_clean |>
pull(body_mass_g) |>
mean(na.rm = TRUE)You should see about 4,002 g — the average weight across penguins with a recorded body mass.
6. 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:
7. 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.
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.
island_tidy <- island_wide |>
pivot_longer(
cols = everything(),
names_to = "island",
values_to = "camp_elevation_m"
)
island_tidyThree 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().
8. 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.
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.
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.
9. Histogram with ggplot2
Start a figure with ggplot(), map body_mass_g to the x-axis, and add geom_histogram(). Use penguins_joined (or penguins_clean). Add a title with labs(title = "...").
From ?ggplot: ggplot(data = NULL, mapping = aes(), ...) — then add a layer with +.
From ?geom_histogram: bins one continuous variable.
Example:
ggplot(mtcars, aes(x = mpg)) +
geom_histogram() +
labs(title = "Car fuel economy")Map the x-axis to one continuous column from your joined penguin table and add your own title.
ggplot(penguins_joined, aes(x = body_mass_g)) +
geom_histogram() +
labs(title = "Penguin body mass")A histogram shows how one continuous variable is distributed — how many penguins fall in each weight range.
10. Scatterplot — two continuous variables
Build a scatterplot: flipper length on the x-axis, body mass on the y-axis. Map species to color so each species gets its own hue. Use geom_point().
From ?geom_point: scatterplot of two continuous variables. Map a third variable to color inside aes().
Example:
ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
geom_point()Use flipper length and body mass on the axes and map species to color on the penguin data.
ggplot(penguins_joined, aes(
x = flipper_length_mm,
y = body_mass_g,
color = species
)) +
geom_point()Scatterplots relate two continuous measurements. Color adds a third variable — here, species — so you can see clusters.
11. Boxplot — compare groups (Assignment 2 pattern)
Coding Assignment 2 builds a boxplot one layer at a time. Here is the full pattern in one exercise:
- Put island on the x-axis, body_mass_g on the y-axis
- Use
reorder(island, camp_elevation_m)so islands sort from lowest to highest elevation - Add
geom_boxplot()withfillmapped to island - Add
theme_minimal()and readablelabs()
From ?geom_boxplot: compare a numeric variable across groups. From ?reorder: reorder(x, X) sorts groups by a numeric column.
Example pattern:
ggplot(mtcars, aes(
x = reorder(factor(cyl), mpg, FUN = median),
y = mpg,
fill = factor(cyl)
)) +
geom_boxplot() +
theme_minimal() +
labs(title = "MPG by cylinder count", x = "Cylinders", y = "MPG")Apply the same layering — boxplot, fill, theme_minimal(), labs() — to islands and body mass on your joined table.
ggplot(penguins_joined, aes(
x = reorder(island, camp_elevation_m),
y = body_mass_g,
fill = island
)) +
geom_boxplot() +
theme_minimal() +
labs(
title = "Body mass by island",
x = "Island (low to high elevation)",
y = "Body mass (g)"
)Boxplots compare a continuous variable across groups. reorder() lines groups up by elevation — the same trick Assignment 2 uses with population and elevation.
12. Recap — verbs you practiced
| Function | Role |
|---|---|
library(tidyverse) |
Loads dplyr, ggplot2, tidyr, and related packages |
glimpse() |
Quick look at rows, columns, and types |
|
> |
filter() |
Keeps rows that match a condition (with == or !is.na()) |
mean(..., na.rm = TRUE) |
Average of a numeric column, skipping missing values |
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 |
ggplot() + aes() |
Starts a figure and maps columns to axes, color, or fill |
geom_histogram() |
Distribution of one continuous variable |
geom_point() |
Relationship between two continuous variables |
geom_boxplot() |
Compare a continuous variable across groups |
reorder() |
Sorts groups by a numeric variable (e.g. elevation) |
theme_minimal() + labs() |
Cleans appearance and adds titles/labels |
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 core of Coding Assignment 2.
13. Transfer to Coding Assignment 2
On Canvas you will repeat this workflow with hemoglobin measurements and population elevation data:
library(tidyverse)and readHumanHemoglobinElevation.csvglimpse(),filter(!is.na(...)), andmean(..., na.rm = TRUE)- Read
PopulationElevation.csv(wide layout),pivot_longer(), thenleft_join() - Build a boxplot step by step with
ggplot(),geom_boxplot(),fill,theme_minimal(), andlabs()
Same functions, different biological story — altitude adaptation instead of penguin islands. You already practiced every step here.
Keep playing
No grading here. Try swapping geoms, filtering to one species first, or predicting what a plot will look like before you run it.
Try this with a partner. One of you predicts the figure; the other runs the code. Swap roles. Saying your prediction out loud first is one of the fastest ways to learn.