UPDATE: 2022-10-28 19:52:55

はじめに

funneljoinパッケージについて、パッケージのGithubと開発者のブログで紹介されている内容、使い方をまとめておく。funneljoinパッケージの目的は、人がどのように動いたのか、何を行ったのか、という行動ファネルを分析するのを手助けするためのパッケージ。下記を参考にしている。今度のOsakaRのネタにしようかどうか悩み中。

# library(remotes)
# install_github("robinsones/funneljoin")

library(tidyverse)
library(funneljoin)

パッケージのGithubにある下記の訪問履歴と登録履歴のデータをお借りして説明する。

landed <- landed %>%
  rename(landed_at = timestamp, user_id_x = user_id)
 
registered <- registered %>%
  rename(registered_at = timestamp, user_id_y = user_id)

list(
landed,
registered
)
## [[1]]
## # A tibble: 9 × 2
##   user_id_x landed_at 
##       <dbl> <date>    
## 1         1 2018-07-01
## 2         2 2018-07-01
## 3         3 2018-07-02
## 4         4 2018-07-01
## 5         4 2018-07-04
## 6         5 2018-07-10
## 7         5 2018-07-12
## 8         6 2018-07-07
## 9         6 2018-07-08
## 
## [[2]]
## # A tibble: 8 × 2
##   user_id_y registered_at
##       <dbl> <date>       
## 1         1 2018-07-02   
## 2         3 2018-07-02   
## 3         4 2018-06-10   
## 4         4 2018-07-02   
## 5         5 2018-07-11   
## 6         6 2018-07-10   
## 7         6 2018-07-11   
## 8         7 2018-07-07

after_join関数

dplyrパッケージのjoin関数と同じでafter_left_join()after_inner_join()などがあり、typeを変更することで紐付け方を変更する。引数は下記の通り。

引数 内容
by_time 各テーブルの時間カラムを指定。datetime型のまたはdate型のカラム。時間yが時間xの後、または時間xと同じかどうかなど、フィルタリングするために使用される。
by_user 各テーブルのユーザまたはID列を指定。一致する行のペアは同一でなければならない。
type “first-first”、“last-first”、“any-firstafter”など、イベントペアを区別するために使用されるファネルのタイプを指定。
suffix dplyrのjoin関数と同様に、両方のテーブルにあるカラム名にサフィックスを指定。

typeは下記を指定できる。基本的には先なのか、後なのか、全部なのかなどをfirstlastanyで指定する。下記のタイプが一般的。

  • first-first: 参加前の各ユーザーの最も古いxyを紐付ける。例えば、実験があったとして、最初の「参加」以降に、最初に「登録」した時を取得したい場合はこのタイプを利用する。この場合、登録して、実験に参加し、再び登録した場合は紐付かない。
  • first-firstafter: これは参加前の各ユーザーの最も古いxyを紐付ける。例えば、実験があったとして、最初の「参加」以降に、最初に「登録」した時を取得したい場合はこのタイプを利用する。
  • lastbefore-firstafter: 例えば、ラストクリック型の広告アトリビューションなんかで役に立つ。最初のCVの前で、最後にクリックした広告が必要なとき。
  • any-firstafter: すべてのx以降で最初のyが続くものを取る。例えば、誰かがホームページを訪問した回数と、その後に訪問した最初の製品ページをすべて取得したい場合など。
  • any-any: すべてのx以降ですべてのyが続くものを取る。例えば、誰かがホームページを訪問した回数と、その後に見たすべての製品ページを表示する。

文字でみてもよくわからんので、上記の5タイプを実際に動かしていく。理解をすすめるために、私が作った画像をアップしているが、作った過程で誤りがあればごめんなさい。画像のタイトルにタイプを表記しているが、意図的に括弧をつけている。これは、lastbefore-firstafterなんかはbeforeが前のアクションのデータで、afterがアクション後のデータと考えると、「アクション前の最後とアクション後の最初」を紐付けるタイプと解釈できるので、このようにしており、実際のタイプに無いものは空括弧にしている。

また、挙動を確認するために、簡単なサンプルのデータを作っている。

df_x <- tibble::tibble(user_id_x = 1,
                       landed_at = seq(as.Date("2020-05-05"), by = "day", length.out = 2)
                       )

df_y <- tibble::tibble(user_id_y = 1,
                       registered_at = seq(as.Date("2020-05-01"), by = "day", length.out = 9)
                       )

list(df_x,df_y)
## [[1]]
## # A tibble: 2 × 2
##   user_id_x landed_at 
##       <dbl> <date>    
## 1         1 2020-05-05
## 2         1 2020-05-06
## 
## [[2]]
## # A tibble: 9 × 2
##   user_id_y registered_at
##       <dbl> <date>       
## 1         1 2020-05-01   
## 2         1 2020-05-02   
## 3         1 2020-05-03   
## 4         1 2020-05-04   
## 5         1 2020-05-05   
## 6         1 2020-05-06   
## 7         1 2020-05-07   
## 8         1 2020-05-08   
## 9         1 2020-05-09

first-first

おさらいすると、これは参加前の各ユーザーの最も古いxyを紐付ける。例えば、実験があったとして、最初の「参加」以降に、最初に「登録」した時を取得したい場合はこのタイプを利用する。この場合、登録して、実験に参加し、再び登録した場合は紐付かない、というもの。

landed %>%
  after_left_join(registered, 
                   by_user = c("user_id_x" = "user_id_y"),
                   by_time = c("landed_at" = "registered_at"),
                   type = "first-first") %>% 
  arrange(user_id_x)
## # A tibble: 6 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2018-07-01 2018-07-02   
## 2         2 2018-07-01 NA           
## 3         3 2018-07-02 2018-07-02   
## 4         4 2018-07-01 NA           
## 5         5 2018-07-10 2018-07-11   
## 6         6 2018-07-07 2018-07-10

サンプルデータで確認してみる。

df_x %>%
   after_left_join(df_y, 
                   by_user = c("user_id_x" = "user_id_y"),
                   by_time = c("landed_at" = "registered_at"),
                   type = "first-first")
## # A tibble: 1 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2020-05-05 NA

first-firstafter

おさらいすると、これは参加前の各ユーザーの最も古いxyを紐付ける。例えば、実験があったとして、最初の「参加」以降に、最初に「登録」した時を取得したい場合はこのタイプを利用する、というもの。

landed %>%
  after_left_join(registered, 
                  by_user = c("user_id_x" = "user_id_y"),
                  by_time = c("landed_at" = "registered_at"),
                  type = "first-firstafter") %>% 
  arrange(user_id_x)
## # A tibble: 6 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2018-07-01 2018-07-02   
## 2         2 2018-07-01 NA           
## 3         3 2018-07-02 2018-07-02   
## 4         4 2018-07-01 2018-07-02   
## 5         5 2018-07-10 2018-07-11   
## 6         6 2018-07-07 2018-07-10

サンプルデータで確認してみる。

df_x %>%
   after_left_join(df_y, 
                   by_user = c("user_id_x" = "user_id_y"),
                   by_time = c("landed_at" = "registered_at"),
                   type = "first-firstafter")
## # A tibble: 1 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2020-05-05 2020-05-05

lastbefore-firstafter

おさらいすると、これは、例えば、ラストクリック型の広告アトリビューションなんかで役に立つ。最初のCVの前で、最後にクリックした広告が必要なとき、というもの。

landed %>%
  after_left_join(registered, 
                  by_user = c("user_id_x" = "user_id_y"),
                  by_time = c("landed_at" = "registered_at"),
                  type = "lastbefore-firstafter") %>% 
  arrange(user_id_x)
## # A tibble: 9 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2018-07-01 2018-07-02   
## 2         2 2018-07-01 NA           
## 3         3 2018-07-02 2018-07-02   
## 4         4 2018-07-01 2018-07-02   
## 5         4 2018-07-04 NA           
## 6         5 2018-07-10 2018-07-11   
## 7         5 2018-07-12 NA           
## 8         6 2018-07-07 NA           
## 9         6 2018-07-08 2018-07-10

サンプルデータで確認してみる。このような5日と6日の場合、6日のみ6日と紐づくと思ったが、5日と6日の間に5日があるので、5日と5日が紐づく。

df_x %>%
   after_left_join(df_y, 
                   by_user = c("user_id_x" = "user_id_y"),
                   by_time = c("landed_at" = "registered_at"),
                   type = "lastbefore-firstafter")
## # A tibble: 2 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2020-05-05 2020-05-05   
## 2         1 2020-05-06 2020-05-06

パッケージののサンプルデータでidが6だけで検証してみると、思ったとおりに動く。

df_x2 <- tibble::tibble(user_id_x = 1,
                       landed_at = as.Date(c("2020-07-07", "2020-07-8"))
                       )

df_y2 <- tibble::tibble(user_id_y = 1,
                       registered_at = as.Date(c("2020-07-07", "2020-07-10")))

df_x2 %>%
  after_left_join(df_y2, 
                  by_user = c("user_id_x" = "user_id_y"),
                  by_time = c("landed_at" = "registered_at"),
                  type = "lastbefore-firstafter")
## # A tibble: 2 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2020-07-07 2020-07-07   
## 2         1 2020-07-08 2020-07-10
# 8日だと7日は紐付かず、8日が8日と紐づく。
df_x3 <- tibble::tibble(user_id_x = 1,
                        landed_at = as.Date(c("2020-07-07", "2020-07-8"))
                        )
df_y3 <- tibble::tibble(user_id_y = 1,
                        registered_at = as.Date(c("2020-07-08", "2020-07-10")))
df_x3 %>%
   after_left_join(df_y3, 
                   by_user = c("user_id_x" = "user_id_y"),
                   by_time = c("landed_at" = "registered_at"),
                   type = "lastbefore-firstafter")
## # A tibble: 2 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2020-07-07 NA           
## 2         1 2020-07-08 2020-07-08

any-firstafter

おさらいすると、これは、すべてのx以降で最初のyが続くものを取る。例えば、誰かがホームページを訪問した回数と、その後に訪問した最初の製品ページをすべて取得したい場合などに役立つ、というもの。

landed %>%
  after_left_join(registered, 
                  by_user = c("user_id_x" = "user_id_y"),
                  by_time = c("landed_at" = "registered_at"),
                  type = "any-firstafter") %>% 
  arrange(user_id_x)
## # A tibble: 9 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2018-07-01 2018-07-02   
## 2         2 2018-07-01 NA           
## 3         3 2018-07-02 2018-07-02   
## 4         4 2018-07-01 2018-07-02   
## 5         4 2018-07-04 NA           
## 6         5 2018-07-10 2018-07-11   
## 7         5 2018-07-12 NA           
## 8         6 2018-07-07 2018-07-10   
## 9         6 2018-07-08 2018-07-10

サンプルデータで確認してみる。

df_x %>%
   after_left_join(df_y, 
                   by_user = c("user_id_x" = "user_id_y"),
                   by_time = c("landed_at" = "registered_at"),
                   type = "any-firstafter")
## # A tibble: 2 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2020-05-05 2020-05-05   
## 2         1 2020-05-06 2020-05-06

any-any

おさらいすると、これはすべてのx以降ですべてのyが続くものを取る。例えば、誰かがホームページを訪問した回数と、その後に見たすべての製品ページを表示する、というもの。

landed %>%
  after_left_join(registered, 
                  by_user = c("user_id_x" = "user_id_y"),
                  by_time = c("landed_at" = "registered_at"),
                  type = "any-any") %>% 
  arrange(user_id_x)
## # A tibble: 11 × 3
##    user_id_x landed_at  registered_at
##        <dbl> <date>     <date>       
##  1         1 2018-07-01 2018-07-02   
##  2         2 2018-07-01 NA           
##  3         3 2018-07-02 2018-07-02   
##  4         4 2018-07-01 2018-07-02   
##  5         4 2018-07-04 NA           
##  6         5 2018-07-10 2018-07-11   
##  7         5 2018-07-12 NA           
##  8         6 2018-07-07 2018-07-10   
##  9         6 2018-07-07 2018-07-11   
## 10         6 2018-07-08 2018-07-10   
## 11         6 2018-07-08 2018-07-11

サンプルデータで確認してみる。

df_x %>%
   after_left_join(df_y, 
                   by_user = c("user_id_x" = "user_id_y"),
                   by_time = c("landed_at" = "registered_at"),
                   type = "any-any")
## # A tibble: 9 × 3
##   user_id_x landed_at  registered_at
##       <dbl> <date>     <date>       
## 1         1 2020-05-05 2020-05-05   
## 2         1 2020-05-05 2020-05-06   
## 3         1 2020-05-05 2020-05-07   
## 4         1 2020-05-05 2020-05-08   
## 5         1 2020-05-05 2020-05-09   
## 6         1 2020-05-06 2020-05-06   
## 7         1 2020-05-06 2020-05-07   
## 8         1 2020-05-06 2020-05-08   
## 9         1 2020-05-06 2020-05-09

ファネル分析

funneljoinパッケージにはjoin関数以外にもファネル分析に特化した関数がいくつかある。サンプルデータは下記の通り。

activity <- tibble::tribble(
  ~ "user_id", ~ "event", ~ "timestamp",
  1, "landing", "2019-07-01",
  1, "registration", "2019-07-02",
  1, "purchase", "2019-07-07",
  1, "purchase", "2019-07-10",
  2, "landing", "2019-08-01",
  2, "registration", "2019-08-15",
  3, "landing", "2019-05-01",
  3, "registration", "2019-06-01",
  3, "purchase", "2019-06-04",
  4, "landing", "2019-06-13"
)

activity
## # A tibble: 10 × 3
##    user_id event        timestamp 
##      <dbl> <chr>        <chr>     
##  1       1 landing      2019-07-01
##  2       1 registration 2019-07-02
##  3       1 purchase     2019-07-07
##  4       1 purchase     2019-07-10
##  5       2 landing      2019-08-01
##  6       2 registration 2019-08-15
##  7       3 landing      2019-05-01
##  8       3 registration 2019-06-01
##  9       3 purchase     2019-06-04
## 10       4 landing      2019-06-13

funnel_start()は下記の引数をとる。

引数 内容
tbl イベントのテーブル
moment_type ファネルにおける最初のイベント
moment moment_typeを示すカラム名
tstamp モーメントのタイムスタンプを持つカラムの名前
user そのモーメントを行ったユーザーを示すカラムの名前。

funnel_start()は、user_idstimestamp_{evrnt}を持つテーブルを返す。

activity %>%
   funnel_start(moment_type = "landing", 
                moment = "event", 
                tstamp = "timestamp", 
                user = "user_id")
## # A tibble: 4 × 2
##   user_id timestamp_landing
##     <dbl> <chr>            
## 1       1 2019-07-01       
## 2       2 2019-08-01       
## 3       3 2019-05-01       
## 4       4 2019-06-13

ファネルに更にモーメントを追加するには、funnel_step()を使う。funnel_start()で各パートに使用するカラムを指定したので、必要なのはmoment_typeafter_join()のタイプ。after_join()を理解していると、わかり良い。1つのテーブルを条件ごとにテーブルを内部的に分け、after_join()で結合する。

activity %>%
   funnel_start(moment_type = "landing", 
                moment = "event", 
                tstamp = "timestamp", 
                user = "user_id") %>%
   funnel_step(moment_type = "registration",
               type = "first-firstafter")
## # A tibble: 4 × 3
##   user_id timestamp_landing timestamp_registration
##     <dbl> <chr>             <chr>                 
## 1       3 2019-05-01        2019-06-01            
## 2       4 2019-06-13        <NA>                  
## 3       1 2019-07-01        2019-07-02            
## 4       2 2019-08-01        2019-08-15

ファネルに更にモーメントを追加するには、再度funnel_step()を使う。

activity %>%
     funnel_start(moment_type = "landing", 
                  moment = "event", 
                  tstamp = "timestamp", 
                  user = "user_id") %>%
     funnel_step(moment_type = "registration",
                 type = "first-firstafter") %>%
     funnel_step(moment_type = "purchase",
                 type = "first-any")
## # A tibble: 5 × 4
##   user_id timestamp_landing timestamp_registration timestamp_purchase
##     <dbl> <chr>             <chr>                  <chr>             
## 1       3 2019-05-01        2019-06-01             2019-06-04        
## 2       1 2019-07-01        2019-07-02             2019-07-07        
## 3       1 2019-07-01        2019-07-02             2019-07-10        
## 4       2 2019-08-01        2019-08-15             <NA>              
## 5       4 2019-06-13        <NA>                   <NA>

funnel_step()はモーメントをまとめて指定でき、summarize_funnel()を使うことでファネル分析の結果が得られる。最後に、summaryize_funnel()を使って、ファネルの各次のステップに何人の人が通過したのか、何%の人が通過したのかを理解することが可能。funnel_steps()に切り替えてコードを少し短くすることもできる。各ステップのtypeを順に与える。

activity %>%
     funnel_start(moment_type = "landing", 
                  moment = "event", 
                  tstamp = "timestamp", 
                  user = "user_id") %>%
     funnel_steps(moment_types = c("registration", "purchase"),
                  type = "first-firstafter") %>%
     summarize_funnel()
## # A tibble: 3 × 4
##   moment_type  nb_step pct_cumulative pct_step
##   <fct>          <int>          <dbl>    <dbl>
## 1 landing            4           1      NA    
## 2 registration       3           0.75    0.75 
## 3 purchase           2           0.5     0.667

また、first-anyのように、ユーザーに対して1つのタイプの複数のモーメントを紐付けるタイプを使用した場合、より多くの行をユーザーごとに取得することが可能。例えば、ユーザー1は2回の購入をしているので、2行を持つことになる。timestamp_landingtimestamp_registrationはどちらの行も同じで、異なるtimestamp_purchaseを持つ。のべカウントか、ユニークカウントの違いに似ている。

activity %>%
  funnel_start(moment_type = "landing", 
               moment = "event", 
               tstamp = "timestamp", 
               user = "user_id") %>%
  funnel_steps(moment_types = c("registration", "purchase"),
               type = "first-any")
## # A tibble: 5 × 4
##   user_id timestamp_landing timestamp_registration timestamp_purchase
##     <dbl> <chr>             <chr>                  <chr>             
## 1       3 2019-05-01        2019-06-01             2019-06-04        
## 2       1 2019-07-01        2019-07-02             2019-07-07        
## 3       1 2019-07-01        2019-07-02             2019-07-10        
## 4       2 2019-08-01        2019-08-15             <NA>              
## 5       4 2019-06-13        <NA>                   <NA>
activity %>%
   funnel_start(moment_type = "landing", 
                moment = "event", 
                tstamp = "timestamp", 
                user = "user_id") %>%
   funnel_steps(moment_types = c("registration", "purchase"),
                type = "first-firstafter")
## # A tibble: 4 × 4
##   user_id timestamp_landing timestamp_registration timestamp_purchase
##     <dbl> <chr>             <chr>                  <chr>             
## 1       3 2019-05-01        2019-06-01             2019-06-04        
## 2       1 2019-07-01        2019-07-02             2019-07-07        
## 3       2 2019-08-01        2019-08-15             <NA>              
## 4       4 2019-06-13        <NA>                   <NA>

以上で、funneljoinパッケージのまとめは終わり。ファネル分析に特化したパッケージではあるが、使い方によっては、時間経過をもつ履歴データであれば、前処理の部分で役立てることができるだし、fuzzyjoinパッケージを使って、不等号を使ってコネコネ結合していたものが、この関数で代替できる部分もあるかもしれない。