Activity: Wrangling Your Data

Importing more than one way, reshaping columns, and saving what you make

Hands-on worksheet: reading CSV and Excel files, filter/select/mutate/arrange, the full boolean-operator toolkit (%in%, is.na(), AND vs. OR), recoding with case_when(), log and unit transforms, catching duplicate rows, and write_csv().
Author

Bill Perry

Worksheet: Wrangling Your Data

πŸ“₯ Download the data (.csv) πŸ“₯ Download the data (.xlsx) πŸ“₯ Download the messy file

πŸ“₯ Script skeleton (.R)

ImportantColumn names in pine_data.csv / pine_data.xlsx

Both files hold the same six columns, with exactly these names. Type them exactly as written β€” R is case-sensitive, and Needle_Length or needle_length will not work:

Column What it is
source_file the raw team file the row came from
team field team β€” one team = one tree
replicate needle number within a tree side (1-8)
side sun exposure β€” "shady" or "sunny"
needle_length_mm needle length, millimetres
needle_width_mm needle width, millimetres

After you read the file in, run names(pine_df) and check the names on your screen against this table before you write any other code.


Part 1 Β· Import the same data two ways

πŸ“– Read more: R4DS Β§7 β€” Data import and Β§20 β€” Spreadsheets.

β–Ά Run this in your Script:

# ── Part 1.1 Β· Import the same data two ways ────────────────────────────
library(tidyverse)
library(readxl)

pine_csv_df   <- read_csv("data/pine_data.csv")
pine_excel_df <- read_excel("data/pine_data.xlsx")

πŸ’‘ You do not need glimpse(). Click pine_csv_df in the Variables pane in Positron β€” it lists every column name, its type, and how many rows, right there.

✏️ Your turn: List the six column names and, for each, its type (<chr>, <dbl>, etc.):

Column:              Type:

Column:              Type:
Column:              Type:

Column:              Type:

Column:              Type:

Column:              Type:

Part 2 Β· filter() β€” picking rows

πŸ“– Read more: R4DS Β§3.2 β€” filter().

β–Ά Run this:

# ── Part 2.1 Β· `filter()` ───────────────────────────────────────────────
pine_csv_df |> filter(side == "shady") |> head()
pine_csv_df |> filter(needle_length_mm > 17) |> head()

✏️ Your turn: Write a filter() that keeps only sunny (side == "sunny") needles longer than 15 mm.

# ── Part 2.2 Β· `filter()` ───────────────────────────────────────────────
# Write your code in the R script:

⚠️ Watch out! filter(side = "shady") (one =) is an error. You need == to test equality.


Part 2b Β· filter() β€” OR, %in%, and missing values

πŸ“– Read more: R4DS Β§12 β€” Logical vectors and Β§18 β€” Missing values.

β–Ά Run this:

# ── Part 2b.1 Β· `filter()` ──────────────────────────────────────────────
# OR: shady side, OR any exceptionally long needle
pine_csv_df |> filter(side == "shady" | needle_length_mm > 18)

# %in%: match one of several named teams
pine_csv_df |> filter(team %in% c("botany", "waterbugs"))

✏️ Your turn: Rewrite the %in% line above as a chain of | conditions instead β€” same result, longer code.

# ── Part 2b.2 Β· `filter()` ──────────────────────────────────────────────
# Write your code in the R script:

β–Ά Run this:

# ── Part 2b.3 Β· `filter()` ──────────────────────────────────────────────
survey_df <- tibble(
  site  = c("A", "B", "C", "D"),
  count = c(12, NA, 8, NA)
)

survey_df |> filter(is.na(count))
survey_df |> filter(!is.na(count))

✏️ Your turn: Write a filter() on pine_csv_df that keeps sunny (side == "sunny") needles that are also longer than 14mm β€” think about whether you need ,/& or |.

# ── Part 2b.4 Β· `filter()` ──────────────────────────────────────────────
# Write your code in the R script:

Part 3 Β· select() β€” picking columns

πŸ“– Read more: R4DS Β§3.3 β€” select().

β–Ά Run this:

# ── Part 3.1 Β· `select()` ───────────────────────────────────────────────
pine_csv_df |> select(side, needle_length_mm) |> head()
pine_csv_df |> select(-source_file) |> head()

✏️ Your turn: Write a select() that keeps every column except replicate.

# ── Part 3.2 Β· `select()` ───────────────────────────────────────────────
# Write your code in the R script:

Part 4 Β· mutate() β€” new and changed columns

πŸ“– Read more: R4DS Β§3.3 β€” mutate() and Β§13 β€” Numbers.

β–Ά Run this:

# ── Part 4.1 Β· `mutate()` ───────────────────────────────────────────────
pine_csv_df |>
  mutate(needle_length_cm = needle_length_mm / 10) |>
  head()

pine_csv_df |>
  mutate(log_length = log(needle_length_mm)) |>
  head()

✏️ Your turn: Add a column called size_class that is "long" when needle_length_mm > 15 and "short" otherwise. Use if_else().

# ── Part 4.2 Β· `mutate()` ───────────────────────────────────────────────
# Write your code in the R script:

✏️ Your turn: Does mutate() change how many rows your data frame has? How many columns did each example above add?

________________________

Tip

πŸš€ If you finish early: Add a second transformed column β€” a square-root transform (sqrt(needle_length_mm)) alongside your log transform. Which one looks like it changes the spread of the values more?


Part 4b Β· case_when() β€” recoding and binning

πŸ“– Read more: R4DS Β§12.5 β€” Conditional transformations.

β–Ά Run this:

# ── Part 4b.1 Β· `case_when()` ───────────────────────────────────────────
pine_csv_df |>
  mutate(
    size_class = case_when(
      needle_length_mm < 13 ~ "short",
      needle_length_mm < 16 ~ "medium",
      TRUE                  ~ "long"
    )
  ) |>
  count(size_class)

✏️ Your turn: Change the two cutoffs (13 and 16) to different values of your choosing. How do the counts in each bin shift?

# ── Part 4b.2 Β· `case_when()` ───────────────────────────────────────────
# Write your code in the R script:

✏️ Your turn: What happens if you delete the TRUE ~ "long" line and rerun? Try it, then explain why that’s dangerous.


____________________________________________________________________________________

β–Ά Run this:

# ── Part 4b.3 Β· `case_when()` ───────────────────────────────────────────
pine_csv_df |>
  mutate(
    team_code = case_when(
      team == "botany"     ~ "BOT",
      team == "grace_jace" ~ "GRJ",
      team == "one"        ~ "ONE",
      team == "waterbugs"  ~ "WBG",
      TRUE                 ~ "UNK"
    )
  ) |>
  distinct(team, team_code)

✏️ Your turn: Add a size_class column (as above) to pine_wrangled_df’s recipe β€” you’ll build the full pipeline with it in Part 6.

Tip

πŸš€ If you finish early: Write a case_when() that uses two conditions per row (e.g., side == "shady" & needle_length_mm > 17 ~ "long shady") to build a combined category.


Part 5 Β· arrange() β€” sorting rows

πŸ“– Read more: R4DS Β§3.2 β€” arrange().

β–Ά Run this:

# ── Part 5.1 Β· `arrange()` ──────────────────────────────────────────────
pine_csv_df |> arrange(needle_length_mm)
pine_csv_df |> arrange(desc(needle_length_mm))

✏️ Your turn: Sort the data by side, and within each side, by needle_length_mm from longest to shortest.

# ── Part 5.2 Β· `arrange()` ──────────────────────────────────────────────
# Write your code in the R script:

Part 6 Β· The full pipeline

β–Ά Run this:

# ── Part 6.1 Β· The full pipeline ────────────────────────────────────────
pine_wrangled_df <- pine_csv_df |>
  filter(needle_length_mm > 0) |>
  select(team, side, needle_length_mm, needle_width_mm) |>
  mutate(
    needle_length_cm  = needle_length_mm / 10,
    log_length = log(needle_length_mm),
    size_class = case_when(
      needle_length_mm < 13 ~ "short",
      needle_length_mm < 16 ~ "medium",
      TRUE                  ~ "long"
    )
  ) |>
  arrange(side, desc(needle_length_mm))

head(pine_wrangled_df)

✏️ Your turn: In your own words, read this pipeline out loud, step by step, the way we did in lecture.

Take pine_csv_df, then ______________________, then ______________________,

then ______________________, then ______________________.

Part 7 Β· Finding duplicate rows

πŸ“– Read more: R4DS Β§14 β€” Strings for the text cleanup, and Gotelli & Ellison Ch. 8 (Managing and Curating Data) on Canvas for why this matters.

β–Ά Run this:

# ── Part 7.1 Β· Finding duplicate rows ───────────────────────────────────
needle_check_df <- read_csv("data/pine_needles_error.csv")

nrow(needle_check_df)
n_distinct(needle_check_df)

✏️ Your turn: Do nrow() and n_distinct() match?
What does that tell you?

_______________________________________________________________________

β–Ά Run this:

# ── Part 7.2 Β· Finding duplicate rows ───────────────────────────────────
needle_check_df |> filter(duplicated(needle_check_df))

needle_clean_df <- needle_check_df |> distinct()

nrow(needle_clean_df)

✏️ Your turn: How many rows did distinct() remove? Why does this matter for the sample size you’d report?

_________________________________________________________________________

⚠️ Watch out! distinct() only catches exact duplicate rows β€” a mistiped name won’t be caught. Always look at your data too.


Part 8 Β· Saving your work with write_csv()

Everything runs one way: data/ (raw, read-only) β†’ scripts/ (your code) β†’ output/ (results).

πŸ“– Read more: R4DS Β§7.5 β€” Writing to a file.

Before you save, trim to the columns worth keeping:

β–Ά Run this:

# ── Part 8.1 Β· Saving your work with `write_csv()` ──────────────────────
pine_out_df <- pine_wrangled_df |>
  select(team, side, needle_length_mm, needle_width_mm)

write_csv(pine_out_df, "output/pine_data_wrangled.csv")

Check your output/ folder β€” the new CSV should be there, and data/pine_data.csv should be exactly as it was.

⚠️ Watch out! write_csv() overwrites without asking. That is fine in output/, because your script can rebuild anything in there. It would be a disaster in data/.

✏️ Your turn: Save your cleaned needle_clean_df from Part 7 to output/pine_needles_cleaned.csv.

# ── Part 8.2 Β· Saving your work with `write_csv()` ──────────────────────
# Write your code in the R script:

⚠️ Watch out! Never give a wrangled file the same name as the raw file. If your script’s output can overwrite your raw data, one accidental re-run destroys it permanently.


Part 9 Β· Review and checkpoint

At this point you can:

✏️ Your turn β€” before you move on: Run your whole script top to bottom.

Ran cleanly? Y / N β€” if not, the error was:

____________________________________________________________

Note

πŸ“€ What to turn in before next class

Upload both of these to the course management system:

  1. Your code β€” the scripts/ folder (or just 02_wrangling_data.R)
  2. This worksheet, with your written answers

Readings

Before next class β€” chapter PDFs are posted on Canvas:

  • Gotelli & Ellison, A Primer of Ecological Statistics β€” Ch. 8, Managing and Curating Data (raw vs. processed data, and the audit trail from field sheet to analysis)
  • Whitlock & Schluter, The Analysis of Biological Data β€” Ch. 1, Statistics and Samples, Β§1.4 on types of variables β€” the categorical/numerical distinction behind case_when() and every group_by() to come

Neither statistics text teaches dplyr. Read them for why a clean, documented dataset matters; read R4DS for how.

R for Data Science (2nd ed.) β€” free online at r4ds.hadley.nz:

Chapter Covers Worksheet part
Β§3 β€” Data transformation filter(), select(), mutate(), arrange(), the pipe Parts 2-6
Β§5 β€” Data tidying one row per observation β€” the shape we are aiming for Part 6
Β§7 β€” Data import read_csv(), write_csv() Parts 1, 8
Β§12 β€” Logical vectors &, \|, %in%, is.na(), case_when() Parts 2b, 4b
Β§13 β€” Numbers unit conversions, logs, as.numeric() Part 4
Β§14 β€” Strings str_to_lower(), str_squish(), fixing typo’d categories Part 7
Β§18 β€” Missing values NA, na_if(), sentinel codes like -999 Parts 2b, 7
Β§20 β€” Spreadsheets reading .xlsx with readxl Part 1