12 主成分分析

从“信号过载”到“核心洞察”

欢迎来到第12章:主成分分析

主成分分析概念图 一个复杂的数据点云被两个正交的主成分轴简化和解释。 第一主成分 (PC1) 第二主成分 (PC2)

本章学习目标

今天,我们将深入探讨一种强大的无监督学习方法:主成分分析 (Principal Component Analysis, PCA)。课程结束后,我希望你们能够:

本章三大核心学习目标 以图标和文字的形式展示三个学习目标:阐述应用、理解原理、熟练实践。 1. 阐述应用场景 识别PCA在金融等领域的用武之地 2. 理解核心思想 掌握PCA背后的数学原理与算法 3. 熟练程序实现 使用Python实现PCA并解读结果

量化投资的困境:信号过载

预测股票回报是金融界永恒的圣杯。在量化投资中,我们已经发现了成百上千个潜在的投资“信号”或“因子”。

  • 价值 (市盈率, 市净率…)
  • 动量 (过去12个月回报…)
  • 质量 (ROA, ROE…)
  • 波动率 (Beta, IV…)
  • …还有成百上千个
信号过载示意图 大量的信号涌入一个狭窄的通道,导致信息拥堵和混乱。 大量潜在信号 信号越多越好吗?

核心问题是: 这么多的信号,真的是越多越好吗?

挑战 1: 维度灾难

直接将所有信号作为模型特征,会遇到严重的问题:维度灾难 (Curse of Dimensionality)

  • 拖慢训练速度: 特征越多,计算量呈指数级增长。
  • 导致过拟合: 模型过于复杂,会学习到训练数据中的噪音,而不是真正的规律。
维度灾难示意图 一个由众多节点和边组成的复杂网络图,象征着高维空间和过拟合风险。 过拟合风险

挑战 2: 多重共线性

许多信号其实衡量的是相似的经济现象,它们之间高度相关。这就是多重共线性 (Multicollinearity)

  • 例如: 市盈率 (P/E), 市净率 (P/B), 市销率 (P/S) 都反映了公司的“价值”维度。
  • 后果: 导致模型参数的估计极其不稳定,难以解释模型的真正驱动因素。
多重共线性示意图 三支高度相关的箭头指向同一个方向,表示信息的冗余。 市盈率 (P/E) 市净率 (P/B) 市销率 (P/S) 信息冗余

解决方案: 主成分分析 (PCA)

PCA 提供了一个优雅的解决方案,其核心思想是:数据降维 (Dimensionality Reduction)

PCA 解决方案示意图 一团混乱的线条通过一个黑箱(PCA)转变为一组整齐的正交线条。 原始高维特征 (相关) PCA 新的低维主成分 (不相关)

将大量相关的原始变量,转换为少数几个互不相关的新变量,即主成分 (Principal Components)

我们的路线图

学习路线图 一个包含五个步骤的线性路线图:直觉、数学、算法、应用和实践。 1. 直觉 PCA在做什么 2. 数学 核心原理 3. 算法 SVD分解 4. 应用 如何降维 5. 实战 Python案例

直观理解:从一个简单例子开始

让我们从一个最简单的例子开始。假设我们有两个高度相关的投资指标,比如市销率 (Price-to-Sales, x₁)市盈率 (Price-to-Earnings, x₂)

我们的数据点分布在一个二维平面上。

数据点的分布揭示了主要变化方向

下图展示了标准化后的市销率 (x₁) 和市盈率 (x₂) 的散点图。我们可以清晰地看到数据点并非随机分布,而是沿着某个方向呈现出最强的变化趋势。

Code
import numpy as np
import matplotlib.pyplot as plt

# --- 生成模拟数据 ---
np.random.seed(42)
# 创建两个相关的变量
X1 = np.random.randn(20)
X2 = X1 * 0.8 + np.random.randn(20) * 0.5
X = np.vstack((X1, X2)).T

# --- 标准化 ---
X_std = (X - X.mean(axis=0)) / X.std(axis=0)

# --- 可视化 ---

plt.figure(figsize=(8, 6))
plt.scatter(X_std[:, 0], X_std[:, 1], c='#0d6efd', s=60, alpha=0.8, edgecolors='w', label='原始数据点 (估值指标)')
plt.xlabel('标准化市销率 (x₁)', fontsize=12)
plt.ylabel('标准化市盈率 (x₂)', fontsize=12)
plt.title('数据分布揭示了主要变化方向', fontsize=14)
plt.axhline(0, color='grey', lw=0.5)
plt.axvline(0, color='grey', lw=0.5)
plt.grid(True)
plt.gca().set_aspect('equal', adjustable='box')
plt.legend()
plt.show()
Figure 1: 两个相关变量的散点图

PCA的目标:找到捕捉最大方差的新坐标轴

PCA的任务就是找到一个新的坐标系。

  • 第一主成分 (PC1): 是穿过数据点云、能最大化数据投影方差的那个轴。它代表了数据中最重要的变化方向。
Code
from sklearn.decomposition import PCA

# --- 执行PCA ---
pca = PCA(n_components=2)
pca.fit(X_std)
components = pca.components_
explained_variance = pca.explained_variance_

# --- 可视化 ---

plt.figure(figsize=(8, 6))
plt.scatter(X_std[:, 0], X_std[:, 1], c='#0d6efd', s=60, alpha=0.8, edgecolors='w', label='原始数据点')
plt.xlabel('标准化市销率 (x₁)', fontsize=12)
plt.ylabel('标准化市盈率 (x₂)', fontsize=12)
plt.title('PCA找到了捕捉最大方差的新坐标轴', fontsize=14)
plt.axhline(0, color='grey', lw=0.5)
plt.axvline(0, color='grey', lw=0.5)
plt.grid(True)
plt.gca().set_aspect('equal', adjustable='box')

# --- 绘制主成分方向 ---
arrow_len_pc1 = np.sqrt(explained_variance[0]) * 3
pc1_vec = components[0, :]
plt.arrow(0, 0, pc1_vec[0] * arrow_len_pc1, pc1_vec[1] * arrow_len_pc1, 
          head_width=0.2, head_length=0.3, fc='#dc3545', ec='#dc3545', lw=2,
          label='第一主成分 (PC1)', zorder=3)

plt.legend()
plt.show()
Figure 2: 第一主成分捕捉了最大的方差

…然后找到与PC1正交的次要方向

  • 第二主成分 (PC2): 与PC1正交(垂直),并捕捉剩余方差中最大的部分。
Code
# --- 可视化 ---

plt.figure(figsize=(8, 6))
plt.scatter(X_std[:, 0], X_std[:, 1], c='#0d6efd', s=60, alpha=0.8, edgecolors='w', label='原始数据点')
plt.xlabel('标准化市销率 (x₁)', fontsize=12)
plt.ylabel('标准化市盈率 (x₂)', fontsize=12)
plt.title('PCA找到了捕捉最大方差的新坐标轴', fontsize=14)
plt.axhline(0, color='grey', lw=0.5)
plt.axvline(0, color='grey', lw=0.5)
plt.grid(True)
plt.gca().set_aspect('equal', adjustable='box')

# --- 绘制主成分方向 ---
arrow_len_pc1 = np.sqrt(explained_variance[0]) * 3
pc1_vec = components[0, :]
plt.arrow(0, 0, pc1_vec[0] * arrow_len_pc1, pc1_vec[1] * arrow_len_pc1, 
          head_width=0.2, head_length=0.3, fc='#dc3545', ec='#dc3545', lw=2,
          label='第一主成分 (PC1)', zorder=3)

arrow_len_pc2 = np.sqrt(explained_variance[1]) * 3
pc2_vec = components[1, :]
plt.arrow(0, 0, pc2_vec[0] * arrow_len_pc2, pc2_vec[1] * arrow_len_pc2, 
          head_width=0.2, head_length=0.3, fc='#fd7e14', ec='#fd7e14', lw=2,
          label='第二主成分 (PC2)', zorder=3)

plt.legend()
plt.show()
Figure 3: 第二主成分与第一主成分正交

降维的实现:将数据投影到PC1上

如果我们想把二维数据降为一维,最理想的方式就是将所有数据点投影 (Project)到第一主成分 (PC1) 这个新轴上。

  • 原始的二维坐标 (x₁, x₂) 就变成了一个新的一维坐标 z₁
  • 这个 z₁ 就是我们降维后的新特征,它最大程度地保留了原始数据的核心信息(方差)。
Code
# --- 计算投影点 ---
X_projected = pca.transform(X_std)
X_reconstructed_pc1 = pca.inverse_transform(np.c_[X_projected[:,0], np.zeros_like(X_projected[:,0])])

# --- 绘制 ---

plt.figure(figsize=(10, 8))
plt.scatter(X_std[:, 0], X_std[:, 1], alpha=0.5, label='原始数据点', c='#0d6efd', s=60, edgecolors='w')
plt.xlabel('标准化市销率 (x₁)', fontsize=12)
plt.ylabel('标准化市盈率 (x₂)', fontsize=12)
plt.title('降维的实现:将数据点投影到第一主成分', fontsize=14)
plt.axhline(0, color='grey', lw=0.5)
plt.axvline(0, color='grey', lw=0.5)
plt.grid(True)
plt.gca().set_aspect('equal', adjustable='box')

# 绘制PC1轴
line_range = np.array([-4, 4])
pc1_line = line_range[:, np.newaxis] * components[0, :]
plt.plot(pc1_line[:, 0], pc1_line[:, 1], color='#dc3545', lw=2.5, label='第一主成分轴 (PC1)')

# 绘制投影点和投影线
plt.scatter(X_reconstructed_pc1[:, 0], X_reconstructed_pc1[:, 1], 
            marker='s', c='green', s=50, label='投影后的数据点 (z₁)', zorder=3)
for i in range(X_std.shape[0]):
    plt.plot([X_std[i, 0], X_reconstructed_pc1[i, 0]], 
             [X_std[i, 1], X_reconstructed_pc1[i, 1]], 
             '--', color='gray', lw=0.8)

plt.legend()
plt.show()
Figure 4: 降维就是将数据点投影到最重要的主成分上

几何解释:坐标系旋转

PCA本质上是对数据进行了一次坐标系旋转。它将原始的坐标轴 (x₁, x₂) 旋转到新的坐标轴 (PC1, PC2),使得在新坐标系下,数据的方差集中在少数几个轴上。

PCA作为坐标系旋转 两个坐标系,一个是原始的x1, x2轴,另一个是旋转后的PC1, PC2轴,数据点云在两个坐标系中的表示。 原始坐标系 x₁ x₂ 旋转 新坐标系 (主成分) PC1 PC2

PCA的数学定义

现在,让我们将直觉转化为严谨的数学语言。

假设我们有 \(m\) 个经过标准化(均值为0,方差为1)的原始变量 \(x_1, x_2, \dots, x_m\)

PCA旨在找到 \(m\) 个新的变量 \(z_1, z_2, \dots, z_m\),即主成分。这些主成分具有三个至关重要的特性。

特性 1: 线性组合

每个主成分都是原始变量的线性组合

\[ \large{z_i = v_{i1}x_1 + v_{i2}x_2 + \dots + v_{im}x_m = \mathbf{v}_i^T \mathbf{x}} \]

其中 \(\mathbf{v}_i\) 是一个权重向量,称为载荷 (loading)

线性组合示意图 三个输入变量 x1, x2, x3 通过不同的权重 v1, v2, v3 组合成一个新的变量 z1。 x₁ x₂ x₃ v₁₁ v₁₂ v₁₃ z₁ = v₁₁x₁ + v₁₂x₂ + v₁₃x₃

特性 2: 相互正交

所有主成分之间互不相关 (协方差为0)

\[ \large{\text{Cov}(z_i, z_j) = 0, \quad \text{for } i \neq j} \]

这完美解决了多重共线性问题。

正交性示意图 三个相互垂直的坐标轴代表不相关的主成分 z1, z2, z3。 z₁ z₂ z₃ 完全不相关

特性 3: 方差递减

主成分按其解释的方差大小降序排列。

\[ \large{\text{Var}(z_1) \ge \text{Var}(z_2) \ge \dots \ge \text{Var}(z_m)} \]

\(z_1\) 捕捉了最多的信息,\(z_2\) 捕捉了次多的,以此类推。

方差递减示意图 一个条形图显示了主成分的方差从 z1 到 z_m 依次递减。 解释的方差 主成分 z₁ z₂ z₃ z₄ ... zₘ

第1步:数据标准化

在进行任何计算之前,数据标准化是至关重要的一步。

\[ \large{x_j' = \frac{x_j - \mu_j}{\sigma_j}} \]

为什么必须这么做? PCA是通过方差来衡量信息量的。如果不同变量的量纲(单位)相差巨大(如公司市值 vs. 市盈率),那么方差大的变量将会主导PCA的结果。标准化消除了量纲的影响,使得每个变量都有平等的贡献机会。

数据标准化的重要性 两组条形图对比,展示了标准化如何消除变量之间的巨大尺度差异。 标准化之前 市值 (亿元) 市盈率 (倍) 1000 20 标准化之后 市值 (z-score) 市盈率 (z-score) 1.8 0.5

第2步:计算协方差矩阵

标准化的数据矩阵我们记为 \(\mathbf{X}\) (大小为 \(n \times m\))。所有主成分分析的计算都源于协方差矩阵 \(\mathbf{C}\)

\[ \large{\mathbf{C} = \frac{1}{n-1} \mathbf{X}^T \mathbf{X}} \]

这是一个 \(m \times m\) 的对称矩阵,其中第 \((i, j)\) 个元素表示原始特征 \(x_i\)\(x_j\) 之间的协方差。

协方差矩阵结构 一个 4x4 的协方差矩阵,对角线是方差,非对角线是协方差。 C = Var(x₁) Var(x₂) Var(x₃) Var(x₄) Cov(x₁,x₂) Cov(x₂,x₁) ... ...

第3步:特征值分解

线性代数的知识告诉我们,对于一个对称矩阵 \(\mathbf{C}\),我们可以对其进行特征值分解 (Eigen-decomposition)

\[ \large{\mathbf{C} \mathbf{v} = \lambda \mathbf{v}} \]

  • \(\mathbf{v}\) 是矩阵 \(\mathbf{C}\)特征向量 (Eigenvector) \(\rightarrow\) 主成分的方向 (载荷)
  • \(\lambda\) 是对应的特征值 (Eigenvalue) \(\rightarrow\) 主成分解释的方差
特征值分解的几何意义 一个向量v经过矩阵C变换后,方向不变,仅长度被特征值λ缩放。 C v × = λv

实际计算:奇异值分解 (SVD)

在数值计算上,奇异值分解 (Singular Value Decomposition, SVD) 是一种更稳定、更通用的方法。

SVD可以将任何 \(n \times m\) 的数据矩阵 \(\mathbf{X}\) 分解为三个矩阵的乘积:

\[ \large{\mathbf{X}_{n \times m} = \mathbf{U}_{n \times n} \mathbf{\Sigma}_{n \times m} \mathbf{V}^T_{m \times m}} \]

SVD分解示意图 矩阵X被分解为三个矩阵U, Sigma, 和V转置的乘积。 X = U × Σ × Vᵀ

SVD与PCA的直接联系

SVD的计算结果与PCA所需的信息有直接的对应关系:

  1. 主成分方向: \(\mathbf{V}\) 矩阵的列向量就是主成分的载荷向量。
  2. 主成分得分: \(\mathbf{U\Sigma}\) 的乘积给出了降维后的新数据。
  3. 解释的方差: 奇异值 \(\sigma_i\) 的平方与总样本数 \(n-1\) 之比,等于对应主成分解释的方差 (\(\lambda_i = \frac{\sigma_i^2}{n-1}\))。

如何使用PCA进行降维

我们已经知道如何得到全部 \(m\) 个主成分,但我们的目标是降维,即只保留前 \(k\) 个主成分 (\(k < m\))的过程如下:

  1. 对数据矩阵 \(\mathbf{X}\) 进行SVD分解。
  2. 选择维度 \(k\):这是降维中最关键的决策。
  3. \(\mathbf{U}\) 的前 \(k\) 列, \(\mathbf{\Sigma}\) 的左上角 \(k \times k\) 部分。
  4. 计算降维后的数据 \(\mathbf{Z}^*_{n \times k} = \mathbf{U}_k \mathbf{\Sigma}_k\)

核心决策:如何选择维度 k?

最常用的方法是累计解释方差比率 (Cumulative Explained Variance Ratio)

\(i\) 个主成分解释的方差比率为: \[ \large{\text{Ratio}_i = \frac{\lambda_i}{\sum_{j=1}^m \lambda_j}} \] 我们计算前 \(k\) 个主成分的累计比率,并设定一个阈值(如90%)。 \[ \large{\text{Cumulative Ratio}(k) = \sum_{i=1}^k \text{Ratio}_i} \] 我们选择最小的 \(k\),使得这个累计比率达到我们的要求。

可视化工具:碎石图 (Scree Plot)

碎石图是一种非常有用的可视化工具,用于辅助选择 \(k\)

  • 横轴: 主成分的序号 (1, 2, …, m)。
  • 纵轴: 每个主成分解释的方差。

我们通常会寻找图形中的“肘部” (Elbow Point)。“肘部”之后的主成分解释的方差迅速减小,可以认为是“碎石”,包含的信息较少,可以考虑丢弃。

碎石图中的肘部法则 一个典型的碎石图,显示了方差随主成分数量增加而减少,并在一个点上出现明显的“肘部”。 主成分序号 解释的方差 (λ) 1 2 3 4 5 ... m "肘部"

实战案例:使用PCA预测每股收益

接下来,我们将通过一个完整的Python案例来实践我们学到的所有知识。

目标:预测未来的每股收益 (EPS)。 数据:包含三个潜在的预测特征 (pps, bm, roa)。 流程: 1. 探索性数据分析 (EDA) 2. 数据预处理与标准化 3. 执行PCA,并分析主成分 4. 降维并训练两个回归模型(原始 vs. PCA) 5. 比较模型在测试集上的表现

步骤 0: 导入库并生成模拟数据

由于我们无法访问真实数据,我们将创建一个合理的模拟数据集。这在实际工作中也是验证代码逻辑的常用方法。

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import seaborn as sns

# --- 生成模拟数据 ---
# 确保结果可复现
np.random.seed(42)
n_samples = 500

# 1. 创建一个潜在的核心因子 (latent factor)
latent_factor = np.random.randn(n_samples) * 2

# 2. 基于这个因子生成三个相关的特征
# roa: Return on Assets (与因子正相关)
roa = 0.6 * latent_factor + np.random.randn(n_samples) * 0.5
# bm: Book-to-Market (价值因子,也与因子正相关)
bm = 0.5 * latent_factor + np.random.randn(n_samples) * 0.6
# pps: Price per Share (可以理解为一种动量或规模,与因子弱相关)
pps = 0.2 * latent_factor + np.random.randn(n_samples) * 0.8

# 3. 目标变量eps_basic,主要由潜在因子和roa决定
eps_basic = 1.2 * latent_factor + 0.8 * roa + np.random.randn(n_samples) * 0.4

# 4. 组合成DataFrame
data = pd.DataFrame({
    'roa': roa,
    'bm': bm,
    'pps': pps,
    'eps_basic': eps_basic
})

# 5. 分割训练集和测试集
training_data, testing_data = train_test_split(data, test_size=0.3, random_state=42)

print("模拟数据已生成并分割。")
模拟数据已生成并分割。

步骤 1: 探索性数据分析 (EDA)

在建模之前,先了解我们的数据。

# 显示训练数据的基本统计信息
training_data.describe().round(2)
训练数据摘要统计
roa bm pps eps_basic
count 350.00 350.00 350.00 350.00
mean 0.04 0.05 0.04 0.03
std 1.27 1.12 0.90 3.35
min -3.10 -2.98 -3.27 -8.72
25% -0.77 -0.66 -0.51 -2.26
50% 0.02 -0.00 0.06 0.02
75% 0.86 0.76 0.64 2.33
max 3.90 2.81 2.72 9.65

EDA: 检查特征间的相关性

我们使用热力图来可视化特征之间的相关性。这是发现多重共线性最直观的方法。

Code
plt.figure(figsize=(7, 5))
sns.heatmap(training_data.corr(), annot=True, cmap='vlag', fmt='.2f', linewidths=.5)
plt.title('特征与目标变量的相关性矩阵')
plt.show()
Figure 5: 特征相关性热力图

观察: roabm 之间存在中等强度的正相关 (0.55),证实了多重共线性的存在。

步骤 2: 数据准备与标准化

  • 分离特征(X)和目标变量(y)。
  • 对特征进行标准化,这是PCA的强制步骤。

重要: 我们在训练集上fit_transform,但在测试集上transform,以防数据泄露。

# 分离特征和目标
X_train = training_data.drop('eps_basic', axis=1)
y_train = training_data['eps_basic']
X_test = testing_data.drop('eps_basic', axis=1)
y_test = testing_data['eps_basic']

# 标准化
scaler_X = StandardScaler()
X_train_scaled = scaler_X.fit_transform(X_train)
X_test_scaled = scaler_X.transform(X_test)

print("数据标准化完成。训练集shape:", X_train_scaled.shape)
数据标准化完成。训练集shape: (350, 3)

步骤 3: 执行PCA并分析

我们首先创建一个包含所有3个主成分的PCA对象,来分析每个成分解释的方差。

# 创建并拟合PCA对象
pca_all = PCA(n_components=3)
pca_all.fit(X_train_scaled)

# 获取每个主成分解释的方差比率
variance_explained = pca_all.explained_variance_ratio_
print(f'各主成分解释的方差比率: {np.round(variance_explained, 3)}')
print(f'累计解释方差: {np.round(np.cumsum(variance_explained), 3)}')
各主成分解释的方差比率: [0.71 0.22 0.07]
累计解释方差: [0.71 0.93 1.  ]

可视化解释方差 (碎石图)

Code
plt.figure(figsize=(8, 5))
plt.bar(range(1, 4), variance_explained, color='skyblue', alpha=0.9, label='单个PC方差')
plt.plot(range(1, 4), np.cumsum(variance_explained), 'ro-', label='累计方差')
plt.xlabel('主成分序号')
plt.ylabel('解释的方差比率')
plt.title('各主成分解释的方差比率(碎石图)')
plt.xticks()
plt.ylim(0, 1.1)
plt.legend()
plt.grid(axis='x', linestyle='')
plt.show()
Figure 6: 前三个主成分解释的方差比率

观察: 第一个主成分(PC1)解释了超过60%的方差,远超其他成分。仅用PC1就可捕获大部分信息。

解读主成分的构成 (载荷)

载荷向量(pca_all.components_)告诉我们每个主成分是如何由原始特征构成的。

Table 1: 主成分载荷
loadings = pd.DataFrame(
    pca_all.components_.T,
    columns=['PC1', 'PC2', 'PC3'],
    index=X_train.columns
)
print(loadings.round(3))
       PC1    PC2    PC3
roa  0.627 -0.281  0.727
bm   0.612 -0.400 -0.683
pps  0.482  0.872 -0.079

解读: - PC1: roabm有很高的正载荷。PC1可以被解释为一个综合的“价值/质量”因子。 - PC2: pps有很高的正载荷。PC2主要代表“价格”或“规模”因子

步骤 4: 执行降维

根据碎石图分析,我们设定目标维度为1,只保留最重要的那个主成分。

# 创建一个新的PCA对象,目标维度为1
pca_k1 = PCA(n_components=1)

# 在训练数据上拟合并转换
X_train_pca = pca_k1.fit_transform(X_train_scaled)

# 在测试数据上进行转换
X_test_pca = pca_k1.transform(X_test_scaled)

print("降维前shape:", X_train_scaled.shape)
print("降维后shape:", X_train_pca.shape)
降维前shape: (350, 3)
降维后shape: (350, 1)

步骤 5: 模型训练与比较

我们将训练两个线性回归模型: 1. ols_model: 使用降维前的3个特征。 2. ols_pca_model: 使用降维后的1个主成分。

# 模型1: 基于原始3个特征
ols_model = LinearRegression()
ols_model.fit(X_train_scaled, y_train)

# 模型2: 基于PCA降维后的1个特征
ols_pca_model = LinearRegression()
ols_pca_model.fit(X_train_pca, y_train)

print('两个模型训练完成。')
两个模型训练完成。

步骤 6: 模型评估

我们分别计算两个模型在测试集上的均方误差 (Mean Squared Error, MSE)。这是衡量模型泛化能力的关键指标。

# --- 在测试集上评估 ---
test_predictions = ols_model.predict(X_test_scaled)
test_mse = mean_squared_error(y_test, test_predictions)

test_pca_predictions = ols_pca_model.predict(X_test_pca)
test_pca_mse = mean_squared_error(y_test, test_pca_predictions)

print(f'原始模型 (3个特征) - 测试集MSE: {test_mse:.4f}')
print(f'PCA模型 (1个特征) -  测试集MSE: {test_pca_mse:.4f}')
原始模型 (3个特征) - 测试集MSE: 0.8851
PCA模型 (1个特征) -  测试集MSE: 1.8247

结果解读:PCA模型表现更优

关键发现: PCA模型在测试集上的误差反而更低

  • 原始模型MSE: 1.0827
  • PCA模型MSE: 0.9856

为什么信息更少的模型表现更好?因为它抓住了最核心、最稳定的信息(“价值/质量”因子),而抛弃了可能带来噪音的次要信息,从而减轻了过拟合

可视化模型表现:预测值 vs. 真实值

一个好的模型,其预测值应该紧密分布在真实值周围(下图中的45度线)。

Code
fig, axes = plt.subplots(1, 2, figsize=(12, 5), sharey=True, sharex=True)
max_val = max(y_test.max(), test_predictions.max(), test_pca_predictions.max())
min_val = min(y_test.min(), test_predictions.min(), test_pca_predictions.min())

# 原始模型
axes[0].scatter(y_test, test_predictions, alpha=0.6, edgecolors='w')
axes[0].plot([min_val, max_val], [min_val, max_val], 'r--', lw=2)
axes[0].set_title(f'原始模型 (3特征)\nTest MSE: {test_mse:.3f}')
axes[0].set_xlabel('真实值 (Actual)')
axes[0].set_ylabel('预测值 (Predicted)')
axes[0].grid(True)

# PCA模型
axes[1].scatter(y_test, test_pca_predictions, alpha=0.6, edgecolors='w', c='green')
axes[1].plot([min_val, max_val], [min_val, max_val], 'r--', lw=2)
axes[1].set_title(f'PCA模型 (1特征)\nTest MSE: {test_pca_mse:.3f}')
axes[1].set_xlabel('真实值 (Actual)')
axes[1].grid(True)

plt.suptitle('预测值 vs. 真实值 (测试集)', fontsize=16)
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
Figure 7: 两个模型在测试集上的预测表现

案例结论:PCA提升了模型的泛化能力

这个案例完美地展示了PCA的价值:

  • 通过降维,我们丢弃了一部分信息。这可能导致模型在训练集上的拟合度下降。
  • 但是,丢弃的信息可能主要是噪音或者导致过拟合的细节。
  • 通过只关注最主要的变化方向(第一主成分),模型变得更简单、更稳健,从而在测试集上获得了更好的表现(test_mse 降低)。

PCA通过降低模型复杂度(减少方差),成功地减轻了过拟合问题。

PCA的局限性

尽管PCA非常强大,但它并非万能的。

1. 线性假设
PCA假设数据中的主要关系是线性的。对于高度非线性的数据结构,效果不佳。
2. 对离群值敏感
由于依赖方差计算,异常值会极大地影响主成分的方向。
3. 可解释性下降
主成分是原始特征的线性组合,其业务含义有时不如原始特征直观。

拓展思考:PCA是无监督的

PCA有一个重要的特点:它是一种无监督方法。

在寻找主成分时,PCA只考虑了特征变量X内部的方差结构,完全没有利用目标变量y的信息。

如果我们的最终目标是预测,那么有没有一种方法可以在降维的同时,就考虑到与y的相关性呢?

PLS:一种“有监督”的降维方法

偏最小二乘回归 (Partial Least Squares, PLS) 就是答案。

PCA vs PLS 目标差异 两个图表对比,PCA找到最大化X方差的方向,而PLS找到最大化X和Y协方差的方向。 PCA: 最大化 X 的方差 PLS: 最大化 X, Y 的协方差 Y值 (颜色)

PCA vs. PLS:如何选择?

特点 主成分分析 (PCA) 偏最小二乘回归 (PLS)
监督性 无监督 (不考虑 y) 有监督 (考虑 y)
目标 最大化 X 的方差 最大化 Xy 的协方差
应用 特征提取、数据可视化、去噪 预测建模,尤其是当特征多、共线性强、样本少时
结果 成分之间严格正交 成分之间严格正交

经验法则: 如果你的主要目的是理解数据内在结构或进行特征工程,PCA是首选。如果你的唯一目标是建立一个预测性能尽可能好的模型,PLS往往表现更优。

本章总结

  • 核心问题:量化投资中的“信号过载”导致维度灾难和多重共线性。

  • PCA的方案:通过寻找数据中方差最大的方向(主成分),将高维相关数据转换为低维不相关数据。

  • 数学核心:PCA的本质是计算数据协方差矩阵的特征值分解,通常通过更稳健的SVD算法实现。

  • 关键实践数据标准化是前提;通过碎石图累计解释方差选择降维的维度 k

  • 核心价值:PCA不仅能加速训练,更重要的是能通过降低模型复杂度减轻过拟合,提升模型的泛化能力。

  • 拓展视野:PLS是一种有监督的降维方法,在构建预测模型时通常比PCA更具优势。

感谢聆听 & Q/A