
上周有人拿了一份订单导出表给我问能不能帮忙做一下数据清洗。这份 CSV 的打开方式很典型日期列里横杠和斜杠混用金额列里夹着“元”和全角空格用户 ID 被 Excel 转成了科学计数法末尾还有二十几行看似重复、主键却不完全一样的记录。我一边处理一边意识到数据清洗这个词听起来是在做“打扫卫生”实际是在解决信任问题清洗后的数据必须让下游程序可以无条件依赖。很多人学 pandas 数据清洗时第一反应是找“哪个 API 能删空值”“哪个参数能去重”这没有错但容易把数据清洗理解成几个孤立的动作。真正的问题通常不是不会用函数而是不清楚要洗到什么程度也不清楚每一步为什么这样做。数据清洗不是一次性的体力活而是一套可以沉淀成流程、可以批量复用、可以在出错时按顺序排查的工程实践。1. 数据清洗真正要解决的不是“脏”而是“不可信”1.1 表面功夫删空值、去重复、改格式数据清洗最容易看到的部分就是处理缺失值、重复值和格式问题。比如用dropna()删掉空行用drop_duplicates()去掉重复记录用astype()改字段类型。这些操作单独看都很简单任何一个 pandas 教程都会讲。但落到真实场景时麻烦的是这些操作不能随便用。删除空值之前你要先确认这些空值是不是真的有业务含义去重之前你要确认“重复”的业务定义是两列完全相同还是三列组合相同转换类型之前你还要确认那些“看起来是数字”的字段是否真能被安全转换。也就是说函数只是工具真正重要的是你定义清楚“干净”的标准。1.2 深层目标让下游可以无脑消费如果只是给自己看数据没那么干净也能凑合。但数据清洗的难点通常在于下游还有别的程序等着消费这份数据做报表的同事、训练模型的脚本、对外提供的接口、量化回测的策略。任何一个环节遇到不该出现的NaN、变成字符串的数字、统一不了的日期格式都会中断。所以我的判断是数据清洗的本质是把数据从“人眼能看懂”变成“程序能无条件信任”。这里的“无条件”三个字很重要。程序不会像人一样一边看一边猜它只会按照类型、格式、空值规则往下跑。清洗越接近“规则明确、边界清楚”下游就越省心。很多人也犯过类似的错拿到一份数据先把空值删掉然后输出 CSV 就交差了。结果下游做统计的人发现某个月的订单金额突然少了一大截追查下来才意识到那批订单的金额列不是空值而是被 Excel 转成了科学计数法读进来之后变成了字符串。从那之后我才明白清洗的每一个动作背后都要有一个业务原因否则就是一次没有把握的冒险。1.3 回答一个问题清洗到什么程度才算完成很多人的困惑是“到底要洗到什么程度”。一个可执行的判断标准是把清洗后的数据交给一个完全不了解原始业务的人他不看备注也能直接做统计就基本算合格了。具体来说包括每列类型符合预期日期是日期数值是数值ID 是字符串。缺失值要么被填充要么被标记不能留下模糊的空值。重复记录按业务主键去重且知道保留哪一条。列名统一、索引稳定、导出格式明确。清洗规则可以被重新执行而不是只存在某一个人的记忆里。这个标准不复杂但能帮你判断“要不要继续洗”。2. 环境不是越新越好先跑通最小闭环2.1 Python 与 pandas 的安装版本适配比新版本更重要标题虽然挂着“2026 年最新版”但落到实操上真正决定体验的不是某个新接口而是你和当前环境的适配。pandas 每年都在迭代接口会小幅调整但缺失值、重复数据、类型转换、列名规范这几件事核心思路没有大变。所以不要被“最新版”三个字带偏先把环境跑通比追求最新版本更重要。实际项目里我不建议直接在系统全局环境里pip install pandas。更稳妥的做法是给每个项目建一个虚拟环境python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install --upgrade pip pip install pandas pyarrow openpyxlpyarrow是为了后面导出parquet格式openpyxl是为了读写 Excel。如果只是学习装 pandas 够用如果要进入真实项目这两个建议一起装上。如果遇到“pandas 安装不成功”“Python 3.10 应该配哪个 pandas 版本”之类的问题排查思路是先去python --version确认解释器版本再确认 pip 指向的是不是同一个 Python最后用pip check检查依赖冲突。pandas 2.x 是这些年来常见的稳定大版本但具体能不能装取决于你本机的 Python 版本和操作系统不建议硬按网上旧教程锁版本。2.2 用最小样例验证数据读入环境装好之后先用一个小文件把读入跑通不要一上来就处理几十列的大表。创建一个样例文件比如orders_raw.csv包含几行典型脏数据然后执行import pandas as pd df pd.read_csv(orders_raw.csv, encodingutf-8-sig) print(df.shape) print(df.head()) print(df.dtypes)这一步的目的是确认三件事文件能不能读进来编码有没有乱码类型有没有按预期解析。如果读进来的结果已经和你预期差很远后面的清洗就没有意义。2.3 读入阶段就做掉一半脏数据很多人不知道pandas 在read_csv阶段就能处理很多脏数据问题根本不需要等读入后再补救。常见写法是df pd.read_csv( orders_raw.csv, encodingutf-8-sig, sep,, na_values[, NULL, null, N/A, NA], keep_default_naTrue, )na_values可以把常见的空值标记统一识别成NaN避免同一列里同时出现空字符串、NULL、N/A三种缺失形态。sep用于指定分隔符有的文件是分号有的是制表符不指定的话可能整列挤在一起。encoding则解决中文乱码utf-8-sig是处理带 BOM 的 CSV 比较稳妥的选择也可以根据文件实际情况调整成gbk。读取参数是第一个排查点。很多人清洗结果不对不是函数写错而是数据在进入 DataFrame 的时候就已经错了。3. 三类高频脏数据缺失、重复、格式不一致3.1 缺失值处理删除、填充还是标记缺失值是最常见的问题但处理方式不能一概而论。先用df.isna().sum()看每一列的缺失数量再做出判断。处理方式适用场景注意点直接删除缺失比例极低或缺失值无业务含义删除前先记录行数变化填充数值列可用中位数类别列可用“未知”填充会改变分布要记录规则标记缺失缺失本身可能是信息新增_is_missing列不污染原字段一个比较稳妥的流程是先删掉缺失比例过高的列再对数值列和类别列分别处理最后给那些“缺失可能有含义”的字段加标记。# 删除缺失比例超过 60% 的列 threshold 0.6 df df.dropna(threshint(len(df) * threshold), axis1) # 数值列填充中位数类别列填充“未知” df[amount] df[amount].fillna(df[amount].median()) df[category] df[category].fillna(未知) # 对缺失可能有含义的列单独打标 df[income_missing] df[income].isna().astype(int)这里最想强调的是不要看到缺失就只想着删。缺失比例高不一定是数据质量差可能是业务上本来就不该有值。比如“会员到期时间”对未续费用户来说就是空的这种空值是有业务含义的填一个默认日期反而是错误的。3.2 重复数据处理按业务主键去重drop_duplicates()是最容易滥用去重的函数。很多人默认对整个 DataFrame 去重结果把本该保留的多条记录也删了。正确做法是先想清楚“重复”的业务定义。比如订单表里user_id和order_id两列都相同才算是重复那代码是df df.drop_duplicates(subset[user_id, order_id], keepfirst)如果有人问“pandas 如果指定两列的值均相同则取第一条数据即可”答案就是这个subset参数。keepfirst表示保留第一次出现的记录keeplast则是保留最后一次。实际项目中我建议先执行duplicated_count df.duplicated(subset[user_id, order_id]).sum() print(f重复记录数: {duplicated_count})看到重复数之后再决定去重方式比直接调用drop_duplicates()要安全得多。重复数据的出现往往反映上游系统有问题如果只是默默地删掉问题还会再来。3.3 类型与格式统一日期、数值、字符串热词里“pandas 数据类型转换”被搜索很多次说明这是高频需求。类型转换的核心不是记参数而是记住一条原则先转换再检查最后处理转换失败的值。日期列统一用pd.to_datetime并设置errorscoerce这样无法解析的日期会变成NaT而不是直接报错中断df[order_date] pd.to_datetime(df[order_date], errorscoerce) print(df[order_date].isna().sum())日期解析失败会变成NaT后续需要和缺失值一样处理。如果失败的行不多可以结合业务决定删除或填充如果很多要回到原始数据确认输入格式不要硬洗。数值列用pd.to_numeric同样设置errorscoercedf[amount] pd.to_numeric(df[amount].str.replace(元, ).str.strip(), errorscoerce)字符串 ID 类字段如果直接astype(str)NaN会变成字符串nan这往往不是我们想要的。所以要先处理缺失再转换df[user_id] df[user_id].fillna(unknown).astype(str)格式清洗还包含一个很多人忽略的点不可见字符。金额、姓名、备注列里可能混着全角空格、换行符、零宽字符。可以用字符串处理函数清理df[remark] df[remark].fillna().astype(str).str.replace(r\s, , regexTrue)这一步不是每次都需要但如果你发现“明明两个值看起来一样drop_duplicates却没有去重”优先怀疑的就是不可见字符。4. 从“干净表”到“结构化数据模型”4.1 规范列名和索引清洗完脏值之后下一个层次是让表本身更有结构。第一步是列名统一。不同系统导出的列名五花八门有大小写、有空格、有中文括号。进入建模前最好统一成小写加下划线的风格df.columns ( df.columns .str.strip() .str.lower() .str.replace(r\s, _, regexTrue) .str.replace(r[^\w\u4e00-\u9fa5], _, regexTrue) )索引也一样。如果数据带有稳定的唯一标识比如order_id可以考虑设为索引df df.set_index(order_id)要注意只有确认这个字段唯一且非空时才适合设为索引。如果只是想快速检索也可以不设索引保持普通列即可。4.2 派生字段与口径统一很多时候原始字段满足不了分析需求需要从现有字段中生成新字段。例如从订单日期里提取月份df[order_month] df[order_date].dt.to_period(M)或者根据金额和数量计算单价df[unit_price] df[amount] / df[quantity]这类派生字段能让后续分析更直接但也带来一个要求口径必须明确。单位是什么空值怎么处理除数为 0 怎么处理都要有统一规则。最怕的是不同批次的数据里同一字段的计算口径不一致导致统计结果对不上。4.3 输出规范中间层数据要可溯源、可复现清洗完成后输出格式要尽量做到“可溯源、可复现”。可溯源的意思是下游看到一份数据知道它是从哪里来的、中间经历了哪些处理。可复现的意思是下次拿到新的原始数据用同一套脚本能产出同样结构的结果。保存文件时推荐优先考虑parquet而不是 CSV。parquet 保留了数据类型读取快体积小和后续建模、批处理链路也更搭df.to_parquet(orders_clean.parquet, indexFalse)如果同事还在用 Excel 查看结果可以再导出一份 CSVdf.to_csv(orders_clean.csv, indexFalse, encodingutf-8-sig)如果团队里有人用 Spark清洗后的数据存成 parquet 也能直接衔接feather 适合单机多语言场景但跨工具兼容优先选 parquet。需要提醒的是旧版 Excel.xls格式有 65536 行的上限如果清洗后的数据行数较多不要硬往一个 Excel 表里塞直接用 CSV 或 parquet 更合适。5. 用一套可复用的清洗流水线避免每次重写5.1 清洗流程拆成五个步骤很多人的清洗脚本是一次性写的数据跑完就丢。下次拿到新文件又要重新看字段、重新踩坑。更合理的做法是把清洗流程固定成一个“五步法”读入设置encoding、sep、na_values、dtype。探查看shape、info、describe、缺失和重复情况。修复处理缺失、重复、类型、格式、异常值。输出统一列名和索引导出 parquet 或 CSV。记录把清洗规则和关键统计量写入日志或文档。这五步不是线性的死流程而是一个可循环的框架。每次拿到新数据先跑探查再决定修复避免一上来就套用上次的规则。5.2 单文件脚本化把清洗逻辑封装成一个函数是流水线的基础def clean_orders(file_path: str) - pd.DataFrame: df pd.read_csv( file_path, encodingutf-8-sig, na_values[, NULL, null, N/A, NA], ) df df.drop_duplicates(subset[user_id, order_id], keepfirst) df[order_date] pd.to_datetime(df[order_date], errorscoerce) df[amount] pd.to_numeric(df[amount], errorscoerce) df df.dropna(subset[amount]) df.columns df.columns.str.strip().str.lower().str.replace(r\s, _, regexTrue) return df函数的好处是逻辑可以被测试可以被其他脚本调用也可以被复用处理多个文件。5.3 批量处理多个文件与日志真实工作中经常是一次拿到十几个 CSV每个文件格式还不完全一样。用pathlib可以批量处理from pathlib import Path