公司动态
Pandas数据清洗实战:从脏数据到结构化数据的完整流程
如果你做过数据分析或数据处理大概率经历过这样一种状态打开一份 Excel 或 CSV 文件里面既有合并单元格、又有空行空列日期一会儿是字符串一会儿是时间戳金额列还混着1,200元这种带符号文本。真正用于分析的时间很少大部分时间都耗在把数据弄干净上。数据清洗从来不是数据分析流程中最性感的部分但它决定了后续建模、报表、可视化的可信度。用一句话概括脏数据不清理模型越努力越尴尬。而 Python 生态里最常用的数据清洗工具就是 Pandas。这篇文章会从实际开发场景出发完整走一遍规范原始数据、修复脏数据、结构化数据的 Pandas 实战流程同时给出 2026 年仍然适用的最佳实践和排错思路。文章会按这个顺序展开先讲清楚数据清洗要解决什么问题再讲 Pandas 的核心数据结构与常用方法接着完成环境配置然后进入完整的清洗实战——包括缺失值、重复值、异常值、类型转换、文本清洗和索引重置最后给出验证方法、常见问题与工程建议。全程代码都可以直接复制运行建议先收藏再跟着敲一遍。1. 这篇文章真正要解决的问题很多初学者对 Pandas 的认知停留在读取 CSV、算个平均值、画个图但真正进入项目之后才会发现数据清洗才是耗时最多的环节。一个典型的数据分析任务时间分配大致是这样的工作内容耗时占比理解业务与定义指标20%数据采集与获取15%数据清洗与预处理40%建模分析与可视化25%这不是夸张。尤其是来自不同系统的数据汇合之后字段口径不统一、编码格式混乱、缺失值和异常值交织在一起如果不在清洗阶段把数据规范成结构化表格后面的分析全是空中楼阁。这篇文章要解决的三个核心痛点原始数据不符合预期列名不规范、重复列、空行空列、数据类型混乱。脏数据影响分析结论缺失值、重复值、异常值、文本中的隐藏字符容易导致统计结果偏离真实情况。数据无法直接用于建模建模工具和机器学习算法通常要求输入为结构化数据即每行一个样本、每列一个特征、单元格为标量值。读完这篇文章你会掌握一套可复用的 Pandas 数据清洗流程从加载数据开始到规范化列名、处理缺失值、去重、修复类型、清洗文本、处理异常值最后得到一份干净、规范、可直接用于分析或建模的结构化数据。2. Pandas 核心概念与常用数据结构在进入实战之前先把两个最基础也最容易混淆的概念理清楚Series 和 DataFrame。Series是 Pandas 中的一维数组类似 Excel 中的一列每个元素都有对应的索引。DataFrame是二维表格数据结构由多个 Series 组成有行索引和列名。大多数数据清洗操作都发生在 DataFrame 上。这里有必要引入一个职场类比DataFrame 就像一张规范化的 Excel 工作表每一行是一条记录每一列是一个字段而 Series 就是这个工作表中的某一列。Pandas 数据清洗的常用方法大致可以分为几类分类常用方法数据查看head()、info()、describe()、shape、dtypes缺失值处理isnull()、dropna()、fillna()重复值处理duplicated()、drop_duplicates()类型转换astype()、pd.to_datetime()、pd.to_numeric()文本清洗str.strip()、str.replace()、str.contains()异常值处理描述统计、分位数过滤、业务规则过滤索引与列操作set_index()、reset_index()、rename()一个容易误解的地方是dropna()并不总是最佳选择。如果缺失值占比很高直接删除会导致样本量急剧下降如果缺失值有业务含义比如未填写用固定值填充可能更合理。清洗策略需要结合数据和业务场景来判断而不是机械地套方法。3. 环境准备与 Pandas 安装3.1 Python 环境要求Pandas 是基于 NumPy 构建的所以在安装 Pandas 之前需要保证 Python 环境正常。2026 年的主流 Python 版本已经发展到 3.12 及以上但不同 Pandas 版本对不同 Python 版本有兼容性要求。更稳妥的判断是安装时以官方文档标注的版本对应关系为准不要盲目装最新版。如果你使用的是 Anaconda通常已经预装了 Pandas可以直接使用。如果是原生 Python 环境建议使用虚拟环境管理依赖避免不同项目之间的包冲突。3.2 安装 Pandaspip install pandas如果你的网络环境使用默认源较慢可以临时指定国内镜像源pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后验证版本import pandas as pd print(pd.__version__)如果需要在 Jupyter Notebook 或 VS Code 中使用 Pandas直接在代码中导入即可。VS Code 中需要先配置好 Python 解释器然后用终端安装依赖。3.3 本项目使用的演示数据为了完整展示数据清洗流程下面所有示例使用的是一份模拟的用户订单数据——包含用户 ID、用户名、手机号、下单日期、订单金额、商品分类等字段。这份数据故意混入了缺失值、重复记录、类型错误、文本空格和异常金额目的是最大程度还原真实场景。# 文件路径data/raw_orders.csv user_id,user_name,phone,order_date,amount,category 1001,zhangsan,13812345678,2025-01-01,299.00,电子产品 1002,lisi,13998765432,2025-01-02,,服装 1003,wangwu,13711112222,2025/01/03,158.5,食品 1004,zhaoliu,,2025-01-04,89,电子产品 1001,zhangsan,13812345678,2025-01-01,299.00,电子产品 1005,sunqi,13633334444,2025-01-05,1299.00,家电 1006,zhouba,13555556666,2025-01-06,5000,数码 1007,wujiu,13477778888,2025-01-07,abc,食品 1008,zhengshi,13399990000,2025-01-08,66.6,服装你可以把这份内容保存为data/raw_orders.csv也可以在代码中直接用io.StringIO创建 DataFrame两种方式下文都会演示。4. 数据清洗核心流程拆解数据清洗不是一蹴而就的它有一套稳定的操作顺序。如果顺序颠倒比如先做类型转换、再处理缺失值很可能因为缺失值导致类型转换报错。推荐流程如下数据概览与结构检查列名规范化重复值处理缺失值处理数据类型转换文本与格式清洗异常值处理索引重置与结果导出每一步都有明确目标接下来会在实战中逐个演示。4.1 数据概览与结构检查拿到数据的第一件事不是急着清洗而是先用info()和head()了解数据长什么样。import pandas as pd df pd.read_csv(data/raw_orders.csv) print(df.shape) print(df.info()) print(df.head())df.shape输出行数和列数df.info()会显示每列的数据类型、非空值数量df.head()查看前五行数据。这一步能快速发现几类问题列数是否符合预期、哪些列存在缺失、哪些列类型明显异常。从演示数据可以看到phone列有缺失值。amount列出现abc这样的非数值内容。order_date列日期格式不统一有的用横线、有的用斜杠。存在完全重复的行。4.2 列名规范化原始数据的列名经常带有空格、大小写混用、中英文混排直接使用容易在后续代码中出错。规范化列名的通用思路是全部转为小写、空格替换为下划线、去除特殊字符。df.columns df.columns.str.strip().str.lower().str.replace( , _) print(df.columns)这里用到了 Pandas 的字符串方法str.strip()去掉列名首尾空格str.lower()转为小写str.replace( , _)将空格替换为下划线。列名一旦统一后续引用时就不会出现有时候是UserName、有时候是username的尴尬。4.3 重复值处理重复值分为两类完全重复的行以及部分关键字段重复、其他字段不同的行。前者直接删除即可后者需要结合业务判断保留哪一条。# 检查完全重复的行 print(df.duplicated().sum()) # 删除完全重复的行 df df.drop_duplicates()如果只想基于某几列判断重复可以传参指定列名。例如如果指定两列的值均相同则取第一条数据即可在 Pandas 中这样写df df.drop_duplicates(subset[user_id, order_date], keepfirst)keepfirst表示保留重复记录中的第一条keeplast表示保留最后一条。在企业数据中这种同一用户在同一个时间只能有一条订单的规则非常常见。4.4 缺失值处理缺失值处理的策略选择和业务强相关。一般有三种思路第一种是直接删除。当缺失值占比极小且删除后不影响样本代表性时使用。第二种是填充。数值列可以用均值、中位数、众数或固定值填充类别列可以用众数或未知填充。第三种是保留。如果缺失值本身有业务含义比如用户未填写手机号可以保留并单独标记。# 查看每列缺失值数量 print(df.isnull().sum()) # 数值列用中位数填充 df[amount] pd.to_numeric(df[amount], errorscoerce) df[amount] df[amount].fillna(df[amount].median()) # 类别列用未知填充 df[phone] df[phone].fillna(未知)需要特别注意的是填充策略要写在类型转换之后。因为pd.to_numeric遇到abc这类非数值内容时如果errorscoerce会将其转为NaN之后再填充中位数。这个顺序如果反过来填充时会把abc也当作有效值。4.5 数据类型转换Pandas 中常见的类型问题有三个数值列被读成字符串比如金额带货币符号或千分位逗号。日期列被读成字符串。布尔列被读成 0/1 整数。数值列转换最常用pd.to_numeric日期列转换最常用pd.to_datetime。# 金额列转为数值类型 df[amount] pd.to_numeric(df[amount], errorscoerce) # 日期列统一转为 datetime 类型 df[order_date] pd.to_datetime(df[order_date], errorscoerce) # 检查转换后的类型 print(df.dtypes)pd.to_datetime能自动识别多种日期格式比如2025-01-01和2025/01/03都能被正确解析。虽然 Pandas 能识别多种格式但在实际项目中仍然建议统一日期格式因为不同国家或系统导出的日期格式差异很大。4.6 文本与格式清洗文本列的问题是隐藏杀手。经常出现的情况包括字符串首尾有空格、存在不可见字符、大小写不一致、混入换行符或制表符。# 去除 user_name 列首尾空格 df[user_name] df[user_name].str.strip() # 统一转小写 df[user_name] df[user_name].str.lower() # 检查是否还有空字符串 print(df[user_name].value_counts())如果文本中有特殊符号可以用正则表达式和str.replace清理。例如去除手机号中的横线或空格df[phone] df[phone].str.replace(r\D, , regexTrue)\D表示非数字字符regexTrue启用正则模式。这一步会把手机号中的空格、横线等非数字字符全部移除只保留纯数字。4.7 异常值处理异常值没有一个统一的数学定义。常见处理方式有三种基于业务规则过滤。基于统计分布过滤如超过均值加减 3 倍标准差。基于分位数过滤如低于 1% 分位或高于 99% 分位。实际项目里第一种方式最常用也最安全。以订单金额为例如果业务规则是订单金额必须大于 0那么小于等于 0 的数据就是异常。# 查看金额的统计分布 print(df[amount].describe()) # 过滤金额小于等于 0 的异常数据 df df[df[amount] 0]需要注意的是不要脱离业务做纯统计过滤。比如某些促销订单金额只有 0.01 元用统计方法很容易被误判为异常。4.8 索引重置与结果导出清洗完成后由于删除了部分行索引可能不连续。建议用reset_index重建索引。df df.reset_index(dropTrue) print(df.head())最后导出为 CSV 或 Parquet 格式。Parquet 是列式存储格式在数据量大、需要后续复用时会明显提升读写性能CSV 则方便分发和人工查看。# 导出为 CSV df.to_csv(data/clean_orders.csv, indexFalse, encodingutf-8-sig)encodingutf-8-sig是为了在 Windows 下用 Excel 打开 CSV 时中文不乱码。indexFalse表示不写入行索引。5. 完整示例代码实现下面把整个清洗流程组装成一个完整的 Python 脚本。你可以将脚本保存为clean_orders.py然后在终端运行。# 文件路径clean_orders.py import pandas as pd def load_data(file_path): 读取原始数据 df pd.read_csv(file_path) print(f原始数据形状{df.shape}) return df def clean_columns(df): 规范化列名 df.columns df.columns.str.strip().str.lower().str.replace( , _) return df def remove_duplicates(df): 删除完全重复的行 before df.shape[0] df df.drop_duplicates(subset[user_id, order_date], keepfirst) after df.shape[0] print(f去重前 {before} 行去重后 {after} 行) return df def fix_types(df): 修复数据类型金额转数值日期转 datetime df[amount] pd.to_numeric(df[amount], errorscoerce) df[order_date] pd.to_datetime(df[order_date], errorscoerce) return df def handle_missing(df): 处理缺失值 df[amount] df[amount].fillna(df[amount].median()) df[phone] df[phone].fillna(未知) df[category] df[category].fillna(未知) return df def clean_text(df): 清洗文本列 df[user_name] df[user_name].str.strip().str.lower() df[phone] df[phone].str.replace(r\D, , regexTrue) return df def filter_outliers(df): 过滤异常金额 df df[df[amount] 0] return df def reset_and_export(df, output_path): 重置索引并导出 df df.reset_index(dropTrue) df.to_csv(output_path, indexFalse, encodingutf-8-sig) print(f清洗完成导出文件{output_path}) print(f清洗后数据形状{df.shape}) return df def main(): input_path data/raw_orders.csv output_path data/clean_orders.csv df load_data(input_path) df clean_columns(df) df remove_duplicates(df) df fix_types(df) df handle_missing(df) df clean_text(df) df filter_outliers(df) df reset_and_export(df, output_path) print(\n清洗后数据预览) print(df.head(10)) if __name__ __main__: main()这个脚本的思路是一个函数做一件事每一步都有清晰的输入输出。在实际项目中这种函数化拆分极大方便了排查问题——如果某一步出错只需要单独调用对应函数而不需要重跑整段代码。如果你暂时不想创建 CSV 文件也可以用io.StringIO直接在内存中创建 DataFrameimport pandas as pd from io import StringIO csv_data user_id,user_name,phone,order_date,amount,category 1001,zhangsan,13812345678,2025-01-01,299.00,电子产品 1002,lisi,13998765432,2025-01-02,,服装 1003,wangwu,13711112222,2025/01/03,158.5,食品 df pd.read_csv(StringIO(csv_data)) print(df)这个方法在写测试脚本或快速验证代码逻辑时非常方便。6. 运行结果与效果验证在终端运行完整脚本python clean_orders.py预期输出大致如下原始数据形状(9, 6) 去重前 9 行去重后 8 行 清洗完成导出文件data/clean_orders.csv 清洗后数据形状(8, 6)这里因为有两条重复记录去重后从 9 行变为 8 行异常金额abc被to_numeric转为缺失值再由中位数填充所以行数没有变化。验证清洗效果可以从三个角度检查第一检查类型。df.dtypes中amount应为float64order_date应为datetime64[ns]。第二检查缺失值。df.isnull().sum()每一列都应为 0。第三检查重复值。df.duplicated().sum()应为 0。如果运行失败先从这几方面排查CSV 文件路径是否正确、Pandas 是否已安装、Python 版本是否兼容。路径问题最常见的表现是FileNotFoundError解决方式是把输入路径改为绝对路径或者确认终端当前目录与脚本所在目录一致。7. 常见问题与排查思路数据清洗过程中有不少细节坑下面按出现频率整理了一份排查表。问题现象可能原因排查方式解决方案pd.read_csv报 UTF-8 编码错误CSV 文件编码不是 UTF-8用文本编辑器查看文件编码指定encodinggbk或encodingutf-8-sig后重新读取astype()报无法转换列中包含无法解析的内容先打印df[col].unique()改用pd.to_numeric(..., errorscoerce)drop_duplicates去重后行数没变化整行内容存在隐藏空格或不可见字符检查原始数据中的空白字符先str.strip()再重新去重fillna填充后仍有缺失值数据中缺失值是空字符串而非 NaN检查df[col].value_counts()先用replace(, np.nan)将空字符串转为 NaNto_datetime后出现 NaT日期格式混入无法识别的格式打印无法解析的具体值使用format参数指定格式或先清洗日期文本中文输出乱码编码格式不一致检查终端和文件编码导出 CSV 时使用encodingutf-8-sig这里有几个容易被忽视的细节。第一个是空字符串和 NaN 的区别。某些系统导出数据时缺失值不是空白单元格而是空字符串。isnull()对空字符串返回False所以这类缺失值需要先用replace(, np.nan)转换后再处理。第二个是日期格式。pd.to_datetime默认能处理大多数常见格式但遇到2025年1月1日或01/02/2025这类格式时解析结果可能与预期不同。更稳妥的做法是指定format参数df[order_date] pd.to_datetime(df[order_date], format%Y-%m-%d)第三个是性能问题。当数据量达到千万行级别时逐行iterrows()几乎不可用。应尽量使用向量化方法比如str.replace、pd.to_numeric避免显式循环。如果文件特别大还可以分块读取chunks pd.read_csv(data/large_file.csv, chunksize100000) for chunk in chunks: process(chunk)8. 最佳实践与工程建议数据清洗在真实项目中不是一次性任务而是一个需要持续维护的流程。基于实践经验总结几条工程建议。8.1 保留原始数据清洗副本永远不要在原始数据上直接修改。建议复制一份到df_clean或者在清洗前将原始文件备份到独立目录。原因很简单清洗策略可能随着业务理解加深而调整原始数据一旦被覆盖想回溯就非常困难。df_raw pd.read_csv(data/raw_orders.csv) df df_raw.copy()8.2 每次清洗都做记录在清洗脚本中定义清洗日志记录每一步处理了哪些行、哪些列、使用了什么策略。后续如果分析结果异常可以快速定位是清洗逻辑导致的还是模型问题导致的。cleaning_log [] cleaning_log.append(f去重删除 {before - after} 行) cleaning_log.append(f缺失值金额列中位数填充 {na_count} 个)如果团队协作还可以把清洗逻辑封装成独立的 Python 模块配合配置文件和日志系统使用。8.3 建立数据验证断言在清洗流程的关键节点加入断言比如清洗后不应存在缺失值、金额必须大于 0让错误在最早阶段暴露。assert df[amount].notnull().all(), 金额列存在缺失值 assert (df[amount] 0).all(), 金额列存在小于等于 0 的值8.4 区分清洗与特征工程数据清洗的目标是让数据准确、一致、可用比如修复类型、处理缺失值、去除重复。特征工程的目标是让数据更适合模型学习比如构造新列、归一化、编码类别变量。这两个阶段不要混在一起否则清洗逻辑会越来越难维护。8.5 尽量向量化而不是逐行循环Pandas 的性能瓶颈通常来自 Python 层循环。能用str方法、apply或内置聚合函数解决的问题就不要写for循环。如果数据规模实在太大再考虑引入 Dask、Polars 或 Spark但那是另一个话题了。8.6 将清洗流程做成可复用函数如果把所有清洗逻辑写在 Jupyter Notebook 的一个单元格里第一次运行可能没问题但换了新数据之后就得反复改代码。更好的做法是把清洗流程封装成函数配合参数配置让同一套清洗逻辑可以适配多个数据源。9. 总结与后续学习方向这篇文章完整走了一遍基于 Pandas 的数据清洗流程包括环境准备、数据概览、列名规范化、去重、缺失值处理、类型转换、文本清洗、异常值过滤和结果导出。核心思路可以提炼成一句话清洗的核心不是把数据删到最少而是让数据的准确性、一致性和可用性达到分析或建模的基本门槛。接下来可以按自己的数据场景做一轮实践建议顺序如下找一份你实际使用过的 CSV 或 Excel 数据用本文的流程跑一遍。观察清洗前后df.info()和df.describe()的差异确认每一步都产生了实际效果。尝试加入更复杂的清洗规则比如基于业务字典做分类映射、合并多个数据源、处理时间窗口数据。学习 Parquet 格式的读写大数据量的场景下性能差异会非常明显。pd.read_parquet和df.to_parquet值得熟练掌握。如果你做的是金融、电商或用户行为数据分析还需要进一步学习时间序列重采样、窗口函数、分组聚合等进阶内容这些后续可以单独展开。建议把这篇文章收藏需要用清洗代码的时候可以直接翻出来对照使用。