第三章:数据准备与特征工程
好,咱们进入实战环节了。数据准备和特征工程,说白了就是给算法喂饭。饭好不好吃,直接决定模型能不能干活。我见过太多项目,模型调了半天没效果,最后发现是数据没洗干净。
3.1 订单数据清洗
拿到原始订单数据,第一件事不是急着建模。先看看数据长什么样。我个人习惯,先用 df.info() 和 df.describe() 扫一眼。
核心清洗步骤:
- 缺失值处理:订单金额、商品ID、用户ID不能为空。空着就删掉。
- 异常值过滤:订单金额为0或负数?明显有问题。我曾经遇到过一批测试订单混入生产数据,金额全是0.01元。
- 重复订单去重:同一个订单号出现多次,保留第一条。
- 时间格式统一:把字符串转成 datetime 类型。
import pandas as pd
df = pd.read_csv('orders.csv')
# 删除缺失关键字段的行
df = df.dropna(subset=['order_id', 'product_id', 'amount'])
# 过滤异常金额
df = df[(df['amount'] > 0) & (df['amount'] < 100000)]
# 去重
df = df.drop_duplicates(subset='order_id')
# 时间格式化
df['order_time'] = pd.to_datetime(df['order_time'])
注意:清洗时别太激进。有些异常值其实是业务特征,比如大额团购订单。建议先做分布统计,再决定阈值。
3.2 商品属性编码
商品信息通常是文本或类别型数据。模型不认识中文,得转成数字。嗯,这里要注意编码方式的选择。
常用的编码方式:
| 编码方式 | 适用场景 | 缺点 |
|---|---|---|
| Label Encoding | 有序类别(如商品等级A、B、C) | 无序类别会引入虚假顺序 |
| One-Hot Encoding | 无序类别(如商品品类) | 类别太多会导致维度爆炸 |
| Target Encoding | 高基数类别(如商品ID) | 容易过拟合,需要交叉验证 |
我个人习惯,对于商品品类这种几十个类别的,用 One-Hot。对于商品ID这种成千上万的,用 Target Encoding 或者干脆做频率编码。
# One-Hot 编码示例
df = pd.get_dummies(df, columns=['category'], prefix='cat')
# Target Encoding 示例(用目标变量的均值编码)
mean_encoding = df.groupby('product_id')['target'].mean()
df['product_id_encoded'] = df['product_id'].map(mean_encoding)
避坑指南:我曾经在 Target Encoding 上吃过亏。直接对整个数据集编码,导致训练集和测试集信息泄露。正确做法是在交叉验证的每一折内单独计算编码值。
3.3 地址经纬度提取
订单地址里藏着大量空间信息。比如用户住在哪个区、离仓库多远。这些对拆单路径规划特别重要。
地址转经纬度,说白了就是地理编码。我常用的方案:
- 高德/百度地图API:精度高,但有调用次数限制
- 离线库:比如使用行政区划表,把地址映射到区级中心点
- 规则提取:从地址字符串中正则匹配省市区
import requests
def geocode(address):
url = 'https://restapi.amap.com/v3/geocode/geo'
params = {
'key': '你的API_KEY',
'address': address,
'city': '北京'
}
resp = requests.get(url, params=params)
if resp.json()['status'] == '1':
location = resp.json()['geocodes'][0]['location']
lng, lat = location.split(',')
return float(lng), float(lat)
return None, None
df['lng'], df['lat'] = zip(*df['address'].apply(geocode))
注意:API调用很慢,几万条数据可能要跑几个小时。建议先做缓存,或者用批量接口。我一般会把结果存到本地SQLite,下次直接查。
3.4 时间窗口特征构建
订单时间不是孤立的。用户的行为有周期性。你想想看,早高峰和深夜的订单,配送路径肯定不一样。
常用的时间特征:
- 基础特征:小时、星期几、是否周末、是否节假日
- 滑动窗口:过去1小时、3小时、24小时内的订单量
- 周期性编码:用sin/cos编码小时和星期,保留周期性
- 距离上次订单:同一个用户上次下单到现在多久了
# 基础时间特征
df['hour'] = df['order_time'].dt.hour
df['weekday'] = df['order_time'].dt.weekday
df['is_weekend'] = df['weekday'].isin([5, 6]).astype(int)
# 周期性编码
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
# 滑动窗口特征(按用户分组)
df['past_1h_orders'] = df.groupby('user_id')['order_time'].transform(
lambda x: x.rolling('1h', min_periods=1).count()
)
个人经验:滑动窗口的窗口大小很关键。太小了噪声大,太大了反应迟钝。我一般会试3个不同的窗口,然后让模型自己选。另外,记得按用户分组,不然会把所有人的订单混在一起算。
3.5 本章知识体系
下面这张图,是我自己总结的数据准备流程。每次做项目前我都会过一遍,防止漏掉关键步骤。
数据准备这一步,看起来琐碎,但决定了模型的上限。我见过太多人花90%的时间调参,却不愿意花一天时间好好做特征工程。结果呢?模型精度死活上不去。
记住一句话:垃圾进,垃圾出。数据质量不行,再牛的算法也白搭。