讲个真事,上周帮一个做电商运营的朋友处理订单数据,她发来一张八百多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', engine='openpyxl') # 概览数据 print("数据形状:", df.shape) print("列名:", df.columns.tolist()) print("前5行数据:") print(df.head()) print("数据类型信息:") print(df.info()) # 保存清洗结果 df.to_csv('clean_data.csv', index=False, encoding='utf-8-sig')encoding='utf-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=['订单号'], keep='last')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 + errors='coerce' 将非法值转为NaN df['单价'] = pd.to_numeric(df['单价'], errors='coerce') df['数量'] = pd.to_numeric(df['数量'], errors='coerce') # 2. 日期列转换 df['订单日期'] = pd.to_datetime(df['订单日期'], errors='coerce', format='%Y-%m-%d')errors='coerce'的意思是:遇到无法解析的值不要报错,而是返回NaN。这样我们能保证后续代码继续执行,而所有转换失败的值会被标记为缺失值,稍后统一处理。如果不加这个参数,一行脏数据能让整段代码崩溃。
日期转换还有一个参数要注意——format。如果日期字符串的格式是标准的2024-01-15,Pandas的to_datetime可以不传format自动推断,但速度慢;如果是2024/01/15、20240115这类不标准格式,建议指定format,解析更快更准。
还有一类非常隐蔽的类型问题:数值列里混入了千分位逗号。比如"1,234",Pandas会把它当作字符串处理,用to_numeric转换的时候会得到NaN。这种要先去掉逗号再做类型转换:
df['金额'] = df['金额'].astype(str).str.replace(',', '') df['金额'] = pd.to_numeric(df['金额'], errors='coerce')3.5 第五步:处理文本列中的空格和格式不统一问题
文本列的脏主要体现在几个方面:首尾空格、中间多余空格、大小写不统一、全角半角混用。这些看起来不影响数据含义,但一旦用groupby去汇总,相同含义的数据会被拆成多个组,统计结果直接失真。
# 去除首尾空格 df['客户名称'] = df['客户名称'].str.strip() df['备注'] = df['备注'].str.strip() # 去除中间多余空格:把连续多个空格替换成一个 df['客户名称'] = df['客户名称'].str.replace(r'\s+', ' ', regex=True) # 全角转半角(处理全角数字、英文字母) 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()这里需要补充一个细节:为什么全角转半角这么重要?因为实际业务中,用户可能用中文输入法在数字输入框里输了数字,存储时保存为全角的"123",后面任何数值操作都会失败。全角转半角的函数在清洗身份证号、手机号、金额时经常用到,建议封装成工具函数反复调用。
文本格式统一还包括:省份名称的简称和全称混用("广东"和"广东省")、日期时间格式的混用("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(method='ffill') # 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(drop=True) # 4. 最终体检:确认没有残留问题 assert df['订单号'].is_unique, "订单号存在重复!" assert df['订单金额'].notnull().all(), "订单金额存在缺失!" assert (df['订单金额'] >= 0).all(), "订单金额存在负值!"assert断言是很好的数据质量把关手段。写完清洗脚本后,把所有"必须满足的条件"写成一串断言,跑完能通过就说明数据质量合格了,不通过就返回检查。这个习惯能帮你守住数据质量的底线。
另外注意:reset_index(drop=True)这步很多新手会漏。如果不重置索引,删除行之后的索引是断裂的,后面如果再按索引筛选或合并数据会出问题。
3.10 第十步:导出清洗结果与沉淀清洗规则
最后一步是输出,但输出不只是写一个文件那么简单。实际项目里你需要考虑输出格式、编码、以及清洗规则的沉淀。
# 导出清洗后的数据(去除索引列,避免Excel打开多一列) df.to_excel('clean_sales_data.xlsx', index=False, engine='openpyxl') # 导出为CSV格式(utf-8-sig避免Excel打开乱码) df.to_csv('clean_sales_data.csv', index=False, encoding='utf-8-sig') # 单独导出异常值记录,供业务方确认 abnormal = df[outlier_mask] abnormal.to_excel('异常值待确认.xlsx', index=False) # 打印最终的清洗报告 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['单价'], errors='coerce') df['数量'] = pd.to_numeric(df['数量'], errors='coerce') df['订单金额'] = pd.to_numeric(df['订单金额'], errors='coerce') # 日期解析:尝试多种格式 df['订单日期'] = pd.to_datetime(df['订单日期'], errors='coerce') # ======================================== # 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(drop=True) # ======================================== # Step 10: 导出 # ======================================== df.to_excel('clean_sales_orders.xlsx', index=False, engine='openpyxl') print("\n========== 清洗完成 ==========") print(df) print("清洗后数据量:", len(df))你注意看我在这个示例里隐含的几条经验:
- 日期里有
'2024.01.08'这种非标准格式,甚至还有一个'zzz'垃圾值,pd.to_datetime(errors='coerce')一次搞定,解析不了的变成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_state=42) 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', encoding='utf-8-sig') # 如果还是乱码,尝试 GBK / GB2312 df = pd.read_csv('data.csv', encoding='gbk')判断编码的方法:先用open读文件头部字节尝试不同编码,看哪种不乱码。实际项目里Excel导出的CSV大概率是GBK编码,网页下载的大概率是UTF-8。
5.3 数据量太大,Pandas直接内存溢出了怎么办
遇到大文件(超过几个GB)时,Pandas会吃力。有两个方向:
一是分块读取,处理完再合并:
chunk_iter = pd.read_csv('big_data.csv', chunksize=100000) clean_chunks = [] for chunk in chunk_iter: # 对每块做清洗 chunk = chunk.drop_duplicates() chunk['单价'] = pd.to_numeric(chunk['单价'], errors='coerce') clean_chunks.append(chunk) df_result = pd.concat(clean_chunks, ignore_index=True)二是使用更高效的数据类型。Pandas读数据时默认用int64、float64,如果你本身知道某列的数据范围,可以手动指定低精度的类型,能显著减少内存占用:
df = pd.read_csv('data.csv', dtype={ '数量': 'int32', '单价': 'float32', }, usecols=['订单号', '数量', '单价', '省份'])usecols参数只读需要的列,也是减少内存占用的有效手段,读取时你只选择用到的列,而不是把所有列都拉进来。
5.4 日期列处理中最容易被忽略的坑
日期处理的坑,我见过太多了。这里说三个:
一是日期字符串的月份和日期写反了。比如01/02/2024,Pandas默认按month/day/year解析,如果你的数据是day/month/year,解析结果会错位。遇到这种情况要显式指定format:pd.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, errors='coerce') |
| 日期排序错乱 | 日期是字符串格式 | pd.to_datetime() |
| 分组统计结果异常偏少 | 文本列存在空格或大小写差异 | str.strip()+str.upper() |
| 读取CSV中文乱码 | 编码不匹配 | 指定encoding='utf-8-sig'或'gbk' |
| 保存Excel后多出一列 | 默认保存了索引列 | index=False |
| 行数莫名减少 | 未注意dropna()默认丢弃所有含空值行 | 检查subset参数后使用 |
| 修改一个df另一个也变了 | 浅拷贝导致共享引用 | 用.copy()创建深拷贝 |
| 数据合并后索引错乱 | 未重置索引 | reset_index(drop=True) |
6. 经验的沉淀:从清洗脚本到可维护的清洗管线
前面讲完了十步流程和常见坑,最后我想聊一个更重要的问题:清洗脚本怎么从"一次性脚本"变成"可维护的清洗管线"。
我在真实项目里发现,数据清洗脚本最怕的是逻辑堆成一坨。今天删个列,明天加个映射,后天修个bug,三个月后没人敢动了。解决方法是按函数拆分,让每一步都是一个独立且可测试的单元。
def load_raw_data(path): return pd.read_excel(path, engine='openpyxl') 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', index=False) print("清洗完成") if __name__ == '__main__': main()这种写法有很直接的好处:每个函数只做一件事,测试时针对单个函数验证逻辑;数据源变了,只要改读取函数;清洗规则变了,只改对应函数。整个流程相当于一条流水线,后期维护和多人协做都很方便。
另外,我习惯把每一步的清洗记录打日志,比如处理了多少行重复、填了多少个缺失值,这样在排查数据问题时能追溯每一步的具体操作。
最终说一句实在话:数据清洗没有一劳永逸的银弹,因为每一份数据都有自己的脾气,但十步流程是经过大规模实战验证过的通用框架。按这个流程走,再脏的数据也能一步步变成可以放心分析的样子。我自己的体会是,前几次做清洗会觉得繁琐,但当你能一次把脚本跑通、输出的数据完全合规时,那种成就感不亚于完成一个复杂模型。建议你先拿自己手头的数据练手,跑通之后再把心得回头对照这篇文章,你会发现自己理解得远比看一遍要深。