Week 6 · Thursday, Oct 29 · Lecture
| Time | What |
|---|---|
| 1:30 | DS 5 check: swap laptops |
| 1:45 | Writing data: from code, into results/ |
| 2:10 | Splitting: one data frame into many |
| 2:30 | Combining: stacking rows, matching by key |
| 2:55 | Tuesday, and the mid-quarter report |
Learning goals: write a data frame to CSV and check it by reading it back, split a data frame by group, stack pieces back together, and attach a lookup table by a key column, counting rows at every step.
Still no AI. Before we start, download sites.csv into your data/ folder.
⏱ 15 min
Swap laptops with your neighbor and open their week05/ds05_notes.R.
On the Ed thread “DS 5 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/writing-combining.R:
results/stat266a/
├── data/ <- raw files, never edited
├── scripts/ <- code you source()
├── figures/ <- plots, made by ggsave()
├── results/ <- new: cleaned data and tables, made by code
└── week06/
Anything in figures/ or results/ can be deleted and remade by running a script.
Important
Never write over a file in data/. If the raw file changes, you can’t check your cleaning against it anymore.
write.csv()An unnamed first column appeared. Read the file back and see what it becomes:
Those are the row names, written as data. Read the file back and you get a column X you never made. Every time you round-trip the file, you add another one.
row.names = FALSETip
Every write.csv() you write in this course: row.names = FALSE. (readr::write_csv() does this by default, if you prefer it.)
back <- read.csv("results/survey_clean.csv")
dim(back) #> [1] 45 5
dim(survey) #> [1] 45 5
str(back)
#> 'data.frame': 45 obs. of 5 variables:
#> $ id : int 1 2 3 4 5 6 7 8 9 10 ...
#> $ site : chr "east" "north" "east" "north" ...
#> $ age : int 26 38 39 18 21 NA 23 28 39 37 ...
#> $ commute_min: int 23 11 17 7 18 21 16 24 14 18 ...
#> $ visits : int 1 6 0 0 4 6 0 5 5 5 ...NAs are back as NAcommute_min was num and came back int: a CSV only stores text, so R guesses the type again when it readsA CSV keeps values, not types. Factors, dates, and the order of factor levels are not saved. That’s why every analysis script reads the CSV and then checks it.
Week 4: ggsave() writes one plot per file. For a multi-page PDF, open a device, print each plot, close it:
Warning
Until dev.off() runs, the file isn’t finished, and your plots won’t show in Positron either. If plots “disappear”, run dev.off() until it says null device.
⏱ 8 min
In your script:
gm07: the gapminder rows for 2007, with only country, continent, lifeExp and gdpPercapresults/gm07.csv, with no row namesback. Check three things: dim(), names(), and that back$lifeExp matches gm07$lifeExpresults/gm07.csv in Positron. What’s in the first line?gm07 <- gapminder[gapminder$year == 2007, c("country", "continent", "lifeExp", "gdpPercap")]
write.csv(gm07, "results/gm07.csv", row.names = FALSE)
back <- read.csv("results/gm07.csv")
dim(back) #> [1] 142 4
identical(names(back), names(gm07)) #> [1] TRUE
all.equal(back$lifeExp, gm07$lifeExp) #> [1] TRUEall.equal(), not ==: it allows for the tiny rounding of numbers written as text.
split(data, group) gives a list of data frames, one per value of groupWarning
split() drops rows whose group is NA: here, the person with no site (id 27). No warning. You only know because you counted.
The same thing to every piece, with Week 5’s sapply():
Or one file per group, with Week 3’s loop:
by_site[[s]]: double brackets give you the data frame itself; single brackets would give you a list of one.
⏱ 6 min
gapminder by continent into by_contlifeExp in each continentsplit(gapminder, gapminder$year) give per piece? CheckStacking: same columns, more rows
id site ... id site ...
1 east + 101 east
3 east 102 south
rbind(), bind_rows()
Matching: same rows, more columns
site ... + site building
east east Oak Hall
north north Pine Hall
merge(), next week: joins
Important
Before and after every combine: count the rows. Know what number you expect before you look.
bind_rows() from dplyr stacks a whole list at once, and can record where each row came from:
Still 44: what split() dropped doesn’t come back. Splitting and stacking are only a round trip when the group column has no NAs.
Take away commute_min from one piece:
| Columns don’t match | |
|---|---|
rbind() |
error: you have to fix it |
bind_rows() |
fills the gaps with NA, no message |
bind_rows() is more convenient and less careful. After it: colSums(is.na(z)). 15 new NAs you didn’t have before means a column didn’t line up.
data/sites.csv: one row per site.
Question: do people living farther from the library visit it less? We need km_to_library on every row of survey.
west is a site where nobody in the survey lives. Keep an eye on it.
cbind() is not the toolRow 3 lives in east and got Cedar Hall. No error.
cbind() glues by position: row 1 with row 1. That’s right only when both tables are in the same order, row for row. A lookup table almost never is.
merge(): match by keysurvey, find the row of sites with the same site, and add its columnssite = NA had no match, and merge() dropped themall.x = TRUE| Keeps | Rows here | |
|---|---|---|
merge(x, y) |
only rows with a match in both | 44 |
merge(x, y, all.x = TRUE) |
every row of x; NA where y has no match |
45 |
merge(x, y, all = TRUE) |
every row of both | 46: adds west, with NA survey values |
For “add information to my data”, you almost always want all.x = TRUE: the number of rows should stay what it was. Next week, dplyr calls this a left_join().
⏱ 10 min
Before each line: write down the number of rows you expect. Then run it.
merge() survey with sites, keeping every survey row. Call it survey_sitesvisits for each value of km_to_library (hint: tapply()). Closer, more visits?merge(survey, sites, all = TRUE). Which row is new, and what’s in it?survey_sites to results/survey_sites.csv, read it back, check dim()No clear pattern: east is farthest and visits least, but north is closest and doesn’t visit most. Three sites isn’t much to go on.
results/ and figures/, made by code. data/ is never written towrite.csv(x, file, row.names = FALSE), then read it back and check dim(), names(), str()pdf(), print() each plot, dev.off(): several plots in one filesplit(df, group) gives a list of data frames, and drops rows where group is NArbind() (errors on mismatched columns) or bind_rows() (fills with NA, silently)merge(), never cbind(). all.x = TRUE keeps every row of your data9:50-10:55 am · DS 6
Lesson 12: Writing Data and Lesson 13: dplyr I, alone, no AI. Two PDFs.
1:30-3:05 pm · Combining data practice I
Starts with the DS 6 check: swap laptops, same as today. Then a second wave of the survey to stack, match and check.
Important
Mid-quarter report due Tuesday. AI is allowed with disclosure: include the “AI use” section (template) and, in a pair, a contribution statement.
Before Tuesday: read the Lesson 12 and Lesson 13 pages. Lesson 13 introduces the pipe |>; the DS is your first practice with it.
Next: Tuesday, Nov 3 · DS 6 in the morning, combining data practice in the afternoon.

STAT 266A · Fall 2026 · UC Santa Cruz