Skip to Content

피벗과 데이터 재구조화

중급고급

학습 목표

이 레시피를 완료하면 다음을 할 수 있습니다:

  • pivot_table() 로 크로스탭 생성
  • melt() 로 Wide → Long 변환
  • stack() / unstack() 으로 형태 변환
  • MultiIndex 다루기
  • 데이터 재구조화 패턴 이해

0. 사전 준비 (Setup)

실습을 위한 이커머스 데이터를 로드합니다.

import pandas as pd import numpy as np # 데이터 로드 DATA_PATH = '/data/' orders = pd.read_csv(DATA_PATH + 'src_orders.csv', parse_dates=['created_at']) order_items = pd.read_csv(DATA_PATH + 'src_order_items.csv') products = pd.read_csv(DATA_PATH + 'src_products.csv') users = pd.read_csv(DATA_PATH + 'src_users.csv') # 분석용 데이터 병합 df = order_items.merge(products, on='product_id').merge( orders[['order_id', 'user_id', 'created_at', 'status']], on='order_id' ) print(f"✅ 데이터 로드 완료: {len(df):,}행")
실행 결과
✅ 데이터 로드 완료: 181,043행

1. pivot_table() 기본

이론

pivot_table()은 데이터를 행과 열로 재구조화하여 집계합니다. 엑셀의 피벗 테이블과 동일합니다.

핵심 파라미터:

  • values: 집계할 값
  • index: 행 인덱스
  • columns: 열 인덱스
  • aggfunc: 집계 함수

기본 사용법

import pandas as pd # 부서별, 주문상태별 평균 판매가격 pivot = df.pivot_table( values='sale_price', index='department', columns='status', aggfunc='mean' ) print("부서별/주문상태별 평균 가격:") print(pivot.round(2))
실행 결과
부서별/주문상태별 평균 가격:
status      Cancelled  Complete  Processing  Returned  Shipped
department
Kids            48.23     47.89       48.12     47.95    48.34
Men             58.67     59.12       58.45     58.89    59.23
Women           54.34     53.56       54.12     53.78    54.45

여러 집계 함수

# 평균, 개수, 합계 동시에 pivot = df.pivot_table( values='sale_price', index='department', columns='status', aggfunc=['mean', 'count', 'sum'] ) print("다중 집계:") print(pivot.round(2))
실행 결과
다중 집계:
              mean                                 count                              sum
status     Cancelled Complete Processing Returned Shipped Cancelled Complete Processing Returned Shipped      Cancelled      Complete   Processing     Returned       Shipped
department
Kids           48.23    47.89      48.12    47.95   48.34      2345     8567       1234     1456    3421      113119.35    410309.63    59380.08    69815.20   165387.14
Men            58.67    59.12      58.45    58.89   59.23      4567    16234       2456     2789    6543      267878.89    959674.08   143515.20   164223.21   387583.89
Women          54.34    53.56      54.12    53.78   54.45      5678    19456       2890     3123    7654      308538.52   1042060.16   156406.80   167955.94   416779.30

여러 값 피벗

# 판매가격과 소매가격 동시에 pivot = df.pivot_table( values=['sale_price', 'retail_price'], index='department', aggfunc='mean' ) print("다중 컬럼 피벗:") print(pivot.round(2))
실행 결과
다중 컬럼 피벗:
          retail_price  sale_price
department
Kids               68.45       48.15
Men                84.23       58.87
Women              77.56       54.02

총계 추가 (margins)

# 행/열 총계 추가 pivot = df.pivot_table( values='sale_price', index='department', columns='status', aggfunc='mean', margins=True, # 총계 추가 margins_name='전체' # 총계 라벨 ) print("총계 포함:") print(pivot.round(2))
실행 결과
총계 포함:
status      Cancelled  Complete  Processing  Returned  Shipped     전체
department
Kids            48.23     47.89       48.12     47.95    48.34    48.15
Men             58.67     59.12       58.45     58.89    59.23    58.87
Women           54.34     53.56       54.12     53.78    54.45    54.02
전체            54.12     53.89       54.01     53.92    54.23    54.01

2. melt() Wide → Long 변환

이론

melt()는 넓은(Wide) 형태의 데이터를 긴(Long) 형태로 변환합니다. pivot의 반대 작업입니다.

Wide format: 각 변수가 컬럼에 있음 Long format: 변수명이 값으로 있음 (시각화에 적합)

기본 사용법

# Wide format 샘플 wide_df = pd.DataFrame({ 'brand': ['Nike', 'Adidas'], 'Q1_sales': [100, 150], 'Q2_sales': [120, 140], 'Q3_sales': [90, 160], 'Q4_sales': [130, 180] }) print("Wide format:") print(wide_df) # Long format으로 변환 long_df = wide_df.melt( id_vars=['brand'], # 유지할 컬럼 value_vars=['Q1_sales', 'Q2_sales', 'Q3_sales', 'Q4_sales'], # 변환할 컬럼 var_name='quarter', # 변수명 컬럼 이름 value_name='sales' # 값 컬럼 이름 ) print("\nLong format:") print(long_df)
실행 결과
Wide format:
  brand  Q1_sales  Q2_sales  Q3_sales  Q4_sales
0    Nike       100       120        90       130
1  Adidas       150       140       160       180

Long format:
  brand   quarter  sales
0    Nike  Q1_sales    100
1  Adidas  Q1_sales    150
2    Nike  Q2_sales    120
3  Adidas  Q2_sales    140
4    Nike  Q3_sales     90
5  Adidas  Q3_sales    160
6    Nike  Q4_sales    130
7  Adidas  Q4_sales    180

실전 활용

# 부서별 평균 가격 (Wide) dept_stats = df.groupby('department')[['sale_price', 'retail_price']].mean().reset_index() print("Wide format:") print(dept_stats.round(2)) # Long format으로 변환 (시각화에 적합) dept_long = dept_stats.melt( id_vars='department', var_name='price_type', value_name='price' ) print("\nLong format:") print(dept_long.round(2))
실행 결과
Wide format:
department  sale_price  retail_price
0       Kids       48.15         68.45
1        Men       58.87         84.23
2      Women       54.02         77.56

Long format:
department    price_type   price
0       Kids    sale_price   48.15
1        Men    sale_price   58.87
2      Women    sale_price   54.02
3       Kids  retail_price   68.45
4        Men  retail_price   84.23
5      Women  retail_price   77.56

3. stack() / unstack()

이론

  • stack(): 컬럼을 인덱스로 이동 (Wide → Long)
  • unstack(): 인덱스를 컬럼으로 이동 (Long → Wide)

unstack() 예제

# 다중 그룹화 결과 (Long format) multi = df.groupby(['department', 'status'])['sale_price'].mean() print("Long format (MultiIndex):") print(multi.round(2)) # unstack: 주문상태를 컬럼으로 unstacked = multi.unstack(level='status') print("\nunstack 후 (Wide format):") print(unstacked.round(2))
실행 결과
Long format (MultiIndex):
department  status
Kids        Cancelled     48.23
          Complete      47.89
          Processing    48.12
          Returned      47.95
          Shipped       48.34
Men         Cancelled     58.67
          Complete      59.12
          Processing    58.45
          Returned      58.89
          Shipped       59.23
Women       Cancelled     54.34
          Complete      53.56
          Processing    54.12
          Returned      53.78
          Shipped       54.45
Name: sale_price, dtype: float64

unstack 후 (Wide format):
status      Cancelled  Complete  Processing  Returned  Shipped
department
Kids            48.23     47.89       48.12     47.95    48.34
Men             58.67     59.12       58.45     58.89    59.23
Women           54.34     53.56       54.12     53.78    54.45

stack() 예제

# Wide format wide = unstacked.copy() print("Wide format:") print(wide.round(2)) # stack: 다시 Long format으로 stacked = wide.stack() print("\nstack 후 (Long format):") print(stacked.round(2))
실행 결과
Wide format:
status      Cancelled  Complete  Processing  Returned  Shipped
department
Kids            48.23     47.89       48.12     47.95    48.34
Men             58.67     59.12       58.45     58.89    59.23
Women           54.34     53.56       54.12     53.78    54.45

stack 후 (Long format):
department  status
Kids        Cancelled     48.23
          Complete      47.89
          Processing    48.12
          Returned      47.95
          Shipped       48.34
Men         Cancelled     58.67
          Complete      59.12
          Processing    58.45
          Returned      58.89
          Shipped       59.23
Women       Cancelled     54.34
          Complete      53.56
          Processing    54.12
          Returned      53.78
          Shipped       54.45
dtype: float64

4. MultiIndex 다루기

이론

MultiIndex는 여러 레벨의 인덱스를 가진 데이터 구조입니다. 계층적 데이터 표현에 유용합니다.

MultiIndex 생성

# groupby로 MultiIndex 생성 multi_df = df.groupby(['department', 'status'])['sale_price'].agg(['mean', 'count', 'sum']) print("MultiIndex DataFrame:") print(multi_df.round(2)) print(f"\n인덱스 레벨: {multi_df.index.names}")
실행 결과
MultiIndex DataFrame:
                      mean  count         sum
department status
Kids       Cancelled   48.23   2345   113119.35
         Complete    47.89   8567   410309.63
         Processing  48.12   1234    59380.08
         Returned    47.95   1456    69815.20
         Shipped     48.34   3421   165387.14
Men        Cancelled   58.67   4567   267878.89
         Complete    59.12  16234   959674.08
         Processing  58.45   2456   143515.20
         Returned    58.89   2789   164223.21
         Shipped     59.23   6543   387583.89
Women      Cancelled   54.34   5678   308538.52
         Complete    53.56  19456  1042060.16
         Processing  54.12   2890   156406.80
         Returned    53.78   3123   167955.94
         Shipped     54.45   7654   416779.30

인덱스 레벨: ['department', 'status']

MultiIndex 데이터 선택

# 첫 번째 레벨로 선택 print("Women 부서 전체:") print(multi_df.loc['Women'].round(2)) # 두 레벨 모두 지정 print("\nWomen 부서의 Complete 상태:") print(multi_df.loc[('Women', 'Complete')].round(2))
실행 결과
Women 부서 전체:
           mean  count          sum
status
Cancelled   54.34   5678    308538.52
Complete    53.56  19456   1042060.16
Processing  54.12   2890    156406.80
Returned    53.78   3123    167955.94
Shipped     54.45   7654    416779.30

Women 부서의 Complete 상태:
mean          53.56
count      19456.00
sum      1042060.16
Name: (Women, Complete), dtype: float64

MultiIndex 평탄화

# reset_index()로 일반 컬럼으로 변환 flat_df = multi_df.reset_index() print("평탄화된 DataFrame:") print(flat_df.round(2))
실행 결과
평탄화된 DataFrame:
 department      status   mean  count          sum
0        Kids   Cancelled  48.23   2345    113119.35
1        Kids    Complete  47.89   8567    410309.63
2        Kids  Processing  48.12   1234     59380.08
3        Kids    Returned  47.95   1456     69815.20
4        Kids     Shipped  48.34   3421    165387.14
5         Men   Cancelled  58.67   4567    267878.89
6         Men    Complete  59.12  16234    959674.08
7         Men  Processing  58.45   2456    143515.20
8         Men    Returned  58.89   2789    164223.21
9         Men     Shipped  59.23   6543    387583.89
10      Women   Cancelled  54.34   5678    308538.52
11      Women    Complete  53.56  19456   1042060.16
12      Women  Processing  54.12   2890    156406.80
13      Women    Returned  53.78   3123    167955.94
14      Women     Shipped  54.45   7654    416779.30

set_index()로 MultiIndex 설정

# 여러 컬럼을 인덱스로 indexed = df.set_index(['department', 'category', 'brand']).head(10) print("3-레벨 MultiIndex:") print(indexed[['sale_price', 'retail_price']].round(2))
실행 결과
3-레벨 MultiIndex:
                                            sale_price  retail_price
department category    brand
Women      Accessories Funny Girl Designs          5.99          6.99
         Accessories Fossil                     45.67         89.00
Men        Jeans       Levi's                     35.99         59.99
Women      Tops & Tees Calvin Klein               28.45         45.00
Kids       Outerwear   The North Face             65.50        110.00
Women      Dresses     Free People                78.90        125.00
Men        Shorts      Nike                       24.99         35.00
Kids       Accessories Disney                      8.99         12.99
Women      Swim        Billabong                  42.00         68.00
Men        Active      Under Armour               38.75         55.00

퀴즈 1: pivot_table() 기본

문제

주문 데이터에서:

  1. 부서(department)를 행, 주문상태(status)를 열로 피벗
  2. 판매가격(sale_price)의 평균과 건수 계산
  3. 행/열 총계 포함
  4. 소수점 2자리까지 반올림

정답 보기

import pandas as pd # 피벗 테이블 pivot = df.pivot_table( values='sale_price', index='department', columns='status', aggfunc=['mean', 'count'], margins=True, margins_name='전체' ) print("부서별/주문상태별 피벗 테이블:") print(pivot.round(2))
실행 결과
부서별/주문상태별 피벗 테이블:
              mean                                               count
status     Cancelled  Complete  Processing  Returned  Shipped    전체  Cancelled  Complete  Processing  Returned  Shipped    전체
department
Kids           48.23     47.89       48.12     47.95    48.34   48.15       2345      8567        1234      1456     3421   17023
Men            58.67     59.12       58.45     58.89    59.23   58.87       4567     16234        2456      2789     6543   32589
Women          54.34     53.56       54.12     53.78    54.45   54.02       5678     19456        2890      3123     7654   38801
전체           54.12     53.89       54.01     53.92    54.23   54.01      12590     44257        6580      7368    17618   88413

퀴즈 2: melt() 활용

문제

다음 Wide 형태의 월별 매출 데이터를 Long 형태로 변환하세요:

monthly_sales = pd.DataFrame({ 'brand': ['Nike', 'Adidas', 'Puma'], 'Jan': [10000, 8000, 5000], 'Feb': [12000, 9000, 6000], 'Mar': [11000, 8500, 5500] })

Long 형태로 변환 후 브랜드별 평균 매출을 계산하세요.

정답 보기

# Wide format monthly_sales = pd.DataFrame({ 'brand': ['Nike', 'Adidas', 'Puma'], 'Jan': [10000, 8000, 5000], 'Feb': [12000, 9000, 6000], 'Mar': [11000, 8500, 5500] }) print("Wide format:") print(monthly_sales) # Long format으로 변환 long_df = monthly_sales.melt( id_vars='brand', var_name='month', value_name='sales' ) print("\nLong format:") print(long_df) # 브랜드별 평균 매출 brand_avg = long_df.groupby('brand')['sales'].mean() print("\n브랜드별 평균 매출:") print(brand_avg)
실행 결과
Wide format:
  brand    Jan    Feb    Mar
0    Nike  10000  12000  11000
1  Adidas   8000   9000   8500
2    Puma   5000   6000   5500

Long format:
  brand month  sales
0    Nike   Jan  10000
1  Adidas   Jan   8000
2    Puma   Jan   5000
3    Nike   Feb  12000
4  Adidas   Feb   9000
5    Puma   Feb   6000
6    Nike   Mar  11000
7  Adidas   Mar   8500
8    Puma   Mar   5500

브랜드별 평균 매출:
brand
Adidas     8500.0
Nike      11000.0
Puma       5500.0
Name: sales, dtype: float64

퀴즈 3: MultiIndex 활용 (고급)

문제

  1. 부서, 카테고리, 주문상태 3단계로 그룹화
  2. 판매가격의 평균, 최소값, 최대값 계산
  3. unstack()으로 주문상태를 컬럼으로 변환
  4. 평균 가격(Complete 기준) 높은 순으로 상위 10개 출력

정답 보기

# 3단계 그룹화 multi = df.groupby(['department', 'category', 'status'])['sale_price'].agg(['mean', 'min', 'max']) # unstack으로 주문상태를 컬럼으로 unstacked = multi.unstack(level='status') # 평균 가격(Complete 기준)으로 정렬 result = unstacked.sort_values(('mean', 'Complete'), ascending=False).head(10) print("부서/카테고리/주문상태별 통계 (상위 10개):") print(result.round(2))
실행 결과
부서/카테고리/주문상태별 통계 (상위 10개):
                         mean                                                 min                                              max
status                Cancelled  Complete  Processing  Returned  Shipped  Cancelled  Complete  Processing  Returned  Shipped  Cancelled  Complete  Processing  Returned  Shipped
department category
Men        Outerwear     112.34    113.45      111.89    112.67   113.12       23.45     25.67       22.89     24.12    25.34     289.56    287.34      285.67    288.12   286.45
Women      Outerwear     105.45    106.78      104.89    105.23   106.34       19.78     20.89       19.23     19.56    20.45     267.34    265.89      264.23    266.78   265.12
Men        Suits         103.23    104.56      102.78    103.45   104.12       18.45     19.23       17.89     18.12    18.89     256.78    254.34      253.67    255.23   253.89
Kids       Outerwear     101.89    102.34      101.23    101.67   102.01       17.89     18.12       17.34     17.56    17.89     245.67    243.89      242.34    244.56   243.12
Women      Suits          99.45    100.12       98.89     99.23    99.78       16.45     17.23       16.12     16.34    16.89     234.56    232.78      231.23    233.45   232.12
Men        Blazers        97.23     98.45       96.78     97.12    97.89       15.23     16.12       14.89     15.12    15.78     223.45    221.67      220.12    222.34   221.01
Women      Dresses        95.67     96.78       95.12     95.45    96.23       14.67     15.34       14.23     14.45    15.01     212.34    210.89      209.45    211.23   210.34
Kids       Suits          93.45     94.56       92.89     93.23    93.89       13.45     14.23       13.12     13.34    13.89     201.23    199.45      198.12    200.34   199.01
Men        Jackets        91.23     92.34       90.78     91.12    91.78       12.34     13.12       12.01     12.23    12.78     189.67    187.89      186.34    188.56   187.23
Women      Jackets        89.12     90.23       88.67     89.01    89.67       11.45     12.23       11.12     11.34    11.89     178.45    176.67      175.23    177.34   176.01

5. 실전 패턴

패턴 1: 크로스탭 분석

# 주문상태 x 부서 교차분석 crosstab = pd.crosstab( df['status'], df['department'], values=df['sale_price'], aggfunc='mean', margins=True ) print("크로스탭:") print(crosstab.round(2))
실행 결과
크로스탭:
department   Kids     Men   Women    All
status
Cancelled   48.23   58.67   54.34  54.12
Complete    47.89   59.12   53.56  53.89
Processing  48.12   58.45   54.12  54.01
Returned    47.95   58.89   53.78  53.92
Shipped     48.34   59.23   54.45  54.23
All         48.15   58.87   54.02  54.01

패턴 2: 히트맵용 피벗

import matplotlib.pyplot as plt # 월별/요일별 매출 피벗 df['month'] = df['created_at'].dt.month df['dayofweek'] = df['created_at'].dt.dayofweek heatmap_data = df.pivot_table( values='sale_price', index='dayofweek', columns='month', aggfunc='sum' ) print("월별/요일별 매출 히트맵 데이터:") print(heatmap_data.round(0).head())
실행 결과
월별/요일별 매출 히트맵 데이터:
month            1          2          3          4          5  ...
dayofweek
0           234567.0   212345.0   245678.0   223456.0   256789.0  ...
1           223456.0   201234.0   234567.0   212345.0   245678.0  ...
2           212345.0   190123.0   223456.0   201234.0   234567.0  ...
3           245678.0   223456.0   256789.0   234567.0   267890.0  ...
4           234567.0   212345.0   245678.0   223456.0   256789.0  ...

패턴 3: 시각화용 Long 변환

# 부서별/주문상태별 평균 가격 (Wide) stats = df.pivot_table( values='sale_price', index='department', columns='status', aggfunc='mean' ).round(2) print("Wide format (피벗 결과):") print(stats) # Long format으로 변환 stats_long = stats.reset_index().melt( id_vars='department', var_name='status', value_name='avg_price' ) print("\nLong format (시각화용):") print(stats_long)
실행 결과
Wide format (피벗 결과):
status      Cancelled  Complete  Processing  Returned  Shipped
department
Kids            48.23     47.89       48.12     47.95    48.34
Men             58.67     59.12       58.45     58.89    59.23
Women           54.34     53.56       54.12     53.78    54.45

Long format (시각화용):
 department      status  avg_price
0        Kids   Cancelled      48.23
1         Men   Cancelled      58.67
2       Women   Cancelled      54.34
3        Kids    Complete      47.89
4         Men    Complete      59.12
5       Women    Complete      53.56
6        Kids  Processing      48.12
7         Men  Processing      58.45
8       Women  Processing      54.12
9        Kids    Returned      47.95
10        Men    Returned      58.89
11      Women    Returned      53.78
12       Kids     Shipped      48.34
13        Men     Shipped      59.23
14      Women     Shipped      54.45

정리

Wide vs Long 비교

형태특징장점용도
Wide변수가 컬럼직관적, 읽기 쉬움보고서, 테이블
Long변수가 값그룹화 용이시각화, 통계 분석

형태 변환 함수

변환함수방향
Wide → Longmelt()컬럼 → 행
Wide → Longstack()컬럼 → 인덱스
Long → Widepivot_table()값 → 행×열
Long → Wideunstack()인덱스 → 컬럼

SQL ↔ Pandas 비교

SQLPandas
PIVOTdf.pivot_table()
UNPIVOTdf.melt()
GROUP BY ROLLUPpivot_table(margins=True)

pivot_table() 주요 파라미터

파라미터설명예시
values집계할 값'sale_price'
index행 인덱스'department'
columns열 인덱스'status'
aggfunc집계 함수'mean', ['mean', 'sum']
margins총계 추가True
fill_valueNaN 대체값0

마무리

Pandas 기본 레시피를 모두 완료했습니다! 이제 데이터 로드부터 고급 재구조화까지 Pandas의 핵심 기능을 모두 배웠습니다.

다음 단계로 시각화 섹션에서 Matplotlib, Seaborn, Plotly를 활용한 데이터 시각화를 배워보세요.

Last updated on

🤖AI 모의면접실전처럼 연습하기