概要
前回は直交表の作成方法について解説しましたが、今回はその直交表をもとに実施した調査データを使って、コンジョイント分析を行います。
直交表の解説はこちら ↓
❓コンジョイント分析とは❓
製品やサービスの複数の属性(例えば、価格、デザイン、機能など)が消費者の選好にどのように影響しているかを定量的に明らかにするための手法です。
具体的には、
✅ 各属性やその水準(選択肢)が消費者の購買意思決定にどれだけ影響を与えているか。
✅ 消費者がどの属性を重視しているか。
✅ どの組み合わせの製品がより好まれるか。
を調べます。
❓OLS(Ordinary Least Squares:最小二乗法)回帰とは❓
統計学や機械学習で広く使われる回帰分析の手法の一つです。
売上予測や効果検証、特徴量の影響度分析など、さまざまな分野で利用されています。
❓効用値(part-worth)とは❓
コンジョイント分析では、製品を構成する各属性(およびその水準)に対して、ユーザーがどの程度好んでいるかを数値化します。
この数値を 効用値 と呼びます。
属性 | 水準(選択肢) | 効用値の傾向 | 解説 |
---|---|---|---|
バッテリー容量 | 大容量 | 高い効用値(プラス) | バッテリー容量が大きいほど好まれるため、効用値が高くなる |
価格 | 高価格 | 低い効用値(マイナス) | 価格が高いほど嫌われるため、効用値がマイナスになる |
効用値はあくまで相対的な好みの強さを表すもので、単体で見ると意味を持ちません。
しかし、異なる製品の効用値を合計して比較することで、どちらの製品がより好まれているかを判断できます。
調査内容
90名の被験者に対し、直交表に基づく9種類の商品パターン
を提示し、それぞれの購入意向について回答してもらいました。
評価 | 数値(変換後) | 意味 |
---|---|---|
欲しい | 2 | 強い購入意向 |
やや欲しい | 1 | やや購入したい |
どちらでもない | 0 | 購入意向なし(ニュートラル) |
たぶん買わない | -1 | やや購入拒否 |
絶対に買わない | -2 | 強い購入拒否 |
サンプルデータ
・A列:ID → 被験者ごとのユニークID。
・B列~E列:属性情報 → 性別、年齢、職業、本人の収入など。
・F列:OS(ブランド) → 被験者が現在使用しているOSやブランド情報。
・G列~O列:直交表の0~8に対する回答 → 各パターンに対する評価データ。
環境
python3.x
jupyter lab
コード解説
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
📚 直交表データを読み込む
#============================================
# 1. 直交表ファイル読み込む
#============================================
# 直交表データをCSVファイルから読み込み(1列目をインデックスとして設定)
df_design = pd.read_csv('直交表.csv', index_col=0, header=0)
# 属性名(直交表のインデックス)をリスト形式で取得
attributes = df_design.index.tolist()
📚 選択肢番号(カラム名)を取り出し行列入れ替える
#============================================
# 2. データを縦持ち(ロング形式)に変換
#============================================
# 直交表の行列を転置して、選択肢番号を列に変更
df_long = df_design.T.reset_index().rename(columns={'index':'選択肢番号'})
💡直交表の行と列を入れ替えて「選択肢番号」を新しい列として扱うことで
後続の解析や加工がやりやすくなります。
# 選択肢番号を文字列から整数型に変換(データ型の統一のため)
df_long['選択肢番号'] = df_long['選択肢番号'].astype(int)
💡この変換で数値として扱う処理やソートなどがスムーズになります。
📚 調査データを読み込む
#============================================
# 3. 調査データファイル読み込み
#============================================
df_survey = pd.read_csv('調査データ.csv')
📚 QA_0~QA_8を縦持ち(ロング形式)に変換
#============================================
# 4. QA_0~QA_8を縦持ち化(meltで整形)
#============================================
# 対象列(9つの評価項目)をリストで定義
qa_cols = [f'QA_{i}' for i in range(9)]
💡QA_0 から QA_8 までの列名を for 文で自動生成してリスト化しています。
こうしておくと、あとで melt() で扱いやすくなります。
# IDとOS(ブランド)をキーとして、QA列を縦持ちに変換
df_melt = df_survey.melt(id_vars=['ID', 'OS(ブランド)'], value_vars=qa_cols, var_name='QA', value_name='評価値')
・melt() → 横に並んだ「QA_0〜QA_8」の評価を縦方向に変換する。
・id_vars=[] → そのまま残す列を指定(今回は ID と OS(ブランド))。
・value_vars=qa_cols → 縦方向に変換したい列のリスト(QA_0〜QA_8)を指定。
・var_name='QA'
→ 新しく作る列の名前。縦持ちにしたときに元のカラム名(QA_0など)が入る。
・value_name='評価値
→ 新しく作る列の名前。縦持ちにしたときに評価値(2, 1, 0, -1, -2)が入る。
# 'QA'列から文字列 'QA_' を取り除き、数値(選択肢番号)に変換
df_melt['選択肢番号'] = df_melt['QA'].str.replace('QA_', '').astype(int)
💡QA列には "QA_0", "QA_1" といった文字列が入っていますが、
そこから"QA_" の部分を取り除いて
数字だけを抽出し、astype(int) で文字列から整数に変換しています。
こうして作成した 選択肢番号 列は、直交表の選択肢番号と対応しているため
この後の、結合や解析で利用します
📚 データ結合
#============================================
# 5. 属性水準データと評価データを結合(選択肢番号をキーに)
#============================================
df_merged = pd.merge(df_melt, df_long, on='選択肢番号', how='left')
📚 ダミー変数の作成
#============================================
# 6. 属性水準のカテゴリ変数をダミー変数化
#============================================
df_dummies = pd.get_dummies(df_merged[attributes])
・pd.get_dummies()
→ カテゴリ変数のを0/1のダミー変数に変換。
→ df_merged の属性水準(attributesリスト内の列)を一括で変換。
❓なぜダミー変数を作るの❓
カテゴリデータを数値化して、分析に使える形に整えるのがダミー変数の役割なんです。
多くの統計モデルや機械学習アルゴリズムは数値データしか扱えません。
たとえば、「色」が「赤」「青」「緑」といったカテゴリだった場合、
文字列のままではモデルは理解できません。そこで、
「赤」なら [1,0,0]
「青」なら [0,1,0]
「緑」なら [0,0,1]
のように0か1の列(ダミー変数)に変換します。
これにより、モデルは各カテゴリの違いを数値的に認識し特徴量として利用できるようになります。
🔍 コンジョイント分析の場合
商品の「属性水準」もカテゴリデータなので、これらをダミー変数に変換することで、どの属性が購入意向にどれだけ影響しているかを定量的に分析できるようになります。
📚全体効用値の推定(全体モデル)
#============================================
# 7. 全体効用値の推定
#============================================
# 属性水準のダミー変数をXに
X_all = pd.get_dummies(df_merged[attributes])
# 評価値(目的変数)をyに
y_all = df_merged['評価値']
# モデルを学習・推定
model_all = sm.OLS(y_all, X_all)
res_all = model_all.fit()
# 結果の表示
#print("=== 全体効用値 ===")
#print(res_all.params)
・y_all → 目的変数。「評価値」(-2〜+2)
・X_all → 説明変数。ダミー変数に変換した属性水準。
・sm.OLS() → statsmodels の関数で最小二乗法による線形回帰 を実行。
👉 ここでは、全体の被験者データをまとめて回帰分析しています。
この回帰によって、
どの属性水準が全体的にどのくらい好まれているか(効用値)
を数値として把握できます。
回帰係数として出力される値が、各属性水準の「全体効用値」 になります。
この値が大きいほど「好まれている傾向が強い」、小さい(マイナス)ほど「避けられる傾向がある」と解釈できます。
get_dummies() の落とし穴:多重共線性
pd.get_dummies() を使うと、カテゴリ変数が複数の列に展開されますが、
すべての水準を含めると「多重共線性」のリスクがあります。
✅ 対策:drop_first=True を使う
1つの水準を「基準」として削除することで、共線性を回避できます。
pd.get_dummies(df[attributes], drop_first=True)
🧠 この設定は、「基準水準に対して、他の水準がどれだけ効用を持つか」
を示す相対効用値の解釈にもつながります。
📚 現在使用中のOS(ブランド)ごとの効用値を推定
#============================================
# 8. 現在使用中のOS(ブランド)効用値推定
#============================================
manufacturer_utilities = []
# OSごとにグループ化し、OLS回帰を実行
for manu, group in df_merged.groupby('OS(ブランド)'):
X = pd.get_dummies(group[attributes])
y = group['評価値']
model = sm.OLS(y, X)
res = model.fit()
coef = res.params
# ダミー列すべてを揃えて、値が無いところは0に(穴埋め)
coef_full = pd.Series(0, index=df_dummies.columns)
coef_full.update(coef)
# 使用中のOS(ブランド)名を保持
coef_full['OS(ブランド)'] = manu
manufacturer_utilities.append(coef_full)
# 各ブランドごとの効用値をまとめたDataFrameに変換
df_manufacturer_util = pd.DataFrame(manufacturer_utilities)
・groupby('OS(ブランド)')
→ 回帰分析を「現在使用中のブランド」ごとに分けて実行。
・X → 説明変数。属性水準をダミー変数に変換したもの。
・y → 目的変数。「評価値」(-2~+2)を使用。
・sm.OLS() →statsmodels の線形回帰(最小二乗法)モデル。
・coef →属性水準ごとの回帰係数(=効用値)を取得。
・coef_full.update() →ダミーが無い水準には0を補完(全ブランドで列を統一)。
・df_manufacturer_util →各ブランドごとの効用値が並ぶDataFrameを生成。
📨 output
OLS(最小二乗法) は、「説明変数の数」>「サンプル数」 のような状況では失敗しやすくなります。
💥 主な原因
- 行列のランクが足りない(多重共線性)
- 係数が推定できない(行列の逆行列が計算できない)
- エラー:Singular matrix(特異行列)
✅ 対策案
① Ridge回帰(L2正則化)にする
→ 特にカテゴリが多く、ダミー変数が増えているときには Ridge が安定です。
from sklearn.linear_model import Ridge
model = Ridge(alpha=1.0)
model.fit(X, y)
② try-except で回避(ID別分析などループ内)
→ 特定のIDでエラーが出ても全体処理が止まらないようにする
try:
model = sm.OLS(y, X).fit()
except Exception as e:
print(f"エラー発生: {e}")
③ 列数を絞る(不要な属性水準を削る)
→ 分析目的に合わせて、重要度の低い変数は外すのも有効です。
🧠 モデルが失敗しても慌てずに、上記のような対応を検討してみてください。
#============================================
# 列名から属性名のprefixを削除して水準名だけに
#============================================
def rename_cols(col):
for attr in attributes:
if col.startswith(attr + '_'):
return col.replace(attr + '_', '', 1)
return col
df_manufacturer_util = df_manufacturer_util.rename(columns=rename_cols)
# OS(ブランド)列を先頭に移動
cols = ['OS(ブランド)'] + [c for c in df_manufacturer_util.columns if c != 'OS(ブランド)']
df_manufacturer_util = df_manufacturer_util[cols]
#print("\n===現在使用中のOS(ブランド)別効用値 ===")
#print(df_manufacturer_util)
・rename_cols()
→ 属性名を削除して、水準名だけに整えます。
→ 例:OS(ブランド)_iPhone → iPhone
・cols = [...] → 列の順番を調整して「OS(ブランド)」列を一番前に移動。
👉 各 OS(ブランド)ごとに回帰分析を行ったことで
属性水準ごとの効用値(=好まれ度) を一覧で比較できるようになりました。
これにより、どの属性がどのブランドユーザーに好まれているかを
数値で可視化できます。
⚠️ col.split('_')[1] のような単純な分割だと、
水準名に _ が含まれる場合に誤って分割される恐れがあります。
➡ startswith(attr + '_') を使って「属性名に確実に一致する部分だけを削除」しているのがポイントです。
📚 ID別効用値
#============================================
# 9. ID別効用値推定
#============================================
utility_list = []
for id_, group in df_merged.groupby('ID'):
X = pd.get_dummies(group[attributes])
y = group['評価値']
model = sm.OLS(y, X)
res = model.fit()
coef = res.params
coef_full = pd.Series(0, index=df_dummies.columns)
coef_full.update(coef)
coef_full['ID'] = id_
utility_list.append(coef_full)
df_utility = pd.DataFrame(utility_list)
df_utility = df_utility.rename(columns=rename_cols)
・groupby('ID') → 各被験者(ID)ごとにデータをグループ化。
・pd.get_dummies() → 属性水準をダミー変数に変換(カテゴリ→数値化)。
・sm.OLS() → statsmodelsの線形回帰モデル(最小二乗法)を使用。
・coef_full.update(coef) → 回帰結果の係数だけ上書き。その他の水準は0のまま保持。
・utility_list.append(...) → 各IDの効用値(回帰係数)をリストに追加。
# --- スマホメーカをマージ ---
df_utility = df_utility.merge(df_survey[['ID', 'OS(ブランド)']], on='ID', how='left')
# 列順:ID → スマホメーカ → 効用値列
cols = ['ID', 'OS(ブランド)'] + [c for c in df_utility.columns if c not in ['ID', 'OS(ブランド)']]
df_utility = df_utility[cols]
・cols = [...] → 列の順番を整理。「ID」と「OS(ブランド)」を先頭に配置。
:::note
👉 各ユーザーごとの効用値が一目でわかります。
誰がどの属性を好む傾向があるか、パーソナライズされた嗜好の分析に使えます。
また、この効用値をベースにクラスタ分析を行えば、
嗜好が似たユーザーグループを抽出することも可能です。
📚 CSV保存
#============================================
# 10. CSV保存
#============================================
df_utility.to_csv('効用値_ID別.csv', index=False, encoding="utf-8-sig")
df_manufacturer_util.to_csv('効用値_現在使用中のOS(ブランド)別効用値別.csv', index=False, encoding="utf-8-sig")
pd.DataFrame(res_all.params, columns=['効用値']).to_csv('効用値_全体.csv', encoding='utf-8-sig')
📚 現在使用中のOS(ブランド)別の効用値でグラフを作成
df_manufacturer_utilで棒グラフを作成します
# 一時的に警告オフ
with warnings.catch_warnings():
warnings.simplefilter("ignore")
# 日本語フォント設定(簡易版)
plt.rcParams['font.family'] = 'Meiryo' # WindowsならMeiryoが安全、MacならHiragino系
plt.rcParams['axes.unicode_minus'] = False # マイナス符号の文字化け防止
# データの整形とプロット
df_manuf_plot = df_manufacturer_util.set_index('OS(ブランド)').T
df_manuf_plot.plot(kind='bar', figsize=(15, 6))
# 凡例を右側の外に出す
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
plt.title('OS(ブランド)別効用値の比較')
plt.ylabel('効用値')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
見た瞬間に理解できないグラフは資料とは言えない
📊 同じデータを使ってエクセルで折れ線グラフ化してみます
👉 棒グラフは全体の傾向を見るのに適していますが、
折れ線グラフは「ブランドごとのパターンの違い」を視覚的に追いやすいです。
🔍 考察:OS(ブランド)別の効用値から読み取れること
✅ OS(ブランド)
各ユーザーは「現在使っているOS(ブランド)」に強い愛着を示す傾向。
たとえば、iPhoneユーザーはiPhoneを高く評価し、AndroidユーザーはAndroidを好む。
→ ブランドロイヤルティ(忠誠心)が明確に出ている。
🔋 バッテリー容量
「5000mAh」が全体的に高評価。
ただし、OSによっては「4000mAh」が最適とされるケースも。
→ バッテリー容量は多ければ多いほど良いわけではなく、他要因とのバランスが重要。
📱 画面サイズ
「6インチ」はすべてのユーザーで低評価。
一方、「5インチ」と「6.8インチ」で評価が割れている。
→ 小型派(携帯性重視)と大型派(視認性重視)のユーザー分化が見られる。
💰 価格
「9万〜13万円」が最も高評価。
「5万円」は一部ユーザーで好まれるが、評価のブレが大きい。
「17万円」は明確に不人気。
→ 安すぎても不安・高すぎても敬遠、中価格帯にニーズが集中。
🔍 属性水準の効用値の見方
📌プラスの値:その水準が好まれている
📌マイナスの値:その水準が避けられている
ブランド別に分けて可視化することで、ユーザーごとの嗜好の違いや優先順位のズレが一目でわかります。
まとめ
コンジョイント分析のPython実装を通して、
製品の各属性がユーザーの評価にどう影響しているかを可視化・分析してみました。
次回の記事では、購買確率 について解説します👋