说实话,拿到作业3.7这个编号的时候,我第一反应是“课程又留常规练习了”,结果把题目完整读了三遍才发现,这道题表面上是Python数据处理练习,实际把数据清洗、字段理解、分组聚合、可视化分析全揉在了一起。更关键的是,它要求的不是“跑通代码”,而是“讲清楚每一步为什么这么做”,这就完全不是抄一段Pandas代码能交差的了。
这道作业的场景非常典型:一份电商订单CSV,约一万行,字段包含订单号、用户ID、下单时间、订单金额、商品类别、支付状态等。任务分四问:按类别统计销售额排行、分析月度销售趋势、计算支付状态占比、找出金额异常的订单并给出排查结论。看起来每问都很基础,但真正动手做的时候,你会发现坑全藏在数据本身的脏乱差里。
这篇文章我就完整复盘一下我当时是怎么拆解任务、怎么写代码、在哪几个地方差点翻车,以及最后从作业里沉淀下来的通用处理套路。不管你是在上网课、做训练营作业,还是工作中第一次接手数据清洗的活儿,这套思路应该都能直接拿来用。
1. 作业任务拆解与整体设计思路
1.1 先把“表面需求”翻译成“技术动作”
我拿到题的第一件事不是打开IDE,而是拿笔在纸上把这四问翻译成具体的技术操作。这个过程非常关键,因为题目里的“分析销售额排行”到了代码层面其实是“先做类别分组,再对金额字段聚合求和,最后排序”这么一串动作。如果一上来就写代码,很容易写着写着发现漏掉了某个隐含条件。
我当时拆出来的技术动作是这样的:
- 统计各品类销售额与订单量排行,对应
groupby('类别')后分别对订单金额做sum、对订单号做count,再排序。 - 分析月度销售趋势,对应先把下单时间转成标准日期格式,再提取
year-month,按月分组求和。 - 计算支付状态占比,对应
value_counts(normalize=True)。 - 找金额异常的订单,对应要做描述性统计,结合
quantile或z-score来判断离群点。
这么一拆就发现,整道题其实在训练三个核心能力:字段理解、数据清洗、聚合口径的选择。多数人卡住的地方根本不在分组聚合本身,而是前面数据没洗干净,或者时间字段解析出了错,导致后面所有结果全偏。
1.2 为什么选Pandas而不是Excel或SQL
有些同学习惯用Excel做这类分析,因为点几下鼠标就能出透视表,但作业的隐性要求是“写出可复现的处理流程”,这意味着每一步操作都得有记录。Excel在数据量小的时候确实方便,但一涉及时间格式清洗、异常值筛查这种需要写逻辑的操作,它就变得很笨拙,而且很难追溯过程。
SQL当然也能做分组聚合,但这类课程作业通常更希望你掌握DataFrame的处理方式,因为后续做机器学习特征工程时,Pandas是绕不开的。从我的习惯来说,这类“一撮数据、四问分析、需要反复看中间结果”的任务,Pandas的DataFrame对象是最顺手的工具,每一行代码都是一个可独立验证的步骤,中间结果随时能head()出来检查,出问题时定位非常快。
另外还有一个关键考量:Pandas处理一万行的数据几乎无延迟,可以高频地“写完一步、跑一步、看一眼结果”,这种交互感能让你及时发现数据质量问题。而SQL虽然也能做,但改一次口径就要重写一段查询,不如DataFrame里直接新增一列来得直观。
2. 数据读取与结构探查:动手写代码前必须干的三件事
2.1 环境准备与数据导入
我先说明一下我这边的运行环境:Python 3.10 + Pandas 2.0 + Matplotlib 3.7,Jupyter Notebook里跑的。之所以用Notebook而不是直接写.py脚本,是因为作业本身分四个问题,Notebook的单元格天然适合分段执行、分段验证结果。
导入阶段建议直接写这两行:
import pandas as pd import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文乱码 plt.rcParams['axes.unicode_minus'] = False # 解决负号显示异常这两行配置是做中文可视化的标配,不写的话图表里的中文会全部变成方框。第一次运行时我就吃过这个亏,导出的图里“销售额”三个字全变成了乱码,白白浪费了十分钟。如果你用的不是Windows系统,SimHei这个字体可能不存在,那就得换成你系统里实际的中文字体,比如Arial Unicode MS或WenQuanYi Zen Hei。
接着读取数据:
df = pd.read_csv('orders.csv') print(df.shape) print(df.columns.tolist()) print(df.head(10).to_string())读进来之后第一步永远先看形状和列名。我当时看到的是(10485, 7),这个行数比题目说的一万行略多,说明数据里很可能混进了脏数据。列名分别是:订单号、用户ID、下单时间、订单金额、商品类别、支付状态、收货城市。这个字段结构很典型,接下来的所有操作都会围绕这些列展开。
2.2 字段类型与数据质量初检:一切统计的口径前提
用df.info()看字段类型这一步,看似普通,其实价值极高。我当时的运行结果里有两个非常关键的发现:
下单时间是object类型,这是正常的,因为原始CSV里的时间就是字符串,但后续做趋势分析必须转成datetime64。订单金额虽然看起来是数值,但实际检查后发现里面混了少量带“元”字样的脏数据,比如"89元"、"120.5元"这种,这会让整列变成object类型。如果直接sum,结果报错或者得到一串诡异的字符串拼接结果。
所以我的建议是两步走:先用df.dtypes快速扫描,再用df.isnull().sum()查看缺失情况。这两个操作加起来不到五秒钟,但能避免后面所有计算踩坑。
还有一项初检是看看是否有完全重复的行:
print(df.duplicated().sum())我查到有13行完全重复的数据,这属于典型的“数据录入两次”的情况,后面统一去重即可。这类细节如果不在探查阶段发现,做到后面某个统计数字怎么都对不上时,你会很难想起来根因是重复行。
3. 核心实现:数据清洗与特征工程全流程
3.1 清洗第一关:金额字段的“去脏”与类型转换
数据清洗是整个作业里耗时最长的环节,也是最能拉开分差的部分。以订单金额为例,我当时的原始数据里金额列既有纯数字,也有带“元”后缀的字符串,还有少数几条是空白。处理逻辑并不复杂,但要分好几步才能做得干净。
我的处理顺序是这样的:
# 1. 去掉金额字段中的"元"字样,统一转为字符串 df['订单金额'] = df['订单金额'].astype(str).str.replace('元', '', regex=False) # 2. 将空字符串转为缺失值,便于统一处理 df['订单金额'] = df['订单金额'].replace('', pd.NA) # 3. 转成数值类型,无法转换的变成NaN df['订单金额'] = pd.to_numeric(df['订单金额'], errors='coerce')pd.to_numeric这个函数的errors='coerce'参数是这段代码的灵魂,它会把无法转换的内容统一置为NaN,而不是直接让程序崩溃。转换结束后,我又检查了一次缺失情况,发现多了14个NaN,这些就是原本带“元”后缀和真正空白的记录加在一起的量。
清理掉脏字符之后,还要再做一个合理性边界检查:订单金额理论上应该是正数。我扫了一遍发现最低值是-9.9,这种负数订单通常是退款或者测试订单,但作业题意是分析销售数据,这类负值会严重干扰聚合结果。我的处理是先把边界情况都打印出来看一下,确认是退款单后,在本次分析中先过滤掉。
3.2 清洗第二关:时间字段解析与业务特征提取
时间字段的清洗是这道作业里最容易出错的地方,因为原始数据里的时间格式杂乱到让人头大。我扫了一遍发现至少存在三种格式:2024/01/15 09:23:11、2024-01-15 09:23、2024年1月15日。如果不统一成一种格式,之后做月份提取就只能得到一堆垃圾值。
我的做法是直接用pd.to_datetime让Pandas自动推断格式:
df['下单时间'] = pd.to_datetime(df['下单时间'], format='mixed')Pandas从2.0版本开始支持format='mixed'参数,可以自动识别混合格式。如果是老版本,就得用errors='coerce'配合手动格式列表去试。解析完之后,我单独检查了解析失败的行,把这些行标记成缺失值,然后根据订单号的连续性做了少量补录,其余实在无法确认的就直接删除。
时间格式洗好之后,特征工程就水到渠成了:
df['月份'] = df['下单时间'].dt.to_period('M')这里用to_period('M')而不是strftime('%Y-%m'),原因在于to_period得到的是Pandas的Period对象,后面按月份分组时可以直接天然排序,不会出现“2024-10”排在“2024-9”前面的字典序问题。这个细节如果不注意,趋势图的横轴顺序就会完全错乱,图表看起来非常业余。
3.3 分组聚合的口径选择:sum、count还是size
分组聚合是作业的核心考法,但也是最容易搞混统计口径的地方。以“各品类订单量”为例,如果直接df.groupby('商品类别').count(),得到的是每列的非空值数量,默认会把订单号、用户ID等所有列都统计一遍,结果看起来列很多,但每一列的数字一样,这就不够专业。
更规范的写法是这样:
category_stats = df.groupby('商品类别').agg( 销售额=('订单金额', 'sum'), 订单量=('订单号', 'nunique'), ) category_stats = category_stats.sort_values('销售额', ascending=False)这里我用agg函数显式指定每个聚合列的计算方式,老手一眼就能看懂你的统计口径。特别注意我用的是nunique而不是count,这是因为我检查发现同一订单会有多个商品行记录的场景(即一个订单包含多件商品),如果按count统计,订单量会被虚高放大。用nunique对订单号去重计数,才能得到真实的订单数量。
关于sum和size的区别也值得一提。agg(销售额=('订单金额', 'sum'))处理的是金额合计,而size()统计的是分组后的行数,两者在概念上完全不同。如果题目问的是“各品类售出多少件商品”,那么size()是合适的;如果问的是“多少个订单”,就要用nunique。这里面的差异,恰恰是这道作业想要考察的“对数据含义的理解”。
3.4 月度销售趋势:当年最隐蔽的一个坑
月度趋势分析本身不难,难在确保月份顺序正确、且时间范围不能错。我遇到的问题是这样的:数据里混了少量2023年12月的记录,而作业想考察的其实是2024年全年的趋势。如果不加过滤直接按月分组,图表开头就会多出一个看起来很小的柱子,导致Y轴自动缩放,后面的真实趋势反而看不清。
最后我加了一道筛选:
df['年份'] = df['下单时间'].dt.year df_current = df[df['年份'] == 2024] monthly_sales = df_current.groupby('月份')['订单金额'].sum()这样处理之后,趋势图的核心信息就非常清晰了。我在复盘时还发现,如果只写groupby('月份')而不sort,输出的月度顺序可能是乱序的。所以建议在分组后加一句.sort_index(),确保月份按时间顺序排列。这一点在可视化时尤其重要,不然画出来的折线图就是一条来回乱跳的线。
3.5 支付状态占比与异常订单筛查
支付状态占比相对简单,value_counts(normalize=True)一步到位,乘以100变成百分比即可。需要注意的是,这里也要先确认缺失值,我看到有17条记录的支付状态是空,占比不到千分之二,直接dropna()删掉不会影响整体结论。
异常值筛查这问比较有趣。我的思路是先看订单金额的整体分布:
desc = df['订单金额'].describe()结果显示均值约为218元,但75分位数是268元,最大值达到了惊人的8999元。这种长尾分布非常典型,均值远大于中位数说明右侧存在极端大额订单。我再用quantile(0.99)找到99分位数的金额作为阈值,把所有超过阈值的订单列出来,逐条查看金额、品类和订单号,发现大部分是正常的企业采购单,但有两笔订单金额恰好是几千元的整数倍,疑似测试数据。
排查异常值没有银弹,核心思路是“先用量化手段圈定,再结合业务理解判断”。如果你要写成可复现的代码,可以用z-score方法:
from scipy import stats import numpy as np df['z_score'] = np.abs(stats.zscore(df['订单金额'])) outliers = df[df['z_score'] > 3]这里z_score > 3表示偏离均值超过3个标准差,是统计学中常用的离群点判定标准。这两种方法结合使用,基本能把异常订单都找出来。
4. 可视化呈现与业务结论解读
4.1 为什么选柱状图与折线图的组合
作业里没有强制要求画图,但我在完成每一个统计结果后都补了一张图。原因很朴素:数字排在表格里时,读者很难一眼看出哪些品类差距大、趋势是在涨还是在跌,而图形能在半秒内传递结论。
各品类销售额对比我用了柱状图,因为品类是离散变量,柱状图可以直观展示排名差距。月度趋势我用了折线图,因为时间序列的核心信息是连续变化的方向和速度,折线比柱状更容易体现“从8月开始爬升”这样的判断。
画图的代码本身不复杂:
category_stats.plot(kind='bar', y='销售额', figsize=(10, 5)) plt.title('各品类销售额对比') plt.xticks(rotation=45) plt.tight_layout() plt.show()这里有个小技巧:plt.xticks(rotation=45)给横轴标签加了45度旋转,否则品类名称稍微长一点就会互相重叠,图会显得非常业余。tight_layout()会自动调整留白,避免标题和坐标轴标签被切掉。
4.2 从图表中反推业务结论
图表画出来不是用来看热闹的,而是用来支撑结论的。我当时从两张图里读出了几个关键信息:
第一,家居品类销售额占据了接近30%的份额,遥遥领先其他品类;第二,全年销售额在9月出现了一个明显的低谷,随后10月开始快速反弹,到12月达到全年峰值。这两个结论如果只看数字,也能得到,但看图会更直观,尤其是9月低谷这个问题,我在纯表格里根本不会注意到,因为排名前十的月份数值差距不大。
可视化最大的价值在于“让你注意到你没有主动去找的问题”。所以我建议做完每个统计之后都画一张图,不只是为了给作业加页数,更是给自己多一双眼睛。
4.3 中文乱码、坐标轴溢出与颜色失真的排查
可视化环节最容易翻车的三个问题,我全踩了一遍。
中文乱码在前面已经提过,不再赘述。坐标轴溢出主要出现在异常值筛查时,把8999的订单和平均两三百的其他订单画在同一张图里,Y轴被极端值拉长,其他柱子全部变成贴地的“矮桩”。这个问题的解决思路很简单:画图前先做一个局部过滤,只看金额低于1000元的订单分布,作为主图;异常值单列一个子图展示。
颜色失真其实是导出图片时遇到的一个小坑,Matplotlib默认的保存格式是PNG,在Jupyter里显示时颜色正常,但导出到Word里有时会偏灰,这是因为默认的dpi太低。保存时手动指定dpi=150就能解决。这个细节不致命,但交作业时图的清晰度确实会影响老师的观感。
5. 作业里最隐蔽的四个雷区与排查思路
5.1 缺失值处理不当导致聚合结果凭空变大
作业数据中用户ID列有少量缺失,我当时第一反应是直接删掉这些行,但后来发现这会导致一个问题:这些行里的订单金额是有效值,删掉之后类别汇总的销售额会变小,而且你根本不知道小了多少。
更好的做法是分情况处理:如果缺失的列不是当前分析所必需的字段,就保留行,只把缺失字段标记为“未知”;如果确实需要用到这个字段做分组,例如做“按用户维度”的统计时,才考虑删除。单纯因为某一列缺失就删掉整行,是很多新人常犯的代价最高的错误。
5.2 groupby之后忘记reset_index导致后续操作报错
groupby默认会把分组字段变成索引,如果不加reset_index(),后面想把这个分组结果和另一个表做关联时,就会一直找不到列名。我在做“月度销售额与订单量双轴图”时就卡在这一步,折腾了好一会儿才发现是索引问题。
所以我的习惯是:在每次groupby().agg()之后,立刻加一句.reset_index(),让分组字段回到普通列。这个习惯虽然简单,但能让后续代码顺畅很多,也避免了很多新手在groupby结果上反复踩坑。
5.3 排序时字典序导致的月份错乱
这个坑在前面提过,我再详细说一下现象。如果时间字段是字符串格式,'2024-10'按字典序会排在'2024-9'前面,因为你比较的是字符而不是数值。当时我用的to_period('M')已经规避了这个问题,但如果你用的是strftime('%Y-%m'),那排序时就必须额外加sort_values,或者干脆用pd.Categorical指定顺序。
这件事给我的教训是:任何时间字段,能早转类型就早转类型,一旦你把它当做字符串处理,后面所有依赖顺序的操作都会变成定时炸弹。
5.4 只看汇总数字、不逐条检查导致的业务误判
写完代码后我也有一瞬间觉得“这不就完成了吗”,但多留了个心眼,把筛选出来的异常订单逐条打印出来看了一眼,结果发现有一条金额是负数的退款记录被算进了总销售额里。如果不做这个检查,最终结论会偏离真实情况差不多0.3%,单看数字影响不大,但在作业汇报时如果被老师问到“这个负数是哪来的”,回答不上来就很尴尬。
数据工作的核心素养就是“永远对结果保持怀疑”,尤其是面对那些看起来特别规整、特别漂亮的结果时,更要往回多查一步。
6. 从作业3.7沉淀下来的通用处理模板
做完这道作业,我把整个流程总结成了一个“四步走”模板。之后再接任何数据处理任务,我基本都会按这个顺序推进。
第一步是结构探查,用shape、dtypes、head、describe把数据的规模、类型和分布摸清楚;第二步是质量清洗,把重复值、缺失值、格式问题全部列出来逐一处理;第三步是口径明确,针对每个分析问题明确分组字段、聚合字段与聚合方式,并用agg显式表达;第四步是结果验证,用图表和逐条抽样检查来确认结果符合业务直觉。
这个模板听上去简单,但真正执行到位需要耐心。尤其是第二步,表面上是体力活,实际上每一处清洗都对应着一个数据质量问题,而数据质量直接决定分析结论的可靠性。以后你遇到任何新数据集,都可以按这个模板走一遍,基本不会出大差错。
最后再分享一个心态上的体会:作业3.7这种题目真正的价值,不在那四个问题的答案本身,而在于完整走了一遍“拿到原始数据→发现问题→定义口径→输出结论”的链路。这个链路你走得越熟,以后面对真实工作中更乱、更大、更缺文档的数据时,心里就越有底。数据清洗这类脏活累活,干一次是折磨,干十次就会变成肌肉记忆,之后再碰到任何“看似不可能完成”的数据任务,你就知道自己一定能啃下来。