多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python数据分析第九天上机复盘:订单数据清洗到可视化全流程

Python数据分析第九天上机复盘:订单数据清洗到可视化全流程 第九天上机我盯着屏幕上那行空列表的输出愣了半天。明明代码逻辑看起来都对跑出来的结果却是一片空白。后来才发现问题根本不在语法而在数据本身——这是一份藏着各种脏数据的销售订单表。这节课的名字就叫Day 09上机作业也不是让我们徒手造一个爬虫或者写一套算法而是面对一份真实凌乱的订单明细完成从数据探查、清洗、字段衍生到分组汇总、可视化输出的完整闭环。如果你正在参加类似的数据分析训练营或者自己按课程表学到第九天附近这篇复盘应该能帮上不少忙。说实话前几天的上机基本都是照着语法写练习代码能运行就算过关。但从第九天开始画风明显变了——题目只给了三张表和一个含糊的分析目标剩下全靠你自己判断。这篇就把我踩过的坑、做出的每一个关键判断和背后的理由以及最后整理报告的习惯从头到尾捋一遍。适合正在学Python数据分析、准备做中期项目或者刚接触真实数据集但不知道从哪下手的读者。1. 第九天上机到底做了什么任务本身比代码更值得拆解1.1 一张订单明细表引出的三个核心问题上机题给的数据并不复杂至少从文件数量上看是这样一张订单明细表orders.csv一张客户信息表customers.csv一张产品信息表products.csv。目标描述也写得很简单基于订单明细完成销售数据分析输出月度销售额趋势、地区销售对比、品类构成和Top10热销产品。但真正打开文件之后问题就接踵而至。首先是orders.csv里将近15%的订单行没有交付日期还有几行的折扣字段带着百分号后缀另外一些客户编号在customers表里根本查不到。其次是产品表里的品类字段存在三种写法同一个数码配件分别被写成数码配件数码-配件数码/配件。这些问题单独看都不起眼但叠加在一起几乎会让所有后续的聚合统计全部跑偏。这其实暴露了一个关键事实数据分析的大部分时间不是花在建模上而是花在弄清楚数据到底长什么样上。所以这次上机最有价值的环节并不是最后的图表而是前期的探查和清洗判断。1.2 Day 09的训练意图从会写代码跨到能处理数据我后来回头看课程安排发现第九天这个节点卡得很讲究。前八天覆盖了Python基础语法、pandas的数据结构、文件读写和基础可视化学员基本具备了用代码操作DataFrame的能力。但会操作数据和能把数据变成可信的结论中间隔着一整段数据质量的认知空白。这次的题目就是专门来补这段空白的。它要求你面对不完整、不一致、有冗余的数据时能做出合理决策哪些缺失要填充哪些缺失该删除哪些记录算重复哪些异常值是真实的业务波动而不是录入错误。这些判断书上没有标准答案只能靠对业务逻辑的理解来推演。也就是说第九天上机看起来是在考pandas函数实际上考的是数据思维。函数记不住可以查文档但这一列缺失20%意味着什么这两个字段重复了该信谁这类问题文档不会告诉你。2. 动手前先花十分钟做初始探查环境、编码、数据概览2.1 文件读取的三处暗坑路径、编码、分隔符上机一开始我就吃了环境的亏。训练营提供的作业压缩包解压后文件的路径里带着一层又一层的中文文件夹名而我的工作目录还停留在默认位置。第一次用相对路径读取直接报错这算是我犯的最基础但也最常见的错误。解决方式其实很简单用pathlib来定位文件而不是手动拼接字符串。from pathlib import Path data_dir Path.cwd() / data orders pd.read_csv(data_dir / orders.csv, encodinggbk)这里有个细节容易踩坑文件明明保存的是GBK编码用pandas默认的UTF-8去读会在中文字段上直接抛UnicodeDecodeError。起初我以为自己文件路径写错了后来才意识到是编码问题。如果你在Windows上拿到一份从业务系统导出的CSV最常见的编码要么是UTF-8要么是GBK遇到乱码不要慌先花10秒换encoding参数重试一遍。判断编码还有个土办法用文本编辑器打开CSV看中文是否正常显示或者直接在Python里试着读原始字节with open(data_dir / orders.csv, rb) as f: raw f.readline() print(raw[:200])如果看到一堆类似\xc4\xe3\xba\xc3的字节序列那基本是GBK编码。如果中文直接显示为正文那就是UTF-8。再一个读取坑是分隔符——CSV并不都是逗号分隔有些导出文件会用制表符或分号。pd.read_csv默认支持逗号遇到分隔符异常时读进来的DataFrame会变成一列此时要传参sep\t或sep;。刚开始我过于信任文件的扩展名忽略了这些基础检查结果后续的列名全部错位。2.2 df.info()只能看到一半事实空字符串与NULL伪装文件读进来之后我习惯性地先敲了一遍df.info()和df.head()。表面上一切正常订单表有8000多行客户表有1200行产品表有90行列类型看起来也大致合理。但如果只看到这一步后面的清洗步骤就会跑偏。我列了一个小小的探查清单建议每个拿到真实数据的人都照着做一遍# 1. 看缺失值的真实分布 print(orders.isnull().sum()) # 2. 看每一列的null值直方图 for col in orders.columns: nulls orders[col].isnull().sum() if nulls 0: print(col, nulls)问题恰恰出在这里有相当一部分缺失值根本不是NaN而是空字符串和字符串NULL。销售日期这一列看起来没有NaN实际上有很多行是空字符串还有个别行写着字符串形式的NULL。pandas读取后这两者不会被识别为缺失值它们只是普通字符串。这就是为什么说df.info()只能看到一半事实。.info()报告的空值是真正的NaN但空字符串会被当成普通对象列甚至会让整列数据类型的推断失真。比如discount这一列里混了几行空字符串整列就被推断成object而不是float。处理这种伪装缺失值时我先做了一步统一转换import numpy as np # 将空字符串和NULL文本统一替换为np.nan orders orders.replace({: np.nan, NULL: np.nan, null: np.nan}) orders[order_date] pd.to_datetime(orders[order_date], errorscoerce)errorscoerce会把所有无法解析的日期变成NaN这一步之后再跑一遍isnull().sum()缺失情况才真正浮出水面。如果在探查阶段跳过这一层转换后面所有日期统计都会把空字符串日期当作一种奇葩的未知日期参与分组结果就是时间轴上多出一个莫名其妙的类别。3. 清洗不是删缺失值那么简单每一步都要问业务含义3.1 缺失值处理为什么同一列里不同缺失行要区别对待前两步探查做完之后订单表的缺失情况终于清楚了交付日期缺失率约15%折扣字段缺失率约5%还有一小部分客户编号无法匹配到客户表。拿到这个分布新手最容易犯的错误就是一句dropna(howany)把所有带缺失的行全部抹掉。但真实数据不能这么暴力。我这次上机最大的体会就是缺失值处理必须以业务含义为依据。同样是交付日期缺失要分两种情况看部分订单的交付日期缺失但订单日期存在且订单状态显示已发货部分订单的交付日期缺失订单状态显示已取消对于已取消订单交付日期缺失是完全合理的信息不需要填也不需要删对于已发货订单可以选择用订单日期加上该产品的平均交付周期做估算或者标记为待确认。我当时没有机械地删除所有缺失行而是新增了一列delivery_status把缺失分成了两个语义。再来看折扣字段。订单折扣缺失率只有5%而且这些行的产品集中在少数几个低毛利品类上说明这不是随机缺失而是录入环节的系统性漏填。直接删除会损失样本用全局中位数填充又可能扭曲业务规律。我最后的处理是按产品品类分组用同品类订单的折扣中位数去做填充同时保留一个discount_imputed标志列。这样后续如果要做价格敏感性分析还能回到原始缺失状态。3.2 重复记录与异常值界定同一条记录是个业务问题重复记录的处理也不能一概而论。订单明细表不是每行天然唯一的同一个订单号下会有多个商品行。刚开始我直接用duplicated(subset[order_id])去查重复结果返回了一大片。后来才意识到在订单明细表里重复的定义应该是订单号产品ID行号联合唯一而不是订单号唯一。我调整了重复判断的维度最后只清理了六条真正完全相同的记录。这类记录大概率是源系统重复写入导致的。判断是否重复不只是看所有列是否一致也要思考业务上每条记录的粒度是什么。你首先要搞清楚这张表每一行代表什么再做去重判断否则极有可能把真实的多行明细当成重复给删掉。异常值方面订单明细里的数量列有一行是-5单价列有一行是0.01。单看数值-5肯定是异常但结合备注字段补发赠品来看这类负数可能是业务上的退货冲抵不能简单删除。我的处理方式是先把这些标记为outlier_flag然后单独做了一次分布验证确认它们在总量中占比极小后才在汇总统计时排除。4. 从清洗到分析字段衍生和分组汇总的正确顺序4.1 向量化计算与空值传染订单金额和毛利率是怎么算出来的清洗告一段落接下来就是字段衍生。题目要求最终要产出销售额毛利额毛利率三类指标这些指标的基础字段原始表里都没有需要自己构造。常见的错误是直接用for循环逐行遍历计算遇到8000行数据还勉强能跑但一旦数据量上到八万行性能就会急剧下降。正确的做法是用pandas的向量化运算orders[amount] ( orders[quantity] * orders[unit_price] * (1 - orders[discount]) ) orders[cost] orders[quantity] * orders[unit_cost] orders[gross_profit] orders[amount] - orders[cost] orders[gross_margin] orders[gross_profit] / orders[amount]这里有一个看起来不起眼但很容易翻车的点空值的传染性。如果某一行discount是NaN那么1 - NaN的结果是NaN整行amount也是NaN。前面清洗时把小部分缺失折扣填充掉了但还有个别行的数量字段是空值导致计算出来的金额仍然是NaN。所以做字段衍生之前一定要先确认所有参与运算的关键列已经完成空值处理。再补充一个关于金额口径的细节。订单明细表里unit_price本身带了两位小数但有些价格条目有三位小数销售系统导出时并没有统一格式化。如果你直接相加最终汇总金额会出现0.01的误差。我在衍生字段之后统一做了一轮四舍五入orders[amount] orders[amount].round(2)这样做的原因是保证后续所有汇总层级产品、地区、月份在加总时不会因为浮点误差产生分叉。数据分析的结果如果差了几分钱业务方是可以接受的但如果报表对不上账信任就会瞬间崩塌所以金额字段宁可多一步round也不要省。4.2 groupby与pivot_table什么时候用哪个字段就绪之后便进入分组汇总环节。题目要三个维度月份趋势、地区对比、品类构成。很多初学者会把这三个需求硬塞进同一个groupby里结果输出一个难以阅读的长表。我在第九天上机时尝试过好几种写法最后确定的原则是先把维度拆开分析再在最后用透视表做整合。月度销售额趋势最自然的是用groupby加resamplemonthly ( orders.set_index(order_date) .resample(M)[amount] .sum() .reset_index() )注意这里的resample(M)会默认把月末日期作为分组标签做折线图时标签会显示成2024-01-31这种形式记得再格式化成2024-01否则图表x轴会显得很脏。地区对比和品类构成则适合用pivot_table来做二维交叉汇总region_cat pd.pivot_table( orders, valuesamount, indexregion, columnscategory, aggfuncsum, marginsTrue, margins_name合计, )marginsTrue会在行列的末尾自动加一个合计用来快速看每个地区和品类的绝对体量。比较让人纠结的是要不要把销售额和毛利率放在同一个透视表里。我试过把values传成两列但结果会变成一个多层列索引的DataFrame阅读负担很大。最后采用了拆表思路一张表看量的构成另一张表看毛利率两者在最终报告里用图表呈现。groupby和pivot_table的核心区别在于前者适合做分组后的任意聚合再行处理后者适合快速搭建交叉表。实际项目里我基本两者混用先groupby验证数据明细再pivot_table简化展示。5. 用图表把结论讲清楚可视化输出和报告组织5.1 选图与中文字体三层结论对应三种图汇总数据出来之后下一步是出图。上机题对图表没有硬性类型限制但要求结论清晰。我在做图表时给自己定了一个原则一张图只回答一个问题宁可多画几张也不要叠成一团。月度趋势用折线图这是一个不会错的选择。但如果只画一条总销售额折线信息量其实不够。我额外加了一条近三个月移动平均线这样能从曲线里看出增长的真正动能而不是被单月波动干扰。地区对比用横向条形图因为地区名称较长横向条形在可读性上明显优于纵向柱状图。品类构成我选的是横向条形加百分比标签而不是饼图——饼图在维度超过五个时视觉上的面积对比非常不可靠。还有个绕不开的坑是matplotlib的中文字体。Windows上经常遇到中文显示成小方块Linux服务器则更明显。我在上机一开始就配了字体参数import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, WenQuanYi Zen Hei] plt.rcParams[axes.unicode_minus] Falseaxes.unicode_minus这个参数特别容易被忽略不设置的话坐标轴上的负号会显示成一个奇怪的方块。做毛利率对比图时如果出现负毛利区间负号显示不正确整张图的说服力都会受影响。另外出图前务必调整x轴标签的旋转角度plt.xticks(rotation45)这一个参数就能让月份标签不再互相堆叠。5.2 工程目录和中间结果落盘让每一步都可以被复查代码跑通、图表出完上机看起来已经结束了。但我最后又花了一些时间做了一件很多人觉得多余的事整理工程目录和落盘中间结果。每个清洗步骤产生的中间表我都单独存了一份CSV放在interim目录里最终用于画图的汇总表放在processed目录里图表输出统一放到report目录。目录结构大概是这样的day09/ ├── data/ │ ├── raw/ # 原始三张表只读 │ └── processed/ # 清洗后的订单、客户端到端 ├── interim/ # 中间清洗结果 ├── report/ │ ├── charts/ # 所有图表 │ └── summary.csv # 核心汇总表 └── notebook.ipynb # 主分析脚本很多人会觉得这样很啰嗦但在真实项目里业务方很可能在报告发出后的第二天会追问你刚才说的那个毛利率去掉退货单后是多少如果你没有保留中间步骤那就得重新跑一遍全流程而且还不一定记得当初具体做了哪几步处理。中间结果落盘之后我把每一步的清洗决策都写在脚本的注释和报告附注里。比如交付日期缺失16.2%其中已取消订单占比11%故不填充折扣字段按品类中位数填充填充量5%已标记这样等到复盘时整个分析链路可以随时复现。这个习惯在第九天上机时还看不出优势等到了中期项目会非常感谢当时的自己。6. 这次上机暴露的三个坏习惯比作业分数更值得记住6.1 不看原始数据就写处理逻辑我前面提到空字符串和NULL伪装缺失的问题其实根源在于我一开始没有真正去看原始数据的内容只停留在DataFrame的信息摘要层。拿到一张表就急着写后续的清洗函数结果函数一跑发现大量预期外的值没有覆盖到。后来我强迫自己养成一个习惯任何一张新表先打印前50行再随机抽20行查看用肉眼确认字段的样式和规律。这一步看起来原始但能让你迅速建立起对数据的直觉。比如你会注意到客户编号全是CU_00001这种前缀格式而订单编号是纯数字这些信息在写join逻辑时至关重要。第九天上机之后我把先看数据再写代码写进了自己的检查清单每次处理数据前都会做一次快速抽样。6.2 print满天飞函数返回值和输出混在一起排查数据问题时我习惯在各处插入print来观察中间变量这本身没问题问题出在这些print最后没有清理混进了正式的分析脚本里。跑出来的结果里控制台输出和图表输出两套信息互相干扰一度让我分不清哪份数据才是可靠的。更合理的做法是把探查性的print统一放进一个专门的exploration代码块或者用日志输出的方式来保留关键信息而不是随处print。函数内部尽量用return返回结构化结果只在主流程的特定节点输出信息。这样脚本在复现时控制台日志是干净且有层次的后续排查也能快速定位到具体环节。6.3 中间结果不做版本管理第九天上机的脚本后期被我改了很多轮清洗逻辑一会儿改成按品类填充折扣一会儿又改回全局删除缺失行。因为没有版本管理改到后面我自己都记不清当前这份图表用的是哪一版清洗结果。这个坏习惯地解决也很简单在Jupyter里多使用新建单元格加注释的方式记录变更在脚本里用Markdown标题标出V1初步清洗V2增加品类填充策略或者直接引入git做版本控制。不要轻视这些笨办法数据项目里最贵的成本从来不是写代码而是追溯这份结果到底怎么来的。那次上机最后的结果其实不算惊艳——月度趋势、地区对比、Top10热销都是基础图表。但我后来意识到第九天上机真正想让我学会的不是画图技巧而是在一团乱麻的数据面前保持清醒的判断力。每次遇到数据结果显示好像不太对的时候我都会想起课上那句提醒先别急着改模型回去看看原始数据。这大概是比任何函数语法都值钱的一句话。
返回列表