Documentation Python

pandasのapply関数は、データフレームやシリーズに対して柔軟に関数を適用できる強力なメソッドです。この記事では、基本的な使い方から応用例、パフォーマンスの注意点まで解説します。

apply, map, applymapの比較

メソッド対象用途戻り値
applyDataFrame/Series行・列全体に関数を適用Series/DataFrame
mapSeriesのみ要素ごとの変換・置換Series
applymapDataFrameのみ全要素に関数を適用DataFrame
transformDataFrame/Series形状を維持した変換DataFrame/Series

基本的な使い方

構文

# DataFrameの場合
DataFrame.apply(func, axis=0, raw=False, result_type=None, args=(), **kwargs)

# Seriesの場合
Series.apply(func, convert_dtype=True, args=(), **kwargs)

主なパラメータ:

  • func: 適用する関数
  • axis: 0=列方向、1=行方向(DataFrameのみ)
  • raw: Trueの場合、NumPy配列として渡す(高速化)
  • args: 関数への追加引数

Seriesへの適用

import pandas as pd

# サンプルデータ
df = pd.DataFrame({
    '名前': ['田中', '鈴木', '佐藤', '高橋'],
    '年齢': [25, 30, 28, 35],
    '売上': [100000, 150000, 120000, 180000]
})

# Seriesにapplyを適用(各要素に関数を適用)
df['年齢区分'] = df['年齢'].apply(lambda x: '30代' if x >= 30 else '20代')
print(df)
#    名前  年齢     売上 年齢区分
# 0  田中   25  100000   20代
# 1  鈴木   30  150000   30代
# 2  佐藤   28  120000   20代
# 3  高橋   35  180000   30代

DataFrameへの列方向の適用(axis=0)

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'A': [1, 2, 3, 4],
    'B': [10, 20, 30, 40],
    'C': [100, 200, 300, 400]
})

# 各列の合計を計算
col_sums = df.apply(sum, axis=0)
print(col_sums)
# A     10
# B    100
# C   1000
# dtype: int64

# 各列の統計情報を取得
stats = df.apply(lambda x: pd.Series({
    '平均': x.mean(),
    '標準偏差': x.std(),
    '最大': x.max()
}), axis=0)
print(stats)

DataFrameへの行方向の適用(axis=1)

import pandas as pd

df = pd.DataFrame({
    '国語': [80, 70, 90],
    '数学': [85, 75, 95],
    '英語': [78, 82, 88]
})

# 各行(生徒)の合計点を計算
df['合計'] = df.apply(sum, axis=1)

# 各行の平均点を計算
df['平均'] = df.apply(lambda row: row[['国語', '数学', '英語']].mean(), axis=1)

print(df)
#    国語  数学  英語   合計   平均
# 0   80   85   78  243  81.0
# 1   70   75   82  227  75.7
# 2   90   95   88  273  91.0

map, applymapとの違い

mapの使用例(Seriesのみ)

import pandas as pd

df = pd.DataFrame({
    'コード': ['A', 'B', 'C', 'A', 'B'],
    '値': [10, 20, 30, 40, 50]
})

# 辞書でマッピング
コード変換 = {'A': 'Apple', 'B': 'Banana', 'C': 'Cherry'}
df['商品名'] = df['コード'].map(コード変換)

print(df)
#   コード   値     商品名
# 0    A   10    Apple
# 1    B   20   Banana
# 2    C   30   Cherry
# 3    A   40    Apple
# 4    B   50   Banana

# 関数でマッピング
df['コード大文字'] = df['コード'].map(str.lower)

applymap(map)の使用例(DataFrame全要素)

import pandas as pd

df = pd.DataFrame({
    'A': [1.234, 2.345, 3.456],
    'B': [4.567, 5.678, 6.789]
})

# 全要素を小数点2桁にフォーマット
# pandas 2.1.0以降は map() を使用
formatted = df.map(lambda x: f'{x:.2f}')
# pandas 2.0以前は applymap() を使用
# formatted = df.applymap(lambda x: f'{x:.2f}')

print(formatted)
#       A     B
# 0  1.23  4.57
# 1  2.35  5.68
# 2  3.46  6.79

実践的な例

条件付き処理

import pandas as pd

df = pd.DataFrame({
    '商品': ['りんご', 'バナナ', 'オレンジ', 'ぶどう'],
    '価格': [150, 100, 200, 500],
    '在庫': [50, 0, 30, 10]
})

def 在庫状態(row):
    """在庫状態を判定"""
    if row['在庫'] == 0:
        return '在庫切れ'
    elif row['在庫'] < 20:
        return '残りわずか'
    else:
        return '在庫あり'

df['状態'] = df.apply(在庫状態, axis=1)
print(df)

文字列処理

import pandas as pd

df = pd.DataFrame({
    '氏名': ['田中 太郎', '鈴木 花子', '佐藤 一郎'],
    'メール': ['tanaka@example.com', 'SUZUKI@EXAMPLE.COM', 'sato@Example.Com']
})

# 姓と名を分割
df['姓'] = df['氏名'].apply(lambda x: x.split()[0])
df['名'] = df['氏名'].apply(lambda x: x.split()[1])

# メールアドレスを小文字に正規化
df['メール正規化'] = df['メール'].apply(str.lower)

print(df)

追加引数を渡す

import pandas as pd

def 税込計算(価格, 税率=0.1):
    """税込価格を計算"""
    return int(価格 * (1 + 税率))

df = pd.DataFrame({
    '商品': ['A', 'B', 'C'],
    '価格': [1000, 2000, 3000]
})

# args で追加引数を渡す
df['税込(8%)'] = df['価格'].apply(税込計算, args=(0.08,))
df['税込(10%)'] = df['価格'].apply(税込計算, args=(0.10,))

# kwargs でキーワード引数を渡す
df['税込(軽減税率)'] = df['価格'].apply(税込計算, 税率=0.08)

print(df)

グループ別の処理

import pandas as pd

df = pd.DataFrame({
    'カテゴリ': ['A', 'A', 'B', 'B', 'A'],
    '値': [10, 20, 30, 40, 50]
})

# グループ内での正規化(各グループ内で0-1にスケール)
def normalize(series):
    return (series - series.min()) / (series.max() - series.min())

df['正規化'] = df.groupby('カテゴリ')['値'].apply(normalize)
print(df)

パフォーマンスの最適化

ベクトル化との比較

import pandas as pd
import numpy as np
import time

# 大きなデータフレーム
df = pd.DataFrame({
    'A': np.random.randn(100000),
    'B': np.random.randn(100000)
})

# 方法1: apply(遅い)
start = time.time()
result1 = df.apply(lambda row: row['A'] + row['B'], axis=1)
print(f"apply: {time.time() - start:.3f}秒")

# 方法2: ベクトル化(速い)
start = time.time()
result2 = df['A'] + df['B']
print(f"ベクトル化: {time.time() - start:.3f}秒")

# 方法3: NumPy(最速)
start = time.time()
result3 = np.add(df['A'].values, df['B'].values)
print(f"NumPy: {time.time() - start:.3f}秒")

raw=Trueで高速化

import pandas as pd
import numpy as np

df = pd.DataFrame(np.random.randn(10000, 3), columns=['A', 'B', 'C'])

# raw=False(デフォルト): Seriesとして渡される
%timeit df.apply(np.sum, axis=1)

# raw=True: NumPy配列として渡される(高速)
%timeit df.apply(np.sum, axis=1, raw=True)

推奨される代替手段

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'A': [1, 2, 3, 4, 5],
    'B': [10, 20, 30, 40, 50]
})

# 単純な算術演算はベクトル化
df['C'] = df['A'] + df['B']  # applyより高速
df['D'] = df['A'] ** 2       # applyより高速

# 条件付き処理はnp.whereやnp.select
df['E'] = np.where(df['A'] > 3, 'high', 'low')

# 複数条件
conditions = [
    df['A'] < 2,
    df['A'] < 4,
    df['A'] >= 4
]
choices = ['小', '中', '大']
df['F'] = np.select(conditions, choices)

print(df)

エラー処理

import pandas as pd

df = pd.DataFrame({
    '値': [10, 0, 20, None, 30]
})

def safe_divide(x, divisor):
    """安全な除算"""
    try:
        if x is None or pd.isna(x):
            return None
        if divisor == 0:
            return float('inf')
        return x / divisor
    except Exception as e:
        return None

df['結果'] = df['値'].apply(safe_divide, args=(5,))
print(df)

まとめ

用途推奨方法
要素ごとの単純な変換map() または ベクトル化
行全体を参照する処理apply(axis=1)
列の集計apply(axis=0) または 組み込みメソッド
全要素への適用map()(DataFrame)
高速な数値計算NumPyのベクトル化

applyは柔軟性が高く便利ですが、パフォーマンスが重要な場合はベクトル化された操作を優先しましょう。

参考文献

円