阅读mutate源码学习dplyr

dplyr是R语言里面处理数据数据非常好使的包,但是最近使用它解决一些问题时遇到了瓶颈,并且搜到的教程都特别基础,所以我打算从源码的角度去找解决方案。

为了理解dplyr::mutate这个函数,我们需要借助一个实例,分别思考mutate(mtcars), mutate(mtcars, gear+carb)mutate(mtcars, new=gear+carb)会在运行的时候的处理流。

在R语言中直接输入函数的名字,就能看到"mutate"的源码

> dplyr::mutate
function (.data, ...) 
{
    UseMethod("mutate")
}
<environment: namespace:dplyr>

这告诉我们,"mutate"其实是一个泛型函数( generic function),因为它使用UseMethod()调用和数据结构对应的"mutate". 我们可以用”methods“去找给定泛型函数的所有实现方法

PS: 如果你输入的不是数据框或者tbl_df,那么泛型函数就会报错, 因为找不到对应的

> methods(mutate)
[1] mutate.data.frame* mutate.default*    mutate.tbl_df* 

由于"mtcars"是数据框,那么UseMethod就会选择mutate.data.frame作为实际使用的函数。但是你不能通过直接在命令里输这些mutate.data.frame来查看它的源代码,这是因为*标注的是不可见函数(nonvisible function),就是不在默认命名空间中的而函数,,你需要用getAnywhere()找到这些函数,然后使用命名空间限定符来访问。

> getAnywhere(mutate.data.frame)
A single object matching ‘mutate.data.frame’ was found
It was found in the following places
  registered S3 method for mutate from namespace dplyr
  namespace:dplyr
with value

function (.data, ...) 
{
    as.data.frame(mutate(tbl_df(.data), ...))
}

这说明对于data.frame类的数据,会先用tbl_df更改数据结构,重新调用mutate函数。那么对于tbl_df类,泛型函数就会调用mutate.tbl_df*

> getAnywhere(mutate.tbl_df)
A single object matching ‘mutate.tbl_df’ was found
It was found in the following places
  registered S3 method for mutate from namespace dplyr
  namespace:dplyr
with value

function (.data, ...) 
{
    dots <- named_quos(...)
    mutate_impl(.data, dots)
}

第一个函数named_quos有两个作用,第一返回quosure类,第二保证quosure类都是由名字的,所以mutate(mtcars, gear+carb)的新增列的名字就是gear+carb.

第二个函数调用了mutate_impl,以.data和dots作为输入,这个函数也是不可见的,所以也要用getAnywhere

> getAnywhere(mutate_impl)
A single object matching ‘mutate_impl’ was found
It was found in the following places
  namespace:dplyr
with value

function (df, dots) 
{
    .Call(`_dplyr_mutate_impl`, df, dots)
}

.Call函数是C/C++代码的交互界面,负责调用_dplyr_mutate_impl模块,传入的就是数据框和dots对象。

R语言部分的代码到此就结束了,因为后续就是调用C/C++代码编译后的函数。
这部分的代码在GitHub上托管,https://github.com/tidyverse/dplyr/blob/master/src/mutate.cpp。虽然我几乎没用C/C++写代码,但还能勉强看代码

接着之前_dplyr_mutate_impl,对应代码如下

// [[Rcpp::export]]
SEXP mutate_impl(DataFrame df, QuosureList dots) {
  if (dots.size() == 0) return df;
  check_valid_colnames(df);
  if (is<RowwiseDataFrame>(df)) {
    return mutate_grouped<RowwiseDataFrame, LazyRowwiseSubsets>(df, dots);
  } else if (is<GroupedDataFrame>(df)) {
    return mutate_grouped<GroupedDataFrame, LazyGroupedSubsets>(df, dots);
  } else {
    return mutate_not_grouped(df, dots);
  }
}

SEXP类由Rcpp包提供,让R包能够方便的使用.Call和C/C++代码交互,这样子就不需要写专门的代码将R语言的数据结构转换成C++数据结构。

然后判断quosure列表的长度,数据框是否存在无效的列名,是否需要分组计算。当长度为0时返回原来的数据框。后面就是具体运算的代码,读起来真的是费劲,但是对于我而言,只需要了解 QuosureList dots 最后是如何被使用的就行。

dots只是存放表达的中间态,随后会经由循环传给NamedQuosure类,后续这些指令传给call_proxy,而这个类来自于"#include <dplyr/Result/CallProxy.h>"。不能再继续了,因为此恨绵绵无绝期,继续就是Rcpp这个无底洞,放弃吧。

总结一句:理解dplyr包的关键在于,你得知道dplyr包本身不参与的数据处理,它只是生成SQL语言转述给后端的数据库,让数据库完成数据处理部分。换句话说,它对SQL语句的简洁封装,实现前后端分离。

通过读源代码的方式,我理解到掌握mutate的核心其实学会dplyr编程,学会将你需要执行的表达式传递给dots,你就能自由的使用mutate甚至是其他所有dplyr系列。

mutate拓展函数: mutate_if,mutate_all,mutate_at就是学习mutate的最好案例

这三个函数虽然看起来不同,但是殊途同归,最后都是mutate(.tbl, !(!(!funs))),funs虽然由看似不同的manip_ifmanip_at, manip_all构建,但是本质上都是manip_apply_sym的变体而已。接下来用mutate_all(mtcars, funs(mean)的执行过程来辅助理解。

首先.tbl=mtcars.funs=funs(mean)设置函数内局部变量,

然后调用manip_all,源代码如下

manip_all <- function (.tbl, .funs, .quo, .env, ...) 
{
  syms <- syms(tbl_nongroup_vars(.tbl))
# 这一步得到目标处理列列名的符号列表
  funs <- as_fun_list(.funs, .quo, .env, ...)
# 这里的.funs会是funs(./2.54)运算后的fun_list, 而.quo则是funs(./2.54本身,是一个quosure类
#.env则表示当前所处环境,最后将所有提供的函数合并成一个fun_list。
  manip_apply_syms(funs, syms, .tbl)
}

syms以列表形式存储待处理的列名的符号(不是字符串),funs以fun_calls数据结构存放要执行的函数。最后这些参数连同数据框本身传给manip_apply_syms,返回一个列表,这个列表记录着需要执行的运算。

syms <- syms(tbl_nongroup_vars(mtcars))
funlist <- dplyr:::as_fun_list(funs(mean), quo(mean), caller_env())
dplyr:::manip_apply_syms(funlist,syms, mtcars)
# 运行结果
$mpg
<quosure>
  expr: ^mean(mpg)
  env:  global
$cyl
<quosure>
  expr: ^mean(cyl)
  env:  global
...
$carb
<quosure>
  expr: ^mean(carb)
  env:  global

最后就会出现一个非常神奇的表达形式!(!(!funs)), 我目前还不知道有什么用。可能是rlange !!!,用于将列表的内容解压,所以下面两个表达是等价的

# 表达一
mutate_all(mtcars,funs(mean)
# 表达二
syms <- syms(tbl_nongroup_vars(mtcars))
funlist <- dplyr:::as_fun_list(funs(mean), quo(mean), caller_env())
rs <- dplyr:::manip_apply_syms(funlist,syms, mtcars)
mutate(mtcars,!!!rs)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容