2026/10/19 16:55:39

数据清洗实战:Pandas处理30万行脏订单数据的完整记录

数据清洗实战:Pandas处理30万行脏订单数据的完整记录 大概从第9天开始我每天都会给自己安排一个接近真实的小任务第9天是抓取静态页面第10天是写一个能轮询的监控脚本第11天处理了Excel合并第12天自己画了十几张图表。到了第13天我决定不再用那些教学版的规整数据而是造了一份糟糕透顶的模拟业务数据专门用来练手。结果就是这一天的上机练习比我前面所有天数加起来都更有收获也终于理解了练习这件事的真正分水岭在哪里。如果你也正在坚持某种上机练习、编程实操或者技能打卡又刚好到了学了不少但一上手就卡壳的阶段这篇内容或许能帮上忙。第13天听起来没什么特别的但它正好处在一个微妙的位置——前面12天的热情已经消耗得差不多能不能把技能真正用起来就看这一天能不能扛住挫败感。1. 第13天为什么容易成为练习计划的分水岭1.1 前12天我到底在练什么先交代一下背景。我给自己定了一个三个月的上机练习计划目标是能独立完成数据处理类的小项目。前四天全在磨Python基础从变量、循环、函数一路写到文件读写第五天到第八天集中啃Pandas把DataFrame的切片、分组、合并、透视表挨个过了两遍第九天到第十二天开始做“半真实”的练习比如用现成的接口拉数据、写自动化脚本、给数据画图。这个阶段有个共性数据都是干净的。要么是教程自带的示例数据要么是我自己刚生成的规整表格字段齐全、格式统一、没有奇怪的字符。做这种练习最大的错觉就是“我已经会了”。比如我用Pandas处理一份三千行的销售数据整个过程行云流水我还顺手写了三十行美化代码觉得自己基本功已经很扎实了。但到第13天早上我坐在工位前重新想了下这件事如果换一份真正的业务数据我还能这么轻松吗答案我并不确定。1.2 从照着写到接得住脏数据的质变第13天之前的所有练习本质上都是“照着教程的逻辑复现一遍”区别只是复杂程度不同。可真实环境里的数据没有人会提前帮你把格式整理好。字段该是数字的变成了字符串日期有斜杠有横杠还有中文格式金额里带着逗号和货币符号同一批记录重复出现甚至某些单元格里还混着肉眼根本看不见的字符。我后来总结过一句话前12天在练习写代码第13天才开始练习解决问题。解决不是从写好代码开始而是从读懂数据、识别异常、做判断开始的。这种转变成不转变直接决定练习有没有意义。所以第13天任务的核心不是完成一个多大难度的项目而是让自己体验一次完整的“救火”过程拿到一份不友好的数据分析哪里有问题设计处理方案写代码发现报错反复调整最后产出一份能用的结果。1.3 给自己加的一天压力测试我把第13天设计成一次压力测试是因为按照计划后面还有更复杂的任务在等。如果到了第28天、第45天才发现自己连乱数据都接不住再回头补基础时间成本会高很多。倒不如在第13天主动制造一次麻烦让自己在那一天把所有容易踩的坑都踩一遍后面再遇到类似问题至少心里有数。这次压力测试我设了三条规则。第一条整个过程不翻教程最多查官方文档第二条必须用代码解决问题不手工改数据第三条无论多狼狈当天必须产出一个可复现的清洗脚本。事实证明这三条规则直接决定了我后面几天的练习质量。2. 当天练习项目一份乱到失控的模拟订单数据2.1 数据是怎么造出来的为了接近真实我用脚本“故意”造了一份乱数据30万行23个字段涵盖了订单编号、客户名称、商品分类、数量、单价、订单金额、订单日期、发货区域、收货地址、备注等。造数据的时候我刻意注入了很多问题模拟的是一个从业务系统直接导出、中间没有经过任何规范的原始文件。比如订单日期那一列有的是2025/03/15有的是2025-03-15有的直接写成2025年3月15日甚至还有几条的时间戳精确到了秒金额字段里有1,299.00、有899.00还有纯数字客户名称里同一个分店出现过三种写法带括号的、带横线的、中间多空格的全都能对上最阴间的是某几个数字字段的单元格里前后带着不可见的空格字符肉眼完全看不出区别一旦转类型就报错。我用CSV存下来文件名起的是order_v13_raw.csv830多兆的数据量在电脑上处理倒是没有压力但Pandas光是把文件读进内存就花了差不多二十秒。加载完的第一件事看数据详情。import pandas as pd df pd.read_csv(order_v13_raw.csv, encodingutf-8, low_memoryFalse) print(df.shape) print(df.dtypes) print(df.info(verboseTrue))输出结果里订单编号倒是int64可订单金额、订单日期、客户名称都是object类型。30万行数据里缺失值、重复行、格式混乱全集中在这几个字段上。这份数据的脏基本把日常处理中最常遇见的几类问题都覆盖全了。2.2 脏数据的五大典型问题清单我把检查结果整理成了一张表方便后面逐项处理问题类型涉及字段具体表现影响同一字段多种格式订单日期斜杠、横杠、中文、时间戳混用无法直接排序、聚合金额格式混乱订单金额、单价货币符号、千分位逗号、文本数字无法转成数值计算隐性空白字符客户名称、备注单元格前后有不可见字符分组、匹配时对不上重复记录全部字段同一订单号出现多次统计总量虚高计量单位错位数量部分行把“件”也写进数值类型转换报错这份清单看起来很基础真正上手处理才知道有多折磨。每个问题背后都需要单独设计判断逻辑不能一招平推。2.3 目标产出可以直接做分析的表我给自己定义的交付标准是清洗后的数据保存为order_v13_clean.csv满足三个条件——日期统一成YYYY-MM-DD金额、数量全部是数值类型重复订单只保留最新的一条同时打印一份清洗报告说明每处改动删了多少行、修了多少行。这就要求我在处理每一步时都清楚“为什么会这样处理”而不是随手dropna()一把梭。说实话最初半小时我是有点慌的。看着屏幕上那一排object类型的字段再加上五花八门的日期写法我的脑子里几乎瞬间冒出“要不直接重新造一份规整数据”的念头。但想想这一天的意义还是硬着头皮拆任务拆了下去。3. 字段标准化里的三个决定性选择3.1 为什么是Pandas脚本而不是Excel或数据库数据清洗方案有很多种Excel也能做SQL也能做但我最终选择了Pandas脚本原因很实际第一30万行的数据在Excel里筛选和手动修改机器已经开始卡了更不用说造一个可复用的处理流程第二SQL虽然适合处理结构化数据但对于正则匹配、字段拆分这类操作写起来远不如Python顺手第三也是最重要的我后续的练习方向是用Python做完整的数据分析链路清洗、处理、建模都要在同一套环境里打通。所以在方案选型上我基本没纠结。Pandas的向量化操作加Python的re模块足够覆盖这份数据90%以上的清洗需求。真到了几十个G级别的数据再考虑换Spark或者DuckDB但以30万行的量级Pandas是练习成本最低、反馈最快的选择。3.2 日期字段先统一、再分析日期是最典型的看着不难、做起来一堆坑的字段。我的第一版方案是直接对整列做to_datetime看一眼报错列表就发现不行因为有中文日期混在其中直接转换会抛异常。我最后的设计分三路import re def parse_date(s): if pd.isna(s): return None if isinstance(s, str): if 年 in s: m re.search(r(\d{4})年(\d{1,2})月(\d{1,2})日?, s) if m: return pd.Timestamp(yearint(m.group(1)), monthint(m.group(2)), dayint(m.group(3))) for fmt in (%Y/%m/%d, %Y-%m-%d, %Y-%m-%d %H:%M:%S, %Y/%m/%d %H:%M:%S): try: return pd.to_datetime(s, formatfmt) except ValueError: continue try: return pd.to_datetime(s) except Exception: return None df[订单日期_标准化] df[订单日期].apply(parse_date) df df.dropna(subset[订单日期_标准化])这里有个细节我没有直接把原字段替换掉而是先新增一列标准化的结果检查无误后再覆盖回去。这样做的目的是保留一个“原始痕迹”万一处理结果有问题还能回头对照。从结果看30万行里有1170行日期字段缺失或格式完全无法解析我选择了直接丢弃。为什么不是填充因为对订单数据来说日期是最核心的维度之一缺了它这条记录在多数的分析场景里都用不上硬填一个默认值反而会在后续统计中引入系统性偏差。3.3 金额字段让数字真的变成数字金额字段的处理核心是“从文本到数值”的转换。基于我扫了一眼数据内容整理出几个规则去掉千位分隔符,去掉人民币符号、英文逗号残留、前后空格处理“金额 备注文字”混写的单元格比如1299.00含运费把无法解析的值单独标记出来而不是粗暴替换为0代码如下def to_number(x, field_name金额): if pd.isna(x): return pd.NA if isinstance(x, (int, float)): return x s str(x) s s.replace(, ).replace(,, ) s s.replace(, ).replace(¥, ).strip() m re.search(r(\d\.?\d*), s.replace(,, )) return float(m.group(1)) if m else pd.NA df[金额_标准化] df[订单金额].apply(lambda x: to_number(x, 订单金额))这一步跑完之后又发现了新问题有几十行数据金额解析出来是负数。凑近一看才发现数据里存在“退款单”金额前带着减号。负数的存在本身是合理的但在清洗时不能忽略必须明确告诉下游使用者“这个字段是带符号的净额”否则统计汇总时特别容易算错。金额字段的缺失值我选择了填0。理由和日期正好相反金额是所有汇总分析的必用字段如果保留缺失下游每次 sum 都会跳过最后得到的结果很容易被误解为业务真实值偏低。这里补0有一个前提——缺失的行数量很少只有几百行且不会影响整体趋势。如果缺失比例高于10%补0就是灾难。3.4 去重逻辑不是整行一致才叫重复去重这个动作看着简单其实是当天我思考最久的一个环节。如果只按“整行完全相同”去重那么同一订单号但收货地址写错了一个字的记录就永远不会被识别为重复。实际的业务判断逻辑应该是以订单编号为唯一键如果同一订单编号出现多次保留最新状态的一条其余视作重复。但这里还藏着一个陷阱订单编号本身也有重复的写法比如某些编号带前缀、某些不带。我后面是在清洗报告打印出来之后才发现直接拿原始订单编号做 key 会导致一小部分记录对不上账。保险起见我先用正则把订单编号统一为纯数字格式再作为去重键。df[订单编号_归一] df[订单编号].astype(str).str.replace(r[^0-9], , regexTrue) df df.sort_values(订单日期_标准化, ascendingTrue) # 同一订单编号保留日期最新、状态最完整的一条 df df.sort_values([订单编号_归一, 订单日期_标准化]) df_clean df.drop_duplicates(subset[订单编号_归一], keeplast)至于最后保留的资料需不需要和清洗前其他列做拼接就要看项目边界了。当天我选择了只保留清洗后的最终字段集不额外合并历史版本。4. 耗时最久的一次报错根因是看不见的字符4.1 奇怪的现象数字死活转不成数值前面提到的流程在中午之前就跑得差不多了但有一处问题卡了我将近四十分钟差点让我把电脑合上。问题出在数量这一列。我用pd.to_numeric转换时报错提示没法解析某些值。我去翻了原始数据看到那一列的内容就是3、5、12这类普通的整数凭什么转不了我当时的第一个反应是数据类型问题但astype(int)也同样报错。接着我用df[数量].unique()看了一遍发现值本身没有任何异常。换了.value_counts()之后依然看不到可疑值。最离谱的是显示为3的那一行我单独复制出来手动int(3)又能成功。这种“看起来一样、程序里不一样”的情况做过数据处理的人应该都不陌生——数据里大概率藏着不可见字符。4.2 排查链路从repr到编码再到零宽字符当值打印出来看不出问题的时候第一个动作一定是看它的repr()表示而不是看print()的输出。普通空格在print()下是看不见的但在repr()里会以 的形式展示而其他特殊字符比如 \xa0 或者零宽空格也会在 repr 里露出真面目。我写了几行代码验证bad_values df.loc[df[数量].astype(str).apply(lambda x: not x.strip().isdigit()), 数量] for v in bad_values.head(10): print(repr(str(v)))结果非常明显3\xa0 12\xa0\xa0 5\xa0问题出在\xa0也就是不换行空格。它看起来和普通空格一样但不是 ASCII 空格正则里的\s在某些模式下匹配不到它而str.isdigit()遇到它的判断是 False所以字符串转整数直接失败。我一开始以为是编码问题检查了文件编码确认UTF-8没有异常又怀疑是换行符最后才通过repr锁定了它。4.3 处理方案不猜直接看Unicode码点定位到\xa0后处理其实非常简单。用正则把所有不可见的特殊空白统一替换掉import re def clean_invisible_whitespace(s): if isinstance(s, str): # \xa0: 不换行空格, \u200b: 零宽空格, \u200c/\u200d: 零宽连接符/断符 s re.sub(r[\xa0\u200b\u200c\u200d\u200e\u200f], , s) s re.sub(r\s, , s) return s df[数量_清洗] df[数量].astype(str).apply(clean_invisible_whitespace) df[数量_清洗] pd.to_numeric(df[数量_清洗], errorscoerce)替换之后数量这一列瞬间变得干净总值5700多条记录受影响占0.19%。这个比例虽然不大但如果一开始不处理后面的订单明细汇总一定会带上幽灵式的偏差而且极难察觉。我后来养成了一个习惯凡是字符串字段在清洗阶段统一跑一遍“不可见字符清理”。这一招看起来笨但在真实项目里能省下后面无数个查 bug 的夜晚。4.4 排查方法背后的通用套路这次报错值得单独写一节不是因为它难而是因为它代表了一类非常普遍的坑数据本身没问题才会让人抓狂。只要数据里有一点“看不见”的异常报错信息指向的往往不是真正的原因。我把当天的排查过程拆解成了一套可复用的链条后面再遇到类似问题就按这个顺序来打印数据的dtypes判断是不是类型本身的问题打印unique()和value_counts()找极端值对可疑单元格打印repr()看隐藏字符如果怀疑编码直接看字符码点[hex(ord(c)) for c in s]用最小样本str转换确认能不能复现定位后用正则统一处理并保留清洗前后对照这套链路处理了当天后续出现的“客户名称里零宽空格导致的分组异常”问题。当时的现象是同一个门店名称被分成了两组耗时不长因为我已经知道去查隐藏字符了。5. 上机练习第13天的效率方法三份文档与一张检查清单5.1 为什么笔记记不住报错记录反而最有用这一天练习进行到下午我明显感觉到和前面12天的差异写代码的时间其实没多少大部分时间都在“诊断数据”。中午休息的时候我想如果按照以前的做法把每个函数抄进笔记这些处理逻辑过几天我肯定还是记不住。原因很简单——手抄的笔记是知识点的搬运而这一天我踩的每个坑都带上下文。比如\xa0这个问题如果不和“数字转类型报错”这个场景绑定下次换一批数据我根本不会想到去查它。所以我当天给自己另建了一份文档叫“报错日志”每一条都包含完整报错信息、触发场景、排查步骤、最终解法、可复现的最小代码。这份文档当天就记了八条包括pd.to_numeric遇到混合类型报错、groupby结果和预期不一致、merge之后行数暴增、read_csv读入中文列名乱码等。之后每一天我都会先花十分钟回看这份文档再去练新内容。它比任何教程笔记都更“贴身”。5.2 把一天拆成三个半小时的会话降低启动成本第13天不是一口气硬扛下来的。我把全天的练习拆成了三个独立的会话上午两个小时专门处理数据探索和问题清单下午两个小时做字段标准化和去重晚饭后一个半小时写清洗报告和复盘。每段之间必须休息至少半小时让大脑从“陷入细节”的状态里抽离。原因是清洗数据这件事特别容易让人陷入隧道视野——盯着一个报错看二十分钟不挪眼然后发现原因在旁边另一列。休息的过程反而常常冒出解决方案我就遇到过两次一次是走开倒水的时候想起用repr()看隐藏字符另一次是晚饭前突然意识到订单编号应该先归一化再去重。这种拆法还有一个实际好处每次重新坐到电脑前打开文档的第一眼都是“今天的整体目标是什么”而不是从一堆代码里找刚才做到哪。启动成本变低了就更容易坚持。5.3 收尾三步保存脚本、写说明、重跑全流程当天全部处理结束后我按固定流程做了三件事。第一把所有处理代码整合成一个clean_orders.py删除调试用的临时代码块让脚本从头到尾能顺畅执行第二写一段不超过两百字的处理说明记录数据来源、清洗规则、输出文件格式、已知遗留问题这段说明相当于给未来的自己留的接口文档第三在干净环境下重跑一遍完整脚本从read_csv到输出order_v13_clean.csv保证结果可复现。重跑这一步特别重要。很多练习时能跑通的代码换一个环境或者换一个入口就完全跑不通了。我当天第二次运行时就遇到临时文件路径写死的问题——第一次在IDE里跑没问题第二次直接python clean_orders.py就报找不到文件。调整之后才真正完成了闭环。5.4 通过第13天后练习节奏应该怎么调拿到干净的order_v13_clean.csv之后我又顺手做了三件事统计每天的订单量并画了趋势线计算每个分类的销售额占比再做了个简单的月度对比。这些分析部分我控制在一小时内完成没有深入建模因为当天的核心目标已经达成完成脏数据清洗链路。通过第13天之后我给自己定了一个新的练习原则从第14天开始每个新练的技术方向都必须建立在“不干净的数据”上。学合并操作就准备两份字段对不齐的数据学图表绘制就用刚洗完但仍有部分异常值的表格学着做预测模型就找一个缺日期、带重复的历史数据集。这样做之后练习的体验完全变了。以前做练习是“把教程跑通”现在做练习是“把问题解决”。两种状态听起来差别不大实际上一个是被动跟随一个是主动思考。第13天作为这个转变的临界点也确实没有辜负我给自己安排的这次压力测试。最后再分享一个很小的体会就是那天我用了一整天处理数量、金额、日期的标准化满脑子都是正则表达式但真正记住得最牢的反而是“用 repr 看字符串真实内容”这个小到不行的动作。很多高手的代码和你差的不多差的是面对异常时候的那条排查路径。路径越丰富解决问题越稳。