pandas扩展:apply自定义函数、分组进阶(五大核心)、透视表
apply自定义函数
apply()
函数是 Pandas 中非常强大和常用的功能,它的存在解决了数据分析中的一个核心需求:灵活地对数据应用自定义操作。
为什么需要 apply()
函数?
- 超越内置函数的限制:Pandas 虽然提供了很多内置方法(如
sum()
,mean()
等),但无法覆盖所有可能的计算需求。 - 处理复杂逻辑:有些操作需要复杂的条件判断或多步计算,无法用简单的向量化操作完成。
- 代码复用:可以将复杂的操作封装成函数,然后通过
apply()
应用到数据上。
apply()
的主要作用
apply()
允许你将任意函数应用到:
- DataFrame 的每一行或每一列
- Series 的每一个元素
基本语法
# 对 DataFrame
df.apply(function, axis=0) # axis=0: 对每列应用函数
df.apply(function, axis=1) # axis=1: 对每行应用函数# 对 Series
series.apply(function) # 对每个元素应用函数
实例
1. 对 Series 使用 apply()
(元素级操作)
import pandas as pd# 创建示例数据
df = pd.DataFrame({'name': ['Alice', 'Bob', 'Charlie'],'score': [85, 92, 78],'subject': ['math', 'English', 'MATH']
})# 示例1:字符串处理 - 统一大小写
df['subject'] = df['subject'].apply(str.upper)
print(df['subject'])# 示例2:数值转换 - 分数转等级
def score_to_grade(score):if score >= 90:return 'A'elif score >= 80:return 'B'elif score >= 70:return 'C'else:return 'D'df['grade'] = df['score'].apply(score_to_grade)
print(df[['score', 'grade']])
2. 对 DataFrame 使用 apply()
(行或列级操作)
# 示例3:基于多列计算新值 - 计算综合评分
def calculate_final_score(row):base_score = row['score']# 根据科目给不同权重if row['subject'] == 'MATH':return base_score * 1.1else:return base_score * 1.05df['final_score'] = df.apply(calculate_final_score, axis=1)
print(df[['score', 'subject', 'final_score']])
3. 实际应用场景
# 示例4:数据清洗 - 处理异常值
def clean_age(age):if age < 0:return 0elif age > 150:return 150else:return ageages = pd.Series([-5, 25, 200, 30])
clean_ages = ages.apply(clean_age)
print(clean_ages)
# 示例5:日期处理
import datetime# 假设我们有天数数据,想转换成年龄段
def days_to_category(days):years = days / 365.25if years < 18:return '未成年'elif years < 60:return '成年人'else:return '老年人'days_series = pd.Series([365*16, 365*30, 365*70])
age_categories = days_series.apply(days_to_category)
print(age_categories)
4. 使用 lambda 函数(简洁写法)
# 快速实现简单逻辑
df['score_squared'] = df['score'].apply(lambda x: x**2)
df['is_high_score'] = df['score'].apply(lambda x: '是' if x >= 85 else '否')# 对多列操作
df['info'] = df.apply(lambda row: f"{row['name']}的{row['subject']}成绩是{row['score']}", axis=1)
apply()
的优势
- 灵活性:可以应用任何 Python 函数
- 可读性:代码逻辑清晰
- 复用性:函数可以多次调用
- 简化复杂操作:将复杂逻辑封装起来
注意事项
- 对于简单的数学运算,直接使用向量化操作更快
apply()
在处理大数据时可能较慢,因为它本质上是循环- 尽量使用向量化操作替代
apply()
以获得更好的性能
向量化后面会提到
lambda
当函数比较简单的时候, 没有必要创建一个def 一个函数, 可以使用lambda表达式创建匿名函数
向量化函数
向量化函数是指能够一次性对整个数组或数据序列的所有元素进行操作的函数,而不是逐个元素循环处理。
与普通循环的区别
import pandas as pd
import numpy as np# 创建示例数据
data = pd.Series([1, 2, 3, 4, 5])# 普通循环方式(低效)
result_loop = []
for x in data:result_loop.append(x ** 2)# 向量化方式(高效)
result_vectorized = data ** 2 # 或 data.pow(2)
Pandas 中的向量化函数类型
1. 数学运算(自动向量化)
s = pd.Series([1, 2, 3, 4, 5])# 基本运算
s + 10 # 所有元素加10
s * 2 # 所有元素乘以2
s ** 2 # 所有元素平方# 三角函数
np.sin(s) # 所有元素求正弦
np.log(s) # 所有元素求对数
2. 统计函数
s = pd.Series([1, 2, 3, 4, 5])# 这些都是向量化的聚合函数
s.sum() # 求和
s.mean() # 平均值
s.std() # 标准差
s.max() # 最大值
s.min() # 最小值
3. 字符串向量化操作
names = pd.Series([' Alice', 'BOB ', 'Charlie '])# 字符串方法都是向量化的
names.str.strip() # 去除首尾空格
names.str.lower() # 转小写
names.str.upper() # 转大写
names.str.len() # 计算字符串长度
names.str.contains('a') # 检查是否包含'a'
4. 条件向量化操作
scores = pd.Series([85, 92, 78, 95, 67])# 使用 np.where 进行向量化条件判断
grades = np.where(scores >= 90, 'A', np.where(scores >= 80, 'B', 'C'))# 使用 pandas 的 mask
pass_fail = scores.where(scores >= 70, 'Fail')
自定义向量化函数(注意:这不是真正的向量化)
1. 使用 np.vectorize
import numpy as np# 定义普通函数
def custom_func(x):if x > 80:return x * 1.1else:return x * 0.9# 向量化这个函数
vectorized_func = np.vectorize(custom_func)# 应用到整个 Series
scores = pd.Series([85, 92, 78, 95, 67])
result = vectorized_func(scores)
2. 使用 pd.Series.apply
# 注意:apply 本质上是循环,不是真正的向量化
result = scores.apply(custom_func) # 较慢
向量化的优势
1. 性能优势
import time
import numpy as np# 大数据集测试
large_data = pd.Series(np.random.randn(1000000))# 向量化方式
start = time.time()
result1 = large_data ** 2
vectorized_time = time.time() - start# 循环方式(非常慢,仅作对比)
start = time.time()
result2 = [x**2 for x in large_data]
loop_time = time.time() - startprint(f"向量化: {vectorized_time:.4f}秒")
print(f"循环: {loop_time:.4f}秒")
# 向量化通常快10-100倍
2. 代码简洁性
# 向量化:一行代码
normalized = (data - data.mean()) / data.std()# 循环:多行代码
mean_val = data.mean()
std_val = data.std()
normalized = []
for x in data:normalized.append((x - mean_val) / std_val)
常见的向量化函数示例
# 1. 数据标准化
data = pd.Series([10, 20, 30, 40])
normalized = (data - data.min()) / (data.max() - data.min())# 2. 条件赋值
scores = pd.Series([85, 92, 78, 95])
# 向量化条件
level = np.where(scores >= 90, '高级', np.where(scores >= 80, '中级', '初级'))# 3. 时间序列处理
dates = pd.date_range('2023-01-01', periods=5)
# 向量化提取日期信息
years = dates.year
months = dates.month
days = dates.day# 4. 缺失值处理
data_with_nan = pd.Series([1, 2, np.nan, 4, 5])
# 向量化填充
filled = data_with_nan.fillna(0)
# 向量化检测
is_null = data_with_nan.isna()
向量化 vs apply vs vectorize
特性/方法 | 真正向量化操作 | pd.Series.apply | numpy.vectorize |
---|---|---|---|
实现方式 | 底层C/C++实现,同时处理整个数组 | 逐元素应用函数的Python循环 | 将标量函数包装成可处理数组的函数 |
性能 | 最高 | 中等 | 略高于apply |
返回类型 | 保持原类型(pd.Series/np.ndarray) | pd.Series(保持索引) | np.ndarray |
适用场景 | 简单数学运算、NumPy函数 | 数据清洗、复杂逻辑处理 | 复杂自定义函数、多数组操作 |
代码示例 | s ** 2 np.sqrt(s) | s.apply(lambda x: x**2) | @np.vectorize def func(x): return x**2 |
保持pandas特性 | 是(如果是pandas对象) | 是(索引、标签等) | 否(返回numpy数组) |
多参数支持 | 有限 | 通过额外参数 | 是(多个数组参数) |
推荐使用优先级 | 1(首选) | 3(需要pandas特性时) | 2(复杂函数时) |
总结:
- 简单运算优先使用向量化操作
- 复杂逻辑才使用
apply
- 尽量避免 Python 循环
您总结得非常好!这确实是 Pandas 分组操作(GroupBy)的五大核心类型。您已经抓住了核心框架,我来为您系统性地补充、完善和深化这些概念,帮助您彻底理解 groupby
的完整逻辑和应用场景。
Pandas 分组操作五大核心类型
示例数据:
import pandas as pd
import numpy as npdf = pd.DataFrame({'地区': ['北京', '上海', '北京', '上海', '广州', '广州'],'产品': ['A', 'B', 'A', 'B', 'A', 'B'],'销售额': [100, 150, 200, 180, 120, 90],'数量': [10, 15, 20, 12, 10, 6],'日期': pd.to_datetime(['2023-01-01', '2023-01-02', '2023-02-01', '2023-02-02', '2023-01-05', '2023-02-10'])
})
print(df)
一、分组(Split)—— groupby()
核心作用:
将一个 DataFrame 拆分成多个子组(sub-groups),每个子组是一个“小表格”,后续操作在这些小组上进行。
本质:
groupby
返回的是一个 DataFrameGroupBy
对象,它还没有计算,只是“承诺”了分组方式。
grouped = df.groupby('地区')
print(type(grouped)) # <class 'pandas.core.groupby.generic.DataFrameGroupBy'>
分组方式详解:
分组方式 | 示例 | 说明 |
---|---|---|
单列分组 | df.groupby('地区') | 最常见 |
多列分组 | df.groupby(['地区', '产品']) | 生成多级索引 |
按函数分组 | df.groupby(df['日期'].dt.month) | 按月份分组 |
按索引分组 | df.groupby(level=0) | 按行索引分组 |
自定义规则 | df.groupby(df['销售额'] > 150) | 布尔条件分组 |
技巧:可以用
grouped.groups
查看每个组包含哪些行索引。
二、聚合(Aggregate)—— agg()
/ aggregate()
核心作用:
对每个分组计算一个汇总值,结果是一个更小的 DataFrame 或 Series(行数 ≤ 原始数据)。
特点:
- 每个组 → 一个值
- 降维操作
常用聚合函数:
# 单函数聚合
df.groupby('地区')['销售额'].sum() # 每个地区的总销售额# 多函数聚合
df.groupby('地区')['销售额'].agg(['sum', 'mean', 'count'])# 对不同列使用不同函数
df.groupby('地区').agg({'销售额': ['sum', 'mean'],'数量': 'sum'
})
自定义聚合函数:
def range_func(x):return x.max() - x.min()df.groupby('地区')['销售额'].agg(range_func)
注意:
agg
可以接受函数名字符串、函数对象、列表、字典,非常灵活!
三、转换(Transform)—— transform()
核心作用:
对每个分组进行计算,但返回结果的形状与原始数据一致(行数不变),用于组内标准化、填充、打分等。
本质:
- 每个组 → 一组值(长度 = 该组行数)
- 不降维,保持原始结构
经典应用场景:
# 1. 组内标准化(Z-score)
df['销售额_标准化'] = df.groupby('地区')['销售额'].transform(lambda x: (x - x.mean()) / x.std()
)# 2. 填充组内缺失值(用组均值)
df['销售额_填充'] = df.groupby('地区')['销售额'].transform(lambda x: x.fillna(x.mean())
)# 3. 计算组内排名
df['销售额_组内排名'] = df.groupby('地区')['销售额'].transform('rank'
)# 4. 创建新特征:组均值作为新列
df['地区平均销售额'] = df.groupby('地区')['销售额'].transform('mean')
transform
是特征工程的利器!
四、过滤(Filter)—— filter()
核心作用:
根据组的整体属性,保留或丢弃某些组,而不是对单行过滤。
与 query
/ boolean indexing
的区别:
df[df['销售额'] > 100]
:按行条件过滤df.groupby('地区').filter(lambda x: x['销售额'].sum() > 250)
:按组条件过滤
实例:
# 保留总销售额 > 250 的地区
high_sales_regions = df.groupby('地区').filter(lambda group: group['销售额'].sum() > 250
)# 保留记录数 >= 2 的组
df.groupby('地区').filter(lambda x: len(x) >= 2)# 保留组内方差较大的组
df.groupby('地区').filter(lambda x: x['销售额'].std() > 50)
filter
返回的是原始数据的子集,不是统计结果。
五、应用(Apply)—— apply()
核心作用:
最灵活、最强大的操作,可以对每个分组的 完整 DataFrame 进行任意操作。
与 agg
和 transform
的区别:
方法 | 输入 | 输出要求 | 返回形状 |
---|---|---|---|
agg | Series/数值 | 标量 | 降维 |
transform | Series | 同长度Series | 不降维 |
apply | DataFrame | 任意 | 灵活 |
应用场景:
# 1. 组内排序
df.groupby('地区').apply(lambda group: group.sort_values('销售额', ascending=False)
)# 2. 返回复杂结构(如字典)
df.groupby('地区').apply(lambda g: {'总销售额': g['销售额'].sum(), '平均数量': g['数量'].mean()}
)# 3. 组内线性回归(示例)
from scipy.stats import linregress
def fit_trend(group):slope, _, _, _, _ = linregress(group.index, group['销售额'])return slope# 4. 返回 DataFrame(多行)
df.groupby('地区').apply(lambda g: g[['销售额', '数量']].assign(占比=lambda x: x['销售额']/x['销售额'].sum())
)
注意:
apply
可能较慢,尽量用agg
或transform
替代。
五大操作对比
类型 | 方法 | 输入单元 | 输出形状 | 典型用途 |
---|---|---|---|---|
分组 | groupby() | 列/函数/索引 | GroupBy对象 | 拆分数据 |
聚合 | agg() | 每组的列(Series) | 降维(1值/组) | 统计汇总 |
转换 | transform() | 每组的列(Series) | 同原始行数 | 组内标准化、填充 |
过滤 | filter() | 整个组(DataFrame) | 行子集 | 筛选重要组 |
应用 | apply() | 整个组(DataFrame) | 任意 | 复杂自定义逻辑 |
总结
- 优先使用
agg
和transform
:它们性能好,语义清晰。 transform
是特征工程的核心:学会用它创建“组内特征”。filter
用于数据预筛选:比如只分析大客户、活跃地区。apply
是最后的选择:当其他方法无法满足时再用。
实战示例
# 目标:分析各地区各产品的表现,并标准化销售额
result = (df.groupby(['地区', '产品']).apply(lambda g: g.assign(地区产品平均 = g['销售额'].mean(),标准化销售额 = (g['销售额'] - g['销售额'].mean()) / g['销售额'].std())).reset_index(drop=True)
)
数据透视表
数据透视表是一种交叉汇总表,它可以:
- 按一个或多个字段分组
- 对数值进行聚合计算(如求和、平均、计数等)
- 将结果以二维表格的形式展示(行 × 列)
核心函数:pd.pivot_table()
基本语法:
pd.pivot_table(data, # DataFrameindex, # 行索引(分组依据)columns, # 列(用于展开的分类)values, # 要聚合的数值列aggfunc, # 聚合函数(如 'sum', 'mean' 等)fill_value, # 填充缺失值margins # 是否添加总计行/列
)
实例
示例数据:
import pandas as pd
import numpy as npdf = pd.DataFrame({'地区': ['北京', '上海', '北京', '上海', '广州', '广州', '北京', '上海'],'产品': ['A', 'B', 'A', 'B', 'A', 'B', 'A', 'B'],'季度': ['Q1', 'Q1', 'Q2', 'Q2', 'Q1', 'Q2', 'Q1', 'Q1'],'销售额': [100, 150, 200, 180, 120, 90, 130, 160],'数量': [10, 15, 20, 12, 10, 6, 13, 16]
})print(df)
示例 1:基本透视表(按地区和产品汇总销售额)
pivot = pd.pivot_table(data=df,index='地区', # 行:按地区分组columns='产品', # 列:按产品展开values='销售额', # 聚合的值aggfunc='sum' # 聚合方式
)print(pivot)
输出:
产品 A B
地区
北京 430 NaN
上海 NaN 490
广州 120 90
说明:北京没有产品 B 的销售,所以是
NaN
。
示例 2:填充缺失值 + 添加总计
pivot = pd.pivot_table(data=df,index='地区',columns='产品',values='销售额',aggfunc='sum',fill_value=0, # 把 NaN 替换为 0margins=True, # 添加总计行和列margins_name='总计'
)print(pivot)
输出:
产品 A B 总计
地区
北京 430 0 430
广州 120 90 210
上海 0 490 490
总计 550 580 1130
示例 3:多级行索引(index 多列)
pivot = pd.pivot_table(data=df,index=['地区', '季度'], # 多级行索引columns='产品',values='销售额',aggfunc='sum',fill_value=0
)print(pivot)
输出:
A B
地区 季度
北京 Q1 230 0Q2 200 0
上海 Q1 0 310Q2 0 180
广州 Q1 120 0Q2 0 90
示例 4:多聚合函数 + 多值列
pivot = pd.pivot_table(data=df,index='地区',columns='产品',values=['销售额', '数量'], # 多个值列aggfunc={'销售额': 'sum', '数量': 'mean'}, # 不同列用不同聚合fill_value=0
)print(pivot)
输出:
销售额 数量
产品 A B A B
地区
北京 430 0 14.3 0.0
广州 120 90 10.0 6.0
上海 0 490 0.0 15.5
示例 5:使用 aggfunc
传入多个函数
pivot = pd.pivot_table(data=df,index='产品',values='销售额',aggfunc=['mean', 'sum', 'count'],margins=True
)print(pivot)
输出:
mean sum count
产品
A 170.0 680 4
B 205.0 820 4
All 187.5 1500 8
pivot_table
vs groupby
特性 | pivot_table | groupby |
---|---|---|
输出形式 | 二维交叉表(行×列) | 一维分组结果 |
易读性 | 高(适合报表) | 中 |
灵活性 | 高(支持行列展开) | 极高(支持 transform/filter 等) |
典型用途 | 报表、可视化前处理 | 数据清洗、特征工程 |
** 总结**:
- 想生成“表格报表” → 用
pivot_table
- 想做“数据处理流水线” → 用
groupby
实际应用场景
- 销售分析:按地区、产品、时间维度汇总销售额
- 用户行为分析:按用户分组统计访问次数、停留时间
- 财务报表:生成月度/季度汇总表
- A/B 测试:对比不同组的转化率、均值
- 数据探索:快速查看分类变量之间的关系
补充
- 如果只想重塑数据而不聚合,用
df.pivot()
(但要求索引唯一) pivot_table
会自动处理重复索引(通过聚合)- 可以结合
plot()
直接可视化:pivot.plot(kind='bar', title='各地区产品销售额')
总结
方法 | 适用场景 |
---|---|
pd.pivot_table() | 生成交叉汇总表、做报表、探索数据关系 |
df.groupby() | 复杂分组操作、特征工程、数据清洗 |