AI特征工程折腾手记
项目是这样的:根据用户最近30天的行为数据(点击、收藏、购买、停留时长等),预测接下来7天的购买概率。
数据大概长这样:
| user_id | click_count | fav_count | buy_count | avg_duration | last_active | device |
|---|---|---|---|---|---|---|
| 1001 | 156 | 23 | 5 | 120 | 3天前 | mobile |
| 1002 | 8 | 1 | 0 | 45 | 1小时前 | web |
| 1003 | 0 | 0 | 0 | 0 | 30天前 | mobile |
问题背景
遇到的坑
项目是这样的:根据用户最近30天的行为数据(点击、收藏、购买、停留时长等),预测接下来7天的购买概率。数据大概长这样:
| user_id | click_count | fav_count | buy_count | avg_duration | last_active | device |
|---|---|---|---|---|---|---|
| 1001 | 156 | 23 | 5 | 120 | 3天前 | mobile |
| 1002 | 8 | 1 | 0 | 45 | 1小时前 | web |
| 1003 | 0 | 0 | 0 | 0 | 30天前 | mobile |
表面上看数据挺完整,但实际问题一大堆:
- 极端值:有的用户每天点击几千次,有的一个月只有1次,直接用原始数值会把模型带偏
- 缺失值:有些用户从来没有收藏或购买,这些0到底是"没做过"还是"数据缺失"?
- 时间含义丢失:用"3天前"、“1小时前"这样的相对时间,模型学不到真正的衰减模式
- 高基数类别特征:user_id、设备类型这种字段直接丢进去会让模型过拟合或维度爆炸
最明显的结果是,模型对高频用户预测准确,对低频用户基本瞎猜。这不叫学习,这叫记忆。
整体思路
特征工程不是一蹴而就的,更像是一个迭代过程:
图里看着挺清晰,实际做的时候每个环节都能踩几个坑。下面按顺序展开说。
数据清洗
处理极端值
先用统计学方法识别极端值,然后决定是截断、缩放还是丢弃。
import numpy as np
import pandas as pd
# 简单的3-sigma法识别极端值
def detect_outliers(df, column):
mean = df[column].mean()
std = df[column].std()
return (df[column] > mean + 3*std) | (df[column] < mean - 3*std)
outliers = detect_outliers(df, 'click_count')
print(f"异常点击数量: {outliers.sum()}")

这张图解释了3-sigma法的核心思想:大部分数据会落在3个标准差内,超出这个范围就可能需要处理。
处理方式有几种:
截断:把超出范围的值固定在边界值上
def cap_values(df, column, upper=None, lower=None): upper = upper or df[column].quantile(0.99) lower = lower or df[column].quantile(0.01) df[column] = df[column].clip(lower=lower, upper=upper)对数变换:对偏态数据用对数压缩
df['log_click_count'] = np.log1p(df['click_count'])分桶:直接离散化,适合数值差异特别大的场景
df['click_level'] = pd.cut(df['click_count'], bins=[-1, 0, 10, 50, 1000], labels=['无', '低', '中', '高'])
实际效果:对数变换后,高频用户的权重被压缩了,模型对低频用户的预测准确率从40%提升到了65%。
处理缺失值
这次遇到的缺失值有两类:真正的空值(字段没填)和语义上的0值(比如用户从来没买过东西)。处理方式完全不同:
真正的空值:用均值、中位数、众数填充,或者直接删除
df['avg_duration'] = df['avg_duration'].fillna(df['avg_duration'].median())语义0值:需要单独造特征
df['has_buy_history'] = (df['buy_count'] > 0).astype(int) df['is_new_user'] = (df['click_count'] == 0).astype(int)
这点很关键:很多时候0不是没有信息,而是"没有正面信息”。单独造个特征把这种状态标出来,模型更容易学习。
特征构造
时间特征
原始数据里的相对时间(“3天前”)没法直接用。我把它转换成几个更直观的特征:
from datetime import datetime, timedelta
def parse_relative_time(time_str):
if '小时前' in time_str:
hours = int(time_str.replace('小时前', ''))
return datetime.now() - timedelta(hours=hours)
elif '天前' in time_str:
days = int(time_str.replace('天前', ''))
return datetime.now() - timedelta(days=days)
return None
df['last_active_time'] = df['last_active'].apply(parse_relative_time)
df['days_since_last_active'] = (datetime.now() - df['last_active_time']).dt.days
df['is_recent_active'] = (df['days_since_last_active'] <= 3).astype(int)
这样处理后,时间特征就有了明确的数值含义,模型能学到"越近越重要"的衰减规律。
交互特征
单个特征往往信息量有限,组合起来才有意思:
# 点击转化率
df['click_to_buy_rate'] = df['buy_count'] / (df['click_count'] + 1)
# 活跃度评分(自定义规则)
df['activity_score'] = (
df['click_count'] * 0.1 +
df['fav_count'] * 0.3 +
df['buy_count'] * 0.6
)
# 设备 × 活跃等级组合
df['device_activity'] = df['device'] + '_' + df['click_level'].astype(str)

这张图展示了交互特征的构建思路:两个原始特征通过某种运算生成新特征,往往能捕捉到单个特征看不到的模式。
特征选择
造完特征不代表都要用上。这次实际构造了47个特征,但最后只用了21个。选特征的方法有几种:
相关性分析
import seaborn as sns
import matplotlib.pyplot as plt
# 计算特征与目标变量的相关性
correlations = df.corr()['target'].abs().sort_values(ascending=False)
print(correlations.head(10))
# 特征间相关性(避免共线性)
plt.figure(figsize=(12, 8))
sns.heatmap(df.corr(), cmap='coolwarm', center=0)
plt.title('特征相关性热力图')
plt.savefig('static/images/posts/270-ai-feature-engineering-raw-high-quality-features/correlation-heatmap.webp')
特征重要性
训练一个简单的模型(比如随机森林或XGBoost),看每个特征的重要性:
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
importance = pd.DataFrame({
'feature': X_train.columns,
'importance': rf.feature_importances_
}).sort_values('importance', ascending=False)
print(importance)
递归特征消除
逐步移除最不重要的特征,观察模型效果变化:
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
selector = RFE(LogisticRegression(max_iter=1000), n_features_to_select=20)
selector.fit(X_train, y_train)
selected_features = X_train.columns[selector.support_]
实际效果:去掉相关性超过0.9的特征后,模型训练时间减少了40%,但准确率几乎没变化。
特征变换
标准化 vs 归一化
数值范围差异大的特征需要做缩放,否则会让梯度下降收敛变慢或某些算法失效:
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 标准化(均值0,方差1)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
# 归一化(映射到0-1)
minmax = MinMaxScaler()
X_train_normalized = minmax.fit_transform(X_train)
选择哪个?看数据和算法:
- 有明确边界的特征用归一化(比如概率、百分比)
- 正态分布或范围不固定的特征用标准化
- 树模型(随机森林、XGBoost)通常不需要
类别特征编码
类别特征不能直接用数值编码(会引入顺序关系),常用方法:
# One-Hot编码(低基数类别)
df_encoded = pd.get_dummies(df, columns=['device'], drop_first=True)
# 目标编码(高基数类别)
from category_encoders import TargetEncoder
encoder = TargetEncoder()
df['user_id_encoded'] = encoder.fit_transform(df['user_id'], df['target'])

这张图对比了几种编码方式的适用场景:低基数用One-Hot,高基数用目标编码或嵌入。
踩坑记录
坑1:数据泄露
最初我构造了"用户历史购买金额"作为特征,但发现训练集表现特别好,测试集很差。原因很简单:这个特征包含了未来信息(预测未来7天时,不应该用未来7天的数据)。
解决方式:严格按时间切分数据,确保所有特征都来自预测时间点之前。
# 正确的时间切分
train_data = df[df['date'] < '2026-06-01']
test_data = df[df['date'] >= '2026-06-01']
坑2:高基数类别
user_id这种高基数特征直接One-Hot编码会爆炸。尝试过几种方法:
- 直接删除:效果很差,用户差异信息丢了
- 统计聚合:比如"每个用户平均点击数",但会泄露信息
- 目标编码:效果好,但容易过拟合
最后用了分层采样 + 目标编码的混合方案,在数据泄露和特征信息量间找平衡。
坑3:过拟合
特征太多、太复杂时容易过拟合,表现就是训练集准确率接近100%,测试集只有70%左右。
解决方式:
- 正则化:L1/L2正则、Dropout
- 特征选择:砍掉低重要性特征
- 交叉验证:用验证集提前发现过拟合
from sklearn.model_selection import cross_val_score
scores = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy')
print(f"交叉验证准确率: {scores.mean():.3f} ± {scores.std():.3f}")
最终结果
折腾一圈下来,主要改进点:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 测试集准确率 | 68% | 89% | +21% |
| 低频用户准确率 | 40% | 82% | +42% |
| 模型训练时间 | 45分钟 | 27分钟 | -40% |
| 特征数量 | 12个 | 21个 | +75% |
最有意思的是,模型结构几乎没变,只是换了特征工程方式。这让我意识到:在数据质量很差的情况下,再高级的模型也救不回来。
小结
特征工程不是什么高深理论,更像是一门手艺。你需要理解数据的业务含义,知道哪些信息有用,哪些是噪音,然后用合适的方式表达出来。
这次踩坑也让我明白几个道理:
- 先看数据,再选算法:数据质量决定了上限,算法只是逼近这个上限
- 简单特征优先:复杂特征不一定更好,先试简单的,确实不够再加
- 严格切分数据:数据泄露会让所有努力白费,宁可牺牲一点信息量
- 持续验证:每一步改动都要用验证集检查效果,不要一口气改完
最后,如果你也在做类似的预测项目,建议先花时间把特征工程做好,而不是上来就调模型。毕竟,一把再好的菜刀,也切不出烂肉的味道。
版权声明: 本文首发于 指尖魔法屋-AI特征工程折腾手记(https://blog.thinkmoon.cn/post/270-ai-feature-engineering-raw-high-quality-features/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。