Mímisbrunnr知恵の泉

← マーケティングサイエンス 一覧

🎓 レベル:標準 | 重要度:A(必須)

📎 関連:第5章 顧客選好と選択モデル 目次 | 前提:離散選択モデル(多項ロジット・MNL)

要点(BLUF)

1. コンジョイント分析とは:製品を属性の束として評価する

新しいパッケージ飲料を設計するとき、「ブランドは?」「容量は?」「価格は?」を別々に聞いても、実際の購買は属性のトレードオフで決まります——「ブランドCは魅力的だが、その分高い」。コンジョイント分析(conjoint = 同時に考慮する)は、複数の属性を組み合わせたプロファイルを丸ごと評価させ、その選択から各属性水準の価値を分解する手法です。

主流の選択型コンジョイント(CBC)では、回答者に「A:ブランドB・大容量・900円」「B:ブランドA・標準・500円」…と並べた選択セットを見せ、いちばん欲しいものを選ばせます。これは 離散選択モデル(多項ロジット・MNL) の選択データそのもので、各プロファイルの確定効用を属性ダミー+価格で表し、MNL を最尤推定すれば、各水準の**部分効用(part-worth)**が手に入ります。出力は3つ——属性重要度・WTP・市場シェアです。

flowchart LR
  P["製品 = 属性の束(ブランド・容量・価格)"] --> C["選択型コンジョイント設計"]
  C --> M["MNL を最尤推定"]
  M --> PW["部分効用 part-worth"]
  PW --> I["属性重要度(レンジ比)"]
  PW --> W["支払意思額 WTP"]
  PW --> S["市場シェア・シミュレーション"]

2. 部分効用・属性重要度・WTP の数式

プロファイルの確定効用は、各属性水準の部分効用の和です。本ノートの属性(ブランド/容量/価格)なら

V=βB1[ブランドB]+βC1[ブランドC]+β1[容量大]+βpricepriceV = \beta_B\,\mathbb{1}[\text{ブランドB}] + \beta_C\,\mathbb{1}[\text{ブランドC}] + \beta_{\text{大}}\,\mathbb{1}[\text{容量大}] + \beta_{\text{price}}\cdot\text{price}

と書けます。1[]\mathbb{1}[\cdot] はダミー変数で、基準水準(ブランドA・容量標準)は部分効用 00 に固定します。係数 βB,βC,β\beta_B,\beta_C,\beta_{\text{大}} が各水準の部分効用、βprice\beta_{\text{price}} が価格1円あたりの効用変化です。推定は 離散選択モデル(多項ロジット・MNL) と同じ MNL の最尤法——確定効用をソフトマックスに通し、選ばれたプロファイルの対数尤度を最大化します。

得られた部分効用から、3つの実務指標を作ります。第一に属性重要度。属性ごとに部分効用の**レンジ(最大水準 - 最小水準)**を取り、全属性のレンジ合計で割ります。

重要度a=RaaRa,Ra=maxβaminβa\text{重要度}_a = \frac{R_a}{\sum_{a'} R_{a'}}, \qquad R_a = \max_{\ell}\beta_{a\ell} - \min_{\ell}\beta_{a\ell}

価格は数値属性なので、設計で使った価格幅を使って Rprice=βprice×(価格max価格min)R_{\text{price}} = |\beta_{\text{price}}|\times(\text{価格}_{\max}-\text{価格}_{\min}) とします。重要度は「その属性を動かすと効用がどれだけ振れるか」の比です。

第二に支払意思額(WTP, Willingness To Pay)。ある水準の部分効用を、価格係数の絶対値で割ります。

WTP()=ββprice\text{WTP}(\ell) = \frac{\beta_{\ell}}{|\beta_{\text{price}}|}

これは「その水準が生む効用を、価格にして何円ぶんか」——たとえば容量を大にして得る満足は何円の値引きと釣り合うか、です。効用スケールが分子分母で打ち消し合うので、β\beta の規模が識別されなくても WTP は円という解釈可能な単位になります(離散選択モデル(多項ロジット・MNL) の「比に意味がある」の具体形)。

第三に市場シェア・シミュレーション。新製品の候補プロファイルを並べ、推定部分効用で各効用 VpV_p を計算し、ソフトマックスでシェアを予測します。

シェアp=eVpqeVq\text{シェア}_p = \frac{e^{V_p}}{\sum_{q} e^{V_q}}

これで「ブランドCの標準容量700円を出したら、既存ラインの中で何%取れるか」を発売前に見積もれます。

3. 部分効用の推定・WTP・市場シェア(コード)

回答者300人 × 各10選択セット × 3プロファイル=3000タスクの選択型コンジョイントデータを合成します。属性はブランド(A基準/B/C)・容量(標準基準/大)・価格(円)。真の部分効用 ブランド B=0.5=0.5/C=1.0=1.0、容量 大=0.8=0.8βprice=0.0015\beta_{\text{price}}=-0.0015(円あたり)の効用に Gumbel を足し、効用最大を選ばせます。これをダミー+数値で MNL 最尤推定し、部分効用を回復して属性重要度・WTP・市場シェアを計算します。

なお価格は円のままだと係数が 0.0015-0.0015 と極端に小さく、最適化のスケールが他の部分効用(1\sim 1)と桁違いで不安定になります。そこで推定では価格を千円単位にスケールし、最後に円あたりに戻します。勾配は (予測確率実選択)×属性\sum(\text{予測確率}-\text{実選択})\times\text{属性} という解釈の良い形(ロジスティック回帰のスコアと同形)で与え、BFGS に渡します。

import numpy as np
import pandas as pd
from scipy.optimize import minimize

# 選択型コンジョイント。300人×10セット×3プロファイル=3000タスク。属性はブランド
# (A基準/B/C)、容量(標準基準/大)、価格(円)。真の部分効用に Gumbel を足し効用最大を選ばせる。
rng = np.random.default_rng(0)
n_resp, n_set, n_alt = 300, 10, 3
n_task = n_resp * n_set
brand = rng.integers(0, 3, (n_task, n_alt))        # 0=A, 1=B, 2=C
cap = rng.integers(0, 2, (n_task, n_alt))          # 0=標準, 1=大
price_levels = np.array([300, 500, 700, 900, 1100])
price = rng.choice(price_levels, (n_task, n_alt)).astype(float)

brand_B = (brand == 1).astype(float)               # ダミー化
brand_C = (brand == 2).astype(float)
cap_L = (cap == 1).astype(float)

pw_true = np.array([0.5, 1.0, 0.8, -0.0015])       # [B, C, 大, 円あたり]
V = pw_true[0]*brand_B + pw_true[1]*brand_C + pw_true[2]*cap_L + pw_true[3]*price
U = V + rng.gumbel(0, 1, (n_task, n_alt))
choice = U.argmax(axis=1)

# 推定の数値安定のため価格を千円単位にスケール(円のままだと係数が極端に小さく不安定)
price_k = price / 1000.0
Xd = np.stack([brand_B, brand_C, cap_L, price_k], axis=2)   # (n_task, n_alt, 4)
idx = np.arange(n_task)

# 負の対数尤度と勾配を同時に返す。勾配は Σ(予測確率 − 実選択)×属性(ロジスティック回帰のスコアと同形)。
def nll_and_grad(theta):
    v = Xd @ theta
    v = v - v.max(axis=1, keepdims=True)
    P = np.exp(v) / np.exp(v).sum(axis=1, keepdims=True)
    Y = np.zeros_like(P); Y[idx, choice] = 1.0
    nll = -np.sum(np.log(P[idx, choice]))
    grad = ((P - Y)[:, :, None] * Xd).sum(axis=(0, 1))
    return nll, grad

res = minimize(nll_and_grad, x0=np.zeros(4), method="BFGS", jac=True)
pw = res.x.copy()
pw[3] = res.x[3] / 1000.0          # 千円あたり → 円あたりに戻す

names = ["ブランドB", "ブランドC", "容量大", "価格(円あたり)"]
tbl = pd.DataFrame({"真の部分効用": pw_true, "推定部分効用": pw}, index=names)
print("=== コンジョイント:MNLによる部分効用の回復 ===")
print(tbl.to_string(formatters={"真の部分効用": "{:.4f}".format, "推定部分効用": "{:.4f}".format}))
print(f"収束: {res.success} 負の対数尤度: {res.fun:.1f}")

# 属性重要度=その属性の部分効用レンジ / 全属性レンジ合計
b_price = abs(pw[3])
rng_brand = max(0.0, pw[0], pw[1]) - min(0.0, pw[0], pw[1])      # A=0 を含むレンジ
rng_cap = abs(pw[2])
rng_price = b_price * (price_levels.max() - price_levels.min())   # 価格レンジ×|β|
total = rng_brand + rng_cap + rng_price
imp = pd.DataFrame({"レンジ": [rng_brand, rng_cap, rng_price],
                    "重要度": [rng_brand/total, rng_cap/total, rng_price/total]},
                   index=["ブランド", "容量", "価格"])
print("\n=== 属性重要度(部分効用レンジの比)===")
print(imp.to_string(formatters={"レンジ": "{:.3f}".format, "重要度": "{:.1%}".format}))

# 支払意思額 WTP=部分効用 / |β_price|(円)
wtp = pd.DataFrame({"WTP(円)": [pw[2]/b_price, pw[1]/b_price, pw[0]/b_price]},
                   index=["容量大", "ブランドC", "ブランドB"])
print("\n=== 支払意思額 WTP(その水準にいくら余計に払うか)===")
print(wtp.to_string(formatters={"WTP(円)": "{:.0f}".format}))

# 市場シェア・シミュレーション:新製品3案の効用を推定部分効用で計算しソフトマックス
prof = pd.DataFrame({"ブランド": ["A", "B", "C"], "容量": ["標準", "大", "標準"],
                     "価格": [500, 900, 700]}, index=["製品1", "製品2", "製品3"])
bB = np.array([0, 1, 0], float); bC = np.array([0, 0, 1], float)
cL = np.array([0, 1, 0], float); pr = np.array([500, 900, 700], float)
Vp = pw[0]*bB + pw[1]*bC + pw[2]*cL + pw[3]*pr
prof["効用"] = Vp
prof["市場シェア"] = np.exp(Vp) / np.exp(Vp).sum()
print("\n=== 市場シェア・シミュレーション(新製品3案)===")
print(prof.to_string(formatters={"効用": "{:.3f}".format, "市場シェア": "{:.1%}".format}))

出力:

=== コンジョイント:MNLによる部分効用の回復 ===
          真の部分効用  推定部分効用
ブランドB     0.5000  0.4650
ブランドC     1.0000  1.0946
容量大       0.8000  0.7168
価格(円あたり) -0.0015 -0.0016
収束: True 負の対数尤度: 2867.2

=== 属性重要度(部分効用レンジの比)===
       レンジ   重要度
ブランド 1.095 35.6%
容量   0.717 23.3%
価格   1.264 41.1%

=== 支払意思額 WTP(その水準にいくら余計に払うか)===
      WTP(円)
容量大      454
ブランドC    693
ブランドB    294

=== 市場シェア・シミュレーション(新製品3案)===
    ブランド  容量   価格     効用 市場シェア
製品1    A  標準  500 -0.790 20.4%
製品2    B   大  900 -0.241 35.3%
製品3    C  標準  700 -0.012 44.4%

出力の意味:まず部分効用の回復。ブランドB 0.4650.465(真 0.50.5)、ブランドC 1.0951.095(真 1.01.0)、容量大 0.7170.717(真 0.80.8)、価格 0.0016-0.0016(真 0.0015-0.0015)と、選択データだけから各水準の価値を取り戻せています。属性重要度はブランド 35.6%35.6\%・容量 23.3%23.3\%・価格 41.1%41.1\%——この設計では価格幅が広い(30030011001100円)ぶん価格が最も効き、ブランドが続きます。WTPは、β\beta の効用スケールが分子分母で打ち消されてで読めます:容量大は 454454円、ブランドCは 693693円ぶんの価値。真の部分効用で計算した理屈値は 0.8/0.00155330.8/0.0015\approx533円・1.0/0.00156671.0/0.0015\approx667円で、推定では分子(部分効用)と分母(βprice|\beta_{\text{price}}|)の両方に推定誤差が乗るため 454454693693 とややぶれますが、桁感は合っています。最後の市場シェア・シミュレーションでは、3つの新製品案の効用をソフトマックスに通し、製品3(ブランドC・標準・700円)が 44.4%44.4\% で最有力と予測されました。発売前に「どの構成が何%取れるか」を数字で比較できるのが、コンジョイントの実務的な強みです。

WTP と市場シェアは意思決定に直結する出力なので、1枚にまとめておきます(このブロックはデータ生成と推定を内部で再実行し、上の表と同じ値を描きます)。

import matplotlib
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from scipy.optimize import minimize

# データ生成とMLEをこのブロック内で完結させ、推定部分効用からWTPと市場シェアを描く。
rng = np.random.default_rng(0)
n_resp, n_set, n_alt = 300, 10, 3
n_task = n_resp * n_set
brand = rng.integers(0, 3, (n_task, n_alt))
cap = rng.integers(0, 2, (n_task, n_alt))
price_levels = np.array([300, 500, 700, 900, 1100])
price = rng.choice(price_levels, (n_task, n_alt)).astype(float)
brand_B = (brand == 1).astype(float); brand_C = (brand == 2).astype(float); cap_L = (cap == 1).astype(float)
pw_true = np.array([0.5, 1.0, 0.8, -0.0015])
V = pw_true[0]*brand_B + pw_true[1]*brand_C + pw_true[2]*cap_L + pw_true[3]*price
choice = (V + rng.gumbel(0, 1, (n_task, n_alt))).argmax(axis=1)

Xd = np.stack([brand_B, brand_C, cap_L, price/1000.0], axis=2)
idx = np.arange(n_task)
def nll_and_grad(theta):
    v = Xd @ theta; v = v - v.max(axis=1, keepdims=True)
    P = np.exp(v) / np.exp(v).sum(axis=1, keepdims=True)
    Y = np.zeros_like(P); Y[idx, choice] = 1.0
    return -np.sum(np.log(P[idx, choice])), ((P - Y)[:, :, None] * Xd).sum(axis=(0, 1))
res = minimize(nll_and_grad, np.zeros(4), method="BFGS", jac=True)
pw = res.x.copy(); pw[3] = res.x[3] / 1000.0
b_price = abs(pw[3])

fig, ax = plt.subplots(1, 2, figsize=(11, 4.3))
wtp_labels = ["ブランドB", "ブランドC", "容量大"]
wtp_vals = [pw[0]/b_price, pw[1]/b_price, pw[2]/b_price]
ax[0].bar(wtp_labels, wtp_vals, color=["C0", "C0", "C1"])
for i, v in enumerate(wtp_vals):
    ax[0].text(i, v + 8, f"{v:.0f}円", ha="center", fontsize=10)
ax[0].set_ylabel("支払意思額 WTP(円)")
ax[0].set_title("各水準のWTP=部分効用 / |β_price|")
ax[0].set_ylim(0, 800)

prof_labels = ["製品1\nA/標準/500円", "製品2\nB/大/900円", "製品3\nC/標準/700円"]
bB = np.array([0, 1, 0], float); bC = np.array([0, 0, 1], float)
cL = np.array([0, 1, 0], float); pr = np.array([500, 900, 700], float)
Vp = pw[0]*bB + pw[1]*bC + pw[2]*cL + pw[3]*pr
share = np.exp(Vp) / np.exp(Vp).sum()
ax[1].bar(prof_labels, share, color=["C7", "C2", "C3"])
for i, v in enumerate(share):
    ax[1].text(i, v + 0.01, f"{v:.1%}", ha="center", fontsize=10)
ax[1].set_ylabel("予測市場シェア")
ax[1].set_title("新製品3案の市場シェア・シミュレーション")
ax[1].set_ylim(0, 0.55)
fig.tight_layout()
plt.show()

左の棒は WTP(容量大 454454円・ブランドC 693693円・ブランドB 294294円)、右は新製品3案の予測市場シェアです。ブランドCの存在感(693693円ぶんの価値)と、それでも価格次第でシェアが入れ替わる様子——製品2(ブランドB・大・900円)が 35.3%35.3\%、製品3(ブランドC・標準・700円)が 44.4%44.4\%——が一目で読めます。価格を動かしてこの右図を描き直せば、価格弾力性を製品ライン全体のシェアで評価でき、価格最適化(価格最適化(利益最大化))の議論にそのままつながります。

⚠️ よくある誤解

関連ノート