最近在折腾一个电商销售数据项目,用Python把几十万条订单记录拆了个底朝天。这个项目不算复杂,但却是数据分析里最典型、最能练手的一条完整链路:从原始订单表出发,经过数据清洗、指标加工、分组聚合到可视化输出,最后产出一份能直接指导运营决策的结论。如果你刚学完Python基础语法,或者手里正好有一份不知道怎么下手的销售Excel,这篇文章的思路和代码可以直接抄,我会把每一步为什么这么做也讲清楚。
项目本身没有用什么高深算法,核心就是pandas加matplotlib,外加一些业务分析框架。真正花时间的不是写代码,而是想清楚要看哪几个指标、怎么处理脏数据、分析结果怎么落地。下面我把完整过程拆开讲,包括我踩过的坑和排查方法,希望你能少走弯路。
1. 拿到数据后,我先做了什么:整体思路与目标拆解
1.1 这个项目到底要解决什么问题
电商销售数据看起来就是一张订单表,但不同角色关心的点完全不同:运营想看整体销售额有没有波动、哪些商品撑起了大盘、用户是不是回头客;采购想看哪些品类卖得动、应该补什么货;财务想看收入成本是否匹配。如果一开始不把业务问题定下来,后面分析就是无头苍蝇。我在动手前先把目标拆成了三个层次:看清大盘走势、找出主力商品、识别核心用户群体。
这个项目采用的示例数据是某电商平台一段时间内的订单明细,字段包括订单ID、用户ID、下单日期、商品分类、商品名称、购买数量、单价、成本、订单金额等。我给自己定的交付物是一份带图表的分析报告,以及几组可直接给业务方用的结论,比如“A类商品贡献了80%销售额,需要保证库存”“30%用户贡献了70%成交额,应该做定向召回”。
拆解目标还有个好处:防止分析范围无限扩散。一开始有人建议我顺便把库存周转率、物流时效、会员成长体系统统做了,我拒绝了。聚焦在销售数据本身,先把销售趋势、商品贡献、用户价值这三件事做透,比每个都浅尝辄止有用得多。后续要扩展,完全可以在同一套清洗流程上继续加。
1.2 为什么选Python而不是Excel或BI工具
这类问题会有人问“Excel透视表不也能做吗”。确实能做,但有几个绕不开的痛点:数据量一旦超过几十万行,Excel会明显卡顿;复杂的清洗规则、异常值处理、跨表关联,用Excel公式写起来既难维护又容易出错;另外分析要可复现,如果下个月数据更新了,你还得重新操作一遍菜单,而Python脚本只需要重新跑一次。
BI工具如Tableau、Power BI适合做固定看板,但它们更适合“已知要持续监控的指标”。而项目早期往往是探索性的,你可能今天想看用户复购,明天想看品类关联,每个问题都要快速验证和迭代。Python环境下用pandas做探索性数据分析非常自由,写一段代码跑一下,结果立刻出来,再写一段又看到新角度。而且matplotlib、plotly这类库可以直接生成自定义图表,不用受制于BI工具预设的图类型。
当然,这不是说Excel和BI没价值。如果公司已经有了成熟的数据仓库,日常监控用BI更高效。但作为个人练手或者第一次做销售数据深度分析,Python是最合适的选择,因为它把数据获取、清洗、分析、建模、可视化全链路都打通了,技能也是通用的。
2. 数据清洗与预处理:先解决“脏数据”才能谈分析
2.1 先摸清字段结构和原始数据的样子
拿到数据第一件事不是算销售额,而是看数据结构。我习惯先读取前几行并查看字段信息,这一步能发现很多隐藏问题。例子里的数据集是CSV格式,读取时我用pandas指定一些常见参数,避免踩编码和类型解析的坑。
import pandas as pd import numpy as np import matplotlib.pyplot as plt df = pd.read_csv('sales_data.csv', encoding='utf-8', parse_dates=['order_date']) print(df.head()) print(df.info()) print(df.describe())df.info()会展示每列的非空数量、数据类型和内存占用,我遇到过明明是一个数字列,却被读成了字符串,因为原始数据里有空格或者金额符号。parse_dates参数可以让pandas在读取时就尝试把日期列转成datetime类型,省得后面再转换。
字段结构梳理清楚之后,我习惯列出一张简单的数据字典,不用很正式,自己看得懂就行:
| 字段名 | 含义 | 类型 | 是否允许为空 |
|---|---|---|---|
| order_id | 订单ID | string | 否 |
| user_id | 用户ID | string | 否 |
| order_date | 下单日期 | datetime | 否 |
| category | 商品分类 | string | 是 |
| product_name | 商品名称 | string | 是 |
| quantity | 购买数量 | int | 否 |
| unit_price | 商品单价 | float | 否 |
| cost | 成本价 | float | 是 |
| revenue | 订单金额 | float | 否 |
这张数据字典看起来简单,但它在后续清洗中作用很大。比如我发现原始数据里并没有revenue列,只有数量和单价,那就得自己计算;如果cost缺失,很多成本分析就不能做,需要在清洗阶段处理好。
2.2 缺失值、重复值和异常值处理三步走
清洗的核心是让数据变得可信。第一步先查缺失值占比,第二步查完全重复的订单记录,第三步查业务上不合理的异常值。
# 缺失值情况 missing = df.isnull().sum() print(missing[missing > 0]) # 重复记录 dup_count = df.duplicated().sum() print('重复记录数:', dup_count) # 业务异常值:数量或单价小于等于0 invalid_quantity = df[df['quantity'] <= 0] invalid_price = df[df['unit_price'] <= 0] print('异常数量记录数:', len(invalid_quantity)) print('异常单价记录数:', len(invalid_price))处理缺失值时我不会无脑填零。如果是分类字段如分类名缺失,我会尝试从商品名称推断,推不出来就统一标记为“未知分类”。如果是成本缺失,且缺失比例不高,可以按同品类平均成本填充,但一定要在分析报告里注明“成本含估算值”。对于重复记录,如果所有字段完全一样,大概率是重复导入或页面重复提交导致的,可以直接去重。
异常值要结合业务规则判断。我遇到过“促销价0元”的商品,这类不是错误,是引流赠品,如果直接过滤掉会损失真实销售行为。对于负单价、数量为0或超大数据量级的记录,我会先看占比,少于千分之一就剔除,占比高就得回去找数据提供方核实。有一个原则你最好记住:清洗逻辑要可解释、可追溯,不要为了好看而随便删数据。
2.3 日期字段和时间特征提取
订单日期是分析销售趋势的基石。如果原始数据里日期是字符串且格式不统一,比如有的“2024/1/5”,有的“2024-01-05”,统一转换就会报错。我推荐在转换时加上errors='coerce',解析不了的值变成NaT,之后再集中处理。
df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce') df = df.dropna(subset=['order_date']) df['order_year'] = df['order_date'].dt.year df['order_month'] = df['order_date'].dt.month df['year_month'] = df['order_date'].dt.to_period('M') df['weekday'] = df['order_date'].dt.weekday # 0=周一 df['hour'] = df['order_date'].dt.hour提取时间特征不是为了堆字段,而是后续分析需要。做月度趋势用year_month,看“一周里哪几天卖得好”用weekday,如果数据采集到具体时间点,还可以看“一天里哪个时段下单多”,这对客服排班、投放时段选择都有价值。时间字段处理完,数据清洗的主体工作就算完成了,可以进入正式分析。
3. 核心分析一:销售额与商品维度的下钻
3.1 整体销售走势怎么看:日、周、月三个时间尺度
先算核心指标销售额,再按时间聚合。聚合时最常犯的错是把订单ID数当成销售额,或者没把数量乘单价。我这里统一计算后存成列。
df['revenue'] = df['quantity'] * df['unit_price'] # 月度趋势 monthly = df.groupby('year_month')['revenue'].sum() print(monthly) # 每日趋势 daily = df.groupby('order_date')['revenue'].sum() print(daily.head())看整体走势不要只看一张图。月度趋势能看出长周期波动,比如双11所在月份销售额是否显著抬高;日趋势能看出促销节点、节假日、突发流量带来的短周期波动。还可以看周维度:把每周一至周日聚合,如果周末销售额明显高于工作日,说明用户偏个人消费场景;反之如果工作日更高,可能是企业采购为主。这些判断直接影响后续运营策略。
weekly = df.groupby('weekday')['revenue'].mean() print(weekly)我用过一个小技巧:把日趋势画出来后,叠加上移动平均线,能过滤掉周末带来的周期性跳动,让真正上升或下降的趋势更清楚。比如用daily.rolling(7).mean(),这比直接盯着每天的锯齿形曲线好懂得多。
3.2 商品贡献度分析:找出支撑大盘的20%商品
商品维度分析,第一步看品类汇总,第二步看单个商品排名,第三步计算累计销售额占比,做ABC分类。
# 品类维度的销售额和销量 cat_stats = df.groupby('category').agg( total_revenue=('revenue', 'sum'), total_quantity=('quantity', 'sum') ).sort_values('total_revenue', ascending=False) print(cat_stats) # 单个商品维度 product_stats = df.groupby('product_name').agg( total_revenue=('revenue', 'sum'), order_count=('order_id', 'nunique'), total_quantity=('quantity', 'sum') ).sort_values('total_revenue', ascending=False) print(product_stats.head(20))商品贡献的核心思想是帕累托法则,也就是通常说的二八定律。计算每个商品销售额占比,然后按销售额从高到低算累计占比。累计占比达到80%之前的商品是A类,80%到95%是B类,剩下的是C类。下面这段代码把分类标签加到商品汇总表上:
product_stats['revenue_ratio'] = product_stats['total_revenue'] / product_stats['total_revenue'].sum() product_stats['cum_ratio'] = product_stats['revenue_ratio'].cumsum() def abc_label(ratio): if ratio <= 0.8: return 'A' elif ratio <= 0.95: return 'B' else: return 'C' product_stats['abc_class'] = product_stats['cum_ratio'].apply(abc_label) print(product_stats['abc_class'].value_counts())做完这个分类,结论非常直观:可能几百个商品里只有几十个属于A类,却撑起了绝大部分销售额。A类商品必须重点维护库存,不能断货;B类商品保持正常采购和运营;C类商品数量多但贡献有限,不需要过度投入,甚至可以清理长尾。这个分析对采购和运营都有指导意义,比单纯列个Top10要实用得多。
3.3 促销活动效果怎么评估
很多订单表里有促销标记或者活动ID,如果原始数据没有,也可以用订单日期划分活动时间段。我当时手头的表里正好有“是否参与促销”字段,于是做了一个对比分析:
promo_stats = df.groupby('is_promo').agg( total_revenue=('revenue', 'sum'), order_count=('order_id', 'nunique'), customer_count=('user_id', 'nunique') ) promo_stats['avg_order_value'] = promo_stats['total_revenue'] / promo_stats['order_count'] print(promo_stats)评估促销不能只看销售额。很多情况是促销期间卖得多,但大量订单是低价清库存,也可能把本来会在正常价购买的订单提前到促销日消化。所以还要看三个指标:促销期的日均销售额对比活动前一周平日销售额;客单价是升高还是降低;活动结束后的一周销售额会不会断崖式下跌。
如果促销带来的是短期冲量、长期透支,那效果评估就要打个问号。做销售数据分析时,不只要给数字,还要给数字背后的业务解释。比如“今年6月销售额同比增长28%,主要是日用百货类目促销拉动,但该类目毛利率比均值低了5个百分点”,这种结论才能让业务方真正用起来。
4. 核心分析二:用户维度与复购行为拆解
4.1 简化版RFM模型的落地实现
用户分析里最经典的框架是RFM,分别用最近一次消费时间recency、消费频率frequency和消费金额monetary来给用户分层。这个模型在电商圈用了很多年,核心逻辑是:最近买过的人比很久没买的人更容易被激活,买得频繁的人价值更高,花得多的人贡献更大。手头有订单明细的话,用pandas十几行就能算出来。
reference_date = df['order_date'].max() + pd.Timedelta(days=1) rfm = df.groupby('user_id').agg( recency=('order_date', lambda x: (reference_date - x.max()).days), frequency=('order_id', 'nunique'), monetary=('revenue', 'sum') ) print(rfm.describe())这里的reference_date用来计算“距离现在多少天没有购买”,我习惯取数据最大日期加一天作为基准,避免跨月和跨年时基准不稳。频率指标要特别注意:一个订单可能包含多个商品,同一订单会出现多行,所以计算频次时用order_id去重计数,而不是用行数。这个细节很多人会踩坑,数出来一个用户买了上百次,实际上没那么多订单。
4.2 用户分层:把用户分成几个可运营的群体
算完RFM之后,要给每个指标打标签。简单做法是用中位数作为阈值:高于中位数记为1,低记为0。然后组合出8类用户,但8类太多,我一般合并成5类重点用户。
rfm['R_score'] = (rfm['recency'] <= rfm['recency'].median()).astype(int) rfm['F_score'] = (rfm['frequency'] >= rfm['frequency'].median()).astype(int) rfm['M_score'] = (rfm['monetary'] >= rfm['monetary'].median()).astype(int) def user_segment(row): if row['R_score'] and row['F_score'] and row['M_score']: return '重要价值客户' if row['R_score'] and row['F_score'] == 0 and row['M_score']: return '重要发展客户' if row['R_score'] and row['F_score'] and row['M_score'] == 0: return '潜力客户' if row['R_score'] == 0 and row['F_score'] and row['M_score']: return '重要唤回客户' if row['R_score'] == 0: return '一般挽留客户' return '普通新客户' rfm['segment'] = rfm.apply(user_segment, axis=1) segment_counts = rfm['segment'].value_counts() print(segment_counts)分组阈值不一定非用中位数,也可以用业务经验定的值,比如“30天内购买过算活跃”“累计购买5单以上算高频”。中位数的好处是自动适配数据分布,坏处是两组人数差不多,区分度可能会不够。如果做用户分层,我更建议结合客单价和毛利率再微调,比如客单价高的用户即使买得少也有很大价值,不应该被简单归为低价值用户。
4.3 复购率这个指标到底怎么算
复购率是电商老板最爱问的指标,但它必须定义清楚。我这里采用的算法是:统计每个用户的购买订单数,订单数超过1的用户占总用户数的比例。实际计算时还要规定计算周期,比如“某月新用户中,在后续90天内再次购买的比例”,这个叫新客次月复购率,和整体复购率意义不同。
user_order_counts = df.groupby('user_id')['order_id'].nunique() repurchase_rate = (user_order_counts > 1).mean() print('整体复购率:', round(repurchase_rate, 4))复购率数据看起来可能很低,别急着怀疑代码。很多平台首次购买用户占大头,一次性用户会把分母拉高,导致复购率跑不起来。这时候可以拆成两个人群看:新用户首单后30天内复购率、老用户月度复购率。我还习惯把用户分层结果和复购率关联起来,比如“重要唤回客户”里有20%在过去30天内没有新增订单,那说明最近的召回活动力度不够或触达渠道有问题。用户分析的价值不在于模型多高级,而在于能不能落到运营动作上。
5. 可视化输出:让数据自己说话
5.1 先解决matplotlib中文乱码和负号显示问题
画图最挫败的时刻,不是代码报错,而是所有标签变成方框。matplotlib默认字体不支持中文,必须先设置字体。我一般把字体配置封装在项目入口处,避免每个图表重复设置。
plt.rcParams['font.sans-serif'] = ['SimHei', 'Noto Sans CJK SC', 'PingFang SC'] plt.rcParams['axes.unicode_minus'] = False如果你在不同操作系统上跑,Windows用SimHei,macOS用水黑字体,Linux需要安装文泉驿或Noto Sans CJK。网上给的字体列表,不一定适用你的环境,最靠谱的办法是用matplotlib.font_manager查看系统已安装字体,挑一个可用中文的放进去。另外axes.unicode_minus必须设成False,否则坐标轴上的负号会显示成方块,影响整个图的观感。
5.2 用pandas聚合结果直接画趋势图
清洗和聚合完成后,画图本身很轻量。pandas的DataFrame自带plot方法,底层就是matplotlib,适合快速出图。月度销售趋势我用折线图,商品Top10用横向条形图,用户分层用饼图或柱状图。
fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # 月度销售额趋势 monthly.plot(ax=axes[0, 0], kind='line', marker='o', title='月度销售额趋势') axes[0, 0].set_xlabel('月份') axes[0, 0].set_ylabel('销售额') # 品类销售额占比 cat_stats['total_revenue'].head(8).plot(ax=axes[0, 1], kind='barh', title='主要品类销售额') axes[0, 1].set_xlabel('销售额') # 用户分层构成 segment_counts.plot(ax=axes[1, 0], kind='bar', title='用户分层数量') axes[1, 0].set_xlabel('用户群体') axes[1, 0].set_ylabel('用户数') # 周内销售分布 weekly.plot(ax=axes[1, 1], kind='line', marker='s', title='周内销售额均值') axes[1, 1].set_xticks(range(7)) axes[1, 1].set_xticklabels(['周一', '周二', '周三', '周四', '周五', '周六', '周日']) axes[1, 1].set_ylabel('销售额均值') plt.tight_layout() plt.show()子图的布局适合写分析报告,一张图说明一个观点。注意tight_layout()一定要加,否则标题和坐标轴标签容易重叠。图表的标题不应该是“趋势图”这种废话,而是直接告诉读者结论,比如“月度销售额在促销季出现明显峰值”,让看图的人不需要自己下结论。
5.3 画图时横坐标太密集怎么办
日维度数据画图,横轴标签会挤成一团。这个问题很常见,通常有三种解法:让标签旋转、减少标签数量、设置自适应刻度。第一种最省事:
plt.figure(figsize=(12, 5)) daily.plot() plt.xticks(rotation=45) plt.tight_layout() plt.show()如果旋转后还是太密,就每隔N天显示一个刻度:
import matplotlib.dates as mdates fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(daily.index, daily.values) ax.xaxis.set_major_locator(mdates.DayLocator(interval=7)) ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) plt.xticks(rotation=30) plt.show()DayLocator(interval=7)表示每7个点显示一个主刻度,X轴就不会被标签堆满。如果做月度图,可以换成MonthLocator。我自己做日常分析时,习惯把图保存成PNG之前先用figsize=(12, 5)这类的宽画布,字再密也比小图清楚。实在不行还有个大招:不画日线,改成周聚合或两周聚合。数据可视化的目标是把趋势说清楚,如果画出来都没法看,说明聚合尺度选得不对。
6. 项目实操中踩过的坑与排查实录
6.1 这几个高频问题,你很可能也会遇到
写Python分析销售数据,最让人头疼的不是分析逻辑,而是各种意外。我把项目里真实遇到过的坑整理成了一张速查表:
| 我遇到的现象 | 排查思路 | 解决办法 |
|---|---|---|
读取CSV报UnicodeDecodeError | 文件编码不是UTF-8,中文Windows常存成GBK | 读取时指定encoding='gbk'或encoding='utf-8-sig' |
| 日期字段变成字符串,聚合按字母排序 | 读取时没有指定parse_dates,或日期格式不一致 | pd.to_datetime(..., errors='coerce')统一转换 |
| 销售额一列有单位符号或逗号 | 列被识别成object类型 | 用str.replace去掉符号后再转数值,如df['revenue'].str.replace(',', '').astype(float) |
| 分组聚合后得到Series,却想当DataFrame用 | 忘加reset_index()或as_index=False | df.groupby('col', as_index=False).agg(...) |
| 中文图标显示方块 | matplotlib字体没有设置为中文字体 | 设置rcParams['font.sans-serif'],并重启内核 |
| 数据量几百兆,pandas内存不够 | 读入所有字段后内存爆满 | 只读取需要的字段,利用usecols,必要时用dtype压缩数值类型 |
| 统计用户频次时数值虚高 | 一个订单多行导致重复计数 | 用order_id.nunique()计算订单数 |
还有一个坑我必须单独说:groupby聚合后忘记reset_index(),会导致后续df['某列']操作失败或者索引对不上。我在早期项目里浪费过不少时间,后来习惯统一写成df.groupby(..., as_index=False).agg(...),直接保留普通整数索引。如果你要合并多个聚合结果,这个细节一定注意。
6.2 从分析结果到业务落地,我有几点实在体会
分析做完了,图也画了,最后一步是把结果讲给业务听。我个人的经验是,不要抛一堆表格和指标,而是把结论压缩成三句话:整体销售好不好,好或不好是由哪个因素引起的,下一步应该做什么。比如“月度销售额环比下降6%,主要来自华东区域订单量下降,建议先排查该区域物流异常或竞品活动影响。”这种结论业务方听完就能反应。
另一个经验是分析结果要及时和数据提供方核对。我做过一次销售额分析,发现临近月底销售额突然翻倍,差点写进报告说是增长。后来一问才知道,月底结算时财务把一批线下转账订单导入了同一张表。如果不核对业务背景,这种假象会直接误导决策。所以现在我在分析里遇到异常波动,第一反应不是找代码问题,而是先问数据从哪里来、中间有没有人工操作。
还有一个体会是分析代码要参数化。比如活动周期、用户分层阈值、ABC分类的80%比例,这些都不应该写死,最好放到配置文件或代码开头的变量里。因为下个月数据更新,运营问你“如果A类从80%改成70%,结果会怎么变”,你只需要改一个参数重新跑一遍,几分钟就能给出答案。这种可复用性才是用Python做分析相较于一次性Excel操作的最大优势。
如果你正准备做类似电商销售数据分析,建议复制项目里的核心流程,但一定要结合自己的数据和业务目标来调整指标。数据清洗会更脏一点、业务假设会更复杂一点,但只要大方向对,结果就不会差。我在实际做下来后最满意的是这套流程可以反复跑,每次数据更新都能快速生成新报告,节省下来的时间足够我多验证两个分析角度。