작성자: admin 작성일시: 2016-07-08 23:34:29 조회수: 2540 다운로드: 136
카테고리: Python 태그목록:

피봇테이블과 그룹 분석

피봇 테이블

피봇 테이블(pivot table)이란 데이터 열(column) 중에서 두 개를 키(key)로 사용하여 데이터를 선택하는 방법을 말한다.

피봇 테이블을 사용하기 위해서는 키가 될 수 있는 두 개의 열(column) 혹은 필드(field)를 선택하여 이 두 열을

  • 행 인덱스 (row index)
  • 열 인덱스 (column index)

로 변경하고 행 조건과 열 조건에 맞는 데이터를 찾아서 해당 칸에 넣는다. 만약 주어진 데이터가 없으면 NaN 값을 넣는다.

Pandas는 피봇 테이블을 만들기 위한 pivot 메서드를 제공한다. 첫번째 인수로는 행 인덱스로 사용할 열 이름, 두번째 인수로는 열 인덱스로 사용할 열 이름, 그리고 마지막으로 데이터로 사용할 열 이름을 넣는다.

In:
data = {
    'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada'],
    'year': [2000, 2001, 2002, 2001, 2002],
    'pop': [1.5, 2.5, 3.0, 2.5, 3.5]
}
df = pd.DataFrame(data, columns=["state", "year", "pop"])
df
Out:
state year pop
0 Ohio 2000 1.5
1 Ohio 2001 2.5
2 Ohio 2002 3.0
3 Nevada 2001 2.5
4 Nevada 2002 3.5
In:
df.pivot("state", "year", "pop")
Out:
year 2000 2001 2002
state
Nevada NaN 2.5 3.5
Ohio 1.5 2.5 3.0

사실 피봇 테이블은 다음과 같이 set_index 명령과 unstack 명령을 사용해서 만들 수도 있다.

In:
df.set_index(["state", "year"]).unstack()
Out:
pop
year 2000 2001 2002
state
Nevada NaN 2.5 3.5
Ohio 1.5 2.5 3.0

행 인덱스와, 열 인덱스가 될 자료는 키(key)의 역할을 해야 한다. 즉, 이 값으로 데이터가 유일하게(unique) 결정되어야 한다. 만약 조건에 해당하는 데이터가 2개 이상인 경우에는 에러가 발생한다.

In:
df.pivot("year", "pop", "state")

ValueErrorTraceback (most recent call last)
 in ()
----> 1 df.pivot("year", "pop", "state")

/home/dockeruser/anaconda2/lib/python2.7/site-packages/pandas/core/frame.pyc in pivot(self, index, columns, values)
   3933         """
   3934         from pandas.core.reshape import pivot
-> 3935         return pivot(self, index=index, columns=columns, values=values)
   3936 
   3937     def stack(self, level=-1, dropna=True):

/home/dockeruser/anaconda2/lib/python2.7/site-packages/pandas/core/reshape.pyc in pivot(self, index, columns, values)
    336         indexed = Series(self[values].values,
    337                          index=MultiIndex.from_arrays([index, self[columns]]))
--> 338         return indexed.unstack(columns)
    339 
    340 

/home/dockeruser/anaconda2/lib/python2.7/site-packages/pandas/core/series.pyc in unstack(self, level, fill_value)
   2088         """
   2089         from pandas.core.reshape import unstack
-> 2090         return unstack(self, level, fill_value)
   2091 
   2092     # ----------------------------------------------------------------------

/home/dockeruser/anaconda2/lib/python2.7/site-packages/pandas/core/reshape.pyc in unstack(obj, level, fill_value)
    411     else:
    412         unstacker = _Unstacker(obj.values, obj.index, level=level,
--> 413                                fill_value=fill_value)
    414         return unstacker.get_result()
    415 

/home/dockeruser/anaconda2/lib/python2.7/site-packages/pandas/core/reshape.pyc in __init__(self, values, index, level, value_columns, fill_value)
    101 
    102         self._make_sorted_values_labels()
--> 103         self._make_selectors()
    104 
    105     def _make_sorted_values_labels(self):

/home/dockeruser/anaconda2/lib/python2.7/site-packages/pandas/core/reshape.pyc in _make_selectors(self)
    139 
    140         if mask.sum() < len(self.index):
--> 141             raise ValueError('Index contains duplicate entries, '
    142                              'cannot reshape')
    143 

ValueError: Index contains duplicate entries, cannot reshape

그룹 분석

이렇게 특정 조건에 맞는 데이터가 하나 이상 즉, 그룹일 경우는 그룹 분석을 해야 한다.

그룹 분석은 피봇 테이블과 달리 키에 의해서 결정되는 데이터가 복수개가 있어도 괜찮다. 대신 연산을 통해 복수개의 그룹 데이터에 대한 대표값을 정한다. Pandas 에서는 groupby 명령과 그룹 연산 메서드를 이용하여 그룹 분석을 한다.

그룹 연산을 하는 방법은 다음과 같다.

  1. 분석하고자 하는 시리즈나 데이터프레임에 groupby 메서드를 호출한다.
  2. 호출한 결과에 그룹 연산을 수행한다.

groupby 메서드

groupby 메서드는 데이터를 그룹 별로 분류하는 역할을 한다. groupby 메서드의 인수로는 다음과 같은 값을 사용한다.

  • 열 또는 열의 리스트
  • 행 인덱스

연산 결과로 GroupBy 클래스 객체를 반환하는데 이 객체에는 그룹별로 연산을 할 수 있는 그룹 연산 메서드가 있다.

그룹 연산 메서드

groupby 결과, 즉 GroupBy 클래스 객체의 뒤에 붙일 수 있는 그룹 연산 메서드는 다양하다. 전체 목록을 보려면 다음 웹사이트를 참조한다.

다음은 자주 사용되는 그룹 연산 메서드들이다.

  • size(), count(): 갯수
  • mean(), median(), min(), max(): 평균, 중앙값, 최소, 최대
  • sum(), prod(), std(), var(), quantile() : 합계, 곱, 표준편차, 분산, 사분위수
  • first(), last(): 가장 첫번째 데이터와 가장 나중 데이터

이 외에도 많이 사용되는 것으로는 다음과 같은 그룹 연산이 있다.

  • agg(), aggregate()

    • 만약 원하는 그룹 연산이 없는 경우 함수를 만들고 이 함수를 agg()에 전달한다.
    • 또는 여러가지 그룹 연산을 동시에 하고 싶은 경우 함수 이름 문자열의 리스트를 전달한다.
  • transform()

    • 그룹 연산으로 대표값을 만든 다음, 이 대표 값을 새로운 열(column)로 원래 데이터프레임에 추가한다.
  • describe()

    • 하나의 그룹 대표값이 아니라 여러개의 값을 데이터프레임으로 구한다.
  • apply()

    • describe() 처럼 하나의 대표값이 아닌 데이터프레임을 출력하지만 원하는 그룹 연산이 없는 경우에 사용한다.

예를 들어 다음과 같은 데이터가 있을 때 key1 값에 따른 data1의 평균은 어떻게 구할까? 그룹 분석으로 이 결과를 구할 수 있다.

In:
np.random.seed(0)
df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
                   'key2' : ['one', 'two', 'one', 'two', 'one'],
                   'data1' : np.random.randn(5),
                   'data2' : np.random.randn(5)})
df
Out:
data1 data2 key1 key2
0 1.764052 -0.977278 a one
1 0.400157 0.950088 a two
2 0.978738 -0.151357 b one
3 2.240893 -0.103219 b two
4 1.867558 0.410599 a one

우선 데이터프레임 전체에 대해 그룹분석을 하자. groupby 명령을 수행하고 그 결과에 대해 각각 평균을 구하기 위해 mean이라는 그룹 연산을 하였다.

In:
df.groupby(df.key1).mean()
Out:
data1 data2
key1
a 1.343923 0.127803
b 1.609816 -0.127288

만약 열 data1에 대해서만 하고 싶다면 미리 시리즈를 구하거나 groupby 반환값 또는 최종 그룹연산 결과에서 data1만 뽑아도 된다.

In:
df.data1.groupby(df.key1).mean()
Out:
key1
a    1.343923
b    1.609816
Name: data1, dtype: float64
In:
df.groupby(df.key1)["data1"].mean()
Out:
key1
a    1.343923
b    1.609816
Name: data1, dtype: float64
In:
df.groupby(df.key1).mean()["data1"]
Out:
key1
a    1.343923
b    1.609816
Name: data1, dtype: float64

그럼 이번에는 복합 key (key1, key2) 값에 따른 data1의 평균을 구하자. 이 문제는 피봇 데이블과 유사하다. 분석하고자 하는 키가 여러개이면 리스트를 사용한다.

In:
df.data1.groupby([df.key1, df.key2]).mean()
Out:
key1  key2
a     one     1.815805
      two     0.400157
b     one     0.978738
      two     2.240893
Name: data1, dtype: float64

이 값은 보기 어려우므로 unstack 명령으로 피봇 데이블 형태로 만든다.

In:
df.data1.groupby([df.key1, df.key2]).mean().unstack("key2")
Out:
key2 one two
key1
a 1.815805 0.400157
b 0.978738 2.240893

pivot_table

Pandas는 pivot 명령과 groupby 명령의 중간적 성격을 가지는 pivot_table 명령도 제공한다.

pivot_table 명령은 groupby 명령처럼 그룹 분석을 하지만 최종적으로는 pivot 명령처럼 피봇테이블을 만든다. 즉 groupby 명령의 결과에 unstack을 자동 적용하여 2차원적인 형태로 변형한다. 사용 방법은 다음과 같다.

  • pivot_table(data, values=None, index=None, columns=None, aggfunc='mean', fill_value=None, margins=False, margins_name='All')
    • data: 분석할 데이터프레임 (메서드일 때는 필요하지 않음)
    • values: 분석할 데이터프레임에서 분석할 열
    • index: 행 인덱스로 들어갈 키 열 또는 키 열의 리스트
    • columns: 열 인덱스로 들어갈 키 열 또는 키 열의 리스트
    • aggfunc: 분석 메서드
    • fill_value: NaN 대체 값
    • margins: 오른쪽과 아래에 합계를 붙일지 여부
    • margins_name: 합계 열(행)의 이름

따라서 일반 피봇 테이블의 관점에서 볼 때는 pivot을 수행하지만 데이터가 유니크하게 선택되지 않으면 aggfunc 인수로 정의된 함수를 수행하여 대표값을 계산하는 것과 같다.

예를 들어 위에서 만들었던 피봇 테이블은 pivot_table 명령으로 다음과 같이 만들 수도 있다.

In:
df.pivot_table("data1", "key1", "key2")
Out:
key2 one two
key1
a 1.815805 0.400157
b 0.978738 2.240893

마진을 더하는 것도 간단하다.

In:
df.pivot_table("data1", "key1", "key2", margins=True, margins_name="합계")
Out:
key2 one two 합계
key1
a 1.815805 0.400157 1.343923
b 0.978738 2.240893 1.609816
합계 1.536783 1.320525 1.450280

TIP 데이터 예제

식당에서 식사 후 내는 팁(tip)과 관련된 데이터를 이용하여 좀더 구체적으로 그룹 분석 방법을 살펴본다. 우선 seaborn에 설치된 샘플 데이터를 로드한다.

In:
tips = sns.load_dataset("tips")
tips.tail()
Out:
total_bill tip sex smoker day time size
239 29.03 5.92 Male No Sat Dinner 3
240 27.18 2.00 Female Yes Sat Dinner 2
241 22.67 2.00 Male Yes Sat Dinner 2
242 17.82 1.75 Male No Sat Dinner 2
243 18.78 3.00 Female No Thur Dinner 2

이 데이터프레임에서 각각의 컬럼은 다음을 뜻한다.

  • total_bill: 식사대금
  • tip: 팁
  • sex: 성별
  • smoker: 흡연/금연 여부
  • day: 요일
  • time: 시간
  • size: 인원

우선 식사대금와 팁의 비율을 나타내는 tip_pct를 추가하자.

In:
tips['tip_pct'] = tips['tip'] / tips['total_bill']
tips.tail()
Out:
total_bill tip sex smoker day time size tip_pct
239 29.03 5.92 Male No Sat Dinner 3 0.203927
240 27.18 2.00 Female Yes Sat Dinner 2 0.073584
241 22.67 2.00 Male Yes Sat Dinner 2 0.088222
242 17.82 1.75 Male No Sat Dinner 2 0.098204
243 18.78 3.00 Female No Thur Dinner 2 0.159744

다음으로 각 열의 데이터에 대해 간단히 분포를 알아본다.

In:
tips.describe()
Out:
total_bill tip size tip_pct
count 244.000000 244.000000 244.000000 244.000000
mean 19.785943 2.998279 2.569672 0.160803
std 8.902412 1.383638 0.951100 0.061072
min 3.070000 1.000000 1.000000 0.035638
25% 13.347500 2.000000 2.000000 0.129127
50% 17.795000 2.900000 2.000000 0.154770
75% 24.127500 3.562500 3.000000 0.191475
max 50.810000 10.000000 6.000000 0.710345

그룹별 통계

우선 성별로 나누어 데이터 갯수를 세어본다.

In:
tips.groupby("sex").count()
Out:
total_bill tip smoker day time size tip_pct
sex
Male 157 157 157 157 157 157 157
Female 87 87 87 87 87 87 87

데이터 갯수의 경우 NaN 데이터가 없다면 모두 같은 값이 나올 것이다. 이 때는 size 명령을 사용하면 더 간단히 표시된다. size 명령은 NaN이 있어도 상관하지 않는다.

In:
tips.groupby("sex").size()
Out:
sex
Male      157
Female     87
dtype: int64

이번에는 성별과 흡연유무로 나누어 데이터의 갯수와 팁 액수의 평균을 알아본다.

In:
tips.groupby(["sex", "smoker"]).size()
Out:
sex     smoker
Male    Yes       60
        No        97
Female  Yes       33
        No        54
dtype: int64

좀 더 보기 좋도록 피봇 데이블 형태로 보자.

In:
tips.pivot_table("tip_pct", "sex", "smoker", aggfunc="count", margins=True)
Out:
smoker Yes No All
sex
Male 60.0 97.0 157.0
Female 33.0 54.0 87.0
All 93.0 151.0 244.0
In:
tips.pivot_table("tip_pct", "sex", "smoker")
Out:
smoker Yes No
sex
Male 0.152771 0.160669
Female 0.182150 0.156921

이번에는 성별과 흡연유무로 나누어 팁 액수의 평균뿐 아니라 여러가지 통계값을 알아본다.

In:
tips.groupby(["sex", "smoker"])[["tip", "tip_pct"]].describe()
Out:
tip tip_pct
sex smoker
Male Yes count 60.000000 60.000000
mean 3.051167 0.152771
std 1.500120 0.090588
min 1.000000 0.035638
25% 2.000000 0.101845
50% 3.000000 0.141015
75% 3.820000 0.191697
max 10.000000 0.710345
No count 97.000000 97.000000
mean 3.113402 0.160669
std 1.489559 0.041849
min 1.250000 0.071804
25% 2.000000 0.131810
50% 2.740000 0.157604
75% 3.710000 0.186220
max 9.000000 0.291990
Female Yes count 33.000000 33.000000
mean 2.931515 0.182150
std 1.219916 0.071595
min 1.000000 0.056433
25% 2.000000 0.152439
50% 2.880000 0.173913
75% 3.500000 0.198216
max 6.500000 0.416667
No count 54.000000 54.000000
mean 2.773519 0.156921
std 1.128425 0.036421
min 1.000000 0.056797
25% 2.000000 0.139708
50% 2.680000 0.149691
75% 3.437500 0.181630
max 5.200000 0.252672

이번에는 각 그룹에서 가장 많은 팁과 가장 적은 팁의 차이를 알아보자. 함수가 없으므로 만들어야 한다.

In:
def peak_to_peak(x):
    return x.max() - x.min()

tips.groupby(["sex", "smoker"])[["tip"]].agg(peak_to_peak)
Out:
tip
sex smoker
Male Yes 9.00
No 7.75
Female Yes 5.50
No 4.20

만약 여러가지 그룹연산을 동시에 하고 싶다면 다음과 같이 리스트를 넣는다.

In:
tips.groupby(["sex", "smoker"]).agg(["mean", peak_to_peak])[["total_bill"]]
Out:
total_bill
mean peak_to_peak
sex smoker
Male Yes 22.284500 43.56
No 19.791237 40.82
Female Yes 17.977879 41.23
No 18.105185 28.58

만약 데이터 열마다 다른 연산을 하고 싶다면 열 라벨과 연산 이름(또는 함수)를 딕셔너리로 넣는다.

In:
tips.groupby(["sex", "smoker"]).agg({'tip_pct' : 'mean', 'total_bill' : peak_to_peak})
Out:
tip_pct total_bill
sex smoker
Male Yes 0.152771 43.56
No 0.160669 40.82
Female Yes 0.182150 41.23
No 0.156921 28.58

pivot_table 명령을 사용하면 좀 더 간단하게 코드를 작성할 수 있다.

In:
tips.pivot_table(index=['sex', 'smoker'])
Out:
size tip tip_pct total_bill
sex smoker
Male Yes 2.500000 3.051167 0.152771 22.284500
No 2.711340 3.113402 0.160669 19.791237
Female Yes 2.242424 2.931515 0.182150 17.977879
No 2.592593 2.773519 0.156921 18.105185
In:
tips.pivot_table(['tip_pct', 'size'], index=['sex', 'day'], columns='smoker')
Out:
tip_pct size
smoker Yes No Yes No
sex day
Male Thur 0.164417 0.165706 2.300000 2.500000
Fri 0.144730 0.138005 2.125000 2.000000
Sat 0.139067 0.162132 2.629630 2.656250
Sun 0.173964 0.158291 2.600000 2.883721
Female Thur 0.163073 0.155971 2.428571 2.480000
Fri 0.209129 0.165296 2.000000 2.500000
Sat 0.163817 0.147993 2.200000 2.307692
Sun 0.237075 0.165710 2.500000 3.071429
In:
tips.pivot_table('size', index=['time', 'sex', 'smoker'],
                 columns='day', aggfunc='sum', fill_value=0)
Out:
day Thur Fri Sat Sun
time sex smoker
Lunch Male Yes 23 5 0 0
No 50 0 0 0
Female Yes 17 6 0 0
No 60 3 0 0
Dinner Male Yes 0 12 71 39
No 0 4 85 124
Female Yes 0 8 33 10
No 2 2 30 43

연습 문제 1

타이타닉 승객 데이터를 이용하여 다음 분석을 실시하라. 데이터는 다음과 같이 받을 수 있다.

titanic = sns.load_dataset("titanic")
  1. qcut 명령으로 세 개의 나이 그룹을 만든다.
  2. 성별, 선실, 나이 그룹에 의한 생존율을 데이터프레임으로 계산한다. 행에는 성별 및 나이 그룹에 대한 다중 인덱스를 사용하고 열에는 선실 인덱스를 사용한다.
  3. 성별 및 선실에 의한 생존율을 피봇 데이터 형태로 만든다.

질문/덧글

그룹 연산 질문드립니다. lucc*** 2016년 9월 19일 9:20 오후

그룹 연산 중에 최빈값을 구하는 것은 없어서, 인터넷에 흘러다니는 정보들을 조합해서 이런 예시를 만들었습니다.

import pandas as pd
data = {
'test' : [1,1,1,1,2,2,2,2],
'result' : [10,10,20,20,30,30,40,40]}
df = pd.DataFrame(data)
most = df.result.groupby(df.test).apply(pd.Series.mode)
print(most)

결과
test
1 0 10
1 20
2 0 30
1 40

결과가 위와 같이 나오는데, 둘 중에 하나를 고를 수 있는 방법이 있나요?

예시)
1 1 20
2 1 40

답변: 그룹 연산 질문드립니다. 관리자 2016년 9월 20일 8:04 오전

다음과 같이 해보세요.

def mode(x):
return sp.stats.mode(x)[0][0]

most = df.result.groupby(df.test).agg(mode)