第12回 次元削減
はじめに
本シリーズ「Pythonで学ぶデータ分析実践」の最終回となる第12回では、次元削減について解説します。
実際のデータ分析では、数十〜数百の特徴量(変数)を持つデータを扱うことが珍しくありません。しかし、変数が多すぎると計算コストの増大、過学習、可視化の困難さなどの問題が生じます。次元削減は、データの重要な情報を保ちながら変数の数を減らす技術です。
本記事では、最も広く使われる主成分分析(PCA)を中心に、寄与率による次元数の決定方法と、低次元空間での可視化手法を実践的に解説します。
シリーズ全体の構成(全12回)
| 回 | テーマ |
|---|---|
| 第1回 | データ分析とは |
| 第2回 | データの前処理 |
| 第3回 | データの把握 |
| 第4回 | 相関分析 |
| 第5回 | 統計的推定 |
| 第6回 | 統計的検定 |
| 第7回 | 分散分析 |
| 第8回 | 回帰分析 |
| 第9回 | 時系列データ分析 |
| 第10回 | クラス分類 |
| 第11回 | クラスタリング |
| 第12回 | 次元削減(本記事) |
12-1. 次元削減とは
次元削減とは、データが持つ重要な情報(分散)をできるだけ保持しながら、特徴量の数(次元数)を減らす手法です。
なぜ次元削減が必要か
| 問題 | 説明 |
|---|---|
| 次元の呪い | 高次元ではデータがスパースになり、距離ベースの手法が機能しにくい |
| 計算コスト | 特徴量が多いほど学習に時間がかかる |
| 過学習 | 特徴量が多すぎるとノイズも学習してしまう |
| 可視化の困難さ | 人間は3次元までしか直感的に理解できない |
| 多重共線性 | 相関の高い変数が多いと回帰分析が不安定になる |
次元削減の手法
| 手法 | 種類 | 特徴 |
|---|---|---|
| 主成分分析(PCA) | 線形 | 最も広く使われる。分散最大化 |
| カーネルPCA | 非線形 | 非線形構造を持つデータに対応 |
| t-SNE | 非線形 | 高次元データの可視化に特化 |
| UMAP | 非線形 | t-SNEの高速版。構造保持性が高い |
| LDA | 線形 | 教師ありの次元削減(クラス間分離最大化) |
12-2. 主成分分析(PCA)
**主成分分析(PCA: Principal Component Analysis)**は、データの分散が最大となる方向(主成分)を見つけ、その方向に射影することで次元を削減する手法です。
PCAの仕組み
1. データを標準化する(平均0、分散1)
2. 共分散行列を計算する
3. 共分散行列の固有値と固有ベクトルを求める
4. 固有値の大きい順に固有ベクトルを並べる
5. 上位k個の固有ベクトルで新たな座標系を構成する
6. 元データを新座標系に射影する
主成分の意味
- 第1主成分: データの分散が最大となる方向
- 第2主成分: 第1主成分と直交し、残りの分散が最大となる方向
- 第k主成分: 第1〜第(k-1)主成分と直交し、残りの分散が最大となる方向
Pythonでの実装
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import japanize_matplotlib
# データの準備
iris = load_iris()
X = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target
# 標準化(PCAの前に必須)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# PCAの実行(全主成分を計算)
pca = PCA()
X_pca = pca.fit_transform(X_scaled)
print("=== PCA結果 ===")
print(f"元の次元数: {X.shape[1]}")
print(f"固有値(分散): {pca.explained_variance_}")
print(f"寄与率: {pca.explained_variance_ratio_}")
print(f"累積寄与率: {np.cumsum(pca.explained_variance_ratio_)}")
主成分負荷量(各特徴量の寄与)
# 主成分負荷量(各特徴量が各主成分にどの程度寄与するか)
loadings = pd.DataFrame(
pca.components_.T,
columns=[f"PC{i+1}" for i in range(4)],
index=iris.feature_names
)
print("\n=== 主成分負荷量 ===")
print(loadings.round(4))
# ヒートマップで可視化
import seaborn as sns
plt.figure(figsize=(8, 5))
sns.heatmap(loadings, annot=True, cmap='coolwarm', fmt='.3f',
vmin=-1, vmax=1, center=0)
plt.title("主成分負荷量(各特徴量の主成分への寄与)")
plt.tight_layout()
plt.show()
12-3. 寄与率
寄与率は、各主成分がデータの全分散のうちどの程度を説明するかを表す指標です。累積寄与率を見て、何次元に削減するかを決定します。
寄与率の定義
$$寄与率_k = \frac{\lambda_k}{\sum_{i=1}^{p} \lambda_i}$$
- $\lambda_k$: 第k主成分の固有値(分散)
- p: 元の次元数
次元数の決定基準
| 基準 | 説明 |
|---|---|
| 累積寄与率80%以上 | 全分散の80%以上を説明できる次元数を選ぶ |
| 累積寄与率90%以上 | より保守的な基準 |
| 固有値1以上(カイザー基準) | 固有値が1以上の主成分のみ採用 |
| スクリープロット | 固有値の減少が鈍化する点で切る |
寄与率の可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 寄与率の棒グラフ
axes[0].bar(range(1, 5), pca.explained_variance_ratio_,
color='steelblue', alpha=0.8, edgecolor='black')
axes[0].set_xlabel("主成分", fontsize=12)
axes[0].set_ylabel("寄与率", fontsize=12)
axes[0].set_title("各主成分の寄与率", fontsize=13)
axes[0].set_xticks(range(1, 5))
axes[0].grid(axis='y', alpha=0.3)
for i, v in enumerate(pca.explained_variance_ratio_):
axes[0].text(i+1, v+0.01, f'{v:.3f}', ha='center', fontsize=10)
# 累積寄与率の折れ線グラフ
cum_ratio = np.cumsum(pca.explained_variance_ratio_)
axes[1].plot(range(1, 5), cum_ratio, 'o-', color='steelblue',
markersize=10, linewidth=2)
axes[1].axhline(0.8, color='red', linestyle='--', alpha=0.7, label='80%ライン')
axes[1].axhline(0.9, color='orange', linestyle='--', alpha=0.7, label='90%ライン')
axes[1].set_xlabel("主成分数", fontsize=12)
axes[1].set_ylabel("累積寄与率", fontsize=12)
axes[1].set_title("累積寄与率", fontsize=13)
axes[1].set_xticks(range(1, 5))
axes[1].set_ylim(0, 1.05)
axes[1].legend(fontsize=10)
axes[1].grid(alpha=0.3)
for i, v in enumerate(cum_ratio):
axes[1].text(i+1.1, v, f'{v:.3f}', fontsize=10)
plt.tight_layout()
plt.show()
# 次元数の決定
for threshold in [0.8, 0.9, 0.95]:
n_components = np.argmax(cum_ratio >= threshold) + 1
print(f"累積寄与率{threshold*100:.0f}%以上 → {n_components}次元で十分")
実践:指定した次元数でPCA
# 2次元に削減
pca_2d = PCA(n_components=2)
X_2d = pca_2d.fit_transform(X_scaled)
print(f"\n=== 2次元PCA ===")
print(f"累積寄与率: {sum(pca_2d.explained_variance_ratio_):.4f}")
print(f"(元データの情報の{sum(pca_2d.explained_variance_ratio_)*100:.1f}%を保持)")
12-4. 可視化
次元削減の最も重要な応用の一つが、高次元データの2次元・3次元での可視化です。
PCAによる2次元可視化
plt.figure(figsize=(8, 6))
for i, name in enumerate(iris.target_names):
mask = y == i
plt.scatter(X_2d[mask, 0], X_2d[mask, 1], s=60, alpha=0.7,
edgecolors='black', linewidths=0.5, label=name)
plt.xlabel(f"第1主成分(寄与率: {pca_2d.explained_variance_ratio_[0]:.3f})", fontsize=11)
plt.ylabel(f"第2主成分(寄与率: {pca_2d.explained_variance_ratio_[1]:.3f})", fontsize=11)
plt.title("PCAによるIrisデータの2次元可視化", fontsize=13)
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.show()
バイプロット(データ点と主成分負荷量の同時表示)
fig, ax = plt.subplots(figsize=(9, 7))
# データ点のプロット
for i, name in enumerate(iris.target_names):
mask = y == i
ax.scatter(X_2d[mask, 0], X_2d[mask, 1], s=50, alpha=0.6,
edgecolors='black', linewidths=0.5, label=name)
# 主成分負荷量の矢印
scale = 3
for j, feature in enumerate(iris.feature_names):
ax.arrow(0, 0, pca_2d.components_[0, j]*scale, pca_2d.components_[1, j]*scale,
head_width=0.05, head_length=0.03, fc='red', ec='red', alpha=0.8)
ax.text(pca_2d.components_[0, j]*scale*1.15, pca_2d.components_[1, j]*scale*1.15,
feature, color='red', fontsize=9, ha='center')
ax.set_xlabel("第1主成分", fontsize=11)
ax.set_ylabel("第2主成分", fontsize=11)
ax.set_title("バイプロット(データ点 + 特徴量の寄与方向)", fontsize=13)
ax.legend(fontsize=10)
ax.grid(alpha=0.3)
ax.axhline(0, color='gray', linewidth=0.5)
ax.axvline(0, color='gray', linewidth=0.5)
plt.tight_layout()
plt.show()
t-SNEによる可視化
PCAは線形変換のため、非線形構造を持つデータの可視化にはt-SNEが有効です。
from sklearn.manifold import TSNE
# t-SNEで2次元に変換
tsne = TSNE(n_components=2, random_state=42, perplexity=30)
X_tsne = tsne.fit_transform(X_scaled)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# PCA
for i, name in enumerate(iris.target_names):
mask = y == i
axes[0].scatter(X_2d[mask, 0], X_2d[mask, 1], s=50, alpha=0.7,
edgecolors='black', linewidths=0.5, label=name)
axes[0].set_title("PCA", fontsize=13)
axes[0].set_xlabel("第1主成分")
axes[0].set_ylabel("第2主成分")
axes[0].legend()
axes[0].grid(alpha=0.3)
# t-SNE
for i, name in enumerate(iris.target_names):
mask = y == i
axes[1].scatter(X_tsne[mask, 0], X_tsne[mask, 1], s=50, alpha=0.7,
edgecolors='black', linewidths=0.5, label=name)
axes[1].set_title("t-SNE", fontsize=13)
axes[1].set_xlabel("t-SNE 次元1")
axes[1].set_ylabel("t-SNE 次元2")
axes[1].legend()
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.show()
PCAとt-SNEの使い分け
| 項目 | PCA | t-SNE |
|---|---|---|
| 変換の種類 | 線形 | 非線形 |
| 目的 | 次元削減 + 可視化 | 可視化に特化 |
| 逆変換 | 可能 | 不可能 |
| 新規データへの適用 | 可能(transform) | 不可能(再学習が必要) |
| 計算速度 | 高速 | 遅い(大規模データ注意) |
| グローバル構造 | 保持される | 保持されにくい |
| ローカル構造 | 保持されにくい | よく保持される |
補足:次元削減の実践的な活用
PCAによる前処理(機械学習パイプライン)
from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
# PCA + ロジスティック回帰のパイプライン
pipe = Pipeline([
('scaler', StandardScaler()),
('pca', PCA(n_components=2)),
('classifier', LogisticRegression(max_iter=1000))
])
# 交差検証で評価
scores = cross_val_score(pipe, X, y, cv=5, scoring='accuracy')
print(f"=== PCA(2次元) + ロジスティック回帰 ===")
print(f"正解率: {scores.mean():.4f} ± {scores.std():.4f}")
# 次元数を変えて比較
print(f"\n=== 次元数と正解率の関係 ===")
for n in [1, 2, 3, 4]:
pipe.set_params(pca__n_components=n)
scores = cross_val_score(pipe, X, y, cv=5, scoring='accuracy')
print(f" {n}次元: {scores.mean():.4f} ± {scores.std():.4f}")
大規模データでの活用例
from sklearn.datasets import fetch_openml
# 手書き数字データ(64次元 → 2次元で可視化)
from sklearn.datasets import load_digits
digits = load_digits()
X_digits = digits.data # 64特徴量(8×8ピクセル)
y_digits = digits.target
# PCAで2次元に削減
scaler_d = StandardScaler()
X_d_scaled = scaler_d.fit_transform(X_digits)
pca_digits = PCA(n_components=2)
X_d_2d = pca_digits.fit_transform(X_d_scaled)
print(f"元の次元数: {X_digits.shape[1]}")
print(f"削減後: 2次元")
print(f"累積寄与率: {sum(pca_digits.explained_variance_ratio_):.4f}")
# 可視化
plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_d_2d[:, 0], X_d_2d[:, 1], c=y_digits,
cmap='tab10', s=10, alpha=0.6)
plt.colorbar(scatter, label='数字')
plt.xlabel("第1主成分")
plt.ylabel("第2主成分")
plt.title("手書き数字データのPCA可視化(64次元→2次元)")
plt.grid(alpha=0.3)
plt.show()
まとめ
本記事では、次元削減として以下の内容を解説しました。
| 項目 | ポイント |
|---|---|
| 次元削減とは | データの情報を保持しながら変数の数を減らす技術 |
| PCA | 分散最大化による線形次元削減。最も広く使用 |
| 寄与率 | 各主成分の説明力。累積80〜90%を基準に次元数を決定 |
| 可視化 | PCAで全体構造、t-SNEで局所構造を把握 |
チェックリスト
- PCAの前にデータを標準化したか
- 寄与率と累積寄与率を確認したか
- 適切な次元数を選択したか
- 主成分負荷量で各主成分の意味を解釈したか
- バイプロットで特徴量の寄与方向を確認したか
- 必要に応じてt-SNEやUMAPで非線形構造を確認したか
シリーズ全体のまとめ
全12回を通じて、データ分析の基礎から実践までを体系的に学んできました。
| 回 | テーマ | キーワード |
|---|---|---|
| 第1回 | データ分析とは | CRISP-DM、ビッグデータ、Python |
| 第2回 | データの前処理 | 欠損値、外れ値、スケーリング |
| 第3回 | データの把握 | 基本統計量、ヒストグラム、正規分布 |
| 第4回 | 相関分析 | 相関係数、相関比、連関係数 |
| 第5回 | 統計的推定 | 点推定、区間推定、信頼区間 |
| 第6回 | 統計的検定 | p値、t検定、カイ二乗検定 |
| 第7回 | 分散分析 | F値、多重比較、交互作用 |
| 第8回 | 回帰分析 | 単回帰、重回帰、GLM、R² |
| 第9回 | 時系列データ分析 | ARIMA、SARIMA、季節性 |
| 第10回 | クラス分類 | ロジスティック回帰、SVM、ランダムフォレスト |
| 第11回 | クラスタリング | k-means、階層、エルボー法 |
| 第12回 | 次元削減 | PCA、寄与率、t-SNE |
本シリーズで学んだ内容は、機械学習やディープラーニングを学ぶ上での土台となります。ぜひ実際のデータに触れながら、各手法の理解を深めていただければ幸いです。
シリーズ記事一覧
- 【第1回】Pythonで学ぶデータ分析実践 〜データ分析とは〜
- 【第2回】データの前処理(欠損値・外れ値・ダミー変数・データスケーリングなど)
- 【第3回】データの把握(基本統計量・ヒストグラム・箱ひげ図・散布図など)
- 【第4回】相関分析(共分散・相関係数・相関比・連関係数など)
- 【第5回】統計的推定(母集団と標本・点推定・区間推定・信頼区間)
- 【第6回】統計的検定(仮説検定・p値・t検定・カイ二乗検定など)
- 【第7回】分散分析(一元分散分析・二元分散分析・多重比較)
- 【第8回】回帰分析(単回帰・重回帰・一般化線形モデル・モデル評価)
- 【第9回】時系列データ分析(トレンド・季節性・ARIMA・SARIMAなど)
- 【第10回】クラス分類(決定木・ランダムフォレスト・ロジスティック回帰など)
- 【第11回】クラスタリング(k-means・階層クラスタリング・エルボー法など)
- 【第12回】次元削減(主成分分析・寄与率・可視化)
- 【発展編】分析精度の向上(特徴量エンジニアリング・ハイパーパラメータ調整・アンサンブル学習など)
- 【発展編】ニューラルネットワーク入門(パーセプトロン・誤差逆伝播法・TensorFlow・Kerasなど)