2026年10月8日に、Princeの現行リポジトリとCRAN caの実装、summary.caを再読しました。PrinceはCAを含む多変量探索分析をscikit-learn風APIで扱い、補助行・列や重みを案内しています。CRANの実装と要約メソッドは、分析次元、補助行列、主座標、寄与、cosineの計算・表示範囲を扱います。これらは座標と診断量の仕様であり、図の近さを因果、顧客セグメントの効果、売上へ変換する根拠ではありません。
色と評価のクロス表を図にしたら、「赤」と「好き」が近くに出ました。これだけで赤を推せば売れる、と判断してよいでしょうか。
対応分析(コレスポンデンス分析、CA)は、度数表の構成比の違いを少数の軸に整理する方法です。ただし、行の点と列の点の近さを、そのまま関連の強さとは読めません。 座標の尺度、表の内訳、どの距離を保存した図かを先に確かめます。
この記事では旧来の「赤/青×好き/普通/嫌い」の教材を1軸で読み直し、別の架空3×3表で2軸の意味を計算します。実在する購入者調査や施策の効果ではありません。
30秒で要点
- 入力は同じ観測単位から作った非負の度数表。行・列の合計が0のカテゴリは、そのまま座標計算に入れません。
- 3×2表の非自明な軸は最大1本です。「二次元の図」を作ることを目的にしないでください。
- 行同士は行プロファイル、列同士は列プロファイルを比較します。主座標で全有効軸を使えば、それぞれのχ²距離を再現できます。
- 軸の寄与率、点の寄与、点のcos²は別の指標です。遠い点や大きな点を見つけただけでは解釈は決まりません。
- 見つけた違いは仮説の候補です。広告・訴求の効果は、別に検証します。
元の3×2表で、人数と割合を分ける
1人が1つの色と1つの評価に分類された、という架空の130人を考えます。行は評価、列は色です。単位を変えた売上金額や複数回答を、同じ人数表であるかのように混ぜないことが出発点です。
表が収まらない場合は、横にスクロールできます
| 評価 | 赤 | 青 | 行合計 | 赤の行内割合 |
|---|---|---|---|---|
| 好き | 30 | 15 | 45 | 66.67% |
| 普通 | 20 | 30 | 50 | 40.00% |
| 嫌い | 10 | 25 | 35 | 28.57% |
| 列合計 | 60 | 70 | 130 | 46.15% |
「好き」の赤の割合は30/45です。赤の人数30だけを「普通」の20と比べると、行の大きさと内訳が混ざります。行を合計1にしたベクトルを行プロファイルと呼びます。全体の赤/青の割合は60/130と70/130です。
独立な構成を基準にすると、「好き×赤」の期待度数は 45×60/130 ≈ 20.7692。全6セルについて χ² = Σ (観測度数 - 期待度数)² / 期待度数 を足すと、χ² = 624/49 ≈ 12.734694 です。CAの総慣性は χ²/N = 24/245 ≈ 0.097959 になります。
これは表の独立からのずれを記述する計算です。ここでは架空表からp値や母集団の結論を出しません。検定をするなら、標本抽出や観測の独立性を別に確認します。
3×2表を2軸にしても、情報は増えない
通常のCAの有効次元の上限は min(行数 - 1, 列数 - 1)。この表では min(2, 1) = 1 です。1軸が総慣性の100%を持っていても、「市場を完全に説明した」という意味ではありません。この表のプロファイル差が1軸に収まるという意味です。
計算では、度数を総数で割った相対度数行列Pと、行・列の質量r、cを使います。質量は各行・列の合計が全体に占める割合です。標準化残差 S(i,j) = (P(i,j) - r(i)c(j)) / √(r(i)c(j)) を特異値分解(SVD)し、特異値の2乗を各軸の慣性とします。
この計算関係と座標の種類は、CAの理論付録とcaの実装で確認できます。特異ベクトルの符号は反転できるので、左右が入れ替わっただけで別の結論になったとは考えません。
2軸の教材では「何を説明できたか」を数で見る
次は2軸を区別できるように作った別の架空150人です。先ほどの130人を追加調査した結果ではありません。行A/B/Cを評価群、列X/Y/Zを選択肢とします。
表が収まらない場合は、横にスクロールできます
| 評価群 | X | Y | Z | 合計 |
|---|---|---|---|---|
| A | 30 | 15 | 5 | 50 |
| B | 15 | 20 | 15 | 50 |
| C | 5 | 15 | 30 | 50 |
| 合計 | 50 | 50 | 50 | 150 |
各期待度数は50/3。この例の特異値は0.5と0.1、軸の慣性は0.25と0.01です。総慣性0.26、χ²は39。第1軸は 0.25/0.26 ≈ 96.15%、第2軸は約3.85%を占めます。
図では第1軸の符号をAが正になる向きにそろえました。点の主座標は次のとおりです。この対称な教材では行と列の座標が同じになります。
表が収まらない場合は、横にスクロールできます
| 行点 / 列点 | 第1軸 | 第2軸 | 行の第1軸への寄与 | 行の第1軸のcos² |
|---|---|---|---|---|
| A / X | 0.612372 | 0.070711 | 50.00% | 98.68% |
| B / Y | 0.000000 | -0.141421 | 0.00% | 0.00% |
| C / Z | -0.612372 | 0.070711 | 50.00% | 98.68% |
第1軸が全体の96.15%を説明しても、Bの特徴は第1軸には表れません。 Bの原点からのずれはすべて第2軸にあり、その軸への寄与は66.67%です。全体として小さい軸を機械的に捨てると、対象とする点の違いを落とす場合があります。
距離・寄与・cos²を混同しない
表が収まらない場合は、横にスクロールできます
| 見るもの | 答える問い | 読むときの条件 |
|---|---|---|
| 行同士のχ²距離 | 行の構成比はどれだけ違うか | 列質量で重み付け。行主座標の全有効軸なら距離を再現、低次元なら近似 |
| 列同士のχ²距離 | 列の構成比はどれだけ違うか | 行質量で重み付け。列主座標の尺度を確認 |
| 軸の慣性 / 総慣性 | 表全体のずれをその軸がどれだけ担うか | 総慣性0なら、この割合は定義できない |
| 点の軸への寄与 | その軸を作るうえで点がどれだけ重いか | 行では r(i)×F(i,k)²/λ(k)。距離だけでなく質量が効く |
| 点のcos² | 点の原点からのずれをその軸がどれだけ表すか | F(i,k)²/Σ F(i,l)²。原点そのものの点では定義できない |
例えば行Aと行Bのプロファイルは (0.6,0.3,0.1) と (0.3,0.4,0.3)。列質量が各1/3なので、χ²距離の2乗は (0.3²+(-0.1)²+(-0.2)²)/(1/3) = 0.42。主座標でも 0.612372² + (0.070711+0.141421)² ≈ 0.42 です。
この等式は行同士についてのものです。両方の主座標を重ねた対称マップで、行Aと列Xが近いことを同じ距離の意味で読まないでください。MASSの説明も、その等尺度プロットの行・列スコア間の解釈に注意を促しています。組合せを見るときは元のセル・行内割合・独立期待値に戻ります。
点の寄与とcos²の計算はcaのsummary実装でも分かれています。PrinceのCA資料も座標・寄与・cosine similaritiesを別に出力します。数字の名前だけでなく、分母が何かを確認してください。
Pythonで旧表と追加表を確かめる
以下はNumPyだけで、標準化残差のSVDから行・列主座標までを計算する独立した教材です。旧ミニコードの「クロス表から座標を得る」役割を残し、3×2表に2つの有効軸を要求しない形にしました。
import numpy as np
def ca(counts):
n = np.asarray(counts, dtype=float)
assert np.isfinite(n).all() and (n >= 0).all()
assert (n.sum(axis=0) > 0).all() and (n.sum(axis=1) > 0).all()
p = n / n.sum()
r, c = p.sum(axis=1), p.sum(axis=0)
s = (p - np.outer(r, c)) / np.sqrt(np.outer(r, c))
u, values, vt = np.linalg.svd(s, full_matrices=False)
keep = values > 1e-12 # この教材の数値誤差用。一般の有意性判定ではない
u, values, v = u[:, keep], values[keep], vt.T[:, keep]
for k in range(len(values)):
if u[np.argmax(np.abs(u[:, k])), k] < 0:
u[:, k] *= -1
v[:, k] *= -1
f = u * values / np.sqrt(r[:, None])
g = v * values / np.sqrt(c[:, None])
inertia = values ** 2
chi2 = n.sum() * inertia.sum()
return inertia, f, g, chi2
old = [[30, 15], [20, 30], [10, 25]]
new = [[30, 15, 5], [15, 20, 15], [5, 15, 30]]
for label, table in [("old", old), ("new", new)]:
inertia, rows, cols, chi2 = ca(table)
print(label, "axes", len(inertia), "inertia", inertia, "chi2", chi2)
print("row principal coordinates", rows)
print("column principal coordinates", cols)
assert len(ca(old)[0]) == 1
np.testing.assert_allclose(ca(old)[0], [24 / 245])
np.testing.assert_allclose(ca(new)[0], [0.25, 0.01])
np.testing.assert_allclose(ca(new)[3], 39)
np.testing.assert_allclose(ca(2 * np.array(new))[0], ca(new)[0])
出力は旧表が1軸・慣性約0.097959・χ²約12.734694、新表が2軸・慣性0.25/0.01・χ²39です。軸の符号は環境や同率の選び方で変わり得ます。座標を比較するときは符号を対応させ、同じ尺度かも確認します。
Princeを使う場合のパッケージ名は公式READMEでは princeです。旧例のprince-caという導入名は引き継ぎません。本記事の実行教材は上のNumPy版であり、Prince APIの実行結果として表示していません。
ケースを施策へつなぐ前の3問
色×評価のケースなら、図を見て訴求を決める前に「どの対象者の、どの割合を比較したいか」を決めます。赤が好きな層に広告を出す判断と、広告によって赤の評価を上げられるかという問いは異なります。
- 1つの行が極端に大きいときは? 行の人数と行内割合を並べ、母集団構成・重複集計・欠測の扱いを確認します。質量は解析の一部です。見やすくするためだけに人数をそろえると問いが変わります。カテゴリ統合にも内容上の理由が必要です。
- 行と列を混同しそうなときは? まず同種類の点だけを表示し、行なら行プロファイルの表に戻ります。異種点の組合せは元セルと期待値で確かめます。
- 第1・第2軸の割合が低いときは? 残りの軸と各点のcos²を確認します。今回の3×3教材には第3の非自明軸はありません。図に残らなかった構造を、配置だけで断定しないでください。
5分で確認する、分析の前提と次の検証
目的は「図を作る」ではなく、比較したい組合せを明確にすることです。次の3点を短く書けなければ、その点を補ってから進みます。
- 目的・判断軸:誰に対する何の判断か。割合差、費用、対象人数のどれが意思決定に必要か。
- データ:1件の単位、期間、カテゴリ、欠測・重複・複数回答を説明できるか。ゼロ行列和を除く場合も理由を残す。
- 検証の循環:図で作った仮説を、新しい対象や適切に設計した比較でどう確かめるか。探索図を施策効果の証拠にしない。
全セルの度数を一律2倍にしてもP、質量、プロファイル、慣性は変わりません。一方、χ²は2倍になります。ある行だけを増やす場合は構成が変わるので別問題です。「規模が違えば標準化すればよい」と一括りにしないことが、旧ケースを判断に使う要点です。
距離からの配置を比較したいならMDS、検定の条件はカイ二乗検定、まとめ方の目的はクラスター分析へ進めます。連続変数の軸を考えるPCAとの違いも確認し、全体の選択は統計の判断ハブで整理してください。