pandas の MultiIndex を使いこなす

pandas の MultiIndex(階層的インデックス)を使うと、複数の軸を持つデータを 2 次元の DataFrame で効率的に表現できます。集計結果の整理やピボットテーブル風の操作に威力を発揮します。

MultiIndex の作成

MultiIndex を作る方法はいくつかあります。

import pandas as pd
import numpy as np

# タプルのリストから作成
index = pd.MultiIndex.from_tuples([
    ('東京', '2023'),
    ('東京', '2024'),
    ('大阪', '2023'),
    ('大阪', '2024')
], names=['都市', ''])

df = pd.DataFrame({
    '売上': [100, 120, 80, 90]
}, index=index)

print(df)
#            売上
# 都市 年        
# 東京 2023   100
#      2024   120
# 大阪 2023    80
#      2024    90

配列から作成することもできます。

# 配列から作成
index = pd.MultiIndex.from_arrays([
    ['東京', '東京', '大阪', '大阪'],
    ['2023', '2024', '2023', '2024']
], names=['都市', ''])

直積(全組み合わせ)を作る場合は from_product が便利です。

# 直積から作成
cities = ['東京', '大阪', '名古屋']
years = ['2023', '2024']

index = pd.MultiIndex.from_product([cities, years], names=['都市', ''])
print(index)
# MultiIndex([('東京', '2023'),
#             ('東京', '2024'),
#             ('大阪', '2023'),
#             ('大阪', '2024'),
#             ('名古屋', '2023'),
#             ('名古屋', '2024')],
#            names=['都市', '年'])

groupby からの自動生成

実務で最もよく遭遇するのは、groupby の結果として生成される MultiIndex です。

df = pd.DataFrame({
    '都市': ['東京', '東京', '大阪', '大阪', '東京', '大阪'],
    '': [2023, 2024, 2023, 2024, 2023, 2024],
    '売上': [50, 60, 40, 45, 50, 45]
})

# グループ化すると MultiIndex になる
result = df.groupby(['都市', ''])['売上'].sum()
print(result)
# 都市  年  
# 大阪  2023     40
#       2024     90
# 東京  2023    100
#       2024     60
# Name: 売上, dtype: int64

print(type(result.index))
# <class 'pandas.core.indexes.multi.MultiIndex'>

データの選択

MultiIndex からデータを選択する方法は複数あります。

import pandas as pd

index = pd.MultiIndex.from_product(
    [['東京', '大阪'], ['2023', '2024']],
    names=['都市', '']
)
df = pd.DataFrame({
    '売上': [100, 120, 80, 90],
    '利益': [10, 15, 8, 12]
}, index=index)

# 第1レベルで選択
print(df.loc['東京'])
#       売上  利益
# 年            
# 2023  100   10
# 2024  120   15

# 両レベルを指定
print(df.loc[('東京', '2024')])
# 売上    120
# 利益     15
# Name: (東京, 2024), dtype: int64

スライスを使った範囲選択も可能です。

# 複数の値を選択
print(df.loc[['東京', '大阪']])

# 第2レベルで選択(xs を使う)
print(df.xs('2024', level=''))
#       売上  利益
# 都市          
# 東京  120   15
# 大阪   90   12

xs(cross-section)は特定のレベルの値を抽出するのに便利です。

loc

第 1 レベルからの階層的なアクセスに適している

xs

特定レベルの値で横断的に抽出したいときに使う

インデックスのリセット

MultiIndex を通常の列に戻すには reset_index を使います。

# MultiIndex を列に変換
df_flat = df.reset_index()
print(df_flat)
#   都市    年  売上  利益
# 0  東京  2023  100   10
# 1  東京  2024  120   15
# 2  大阪  2023   80    8
# 3  大阪  2024   90   12

# 特定レベルのみリセット
df_partial = df.reset_index(level='')
print(df_partial)
#        年  売上  利益
# 都市                
# 東京  2023  100   10
# 東京  2024  120   15
# 大阪  2023   80    8
# 大阪  2024   90   12

レベルの操作

レベルの順序を入れ替えたり、ソートしたりできます。

# レベルを入れ替え
df_swapped = df.swaplevel()
print(df_swapped)
#            売上  利益
# 年   都市          
# 2023 東京  100   10
# 2024 東京  120   15
# 2023 大阪   80    8
# 2024 大阪   90   12

# インデックスでソート
df_sorted = df_swapped.sort_index()
print(df_sorted)
#            売上  利益
# 年   都市          
# 2023 大阪   80    8
#      東京  100   10
# 2024 大阪   90   12
#      東京  120   15

列の MultiIndex

行だけでなく、列も MultiIndex にできます。

# 列が MultiIndex の DataFrame
columns = pd.MultiIndex.from_product(
    [['売上', '利益'], ['Q1', 'Q2']],
    names=['指標', '四半期']
)

df = pd.DataFrame(
    np.random.randint(50, 150, (3, 4)),
    index=['東京', '大阪', '名古屋'],
    columns=columns
)

print(df)
# 指標    売上       利益     
# 四半期   Q1   Q2   Q1   Q2
# 東京    89  123   67  102
# 大阪   112   78   91   55
# 名古屋   95  141   73  118

# 列の選択
print(df['売上'])
# 四半期   Q1   Q2
# 東京    89  123
# 大阪   112   78
# 名古屋   95  141

print(df['売上']['Q1'])
# 東京     89
# 大阪    112
# 名古屋    95
# Name: Q1, dtype: int64

stack と unstack

stack は列の MultiIndex を行に移動し、unstack は行の MultiIndex を列に移動します。

df = pd.DataFrame({
    '売上': [100, 120, 80, 90],
    '利益': [10, 15, 8, 12]
}, index=pd.MultiIndex.from_product(
    [['東京', '大阪'], ['2023', '2024']],
    names=['都市', '']
))

# unstack: 行→列
unstacked = df.unstack(level='')
print(unstacked)
#       売上       利益     
# 年   2023 2024 2023 2024
# 都市                    
# 大阪   80   90    8   12
# 東京  100  120   10   15

# stack: 列→行
stacked = unstacked.stack(level='')
print(stacked)
#            売上  利益
# 都市 年            
# 大阪 2023   80    8
#      2024   90   12
# 東京 2023  100   10
#      2024  120   15
メソッド動作結果
stack列 → 行より縦長になる
unstack行 → 列より横長になる

集計との組み合わせ

MultiIndex は pivot_table の結果としてもよく現れます。

df = pd.DataFrame({
    '都市': ['東京', '東京', '大阪', '大阪'] * 2,
    '': [2023, 2024] * 4,
    '商品': ['A', 'A', 'A', 'A', 'B', 'B', 'B', 'B'],
    '売上': [100, 120, 80, 90, 50, 60, 40, 45]
})

# ピボットテーブル
pivot = pd.pivot_table(
    df,
    values='売上',
    index=['都市', ''],
    columns='商品',
    aggfunc='sum'
)

print(pivot)
# 商品        A   B
# 都市 年          
# 大阪 2023  80  40
#      2024  90  45
# 東京 2023 100  50
#      2024 120  60

MultiIndex は最初は複雑に感じますが、大きなデータセットの集計結果を整理するのに非常に便利です。reset_index で通常の DataFrame に戻せるので、慣れないうちは必要に応じて変換しながら使うとよいでしょう。