PCA 原理:主成分分析的数学基础与直观理解

PCA,全称主成分分析,听起来很唬人。说白了,就是在高维数据里找到最重要的方向。

我最早接触 PCA 是在处理基因表达数据的时候。那时候数据集有 20000 多个基因维度,画个散点图根本看不清楚。一位前辈跟我说:"你先把数据 PCA 一下。" 嗯,从此我就入了这个坑。

你可能会问:为什么降维非得用 PCA?直接随机挑几个特征不行吗?

别急,我们先从它的数学原理说起,然后我会告诉你它到底在干什么。

核心思想:方差最大与重构误差最小

PCA 想做的就两件事:

  • 找到一组新坐标轴(称为主成分),使得数据投影到这些轴上方差最大。
  • 同时保证投影后与原始数据的距离最小(重构误差最小)。

这两件事其实是等价的。我习惯这样说:主成分就是数据"最散开"的那些方向。你想想看,如果数据在某个方向上挤成一团,那这个方向对我们区分样本就没啥帮助,对吧?

拿一个简单的 2D 数据举个例子。假设你有两个基因 x 和 y,它们的表达值画出来是一个扁平的椭圆。PCA 找到的第一个主成分就是椭圆的长轴方向,第二个主成分就是短轴方向。保留长轴、丢掉短轴,信息损失最小。

数学推导:协方差矩阵的特征分解

下面我尽量用白话把数学过程捋一遍。

  1. 中心化:把每个特征减去其均值,让数据中心在原点上。
  2. 计算协方差矩阵:反映特征之间的线性关系。
  3. 特征分解:求出协方差矩阵的特征值和特征向量。
  4. 排序选主成分:按特征值从大到小排序,特征值越大,对应方向的方差越大,保留。
  5. 投影:原始数据乘以选出的特征向量矩阵,得到降维后的数据。

代码实现起来其实很短。下面是一个纯 NumPy 的 PCA,不依赖 sklearn:

import numpy as np

def pca_numpy(X, n_components=2):
    # 中心化
    X_mean = np.mean(X, axis=0)
    X_centered = X - X_mean
    # 协方差矩阵
    cov = np.cov(X_centered, rowvar=False)
    # 特征分解
    eigenvalues, eigenvectors = np.linalg.eigh(cov)
    # 按特征值降序排列
    idx = np.argsort(eigenvalues)[::-1]
    eigenvectors = eigenvectors[:, idx]
    # 取前 n_components 个
    W = eigenvectors[:, :n_components]
    # 投影
    X_pca = X_centered @ W
    return X_pca, eigenvalues[idx], W
提示: 用 eigh 而不是 eig 是因为协方差矩阵是对称阵,eigh 更快、更稳定。我之前踩过坑用 eig,结果在数据量大的时候跑得特别慢。

直观理解:旋转坐标系

想象你手里拿着一个3D物体。PCA 就是把它转到一个最佳角度,让影子(投影)在墙上的轮廓尽可能宽。影子越宽,信息保留越多。

下面这张图总结了 PCA 的完整流程:

PCA 核心流程 原始数据 n×p 矩阵 中心化 每列减去均值 协方差矩阵 p×p 对称阵 特征分解 特征值 λ 与特征向量 v 排序选择 取前k个主成分 投影降维 Xpca = Xcentered·W 降维结果 n×k 矩阵 重要 特征值大小 决定保留程度

主成分的数学含义

每一个主成分都是一个线性组合,而不是简单选几个原始特征。比如第一主成分可能是:0.5×基因A + 0.3×基因B - 0.2×基因C。每个原始特征前面都有一个权重(载荷)。

特征值 λ 代表这个主成分能解释多少方差。通常我们会算方差解释率:

explained_variance_ratio = eigenvalues / sum(eigenvalues)

比如前两个主成分加起来解释了 70% 的方差,那就说明降维到 2D 保留了七成的信息。

主成分 特征值 方差解释率 累积解释率
PC1 12.5 55.6% 55.6%
PC2 4.2 18.7% 74.3%
PC3 2.1 9.3% 83.6%
PC4 1.0 4.4% 88.0%

看到没?前两个主成分就解释了 74% 的方差,剩下几十个维度加起来才 26%。这也是为什么 PCA 降维效果这么好——大部分信息其实集中在前几个方向。

PCA 与批次效应的关系

在我做的项目里,PCA 最常用的场景就是看样本有没有按批次聚类。如果有明显的批次聚类,PC1 或 PC2 的载荷往往跟批次变量高度相关。

具体怎么判断?很简单:把样本按批次染色,画 PCA 散点图。如果同批次的样本挤在一起,而不同批次分开,说明批次效应很强。这时候就需要用后几章的方法去校正了。

注意: PCA 假设数据是线性的。如果批次效应是非线性的(比如测序深度带来的偏移),PCA 可能看不出来。那时候可以考虑后面的 UMAP 或 t-SNE 来辅助诊断。

实战:用 PCA 做批次效应可视化

我习惯先跑一个快速 PCA,看一下数据概况。下面是一个完整的例子,用 seaborn 画散点图矩阵:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.decomposition import PCA

# 假设 data 是表达矩阵,行样本,列基因
pca = PCA(n_components=3)
pca_result = pca.fit_transform(data)

# 组装成 DataFrame
pca_df = pd.DataFrame(pca_result, columns=['PC1','PC2','PC3'])
pca_df['batch'] = batch_labels

# 画图
plt.figure(figsize=(8,6))
sns.scatterplot(data=pca_df, x='PC1', y='PC2', hue='batch', alpha=0.7)
plt.title('PCA colored by batch')
plt.show()

这段代码在我的项目里几乎每个批次检测脚本都会出现。很多时候,一看到 PC1 和 PC2 上批次之间的明显分离,我就知道该做校正了。

常见误区与避坑指南

  • 误区一:PCA 之前不标准化。如果不同特征的尺度差很多(比如基因表达量从 0 到 10000),大尺度特征会主导 PCA。我建议先做 Z-score 标准化:StandardScaler。
  • 误区二:认为 PCA 能处理缺失值。PCA 不支持缺失数据。需要先填充或者删除缺失严重的特征。
  • 误区三:只盯着 PC1 和 PC2 看。有时候批次效应可能藏在 PC3 或 PC4 上。我的经验是至少看前 5 个主成分的散点图矩阵。
一个小技巧: 用 pca.components_ 可以提取每个主成分的载荷。检查载荷最大的几个基因,往往能找出批次效应的源头(比如某些批次特异的标记基因)。

一次性解释所有主成分:碎石图

碎石图(Scree Plot)是判断保留多少主成分的经典工具。横轴是主成分序号,纵轴是特征值。

import matplotlib.pyplot as plt

eigenvalues = pca.explained_variance_
plt.figure(figsize=(6,4))
plt.plot(range(1, len(eigenvalues)+1), eigenvalues, 'o-')
plt.xlabel('Principal Component')
plt.ylabel('Eigenvalue')
plt.title('Scree Plot')
plt.axhline(y=1, color='gray', linestyle='--', linewidth=0.8)
plt.show()

一般保留特征值大于 1 的主成分(Kaiser 准则),或者看曲线"肘部"拐点。我个人倒不特别死守规则,只要累积解释率超过 70%~80%,就算够用。