A/B テストで施策の効果を検証!エンジニアのための R 入門


 (@a_bicky) 

R A/B 




A/B  KPI 











R使

  



Python, Julia 使






OS Mac Windows  Linux  R 3.2.0 

R


使


使

R

RStudio  ESS  IDE 

Ruby  gem

ggplot2 使

data.table, dplyr, stringi 


R*1



R



IDE 



R使

使



data.frame 



A/B 

 (CVR) 

CVR 







R





R


Mac  Homebrew Linux 
% brew tap homebrew/science
% brew install r

R
% R

R version 3.2.0 (2015-04-16) -- "Full of Ingredients"
Copyright (C) 2015 The R Foundation for Statistical Computing
Platform: x86_64-apple-darwin14.3.0 (64-bit)Rは、自由なソフトウェアであり、「完全に無保証」です。
 一定の条件に従えば、自由にこれを再配布することができます。
 配布条件の詳細に関しては、'license()' あるいは 'licence()' と入力してください。Rは多くの貢献者による共同プロジェクトです。
 詳しくは 'contributors()' と入力してください。
 また、RRのパッケージを出版物で引用する際の形式については
 'citation()' と入力してください。

 'demo()' と入力すればデモをみることができます。
 'help()' とすればオンラインヘルプが出ます。
 'help.start()' で HTML ブラウザによるヘルプがみられます。
 'q()' と入力すればRを終了します。

>

R使

R q Ctrl-D 
>q()
Save workspace image? [y/n/c]:n

Save workspace image? y .RData 


2
















% # 言語の英語化
% echo 'LANGUAGE=en' >> ~/.Renviron
% # パッケージをインストールする際のリポジトリを設定
% echo 'options(repos = "http://cran.ism.ac.jp")' >> ~/.Rprofile

.Renviron R

.Rprofile options  repos *2

IDE 


R使 IDE 便 IDE   IDE 2


RStudio

ESSEmacs 


RStudio 使 Option+Shift+KWindows  Alt+Shift+K 

ESS  MELPA  init.el  M-x package-list-packages  ess melpa-stable  ess-15.3 
(when (require 'package nil t)
  (add-to-list 'package-archives '("melpa-stable" . "http://melpa-stable.milkbox.net/packages/") t)
  (package-initialize))

ESS 

R使

使


使R
% R -q  # -q オプションを指定するとスタートメッセージを表示しない
>

 TAB 
> read
read.csv          read.delim        read.fortran      read.socket       readChar          readLines
read.csv2         read.delim2       read.ftable       read.table        readCitationFile  readRDS
read.dcf          read.DIF          read.fwf          readBin           readline          readRenviron

 ?help 
> # print 関数のドキュメントを参照する
> ?print
> help("print")

 data.table::fread  & 
> # 特殊なシンボルはバッククォートで括る
> ?`::`
> ?`&`


> R.version
               _                           
platform       x86_64-apple-darwin13.4.0   
arch           x86_64                      
os             darwin13.4.0                
system         x86_64, darwin13.4.0        
status                                     
major          3                           
minor          2.0                         
year           2015                        
month          04                          
day            16                          
svn rev        68180                       
language       R                           
version.string R version 3.2.0 (2015-04-16)
nickname       Full of Ingredients         


> # print 関数の内容を表示
> print
function (x, ...) 
UseMethod("print")
<bytecode: 0x7f9782441a38>
<environment: namespace:base>

R


GDB  Ruby  pry-byebug, pry-stack_explorer R便 使
内容 R GDB pry-byebug/pry-stack_explorer
コールスタックを表示 where bt show-stack
ステップオーバー n n next
ステップイン s s step
ステップアウト f fin step
次のブレークポイントまで実行 c c continue
ヘルプを表示 help h help
処理を中断して終了 Q q quit
関数 f にブレークポイントを設定 debug(f) break f break ClassName#f
関数 f のブレークポイントを削除 undebug(f) clear f break –delete <breakpoint number&rt;
現在の位置にブレークポイントを設定 browser() binding.pry

 debug 使  traceback 
>f<- function(x) {y<- as.character(x);g(y) }
>g<- function(x) sum(x)
>f(1:5)
Error in sum(x) : invalid 'type' (character) of argument
> traceback()
2:g(y)at#1
1:f(1:5)

 gdebug(g)  g
> debug(g)
>f(1:5)
debugging in:g(y)
debug: sum(x)
Browse[2]> 

R辿parent.frame  evalq 使
Browse[2]> # 親フレーム(関数f)のオブジェクト一覧
Browse[2]> evalq(ls(), parent.frame())
[1] "x" "y"
Browse[2]> # 親フレーム(関数f)のxの値
Browse[2]> evalq(x, parent.frame())
[1] 1 2 3 4 5

使

edit  trace 使

data.frame 


R data.frame   data.frame 使
> # R のデータの例でよく使われる iris データ
> head(iris)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
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
> # 各列のクラス
> sapply(iris, class)
Sepal.Length  Sepal.Width Petal.Length  Petal.Width      Species 
   "numeric"    "numeric"    "numeric"    "numeric"     "factor" 
> # 1 行目のデータを取得
> iris[1, ]
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
> # Species の列を取得
> head(iris$Species)
[1] setosa setosa setosa setosa setosa setosa
Levels: setosa versicolor virginica

A/B 


R使使

 (LP) LPLP 4/1  4/7 1 A/B LP


使使
> pkgs <-c("data.table", "dplyr", "tidyr", "ggplot2")
> install.packages(pkgs)
> # 各パッケージのバージョン
> sapply(pkgs, packageVersion, simplify = FALSE)
$data.table
[1]1.9.4’

$dplyr
[1]0.4.1’

$tidyr
[1]0.2.0’

$ggplot2
[1]1.0.1’

使 create_sample_data   source 使source  URL 
> # R 3.2.0 でないと https には対応していないので注意
> source("https://gist.githubusercontent.com/abicky/3a4789c3fd163a71606c/raw/5f1aeb86b8f0eb50caf386aa3fce9bc5354df9b5/create_sample_data.R")
> # 擬似データ(40 MB 程度)の作成
> file_paths <- create_sample_data()
Creating logs of 'a' pattern .......... done
Creating logs of 'b' pattern .......... done
Writing files done

create_sample_data  event_logs.tsv  access_logs.tsv  list event_log_file  access_log_file 

event_logs.tsv  access_logs.tsv   read.table 使read.table  data.table  fread 使*3data.table data.frame  data.table 使fread  data.table 
> # file_paths$event_log_file には event_logs.tsv の絶対パスが格納されている
> event_logs <- data.table::fread(file_paths$event_log_file)
> event_logs
              time   user_id event pattern
     1: 1428373621 201681931   imp       a
     2: 1428299552 898389685   imp       a
     3: 1427862703 944675268   imp       a
     4: 1428109708 660797792   imp       a
     5: 1428236105 629114044   imp       a
    ---                                   
259289: 1427877582 671321141    cv       b
259290: 1428203926 733054604    cv       b
259291: 1427948288 590425796    cv       b
259292: 1428140625  29272541    cv       b
259293: 1428052793 466384837    cv       b
> # file_paths$access_log_file には access_logs.tsv の絶対パスが格納されている
> access_logs <- data.table::fread(file_paths$access_log_file)
> access_logs
               time   user_id  page
      1: 1427862710 944675268 page1
      2: 1427862739 944675268 page3
      3: 1427862750 944675268 page1
      4: 1428236117 629114044 page2
      5: 1428236120 629114044 page1
     ---                           
1251843: 1430046543 466384837 page1
1251844: 1430220035 466384837 page2
1251845: 1430220043 466384837 page1
1251846: 1430220055 466384837 page3
1251847: 1430220071 466384837 page2

event_logs  time  unix timeevent  imp  cv LP impression  conversionpattern aLPbLP

access_logs  time  unix timepage  page 使

 (CVR) 


CVR  dplyr 使dplyr SQL *4

 tbl_df  dplyr  data.frame data.table 


(一)data.table  POSIXlt 

(二) tbl_df 

(三)data.table  data.frameread.table  dplyr 

> # パッケージのロード(C++ の using namespace dplyr、Python の from dplyr import * に相当)
> library(dplyr)

Attaching package: 'dplyr'

The following object is masked from 'package:stats':

    filter

The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
> event_log_df <- tbl_df(event_logs)

CVR  imp cvUUSQL 
SELECT
  pattern,
  event,
  COUNT(DISTINCT(user_id)) uu
FROM
  event_logs
GROUP BY
  pattern,
  event
;

 SQL 


(一)pattern, event 

(二) user_id 


 dplyr 
> event_counts <- event_log_df %>%
+   # 1. pattern, event でグループ化する
+   group_by(pattern, event) %>%
+   # 2. それぞれのグループに関してユニークな user_id をカウントする
+   summarize(uu= n_distinct(user_id))
> event_counts
Source: local data frame [4x3]
Groups: pattern

  pattern event     uu
1       a    cv  30097
2       a   imp 100399
3       b    cv  28901
4       b   imp  99896

%>% dplyr *5dplyr 使 pipe (|) 

%>% 使
event_counts <- dplyr::summarize(
  dplyr::group_by(event_log_df, pattern, event),uu= n_distinct(user_id)
)

cv / imp 
pattern event uu
a cv 30097
a imp 100399
b cv 28901
b imp 99896

これを次のような横長のデータに変換できれば cv の列を imp の列で割るだけで CVR が算出できます。

pattern cv imp
a 30097 100399
b 28901 99896

 tidyr  spread 使tidyr 便 spread 1 imp, cv  event 21 event uuuu
> event_counts %>%
+   tidyr::spread(event,uu)
Source: local data frame [2x3]

  pattern    cv    imp
1a30097 100399
2b28901  99896

event  cv imp 
> event_counts_with_cvr <- event_counts %>%
+   tidyr::spread(event,uu) %>%
+   # cvr という列を追加
+   dplyr::mutate(cvr =cv/ imp)
> event_counts_with_cvr
Source: local data frame [2x4]

  pattern    cv    imp       cvr
1a30097 100399 0.2997739
2b28901  99896 0.2893109

pattern aLP CVR  30.0 %  pattern b  28.9 % 

CVR 


LP CVR  28.9 %  30.0 % 

 prop.test 使prop.test 121cv2 imp alternative  greater  less 
> # event_counts_with_cvr のcv列の内容を取得
>cv<- event_counts_with_cvr$cv> # event_counts_with_cvr の imp 列の内容を取得
> imp <- event_counts_with_cvr$imp
> prop.test(cv, imp, alternative = "greater")

    2-sample test for equality of proportions with continuity
    correction

data:  cv out of imp
X-squared = 26.331,df= 1,p-value = 1.438e-07
alternative hypothesis: greater
95 percent confidence interval:
 0.007102618 1.000000000
sample estimates:
   prop 1    prop 2 
0.2997739 0.2893109 



2-sample test for equality of proportions with continuity correction 2 (continuity correction) *6

X-squared = 26.331, df = 1, p-value = 1.438e-07  p-value pattern a  CVR  pattern b  CVR CVR  A/B *7CVR  1.44 ×10-7 

95 %  CVR 



p-value  0.05 5 %  CVR e.g. 


LP CVR  n*8

SQL UUPostgreSQL
SELECT
  pattern,
  elapsed_days,
  uu,
  uu::float / first_value(uu) OVER (PARTITION BY pattern ORDER BY elapsed_days)  retention_ratio
FROM (
  SELECT
    pattern,
    elapsed_days,
    COUNT(DISTINCT a.user_id) uu
  FROM (
    SELECT
      user_id,
      to_timestamp(time)::date - to_timestamp(min(time) OVER (PARTITION BY user_id))::date elapsed_days
    FROM
      access_logs
  ) a
  INNER JOIN
    event_logs e
  ON
    a.user_id = e.user_id
  WHERE
    event = 'cv'
    AND elapsed_days < 14
  GROUP BY
    pattern,
    elapsed_days
) t
ORDER BY
  pattern,
  elapsed_days
;

 SQL 


(一)access_logs  user_id 

(二) unix time 

(三)2 time  elapsed_days 

(四)event_logs  user_id  inner join 

(五)event  cv  elapsed_days 14

(六)pattern, elapsed_days 

(七) user_id 

(八)pattern 

(九) elapsed_days uuuu

(十)pattern, elapsed_days 


 dplyr 
> # dplyr 用の data.frame に変換
> access_log_df <- dplyr::tbl_df(access_logs)
> # unix time を date に変換する関数
> time_to_date <- function(time) {
+   as.Date(as.POSIXct(time, origin = "1970-01-01",tz= "Asia/Tokyo"))
+ }
> uu_transitions <- access_log_df %>%
+   # 1. access_logs を user_id でグループ化する
+   group_by(user_id) %>%
+   # 2. グループごとに最小の unix time を算出する
+   mutate(min_time = min(time)) %>%
+   # グループ化を解除(集約処理が終わったら ungroup するのが無難)
+   ungroup() %>%
+   # 3. 2 で算出した時間と time を日に変換してから差分を算出して elapsed_days とする
+   mutate(elapsed_days = time_to_date(time) - time_to_date(min_time)) %>%
+   # 4. event_logs と user_id で inner join する
+   inner_join(event_log_df,by= "user_id") %>%
+   # 5. event が 'cv' で elapsed_days が14より小さいレコードに限定する
+   filter(event == "cv" & elapsed_days < 14) %>%
+   # 6. pattern, elapsed_days でグループ化する
+   group_by(pattern, elapsed_days) %>%
+   # 7. グループごとにユニークな user_id をカウントする
+   summarize(uu= n_distinct(user_id)) %>%
+   # 8. pattern でグループ化する
+   group_by(pattern) %>%
+   # 9. グループごとに elapsed_days が最小のレコードのuuの値でuuを割る
+   mutate(retention_ratio =uu/ first(uu, order_by = "elapsed_days")) %>%
+   # グループ化を解除(集約処理が終わったら ungroup するのが無難)
+   ungroup() %>%
+   # 10. pattern, elapsed_days でソートする
+   arrange(pattern, elapsed_days)

CVR 
関数 概要
filter 条件にマッチするデータのみ抽出する(SQL の WHERE 句に相当)
arrange 指定したフィールドでソート(SQL の ORDER BY 句に相当)
mutate 指定したフィールドを追加(SQL の SELECT *, new_field に相当)

group_by  mutate  SQL  PARTION BY 

filter  & 使 AND  | 使
> # Ruby の (1..5).to_a に相当
>x<- 1:5
> # 2 より大きい要素は TRUE になる
>x> 2
[1] FALSE FALSE  TRUE  TRUE  TRUE
> # 4 より小さい要素は TRUE になる
>x< 4
[1]  TRUE  TRUE  TRUE FALSE FALSE
> # ベクトルの論理積。2より大きく4より小さい要素は TRUE になる
>x> 2 &x< 4
[1] FALSE FALSE  TRUE FALSE FALSE

filter  TRUE  event  cv  elapsed_days 14


uu_transitions <- access_log_df %>%
  group_by(user_id) %>%
  mutate(min_time = min(time)) %>%
  ungroup() %>%
  mutate(elapsed_days = time_to_date(time) - time_to_date(min_time)) %>%
  filter(elapsed_days < 14) %>%
  select(user_id, elapsed_days) %>%
  distinct(user_id, elapsed_days) %>%
  inner_join(
    event_log_df %>%
      filter(event == "cv") %>%
      select(user_id, pattern),by= "user_id"
  ) %>%
  group_by(pattern, elapsed_days) %>%
  summarize(uu= n_distinct(user_id)) %>%
  group_by(pattern) %>%
  mutate(retention_ratio =uu/ first(uu, order_by = "elapsed_days")) %>%
  ungroup() %>%
  arrange(pattern, elapsed_days)


> # print 関数のn引数に表示する行数を指定することで全データを表示
> print(uu_transitions,n= nrow(uu_transitions))
Source: local data frame [28x4]

   pattern elapsed_days    uu retention_ratio
1a0 days 30097       1.0000000
2a1 days 11270       0.3744559
3a2 days  9747       0.3238529
4a3 days  9741       0.3236535
5a4 days  8830       0.2933847
6a5 days  8175       0.2716218
7a6 days  7716       0.2563711
8a7 days  7223       0.2399907
9a8 days  6946       0.2307871
10a9 days  6602       0.2193574
11a10 days  6437       0.2138751
12a11 days  6053       0.2011164
13a12 days  5752       0.1911154
14a13 days  5555       0.1845699
15b0 days 28901       1.0000000
16b1 days 12719       0.4400886
17b2 days 11379       0.3937234
18b3 days 11162       0.3862150
19b4 days 10054       0.3478772
20b5 days  9377       0.3244524
21b6 days  8766       0.3033113
22b7 days  8283       0.2865991
23b8 days  7944       0.2748694
24b9 days  7552       0.2613058
25b10 days  7214       0.2496107
26b11 days  6929       0.2397495
27b12 days  6541       0.2263243
28b13 days  6261       0.2166361

uu pattern a  pattern a  pattern b 14 (elapsed_days 13) uu pattern a 

 ggplot2 使 ggplot2  plot 使 ggplot2 使R使 ggplot2 使
> library(ggplot2)
Loading required package: methods
> # グラフにデータ (uu_transitions) をセットし、横軸を elapsed_days、縦軸をuuにする
>g<- ggplot(uu_transitions, aes(x= as.integer(elapsed_days),y=uu))
> # 折れ線グラフ (geom_line) を描画する。その際 pattern によって色を変える
>g+ geom_line(aes(color = pattern))

f:id:a_bicky:20150508114152p:plain
 pattern 使使 ggplot2 使 ggplot2 使

3ggplot2

 pattern a 2 pattern b UULP

LP

R


 Google Analytics R

 access_log_df 使 4/7  A/B 
> cohort <- access_log_df %>%
+   group_by(user_id) %>%
+   mutate(min_time = min(time)) %>%
+   ungroup() %>%
+   mutate(
+     registraion_date = time_to_date(min_time),
+     elapsed_days = time_to_date(time) - time_to_date(min_time)
+   ) %>%
+   filter(time < as.POSIXct("2015-04-08",tz= "Asia/Tokyo")) %>%
+   group_by(registraion_date, elapsed_days) %>%
+   summarize(uu= n_distinct(user_id)) %>%
+   group_by(registraion_date) %>%
+   mutate(retention_ratio = round(uu/ first(uu, order_by = elapsed_days), 3))
> cohort
Source: local data frame [36x4]
Groups: registraion_date

   registraion_date elapsed_days   uu retention_ratio
1        2015-03-31       0 days  532           1.000
2        2015-03-31       1 days  187           0.352
3        2015-03-31       2 days  195           0.367
4        2015-03-31       3 days  188           0.353
5        2015-03-31       4 days  191           0.359
6        2015-03-31       5 days  176           0.331
7        2015-03-31       6 days  145           0.273
8        2015-03-31       7 days   31           0.058
9        2015-04-01       0 days 8351           1.000
10       2015-04-01       1 days 3326           0.398
..              ...          ...  ...             ...

 tidyr::spread 
> cohort %>%
+   select(registraion_date, elapsed_days, retention_ratio) %>%
+   tidyr::spread(elapsed_days, retention_ratio) %>%
+   arrange(registraion_date)
Source: local data frame [8x9]

  registraion_date 0     1     2     3     4     5     6     7
1       2015-03-31 1 0.352 0.367 0.353 0.359 0.331 0.273 0.058
2       2015-04-01 1 0.398 0.358 0.355 0.322 0.297 0.250    NA
3       2015-04-02 1 0.411 0.356 0.350 0.312 0.264    NA    NA
4       2015-04-03 1 0.396 0.348 0.361 0.287    NA    NA    NA
5       2015-04-04 1 0.415 0.359 0.322    NA    NA    NA    NA
6       2015-04-05 1 0.417 0.336    NA    NA    NA    NA    NA
7       2015-04-06 1 0.368    NA    NA    NA    NA    NA    NA
8       2015-04-07 1    NA    NA    NA    NA    NA    NA    NA



A/B R使

R Tokyo.R R


R

R





R



dplyr  vignettes

dplyr 使





R













R





R 

dplyr 使dplyr 



R parent.env  parent.frame 





R


R

 knitr 使

*1:コーディングスタイルに迷ったら R 界で影響力の大きい Hadley Wickham 氏のコーディングスタイルに従うのが良いと思います

*2:執筆当時は筑波大学のリポジトリを指定していましたが、2015 年の 6 月に閉鎖したので変更しました(2017-04-22 追記)

*3:手元の環境だと、アクセスログを読み込むのに read.table は data.table::fread の 100 倍近く時間がかかります

*4:まだ枯れていないのでバグを踏むことはありますが・・・

*5:厳密には magrittr パッケージが提供しているものを dplyr パッケージが取り込んでいます

*6:気になるようであれば correct 引数に FALSE を指定することで補正を行わなくすることも可能です

*7:A であれば B が得られる確率が p-value であって、B が得られたから A である確率ではないことを強調しておきます

*8:個人的に、普段の分析では n 週間後にアクセスした割合を採用しています。ソーシャルゲームのように毎日アクセスすることを前提としたサービスでは n 日後にアクセスした割合で良いと思います