
做数据处理这半年我最深的一个体会是R语言本身并不难难的是你用什么样的思路去操作一张表。很多新手一上来就啃ggplot2画图、啃机器学习包结果卡在数据清洗这一步连一列变量都改不明白。这篇笔记就围绕数据处理里最基础也最常用的四个动作——增加、修改、选取、排列把变量和观测两类对象的操作讲透。这篇内容不需要你有任何编程基础只要理解了“列是变量、行是观测”这两个概念跟着一步步敲就能把R的数据处理底子打好。1. 数据处理的核心逻辑先想清楚你要动的是变量还是观测1.1 一张表里的两个维度变量和观测分别代表什么在R语言里我们面对的数据绝大多数都是二维表结构。你可以把这张表想象成一个Excel表格横向一行是一个观测observation通常代表一个样本或者一条记录纵向一列是一个变量variable代表样本的某一个属性。举个最常见的例子。你在收集某班级学生的考试成绩那这个数据集里每个学生就是一行观测而姓名、语文成绩、数学成绩、总分这些字段就是列也就是变量。在实际数据处理中最容易犯的错误就是搞混这两个维度——对列做行操作或者对行做列操作。比如你想给每个学生加一列“是否及格”却写成了在行方向上空循环整个逻辑就歪了。在这篇文章里我会始终坚持一个习惯先明确要操作的对象是变量列还是观测行再选择对应的函数和操作方式。这套思维一旦建立起来后面学任何数据处理包都会快很多。1.2 为什么推荐用tidyverse体系而不是基础R的$符号R语言本身提供了很多基础的数据操作方式比如用$提取列、用[行,列]索引来取子集、用cbindrbind来拼接数据。这些方法不是不能用但在实际项目中会暴露出几个问题代码冗长、可读性差、逻辑容易被索引号绕晕。相比之下tidyverse体系里的dplyr包把数据操作抽象成了几个动词mutate增加或修改列、select选列、filter按条件选行、arrange排序、summarise汇总。每个动词只负责一件事参数命名也直观配合管道符%%或R 4.1之后的原生管道|可以把一连串操作像流水线一样串起来。我在这篇笔记里会以dplyr为主不是因为基础R不能用而是因为对新手来说dplyr的语法更像是在“描述你想做什么”而不是“告诉计算机怎么一步步做”。这能帮你把注意力集中在业务逻辑上而不是被下标计算消耗掉。1.3 实战前的准备工作安装加载包与示例数据构建在开始正文之前先把环境准备好。你需要安装并加载dplyr包同时准备一份练习数据。这里我用一个模拟的学生成绩表做演示数据完全是虚构的但结构足够典型。安装和加载的代码如下install.packages(dplyr) library(dplyr)创建示例数据的代码如下我用tibble()来构建一个整洁的数据框。你可以看到这里有三列学号、姓名、成绩。注意成绩里面有重复值还有一个缺失值NA这都是为了后文演示需要而特意设置的。library(tibble) students - tibble( 学号 c(001, 002, 003, 004, 005, 006), 姓名 c(张明, 李华, 王芳, 赵磊, 孙悦, 周凯), 成绩 c(85, 92, 78, NA, 65, 92) ) students运行之后你会看到一张6行3列的小表。这张表的数据量很小但足够覆盖增、改、选、排这四类常用操作。接下来我们就从“变量操作”和“观测操作”两个维度分别展开。2. 变量操作如何新增一列和修改一列的值2.1 mutate()函数的本质基于现有列计算新列新增变量的首选工具是mutate()。它做的事情是在数据框右侧追加一列同时保留原有所有列。这一列可以是常数也可以是基于现有列计算得到的结果。先看一个最简单的用法给成绩表增加两列一列是全员相同的班级编号一列是每个学生成绩加5分的“平时分”。代码如下students_new - students %% mutate( 班级 高二3班, 成绩_平时分 成绩 5 ) students_new这里我给你拆解一下逻辑。第一个参数班级 高二3班右侧是一个常数R会自动把常数复制到每一行得到一列内容完全相同的变量。第二个参数成绩_平时分 成绩 5右侧是一个向量运算R会逐行取出“成绩”列的值加5后存入新列。关键点在于mutate()里新列可以引用刚刚创建的其他新列。这在实际操作中非常省事。举个例子如果我先创建了“成绩_平时分”然后想基于它再计算是否超过90分我可以在同一个mutate()里直接引用前一列students_more - students %% mutate( 成绩_平时分 成绩 5, 是否优秀 ifelse(成绩_平时分 90, 是, 否) )学完这个用法你会发现新增列这个动作完全不需要额外写循环也不需要担心列数对不上的问题mutate()会保证结果的行数不变。这一点比在Excel里手动拉公式要严谨得多因为你的每一步操作都有记录可复现。2.2 修改已有变量的三个思路重写、变换、条件赋值修改一列的值本质上也是mutate()。区别在于你要给新列起的名字和旧列名相同。这样R不会新增一列而是覆盖原列的内容。第一种思路直接重写。比如我想把成绩列全部替换成满分100分就可以写成绩 100所有行的成绩都会被改写为100。这种操作在真实项目中常用于修正错误数据或者强制标准化某个字段。第二种思路变换。比如我想根据成绩评定等级就可以用case_when()做多条件判断。这个函数比嵌套ifelse清晰得多。示例代码如下students_grade - students %% mutate( 成绩 case_when( 成绩 90 ~ 优秀, 成绩 80 ~ 良好, 成绩 60 ~ 及格, TRUE ~ 不及格 ) )这里解释一下case_when()的规则它按顺序从上往下匹配条件一旦某个条件为TRUE就返回对应的值最后一行TRUE ~ 不及格是兜底设置意思是以上所有条件都不满足时执行。需要注意如果成绩列里有NANA做比较运算会返回NA不会触发任何条件结果也会是NA。这个问题我在第5部分会详细说明。第三种思路条件赋值。也就是说我只想修改满足某些条件的特定行的值。比如我想把所有成绩低于60分的同学的成绩统一改为60分可以结合mutate()和ifelse()实现students_floor - students %% mutate(成绩 ifelse(成绩 60, 60, 成绩))ifelse()的三个参数依次是条件、条件为TRUE时的值、条件为FALSE时的值。整个表达式的意思是当成绩小于60分时取60否则保留原值。这种写法比在Excel里筛选后手动覆盖更安全因为你所有的修改规则都会留在代码里别人看代码就知道你做了什么改起来也方便。2.3 变量操作中的常见误区别在mutate()里直接用max()和min()新手在使用mutate()时经常栽在一个地方想给数据框加一列“成绩与最高分的差值”于是写了mutate(差值 成绩 - max(成绩))结果发现所有行的差值都一样。这是为什么因为max(成绩)是一个标量函数它返回的是整个向量的最大值而不是每一行的最大值。当你把它放进mutate()里R会把这个标量回收到每一行所以计算结果自然变成了“每个成绩减去同一个最高分”这其实是正确的。但如果你希望的是“每一行和它所在组的最高分作差”那就要用到分组操作group_by()或者用窗口函数filter(成绩 max(成绩))的思路这已经不是本篇的范畴。这个例子想提醒你的是mutate()里默认是按行独立计算遇到聚合函数时要思考清楚你是想要全局标量还是想要按行、按组的值。搞清楚了这一点很多诡异的结果就能解释得通了。3. 变量选取用select()精准锁定你需要的列3.1 按列名选取和按位置选取的写法对比当你面对一个几十列甚至上百列的大数据集时最迫切的需求往往是“我只想留下其中几列”。select()函数就是干这个的。按列名选取是最直观的方式。假设我只需要学号和成绩可以这样写students_sub - students %% select(学号, 成绩)如果你想排除某些列在列名前面加负号即可students_drop - students %% select(-姓名)这一行代码的意思是保留除“姓名”以外的所有列。结合场景很常见的需求你从外部读入了一份数据里面有ID列、有各种乱七八糟的中间计算列你只想删掉其中一两列用负号写法比重新列出所有保留列要省事得多。按位置选取也有应用场景。当列名特别长或者列名有特殊字符时直接按列所在的位置来选反而更稳。比如数据框的第1列是学号、第3列是成绩我可以写students_pos - students %% select(1, 3)这里传入的是数字位置1和3分别代表第1列和第3列。位置选取的隐患是一旦原始数据的列顺序发生变动你的代码结果就会出错。所以我的建议是能用列名就不用位置列名比位置稳定得多。位置选取更适合临时查看数据而不是写进正式的数据处理流程里。3.2 select()的辅助函数starts_with、ends_with、matches、everything除了直接给出列名或位置select()还提供了一系列辅助函数方便你在列名很多的时候按模式匹配列名。举几个最常用的students_sel - students %% select(starts_with(成绩)) students_sel2 - students %% select(ends_with(名)) students_sel3 - students %% select(matches(成绩|姓名))starts_with(成绩)选取列名以“成绩”开头的列。ends_with(名)选取列名以“名”结尾的列比如“姓名”、“网名”等。matches(成绩|姓名)用正则表达式匹配列名这里匹配的是列名里包含“成绩”或“姓名”的列。还有一个非常实用的函数是everything()。它通常和select()配合使用用来调整列的顺序。比如我想把“姓名”这一列移到最前面其他列保持原序可以写students_reorder - students %% select(姓名, everything())everything()表示“剩下的所有列”。这种用法在把ID列或标签列调整到表格最前方的时候非常顺手。实际做报告时你从数据库里拉出来一张20列的表想把关键标识列放到前两列方便肉眼快速浏览这一招比手动输入20个列名高效得多。3.3 选取列时的两个注意点重复列名处理和列名含特殊字符实际操作中会遇到两个容易让人卡壳的问题。第一个是列名重复。如果你的数据框里有两列都叫“成绩”select(成绩)会同时选取这两列并且结果里会出现重复列名。这在后续操作中会造成很大的混乱。我的建议是一旦发现重复列名先用janitor::clean_names()或者R基部的make.names()统一修正列名再往下走流程。第二个是列名含特殊字符。中文列名在R里能用但有时候用起来别扭。比如列名是成绩-期末里面带了减号select()会认为减号是排除符号导致报错。解决办法是用反引号包裹列名就像这样students_weird - students %% select(成绩-期末)实际上英文列名虽然没这个烦恼但从团队协作的角度看中文列名在图表标注和报告输出时反而更方便因为它省去了列名映射的环节。只是你要记住一个原则如果列名里有特殊字符空格、减号、括号等要么在操作时用反引号包裹要么在数据读取后立刻统一改名。这一点越早养成习惯越好。4. 观测操作筛选满足条件的行4.1 filter()函数的基本逻辑行筛选与条件判断操作行的核心函数是filter()。它做的事情是保留满足条件的所有行不满足的筛掉。与select()操作列不同filter()处理的是行方向。最简单的场景筛选出成绩大于等于90分的同学。students_high - students %% filter(成绩 90)这里的条件表达式的运算规则是逐行判断的。第一行成绩是8585 90结果为FALSE这一行会被过滤掉第二行成绩是92结果为TRUE这一行被保留。筛选完毕后R返回的是一个行数变小的数据框。多个条件组合时可以写多个条件参数也可以用一个参数里的逻辑运算符连接。下面两种写法等价students_mid - students %% filter(成绩 80, 成绩 90) students_mid2 - students %% filter(成绩 80 成绩 90)第一种写法里filter()的多个参数之间是“并且”的关系。第二种写法用运算符显式地连接两个条件。我个人的习惯是条件少的时候写在同一个参数里用连接条件多的时候拆成多个参数每行一个条件代码一目了然。4.2 常用比较运算符和逻辑运算符的适用场景在filter()里比较运算符和逻辑运算符是构建条件的基础。列个表总结一下运算符含义示例解释大于成绩 80成绩大于80大于等于成绩 90成绩大于等于90小于成绩 60成绩小于60小于等于成绩 70成绩小于等于70等于班级 A班班级等于A班!不等于班级 ! A班班级不等于A班%in%属于集合班级 %in% c(A班,B班)班级属于A班或B班并且成绩 60 成绩 90成绩在60到90之间或者成绩 60!非!成绩 %in% c(85, 92)成绩不等于85且不等于92这里特别要提醒这个运算符。它用于判断相等但在浮点数场景下会有精度问题。比如成绩 92.1可能是FALSE因为计算机存储的92.1可能是一个无限接近但不完全相等的二进制数。面对数值型变量的等值判断我通常建议用near()函数比如near(成绩, 92.1)它能容忍一个微小的误差范围。另一个高频率需求是筛选字符型列。比如我的数据里新增了一列“班级”想筛选班级为“高二3班”的行就用filter(班级 高二3班)。注意字符串必须用双引号或单引号包裹一个常见的错误是漏了引号R会把“高二3班”当成一个对象名去查找然后报错“object not found”。4.3 用slice()和distinct()处理行位置的选取与去重除了按条件筛选行还有两类行操作非常常用按位置选行、按重复值去重。slice()用于按位置选取行。比如我想取第2行到第4行可以写students_slice - students %% slice(2:4)这个函数在查看数据子集、随机抽样、或者配合arrange()取前几名的场景中特别好用。比如你想看成绩最高的三个人是谁可以先按成绩降序排列再用slice(1:3)取出前三行。这个组合操作在“取Top N”的场景下简直是标配。distinct()用于去除重复行。回到我们最初的示例数据注意“成绩”列里有两个人都是92分。如果我只关心有哪些成绩值出现过不关心具体是哪些学生就可以按“成绩”列去重students_unique - students %% distinct(成绩)distinct()如果不指定列名会对整个数据框去重也就是只有所有列完全相同的行才会被去除如果指定了列名比如distinct(成绩)则只根据这一列判断重复重复的行中保留第一行。这个函数在清理脏数据时很常用。比如你从Excel导入的数据里同一个ID出现了好几行你想看看这个ID对应多少种不同的记录先distinct()再数行数效率非常高。需要特别注意distinct()的去重范围局限在单个数据框内部它不会考虑其他表的重复情况。如果你要对多个数据框做合并去重那就需要后续学习连接操作join了这里先不展开。5. 观测排列用arrange()控制行的顺序5.1 升序、降序与多条件排序的写法很多时候数据集的行顺序是乱的或者你想按某一列的大小来重新组织数据的排列顺序。arrange()函数就是为这个场景服务的。最基本的用法是按“成绩”列升序排列students_sorted - students %% arrange(成绩)运行之后成绩最小的排在最前面最大的排在最后面。如果想降序排列在列名外面加一个desc()函数即可students_desc - students %% arrange(desc(成绩))desc()是dplyr专门提供的降序指示函数。有很多人尝试直接写arrange(-成绩)这在部分场景下也能生效但对字符列会报错。所以我建议一律用desc()既统一又不会出错。多条件排序也很常见。比如先按班级排序同一个班级里再按成绩降序排列student_rank - students %% arrange(班级, desc(成绩))在arrange()里写在前面的条件优先级更高。这个例子里所有“高二3班”的行会排在一起班级内按成绩从高到低排然后才是“高二4班”的行以此类推。理解多条件排序的关键是前面的条件起分组作用后面的条件在组内起作用。5.2 排序遇上缺失值NA会排到哪里去这是一个非常容易翻车的地方。默认情况下arrange()会把缺失值NA放在排序结果的最后面。无论你是升序还是降序NA总是沉底。如果你的业务逻辑要求NA排在最前面可以借助desc()加is.na()的组合实现。比如这样写students_na_first - students %% arrange(desc(is.na(成绩)), 成绩)我们来拆解一下这个操作。第一条件desc(is.na(成绩))当成绩是NA时is.na(成绩)为TRUE在逻辑值运算里TRUE相当于1FALSE相当于0。desc()会把TRUE排到前面于是NA的行就到了顶部。第二条件成绩再对这些行按成绩升序排列。这就实现了“NA置顶非NA按成绩排序”的效果。你可能会疑惑为什么默认要把NA放最后我的理解是R的设计哲学里NA代表“未知”它比任何已知的数值都更“大”所以升序时被排在最后。但在实际业务中“缺了成绩的人反而要优先处理”的需求很常见所以掌握这个NA置顶技巧非常有用。5.3 排列与行号的联动如何取前几名和后几名排序通常不是终点它往往和取子集、计数、标记名次等操作配合使用。最典型的场景就是“取前N名”。有了arrange()和slice()的组合取前几名的写法非常简洁。如果想看成绩最高的三个人top3 - students %% arrange(desc(成绩)) %% slice(1:3)如果想看成绩最低的三个人那就别用desc()bottom3 - students %% arrange(成绩) %% slice(1:3)还有一种思路是用row_number()函数。它可以在mutate()里给每一行生成一个行号。结合arrange()排序后再生成行号就能给每个学生一个明确的排序名次students_ranked - students %% arrange(desc(成绩)) %% mutate(排名 row_number())row_number()生成的排名是连续的1、2、3……但它不处理并列情况。如果两个学生成绩相同谁排在前面谁就得到更小的名次。如果你希望并列时名次相同那就得考虑dense_rank()或者min_rank()这类处理并列名次的方式了。关于排名函数我后文还会再提一下。6. 观测的增与删如何新增行和删除行6.1 bind_rows()与add_row()按行拼接数据的两种方式新增观测也就是给数据框增加一行或多行。最常用的函数是bind_rows()。假设你有一个新同学转学进来成绩还没录入你可以把这条新记录拼接到原数据框的下面new_student - tibble( 学号 007, 姓名 吴桐, 成绩 NA ) students_combined - bind_rows(students, new_student)bind_rows()的工作逻辑是按列名匹配把第二个数据框的每一行追加到第一个数据框的尾部。如果两个数据框的列名不一致它会自动补NA填充缺失的列。这一点在实际工作中特别有用因为不同时间读取的数据可能列顺序不一样用bind_rows()就避免了手动调整列顺序的麻烦。如果你只想添加一行而且这行的值比较零散也可以用add_row()函数students_add - students %% add_row(学号 008, 姓名 郑强, 成绩 88)add_row()是直接在数据框末尾添加一行。它的好处是你不需要先创建一个完整的数据框直接列出各列的值即可。注意add_row()默认加在最后一行如果你想插到指定位置可以用参数.before比如add_row(学号008, 姓名郑强, 成绩88, .before 3)。这个参数在特定场景里很实用但说实话日常处理里我绝大部分时候都是直接在末尾追加。6.2 删除行的正确姿势负索引与filter()取舍删除观测按条件筛选本质上就是一种删除方式。比如我想删除成绩小于60分的同学可以直接用filter(成绩 60)。但在某些场景里你会想用“负索引”直接按位置删行。slice()配合负数可以按位置删除行。比如想删除第2行students_no2 - students %% slice(-2)slice(-2)表示“不选取第2行”也就是把第2行去掉。如果想删除连续的多行比如删除第2行到第4行则不能直接写slice(-2:-4)那样R会尝试把负数序列给slice()大概率报错。正确写法是先明确要保留的行或者用slice(-c(2, 3, 4))students_no234 - students %% slice(-c(2, 3, 4))这里c(2, 3, 4)是向量负号表示排除这些位置。关于“按条件删除”和“按位置删除”我的选用标准是如果业务上有明确的删除条件比如删除成绩低于60的行、删除状态为“离职”的行优先用filter()因为代码的意图更清晰只有当你确实需要按数据框的物理位置操作时才使用负索引。还有一个需要小心的地方切片删除行的操作会连带影响后续的行号索引。如果你在一个管道里先删了第2行再想删原来的第5行就不能直接写slice(-5)了因为此时数据框的行号已经变了。这种场景下建议先用filter()的明确条件去锁定要删的行避免位置漂移导致的错误。6.3 替换与覆盖把特定观测的值改成新值最后再补充一个经常被归入“改”的操作替换特定观测的数值。它跟新增列、修改整列不同目标是“某一行的某个值”。最基础的方法是结合filter()和mutate()。比如我想把学号为“003”的同学的成绩改成80分可以先筛选出这一行再修改students_fix - students %% mutate(成绩 ifelse(学号 003, 80, 成绩))这个思路的本质是在整列上操作但通过条件判断只对满足条件的行赋予新值其他行保留原值。熟练之后你会发现这种写法非常安全因为它保留了完整的修改规则。如果你偏好更直白的“定位-修改”风格R基础包里也有replace()函数。它接收三个参数原向量、需要替换的位置或条件、新值。例如students$成绩 - replace(students$成绩, students$学号 003, 80)但要注意replace()是在基础R层面操作如果你没有在tidyverse的管道里工作直接用$符号也没问题。这里我只是提供两种思路你用哪种顺手就用哪种。7. 初学者最容易踩坑的5个数据处理错误7.1 把字符串当数字隐式类型转换引发的迷惑结果在真实的数据集里数字常常被读成字符型。比如你从csv文件导入成绩列因为某些行为出现“缺考”这样的文字R会把整列识别为字符型。此时你做成绩 80的判断R不会报错但会比较两个字符串的字母顺序结果完全不是你以为的那个意思。判断一列是不是数值型的简单方法class(students$成绩)如果结果是character你就需要用as.numeric()做类型转换。如果列里有“缺考”这样的文字直接转换会产生NA此时你需要先处理这些非数字字符把它们变成NA或者用readr::parse_number()提取数字部分。这一点我特别想说R不会主动帮你把字符型数字转成数值型它只会默默输出一个看似合理实则错误的结果。每次进行数值比较前先检查数据类型能帮你避开最隐蔽的坑。7.2 用号做相等判断赋值和判等混淆在R里既可以用作函数参数赋值也可以用于赋值。但在条件判断中等值判断必须使用。很多从其他语言转过来的初学者在filter()里写filter(成绩 90)R会把它解释为参数成绩被赋值为90而不是筛选条件结果整个数据框返回所有行。这个报错有时候不出现因为filter()会把这个赋值结果当作条件而赋的值本身是90R会把它当逻辑值处理。数字90在逻辑判断里相当于TRUE于是所有行都被保留。这种错误最坑的地方在于它不报错而且结果看似正常其实是全量数据。你在排查问题的时候很难一眼发现。我的检查方法是遇到filter()结果行数异常多时先看看条件里是不是少写了一个等号。在R里凡是做判断一律用双等号。7.3 NA的传播与判断为什么is.na()不可替代在数据处理中NA缺失值是一个非常特殊的存在。它的传染性极强。任何包含NA的数值运算结果都是NA。比如NA 5返回NANA 3返回NANA NA返回NA。这导致了一个常见错误想筛选出成绩为NA的行用filter(成绩 NA)结果一行都筛不出来。原因就是成绩 NA在每一行上都会返回NA而filter()只保留条件为TRUE的行NA会被当作FALSE处理。正确的写法是students_na - students %% filter(is.na(成绩))is.na()是一个专门用来判断缺失值的函数。它返回TRUE表示该位置是缺失值返回FALSE表示不是。这里一定不要用 NA要用is.na()。类似的如果你想筛选非缺失的行就写filter(!is.na(成绩))。这个!符号表示取反。熟练掌握is.na()你才能在和缺失值打交道时不迷路。7.4 逻辑运算符优先级别忘了括号的存在意义在R里逻辑运算符的优先级低于比较运算符所以成绩 60 成绩 90能正常执行。但如果条件复杂比如成绩 60 | 班级 A班 性别 男你可能会被优先级搞晕。R的运算符优先级里的优先级高于|所以上面这个表达式会被解释为“成绩60 或者 (班级为A班且性别为男)”。这可能不是你想要的。我的习惯是凡是混合使用了和|一律加括号明确分组。比如你想表达“成绩60或班级为A班且性别为男”应该写成students_logic - students %% filter((成绩 60 | 班级 A班) 性别 男)括号不仅让R准确理解你的意图也让读代码的其他人不会产生歧义。在数据处理这件事上牺牲一点简洁性换明确性永远值得。7.5 排序时受NA干扰先处理缺失再排序更稳妥刚才提到过arrange()默认把NA放到最后。如果你要排序的列里有大量NA而且你想按业务规则处理它们不要依赖默认行为先确定NA的优先级再排序。一种稳妥的做法是先filter掉NA再做排序students_clean_sorted - students %% filter(!is.na(成绩)) %% arrange(desc(成绩))这种做法适用于“缺失值本来就该剔除”的场景。如果缺失值需要保留但排在最后那就用默认的arrange()即可。如果缺失值需要置顶就用我之前说的arrange(desc(is.na(成绩)), 成绩)。总之一句话排序之前先想清楚NA的位置。不要让默认行为替你做了决定。8. 综合实战用一个小案例串联全部操作8.1 需求说明从原始成绩表到最终分析表的完整流程说了这么多单个操作你可能还是有点散。这里我设计一个完整的实战案例把今天讲的所有操作串联起来让你看看在真实场景里它们是怎么配合的。假设我现在拿到一张学生成绩表包含学号、姓名、班级、语文、数学、英语六列。我的需求是把这张表变成一份“班级总分排名表”具体步骤是删除成绩全为NA的无用行新增“总分”列计算语文、数学、英语三科成绩之和保留必要列学号、姓名、班级、总分按总分降序排列取每个班级的前3名。先构造原始数据library(dplyr) scores - tibble( 学号 c(001, 002, 003, 004, 005, 006, 007, 008), 姓名 c(张明, 李华, 王芳, 赵磊, 孙悦, 周凯, 吴桐, 郑强), 班级 c(A班, A班, A班, B班, B班, B班, B班, A班), 语文 c(85, 92, 78, 88, NA, 65, 90, 70), 数学 c(90, 88, 95, 79, 86, 92, 85, 83), 英语 c(78, 91, 84, 82, 88, 70, NA, 89) ) scores注意这里的数据里有三个坑赵磊的数学成绩低了点但可以用孙悦的语文是NA吴桐的英语是NA。这些缺失值都会影响后面的汇总计算。8.2 分步实现与代码解读每一步为什么这么写第一步删除全为NA的行这里没有整行都是NA的所以跳过。但假设有可以用filter(!(is.na(语文) is.na(数学) is.na(英语)))。第二步新增总分列。这里有一个关键细节rowSums()函数在处理NA时有一个参数na.rm设为TRUE表示忽略缺失值再求和。但我的建议是如果缺失值过多直接忽略会造成总分偏高所以先看看缺失情况再决定。这里假设缺失值可行我们用以下方式计算scores_total - scores %% mutate( 总分 rowSums(select(., 语文, 数学, 英语), na.rm TRUE) )这里我用了一个select(., 语文, 数学, 英语)的写法它的意思是临时选取语文、数学、英语三列参与计算。rowSums是基础包里的函数专门对行求和。na.rm TRUE表示忽略NA。第三步保留所需列scores_clean - scores_total %% select(学号, 姓名, 班级, 总分)第四步按总分降序排列scores_sorted - scores_clean %% arrange(desc(总分))第五步取每个班级的前3名。这一步需要用到分组操作scores_top3 - scores_sorted %% group_by(班级) %% slice_head(n 3) %% ungroup()group_by(班级)的作用是把数据按班级拆分成多个小组之后的slice_head(n 3)对每个小组取前3行。ungroup()是撤销分组把结果还原成普通数据框。8.3 最终结果和复盘学到了什么把整个流程串起来完整代码如下final_result - scores %% mutate( 总分 rowSums(select(., 语文, 数学, 英语), na.rm TRUE) ) %% select(学号, 姓名, 班级, 总分) %% arrange(desc(总分)) %% group_by(班级) %% slice_head(n 3) %% ungroup() final_result运行这段代码你会得到一个按班级分组后的TOP3名单。在做这个案例的过程中我至少用到了今天讲的五类操作mutate新增列、select选列、arrange排序、group_by配合slice取行子集、ungroup恢复结构。这就是tidyverse流水线的魅力每一步操作都有清晰的名字每一个动词都在描述你想做什么。你不需要记住复杂的下标运算只需要理解“变量”和“观测”这两个维度然后选择合适的动词即可。9. 高效数据处理的三个习惯9.1 可复现至上写代码而非手动点击数据处理最怕什么最怕“这次处理完了下次数据一变又得从头来”。在Excel里手动改虽然快但每一步操作都不可追溯。而在R里用mutate、select、filter、arrange这些函数写出来的代码本身就是你的处理日志。我强烈建议你从一开始就养成这样的习惯所有数据修改操作都写在脚本里而不是直接在环境中手动改数据对象。这样做的直接好处是当数据源更新后你只需要重新运行一遍脚本就能得到最新处理结果而不需要重新点一遍菜单。更进一步你可以把整个处理流程封装成一个函数。比如处理不同班级的成绩表你只需要改一个参数就能跑完全流程。这个进阶操作等你把今天的基础内容掌握之后再学也不迟。9.2 小步快跑每做一步就查看一次结果即使你在管道符里写了很长一串操作我也建议你每完成一步操作就查看一下结果是否符合预期。我的习惯是在构建管道时先写完一小段运行查看再往下加。原因很简单数据处理错误具有累积性。如果第一步就选错了列后面所有步骤都会基于错误数据运行排查起来会更困难。而一步一查的方式能把问题控制在最小范围内。等你熟练到一定程度可以一次写完整个管道但调试的过程中还是应该随时打印中间结果。9.3 善于利用RStudio的查看器View()函数不丢人RStudio的View()函数能在表格视图中展示数据框它配合filter()、select()使用可以让你直观地看到每一步操作的结果。这不丢人反而很高效。特别是在检查最终结果是否符合预期时用View()点开一看远比在控制台里瞪着打印出来的几十行文字更直观。如果你处理的是几百行、几千行的数据View()更是必备工具。10. 写在最后的一点实用心得做了一段时间的数据处理以后我发现真正拖慢效率的往往不是不会写某个函数而是对数据结构的理解不够透彻。变量和观测这两个词听起来简单但是当你的表有五十列、两千行时你要清楚自己是在横着给样本加属性还是竖着给字段加样本这决定了你选用哪一族的函数。这篇笔记里所有代码我都建议你亲手敲一遍然后尝试改改参数比如把条件从 90改成 85看看结果怎么变。R语言的学习没有捷径但数据处理的套路是可以复用的。你把这些基础动作练熟了后面不管是做数据可视化、统计建模还是机器学习都会顺畅得多。如果你在工作里遇到某个报错想不明白欢迎在本篇笔记下方留言我会尽量帮你排查。下一篇笔记我打算写数据透视与分组汇总也就是group_by()和summarise()的深度用法敬请期待。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。