做数据分析这几年,Pandas是我电脑里打开频率最高的库,没有之一。凡是从Excel、数据库、接口里扒拉出来的结构化数据,到了它手里基本都能干干净净地整理好,再喂给后续的可视化、机器学习流程。网上关于Pandas的教程不少,但多半是零散的API翻译,或者只讲某个孤立技巧,很多新手对着DataFrame一脸懵:这玩意到底怎么建、怎么改、怎么跟真实业务对上?这篇就把我从数据整理、数据清洗到进阶聚合的完整使用路径梳理一遍,每一步都配上能直接跑的示例代码,希望能帮你把这块硬骨头啃下来。
1. 为什么数据科学场景里绕不开Pandas
我最早接触数据处理时用的是纯Python写循环,等数据量一旦到几万行,那种龟速会让你怀疑人生。换成Pandas之后,同样的筛选、分组、求和,几乎就是一行代码的事,背后的向量化计算把性能瓶颈直接抹平了。这也是Pandas在Python数据分析生态里地位这么稳固的根本原因:它把Excel的操作逻辑搬到了代码里,又比SQL更灵活,尤其在探索性分析和数据预处理阶段,几乎没有哪个库能像它这样顺手。
Pandas的两个核心数据结构是Series和DataFrame。Series可以理解成一列带索引的数据,像一张表的某一行或某一列;DataFrame则是整个二维表,有行有列,每个列可以是不同的类型。很多初学者一开始纠结要不要先学Series再学DataFrame,我的建议是直接上手DataFrame,等遇到取某列、取某行的场景,自然就搞清楚Series是什么了。你可以在Jupyter Notebook里随手敲几行创建练习,理解一下传字典、传列表、传二维数组这些常见构造方式,底子打牢之后,后面的筛选和变换才能跟得上。
安装这一步拦住了不少新人。不同环境下的安装方式稍有差别,比如在PyCharm里可以直接去Settings找Project Interpreter,点加号搜索pandas安装,也可以在终端用pip install pandas或者conda install pandas。Jupyter里则更简单,一般只需在代码单元格写!pip install pandas,运行一次就能装好。安装慢或者超时的话,记得换成国内镜像源,比如清华的镜像,速度会明显提升。装完之后一定要验证一下导入是否正常,输入import pandas as pd不报错,就说明环境已经就绪了。
1.1 环境准备与安装排查
很多报错其实不是代码的问题,而是Pandas根本没装进当前使用的Python解释器里。比如在PyCharm里新建了项目但没用虚拟环境,或者Jupyter的kernel对应的Python版本和pip安装时用的是同一个解释器,这些不一致会导致import pandas时报ModuleNotFoundError。我的习惯是装完环境后马上运行print(pd.version),把版本号打出来看一眼,至少能确定Pandas确实在这个解释器里可用。
另外有个很容易被忽略的点:Pandas对Python版本有要求,新版本Pandas通常需要Python 3.9及以上。如果你的Python太老,pip会尝试去找旧版Pandas,却可能因为编译问题失败。这时候我建议要么升级Python版本,要么安装与Python版本匹配的Pandas历史版本,不要卡在报错信息里死磕。还有,Windows环境下偶尔会出现缺少VC运行时依赖的诡异问题,直接去官网装对应的Microsoft Visual C++ Redistributable就能解决,这类问题通常和代码逻辑无关。
1.2 Series和DataFrame的创建练习
Pandas里创建Series最常用的方式是传一个字典,键会成为索引,值成为数据;传列表则自动生成从0开始的整数索引。DataFrame的创建方式更灵活,字典、列表、NumPy二维数组、甚至另一个DataFrame都能变成DataFrame。下面这个简单练习我经常推荐给入门的人,它能一次性搞明白索引和数据的关系:
import pandas as pd import numpy as np s = pd.Series({"语文": 90, "数学": 92, "英语": 85}) print(s) df = pd.DataFrame({ "姓名": ["张伟", "李娜", "王芳"], "城市": ["北京", "上海", "广州"], "年龄": [25, 30, 28] }) print(df)创建之后可以试试df.shape、df.columns、df.index、df.info()这些查看属性,慢慢就会建立对数据结构的直观感觉。很多网上的练习题也会围绕Series和DataFrame的创建展开,比如头歌(Educoder)平台的数据预处理Pandas练习,基本上都是从完成这两个结构的花式创建开始的。
2. 数据读取与写出
实际业务里的数据几乎不会手动敲进去,都是从CSV、Excel、JSON、数据库或接口里读进来的。Pandas在这块做得非常成熟,read_csv、read_excel、read_json接口设计得高度统一,参数也很丰富,只要掌握了几个常用参数,基本能应对绝大多数日常导入场景。
读取CSV时我最低限度会设置的有三个参数:文件路径、编码方式、分隔符。最常见的中文乱码问题,大部分通过encoding="utf-8"或engine="python"就能解决;某些特殊格式的分隔符并不是逗号,而是制表符或分号,这时候直接指定sep参数就行。Excel文件的读取则要看你读到的是xlsx还是xls格式,xlsx依赖openpyxl,xls依赖xlrd,两者版本不一致时容易出现import错误,必要时需要pip install openpyxl或在环境中同时装好这两个库。
2.1 结构化数据读取的详细参数
先看一个比较完整的CSV读取示例,我把常见参数都标上注释:
import pandas as pd df = pd.read_csv( "sales_data.csv", sep=",", encoding="utf-8", parse_dates=["下单时间"], dtype={"订单号": str, "金额": float}, usecols=["订单号", "客户名", "下单时间", "金额"] )parse_dates能让指定列在读取时直接转成时间类型,比读进来之后再做to_datetime省一步。dtype参数用于指定列的数据类型,比如订单号为纯数字,但实际是字符串性质的编号,如果不在读取阶段指定str,后面就会出现类似101和0101这种前导零丢失的问题。usecols则能只保留需要的列,对大文件来说既省内存又减少后面无意义的列操作。
读取Excel也是类似思路,区别在于工作表的指定。如果你不指定sheet_name,会默认读到第一个工作表;如果工作簿里有多个sheet,可以传sheet_name="销售明细"或者sheet_name=0来指定。读取多张表还可以用sheet_name=None,返回的是一个以工作表名为键、以DataFrame为值的字典。
2.2 数据写出与文件管理
数据处理完总要落盘保存。to_csv和to_excel使用逻辑跟读取是对称的,写CSV时index=False几乎是必须的,否则数据会多出一列行号,后面再读进来就会多一列Unnamed: 0的冗余列。写Excel时多个DataFrame要写到同一个工作簿里,可以用pd.ExcelWriter配合to_excel的sheet_name参数,这个需求在做日报、周报时特别常见。
df.to_csv("clean_sales.csv", index=False, encoding="utf-8-sig")这里编码我特意用utf-8-sig而不是utf-8,是因为utf-8-sig输出的文件用Excel打开时不会出现中文乱码。这个细节不干这行的人很难注意到,但做数据交付时是关键。写多个sheet的示例也一并给出:
with pd.ExcelWriter("report.xlsx", engine="openpyxl") as writer: df1.to_excel(writer, sheet_name="销售汇总", index=False) df2.to_excel(writer, sheet_name="订单明细", index=False) df3.to_excel(writer, sheet_name="退款记录", index=False)3. 数据清洗实战
说句实在话,真实项目里数据清洗占据的时间常常比建模本身还要多。脏数据不外乎几个特征:缺失值、重复值、类型不对、异常值。Pandas给每种脏数据都提供了对应的处理手段,关键是你要知道在什么场景下选哪种方案,而不是所有缺失值都无脑fillna(0)。
数据预处理在像头歌这样的练习平台上是单独一个模块,但在真实工作中它从来不是孤立的步骤,而是决定后续分析结果可靠性的地基。清洗得好不好,最直接的后果就是统计口径对不对,所以这块我非常建议多花时间练习,不要急于往建模阶段冲。
3.1 缺失值处理
拿到一个DataFrame后的第一件事,一般是检查缺失值分布。isnull()配合sum()可以快速统计每列的缺失数量,info()也会在输出里显示每列的非空计数,二者搭配着用,对数据的健康程度心里就有数了。
删除缺失值用的是dropna(),这里的参数有点讲究:dropna()默认删除任何含有缺失值的行,如果你只想删除那些全部列都为缺失值的行,要用how="all";如果只想看某一列或某几列是否有缺失,可以用subset指定列名列表。但删除不是万能的,数据本身很贵时,填充往往更合适:
df["金额"].fillna(df["金额"].median(), inplace=True) df["备注"].fillna("无", inplace=True) df["时间"].ffill(inplace=True)能注意到的规律是:数值型列适合用均值或中位数填充,类别型列适合用众数或固定值填充,时间序列数据则常用ffill或bfill,也就是用上一个或下一个有效值来填充。再加上interpolate()方法可以按线性插值填充,许多看似复杂的缺失场景,其实用这几招就能覆盖。
3.2 数据类型转换与异常值处理
数据类型转换是另一个高频操作。astype()是通用转型方法,但要小心把浮点列直接转int时如果是空值会报错,把“20230101”这种字符串转成时间需要pd.to_datetime,把object类型且内容是数字的列转成数值型需要pd.to_numeric,并且可以配合errors="coerce"把非法值变成NaN。这三个方法基本能处理日常90%的类型转换需求。
df["年龄"] = df["年龄"].astype(int) df["下单时间"] = pd.to_datetime(df["下单时间"]) df["销售额"] = pd.to_numeric(df["销售额"], errors="coerce")异常值处理往往依赖业务规则,比如销售金额不能为负、年龄不能在0到150之外,筛选出来之后可以用布尔索引结合clip()或loc直接修正,也可以做截断。更统计学的做法是看四分位数和IQR,把超出上下限的值标记出来再处理。
处理重复值时,duplicated()和drop_duplicates()是一对好用的组合。前者返回布尔序列方便查看,后者直接去重。注意去重时可以指定subset参数,比如只根据手机号去重,还可以用keep参数控制保留第一行还是最后一行。
4. 筛选、分组与聚合
筛选和分组是Pandas里使用频率最高的两块功能。筛选让人又爱又恨,因为写法实在太多:df[df["年龄"] > 30]、df.loc[条件, 列名]、df.query("年龄 > 30"),每种都能做类似的事,但在性能和可读性上各有取舍。我的一般原则是:简单条件用布尔索引,复杂条件或需要选列时用loc,要求书写简便时用query。
分组聚合是让Pandas真正显出威力的地方。groupby之后可以跟agg、transform、filter、apply等方法,配合reset_index把分组键重新变成普通列,整个数据整理链路就非常顺畅了。如果你做数据预处理时头歌上的练习都能顺手完成,那么到这里你离实战已经不远了,剩下的就是多拿真实数据练手。
4.1 筛选与切片
先看一个组合筛选的示例:
# 筛选年龄大于30且城市为"上海"的记录 condition = (df["年龄"] > 30) & (df["城市"] == "上海") sub_df = df.loc[condition, ["姓名", "城市", "年龄"]]这里有两个新手容易踩的坑:一是多条件必须用&、|、~,而不能用and、or、not;二是条件表达式外层一定要加括号,因为Python运算符优先级的问题,不加括号会报错。筛选之后的子集尽量用副本,避免后续修改影响原DataFrame。
loc是基于标签的索引,iloc是基于整数位置的索引,两者都支持切片写法。df.loc[2:5]和df.iloc[2:5]在索引是连续整数时看起来差不多,但一旦索引不是0到N-1的顺序,这两个切片结果就完全不同,所以掌握它们之间的差别是基本功。
4.2 groupby分组聚合实战
分组聚合的真实价值在于用一句话完成原本要写很多行for循环的数据汇总。groupby对象本身是懒计算的,只有调用聚合方法时才会真正执行。最常用的几个聚合函数是sum()、mean()、count()、size()、max()、min()、nunique()。如果需要同时看多个统计量,直接groupby后调用agg并传一个字典就能搞定:
grouped = df.groupby("城市").agg({ "销售额": ["sum", "mean", "count"], "年龄": ["min", "max"] })得到的结果会带多层列索引,如果嫌麻烦,可以把列名用列表传进去,再看一眼结果结构。分组后要重新恢复成扁平表,记得调用reset_index()。另外一个常见需求是按天、周、月做聚合,这就要把时间列设成索引,然后用resample(),这比groupby更贴时间序列场景。
多个DataFrame需要合并时,concat是纵向或横向拼接的首选,merge则适合像SQL一样按键关联。concat在处理相同结构的多张表时非常方便,merge则解决一对一、一对多、多对多这类复杂关联。合并之后要留意索引重置,避免出现重复或丢失索引。
5. 进阶技巧与性能优化
Pandas入门容易,进阶难。很多人能把日常统计做完,但一遇到大数据量或复杂变换就开始发怵。这一节我从实际踩坑经验里提炼几个话题:apply与向量化的选择、时间序列的进阶处理、以及如何用Pandas对接接口数据甚至多线程采集。
真实项目里,性能优化往往不是从更复杂的语法技巧开始的,而是先看你的代码是否在循环里反复处理DataFrame。如果你发现自己写了for循环,在循环里一行行地改DataFrame,那大概率性能会非常难看。我的经验是:能用向量化运算就别用apply,能用apply就别用循环。
5.1 apply函数与向量化
apply的定位是当你无法用原生向量化运算表达逻辑时,用自定义函数逐行处理。比如计算一个订单的折扣后金额,可以先定义一个计算逻辑的函数,再通过apply应用到每一行:
def calc_discount(row): if row["金额"] >= 1000: return row["金额"] * 0.8 elif row["金额"] >= 500: return row["金额"] * 0.9 return row["金额"] df["折后金额"] = df.apply(calc_discount, axis=1)axis=1表示逐行处理,这是初学apply时最容易弄错的参数。但apply在数据量大时性能不算好,它本质上是Python层面的循环。如果业务逻辑能改写成NumPy的内置函数或Pandas的向量化方法,尽量优先用后者。比如上面的计算,其实用np.where或Pandas布尔索引就可以更高效地实现。掌握apply能让你灵活处理复杂逻辑,同时也要有意识地把简单的if-else逻辑拆成向量化表达式。
5.2 时间序列处理
时间序列在Pandas里是个大话题。把字符串列转成datetime之后,就可以用dt访问器提取年月日、星期、季度等属性。按小时或按天重采样时,resample非常方便:
df["时间"] = pd.to_datetime(df["时间"]) df.set_index("时间", inplace=True) daily_sales = df["金额"].resample("D").sum() monthly_sales = df["金额"].resample("M").count()resample必须作用在时间索引上,所以set_index那步不能省。重采样之后得到的结果,时间索引可能不是连续的日期,可以用reindex或asfreq补全缺失日期。再往上进阶,移动窗口计算rolling()、时间差计算diff()和shift()也比较常用,比如计算销售额环比、同比时就能派上用场。
5.3 多线程与真实接口数据案例
实际工作里经常需要从接口拉取数据,再把返回的JSON或列表整理成DataFrame。这里有个从热搜词里看到的典型场景,我给它补全成一个可运行的多线程采集示例,很适合做练习。假设你有一批股票代码,需要分别请求财务数据,返回的数据每条大概是一个字典或嵌套结构,可以用如下方式并发采集:
import pandas as pd import requests import random import time from concurrent.futures import ThreadPoolExecutor, as_completed def fetch_financial_data(stock_code): url = f"http://example.com/api/financial/{stock_code}" try: resp = requests.get(url, timeout=10) resp.raise_for_status() data = resp.json() return {"stock_code": stock_code, "report_type": data.get("report_type"), "net_profit": data.get("net_profit")} except Exception as e: return {"stock_code": stock_code, "error": str(e)} stock_codes = ["000001", "600519", "000002", "601318"] results = [] with ThreadPoolExecutor(max_workers=4) as executor: future_map = {executor.submit(fetch_financial_data, code): code for code in stock_codes} for future in as_completed(future_map): result = future.result() results.append(result) print("已完成: ", result["stock_code"]) df_result = pd.DataFrame(results)这个例子里几个要点值得多说几句:requests访问接口时要设置超时,避免某个请求卡住导致整个线程池等待;返回结果统一先存成列表再一次性转DataFrame,比在循环里反复pd.concat高效得多;异常要单独捕获,否则一个接口报错可能拖垮整个采集任务。
6. 常见问题与排查技巧实录
写代码多了,你早晚会遇到一些诡谲的报错。Pandas的报错有时候很长,但信息量很大,关键是要耐下心看最上面和最下面的部分。这里记录几个我经常遇到、也被问过很多次的问题,按出现频率排序,每个都给出定位思路和解决方案。
先讲最让人摸不着头脑的一个:AttributeError: module 'pandas' has no attribute 'core'。这个报错网上一搜一大把,原因五花八门,最常见的是你的文件名或脚本名叫pandas.py,导致import pandas时实际导入的是自己写的那个文件,自然找不到core属性。解决方法很简单:重命名你自己的脚本,不要用pandas.py这种跟库重名的文件名。
6.1 安装与导入阶段问题
安装Pandas时最容易遇到的是镜像源慢、超时、权限不够这三种。慢和超时可以用-i参数指定国内镜像解决,权限不够在Windows下使用管理员终端重装,在Linux或macOS下可以考虑使用虚拟环境绕开系统的site-packages保护机制。还有pip版本太旧可能导致依赖解析异常,先升级一下pip是个好习惯。
PyCharm里安装后仍然无法import,多半是解释器没有选对。打开Settings里的Project Interpreter确认一下当前项目的解释器路径,如果刚才装Pandas用的pip属于系统Python,但项目选择的是虚拟环境,那就肯定找不到。把解释器切换到同一个环境,问题自然消失。Jupyter里有时候import pandas会报错,而命令行里却不报错,这通常是ipykernel与当前Python环境不一致,可以在Notebook里执行import sys; sys.executable看看当前用的是哪个Python,再和命令行对比。
6.2 数据处理阶段问题
读取Excel报错一般是缺少依赖库。xlsx格式需要openpyxl,xls格式需要xlrd,老版本的Pandas可能还要求xlrd为主依赖,所以如果你读xlsx文件报错,直接pip install openpyxl就能解决。读取CSV中文乱码时,把encoding从utf-8改成gbk或utf-8-sig逐个测试,注意保存文件时如果是要交付给同事,以Excel打开不乱码为准。
数据导入后没有报错,但类型全部是object,这往往是CSV文件里混入了空字符串或特殊符号。可以先看看df["列名"].unique()里的值,再决定是替换成NaN还是直接剔除。处理前建议先复制一份DataFrame,用df.copy()做操作,避免原数据被污染后无法追溯。类型转换时如果遇到Cannot interpret 'X' as a data type这种报错,一般是astype的参数不合法,检查一下有没有把字符串拼错。
分组聚合时出现KeyError,通常是分组键的列名不存在,或者列名里有不可见字符。打印df.columns就能发现端倪,可能有的列名带空格,有的列名大小写不匹配。用简单的重命名步骤df.columns = [c.strip() for c in df.columns]能把类似问题一次性解决。merge时报错on参数找不到列,也基本是列名不一致,先把公共键的列名统一再合并,能省去很多无谓的排查时间。
隐式链式赋值警告也是Pandas里的经典坑。大概场景是你先筛选出一个子集,再给这个子集赋值,结果发现原DataFrame没变或代码本身有警告。原因是Pandas无法确定你赋值的目标是视图还是副本。最好的习惯是:只要想修改数据,就明确用loc在一个完整的赋值表达式里完成,或者在筛选后先来一个.copy(),让Pandas知道你要操作的是一个独立的副本。
6.3 进阶避坑与性能调试
数据量大时,Pandas运行慢是正常现象。定位慢在哪一步,可以用%timeit或ipython的time魔法命令做小范围验证。如果发现是apply太慢,尝试把函数改成向量化写法;如果是merge太慢,看看合并键有没有先排序或去重,偶尔提前用sort_values给键排序能让merge快不少。
内存优化方面,一个实用技巧是把不必要的float64降到float32,把object列转成category类型,尤其当某列重复值很多时,category类型能大幅降低内存占用。读取大文件时还可以尝试分块读取,pd.read_csv的chunksize参数能让你分批处理完再合并结果。真正超大的数据该用Dask或Polars之类,但那是另一个话题了,至少要能分清Pandas的适用边界,避免在错误场景里硬扛。
还有个细节是Pandas版本升级带来的API变更。旧代码在旧版本Pandas上跑得好好的,换到新版就开始报FutureWarning甚至DeprecationWarning,这时候先看警告信息里提示的替代写法,再对照Pandas官方文档迁移。比如早期的一些inplace参数和append方法,在新版本里都被弱化或不建议使用了,处理方式通常是把链式调用改成赋值式调用,熟悉这种迁移思路才能跟上不断更新的生态。
如果看到pd.Series.append之类的方法被移除,不要慌。这属于Pandas 2.0以后逐步清理旧API的结果,思路是改用pd.concat([s1, s2])。新版越来越强调显式、可预测的操作,这种变化方向其实更利于写健壮代码。我在好几个老项目里都遇到过这种兼容性问题,处理方式万变不离其宗:看警告,查官方,改调用。