Shixiang Wang

分析之前的数据准备

王诗翔 · 2018-06-06

Catagories: · Tags: r, prepare,

来源:《R核心技术手册》

数据分析项目中大多数的时间都用在了准备数据上,一个典型的项目80%的精力都花在分析而进行的发现、清洗和准备数据上。只有不到5%的精力用于分析(剩下的时间都耗在了写报告上面)。

合并数据集

数据分析中最常见的一个障碍是将存储在两个不同地方的数据组合到一起。

粘贴数据结构

R提供了几个函数可以将多个数据结构粘贴成一个数据结构。

paste

paste函数可以将多个字符型向量连接成一个向量,默认向量的值是用空格分隔的,我们可以通过sep参数指定分隔符号,而collapse参数可以用来指定这些值之间的连接符号。

x <- c("a", "b", "c", "d", "e")
y <- c("A", "B", "C", "E", "F")
# 默认
paste(x, y)
#> [1] "a A" "b B" "c C" "d E" "e F"
# 使用自定义分隔符
paste(x, y, sep = ",")
#> [1] "a,A" "b,B" "c,C" "d,E" "e,F"
# 指定连接符
paste(x, y, sep = "-", collapse = "#")
#> [1] "a-A#b-B#c-C#d-E#e-F"

另外,使用简化版的paste0函数只提供连接参数,没有分隔。

paste0(x, y, collapse = "#")
#> [1] "aA#bB#cC#dE#eF"

rbind 和 cbind

rbindcbind函数分别可以以增加行或列的形式将几个对象(矩阵或者数据框)合并起来。你可以将它想象为以垂直或者水平地将两张表拼在一起。

merge

merge(x, y, by = , by.x = , by.y = , ...)

xy指定用于合并的数据框,by对应xy共有的列名,后面by.xby.y用于分别指定用于合并的列名。

数据转换

数据框中常用的更改变量的函数是transform,它定义如下:

transform(`_data`, ...)

这个函数首先要指定一个数据框,跟着是一系列的表达式,表达式中的变量是数据框中的变量,transform函数会完成每个表达式中的计算,然后返回最终的数据框。

head(mtcars)
#>                    mpg cyl disp  hp drat   wt qsec vs am gear carb
#> Mazda RX4         21.0   6  160 110 3.90 2.62 16.5  0  1    4    4
#> Mazda RX4 Wag     21.0   6  160 110 3.90 2.88 17.0  0  1    4    4
#> Datsun 710        22.8   4  108  93 3.85 2.32 18.6  1  1    4    1
#> Hornet 4 Drive    21.4   6  258 110 3.08 3.21 19.4  1  0    3    1
#> Hornet Sportabout 18.7   8  360 175 3.15 3.44 17.0  0  0    3    2
#> Valiant           18.1   6  225 105 2.76 3.46 20.2  1  0    3    1
mtcars.transformed <-  transform(mtcars, newVar = disp / hp)
mtcars.transformed
#>                      mpg cyl disp  hp drat   wt qsec vs am gear carb newVar
#> Mazda RX4           21.0   6  160 110 3.90 2.62 16.5  0  1    4    4   1.45
#> Mazda RX4 Wag       21.0   6  160 110 3.90 2.88 17.0  0  1    4    4   1.45
#> Datsun 710          22.8   4  108  93 3.85 2.32 18.6  1  1    4    1   1.16
#> Hornet 4 Drive      21.4   6  258 110 3.08 3.21 19.4  1  0    3    1   2.35
#> Hornet Sportabout   18.7   8  360 175 3.15 3.44 17.0  0  0    3    2   2.06
#> Valiant             18.1   6  225 105 2.76 3.46 20.2  1  0    3    1   2.14
#> Duster 360          14.3   8  360 245 3.21 3.57 15.8  0  0    3    4   1.47
#> Merc 240D           24.4   4  147  62 3.69 3.19 20.0  1  0    4    2   2.37
#> Merc 230            22.8   4  141  95 3.92 3.15 22.9  1  0    4    2   1.48
#> Merc 280            19.2   6  168 123 3.92 3.44 18.3  1  0    4    4   1.36
#> Merc 280C           17.8   6  168 123 3.92 3.44 18.9  1  0    4    4   1.36
#> Merc 450SE          16.4   8  276 180 3.07 4.07 17.4  0  0    3    3   1.53
#> Merc 450SL          17.3   8  276 180 3.07 3.73 17.6  0  0    3    3   1.53
#> Merc 450SLC         15.2   8  276 180 3.07 3.78 18.0  0  0    3    3   1.53
#> Cadillac Fleetwood  10.4   8  472 205 2.93 5.25 18.0  0  0    3    4   2.30
#> Lincoln Continental 10.4   8  460 215 3.00 5.42 17.8  0  0    3    4   2.14
#>  [ reached 'max' / getOption("max.print") -- omitted 16 rows ]

对对象的每个元素进行函数运算

apply函数簇

该内容参考【r<-高级|理论】apply,lapply,sapply用法探索学习 #### plyr软件包

apply函数众多,参数也有些不同,幸运的是,我们可以使用plyr包来避免这些函数的细节。

plyr包包含了12个命名与其功能有逻辑关联的函数,用于将某个函数运行在某个R对象上,并且返回结果。每个函数的输入都是一个数组、数据框或者列表,输出也都是一个数组、数据框或者列表,或者什么都不输出。

输入 输出矩阵 输出数据框 输出列表 不输出
数组 aaply adply alply a_ply
数据框 daply ddply dlply d_ply
列表 laply ldply llply l_ply

所有的函数都接受下面的参数。

参数 描述 默认值
.data 输入的数据对象
.fun 要运行的函数 NULL
.progress 进度条类型(用create_progress构建);选项可包括“none”,“text”,“tk”和“win” “none”
.expand 若.data是一个数据框,则该参数控制输出如何扩展;.expand=TRUE表示1维输出,.expand=FALSE表示n维输出 TRUE
.parallel 指定是否并行地运行函数(通过foreach) FALSE
其他传递给.fun的参数

举几个例子:

library(plyr)
d <- data.frame(x=1:5, y=6:10)
# (1)输入为列表,输出也为列表
lapply(d, function(x) 2 ^ x)
#> $x
#> [1]  2  4  8 16 32
#> 
#> $y
#> [1]   64  128  256  512 1024
# 等价命令是llply
llply(.data=d, .fun=function(x) 2^x)
#> $x
#> [1]  2  4  8 16 32
#> 
#> $y
#> [1]   64  128  256  512 1024
# (2) 输入为矩阵,输出为列表
x = as.matrix(d)
apply(X = x, MARGIN = 1, FUN=paste, collapse=",")
#> [1] "1,6"  "2,7"  "3,8"  "4,9"  "5,10"
# 等价命令
aaply(.data = x, .margins = 1, .fun=paste, collapse=",")
#>      1      2      3      4      5 
#>  "1,6"  "2,7"  "3,8"  "4,9" "5,10"
# (3)输入为数据框,输出为矩阵
t(sapply(d, FUN=function(x) 2 ^ x))
#>   [,1] [,2] [,3] [,4] [,5]
#> x    2    4    8   16   32
#> y   64  128  256  512 1024
# 等价命令
aaply(.data=as.matrix(d), .margins = 2, .fun=function(x) 2 ^ x)
#>    
#> X1   1   2   3   4    5
#>   x  2   4   8  16   32
#>   y 64 128 256 512 1024

数据分段

shingle

Shingle对象是因子对象的连续性泛化,一个Shingle对象包括一个数字向量和一组间隔,各个间隔允许重叠,这种结构十分类似于屋檐上的瓦片结构。Shingle对象广泛应用于lattice包,它允许我们轻松地把条件或者分组变量作为连续变量使用。

lattice::shingle(x, intervals = unique(x))
#> 
#> Data:
#>  [1]  1  2  3  4  5  6  7  8  9 10
#> 
#> Intervals:
#>   min max count
#> 1   1   6     6
#> 2   2   7     6
#> 3   3   8     6
#> 4   4   9     6
#> 5   5  10     6
#> 
#> Overlap between adjacent intervals:
#> [1] 5 5 5 5

intervals参数用来指定在什么地方分割箱子,你可以用一个数值向量来指定分割的位置,也可以使用一个两列的矩阵,每一列表示一个特定的间距。equal.count函数可以用来创建一个shingle,每个箱子有相同个数的观测值:

lattice::equal.count(x)
#> 
#> Data:
#>  [1]  1  2  3  4  5  6  7  8  9 10
#> 
#> Intervals:
#>   min  max count
#> 1 0.5  3.5     3
#> 2 1.5  4.5     3
#> 3 3.5  6.5     3
#> 4 4.5  7.5     3
#> 5 6.5  9.5     3
#> 6 7.5 10.5     3
#> 
#> Overlap between adjacent intervals:
#> [1] 2 1 2 1 2

Cut

cut函数可以很方便地将一个连续性变量切割成很多个小片段。输入是一个数值向量,输出是一个因子,因子的每个水平对应输入向量的每个区间范围。

cut(x, breaks, labels = NULL, include.lowest = FALSE, right=TRUE, dig.lab=3,
ordered_result = FALSE, ...)

例如,假设我们想要统计平均击球数据在某个范围内的选手的数量,可以使用cut函数与table函数:

# 读入示例数据
library(nutshell)
#> 载入需要的程辑包:nutshell.bbdb
#> 载入需要的程辑包:nutshell.audioscrobbler
data("batting.2008")
# 首先在数据框中加入击球平均值
batting.2008.AB <- transform(batting.2008, AVG = H / AB)
# 选择100 AB以上的球员(为了统计显著性)
batting.2008.over100AB <- subset(batting.2008.AB, subset = (AB > 100))
# 把结果分为10份
battingavg.2008.bins <- cut(batting.2008.over100AB$AVG, breaks = 10)
table(battingavg.2008.bins)
#> battingavg.2008.bins
#> (0.137,0.163] (0.163,0.189] (0.189,0.215]  (0.215,0.24]  (0.24,0.266] 
#>             4             6            24            67           121 
#> (0.266,0.292] (0.292,0.318] (0.318,0.344]  (0.344,0.37]  (0.37,0.396] 
#>           132            70            11             5             2

利用分组变量合并对象

有时候,我们可能想要将几个相似的对象(向量或者数据框)合并成一个数据框,数据框中有一列用来表示数据的来源。lattice包中的make.groups函数可以实现这个功能:

library(lattice)
make.groups(...)

例如我们将下面不同的向量合成一个数据框:

hat.sizes <- seq(from = 6.25, to = 7.75, by = .25)
pants.sizes <- c(30:34, 36, 38, 40)
shoe.sizes <- seq(from=7, to=12)
lattice::make.groups(hat.sizes, pants.sizes, shoe.sizes)
#>               data       which
#> hat.sizes1    6.25   hat.sizes
#> hat.sizes2    6.50   hat.sizes
#> hat.sizes3    6.75   hat.sizes
#> hat.sizes4    7.00   hat.sizes
#> hat.sizes5    7.25   hat.sizes
#> hat.sizes6    7.50   hat.sizes
#> hat.sizes7    7.75   hat.sizes
#> pants.sizes1 30.00 pants.sizes
#> pants.sizes2 31.00 pants.sizes
#> pants.sizes3 32.00 pants.sizes
#> pants.sizes4 33.00 pants.sizes
#> pants.sizes5 34.00 pants.sizes
#> pants.sizes6 36.00 pants.sizes
#> pants.sizes7 38.00 pants.sizes
#> pants.sizes8 40.00 pants.sizes
#> shoe.sizes1   7.00  shoe.sizes
#> shoe.sizes2   8.00  shoe.sizes
#> shoe.sizes3   9.00  shoe.sizes
#> shoe.sizes4  10.00  shoe.sizes
#> shoe.sizes5  11.00  shoe.sizes
#> shoe.sizes6  12.00  shoe.sizes

随机抽样

有时候数据太多,或者出于统计或计算性能的原因,你想要将数据随机分为几部分构建模型(通常分为训练集、测试集和评估集)。

最简单地方法就是使用sample函数,它可以对一个向量做随机抽样。

sample(x, size, replace = FALSE, prob = NULL)

当对数据框做sample操作时,实际返回的是列的随机抽样结果,而不是行。因为数据框是向量的列表,sample实际抽样的是这个列表的元素。所以要注意一下。

对于观察结果做行的随机抽样,需要使用sample函数创建一组行号的抽样结果,然后再使用索引选取这些行号所对应的行。比如我们随机抽样batting.2008数据集的5条记录:

batting.2008[sample(1:nrow(batting.2008), 5), ]
#>        nameLast nameFirst weight height bats throws      debut birthYear
#> 789        Henn      Sean    215     76    R      L 2005-05-04      1981
#> 1360 Saccomanno      Mark    210     75    R      R 2008-09-08      1980
#> 1098     Pearce    Steven    200     71    R      R 2007-09-01      1983
#> 416        Wise      Matt    197     76    R      R 2000-08-02      1975
#> 202     Kendall     Jason    180     72    R      R 1996-04-01      1974
#>       playerID yearID stint teamID lgID   G G_batting  AB  R   H 2B 3B HR RBI
#> 789   hennse01   2008     1    SDN   NL   4         4   0  0   0  0  0  0   0
#> 1360 saccoma01   2008     1    HOU   NL  10        10  10  1   2  1  0  1   2
#> 1098 pearcst01   2008     1    PIT   NL  37        37 109  6  27  7  0  4  15
#> 416   wisema01   2008     1    NYN   NL   8         8   0  0   0  0  0  0   0
#> 202  kendaja01   2008     1    MIL   NL 151       151 516 46 127 30  2  2  49
#>      SB CS BB SO IBB HBP SH SF GIDP G_old
#> 789   0  0  0  0   0   0  0  0    0     4
#> 1360  0  0  0  3   0   0  0  0    0    10
#> 1098  2  0  5 22   0   3  0  2    1    37
#> 416   0  0  0  0   0   0  0  0    0     8
#> 202   8  3 50 45   7  13  6  2    5   151

还可以使用这种技术做更复杂的随机抽样,比如你想要随机统计3个对的情况,可以这样:

batting.2008$teamID <- as.factor(batting.2008$teamID)
levels(batting.2008$teamID)
#>  [1] "ARI" "ATL" "BAL" "BOS" "CHA" "CHN" "CIN" "CLE" "COL" "DET" "FLO" "HOU"
#> [13] "KCA" "LAA" "LAN" "MIL" "MIN" "NYA" "NYN" "OAK" "PHI" "PIT" "SDN" "SEA"
#> [25] "SFN" "SLN" "TBA" "TEX" "TOR" "WAS"
# 抽样例子
sample(levels(batting.2008$teamID), 3)
#> [1] "CHN" "HOU" "ATL"
# 使用例子
batting.2008.3teams <- batting.2008[is.element(batting.2008$teamID, sample(levels(batting.2008$teamID),3)), ]
batting.2008.3teams
#>      nameLast nameFirst weight height bats throws      debut birthYear
#> 5      Ankiel      Rick    210     73    L      L 1999-08-23      1979
#> 9     Aurilia      Rich    170     72    R      R 1995-09-06      1971
#> 12       Bale      John    205     76    L      L 1999-09-30      1974
#> 57  Carpenter     Chris    215     78    R      R 1997-05-12      1975
#> 99  Duckworth   Brandon    185     74    B      R 2001-08-07      1976
#> 103    Durham       Ray    170     68    B      R 1995-04-26      1971
#>      playerID yearID stint teamID lgID   G G_batting  AB  R   H 2B 3B HR RBI SB
#> 5   ankieri01   2008     1    SLN   NL 120       120 413 65 109 21  2 25  71  2
#> 9   aurilri01   2008     1    SFN   NL 140       140 407 33 115 21  1 10  52  1
#> 12   balejo01   2008     1    KCA   AL  13         0   0  0   0  0  0  0   0  0
#> 57  carpech01   2008     1    SLN   NL   4         4   5  0   0  0  0  0   0  0
#> 99  duckwbr01   2008     1    KCA   AL   7         0   0  0   0  0  0  0   0  0
#> 103 durhara01   2008     1    SFN   NL  87        87 263 43  77 23  0  3  32  6
#>     CS BB  SO IBB HBP SH SF GIDP G_old
#> 5    1 42 100   3   5  0  3    8   120
#> 9    1 30  56   4   1  0  2   11   140
#> 12   0  0   0   0   0  0  0    0    13
#> 57   0  0   3   0   0  0  0    0     4
#> 99   0  0   0   0   0  0  0    0     7
#> 103  2 38  49   0   2  0  1    6    87
#>  [ reached 'max' / getOption("max.print") -- omitted 129 rows ]

这个函数对于数据的各种复杂抽样非常方便,但你可能还需要用到更复杂的抽样方式,比如分层抽样、整群抽样、最大熵抽样,这些方法都可以在sampling包中找到。

汇总函数

tapply与aggregate

tapply函数用于向量的汇总分析,是一个非常灵活的函数。可以设置对向量X的某个子集做汇总,也可以指定汇总函数:

tapply(X, INDEX, FUN = , ..., simplify = )

比如计算各队本垒打的总数:

tapply(X=batting.2008$HR, INDEX=list(batting.2008$teamID), FUN=sum)
#> ARI ATL BAL BOS CHA CHN CIN CLE COL DET FLO HOU KCA LAA LAN MIL MIN NYA NYN OAK 
#> 159 130 172 173 235 184 187 171 160 200 208 167 120 159 137 198 111 180 172 125 
#> PHI PIT SDN SEA SFN SLN TBA TEX TOR WAS 
#> 214 153 154 124  94 174 180 194 126 117

也可以使用返回多个值的函数,例如fivenum计算各个联盟球员击球平均数:

tapply(batting.2008$H/batting.2008$AB, INDEX=list(batting.2008$lgID), fivenum)
#> $AL
#> [1] 0.000 0.176 0.249 0.283 1.000
#> 
#> $NL
#> [1] 0.0000 0.0952 0.2173 0.2680 1.0000

plyr包中没有tapply的等价功能。

by是和tapply有密切关系的一个函数,区别在于by是用于数据框的。下面是一个例子:

by(batting.2008[, c("H", "2B", "3B", "HR")],
   INDICES = list(batting.2008$lgID, batting.2008$bats), FUN=mean)
#> Warning in mean.default(data[x, , drop = FALSE], ...): 参数不是数值也不是逻辑
#> 值:回覆NA

#> Warning in mean.default(data[x, , drop = FALSE], ...): 参数不是数值也不是逻辑
#> 值:回覆NA

#> Warning in mean.default(data[x, , drop = FALSE], ...): 参数不是数值也不是逻辑
#> 值:回覆NA

#> Warning in mean.default(data[x, , drop = FALSE], ...): 参数不是数值也不是逻辑
#> 值:回覆NA

#> Warning in mean.default(data[x, , drop = FALSE], ...): 参数不是数值也不是逻辑
#> 值:回覆NA

#> Warning in mean.default(data[x, , drop = FALSE], ...): 参数不是数值也不是逻辑
#> 值:回覆NA
#> : AL
#> : B
#> [1] NA
#> ------------------------------------------------------------ 
#> : NL
#> : B
#> [1] NA
#> ------------------------------------------------------------ 
#> : AL
#> : L
#> [1] NA
#> ------------------------------------------------------------ 
#> : NL
#> : L
#> [1] NA
#> ------------------------------------------------------------ 
#> : AL
#> : R
#> [1] NA
#> ------------------------------------------------------------ 
#> : NL
#> : R
#> [1] NA

另一个用于数据汇总的函数是aggregate

aggregate(x, by, FUN, ...)

也可以用于时间序列,参数略有不同。

下面看一个按球队统计击球数的例子:

aggregate(x=batting.2008[, c("AB", "H", "BB", "2B", "3B", "HR")],
          by=list(batting.2008$teamID), FUN=sum)
#>    Group.1   AB    H  BB  2B 3B  HR
#> 1      ARI 5409 1355 587 318 47 159
#> 2      ATL 5604 1514 618 316 33 130
#> 3      BAL 5559 1486 533 322 30 172
#> 4      BOS 5596 1565 646 353 33 173
#> 5      CHA 5553 1458 540 296 13 235
#> 6      CHN 5588 1552 636 329 21 184
#> 7      CIN 5465 1351 560 269 24 187
#> 8      CLE 5543 1455 560 339 22 171
#> 9      COL 5557 1462 570 310 28 160
#> 10     DET 5641 1529 572 293 41 200
#> 11     FLO 5499 1397 543 302 28 208
#> 12     HOU 5451 1432 449 284 22 167
#> 13     KCA 5608 1507 392 303 28 120
#> 14     LAA 5540 1486 481 274 25 159
#> 15     LAN 5506 1455 543 271 29 137
#> 16     MIL 5535 1398 550 324 35 198
#> 17     MIN 5641 1572 529 298 49 111
#> 18     NYA 5572 1512 535 289 20 180
#> 19     NYN 5606 1491 619 274 38 172
#> 20     OAK 5451 1318 574 270 23 125
#> 21     PHI 5509 1407 586 291 36 214
#> 22     PIT 5628 1454 474 314 21 153
#> 23     SDN 5568 1390 518 264 27 154
#> 24     SEA 5643 1498 417 285 20 124
#> 25     SFN 5543 1452 452 311 37  94
#> 26     SLN 5636 1585 577 283 26 174
#> 27     TBA 5541 1443 626 284 37 180
#> 28     TEX 5728 1619 595 376 35 194
#>  [ reached 'max' / getOption("max.print") -- omitted 2 rows ]

计数

使用tabulatetable函数。