🎓 レベル:発展 | 重要度:A(必須)
📎 関連:確率的購買モデル(BG/NBD) | 前提:残存効果(adstock)と飽和
要点(BLUF)
- 残存効果(adstock)と飽和 で描いた飽和応答曲線を使い、限られた総予算 を複数チャネルへ配分して、総応答を最大化するのが予算最適化です。これは等式制約(予算を使い切る)付きの制約付き最適化で、ラグランジュ法で解けます。
- 最適配分の条件は明快で、各チャネルの限界応答 がすべて等しくなるところ(=ラグランジュ乗数 )。これが等限界原理です。「次の1円をどこに置くと最も効くか」を考え、最も効くチャネルへ動かし続けると、最後はどのチャネルでも次の1円の見返りが同じになる——だから限界が揃います。
- Hill 飽和の3チャネル(TV /Digital /Promo 、総予算 )を
scipy.optimize.minimize(SLSQP)で解くと、最適配分は TV ・Digital ・Promo 、総応答 。これは均等配分()と β比例配分()を上回り、最適点では3チャネルの限界応答がすべて で一致します。配分は平均効率でなく限界で決める——飽和ゆえ、大きいチャネルでも限界は逓減します。⚠️ 応答曲線 の推定誤差や静学性の限界があり、実務の予算最適化は要最新確認。
1. 予算配分問題:飽和があるから「限界」で配る
手元に総予算 があり、TV・Digital・Promo といった複数チャネルへ振り分けるとします。素朴には「均等に配る」か「チャネルの規模(最大効果 )に比例して配る」あたりが思いつきます。しかしどちらも最適ではありません。理由は 残存効果(adstock)と飽和 の**飽和(収穫逓減)**です。
各チャネルの応答は出稿に比例しません。少額のうちは効きが立ち上がり、出すほど頭打ちになる(Hill 曲線)。すると「いま大きく貢献しているチャネル」が、追加の1円でも大きく効くとは限りません。すでに飽和域に入っていれば、その1円の見返り(=限界応答)は小さい。逆に出稿の薄いチャネルは、まだ限界が高い余地を残しています。だから配分は、チャネルの平均的な大きさではなく、**「次の1円の効き=限界応答」**で決めるべきなのです。
直感はこうです。いまの配分から、限界応答がいちばん高いチャネルへ1円動かすと総応答が増える。動かすとそのチャネルは飽和が進んで限界が下がり、減らした側は限界が上がる。これを繰り返すと、やがてどのチャネルでも次の1円の見返りが同じになり、もう動かしても得しない——そこが最適です。つまり最適配分では限界応答が全チャネルで揃う(等限界原理)。
flowchart LR B["総予算 B(固定)"] --> ALLOC["配分 s_1, s_2, ..., s_C"] ALLOC --> F["各チャネルの飽和応答 f_c(s_c)"] F --> SUM["総応答 Σ f_c(s_c) を最大化"] SUM --> RULE["最適:限界応答 f_c'(s_c) が全チャネルで一致"]
これは 価格最適化(利益最大化) で「利益の微分=0」を解いたのと同じ、微分で最適を特徴づける発想の、制約付き版です。次節で数式にします。
2. 数式:制約付き最適化とラグランジュ(等限界原理)
チャネル への配分を 、その飽和応答を とします。総予算 を使い切り、配分は非負、という制約のもとで総応答を最大化します。
等式制約付きの最大化なので、ラグランジュ関数を作ります。乗数 を予算制約に当てます。
内点(すべて )での最適は、各 で偏微分がゼロ。
これが等限界原理です。最適では、すべてのチャネルの限界応答 が共通の値 に等しい。 は「予算をもう1単位増やしたときに総応答がどれだけ伸びるか」という**予算の影の価格(限界価値)**でもあります。 と を連立すれば、最適配分 と が決まります。
飽和は 残存効果(adstock)と飽和 と同じ Hill 関数()を使います。
限界応答は、商の微分でこうなります。
- は上限(最大効果)、 は半飽和点()。 が小さいほど早く飽和します。
- は出稿 とともにいったん増え、変曲点 を境に減少します( の S 字ゆえ)。最適配分は通常この変曲点より上の収穫逓減域に落ち、そこで限界が揃います。
⚠️ 凸性の注意: の Hill は S 字で、原点近くは凸(限界が増える区間)を含むため、問題は厳密には凸最適化ではありません。等限界条件は1階の必要条件で、複数の停留点があり得ます。実務では妥当な初期点(例:均等配分)から解き、必要なら多点スタートで大域性を確かめます。本ノートの数値例では、最適配分が全チャネルとも変曲点より上の凹領域に入り、等限界の解が大域最適になります(コードで検証)。凸最適化・ラグランジュ双対の理論は最適化・統計のテキストへ。
3. SLSQPで最適配分を解く(コード)
3チャネルの Hill 飽和(TV・Digital・Promo)に総予算 を配分します。scipy.optimize.minimize の SLSQP(逐次二次計画法)で、等式制約 と境界 のもと、負の総応答を最小化=総応答を最大化します。均等配分・β比例配分と総応答を比べ、最適が最大であること、そして最適点で各チャネルの限界応答 が一致することを確かめます。数式との対応:目的が 、制約が 、marginal が 。
import numpy as np
from scipy.optimize import minimize
# 3チャネルの Hill 飽和応答 f_c(s) = β_c · s^2/(s^2 + κ_c^2)(04-04 の α=2)
beta = np.array([1000.0, 800.0, 600.0]) # 各チャネルの最大効果(応答の上限)
kappa = np.array([40.0, 25.0, 15.0]) # 半飽和点(応答が上限の半分に達する出稿)
names = ["TV", "Digital", "Promo"]
B = 100.0 # 総予算
def response(s):
s = np.asarray(s, dtype=float)
return beta * s**2 / (s**2 + kappa**2)
def total_response(s):
return response(s).sum()
def marginal(s):
# f_c'(s) = 2 β_c κ_c^2 s / (s^2 + κ_c^2)^2
s = np.asarray(s, dtype=float)
return 2*beta*kappa**2*s / (s**2 + kappa**2)**2
# === 最適化:SLSQP で総応答を最大化(= 負の総応答を最小化)s.t. Σs=B, s>=0 ===
cons = {"type": "eq", "fun": lambda s: s.sum() - B}
bnds = [(0.0, B)] * 3
x0 = np.full(3, B/3) # 均等配分から出発
res = minimize(lambda s: -total_response(s), x0, method="SLSQP",
bounds=bnds, constraints=cons,
options={"ftol": 1e-12, "maxiter": 500})
s_opt = res.x
# === 比較する配分:均等/β比例(チャネル規模に比例)===
s_even = np.full(3, B/3)
s_prop = B * beta / beta.sum()
print("=== 配分方式と総応答の比較(総予算 B=100)===")
print(f"{'配分':<12}{'TV':>9}{'Digital':>10}{'Promo':>9}{'総応答':>12}")
for label, s in [("均等", s_even), ("β比例", s_prop), ("最適(SLSQP)", s_opt)]:
print(f"{label:<12}{s[0]:9.2f}{s[1]:10.2f}{s[2]:9.2f}{total_response(s):12.2f}")
# === 最適点で各チャネルの限界応答が一致(等限界原理=ラグランジュ乗数 λ)===
mg = marginal(s_opt)
print("\n=== 最適点の限界応答 f_c'(s)(全チャネルで一致=ラグランジュ乗数 λ)===")
for nm, s, g in zip(names, s_opt, mg):
print(f"{nm:<8} 配分 s = {s:6.2f} 限界 f'(s) = {g:.4f}")
print(f"限界応答の 最大 - 最小 = {mg.max()-mg.min():.2e} (ほぼ 0 = 等限界が成立)")
出力:
=== 配分方式と総応答の比較(総予算 B=100)===
配分 TV Digital Promo 総応答
均等 33.33 33.33 33.33 1420.80
β比例 41.67 33.33 25.00 1473.58
最適(SLSQP) 44.23 33.08 22.69 1476.80
=== 最適点の限界応答 f_c'(s)(全チャネルで一致=ラグランジュ乗数 λ)===
TV 配分 s = 44.23 限界 f'(s) = 11.1913
Digital 配分 s = 33.08 限界 f'(s) = 11.1913
Promo 配分 s = 22.69 限界 f'(s) = 11.1913
限界応答の 最大 - 最小 = 2.73e-06 (ほぼ 0 = 等限界が成立)
出力の意味:
最適が最大。総応答は、均等 < β比例 < 最適 。均等配分は最も劣り、最適に対し も取りこぼします()。チャネル規模 に比例させる β比例はかなり健闘しますが、それでも最適には 及びません——比例配分は「それらしい」が最適ではない。同じ予算 で、配り方を変えるだけで総応答が動くこと、そして最適が確かに最大であることが数字で確認できます。
等限界の成立。最適配分は TV ・Digital ・Promo 。注目は限界応答で、3チャネルとも にぴたり揃っています(最大−最小が =事実上ゼロ)。これが §2 のラグランジュ条件 の数値的な確認です。最適点では「次の1円の見返り」がどのチャネルでも同じ ——だからもう動かしても得しない。この共通値 が予算の影の価格で、「予算を1単位増やせば総応答が約 伸びる」も意味します。
なぜβ比例とずれるか。β比例(TV ・Promo )に対し、最適は TV をやや増やし()、Promo をやや減らし()ます。Promo は と早く飽和するので、 も出すと限界が下がりきっている。一方 TV は でゆっくり飽和するから、まだ限界が高い。そこで最後の数単位を Promo から TV へ移すと総応答が増える——限界が揃うまで移した結果が最適配分です。「大きいチャネルに比例して配る」が、飽和の速さの違いを無視していることが、このズレに表れています。
各チャネルの応答曲線と最適配分点、配分方式ごとの総応答を図にします。
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from scipy.optimize import minimize
beta = np.array([1000.0, 800.0, 600.0])
kappa = np.array([40.0, 25.0, 15.0])
names = ["TV", "Digital", "Promo"]
B = 100.0
def response(s):
s = np.asarray(s, dtype=float)
return beta * s**2/(s**2+kappa**2)
def total_response(s):
return response(s).sum()
cons = {"type": "eq", "fun": lambda s: s.sum()-B}
res = minimize(lambda s: -total_response(s), np.full(3, B/3), method="SLSQP",
bounds=[(0, B)]*3, constraints=cons,
options={"ftol": 1e-12, "maxiter": 500})
s_opt = res.x
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.3))
# 左:各チャネルの応答曲線と最適配分点(●)
g = np.linspace(0, B, 400)
colors = ["C0", "C1", "C2"]
for j, (nm, c) in enumerate(zip(names, colors)):
fc = beta[j]*g**2/(g**2+kappa[j]**2)
ax1.plot(g, fc, color=c, lw=2, label=f"{nm}(β={beta[j]:.0f}, κ={kappa[j]:.0f})")
sj = s_opt[j]
ax1.plot(sj, beta[j]*sj**2/(sj**2+kappa[j]**2), "o", color=c, ms=10,
markeredgecolor="black")
ax1.set_xlabel("そのチャネルへの配分 s")
ax1.set_ylabel("応答 f(s)")
ax1.set_title("各チャネルの飽和応答曲線と最適配分点(●)")
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3)
# 右:配分方式ごとの総応答
s_even = np.full(3, B/3)
s_prop = B*beta/beta.sum()
labels = ["均等", "β比例", "最適"]
totals = [total_response(s) for s in (s_even, s_prop, s_opt)]
bars = ax2.bar(labels, totals, color=["gray", "C4", "C3"])
for b, v in zip(bars, totals):
ax2.text(b.get_x()+b.get_width()/2, v+6, f"{v:.1f}", ha="center", fontsize=10)
ax2.set_ylabel("総応答 Σ f_c(s_c)")
ax2.set_ylim(0, max(totals)*1.12)
ax2.set_title("総応答:最適配分が最大")
ax2.grid(alpha=0.3, axis="y")
fig.tight_layout()
plt.show()
左図は3本の Hill 応答曲線(S 字)と、最適配分点(黒縁の●)です。各チャネルは飽和の速さが違い(Promo は早く寝て、TV はゆっくり立ち上がる)、最適点では3つの曲線の接線の傾き(=限界)が等しくなっています——図では各●での傾きが揃うイメージです。右図は総応答の棒で、均等 < β比例 < 最適の並び。均等が最も低く、最適がわずかに β比例を上回る——配り方だけで総応答が動くこと、最適が頂点にあることが一目で分かります。
⚠️ よくある誤解
- 配分は平均効率でなく「限界」で決める:「このチャネルは貢献額(平均)が大きいから多く配る」は誤りです。飽和ゆえ、大きいチャネルでもすでに飽和域なら次の1円の見返り(限界)は小さい。最適は限界応答 が全チャネルで揃うところ(本ノートで )。平均 ROI と限界 ROI は別物で、配分を動かすのは限界のほうです。
- 均等配分・比例配分は最適でない:均等は最も劣り(本ノートで 対 最適 )、規模 への比例も惜しいが最適に届きません()。比例配分は飽和の速さ()の違いを無視するため、早く飽和するチャネルへ配りすぎます。ヒューリスティックで満足せず、限界を揃える最適化を解くべきです。
- 応答曲線 の推定誤差が最適配分を動かす:本ノートは を既知として配分を解きましたが、実際はこれらを マーケティングミックスモデリング(MMM) でデータから推定します。 が少しずれれば飽和の速さが変わり、最適配分も動く。最適化の出力は応答曲線の推定に乗るので、点推定の配分を鵜呑みにせず、 の不確実性を振った感度で見るべきです(事前情報を入れるベイズ MMM が有効)。
- 予算最適化は静学である:本ノートの定式化は「同一期間に各チャネルへいくら」という1時点の問題です。残存効果(adstock)と飽和 の **adstock(時間的な繰越)**や、競合の反応、季節性、複数期にまたがる配分(異時点最適化)は入っていません。残存効果まで含めると「いつ・いくら」の動的計画になり、ぐっと難しくなります。
- 総応答の最大化=利益の最大化とは限らない:ここでの目的は「応答(売上やコンバージョン)の総和」です。チャネルごとにコスト構造や利益率が違えば、最大化すべきは応答でなく利益(応答 × 粗利 − コスト)かもしれません。 を粗利ベースに置き換えれば同じ枠組みで解けますが、何を最大化するかは目的しだいです。
⚠️ 要最新確認:実務の予算最適化は、応答曲線の推定(MMM)と一体で、ツールも手法も動きが速い領域です(Google の Meridian など、応答曲線・限界 ROI ベースの最適化を備えた MMM ツールが各社から出ています)。最新の実装・ベストプラクティスは各自で確認してください。本ノートは「飽和応答 → 制約付き最適化 → 等限界原理」という数理の核を、合成データで示すことに徹しています。
関連ノート
- 確率的購買モデル(BG/NBD)(同じ第9章・同バッチ。取引履歴から将来の購買と生存を確率モデルで予測)
- 第9章 発展トピック 目次
- 残存効果(adstock)と飽和(前提。配分する飽和応答曲線 はここで導入した。adstock の繰越は静学最適化では未考慮)
- マーケティングミックスモデリング(MMM)(応答曲線 の推定。最適配分はこの推定に乗る)
- 価格最適化(利益最大化)(最適化の発想。微分=0 で最適を特徴づける。本ノートはその制約付き版)
- 凸最適化・ラグランジュ双対の理論は最適化/統計テキストへ。実務の予算最適化ツールは要最新確認
- マーケティング・サイエンス 全体目次