做数据分析类项目这些年,我最大的感触是:真正决定一个电商数据分析项目成败的,往往不是算法有多先进、图表多花哨,而是你能不能把手头那堆乱糟糟的销售流水整理成一张能回答业务问题的干净表。尤其是用Python处理电商销售数据,pandas这几行代码人人都能跑通,但不同人跑出来的结论价值天差地别——差距就藏在数据清洗的细节里、藏在业务指标的拆解逻辑里、藏在"为什么这么算"的思考里。
这篇博文就从一个真实的场景出发:你拿到了一份某电商平台的销售订单数据,要完成从环境搭建、数据清洗到核心指标分析和可视化的完整流程。我会把每一步背后为什么要这么做、踩过哪些坑、怎么避免,全部摊开来聊。无论你是刚装好Python准备入门的小白,还是已经会跑pandas但总觉得分析思路不成体系的朋友,这份实战记录都能帮你少走不少弯路。
1. 为什么先想清楚"卖什么分析",而不是急着写代码
很多人拿到数据的第一反应是打开Jupyter Notebook,import pandas as pd,然后df.head()看一眼就不知道下一步干嘛了。这个状态我太熟悉了,因为我自己也是从这步过来的。但后来我发现,先花10分钟想清楚业务目标,比后面省下的1小时还值。
1.1 电商数据分析到底在回答哪些问题
电商销售数据分析绕来绕去,本质上回答的就这几类问题:
- 卖了多少钱:整体GMV(成交总额)、销售额的日/周/月走势,哪些时间段是高峰,哪些是低谷。
- 什么东西好卖:哪些商品或品类贡献了主要收入,哪些是滞销品,需不需要调整库存。
- 谁在买:客户的地域分布、新老客占比、复购情况,谁是你的核心人群。
- 卖得健不健康:有没有异常订单、退货率多高、客单价是否合理。
这四类问题对应到订单表上,就是不同的字段组合和聚合方式。你在动手清洗之前,脑子里最好先有一张"问题-字段-操作"的对应表。
| 业务问题 | 需要的字段 | 核心操作 |
|---|---|---|
| 整体销售趋势 | 订单日期、销售额 | 按日期分组求和 |
| 品类贡献度 | 商品类目、销售额 | 按类目分组,算占比 |
| 客户复购情况 | 客户ID、订单日期 | 去重后统计购买次数 |
| 地域分布 | 收货省份、订单量 | 按省份分组计数 |
这个表不用写得特别正式,但心里要有数。否则你就会陷入"天天在处理数据,却不知道自己在找什么答案"的泥潭。
1.2 这份实战项目的数据长什么样
我们假设拿到的是一份包含这些字段的销售订单表:
order_id:订单编号order_date:下单日期user_id:用户IDproduct_id:商品IDcategory:商品类目quantity:购买数量unit_price:单价total_amount:订单金额province:收货省份
实际的表会比这个脏得多,比如日期有各种格式、金额字段里混着人民币符号、同一用户因为换手机号产生了多个ID。但不管原始数据多乱,你的目标是把它们都规整成上面这种"一行一个可分析单元"的结构。
这里我强烈建议:拿到数据后,先别做任何处理,直接打开原始文件翻一遍前20行和后20行。这样你对数据质量会有一个直观感受,后面写清洗代码的时候,你才知道要防哪些坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分析环境的搭建顺序:先把坑埋在Python环境这一步
这部分写给刚入门的朋友。我知道很多人是看了一篇教程就决定学Python数据分析的,第一步就是装环境。装环境看起来简单,其实最容易埋雷。
2.1 数据分析到底需要装哪些库
Python数据分析最核心的库,其实就这几个:
- pandas:表格数据处理,主角中的主角。
- numpy:数值计算底层库,pandas依赖它。
- matplotlib:基础可视化。
- openpyxl:处理Excel文件的引擎,pandas写Excel需要它。
有人会问,不装Anaconda直接装官方Python行不行?完全行。我自己现在就是Windows上用官方Python + VSCode,需要什么库就pip install什么。Anaconda的好处是帮你一次性打包装了上百个库,简单省事,但缺点也很明显:环境臃肿、装包容易冲突。对于电商数据这种分析场景,我建议用官方Python,按需安装,你的环境干净,后面排查问题会容易得多。
2.2 pip安装的加速办法和版本兼容问题
在国内用pip装包,那个下载速度真的让人抓狂。一个pandas才几MB还好,但要装torch那种几百MB的包,默认源能卡到你怀疑人生。解决办法是通过-i参数指定清华源或阿里源:
bash复制pip install pandas numpy matplotlib openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple
如果你希望以后都不用手动加源,可以用下面的命令把清华源设置为默认:
bash复制pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
设置完再直接pip install就行,速度天壤之别。
装包的时候还有一个高频坑:版本冲突。比如你项目里有人用了旧版pandas写的代码,你新装的是pandas 2.x,某些API行为变了,代码跑出来结果不一样。所以我的习惯是装完库以后,在Python环境里执行一下:
python复制import pandas as pd
print(pd.__version__)
快速确认版本,心里有数。做数据分析这种偏稳定的活,不建议追求最新版本,够用、稳定就是最好的。
2.3 VSCode配置Python环境的关键一步
近几年很多人用VSCode写Python,但配置环境的时候常遇到一个问题:在终端里python --version明明显示3.11,可跑代码时右下角解释器却是另一个版本,或者装了库还是提示ModuleNotFoundError。这个问题的根源是VSCode没有正确选择解释器。
解决办法是:
- 打开VSCode,按
Ctrl+Shift+P打开命令面板。 - 输入"Python: Select Interpreter"。
- 选择你安装Python的那个解释器路径。
- 在终端里运行
python -m pip list,确认你要用的库已经装在这个解释器对应的环境里。
记住一个关键点:VSCode里运行的Python环境 = 解释器选择的那个环境,跟你系统PATH里默认的python可能不是同一个。凡是遇到"装好了却找不到包"的问题,优先检查这一步。
3. 拿到原始销售表之后,第一件事不是算销售额
数据清洗是整个分析过程中最耗时间也最体现功力的环节。很多时候80%的时间都在处理数据质量问题,真正写分析代码反而很快。这一节我就把清洗环节的核心步骤和最容易踩的坑讲透。
3.1 导入数据后,先看结构再动手
拿到数据文件后(假设是CSV),第一步代码是这样的:
python复制import pandas as pd
df = pd.read_csv("sales_data.csv", encoding="utf-8")
print(df.shape)
print(df.dtypes)
print(df.head(10))
df.shape告诉你这个表有多少行多少列,dtypes告诉你每列的数据类型,head(10)让你快速浏览前10行。这三行看完,你对数据的整体情况就有感觉了。
这里有个很常见的编码问题:CSV文件如果是GBK编码(国内Excel导出的文件很常见),encoding="utf-8"会直接报UnicodeDecodeError。遇到这个错,改成:
python复制df = pd.read_csv("sales_data.csv", encoding="gbk")
为了保险,可以加一个errors="ignore"参数先读进来看看,但不建议长期依赖它,因为忽略错误可能意味着部分乱码。
3.2 日期字段的统一:文本、时间戳和日期索引
电商订单日期经常是字符串,比如"2024/1/5"、"2024-01-05"、"2024年1月5日"混在一起。不统一的话,后面按时间聚合就会出乱子。
统一方法是用pd.to_datetime():
python复制df["order_date"] = pd.to_datetime(df["order_date"])
print(df["order_date"].dtype) # datetime64[ns]
统一成datetime类型之后,你才能做这些事:按月份提取df["order_date"].dt.to_period("M")、按星期几统计销售、计算两个日期之间的间隔等。
这里有一个小技巧:如果你的日期数据是从Excel里读出来的,有时候会显示成数字(比如45292),这是因为Excel把日期存成了序列号。pd.to_datetime()能直接识别不少情况,但如果遇到大量解析失败,可以用pd.to_datetime(df["order_date"], origin="1899-12-30", unit="D")这种指定origin的方式处理,Excel日期序列号对应的起点就是这个值。
3.3 金额字段里的隐藏脏数据:符号、空格和文本
金额字段是最容易出现隐性问题的地方。我见过一份表,金额列长这样:"¥1,299.00"、"1,299.00 "(末尾带空格)、"1299"、"#N/A"。直接用sum()求和会直接报错或得到0,因为这一列还是object类型。
清洗逻辑分两步:
python复制# 第一步:去掉人民币符号和千分位逗号
df["total_amount"] = df["total_amount"].astype(str).str.replace("¥", "")
df["total_amount"] = df["total_amount"].str.replace(",", "")
# 第二步:转成数值类型,无法转换的变成NaN
df["total_amount"] = pd.to_numeric(df["total_amount"], errors="coerce")
# 第三步:看看到底哪些行变成了NaN,决定是丢弃还是补全
print(df[df["total_amount"].isna()])
errors="coerce"这个参数非常关键:解析失败的值不会让程序崩溃,而是变成NaN。然后你再单独去看这些NaN对应的行,判断它们是退货单、测试单还是单纯的乱码,再决定处理方式。这种"先放行再检查"的思路,比用try...except去硬处理要高效得多。
3.4 重复订单和彻底缺失的行:舍得删除
重复订单是电商数据里躲不开的问题。用户点击两次提交按钮、页面刷新导致重复回调,都可能产生重复订单。去重时要以order_id为唯一键:
python复制print("去重前行数:", df.shape[0])
df = df.drop_duplicates(subset=["order_id"], keep="first")
print("去重后行数:", df.shape[0])
keep="first"表示保留重复订单中的第一条。但这里要注意一个业务细节:如果重复订单的金额、数量字段有差异(有时候一条是初始订单,一条是修改后的订单),你还需要判断该保留哪一条,不能机械地保留第一条。
至于那些关键字段(如total_amount、order_date)全是NaN的行,处理方法就一个——删除。因为这样的行对任何分析都没贡献,留着反而会干扰聚合计算。
python复制df = df.dropna(subset=["total_amount", "order_date"])
有些教程告诉你用dropna()无差别删除所有含空值的行,这是有风险的。结合业务场景看,province为空可能还能接受,total_amount为空就真的没法分析了。所以subset参数一定要指定要检查的字段。
3.5 异常值识别:数量是负数怎么办,单价高得离谱怎么办
洗完基础数据后,还要做一步异常值检查。电商场景下最典型的异常就是负数金额和负数量。这个可能是退款订单、也可能是数据采集时把"已退款的金额"直接减掉了。如果只是想分析"销售额",退款订单本身也有分析价值,但你要么单独拎出来看,要么明确排除,不能混在一起。
检查的方式是用描述性统计:
python复制print(df[["quantity", "unit_price", "total_amount"]].describe())
describe()会输出每列的计数、均值、标准差、最小值、25%、50%、75%和最大值。一眼扫过去,如果quantity的最小值是-5,或者unit_price的最大值是999999,就要去查这几行数据了。
还有一个经验:不要轻易删除你认为是异常值的记录。先看业务上是否合理——一件商品卖2500可能正常,卖250000大概率是测试单或录入错误。对这种行,我的习惯是先筛选出来看一眼再决定,而不是用一行代码全部删掉。
4. 核心分析四板斧:从GMV到复购率的完整拆解
数据清洗完毕,下面进入最出成果的部分——分析。这一节我按前面提到的四类业务问题,给出具体的代码实现和业务解读。
4.1 整体销售趋势:先看大盘,再看波动
分析销售额趋势之前,要先定义一个基本口径:你算的是订单金额的总和,还是扣除退款后的净额?我建议先按总额算一遍,单独标注退款情况,这样心里有数。
按月份聚合的代码:
python复制df["month"] = df["order_date"].dt.to_period("M")
monthly_sales = df.groupby("month")["total_amount"].sum().reset_index()
print(monthly_sales)
这里有个细节:groupby之后返回的是Series,我习惯用reset_index()把它变回DataFrame,方便后续处理。如果不加这一步,后面想新增一列"环比增长率"就会有点别扭。
"环比增长率"的计算是电商分析里的必需品:
python复制monthly_sales["环比增长率"] = monthly_sales["total_amount"].pct_change() * 100
pct_change()会计算当前行相对于上一行的百分比变化。比如12月比11月增长了15%,这个指标对于判断年底大促有没有真正拉动销售非常直观。
做到这一步,你会遇到一个很常见的坑:月份排序乱了。因为to_period("M")返回的是Period类型,直接排序可能不是按时间顺序,而是按字符串顺序排(比如"2024-10"排在"2024-9"前面)。解决办法是用sort_values并指定排序函数,或者干脆把Period转成字符串格式再排序:
python复制monthly_sales["month"] = monthly_sales["month"].astype(str)
monthly_sales = monthly_sales.sort_values("month")
只要保证"2024-09"这种带前导零的格式,字符串排序就等于时间排序。这也是为什么规范日期格式非常重要的原因。
4.2 品类贡献度:用帕累托分析找出核心品类
电商运营里有个经典的"二八法则":20%的商品贡献80%的销售额。用pandas算品类贡献度非常直接:
python复制category_stats = df.groupby("category")["total_amount"].sum().sort_values(ascending=False).reset_index()
category_stats["累计占比"] = category_stats["total_amount"].cumsum() / category_stats["total_amount"].sum() * 100
print(category_stats)
cumsum()是累计求和,除以总额再乘100就得到累计占比。你一眼就能看出哪个类目贡献了前50%的收入,哪些类目是长尾。
拿到这个结果之后,业务动作是什么?对于贡献大、利润率高的核心品类,运营资源要倾斜;对于销售额和利润都很低的边角品类,要考虑是否清仓或淘汰。分析的价值不在于算出数字,而在于你拿数字去指导决策。
4.3 客户复购分析:从订单流水中提炼用户的购买习惯
订单表是流水,一行一个订单,但"复购"是一个用户维度的问题,所以需要从订单表转换到用户表。这是数据分析里非常典型的一次"表结构转换"。
python复制user_purchase = df.groupby("user_id")["order_date"].agg(["count", "min", "max"]).reset_index()
user_purchase.columns = ["user_id", "购买次数", "首次购买日期", "最近购买日期"]
print(user_purchase.head())
拿到每个用户的购买次数后,可以算出复购率。复购率有两个口径,我建议先按最简单的口径算:在所有购买过的用户中,购买次数≥2的用户占比。
python复制repurchase_rate = (user_purchase["购买次数"] >= 2).mean() * 100
print(f"复购率: {repurchase_rate:.2f}%")
注意(user_purchase["购买次数"] >= 2)这个布尔Series直接求mean()的技巧:pandas会把True当作1、False当作0,所以均值就是占比。少写好几行代码。
如果你想做更深入的RFM分析(Recency、Frequency、Monetary),这里的数据已经够了:
python复制import datetime
current_date = df["order_date"].max() + pd.Timedelta(days=1)
rfm = df.groupby("user_id").agg(
R=("order_date", lambda x: (current_date - x.max()).days),
F=("order_date", "count"),
M=("total_amount", "sum")
).reset_index()
R代表最近购买距离现在的天数,数字越小越活跃;F是购买频次;M是累计消费金额。三个指标分别算三分位数,就能给用户分层:高价值活跃用户、流失边缘用户、新用户等等。这个模型虽然简单,但在没有专门数据团队的小电商公司里非常实用。
4.4 地域分布:按省份聚合并识别核心市场
地域分析主要看两个指标:订单量和销售额。用省份分组:
python复制province_stats = df.groupby("province")["total_amount"].sum().sort_values(ascending=False).reset_index()
top10_provinces = province_stats.head(10)
print(top10_provinces)
这里有一个值得做的延伸:结合平均客单价看省份差异。有些省份订单量不大但客单价很高,说明当地消费者偏高端,适合主推利润款;有些省份订单量大但客单价低,更适合做走量款和满减活动。
python复制province_stats["客单价"] = province_stats["total_amount"] / df.groupby("province")["total_amount"].count().sort_values(ascending=False).reset_index()["total_amount"]
(如果你不想写得那么绕,也可以用分组agg一步到位:分组列同时算sum和count,然后手动相除。)
5. 可视化输出:让老板一眼就看懂的三种图
分析结果出来以后,光给一堆表格是没人看的。真正的项目报告需要图来辅助说明。matplotlib是Python里最基础、兼容性最好的绘图库,这里我就拿它演示。
5.1 折线图:销售趋势的第一选择
折线图最适合展示时间序列的趋势变化:
python复制import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文乱码
plt.rcParams["axes.unicode_minus"] = False # 解决负号显示异常
plt.figure(figsize=(12, 6))
plt.plot(monthly_sales["month"].astype(str), monthly_sales["total_amount"], marker="o")
plt.title("月度销售额趋势")
plt.xlabel("月份")
plt.ylabel("销售额")
plt.xticks(rotation=45)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("monthly_sales.png", dpi=150)
plt.show()
有个细节:plt.rcParams["font.sans-serif"] = ["SimHei"]这一行是必须的,否则图里的中文会显示成方框。如果你在Mac上,改成["Arial Unicode MS"]或["PingFang SC"]。
5.2 柱状图:品类贡献和地域差异
品类对比用柱状图最清晰。如果是排名类数据,我习惯用水平柱状图(barh),类别名称好读不打架:
python复制plt.figure(figsize=(10, 8))
plt.barh(top10_provinces["province"], top10_provinces["total_amount"])
plt.title("销售额Top10省份")
plt.xlabel("销售额")
plt.gca().invert_yaxis() # 让最大的在顶部
plt.tight_layout()
plt.savefig("province_sales.png", dpi=150)
plt.show()
在柱状图上方标注数值,也是个很讨喜的小动作:
python复制for i, v in enumerate(top10_provinces["total_amount"]):
plt.text(v, i, f"{v:,.0f}", va="center", fontsize=9)
5.3 占比可视化:饼图不是唯一选择
很多人一看到占比就想画饼图。但饼图在类别超过5个时很难读,人眼对"角度"的感知不如对"长度"的感知敏锐。我的建议是:5个以内用饼图没问题,5个以上用横向条形图或者帕累托图更好。如果你一定要用饼图,加上百分比标签:
python复制plt.figure(figsize=(8, 8))
plt.pie(category_stats["total_amount"][:5], labels=category_stats["category"][:5], autopct="%.1f%%")
plt.title("Top5品类销售占比")
plt.tight_layout()
plt.savefig("category_pie.png", dpi=150)
plt.show()
6. 跑数据时最容易翻车的几个细节
最后单独开一篇写我在实际运行中反复踩过的坑。这些坑不一定写在教科书里,但每一个都真实浪费过我的时间。
6.1 groupby之后为什么数据"少了一列"
新手最常见的困惑:df.groupby("month")["total_amount"].sum()得到的结果里,month列好像变成了索引而不是普通列。打印出来的时候看起来像:
text复制month
2024-01 10000
2024-02 12000
这是Series,不是DataFrame。如果你下一步想把结果和另一个表合并,或者想按索引排序,就容易出问题。所以我习惯每次都加reset_index(),强制把索引恢复成列:
python复制monthly_sales = df.groupby("month")["total_amount"].sum().reset_index()
这样month就是一个实实在在的列,后面用merge、sort_values都不会出幺蛾子。
6.2 matplotlib中文乱码的三种情况
中文乱码这个问题,每次新换一台机器都会遇到。我遇到过三种情况:
- 图上中文显示成方框或乱码 → 字体没设置对,用
plt.rcParams["font.sans-serif"] = ["SimHei"]。 - 设置了字体还是乱码 → 系统里根本没有这个字体,需要安装字体或换用系统已有的字体。
- 标签和标题正常,但图例是方块 → 图例的字体设置没跟上,可以在绘制前统一设置
plt.rcParams。
有一个更彻底的办法:把它写进一个配置文件,以后每个项目开头直接exec(open("plt_config.py").read()),一次性解决中文字体和负号问题。这属于我个人的小习惯,但确实省事。
6.3 pd.to_numeric不会自动改变原列
这点太容易忽略了。很多人写完pd.to_numeric(df["total_amount"], errors="coerce"),马上跟着print(df["total_amount"].dtype),发现还是object,就觉得代码白写了。原因是pd.to_numeric返回的是新Series,不会原地修改原列。你必须要么重新赋值:
python复制df["total_amount"] = pd.to_numeric(df["total_amount"], errors="coerce")
要么用df["total_amount"] = df["total_amount"].astype(float)(前提是已经确定格式干净)。
6.4 用plot出图时,索引会成为横轴
还有一个高频问题:df["total_amount"].plot()出来的图,横轴不是0、1、2这种默认索引,就是DataFrame的索引。如果你没有reset_index(),而这个索引恰好是按日期排的,横轴就会直接显示日期,看起来像"自动识别了时间"。这有时是好事,有时也会误导你——比如当你groupby之后索引变成类目的名字,画出来的横轴就是类目名。
掌握这个规律后,画图之前你会习惯性地想清楚:当前DataFrame的索引到底是什么。想不清楚就先reset_index(),保证横轴是你想要的列。
说实话,这套流程我做过不止一次,每次数据源不一样,但处理思路几乎完全一样:先看结构、清洗脏数据、统一字段格式、按业务问题分组统计、最后可视化讲出故事。你说它难吗?代码层面真的不难。难的是在每个环节你都清楚自己在干什么、为什么这么干。尤其是清洗环节,表面上最枯燥,实际上最决定分析质量。数据没洗干净,后面画出来的图再好看,结论也是站不住脚的。
如果你正准备开始做自己的第一个电商数据分析项目,我的建议是:别一上来就找什么Kaggle大数据集,先用自己手头能拿到的小数据(哪怕几百行)跑通整个流程。数据量小反而让你有精力去关注每一步的细节,等你完整跑过一遍,后面遇到百万行数据也不会慌,无非是换一种读取方式和优化点,核心分析逻辑完全一致。
