Week 6 · Tuesday, Nov 3 · Lecture
| Time | What |
|---|---|
| 1:30 | DS 6 check: swap laptops |
| 1:45 | A second wave of the survey: stack it |
| 2:05 | Questions about both waves, with pipes |
| 2:25 | “It runs. Is it right?”: a merge that multiplies rows |
| 2:45 | Write it out, and your project’s combine |
Learning goals: stack two versions of a dataset that don’t quite match, answer questions about them with dplyr pipes, and catch a combine that silently adds or drops rows.
You need survey_wave2.csv, buildings.csv and Thursday’s sites.csv in data/.
⏱ 15 min
Swap laptops with your neighbor and open their week06/ds06_notes.R.
On the Ed thread “DS 6 check”, one post per pair: your topic, and one sentence on why it was hard.
The 1-point discussion check. I read every post, and they decide what we review.
Create week06/combining-practice.R:
The story: a month after the first survey, a second team surveyed 20 more people. Same questions, typed in by a different person. You want one data frame with everyone.
str(wave2)
#> 'data.frame': 20 obs. of 5 variables:
#> $ id : int 101 102 103 104 105 106 107 108 109 110 ...
#> $ site : chr "east" "south" "east" "south" ...
#> $ age : int 28 30 18 33 41 22 21 27 31 35 ...
#> $ visits : int 4 5 0 3 5 1 5 2 5 3 ...
#> $ commute: int 8 14 35 6 21 35 8 9 36 36 ...
names(survey) #> [1] "id" "site" "age" "commute_min" "visits"
names(wave2) #> [1] "id" "site" "age" "visits" "commute"⏱ 2 min
Two differences between the columns. Which one matters for stacking?
The order doesn’t matter: rbind() and bind_rows() match columns by name. The name does: commute is not commute_min.
65 rows is right. 6 columns is not.
bind_rows() kept both names: wave 1 has NA in commute, wave 2 has NA in commute_min. No message. colSums(is.na()) is how you’d notice: 21 NAs where Thursday you had 1.
wave, and the same NAs as Thursday.id = "wave" numbers the inputs in order: "1", "2". It’s text, not a numberTip
One more check before trusting it: is every id still unique? any(duplicated(both$id)) is FALSE. Wave 2 starts at 101 for a reason.
⏱ 6 min
rename() and the bind_rows() to your scriptidstable() with two columnsif (nrow(both) != 65) stop("expected 65 rows, found ", nrow(both))
if (ncol(both) != 6) stop("expected 6 columns: did a name not match?")
if (any(duplicated(both$id))) stop("duplicated ids across waves")
table(both$site, both$wave, useNA = "ifany")
#> 1 2
#> east 11 7
#> north 15 7
#> south 18 6
#> <NA> 1 0Note
12 people commute more than 30 minutes. filter() silently drops rows where the condition is NA (id 35). Base [ ] would have given you a row of NAs instead. Different tools, different defaults: count.
⏱ 12 min
One pipe each. Before you run it, write down roughly what you expect.
id, site and visitscommute_h (hours), and keep only the people with a commute of half an hour or more. How many?visits in wave 1 and in wave 2 (use filter() and pull(), then mean()). Did the second team find the same thing?both |> filter(site == "north", visits >= 5) |> nrow() #> [1] 5
both |> filter(wave == "2") |> arrange(desc(visits)) |>
select(id, site, visits) |> slice_head(n = 3) # ids 116, 117, 102
both |> mutate(commute_h = commute_min / 60) |>
filter(commute_h >= 0.5) |> nrow() #> [1] 14
both |> filter(wave == "1") |> pull(visits) |> mean() #> [1] 3.444444
both |> filter(wave == "2") |> pull(visits) |> mean() #> [1] 3.15wave == "2", in quotes: it’s text. wave == 2 also works here, because R converts the 2 to text to compare. Don’t rely on that.
Thursday’s lookup table, now on both waves:
65 in, 65 out, and the one missing building is the person with no site. Every number accounted for. That’s the standard for the rest of the course.
A student asked an AI to “add the building and its distance to the library to my survey data and tell me how often people visit.” It used buildings.csv:
Result. Respondents visit the library 3.3 times a week on average (n = 86). There is essentially no relationship between distance to the library and visits (r = −0.02).
⏱ 10 min
In pairs. The code runs with no errors or warnings. Find what’s wrong, how you found it, and what you’d do instead. No other AI.
| Problem | How you’d find it |
|---|---|
north has two rows in the lookup, so all 22 north people appear twice |
nrow() before and after; sum(duplicated(buildings$site)) |
| “n = 86” counts rows, not people. There are 65 | n_distinct(survey_b$id) |
The person with no site disappeared: no all.x = TRUE |
65 in, but only 64 distinct ids out |
| The mean and correlation weight north double | Redo on the right data: 3.35 and −0.08 |
A lookup table needs one row per key. Check it before you merge.
On Ed, thread “AI audit: merge”: one post per pair, the problem you found first, and how.
At the end of week06/combining-practice.R:
Tip
Restart R, source the script. If it runs, someone else can rebuild results/survey_both_waves.csv from data/ with one command. Your project should work the same way.
⏱ 10 min
For your mid-quarter report (due today):
filter(), write a comment with the row count before and after, and the reason it changedresults/, and does your report read it from there?AI on the report: allowed with disclosure. If an AI wrote any combine or filter, the row-count checks are your part, and they go in the report.
rbind() and bind_rows() match columns by name, in any order. A different name is a different columnbind_rows() fills mismatches with NA silently: check ncol() and colSums(is.na()) afterrename(new = old) before stacking. .id records where each row came fromfilter() drops NA rows silently; base [ ] doesn’t. Count either waynrow(), n_distinct(id), colSums(is.na())Thursday, Nov 5: dplyr II: groups and joins. group_by() replaces today’s filter() per wave, and left_join() replaces merge(all.x = TRUE).

STAT 266A · Fall 2026 · UC Santa Cruz