Pandas DataFrame完全指南:从创建到数据清洗的十大核心操作

📅 发布时间:2026/10/14 1:45:57
Pandas DataFrame完全指南:从创建到数据清洗的十大核心操作
1. DataFrame是什么一张能装各种数据的电子表格1.1 DataFrame的三大特征与Series的关系先说点实际的。我第一次接触Pandas的时候最困惑的就是不知道Series和DataFrame到底有什么区别。后来我用一个很土但很好记的方式来理解Series是一列数据DataFrame是多列数据拼在一起。DataFrame就像Excel里面的那张工作表有行有列列有列名行有索引。而Series就是这张表里的某一列。DataFrame有三个非常关键的特征理解了这三个特征后面所有操作都能顺理成章第一它是二维结构。这个二维不是你数学课上学的那种坐标系你可以把它想象成一张表格。横着看是行竖着看是列。数据分析里绝大部分数据其实都是这种二维形式比如销售记录、用户信息、实验数据一行是一条记录一列是一个字段。第二它是有标签的。每一列有列名每一行有行索引。这一点比NumPy的二维数组要友好得多。NumPy数组里你要访问数据只能靠位置比如arr[0, 1]取第一行第二列但DataFrame里你可以直接df.loc[行标签, 列标签]或者df[df[年龄] 18]这样用条件去筛。标签的意义在于你不用记住第几列存的是什么你只需要知道列名就行了。第三每一列的数据类型可以不一样。这是DataFrame和NumPy数组一个非常重要的区别。NumPy数组要求所有元素类型一致但现实中的数据哪有那么规整一列是整数年龄一列是字符串姓名一列是浮点型收入DataFrame都能装得下。这个特性让我在数据清洗的时候省了太多事。1.2 为什么用Pandas而不直接操作Excel或纯Python有人可能会问Excel也能做数据分析Python列表也能存数据为什么要用Pandas我用一个实际场景来说明。假设你有一份十万行的销售数据要按省份、按月份汇总销售额。用Excel做你可能会用透视表用纯Python写循环你要写好几层嵌套但用Pandas三行代码搞定先groupby再聚合再重置索引。全流程跑下来可能连一秒钟都用不了。再举一个例子当你需要把两个表关联起来比如一份订单表和一份用户表订单表里有user_id用户表里有id。你要把用户的城市信息加到订单表里。在Excel里你可能要用VLOOKUP填公式、下拉、还要担心匹配错误。在Pandas里就是一个merge方法一行代码的事匹配逻辑、连接方式全都可控。还有一个很重要的点Pandas的数据结构和数据库表非常像。你如果以后去接触SQL、接触数据仓库会发现思维方式是完全相通的。学会了DataFrame你就等于学会了数据分析的第一块基石。1.3 安装与准备工作五秒钟搞定环境正式开始之前先把环境准备好。如果你用的是Anaconda那大概率已经内置了Pandas不需要额外安装。如果你用的是原生Python打开终端或者命令行一条命令搞定pip install pandas安装完成后在代码里导入它。导入的时候大家都会用别名pd这是社区约定俗成的习惯就像NumPy用np一样看到pd你就知道是Pandasimport pandas as pd import numpy as np后面所有代码都默认已经执行了这一行导入。注意我强烈建议你在一个虚拟环境里操作不要直接往系统Python里装包。原因很简单不同项目的依赖版本可能会互相冲突装一个包顺带升级了一堆依赖回头某个项目就跑不起来了。我用conda创建环境的时候通常直接指定Python版本conda create -n pandas_tutorial python3.10 -y然后激活环境再装Pandas。这个习惯在踩过一次系统Python被搞得乱七八糟的坑之后就再也没有变过。2. 创建DataFrame从零开始的五种常用姿势2.1 用字典创建最直观、最符合直觉的方式创建DataFrame最常见的方法是传一个字典进去。字典的键会成为列名字典的值通常是列表会成为那一列的数据df pd.DataFrame({ 姓名: [张三, 李四, 王五, 赵六], 年龄: [25, 30, 28, 22], 城市: [北京, 上海, 广州, 深圳], 月收入: [12000, 15000, 11000, 9800] }) print(df)输出结果会是一张4行4列的表格行索引默认是0到3。这里有个细节值得注意字典的键顺序在Python 3.7之后是有序的所以DataFrame的列顺序会严格按照你写字典时的顺序排列不用担心乱掉。如果没有给行索引指定名字Pandas会默认使用0到n-1的整数。如果你想自定义行索引可以在创建时加一个index参数df pd.DataFrame( {产品: [鼠标, 键盘, 显示器], 价格: [99, 199, 899]}, index[订单A, 订单B, 订单C] )这样行索引就变成了订单A、订单B、订单C有点像是给每一行起了个名字。2.2 用列表的列表创建适合先收集数据再统一生成第二种常见方式是传入一个二维列表同时指定列名data [ [苹果, 5, 3.5], [香蕉, 10, 2.8], [橘子, 7, 4.2] ] df pd.DataFrame(data, columns[水果, 数量, 单价])这种方式在处理从文件读取的数据时特别有用。比如你写了一个爬虫或读了一个文本文件所有数据先存成一个二维列表最后一步再转成DataFrame结构非常清晰。但注意一个细节如果列表的列数多于columns指定的数量会直接报错。反过来如果列数少于columns指定的数量其余列会用NaN填充。这些都是我在实操中确实遇到过的边界情况。2.3 从NumPy数组创建数据分析联合作战的基础Pandas和NumPy是天生一对。很多场景下你用NumPy生成了数据然后需要加上列名做进一步分析这时候直接把数组传给DataFrame就行arr np.random.randn(5, 3) # 5行3列的标准正态分布随机数 df pd.DataFrame(arr, columns[A, B, C]) print(df)这里生成的A、B、C三列每列都是浮点数。你可能会想这样跟直接用NumPy数组有什么区别区别在于有了Pandas之后你可以用df.describe()一行拿到每列的均值、标准差、最小值、四分位数而用纯NumPy你得自己慢慢算。还有一种常见用法是从二维数组创建后把某列设为索引df pd.DataFrame(arr, columns[A, B, C]) df.index [row1, row2, row3, row4, row5]2.4 从CSV或Excel直接读取现实中用得最多的方式真实项目里你很少会手动创建DataFrame更多是读取文件。Pandas的read_csv方法是我用得最多的函数没有之一df pd.read_csv(sales_data.csv, encodingutf-8)一行代码文件里的数据就变成了DataFrame。如果你读Excel文件则要用read_excel它需要额外依赖openpyxl或xlrd库df pd.read_excel(sales_data.xlsx, sheet_nameSheet1)读取文件的时候有几个参数非常实用比如encodingutf-8能解决大部分中文乱码问题sep;用来指定分隔符nrows100只读取前100行做快速预览headerNone表示文件没有表头列名需要你自己指定df pd.read_csv(data.txt, sep\t, headerNone, names[col1, col2, col3])提示读取大文件时先用nrows和usecols做预览是非常好的习惯。比如这个文件是一千多万行的日志数据直接全部加载可能把内存占满甚至卡死。我通常先读前1000行看看结构再判断需要哪些列、怎么处理缺失值最后才决定是全量读取还是分批处理。2.5 创建时容易踩的三个坑第一个坑所有列表长度不一致。字典里如果姓名有4个值、年龄只有3个Pandas会报错提示长度不匹配。这个错误信息通常在项目早期出现很多次我的经验是在构造数据之前先认真检查数据的完整性别指望Pandas帮你自动补齐它能用NaN填充已经是极限了。第二个坑索引重复。如果你的行索引有重复值pandas并不会在创建时报错但等到你后面用loc做筛选时可能会得到意外的多行结果甚至有些操作直接报错。这个延迟爆炸的问题很难排查所以最好在创建时就保证索引唯一性或者用drop_duplicates处理。第三个坑中文列名。DataFrame完全支持中文列名但有些老函数或者可视化库对中文的支持不够好。我的建议是列名可以用中文但尽量避免在列名里带空格和特殊符号那会给你后面的操作比如使用.query()方法添很多麻烦。3. 数据查看与初步探索拿到数据先别急着分析3.1 前几行和后几行快速掌握数据外貌拿到一个新的DataFrame第一件事永远是看一眼。有两个方法最常用df.head() # 默认显示前5行 df.tail(3) # 显示最后3行这两个方法是我每天都用的特别是处理不熟悉的数据时head()能帮你快速确认列名对不对、数据格式对不对、有没有明显异常。而tail()在有些场景下也别有用途比如检查数据读取是否完整或者看最新的几条记录长什么样。如果你想要显示更多行或更多列可以这样设置显示选项pd.set_option(display.max_rows, 100) pd.set_option(display.max_columns, 20) pd.set_option(display.max_colwidth, 50) # 控制列内容的显示宽度这三个设置在我用Jupyter Notebook做数据分析时会经常用到。尤其是max_columns当你的表有几十列时默认设置会把中间一堆列折叠成省略号非常影响判断。3.2 info() 和 describe()最实用的两个探查方法head()只让你看到表皮info()才是真正帮你了解数据结构的神器df.info()它会输出三块信息行数范围和总行数、每一列的列名和数据类型Dtype、以及非空值的数量。通过info()的输出你能快速发现哪几列有缺失值、哪几列的类型不对比如明明应该是数字却显示为object。这个过程我一般称它为给数据做体检。describe()则是对数值型列做统计描述df.describe()输出包括计数count、均值mean、标准差std、最小值min、25%分位数、50%分位数中位数、75%分位数和最大值max。每一行都能帮你快速了解数据的分布情况。如果你希望describe()在分组时更有针对性可以这样用df.describe(percentiles[0.1, 0.9])3.3 形状、列名和索引基础信息快速获取还有三个非常基础的属性值得记牢df.shape # 返回 (行数, 列数)比如 (1000, 12) df.columns # 返回列索引对象可以看到所有列名 df.index # 返回行索引对象其中df.shape是我看得最多的一个属性它让我时刻知道自己的数据规模。而df.columns配合tolist()方法可以直接把列名转成Python列表这在动态处理列名时非常方便column_list df.columns.tolist()3.4 实战小例子五分钟快速了解一份陌生数据综合上面几个方法我通常用一个五连击来快速认识任何DataFramedf.head() df.info() df.describe() df.isnull().sum() # 统计每列缺失值数量 df.nunique() # 统计每列唯一值的数量这五条命令基本能在五分钟内让我对一份数据有全方位的认识。比如df.isnull().sum()的输出能直接告诉我哪些列需要处理缺失值df.nunique()能让我发现哪些列是分类变量、哪些列是唯一的ID字段。4. 数据筛选与索引用法取数据的两大流派详解4.1 列的选择单列、多列和排除某些列DataFrame取列的操作是最基础也最常用的。单个列可以用两种方式取df[年龄] # 返回一个Series df.年龄 # 返回一个Series但这种方式不推荐第二种方式属性访问虽然写起来很简洁但存在隐患——如果列名和DataFrame的方法名或属性名冲突比如有一列叫count或info就会出问题。所以我统一的做法是永远用方括号的形式取列绝不用属性访问。取多列时要传一个列表给它df[[姓名, 年龄]] # 返回一个DataFrame包含两列取除某些列之外的所有列可以用drop方法df.drop(columns[姓名, 城市])这个方法默认不修改原数据的你需要加一个inplaceTrue才会原地修改。关于inplace这个参数我多说一句在Pandas新版中它被标记为废弃倾向建议尽量避免使用——而是把结果重新赋值给变量这样做逻辑清楚也避免原数据到底改没改的混乱df df.drop(columns[姓名, 城市])4.2 行的筛选布尔条件的威力行筛选最常见的是用布尔条件。比如筛选年龄大于25的人df[df[年龄] 25]这段代码看起来有点绕我来解释一下执行的步骤df[年龄] 25 会生成一个长度和DataFrame行数相同的布尔Series每一行是True或False。然后把这个布尔Series作为索引传给dfPandas就会保留所有True所在的行。这个过程非常重要理解了它你就理解了Pandas所有条件筛选的本质。多个条件可以用且和|或来组合注意每个条件都要用括号括起来df[(df[年龄] 25) (df[城市] 北京)]不加括号的话Python的运算符优先级会导致你得到莫名其妙的结果这是我见过最多新手犯的错误之一。还有一个常用的方法是isin()专门用来筛选某列的值是否在一个集合里df[df[城市].isin([北京, 上海, 深圳])]4.3 loc和iloc标签索引与位置索引的分工当筛选逻辑变复杂时我会用loc和iloc这两个方法它们是所有Pandas用户的必修课。loc是按标签行列的名称来取数df.loc[0, 姓名] # 第0行、姓名列 df.loc[0:2, [姓名, 年龄]] # 第0行到第2行注意包含第2行姓名和年龄两列iloc是按位置行列的序号来取数df.iloc[0, 0] # 第一行第一列 df.iloc[0:2, 0:2] # 前两行前两列注意这里的切片不包含末尾这里有个非常容易搞混的坑loc的切片包含末尾iloc的切片不包含末尾。这个差异是无数bug的来源我自己也在上面吃过亏。我的记忆方法是loc是按名字找人说要找2号到5号那包含2号和5号iloc是按位置数人头数到第5个才停不包含第5个。在实际数据分析中loc的高阶用法尤其好用。你可以直接对一个布尔条件用loc来做选择df.loc[df[年龄] 25, [姓名, 月收入]]这句话意思是筛选年龄大于25的行同时只取姓名和月收入两列一步到位。4.4 条件组合、query方法与字符串筛选除了上面的方法还有两个方法我经常用。query()让我能用字符串表达式的形式来写筛选条件代码看上去清爽很多df.query(年龄 25 and 城市 北京)它的好处是解决了多层括号地狱的问题对列的引用不需要重复写df。但是要注意query里直接使用字符串字符串要加引号如果字符串本身包含引号需要小心嵌套转义。总体来说熟练之后用query确实很高效适合在Notebook里快速探索数据。字符串筛选则用str.contains()这个适合搜索包含某关键字的行比如df[df[姓名].str.contains(张)]这是一个包含而不是完全匹配的逻辑非常有用。值得注意的是如果列中存在NaNstr.contains()默认会返回False而不是报错但如果你设置了naTrue则缺失值所在行也会被保留。4.5 硬核提醒链式操作与SettingWithCopyWarning有一个问题我不止一次遇到过也在网上帮别人解答过无数次。当你要在一个筛选后的子集上做修改时经常会遇到SettingWithCopyWarning这个警告。比如df[df[年龄] 25][备注] 已验证运行后可能会看到警告信息。这个警告的本质是你可能在Pandas的内部副本上修改数据而原始数据并没有被改变。这种你以为改了其实没改的情况是非常危险的数据分析事故。正确做法是使用.loc进行链式操作df.loc[df[年龄] 25, 备注] 已验证这个问题的根因是链式索引链式索引先筛选出一个视图然后再在这个视图上赋值。我养成的铁律是永不使用链式赋值永远用.loc。虽然有些情况下链式赋值碰巧能工作但迟早会给你埋雷。4.6 实战演练从销售表里找出重点客户我们用一个综合的小例子把这几个方法串起来。假设我有一份客户购买记录表字段包括客户ID、客户姓名、城市、总消费金额、购买次数、最近购买日期。现在我要找出总消费超过5000且购买次数大于3次的客户并按总消费降序排列# 第一步先用布尔筛选锁定目标 high_value df[(df[总消费金额] 5000) (df[购买次数] 3)] # 第二步只需要几个关键列 high_value high_value.loc[:, [客户ID, 客户姓名, 总消费金额, 购买次数]] # 第三步排序 high_value high_value.sort_values(总消费金额, ascendingFalse) # 第四步重置索引避免排序后索引乱掉 high_value high_value.reset_index(dropTrue)这里最后一步的reset_index(dropTrue)是我个人的习惯目的很简单排序后原有的索引顺序已经乱套重置索引能让后续操作更干净dropTrue表示不保留原来的索引列。5. 数据的增删改DataFrame的日常维护操作5.1 新增列直接赋值与运算生成新增列最直接的方式就是给它赋值df[年均收入] df[月收入] * 12这句话做了什么它从已有的月收入列计算出年均收入然后作为新列加入到DataFrame末尾并且原df直接就多了一列。你也可以一次性创建多个新列但通常我还是分步写修改起来容易排查。从其他列衍生新列是数据分析中最常见的操作之一。另外还有一个apply方法也能创建新列它让你可以对每一行或每一列应用一个函数df[收入等级] df[月收入].apply(lambda x: 高 if x 12000 else 中低)apply的灵活性很强但它的性能也比较慢。如果数据量大几百万行以上我一般会用NumPy的wheredf[收入等级] np.where(df[月收入] 12000, 高, 中低)这样做的执行速度快很多。5.2 修改列数据replace与条件赋值修改某列的值本质上就是给那个位置重新赋值。比如把所有城市里的北京改为帝都df[城市] df[城市].replace(北京, 帝都)如果你需要把多个值替换为一个新值可以用字典df[城市] df[城市].replace({北京: 帝都, 上海: 魔都})对于某条件下把某列改成某个值这种常见操作还是回到那个铁律——用locdf.loc[df[年龄] 30, 备注] 中年用户注意df[城市].replace()返回的是一个新的Series你需要把它赋回原列才会生效。这和我们之前说到的inplace本质上是一个道理。5.3 删除行和列drop的两种方向用drop删除行和列需要指定axis方向或者直接使用参数。不推荐使用axis这样的方式因为axis到底哪个是0哪个是1我每次都要想一下其实0是行1是列但总是记混。更推荐明确用参数来删除列df df.drop(columns[月收入]) df df.drop(index[0, 1]) # 删除前两行如果你确定要在原数据上修改也可以加inplaceTrue但我之前说过我通常选择重新赋值给新变量。这样原数据还在内存里哪天删错了还能恢复。删除重复行也很常用df df.drop_duplicates(subset[客户ID], keepfirst)这个操作的用途很广比如客户数据可能有重复记录要去重保留第一次出现的记录。subset参数指定判断重复的列keepfirst表示保留第一条。5.4 排序、重置索引与rename列名排序有两种思路按值排序和按索引排序。df_sorted df.sort_values(by[年龄, 月收入], ascending[True, False])这个写法可以对多列排序年龄升序、月收入降序。一个细节是排序后索引会跟着行一起走所以排序后如果要进一步处理最好reset_index。重命名列名可以使用rename方法它接受一个字典映射df df.rename(columns{姓名: 客户姓名, 月收入: 月度收入})这个操作常用于清洗阶段把不统一的列名整理成规范形式。5.5 修改列顺序从混乱到整齐的组合拳有时候列的顺序和你的分析思路不一致调整顺序的方法是重新按列表取列df df[[客户ID, 客户姓名, 城市, 月度收入, 年均收入]]这其实利用了取多列时传列表的特性。如果列很多你要将某些列放到前面可以这样把列名排个序cols [城市] [c for c in df.columns if c ! 城市] df df[cols]6. 缺失值处理数据清洗里最绕不开的环节6.1 先发现缺失isna、notna与缺失统计现实中几乎没有一份数据是干干净净的。缺失值处理是整个数据处理流程中占比最高的一环也是最容易出错的一环。第一步永远是发现缺失。用isna()可以返回一个布尔DataFrame表示每个元素是否为缺失值df.isna().sum() # 按列汇总缺失值数量这个输出是一列非常直观的报告比如月收入列有15个缺失值城市列有2个缺失值。我用它来决定后续策略。如果你想看具体哪些位置缺失可以用布尔索引定位df[df[月收入].isna()]6.2 三种处理策略删除、填充、插值处理缺失值没有一招鲜的方法完全看业务场景。我把常见策略总结为三类策略一删除缺失行。df_drop df.dropna()这个方法会把任何包含NaN的行都删掉。听起来省事但风险很大——如果某一行本来只有一个小单元格缺失你删掉整行就会损失大量有效信息。所以我一般只在缺失值占比极低时比如低于1%才用dropna而且更好的是使用subset参数限定判断范围df_drop df.dropna(subset[关键字段])策略二填充。最常用的是fillna方法。可以填充固定值比如客户类别缺失时填未知df[客户类别] df[客户类别].fillna(未知)可以填充数值列的均值或中位数这是处理数值型缺失的一个很通用的做法df[月收入] df[月收入].fillna(df[月收入].median())但这里有个细节用整列均值可能会受极端值影响所以一般情况下用中位数比均值更稳健。对于时间序列数据还可以用前向填充用前一个值填充df[价格] df[价格].ffill()策略三插值。Pandas的interpolate方法可以用线性插值填充缺失值特别适合时间序列或趋势明显的连续数据df[温度] df[温度].interpolate()这个方法的原理是通过已知点来估计缺失点的值比盲目填充均值要聪明得多。处理平滑数据的缺失用interpolate会非常顺手。6.3 缺失值处理顺序什么时候处理、按什么顺序处理根据我的经验处理缺失值得排在所有分析动作之前。如果先做了筛选、合并、分组再回头处理缺失值整个操作流程会变得极其混乱。具体顺序我建议先了解整体head、info、isna → 处理重复值和格式问题 → 处理缺失值 → 再进入筛选、分组和聚合阶段。还有一个好习惯在处理缺失值时新的数据copy一份出来再操作。把原始数据备份成一个新变量这样任何处理出问题你可以对比和回滚df_clean df.copy()6.4 缺失值处理实战客户数据清洗示例假设我的某份客户数据长这样有缺失、有重复我做三步处理# 1. 删除重复记录保留第一条 df_clean df.drop_duplicates(subset客户ID) # 2. 城市为空的填未知 df_clean[城市] df_clean[城市].fillna(未知) # 3. 月收入为空的中位数填充 median_income df_clean[月收入].median() df_clean[月收入] df_clean[月收入].fillna(median_income)这三步完成之后我再仔细查看df_clean.info()来确认没有缺失值了基本可以进入下一步分析。7. 数据的合并与连接把多张表拼在一起的艺术7.1 concat按行或按列的堆叠当你有多个结构相同的数据集比如一个月的多份分表最常用的是pd.concat。它默认按行堆叠df_all pd.concat([df_1月, df_2月, df_3月], ignore_indexTrue)ignore_indexTrue的意思是忽略原来各表自己的索引重新从0开始编号。如果不设置这个参数你会发现合并后索引是乱的需要再reset一次。如果你想按列堆叠比如给同一批样本增加更多特征需要设置axis1df_combined pd.concat([df_features1, df_features2], axis1)7.2 merge像SQL JOIN一样关联两张表concat只是上下叠或左右拼真正的表关联要用merge。这个方法让我在不需要写SQL的情况下也能完成数据库风格的JOIN。最简单的内连接inner join取两张表的交集df_orders pd.read_csv(orders.csv) df_users pd.read_csv(users.csv) df_result pd.merge(df_orders, df_users, left_onuser_id, right_onid)left_on和right_on指定左边表和右边表各自的关联键。这在两个表关联键名称不同时非常有用如果两边键名一样直接写on就行df_result pd.merge(df_orders, df_users, onuser_id)merge的how参数决定连接方式不仅支持inner还支持left、right和outer。我用一个生活化的类比来解释这几种连接的区别。假设左边是本月有订单的用户右边是本月登录过的用户。inner join的结果是既下单又登录的用户left join则把所有下单用户全保留登录信息有没有都无所谓没有的就填空right join反过来outer join是把两种用户都保留空的地方填NaN。在实际处理中left join是最常用的。比如给订单表补上用户信息订单一定是主体信息能匹配就匹配匹配不上就置空但订单记录不能丢。7.3 常见错误关联键的重复与索引对齐的陷阱merge的坑也不少。最常见的是关联键在两边都出现了多行比如用客户ID关联但左边订单表里每个客户可能有多条订单右边用户表里一个客户也可能有多条记录因为数据没去重。这时inner join会产生笛卡尔积行数会暴增数据量瞬间爆炸。另一个常见错误是忘记重置索引就直接拼接。如果两张表都有意义的索引拼接时它们会做索引对齐结果可能产生大量NaN。这个细节花了评论区很多人很长时间才排查清楚。7.4 实战订单表和用户表的关联我们来做一个完整的模拟场景。订单表orders有三列订单ID、用户ID、金额。用户表users有三列用户ID、姓名、城市。现在要把用户信息加到订单表# 确保用户ID格式一致这个细节常被忽视 orders[用户ID] orders[用户ID].astype(str).str.strip() users[用户ID] users[用户ID].astype(str).str.strip() # 关联 df_merged pd.merge(orders, users, on用户ID, howleft)这里的astype(str).str.strip()是我经常会加的防护性代码——两个表里同一个ID很有可能一个是整数、一个是字符串或者一个带前后空格、一个不带。不做这步处理就会导致大量匹配失败结果看起来很随机。这个坑我在实际项目中踩过不止一回。8. 分组、聚合与数据透视数据分析里的高频动作8.1 groupby按维度拆开再分别处理如果说DataFrame是一张表groupby就是按照某个字段或多个字段把表拆成若干小组然后对每个小组做统计。这是数据分析的灵魂操作。最简单的用法是分组后算均值df.groupby(城市)[月收入].mean()这句话的意思是按城市分组对每个组内的月收入列求平均。输出是一个Series索引是城市名值是平均收入。如果你希望输出是DataFrame可以在groupby后加reset_indexdf_grouped df.groupby(城市)[月收入].mean().reset_index()这个习惯我非常推荐尤其是后续还想继续用列名访问时。多列分组也更常见df.groupby([城市, 性别])[月收入].mean()这会输出一个两层索引的分组统计结果。两层索引虽然信息丰富但不太直观。只要加上reset_index或使用as_indexFalse参数它就会变成一张城市、性别、平均收入三列的规整表格。8.2 agg同时算多个统计量如果我想一次算出多个指标比如每个城市的收入均值、最大值和人数result df.groupby(城市).agg({ 月收入: [mean, max], 姓名: count })这样输出就会有多层列索引看起来比较复杂。为了让结果更易用我通常会在最后reset_index并把列名重新改好。agg方法非常灵活你甚至可以对不同列应用不同的聚合函数比如一列算sum、一列算mean。这是整个Pandas里最强大的功能之一。8.3 pivot_tableExcel透视表的Pandas版如果你以前用过Excel透视表那么pivot_table会让你有一种熟悉感。它把行维度、列维度和值分开写代码更直观pivot pd.pivot_table( df, index城市, # 行维度 columns性别, # 列维度 values月收入, # 要统计的值 aggfuncmean # 聚合方式 )结果就是一张行是城市、列是性别、值是平均收入的矩阵。这种方式特别适合做报表和环比对比。pivot_table里面还有一个参数fill_value0可以把空值直接填成0这在汇报PR报告的时候很有用。还有一个参数marginsTrue可以帮你自动加一行总计类似Excel的行总计列总计在做季度汇总时特别方便。8.4 分组后的复杂操作transform和自定义函数有时候我不只想得到每个组的汇总值还想把汇总值广播回每一行。比如我想标记每个城市收入最高的人df[城市最高收入] df.groupby(城市)[月收入].transform(max)transform的作用就是对每个组计算统计值然后把这个值放回组内每一行。对比每个人自己的月收入你就能快速判断他是不是该城市收入最高的人。transform配合agg的任意函数组合能处理的分组场景非常多。如果你需要更自由的逻辑agg里还可以传入自定义函数def range_func(x): return x.max() - x.min() df.groupby(城市)[月收入].agg(range_func)这种方法灵活但性能会稍慢数据量大的时候要谨慎。9. 时间序列数据处理让时间字段真正成为时间9.1 字符串转时间to_datetime是标准配置在真实数据集里时间字段常常是一串字符串比如2024-03-15 14:30:00。如果不把它转成真正的时间类型Pandas就不知道这是时间你做不了任何时间相关运算。标准转换方式df[下单时间] pd.to_datetime(df[下单时间])pd.to_datetime非常智能能自动识别2024/03/15、2024-03-15、15/03/2024 14:30等多种格式。但你的数据如果格式特殊比如2024.03.15可能需要加format参数明确告诉它格式否则可能解析失败或解析错误。9.2 用时间做索引按年、月、日聚合转成时间类型后我最常做的是把它设为索引然后用resample做时间维度聚合df[下单时间] pd.to_datetime(df[下单时间]) df df.set_index(下单时间) monthly_sales df[销售额].resample(M).sum()resample(M)的意思是按月汇总。常用的频率参数还有D按天、W按周、Q按季度、Y按年。这个功能在处理日志、订单、传感器数据时非常刚需。9.3 时间字段拆解与高级操作有时候你需要把时间拆成年、月、日、星期几方便做多维分析df[年份] df[下单时间].dt.year df[月份] df[下单时间].dt.month df[星期] df[下单时间].dt.dayofweek # 0星期一 df[小时] df[下单时间].dt.hour注意这里用到了.dt这个访问器它是专门用于时间类型Series的只有当你把列转成datetime类型后才能使用dt.year等属性。如果不转直接调用会报错。时间序列的滑动窗口也是分析的重要工具比如计算7天滚动平均值df[7日均值] df[销售额].rolling(window7).mean()rolling的应用面很广不管是做趋势平滑、还是计算MA技术指标它的思路都是按固定窗口滑过去对窗口内数据算统计值。10. 常见问题与排查技巧实录10.1 读CSV出现乱码怎么办中文乱码是读取CSV时最常见的状况。我的排查顺序是先看文件是什么编码的。用记事本打开另存为时能看到编码选项。绝大多数默认是utf-8但偶尔会遇到gbk或者gb2312。解决办法就是指定编码df pd.read_csv(文件.csv, encodinggbk)如果你的文件是gbk编码用utf-8读就会报UnicodeDecodeError。反过来用gbk读utf-8的文件也可能出乱码。更进阶的方案是用encodingutf-8-sig来兼容BOM头带BOM的UTF-8文件这个细节能快速解决你遇到的奇怪问题。10.2 loc和iloc切片结果不一致我之前提到过loc[:, 0:3]包含第3列而iloc[:, 0:3]不含第3列。无数人在此吃亏。如果你想要前3列那应该用iloc[:, :3]位置索引0、1、2。如果你想要从A列到C列那就用loc[:, A:C]标签索引包含C列。这两个方法的设计逻辑大不相同使用前要想清楚你要的到底是位置还是标签。10.3 groupby之后怎么继续访问列groupby的结果是GroupBy对象它不是DataFrame。很多人第一次用会困惑df.groupby(城市)没反应这就对了因为它还没有真正做计算。你需要在后面跟上聚合方法比如.mean()、.sum()、.agg()才会真正产生结果。如果你希望结果直接变成DataFrame可以在groupby里加参数df.groupby(城市, as_indexFalse)[月收入].mean()10.4 SettingWithCopyWarning的终极解法前面说过链式索引的问题。如果你不想看到这个警告同时确保操作安全我的方法是所有需要修改子集的操作都用.loc显式完成。df.loc[df[年龄] 30, 标签] X还有一个备选做法是显式copysubset df[df[年龄] 30].copy()copy之后你可以放心修改subset完全不担心原df被意外改动。这个习惯在做复杂数据处理时非常省心。10.5 大DataFrame内存爆掉怎么办如果数据量非常大几千万行Pandas很可能会把内存吃满。我通常的应对方法是一用dtype参数指定数据类型把可以降级的列降到更小类型。比如整数从int64降到int32甚至int8内存可以减小一半以上。二分批读取用chunksize参数reader pd.read_csv(big_file.csv, chunksize100000) for chunk in reader: process(chunk)三只用需要的列用usecols参数让Pandas不要加载无关列。三个方法组合使用基本能应对大多数内存不足的问题。11. 数据输出的几种方式结果怎么保存和展示11.1 to_csv保存成文件处理完数据最终要落盘保存。最常见的输出是CSVdf.to_csv(result.csv, indexFalse, encodingutf-8-sig)这里有两个参数非常关键。indexFalse是防止把行索引也写进CSV这个值默认是True很多人都被默认值坑过——多出一列莫名其妙的无名列。encodingutf-8-sig是为了让Excel打开CSV时不出现中文乱码这个细节是我在给别人交付文件时反复踩坑总结出来的。11.2 to_excel输出Excel报表如果业务方需要用Excel打开可以用to_exceldf.to_excel(result.xlsx, sheet_name数据汇总, indexFalse)打开Excel工作簿时如果表很多还可以配合ExcelWriter把多张DataFrame写到同一个工作簿的不同sheetwith pd.ExcelWriter(多表结果.xlsx) as writer: df1.to_excel(writer, sheet_name订单, indexFalse) df2.to_excel(writer, sheet_name用户, indexFalse)11.3 在Jupyter Notebook里直接展示最后一个输出方式其实大家每天都在用——在Notebook里直接输入df单元格下方会把表格渲染得非常美观。配合style可以给表格做简单格式化比如高亮最大值、设定背景色等。不过这一层更多是锦上添花核心还是DataFrame本身的操作能力。12. 从基础到习惯我个人的几个铁律说到最后我想分享的不是更多的函数而是我用DataFrame这几年形成的几个铁律。这些习惯让我在写长代码、维护复杂脚本时明显少踩很多坑。第一个习惯是永不直接修改原始数据。哪怕是一个简单的fillna我也会先df.copy()一下再操作。这样做的代价很小但收益巨大——任何一步改错了你都可以回到原点重来。尤其在调试阶段这个习惯能省下大把时间。第二个习惯是“每处理一步都要回头看一眼结果”。不要一口气写完十行数据处理代码再执行那样一旦出错你根本不知道是哪一步造成的。正确的做法是每执行一个操作马上用head()或者shape看一眼验证结果是否符合预期。第三个习惯是写很清晰的代码注释。数据处理的代码通常是一连串转换逻辑隔一个月回头看如果没注释你大概率已经忘了当初为什么这么做。我一般每三步操作写一行注释说明这一步的目的和预期输出。第四个习惯是对不确定的API先查版本再看用法。Pandas更新得比较频繁不同版本之间接口可能发生变化。遇到不确定的方法先看版本docstrings再动手比你盲猜要靠谱得多。DataSet本身并不难它的难在于量大面广函数多得让人眼花缭乱。但只要你掌握了核心的思维模式——有标签的二维表格、基于标签的索引、分组聚合、布尔筛选、缺失值处理——你就能应付绝大多数实际任务。其他记不住的具体函数随时查文档、查网上的示例就行真正的硬功夫就是把上面这几个核心概念练得滚瓜烂熟。