“数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限。“特征工程是机器学习中最耗时但也最重要的环节,往往比模型调参带来更大的性能提升。
1. 特征理解与分析
1.1 特征类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 数值型 | 连续或离散的数字 | 年龄、收入、温度 |
| 类别型 | 有限个离散值 | 性别、城市、品牌 |
| 有序型 | 有顺序的类别 | 学历、等级、评分 |
| 时间型 | 日期时间 | 注册时间、购买时间 |
| 文本型 | 自然语言 | 评论、描述 |
| 地理型 | 经纬度 | GPS 坐标 |
1.2 探索性数据分析 (EDA)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
df = pd.read_csv('data.csv')
# 基本信息
print(df.info())
print(df.describe())
print(df.isnull().sum() / len(df) * 100) # 缺失率
# 分布分析
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
for idx, col in enumerate(df.select_dtypes(include=[np.number]).columns[:6]):
ax = axes[idx // 3, idx % 3]
sns.histplot(df[col], kde=True, ax=ax)
ax.set_title(f'Distribution of {col}')
# 相关性热力图
corr = df.corr(numeric_only=True)
plt.figure(figsize=(12, 10))
sns.heatmap(corr, annot=True, cmap='coolwarm', center=0)
1.3 异常值检测
# IQR 方法
def detect_outliers_iqr(df, column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return df[(df[column] < lower) | (df[column] > upper)]
# Z-Score 方法
from scipy import stats
z_scores = np.abs(stats.zscore(df.select_dtypes(include=[np.number])))
outliers = (z_scores > 3).any(axis=1)
# Isolation Forest
from sklearn.ensemble import IsolationForest
iso = IsolationForest(contamination=0.1, random_state=42)
outliers = iso.fit_predict(df.select_dtypes(include=[np.number]))
2. 数值型特征处理
2.1 归一化与标准化
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# Z-Score 标准化(均值为 0,方差为 1)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Min-Max 归一化(缩放到 [0, 1])
scaler = MinMaxScaler(feature_range=(0, 1))
X_normalized = scaler.fit_transform(X)
# RobustScaler(使用中位数和 IQR,对异常值鲁棒)
scaler = RobustScaler()
X_robust = scaler.fit_transform(X)
选择准则:
- 数据分布近似高斯 → StandardScaler
- 需要固定范围(如神经网络输入)→ MinMaxScaler
- 存在异常值 → RobustScaler
2.2 数学变换
# 对数变换(处理右偏分布)
df['log_income'] = np.log1p(df['income'])
# Box-Cox 变换(自动找最优 λ)
from scipy.stats import boxcox
df['boxcox_income'], lambda_param = boxcox(df['income'] + 1)
# 平方根变换
df['sqrt_age'] = np.sqrt(df['age'])
# 分位数变换(映射到均匀/正态分布)
from sklearn.preprocessing import QuantileTransformer
qt = QuantileTransformer(output_distribution='normal')
X_transformed = qt.fit_transform(X)
2.3 分箱(Binning)
# 等宽分箱
df['age_bin'] = pd.cut(df['age'], bins=5, labels=['very_young', 'young', 'middle', 'old', 'very_old'])
# 等频分箱(每个区间样本数相同)
df['income_quintile'] = pd.qcut(df['income'], q=5, labels=['Q1', 'Q2', 'Q3', 'Q4', 'Q5'])
# 基于业务规则分箱
def age_group(age):
if age < 18: return '未成年'
elif age < 35: return '青年'
elif age < 50: return '中年'
elif age < 65: return '中老年'
else: return '老年'
df['age_group'] = df['age'].apply(age_group)
3. 类别型特征编码
3.1 基础编码
# One-Hot 编码(低基数类别)
df_encoded = pd.get_dummies(df, columns=['gender', 'city_level'], prefix=['gender', 'level'])
# Label Encoding(高基数或有序类别)
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['brand_encoded'] = le.fit_transform(df['brand'])
# 目标编码 / 均值编码(高基数,用目标变量统计值编码)
def target_encode(df, col, target, smoothing=1.0):
global_mean = df[target].mean()
agg = df.groupby(col)[target].agg(['mean', 'count'])
counts = agg['count']
means = agg['mean']
smooth = (counts * means + smoothing * global_mean) / (counts + smoothing)
return df[col].map(smooth)
df['city_target_encoded'] = target_encode(df, 'city', 'target', smoothing=10)
3.2 高基数类别处理
# 频率编码(用出现次数编码)
freq_map = df['category'].value_counts().to_dict()
df['category_freq'] = df['category'].map(freq_map)
# 聚类编码(对高基数类别聚类)
from sklearn.cluster import KMeans
# 用其他特征聚类
category_features = df.groupby('category').agg({
'price': ['mean', 'std'],
'sales': ['mean', 'sum'],
'rating': 'mean'
}).fillna(0)
kmeans = KMeans(n_clusters=20, random_state=42)
df['category_cluster'] = kmeans.fit_predict(category_features).reindex(df['category']).values
4. 时间序列特征
4.1 基础时间特征
df['datetime'] = pd.to_datetime(df['timestamp'])
df['year'] = df['datetime'].dt.year
df['month'] = df['datetime'].dt.month
df['day'] = df['datetime'].dt.day
df['hour'] = df['datetime'].dt.hour
df['dayofweek'] = df['datetime'].dt.dayofweek
df['dayofyear'] = df['datetime'].dt.dayofyear
df['weekofyear'] = df['datetime'].dt.isocalendar().week
df['quarter'] = df['datetime'].dt.quarter
# 周期性编码(用 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)
4.2 滞后特征与滑动窗口
# 按用户分组,构造滞后特征
df = df.sort_values(['user_id', 'datetime'])
# 滞后特征
df['price_lag_1'] = df.groupby('user_id')['price'].shift(1)
df['price_lag_7'] = df.groupby('user_id')['price'].shift(7)
# 滑动窗口统计
for window in [7, 14, 30]:
df[f'price_mean_{window}d'] = df.groupby('user_id')['price'].transform(
lambda x: x.rolling(window=window, min_periods=1).mean()
)
df[f'price_std_{window}d'] = df.groupby('user_id')['price'].transform(
lambda x: x.rolling(window=window, min_periods=1).std()
)
df[f'price_max_{window}d'] = df.groupby('user_id')['price'].transform(
lambda x: x.rolling(window=window, min_periods=1).max()
)
# 差分特征
df['price_diff_1'] = df.groupby('user_id')['price'].diff(1)
df['price_pct_change'] = df.groupby('user_id')['price'].pct_change()
# 历史累计特征
df['cumulative_spend'] = df.groupby('user_id')['amount'].cumsum()
df['order_count'] = df.groupby('user_id').cumcount() + 1
4.3 趋势特征
from scipy import signal
# 趋势分解
from statsmodels.tsa.seasonal import seasonal_decompose
decomposition = seasonal_decompose(df.set_index('datetime')['sales'],
model='additive', period=7)
df['trend'] = decomposition.trend.values
df['seasonal'] = decomposition.seasonal.values
# 统计特征:偏度、峰度
df['sales_skew_30d'] = df.groupby('user_id')['sales'].transform(
lambda x: x.rolling(30, min_periods=5).skew()
)
5. 交互特征
5.1 数值交互
# 多项式特征
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=False, include_bias=False)
X_poly = poly.fit_transform(X[['age', 'income']])
# 比率特征
df['income_per_age'] = df['income'] / df['age']
df['spend_ratio'] = df['spend'] / df['income']
# 加减交互
df['income_minus_avg'] = df['income'] - df.groupby('city')['income'].transform('mean')
5.2 类别-数值交互
# 分组统计特征
df['avg_price_by_category'] = df.groupby('category')['price'].transform('mean')
df['price_diff_from_category_avg'] = df['price'] - df['avg_price_by_category']
# 分组排名
df['price_rank_in_category'] = df.groupby('category')['price'].rank(method='dense')
6. 文本特征
6.1 基础文本特征
df['text_length'] = df['text'].str.len()
df['word_count'] = df['text'].str.split().str.len()
df['avg_word_length'] = df['text'].apply(lambda x: np.mean([len(w) for w in x.split()]))
df['exclamation_count'] = df['text'].str.count('!')
df['question_count'] = df['text'].str.count('\?')
df['uppercase_ratio'] = df['text'].apply(lambda x: sum(1 for c in x if c.isupper()) / len(x))
6.2 TF-IDF / Count 向量
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
# TF-IDF
tfidf = TfidfVectorizer(
max_features=1000,
ngram_range=(1, 2),
stop_words='english',
min_df=2,
max_df=0.8
)
tfidf_matrix = tfidf.fit_transform(df['text'])
# 主题特征 (LDA)
from sklearn.decomposition import LatentDirichletAllocation
lda = LatentDirichletAllocation(n_components=10, random_state=42)
topic_features = lda.fit_transform(tfidf_matrix)
7. 地理特征
# 经纬度距离
def haversine_distance(lat1, lon1, lat2, lon2):
R = 6371 # 地球半径 km
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
return 2 * R * np.arcsin(np.sqrt(a))
df['distance_to_center'] = haversine_distance(
df['lat'], df['lon'], 39.9042, 116.4074 # 北京中心
)
# 网格编码
df['lat_grid'] = (df['lat'] * 100).astype(int)
df['lon_grid'] = (df['lon'] * 100).astype(int)
df['grid_id'] = df['lat_grid'].astype(str) + '_' + df['lon_grid'].astype(str)
8. 特征选择
8.1 过滤法 (Filter)
from sklearn.feature_selection import SelectKBest, f_classif, mutual_info_classif, chi2
# 方差分析 F 值
selector = SelectKBest(score_func=f_classif, k=20)
X_selected = selector.fit_transform(X, y)
# 互信息
selector = SelectKBest(score_func=mutual_info_classif, k=20)
X_selected = selector.fit_transform(X, y)
# 卡方检验(非负特征)
selector = SelectKBest(score_func=chi2, k=20)
X_selected = selector.fit_transform(X, y)
# 查看选中特征
selected_features = X.columns[selector.get_support()]
print(f"选中特征: {selected_features.tolist()}")
8.2 包装法 (Wrapper)
from sklearn.feature_selection import RFE, RFECV
from sklearn.ensemble import RandomForestClassifier
# 递归特征消除
estimator = RandomForestClassifier(n_estimators=100, random_state=42)
rfe = RFE(estimator, n_features_to_select=15, step=1)
X_rfe = rfe.fit_transform(X, y)
# 带交叉验证的 RFE
rfecv = RFECV(estimator, step=1, cv=5, scoring='f1_macro')
X_rfecv = rfecv.fit_transform(X, y)
print(f"最优特征数: {rfecv.n_features_}")
8.3 嵌入法 (Embedded)
from sklearn.linear_model import Lasso, Ridge
from sklearn.ensemble import RandomForestClassifier
# Lasso 自动特征选择
lasso = Lasso(alpha=0.01)
lasso.fit(X, y)
selected = X.columns[np.abs(lasso.coef_) > 0]
# 随机森林特征重要性
rf = RandomForestClassifier(n_estimators=200, random_state=42)
rf.fit(X, y)
importances = pd.DataFrame({
'feature': X.columns,
'importance': rf.feature_importances_
}).sort_values('importance', ascending=False)
8.4 稳定性选择
from sklearn.linear_model import RandomizedLasso
rlasso = RandomizedLasso(alpha=0.025)
rlasso.fit(X, y)
selected = X.columns[rlasso.scores_ > 0.5]
9. 自动化特征工程
9.1 Featuretools
import featuretools as ft
# 定义实体集
es = ft.EntitySet(id='customer_data')
# 添加实体
es = es.add_dataframe(
dataframe_name='transactions',
dataframe=transactions_df,
index='transaction_id',
time_index='timestamp'
)
# 添加用户实体
es = es.add_dataframe(
dataframe_name='users',
dataframe=users_df,
index='user_id'
)
# 定义关系
relationship = ft.Relationship(
es["users"]["user_id"],
es["transactions"]["user_id"]
)
es = es.add_relationship(relationship)
# 自动特征合成
feature_matrix, feature_defs = ft.dfs(
entityset=es,
target_dataframe_name='users',
agg_primitives=['mean', 'sum', 'max', 'min', 'std', 'count', 'trend'],
trans_primitives=['day', 'month', 'diff', 'absolute'],
max_depth=2
)
print(f"生成 {len(feature_defs)} 个特征")
9.2 AutoFeat
from autofeat import AutoFeatRegressor
model = AutoFeatRegressor(
feateng_steps=2,
featsel_runs=3,
max_gb=16
)
X_new = model.fit_transform(X, y)
10. 特征存储与管理
# Feast:特征存储
from feast import Entity, Feature, FeatureView, ValueType
from feast import FileSource
# 定义实体
user = Entity(name="user_id", value_type=ValueType.INT64)
# 定义特征视图
user_stats_view = FeatureView(
name="user_stats",
entities=["user_id"],
ttl=timedelta(days=1),
features=[
Feature(name="avg_purchase_amount", dtype=ValueType.FLOAT),
Feature(name="total_orders", dtype=ValueType.INT64),
Feature(name="days_since_last_order", dtype=ValueType.INT64),
],
online=True,
source=FileSource(
path="data/user_stats.parquet",
event_timestamp_column="timestamp"
),
)
# 在线获取特征(低延迟)
from feast import FeatureStore
store = FeatureStore(repo_path=".")
features = store.get_online_features(
features=["user_stats:avg_purchase_amount", "user_stats:total_orders"],
entity_rows=[{"user_id": 123}, {"user_id": 456}]
).to_dict()
总结
特征工程速查表:
| 特征类型 | 处理方法 | 工具 |
|---|---|---|
| 数值型 | 标准化/归一化、对数变换、分箱 | sklearn.preprocessing |
| 类别型 | One-Hot、Label、Target、Frequency 编码 | pandas, category_encoders |
| 时间型 | 分解、滞后、滑动窗口、趋势 | pandas datetime |
| 文本型 | 长度统计、TF-IDF、预训练嵌入 | sklearn.feature_extraction |
| 地理型 | 距离、网格、聚类 | 自定义 + scipy |
| 缺失值 | 均值/中位数填充、模型预测、标记 | sklearn.impute |
工程建议:
- 先理解业务,再构造特征,避免"为特征而特征”
- 验证特征稳定性,训练集和测试集分布应一致
- 特征重要性排序,优先保留高价值特征
- 自动化工具(Featuretools) 辅助,但人工构造的业务特征往往更有价值
- 线上特征一致性:离线训练和在线推理使用同一份特征逻辑
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。