Control Flow

Week 3 · Thursday, Oct 8 · Lecture

Dr. Marcela Alfaro Córdoba

Today’s plan

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.

DS 2 discussion check

How this works

  1. Your two printouts (Lessons 3 and 4) on the desk, laptop closed
  2. I ask for volunteers. Raise your hand when you have a line you’re ready to talk through
  3. I point at a line on your own printout and ask you to explain it, or to predict what it prints
  4. “I’m not sure, but I think…” followed by reasoning is a good answer

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.

Warm-up, in pairs

⏱ 6 min

Swap printouts with your neighbor. Pick one exercise from their Lesson 4 PDF and ask:

  • “What does this line do?”
  • “What would happen if you ran this line before that one?”

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.

The kind of question you’ll get

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.

Volunteers

⏱ 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.

Today’s script

One script per lecture, starting from data

Open stat266a with File → Open Folder…, then create week03/control-flow.R:

# Week 3 · Control flow
# Run top to bottom after a restart. If it doesn't, fix it.

source("scripts/load-gapminder.R")
  • That’s the loading script from Tuesday. One line rebuilds gapminder from the raw file
  • Everything you type today goes below that line, in this file
  • Try things in the console if you want. Anything you want to keep goes into the script

Tip

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 for

week03/demo.R:

x <- 5
x
print(x)
source("week03/demo.R")
#> [1] 5

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().

source("week03/demo.R", echo = TRUE)    # show every line and its output

str(), print() and message() always print. That’s why Tuesday’s loading script ends with str(gapminder).

Making decisions

if / else

x <- 12
if (x > 10) {
  print("big")
} else if (x > 5) {
  print("medium")
} else {
  print("small")
}
#> [1] "big"
  • R checks the conditions in order and runs the first block whose condition is TRUE
  • else if and else are optional
  • else goes on the same line as the } before it

Your turn: predict first

⏱ 4 min

On paper. What does each chunk print?

temp <- 18
if (temp > 25) {
  print("hot")
} else if (temp > 15) {
  print("mild")
} else {
  print("cold")
}

temp <- 30
if (temp > 15) print("mild") else if (temp > 25) print("hot")

"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 FALSE

heights <- c(5, 12, 8)
if (heights > 10) print("someone is tall")
#> Error in if (heights > 10) print("someone is tall") :
#>   the condition has length > 1

Squash a vector of comparisons into one answer:

any(heights > 10)      #> [1] TRUE     is at least one TRUE?
all(heights > 10)      #> [1] FALSE    are they all TRUE?
if (any(heights > 10)) print("someone is tall")
#> [1] "someone is tall"
if (NA > 1) print("?")
#> Error in if (NA > 1) print("?") : missing value where TRUE/FALSE needed

if can’t decide on “I don’t know”. Missing values need handling before the if.

Combining conditions

Inside if ( ) Inside [ ], element by element
&& and & and
\|\| or \| or
! not ! not
x <- c(3, 8)
if (any(x > 5) && all(x > 0)) print("ok")
#> [1] "ok"
if (x > 0 && TRUE) print("?")
#> Error in x > 0 && TRUE : 'length = 2' in coercion to 'logical(1)'

&& refuses vectors on purpose. If you see that error, you forgot an any() or all().

Ask the data a question

In your script:

if (any(gapminder$year == 1987)) {
  print("1987 is in the data")
} else {
  print("no 1987")
}
#> [1] "1987 is in the data"

any(gapminder$year == 2010)    #> [1] FALSE
all(gapminder$pop > 0)         #> [1] TRUE

if can also return a value, which is handy for labels:

n <- 7
parity <- if (n %% 2 == 0) "even" else "odd"
parity     #> [1] "odd"

stop(): fail loudly

if (nrow(gapminder) != 1704) {
  stop("gapminder should have 1704 rows, found ", nrow(gapminder))
}
  • If the condition is FALSE, nothing happens and the script carries on
  • If it’s TRUE, R halts the whole script with your message:
#> Error in eval(ei, envir) : gapminder should have 1704 rows, found 1700

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.

Your turn: is that year in the data?

⏱ 6 min

In week03/control-flow.R, for y <- 1990:

  1. Write an if/else that sets report to "found" if gapminder has any rows from year y, and "missing" otherwise
  2. Change y to 1992 and run it again. Don’t change anything else
  3. Write the one-line version: report <- if (...) "found" else "missing"
y <- 1990
if (any(gapminder$year == y)) {
  report <- "found"
} else {
  report <- "missing"
}
report      #> [1] "missing"     the data go 1952, 1957, ... every 5 years

Use y, not a typed-in 1990. Code that uses the variable keeps working when the value changes.

Repeating steps

for loops

for (yr in c(1952, 1977, 2007)) {
  print(paste("The year is", yr))
}
#> [1] "The year is 1952"
#> [1] "The year is 1977"
#> [1] "The year is 2007"
  • yr takes each value of the vector in turn, and the body runs once per value
  • The name yr is yours to choose. Pick one that says what it holds
  • Without print(), a loop shows nothing. Same rule as source()

Saving results: grow vs. fill

Growing, one element at a time:

out <- numeric(0)
for (yr in c(1952, 1977, 2007)) {
  out <- c(out, yr)
}

Filling, into a vector made at full length first:

squares <- numeric(5)        # five zeros, ready to fill
for (i in 1:5) {
  squares[i] <- i ^ 2
}
squares      #> [1]  1  4  9 16 25

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.

Loop over positions with seq_along()

le <- c(43.8, 76.4, 72.3)
band <- character(length(le))
for (i in seq_along(le)) {
  if (le[i] < 60) {
    band[i] <- "low"
  } else if (le[i] < 75) {
    band[i] <- "mid"
  } else {
    band[i] <- "high"
  }
}
band     #> [1] "low"  "high" "mid"
seq_along(character(0))       #> integer(0)     loop runs 0 times
1:length(character(0))        #> [1] 1 0        loop runs TWICE, on nothing

seq_along(v) is the safe way to say “every position of v”, even when v turns out to be empty.

Your turn: grow vs. fill

⏱ 6 min

This builds grown with a nested loop:

grown <- numeric(0)
for (i in 1:3) {
  for (j in 1:4) {
    grown <- c(grown, i * 10 + j)
  }
}
  1. Predict length(grown) and its first five values. Then run it
  2. Build the same values in filled: make it with numeric() at full length first, then fill it by position inside the same nested loop
  3. Check with identical(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.

Loop over groups

One trick we cover fully on Tuesday: v[condition] keeps the elements of v where the condition is TRUE.

gapminder$lifeExp[gapminder$continent == "Europe"]   # Europe's values only
for (cont in sort(unique(gapminder$continent))) {
  m <- mean(gapminder$lifeExp[gapminder$continent == cont])
  print(paste(cont, round(m, 1)))
}
#> [1] "Africa 48.9"
#> [1] "Americas 64.7"
#> [1] "Asia 60.1"
#> [1] "Europe 71.9"
#> [1] "Oceania 74.3"

unique() for a text column, levels() for a factor. Either gives you one value per group to loop over.

Storing one result per group

Use a named vector, started empty:

verdict <- character(0)
for (cont in sort(unique(gapminder$continent))) {
  m <- mean(gapminder$lifeExp[gapminder$continent == cont])
  verdict[cont] <- if (m > 60) "above" else "below"
}
verdict
#>   Africa Americas     Asia   Europe  Oceania
#>  "below"  "above"  "above"  "above"  "above"

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.

Your turn: three bands per country

⏱ 10 min

In your script, with lo <- 60 and hi <- 75:

  1. Make gm07, only the 2007 rows: gm07 <- gapminder[gapminder$year == 2007, ]. How many rows should it have? Check
  2. Loop over the rows of gm07 and classify each country’s lifeExp as "low" (under lo), "mid" (from lo up to, not including, hi), or "high" (hi or more)
  3. Store the results in band, made at full length before the loop
  4. table(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, briefly

Repeat until a condition changes

How many years until $1,000 doubles at 7% a year?

balance <- 1000
years <- 0
while (balance < 2000) {
  balance <- balance * 1.07
  years <- years + 1
}
years       #> [1] 11

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.

A check script you can source()

Checks belong in a file, too

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")

How it fits together

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:

source("scripts/load-gapminder.R")
source("scripts/check-gapminder.R")

Each file does one job, and every later script starts with the same two lines. That’s the shape your project will have.

Your turn: break it on purpose

⏱ 8 min

  1. Create scripts/check-gapminder.R from two slides ago. Add both source() lines to the top of week03/control-flow.R
  2. Restart R (Session → Restart R), then source week03/control-flow.R. You should see gapminder checks passed
  3. Now break the data in the console: gapminder$pop[5] <- NA. Run only source("scripts/check-gapminder.R"). What happens?
  4. Add one check of your own: year should be between 1952 and 2007
  5. Restart and source again. Is your data fixed? Why?

Step 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.

Key points

  • if needs one TRUE/FALSE. Use any() / all() to get one from a vector
  • Conditions are checked in order; the first TRUE wins
  • && / || in if; & / | in [ ]
  • for (x in v) runs the body once per element; use seq_along(v) for positions
  • Make the output first (numeric(n), character(0)), then fill it
  • Loop over unique() or levels() to do something per group
  • stop() in a sourced check script turns a silent problem into a loud one
  • Inside loops and sourced scripts, nothing prints unless you print() it

Before you go

Tuesday DS (Oct 13, 9:50 am): Lesson 6: Control Flow. One PDF this week.

  • Alone, no AI, including for help
  • Bring the printout to Tuesday, Oct 20, 1:30 pm (the discussion check moved there)

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.