x <- c(1, 2, 3)
f <- function(v) v * 2
class(f) # a function is an object, exactly like a vector#> [1] "function"
class(`+`) # so is the plus sign#> [1] "function"
`+`(x, 1) # so x + 1 is a call to that object#> [1] 2 3 4
Thursday, August 27, 2026
Slides · Live script · Source
This is the densest session of the semester. Everything below is in the live script. Run it line by line at home, as many times as it takes. You do not need to memorize anything today.
By the end of this session you can:
This is a lot for one session, and that is deliberate. Nobody absorbs it all in the room. See it once today, then re-run the script until it sticks.
The distinction tells you who to ask when something breaks. “R does not have that function” is a language question, and the answer is a package or a different function. “R will not start” is a program question, and the answer is a reinstall. Most people report the second when they mean the first.
R logo © The R Foundation, used unmodified under CC-BY-SA 4.0.
Everything that exists is an object. Everything that happens is a function call.
John Chambers, one of R’s designers
x <- c(1, 2, 3)
f <- function(v) v * 2
class(f) # a function is an object, exactly like a vector#> [1] "function"
class(`+`) # so is the plus sign#> [1] "function"
`+`(x, 1) # so x + 1 is a call to that object#> [1] 2 3 4
The two lines are a pair, and the second is the half people garble. A function is an object: it has a class, a name you gave it, and a place in your environment, exactly like a vector. You can store one in a list, hand one to another function, or print it by typing its name without parentheses. class() says function for both of the ones above.
What happens is the call. x + 1 is a call to the function object named +, and the backticks let you write that name where R would otherwise read an operator. `[`(x, 2) is the same trick: the square bracket is a function too.
You will never write code this way. Knowing it is true explains why R is consistent: there is one mechanism underneath, not a list of special cases.
Underneath, the two function objects differ: typeof(f) is closure and typeof(`+`) is builtin. That is the class-and-type distinction again.
Code organised around objects: values that carry their own class, where the class decides what a function does with them.
Polymorphism means that a developer can consider a function’s interface separately from its implementation, making it possible to use the same function form for different types of input.
Hadley Wickham, Advanced R
Plainly: one name, many behaviours. You write summary(x) without knowing what x is. R reads the class and runs the version built for it.
summary(c(1, 2, 3, 100))#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 1.00 1.75 2.50 26.50 27.25 100.00
summary(c("a", "b", "a"))#> Length Class Mode
#> 3 character character
summary() is one name and two different behaviors. On numbers it returns six statistics. On text it reports length, class and mode, because quartiles of “a” mean nothing. R reads the object’s class and picks a method to match. The technical word is polymorphism: one interface, many implementations, chosen by the class of what you hand it.
That is the whole advantage, and it is worth being concrete. You learn summary(), print() and plot() once. Install a package next month that defines some new kind of object, and those three will usually work on it straight away, because the package author wrote methods for the generics you already know. You are not learning a fresh vocabulary per package. Without this you would need summary_vector(), summary_data_frame(), summary_regression(), and a new name for every type anyone ever invents.
Two ways to arrange it. Python and Java put the methods inside the object, so calls read object.method(arg). R puts them inside the function: summary() is a generic that owns a set of methods and chooses between them, so calls read generic(object, arg). Same idea, opposite arrangement. R’s version is why R code looks like ordinary function calls and never like x.summary().
So which languages are object-oriented? Python thoroughly, in the methods-inside-the-object style. R thoroughly, in the methods-inside-the-function style, and it has four systems for it: S3, S4 and Reference Classes in base R, plus R6 from a package. This course uses S3 and never says so, because S3 asks nothing of you: an object carries a class attribute, and a generic like summary() dispatches to summary.factor() or summary.data.frame().
Stata is the interesting case. The command language you type is not object-oriented: regress y x is an instruction, not a message to an object. But Stata’s two programming languages both provide classes, [P] class for ado and [M-2] class for Mata. The distinction is between what a user types and what a package author writes, and it is a good reminder that “object-oriented” describes how code is organised, not how good it is.
For the formal version, see the OOP chapter of Advanced R, linked from the Resources page.
Two words that sound alike and are not.
class(as.Date("2025-01-07"))#> [1] "Date"
typeof(as.Date("2025-01-07"))#> [1] "double"
class(factor("a"))#> [1] "factor"
typeof(factor("a"))#> [1] "integer"
Class is what an object announces, and what functions dispatch on. Type is how R stores it underneath. Usually the two agree, and you can ignore the difference. When they disagree you get the traps in this session. A Date is a number with a class attached, counting days from 1970. A factor is a set of integers with a class attached.
This page says class almost everywhere, because class() is the function you will actually run. Type comes back once, at the factor trap, which is where the difference costs you something.
| Stata | R | Python | |
|---|---|---|---|
| Built for | data analysis | data analysis | general programming |
| Data in memory | one active dataset; more via frames |
many named objects | many named objects |
| Naming the data | implicit: regress y x |
explicit: lm(y ~ x, data = d) |
explicit |
| Where tables come from | the dataset is the workspace | objects you create and name | from libraries |
| Strongest at | a clean panel to a published table | statistical methods and graphics | pipelines, APIs, ML, production code |
| Who uses it | economists, social scientists | statisticians, many fields | programmers, CS, machine learning |
Economics is a Stata field. That is measured, not folklore.
Stata works on one active dataset, and commands act on it without naming it: regress y x needs no data = because there is only one thing it could mean. Since Stata 16 the frames command holds several datasets at once, but one of them is still the active one, and that shapes how Stata code reads.
R has no ambient dataset. Every object has a name and every function says which one it works on. The result of lm() is itself an object you can store, print, and take apart.
That is what the last row means. In Stata the table is the workspace: a variable is a column of the dataset, not a thing you can hold on its own. In R a vector or a data frame is an object like any other. You make it, name it, hand it to a function, and keep ten more beside it.
Grammar makes the difference concrete. A Stata command drops its subject because the subject is understood. R names every noun, and lets several be in play at once. That is why R needs data = d and Stata does not, and why R can hold your raw file, your cleaned file, and three models at the same time.
Python is a general-purpose language that acquired data tools later. R was built for data analysis from the start, so vectors, data frames, and formulas like y ~ x belong to the language rather than to a library.
The last row of the table needs a word. c() and data.frame() work the moment R starts, because vectors and tables are part of R itself. Python has neither. You get them from libraries: collections of code somebody else wrote, which you install once and load in each script. Two matter for data work. numpy adds numeric arrays and fast arithmetic over them. pandas adds a table type called a DataFrame, which is R’s data frame under a different name. A Python analyst loads both at the top of nearly every script.
That is not a defect, it is what “general-purpose” means: Python keeps a small core and you add what the job needs. R made the opposite trade, which is why R feels quick for a table and a model and clumsy for building an application. R has libraries too, called packages. The difference is what you get before you install anything.
What each is strongest at. Stata gives you the shortest path from a clean panel to a published table: one documented way to do most things, and versioned, so old code keeps running. R has the widest library of statistical methods and the strongest tools for turning a messy source into a publication figure. Python is general programming: pipelines, APIs, scraping, machine learning, and software other people run.
Lars Vilhuber’s annual Report of the AEA Data Editor (AEA Papers and Proceedings 114, 2024, 878-90) tabulates the software found in every replication package the AEA journals accept. Stata appears in the large majority of them. MATLAB and R follow, each in a substantial minority, and Python has gone from almost absent before 2016 to a meaningful share. The trend worth noticing is that a large and rising fraction of packages use more than one language: the question is becoming which tool for which step, not which tool.
Across disciplines the picture is different. Robert Muenchen has tracked scholarly use by software for more than a decade in The Popularity of Data Science Software. R and Python dominate the overall counts, while Stata’s use is concentrated in economics and parts of political science, sociology, and epidemiology. That concentration is why Stata can look universal from inside economics and marginal from outside it, and why a paper you want to reuse may well arrive in a language your field does not default to.
None of this makes one of them correct. It does mean that the tool you know determines whose code you can read, and that reading other people’s code is most of what reuse means.
Everything in R is an object with a name you give it and a class R assigns.
x <- c(1, 2, 3, 4, 5) # the arrow points from the value into the name
x = c(1, 2, 3, 4, 5) # also assigns; you will meet it in other code
c(1, 2, 3, 4, 5) -> x # legal, and points the other way
x#> [1] 1 2 3 4 5
class(x)#> [1] "numeric"
length(x)#> [1] 5
<- is the assignment arrow, and it is the convention in R. = assigns too, and you will meet it in other people’s code and in code an AI writes for you. -> assigns rightward. It is legal and rare.
Two reasons this course writes <-. The arrow shows which way the value moves, and = does not. And = already means something else inside a function call: in seq(from = 1), the = sets an argument, it does not create an object called from.
Do not confuse = with ==. One assigns. The other tests two things for equality and returns TRUE or FALSE. You meet it in the next section.
R prints nothing when you assign; that is normal. Type the object’s name on its own line to see it.
ls() # everything you have named, right now#> [1] "f" "x"
5 + 3 # this prints, and is gone#> [1] 8
ls() # nothing new: it was never given a name#> [1] "f" "x"
ls() lists what is in your environment. RStudio shows the same list in the Environment pane, top right, and that pane is worth watching while you work: if an object is not in it, no line of your script created it.
The second half matters more than it looks. A result you do not assign is printed and discarded. 5 + 3 shows you 8 and leaves nothing behind, so the next line cannot use it. This is the most common reason a script “worked” in the console and then fails when you re-run it from the top: the object it needs was never actually stored, only displayed.
rm(x) removes one object; rm(list = ls()) empties the environment. Do that before a final re-run, so you find out whether the script really builds everything it uses.
Every function ships with a help page. Both of these open it, from the console:
?seq # the help page for seq()
help(seq) # the same thing
??regression # search the help system when you do not know the nameThe page opens in RStudio’s Help pane. Read the Usage block first: it lists the arguments and their defaults. Then go to Examples at the bottom and run one. The prose in between is written for people who already know the function.
? needs the exact name. ?? searches titles and keywords, so reach for it when you know what you want but not what it is called.
A vector is an ordered sequence of values that are all the same class: all numbers, or all text, or all TRUE/FALSE. It is the basic unit of R: the single number 5 is a vector of length one.
1:10#> [1] 1 2 3 4 5 6 7 8 9 10
seq(0, 100, by = 25)#> [1] 0 25 50 75 100
seq(0, 1, length.out = 5)#> [1] 0.00 0.25 0.50 0.75 1.00
rep(c("a", "b"), times = 3)#> [1] "a" "b" "a" "b" "a" "b"
rep(c("a", "b"), each = 3)#> [1] "a" "a" "a" "b" "b" "b"
Arithmetic applies to every element at once. You almost never write a loop for this:
x * 2#> [1] 2 4 6 8 10
x + c(10, 20, 30, 40, 50)#> [1] 11 22 33 44 55
x + c(0, 100) # lengths do not match -- R recycles, and warns#> Warning in x + c(0, 100): longer object length is not a multiple of shorter
#> object length
#> [1] 1 102 3 104 5
One number out of a whole vector.
sum(x)#> [1] 15
mean(x)#> [1] 3
min(x)#> [1] 1
max(x)#> [1] 5
range(x) # both ends at once#> [1] 1 5
round(mean(x), 2) # trim the decimals before you report a number#> [1] 3
# One line, two commands: what is the mean of 0, 25, 50, 75, 100?mean(seq(0, 100, by = 25))#> [1] 50
R reads from the inside out. seq() runs first and hands its answer to mean(). Nesting is how you combine commands today, and it is worth getting comfortable with: most real lines do two or three things at once.
class(1)#> [1] "numeric"
class("a")#> [1] "character"
class(TRUE)#> [1] "logical"
class(as.Date("2025-01-07"))#> [1] "Date"
Four classes carry almost all of empirical work: numeric (numbers), character (text), logical (TRUE/FALSE), and Date. A vector holds exactly one of them. That constraint sets up the next section.
A vector can only hold one class. So when you mix classes, R does not stop or warn. It silently converts everything to a class that can hold them all.
c(1, 2, 3)#> [1] 1 2 3
c(1, 2, "three") # one text value converts the whole vector#> [1] "1" "2" "three"
class(c(1, 2, "three"))#> [1] "character"
sum(c(TRUE, FALSE, TRUE)) # TRUE becomes 1, FALSE becomes 0#> [1] 2
mean(c(TRUE, FALSE, TRUE)) # so the mean of a logical is a proportion#> [1] 0.6666667
This one is useful rather than dangerous, and you will use it all semester. Counting how many rows meet a condition is a sum() over a logical vector. The share of rows that meet it is a mean() over the same vector.
sort(c(10, 9, 100))#> [1] 9 10 100
sort(c("10", "9", "100")) # character by character#> [1] "10" "100" "9"
Text sorts character by character, so "100" lands before "9" because "1" comes before "9". Nothing errors. A column of years, ZIP codes, or account numbers read as text will sort into an order that looks almost right. It comes back today at the factor trap.
The convenience has a cost. Here is what one stray text entry does to a column of numbers:
readings <- c(12.4, 18.1, 9.7, "n/a", 22.3)
class(readings)#> [1] "character"
mean(readings)#> Warning in mean.default(readings): argument is not numeric or logical:
#> returning NA
#> [1] NA
mean() did not stop. It returned NA with a warning you will scroll past, and if that value had gone into a table nobody would have seen a red flag. This is the failure mode the whole course trains you to catch: it ran, and it was wrong.
Repair it, then check the repair:
readings <- as.numeric(readings) # "n/a" cannot convert -- it becomes NA#> Warning: NAs introduced by coercion
readings#> [1] 12.4 18.1 9.7 NA 22.3
mean(readings, na.rm = TRUE)#> [1] 15.625
NA stands for not available. It is not zero, and it is not the text "NA". It marks a value R does not have, and R enforces that: any computation touching an unknown returns an unknown.
temps <- c(21.0, NA, 19.4, 23.8)
mean(temps)#> [1] NA
mean(temps, na.rm = TRUE)#> [1] 21.4
is.na(temps)#> [1] FALSE TRUE FALSE FALSE
sum(is.na(temps))#> [1] 1
temps == NA # == tests for equality; this is not how you test for NA#> [1] NA NA NA NA
== is a test, not an assignment. It asks whether two things are equal and returns TRUE or FALSE for each element.
Testing against NA returns NA rather than FALSE, and that is consistent rather than broken. R does not know whether an unknown value equals an unknown value, so it declines to guess. This is why is.na() exists: it asks a different question, “is this value missing,” which R can always answer.
Square brackets pull elements out by position.
x <- c(10, 20, 30, 40, 50)
x[1]#> [1] 10
x[c(1, 3)]#> [1] 10 30
x[-1] # everything except the first#> [1] 20 30 40 50
x[length(x)] # the last, however long it is#> [1] 50
Get comfortable with this one: every filter you write for the rest of the semester works this way.
x > 25 # a TRUE/FALSE for every element#> [1] FALSE FALSE TRUE TRUE TRUE
x[x > 25] # keep the elements where it is TRUE#> [1] 30 40 50
sum(x > 25) # count them -- TRUE is 1#> [1] 3
x[x > 25 & x < 45] # & is and, | is or#> [1] 30 40
Read x[x > 25] aloud as “x, where x is greater than 25.” The condition inside the brackets produces a logical vector, and R keeps the positions that are TRUE. Once you see it that way, sum(x > 25) is not a trick: it counts the TRUEs.
# w <- c(12, 45, 7, 33, 88, 21)
#
# One line each: how many values are above 20?
# what is the mean of just those?w <- c(12, 45, 7, 33, 88, 21)
sum(w > 20)#> [1] 4
mean(w[w > 20])#> [1] 46.75
The second line nests too: the subset runs first, then mean() gets what survives. This pair is the single most reused pattern in the course. Session 4 writes it as filter() and summarize(), over the same idea.
A data frame is a set of equal-length vectors standing side by side: columns of possibly different classes, rows that line up. Every dataset in this course is one.
d <- data.frame(
site = c("Compton", "Compton", "Reseda"),
date = c("2025-01-01", "2025-01-02", "2025-01-01"),
pm25 = c(53.2, 33.6, 47.0)
)
d#> site date pm25
#> 1 Compton 2025-01-01 53.2
#> 2 Compton 2025-01-02 33.6
#> 3 Reseda 2025-01-01 47.0
str(d)#> 'data.frame': 3 obs. of 3 variables:
#> $ site: chr "Compton" "Compton" "Reseda"
#> $ date: chr "2025-01-01" "2025-01-02" "2025-01-01"
#> $ pm25: num 53.2 33.6 47
e <- data.frame(site = "Reseda", date = "2025-01-02", pm25 = 41.5)
identical(names(d), names(e)) # same columns, same order?#> [1] TRUE
both <- rbind(d, e) # stack the rows
nrow(both)#> [1] 4
rbind() stacks frames that share their column names, in the same order. Check with identical(names(d), names(e)) before you stack, not after: if the names differ, rbind() errors, and if they match in a different order you get a silently scrambled frame.
This comes up whenever a source hands you one file per year, per state, or per month. You read them, check the names agree, and stack.
Subsetting takes two coordinates: d[rows, columns]. Leave one blank to mean “all of them.”
d$pm25 # one column, by name#> [1] 53.2 33.6 47.0
d[1, ] # first row, all columns#> site date pm25
#> 1 Compton 2025-01-01 53.2
d[, "pm25"] # all rows, one column#> [1] 53.2 33.6 47.0
d[d$pm25 > 40, ] # the rows where a condition is TRUE#> site date pm25
#> 1 Compton 2025-01-01 53.2
#> 3 Reseda 2025-01-01 47.0
sort() returns values. order() returns the positions that would put a vector in order.
v <- c(10, 50, 30)
order(v) # positions, not values#> [1] 1 3 2
v[order(v)] # which is what sort() does#> [1] 10 30 50
d[order(d$pm25), ] # so this sorts the whole frame by one column#> site date pm25
#> 2 Compton 2025-01-02 33.6
#> 3 Reseda 2025-01-01 47.0
#> 1 Compton 2025-01-01 53.2
Positions are what you need when the values are one column of a table. sort() puts a column in order but cannot bring the other columns with it. order() hands you the row numbers instead, and the brackets do the rest. order(-x) reverses it.
This is the third way to subset, after position and condition, and it is the one you compute rather than type.
A factor is how R stores a categorical column: the values live as numeric codes, with a lookup table of labels called levels.
site <- factor(c("Compton", "Reseda", "Compton", "Compton"))
site#> [1] Compton Reseda Compton Compton
#> Levels: Compton Reseda
levels(site)#> [1] "Compton" "Reseda"
table(site)#> site
#> Compton Reseda
#> 3 1
You will rarely create a factor on purpose this early. You will run into them: some functions return them, and some files load as them. For now, know what one is, and know the trap in the next section.
Remember "10" sorting before "9"? Factors do the same thing, and it is harder to see.
f <- factor(c("10", "9", "100")) # numbers, stored as categories
as.numeric(f) # NOT the numbers -- the level codes#> [1] 1 3 2
as.numeric(as.character(f)) # the repair: text first, then number#> [1] 10 9 100
as.numeric() on a factor returns the internal codes (the position of each value in the alphabetically sorted level table), not the numbers the labels spell. You get no error, no warning, and plausible output. The repair is always the same two steps: as.character() first, then as.numeric().
This is the one place today where class and type come apart. A factor’s class is factor; its type, the way R stores it, is integer. as.numeric() reaches past the class down to the storage, which is why you get codes. as.character() first asks the class for its labels, and then there are real numbers to convert.
Eighteen exercises, one or two lines each, in the order the session covered them. They are in the live script too. Try each one before you open the solutions.
# Vectors and sequences
# 1. Build the whole numbers 1 to 20.
# 2. Build the even numbers from 2 to 20.
# 3. Repeat "yes" and "no", alternating, four times each.
# 4. How many odd numbers are there between 1 and 99? Two commands, one line.
#
# Classes and conversion
# 5. Predict first, then check: what class is c(1, 2, "3")?
# 6. x <- c("4.5", "2.1", "8.8") arrived as text. Get its mean.
# 7. Sort c("5", "10", "9") into true numeric order, in one line.
# 8. y <- c(3, NA, 7, NA, 12). How many values are missing?
# 9. Same y: what is its mean, ignoring the gaps?
#
# Subsetting
# 10. z <- c(10, 25, 3, 47, 18, 60). Keep only the values above 20.
# 11. Same z: how many values are above 20?
# 12. Same z: what is the mean of the values above 20?
# 13. Same z: drop the first and the last value, however long z is.
# 14. Same z: put it in order without using sort().
#
# Data frames and factors
# 15. Build a data frame: city = Ithaca, Buffalo; pop = 30, 275.
# 16. From it, print the row where pop is above 100.
# 17. f <- factor(c("2019", "2007", "2013")). Put the years in order,
# as numbers.
# 18. Same f: what does as.numeric(f) give instead, and why?# 1
1:20
#> [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# 2
seq(2, 20, by = 2)
#> [1] 2 4 6 8 10 12 14 16 18 20
# 3
rep(c("yes", "no"), times = 4)
#> [1] "yes" "no" "yes" "no" "yes" "no" "yes" "no"
# 4
length(seq(1, 99, by = 2))
#> [1] 50
# 5 one text value converts the whole vector
class(c(1, 2, "3"))
#> [1] "character"
# 6
ex <- c("4.5", "2.1", "8.8")
mean(as.numeric(ex))
#> [1] 5.133333
# 7
sort(as.numeric(c("5", "10", "9")))
#> [1] 5 9 10
# 8
y <- c(3, NA, 7, NA, 12)
sum(is.na(y))
#> [1] 2
# 9
mean(y, na.rm = TRUE)
#> [1] 7.333333
# 10
z <- c(10, 25, 3, 47, 18, 60)
z[z > 20]
#> [1] 25 47 60
# 11
sum(z > 20)
#> [1] 3
# 12
mean(z[z > 20])
#> [1] 44
# 13
z[-c(1, length(z))]
#> [1] 25 3 47 18
# 14
z[order(z)]
#> [1] 3 10 18 25 47 60
# 15
cities <- data.frame(city = c("Ithaca", "Buffalo"), pop = c(30, 275))
cities
#> city pop
#> 1 Ithaca 30
#> 2 Buffalo 275
# 16
cities[cities$pop > 100, ]
#> city pop
#> 2 Buffalo 275
# 17
f <- factor(c("2019", "2007", "2013"))
sort(as.numeric(as.character(f)))
#> [1] 2007 2013 2019
# 18 the level codes, not the years: levels sort alphabetically as
# "2007" "2013" "2019", so 2019 is code 3, 2007 is 1, 2013 is 2
as.numeric(f)
#> [1] 3 1 2Making and inspecting things
| Code | What it does |
|---|---|
x <- c(1, 2, 3) |
Assign a vector |
1:10, seq(0, 100, by = 25) |
Regular sequences |
rep(x, times =), rep(x, each =) |
Repetition, two different ways |
length(x), class(x) |
How many, and what class |
sum(x), mean(x), min(x), max(x), range(x) |
One number out of a vector |
ls() |
What is in the workspace |
?mean |
The help page for a function |
factor(x), levels(f) |
A categorical column and its categories |
Types and missingness
| Code | What it does |
|---|---|
as.numeric(x), as.character(x) |
Convert on purpose |
is.na(x), sum(is.na(x)) |
Test for missing; count it |
mean(x, na.rm = TRUE) |
Compute over what is there |
Subsetting
| Code | What it does |
|---|---|
x[1], x[c(1, 3)], x[-1] |
By position; - drops |
order(x), x[order(x)] |
The positions that sort it; -x reverses |
x[x > 25] |
“x, where x is greater than 25” |
sum(x > 25), which(x > 25) |
Count them; find where they are |
d$col |
One column by name |
d[rows, cols] |
Blank means “all of them” |
d[d$col > 40, ] |
The rows where a condition holds |
Things that bite
| Symptom | Cause |
|---|---|
A numeric column is chr |
One stray text value converted the whole thing |
"100" sorts before "9" |
It is text; text sorts character by character |
mean() returns NA |
There are NAs; use na.rm = TRUE |
x == NA gives NA |
Use is.na(x) |
as.numeric() on a factor gives codes |
as.character() first, then as.numeric() |