ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Rossmann Store Sales EDA实战:从数据清洗到销售预测特征工程

Rossmann Store Sales EDA实战:从数据清洗到销售预测特征工程 先说明一下这里的 EDA 指的是 Exploratory Data Analysis也就是数据探索性分析可不是画原理图那类 EDA 软件千万别搞混。做数据竞赛也好做实际业务分析也好我始终觉得拿到数据后第一件事不是急着建模而是沉下心把数据摸一遍。Rossmann Store Sales 这个 Kaggle 经典赛题我前前后后做过好几遍每次重新做 EDA 都会有新发现这也就是为什么我特别建议新手从这个项目入手数据量适中、业务场景清晰、特征类型丰富非常适合用来建立一套自己的 EDA 分析框架。这篇博文就完整记录一下我在这个项目上做探索性分析的流程、代码和踩过的坑希望能给正在入门数据竞赛的朋友一些可以照着抄的参考。1. 项目背景与数据业务理解先搞清楚赛题再动手1.1 Rossmann Store Sales 到底是一个什么任务Rossmann 是德国的一家连锁药店这个竞赛的目标是预测旗下 1115 家门店在 2015 年 8 月 1 日到 9 月 30 日这 6 周内的每日销售额。训练集从 2013 年 1 月 1 日开始到 2015 年 7 月 31 日结束时间跨度大约两年半。评估指标用的是 RMSPE即 Root Mean Square Percentage Error这个指标对预测值做了百分比归一化意味着不同销售额量级的门店会被尽量公平地衡量因为它是按比例计算误差的而不是看绝对误差。从业务上理解这是一类典型的销售预测问题难点在于销售数据背后混着大量周期性因素和一次性事件因素。周期性因素包括一周内的星期规律、一年的季节变化以及各门店自身的经营节奏一次性事件因素则包括公共假期、学校假期、临时促销等。EDA 阶段的核心任务就是把这些混杂在一起的因素一点点拆开看清每一个特征单独和销售额之间的关系为后面的特征工程和模型选择打基础。我个人的经验是拿到这种零售业务数据先别急着调用各种高级可视化库而是先把业务字段的含义逐条搞清楚。很多数据分析做得像无头苍蝇本质上是没有先做一个“业务字段词典”导致后面做的每一张图都不知道在回答什么业务问题。1.2 数据文件与字段逐个拆解这个项目一共有三个表train.csv、test.csv 和 store.csv。训练集和测试集都是按 商店 ID 日期 的形式组织的日级记录store.csv 则是商店的静态属性。主键关系上train 和 test 通过 Store 字段关联 store 表做 EDA 时我习惯先把 train 和 store 合并起来因为很多有意思的交叉分析需要商店静态属性参与。import pandas as pd import numpy as np train pd.read_csv(train.csv) store pd.read_csv(store.csv) df train.merge(store, onStore, howleft)合并之后我们来逐个看字段。训练集和测试集共有的字段包括 Store、DayOfWeek、Date、Open、Promo、StateHoliday、SchoolHoliday。train 里独有的字段是 Sales 和 Customerstest 里独有的是 Id。store 表里则是 StoreType、Assortment、CompetitionDistance、CompetitionOpenSinceMonth、CompetitionOpenSinceYear、Promo2、Promo2SinceWeek、Promo2SinceYear、PromoInterval。StateHoliday这个字段容易踩坑里面的取值分别是 0 表示无假期、a 表示公共假期、b 表示复活节、c 表示圣诞节。这个字段在读取时可能会被解析成字符串后续要做数值化处理。Assortment是商品分类级别a 是基本型、b 是附加型、c 是扩展型字符型数据在建模前需要编码。Promo是当天是否有促销活动的二值标记Promo2是门店是否参与持续促销计划的标记这两个字段名字相近但业务含义完全不同Promo 是临时性促销Promo2 是周期性持续促销后面分析时要把它们区分清楚。1.3 环境准备与代码骨架我习惯用 Jupyter Notebook 做前期探索因为交互式绘图方便反复调整。Python 环境主要依赖 pandas、numpy、matplotlib 和 seaborn。跨项目统一设置一下图表风格能省掉大量重复调参时间。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False sns.set_style(whitegrid)加载数据后我建议先对 Dataframe 做一个概览df.info()看字段类型和缺失情况df.describe()看数值字段的基础统计量df.head()看前几行数据格式。这一步虽然简单但能让你对数据规模和脏数据情况有个直觉。比如我这次加载后看到训练集约 101 万行商店属性表 1115 行就马上意识到后续所有分析都要带着“101 万行”这个量级去思考什么操作会慢、什么操作需要采样都会心里有数。2. 数据质量体检缺失值、类型、重复记录的排查2.1 缺失值检查与处理策略拿到训练数据第一步就是看缺失情况。用 pandas 的isnull()方法统计每一列缺失值数量我一般会写成一个小函数顺便输出缺失比例这样看着更直观。def missing_summary(df): missing df.isnull().sum() missing missing[missing 0].sort_values(ascendingFalse) return pd.DataFrame({缺失数量: missing, 缺失比例: missing / len(df)}) missing_summary(df)在这个项目里主要缺失集中在 store 表的几个字段上。CompetitionDistance 有少量缺失通常可以用中位数填充或者用距离较远的商店推测CompetitionOpenSinceMonth 和 CompetitionOpenSinceYear 是一对字段它们同时缺失表示这家店没有竞争对手或者信息确实没记录这种缺失本身可能就是一个有业务含义的特征Promo2SinceWeek、Promo2SinceYear 和 PromoInterval 同样成组缺失因为这些字段只在商店参与了 Promo2 计划时才有记录。处理缺失值我最想提醒的一点是不要一上来就机械地用 mean 或者 median 填充。你要先想清楚“为什么缺失”在这个项目里Promo2 相关字段缺失代表“没有参加持续促销”这是一个有效信息不是错误。分析阶段先用特殊值标记比如保留 NaN 参与分组观察它的模式和完整数据有没有差异比盲目填充更能保护信息量。2.2 数据类型校准与字段验证pandas 自动解析的数据类型不一定符合真实含义。重点检查 Date 字段是否被解析成了 object 类型StateHoliday 是否被读成了字符串。处理方式很简单df[Date] pd.to_datetime(df[Date]) df[StateHoliday] df[StateHoliday].astype(str)类型校准之后我还习惯做一次取值范围的合理性验证。比如 Open 字段只能是 0 或 1DayOfWeek 应该在 1 到 7 之间Sales 应该是非负数Customers 也应该是非负数。这些验证用 pandas 的value_counts()加条件筛选就能快速完成。别小看这一步数据竞赛的坑往往藏在字段语义里比如测试集中可能出现了训练集没有见过的 StateHoliday 取值如果不在 EDA 阶段发现建模时一定会出编码错误。类型验证还有一个容易被忽略的点同一天同一家店是否可能出现多条记录。从数据建模的角度每个 商店-日期 组合理论上应该唯一所以有必要做一次重复键检查dup_keys df.groupby([Store, Date]).size() dup_keys dup_keys[dup_keys 1] print(len(dup_keys))如果出现重复一般是因为原始数据导出问题需要进一步查看具体记录判断是保留还是合并。如果数量极少删除重复即可如果数量较多就要回到数据源头确认逻辑。2.3 训练集与测试集时间窗口核对时间窗口的核对用一句话就能说清训练集必须严格覆盖测试集之前的时间不能有交叉。Rossmann 这个项目里训练集截止到 2015 年 7 月 31 日测试集从 2015 年 8 月 1 日开始两者无缝衔接。这里有一个很实际的意义时间序列类任务中特征的时间跨度决定了模型是否会出现数据泄露。比如如果我们要构造“过去 7 天销售额均值”这样的滞后特征那么 2015 年 7 月 25 日到 7 月 31 日这 7 天的数据是合法的因为它们在预测窗口之前但如果你不小心把 8 月 1 日之后的任何信息混进特征那模拟的线上表现就会失真提交成绩会被取消或者被刷出榜单。我通常在 EDA 阶段就把 训练集、测试集 的时间范围打印出来并计算每个商店在训练集中有多少条记录、在测试集中有多少条做一个门店覆盖度检查。如果出现某些门店只在测试集出现而训练集完全没有这类冷启动门店的预测难度会显著提高需要记下来特征工程阶段要特别处理。3. 目标变量与单变量分布分析3.1 销售额的分布形态与对数变换销售额是本项目的预测目标EDA 第一步就是看它的分布。画直方图是最直接的fig, axes plt.subplots(1, 2, figsize(14, 5)) sns.histplot(df[df[Sales] 0][Sales], bins100, axaxes[0]) axes[0].set_title(Sales Distribution) sns.histplot(np.log1p(df[df[Sales] 0][Sales]), bins100, axaxes[1]) axes[1].set_title(Log(Sales) Distribution) plt.show()销售额分布有两个重要特征。第一存在大量 0 值记录这些是未营业门店也就是 Open 0 的情况它们的销售额就是 0。第二在正销售额范围内分布明显右偏大多数门店日常销售额集中在较低区间少数门店销售额特别高。如果直接用原始销售额建模模型会被那些极端大值带偏RMSPE 也会被少数误差极大的样本主导。所以对数变换几乎是必选项。对销售额取 log1p 之后分布会接近正态模型拟合更容易误差也更均衡。后面如果要交叉验证评估 RMSPE要注意在指标计算时做指数逆变换还原真实销售额。这里提供一个经验值训练集中单日销售额的中位数大概在 5000 到 7000 的区间但这只是一个参考不同窗口下结论会略有波动。3.2 顾客数、营业状态与促销标记的分布顾客数和销售额一样也是右偏分布但有趣的是它和销售额的相关性非常强。顾客数可以作为后续建模时一个潜在的强特征甚至可以把它当作一个中间任务先预测顾客数再基于顾客数预测销售额。Rossmann 竞赛中就有人用这种两阶段思路把测试集的顾客数预测出来再结合促销信息推算销售额提升效果明显。看一下分类字段的分布print(df[Open].value_counts(normalizeTrue)) print(df[Promo].value_counts(normalizeTrue)) print(df[StateHoliday].value_counts(normalizeTrue))一般来说训练集中大约八成左右的记录是营业状态两成是不营业。促销日占比大约在三到四成。StateHoliday 的取值分布极不均衡绝大多数是普通工作日0公共假期占比很低复活节和圣诞节更是稀少。这种极端不均衡的分类特征直接做 one-hot 编码后可能会变成稀疏特征在树模型上问题不大在线性模型上就需要谨慎处理。3.3 类别特征的基础统计StoreType 有 a、b、c、d 四种类型Assortment 有 a、b、c 三类。做单变量分析时我会看两个东西每个类别的记录占比以及每个类别下销售额的分布差异。用 seaborn 的boxplot或者violinplot可视化能很快看出不同类别间的销售水平差异。sns.boxplot(datadf[df[Sales] 0], xStoreType, ySales) plt.show()这个图几乎每次都会给我一个稳定的结论不同 StoreType 之间的销售额有肉眼可见的差异。d 类门店的销售水平明显高于其他类型b 类则整体偏低。这类结论在特征工程阶段可以直接转化为分组统计特征比如按 StoreType 计算销售额均值/中位数然后作为该类别门店的基准水平加入到模型中作为等级锚点。但是这里我必须提醒一句单变量分析只能看到现象不能直接断定因果。StoreType 和销售额的关系里混杂了门店规模和位置等未观测因素。EDA 的价值是让你有方向感真正要验证哪些因素有效还是得靠后面建模型做特征重要性对比。4. 多变量分析与业务洞察4.1 促销对销售和顾客数的提升效果促销是 Rossmann 数据里最重要的干预变量之一几乎所有优秀方案都围绕促销构造了大量特征。我先做最基础的对比分析促销日和非促销日的销售额、顾客数分布差异。promo_summary df[df[Sales] 0].groupby(Promo)[Sales].agg([mean, median, count]) print(promo_summary)结论通常非常明显促销日的平均销售额远高于非促销日顾客数也有显著提升。这说明促销不仅能拉升客单价还能带来更多客流。更细一层可以把促销和 StoreType 交叉起来看不同门店类型对促销的响应幅度是否一致。一般会发现大型门店促销的绝对增幅更大但小型门店的增幅比例可能更高。这个“促销弹性”差异在特征工程里非常有价值可以按门店计算促销日的平均销售增幅作为门店对促销敏感度的量化指标。促销分析的坑在于不要只看天数均值。促销在某些日期可能连续多日进行和星期效应、节假日效应纠缠在一起。为了拆干净可以控制星期几来比较同一星期下的促销 vs 非促销或者用简单的线性回归把星期、节假日先拟合掉再看促销残差。EDA 阶段能做到“控制单一变量”的思维后面建模时做特征消除和因果推断都会顺利很多。4.2 节假日与学校假期的销售差异StateHoliday 和 SchoolHoliday 直接反映外部事件对客流的影响。分析时我喜欢把节假日类型叠加到星期上做一个透视表看每个星期几在不同假期类型下的销售额趋势。pivot pd.pivot_table( df[df[Sales] 0], valuesSales, indexDayOfWeek, columnsStateHoliday, aggfuncmean ) print(pivot)一般能看到一个规律普通工作日的销售额相对平稳而公共假期或圣诞节当天很多门店直接休息Open 0导致整体销售贡献下降但在假期之前的一两天往往会出现囤货式消费销售额反而上升。这种“节前效应”在特征工程中很值得挖掘最简单的方式是构造“距最近节假日还有几天”和“距最近节假日结束过了几天”这类时间距离特征。SchoolHoliday 的影响在 Rossmann 德国门店场景里也很显著学校放假期间家庭出行增多药店客流会变化。不过要注意SchoolHoliday 的影响方向和大小可能因门店所处位置而完全不同位于学区附近的门店受影响大商业区的门店则不明显。EDA 阶段可以按商店类型分别统计学校假期的销售额变化为后续做区域型特征提供依据。4.3 商店类型与商品分类的影响把 StoreType 和 Assortment 等静态特征结合销售额做多维透视能发现一些有意思的模式。比如某种 StoreType 搭配特定 Assortment 级别的门店销售额特别高反过来某个组合下门店业绩普遍偏差。这种交叉组合可以作为一个综合类别特征加入模型。cross df[df[Sales] 0].groupby([StoreType, Assortment])[Sales].agg([mean, count]) print(cross)另一个静态特征是 CompetitionDistance也就是最近竞争对手的距离。一般分析下来距离越近销售额会偏低说明竞争压力真实存在但距离特别远和完全没有竞争对手的门店销售额水平可能反而走高。不过这个关系的非线性很强直接当线性特征用容易丢失信息。EDA 中我会画一个散点图或分桶均值曲线观察距离对销售影响的拐点在哪里然后决定构造分段特征还是只取 log 变换。4.4 时间趋势与周期性规律时间维度的分析是零售数据 EDA 的重头戏。画销售额随时间的整体趋势图可以看季节性和趋势性df_daily df.groupby(Date)[Sales].sum().reset_index() plt.figure(figsize(16, 5)) plt.plot(df_daily[Date], df_daily[Sales]) plt.title(Daily Total Sales) plt.show()整体趋势通常会呈现以年为周期的波动下半年节假日密集销售高峰更容易出现在 11 月到 12 月。周维度上周末和工作日的差异在 Rossmann 数据里非常清晰尤其是周日大量门店休息销售额贡献极低。做一个按星期几聚合的柱状图能直观看到周内波动规律。时间分析还有一个常常被忽略的部分不同门店的销售时间模式可能不同。有些门店周中强有些门店周末强这个模式本身就可以作为一个聚类特征。业内常用做法是对每个门店做“星期几销售额占该店总销售额比例”的归一化向量然后做 KMeans 聚类把门店划分为不同时间模式组。这种从 EDA 里长出来的想法比盲目堆特征要有说服力得多。5. 相关性分析与建模启示5.1 数值特征的相关性矩阵做完多变量分析可以顺手算一个数值特征的相关性矩阵快速把握全局关系。注意这里最好只取销售额大于 0 的记录否则 0 值会扭曲相关系数。numeric_cols [Sales, Customers, Promo, CompetitionDistance] sns.heatmap(df[df[Sales] 0][numeric_cols].corr(), annotTrue, cmapcoolwarm) plt.show()相关系数矩阵里最扎眼的一般是 Sales 和 Customers 之间的强正相关通常在 0.8 以上这在零售场景里非常合理。Promo 和 Customers 也有正相关说明促销确实能引流。CompetitionDistance 和 Sales 的相关性则比较弱但这不代表没有关系只是单纯看线性相关无法体现非线性效应所以前面我才会建议分桶观察。这里提醒一句相关系数只捕捉线性关系对非线性特征基本无效。EDA 时别只依赖相关系数分组均值、分位数对比、甚至简单的决策树特征重要性都更能反映非线性关系。5.2 从 EDA 落地的特征工程清单EDA 做到后面心里应该自然生出一张特征工程清单。我把 Rossmann 项目里最常用且有效的几类特征整理如下促销特征是否促销、促销连续天数、促销距上次促销间隔、门店促销频率、促销日销售增幅。时间特征月份、星期几、是否为月初/月末、第几周、距最近节假日天数、距上次学校假期天数。静态门店特征StoreType、Assortment、CompetitionDistance 分桶、竞争对手开店月数。门店-时间交互特征按门店聚合的销售额均值、按门店与星期几聚合的历史平均销售额、按门店与月份聚合的历史平均销售额。滞后特征前 1 天、前 7 天、前 28 天的销售额均值、顾客数均值通过时间窗口构建。这些特征不一定要全部做出来而是应该根据 EDA 中观察到的信号有选择地构造。比如你发现促销对 StoreType 为 d 的门店影响特别大那就很有必要做一个“促销 × StoreType”的交互特征。5.3 对模型选择与评估的影响EDA 还能帮助我们判断用什么模型框架。Rossmann 这类带强周期性和大量类别特征的问题梯度提升树几乎是首选原因在于它天然支持类别特征、能拟合复杂非线性、对缺失值有内置处理。线性模型需要做大量编码和标准化在这个项目里的竞争力通常弱于树模型。从评估指标看因为 RMSPE 按比例计算误差训练时应该关注相对误差而不是绝对误差。EDA 阶段可以先按 Sales 分位数把样本分桶分别看各桶内的 RMSPE确认误差主要来自哪一部分是低销售额的小店容易预测不稳还是高销售额的大店误差绝对值大但比例低。这样建模调参时就能有的放矢而不是盯着一个总指标盲目调参。6. 常见问题与实操心得6.1 新手在 EDA 阶段最容易踩的坑第一个坑就是把 EDA 当成一种“形式”画了一堆图却不知道回答什么问题。我会给自己定一个规矩每画一张图之前先在便签上写下“我要验证的假设是什么”。如果写不出假设这张图多半不用画。比如“判断 StoreType 对销售额有没有影响”是有方向的假设“看看所有字段长什么样”则容易画出没用的大杂烩。第二个坑是忽略 Open 0 和 Open 1 的分裂。很多新手把全部数据混在一起算销售额分布结果 0 值堆成一座山各种统计量和图形都失去参考意义。正确姿势是先按 Open 拆分分析营业日的销售规律再把关门概率单独建模因为关门和销售额是两个不同的问题关门是事件预测销售额是数值预测。第三个坑是不做数据量感知直接跑全量可视化。101 万行数据画散点图会渲染非常慢而且过度绘制严重什么都看不出来。处理办法是随机采样几万行再画或者用sns.kdeplot、sns.histplot这类密度图代替散点。采样不影响整体分布判断但能显著提升交互体验。第四个坑是没有把 EDA 和验证集设计结合起来。这个项目是时间序列如果随机打乱做交叉验证就会把未来的信息泄露给过去线上表现会被严重高估。EDA 阶段就应该建立“按时间切分”的验证思路比如用 2015 年 5 月到 7 月做验证。提前把验证方案想清楚比建模时亡羊补牢强得多。6.2 加快 EDA 效率的几个习惯我自己的 EDA 流程基本稳定在“质量体检 - 单变量 - 多变量 - 时间序列 - 结论清单”这个闭环里每一步都输出到一个总结论文件。习惯用 pandas 的groupby和pivot_table快速做聚合而不是反复写循环。很多探索只靠一两个分组透视就能看出规律不需要每次都上可视化。可视化留给真正需要判断形态和非线性关系的场景。另外建议把 EDA 的结论用 markdown 记录在一个固定模块里每条结论都标注“数据来源字段”和“对建模的潜在影响”。做特征工程时直接翻这份清单效率会高很多。这个习惯我保持了很久对比赛和实际项目都有帮助。6.3 更进一步的探索方向如果基础 EDA 做完还有余力可以继续往下走。比较有价值的方向包括门店销售额的时序分解把趋势、季节、残差拆开分析促销弹性的聚类分析用门店对促销的反应模式做人群细分外部数据的融合比如天气、节假日日历、地区经济指标虽然原赛题没给但这类外部变量经常能进一步提升模型表现。这些方向不一定每个都有用但会把你从“画图分析师”变成“业务洞察者”这个转变才是 EDA 真正的价值所在。我个人在实操中最大的体会是EDA 不是一次性工作而是会和特征工程、建模反复迭代的循环过程。第一版 EDA 可能只回答“数据大概长什么样”当你建完一版基线模型之后再带着残差分析回到 EDA 里往往能看到之前忽略的模式。最后再分享一个小技巧每次跑完 EDA把当时觉得最关键的三个结论写在便签上贴起来等到建模时如果发现特征没效果回去看看是不是当时漏掉了什么交叉项。这个小习惯帮我避免了很多无意义的重复实验也算是在 Rossmann 这类项目上反复打磨总结出来的经验。
返回列表