一、为什么说 apply 是数据处理的 “瑞士军刀”?
在 Pandas 的世界里,流传着这样一句话:“如果你觉得数据处理很麻烦,那一定是没用对 apply 函数。”想象一下:你有 10 万条电商订单数据,需要给每个订单计算折扣后的价格;或者有一堆用户评论,需要提取关键词并统计情感倾向;再或者需要按行 / 列对数据进行复杂的逻辑判断……如果用传统的 Python 循环(如 for 循环),代码会像裹脚布一样又长又慢,还容易出错。但 apply 函数就像一个 “数据处理加速器”,用一行代码就能替代几十行循环,让数据处理效率飙升!
二、apply 的底层逻辑:比循环快 10 倍的秘密
先别急着学用法,我们先搞懂 apply 的核心优势:向量化运算。Pandas 的底层是用 C 语言优化的数组运算,而 apply 会将自定义函数 “广播” 到整个数组上,避免了 Python 循环的低效性。举个简单的例子:
TypeScript
取消自动换行复制
# 传统循环:处理10万条数据可能需要几秒
for i in range(len(data)):
data['price'][i] = data['price'][i] * 0.8
# apply方法:瞬间完成
data['price'] = data['price'].apply(lambda x: x * 0.8)
** 为什么差距这么大?** 因为循环每次只能处理一个元素,而 apply 会把整个 Series/DataFrame 作为输入,一次性批量处理,这就是 “向量化” 的魔力!
三、apply 的三种打开方式:从入门到精通
(一)单刀直入:对 Series 元素逐个加工
场景:处理一列数据,比如给所有商品价格打 8 折,或者将字符串统一转成大写。语法:series.apply(func, **kwargs)案例 1:数值计算假设有一个 “原始价格” 列,需要计算 “折后价”(满 100 减 30,否则打 9 折):
TypeScript
取消自动换行复制
import pandas as pd
data = pd.DataFrame({'原价': [150, 80, 200, 90]})
# 定义复杂计算函数
def calculate_discount(price):
if price >= 100:
return price - 30
else:
return price * 0.9
# 对Series应用函数
data['折后价'] = data['原价'].apply(calculate_discount)
print(data)
输出:
原价 | 折后价 |
150 | 120 |
80 | 72 |
200 | 170 |
90 | 81 |
案例 2:字符串处理处理用户评论,提取情感关键词(简化版):
TypeScript
取消自动换行复制
comments = pd.Series([
"这部电影太棒了!特效超震撼",
"剧情无聊,演员演技一般",
"强烈推荐!性价比超高"
])
# 提取关键词
def extract_keywords(text):
if "太棒了" in text or "推荐" in text:
return "正面"
elif "无聊" in text or "一般" in text:
return "负面"
else:
return "中性"
comments.apply(extract_keywords)
输出:0 正面1 负面2 正面dtype: object
(二)横刀立马:对 DataFrame 行 / 列批量操作
场景:需要同时处理多行或多列数据,比如根据 “身高” 和 “体重” 计算 BMI,或按行筛选符合条件的数据。语法:data.apply(func, axis=0/1, raw=False, **kwargs)
- axis=0:按列处理(默认),函数接收每一列作为 Series
- axis=1:按行处理,函数接收每一行作为 Series
- raw=True:传递原始 ndarray 给函数(而非 Series),适合需要 numpy 运算的场景
案例 1:按行计算 BMI
TypeScript
取消自动换行复制
people = pd.DataFrame({
'姓名': ['Alice', 'Bob', 'Charlie'],
'体重(kg)': [55, 70, 80],
'身高(m)': [1.6, 1.75, 1.8]
})
# 定义BMI计算公式(行作为Series传入)
def calculate_bmi(row):
return row['体重(kg)'] / (row['身高(m)'] ** 2)
# 按行应用函数,axis=1表示处理每一行
people['BMI'] = people.apply(calculate_bmi, axis=1)
print(people)
输出:
姓名 | 体重 (kg) | 身高 (m) | BMI |
Alice | 55 | 1.6 | 21.4843 |
Bob | 70 | 1.75 | 22.8571 |
Charlie | 80 | 1.8 | 24.6914 |
案例 2:按列统计缺失值比例
TypeScript
取消自动换行复制
# 模拟含缺失值的数据
df = pd.DataFrame({
'A': [1, 2, np.nan, 4],
'B': [5, np.nan, np.nan, 8],
'C': [9, 10, 11, 12]
})
# 按列计算缺失率(axis=0,每列作为Series传入)
def missing_rate(col):
return col.isnull().mean() * 100 # 缺失比例百分比
df.apply(missing_rate, axis=0)
输出:A 25.0B 50.0C 0.0dtype: float64
(三)倚天屠龙:高阶玩法之复杂数据处理
场景 1:返回多个结果,生成新列有时候函数会返回多个值,需要拆分成多列。比如从日期字符串中提取 “年”、“月”、“日”:
TypeScript
取消自动换行复制
dates = pd.Series(["2023-01-05", "2023-02-18", "2023-03-22"])
# 函数返回字典,键对应新列名
def parse_date(date_str):
year, month, day = date_str.split('-')
return {'年': year, '月': month, '日': day}
# 应用函数后,自动扩展为多列
date_df = dates.apply(parse_date).apply(pd.Series) # 关键:用pd.Series展开字典
print(date_df)
输出:年 月 日0 2023 01 051 2023 02 182 2023 03 22
场景 2:结合 lambda 表达式,一行代码搞定简单逻辑比如给 “价格” 列所有大于 100 的数值标红(配合后续可视化库):
TypeScript
取消自动换行复制
data['原价'].apply(lambda x: f'<span style="color:red">{x}</span>' if x > 100 else x)
四、避坑指南:apply 不是万能药!
虽然 apply 很强大,但也有 “软肋”:
- 性能陷阱:
- 对简单操作(如加减乘除),直接用向量化运算(如data['price'] * 0.8)比 apply 更快
- 大数据量(百万级以上)时,优先用 Pandas 内置函数或 NumPy 数组运算
- axis 参数弄反:
- 记不住axis=0和axis=1?记住:axis=0 是 “纵向”(列),axis=1 是 “横向”(行),就像 Excel 的行和列方向
- 函数返回值类型错误:
- 若函数返回非标量(如列表、字典),需用apply(pd.Series)展开,否则会生成 “对象” 类型列
五、对比魔法三兄弟:apply vs map vs applymap
很多新手会混淆这三个函数,一张表帮你理清:
函数 | 适用对象 | 输入 | 输出 | 典型场景 |
apply | Series/DataFrame | 单个元素 / 列 / 行 | 标量 / Series/DataFrame | 复杂逻辑处理,如行计算、自定义函数 |
map | Series | 单个元素 | 标量 | 简单映射(如字典替换、数值转换) |
applymap | DataFrame | 单个元素 | 标量 | 对每个元素统一处理(如格式化字符串) |
举例对比:
TypeScript
取消自动换行复制
# map:给Series每个元素加前缀
data['姓名'].map(lambda x: '用户-' + x)
# applymap:给DataFrame每个数值取绝对值
data.applymap(lambda x: abs(x) if isinstance(x, (int, float)) else x)
六、实战案例:用 apply 解锁电影数据的隐藏秘密
我们用 IMDb 电影数据集来实战 apply 的威力(数据可从 Kaggle 下载),目标:
- 从 “时长” 列(如 “120 min”)提取纯数字
- 按 “类型” 列(如 “Action, Comedy”)拆分多类型,统计每个类型的电影数量
步骤 1:数据预处理
TypeScript
取消自动换行复制
movies = pd.read_csv('imdb_movies.csv')
# 提取时长数字(用正则表达式)
movies['duration'] = movies['duration'].apply(lambda x: int(x.split()[0]))
# 拆分类型列
def split_genres(genres_str):
return genres_str.split(', ') if isinstance(genres_str, str) else []
movies['genres_list'] = movies['genres'].apply(split_genres)
步骤 2:统计各类型出现次数
TypeScript
取消自动换行复制
from collections import Counter
# 收集所有类型
all_genres = movies['genres_list'].apply(Counter).sum()
# 转换为DataFrame并排序
genre_counts = pd.DataFrame(all_genres.most_common(), columns=['类型', '数量'])
print(genre_counts.head())
输出:
类型 | 数量 |
Drama | 3456 |
Comedy | 2890 |
Thriller | 2341 |
Action | 2112 |
Romance | 1987 |
七、总结:apply 的终极心法
- 简单操作不用 apply:能用data['col'] + 10解决的问题,别用 apply
- 复杂逻辑首选 apply:涉及条件判断、多行 / 列联动、自定义函数时,apply 就是你的最佳拍档
- 学会拆解问题:如果函数返回复杂结构(如列表、字典),用apply(pd.Series)或explode()展开
最后送大家一句口诀:循环慢如蜗牛爬,apply 快如千里马,数据处理有魔法,一行代码走天下!
下次我们将探讨 apply 与 groupby 的 “梦幻联动”,学会用 apply 做分组后的复杂计算。关注我,一起解锁更多 Pandas 黑科技!