4、主成分分析(PCA)原理:协方差矩阵、特征值与特征向量、方差解释率
说到降维,PCA 绝对是最经典的方法之一。我刚开始做环境数据分析那会儿,面对几十个监测指标,头都大了。后来一个老前辈跟我说:「你先跑个 PCA 看看。」嗯,这一跑,就打开了新世界的大门。
说白了,PCA 的核心思想就一句话:在损失尽可能少信息的前提下,把高维数据压缩到低维。怎么做到的呢?它找到数据里「最关键的几个方向」,然后只保留这些方向上的信息。
核心逻辑: PCA 不是简单地扔掉一些变量,而是通过线性变换,构造出一组新的、互不相关的变量(主成分),这些新变量按方差大小排序,前几个就包含了原始数据的大部分信息。
4.1 协方差矩阵:数据关系的「体检报告」
在做 PCA 之前,我们得先搞清楚数据内部的关系。协方差矩阵就是干这个的。你想想看,如果有两个变量,一个升高另一个也跟着升高,那它们的协方差就是正的;如果一个升高另一个反而下降,那就是负的。
我记得有一次处理空气质量数据,PM2.5 和 CO 的协方差特别大。这其实很好理解——它们都来自燃烧过程。协方差矩阵把这种关系量化了,而且是对所有变量两两计算。
小提示: 在实际项目中,我建议先对数据做标准化处理。因为不同环境指标的尺度差异太大了——温度可能是 0-40℃,而 PM2.5 可能是 0-500 μg/m³。如果不标准化,尺度大的变量会主导协方差矩阵,PCA 结果就偏了。
协方差矩阵的数学形式是这样的:
# 假设 X 是 n 行 p 列的数据矩阵(n 个样本,p 个变量)
# 协方差矩阵 C 是 p×p 的方阵
# C[i][j] = cov(X[:,i], X[:,j])
import numpy as np
# 生成示例数据:5个样本,3个环境指标
data = np.array([
[23.5, 45.2, 0.032], # 温度, 湿度, CO2浓度
[24.1, 48.7, 0.035],
[22.8, 42.3, 0.029],
[25.0, 50.1, 0.038],
[23.9, 46.8, 0.033]
])
# 计算协方差矩阵
cov_matrix = np.cov(data, rowvar=False)
print("协方差矩阵:")
print(cov_matrix)
输出结果会是一个 3×3 的矩阵。对角线上的值是每个变量自己的方差,非对角线就是变量间的协方差。嗯,这里要注意:np.cov 默认用无偏估计(除以 n-1),这在样本量不大的时候更准确。
4.2 特征值与特征向量:找到「主心骨」
协方差矩阵算出来了,然后呢?我们需要对它做特征分解。说白了,就是找到这个矩阵的「主心骨」——那些数据变化最剧烈的方向。
特征向量代表方向,特征值代表这个方向上的「重要程度」。特征值越大,说明数据在这个方向上的方差越大,也就是信息量越大。
关键理解: 最大的特征值对应的特征向量,就是第一主成分的方向。第二大的对应第二主成分,以此类推。这些主成分之间是正交的(互不相关),这正是 PCA 的巧妙之处。
我在项目中遇到过一个问题:特征值特别接近,这时候选几个主成分就有点纠结了。比如有两个特征值分别是 2.8 和 2.6,那说明这两个方向的信息量差不多,只取一个会损失不少信息。
# 对协方差矩阵做特征分解
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
print("特征值:", eigenvalues)
print("特征向量(每列是一个特征向量):")
print(eigenvectors)
# 按特征值从大到小排序
idx = np.argsort(eigenvalues)[::-1]
eigenvalues_sorted = eigenvalues[idx]
eigenvectors_sorted = eigenvectors[:, idx]
print("\n排序后的特征值:", eigenvalues_sorted)
你可能会问:为什么非得是特征向量?我的理解是,协方差矩阵是对称的,对称矩阵的特征向量天然就是正交的。这保证了我们找到的主成分之间没有冗余信息。很优雅,对吧?
4.3 方差解释率:每个主成分「贡献了多少」
特征值的大小直接告诉我们每个主成分的重要性。但怎么量化呢?用方差解释率。
方差解释率的计算很简单:每个特征值除以所有特征值的总和。结果就是该主成分「解释」了总方差的百分之多少。
| 主成分 | 特征值 | 方差解释率 | 累计方差解释率 |
|---|---|---|---|
| PC1 | 2.85 | 57.0% | 57.0% |
| PC2 | 1.62 | 32.4% | 89.4% |
| PC3 | 0.53 | 10.6% | 100.0% |
你看这个例子,前两个主成分就解释了 89.4% 的方差。这意味着我们可以把 3 维数据降到 2 维,只损失 10.6% 的信息。在实际项目中,我一般会保留累计方差解释率达到 85%-95% 的主成分数量。
避坑指南: 我曾经犯过一个错误——只看方差解释率,觉得越高越好。后来发现,如果数据本身噪声很大,强行保留 95% 的方差反而会把噪声也保留下来。这时候适当降低阈值(比如 80%),反而能起到去噪的效果。所以,阈值不是死的,要结合业务场景来定。
# 计算方差解释率
explained_variance_ratio = eigenvalues_sorted / np.sum(eigenvalues_sorted)
cumulative_ratio = np.cumsum(explained_variance_ratio)
print("方差解释率:", explained_variance_ratio)
print("累计方差解释率:", cumulative_ratio)
# 通常选择累计方差解释率 > 0.85 的主成分数量
n_components = np.argmax(cumulative_ratio > 0.85) + 1
print(f"建议保留的主成分数量:{n_components}")
4.4 完整流程:从原始数据到降维结果
好了,我们把上面的步骤串起来,看看 PCA 的完整流程。我个人习惯用 scikit-learn 来实现,因为它封装得很好,但理解底层原理更重要。
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
# 1. 标准化数据
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
# 2. 执行 PCA
pca = PCA(n_components=2) # 降到2维
data_pca = pca.fit_transform(data_scaled)
# 3. 查看结果
print("降维后的数据形状:", data_pca.shape)
print("各主成分的方差解释率:", pca.explained_variance_ratio_)
print("累计方差解释率:", np.sum(pca.explained_variance_ratio_))
# 4. 可视化(可选)
plt.figure(figsize=(8,6))
plt.scatter(data_pca[:, 0], data_pca[:, 1], c='steelblue', s=60)
plt.xlabel('第一主成分')
plt.ylabel('第二主成分')
plt.title('PCA 降维结果')
plt.grid(True, alpha=0.3)
plt.show()
实用技巧: 在环境数据分析中,我经常用 PCA 的载荷矩阵(即特征向量)来解读每个主成分的实际含义。比如第一主成分在温度、湿度上的载荷很大,那它可能代表「气象条件」这个综合因子。这种解读在写报告时特别有用。
最后总结一下:PCA 的核心就是协方差矩阵 → 特征分解 → 按方差解释率选主成分。这三个步骤环环相扣,缺一不可。你想想看,从一堆杂乱的数据中,找到最核心的几个方向,这就是 PCA 的魅力所在。