第2章 R语言数据结构:从向量到列表,一栋数据大厦
做特征工程,说白了就是在跟数据结构打交道。你拿到的原始数据可能是表格,中间处理结果可能是向量,最后模型需要的又可能是矩阵——这五种结构就是你的工具箱。我个人习惯先把它们摸透,后面写管道、建特征才不会卡壳。
R的数据结构可以分成两大类:同质结构(所有元素类型一样)和异质结构(元素可以不同类型)。向量、矩阵、数组属于前者,数据框和列表属于后者。下面逐个聊。
2.1 向量(Vector)——最基础的数据单元
向量是R里最核心的结构,其他很多结构本质上都是向量的组合。创建向量最简单的方式就是用 c() 函数,中文叫“组合”。
# 数值向量
num_vec <- c(3.14, 2.71, 1.618)
# 字符向量
char_vec <- c("房价", "户型", "面积")
# 逻辑向量
log_vec <- c(TRUE, FALSE, TRUE)
注意R里面没有标量(单个值),写 x <- 5 其实创建的是一个长度为1的向量。这一点我刚开始学的时候老忘。
索引方式:R是1-based索引(从1开始),这个跟Python不一样。
num_vec[2] # 返回2.71
num_vec[-1] # 去掉第一个元素,返回剩余
避坑指南:我曾经在特征筛选时用 which() 找符合条件的索引,结果写成了 num_vec[num_vec > 3] 返回的是元素本身,不是位置——用 which(num_vec > 3) 才拿到索引。这两个容易搞混。
向量还有一个“循环”特性,做运算时短向量会自动补长,这在矩阵乘法和加偏差时非常有用,但如果你没搞清楚,可能会出悄悄错误。
2.2 矩阵(Matrix)——行和列的战场
矩阵本质上是加了维度属性的向量。用 matrix() 创建。
mat <- matrix(1:12, nrow = 3, ncol = 4)
mat
# [,1] [,2] [,3] [,4]
# [1,] 1 4 7 10
# [2,] 2 5 8 11
# [3,] 3 6 9 12
默认按列填充(先填满第一列),如果想按行,加 byrow = TRUE。矩阵的维度用 dim(mat) 查看,返回两个数字:行数和列数。
矩阵运算:
- 逐元素乘除:直接用
*和/ - 矩阵乘法:用
%*%,这是你算协方差矩阵、特征工程中做降维时绕不开的符号 - 转置:
t(mat)
实际经验:在特征工程里,很多时候我们先把数据框转换成矩阵,再丢给线性代数函数。比如做PCA时,原始数据矩阵要中心化,R里写 scale(mat, center = TRUE, scale = FALSE) 就完事。
2.3 数组(Array)——维度再升级
数组可以看作是矩阵的推广:矩阵是二维,数组可以是任意维度。用 array() 创建,需要指定数据和维度向量。
arr <- array(1:24, dim = c(2, 3, 4))
dim(arr) # 2 3 4
上面创建了一个有4个“层”的数组,每层是2行3列的矩阵。你可以在特征工程中把不同时间切片的数据堆到数组里,比如每日特征的月份序列。
索引多维数组时,哪个维度对应什么含义,建议写注释。我曾经在写时空特征提取时,把时间维度和空间维度搞反,结果模型怎么调都不对——检查了半天才发现索引顺序错了。
2.4 数据框(Data Frame)——日常工作的主力
数据框是表格型数据,每一列可以不同类型(一列全是数字、另一列全是字符串),这是它和矩阵的最大区别。创建方法:
df <- data.frame(
id = 1:4,
city = c("北京", "上海", "深圳", "广州"),
price = c(50000, 60000, 45000, 32000),
is_sale = c(TRUE, FALSE, TRUE, FALSE)
)
常用操作:
- 查看列名:
names(df) - 提取列:
df$price或df[["price"]] - 行筛选:
df[df$price > 40000, ] - 增加新列:
df$price_per_sqm <- df$price / 100
注意:默认情况下,data.frame() 会把字符串列转为因子(factor),如果你不想要这个行为,记得加 stringsAsFactors = FALSE。我在第一次做回归时,分类变量被当成因子编码方式搞乱了特征,后来改用 tibble::tibble() 省心多了。
在特征工程中,数据框是最常用的容器:原始数据、预处理后的特征、衍生特征都可以放在里面,用 dplyr 管道操作非常顺手。
2.5 列表(List)——装下一切
列表是最灵活的结构,它可以包含任意类型、任意长度的元素,甚至可以装另一个列表。
my_list <- list(
title = "特征工程手册",
data = mtcars, # 数据框
coeffs = c(0.3, -1.2, 0.8), # 向量
sub_model = list( # 嵌套列表
weight = 0.5,
intercept = 2.1
)
)
访问方式:
my_list$title—— 通过名字访问my_list[[2]]—— 通过位置索引,返回元素本身my_list[2]—— 返回一个子列表(注意:仍是一个列表)
我个人经验:列表常用来保存模型训练中间产物,比如特征工程流程的各个阶段输出。如果想把多个类型不同的结果打包返回,列表就是最佳选择。
2.6 五种数据结构速览对比
| 结构 | 维度 | 元素类型 | 典型用途 |
|---|---|---|---|
| 向量 | 1维 | 同质 | 存储单列数据、系数、索引 |
| 矩阵 | 2维 | 同质 | 线性代数运算、距离矩阵 |
| 数组 | 多维(≥2) | 同质 | 图像数据、时间序列堆叠 |
| 数据框 | 2维 | 每列异质 | 日常表格数据、特征存储 |
| 列表 | 递归结构 | 完全异质 | 包裹多个对象、复杂返回 |
你想想看,这五种结构其实可以看作是一个模型——从最简单的向量开始,慢慢往上堆维度、堆灵活性,最后得到列表这种“万能袋”。掌握了它们,你在R里写特征工程代码就有了骨架。
这张图把五种结构的衍生关系串起来了。你在写代码时,心里最好有个这样的谱:从向量出发,叠加维度变成矩阵/数组;为每一列赋予不同类型,就成了数据框;如果要装更复杂的东西,就上列表。
避坑二:我曾经为了图省事,把所有特征放在一个列表里,结果后续想转成数据框时用了 as.data.frame(),发现列表里元素长度不一样就报错——这时需要先用 do.call(cbind, ...) 或者 dplyr::bind_cols() 对齐长度。