Skip to contents

Data

Impressions Data

glimpse(mmm_imps)
#> Rows: 200
#> Columns: 5
#> $ Date       <date> 2018-01-07, 2018-01-14, 2018-01-21, 2018-01-28, 2018-02-04…
#> $ mi_tv      <dbl> 344275.4, 0.0, 0.0, 0.0, 0.0, 204905.0, 0.0, 243839.5, 0.0,…
#> $ mi_radio   <dbl> 0.00, 17206.08, 21155.95, 13623.15, 0.00, 19578.15, 0.00, 1…
#> $ mi_banners <dbl> 0.000, 3731.596, 2217.981, 0.000, 4274.270, 2801.566, 3329.…
#> $ kpi_sales  <dbl> 9779.80, 13245.19, 12022.66, 8846.95, 9797.07, 13527.65, 96…

Spend Data

glimpse(mmm_spend)
#> Rows: 200
#> Columns: 5
#> $ Date       <date> 2018-01-07, 2018-01-14, 2018-01-21, 2018-01-28, 2018-02-04…
#> $ mi_tv      <dbl> 13528.10, 0.00, 0.00, 0.00, 0.00, 8045.44, 0.00, 9697.29, 0…
#> $ mi_radio   <dbl> 0.00, 5349.65, 4235.86, 3562.21, 0.00, 4310.55, 0.00, 4478.…
#> $ mi_banners <dbl> 0.00, 2218.93, 2046.96, 0.00, 2187.29, 1992.98, 2253.02, 20…
#> $ kpi_sales  <dbl> 9779.80, 13245.19, 12022.66, 8846.95, 9797.07, 13527.65, 96…

Model Recipes

Recipe without transformation

ln_init_mmm <-
  recipe(kpi_sales ~ ., data = mmm_imps) |>
  add_role(c(mi_tv, mi_radio, mi_banners), new_role = "mi") |>
  update_role(Date, new_role = "temp") |>
  update_role_requirements("temp", bake = FALSE)

Recipe with transformations with default hyperparameters

cs_init_mmm <-
  recipe(kpi_sales ~ ., data = mmm_imps) |>
  add_role(c(mi_tv, mi_radio, mi_banners), new_role = "mi") |>
  update_role(Date, new_role = "temp") |>
  update_role_requirements("temp", bake = FALSE) |>
  step_geometric_adstock(mi_banners) |>
  step_geometric_adstock(mi_tv) |>
  step_geometric_adstock(mi_radio) |>
  step_hill_saturation(mi_banners, max_ref = TRUE) |>
  step_hill_saturation(mi_tv, max_ref = TRUE) |>
  step_hill_saturation(mi_radio, max_ref = TRUE)
dl_init_mmm <-
  recipe(kpi_sales ~ ., data = mmm_imps) |>
  add_role(c(mi_tv, mi_radio, mi_banners), new_role = "mi") |>
  update_role(Date, new_role = "temp") |>
  update_role_requirements("temp", bake = FALSE) |>
  step_delayed_adstock(mi_banners) |>
  step_delayed_adstock(mi_tv) |>
  step_delayed_adstock(mi_radio) |>
  step_hill_saturation(mi_banners, max_ref = TRUE) |>
  step_hill_saturation(mi_tv, max_ref = TRUE) |>
  step_hill_saturation(mi_radio, max_ref = TRUE)
wb_init_mmm <-
  recipe(kpi_sales ~ ., data = mmm_imps) |>
  add_role(c(mi_tv, mi_radio, mi_banners), new_role = "mi") |>
  update_role(Date, new_role = "temp") |>
  update_role_requirements("temp", bake = FALSE) |>
  step_weibull_adstock(mi_banners) |>
  step_delayed_adstock(mi_tv) |>
  step_delayed_adstock(mi_radio) |>
  step_hill_saturation(mi_banners, max_ref = TRUE) |>
  step_hill_saturation(mi_tv, max_ref = TRUE) |>
  step_hill_saturation(mi_radio, max_ref = TRUE)

Recipe with tuned carryover and saturation transformations

cs_tuned_mmm <-
  recipe(kpi_sales ~ ., data = mmm_imps) |>
  add_role(c(mi_tv, mi_radio, mi_banners), new_role = "mi") |>
  update_role(Date, new_role = "temp") |>
  update_role_requirements("temp", bake = FALSE) |>
  step_geometric_adstock(mi_banners, decay = 0.432, max_carryover = 1) |> 
  step_hill_saturation(mi_banners, shape = 1.30, max_ref = TRUE) |> 
  step_geometric_adstock(mi_tv, decay = 0.0181, max_carryover = 8) |> 
  step_hill_saturation(mi_tv, shape = 0.783, max_ref = TRUE) |> 
  step_geometric_adstock(mi_radio, decay = 0.193, max_carryover = 1) |> 
  step_hill_saturation(mi_radio, shape = 1.06, max_ref = TRUE)

Setup Workflows

Using the same linear regression with the Bayesian estimation engine.

lm_mod <- linear_reg() |> set_engine("stan")

ln_init_wflow <-
  workflow() |>
  add_model(lm_mod) |>
  add_recipe(ln_init_mmm)

cs_init_wflow <-
  workflow() |>
  add_model(lm_mod) |>
  add_recipe(cs_init_mmm)

dl_init_wflow <-
  workflow() |>
  add_model(lm_mod) |>
  add_recipe(dl_init_mmm)

wb_init_wflow <-
  workflow() |>
  add_model(lm_mod) |>
  add_recipe(wb_init_mmm)

cs_tuned_wflow <-
  workflow() |>
  add_model(lm_mod) |>
  add_recipe(cs_tuned_mmm)

Fitting Models / Running Workflows

ln_init_fit <- ln_init_wflow |> fit(data = mmm_imps)
cs_init_fit <- cs_init_wflow |> fit(data = mmm_imps)
dl_init_fit <- dl_init_wflow |> fit(data = mmm_imps)
wb_init_fit <- wb_init_wflow |> fit(data = mmm_imps)
cs_tuned_fit <- cs_tuned_wflow |> fit(data = mmm_imps)

Use tidy to extract fitted parameters

tidy(ln_init_fit, conf.int = TRUE)
#> # A tibble: 4 × 5
#>   term         estimate  std.error  conf.low conf.high
#>   <chr>           <dbl>      <dbl>     <dbl>     <dbl>
#> 1 (Intercept) 6992.     220.       6651.     7359.    
#> 2 mi_tv          0.0138   0.000796    0.0125    0.0152
#> 3 mi_radio       0.131    0.0107      0.114     0.148 
#> 4 mi_banners     0.661    0.0672      0.553     0.767
tidy(cs_init_fit, conf.int = TRUE)
#> # A tibble: 4 × 5
#>   term         estimate std.error  conf.low conf.high
#>   <chr>           <dbl>     <dbl>     <dbl>     <dbl>
#> 1 (Intercept) 1760.     402.      1114.     2425.    
#> 2 mi_tv          0.0281   0.00119    0.0261    0.0300
#> 3 mi_radio       0.252    0.0218     0.217     0.288 
#> 4 mi_banners     1.92     0.165      1.65      2.20
tidy(dl_init_fit, conf.int = TRUE)
#> # A tibble: 4 × 5
#>   term         estimate std.error  conf.low conf.high
#>   <chr>           <dbl>     <dbl>     <dbl>     <dbl>
#> 1 (Intercept) 5376.     594.      4382.     6366.    
#> 2 mi_tv          0.0202   0.00153    0.0176    0.0227
#> 3 mi_radio       0.145    0.0336     0.0902    0.200 
#> 4 mi_banners     0.941    0.239      0.518     1.32
tidy(wb_init_fit, conf.int = TRUE)
#> # A tibble: 4 × 5
#>   term         estimate std.error  conf.low conf.high
#>   <chr>           <dbl>     <dbl>     <dbl>     <dbl>
#> 1 (Intercept) 4107.     502.      3288.     4917.    
#> 2 mi_tv          0.0204   0.00144    0.0181    0.0227
#> 3 mi_radio       0.154    0.0295     0.106     0.201 
#> 4 mi_banners     1.50     0.190      1.19      1.81
tidy(cs_tuned_fit, conf.int = TRUE)
#> # A tibble: 4 × 5
#>   term         estimate  std.error  conf.low conf.high
#>   <chr>           <dbl>      <dbl>     <dbl>     <dbl>
#> 1 (Intercept) 6563.     197.       6238.     6883.    
#> 2 mi_tv          0.0162   0.000718    0.0150    0.0174
#> 3 mi_radio       0.116    0.00791     0.103     0.129 
#> 4 mi_banners     0.854    0.0736      0.733     0.972

Post-Modeling

Named List of trained models

models <- list(
  ln_init_fit = ln_init_fit,
  cs_init_fit = cs_init_fit,
  dl_init_fit = dl_init_fit,
  wb_init_fit = wb_init_fit,
  cs_tuned_fit = cs_tuned_fit
  )

Model Decomposition

model_decomps <- decompose(models, new_data = mmm_imps, spend_data = mmm_spend)
model_decomps
#> # A tibble: 5 × 5
#>   model        workflow   decomp             roi              pred              
#>   <chr>        <list>     <list>             <list>           <list>            
#> 1 ln_init_fit  <workflow> <tibble [800 × 9]> <tibble [4 × 4]> <tibble [200 × 1]>
#> 2 cs_init_fit  <workflow> <tibble [800 × 9]> <tibble [4 × 4]> <tibble [200 × 1]>
#> 3 dl_init_fit  <workflow> <tibble [800 × 9]> <tibble [4 × 4]> <tibble [200 × 1]>
#> 4 wb_init_fit  <workflow> <tibble [800 × 9]> <tibble [4 × 4]> <tibble [200 × 1]>
#> 5 cs_tuned_fit <workflow> <tibble [800 × 9]> <tibble [4 × 4]> <tibble [200 × 1]>

Model Metrics & Fit

model_metrics(model_decomps, new_data = mmm_imps)
#> # A tibble: 5 × 4
#>   model        workflow     rsq  mape
#>   <chr>        <list>     <dbl> <dbl>
#> 1 cs_tuned_fit <workflow> 0.819  9.42
#> 2 cs_init_fit  <workflow> 0.814  9.90
#> 3 ln_init_fit  <workflow> 0.752 11.2 
#> 4 wb_init_fit  <workflow> 0.618 13.7 
#> 5 dl_init_fit  <workflow> 0.533 15.4
plot_model_fit(model_decomps)

Contributions

Contribution percent breakdown

plot_base_contribution(model_decomps)

model_decomps |> 
  filter(model == "cs_tuned_fit") |> 
  plot_channel_contribution_month(
    begin_date = "2021-04-01", 
    end_date = "2022-01-01"
    )

Channel contrbution volume and ROIs

channel_metrics(model_decomps)
#> # A tibble: 4 × 11
#>   term   sales_ln_init_fit sales_cs_init_fit sales_dl_init_fit sales_wb_init_fit
#>   <chr>              <dbl>             <dbl>             <dbl>             <dbl>
#> 1 base            1397307.           352848.          1075685.           821563.
#> 2 mi_tv            208236.           594139.           447573.           451876.
#> 3 mi_ra…           210475.           449617.           250609.           266390.
#> 4 mi_ba…           316862.           736554.           361497.           594800.
#> # ℹ 6 more variables: sales_cs_tuned_fit <dbl>, ROI_ln_init_fit <dbl>,
#> #   ROI_cs_init_fit <dbl>, ROI_dl_init_fit <dbl>, ROI_wb_init_fit <dbl>,
#> #   ROI_cs_tuned_fit <dbl>

Plot ROIs

plot_roi(model_decomps)

Response Curves

Generate response from model decomposition

response <- response_curves(model_decomps)
response
#> # A tibble: 24,000 × 8
#>    model       var        var_shape var_nu mid_point_x mid_point_y     x      y
#>    <chr>       <chr>          <dbl>  <dbl>       <dbl>       <dbl> <dbl>  <dbl>
#>  1 cs_init_fit mi_banners         1      1     304145.     736554.    0      0 
#>  2 cs_init_fit mi_banners         1      1     304145.     736554.  304.  1472.
#>  3 cs_init_fit mi_banners         1      1     304145.     736554.  609.  2942.
#>  4 cs_init_fit mi_banners         1      1     304145.     736554.  913.  4408.
#>  5 cs_init_fit mi_banners         1      1     304145.     736554. 1217.  5872.
#>  6 cs_init_fit mi_banners         1      1     304145.     736554. 1521.  7333.
#>  7 cs_init_fit mi_banners         1      1     304145.     736554. 1826.  8790.
#>  8 cs_init_fit mi_banners         1      1     304145.     736554. 2130. 10245.
#>  9 cs_init_fit mi_banners         1      1     304145.     736554. 2434. 11697.
#> 10 cs_init_fit mi_banners         1      1     304145.     736554. 2739. 13146.
#> # ℹ 23,990 more rows

Visualize response curves

Estimate mROI and ROI from the response curves

mroi(response)
#> # A tibble: 12 × 4
#> # Groups:   var [3]
#>    var        model          ROI  mROI
#>    <chr>      <chr>        <dbl> <dbl>
#>  1 mi_banners cs_init_fit  2.42  1.21 
#>  2 mi_banners cs_tuned_fit 1.12  0.727
#>  3 mi_banners dl_init_fit  1.19  0.595
#>  4 mi_banners wb_init_fit  1.96  0.978
#>  5 mi_radio   cs_init_fit  1.02  0.508
#>  6 mi_radio   cs_tuned_fit 0.493 0.261
#>  7 mi_radio   dl_init_fit  0.566 0.283
#>  8 mi_radio   wb_init_fit  0.602 0.301
#>  9 mi_tv      cs_init_fit  1.01  0.504
#> 10 mi_tv      cs_tuned_fit 0.449 0.176
#> 11 mi_tv      dl_init_fit  0.760 0.380
#> 12 mi_tv      wb_init_fit  0.767 0.384