走进一家繁忙的餐厅厨房:点菜单从传菜口一张张飞进来,掌勺大厨左手持漏勺(filter)筛掉标注“暂停供应”的菜品,右手捏笔在单子上补一列“小计”(mutate),收银台按桌号把菜单夹在一起算总账(group_by + summarise),后厨的储物柜阿姨把散乱堆放的食材标签按“品名-数量”重新归档(tidyr 的长宽转换)。这套流水线之所以高效,靠的是一条在工位之间不停转动的传送带——它就是 R 里的管道操作符 %>%。 dplyr 和 tidyr 正是 R 生态中这套厨房流水线的两大核心:dplyr 提供一套“动词”来加工数据表,tidyr 负责把数据表整理成整洁的长/宽形态。两者配合使用,能让数据清洗、聚合、连接一气呵成,把 messy data(脏乱数据)变成规规矩矩的分析底稿。本文就用这个厨房故事贯穿全程,带你认识这对“黄金搭档”。

岗位关联

掌握 dplyr/tidyr,等于给你的数据分析流水线装上标准化传送带:无论数据多脏多乱,你都能用一套统一的动词快速清洗、聚合、连接——这是 R 数据岗位最基本也最值钱的硬技能。

目录
  1. 可视化演示
  2. dplyr 动词核心:四件趁手的厨房工具
  3. 管道操作符 %>%:厨房里的流水线传送带
  4. 分组汇总:按桌结算的收银台
  5. 长宽转换:储物柜的两个收纳方向
  6. 跨列操作 across:一勺盐撒一排菜
  7. 多表连接 join:把两本账册拼成一桌宴席
  8. 缺失值处理:涂改不清的菜单怎么办
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示正态分布N(μ,σ²)的概率密度函数曲线,支持调节均值μ和标准差σ,实时观察分布形状变化及累积概率。

本节配有交互动效(进入视口后自动加载,约 3KB)。

概率密度函数 钟形曲线描述连续随机变量的概率密度分布 μ决定中心位置,σ决定曲线宽窄 f(x) = (1/σ√2π) · e^(-(x-μ)²/2σ²)

dplyr 动词核心:四件趁手的厨房工具

走进厨房,你会看到掌勺师傅的案板上只摆四件工具:漏勺、拣菜盘、刻刀和摆盘夹。漏勺从汤锅里捞出想要的食材——对应 dplyr 的 filter,按条件筛行;拣菜盘把需要的配菜从一堆原料中挑出来——对应 select,按列名选列;刻刀在食材上划出花刀,让菜更入味——对应 mutate,根据已有列算出新列;摆盘夹把菜品码放整齐——对应 arrange,按某列排序。这四件工具构成了 dplyr 最核心的“动词体系”。

在 R 中,一张数据表叫 data.frame(数据框),你可以把它想象成一本网格账本:每一行是一条记录(比如一道菜的订单),每一列是一个字段(比如菜品名、价格、数量)。dplyr 的哲学是:把数据处理拆成一个个“动词”,每个动词接受一个数据框,返回一个新的数据框,而且绝不修改原始数据——就像漏勺捞食材不会破坏汤底一样。

filter() 按行过滤数据,比如 filter(df, price > 30) 只留下单价超过 30 元的菜品;select() 按列挑选,比如 select(df, dish, price) 只抽出菜品和价格两栏;mutate() 生成新列,比如 mutate(df, subtotal = price * qty) 给每行加一列“小计”;arrange() 对行排序,arrange(df, desc(price)) 按价格降序排列。

这四件工具单独使用已经很方便,但它们的真正威力在于“组合”——而组合它们的关键,就是下一节要讲的厨房传送带:管道操作符。

verb(df,args)→dfnew\text{verb}(\text{df}, \text{args}) \to \text{df}_{\text{new}}
这个公式说明:任何一个 dplyr 动词,输入一张表,外加一些参数,就能得到一张新表,且原表保持原样。
  1. verb(动词)dplyr 内置的操作函数,如 filter、select、mutate、arrange,每个动词只负责一类操作。
    verb∈{filter,select,mutate,arrange}\text{verb} \in \{\text{filter}, \text{select}, \text{mutate}, \text{arrange}\}
  2. df(数据框)操作对象,就是一张像账本一样的表,行是记录,列是字段。
    df={r1,r2,…,rn}\text{df} = \{r_1, r_2, \dots, r_n\}
  3. args(参数)告诉动词“怎么做”,比如筛选条件、列名、新列的算式。
    args=(条件,列名,表达式)\text{args} = (\text{条件}, \text{列名}, \text{表达式})
  4. df\_new(新数据框)动词返回的新表,原表原封不动,每一步操作都可复查、可回退。
    dfnew=verb(df,args)且df不变\text{df}_{\text{new}} = \text{verb}(\text{df}, \text{args}) \quad \text{且} \quad \text{df} 不变

管道操作符 %>%:厨房里的流水线传送带

假设你是一名传菜员,每道菜要经过切配、烹炒、装盘三道工序,你需要在三个灶台之间来回跑,把半成品端来端去——很快你就会累垮。厨房真正高效的方案是一条传送带:把点菜单往上一放,它自己流经每个工位,每个师傅只管加工自己负责的那一环。R 里的管道操作符 %>% 就是这条传送带。

管道做的事极其简单:x %>% f(y) 完全等价于 f(x, y)——它把左边的值“喂”给右边的函数,作为第一个参数。当多个管道连在一起时,代码就从“层层嵌套、从里往外读”变成“从左到右、一行一步”的自然语言流。

来看一个对比。不使用管道时,你想“先筛选出单价超过 30 的菜,再按价格降序排序”,R 代码要写成 arrange(filter(df, price > 30), desc(price))——你得从最里层的 filter 开始读到最外层。使用管道后:

一眼就能看出数据流:df → filter → arrange,代码就是一个流程图的文字版。

传送带本身并不能让厨师变快,它只是免去了来回奔波的工夫,让注意力集中在每一道工序上。R 社区里,管道让 dplyr 动词的组合成为日常——接下来我们看看最常用的组合:分组汇总。

df  %>%  f()  %>%  g()=g(f(df))\text{df} \;\text{\%>\%}\; f() \;\text{\%>\%}\; g() = g(f(\text{df}))
管道的本质就是“把左边的结果自动喂给右边函数当第一个参数”,把嵌套调用展开成从左到右的流水线。
  1. df(数据)流水线的起点,一摞待处理的点菜单放进传送带。
    input=df\text{input} = \text{df}
  2. %>%(管道)传送带本身,把左侧结果传到右侧函数的第一个参数位置。
    a  %>%  f≡f(a)a \;\text{\%>\%}\; f \equiv f(a)
  3. f()(第一个工位)第一道工序,比如 filter 筛行。
    mid=f(df)\text{mid} = f(\text{df})
  4. g()(第二个工位)第二道工序,比如 arrange 排序。
    out=g(mid)\text{out} = g(\text{mid})
  5. = 等价符号管道链与嵌套调用的结果完全相同,它只是换了一种更易读的写法。
    out=g(f(df))\text{out} = g(f(\text{df}))
r1 行
df %>% filter(price > 30) %>% arrange(desc(price))

分组汇总:按桌结算的收银台

餐厅里最高效的结算方式,不是一盘菜一盘菜地算,而是把同一个桌号的所有点菜单用夹子夹在一起,然后飞快地算出这张桌子的总消费。group_by 就是那个夹子——它按某一列(比如桌号)把数据框划分成若干小组;summarise 则是收银台的算盘——针对每个小组计算均值、总和、计数等统计量。

group_by(df, table) 这一行代码单独运行,看起来什么都没发生:数据框的样子没有变。这是因为分组只是“打标签”——R 默默记住了这张表按 table 分成了几个组,等到后续的 summarise 或 mutate 到来时,再按照分组逐个计算。

summarise() 配合分组使用,输出的是“每组一行”的汇总表。最常用的三个助手是:mean() 求均值、sum() 求总和、n() 统计本组行数。n() 是 dplyr 专属的计数函数,不需要任何参数,只要在 summarise 内调用即可。

分组汇总让“一桌一结”变得清爽。但现实中的数据往往不是恰好长成你需要的形状——一张表可能列太多(宽),也可能行太多(长)。这时候就需要 tidyr 出马了。

xˉg=1ng∑i=1ngxg,i\bar{x}_g = \frac{1}{n_g} \sum_{i=1}^{n_g} x_{g,i}
公式的含义是:第 g 组(比如 1 号桌)的平均消费 = 组内所有消费之和 ÷ 组内记录条数。
  1. g(分组标识)表示某个小组,比如桌号为 1 的所有订单集合。
    g∈{1,2,…,K}g \in \{1, 2, \dots, K\}
  2. x\_g,i(组内观测值)某个特定订单的金额——第 g 组里的第 i 条记录。
    xg,i=第 g 组第 i 条记录的消费x_{g,i} = \text{第 } g \text{ 组第 } i \text{ 条记录的消费}
  3. Σ(求和)把组内所有金额加起来,相当于收银员把一桌订单的金额逐行相加。
    ∑i=1ngxg,i=xg,1+xg,2+⋯+xg,ng\sum_{i=1}^{n_g} x_{g,i} = x_{g,1} + x_{g,2} + \dots + x_{g,n_g}
  4. n\_g(组内样本量)第 g 组的记录条数;n() 函数在 summarise 里就返回这个数字。
    ng=第 g 组的行数n_g = \text{第 } g \text{ 组的行数}
  5. x̄\_g(组均值)第 g 组的平均消费,summarise 输出的核心统计量。
    xˉg=Sgng,Sg=∑i=1ngxg,i\bar{x}_g = \frac{S_g}{n_g}, \quad S_g = \sum_{i=1}^{n_g} x_{g,i}

长宽转换:储物柜的两个收纳方向

每个厨房都有一个储物柜。一种收纳方式是“宽柜格”:每个柜格标明一种食材(鸡肉、牛肉、蔬菜),格子里放着不同的数量;另一种是“长清单”:一张流水账,每行一条记录——食材、数量、时间,一行一个条目。宽柜格适合人去找某一种食材,长清单适合机器核对总量。tidyr 的 pivot_longer 和 pivot_wider 就是在这两种形态之间互相转换的工具。

一张“宽表”通常把多个相关变量并列成多列,比如三列分别是 2023 年、2024 年、2025 年的销售额。pivot_longer() 把这三列“堆叠”成两列:一列叫“年份”(原来的列名变成值),一列叫“销售额”(原来的数值搬进去)。转换后,三列变两列,行数变为原来的三倍。

反过来,pivot_wider() 把长表中的“名称列”重新拆成多个新列,长表变宽表。两者互为逆操作。为什么要转来转去?因为画图(比如 ggplot2)和数据建模更喜欢长表,报表展示更喜欢宽表。数据本身没变,变的只是摆放的姿势。

储物柜的两种收纳方向,让你的数据能按需切换姿势。但还有些时候,你不想挨个处理每一列,而是想对一批列同时做同样的事——这就是 across 的用武之地。

dflong=pivot_longer(dfwide)  ⇔  dfwide=pivot_wider(dflong)\text{df}_{\text{long}} = \text{pivot\_longer}(\text{df}_{\text{wide}}) \;\Leftrightarrow\; \text{df}_{\text{wide}} = \text{pivot\_wider}(\text{df}_{\text{long}})
长宽转换是一对可逆操作:pivot_longer 把宽表“叠”成长表,pivot_wider 把长表“摊”回宽表。
  1. df\_wide(宽表)多列并列的表,人眼阅读方便。
    dfwide={列1,列2,…,列k}\text{df}_{\text{wide}} = \{\text{列}_1, \text{列}_2, \dots, \text{列}_k\}
  2. pivot\_longer(变长)把指定的多列合起来,列名变成一列“名称”,列值变成一列“数值”。
    dflong=stack(dfwide)\text{df}_{\text{long}} = \text{stack}(\text{df}_{\text{wide}})
  3. df\_long(长表)只有“名称-值”两个关键列,行数增加,便于机器处理。
    dflong={(名称j,值j),  j=1,…,N}\text{df}_{\text{long}} = \{(\text{名称}_j, \text{值}_j), \; j = 1, \dots, N\}
  4. ⇔(可逆符号)两种转换互为逆运算,信息不丢失。
    pivot_wider(pivot_longer(D))=D\text{pivot\_wider}(\text{pivot\_longer}(D)) = D
  5. pivot\_wider(变宽)把“名称”列的值拆回多个新列,恢复宽表。
    dfwide=unstack(dflong)\text{df}_{\text{wide}} = \text{unstack}(\text{df}_{\text{long}})

跨列操作 across:一勺盐撒一排菜

后厨出菜高峰时,一排五道菜同时等着调味。有经验的师傅不会一盘一盘撒盐,而是左手端盐碗、右手捏盐勺,从第一盘一路撒到第五盘——一次操作,五盘到位。across() 就是这把“一勺盐撒一排菜”的手:在 dplyr 里,它对选中的多列批量应用同一个函数,省去逐个重复书写。

across() 通常跟 mutate() 或 summarise() 配合使用。最经典的写法:

这行代码把数据框里所有数值型列都做标准化(scale)。同理,也可以用 across(c(price, qty), log) 对指定两列取对数。where(is.numeric) 是一个“列选择器”——它像一个自动拣选的手,把满足条件的列挑出来批量处理。

在 summarise() 中使用 across 的典型场景是批量求均值:df %>% summarise(across(where(is.numeric), mean)),一下子得到所有数值列的平均值,不必为每一列写一次 mean(col)。

有了 across,批量加工变成一行代码。但现实中的数据往往散落在多张表里,分析之前要先把它们拼成一张大表——这就要用到 join 系列了。

across(cols,f)={ f(colj)∣colj∈cols }\text{across}(\text{cols}, f) = \{\, f(\text{col}_j) \mid \text{col}_j \in \text{cols} \,\}
公式的意思是:对 cols 集合里的每一列 col_j,都执行一次函数 f,得到一组结果。
  1. cols(列集合)被批量处理的列,可以用 c() 列举,也可以用 where(is.numeric) 按类型挑。
    cols={col1,col2,…,colk}\text{cols} = \{\text{col}_1, \text{col}_2, \dots, \text{col}_k\}
  2. f(函数)要批量执行的加工,比如 mean、log、scale。
    f∈{mean,log,scale,… }f \in \{\text{mean}, \text{log}, \text{scale}, \dots\}
  3. col\_j(某列)集合中的一列,比如 price 列或 qty 列。
    colj={值1,值2,…,值n}\text{col}_j = \{\text{值}_1, \text{值}_2, \dots, \text{值}_n\}
  4. 集合推导式对每一列执行 f,输出结果列表;这是“撒盐”动作的数学化描述。
    result={f(col1),f(col2),… }\text{result} = \{ f(\text{col}_1), f(\text{col}_2), \dots \}
r1 行
df %>% mutate(across(where(is.numeric), scale))

多表连接 join:把两本账册拼成一桌宴席

会计要对账时,手上有两本册子:一本记录每张订单的桌号和菜品,另一本记录每张订单的结算金额。要得到“哪个桌号消费了多少”,必须把两本册子按相同的“单号”对齐——这就像把两张分开的点菜单用订书机钉在一起。R 里的 join 系列函数就是这个订书机。

dplyr 提供了一整套连接函数:left_join() 保留左表的所有行,把右表的匹配信息补到右边,右表没匹配上的就填 NA;inner_join() 只保留两表都能匹配上的行;right_join() 是 left_join 的镜像,保留右表全部行;full_join() 保留两表的全部行。四个函数,核心都是“按共同的列(键 key)把两行对齐”。

最常用的是 left_join(df1, df2, by = c("order_id" = "订单号"))。by 参数指定对齐的键:当两个表的键列名一样时写 by = "id",不一样时可以写成 by = c("a" = "b")。连接后,原本散落两张表的信息终于回到了同一张表里,后续的 filter、mutate 就能统一处理了。

把多张表拼在一起之后,数据总算齐全了。不过,翻开任何一本真实的账册,总有几行字迹模糊、被汤汁沾污——这就是缺失值问题。

left_join(df1,df2,by=key)\text{left\_join}(\text{df}_1, \text{df}_2, \text{by} = \text{key})
这个式子表示把 df1 当主表,按 key 列去 df2 里找匹配行,把找到的字段拼到右侧。
  1. df1(左表/主表)连接结果以它为基础,它的每一行都会保留。
    df1=主表\text{df}_1 = \text{主表}
  2. df2(右表/附表)按 key 匹配 df1,提供要补充的字段。
    df2=附表\text{df}_2 = \text{附表}
  3. by = key(连接键)两个表共同的列名,就像两本账册里相同的“订单号”。
    key=共同列\text{key} = \text{共同列}
  4. 结果(拼接)df1 每行找一个匹配的 df2 行,并排拼接;匹配不到就补缺失值。
    res={(r1,r2)∣r1.key=r2.key}∪{未匹配的 r1}\text{res} = \{(r_1, r_2) \mid r_1.\text{key} = r_2.\text{key}\} \cup \{\text{未匹配的 } r_1\}

缺失值处理:涂改不清的菜单怎么办

点菜单在厨房里传递久了,总有几行被油渍覆盖、字迹模糊——可能看不清辣度,可能忘了填数量。面对这样的单子,厨师有两个选择:要么把整张看不清的单子扔掉(drop_na),要么根据经验把缺的信息补上(replace_na)。数据清洗里的缺失值(NA)处理,正是这两种思路。

先要“发现”缺失值。is.na(df) 会返回一个跟 df 同形状的 TRUE/FALSE 矩阵,TRUE 表示那个位置是缺失值。更直观的检查是 df %>% summarise(across(everything(), ~ sum(is.na(.)))),它能数出每一列有多少个 NA。

处理缺失值的两个主力函数:drop_na() 默认删除任何含 NA 的行,也可以指定列 drop_na(df, price) 只针对 price 列为 NA 的行做删除;replace_na() 则把 NA 替换成指定值,比如 replace_na(df, list(price = 0)) 把 price 列的缺失改成 0。此外,很多统计函数都带 na.rm 参数——mean(df$price, na.rm = TRUE) 会在计算前剔除缺失值,等价于“跳过字迹不清的单子再算账”。

至此,从筛选、选列、排序,到分组汇总,再到长宽转换、跨列批量操作、多表连接、缺失值清洗——这一套流水线就是数据分析中必做的“数据准备”(data wrangling)。有了 dplyr 和 tidyr 这对黄金搭档,再 messy 的数据也能收拾得干干净净,像从传送带末端端出的一盘盘规整好菜。

dfclean=drop_na(df)或dffilled=replace_na(df,值)\text{df}_{\text{clean}} = \text{drop\_na}(\text{df}) \quad \text{或} \quad \text{df}_{\text{filled}} = \text{replace\_na}(\text{df}, \text{值})
清洗缺失值只有两条路:删掉有问题的行,或者用指定值补上缺失,两者都让后续计算不受 NA 干扰。
  1. df(原始表)含有 NA 的数据框,就像沾了油污的菜单。
    df=含缺失值的表\text{df} = \text{含缺失值的表}
  2. drop\_na(删行)把含缺失值的整行丢弃,简单粗暴但有效。
    dfclean=drop_na(df)  ⇒  每行无 NA\text{df}_{\text{clean}} = \text{drop\_na}(\text{df}) \;\Rightarrow\; \text{每行无 NA}
  3. replace\_na(填充)把指定列里的 NA 换成给定值,保留整行数据。
    dffilled=replace_na(df,list(price=0))\text{df}_{\text{filled}} = \text{replace\_na}(\text{df}, \text{list}(\text{price} = 0))
  4. 值(填充值)填 0 还是填均值,取决于业务含义——就像有些菜填“微辣”还是“不辣”要问客人。
    fill=业务上合理的替代值\text{fill} = \text{业务上合理的替代值}
  5. clean / filled(清洗结果)两种做法的目标都是让数据不再有 NA,后续计算才安稳。
    dfclean 或 dffilled 均不含 NA\text{df}_{\text{clean}} \text{ 或 } \text{df}_{\text{filled}} \text{ 均不含 NA}

AI 应用场景

电商订单数据清洗流水线

阶段:数据清洗阶段

解决的问题:传统方式是用 Excel 手工打开导出文件、逐列删除、逐行筛选,每次报表都要重来一遍,效率低且容易出错。AI 数据管道要求每天自动处理新增数据,dplyr/tidyr 的动词流水线可以一次性写成可复用的清洗脚本。

具体案例:某电商公司每天导出的订单表有 80 列,分析师只用到其中的 10 列,且每月有约 2% 的记录存在缺失或重复。分析师用 select() 挑出关键列、filter() 剔除无效订单、drop_na() 清掉缺失信息、mutate() 计算金额小计,最后把这套操作写成一个 R 脚本,每天定时跑一遍,数据到底时已是干净整齐的分析底稿。

金融风控特征工程中的多表拼接与批量标准化

阶段:特征工程阶段

解决的问题:建模前需要把用户基础信息、交易流水、历史逾期记录三张表合并成一张宽表。传统做法是用 SQL 手工写多个 JOIN,字段一多就难以维护。dplyr 的 join 系列配合 across 可以在一段管道里完成拼表、选列、标准化。

具体案例:某风控团队用 left_join() 按用户 ID 把三张表拼在一起,用 mutate(across(where(is.numeric), scale)) 对所有数值型特征做标准化,再用 group_by(user_id) %>% summarise(...) 生成用户级聚合特征,整个特征制作过程从两天缩短到两小时。

医学临床试验数据的长宽转换

阶段:数据预处理阶段

解决的问题:临床试验数据常以宽表存储(每位患者一行,每个访视时间点一列),但统计建模和绘图需要长表(每个患者-时间点一行)。手工转置极易出错,pivot_longer 可以精确、可逆地完成转换。

具体案例:某肿瘤临床试验记录了患者在第 1、2、3 个月的肿瘤尺寸,原始数据是每个时间点一列的宽表。统计分析师用 pivot_longer(cols = c(m1, m2, m3), names_to = "month", values_to = "size") 转成长表,再用 ggplot2 绘制每个患者的生长曲线,全程不到十行代码。

Python 演示

python32 行
# 用 pandas 模拟 dplyr 核心流水线import pandas as pd  def make_orders():    # 原始订单表: 订单号, 桌号, 菜品, 单价, 数量, 是否缺货    orders = [        {"order_id": "A01", "table": 1, "dish": "kung_bao", "price": 38, "qty": 2, "out_of_stock": False},        {"order_id": "A02", "table": 1, "dish": "rice", "price": 3, "qty": 4, "out_of_stock": False},        {"order_id": "A03", "table": 2, "dish": "pork", "price": 58, "qty": 1, "out_of_stock": True},        {"order_id": "A04", "table": 2, "dish": "veg", "price": 22, "qty": 2, "out_of_stock": False},    ]    return pd.DataFrame(orders)  def main():    df = make_orders()    # 管道式数据处理: 筛选 -> 新列 -> 分组汇总    result = (        df        # filter: 剔除缺货菜品        .query("out_of_stock == False")        # mutate: 为每道菜计算小计        .assign(subtotal=lambda d: d["price"] * d["qty"])        # group_by 桌号 + summarise 每桌小计之和        .groupby("table", as_index=False)["subtotal"].sum()    )    print(result)  if __name__ == "__main__":    main()
目的:用 pandas 的链式调用复刻 dplyr 的“筛选 → 生成新列 → 分组汇总”流水线,直观展示 filter/mutate/group_by+summarise 三组动词的对应关系。
  1. make_orders()构造一份示例订单表,对应厨房里的原始点菜单。
  2. df数据框对象,等价于 R 的 data.frame,是流水线起点。
  3. .query("out_of_stock == False")等价于 filter(),按行条件筛掉缺货菜品。
  4. .assign(subtotal=lambda d: d["price"] * d["qty"])等价于 mutate(),用现有列算出新列“小计”。
  5. .groupby("table", as_index=False)["subtotal"].sum()等价于 group_by() + summarise(),按桌号汇总小计之和。
  6. print(result)输出最终结果,每一桌的消费总额一目了然。

常见误区

误区“我想把数据里某一列留下来,于是写了 filter(df, 列名),结果输出是空的。filter 不是过滤吗?怎么把整列过滤掉了?”

正解filter 是“按行筛”,它检查每一行的值是否满足条件;select 才是“按列挑”。想留列请写 select(df, 列名)。两者的分工就像漏勺捞食材和拣菜盘挑配料,一个管“哪些行留下”,一个管“哪些列留下”。

为何常见:SQL 里 select 同时管行和列,很多新手把数据库经验直接搬进 R,忽略了 dplyr 把动词拆分得更细。

误区“我写了 `df %>% filter(df$price > 30) %>% select(df$name)`,结果报错找不到列。管道不是已经传了 df 吗?”

正解管道已把 df 作为第一个参数传给下一步,在函数内部应该直接用列名 price 和 name,不要再写 df$price。管道传送带已经把菜单送过来了,你只需要在菜单上写字,不必再把整本菜单从传送带上拿下来。

为何常见:习惯了用 $ 取列,总觉得“不写全名不放心”,其实管道里的数据就是当前那一步的输入,引用列名足够。

误区“我运行了 `group_by(df, table)`,数据一点变化都没有,是不是函数根本不起作用?”

正解group_by 只做“打分组标签”,真正起效要等后续的 summarise 或 mutate。这是 R 把“分组状态”和“聚合计算”拆成两步的优雅设计——夹子夹好单子后,算盘还没拨,自然看不出变化。

为何常见:SQL 的 GROUP BY 一写就立即聚合,而 R 的 group_by 是惰性的,等待后续操作触发,新手看到“没变化”就容易误判。

误区“我用 pivot_longer 把三列并成一列之后,原来的列名不见了,是不是数据被破坏了?”

正解列名变成了“名称”列里的值,信息完整无损,只是换了个存放位置。用 pivot_wider 可以完全还原。就像把宽柜格的标签摘下来贴到长清单上,食材没有少,只是整理方式变了。

为何常见:人眼习惯了表头,看到列名消失就慌,其实表格的价值在信息本身,不在表头的位置。

误区“我辛辛苦苦清洗完数据,`mean(df$price)` 却返回 NA,以为是数据全坏了。”

正解R 的均值默认不舍弃缺失值——只要数据里有 NA,结果就是 NA,这是刻意为之,为了提醒你数据里还有“字迹不清的单子”。需要 na.rm = TRUE 才会跳过 NA 计算。drop_na 和 replace_na 是在源头处理,na.rm 是在计算时绕过。

为何常见:Excel 的 AVERAGE 会自动忽略空白单元格,R 更严谨,默认行为不同,习惯 Excel 的人最容易踩这个坑。

面试题

Q1. 在 dplyr 中,filter() 函数的作用是什么?

单选题 · easy

  • A.筛选符合条件的行← 正确答案
  • B.挑选指定的列
  • C.对数据表按列排序
  • D.生成一个新的计算列

答案:A。filter 按行筛选,就像漏勺捞食材;select 才是选列,arrange 是排序,mutate 是生成新列。四者各司其职,不能混淆。

Q2. df %>% filter(price > 10) %>% select(name) 等价于以下哪个?

单选题 · mid

  • A.select(filter(df, price > 10), name)← 正确答案
  • B.filter(select(df, name), price > 10)
  • C.select(df, name) %>% filter(price > 10)
  • D.select(filter(df, price > 10), price)

答案:A。管道从左到右执行,先 filter 再 select,等价于先内层后外层的嵌套写法 A。其他选项要么调换了顺序,要么选错了列。

Q3. 关于管道操作符 %>% 的说法,哪些是正确的?

多选题 · mid

  • A.把左值作为右函数第一个参数← 正确答案
  • B.让代码从左到右自然阅读← 正确答案
  • C.会直接修改原始数据框
  • D.可以减少中间变量的使用← 正确答案

答案:ABD。管道是传送带,把结果传给下一个函数当第一个参数,也让代码可读性更好、少写中间变量。但它不会修改原数据框,C 错误。

Q4. group_by(df, city) 单独运行后会发生什么?

单选题 · mid

  • A.数据框按 city 排序
  • B.数据框按 city 去重
  • C.看起来没有明显变化← 正确答案
  • D.每行被合并成一组

答案:C。group_by 只打分组标签,类似用夹子把同一桌的单子夹好,但还没算账。必须等后续 summarise 或 mutate 才会看到分组效果。

Q5. summarise() 中哪个函数可以统计组内行数?

单选题 · easy

  • A.count()
  • B.n()← 正确答案
  • C.length()
  • D.size()

答案:B。n() 是 dplyr 专属的无参计数函数,在 summarise 里调用即可返回组内行数。count() 是另一套快捷 API,length() 需要传参数,size() 不存在于 dplyr。

Q6. 关于 pivot_longer() 的效果,正确的描述是?

多选题 · mid

  • A.把多列压缩成“名称-值”两列← 正确答案
  • B.把长表还原成宽表
  • C.列数减少、行数增加← 正确答案
  • D.自动删除缺失值

答案:AC。pivot_longer 把宽表变长表,多列堆叠成“名称-值”两列,列数减少、行数增加。还原宽表要用 pivot_wider;删除缺失值是 drop_na 的职责。

Q7. 在 mutate() 内使用 across() 的效果是什么?

单选题 · hard

  • A.对选中的多列执行同一个函数← 正确答案
  • B.对所有行逐行执行函数
  • C.只对第一列执行函数
  • D.生成交叉验证的结果

答案:A。across(cols, fun) 对 cols 中的每一列应用 fun,像一勺盐撒一排菜。它作用于列,不是行;也不是只处理第一列,更不是交叉验证。

Q8. left_join(df1, df2, by = "key") 返回什么结果?

单选题 · mid

  • A.只返回两表都匹配的行
  • B.返回 df1 全部行加 df2 匹配列← 正确答案
  • C.返回 df2 全部行加 df1 匹配列
  • D.返回两表的笛卡尔积

答案:B。left_join 以左表为主,df1 每一行都保留,df2 只提供匹配行的字段;匹配不到的补 NA。A 是 inner_join,C 是 right_join,D 是 cross_join。

Q9. 下列哪个函数能把缺失值填充为指定值?

单选题 · easy

  • A.drop_na()
  • B.replace_na()← 正确答案
  • C.na.omit()
  • D.is.na()

答案:B。replace_na() 用指定值替换 NA,像给涂改不清的菜单补上信息。drop_na 是删除含 NA 的行,na.omit 是 base R 里的删行函数,is.na 只是做缺失检查。

Q10. 关于 dplyr 动词的共同特点,正确的是?

多选题 · hard

  • A.不修改原始数据框← 正确答案
  • B.返回新的数据框← 正确答案
  • C.必须配合管道才有效
  • D.第一个参数总是数据框← 正确答案

答案:ABD。dplyr 动词的第一个参数都是数据框,输出新数据框且不改原数据。管道只是提升可读性的语法糖,不是必需的——单独调用动词同样有效,C 错误。