R: R Data Frames and Factors
Last updated: 2026-08-26
In the previous 9 lessons, we learned about vectors, matrices, and lists—each of which has its limitations. Data frames (data.frame) solve all these problems: they’re as easy to read as an Excel spreadsheet and as efficient as a matrix. In this lesson, we’ll learn about the true “star” of R—the data frame.
Data frames and the tidyverse are at the heart of data science in R. Starting with this lesson, we’ll officially dive into the world of “data analysis with R.”
1. What You'll Learn
- Differences Between
data.frame()andtibble() - Creating, Viewing, Accessing, and Modifying Data Frames
- Filter, Select, Arrange
- Factor (factor) categorical variable
- Getting Started with dplyr (The "Five Essentials" of Data Processing)
- Grouped aggregation (group_by + summarise)
- Practical Application: Comprehensive Analysis of Employee Data
2. A Story of Converting Excel to R
(1) Pain Point: Excel Can’t Handle It
Chen's market research spreadsheet has 10,000 rows × 8 columns:
EmployeesID Name Department Salary Years Performance Gender Start Date
2024001 Alice Data Department 15000 3 A M 2021-03-15
2024002 Bob Engineering Department 18000 5 A+ M 2019-08-20
...
Calculating average salaries by "department," sorting by "performance," and identifying high-earning employees... It takes 30 minutes in Excel, with formulas nested to the point of crashing.
(2) Solution using R
library(dplyr)
# 5 Code execution complete 5 An Analysis
df |>
group_by(Department) |>
summarise(Average Wage = mean(Salary), Number of people = n()) |>
arrange(desc(Average Wage)) |>
filter(Average Wage > 15000)
5 lines of code instead of 30 minutes in Excel. That’s the power of DataFrame + dplyr.
3. data.frame: R's "Main Character"
(1) What is a data frame?
graph TB
A["Data Frame data.frame"] --> B["Each column = 1 equal-length vectors"]
A --> C["Each line = 1 records"]
A --> D["Mixed Types(Each column is independent)"]
A --> E["The bottom layer is'A list of vectors of equal length'"]
style A fill:#fff3cd
style B fill:#cce5ff
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#e1d4ff
A data frame is a "list of vectors of equal length":
- Each column is a vector (of the same type)
- All columns are the same length
- Different columns can be of different types (numeric columns + character columns + logical columns)
(2) Create a data frame
# Methods 1:data.frame()(Basics R)
df <- data.frame(
name = c("Alice", "Bob", "Charlie"),
age = c(25, 30, 35),
score = c(95, 88, 92),
passed = c(TRUE, TRUE, TRUE)
)
print(df)
# name age score passed
# 1 Alice 25 95 TRUE
# 2 Bob 30 88 TRUE
# 3 Charlie 35 92 TRUE
# Methods 2:tibble(tidyverse Version,Recommendations)
# install.packages("tibble")
library(tibble)
df2 <- tibble(
name = c("Alice", "Bob", "Charlie"),
age = c(25, 30, 35),
score = c(95, 88, 92)
)
print(df2)
(3) data.frame vs tibble
| Feature | data.frame | tibble |
|---|---|---|
| Print all rows by default (may cause the system to freeze with large datasets) | Print the first 10 rows + column types by default | |
| Column Name | Supports Special Characters | Enforce Validity |
| Subset of a subset | df[, "col"] Returns a vector |
Always returns a tibble |
| Performance | Slower | Faster |
| String | Convert to factor by default | Keep as string |
# View Structure
str(df)
# 'data.frame': 3 obs. of 4 variables:
# $ name : chr "Alice" "Bob" "Charlie"
# $ age : num 25 30 35
# $ score : num 95 88 92
# $ passed: logi TRUE TRUE TRUE
# Summary Statistics
summary(df)
# name age score passed
# Length:3 Min. :25 Min. :88 Mode :logical
# Class :character 1st Qu.:27 Median :92 TRUE:3
# Mode :character Mean :30 Mean :91 NA's :0
# 3rd Qu.:32 3rd Qu.:93
# Max. :35 Max. :95
4. Accessing DataFrames
(1) 4 Ways to Access
| Method | Syntax | Return | Purpose |
|---|---|---|---|
df[i, j] |
Row i, Column j | Scalar/Vector/DataFrame | Row-Column Combination |
df[i, ] |
Row i | DataFrame | Get Row |
df[, j] |
Column j | Vector (base R) / Data Frame | Extract Column |
df$name |
By Column Name | Vector | Get Column |
df[["name"]] |
By Column Name | Vector | Get Column |
df <- data.frame(
name = c("Alice", "Bob", "Charlie"),
age = c(25, 30, 35),
score = c(95, 88, 92)
)
# Retrieve a single element
df[2, 3] # [1] 88
df[2, "score"] # [1] 88
# Select the entire row
df[1, ] # Entire row (Data Frame)
# name age score
# 1 Alice 25 95
# Round an entire column
df[, "age"] # Digital Vectors [1] 25 30 35
df$age # Ibid.
df[["age"]] # Ibid.
(2) Logical Index (Filter)
# Find the Fraction > 90 line
df[df$score > 90, ]
# name age score
# 1 Alice 25 95
# 3 Charlie 35 92
# Multiple conditions
df[df$age > 25 & df$score > 85, ]
(3) ⚠️ Important: Retrieving columns in base R vs. tibble
# base R:Retrieve the column return vector
df[, "age"] # [1] 25 30 35 ← Digital Vectors
# tibble:Retrieve and return the column tibble
library(tibble)
tb <- as_tibble(df)
tb[, "age"] # ← tibble(1 row 1 col)
# Want to extract a vector from tibble: Use [[ ]] or $
tb[["age"]] # [1] 25 30 35 ← Digital Vectors
[, "col"] does not automatically convert to a vector. This is a "safety" design feature—it prevents the system from mistakenly interpreting large data frames as vectors, which could cause the entire printout to freeze.
5. Modifying the Data Frame
(1) Add a column
# Use $ to add
df$city <- c("Beijing", "Shanghai", "Guangzhou")
# Use transform()
df <- transform(df, bonus = score * 100)
# Use within() to reference column names
df <- within(df, level <- ifelse(score >= 90, "A", "B"))
print(df)
# name age score city bonus level
# 1 Alice 25 95 Beijing 9500 A
# 2 Bob 30 88 Shanghai 8800 B
# 3 Charlie 35 92 Guangzhou 9200 A
(2) Modify Columns
# Array Assignment
df$age <- df$age + 1
# Condition Modification
df$score[df$name == "Bob"] <- 90
(3) Delete a column
# Assign NULL
df$bonus <- NULL
df$level <- NULL
(4) Add/Delete Rows
# Add a row: Use rbind
new_row <- data.frame(name = "Diana", age = 28, score = 85, city = "Shenzhen")
df <- rbind(df, new_row)
# Delete Row:Using Negative Indexes
df <- df[-1, ] # Delete 1st row
6. Factor: Categorical variable
(1) Why are factors needed?
In R, using factors to represent categorical variables (such as "low/medium/high") is more powerful than using character vectors:
# Character vector(Disorder)
gender_char <- c("M", "F", "M", "F", "M")
# factor(Orderly + Finite values)
gender_factor <- factor(gender_char, levels = c("M", "F"))
gender_factor
# [1] M F M F M
# Levels: M F
(2) The Advantages of Factors
graph TB
A["Character vector 'Low' 'Mid' 'High'"] --> B["factor factor<br/>+ levels Order<br/>+ Finite values<br/>+ Save memory"]
A --> C["Disadvantages: Cannot sort and compare"]
B --> D["Advantages: Can be compared for size<br/>Can be grouped in order<br/>Statistics/Knowing the Number of Categories in Modeling"]
style A fill:#f8d7da
style B fill:#d4edda
style D fill:#cce5ff
# Compare Sizes(Characters cannot,Factors can)
sizes <- factor(c("S", "L", "Mid", "L", "S"),
levels = c("S", "Mid", "L"),
ordered = TRUE)
sizes[1] < sizes[2] # TRUE <- Characters can't do that!
(3) Common Operations on Factors
# View levels
levels(sizes)
# [1] "S" "Mid" "L"
# Frequency Count
table(sizes)
# sizes
# S Mid L
# 2 1 2
# Reorder
sizes <- factor(sizes, levels = c("L", "Mid", "S")) # Reverse the order
levels(sizes)
# [1] "L" "Mid" "S"
forcats package (covered in Lesson 11) to handle factors, as it’s more elegant than base R.
(4) Factors in the data frame
# data.frame() Convert character strings to factors by default (This is a trap!)
df <- data.frame(
name = c("Alice", "Bob"), # Character vector
stringsAsFactors = FALSE # Disable Auto-Conversion(Recommendations)
)
str(df)
# $ name: chr "Alice" "Bob" ← Preserve characters
stringsAsFactors = FALSE by default, but older code may use a different value. It is recommended to explicitly write stringsAsFactors = FALSE.
7. Getting Started with dplyr: The Five Essentials of Data Processing
dplyr is at the heart of the tidyverse; five functions cover 80% of data processing scenarios:
(1) Installation and Loading
install.packages("dplyr")
library(dplyr)
(2) Five-Part Function
| Function | Purpose | SQL Equivalent |
|---|---|---|
filter() |
Filter rows | WHERE |
select() |
Select Column | SELECT |
mutate() |
Add/Modify Column | — |
arrange() |
Sort | ORDER BY |
summarise() |
Aggregate Statistics | GROUP BY |
(3) The chained operator |>
# |> is R 4.1+'s "pipe symbol", Pass the result from the previous step to the next step
df |> filter(age > 25) |> select(name, age)
(4) Hands-On: Processing Employee Data
# Prepare the data
employees <- tibble(
id = 2024001:2024010,
name = c("Alice", "Bob", "Charlie", "Diana", "Eve",
"Frank", "Grace", "Henry", "Ivy", "Jack"),
department = c("Data Department", "Engineering Department", "Data Department", "Product Department", "Engineering Department",
"Data Department", "Engineering Department", "Product Department", "Data Department", "Engineering Department"),
salary = c(15000, 18000, 16000, 20000, 17000,
15500, 19000, 21000, 16500, 17500),
years = c(3, 5, 4, 7, 4, 2, 6, 8, 3, 5),
performance = factor(
c("A", "A+", "B", "A+", "A", "B", "A", "A+", "B", "A"),
levels = c("B", "A", "A+"),
ordered = TRUE
)
)
# 1. filter:Screening Data Department Employees
data_dept <- employees |> filter(department == "Data Department")
# 2. select:Select a subset of columns
basic_info <- employees |> select(name, department, salary)
# 3. mutate:Add an "Annual Salary" column
employees <- employees |> mutate(annual_salary = salary * 12)
# 4. arrange:Sort by salary in descending order
ranked <- employees |> arrange(desc(salary))
# 5. summarise:By Department
dept_stats <- employees |>
group_by(department) |>
summarise(
Number of people = n(),
Average Wage = mean(salary),
Highest Salary = max(salary),
Total Annual Salary = sum(annual_salary)
) |>
arrange(desc(Average Wage))
print(dept_stats)
# # A tibble: 3 × 5
# department Number of people Average Wage Highest Salary Total Annual Salary
# <chr> <int> <dbl> <dbl> <dbl>
# 1 Product Department 2 20500 21000 492000
# 2 Engineering Department 4 17875 19000 858000
# 3 Data Department 4 15750 16500 756000
(5) Example of a 5-Step Combination
# Comprehensive Example:Identify High-Performing, High-Earning Employees
top_talent <- employees |>
filter(performance %in% c("A+", "A")) |> # Performance Screening A+ or A
filter(salary > 16000) |> # ② Filter by High Salary
mutate(salary_level = ifelse(salary > 18000, "High", "Upper-middle")) |> # ③ Add Category
arrange(desc(salary)) |> # ④ Sort
select(name, department, salary, performance, salary_level) # ⑤ Select Column
print(top_talent)
# # A tibble: 3 × 5
# name department salary performance salary_level
# <chr> <chr> <dbl> <ord> <chr>
# 1 Henry Product Department 21000 A+ High
# 2 Diana Product Department 20000 A+ High
# 3 Bob Engineering Department 18000 A+ Upper-middle
8. Complete Example: Comprehensive Analysis of Employee Data
Below is an example of a complete workflow that ties together all the concepts covered in this lesson.
▶ Example: Comprehensive Analysis of Company Employee Data
# ============================================
# Comprehensive Analysis of Company Employee Data
# Features:Using a DataFrame + dplyr Comprehensive Analysis 10 employees
# ============================================
library(dplyr)
# 1. Prepare data
employees <- tibble(
id = 2024001:2024010,
name = c("Alice", "Bob", "Charlie", "Diana", "Eve",
"Frank", "Grace", "Henry", "Ivy", "Jack"),
department = c("Data Department", "Engineering Department", "Data Department", "Product Department", "Engineering Department",
"Data Department", "Engineering Department", "Product Department", "Data Department", "Engineering Department"),
salary = c(15000, 18000, 16000, 20000, 17000,
15500, 19000, 21000, 16500, 17500),
years = c(3, 5, 4, 7, 4, 2, 6, 8, 3, 5),
performance = factor(
c("A", "A+", "B", "A+", "A", "B", "A", "A+", "B", "A"),
levels = c("B", "A", "A+"),
ordered = TRUE
)
)
cat("=== Raw Data ===\n")
print(employees)
# 2. View Structure
cat("\n=== Data Structures ===\n")
str(employees)
# 3. Summary Statistics
cat("\n=== Abstract ===\n")
summary(employees)
# 4. By Department
cat("\n=== By Department ===\n")
dept_summary <- employees |>
group_by(department) |>
summarise(
Number of people = n(),
Average Wage = mean(salary),
Median Wage = median(salary),
Highest Salary = max(salary),
Minimum Wage = min(salary),
Average Length of Service = round(mean(years), 1)
) |>
arrange(desc(Average Wage))
print(dept_summary)
# 5. Identify High-Performing Employees (A+ or A)
cat("\n=== Employees with Outstanding Performance ===\n")
top_perf <- employees |>
filter(performance %in% c("A+", "A")) |>
arrange(desc(salary)) |>
select(name, department, salary, performance, years)
print(top_perf)
# 6. Add"Pay Grade"col
cat("\n=== Salary Grade Classification ===\n")
employees <- employees |>
mutate(
salary_level = factor(
ifelse(salary >= 18000, "High",
ifelse(salary >= 16000, "Mid", "Low")),
levels = c("Low", "Mid", "High"),
ordered = TRUE
),
annual_salary = salary * 12
)
# By Pay Grade
level_summary <- employees |>
group_by(salary_level) |>
summarise(Number of people = n(), Average Wage = mean(salary))
print(level_summary)
# 7. Identify the Best Employees (High Salary + High Performance + Senior)
cat("\n=== Top Employee (High Salary + Outstanding Performance + Senior) ===\n")
top_talent <- employees |>
filter(salary >= 17000, performance == "A+", years >= 5) |>
select(name, department, salary, years, performance, annual_salary)
print(top_talent)
Expected Output (Excerpt):
=== By Department ===
# A tibble: 3 × 7
department Number of people Average Wage Median Wage Highest Salary Minimum Wage Average Length of Service
<chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Product Department 2 20500 20500 21000 20000 7.5
2 Engineering Department 4 17875 17750 19000 17000 4.5
3 Data Department 4 15750 15750 16500 15000 3
=== Employees with Outstanding Performance ===
# A tibble: 6 × 5
name department salary performance years
<chr> <chr> <dbl> <ord> <dbl>
1 Henry Product Department 21000 A+ 8
2 Diana Product Department 20000 A+ 7
3 Bob Engineering Department 18000 A+ 5
4 Grace Engineering Department 19000 A 6
5 Eve Engineering Department 17000 A 4
6 Jack Engineering Department 17500 A 5
❓ FAQ
data.frame and tibble?tibble for new projects—it’s more print-friendly, performs better, and has stricter data types. When you’re learning, start with data.frame (no need to load a package), and switch to tibble once you’re familiar with it.ordered = TRUE) ② They save memory (only store integer encodings) ③ They allow for statistical analysis and modeling when the number of categories is known (avoiding the "unknown value" problem).filter(df, ...) replaces df[...], and select(df, ...) replaces df[, ...]. dplyr is more readable and consistent, and it adheres to the tidyverse standard for data science.|> and %>%?|> is the native pipe symbol in R 4.1+ (no package loading required), while %>% is the pipe symbol from the magrittr package (the old tidyverse). New code uses |>, but %>% is still commonly seen in older code and tutorials.mutate and transform?mutate() can reference a column that was just created (mutate(df, y = x * 2, z = y + 1)), while transform() cannot. This is why mutate is more popular in the tidyverse.📖 Summary
- A data frame is a "list of vectors of equal length," where each column has a distinct data type and each row contains a data record.
data.frame()R Basics,tibbleThe Modern tidyverse (Recommended for new projects)- 4 accesses:
df[i, j]df$namedf[["name"]]df[, j] - A factor is an "ordered categorical variable"; levels control the order, saving memory
- dplyr five-piece set:
filterselectmutatearrangesummarise group_by() | summarise()Perform group aggregation;|>pipe symbol to chain multiple operationsmutatereferences the column that was just created (whichtransformcannot do), which is whymutateis more popular in the tidyverse
📝 Exercises
-
Basic Exercise: Use
data.frame()to create astudentsdataframe (6 students × 4 columns: name/age/score/passed). Print the dataframe,str(), andsummary(), and access all columns in the third row. -
Basic Problem: Convert the
namecolumn from the previous problem tofactor(3 levels), and compare the differences betweenas.character()andas.numeric(). Verify that the factor supports comparison of magnitudes (>). -
Basic Problem: Install and load
dplyr, then usefilter()to filter students fromscore > 85based on thestudentsfrom the previous problem, and sort them in descending order by score usingarrange(). -
Advanced Exercise: Use
tibbleto create asalesdata frame (10 rows × 4 columns: product/region/amount/date). ① Usemutate()to sumdiscount = amount * 0.1; ② Usegroup_by(region) + summarise()to calculate the total sales for each region; ③ Usearrange(desc())to rank the regions. -
Challenge: Using simulated employee data (10 employees, 3 departments, salary/years of service/performance), complete the 7-step analysis: ① Load dplyr ② Calculate the number of employees and average salary by department ③ Identify high-salary employees with ≥ 5 years of service ④ Use
mutateto add an annual salary column ⑤ Usecase_when(orifelse) to categorize by salary brackets ⑥ Sort by performance ⑦ Write to a new data frametop_employeesand save as a CSV file (write.csv()).