电商销售数据分析实战:从数据清洗到RFM用户分层

发布时间:2026/9/8 8:58:11
电商销售数据分析实战:从数据清洗到RFM用户分层 1. 这个分析项目到底在解决什么问题先交代一下背景。我接到的任务是分析某电商平台一整年的销售数据原始数据是几万条订单记录包含订单号、用户ID、商品类目、成交金额、下单时间、支付方式这些字段。客户的核心诉求有三层第一搞清楚整体销售走势和节奏第二找到贡献主要营收的商品结构和用户分层第三能从数据里挖出几条可以直接指导运营动作的结论而不是给一堆图表让运营自己猜。说实话这类需求在电商数据分析里非常典型。很多初学者拿到数据第一反应就是跑个折线图看看趋势但实际做下来会发现真正有价值的部分往往藏在数据清洗和特征工程的细节里。比如订单表里经常出现重复支付的记录、退款订单没标记、同一用户在不同渠道下的单需要去重这些问题如果不处理干净后面所有统计结果都是错的。我用的工具链很常规Python 3.10 pandas 2.0 NumPy 1.24 Matplotlib 3.7 Seaborn 0.12外加 Jupyter Notebook 做交互式探索。没有上 Spark 或者 Dask 这类分布式框架原因很简单数据量是几万条级别pandas 处理绰绰有余杀鸡不用牛刀。但如果你是处理千万级以上的订单数据或者需要做实时指标计算那确实要考虑换工具这个后面我会单独讲。整个项目我拆成了五个阶段数据探索与清洗、销售趋势分析、商品品类结构分析、用户价值分层、结论与建议输出。每一步都有对应的产出物和验收标准这样做的好处是每个阶段都能自查不会到最后发现数据出了问题还要倒回去重做。2. 数据清洗中那些容易翻车的细节2.1 从拿到原始数据到能分析的DataFrame第一步永远是眼睛先看不要急着写代码。我先用 Excel 打开原始 CSV 文件看了前几百行确认字段格式、缺失值情况、有没有乱码。这个动作看似原始但能帮我对数据质量有个直观判断比直接用df.info()更真实。然后才进入 pandas 流程import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 设置中文显示这一步在Mac/Linux/Windows上略有差异 plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, WenQuanYi Micro Hei] plt.rcParams[axes.unicode_minus] False df pd.read_csv(sales_data.csv, encodingutf-8) print(df.shape) print(df.info()) print(df.head())这里有一个特别容易踩的坑CSV 文件的编码。我这次拿到的文件是 UTF-8 编码但很多电商系统导出的 CSV 其实是 GBK 编码直接用pd.read_csv(sales_data.csv)会报UnicodeDecodeError或者读出来全是乱码。解决办法是在read_csv里指定encodinggbk或者encodinggb18030后者兼容性更好。2.2 重复订单、退款订单和异常金额的处理接下来是数据清洗的核心环节。我梳理了四类必须处理的问题第一重复记录。同一笔订单因为系统重试或者用户多次提交可能在表里出现多行。我按照订单号分组统计出现次数找出重复项然后保留第一条。# 检查重复订单号 dup_count df.duplicated(subsetorder_id, keepFalse).sum() print(f重复订单记录数: {dup_count}) # 删除重复记录保留第一条 df df.drop_duplicates(subsetorder_id, keepfirst)第二退款订单。电商数据里退款订单很正常但如果不剔除销售额会被高估。原始数据里有一个order_status字段取值包括 已完成、已退款、待发货 等。我把已退款和待发货的订单单独拎出来分析的时候分成有效订单和全量订单两个口径。# 标记退款订单 df[is_refund] df[order_status].apply(lambda x: 1 if x 已退款 else 0) # 有效订单数据集 df_valid df[df[is_refund] 0].copy() # 对比两个口径的总销售额 total_all df[amount].sum() total_valid df_valid[amount].sum() print(f全量销售额: {total_all:.2f}, 有效销售额: {total_valid:.2f})这里我多说一句很多分析报告只给一个销售额数字不说统计口径这是很不专业的做法。不同口径下得出的结论可能相差很大所以我在后面所有图表里都统一用有效订单数据但在汇报时会标注清楚。第三异常金额。我遇到过订单金额为负数、金额为 0 但有成交记录、金额超过 10 万的离谱数据。这些一般是测试订单、系统 bug 或者恶意刷单。处理策略是先看占比如果占比很小1%就直接剔除如果占比大就需要跟业务方确认不能自己拍脑袋删。# 剔除异常金额 df df[(df[amount] 0) (df[amount] 100000)]第四用户 ID 缺失或者格式不一致。有些订单的 user_id 是 NaN有些是字符串数字混在一起。我统一转成字符串缺失的直接标记为unknown_user后续做用户分析时单独处理。df[user_id] df[user_id].astype(str).str.strip() df[user_id] df[user_id].fillna(unknown_user)2.3 时间字段的标准化暗藏玄机的细节时间字段是本次分析里最容易被忽视、但影响最大的一个环节。原始数据的order_time是字符串格式类似 2024-05-12 14:23:56但其中有几条记录是 2024/5/12 14:23还有几条是 2024-05-12格式不统一。必须统一转成 pandas 的 datetime 类型df[order_time] pd.to_datetime(df[order_time], errorscoerce)这里有个关键参数errorscoerce遇到无法解析的格式会变成 NaT 而不是直接报错方便我们定位异常数据。转换后要检查有多少条 NaT然后决定是丢弃还是手动修正。时间字段标准化之后我额外提取了四个衍生字段年、月、日、星期几。df[year] df[order_time].dt.year df[month] df[order_time].dt.month df[day] df[order_time].dt.day df[weekday] df[order_time].dt.weekday # 0周一, 6周日 df[hour] df[order_time].dt.hour这些衍生字段在后面做趋势分析、星期规律分析和时段分析时会反复用到。尤其是weekday和hour很多初学同学想不到要提取结果后面想分析周末销售是否更好、晚上几点下单最多的时候又要回头重新处理数据浪费时间。2.4 商品类目字段的归类合并商品类目在原始数据里非常脏。同样的商品有的记录叫手机有的叫智能手机有的叫手机通讯。不统一的话按类目分组统计时会出现十几个相似类目图表根本没法看。我用了一个简单的规则脚本做归一化先列出所有唯一类目值然后按关键词映射到顶层类目。category_map { 手机: 手机数码, 智能手机: 手机数码, 手机通讯: 手机数码, 笔记本电脑: 电脑办公, 台式机: 电脑办公, 平板: 电脑办公, 上衣: 服饰鞋包, 裤子: 服饰鞋包, 运动鞋: 服饰鞋包, # ... 更多映射 } df[category_group] df[category].map(category_map).fillna(其他)这一步做完数据清洗才算真正结束。整个过程大概占了整个项目 40% 的时间这完全正常。很多新手急着画图结果输出的图表自己都不敢信就是因为在清洗环节偷了懒。3. 销售趋势分析看清大局才能谈细节3.1 按月销售趋势与金九银十验证数据清洗完成后第一件事是看整体盘子有多大。total_revenue df_valid[amount].sum() total_orders df_valid[order_id].nunique() total_users df_valid[user_id].nunique() avg_order_value total_revenue / total_orders print(f总销售额: {total_revenue:.2f}) print(f总订单数: {total_orders}) print(f总用户数: {total_users}) print(f客单价: {avg_order_value:.2f})然后按月汇总销售额和订单量df_valid[year_month] df_valid[order_time].dt.to_period(M).astype(str) monthly df_valid.groupby(year_month).agg( revenue(amount, sum), order_count(order_id, count) ).reset_index()画折线图两条线分别表示月销售额和月订单量。我得到的结论是全年销售呈现明显的季节性波动9-11月是全年高峰期12月有明显回落2月因为春节因素处于低谷。这个规律跟电商行业的金九银十说法基本吻合——9月、10月有开学季和国庆促销11月有双十一大促三个月的合计销售额占了全年的 45% 左右。但这里我要提醒一点看到趋势图上的峰值不要急着下结论说这个月做得好。一定得去查一下这个月是不是有大促活动、是不是有节日、是不是有重点商品上新。数据本身不会告诉你背后的业务原因需要结合运营日历去解读。我当时就发现 6 月有一个小高峰后来查了运营记录才知道是年中大促叠加新品发布。3.2 星期规律与每日时段热力分析除了月度趋势我还做了两件很多分析报告里不会深挖的事星期规律和时段规律。星期规律是订单量在周一至周日的分布。我原本预想周末会更高结果实际数据显示周二和周三反而是下单高峰周末略低。后来分析原因发现这个电商平台的用户群体以企业采购为主工作日集中下单周末反而少。这就是为什么不能凭直觉做业务的判断必须看数据。weekday_order df_valid.groupby(weekday).agg( order_count(order_id, count), revenue(amount, sum) ).reset_index() weekday_order[weekday_name] [周一, 周二, 周三, 周四, 周五, 周六, 周日]时段规律我用的是小时维度按小时-星期做交叉表再用 Seaborn 的热力图展示。展示出来之后非常直观能明显看到工作日上午 10-11 点、下午 3-4 点有两个下单高峰。这个发现对后续运营投放很有价值——广告预算应该集中在这些时间段客服排班也可以据此调整。pivot_table df_valid.pivot_table( indexweekday, columnshour, valuesorder_id, aggfunccount, fill_value0 ) plt.figure(figsize(16, 8)) sns.heatmap(pivot_table, cmapYlOrRd, annotFalse) plt.title(星期-小时订单量热力图) plt.show()3.3 趋势分析的几张关键图我做了四张核心趋势图分别是月度销售额折线图、月度订单量柱状图、星期订单量柱状图、星期-小时热力图。这四张图构成了宏观趋势分析的基础。做图表时有几个注意事项分享给大家图表标题、坐标轴标签、图例必须齐全中文显示要提前设置好字体否则导出图片时中文会变成方框。颜色不要用默认的彩虹色建议统一用一套色系比如蓝色系或者企业VI色系这样图表放在一起比较协调。图表尺寸要设置合理plt.figure(figsize(12, 6))比默认尺寸更适合屏幕阅读。导出的图片要设置dpi150以上否则放到 PPT 或文档里会模糊。plt.figure(figsize(12, 6)) plt.plot(monthly[year_month], monthly[revenue], markero, linewidth2) plt.title(月度销售额趋势) plt.xlabel(月份) plt.ylabel(销售额元) plt.xticks(rotation45) plt.grid(True, alpha0.3) plt.tight_layout() plt.savefig(monthly_revenue.png, dpi150) plt.show()4. 商品品类结构分析找出真正赚钱的品类4.1 销售额构成与增速的双维分析光看总盘子不够还得看结构。电商运营最关心的问题是哪些品类贡献了主要营收哪些品类增速快、有潜力销售额构成分析很简单用groupby按品类分组计算销售额、订单量、客单价、占比。category_stats df_valid.groupby(category_group).agg( revenue(amount, sum), order_count(order_id, count), avg_amount(amount, mean) ).reset_index() category_stats[revenue_share] category_stats[revenue] / category_stats[revenue].sum() category_stats category_stats.sort_values(revenue, ascendingFalse)我得到的结果是手机数码类贡献了总营收的 38%是绝对的营收主力电脑办公类占比 22%排在第二位服饰鞋包类占比 15%美妆个护类占比 10%剩下的是食品生鲜、家居家装等其他品类。但这里有个问题占比高的品类不一定值得重点投放因为可能已经处于存量市场。所以我还算了各品类的月均销售额增速用销售额构成和增速构成一张二维散点图横轴是销售占比纵轴是近半年增速气泡大小代表订单量。落在右上角的是明星品类落在左上角的是潜力品类落在右下角的是现金牛品类落在左下角的是瘦狗品类。这个思路借鉴了波士顿矩阵但做成了数据驱动的版本。4.2 客单价的品类差异与运营启示不同品类的客单价差异非常大。手机数码客单价接近 3000 元服饰鞋包客单价只有 150 元左右。这意味着它们的运营策略完全不同高客单价品类要注重信任背书、售后保障、分期付款等降低决策门槛的手段低客单价品类要注重组合销售、满减优惠、冲动消费刺激。category_stats[avg_amount] category_stats[revenue] / category_stats[order_count] category_stats category_stats.sort_values(avg_amount, ascendingFalse) print(category_stats[[category_group, avg_amount]])这个分析看似简单但对业务指导意义很大。我当时写报告的时候就专门强调了手机数码虽然营收占比高但复购率低用户购买周期长不能靠它做用户活跃服饰鞋包虽然客单价低但复购频次高是可以用来拉动用户粘性的品类。运营资源应该往哪个品类倾斜数据已经给出了方向。4.3 Top10 热销商品与连带购买分析我进一步按商品维度做了 Top10 热销排行输出热销商品的销售额、销量、毛利估算如果能拿到成本字段的话。这里有个小小的提醒热销商品不要只看销售额还要看销量和毛利率。有些商品销售额高但毛利极低是引流款有些商品销售额不高但毛利极高是利润款。两种商品的价值不同运营策略也不同。连带购买分析也是电商分析的一个隐藏亮点。我通过订单明细数据用关联规则找出了经常同时出现在一个订单里的商品组合。比如手机膜和手机壳同时出现的概率很高笔记本电脑和电脑包、无线鼠标的关联度也很高。这个发现可以直接指导商品推荐的设置以及详情页的交叉推荐模块。当然关联规则分析比如 Apriori 算法在这个数据量级下不算难但如果数据量巨大需要考虑用 FP-Growth 等更高效的算法。我在这个项目里直接用 pandas 做简单的两两组合计数已经能满足需求from itertools import combinations from collections import Counter # 每个订单的商品组合 order_items df.groupby(order_id)[product_name].apply(list) combo_counter Counter() for items in order_items: if len(items) 2: for combo in combinations(sorted(set(items)), 2): combo_counter[combo] 1 top_combos combo_counter.most_common(10)5. 用户价值分层RFM模型的电商实战5.1 RFM计算过程与分层规则设计用户分析是电商数据分析的重头戏。我这次采用 RFM 模型Recency最近一次购买距今的天数、Frequency购买频率、Monetary消费金额。这个模型不新鲜但非常实用而且实现难度不高。计算过程直接用 pandas 的groupby和aggimport datetime as dt # 确定分析截止日期 current_date df_valid[order_time].max() pd.Timedelta(days1) rfm df_valid.groupby(user_id).agg( recency(order_time, lambda x: (current_date - x.max()).days), frequency(order_id, count), monetary(amount, sum) ).reset_index()这里有个细节值得注意current_date我取的是数据里的最大下单时间再加一天而不是取系统当前日期。原因很简单这是一份历史数据用系统当前日期会导致所有用户的 recency 都偏大失去比较意义。得到 R、F、M 三个指标后需要一个分层规则。常见做法是分别计算三个指标的中位数高于中位数记 1低于或等于记 0然后组合成 8 个用户类别。rfm[R_rank] (rfm[recency] rfm[recency].median()).astype(int) rfm[F_rank] (rfm[frequency] rfm[frequency].median()).astype(int) rfm[M_rank] (rfm[monetary] rfm[monetary].median()).astype(int) def rfm_label(row): if row[R_rank] 1 and row[F_rank] 1 and row[M_rank] 1: return 重要价值用户 elif row[R_rank] 1 and row[F_rank] 0 and row[M_rank] 1: return 重要发展用户 elif row[R_rank] 0 and row[F_rank] 1 and row[M_rank] 1: return 重要保持用户 elif row[R_rank] 0 and row[F_rank] 0 and row[M_rank] 1: return 重要挽留用户 elif row[R_rank] 1 and row[F_rank] 1 and row[M_rank] 0: return 一般价值用户 elif row[R_rank] 1 and row[F_rank] 0 and row[M_rank] 0: return 一般发展用户 elif row[R_rank] 0 and row[F_rank] 1 and row[M_rank] 0: return 一般保持用户 else: return 一般挽留用户 rfm[user_segment] rfm.apply(rfm_label, axis1)这里的分层命名参考了经典 RFM 模型但实际业务中可以按自己的习惯调整。比如把重要价值用户叫作高价值核心用户一般挽留用户叫作流失风险用户让业务团队更容易理解。5.2 用户分层结果与针对性运营建议分层之后统计每个类别的用户数和贡献金额占比segment_stats rfm.groupby(user_segment).agg( user_count(user_id, count), total_monetary(monetary, sum) ).reset_index() segment_stats[user_ratio] segment_stats[user_count] / segment_stats[user_count].sum() segment_stats[monetary_ratio] segment_stats[total_monetary] / segment_stats[total_monetary].sum()我得到的结果非常有说服力重要价值用户只占用户总量的 12%但贡献了总营收的 56%。这就是典型的二八法则在电商数据里的呈现。同时一般挽留用户和重要挽留用户加起来占比接近 30%这部分用户有流失风险需要重点关注。基于分层结果我给运营团队提了几个具体建议对重要价值用户提供专属客服、新品优先体验、积分加倍等权益维持他们的忠诚度。对重要发展用户最近有购买且金额不低但频次低重点推送高频品类的优惠券提升购买频次。对重要保持用户购买频次和金额都很高但最近没有购买需要唤醒比如发送好久不见类型的定向优惠券。对重要挽留用户历史上贡献过较大金额但最近没买、频次也低了需要用大额限时优惠做最后的挽留。对一般挽留用户价值不高且濒临流失不建议投入过多营销资源。这些建议不是空话每一条都能对应到具体的运营动作这比单纯堆砌要提升用户粘性这种正确的废话有价值得多。5.3 新老用户构成与复购率分析除了 RFM我还做了新老用户分析。通过用户第一次下单时间判断是否为新用户然后统计每个月的用户构成。df_valid[first_order_time] df_valid.groupby(user_id)[order_time].transform(min) df_valid[is_new_user] (df_valid[first_order_time] df_valid[order_time]).astype(int)复购率计算逻辑是统计每个用户的下单次数下单次数为 1 的是新用户大于 1 的是复购用户复购率 复购用户数 / 总下单用户数。我那次算出的整体复购率只有 23% 左右说明这个平台的用户粘性一般需要通过会员体系、积分体系、定期促销等手段提升复购。6. 数据可视化与报告输出图表怎么组合才有效6.1 图表选型与组合原则数据分析的价值最终要通过可视化输出但图表不是越多越好。我遵循的原则是一张图只回答一个问题。整体趋势用折线图结构比较用饼图或堆叠柱状图对比排序用条形图分布情况用直方图或箱线图关系分析用散点图。这次项目里我最终输出的图表组合是分析模块图表类型回答的问题月度销售趋势折线图全年销售节奏如何哪些月份是高峰星期规律柱状图一周中哪天订单最多时间热力热力图每天的哪个时段订单集中品类结构饼图/条形图哪些品类贡献主要营收品类客单价对比条形图不同品类的消费水平差异用户分层饼图各类用户的构成比例用户价值分布散点图用户的消费金额与购买频次关系Top10商品条形图哪些商品是主力爆款6.2 一组实用的可视化代码示例下面给出一段生成品类销售额占比的完整代码适合直接套用plt.figure(figsize(10, 8)) colors sns.color_palette(Blues_r, len(category_stats)) explode [0.03] * len(category_stats) plt.pie( category_stats[revenue], labelscategory_stats[category_group], autopct%.1f%%, startangle90, colorscolors, explodeexplode ) plt.title(各品类销售额占比) plt.axis(equal) plt.tight_layout() plt.savefig(category_pie.png, dpi150, bbox_inchestight) plt.show()做热力图的时候注意一点sns.heatmap的annot参数如果设为 True会在每个格子里显示数值但当格子数量很多时会非常拥挤。我一般只在数据量小的时候开annot像星期-小时这种 7x24 的矩阵关闭annot反而更清晰。6.3 报告输出结论先行、数据佐证报告输出的核心原则是结论先行每一页 PPT 的标题就是一个结论然后放佐证的图表最后给具体建议。不要先贴一张大图让读者自己解读那是自嗨不是汇报。我的报告结构是核心结论摘要一页纸写最重要的3-5个结论整体销售趋势品类结构分析用户价值分析运营建议清单每一部分都用结论 → 图表 → 数据佐证 → 建议的格式。这样的报告老板和业务团队都能快速抓住重点不用从头到尾自己啃数据。另外如果读者需要把分析结果做成交互式看板可以考虑用 Streamlit 或者 Dash 快速搭建。pandas 负责数据计算Plotly 负责交互图表Streamlit 负责页面框架三件套很快就能出一个像模像样的数据看板。我后来在这个项目基础上做了个简版看板用 Streamlit 实现下拉框筛选品类、动态展示月度趋势整个过程不到两个小时。对电商数据的日常监控来说这个方案比每次重新跑一遍 Jupyter Notebook 高效得多。7. 整个项目踩过的坑与避坑清单7.1 排查链路完整复盘从错误结果倒推到元凶这个项目最有意思的部分其实是排查一个错误的结论。我在初步分析月度趋势时发现 3 月份的销售额高得离谱几乎是其他月份的三倍。当时第一反应是3月是不是有大促但查了运营日历发现并没有。于是我开始排查。第一步复查 3 月份的数据量发现订单数确实多出了几千条。第二步按小时拆分 3 月份的订单发现大量订单集中在同一天凌晨 2 点到 5 点而且金额都接近 9999 元。第三步抽查这些订单的用户 ID发现都是同一批测试账号。第四步查原始数据源头确认这是运营在做系统压力测试时产生的测试数据当时没有从线上环境清理掉。处理方案是剔除这些测试账号的订单重新计算月度汇总。剔除后3 月份的销售额回落到正常水平整体趋势变得合理。这个排查过程给我最大的启发是数据异常不要急着删先查清楚原因再决定怎么处理。删错了数据结论就失真了不删结论也被污染。最好的做法是先保留异常标记分析时按字段过滤而不是直接在原表上删除。7.2 遇到的常见报错与解决方法我把这次项目中遇到的几个高频报错整理成了一张表方便读者对照排查报错信息原因解决方法UnicodeDecodeErrorCSV编码不是UTF-8尝试encodinggbk或encodinggb18030SettingWithCopyWarning对DataFrame切片后再赋值用.copy()创建显式副本ValueError: cannot convert float NaN to integer包含NaN的列转整数先fillna()再转换KeyError: xxx列名不存在或拼写错误用df.columns检查列名AttributeError: NoneType object has no attribute xxx变量为None检查前面的操作是否返回空结果NameError: name plt is not defined没导入matplotlib执行import matplotlib.pyplot as plt其中SettingWithCopyWarning对新手来说最常见。比如df_valid df[df[is_refund] 0] df_valid[year_month] df_valid[order_time].dt.to_period(M) # 这里会警告原因是df_valid是df的一个视图而非副本对它做赋值操作会触发警告。解决办法是在切片时加.copy()df_valid df[df[is_refund] 0].copy() df_valid[year_month] df_valid[order_time].dt.to_period(M)7.3 关于工具链升级的一些看法如果你处理的数据量大很多比如千万级订单量pandas 单机内存可能会不够用。那要怎么办我建议按四个档位升级第一档优化 pandas 代码用category数据类型压缩内存用vectorized操作替代循环用chunksize分块读取。第二档用 PolarsRust 写的 DataFrame 库处理速度是 pandas 的数倍内存占用更小API 风格跟 pandas 很接近迁移成本不大。唯一的痛点是中文文档和生态不如 pandas 丰富。第三档用 Dask 或者 Spark支持分布式计算适合真正的大数据场景。但学习成本明显上升运维也要考虑不建议小项目硬上。第四档上数仓体系用 SQL 做ETL和汇总用BI工具如 PowerBI、Tableau做可视化Python 只负责复杂建模。我这个项目里pandas 完全够用。做电商数据分析大部分工作量其实在理解业务 清洗数据 提炼结论计算引擎只是工具工具选型取决于数据规模和团队能力没必要一味追求大而全。7.4 能直接抄的经验清单最后分享几条我在这个项目里沉淀下来的实操经验每一条都是从踩坑里总结出来的拿到数据先看df.describe()和df.isnull().sum()这两个命令能让你在 10 秒内了解数据的基本健康状况。所有金额字段最好单位统一要么都是元要么都是分混用会导致分析结果差了百倍。做月度趋势时注意月份的字符串排序问题。10月排在2月前面用 sort 会出乱序。解决办法是先把月份转成整数再排序。写 CSV 导出时用indexFalse否则会多出一列无意义的索引。报告里的每个数字都要能追溯。保留分析代码和中间结果文件不然过两周有人问这个数字怎么来的你可能自己都说不清楚。另外一个很常见的坑不要用matplotlib的默认样式直接出图先用sns.set_theme(stylewhitegrid)或者类似配置图的质量会提升一个档次。这在做汇报时影响很大同样一份数据图好不好看决定了老板愿不愿意认真看。