“ch4_2” 是我电脑里一个项目文件夹的名字,也是我最近在整理的一套数据分析实操项目的第四章第二节。如果你打开这个目录,会看到一堆 .py、.ipynb 和 .csv 文件,但它们其实对应一个完整的小项目:用 Python 对一份电商订单数据进行清洗、聚合分析,并产出可视化图表和结论。这节内容解决的核心问题,就是“拿到一份乱糟糟的表格,怎么把它变成能讲故事、能支撑决策的数据结果”。如果你正在学 Pandas,或者工作中经常要处理 Excel 导出的脏数据,这篇记录应该能给你一些能直接抄作业的思路。
这个项目本身没什么高深算法,但每一步都是实际干活时躲不开的细节,比如日期格式不统一、金额字段混着单位、同一个用户重复下单怎么算复购,这些琐碎问题才是数据分析里真正耗时间的地方。ch4_2 这个编号,对应我整理的一套实战课程的第四章第二节,前几章分别是环境搭建、Pandas 基础和数据可视化入门,而这一节就是把前面那些零散知识点串起来,做一次完整的数据处理演练。下面我按实际的推进顺序,把设计和踩坑过程都写出来。
1. 项目方向与整体设计思路
1.1 为什么叫 ch4_2,它在一个什么结构里
先解释一下这个编号的来历。我习惯把学习项目的代码按章节归档,每一章对应一类主题,每一节对应一个可独立运行的小案例。ch4_2 就是第四章的第二个案例,第四章的主题是“数据清洗与聚合分析”,ch4_1 做的是单表数据清洗,ch4_2 则把清洗和分析串成一条完整的流水线。这样命名的好处是,文件多了以后依然能快速定位知识点,比如想找“分组聚合”相关代码,直接进 ch4 目录翻就行,不用靠记忆去猜哪个文件名对应什么内容。
整个项目的工作目标也很简单:拿到一份模拟的电商订单明细表,里面有用户 ID、下单日期、商品类目、销售额、所在城市等字段,但数据质量比较差。需要做三件事:第一,把数据清洗到可以直接分析的状态;第二,按时间、城市、类目等维度做聚合统计,算出月度销售额、城市贡献、品类占比、复购率这些核心指标;第三,用图表把结果呈现出来,形成一份简短的分析小结。
这其实也对应了实际工作中最常见的需求:业务方丢给你一张导出表,问你“这个月哪个城市卖得最好”“复购用户占多少”,你不能直接拿原始表去回答,必须先理解字段、处理脏数据、再按业务口径计算。ch4_2 的价值就是把这条链路完完整整走一遍。
1.2 技术方案选型:为什么是 Python + Pandas
市面上能做数据清洗的工具很多,Excel 可以、SQL 也可以,但我最终选择 Python + Pandas,有几个很实际的理由。首先是字段处理的灵活性,Excel 处理几万行数据时就容易卡顿,而且在做“按用户 ID 去重后统计复购”这类逻辑时,公式写起来非常绕。SQL 虽然擅长聚合,但前提是数据已经在数据库里,而实际工作中我们拿到的往往是 CSV 或 Excel 文件,还得先导入,调试成本不低。
Pandas 正好卡在中间:它的 DataFrame 结构对表格操作非常直观,一行代码能完成筛选、去重、分组、合并;它不要求数据必须放在数据库里,本地文件直接读入就能开工。另外,从学习和复用的角度看,Pandas 的代码写一次就能长期使用,下次拿到相同结构的数据,只要改一下文件路径,其他逻辑基本不用动,这种可复用性很适合做个人分析工具箱。
我选的 Python 版本是 3.10,Pandas 版本 1.5.3,Matplotlib 3.6.2,Seaborn 0.12.2。这些版本搭配比较稳定,不建议一上来就追最新版,有些 API 变化会导致老代码报错。如果你是新手,直接装 Anaconda,里面的依赖基本是配好的,能少踩很多坑。
1.3 项目目录与处理流程设计
在动手写代码前,我先规划了一个清晰的目录结构,避免所有代码堆在一个文件里。ch4_2 文件夹下分成了 data、scripts、output 三个子目录:data 存放原始数据和清洗后的结果,scripts 按步骤拆分了三个脚本,output 放图表和分析小结。这样的好处是,运行完一轮之后,原始数据、中间产物、最终成果各归其位,复盘时不会手忙脚乱。
整个处理流程我分成了五个阶段:数据加载与初步探查、数据清洗、指标计算、可视化输出、结论整理。这五个阶段不是随手拍的,而是根据数据分析的基本逻辑来的——先要搞清楚“我手里有什么”,再解决“数据能不能用来分析”,然后是“能算什么”,最后才是“怎么展示、怎么说清楚”。很多人一上来就画图,结果画的都是脏数据,做出来的结论自然站不住脚。
流程上还有一个细节:每个阶段我都会单独设一个检查点,比如清洗完先 df.info() 和 df.describe() 看一眼,再进下一步。数据工作最怕的就是闷头跑完一条流水线,最后发现源头就错了,所以宁可多花几次打印的时间,也不要跳步。
2. 数据加载与清洗实操
2.1 原始数据结构与字段说明
项目用的是一份模拟电商订单数据,一共 8932 条记录,13 个字段。字段包括订单编号、下单时间、用户 ID、用户城市、商品类目、商品名称、单价、数量、实付金额、支付方式、订单状态、收货地址、备注。这份数据的生成逻辑参考了真实电商平台的订单结构,所以各种“意外”都非常典型。
刚拿到数据时,我先用 df.head() 和 df.info() 做初步探查,发现了几个明显问题:下单时间字段里既有“2024-03-15 13:22:45”这样的标准格式,也有“2024/3/5”这种只有日期的写法;实付金额里有 32 条记录的值是“-1”,明显是异常占位;用户城市字段存在“上海市”和“上海”两种写法;备注字段全是空值,基本没有分析价值;还有 11 条订单出现了完全重复的记录。这些还不是全部问题,更多细节是在清洗过程中逐渐暴露的。
这类脏数据在实际工作中很常见,因为它们来自不同业务系统、不同时期的录入规范差异。真正做项目的时候,第一步不是急着清洗,而是先建立“字段字典”,把每个字段的含义、类型、取值范围、是否有空值记录下来。这一步虽然费一点时间,但后面写聚合逻辑时会轻松很多,因为你需要依赖这些信息去判断一个结果是不是合理。
2.2 清洗规则与背后的判断依据
我把清洗拆成四个动作:去重、缺失处理、类型统一、异常值修正。每个动作都不是机械执行,而是要回答“为什么这么做”。
首先是去重。我用的判断标准是“订单编号 + 实付金额”两个字段同时相同才算重复,因为正常情况下同一个订单号可能出现多次支付记录,但金额不同代表的是不同事件。判断出来 11 条重复后,直接保留第一条、删掉其余。这里要特别提醒,不要用整行数据做去重,因为备注、收货地址这类字段只要有细微差异,整行去重就会失效。
其次是缺失值处理。备注字段接近 100% 空值,我直接选择删掉这一列,因为即使补全也没有业务意义。下单时间字段有 7 条记录缺失,但订单编号和用户 ID 都存在,所以我根据同一用户其他订单的平均下单间隔做了一次估算填充。说实话,这种填充方式在严谨的分析场景里有些冒险,但对于教学项目来说,它能展示“缺失值可以有多种处理策略”,而不是只用 dropna 或者 fillna 这一个动作。
第三是类型统一。我写了一个自定义函数,把日期格式统一成“YYYY-MM-DD”,同时把实付金额从字符串转成浮点数。这里有一个容易踩的坑:直接用 pd.to_datetime 处理混合格式时,Pandas 有可能把“2024/3/5”解析成 2024 年 3 月 5 日,也可能因为格式歧义产生错误,所以先规范化再转换会更稳。字符串转数字时,我还顺手去掉了金额里的“¥”和空格,因为原始数据里这两类符号都存在。
最后是异常值修正。实付金额为 -1 的 32 条记录,我核对了对应的单价和数量,发现用“单价 × 数量 × 折扣”可以还原出正确金额,于是做了修正,而不是删除。这个思路很关键:异常值不一定要删,先尝试还原业务口径。城市字段统一成去掉“市”后缀的标准名称,这样后面做分组时“上海”和“上海市”才能合并到一起。
2.3 关键清洗代码示例与运行结果
加载数据的代码很简单,重点是 read_csv 里要指定 encoding 参数,否则 Windows 环境下很容易出现中文乱码。我用的编码是“utf-8”,如果你的源文件是 Excel 另存的 CSV,可能需要改成“gbk”或“gb18030”,这个在排查篇里会单独讲。
import pandas as pd import numpy as np raw_df = pd.read_csv('data/raw_orders.csv', encoding='utf-8-sig') print(raw_df.shape) print(raw_df.info())去重和列删除是两步基础操作:
df = raw_df.drop_duplicates(subset=['订单编号', '实付金额'], keep='first') df = df.drop(columns=['备注'])日期清洗函数我建议大家直接收藏,因为混合日期格式这个问题太常见了。下面这个函数把常见的几种分隔符都做了处理:
def clean_date(s): if pd.isnull(s): return np.nan s = str(s).strip() s = s.replace('/', '-').replace('年', '-').replace('月', '-').replace('日', '') # 有的记录只有日期没有时间,统一补上 00:00:00 if len(s) < 19: s = s + ' 00:00:00' return pd.to_datetime(s, errors='coerce')跑完清洗后,我用一条 assertion 做校验,确保核心字段没有空值:
assert df['下单时间'].notnull().all() assert df['实付金额'].notnull().all()清洗后的数据从 8932 条变成了 8914 条,字段从 13 个变成 12 个,整体量级变化不大,但每一行数据的质量都可靠了。这个时候再往下做聚合,我心里是有底的。实际项目中我还喜欢多打印一次清洗前后各字段的空值数量对比,放在代码注释里,方便后面回溯。
3. 多维度聚合与核心指标计算
3.1 分析问题定义与拆解
清洗完成之后,接下来要回答几个具体的业务问题:整体销售趋势是怎样的,哪个月卖得最好;哪些城市贡献了绝大部分销售额;不同商品类目的销量结构如何;用户复购情况怎么样。这些问题不是随手列的,而是我在设计项目时根据“时间、地域、品类、用户”四个经典分析维度拆出来的。任何一份电商数据,基本都能从这四个角度找到突破口。
每个问题都对应不同的聚合方式和业务口径。比如“月度销售额”,口径是“按订单的下单时间月份分组,对实付金额求和”;“城市销售额”,则是“按城市分组求和后排序”。“复购率”稍微复杂一些,要先找出每个用户的下单次数,再统计下单次数大于等于 2 的用户占比。这里有一个容易搞错的点:复购率的分母应该是“有过成功下单行为的用户总数”,而不是“订单总数”,这两个口径算出来的数字差别非常大。
我在代码里用注释把这些口径写在对应的聚合语句上方。这样做不仅是给自己留记录,更重要的是让看代码的人知道“这个数字是怎么算出来的”。数据工作很多时候不是算法难,而是口径不一致,同一个指标两个人算出来不同,基本都能追溯到口径定义上。
3.2 groupby 聚合的实操细节
Pandas 的 groupby 用起来很顺手,但有一些细节会影响结果准确性。第一个细节是分组前要把用于分组的字段转换成合适的类型,比如“月份”最好单独提取成一个新的字符串列,而不是在 groupby 里对时间做复杂处理。我新增了df['月份'] = df['下单时间'].dt.strftime('%Y-%m'),这样后面按月份分组时就非常清爽。
月度销售额的代码如下:
monthly_sales = ( df.groupby('月份')['实付金额'] .sum() .reset_index() .sort_values('月份') )注意这里用了 reset_index,因为 groupby 之后月份会成为索引,如果不转回列,后续绘图时 x 轴数据会不好取。我早期就经常忘记这一步,结果图出来坐标轴莫名其妙。排序也别忘了,groupby 默认按索引排序,但你最好显式 sort_values,避免依赖默认行为。
城市维度的聚合类似,但我会在聚合后计算一下占比,方便判断头部城市是否有绝对主导地位:
city_sales = df.groupby('用户城市')['实付金额'].sum().reset_index() city_sales = city_sales.sort_values('实付金额', ascending=False) city_sales['占比'] = city_sales['实付金额'] / city_sales['实付金额'].sum()计算复购率时,我先生成一个“每个用户的订单数”中间 DataFrame,再在这个基础上做判断:
user_orders = df.groupby('用户ID')['订单编号'].count().reset_index() user_orders.columns = ['用户ID', '下单次数'] total_users = len(user_orders) repurchase_users = len(user_orders[user_orders['下单次数'] >= 2]) repurchase_rate = repurchase_users / total_users这个逻辑并不复杂,但却是最容易被写错的。如果你直接在原始订单上统计“下单次数大于 1 的订单数占比”,就会得到完全不同的、没有业务意义的数字。这其实就是聚合层级的问题,先搞清楚你要统计的主体是“用户”还是“订单”。
3.3 指标计算中的口径选择与校验
指标算完之后,不能直接拿去画图,要先做合理性校验。我用了几条简单的经验规则,比如各月销售额加起来要等于清洗后数据的实付金额总和;城市占比的合计要等于 100%;用户总数加上所有用户订单数,得到的平均客单价应该在合理范围内。这些校验本质上是把“总和”和“各部分之和”对账,适用于任何聚合分析。
这里我也特别想提一下“切比雪夫距离”式的数据敏感度:你在预览数据时觉得某个月销售额特别高,就要回去看那个月是不是有异常大额订单。我在做月度聚合时就发现 5 月销售额明显偏高,查了明细后发现有两笔金额超过 5 万元的订单,占比极大。这种极端值在现实业务中可能是大客户采购,也可能是录入错误,需要单独判断。我最后选择保留它们,但在分析小结里单独标注了“该月销售额受大额订单影响较大”这个提示。
对于复购率,我也做了分城市的交叉观察,发现一线城市的复购率明显高于新一线,而低线城市更多是单次订单。这个观察虽然只是描述性的,但它展示了一个思路:单一的总体指标往往不够,拆分到维度里才能看出结构差异。很多时候业务方问“复购率是多少”,其实更想知道“哪里高、哪里低、为什么”。
4. 可视化呈现与结论输出
4.1 图表选型逻辑
数据算完,下一步是画图。图表不是越多越好,而是要为每个问题选择最合适的表达方式。月度销售额本质是随时间变化的值,我用折线图,能同时看出趋势和波动;城市销售额是不同类别之间的比较,我用横向柱状图,因为城市名一多,纵向柱状图的标签会挤在一起;品类结构占比用饼图,但价格型占比我控制了品类数量,超过 8 个就把小众品类合并成“其他”,避免饼图碎成一大片。
这里想多说一句,很多人喜欢用饼图,但饼图只适合展示“部分与整体”的关系,而且分类不宜过多。像“各省份销售额都在总盘子里占多大比例”这种问题,饼图合适;但如果是“排名前十的城市分别卖了多少”,柱状图明显更好读。选图之前先问自己一句话:我想让读者一眼看到什么?
4.2 Matplotlib 绘图代码与细节调整
用 Matplotlib 画图时,有两个中文字体问题需要处理,否则图里的中文全是方框。第一是设置字体,我用的是 SimHei;第二是确保坐标轴负号正常显示。这两行配置我写了一个统一的样式脚本,放在 scripts 目录下,每次画图前先运行它。
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False月度销售额折线图的代码大概长这样:
fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(monthly_sales['月份'], monthly_sales['实付金额'], marker='o', linewidth=2) ax.set_title('月度销售额趋势') ax.set_xlabel('月份') ax.set_ylabel('销售额(元)') plt.xticks(rotation=45) plt.tight_layout() plt.savefig('output/monthly_sales.png', dpi=150) plt.show()这里有个小细节:xticks 旋转 45 度,是为了防止月份标签叠在一起。tight_layout 可以自动调整空白边距,避免标题被截掉。savefig 时设置 dpi 不低于 150,这样插入文档或 PPT 时图片不会模糊。
城市销售额柱状图我用的是 seaborn 的 barplot,因为它自带的配色和样式会好看一些,不需要额外调很多参数:
import seaborn as sns plt.figure(figsize=(10, 6)) sns.barplot(data=city_sales.head(10), x='实付金额', y='用户城市', palette='Blues_d') plt.title('销售额前十城市') plt.tight_layout() plt.savefig('output/top10_city_sales.png', dpi=150)在画完所有图之后,我还会统一打印一次每个统计结果的前几行,把数字和图表放在一起做交叉确认。这样做看似多余,其实非常有价值,因为图是视觉判断,数字是精确判断,两个人看图得出的结论可能不同,但看数字不会有歧义。
4.3 分析小结的整理方式
项目产出不只是一堆图和代码,还需要一段能直接给人看的小结。我会在 output 目录里放一个 Markdown 文件,里面包含三块内容:数据概况、核心结论、附注与局限。数据概况记录清洗前后记录数、字段数、时间范围;核心结论对应前面拆解的四个问题,每一条都用“数据 + 结论”的句式;附注与局限则标注极端值影响、估算填充的字段等。
举个例子,数据概况部分写“原始数据 8932 条,清洗后 8914 条,时间范围 2024-01 至 2024-06”,核心结论部分写“5 月销售额最高,达 68.3 万元,但其中两笔订单金额占比超 15%;上海、北京、广州合计贡献约 42% 的销售额;食品类目销量占比最高,达 34%;整体复购率为 23.6%,其中一线城市复购率约 31%,四线城市约 15%”。这种写法后续做汇报时基本可以直接复用。
我还养成了一个习惯:把每次清洗和分析中的关键决策记录在一个“决策日志”里,哪怕只是三五行字。比如“为什么缺失时间用均值填充”“为什么保留 5 月极端大额订单”,这样过了半年再回看项目,你还能想起来当时的业务背景。
5. 排查记录与避坑指南
5.1 高频问题速查表
做这类项目时,我遇到的报错和奇怪结果非常多,这里整理一个速查表,按出现频率排序:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 读取 CSV 中文乱码 | 文件编码与 read_csv 指定编码不一致 | 改用 encoding='utf-8-sig' 或 'gbk',也可以先打开文件看编码 |
| to_datetime 解析错误或警告 | 日期字符串混用“/”“年月日”等格式 | 先统一格式,再用 errors='coerce',最后检查 NaT 行 |
| 聚合结果索引混乱 | groupby 后没有 reset_index | 统一在聚合后 reset_index,养成习惯 |
| 图表中文显示为方框 | 缺少中文字体配置 | 设置 plt.rcParams['font.sans-serif'] = ['SimHei'] |
| 聚合后的数值比预期大很多 | 存在重复记录或异常值未处理 | 回溯到清洗步骤,检查去重逻辑和异常值判断 |
| 单列求和时提示类型错误 | 有字符串混在数字列里 | 用 pd.to_numeric(..., errors='coerce') 做强制转换 |
这张表后面我会持续补充,遇到新问题就往上加。你会发现很多报错其实是同一类问题反复出现,比如日期格式和编码,处理过一次之后就熟练了。
5.2 实际踩过的几个坑
第一个坑是 Windows 系统下文件路径的编码问题。有一次我在脚本里写df = pd.read_csv('data/raw_orders.csv'),本地跑没问题,但换了一台电脑后文件路径带中文,直接抛 UnicodeDecodeError。后来我统一用Path模块管理路径,并养成了在 read_csv 里显式写 encoding 的习惯。
第二个坑是 groupby 完发现销售额比原始总和少了近十万。排查了很久,发现是清洗时把“实付金额”为 -1 的异常值直接过滤掉了,而后来又做了一次去重,把一些真实的大额订单也误伤了。这个问题给我的教训是:每一步清洗都会影响后续结果,每一步操作都要谨慎,并且要保存“清洗前备份”和“清洗后结果”两份数据,方便做对比。
第三个坑和复购率有关。我第一次算复购率时用了df[df['下单次数'] > 1]的订单数去除总订单数,结果算出复购率只有 8%,后来想起口径问题改成“用户数”做分母,才变成 23.6%。数字差了将近三倍,但很多初级分析都会掉进这个坑。所以每算一个指标,我都会在注释里写明分子是什么、分母是什么、代表什么业务含义。
第四个坑是图表保存后不完整,月度折线图保存出来 x 轴最后一个月份标签被切掉一半。这是因为保存时用的默认 bbox_inches 参数。解决方法是 savefig 时加上bbox_inches='tight',这个参数能自动调整画布边界,保证保存出来的图片完整。
5.3 内存与性能优化的小技巧
数据量在近万条时,Pandas 跑这些操作几乎零延迟,但真实工作里数据量很容易到几百万行,所以从一开始就要注意性能习惯。我常用的技巧有这么几个:读取时只加载需要的列,用usecols参数;字符串列用category类型,能大幅降低内存占用;尽量用向量化操作而不是 for 循环遍历行;聚合时先过滤再 groupby,减少参与分组的数据量。
比如原始数据有 13 列,但我们的分析只用其中 6 列,那我就会这样读取:
df = pd.read_csv('data/raw_orders.csv', usecols=['订单编号', '下单时间', '用户ID', '用户城市', '商品类目', '实付金额'], encoding='utf-8-sig')这类知识在 ch4_2 的正文里没有展开讲,但实际项目里非常重要。等到数据量大了再想优化就晚了,那时候每跑一步都要等很久,非常折磨人。
6. 项目复盘与可复用资产
6.1 从 ch4_2 沉淀出来的东西
这个项目做完,我最大的收获不是代码本身,而是把一套数据分析流程固化了下来。现在拿到任何一张表,我几乎不用思考就按这个流程走:先探查、再清洗、然后聚合、最后可视化。这个顺序的好处是每一个阶段都有明确产出,不容易乱。
更实用的是,我把清洗和聚合的常用代码整理成了一个自定义模块,里面封装了日期清洗、编码识别、去重、异常值修正这些函数。以后遇到新项目,直接import data_clean_utils就能调用,不用重写一遍。这个做法非常推荐,尤其是同一类业务数据会反复收集时,一次封装、长期受益。
我还把这次分析形成的图表和结论做成了一个 PDF 文件,方便放到个人作品集里。做数据相关岗位的朋友都知道,作品集里放“完整的数据分析项目”比放零散的代码更有说服力,因为它展示的是你从拿到脏数据到形成结论的完整能力。
6.2 如何扩展这个项目
如果你想在这个基础上继续深入,有几个方向可以参考。第一个方向是增加预测,利用前六个月的数据训练一个简单的回归或时间序列模型,预测后三个月的销售额,这能把项目从“描述性分析”升级成“预测性分析”。第二个方向是增加用户分层,用 RFM 模型把用户分成高价值、流失风险、新客等群体,这会引入更多业务分析思维。第三个方向是换一份真实数据,比如一些开源平台上的电商数据集,数据量更大、噪声更多,能进一步检验你的清洗逻辑是否健壮。
我自己的经验是,做完一个项目后不要急着做下一个,先花一天时间把这些扩展空间想一遍,哪怕只做其中一个方向,都比重复做十个同类型项目更有成长。
最后再分享一个小技巧:数据分析项目里的“过程文档”和最终结果一样重要。我会在 scripts 目录里留下一个 README,记录每段脚本的运行顺序、数据路径、预期产出物。这样三个月后想重新跑一遍,照着 README 点一遍就能复现,不会因为忘了某一步而卡住。ch4_2 现在对我来说,就是一个能随时复现、能随手扩展、也能拿去展示的完整样例。