1. Pandas排在一堆表格折腾什么名堂——先用一个真实场景把底层需求讲透
大概三年前,我处理一份十几万行的渠道订单明细时,一开始用的是纯Python循环加字典统计。那段代码跑起来慢得离谱,一个分组求和能转好几分钟,而且写着写着就乱套:一会儿是for row in rows,一会儿把列表套进列表,后面接手的人看了直接想骂人。后来一个同事递给我一句话:“别硬写了,用pandas。”
当时我对pandas的理解还停留在“一个能存表格的库”,真上手之后才意识到,这东西根本不是“存表格”那么简单。它是把“对表格数据的思考方式”整个换了一个维度:从“逐行遍历、逐个判断”换成了“整列运算、一次性广播”。难听点说,以前你在给Excel写宏,之后你在用一种更接近数据库的方式操作内存里的表。
这篇文章的目标读者,我默认是这么几类人:Python基础语法已经过关、写过几十行处理数据的代码但总觉得别扭的同学;已经从文本文件或者接口里拿到过一批数据、想要做清洗聚合但不知道该怎么组织的朋友;还有那些看了不少教程、能跟着敲出df.groupby('城市').mean()但内心其实不明白“为什么groupby之后不直接返回一个DataFrame”的人。
如果你的水平已经能熟练操作DataFrame的索引切片、合并、透视,那这篇对你来说前面几节可以快速扫一眼,重点看后面关于groupby底层逻辑重建、merge易错点、以及性能习惯的部分——那才是这篇文章里我认为最值钱的内容。
先说清楚pandas到底“解决了什么问题”。用一句话概括:Python原生语法处理表格数据时,代码复杂度和数据量呈线性恶化;pandas把二维表抽象成带轴标签的结构,让行、列、标签、值四者之间的操作可以批量完成。
这句话拆开有两层意思。
第一层,Python原生处理二维表,本质上是在“列表套列表”或者“列表套字典”里手动维护下标关系。你想要“按城市分组算平均销售额”,你得先找出全部城市,再为每个城市开一个列表往里塞数值,最后再对每个列表求均值。这套动作写出来大概二十行,跑起来效率还低,因为每次都重新扫描。
第二层,pandas把“行”和“列”都给了名字(索引和列名),操作的时候不用再记着“第零列是城市、第三列是销售额”,而是直接说“我要处理df['销售额']这一列”。当你想把两组数据按某个键合并在一起,直接merge;当你想把长表拉成宽表,直接pivot_table。这些操作每一件都相当于原生语法下十几行到几十行的体力活。
所以我在带新人的时候经常说:别看pandas的API又多又杂,真正理解它的门槛只有一个元认知——所有操作都可以想象成“整条数据流在管道的各个节点上被变换”,而不是“一行一行地手动加工”。
这个开头的结论如果大家能消化掉,后面所有章节都变得顺理成章。
2. Series与DataFrame两个底层结构得掰开揉碎——对轴的理解决定你会不会用
用pandas的第一步,永远是搞清楚它的两个核心容器长什么样。我见过不止一个新手把DataFrame当Excel表格理解之后就万事大吉,结果遇到axis=0和axis=1就开始蒙圈,遇到set_index和reset_index就乱套。说到底,就是没有真正理解“轴”这个概念。
2.1 Series不是“一列数字”,是“带标签的一维数组”
Series最简单的理解是一个“带标签的数组”。它有两个组成部分:左边的index(索引)和右边的values(值)。普通Python列表[10, 20, 30]只有值,想访问第二个元素只能用位置values[1];Series则允许你给它每个元素起个名字:
import pandas as pd s = pd.Series([10, 20, 30], index=['一月份', '二月份', '三月份']) print(s['二月份']) # 20看起来很简单的功能,但它的意义是革命性的:你不再依赖“位置”来访问数据,而是依赖“语义”来访问数据。这个转变真的很关键,因为真实业务里的数据顺序经常变化,但如果每个值都带着自己的标签,无论怎么排序、过滤,你只要按照标签取数就不会取错。
Series的索引不要求是唯一的。两个索引可以相同,此时用s['二月份']会返回一个小Series而不是标量,这个特性在处理重复标签数据时非常有用。初始学习阶段我不建议去深究重复索引的取数逻辑,但是心里要有数:不要以为索引就是主键。
Series和Python字典的区别也值得说一下。字典是“键值对的容器”,Series是一维带标签数组,它除了取数之外还支持算术运算、统计函数、自动对齐。比如两个Series相加,pandas会按照索引自动对齐,而不是简单地把数值按位置相加:
a = pd.Series({'x': 1, 'y': 2}) b = pd.Series({'y': 3, 'z': 4}) print(a + b) # x NaN # y 5.0 # z NaN这里就产生了一个非常核心的概念:数据对齐。两个数据集的索引没有重叠的部分,运算结果会给NaN(缺失值),而不是报错。这个特性从Series延伸到DataFrame,是pandas区别于纯数组计算的最重要机制之一。
2.2 DataFrame的二维本质:一串Series拼成的表
DataFrame的官方定义是“带标签的二维数据结构”,听起来抽象,但你可以把它想象成“一个由多个Series组成的字典”,每个Series就是表里的一列,Series的索引就是DataFrame的行索引。
很多人忽略了一件事:DataFrame的行索引和列索引是两个独立的概念。列索引是列名,行索引默认是0到n-1的整数,但你可以把任何一个有业务含义的列设成行索引(比如日期、订单号),这就是set_index干的事情。
我在实际项目里最常用的一个习惯是:拿到数据后,先看df.info()确认列名、非空值数量和数据类型,再看df.head()观察样例数据,然后立刻判断哪一列适合做行索引。这一步看着简单,却决定了之后所有操作的顺滑程度。比如时间序列数据,我一律把日期列设为索引,这样后续按时间切片直接df['2024-01':'2024-03']就完成了,比先用布尔掩码筛选要舒服得多。
DataFrame的另一个特点是可以容纳多种数据类型:一列是整数,一列是字符串,一列是浮点,一列是日期对象,完全没问题。整张表在底层被拆成多个独立的一维数组(按列存储),每列类型独立优化,这也是为什么DataFrame能高效处理异构表格数据的原因。
2.3 索引、切片、布尔过滤——最常用却最容易混淆的操作组
DataFrame的取数操作大概可以分三类。
第一类是按列取:df['列名']返回Series,df[['列名1', '列名2']]返回DataFrame,注意中括号里面是单引号还是列表,返回结果完全不一样。这里的坑是:你写df['列名']得到的是Series,但有些函数(比如apply)处理Series没问题,处理DataFrame时需要额外的轴参数,很多人第一次踩坑就踩在这里。
第二类是按行取:.loc[]按行索引的标签取,.iloc[]按行索引的位置取。这两个我必须建议所有初学者强制记忆区分:loc的括号里写的是“标签”或者“布尔数组”,iloc的括号里写的是“整数位置”。比如df.iloc[0]永远是第一行,而df.loc[0]取的是行索引标签为0的那一行——如果行索引不是整数,df.loc[0]很可能直接报错。
第三类是布尔过滤:df[df['销售额'] > 10000],里面那一整串df['销售额'] > 10000会生成一个布尔Series,再把它作为筛选条件。这张“布尔面具”是pandas选项式数据筛选的灵魂,几乎所有复杂筛选最终都归结为“构造一个正确长度的布尔数组”。
我把这三类操作称为“取数的三板斧”,大多数业务需求——比如“保留华东区域且销售额超过1万的订单里的前五十条”——就是这三类操作的组合。
很多人问过我:为什么pandas要搞出[]、.loc、.iloc三种取数方式,直接用一种统一的不行吗?答案是历史原因加实用性考量。[]是Python对象的原生方法,语法简洁但容易歧义(到底是取列还是取行,看参数类型);.loc和.iloc是pandas后来强化的显式接口,一眼就能看出你是按标签取还是按位置取。面对正在运行的生产代码,我强烈的建议是:能写df.loc[df['城市'] == '上海', '销售额']就千万不要写df[df['销售额']]['城市']这样语义模糊的链式取数,后者不仅可读性差,还可能触发不可预期的视图/复制问题。
3. 数据读进来先别急着分析——数据读写的关键参数和清洗环节必须过一遍
pandas最强能打的地方不是“存数据”,而是“把各种格式的数据拉进来变成DataFrame”。CSV、Excel、JSON、SQL、剪贴板,都有一行读入的接口。看起来区别就是read_csv和read_excel不同函数,但真正决定后续操作顺不顺利的,是读入时那些容易被跳过的参数。
3.1 read_csv里的几个真正值钱的参数
pd.read_csv('路径/文件名.csv')是最基础的用法,但实际业务里一份CSV文件很难这么干净。
最常见的坑是编码问题。一个CSV文件可能是utf-8,可能是gbk,也可能是带BOM的utf-8-sig。读出来乱码的时候换编码是个笨办法,更靠谱的是先看一眼文件前几行,或者直接用file命令看一下编码。我的习惯是读取时不指定encoding,先报错或乱码,再用encoding='gbk'试,再不行试encoding='utf-8-sig'。
第二个高频参数是parse_dates。如果你的数据里有一列是“2024-06-01 12:30:00”这种字符串,默认情况下它会被读成object类型,后面的时间运算(比如减一天、按周分组)统统做不了。你需要提前告诉pandas把它解析成时间类型:
df = pd.read_csv('订单.csv', parse_dates=['下单时间'])这样读进来的df['下单时间']才是datetime64类型,才可以直接做时间差计算、按月聚合、画时间序列图。这一步是很多人“数据看起来没错但就是没法分析”的根源。
第三个参数是usecols。一张十几列的表,你只需要其中四五列,完全可以在读取时只保留需要的列,大幅减少内存占用,也能让数据预览更清爽。还有nrows,大文件调试的时候先读前一千行验证代码逻辑,别动不动就全量读入。
第四个常用参数是dtype。有些列是ID类型但保存成了数字,比如“订单编号1234567890”,默认读成int64,但真实的业务编号应当作字符串处理,因为它们不参与数学运算,甚至可能被pandas的数值推断误伤(比如编号“00123”读进来变成123,丢失了前导零)。此时读入时指定dtype={'订单编号': str}就可以保持原样。
3.2 缺失值、重复行和类型转换——清洗三板斧
读进来的数据很少直接能用于分析,通常逃不掉三类问题。
缺失值是最大的一类。一个DataFrame里出现NaN,要么是数据本身就没采集到,要么是在合并时对齐失败产生的。应对手段就两个方向:扔掉或者填上。dropna()默认丢到有缺失值的行;fillna()可以用默认值、前一条值、后一条值或者该列均值填充。什么时候该填、什么时候该丢,没有绝对标准,只能根据分析目的来。比如算销售额总和的时候,如果某行的数值列缺失,保留不填会导致总和偏小,但如果该行其他列信息还有用,直接整行删除又可惜。这时候可以先填充0再做汇总,或者先做汇总再单独评估缺失的影响。
重复行的处理很多人会用df.drop_duplicates(),但我要提醒一个细节:这个函数默认判断的是整行完全相同才算重复。如果你只想按“订单号”去重,要写df.drop_duplicates(subset=['订单号'])。另外还有一个选择是keep参数:保留第一条重复记录、最后一条,还是都不保留。业务里常用的场景是“同一订单出现多次配送记录,只取最后一次状态”,这就是keep='last'。
类型转换是隐藏的坑王。df['金额']看起来是数字,但df['金额'].dtype可能是object,因为原始数据里混入了一个空字符串或者“12,000”这样的带逗号文本。此时astype('float64')直接报错,正确的处理方式是先把这类字符清洗掉,用pd.to_numeric(series, errors='coerce')把无法转换的非数字强制变成NaN,然后再统一填充。
3.3 数据写出的注意事项——别让辛辛苦苦清洗完的数据载在最后一步
跟读取对称,写出也有几个常见坑。df.to_csv('结果.csv', index=False)是必须养成习惯的基础操作,不然你的输出文件里会莫名其妙多出一列Unnamed: 0,白白浪费别人下载文件后第一轮洁癖发作的时间。
如果需要保存带格式的Excel多工作表,可以用pd.ExcelWriter:
with pd.ExcelWriter('结果.xlsx') as writer: df1.to_excel(writer, sheet_name='汇总', index=False) df2.to_excel(writer, sheet_name='明细', index=False)ExcelWriter的好处是能把多张表写进同一个工作簿,这在交周报、出报表场景下非常实用。
还有一个很少被提到但特别影响体验的细节:写出CSV时,本身含有中文的DataFrame最好指定encoding='utf-8-sig'。普通utf-8在部分版本的Excel里打开会乱码,utf-8-sig多带一个BOM头,Excel能正确识别中文编码。这个小改动在跨部门协作时能省掉无数个“为什么我打开是乱码”的问答。
数据清洗这块,我想强调的是流程意识。不要指望一段代码解决所有脏数据问题。我自己的惯例是:读入数据后在项目笔记本里先做一版“体检”——打印列名、行数、缺失值统计、各列类型,再动手清洗。体检结果决定了清洗策略,而不是凭着感觉一顿dropna瞎操作。清洗过程的每一步最好都另存一个新列名或者新DataFrame,保留原始数据随时回溯,这也是经验之谈。
4. groupby拆分合并的那一套逻辑才是Pandas的灵魂——搞清楚它在底层究竟干了什么
如果让我只选一个pandas里最值钱的功能,我会毫不犹豫选groupby。它是“分组-聚合”这套数据思维在Python里最经典、最优雅的实现,也是很多从Excel转过来的人最难理解透彻的一块。
4.1 groupby不是“一个函数”,是“拆分-应用-合并”三步范式
很多教程讲groupby一笔带过,告诉你“按城市分组、求平均销售额用这个就行了”,但完全没有解释为什么df.groupby('城市').mean()能直接得到结果。如果你的理解也停留在这个层面,一旦遇到更复杂的需求,比如“每个城市、每月、各品类合计”,立刻就会卡住。
groupby的底层逻辑是“拆分-应用-合并”,英文叫 split-apply-combine。第一步是按分组键把数据拆成若干个小组;第二步是把你指定的聚合函数应用到每个小组上;第三步是把所有结果合并回一张表。
举个例子,df.groupby('城市')['销售额'].mean()这个表达式就可以展开成三个动作:
- 按“城市”列把整张表分成若干子DataFrame,每个子表包含某一个城市的所有行;
- 从每个子表里取出“销售额”这一列,计算平均值;
- 最后把每个城市的平均值汇总成一个索引为城市名、值为平均销售额的Series。
所以groupby之后如果不紧跟着聚合函数,它并不会返回一个你肉眼可见的结果,而是一个GroupBy对象——你可以把它理解成“这堆数据已经被分组了,但还没做任何计算”。如果你尝试print(df.groupby('城市')),得到的是“DataFrameGroupBy”对象的描述,而不是数据本身,这就是很多新手困惑的点。
4.2 agg这个名字不太好听,但它是聚合操作的核心开关
实际业务里的聚合需求往往不止一个函数:既要总和,又要均值,还想要最大值、最小值,甚至还想看数量有多少。你有没有想过,df.groupby('城市')['销售额'].agg(['sum', 'mean', 'max', 'min', 'count'])为什么能一次性算出这么多结果?
因为agg允许你传一个“函数列表”,pandas会对每一列分别应用这些函数,然后把结果拼成一张新DataFrame。列名默认是'test_name_original'这种(函数名, 原列名)的多级索引,不太好看,你可以在agg后面紧接着.round()或者重命名列名,让结果变得直观。
聚合时还有一个很容易被忽略的维度:groupby的默认行为是“以分组键作为新DataFrame的索引”。如果你不希望城市名变成索引而是保持为普通列,可以在groupby后加as_index=False。这个参数在后续merge或者to_excel的时候影响很大,因为很多人不习惯看索引,经常把索引列当成普通列操作,结果出现KeyError。老实说,我早期写代码就吃过这个亏,groupby后的DataFrame索引列没还原,直接to_csv,输出结果缺了一列“城市”,还找了半天。
4.3 透视表与交叉表——多维分组的便利表达
如果你觉得groupby之后还需要用unstack才能把多维分组变成宽表太麻烦,那么pivot_table和crosstab就是专门为你设计的。
pivot_table可以看作是“带维度布局的分组聚合”:
pd.pivot_table(df, values='销售额', index='城市', columns='月份', aggfunc='sum')这一行代码做的事情,等价于先按城市和月份分组,求和,再把月份那一维给展开成多列。产出是一个二维表格:行是城市,列是月份,格子是销售额,非常直观。
透视表底层仍然是groupby,但它的优势在于处理的结果直接呈矩阵布局,特别适合出报表和进一步画热力图。我的经验是,如果要快速“扫一眼数据在各维度的分布形态”,透视表比groupby更顺手;如果是继续做链式分析(比如过滤、合并、接下一步操作),groupby的结果更合适。
crosstab专门用于统计两个离散列的出现频数,行是第一个变量的取值,列是第二个变量的取值。它统计的是数量而不是数值的和,所以在做“订单状态与支付方式的分布分析”时特别好用。它其实是pivot_table在aggfunc='count'场景下的快捷方式,但用起来是真的方便。
4.4 一个复杂的实战例子——把每一步都拆开看
单独讲概念不如直接看一个贯穿的分组案例。假设我们的DF里有三列:城市、品类、销售额,要把每个城市下每个品类的销售额总和算出来,并按城市总销售额排序。
result = ( df.groupby(['城市', '品类'], as_index=False)['销售额'] .sum() ) city_total = result.groupby('城市', as_index=False)['销售额'].sum() city_total.columns = ['城市', '城市总销售额'] result = result.merge(city_total, on='城市') result = result.sort_values('城市总销售额', ascending=False)第一步按两个键分组,得到每个城市-品类的细粒度总和;第二步按城市再聚合一次,得到每个城市的总盘;第三步把这两个结果按城市合并起来,让每行既能看到品类的贡献,也能看到城市的体量;最后按城市总销售额排序。这样一张结果表就能直接回答“哪些城市的哪个品类贡献最大、哪些城市的盘子本来就大”这类问题。
这个例子想说明的是:groupby可以连续运作,先细粒度分组,再粗粒度分组,最后通过merge把不同层次的结果合并。掌握这个链路,比学一百个孤立函数都管用——因为大多数业务分析都是由“不同粒度的汇总指标”组合而成的。
5. 连接合并与重塑——想把多张表玩成一张表,必须读懂merge的几个隐藏规则
做数据分析的人迟早会遇到这样一个场景:你有两张表,一张是订单明细(订单号、商品、金额、时间),另一张是客户信息(客户ID、地区、等级),要把两张表按客户ID对应起来。这就是“表连接”,pandas里最常用的方法是merge和concat。
5.1 merge的四种连接方式——on、how、left_on、right_on到底怎么配
pd.merge(df1, df2, on='客户ID')是最标准的写法,意思是“两张表都有一列叫客户ID,以这一列相等作为条件,把两行拼成一行”。on只能用在两边的键列名完全一致的场合,如果不一致,就要用left_on和right_on分别指定:
pd.merge(订单表, 客户表, left_on='客户编号', right_on='客户ID', how='left')这里的how参数是另一大重点。它是把哪张表作为基准:
how='inner':只要两边匹配得上的记录(交集)how='left':保留左边表的所有行,匹配不上的右边字段填充NaNhow='right':保留右边表的所有行,匹配不上的左边字段填充NaNhow='outer':两边的全部记录都保留,匹配不上的填NaN
实际业务中使用频次最高的是left。比如“所有订单都保留,客户信息能查到就带出来,查不到也不丢订单”这种需求,正好对应how='left'。如果你误用了inner,那客户信息缺失的订单会整个消失,结果表行数变少,后续汇总全是错的。这个错误我用肉眼都见过好多次,教训很深。
5.2 merge时两个最常见的翻车现场——键不唯一和数据膨胀
第一个翻车现场是键不唯一。假设订单表里同一个客户有10笔订单,客户表里这个客户只有一行信息,这时候merge很聪明,会给这10笔订单都复制一份客户信息,结果行数不变。
但反过来就危险了:如果客户表里同一个客户出现了2行(比如一个人维护成两行,等级信息不一致),那订单表里这个客户的10笔订单会和客户表的2行进行笛卡尔积,变成20行。行数不可思议地膨胀了,而你往往过了很久才发现总和怎么翻倍了。这类问题很难用肉眼察觉,最管用的排查手段是merge之前对键列检查唯一性:
print(客户表['客户ID'].duplicated().sum())一旦发现客户表有重复ID,先去掉重复,再merge。这个习惯真的救了我很多次。
第二个翻车现场是merge后出现名称冲突的列。pd.merge(订单表, 客户表, on='客户ID')如果两张表里都有“创建时间”这一列,pandas会自动把它们改名为“创建时间_x”和“创建时间_y”。这种列名毫无可读性,后续你计算时间差时特别容易拿错列。解决方案是merge前用suffixes参数指定后缀,或者干脆在merge前把重复列名rename掉。
5.3 concat和merge的区别——一个按行堆,一个按列拼
pd.concat([df1, df2])默认是按“行方向”往下堆,要求两表的列名一致。它的典型场景是把多个月份的数据文件读进来后纵向拼接成一张全年总表。pd.concat([df1, df2], axis=1)则是横向拼接,像在Excel里把两张表并排放,连接的条件是行索引对齐。
concat不关心行之间的“业务关联”,它只做物理上的拼接。所以什么时候用merge、什么时候用concat,判断标准很简单:是否需要根据某个键把两张表的行对应起来?需要就用merge;只是把结构相同的几块数据连起来就用concat。
5.4 长表宽表互相转——pandas里tap最容易被小看的换形能力
除了连接,重塑是另一种高频操作。Excel里有“数据透视表”,pandas里对应的就是上面提到的pivot_table;反过来,宽表变长表的操作叫melt。
melt的使用场景很典型:你手上有一张表,每一列是不同月份的销售额(1月、2月、3月...),但做时间序列分析时需要一个“长表”:每行是一个日期点的单一数值。这时:
long_df = df.melt(id_vars=['城市'], value_vars=['1月', '2月', '3月'], var_name='月份', value_name='销售额')id_vars是需要保留不变的标识列,value_vars是哪些列要被“折叠”成两列:一列是变量名,一列是变量值。这个函数在处理多期数据的业务报表时非常常用,但教程很少重点讲。
stack和unstack也是表形转换的工具,它们是围绕“索引的层级”做变换的,功能上跟pivot_table有重叠,但适用场景稍微专业些。日常数据分析中melt和pivot_table的组合基本可以覆盖九成需求,不用非得把四个函数全都用熟。
6. 写了半年Pandas之后我几乎不再用apply了——性能优化的两三个核心习惯
文章接近尾声,我想聊点“API文档上不会写、但生产环境迟早教你做人”的经验。
很多新手学了apply之后非常兴奋:可以传Lambda表达式对每一行做自定义操作,灵活又方便。这个函数确实很好,但也是最容易让代码变慢的元凶。
apply本质上仍然是“逐行(或逐列)Python循环”,它的形式很简练,但性能远不如内置的向量化操作。pandas的设计初衷是尽量把运算下沉到底层C或NumPy的数组级别,而不是每次在Python层跑循环。比如“对销售额每行加10%提成再做向下取整”,正确做法是:
df['调整后'] = np.floor(df['销售额'] * 1.1)这里的*1.1和np.floor都会沿着整个Column一次性向量化执行,几十万行数据瞬间完成。而这段代码用apply写:df['调整后'] = df['销售额'].apply(lambda x: math.floor(x * 1.1)),写法一样短,性能可能慢几十倍。
另一个被忽视的性能点是用好query和eval。如果你的DataFrame有几十列、几百万行,而你需要做多条件筛选,df[(df['销售额'] > 1000) & (df['城市'] == '上海')]这种写法每一步都会生成一个中间布尔Series,占用额外内存。改用df.query('销售额 > 1000 and 城市 == "上海"'),pandas会调用底层的表达式求值引擎,在不用动整张表的情况下完成筛选,内存和速度都有改善。
还有一点是尽量别在循环里反复拼接DataFrame。我曾经见过一段代码,在for循环里逐行pd.concat,跑几百次就要好几分钟。正确的做法是把每一次的结果放进一个列表,循环结束后一次性pd.concat:
pieces = [] for chunk in chunks: result = process(chunk) pieces.append(result) final = pd.concat(pieces, ignore_index=True)一次拼接的开销远小于多次拼接的开销之和,这个规则几乎适用于任何数据处理场景。
内存方面,如果你的表足够大,读进来发现内存吃紧,可以检查每列类型,把那些取值种类很少的列转成category类型:
df['等级'] = df['等级'].astype('category')类别型列在底层用整数编码存储,能省下不少内存,尤其当某个列只有十几个不重复值时效果拔群。
性能优化这块归根结底就是一个观念转变:**能用一次性向量化操作解决的,就不要写循环;能整列整列处理的,就不要逐行逐行地去想。**你在Excel里拖动公式的时候,也是拉一列一列的,没有人会一行一行地双击右下角,对吧?pandas的思维方式跟这个完全同构。
但我也不是完全否定apply。当逻辑确实需要访问多列、并且包含无法向量化的条件分支时,apply是合理的工具。关键是要有取舍意识:先问自己“这个操作能不能拆成纯数组运算”,能就绝不apply。
这篇文章里讲的都是平时最容易碰到、也最容易出问题的点:从Series和DataFrame的轴概念,到读数和清洗习惯,再到groupby的拆分-应用-合并本质,最后是merge的键和性能习惯。我个人这几年的体会可以浓缩成一句:pandas不是一个需要背API的库,而是一套“把表格思考方式搬进代码里”的思维框架,你越早建立这个框架,后面遇到的每一个数据问题都会轻松很多。