06 · Data Frames Basics¶
What a data frame is¶
A data frame is R's core structure for tabular data — rows are observations, columns are variables, and each column is a vector (so every value in a column shares one type, but different columns can differ). It's the structure you'll spend the most time with in every later module.
students <- data.frame(
name = c("Alice", "Bob", "Carol"),
age = c(23, 25, 22),
passed = c(TRUE, FALSE, TRUE)
)
students
# name age passed
# 1 Alice 23 TRUE
# 2 Bob 25 FALSE
# 3 Carol 22 TRUE
Inspecting a data frame¶
str(students) # STRucture: compact summary of columns and types
# 'data.frame': 3 obs. of 3 variables:
# $ name : chr "Alice" "Bob" "Carol"
# $ age : num 23 25 22
# $ passed: logi TRUE FALSE TRUE
nrow(students) # [1] 3
ncol(students) # [1] 3
dim(students) # [1] 3 3
names(students) # [1] "name" "age" "passed"
colnames(students) # same as names() for data frames
head(students, 2) # first 2 rows
summary(students) # per-column summary stats
str() is one of the most useful functions in all of R for a first look at
any unfamiliar object — reach for it constantly.
Selecting columns¶
students$name # a vector: "Alice" "Bob" "Carol"
students[["age"]] # equivalent to $ -- a vector
students["age"] # a one-column DATA FRAME, not a vector
students[, "age"] # a vector -- comma selects "all rows, this column"
students[, c("name", "age")] # a two-column data frame
Selecting rows¶
students[1, ] # first row (all columns)
# name age passed
# 1 Alice 23 TRUE
students[1:2, ] # first two rows
students[students$age > 22, ] # filter by condition -- rows where TRUE
# name age passed
# 1 Alice 23 TRUE
# 2 Bob 25 FALSE
The general pattern is df[rows, columns] — leaving either side blank means
"all". This is the base-R way to filter; Level 2
introduces dplyr::filter(), which reads more clearly for complex conditions.
Adding and modifying columns¶
students$grade <- c("B", "C", "A")
students
# name age passed grade
# 1 Alice 23 TRUE B
# 2 Bob 25 FALSE C
# 3 Carol 22 TRUE A
students$age <- students$age + 1 # modify an existing column in place
students$age
# [1] 24 26 23
Adding rows¶
new_student <- data.frame(name = "Dave", age = 30, passed = TRUE, grade = "A")
students <- rbind(students, new_student)
nrow(students)
# [1] 4
rbind() requires the new data frame to have the same column names and
compatible types; a mismatch throws an error rather than silently corrupting
data.
Sorting¶
students[order(students$age), ] # ascending by age
students[order(-students$age), ] # descending (negate to reverse)
students[order(students$passed, students$age), ] # multiple sort keys
Basic aggregation¶
mean(students$age)
# [1] 25.75
tapply(students$age, students$passed, mean)
# FALSE TRUE
# 26 23.66667
tapply() groups a vector by another vector and applies a function to each
group — a preview of the group-by pattern that dplyr::group_by() handles
more readably in Level 2.
data.frame cheat sheet¶
| Task | Syntax |
|---|---|
| Create | data.frame(col1 = ..., col2 = ...) |
| Inspect structure | str(df) |
| Dimensions | nrow(df), ncol(df), dim(df) |
| Column as vector | df$col or df[["col"]] or df[, "col"] |
| Column as data frame | df["col"] |
| Filter rows | df[df$col > 10, ] |
| Add/modify column | df$new_col <- ... |
| Add row | rbind(df, new_row_df) |
| Sort | df[order(df$col), ] |
| Group + summarize | tapply(df$value, df$group, mean) |
How It Actually Works¶
A data frame is not a 2-D matrix under the hood — it's a list of
equal-length vectors (one per column) with a class attribute of
"data.frame" and a row.names attribute, plus S3 methods that make it
print and index like a table. df$col is really `[[`(df, "col")
returning that column's underlying vector directly (no copy). df[1, ],
by contrast, goes through the [.data.frame S3 method, which has to
rebuild a whole new (one-row) list from scratch — this is one reason
row-wise operations on data frames are much slower than column-wise ones:
column access is native list indexing, row access requires reconstructing
a mini data frame every time.
Because each column is an independent vector, mixed types across columns
are free (numeric here, character there) but a single column is still a
homogeneous atomic vector — assigning a character into a numeric column
triggers the same coercion rules as any other vector, silently converting
the whole column. str() works by walking this list-of-vectors structure
and reporting each element's class and a peek at its values.
🔀 See this in another language¶
- MATLAB — Plotting & Visualization Basics
- JavaScript — Strings & Template Literals
- Rust — Collections (Vec, String, HashMap)
Exercise¶
Create a data frame products with columns name (character), price
(numeric), and category (character) for 5 made-up products across 2
categories. Print str(products), then filter to only products with
price > 10, then add a new logical column on_sale of your choosing, and
finally use tapply() to print the average price per category.