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



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:

TEXT 📖 Display only
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

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?

100%
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":

(2) Create a data frame

R
# 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 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
R
# 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
R
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)

R
# 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

R
# 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
⚠️ Note: tibble's [, "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

R
# 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

R
# Array Assignment
df$age <- df$age + 1

# Condition Modification
df$score[df$name == "Bob"] <- 90

(3) Delete a column

R
# Assign NULL
df$bonus <- NULL
df$level <- NULL

(4) Add/Delete Rows

R
# 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:

R
# 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

100%
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
R
# 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

R
# 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"
⚠️ Note: In the tidyverse era, it’s recommended to use the forcats package (covered in Lesson 11) to handle factors, as it’s more elegant than base R.

(4) Factors in the data frame

R
# 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
💡 Tip: The new version of R (4.x) uses 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

R
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 |>

R
# |> 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

R
# 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

R
# 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
▶ Try it Yourself

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

R 📖 Display only
# ============================================
# 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)
61 logic lines (exceeds 40-line limit, display only)

Expected Output (Excerpt):

TEXT 📖 Display only
=== 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

Q How do I choose between data.frame and tibble?
A Use 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.
Q What are the advantages of factors?
A Three major advantages: ① They can be compared for magnitude (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).
Q What is the relationship between dplyr and base R?
A dplyr is a modern wrapper for base R. 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.
Q What is the difference between |> and %>%?
A |> 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.
Q What is the difference between mutate and transform?
A 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


📝 Exercises

  1. Basic Exercise: Use data.frame() to create a students dataframe (6 students × 4 columns: name/age/score/passed). Print the dataframe, str(), and summary(), and access all columns in the third row.

  2. Basic Problem: Convert the name column from the previous problem to factor (3 levels), and compare the differences between as.character() and as.numeric(). Verify that the factor supports comparison of magnitudes (>).

  3. Basic Problem: Install and load dplyr, then use filter() to filter students from score > 85 based on the students from the previous problem, and sort them in descending order by score using arrange().

  4. Advanced Exercise: Use tibble to create a sales data frame (10 rows × 4 columns: product/region/amount/date). ① Use mutate() to sum discount = amount * 0.1; ② Use group_by(region) + summarise() to calculate the total sales for each region; ③ Use arrange(desc()) to rank the regions.

  5. 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 mutate to add an annual salary column ⑤ Use case_when (or ifelse) to categorize by salary brackets ⑥ Sort by performance ⑦ Write to a new data frame top_employees and save as a CSV file (write.csv()).

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏