apply自定义函数

apply() 函数是 Pandas 中非常强大和常用的功能,它的存在解决了数据分析中的一个核心需求:灵活地对数据应用自定义操作

为什么需要 apply() 函数?

  1. 超越内置函数的限制:Pandas 虽然提供了很多内置方法(如 sum(), mean() 等),但无法覆盖所有可能的计算需求。
  2. 处理复杂逻辑:有些操作需要复杂的条件判断或多步计算,无法用简单的向量化操作完成。
  3. 代码复用:可以将复杂的操作封装成函数,然后通过 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.1
    else:
        return base_score * 1.05

df['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 0
    elif age > 150:
        return 150
    else:
        return age

ages = 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.25
    if 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() 的优势

  1. 灵活性:可以应用任何 Python 函数
  2. 可读性:代码逻辑清晰
  3. 复用性:函数可以多次调用
  4. 简化复杂操作:将复杂逻辑封装起来

注意事项

  • 对于简单的数学运算,直接使用向量化操作更快
  • 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.1
    else:
        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() - start

print(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 分组操作五大核心类型

示例数据:

import pandas as pd
import numpy as np

df = 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 进行任意操作。

aggtransform 的区别:

方法 输入 输出要求 返回形状
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 可能较慢,尽量用 aggtransform 替代。


五大操作对比

类型 方法 输入单元 输出形状 典型用途
分组 groupby() 列/函数/索引 GroupBy对象 拆分数据
聚合 agg() 每组的列(Series) 降维(1值/组) 统计汇总
转换 transform() 每组的列(Series) 同原始行数 组内标准化、填充
过滤 filter() 整个组(DataFrame) 行子集 筛选重要组
应用 apply() 整个组(DataFrame) 任意 复杂自定义逻辑

总结

  1. 优先使用 aggtransform:它们性能好,语义清晰。
  2. transform 是特征工程的核心:学会用它创建“组内特征”。
  3. filter 用于数据预筛选:比如只分析大客户、活跃地区。
  4. 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,           # DataFrame
    index,          # 行索引(分组依据)
    columns,        # 列(用于展开的分类)
    values,         # 要聚合的数值列
    aggfunc,        # 聚合函数(如 'sum', 'mean' 等)
    fill_value,     # 填充缺失值
    margins         # 是否添加总计行/列
)

实例

示例数据:

import pandas as pd
import numpy as np

df = 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 替换为 0
    margins=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   0
     Q2     200   0
上海 Q1       0 310
     Q2       0 180
广州 Q1     120   0
     Q2       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

实际应用场景

  1. 销售分析:按地区、产品、时间维度汇总销售额
  2. 用户行为分析:按用户分组统计访问次数、停留时间
  3. 财务报表:生成月度/季度汇总表
  4. A/B 测试:对比不同组的转化率、均值
  5. 数据探索:快速查看分类变量之间的关系

补充

  • 如果只想重塑数据而不聚合,用 df.pivot()(但要求索引唯一)
  • pivot_table自动处理重复索引(通过聚合)
  • 可以结合 plot() 直接可视化:
    pivot.plot(kind='bar', title='各地区产品销售额')
    

总结

方法 适用场景
pd.pivot_table() 生成交叉汇总表、做报表、探索数据关系
df.groupby() 复杂分组操作、特征工程、数据清洗

更多推荐