主成分分析

バイオインフォ基礎

1. 確率・統計と行列

(1) 残差ベクトル

観測ベクトル $\mathbf{y}\in\mathbb{R}^n$、モデルの予測 $\hat{\mathbf{y}}\in\mathbb{R}^n$ に対して、残差ベクトルは
$$
\mathbf{e}=\mathbf{y}-\hat{\mathbf{y}}
$$
で定義する。$|\mathbf{e}|$ が小さいほど当てはまりが良い。

(2) 分散共分散行列

データ行列 $X\in\mathbb{R}^{n\times p}$(行=サンプル、列=変数)。各列を平均0に中心化した後の標本分散共分散行列を
$$
S=\frac{1}{\,n-1\,}X^{\mathsf T}X
$$
と定める。$S_{ii}$ は変数 $i$ の分散、$S_{ij}$ は変数 $i$ と $j$ の共分散。

(3) 相関係数行列

各変数の標準偏差を $d_i=\sqrt{S_{ii}}$ とすると、相関係数行列 $R$ は
$$
R_{ij}=\frac{S_{ij}}{d_i d_j}
$$

(4) マハラノビス距離

平均 $\boldsymbol{\mu}$、共分散 $S$ に対し、点 $\mathbf{x}$ の距離は
$$
d_M(\mathbf{x})=\sqrt{(\mathbf{x}-\boldsymbol{\mu})^{\mathsf T}S^{-1}(\mathbf{x}-\boldsymbol{\mu})}
$$

(5) 一般化分散

全体のばらつきの尺度は
$$
\det(S)
$$

(6) フルランクとランク落ち

$\mathrm{rank}(S)=p$ ならフルランク。小さい場合は線形従属(多重共線性)。

(7) 多重共線性

説明変数がほぼ線形結合になっている現象。$S$ が特異(または縮退)に近い。


2. 分散共分散行列の性質

(1) 分散共分散行列のサイズ

$S\in\mathbb{R}^{p\times p}$ は正方行列。

(2) 分散共分散行列は対称行列

定義から $S_{ij}=S_{ji}$。よって $S=S^{\mathsf T}$。

(3) 分散共分散行列は非負値(半正定値)

任意の $\mathbf{a}\in\mathbb{R}^p$ で
$$
\mathbf{a}^{\mathsf T}S\mathbf{a}=\frac{1}{\,n-1\,}|X\mathbf{a}|^2\ge 0
$$
よって $S$ は半正定値。


3. 主成分分析の導出

(1) 特定の方向の分散($w$ 方向の分散が $w^{\mathsf T}Sw$ になることの証明)

状況設定:中心化済みデータ $X\in\mathbb{R}^{n\times p}$、方向ベクトル $\mathbf{w}\in\mathbb{R}^p$。各サンプル $\mathbf{x}_i^{\mathsf T}$ の射影(スカラー)を $z_i=\mathbf{x}_i^{\mathsf T}\mathbf{w}$ とする。射影ベクトルを $\mathbf{z}=X\mathbf{w}\in\mathbb{R}^n$ と書ける。

標本分散の定義(平均は 0):
$$
\mathrm{Var}(\mathbf{z})=\frac{1}{\,n-1\,}\sum_{i=1}^{n}z_i^2=\frac{1}{\,n-1\,}\mathbf{z}^{\mathsf T}\mathbf{z}
$$

$\mathbf{z}=X\mathbf{w}$ を代入して
$$
\mathrm{Var}(\mathbf{z})=\frac{1}{\,n-1\,}\mathbf{w}^{\mathsf T}X^{\mathsf T}X\mathbf{w}=\mathbf{w}^{\mathsf T}\Big(\frac{1}{\,n-1\,}X^{\mathsf T}X\Big)\mathbf{w}=\mathbf{w}^{\mathsf T}S\mathbf{w}
$$

以上で、方向 $\mathbf{w}$ の分散は $\,\mathbf{w}^{\mathsf T}S\mathbf{w}\,$ と等しいことが示せた。これはレイリー商の形であり、後で最大化する。


(2) ラグランジュの未定乗数法(最大分散方向が固有問題に帰着することの証明)

目的:分散 $\mathbf{w}^{\mathsf T}S\mathbf{w}$ を最大化したい。ただし尺度の任意性($\alpha\mathbf{w}$ にすると分散は $\alpha^2$ 倍)を除くため、制約 $|\mathbf{w}|=1$ を課す。

最適化問題:
$$
\max_{\mathbf{w}}\ \mathbf{w}^{\mathsf T}S\mathbf{w}\quad\text{s.t.}\quad \mathbf{w}^{\mathsf T}\mathbf{w}=1
$$

ラグランジュ関数:
$$
\mathcal{L}(\mathbf{w},\lambda)=\mathbf{w}^{\mathsf T}S\mathbf{w}-\lambda(\mathbf{w}^{\mathsf T}\mathbf{w}-1)
$$

$\mathbf{w}$ について勾配を 0 にする($\nabla_{\mathbf{w}}\mathcal{L}=\mathbf{0}$)。後述(3)のベクトル解析から
$$
\nabla_{\mathbf{w}}(\mathbf{w}^{\mathsf T}S\mathbf{w})=2S\mathbf{w},\quad \nabla_{\mathbf{w}}(\mathbf{w}^{\mathsf T}\mathbf{w})=2\mathbf{w}
$$
よって
$$
2S\mathbf{w}-2\lambda\mathbf{w}=\mathbf{0}\ \Rightarrow\ S\mathbf{w}=\lambda\mathbf{w}
$$

すなわち、最適方向 $\mathbf{w}$ は $S$ の固有ベクトル、対応する分散は固有値 $\lambda$ に等しい。レイリー商の性質(またはクーラント・フィッシャーの極値原理)から、最大値は最大固有値 $\lambda_1$、そのときの最適解は対応固有ベクトル $\mathbf{w}_1$。同様に、$\mathbf{w}_1$ に直交という制約を加えると 2 番目の固有ベクトルが得られ、以後も同様。


(3) ベクトル解析(勾配の厳密証明)

主張1
$$
\nabla_{\mathbf{x}}(\mathbf{x}^{\mathsf T}\mathbf{x})=2\mathbf{x}
$$

証明:$\mathbf{x}^{\mathsf T}\mathbf{x}=\sum_{i=1}^{p}x_i^2$。$k$ 成分で偏微分すると $\partial/\partial x_k$ は $2x_k$、したがって勾配ベクトル全体は $[2x_1,\dots,2x_p]^{\mathsf T}=2\mathbf{x}$。

主張2(一般形):
$$
\nabla_{\mathbf{x}}(\mathbf{x}^{\mathsf T}A\mathbf{x})=(A+A^{\mathsf T})\mathbf{x}
$$

証明:成分で書くと $\mathbf{x}^{\mathsf T}A\mathbf{x}=\sum_{i,j}a_{ij}x_i x_j$。$k$ で偏微分すると
$$
\frac{\partial}{\partial x_k}\sum_{i,j}a_{ij}x_i x_j=\sum_{j}a_{kj}x_j+\sum_{i}a_{ik}x_i
$$
これは第 $k$ 成分が $\big((A+A^{\mathsf T})\mathbf{x}\big)_k$ であることに一致する。したがって主張成立。特に $A$ が対称($S=S^{\mathsf T}$)なら $$ \nabla_{\mathbf{x}}(\mathbf{x}^{\mathsf T}A\mathbf{x})=2A\mathbf{x}
$$
となる。


(4) 主成分分析(固有値問題としての導出とデータ変換の手順)

導出の

  • 方向 $\mathbf{w}$ に沿う分散は $\mathbf{w}^{\mathsf T}S\mathbf{w}$。
  • 制約 $|\mathbf{w}|=1$ の下で最大化すると $S\mathbf{w}=\lambda\mathbf{w}$ に帰着。
  • 最大分散の主成分軸は、$S$ の最大固有値に対応する固有ベクトル $\mathbf{w}_1$。次の主成分は $\mathbf{w}_1$ に直交という制約の下で最大化し、固有値順に $\mathbf{w}_2,\mathbf{w}_3,\dots$ が得られる(互いに直交)。

固有値分解と変換
$S$ の固有値分解を
$$
S=V\Lambda V^{\mathsf T}
$$
とする。$V=[\mathbf{v}_1,\dots,\mathbf{v}_p]$(直交行列)、$\Lambda=\mathrm{diag}(\lambda_1,\dots,\lambda_p)$($\lambda_1\ge\cdots\ge\lambda_p\ge 0$)。主成分軸は $\mathbf{v}_i$、各主成分の分散は $\lambda_i$。中心化データ $X$ を $k$ 次元に写像するスコアは $$ Z=X V_k $$ ここで $V_k=[\mathbf{v}_1,\dots,\mathbf{v}_k]$。第 $i$ 主成分の列 $Z_{(\cdot,i)}$ は、$X$ を $\mathbf{v}_i$ に射影した値。再構成(低次元近似)は
$$
\hat{X}=Z V_k^{\mathsf T}=X V_k V_k^{\mathsf T}
$$
で与えられ、これが最小二乗誤差の意味で最良のランク $k$ 近似になる(エッカート・ヤングの定理;SVD と等価)。

二つの視点の同値

  • 固有値分解視点:$S$ を対角化して主方向と分散を得る。
  • SVD 視点:$X=U\Sigma V^{\mathsf T}$ として、$V$ が主成分軸、$\Sigma^2/(n-1)$ が固有値(分散)に一致。

(5) 寄与率(固有値の和=全分散、寄与率の定義と性質の証明)

主張A(全分散=固有値和):
$$
\sum_{i=1}^{p}\lambda_i=\mathrm{tr}(S)=\sum_{j=1}^{p}S_{jj}
$$

証明:トレース不変性より
$$
\mathrm{tr}(S)=\mathrm{tr}(V\Lambda V^{\mathsf T})=\mathrm{tr}(\Lambda)=\sum_{i=1}^{p}\lambda_i
$$
一方、$\mathrm{tr}(S)=\sum_j S_{jj}$ は各変数の分散の総和であり、これが全分散。よって等しい。

主張B(寄与率の定義):
第 $i$ 主成分の寄与率(全分散に占める割合)は
$$
\mathrm{CR}_i=\frac{\lambda_i}{\sum_{j=1}^{p}\lambda_j}
$$
累積寄与率は
$$
\mathrm{CCR}_k=\frac{\sum_{i=1}^{k}\lambda_i}{\sum_{j=1}^{p}\lambda_j}
$$

解釈:$\lambda_i$ は第 $i$ 主成分の分散、分子は「保持した情報量」、分母は「全情報量」。したがって比率が「どれだけの分散(情報)を説明できたか」を表す。SVD では $\lambda_i=\sigma_i^2/(n-1)$($\sigma_i$ は特異値)なので、$\sum \sigma_i^2/(n-1)=\mathrm{tr}(S)$ と一致する。


4. Pythonによる実装

(1) 固有値分解による実装

import numpy as np

def pca_eig(X, k=None, center=True, scale=False):
    """
    PCA via eigen-decomposition of the covariance matrix (no scikit-learn).

    Parameters
    ----------
    X : ndarray of shape (n_samples, n_features)
        Input data. Rows are samples, columns are features.
    k : int or None
        Number of principal components to keep. If None, keep all.
    center : bool
        If True, subtract column means.
    scale : bool
        If True, standardize columns to unit variance after centering.

    Returns
    -------
    scores : ndarray, shape (n_samples, k)
        Projected data (principal component scores): X_centered @ components.
    components : ndarray, shape (n_features, k)
        Principal axes (eigenvectors / loadings). Columns are components.
    explained_variance : ndarray, shape (k,)
        Eigenvalues of the covariance matrix (variances of each PC).
    explained_variance_ratio : ndarray, shape (k,)
        Fraction of total variance explained by each PC.
    mean_ : ndarray, shape (n_features,)
        Per-feature means used for centering.
    scale_ : ndarray, shape (n_features,)
        Per-feature scales used for standardization (1.0 if not scaled).
    """
    X = np.asarray(X, dtype=float)
    n_samples, n_features = X.shape

    # 1) Center (and optionally scale) the data
    mean_ = X.mean(axis=0) if center else np.zeros(n_features)
    Xc = X - mean_
    if scale:
        # ddof=1 to match sample std used with covariance (n-1 in denominator)
        scale_ = Xc.std(axis=0, ddof=1)
        # Protect against zero-variance columns
        scale_[scale_ == 0.0] = 1.0
        Xc = Xc / scale_
    else:
        scale_ = np.ones(n_features)

    # 2) Covariance matrix (sample covariance, divisor n-1)
    # shape: (n_features, n_features)
    S = (Xc.T @ Xc) / (n_samples - 1)

    # 3) Symmetric eigen-decomposition
    # eigh returns eigenvalues in ascending order
    evals, evecs = np.linalg.eigh(S)

    # 4) Sort by descending eigenvalue (largest variance first)
    order = np.argsort(evals)[::-1]
    evals = evals[order]
    evecs = evecs[:, order]

    # 5) Select number of components
    if k is None:
        k = n_features
    k = int(k)
    components = evecs[:, :k]              # (n_features, k)
    explained_variance = evals[:k]         # (k,)
    total_var = evals.sum()
    explained_variance_ratio = explained_variance / total_var if total_var > 0 else np.zeros_like(explained_variance)

    # 6) Scores (projected data)
    scores = Xc @ components               # (n_samples, k)

    return scores, components, explained_variance, explained_variance_ratio, mean_, scale_


# ---------- Example usage ----------
if **name** == "**main**":
    # Toy data: 6 samples, 3 features
    X = np.array([
        [2.5, 2.4, 0.5],
        [0.5, 0.7, 0.1],
        [2.2, 2.9, 0.6],
        [1.9, 2.2, 0.4],
        [3.1, 3.0, 0.9],
        [2.3, 2.7, 0.7]
    ])

    scores, comps, ev, evr, mean_, scale_ = pca_eig(X, k=2, center=True, scale=False)

    print("Mean:", mean_)
    print("Components (columns are PCs):\n", comps)
    print("Explained variance:", ev)
    print("Explained variance ratio:", evr)
    print("Scores (projected data):\n", scores)

    # To transform NEW data X_new to the PC space:
    # X_new_scores = ((X_new - mean_) / scale_) @ comps

(2) 特異値分解による実装

import numpy as np

def pca_svd(X, k=None, center=True, scale=False):
    """
    PCA via Singular Value Decomposition (no scikit-learn).

    Parameters
    ----------
    X : ndarray of shape (n_samples, n_features)
        Input data (rows = samples, cols = features).
    k : int or None
        Number of principal components to keep. If None, keep all.
    center : bool
        If True, subtract column means.
    scale : bool
        If True, standardize columns to unit variance after centering.

    Returns
    -------
    scores : ndarray, shape (n_samples, k)
        Projected data in PC space (principal component scores).
    components : ndarray, shape (n_features, k)
        Principal axes / loadings (columns are PCs).
    explained_variance : ndarray, shape (k,)
        Variance of each selected PC = s_i^2 / (n_samples - 1).
    explained_variance_ratio : ndarray, shape (k,)
        Fraction of total variance explained by each selected PC.
    mean_ : ndarray, shape (n_features,)
        Column means used for centering.
    scale_ : ndarray, shape (n_features,)
        Column scales used for standardization (1.0 if not scaled).
    """
    X = np.asarray(X, dtype=float)
    n_samples, n_features = X.shape

    # 1) Center (and optionally scale) the data
    mean_ = X.mean(axis=0) if center else np.zeros(n_features)
    Xc = X - mean_
    if scale:
        scale_ = Xc.std(axis=0, ddof=1)
        scale_[scale_ == 0.0] = 1.0  # protect zero-variance cols
        Xc = Xc / scale_
    else:
        scale_ = np.ones(n_features)

    # 2) Thin SVD
    # Xc = U @ diag(s) @ Vt, with U (n_samples x r), Vt (r x n_features)
    U, s, Vt = np.linalg.svd(Xc, full_matrices=False)

    # 3) Choose number of components
    r = min(n_samples, n_features)
    if k is None:
        k = r
    k = int(k)

    # 4) Components (loadings): columns are PCs
    components = Vt[:k, :].T  # shape (n_features, k)

    # 5) Scores (projected data)
    # scores = U @ diag(s) using only top-k singular values
    scores = U[:, :k] \* s[:k]  # broadcasting, shape (n_samples, k)

    # 6) Explained variance and ratio
    # Total variance of centered data = sum(s^2)/(n_samples - 1)
    explained_variance = (s[:k] ** 2) / (n_samples - 1)
    total_variance = (s ** 2).sum() / (n_samples - 1)
    explained_variance_ratio = explained_variance / total_variance if total_variance > 0 else np.zeros_like(explained_variance)

    return scores, components, explained_variance, explained_variance_ratio, mean_, scale_


# ---------- Example usage ----------
if **name** == "**main**":
    # Toy data: 6 samples, 3 features
    X = np.array([
        [2.5, 2.4, 0.5],
        [0.5, 0.7, 0.1],
        [2.2, 2.9, 0.6],
        [1.9, 2.2, 0.4],
        [3.1, 3.0, 0.9],
        [2.3, 2.7, 0.7]
    ])

    scores, comps, ev, evr, mean_, scale_ = pca_svd(X, k=2, center=True, scale=False)

    print("Mean:", mean_)
    print("Components (columns are PCs):\n", comps)
    print("Explained variance:", ev)
    print("Explained variance ratio:", evr)
    print("Scores (projected data):\n", scores)

    # To transform NEW data X_new using the fitted model:
    # X_new_scores = ((X_new - mean_) / scale_) @ comps

(3) Pythonのscikit-learnによる実装

# scikit-learn PCA examples
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# --- Toy data: rows = samples, cols = features ---
X = np.array([
    [2.5, 2.4, 0.5],
    [0.5, 0.7, 0.1],
    [2.2, 2.9, 0.6],
    [1.9, 2.2, 0.4],
    [3.1, 3.0, 0.9],
    [2.3, 2.7, 0.7]
], dtype=float)

# ============================================================
# 1) BASIC PCA (no standardization). PCA uses SVD internally.
# ============================================================
pca = PCA(n_components=2, svd_solver="auto", random_state=0)
scores = pca.fit_transform(X)   # shape: (n_samples, n_components)

print("=== BASIC PCA ===")
print("Mean (per feature):", pca.mean_)
print("Components (rows are PCs):\n", pca.components_)
print("Explained variance:", pca.explained_variance_)
print("Explained variance ratio:", pca.explained_variance_ratio_)
print("Singular values:", pca.singular_values_)
print("Scores (projected data):\n", scores)

# Reconstruct back (rank-2 approximation here equals original rank if 2 >= rank)
X_recon = pca.inverse_transform(scores)
print("Reconstruction:\n", X_recon)

# ============================================================
# 2) STANDARDIZED PCA (common in practice)
#    Standardize features to zero-mean, unit-variance before PCA.
# ============================================================
pipe = Pipeline(steps=[
    ("scaler", StandardScaler(with_mean=True, with_std=True)),
    ("pca", PCA(n_components=0.95, svd_solver="full", random_state=0))  # keep 95% variance
])
scores_std = pipe.fit_transform(X)

print("\n=== STANDARDIZED PCA (keep 95% variance) ===")
pca_std = pipe.named_steps["pca"]
print("n_components_ selected:", pca_std.n_components_)
print("Explained variance ratio (cumulative):", np.cumsum(pca_std.explained_variance_ratio_))
print("Scores:\n", scores_std)

# Transform NEW data with the same pipeline (fit parameters reused)
X_new = np.array([[2.8, 2.6, 0.8],
                  [1.0, 0.9, 0.2]], dtype=float)
X_new_scores = pipe.transform(X_new)
print("New data scores:\n", X_new_scores)

# Inverse transform to original feature space (after PCA+scaler)
X_new_recon = pipe.inverse_transform(X_new_scores)
print("New data reconstruction:\n", X_new_recon)

# ============================================================
# 3) PCA with WHITENING (optional)
#    Whitening scales PC scores by 1/sqrt(eigenvalue) -> unit variance per PC.
#    Useful for some downstream models, but it changes interpretability.
# ============================================================
pipe_whiten = Pipeline(steps=[
    ("scaler", StandardScaler()),
    ("pca", PCA(n_components=2, whiten=True, random_state=0))
])
scores_white = pipe_whiten.fit_transform(X)

print("\n=== PCA with WHITENING ===")
pca_w = pipe_whiten.named_steps["pca"]
print("Components (rows are PCs):\n", pca_w.components_)
print("Explained variance:", pca_w.explained_variance_)
print("Explained variance ratio:", pca_w.explained_variance_ratio_)
print("Whitened scores (unit variance per PC):\n", scores_white)