見出し画像

【Python入門:主成分分析編#3】主成分はいくつ残す? 寄与率とスクリープロットで決める

10個の主成分、全部使う?

前回までで、PCAの仕組みを理解しました。共分散行列を固有値分解すれば、主成分の方向と、その方向での分散が求まるのでしたね。

でも、ここで新たな疑問が生まれます。

元のデータが10次元なら、主成分も10個得られます。では、その10個全部使いますか?

...それでは次元削減になりません。

かといって、1個だけでは情報が失われすぎるかもしれません。

「結局、いくつ残せばいいの?」

今回は、この問いに答えるための道具を学びます。キーワードは「寄与率」と「スクリープロット」です。


🎯 この記事で学べること

  • 寄与率・累積寄与率の意味と計算方法

  • スクリープロットの描き方と読み方

  • 主成分数を決める3つの基準(肘法・累積寄与率・カイザー基準)

  • 実際にIrisデータを2次元に圧縮して可視化する



寄与率って何?売上分析で考えてみよう

寄与率を理解するために、ビジネスの例で考えてみましょう。

ある会社の売上に影響する要因として、「広告費」「営業人数」「商品数」「立地」「価格」など10個の変数があるとします。

PCAを適用すると、10個の主成分が得られます。

もし第1主成分が売上変動の40%を説明し、第2主成分が25%を説明するとしたら、この2つだけで全体の65%を説明できることになります。残りの8個は合わせて35%しか説明しません。

この「全体の分散のうち、各主成分が何%を説明するか」を表す指標が寄与率(explained variance ratio)です。

数式で書くと、こうなります。

$$
\text{第k主成分の寄与率} = \frac{\lambda_k}{\sum_{i=1}^{p} \lambda_i}
$$

  • 分子:第k主成分の固有値(その方向での分散)

  • 分母:すべての固有値の合計(全分散)

つまり、固有値が大きい主成分ほど、情報をたくさん持っているということです。


準備:Irisデータセットを使う

今回は、機械学習の定番データセット「Iris(アヤメ)」を使います。

3種類のアヤメ(Setosa、Versicolor、Virginica)について、4つの特徴量が測定されています。

以下、コードです。

import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib # 日本語表示

np.random.seed(42)

# データ読み込み
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
target_names = iris.target_names

# DataFrameで確認
df_iris = pd.DataFrame(X, columns=feature_names)
df_iris['species'] = [target_names[i] for i in y]

print(f"サンプル数: {len(df_iris)}")
print(f"特徴量: {feature_names}")
print(f"品種: {list(target_names)}")
print()
print(df_iris.head())

以下、実行結果です。

サンプル数: 150
特徴量: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
品種: ['setosa', 'versicolor', 'virginica']

sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) \
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2

species
0 setosa
1 setosa
2 setosa
3 setosa
4 setosa

150サンプル、4次元のデータです。これを2次元に圧縮して、3品種がどう分布するか可視化するのが今回の目標です。

データの標準化

PCAの前に、データを標準化(平均0、標準偏差1に変換)します。変数ごとにスケールが異なる場合、これは必須の前処理です。

以下、コードです。

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
​
print("標準化後の平均:", X_scaled.mean(axis=0).round(2))
print("標準化後の標準偏差:", X_scaled.std(axis=0).round(2))

以下、実行結果です。

標準化後の平均: [-0. -0. -0. -0.]
標準化後の標準偏差: [1. 1. 1. 1.]


共分散行列の固有値分解

標準化したデータで固有値分解を行います。

以下、コードです。

# 共分散行列を計算
n = len(X_scaled)
cov_matrix = (X_scaled.T @ X_scaled) / (n - 1)

# 固有値分解
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
​
# 固有値が大きい順にソート
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx].real
eigenvectors = eigenvectors[:, idx].real
​
print("固有値(各主成分の分散):")
for i, ev in enumerate(eigenvalues):
    print(f"  第{i+1}主成分: {ev:.4f}")

以下、実行結果です。

固有値(各主成分の分散):
第1主成分: 2.9381
第2主成分: 0.9202
第3主成分: 0.1477
第4主成分: 0.0209

4つの固有値が表示されます。第1主成分が最も大きく、順に小さくなっていきます。


📊 寄与率と累積寄与率を計算する

いよいよ寄与率の計算です。

以下、コードです。

# 寄与率を計算
total_variance = np.sum(eigenvalues)
explained_ratio = eigenvalues / total_variance

# 累積寄与率を計算
cumulative_ratio = np.cumsum(explained_ratio)

print(f"{'主成分':<8} {'固有値':>10} {'寄与率':>10} {'累積寄与率':>10}")
print("-" * 45)
for i in range(len(eigenvalues)):
    print(
        f"PC{i+1:<6} {eigenvalues[i]:>10.4f} "
        f"{explained_ratio[i]*100:>9.2f}% "
        f"{cumulative_ratio[i]*100:>9.2f}%")

以下、実行結果です。

主成分   固有値   寄与率   累積寄与率
---------------------------------------------
PC1      2.9381     72.96%     72.96%
PC2      0.9202     22.85%     95.81%
PC3      0.1477       3.67%     99.48%
PC4      0.0209       0.52%    100.00%

結果を見ると、第1・第2主成分だけで累積寄与率が約95%に達しています。

これは、「4次元のデータを2次元に減らしても、情報の95%は保持できる」ということです。すごい圧縮率ですね!


📈 スクリープロットを描く

スクリープロット(scree plot)は、固有値や寄与率を主成分の順にグラフ化したものです。

「scree」は山の斜面にたまった岩屑のこと。グラフの形が岩屑の斜面に似ていることから名付けられました。

以下、コードです。

fig, axes = plt.subplots(2, 1, figsize=(10, 10))

# 上: 固有値のスクリープロット
ax = axes[0]
x = np.arange(1, len(eigenvalues) + 1)
ax.bar(
    x, eigenvalues, 
    alpha=0.7, color='steelblue'
)
ax.plot(
    x, eigenvalues, 
    'ro-', markersize=8, linewidth=2
)
ax.set_xlabel('主成分')
ax.set_ylabel('固有値')
ax.set_title('スクリープロット(固有値)')
ax.set_xticks(x)
ax.set_xticklabels([f'PC{i}' for i in x])
ax.grid(True, axis='y', alpha=0.3)

# 下: 寄与率と累積寄与率
ax = axes[1]
ax.bar(
    x, explained_ratio * 100, 
    alpha=0.7, color='steelblue', 
    label='寄与率'
)
ax.plot(
    x, cumulative_ratio * 100, 
    'ro-', markersize=8, linewidth=2, 
    label='累積寄与率'
)
ax.axhline(
    y=80, 
    color='green', linestyle='--', linewidth=2, 
    label='80%ライン'
)
ax.set_xlabel('主成分')
ax.set_ylabel('寄与率 (%)')
ax.set_title('寄与率と累積寄与率')
ax.set_xticks(x)
ax.set_xticklabels([f'PC{i}' for i in x])
ax.set_ylim(0, 105)
ax.legend(loc='center right')
ax.grid(True, axis='y', alpha=0.3)

plt.tight_layout()
plt.show()

以下、実行結果です。

上のグラフでは、第1・第2主成分の固有値が大きく、第3以降は急に小さくなっています。

下のグラフでは、累積寄与率が80%を超えるポイントが一目でわかります。


🔍 主成分数を決める3つの基準

1. 肘法(エルボー法)

スクリープロットの形を見て、曲線が急に平らになる「肘(elbow)」の位置で打ち切る方法です。

先ほどのグラフでは、第2主成分と第3主成分の間で傾きが急に緩やかになっています。この「肘」の位置で打ち切り、第1・第2主成分を採用します。

メリット: 視覚的にわかりやすい デメリット: 肘がはっきり見えないこともある

2. 累積寄与率による判断

最もよく使われる方法です。「どの程度まで情報を残したいか」を決めて、その割合を超える最小の主成分数を採用します。

よく使われる目安は以下の通りです。

  • 可視化・簡易分析: 80%程度

  • 分析の前処理: 90%前後

  • 情報損失を最小限に: 95%程度

今回の例では、2主成分で約95%に達しているので、「95%を保持する」という基準なら主成分数は2と判断できます。

3. カイザー基準(固有値 > 1)

標準化したデータでは、「固有値が1より大きい主成分のみを採用する」という経験則があります。

固有値1未満の主成分は、元の変数1個分にも満たない情報しか持たないという考え方です。

今回は第1主成分の固有値が1を大きく上回り、第2主成分は1未満。この基準だと第1主成分のみという、かなり厳しめの判断になります。

どれが正解?

唯一の正解はありません。

分析の目的、データの性質、後続の処理との兼ね合いで判断します。

今回は、肘法と累積寄与率(95%)の両方が「2主成分で十分」を支持しているので、2主成分を採用するのが妥当でしょう。


🎨 主成分得点:データを新しい座標系で見る

主成分の方向が分かったら、各データ点をその方向に投影します。この投影後の座標を主成分得点(principal component score)と呼びます。

数式で書くと、こうなります。

$$
Z = X \cdot V
$$

  • X: 中心化されたデータ

  • V: 固有ベクトルを並べた行列

  • Z: 主成分得点(次元削減後のデータ)

以下、コードです。

# 上位2つの固有ベクトルを取得
W = eigenvectors[:, :2]  # 4×2の行列
​
# 主成分得点を計算
Z = X_scaled @ W  # 150×2の行列
​
print("主成分得点の形状:", Z.shape)

以下、実行結果です。

主成分得点の形状: (150, 2)

4次元から2次元に圧縮されました!


散布図で3品種を可視化する

いよいよ、圧縮したデータを散布図にプロットします。

以下、コードです。

plt.figure(figsize=(10, 8))

colors = ['#E74C3C', '#2ECC71', '#3498DB']
for i, (species, color) in enumerate(zip(target_names, colors)):
    mask = y == i
    plt.scatter(
        Z[mask, 0], Z[mask, 1],
        c=color, label=species,
        s=80, alpha=0.7, edgecolors='white'
    )

plt.xlabel(f'第1主成分 (寄与率: {explained_ratio[0]*100:.1f}%)')
plt.ylabel(f'第2主成分 (寄与率: {explained_ratio[1]*100:.1f}%)')
plt.title('Irisデータセットの主成分分析')
plt.legend(title='品種')
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color='gray', linestyle='-', linewidth=0.5)
plt.axvline(x=0, color='gray', linestyle='-', linewidth=0.5)
plt.show()

以下、実行結果です。

3品種がかなり明確に分離されています!

特にSetosa(赤)は他の2品種と完全に分離。VersicolorとVirginicaも、多少の重なりはあるものの、おおむね区別できます。

4次元のデータを直接見ることはできませんが、2次元に圧縮することで、データの構造が目で見えるようになりました。


📊 主成分負荷量:どの変数が効いているか

各特徴量が主成分にどれくらい寄与しているかを見てみましょう。これを主成分負荷量(loading)と呼びます。

$$
\text{負荷量} = \sqrt{\lambda_k} \cdot v_k
$$

固有ベクトル(方向)に、標準偏差(広がり)を掛け合わせたものです。

以下、コードです。

# 主成分負荷量を計算
loadings = pd.DataFrame(
    eigenvectors[:, :2] * np.sqrt(eigenvalues[:2]),
    index=feature_names,
    columns=['PC1', 'PC2']
)
​
print("主成分負荷量:")
print(loadings.round(3))

以下、実行結果です。

主成分負荷量:
                                   PC1        PC2
sepal length (cm)      0.893      -0.362
sepal width (cm)      -0.462      -0.886
petal length (cm)       0.995      -0.023
petal width (cm)         0.968      -0.064

  • PC1: petal length、petal width、sepal lengthの負荷量の絶対値が大きい → 「花のサイズ全般」を表す軸

  • PC2: sepal widthの負荷量の絶対値が大きい → 「がく片の幅」を表す軸

バイプロット:サンプルと変数を同時に可視化

主成分得点(サンプルの位置)と主成分負荷量(変数の寄与)を同じグラフに描いたものをバイプロットと呼びます。

以下、コードです。

plt.figure(figsize=(10, 8))
​
# サンプルをプロット
for i, (species, color) in enumerate(zip(target_names, colors)):
    mask = y == i
    plt.scatter(Z[mask, 0], Z[mask, 1],
                c=color, label=species,
                s=80, alpha=0.7, edgecolors='white')
​
# 変数の矢印をプロット
scale = 2  # 矢印の見やすさ調整
loadings_scaled = loadings * scale
for i in range(len(loadings)):
    plt.arrow(0, 0, 
              loadings_scaled.iloc[i, 0], 
              loadings_scaled.iloc[i, 1],
              color='black', alpha=0.6, head_width=0.05)
    plt.text(loadings_scaled.iloc[i, 0] * 1.1,
             loadings_scaled.iloc[i, 1] * 1.1,
             loadings.index[i], fontsize=9)
​
plt.xlabel(f'第1主成分 (寄与率: {explained_ratio[0]*100:.1f}%)')
plt.ylabel(f'第2主成分 (寄与率: {explained_ratio[1]*100:.1f}%)')
plt.title('バイプロット')
plt.legend(title='品種')
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color='gray', linestyle='-', linewidth=0.5)
plt.axvline(x=0, color='gray', linestyle='-', linewidth=0.5)
plt.show()

以下、実行結果です。

矢印の読み方は以下の通りです。

  • 矢印が長い: その変数は主成分によってよく説明されている

  • 矢印の向き: 主成分への正・負の影響

  • 矢印同士の角度: 鋭角なら正の相関、鈍角なら負の相関


まとめ

今回は「主成分をいくつ残すか」を決める方法を学びました。

寄与率は、各主成分が全体の分散の何%を説明するかを表します。固有値を全固有値の合計で割って計算します。

累積寄与率は、上位k個の主成分で全体の何%の情報を保持できるかを表します。

スクリープロットは、固有値や寄与率をグラフ化して、主成分の重要度を視覚的に把握するツールです。

主成分数を決める基準には、累積寄与率(80〜95%)、カイザー基準(固有値>1)、肘法(エルボー法)などがあります。

主成分得点は、データを主成分方向に投影した座標です。これが次元削減後のデータになります。

主成分負荷量は、各変数が主成分にどれだけ寄与しているかを表します。


📚 参考にした情報源


次回予告

ここまで手計算でPCAの仕組みを理解してきました。

でも実務では、毎回こんな計算はしません。ライブラリを使えば数行で終わります。

次回は、scikit-learnを使ってPCAをサクッと実行する方法を学びます。手計算の知識があるからこそ、ライブラリの出力が理解できるようになります。


最後まで読んでいただきありがとうございました! 「スキ」を押していただけると励みになります 🙌


いいなと思ったら応援しよう!