news 2026/10/19 16:55:22

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据清洗实战: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.csv,830多兆的数据量在电脑上处理倒是没有压力,但Pandas光是把文件读进内存,就花了差不多二十秒。加载完的第一件事,看数据详情。

import pandas as pd df = pd.read_csv("order_v13_raw.csv", encoding="utf-8", low_memory=False) print(df.shape) print(df.dtypes) print(df.info(verbose=True))

输出结果里,订单编号倒是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(year=int(m.group(1)), month=int(m.group(2)), day=int(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, format=fmt) 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 金额字段:让"数字"真的变成数字

金额字段的处理,核心是“从文本到数值”的转换。基于我扫了一眼数据内容,整理出几个规则:

  1. 去掉千位分隔符,
  2. 去掉人民币符号¥、英文逗号残留、前后空格
  3. 处理“金额 + 备注文字”混写的单元格,比如1299.00(含运费)
  4. 把无法解析的值单独标记出来,而不是粗暴替换为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]", "", regex=True) df = df.sort_values("订单日期_标准化", ascending=True) # 同一订单编号保留日期最新、状态最完整的一条 df = df.sort_values(["订单编号_归一", "订单日期_标准化"]) df_clean = df.drop_duplicates(subset=["订单编号_归一"], keep="last")

至于最后保留的资料需不需要和清洗前其他列做拼接,就要看项目边界了。当天我选择了只保留清洗后的最终字段集,不额外合并历史版本。

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["数量_清洗"], errors="coerce")

替换之后,数量这一列瞬间变得干净,总值5700多条记录受影响,占0.19%。这个比例虽然不大,但如果一开始不处理,后面的订单明细汇总一定会带上幽灵式的偏差,而且极难察觉。

我后来养成了一个习惯:凡是字符串字段,在清洗阶段统一跑一遍“不可见字符清理”。这一招看起来笨,但在真实项目里能省下后面无数个查 bug 的夜晚。

4.4 排查方法背后的通用套路

这次报错值得单独写一节,不是因为它难,而是因为它代表了一类非常普遍的坑:数据本身没问题才会让人抓狂。只要数据里有一点“看不见”的异常,报错信息指向的往往不是真正的原因。

我把当天的排查过程拆解成了一套可复用的链条,后面再遇到类似问题就按这个顺序来:

  1. 打印数据的dtypes,判断是不是类型本身的问题
  2. 打印unique()和value_counts(),找极端值
  3. 对可疑单元格打印repr(),看隐藏字符
  4. 如果怀疑编码,直接看字符码点[hex(ord(c)) for c in s]
  5. 用最小样本str转换,确认能不能复现
  6. 定位后用正则统一处理,并保留清洗前后对照

这套链路处理了当天后续出现的“客户名称里零宽空格导致的分组异常”问题。当时的现象是同一个门店名称被分成了两组,耗时不长,因为我已经知道去查隐藏字符了。

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 看字符串真实内容”这个小到不行的动作。很多高手的代码和你差的不多,差的是面对异常时候的那条排查路径。路径越丰富,解决问题越稳。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 16:50:48

AI文生视频实战:原理、提示词写法、翻车排查与场景边界

上个月我把一句话丢进AI视频生成工具——那句话大概是“一只银色的机械狐狸在废弃火车站的月台上抖落身上的雪”。当时我盯着生成进度条,心态其实很平淡:能出个能当动态壁纸的东西就行。结果第一版渲染出来,狐狸尾巴上每一绺毛都带着风雪飘散…

作者头像 李华
网站建设 2026/10/19 16:50:44

BTP Event Mesh

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 16:49:37

端侧多模态实时对话系统开发实战:9B模型的流式架构与性能调优

上个月我在做“某端侧多模态实时对话 Demo”的性能优化,项目代号模拟项目X,核心是把一个 9B 参数的视觉语言模型塞进手机,实现“摄像头看得到、麦克风听得见、离线也能聊”的交互体验。第一版跑通时大家都很兴奋,但真到演示环节就…

作者头像 李华
网站建设 2026/10/19 16:46:51

Qt项目VS2019发布实战:windeployqt+Inno Setup一键打包

1. 项目概述:为什么Qt项目发布不是“编译完就完事”?在VS2019环境下用Qt开发完一个功能完整的桌面应用,比如一个带图表分析、数据库连接和多语言界面的工业数据采集工具,点下“生成解决方案”看到绿色的成功提示,很多人…

作者头像 李华
网站建设 2026/10/19 16:46:49

模型微调在AI+场景的落地实践:判断、流程与避坑指南

做过不少“AI落地”的项目之后,我得先泼一盆冷水:模型微调这件事,被严重高估了,也被严重低估了。高估的是它作为“万能解药”的救场能力,低估的是它真正落地时需要的工程颗粒度。很多团队一听“场景定制”,…

作者头像 李华
网站建设 2026/10/19 16:46:48

FileZilla+SFTP:Windows 11与Ubuntu 20.04文件互传全攻略

1. 为什么选FileZilla做跨系统文件互传做Linux和Windows双系统的小伙伴,十有八九都会遇到一个尴尬场景:Windows 11下随手存的资料、代码包、安装程序,到Ubuntu 20.04里却没带过来。用U盘拷吧,来回插拔确实麻烦,大文件还…

作者头像 李华