☰
Pandas数据清洗全流程实战:从原始数据到结构化输出
2026/10/24 19:10:40 网站建设 项目流程

这次我们直接基于 Pandas 把一套完整的数据清洗流程跑通。平时工作里最常见的脏数据无非是空值、重复值、格式不一致、异常值、文本乱码这几种,很多同学拿到数据先df.head()看一眼就开始写处理代码,结果处理到一半发现字段是字符串、某几行本来就是重复的、日期格式全乱,最后清洗结果根本不敢直接拿去建模或出报表。这篇文章会从读取原始数据开始,一直做到输出规范、结构化数据,中间覆盖缺失值处理、重复值清理、类型转换、异常值过滤、文本与日期格式化、长宽表转换、批量文件处理,以及大数据量下的性能优化思路。

这套流程对硬件没有特殊要求,普通办公电脑就能跑,Pandas 安装用 pip 一条命令,Windows、macOS、Linux 都支持。核心能力可以归纳成几句话:缺失值能补、重复数据能去、类型能转、异常值能筛、文本能清洗、日期能统一、批量任务能跑、结果能导出成 CSV、Excel、Parquet 等格式,清洗逻辑还能封装成函数供 API 服务调用。无论你是做数据分析、数据开发,还是正在准备 Python 数据相关岗位面试,这套流程都值得照着跑一遍。下面直接从环境准备开始。

1. 核心能力速览与适用边界

能力项说明
技术栈Python + Pandas 2.x,底层依赖 NumPy,生态成熟
核心功能缺失值处理、重复值清理、异常值过滤、数据类型转换、文本清洗、日期标准化、结构化输出
输入数据CSV、Excel、JSON、Parquet、Feather、数据库查询结果
输出数据清洗后的 DataFrame,可导出为 CSV、Excel、Parquet、Feather
硬件要求普通办公电脑即可,内存 8G 以上处理中等数据量更稳
操作系统Windows / macOS / Linux
安装方式pip 或 conda
批量任务支持,通过脚本循环处理多个文件
API 对接清洗逻辑可封装成函数,供 FastAPI、Flask 等服务调用
适合人群数据分析师、数据开发工程师、业务运营、Python 学习者、数据岗位面试准备者

适用场景非常明确:拿到一份不规范的原始数据,需要整理成能用于统计分析、可视化、机器学习建模的结构化表格。比如从业务系统导出的订单表、用户表,从第三方平台下载的报表,或者爬虫抓下来的非结构化文本。这类场景的共性问题是字段错位、格式乱、有缺失和重复,需要一套可复用的清洗流程。

边界也要说清楚。Pandas 不是大数据引擎,单表数据量达到几千万行时,内存占用会明显上升;如果数据规模达到 TB 级,应该考虑 Spark、DuckDB 或数据库侧预处理,而不是硬塞进 Pandas。另外,涉及用户手机号、身份证、地址等敏感信息时,处理前必须确认数据来源合法、已获得授权,在演示和分享时也要先脱敏。数据清洗本身不产生数据,只修正数据,动手之前先备份原始文件永远是第一原则。

2. 环境准备:Python 与 Pandas 安装

清洗流程基于 Python 3 运行,推荐使用 3.9 及以上版本,当前 Pandas 2.x 稳定版安装和使用都很简单,不需要手动编译。如果你之前没有安装过 Python 环境,这里给一套完整的准备步骤。

2.1 检查 Python 环境

打开终端,先确认本机 Python 和 pip 是否可用:

python -V pip -V

如果系统同时装了多个 Python 版本,建议使用虚拟环境,避免不同项目依赖冲突:

python -m venv venv

Windows 激活虚拟环境:

venv\Scripts\activate

macOS 或 Linux 激活虚拟环境:

source venv/bin/activate

2.2 安装 Pandas 及相关依赖库

数据清洗除了 Pandas,通常还会用到 NumPy,读取 Excel 文件需要 openpyxl。用 pip 一次性安装:

pip install pandas numpy openpyxl

如果速度慢,可以换国内镜像源:

pip install pandas numpy openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple

验证安装是否成功:

python -c "import pandas as pd; print(pd.__version__)"

看到版本号输出说明环境就绪。在 PyCharm 里操作时,需要确认当前项目解释器选的是这个虚拟环境;在 VSCode 里则要选择对应的 Python 解释器。Jupyter Notebook 也可以直接使用,交互式分步清洗时更方便观察中间结果。

3. 原始数据读取与数据体检

清洗的第一步不是处理脏数据,而是把数据读进来,做一次“体检”。只有知道数据哪里脏、脏到什么程度,才能决定后续用哪种策略。

3.1 读取常见数据文件

import pandas as pd # 读取 CSV 文件 df = pd.read_csv("raw_data.csv", encoding="utf-8")

如果 CSV 文件是中文且编码不是 utf-8,常常会报UnicodeDecodeError。可以尝试指定其他编码,或者用 try except 自动探测:

try: df = pd.read_csv("raw_data.csv", encoding="utf-8") except UnicodeDecodeError: df = pd.read_csv("raw_data.csv", encoding="gbk")

读取 Excel 文件:

df = pd.read_excel("raw_data.xlsx", sheet_name="订单", engine="openpyxl")

读取 JSON、Parquet、Feather 也很常用,尤其 Parquet 和 Feather 格式在列式存储、压缩率和读取速度上比 CSV 更适合做中间结果:

df = pd.read_json("raw_data.json") df = pd.read_parquet("raw_data.parquet") df = pd.read_feather("raw_data.feather")

3.2 数据体检三件套

# 1. 看整体结构和非空情况 print(df.info()) # 2. 看数值列分布 print(df.describe()) # 3. 看缺失值和重复值 print(df.isnull().sum()) print(df.duplicated().sum())

df.info()会输出每个列名、非空数量、数据类型,能快速发现列名是否规范、字段类型是否合理、哪些列存在缺失。df.describe()针对数值列输出均值、标准差、最小值、四分位数、最大值,用于初步判断是否存在超出合理范围的异常值。df.isnull().sum()和df.duplicated().sum()则直接给出缺失样本数和重复样本数。

真实业务里可能拿到几百列的表,这时候先不要无脑全看。优先关注唯一键列、时间列、金额列、手机号或身份证这类核心标识列,判断这几类字段是否可用,其他列可以后面按需处理。

为了演示完整流程,下面构造一个包含多种脏数据的模拟订单表,后续所有清洗操作都基于这个数据:

import pandas as pd df = pd.DataFrame({ "订单号": ["A001", "A002", "A002", "A003", "A004", "A005"], "用户ID": ["U_001", "U_002", "U_002", "U_003", "U_004", "U_005"], "下单日期": ["2024/1/3", "2024/01/05", "20240108", "2024-02-14", "2024年3月2日", "2024-02-30"], "手机号": ["13800138000", "13912345678", "13912345678", "021-12345678", "hello", "13711112222"], "金额": [100.5, "200,000", 300, None, "五十", 400], "地区": [" 上海 ", "北京", "北京", "广州", "深圳", "未知"] }) print(df)

这个数据包含的问题有:订单号 A002 重复,手机号列混入了座机号和字符串,金额列有字符串、数字和空值,日期列存在多种格式,地区列有空格和“未知”分类。接下来进入正式修复环节。

4. 脏数据修复:缺失值、重复值、异常值与类型转换

这一节是整套数据清洗流程的核心,解决的问题覆盖日常场景里八成以上的脏数据。每类问题先说明判断思路,再给出可直接套用的代码。

4.1 缺失值处理

处理缺失值之前先明确需求:这个字段能否为空、缺失比例多少、业务上能不能用其他字段推算。盲目fillna(0)会把缺失和真实为零混在一起,影响统计口径。

# 查看缺失情况 print(df.isnull().sum()) # 方案一:行内有关键字段缺失,直接删除 df_dropna = df.dropna(subset=["订单号"]) # 方案二:数值列缺失用均值或中位数填充 df["金额"] = pd.to_numeric(df["金额"], errors="coerce") df["金额"] = df["金额"].fillna(df["金额"].median()) # 方案三:时间序列数据用插值填充 # 示例:df["数值"] = df["数值"].interpolate()

在模拟数据里,可以演示金额列的空值用中位数填充,因为金额通常右偏分布,均值容易受极端值影响,中位数更稳健。对于时间序列数据,比如按小时采集的指标,缺失值用interpolate()做线性插值往往效果更好。注意:如果某个字段缺失比例超过 50%,且不是关键字段,直接删除该列通常是更合理的选择。

4.2 重复值处理

重复数据会导致统计结果被放大。Pandas 的判断逻辑是按行比较,也可以用subset指定部分列判断是否重复。热搜词里有一句很典型:“如果指定两列的值均相同,则取第一条数据即可”,这就是drop_duplicates(subset=[...], keep="first")最常用的场景。

# 查看全行重复 print(df.duplicated().sum()) # 基于订单号和用户ID两个字段判断重复,保留第一条 df = df.drop_duplicates(subset=["订单号", "用户ID"], keep="first") # 如果想保留最后一条 # df = df.drop_duplicates(subset=["订单号", "用户ID"], keep="last")

这里要特别说明:去重之前必须先确认业务语义。两个订单号相同是否一定意味着重复下单?如果订单号本身可以重复,只是用户操作记录,就不能盲目删除。所以指定subset列的时候,要选能真正唯一标识一条记录的字段组合。

4.3 数据类型转换

Pandas 中常见的类型问题有三个:数值列被读成字符串、日期列被读成字符串、分类列被读成 object。直接astype在数据干净时可以用,但一旦遇到"200,000"、"五十"这种值就会报错或被转成错误结果,更稳妥的做法是使用带errors参数的方法。

# 金额列:先去逗号,再转数值,无法转换的置为 NaN df["金额"] = ( df["金额"] .astype(str) .str.replace(",", "", regex=False) .str.replace("元", "", regex=False) ) df["金额"] = pd.to_numeric(df["金额"], errors="coerce")

pd.to_numeric(errors="coerce")会把"五十"这类无法解析的字符串转成 NaN,之后可以按缺失值策略再处理,不会让整个程序中断。这个方法是数据清洗里最值得记住的类型转换技巧之一。

日期列的统一处理也属于类型转换,会在下一节单独展开,因为它涉及格式问题比较多。

4.4 异常值处理

异常值的判断要看业务含义。比如订单金额一般不会为负数,也不会高到离谱;年龄字段理论上应该在 0 到 120 之间。常见做法有三种:业务阈值过滤、四分位数 IQR 法、Z-Score 标准差法。

# 方式一:业务阈值过滤 df = df[(df["金额"] >= 0) & (df["金额"] <= 1000000)] # 方式二:IQR 四分位距法 Q1 = df["金额"].quantile(0.25) Q3 = df["金额"].quantile(0.75) IQR = Q3 - Q1 df = df[(df["金额"] >= Q1 - 1.5 * IQR) & (df["金额"] <= Q3 + 1.5 * IQR)] # 方式三:Z-Score 法,一般选择 |z| > 3 视为异常 import numpy as np df["金额_zscore"] = (df["金额"] - df["金额"].mean()) / df["金额"].std() df = df[np.abs(df["金额_zscore"]) <= 3]

IQR 法对非正态分布的数值列更稳健,适合金额、响应时间这类偏态分布字段。阈值处理之后最好再做一次describe(),确认清洗后的分布是否符合预期。

5. 文本与格式类脏数据治理

很多脏数据不是缺、不是重复,而是格式不规范。常见问题包括:字符串首尾有空格、中英文大小写不统一、电话和日期格式混乱、同一分类有多种叫法。这部分用 Pandas 的字符串方法可以批量解决。

5.1 清理空格与统一大小写

# 去掉首尾空格 df["地区"] = df["地区"].str.strip() # 去除字符串中间的多余空格 df["地区"] = df["地区"].str.replace(r"\s+", "", regex=True) # 统一大小写,例如对英文用户名 # df["用户名"] = df["用户名"].str.lower()

处理后的地区列不会再有" 上海 "这种前面带空格的值,后续分组统计时结果更准确。

5.2 手机号和电话格式清洗

手机号列出现座机号、字母甚至完全无关的文本,是用户表里很常见的脏数据。处理思路是先用正则把 11 位手机号提取出来,同时把明显无效的值标记为缺失,最后对手机号做脱敏。

def clean_phone(value): s = str(value).strip() if len(s) == 11 and s.isdigit() and s.startswith("1"): return s match = re.search(r"1[3-9]\d{9}", s) return match.group(0) if match else None import re df["手机号"] = df["手机号"].apply(clean_phone) # 脱敏:保留前三位和后四位,中间四位用星号代替 df["手机号"] = df["手机号"].str.replace(r"(\d{3})\d{4}(\d{4})", r"\1****\2", regex=True)

这里用了re.search从包含座机号、字母的原始值里尽量提取手机号,提取不到就返回 None,再走缺失值处理流程。这个函数可以直接复用,加注释后放进项目工具模块。

5.3 日期格式统一

日期清洗的核心目标是让数据中的日期统一成datetime64类型,再按需求格式化为字符串或提取年月日字段。Pandas 的to_datetime会自动识别多种格式,但遇到"2024-02-30"这种不存在的日期时会失败,用errors="coerce"把解析失败的置为 NaT。

df["下单日期"] = pd.to_datetime(df["下单日期"], errors="coerce") # 查看解析结果 print(df["下单日期"]) # 统一格式化为字符串 df["下单日期"] = df["下单日期"].dt.strftime("%Y-%m-%d") # 提取年、月、日到单独字段 df["年"] = pd.to_datetime(df["下单日期"]).dt.year df["月"] = pd.to_datetime(df["下单日期"]).dt.month

清洗之后,2024/1/3、20240108、2024年3月2日都会被统一成2024-01-03这样的标准格式,后续做时间序列分析、按月份汇总时非常方便。

5.4 分类字段的别名统一

同一个值在业务表里可能有多种写法,比如“北京”、“北京市”、“beijing”都表示北京。建议先看唯一值,再映射成标准分类。

print(df["地区"].value_counts()) region_map = { "北京": "北京", "北京市": "北京", "beijing": "北京", "上海": "上海", "上海市": "上海", "广州": "广州", "深圳": "深圳" } df["地区标准化"] = df["地区"].map(region_map).fillna("未知")

映射时用fillna("未知")兜底,避免新出现的分类值变成 NaN 后丢失信息。完成这一步,分类字段就变成真正可控的结构化字段了。

6. 结构化数据整理、批量处理与结果输出

数据修完问题之后,还需要把表格整理成方便分析和建模的结构化形态。这个环节包括字段拆分与合并、长宽表转换、分组聚合,以及批量处理多个数据文件,最后导出成不同格式。

6.1 字段拆分与合并

如果原始数据里有“张三_13800138000”这种合并字段,可以用str.split拆开。如果原始数据把省市区放在一个地址字段里,也可以按分隔符拆分。

# 示例:将 "姓名_手机号" 拆成两列 df["姓名"], df["联系电话"] = df["用户信息"].str.split("_", expand=True) # 示例:将地址按“省市区”拆分 # df[["省", "市", "区"]] = df["地址"].str.extract(r"(.+?省)?(.+?市)?(.+?区)?")

反过来,如果需要把分散字段拼成一个字段,可以用字符串相加或cat方法:

df["完整地址"] = df["省"].fillna("") + df["市"].fillna("") + df["区"].fillna("")

6.2 长表与宽表转换

业务数据经常需要在这两种结构之间切换。宽表适合人看,长表适合机器分析和可视化库输入。Pandas 提供melt和pivot_table两个核心方法。

# 长表转宽表:按用户ID统计各地区金额合计 tooth = df.pivot_table(index="用户ID", columns="地区标准化", values="金额", aggfunc="sum", fill_value=0) # 宽表转长表 long_df = tooth.reset_index().melt(id_vars="用户ID", var_name="地区", value_name="金额")

pivot_table是结构化数据建模过程中非常常用的聚合操作,能把明细数据变成特征矩阵。比如做用户画像时,按用户 ID 做行、行为类型做列、行为次数或金额做值,得到的数据可以直接进入 sklearn 建模流程。

6.3 分组聚合统计

清洗后的数据可以正常做各种聚合:

summary = ( df.groupby("地区标准化")["金额"] .agg(["count", "sum", "mean", "std"]) .round(2) ) print(summary)

6.4 批量处理多个数据文件

批量任务不复杂,核心就是用glob或os.listdir拿到文件列表,循环读取、清洗、写出。真实项目中通常把清洗流程抽成一个函数,脚本只负责任务调度。

import glob import pandas as pd def clean_data(df): df = df.copy() df = df.drop_duplicates(subset=["订单号", "用户ID"], keep="first") df["金额"] = pd.to_numeric(df["金额"].astype(str).str.replace(",", "", regex=False), errors="coerce") df["下单日期"] = pd.to_datetime(df["下单日期"], errors="coerce") return df for file_path in glob.glob("data/raw/*.csv"): df = pd.read_csv(file_path, encoding="utf-8") df_clean = clean_data(df) file_name = file_path.split("/")[-1].replace(".csv", "_clean.csv") df_clean.to_csv(f"data/clean/{file_name}", index=False, encoding="utf-8-sig") print(f"处理完成: {file_name}, 清洗前 {len(df)} 行, 清洗后 {len(df_clean)} 行")

批量处理建议每完成一个文件就打印一行日志,记录清洗前后行数,方便后面核对问题。文件输出目录和原始目录分离,避免覆盖原始文件。

6.5 导出多种结构化格式

# 导出 CSV,utf-8-sig 便于 Excel 打开不乱码 df.to_csv("clean_data.csv", index=False, encoding="utf-8-sig") # 导出 Excel df.to_excel("clean_data.xlsx", index=False, sheet_name="清洗结果", engine="openpyxl") # 导出 Parquet,列式存储,后续读取快且占用空间小 df.to_parquet("clean_data.parquet", index=False) # 导出 Feather,跨语言共享速度快 df.to_feather("clean_data.feather")

从工程稳定性考虑,中间结果建议用 CSV 让人能直观检查,最终要交给下游模型或大数据平台的数据用 Parquet 或 Feather 更合适。Pandas 读取 Parquet 的速度比 CSV 快不少,而且文件更小。

6.6 清洗逻辑对接 API 服务

清洗函数封装好之后,可以很方便地接到 Web 接口服务上。FastAPI 是当前比较常用的轻量方案,示例代码如下:

# app.py from fastapi import FastAPI import pandas as pd from cleaner import clean_data app = FastAPI() @app.post("/clean") async def clean(records: list[dict]): df = pd.DataFrame(records) df_clean = clean_data(df) return df_clean.to_dict(orient="records")

这个示例需要根据实际项目结构调整,比如是否校验字段、是否需要鉴权、是否限制请求体大小。启动服务后,可以用 curl 或 Postman 发送 JSON 数组,接口返回清洗后的 JSON 结果,适合对接数据上报、文件预处理等场景。

7. 性能观察与大数据量处理思路

Pandas 处理几十万行数据一般没有问题,但数据量达到千万行级别时,内存占用和运行时间会成为瓶颈。这一节讲怎么观察资源占用,以及有哪些可以立刻落地的优化思路。

7.1 观察内存占用

# 查看每列内存占用 print(df.memory_usage(deep=True)) # 查看整个 DataFrame 内存占用 print(df.memory_usage(deep=True).sum() / 1024 / 1024, "MB")

memory_usage(deep=True)能看出来哪些列占内存最大。object 类型列通常比数值列更占空间,尤其是包含较长字符串时。

7.2 降低内存占用的实用手段

第一是压缩数据类型。地区、性别、状态这类字段数量有限,可以转成 category 类型:

df["地区"] = df["地区"].astype("category") print(df.memory_usage(deep=True).sum() / 1024 / 1024, "MB")

第二是只读需要的列。如果原始 CSV 有 200 列,而清洗只需要其中 30 列,读取时直接指定usecols:

df = pd.read_csv("large_file.csv", usecols=["订单号", "用户ID", "下单日期", "金额", "地区"])

第三是分块读取。chunksize把一个大文件拆成多个小批次逐块处理,避免一次性把全部数据加载进内存:

chunk_iter = pd.read_csv("large_file.csv", chunksize=50000) result_chunks = [] for chunk in chunk_iter: chunk_clean = clean_data(chunk) result_chunks.append(chunk_clean) df_all = pd.concat(result_chunks, ignore_index=True)

分块处理时要注意去重和缺失值填充这些全局操作可能会跨块失效。比如去重需要记住已经出现过的订单号,可以在循环里维护一个集合,只保留集合中没见过的数据。

7.3 向量化操作优先

Pandas 执行性能差距最大的操作就是「遍历行」。能用内置向量化方法解决的问题,尽量不要用for循环逐行处理。

# 性能差:遍历修改 # for i, row in df.iterrows(): # df.at[i, "金额"] = clean_money(row["金额"]) # 性能好:向量化 + apply 结合 df["金额"] = pd.to_numeric(df["金额"].astype(str).str.replace(",", "", regex=False), errors="coerce")

字符串处理、条件筛选、缺失值填充、数值运算都尽量用 Pandas 自带方法,只有遇到非常复杂的自定义业务规则时才用apply,且先用df.sample(1000)做小数据量验证,再跑全量。

8. 常见问题与排查方法

问题现象可能原因排查方式解决方案
读取 CSV 报 UnicodeDecodeError文件编码不是 utf-8用记事本打开文件查看编码指定 encoding="gbk" 或 "gb18030"
读取 Excel 报缺少 openpyxl未安装 Excel 读取引擎查看报错信息pip install openpyxl
金额列 astype 转换报错列中存在逗号、货币符号、中文文本先打印 df["金额"].unique()先清理符号再用 pd.to_numeric(errors="coerce")
日期解析失败日期格式过多或包含非法日期打印无法解析的值用 errors="coerce" 把失败值置为 NaT
字段是字符串但想当数字算读取时类型推断错误查看 df.dtypes用 to_numeric 或 astype 转换
内存不足数据量过大或列数过多memory_usage 查看占用分块读取、只读关键列、类型转 category
drop_duplicates 后行数异常指定的 subset 列没有唯一标识性核对业务字段更换判断字段组合
批量处理卡住某个文件读取失败或数据量过大查看循环日志增加 try except 和超时观察
导出 CSV 后 Excel 中文乱码编码用了默认 utf-8用文本编辑器打开观察使用 encoding="utf-8-sig"

排查问题最重要的是先定位是「读入阶段」问题还是「清洗阶段」问题。读入阶段看报错信息,清洗阶段用unique()、value_counts()、isnull().sum()逐步缩小范围。建议把每个阶段的中间结果打印或导出,这样能快速定位是哪一步把数据搞坏了。

9. 最佳实践与使用建议

到这里,整套数据清洗流程已经跑通。最后补几条工程实践建议,这些才是实际项目里最值钱的部分。

第一,清洗前备份原始数据。不管多熟练,清洗都是有损操作,原始文件必须保留。批量处理时建议把原始文件放在data/raw/,清洗后放在data/clean/,目录分层清晰,避免误覆盖。

第二,清洗过程尽量有日志。打印或输出每个步骤前后的行数、字段数、缺失值数量。这样后期数据出问题,可以回溯是哪一步引入的。

第三,清洗逻辑模块化。不要把所有处理代码堆在一个脚本里,建议拆成read_data.py、cleaners.py、output.py这样的模块。clean_data()这种核心函数单独放,方便测试和复用。

第四,小数据验证再全量跑。先用df.sample(100)或df.head(1000)验证整个流程,确认逻辑正确后再处理全量数据。批量任务更要先拿一个文件试跑,再循环所有文件。

第五,涉及敏感数据必须脱敏。手机号、身份证、地址、银行卡号这类信息在展示、分享、提交代码到外部平台之前,一律做脱敏处理。数据来源要合法,处理权限要明确,这是数据工作者的基本底线。

第六,保留可复现环境。把依赖版本固定在requirements.txt里,比如pandas==2.2.2、numpy==1.26.4,方便别人或未来的自己复现结果。

如果你想继续深入,下一步建议按这个顺序走:先用 NumPy 补充数组运算能力,然后学习用 Matplotlib、Seaborn 做清洗后的数据可视化,接着用 Scikit-learn 把清洗后的结构化数据喂给机器学习模型,最后了解 DuckDB 或 PySpark 来处理 Pandas 撑不住的大数据量场景。

数据清洗的价值不在代码量,而在流程的稳定性和可维护性。先把本文这套“读取 -> 体检 -> 修复 -> 结构化 -> 批量 -> 性能优化”的流程练熟,再用自己的真实数据跑一遍,会比硬背几十个 API 有效得多。建议把文中的代码整理成自己的清洗工具模块,下次拿到新数据直接复用即可。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询