Skip to content

Pandas 피벗 테이블: Excel처럼 데이터 요약·재구성하기 (가이드)

Updated on

Excel로 일해본 분석가라면 누구나 피벗 테이블을 알고 있습니다. 어떤 필드는 행 영역으로, 다른 필드는 열 영역으로 끌어다 놓고, 요약 함수를 고르면—원시 거래 데이터로 가득한 촘촘한 시트가 카테고리별 합계, 지역별 평균, 월별 건수 같은 깔끔한 요약표로 바뀝니다. 이제 같은 일을 Python에서 해야 하는데, unstack를 곁들인 중첩 groupby 호출을 작성하는 방식은 어딘가 투박하고 가독성이 떨어집니다.

진짜 답답함은 데이터에 여러 단계의 그룹핑이 필요하거나, 소계/총계가 필요하거나, 서로 다른 컬럼에 서로 다른 집계 함수를 동시에 적용하고 싶을 때 시작됩니다. groupby, agg, unstack를 체이닝해서 이어 붙이면 데이터 형태가 조금만 바뀌어도 쉽게 깨지는 취약한 코드가 되기 쉽습니다.

pandas pivot_table 함수는 Excel 스타일 피벗 테이블의 강력함을 Python에서도 깔끔한 선언형 API로 제공합니다. 한 번의 함수 호출로 그룹핑, 집계, 다단계 인덱싱, 소계/총계, 결측값 처리까지 모두 다룰 수 있습니다. 이 가이드는 모든 파라미터를 정리하고, 실전 예제를 단계별로 살펴본 뒤, pivot_tablegroupby, pivot, crosstab과 비교해 상황별로 어떤 도구를 선택해야 하는지 명확히 정리합니다.

📚

pd.pivot_table()이 하는 일

pd.pivot_table()은 DataFrame으로부터 스프레드시트 스타일의 요약 테이블을 만듭니다. 행 레이블로 사용할 컬럼(index), 열 헤더로 사용할 컬럼(columns), 집계할 값(values), 적용할 집계 함수(aggfunc)를 지정하면 됩니다. 결과는 각 셀에 요약 통계량이 들어 있는 새로운 DataFrame입니다.

간단한 전/후 예시는 다음과 같습니다.

원본 데이터:

regionproductrevenue
NorthWidget1200
NorthGadget800
SouthWidget1500
SouthGadget950
NorthWidget1400
SouthWidget1600

피벗 테이블(지역·제품별 revenue 합계):

regionGadgetWidget
North8002600
South9503100

각 셀에는 해당 지역-제품 조합의 revenue 합계가 들어 있습니다.

pd.pivot_table() 문법과 파라미터

pd.pivot_table(data, values=None, index=None, columns=None, aggfunc='mean',
               fill_value=None, margins=False, dropna=True, margins_name='All',
               observed=False, sort=True)

파라미터 레퍼런스

ParameterDescriptionDefault
data요약할 DataFrameRequired
values집계할 컬럼(들)None (모든 숫자형 컬럼)
index행 레이블로 사용할 컬럼(들)None
columns열 헤더로 사용할 컬럼(들)None
aggfunc집계 함수(들): 'mean', 'sum', 'count', 'min', 'max', 또는 함수/dict/list'mean'
fill_value결과의 NaN을 대체할 값None
margins행/열 합계(소계/총계) 추가False
margins_name합계 행/열의 이름'All'
dropna값이 전부 NaN인 컬럼 제외True
observedcategorical 컬럼에서 관측된 카테고리만 표시False
sort결과 정렬 여부True

모든 예제에 사용할 샘플 데이터

아래의 모든 예제는 이 판매(sales) 데이터셋을 사용합니다.

import pandas as pd
import numpy as np
 
sales = pd.DataFrame({
    'date': pd.to_datetime(['2025-01-15', '2025-01-20', '2025-02-10', '2025-02-18',
                            '2025-01-12', '2025-02-22', '2025-01-25', '2025-02-14',
                            '2025-01-30', '2025-02-05', '2025-01-18', '2025-02-28']),
    'region': ['North', 'North', 'North', 'North', 'South', 'South',
               'South', 'South', 'East', 'East', 'East', 'East'],
    'product': ['Widget', 'Gadget', 'Widget', 'Gadget',
                'Widget', 'Gadget', 'Widget', 'Gadget',
                'Widget', 'Gadget', 'Widget', 'Gadget'],
    'salesperson': ['Alice', 'Alice', 'Bob', 'Bob',
                    'Charlie', 'Charlie', 'Diana', 'Diana',
                    'Eve', 'Eve', 'Frank', 'Frank'],
    'revenue': [1200, 800, 1400, 850, 1500, 950, 1100, 780, 1300, 900, 1250, 870],
    'units': [10, 8, 12, 9, 15, 10, 11, 8, 13, 9, 12, 9]
})
 
print(sales)

기본 피벗 테이블: 지역별 Revenue 합계

table = pd.pivot_table(sales, values='revenue', index='region', aggfunc='sum')
print(table)

Output:

        revenue
region
East       4320
North      4250
South      4330

이는 sales.groupby('region')['revenue'].sum()와 동일하지만, Series가 아니라 DataFrame을 반환합니다.

열 헤더 추가하기

columns 파라미터를 추가하면 두 번째 변수로 요약을 더 쪼갤 수 있습니다.

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum')
print(table)

Output:

product  Gadget  Widget
region
East       1770    2550
North      1650    2600
South      1730    2600

이제 각 셀은 특정 region-product 쌍의 총 revenue를 보여줍니다.

여러 집계 함수를 동시에 적용하기

aggfunc에 함수 리스트를 넘기면 여러 통계를 한 번에 계산할 수 있습니다.

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc=['sum', 'mean', 'count'])
print(table)

Output:

          sum          mean                count
product Gadget Widget Gadget  Widget      Gadget Widget
region
East      1770   2550  885.0  1275.0           2      2
North     1650   2600  825.0  1300.0           2      2
South     1730   2600  865.0  1300.0           2      2

결과의 컬럼은 MultiIndex가 되며, 상위 레벨에는 집계 함수가, 두 번째 레벨에는 product가 놓입니다.

컬럼별로 다른 집계 함수 적용하기

aggfunc에 dict를 사용하면 value 컬럼별로 서로 다른 집계 함수를 적용할 수 있습니다.

table = pd.pivot_table(sales, values=['revenue', 'units'], index='region',
                       columns='product',
                       aggfunc={'revenue': 'sum', 'units': 'mean'})
print(table)

Output:

       revenue         units
product Gadget Widget Gadget Widget
region
East      1770   2550    9.0   12.5
North     1650   2600    8.5   11.0
South     1730   2600    9.0   13.0

revenue는 합계를 내고 units는 평균을 냅니다. Excel 피벗 테이블에서 필드마다 요약 함수를 다르게 지정하는 것과 동일합니다.

margins로 소계/총계 추가하기

margins 파라미터는 전체 합계를 보여주는 행과 열을 추가합니다.

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum', margins=True)
print(table)

Output:

product  Gadget  Widget    All
region
East       1770    2550   4320
North      1650    2600   4250
South      1730    2600   4330
All        5150    7750  12900

All 행은 제품별 총 revenue를 보여주고, All 열은 지역별 총 revenue를 보여줍니다. 오른쪽 아래 셀은 전체 총합입니다.

margins_name으로 라벨을 바꿀 수도 있습니다.

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum',
                       margins=True, margins_name='Total')
print(table)

fill_value로 결측값 처리하기

데이터에 특정 조합이 존재하지 않으면 피벗 테이블에 NaN이 생깁니다. fill_value로 이를 대체할 수 있습니다.

# Remove one row to create a missing combination
sales_missing = sales.drop(index=0)
 
table = pd.pivot_table(sales_missing, values='revenue', index='region',
                       columns='product', aggfunc='sum', fill_value=0)
print(table)

NaN을 0으로 바꿔주면 표시도 깔끔해지고, 이후 계산에서 문제가 생기는 것도 줄일 수 있습니다.

다단계 인덱스(행 그룹핑)

index에 컬럼 리스트를 넘기면 계층형 행 레이블을 만들 수 있습니다.

table = pd.pivot_table(sales, values='revenue',
                       index=['region', 'salesperson'],
                       columns='product', aggfunc='sum')
print(table)

Output:

product                Gadget  Widget
region salesperson
East   Eve              900.0  1300.0
       Frank            870.0  1250.0
North  Alice            800.0  1200.0
       Bob              850.0  1400.0
South  Charlie          950.0  1500.0
       Diana            780.0  1100.0

각 region 아래에 salesperson이 중첩되어, 데이터를 드릴다운해서 보는 형태가 됩니다.

다단계 컬럼

마찬가지로 columns에 리스트를 넘기면 계층형 컬럼 헤더를 만들 수 있습니다.

sales['month'] = sales['date'].dt.month_name()
 
table = pd.pivot_table(sales, values='revenue', index='region',
                       columns=['product', 'month'], aggfunc='sum', fill_value=0)
print(table)

이렇게 하면 상위 헤더는 product, 하위 헤더는 month가 되는 2단 컬럼 헤더가 생성됩니다.

사용자 정의 집계 함수 사용하기

aggfunc에는 lambda 함수나 NumPy 함수 등 호출 가능한(callable) 어떤 것이든 전달할 수 있습니다.

# Range (max - min) of revenue by region
table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc=lambda x: x.max() - x.min())
print(table)

Output:

product  Gadget  Widget
region
East         30      50
North        50     200
South       170     400

그 외 유용한 사용자 정의 집계 예시:

# Coefficient of variation
table = pd.pivot_table(sales, values='revenue', index='region',
                       aggfunc=lambda x: x.std() / x.mean() * 100)

실전 예제: 학생 성적 분석

students = pd.DataFrame({
    'student': ['Alice', 'Alice', 'Alice', 'Bob', 'Bob', 'Bob',
                'Charlie', 'Charlie', 'Charlie', 'Diana', 'Diana', 'Diana'],
    'subject': ['Math', 'Science', 'English'] * 4,
    'semester': ['Fall', 'Fall', 'Fall', 'Fall', 'Fall', 'Fall',
                 'Spring', 'Spring', 'Spring', 'Spring', 'Spring', 'Spring'],
    'score': [92, 88, 95, 78, 85, 72, 90, 93, 88, 85, 79, 91]
})
 
# Average score by subject and semester
table = pd.pivot_table(students, values='score', index='subject',
                       columns='semester', aggfunc='mean', margins=True)
print(table.round(1))

Output:

semester   Fall  Spring   All
subject
English    83.5    89.5  86.5
Math       85.0    87.5  86.2
Science    86.5    86.0  86.2
All        85.0    87.7  86.3

이 표만 봐도 Spring 학기의 평균이 전체적으로 조금 더 높고, 그중에서도 English의 개선 폭이 가장 크다는 것을 즉시 알 수 있습니다.

실전 예제: 월별 판매 리포트

# Create a monthly sales summary
sales['month'] = sales['date'].dt.strftime('%Y-%m')
 
report = pd.pivot_table(sales, values=['revenue', 'units'],
                        index='region', columns='month',
                        aggfunc={'revenue': 'sum', 'units': 'sum'},
                        margins=True, fill_value=0)
print(report)

Excel에서 만들던 것과 같은 형태의 월별 요약 리포트가 생성되며, 지역·월별 합계와 총계까지 한 번에 포함됩니다.

pivot_table vs pivot vs groupby vs crosstab

이 네 함수는 기능이 일부 겹치지만, 각각의 대표적인 사용 목적이 다릅니다.

Featurepivot_table()pivot()groupby()crosstab()
AggregationYes (any function)No (raises on duplicates)Yes (any function)Yes (limited)
Handles duplicatesYes, via aggfuncNoYesYes
Subtotals (margins)YesNoNo (manual)Yes
Fill missing valuesYes (fill_value)NoNoYes (fill_value)
InputDataFrameDataFrameDataFrameSeries/arrays
OutputDataFrameDataFrameDataFrame/SeriesDataFrame
Multi-level indexYesYesYesYes
Best forSummarizing data with aggregationReshaping unique key-value dataFlexible grouped analysisFrequency tables
Default functionmeanN/AN/Acount

언제 무엇을 써야 하나요?

pivot_table()을 사용해야 하는 경우: 집계가 필요한 Excel 스타일 요약표(소계/총계, 여러 집계 함수, 결측값 처리 포함)가 필요할 때. 요약 테이블 생성에 가장 강력하고 읽기 쉬운 선택입니다.

pivot()을 사용해야 하는 경우: index와 columns 조합이 유일(unique)해서 집계가 필요 없고, 단순히 reshape만 하고 싶을 때. 집계 단계를 건너뛰기 때문에 pivot_table()보다 빠릅니다.

groupby()를 사용해야 하는 경우: 그룹별 계산이 필요하지만 wide-format 결과가 꼭 필요하진 않을 때. groupby는 기본적으로 long-format을 반환합니다. groupby().unstack()pivot_table과 비슷한 결과를 만들 수도 있지만, 보통 pivot_table이 더 읽기 쉽습니다.

crosstab()을 사용해야 하는 경우: 범주형 변수의 빈도표/교차표를 만들 때. crosstab()은 DataFrame뿐 아니라 Series나 배열도 바로 받을 수 있고, 기본 동작이 count입니다.

동치(Equivalence) 예시

아래 세 줄은 동일한 결과를 만듭니다.

# pivot_table approach
result1 = pd.pivot_table(sales, values='revenue', index='region',
                         columns='product', aggfunc='sum')
 
# groupby + unstack approach
result2 = sales.groupby(['region', 'product'])['revenue'].sum().unstack()
 
# Both produce the same table
print(result1.equals(result2))  # True

특히 margins, fill_value, 여러 집계 함수를 추가할수록 pivot_table 쪽이 훨씬 읽기 쉬워집니다.

MultiIndex 컬럼 평탄화(Flatten)하기

여러 집계 함수를 사용하면 MultiIndex 컬럼이 생겨 다루기 불편한 경우가 많습니다.

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc=['sum', 'mean'])
 
# Flatten the column MultiIndex
table.columns = ['_'.join(col).strip() for col in table.columns.values]
print(table)

Output:

        sum_Gadget  sum_Widget  mean_Gadget  mean_Widget
region
East          1770        2550        885.0       1275.0
North         1650        2600        825.0       1300.0
South         1730        2600        865.0       1300.0

이제 컬럼이 단순 문자열이 되어 참조하기가 훨씬 쉬워집니다.

피벗 테이블 정렬과 필터링

피벗 테이블도 일반 DataFrame이므로 정렬/필터링을 그대로 적용할 수 있습니다.

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum', margins=True)
 
# Sort by total revenue (All column), descending
sorted_table = table.sort_values('All', ascending=False)
print(sorted_table)
 
# Filter to show only regions with Widget revenue > 2500
filtered = table[table['Widget'] > 2500]
print(filtered)

피벗 테이블 내보내기

피벗 테이블을 Excel(이해관계자가 기대하는 형식)이나 CSV로 저장할 수 있습니다.

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum', margins=True)
 
# Export to Excel
table.to_excel('sales_pivot.xlsx', sheet_name='Revenue Summary')
 
# Export to CSV
table.to_csv('sales_pivot.csv')

성능 팁

pivot_table()은 내부적으로 groupby를 호출하므로 성능은 대체로 비슷합니다. 대용량 데이터에서는 다음을 참고하세요.

Dataset SizeExpected Time
100K rows, 2 group cols~5 ms
1M rows, 2 group cols~50 ms
10M rows, 3 group cols~500 ms

최적화 전략:

  1. 피벗 전 데이터 줄이기pivot_table() 호출 전 필요한 행만 필터링하고, 필요한 컬럼만 선택하세요.
  2. categorical dtype 사용 — 문자열 컬럼을 category dtype으로 바꾸면 그룹핑이 더 빨라질 수 있습니다.
  3. lambda aggfunc 피하기'sum', 'mean' 같은 내장 문자열 집계는 최적화된 C 코드로 동작합니다. lambda는 느린 Python 루프로 폴백되는 경우가 많습니다.
# Faster: use categorical dtypes
sales['region'] = sales['region'].astype('category')
sales['product'] = sales['product'].astype('category')
 
# Faster: use string name instead of lambda
table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum')  # 'sum' is optimized

PyGWalker로 피벗 테이블 시각화하기

pd.pivot_table()은 수치 요약에 매우 유용하지만, 때로는 패턴을 인터랙티브하게 시각화해서 탐색해야 합니다. PyGWalker (opens in a new tab)는 오픈소스 Python 라이브러리로, 초기 설정 이후에는 코드 없이도 드래그 앤 드롭 UI로 피벗 테이블, 막대 차트, 히트맵 등을 만들 수 있게 해줍니다.

import pandas as pd
import pygwalker as pyg
 
sales = pd.DataFrame({
    'region': ['North', 'North', 'South', 'South', 'East', 'East'] * 2,
    'product': ['Widget', 'Gadget'] * 6,
    'revenue': [1200, 800, 1500, 950, 1300, 900, 1400, 850, 1100, 780, 1250, 870],
    'units': [10, 8, 15, 10, 13, 9, 12, 9, 11, 8, 12, 9]
})
 
# Launch interactive pivot table and visualization
walker = pyg.walk(sales)

PyGWalker는 Tableau처럼 region을 rows로, product를 columns로, revenue를 values로 드래그해 시각적으로 피벗 테이블을 만들 수 있는 인터페이스를 제공합니다. 테이블 뷰와 차트 뷰를 즉시 전환할 수 있고, 집계 함수를 바꿔가며 비교하거나 결과를 내보내는 작업도 추가 코딩 없이 할 수 있습니다.

Google Colab (opens in a new tab)이나 Kaggle (opens in a new tab)에서 PyGWalker를 바로 써보거나, pip install pygwalker로 설치해 사용할 수 있습니다.

FAQ

pandas에서 pivot과 pivot_table의 차이는 무엇인가요?

pivot()은 집계 없이 데이터를 reshape합니다. 즉 index와 columns 조합이 유일해야 하며, 중복이 있으면 에러가 발생합니다. pivot_table()sum이나 mean 같은 함수로 중복을 집계해 처리할 수 있습니다. 단순 reshape는 pivot(), 집계나 소계/총계가 필요하면 pivot_table()을 사용하세요.

pandas 피벗 테이블에 합계(margins)를 추가하려면 어떻게 하나요?

pivot_table() 호출에서 margins=True를 설정하세요: pd.pivot_table(df, values='revenue', index='region', columns='product', aggfunc='sum', margins=True). 그러면 소계를 담은 All 행과 열이 추가됩니다. 라벨은 margins_name='Total'로 바꿀 수 있습니다.

피벗 테이블에서 여러 집계 함수를 사용할 수 있나요?

가능합니다. aggfunc에 리스트를 전달하세요: aggfunc=['sum', 'mean', 'count']. 그러면 함수 레벨과 값 컬럼 레벨로 구성된 MultiIndex 컬럼 헤더가 생성됩니다. 또한 dict를 전달해 컬럼별로 다른 함수를 적용할 수도 있습니다: aggfunc={'revenue': 'sum', 'units': 'mean'}.

pandas에서 pivot_table은 groupby와 어떻게 다른가요?

둘 다 집계를 수행하지만, pivot_table()은 wide-format 결과(그룹 변수 중 하나가 열 헤더가 됨)를 만들어주는 반면 groupby()는 기본적으로 long-format 결과를 만듭니다. 또한 pivot_table()은 margins(소계/총계)와 fill_value를 기본 기능으로 지원합니다. 내부적으로 pivot_table()groupby()를 사용합니다.

피벗 테이블의 NaN 값은 어떻게 처리하나요?

fill_value 파라미터로 NaN을 특정 값으로 대체하세요: pd.pivot_table(df, ..., fill_value=0). dropna=True(기본값)는 모든 값이 NaN인 컬럼을 결과에서 제외합니다.

백분율(percentage) 값으로 피벗 테이블을 만들 수 있나요?

가능합니다. 먼저 count나 sum으로 피벗 테이블을 만든 뒤, 전체 합으로 나누면 됩니다. 백분율 기반 교차표가 목적이라면 pd.crosstab()normalize를 사용할 수도 있습니다. 피벗 테이블에서는 보통 수동으로 계산합니다: table = table.div(table.sum(axis=1), axis=0) * 100.

결론

pandas의 pivot_table() 함수는 Python에서 요약 테이블을 만드는 가장 다재다능한 도구입니다. 기억해야 할 핵심은 다음과 같습니다.

  • **values, index, columns**로 무엇을 요약하고, 행을 어떻게 그룹핑하며, 열을 어떻게 그룹핑할지 정의합니다.
  • **aggfunc**로 집계 함수를 지정합니다. 여러 함수는 리스트로, 컬럼별 함수는 dict로 지정할 수 있습니다.
  • **margins=True**로 소계/총계를 추가할 수 있는데, 이는 Excel의 “Grand Total”에 해당합니다.
  • **fill_value**로 존재하지 않는 조합에서 생기는 결측값을 기본값(대개 0)으로 채웁니다.
  • 가독성 측면에서 groupby().unstack()보다 pivot_table()을 선호하세요(특히 margins나 fill_value가 필요할 때).
  • 데이터에 유일 키 조합이 보장되고 집계가 필요 없다면 pivot_table()보다 pivot()이 더 적합합니다.
  • 범주형 변수의 단순 빈도표에는 crosstab()이 적합합니다.

피벗 테이블을 인터랙티브하게 탐색하고 싶다면, PyGWalker (opens in a new tab)가 Jupyter Notebook 안에서 Excel 피벗 테이블과 유사한 드래그 앤 드롭 경험을 제공합니다.

📚