机器学习特征工程精选:缩放、编码、聚合、嵌入与自动化全解析
高质量模型的胜负手往往不在算法复杂度,而在特征构造的精细度。许多模型性能的瓶颈,恰恰来自那些常被忽视的特征工程细节。
第1部分:数值特征
1.1 缩放
大多数机器学习算法对量纲高度敏感——一个取值跨越0到1,000,000的列,会在训练中压制另一个仅0到1的列。这一点不容忽视。
三种主流缩放器各有适用场景:StandardScaler适用于近似正态分布的数据,是默认首选;MinMaxScaler将值压缩至[0,1],适合神经网络输入层;RobustScaler基于中位数与四分位距(IQR),对异常值不敏感,数据含离群点时更可靠。选对工具,事半功倍。
from sklearn.preprocessing import RobustScaler
df['salary_scaled'] = RobustScaler().fit_transform(df[['salary']])
⚠️ 注意:缩放器必须仅在训练集上拟合。在完整数据集上拟合会引入信息泄漏,这是常见陷阱。
1.2 对数变换
当数值列严重右偏——如收入、价格、营收等典型场景——对数变换能拉平分布,帮助模型捕捉潜在规律。
import numpy as np
df['revenue_log'] = np.log1p(df['revenue']) # log1p 可安全处理零值
1.3 分箱
连续变量有时转化为类别更有效。pd.cut()生成等宽分箱,适合均匀分布的数据;pd.qcut()按分位数切分,每个箱内样本量均等,更适合偏态数据。例如年龄分组,直观易懂:
df['age_group'] = pd.cut(df['age'], bins=[0, 18, 35, 55, 100],
labels=['teen', 'young_adult', 'adult', 'senior'])
1.4 交互特征
两个特征组合后的表达能力往往超越各自独立使用。例如房价评估中,每平方英尺价格比单纯价格或面积更有预测力;债务收入比同理。
df['price_per_sqft'] = df['price'] / df['sqft']
df['debt_to_income'] = df['debt'] / df['income']
线性模型中,多项式特征有助于捕获非线性关系:
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, include_bias=False)
# 生成: age, salary, age², salary², age × salary
1.5 裁剪异常值
与其直接删除异常值,不如截断至合理的百分位区间,既保留样本量,又避免极端值干扰:
lower = df['salary'].quantile(0.01)
upper = df['salary'].quantile(0.99)
df['salary_clipped'] = df['salary'].clip(lower=lower, upper=upper)
第2部分:类别特征
2.1 独热编码
将每个类别展开为独立的0/1列,适用于无内在顺序的名目类别。这是最基础且最常用的方案。
df_encoded = pd.get_dummies(df, columns=['city'], drop_first=True)
⚠️ 注意:若某列包含500个唯一值,独热编码会生成500列,导致特征维度爆炸。此时应改用目标编码。
2.2 标签编码
为每个类别赋予整数,仅限数据确实存在有序关系的场景。例如教育程度映射为数字,因为存在明确的高低顺序。
df['education'] = df['education'].map({
'High School': 0, 'Bachelor': 1, 'Master': 2, 'PhD': 3
})
但注意:切勿对城市名这类名义数据做标签编码——模型会错误认为 London > Mumbai,毫无意义。
2.3 目标编码
用对应分组的目标变量均值替换类别值,处理高基数特征时效果显著。例如用城市对应的用户流失率来编码城市ID。
from category_encoders import TargetEncoder
df['city_encoded'] = TargetEncoder().fit_transform(df['city'], df['churn'])
⚠️ 风险在于数据泄漏。生产环境推荐采用交叉折叠目标编码来规避。
2.4 频率编码
用每个类别的出现频率替换原始值。实现简单,但在树模型中常常表现惊艳——有时比复杂编码更有效。
freq_map = df['city'].value_counts(normalize=True)
df['city_freq'] = df['city'].map(freq_map)
2.5 二进制编码
介于标签编码与独热编码之间的折中方案,用较少列数处理高基数特征。例如100个类别只需7个二进制列即可表达。
from category_encoders import BinaryEncoder
df_encoded = BinaryEncoder().fit_transform(df[['city']])
# 100 个类别 → 仅 7 个二进制列
第3部分:日期时间特征
原始日期对多数模型毫无意义,需要提取其中蕴含的时间信息。
3.1 标准提取
df['order_date'] = pd.to_datetime(df['order_date'])
df['month'] = df['order_date'].dt.month
df['day_of_week'] = df['order_date'].dt.dayofweek
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
df['quarter'] = df['order_date'].dt.quarter
df['days_since'] = (df['order_date'] - pd.Timestamp('2024-01-01')).dt.days
3.2 周期编码
月份若作为普通数字输入,模型会认为十二月(12)与一月(1)相距甚远——但两者仅隔一个月,是循环关系。正弦和余弦变换能保留周期结构:
import numpy as np
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
同理适用于一天中的小时(除以24)或星期几(除以7),完美保留周期关系。
3.3 工作日历特征
import holidays
indian_holidays = holidays.India(years=2025)
df['is_holiday'] = df['order_date'].apply(lambda d: d in indian_holidays).astype(int)
df['is_month_end'] = df['order_date'].dt.is_month_end.astype(int)
第4部分:文本特征
4.1 基础统计特征
在引入复杂NLP方法之前,先提取简单统计量。实际效果往往超出预期——比如评论字数、平均词长、是否包含问号、大写字母占比等。
df['word_count'] = df['review'].str.split().str.len()
df['avg_word_len'] = df['review'].str.len() / df['word_count']
df['has_question'] = df['review'].str.contains(r'\\?').astype(int)
df['uppercase_ratio'] = df['review'].apply(
lambda x: sum(c.isupper() for c in str(x)) / max(len(str(x)), 1)
)
4.2 TF-IDF
TF-IDF将文本转化为按词项重要性加权的数值表示,是经典且高效的方法。
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(max_features=100, ngram_range=(1, 2), stop_words='english')
X_tfidf = tfidf.fit_transform(df['review'])
4.3 情感得分
from textblob import TextBlob
df['sentiment'] = df['review'].apply(lambda x: TextBlob(str(x)).sentiment.polarity)
# 范围从 -1(极度消极)到 1(极度积极)
4.4 句子嵌入
更现代的做法是利用预训练模型将文本压缩为稠密向量,捕获语义信息。在深度学习场景下,表达能力远超TF-IDF。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = model.encode(df['review'].tolist())
# 形状: (n_rows, 384) — 每行变成 384 个数值特征
第5部分:地理空间特征
5.1 距离特征
一个数据点与关键地标之间的距离,本身就蕴含大量预测信息。例如房产到市中心的距离,往往比经纬度坐标更有预测力。
from math import radians, sin, cos, sqrt, atan2
def haversine(lat1, lon1, lat2, lon2):
R = 6371
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
a = sin((lat2-lat1)/2)**2 + cos(lat1)*cos(lat2)*sin((lon2-lon1)/2)**2
return R * 2 * atan2(sqrt(a), sqrt(1-a))
city_centre = (28.6139, 77.2090)
df['dist_to_centre_km'] = df.apply(
lambda r: haversine(r['lat'], r['lon'], *city_centre), axis=1
)
5.2 Geohash
Geohash将经纬度编码为短字符串,每个前缀对应一个地理区域,天然适合位置聚合。例如精度5对应约5km的区域,可直接作为类别特征使用。
import pygeohash as pgh
df['geohash_5'] = df.apply(lambda r: pgh.encode(r['lat'], r['lon'], precision=5), axis=1)
# precision 5 ≈ 5km 区域
第6部分:聚合特征
在生产环境的机器学习系统中,聚合类特征的价值极高,尤其对客户行为与交易数据而言。这几乎是提升模型分数最有效的路径之一。
6.1 分组聚合
stats = df.groupby('customer_id').agg(
total_orders=('order_id', 'count'),
total_spent=('amount', 'sum'),
avg_order_value=('amount', 'mean'),
max_order=('amount', 'max')
).reset_index()
df = df.merge(stats, on='customer_id', how='left')
6.2 滞后和滚动特征
序列数据中,过去N个时间窗口内的表现往往是最强的预测信号。例如前一次消费金额、金额变化量、近30天累计支出等。
df = df.sort_values(['customer_id', 'order_date'])
df['prev_order_amount'] = df.groupby('customer_id')['amount'].shift(1)
df['amount_change'] = df['amount'] - df['prev_order_amount']
df['rolling_30d_spend'] = (
df.groupby('customer_id')['amount']
.transform(lambda x: x.rolling(3).sum())
)
第7部分:特征选择
构造特征只是工作的一半,另一半是剔除无用特征。特征并非越多越好,冗余与噪声会拖累模型。
7.1 删除低方差特征
若某列取值几乎不变(如全常数),模型从中学不到任何信息,直接移除。
from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.01)
X_reduced = selector.fit_transform(X)
7.2 删除高相关特征
高度相关的特征本质上是冗余信息。保留一个,丢弃其余,可有效降低共线性问题。
corr = df.corr().abs()
upper = corr.where(np.triu(np.ones(corr.shape), k=1).astype(bool))
to_drop = [col for col in upper.columns if any(upper[col] > 0.95)]
df.drop(columns=to_drop, inplace=True)
7.3 特征重要性
用树模型对特征排序,重要性接近零的径直剔除,简单粗暴但有效。
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
importance = pd.Series(model.feature_importances_, index=X_train.columns)
print(importance.sort_values(ascending=False).head(20))
7.4 SHAP值
SHAP不仅揭示特征重要性,还能解释每个特征对单条预测结果的具体影响方向与幅度。这是当前最受认可的可解释性工具。
import shap
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_train)
shap.summary_plot(shap_values, X_train)
第8部分:自动化特征工程
当候选组合数量庞大时,手动构造特征不再可行。更优方案是程序批量生成,再通过特征选择环节筛选。Featuretools正是为此而生。
import featuretools as ft
es = ft.EntitySet(id='orders')
es = es.add_dataframe(dataframe_name='orders', dataframe=df,
index='order_id', time_index='order_date')
feature_matrix, feature_defs = ft.dfs(
entityset=es,
target_dataframe_name='orders',
agg_primitives=['sum', 'mean', 'count', 'max', 'std'],
trans_primitives=['month', 'weekday', 'is_weekend'],
max_depth=2
)
print(f"Generated {len(feature_defs)} features automatically")
跑完之后,依次过方差过滤、相关性过滤,再根据特征重要性得分筛选,保留下来的就是值得投入的。自动化生成配合人工筛选,效率远超手动逐个构造。
总结
特征工程是领域知识与技术能力的交汇点。算法再精妙,也无法弥补特征层面的粗糙。持续产出高质量模型的工程师,往往不是掌握算法最多的人,而是对数据理解最深的人。从简单特征起步,量化每一步的收益,只在简单版本不敷使用时才引入复杂度。这才是务实的实践路径。