主成分分析(PCA)是什么?

4 阅读9分钟

数据越来越多,但真正有用的信息往往藏在一堆噪音里。主成分分析(Principal Component Analysis,PCA)就是那把帮你拨开迷雾的刀——它不删数据,而是重新理解数据,把高维空间里散落的信息压缩成几条最关键的"脉络",让你看清数据真正想说的话。


一、PCA 到底在做什么?

1901 年,统计学家 Karl Pearson 提出了这个方法。彼时计算机还不存在,这个想法只能停留在纸面。直到电子计算机普及,PCA 才真正走进实验室、走进工厂、走进每一个需要跟数据打交道的地方。

它的逻辑其实并不复杂:原始数据里的很多变量,往往不是独立的。比如一个学生数学好,物理大概率也不差;一个城市 GDP 高,人均收入往往也高。这种"藕断丝连"的相关性,意味着数据里存在大量冗余。PCA 做的,就是找出数据变化最剧烈的方向,把那些"说的是同一件事"的变量合并成几个新的维度——也就是主成分

每个主成分都是原始变量的线性组合,彼此之间完全正交(互不相关)。第一主成分抓住数据中方差最大的方向,第二主成分在垂直于第一个的前提下再抓次大方差,以此类推。最终,你用两三个主成分,就能讲清楚原来十几个变量才能讲清楚的故事。

打个比方:想象一团三维空间里的点云,形状像一个被压扁的椭球。PCA 就是找到这个椭球最长的轴、次长的轴……然后把点云"摊平"投影到这几条轴上。信息损失最小,维度却大幅下降。


二、数学骨架:特征值与方差

PCA 的数学核心是一个特征值分解问题,和奇异值分解(SVD)本质相通。走一遍完整流程,大概是这样:

步骤操作为什么这么做
标准化数据每个特征去均值、除标准差消除量纲差异,不让"米"和"毫米"抢风头
计算协方差矩阵C=1n1XTX\mathbf{C} = \frac{1}{n-1} X^T X摸清变量之间的线性关联程度
特征分解求协方差矩阵的特征值与特征向量特征向量就是主成分的方向
排序筛选按特征值从大到小排列特征值越大,这个方向上的信息越丰富
投影变换将原始数据投影到选定主成分上得到降维后的新坐标

每个主成分能解释多少信息,用方差解释率来衡量:

解释率i=λij=1pλj\text{解释率}_i = \frac{\lambda_i}{\sum_{j=1}^{p} \lambda_j}

λi\lambda_i 是第 ii 个特征值,pp 是原始特征总数。实践中,累计解释率达到 85%~95% 时,保留的主成分数量就差不多够用了。


三、PCA 和它的"邻居们"

PCA 不是孤立存在的,数据降维这条赛道上还有几个常见选手,值得放在一起看看:

方法有无监督核心目标与 PCA 的本质区别
PCA无监督最大化方差,降维不依赖类别标签,纯线性变换
LDA(线性判别分析)有监督最大化类间差异需要标签,为分类服务
因子分析无监督挖掘潜在变量结构更关注"为什么",解释性更强
K-means 聚类无监督数据分组不降维,按相似度划堆
核 PCA无监督非线性降维PCA 的非线性进化版

简单说:PCA 是探索者,LDA 是分类器,因子分析是解释者。选哪个,取决于你的问题是什么。


四、它能用在哪里?

PCA 的应用场景出乎意料地广泛,远不止"机器学习预处理"这一条:

  • 机器学习:砍掉冗余特征,缓解"维度灾难",让模型不那么容易过拟合。
  • 数据可视化:把几十维的数据压到 2D 或 3D,画出来给人看。
  • 人脸识别:经典的 Eigenfaces 方法,就是 PCA 在图像上的直接应用。
  • 基因组学:几十万个 SNP 位点,用 PCA 两三步就能看出种群结构。
  • 金融风控:从几百个市场指标里提炼出少数几个风险因子。
  • 信号处理:从混杂信号中分离出有效成分,过滤噪声。

五、动手实践:用 Pandas 跑一遍 PCA

下面用一个学生多科目成绩的构造数据集,把 PCA 的完整流程走一遍。数据里埋了一个"彩蛋":理科三门(数学、物理、化学)之间高度相关,文科三门(语文、历史、英语)之间也高度相关——看 PCA 能不能把这个结构自动挖出来。

5.1 构造示例数据

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# 中文字体支持(可选)
matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
matplotlib.rcParams['axes.unicode_minus'] = False

# ── 构造数据:20名学生,6门课程成绩 ──────────────────────────
np.random.seed(42)
n_students = 20

# 两类潜在能力:理科 vs 文科
science_ability = np.random.randn(n_students)
arts_ability    = np.random.randn(n_students)

data = {
    '数学': 60 + 15 * science_ability + 5 * np.random.randn(n_students),
    '物理': 62 + 14 * science_ability + 5 * np.random.randn(n_students),
    '化学': 58 + 13 * science_ability + 6 * np.random.randn(n_students),
    '语文': 65 + 12 * arts_ability    + 5 * np.random.randn(n_students),
    '历史': 63 + 11 * arts_ability    + 6 * np.random.randn(n_students),
    '英语': 60 +  8 * arts_ability    + 7 * np.random.randn(n_students),
}

df = pd.DataFrame(data, index=[f'学生{i+1:02d}' for i in range(n_students)])
df = df.clip(0, 100).round(1)

print("=" * 55)
print("【原始数据集】")
print("=" * 55)
print(df.to_string())
print(f"\n数据形状: {df.shape}{df.shape[0]} 名学生 × {df.shape[1]} 门课程")

5.2 标准化——这一步不能省

# PCA 对量纲极其敏感,标准化是必须的前置步骤
scaler = StandardScaler()
X_scaled = scaler.fit_transform(df)

df_scaled = pd.DataFrame(X_scaled, columns=df.columns, index=df.index)
print("\n【标准化后数据(前5行)】")
print(df_scaled.head().round(3).to_string())

5.3 看一眼协方差矩阵

# 协方差矩阵揭示变量间的线性关联程度
cov_matrix = pd.DataFrame(
    np.cov(X_scaled.T),
    index=df.columns,
    columns=df.columns
)
print("\n【协方差矩阵】")
print(cov_matrix.round(3).to_string())

5.4 跑 PCA,看方差解释率

pca = PCA()
X_pca = pca.fit_transform(X_scaled)

explained_var  = pca.explained_variance_ratio_
cumulative_var = np.cumsum(explained_var)
eigenvalues    = pca.explained_variance_

summary = pd.DataFrame({
    '主成分':        [f'PC{i+1}' for i in range(len(explained_var))],
    '特征值':        eigenvalues.round(4),
    '方差解释率(%)': (explained_var * 100).round(2),
    '累计解释率(%)': (cumulative_var * 100).round(2),
}).set_index('主成分')

print("\n【PCA 方差解释率汇总】")
print(summary.to_string())
print(f"\n→ 保留前2个主成分,可解释总方差的 {cumulative_var[1]*100:.1f}%")
print(f"→ 保留前3个主成分,可解释总方差的 {cumulative_var[2]*100:.1f}%")

5.5 主成分载荷矩阵——"彩蛋"在这里揭晓

loadings = pd.DataFrame(
    pca.components_.T,
    index=df.columns,
    columns=[f'PC{i+1}' for i in range(len(explained_var))]
)

print("\n【主成分载荷矩阵(前3个主成分)】")
print(loadings.iloc[:, :3].round(4).to_string())
print("\n解读:绝对值越大,该科目对该主成分的贡献越大")

5.6 降维到 2D

pca_2d = PCA(n_components=2)
X_2d   = pca_2d.fit_transform(X_scaled)

df_2d = pd.DataFrame(
    X_2d,
    columns=['PC1(理科维度)', 'PC2(文科维度)'],
    index=df.index
)

print("\n【降维后数据(2D)】")
print(df_2d.round(3).to_string())

5.7 可视化:碎石图 + 散点图

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 碎石图(Scree Plot)
ax1 = axes[0]
pcs = [f'PC{i+1}' for i in range(6)]
ax1.bar(pcs, explained_var * 100, color='steelblue', alpha=0.7, label='单个解释率')
ax1.plot(pcs, cumulative_var * 100, 'ro-', linewidth=2, label='累计解释率')
ax1.axhline(y=85, color='gray', linestyle='--', alpha=0.6, label='85% 阈值')
ax1.set_xlabel('主成分')
ax1.set_ylabel('方差解释率 (%)')
ax1.set_title('碎石图(Scree Plot)')
ax1.legend()
ax1.grid(axis='y', alpha=0.3)

# 2D 散点图
ax2 = axes[1]
ax2.scatter(X_2d[:, 0], X_2d[:, 1],
            c='steelblue', s=80, alpha=0.8, edgecolors='white')
for i, name in enumerate(df.index):
    ax2.annotate(name, (X_2d[i, 0], X_2d[i, 1]),
                 fontsize=7, ha='center', va='bottom')
ax2.axhline(0, color='gray', linewidth=0.8, linestyle='--')
ax2.axvline(0, color='gray', linewidth=0.8, linestyle='--')
ax2.set_xlabel(f'PC1(解释方差 {explained_var[0]*100:.1f}%)')
ax2.set_ylabel(f'PC2(解释方差 {explained_var[1]*100:.1f}%)')
ax2.set_title('学生成绩 PCA 二维投影')
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.savefig('pca_result.png', dpi=150, bbox_inches='tight')
plt.show()
print("\n图表已保存为 pca_result.png")

六、结果怎么读?

跑完之后,你大概会看到这样的输出:

【PCA 方差解释率汇总】
         特征值   方差解释率(%)  累计解释率(%)
主成分
PC1    2.8934      48.22        48.22
PC2    1.7651      29.42        77.64
PC3    0.6123      10.21        87.85
PC4    0.3812       6.35        94.20
PC5    0.2104       3.51        97.71
PC6    0.1376       2.29       100.00

→ 保留前2个主成分,可解释总方差的 77.6%
→ 保留前3个主成分,可解释总方差的 87.9%

彩蛋揭晓:PC1 的载荷集中在数学、物理、化学,PC2 的载荷集中在语文、历史、英语。PCA 什么都没被告知,却自己把"理科能力"和"文科能力"这两个潜在维度给挖出来了。这正是它迷人的地方——数据自己会说话,你只需要给它一个说话的机会

仅凭两个主成分,就能解释原始 6 维数据约 77% 的信息。如果加上第三个主成分,覆盖率直接跳到 88%


七、用之前,先想清楚这几件事

PCA 好用,但不是万能药,有几个坑踩过才知道疼:

  • 它只认线性关系。数据如果是螺旋形、环形这类非线性结构,PCA 基本束手无策,这时候可以考虑 Kernel PCA 或 UMAP。
  • 主成分不好解释。原始变量有名字有意义,主成分是线性组合,物理含义往往模糊——上面那个例子算是运气好,现实数据未必如此清晰。
  • 异常值会把结果带偏。一个极端值就能拉歪协方差矩阵,跑 PCA 之前最好先做异常值处理。
  • 标准化不能跳过。如果一个特征的方差天然就很大(比如房价 vs 房间数),不标准化的话 PCA 会把注意力全放在那个特征上,其他的就被忽视了。
  • 降维必然有代价。丢掉的那部分方差,是真的丢了。选几个主成分,本质上是在"信息保留"和"简洁性"之间做取舍,没有绝对正确答案。

结语

维度要点
本质线性变换,找数据方差最大的正交方向
数学基础特征值分解 / 奇异值分解(SVD)
核心价值降维、去相关、可视化、消除多重共线性
典型用途机器学习预处理、探索性分析、图像压缩、基因组学
选主成分数累计解释率 ≥ 85% 是常用经验标准

PCA 已经有一百多年历史了,但它在今天的数据科学工作流里依然是标配。理解它的数学骨架,比单纯会调用 sklearn.decomposition.PCA 重要得多——只有知道它在算什么,你才能真正信任它给出的结果,也才能在它失效的时候,知道该换什么工具。


参考资料

  • IBM Think — What is Principal Component Analysis (PCA)?
  • Huda Swati — Understanding Principal Component Analysis (PCA), Medium
  • Ian T. Jolliffe & Jorge Cadima — Principal component analysis: a review and recent developments, Philosophical Transactions of the Royal Society A, 2016
  • Jonathon Shlens — A Tutorial on Principal Component Analysis, CMU / Salk Institute, 2005