资讯详情 R语言数据结构详解:矩阵、数组、列表与数据框实战指南
📅 2026/10/4 5:58:46
先聊点题外话。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, nrow3)以为会得到行序列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 FALSEa[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 w0C是标定矩阵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 - zhangsanexample.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 FALSEdf2 - 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语言的很多设计乍看奇怪比如[[]]和[]的区别、按列填充的默认规则其实背后都有设计逻辑。只是这些逻辑不会有人主动告诉你必须靠实操去碰。如果你还在初学者阶段别急着追求高深建模先把手里的数据结构弄明白后面分析数据、写函数的效率会提升一大截。