22. 다중 인덱스#

import numpy as np
import pandas as pd
pd.options.display.max_rows = 20
pd.options.display.max_colwidth = 80
pd.options.display.max_columns = 20
np.random.seed(12345)
import matplotlib.pyplot as plt

plt.rc("figure", figsize=(10, 6))
np.set_printoptions(precision=4, suppress=True)

22.1. 시리즈 계층 인덱싱#

다중 인덱스 사용 시리즈

data = pd.Series(np.random.uniform(size=9),
                 index=[["a", "a", "a", "b", "b", "c", "c", "d", "d"],
                        [1, 2, 3, 1, 3, 1, 2, 2, 3]])
data
a  1    0.929616
   2    0.316376
   3    0.183919
b  1    0.204560
   3    0.567725
c  1    0.595545
   2    0.964515
d  2    0.653177
   3    0.748907
dtype: float64
data.index
MultiIndex([('a', 1),
            ('a', 2),
            ('a', 3),
            ('b', 1),
            ('b', 3),
            ('c', 1),
            ('c', 2),
            ('d', 2),
            ('d', 3)],
           )

시리즈 인덱싱/슬라이싱

  • 0-레벨 인덱싱

data["b"]
1    0.204560
3    0.567725
dtype: float64
  • 0-레벨 슬라싱

data["b":"c"]
b  1    0.204560
   3    0.567725
c  1    0.595545
   2    0.964515
dtype: float64
  • 0-레벨 팬시 인덱싱

data.loc[["b", "d"]]
b  1    0.204560
   3    0.567725
d  2    0.653177
   3    0.748907
dtype: float64
  • 0-레벨과 1-레벨 동시 인덱싱/슬라이싱: loc 속성과 쉼표로 구분되는 레벨 별 인덱싱/슬라이싱 적용

data.loc[:, 2]
a    0.316376
c    0.964515
d    0.653177
dtype: float64

스택과 언스택

언스택은 행 인덱스의 가장 깊은 라벨을 열 인덱스의 가장 깊은 라벨로 변환한다.

data.unstack()
1 2 3
a 0.929616 0.316376 0.183919
b 0.204560 NaN 0.567725
c 0.595545 0.964515 NaN
d NaN 0.653177 0.748907

스택은 열 인덱스의 가장 깊은 라벨을 행 인덱스의 가장 깊은 라벨로 변환한다.

data.unstack().stack()
a  1    0.929616
   2    0.316376
   3    0.183919
b  1    0.204560
   3    0.567725
c  1    0.595545
   2    0.964515
d  2    0.653177
   3    0.748907
dtype: float64

22.2. 데이터프레임 계층 인덱싱#

다중 행/열 인덱스 사용 데이터프레임

frame = pd.DataFrame(np.arange(12).reshape((4, 3)),
                     index=[["a", "a", "b", "b"], [1, 2, 1, 2]],
                     columns=[["Ohio", "Ohio", "Colorado"],
                              ["Green", "Red", "Green"]])
frame
Ohio Colorado
Green Red Green
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11

행과 열 인덱스의 각 레벨에 이름 지정

frame.index.names = ["key1", "key2"]
frame.columns.names = ["state", "color"]
frame
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11

행 인덱스는 2개의 레벨로 구성

frame.index.nlevels
2

기본 인덱싱은 열 인덱스의 0-레벨에 적용

frame["Ohio"]
color Green Red
key1 key2
a 1 0 1
2 3 4
b 1 6 7
2 9 10

22.2.1. 다중 인덱스 레벨 교환과 인덱스 라벨 정렬#

레벨 교환

  • 레벨 이름 활용

frame.swaplevel("key1", "key2")
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
2 a 3 4 5
1 b 6 7 8
2 b 9 10 11
  • 정수 레벨 활용

frame
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
frame.swaplevel(0, 1)
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
2 a 3 4 5
1 b 6 7 8
2 b 9 10 11

인덱스 라벨 정렬

먼저 1-레벨의 라벨을 정렬한다.

frame.sort_index(level=1)
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
b 1 6 7 8
a 2 3 4 5
b 2 9 10 11

레벨을 교환한다.

frame.swaplevel(0, 1).sort_index(level=0)
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
b 6 7 8
2 a 3 4 5
b 9 10 11

22.2.2. 레벨 단위 그룹화#

frame.groupby(level="key2").sum()
state Ohio Colorado
color Green Red Green
key2
1 6 8 10
2 12 14 16

레벨과 축을 활용한 그룹화

  • level="color": 열 인덱스의 1-레벨 기준

  • axis="columns": 열에 사용된 라벨 단위로 그룹화

frame
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
frame.groupby(level="color", axis="columns").sum()
color Green Red
key1 key2
a 1 2 1
2 8 4
b 1 14 7
2 20 10

22.2.3. 인덱스 지정과 초기화#

frame = pd.DataFrame({"a": range(7), "b": range(7, 0, -1),
                      "c": ["one", "one", "one", "two", "two",
                            "two", "two"],
                      "d": [0, 1, 2, 0, 1, 2, 3]})
frame
a b c d
0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
3 3 4 two 0
4 4 3 two 1
5 5 2 two 2
6 6 1 two 3
  • set_index() 메서드: 열의 특정 라벨을 이용하여 (멀티)인덱스를 지정한다.

frame2 = frame.set_index(["c", "d"])
frame2
a b
c d
one 0 0 7
1 1 6
2 2 5
two 0 3 4
1 4 3
2 5 2
3 6 1

인덱스에 사용된 열을 그대로 둘 수도 있다.

frame.set_index(["c", "d"], drop=False)
a b c d
c d
one 0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
two 0 3 4 two 0
1 4 3 two 1
2 5 2 two 2
3 6 1 two 3
  • reset_index() 메서드: (멀티)인덱스를 열로 변환시키고 정수 인덱스를 지정

frame2.reset_index()
c d a b
0 one 0 0 7
1 one 1 1 6
2 one 2 2 5
3 two 0 3 4
4 two 1 4 3
5 two 2 5 2
6 two 3 6 1
  • drop=True 옵션: 인덱스로 사용된 라벨을 모두 삭제

frame2.reset_index(drop=True)
a b
0 0 7
1 1 6
2 2 5
3 3 4
4 4 3
5 5 2
6 6 1

22.3. 모양 변환#

22.3.1. 항목 재배열#

  • 스택

  • 언스택

data = pd.DataFrame(np.arange(6).reshape((2, 3)),
                    index=pd.Index(["Ohio", "Colorado"], name="state"),
                    columns=pd.Index(["one", "two", "three"],
                    name="number"))
data
number one two three
state
Ohio 0 1 2
Colorado 3 4 5
result = data.stack()
result
state     number
Ohio      one       0
          two       1
          three     2
Colorado  one       3
          two       4
          three     5
dtype: int64
result.unstack()
number one two three
state
Ohio 0 1 2
Colorado 3 4 5
result.unstack(level=0)
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
result.unstack(level="state")
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
s1 = pd.Series([0, 1, 2, 3], index=["a", "b", "c", "d"], dtype="Int64")
s2 = pd.Series([4, 5, 6], index=["c", "d", "e"], dtype="Int64")
data2 = pd.concat([s1, s2], keys=["one", "two"])

data2
one  a    0
     b    1
     c    2
     d    3
two  c    4
     d    5
     e    6
dtype: Int64
data2.unstack()
a b c d e
one 0 1 2 3 <NA>
two <NA> <NA> 4 5 6
data2.unstack().stack()
one  a    0
     b    1
     c    2
     d    3
two  c    4
     d    5
     e    6
dtype: Int64
  • dropna=False 키워드 인자: 항목 재배치 과정에서 발생하는 결측치를 그대로 둚

data2.unstack().stack(dropna=False)
one  a       0
     b       1
     c       2
     d       3
     e    <NA>
two  a    <NA>
     b    <NA>
     c       4
     d       5
     e       6
dtype: Int64

레벨 활용 스택/언스택

df = pd.DataFrame({"left": result, "right": result + 5},
                  columns=pd.Index(["left", "right"], name="side"))
df
side left right
state number
Ohio one 0 5
two 1 6
three 2 7
Colorado one 3 8
two 4 9
three 5 10
df.unstack(level="state")
side left right
state Ohio Colorado Ohio Colorado
number
one 0 3 5 8
two 1 4 6 9
three 2 5 7 10
df.unstack(level="state").stack(level="side")
state Colorado Ohio
number side
one left 3 0
right 8 5
two left 4 1
right 9 6
three left 5 2
right 10 7

테이블 데이텃세에서 긴 모양 데이터프레임 생성

예제: 거시경제 데이터셋 활용

  • 데이터셋 다운로드

base_url = "https://raw.githubusercontent.com/codingalzi/datapy/master/jupyter-book/examples/"
file = "macrodata.csv"

data = pd.read_csv(base_url + file)

1959년부터 2009년까지 총 203개의 분기quarter별 데이터 샘플 포함

data.shape
(203, 14)
  • head() 메서드: 처음 5행 확인. 열 라벨도 확인 가능

data.head()
year quarter realgdp realcons realinv realgovt realdpi cpi m1 tbilrate unemp pop infl realint
0 1959.0 1.0 2710.349 1707.4 286.898 470.045 1886.9 28.98 139.7 2.82 5.8 177.146 0.00 0.00
1 1959.0 2.0 2778.801 1733.7 310.859 481.301 1919.7 29.15 141.7 3.08 5.1 177.830 2.34 0.74
2 1959.0 3.0 2775.488 1751.8 289.226 491.260 1916.4 29.35 140.5 3.82 5.3 178.657 2.74 1.09
3 1959.0 4.0 2785.204 1753.7 299.356 484.052 1931.3 29.37 140.0 4.33 5.6 179.386 0.27 4.06
4 1960.0 1.0 2847.699 1770.5 331.722 462.199 1955.5 29.54 139.6 3.50 5.2 180.007 2.31 1.19

열 라벨은 거시경제 지표 14개

data.columns
Index(['year', 'quarter', 'realgdp', 'realcons', 'realinv', 'realgovt',
       'realdpi', 'cpi', 'm1', 'tbilrate', 'unemp', 'pop', 'infl', 'realint'],
      dtype='object')

열 일부 데이터만 사용

data = data.loc[:, ["year", "quarter", "realgdp", "infl", "unemp"]]
data.head()
year quarter realgdp infl unemp
0 1959.0 1.0 2710.349 0.00 5.8
1 1959.0 2.0 2778.801 2.34 5.1
2 1959.0 3.0 2775.488 2.74 5.3
3 1959.0 4.0 2785.204 0.27 5.6
4 1960.0 1.0 2847.699 2.31 5.2
  • 기간 인덱스: pd.PeriodIndex 객체 활용. 년도(year)와 분기(quarter) 결합

주의사항: pop() 메서드를 사용하기에 data 데이터프레임에서 해당 열 삭제됨

periods = pd.PeriodIndex(year=data.pop("year"),       # pop()
                         quarter=data.pop("quarter"), # pop()
                         name="date")                 # 인덱스 이름
periods
PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
             '1960Q3', '1960Q4', '1961Q1', '1961Q2',
             ...
             '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
             '2008Q4', '2009Q1', '2009Q2', '2009Q3'],
            dtype='period[Q-DEC]', name='date', length=203)
  • to_timestamp("D"): 일 단위의 DatetimeIndex로 변환

data.index = periods.to_timestamp("D")
data.index
DatetimeIndex(['1959-01-01', '1959-04-01', '1959-07-01', '1959-10-01',
               '1960-01-01', '1960-04-01', '1960-07-01', '1960-10-01',
               '1961-01-01', '1961-04-01',
               ...
               '2007-04-01', '2007-07-01', '2007-10-01', '2008-01-01',
               '2008-04-01', '2008-07-01', '2008-10-01', '2009-01-01',
               '2009-04-01', '2009-07-01'],
              dtype='datetime64[ns]', name='date', length=203, freq='QS-OCT')
data.head()
realgdp infl unemp
date
1959-01-01 2710.349 0.00 5.8
1959-04-01 2778.801 2.34 5.1
1959-07-01 2775.488 2.74 5.3
1959-10-01 2785.204 0.27 5.6
1960-01-01 2847.699 2.31 5.2
  • 리인덱싱

data = data.reindex(columns=["realgdp", "infl", "unemp"])

열 인덱스에 이름 지정

data.columns.name = "item"

data.head()
item realgdp infl unemp
date
1959-01-01 2710.349 0.00 5.8
1959-04-01 2778.801 2.34 5.1
1959-07-01 2775.488 2.74 5.3
1959-10-01 2785.204 0.27 5.6
1960-01-01 2847.699 2.31 5.2

항목 재배열: 스택/언스택 활용

data.stack()
date        item   
1959-01-01  realgdp     2710.349
            infl           0.000
            unemp          5.800
1959-04-01  realgdp     2778.801
            infl           2.340
                         ...    
2009-04-01  infl           3.370
            unemp          9.200
2009-07-01  realgdp    12990.341
            infl           3.560
            unemp          9.600
Length: 609, dtype: float64
data.stack().reset_index()
date item 0
0 1959-01-01 realgdp 2710.349
1 1959-01-01 infl 0.000
2 1959-01-01 unemp 5.800
3 1959-04-01 realgdp 2778.801
4 1959-04-01 infl 2.340
... ... ... ...
604 2009-04-01 infl 3.370
605 2009-04-01 unemp 9.200
606 2009-07-01 realgdp 12990.341
607 2009-07-01 infl 3.560
608 2009-07-01 unemp 9.600

609 rows × 3 columns

long_data = (data.stack()
             .reset_index()
             .rename(columns={0: "value"}))
long_data[:10]
date item value
0 1959-01-01 realgdp 2710.349
1 1959-01-01 infl 0.000
2 1959-01-01 unemp 5.800
3 1959-04-01 realgdp 2778.801
4 1959-04-01 infl 2.340
5 1959-04-01 unemp 5.100
6 1959-07-01 realgdp 2775.488
7 1959-07-01 infl 2.740
8 1959-07-01 unemp 5.300
9 1959-10-01 realgdp 2785.204

22.3.2. 피버팅#

긴 모양 데이터프레임을 넓은 모양 데이터프레임으로 변환하는 과정을 살펴 본다.

한 개의 열로 구성된 갑을 사용하는 피버팅

pivoted = long_data.pivot(index="date", columns="item", values="value")

pivoted.head()
item infl realgdp unemp
date
1959-01-01 0.00 2710.349 5.8
1959-04-01 2.34 2778.801 5.1
1959-07-01 2.74 2775.488 5.3
1959-10-01 0.27 2785.204 5.6
1960-01-01 2.31 2847.699 5.2

여러 개의 열로 구성된 갑을 사용하는 피버팅

long_data["value2"] = np.random.standard_normal(len(long_data))
long_data[:10]
date item value value2
0 1959-01-01 realgdp 2710.349 1.248804
1 1959-01-01 infl 0.000 0.774191
2 1959-01-01 unemp 5.800 -0.319657
3 1959-04-01 realgdp 2778.801 -0.624964
4 1959-04-01 infl 2.340 1.078814
5 1959-04-01 unemp 5.100 0.544647
6 1959-07-01 realgdp 2775.488 0.855588
7 1959-07-01 infl 2.740 1.343268
8 1959-07-01 unemp 5.300 -0.267175
9 1959-10-01 realgdp 2785.204 1.793095

값으로 사용되는 열 별로 열 인덱스 지정. 따라서 다중 인덱스가 열 인덱스로 사용됨.

pivoted = long_data.pivot(index="date", columns="item")
pivoted.head()
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-01-01 0.00 2710.349 5.8 0.774191 1.248804 -0.319657
1959-04-01 2.34 2778.801 5.1 1.078814 -0.624964 0.544647
1959-07-01 2.74 2775.488 5.3 1.343268 0.855588 -0.267175
1959-10-01 0.27 2785.204 5.6 -0.652929 1.793095 -1.886837
1960-01-01 2.31 2847.699 5.2 0.644448 1.059626 -0.007799
pivoted["value"].head()
item infl realgdp unemp
date
1959-01-01 0.00 2710.349 5.8
1959-04-01 2.34 2778.801 5.1
1959-07-01 2.74 2775.488 5.3
1959-10-01 0.27 2785.204 5.6
1960-01-01 2.31 2847.699 5.2

피버팅은 set_index() 메서드와 unstack() 메서드를 연속적으로 적용하는 것과 동일한 결과를 낸다.

long_data.head(10)
date item value value2
0 1959-01-01 realgdp 2710.349 1.248804
1 1959-01-01 infl 0.000 0.774191
2 1959-01-01 unemp 5.800 -0.319657
3 1959-04-01 realgdp 2778.801 -0.624964
4 1959-04-01 infl 2.340 1.078814
5 1959-04-01 unemp 5.100 0.544647
6 1959-07-01 realgdp 2775.488 0.855588
7 1959-07-01 infl 2.740 1.343268
8 1959-07-01 unemp 5.300 -0.267175
9 1959-10-01 realgdp 2785.204 1.793095
long_data.set_index(["date", "item"])
value value2
date item
1959-01-01 realgdp 2710.349 1.248804
infl 0.000 0.774191
unemp 5.800 -0.319657
1959-04-01 realgdp 2778.801 -0.624964
infl 2.340 1.078814
... ... ... ...
2009-04-01 infl 3.370 0.904582
unemp 9.200 1.374036
2009-07-01 realgdp 12990.341 1.132698
infl 3.560 0.314682
unemp 9.600 1.515960

609 rows × 2 columns

unstacked = long_data.set_index(["date", "item"]).unstack(level="item")
unstacked.head()
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-01-01 0.00 2710.349 5.8 0.774191 1.248804 -0.319657
1959-04-01 2.34 2778.801 5.1 1.078814 -0.624964 0.544647
1959-07-01 2.74 2775.488 5.3 1.343268 0.855588 -0.267175
1959-10-01 0.27 2785.204 5.6 -0.652929 1.793095 -1.886837
1960-01-01 2.31 2847.699 5.2 0.644448 1.059626 -0.007799

22.3.3. 언피버팅#

넓은 모양 데이터프레임을 긴 모양 데이터프레임으로 변환하는 과정을 살펴 본다.

df = pd.DataFrame({"key": ["foo", "bar", "baz"],
                   "A": [1, 2, 3],
                   "B": [4, 5, 6],
                   "C": [7, 8, 9]})
df
key A B C
0 foo 1 4 7
1 bar 2 5 8
2 baz 3 6 9
  • set_index() 메서드와 stack() 메서드를 연속적용하면 여러 개의 열이 하나의 열로 구성된 긴 모양 데이터프레임 생성 가능

df.set_index(["key"])
A B C
key
foo 1 4 7
bar 2 5 8
baz 3 6 9
df.set_index(["key"]).stack()
key   
foo  A    1
     B    4
     C    7
bar  A    2
     B    5
     C    8
baz  A    3
     B    6
     C    9
dtype: int64
reshaped = df.set_index(["key"]).stack().reset_index()
reshaped
key level_1 0
0 foo A 1
1 foo B 4
2 foo C 7
3 bar A 2
4 bar B 5
5 bar C 8
6 baz A 3
7 baz B 6
8 baz C 9
reshaped.columns = ["key", "variable", "value"]
reshaped.sort_values("key")
key variable value
3 bar A 2
4 bar B 5
5 bar C 8
6 baz A 3
7 baz B 6
8 baz C 9
0 foo A 1
1 foo B 4
2 foo C 7
  • pd.melt() 함수: 위 과정을 한 번에 처리한다.

    • id_vars 키워드 인자: 그룹 식별자 사용될 값들의 열 지정.

melted = pd.melt(df, id_vars="key")
melted
key variable value
0 foo A 1
1 bar A 2
2 baz A 3
3 foo B 4
4 bar B 5
5 baz B 6
6 foo C 7
7 bar C 8
8 baz C 9
melted = melted.sort_values("key")

melted
key variable value
1 bar A 2
4 bar B 5
7 bar C 8
2 baz A 3
5 baz B 6
8 baz C 9
0 foo A 1
3 foo B 4
6 foo C 7
  • pivot() 메서드를 적용하면 다시 넓은 모양 데이터프레임으로 변환

reshaped = melted.pivot(index="key", columns="variable",
                        values="value")
reshaped
variable A B C
key
bar 2 5 8
baz 3 6 9
foo 1 4 7
reshaped.reset_index()
variable key A B C
0 bar 2 5 8
1 baz 3 6 9
2 foo 1 4 7
  • value_vars=["A", "B"] 키워드 인자: value 열에 사용되는 값과 연관된 열 지정. 여기서는 A, B열의 값만 사용.

pd.melt(df, id_vars="key", value_vars=["A", "B"])
key variable value
0 foo A 1
1 bar A 2
2 baz A 3
3 foo B 4
4 bar B 5
5 baz B 6
  • id_vars 키워드 인자 생략: 그룹 식별자 사용하지 않음

pd.melt(df, value_vars=["A", "B", "C"])
variable value
0 A 1
1 A 2
2 A 3
3 B 4
4 B 5
5 B 6
6 C 7
7 C 8
8 C 9
pd.melt(df, value_vars=["key", "A", "B"])
variable value
0 key foo
1 key bar
2 key baz
3 A 1
4 A 2
5 A 3
6 B 4
7 B 5
8 B 6