PythonによるMMM(マーケティングミックスモデリング)とビジネス活用

- 振返り分析・最適投資配分編(その4)-
線形回帰系MMMの最適投資配分
(季節成分の考慮)

PythonによるMMM(マーケティングミックスモデリング)とビジネス活用- 振返り分析・最適投資配分編(その4)-線形回帰系MMMの最適投資配分(季節成分の考慮)

データを活用したマーケティング戦略は、ビジネスの成功に不可欠です。その中心に位置するのが、マーケティングミックスモデリング(MMM)です。

マーケティングミックスモデリング(MMM)は、過去のデータを分析する「振り返り分析」と未来のトレンドを予測する「近未来分析」の両方で有効に活用される強力なツールです。

どの広告・販促の手段に、どの程度のコストをかけて投資をするのが効率的なのか? 広告・販促の手段にかけるコストの最適投資配分です。

 

色々なアプローチがありますが、ストレートなのは非線形計画問題(非線形最適化)を定式化しソルバーで解くことです。具体的に言うと、PythonのScyPyである程度は実現することができます。

前回は「アドストックを考慮すべきどうか、コスト最適化の対象かどうか、特徴量を指定し自動調整し構築する線形回帰系MMM」というお話しをします。

 

多くの時系列データには季節性があります。前回のモデルには、季節性などを考慮していませんでした。

今回は、「季節性を考慮したRidge MMMパイプラインを構築し最適投資配分を求める方法」のお話しをします。

データセットと最適投資配分

 今回利用するデータセット

以下のデータセットを利用します。

  • 目的変数:売上金額(Sales)

  • 説明変数:
    • メディア:TVCM、Newspaper、Webのコスト
    • イベント:XMas(クリスマス)フラグ(1: 12月25日、0: その他)

 

今回利用するデータセットは、以下からダウンロードできます。

MMM_xmas.csv
https://www.salesanalytics.co.jp/dcbd

 

今回は、以下のように設定します。

  • アドストックを考慮する
    • TVCMのコスト
    • Newspaperのコスト
    • Webのコスト
  • アドストックを考慮しない
    • XMas(クリスマス)フラグ(1: 12月25日、0: その他)
    • 季節成分(三角関数で表現)

季節成分を表現する三角関数(Sin, Cos)の特徴量は、データセットには含まれていません。データセットを読み込んだ後に、この特徴量を生成します。

 

 今回構築するMMMと最適化

今回のMMM(マーケティングミックスモデリング)の予算最適化問題は、全体コスト一定の下で売上を最大化する、広告・販促の手段に対するコストの最適配分を求める問題になります。

 

MMMそのものが目的関数、MMMの出力である売上が目的変数になります。推定器にはRidge回帰を使います。

このRidge MMMパイプラインのハイパーパラメータは、Optunaで自動チューニングします。

 

特徴量は2つの視点で分けていきます。

  • アドストックを考慮する特徴量 or そうでない特徴量
    • 考慮する特徴量:TVCM、Newspaper、Webのコスト
    • そうでない特徴量:XMas(クリスマス)フラグ、季節成分(三角関数で表現)
  • 最適化対象となる特徴量 or そうではない特徴量
    • 最適化対象となる特徴量:TVCM、Newspaper、Webのコスト
    • そうではない特徴量:XMas(クリスマス)フラグ、季節成分(三角関数で表現)

 

今回のケースですと、アドストックを考慮すべき特徴量=最適化対象となる特徴量です。

 

準備

 モジュールの読み込み

先ず、必要なモジュールを読み込みます。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from mmm_functions5 import *
from mmm_functions5 import *
from mmm_functions5 import *

 

Pythonファイル(mmm_functions5.py)そのものは、以下からダウンロードできます。

mmm_functions5.py ※zipファイルを解凍してお使いください
https://www.salesanalytics.co.jp/uno8

 

mmm_functions5.pyには、MMMで共通して利用する処理を関数化したものや、クラス、モジュール類などを記載しています。前回作った最適化用の関数も含まれています。

mmm_functions5.pyを利用するときは、実行するPythonファイルやNotebookと同じフォルダに入れておいてください。

上手くいかないときは、mmm_functions5.pyをメモ帳などで開き内容をコピーし、実行するPythonファイルやNotebookにコードを張り付け、Pythonで関数を作ってからMMM構築などを行ってください。

 

 データセットの読み込み

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# データセット読み込み
dataset = 'https://www.salesanalytics.co.jp/dcbd'
df = pd.read_csv(
dataset,
parse_dates=['Week'],
index_col='Week',
)
# データセット読み込み dataset = 'https://www.salesanalytics.co.jp/dcbd' df = pd.read_csv( dataset, parse_dates=['Week'], index_col='Week', )
# データセット読み込み
dataset = 'https://www.salesanalytics.co.jp/dcbd'
df = pd.read_csv(
    dataset,
    parse_dates=['Week'],
    index_col='Week',
)

 

季節性を表現する特徴量を、三角関数(Sin , Cos)で生成します。

SinとCosのセットであるフーリエ項(基底)をその程度作るのかで、季節性の表現の柔軟性が変わります。多ければ多いほど柔軟な表現になります。フーリエ項(基底)の係数は、データで後で学習し求めます。

まず、フーリエ項(基底)を生成し元のデータセットに付け加える関数を定義します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#
# 三角関数特徴量の生成関数を定義
#
def add_fourier_terms(df, num, seasonal):
'''
引数df: 元のデータフレーム
引数num: フーリエ項の数(基底の数)、sinとcosのセット数
引数seasonal: フーリエ変換の周期
戻り値: フーリエ項を追加後のデータフレーム
'''
# t列がdfにない場合、t列を0からdfの長さまでの連番で作成
if 't' not in df.columns:
df['t'] = pd.RangeIndex(start=0, stop=len(df))
# 三角関数特徴量(フーリエ項)を追加
for i in range(1, num + 1):
# sin項を追加
df['sin_' + str(i)] = np.sin(i * 2 * np.pi * df.t / seasonal)
# cos項を追加
df['cos_' + str(i)] = np.cos(i * 2 * np.pi * df.t / seasonal)
return df
# # 三角関数特徴量の生成関数を定義 # def add_fourier_terms(df, num, seasonal): ''' 引数df: 元のデータフレーム 引数num: フーリエ項の数(基底の数)、sinとcosのセット数 引数seasonal: フーリエ変換の周期 戻り値: フーリエ項を追加後のデータフレーム ''' # t列がdfにない場合、t列を0からdfの長さまでの連番で作成 if 't' not in df.columns: df['t'] = pd.RangeIndex(start=0, stop=len(df)) # 三角関数特徴量(フーリエ項)を追加 for i in range(1, num + 1): # sin項を追加 df['sin_' + str(i)] = np.sin(i * 2 * np.pi * df.t / seasonal) # cos項を追加 df['cos_' + str(i)] = np.cos(i * 2 * np.pi * df.t / seasonal) return df
#
# 三角関数特徴量の生成関数を定義
#

def add_fourier_terms(df, num, seasonal):
    '''
    引数df: 元のデータフレーム
    引数num: フーリエ項の数(基底の数)、sinとcosのセット数
    引数seasonal: フーリエ変換の周期
    戻り値: フーリエ項を追加後のデータフレーム
    '''
    # t列がdfにない場合、t列を0からdfの長さまでの連番で作成
    if 't' not in df.columns:
        df['t'] = pd.RangeIndex(start=0, stop=len(df))

    # 三角関数特徴量(フーリエ項)を追加
    for i in range(1, num + 1):
        # sin項を追加
        df['sin_' + str(i)] = np.sin(i * 2 * np.pi * df.t / seasonal)
        # cos項を追加
        df['cos_' + str(i)] = np.cos(i * 2 * np.pi * df.t / seasonal)

    return df

 

この関数を使い、季節成分を生成しデータセットに加えます。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 三角関数特徴量を追加したデータフレームを表示して確認
df = add_fourier_terms(df, num=5, seasonal=52.25)
print(df) #確認
# 三角関数特徴量を追加したデータフレームを表示して確認 df = add_fourier_terms(df, num=5, seasonal=52.25) print(df) #確認
# 三角関数特徴量を追加したデータフレームを表示して確認
df = add_fourier_terms(df, num=5, seasonal=52.25)

print(df) #確認

周期を52.25に設定しています。年間の週の数が52~53週間あり、多くは年間52週間ですが約4年に1度の間隔で53週になるからです。

 

以下、実行結果です。

              Sales     TVCM  Newspaper     Web  xmas    t     sin_1  \
Week                                                                   
2018-01-07  2132000  1312200          0       0     0    0  0.000000   
2018-01-14  2596100        0     502900  237400     0    1  0.119963   
2018-01-21  2236200        0     432100  200600     0    2  0.238193   
2018-01-28  1680900        0     338400       0     0    3  0.352983   
2018-02-04  2155400        0          0  234000     0    4  0.462674   
...             ...      ...        ...     ...   ...  ...       ...   
2021-11-28   833900        0          0       0     0  203 -0.660522   
2021-12-05  2064700        0     516700  166800     0  204 -0.565683   
2021-12-12  1689500        0          0  206600     0  205 -0.462674   
2021-12-19  3399200        0     664500       0     1  206 -0.352983   
2021-12-26  1461100        0          0  197600     0  207 -0.238193   

               cos_1     sin_2     cos_2     sin_3     cos_3     sin_4  \
Week                                                                     
2018-01-07  1.000000  0.000000  1.000000  0.000000  1.000000  0.000000   
2018-01-14  0.992778  0.238193  0.971218  0.352983  0.935630  0.462674   
2018-01-21  0.971218  0.462674  0.886528  0.660522  0.750806  0.820348   
2018-01-28  0.935630  0.660522  0.750806  0.883026  0.469324  0.991849   
2018-02-04  0.886528  0.820348  0.571865  0.991849  0.127421  0.938256   
...              ...       ...       ...       ...       ...       ...   
2021-11-28  0.750806 -0.991849  0.127421 -0.828851 -0.559470 -0.252764   
2021-12-05  0.824623 -0.932951  0.360005 -0.972981 -0.230887 -0.671733   
2021-12-12  0.886528 -0.820348  0.571865 -0.991849  0.127421 -0.938256   
2021-12-19  0.935630 -0.660522  0.750806 -0.883026  0.469324 -0.991849   
2021-12-26  0.971218 -0.462674  0.886528 -0.660522  0.750806 -0.820348   

               cos_4     sin_5     cos_5  
Week                                      
2018-01-07  1.000000  0.000000  1.000000  
2018-01-14  0.886528  0.565683  0.824623  
2018-01-21  0.571865  0.932951  0.360005  
2018-01-28  0.127421  0.972981 -0.230887  
2018-02-04 -0.345941  0.671733 -0.740793  
...              ...       ...       ...  
2021-11-28 -0.967528  0.449297 -0.893383  
2021-12-05 -0.740793 -0.134872 -0.990863  
2021-12-12 -0.345941 -0.671733 -0.740793  
2021-12-19  0.127421 -0.972981 -0.230887  
2021-12-26  0.571865 -0.932951  0.360005  

[208 rows x 16 columns]

 

季節成分を表現する特徴量が付け加えられているのが分かるかと思います。機能するかどうか分かりませんが、トレンド特徴量 t も追加しています。

 

目的変数(予測対象)と説明変数に分解します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 説明変数Xと目的変数yに分解
X = df.drop(columns=['Sales'])
y = df['Sales']
# 説明変数Xと目的変数yに分解 X = df.drop(columns=['Sales']) y = df['Sales']
# 説明変数Xと目的変数yに分解
X = df.drop(columns=['Sales'])
y = df['Sales']

 

目的変数y(売上)をプロットします。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
y.plot()
plt.show()
y.plot() plt.show()
y.plot()
plt.show()

 

以下、実行結果です。

 

説明変数X(各メディアのコスト)をまとめてプロットします。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
X.plot(subplots=True,figsize=(12,X.shape[1]*3))
plt.show()
X.plot(subplots=True,figsize=(12,X.shape[1]*3)) plt.show()
X.plot(subplots=True,figsize=(12,X.shape[1]*3))
plt.show()

 

以下、実行結果です。季節成分を表現する特徴量も表示されています。

 

目的関数作り

今回の最適投資配分問題の目的関数は、Ridge MMMMパイプラインそのものになります。

 

 アドストックを考慮する特徴量のリスト

先ず、アドストックを考慮する特徴量のリストを作ります。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# アドストックを考慮する特徴量のリストを作成
apply_effects_features = ['TVCM', 'Newspaper', 'Web']
# アドストックを考慮する特徴量のリストを作成 apply_effects_features = ['TVCM', 'Newspaper', 'Web']
# アドストックを考慮する特徴量のリストを作成
apply_effects_features = ['TVCM', 'Newspaper', 'Web']

 

 最適なハイパーパラメータの探索

Ridge MMMパイプラインハイパーパラメータOptunaで探索します。

先ず、Optunaの目的関数を定義します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#
# Optunaの目的関数
#
def ridge_objective_apply_effects(trial, X, y, apply_effects_features):
carryover_params = []
curve_params = []
# 列名リストからインデックスのリストを作成
apply_effects_indices = [X.columns.get_loc(column) for column in apply_effects_features]
no_effects_indices = list(set(range(X.shape[1])) - set(apply_effects_indices))
for feature in apply_effects_features:
carryover_length = trial.suggest_int(f'carryover_length_{feature}', 1, 10)
carryover_peak = trial.suggest_int(f'carryover_peak_{feature}', 1, carryover_length)
carryover_rate1 = trial.suggest_float(f'carryover_rate1_{feature}', 0, 1)
carryover_rate2 = trial.suggest_float(f'carryover_rate2_{feature}', 0, 1)
carryover_c1 = trial.suggest_float(f'carryover_c1_{feature}', 0, 2)
carryover_c2 = trial.suggest_float(f'carryover_c2_{feature}', 0, 2)
carryover_params.append({
'length': carryover_length,
'peak': carryover_peak,
'rate1': carryover_rate1,
'rate2': carryover_rate2,
'c1': carryover_c1,
'c2': carryover_c2,
})
saturation_function = trial.suggest_categorical(f'saturation_function_{feature}', ['logistic', 'exponential'])
if saturation_function == 'logistic':
curve_param_L = trial.suggest_float(f'curve_param_L_{feature}', 0, 10)
curve_param_k = trial.suggest_float(f'curve_param_k_{feature}', 0, 10)
curve_param_x0 = trial.suggest_float(f'curve_param_x0_{feature}', 0, 2)
curve_params.append({
'saturation_function': saturation_function,
'L': curve_param_L,
'k': curve_param_k,
'x0': curve_param_x0,
})
elif saturation_function == 'exponential':
curve_param_d = trial.suggest_float(f'curve_param_d_{feature}', 0, 10)
curve_params.append({
'saturation_function': saturation_function,
'd': curve_param_d,
})
alpha = trial.suggest_float('alpha', 1e-3, 1e+3)
preprocessor = ColumnTransformer(
transformers=[
('effects', Pipeline([
('carryover', CustomCarryOverTransformer(carryover_params=carryover_params)),
('saturation', CustomSaturationTransformer(curve_params=curve_params))
]), apply_effects_indices),
('no_effects', 'passthrough', no_effects_indices)
],
remainder='drop'
)
pipeline = Pipeline(steps=[
('scaler', MinMaxScaler()),
('preprocessor', preprocessor),
('ridge', Ridge(alpha=alpha))
])
tscv = TimeSeriesSplit(n_splits=5)
scores = cross_val_score(pipeline, X, y, cv=tscv, scoring='neg_mean_squared_error')
rmse = np.mean([np.sqrt(-score) for score in scores])
return rmse
# # Optunaの目的関数 # def ridge_objective_apply_effects(trial, X, y, apply_effects_features): carryover_params = [] curve_params = [] # 列名リストからインデックスのリストを作成 apply_effects_indices = [X.columns.get_loc(column) for column in apply_effects_features] no_effects_indices = list(set(range(X.shape[1])) - set(apply_effects_indices)) for feature in apply_effects_features: carryover_length = trial.suggest_int(f'carryover_length_{feature}', 1, 10) carryover_peak = trial.suggest_int(f'carryover_peak_{feature}', 1, carryover_length) carryover_rate1 = trial.suggest_float(f'carryover_rate1_{feature}', 0, 1) carryover_rate2 = trial.suggest_float(f'carryover_rate2_{feature}', 0, 1) carryover_c1 = trial.suggest_float(f'carryover_c1_{feature}', 0, 2) carryover_c2 = trial.suggest_float(f'carryover_c2_{feature}', 0, 2) carryover_params.append({ 'length': carryover_length, 'peak': carryover_peak, 'rate1': carryover_rate1, 'rate2': carryover_rate2, 'c1': carryover_c1, 'c2': carryover_c2, }) saturation_function = trial.suggest_categorical(f'saturation_function_{feature}', ['logistic', 'exponential']) if saturation_function == 'logistic': curve_param_L = trial.suggest_float(f'curve_param_L_{feature}', 0, 10) curve_param_k = trial.suggest_float(f'curve_param_k_{feature}', 0, 10) curve_param_x0 = trial.suggest_float(f'curve_param_x0_{feature}', 0, 2) curve_params.append({ 'saturation_function': saturation_function, 'L': curve_param_L, 'k': curve_param_k, 'x0': curve_param_x0, }) elif saturation_function == 'exponential': curve_param_d = trial.suggest_float(f'curve_param_d_{feature}', 0, 10) curve_params.append({ 'saturation_function': saturation_function, 'd': curve_param_d, }) alpha = trial.suggest_float('alpha', 1e-3, 1e+3) preprocessor = ColumnTransformer( transformers=[ ('effects', Pipeline([ ('carryover', CustomCarryOverTransformer(carryover_params=carryover_params)), ('saturation', CustomSaturationTransformer(curve_params=curve_params)) ]), apply_effects_indices), ('no_effects', 'passthrough', no_effects_indices) ], remainder='drop' ) pipeline = Pipeline(steps=[ ('scaler', MinMaxScaler()), ('preprocessor', preprocessor), ('ridge', Ridge(alpha=alpha)) ]) tscv = TimeSeriesSplit(n_splits=5) scores = cross_val_score(pipeline, X, y, cv=tscv, scoring='neg_mean_squared_error') rmse = np.mean([np.sqrt(-score) for score in scores]) return rmse
#
# Optunaの目的関数
#

def ridge_objective_apply_effects(trial, X, y, apply_effects_features):

    carryover_params = []
    curve_params = []

    # 列名リストからインデックスのリストを作成
    apply_effects_indices = [X.columns.get_loc(column) for column in apply_effects_features]
    no_effects_indices = list(set(range(X.shape[1])) - set(apply_effects_indices))
    
    for feature in apply_effects_features:
        carryover_length = trial.suggest_int(f'carryover_length_{feature}', 1, 10)
        carryover_peak = trial.suggest_int(f'carryover_peak_{feature}', 1, carryover_length)
        carryover_rate1 = trial.suggest_float(f'carryover_rate1_{feature}', 0, 1)
        carryover_rate2 = trial.suggest_float(f'carryover_rate2_{feature}', 0, 1)
        carryover_c1 = trial.suggest_float(f'carryover_c1_{feature}', 0, 2)
        carryover_c2 = trial.suggest_float(f'carryover_c2_{feature}', 0, 2)
        carryover_params.append({
            'length': carryover_length, 
            'peak': carryover_peak, 
            'rate1': carryover_rate1, 
            'rate2': carryover_rate2, 
            'c1': carryover_c1, 
            'c2': carryover_c2,
        })

        saturation_function = trial.suggest_categorical(f'saturation_function_{feature}', ['logistic', 'exponential'])
        if saturation_function == 'logistic':
            curve_param_L = trial.suggest_float(f'curve_param_L_{feature}', 0, 10)
            curve_param_k = trial.suggest_float(f'curve_param_k_{feature}', 0, 10)
            curve_param_x0 = trial.suggest_float(f'curve_param_x0_{feature}', 0, 2)
            curve_params.append({
                'saturation_function': saturation_function,
                'L': curve_param_L,
                'k': curve_param_k,
                'x0': curve_param_x0,
            })
        elif saturation_function == 'exponential':
            curve_param_d = trial.suggest_float(f'curve_param_d_{feature}', 0, 10)
            curve_params.append({
                'saturation_function': saturation_function,
                'd': curve_param_d,
            })

    alpha = trial.suggest_float('alpha', 1e-3, 1e+3)

    preprocessor = ColumnTransformer(
        transformers=[
            ('effects', Pipeline([
                ('carryover', CustomCarryOverTransformer(carryover_params=carryover_params)),
                ('saturation', CustomSaturationTransformer(curve_params=curve_params))
            ]), apply_effects_indices),
            ('no_effects', 'passthrough', no_effects_indices)
        ],
        remainder='drop'
    )

    pipeline = Pipeline(steps=[
        ('scaler', MinMaxScaler()),
        ('preprocessor', preprocessor),
        ('ridge', Ridge(alpha=alpha))
    ])

    tscv = TimeSeriesSplit(n_splits=5)
    scores = cross_val_score(pipeline, X, y, cv=tscv, scoring='neg_mean_squared_error')
    rmse = np.mean([np.sqrt(-score) for score in scores])

    return rmse

 

この目的関数を使い、最適なハイパーパラメータを探索する関数を定義します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#
# Optunaによる最適なハイパーパラメータの探索実行
#
def run_optimization(objective, X, y, apply_effects_features, n_trials=1000, study=None):
# Optunaのスタディの作成と最適化の実行
if study is None:
study = optuna.create_study(direction='minimize')
objective_with_data = partial(
objective,
X=X, y=y,
apply_effects_features=apply_effects_features)
study.optimize(
objective_with_data,
n_trials=n_trials,
show_progress_bar=True)
# 最適化の実行結果の表示
print("Best trial:")
trial = study.best_trial
print(f"Value: {trial.value}")
print("Params: ")
for key, value in trial.params.items():
print(f"{key}: {value}")
return study
# # Optunaによる最適なハイパーパラメータの探索実行 # def run_optimization(objective, X, y, apply_effects_features, n_trials=1000, study=None): # Optunaのスタディの作成と最適化の実行 if study is None: study = optuna.create_study(direction='minimize') objective_with_data = partial( objective, X=X, y=y, apply_effects_features=apply_effects_features) study.optimize( objective_with_data, n_trials=n_trials, show_progress_bar=True) # 最適化の実行結果の表示 print("Best trial:") trial = study.best_trial print(f"Value: {trial.value}") print("Params: ") for key, value in trial.params.items(): print(f"{key}: {value}") return study
#
# Optunaによる最適なハイパーパラメータの探索実行
#

def run_optimization(objective, X, y, apply_effects_features, n_trials=1000, study=None):

    # Optunaのスタディの作成と最適化の実行
    if study is None:
        study = optuna.create_study(direction='minimize')

    objective_with_data = partial(
        objective, 
        X=X, y=y, 
        apply_effects_features=apply_effects_features)
        
    study.optimize(
        objective_with_data, 
        n_trials=n_trials, 
        show_progress_bar=True)

    # 最適化の実行結果の表示
    print("Best trial:")
    trial = study.best_trial

    print(f"Value: {trial.value}")
    print("Params: ")
    for key, value in trial.params.items():
        print(f"{key}: {value}")

    return study

 

この関数を実行し、最適なハイパーパラメータを探索します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
study = run_optimization(
ridge_objective_apply_effects,
X, y,
apply_effects_features,
n_trials=5000)
study = run_optimization( ridge_objective_apply_effects, X, y, apply_effects_features, n_trials=5000)
study = run_optimization(
    ridge_objective_apply_effects, 
    X, y, 
    apply_effects_features, 
    n_trials=5000)

 

以下、実行結果です。

Best trial: 4938. Best value: 237010: 100%|██████████| 5000/5000 [52:42<00:00,  1.58it/s]Best trial:
Value: 237010.09434071128
Params: 
carryover_length_TVCM: 5
carryover_peak_TVCM: 1
carryover_rate1_TVCM: 0.9729271018598733
carryover_rate2_TVCM: 0.2966298925357188
carryover_c1_TVCM: 1.2314141219113544
carryover_c2_TVCM: 0.546825967722917
saturation_function_TVCM: exponential
curve_param_d_TVCM: 1.9183272984812656
carryover_length_Newspaper: 2
carryover_peak_Newspaper: 1
carryover_rate1_Newspaper: 0.17197042046525332
carryover_rate2_Newspaper: 0.43965062701472585
carryover_c1_Newspaper: 1.0338752928967323
carryover_c2_Newspaper: 0.13346846589535857
saturation_function_Newspaper: exponential
curve_param_d_Newspaper: 1.6739264306006938
carryover_length_Web: 7
carryover_peak_Web: 1
carryover_rate1_Web: 0.5503030669166483
carryover_rate2_Web: 0.19277860756776197
carryover_c1_Web: 1.3333660220153671
carryover_c2_Web: 1.751549177057382
saturation_function_Web: logistic
curve_param_L_Web: 1.674062430952774
curve_param_k_Web: 2.8891224711920658
curve_param_x0_Web: 0.3463030631752817
alpha: 0.08860413121661548

 

 MMMパイプラインの構築

Optunaで探索し見つけたハイパーパラメータを取得し、Ridge MMMパイプラインを構築します。

そのための関数を定義します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def create_model_from_trial_ridge(trial, X, y, apply_effects_features):
#
# Optunaの実行結果からハイパーパラメータを取得
#
carryover_keys = ['length', 'peak', 'rate1', 'rate2', 'c1', 'c2']
curve_keys_logistic = ['L', 'k', 'x0']
curve_keys_exponential = ['d']
# ハイパーパラメータを抽出しキーと値の辞書を作成
def fetch_params(prefix, feature_name, params, trial_params):
return {key: trial_params[f'{prefix}_{key}_{feature_name}'] for key in params}
# キャリーオーバー効果関数ハイパーパラメータを抽出
carryover_params = [fetch_params('carryover', feature_name, carryover_keys, trial.params) for feature_name in apply_effects_features]
# 飽和関数ハイパーパラメータを抽出
curve_params = []
for feature_name in apply_effects_features:
saturation_function = trial.params[f'saturation_function_{feature_name}']
curve_param = {'saturation_function': saturation_function}
if saturation_function == 'logistic':
curve_param.update(fetch_params('curve_param', feature_name, curve_keys_logistic, trial.params))
elif saturation_function == 'exponential':
curve_param.update(fetch_params('curve_param', feature_name, curve_keys_exponential, trial.params))
curve_params.append(curve_param)
# 推定器ハイパーパラメータを抽出
alpha = trial.params['alpha']
#
# MMMパイプラインの構築&amp;学習
#
MMM_pipeline, trained_model, pred = build_MMM_pipeline_ridge(
X, y,
apply_effects_features,
carryover_params,
curve_params,
alpha)
# 最適ハイパーパラメータの集約
model_params = [
carryover_params,
curve_params,
alpha]
return trained_model,model_params
def create_model_from_trial_ridge(trial, X, y, apply_effects_features): # # Optunaの実行結果からハイパーパラメータを取得 # carryover_keys = ['length', 'peak', 'rate1', 'rate2', 'c1', 'c2'] curve_keys_logistic = ['L', 'k', 'x0'] curve_keys_exponential = ['d'] # ハイパーパラメータを抽出しキーと値の辞書を作成 def fetch_params(prefix, feature_name, params, trial_params): return {key: trial_params[f'{prefix}_{key}_{feature_name}'] for key in params} # キャリーオーバー効果関数ハイパーパラメータを抽出 carryover_params = [fetch_params('carryover', feature_name, carryover_keys, trial.params) for feature_name in apply_effects_features] # 飽和関数ハイパーパラメータを抽出 curve_params = [] for feature_name in apply_effects_features: saturation_function = trial.params[f'saturation_function_{feature_name}'] curve_param = {'saturation_function': saturation_function} if saturation_function == 'logistic': curve_param.update(fetch_params('curve_param', feature_name, curve_keys_logistic, trial.params)) elif saturation_function == 'exponential': curve_param.update(fetch_params('curve_param', feature_name, curve_keys_exponential, trial.params)) curve_params.append(curve_param) # 推定器ハイパーパラメータを抽出 alpha = trial.params['alpha'] # # MMMパイプラインの構築&amp;学習 # MMM_pipeline, trained_model, pred = build_MMM_pipeline_ridge( X, y, apply_effects_features, carryover_params, curve_params, alpha) # 最適ハイパーパラメータの集約 model_params = [ carryover_params, curve_params, alpha] return trained_model,model_params
def create_model_from_trial_ridge(trial, X, y, apply_effects_features):

    #
    # Optunaの実行結果からハイパーパラメータを取得
    #

    carryover_keys = ['length', 'peak', 'rate1', 'rate2', 'c1', 'c2']
    curve_keys_logistic = ['L', 'k', 'x0']
    curve_keys_exponential = ['d']

    # ハイパーパラメータを抽出しキーと値の辞書を作成
    def fetch_params(prefix, feature_name, params, trial_params):
        return {key: trial_params[f'{prefix}_{key}_{feature_name}'] for key in params}

    # キャリーオーバー効果関数ハイパーパラメータを抽出
    carryover_params = [fetch_params('carryover', feature_name, carryover_keys, trial.params) for feature_name in apply_effects_features]

    # 飽和関数ハイパーパラメータを抽出
    curve_params = []
    for feature_name in apply_effects_features:
        saturation_function = trial.params[f'saturation_function_{feature_name}']
        curve_param = {'saturation_function': saturation_function}
        
        if saturation_function == 'logistic':
            curve_param.update(fetch_params('curve_param', feature_name, curve_keys_logistic, trial.params))
        elif saturation_function == 'exponential':
            curve_param.update(fetch_params('curve_param', feature_name, curve_keys_exponential, trial.params))
        
        curve_params.append(curve_param)

    # 推定器ハイパーパラメータを抽出
    alpha = trial.params['alpha']

    #
    # MMMパイプラインの構築&amp;学習
    #

    MMM_pipeline, trained_model, pred = build_MMM_pipeline_ridge(
        X, y, 
        apply_effects_features, 
        carryover_params, 
        curve_params,
        alpha)

    # 最適ハイパーパラメータの集約
    model_params = [
        carryover_params, 
        curve_params, 
        alpha]

    return trained_model,model_params

 

戻り値のtrained_modelが学習済みのRidge MMMパイプラインで、model_paramsがそのハイパーパラメータです。

では、この関数を実行します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
trained_model,model_params = create_model_from_trial_ridge(
study.best_trial,
X, y,
apply_effects_features)
trained_model,model_params = create_model_from_trial_ridge( study.best_trial, X, y, apply_effects_features)
trained_model,model_params = create_model_from_trial_ridge(
    study.best_trial, 
    X, y, 
    apply_effects_features)

 

以下、実行結果です。

RMSE: 177802.61978860805
MAE: 145419.2861950865
MAPE: 0.07475479898495105
R2: 0.9116853811170141

 

アドストックの状況をグラフで確認します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# グラフで確認
best_carryover_params = model_params[0]
best_curve_params = model_params[1]
plot_effects(
best_carryover_params,
best_curve_params,
apply_effects_features)
# グラフで確認 best_carryover_params = model_params[0] best_curve_params = model_params[1] plot_effects( best_carryover_params, best_curve_params, apply_effects_features)
# グラフで確認
best_carryover_params = model_params[0]
best_curve_params = model_params[1]

plot_effects(
    best_carryover_params, 
    best_curve_params, 
    apply_effects_features)

 

以下、実行結果です。

 

 売上貢献度とマーケティングROI

このMMMパイプラインから、売上貢献度やマーケティングROIなどを求めていきます。

先ずは、貢献度の推移です。

以下、関数です。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 貢献度の算出
def calculate_and_plot_contribution(y, X, model, ylim=None, apply_effects_features=None):
"""
各媒体の売上貢献度を算定し、結果をプロットする関数。
:param y: ターゲット変数
:param X: 特徴量のデータフレーム
:param model: 学習済みモデル
:param ylim: y軸の表示範囲
:return: 各媒体の貢献度
"""
if apply_effects_features is None:
apply_effects_features = X.columns
# yの予測
pred = model.predict(X)
pred = pd.DataFrame(pred, index=X.index, columns=['y'])
# 値がすべて0の説明変数
X_ = X.copy()
X_[apply_effects_features] = 0
# Baseの予測
base = model.predict(X_)
pred['Base'] = base
# 各媒体の予測
for feature in apply_effects_features:
X_[apply_effects_features] = 0
X_[feature] = X[feature]
pred[feature] = model.predict(X_) - base
# 予測値の補正
correction_factor = y.div(pred.y, axis=0)
pred_adj = pred.mul(correction_factor, axis=0)
contribution = pred_adj.drop(columns=['y'])
# apply_effects_features以外をbaseに集約
contribution['Base'] = contribution.drop(columns=apply_effects_features).sum(axis=1)
contribution = contribution[['Base'] + list(apply_effects_features)]
# エリアプロット
if ylim is not None:
ax = contribution.plot.area()
handles, labels = ax.get_legend_handles_labels()
ax.legend(reversed(handles), reversed(labels))
ax.set_ylim(ylim)
plt.title('Contributions over time')
plt.show()
return contribution
# 貢献度の算出 def calculate_and_plot_contribution(y, X, model, ylim=None, apply_effects_features=None): """ 各媒体の売上貢献度を算定し、結果をプロットする関数。 :param y: ターゲット変数 :param X: 特徴量のデータフレーム :param model: 学習済みモデル :param ylim: y軸の表示範囲 :return: 各媒体の貢献度 """ if apply_effects_features is None: apply_effects_features = X.columns # yの予測 pred = model.predict(X) pred = pd.DataFrame(pred, index=X.index, columns=['y']) # 値がすべて0の説明変数 X_ = X.copy() X_[apply_effects_features] = 0 # Baseの予測 base = model.predict(X_) pred['Base'] = base # 各媒体の予測 for feature in apply_effects_features: X_[apply_effects_features] = 0 X_[feature] = X[feature] pred[feature] = model.predict(X_) - base # 予測値の補正 correction_factor = y.div(pred.y, axis=0) pred_adj = pred.mul(correction_factor, axis=0) contribution = pred_adj.drop(columns=['y']) # apply_effects_features以外をbaseに集約 contribution['Base'] = contribution.drop(columns=apply_effects_features).sum(axis=1) contribution = contribution[['Base'] + list(apply_effects_features)] # エリアプロット if ylim is not None: ax = contribution.plot.area() handles, labels = ax.get_legend_handles_labels() ax.legend(reversed(handles), reversed(labels)) ax.set_ylim(ylim) plt.title('Contributions over time') plt.show() return contribution
# 貢献度の算出
def calculate_and_plot_contribution(y, X, model, ylim=None, apply_effects_features=None):
    """
    各媒体の売上貢献度を算定し、結果をプロットする関数。

    :param y: ターゲット変数
    :param X: 特徴量のデータフレーム
    :param model: 学習済みモデル
    :param ylim: y軸の表示範囲
    :return: 各媒体の貢献度
    """
    if apply_effects_features is None:
        apply_effects_features = X.columns

    # yの予測
    pred = model.predict(X)
    pred = pd.DataFrame(pred, index=X.index, columns=['y'])

    # 値がすべて0の説明変数
    X_ = X.copy()
    X_[apply_effects_features] = 0

    # Baseの予測
    base = model.predict(X_)
    pred['Base'] = base

    # 各媒体の予測
    for feature in apply_effects_features:
        X_[apply_effects_features] = 0
        X_[feature] = X[feature]
        pred[feature] = model.predict(X_) - base

    # 予測値の補正
    correction_factor = y.div(pred.y, axis=0)
    pred_adj = pred.mul(correction_factor, axis=0)
    contribution = pred_adj.drop(columns=['y'])

    # apply_effects_features以外をbaseに集約
    contribution['Base'] = contribution.drop(columns=apply_effects_features).sum(axis=1)
    contribution = contribution[['Base'] + list(apply_effects_features)]

    # エリアプロット
    if ylim is not None:
        ax = contribution.plot.area()
        handles, labels = ax.get_legend_handles_labels()
        ax.legend(reversed(handles), reversed(labels))
        ax.set_ylim(ylim)
        plt.title('Contributions over time')
        plt.show()

    return contribution

 

この関数を使います。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 貢献度の算出
contribution = calculate_and_plot_contribution(
y, X,
trained_model, (0, 5e6),
apply_effects_features)
# 数値を表示
print(contribution)
# 貢献度の算出 contribution = calculate_and_plot_contribution( y, X, trained_model, (0, 5e6), apply_effects_features) # 数値を表示 print(contribution)
# 貢献度の算出
contribution = calculate_and_plot_contribution(
    y, X, 
    trained_model, (0, 5e6), 
    apply_effects_features)

# 数値を表示
print(contribution)

 

以下、実行結果です。

                    Base          TVCM      Newspaper            Web
Week                                                                
2018-01-07  9.733873e+05  1.158613e+06       0.000000       0.000000
2018-01-14  9.484278e+05  4.575676e+05  572240.872421  617863.665059
2018-01-21  8.437181e+05  2.446132e+05  590972.178279  556896.546019
2018-01-28  8.832017e+05  1.694063e+05  540022.457974   88269.537959
2018-02-04  1.079710e+06  1.453228e+05  242901.141448  687466.000783
...                  ...           ...            ...            ...
2021-11-28  6.187977e+05  0.000000e+00  160501.533787   54600.793797
2021-12-05  9.717705e+05  0.000000e+00  614667.094955  478262.412278
2021-12-12  8.355731e+05  0.000000e+00  285893.821197  568033.124942
2021-12-19  2.624556e+06  0.000000e+00  676402.630602   98241.655198
2021-12-26  7.368754e+05  0.000000e+00  303580.715522  420643.921625

[208 rows x 4 columns]

 

次に、貢献度の構成比を求めます。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 貢献度構成比の算出
contribution_results = summarize_and_plot_contribution(contribution)
print(contribution_results)
# 貢献度構成比の算出 contribution_results = summarize_and_plot_contribution(contribution) print(contribution_results)
# 貢献度構成比の算出
contribution_results = summarize_and_plot_contribution(contribution)

print(contribution_results)

 

以下、実行結果です。

           contribution     ratio
Base       1.939308e+08  0.436588
TVCM       8.731737e+07  0.196574
Newspaper  7.037395e+07  0.158430
Web        9.257458e+07  0.208409

 

売上貢献度とコストから、マーケティングROIを求めます。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# マーケティングROIの算出
ROI = calculate_marketing_roi(
X[apply_effects_features],
contribution[apply_effects_features]
)
# 数値を表示
print(ROI)
# マーケティングROIの算出 ROI = calculate_marketing_roi( X[apply_effects_features], contribution[apply_effects_features] ) # 数値を表示 print(ROI)
# マーケティングROIの算出
ROI = calculate_marketing_roi(
    X[apply_effects_features], 
    contribution[apply_effects_features]
)

# 数値を表示
print(ROI)

 

以下、実行結果です。

TVCM         0.501653
Newspaper    0.530328
Web          1.954888
dtype: float64

 

売上貢献度(横軸)×マーケティングROI(縦軸)のマップを描きます。円の大きさはコストを表します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 散布図作成(売上貢献度×マーケティングROI)
data_to_plot = plot_scatter_of_contribution_and_roi(
X[apply_effects_features],
contribution[apply_effects_features]
)
# 散布図の数値を表示
print(data_to_plot)
# 散布図作成(売上貢献度×マーケティングROI) data_to_plot = plot_scatter_of_contribution_and_roi( X[apply_effects_features], contribution[apply_effects_features] ) # 散布図の数値を表示 print(data_to_plot)
# 散布図作成(売上貢献度×マーケティングROI)
data_to_plot = plot_scatter_of_contribution_and_roi(
    X[apply_effects_features], 
    contribution[apply_effects_features]
)

# 散布図の数値を表示
print(data_to_plot)

 

以下、実行結果です。

           contribution_percentage       ROI      cost
TVCM                      0.348898  0.501653  58147500
Newspaper                 0.281197  0.530328  45986200
Web                       0.369905  1.954888  31329300

 

最適投資配分

構築したRidge MMMMパイプライン最大化する最適投資配分問題を解き、広告・販促のコストの最適投資配分を求めます。

最後に、現状と最適化後の結果を比較し、どのような変化が起こるかを出力します。

 

 最適化する期間の設定

最適化する期間は、直近1年間(52週間)です。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 最適化期間を表す変数、ここでは直近1年間(52週間)を指定
term = 52
# 最適化期間における対象特徴量Xのデータを抽出
X_actual = X[-term:]
# 最適化期間における目的変数yのデータを抽出
y_actual = y[-term:]
# 最適化期間を表す変数、ここでは直近1年間(52週間)を指定 term = 52 # 最適化期間における対象特徴量Xのデータを抽出 X_actual = X[-term:] # 最適化期間における目的変数yのデータを抽出 y_actual = y[-term:]
# 最適化期間を表す変数、ここでは直近1年間(52週間)を指定
term = 52

# 最適化期間における対象特徴量Xのデータを抽出
X_actual = X[-term:]

# 最適化期間における目的変数yのデータを抽出
y_actual = y[-term:]

 

最適化対象となる特徴量のリストを作ります。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 最適化対象の特徴量のリストを作成
optimized_features = ['TVCM', 'Newspaper', 'Web']
# 最適化対象の特徴量のリストを作成 optimized_features = ['TVCM', 'Newspaper', 'Web']
# 最適化対象の特徴量のリストを作成
optimized_features = ['TVCM', 'Newspaper', 'Web']

 

 最適化の実行

MMMパイプラインの出力である売上を全体コスト一定(現在のコストを合計した値)のもとで最大化する、非線形最適問題を定義し、最適解を求めます。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#
# 最適投資配分
#
opt_results = optimize_investment(
trained_model,
X_actual,
optimized_features,
niter = 100)
# # 最適投資配分 # opt_results = optimize_investment( trained_model, X_actual, optimized_features, niter = 100)
#
# 最適投資配分
#

opt_results = optimize_investment(
    trained_model, 
    X_actual, 
    optimized_features, 
    niter = 100)

 

最適化の計算が終了したら、その最適解を取得します。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#
# 最適解の取得
#
X_optimized = opt_results['X_optimized']
# 最適化後の特徴量を表示
print(X_optimized)
# # 最適解の取得 # X_optimized = opt_results['X_optimized'] # 最適化後の特徴量を表示 print(X_optimized)
#
# 最適解の取得
#

X_optimized = opt_results['X_optimized']

# 最適化後の特徴量を表示
print(X_optimized)

 

以下、実行結果です。

                    TVCM     Newspaper            Web  xmas    t     sin_1  \
Week                                                                         
2021-01-03  2.061352e+05  2.644306e+05  310760.065241     0  156 -0.090067   
2021-01-10  2.820323e+05  1.117502e+05  230997.697922     0  157  0.030059   
2021-01-17  2.949620e+05  2.160800e+05  242852.249510     0  158  0.149750   
2021-01-24  3.444725e+05  1.516047e+05  245585.117776     0  159  0.267279   
2021-01-31  1.351958e+05  1.238194e+05  265601.756952     0  160  0.380947   
2021-02-07  1.343701e+05  2.116608e+05  248820.252306     0  161  0.489113   
2021-02-14  1.107157e+05  1.326450e+05  243661.017964     0  162  0.590215   
2021-02-21  2.390946e+05  1.204536e+05  274648.400151     0  163  0.682792   
2021-02-28  3.677431e+05  1.253575e+05  254981.515806     0  164  0.765507   
2021-03-07  1.968231e+05  1.785299e+05  225062.292553     0  165  0.837166   
2021-03-14  1.750784e+05  3.667548e+04  236888.864095     0  166  0.896734   
2021-03-21  2.848373e+05  2.094940e+05  242672.624940     0  167  0.943350   
2021-03-28  1.183038e+05  1.570854e+05  256401.391217     0  168  0.976341   
2021-04-04  2.465790e+05  1.830803e+05  245122.335734     0  169  0.995231   
2021-04-11  3.441379e+05  1.410329e+05  250977.997496     0  170  0.999746   
2021-04-18  2.160167e+05  2.380970e+05  274478.956773     0  171  0.989821   
2021-04-25  9.369076e+04  5.558819e+04  227047.171968     0  172  0.965601   
2021-05-02  2.576445e+05  2.332610e+05  256482.973451     0  173  0.927434   
2021-05-09  2.763834e+05  2.945121e+04  245927.612613     0  174  0.875872   
2021-05-16  2.750688e+05  2.015922e+05  275373.440519     0  175  0.811659   
2021-05-23  2.728347e+05  2.553521e+05  248861.686229     0  176  0.735724   
2021-05-30  2.784224e+05  1.308278e+05  249622.778472     0  177  0.649162   
2021-06-06  2.546980e+05  1.921193e+05  248196.469036     0  178  0.553225   
2021-06-13  2.297822e+05  3.544338e+04  260218.581156     0  179  0.449297   
2021-06-20  9.609488e+04  2.627430e+05  246502.612645     0  180  0.338879   
2021-06-27  3.356055e+05  1.732794e+05  246117.691204     0  181  0.223568   
2021-07-04  9.761630e+04  1.026479e+05  250492.129535     0  182  0.105027   
2021-07-11  3.944771e+05  1.773457e+05  270486.425030     0  183 -0.015031   
2021-07-18  1.046504e+05  1.469566e+05  260655.360809     0  184 -0.134872   
2021-07-25  1.920452e+05  1.764217e+05  250140.277433     0  185 -0.252764   
2021-08-01  1.129333e+05  1.051399e+05  250111.803128     0  186 -0.367006   
2021-08-08  1.134429e+05  1.729849e+05  244519.653023     0  187 -0.475947   
2021-08-15  3.832926e+05  2.737565e+05  256552.331685     0  188 -0.578014   
2021-08-22  2.467442e+05  1.093583e+05  263957.792264     0  189 -0.671733   
2021-08-29  1.026868e+05  1.371510e+05  258313.865490     0  190 -0.755750   
2021-09-05  3.275985e+05  3.043407e+05  255756.315131     0  191 -0.828851   
2021-09-12  2.076493e+05  2.153542e+05  269322.250659     0  192 -0.889981   
2021-09-19  2.487868e+05  1.538675e+05  247410.733366     0  193 -0.938256   
2021-09-26  1.573702e+05  1.152559e+05  256671.594054     0  194 -0.972981   
2021-10-03  1.274701e+05  2.426804e+05  258019.369393     0  195 -0.993652   
2021-10-10  3.443403e+05  1.301170e+05  251272.048838     0  196 -0.999972   
2021-10-17  2.602334e+05  1.024540e+05  252699.894656     0  197 -0.991849   
2021-10-24  1.848510e+05  1.417980e+05  260098.047352     0  198 -0.969400   
2021-10-31  3.031779e+05  2.406517e+05  244038.726161     0  199 -0.932951   
2021-11-07  9.548359e+04  1.855220e+05  252773.081327     0  200 -0.883026   
2021-11-14  3.483452e+05  2.466583e+05  264883.660641     0  201 -0.820348   
2021-11-21  1.709500e+05  1.139804e+05  270617.597124     0  202 -0.745821   
2021-11-28  4.228808e+05  2.833392e+05  228132.460122     0  203 -0.660522   
2021-12-05  1.072361e+05  1.840752e+05  217757.755691     0  204 -0.565683   
2021-12-12  3.004597e+05  1.681535e+05  301859.071554     0  205 -0.462674   
2021-12-19  9.516851e+03  2.258705e+05  220780.378198     1  206 -0.352983   
2021-12-26  1.018596e-16  3.008496e-09  222917.397170     0  207 -0.238193   

               cos_1     sin_2     cos_2     sin_3     cos_3     sin_4  \
Week                                                                     
2021-01-03  0.995936 -0.179402  0.983776 -0.267279  0.963619 -0.352983   
2021-01-10  0.999548  0.060090  0.998193  0.090067  0.995936  0.119963   
2021-01-17  0.988724  0.296123  0.955150  0.435817  0.900035  0.565683   
2021-01-24  0.963619  0.515110  0.857124  0.725461  0.688264  0.883026   
2021-01-31  0.924597  0.704445  0.709759  0.921708  0.387885  0.999972   
2021-02-07  0.872220  0.853229  0.521537  0.999294  0.037570  0.889981   
2021-02-14  0.807246  0.952897  0.303293  0.948231 -0.317582  0.578014   
2021-02-21  0.730613  0.997713  0.067590  0.775092 -0.631848  0.134872   
2021-02-28  0.643427  0.985096 -0.172003  0.502168 -0.864770 -0.338879   
2021-03-07  0.546948  0.915773 -0.401695  0.164595 -0.986361 -0.735724   
2021-03-14  0.442570  0.793734 -0.608264 -0.194169 -0.980968 -0.965601   
2021-03-21  0.331799  0.626005 -0.779819 -0.527935 -0.849285 -0.976341   
2021-03-28  0.216236  0.422240 -0.906484 -0.793734 -0.608264 -0.765507   
2021-04-04  0.097550  0.194169 -0.980968 -0.957348 -0.288936 -0.380947   
2021-04-11 -0.022545 -0.045079 -0.998983 -0.997713  0.067590  0.090067   
2021-04-18 -0.142315 -0.281733 -0.959493 -0.909632  0.415415  0.540641   
2021-04-25 -0.260029 -0.502168 -0.864770 -0.704445  0.709759  0.868520   
2021-05-02 -0.373987 -0.693697 -0.720267 -0.408567  0.912728  0.999294   
2021-05-09 -0.482544 -0.845293 -0.534303 -0.060090  0.998193  0.903285   
2021-05-16 -0.584131 -0.948231 -0.317582  0.296123  0.955150  0.602282   
2021-05-23 -0.677282 -0.996584 -0.082579  0.614213  0.789141  0.164595   
2021-05-30 -0.760650 -0.987570  0.157177  0.853229  0.521537 -0.310446   
2021-06-06 -0.833032 -0.921708  0.387885  0.982400  0.186791 -0.715033   
2021-06-13 -0.893383 -0.802788  0.596265  0.985096 -0.172003 -0.957348   
2021-06-20 -0.940830 -0.637656  0.770322  0.860971 -0.508653 -0.982400   
2021-06-27 -0.974688 -0.435817  0.900035  0.626005 -0.779819 -0.784502   
2021-07-04 -0.994469 -0.208892  0.977939  0.310446 -0.950591 -0.408567   
2021-07-11 -0.999887  0.030059  0.999548 -0.045079 -0.998983  0.060090   
2021-07-18 -0.990863  0.267279  0.963619 -0.394801 -0.918766  0.515110   
2021-07-25 -0.967528  0.489113  0.872220 -0.693697 -0.720267  0.853229   
2021-08-01 -0.930218  0.682792  0.730613 -0.903285 -0.429041  0.997713   
2021-08-08 -0.879474  0.837166  0.546948 -0.996584 -0.082579  0.915773   
2021-08-15 -0.816027  0.943350  0.331799 -0.961583  0.274513  0.626005   
2021-08-22 -0.740793  0.995231  0.097550 -0.802788  0.596265  0.194169   
2021-08-29 -0.654861  0.989821 -0.142315 -0.540641  0.841254 -0.281733   
2021-09-05 -0.559470  0.927434 -0.373987 -0.208892  0.977939 -0.693697   
2021-09-12 -0.455998  0.811659 -0.584131  0.149750  0.988724 -0.948231   
2021-09-19 -0.345941  0.649162 -0.760650  0.489113  0.872220 -0.987570   
2021-09-26 -0.230887  0.449297 -0.893383  0.765507  0.643427 -0.802788   
2021-10-03 -0.112498  0.223568 -0.974688  0.943350  0.331799 -0.435817   
2021-10-10  0.007516 -0.015031 -0.999887  0.999746 -0.022545  0.030059   
2021-10-17  0.127421 -0.252764 -0.967528  0.927434 -0.373987  0.489113   
2021-10-24  0.245485 -0.475947 -0.879474  0.735724 -0.677282  0.837166   
2021-10-31  0.360005 -0.671733 -0.740793  0.449297 -0.893383  0.995231   
2021-11-07  0.469324 -0.828851 -0.559470  0.105027 -0.994469  0.927434   
2021-11-14  0.571865 -0.938256 -0.345941 -0.252764 -0.967528  0.649162   
2021-11-21  0.666146 -0.993652 -0.112498 -0.578014 -0.816027  0.223568   
2021-11-28  0.750806 -0.991849  0.127421 -0.828851 -0.559470 -0.252764   
2021-12-05  0.824623 -0.932951  0.360005 -0.972981 -0.230887 -0.671733   
2021-12-12  0.886528 -0.820348  0.571865 -0.991849  0.127421 -0.938256   
2021-12-19  0.935630 -0.660522  0.750806 -0.883026  0.469324 -0.991849   
2021-12-26  0.971218 -0.462674  0.886528 -0.660522  0.750806 -0.820348   

               cos_4     sin_5     cos_5  
Week                                      
2021-01-03  0.935630 -0.435817  0.900035  
2021-01-10  0.992778  0.149750  0.988724  
2021-01-17  0.824623  0.682792  0.730613  
2021-01-24  0.469324  0.976341  0.216236  
2021-01-31  0.007516  0.927434 -0.373987  
2021-02-07 -0.455998  0.553225 -0.833032  
2021-02-14 -0.816027 -0.015031 -0.999887  
2021-02-21 -0.990863 -0.578014 -0.816027  
2021-02-28 -0.940830 -0.938256 -0.345941  
2021-03-07 -0.677282 -0.969400  0.245485  
2021-03-14 -0.260029 -0.660522  0.750806  
2021-03-21  0.216236 -0.119963  0.992778  
2021-03-28  0.643427  0.462674  0.886528  
2021-04-04  0.924597  0.883026  0.469324  
2021-04-11  0.995936  0.993652 -0.112498  
2021-04-18  0.841254  0.755750 -0.654861  
2021-04-25  0.495655  0.252764 -0.967528  
2021-05-02  0.037570 -0.338879 -0.940830  
2021-05-09 -0.429041 -0.811659 -0.584131  
2021-05-16 -0.798284 -0.999746 -0.022545  
2021-05-23 -0.986361 -0.837166  0.546948  
2021-05-30 -0.950591 -0.380947  0.924597  
2021-06-06 -0.699090  0.208892  0.977939  
2021-06-13 -0.288936  0.725461  0.688264  
2021-06-20  0.186791  0.987570  0.157177  
2021-06-27  0.620126  0.903285 -0.429041  
2021-07-04  0.912728  0.502168 -0.864770  
2021-07-11  0.998193 -0.075087 -0.997177  
2021-07-18  0.857124 -0.626005 -0.779819  
2021-07-25  0.521537 -0.957348 -0.288936  
2021-08-01  0.067590 -0.952897  0.303293  
2021-08-08 -0.401695 -0.614213  0.789141  
2021-08-15 -0.779819 -0.060090  0.998193  
2021-08-22 -0.980968  0.515110  0.857124  
2021-08-29 -0.959493  0.909632  0.415415  
2021-09-05 -0.720267  0.985096 -0.172003  
2021-09-12 -0.317582  0.715033 -0.699090  
2021-09-19  0.157177  0.194169 -0.980968  
2021-09-26  0.596265 -0.394801 -0.918766  
2021-10-03  0.900035 -0.845293 -0.534303  
2021-10-10  0.999548 -0.999294  0.037570  
2021-10-17  0.872220 -0.802788  0.596265  
2021-10-24  0.546948 -0.324699  0.945817  
2021-10-31  0.097550  0.267279  0.963619  
2021-11-07 -0.373987  0.765507  0.643427  
2021-11-14 -0.760650  0.995231  0.097550  
2021-11-21 -0.974688  0.875872 -0.482544  
2021-11-28 -0.967528  0.449297 -0.893383  
2021-12-05 -0.740793 -0.134872 -0.990863  
2021-12-12 -0.345941 -0.671733 -0.740793  
2021-12-19  0.127421 -0.972981 -0.230887  
2021-12-26  0.571865 -0.932951  0.360005

 

 最適投資配分の最適化結果の出力

現状と最適化の結果を比較していきます。

先ずは、売上金額とマーケティングROIです。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#
# 現状と最適配分時の比較(yとマーケティングROI)
#
result = compare_y_and_marketing_roi(
X_optimized,
X_actual,
y_actual,
trained_model,
apply_effects_features)
# 数値の表示
for key,value in result.items():
print(f"{key}: {value}")
# # 現状と最適配分時の比較(yとマーケティングROI) # result = compare_y_and_marketing_roi( X_optimized, X_actual, y_actual, trained_model, apply_effects_features) # 数値の表示 for key,value in result.items(): print(f"{key}: {value}")
#
# 現状と最適配分時の比較(yとマーケティングROI)
#

result = compare_y_and_marketing_roi(
    X_optimized, 
    X_actual,
    y_actual, 
    trained_model, 
    apply_effects_features)

# 数値の表示
for key,value in result.items():
    print(f"{key}: {value}")

 

以下、実行結果です。

y_actual_sum: 112534400
y_optimized_sum: 122668392
y_change_percent: +9.01 %
roi_actual: 2.39
roi_optimized: 2.70
roi_change_point: +0.31 points

 

次に、投資配分です。

以下、コードです。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#
# 投資配分構成比の比較
#
comparison_df = plot_comparative_allocation(
X_actual,
X_optimized,
apply_effects_features)
# 数値の表示
print(comparison_df)
# # 投資配分構成比の比較 # comparison_df = plot_comparative_allocation( X_actual, X_optimized, apply_effects_features) # 数値の表示 print(comparison_df)
#
# 投資配分構成比の比較
#

comparison_df = plot_comparative_allocation(
    X_actual, 
    X_optimized, 
    apply_effects_features)

# 数値の表示
print(comparison_df)

 

以下、実行結果です。

           Actual Allocation  Optimized Allocation
TVCM                0.397076              0.344385
Newspaper           0.339156              0.259919
Web                 0.263768              0.395696

 

まとめ

今回は、「季節性を考慮したRidge MMMパイプラインを構築し最適投資配分を求める方法」のお話しをしました。

ただ季節性は、広告の効果にも影響を及ぼすことでしょう。たとえば、同じ広告コストを投下しても、時期によって売上を伸ばす力が異なるということです。

幾つかやり方があります。

その1つが、季節変数と広告変数の交互作用をモデルに組み込むという方法です。

次回は、その交互作用項を組み込んだRidge MMMパイプラインを構築し最適投資配分を求める方法のお話しをしました。