数据挖掘与R语言

第2讲:数据类型与数据对象

2026年03月18日

上讲回顾

  • 数据挖掘:用计算机从大量数据中自动发现规律
  • 核心警示:相关 ≠ 因果(谷歌流感的教训)
  • 工具链:先装 R(引擎),再装 RStudio(驾驶舱)
  • 数据读写read.csv() / write.csv()read_excel() / write_xlsx()

本讲内容

  • Part 1:数据类型 ——R 认识哪几种"数据"?(约15分钟)
  • Part 2:向量 ——最基础的数据容器,深挖其用法(约25分钟)
  • Part 3:矩阵 ——向量的二维升级(约10分钟)
  • Part 4:数据框 ——数据分析的主战场(约25分钟)
  • Part 5:列表与因子 ——灵活容器与分类变量(约15分钟)

提示

本讲代码量大,我们需要建一个 Quarto 文件,便于课后反复练习。

Part 1 数据类型

R 认识哪几种"数据"?

1.1 为什么要区分数据类型?

先看一个让初学者困惑的例子:

x <- "3"
y <- 3

# x + y   # 这行会报错!

错误信息是:Error in x + y : non-numeric argument to binary operator

意思是:你在用"加号"操作一个不是数字的东西。

x 看起来像数字,但它是用引号包起来的——对 R 来说,引号里的东西是文字,不是数字,就像 "苹果" 一样,当然不能做加法。

重要

数据类型决定了数据能做什么操作。数字可以加减乘除,文字可以拼接搜索,逻辑值可以做真假判断。搞清楚类型,才能写出正确的代码。

1.2 六种基本数据类型

R 有六种基本类型,日常最常用的是前三种:

类型 英文名 示例 用途
数值 numeric / double 3.14100 计算、统计
整数 integer 1L100L 整数计算(加 L 标记)
字符 character "张三""hello" 文本、标签
逻辑 logical TRUEFALSE 条件判断、筛选
复数 complex 3+2i 数学(很少用到)
原始 raw 底层字节(几乎用不到)

注记

日常数据分析中,95% 以上的情况只涉及数值字符逻辑三种类型。掌握这三种就够用了。

1.3 用 class()typeof() 查看类型

class(3.14)       # 数值
[1] "numeric"
class("你好")     # 字符
[1] "character"
class(TRUE)       # 逻辑
[1] "logical"
class(1L)         # 整数
[1] "integer"
# typeof() 更底层,class() 更常用
typeof(3.14)   # "double"(双精度浮点数)
[1] "double"
typeof(1L)     # "integer"
[1] "integer"
typeof(TRUE)   # "logical"
[1] "logical"
typeof("abc")  # "character"
[1] "character"

提示

记住 class() 就够了,它返回的是你平时说的那种类型名称。typeof() 是更底层的存储类型,一般不需要用。

1.4 数值型

# 整数和小数在 R 里都叫 numeric(数值型)
a <- 42
b <- 3.14
c <- -100.5

class(a)   # numeric
[1] "numeric"
class(b)   # numeric
[1] "numeric"
# 科学计数法也是数值型
大数 <- 1.5e6    # 1.5 × 10^6 = 1500000
小数 <- 2.3e-4   # 2.3 × 10^-4 = 0.00023

大数
[1] 1500000
小数
[1] 0.00023
# 三个特殊数值
Inf        # 无穷大(如 1/0 的结果)
[1] Inf
-Inf       # 负无穷大
[1] -Inf
NaN        # 不是数字(Not a Number),如 0/0
[1] NaN

1.5 字符型

字符型就是我们常说的"文本"或"字符串",必须用引号包起来:

姓名 <- "张三"
城市 <- "成都"
课程 <- "数据挖掘与R语言"

# 用 paste() 拼接字符串
paste(姓名, "来自", 城市)
[1] "张三 来自 成都"
paste0(姓名, "来自", 城市)
[1] "张三来自成都"
paste0(姓名, "——", 课程)  # paste0 默认不加空格
[1] "张三——数据挖掘与R语言"
# 字符串操作常用函数
nchar("数据挖掘")                  # 字符数量(4个汉字)
[1] 4
toupper("hello world")             # 转大写
[1] "HELLO WORLD"
tolower("HELLO WORLD")             # 转小写
[1] "hello world"
substr("数据挖掘与R语言", 1, 4)    # 截取第1到第4个字符
[1] "数据挖掘"

警告

"3"3 是完全不同的东西!class("3")"character",不能直接参与数学运算。用 as.numeric("3") 可以转换。

1.6 逻辑型

逻辑型只有两个值:TRUE(真)和 FALSE(假):

a <- TRUE
b <- FALSE

class(a)
[1] "logical"
!a          # 取反:TRUE 变 FALSE
[1] FALSE
a & b       # 与:两个都为 TRUE 才是 TRUE
[1] FALSE
a | b       # 或:至少一个 TRUE 就是 TRUE
[1] TRUE
# 比较运算会产生逻辑值
5 > 3             # TRUE
[1] TRUE
10 == 10          # TRUE(注意是双等号 ==)
[1] TRUE
"abc" != "ABC"    # TRUE(不等于)
[1] TRUE
# 逻辑值可以做数学运算!TRUE = 1,FALSE = 0
sum(c(TRUE, FALSE, TRUE, TRUE))    # 统计 TRUE 的个数
[1] 3
mean(c(TRUE, FALSE, TRUE, TRUE))   # TRUE 的比例
[1] 0.75

1.7 类型转换:as.*() 函数族

当类型不对时,用 as.*() 函数转换:

# 字符转数值
as.numeric("3.14")
[1] 3.14
as.integer("42")
[1] 42
# 数值转字符
as.character(100)
[1] "100"
as.character(3.14)
[1] "3.14"
# 数值转逻辑:0 是 FALSE,其他都是 TRUE
as.logical(0)
[1] FALSE
as.logical(1)
[1] TRUE
as.logical(-99)
[1] TRUE
# 逻辑转数值:TRUE = 1,FALSE = 0
as.numeric(TRUE)
[1] 1
as.numeric(FALSE)
[1] 0
# 无法转换时会产生 NA(缺失值),并给出警告
as.numeric("你好")   # "你好" 不是数字,返回 NA
[1] NA

1.8 小测验:你能猜出结果吗?

在运行之前,先猜猜每行的结果:

class(TRUE)
class(1L)
class(1.0)
as.numeric("100")
as.character(TRUE)
TRUE + TRUE + FALSE
sum(c(80, 90, 75) >= 80)
class(TRUE)
[1] "logical"
class(1L)
[1] "integer"
class(1.0)
[1] "numeric"
as.numeric("100")
[1] 100
as.character(TRUE)
[1] "TRUE"
TRUE + TRUE + FALSE
[1] 2
sum(c(80, 90, 75) >= 80)
[1] 2

Part 2 向量

最基础的数据容器,深挖其用法

2.1 向量是 R 的"细胞"

重要

向量(vector)是 R 中最基础的数据结构。

R 里几乎所有数据都是向量——包括你认为的"单个数字",实际上是长度为 1 的向量。理解向量,就理解了 R 的核心逻辑。

# c() 函数:combine,把多个值合并成向量
成绩 <- c(85, 92, 78, 96, 88, 73, 91)
姓名 <- c("张三", "李四", "王五", "赵六", "陈七", "吴八", "周九")
是否优秀 <- c(FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, TRUE)

length(成绩)   # 向量长度(元素个数)
[1] 7
class(成绩)    # 向量的类型
[1] "numeric"

注记

向量有一个重要约束:所有元素必须是同一种类型。如果混合了不同类型,R 会自动把它们统一成一种类型(称为"类型强制转换")。

2.2 创建向量的三种方式

# 方式一:c() 手动输入(最灵活)
x <- c(10, 20, 30, 40, 50)

# 方式二:seq() 生成等差数列
seq(1, 10, by = 2)          # 1到10,步长为2
[1] 1 3 5 7 9
seq(0, 1, length.out = 6)   # 0到1,生成6个等间距的数
[1] 0.0 0.2 0.4 0.6 0.8 1.0
# 方式三:: 冒号,生成连续整数(最简洁)
1:10       # 1到10
 [1]  1  2  3  4  5  6  7  8  9 10
10:1       # 倒序
 [1] 10  9  8  7  6  5  4  3  2  1
5:15
 [1]  5  6  7  8  9 10 11 12 13 14 15
# rep() 重复
rep(0, times = 5)           # 重复0,5次
[1] 0 0 0 0 0
rep(c(1, 2, 3), times = 3)  # 重复整个向量
[1] 1 2 3 1 2 3 1 2 3
rep(c("A", "B"), each = 3)  # 每个元素重复3次
[1] "A" "A" "A" "B" "B" "B"

2.3 类型强制转换:混合类型时发生什么?

向量要求所有元素同类型。如果你混合了类型,R 会悄悄帮你转换:

# 数值 + 字符 → 全部变字符(字符"胜")
c(1, 2, "三")
[1] "1"  "2"  "三"
# 逻辑 + 数值 → 全部变数值(TRUE=1,FALSE=0)
c(TRUE, FALSE, 3, 4)
[1] 1 0 3 4
# 逻辑 + 字符 → 全部变字符
c(TRUE, "hello")
[1] "TRUE"  "hello"

强制转换的优先级

字符 > 数值 > 逻辑

也就是说,只要混入了字符,全都变字符;混入了数值(没有字符),逻辑值就变成 0 和 1。

这是 R 的"自救机制",但它可能悄悄引入 bug——所以要养成查看数据类型的习惯。

2.4 向量的命名

可以给向量的每个元素起个名字,方便按名字访问:

# 方式一:创建时命名
成绩单 <- c(张三 = 85, 李四 = 92, 王五 = 78, 赵六 = 96)
成绩单
张三 李四 王五 赵六 
  85   92   78   96 
# 方式二:用 names() 事后命名
分数 <- c(85, 92, 78, 96)
names(分数) <- c("张三", "李四", "王五", "赵六")
分数
张三 李四 王五 赵六 
  85   92   78   96 
# 按名字访问
成绩单["李四"]
李四 
  92 
成绩单[c("张三", "赵六")]
张三 赵六 
  85   96 
# 查看所有名字
names(成绩单)
[1] "张三" "李四" "王五" "赵六"

2.5 向量索引:用 [ ] 取元素

取向量中的元素,用方括号 [ ] 加上位置编号(R 从 1 开始计数,不是 0):

x <- c(10, 20, 30, 40, 50)

x[1]         # 第1个元素
[1] 10
x[3]         # 第3个元素
[1] 30
x[c(1, 3)]   # 第1和第3个元素
[1] 10 30
x[2:4]       # 第2到第4个元素
[1] 20 30 40
x[-1]        # 除了第1个以外的所有元素(负号 = 排除)
[1] 20 30 40 50
x[-c(1, 5)]  # 排除第1和第5个
[1] 20 30 40

注记

R 的下标从 1 开始,这和 Python、JavaScript 等语言(从 0 开始)不同。如果你以前学过其他语言,要注意这个区别。

2.6 逻辑索引:用条件筛选元素

这是 R 中最常用、最强大的取数方式——用条件表达式筛选满足要求的元素:

成绩 <- c(85, 92, 78, 96, 88, 73, 91)
姓名 <- c("张三", "李四", "王五", "赵六", "陈七", "吴八", "周九")

# 第一步:生成逻辑向量
成绩 >= 90   # 运行前想一想,这个代码会产生什么样的结果?
[1] FALSE  TRUE FALSE  TRUE FALSE FALSE  TRUE
# 第二步:用逻辑向量筛选
成绩[成绩 >= 90]     # 筛选出90分以上的成绩
[1] 92 96 91
姓名[成绩 >= 90]     # 找到对应的名字
[1] "李四" "赵六" "周九"
# 多条件组合
成绩[成绩 >= 80 & 成绩 < 90]    # 80分(含)到90分(不含)之间
[1] 85 88
姓名[成绩 >= 80 & 成绩 < 90]
[1] "张三" "陈七"

提示

逻辑索引是数据筛选的核心思路——先"打标记"(生成 TRUE/FALSE),再"按标记取值"。这个思路在数据框操作中同样适用。

2.7 修改向量中的元素

[ ] 不只能取值,还能赋值:

x <- c(10, 20, 30, 40, 50)

# 修改单个元素
x[3] <- 999
x
[1]  10  20 999  40  50
# 修改多个元素
x[c(1, 5)] <- 0
x
[1]   0  20 999  40   0
# 按条件修改(非常实用!)
成绩 <- c(85, 92, 78, 96, 88, 73, 91)
成绩[成绩 < 80] <- 80  # 所有低于80分的都改成80(设置下限)
成绩
[1] 85 92 80 96 88 80 91
# 追加元素
y <- c(1, 2, 3)
y <- c(y, 4, 5)   # 在末尾添加
y
[1] 1 2 3 4 5

2.8 向量化运算:R 的高效秘诀

向量化运算的意思是:对向量中每个元素自动逐一操作,无需写循环

x <- c(1, 2, 3, 4, 5)

x * 2      # 每个元素乘以2
[1]  2  4  6  8 10
x^2        # 每个元素求平方
[1]  1  4  9 16 25
sqrt(x)    # 每个元素求平方根
[1] 1.00 1.41 1.73 2.00 2.24
x + 10     # 每个元素加10
[1] 11 12 13 14 15
# 两个等长向量之间的运算:对应位置逐一计算
a <- c(10, 20, 30)
b <- c(3, 4, 5)

a + b
[1] 13 24 35
a * b
[1]  30  80 150
a / b
[1] 3.33 5.00 6.00

提示

如果你以前学过 Python 或 Java,你会习惯用 for 循环处理一组数据。在 R 里,向量化运算让你不需要写循环就能完成同样的任务,而且速度更快、代码更简洁。

2.9 向量的常用统计函数

成绩 <- c(85, 92, 78, 96, 88, 73, 91, NA)  # 注意:有一个缺失值 NA

sum(成绩, na.rm = TRUE)     # 求和(na.rm = TRUE 表示忽略缺失值)
[1] 603
sum(成绩)                   # 如果不忽略NA呢?
[1] NA
mean(成绩, na.rm = TRUE)    # 平均值
[1] 86.1
median(成绩, na.rm = TRUE)  # 中位数
[1] 88
sd(成绩, na.rm = TRUE)      # 标准差
[1] 8.15
var(成绩, na.rm = TRUE)     # 方差
[1] 66.5
max(成绩, na.rm = TRUE)    # 最大值
[1] 96
min(成绩, na.rm = TRUE)    # 最小值
[1] 73
range(成绩, na.rm = TRUE)  # 范围(返回最小值和最大值)
[1] 73 96
which.max(成绩)            # 最大值的位置
[1] 4
which.min(成绩)            # 最小值的位置
[1] 6
# 处理缺失值:is.na() 检测,na.omit() 删除
is.na(成绩)      # 哪些位置是 NA?
[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE
na.omit(成绩)    # 删掉 NA,返回干净的向量
[1] 85 92 78 96 88 73 91
attr(,"na.action")
[1] 8
attr(,"class")
[1] "omit"

2.10 实战练习:向量综合操作

用一个完整的例子把向量操作串起来:

# 某班10位同学的三次作业成绩(满分100)
姓名 <- c("A同学","B同学","C同学","D同学","E同学",
           "F同学","G同学","H同学","I同学","J同学")
作业1 <- c(88, 72, 95, 61, 83, 90, 77, 68, 92, 85)
作业2 <- c(91, 68, 87, 75, 80, 95, 82, 71, 88, 90)
作业3 <- c(85, 80, 92, 70, 88, 88, 79, 65, 95, 83)

# 计算每人的平均分
均分 <- (作业1 + 作业2 + 作业3) / 3
round(均分, 1)    # 保留1位小数
 [1] 88.0 73.3 91.3 68.7 83.7 91.0 79.3 68.0 91.7 86.0
# 找出三次作业平均分最高的同学
姓名[which.max(均分)]
[1] "I同学"
# 找出平均分低于75的同学及其分数
低分同学 <- 姓名[均分 < 75]
低分分数 <- round(均分[均分 < 75], 1)
paste(低分同学, ":", 低分分数, "分")
[1] "B同学 : 73.3 分" "D同学 : 68.7 分" "H同学 : 68 分"  

Part 3 矩阵

向量的二维升级

3.1 什么是矩阵?

矩阵是二维的数据结构——有行,也有列:

# matrix() 创建矩阵
# byrow = TRUE:按行填充(默认是按列)
m <- matrix(1:12, nrow = 3, ncol = 4, byrow = TRUE)
m
     [,1] [,2] [,3] [,4]
[1,]    1    2    3    4
[2,]    5    6    7    8
[3,]    9   10   11   12
dim(m)      # 维度:3行4列
[1] 3 4
nrow(m)     # 行数
[1] 3
ncol(m)     # 列数
[1] 4
class(m)    # 类型是 matrix
[1] "matrix" "array" 

注记

矩阵和向量一样,所有元素必须是同一种类型。矩阵本质上是"有形状的向量"——把向量按行列排列就得到矩阵。

3.2 矩阵的行列命名与索引

# 给行和列命名
成绩矩阵 <- matrix(
  c(85, 92, 78,
    96, 88, 73,
    91, 84, 79),
  nrow = 3, byrow = TRUE,
  dimnames = list(
    c("张三", "李四", "王五"),   # 行名
    c("语文", "数学", "英语")    # 列名
  )
)
成绩矩阵
     语文 数学 英语
张三   85   92   78
李四   96   88   73
王五   91   84   79
# 用 [行, 列] 索引
成绩矩阵[1, 2]        # 第1行第2列
[1] 92
成绩矩阵["张三", ]    # 张三的所有成绩(整行)
语文 数学 英语 
  85   92   78 
成绩矩阵[, "数学"]    # 所有人的数学成绩(整列)
张三 李四 王五 
  92   88   84 

3.3 矩阵运算

# 逐元素运算
成绩矩阵 * 2        # 每个元素乘以2(加分!)
     语文 数学 英语
张三  170  184  156
李四  192  176  146
王五  182  168  158
成绩矩阵 + 5        # 每个元素加5
     语文 数学 英语
张三   90   97   83
李四  101   93   78
王五   96   89   84
# 按行或列汇总
rowSums(成绩矩阵)    # 每位同学的总分
张三 李四 王五 
 255  257  254 
colMeans(成绩矩阵)   # 每门课的平均分
语文 数学 英语 
90.7 88.0 76.7 
# apply() 函数:对每行(MARGIN=1)或每列(MARGIN=2)应用函数
apply(成绩矩阵, MARGIN = 1, mean)   # 每人平均分(按行)
张三 李四 王五 
85.0 85.7 84.7 
apply(成绩矩阵, MARGIN = 2, max)    # 每科最高分(按列)
语文 数学 英语 
  96   92   79 

注记

在数据分析实践中,矩阵主要用于数学计算(线性代数、机器学习算法);数据框才是日常数据分析的主力。矩阵了解即可,下一 Part 才是重头戏。

Part 4 数据框

数据分析的主战场

4.1 为什么需要数据框?

现实中的数据长这样——每一列是不同类型的变量:

姓名(字符) 年龄(数值) 成绩(数值) 是否通过(逻辑)
张三 20 85 TRUE
李四 21 92 TRUE
王五 20 55 FALSE

矩阵要求所有元素同类型,放不下这种"混合"数据。数据框(data.frame) 专门解决这个问题:

  • 每一是一个向量(同类型)
  • 不同可以是不同类型
  • 每一代表一个观测(一条记录)

4.2 创建数据框

# data.frame() 创建数据框
学生 <- data.frame(
  姓名   = c("张三", "李四", "王五", "赵六"),
  年龄   = c(20, 21, 20, 22),
  成绩   = c(85, 92, 55, 78),
  是否通过 = c(TRUE, TRUE, FALSE, TRUE)
)

学生
  姓名 年龄 成绩 是否通过
1 张三   20   85     TRUE
2 李四   21   92     TRUE
3 王五   20   55    FALSE
4 赵六   22   78     TRUE
nrow(学生)     # 行数(观测数)
[1] 4
ncol(学生)     # 列数(变量数)
[1] 4
dim(学生)      # 维度
[1] 4 4
names(学生)    # 列名(变量名)
[1] "姓名"     "年龄"     "成绩"     "是否通过"

4.3 str()summary():快速了解数据

拿到任何一份新数据,第一步先用这两个函数"扫描"一遍:

# str():结构概览(每列的类型和前几个值)
str(学生)
'data.frame':   4 obs. of  4 variables:
 $ 姓名    : chr  "张三" "李四" "王五" "赵六"
 $ 年龄    : num  20 21 20 22
 $ 成绩    : num  85 92 55 78
 $ 是否通过: logi  TRUE TRUE FALSE TRUE
# summary():各列的统计摘要
summary(学生)
     姓名                年龄           成绩       是否通过      
 Length:4           Min.   :20.0   Min.   :55.0   Mode :logical  
 Class :character   1st Qu.:20.0   1st Qu.:72.2   FALSE:1        
 Mode  :character   Median :20.5   Median :81.5   TRUE :3        
                    Mean   :20.8   Mean   :77.5                  
                    3rd Qu.:21.2   3rd Qu.:86.8                  
                    Max.   :22.0   Max.   :92.0                  

提示

建议养成习惯:每次加载新数据,先跑 str()summary(),就像医生给病人做的"体检"——先了解整体情况,再做详细分析。

4.4 访问数据框的列

有三种方式,用哪种都行,根据场合和习惯选择:

# 方式一:$ 符号(最常用,支持自动补全)
学生$成绩
[1] 85 92 55 78
学生$姓名
[1] "张三" "李四" "王五" "赵六"
# 方式二:[[ ]] 双方括号(变量名是字符串时很有用)
学生[["年龄"]]
[1] 20 21 20 22
# 方式三:[ , ] 行列索引
学生[, "成绩"]    # 第"成绩"列(注意逗号!)
[1] 85 92 55 78
学生[, 3]         # 第3列
[1] 85 92 55 78

注记

学生$成绩学生[["成绩"]] 都返回向量学生[, "成绩"] 通常也返回向量,但在某些情况下行为不同。推荐用 $[[]],更明确、更安全。

4.5 访问数据框的行

学生[1, ]    # 第1行(张三的记录)
  姓名 年龄 成绩 是否通过
1 张三   20   85     TRUE
学生[2, ]    # 第2行
  姓名 年龄 成绩 是否通过
2 李四   21   92     TRUE
学生[1:2, ]  # 第1到第2行
  姓名 年龄 成绩 是否通过
1 张三   20   85     TRUE
2 李四   21   92     TRUE
# 同时指定行和列
学生[1, "成绩"]                      # 张三的成绩
[1] 85
学生[c(1, 3), c("姓名", "成绩")]     # 第1、3行的姓名和成绩
  姓名 成绩
1 张三   85
3 王五   55

提示

记住口诀:[ 行, 列 ]——逗号前是行,逗号后是列。留空表示"全部":学生[1, ] 是第1行所有列,学生[, 1] 是所有行第1列。

4.6 用逻辑条件筛选行(重点!)

这是数据分析中最核心的操作之一——按条件筛选记录:

# 筛选成绩 >= 80 的学生
学生[学生$成绩 >= 80, ]
  姓名 年龄 成绩 是否通过
1 张三   20   85     TRUE
2 李四   21   92     TRUE
# 多条件组合:成绩>=80 且 已通过
学生[学生$成绩 >= 80 & 学生$是否通过 == TRUE, ]
  姓名 年龄 成绩 是否通过
1 张三   20   85     TRUE
2 李四   21   92     TRUE
# 筛选特定人名
学生[学生$姓名 %in% c("张三", "王五"), ]
  姓名 年龄 成绩 是否通过
1 张三   20   85     TRUE
3 王五   20   55    FALSE
# subset() 函数:更简洁的写法(不用反复写数据框名)
subset(学生, 成绩 >= 80)
  姓名 年龄 成绩 是否通过
1 张三   20   85     TRUE
2 李四   21   92     TRUE
subset(学生, 年龄 == 20, select = c(姓名, 成绩))
  姓名 成绩
1 张三   85
3 王五   55

4.7 添加新列

# 用 $ 直接新建列
学生$等级 <- ifelse(学生$成绩 >= 90, "优秀",
              ifelse(学生$成绩 >= 80, "良好",
              ifelse(学生$成绩 >= 60, "及格", "不及格")))
学生
  姓名 年龄 成绩 是否通过   等级
1 张三   20   85     TRUE   良好
2 李四   21   92     TRUE   优秀
3 王五   20   55    FALSE 不及格
4 赵六   22   78     TRUE   及格
# 基于现有列计算新列
学生$偏差 <- 学生$成绩 - mean(学生$成绩)  # 与均值的差距
round(学生$偏差, 1)
[1]   7.5  14.5 -22.5   0.5

提示

ifelse(条件, 值_如果TRUE, 值_如果FALSE) 是向量化的条件判断函数,类似 Excel 的 IF 函数,可以嵌套使用。

4.8 实战:用 penguins 数据框做分析

library(palmerpenguins)

# 先做体检
str(penguins)
tibble [344 × 8] (S3: tbl_df/tbl/data.frame)
 $ species          : Factor w/ 3 levels "Adelie","Chinstrap",..: 1 1 1 1 1 1 1 1 1 1 ...
 $ island           : Factor w/ 3 levels "Biscoe","Dream",..: 3 3 3 3 3 3 3 3 3 3 ...
 $ bill_length_mm   : num [1:344] 39.1 39.5 40.3 NA 36.7 39.3 38.9 39.2 34.1 42 ...
 $ bill_depth_mm    : num [1:344] 18.7 17.4 18 NA 19.3 20.6 17.8 19.6 18.1 20.2 ...
 $ flipper_length_mm: int [1:344] 181 186 195 NA 193 190 181 195 193 190 ...
 $ body_mass_g      : int [1:344] 3750 3800 3250 NA 3450 3650 3625 4675 3475 4250 ...
 $ sex              : Factor w/ 2 levels "female","male": 2 1 1 NA 1 2 1 2 NA NA ...
 $ year             : int [1:344] 2007 2007 2007 2007 2007 2007 2007 2007 2007 2007 ...

4.9 penguins 数据框:筛选与统计

# 只看 Chinstrap 企鹅
chinstrap <- subset(penguins, species == "Chinstrap")
nrow(chinstrap)
[1] 68
# Adelie 企鹅的平均体重(忽略缺失值)
adelie <- subset(penguins, species == "Adelie")
mean(adelie$body_mass_g, na.rm = TRUE)
[1] 3701
# 三种企鹅的平均翼展
tapply(penguins$flipper_length_mm, penguins$species, mean, na.rm = TRUE)
   Adelie Chinstrap    Gentoo 
      190       196       217 
# 找出体重最重的5只企鹅
penguins_clean <- na.omit(penguins[, c("species", "island", "body_mass_g")])
top5 <- penguins_clean[order(penguins_clean$body_mass_g, decreasing = TRUE), ]
head(top5, 5)
# A tibble: 5 × 3
  species island body_mass_g
  <fct>   <fct>        <int>
1 Gentoo  Biscoe        6300
2 Gentoo  Biscoe        6050
3 Gentoo  Biscoe        6000
4 Gentoo  Biscoe        6000
5 Gentoo  Biscoe        5950

4.10 数据框的合并与拼接

# rbind():纵向拼接(行数增加,列名要一致)
df1 <- data.frame(姓名 = c("张三", "李四"), 成绩 = c(85, 92))
df2 <- data.frame(姓名 = c("王五", "赵六"), 成绩 = c(78, 96))
rbind(df1, df2)
  姓名 成绩
1 张三   85
2 李四   92
3 王五   78
4 赵六   96
# cbind():横向拼接(列数增加,行数要一致)
基本信息 <- data.frame(姓名 = c("张三", "李四"), 年龄 = c(20, 21))
成绩信息 <- data.frame(语文 = c(85, 92), 数学 = c(88, 79))
cbind(基本信息, 成绩信息)
  姓名 年龄 语文 数学
1 张三   20   85   88
2 李四   21   92   79
# merge():按共同变量关联合并(类似 Excel VLOOKUP)
学生ID <- data.frame(姓名 = c("张三","李四","王五"), 学号 = c("001","002","003"))
学生成绩 <- data.frame(学号 = c("001","003","002"), 分数 = c(85, 78, 92))
merge(学生ID, 学生成绩, by = "学号")
  学号 姓名 分数
1  001 张三   85
2  002 李四   92
3  003 王五   78

Part 5 列表与因子

灵活容器与分类变量

5.1 列表:什么都能装

向量和矩阵要求所有元素类型相同;列表则没有这个限制——可以装任意类型、任意结构的东西:

# list() 创建列表
学生档案 <- list(
  姓名   = "张三",
  年龄   = 20,
  成绩   = c(85, 92, 78),           # 一个向量
  联系方式 = list(                   # 嵌套另一个列表
    电话 = "138-xxxx-xxxx",
    邮件 = "zhangsan@email.com"
  )
)

str(学生档案)
List of 4
 $ 姓名    : chr "张三"
 $ 年龄    : num 20
 $ 成绩    : num [1:3] 85 92 78
 $ 联系方式:List of 2
  ..$ 电话: chr "138-xxxx-xxxx"
  ..$ 邮件: chr "zhangsan@email.com"

5.2 访问列表中的元素

# $ 访问具名元素
学生档案$姓名
[1] "张三"
学生档案$成绩
[1] 85 92 78
# [[ ]] 双方括号(可用名字或位置)
学生档案[["年龄"]]
[1] 20
学生档案[[3]]       # 第3个元素(即成绩向量)
[1] 85 92 78
# 访问嵌套列表
学生档案$联系方式$电话
[1] "138-xxxx-xxxx"
学生档案[["联系方式"]][["邮件"]]
[1] "zhangsan@email.com"

注记

[单括号][[双括号]] 的区别:

  • 列表[1] 返回列表(把第1个元素装在一个列表里)
  • 列表[[1]] 返回第1个元素本身(取出来,不再是列表)

日常取元素用 [[]]$

5.3 列表的实际应用场景

什么时候用列表?

列表在日常分析中不如数据框常见,但在以下场景非常重要:

  • 函数返回多种类型的结果时(如统计检验结果:t.test() 的返回值就是列表)
  • 储存不同长度的数据集合
  • 处理 JSON 数据(从网络 API 获取的数据通常是列表格式)
# 示例:t.test() 返回的就是列表
结果 <- t.test(penguins$bill_length_mm ~ penguins$sex, data = penguins)
class(结果)       # 是 list
[1] "htest"
names(结果)       # 里面有哪些元素?
 [1] "statistic"   "parameter"   "p.value"     "conf.int"    "estimate"   
 [6] "null.value"  "stderr"      "alternative" "method"      "data.name"  
结果$p.value      # 取出 p 值
[1] 1.07e-10
结果$conf.int     # 取出置信区间
[1] -4.87 -2.65
attr(,"conf.level")
[1] 0.95

5.4 因子:专门处理分类变量

因子(factor)是 R 专门为分类变量设计的数据类型:

# 普通字符向量
等级_字符 <- c("及格", "优秀", "良好", "及格", "优秀", "不及格")
class(等级_字符)
[1] "character"
# 转为因子
等级_因子 <- factor(等级_字符)
class(等级_因子)
[1] "factor"
等级_因子
[1] 及格   优秀   良好   及格   优秀   不及格
Levels: 不及格 优秀 及格 良好
levels(等级_因子)   # 查看因子的水平(按字母/笔画排序)
[1] "不及格" "优秀"   "及格"   "良好"  

注记

注意 levels() 的排序:R 默认按字母顺序排列因子水平(对中文是按拼音)。"不及格"排在"良好"前面,这不符合逻辑——下一页讲如何自定义顺序。

5.5 有序因子:设定正确的水平顺序

# 指定 levels 参数控制顺序,ordered = TRUE 表示有序
等级_有序 <- factor(
  c("及格", "优秀", "良好", "及格", "优秀", "不及格"),
  levels  = c("不及格", "及格", "良好", "优秀"),   # 从低到高
  ordered = TRUE   # 有序因子,可以比较大小
)

等级_有序
[1] 及格   优秀   良好   及格   优秀   不及格
Levels: 不及格 < 及格 < 良好 < 优秀
levels(等级_有序)
[1] "不及格" "及格"   "良好"   "优秀"  
# 有序因子可以比较大小
等级_有序[1] < 等级_有序[2]   # 及格 < 优秀
[1] TRUE

5.6 因子在数据分析中的作用

# 因子在 table() 中会自动按水平顺序排列
table(等级_有序)
等级_有序
不及格   及格   良好   优秀 
     1      2      1      2 
# penguins 中的 species 和 island 就是因子
class(penguins$species)
[1] "factor"
levels(penguins$species)    # 三种企鹅
[1] "Adelie"    "Chinstrap" "Gentoo"   
# 因子在 table() 中统计频数
table(penguins$species)

   Adelie Chinstrap    Gentoo 
      152        68       124 
table(penguins$species, penguins$island)  # 交叉表
           
            Biscoe Dream Torgersen
  Adelie        44    56        52
  Chinstrap      0    68         0
  Gentoo       124     0         0

为什么不直接用字符?

因子 vs. 字符的核心区别:

  • 因子知道自己有固定的类别,不会出现意外的新类别
  • 因子可以设定顺序,影响图表的显示顺序
  • 统计模型(回归等)需要因子来正确处理分类变量

一个很常见的 bug:忘记把分类变量转成因子,导致模型把它当成普通文字处理。

5.7 四种数据对象总结对比

对象 维度 元素类型 典型用途
向量 1维 必须同类型 单列数据、中间计算
矩阵 2维 必须同类型 数学运算、机器学习算法
数据框 2维 每列可不同 日常数据分析(主力!)
列表 任意 任意混合 函数返回值、JSON数据
# 验证你的理解:用 class() 检查 penguins 各列的类型
sapply(penguins, class)
          species            island    bill_length_mm     bill_depth_mm 
         "factor"          "factor"         "numeric"         "numeric" 
flipper_length_mm       body_mass_g               sex              year 
        "integer"         "integer"          "factor"         "integer" 

5.8 综合实战:串联本讲所有内容

# 1. 加载数据,做体检
library(palmerpenguins)
cat("企鹅数量:", nrow(penguins), "\n")
企鹅数量: 344 
cat("变量数量:", ncol(penguins), "\n")
变量数量: 8 
cat("缺失值总数:", sum(is.na(penguins)), "\n")
缺失值总数: 19 
# 2. 清洗:去掉有缺失值的行
企鹅 <- na.omit(penguins)
cat("清洗后企鹅数量:", nrow(企鹅), "\n")
清洗后企鹅数量: 333 
# 3. 新建分类列(用向量化 ifelse)
企鹅$体型 <- ifelse(企鹅$body_mass_g >= 4500, "大型", "小型")
table(企鹅$体型, 企鹅$species)
      
       Adelie Chinstrap Gentoo
  大型      8         3    104
  小型    138        65     15

5.9 综合实战(续)

# 4. 用列表汇总三种企鹅的统计结果
物种列表 <- levels(企鹅$species)

结果 <- lapply(物种列表, function(sp) {
  子集 <- subset(企鹅, species == sp)
  list(
    物种     = sp,
    数量     = nrow(子集),
    平均体重 = round(mean(子集$body_mass_g), 0),
    平均翼展 = round(mean(子集$flipper_length_mm), 1)
  )
})

# 5. 输出结果
for (r in 结果) {
  cat(r$物种, ":", r$数量, "只,",
      "平均体重", r$平均体重, "g,",
      "平均翼展", r$平均翼展, "mm\n")
}
Adelie : 146 只, 平均体重 3706 g, 平均翼展 190 mm
Chinstrap : 68 只, 平均体重 3733 g, 平均翼展 196 mm
Gentoo : 119 只, 平均体重 5092 g, 平均翼展 217 mm

本讲小结

  • 数据类型:数值(numeric)、字符(character)、逻辑(logical)是三种最常用的类型。用 class() 查看,用 as.*() 转换。"3"3 是不同的——引号决定类型。

  • 向量:R 的基础单元。用 c() 创建,[ ] 索引,逻辑索引是筛选数据的核心方式。向量化运算让你无需写循环就能处理一列数据。

  • 矩阵:二维的向量,元素必须同类型。适合数学运算,apply() 可以按行或列汇总。

  • 数据框:日常分析的主力。每列可以是不同类型,用 $ 取列,[行, 列] 取元素,subset() 筛选行,merge() 关联合并。

  • 列表:什么都能装的灵活容器。函数返回的复杂结果通常是列表,用 $[[]] 取元素。

  • 因子:分类变量的正确姿势。设定 levels 控制顺序,ordered = TRUE 允许大小比较。

课后练习

基础练习(必做)

  1. 创建一个向量,记录你这学期所有课程的学分(至少5门),计算总学分和平均学分,筛选出学分 ≥ 3 的课程
  2. data.frame() 创建一个包含5位同学信息的数据框(至少包含:姓名、性别、成绩三列),然后筛选出成绩前3名
  3. 把成绩列转成有序因子,水平为:不及格 < 及格 < 良好 < 优秀,验证 "良好" < "优秀" 是否为 TRUE

进阶挑战(选做)

  1. 加载 penguins 数据,统计每个岛屿(island)上各种企鹅(species)的平均嘴峰长度(bill_length_mm),用矩阵或 tapply() 展示结果
  2. lapply()penguins 的数值列(第3至6列)分别计算均值和标准差,把结果存成列表
  3. penguins 数据框中新增一列 bmi_like,定义为 body_mass_g / (flipper_length_mm^2),找出该指标最大的3只企鹅

下讲预告

第3讲:R语言中的控制语句及函数

  • if 分支
  • for 循环
  • while 循环
  • switch 语句
  • 自定义函数
  • R语言中的内置函数

谢谢!

第2讲:数据类型与数据对象


"数据结构选对了,问题就解决了一半。"

数据科学界流传的经验之谈