🎓 レベル:標準 | 重要度:A(必須)
📎 関連:アトリビューション分析 | 前提:マーケティングミックスモデリング(MMM)
要点(BLUF)
- 広告効果には2つの非線形性があります。残存効果(adstock)=時間的な繰越——今週の出稿は今週だけでなく翌週以降にも効きが残る。飽和(saturation)=量的な頭打ち——出稿を増やすほど追加1単位の効きが鈍る(収穫逓減)。マーケティングミックスモデリング(MMM) の素の線形は、このどちらも無視していました。本ノートはこの2つを非線形変換として線形 MMM に組み込み、現実的にします。
- 幾何 adstock は (=繰越率)。 で、週2にだけ 出した単発パルス
x=[0,0,100,0,0,0,0,0]は、adstock 系列a=[0,0,100,50,25,12.5,6.25,3.125]という指数減衰の尾を引きます。出稿した週で効果が終わらず、後の週へ繰り越されるわけです。 - 飽和は Hill 関数 ()。、 で、出稿を2倍()にしても応答は 倍(2倍未満)——収穫逓減です。実際に効果として使うのは合成 。これらを無視した線形 MMM は高出稿域を過大に外挿してしまい、予算配分は平均でなく**限界(追加1単位)**で考えるべきだ、という マーケティングミックスモデリング(MMM) の注意の数理的な裏付けになります。
1. 残存効果(adstock)と飽和:広告効果の時間と量の非線形性
マーケティングミックスモデリング(MMM) の線形 MMM は、売上を各チャネルの出稿額に比例させていました(係数 倍)。これは2つの意味で単純すぎます。
第一に、広告は打った瞬間に効いて、その週で消えるわけではありません。今週見た CM は、来週・再来週の購買にも影を落とします。この時間的な繰越を**残存効果(adstock/carryover)**と呼びます。テレビやブランド広告は尾が長く(ゆっくり減衰)、検索広告は尾が短い(すぐ減衰)、というように繰越の強さはチャネルごとに違います。
第二に、出稿は増やすほど効きが鈍ります。視聴者の上限・広告疲れ・刈り取り尽くしで、 倍出しても売上は 倍にならない。この量的な頭打ちが飽和(saturation)=収穫逓減です。線形のまま「もっと出せば比例で伸びる」と外挿すると、過大な期待で予算を盛りすぎます。
flowchart LR X["出稿 x_t(毎週の額)"] --> AD["adstock:時間で繰り越す(尾を引く)"] AD --> SAT["飽和:量で頭打ち(収穫逓減)"] SAT --> EFF["広告の効果(売上への寄与)"] EFF --> MMM["線形MMMの回帰に入れる(04-02の拡張)"]
ポイントは、この2つは別方向の非線形性だということです。adstock は時間方向(いつ効くか)、飽和は量方向(どれだけ出すと頭打ちか)。順番に通すパイプライン——まず出稿を時間で繰り越し(adstock)、その繰越済みの量を飽和に通す——として MMM に入れます。
2. 数式:幾何 adstock と Hill 飽和、その合成
幾何 adstock は、今週の出稿に「先週までの繰越」を一定割合 で足します。
が繰越率で、 なら繰越なし(、線形 MMM そのもの)、 が に近いほど尾が長く残ります。この漸化式を展開すると、過去の出稿に幾何級数の重みがかかった形になります。
つまり 週前の出稿は 倍だけ今週に効く—— なら と半減していく尾です。単発で を1回だけ出すと、その後の週に と繰り越されるのはこのためです。
飽和は Hill 関数で表します。
- (カッパ)は半飽和点:。応答が上限の半分に達する出稿量です。
- (アルファ)は形状(急峻さ)。 だと S 字——出稿が小さいうちは立ち上がりが鈍く(変曲点 まで増加的)、中盤で急、高出稿で寝る、の3局面になります。 なら原点から純粋に逓減する凹型です。応答は必ず から の間に収まり、いくら出しても を超えない——この上限が「頭打ち」を表現します。(飽和は指数型 で表すこともあります。)
実際に MMM で使う広告効果は、この2つを順に通した合成です。出稿を時間で繰り越してから、その量を飽和に通します。
マーケティングミックスモデリング(MMM) の線形 を、この に置き換えるのが、現実的な MMM への拡張です。係数 に加え、繰越率 と飽和の もデータから推定するパラメータが増えます(推定の方法論は統計・ベイズの領域)。
3. adstock・飽和・合成を実装する(コード)
3つを順に確かめます。(a) 幾何 adstock を単発パルスに適用して尾を見る、(b) Hill 飽和で出稿を増やしたときの限界応答が寝ることを表で見る、(c) 合成 を計算する、の順です。
import numpy as np
# (a) 幾何 adstock:a_t = x_t + λ a_{t-1}。λ=0.5、週2に100の単発出稿(パルス)。
def adstock(x, lam):
a = np.zeros_like(x, dtype=float)
prev = 0.0
for t in range(len(x)):
a[t] = x[t] + lam * prev
prev = a[t]
return a
x = np.array([0, 0, 100, 0, 0, 0, 0, 0], dtype=float)
a = adstock(x, lam=0.5)
print("=== (a) adstock:単発出稿の効果が指数減衰の尾を引く(λ=0.5)===")
print("出稿 x =", x.tolist())
print("adstock a =", a.tolist())
print("→ 週2に出した100が、翌週50・25・12.5…と繰り越して効き続ける")
# (b) Hill 飽和:f(x) = x^α / (x^α + κ^α)。α=2, κ=50。
def hill(x, alpha, kappa):
x = np.asarray(x, dtype=float)
return x**alpha / (x**alpha + kappa**alpha)
alpha, kappa = 2.0, 50.0
spend = np.array([0, 25, 50, 100, 200], dtype=float)
resp = hill(spend, alpha, kappa)
dresp = np.diff(resp, prepend=0.0) # 限界(前ステップとの差)
dspend = np.diff(spend, prepend=0.0)
per_unit = np.divide(dresp, dspend, out=np.zeros_like(dresp), where=dspend > 0)
print("\n=== (b) Hill 飽和:出稿を増やすほど限界が寝る(α=2, κ=50)===")
print(f"{'出稿':>6}{'飽和応答f':>12}{'限界Δf':>12}{'1単位あたり':>14}")
for s, r, d, pu in zip(spend, resp, dresp, per_unit):
print(f"{s:6.0f}{r:12.4f}{d:12.4f}{pu:14.5f}")
print(f"変曲点 x = κ√((α-1)/(α+1)) = {kappa*np.sqrt((alpha-1)/(alpha+1)):.2f}")
print(f"f(50)={hill(50,alpha,kappa):.2f}, f(100)={hill(100,alpha,kappa):.2f}"
f" → 出稿を2倍(50→100)にしても応答は {hill(100,alpha,kappa)/hill(50,alpha,kappa):.2f} 倍(2倍未満)")
# (c) 合成効果:飽和(adstock(x))。adstock の尾の各点をさらに Hill で飽和させる。
combined = hill(a, alpha, kappa)
print("\n=== (c) 合成効果 f(adstock(x)):繰越(時間)と飽和(量)を重ねる ===")
print("adstock a =", np.round(a, 3).tolist())
print("合成 f(a) =", np.round(combined, 4).tolist())
print("→ 出稿が当たる週2で応答0.8、以降は繰越の尾が飽和でさらに縮みながら減衰")
出力:
=== (a) adstock:単発出稿の効果が指数減衰の尾を引く(λ=0.5)===
出稿 x = [0.0, 0.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0]
adstock a = [0.0, 0.0, 100.0, 50.0, 25.0, 12.5, 6.25, 3.125]
→ 週2に出した100が、翌週50・25・12.5…と繰り越して効き続ける
=== (b) Hill 飽和:出稿を増やすほど限界が寝る(α=2, κ=50)===
出稿 飽和応答f 限界Δf 1単位あたり
0 0.0000 0.0000 0.00000
25 0.2000 0.2000 0.00800
50 0.5000 0.3000 0.01200
100 0.8000 0.3000 0.00600
200 0.9412 0.1412 0.00141
変曲点 x = κ√((α-1)/(α+1)) = 28.87
f(50)=0.50, f(100)=0.80 → 出稿を2倍(50→100)にしても応答は 1.60 倍(2倍未満)
=== (c) 合成効果 f(adstock(x)):繰越(時間)と飽和(量)を重ねる ===
adstock a = [0.0, 0.0, 100.0, 50.0, 25.0, 12.5, 6.25, 3.125]
合成 f(a) = [0.0, 0.0, 0.8, 0.5, 0.2, 0.0588, 0.0154, 0.0039]
→ 出稿が当たる週2で応答0.8、以降は繰越の尾が飽和でさらに縮みながら減衰
出力の意味:(a) adstock は数式どおりの尾を作ります。週2に出した が、 で翌週 、その次 、、、 と半減しながら繰り越されます——出稿はその週で終わらない。テレビ広告の「あとからじわじわ効く」を、たった1行 で表現できているわけです。
(b) 飽和は限界(追加1単位の効き)が寝ていくことを示します。注目は右端の「1単位あたり」列です。 の Hill は S 字なので、出稿が小さい立ち上がり区間(変曲点 より下)では限界がむしろ増え( の より の が大きい)、変曲点を越えると収穫逓減に入ります。実務で効くのはこの逓減で、 と進むほど1単位あたりの効きは と急速に寝ます。集約すると ——出稿を2倍にしても応答は 倍にしかなりません。線形ならここで 倍を期待してしまう。この差が、高出稿域での過大外挿の正体です。
(c) 合成 は2つを重ねます。adstock の尾 [0,0,100,50,25,12.5,6.25,3.125] を Hill に通すと、効果は [0,0,0.8,0.5,0.2,0.0588,0.0154,0.0039]。出稿が当たる週2で応答 とピークを打ち、以降は繰越の尾が、飽和でさらに縮みながら減衰します。時間方向(adstock)と量方向(飽和)の非線形性が1本の効果系列に同居しているのが見て取れます。
左に adstock の減衰、右に飽和カーブを並べて、2つの非線形性を図にします。
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
def adstock(x, lam):
a = np.zeros_like(x, dtype=float); prev = 0.0
for t in range(len(x)):
a[t] = x[t] + lam * prev; prev = a[t]
return a
def hill(x, alpha, kappa):
x = np.asarray(x, dtype=float)
return x**alpha / (x**alpha + kappa**alpha)
x = np.array([0, 0, 100, 0, 0, 0, 0, 0], dtype=float)
a = adstock(x, 0.5)
alpha, kappa = 2.0, 50.0
weeks = np.arange(len(x))
fig, ax = plt.subplots(1, 2, figsize=(11, 4.3))
# 左:adstock の減衰(出稿パルスと繰越の尾)
ax[0].bar(weeks, x, width=0.5, color="#cccccc", label="出稿 x(単発パルス)")
ax[0].plot(weeks, a, color="C3", marker="o", lw=1.8, label="adstock a(繰越)")
for t in range(2, len(x)):
ax[0].annotate(f"{a[t]:g}", (weeks[t], a[t]), textcoords="offset points",
xytext=(0, 6), ha="center", fontsize=8, color="C3")
ax[0].set_xlabel("週"); ax[0].set_ylabel("出稿・adstock")
ax[0].set_title("adstock:単発出稿が繰り越して減衰(λ=0.5)")
ax[0].legend(loc="upper right", fontsize=9)
# 右:Hill 飽和カーブ(限界が寝る)
g = np.linspace(0, 250, 400)
ax[1].plot(g, hill(g, alpha, kappa), color="C0", lw=2, label="飽和応答 f(Hill)")
pts = np.array([0, 25, 50, 100, 200], dtype=float)
ax[1].plot(pts, hill(pts, alpha, kappa), "o", color="C1", ms=7, label="出稿点")
ax[1].axvline(kappa, color="gray", ls=":", lw=1)
ax[1].text(kappa + 3, 0.06, "κ=50で f=0.5", color="gray", fontsize=9)
ax[1].annotate("ここで限界が寝る\n(追加1単位の効きが逓減)",
xy=(200, hill(200, alpha, kappa)), xytext=(120, 0.55),
arrowprops=dict(arrowstyle="->", color="C3"), fontsize=9, color="C3")
ax[1].set_xlabel("出稿(量)"); ax[1].set_ylabel("飽和応答 f(0〜1)")
ax[1].set_title("飽和(Hill, α=2):収穫逓減で頭打ち")
ax[1].legend(loc="lower right", fontsize=9)
fig.tight_layout()
plt.show()
左の図は、灰色の単発パルス(週2の )に対し、赤い線が と尾を引く——adstock が時間方向に効果を広げるさまです。右の図は Hill 飽和カーブで、出稿点(オレンジ)が右に行くほど曲線が寝て、追加出稿の見返り(傾き=限界)が小さくなるのが分かります。 で応答がちょうど半分()に達する点も目印になります。
⚠️ よくある誤解
- adstock・飽和を無視した線形 MMM は高出稿域を過大に外挿する:マーケティングミックスモデリング(MMM) の素の線形は「出稿1単位あたり 」が一定だと仮定します。しかし飽和があるなら、高出稿域では限界が寝る(本ノートで の1単位あたりが )。線形のまま予算を増やすと、実際には得られない比例の伸びを期待して盛りすぎます。04-02 で触れた「線形は過大外挿しうる」という注意の、これが数理的な裏付けです。
- はデータから推定すべきで、決め打ちではない:本ノートは説明のため と固定しましたが、繰越率も飽和の形もチャネルごとに違い、本来はデータに当てて推定します(テレビは 大・検索は小、など)。係数 に加えてこれらの非線形パラメータが増えるため、推定は素の線形回帰より難しく、事前情報を入れるベイズ MMM がよく使われます(推定論は統計・ベイズの教材へ)。
- 飽和があるので予算配分は「限界」で考える:チャネルの貢献額(平均)が大きくても、すでに飽和域に入っていれば追加1単位の見返りは小さいかもしれません。逆に出稿の薄いチャネルは限界がまだ高い余地があります。最適配分は平均貢献ではなく各チャネルの限界応答が揃うところ——つまり「次の1円をどこに置くと最も効くか」で決めます。予算最適化の定式化は第9章 発展トピックで扱います。
- 繰越(adstock)と飽和は別物——混同しない:adstock は時間方向(今週の出稿が後の週にも効く)、飽和は量方向(同じ週に出しすぎると頭打ち)。片方だけ入れても不十分で、両方を順に通して初めて現実に近づきます。なお量方向の収穫逓減という意味では、飽和は 需要曲線と価格弾力性 の「価格を下げても需要の伸びが鈍る」逓減と同じ発想——反応が比例しないという市場反応の一般的な性質です。
関連ノート
- マーケティングミックスモデリング(MMM)(前提・この非線形変換を入れる先。素の線形を adstock+飽和で現実化する)
- アトリビューション分析(同じ第4章。個人経路への貢献配分)
- 第4章 市場反応モデル 目次
- 需要曲線と価格弾力性(飽和も「反応が比例しない」収穫逓減という点で価格弾力性と通じる)
- 繰越率・飽和パラメータの推定論は統計テキストの回帰/事前情報を入れるベイズ MMM はベイズ統計テキストの階層モデルへ/飽和を踏まえた予算最適化は第9章 発展トピックへ
- マーケティング・サイエンス 全体目次