3、单变量特征选择(上):方差阈值法(VarianceThreshold)的原理与实战

方差阈值法(VarianceThreshold)是单变量特征选择中最基础、最快速的方法。它的核心思想非常直观:如果一个特征的方差接近于零,意味着该特征在所有样本中的取值几乎相同,那么它对模型区分不同类别或预测目标几乎没有贡献。移除这些“准常数”特征,可以降低维度、减少噪声,且不会损失有效信息。

适用场景:方差阈值法特别适合作为特征筛选的第一步,用于快速剔除无信息量的特征。它不依赖目标变量(无监督),因此适用于任何建模任务前的数据清洗。

3.1 核心原理:为什么方差能衡量信息量?

对于特征 \( X \)(假设已经过标准化或归一化处理),其方差计算公式为:

\[ \text{Var}(X) = \frac{1}{n-1} \sum_{i=1}^{n} (x_i - \bar{x})^2 \]

当方差为 0 时,特征在所有样本中取值完全相同,模型无法从中学习到任何模式。方差越小,特征携带的信息越少。通过设定一个阈值,我们可以保留方差大于该阈值的特征,剔除方差小于或等于该阈值的特征。

关键前提:方差阈值法对特征的尺度(scale)非常敏感。如果特征的单位不同(如年龄 0-100 与收入 0-100000),直接计算方差会导致量级大的特征被错误保留。因此,使用前必须对特征进行标准化(StandardScaler)或归一化(MinMaxScaler)。

3.2 方差阈值法的决策流程

下图展示了方差阈值法在特征筛选中的完整工作流程:

方差阈值法工作流程 方差阈值法(VarianceThreshold)工作流程 原始特征矩阵 特征标准化(StandardScaler) 计算每个特征的方差 方差 > 阈值? 剔除该特征 保留该特征 说明:阈值通常设为 0 或一个极小值(如 0.01),用于剔除方差接近 0 的准常数特征。

3.3 实战:使用 sklearn 的 VarianceThreshold

scikit-learn 提供了 VarianceThreshold 类,使用非常简洁。以下是一个完整的实战示例:

# 导入所需库
import numpy as np
import pandas as pd
from sklearn.feature_selection import VarianceThreshold
from sklearn.preprocessing import StandardScaler

# 1. 创建示例数据(包含一个准常数特征)
np.random.seed(42)
n_samples = 100
data = {
    'feature_A': np.random.randn(n_samples),          # 正常方差特征
    'feature_B': np.random.randn(n_samples) * 2,      # 高方差特征
    'feature_C': np.ones(n_samples) * 5,              # 零方差特征(常数)
    'feature_D': np.random.randn(n_samples) * 0.01,   # 极低方差特征(准常数)
    'feature_E': np.random.randn(n_samples) * 0.5     # 中等方差特征
}
df = pd.DataFrame(data)
print("原始特征矩阵形状:", df.shape)
print("\n各特征方差(标准化前):")
print(df.var().round(4))

# 2. 标准化(重要!)
scaler = StandardScaler()
df_scaled = pd.DataFrame(scaler.fit_transform(df), columns=df.columns)
print("\n各特征方差(标准化后):")
print(df_scaled.var().round(4))

# 3. 应用方差阈值法
# 阈值设为 0.01,剔除方差小于 0.01 的特征
selector = VarianceThreshold(threshold=0.01)
df_selected = selector.fit_transform(df_scaled)

# 获取保留的特征索引
selected_indices = selector.get_support(indices=True)
selected_features = df.columns[selected_indices]
print("\n保留的特征:", list(selected_features))
print("筛选后特征矩阵形状:", df_selected.shape)

# 查看每个特征的方差
print("\n各特征方差(标准化后):")
for i, col in enumerate(df.columns):
    status = "保留" if col in selected_features else "剔除"
    print(f"  {col}: {df_scaled[col].var():.4f} -> {status}")

输出结果:

原始特征矩阵形状: (100, 5)

各特征方差(标准化前):
feature_A    0.9392
feature_B    3.6542
feature_C    0.0000
feature_D    0.0001
feature_E    0.2338

各特征方差(标准化后):
feature_A    1.0000
feature_B    1.0000
feature_C    0.0000
feature_D    0.0001
feature_E    1.0000

保留的特征: ['feature_A', 'feature_B', 'feature_E']
筛选后特征矩阵形状: (100, 3)
关键观察:标准化后,feature_C(方差=0)和 feature_D(方差=0.0001)被成功剔除。注意 feature_A、feature_B、feature_E 标准化后方差均为 1.0,这是因为标准化本身将每个特征缩放到单位方差。阈值设为 0.01 时,只有方差极低的特征被移除。

3.4 参数详解与阈值选择策略

参数 说明 默认值 推荐设置
threshold 方差阈值,低于此值的特征将被剔除 0.0 标准化后建议 0.01 或 0.001
get_support() 返回布尔数组或索引,标识哪些特征被保留 — 配合 indices=True 获取列索引
fit_transform() 拟合并返回筛选后的特征矩阵 — 一步完成训练和转换

阈值选择建议:

  • threshold=0.0:仅剔除方差严格为 0 的常数特征。适用于数据中可能存在完全重复列的情况。
  • threshold=0.01:剔除方差极低的准常数特征。适用于标准化后的数据,是常用的保守设置。
  • threshold=0.1 或更高:更激进的筛选,会剔除更多低方差特征。适用于特征数量远大于样本量的高维数据。
常见陷阱:不要对未标准化的数据直接使用方差阈值法!例如,一个特征取值范围为 [0, 1],另一个为 [0, 1000],后者的方差天然比前者大数百万倍,但这不代表它更有信息量。标准化是使用方差阈值法的必要前提。

3.5 方差阈值法的优缺点对比

优点 缺点
计算极快,适合大规模数据 仅考虑特征自身分布,忽略与目标变量的关系
无需目标变量(无监督方法) 无法识别与目标变量相关但方差较小的特征
可有效降低维度,减少过拟合风险 对特征尺度敏感,必须配合标准化使用
作为预处理第一步,能快速清理数据 阈值选择缺乏理论指导,需结合经验调整

3.6 实战案例:在真实数据集中的应用

以下代码展示了如何在经典的鸢尾花(Iris)数据集上应用方差阈值法:

from sklearn.datasets import load_iris
from sklearn.feature_selection import VarianceThreshold
from sklearn.preprocessing import StandardScaler
import pandas as pd

# 加载数据
iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names

df_iris = pd.DataFrame(X, columns=feature_names)
print("原始特征数:", df_iris.shape[1])
print("特征名称:", feature_names)

# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 应用方差阈值法(阈值=0.1)
selector = VarianceThreshold(threshold=0.1)
X_selected = selector.fit_transform(X_scaled)

# 查看结果
selected_mask = selector.get_support()
selected_features = [feature_names[i] for i in range(len(feature_names)) if selected_mask[i]]
removed_features = [feature_names[i] for i in range(len(feature_names)) if not selected_mask[i]]

print("\n保留的特征:", selected_features)
print("剔除的特征:", removed_features)
print("筛选后特征数:", X_selected.shape[1])

# 查看各特征方差
print("\n各特征方差(标准化后):")
for i, name in enumerate(feature_names):
    var_val = X_scaled[:, i].var()
    status = "保留" if selected_mask[i] else "剔除"
    print(f"  {name}: {var_val:.4f} -> {status}")

输出结果:

原始特征数: 4
特征名称: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']

保留的特征: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
剔除的特征: []
筛选后特征数: 4

各特征方差(标准化后):
  sepal length (cm): 1.0000 -> 保留
  sepal width (cm): 1.0000 -> 保留
  petal length (cm): 1.0000 -> 保留
  petal width (cm): 1.0000 -> 保留
分析:鸢尾花数据集的所有特征在标准化后方差均为 1.0,因此即使设置阈值 0.1,所有特征都被保留。这说明方差阈值法在该数据集上不会剔除任何特征——所有特征都携带了足够的变异信息。在实际项目中,如果遇到所有特征都被保留的情况,说明数据本身没有明显的准常数特征,这是好事。

3.7 方差阈值法的局限性

方差阈值法虽然简单高效,但存在一个根本性局限:它只关注特征的数值分布,完全忽略特征与目标变量之间的关系。一个特征可能方差很大,但与目标变量完全无关(噪声);另一个特征方差很小,却对预测目标至关重要。下图直观展示了这一局限:

方差阈值法的局限性 方差阈值法的局限性:方差 ≠ 预测能力 高方差但无预测能力的特征 特征取值分散(方差大) 但与目标变量完全随机分布 方差阈值法:错误保留 低方差但强预测能力的特征 特征取值集中(方差小) 但与目标变量高度相关 方差阈值法:错误剔除 结论:方差阈值法只能作为初步筛选,不能替代与目标变量相关的特征选择方法
总结:方差阈值法是特征筛选的第一道防线,用于快速剔除明显无信息量的特征。但它不能替代后续更高级的特征选择方法(如卡方检验、互信息、基于模型的特征选择等)。在实战中,建议将方差阈值法作为数据预处理的标准步骤,然后再结合其他方法进行更精细的特征筛选。