Common Code 05 — mutate()
Creating and transforming columns — ecological math, logarithms, and regression equations
how to do math and add columns based on calculations or mutations
Creating new columns with mutate()
mutate() adds new columns to a data frame, or overwrites existing ones. Each new column is computed row by row from whatever expression you give it — simple arithmetic, logarithms, conditional logic, or a full regression equation applied to every observation at once.
⬇️ Download the companion R script — all examples ready to run:
05_mutate.R
💡 Following along? Examples use
penguinsfrompalmerpenguins. The ecological math sections apply directly to any field data you collect.
Packages needed
library(tidyverse)
library(palmerpenguins)1 · The basics
mutate() always goes at the end of a pipe, after your filter() and select() steps. New columns can immediately reference other columns just created in the same mutate() call:
penguins |>
mutate(
mass_kg = body_mass_g / 1000,
flipper_m = flipper_length_mm / 1000,
mass_per_flip = mass_kg / flipper_m # uses both columns just made above
)💡 Key idea:
mutate()never changes existing rows or drops columns — it only adds or replaces. The original data frame is untouched unless you overwrite it with<-. Store results in a new object with a descriptive name.
2 · Simple arithmetic
The operators are what you expect: +, -, *, /, ^ for power. Reference any column by name, mix columns with constants freely:
penguins |>
mutate(
mass_kg = body_mass_g / 1000, # unit conversion g → kg
bill_ratio = bill_length_mm / bill_depth_mm, # length-to-depth ratio
bill_area_mm2 = bill_length_mm * bill_depth_mm, # rough bill area proxy
mass_centered = body_mass_g - mean(body_mass_g, na.rm = TRUE) # centre on mean
)💡 Bill ratios and shape indices are a classic ecology move — dividing two measurements to capture shape independent of size. The same idea appears in body condition indices, cephalic indices, and leaf shape metrics.
3 · Logarithms — the most important transformation in ecology
Log-transformation is everywhere in ecology: species–area relationships, metabolic scaling, abundance distributions, allometric growth. Understanding which log function to use matters.
penguins |>
mutate(
log10_mass = log10(body_mass_g), # base-10 — most interpretable
ln_mass = log(body_mass_g), # natural log (base e) — used in models
log2_mass = log2(body_mass_g) # base-2 — used in some genetics work
)⚠️ Watch out!
log()in R is the natural log (base e ≈ 2.718), NOT base-10. This catches almost everyone the first time. Uselog10()when you want base-10. When in doubt, be explicit.
Which log to use:
| Log | R function | Use when |
|---|---|---|
| Base 10 | log10() |
Plotting, interpreting orders of magnitude, species–area |
| Natural (base e) | log() |
Statistical models (GLMs, regression), likelihood |
| Base 2 | log2() |
Gene expression, information theory |
Back-transforming
Always know how to undo a transformation — you will need it when reporting model predictions on the original scale:
penguins |>
mutate(
log10_mass = log10(body_mass_g),
mass_back = 10 ^ log10_mass, # undo log10: 10^x
ln_mass = log(body_mass_g),
mass_back_ln = exp(ln_mass) # undo natural log: e^x
)4 · Exponents and powers
The ^ operator raises to any power. This is directly useful for allometric and metabolic scaling, which follow power laws:
penguins |>
mutate(
mass_kg = body_mass_g / 1000,
metabolic_est = mass_kg ^ 0.75, # Kleiber's Law: metabolic rate ∝ mass^0.75
flipper_area = flipper_length_mm ^ 2, # area scales as length²
flipper_vol = flipper_length_mm ^ 3 # volume scales as length³
)💡 Kleiber’s Law — basal metabolic rate scales to body mass to the power of ¾ across animals spanning 20 orders of magnitude. A single
^ 0.75inmutate()lets you compute a metabolic estimate for every row in your data frame instantly.
# sqrt() is ^ 0.5 — sometimes used to stabilise variance in count data
penguins |>
mutate(sqrt_mass = sqrt(body_mass_g))5 · Applying a regression equation
After you fit a linear model, mutate() is the cleanest way to apply the equation back to your data frame — either to generate predicted values for plotting or to calculate residuals.
Simple linear regression: y = mx + b
Say your regression of body mass on flipper length gave:
body_mass_g = 49.7 × flipper_length_mm − 5781
penguins |>
mutate(mass_predicted = 49.7 * flipper_length_mm - 5781)Plot observed vs. predicted:
penguins |>
mutate(mass_predicted = 49.7 * flipper_length_mm - 5781) |>
ggplot(aes(x = flipper_length_mm)) +
geom_point(aes(y = body_mass_g), color = "steelblue", alpha = 0.6) +
geom_line(aes(y = mass_predicted), color = "tomato", linewidth = 1) +
labs(
x = "Flipper length (mm)",
y = "Body mass (g)",
title = "Observed vs. predicted body mass"
) +
theme_classic()Log-log (allometric) regression
Log-log regression is the backbone of allometric studies: log₁₀(mass) = −3.41 + 1.52 × log₁₀(flipper length)
Apply it and back-transform predictions to the original scale:
penguins |>
mutate(
log10_flip = log10(flipper_length_mm),
log10_mass_hat = -3.41 + 1.52 * log10_flip,
mass_predicted = 10 ^ log10_mass_hat # back-transform to grams
)💡 Key idea: Back-transforming a log-scale prediction with
10^x(orexp(x)for natural log) gives you the geometric mean prediction on the original scale — which is what you want for a log-normal relationship. Report these back-transformed values, not the log-scale ones.
6 · Conditional columns
Two categories — if_else()
penguins |>
mutate(
size_class = if_else(body_mass_g >= 4000, "large", "small")
)if_else(test, value_if_TRUE, value_if_FALSE) — all three arguments must return the same data type.
Three or more categories — case_when()
penguins |>
mutate(
size_class = case_when(
body_mass_g >= 5000 ~ "large",
body_mass_g >= 3500 ~ "medium",
body_mass_g < 3500 ~ "small",
.default = "unknown" # catches NA and anything else
)
)💡
case_when()evaluates conditions top to bottom and stops at the first match — just like an if/else if chain. Put the most specific conditions first..defaultcatches anything that matched nothing, includingNA.
7 · Overwriting an existing column
Use the same name on the left to replace a column in place. The most common use is fixing a type that imported incorrectly:
# Year imported as numeric — treat it as a category for plotting
penguins |>
mutate(year = as.factor(year))
# A measurement column that imported as character
my_data |>
mutate(length_mm = as.numeric(length_mm))8 · Complete ecological pipeline
Putting it all together — bill shape index, log-transformation, size classification, factor ordering — all in one readable pipe:
penguins_analysis <- penguins |>
drop_na() |>
mutate(
bill_ratio = bill_length_mm / bill_depth_mm,
log10_mass = log10(body_mass_g),
log10_flip = log10(flipper_length_mm),
size_class = case_when(
body_mass_g >= 5000 ~ "large",
body_mass_g >= 3500 ~ "medium",
TRUE ~ "small"
),
species = factor(species, levels = c("Adelie", "Chinstrap", "Gentoo"))
) |>
select(species, sex, bill_ratio, log10_mass, log10_flip, size_class)
glimpse(penguins_analysis)Then the classic allometric log–log plot:
ggplot(penguins_analysis,
aes(x = log10_flip, y = log10_mass, color = species)) +
geom_point(size = 2, alpha = 0.7) +
labs(
title = "Allometric scaling: body mass vs. flipper length",
x = "log₁₀ Flipper length (mm)",
y = "log₁₀ Body mass (g)",
color = "Species"
) +
theme_classic()Quick reference
| Task | Code |
|---|---|
| Unit conversion | mutate(mass_kg = body_mass_g / 1000) |
| Ratio | mutate(bill_ratio = bill_length_mm / bill_depth_mm) |
| Base-10 log | mutate(log10_mass = log10(body_mass_g)) |
| Natural log | mutate(ln_mass = log(body_mass_g)) |
| Undo log10 | mutate(mass = 10 ^ log10_mass) |
| Undo natural log | mutate(mass = exp(ln_mass)) |
| Power / exponent | mutate(metabolic = mass_kg ^ 0.75) |
| Square root | mutate(sqrt_mass = sqrt(body_mass_g)) |
| Linear regression eq. | mutate(predicted = 49.7 * flipper_length_mm - 5781) |
| Log-log prediction | mutate(pred = 10 ^ (-3.41 + 1.52 * log10(flipper_length_mm))) |
| Binary category | mutate(size = if_else(body_mass_g >= 4000, "large", "small")) |
| Multiple categories | mutate(size = case_when(... ~ ..., .default = "unknown")) |
| Fix column type | mutate(year = as.factor(year)) |
| Centre on mean | mutate(mass_c = body_mass_g - mean(body_mass_g, na.rm = TRUE)) |
End of Common Code 05 — mutate(). Next: Common Code 06 — Advanced plotting and themes.