Seeking Help + Data Structures

Week 2 · Thursday, Oct 1 · Lecture

Dr. Marcela Alfaro Córdoba

Today’s plan

Time What
1:30 Warm-up: predict three lines
1:40 Reading a help page
2:00 Reading an error message, and the help routine
2:20 Data types and vectors
2:40 Coercion: why one bad value ruins a column
2:55 Names, factors, lists, matrices

Learning goals: get yourself unstuck using R’s own documentation, and explain why every element of a vector has the same type.

Type along. Still no AI, including as a help source. Today is about becoming your own help desk.

Warm-up: predict first

⏱ 5 min

On paper, without running anything. What does R print?

x <- 5
y <- x * 2
x <- 100
y

z <- c(1, 2, 3)
z * 2

0.5 + 0.25 == 0.75

Then run them. y is 10: assignment copies the value, not a live link.

z * 2 gives 2 4 6: R works on whole vectors at once. Hold on to that.

Seeking help

Build paths, not answers

When you’re stuck, the fastest fix is someone handing you working code. It works today, and it does nothing for you next week.

Every time you

  1. read the help page,
  2. read the error message,
  3. try something small,
  4. compare what happened with what you expected,

you build a path in your head you can use again, on any problem, without anyone’s help.

That is what fluency in a language is: knowing where to look and how to read what you find. Not memorizing functions.

Why not a chatbot (yet)

  • You can’t check the answer yet. These tools produce code that looks right but uses the wrong argument, invents a function, or answers a slightly different question. Spotting that takes exactly the reading skills we build today.
  • They skip the part that teaches you. Getting there through the help page is the exercise. A pasted answer finishes the task and skips the learning.
  • You’ll use them better later. People who can read docs and errors ask sharper questions and catch the mistakes. We come back to this in Week 5.

Important

Today’s rule: if you’re stuck, the help page comes first, a person comes second. No chatbot.

Anatomy of a help page

Type ?median or help("median") in the Console. The page opens in the Help pane.

Every help page has the same sections:

Section What it tells you
Description What the function does
Usage How to call it, with each argument’s default
Arguments What each argument means and what type it expects
Value What comes back
See Also Related functions. Often this is the one you needed
Examples Runnable code

Read Arguments first, then jump straight to Examples.

Reading Usage carefully

median(x, na.rm = FALSE, ...)
  • x has no default: you must supply it
  • na.rm = FALSE has a default: optional, and this is what you get if you say nothing
  • ... means “other arguments, passed on to other methods”
scores <- c(4, 9, NA, 2)
median(scores)
#> [1] NA
median(scores, na.rm = TRUE)
#> [1] 4

The default told you it would do that. Missing data is never silently dropped in R.

Two shortcuts worth knowing

example(median)     # run every example on the page
args(round)         # just the argument list
#> function (x, digits = 0, ...)

Operators have help pages too, in quotes or backticks:

?"%%"      # remainder
?"["       # subsetting
?"<-"      # assignment

Packages ship longer tutorials called vignettes:

help(package = "stats")
browseVignettes("dplyr")

Your turn: sep or collapse?

⏱ 6 min

Open ?paste. Using only the help page, work out the difference between sep and collapse. Then predict each of these before running it:

paste(c("a", "b"), "c")
paste(c("a", "b"), "c", sep = ",")
paste(c("a", "b"), "c", collapse = "|")
paste(c("a", "b"), "c", sep = ",", collapse = "|")

Check: "a c" "b c" · "a,c" "b,c" · "a c|b c" · "a,c|b,c"

sep goes between the pieces of each element. collapse glues the whole result into one string. Length 2 → length 2, or length 2 → length 1.

When you don’t know the function’s name

??"standard deviation"    # searches titles + descriptions of every installed help page
apropos("mean")           # every function whose NAME contains "mean"
#> [1] "colMeans" "kmeans" "mean" "mean.Date" "rowMeans" ...

Also:

  • The See Also section of a function you already know
  • ls("package:stats") to see everything a package offers
  • A web search: the word R plus what you want to do, or the exact error text in quotes

Tip

Trust, in order: the official docs, the Posit Community forum, Stack Overflow answers that explain rather than just paste code.

Your turn: find the right reader

⏱ 8 min

Two files, both with columns separated by tabs. One writes decimals with a period (12.5), the other with a comma (12,5, as in most of Latin America and Europe).

Find the function and the arguments that read each one correctly. Start from:

??"read table"
?read.table

Then say how you’d check that the numbers really came in as numbers.

read.delim(file) for the first. read.delim(file, dec = ",") — or read.table(file, sep = "\t", dec = ",", header = TRUE) — for the second. Check with str(): you want num, not chr.

Errors are information

Two halves of an error message

log("ten")
#> Error in log("ten") : non-numeric argument to mathematical function

Where

Error in log("ten") :

The call that failed. Look at that line.

What

non-numeric argument to mathematical function

The reason. Usually a type problem.

An error is not a verdict on you. It is R telling you, as precisely as it can, what it could not do.

The four you’ll see most

Message Usually means
object 'x' not found Typo, wrong capitalization, or you never ran the line that creates x
could not find function "x" Typo, or the package isn’t loaded with library()
unexpected symbol / unexpected ')' A missing comma, bracket, or quote on or just before that line
non-numeric argument to ... You gave text (maybe numbers stored as text) to something that needs numbers

Note

Warning ≠ Error. An error stopped; nothing happened. A warning means R did it anyway and wants you to look. NAs introduced by coercion is a warning you must never ignore.

The routine, in order

  1. Read the whole message, out loud if needed
  2. Re-read the line it points to
  3. Open the help page of the function named in the message
  4. Try the smallest possible example that reproduces it
  5. Search the exact message text
  6. Ask a person, bringing your small example

Tip

Step 4 is the one people skip and the one that solves it. If log(my_column) fails, try log(my_column[1]), then log(5). The step where it starts working tells you where the problem is.

Your turn: three broken lines

⏱ 8 min

Run each line. For each one, write down (a) the where, (b) the what, and (c) the fix.

mean(c(2, 4, 6)
Mean(c(2, 4, 6))
mean(c(2, 4, "6"))

Then one more, which prints no error at all:

mean(c(2, 4, NA))

Missing ) · capital M, R is case-sensitive · "6" makes the whole vector text · and the last one is correct behavior: NA means “I don’t know”, so the mean is unknown too.

Asking a good question

A question people can actually answer contains:

  1. What you were trying to do
  2. The smallest code that shows the problem, that they can copy and run
  3. The full error, copied, not paraphrased
  4. What you already tried
  5. Your setup: sessionInfo()
dput(head(my_data, 5))   # paste your data INTO the question
#> structure(list(x = c(1, 2, 3, 4, 5), ...

Half the time, shrinking the example to something postable solves it before you post. That’s not wasted work.

Data structures

Start with a small table

plants <- data.frame(species   = c("fern", "basil", "cactus"),
                     height_cm = c(32.5, 21.0, 14.2),
                     flowering = c(FALSE, TRUE, TRUE))
plants
#>   species height_cm flowering
#> 1    fern      32.5     FALSE
#> 2   basil      21.0      TRUE
#> 3  cactus      14.2      TRUE
plants$height_cm            # one column, with $
#> [1] 32.5 21.0 14.2
plants$height_cm * 10       # in millimetres
#> [1] 325 210 142
paste("My", plants$species)
#> [1] "My fern"   "My basil"  "My cactus"

And one thing that fails

plants$height_cm + plants$species
#> Error in plants$height_cm + plants$species :
#>   non-numeric argument to binary operator

Two ideas explain this, and most of R’s surprising behavior:

  1. Each column of a data frame is a vector.
  2. Every element of a vector has the same type.

The rest of this lecture is those two sentences, unpacked.

The five atomic types

typeof(3.14)        #> "double"      numbers, the default
typeof(2L)          #> "integer"     whole numbers; L makes it an integer
typeof(TRUE)        #> "logical"     TRUE / FALSE
typeof("fern")      #> "character"   text
typeof(1 + 2i)      #> "complex"     rarely needed

Three questions, three functions:

Question Function On plants$flowering
What type are the elements? typeof() "logical"
What kind of object is this? class() "logical"
Show me structure + a preview str() logi [1:3] FALSE TRUE TRUE

str() is the one you’ll use most

str(plants)
#> 'data.frame':    3 obs. of  3 variables:
#>  $ species  : chr  "fern" "basil" "cactus"
#>  $ height_cm: num  32.5 21 14.2
#>  $ flowering: logi  FALSE TRUE TRUE
  • 3 obs. = 3 rows, 3 variables = 3 columns
  • one line per column: name, type, first values
  • chr character · num double · int integer · logi logical

Tip

str() on anything new, always, before you do anything else with it. It answers “what am I actually holding?”

Making vectors

c(4, 9, 10)                        # combine
1:10                               # a sequence of integers
seq(10, 100, by = 10)              # step
seq(0, 1, length.out = 5)          #> 0.00 0.25 0.50 0.75 1.00
rep(c("a", "b"), times = 3)
vector("numeric", length = 3)      #> 0 0 0    an empty one to fill
vector("character", length = 2)    #> "" ""
scores <- c(4, 9, 10)
length(scores)      #> [1] 3
scores[2]           #> [1] 9        one element, by position
scores[2] <- 99     # change it in place
scores              #> [1]  4 99 10

Coercion: the type hierarchy

A vector holds one type. Mix types, and R quietly converts everything to the most flexible one present:

logical → integer → double → complex → character

c(2, "three")       #> "2"     "three"     character wins
c(TRUE, 5)          #> [1] 1 5             TRUE becomes 1
c(TRUE, "yes")      #> "TRUE"  "yes"
c(2L, 3.5)          #> [1] 2.0 3.5

Important

No error, no warning. R did what you asked. This is why str() is a habit and not a chore.

Your turn: predict the type

⏱ 5 min

Open ?c and read what it says about combining different types. Then predict what typeof() reports for each, writing "logical", "integer", "double" or "character":

c(1, 2, 3)
c("d", "e", "f")
c(1, 2, "f")
c(TRUE, FALSE, 1)
c(1L, 2L)
c(1L, 2.5)

double · character · character · double · integer · double

Why this matters: a real column

One person typed a range instead of a number:

plants2 <- rbind(plants,
                 data.frame(species = "mint", height_cm = "14 or 16",
                            flowering = TRUE))
str(plants2)
#>  $ height_cm: chr  "32.5" "21" "14.2" "14 or 16"
mean(plants2$height_cm)
#> Error in mean.default(...) : argument is not numeric or logical

One bad cell turned 4 numbers into 4 strings. In a 50,000-row CSV you will not see it. str() will.

Converting on purpose

as.numeric(c("0", "2", "4"))     #> [1] 0 2 4
as.character(1:3)                #> "1" "2" "3"
as.logical(c(0, 1, 1))           #> FALSE TRUE TRUE
as.integer(3.9)                  #> [1] 3          truncates, never rounds
as.numeric(c("2.5", "14 or 16"))
#> [1] 2.5  NA
#> Warning message: NAs introduced by coercion

Warning

That warning is R telling you exactly which assumption failed. Read it, find the value it choked on, and fix the value. Don’t wrap it in suppressWarnings().

Your turn: clean a messy column

⏱ 8 min

Using plants2 from two slides ago:

  1. Print it, then run str(plants2). Which column has the wrong type? What should it be?
  2. Find the bad row. Which row number is it? (plants2$height_cm)
  3. Replace that entry with the mean of its two numbers: plants2$height_cm[ROW] <- "15"
  4. Convert the column: plants2$height_cm <- as.numeric(plants2$height_cm)
  5. Check with str(), then mean(plants2$height_cm)

If you get NA, a step went wrong. Go back to step 3.

This is 80% of real data cleaning: str(), find the culprit, fix the value, convert, check.

Your turn: scale a sequence

⏱ 4 min

from <- 3
to <- 12
k <- 5

Make s, the whole numbers from from to to, and scaled, equal to s times k.

Use the variables, not typed-in numbers. Then check length(s).

s <- from:to          # or seq(from, to)
scaled <- s * k
length(s)             #> [1] 10

Why does s * k work with a single k? R recycles the short one. Very handy, and occasionally a trap.

Names: labels on a vector

fares <- c(adult = 2.50, student = 1.25)
fares
#>   adult student
#>    2.50    1.25
names(fares)                       #> "adult" "student"
fares["student"]                   # look up by name, not position
#> student
#>    1.25
names(fares)[2] <- "child"         # change one
names(fares) <- c("A", "C")        # replace all
fares$adult
#> Error: $ operator is invalid for atomic vectors

$ is for lists and data frames. Named vectors use ["name"].

Your turn: number the months

⏱ 5 min

R ships with LETTERS, letters, month.name and month.abb. Print them.

Then build month_no: the numbers 1 to 12, with the abbreviated month names as their names. Look up October by name.

month_no <- ____
names(month_no) <- ____
month_no["Oct"]
month_no <- 1:12
names(month_no) <- month.abb
month_no["Oct"]
#> Oct
#>  10

Bonus: what does typeof(names(month_no)) give, and why?

Factors: categories with a fixed set of levels

size <- factor(c("small", "large", "small", "medium"))
size
#> [1] small  large  small  medium
#> Levels: large medium small
levels(size)          #> "large" "medium" "small"    alphabetical!
table(size)
#> size
#>  large medium  small
#>      1      1      2
size <- factor(size, levels = c("small", "medium", "large"))   # your order
as.integer(size)      #> [1] 1 3 1 2     factors are integer codes + labels

Warning

as.numeric() on a factor gives you the codes, not the values. For a factor of numbers, use as.numeric(as.character(f)).

Lists: anything goes

A list is a vector whose elements can be anything, of any type and any length.

trip <- list(place = "Monteverde", days = 3,
             species_seen = c("quetzal", "sloth"))
str(trip)
#> List of 3
#>  $ place       : chr "Monteverde"
#>  $ days        : num 3
#>  $ species_seen: chr [1:2] "quetzal" "sloth"
trip$species_seen        #> "quetzal" "sloth"
trip[["days"]]           #> [1] 3
trip["days"]             # a LIST of length 1, not the number

The punchline

A data frame is a list of equal-length vectors.

typeof(plants)      #> [1] "list"
class(plants)       #> [1] "data.frame"
length(plants)      #> [1] 3         ...columns!

Everything about pulling pieces out of a data frame follows from that:

You write You get
plants$species, plants[["species"]], plants[[1]] the column, a vector
plants["species"], plants[1] a one-column data frame
plants[2, 1] one value
plants[, 1] all rows of column 1, a vector
plants[2, ] row 2, a one-row data frame

Your turn: vector or data frame?

⏱ 6 min

Predict what class() returns for each: "data.frame", "character", "numeric" or "logical".

plants[1]
plants[[1]]
plants$height_cm
plants["flowering"]
plants[1, 1]
plants[, 2]
plants[2, ]

Then check with class(). One bracket keeps the box; two brackets take the contents out.

data.frame · character · numeric · data.frame · character · numeric · data.frame

Matrices: a vector with dimensions

m <- matrix(1:6, nrow = 2, ncol = 3)     # filled column by column
m
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
dim(m)     #> [1] 2 3
nrow(m); ncol(m)
matrix(1:6, nrow = 2, byrow = TRUE)      # filled row by row
#>      [,1] [,2] [,3]
#> [1,]    1    2    3
#> [2,]    4    5    6
length(m)      #> [1] 6      not 2, not 3
class(m)       #> "matrix" "array"

A matrix is one long vector wearing a dim attribute. Same type everywhere, like any vector.

Your turn: which command made this?

⏱ 5 min

#>      [,1] [,2]
#> [1,]    4    1
#> [2,]    9    5
#> [3,]   10    7

Which of these produced it? Reason it out first, then check.

  1. matrix(c(4, 9, 10, 1, 5, 7), nrow = 3)
  2. matrix(c(4, 9, 10, 1, 5, 7), ncol = 2, byrow = TRUE)
  3. matrix(c(4, 1, 9, 5, 10, 7), nrow = 3, byrow = TRUE)
  4. matrix(c(4, 1, 9, 5, 10, 7), ncol = 2)

1 and 3. Two different commands, one result: column-by-column with the values in column order, or row-by-row with the values in row order.

The whole map

Same type only

  • vector — 1 dimension
  • matrix — 2 dimensions
  • array — n dimensions

Mixed types

  • list — anything, any length
  • data.frame — a list of equal-length vectors

With extras

  • factor — integer codes + labels
  • Attributes: names, dim, levels


A data frame is the rectangle you’ll work in all quarter. Everything under it is a vector, and every vector has one type.

Key points

  • ?name opens a help page. Read Arguments, run Examples
  • ??"topic" and apropos() find functions when you don’t know the name
  • An error has a where and a what. Read both, then shrink the example
  • The five atomic types: logical, integer, double, complex, character
  • Mixing types coerces silently, toward character. str() catches it
  • A data frame is a list of equal-length column vectors
  • $ and [[ ]] give a vector; [ ] with one index keeps a data frame

Before you go

Tuesday DS (Oct 6, 9:50 am): Lesson 3: Seeking Help and Lesson 4: Data Structures.

  • Solve it alone, no AI, including for help
  • Download the proof-of-completion PDF from each lesson page
  • Bring the printout to Thursday, Oct 8: that’s your first discussion check

Before Tuesday’s lecture: make a data/ folder inside stat266a and save gapminder_data.csv into it. We start from real data.

Next: Tuesday, Oct 6 · Exploring Data Frames

Stuck on the DS? Office hours after class, or Ed Discussion. Bring the exact error message.