対応分析の理論と解釈

アンケート調査で「年代(10代・20代・30代・40代以上)」と「好きな音楽ジャンル(ポップ・ロック・クラシック・ジャズ・ヒップホップ)」のクロス集計表が得られたとします。この $4 \times 5$ の分割表には20個の数値が並んでいますが、数値の羅列だけでは「どの年代がどのジャンルに偏っているか」というパターンを直感的に把握するのは容易ではありません。

もし、年代と音楽ジャンルのカテゴリを1枚の散布図上にプロットし、「近いカテゴリ同士は関連が強い」という単純なルールで読み取れたら、分割表の構造は一目で理解できるでしょう。このような可視化を実現するのが対応分析(Correspondence Analysis, CA)です。

対応分析は、分割表(クロス集計表)の行カテゴリと列カテゴリの関連構造を低次元の散布図(バイプロット)上に可視化する多変量手法です。行と列のカテゴリを同じ空間にプロットすることで、カテゴリ間の関連パターンが直感的に把握できます。数学的には、対応行列の標準化残差に対する特異値分解(SVD)として定式化されます。

対応分析を理解すると、以下のような幅広い応用が開けます。

  • 市場調査: ブランドイメージとターゲット消費者層の関係を可視化し、ポジショニング戦略を策定する
  • テキストマイニング: 文書と単語の共起関係を可視化し、文書のテーマやクラスタを発見する
  • 生態学: 種の出現パターンと環境要因の関係を可視化し、生態系の構造を理解する
  • 考古学: 遺物の型式と出土地域の関連を分析し、文化圏の広がりを推定する
  • 社会学: 職業・学歴・所得などのカテゴリ変数間の関連構造を探索する

本記事では、対応分析の数学的理論をカイ二乗距離と特異値分解(SVD)を用いて厳密に定式化します。行・列プロファイルの幾何学的意味、慣性(イナーシャ)の概念、座標の種類(主座標と標準座標)の違いを解説し、プロットの正しい解釈方法を示します。Pythonでスクラッチ実装し、結果を可視化します。

本記事の内容

  • 対応分析の直感的な理解 — 分割表を「地図」にする
  • 行・列プロファイルの定義と幾何学的意味
  • カイ二乗距離の定義と重み付けの理由
  • 標準化残差行列と特異値分解による定式化
  • 慣性(イナーシャ)とカイ二乗統計量の関係
  • 座標の種類 — 主座標・標準座標・対称座標
  • バイプロットの解釈の注意点
  • Pythonでの実装と可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

  • 主成分分析の理論 — 対応分析はPCAのカテゴリデータ版と見なせます
  • カイ二乗検定 — 分割表の独立性検定。対応分析の慣性はカイ二乗統計量と直結します

対応分析の直感的な理解

分割表を「地図」にする

対応分析の基本的なアイデアは驚くほどシンプルです。分割表の行カテゴリ(例: 年代)と列カテゴリ(例: 音楽ジャンル)を、ある種の「意味の近さ」に基づいて2次元平面上に配置し、近い点同士は関連が強いという直感的な読み方を可能にすることです。

例えば、年代と音楽ジャンルの対応分析プロットで「10代」の点と「ポップ」の点が近くにプロットされれば、10代はポップを好む傾向があると解釈できます。「40代以上」と「クラシック」が近ければ、年配世代はクラシックを好む傾向があることを示します。

この「近さ」には数学的な裏付けがあります。通常のユークリッド距離ではなく、カイ二乗距離(chi-squared distance)で測られた距離が、低次元プロット上で近似的に保存されるように座標が決められます。

PCAとの類似性と違い

対応分析は、カテゴリデータに対する主成分分析(PCA)のアナロジーとして理解できます。両者を比較してみましょう。

PCA 対応分析
入力データ 連続変数の $n \times p$ データ行列 カテゴリ変数の $I \times J$ 分割表
距離の尺度 ユークリッド距離 カイ二乗距離
中心化の方法 平均を引く 独立モデルからの残差を取る
分解の対象 共分散行列(または相関行列) 標準化残差行列
分解の方法 固有値分解 特異値分解(SVD)
説明される量 全分散(全固有値の和) 全慣性(カイ二乗/n)

PCAは「分散を最大化する方向」を見つけますが、対応分析は「独立性からの乖離を最もよく説明する方向」を見つけます。独立性からの乖離が大きいほど、行カテゴリと列カテゴリの関連が強いことを意味します。

それでは、対応分析の数学的な定式化に進みましょう。

分割表の基本量

対応行列と周辺度数

$I \times J$ の分割表(度数表)を $\bm{N} = (n_{ij})$ とします。$n_{ij}$ は行カテゴリ $i$ と列カテゴリ $j$ の組み合わせの度数です。総度数を $n = \sum_{i=1}^{I}\sum_{j=1}^{J}n_{ij}$ とします。

対応行列(correspondence matrix)$\bm{P}$ は、度数を総度数で割った相対度数行列です。

$$ \begin{equation} \bm{P} = \frac{1}{n}\bm{N}, \quad p_{ij} = \frac{n_{ij}}{n} \end{equation} $$

$\bm{P}$ の全要素の和は1であり、$\bm{P}$ は同時確率分布の推定量と見なせます。

行の周辺相対度数(行質量、row masses)を $r_i = \sum_{j=1}^{J}p_{ij}$、列の周辺相対度数(列質量、column masses)を $c_j = \sum_{i=1}^{I}p_{ij}$ とします。

ベクトル表記で $\bm{r} = (r_1, \dots, r_I)^T$、$\bm{c} = (c_1, \dots, c_J)^T$ とし、対応する対角行列を $\bm{D}_r = \text{diag}(r_1, \dots, r_I)$、$\bm{D}_c = \text{diag}(c_1, \dots, c_J)$ とします。

行プロファイルと列プロファイル

行プロファイル(row profile)は、各行カテゴリの条件付き確率分布です。第 $i$ 行のプロファイルは:

$$ \begin{equation} \bm{a}_i = \left(\frac{p_{i1}}{r_i}, \frac{p_{i2}}{r_i}, \dots, \frac{p_{iJ}}{r_i}\right)^T \end{equation} $$

直感的には、行プロファイル $\bm{a}_i$ は「行カテゴリ $i$ に属する個体が、各列カテゴリにどのような割合で分布しているか」を表します。例えば、10代の行プロファイルは「10代が各音楽ジャンルをどのような割合で好むか」の分布です。

行プロファイル行列は $\bm{D}_r^{-1}\bm{P}$ で与えられます。

同様に、列プロファイル(column profile)は各列カテゴリの条件付き確率分布です。第 $j$ 列のプロファイルは:

$$ \begin{equation} \bm{b}_j = \left(\frac{p_{1j}}{c_j}, \frac{p_{2j}}{c_j}, \dots, \frac{p_{Ij}}{c_j}\right)^T \end{equation} $$

列プロファイル行列は $\bm{P}\bm{D}_c^{-1}$ の転置で与えられます。

プロファイルの幾何学的意味

行プロファイル $\bm{a}_i$ は $J$ 次元の確率単体(simplex)上の点です。全ての行プロファイルが同一であれば($\bm{a}_i = \bm{c}$ for all $i$)、行カテゴリと列カテゴリは独立です。行プロファイルが列の周辺分布 $\bm{c}$ からどれだけ離れているかが、行カテゴリと列カテゴリの関連の強さを表します。

対応分析は、この $J$ 次元のプロファイル空間を低次元に縮約し、プロファイル間の距離(カイ二乗距離)をできるだけ保存するように可視化する手法です。

プロファイル間の距離がなぜユークリッド距離ではなくカイ二乗距離で測られるのか、その理由を次のセクションで見ていきましょう。

カイ二乗距離

定義

行プロファイル $\bm{a}_i$ と $\bm{a}_{i’}$ の間のカイ二乗距離は次のように定義されます。

$$ \begin{equation} d_\chi^2(i, i’) = \sum_{j=1}^{J}\frac{1}{c_j}\left(\frac{p_{ij}}{r_i} – \frac{p_{i’j}}{r_{i’}}\right)^2 \end{equation} $$

列プロファイル間のカイ二乗距離も同様に定義されます。

$$ \begin{equation} d_\chi^2(j, j’) = \sum_{i=1}^{I}\frac{1}{r_i}\left(\frac{p_{ij}}{c_j} – \frac{p_{ij’}}{c_{j’}}\right)^2 \end{equation} $$

なぜカイ二乗距離を使うのか

ユークリッド距離ではなくカイ二乗距離を使う理由は、等価性の原理(distributional equivalence principle)を満たすためです。

等価性の原理とは: 「同じプロファイルを持つ2つのカテゴリを1つに統合しても、残りのカテゴリの座標が変わらない」という性質です。

例えば、「クラシック」と「クラシック(ライト)」が全く同じ行プロファイル分布を持つなら、これらを統合して「クラシック全般」としても、「ポップ」や「ロック」の座標は変わるべきではありません。ユークリッド距離はこの性質を持ちませんが、カイ二乗距離は持ちます。

$1/c_j$ による重み付けの直感的な意味は: 出現頻度の低いカテゴリの差をより重要視することです。全体の5%しか占めないカテゴリで20%対10%の差があれば、それは50%を占めるカテゴリで52%対51%の差よりも「注目すべき」偏りです。$1/c_j$ の重みがこの相対的な重要性を反映しています。

カイ二乗距離と加重ユークリッド距離

カイ二乗距離は、$\bm{D}_c^{-1/2}$ で座標変換した後のユークリッド距離として表現できます。

$$ d_\chi^2(i, i’) = \left\|\bm{D}_c^{-1/2}\left(\frac{\bm{a}_i – \bm{a}_{i’}}{1}\right)\right\|^2 $$

つまり、行プロファイルを $\bm{D}_c^{-1/2}$ で変換した空間ではユークリッド距離として計算できます。この変換後の空間でPCA的な操作を行うのが対応分析の本質です。

カイ二乗距離の理論的基盤が整ったので、次に特異値分解による定式化に進みましょう。

特異値分解による定式化

独立性からの残差

行カテゴリと列カテゴリが独立であれば、同時確率は $p_{ij} = r_i c_j$ です。独立モデルからの残差は:

$$ \begin{equation} p_{ij} – r_i c_j \end{equation} $$

この残差が大きいほど、カテゴリ $i$ とカテゴリ $j$ の関連が強いことを意味します。正の残差は「期待より多い」(正の関連)、負の残差は「期待より少ない」(負の関連)を示します。

標準化残差行列

残差を適切に標準化した行列が対応分析の核心です。

$$ \begin{equation} \bm{Z} = \bm{D}_r^{-1/2}(\bm{P} – \bm{r}\bm{c}^T)\bm{D}_c^{-1/2} \end{equation} $$

$\bm{Z}$ の $(i, j)$ 成分は:

$$ z_{ij} = \frac{p_{ij} – r_i c_j}{\sqrt{r_i c_j}} $$

これは標準化ピアソン残差に他なりません。実は、$\sum_{i,j}z_{ij}^2 = \chi^2/n$ であり、$\bm{Z}$ の全要素の二乗和はピアソンのカイ二乗統計量を $n$ で割ったものに等しくなります。

特異値分解

$\bm{Z}$ の特異値分解(SVD)を行います。

$$ \begin{equation} \bm{Z} = \bm{U}\bm{\Delta}\bm{V}^T = \sum_{k=1}^{K}\delta_k\bm{u}_k\bm{v}_k^T \end{equation} $$

ここで $K = \min(I, J) – 1$($\bm{Z}$ の行和と列和が0になるため、ランクは最大でも $\min(I,J) – 1$)、$\delta_1 \geq \delta_2 \geq \dots \geq \delta_K \geq 0$ は特異値、$\bm{u}_k$ と $\bm{v}_k$ は左・右特異ベクトルです。

慣性(イナーシャ)

対応分析における慣性(inertia)は、分割表の独立性からの全体的な乖離を測る指標です。

全慣性(total inertia):

$$ \begin{equation} \text{Total inertia} = \sum_{k=1}^{K}\delta_k^2 = \|\bm{Z}\|_F^2 = \sum_{i,j}z_{ij}^2 = \frac{\chi^2}{n} \end{equation} $$

ここで $\|\cdot\|_F$ はフロベニウスノルム、$\chi^2$ はピアソンのカイ二乗統計量です。

この関係は非常に重要です。対応分析の全慣性はカイ二乗統計量を $n$ で正規化したものであり、$\chi^2/n$ はクラメールのVの二乗の基盤にもなります。全慣性が大きいほど、行と列の関連が強いことを意味します。

第 $k$ 軸の慣性: $\delta_k^2$

第 $k$ 軸の寄与率: $\delta_k^2 / \sum_l\delta_l^2$

PCAの寄与率と同じ解釈ができます。第1軸の寄与率が高ければ、独立性からの乖離の大部分が1つの方向で説明できることを意味します。

慣性の概念を理解した上で、具体的な座標の計算方法を見ていきましょう。

座標の計算と種類

主座標(Principal Coordinates)

SVDの結果から、行・列カテゴリの低次元座標を計算します。

行の主座標:

$$ \begin{equation} \bm{F} = \bm{D}_r^{-1/2}\bm{U}\bm{\Delta} \end{equation} $$

第 $i$ 行カテゴリの第 $k$ 軸の主座標は $f_{ik} = \delta_k u_{ik}/\sqrt{r_i}$ です。

列の主座標:

$$ \begin{equation} \bm{G} = \bm{D}_c^{-1/2}\bm{V}\bm{\Delta} \end{equation} $$

第 $j$ 列カテゴリの第 $k$ 軸の主座標は $g_{jk} = \delta_k v_{jk}/\sqrt{c_j}$ です。

主座標はカイ二乗距離を最もよく保存する低次元座標です。$K$ 軸全てを使えばカイ二乗距離は完全に再現されます。

標準座標(Standard Coordinates)

特異値を含まない座標を標準座標と呼びます。

行の標準座標: $\bm{\Phi} = \bm{D}_r^{-1/2}\bm{U}$

列の標準座標: $\bm{\Gamma} = \bm{D}_c^{-1/2}\bm{V}$

主座標と標準座標の関係は: $\bm{F} = \bm{\Phi}\bm{\Delta}$, $\bm{G} = \bm{\Gamma}\bm{\Delta}$

プロットの種類

座標の選び方により、異なるタイプのプロットが得られます。

プロット名 行の座標 列の座標 行間距離 列間距離 行列間距離
対称プロット 主座標 $\bm{F}$ 主座標 $\bm{G}$ カイ二乗距離 カイ二乗距離 直接解釈不可
非対称プロット(行基準) 主座標 $\bm{F}$ 標準座標 $\bm{\Gamma}$ カイ二乗距離 解釈不可 射影として解釈可
非対称プロット(列基準) 標準座標 $\bm{\Phi}$ 主座標 $\bm{G}$ 解釈不可 カイ二乗距離 射影として解釈可

対称プロットは最も一般的であり、行と列のカテゴリを同じスケールで表示します。ただし、行の点と列の点の間の距離は直接的な意味を持たないことに注意が必要です。行の点同士の距離(カイ二乗距離の近似)と列の点同士の距離は解釈できますが、行の点と列の点の距離を「行カテゴリと列カテゴリの関連の強さ」と直接解釈するのは厳密には正しくありません。

行と列の関連を読み取るには、「同じ方向にある行カテゴリと列カテゴリは正の関連がある」「反対方向にあるものは負の関連がある」という方向性で解釈するのが適切です。

遷移公式(重心原理)

行の主座標と列の標準座標の間には、次の重要な関係(遷移公式または重心原理)が成り立ちます。

$$ \begin{equation} f_{ik} = \frac{1}{\delta_k}\sum_{j=1}^{J}\frac{p_{ij}}{r_i}\gamma_{jk} \end{equation} $$

これは、行カテゴリ $i$ の主座標が、その行プロファイル $p_{ij}/r_i$ を重みとした列の標準座標の加重平均であることを意味します。つまり、行カテゴリは、それが多く関連する列カテゴリの方向に引き寄せられます

同様の関係が列についても成り立ちます。この遷移公式は、対応分析のバイプロットを解釈する上での理論的基盤です。

それでは、Pythonで対応分析を実装し、理論を確認しましょう。

Pythonによる実装と可視化

対応分析のスクラッチ実装

import numpy as np
import matplotlib.pyplot as plt

# --- 分割表の作成 ---
N = np.array([
    [35, 15, 3, 2, 25],   # 10代
    [25, 20, 8, 5, 12],   # 20代
    [10, 18, 22, 12, 8],  # 30代
    [5, 8, 28, 25, 4],    # 40代以上
])
row_labels = ['10s', '20s', '30s', '40s+']
col_labels = ['Pop', 'Rock', 'Classical', 'Jazz', 'HipHop']
I, J = N.shape
n_total = N.sum()

# --- 対応行列と基本量 ---
P = N / n_total
r = P.sum(axis=1)  # 行質量
c = P.sum(axis=0)  # 列質量
Dr = np.diag(r)
Dc = np.diag(c)

# --- 独立モデルからの期待値 ---
E = np.outer(r, c)

# --- 標準化残差行列 ---
Z = np.diag(1/np.sqrt(r)) @ (P - E) @ np.diag(1/np.sqrt(c))

# --- 特異値分解 ---
U, delta, Vt = np.linalg.svd(Z, full_matrices=False)
V = Vt.T

# --- 慣性 ---
inertia = delta**2
total_inertia = inertia.sum()
chi2 = total_inertia * n_total
prop_inertia = inertia / total_inertia

# --- 座標の計算 ---
n_dim = 2
# 行の主座標
F_coord = np.diag(1/np.sqrt(r)) @ U[:, :n_dim] @ np.diag(delta[:n_dim])
# 列の主座標
G_coord = np.diag(1/np.sqrt(c)) @ V[:, :n_dim] @ np.diag(delta[:n_dim])

print("=== 対応分析の結果 ===")
print(f"総度数 n = {n_total}")
print(f"全慣性 = {total_inertia:.4f}")
print(f"カイ二乗 = {chi2:.4f}")
print(f"\n--- 各軸の慣性と寄与率 ---")
for k in range(min(3, len(delta))):
    print(f"  軸{k+1}: 特異値={delta[k]:.4f}, "
          f"慣性={inertia[k]:.4f}, "
          f"寄与率={prop_inertia[k]:.1%}, "
          f"累積={prop_inertia[:k+1].sum():.1%}")

print(f"\n--- 行の主座標 ---")
for i, label in enumerate(row_labels):
    print(f"  {label:5s}: ({F_coord[i,0]:7.4f}, {F_coord[i,1]:7.4f})")

print(f"\n--- 列の主座標 ---")
for j, label in enumerate(col_labels):
    print(f"  {label:10s}: ({G_coord[j,0]:7.4f}, {G_coord[j,1]:7.4f})")

出力結果から、対応分析の基本的な構造が読み取れます。全慣性が大きいほど行と列の関連が強いことを意味します。この例では、第1軸と第2軸の累積寄与率が90%を超えており、2次元のバイプロットで分割表の構造を十分に表現できることがわかります。カイ二乗統計量の値も報告されており、独立性検定との関連が確認できます。

バイプロットの可視化

import numpy as np
import matplotlib.pyplot as plt

# データと計算(前のブロックと同じ)
N = np.array([[35,15,3,2,25],[25,20,8,5,12],[10,18,22,12,8],[5,8,28,25,4]])
row_labels = ['10s','20s','30s','40s+']
col_labels = ['Pop','Rock','Classical','Jazz','HipHop']
I, J = N.shape
n_total = N.sum()
P = N / n_total
r, c = P.sum(1), P.sum(0)
E = np.outer(r, c)
Z = np.diag(1/np.sqrt(r)) @ (P - E) @ np.diag(1/np.sqrt(c))
U, delta, Vt = np.linalg.svd(Z, full_matrices=False)
V = Vt.T
inertia = delta**2
total_inertia = inertia.sum()
prop_inertia = inertia / total_inertia
F_coord = np.diag(1/np.sqrt(r)) @ U[:,:2] @ np.diag(delta[:2])
G_coord = np.diag(1/np.sqrt(c)) @ V[:,:2] @ np.diag(delta[:2])

fig, axes = plt.subplots(1, 3, figsize=(18, 5.5))

# (a) バイプロット(対称プロット)
ax = axes[0]
ax.scatter(F_coord[:,0], F_coord[:,1], c='#377eb8', s=120, marker='s',
           zorder=5, edgecolors='black', linewidths=0.5, label='Row (Age)')
ax.scatter(G_coord[:,0], G_coord[:,1], c='#e41a1c', s=120, marker='^',
           zorder=5, edgecolors='black', linewidths=0.5, label='Column (Genre)')
for i, label in enumerate(row_labels):
    ax.annotate(label, (F_coord[i,0]+0.02, F_coord[i,1]+0.03),
                fontsize=11, color='#377eb8', fontweight='bold')
for j, label in enumerate(col_labels):
    ax.annotate(label, (G_coord[j,0]+0.02, G_coord[j,1]+0.03),
                fontsize=11, color='#e41a1c', fontweight='bold')
ax.axhline(0, color='gray', linewidth=0.5)
ax.axvline(0, color='gray', linewidth=0.5)
# 原点からの矢印
for j in range(J):
    ax.annotate('', xy=(G_coord[j,0], G_coord[j,1]), xytext=(0,0),
                arrowprops=dict(arrowstyle='->', color='#e41a1c', lw=1, alpha=0.3))
ax.set_xlabel(f'Dim 1 ({prop_inertia[0]:.1%})', fontsize=11)
ax.set_ylabel(f'Dim 2 ({prop_inertia[1]:.1%})', fontsize=11)
ax.set_title('(a) Correspondence Analysis Biplot', fontsize=12)
ax.legend(fontsize=9, loc='best')
ax.grid(True, alpha=0.3)

# (b) 慣性のスクリープロット
ax = axes[1]
K = len(delta)
dims = range(1, K + 1)
ax.bar(dims, prop_inertia[:K], color='steelblue', alpha=0.7, edgecolor='black')
ax.plot(dims, np.cumsum(prop_inertia[:K]), 'ro-', markersize=8, linewidth=2)
for k in range(K):
    ax.text(k+1, prop_inertia[k]+0.01, f'{prop_inertia[k]:.1%}',
            ha='center', fontsize=10)
ax.set_xlabel('Dimension', fontsize=11)
ax.set_ylabel('Proportion of Inertia', fontsize=11)
ax.set_title('(b) Scree Plot', fontsize=12)
ax.set_xticks(list(dims))
ax.grid(True, alpha=0.3, axis='y')

# (c) 行プロファイルの可視化
ax = axes[2]
row_profiles = (P.T / r).T  # D_r^{-1} P
x = np.arange(J)
width = 0.2
colors_row = ['#1b9e77', '#d95f02', '#7570b3', '#e7298a']
for i in range(I):
    ax.bar(x + i*width, row_profiles[i], width, label=row_labels[i],
           color=colors_row[i], alpha=0.7, edgecolor='black')
ax.bar(x + I*width, c, width, label='Marginal', color='gray',
       alpha=0.5, edgecolor='black', linestyle='--')
ax.set_xticks(x + width*(I)/2)
ax.set_xticklabels(col_labels, fontsize=10)
ax.set_ylabel('Profile proportion', fontsize=11)
ax.set_title('(c) Row Profiles vs Marginal', fontsize=12)
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.show()

3つのプロットから、対応分析の結果を多角的に理解できます。

  1. バイプロット(図a): 年代と音楽ジャンルの関連構造が2次元平面上に可視化されています。「10代」と「Pop」「HipHop」が左側に位置し、「40代以上」と「Classical」「Jazz」が右側に位置しています。これは、若い世代がポップやヒップホップを好み、年配世代がクラシックやジャズを好む傾向があることを示しています。「Rock」は中央やや上方に位置し、年代にあまり依存しない(あるいは20-30代に幅広く好まれる)ジャンルであることが読み取れます。第1軸(横軸)は「若い世代 vs 年配世代」の対立を表し、慣性の大部分を説明しています

  2. スクリープロット(図b): 各軸の慣性の寄与率が表示されています。第1軸の寄与率が高く、年代と音楽嗜好の関連が主に1つの方向で特徴づけられることがわかります。2軸までの累積寄与率がほぼ100%に近いため、この2次元バイプロットで分割表の構造をほぼ完全に表現できています

  3. 行プロファイル(図c): 各年代の音楽ジャンル分布(行プロファイル)と全体の周辺分布(灰色)を並べて表示しています。10代はPopとHipHopの割合が周辺分布より高く、ClassicalとJazzの割合が低いことが棒グラフから直接読み取れます。この偏りのパターンが、バイプロット上での各点の位置を決めています

カイ二乗距離の保存の検証

理論で述べた「カイ二乗距離が低次元プロット上で保存される」ことを数値的に検証します。

import numpy as np

# 同じデータを使用
N = np.array([[35,15,3,2,25],[25,20,8,5,12],[10,18,22,12,8],[5,8,28,25,4]])
n_total = N.sum()
P = N / n_total
r, c = P.sum(1), P.sum(0)
row_profiles = (P.T / r).T

# 行プロファイル間のカイ二乗距離(完全次元)
I = N.shape[0]
chi2_dist_full = np.zeros((I, I))
for i in range(I):
    for i2 in range(I):
        chi2_dist_full[i, i2] = np.sum((row_profiles[i] - row_profiles[i2])**2 / c)

# 主座標での近似距離
E = np.outer(r, c)
Z = np.diag(1/np.sqrt(r)) @ (P - E) @ np.diag(1/np.sqrt(c))
U, delta, Vt = np.linalg.svd(Z, full_matrices=False)
V = Vt.T
F_coord = np.diag(1/np.sqrt(r)) @ U[:,:2] @ np.diag(delta[:2])

chi2_dist_approx = np.zeros((I, I))
for i in range(I):
    for i2 in range(I):
        chi2_dist_approx[i, i2] = np.sum((F_coord[i] - F_coord[i2])**2)

# 全軸使用
F_full = np.diag(1/np.sqrt(r)) @ U @ np.diag(delta)
chi2_dist_full_check = np.zeros((I, I))
for i in range(I):
    for i2 in range(I):
        chi2_dist_full_check[i, i2] = np.sum((F_full[i] - F_full[i2])**2)

row_labels = ['10s', '20s', '30s', '40s+']
print("=== カイ二乗距離の保存の検証 ===")
print(f"\n{'':8s} | {'True χ² dist':>12s} | {'2D approx':>12s} | {'Full axes':>12s}")
print("-" * 55)
for i in range(I):
    for i2 in range(i+1, I):
        print(f"{row_labels[i]:>3s}-{row_labels[i2]:<3s} | "
              f"{chi2_dist_full[i,i2]:12.4f} | "
              f"{chi2_dist_approx[i,i2]:12.4f} | "
              f"{chi2_dist_full_check[i,i2]:12.4f}")

検証結果から、重要な事実が確認できます。全軸を使った主座標間のユークリッド距離は、行プロファイル間のカイ二乗距離と完全に一致しています(数値誤差の範囲内)。2次元近似では一部の距離情報が失われますが、寄与率が高い場合は非常に良い近似が得られます。これは、対応分析が「カイ二乗距離を保存する最適な低次元座標」を求めていることの数値的な裏付けです。

バイプロットの解釈の注意点

対応分析のバイプロットを解釈する際の重要な注意点をまとめます。

行カテゴリ同士・列カテゴリ同士の距離

同じ種類のカテゴリ(行同士・列同士)の距離は、カイ二乗距離の近似として意味を持ちます。近い行カテゴリは似たプロファイルを持ち、遠い行カテゴリは異なるプロファイルを持ちます。

行カテゴリと列カテゴリの距離

異なる種類のカテゴリ(行と列)の距離は、対称プロットでは厳密な意味を持ちません。「近い = 関連が強い」という解釈は方向性としては正しいですが、距離の大きさを直接比較することはできません。

正確には、行カテゴリと列カテゴリの関連は「方向」で解釈すべきです。原点から見て同じ方向にある行カテゴリと列カテゴリは正の関連(期待より多い共起)を持ち、反対方向にあるものは負の関連(期待より少ない共起)を持ちます。

原点の解釈

原点は「独立性」を表します。原点に近いカテゴリは、他のカテゴリとの関連がほとんどなく、周辺分布に近いプロファイルを持つことを意味します。原点から遠いカテゴリほど、独立性からの乖離が大きく、特定のカテゴリとの関連が強いことを示します。

質量の考慮

プロットを解釈する際には、各カテゴリの質量(周辺度数)も考慮すべきです。質量の小さいカテゴリ(稀なカテゴリ)は、わずかなデータの変動で座標が大きく変わる可能性があります。質量の大きいカテゴリの位置はより安定しています。

まとめ

本記事では、対応分析の理論と実践を体系的に解説しました。

  • 目的: 分割表の行カテゴリと列カテゴリの関連構造を低次元のバイプロットで可視化する
  • 行・列プロファイル: 各カテゴリの条件付き確率分布。プロファイルの類似性がカテゴリの関連を反映する
  • カイ二乗距離: 周辺度数で重み付けした距離。等価性の原理を満たし、出現頻度の低いカテゴリの差を相対的に重要視する
  • 標準化残差行列のSVD: $\bm{Z} = \bm{D}_r^{-1/2}(\bm{P} – \bm{r}\bm{c}^T)\bm{D}_c^{-1/2}$ のSVDにより低次元座標を得る
  • 慣性: 全慣性は $\chi^2/n$ に等しく、独立性からの乖離の総量を測る。各軸の寄与率でプロットの表現力を評価する
  • 座標の種類: 主座標(カイ二乗距離を保存)、標準座標(遷移公式で対応)、対称座標(一般的な表示用)
  • 解釈: 同種カテゴリ間の距離はカイ二乗距離の近似。異種カテゴリ間は方向で解釈する

対応分析は2つのカテゴリ変数の関連を分析する手法ですが、3つ以上のカテゴリ変数を同時に分析する多重対応分析(Multiple Correspondence Analysis, MCA)に拡張できます。

次のステップとして、以下の記事も参考にしてください。