资讯动态

R语言四种数据结构详解:矩阵、数组、列表与数据框

发布时间:2026/10/4 8:05:34 来源:尧图企业网站定制
R语言笔记三矩阵、数组、列表和数据框如果你和我一样刚开始学R语言的时候脑子里全是数据结构到底有几种的疑问那这篇笔记应该能帮你理顺思路。在R里向量是最基础的单元但真正做数据分析时天天打交道的其实是矩阵、数组、列表和数据框这四样东西。它们各自有不同的形态和适用场景搞不清楚很容易在写代码的时候卡壳。这篇笔记是R语言学习系列的第三篇定位是基础但绝不浅薄我会把这四种数据结构的创建方式、访问语法、常见操作、典型坑点全部过一遍并且结合真实的数据分析场景说明它们各自解决什么问题。适合刚学完向量和因子、准备进入数据容器阶段的朋友也适合用R写过一段时间脚本但总觉得概念有模糊地带的人。1. 为什么R要设计出这么多容器从数据形态倒推数据结构的选择很多人学R的时候第一个绕不过去的弯就是明明一个向量就能放一堆数据为什么还要搞出矩阵、数组、列表和数据框这四种东西到底有什么区别我的理解是这跟现实中我们怎么组织和存储数据是一回事。你去超市买菜可以用一个袋子装所有东西但你不会把鸡蛋和酱油放在同一个袋子里不管会不会压碎——你会用不同的容器去装不同形态的东西。R里的数据结构也是按数据形态来设计的。向量只能装一种类型的数据比如全是数字或者全是字符串。但真实世界的数据往往是多维的、混合的这时候向量就不够用了。R设计了四种更复杂的容器各有各的规则矩阵二维的、所有元素类型相同的表格。适合存放同一类指标的数据比如一组样本的基因表达量全是数字。数组可以超过二维的立方体数据。适合存放需要多维度索引的数据比如不同时间点、不同地点、不同指标的温度测量值。列表什么都能装的大袋子。你可以把向量、矩阵、数据框、甚至另一个列表都塞进一个列表里而且每个元素可以长度不同、类型不同。适合存放并不规整的对象。数据框二维的、每一列可以不同类型但列长度必须一致的表格。这最接近Excel和SQL表格是数据分析中使用频率最高的结构。一句话总结矩阵和数组是严格的容器列表是自由的容器数据框是最像人话的容器。这一点想明白之后后面学每种结构的细节就不会觉得混乱了。你只需要记住一个原则先看你的数据是什么形态再选容器。2. 矩阵同质数据的最常用二维容器2.1 三种创建方式矩阵我用的频率相当高尤其是在做统计模拟和线性代数运算的时候。创建矩阵最直接的函数是matrix()。# 方式一直接指定行数和列数 m1 - matrix(1:12, nrow 3, ncol 4) m1 # 输出 # [,1] [,2] [,3] [,4] # [1,] 1 4 7 10 # [2,] 2 5 8 11 # [3,] 3 6 9 12注意看数据默认是按列填充的也就是先填满第一列再填第二列。如果你希望按行填充必须设置byrow TRUE。m2 - matrix(1:12, nrow 3, ncol 4, byrow TRUE) m2 # [,1] [,2] [,3] [,4] # [1,] 1 2 3 4 # [2,] 5 6 7 8 # [3,] 9 10 11 12这个byrow参数坑了我好几次因为很多其他编程语言是按行填充的但R默认按列。你要么每次写代码都记得这个差异要么干脆都用byrow TRUE省得后面读数据的时候怀疑人生。第二种创建方式是拼装。如果有几个等长的向量想拼成一个矩阵用cbind()按列拼接和rbind()按行拼接非常方便。x - c(1, 2, 3) y - c(4, 5, 6) z - c(7, 8, 9) cbind(x, y, z) # x y z # [1,] 1 4 7 # [2,] 2 5 8 # [3,] 3 6 9 rbind(x, y, z) # [,1] [,2] [,3] # x 1 2 3 # y 4 5 6 # z 7 8 9第三种方式是把向量加上dim属性变成矩阵这种写法不常见但可以加深理解。v - 1:12 dim(v) - c(3, 4) v # [,1] [,2] [,3] [,4] # [1,] 1 4 7 10 # [2,] 2 5 8 11 # [3,] 3 6 9 12其实矩阵底子里还是向量只是有了dim这个维度属性。看到这个例子之后你对R是向量化语言这句话会有更深的感觉。2.2 行列索引和子集提取矩阵的索引语法和向量一脉相承用方括号逗号左边是行右边是列。m - matrix(1:20, nrow 4, byrow TRUE) m # [,1] [,2] [,3] [,4] [,5] # [1,] 1 2 3 4 5 # [2,] 6 7 8 9 10 # [3,] 11 12 13 14 15 # [4,] 16 17 18 19 20 m[2, 3] # 第2行第3列 8 m[1, ] # 第1行 1 2 3 4 5 m[, 4] # 第4列 4 9 14 19 m[c(1, 3), c(2, 4)] # 第1、3行第2、4列提取子集时会遇到一个常见的坑如果你取矩阵的一行或一列R默认会把这个结果降维成向量。这在某些场景下不是你想要的结果。m[, 4] # [1] 4 9 14 19 - 这是一个向量不是矩阵 m[, 4, drop FALSE] # [,1] # [1,] 4 # [2,] 9 # [3,] 14 # [4,] 19如果后续你还需要把这个结果当矩阵用比如做矩阵乘法就必须加上drop FALSE否则维度信息丢失会直接报错或者算出完全不同的结果。这个细节我建议直接记在本子上。2.3 矩阵运算和常用函数矩阵运算里有几个函数是天天用的t(m)转置%*%矩阵乘法注意是百分号包裹星号不是单纯的*solve(A)矩阵求逆det(A)行列式rowSums()/colSums()对行或列求和rowMeans()/colMeans()对行或列求平均这里最容易混淆的是*和%*%的区别。*是元素逐个相乘也叫Hadamard积%*%才是线性代数里的矩阵乘法。初学者写矩阵乘法时忘了用%*%出来的结果完全不对而且R不会报错只会给你一个看似合理的数字特别危险。A - matrix(1:4, nrow 2) B - matrix(5:8, nrow 2) A * B # 元素级相乘 # [,1] [,2] # [1,] 5 21 # [2,] 12 32 A %*% B # 矩阵乘法 # [,1] [,2] # [1,] 23 31 # [2,] 34 462.4 给矩阵的行列起名字在处理真实数据时纯数字的行号和列号很难看懂dimnames可以解决这个问题。m - matrix(1:9, nrow 3, byrow TRUE) rownames(m) - c(甲, 乙, 丙) colnames(m) - c(数学, 语文, 英语) m # 数学 语文 英语 # 甲 1 2 3 # 乙 4 5 6 # 丙 7 8 9 # 用行列名提取数据 m[乙, 英语] # 6给矩阵加了行列名之后不仅可读性大幅提升索引时也能直接按名字取数日常分析方便很多。如果你是从CSV读进来的数据很多情况下程序已经把列名给你设置好了。3. 数组超过二维的多维数据容器3.1 数组是什么场景下用的数组可以理解成矩阵的往上扩展版矩阵是二维的数组可以是三维、四维甚至更高维。我用数组最典型的场景是在气象数据处理中——每一天、每个观测站、每个气象指标如果一次性读进来就是一个三维数组。# 生成一个三维数组2个站点 × 3天 × 4个指标 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 # , , 3 # [,1] [,2] [,3] # [1,] 13 15 17 # [2,] 14 16 18 # , , 4 # [,1] [,2] [,3] # [1,] 19 21 23 # [2,] 20 22 24这个数组的维度含义是dim[1]是站点编号2个站点dim[2]是天数3天dim[3]是指标编号4个指标。所以arr[1, 2, 3]就表示第1个站点、第2天、第3个指标的数值。3.2 创建数组时数据怎么排列array()函数的核心参数是dimR会按照第一个维度优先变化的顺序填充数据。# 二维数组其实就是矩阵 arr2d - array(1:6, dim c(2, 3)) # [,1] [,2] [,3] # [1,] 1 3 5 # [2,] 2 4 6可以看到填数据的时候是从第一维开始变化也就是先填第1维的所有值行方向再移动到第2维的下一个值下一列。在三维数组里就是先把前两个维度组成的层填满再填第三维的下一层。3.3 在数组上做运算数组也可以和向量一样做算术运算遵循循环规则较短的向量会被自动补齐到数组长度再做运算。arr2 - arr * 2 # 每个元素都乘以2 arr2如果做跨维度汇总比如想计算每个站点3天的平均气温可以用apply()函数沿指定维度做运算。# 假设arr是 2个站点 × 3天 × 4个指标 # 沿第2维度天求平均得到 2站点 × 4指标 的矩阵 apply(arr, c(1, 3), mean) # [,1] [,2] [,3] [,4] # [1,] 3 15 27 39 # [2,] 4 16 28 40apply(X, MARGIN, FUN)的第二个参数MARGIN可以传一个向量表示保留哪些维度。在上面例子里传c(1, 3)就是保留站点和指标两个维度对天这个维度求均值结果就坍缩成一个2×4的矩阵。刚学apply的时候很容易搞混它的返回值维度。我的经验是MARGIN传什么维度结果就保留什么维度。传1结果就是按行处理返回长度等于行数的向量传c(1, 2)返回一个矩阵传1:3返回维度相同的数组。3.4 数组与矩阵的边界有个容易忽略的点是二维数组和矩阵在绝大多数情况下是可以互换的但维度信息不完全等价。你可以在矩阵上用dim()函数设置三个维度比如dim(m) - c(2, 3, 2)它就会变成数组。反过来数组也能被提取成矩阵。dim(arr) # 2 3 4 # 提取第1个指标的2×3矩阵 arr[, , 1]实际做数据分析时三维以上的数组其实用得不多最常用的还是数据框。数组更多出现在科研计算、模拟、图像处理这类场景。但理解数组对理解R的维度这个概念非常有帮助因为它的存在让数据是有维度的这件事变得特别直观。4. 列表什么都能装的自由容器4.1 列表的结构和创建如果说矩阵和数据框是规整的表格那列表就是自由的柜子。柜子里的每一层可以放完全不同的东西一个抽屉放数字向量一个抽屉放字符串一个抽屉放矩阵还有一个抽屉可以放整个数据框。student - list( name 张三, age 22, scores c(85, 90, 78), info data.frame( course c(数学, 英语, 编程), score c(85, 90, 78) ) )列表跟数据框最大的区别是数据框每一列长度必须相同列表没有这个约束。所以你可以用列表装长度不等的多个向量这在数据处理时很常见——比如从网页上抓下来的数据每个类别的样本量不一样用列表存储非常自然。4.2 列表索引两种方括号的世纪性大坑列表的索引是新手最容易崩溃的地方因为R给列表设计了两种不同的方括号。lst - list(a 1:3, b hello, c matrix(1:4, nrow 2)) lst[1] # 返回一个子列表仍然包含元素名和属性 lst[[1]] # 返回向量本身1 2 3 lst$a # 等价于 lst[[a]]返回1 2 3 lst[[a]] # 返回1 2 3简单记单括号[]取出来还是一个列表双括号[[]]才能取出列表里的原始对象。很多人写lst[1]然后想对结果做向量运算结果发现全是NA或者报错根源就在这里。还有两个更容易忽略的细节第一lst$a和lst[[a]]在大多数情况下等价但在某些场景下有差异。比如你想根据一个变量名来取元素col_name - a lst[[col_name]] # 返回 lst$a 的值 lst$col_name # 不会取到 lst$a而是取 lst$col_name不存在则返回NULL这种变量名当索引的需求在写循环或者批量处理的时候特别常见所以用lst[[col_name]]比lst$col_name稳妥得多。第二检查列表是否有某个元素别用lst$name NULL这种写法直接is.null(lst$name)判断或者在取之前用name %in% names(lst)确认。4.3 列表的增删改列表的操作也很灵活可以随时加新元素、删旧元素。lst$d - c(10, 20) # 添加新元素 lst$b - NULL # 删除名为b的元素 lst[[e]] - new # 用字符串名添加 names(lst) # a c d e删除列表元素的时候有个行为需要注意lst$b - NULL不光是删除了元素还会把后面的元素往前挪。如果你只是想把某个元素置空显式赋NULL和删除是一回事这一点和Python的dict不太一样。列表还支持嵌套就是列表里再套列表。取嵌套列表的值时需要连续用两层双括号或者用$一路剥开。写深层索引的时候我建议拆成多步赋值可读性会好很多不然很容易出现一串[[1]][[x]]把眼睛都看花了。4.4 列表的遍历和批量操作遍历列表是lapply和sapply函数的主场。lapply对列表每个元素应用函数返回一个同样长度的列表sapply会自动简化结果能简化为向量就简化成向量。scores - list(数学 c(85, 90, 78), 英语 c(70, 88), 编程 c(95)) # 计算每个学科的平均分 lapply(scores, mean) # $数学 # [1] 84.33333 # $英语 # [1] 79 # $编程 # [1] 95 sapply(scores, mean) # 数学 英语 编程 # 84.33333 79.00000 95.00000列表配合lapply系函数是R里没有for循环也能批量处理的核心思路和函数式编程里map的概念很像。在数据处理管线中列表经常作为中间枢纽先按组拆分数据放进列表再对每组做同样的处理最后合并。这个模式我会在最后的综合例子里再演示一遍。5. 数据框最接近人话的数据表格5.1 数据框的本质与读取方式数据框是R里最像Excel表格的数据结构有行有列每一列可以存不同类型的数据数字、字符串、因子但是每一列的长度必须一致。这种结构对应着真实数据分析里最典型的样本×变量格式。数据框的底层其实是一个列表它的每一列都是列表的一个元素所以你在数据框上使用$取列名、用[[]]取列这些技巧都继承自列表。但数据框加了一个约束所有列等长因此能像矩阵那样用行号取行。实际工作中我绝大多数情况是从文件读取数据框而不是手动创建。# 读取CSV文件stringsAsFactors看情况设置 df - read.csv(data.csv, stringsAsFactors FALSE) # 看看数据结构 str(df) # 显示每列的类型和部分值 head(df) # 默认显示前6行 # 用dplyr风格查看 library(dplyr) glimpse(df)5.2 数据框的创建和基础访问data.frame()是手动创建数据框的最常用方式df - data.frame( 姓名 c(张三, 李四, 王五), 年龄 c(22, 25, 28), 城市 c(北京, 上海, 广州) ) df数据框的访问方式继承了矩阵和列表两套语法非常丰富也因此容易混df$姓名 # 取一列返回向量 df[[2]] # 取第2列返回向量 df[2] # 取第2列返回一个只有1列的数据框 df[, 2] # 取第2列向量 df[, 2, drop FALSE] # 保留数据框结构 df[2, ] # 取第2行返回数据框 df[2, 3] # 取第2行第3列这里有两个容易踩的坑第一df[2]和df[, 2]不一样。前者返回单列数据框后者返回向量。我在写代码的时候经常忘了这个区别特别是在用lapply时混入了数据框结构结果处理结果完全不同。第二df[df$年龄 23, ]这种条件筛选语法对新手来说很不友好那个逗号位置错一个就报错。我更推荐用subset()函数参数直白一些subset(df, 年龄 23) subset(df, 年龄 23, select c(姓名, 城市))或者直接用dplyr包里的filter()读起来更接近英文自然语言。5.3 增加和删除列给数据框加新列非常简单df$年级 - c(3, 4, 5) # 直接赋值新列 df$年龄分组 - ifelse(df$年龄 25, 大龄, 年轻)但注意一个坑你赋值的向量长度必须跟数据框行数一致否则R会报错或者说替换数据长度错误。当然如果赋一个常量R会自动补齐所有行这个行为非常贴心df$学校 - 示例大学删除列有两种方式df$学校 - NULL # 方式一置空即可 df - df[, -which(names(df) 年龄分组)] # 方式二按列号删除我比较常用第一种因为它更直白且不易出错。第二种方式在你需要批量删除多列时有用。5.4 处理缺失值和因子真实数据几乎都有缺失值可以说处理缺失值是数据分析的必修课。数据框里缺失值用NA表示。常用的处理方式有# 查看缺失值 is.na(df) colSums(is.na(df)) # 每列缺失数量 # 删除有缺失的行 na.omit(df) df[complete.cases(df), ] # 填充缺失值基础版 df$年龄[is.na(df$年龄)] - mean(df$年龄, na.rm TRUE)另一个常见坑是字符串列的因子自动转换问题。在老版本R里read.csv()读进来的字符串列会自动变成因子这常常导致后续处理报错比如你试图修改一个因子的某个水平会得到一堆invalid factor level提示。新版本R4.0以上默认stringsAsFactors FALSE了但如果你还在用老代码或者某些包会自动转因子要注意区分。# 如果列已经是因子转为字符串 df$城市 - as.character(df$城市) # 也可以重新设置因子水平 df$城市 - factor(df$城市, levels c(北京, 上海, 广州))5.5 行列合并和连接数据框的合并有两种场景按行堆叠和按列拼接以及更复杂一点的按条件匹配连接。按行堆叠用rbind()要求两个数据框列名一致、列的顺序一致。这个约束违反时只会报一个莫名其妙的错误实际检查起来很麻烦。可以用dplyr::bind_rows()它允许列名不一致且自动匹配我个人基本已经不再用rbind合并数据框了。按列拼接用cbind()要求行数一致。这个操作相对少用因为如果两个数据框的顺序不一致拼出来的结果就会错位。提示在做任何拼接之前先确认两个数据框的行顺序是否对应。如果不确定用共同的ID列做连接才是稳妥的。按条件匹配连接是数据框的绝活也是SQL里JOIN的概念。基础R的merge()函数df1 - data.frame(id c(1, 2, 3), 分数 c(80, 70, 90)) df2 - data.frame(id c(1, 2, 3), 城市 c(北京, 上海, 广州)) merge(df1, df2, by id)如果使用dplyr生态inner_join()、left_join()、right_join()等函数功能更强、语义更清楚遇到复杂连接条件时优先选它。6. 一个综合实战把四种结构串起来解决实际问题学了概念之后必须动手。我设计一个小任务把矩阵、数组、列表和数据框全部用上流程跟实际做一次简单数据分析很像。任务设定我有一个5名学生、4门课程的成绩数据需要用矩阵存储然后转成数据框同时用一个列表记录班级的元信息班主任姓名、考试日期、满分情况最终计算每科平均分和每个学生的总分再按总分排个序。第一步创建矩阵并加行列名scores_matrix - matrix( c(85, 90, 78, 88, 70, 68, 75, 80, 72, 65, 92, 88, 86, 90, 84, 76, 70, 82, 79, 85), nrow 5, byrow TRUE, dimnames list( c(stu1, stu2, stu3, stu4, stu5), c(数学, 英语, 编程, 统计) ) ) scores_matrix第二步把矩阵转为数据框。这一步非常关键因为矩阵里的所有元素是同一类型数据框允许每一列是不同类型转换之后你可以在里面加姓名字符串等列scores_df - as.data.frame(scores_matrix) scores_df$姓名 - c(张三, 李四, 王五, 赵六, 孙七) scores_df - scores_df[, c(姓名, 数学, 英语, 编程, 统计)] scores_df第三步用列表存元信息class_info - list( 班主任 刘老师, 考试日期 2024-01-15, 满分 c(数学 100, 英语 100, 编程 100, 统计 100), 说明 期中考试 )这里列表每个元素的长度和类型都不同正是列表的典型使用场景。第四步做汇总统计。用apply()对矩阵按行或按列计算# 每科的平均分 colMeans(scores_matrix) # 每个学生的总分矩阵按行求和 rowSums(scores_matrix) # apply的等价写法 apply(scores_matrix, 1, sum)然后把这些汇总结果加入数据框scores_df$总分 - rowSums(scores_matrix) scores_df$平均分 - rowMeans(scores_matrix)第五步排序并输出scores_df[order(scores_df$总分, decreasing TRUE), ]如果把整个流程合起来看你会发现每种结构在里面各司其职矩阵用来做同质数值运算数据框用来做结构化的表格展示和扩展列列表用来存放非结构化元信息。等你在真实项目里跑几遍这样的流程对R的数据结构体系就会有肌肉记忆了。最后分享一个我从实际使用中总结的小心得很多人学R的时候爱背矩阵适合XX数据框适合XX这种规则但一旦上了真实项目界限其实没那么分明。data.frame在95%的场景下是主力矩阵在需要做线性代数运算时不可替代列表是万能兜底数组则是解决多维度问题的特定武器。你把每个结构的创建、索引、运算逻辑练熟真正要处理什么数据的时候自然就能拿起来用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑