从Kaggle经典赛题到实战:Rossmann销售额预测的数据探索与特征工程全解析
1. 初识Rossmann销售额预测赛题
第一次接触Kaggle的Rossmann销售额预测比赛时,我被这个真实业务场景的问题吸引了。想象一下,你是一家连锁药店的区域经理,需要提前六周预测每家门店未来48天的销售额——这直接关系到库存管理、人员排班和促销策略。比赛提供的2013-2015年德国1115家Rossmann门店数据,包含了日期、促销、节假日等丰富信息,简直就是时间序列预测的完美练兵场。
原始数据分为两个核心文件:train.csv记录每日销售情况,store.csv存储门店静态属性。我习惯先用pandas快速浏览数据结构:
import pandas as pd train = pd.read_csv('train.csv', parse_dates=['Date']) stores = pd.read_csv('store.csv') print(train.head()) print(stores.describe())关键字段包括:
- 日期特征:Date, DayOfWeek, Month
- 运营状态:Open(是否营业), Promo(是否促销)
- 节假日:StateHoliday(国家假日), SchoolHoliday(学校假期)
- 门店属性:StoreType(4种类型), Assortment(商品等级)
- 竞争信息:CompetitionDistance(最近竞对距离)
2. 数据清洗与异常处理实战
拿到原始数据后,我花了整整两天时间做数据清洗。首先是处理缺失值——CompetitionDistance有3%的缺失,我的处理策略是用同类型门店的中位数填充:
stores['CompetitionDistance'] = stores.groupby('StoreType')['CompetitionDistance']\ .transform(lambda x: x.fillna(x.median()))最棘手的是销售数据中的零值问题。原始数据中有0.3%的销售记录为0,分析发现它们分两种情况:
- 门店当天歇业(Open=0)
- 门店营业但销售额异常(Open=1)
对于第一种情况,我直接排除(测试集也只预测营业日的销售)。第二种情况采用Prophet模型进行插值:
from prophet import Prophet def fill_zero_sales(store_df): model = Prophet(yearly_seasonality=True) model.fit(store_df[store_df['Sales'] > 0]) future = model.make_future_dataframe(periods=365) forecast = model.predict(future) return forecast[['ds', 'yhat']]3. 时间序列特征深度挖掘
3.1 基础时间特征构造
日期本身没有预测价值,需要分解出有业务意义的特征。我创建了这些特征:
- 周循环特征:用sin/cos编码DayOfWeek(周一=sin(0), 周日=sin(2π))
- 月份效应:德国12月圣诞季销售激增
- 促销周期:Promo2是连续促销,需要计算当前处于促销周期的第几周
# 周循环编码示例 train['DayOfWeek_sin'] = np.sin(2 * np.pi * train['DayOfWeek'] / 7) train['DayOfWeek_cos'] = np.cos(2 * np.pi * train['DayOfWeek'] / 7)3.2 业务事件特征
通过分析节假日前后销售曲线,我发现:
- 圣诞节前两周销售额增长35%
- 复活节前一周增长22%
- 学校假期期间日均销售额比平时高18%
于是构造了"距离下次节假日天数"特征:
holiday_dates = train[train['StateHoliday'] != '0']['Date'].unique() train['DaysToNextHoliday'] = train['Date'].apply( lambda x: min([(d - x).days for d in holiday_dates if d > x], default=30))4. 门店画像与竞争分析
4.1 门店分层特征
StoreType和Assortment的交叉分析揭示出关键洞见:
- B类门店+扩展商品组合的门店,平均销售额比其他组合高62%
- 周日营业的门店(仅占15%)贡献了28%的总销售额
我为此创建了门店分层标签:
stores['PremiumStore'] = ((stores['StoreType'] == 'b') & (stores['Assortment'] == 'c')).astype(int)4.2 竞争特征工程
处理竞争对手数据时踩过一个坑:CompetitionOpenSince字段是竞对开业时间,需要计算"竞对已开业时长":
stores['CompetitionDuration'] = (pd.to_datetime('2015-08-01') - pd.to_datetime(stores['CompetitionOpenSinceYear'].astype(str) + '-' + stores['CompetitionOpenSinceMonth'].astype(str) + '-01')) stores['CompetitionDuration'] = stores['CompetitionDuration'].dt.days.clip(lower=0)热力图分析显示:竞对距离<500米的门店,促销效果会降低约15%。因此我创建了竞争强度指标:
stores['CompetitionIntensity'] = 1 / (1 + stores['CompetitionDistance']/1000)5. 高级特征工程技巧
5.1 动态时间窗口统计
借鉴比赛冠军方案,我实现了滚动统计特征:
- 过去30天同门店的销售均值/标准差
- 去年同期销售额变化率
- 最近一次促销的销售提升比例
def rolling_features(df, window=30): return df.groupby('Store')['Sales']\ .rolling(window, min_periods=1)\ .agg(['mean', 'std'])\ .add_prefix(f'rolling_{window}_')5.2 prophet时间序列分解
使用Facebook的Prophet模型分解每家店的销售趋势:
- Trend:长期趋势项
- Weekly:周周期项
- Yearly:年周期项
from prophet import Prophet def decompose_store(store_data): m = Prophet(weekly_seasonality=True, yearly_seasonality=True) m.fit(store_data.rename(columns={'Date':'ds', 'Sales':'y'})) components = m.predict(store_data) return components[['trend', 'weekly', 'yearly']]6. 特征筛选与模型优化
6.1 Null Importance特征筛选
面对300+特征,我采用Null Importance方法筛选:
- 用全部特征训练LightGBM模型记录特征重要性
- 随机打乱标签y,重复训练100次
- 保留真实重要性 > 打乱后重要性75分位数的特征
from lightgbm import LGBMRegressor def null_importance(X, y, n_runs=100): model = LGBMRegressor() model.fit(X, y) real_imp = pd.DataFrame(model.feature_importances_, index=X.columns) null_imp = pd.DataFrame() for _ in range(n_runs): y_shuffled = y.sample(frac=1) model.fit(X, y_shuffled) imp = pd.DataFrame(model.feature_importances_, index=X.columns) null_imp = pd.concat([null_imp, imp], axis=1) return real_imp, null_imp6.2 模型融合策略
最终方案融合了三个模型:
- XGBoost:处理数值型特征
- LightGBM:高效处理类别特征
- CatBoost:自动处理缺失值
融合时采用加权平均,权重通过交叉验证确定:
weights = { 'xgb': 0.4, 'lgb': 0.35, 'cat': 0.25 } final_pred = (weights['xgb'] * xgb_pred + weights['lgb'] * lgb_pred + weights['cat'] * cat_pred)7. 比赛经验与实用建议
通过这次比赛,我总结了几个关键经验:
- 业务理解优先:花时间研究德国零售业特点,比调参更重要
- 可视化驱动分析:每个特征生成时都绘制其与Sales的关系图
- 稳健的验证策略:采用时间序列交叉验证(TimeSeriesSplit)
- 避免过度工程:最终只用到了70个核心特征
对于想尝试该赛题的朋友,建议从简化版开始:
- 先只用10家门店数据
- 实现基础时间特征+门店属性
- 逐步添加促销/节假日特征
- 最后扩展至全量数据
零售预测的魅力在于,每个业务决策都能反映在数据中。这次实战让我深刻体会到:好的特征工程,就是把业务语言翻译成模型能理解的数据语言。
