PCA 原理:主成分分析的数学基础与直观理解
PCA,全称主成分分析,听起来很唬人。说白了,就是在高维数据里找到最重要的方向。
我最早接触 PCA 是在处理基因表达数据的时候。那时候数据集有 20000 多个基因维度,画个散点图根本看不清楚。一位前辈跟我说:"你先把数据 PCA 一下。" 嗯,从此我就入了这个坑。
你可能会问:为什么降维非得用 PCA?直接随机挑几个特征不行吗?
别急,我们先从它的数学原理说起,然后我会告诉你它到底在干什么。
核心思想:方差最大与重构误差最小
PCA 想做的就两件事:
- 找到一组新坐标轴(称为主成分),使得数据投影到这些轴上方差最大。
- 同时保证投影后与原始数据的距离最小(重构误差最小)。
这两件事其实是等价的。我习惯这样说:主成分就是数据"最散开"的那些方向。你想想看,如果数据在某个方向上挤成一团,那这个方向对我们区分样本就没啥帮助,对吧?
拿一个简单的 2D 数据举个例子。假设你有两个基因 x 和 y,它们的表达值画出来是一个扁平的椭圆。PCA 找到的第一个主成分就是椭圆的长轴方向,第二个主成分就是短轴方向。保留长轴、丢掉短轴,信息损失最小。
数学推导:协方差矩阵的特征分解
下面我尽量用白话把数学过程捋一遍。
- 中心化:把每个特征减去其均值,让数据中心在原点上。
- 计算协方差矩阵:反映特征之间的线性关系。
- 特征分解:求出协方差矩阵的特征值和特征向量。
- 排序选主成分:按特征值从大到小排序,特征值越大,对应方向的方差越大,保留。
- 投影:原始数据乘以选出的特征向量矩阵,得到降维后的数据。
代码实现起来其实很短。下面是一个纯 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 的完整流程:
主成分的数学含义
每一个主成分都是一个线性组合,而不是简单选几个原始特征。比如第一主成分可能是: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,看一下数据概况。下面是一个完整的例子,用 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%,就算够用。