[原]数据科学教程:R语言与DataFrame[2016版]

data frame

什么是DataFrame

引用 r-tutor上的定义:

DataFrame 是一个表格或者类似二维数组的结构,它的各行表示一个实例,各列表示一个变量。

没错,DataFrame就是类似于Excel表格和MySQL数据库一样是一个结构化的数据体。而这种结构化的数据体是当代数据流编程中的中流砥柱,几乎所有先进算法的载体都是DataFrame,比如现在我们耳熟能详的逻辑回归算法、贝叶斯算法、支持向量机算法、XGBoost算法等等都建立在这个数据流编程的基础之上,我们可以在R、Python、Scala等函数式编程中找到他们的身影。

R中的DataFrame数据流编程

参考前文 [[原]基于RStudio Webinars的统计报告Web化与工程化实践总结](https://segmentfault.com/a/11...、数据清洗、数据处理、数据可视化以及数据建模五个模块。

数据读取 readr/httr/DBI

readr

readr简化了我们读取多种格式表格型数据的方法,包括分割文件withread_delim(),read_csv()read_tsv()read_csv2()、固定宽度文件读取的read_fwf()read_table()以及read_log()来读取Web日志文件。在参数配置方面是和原生的read.xxx()函数族是看齐的。

readr是利用C++和RCpp编写的,所以执行的速度是相当快的,不过相对于直接用C语言写的data.table::fread()就稍微慢大概1.2-2倍左右。在实际使用中,data.talbe::fread()的读取速度可以比原生的read.csv有3-10倍的提升速度。

httr

httr是一个高级的网络请求库,类似于Python中的Tornado和Requests,除了提供基本的Restful接口设计功能,比如GET(), HEAD(), PATCH(), PUT(), DELETE()POST(),还提供了OAuth的调用,比如oauth1.0_token()oauth2.0_token()。而且httr还提供了诸如session、cookie、SSL、header、proxy、timeoutd等更过高级管理功能。当然你可以用它来做简单的爬虫应用,如果需要更高级的爬虫,我们需要投入rvest的怀抱来支持诸如xpath等高级爬虫特性。

DBI

DBI是一个为R与数据库通讯的数据库接口。相当于Java里面的DAO,Python里的Torndb和Tornlite,方便多种关系型数据库的SQL请求。其中最亮眼的是,R中的DataFrame和数据库之前可以以整个数据框插入的形式插入数据而不需要再拼接SQL语句。

以下是一个官方文档的示例:

library(DBI)
# 创建一个临时内存的 RSQLite 数据库
con <- dbConnect(RSQLite::SQLite(), dbname = ":memory:")

dbListTables(con)
# 直接插入整个数据框到数据库中
dbWriteTable(con, "mtcars", mtcars)
dbListTables(con)

dbListFields(con, "mtcars")
dbReadTable(con, "mtcars")

# 你可以获取所有结果:
res <- dbSendQuery(con, "SELECT * FROM mtcars WHERE cyl = 4")
dbFetch(res)
dbClearResult(res)

# 或者一次取一块
res <- dbSendQuery(con, "SELECT * FROM mtcars WHERE cyl = 4")
while(!dbHasCompleted(res)){
  chunk <- dbFetch(res, n = 5)
  print(nrow(chunk))
}
dbClearResult(res)

dbDisconnect(con)

clipboard.png

数据清洗 tidyr/jsonlite

tidyr

tidyr是一个数据清洗的新包,正在取代reshape2spreadsheets等包。清洁的数据在数据处理的后续流程中十分重要,比如数据变化(dplyr),可视化(ggplot2/ggvis)以及数据建模等。tidyr主要提供了一个类似Excel中数据透视表(pivot table)的功能,提供gatherspread函数将数据在长格式和宽格式之间相互转化,应用在比如稀疏矩阵和稠密矩阵之间的转化。此外,separateunion方法提供了数据分组拆分、合并的功能,应用在nominal数据的转化上。

jsonlite

类似于Python中的json库,参考前文 [[原]数据流编程教程:R语言与非结构化数据共舞](https://segmentfault.com/a/11...,我们可以知道jsonlite是一个标准的json转化库,依赖于jsonlite我们可以自由地在JSON和DataFrame之间相互转化。

数据处理 dplyr/rlist/purrr

dplyr

dplyr包是现在数据流编程的核心,同时支持主流的管道操作 %>%,主要的数据处理方法包括:

  • 高级查询操作:

select(): 按列变量选择
filter(): 按行名称分片
slice(): 按行索引分片
mutate(): 在原数据集最后一列追加一些数据集
summarise(): 每组聚合为一个小数量的汇总统计,通常结合gruop_by()使用
arrange(): 按行排序

  • 关联表查询

inner_join(x, y): 匹配 x + y
left_join(x, y): 所有 x + 匹配 y
semi_join(x, y): 所有 x 在 y 中匹配的部分
anti_join(x, y): 所有 x 在 y 中不匹配的部分

  • 集合操作

intersect(x, y): x 和 y 的交集(按行)
union(x, y): x 和 y 的并集(按行)
setdiff(x, y): x 和 y 的补集 (在x中不在y中)

更多详细操作可以参考由SupStats翻译的 数据再加工速查表,比Python的老鼠书直观很多。

rlist

参考前文 数据流编程教程:R语言与非结构化数据共舞,我们知道,区别于dplyr包,rlist包是针对非结构化数据处理而生的,也对以list为核心的数据结构提供了类似DataFrame的高级查询、管道操作等等方法。

purrr

purrr向Scala这样的具有高级类型系统的函数式编程语言学习,为data frame的操作提供更多的函数式编程方法,比如map、lambda表达式。此外,purrr引入了静态类型,来解决原生的apply函数族类型系统不稳定的情况。

我遇到过一个非常头疼的apply函数的问题:apply内的表达式计算结果不一致。

# 原来表达式是这样的,但是返回的计算结果不对:
# x1,x2,x3都是一个含有NA值的一个10x10的矩阵
apply(x1*x2-x1*x3,1,sum,na.rm=T)

于是改成分步计算才能得到正确答案。

t1 <- apply(x1 * x2,1,sum,na.rm=T)
t2 <- apply(x1 * x3,1,sum,na.rm=T)
t3 <- t1 - t2

如果使用purrr包就可以很好的解决这一问题。参考 Wisdom's Quintessence: Purrr package for R is good for performance 的例子:

library(purrr)
 
mtcars %>%
  split(.$cyl) %>%
  map(~ lm(mpg ~ wt, data = .)) %>%
  map(summary) %>%
  map_dbl("r.squared")

具体使用可以参考Rstudio Blog:purrr 0.2.0

数据可视化 ggplot2/ggvis

ggplot2

ggplot2 是一个增强的数据可视化R包,帮助我们轻松创建令人惊叹的多层图形。它的设计理念类似于PhotoShop,具体参数包含设计对象、艺术渲染、统计量、尺寸调整、坐标系统、分片显示、位置调整、动画效果等等。

更多操作可以查看ggplot2与数据可视化速查表官方文档

实战可以参考R Graphics Cookbook一书

ggvis

ggvis是吸收了ggplot2vega以及d3的精华,目标旨在配合shiny打造动态可交互的可视化组件。ggvis最明显的区别就是在作图时直接支持%>%的管道操作,比如:

diamonds %>% ggvis(~carat, ~price, fill=~clarity) %>% layer_points(opacity:=1/2)

ggplot2与ggvis的关系类似于plyr与dplyr的关系,都是一种演化过程。

数据建模 broom

broom

在机器学习的本质其实就是各种姿势的回归,而在R中的各种回归分析往往不会返回一个整齐的data frame 结果。比如

lmfit <- lm(mpg ~ wt, mtcars)
lmfit
## 
## Call:
## lm(formula = mpg ~ wt, data = mtcars)
## 
## Coefficients:
## (Intercept)           wt  
##      37.285       -5.344

这时候broom包就派上用场了,直接将统计结果转化为data frame格式:

library(broom)
tidy(lmfit)
##          term  estimate std.error statistic      p.value
## 1 (Intercept) 37.285126  1.877627 19.857575 8.241799e-19
## 2          wt -5.344472  0.559101 -9.559044 1.293959e-10

augment()函数返回data frame格式的s其所有他参数结果

head(augment(lmfit))
##           .rownames  mpg    wt  .fitted   .se.fit     .resid       .hat    .sigma      .cooksd  .std.resid
## 1         Mazda RX4 21.0 2.620 23.28261 0.6335798 -2.2826106 0.04326896  3.067494 1.327407e-02 -0.76616765
## 2     Mazda RX4 Wag 21.0 2.875 21.91977 0.5714319 -0.9197704 0.03519677  3.093068 1.723963e-03 -0.30743051
## 3        Datsun 710 22.8 2.320 24.88595 0.7359177 -2.0859521 0.05837573  3.072127 1.543937e-02 -0.70575249
## 4    Hornet 4 Drive 21.4 3.215 20.10265 0.5384424  1.2973499 0.03125017  3.088268 3.020558e-03  0.43275114
## 5 Hornet Sportabout 18.7 3.440 18.90014 0.5526562 -0.2001440 0.03292182  3.097722 7.599578e-05 -0.06681879
## 6           Valiant 18.1 3.460 18.79325 0.5552829 -0.6932545 0.03323551  3.095184 9.210650e-04 -0.23148309

glance()函数,返回data frame格式的部分参数结果

glance(lmfit)
##   r.squared adj.r.squared    sigma statistic      p.value df    logLik
## 1 0.7528328     0.7445939 3.045882  91.37533 1.293959e-10  2 -80.01471
##        AIC      BIC deviance df.residual
## 1 166.0294 170.4266 278.3219          30

DataFrame优化

data.table

众所周知,data.frame的几个缺点有:

  1. 大数据集打印缓慢
  2. 内部搜索缓慢
  3. 语法复杂
  4. 缺乏内部的聚合操作

针对这几个问题,data.table应运而生。data.table完美兼容data.frame,这意味着之前对data.frame的操作我们可以完全保留,并且支持更多方便的数据操作方法。

data.table还参考了NoSQL中流行的Key-Value形式,引入了setkey()函数,为数据框设置关键字索引。

值得一提的是data.table引入了全新的索引形式,大大简化了data frame的分片形式,提供接近于原生矩阵的操作方式并直接利用C语言构造底层,保证操作的速度。

对比操作

对比data.table 和 dplyr 的操作:

操作 data.table dplyr
按行分片 DT[1:2,] DF[1:2,]
按列分片 DT[,1:2,with=False] DF[,1:2]
分组summarise DT[, sum(y), by=z] DF %>% group_by(z) %>% summarise(sum(y))
分组mutate DT[, y := cumsum(y), by=z] ans <- DF %>% group_by(z) %>% mutate(y = cumsum(y))
筛选后分组汇总 DT[x > 2, sum(y), by=z] DF %>% filter(x>2) %>% group_by(z) %>% summarise(sum(y))
筛选后分组更新 DT[x > 2, y := cumsum(y), by=z] ans <- DF %>% group_by(z) %>% mutate(y = replace(y, which(x>2), cumsum(y)))
分组后按条件汇总 DT[, if(any(x > 5L)){y[1L]-y[2L]}else{y[2L], by=z]} DF %>% group_by(z) %>% summarise(if (any(x > 5L)) y[1L]-y[2L] else y[2L])
apply函数族
操作 data.table dplyr
分组扩展各list DT[, (cols) := lapply(.SD, sum), by=z] ans <- DF %>% group_by(z) %>% mutate_each(funs(sum))
分组汇总各list DT[, lapply(.SD, sum), by=z] DF %>% group_by(z) %>% summarise_each(funs(sum))
分组汇总各list DT[, c(lapply(.SD, sum),lapply(.SD, mean)), by=z] DF %>% group_by(z) %>% summarise_each(funs(sum, mean))
分组汇总各list DT[, c(.N, lapply(.SD, sum)), by=z] DF %>% group_by(z) %>% summarise_each(funs(n(), mean))
join 操作
setkey(DT1, x, y)
操作 data.table dplyr
一般join DT1[DT2] left_join(DT2, DT1)
择列join DT1[DT2, .(z, i.mul)] left_join(select(DT2, x,y,mul), select(DT1, x,y,z))
聚合join DT1[DT2, .(sum(z)*i.mul), by=.EACHI] DF1 %>% group_by(x, y) %>% summarise(z=sum(z)) %>% inner_join(DF2) %>% mutate(z = z*mul) %>% select(-mul)
更新join DT1[DT2, z := cumsum(z)*i.mul, by=.EACHI] join and group by + mutate
滚动join DT1[DT2, roll = -Inf] /
其他变量控制输出 DT1[DT2, mult = "first"] /
拼接操作
操作 data.table dplyr
分组再分list聚合 DT[, list(x[1], y[1]), by=z] DF %>% group_by(z) %>% summarise(x[1], y[1])
分组再分list拼接 DT[, list(x[1:2], y[1]), by=z] DF %>% group_by(z) %>% do(data.frame(.$x[1:2], .$y[1]))
分组取分位数聚合 DT[, quantile(x, 0.25), by=z] DF %>% group_by(z) %>% summarise(quantile(x, 0.25))
分组取分位数拼接 DT[, quantile(x, c(0.25, 0.75)), by=z] DF %>% group_by(z) %>% do(data.frame(quantile(.$x, c(0.25, 0.75))))
分组分list聚合拼接 DT[, as.list(summary(x)), by=z] DF %>% group_by(z) %>% do(data.frame(as.list(summary(.$x))))

更多操作详情可查看data.table速查表

DataFrame可视化

DT

DT包是谢溢辉老师的大作,为data frame数据提供了非常好的可视化功能,并且提供了筛选、分页、排序、搜索等数据查询操作。

library(DT)
datatable(iris)

datatable

此外,DT包还提供了大量的UI定制的功能,对html、css和js进行深度定制。比如:

m = matrix(c(
  '<b>Bold</b>', '<em>Emphasize</em>', '<a href="http://rstudio.com">RStudio</a>',
  '<a href="#" onclick="alert(\'Hello World\');">Hello</a>'
), 2)
colnames(m) = c('<span style="color:red">Column 1</span>', '<em>Column 2</em>')
datatable(m)  # 默认 escape = TRUE 

datatable(m, escape = FALSE)

raw_matrix %>%
DT::datatable(options = list(pageLength = 30, dom = 'tip')) %>% 
  DT::formatStyle(columns = c("A","B")
    background = styleColorBar(c(0, max(raw_matrix,na.rm = TRUE)), 'steelblue'),
    backgroundSize = '100% 50%',
    backgroundRepeat = 'no-repeat',
    backgroundPosition = 'center') 

分布式DataFrame

sparklyr

sparklyr是 rstudio 公司为链接spark 和dataframe 编写的一套分布式数据处理框架,用一个统一的跨引擎API简化了多数据源的分析操作,进一步将data frame底层的分布式傻瓜化。

在R中使用sparklyr::spark_apply,我们不需要修改之前任何的代码,并且绕过Hadoop的绝对限制,就可以让data frame格式的数据,自动获得分布式处理的能力!

# 创建 spark链接 管理器来运行Spark引擎
sc <- sparklyr::spark_connect(master = "local",

version = "2.2.0",
method = "livy",
config = sparklyr::config())



# 通过SQL读取spark数据
ddf <- DBI::dbGetQuery(sc, "select * from mtcars")

/*Basic Stats*/
# 返回行/列的值
ncol(ddf)
nrow(ddf)

# 在 DF 上进行运行标准汇总
summary(ddf)

更多具体操作可以参考官方指南

各数据处理框架性能对比图

DataFrame在R、Python和Spark三者中的联系

操作 R Python Spark
base Pandas spark SQL
读取csv read.csv() read_csv() spark-csv
计数 nrow() pandasDF.count() sparkDF.count()
分片 head(data,5) pandasDF.head(5) sparkDF.show(5)
推断类型 自动推断 自动推断 默认为string类型
标准差计算中的NaN处理 视为NA 自动排除 视为NaN
特征工程 dplyr::mutate() pandasDF['new'] sparkDF.withColumn()

DataFrame 之我见

  1. 处理数据的第一语言还是 SQL语句,因为SQL是DSL,这样就对使用者没有Python或者R的要求,也方便与DBA的维护。在R中可以使用 sqldf 通过SQL直接操作DataFrame,在Python中可以使用 pysqldf
  2. 处理数据的第二语言则是 tidyverse 或者 pandas,使用这样的链式调用方法可以提升数据流的处理效率,规避一些原生SQL在不同数据库中执行情况不同或者可读性较差的问题。
  3. 处理数据的第三语言则是 data.table 或者 scala,使用这样高性能的方法可以在关键步骤提升数据处理效率到极致,不过会牺牲一部分维护性。

参考资料

作为分享主义者(sharism),本人所有互联网发布的图文均遵从CC版权,转载请保留作者信息并注明作者 Harry Zhu 的 FinanceR专栏:https://segmentfault.com/blog...,如果涉及源代码请注明GitHub地址:https://github.com/harryprince。微信号: harryzhustudio
商业使用请联系作者。
阅读 13k

推荐阅读
FinanceR
用户专栏

循环写作,持续更新,形成闭环,贵在坚持

1011 人关注
61 篇文章
专栏主页