Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

マーケットシミュレーション

コンジョイント分析(選択型コンジョイント分析(CBC)と多項ロジットモデル階層ベイズモデルによる個人レベル部分効用の推定)で推定した部分効用モデルを使うと、実際にはまだ市場に存在しない製品ラインナップ(自社の新製品案+競合製品)を仮想的に設定し、それぞれがどの程度のシェアを獲得しそうかを事前にシミュレーションできる。これをマーケットシミュレーション(market simulation)、あるいは**シェア・オブ・プリファレンス分析(share of preference analysis)**と呼ぶ。

シミュレーションの手順

  1. シミュレーションしたい競合シナリオ(市場に並ぶ製品案の集合、S={1,,J}S = \{1, \dots, J\})を定義する

  2. 各回答者nnの部分効用β^n\hat{\boldsymbol{\beta}}_n(階層ベイズなどで個人レベル推定したもの)を使って、シナリオ内の各製品の効用V^nj=xjβ^n\hat{V}_{nj} = \mathbf{x}_j^\top \hat{\boldsymbol{\beta}}_nを計算する

  3. 何らかのシミュレーションルールに従って、回答者ごとの選択確率(またはシェア)を求める

  4. 全回答者について平均をとり、製品ごとの推定市場シェアとする

share^j=1Nn=1NP^n(jS)\widehat{\text{share}}_j = \frac{1}{N} \sum_{n=1}^{N} \hat{P}_n(j \mid S)

シミュレーションルール

(1) ファーストチョイス・ルール(first choice rule):各回答者は効用が最大の製品を確率1で選ぶとみなす。

P^n(jS)=1[j=argmaxkSV^nk]\hat{P}_n(j \mid S) = \mathbb{1}\left[j = \arg\max_{k \in S} \hat{V}_{nk}\right]

シンプルで直感的だが、僅差の効用でも「勝者総取り」になるため、シェアの変化が滑らかでない(急激に0か1になる)。

(2) シェア・オブ・プリファレンス・ルール(share of preference rule / logit rule):MNLの選択確率をそのまま各回答者の「シェア」とみなす。

P^n(jS)=exp(V^nj)kSexp(V^nk)\hat{P}_n(j \mid S) = \frac{\exp(\hat{V}_{nj})}{\sum_{k \in S} \exp(\hat{V}_{nk})}

効用の差が僅かでも滑らかにシェアが変化するが、選択型コンジョイント分析(CBC)と多項ロジットモデルで触れたIIA仮定の影響を受けやすい(似た製品同士が過度にシェアを奪い合う)。

(3) ランダマイズド・ファーストチョイス(Randomized First Choice, RFC):Huber, Orme, Miller(1999)が提案した方法で、効用に追加のランダム誤差項(属性重みの分散+標準ガンベル誤差など)を加えたうえでファーストチョイス・ルールを適用し、これを多数回モンテカルロ・シミュレーションして平均をとる。ファーストチョイスの急峻さとロジットルールのIIA問題の中間的な性質を持ち、実務(Sawtooth Softwareなど)で広く使われている。

実装例

5人分の製品案(自社の新製品Xを含む)からなる競合シナリオについて、100人分の個人レベル部分効用(階層ベイズ推定を模して分散を持たせて生成)から、3つのルールでシェアを比較する。

import numpy as np
import pandas as pd

rng = np.random.default_rng(0)

feature_names = ["価格::1,000円", "価格::1,500円", "容量::500ml", "ブランド::A社", "ブランド::B社"]
gamma_true = np.array([1.2, 0.5, -0.3, 0.6, 0.1])   # 母集団平均の部分効用
sigma_true = np.array([0.5, 0.3, 0.2, 0.4, 0.2])    # 個人差

N_RESP = 100
beta_n = gamma_true + rng.normal(0, sigma_true, size=(N_RESP, len(feature_names)))

# シミュレーションしたい競合シナリオ(製品ごとの属性ダミー)
scenario = pd.DataFrame({
    "product":         ["自社新製品X", "競合A", "競合B", "競合C", "競合D"],
    "価格::1,000円":     [1, 0, 0, 1, 0],
    "価格::1,500円":     [0, 1, 0, 0, 1],
    "容量::500ml":       [1, 1, 0, 1, 0],
    "ブランド::A社":      [1, 0, 0, 0, 1],
    "ブランド::B社":      [0, 1, 1, 0, 0],
}).set_index("product")

V = beta_n @ scenario[feature_names].to_numpy().T  # shape: (N_RESP, J)
scenario
# (1) ファーストチョイス・ルール
first_choice = np.zeros_like(V)
first_choice[np.arange(N_RESP), V.argmax(axis=1)] = 1
share_first_choice = first_choice.mean(axis=0)

# (2) シェア・オブ・プリファレンス・ルール(ロジットルール)
exp_V = np.exp(V)
share_logit = (exp_V / exp_V.sum(axis=1, keepdims=True)).mean(axis=0)

# (3) ランダマイズド・ファーストチョイス(RFC):ガンベル誤差を加えて多数回試行した平均
N_SIM = 2000
rfc_counts = np.zeros(V.shape[1])
for _ in range(N_SIM):
    noisy_V = V + rng.gumbel(size=V.shape)
    winners = noisy_V.argmax(axis=1)
    rfc_counts += np.bincount(winners, minlength=V.shape[1])
share_rfc = rfc_counts / (N_SIM * N_RESP)

pd.DataFrame({
    "first_choice": share_first_choice,
    "share_of_preference(logit)": share_logit,
    "RFC": share_rfc,
}, index=scenario.index).style.format("{:.1%}")

ファーストチョイス・ルールは最も極端な(0か1に近い)シェア配分になりやすく、ロジットルールは相対的に均等寄りのシェアを与える。RFCは両者の中間的な結果になる傾向がある。どのルールを採用するかは、実際の市場での「勝者総取り度合い」がどの程度かという業界知識と、ホールドアウトタスク(実験計画(属性・水準の設計)モデル評価)での予測精度検証によって選ぶ。

What-ifシミュレーション

同じ枠組みで、自社製品Xの属性(価格や容量)を変化させたときにシェアがどう変化するかをシミュレーションできる。これにより、価格改定や新機能追加の意思決定を、実際の市場投入前に定量的に評価できる。

def simulate_share(scenario_df, beta_n, rule="logit"):
    V = beta_n @ scenario_df[feature_names].to_numpy().T
    if rule == "logit":
        exp_V = np.exp(V)
        return (exp_V / exp_V.sum(axis=1, keepdims=True)).mean(axis=0)
    elif rule == "first_choice":
        fc = np.zeros_like(V)
        fc[np.arange(len(V)), V.argmax(axis=1)] = 1
        return fc.mean(axis=0)

# 自社新製品Xの価格を1,000円 -> 1,500円に値上げした場合のシェア変化
scenario_price_up = scenario.copy()
scenario_price_up.loc["自社新製品X", ["価格::1,000円", "価格::1,500円"]] = [0, 1]

before = simulate_share(scenario, beta_n)
after = simulate_share(scenario_price_up, beta_n)

pd.DataFrame({"値上げ前": before, "値上げ後": after}, index=scenario.index).style.format("{:.1%}")

参考

  • Huber, J., Orme, B., & Miller, C. (1999). Dealing with product similarity in conjoint simulations. Sawtooth Software Conference Proceedings.

  • Orme, B. K. (2010). Getting Started with Conjoint Analysis: Strategies for Product Design and Pricing Research. Research Publishers LLC.