先聊点题外话。R语言入门这件事,我一直觉得最大的门槛不是语法,不是包,而是“数据到底怎么存、怎么取”。矩阵、数组、列表和数据框这四个东西,就像四个形状不同的盒子,有人往里塞数据塞得顺手,有人动不动就报错,报错之后一查,,多半是盒子选错了。这篇笔记是我自己学R语言时整理出来的,从矩阵讲到数据框,把每个数据结构的创建方式、索引规则和最容易踩的坑都过一遍。适合刚学完向量和因子、准备深入数据结构部分的人,也适合已经用了一段时间R但偶尔还会被结构搞得头晕的读者。
1. 数据结构为什么值得单独花时间学
1.1 一切从“数据放在哪”开始
很多人学R语言,最先接触的是向量,比如x <- c(1, 2, 3)。向量解决的是“一堆同类型数据怎么存”的问题,但它只有一维。现实里的数据很少只有一维:一张表格有行有列,一个实验可能有多个时间点、多个变量、多个重复。这时候就得靠矩阵、数组、列表和数据框来把数据组织起来。
我刚开始学的时候,总想着偷懒,不管什么数据都塞进数据框里,后来发现有些运算用矩阵方便得多,有些返回值只能用列表来接,结构选错了,后面全是事。所以说,这四个结构不是“学完就忘”的知识点,而是R语言里几乎所有操作的底层逻辑。数据清洗、统计建模、画图,每一步都逃不开它们。
1.2 四个结构的递进关系
这四个结构不是并列关系,更像是递进关系。矩阵是向量的二维版,所有元素必须是同一类型;数组又是矩阵的高维版,可以是三维、四维;列表则是“什么都能装”的容器,不同元素甚至可以是不同结构;数据框则是列表的一个特例,它要求每个元素(列)等长,适合存表格数据。
理解这条递进线,后面学起来会特别顺。就好比搭积木:向量是单块积木,矩阵是把积木排成方阵,数组是搭成立体结构,列表是一个可以把积木、模型、图纸都塞进去的大箱子,数据框则是“规则地按列摆放”的特殊箱子。R语言里几乎所有数据操作,最后都能落到这四种盒子的选择上。
2. 矩阵:线性代数运算的核心载体
2.1 matrix()函数创建矩阵,注意byrow参数
矩阵在R语言里最常见的创建方式就是matrix()。这个函数的核心参数是data、nrow、ncol和byrow。直接看代码感受一下:
# 按列填充,默认行为 m1 <- matrix(1:9, nrow = 3) m1 # [,1] [,2] [,3] # [1,] 1 4 7 # [2,] 2 5 8 # [3,] 3 6 9 # 按行填充 m2 <- matrix(1:9, nrow = 3, byrow = TRUE) m2 # [,1] [,2] [,3] # [1,] 1 2 3 # [2,] 4 5 6 # [3,] 7 8 9这里最容易被忽略的就是byrow参数。R语言默认是按列填充的,很多新手第一次写matrix(1:9, nrow=3),以为会得到行序列1 2 3 / 4 5 6 / 7 8 9,结果得到的是按列排列的形式,一下子就懵了。记住:想让数据按行排,就把byrow = TRUE写上。这个参数之所以重要,是因为它在真实场景里常常决定你后面处理数据时能不能一眼看出规律。
给矩阵的行列命名也是常见操作,用dimnames参数:
m3 <- matrix(1:9, nrow = 3, byrow = TRUE, dimnames = list(c("r1", "r2", "r3"), c("c1", "c2", "c3")))有了行名列名之后,索引和查看都会舒服很多,尤其是数据维度高的时候,光看[,1] [,2]这种默认标签,很容易看花眼。
2.2 矩阵索引:单行单列返回的是什么
矩阵的索引和向量一脉相承,都是方括号。但这里有个关键细节,直接影响你后续代码写不写得出:
a <- matrix(1:12, nrow = 3, byrow = TRUE) a # [,1] [,2] [,3] [,4] # [1,] 1 2 3 4 # [2,] 5 6 7 8 # [3,] 9 10 11 12 a[1, ] # 取第1行,返回一个向量 # [1] 1 2 3 4 a[, 2] # 取第2列,返回一个向量 # [1] 2 6 10 a[2, 3] # 取第2行第3列,返回标量 # [1] 7 a[1:2, 3:4] # 取子矩阵 # [,1] [,2] # [1,] 3 4 # [2,] 7 8注意,a[1, ]取出来的一行,在R语言里会自动降维成向量。如果你希望它保持矩阵的形式,需要加drop = FALSE:
a[1, , drop = FALSE] # [,1] [,2] [,3] [,4] # [1,] 1 2 3 4这个drop参数特别容易坑人。比如你写了个函数,输入是矩阵,内部用a[1, ]取了一行,然后拿去做矩阵乘法,结果因为返回的是向量,维度对不上,直接报错。我在刚开始写代码时几乎每周都会被这种问题折磨一次,后来养成习惯:凡是取矩阵的子集后还要保持二维结构的,一律加drop = FALSE。
2.3 矩阵运算:乘法、转置、求逆一个都别搞混
矩阵在R语言里最爽的地方就是线性代数运算很方便。但注意,*和%*%是完全不同的两个运算符。*是逐元素相乘,%*%才是数学意义上的矩阵乘法:
A <- matrix(1:4, nrow = 2) B <- matrix(rep(2, 4), nrow = 2) A * B # 对应位置相乘 # [,1] [,2] # [1,] 2 6 # [2,] 4 8 A %*% B # 真正的矩阵乘法 # [,1] [,2] # [1,] 10 10 # [2,] 14 14除此之外,转置、求逆、行列式也是高频操作:
t(A) # 转置 solve(A) # 求逆 det(A) # 行列式 eigen(A) # 特征值分解这里插一个实际工程里的例子。很多人学矩阵乘法时觉得抽象,其实它在数据处理里的应用非常广。比如传感器标定,某个通道的测量值 v 和真实值 w 之间往往满足关系 w = C %*% v + w0,C是标定矩阵,w0是零漂。这时候只要把多组标定数据整理成矩阵,直接用R的矩阵运算就能一次性完成所有通道的校正,不用一行行写循环。很多热词里出现“增广矩阵”“分块矩阵求逆”这类数学名词,本质也都是给这种矩阵运算做铺垫。R语言里遇到线性方程组 Ax = b,直接solve(A, b)就能解,和你手写增广矩阵消元的思路一致,但快得多。
3. 数组:把矩阵堆成立体结构
3.1 array()函数与dim参数
数组是矩阵的高维推广,创建函数的语法最核心的就是dim参数。矩阵是二维的,数组可以是三维、四维,甚至更高。来看一个典型的三维数组:
arr <- array(1:24, dim = c(2, 3, 4)) arr # , , 1 # # [,1] [,2] [,3] # [1,] 1 3 5 # [2,] 2 4 6 # # , , 2 # # [,1] [,2] [,3] # [1,] 7 9 11 # [2,] 8 10 12 # ...这里dim = c(2, 3, 4)的意思很直白:第一个维度长度是2,第二个是3,第三个是4。你可以用“先定形状,再往里填数”的眼光来看待它。输入的数据长度必须等于各维度长度的乘积,也就是2 * 3 * 4 = 24,否则R会直接报错或者循环补齐数据,后者特别危险,因为R会“好心”地重复你的数据,你是不是真的想要,它不管。
数组的维度也可以命名,与矩阵的dimnames类似:
arr2 <- array(1:24, dim = c(2, 3, 4), dimnames = list(c("甲", "乙"), c("x", "y", "z"), c("t1", "t2", "t3", "t4")))3.2 数组的实际使用场景
很多人学数组的时候觉得“我数据结构里好像用不到三维数据”,但实际上一碰到时间序列的横截面数据、图像处理里的多通道像素、重复实验的多批次测量,数组就非常合适。
举一个场景:你做了两个实验对象,测了3个指标,连续测了4天。把每一天的数据看成一张矩阵,4天就是4张矩阵叠在一起,这就是一个维度为c(2, 3, 4)的三维数组。想取第2个对象第3个指标第4天的值,直接写arr[2, 3, 4]。
数组的索引规则和矩阵完全一致,只是方括号里多几个下标。想要对数组做某个维度的批量运算,可以用apply()函数。比如想对上面那个数组按第一维和第二维汇总(也就是在每个“日”层上把两个对象、三个指标分别求平均),可以写:
apply(arr2, MARGIN = c(1, 2), mean)MARGIN指定保留哪些维度,简单理解:你想对哪个维度“折叠”掉,就不要把它写在MARGIN里。这个思路在矩阵和数组里完全通用。
我觉得数组这个结构,很多人可能用得少,但我建议还是学一遍,因为很多R包在处理栅格数据、影像数据时内部就是多维数组,明白结构才能理解后面的数据操作。
4. 列表:什么都能装的万能容器
4.1 list()创建列表
列表和矩阵、数组最大的不同在于:它不要求元素类型相同。你可以在同一个列表里放一个数值向量、一个字符串、一个矩阵,甚至再嵌套一个列表。这在R语言里几乎是不可或缺的能力,因为很多统计建模函数返回的就是一个列表。
my_list <- list( name = "张三", age = 25, scores = c(88, 92, 77), meta = data.frame(city = "北京", job = "数据分析师") )注意这里每个元素都有名字,name、age、scores、meta。列表里装的东西类型各异,这在数据框里是做不到的,数据框的每一列长度必须一致,而列表完全不限制。
4.2 三种索引方式,傻傻分不清
列表的索引是新手最容易翻车的地方。同样一个my_list,用[]、[[]]、$取出来的东西完全不同:
my_list[1] # 返回一个列表,里面包含name这个元素 my_list[[1]] # 返回"张三"这个字符串本身 my_list$name # 返回"张三"[]相当于“把盒子拿给你”,[[]]和$相当于“把盒子里的东西倒出来”。为什么这个区别重要?因为很多函数要求传入的是向量而不是列表。如果你把my_list[1]直接传给需要字符串的函数,八成会报错。用str()看结构能明显看出差异:
str(my_list[1]) # List of 1 # $ name: chr "张三" str(my_list[[1]]) # chr "张三"区别一目了然。还有一个细节:$只能用于有名字的列表元素,而且支持部分匹配。比如my_list$nam也能取出name的内容,但这属于“R的善意”,有时候反而害人。假如你有一个元素叫name1,又有一个元素叫name,部分匹配可能取到意想不到的值。想完全避免这种不确定,就用[[ ]]加完整名字,或者把options(warnPartialMatchDollar = TRUE)打开提醒自己。
4.3 列表的增删改和常用操作
列表的增加和删除非常灵活:
# 增加一个新元素 my_list$email <- "zhangsan@example.com" # 删除一个元素,赋NULL就行 my_list$age <- NULL # 修改元素名字 names(my_list) <- c("姓名", "分数", "元数据", "邮箱")删除元素时用NULL清空,这个操作是R里挺特别的设计,但凡学过其他编程语言的人刚开始都会不习惯,总想着remove = TRUE之类的东西。其实就一个原则:让元素变成NULL,它就从列表里消失了。
列表还有一个高频操作是批量处理,常用lapply()和sapply():
lapply(my_list, class) # 返回列表,每个元素是class()的结果 sapply(my_list, length) # 尽量简化为向量或矩阵lapply返回的结果仍然是个列表,sapply会尝试简化结构。这一对函数配合列表使用,是R语言里替代for循环的重要姿势。
4.4 列表和R语言生态的紧密关系
很多新手不理解为什么列表这么重要,直到他们去执行lm()、t.test()这类统计分析函数,然后对着返回值发愣。比如lm()返回的对象,本质上就是一个包含了系数、残差、拟合值、方差分析表的列表。你用summary()看得到漂亮输出,是因为R内部给这个列表设计了专门的显示方法。如果你想取模型里面的某个值,比如很常见的“我要把回归系数提取出来”,就是model$coefficients或者model[["coefficients"]]。
列表在R语言里相当于一块“万能收纳板”。网上搜索“r语言数据分析案例”,你会发现很多案例的中间结果都是列表形式存放的。理解列表的索引规则,后面用包、读模型、调参都会顺手很多。
5. 数据框:数据分析里的绝对主角
5.1 数据框和Excel表格的对应关系
数据框是R语言里最接近日常表格概念的数据结构。每一列代表一个变量,每一行代表一条观测。它本质上是一个特殊形式的列表:列表的每个元素是一列,所有列长度相同。用data.frame()创建:
df <- data.frame( id = 1:4, name = c("A", "B", "C", "D"), score = c(88, 92, 77, 85), pass = c(TRUE, TRUE, FALSE, TRUE) ) df # id name score pass # 1 1 A 88 TRUE # 2 2 B 92 TRUE # 3 3 C 77 FALSE # 4 4 D 85 TRUE创建时每列用列名 = 向量的方式定义。正因为数据框是“每列是一个等长向量”的结构,所以不同列可以是不同类型,数值列、字符列、逻辑列共存同一张表,但同一列内部必须是同类型。
5.2 数据框的索引:$, [[]], 和行列组合
数据框的索引方式很多,我先列常用的:
df$score # 用$取列,返回向量 df[["score"]] # 用[[ ]]取列,效果和$类似 df[, "score"] # 行列索引方式取列,返回向量 df["score"] # 只取列,但返回的是数据框(单列) df[2, 3] # 第2行第3列 df[df$score > 85, ] # 筛选score大于85的所有行这里最绕的是df["score"]和df[["score"]]的区别。前者返回一个单列数据框,后者返回一个向量。很多人把两者混着用,然后发现有些函数能处理向量却不能处理数据框,报错信息千奇百怪。
新增列、修改列也很直白:
df$grade <- c("优秀", "优秀", "及格", "良好") df$grade <- NULL # 删掉grade列要注意的是,用df$新列名 <- 向量新增列时,新向量长度必须和原数据框行数一致,否则R会报错或者循环补齐。循环补齐看着方便,但实际上很容易掩盖bug,我有一次不小心用了一个长度为2的向量去给4行的数据框加列,结果R自动把向量重复了一遍,没有报错,数据却全错了。所以加列时一定检查长度。
5.3 数据框的两个经典大坑
关于数据框,有两个坑我必须单独拎出来讲,因为它们几乎人人都踩过。
第一个坑:字符串会被自动转成因子。在R语言的老版本里(4.0之前),data.frame()默认把字符列转成因子,也就是不存储原始字符串,而是存一个数字编码加一个标签表。这对传统统计学分析是好事,因为很多统计模型要求分类变量是因子;但对数据清洗来说就很麻烦,你明明输入的是"A" "B" "A",取出来却告诉你Levels: A B。解决办法是显式指定:
df <- data.frame(name = c("A", "B", "C"), stringsAsFactors = FALSE)R 4.0之后的默认行为已经改了,不再自动转因子,但很多旧代码和旧习惯还留在网上,所以看到这个参数要知道是怎么回事。
第二个坑:数据的列名被“净化”了。R在创建数据框时,默认会调用make.names()把列名处理成“合法标识符”。举个例子,你导入的表格第一行是"sale-price"或者"2023年收入",创建数据框后列名可能变成sale.price和X2023.年收入。如果不想让R动你的列名,设置check.names = FALSE:
df2 <- data.frame("2023收入" = c(1, 2), check.names = FALSE)这两个坑都属于“R自动帮你做了事情,但做的未必是你想要的”。所以拿到一个数据框,第一步永远是用str()扫一遍,确认列名、类型都符合预期,再开始后续分析。
5.4 数据框与其他结构的转换
数据框和矩阵、列表之间可以互相转换:
as.matrix(df[ , c("score", "pass")]) # 数值列转矩阵 as.data.frame(matrix(1:9, nrow = 3)) # 矩阵转数据框 as.list(df) # 数据框转列表,每一列是一个列表元素转换时要特别注意类型问题。as.matrix()如果遇到数据框里既有数值列又有字符列,R会把所有列统一转成字符型,因为矩阵要求元素类型相同。你原本计算的score是数值,转完矩阵变字符串,后面一算均值直接报错。这种类型强制转换是R里最容易“安静地搞破坏”的操作之一,务必留意。
6. 一张表整理四种结构的定位与选择
6.1 核心差异对比
我把这四个结构放到一张表里,方便对照:
| 结构 | 维度 | 元素类型 | 典型场景 | 创建函数 |
|---|---|---|---|---|
| 向量 | 一维 | 同类型 | 单个变量的一组观测值 | c() |
| 矩阵 | 二维 | 同类型 | 线性代数运算、多元统计输入 | matrix() |
| 数组 | 三维及以上 | 同类型 | 多批次观测、栅格数据 | array() |
| 列表 | 不固定 | 可混合 | 函数返回值、装载多个对象 | list() |
| 数据框 | 二维(列内等长) | 列间可混合、列内同类型 | 表格数据、数据分析主战场 | data.frame() |
这个表格我建议收藏。我在实际中判断用哪种结构,基本就几句话:数据是几维的?元素类型是否统一?要不要做矩阵运算?如果答案是“二维、类型混合、主要是统计建模”,那大概率用数据框;如果答案是“二维、全是数值、要做线性代数”,那就用矩阵;如果元素零散且类型各异,那就列表;如果对象维度超过二维,数组就登场了。
6.2 项目里常见的“结构选型”思考过程
举个例子,假设你接到一个任务,分析某门店一周的销售数据,包含日期、商品类别、销量、单价、是否打折。这种数据天然适合数据框,因为列的类型混合:日期是Date,类别是字符或因子,销量和单价是数值,是否打折是逻辑值。你不可能用矩阵存它,因为矩阵只能存单一类型。
但如果任务变成“计算销量和单价的协方差矩阵”,你就可以单独把这两列抽出来,转成矩阵,然后直接调用cov()或cor()。很多人问为什么不直接在数据框上算,因为数据框允许混合类型,它本身不是数值型的,很多需要纯数值矩阵的运算会拒绝接收数据框。
再比如,你写一个函数,要返回多个不同类型的结果:一个模型对象、一个拟合值向量、一个数据框。这种时候列表就是最自然的选择,因为函数只能返回一个对象,你把这个对象做成列表,外面想取什么都方便。R里大量包就是这么设计的,虽然不便,但习惯之后效率很高。
7. 实操中的常见坑与排查技巧
7.1 矩阵子集“悄悄变成向量”
我前面提到过,a[1, ]返回向量,而不是矩阵。这个问题在写循环时尤其明显。比如你写了一个处理函数,内部逐行处理一个矩阵,然后试图把每行rbind起来:
result <- NULL for (i in 1:nrow(a)) { row_now <- a[i, ] # 这里已经变成向量了 result <- rbind(result, row_now) }如果每一行长度相同,结果可能还能拼起来;如果某一行有缺失值或者长度意外变化,rbind就会报错。排查这类问题,最简单的办法是:
class(a[i, ])看到integer或者numeric,你就知道R把你的行降维成向量了。想要保持矩阵维度,记得drop = FALSE。
7.2 列表取元素时,[]和[[]]搞混
这个坑在取模型结果时最典型。比如model$coefficients能正常返回一个向量,但如果写model["coefficients"],返回的是一个单元素列表,后续做运算就可能报错“非数值参数”。我自己排查这种问题时的标准动作是:先str()看看结构,再用is.list()判断,如果是列表,就换成[[]]或$。
7.3 字符串和因子的自动转换
老版本R里,data.frame()默认把字符串列变成因子这件事,坑了无数人。你导入一份CSV,然后对某个字符列做字符串替换,结果发现替换失败,因为它是因子不是字符。解决办法是导入时设置stringsAsFactors = FALSE,或者用read.csv()的时候直接指定。R语言新版本已经改掉这个默认值了,但很多教程和旧代码还在,所以看到因子列,心里默念一句“有可能是字符串”,再动手处理。
7.4 用str()代替print(),观察结构才是王道
最后分享一个我的实操习惯:拿到任何R对象,第一件事先跑str(),而不是print()或head()。str()会清楚地告诉你这个对象的类型、维度、每个元素的类型和部分值。它能第一时间暴露“这是个列表”“这列是因子”“这行被转成了向量”等问题。用熟了之后,你会发现很多报错在没发生之前就已经被看出来了。
数据框有问题?str(df)。列表取不到值?str(my_list)。矩阵结果不对?str(你的矩阵)。这一招比任何调试工具都管用。
这篇笔记写到这里,基本覆盖了矩阵、数组、列表和数据框四个核心数据结构。我自己学下来最大的感受是:R语言的很多设计乍看奇怪,比如[[]]和[]的区别、按列填充的默认规则,其实背后都有设计逻辑。只是这些逻辑不会有人主动告诉你,必须靠实操去碰。如果你还在初学者阶段,别急着追求高深建模,先把手里的数据结构弄明白,后面分析数据、写函数的效率会提升一大截。