Python数据清洗实战:环境准备、核心技术到高效管道

发布时间:2026/9/8 2:58:26
Python数据清洗实战:环境准备、核心技术到高效管道 2. 环境准备先把自己的Python环境收拾利索2.1 不同操作系统下的Python安装建议写数据清理之前先得把Python这个厨房搭好。我见过太多人卡在第一步装了Python但不知道装在哪pip装包报错或者Mac上同时存在系统自带Python、Homebrew版Python、Anaconda版Python互相打架最后连import pandas都跑不通。建议项目的每个依赖版本都能稳定复现这才是数据清理工作的底线。如果你是Windows用户直接去官网下安装包就行记得勾选Add Python to PATH这个选项不勾命令行里敲python大概率没反应。macOS用户则要多留个心眼——系统自带的Python 2早就退役了新版macOS虽然预装了Python 3但那是给系统工具用的权限受限。我建议统一用Homebrew安装brew install python3.11装完之后python3 --version确认版本再把/opt/homebrew/bin加进PATH。Linux用户则可以用apt install python3-pip或者编译安装但建议优先用系统包管理器后面省心很多。再多说一句关于macOS数据的清理。很多人在Mac上堆积了多个Python发行版Xcode命令行工具带一套、Homebrew带一套、Anaconda又带一套磁盘空间被吃掉好几个G。如果你也是这种情况建议只保留一个主力环境把其他卸载干净。Anaconda如果长期不用rm -rf ~/opt/anaconda3再清理~/.zshrc里的初始化脚本能腾出不少空间。这些操作和后面要讲的数据清理很像——先把无用数据清除再让核心流程跑得更快。2.2 用venv隔离项目依赖被低估的保命技能很多人装包习惯直接用全局环境pip install xxx一路装到底。短时间没问题但项目一多就乱套A项目需要pandas 1.3B项目需要pandas 2.0装完B再把A跑一遍直接报错。解决这件事不用装额外工具Python自带的venv就够用。# 创建虚拟环境 python3 -m venv .venv # 激活虚拟环境macOS/Linux source .venv/bin/activate # Windows .venv\Scripts\activate # 安装依赖 pip install pandas numpy openpyxl虚拟环境相当于给每个项目一个独立的小房间依赖互不干扰。数据清理这种探索性质很强的工作经常要临时装新库试效果有了venv随便折腾环境坏了直接删掉重建五秒钟的事。我自己的习惯是每个数据项目都新建一个venv并在项目根目录放一个requirements.txt记录依赖这样换电脑、换同事一条pip install -r requirements.txt就能把环境恢复出来。2.3 推荐使用的核心库做数据清理我的主力工具箱就四个库每个都有不可替代的位置库名主要用途使用场景pandas表格数据处理、缺失值/重复值清洗90%的数据清理工作numpy数值计算、数组操作数值异常检测、向量化计算openpyxlExcel文件的读取和写入处理.xlsx格式的业务数据matplotlib/seaborn数据可视化通过图表快速发现异常值有些场景还需要re正则表达式处理文本或者datetime解析时间字段这些是Python标准库虚环境里默认就有。但核心就一句话pandas负责干活numpy负责算数openpyxl负责兼容Excel可视化库负责帮你看出问题。别一上来就装一堆花里胡哨的库先用这四个等不够了再说。3. 数据加载先把数据装进碗里3.1 Common数据格式的读取姿势数据清理的第一步是把原始数据读进DataFrame里。不同格式的读取方法差别不小我逐个说下最常见的三种CSV文件。这是最常见的数据交换格式pandas一行就能读import pandas as pd df pd.read_csv(sales_data.csv, encodingutf-8)但坑也最多。encoding参数我建议显式指定别用默认值。国内很多系统导出的CSV其实是GBK/GB18030编码你不指定的话轻则乱码重则直接报UnicodeDecodeError。遇到乱码时把encoding换成utf-8、gbk、gb18030逐个试实在不行可以用chardet库自动检测编码import chardet with open(sales_data.csv, rb) as f: result chardet.detect(f.read(10000)) print(result[encoding]) # 比如 {encoding: utf-8}Excel文件。用pd.read_excel()读取但前提是装了openpyxl或xlrd库。实际业务里Excel常常是一锅炖多个sheet、合并单元格、标题行不在第一行、表头下面还有一行单位说明。我的经验是把所有的读取参数一次配齐df pd.read_excel( report_2024.xlsx, sheet_name订单明细, # 指定sheet header0, # 第0行为列名 skiprows[1], # 跳过多余的行 dtype{订单号: str} # 强制订单号读成字符串 )dtype这个参数值得多说一句。像订单号、手机号、身份证号这些列如果让pandas自动判断类型很可能会被读成数字类型然后前导零被吃掉、长数字被转成科学计数法清洗的时候悔到肠子青。所以读数据的时候就把这些列的dtype指定成str后面能少哭一遭。JSON文件。JSON在接口数据、配置文件里非常常见。读取方式分两种纯JSON列表直接pd.json_normalize()展开成表格嵌套JSON则要先手动拍平。比如接口返回的数据是嵌套的{user: {name: 张三, age: 25}, order: {id: A1001, amount: 99.5}}可以用json_normalize自动展开。3.2 先看一眼数据再动手拿到DataFrame之后千万别急着清洗先做三件事看轮廓、看类型、看前几行。# 1. 看数据规模多少行多少列 print(df.shape) # 2. 看每一列的类型、非空值数量 print(df.info()) # 3. 看前5行数据长什么样 print(df.head())df.info()是我最依赖的命令之一它会告诉你每一列的数据类型dtype、非空值数量、内存占用。通过它你一眼就能看出哪些列存在缺失值非空值数量小于总行数、哪些列类型不对比如数值列显示object、整张表有多大。df.head()则能帮你快速判断数据格式是否符合预期日期列是不是标准的YYYY-MM-DD、金额列有没有混入奇怪的符号、文本列有没有明显的前后空格。把这些信息拼起来你对这堆数据脏不脏心里就有底了。3.3 大文件加载的备选方案有些CSV文件动辄几个G直接read_csv会把内存吃满。碰到这种情况有两个常用的降级策略策略一分块读取按需处理chunk_iter pd.read_csv(huge_data.csv, chunksize100000) for chunk in chunk_iter: # 每块100000行逐块处理 process(chunk)策略二只读需要的列如果你只需要其中几列用usecols参数减少内存占用df pd.read_csv(huge_data.csv, usecols[订单号, 金额, 时间], dtype{订单号: str})4. 数据探索清洗前必须做的基础功课4.1 Describe info 的组合用法数据清洗不是拿起来就干先得了解数据的基本盘。我的习惯是先跑一遍df.describe(includeall)print(df.describe(includeall))这个命令会输出每个列的非空值个数、唯一值个数、众数、均值、标准差、最小值、四分位数、最大值。通过这些统计量你能快速发现几类异常数值列的最小值是负数而业务场景里金额/数量不应该为负最大值是天文数字明显超出合理区间某列唯一值个数极少可能是分类变量也可能这列根本就是脏数据众数和均值差得远说明数据分布偏斜可能混入了异常值df.info()看类型df.describe()看分布两者配合就能对整张表有个大概画像。4.2 用可视化辅助发现异常统计数字有时候会骗人。均值100的数据可能是99个1和1个10000算出来的光看数字发现不了画个图就一目了然了。这是我一直强调的经验清洗前先画图哪怕只是简单的分布直方图也比盯着数字看半天管用。import matplotlib.pyplot as plt # 画金额分布直方图 plt.hist(df[amount], bins50) plt.show()如果图里出现一座山峰加一个孤立的柱子那个孤立的柱子基本就是异常值。箱线图也是检测异常值的好工具它会把超出1.5倍四分位距的点直接标出来。用五分钟画图能省下两小时的盲目清洗。4.3 定位重复数据和空值分布这两件事也是探索环节就要做的# 每一列的空值数量 print(df.isnull().sum()) # 全表重复行数量 print(df.duplicated().sum())空值的分布情况很关键——是集中在某几列还是分散在各列是少量散落还是某列有超过一半的缺失这些信息决定了后面用哪种方式处理缺失值。重复行数据也要先摸清楚如果重复行特别多要怀疑是数据采集逻辑出了问题而不是简单drop_duplicates()掉就完事。5. 核心清洗技术上缺失值处理的完整方案5.1 三种处理思路删、填、留缺失值是数据清理最核心的阵地。处理方法说穿了就三种删掉、填上、保留。但每种方案适用场景完全不同做错了会引入严重偏差。方案一删除缺失值# 删除所有含缺失值的行 df_dropna df.dropna() # 只在某列有缺失时才删除该行 df_dropna_col df.dropna(subset[订单号]) # 删除整列都是空值的列 df df.dropna(axis1, howall)删除适用于缺失比例很低比如不到1%的场景。比如100万行里有3000行缺了一个字段直接删掉不影响大局。但如果缺失比例达到30%删掉就太奢侈了信息损失太大。方案二填充缺失值# 用固定值填充 df[渠道] df[渠道].fillna(未知) # 用均值/中位数填充数值列 df[金额] df[金额].fillna(df[金额].median()) # 用众数填充分类列 df[城市] df[城市].fillna(df[城市].mode()[0]) # 时间序列数据用前向/后向填充 df[价格] df[价格].fillna(methodffill)固定值适合分类变量未知本身就是一个合理的类别均值/中位数适合数值变量但要注意用均值填充会让数据的方差变小过度集中在中位数附近所以业务数据如果后续要做统计分析得慎重前向填充ffill适合时间序列比如股票价格偶尔缺一两天的数据用前一天的值补上是合理的。方案三保留缺失值有些模型本身就能处理缺失值比如XGBoost、LightGBM它们在分裂节点时会自动处理缺失情况。如果你用这类模型缺失值可以保留。有些统计场景下缺失本身也是信息——比如用户没有填写收入这可能意味着他不太愿意透露财务状况这个缺失本身就有业务含义。5.2 缺失值处理的决策心法说了三种方案但具体到某份数据怎么选我的判断标准就三条缺失比例低于5%行数又不算少直接删行干净利落。缺失比例在5%-30%之间列是重要特征填充。数值列用中位数分类列用众数有偏态用中位数。缺失比例超过30%又要靠这一列做分析建议把是否缺失单独做成一个二值特征0/1然后缺失值随便填一个占位值让模型自己学。还要特别注意一种情况业务上的空值本身有意义。比如退款时间为空意味着这笔订单没有退款这时候你把它删了或者填了反而破坏了业务信息的完整性。做个是否退款的二值特征才是正确操作。5.3 时间列与文本列的缺失特殊处理时间列缺失一般只能删除或者保留缺失标记因为填一个中位数日期在业务上没有意义。文本列缺失也有讲究比如地址的省市区三级省市有值但区为空这种不是真缺失是数据采集粒度的问题可以保留如果整条地址全为空那就看其他字段是否支持补充。6. 核心清洗技术下重复值、异常值与类型纠偏6.1 重复值的几种去重姿势drop_duplicates()是去重的主力方法但细节里有魔鬼# 完全重复的行 df df.drop_duplicates() # 只根据某几列判断重复 df df.drop_duplicates(subset[订单号]) # 保留第一个还是最后一个 df df.drop_duplicates(subset[用户ID], keepfirst) df df.drop_duplicates(subset[用户ID], keeplast) # 直接看哪些行重复 df[df.duplicated()]最常踩的坑是你以为的重复在业务上不是重复。比如同一用户同一商品买了两单它们在用户ID商品ID上重复但其实是两笔独立交易。所以去重之前一定要想清楚唯一的粒度是什么。还有有些系统导出的数据格式完全一致但中间夹杂着空行或注释行这种要先过滤——df[df[订单号].notna()]。6.2 异常值检测三种方法与组合拳异常值检测的方法有不少但核心思路是找出偏离正常范围的值。三种最常用的方法方法一3σ原则标准差法如果数据近似正态分布99.7%的值会落在均值±3个标准差范围内超出这个范围的就视为异常。mean df[金额].mean() std df[金额].std() df_outlier df[(df[金额] mean 3 * std) | (df[金额] mean - 3 * std)]但这个方法的弱点是如果数据本身就被某几个极端值污染了均值和标准差本身也不可靠。所以3σ原则更适合值域稳定的指标比如体重、温度、客单价。方法二IQR四分位距法用四分位数来判断异常值更稳健。Q1是25%分位数Q3是75%分位数IQRQ3-Q1超出Q1-1.5*IQR和Q31.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_clean df[(df[金额] lower_bound) (df[金额] upper_bound)]IQR法的好处是对分布没太多要求中位数和四分位数不受极端值影响。这也是我日常最常用的方法。方法三业务规则法业务规则往往比统计方法更直接有效。比如订单金额不能为负、年龄在0-120之间、折扣率在0-1之间这些规则都是领域知识统计方法永远替代不了。df df[(df[金额] 0) (df[金额] 100000)] df df[(df[年龄] 0) (df[年龄] 120)]我实际做项目时的习惯是先用业务规则过滤确定性的非法值再用IQR法寻找潜在的异常点最后把筛出来的异常点打印出来人工过一遍。别用纯统计方法直接删除有些异常值可能就是高价值客户或者真实的大单。统计方法是提示业务判断才是裁决。6.3 数据类型纠偏dtype不对什么都白搭数据清理里有一项特别不起眼但特别破坏力的工作——类型不匹配。这个字段的坑我踩过太多次差点把日期当成数字做了加减。# 强行转换数值类型 df[金额] pd.to_numeric(df[金额], errorscoerce) # 统一转成字符串 df[订单号] df[订单号].astype(str) # 日期时间解析 df[日期] pd.to_datetime(df[日期], format%Y-%m-%d, errorscoerce)重点说下errorscoerce这个参数它会让转换失败的值变成NaN缺失值。这样做的意义是类型转换不会直接把程序搞崩而是把脏数据暴露成缺失值你再看isnull().sum()就知道有多少脏值混进去了。用errorscoerce就是先让它跑起来再看看哪里有问题的思路。日期字段尤其容易出问题。业务系统导出的日期格式五花八门2024/1/1、2024-01-01、20240101、01/01/2024。pd.to_datetime()大多能自动识别但最好用format参数显式指定这样既能提高解析速度也能避免歧义。7. 文本数据清理被低估的工作量7.1 前后空格与大小写小问题大麻烦文本清洗看似没什么技术含量但ABC公司和ABC公司 末尾多一个空格在分组统计时会被当成两个不同的公司。等做完了报表发现数据对不上回头排查半天结果就是空格造成的。# 去掉字符串首尾的空格 df[公司名称] df[公司名称].str.strip() # 统一大小写 df[邮箱] df[邮箱].str.lower() # 替换多余的空格 df[备注] df[备注].str.replace(r\s, , regexTrue)我处理文本数据的铁律凡是来自人工录入的字段一律先strip再lower。人工录入就意味着不可靠这是数据分析行业的原罪。7.2 用正则表达式清洗格式混乱的字段有些文本字段光靠strip解决不了。比如电话号码混入了86、-、(区号)等各种格式或者地址字段里混杂了街道名小区名楼栋号想统一格式就需要正则表达式。# 提取所有数字 df[电话号码] df[联系电话].str.findall(r\d).str.join() # 把多个连续空白字符替换成单个空格 df[地址] df[地址].str.replace(r\s, , regexTrue)正则表达式的学习曲线有点陡但做文本清理绕不过去。我的建议是别尝试一次写出完美正则先写一个能覆盖80%情况的跑一遍看结果再迭代加强。清理的效果验证很容易用df[列名].unique()看看去重后的数据长什么样一目了然。7.3 字符串碎片切分与合并一个字段里塞了多个信息的情况也很常见。比如北京市-朝阳区-望京街道把省市区分到了一列里可以用str.split拆开df[[省份, 城市, 区县]] df[地区].str.split(-, expandTrue, n2)反过来多个字段要合并成一个比如把年份、月份、日期合成完整日期df[完整日期] df[年份].astype(str) - df[月份].astype(str) - df[日期].astype(str)8. 数据清洗流程再造从脚本到可复用管道8.1 把清洗逻辑写成一个函数刚开始做数据清理的时候我习惯写一个很长的脚本从上到下一条条改数据。后来发现一个问题换了新数据明明清洗逻辑差不多脚本却因为各种硬编码跑不通。更好的做法是把清洗逻辑封装成函数每类清洗一个函数最后组合成一个流程管道def clean_orders(df): 订单数据的标准清洗流程 # 1. 去重 df df.drop_duplicates(subset[订单号]) # 2. 类型纠偏 df[金额] pd.to_numeric(df[金额], errorscoerce) df[日期] pd.to_datetime(df[日期], errorscoerce) # 3. 过滤非法值 df df[(df[金额] 0)] # 4. 填充缺失 df[渠道] df[渠道].fillna(未知) # 5. 文本清洗 df[用户] df[用户].str.strip() return df df_clean clean_orders(df)把清洗逻辑封装成函数有这么几个好处一是逻辑清晰每一步都有注释说明二是可复用下次来一批结构类似的订单数据直接调用同一个函数三是每个项目沉淀下来的清洗函数会变成一个内部工具库随着做过的项目越多工具库越完善新项目起步越快。8.2 数据血缘清洗前后都留一份备份这是个惨痛教训换来的经验。有一次我清洗完数据直接覆盖了原DataFrame结果发现清洗逻辑有误需要回看原始数据但原始数据已经没了只能重新导一遍。从那以后我立了一条规矩原始数据永不覆盖清洗结果单独保存。# 原始数据保存一份 df_raw pd.read_csv(src_data.csv) # 清洗过程在副本上进行 df df_raw.copy() # 清理完成后把结果存成独立文件 df_clean clean_orders(df) df_clean.to_csv(clean_data.csv, indexFalse, encodingutf-8-sig)另外特别提醒保存CSV时加上encodingutf-8-sig。因为如果你用Excel打开一个纯utf-8编码的CSV中文会乱码utf-8-sig会带一个BOM头Excel就能正确识别。这个细节如果你不处理交付的成果在同事那里大概率会被打回来。8.3 把清洗管道做成可重用的模板做数据清理做久了你会发现套路都是相通的。不同的业务数据清洗流程的骨架几乎一致只是细节参数不同。我目前常用的模板长这样def build_clean_pipeline(raw_df, config): config 里配置各个阶段的策略 { dedup: {subset: [订单号], keep: first}, dtype: {金额: float, 日期: datetime, 订单号: str}, fillna: {渠道: 未知, 金额: median}, filter: {金额: (, 0), 日期: (, 2024-01-01)} } # 1. 复制原始数据绝不原地修改 df raw_df.copy() # 2. 去重 if dedup in config: df df.drop_duplicates(**config[dedup]) # 3. 类型转换 if dtype in config: for col, dtype in config[dtype].items(): if dtype datetime: df[col] pd.to_datetime(df[col], errorscoerce) elif dtype str: df[col] df[col].astype(str) else: df[col] pd.to_numeric(df[col], errorscoerce) # 4. 缺失值填充 if fillna in config: for col, strategy in config[fillna].items(): if strategy median: df[col] df[col].fillna(df[col].median()) elif strategy mode: df[col] df[col].fillna(df[col].mode()[0]) else: df[col] df[col].fillna(strategy) # 5. 业务规则过滤 if filter in config: for col, (op, val) in config[filter].items(): if op : df df[df[col] val] elif op : df df[df[col] val] elif op : df df[df[col] val] return df这个模板的精髓在于把清洗规则参数化逻辑本身不随项目变化。用这个模板面对一个新的数据源你需要做的是读数据看看info()和describe()然后写一份config字典。清洗管道本身在多个项目中反复使用bug被消灭得很干净而且每个项目的清洗逻辑都记录在配置里过了三个月回来看也知道当时做了啥。9. 常见问题与排查技巧实录9.1 常见报错与解决方案速查表我在数据清理项目里踩过的坑整理成了一张速查表如果你也遇到类似的问题可以参考报错/问题原因分析解决方案UnicodeDecodeError文件编码不是utf-8改用encodinggbk或先用chardet检测SettingWithCopyWarningDataFrame切片后做赋值用.copy()显式复制避免链式赋值KeyError列名拼写不对或者列名有隐藏空格df.columns先查看实际列名必要时df.columns.str.strip()NaN看起来是文本有些源文件里用NA/-占位空值用pd.read_csv(..., na_values[NA, -, ])指定缺失值标记日期解析变成NaT数据里有非标准日期格式先用errorscoerce识别哪些值有问题再针对性处理内存爆炸文件太大一次性读入分块读取、只读需要的列、降低dtype精度Excel打开CSV乱码编码是utf-8无BOM保存时用encodingutf-8-sig9.2SettingWithCopyWarning到底在提醒什么这个是pandas新手最容易踩又最容易忽视的警告。我直接说结论当你对一个DataFrame切片得到的子DataFrame进行赋值时pandas不确定你是想改子集还是改原表所以会警告你。这种不确定性非常危险因为结果依赖于当时的索引状态——可能这次碰巧改对了下次同样的代码就静默失效。正确的做法是如果要修改原表直接操作原表如果要操作副本先.copy()再操作。迷之警告出现的时候别用pd.options.mode.chained_assignment None去屏蔽它那是鸵鸟政策。花两道墙的时间理解链式赋值的原理以后会省非常多的事。9.3 小技巧数据清理前先做一份异常报告与其清洗完发现结果对不上再回头排查我建议在清洗前先自动生成一份异常报告report { 原始行数: len(df), 重复行数: df.duplicated().sum(), 各列缺失值: df.isnull().sum().to_dict(), 金额负数: (df[金额] 0).sum(), 未知渠道: (df[渠道] ).sum(), } print(report)这份报告的价值在于它给了你清洗前后的对照基线。清洗完了再生成一次报告两次一对比你就能清清楚楚地知道每一步清洗影响到了多少数据。如果发现未知渠道从100个变成了0个但你明明没处理渠道列——那一定哪里出了问题趁早排查。最后再分享一点我自己的感受。做数据清理这行干久了最大的体会是数据清理没有一次到位这回事。你永远不知道下一批数据里会藏着什么新花样可能是编码又变了可能是源系统规则调整了可能是某个字段突然多了一种新格式。所以不要期望写一个脚本一劳永逸而是要养成好习惯先探索、再清洗、留备份、做报告。这四条坚持下来不管数据多脏你都能稳住阵脚。这篇先聊到这后面我会继续分享更多实际项目里的数据清洗案例和进阶技巧感兴趣的朋友可以持续关注。