30 분 소요

데이터 준비하기: 조인, 병합, 변형

대부분의 경우 데이터는 여러 파일이나 데이터베이스 혹은 분석하기 쉽지 않은 형태로 기록되어 있다. 여기서는 데이터를 합치고, 재배열할 수 있는 도구들을 살펴보자.

먼저 데이터를 병합하거나 변환하는 과정에서 사용되는 판다스의 멀티 인덱스의 개념을 알아보고 이를 활용하여 데이터를 다듬는 과정을 심도 있게 살펴볼 것이다.

멀티 인덱스

멀티 인덱스는 판다스의 중요한 기능인데 축에 대해 다중(둘 이상) 인덱스 단계를 지정할 수 있도록 해준다. 약간 추상적으로 말하면, 높은 차원의 데이터를 낮은 차원의 형식으로 다룰 수 있게 해주는 기능이다. 간단한 예제를 하나 살펴보자. 우선 리스트의 리스트(또는 배열)를 인덱스로 하는 시리즈를 하나 생성하자.

data = pd.Series(np.random.randn(9),
                 index=[['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'],
                        [1, 2, 3, 1, 3, 1, 2, 2, 3]])

data
a  1    0.860255
   2    0.990249
   3   -1.681696
b  1   -1.016254
   3    1.249738
c  1   -0.507521
   2   -0.063223
d  2   -0.328800
   3   -0.163897
dtype: float64

지금 생성한 객체가 MultiIndex를 인덱스로 하는 시리즈인데, 인덱스의 계층을 보여주고 있다. 바로 위 단계의 인덱스를 이용해서 하위 계층을 직접 접근할 수 있다.

data.index
MultiIndex([('a', 1),
            ('a', 2),
            ('a', 3),
            ('b', 1),
            ('b', 3),
            ('c', 1),
            ('c', 2),
            ('d', 2),
            ('d', 3)],
           )

계층적으로 인덱싱된 객체는 데이터의 부분집합을 부분적 인덱싱으로 접근partial indexing하는 것이 가능하다.

data['b']
1   -1.016254
3    1.249738
dtype: float64
data['b':'c']
b  1   -1.016254
   3    1.249738
c  1   -0.507521
   2   -0.063223
dtype: float64
data.loc[['b', 'd']]
b  1   -1.016254
   3    1.249738
d  2   -0.328800
   3   -0.163897
dtype: float64

하위 계층의 객체를 선택하는 것도 가능하다.

data.loc[:, 2]
a    0.990249
c   -0.063223
d   -0.328800
dtype: float64

계층적인 인덱스는 데이터를 재형성하고 피벗테이블 생성 같은 그룹 기반의 작업을 할 때 중요하게 사용된다. 예를 들어 위에서 만든 시리즈 객체에 unstack() 메서드를 사용해서 데이터를 데이터프레임으로 새롭게 배열할 수도 있다.

print(data.unstack())
print('\n')
print(type(data.unstack()))
          1         2         3
a  0.860255  0.990249 -1.681696
b -1.016254       NaN  1.249738
c -0.507521 -0.063223       NaN
d       NaN -0.328800 -0.163897


<class 'pandas.core.frame.DataFrame'>

unstack()의 반대 작업은 stack() 메서드로 수행한다.

print(data.unstack().stack())
print('\n')
print(type(data.unstack().stack()))
a  1    0.860255
   2    0.990249
   3   -1.681696
b  1   -1.016254
   3    1.249738
c  1   -0.507521
   2   -0.063223
d  2   -0.328800
   3   -0.163897
dtype: float64


<class 'pandas.core.series.Series'>

stack()unstack() 메서드는 후에 더 자세히 알아보기로 한다.

데이터프레임에서는 두 축 모두 계층적 인덱스를 가질 수 있다.

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

칼럼의 부분집합을 부분적인 인덱싱으로 접근하는 것도 칼럼에 대한 부분적 인덱싱과 비슷하게 사용하면 된다.

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

참고로 MultiIndex는 따로 생성한 다음에 재사용 가능하고 다음처럼 재지정도 가능하다.

multi_columns = pd.MultiIndex.from_arrays([['NS', 'P', 'P'], ['Green', 'Blue', 'Green']],
                          names=['state', 'color'])

frame.columns = multi_columns

frame
state NS P
color Green Blue Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11

계층의 순서를 바꾸고 정렬하기

계층적 인덱스에서 계층의 순서를 바꾸거나 지정된 계층에 따라 데이터를 정렬해야 하는 경우가 있을 수 있다. swaplevel()은 넘겨받은 두 개의 계층 번호나 이름이 뒤바뀐 새로운 객체를 반환한다(하지만 데이터는 변경되지 않는다).

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('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

반면 sort_index() 메서드는 단일 계층에 속한 데이터를 정렬한다. swaplevel()을 이용해서 계층을 바꿀 때 sort_index()를 사용해서 결과가 사전적으로 정렬되도록 만드는 것도 드물지 않은 일이다.

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

객체가 계층적 인덱스로 상위 계층부터 사전식으로 정렬되어 있다면(sort_index(level=0)이나 sort_index()의 결과처럼) 데이터를 선택하는 성능이 훨씬 좋아진다.

계층별 요약 통계

데이터프레임과 시리즈의 많은 기술 통계와 요약 통계는 level 옵션을 가지고 있는데, 어떤 한 축에 대해 합을 구하고 싶은 단계를 지정할 수 있는 옵션이다. 앞에서 살펴본 데이터프레임에서 로우나 칼럼을 아래처럼 계층별로 합할 수 있다.

frame.sum(level='key2')
C:\Users\Sangjin\AppData\Local\Temp\ipykernel_32044\2004046222.py:1: FutureWarning: Using the level keyword in DataFrame and Series aggregations is deprecated and will be removed in a future version. Use groupby instead. df.sum(level=1) should use df.groupby(level=1).sum().
  frame.sum(level='key2')
state Ohio Colorado
color Green Red Green
key2
1 6 8 10
2 12 14 16
frame.sum(level='color', axis=1)
C:\Users\Sangjin\AppData\Local\Temp\ipykernel_32044\4133796543.py:1: FutureWarning: Using the level keyword in DataFrame and Series aggregations is deprecated and will be removed in a future version. Use groupby instead. df.sum(level=1) should use df.groupby(level=1).sum().
  frame.sum(level='color', axis=1)
color Green Red
key1 key2
a 1 2 1
2 8 4
b 1 14 7
2 20 10

이는 내부적으로 다음처럼 판다스의 groupby 기능을 이용해서 구현되었는데, 판다스에서는 같은 결과를 출력하는 다음을 권고한다. 또한 groupby에 대해서는 후에 알아볼 것이다.

frame.groupby(level='key2').sum()
state Ohio Colorado
color Green Red Green
key2
1 6 8 10
2 12 14 16
frame.groupby(axis=1, level='color').sum()
color Green Red
key1 key2
a 1 2 1
2 8 4
b 1 14 7
2 20 10

데이터프레임의 칼럼 사용하기

데이터프레임에서 로우의 인덱스로 하나 이상의 칼럼을 사용하는 것은 드물지 않은 일이다. 아니면 로우의 인덱스를 데이터프레임의 칼럼으로 옮기고 싶을 것이다. 다음은 예제 데이터프레임이다.

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() 함수는 set_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

데이터 합치기

데이터가 여러 군데 나누어져 있을 때 하나로 합치거나 데이터를 연결해야 하는 경우가 있다. 판다스 객체에 저장된 데이터는 다음을 통해 여러 가지 방법으로 합칠 수 있다.

  • pandas.merge()는 하나 이상의 키를 기준으로 데이터프레임의 로우를 합친다. SQL이나 다른 관계형 데이터베이스의 join 연산과 유사하다.

  • pandas.concat()은 하나의 축을 따라 객체를 이어붙인다.

  • combine_first 인스턴스 메서드는 두 객체를 포개서 한 객체에서 누락된 데이터를 다른 객체에 있는 값으로 채울 수 있도록 한다.

각각의 데이터를 합치는 방법에 대해 다양한 예제와 함께 살펴보자.

데이터베이스 스타일로 데이터프레임 합치기

병합(머지merge)이나 조인join연산은 관계형 데이터베이스의 핵심적인 연산인데, 하나 이상의 를 사용해서 데이터 집합의 로우를 합친다. 여기서 말하는 키(key)란 기준이 되는 칼럼이나 인덱스를 뜻한다. 키가 되는 칼럼이나 인덱스는 반드시 양쪽 데이터프레임에 모두 존재해야 한다. 판다스의 merge()함수를 이용해서 이런 알고리즘을 데이터에 적용할 수 있다.

df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
                    'data1': range(7)})
df2 = pd.DataFrame({'key': ['a', 'b', 'd'],
                    'data2': range(3)})
print(df1)
print('\n')
print(df2)
  key  data1
0   b      0
1   b      1
2   a      2
3   c      3
4   a      4
5   a      5
6   b      6


  key  data2
0   a      0
1   b      1
2   d      2

위 예제는 다대일의 경우다. df1의 데이터는 key 칼럼에 여러 개의 a, b 로우를 가지고 있고 df2의 데이터는 key 칼럼에 유일한 로우를 가지고 있다. 이 객체에 대해 merge() 함수를 호출하면 다음과 같은 결과를 얻는다.

pd.merge(df1, df2)
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0

위의 예시에서는 어떤 칼럼을 기준으로 병합할 것인지 명시하지 않았는데, merge() 함수는 중복된 칼럼 이름을 키로 사용한다(위 예에서는 key 칼럼). 하지만 명시적으로 지정해주는 습관을 들이는 게 좋다.

pd.merge(df1, df2, on='key')
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0

만약 두 객체에 중복된 칼럼 이름이 하나도 없다면 따로 지정해주면 된다.

df3 = pd.DataFrame({'lkey': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
                    'data1': range(7)})
df4 = pd.DataFrame({'rkey': ['a', 'b', 'd'],
                    'data2': range(3)})

pd.merge(df3, df4, left_on='lkey', right_on='rkey')
lkey data1 rkey data2
0 b 0 b 1
1 b 1 b 1
2 b 6 b 1
3 a 2 a 0
4 a 4 a 0
5 a 5 a 0

결과를 잘 살펴보면 ‘c’와 ‘d’에 해당하는 값이 빠진 것을 알 수 있다. merge() 함수는 기본적으로 내부 조인inner join을 수행하여 교집합인 결과를 반환한다. how 인자로 ‘left’, ‘right’, ‘outer’를 넘겨서 각각 왼쪽 조인, 오른쪽 조인, 외부 조인을 수행할 수도 있다. 외부 조인은 합집합인 결과를 반환하고 왼쪽 조인과 오른쪽 조인은 각각 왼쪽 또는 오른쪽의 모든 로우를 포함하는 결과를 반환한다.

pd.merge(df1, df2, on='key', how='outer')
key data1 data2
0 b 0.0 1.0
1 b 1.0 1.0
2 b 6.0 1.0
3 a 2.0 0.0
4 a 4.0 0.0
5 a 5.0 0.0
6 c 3.0 NaN
7 d NaN 2.0

다음은 how 옵션에 따라 조인 연산이 어떻게 동작하는지 요약한 것이다.

옵션 동작
'inner' 양쪽 테이블 모두에 존재하는 키 조합을 사용한다.
'left' 왼쪽 테이블에 존재하는 모든 키 조합을 사용한다.
'right' 오른쪽 테이블에 존재하는 모든 키 조합을 사용한다.
'outer' 양쪽 테이블에 존재하는 모든 키 조합을 사용한다.

다대다 병합은 잘 정의되어 있긴 하지만 직관적이지는 않다.

df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
                    'data1': range(6)})
df2 = pd.DataFrame({'key': ['a', 'b', 'a', 'b', 'd'],
                    'data2': range(5)})

print(df1)
print('\n')
print(df2)
  key  data1
0   b      0
1   b      1
2   a      2
3   c      3
4   a      4
5   b      5


  key  data2
0   a      0
1   b      1
2   a      2
3   b      3
4   d      4
pd.merge(df1, df2, on='key', how='left')
key data1 data2
0 b 0 1.0
1 b 0 3.0
2 b 1 1.0
3 b 1 3.0
4 a 2 0.0
5 a 2 2.0
6 c 3 NaN
7 a 4 0.0
8 a 4 2.0
9 b 5 1.0
10 b 5 3.0

다대다 조인은 두 로우의 데카르트곱을 반환한다. 왼쪽 데이터프레임에는 3개의 ‘b’ 로우가 있고 오른쪽에는 2개의 ‘b’ 로우가 있으며, 결과는 6개의 ‘b’ 로우가 된다. 조인 메서드는 결과에 나타나는 구별되는 키에 대해서만 적용된다.

pd.merge(df1, df2, how='inner')
key data1 data2
0 b 0 1
1 b 0 3
2 b 1 1
3 b 1 3
4 b 5 1
5 b 5 3
6 a 2 0
7 a 2 2
8 a 4 0
9 a 4 2

여러 개의 키를 기준으로 병합하려면 칼럼 이름이 담긴 리스트를 넘기면 된다.

left = pd.DataFrame({'key1': ['foo', 'foo', 'bar'],
                     'key2': ['one', 'two', 'one'],
                     'lval': [1, 2, 3]})
right = pd.DataFrame({'key1': ['foo', 'foo', 'bar', 'bar'],
                      'key2': ['one', 'one', 'one', 'two'],
                      'rval': [4, 5, 6, 7]})
print(left)
print('\n')
print(right)
  key1 key2  lval
0  foo  one     1
1  foo  two     2
2  bar  one     3


  key1 key2  rval
0  foo  one     4
1  foo  one     5
2  bar  one     6
3  bar  two     7
pd.merge(left, right, on=['key1', 'key2'], how='outer')
key1 key2 lval rval
0 foo one 1.0 4.0
1 foo one 1.0 5.0
2 foo two 2.0 NaN
3 bar one 3.0 6.0
4 bar two NaN 7.0

merge() 메서드의 종류에 따라 어떤 키 조합이 결과로 반환되는지 알려면 실제 구현과는 조금 다르지만 여러 개의 키가 들어 있는 튜플의 배열이 단일 조인키로 사용된다고 생각하면 된다.

칼럼과 칼럼을 조인할 때 전달한 데이터프레임 객체의 인덱스는 무시된다.

병합 연산에서 고려해야 할 마지막 사항은 겹치는 칼럼 이름에 대한 처리다. 다음에서 살펴보겠지만 축 이름을 변경해서 수동으로 칼럼 이름이 겹치게 할 수도 있고, merge() 함수에 있는 suffixes 인자로 두 데이터프레임 객체에서 겹치는 칼럼 이름 뒤에 붙일 문자열을 지정해줄 수도 있다.

pd.merge(left, right, on='key1')
key1 key2_x lval key2_y rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
pd.merge(left, right, on='key1', suffixes=('_left', '_right'))
key1 key2_left lval key2_right rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7

다음은 merge() 함수의 인자를 정리한 것이다.

인자 동작
left 병합하려는 데이터프레임 중 왼쪽에 위치한 데이터프레임
right 병합하려는 데이터프레임 중 오른쪽에 위치한 데이터프레임
how 조인 방법. inner', 'outer', 'left', 'right'. 기본값은 'inner'
on 조인하려는 칼럼 이름. 반드시 두 데이터프레임 객체 모두에 존재하는 이럼이어야 한다. 만약 명시되지 않고 다른 조인키도 주어지지 않으면 left와 right에서 공통되는 칼럼을 조인키로 사용한다.
left_on 조인키로 사용할 left 데이터프레임의 칼럼
right_on 조인키로 사용할 right 데이터프레임의 칼럼
left_index 조인키로 사용할 left 데이터프레임의 인덱스 로우(다중 인덱스일 경우 키)
right_index 조인키로 사용할 right 데이터프레임의 인덱스 로우(다중 인덱스일 경우 키)
sort 조인키에 따라 병합된 데이터를 사전순으로 정렬. 기본값은 True. 대용량 데이터의 경우 False로 하면 성능상의 이득을 얻을 수 있다.
suffixes 칼럼 이름이 겹칠 경우 각 칼럼 이름 뒤에 붙일 문자열의 튜플. 기본값은 ('_x', '_y').
copy False일 경우. 예외적인 경우에 데이터가 결과로 복사되지 않도록 한다. 기본값은 항상 복사가 이루어진다.
indicator merge라는 이름의 특별한 칼럼을 추가하여 각 로우의 소스가 어디인지 나타낸다. 'left_only', 'right_only', 'both' 값을 가진다.

로우 인덱스 병합하기

병합하려는 키가 데이터프레임의 인덱스일 경우가 있다. 이런 경우에는 left_index=True 혹은 right_index=True 옵션(또는 둘 다)을 지정해서 해당 인덱스를 병합키로 사용할 수 있다.

left1 = pd.DataFrame({'key': ['a', 'b', 'a', 'a', 'b', 'c'],
                      'value': range(6)})
right1 = pd.DataFrame({'group_val': [3.5, 7]}, index=['a', 'b'])

print(left1)
print('\n')
print(right1)
  key  value
0   a      0
1   b      1
2   a      2
3   a      3
4   b      4
5   c      5


   group_val
a        3.5
b        7.0
pd.merge(left1, right1, left_on='key', right_index=True)
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0

병합은 기본적으로 교집합을 구하지만 외부 조인을 실행해서 합집합을 구할 수도 있다.

pd.merge(left1, right1, left_on='key', right_index=True, how='outer')
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0
5 c 5 NaN

멀티 인덱스 데이터는 암묵적으로 여러 키를 병합하는 것이라 약간 복잡하다.

lefth = pd.DataFrame({'key1': ['Ohio', 'Ohio', 'Ohio',
                               'Nevada', 'Nevada'],
                      'key2': [2000, 2001, 2002, 2001, 2002],
                      'data': np.arange(5.)})
righth = pd.DataFrame(np.arange(12).reshape((6, 2)),
                      index=[['Nevada', 'Nevada', 'Ohio', 'Ohio',
                              'Ohio', 'Ohio'],
                             [2001, 2000, 2000, 2000, 2001, 2002]],
                      columns=['event1', 'event2'])
print(lefth)
print('\n')
print(righth)
     key1  key2  data
0    Ohio  2000   0.0
1    Ohio  2001   1.0
2    Ohio  2002   2.0
3  Nevada  2001   3.0
4  Nevada  2002   4.0


             event1  event2
Nevada 2001       0       1
       2000       2       3
Ohio   2000       4       5
       2000       6       7
       2001       8       9
       2002      10      11

이 경우에는 리스트로 여러 개의 칼럼을 지정해서 병합해야 한다(중복되는 인덱스값을 다룰 때는 how=’outer’ 옵션을 사용해야 한다).

pd.merge(lefth, righth, left_on=['key1', 'key2'], right_index=True)
key1 key2 data event1 event2
0 Ohio 2000 0.0 4 5
0 Ohio 2000 0.0 6 7
1 Ohio 2001 1.0 8 9
2 Ohio 2002 2.0 10 11
3 Nevada 2001 3.0 0 1
pd.merge(lefth, righth, left_on=['key1', 'key2'],
         right_index=True, how='outer')
key1 key2 data event1 event2
0 Ohio 2000 0.0 4.0 5.0
0 Ohio 2000 0.0 6.0 7.0
1 Ohio 2001 1.0 8.0 9.0
2 Ohio 2002 2.0 10.0 11.0
3 Nevada 2001 3.0 0.0 1.0
4 Nevada 2002 4.0 NaN NaN
4 Nevada 2000 NaN 2.0 3.0

양쪽에 공통적으로 존재하는 여러 개의 인덱스를 병합하는 것도 가능하다.

left2 = pd.DataFrame([[1., 2.], [3., 4.], [5., 6.]],
                     index=['a', 'c', 'e'],
                     columns=['Ohio', 'Nevada'])
right2 = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [13, 14]],
                      index=['b', 'c', 'd', 'e'],
                      columns=['Missouri', 'Alabama'])
print(left2)
print('\n')
print(right2)
   Ohio  Nevada
a   1.0     2.0
c   3.0     4.0
e   5.0     6.0


   Missouri  Alabama
b       7.0      8.0
c       9.0     10.0
d      11.0     12.0
e      13.0     14.0
pd.merge(left2, right2, how='outer', left_index=True, right_index=True)
Ohio Nevada Missouri Alabama
a 1.0 2.0 NaN NaN
b NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0
d NaN NaN 11.0 12.0
e 5.0 6.0 13.0 14.0

로우 인덱스로 병합할 때 데이터프레임의 join() 메서드를 사용하면 편리하다. join() 메서드는 칼럼이 겹치지 않으며 완전히 같거나 유사한 인덱스 구조를 가진 여러 개의 데이터프레임 객체를 병합할 때 사용할 수 있다. 위에서 본 예제는 다음처럼 작성할 수 있다.

left2.join(right2, how='outer')
Ohio Nevada Missouri Alabama
a 1.0 2.0 NaN NaN
b NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0
d NaN NaN 11.0 12.0
e 5.0 6.0 13.0 14.0

과거에 작성된 판다스의 일부 코드 제약으로 인해 데이터프레임의 join() 메서드는 왼쪽 조인을 수행한다. 즉, left1에 join() 메서드를 적용하면서 right1을 인자로 전달하면 왼쪽에 위치한 left1의 행 인덱스를 기준으로 결합하는 how=’left’ 옵션이 기본 적용된다. join() 메서드를 호출한 데이터프레임의 칼럼 중 하나에 대해 조인을 수행하는 것도 가능하다.

left1.join(right1, on='key')
key value group_val
0 a 0 3.5
1 b 1 7.0
2 a 2 3.5
3 a 3 3.5
4 b 4 7.0
5 c 5 NaN

마지막으로 인덱스 대 인덱스로 두 데이터프레임을 병합하려면 그냥 간단하게 병합하려는 데이터프레임의 리스트를 join() 메서드로 넘기면 된다. 하지만 보통 이런 병합은 아래에서 살펴볼 concat()메서드를 사용한다.

another = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [16., 17.]],
                       index=['a', 'c', 'e', 'f'], 
                       columns=['New York', 'Oregon'])
print(left2)
print('\n')
print(right2)
print('\n')
print(another)
   Ohio  Nevada
a   1.0     2.0
c   3.0     4.0
e   5.0     6.0


   Missouri  Alabama
b       7.0      8.0
c       9.0     10.0
d      11.0     12.0
e      13.0     14.0


   New York  Oregon
a       7.0     8.0
c       9.0    10.0
e      11.0    12.0
f      16.0    17.0
left2.join([right2, another])
Ohio Nevada Missouri Alabama New York Oregon
a 1.0 2.0 NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0 9.0 10.0
e 5.0 6.0 13.0 14.0 11.0 12.0
left2.join([right2, another], how='outer')
Ohio Nevada Missouri Alabama New York Oregon
a 1.0 2.0 NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0 9.0 10.0
e 5.0 6.0 13.0 14.0 11.0 12.0
b NaN NaN 7.0 8.0 NaN NaN
d NaN NaN 11.0 12.0 NaN NaN
f NaN NaN NaN NaN 16.0 17.0

축 따라 이어붙이기

서로 다른 데이터프레임들의 구성 형태와 속성이 균일하다면, 행 또는 열 중에 어느 한 방향으로 이어 붙여도 데이터의 일관성을 유지할 수 있다.

데이터를 합치는 또 다른 방법으로 이어붙이기concatenation(연결binding, 적층stacking이라고 한다)가 있다. NumPy는 ndarray를 이어붙이는 concatenate() 함수를 제공한다.

arr = np.arange(12).reshape((3, 4))

arr
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
np.concatenate([arr, arr], axis=0)
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
np.concatenate([arr, arr], axis=1)
array([[ 0,  1,  2,  3,  0,  1,  2,  3],
       [ 4,  5,  6,  7,  4,  5,  6,  7],
       [ 8,  9, 10, 11,  8,  9, 10, 11]])

시리즈나 데이터프레임 같은 판다스 객체의 컨텍스트 내부에는 축마다 이름이 있어서 배열을 쉽게 이어붙일 수 있도록 되어 있다. 이때 다음 사항을 고려해야 한다.

  • 만약 연결하려는 두 객체의 인덱스가 서로 다르면 결과는 그 인덱스의 교집합이어야 하는가 아니면 합집합이어야 하는가?

  • 합쳐진 결과에서 합쳐지기 전 객체의 데이터를 구분할 수 있어야 하는가?

  • 어떤 축으로 연결할 것인지 고려해야 하는가? 많은 경우 데이터프레임의 기본 정수 라벨이 가장 먼저 무시된다.

판다스의 concat() 함수는 위 사항에 대한 답을 제공한다. concat() 함수가 어떻게 동작하는지 다양한 예제로 알아보자. 인덱스가 겹치지 않는 3개의 시리즈 객체가 있다고 하자.

s1 = pd.Series([0, 1], index=['a', 'b'])
s2 = pd.Series([2, 3, 4], index=['c', 'd', 'e'])
s3 = pd.Series([5, 6], index=['f', 'g'])

display(s1)
display(s2)
display(s3)
a    0
b    1
dtype: int64
c    2
d    3
e    4
dtype: int64
f    5
g    6
dtype: int64

이 세 객체를 리스트로 묶어서 concat() 함수에 전달하면 값과 인덱스를 연결해준다.

pd.concat([s1, s2, s3])
a    0
b    1
c    2
d    3
e    4
f    5
g    6
dtype: int64

concat() 함수는 axis=0을 기본값으로 하여 새로운 시리즈 객체를 생성한다. 만약 axis=1을 넘긴다면 결과는 시리즈가 아니라 데이터프레임이 될 것이다(axis=1은 칼럼을 의미한다).

pd.concat([s1, s2, s3], axis=1)
0 1 2
a 0.0 NaN NaN
b 1.0 NaN NaN
c NaN 2.0 NaN
d NaN 3.0 NaN
e NaN 4.0 NaN
f NaN NaN 5.0
g NaN NaN 6.0

겹치는 축이 없기 때문에 외부 조인으로 정렬된 합집합을 얻었지만 join=’inner’를 넘겨서 교집합을 구할 수도 있다.

s4 = pd.concat([s1, s3])

s4
a    0
b    1
f    5
g    6
dtype: int64
pd.concat([s1, s4], axis=1)
0 1
a 0.0 0
b 1.0 1
f NaN 5
g NaN 6
pd.concat([s1, s4], axis=1, join='inner')
0 1
a 0 0
b 1 1

마지막 예제에서 ‘f’와 ‘g’ 라벨은 join=’inner’ 옵션으로 인해 사라지게 된다.

시리즈를 이어붙이기 전의 개별 시리즈를 구분할 수 없는 문제가 생기는데, 이어붙인 축에 대해 계층적 인덱스를 생성하여 식별이 가능하도록 할 수 있다. 계층적 인덱스를 생성하려면 keys 인자를 사용하면 된다.

result = pd.concat([s1, s1, s3], keys=['one', 'two', 'three'])

result
one    a    0
       b    1
two    a    0
       b    1
three  f    5
       g    6
dtype: int64
result.index
MultiIndex([(  'one', 'a'),
            (  'one', 'b'),
            (  'two', 'a'),
            (  'two', 'b'),
            ('three', 'f'),
            ('three', 'g')],
           )
result.unstack()
a b f g
one 0.0 1.0 NaN NaN
two 0.0 1.0 NaN NaN
three NaN NaN 5.0 6.0

시리즈를 axis=1로 병합할 경우 keys는 데이터프레임의 칼럼 제목이 된다.

pd.concat([s1, s2, s3], axis=1, keys=['one', 'two', 'three'])
one two three
a 0.0 NaN NaN
b 1.0 NaN NaN
c NaN 2.0 NaN
d NaN 3.0 NaN
e NaN 4.0 NaN
f NaN NaN 5.0
g NaN NaN 6.0

데이터프레임 객체에 대해서도 지금까지와 같은 방식으로 적용할 수 있다.

df1 = pd.DataFrame(np.arange(6).reshape((3, 2)), index=['a', 'b', 'c'],
                   columns=['one', 'two'])
df2 = pd.DataFrame(5 + np.arange(4).reshape((2, 2)), index=['a', 'c'],
                   columns=['three', 'four'])

display(df1)
display(df2)
one two
a 0 1
b 2 3
c 4 5
three four
a 5 6
c 7 8
pd.concat([df1, df2], axis=1, keys=['level1', 'level2'])
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0

리스트 대신 객체의 사전을 넘기면 사전의 키가 keys 옵션으로 사용된다.

pd.concat({'level1': df1, 'level2': df2}, axis=1)
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0

계층적 인덱스를 생성할 때 사용할 수 있는 몇 가지 추가적인 옵션은 밑의 표를 참조하자. 예를 들어 새로 생성된 계층의 이름은 names 인자로 지정할 수 있다.

pd.concat([df1, df2], axis=1, keys=['level1', 'level2'], names=['upper', 'lower'])
upper level1 level2
lower one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0

마지막으로 데이터프레임의 로우 인덱스가 분석에 필요한 데이터를 포함하고 있지 않은 경우 어떻게 할 것인가?

df1 = pd.DataFrame(np.random.randn(3, 4), columns=['a', 'b', 'c', 'd'])
df2 = pd.DataFrame(np.random.randn(2, 3), columns=['b', 'd', 'a'])

display(df1)
display(df2)
a b c d
0 0.082446 -0.361550 1.677819 1.552201
1 1.135325 -1.519708 0.201073 -0.229441
2 -0.421589 1.990136 0.800249 -0.306256
b d a
0 1.633717 0.630153 -0.098665
1 1.994450 -0.548019 0.007595

다음과 같이 인덱스가 0, 1, 2, 0, 1로 유용하지 않은 인덱스를 갖고 있다.

pd.concat([df1, df2])
a b c d
0 0.082446 -0.361550 1.677819 1.552201
1 1.135325 -1.519708 0.201073 -0.229441
2 -0.421589 1.990136 0.800249 -0.306256
0 -0.098665 1.633717 NaN 0.630153
1 0.007595 1.994450 NaN -0.548019

이 경우 ignore_index=True 옵션을 주면 된다.

pd.concat([df1, df2], ignore_index=True)
a b c d
0 0.082446 -0.361550 1.677819 1.552201
1 1.135325 -1.519708 0.201073 -0.229441
2 -0.421589 1.990136 0.800249 -0.306256
3 -0.098665 1.633717 NaN 0.630153
4 0.007595 1.994450 NaN -0.548019

다음은 concat() 함수의 인자를 정리한 것이다.

인자 설명
objs 이어붙일 판다스 객체의 사전이나 리스트. 필수 인자
axis 이어붙일 축 방향. 기본값은 0
join 조인 방식. 'inner'(내부 조인, 교집합)과 'outer'(외부 조인, 합집합)가 있으며 기본값은 'outer'
keys 이어붙일 객체나 이어붙인 축에 대한 계층 인덱스를 생성하는 데 연관된 값이다. 리스트나 임의의 값이 들어 있는 배열. 튜플의 배열 또는 배열의 리스트(levels 옵션에 다차원 배열이 넘오온 경우)가 있다.
levels 계층 인덱스 레벨로 사용할 인덱스를 지정한다. keys가 넘어온 경우 여러 개의 인덱스를 지정한다.
names keys나 levels 혹은 둘 다 있을 경우 생성된 계층 레벨을 위한 이름
verify_integrity 이어붙인 객체에 중복되는 축이 있는지 검사하고 있다면 예외를 발생시킨다. 기본값은 False로, 중복을 허용한다.
ignore_index 이어붙인 축의 인덱스를 유지하지 않고 range(total_length)로 새로운 인덱스를 생성한다.

겹치는 데이터 합치기

데이터를 합칠 때 병합이나 이어붙이기로는 불가능한 상황이 있는데, 두 데이터셋의 인덱스가 일부 겹치거나 전체가 겹치는 경우가 그렇다. 벡터화된 if-else 구문을 표현하는 NumPy의 where() 함수로 자세히 알아보자.

a = pd.Series([NA, 2.5, NA, 3.5, 4.5, NA],
              index=['f', 'e', 'd', 'c', 'b', 'a'])
b = pd.Series(np.arange(len(a), dtype=np.float64),
              index=['f', 'e', 'd', 'c', 'b', 'a'])
b[-1] = NA

display(a)
display(b)
f    NaN
e    2.5
d    NaN
c    3.5
b    4.5
a    NaN
dtype: float64
f    0.0
e    1.0
d    2.0
c    3.0
b    4.0
a    NaN
dtype: float64
np.where(pd.isnull(a), b, a) # a 값이 null이면 b를, null이 아니면 a를 출력
array([0. , 2.5, 2. , 3.5, 4.5, nan])

시리즈 객체의 conbine_first() 메서드는 위와 동일한 연산을 제공하며 데이터 정렬 기능까지 제공한다.

b[:-2].combine_first(a[2:])
a    NaN
b    4.5
c    3.0
d    2.0
e    1.0
f    0.0
dtype: float64
b.combine_first(a)
f    0.0
e    1.0
d    2.0
c    3.0
b    4.0
a    NaN
dtype: float64

왜 인덱싱을 한 후 combine_first()를 진행했는지 이해가 가지 않는다. 인덱싱을 해야 데이터 정렬을 해주고, 하지 않으면 그대로 출력해준다. 이 부분에 대해서는 따로 알아봐야겠다.

데이터프레임에서 combine_first() 메서드는 칼럼에 대해 같은 동작을 한다. 그러므로 호출하는 객체에서 누락된 데이터를 인자로 넘긴 객체에 있는 값으로 채워 넣을 수 있다.

df1 = pd.DataFrame({'a': [1., NA, 5., NA],
                    'b': [NA, 2., NA, 6.],
                    'c': range(2, 18, 4)},
                  index=[2, 3, 1, 0])
df2 = pd.DataFrame({'a': [5., 4., NA, 3., 7.],
                    'b': [NA, 3., 4. ,6., 8.]},
                   index=[3, 4, 2, 0, 1])

display(df1)
display(df2)
a b c
2 1.0 NaN 2
3 NaN 2.0 6
1 5.0 NaN 10
0 NaN 6.0 14
a b
3 5.0 NaN
4 4.0 3.0
2 NaN 4.0
0 3.0 6.0
1 7.0 8.0
df1.combine_first(df2)
a b c
0 3.0 6.0 14.0
1 5.0 8.0 10.0
2 1.0 4.0 2.0
3 5.0 2.0 6.0
4 4.0 3.0 NaN

데이터프레임을 대상으로 했을 때는 누락값을 채워줌과 동시에 인덱스 정렬까지 해주었다. 기준이 뭘까?

재형성과 피벗

표 형식의 데이터를 재배치하는 다양한 기본 연산이 존재한다. 이런 연산을 재형성 또는 피벗 연산이라고 한다.

계층적 인덱스로 재형성하기

계층적 인덱스는 데이터프레임의 데이터를 재배치하는 다음과 같은 방식을 제공한다.

  • stack

    데이터의 칼럼을 로우로 피벗(또는 회전)시킨다.

  • unstack

    로우를 칼럼으로 피벗시킨다.

몇 가지 예제를 통해 위 연산을 좀 더 알아보자. 문자열이 담긴 배열을 로우와 칼럼의 인덱스로 하는 작은 데이터프레임이 있다.

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

stack() 메서드를 사용하면 칼럼이 로우로 피벗되어서 다음과 같은 시리즈 객체를 반환한다.

result = data.stack()

result
state     number
Ohio      one       0
          two       1
          three     2
Colorado  one       3
          two       4
          three     5
dtype: int32

unstack() 메서드를 사용하면 위 계층적 인덱스를 가진 시리즈로부터 다시 데이터프레임을 얻을 수 있다.

result.unstack()
number one two three
state
Ohio 0 1 2
Colorado 3 4 5

기본적으로 가장 안쪽에 있는 레벨(가장 큰 레벨)부터 끄집어내는데(stack()도 마찬가지다), 레벨 숫자나 이름을 전달해서 끄집어낼 단계를 지정할 수 있다.

result.unstack(0)
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
result.unstack('state')
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5

해당 레벨에 있는 모든 값이 하위그룹에 속하지 않을 경우 unstack()을 하게 되면 누락된 데이터가 생길 수 있다.

s1 = pd.Series([0, 1, 2, 3], index=['a', 'b', 'c', 'd'])
s2 = pd.Series([4, 5, 6], index=['c', 'd', 'e'])

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.0 1.0 2.0 3.0 NaN
two NaN NaN 4.0 5.0 6.0
data2.unstack(0)
one two
a 0.0 NaN
b 1.0 NaN
c 2.0 4.0
d 3.0 5.0
e NaN 6.0

stack() 메서드는 누락된 데이터를 자동으로 걸러내기 때문에 연산을 쉽게 원상 복구할 수 있다.

data2.unstack()
a b c d e
one 0.0 1.0 2.0 3.0 NaN
two NaN NaN 4.0 5.0 6.0
data2.unstack().stack()
one  a    0.0
     b    1.0
     c    2.0
     d    3.0
two  c    4.0
     d    5.0
     e    6.0
dtype: float64
data2.unstack().stack(dropna=False)
one  a    0.0
     b    1.0
     c    2.0
     d    3.0
     e    NaN
two  a    NaN
     b    NaN
     c    4.0
     d    5.0
     e    6.0
dtype: float64

데이터프레임을 unstack()할 때 unstack 레벨은 결과에서 가장 낮은 단계가 된다.

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()
side left right
number one two three one two three
state
Ohio 0 1 2 5 6 7
Colorado 3 4 5 8 9 10
df.unstack('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

stack()을 호출할 때 쌓을 축의 이름을 지정할 수 있다.

df.unstack('state').stack('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

긴 형식에서 넓은 형식으로 피벗하기

데이터베이스나 CSV 파일에 여러 개의 시계열 데이터를 저장하는 일반적인 방법은 시간 순서대로 나열하는 것이다. 예제 데이터를 읽어서 시계열 데이터를 다뤄보자.

data = pd.read_csv('examples/macrodata.csv')

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
periods = pd.PeriodIndex(year=data.year, quarter=data.quarter,
                         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)
columns = pd.Index(['realgdp', 'infl', 'unemp'], name='item')

columns
Index(['realgdp', 'infl', 'unemp'], dtype='object', name='item')
data = data.reindex(columns=columns)

data
item realgdp infl unemp
0 2710.349 0.00 5.8
1 2778.801 2.34 5.1
2 2775.488 2.74 5.3
3 2785.204 0.27 5.6
4 2847.699 2.31 5.2
... ... ... ...
198 13324.600 -3.16 6.0
199 13141.920 -8.79 6.9
200 12925.410 0.94 8.1
201 12901.504 3.37 9.2
202 12990.341 3.56 9.6

203 rows × 3 columns

data.index = periods.to_timestamp('D', 'end')

data
item realgdp infl unemp
date
1959-03-31 23:59:59.999999999 2710.349 0.00 5.8
1959-06-30 23:59:59.999999999 2778.801 2.34 5.1
1959-09-30 23:59:59.999999999 2775.488 2.74 5.3
1959-12-31 23:59:59.999999999 2785.204 0.27 5.6
1960-03-31 23:59:59.999999999 2847.699 2.31 5.2
... ... ... ...
2008-09-30 23:59:59.999999999 13324.600 -3.16 6.0
2008-12-31 23:59:59.999999999 13141.920 -8.79 6.9
2009-03-31 23:59:59.999999999 12925.410 0.94 8.1
2009-06-30 23:59:59.999999999 12901.504 3.37 9.2
2009-09-30 23:59:59.999999999 12990.341 3.56 9.6

203 rows × 3 columns

ldata = data.stack().reset_index().rename(columns={0: 'value'})

ldata
date item value
0 1959-03-31 23:59:59.999999999 realgdp 2710.349
1 1959-03-31 23:59:59.999999999 infl 0.000
2 1959-03-31 23:59:59.999999999 unemp 5.800
3 1959-06-30 23:59:59.999999999 realgdp 2778.801
4 1959-06-30 23:59:59.999999999 infl 2.340
... ... ... ...
604 2009-06-30 23:59:59.999999999 infl 3.370
605 2009-06-30 23:59:59.999999999 unemp 9.200
606 2009-09-30 23:59:59.999999999 realgdp 12990.341
607 2009-09-30 23:59:59.999999999 infl 3.560
608 2009-09-30 23:59:59.999999999 unemp 9.600

609 rows × 3 columns

PeriodIndex는 후에 제대로 알아보겠지만 간단히 설명하면 시간 간격을 나타내기 위한 자료형으로, 연도(year)와 분기(quarter) 칼럼을 합친다.

ldata와 같은 형식을 형식이라고 부르며, 여러 시계열이나 둘 이상의 키(예제에서는 data와 item)를 가지고 있는 다른 관측 데이터에서 사용한다. 각 로우는 단일 관측치를 나타낸다.

MySQL 같은 관계형 데이터베이스는 테이블에 데이터가 추가되거나 삭제되면 item 칼럼에 별개의 값을 넣거나 빼는 방식으로 고정된 스키마(칼럼 이름과 데이터형)에 데이터를 저장한다. 위 예에서 data와 item은 관계형 데이터베이스 관점에서 얘기하자면 기본키primary key가 되어 관계 무결성을 제공하며 쉬운 조인 연산과 프로그램에 의한 질의를 가능하게 해준다. 물론 단점도 있는데, 길이가 긴 형식으로는 작업이 용이하지 않을 수 있어서 하나의 데이터프레임에 date 칼럼의 시간값으로 인덱스된 개별 item을 칼럼으로 포함시키는 것을 선호할지도 모른다.

데이터프레임의 pivot() 메서드가 바로 이런 변형을 지원한다.

pivoted = ldata.pivot('date', 'item', 'value')

pivoted
item infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2
... ... ... ...
2008-09-30 23:59:59.999999999 -3.16 13324.600 6.0
2008-12-31 23:59:59.999999999 -8.79 13141.920 6.9
2009-03-31 23:59:59.999999999 0.94 12925.410 8.1
2009-06-30 23:59:59.999999999 3.37 12901.504 9.2
2009-09-30 23:59:59.999999999 3.56 12990.341 9.6

203 rows × 3 columns

pivot() 메서드의 처음 두 인자는 로우와 칼럼 인덱스로 사용될 칼럼 이름이고 마지막 인자는 데이터프레임에 채워 넣을 값을 담고 있는 칼럼 이름이다. 한 번에 두 개의 칼럼을 동시에 변형한다고 하자.

ldata['value2'] = np.random.randn(len(ldata))

ldata[:10]
date item value value2
0 1959-03-31 23:59:59.999999999 realgdp 2710.349 -0.574210
1 1959-03-31 23:59:59.999999999 infl 0.000 -0.092744
2 1959-03-31 23:59:59.999999999 unemp 5.800 0.060117
3 1959-06-30 23:59:59.999999999 realgdp 2778.801 -0.694223
4 1959-06-30 23:59:59.999999999 infl 2.340 -0.099797
5 1959-06-30 23:59:59.999999999 unemp 5.100 0.788007
6 1959-09-30 23:59:59.999999999 realgdp 2775.488 -0.481608
7 1959-09-30 23:59:59.999999999 infl 2.740 -1.457328
8 1959-09-30 23:59:59.999999999 unemp 5.300 -0.607005
9 1959-12-31 23:59:59.999999999 realgdp 2785.204 0.455099

마지막 인자를 생략해서 계층적 칼럼을 가지는 데이터프레임을 얻을 수 있다.

pivoted = ldata.pivot('date', 'item')

pivoted[:5]
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8 -0.092744 -0.574210 0.060117
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1 -0.099797 -0.694223 0.788007
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3 -1.457328 -0.481608 -0.607005
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6 0.591787 0.455099 2.618562
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2 0.593470 -0.412954 1.592000
pivoted['value'][:5]
item infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2

pivot()은 단지 set_index()를 사용해서 계층적 인덱스를 만들고 unstack() 메서드를 이용해서 형태를 변경하는 단축키 같은 메서드다.

unstacked = ldata.set_index(['date', 'item']).unstack('item')

unstacked
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8 -0.092744 -0.574210 0.060117
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1 -0.099797 -0.694223 0.788007
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3 -1.457328 -0.481608 -0.607005
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6 0.591787 0.455099 2.618562
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2 0.593470 -0.412954 1.592000
... ... ... ... ... ... ...
2008-09-30 23:59:59.999999999 -3.16 13324.600 6.0 -2.149614 -0.357146 -0.272126
2008-12-31 23:59:59.999999999 -8.79 13141.920 6.9 -0.140082 1.517271 0.659169
2009-03-31 23:59:59.999999999 0.94 12925.410 8.1 -0.697002 0.207336 -1.582654
2009-06-30 23:59:59.999999999 3.37 12901.504 9.2 1.269960 -1.465134 1.894946
2009-09-30 23:59:59.999999999 3.56 12990.341 9.6 0.470281 -0.663117 0.677829

203 rows × 6 columns

넓은 형식에서 긴 형식으로 피벗하기

pivot()과 반대되는 연산은 pandas.melt()다. 하나의 칼럼을 여러 개의 새로운 데이터프레임으로 생성하기보다는 여러 칼럼을 하나로 병합하고 데이터프레임을 입력보다 긴 형태로 만들어낸다.

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

‘key’ 칼럼을 그룹 구분자로 사용할 수 있고 다른 칼럼을 데이터값으로 사용할 수 있다. pandas.melt()를 사용할 때는 반드시 어떤 칼럼을 그룹 구분자로 사용할 것인지 지정해야 한다. 여기서는 ‘key’를 그룹 구분자로 지정하자.

melted = pd.melt(df, ['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

pivot()을 사용해서 원래 모양으로 되돌릴 수 있다.

reshaped = melted.pivot('key', 'variable', 'value')

reshaped
variable A B C
key
bar 2 5 8
baz 3 6 9
foo 1 4 7
melted.set_index(['key', 'variable']).sort_index().unstack()
value
variable A B C
key
bar 2 5 8
baz 3 6 9
foo 1 4 7

pivot()의 결과는 로우 라벨로 사용하던 칼럼에서 인덱스를 생성하므로 reset_index()를 이용해서 데이터를 다시 칼럼으로 돌려놓자.

display(reshaped)
reshaped.index
variable A B C
key
bar 2 5 8
baz 3 6 9
foo 1 4 7
Index(['bar', 'baz', 'foo'], dtype='object', name='key')
reshaped.reset_index()
variable key A B C
0 bar 2 5 8
1 baz 3 6 9
2 foo 1 4 7

데이터값으로 사용할 칼럼들의 집합을 지정할 수도 있다.

df
key A B C
0 foo 1 4 7
1 bar 2 5 8
2 baz 3 6 9
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

pandas.melt()는 그룹 구분자(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

댓글남기기