pandasのapply関数は、データフレームやシリーズに対して柔軟に関数を適用できる強力なメソッドです。この記事では、基本的な使い方から応用例、パフォーマンスの注意点まで解説します。
apply, map, applymapの比較
| メソッド | 対象 | 用途 | 戻り値 |
|---|---|---|---|
apply | DataFrame/Series | 行・列全体に関数を適用 | Series/DataFrame |
map | Seriesのみ | 要素ごとの変換・置換 | Series |
applymap | DataFrameのみ | 全要素に関数を適用 | DataFrame |
transform | DataFrame/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は柔軟性が高く便利ですが、パフォーマンスが重要な場合はベクトル化された操作を優先しましょう。