UPDATE: 2023-05-04 02:46:38
Marketing Mix Modeling(MMM)のシリーズでは、Meta社(Facebook)が開発したRobynパッケージのドキュメントをなぞりながら、MMMを理解するために必要な知識を整理し、RでMMMを実行するまでをまとめている。基本的には下記の公式ドキュメントを参考にしている。
ここでは、AdstockとDiminishing returns(Saturation)への理解を進めていく。Diminishing returnsはcarryoverとも呼ばれるとのこと。
AdstockはTVCMをもとに考えるとわかりやすい。TVCMは見たからと言って、すぐに購買につながるわけではなく、数日後、数週間後、数カ月後に再想起することで購買につなげる力があったりする。この性質があると仮定できそうなのに、データ上、支出の数字と購買金額は、同じ日に記録されるわけでもなく、別の日の売上に含まれてしまう。つまり、このようなデータ生成過程を考慮せず、モデリングを行うと、精度が高いモデルが構築できない。そのため、TVCMの支出を引き伸ばす、つまりAdstockすることで、変数を変換し、モデルの精度向上を目指す変換の方法。一番シンプルなGeometric Simple Decay Modelは下記の方法で変換する。
\[ y_{t} = x_{t} + \lambda y_{t-1} \] 他にも、Geometric Log Decay ModelやDelayed Simple Decay Modelなどもある。詳しくはこちらの記事が参考になる。Robynでは基本的なAdstock変換だけではなく、ワイブル分布を利用したAdstock変換も利用できるため、広告の性質に合わせて、Adstock変換を行うことで、より現実に沿ったモデル構築ができる。
広告の出稿量を増やすと、広告が到達するユーザーは増加して、需要も伴って増加しそうだが、広告の露出と売上は比例しない、という概念がDiminishing returns。広告を出せば出すだけ売上があがるわけでもなく、いつかは飽和するということ。
この関係を表すために、Adstock変換された値にパラメタを乗じることで、飽和する様子を表すことができる。Robynでの変換方法はドキュメントをみるとわかるが、もう少し複雑なので、下記の式ではなく、Hill関数を利用している。
\[ DiminishingReturns_{t} = Adstock \ y_{t}^{\gamma} \]
Adstock変換とDiminishing
returnsを利用すると、下記のようなイメージになる。元のy
はx
の増加とともに比例して増加しているが、変換されたy_adstock_and_dim
は頭打ちになっていることがわかる。
library(tidyverse)
set.seed(1989)
<- 1:100
x <- 1.1 * x + rnorm(n = length(x))
y
<- function(x, lambda, gamma){
GeometricAdstock_and_DiminishingReturn <- vector(mode = 'numeric', length = length(x))
res 1] <- x[1]
res[for (i in 2:length(x)) {
<- (res[i-1]*lambda + x[i])^gamma
res[i]
}
return(res)
}
<- GeometricAdstock_and_DiminishingReturn(y, 0.3, 0.8)
y_adstock_and_dim tibble(x, y, y_adstock_and_dim) %>%
pivot_longer(
cols = -x,
names_to = 'type',
values_to = 'y'
%>%
) ggplot(aes(x, y, col = type)) +
geom_line() +
theme_classic()
RobynではspendとExposureの関係性を表現する際に、Hill equationによって変換が行われる。
\[ x_{saturated_{i,j}} = \frac{x_{adstocked_{i,j}}^{\alpha}}{x_{adstocked_{i,j}}^{\alpha} + \gamma^{\alpha}} \]
Diminishing returns
これは「TVCM支出(spend)とGRP(Exposure)」、「Facebook支出(spend)とインプレッション(Exposure)」などの支出と露出の関係を表現するためのもので、支出すれば最初はインプレッションがでやすいが、支出し続けると、インプレッションが頭打ちになるという関係を表している。
Robynを実行すると、Michaelis-Menten変換を行ったという旨の警告がでる。これはおそらくだが、Hillの式にはHill係数が含まれており、Hill係数が1の場合、Hillの式はMichaelis–Menten式に一致するからだと思われる。ただ、細かい部分の実装までは追えていないので、誤っている可能性がある。Hill equationやMichaelis-Mentenについては、下記を参考にした。
If the Hill coefficient is equal to 1 (n = 1), the Hill equation is reduced to its simpler form known as the Michaelis-Menten equation.
ここからは、簡単ではあるが変換を行うと回帰モデルのR2乗が増加するかを確認する。あてはまりの良さを表すR2乗で良いのか、という疑問を持つかもしれないが、簡単に調べるだけなので良しとする。本来であれば、時系列クロスバリデーション、Ridge回帰、Nevergradで多目的最適化を行って、変換が有効かどうかを調べるのがよいと思うが、それはRobynパッケージを使えば実行できるので、ここでは簡単に調べるだけにしておく。
まずは変換せずにモデルを構築するとR2乗は0.52となった。
<- read_csv('~/Desktop/adstock.csv')
df <- summary(lm(sales ~ youtube + facebook, data = df))
fit_normal $adj.r.squared fit_normal
## [1] 0.5249186
次はAdstock変換とDiminishing returnsを考慮した変数を利用してモデルを構築する。R2乗は0.63となった。無変換の値よりも当てはまりが良くなっている。
<- summary(lm(sales ~
fit_adstock GeometricAdstock_and_DiminishingReturn(youtube, 0.5, 1) +
GeometricAdstock_and_DiminishingReturn(facebook, 0.5, 0.9),
data = df)
)$adj.r.squared fit_adstock
## [1] 0.6394171
ここで問題となるのが、変換の際のパラメタが決め打ちである点。これを非線形最小二乗法で最適化することで、より当てはまりが良くなるかを確認する。
library(minpack.lm)
<- nlsLM(
fit_nls1 data = df,
~ alpha +
sales * GeometricAdstock_and_DiminishingReturn(youtube, lambda1, gamma1) +
beta1 * GeometricAdstock_and_DiminishingReturn(facebook, lambda2, gamma2),
beta2 start = c(
alpha = fit_adstock$coefficients[1],
beta1 = fit_adstock$coefficients[2],
beta2 = fit_adstock$coefficients[3],
lambda1 = 0.1,
gamma1 = 0.1,
lambda2 = 0.1,
gamma2 = 0.1
),lower = c(-Inf, -Inf, -Inf, 0, 0, 0, 0),
control = nls.control(maxiter = 5000)
)
<-
fit_adstock_finalize lm(
~
sales GeometricAdstock_and_DiminishingReturn(youtube, fit_nls1$m$getPars()[4], fit_nls1$m$getPars()[5]) +
GeometricAdstock_and_DiminishingReturn(facebook, fit_nls1$m$getPars()[6], fit_nls1$m$getPars()[7]),
data = df
)
$m$getPars() fit_nls1
## alpha beta1 beta2 lambda1 gamma1 lambda2 gamma2
## 0.1505591 0.6325461 0.6748594 0.7595052 0.8902439 0.4377238 0.8797812
実行結果を確認すると0.65となって少し当てはまりをよくできた。
summary(fit_adstock_finalize)$adj.r.squared
## [1] 0.659631
観測値と予測値もプロットして確認しておく。
%>%
df bind_cols(idx = 1:200, pred = predict(fit_adstock_finalize, newdata = df)) %>%
select(idx, sales, pred) %>%
pivot_longer(
cols = -idx,
names_to = 'type',
values_to = 'y'
%>%
) ggplot(aes(idx, y, col = type)) +
geom_line(size = 0.5) +
theme_classic()
Ridge回帰と時系列クロスバリデーションを使って、Adstock変換とDiminishing returnsを考慮することによるモデル精度の向上をRMESをもとに再検証する。まずは何も考慮しなかった場合、RMSEは2.28となった。
library(tidymodels)
<- df[1:180,]
df_train <- df[181:200,]
df_test
<- df_train %>%
df_folds rolling_origin(
initial = 120,
assess = 20,
skip = 9,
cumulative = FALSE
) <-
ridge_recipe recipe(formula = sales ~ ., data = df_train)
<-
ridge_spec linear_reg(penalty = tune(), mixture = 0) %>%
set_mode("regression") %>%
set_engine("glmnet")
<- workflow() %>%
ridge_workflow add_recipe(ridge_recipe) %>%
add_model(ridge_spec)
<- grid_regular(penalty(range = c(-5, 5)), levels = 100)
penalty_grid <- tune_grid(
tune_res
ridge_workflow,resamples = df_folds,
grid = penalty_grid
)# autoplot(tune_res)
# collect_metrics(tune_res) %>%
# filter(.metric == 'rmse')
<- select_best(tune_res, metric = "rmse")
best_penalty <- finalize_workflow(ridge_workflow, best_penalty)
ridge_final <- fit(ridge_final, data = df_train)
ridge_final_fit
augment(ridge_final_fit, new_data = df_test) %>%
rmse(truth = sales, estimate = .pred)
## # A tibble: 1 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 2.28
次はAdstock変換とDiminishing returnsを考慮した場合は、RMSEは1.91となった。
$youtube <- GeometricAdstock_and_DiminishingReturn(df$youtube, 0.7595052, 0.8902439)
df$facebook <- GeometricAdstock_and_DiminishingReturn(df$facebook, 0.4377238, 0.8797812)
df
<- df[1:180,]
df_train2 <- df[181:200,]
df_test2
<- df_train2 %>%
df_folds2 rolling_origin(
initial = 120,
assess = 20,
skip = 9,
cumulative = FALSE
)
<- tune_grid(
tune_res
ridge_workflow,resamples = df_folds2,
grid = penalty_grid
)# autoplot(tune_res)
<- select_best(tune_res, metric = "rmse")
best_penalty <- finalize_workflow(ridge_workflow, best_penalty)
ridge_final <- fit(ridge_final, data = df_train2)
ridge_final_fit augment(ridge_final_fit, new_data = df_test2) %>%
rmse(truth = sales, estimate = .pred)
## # A tibble: 1 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 1.91
このように、Adstock変換とDiminishing returnsを考慮するだけでモデルの精度を向上させることができた。