`dplyr::group_by`的進階運用

大家好,今天想跟大家介紹一個使用dplyr時的一個小撇步。他可以讓我們在使用group_by之後,有更多的彈性,並會更理解group_by這好用功能的原理。

背景簡介

我由於工作上要研究網路廣告,常常要比較各種不同方法間的數據。舉例來說,我可能手上有一大筆看起來像這樣的資料:

廣告 網站 點擊
1 廣告D 網站C 0
2 廣告A 網站C 0
3 廣告A 網站C 0
4 廣告B 網站C 1
5 廣告B 網站B 0
6 廣告B 網站B 0

而我想要知道各種廣告在網站A上的表現。透過dplyr,這並不困難:

filter(sample, 網站 == "網站A") %>%  
  group_by(廣告) %>%
  summarise(曝光 = length(點擊), 點擊數 = sum(點擊))
廣告 曝光 點擊數
1 廣告A 329 0
2 廣告B 588 4
3 廣告C 81 0
4 廣告D 71 0

然而,以這個資料來說,我並不確定廣告C廣告D的成效是不是明顯的不同。我想大家能同意,廣告的點擊是有機率在背後的,所以我們必須要透過一些計算才能知道,數據上顯示的:「廣告B比其他廣告好」,是運氣比較好,還是真的比較好。

R 有一個製作95%信賴區間很方便的套件:binom。只要簡單利用binom.confint,就可以快速算出各種方法的confidence interval。
以下就是一個簡單的範例,我們使用預設的95%信心水準(可以使用參數conf.level來調整),統計方法是"exact"(由Clopper and Pearson (1934)所設計出來的方法)。binom套件提供了10種方法給使用者計算信賴區間。

library(binom)  
channel.a.summary <- filter(sample, 網站 == "網站A") %>%  
  group_by(廣告) %>%
  summarise(曝光 = length(點擊), 點擊數 = sum(點擊))
binom.confint(x = channel.a.summary$點擊數, n = channel.a.summary$曝光, methods = "exact")  
method x n mean lower upper
1 exact 0 329 0.00 0.00 0.01
2 exact 4 588 0.01 0.00 0.02
3 exact 0 81 0.00 0.00 0.04
4 exact 0 71 0.00 0.00 0.05

所以根據binom.confint的結果,其實這樣的差距很有可能是來自於運氣,因為他們的信賴區間交錯的不少。

結合group_by + binom.confint

然而剛剛的程式碼,其實可以利用dplyrdo來改進:

filter(sample, 網站 == "網站A") %>%  
  group_by(廣告) %>%
  do(binom.confint(sum(.$點擊), length(.$點擊), methods = "exact"))
廣告 method x n mean lower upper
1 廣告A exact 0 329 0.00 0.00 0.01
2 廣告B exact 4 588 0.01 0.00 0.02
3 廣告C exact 0 81 0.00 0.00 0.04
4 廣告D exact 0 71 0.00 0.00 0.05

這裡的do是一個很有趣的函數。當我們傳遞一個group_by處理過得的table,do會依照group_by的欄位將原本的table切割成若干的子table,並且運用變數.來代表這個子table。然後,再利用do的參數函數來個別計算出結果,最後再拼裝成原本的輸出。

do和原本summarise的差別,就是更有彈性,並且可以一次輸出多欄位。舉例來說,上述的binom.confint就一次輸出meanlowerupper等資訊,而不像summarise,裡面使用的函數一次只能輸出一個值。

do細解

接下來,我簡單說明do的用法。請有興趣的朋友執行以下的程式碼:

group_by(iris, Species) %>% do(browser())  

由於browser的緣故,在do處理子table的時候會暫停,所以各位朋友就可以看到:

Browse[1]> .  
Source: local data frame [50 x 5]

   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
          (dbl)       (dbl)        (dbl)       (dbl)  (fctr)
1           5.1         3.5          1.4         0.2  setosa  
2           4.9         3.0          1.4         0.2  setosa  
3           4.7         3.2          1.3         0.2  setosa  
4           4.6         3.1          1.5         0.2  setosa  
5           5.0         3.6          1.4         0.2  setosa  
6           5.4         3.9          1.7         0.4  setosa  
7           4.6         3.4          1.4         0.3  setosa  
8           5.0         3.4          1.5         0.2  setosa  
9           4.4         2.9          1.4         0.2  setosa  
10          4.9         3.1          1.5         0.1  setosa  
..          ...         ...          ...         ...     ...
Browse[1]> nrow(.)  
[1] 50

第一次暫停的時候,這個.就代表所有Species等於"setosa"的table,也就是filter(iris, Species == "setosa")的結果。 各位可以從nrow(.)得到50看出。

這樣的暫停會有三次,第二次是filter(iris, Species == "versicolor"),第三次是filter(iris, Species == "virginica")

do裡面的函數必須輸出data.framedplyr最後做組裝。舉例來說:

group_by(iris, Species) %>% do(sum(.$Petal.Width))  
## Error: Results are not data frames at positions: 1, 2, 3

由於sum(.$Petal.Width)不是一個data.frame,所以就發生錯誤了。必須要用:

group_by(iris, Species) %>% do(data.frame(sum = sum(.$Petal.Width)))  
## Source: local data frame [3 x 2]
## Groups: Species [3]
## 
##      Species   sum
##       (fctr) (dbl)
## 1     setosa  12.3
## 2 versicolor  66.3
## 3  virginica 101.3

結語

透過group_by + do,我們就可以再簡化程式碼,一次group_by就可以輸出信賴區間相關的分析結果,程式碼也簡潔。這一招是我最近學到的密技,今天就簡單跟大家作個說明,希望以上的內容能幫上大家囉。