ggplot代码复用:清爽,太清爽了!
你的 ggplot
绘图脚本是否已经臃肿不堪、难以维护?曾经实现的功能不知如何复用?本文的目的是解决这些问题,让你的绘图脚本从此清清爽爽。
本文涉及赋予变量、自定义函数、扩展 ggplot
三大技巧,前两种使用门槛较低,刚入门 ggplot
的朋友也可以很快掌握。扩展 ggplot
会涉及面向对象编程,但它的功能也最强大——你可以控制画面上任意一个点、一条线,这正是 ggplot
众多扩展包使用的技巧。
掌握一些辅助函数,对理清 ggplot
对象非常有帮助。例如:
- 对于一个函数,如
element_text()
, 直接输入函数名element_text
会显示函数代码,加问号?element_text
会显示函数文档,element_text()
会得到默认返回值。 - 对于一个对象,
class()
获取其类名,str()
获取其内部结构。 - 使用
waldo::compare()
比较对象前后差异。
赋予变量
library(tidyverse)
ggplot(mpg, aes(x=class, y=cty)) +
geom_jitter(aes(fill=class), shape=21, color='grey50', width=0.2) +
geom_errorbar(stat='summary', fun.min=mean, width=0.4) +
geom_errorbar(stat='summary', fun.min=~mean(.x)-sd(.x), fun.max=~mean(.x)+sd(.x), width=0.3) +
scale_fill_brewer(palette='Accent') +
coord_flip() +
theme_classic(base_size=10) +
theme(
axis.text.x=element_text(angle=45, hjust=1, vjust=1),
axis.title=element_blank()
)
以上是一个标准的绘图程序,创建画布、绘制散点图层、绘制均值标记图层、绘制标准差标记图层、调整 fill
刻度、坐标轴变换、设定基础主题、调整主题。实际上,被 +
分隔的每一项都可以赋值给变量,这意味着将变量自由组合,就可以在不同绘图任务中复用。
a <- ggplot(mpg, aes(x=class, y=cty))
b <- geom_jitter(aes(fill=class), shape=21, color='grey50', width=0.2)
c <- geom_errorbar(stat='summary', fun.min=mean, width=0.4)
d <- geom_errorbar(stat='summary', fun.min=~mean(.x)-sd(.x), fun.max=~mean(.x)+sd(.x), width=0.3)
e <- scale_fill_brewer(palette='Accent')
f <- coord_flip()
g <- theme_classic(base_size=10)
h <- theme(
axis.text.x=element_text(angle=45, hjust=1, vjust=1),
axis.title=element_blank()
)
a + b + c + d + e + f + g + h
除了每一项单独相加,也可以将多项存储到 list
中:
xx <- list(
geom_errorbar(stat='summary', fun.min=mean, width=0.4),
geom_errorbar(stat='summary', fun.min=~mean(.x)-sd(.x), fun.max=~mean(.x)+sd(.x), width=0.3)
)
a + b + xx + e + f + g + h
预设的变量也可以存储到 list
中,然后用 do.call()
调用:
tlist <- list(axis.text.x=element_text(angle=45, hjust=1, vjust=1),
axis.title=element_blank())
h <- do.call('theme', tlist)
通过 class()
和 str()
可以观察到:
- 成品图是
ggplot
对象,而,ggplot(...)
创建画布这个步骤,返回的已经是ggplot
对象。a$data
可以取出tibble
对象的数据集,a$mapping
可以取出uneval
对象的美学映射。 - 三个图层都是
Layer
对象,继承自ggproto
类。b$data, b$mapping, b$geom, b$stat, b$position
可以分别取出tibble, uneval, Geom, Stat, Position
对象,对应图层的五大要素,后三者都继承自ggproto
类。b$constructor
可取出代码语句。 - 美学刻度调整是
Scale
对象,继承自ggproto
类。e$aesthetics
返回调节的具体刻度,e$call
可检查调用的底层scale
函数。 - 坐标轴转换是
Coord
对象,继承自ggproto
类。 - 基础主题、主题调整都是
theme
对象。
自定义函数
我们可以将绘制均值、标准差两个图层封装到一个函数中,并允许选择是否绘制标准差
geom_meansdbar <- function(sd=TRUE) {
list(
geom_errorbar(stat='summary', fun.min=mean, width=0.4),
if (sd) {geom_errorbar(stat='summary',
fun.min=~mean(.x)-sd(.x), fun.max=~mean(.x)+sd(.x), width=0.3)}
)
}
a + b + geom_meansdbar() + e + f + g + h
如果需要调整不同图层的参数,相比于直接增加 geom_meansdbar()
的参数,更合理的方式是
geom_meansdbar <- function(..., sd=TRUE, mean_params=list(), sd_params=list()) {
params <- list(stat='summary', ...)
mean_params <- modifyList(mean_params,
c(params, fun.min=mean, width=0.4))
sd_params <- modifyList(sd_params,
c(params, fun.min=~mean(.x)-sd(.x), fun.max=~mean(.x)+sd(.x), width=0.3))
mean_bar <- do.call('geom_errorbar', mean_params)
sd_bar <- if (sd) do.call('geom_errorbar', sd_params)
list(mean_bar, sd_bar)
}
a + b + geom_meansdbar() + e + f + g + h
这种写法明确区分不同图层的参数,同时用 ...
接收多个图层的共享参数。
有时你可能想仅指定 data, x, y
,让函数直接返回一整幅图。mapping=aes(...)
内遵循数据屏蔽(data masking)规则,不需要通过 x=mpg[['class']]
的形式指定列,这在大多数时候方便了我们的数据操作,而它的代价之一是出现了两种类型的变量:环境变量(environment variable)和数据变量(data variable),显然 aes()
使用数据变量。在自定义函数中,需要使用 {{x}}
的形式引用数据变量。
myplot <- function(data, x, y) {
ggplot(data, aes(x={{x}}, y={{y}})) +
geom_jitter(aes(fill=class), shape=21, color='grey50', width=0.2) +
geom_meansdbar() +
scale_fill_brewer(palette='Accent') +
coord_flip() +
theme_classic(base_size=10) +
theme(
axis.text.x=element_text(angle=45, hjust=1, vjust=1),
axis.title=element_blank()
)
}
myplot(mpg, class, cty)
扩展ggplot
当需求进一步复杂,且没有现成的包可以使用时,可能需要自行扩展 ggplot
。最有用的扩展是定义新的统计变换(StatXXX
类)和几何对象(GeomXXX
类),这些都离不开 ggproto
面向对象系统。
ggproto面向对象系统
由于历史原因,ggplot
使用独特的面向对象系统 ggproto
,该系统习惯的命名方式是驼峰法。在对象内部存储的数据称为字段(fields),函数称为方法(methods)。
# 新建类
Person <- ggproto('Person', NULL,
name = NA,
gender = NA,
description = function(self) {
paste(self$name, self$gender, sep=', ')
}
)
# 类继承
Student <- ggproto('Student', Person,
univ = NA,
major = NA,
school = function(self) {
paste(self$univ, self$major, sep=', ')
}
)
# 实例化
william <- ggproto(NULL, Student,
name = 'william',
gender = 'male',
univ = 'Peking Univ',
major = 'Biology'
)
# 提取字段
william$name
# 调用方法
william$school()
需要注意的是,ggproto
面向对象系统仅在 ggplot
中使用,而且 ggplot
也不会用到该系统的所有功能。在扩展 ggplot
时,并不建议从头创建新类,继承已有的类就足矣实现各种目标。
自定义Stat类
新建名为 StatNewstat
、继承自 Stat
的 ggproto
类,就能以 geom_xxx(stat='newstat')
的形式调用自定义的统计变换。
在继承时,类属性 required_aes
指定了需要映射的刻度,几个类方法非常关键:
setup_params = function(data, params)
方法,提前算好后续计算需要的参数。例如密度图中,考虑不同构面的数据,最终计算出一个合适的带宽。函数内可通过params$var
引用在layer(params=list(...))
中指定的参数,但有个限制条件,这个参数必须在compute_group(...)
中也出现,否则找不到。setup_data = function(data, params)
方法,对数据进行预处理,默认应用到整张数据表上。这里的参数已经被setup_params()
修改过了。compute_group = function(data, scales)
方法,对数据进行最终的统计变换,默认应用到被group
列分割后的各子表上。可以在function(data, scales, var1, var2, ...)
参数列表中直接写layer(params=list(...))
中指定的参数。其实setup_data
可以完成等效操作,只需额外划分组别。
未在 geom_xxx(mapping=aes(...))
显式指定 group
的映射时,该列会自动生成(早在 setup_params()
之前),为数据分配组别。
可从 scales
获取一系列有用的信息,例如 scales$y$range$range
获取 y
轴数据范围,scales$y$trans$name
获取 y
轴使用的尺度变换(如 scale_y_log10(), scale_y_reverse()
),scales$x$map('x1')
获取离散 x
变量值对应 x
轴坐标。
下列代码定义了一个 Stat
类,会将 x,y
坐标的均值指定为坐标原点 (0, 0)
。它的用途并不广泛,但作为展示非常清晰。可以看到,方法 compute_group
内并没有显式指定分组计算,但不同构面的统计变换都是独立的。
StatCenter <- ggproto('StatCenter', Stat,
compute_group = function(data, scales) {
mean_x <- mean(data$x, na.rm=TRUE)
mean_y <- mean(data$y, na.rm=TRUE)
data$x <- data$x - mean_x
data$y <- data$y - mean_y
return (data)
},
required_aes = c('x', 'y')
)
mpg %>% ggplot(aes(x=cty, y=hwy)) + geom_point(stat='center') + facet_wrap(~class, scales='free')
一般也会定义 stat_newstat()
函数,该函数返回 Layer
对象。这样可以设定默认的 Geom
,并在有需要时添加额外参数。
stat_center <- function(mapping = NULL, data = NULL, geom = 'point',
position = 'identity', na.rm = FALSE, show.legend = NA,
inherit.aes = TRUE, ...) {
layer(
stat = StatCenter, data = data, mapping = mapping, geom = geom,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...)
)
}
mpg %>% ggplot(aes(x=cty, y=hwy)) + stat_center() + facet_wrap(~class, scales='free')
自定义Geom类
新建名为 GeomNewgeom
、继承自 Geom
的 ggproto
类,就能以 stat_xxx(geom='newstat')
的形式调用自定义的几何元素。
在继承时,类属性 required_aes
指定了经过统计变换后、会被类方法调用的数据列。类方法 draw_group
调用 ggplot
和/或 grid
包创建几何元素,返回 gList
或 gTree
(较复杂时)对象。
在 draw_group
方法内部,使用 ggplot
创建几何元素时,主要调用 GeomXXX
类的 draw_panel(data, panel_params, coord, ...)
方法,其中 data
是统计变换后的、被 default_aes
修改后的数据,除了 x,y
外也包括 size, colour, fill
等属性。当然,也可以做额外的手动修改,注意描边颜色始终用 colour
而不是 color
。
调用了什么 GeomXXX
类,就需要最终传入的数据具有对应的列,例如 GeomPoint
需要 colour, size
等列,GeomSegment
需要 linewidth, linetype
等列,GeomText
需要 angle, hjust, family, lineheight
等列。遇到报错时,可以对照 ggplot2
的github源码,查看 default_aes
漏掉了什么属性。
通过 panel_params
也可以获取画面信息,例如 panel_params$y.range
获取 y
轴显示范围。
也可以使用 grid
包创建几何元素,但更复杂、与 ggplot
的操作习惯有差别,如果不涉及非常底层的操作,不建议这么做。这种方式主要调用 xxxGrob()
函数,传入的 x,y
坐标单位是画面比例,这就需要先用 coords <- coord$transform(data, panel_params)
转换坐标,然后以 coords$x, coords$y
的形式调用。通过 xxxGrob()
的 gp=gpar()
参数指定属性。作为 grid
包的扩展,gridExtra
包提供了更多 xxxGrob()
函数,例如椭圆。
params | description |
---|---|
col | Colour for lines and borders. |
fill | Colour for filling rectangles, polygons, ... |
alpha | Alpha channel for transparency |
lty | Line type |
lwd | Line width |
lex | Multiplier applied to line width |
lineend | Line end style (round, butt, square) |
linejoin | Line join style (round, mitre, bevel) |
linemitre | Line mitre limit (number greater than 1) |
fontsize | The size of text (in points) |
cex | Multiplier applied to fontsize |
fontfamily | The font family |
fontface | The font face (bold, italic, ...) |
lineheight | The height of a line as a multiple of the size of text |
font | Font face (alias for fontface; for backward compatibility) |
下列代码定义了一个 Geom
类,会在 (x,y)
坐标上画点,并连接点和坐标原点,同样是展示目的。
GeomLinkpoint <- ggproto('GeomLinkpoint', Geom,
required_aes = c('x', 'y'),
default_aes = aes(colour = 'black', linewidth = .5, fill = NA, alpha = NA,
size = 2, linetype = 1, shape = 19, stroke = 1),
draw_group = function(data, panel_params, coord, ...) {
point <- transform(data)
link <- transform(data, xend = 0, yend = 0)
grid::gList(
GeomSegment$draw_panel(link, panel_params, coord, ...),
GeomPoint$draw_panel(point, panel_params, coord, ...)
)
}
)
mpg %>% ggplot(aes(x=cty, y=hwy)) + stat_center(geom='linkpoint') + facet_wrap(~class, scales='free')
一般也会定义 geom_newgeom()
,该函数返回 Layer
对象。这样可以设定默认的 Stat
,并在有需要时添加额外参数。
geom_linkpoint <- function(mapping = NULL, data = NULL,
stat = 'center', position = 'identity',
..., na.rm = FALSE, show.legend = NA,
inherit.aes = TRUE) {
layer(
data = data,
mapping = mapping,
geom = GeomLinkpoint,
stat = stat,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...)
)
}
mpg %>% ggplot(aes(x=cty, y=hwy)) + geom_linkpoint() + facet_wrap(~class, scales='free')
有时,可能只想修改原 Geom
的默认设置,那就不需从底层 Geom
继承。
GeomHollowpoint <- ggproto('GeomHollowpoint', GeomPoint,
default_aes = aes(colour = 'black', linewidth = .5, fill = NA, alpha = NA,
size = 2, linetype = 1, shape = 21, stroke = 1.5)
)
mpg %>% ggplot(aes(x=cty, y=hwy)) + stat_center(geom='hollowpoint') + facet_wrap(~class, scales='free')
Stat和Geom实例
在实际扩展 ggplot
功能时,最常见的需求有三个:
- 为自定义的
geom_xxx()
增加额外参数,并在统计变换时调用。 - 指定是否绘制图形的某一部分。
- 统计变换可以汇总数据、创建新列,为绘制提供便利。
以下代码创建了名为 Skinnybox
的新型几何对象,默认将中位数绘制为空心圆点,在上方绘制75%分位数到最大值的细线,在下方绘制25%分位数到最小值的细线。增加 lower_prob, upper_prob
参数,允许控制分位数的范围;增加 show_median
参数,指定是否绘制中位数。
StatSkinnybox <- ggproto('StatSkinnybox', Stat,
compute_group = function(data, scales, na.rm, lower_prob, upper_prob) {
data <- dplyr::summarise(data,
x = median(x, na.rm=na.rm),
ymin = min(y, na.rm=na.rm),
lower = quantile(y, lower_prob, na.rm=na.rm),
middle = median(y, na.rm=TRUE),
upper = quantile(y, upper_prob, na.rm=na.rm),
ymax = max(y, na.rm=na.rm),
y = median(y, na.rm=na.rm)
)
return (data)
},
required_aes = c('x', 'y')
)
GeomSkinnybox <- ggproto('GeomSkinnybox', Geom,
required_aes = c('x', 'ymin', 'lower', 'middle', 'upper', 'ymax'),
default_aes = aes(colour = 'black', linewidth = 0.5, fill = NA, alpha = NA,
size = 2, linetype = 1, shape = 21, stroke = 1.5),
draw_group = function(data, panel_params, coord, show_median, ...) {
point <- transform(data)
lower_line <- transform(data, x=x, xend=x, y=ymin, yend=lower)
upper_line <- transform(data, x=x, xend=x, y=ymax, yend=upper)
grid::gList(
if (show_median) GeomPoint$draw_panel(point, panel_params, coord, ...),
GeomSegment$draw_panel(lower_line, panel_params, coord, ...),
GeomSegment$draw_panel(upper_line, panel_params, coord, ...)
)
}
)
geom_skinnybox <- function(mapping = NULL, data = NULL, stat = 'skinnybox',
position = 'identity', show.legend = NA, na.rm = TRUE, inherit.aes = TRUE,
show_median=TRUE, lower_prob = 0.25, upper_prob = 0.75, ...) {
layer(
data = data,
mapping = mapping,
stat = stat,
geom = GeomSkinnybox,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(na.rm=na.rm, show_median=show_median,
lower_prob=lower_prob, upper_prob=upper_prob, ...)
)
}
mpg %>% ggplot(aes(x=class, y=cty)) + geom_skinnybox()
mpg %>% ggplot(aes(x=class, y=cty)) + geom_skinnybox(lower_prob=0.1, upper_prob=0.9)
mpg %>% ggplot(aes(x=class, y=cty)) + geom_skinnybox(show_median=FALSE)
在ggplot2: Elegant Graphics for Data Analysis (3e)和官方文档中,你能见到的示例大多使用 draw_panel
而不是 draw_group
绘制。然而,draw_panel
无法识别 group
刻度,也无法很方便地传参(本例换用 draw_panel
在 ggplot v3.4.1
版本中传参报错),建议优先使用 draw_group
。
xy轴翻转
以 geom_histogram
直方图为例,x
轴对应 data
中的数据,y
轴是后期计算的频数,此时 geom_histogram(aes(x=value))
即可绘制。有时,我们希望 y
轴对应 data
,那么只需 geom_histogram(aes(y=value))
。
再以 geom_boxplot
箱线图为例,y
轴对应 data
中的数据,x
轴要么没有、要么是分类变量,此时 geom_boxplot(aes(y=value))
即可绘制。有时,我们希望 x
轴对应 data
,那么只需 geom_boxplot(aes(x=value))
。
然而,方便的xy翻转并非理所当然。回想之前 StatSkinnybox
的 compute_group
中,我们对 y
做了一系列计算,获取 ymin, ymax, lower
等,仅仅更改 mapping
并不能让这些计算自动在 x
上进行。你可以尝试为 geom_skinnybox
映射连续变量到 x
轴,这只会生成不完整的图形。
也就是说,geom_histogram
内含的统计变换StatBin
在 x
上操作,映射 y
时需要额外处理;geom_boxplot
内含的统计变换StatBoxplot
在 y
上操作,映射 x
时需要额外处理。
ggplot
早期的解决方案是原样绘图,依赖 coord_flip()
实现 xy
翻转。现在的主流方案是通过 has_flipped_aes()
检测是否需要翻转,如果需要,将 x,xmin,xmax,xend
等变量通过 flip_data()
重命名为 y
系列,统计变换之后再翻回(对 y
亦然)。在绘制几何元素前,可能也需要翻转保证图形参数正确。
从 compute_group
返回的数据必须翻回原样,因为 ggplot
据此检测需要显示的 x,y
范围。也需要修改 GeomXXX
的 required_aes
,以兼容两个方向的 data
。
StatXXX <- ggproto(...,
setup_params = function(self, data, params) {
# 判断是否需要翻转
params$flipped_aes <- has_flipped_aes(data, params, ...)
return (params)
},
compute_group = function(..., flipped_aes) {
# 如果flipped_aes=TRUE, 翻转后一系列统计变换
data <- flip_data(data, flipped_aes)
...
data$flipped_aes <- flipped_aes
# 翻回
data <- flip_data(data, flipped_aes)
return (data)
},
...
)
GeomXXX <- ggproto(...,
# 设置兼容
required_aes = c('x|y', 'ymin|xmin', 'ymax|xmax'),
setup_params = StatXXX$setup_params,
...
)
那么,has_flipped_aes()
在什么情况下会判定需要翻转呢?根据源码,有以下几种:
data
中有flipped_aes
列,且值为TRUE
- 参数传入
orientation='y'
。这里似乎默认orientation='x'
为正常情况,然而是否翻转要看StatXXX
中compute_group
方法,个人认为has_flipped_aes()
的参数名与作用并不完全匹配 - 仅映射
x
,且main_is_orthogonal
为TRUE
;仅映射y
,且main_is_orthogonal
为FALSE
- 映射了
xmin, xmax
,且range_is_orthogonal
为TRUE
;映射了ymin, ymax
,且range_is_orthogonal
为FALSE
y
为连续变量,且main_is_continuous
为TRUE
;y
为离散变量,且main_is_continuous
为FALSE
至此,我们可以修改 geom_skinnybox()
,以支持 xy
翻转
StatSkinnybox <- ggproto('StatSkinnybox', Stat,
setup_params = function(self, data, params) {
params$flipped_aes <- ggplot2::has_flipped_aes(data, params, main_is_orthogonal = TRUE,
group_has_equal = TRUE,
main_is_optional = TRUE)
return (params)
},
compute_group = function(data, scales, na.rm, lower_prob, upper_prob, flipped_aes) {
data <- ggplot2::flip_data(data, flipped_aes)
data <- dplyr::summarise(data,
x = median(x, na.rm=na.rm),
ymin = min(y, na.rm=na.rm),
lower = quantile(y, lower_prob, na.rm=na.rm),
middle = median(y, na.rm=TRUE),
upper = quantile(y, upper_prob, na.rm=na.rm),
ymax = max(y, na.rm=na.rm),
y = median(y, na.rm=na.rm)
)
data$flipped_aes <- flipped_aes
data <- ggplot2::flip_data(data, flipped_aes)
return (data)
},
required_aes = c('x', 'y')
)
GeomSkinnybox <- ggproto('GeomSkinnybox', Geom,
required_aes = c('y|x', 'ymin|xmin', 'lower|xlower', 'middle|xmiddle', 'upper|xupper', 'ymax|xmax'),
setup_params = StatSkinnybox$setup_params,
default_aes = aes(colour = 'black', linewidth = 0.5, fill = NA, alpha = NA,
size = 2, linetype = 1, shape = 21, stroke = 1.5),
draw_group = function(data, panel_params, coord, show_median, flipped_aes, ...) {
data <- ggplot2::flip_data(data, flipped_aes)
point <- ggplot2::flip_data(transform(data), flipped_aes)
lower_line <- ggplot2::flip_data(transform(data, x=x, xend=x, y=ymin, yend=lower), flipped_aes)
upper_line <- ggplot2::flip_data(transform(data, x=x, xend=x, y=ymax, yend=upper), flipped_aes)
grid::gList(
if (show_median) GeomPoint$draw_panel(point, panel_params, coord, ...),
GeomSegment$draw_panel(lower_line, panel_params, coord, ...),
GeomSegment$draw_panel(upper_line, panel_params, coord, ...)
)
}
)
geom_skinnybox <- function(mapping = NULL, data = NULL, stat = 'skinnybox',
position = 'identity', show.legend = NA, na.rm = TRUE, inherit.aes = TRUE,
show_median=TRUE, lower_prob = 0.25, upper_prob = 0.75, ...) {
layer(
data = data,
mapping = mapping,
stat = stat,
geom = GeomSkinnybox,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(na.rm=na.rm, show_median=show_median,
lower_prob=lower_prob, upper_prob=upper_prob, ...)
)
}
mpg %>% ggplot(aes(y=class, x=cty)) + geom_skinnybox()
其他
在 jupyter
IDE下调用自定义 geom_xxx()
函数绘图,有时会遇到如下警告,不影响结果但有些烦人:
Warning message in geom2trace.default(dots[[1L]][[1L]], dots[[2L]][[1L]], dots[[3L]][[1L]]):
“geom_GeomDesvalue() has yet to be implemented in plotly.
If you'd like to see this geom implemented,
Please open an issue with your example code at
https://github.com/ropensci/plotly/issues”
geom2trace()
函数的作用是将 ggplot
对象转换为 plotly
可交互图形,这个警告的意思是 plotly
包(理所应当地)还没有为我们自建的 Geom
对象创建合适的 geom2trace()
函数,只好调用默认的 geom2trace.default()
。鉴于我们没用到 plotly
包,可能是 jupyter
内核后台调用了。在 CMD
下就不会出现该警告。
解决办法也很简单,定义一个专门的 geom2trace()
处理自建的 Geom
对象即可。如果后续有转 plotly
的需求,还可以进一步完善该函数。
geom2trace.GeomDesvalue <- function(data, params, plot) {}
以上就是 ggplot
代码复用三大技巧的所有内容。当你掌握这些技巧,同时了解如何创建R包时,你将再也不担心代码整理、版本控制等问题。
祝玩的开心!