特征工程基础

数据分析专题 · 从原始数据提取有效特征

专题:Python数据分析系统学习

关键词:数据分析, 特征工程, 特征编码, One-Hot, 标准化, 分箱, 特征选择, TF-IDF, Box-Cox

一、特征工程概述

特征工程(Feature Engineering)是机器学习流程中最关键也最耗时的环节之一。它指的是将原始数据转化为能更好表征问题本质、提升模型性能的特征的过程。业界有句广为流传的话:"数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限。"研究表明,特征工程在实际项目中的时间占比往往高达60%-80%。

特征工程涵盖的范畴非常广泛,主要包括以下几个方面:特征创建(从现有数据派生新特征)、特征编码(将非数值型数据转换为数值形式)、数值特征变换(调整数值分布和尺度)、特征分箱(将连续变量离散化)、缺失值处理(填补或标记缺失)、以及特征选择(从众多特征中挑选最有价值的子集)。本章将逐一深入讲解这些核心技术。

核心原则:好的特征应该具备以下特性——与目标变量有强相关性、具有足够的区分度、对噪声不敏感、在实际部署时易于获取和计算。特征工程不是一蹴而就的,而是一个反复迭代、不断优化的过程,需要结合业务理解和数据探索来逐步完善。

二、特征创建

特征创建是从已有数据中构造新特征的过程,旨在捕捉原始特征之间隐藏的关系或更高层次的信息。这是特征工程中最需要创造力的环节,也是提升模型性能最直接的手段之一。

2.1 多项式特征

多项式特征通过在原始特征的基础上引入幂次和交叉项来捕捉非线性关系。例如对于原始特征 x1x2,二阶多项式特征包括 x1, x2, x1^2, x2^2, x1*x2。这在线性模型无法拟合非线性关系时尤其有用。

import numpy as np from sklearn.preprocessing import PolynomialFeatures # 原始特征矩阵 X = np.array([[2, 3], [4, 5], [6, 7]]) # 生成二阶多项式特征(包含交互项) poly = PolynomialFeatures(degree=2, include_bias=False) X_poly = poly.fit_transform(X) print(X_poly) # 输出: [[ 2. 3. 4. 6. 9.] # [ 4. 5. 16. 20. 25.] # [ 6. 7. 36. 42. 49.]] # 列依次为: x1, x2, x1^2, x1*x2, x2^2 print(poly.get_feature_names_out()) # 输出: ['x0', 'x1', 'x0^2', 'x0 x1', 'x1^2']

2.2 交互特征

交互特征专门用于捕捉特征之间的协同效应。例如用户点击率与页面加载时间的交互作用可能比两者单独对转化率的影响更大。在树模型中交互效应可被自动捕捉,但在线性模型和部分深度学习模型中需要手动构造。

import pandas as pd from sklearn.preprocessing import PolynomialFeatures df = pd.DataFrame({ 'price': [100, 200, 150, 300], 'discount': [0.1, 0.2, 0.15, 0.25], 'category': ['A', 'B', 'A', 'B'] }) # 手动构造交互特征:价格 × 折扣 df['price_discount_interact'] = df['price'] * df['discount'] # 类别与数值的交互:按类别分组后乘以数值 df['category_encoded'] = (df['category'] == 'A').astype(int) df['cat_price_interact'] = df['category_encoded'] * df['price'] print(df) # 交互特征补充了乘法关系,线性模型可直接利用这些组合信号

2.3 日期特征分解

日期时间数据在原始形式下对大多数模型来说是无效的。需要将其分解为具有明确数学意义的数值特征:年、月、日、星期几、小时、是否为节假日、一年中的第几周等。时间序列预测中还可以提取滞后特征和滑动窗口统计量。

import pandas as pd import numpy as np # 构造日期序列 dates = pd.date_range(start='2024-01-01', periods=5, freq='D') df_dates = pd.DataFrame({'date': dates}) # 日期分解——基础特征 df_dates['year'] = df_dates['date'].dt.year df_dates['month'] = df_dates['date'].dt.month df_dates['day'] = df_dates['date'].dt.day df_dates['dayofweek'] = df_dates['date'].dt.dayofweek df_dates['quarter'] = df_dates['date'].dt.quarter df_dates['dayofyear'] = df_dates['date'].dt.dayofyear df_dates['is_weekend'] = (df_dates['dayofweek'] >= 5).astype(int) # 循环编码——适合周期性特征(月份、星期) df_dates['month_sin'] = np.sin(2 * np.pi * df_dates['month'] / 12) df_dates['month_cos'] = np.cos(2 * np.pi * df_dates['month'] / 12) # 滞后特征(lag features)——前N天的值 values = pd.Series([100, 110, 105, 120, 115]) df_dates['lag_1'] = values.shift(1) df_dates['lag_2'] = values.shift(2) # 滑动窗口统计量 df_dates['rolling_mean_3'] = values.rolling(window=3).mean() print(df_dates)

技巧提示:循环编码将周期性特征映射到单位圆上的正弦和余弦分量,避免了传统编码方式(如1月到12月)中"12月和1月距离很远"但实际很接近的问题。这在天气预测、销售季节性分析等场景中非常有效。

2.4 文本特征——TF-IDF与计数向量化

文本数据必须先转化为数值向量才能被模型处理。最基础的方法是计数向量化(CountVectorizer),它统计每个词在文档中出现的频次。更进阶的方法是TF-IDF,它降低了在大量文档中都出现的高频词的权重,提升了具有区分度的专有词的权重。

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer corpus = [ "特征工程是机器学习中最重要的环节", "好的特征决定了模型的上限", "特征工程需要结合业务理解进行反复迭代" ] # 计数向量化 count_vec = CountVectorizer() X_count = count_vec.fit_transform(corpus) print("词汇表:", count_vec.get_feature_names_out()) print("计数矩阵:\n", X_count.toarray()) # TF-IDF向量化 tfidf_vec = TfidfVectorizer() X_tfidf = tfidf_vec.fit_transform(corpus) print("\nTF-IDF矩阵:\n", X_tfidf.toarray()) # TF-IDF自动降低了"的"等无区分度词的权重

参数调优:CountVectorizer和TfidfVectorizer的关键参数包括:max_features(限制最大特征数避免维度灾难)、ngram_range(设置n-gram范围如(1,3)同时捕获单词和短语)、min_df(忽略出现频率低于阈值的词)、stop_words(去除停用词)。实际应用中通常设置为max_features=5000~10000、ngram_range=(1,2)。

三、特征编码

特征编码解决的是"如何将类别型数据转换为数值型数据"的问题。不同的编码策略对模型效果有显著影响,选择哪种编码方法取决于类别变量的基数(唯一值数量)、与目标变量的关系以及使用的模型类型。

3.1 One-Hot编码

One-Hot编码将每个类别值映射为一个二进制向量,其中只有该类别对应的位置为1,其余为0。这是最常用的编码方式,适用于基数较小的类别特征。但当类别数量很多时会导致"维度灾难"。

import pandas as pd from sklearn.preprocessing import OneHotEncoder df = pd.DataFrame({'color': ['red', 'blue', 'green', 'blue', 'red']}) # 方法一:pandas get_dummies df_onehot = pd.get_dummies(df, columns=['color'], prefix='color') print("pandas one-hot:\n", df_onehot) # 方法二:sklearn OneHotEncoder(可部署到生产流水线) encoder = OneHotEncoder(sparse_output=False, drop='first') encoded = encoder.fit_transform(df[['color']]) print("sklearn one-hot (drop first):\n", encoded) print("类别:", encoder.categories_) # One-Hot缺点:当color有1000个类别时,会增加1000列

3.2 Label编码与Ordinal编码

Label编码为每个类别分配一个整数,如red=0, blue=1, green=2。这种方式简单但给类别引入了不存在的序关系——模型可能认为绿色(2)大于蓝色(1),这在无序类别中会造成误导。Ordinal编码与此类似,但适用于有序类别如学历(小学<中学<大学)。

from sklearn.preprocessing import LabelEncoder, OrdinalEncoder # LabelEncoder——适用于目标变量(y)编码 le = LabelEncoder() y = ['cat', 'dog', 'bird', 'dog'] y_encoded = le.fit_transform(y) print("Label编码:", y_encoded) # [0 1 2 1] print("类别映射:", dict(zip(le.classes_, le.transform(le.classes_)))) # OrdinalEncoder——适用于有序类别特征 X = [['高中'], ['本科'], ['硕士'], ['高中']] oe = OrdinalEncoder(categories=[['高中', '本科', '硕士']]) X_encoded = oe.fit_transform(X) print("Ordinal编码:\n", X_encoded)

3.3 Target编码

Target编码(又称均值编码)用目标变量的平均值来替换类别值。例如某个城市的平均房价作为该城市的编码值。这种编码能有效传达类别与目标之间的关系,但容易导致过拟合,需要配合平滑处理和交叉验证使用。

from category_encoders import TargetEncoder from sklearn.model_selection import train_test_split # 示例数据:不同城市的房价 X = pd.DataFrame({'city': ['北京', '上海', '广州', '北京', '上海', '深圳', '广州', '深圳', '北京', '上海']}) y = [800, 750, 500, 820, 730, 700, 520, 710, 790, 740] # 万元 # 必须用训练集拟合,再转换测试集以防数据泄露 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42 ) encoder = TargetEncoder(cols=['city'], smoothing=10) X_train_enc = encoder.fit_transform(X_train, y_train) X_test_enc = encoder.transform(X_test) print("训练集Target编码:\n", X_train_enc) print("测试集Target编码:\n", X_test_enc) # 北京被编码为接近800的值,广州被编码为接近500的值

重要警告:Target编码极其容易导致目标泄露(target leakage)。务必将编码器只在训练集上fit,然后transform训练集和测试集。绝不能先对整个数据集做Target编码再划分训练测试集。建议配合交叉验证使用,在每一折中独立计算编码值。

3.4 二进制编码与哈希编码

二进制编码先将类别映射为整数,再将该整数转为二进制位串,每位作为一列特征。哈希编码使用哈希函数将类别映射到固定维度的向量空间,适用于基数极高(如用户ID、IP地址)的场景。

from category_encoders import BinaryEncoder, HashingEncoder # 二进制编码 df = pd.DataFrame({'user_id': ['u001', 'u002', 'u003', 'u004', 'u005']}) binary_enc = BinaryEncoder(cols=['user_id']) df_binary = binary_enc.fit_transform(df) print("二进制编码:\n", df_binary) # 输出只有log2(N)列,比One-Hot的N列节省大量空间 # 哈希编码——固定输出维度,适合超高基数特征 hashing_enc = HashingEncoder(cols=['user_id'], n_components=6) df_hash = hashing_enc.fit_transform(df) print("哈希编码(6维):\n", df_hash) # 无论user_id有多少种,始终输出6列,但可能存在哈希冲突
编码方法适用场景输出维度是否保留序关系
One-Hot编码低基数类别(<50种)类别数
Label编码目标变量、树模型1会引入错误序
Ordinal编码有序类别(学历、评级)1是,保留有序
Target编码高基数类别、与目标强相关1依目标均值排序
二进制编码中高基数类别log2(类别数)部分保留
哈希编码超高基数(IP、ID)固定(用户设定)

四、数值特征变换

数值特征变换旨在调整特征的分布和尺度,使模型能更有效地学习。不同模型对特征尺度的敏感度不同——线性模型、SVM、KNN和神经网络对特征尺度非常敏感,而树模型则不受影响。

4.1 标准化(StandardScaler)与归一化(MinMaxScaler)

标准化(Z-score标准化)将特征变换为均值为0、标准差为1的分布,公式为 z = (x - μ) / σ。它不将数据限定在固定区间,适合数据存在异常值但大致服从正态分布的情况。MinMaxScaler将特征缩放到[0,1]区间,公式为 x_scaled = (x - min) / (max - min),对异常值极其敏感。

from sklearn.preprocessing import StandardScaler, MinMaxScaler import numpy as np data = np.array([[1], [2], [3], [4], [100]]) # 含异常值100 # 标准化 scaler_std = StandardScaler() data_std = scaler_std.fit_transform(data) print("StandardScaler:\n", data_std) print("均值:", data_std.mean(), "标准差:", data_std.std()) # MinMax缩放 scaler_mm = MinMaxScaler() data_mm = scaler_mm.fit_transform(data) print("MinMaxScaler:\n", data_mm) # 异常值100使其他数据被压缩到很小区间

4.2 RobustScaler与Normalizer

RobustScaler使用中位数和四分位数范围(IQR)进行缩放,对异常值具有鲁棒性。Normalizer将每个样本(行向量)缩放到单位范数(L1或L2),在文本分类和聚类中特别有用。

from sklearn.preprocessing import RobustScaler, Normalizer # RobustScaler——用中位数和IQR,不受异常值影响 data = np.array([[1], [2], [3], [4], [100]]) scaler_robust = RobustScaler() data_robust = scaler_robust.fit_transform(data) print("RobustScaler:\n", data_robust) # 前四个值不会因为100的存在而被过度压缩 # Normalizer——按行归一化到单位范数 X = np.array([[3.0, 4.0], # L2范数为5 [1.0, 0.0]]) normalizer = Normalizer(norm='l2') X_norm = normalizer.fit_transform(X) print("L2 Normalizer:\n", X_norm) print("每行L2范数:", np.linalg.norm(X_norm, axis=1)) # [1, 1]

4.3 幂变换:Box-Cox与Yeo-Johnson

Box-Cox变换可以将偏态分布映射到接近正态分布,公式为 y = (x^λ - 1) / λ(λ ≠ 0)或 y = log(x)(λ = 0)。但Box-Cox要求输入数据为正数。Yeo-Johnson变换是Box-Cox的扩展,支持负数输入,应用范围更广。两者都通过最大似然估计自动寻找最优λ值。

from sklearn.preprocessing import PowerTransformer import numpy as np # 构造右偏数据(长尾分布) np.random.seed(42) data = np.random.exponential(scale=2, size=(1000, 1)) # Box-Cox变换(数据必须为正) boxcox = PowerTransformer(method='box-cox') data_bc = boxcox.fit_transform(data) print("Box-Cox最优λ:", boxcox.lambdas_) print("变换前偏度:", np.round(pd.Series(data.flatten()).skew(), 3)) print("变换后偏度:", np.round(pd.Series(data_bc.flatten()).skew(), 3)) # Yeo-Johnson变换(支持负数) data_with_neg = np.array([-5, -2, 0, 1, 3, 10]).reshape(-1, 1) yj = PowerTransformer(method='yeo-johnson') data_yj = yj.fit_transform(data_with_neg) print("Yeo-Johnson最优λ:", yj.lambdas_) print("变换结果:\n", np.hstack([data_with_neg, data_yj]))

4.4 对数变换

对数变换(log transformation)是最简单也最常用的非线性变换方法,特别适合处理长尾分布数据(如收入、房价、线上消费金额)。其核心公式为 y = log(x + c),其中常数 c 用于处理零值(通常取1)。

import numpy as np import pandas as pd # 典型场景:收入数据高度右偏 incomes = np.array([3000, 5000, 8000, 15000, 25000, 50000, 100000, 500000, 2000000]) # 对数变换 log_incomes = np.log1p(incomes) # log(1 + x),自动处理0值 print("原始数据偏度:", np.round(pd.Series(incomes).skew(), 3)) print("log变换后偏度:", np.round(pd.Series(log_incomes).skew(), 3)) # 变换后数据分布更均匀,模型更容易学习 print("原始:", incomes) print("log1p:", np.round(log_incomes, 2)) # 预测后使用expm1还原 predictions_log = np.array([8.0, 9.5, 11.0]) predictions_original = np.expm1(predictions_log) print("还原后预测:", predictions_original)

何时使用何种缩放:如果数据大致服从正态分布且无极端异常值,使用StandardScaler;如果数据范围固定且无异常值,使用MinMaxScaler;如果存在异常值,优先使用RobustScaler;如果需要对样本进行单位向量的比较(如余弦相似度),使用Normalizer;如果数据呈偏态分布(长尾),使用对数变换或PowerTransformer。

五、特征分箱

特征分箱(Binning)将连续变量离散化为有限个区间(箱)。分箱可以降低噪声对模型的影响,捕捉非线性关系,并增强模型的解释性。分箱后的特征也可以进一步进行WOE编码用于风控模型。

5.1 等距分箱与等频分箱

等距分箱将值域划分为等宽的区间,等频分箱则每个区间包含相同数量的样本。

import pandas as pd import numpy as np np.random.seed(42) ages = np.random.randint(18, 80, size=20) df = pd.DataFrame({'age': ages}) # 等距分箱:将年龄分为4个等宽区间 df['age_equal_width'] = pd.cut(df['age'], bins=4, labels=['青年', '壮年', '中年', '老年']) # 等频分箱:每个箱有相同数量的样本 df['age_equal_freq'] = pd.qcut(df['age'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4']) print(df.sort_values('age'))

5.2 聚类分箱

聚类分箱使用K-means等聚类算法将数据划分为同类簇,每簇作为一个箱子。这种方法能自动发现数据中的自然分组,但需要事先指定聚类数量,且对异常值敏感。

from sklearn.cluster import KMeans import numpy as np # 用K-means对数值特征做聚类分箱 values = np.array([10, 12, 11, 45, 50, 48, 90, 95, 92]).reshape(-1, 1) kmeans = KMeans(n_clusters=3, random_state=42, n_init='auto') labels = kmeans.fit_predict(values) print("原始值 -> 簇标签") for v, l in zip(values.flatten(), labels): print(f" {v:4d} -> {l}") print("\n簇中心:", kmeans.cluster_centers_.flatten()) # 簇0≈低值区, 簇1≈中值区, 簇2≈高值区

5.3 WOE编码

WOE(Weight of Evidence,证据权重)编码是金融风控领域的标准编码方法,尤其用于逻辑回归模型。它将分箱后的每个区间映射为该区间的好/坏样本比的对数值。WOE值也可以用于计算IV(Information Value,信息量)来评估特征预测力。

import numpy as np import pandas as pd # 模拟数据:年龄、是否违约 data = pd.DataFrame({ 'age_bin': ['20-30', '20-30', '30-40', '30-40', '40-50', '40-50', '50-60', '20-30', '30-40', '50-60'], 'default': [1, 0, 1, 0, 0, 0, 1, 1, 0, 0] }) # 计算每个分箱的WOE值 # WOE = ln(好样本占比 / 坏样本占比) def calc_woe(df, feature, target): total_good = (df[target] == 0).sum() total_bad = (df[target] == 1).sum() grouped = df.groupby(feature)[target].agg(['count', 'sum']) grouped.columns = ['total', 'bad'] grouped['good'] = grouped['total'] - grouped['bad'] grouped['good_pct'] = grouped['good'] / total_good grouped['bad_pct'] = grouped['bad'] / total_bad grouped['woe'] = np.log(grouped['good_pct'] / grouped['bad_pct']) grouped['iv'] = (grouped['good_pct'] - grouped['bad_pct']) * grouped['woe'] return grouped woe_df = calc_woe(data, 'age_bin', 'default') print("WOE编码:\n", woe_df) print("\n总IV值:", woe_df['iv'].sum()) # IV<0.02无预测力, 0.02~0.1弱, 0.1~0.3中等, >0.3强

六、缺失值特征工程

现实数据几乎总是包含缺失值。处理缺失不仅是技术问题,更可能蕴含重要的业务信号——缺失本身可能就携带有意义的信息。例如客户未填写收入字段可能暗示其收入不稳定或不愿披露。

6.1 缺失指示列

缺失指示列(Missing Indicator)是一个二进制列,标记该样本在该特征上是否缺失。这能让模型学习到"缺失"本身可能存在的预测信息。有时缺失比实际值更有信号价值。

import pandas as pd import numpy as np from sklearn.impute import MissingIndicator df = pd.DataFrame({ 'age': [25, np.nan, 35, np.nan, 45], 'income': [50000, 60000, np.nan, np.nan, 80000] }) # 手动添加缺失指示列 df['age_missing'] = df['age'].isna().astype(int) df['income_missing'] = df['income'].isna().astype(int) print(df) # 使用sklearn MissingIndicator indicator = MissingIndicator(features='all') X_missing = indicator.fit_transform(df[['age', 'income']]) print("\n缺失指示矩阵:\n", X_missing)

6.2 填充策略对比

不同的填充策略适用于不同的数据类型和缺失模式。选择合适的方法可以显著提升模型效果。

from sklearn.impute import SimpleImputer, KNNImputer import pandas as pd import numpy as np df = pd.DataFrame({ 'A': [1, 2, np.nan, 4, 5], 'B': [np.nan, 2.5, 3.5, np.nan, 5.5], 'C': ['a', 'b', np.nan, 'a', 'b'] }) # 策略1:均值填充(数值特征) mean_imputer = SimpleImputer(strategy='mean') df[['A', 'B']] = mean_imputer.fit_transform(df[['A', 'B']]) print("均值填充:\n", df) # 策略2:中位数填充(对异常值鲁棒) median_imputer = SimpleImputer(strategy='median') # 策略3:众数填充(类别特征) mode_imputer = SimpleImputer(strategy='most_frequent') # 策略4:KNN填充(利用样本相似性) X_num = pd.DataFrame({ 'A': [1, 2, np.nan, 4, 5], 'B': [1.5, 2.5, 3.5, np.nan, 5.5] }) knn_imputer = KNNImputer(n_neighbors=2) X_knn = knn_imputer.fit_transform(X_num) print("\nKNN填充:\n", X_knn) # 策略5:常数填充(适合缺失有特殊含义的场景) const_imputer = SimpleImputer(strategy='constant', fill_value=-999)
填充策略适用场景优点缺点
均值/中位数填充数值型、随机缺失(MCAR)简单快速降低方差、扭曲分布
众数填充类别型特征保持众数频率可能增加偏态
前向/后向填充时间序列保留时间依赖不适合非时序数据
KNN填充低维数值数据利用相似样本信息计算开销大
模型预测填充缺失值有规律可循精度高有数据泄露风险
缺失指示列 + 任意填充缺失本身有信息保留缺失信号增加特征维度

最佳实践:建议对每个含缺失值的特征同时做两件事——①创建一个缺失指示列;②使用合理的值填充原特征。这样模型可以同时利用填充值和缺失本身的信息。对于树模型,缺失值可以不填充,让模型自身学习最佳分裂路径(如XGBoost/LightGBM原生支持缺失值处理)。

七、特征选择

特征选择是从所有特征中挑选出对模型最有贡献的子集的过程。它能降低过拟合风险、减少训练时间、提高模型泛化能力和可解释性。特征选择方法分为三大类:过滤式、包裹式和嵌入式。

7.1 过滤式(Filter Methods)

过滤式方法独立于任何机器学习模型,基于统计指标对特征进行排序和筛选。速度快、可扩展性强,适合高维数据。常用指标包括:方差阈值、皮尔逊相关系数、互信息、卡方检验、方差分析(ANOVA)等。

from sklearn.feature_selection import ( VarianceThreshold, SelectKBest, f_classif, mutual_info_classif, chi2 ) import numpy as np X = np.random.randn(100, 10) y = (X[:, 0] + 0.5 * X[:, 1] + np.random.randn(100) * 0.3 > 0).astype(int) # 方法1:方差阈值——移除方差低于阈值的特征 selector_var = VarianceThreshold(threshold=0.1) X_var = selector_var.fit_transform(X) # 方法2:F检验(ANOVA)——选择与目标最相关的K个特征 selector_f = SelectKBest(score_func=f_classif, k=5) X_f = selector_f.fit_transform(X, y) print("F检验得分:", selector_f.scores_) # 方法3:互信息——捕捉非线性关系 selector_mi = SelectKBest(score_func=mutual_info_classif, k=5) X_mi = selector_mi.fit_transform(X, y) print("互信息得分:", selector_mi.scores_) # 方法4:相关系数矩阵——识别高相关冗余特征 corr_matrix = np.corrcoef(X.T) # 找到相关系数 > 0.8 的特征对,剔除其中一个

7.2 包裹式(Wrapper Methods)

包裹式方法将模型性能作为特征子集的评价标准,通过搜索策略(前向选择、后向消除、递归特征消除)寻找最优子集。计算开销大但效果通常更好。

from sklearn.feature_selection import RFE, RFECV from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold import numpy as np X = np.random.randn(200, 20) y = (X[:, 0] + X[:, 2] > 0).astype(int) # 递归特征消除(RFE)——用模型权重逐步剔除最不重要的特征 estimator = RandomForestClassifier(n_estimators=50, random_state=42) rfe = RFE(estimator=estimator, n_features_to_select=5) rfe.fit(X, y) print("RFE选中特征:", np.where(rfe.support_)[0]) print("特征排名:", rfe.ranking_) # RFECV——用交叉验证自动确定最优特征数量 rfecv = RFECV( estimator=estimator, step=1, cv=StratifiedKFold(5), scoring='accuracy' ) rfecv.fit(X, y) print("最优特征数:", rfecv.n_features_) print("交叉验证得分:\n", rfecv.cv_results_['mean_test_score'])

7.3 嵌入式(Embedded Methods)

嵌入式方法在模型训练过程中自动完成特征选择,兼具过滤式的效率和包裹式的准确性。最突出的两种方式是L1正则化(Lasso)和树模型特征重要性。

7.4 L1正则化

L1正则化(Lasso)通过在损失函数中加入权重绝对值和(L1范数)作为惩罚项,使得部分特征的系数被压缩为零,从而实现自动特征选择。正则化强度由参数C(或alpha)控制,C越小被剔除的特征越多。

from sklearn.linear_model import LogisticRegression import numpy as np # 生成数据:20个特征,只有前5个有效 np.random.seed(42) n_samples, n_features = 200, 20 X = np.random.randn(n_samples, n_features) y = (X[:, 0] + 2 * X[:, 1] - 1.5 * X[:, 2] + np.random.randn(n_samples) * 0.1 > 0).astype(int) # L1正则化逻辑回归——自动特征选择 model = LogisticRegression(penalty='l1', solver='saga', C=0.1, random_state=42) model.fit(X, y) print("L1正则化系数:\n", model.coef_) print("非零系数数量:", np.sum(model.coef_ != 0)) print("选中特征索引:", np.where(model.coef_.flatten() != 0)[0]) # 不同的C值影响特征选择强度 for C_val in [0.01, 0.1, 1.0, 10.0]: m = LogisticRegression(penalty='l1', solver='saga', C=C_val, random_state=42) m.fit(X, y) n_selected = np.sum(m.coef_ != 0) print(f"C={C_val:5.2f} -> 选中 {n_selected} 个特征")

L1 vs L2:L1正则化产生稀疏解(特征系数为零),适用于特征选择;L2正则化(Ridge)将所有系数向零收缩但不等于零,更适合处理多重共线性。ElasticNet结合了L1和L2,在特征选择和稳定性之间取得平衡。

7.5 树模型特征重要性

树模型(随机森林、XGBoost、LightGBM等)可以输出特征重要性得分,衡量每个特征在决策树分裂中的贡献。常用指标包括基于基尼不纯度(Gini importance)和基于排列(permutation importance)的重要性。

from sklearn.ensemble import RandomForestClassifier from sklearn.inspection import permutation_importance import numpy as np import pandas as pd X = np.random.randn(200, 10) y = (X[:, 0] + X[:, 3] - 0.5 * X[:, 7] + np.random.randn(200) * 0.2 > 0).astype(int) rf = RandomForestClassifier(n_estimators=100, random_state=42) rf.fit(X, y) # 基于基尼系数的特征重要性 importance_gini = pd.DataFrame({ 'feature': [f'feat_{i}' for i in range(10)], 'importance': rf.feature_importances_ }).sort_values('importance', ascending=False) print("基尼重要性:\n", importance_gini) # 排列重要性——打乱每列观测模型性能下降程度(更可靠) perm_importance = permutation_importance( rf, X, y, n_repeats=10, random_state=42 ) importance_perm = pd.DataFrame({ 'feature': [f'feat_{i}' for i in range(10)], 'importance_mean': perm_importance.importances_mean, 'importance_std': perm_importance.importances_std }).sort_values('importance_mean', ascending=False) print("\n排列重要性:\n", importance_perm) # 选择重要性排名前K的特征 K = 4 selected_features = importance_gini.head(K)['feature'].values print(f"\n选择 Top-{K} 特征:", selected_features)

实践建议:在实际项目中推荐采用"多层筛选"策略——先用过滤式方法快速剔除无效特征(如方差为零或极低的特征),然后用嵌入式方法(L1正则化或树模型重要性)选出候选特征,最后用包裹式方法(RFECV)精确确定最优子集。这种方法兼顾了效率和效果。

八、核心要点总结

1. 特征工程是机器学习流程中最关键的环节——数据质量比模型选择更重要,特征决定了模型的理论上限。好的特征应具备相关性、区分度、鲁棒性和可获取性。

2. 特征创建需要业务理解与创造力——多项式特征捕捉非线性关系,交互特征发现协同效应,日期分解释放时间信息,TF-IDF提升文本区分度。没有万能方法,需要根据业务场景灵活组合。

3. 编码方式的选择取决于特征类型和基数——低基数无序类别用One-Hot,高基数类别用Target编码或二进制编码,有序类别用Ordinal编码,超高基数用哈希编码。树模型可直接使用Label编码,线性模型更适合One-Hot。

4. 数值变换要匹配数据分布和模型需求——StandardScaler用于正态分布数据,RobustScaler处理含异常值数据,Box-Cox/Yeo-Johnson矫正偏态分布,对数变换适合长尾数据。线性模型和神经网络必须做缩放,树模型不需要。

5. 分箱可增强模型鲁棒性和解释性——等距箱简单直观,等频箱保证样本均匀,聚类分箱发现自然分组,WOE编码是风控领域标配。分箱后结合WOE/IV评估特征预测力。

6. 缺失值处理要兼顾填充和信息保留——缺失指示列+合理填充的组合策略是最稳健的做法。不同填充策略影响模型效果,KNN填充和模型预测填充精度更高但计算成本也更高。

7. 特征选择采用"先宽后窄"的多层策略——先用过滤式快速筛选,再用嵌入式(L1正则化、树模型重要性)缩小范围,最后用包裹式(RFECV)精调。避免过度特征选择导致信息丢失。

8. 特征工程是一个迭代优化过程——没有一蹴而就的最佳特征集。需要结合模型性能反馈反复实验:构建特征→训练模型→评估效果→调整特征→再训练。推荐使用Pipeline管理特征工程流程,确保可复现和可部署。

下一步学习方向:在掌握特征工程基础后,可以进一步学习自动特征工程(Featuretools、AutoFE)、深度学习特征提取(AutoEncoder、Embedding Layer)、以及大规模特征存储和服务(Feature Store架构如Feast、Tecton)等进阶主题。