R語言中的類SQL操作
plyr包可以進(jìn)行類似于數(shù)據(jù)透視表的操作,將數(shù)據(jù)分割成更小的數(shù)據(jù),對分割后的數(shù)據(jù)進(jìn)行些操作,最后把操作的結(jié)果匯總。
本文主要介紹以下內(nèi)容:
- Split-Aapply-Combine 原理介紹
- baby_names的名字排名
- 求分段擬合的系數(shù)
- 部分其他函數(shù)介紹
在正式開始之前,請確保電腦上已經(jīng)安裝plyr,如果沒有,通過install.packages()函數(shù)安裝。
install.packages(plyr) # 安裝plyr包
require(plyr) #載入plyr包
假設(shè)有美國新生嬰兒的取名匯總,每一年,會統(tǒng)計(jì)男孩和女孩的取名情況,形成如下的一張表。
| year| name| percent| sex|
|---------:||---------:||---------:||---------:|
| 1880| John| 0.081541| boy|
| 1880| William| 0.080511| boy|
| 1880| James| 0.050057| boy|
| 1880| Charles| 0.045167| boy|
| 1880| George| 0.043292| boy|
| 1880| Frank| 0.02738| boy|
| 1880| Joseph| 0.022229| boy|
| 1880| Thomas| 0.021401| boy|
baby_names數(shù)據(jù)集包含1880 ~ 2008年間的數(shù)據(jù), 包含統(tǒng)計(jì)的年份(year),新生嬰兒的性別、名字、以及改名字的比例。
以提問并解決問題的形式對plyr做介紹。
- 想知道數(shù)據(jù)集中,每年都有多少記錄?
- 數(shù)據(jù)集中,男孩和女孩名的各自排名?
- 男孩名和女孩名各自排名前100在當(dāng)年中的比例?
數(shù)據(jù)集中,每年都有多少記錄
先假設(shè)我們有某一年的數(shù)據(jù),我們會如何統(tǒng)計(jì)其中的記錄數(shù)呢?由于數(shù)據(jù)集中,每條記錄一行,只需要統(tǒng)計(jì)對應(yīng)的行數(shù)就可以得到對應(yīng)的記錄數(shù)。
寫個(gè)函數(shù)試試
record_count <- function(df) {
return(data.frame(count = nrow(df)))
}
返回值類型是data.frame類型,是為即將介紹的ddply()函數(shù)做鋪墊。先來看看2008年,數(shù)據(jù)集中有多少記錄。
baby_names_2008 <- subset(baby_names, year == 2008)
record_count(baby_names_2008)
# 2000
結(jié)果顯示2000條,貌似我們已經(jīng)得到答案。下面想想,該如何得到1880 ~ 2008這129年間,每年的記錄數(shù)呢?
ddply(baby_names, # 數(shù)據(jù)集
.(year), # 分類的標(biāo)準(zhǔn)
record_count # 函數(shù)
)
結(jié)果比較長,只摘取其中一部分
| year | count |
|---|---|
| 1880 | 2000 |
| 1881 | 2000 |
| 1882 | 2000 |
| 1883 | 2000 |
| 1884 | 2000 |
| 1885 | 2000 |
| 1886 | 2000 |
| 1887 | 2000 |
| 1888 | 2000 |
| 1889 | 2000 |
| 1890 | 2000 |
| 1891 | 2000 |
| 1892 | 2000 |
不錯(cuò),每年都是2000條記錄。再來看看,剛在我們做了什么。
- 定義了一個(gè)負(fù)責(zé)計(jì)數(shù)的函數(shù)
record_count() - 調(diào)用
ddply(),這里出現(xiàn)剛剛定義的函數(shù)
ddply()函數(shù)是plyr包中用于對data.frame結(jié)構(gòu)的數(shù)據(jù)做處理的函數(shù),其結(jié)果也是data.frame。ddply的參數(shù)列表如下:
<pre>
ddply(.data, .variables, .fun = NULL, ..., .progress = "none",
.inform = FALSE, .drop = TRUE, .parallel = FALSE, .paropts = NULL)
</pre>
各部分解釋如下
- 第一個(gè)參數(shù)是要操作的原始數(shù)據(jù)集,比如
baby_name - 第二個(gè)參數(shù)是按照某個(gè)(也可以幾個(gè))變量,對數(shù)據(jù)集分割,比如按照
year對數(shù)據(jù)集分割,可以寫成.(year)的形式 - 第三個(gè)參數(shù)是具體執(zhí)行操作的函數(shù),對分割后的每一個(gè)子數(shù)據(jù)集,調(diào)用該函數(shù)
- 第四個(gè)參數(shù)可選,表示第三個(gè)參數(shù)對應(yīng)函數(shù)所需的額外參數(shù)
其他參數(shù),可以暫時(shí)不用考慮。ddply()函數(shù)會自動的將分割后的每一小部分的計(jì)算結(jié)果匯總,以data.frame的格式保存。<span style="color:red">分割后的數(shù)據(jù),是fun的第一個(gè)參數(shù)。</span>
在上面的描述中,提到的分割、操作、匯總,在plyr包中是一種處理方式("frame"),即"Split - Apply - Combine"。在plyr包中有很多這種處理方式的函數(shù),在介紹這些函數(shù)之前,我們再來看看ddply()的一些更深入的用法。
各年,男孩名與女孩名的各自排名
以2008年的數(shù)據(jù)為例,男孩名"Jacob"的比例最高,排名應(yīng)當(dāng)是第一,"Michael"緊跟其后,排名應(yīng)當(dāng)?shù)诙?,依此類推。對于女孩名?Emma"排名第一,"Isabella"排名第二,"Emily"排名第三等等。我們希望得到這樣的結(jié)果。
對于2008年的數(shù)據(jù),可以通過簡單的rank即可得到,不過要對男孩和女孩分別排序。
baby_names_2008_boy <- subset(baby_names_2008, sex == "boy") # 獲取男孩名
baby_names_2008_boy$rank <- rank(- baby_names_2008_boy$percent) # 排序
head(baby_names_2008_boy) # 查看
對女孩名也執(zhí)行相同的操作,這里就不寫出來了,只需要在subset中,將"boy"替換成"girl"就行。下面來看看2008年,男孩名的排名情況
| year | name | percent | sex | rank |
|---|---|---|---|---|
| 2008 | Jacob | 0.010355 | boy | 1 |
| 2008 | Michael | 0.009437 | boy | 2 |
| 2008 | Ethan | 0.009301 | boy | 3 |
| 2008 | Joshua | 0.008799 | boy | 4 |
| 2008 | Daniel | 0.008702 | boy | 5 |
| 2008 | Alexander | 0.008566 | boy | 6 |
再來看看女孩名的排名結(jié)果:
| year | name | percent | sex | rank |
|---|---|---|---|---|
| 2008 | Emma | 0.009043 | girl | 1 |
| 2008 | Isabella | 0.008941 | girl | 2 |
| 2008 | Emily | 0.008377 | girl | 3 |
| 2008 | Madison | 0.008199 | girl | 4 |
| 2008 | Ava | 0.008198 | girl | 5 |
| 2008 | Olivia | 0.008196 | girl | 6 |
如何利用ddply()對原始數(shù)據(jù)集做相應(yīng)的操作呢?這里需要介紹R語言中的一個(gè)函數(shù)transform(),該函數(shù)對原始數(shù)據(jù)集做一些操作,并把結(jié)果存儲在原始數(shù)據(jù)中,更詳細(xì)的用法,參見幫助文檔?transform。
第一個(gè)版本的處理方式是這樣的
ddply(baby_names,
.(year, sex),
transform,
rank = rank(-percent, ties.method = "first")
)
第二個(gè)參數(shù)有點(diǎn)變化,除了year,還有sex,這表示對baby_name數(shù)據(jù)集,對year和sex分類(類似于SQL中的group by year, sex)。
第四個(gè)參數(shù)是transform的額外參數(shù),如果查看transform的幫助文檔,其函數(shù)調(diào)用方式如下:
<pre>
transform(_data, ...)
</pre>
第一參數(shù)為操作的數(shù)據(jù),在ddply()中為按年份和性別分割后的子數(shù)據(jù)集;后面的...參數(shù)是tag = value的形式,這種tag:value將追加在數(shù)據(jù)中。
由于rank默認(rèn)對數(shù)據(jù)進(jìn)行升序排序,若要實(shí)現(xiàn)逆序排序,常規(guī)的做法是將數(shù)據(jù)的符號取反,這也就是上面的rank函數(shù)中出現(xiàn)-percent的原因。在plyr中,有一個(gè)類似的函數(shù),實(shí)現(xiàn)取反的操作,是desc。
x <- 1:10
desc(x)
# -1 -2 -3 -4 -5 -6 -7 -8 -9 -10
所以,上面對percent取反的操作,可以寫得更優(yōu)雅些,就有了第二個(gè)版本的函數(shù)
baby_names <- ddply(baby_names,
.(year, sex),
transform,
rank = rank(desc(percent), ties.method = "first")
)
注意這里把結(jié)果賦給了baby_name,因?yàn)楹竺孢€會用到排名的信息,就把結(jié)果保存下來。
** 排名前100的男孩名與女孩名在當(dāng)年中的比例**
跟前一問類似,處理方法是:
- 把每年排名前100的數(shù)據(jù)篩選出來
- 把男孩和女孩對應(yīng)的
percent相加
baby_names_top100 <- subset(baby_names, rank <= 100) # 將前100排名的數(shù)據(jù)篩選出來
baby_names_top100_trend <- ddply(baby_names_top100,
.(year, sex), # 按年和性別分割
summarize, # 匯總數(shù)據(jù)
trend = sum(percent)) # 匯總方式(求和)
這里出現(xiàn)一個(gè)新的操作函數(shù)summarize(),該函數(shù)是對數(shù)據(jù)做匯總,與transform不一樣的是,該函數(shù)并不追加結(jié)果到原始數(shù)據(jù),而是產(chǎn)生新的數(shù)據(jù)集。比如想知道,2008年的男孩名中,排名最高和最低的名字的百分比之差,可以通過如下方式求得:
summarize(baby_names_2008_boy, trend = max(percent) - min(percent))
# 0.010266
回到剛才的問題,從1880 ~ 2008年間,男孩名與女孩名的前100所占比例(可以衡量名字大眾化的程度)到底是什么樣的呢?畫個(gè)圖就知道了。

還有什么類似函數(shù)
上面介紹的ddply()是plyr包中處理data.frame的函數(shù),還有處理list,array的函數(shù),匯總起來如下
| arrary | data.frame | list | discarded | |
|---|---|---|---|---|
| arrary | aaply | adply | alply | a_ply |
| data.frame | daply | ddply | dlply | d_ply |
| list | laply | ldply | llply | l_ply |
所有的函數(shù)具有xyply的形式,其中x表示數(shù)據(jù)數(shù)據(jù)類型,y表示輸出數(shù)據(jù)類型,而_表示丟棄。
應(yīng)用舉例
在R語言基礎(chǔ)數(shù)據(jù)集中,有mtcars數(shù)據(jù),其中記錄了車重"weight"、"miles per galon"、"cylinder"等參數(shù)。由圖可知,不同氣缸下,車重與行駛里程有著不同的關(guān)系,如果以線性函數(shù)來刻畫,是三條有著明顯區(qū)別的函數(shù)。

該如何求著三條直線的參數(shù)呢(截距與斜率)?
將問題簡化下,對于數(shù)據(jù)集df,有自變量x,因變量y,如何求y = a x + b的參數(shù)a和b?寫個(gè)函數(shù)試試
linear_fit <- function(df) {
model <- lm(mpg ~ wt, df)
linear_coef <- coef(model)
linear_coef <- data.frame(intercept = linear_coef[1],
slope = linear_coef[-1])
row.names(linear_coef) <- NULL
linear_coef
}
下面再應(yīng)用split - apply - combine的思想求出每一種cyl對應(yīng)數(shù)據(jù)的截距和斜率
mtcars_coef <- ddply(mtcars, .(cyl), linear_fit)
names(mtcars_coef)[2:3] <- c("intercept", "slope")
所得擬合直線的截距和斜率為
| cyl | intercept | slope | |
|---|---|---|---|
| 1 | 4 | 39.57120 | -5.647025 |
| 2 | 6 | 28.40884 | -2.780106 |
| 3 | 8 | 23.86803 | -2.192438 |
再結(jié)合這原圖,把這些直線畫出來,與原圖做個(gè)比較。

黑色的線為擬合的曲線,而彩色短線為系統(tǒng)所繪制的擬合曲線,說明我們的方法正確。
再來看看上面的擬合過程,將對每個(gè)子數(shù)據(jù)集的擬合封裝成一個(gè)函數(shù)linear_fit,這樣做沒有問題,但是使得代碼的可讀性比較差,一種比價(jià)優(yōu)雅的方式是在dlply的第三個(gè)參數(shù)處,直接放上lm函數(shù),將額外的參數(shù)賦給第四個(gè)參數(shù)。
mtcars_model <- dlply(mtcars, .(cyl), lm,
formula = mpg ~ wt)
mtcars_coef <- ldply(mtcars_model, coef)
names(mtcars_coef)[2:3] <- c("intercept", "slope")
注意,這里通過dlply()函數(shù)調(diào)用擬合函數(shù)lm,而把具體的擬合形式formula = mpg ~ wt賦值給第四個(gè)參數(shù)。dlply()函數(shù)返回的是list,list的每個(gè)元素是一個(gè)lm的返回結(jié)果,通過ldply()調(diào)用coef獲得每個(gè)模型對應(yīng)的系數(shù),記得到上述結(jié)果。
讀入多個(gè)文件中的數(shù)據(jù),并合并
下面來看看一個(gè)實(shí)際生活中的問題:
假設(shè)文件夾下有若干.csv文件, 每個(gè)文件的數(shù)據(jù)格式相同,且含有表頭,如何將多個(gè)文件合并成一個(gè)文件呢?
如果沒有表頭的話,操作起來比較容易,可以直接用命令行工具實(shí)現(xiàn),比如在linux下可以cat *.csv > total.csv實(shí)現(xiàn)文件合并。 此處給出一種使用plyr包中提供的ldaply的函數(shù),實(shí)現(xiàn)上出操作,效率不一定是最高的,但可以進(jìn)一步掌握plyr包的特性。
可以繼續(xù)使用上述使用的baby_names數(shù)據(jù)集,使用如下命令, 將baby_names按年份寫到不同的csv文件中。
d_ply(baby_names, .(year),
function(baby) write.csv(baby, paste0(baby$year[1], ".csv"), row.names = FALSE)
)
上述命令將在當(dāng)前文件夾下,產(chǎn)生129個(gè)csv文件,從1880 ~ 2008, 每年一個(gè)文件,以年份命名。
使用如下的命令將
files <- list.files(pattern = "^\\d+\\.csv")
baby_names_recovered <- ldply(files, read.csv, stringsAsFactors = FALSE)
上述命令將129個(gè)文件名存儲在files變量中,通過ldply,讀取每個(gè)文件,并最后通過ldply合并成一個(gè)data.frame。需要說明的是ldply的第一個(gè)參數(shù)要求list,但是files變量卻是vector,這個(gè)沒有影響,函數(shù)內(nèi)部會將第一個(gè)參數(shù)通過as.list()轉(zhuǎn)換成list。
現(xiàn)在需要驗(yàn)證讀入的baby_names_recovered與原始的baby_names一致,使用如下參數(shù)可以做相應(yīng)的比較。
identical(arrange(baby_names, year, name, sex), arrange(baby_names_recovered, year, name, sex))
# TRUE
返回的結(jié)果是TRUE,即二者其實(shí)是一致的。至于為什么要用arrange函數(shù)對數(shù)據(jù)做一下排列,是因重新生成的baby_names_recovered,其讀入數(shù)據(jù)的順序并沒有嚴(yán)格按照年份進(jìn)行。
這里拋出一個(gè)問題,如果不使用plyr包,如何實(shí)現(xiàn)上述操作。
提示:查閱lapply和do.call函數(shù),剩下的函數(shù),已經(jīng)在上面的示例中講解。
部分其他函數(shù)
這一部分將簡略介紹plyr 包中未提及的函數(shù),以及其用法。
未完待續(xù)
參考文獻(xiàn)