본문으로 건너뛰기
김신건의 로그

[Pandas] MultiIndex

· 수정 · 📖 약 1분 · 540자/단어 #python #pandas #multiindex #hierarchical #indexing
Pandas MultiIndex, 계층 인덱스, hierarchical index, pandas 다중 인덱스, MultiIndex DataFrame

정의

MultiIndex 는 여러 단계(level)의 라벨로 구성된 Pandas Index. SQL의 composite key와 비슷하며, 그룹화된 데이터를 자연스럽게 표현한다. groupby, pivot_table, stack 등이 자동으로 생성하는 인덱스 형태.

사용 상황

MultiIndex가 자연스럽게 등장하는 상황:

  • groupby 집계: df.groupby(['city', 'dept']).sum() 결과는 (city, dept) MultiIndex 행
  • pivot_table: index=['year', 'quarter'] 조합 시 MultiIndex 행 생성
  • stack: DataFrame 컬럼을 행 레벨로 내릴 때 자동 생성
  • 패널 데이터: 날짜 + 종목 같은 2차원 식별자 구조
  • 계층 보고서: 지역 > 부서 > 제품 같은 계층적 집계 결과

데이터가 평탄한 구조면 MultiIndex 없이 일반 컬럼으로 두는 게 간단하다. 행/열 변환이 필요하거나, 계층 구조를 그대로 보존해야 할 때 MultiIndex가 빛난다.

구조 시각화

flowchart LR
    subgraph "MultiIndex 2-level 예시"
        C1["Seoul"] --> D1["dept A"]
        C1 --> D2["dept B"]
        C2["Busan"] --> D3["dept A"]
        C2 --> D4["dept B"]
    end
    D1 --> V1["sales: 100"]
    D2 --> V2["sales: 200"]
    D3 --> V3["sales: 150"]
    D4 --> V4["sales: 250"]

생성

import pandas as pd

# from_tuples
mi = pd.MultiIndex.from_tuples(
    [('Seoul', 'A'), ('Seoul', 'B'), ('Busan', 'A')],
    names=['city', 'dept']
)

# from_product (직교곱): 모든 조합 자동 생성
mi = pd.MultiIndex.from_product(
    [['Seoul', 'Busan'], ['A', 'B']],
    names=['city', 'dept']
)

# from_arrays
mi = pd.MultiIndex.from_arrays(
    [['Seoul', 'Seoul', 'Busan'], ['A', 'B', 'A']],
    names=['city', 'dept']
)

# groupby 결과: 자동으로 MultiIndex
df.groupby(['city', 'dept']).sum().index

인덱싱

python
import pandas as pd
mi = pd.MultiIndex.from_tuples(
  [('Seoul','A'),('Seoul','B'),('Busan','A'),('Busan','B')],
  names=['city','dept']
)
df = pd.DataFrame({'sales':[100,200,150,250]}, index=mi)
print(df)
print('---')
print(df.loc['Seoul'])           # outer level
print('---')
print(df.loc[('Seoul','A')])     # tuple 로 양쪽 level
결과
             sales
city  dept
Seoul A        100
    B        200
Busan A        150
    B        250
---
    sales
dept
A       100
B       200
---
sales    100
Name: (Seoul, A), dtype: int64
citydeptsales
SeoulA100
SeoulB200
BusanA150
BusanB250

slice / cross-section

df.xs('Seoul', level='city')          # cross-section
df.xs('A', level='dept')

# IndexSlice: 더 직관적인 다차원 슬라이스
idx = pd.IndexSlice
df.loc[idx[:, 'A'], :]                # 모든 city 의 dept='A'
df.loc[idx['Seoul':, 'A':'B'], :]     # city 범위 + dept 범위

stack / unstack 으로 변환

df.unstack('dept')                    # dept 가 컬럼 level 로
df.unstack()                          # 가장 안쪽 index level -> 컬럼
df.stack()                            # 컬럼 -> index (역변환)

상세 사용법: Pandas stack-unstack

level 조작

df.reset_index()                              # MultiIndex 해제 -> 일반 컬럼
df.reset_index(level='dept')                  # 일부만 해제
df.swaplevel('city', 'dept')                  # level 순서 바꿈
df.sort_index(level=['city', 'dept'])         # level 별 정렬
df.droplevel('dept')                          # level 제거
df.rename_axis(index=['도시', '부서'])        # level 이름 변경

groupby 와의 자연스러운 결합

# groupby 결과는 자동 MultiIndex
result = df.groupby(['city', 'dept'])['sales'].sum()
result.unstack('dept')              # 시각화에 좋은 wide format

# agg 결과도 MultiIndex
agg = df.groupby(['city', 'dept']).agg({'sales': ['sum', 'mean']})
# 컬럼이 ('sales', 'sum'), ('sales', 'mean') MultiIndex

MultiIndex 컬럼

행뿐 아니라 컬럼도 multi-level 가능.

df.pivot_table(index='city', columns=['dept', 'quarter'], values='sales')
# 컬럼이 (dept, quarter) MultiIndex

평탄화

# 컬럼 MultiIndex -> 단일 컬럼명
df.columns = ['_'.join(map(str, c)) for c in df.columns]
# ('sales', 'A') -> 'sales_A'

# reset_index 로 행 MultiIndex 해제
df_flat = df.reset_index()

실전 패턴

교차 집계 후 wide 변환

import pandas as pd

data = {
    'city': ['Seoul', 'Seoul', 'Busan', 'Busan', 'Seoul', 'Busan'],
    'dept': ['A', 'B', 'A', 'B', 'A', 'B'],
    'q': ['Q1', 'Q1', 'Q1', 'Q1', 'Q2', 'Q2'],
    'sales': [100, 200, 150, 250, 120, 180],
}
df = pd.DataFrame(data)

# groupby -> MultiIndex -> unstack 으로 wide format
pivot = (
    df.groupby(['city', 'dept', 'q'])['sales']
    .sum()
    .unstack('q')
    .fillna(0)
)
print(pivot)

특정 level 필터 후 집계

# dept='A' 인 행만 선택 후 city 별 합계
df_mi = df.groupby(['city', 'dept'])['sales'].sum()
dept_a = df_mi.xs('A', level='dept')
# 결과: city 별 A dept 합계 Series

IndexSlice 로 범위 선택

df_mi = df.groupby(['city', 'dept'])['sales'].sum()
df_mi = df_mi.sort_index()

idx = pd.IndexSlice
# Seoul 만, 모든 dept
seoul_sales = df_mi.loc[idx['Seoul', :]]

두 기간 MultiIndex 비교

# pandas 2.x 스타일: 작년 vs 올해 비교
this_year = (
    df[df['year'] == 2025]
    .groupby(['city', 'dept'])['sales']
    .sum()
)
last_year = (
    df[df['year'] == 2024]
    .groupby(['city', 'dept'])['sales']
    .sum()
)

diff = this_year.subtract(last_year, fill_value=0)
pct  = diff / last_year.replace(0, float('nan')) * 100

MultiIndex 컬럼 평탄화 후 저장

result = df.groupby(['region', 'dept']).agg({'sales': ['sum', 'mean', 'std']})
# 컬럼: ('sales', 'sum'), ('sales', 'mean'), ('sales', 'std')

result.columns = ['_'.join(c) for c in result.columns]
# 'sales_sum', 'sales_mean', 'sales_std'

result.reset_index().to_csv('output.csv', index=False)

성능

상황권장
정렬되지 않은 MultiIndex 슬라이스sort_index() 선행 필수, PerformanceWarning 방지
xs() vs loc[idx]xs() 가독성 좋으나 loc[] 가 약간 빠름
unstack() 후 sparse한 경우NaN 폭증 주의, fillna() 또는 long-format 유지
수백만 행 MultiIndexreset_index() 후 일반 컬럼 처리가 간편한 경우 많음
반복 필터IndexSlice 캐싱보다 reset_index() 후 boolean mask 가 빠를 수 있음
# PerformanceWarning 방지: 슬라이스 전 반드시 정렬
df = df.sort_index()
df.loc['Seoul':'Busan']

# 대용량: reset_index 후 boolean mask 가 더 빠를 수 있음
df_flat = df.reset_index()
df_flat[(df_flat['city'] == 'Seoul') & (df_flat['dept'] == 'A')]

함정

1. tuple vs list

df.loc[('Seoul', 'A')]      # 단일 인덱스 -> Series
df.loc[[('Seoul', 'A')]]    # 리스트 -> DataFrame 유지
df.loc['Seoul', 'A']        # 행 라벨 + 컬럼 라벨로 해석 (다른 의미!)

2. SettingWithCopyWarning / ChainedAssignmentError

WARNING

MultiIndex에서 체이닝 인덱싱 사고가 더 자주 발생한다. pandas 2.x 에서는 ChainedAssignmentError 로 바뀐다. .loc[idx, col] = 한 줄로 처리해야 한다.

# ❌ 체이닝 인덱싱 (pandas 2.x: ChainedAssignmentError)
df.loc['Seoul']['sales'] = 999

# ✓ 단일 loc
df.loc[('Seoul', 'A'), 'sales'] = 999

3. 정렬 안 된 MultiIndex 슬라이스

df.loc['Seoul':'Busan']    # PerformanceWarning: lexsort 안 됨
df = df.sort_index()       # 정렬 후 사용

4. level 이름 없는 MultiIndex

# names 가 None -> xs(level=0) 등 위치로 참조해야 해 불편
mi = pd.MultiIndex.from_tuples([('Seoul', 'A'), ('Busan', 'B')])
# ✓ 항상 names 지정
mi = pd.MultiIndex.from_tuples(
    [('Seoul', 'A'), ('Busan', 'B')],
    names=['city', 'dept']
)

5. groupby 후 MultiIndex 해제 필망

# groupby 결과를 일반 DataFrame 으로 쓰려면 reset_index 필요
result = df.groupby(['city', 'dept'])['sales'].sum()
result_df = result.reset_index()   # 일반 컬럼 DataFrame

관련 위키

이 글의 용어 (8개)
[Pandas] .loc / .ilocpandas
정의 - : 라벨 기반 인덱싱 (index/column 이름) - : 정수 위치 기반 인덱싱 (0-based) 이 두 indexer 가 pandas 선택의 표준. 만으로는 헷갈리…
[Pandas] 성능 / 메모리 최적화pandas
정의 pandas 의 흔한 성능 함정과 최적화 패턴. 벡터화 + dtype 선택 + 알고리즘 의 조합이 핵심. 사용 상황 | 상황 | 권장 접근 | |:---|:---| | 단순…
[Pandas] DataFramepandas
정의 은 2차원 레이블 테이블. 각 열이 , 모든 열이 같은 (행 라벨) 를 공유. SQL 테이블 / Excel 시트 / R data.frame 의 Python 대응체. 구조 시…
[Pandas] groupbypandas
정의 는 데이터를 그룹으로 나누고 (split), 각 그룹에 함수를 적용 (apply), 결과를 합쳐 (combine) 새 DataFrame 으로 만드는 split-apply-c…
[Pandas] Indexpandas
정의 는 행 / 열 라벨을 보관하는 객체. / 의 모든 라벨은 Index. 정렬, 정합성, 빠른 lookup 의 기반. 기본은 RangeIndex (0, 1, 2, ...) 지만…
[Pandas] pivot_tablepandas
정의 는 의 확장. 집계 함수를 동반 해 중복 (index, columns) 도 처리. Excel 의 피벗 테이블과 가장 가까운 기능. pivottable 처리 흐름 사용 상황 …
[Pandas] SettingWithCopyWarningpandas
정의 은 pandas 가 가장 자주 던지는 경고. chained indexing 으로 view 인지 copy 인지 모호한 상태에 값을 할당 할 때 발생. 사용 상황 다음 상황에서…
[Pandas] stack / unstackpandas
정의 - : 가장 안쪽 컬럼 레벨 -> 행 레벨 (DataFrame -> Series 또는 더 좁은 DataFrame) - : 가장 안쪽 행 레벨 -> 컬럼 레벨 / 의 Mult…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기