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.

MaxDiff(ベストワーストスケーリング)

MaxDiff(Maximum Difference Scaling、ベストワーストスケーリング, best-worst scaling) は、Louviere & Woodworth(1990年代)が提案した項目のスコアリング手法である。回答者に3〜5個程度の項目からなるセット(サブセット)を複数回提示し、そのつど「最も好ましい(best)」項目と「最も好ましくない(worst)」項目を1つずつ選ばせる。

評定型コンジョイント(評定型(トラディショナル)コンジョイント分析)のように「1〜7点で評価してください」という尺度評定は、回答者ごとに尺度の使い方(scale usage)が異なるという問題があるが、MaxDiffは常に相対比較(一番良い/一番悪い)で答えるため、この問題を避けられる。属性の組み合わせ(プロファイル)ではなく、単一の項目(機能、フレーズ、ブランドなど)のリストに直接重みをつけたい場合に使われる。

モデル

MaxDiffは、選択型コンジョイント分析(CBC)と多項ロジットモデルと同じランダム効用モデル・多項ロジットモデルの枠組みで説明できる。項目jjの効用(好ましさ)をβj\beta_jとすると、あるサブセットSSの中から項目jjが「best」として選ばれる確率は通常のMNLと同じ

P(best=jS)=exp(βj)kSexp(βk)P(\text{best} = j \mid S) = \frac{\exp(\beta_j)}{\sum_{k \in S} \exp(\beta_k)}

「worst」の選択は、効用の符号を反転したMNLとして表現できる。

P(worst=jS)=exp(βj)kSexp(βk)P(\text{worst} = j \mid S) = \frac{\exp(-\beta_j)}{\sum_{k \in S} \exp(-\beta_k)}

実務では、bestとworstを同時に説明する尤度として、「(1) bestを選ぶ」「(2) 残りの項目からworstを選ぶ」という2段階の選択として尤度を構成することが多い(Best-Worstの逐次選択モデル、あるいは全てのJ(J1)J(J-1)通りのbest-worstペアを多項選択とみなす方法もある)。

P(best=j, worst=kS)=P(best=jS)P(worst=kS{j})P(\text{best}=j,\ \text{worst}=k \mid S) = P(\text{best}=j \mid S) \cdot P(\text{worst}=k \mid S \setminus \{j\})

実装例

6つの製品機能(アイデア)についてMaxDiffデータをシミュレーションし、多項ロジットで各機能のスコア(βj\beta_j)を推定する。

import itertools
import numpy as np
import pandas as pd

rng = np.random.default_rng(0)

items = ["高速充電", "防水", "軽量化", "長寿命バッテリー", "ワイヤレス充電", "耐衝撃"]
true_beta = {
    "高速充電": 1.5, "防水": 1.0, "軽量化": 0.2,
    "長寿命バッテリー": 1.8, "ワイヤレス充電": -0.5, "耐衝撃": -0.3,
}

N_RESP = 300
SUBSET_SIZE = 4

records = []
for n in range(N_RESP):
    subset = rng.choice(items, size=SUBSET_SIZE, replace=False)
    utilities = np.array([true_beta[i] for i in subset])

    # best: 効用+ガンベル誤差の最大
    best_idx = np.argmax(utilities + rng.gumbel(size=SUBSET_SIZE))
    # worst: best を除いた残りで、効用+ガンベル誤差の最小(= -効用の最大)
    remaining = [i for i in range(SUBSET_SIZE) if i != best_idx]
    worst_local = np.argmax(
        -utilities[remaining] + rng.gumbel(size=len(remaining))
    )
    worst_idx = remaining[worst_local]

    records.append({
        "resp": n,
        "subset": list(subset),
        "best": subset[best_idx],
        "worst": subset[worst_idx],
    })

df = pd.DataFrame(records)
df.head()
Loading...
# best/worstをそれぞれ「選択タスク」に展開し、conditional logitで同時推定する
# best タスク: 効用 = X . beta
# worst タスク: 効用 = X . (-beta)  <=> worst選択を「-beta のbest選択」とみなす

rows = []
task_id = 0
for _, row in df.iterrows():
    for item in row["subset"]:
        rows.append({
            "task": task_id, "item": item,
            "chosen": int(item == row["best"]),
            "is_worst_task": 0,
        })
    task_id += 1
    for item in row["subset"]:
        rows.append({
            "task": task_id, "item": item,
            "chosen": int(item == row["worst"]),
            "is_worst_task": 1,
        })
    task_id += 1

long_df = pd.DataFrame(rows)

# worstタスクでは効用の符号を反転させたいので、ダミー変数の符号をタスク種別に応じて反転する
X = pd.get_dummies(long_df["item"], drop_first=True).astype(float)
sign = np.where(long_df["is_worst_task"] == 1, -1, 1)
X = X.mul(sign, axis=0)

from statsmodels.discrete.conditional_models import ConditionalLogit

model = ConditionalLogit(long_df["chosen"], X, groups=long_df["task"])
result = model.fit()
result.summary()
# 基準項目(drop_firstで除外された項目)の推定値は0として、全項目のスコアを並べる
base_item = [i for i in items if i not in result.params.index][0]
beta_hat = result.params.to_dict()
beta_hat[base_item] = 0.0

pd.DataFrame({
    "true_beta": true_beta,
    "estimated_beta": beta_hat,
}).sort_values("true_beta", ascending=False)

推定されたスコアの順位(長寿命バッテリー > 高速充電 > 防水 > 軽量化 > 耐衝撃 > ワイヤレス充電)は真の順位と一致しており、相対的な大小関係も概ね復元できている。

MaxDiffスコアの解釈

推定されたβ^j\hat\beta_jは、そのままでは絶対的な尺度を持たない(識別のため基準項目を0とする相対値)。実務では以下のように解釈しやすい形に変換することが多い。

  • シェア・オブ・プリファレンス(share of preference)変換β^j\hat\beta_jをソフトマックス変換し、「全項目の中でその項目が一番選ばれる確率」として%表示する

sharej=exp(β^j)k=1Jexp(β^k)×100%\text{share}_j = \frac{\exp(\hat\beta_j)}{\sum_{k=1}^{J} \exp(\hat\beta_k)} \times 100\%
  • 0〜100スケールへのリスケール:最小項目を0、最大項目を100とする単純な線形変換で見た目を分かりやすくする(統計的な意味は薄い)

コンジョイント分析との違い

MaxDiffは属性×水準の組み合わせ(プロファイル)ではなく、フラットな項目リストを対象とする点で、コンジョイント分析よりも設計がシンプルである。「製品を構成する属性の組み合わせ」を評価したいならコンジョイント分析、「機能一覧・キャッチコピー案・優先順位をつけたい要求仕様など、並列した項目群に優先度をつけたい」ならMaxDiffが向いている。