☰
数据分析作业复盘:从数据清洗到月度销售指标可视化
2026/10/14 5:24:04 网站建设 项目流程

如果你也是某门课的“第二次作业”卡住了,那这篇东西应该能帮上忙。我最近刚交掉一门数据分析课的第二次作业:一份零售订单表,要求完成数据清洗、月度销售指标计算,再输出图表和结论。任务看着不算硬核,可真正动手之后才发现,这类作业最磨人的不是代码,而是“你永远不知道原始数据里埋着多少雷”。这篇就当一次完整复盘,把从拆题到提交每一步的思考、代码、报错都摊开写出来,给正在跟作业搏斗的同学做个参考,也给第一次接触数据处理作业的同学提前打个预防针。

1. 拿到第二次作业,先别写代码:我把时间花在了拆题上

很多人的通病是,作业一下来就开 IDE,先跑通再说。第一次作业可能还来得及,第二次作业如果还这样,基本会越写越乱。我这次先耐住性子,花了小半天把题目拆透了,后面反而顺了很多。

1.1 那次作业到底在考什么

我拿到的是一个 CSV 文件,里面有订单号、订单日期、客户等级、区域、商品分类、单价、数量、销售额这些字段。题目要求就三句话:

  1. 对原始数据做清洗,处理缺失值、重复值和明显错误值;
  2. 按月份统计销售额、订单数、客单价,并按客户等级汇总指标;
  3. 输出图表和分析结论,最终打包成一个可复现的分析脚本。

表面上是考 pandas 和 matplotlib,实际上我觉得出题人埋了两个隐藏考点:第一是数据处理时的边界情况,比如日期格式不统一、销售额出现负数;第二是分析结果能不能“讲出故事”,而不是只丢一张图。

这时候先别着急下载数据,应该把作业归类。我总结过,课程作业基本逃不开三种类型:

作业类型典型特征应对重点
完成型作业代码填空、改参、跑通示例理解每一行在干什么
实验型作业给数据、给方法,复现过程记录过程,保留中间输出
项目型作业给目标、给数据,方案自定需求拆解、方案取舍、结果解释

第二次作业通常是“实验型”和“项目型”的过渡,不会再手把手喂答案。意识到这一点,我就知道不能只盯着“怎么算”来写,还要解释“为什么这么算”。

1.2 把题目翻译成“验收清单”

拆题最实用的一招,是把每句话抠出来,转成一个具体可交付的文件。我当时在笔记本上写了一个清单,就四行:

  • clean_data()函数:清洗后能输出orders_clean.csv;
  • monthly_summary表:统计每个月的销售额、订单数、客单价;
  • 一张按客户等级分组的汇总表;
  • 一张月度销售趋势图,并且中文显示正常。

除了交付物,还要提前搞清楚交作业的格式。有的课要求交 Notebook,有的要求交脚本,有的要求压缩包。这个如果等到提交前一天再问,往往来不及调整。我这次要求交.py脚本加结果文件,所以从一开始我就用脚本方式组织,而不是在 Jupyter Notebook 里堆一堆没顺序的代码块。

我还习惯把题目中的模糊词圈出来。例如题目说“处理缺失值”,但没有说怎么处理。到底是删行还是填充?这取决于业务含义:订单日期缺失没法算月份,只能删;客户等级缺失可以填“未知”,不影响汇总。这种职业判断,就是作业拉开差距的地方。

2. 环境与数据的“第一次相亲”:两小时踩掉一半坑

拆完题之后才是动手环境。这里我劝你千万不要图省事,直接在系统 Python 里装包。第二次作业用到 pandas、matplotlib、openpyxl,依赖关系比第一次复杂得多,稍不注意就会把本机环境搞得一团糟。

2.1 用虚拟环境把依赖锁死

我见过太多同学卡在“第一次作业能跑,第二次作业报错”上,原因基本就是包版本冲突。第二次这种作业正好是装虚拟环境的最佳练习场景。我当时用了最普通的做法:

python -m venv venv # macOS / Linux source venv/bin/activate # Windows venv\Scripts\activate pip install pandas matplotlib openpyxl

如果想把版本控制得更严格,可以顺手把版本号固定下来:

pip install pandas==2.1.4 matplotlib==3.8.3 openpyxl==3.1.2 pip freeze > requirements.txt

固定版本不仅是为了这次作业能跑,更是为了以后重跑时不被环境差异搞崩。你在本机能跑,老师在他的电脑上不一定能跑,一个requirements.txt能省掉很多关于“为什么我这边报错”的来回沟通。

创建好虚拟环境后,我一般不急着写代码,先跑一个最小导入测试:

python -c "import pandas; import matplotlib; print('ok')"

能出ok再继续,免得写到一半发现 matplotlib 拉起一个图形界面就卡死。

2.2 读数据的三个必查项

数据读取是第二个大坑。CSV 文件看起来简单,但编码、分隔符、列名这些细节能让人头大。我读数据第一版就踩了编码坑,后来总结出一个“三步检查法”:

第一步,确认编码。用 Excel 另存的 CSV 经常是 GBK 编码,直接 pandas 读会报UnicodeDecodeError。我这次先试utf-8,报错后立刻改用utf-8-sig:

import pandas as pd df = pd.read_csv("orders.csv", encoding="utf-8-sig") print(df.shape) print(df.head())

注意utf-8-sig比utf-8多一个 BOM 处理,读 Excel 导出的中文文件时基本是标准答案。

第二步,看整体信息。用df.info()看每列的空值数量和数据类型,比肉眼扫 Excel 靠谱得多:

print(df.info())

这一步能迅速暴露两个问题:有些列明明是数值类型却显示object,说明里面有脏数据;有些列的空值比例很高,要决定后续填充还是删除。

第三步,查重复行。尤其是订单号这种唯一标识字段,一旦重复,后面做订单数统计就会虚高:

duplicate_mask = df.duplicated(subset=["订单号"], keep=False) print(df[duplicate_mask].head())

这里我不建议一上来就drop_duplicates()。先看重复数据是全行重复还是只有订单号重复,再决定怎么处理。真实的订单表里,同一个人先下单后取消,再用同一个订单号占坑,这种数据未必是垃圾,盲删会误伤。

2.3 日期和金额字段的“体检”

数据读进来只是开始,真正的清洗其实围绕字段类型展开。第二次作业里最典型的两个类型问题是日期和金额。

我拿到的那份表,订单日期被写成2024/1/5、2024-01-05、2024.01.05三种格式混合。pandas 的to_datetime默认识别的格式有限,但加了errors="coerce"后能容忍很多脏数据:

df["订单日期"] = pd.to_datetime(df["订单日期"], errors="coerce")

这里有个关键点:errors="coerce"会把解析不了的值变成NaT,而不是让程序当场崩溃。这个行为对数据分析作业非常实用,因为你可以把异常值先留着,后面统一统计“有多少条日期无解析”,而不是程序跑一半停下来。

金额字段的问题则是类型和正负。我那个表里销售额列有少量"1,234.56"这种带千分位的字符串。读进来后这一列是object,需要清理后转成数值:

df["销售额"] = ( df["销售额"] .astype(str) .str.replace(",", "", regex=False) .astype(float) )

接下来就要问:负的销售额怎么处理?零售业务里订单退款会出现负数,题目没说清楚时,不能一口咬定是脏数据。我的处理办法是单独建一个退款标记,而不是直接删掉:

df["是否退款"] = df["销售额"] < 0

这样既不影响统计时“要不要剔除退款”的自由度,又能体现你考虑过业务场景,而不是只会执行删除。

3. 指标计算与可视化:第二次作业的“正餐”

清洗完了,终于进入核心环节。作业要求按月和按客户等级做统计,这里我用到的并不是什么高深算法,但其中有两个细节非常值得注意:一个是日期分组的写法,另一个是图表中文字体配置。

3.1 从日期解析到分组统计

日期清洗成datetime后,怎么按月分组?我见过有人把日期截成字符串"2024-01"再用 groupby,这样也行,但更规范的做法是用 pandas 的dt.to_period("M"):

df["月份"] = df["订单日期"].dt.to_period("M") monthly = ( df.groupby("月份", as_index=False) .agg( 销售额=("销售额", "sum"), 订单数=("订单号", "nunique"), 销售数量=("数量", "sum"), ) ) monthly["客单价"] = monthly["销售额"] / monthly["订单数"] monthly["月份"] = monthly["月份"].astype(str) print(monthly)

这里有一个容易翻车的细节:如果用订单号去count(),会把重复订单也数进去,所以要用nunique()。销售统计里的“订单数”通常指去重后的订单数量,不是明细行数。第一次做这个作业的同学,十个里面有八个在这里统计偏大。

再看客单价。客单价的业务定义是“销售额 / 订单数”,不是“销售额 / 销售数量”,更不是平均单价。这种指标定义题里不会写,属于基础常识,写错了图表看起来合理,但经不住追问。

按客户等级汇总可以另外写一个分组,用agg一次算多列:

by_level = ( df.groupby("客户等级", as_index=False) .agg( 订单数=("订单号", "nunique"), 销售额=("销售额", "sum"), 退款额=("销售额", lambda x: min(x.sum(), 0) if (x < 0).any() else 0), ) ) by_level["客单价"] = by_level["销售额"] / by_level["订单数"]

我顺手加了一个“退款额”字段,是因为前面做了退款标记,这里不把退款单独拆出来,老师很容易看出你只做了表面清洗,没有深入看业务。

3.2 中文字体和图表输出

用 matplotlib 出图时,第一个崩溃点永远是中文乱码。很多人代码逻辑没问题,就是图里中文全变成方框。这里把标准化配置贴出来:

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # Windows 常用黑体 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示异常

如果你在 macOS 上用黑体不生效,可以改成:

plt.rcParams["font.sans-serif"] = ["PingFang SC", "Arial Unicode MS"]

但交作业的时候,最稳妥的办法还是把图片导出成高分辨率文件,再插到报告里,而不是直接依赖 Notebook 内嵌图。因为评分系统很可能不解析 Notebook 输出,文件更稳妥。

我这次做了两张图:一张月度销售额趋势折线图,一张客户等级销售额占比柱状图。折线图的实现:

fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(monthly["月份"], monthly["销售额"], marker="o", color="#2E86AB", linewidth=2) ax.set_title("月度销售额趋势") ax.set_xlabel("月份") ax.set_ylabel("销售额(元)") ax.grid(True, linestyle="--", alpha=0.6) plt.tight_layout() plt.savefig("月度销售额趋势.png", dpi=150)

这里有一个关键技巧:用plt.tight_layout()调整边距,再savefig。顺序不能反,否则标题可能被裁掉。另外不要用plt.show(),在脚本环境下调用它容易中断后续流程,而且保存的图片可能空白。

柱状图的写法类似,但更要注意配色和排序。按客户等级画柱状图时,如果等级本身有高低顺序,不要用默认的字母序,直接用自定义顺序:

order = ["普通客户", "会员客户", "VIP客户"] by_level["客户等级"] = pd.Categorical(by_level["客户等级"], categories=order, ordered=True) by_level = by_level.sort_values("客户等级") fig2, ax2 = plt.subplots(figsize=(8, 5)) ax2.bar(by_level["客户等级"], by_level["销售额"], color="#4472C4") ax2.set_title("不同客户等级销售额对比") ax2.set_xlabel("客户等级") ax2.set_ylabel("销售额(元)") plt.tight_layout() plt.savefig("客户等级销售额对比.png", dpi=150)

这里使用Categorical排序是我认为最像“有工作经验”的处理方式。它不改变数据结构,只是给排序制定规则,做报表的人一看就懂。

3.3 结果导出别只丢一张图

作业只要求图表和结论,但我额外把统计表导出成了 Excel。因为这样看数据的人可以自己筛选、排序,比我贴一张截图好太多:

from openpyxl import Workbook with pd.ExcelWriter("分析结果.xlsx", engine="openpyxl") as writer: monthly.to_excel(writer, sheet_name="月度统计", index=False) by_level.to_excel(writer, sheet_name="客户等级统计", index=False)

过程很顺利,所以这里也提醒一句:导出 Excel 前,把列名里的中文改成正常可读的文本,否则 Excel 打开后看起来全是Unnamed或者英文表头,观感很差。

4. 交作业前夜的故障排查实录

就算前面的代码都按部就班写好,一定还是会遇到零碎报错。我这次从开始做到最终提交,前后踩了五个坑,列在这里供大家对照查。

4.1 经典报错定位思路

报错现象常见原因解决方案
UnicodeDecodeError文件编码不是 UTF-8改用encoding="utf-8-sig"或gbk
ParserError或列数对不上分隔符不是逗号读取时指定sep=";"或delimiter="\t"
to_datetime后大量NaT日期格式混杂先统一格式,或用errors="coerce"后单独检查
图里中文乱码缺少字体配置按 3.2 节配置font.sans-serif
Excel 导出失败列名重名或特殊字符清洗列名,去掉前后空格和#符号

印象最深的是日期列。我原以为to_datetime能解决所有日期格式,结果打开数据一看,有一批记录长这样:2024-01-05 00:00:00,还有一批只有时间戳三个字。后者直接把我卡住了。排查方法很简单,先跑一遍:

df["日期解析结果"] = pd.to_datetime(df["订单日期"], errors="coerce") print(df[df["日期解析结果"].isna()]["订单日期"].value_counts())

把解析失败的值value_counts()打出来,立刻就能发现是哪几种“脏格式”在捣乱。

另一个值得单独说的报错是图形窗口卡死。有同学用 matplotlib 画图后直接plt.show(),在作业脚本里如果不关闭图形窗口,后面的代码可能一直挂着。规矩做法是:要么plt.close(fig),要么根本不调用show,直接savefig。

4.2 作业提交前的自检清单

第二次作业跟第一次不一样,不能只看结果文件在不在,还得确认“换个环境能不能跑”。我提交前会做一遍自检,这里分享几个特别实用的检查点:

  • 清空输出、重启 Python 解释器后,整个脚本能否从头到尾跑通;
  • 把venv目录删掉,用requirements.txt重建后能否跑通;
  • 报告里的图表是否用的是最新版本,避免改完代码忘了重新出图;
  • 压缩包名称是否包含自己的学号或姓名,避免老师下到一堆同名文件;
  • 代码里不要有绝对路径,比如C:\Users\...,否则老师电脑上必然报错。

最后一条是很多新手容易忽略的。我写脚本时统一用相对路径orders.csv,然后把数据文件和脚本放在同一个目录。这样压缩包发给别人也能直接跑。如果你用的是 Notebook,还要顺手清空 Cell Output,不然文件体积又大又乱。

还有一个隐藏雷区:不要把临时调试截图塞进去。我见过有人在最终报告里放了几张报错截图,老师批起来会觉得很糊弄。报告只放精心调过的图和分析结论,中间过程自己保留即可。

5. 复盘:第二次作业真正教会我的三件事

作业交完之后,我回过头又重写了一遍代码,重写过程中才意识到这次作业和第一次作业有本质区别。第一次作业做完我只会复制粘贴示例,第二次作业做完,我终于敢说“我自己能判断一列数据该不该删了”。

5.1 看懂“出题人意图”比跑通代码重要

很多同学做数据分析作业,把“能出图”当成最终目标,这是把作业想浅了。第二次作业设置的真实目标,是希望你建立“数据 → 指标 → 结论”的链路。只跑出一张折线图,不解读“为什么三个月销售额连续下滑”,分数绝不会高。

我这次在报告里写了三条结论:月度销售额呈现明显上升趋势;VIP 客户数量虽然少,但贡献了半数以上销售额;退款集中在销售旺季,属于正常业务波动。这三句话每一个都对应一个计算结果,而不是拍脑袋。后来我回想老师课上强调的,其实就是“带着业务问题去做分析”。

5.2 异常处理不是加分项,是保命题

这次作业里我用了很多errors="coerce"、try...except,初看以为只是让代码变稳健,后来才意识到:真实世界给到的数据从来不会像教学示例那么干净,老师留这个作业就是想看你能不能“扛住脏数据”。

有个同学跟我用同一份数据,他写完代码后没有检查NaT数据量,统计月份时分组结果里多了一堆“未知月份”。这就是典型的异常处理不到位。把边界情况处理干净,作业也许不会因此加分,但能避免被扣掉大量“数据质量分”。

5.3 时间盒:第二次作业比想象中多出百分之四十工作量

这是我这次最大的教训。我原计划用两个晚上写完作业,结果整整用了五个晚上:一个晚上拆题和搭环境,半天清洗数据,一天写代码,一天作图写报告,还有一天专门用来排查报错。如果一开始就预留出多 40% 的时间,最后那个通宵就不会发生。

所以给正在做第二次作业的你一个建议:拿到题先估算“如果一切正常要多久”,然后乘以 1.5。这个倍数不是销售话术,是数据处理作业的常规衰减系数。时间充裕的情况下,宁可提前把报告框架写好,也不要留给突发故障。

最后再说一个我实践下来的小技巧:交作业前把脚本里的打印输出完整看一遍。打印结果本身就能充当一个“最低配报告”,很多老师在批作业时会扫一眼控制台输出,如果你的输出整齐、字段完整,第一印象会好不少。第二次作业的难度,恰恰是在这些细节里一点点拉开差距的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询