MaxDiff(Maximum Difference Scaling、ベストワーストスケーリング, best-worst scaling) は、Louviere & Woodworth(1990年代)が提案した項目のスコアリング手法である。回答者に3〜5個程度の項目からなるセット(サブセット)を複数回提示し、そのつど「最も好ましい(best)」項目と「最も好ましくない(worst)」項目を1つずつ選ばせる。
評定型コンジョイント(評定型(トラディショナル)コンジョイント分析)のように「1〜7点で評価してください」という尺度評定は、回答者ごとに尺度の使い方(scale usage)が異なるという問題があるが、MaxDiffは常に相対比較(一番良い/一番悪い)で答えるため、この問題を避けられる。属性の組み合わせ(プロファイル)ではなく、単一の項目(機能、フレーズ、ブランドなど)のリストに直接重みをつけたい場合に使われる。
モデル¶
MaxDiffは、選択型コンジョイント分析(CBC)と多項ロジットモデルと同じランダム効用モデル・多項ロジットモデルの枠組みで説明できる。項目の効用(好ましさ)をとすると、あるサブセットの中から項目が「best」として選ばれる確率は通常のMNLと同じ
「worst」の選択は、効用の符号を反転したMNLとして表現できる。
実務では、bestとworstを同時に説明する尤度として、「(1) bestを選ぶ」「(2) 残りの項目からworstを選ぶ」という2段階の選択として尤度を構成することが多い(Best-Worstの逐次選択モデル、あるいは全ての通りのbest-worstペアを多項選択とみなす方法もある)。
実装例¶
6つの製品機能(アイデア)についてMaxDiffデータをシミュレーションし、多項ロジットで各機能のスコア()を推定する。
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()
# 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スコアの解釈¶
推定されたは、そのままでは絶対的な尺度を持たない(識別のため基準項目を0とする相対値)。実務では以下のように解釈しやすい形に変換することが多い。
シェア・オブ・プリファレンス(share of preference)変換:をソフトマックス変換し、「全項目の中でその項目が一番選ばれる確率」として%表示する
0〜100スケールへのリスケール:最小項目を0、最大項目を100とする単純な線形変換で見た目を分かりやすくする(統計的な意味は薄い)
コンジョイント分析との違い¶
MaxDiffは属性×水準の組み合わせ(プロファイル)ではなく、フラットな項目リストを対象とする点で、コンジョイント分析よりも設計がシンプルである。「製品を構成する属性の組み合わせ」を評価したいならコンジョイント分析、「機能一覧・キャッチコピー案・優先順位をつけたい要求仕様など、並列した項目群に優先度をつけたい」ならMaxDiffが向いている。