Pandas十步数据清洗实战:从脏数据到干净数据集
讲个真事上周帮一个做电商运营的朋友处理订单数据她发来一张八百多MB的Excel说是从后台导出的结果一打开傻眼了客户名称有的带空格有的全角半角混着写、订单日期一部分是文本一部分是日期格式、金额列里竟然混着约100元这种描述、还有重复订单和负数金额。她说这数据她们团队三个运营已经手工改了三天眼睛都快瞎了还剩一半没弄完。我接手后写了个Pandas脚本十分钟跑完出来的数据干净得可以直接进分析模型。这种场景你迟早也会遇到。真实业务里的数据几乎不会像教程里那么整洁要么是从多个渠道汇总的要么是历史系统导出的要么是人工录入带各种意想不到的惊喜。而数据清洗就是整个数据分析流程里最耗时耗力、却又最不能被跳过的一步。行业内有个共识一个数据项目里清洗和预处理要占掉60%到80%的时间。这篇文章我要系统性讲清楚一件事如何用Pandas通过一套固定的十步流程把一份脏乱差的数据整理成可以直接分析和建模的干净数据集。我会把每一步的代码、思路、踩坑点全部拆开讲并且附带一个完整的、可直接运行的实战案例。适合刚学完Pandas基础语法、想真正上手处理真实数据的读者也适合被Excel手工清洗折磨得不行的运营、产品和数据岗位的同学。1. 整体设计思路为什么清洗数据要用Pandas而不是Excel或SQL在开始之前先想明白一个问题为什么数据清洗这件事大家最终都转向了Pandas我自己最早也是用Excel洗数据的VLOOKUP、分列、查找替换、条件格式用得很溜但后来数据量上了百万行之后Excel直接卡到怀疑人生。SQL也能做清洗但很多脏数据的处理需要复杂的流程控制、正则匹配和灵活的文本处理写SQL非常痛苦。Pandas解决的是这三个核心痛点一是内存内DataFrame操作极快百万行级别的数据清洗也就是几秒钟到几十秒钟的事二是语法表达力强清洗逻辑用几行代码就能写清楚可读性好三是生态完善读取各种格式、缺失值处理、类型转换、分组聚合、正则替换全部开箱即用。清洗流程本身是有方法论可循的。我概括的十步流程是先摸底数据全貌再处理重复数据接着规范化列名和类型处理文本格式和统一取值修复缺失值最后处理异常值。这十步的顺序是有讲究的——比如异常值判断往往依赖类型转换和文本规范化的结果所以类型清洗必须排在异常值处理前面。先把基础打牢后面每一步才不会出错。2. 环境准备与工具选型2.1 安装Pandas及配套环境如果你还没装Pandas这一步先把环境搞定。我用的是Python 3.9以上版本Pandas推荐使用2.x以上版本因为2.x在很多API上做了优化性能更好对数据类型支持也更强。# 建议使用国内镜像源安装速度更快 pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple pip install openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple安装openpyxl是因为它负责处理xlsx格式的读写Pandas本身不带这个功能。如果还要处理其他格式可以顺手装一下xlrd旧的xls格式和xlwt写xls格式。如果你用的是Anaconda发行版Pandas通常已经自带。可以用下面的方式确认版本import pandas as pd print(pd.__version__)2.2 为什么选Pandas而不选其他工具前面提到了Excel和SQL的局限这里再补充一点在真实项目里数据清洗往往不是一次性的数据源会定期更新清洗逻辑需要反复运行。用Pandas写成的清洗脚本可以固化沉淀成ETL流程每次数据来了自动跑一遍。而Excel和SQL的清洗操作很难做到这种程度的可复用性。2.3 快速搭建你的第一个清洗骨架任何一次数据清洗项目我的建议是先搭一个骨架脚本包含固定的导入、读取、概览、保存这几个环节。这个骨架可以反复复用import pandas as pd import numpy as np # 读取数据 df pd.read_excel(raw_data.xlsx, engineopenpyxl) # 概览数据 print(数据形状, df.shape) print(列名, df.columns.tolist()) print(前5行数据) print(df.head()) print(数据类型信息) print(df.info()) # 保存清洗结果 df.to_csv(clean_data.csv, indexFalse, encodingutf-8-sig)encodingutf-8-sig这个小细节要注意不加sig的话用Excel打开CSV文件中文会乱码。3. 十步数据清洗全流程从脏数据到干净数据现在进入正文的核心部分。我一步一步拆解每一步都会给出代码、运行逻辑和踩坑心得。为了让你更直观地理解我准备了一个模拟的销售订单数据集里面埋了各种经典的脏数据问题我边清洗边解释。3.1 第一步加载数据并做全方位摸底拿到任何一份数据第一件事绝对不是动手清洗而是先摸清楚数据长什么样。这一步的目标是回答几个问题数据量多大有哪些列每列是什么类型有没有明显的缺失和异常。我通常会这样操作# 读取一个真实的销售订单数据 df pd.read_excel(sales_orders.xlsx) # 1. 看数据规模和列名 print(数据规模, df.shape) print(列名列表, df.columns.tolist()) # 2. 看每列的数据类型、非空数量 print(\n数据类型及缺失概况) print(df.info()) # 3. 看数值列的统计摘要 print(\n数值列统计摘要) print(df.describe()) # 4. 看文本列有哪些取值通过unique快速了解 print(\n订单状态列取值) print(df[订单状态].unique()) # 5. 检查每列缺失值数量 print(\n缺失值数量) print(df.isnull().sum())这一步的输出信息非常关键。通过df.info()你可以一眼看出哪些列被识别成了object类型这通常意味着类型有问题或者是纯文本通过df.describe()可以看到数值列的分布范围如果最大值是正常值的几百倍那说明有极端异常通过缺失值统计则能确认缺失集中在哪些字段上。我见过很多人拿到数据不先摸底就直接用dropna()咔咔往下扔结果把有效数据也删掉了大半这就是典型的调研不足。摸底完之后你心里要对数据状态有数接下来每一步才知道该重点处理哪里。3.2 第二步处理重复数据重复数据是业务数据里最常见的问题由系统重复导出、用户重复提交、多表关联产生的笛卡尔积等导致。Pandas提供了两个层面的重复检测全行重复和指定列重复。# 1. 检查完全重复的行 print(完全重复的行数, df.duplicated().sum()) # 2. 删除完全重复的行 df df.drop_duplicates() # 3. 检查指定列重复比如订单号应该唯一 print(订单号重复条数, df[订单号].duplicated().sum()) # 4. 保留指定列重复行的最后一条记录 df df.drop_duplicates(subset[订单号], keeplast)keep参数有3个可选值first保留第一条默认、last保留最后一条、False所有重复的都要不保留任何一条。实际踩坑提醒判断重复要结合业务背景。订单号重复通常意味着数据导出了两次可以直接删但如果客户ID重复那每个客户对应多条订单是正常的不能用drop_duplicates去删。清洗之前一定要想清楚哪个维度是真正需要唯一的。3.3 第三步列名规范化列名看着是小问题实际上对后面所有操作都有影响。如果列名里有空格、大小写不统一、中英文混写写着写着就会报KeyError。我建议列名统一为小写字母下划线的snake_case风格或者直接规范成统一的中文命名规则。# 查看当前列名 print(df.columns.tolist()) # 统一去除空格、转为小写 df.columns df.columns.str.strip().str.lower().str.replace( , _) # 如果是中文列名可以统一去掉多余空格 df.columns [col.strip() for col in df.columns] # 更细的规范化替换中文括号和英文括号 df.columns df.columns.str.replace(, ().str.replace(, ))这一步常见的坑在于有些Excel文件的列名在导出时会自动加上看不见的字符比如\u00a0这种特殊空格肉眼看不出来运行的时候就报KeyError。我的建议是用repr(df.columns.tolist())打印列名列表能看到不可见字符的真实样子。3.4 第四步数据类型转换数据类型错误是脏数据里隐蔽性最强的一类。最典型的情况是明明应该是数值型的列被读成了object明明应该是日期时间的列被读成了字符串。如果不转换后面的排序、聚合、计算全都会出问题。# 1. 数值列转换用 to_numeric errorscoerce 将非法值转为NaN df[单价] pd.to_numeric(df[单价], errorscoerce) df[数量] pd.to_numeric(df[数量], errorscoerce) # 2. 日期列转换 df[订单日期] pd.to_datetime(df[订单日期], errorscoerce, format%Y-%m-%d)errorscoerce的意思是遇到无法解析的值不要报错而是返回NaN。这样我们能保证后续代码继续执行而所有转换失败的值会被标记为缺失值稍后统一处理。如果不加这个参数一行脏数据能让整段代码崩溃。日期转换还有一个参数要注意——format。如果日期字符串的格式是标准的2024-01-15Pandas的to_datetime可以不传format自动推断但速度慢如果是2024/01/15、20240115这类不标准格式建议指定format解析更快更准。还有一类非常隐蔽的类型问题数值列里混入了千分位逗号。比如1,234Pandas会把它当作字符串处理用to_numeric转换的时候会得到NaN。这种要先去掉逗号再做类型转换df[金额] df[金额].astype(str).str.replace(,, ) df[金额] pd.to_numeric(df[金额], errorscoerce)3.5 第五步处理文本列中的空格和格式不统一问题文本列的脏主要体现在几个方面首尾空格、中间多余空格、大小写不统一、全角半角混用。这些看起来不影响数据含义但一旦用groupby去汇总相同含义的数据会被拆成多个组统计结果直接失真。# 去除首尾空格 df[客户名称] df[客户名称].str.strip() df[备注] df[备注].str.strip() # 去除中间多余空格把连续多个空格替换成一个 df[客户名称] df[客户名称].str.replace(r\s, , regexTrue) # 全角转半角处理全角数字、英文字母 def full_to_half(s): if not isinstance(s, str): return s result [] for char in s: code ord(char) # 全角字符范围0xFF01~0xFF5E对应半角范围0x21~0x7E if code 0x3000: code 32 elif 0xFF01 code 0xFF5E: code - 0xFEE0 result.append(chr(code)) return .join(result) df[客户名称] df[客户名称].apply(full_to_half) # 统一大小写例如省份缩写 df[省份] df[省份].str.upper()这里需要补充一个细节为什么全角转半角这么重要因为实际业务中用户可能用中文输入法在数字输入框里输了数字存储时保存为全角的后面任何数值操作都会失败。全角转半角的函数在清洗身份证号、手机号、金额时经常用到建议封装成工具函数反复调用。文本格式统一还包括省份名称的简称和全称混用广东和广东省、日期时间格式的混用2024-01-01和2024.01.01。这类问题需要依据业务的标准值做映射。3.6 第六步缺失值处理缺失值是数据清洗里的重头戏。但不是所有缺失都要填充关键是要理解缺失值背后的业务含义。缺失可以分三类完全随机缺失、可预测缺失、系统性缺失。处理策略也有三种删除、填充固定值、用统计量填充。# 1. 先看缺失分布 missing df.isnull().sum() print(missing[missing 0]) # 2. 缺失比例很高且无分析价值的列——直接删除 drop_cols [发票号] if 发票号 in df.columns: df df.drop(columns[发票号]) # 3. 数值列用中位数或均值填充 df[数量] df[数量].fillna(df[数量].median()) df[单价] df[单价].fillna(df[单价].mean()) # 4. 分类变量用众数填充 df[订单状态] df[订单状态].fillna(df[订单状态].mode()[0]) # 5. 时间序列用前向填充适合订单日期这类递增字段 df[订单日期] df[订单日期].fillna(methodffill) # 6. 业务上有含义的缺失——填充特定值 df[折扣] df[折扣].fillna(0)这里有几个心得必须单独拎出来讲。数值列填充我优先用中位数而不是均值。原因是中位数对异常值不敏感如果数据里存在极端值均值会被拉偏用均值填充等于把异常值的影响又扩散到了缺失值上。分类变量用众数填充要先看众数是什么。如果数据里有大量的已完成那用众数填充缺失的订单状态基本合理但如果有部分缺失意味着未发起这种特殊语义就不能盲填。还有一个原则是如果进入建模阶段填充策略要记录下来写成文档。因为你填充了哪些值直接影响模型之后上线时的数据预处理逻辑。我在生产环境里会把所有填充规则存成一个JSON配置文件方便复现。3.7 第七步统一文本内容的取值这一步和第五步容易混淆我单独拿出来说。第五步解决的是格式问题——空格、大小写、全角半角这一步解决的是语义口径问题——同一个对象在数据里有多种写法。比如# 看备注列里有哪些不同写法 print(df[支付方式].value_counts()) # 输出可能是 # 微信支付 532 # 微信 123 # wechat 45 # 支付宝 301 # 支付宝支付 120这时候就需要做映射和替换把同义的取值统一为一个标准值。写法是定义一个映射字典用replace统一处理payment_map { 微信: 微信支付, wechat: 微信支付, WeChat: 微信支付, 支付宝: 支付宝, 支付宝支付: 支付宝, ali: 支付宝, 银行卡: 银行卡, 银联: 银联, } df[支付方式] df[支付方式].replace(payment_map)涉及多个值的替换不建议用多次df[列名].replace()连写而要用映射字典一次替换完成代码简洁且不会漏。如果你要处理的是包含子串的匹配比如地址列里有北京市和北京需要用正则或str.contains做规则。这种场景在数据清洗里太常见了我放一个正则替换的例子import re def normalize_address(addr): if not isinstance(addr, str): return addr # 去掉省/市/区字样后面的冗余空格 addr re.sub(r([省市区县])\s, r\1, addr) # 统一道路名 addr addr.replace(路, 路).replace(街, 街) return addr df[收件地址] df[收件地址].apply(normalize_address)3.8 第八步异常值识别与处理异常值不是错误值它是真实存在但与整体分布差异极大的数据点。处理异常值要特别谨慎因为异常的可能是数据本身也可能是业务上的特殊情况比如大客户的超大额订单。识别异常值的方法我常用三种第一种是描述统计法用describe()查看四分位数和极值指标超过合理范围就标出来。第二种是3σ法则适合近似正态分布的数据def detect_outliers_iqr(series): Q1 series.quantile(0.25) Q3 series.quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR return (series lower_bound) | (series upper_bound) outlier_mask detect_outliers_iqr(df[订单金额]) print(异常订单数量, outlier_mask.sum())第三种是业务规则法比如订单数量不能为负数单价不能超过某个上限。# 业务规则异常示例 df[订单金额] df[订单金额].abs() df.loc[df[数量] 0, 数量] np.nan处理策略上异常值可以删除、截断、填充或单独标记。删除是最简单的方式但如果异常值比例较高删除会造成信息损失。我建议的做法是先单独把异常样本打印出来看业务含义如果确认是录入错误就修正如果异常值本身有价值比如大额订单是真实存在的就保留并用一个布尔列is_outlier标记供后续分析时参考。3.9 第九步数据筛选、排序与去重后的最终检查前八步做完之后数据大体已经干净了但还有一个环节不能省最终要保留哪些列、以什么顺序输出、按什么字段排序。这一步决定了交付数据集的最终形态。# 1. 筛选需要的列 keep_cols [订单号, 订单日期, 客户名称, 省份, 支付方式, 商品名称, 单价, 数量, 订单金额, 订单状态] df df[keep_cols] # 2. 按日期和订单号排序 df df.sort_values([订单日期, 订单号], ascending[True, True]) # 3. 重置索引 df df.reset_index(dropTrue) # 4. 最终体检确认没有残留问题 assert df[订单号].is_unique, 订单号存在重复 assert df[订单金额].notnull().all(), 订单金额存在缺失 assert (df[订单金额] 0).all(), 订单金额存在负值assert断言是很好的数据质量把关手段。写完清洗脚本后把所有必须满足的条件写成一串断言跑完能通过就说明数据质量合格了不通过就返回检查。这个习惯能帮你守住数据质量的底线。另外注意reset_index(dropTrue)这步很多新手会漏。如果不重置索引删除行之后的索引是断裂的后面如果再按索引筛选或合并数据会出问题。3.10 第十步导出清洗结果与沉淀清洗规则最后一步是输出但输出不只是写一个文件那么简单。实际项目里你需要考虑输出格式、编码、以及清洗规则的沉淀。# 导出清洗后的数据去除索引列避免Excel打开多一列 df.to_excel(clean_sales_data.xlsx, indexFalse, engineopenpyxl) # 导出为CSV格式utf-8-sig避免Excel打开乱码 df.to_csv(clean_sales_data.csv, indexFalse, encodingutf-8-sig) # 单独导出异常值记录供业务方确认 abnormal df[outlier_mask] abnormal.to_excel(异常值待确认.xlsx, indexFalse) # 打印最终的清洗报告 print(f清洗完成原始数据 {raw_count} 行清洗后 {len(df)} 行删除了 {raw_count - len(df)} 行)清洗规则沉淀是我特别想强调的一点。我曾在一个项目里连续四个月每周都要清洗同类数据最初每次都要重新翻代码想当时为什么要这样处理后来把规则写成了一份说明文档包括每一步的目标、规则、参数和确认过的业务例外效率高了很多。4. 完整实战一份销售订单数据的清洗全过程上面分了十步讲解每一步都是单独的知识点。但真实场景里十步是连贯的一整套流程。我用前面反复提到的那份销售订单数据把完整的清洗代码串起来让你看到它们是如何协同工作的。import pandas as pd import numpy as np import re # # 模拟一份脏销售订单数据替换为真实文件路径即可 # raw_data { 订单号: [A001, A002, A002, A003, A004, A005, A006, A006, A007], 订单日期: [2024-01-05, 2024-01-06, 2024-01-06, 2024.01.08, 2024-01-10, 2024-01-11, 2024-01-12, 2024-01-12, zzz], 客户名称: [ 张三, 李四, 李四, 王五, 赵六, 孙七 , 周八, 周八, 吴九], 省份: [广东, 广东省, 广东, 浙江, 浙江省, 江苏, 江苏, 江苏, 江苏], 支付方式: [微信, wechat, wechat, 支付宝, ali, 微信支付, 银行转, 银行转, 货到付款], 商品名称: [手机, 手机壳, 手机壳, 耳机, 耳机, 数据线, 充电器, 充电器, 手机], 单价: [1,000, 29, 29, 199, 199, 15, 89, 89, 999], 数量: [1, 2, 2, 1, -3, 5, 10, 10, 0], 订单金额: [1000, 58, 58, 199, -597, 75, 890, 890, 999], 订单状态: [已完成, 已完成, 已完成, 进行中, None, 已完成, 已完成, 已完成, 已取消], } df pd.DataFrame(raw_data) print( 原始数据 ) print(df) print(df.info()) # # Step 1: 数据摸底 # print(\n 缺失值统计 ) print(df.isnull().sum()) print(重复行数, df.duplicated().sum()) # # Step 2: 去除重复行 # df df.drop_duplicates() # # Step 3: 列名规范化这里列名已是规范格式演示清洗思路 # df.columns df.columns.str.strip().str.lower() # # Step 4: 类型转换 # # 单价去掉千分位逗号再转数值 df[单价] df[单价].astype(str).str.replace(,, ).str.strip() df[单价] pd.to_numeric(df[单价], errorscoerce) df[数量] pd.to_numeric(df[数量], errorscoerce) df[订单金额] pd.to_numeric(df[订单金额], errorscoerce) # 日期解析尝试多种格式 df[订单日期] pd.to_datetime(df[订单日期], errorscoerce) # # Step 5: 文本格式统一 # df[客户名称] df[客户名称].str.strip() # 省份统一去掉空格映射为标准的省名 province_map {广东: 广东省, 广东: 广东省} df[省份] df[省份].str.strip().replace({ 广东: 广东省, 浙江: 浙江省, }) # # Step 6: 缺失值处理 # df[订单状态] df[订单状态].fillna(待确认) # # Step 7: 分类取值统一 # payment_map { 微信: 微信支付, wechat: 微信支付, ali: 支付宝, } df[支付方式] df[支付方式].replace(payment_map) # # Step 8: 异常值识别与处理 # # 数量为负数或0的问题修正负数取绝对值0视为缺失 df[数量] df[数量].abs() df.loc[df[数量] 0, 数量] np.nan df[数量] df[数量].fillna(df[数量].median()) # 订单金额与单价*数量不一致以单价和数量重算 df[订单金额] df[单价] * df[数量] # # Step 9: 最终筛选与排序 # df df.sort_values([订单日期, 订单号]) df df.reset_index(dropTrue) # # Step 10: 导出 # df.to_excel(clean_sales_orders.xlsx, indexFalse, engineopenpyxl) print(\n 清洗完成 ) print(df) print(清洗后数据量, len(df))你注意看我在这个示例里隐含的几条经验日期里有2024.01.08这种非标准格式甚至还有一个zzz垃圾值pd.to_datetime(errorscoerce)一次搞定解析不了的变成NaN。单价里有千分位逗号先去掉再转numeric。数量有负数和0先取绝对值0用中位数填充。订单金额直接用单价乘数量重算这样比单纯填充更靠谱。4.1 清洗效果的验证方法数据洗完了不能拍脑袋觉得差不多就行了。我建议做三套验证一是量化验证。对比清洗前后的行数、列数、缺失值数量、重复值数量写进清洗报告里让看报告的人一目了然。二是抽样验证。从清洗后的数据里随机抽20到50条人工核对原始数据和清洗后的数据差异确认没有误伤。三是业务验证。把清洗结果交给业务方让熟悉数据的人抽查几个关键字段的语义是否符合预期。print(清洗报告) print(f- 原始数据行数: {raw_count}) print(f- 清洗后行数: {len(df)}) print(f- 删除重复行: {raw_count - len(df)}) print(f- 缺失值总数: {int(df.isnull().sum().sum())}) # 抽样检查 sample df.sample(min(20, len(df)), random_state42) print(\n随机抽取样本) print(sample)5. 常见问题排查与避坑技巧这一部分是我最有感触的很多坑都是踩过之后才明白的。我整理了一份高频问题速查表一个一个说。5.1 SettingWithCopyWarning警告是什么该怎么办这是Pandas新手最容易遇到的警告。我举个例子df pd.read_excel(data.xlsx) part_df df[df[省份] 广东省] part_df[省份] 广东省 # 这里会触发 SettingWithCopyWarning触发警告的原因是part_df是对原数据的一个视图或拷贝不确定Pandas无法判断你是在修改原数据还是修改临时副本。解决办法有两个# 方法一用 .copy() 显式创建副本 part_df df[df[省份] 广东省].copy() part_df[省份] 广东省 # 方法二用 .loc 直接操作原 DataFrame df.loc[df[省份] 广东省, 省份] 广东省说实话这个警告在多数情况下不影响结果但它是一种信号你可能做了超出预期的操作。尽早养成显式.copy()的习惯能省掉很多排查时间。5.2 为什么文件读取中文列名变成了乱码这个问题多见于CSV文件。读取的时候没有指定编码Python默认用了系统编码中文就乱了。解法是读取时明确指定编码# 读CSV时指定 utf-8-sig兼容Excel导出的带BOM的CSV df pd.read_csv(data.csv, encodingutf-8-sig) # 如果还是乱码尝试 GBK / GB2312 df pd.read_csv(data.csv, encodinggbk)判断编码的方法先用open读文件头部字节尝试不同编码看哪种不乱码。实际项目里Excel导出的CSV大概率是GBK编码网页下载的大概率是UTF-8。5.3 数据量太大Pandas直接内存溢出了怎么办遇到大文件超过几个GB时Pandas会吃力。有两个方向一是分块读取处理完再合并chunk_iter pd.read_csv(big_data.csv, chunksize100000) clean_chunks [] for chunk in chunk_iter: # 对每块做清洗 chunk chunk.drop_duplicates() chunk[单价] pd.to_numeric(chunk[单价], errorscoerce) clean_chunks.append(chunk) df_result pd.concat(clean_chunks, ignore_indexTrue)二是使用更高效的数据类型。Pandas读数据时默认用int64、float64如果你本身知道某列的数据范围可以手动指定低精度的类型能显著减少内存占用df pd.read_csv(data.csv, dtype{ 数量: int32, 单价: float32, }, usecols[订单号, 数量, 单价, 省份])usecols参数只读需要的列也是减少内存占用的有效手段读取时你只选择用到的列而不是把所有列都拉进来。5.4 日期列处理中最容易被忽略的坑日期处理的坑我见过太多了。这里说三个一是日期字符串的月份和日期写反了。比如01/02/2024Pandas默认按month/day/year解析如果你的数据是day/month/year解析结果会错位。遇到这种情况要显式指定formatpd.to_datetime(df[date], format%d/%m/%Y)。二是时区问题。如果你处理的是跨时区的时间数据pd.to_datetime返回的是naive时间不带时区信息。要做时区转换的话用df[date].dt.tz_localize和dt.tz_convert。三是datetime.date和pd.Timestamp混用。DataFrame里同一列不能混类型否则排序会报错或者结果不符合预期。建议统一转成pd.Timestamp。5.5 布尔索引筛选数据时常见的坑用多个条件筛选数据时新手容易写成这样# 错误写法 和 | 优先级高于条件比较 df[df[数量] 1 df[订单金额] 100]正确的写法是每个条件都要加括号# 正确写法 df[(df[数量] 1) (df[订单金额] 100)]还有一点and/or和/|的区别。and和or是Python的关键字适合用于标量布尔值和|是位运算符适合用于元素级的布尔数组。在Pandas条件筛选里必须用和|。5.6 常见问题速查表问题现象可能原因解决方案数值列计算报错或结果NaN列中存在非数值文本pd.to_numeric(col, errorscoerce)日期排序错乱日期是字符串格式pd.to_datetime()分组统计结果异常偏少文本列存在空格或大小写差异str.strip()str.upper()读取CSV中文乱码编码不匹配指定encodingutf-8-sig或gbk保存Excel后多出一列默认保存了索引列indexFalse行数莫名减少未注意dropna()默认丢弃所有含空值行检查subset参数后使用修改一个df另一个也变了浅拷贝导致共享引用用.copy()创建深拷贝数据合并后索引错乱未重置索引reset_index(dropTrue)6. 经验的沉淀从清洗脚本到可维护的清洗管线前面讲完了十步流程和常见坑最后我想聊一个更重要的问题清洗脚本怎么从一次性脚本变成可维护的清洗管线。我在真实项目里发现数据清洗脚本最怕的是逻辑堆成一坨。今天删个列明天加个映射后天修个bug三个月后没人敢动了。解决方法是按函数拆分让每一步都是一个独立且可测试的单元。def load_raw_data(path): return pd.read_excel(path, engineopenpyxl) def remove_duplicates(df): return df.drop_duplicates(subset[订单号]) def normalize_columns(df): df df.copy() df.columns df.columns.str.strip().str.lower() return df def clean_payment(df): df df.copy() payment_map {微信: 微信支付, wechat: 微信支付, ali: 支付宝} df[支付方式] df[支付方式].replace(payment_map) return df def fill_missing(df): df df.copy() df[订单状态] df[订单状态].fillna(待确认) return df # 主流程 def main(): df load_raw_data(sales_orders.xlsx) df remove_duplicates(df) df normalize_columns(df) df clean_payment(df) df fill_missing(df) df.to_excel(clean_sales_orders.xlsx, indexFalse) print(清洗完成) if __name__ __main__: main()这种写法有很直接的好处每个函数只做一件事测试时针对单个函数验证逻辑数据源变了只要改读取函数清洗规则变了只改对应函数。整个流程相当于一条流水线后期维护和多人协做都很方便。另外我习惯把每一步的清洗记录打日志比如处理了多少行重复、填了多少个缺失值这样在排查数据问题时能追溯每一步的具体操作。最终说一句实在话数据清洗没有一劳永逸的银弹因为每一份数据都有自己的脾气但十步流程是经过大规模实战验证过的通用框架。按这个流程走再脏的数据也能一步步变成可以放心分析的样子。我自己的体会是前几次做清洗会觉得繁琐但当你能一次把脚本跑通、输出的数据完全合规时那种成就感不亚于完成一个复杂模型。建议你先拿自己手头的数据练手跑通之后再把心得回头对照这篇文章你会发现自己理解得远比看一遍要深。