x <- 1
class(x)Eric Manning
August 28, 2026
[1] "numeric"
The type is R’s label for what kind of thing a value is — every object carries one
Types decide what’s possible: numbers can be averaged, text can be capitalized, etc.
Most confusing errors are type errors in disguise.
| Type | Examples | |
|---|---|---|
numeric |
1, 3.14, -2.5 |
numbers (a.k.a. “double”) |
integer |
1L, 42L |
whole numbers; the L makes it explicit |
character |
"Princeton" |
text, in quotes — 'single' work too |
logical |
TRUE, FALSE |
yes/no answers (T/F acceptable to some) |
Date |
Sys.Date() |
calendar dates |
Coming soon: list, data.frame
Mostly you’ll just say “numeric” — R treats 1 as a double by default
Why keep both? Storage: double is 8 bytes (for the decimals); integer is 4 bytes.
Mostly you’ll just notice it as int vs num in output
An object name holds a value. Assigning again replaces it.
[1] 3
Types decide what’s allowed:
Error in `school + "University"`:
! non-numeric argument to binary operator
R will not guess what you meant.
The as.*() family converts between types
[1] 3
[1] "3.9"
When R can’t convert, you get NA and a warning (not an error!)
[1] TRUE
[1] FALSE
[1] TRUE
== asks “are these equal?” = is an assignment operator, like <-
&, |, !And, or, not:
[1] FALSE
[1] TRUE
[1] FALSE
&& and ||Error in `x | y`:
! operations are possible only for numeric, logical or complex types
If left side is TRUE, right side is skipped. This is useful in if-else statements, other control flows.
These are NOT vectorized.
NANA marks a missing value inside your data
[1] FALSE TRUE FALSE
“Is this value equal to unknown?” — the answer is unknown. Use is.na().
NA has a type[1] "logical"
NA takes the type of the vector it sits in:
[1] "numeric"
[1] "character"
There’s an NA for every type (NA_real_, NA_character_, …)
Usually you don’t need to be explicit.
NA is contagiousOne NA poisons every calculation it touches
[1] NA
You will type na.rm = TRUE a thousand times and one day you will be grateful.
NULLNULL is the absence of an object — not a missing value
[1] TRUE
[1] "Date"
Subtracting two dates is something else: difftime class
c() combines values into a vector
[1] 5 10 15
[1] 3
school is a character vector of length 1.
A vector holds exactly one type. Mix them and R will silently “downcast” where possible.
[1] "1" "two" "TRUE"
Most functions are vectorized: they work element-by-element without being told explicitly
paste()The right way to do school + "University":
[1] "R Bootcamp"
[1] "PrincetonUniversity"
paste() glues with a space; paste0() glues with nothing
It casts numbers to text and vectorizes, all at once:
[1] "Day 1" "Day 2" "Day 3"
[1] 10 20 30
[1] 10 20 30
[1] 25 100 225
(** also works for powers, but write ^)
[1] 1 2 3 4 5
[1] 5 4 3 2 1
any() and all()Collapse a vector of yes/no answers into one answer
[1] FALSE TRUE TRUE
“Did anyone answer yes?” / “Did everyone answer yes?”
[1] 4
[1] "South" "West" "Northeast"
math r_bootcamp
90 95
[ pulls out pieces of a vector
[1] 10
[1] 5 15
A boolean vector inside [ keeps the TRUE positions
[1] 10 15 20
Read x[x > 8] aloud: “x, where x is greater than 8”
[1] 5 10 15 20
head() and tail() peek at the ends. Combine for smallest and biggest:
[1] 5 10
[1] 15 20
[,1] [,2]
[1,] 1 5
[2,] 2 6
[3,] 3 7
[4,] 4 8
byrowmatrix() fills down the columns unless you say otherwise:
[,1] [,2]
[1,] 1 5
[2,] 2 6
[3,] 3 7
[4,] 4 8
[,1] [,2]
[1,] 1 2
[2,] 3 4
[3,] 5 6
[4,] 7 8
cbind()Bind on a new column:
[,1] [,2] [,3]
[1,] 1 5 9
[2,] 2 6 10
[3,] 3 7 11
[4,] 4 8 12
Warning in cbind(m, 1:3): number of rows of result is not a multiple of
vector length (arg 2)
[,1] [,2] [,3]
[1,] 1 5 1
[2,] 2 6 2
[3,] 3 7 3
[4,] 4 8 1
Why the warning?
Recycling is convenient for single values and dangerous for everything else.
Arithmetic is element-by-element, like vectors
[,1] [,2]
[1,] 2 10
[2,] 4 12
[3,] 6 14
[4,] 8 16
%*%Build an N × 1 column — now shapes matter:
[,1]
[1,] 10
[2,] 20
* is not %*%Element-wise * is different. It wants identical shapes, just like + or - or /:
Error in `m * v`:
! non-conformable arrays
(m * 2 worked because a lone number recycles; a mis-shaped matrix does not)
If you mean linear algebra, write %*%.
[row, column][1] 1 5
[1] 5 6 7 8
[1] 7
Different types, different lengths, even other lists
[[ versus [[1] "lists are flexible"
[[ returns the element itself; [ returns a smaller list
[1] "character"
[1] "list"
Why care about lists?
mtcars is a data frame built into R — let’s borrow it
[1] "data.frame"
Typing df prints all rows. Use head() instead:
mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
head() shows just the first rows (six, unless you say otherwise)
str()One line per column: type first, values after
'data.frame': 32 obs. of 11 variables:
$ mpg : num 21 21 22.8 21.4 18.7 ...
$ cyl : num 6 6 4 6 8 ...
$ disp: num 160 160 108 258 360 ...
$ hp : num 110 110 93 110 175 ...
$ drat: num 3.9 3.9 3.85 3.08 3.15 ...
$ wt : num 2.62 2.88 ...
$ qsec: num 16.5 17 ...
$ vs : num 0 0 1 1 0 ...
$ am : num 1 1 1 0 0 ...
$ gear: num 4 4 4 3 3 ...
$ carb: num 4 4 1 1 2 ...
This is usually the first thing to run on any new data (alternatively, View(df) in RStudio)
The car names aren’t a column. They live in the row names
Don’t do this.
Assign into the attribute:
[1] "miles_per_gallon" "cyl" "disp"
[4] "hp" "drat" "wt"
[7] "qsec" "vs" "am"
[10] "gear" "carb"
Same trick works for rownames() — these are just character vectors you can overwrite
$ [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4
table() counts values, but it’s slow on large or high-cardinality vectors
4 6 8
11 7 14
as.matrix(): what do we lose? mpg cyl disp hp
Mazda RX4 21.0 6 160 110
Mazda RX4 Wag 21.0 6 160 110
Datsun 710 22.8 4 108 93
The row names survive because matricies allow row (and column) names.
[1] "matrix" "array"
Matricies require the same type for all cells: one character column would have case the whole matrix into chars
Data frames take [row, column] just like matrices
mpg cyl
Mazda RX4 21 6
Mazda RX4 Wag 21 6
cyl
Mazda RX4 6
Mazda RX4 Wag 6
Datsun 710 4
df["cyl"] is still a data frame — list subsetting with [
[[ or $ or [, "cyl"] or [, 2]) gives the vector itself:
[1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4
And single elements by position: df[1, 1] is 21
data.frame() works like list(): name the columns, give equal-length vectors
school students
1 Princeton 5590
2 Rutgers 36000
Every file lives in a folder, and every folder lives inside another — one big tree (folder = “directory”).
/Users/you/
├── Desktop/
├── Downloads/
└── Documents/
└── bootcamp/
├── day-1.R
└── data/
└── county_population.csv
A file’s path is its full address — the folders you pass through, joined by /:
/Users/you/Documents/bootcamp/data/county_population.csv
(Windows writes it C:\Users\you\... but in R, always write / – it works on every system)
Mac Users: right-/double-click + Option (⌥) + “Copy ‘…’ as Pathname”
If you have everything in ~/Downloads….
R always works from one folder — its working directory:
[1] "/Users/ericmm/Documents/GitHub/r-bootcamp/notes"
A relative path gives directions from there. No absolute file path required.
(“go into data/, grab the file” — and .. means “up one folder”)
setwd() exists, but hard-coded paths break on other computers (or yours when you move stuff around).
Always assume you’re in the project root and make all paths relative.
scripts/../data/county_population.csv relative to that folder (but why won’t we need this?)2020 Census population for every US county. Download it here and save it in data/
[1] "fips,region,division,pop"
[2] "01001,South,East South Central,58805"
[3] "01003,South,East South Central,231767"
A .csv is just text: one row per line, commas between values, names in the first line
read.csv() fips region division pop
1 1001 South East South Central 58805
2 1003 South East South Central 231767
3 1005 South East South Central 25223
Compare with the raw file: "01001" became the number 1001 — the leading zeros are gone!
Tell read.csv() what the column really is:
'data.frame': 3143 obs. of 4 variables:
$ fips : chr "01001" "01003" ...
$ region : chr "South" "South" ...
$ division: chr "East South Central" "East South Central" ...
$ pop : int 58805 231767 25223 22293 59134 ...
This distinction is often important if you are joining data, if ‘0xx’ is different from ‘xx’ etc.
Surveys love codes like -9 or -999 for “no answer.”
[1] -312.6667
NA is always better. (Why?)
read.csv() arguments to know| Argument | When you need it |
|---|---|
header = FALSE |
the file has no column-name row |
skip = 3 |
junk lines (titles, notes) sit above the data |
nrows = 100 |
peek at the first chunk of a huge file |
fill = TRUE |
ragged files — some rows end early |
na.strings |
which codes mean “missing” |
Every one of these is a question about your file. You can always use readLines(path, n = a_small_number) to inspect the file.
summary() fips region division pop
Length :3143 Length :3143 Length :3143 Min. : 64
N.unique :3143 N.unique : 4 N.unique : 9 1st Qu.: 10832
N.blank : 0 N.blank : 0 N.blank : 0 Median : 25698
Min.nchar: 5 Min.nchar: 4 Min.nchar: 7 Mean : 105456
Max.nchar: 5 Max.nchar: 9 Max.nchar: 18 3rd Qu.: 67946
Max. :10014009
Every column at once: quartiles for numbers, profiles for text. Both str() and summary() are useful.
| Format | Functions | Trade-off |
|---|---|---|
.csv |
read.csv() / write.csv() |
opens anywhere; loses types (ask fips!) |
.rds |
readRDS() / saveRDS() |
one R object, restored exactly; R-only |
.rda |
load() / save() |
many objects at once; R-only |
.dta, .xlsx |
haven, readxl packages |
Stata and Excel files 😩 |
Careful with .rda: load() will overwrite existing objects if they have the same name.
Rule of thumb: share .csv with humans, save .rds and .rda for yourself (and collaborators).
Sometimes.
row.names = FALSE or you’ll get a useless, unnamed column
Reasons to prefer one or the other?
[1] 105456.3
Functions compose, evaluated inside-out:
[1] 105456
Assign into a column that doesn’t exist yet — and now it does:
fips region division pop pop_millions
1 01001 South East South Central 58805 0.06
2 01003 South East South Central 231767 0.23
3 01005 South East South Central 25223 0.03
[1] 335760.4
Careful: log() is the natural log — base 10 is log10()
What share of counties hold a million-plus people?
[1] 0.0155902
mean() on booleans again — about 1.6% (sum() would count them)
Give me all the information about the three largest counties:
summary() and beyond Min. 1st Qu. Median Mean 3rd Qu. Max.
64 10832 25698 105456 67946 10014009
Build your own with a named vector:
n mean sd
3143.0 105456.3 335760.4
This gets repetitive if we write the same code for each column – or for a bunch of different data frames…
Morning:
lapply() and friendsAfternoon:
tidyverse for working more effectively with datatidyverse