【Pandas中級編】データ操作を極める!groupby集計、DataFrame結合、apply/map活用術

こんにちは! 前回の「【Pythonデータ分析入門】Pandasで表データ操作自由自在!」では、Pandasの基本的な使い方として、データの読み込みや基本的な集計方法をご紹介しました。Pandasの便利さを少し感じていただけたでしょうか?

今回は、その一歩先へ進み、Pandasを使ったより高度なデータ操作テクニックを深掘りしていきます。具体的には、

  • groupby()を使った柔軟なデータ集計
  • 複数のDataFrameを自在に組み合わせる結合方法 (merge, concat)
  • apply()map()を使った効率的なデータ変換

などを、具体的なサンプルコードと共に解説します。これらのテクニックをマスターすれば、あなたのデータ分析スキルは格段に向上するはずです。それでは、Pandasのさらなる魅力に触れていきましょう!


1. より高度なデータ集計:groupby()を使いこなす

前回の記事でも少し触れましたが、groupby()は特定のキーに基づいてデータをグループ化し、それぞれのグループに対して集計処理を行うための非常に強力なメソッドです。

サンプルデータの準備

まずは、集計操作に使用するサンプルDataFrameを作成しましょう。

import pandas as pd
import numpy as np # NumPyも使うことがあるのでインポート

data = {
    '部門': ['営業部', '開発部', '営業部', '人事部', '開発部', '営業部'],
    '性別': ['男性', '女性', '女性', '男性', '男性', '男性'],
    '給与': [500, 600, 450, 550, 700, 520],
    '残業時間': [20, 10, 15, 5, 12, 25]
}
df_employee = pd.DataFrame(data)
print("--- 社員データ ---")
print(df_employee)

実行結果 (例):

--- 社員データ ---
    部門  性別  給与  残業時間
0  営業部  男性  500    20
1  開発部  女性  600    10
2  営業部  女性  450    15
3  人事部  男性  550     5
4  開発部  男性  700    12
5  営業部  男性  520    25

1.1. 単一キーでのグループ化と集計

「部門」ごとに平均給与を計算してみましょう。

avg_salary_by_dept = df_employee.groupby('部門')['給与'].mean()
print("\n--- 部門ごとの平均給与 ---")
print(avg_salary_by_dept)

実行結果 (例):

--- 部門ごとの平均給与 ---
部門
営業部    490.0
人事部    550.0
開発部    650.0
Name: 給与, dtype: float64

1.2. 複数キーでのグループ化

「部門」と「性別」の両方でグループ化し、それぞれの残業時間の合計を計算します。

total_overtime_by_dept_gender = df_employee.groupby(['部門', '性別'])['残業時間'].sum()
print("\n--- 部門別・性別の合計残業時間 ---")
print(total_overtime_by_dept_gender)

実行結果 (例):

--- 部門別・性別の合計残業時間 ---
部門   性別
営業部  女性    15
     男性    45
人事部  男性     5
開発部  女性    10
     男性    12
Name: 残業時間, dtype: int64

このように階層的なインデックス(マルチインデックス)が作成されます。.reset_index() を使うと、これらのインデックスを通常の列に戻すことができます。

print("\n--- reset_index()適用後 ---")
print(total_overtime_by_dept_gender.reset_index())

1.3. 複数の集計関数を一度に適用 (.agg())

.agg()メソッドを使うと、1つのgroupby操作で複数の集計を同時に行うことができます。

# 部門ごとに給与の平均と残業時間の合計、および人数(count)を計算
dept_summary = df_employee.groupby('部門').agg(
    平均給与=('給与', 'mean'),
    合計残業時間=('残業時間', 'sum'),
    人数=('部門', 'count') # '部門'列でなくても、存在する列なら何でも良い
)
print("\n--- 部門ごとの集計サマリー ---")
print(dept_summary)

# 特定の列に対して複数の集計を行う場合
salary_stats_by_dept = df_employee.groupby('部門')['給与'].agg(['mean', 'min', 'max', 'std'])
print("\n--- 部門ごとの給与統計 ---")
print(salary_stats_by_dept)

実行結果 (例):

--- 部門ごとの集計サマリー ---
      平均給与  合計残業時間  人数
部門
営業部   490.0      60   3
人事部   550.0       5   1
開発部   650.0      22   2

--- 部門ごとの給与統計 ---
           mean  min  max        std
部門
営業部  490.000000  450  520  36.055513
人事部  550.000000  550  550        NaN
開発部  650.000000  600  700  70.710678

.agg() に渡す辞書では、新しい列名をキー、(集計対象列, 集計関数)のタプルを値として指定できます。


2. DataFrameの結合:データを一つにまとめる

実際のデータ分析では、複数のデータソースからの情報を組み合わせて分析することがよくあります。Pandasでは、pd.merge()pd.concat()という2つの主要な関数を使ってDataFrameを結合できます。

サンプルDataFrameの準備

結合操作のために、2つのサンプルDataFrameを作成します。

df_customers = pd.DataFrame({
    '顧客ID': ['C001', 'C002', 'C003', 'C004'],
    '氏名': ['佐藤', '鈴木', '高橋', '田中']
})
print("--- 顧客データ ---")
print(df_customers)

df_orders = pd.DataFrame({
    '注文ID': ['O001', 'O002', 'O003', 'O004', 'O005'],
    '顧客ID': ['C001', 'C003', 'C001', 'C002', 'C005'], # C005は顧客データにいない
    '商品': ['リンゴ', 'バナナ', 'ミカン', 'リンゴ', 'ブドウ'],
    '価格': [100, 80, 120, 100, 200]
})
print("\n--- 注文データ ---")
print(df_orders)

2.1. pd.merge():特定の列をキーとして結合 (SQLのJOIN風)

pd.merge()は、データベースのJOIN操作のように、共通の列(キー)を基準にDataFrameを結合します。

# 内部結合 (両方のDataFrameに存在するキーのみ)
df_merged_inner = pd.merge(df_customers, df_orders, on='顧客ID', how='inner')
print("\n--- 内部結合 (inner) ---")
print(df_merged_inner)

# 左外部結合 (左のDataFrameのキーは全て残す)
df_merged_left = pd.merge(df_customers, df_orders, on='顧客ID', how='left')
print("\n--- 左外部結合 (left) ---")
print(df_merged_left)

# 右外部結合 (右のDataFrameのキーは全て残す)
df_merged_right = pd.merge(df_customers, df_orders, on='顧客ID', how='right')
print("\n--- 右外部結合 (right) ---")
print(df_merged_right)

# 完全外部結合 (両方のDataFrameのキーを全て残す)
df_merged_outer = pd.merge(df_customers, df_orders, on='顧客ID', how='outer')
print("\n--- 完全外部結合 (outer) ---")
print(df_merged_outer)

on='顧客ID'で「顧客ID」列をキーに指定し、howパラメータで結合方法(inner, left, right, outer)を指定します。 もしキーとなる列名が左右のDataFrameで異なる場合は、left_on='左の列名', right_on='右の列名'のように指定できます。

2.2. pd.concat():DataFrameを単純に連結 (縦積み・横並び)

pd.concat()は、複数のDataFrameを単純に行方向(縦)または列方向(横)に連結します。

df1 = pd.DataFrame({'A': ['A0', 'A1'], 'B': ['B0', 'B1']})
df2 = pd.DataFrame({'A': ['A2', 'A3'], 'B': ['B2', 'B3']})
df3 = pd.DataFrame({'C': ['C0', 'C1'], 'D': ['D0', 'D1']})

# 行方向の連結 (デフォルト)
df_concat_rows = pd.concat([df1, df2])
print("\n--- 行方向の連結 ---")
print(df_concat_rows)

# ignore_index=True でインデックスを振り直す
df_concat_rows_reindex = pd.concat([df1, df2], ignore_index=True)
print("\n--- 行方向の連結 (インデックス振り直し) ---")
print(df_concat_rows_reindex)

# 列方向の連結 (axis=1 を指定)
df_concat_cols = pd.concat([df1, df3], axis=1)
print("\n--- 列方向の連結 ---")
print(df_concat_cols)

axis=0(デフォルト)で行方向に、axis=1で列方向に連結します。連結時に元のインデックスが重複する場合があるので、ignore_index=Trueで新しい連番のインデックスを振ると便利なことがあります。


3. 効率的なデータ変換:apply()map()

DataFrameやSeriesの値を一括で変換したり、条件に基づいて新しい値を設定したりする際に便利なのがapply()map()(Seriesの場合)メソッドです。

サンプルデータの準備

df_products = pd.DataFrame({
    '商品名': ['リンゴ', 'バナナ', 'ミカン', 'ブドウ'],
    '価格(税抜)': [100, 80, 120, 200],
    '分類': ['果物', '果物', '果物', '果物']
})
print("--- 商品データ ---")
print(df_products)

3.1. .apply():行または列に関数を適用

apply()は、DataFrameの行全体または列全体に対して、自作の関数やラムダ式を適用できます。

# 消費税10%を加えた税込価格を計算する関数
def calculate_tax_included_price(price):
    return price * 1.10

# '価格(税抜)'列に関数を適用して新しい列を作成
df_products['価格(税込)'] = df_products['価格(税抜)'].apply(calculate_tax_included_price)
print("\n--- 税込価格を追加 (applyで関数) ---")
print(df_products)

# ラムダ式を使って同様の処理
df_products['価格(税込)_lambda'] = df_products['価格(税抜)'].apply(lambda x: x * 1.10)
print("\n--- 税込価格を追加 (applyでラムダ式) ---")
print(df_products[['商品名', '価格(税抜)', '価格(税込)_lambda']])

# 行全体に適用して、価格が100円以上の商品にフラグを立てる
def is_expensive(row):
    if row['価格(税抜)'] >= 100:
        return '高価'
    else:
        return '手頃'

df_products['価格帯'] = df_products.apply(is_expensive, axis=1) # axis=1で行単位の処理
print("\n--- 価格帯を追加 (行にapply) ---")
print(df_products)

axis=0(デフォルト)だと列単位、axis=1だと行単位で関数が適用されます。

3.2. .map():Seriesの各要素を置換

map()はSeriesの各要素に対して、辞書や関数を使って値を置き換えます。

# '分類'列の値を日本語から英語に変換する辞書
category_map = {
    '果物': 'Fruit',
    '野菜': 'Vegetable' # 今回のデータにはないが例として
}
df_products['分類(英)'] = df_products['分類'].map(category_map)
print("\n--- 分類を英語に変換 (mapで辞書) ---")
print(df_products)

# 価格(税抜)に応じてランク付けする関数
def price_to_rank(price):
    if price >= 150:
        return 'A'
    elif price >= 100:
        return 'B'
    else:
        return 'C'

df_products['価格ランク'] = df_products['価格(税抜)'].map(price_to_rank)
print("\n--- 価格ランク付け (mapで関数) ---")
print(df_products)

map()はSeriesにしか使えませんが、特定の列の値を効率的に変換・置換するのに便利です。


まとめ:Pandasでデータ操作の達人を目指そう!

今回のPandas中級編では、

  • groupby()による柔軟な集計(複数キー、複数集計関数)
  • pd.merge()によるキーに基づいたDataFrameの結合
  • pd.concat()による単純なDataFrameの連結
  • .apply()による行・列への関数適用
  • .map()によるSeriesの要素の置換

といった、より実践的なデータ操作テクニックを学びました。

これらの機能を組み合わせることで、複雑なデータも効率よく整形し、分析のための準備を整えることができます。Pandasにはまだまだ多くの強力な機能が備わっています。例えば、時系列データの扱いや、ピボットテーブルの作成、さらにはMatplotlibやSeabornといった可視化ライブラリとの連携など、学べば学ぶほどデータ分析の世界は広がっていきます。

ぜひ、今回学んだことを実際のデータで試し、Pandasの力を実感してみてください。あなたのデータ分析の旅が、さらにエキサイティングなものになることを願っています!

その他の投稿はこちら

コメント

このブログの人気の投稿

タイトルまとめ

これで迷わない!WindowsでPython環境構築する一番やさしい方法 #0

【Python標準ライブラリ完結!】11の冒険をありがとう!君のPython力が飛躍する「次の一歩」とは? 🚀