ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pandas数据清洗10步流程:从脏数据到整洁数据

Pandas数据清洗10步流程:从脏数据到整洁数据 写这篇文章的初衷很直接我做了几年的数据分析发现真正难的不是建模也不是可视化而是项目一开始的那道坎——数据清洗。你可能已经下载了一个Excel表正准备跑个回归或者画个图结果一打开日期格式五花八门金额带着逗号和单位姓名前后有空格还混着重复行和缺失值那一刻整个人都麻了。Pandas 是 Python 生态里做数据清洗绕不开的核心工具这套 10 步流程是我在实际项目中反复打磨出来的每一步都有对应的完整代码能把这堆“脏数据”变成可以直接分析的数据集。这篇文章适合所有被数据折磨过的人入门 Python 的学员、做数据分析和运营报表的同事、写爬虫偶尔需要整理数据的开发者。我会直接用一段虚构但非常典型的脏数据作为案例带你把每一步的代码、原理和坑全部过一遍。你不需要懂很深的编程基础跟着敲就行我尽量把“为什么这样做”也讲清楚。1. 开头先看效果从原始数据到整洁数据动手之前我先给你看一组真实感很强的脏数据。我们做任何清洗第一步不是写代码而是先搞清楚“脏”在哪里。下面我构造了一份包含用户ID、姓名、城市、注册日期、消费金额、年龄、VIP等级的模拟数据里面几乎埋了所有常见脏数据问题缺失值、重复行、日期格式混乱、字符串类型的数字、带货币符号和千分符的金额、数值列混入非数字字符、异常值、文本首尾空格。import pandas as pd import numpy as np df pd.DataFrame({ 用户ID: [U1001, U1002, U1001, U1004, U1005, U1006, None, U1008, U1008, U1010], 姓名: [ 张三, 李四, 张三, 王五, 赵六 , None, 孙七, 李四, 李四, 周八], 城市: [北京, 上海, 北京, 广州, 深圳, 杭州, None, 上海, 上海, 南京], 注册日期: [2024-01-15, 2024/02/20, 2024-01-15, 20240305, 2024-04-10, 2024-05-11, 2024年6月1日, 2024-02-20, 2024-02-20, 2024-07-30], 消费金额: [1,299.50, 899, 1,299.50, None, 3,500.00, 1200.5, 799, 899, 899, 12,000.00], 年龄: [25, 30, 25, 28, 35, None, 22, 30, 30, 120], VIP等级: [A, B, A, C, B, A, C, B, B, A] })这是典型的一眼看去就“脏”的表格。认真看你会发现五个问题用户ID存在空值和重复姓名列有人名前后带空格还有人缺失注册日期四种写法消费金额是字符串还带逗号和小数年龄里居然有个 120 岁——正常人不可能活到 120 岁还来注册你平台明显是异常值。这篇文章的 10 步就是逐一把这些问题拆解掉。清洗后的结果应该是这样所有缺失值被填充或删除重复行只保留一条日期统一成datetime64类型金额变成浮点数文本前后空格被清掉异常年龄被修正或标记最后索引连续数据可以直接用于后续的透视表、分组聚合和可视化。2. 数据清洗的全貌认识与核心理念进入具体步骤之前我先分享一个特别重要的项目经验不要一上来就删数据。很多人拿到数据看到缺失就dropna()看到重复就drop_duplicates()十分钟搞定一切。这种做法的后患很大因为缺失值在不同业务场景里有完全不同的含义。比如用户没有填城市可能只是注册时没授权定位用户没有消费金额可能他压根没下单。这两种情况前者可以考虑填充“未知”后者填充 0 是有偏的删掉可能又影响样本量。所以先搞清楚“这个字段为什么脏、脏了之后对分析目标有什么影响”再决定具体方案。数据清洗本质上是一个“把原始数据规整为统一、准确、可用格式”的过程。在 Pandas 里这个过程的核心理念可以用两句话概括一是“所有操作必须可复现、可回滚”所以每一步我建议你用新的变量名接收结果或者用df.copy()做一份副本不要原地df.fillna(...)一把梭万一后边发现填错了你得重跑整个脚本二是“先宽后严”先把数据的型态dtype和结构梳理清楚再去管具体业务规则上的异常。后续 10 步我大致分成四个板块准备与概览第 1、2 步、缺失值与重复值第 3、4 步、类型与文本规范化第 5、6 步、异常值与最终收尾第 7-10 步。你跟着这个顺序做基本能应付 90% 以上的数据清洗需求。3. 第 1-2 步概览全局与确认指标很多人拿到 DataFrame 第一件事就是df.head()看两眼然后开始瞎猜。我建议你养成一套固定的“体检”顺序info()看类型和缺失shape看规模describe()看数值分布nunique()看离散字段的去重数。这套组合拳打下来你对数据的大致问题已经有数了。3.1 用 info() 摸清列的类型和缺失状况DataFrame.info()是检查数据健康度最高频的函数。它会显示每一列的非空值数量、数据类型、内存占用。对上面这份数据执行df.info()后你能看到class pandas.core.frame.DataFrame RangeIndex: 10 entries, 0 to 9 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 用户ID 9 non-null object 1 姓名 9 non-null object 2 城市 9 non-null object 3 注册日期 10 non-null object 4 消费金额 9 non-null object 5 年龄 9 non-null float64 6 VIP等级 10 non-null object注意这里消费金额列在原始数据里其实只有 9 个非空有一个None所以它是 9 non-null。而年龄列显示 float64 是因为里面混入了NonePandas 会把整列推断成浮点型。你会发现所有带文本的列都是 object 类型这意味着后面必须做类型转换。3.2 用 describe() 和 nunique() 看数值分布与离散字段df.describe()默认只统计数值列它能很快告诉你哪些数值列存在异常量级。执行后你会看到年龄列的最大值是 120最小值是 22这数据一眼就不正常。对于非数值列用df[VIP等级].value_counts()和df[城市].nunique()来看分布和去重数量。提示nunique()有一个常用的dropnaFalse参数如果你想知道缺失值是否被单独当成一类可以加上它。业务上我经常用这个参数判断“未知”类别到底占多大比例。做完这个概览你就能列出一份“待清洗清单”。我习惯写在注释里比如用户ID有缺失、有重复姓名有空格和缺失注册日期格式不统一消费金额是字符串带千分符年龄有超大异常值。接下来的每一步都是对着清单逐一击破。4. 第 3-4 步缺失值处理策略缺失值是最常见的脏数据问题但也是最容易因为“一刀切”而搞坏数据的环节。处理缺失值之前务必先算清楚每一列的缺失比例。如果一列 90% 都是缺失那么填充的意义不大通常建议直接删列如果缺失比例低于 5%且样本量充足直接删除缺失行也可以接受如果缺失比例适中且这列对分析很重要就考虑填充。总之先量化再决策。4.1 精确计算缺失值分布用一行代码可以拿到所有列的缺失数量和比例missing_count df.isna().sum() missing_ratio df.isna().sum() / len(df) missing_df pd.DataFrame({缺失数量: missing_count, 缺失比例: missing_ratio}) print(missing_df)这段代码先拍平缺失矩阵再求和。为什么用isna()而不是isnull()两个函数效果完全一样isna()是isnull()的别名我习惯用isna()因为它和notna()在写法上更好配对。执行后你会看到用户ID、姓名、城市、消费金额、年龄各缺 1 行比例都是 10%。这是典型的中低度缺失可以填充而不是删行。4.2 三种缺失场景的攻略缺失值处理的“正确姿势”取决于业务含义。我把常见情况分成三类每一类有对应的做法第一类是分类文本列比如城市。用户可以来自未知城市用未知填充最合理既保留了样本又不引入有偏的数字猜测。代码是df[城市] df[城市].fillna(未知)。第二类是数值列比如消费金额。填充策略要看业务逻辑如果你的分析目标是“用户消费能力”那么没花钱的用户填 0 是合理的如果目标只是“有消费行为用户的平均客单价”那缺失的不应该当 0而是要么删除要么用均值/中位数填充。我这里把消费金额的缺失填成 0因为没有消费记录的用户就是没下单。年龄的缺失我用整个去重后的中位数填充因为单独看业务逻辑你没有任何依据去猜一个具体用户的年龄。第三类是标识列比如用户ID。这个不建议填充因为它是唯一性标识填了也是编造的。更合理的做法是如果该行其他关键字段也缺失直接删掉如果只是 ID 缺失但消费金额等信息都正常可以标记为“未知用户”保留。以下这组代码演示了差异化的处理策略我把每一列的填充规则写得很清楚# 第3步按业务场景处理缺失值 # 分类文本列城市填未知 df[城市] df[城市].fillna(未知) # 数值列消费金额缺失去填充没有消费记录就填0 df[消费金额] df[消费金额].fillna(0) # 数值列年龄缺失用中位数填充 median_age df[年龄].median() df[年龄] df[年龄].fillna(median_age) # 标识列用户ID缺失但金额存在标记为未知客户 df[用户ID] df[用户ID].fillna(U0000) # 姓名缺失用匿名用户标记避免后续groupby时丢失该样本 df[姓名] df[姓名].fillna(匿名用户)4.3 什么时候应该直接删除缺失行补充一个被很多人忽略的场景。如果一行数据里核心分析字段大面积缺失比如用户ID、消费金额、注册日期这三个关键字段全为空那填充就没有意义直接删除。不要担心样本量减少脏数据带来的分析偏差远比样本量减少更危险。删除用df.dropna(subset[用户ID, 消费金额], thresh2)意思是只在用户ID和消费金额这两个字段上判断至少保留 2 个非空值才保留该行。thresh参数是很多人不知道但很实用的删除逻辑防止误删基本完整的记录。5. 第 5-6 步重复值检测与数据格式统一重复值是最容易处理的脏数据但也藏着细节。Pandas 里duplicated()方法默认是判断整行所有列是否完全一致但实际业务里“完全一致”才算重复情况很少更多时候是某个标识列重复了。比如用户ID为 U1001 的两行消费金额一样但可能一个是补登的记录一个是正式记录如果只看整行会认为它们不重复结果就会污染统计。所以处理重复值时一定要搞清楚“以哪个维度判断重复”。5.1 基于关键列的重复值处理拿这份数据来说用户ID出现 U1001 两次、U1008 两次。我的处理逻辑是只保留每个用户ID的第一条记录因为从业务上看一个用户的注册信息只需要保留一行。代码是# 第4步基于关键列处理重复值 # subset指定判断重复的列keepfirst保留第一条 df df.drop_duplicates(subset[用户ID], keepfirst)执行后数据会从 10 行变成 8 行。这里我多说一句keep参数有三个选择first保留第一条last保留最后一条False删除所有重复行包括原本那条。实务中如果两条记录的消费金额不一致比如一个 100 一个 200你就需要先做聚合再做去重而不是简单保留一条否则信息会丢失。5.2 字符串数字转数值千分符与货币符号的坑接下来是做类型统一。先看消费金额它现在是 object 字符串里面含有逗号和小数点不能直接参与求和。你需要两步操作第一步把千分符,去掉第二步astype(float)转成浮点。如果你直接astype(float)Pandas 会报ValueError因为1,299.50不是一个合法的数字字面量。所以必须先清洗再转换。下面是完整的处理代码# 第5步统一数据类型 # 消费金额去逗号 - 转浮点 df[消费金额] df[消费金额].str.replace(,, , regexFalse).astype(float) # 年龄转整数因为年龄不可能是小数 df[年龄] df[年龄].astype(int)这里str.replace(,, , regexFalse)的regexFalse值得特别说明。早期版本里str.replace默认把第一个参数当正则表达式逗号本身没特殊含义倒也安全但如果你要替换的是$、.、这类有正则含义的字符不写regexFalse会出现结果完全不对甚至报错。所以我现在一律加上regexFalse表示做字面替换避免手滑踩雷。处理完再打印df.dtypes你会看到消费金额已经变成 float64年龄变成 int32。5.3 日期统一to_datetime 处理四种格式日期列在全项目中是重灾区。同一个业务系统导出日期可能有2024-01-15、2024/02/20、20240305、2024年6月1日这四种格式。Pandas 的to_datetime能自动识别大部分常见格式但需要指定format或使用灵活的解析器# 日期统一为datetime类型 df[注册日期] pd.to_datetime(df[注册日期], errorscoerce)to_datetime的errorscoerce参数非常关键。它的意思是遇到无法解析的日期不要报错中断而是转成NaTNot a Time这样脚本能继续跑下去后续你再统一处理这些坏值。如果不加这个参数遇到解析不了的格式整个程序直接抛异常后面代码全部白跑。跑完这行如果数据里还有NaT说明那几行日期格式是 Pandas 默认识别不了的你需要输出这些行看看具体是什么格式再用自定义format去解析。注意日期处理完你还可以用dt访问器提取年、月、日、星期等特征。比如df[注册年份] df[注册日期].dt.year这在后续做时间维度分析时非常方便在清洗阶段顺手把特征空间扩大了。6. 第 7-8 步文本规范化与异常值处理文本脏数据没有标准答案但套路是固定的去空格、去符号、统一大小写、统一同义表达。异常值处理则需要结合业务和统计学方法核心是不要一棍子打死要把异常值区分成“录入错误”和“真实极端值”两类。6.1 姓名与分类文本的清理姓名列存在 张三这种前导空格和赵六 这种尾随空格。这些空格在分组统计时会被当成不同的值直接导致“张三”和“ 张三”被分成两行。处理很简单用str.strip()去掉首尾空格。如果数据里的空格不是首尾而是中间比如“张 三”那strip没用要用str.replace( , )。城市列也可能存在大小写不一致、中英文混用的情况先把所有城市文本做 strip再用str.title()或统一替换成规范表达。# 第6步文本规范化 df[姓名] df[姓名].str.strip() df[城市] df[城市].str.strip()对于这种中文字段大小写规范意义不大重点是去空格、去干扰符号、统一成词汇表中的标准值。如果你在做业务系统对接建议维护一个“标准值映射表”把北京、beijing、BJ都统一成北京市这种映射清洗用df[城市].replace(mapping_dict)一行搞定。6.2 用 IQR 法识别年龄异常值年龄列里那个 120 就是典型的异常值。怎么用代码识别最常用的方法是四分位距IQR法计算 Q125% 分位数和 Q375% 分位数IQR Q3 - Q1通常认为小于 Q1 - 1.5 * IQR 或大于 Q3 1.5 * IQR 的值是离群点。用 Pandas 实现如下# 第7步用IQR法检测异常值 Q1 df[年龄].quantile(0.25) Q3 df[年龄].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR anomaly_mask (df[年龄] lower_bound) | (df[年龄] upper_bound) print(异常值数量, anomaly_mask.sum())跑完后你会看到只有 1 个异常值就是那个 120。处理异常值有三种方式删除、修正、封顶。120 岁这个明显是录入错误你可以直接修正为缺失值然后走前面的缺失值填充流程如果你的业务里年龄的正常范围是 18-80也可以把超过界的值替换成边界值这叫“封顶处理”。这里我选择用中位数修正因为它保留了对整体数据的影响可控。6.3 异常值不是一定得删这里我要特别提醒一点异常值并非总是脏数据。比如订单金额列如果有人买了 10 万块钱的电脑它在数据里就是金额极大的“异常值”但它真实存在不能删。所以 IQR 检测出来的只是一个候选集合你需要结合业务判断看到异常值先打标签而不是直接删。在实际项目里我通常先加一列is_anomaly标记把异常行过滤掉做一次分析再保留异常行做一次分析对比业务结论是否稳健。如果两次结论一致说明异常值不影响大局如果不一致就要深挖原因。7. 第 9-10 步标准化列名、索引重置与最终校验清洗到这一步数据已经比较干净了。但还有一个细节很多人会忽略列名乱、索引跳。drop_duplicates和删除缺失行之后索引会变得不连续比如 0、2、5、7这在后续groupby之后做索引合并时会引发各种诡异问题。列名如果带空格或大小写不一致取列时容易踩坑。所以收尾时一定要做标准化。7.1 列名规范与索引重置我习惯把列名统一成小写英文或规范中文去掉空格和特殊符号。这里为了展示我把中文列名保持原样但将列名中的字符统一处理掉前后空格。然后用reset_index(dropTrue)重置索引dropTrue表示不保留旧索引作为新列# 第8步列名标准化与索引重置 df.columns df.columns.str.strip() df df.reset_index(dropTrue)重置索引这一步很多人觉得可有可无但在真实环境里你在清洗过程中一旦做过dropna或drop_duplicates索引就会留下很多空洞。如果不重置后续df.loc[5:10]这种切片会得到和预期完全不一致的结果调试起来非常痛苦。所以清洗流程的最后务必重置索引。7.2 清洗结果的最终校验清洗完不等于结束还得做一次全量校验。我有一套固定的验证清单看df.info()确认所有列类型正确、无缺失残留看df.duplicated(subset[用户ID]).sum()确认重复值为 0看数值列的describe()确认最大值和最小值都在合理范围。还有一个非常实用的方法把清洗前后数据集的样本量打出来对比你会清楚每一步删了多少行、填了多少值。逻辑上数据行数应该是从 10 行去重后变成 8 行缺失填充后仍为 8 行。# 第9步全量校验 print(剩余缺失值数量: , df.isna().sum().sum()) print(重复用户ID数量: , df.duplicated(subset[用户ID]).sum()) print(df[消费金额].describe())最后把清洗后的数据导出为 CSV 时我强烈建议指定encodingutf-8-sig因为 Excel 默认用 GBK 打开无 BOM 的 UTF-8 文件会乱码utf-8-sig会加一个 BOM 头让 Excel 能正常识别中文。如果文件要给别人用这一步极其重要我见过太多同事因为忘了这个参数被业务方吐槽“导出乱码”。# 第10步导出结果 df.to_csv(clean_users.csv, indexFalse, encodingutf-8-sig)8. 把 10 步封装成一条完整的数据清洗流水线一个人日常写点分析脚本无所谓但如果你要把这套清洗流程用于重复执行的报表任务或者交接给同事最好把它封装成一个函数。这也是我从踩坑中得出来的经验一开始我总在每个脚本里重复写清洗逻辑后来需求一变我改了一个字段的填充规则结果要同步改七八个脚本改漏一个就出问题。封装成函数之后所有清洗逻辑集中在一处改动只需要改函数内部其他地方无需动。下面这个函数整合了上面所有步骤你直接复制到项目里就能用。这个函数有一个特点入参是原始 DataFrame返回值是清洗后的 DataFrame内部做了拷贝。为什么用df.copy()因为 Pandas 很多操作是视图拷贝原地修改有触发SettingWithCopyWarning的风险拷贝一份能避免后续“改了原表”的坑。def clean_user_data(raw_df: pd.DataFrame) - pd.DataFrame: 用户数据清洗流水线输入原始DataFrame返回清洗后的DataFrame df raw_df.copy() # 1. 缺失值处理 df[城市] df[城市].fillna(未知) df[消费金额] df[消费金额].fillna(0) median_age df[年龄].median() df[年龄] df[年龄].fillna(median_age) df[用户ID] df[用户ID].fillna(U0000) df[姓名] df[姓名].fillna(匿名用户) # 2. 重复值处理 df df.drop_duplicates(subset[用户ID], keepfirst) # 3. 类型转换 df[消费金额] df[消费金额].str.replace(,, , regexFalse).astype(float) df[年龄] df[年龄].astype(int) df[注册日期] pd.to_datetime(df[注册日期], errorscoerce) # 4. 文本规范化 df[姓名] df[姓名].str.strip() df[城市] df[城市].str.strip() # 5. 异常值处理IQR法超界值用中位数替代 Q1 df[年龄].quantile(0.25) Q3 df[年龄].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR df.loc[(df[年龄] lower_bound) | (df[年龄] upper_bound), 年龄] int(df[年龄].median()) # 6. 索引重置 df df.reset_index(dropTrue) return df clean_df clean_user_data(df) print(clean_df)封装的时候有两个容易被忽略的细节。第一个是函数内部步骤的顺序我的顺序是先处理缺失再去重再类型转换再文本再异常值。这个顺序不是随便定的比如如果先去重后填缺失重复行里可能包含非缺失值去重会帮助保留更完整的行反过来如果先填缺失再去重就会把原本缺失值错填成猜测值反而增加误差。第二个细节是df.loc[条件, 列名] 值这种写法比df[年龄][条件]更好因为df[年龄][条件]在链式赋值时容易触发SettingWithCopyWarning而你用内层函数封装时这类警告特别容易让使用函数的人困惑。9. 常见问题与避坑技巧实录我把日常做数据清洗时遇到的高频问题做了一个速查表。这些问题几乎在每次培训、每个新项目里都会出现如果你也遇到类似现象直接对照表格排查原因。问题现象原因分析解决方案astype(float)报 ValueError字符串列中混有,、$、%等符号先用str.replace去掉符号再转换类型日期解析后全是 NaT日期格式特殊to_datetime默认识别不了用pd.to_datetime(s, format%Y年%m月%d日, errorscoerce)指定格式清洗后数据行数不对重复值删除或缺失值删除导致行数减少每次删除操作后打印len(df)并在最后记录清洗前后行数dropna()把不该删的行删了默认按整行所有字段判断任一字段缺失就删用subset指定关键字段用thresh指定非空数量阈值分组统计结果出现同一用户多行重复行没有被完全识别存在字段差异先按业务主键去重必要时先聚合再降重导出 CSV 用 Excel 打开乱码缺少 BOM 标记Excel 用 GBK 解析 UTF-8导出时指定encodingutf-8-sig文本列有不可见字符空格、换行、制表符混在字符串里用str.strip()去首尾空白用str.replace(\n, )去换行用repr()查看真实内容除了速查表我再说一个很实际的避坑经验不要过度依赖 view 和拷贝的默认行为。Pandas 有一个历史坑叫“链式索引”比如df[df[年龄] 30][城市] 某市这种写法经常不会真正修改原 DataFrame甚至会报SettingWithCopyWarning。我见过不少同事在这个地方排错了半小时最后发现数据根本没改。我的习惯是只要涉及给子集赋值一律用.loc。这句代码的正确写法是df.loc[df[年龄] 30, 城市] 某市语义清晰且不容易出错。还有一个点是关于apply和自定义函数的适度使用。有些新手学了apply之后什么操作都喜欢写个 lambda 跑一遍比如df[金额] df[金额].apply(lambda x: float(x.replace(,,)))。这个写法没错但能用 Pandas 内置向量化方法解决的尽量不要用循环和 apply因为数据量一大性能差距是几何级别的。比如去除逗号df[金额].str.replace(,, , regexFalse)是向量化的处理 100 万行也很快而apply版本会明显慢很多。数据清洗的项目到后面往往要跑大表一开始就应该养成用向量化操作的习惯。最后分享一个我在实际项目里的个人习惯每次清洗完我不会只看head()而是会把清洗前后的数据用describe()对比一遍。比如消费金额列的 mean 发生了什么变化年龄列的 max 有没有回到合理区间分类字段的 value_counts 有没有出现“未知”这种填充值。如果数据清洗改变了核心指标我会回头检查每一步的填充规则是否合理而不是直接信任代码跑出来的结果。数据清洗这个活真正的核心不是敲代码而是你对业务的理解以及一遍遍校验的耐心。
返回列表