学习 pandas 大概是我入门数据分析时最值的一笔投入。最开始我以为它只是比 Excel 更“能用代码操作”的表格工具后来才真正体会到它把整理数据、清洗数据、分析数据这条链路彻底打通了。这篇笔记不是把官方文档复述一遍而是我从头学过 pandas 之后把真正频繁使用的操作、容易踩的坑、以及这些操作背后的设计逻辑按自己的理解重新梳理了一遍。适合刚学完 Python 基础、准备正式接触数据处理的朋友也适合已经用过一阵、想系统捋一遍知识点的同行。我会尽量把每个关键操作都讲清楚三件事它是干什么的、为什么这样写、实操中会翻哪些车。1. 学习前的准备环境、版本与学习路径1.1 为什么偏偏是 pandas很多初学者会问Python 里明明有列表、字典我为什么要再学一个 pandas这个问题的答案只有你真正处理过一份“像样的表格”才能体会。列表和字典适合存结构化程度很低的数据一旦涉及行和列、缺失值、统计汇总、按条件筛选用原生数据结构写出来的代码会非常啰嗦而且性能堪忧。pandas 的核心抽象是两种数据结构Series 和 DataFrame。DataFrame 可以理解成内存里的一张二维表每一列可以不同类型行有行索引列有列名操作起来和 Excel 很接近但又完全由代码驱动。它背后的存储和计算大量基于 numpy 数组很多操作在底层是 C 或 C 实现的所以哪怕你数据有几十万行常用的筛选、分组、聚合也不会慢到不可接受。再加上整个 Python 数据分析的生态都围绕 pandas 展开数据挖掘建模要用它做特征工程画图前要先整理成 tidy 结构做报表更离不开它。换句话说pandas 不是数据分析工具箱里的一个插件而是最底层的那个工作台。1.2 环境安装与版本确认学习前先把环境准备好。最简单的安装是直接用 pippip install pandas numpy openpyxlnumpy 是 pandas 的底层依赖基本都会自动装好openpyxl 是用来读写 .xlsx 文件的引擎如果你往后要频繁处理 Excel建议一并装上。装完验证一下版本import pandas as pd print(pd.__version__)我建议直接使用 pandas 2.x 的新版本。和旧版 1.x 相比新版在复制视图逻辑、字符串处理、时序功能上都有不少改进网上很多老教程是基于 0.x 或 1.x 写的部分写法现在会报警告甚至直接报错自己踩坑时先看一眼版本号。交互环境我推荐 Jupyter Notebook 或 VS Code 的交互式窗口。原因是数据分析天然是一个“摸索性”的过程你往往需要边看中间结果边调整下一步交互式环境可以让你逐个单元格执行观察每一步的输出而不是把整个脚本一口气跑完然后面对一屏报错。1.3 一条实用的学习路径我不建议你按官方文档从上往下背。pandas 官方文档非常全面API 太多一上来容易陷入“学了后面忘了前面”的困境。更有效的路径是拿一份真实业务表格围绕一条主线逐步展开——先读入数据然后做体检和清洗再做筛选和分组汇总最后尝试多表拼接。每个环节只用最核心的几个函数跑通再逐步扩展。我在学习时习惯用另一套笔记规则每个函数记录三件事——它能处理什么场景、关键参数怎么用、容易出错的地方是什么。这套笔记方式保存下来的才是代码库和文档里查不到的经验值。2. 把数据结构搞明白后面所有操作才有根基2.1 Series 其实是一列带标签的数据很多人一上来就死磕 DataFrame反而把 Series 当配角忽略掉。实际上 DataFrame 的每一列本质就是一个 Series不理解 Series 的索引机制后面到处都会莫名其妙。创建一个 Series 很简单s pd.Series([85, 92, 78], index[语文, 数学, 英语]) print(s)这里的 index 不只是摆设它给每个数值绑定了一个标签。这种设计直接带来一个特性两个 Series 做运算时pandas 会自动按索引对齐而不是机械地按位置相加。如果你有两个数据来源顺序不一样直接相加依然能得到正确的按标签匹配的结果。这在真实场景里省了太多事但也是新手最容易看晕的地方——明明数据顺序对不上结果怎么像是“自己对齐”了取值方面要分清两个方法s.loc[语文]是按标签取s.iloc[0]是按位置取。刚接触时经常混建议养成习惯只要心里想的是“某个名字/某个 ID”一律用 loc只要想的是“第几个”才用 iloc。2.2 DataFrame 是面的概念方向要拎清DataFrame 可以理解为由多个 Series 按列拼成的二维表格。创建方式非常多最直观的是传一个字典键是列名值是列表或 Seriesdf pd.DataFrame({ 姓名: [张三, 李四, 王五], 部门: [销售, 技术, 销售], 销售额: [1200, 900, 1500] })创建之后第一件事不是急着操作而是给数据做一次“体检”df.shape看行列数df.info()看每列类型和缺失情况df.describe()看数值列的分布df.head()看前几行。新手最容易在这个阶段忽略 axis 的概念。pandas 里axis0表示沿着行的方向操作也就是对每一列操作比如df.dropna(axis0)是删掉有缺失值的行axis1表示沿着列的方向操作比如df.drop(columnsxx, axis1)是删列。我经常看到有人把 axis 参数传反结果删错了方向这个方向感一定要在前面就养出来。2.3 索引和列名的双重标签体系pandas 的索引系统是它强大也是容易出错的地方。DataFrame 同时有行索引index和列名columns你在任何一步操作前都要先问自己这次筛选是基于行标签、位置还是列名最常见的翻车例子是写df[0]本意想取第一行结果它被解释成取名为 0 的列直接报 KeyError。取行和取列的正确方式完全不同df[列名]取一列返回 Seriesdf[[列名1, 列名2]]取多列返回 DataFramedf.loc[行标签]按行标签取行df.iloc[行位置]按行位置取行df.at[row, col]和df.iat[row, col]取单个值速度更快还有一个容易忽略的点行索引不一定唯一。如果索引重复loc 取出的是所有匹配的行数量可能超过预期。遇到这种问题先用df.index.is_unique检查一下再做后续处理。3. 数据读取与第一次“体检”3.1 从文件读入时的常用参数真实项目里数据几乎不会以“干净整齐”的姿态出现在你面前。读入这一步就要提前做好防御。以 CSV 为例最常用的参数我列在这里df pd.read_csv( orders.csv, encodingutf-8, sep,, header0, index_colNone, usecols[订单号, 金额, 日期, 地区], dtype{订单号: str}, parse_dates[日期], )逐个解释一下encoding用来处理中文乱码最常见的两种是 utf-8 和 gbk遇到乱码就换另一个usecols只读需要的列大文件时能少占不少内存dtype强制指定列的类型尤其是订单号、身份证这类看起来像数字但绝不能参与运算的列一上来就要强制读成字符串否则会被自动读成 int前导零直接没掉parse_dates提前把日期列转成 datetime 类型。读 Excel 文件则用pd.read_excel(文件.xlsx, sheet_name0)如果你的表头不是在第一行用skiprows整数跳过前面的无用行。这些参数不记熟后面清洗阶段要多花好几倍时间。3.2 一张表读进来后立刻做四项体检我养成的一个习惯是任何表读进来之后不急着写分析代码先做四件事。第一是看shape确认行数、列数是否符合预期。如果源文件说一万行读进来只有八千那大概率是读取时哪些行被吞了或者文件本身不干净。第二是看info()它会把每列的类型、非空数量列得清清楚楚。这里最需要警惕的是明明应该识别成日期的列显示 object明明应该是数值的列因为混入了文本被读成了 object这些都是后面分析结果跑偏的源头。第三是看isnull().sum()逐列统计缺失量。注意缺失量不是越少越好而是要结合业务判断“这里的空是否合理”。比如客户备注为空完全正常但订单金额为空就要警惕。第四是看nunique()统计每列唯一值的数量。这个方法特别适合检查分类列比如地区列如果出现几十个不同值说明录入不规范有“上海”、“上海市”、“上海 ”这类写法混在一起。3.3 初次清洗的三板斧体检之后通常要立刻做三件基础清洗。第一件是去重。如果一条记录有唯一业务主键直接df.drop_duplicates(subset[订单号])。这里要注意 subset 参数如果你想删掉的是“这几个关键字段都重复”的行一定要把字段名传给 subset否则默认是整行完全重复才算重复效果完全不同。第二件是统一空值。真实数据里空值有各种形态空字符串、字符串 NA、null、暂无这些都是文本。pandas 只认pd.NA或NaN表示缺失其余都不会按缺失处理。可以用df.replace({: pd.NA, NA: pd.NA, null: pd.NA})先把脏值统一成缺失值再去走缺失值处理流程更省事的方式是在 read_csv 时直接传na_values[, NA, null]。第三件是修正类型。检查 info 之后把明显类型不对的列用astype转回来比如金额列先去掉千分位逗号再转 float日期列用to_datetime转换。记住一个原则清洗阶段宁可多花几步也不要带着脏数据进入分析阶段。4. 数据选择与修改绕过那些诡异的坑4.1 取列的两个方式语义完全不同df[列名]和df[[列名1, 列名2]]的区别不仅是数量多少而是返回类型不同前者返回 Series后者返回 DataFrame。很多函数对两者的处理逻辑不一样用错时会报一些看起来莫名其妙的错。新增列也比想象中容易翻车。正确做法是直接给新列名赋值一个向量化表达式df[利润率] df[利润] / df[销售额]千万不要写df[新列] 一个只跟前面某一行有关的循环值然后发现整列都是同一个数——这种错误几乎每个新手都犯过根源是没搞懂 pandas 的赋值大致遵循“索引对齐”的广播规则。4.2 条件筛选布尔索引与 query条件筛选是使用频率最高的操作之一。核心思路是先构造一个布尔型 Series然后把这个布尔 Series 交给 DataFrame 去过滤mask df[销售额] 1000 df_high df[mask]多条件组合时要用与和|或并且每个条件必须用括号括起来。很多新手写df[df[销售额] 1000 and df[地区] 华东]直接报错原因就是 Python 的and不能用于这种逐元素判断。如果你的筛选条件比较复杂df.query()往往更好读df.query(销售额 1000 and 地区 华东)query 里面写字符串表达式不需要重复写df[列名]代码清爽很多。不过我建议至少要把布尔索引的原理搞明白再去用 query否则出了 bug 不好排查。4.3 修改数据时的两条铁律修改 DataFrame 里的一部分值最安全的固定写法是df.loc[mask, 等级] A这个写法先按布尔 mask 选中行再指定要修改的列一步到位。两条铁律分别记一下。第一条不要链式赋值。df[df[销售额] 1000][等级] A这种写法是先取子集再往子集里赋值pandas 自己都没法确定这个子集是原表的视图还是副本于是要么触发 SettingWithCopyWarning要么赋值根本没生效你后面输出一看还是空列。第二条需要副本的时候显式用df2 df[条件].copy()。如果你打算反复基于同一个筛选结果做修改却不去 copy改来改去最后可能连原始表也被牵连这种“幽灵修改”排查起来非常耗时间。关于inplaceTrue多说一句老教程特别喜欢用df.dropna(inplaceTrue)这种写法但新版本 pandaas 已经不太推荐了更强调“操作返回新对象、原对象不变”的风格。我自己的习惯是默认不用 inplace而是把结果重新赋值给变量代码更清晰也更容易追溯每一步到底改了什么。5. 清洗环节缺失、重复与异常值处理5.1 缺失值先问为什么再处理缺失值处理没有银弹第一步永远是思考“为什么会缺失”。订单金额缺失可能因为订单未完成用户年龄缺失可能因为注册时没填这两者的处理方式完全不同。前者可以根据业务含义填 0 或者删除后者填什么都会引入偏差。常见的缺失值处理方法df.dropna()直接删有缺失的行或列适合缺失比例极低的情况df.dropna(subset[关键列])只关注关键列是否有缺失其余列缺失不影响df.dropna(thresh5)保留非缺失值不少于 5 个的行适合“整行大面积空白”的情形df.fillna(value)用固定值填充df[列].fillna(methodffill)用上一个有效值填充非常适合时间序列比如库存量、累计值series.interpolate()线性插值适合连续型指标的中间空洞还有一点必须提醒NaN 会“传染”。一旦一列里出现 NaN很多运算结果也会变成 NaN。比如df[金额].sum()有缺失时返回 NaN而不是自动跳过。如果你希望忽略缺失值参与计算很多聚合函数里有skipnaTrue的默认参数但前提是你知道它在起作用。5.2 重复值用 subset 精准去除重复值比缺失值更隐蔽。整行重复一般一眼能看出来但真正危险的是“关键字段重复而其他字段不同”的情况。比如用户行为日志里同一用户、同一时间戳可能记了两条后一条是修正数据这时直接删整行重复没意义需要按关键字段去重。df.drop_duplicates(subset[用户ID, 操作时间])能做到精准去重。keep 参数控制保留哪一条keepfirst 保留第一条keeplast 保留最后一条keepFalse 全部删除。一个真实场景里的教训做用户表和订单表关联时发现结果行数莫名其妙比订单数多查了半天才发现是用户表里同一用户 ID 出现了两条历史记录。做任何带主键语义的关联之前先对主键做唯一性检查这种排查成本最低。5.3 异常值不是越多越好地清洗异常值处理最容易走极端有人一刀切把所有超过 3 倍标准差的值都删掉结果反而破坏了真实业务规律。我的做法是先观察再动手。df.describe()输出的 min、max、25%、75% 分位数能很快暴露不合理范围。比如年龄列出现 200订单金额出现负数这些大概率是问题数据。想限定某个合理范围可以用df[年龄].clip(0, 120)把越界值强制截断也可以用布尔索引把明显异常的行筛出来单独看。df[年龄].quantile([0.99])这种分位数操作也很有用能看出极值集中在哪个范围。替换脏值用df[性别].replace({1: 男, 2: 女})非常方便。有一个细节replace 默认是精准匹配如果原始数据里同时存在 男 和 男 带空格最好先用str.strip()去掉空格否则替换不完整后面 groupby 时会出现两个几乎一样的分类。5.4 文本与日期字段的标准化文本字段的标准化本质是把同一含义的不同写法归一到统一格式。最常用的是 string 的访问器df[姓名] df[姓名].str.strip() # 去首尾空格 df[地区] df[地区].str.replace(市, ) df[编码] df[编码].str.upper() # 统一大写日期字段的规范化则用pd.to_datetimedf[日期] pd.to_datetime(df[日期], errorscoerce)errorscoerce的意思是解析不了的变成 NaN而不是让整个程序报错退出这在清洗脏数据时几乎是必须的。转成 datetime 之后就可以用df[日期].dt.year、.dt.month、.dt.dayofweek提取各种时间成分也可以直接按月份做分组。日期列一旦保持字符串格式排序是字典序分组是按文本分所有时间分析都会静悄悄跑偏。6. 分析神器分组、聚合与透视6.1 groupby 的本质拆分、应用、合并groupby 是整个 pandas 里最该熟练掌握的操作它遵循“拆分-应用-合并”的思路。先把数据按某个字段的不同取值拆成多个组再对每个组做聚合计算最后把结果合并成一个新的表。df.groupby(地区)[销售额].sum()这段代码的意思是按地区拆分数据把每个地区的销售额相加返回一个以地区为索引的 Series。理解了这个流程你就能明白为什么 groupby 的结果不是万能的 DataFrame——分组后它先变成一个 GroupBy 对象你后面跟什么函数决定了对每组的处理方式。多字段分组也很常用df.groupby([地区, 渠道])[销售额].sum()返回的结果会带一个 MultiIndex看着有点复杂。如果你后续还想当普通 DataFrame 用可以加.reset_index()把分组字段变回普通列这几乎是标准操作。6.2 agg一次聚合多个指标groupby 之后只接一个 sum 或 mean 会浪费它一半的威力。用agg可以一次算多个统计量df.groupby(地区)[销售额].agg([sum, mean, count, std])如果要对不同列用不同统计函数可以传字典df.groupby(地区).agg({销售额: sum, 利润: mean})新版本 pandas 更推荐命名聚合结果列名更清晰df.groupby(地区).agg( 总销售额(销售额, sum), 平均利润(利润, mean), )还有一个容易被忽略的 transform。groupby加transform会返回和原来行数一致的结果而不是压缩后的表。比如你想给每一行补上“本地区销售额均值”用来计算每条记录占地区总量的比例transform 就是最合适的工具而且比先 agg 再 merge 快得多。6.3 透视表与长宽转换如果你用过 Excel 的透视表pandas 的pivot_table上手没有任何门槛pd.pivot_table(df, values销售额, index地区, columns月份, aggfuncsum)第一个很容易踩的坑是aggfunc默认是mean不是sum。很多人只想要求和却默默得到了一堆平均值。所以每次写 pivot_table 都养成显式传 aggfunc 的习惯。reshape重塑里还有个高频操作是melt把宽表转成长表。这在做数据可视化前特别常用因为大多绘图库都希望数据是一行一观测的长格式。Stack 和 unstack 则是索引层级的升降平时用得少但在处理分组结果或透视结果时能救命。7. 多表拼接concat 与 merge7.1 concat 是“叠放”merge 是“关联”很多新手搞不清什么时候用 concat什么时候用 merge。我的理解很直观concat 是把两张结构相似的表“摞起来”或“并排贴起来”不关心行与行之间是否有对应关系merge 则像 SQL 里的 JOIN根据某个关键字段把两张表的行关联起来。# 上下拼接多个月份的同结构表 pd.concat([df_1月, df_2月, df_3月], ignore_indexTrue)concat 的axis0是纵向拼行需要各表列名一致axis1是横向拼列靠行索引对齐。这里最容易出的问题是行索引没有重新编号结果是新表里索引还是原来各表的索引出现大量重复索引。所以纵向拼接时记得加ignore_indexTrue。7.2 merge就是 SQL 的 JOINmerge 的使用必须熟练到闭着眼睛能写df_订单.merge(df_用户, on用户ID, howleft)on指定两边同名的关联键如果两边键名不同用left_on和right_on。how参数控制保留哪边的记录inner 是只保留两边都能匹配上的left 是保留左表全部right 是保留右表全部outer 是全部保留。初学者最常翻车的点是关联键重复。如果右表的关联键不唯一一个用户对应多行历史信息merge 之后左表每一行都会和右表的多行匹配行数瞬间膨胀。所以我每次 merge 前会先检查关联键是否唯一df_用户[用户ID].is_unique如果不唯一就要想清楚自己到底想要的是多对一还是多对多必要时先按业务规则去重或聚合再 merge。7.3 多表处理流程建议多个表一起处理时我总结的顺序是每张表先各自清洗和去重再合并。如果先合并再做清洗脏数据会在多张表之间互相污染排查起来非常痛苦。merge 时可以加一个indicatorTrue参数结果里会多出一列_merge标记每行是只在左表、只在右表还是两边都有。这个功能在排查“为什么合并后多出来很多空行”时特别好用比肉眼比对快得多。8. 性能优化把 pandas 用快的几个基本功8.1 向量化优于循环这是 pandas 学习中最该早一点建立的习惯能用整列运算解决的不要写 for 循环。原因很简单pandas 底层大量操作是对整个 numpy 数组执行 C 级别的循环而 Python 的 for 循环每跑一次都要经过解释器速度差出几十上百倍。比如要生成一个新列df[折扣价] df[原价] * df[折扣]这个写法就是向量化直接对整列做运算。新手最容易写成for i in range(len(df)): df.loc[i, 折扣价] df.loc[i, 原价] * df.loc[i, 折扣]跑起来不仅慢还容易触发索引问题。只有极少数必须逐行依赖前面行结果的场景比如累计值、状态跳变才需要循环或专门的高性能写法。8.2 apply 别乱用df.apply(func, axis1)本质上还是循环只是语法上优雅了一点数据量大时依然慢。apply适合那些逻辑复杂、无法简单向量化的场景但使用之前先想一遍能不能用布尔索引、np.where、groupby.transform 替代还有个容易混淆的点map一般用于 Series 的单列值映射applymap用于对整个 DataFrame 的每个元素做操作apply则更常用于按行或按列做函数处理。三者不要混用报错信息会很迷惑。8.3 大文件与大 DataFrame 的内存优化处理几十万行是 pandas 的舒适区到了几百万行就要考虑内存了。最有效的一招是 read_csv 时显式指定 dtype把不参与运算的 ID 列转成字符串、把基数很低的分类列转成 categorydf pd.read_csv(big_data.csv, dtype{用户ID: category, 地区: category})category 类型在底层只存整数码和一份类别表内存占用可以降到一个数量级以下而且 groupby 时速度还会更快。如果文件实在太大用chunksize分块读取每块处理完就写入目标结果不要一次性读完再处理。检查内存占用可以用df.memory_usage(deepTrue).sum()看到结果往往比预想大很多那时候就知道优化方向在哪里了。9. 高频报错与坑位速查9.1 整天见到的 SettingWithCopyWarning这个警告几乎伴随每个 pandas 新手的整个学习期。它本质上是链式索引带来的“视图还是副本”不确定问题。处理方法不复杂筛选后想改值统一用df.loc[mask, 列名] value如果只是想取一个筛选子集来做后续分析就显式.copy()。理解了这两点这个警告基本从你的世界消失。9.2 日期看起来对类型却不对打印出来明明是 2024-01-01但df.info()显示 object于是排序和分组全按字符串逻辑来。遇到日期列第一时间pd.to_datetime(列, errorscoerce)之后所有时间运算都正常了。这个坑之所以常见是因为很多源文件导出时日期列实际存的是文本。9.3 NaN 参与比较导致结果丢失写df[df[分数] 60]时分数为 NaN 的行会被直接过滤掉。如果这些缺失值本身有意义比如缺考你要么先填充一个边界值要么在筛选条件里额外加df[分数].isnull()把缺失行保留下来。关键是意识到条件筛选默认排除缺失而不是报错提醒你。9.4 索引对齐导致“结果乱跳”如果你做df[新列] 某个 Series这个 Series 的索引会和 df 的索引自动对齐对不上的位置补 NaN。这通常不是 bug而是设计特性但确实会让新手怀疑人生。遇到这种结果下意识检查两边索引是否一致必要时先.reset_index(dropTrue)再做。9.5 列名里的“隐形空格”和中英文差异从 Excel 导出的表列名经常带着肉眼看不到的首尾空格于是df[销售额]报 KeyError而你明明看到有一列叫销售额。排查办法是打印df.columns.tolist()用引号把列名框起来看有没有空格。处理方式是df.columns df.columns.str.strip()并且在解析文件时统一好编码。9.6 学习时很容易忽略的两件小事第一个是“改完数据后别忘了赋值回变量”。很多方法默认返回新对象比如df.dropna()不会直接修改原 df如果你没有重新赋值后面的操作用的还是旧数据结果天然不对。第二个是“大段操作前先复制一份数据”。我在踩过几次惨痛教训后凡是要做多步清洗都会先df_原始 df.copy()留个底改坏了随时能回到起点。10. 说点我自己折腾下来的体会写到最后分享一点个人经验。我最初学 pandas 时也走过弯路喜欢从教程里抄 API好像每个函数都见过但一碰到真实数据就卡壳。后来我换了一种方式不再按函数清单学而是拿真实业务数据给自己出题比如“哪个区域的销售额最高但利润率最低”“哪个月度的订单量波动最大”“不同渠道的复购率差异怎么算”。被问题驱动着去翻文档凡是解决问题的函数自然就记住了那些用不上的冷门 API 也没必要强行记。如果你正在学 pandas我建议你也建立一套自己的“实操笔记”每个知识点配一个小例子标注它为什么好用、在什么场景下会踩坑。这份笔记要随着你的使用不断补充而不是一次写完整。真正让你进步的不是背下多少函数而是你处理过的每一份脏数据、定位过的每一个警告、纠正过的每一次索引错位。把这篇笔记里的操作都亲手跑一遍再去面对自己的数据你会有一种“原来不过如此”的踏实感。