特征工程与特征选择

机器学习专题 · 掌握数据特征处理的核心技术

专题:Python机器学习系统学习

关键词:Python, 机器学习, 特征工程, 特征选择, 标准化, 独热编码, TF-IDF, RFE, scikit-learn

一、特征工程概述

1.1 特征工程的定义与重要性

特征工程(Feature Engineering)是机器学习流程中最关键也最耗时的环节之一。它指的是将原始数据转换为能更好表达问题本质的特征的过程,目的是提升模型的性能和准确度。业界有句广为流传的名言:"Garbage in, garbage out"——如果输入的特征质量低下,无论模型多么先进,最终结果都不会理想。Andrew Ng 曾指出:"Coming up with features is difficult, time-consuming, requires expert knowledge. 'Applied machine learning' is basically feature engineering." 在实际工业项目中,数据科学家和机器学习工程师通常会将 60% 到 80% 的时间花在特征工程上。

特征工程之所以如此重要,是因为原始数据通常存在以下几个问题:一是数据格式不统一,比如数值型特征的范围差异巨大;二是包含大量噪声和缺失值;三是原始数据的表达能力有限,难以直接揭示隐藏在数据中的复杂模式;四是高维稀疏数据会导致模型过拟合或计算开销过大。通过有效的特征工程,我们可以解决这些问题,让模型更好地理解数据。

核心原则:特征工程的目标不是简单地"堆砌"特征,而是从数据中提取出对预测任务最有信息量的信号,同时去除噪声和冗余信息。一个好的特征应该具有区分性、可靠性、独立性和可解释性。

1.2 特征工程的主要步骤

特征工程通常包含以下几个核心步骤:特征构建(从原始数据中创建新的特征)、特征提取(通过变换从原始数据中提取更有表达力的特征)、特征处理(处理缺失值、异常值、标准化尺度等)、特征编码(将非数值型数据转换为数值型)、特征选择(从众多特征中挑选出最有用的子集)。

这些步骤在实际项目中通常是迭代进行的:先构建一批基础特征,训练初步模型评估效果,然后根据模型反馈进行特征优化和选择,再重复这个循环直到达到满意的性能。

1.3 特征工程 vs 特征选择

特征工程和特征选择是相辅相成的两个概念,但侧重点不同。特征工程侧重于"创造"和"变换"——如何从原始数据中衍生出更多有用的特征,如何让特征更好地表达数据的内在规律。特征选择则侧重于"筛选"——在已有的特征集合中,挑出最有预测能力、最不冗余的特征子集,以达到降低维度、减少过拟合、提升模型训练速度和可解释性的目的。

打个比方:特征工程就像是在准备食材——洗菜、切菜、腌制、调味,让食材更适合烹饪;特征选择则像是在挑选食材——从冰箱里选出最新鲜、最合适的几样来制作今天的菜肴。二者缺一不可,共同决定了最终"菜品"的质量。

二、数值特征处理

数值特征是机器学习中最常见的数据类型。不同数值特征往往具有不同的量纲和取值范围,如果直接送入模型,量级大的特征会主导模型的学习过程,导致模型性能下降。因此,数值特征的预处理是特征工程的基础环节。

2.1 标准化(StandardScaler)

标准化(Z-score 标准化)是最常用的数值特征处理方法之一。它假设特征服从正态分布,将原始数据转换为均值为 0、标准差为 1 的标准正态分布。其计算公式为:z = (x - μ) / σ,其中 μ 是特征的均值,σ 是特征的标准差。

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 转换后:mean ≈ 0, std ≈ 1

标准化的优势在于对异常值的敏感度相对较低(相比归一化),且保留了原始数据的分布形态。它适用于大多数线性模型、SVM、神经网络等对特征尺度敏感的算法。当特征本身服从或近似正态分布时,标准化效果最佳。

2.2 归一化

归一化是将特征缩放到一个特定的范围(通常是 [0, 1] 或 [-1, 1])的处理方法。scikit-learn 提供了多种归一化方法:

MinMaxScaler: 最常用的归一化方法,将特征缩放到 [0, 1] 区间。公式为 x' = (x - min) / (max - min)。缺点是对异常值非常敏感——如果数据中存在极端值,大部分数据会被压缩到很小的区间内。

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) X_scaled = scaler.fit_transform(X)

RobustScaler: 使用中位数和四分位距(IQR)进行缩放,对异常值具有较好的鲁棒性。公式为 x' = (x - median) / IQR。当数据包含较多异常值时,RobustScaler 是比 MinMaxScaler 更好的选择。

MaxAbsScaler: 将每个特征除以该特征的最大绝对值,将特征缩放到 [-1, 1] 区间。适用于稀疏数据,因为它不会改变数据的稀疏性(不会将零值变为非零)。

2.3 离散化

离散化(分箱)是将连续数值特征转换为离散类别特征的过程。这在某些场景下非常有用,比如特征与目标之间呈现非线性关系时,离散化可以帮助线性模型捕捉这种非线性。

等宽分箱(KBinsDiscretizer + uniform): 将特征的值域划分为 K 个等宽的区间。方法简单直观,但容易受异常值影响,且数据分布不均匀时会导致某些箱中样本极少。

等频分箱(KBinsDiscretizer + quantile): 基于分位数进行划分,使得每个箱中包含大致相同数量的样本。这种方法对异常值不敏感,且能更好地适应数据分布。但边界点的确定受数据分布影响较大。

KMeans 分箱(KBinsDiscretizer + kmeans): 使用一维 KMeans 聚类算法,将聚类中心作为分箱的断点。这种方法能自适应地找到最优的划分位置,但计算开销较大。

from sklearn.preprocessing import KBinsDiscretizer # 等宽分箱 est = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='uniform') X_binned = est.fit_transform(X)

2.4 二值化

二值化(Binarizer)是将数值特征转换为二进制值(0 或 1)的方法,通常基于一个阈值进行划分。大于阈值的设为 1,小于等于阈值的设为 0。适用于特征取值是否超过某个阈值具有重要意义的场景(如年龄是否超过 18 岁、信用评分是否超过某个分数线等)。

from sklearn.preprocessing import Binarizer binarizer = Binarizer(threshold=0.5) X_binary = binarizer.transform(X)

2.5 幂变换

幂变换的目的是使数据更接近正态分布,从而提高模型的性能。常用的幂变换方法包括:

Box-Cox 变换: 适用于正数数据(x > 0),通过参数 λ 控制变换强度。当 λ = 0 时等价于对数变换,λ = 1 时等价于线性变换。Box-Cox 变换能有效处理右偏分布,使其更接近正态分布。

Yeo-Johnson 变换: Box-Cox 变换的扩展,可处理包含零和负值的数据。它在保持 Box-Cox 变换优点的同时,扩展了适用范围,因此在实际应用中更加灵活。

from sklearn.preprocessing import PowerTransformer # Box-Cox 变换(要求数据为正) pt_boxcox = PowerTransformer(method='box-cox') X_boxcox = pt_boxcox.fit_transform(X_positive) # Yeo-Johnson 变换(支持负值和零) pt_yj = PowerTransformer(method='yeo-johnson') X_yj = pt_yj.fit_transform(X)

2.6 对数/指数变换

对数变换是处理长尾分布(如收入、点击量、房价等数据)最常用的方法之一。通过对数变换,可以将呈指数增长的数据压缩到线性尺度上,使模型更容易捕捉模式。常用的变换包括 log(x+1)(处理零值情况)、sqrt 变换和 reciprocal(倒数)变换。这些变换在 NLP 领域的 TF-IDF 计算、推荐系统中的用户行为频次处理等场景中有着广泛的应用。

import numpy as np # 对数变换(处理长尾分布) X_log = np.log1p(X) # log(1 + x),自动处理零值 # 平方根变换(处理计数数据) X_sqrt = np.sqrt(X) # Box-Cox 的自动最优变换 from scipy.stats import boxcox X_optimal, lambda_found = boxcox(X_positive + 1)

三、类别特征处理

类别特征是机器学习中另一类常见的特征类型,如性别、城市、职业等。大多数机器学习算法只能处理数值型数据,因此需要将类别特征转换为数值形式。不同的编码方式适用于不同的场景和模型。

3.1 标签编码(LabelEncoder)

标签编码将每个类别映射为一个整数(如 0, 1, 2, ...)。这种方法简单直接,但缺点是引入了类别之间的大小关系(ordinal relationship),可能会误导模型认为编码值之间存在顺序和距离意义。因此,标签编码通常只适用于目标变量(y)的编码,或者用于本身就是有序的类别特征(如"小/中/大")。

from sklearn.preprocessing import LabelEncoder le = LabelEncoder() y_encoded = le.fit_transform(y)

3.2 独热编码(OneHotEncoder)

独热编码是最常用的类别特征编码方式之一。它为每个类别创建一个二进制虚拟变量(dummy variable),每个样本在其所属类别对应的维度上取值为 1,其余维度为 0。例如,颜色特征有"红、绿、蓝"三个类别,独热编码后会得到三个二进制特征:is_red、is_green、is_blue。

独热编码的优点是简单、无偏、不引入顺序假设,适用于绝大多数线性模型和树模型。缺点是当类别数量很多时,会导致特征维度急剧膨胀(即"维度灾难"),增加模型的计算开销和过拟合风险。在类别数量超过几十个时,通常需要考虑其他编码方式。

from sklearn.preprocessing import OneHotEncoder enc = OneHotEncoder(sparse_output=True, handle_unknown='ignore') X_encoded = enc.fit_transform(X_categorical) # 处理大规模类别时,可以结合 PCA 或截断 SVD 降维

3.3 序数编码(OrdinalEncoder)

序数编码与标签编码类似,但专门用于特征(而非目标变量)。它将每个类别映射为整数,适用于类别之间具有显式顺序关系的特征。例如,学历等级"小学 < 初中 < 高中 < 本科 < 研究生"就适合使用序数编码。需要注意的是,当类别之间不存在自然顺序时使用序数编码,会引入虚假的排序关系。

from sklearn.preprocessing import OrdinalEncoder enc = OrdinalEncoder(categories=[['小学', '初中', '高中', '本科', '研究生']]) X_encoded = enc.fit_transform(X_education)

3.4 目标编码(Target Encoding)

目标编码(也称为均值编码)是用目标变量的统计量(如均值、中位数)来替换类别特征的值。对于回归问题,使用目标变量的均值;对于分类问题,使用正类的概率。目标编码能够很好地处理高基数类别特征(如用户ID、邮政编码等),因为它不会像独热编码那样导致维度爆炸。

目标编码的主要风险是容易导致过拟合——模型可能会"记住"训练数据中每个类别对应的目标值。为了解决这个问题,通常需要添加平滑项(smoothing)和交叉验证策略。常用的实现包括 category_encoders 库中的 TargetEncoder。

# 使用 category_encoders 库(需单独安装) import category_encoders as ce encoder = ce.TargetEncoder(cols=['category_feature'], smoothing=10) X_encoded = encoder.fit_transform(X, y)

3.5 频数编码(Count Encoding)

频数编码是用每个类别在训练集中出现的频次(或频率)来替换原始的类别值。其基本假设是:出现频次较高的类别可能包含更多的"共性"信息,而出现频次较低的类别可能代表"特殊"情况。频数编码的优点是简单高效,且不会增加特征维度,也不会像目标编码那样依赖目标变量。但它丢失了类别本身的语义信息,且不同类别可能有相同的频次(导致特征值冲突)。

3.6 特征哈希(FeatureHasher)

特征哈希(也称为哈希技巧)通过哈希函数将高维的类别特征映射到固定长度的低维向量空间中。它特别适用于处理类别数量极大(数万甚至数百万)的场景,如文本分类中的词条特征、推荐系统中的用户/商品 ID 等。

特征哈希的优点是内存占用固定、支持在线学习。缺点是哈希碰撞会导致信息丢失,且哈希后的特征不具有可解释性。

from sklearn.feature_extraction import FeatureHasher hasher = FeatureHasher(n_features=128, input_type='string') X_hashed = hasher.transform(X_categorical_strings)

3.7 各类编码的适用场景对比

编码方法适用场景优点缺点
标签编码有序类别、目标变量编码简单、不增加维度引入虚假顺序
独热编码低基数无序类别(< 20 类)无偏、可解释维度膨胀
序数编码有明确顺序的类别保留顺序信息仅适用于有序类别
目标编码高基数类别利用目标信息、不增维易过拟合、需交叉验证
频数编码类别频次有意义的场景简单、不易过拟合可能丢失类别语义
特征哈希超高基数类别、在线学习固定维度、内存友好哈希碰撞、不可解释

四、时间特征处理

时间数据在金融、电商、物联网等领域极为常见。时间特征的处理往往需要考虑时间的内在结构(周期性、趋势性、季节性)以及多个时间点之间的关系。

4.1 提取时间分量

将时间戳拆解为更细粒度的分量是时间特征处理的第一步。常用的时间分量包括:年份、月份、日期、星期几(0-6)、小时、分钟、秒、一年中的第几天、第几周、是否周末、是否节假日等。这些基础分量既能作为数值特征直接使用,也能作为后续构造更复杂时间特征的基础。

import pandas as pd df['timestamp'] = pd.to_datetime(df['timestamp']) df['year'] = df['timestamp'].dt.year df['month'] = df['timestamp'].dt.month df['day'] = df['timestamp'].dt.day df['hour'] = df['timestamp'].dt.hour df['weekday'] = df['timestamp'].dt.weekday df['is_weekend'] = df['weekday'].isin([5, 6]).astype(int) df['day_of_year'] = df['timestamp'].dt.dayofyear

4.2 周期特征编码(sin/cos 变换)

时间特征(如小时、月份、星期几)具有天然的周期性。例如,23 点和 0 点在数值上相差很大,但在时间意义上非常接近。如果直接将"小时"作为数值特征输入模型,模型会认为 23 和 0 之间的距离是 23,而实际上它们的距离只有 1 小时。使用 sin/cos 变换可以将周期性时间特征编码为圆形坐标,很好地解决这个问题。

# 将小时编码为圆形特征 df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) # 将月份编码为圆形特征 df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12) df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)

经过 sin/cos 变换后,每个时间点由两个坐标(sin, cos)表示,欧几里得距离能够准确反映时间点之间的实际"距离"。需要注意的是,sin 和 cos 必须成对使用才能完整表达周期性信息。

4.3 时间差特征

在很多实际问题中,时间点之间的"间隔"比时间点的"绝对值"更具预测意义。常见的时间差特征包括:距离上一次事件的时间、距离下一次事件的时间、距离某个固定参考时间(如注册日、购买日)的时间、距离最近节假日的时间等。这些特征能很好地捕捉用户行为模式中的时效性信息。

# 计算时间差 df['days_since_last_purchase'] = ( df.groupby('user_id')['purchase_date'] .diff().dt.days ) # 距离参考时间的天数 df['days_since_registration'] = ( df['event_date'] - df['registration_date'] ).dt.days

4.4 滑动窗口统计特征

滑动窗口特征通过计算过去一段时间窗口内的统计量来捕捉时序数据的动态变化趋势。常用的滑动窗口统计量包括:窗口内的均值、标准差、最大值、最小值、中位数、偏度、峰度、变化率等。窗口大小可以是固定长度(如过去 7 天),也可以是自适应长度(如过去 N 次事件)。

滑动窗口特征能有效捕捉时序数据的短期趋势和波动,在股票预测、销量预测、异常检测等场景中表现优异。但需要注意窗口大小的选择——窗口太小可能包含过多噪声,窗口太大可能忽略近期的变化。

# 7 天滑动窗口均值 df['sales_ma_7'] = ( df.groupby('product_id')['sales'] .transform(lambda x: x.rolling(window=7, min_periods=1).mean()) ) # 14 天滑动窗口标准差 df['sales_std_14'] = ( df.groupby('product_id')['sales'] .transform(lambda x: x.rolling(window=14, min_periods=1).std()) )

五、文本特征处理

文本数据是非结构化数据中最常见的形式之一。将文本转换为机器学习模型可以处理的数值向量是整个 NLP 领域的基础。从传统的统计方法到现代的深度学习词嵌入,文本特征处理经历了长足的发展。

5.1 词袋模型(CountVectorizer)

词袋模型(Bag of Words, BoW)是最经典的文本向量化方法。它忽略文本的语法和词序,只考虑每个词在文档中出现的频次。具体来说,构建一个包含所有文档中所有不重复词的词汇表,然后每篇文档被表示为一个维度等于词汇表大小的向量,每个维度的值为对应词在该文档中出现的次数。

词袋模型的优点在于简单、高效、可解释。缺点是:(1) 忽略词序和上下文信息;(2) 词汇量很大时会产生高维稀疏向量;(3) 常用词(如"的"、"是")可能会主导表示,而真正有意义的词被淹没。

from sklearn.feature_extraction.text import CountVectorizer vectorizer = CountVectorizer(max_features=5000, stop_words='english') X_bow = vectorizer.fit_transform(documents) # 查看词汇表 print(vectorizer.get_feature_names_out()[:10])

5.2 TF-IDF(TfidfVectorizer)

TF-IDF(Term Frequency - Inverse Document Frequency)是对词袋模型的重要改进。它由两部分组成:词频(TF)表示词在文档中出现的频率,逆文档频率(IDF)表示词在整个语料库中的稀有程度。最终权重 = TF * IDF。这样的好处是:一个词如果在某篇文档中出现频繁(TF 高),但在整个语料库中很少出现(IDF 高),那么它就对这篇文档具有很强的区分能力。

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=5000, stop_words='english', ngram_range=(1, 2), # 使用单字词和双字词 max_df=0.8, # 过滤掉在超过 80% 文档中出现的词 min_df=2 # 过滤掉在少于 2 篇文档中出现的词 ) X_tfidf = vectorizer.fit_transform(documents)

TF-IDF 在实践中通常比纯词袋模型效果更好,因为它能有效降低常用词的权重、提升稀有但重要词的权重。在文本分类、信息检索、文档相似度计算等任务中,TF-IDF 一直是非常可靠的基线方法。

5.3 N-gram 特征

词袋模型和 TF-IDF 都基于单个词(unigram),忽略了词与词之间的顺序关系。N-gram 通过将连续的 N 个词作为一个整体来提取特征,能在一定程度上捕捉文本的局部上下文信息。常见的 N-gram 包括 bigram (N=2) 和 trigram (N=3)。

使用 N-gram 时,特征的维度会急剧增加(因为词汇组合的数量远大于单个词的数量),因此通常需要配合特征选择(如卡方检验、互信息法)来筛选最有价值的 N-gram 特征。在实际应用中,通常同时使用 unigram 和 bigram 会取得不错的平衡。

# 在 CountVectorizer 中启用 ngram vectorizer = CountVectorizer(ngram_range=(1, 3), max_features=10000) X_ngram = vectorizer.fit_transform(documents)

5.4 词嵌入特征

词嵌入(Word Embedding)是深度学习时代的文本特征表示方法。与传统的稀疏向量表示不同,词嵌入将每个词映射到一个稠密的低维实数向量(通常为 50-300 维),并且语义相近的词在向量空间中距离更近。经典的词嵌入方法包括 Word2Vec(CBOW 和 Skip-gram)、GloVe 和 FastText。Transformer 时代则涌现了 BERT、RoBERTa 等预训练语言模型,能够生成上下文相关的词向量。

在实际应用中,获取文档级别的嵌入向量通常有两种策略:一是对文档中所有词的词向量取均值(简单平均或加权平均);二是使用预训练模型直接生成文档嵌入(如 Sentence-BERT)。词嵌入特征的维度通常远低于词袋模型,且能捕捉更多语义信息,在大多数 NLP 任务中表现优于传统方法。

# 使用预训练的 Word2Vec 生成文档嵌入 from gensim.models import Word2Vec import numpy as np # 将文档分词后训练 Word2Vec 模型 sentences = [doc.split() for doc in documents] w2v_model = Word2Vec(sentences, vector_size=100, window=5, min_count=2) # 文档嵌入 = 所有词向量的平均值 def document_embedding(doc): words = doc.split() word_vectors = [w2v_model.wv[w] for w in words if w in w2v_model.wv] if not word_vectors: return np.zeros(100) return np.mean(word_vectors, axis=0) X_embed = np.array([document_embedding(doc) for doc in documents])

六、特征选择方法

特征选择的目标是从原始特征集合中挑选出最优的特征子集,以达到降维、减少过拟合、提高模型性能和可解释性的目的。特征选择方法通常分为三大类:过滤法(Filter)、包裹法(Wrapper)和嵌入法(Embedded)。

6.1 过滤法(Filter)

过滤法在模型训练之前独立地对特征进行评估,不依赖于任何具体的机器学习算法。它通过统计指标来度量每个特征与目标变量之间的相关性或区分能力,然后设定阈值进行筛选。

方差阈值(VarianceThreshold): 最简单的过滤方法之一,它移除所有方差低于某个阈值的特征。方差过小的特征(如所有样本取值几乎相同)对区分样本几乎没有贡献。这种方法仅基于特征自身的方差,不依赖目标变量,因此计算极为高效,适合作为预处理的第一步。

from sklearn.feature_selection import VarianceThreshold selector = VarianceThreshold(threshold=0.1) # 移除方差小于 0.1 的特征 X_selected = selector.fit_transform(X)

卡方检验(SelectKBest + chi2): 卡方检验衡量每个特征与分类目标之间的独立性。卡方统计量越大,说明特征与目标的相关性越强。需要注意的是,卡方检验要求特征值非负,且适用于分类问题。它对于文本分类中的特征选择尤其常用。

from sklearn.feature_selection import SelectKBest, chi2 selector = SelectKBest(chi2, k=20) # 选择卡方统计量最高的 20 个特征 X_selected = selector.fit_transform(X, y)

互信息法(mutual_info_classif / mutual_info_regression): 互信息衡量两个变量之间的依赖程度,可以捕捉特征与目标之间的非线性关系(而不仅仅是线性相关)。互信息越大,特征对目标的预测能力越强。互信息法适用于分类和回归问题,且对特征的正负值没有限制,是比卡方检验更通用的方法。

from sklearn.feature_selection import SelectKBest, mutual_info_classif selector = SelectKBest(mutual_info_classif, k=20) X_selected = selector.fit_transform(X, y)

相关系数法: 计算每个数值特征与目标变量之间的皮尔逊相关系数,选择相关系数绝对值较大的特征。相关系数法简单直观、计算快速,但只能捕捉线性关系,无法检测非线性相关性。对于回归问题,相关系数法是一个非常有效的基线选择方法。

6.2 包裹法(Wrapper)

包裹法将特征选择过程"包裹"在特定的机器学习模型中,通过模型的性能表现来评估特征子集的质量。包裹法通常比过滤法更精确(因为考虑了特征之间的交互作用),但计算开销也更大。

递归特征消除(RFE): RFE 的核心思想是:反复训练模型,每次训练后移除重要性最低的特征,直到达到指定的特征数量。RFE 需要配合一个能够输出特征重要性(如线性模型的系数、树模型的特征重要性)的基模型使用。RFECV 是 RFE 的交叉验证版本,可以自动确定最优的特征数量。

from sklearn.feature_selection import RFE from sklearn.svm import SVC estimator = SVC(kernel='linear') selector = RFE(estimator, n_features_to_select=10, step=1) selector.fit(X, y) # 查看被选中的特征 selected_features = selector.support_ feature_ranking = selector.ranking_

前向/后向搜索: 前向搜索从一个空集开始,每次添加一个使模型性能提升最多的特征,直到达到停止条件。后向搜索则从全量特征开始,每次移除一个对模型性能影响最小的特征。这两种搜索策略的优点是考虑到了特征之间的交互作用,但缺点是计算开销随特征数量呈指数增长,因此通常只适用于特征数量较少(几百以内)的场景。

6.3 嵌入法(Embedded)

嵌入法将特征选择过程嵌入到模型训练过程中,在模型拟合的同时完成特征重要性评估和筛选。它结合了过滤法的效率和包裹法的准确性。

L1 正则化(Lasso): L1 正则化(套索回归)通过在损失函数中添加 L1 惩罚项,使得不重要特征的系数被压缩为零,从而实现自动特征选择。L1 正则化在特征数量远大于样本数量的高维场景中特别有效。通过调节正则化强度参数 C(或 alpha),可以控制被选中的特征数量。

from sklearn.linear_model import LogisticRegression # L1 正则化的逻辑回归(自动特征选择) model = LogisticRegression(penalty='l1', C=0.1, solver='liblinear') model.fit(X, y) # 系数为零的特征被自动排除 selected_features = model.coef_[0] != 0

树模型特征重要性: 随机森林、梯度提升树(如 XGBoost、LightGBM)等树集成模型能够输出每个特征的重要性得分。特征重要性通常基于该特征在所有树中被选为分裂点的次数(加权或不加权)。树模型能自然地处理特征之间的交互作用和非线性关系,因此其特征重要性评估在实践中非常可靠。

from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier(n_estimators=100, random_state=42) model.fit(X, y) # 获取特征重要性 importances = model.feature_importances_ # 选择重要性大于阈值的特征 selected_features = X[:, importances > np.median(importances)]

排列重要性(Permutation Importance): 排列重要性是一种模型无关的特征评估方法。它的核心思路是:打乱某个特征的取值顺序,观察模型性能的下降程度。下降越多,说明该特征越重要。排列重要性的优势在于它可以适用于任何模型,且能反映特征在模型中的实际作用。但它需要重新对每个特征进行多次评估,计算开销较大。

from sklearn.inspection import permutation_importance model = RandomForestClassifier(n_estimators=100) model.fit(X, y) result = permutation_importance(model, X_test, y_test, n_repeats=10) importances = result.importances_mean

七、特征构造

特征构造(Feature Construction)是从现有特征中创造新特征的过程,目的是揭示数据中更深层的模式和关系。如果说特征处理是"清洗"和"变换",特征选择是"筛选",那么特征构造就是"创造"——它是特征工程中最需要领域知识和创造力的环节。

7.1 多项式特征(PolynomialFeatures)

多项式特征通过将现有特征进行乘积和幂次组合来生成新特征。例如,对于特征 a 和 b,二阶多项式特征会生成 a^2、b^2、a*b(特征交叉项)。多项式特征能够帮助线性模型捕捉特征之间的非线性关系。但需要注意,多项式特征的维度会随原始特征数量和阶数呈指数增长,因此需要谨慎使用,并配合正则化或特征选择。

from sklearn.preprocessing import PolynomialFeatures poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=False) X_poly = poly.fit_transform(X) # 输出包含所有原始特征、平方项和两两交叉项

7.2 特征交叉(列组合)

特征交叉是创建两个或多个原始特征的笛卡尔积组合。在分类特征中,特征交叉特别有用——例如,将"性别"和"年龄段"两个特征交叉,可以得到"年轻女性"、"中年男性"等新的组合特征。这些组合特征往往能捕捉到单个特征无法表达的信息。在推荐系统中,特征交叉(如 FM、FFM 模型)是提升效果的核心手段之一。

# 使用 DataFrame 进行特征交叉 df['gender_age_cross'] = df['gender'].astype(str) + '_' + df['age_group'].astype(str) # 然后对交叉特征进行独热编码或目标编码

7.3 聚合特征(GroupBy 统计)

聚合特征通过在某个分组键上进行统计计算来生成新的特征。例如,计算每个用户的平均消费金额、每个商品的历史销量中位数、每个类别的商品数量等。聚合特征是表格数据中最有效的特征构造方法之一,它能够将"群体信息"注入到每个样本中,帮助模型理解样本在群体中的相对位置。

常用的聚合统计量包括:count、mean、std(标准差)、max、min、median、nunique(不同值的数量)、sum、skew(偏度)以及自定义的聚合函数。

# 用户维度的聚合特征 user_features = df.groupby('user_id').agg({ 'amount': ['mean', 'std', 'max', 'min', 'sum', 'count'], 'category': 'nunique', 'timestamp': ['max', 'min'] }) user_features.columns = ['_'.join(col) for col in user_features.columns] # 将聚合特征合并回原始 DataFrame df = df.merge(user_features, on='user_id', how='left')

7.4 领域特定特征

领域特定特征是最能体现特征工程"工程"属性的环节。不同领域的数据有着截然不同的特征构造方式。在金融风控中,可能需要构造收入负债比、信用利用率、近三个月查询次数等特征;在电商推荐中,可能需要构造用户-商品的交互频次、最近一次购买距今的天数、商品的类目偏好度等;在医疗健康中,可能需要构造体重指数(BMI)、心率变异性的统计特征、用药依从性等。

构造领域特定特征的核心方法论包括:一是深入理解业务逻辑和数据生成过程,找到"业务指标"和"模型特征"之间的映射关系;二是与领域专家充分沟通,获取他们的经验知识;三是结合探索性数据分析(EDA)的结果,发现数据中的潜在模式;四是通过 A/B 测试或特征重要性验证来评估新特征的实际效果。

最佳实践总结:

1. 始终从简单特征开始,逐步迭代增加复杂度,避免一开始就陷入特征工程的"过度优化"陷阱。

2. 对每个新构造的特征进行验证——计算其与目标变量的相关性、在模型中的重要性、对业务指标的影响。

3. 建立特征存储和版本管理机制,记录每个特征的来源、构造逻辑、生效时间等信息,便于复现和排查问题。

4. 注意特征穿越(data leakage)问题——构造特征时只能使用当前时刻之前的信息,避免使用未来信息。

5. 特征工程是一个迭代过程,需要与模型训练、评估和业务验证紧密配合,形成闭环反馈。