🎓 レベル:標準 | 重要度:A(必須)
📎 関連:第5章 顧客選好と選択モデル 目次 | 前提:離散選択モデル(多項ロジット・MNL)
要点(BLUF)
- コンジョイント分析は、製品を「属性の束」と捉え、選択データから各属性水準の部分効用(part-worth)を推定する手法です。選択型コンジョイントは、離散選択モデル(多項ロジット・MNL) の MNL を属性ダミーに当てはめたものに他なりません。「ブランド・容量・価格のどれをどれだけ重視して選んでいるか」を、回答者の選択から逆算します。
- 合成データ(回答者300人 × 各10選択セット × 3プロファイル、真の部分効用 ブランド B/C、容量 大、 円あたり)で MNL を最尤推定すると部分効用を回復し、属性重要度(部分効用レンジの比)はブランド ・容量 ・価格 になります。
- 部分効用から、支払意思額 WTP 部分効用 (容量大 円・ブランドC 円)と、新製品の市場シェア・シミュレーション(製品1 ・製品2 ・製品3 )が得られます。これが価格設定・製品設計・ポートフォリオの意思決定に直結します。
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 の数式
プロファイルの確定効用は、各属性水準の部分効用の和です。本ノートの属性(ブランド/容量/価格)なら
と書けます。 はダミー変数で、基準水準(ブランドA・容量標準)は部分効用 に固定します。係数 が各水準の部分効用、 が価格1円あたりの効用変化です。推定は 離散選択モデル(多項ロジット・MNL) と同じ MNL の最尤法——確定効用をソフトマックスに通し、選ばれたプロファイルの対数尤度を最大化します。
得られた部分効用から、3つの実務指標を作ります。第一に属性重要度。属性ごとに部分効用の**レンジ(最大水準 最小水準)**を取り、全属性のレンジ合計で割ります。
価格は数値属性なので、設計で使った価格幅を使って とします。重要度は「その属性を動かすと効用がどれだけ振れるか」の比です。
第二に支払意思額(WTP, Willingness To Pay)。ある水準の部分効用を、価格係数の絶対値で割ります。
これは「その水準が生む効用を、価格にして何円ぶんか」——たとえば容量を大にして得る満足は何円の値引きと釣り合うか、です。効用スケールが分子分母で打ち消し合うので、 の規模が識別されなくても WTP は円という解釈可能な単位になります(離散選択モデル(多項ロジット・MNL) の「比に意味がある」の具体形)。
第三に市場シェア・シミュレーション。新製品の候補プロファイルを並べ、推定部分効用で各効用 を計算し、ソフトマックスでシェアを予測します。
これで「ブランドCの標準容量700円を出したら、既存ラインの中で何%取れるか」を発売前に見積もれます。
3. 部分効用の推定・WTP・市場シェア(コード)
回答者300人 × 各10選択セット × 3プロファイル=3000タスクの選択型コンジョイントデータを合成します。属性はブランド(A基準/B/C)・容量(標準基準/大)・価格(円)。真の部分効用 ブランド B/C、容量 大、(円あたり)の効用に Gumbel を足し、効用最大を選ばせます。これをダミー+数値で MNL 最尤推定し、部分効用を回復して属性重要度・WTP・市場シェアを計算します。
なお価格は円のままだと係数が と極端に小さく、最適化のスケールが他の部分効用()と桁違いで不安定になります。そこで推定では価格を千円単位にスケールし、最後に円あたりに戻します。勾配は という解釈の良い形(ロジスティック回帰のスコアと同形)で与え、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 (真 )、ブランドC (真 )、容量大 (真 )、価格 (真 )と、選択データだけから各水準の価値を取り戻せています。属性重要度はブランド ・容量 ・価格 ——この設計では価格幅が広い(〜円)ぶん価格が最も効き、ブランドが続きます。WTPは、 の効用スケールが分子分母で打ち消されて円で読めます:容量大は 円、ブランドCは 円ぶんの価値。真の部分効用で計算した理屈値は 円・円で、推定では分子(部分効用)と分母()の両方に推定誤差が乗るため ・ とややぶれますが、桁感は合っています。最後の市場シェア・シミュレーションでは、3つの新製品案の効用をソフトマックスに通し、製品3(ブランドC・標準・700円)が で最有力と予測されました。発売前に「どの構成が何%取れるか」を数字で比較できるのが、コンジョイントの実務的な強みです。
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(容量大 円・ブランドC 円・ブランドB 円)、右は新製品3案の予測市場シェアです。ブランドCの存在感(円ぶんの価値)と、それでも価格次第でシェアが入れ替わる様子——製品2(ブランドB・大・900円)が 、製品3(ブランドC・標準・700円)が ——が一目で読めます。価格を動かしてこの右図を描き直せば、価格弾力性を製品ライン全体のシェアで評価でき、価格最適化(価格最適化(利益最大化))の議論にそのままつながります。
⚠️ よくある誤解
- 部分効用はスケール依存で、絶対値ではなく水準差が意味を持つ:基準水準を に固定して初めて他水準が測れます(ブランドAを にしたから B・C が読める)。基準を変えれば数字も変わるので、部分効用の絶対値を製品間・調査間で直接比べてはいけません。比較するなら基準化された差か、円に直した WTP で。
- 属性重要度は「設計した水準の範囲」に依存する:重要度はレンジ比なので、価格の振れ幅を 〜円ではなく 〜円に広げれば、価格の重要度は機械的に上がります。「価格がいちばん重要」という結論は、調査でどんな幅を見せたかの産物でもあります。重要度を額面どおり受け取らず、提示水準とセットで解釈してください。
- 申告選好は実購買と乖離しうる:コンジョイントは「こう聞かれたらこう選ぶ」という申告選好(stated preference)で、実際の店頭購買(顕示選好)と必ずしも一致しません。仮想の選択では価格に鈍感になりがち(仮想バイアス)など、既知のズレがあります。重要な意思決定では、実験や実販売データ(第7章)で較正するのが筋です。
- 平均部分効用は異質性を隠す:本ノートの部分効用は全回答者共通の単一値で、「ブランドC好きとコスパ重視層」が混在していても平均でならしてしまいます。平均が中庸でも、実際は二極化しているかもしれません。個人ごとの部分効用を確率分布で表す混合ロジット・階層ベイズ(05-03 選好の異質性)で、この隠れた違いを取り戻します——セグメンテーション(第6章)の入口でもあります。
関連ノート
- 離散選択モデル(多項ロジット・MNL)(前提・コンジョイントは MNL を属性ダミーに当てはめたもの)
- 第5章 顧客選好と選択モデル 目次
- 価格最適化(利益最大化)(WTP は価格設定に直結し、市場シェア・シミュレーションは価格最適化の入力になる)
- 実験計画(直交配列・効率的設計)は統計テキストの実験計画、個人差を確率分布で扱う混合ロジット・階層ベイズは 05-03 選好の異質性で扱います
- マーケティング・サイエンス 全体目次