
上个季度最后一天运营同事从店铺后台导出一份订单明细表三万两千行丢给我的时候只留了一句“帮我看下这个季度卖得怎么样”。我用Python把这堆数据从头到尾捋了一遍最后交付的是一份带图表的分析报告中间踩的坑比预想中多得多——订单状态字段自带各种特殊符号、同一个订单号占三行却还有重复行、实付金额里混着退款负数和0元赠品单。这篇文章就是我处理这份电商销售数据的完整复盘。用到的工具不复杂就是Python加Pandas做清洗和聚合Matplotlib和Seaborn出图。如果你已经熟悉Python基础语法或者刚学完Pandas的教程但还没拿真实业务数据练过手这份复盘应该能帮你省掉不少试错时间。我不会只贴代码会把每个步骤背后的判断逻辑、以及为什么这样处理更靠谱都讲清楚。毕竟真实业务数据跟教程里干干净净的DataFrame完全是两码事。还没装环境的话先去Python官网装一个3.9以上版本然后pip install pandas matplotlib seaborn版本别太老就行。1. 拿到订单表之后先别急着跑先把数据结构和脏数据摸清楚1.1 一份电商订单表里通常藏着什么先看一下原始数据长什么样import pandas as pd df pd.read_csv(orders_quarter.csv, encodingutf-8) print(df.shape) print(df.dtypes) print(df.head(10))实际电商后台导出的订单表一般都会包含下面这些字段字段常见类型说明order_id字符串订单编号一个订单可以包含多个商品行order_datedatetime64下单时间pay_datedatetime64支付时间未支付则为NaTuser_id字符串/整型用户ID有的后台导出的是脱敏IDsku_id字符串商品SKU编号product_name字符串商品/套餐名category字符串类目比如“手机数码”sale_pricefloat单价quantityint数量payment_amountfloat实付金额含所有优惠status字符串订单状态已付款/已发货/已完成/已退款...这个表给我的第一个教训就是后台导出的数据永远不是整洁的。比如订单编号这一列有的后台会带上不可见字符看起来一模一样但实际不一样groupby的时候会出现同一个订单被拆成两个组的诡异现象更麻烦的是同一个订单买了5件商品就有5行这是一个“明细宽表”结构计算订单数的时候如果只数行数订单量和销售额都会虚高。1.2 第一步不是建模是 df.info 和缺失值侦察我拿到表从来不急着跑业务统计先做三件事df.info()看每列的非空数量和数据类型。df.isna().sum()看缺失值分布。df.describe()看看数字列的分布情况。这三件事做完数据大概什么脾气就心里有数了。比如sale_price显示成 object 而不是 float十有八九是有些格子塞了29.9这种带符号的值payment_amount如果最小值是负数那就是退款单混在里面。这些都是后面处理的重点。提示读取 Excel 导出的 CSV 时encodingutf-8和encodinggbk经常来回试实在不行可以df.to_csv(clean.csv, indexFalse, encodingutf-8-sig)转一道能避开大多数编码问题。1.3 一个反直觉的检查敏感字段处理很多人忽略分析电商订单数据时最好把收货人姓名、手机号、详细地址这些字段先删掉。一方面这些字段和销售分析几乎没有关系另一方面如果后面要把数据发到别处或者不小心分享出去隐私上容易出事。我一般直接用drop(columns[consignee, phone, address])把这几个列先干掉让内存和分析链路都干净不少。2. 数据清洗把脏数据扔进“垃圾回收站”之前要做什么清洗不是盲目的而是基于业务规则去判断哪些行可以删、哪些要补、哪些要重新标记。2.1 缺失值先分清楚“不该空”和“可以空”缺失值处理的大忌是“遇到空就 dropna”。在订单表里user_id为空说明这单用户信息有问题如果保留会影响用户维度统计应该删或标记pay_date为空但status是“已付款”那说明支付时间数据丢失未必可以简单删除。我这里的处理逻辑是payment_amount为空且status为“未支付”的行说明只是待支付状态没形成销售直接删掉不影响销售分析payment_amount为空但status不为空的行要挑出来人工看大概率是后台导出乱码。# 先看核心字段的缺失分布 important_cols [order_id, user_id, payment_amount] print(df[important_cols].isna().sum()) # 删除未支付订单 df df[df[status] ! 未支付] # 删除实付金额为空的残次行 df df.dropna(subset[payment_amount])2.2 重复行要区分“合理重复”和“非法重复”这个点需要反复强调。新手最容易犯的错就是把同一个订单号因为多商品占的多行误当成重复行删了。一定要先做一次全字段判断# 完全重复行这种才是需要怀疑的重复 full_dup df.duplicated().sum() print(f完全重复行数: {full_dup}) # 同一订单出现多次先看次数分布是否合理 dup_order_rows df.groupby(order_id).size() print(dup_order_rows.value_counts().sort_index())如果发现同一个订单号出现次数特别多可能有两个原因后台有二次编辑记录的导出或者用户下单后拆单。拆单本身没问题但如果排查后发现同时存在完全相同的两行直接用drop_duplicates()去重即可。2.3 退款单绝不能混进销售额电商订单表里退款单在payment_amount里可能是负数也可能是正数。绝不要等到算了销售额才发现金额是负的。我统计销售额之前都会先把合法销售单过滤出来# 将状态统一为中文标准值有些后台会导出“已退款”“退款中”“售后退款已受理” refund_mask df[status].str.contains(退款, naFalse) df_refunds df[refund_mask] df_sales df[~refund_mask] # 额外实付金额 0 但状态不是退款的多半是赠品/0元单按业务规则处理 zero_orders df_sales[df_sales[payment_amount] 0]这个环节不处理好后面所有指标的口径都会是歪的。我有一次就是忘了先过滤退款单结果月度销售额凭空多算了18%。2.4 数据类型与金额字段的“硬修复”粗暴一点的修复方式也是应急方案# 金额字段如果是 object先替换掉符号再转 float df[payment_amount] ( df[payment_amount] .astype(str) .str.replace(¥, , regexFalse) .str.replace(,, , regexFalse) .str.strip() ) df[payment_amount] pd.to_numeric(df[payment_amount], errorscoerce)errorscoerce这个参数特别好用它会把不能转数字的值变成 NaN这样后面统一处理。同理quantity这种整数列也可以用pd.to_numeric兜底防止出现“数量‘三件’”这种中文值。到这里数据已经能支撑后续计算了但我在实际项目中还会额外保留两个派生列order_month支付月份和line_total行金额 单价 × 数量后面用来算折扣率这是为了让下游分析不用到处再算。3. 指标口径别拍脑袋销售额、客单价、复购率怎么算才不算错3.1 销售额三个候选口径先统一老板说“这个季度销售额多少”其实有三种算法所有订单payment_amount相加包含退款。错虚高。只统计“已付款/已发货/已完成”且排除退款。推荐。只统计已完成的订单。适合做财务对账但会滞后。我自己做主分析时用第2种因为它最能反映真实的销售和回款趋势。需要跟老板对齐口径不然你算出来一千万财务算出来八百万会议桌上就非常尴尬。3.2 客单价按订单算还是按用户算客单价通常等于销售额除以支付订单数。这里的订单数要df[order_id].nunique()而不是len(df)因为一个订单多行会导致分母虚高。我见过不少报告客单价算得异常低就是没绕开“订单行数不等于订单数”这个坑。如果按用户维度算“人均客单价”就是销售额除以付费用户数它反映的是每个花钱的人平均贡献多少金额。两者名字很像业务含义差很多报告里要写清楚是哪一个。total_sales df_sales[payment_amount].sum() valid_orders df_sales[order_id].nunique() paid_users df_sales[user_id].nunique() avg_order_value total_sales / valid_orders # 客单价订单维度 avg_revenue_per_user total_sales / paid_users # 人均贡献3.3 复购率这个月下过两单的用户占比计算复购率的逻辑很直接但陷阱在于“统计周期”和“订单窗口”。我按自然月算的流程是# 给每条订单打上月度标记 df_sales[pay_month] df_sales[pay_date].dt.to_period(M) # 每个用户每月下单次数订单数去重用 nunique repurchase ( df_sales.groupby([user_id, pay_month]) .agg(order_cnt(order_id, nunique)) .reset_index() ) # 月内下单次数 2 的用户为复购用户 repurchase[is_repurchase] (repurchase[order_cnt] 2).astype(int) # 汇总月度付费用户总数 / 复购用户数 monthly_stats ( repurchase.groupby(pay_month) .agg(total_users(user_id, nunique), repurchase_users(is_repurchase, sum)) .assign(repurchase_ratelambda x: x[repurchase_users] / x[total_users]) ) print(monthly_stats)这里还有个小细节如果把分析窗口拉长到90天那么“复购”的定义也要跟着调整比如90天内下单≥2次的用户占比计算时要基于用户在该区间的首单去对齐而不是简单从当月开始算。口径不同复购率能差出5到8个百分点都要给老板讲清楚别让他自己乱猜。3.4 动销率和其他补充指标动销率等于有销售的商品SKU数除以在售SKU数。这个指标能快速反映店铺是不是有一堆“僵尸商品”。在脚本里维护一个字段就能算。我建议做季报时把这几个指标放进同一张汇总表里指标计算方式业务含义销售额sum(payment_amount)排除退款整体盘子销售订单数order_id 去重计数成交量客单价销售额/订单数单均价值复购率月内下单≥2次用户占比用户粘性动销率有售SKU/在售SKU商品健康度这类指标表设计好之后后面每个月的分析报告复用起来就很省事你只需要把同样的函数跑一遍。4. 多维拆解从时间、商品、用户三个方向把数据“切”开4.1 时间趋势日粒度太抖周粒度正好先做日趋势daily_sales ( df_sales.groupby(df_sales[pay_date].dt.date) .agg(sales(payment_amount, sum), orders(order_id, nunique)) .reset_index() )画出来之后大概率是锯齿状因为电商订单天然有周末高、工作日低的节奏。所以我更推荐按周聚合weekly_sales ( df_sales.groupby(pd.Grouper(keypay_date, freqW-MON)) .agg(sales(payment_amount, sum), orders(order_id, nunique)) .reset_index() )W-MON的意思是从周一开始算一周这样更符合业务习惯。环比分析也简单直接用上一周同组数据相除即可。4.2 商品结构头尾效应比平均数重要销售额排名Top10的商品单独做一栏贡献度用“Top10销售额/总销售额”来算。很多电商店铺会出现“二八效应”——20%的SKU贡献80%的销售额这是正常现象但你要看的是尾部那80%的商品是不是还占着库存和运营成本。代码上也就两步product_sales ( df_sales.groupby([sku_id, product_name]) .agg(sales(payment_amount, sum), qty(quantity, sum), orders(order_id, nunique)) .reset_index() .sort_values(sales, ascendingFalse) ) product_sales[sales_share] product_sales[sales] / product_sales[sales].sum() print(product_sales.head(10))再进一步把每个类目的销售额和退货率放一起看。我这次分析就发现“服饰类”销售额排第二退货率却高达31%这时候结论就不是“好卖”而是“卖是真的好卖但品控或展示可能有问题”。这类交叉对比才是数据拆解的价值。4.3 用户分层新客、老客复购和流失用户识别用户维度我只强调一个点新老客的划分要基于“该用户是否在分析窗口之前购买过”而不是“这次订单是否是首单”。# 找出每个用户的首次下单时间 first_order ( df_sales.groupby(user_id)[pay_date].min() .reset_index() .rename(columns{pay_date: first_order_date}) ) # 分析窗口开始时间假设是季度初 window_start pd.Timestamp(2024-01-01) first_order[is_new_user] (first_order[first_order_date] window_start).astype(int)老客复购分析里可以算一下“上季度有购买、本季度没购买”的用户名单这就是流失用户池。把流失用户的商品偏好拉出来后面做召回营销的时候直接可以用。4.4 groupby 之后 index 的坑分享一个很烦的隐性坑groupby().agg()之后分组字段会跑到 index 里如果你连续做两个 groupby 再 merge极容易因为 index 没 reset 而对不上。我习惯在每次groupby后面都加一个.reset_index()或者直接用groupby(user_id, as_indexFalse)能省下大量定位报错的时间。5. 可视化能用一张图讲清楚的事别做成十张5.1 中文字体与坐标轴密集两个最常见的坑Matplotlib 默认不出中文第一次跑出来的图全是方块。最简单粗暴的配置import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False而“横坐标太密集”这个问题很多人在画日粒度折线图时都会碰到一个月30个日期全部挤在一起睁大眼睛也看不清。有两种解法一是拉长画布尺寸二是控制刻度数量fig, ax plt.subplots(figsize(14, 6)) ax.plot(daily_sales[pay_date], daily_sales[sales], color#2E86AB, linewidth2) ax.xaxis.set_major_locator(plt.MaxNLocator(8)) # 最多显示8个刻度 plt.xticks(rotation45) plt.title(每日销售额趋势) plt.grid(alpha0.3) plt.tight_layout() plt.savefig(daily_sales.png, dpi150)5.2 趋势图标注事件比漂亮的线条更重要折线图如果只是在画一条线价值很低。电商数据里必然有大促、上新、周末高峰这些节点用axvline或者annotate把关键日期标出来读者一眼就能看懂哪一天为什么暴涨暴跌# 假设大促日是某一天 promo_date pd.Timestamp(2024-03-08) ax.axvline(promo_date, colorred, linestyle--, alpha0.7) ax.text(promo_date, ax.get_ylim()[1]*0.9, 3.8大促, colorred)5.3 类目占比条形图优先于饼图类目占比如果超过5个类目饼图基本就是灾难。我通常用条形图并且按数值大小排好序再画cat_summary df_sales.groupby(category)[payment_amount].sum().sort_values(ascendingFalse) ax cat_summary.plot(kindbarh, color#5B8FF9, figsize(10, 6)) for idx, value in enumerate(cat_summary.values): ax.text(value, idx, f{value/1e4:.1f}万, vacenter)5.4 图表的标题要写结论这是我反复跟团队成员强调的一点不要把标题写成“类目销售对比”要写成“手机数码类贡献54%销售额但退货率也最高”。图表本身不会说话标题就是它说话的方式。老板看报告只给两分钟标题写清楚结论能让他两秒抓住重点。6. 从数据到决策这次分析最终得出的结论和行动建议6.1 把上面所有分析串起来清洗、指标、拆解、可视化做完最后总要落到“然后呢”。我会输出一份精简的分析小结包含三个核心结论。结论一本季度销售额环比上涨12%但销售增长主要靠大促日拉动日常订单量基本持平。大促日销售占全季8%说明日常流量承接不足应该加强非活动日的内容运营。结论二客单价从148元降到122元同时复购率提升了3个百分点。这说明低价产品带动了复购但用户的单次消费力下滑了可能需要考虑捆绑套餐设计。结论三服饰类目退货率31%远高于全店均值17%。检查商品评价后确认是尺码不符为主因建议详情页增加尺码对照表并针对高退货SKU做品控抽检。每一个结论都必须是“数据 业务判断 行动点”的组合而不是单纯描述“某指标涨了、跌了”。6.2 把分析逻辑沉淀成可复用脚本这是我做数据分析项目的一个习惯。整个流程跑完后我会把从 CSV 读入、清洗、指标计算到图表输出的核心逻辑提炼成一个函数analyze_sales(file_path, window_start)参数化处理字段名和日期范围下个月、下个季度直接跑一遍def analyze_sales(file_path, window_start): df load_and_clean(file_path) metrics calc_core_metrics(df, window_start) charts make_charts(df, metrics) return metrics, charts # 下季度直接这样用 # metrics, charts analyze_sales(orders_q2.csv, 2024-04-01)6.3 一个被低估的环节数据解释权和数据标注最后提醒一个很容易被忽略的环节所有中间结果尽量保留一份to_csv备份。特别是清洗前后的数据进行对比这样同事来质疑“你销售额怎么跟后台导出对不上”时你能把中间表打开指给他看哪些退款单被剔除了。这比口头解释有效得多。还有就是每次分析报告开头写清楚口径比如“本报告销售额定义为已支付且未退款订单的实付金额合计数据来源店铺后台2024年1月1日至3月31日订单明细导出”。这样一份报告丢到群里谁都能看懂谁质疑也都有依据。