Week 3 · Thursday, Oct 8 · Lecture
| Time | What |
|---|---|
| 1:30 | DS 2 discussion check, on paper |
| 1:50 | Today’s script, and what source() doesn’t print |
| 2:00 | Making decisions: if / else |
| 2:25 | Repeating steps: for loops |
| 2:50 | while, briefly |
| 2:55 | A check script you can source() |
Learning goals: make R choose between actions, repeat an action over values and groups, and turn both into a script that checks your data for you.
Laptops closed for the first 20 minutes. Printouts and pencils out. Still no AI.
Note
This is the 1 point of the DS that isn’t the printout. It checks that you can read the code you submitted, not that you remember it word for word.
⏱ 6 min
Swap printouts with your neighbor. Pick one exercise from their Lesson 4 PDF and ask:
Then switch roles.
If you can’t explain a line of your own, circle it. That’s what to review tonight, not a reason to panic.
1. Point to the line where you convert height_cm. What would mean(plants2$height_cm) have returned if you’d run it one line earlier?
NA, with a warning: argument is not numeric or logical. The column was still text.
2. In your “vector or data frame?” answers, which ones were data frames? What’s the rule?
The single-bracket ones with one index, like plants[1], and any [row, ] with a comma and an empty column. One bracket keeps the box.
3. No laptop. What does this print? paste(c("x", "y"), 1:2, sep = "-", collapse = "+")
"x-1+y-2". sep inside each element, collapse across them.
⏱ 12 min
Who wants to go first?
Raise your hand, hold up your printout, and I’ll point at a line. Or pick one yourself: the line you’re proudest of, or the one that took you longest to get right.
Lesson 6, Tuesday’s DS, is about exactly what we do in the next hour.
Open stat266a with File → Open Folder…, then create week03/control-flow.R:
gapminder from the raw fileTip
Missing scripts/load-gapminder.R? It’s on the second-to-last slides of Tuesday’s lecture. Make it now, before we start: the rest of the quarter builds on it.
source() doesn’t print what you don’t ask forweek03/demo.R:
One 5, not two. A bare x prints only when you type it in the console. Inside a sourced script, and inside loops, use print().
if / elseTRUEelse if and else are optionalelse goes on the same line as the } before it⏱ 4 min
On paper. What does each chunk print?
"mild", then "mild" again. 30 is hot, but the first TRUE condition wins. Order your conditions from most to least specific.
if wants exactly one TRUE or FALSESquash a vector of comparisons into one answer:
Inside if ( ) |
Inside [ ], element by element |
|---|---|
&& and |
& and |
\|\| or |
\| or |
! not |
! not |
&& refuses vectors on purpose. If you see that error, you forgot an any() or all().
In your script:
stop(): fail loudlyFALSE, nothing happens and the script carries onTRUE, R halts the whole script with your message:Week 2’s two halves: eval(ei, envir) is source() telling you where; your message is the what.
Important
A script that stops at line 4 with a clear message is far better than one that runs to the end and draws a plot from the wrong data. We’ll build a whole file of these at the end of today.
⏱ 6 min
In week03/control-flow.R, for y <- 1990:
if/else that sets report to "found" if gapminder has any rows from year y, and "missing" otherwisey to 1992 and run it again. Don’t change anything elsereport <- if (...) "found" else "missing"for loopsyr takes each value of the vector in turn, and the body runs once per valueyr is yours to choose. Pick one that says what it holdsprint(), a loop shows nothing. Same rule as source()Growing, one element at a time:
Filling, into a vector made at full length first:
Growing makes R copy the whole vector on every step. Fine for 10 values, painfully slow for 100,000. Make the output first, then fill it.
seq_along()⏱ 6 min
This builds grown with a nested loop:
length(grown) and its first five values. Then run itfilled: make it with numeric() at full length first, then fill it by position inside the same nested loopidentical(grown, filled)filled <- numeric(3 * 4), then filled[(i - 1) * 4 + j] <- i * 10 + j. The tricky part is the position, so write out a few (i, j) pairs by hand first.
One trick we cover fully on Tuesday: v[condition] keeps the elements of v where the condition is TRUE.
unique() for a text column, levels() for a factor. Either gives you one value per group to loop over.
Use a named vector, started empty:
verdict["Asia"] reads one result by name. Here you can’t pre-allocate easily without knowing the groups first, and with 5 groups growing is perfectly fine.
⏱ 10 min
In your script, with lo <- 60 and hi <- 75:
gm07, only the 2007 rows: gm07 <- gapminder[gapminder$year == 2007, ]. How many rows should it have? Checkgm07 and classify each country’s lifeExp as "low" (under lo), "mid" (from lo up to, not including, hi), or "high" (hi or more)band, made at full length before the looptable(band)Then add names(band) <- gm07$country and look up Costa Rica.
142 rows, one per country. table(band): high 44, low 43, mid 55. Costa Rica (78.8) is "high".
while, brieflyHow many years until $1,000 doubles at 7% a year?
Warning
Something inside the loop must eventually make the condition FALSE, or it never stops. If R hangs, press Esc in Positron’s console.
Many loops in R can be replaced by one vectorized line. That’s Week 5. Today, loops are the right tool for learning to think step by step.
source()scripts/check-gapminder.R:
# Checks that gapminder looks the way we expect.
# Run after load-gapminder.R. Stops with a message if anything is off.
if (nrow(gapminder) != 1704) {
stop("expected 1704 rows, found ", nrow(gapminder))
}
for (col in names(gapminder)) {
n_missing <- sum(is.na(gapminder[[col]]))
if (n_missing > 0) {
stop(col, " has ", n_missing, " missing values")
}
}
if (any(gapminder$pop <= 0)) stop("non-positive population")
message("gapminder checks passed")stat266a/
├── data/
│ └── gapminder_data.csv <- raw, never edited
├── scripts/
│ ├── load-gapminder.R <- reads it (Tuesday)
│ └── check-gapminder.R <- checks it (today)
└── week03/
└── control-flow.R <- uses it
Top of week03/control-flow.R:
Each file does one job, and every later script starts with the same two lines. That’s the shape your project will have.
⏱ 8 min
scripts/check-gapminder.R from two slides ago. Add both source() lines to the top of week03/control-flow.Rweek03/control-flow.R. You should see gapminder checks passedgapminder$pop[5] <- NA. Run only source("scripts/check-gapminder.R"). What happens?year should be between 1952 and 2007Step 5: yes, because the load script always starts from the raw file. What you did in the console was never written down, so it’s gone after a restart. Usually that’s a problem. Here it’s the point.
if needs one TRUE/FALSE. Use any() / all() to get one from a vectorTRUE wins&& / || in if; & / | in [ ]for (x in v) runs the body once per element; use seq_along(v) for positionsnumeric(n), character(0)), then fill itunique() or levels() to do something per groupstop() in a sourced check script turns a silent problem into a loud oneprint() itTuesday DS (Oct 13, 9:50 am): Lesson 6: Control Flow. One PDF this week.
Before Tuesday’s lecture: the restart test on week03/control-flow.R. Session → Restart R, source it, and it should run to the end with no errors and print gapminder checks passed.
Project proposal: Tuesday, Oct 20, 3 minutes. Question, data, plan, risks, team. By Tuesday you should have the data loaded by a script, so you can show its first rows. See the guidelines.
Next: Tuesday, Oct 13 · Cleaning data in practice. Subsetting, and a cleaning script you can re-run.

STAT 266A · Fall 2026 · UC Santa Cruz