4. PCA的数学基础:特征值与特征向量
咱们在上一个章节聊了PCA的整体流程——从数据预处理到降维可视化。但说实话,很多人用了半天PCA,对背后那堆数学却是一知半解。你可能会问:「不就是scikit-learn调个.fit_transform()吗?我非得搞懂特征向量是什么?」
嗯,这里我特别想强调一句:不懂特征值和特征向量,你根本判断不了PCA图的可信度。我在几个转录组项目里都遇到过类似的问题——明明画出了漂亮的PCA图,但换个批次之后就全变了样。后来一查,原来是特征值太小的主成分被强行拉进来渲染图,整个分析在画「噪声」。
所以这一节,咱们就把那层数学窗户纸捅破。我尽量不堆公式,把直觉讲明白。
4.1 一个形象理解:PCA在做什么?
你可以把PCA想象成这样一个过程——你在三维空间里有一团数据点,像一堆飘浮的云。你想找一个角度去看这团云,使得你看到的「投影」最分散、信息损失最少。
- 找方向:找出数据方差最大的方向
- 转坐标系:把数据旋转到这些方向上
- 降维:只保留最重要的几个新坐标轴
特征向量,就是新坐标轴的方向。特征值,就是这个方向上数据分散程度的量化。说白了,特征值越大,这个方向承载的信息就越多。
核心理解:特征向量是「往哪看」,特征值是「能看到多少信息」。
4.2 从协方差矩阵到特征分解
具体怎么找到这些方向?PCA的标准步骤是计算数据的协方差矩阵,然后对这个矩阵做特征分解。
你想想看,协方差矩阵描述的是变量之间的相关性。对角线上是各个变量的方差,非对角线上是协方差。我们要做的事,就是找一个变换,让这个矩阵在新的坐标系下尽可能「对角化」——让变量之间不再相关,并且方差集中到少数几个坐标轴上。
数学上,这就等价于求解:
A · v = λ · v
其中:
A = 协方差矩阵
v = 特征向量(方向)
λ = 特征值(该方向上的方差大小)
我记得第一次在R语言里手动算PCA时,看到这个等式没什么感觉。直到后来有一个单细胞数据集的样本标签一直聚不拢,我手动算了一遍特征值,才发现前三个主成分加起来的总解释方差不到30%——怪不得图是乱糟糟的一团。
4.3 特征值与贡献率的关系
在PCA结果的输出里,你经常会看到类似这样的表格:
| 主成分 | 特征值 | 方差贡献率 | 累积贡献率 |
|---|---|---|---|
| PC1 | 4.52 | 0.45 (45%) | 0.45 |
| PC2 | 2.31 | 0.23 (23%) | 0.68 |
| PC3 | 1.18 | 0.12 (12%) | 0.80 |
| PC4 | 0.62 | 0.06 (6%) | 0.86 |
| PC5 | 0.41 | 0.04 (4%) | 0.90 |
这个表格的生成逻辑就是把特征值标准化成百分比。每个特征值除以所有特征值的总和,就是对应的方差贡献率。
实用建议:我个人习惯在评估差异表达分析的可靠性时,只看前两个主成分的解释方差之和。如果PC1 + PC2不到50%,那PCA图上的聚类可能并不稳定。这时候我通常会先做批次校正,或者过滤掉噪声基因,再重新跑PCA。
4.4 特征向量的方向有什么意义?
很多人在看PCA的loading向量(也就是特征向量)时会忽略一个细节:特征向量的正负号没有绝对意义。
什么意思呢?特征向量v和-v是同一个方向,只是指向相反。如果你两次运行PCA得到的结果符号相反,不要慌——这不代表结果不一致。真正重要的是基因在PC轴上的相对排序和载荷大小。
我曾经犯过一个错:在一个基因表达数据集的PCA结果里,看到PC1的loading全是负数,以为自己代码写错了。后来重算了三遍才发现,特征向量的符号翻转是正常的。嗯,从那以后,我再也不纠结符号了,只看基因在PC轴上的相对位置。
4.5 快速计算:用Python手撕特征值
为了让你更直观地理解,这里用一个简单例子演示特征分解的过程。我们用NumPy模拟一个基因表达矩阵:
import numpy as np
from sklearn.preprocessing import StandardScaler
# 模拟数据:10个样本,5个基因
np.random.seed(42)
data = np.random.randn(10, 5)
# 标准化(PCA必须做的步骤)
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
# 计算协方差矩阵
cov_matrix = np.cov(data_scaled, rowvar=False)
# 特征分解
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
# 按特征值从大到小排序
idx = eigenvalues.argsort()[::-1]
eigenvalues_sorted = eigenvalues[idx]
eigenvectors_sorted = eigenvectors[:, idx]
print("特征值(排序后):")
print(eigenvalues_sorted)
print("\n方差贡献率:")
explained_variance = eigenvalues_sorted / np.sum(eigenvalues_sorted)
print(explained_variance)
print("\n累积贡献率:")
print(np.cumsum(explained_variance))
跑完这段代码,你就能看到每个主成分的「重要性排名」。在我的项目中,当累积贡献率超过80%时,我才会认为前几个PC足以代表原始数据的主要结构。
避坑指南:我曾经在一个小样本(n=6)的差异表达分析里直接用sklearn的PCA默认参数画图,结果PC1贡献率高达99%。这听起来很「完美」,对吧?但后来我发现,那是因为样本太少、维度太高,PCA直接把样本间的随机差异当成了主要变异方向。所以,记住:特征值大小必须结合生物学重复数来解读。低于5个生物学重复的场景里,高贡献率可能是假象。
4.6 本章小结:一张图讲清核心逻辑
下面这张图总结了从原始数据到特征值、特征向量的完整流动。你可以把它当作一种心法图,以后每次跑PCA前扫一眼,就能避免很多低级错误。
好了,到现在你应该能理解:特征值就是PCA图的「照妖镜」。一张PCA图好不好看是次要的,关键看它背后的特征值分布是否合理。如果前两个主成分的特征值加起来连总方差的50%都不到,那图上的任何「聚类」都可能是视觉巧合。
一句话总结:看懂特征值,你才真正看懂了PCA。下次画PCA图之前,先打印一下各主成分的贡献率——这是我一直坚持的习惯。