2018年12月29日土曜日

PythonでMCA(コレスポンデンス分析)

Pythonでコレポンをやる

コレスポンデンス分析

  • 略してコレポン

    • 数量化III類(Quantification Theory III)、対応分析(CA: Correspondence Analysis)、多重対応分析(MCA: Multiple Correspondence Analysis)、双対尺度法(Dual Scaling)など、呼び方や近縁手法が複数存在し、数理的には非常に近い関係にある
    • ここでは「コレポン」表現をメインに据えている。実際にはCAだったりMCAだったりするが数理的背景は非常に近いため細かい区別は気にしない
  • point

    • 観測データを単純化することで、データの背後にある潜在的な要素を解釈しやすくする
    • 質的データ(カテゴリデータ)の関係構造を探索するのに適している
    • 主成分分析(Principal Component Analysis)と数理的に近い構造を持つ
    • PCA同様にデータを縮約して、2次元プロットなどで可視化できる
    • 統計的厳密性よりは、結果解釈のしやすさや仮説探索的手法として喜ばれやすい(コレポンに限らない)
    • 大雑把な傾向把握にとどめるのが望ましい(細かいことを気にしないことこそコレポンらしさかも)
  • Rの場合

    1. インストールして library(ca)
    2. 読み込んで実行して ca(table.T)
    3. 可視化 plot(ca(table.T))
  • Pythonの場合

    • ここではmcaライブラリを使用 python -m pip install mca
    • 単機能でシンプルではあるが、新規に始めるならprinceライブラリの方がいろんなことに使いやすいかも

コレポンとPCA

  • コレポンの特徴
    • カテゴリデータや頻度データの関連構造を少数の軸で表現する手法。期待頻度からのズレ(カイ二乗統計量)が大きい方向を探す。
    • カイ二乗距離に基づく幾何を前提としている。行・列の周辺度数で基準化した行列を特異値分解する。
    • 行と列を双対的に扱い、同一空間上に同時布置できる。
  • PCA(主成分分析)の特徴
    • 数値データの分散構造を少数の軸で表現する手法。データのばらつき(分散)が最大になる方向を探す。
    • ユークリッド距離に基づく幾何を前提としている。共分散行列または相関行列を固有値分解する。
    • 行(サンプル)は主成分スコア、列(変数、特徴量)は負荷量として表現される。
  • コレポンとPCAの共通点
    • 多変量データの構造を少数の軸で要約・可視化する次元圧縮手法(データの背後にある構造を捉えるためのもの)
    • データ中の「似たパターン」や「関係構造」を低次元空間へ埋め込む(低次元に圧縮して、似た回答傾向を可視化する)
    • 元データを座標空間へ変換し、近い点ほど似た性質を持つと解釈できる
    • 軸ごとの構造を使って、クラスタ傾向や潜在的な特徴を読み解ける
  • コレポンとPCAの相違点
    • コレポンとPCAは少なくとも数理的な骨格は同じなんだが
    • 軸の解釈に対する考え方の違い、解析結果の安定性など運用上は多くの相違点が存在する(PCAにおける軸は量的な増減方向として解釈しやすい。コレポンにおける軸はカテゴリの対立軸として表れやすい)
    • だがコレポンは適用範囲が広い。理論的前提から多少外れていても、大まかな構造把握用途では有用な場合が多い。特に順序尺度のアンケートデータなどでは、PCA的に見るかコレポン的に見るかの境界が曖昧になることも多い

コレポンの注意点

  • 仮説探索や構造把握に向いた手法であり、推定や検定のような厳密性を主目的とする分析とは少し性格が異なる
  • 適用範囲が広い(深く捉えなくともなんでも実行しやすい)とはいえ、本来持っていた情報を相当捨てていることは気に留めておく
  • 変数間で尺度の意味が大きく異なるデータには向かない(例えば年収と身長と気温を同列の変数として扱うのは不適切)
  • PCA同様に異常値や希少カテゴリは空間構造をゆがめやすい

コレポンの実情

  • 特にマーケティング分野におけるアンケート調査データにおいて重宝されやすい
  • 「結果の見た目がわかりやすい」「直感的に理解しやすい」「解釈やストーリーを考察しやすいので話が弾む」などが主な理由として考えられる
  • 他に「クロス数表があればできちゃう」「たいていの設問形式に対応できちゃう(ように感じやすい)」「簡単な操作で簡単なアウトプットが得られちゃう」もありそう
  • 理論的美しさよりも、実行しやすさや適用範囲の広さ、データの背後構造の見えやすさが魅力と言える

コレポンの守備範囲

  • 理論的には、行・列ともにカテゴリデータの頻度表(人数、出現数などのcountデータ)を対象としている

    • 例えば、行に「カフェのメニュー」、列に「性年代区分」を置いたデータセット(各メニューを好きと回答した人数の性年代別集計値)でおこなうのが王道
    • だがクロス数表を読み慣れている場合はコレポンやる意義を感じにくいかも
    • 性別や年代による違いは想像がしやすいため(ある程度わかってる顕在的要素をなぞってるだけに見えるぶんコレポンの利点が活かされにくい)
  • コレポンをよく使う場面

    • 例えば、行に「カフェのメニュー」、列に「イメージワード」を置いたデータセット(各メニューの印象にあてはまるワードを選択した人数のワード別集計値)
    • イメージが似てるメニューはどれとどれなのか、逆に似ていないのはどれかといった解釈がしやすい(クロス数表からは読み解きにくい)
    • コレポンの理論的枠組みからはやや外れるが、コレポンやる意義を感じさせやすい
  • 他に、

    • 行に「回答者」、列に「イメージワード」を置いたデータセット(回答者各自がカフェの印象にあてはまるワードに5段階評価したローデータ)
    • これも理論的枠組みからは外れる(PCAっぽいともいえる)が、ある人がどんなワードを重視してるか、似た者同士は誰かといった解釈がしやすい
  • 比率表(回答者数全体に占める割合、%データ)のデータセット

    • コレポン自体は実行できるし、頻度表よりも入手しやすい場面もあるなどこちらでおこなう人が多数派かもしれない
    • これまた理論的枠組みからは外れるが、単純化するという性質からさほど弊害が表れにくい(頻度表での実行結果と大差ないなど)
    • 同様理由で、表の縦%や横%が100を超える場合(複数回答形式設問)でもおこなわれているだろう

コレポン結論

  • 理論を超えて使いやすいが、わかりやすさに引きづられない
  • 簡単に得られる情報と引き換えに、捨てている情報も多いことを忘れない

コレポン実行例

  • あるカフェのお客様アンケート結果をコレポンしてみる
  • メニュー × イメージワード
    • 実務においてありがちなコレポンの使い方
    • お店のメニュー(8品)それぞれに対して、印象にあてはまるイメージワード(8項目)をMA(複数回答可能形式)で聴取
    • 行(メニュー)、列(イメージワード)のクロス数表(選択した人数の頻度表)
    • MA選択率よりはイメージワード各5段階評価のTOP2人数の方がコレポン理論には近い。とはいえ回答者負荷の高い設問形式を推奨したくない意図
# import
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import mca

# version
import sys;print("Python", sys.version.split()[0])
import platform;print("OS", platform.platform())
print("is_google_colab:", "google.colab" in sys.modules)
from importlib.metadata import version
packages = [
"pandas",
"numpy",
"matplotlib",
"mca"
]
for pkg in packages:
    try:
        print(pkg, version(pkg))
    except Exception as e:
        print(pkg, "not installed")
Python 3.11.15
OS macOS-26.5-arm64-arm-64bit
is_google_colab: False
pandas 2.2.0
numpy 1.26.4
matplotlib 3.10.9
mca 1.0.4
# サンプルのdataframeを生成する

# メニュー(行)
menus = ['Coffee', 'Tea', 'Latte', 'Espresso', 'Cake', 'Sandwich', 'Cookie', 'Toast']

# イメージワード(列)
words = ['香り高い', 'コスパが良い', '見た目が映える', '濃厚な味わい', 
         'ボリューム満点', '手軽に食べれる', '贅沢な気分', '健康的なイメージ']

# プロットがある程度バラつくように、『Tea』『Cookie』『手軽に食べれる』が中心近辺にくるように平均的な値に寄せている
data = [
    [50, 25,  5, 15,  5, 30, 10,  5], # Coffee
    [25, 20, 15, 10, 10, 35, 15, 15], # Tea
    [10, 10, 45, 35, 10, 20, 30,  5], # Latte
    [40, 10, 10, 55,  5, 15, 25,  5], # Espresso
    [10, 10, 50, 30, 10, 15, 45,  5], # Cake
    [ 5, 25, 10,  5, 50, 40,  5, 30], # Sandwich
    [20, 20, 18, 15, 15, 30, 15, 12], # Cookie
    [10, 40,  5,  5, 35, 40,  5, 20], # Toast
]

df = pd.DataFrame(data, index=menus, columns=words)


# MCAの実行
ncol = df.shape[1]
mca_ben = mca.MCA(df, ncols=ncol, benzecri=False, TOL=1e-8)


# 行スコア(=座標)
result_by_row = pd.DataFrame(mca_ben.fs_r(N=2))
result_by_row.index = list(df.index)
print ("Score by row:")
print(result_by_row.round(3))
Score by row:
              0      1
Coffee    0.010 -0.686
Tea       0.132 -0.137
Latte    -0.513  0.348
Espresso -0.535 -0.413
Cake     -0.594  0.440
Sandwich  0.771  0.284
Cookie    0.080 -0.020
Toast     0.710  0.047
# 列スコア(=座標)
result_by_column = pd.DataFrame(mca_ben.fs_c(N=2))
result_by_column.index = list(df.columns)
print ("Score by column:")
print(result_by_column.round(3))
Score by column:
              0      1
香り高い     -0.184 -0.727
コスパが良い    0.433 -0.130
見た目が映える  -0.533  0.537
濃厚な味わい   -0.629 -0.123
ボリューム満点   0.724  0.326
手軽に食べれる   0.339 -0.065
贅沢な気分    -0.579  0.227
健康的なイメージ  0.642  0.160
# 固有値と寄与率

# MCAでは「行と列のどちらか少ない方の数-1」 が固有値(eigenvalue)の最大数になるためそれを算出
num_of_eigenvalue = min(len(df.index), len(df.columns)) - 1

data = {
    'Factor': range(1, len(mca_ben.L) + 1),
    'value': pd.Series(mca_ben.L),
    'ratio': mca_ben.expl_var(greenacre=False, N=num_of_eigenvalue)
}
columns = ['Factor', 'value', 'ratio']
table2 = pd.DataFrame(data=data, columns=columns).fillna(0)

# ratioの累計(cumulative_ratio)列を追加
table2['cum_ratio'] = table2['ratio'].cumsum()

print("Principal & Inertia(固有値 寄与率):")
print(table2.round(3).to_string(index=False))
Principal & Inertia(固有値 寄与率):
 Factor  value  ratio  cum_ratio
      1  0.266  0.598      0.598
      2  0.131  0.296      0.894
      3  0.033  0.075      0.969
      4  0.007  0.016      0.986
      5  0.004  0.008      0.994
      6  0.003  0.006      1.000
      7  0.000  0.000      1.000

# 上記Factor1,2でプロット
plt.figure(figsize=(7, 6))

# plt.rcParams['font.family'] = ['sans-serif', 'Meiryo']  # windows
plt.rcParams['font.family'] = ['sans-serif', 'Hiragino Sans']  # mac

# plot by row
plt.scatter(result_by_row[0], result_by_row[1], color='tab:blue', s=20, marker="o", label="Row")
for k, v in result_by_row.iterrows():
    plt.annotate(k, v, xytext=(2, 2), textcoords="offset points")

# plot by column
plt.scatter(result_by_column[0], result_by_column[1], color='tab:orange', s=20, marker='s', facecolors="none", label="Column")
for k, v in result_by_column.iterrows():
    plt.annotate(k, v, xytext=(2, 2), textcoords="offset points")

plt.axhline(0, color='gray', linestyle='dashed')
plt.axvline(0, color='gray', linestyle='dashed')
plt.xlabel('Factor 1')
plt.ylabel('Factor 2')
plt.title("Correspondence Analysis\n(Customer Satisfaction Survey Example)")
plt.legend(loc='best')
# plt.gca().set_aspect("equal")
plt.show()

結果の解釈例

  • 左上のCakeとLatteは似たイメージでお客様から捉えられている
  • CakeとLatteはともに「映える」「贅沢」といったリッチなイメージが強そう
  • これらとは対極の右側にはSandwich, Toastが「手軽に食べれる」日常的なMenuとの評価
  • 横軸(第1軸)はハレと日常を表すような背後要因が考えられる
  • 下方のCoffeeはハレと日常の中間的ポジションながらも、Espressoともども「香り高い」イメージ
  • 縦軸(第2軸)は寄与率が1軸ほど大きくなく、背後要因はやや読み解きにくい(今回使用したイメージワードでは不足だった可能性も考えられる)
  • 中心付近にプロットされた「Cookie」「Tea」は他のMenuほどには強い特徴がみられなかったと解釈できる

(2026年5月更新)


0 件のコメント: