モデルと尤度
RFM 指標から将来の購買回数を予測する Pareto / NBD モデルから派生したモデルに BG / NBD モデルがあります。
Pareto / NBD モデルより計算がかんたんです。
モデル:
- 顧客の購買はレート の定常ポアソン過程に従う
- 顧客の購買のたびに確率 で生存、 で離脱する
- の事前分布はパラメータ のガンマ分布とする
- の事前分布はパラメータ のベータ分布とする
さらに、 と は顧客ひとりひとりで異なるとします。
通常は と を周辺化して最尤法でパラメータを推定することが多いようですが、今回は変分ベイズでパラメータを求めます。
変分ベイズを使うメリットは計算の速さです。
記法:
- 観測期間の終点を とする
- 観測期間内の購買回数の合計(フリクエンシー)を とする
- 顧客の i 回目の購買日時を とする
- とする
最後の購買時点で顧客が生存している場合を 、離脱している場合を として、考えるべき尤度は、
です。
対数をとって整理すると、
となります。
変分推論のアルゴリズムの導出
s のサポートが であることに注意すると、s の近似事後分布は以下のベルヌーイ分布です。
q は s にかかっている因子と (1-s) にかかっている因子が足して 1 になるよう正規化したものですから整理すると、以下のようになります。
s の期待値は q です。
- の近似事後分布はパラメータ のガンマ分布
- の近似事後分布はパラメータ のベータ分布
です。
アルゴリズムをまとめて R で書くとこうなりました。
VIgeom <- function(k,tk,t1,Tim,maxit = 20, alpha=1, beta=1, a=1, b=1){ N <- length(k) p <- runif(N) logp <- log(p) lambda <- (k+1)/(Tim) s <- 1/(1+(1-p)/(p*exp(-lambda*(Tim-tk)))) for(i in 1:maxit){ alphahat <- (k-1+alpha) betahat <- (s*Tim+(1-s)*tk-t1+beta) lambda <- alphahat/betahat loglambda <- digamma(alphahat) - log(betahat) ahat <- k-1+s+a bhat <- 1-s+b logp <- digamma(ahat)-digamma(ahat+bhat) log1mp <- digamma(bhat)-digamma(ahat+bhat) s <- 1/(1+exp(log1mp-logp+lambda*(Tim-tk))) } list(lambda=lambda,p=ahat/(ahat+bhat),ahat=ahat,bhat=bhat,s=s) }
生存時間の分布
確率 p で生存する試行を k 回繰り返すので:
これは生存関数であるから, 生存時間の密度関数 g(T) は、
です。
有用な統計量
期間 までの期待購買回数は、
期間 ( 日間)の購買回数を とする。
時点で生存している場合と で離脱している場合を考え、
(2行目から3行目の変形は部分積分を用いた)
本当はこの統計量の平均を求めたほうが気持ちがいいのですが、めんどうなのでパラメータの平均をプラグインして予測します。
例題
顧客生涯価値(CLV)の計算 with R | かものはしの分析ブログ で紹介されている Bruce Hardie: Datasets を用いて将来の購買回数を予測してみます。
タイトルでは顧客生涯価値としましたが、予測するのは単に予測期間の購買回数です。
1997-01-01 から 1997-12-31 までを学習用、1998-01-01から1998-06-30までをテスト用データとします。
library(tidyverse) library(cowplot) df <- read_table(file = "~/data/CDNOW_master/CDNOW_master.txt",col_names = FALSE) df <- mutate(df,X2=as.Date(as.character(X2),"%Y%m%d")) # range(df$X2) # "1997-01-01" "1998-06-30" RFM1 <- dplyr::filter(df,X2<"1998-01-01") %>% arrange(X2) %>% group_by(X1) %>% summarise(first=first(X2), last=last(X2), frequency = n(), monetary=sum(X4)) %>% ungroup() %>% mutate(Tim = as.integer(max(last) - min(first)), t1 = as.integer(first - min(first)), tx = as.integer(last - min(first))) RFM2 <- dplyr::filter(df,X2>="1998-01-01") %>% arrange(X2) %>% group_by(X1) %>% summarise(frequency_test = n(),monetary_test=sum(X4)) RFM_join <- left_join(RFM1,RFM2) %>% mutate(frequency_test=if_else(is.na(frequency_test),0L,frequency_test)) outVI <- VIgeom(k = RFM1$frequency,tk = RFM1$tx,t1 = RFM1$t1,Tim = RFM1$Tim, alpha = 1,beta=1,a = 1,b=1) Ti <- as.integer(as.Date("1998-06-30") - as.Date("1998-01-01")) predfreq <- outVI$s*(1-exp(-outVI$lambda*(1-outVI$p)*Ti))/(1-outVI$p) compdf <- data.frame(test=RFM_join$frequency_test,predict=predfreq) ggplot(compdf,aes(x=test,y=predict))+ geom_hex()+ geom_abline(slope = 1,intercept = 0,linetype=2)+ theme_cowplot()
次の図は期待購買回数と実測値の散布図です。
RMSE は1.28でした。
sqrt(mean((RFM_join$frequency_test-predfreq)^2)) # 1.283156
購買回数が0か1以上かという予測が大事だと思ったので、予測値を丸めてアキュラシーを求めてみました。
アキュラシーは約0.8でした。まあまあ妥当な予測と言えそうです。
pred2 <- predfreq>0.5 test2 <- RFM_join$frequency_test>0 tab <- table(pred2,test2) sum(diag(tab))/sum(tab) # 0.8071277