前阵子帮一个做AI模型的朋友排查训练集问题,发现他清洗数据用的还是最原始的逐行遍历——几百万行数据,光跑一遍就快半小时。我说你这不是在写AI,是在写算盘。Pandas这种为数据分析而生的工具,专门处理的就是这种脏活累活,却在AI项目的预处理环节被大量人用得极其憋屈。这篇东西我不打算讲教科书里面的API大全,而是结合我自己在实际AI数据处理中踩过的坑,把Pandas里真正高价值、高频使用的实战技巧拆开来说,覆盖从环境安装到缺失值处理、去重、类型转换、高性能文件格式的完整环节。尤其适合那些正在用Python做机器学习特征工程、或者打算把数据预处理从Excel手工作坊升级成自动化流水线的读者。
1. 环境安装不是玄学:pycharm里装pandas的正确姿势
1.1 为什么很多人在安装这一步就被劝退了
我不知道你是不是也遇到过这种情况:打开pycharm,新建了一个项目,写了两行import pandas as pd,然后Run,报错ModuleNotFoundError: No module named 'pandas'。这时候很多人下意识去点pycharm右下角的提示“Install pandas”,结果等了三分钟,转圈转了半天,最后弹出一个红色报错。
先搞清楚一个基本概念:pycharm本身不是Python解释器,它只是编辑器。你写的代码要运行,需要一个在系统里安装好的Python解释器来执行。pandas是第三方库,默认不随Python一起安装。所以“装不上”这件事,八成是环境选择或pip源的问题,不是你的操作有多笨。
1.2 两种安装路线的实操对比
装pandas我推荐两种路线,按照你的实际使用习惯选一种。
第一种,直接在pycharm终端里执行命令安装。打开pycharm下方的Terminal面板,先确认当前使用的解释器环境,然后执行:
bash复制pip install pandas
如果你的网络环境访问默认PyPI源很慢,可以切换国内镜像源,比如清华源:
bash复制pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple
第二种路线,用conda创建独立的虚拟环境。这种方式我通常推荐给做AI项目的人,因为AI项目往往要同时装pandas、numpy、scikit-learn、torch这一大套东西,如果全挤在系统Python里,依赖冲突能把你逼疯。用conda创建环境的好处是numpy、pandas这些核心科学计算库会一起装好,依赖版本是经过官方测试匹配的:
bash复制conda create -n ai_data python=3.10
conda activate ai_data
pip install pandas numpy openpyxl
这里有个细节值得展开:为什么我会提openpyxl?因为pandas读Excel文件,底层依赖openpyxl这个引擎来处理.xlsx格式。很多人在pycharm里已经装好了pandas,但pd.read_excel()一执行还是报错,报错信息里明明白白写着需要安装openpyxl或者xlrd。官方文档里read_excel的说明写得很清楚,但大多数人是不看文档直接上手,遇到报错才回过头来补装。
1.3 版本适配的经验之谈
热搜词里有一个问题我觉得问得特别真实:python3.10与哪个pandas版本适配?
这个问题背后是很多人装完发现import报错,而且报错信息五花八门,有的是AttributeError: module 'pandas' has no attribute 'DataFrame',有的是直接Segmentation fault崩溃。根据我的实测经验,Python 3.10配pandas 1.5.x是比较稳妥的选择,如果你想要更新的功能特性,pandas 2.0以上版本在Python 3.9-3.11下都能稳定运行。
但我建议你装完pandas之后,第一时间在终端里执行一下版本检查:
python复制import pandas as pd
print(pd.__version__)
这一步费不了一分钟,却能把90%的环境兼容问题提前暴露出来。我遇到过太多人代码写了一大堆,最后发现是pandas版本太老导致某个API行为不一致。还有一点,如果你的电脑上同时装了多个Python版本,shell命令pip可能指向的不是pycharm里用的那个解释器。解决办法是执行pip --version查看pip对应的Python路径,或者干脆在pycharm的Terminal里执行,这样确保用的是当前项目解释器。
提示:pandas和numpy有版本绑定关系,升级pandas的时候尽量连numpy一起升级。只升pandas不升numpy,很容易出现编译层面二进制不兼容的问题,那种报错不是重装一次就能解决的,往往需要
pip uninstall numpy pandas后重新安装。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据读取与类型转换:预处理的起点
2.1 一读就错?编码、分隔符和引擎的选择
AI项目里数据来源五花八门,最常见的是CSV、Excel、JSON几种格式。CSV看着简单,实际最容易出问题。
我之前从某个业务系统导出一份带中文注释的CSV,用pd.read_csv('data.csv')读出来全是乱码。原因就是文件编码不是默认的UTF-8,而是GBK。解决办法是读取时显式指定编码:
python复制df = pd.read_csv('data.csv', encoding='gbk')
如果遇到编码问题,可以在读之前用文本编辑器打开文件看一眼底部的编码格式,或者用Python探测一下:
python复制with open('data.csv', 'rb') as f:
raw = f.read()
# 尝试用不同编码解码,看哪个不报错
print(raw.decode('utf-8'))
再说分隔符。有些系统导出的CSV不是逗号分隔,而是制表符(.tsv)、分号、甚至竖线。pd.read_csv()默认只认逗号,所以当你看到数据全部挤在一列里的时候,先检查分隔符:
python复制df = pd.read_csv('data.tsv', sep='\t')
df = pd.read_csv('data.csv', sep=';')
还有一个经常被忽略的是engine参数。pandas支持两种解析引擎:C引擎(默认)和Python引擎。C引擎快,但对某些正则表达式的分隔符处理不了;Python引擎慢,但更灵活。当你的CSV文件特别大、几GB级别的时候,C引擎的速度优势明显。而当你的分隔符是多个字符的正则表达式时,必须指定engine='python'。
2.2 astype重锤下的类型陷阱:从字符串到数值的暗坑
数据读取完成后,第一步永远是看类型。很多人直接df.info()不管,然后就开始各种计算,结果出来了自己都不知道那些NaN是哪儿来的。
我强烈建议养成一个习惯:拿到数据先跑一遍df.dtypes,看每列的数据类型是否符合预期。数值列应该是int64或float64的,结果可能是object(字符串),这通常意味着这一列里混入了非数字的内容。
类型转换最常用的方法是astype()。比如把字符串类型的年龄列转成整数:
python复制df['age'] = df['age'].astype(int)
这个操作看着简单,实际坑很多。如果age列里有一个值是空字符串,或者有一个值是"不详"这样的文本,astype(int)直接抛ValueError。更隐蔽的情况是:列里有缺失值NaN,转成int会报错,因为pandas的整数列不支持NaN(虽然pandas 0.24之后有了Int64这种可空整数类型,astype('Int64')可以处理,但那是有意为之的特殊类型)。
我在实际项目里遇到过一个很典型的案例:某个特征列看起来全是数字,但dtypes显示是object。排查后发现,Excel源文件里“性别”这个字段混入了少量文本,导致整列变成了字符串。所以类型转换前,最好先看一眼这个列里的唯一值:
python复制df['col'].unique()
如果是数值列混入了少量脏值,处理方法有几种:把非法的值替换成NaN,然后统一填充;或者用pd.to_numeric()配合errors='coerce'参数,它会把无法转换的值变成NaN而不是直接报错:
python复制df['col'] = pd.to_numeric(df['col'], errors='coerce')
pd.to_numeric比astype更推荐,尤其是在清洗阶段。它的逻辑是“能转就转,不能转就给你NaN”,这样我可以知道哪些位置的原始数据是脏的,再去决定填充还是删除。astype的逻辑是“转不了就报错,整个程序停下来”,在自动化流水线里动不动就中断,处理起来费劲得多。
关于Pandas 2.0里新增的str类型——pd.StringDtype——我多说一句。它在处理“字符串+缺失值”混合列的时候比传统的object类型更稳定,不会出现莫名其妙的类型推断问题,如果你用的是pandas 2.x,建议在读取时通过dtype参数指定字符串列:
python复制df = pd.read_csv('data.csv', dtype={'col': 'string'})
3. 缺失值处理:AI训练前最脏的一仗
3.1 缺失值从哪里来
AI模型的训练效果很大程度上取决于喂进去的数据干净与否,而缺失值就是最典型的脏数据代表。缺失值的来源各有不同:设备采集信号偶尔丢包、业务系统接口字段没值、人工录入Excel时漏填、数据合并时两张表外键匹配不上……不同类型来源的缺失值,处理方法并不一样。
先记住一个核心原则:不要盲目把所有NaN都删掉。缺失率很低的字段,删掉那一行影响不大;缺失率高的字段,你删掉的行数能占到30%,这对样本量的伤害就是致命的,尤其对深度模型来说,数据几乎是命根子。
拿到一份数据后,我通常这么统计缺失情况:
python复制df.isnull().sum().sort_values(ascending=False)
这个一行代码能让你快速了解每一列的缺失数量。再加上缺失率计算:
python复制df.isnull().mean().sort_values(ascending=False)
3.2 删除、填充还是插值:决策链路
处理缺失值有三条路线:删除、填充、插值。怎么选?我分享一下我的决策逻辑。
删除适合以下场景:缺失值占该列比例很低(比如低于1%)、且该列本身对模型不重要。对应代码:
python复制df = df.dropna(subset=['critical_col']) # 只删关键列有缺失的行
填充适合大多数场景。用均值、中位数、众数、常数填充,是机器学习里最常见的做法。选择哪个统计量,取决于数据分布和业务逻辑。均值更容易受异常值影响,中位数则更稳健。
python复制# 用中位数填充数值列
df['age'] = df['age'].fillna(df['age'].median())
# 用众数填充类别列
df['region'] = df['region'].fillna(df['region'].mode()[0])
# 用常数填充,适合处理“缺失本身有含义”的情况
df['income'] = df['income'].fillna(0)
插值适合时间序列数据。比如传感器信号、股价数据,相邻时间点的值存在连续性,用插值比用全局均值合理得多。Pandas提供了interpolate()方法:
python复制df['sensor_value'] = df['sensor_value'].interpolate(method='linear')
还有一个容易被忽略的处理方式:把缺失值本身作为特征。因为某些场景下“该字段缺失”这件事本身就是有信息量的,比如客户没有填写收入字段,可能意味着收入低于某个不愿披露的水平。可以用一个哑变量来标记是否缺失:
python复制df['income_missing'] = df['income'].isnull().astype(int)
3.3 一个经典案例:give me some credit数据集的缺失值处理
“give me some credit”是Kaggle上非常经典的信用评分比赛数据集,国内很多做风控模型的人都拿它练手。这个数据集的训练集有15万行,11个字段,其中好几个字段都存在明显缺失。
我当时处理这个数据集时,注意到MonthlyIncome这个字段缺失率将近20%,而NumberOfDependents缺失率在2%左右。这种差异决定了策略必须不同。
对于MonthlyIncome,我一开始想直接用中位数填充,但后来观察到这个字段严重右偏——大多数人月收入集中在低区间,少数人收入极高。如果用均值填充,会被极端值拉偏,用中位数更贴近典型用户画像;同时我额外加了缺失标记列,因为当时背景是判断信用违约,收入缺失这一行为本身可能关联风险。
对于NumberOfDependents这种缺失率低的字段,直接删掉对应行也不影响样本量,但我选择了用众数填充,因为家庭成员数离散且取值集中,众数代表性够好。
补全之后我还做了一个动作:检查填充前后的分布差异。具体做法:
python复制df['MonthlyIncome'].hist(bins=50)
df_after['MonthlyIncome'].hist(bins=50)
如果填充后的分布明显在某一个值上出现异常尖峰,说明填充值选得可能有问题,或者缺失面太大导致这个值权重过高,这时就要考虑更复杂的技术,比如基于其他特征做回归预测填充。
4. 重复数据的判定与过滤:别让冗余样本污染模型
4.1 一个容易被忽略的问题
重复数据对AI模型的影响有多大?很多人意识不到,几百行全重复的数据可能在梯度计算时被放大权重,让模型对某些样本产生过拟合。但更麻烦的是“部分重复”——两行数据只有个别字段不同,而另外几个关键字段完全相同,这种情况在真实业务数据里非常常见。
Pandas去重首选drop_duplicates()。最简单用法是删除所有列都完全相同的重复行:
python复制df = df.drop_duplicates()
但实际工作中更常见的是按指定列判断重复。比如在用户行为数据里,同一个用户在同一时刻的记录可能被重复采集,这时候只需要根据user_id和timestamp这两列判断去重。
4.2 热搜词里的那个具体需求:指定两列均相同取第一条
这次热搜词里有一条问得非常具体:“pandas如果指定两列的值均相同,则取第一条数据即可”。这其实就是subset参数加上keep参数的组合应用。
python复制df = df.drop_duplicates(subset=['col1', 'col2'], keep='first')
keep参数有三个取值:'first'保留第一次出现的行;'last'保留最后一次出现的行;False删除所有重复的行,一行都不留。绝大多数场景用'first'就够了。
这里我要多补充一个很多人没注意到的细节:drop_duplicates()在执行前,如果列里有NaN值,默认会把NaN也作为“同一个值”来处理,也就是说两行在指定列上都是NaN,会被视为重复。这个行为在某些场景下是合理的,但在另一些场景下可能是灾难。比如你根据user_id去重,但缺失的user_id都被NaN填充,然后所有缺失user_id的行只留了一条——这会导致大量数据莫名其妙消失。
遇到这种情况,建议先去重前先看一下指定列缺失值的占比,再决定是否需要先填充或排除。
4.3 去重后的索引问题
我在实际项目中踩过一个坑:drop_duplicates()之后,DataFrame的索引是保留原位置的,比如原来的行号是1、2、5、9,去重后索引还是1、2、5、9,中间断开。这在后续做切片、合并、分组时会出现两种问题:一是运行时没有报错,但看起来数据量对不上;二是df.loc[5]取到的行跟你以为的不同。
解决办法有两个:
python复制df = df.drop_duplicates().reset_index(drop=True) # 重置索引且不保留旧索引列
或者用ignore_index=True参数:
python复制df = df.drop_duplicates(ignore_index=True)
如果你需要保留原始数据的行号线索做留痕审计,用reset_index()不删旧索引;如果是纯清洗,直接drop=True就好。
5. 从CSV到Parquet:大数据量下的Pandas提速
5.1 为什么CSV在大数据量下让人抓狂
AI项目的数据量上来之后,CSV作为存储格式会越来越让人难受。我自己测过一份大概500万行、30列的数据,CSV格式占用空间约1.2GB,pandas读进来要十几秒,写出去甚至要更久。如果只是偶尔跑一次还能忍,但特征工程阶段往往要反复读写,每次几分钟的等待叠加起来,效率极低。
更关键的是类型信息丢失。CSV是纯文本,读出来之后所有类型都得靠pandas重新推断,字符串列、日期列、数值列,推断错误的概率不小。每次重新读取都要重新推断一遍,浪费时间也容易出错。
这就是Parquet和Feather这类列式存储格式的价值。
5.2 Parquet和Feather的实操对比
Parquet是Apache生态下的列式存储格式,压缩率高,支持分区,是Spark、Hive这些大数据组件的主流格式。Feather是R和Python之间共享数据的轻量格式,读写速度极快,但生态支持没有Parquet广。
它们在Pandas里的用法极其简单:
python复制# 保存
df.to_parquet('data.parquet', engine='pyarrow')
# 读取
df = pd.read_parquet('data.parquet')
# 保存feather
df.to_feather('data.feather')
# 读取
df = pd.read_feather('data.feather')
我在同一份500万行数据上做过对比:CSV读取约14秒,Parquet读取约1.8秒,Feather读取约1秒。写入差距更夸张,CSV写入22秒,Parquet写入5秒,Feather写入1.5秒。文件大小方面,CSV压缩后1.2GB,Parquet压缩后约600MB,Feather不压缩但速度快,约1GB。
这不是夸张,列式存储对AI数据预处理来说真的算是革命性提升。更棒的是,Parquet文件里会保存每一列的数据类型信息——你上次转换好的类型,下次读取直接就是对的,不需要再重新转换,省掉整段类型处理代码。
我现在的习惯是:中间处理结果一律存Parquet,最终交付给模型的训练集也存Parquet,CSV只作为跟外部系统交换数据的边界格式。
注意:使用
to_parquet和to_feather之前需要安装pyarrow或fastparquet库。装pyarrow最简单,直接pip install pyarrow。有些数据集字段特别复杂时,pyarrow和pandas之间的兼容性偶尔会有一些小坑,但常见的数据类型都没问题。
6. 实战串联:用Pandas完成一次完整的AI数据预处理
6.1 场景设定与数据背景
前面讲的都是散点技巧,这里我把它们串成一个完整流程。假设你在做一个信贷风控模型,拿到的原始数据是业务系统的CSV导出,包含用户ID、年龄、月收入、负债率、逾期次数、居住时长等字段,大概10万行。目标是产出一个干净的特征矩阵,供后续训练XGBoost或逻辑回归模型使用。
6.2 完整流水线
第一步,读取数据并做初步勘察:
python复制import pandas as pd
import numpy as np
df = pd.read_csv('raw_data.csv', encoding='utf-8')
print(df.shape)
print(df.dtypes)
print(df.isnull().sum())
第二步,处理明显异常值。比如年龄字段出现负数或者大于120的值,这显然是数据错误,不是正常样本。可以用布尔索引删除:
python复制df = df[(df['age'] >= 18) & (df['age'] <= 100)]
第三步,类型转换。把object类型的数值列转成数值类型,把字符串日期转成datetime类型:
python复制df['income'] = pd.to_numeric(df['income'], errors='coerce')
df['report_date'] = pd.to_datetime(df['report_date'], errors='coerce')
第四步,缺失值处理。按之前说的策略:关键数值列用中位数填充,类别列用众数填充,对缺失有业务含义的列加标记:
python复制df['income'] = df['income'].fillna(df['income'].median())
df['region'] = df['region'].fillna(df['region'].mode()[0])
df['income_missing'] = df['income'].isnull().astype(int)
注意顺序:如果先做income_missing标记,再填充income,就没法标记了。所以缺失标记要放在填充之前。
第五步,去重:
python复制df = df.drop_duplicates(subset=['user_id', 'report_date'], keep='first')
df = df.reset_index(drop=True)
第六步,特征衍生。这是特征工程的核心环节。比如从申请日期和出生日期计算年龄,从负债率和收入构造负债收入比:
python复制df['income'] = pd.to_numeric(df['income'], errors='coerce')
df['debt_to_income'] = df['debt'] / df['income']
6.3 预处理后的自检清单
我的习惯是,跑完流水线之后不急着训练模型,先做一遍自检:
df.shape确认行列数符合预期,清洗后减少了多少行心里有数。df.isnull().sum().sum()全表缺失值总和应该为0,或者极少。df.dtypes确认每个字段类型真正符合模型输入要求。df.describe()看数值列的最大最小值和分位数,发现异常值及时排查。
最后把结果存储为Parquet:
python复制df.to_parquet('cleaned_data.parquet', index=False)
这个完整流程跑下来,10万行数据在一两分钟内搞定,代码量也就三四十行,而且完全可复用。下一次来新数据,改一下文件路径就能跑。
7. 最后补充几个高频函数的实战细节
7.1 merge和concat的正确打开方式
做AI预处理经常要合并多个数据源。pd.concat用于纵向拼接,比如把几个月的数据按行垒起来;pd.merge用于横向关联,类似SQL里的JOIN。
python复制# 纵向拼接:两个结构相同的DataFrame
df_all = pd.concat([df_jan, df_feb], ignore_index=True)
# 横向关联:类似SQL left join
df_merged = pd.merge(df_main, df_info, on='user_id', how='left')
合并的时候最需要注意的是关联键是否有重复值。如果df_info里每个user_id只有一条,left join没问题;但如果关联键有重复,合并出来的行数会爆炸。我建议合并前先用duplicated()检查一下关联键:
python复制assert not df_info['user_id'].duplicated().any()
7.2 groupby在特征聚合中的妙用
特征工程里有一种高频需求:按用户分组计算统计量,比如每个用户近30天的行为次数、平均值、最大值。用groupby加agg组合实现:
python复制user_stats = df.groupby('user_id').agg(
total_actions=('action_id', 'count'),
avg_amount=('amount', 'mean'),
max_amount=('amount', 'max'),
last_active=('timestamp', 'max')
).reset_index()
这个操作可以在几秒内把几百万行行为数据压缩成每个用户一行的统计特征,直接作为模型输入。我几乎所有的特征工程里都离不开这个套路。
7.3 apply函数和向量化操作的取舍
apply很方便,但也是pandas里最容易写慢的操作。因为apply本质上是Python循环,逐行执行一个函数,在百万级数据上可能慢到无法接受。
举例说,你想根据年龄列生成年龄段标签:
python复制def age_group(age):
if age < 30:
return 'young'
elif age < 50:
return 'middle'
else:
return 'old'
df['age_group'] = df['age'].apply(age_group)
这种方式在几十万行数据上还能忍,到了千万行就够呛。更快的方案是pd.cut做分箱:
python复制df['age_group'] = pd.cut(df['age'], bins=[0, 30, 50, 200], labels=['young', 'middle', 'old'])
pd.cut和pd.qcut是向量化操作,底层是C实现,速度比apply快几个量级。能用向量化函数解决的,就别用apply。如果必须要用apply,也可以关注df.itertuples()这种方式写循环,比直接df.iterrows()快很多,但终归不如向量化。
在我实际处理过的AI大数据集项目里,性能瓶颈多数不是模型训练,而是特征工程阶段的数据转换。把数据从CSV换成Parquet,把逐行apply改成向量化操作,往往能让整个流程提速数倍到数十倍,这在调参迭代频繁的AI项目里,省下的时间非常可观。
我的经验是:pandas这个库本身不难学,难的是形成“先用df.head()观察、再批量转化、再检查分布”这种清洗思路。数据预处理在整个AI项目里占的时间经常超过一半,用好了pandas,等于把这部分时间压缩到原先的三分之一不到。而且这些技巧不挑项目——无论是结构化数据建模、推荐系统特征工程,还是大模型微调前的数据整理,核心逻辑都是通的。
