Vectors, factors and data frames
The four things every R user does with a vector, and the data frame rules that decide whether your analysis is correct.
Vectors are the unit of everything
An R vector holds one type only. That single constraint explains coercion rules, why a stray string turns a column of numbers into text, and why c() is the first function everyone learns.
x <- c(4, 8, 15, 16, 23, 42) # a double vector
class(x) # "numeric"
length(x) # 6
x[c(1, 3)] # 4 15 (1-based indexing)
x[x > 15] # 16 23 42 (logical indexing)
x[-1] # drop the first element
y <- c(1, 2)
x + y # y is recycled; fine here because 6 is a multiple of 2
z <- c(1, NA, 3)
sum(z) # NA, not 4: NA is contagious by design
sum(z, na.rm = TRUE) # 4 (the explicit opt-in)
f <- factor(c("low", "high", "low"))
levels(f) # "high" "low" - sorted, not in order of appearance
as.integer(f) # 2 1 2- Indexing starts at 1, and
x[0]returns an empty vector rather than throwing. - Negative indices drop elements instead of selecting them, and you cannot mix positive and negative indices.
- Logical indexing is the idiom for filtering:
x[x > 15]. On a vector containingNAthe condition yieldsNA, so those elements come back asNAtoo. - A factor stores labels plus integer codes.
as.numeric()on a factor returns the codes, so convert throughas.character()when the labels were numbers. - Coercion runs in one direction: logical, integer, double, complex, character. One character element makes the whole vector character.
Data frames
df <- data.frame(
id = 1:5,
group = c("a", "b", "a", "b", "a"),
score = c(10, NA, 30, 40, 25),
stringsAsFactors = FALSE
)
str(df) # the first thing to run on any data frame
df$score # vector access by name
df[["score"]] # identical, and the form to use inside functions
df[df$score > 20 & !is.na(df$score), c("id", "score")]
df$score[is.na(df$score)] <- 0 # explicit imputation
df$band <- ifelse(df$score >= 25, "high", "low")
aggregate(score ~ group, data = df, FUN = mean)
tapply(df$score, df$group, mean) # a named vector
table(df$group)| Structure | Holds | Create with | Access |
|---|---|---|---|
| vector | One type only | c(1, 2, 3) | x[i] |
| factor | Labels plus integer codes | factor(x) | levels(f), as.integer(f) |
| matrix | One type, two dimensions | matrix(1:6, nrow = 2) | m[i, j] |
| list | Anything, any length | list(a = 1, b = "x") | l$a or l[["a"]] |
| data frame | Named equal-length columns | data.frame(...) | df$col, df[i, j] |
| tibble | A stricter data frame | tibble::tibble(...) | Same, plus no partial matching |
⚠️
Two traps account for most silent R bugs. First,
df[i, j] drops to a bare vector when it selects one column — pass drop = FALSE so a function that works on five rows also works on one. Second, factors used to be created automatically for character columns: in R 4.0+ stringsAsFactors defaults to FALSE, but older scripts relying on TRUE will compare and group differently when you run them now.FAQ
Why does mean() return NA?
Because the vector contains at least one
NA. R refuses to guess: pass na.rm = TRUE to ignore them, or drop the rows deliberately. The same applies to sum, sd, min and max.When should I use a list instead of a data frame?
A list is the right container when the elements differ in length or type — the output of
lm is a list, for example. A data frame is a rectangular table where every column has the same number of rows; use it for anything tabular.Related
Data manipulation with base R Series and DataFrame
Last refreshed 2026-09-18.