[Python] 데이터 사이언스 스쿨 - 4.7 피봇테이블과 그룹분석
Updated:
데이터 사이언스 스쿨 자료를 토대로 공부한 내용입니다.
실습과정에서 필요에 따라 내용의 누락 및 추가, 수정사항이 있습니다.
4.7 피봇테이블과 그룹분석
미리보는 요약 정리
pivot(행으로 사용할 열이름, 열로 사용할 열이름, 값)
- 행/열별 중복이 있으면 오류가 생기고 단순히 값만 추출
- 엑셀의 피봇 형태에서 값만 기존 값인 형태
groupby(행으로 사용할 열이름).그룹별 연산 함수( )
- 행/열별 중복에 따른 그룹별 연산값 추출
- 엑셀의 피봇 형태와 가까우나 열 인덱스를 직접 지정할 수 없음
- unstack()을 이용해서 엑셀의 피봇 형태 만들 수 있음
pivot_table(값, 행으로 사용할 열이름, 열로 사용할 열이름, 추가옵션)
- 행/열별 중복이 없으면 pivot 결과 출력 가능
- 열로 사용할 열이름을 지정하지 않으면 groupby 형태 출력 가능
- 엑셀의 피봇 형태
PIVOT
import numpy as np
import pandas as pd
data = {
"도시": ["서울", "서울", "서울", "부산", "부산", "부산", "인천", "인천"],
"연도": ["2015", "2010", "2005", "2015", "2010", "2005", "2015", "2010"],
"인구": [9904312, 9631482, 9762546, 3448737, 3393191, 3512547, 2890451, 263203],
"지역": ["수도권", "수도권", "수도권", "경상권", "경상권", "경상권", "수도권", "수도권"]
}
columns = ["도시", "연도", "인구", "지역"]
df1 = pd.DataFrame(data, columns=columns)
df1
도시 | 연도 | 인구 | 지역 | |
---|---|---|---|---|
0 | 서울 | 2015 | 9904312 | 수도권 |
1 | 서울 | 2010 | 9631482 | 수도권 |
2 | 서울 | 2005 | 9762546 | 수도권 |
3 | 부산 | 2015 | 3448737 | 경상권 |
4 | 부산 | 2010 | 3393191 | 경상권 |
5 | 부산 | 2005 | 3512547 | 경상권 |
6 | 인천 | 2015 | 2890451 | 수도권 |
7 | 인천 | 2010 | 263203 | 수도권 |
# pivot(행, 열, 값 ) - 모두 열 인덱스를 넣어야함
df1.pivot("도시","연도","인구")
연도 | 2005 | 2010 | 2015 |
---|---|---|---|
도시 | |||
부산 | 3512547.0 | 3393191.0 | 3448737.0 |
서울 | 9762546.0 | 9631482.0 | 9904312.0 |
인천 | NaN | 263203.0 | 2890451.0 |
-
pivot()
을 사용하면 지정한 행, 열의 조합에 따른 값을 확인 할 수 있다. -
위 예시에서 인천은 2005년 데이터가 없기 때문에 NaN 값으로 나타난다.
df1.set_index(["도시", "연도"])[["인구"]].unstack(1)
인구 | |||
---|---|---|---|
연도 | 2005 | 2010 | 2015 |
도시 | |||
부산 | 3512547.0 | 3393191.0 | 3448737.0 |
서울 | 9762546.0 | 9631482.0 | 9904312.0 |
인천 | NaN | 263203.0 | 2890451.0 |
set_index()
와unstack()
으로 같은 결과를 출력 가능하다.
# 다중 인덱스 피봇 테이블
df1.pivot(["지역", "도시"], "연도", "인구")
연도 | 2005 | 2010 | 2015 | |
---|---|---|---|---|
지역 | 도시 | |||
경상권 | 부산 | 3512547.0 | 3393191.0 | 3448737.0 |
수도권 | 서울 | 9762546.0 | 9631482.0 | 9904312.0 |
인천 | NaN | 263203.0 | 2890451.0 |
- 다중 인덱스인 경우도
pivot()
을 사용 가능하다.
# set_index와 unstack으로 같은 결과 출력
df1.set_index(["지역","도시", "연도"])[["인구"]].unstack(2)
인구 | ||||
---|---|---|---|---|
연도 | 2005 | 2010 | 2015 | |
지역 | 도시 | |||
경상권 | 부산 | 3512547.0 | 3393191.0 | 3448737.0 |
수도권 | 서울 | 9762546.0 | 9631482.0 | 9904312.0 |
인천 | NaN | 263203.0 | 2890451.0 |
- 다중 인덱스도 마찬가지로
set_index()
와unstack()
으로 같은 결과를 출력 가능하다.
try:
df1.pivot("지역", "연도", "인구")
except ValueError as e:
print("ValueError:", e)
ValueError: Index contains duplicate entries, cannot reshape
-
pivot()
함수를 사용할 때 주의할 점은 행과 열의 조합에 따른 값은 반드시 1개가 존재하여야한다. -
그렇지 않은 경우 에러가 발생한다.
GROUP BY
np.random.seed(0)
df2 = pd.DataFrame({
'key1': ['A', 'A', 'B', 'B', 'A'],
'key2': ['one', 'two', 'one', 'two', 'one'],
'data1': [1, 2, 3, 4, 5],
'data2': [10, 20, 30, 40, 50]
})
df2
key1 | key2 | data1 | data2 | |
---|---|---|---|---|
0 | A | one | 1 | 10 |
1 | A | two | 2 | 20 |
2 | B | one | 3 | 30 |
3 | B | two | 4 | 40 |
4 | A | one | 5 | 50 |
# key1에 따른 그룹 데이터
g = df2.groupby(df2.key1)
g
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000002BC4BFB8220>
# 그룹별 인덱스 위치
g.groups
{'A': [0, 1, 4], 'B': [2, 3]}
-
groupby()
함수를 사용하면 특정 열에 따른 그룹 데이터를 생성 가능하다. -
groupby()
로 생성된 그룹 데이터 객체에groups
속성을 사용하면 그룹별 인덱스 위치를 확인 할 수 있다. -
그룹 데이터 객체를 이용하여서 여러 그룹별 연산을 실행가능하다.
그룹별 연산 예시 - 내장함수
-
size, count: 그룹 데이터의 갯수
-
mean, median, min, max: 그룹 데이터의 평균, 중앙값, 최소, 최대
-
sum, prod, std, var, quantile : 그룹 데이터의 합계, 곱, 표준편차, 분산, 사분위수
-
first, last: 그룹 데이터 중 가장 첫번째 데이터와 가장 나중 데이터
# 자동으로 숫자형만 출력
g.sum()
data1 | data2 | |
---|---|---|
key1 | ||
A | 8 | 80 |
B | 7 | 70 |
- 앞서 생성한 그룹 데이터를 이용하여서 key1의 값별로 data1, data2의 합계를 확인하였다.
# 하나의 열만 보고 싶은 경우 - 3가지 모두 같은 결과
df2.data1.groupby(df2.key1).sum()
df2.groupby(df2.key1)["data1"].sum()
df2.groupby(df2.key1).sum()["data1"]
key1
A 8
B 7
Name: data1, dtype: int64
- 만약 key1의 값별로 하나의 열만 합계를 보고 싶다면 위 3가지 방법을 적용한다.
연습문제 4.7.1
key1의 값을 기준으로 data1의 값을 분류하여 합계를 구한 결과를 시리즈가 아닌 데이터프레임으로 구한다.
# 그룹별 합계 데이터
df2.groupby(df2["key1"]).sum()[["data1"]]
data1 | |
---|---|
key1 | |
A | 8 |
B | 7 |
복합키 사용
df2.groupby([df2.key1, df2.key2]).sum()[["data1"]]
data1 | ||
---|---|---|
key1 | key2 | |
A | one | 6 |
two | 2 | |
B | one | 3 |
two | 4 |
- 여러 열을 지정하여서 그룹별 연산을 수행할 수 있다.
# unstack을 이용하여 피봇테이블 형태1
df2.groupby([df2.key1, df2.key2]).sum()[["data1"]].unstack("key2")
data1 | ||
---|---|---|
key2 | one | two |
key1 | ||
A | 6 | 2 |
B | 3 | 4 |
# unstack을 이용하여 피봇테이블 형태2
df1["인구"].groupby([df1["지역"], df1["연도"]]).sum().unstack("연도")
연도 | 2005 | 2010 | 2015 |
---|---|---|---|
지역 | |||
경상권 | 3512547 | 3393191 | 3448737 |
수도권 | 9762546 | 9894685 | 12794763 |
- 여러 열을 지정해서 그룹별 연산 수행 후
unstack()
을 사용하면 뒤에 나올pivot_table()
형태로 만들 수 있다.
# pivot을 이용해서 같은 결과 출력
temp = df2.groupby([df2.key1, df2.key2]).sum()[["data1"]]
temp = temp.reset_index()
temp.pivot("key1","key2","data1")
key2 | one | two |
---|---|---|
key1 | ||
A | 6 | 2 |
B | 3 | 4 |
-
위 예시는
pivot()
을 이용해서groupby()
결과를 출력해보았다. -
다만
pivot()
을 사용하려고 사전 작업으로groupby()
를 사용하였고 굳이 이렇게 할 필요가 없어 보인다.
# iris 데이터
import seaborn as sns
iris = sns.load_dataset("iris")
iris.head()
sepal_length | sepal_width | petal_length | petal_width | species | |
---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | setosa |
1 | 4.9 | 3.0 | 1.4 | 0.2 | setosa |
2 | 4.7 | 3.2 | 1.3 | 0.2 | setosa |
3 | 4.6 | 3.1 | 1.5 | 0.2 | setosa |
4 | 5.0 | 3.6 | 1.4 | 0.2 | setosa |
- 이번엔 iris 데이터를 이용해서 여러 그룹별 연산을 시행해보자.
그룹별 연산 예시
-
agg
-
만약 원하는 그룹연산이 없는 경우 함수를 만들고 이 함수를 agg에 전달한다.
-
또는 여러가지 그룹연산을 동시에 하고 싶은 경우 함수 이름 문자열의 리스트를 전달한다.
-
-
describe
- 하나의 그룹 대표값이 아니라 여러개의 값을 데이터프레임으로 구한다.
-
apply
- describe 처럼 하나의 대표값이 아닌 데이터프레임을 출력하지만 원하는 그룹연산이 없는 경우에 사용한다.
-
transform
- 그룹에 대한 대표값을 만드는 것이 아니라 그룹별 계산을 통해 데이터 자체를 변형한다.
# agg
def peak_to_peak_ratio(x):
return x.max() / x.min()
iris.groupby(iris.species).agg(peak_to_peak_ratio)
sepal_length | sepal_width | petal_length | petal_width | |
---|---|---|---|---|
species | ||||
setosa | 1.348837 | 1.913043 | 1.900000 | 6.000000 |
versicolor | 1.428571 | 1.700000 | 1.700000 | 1.800000 |
virginica | 1.612245 | 1.727273 | 1.533333 | 1.785714 |
-
agg()
함수에 최대값/최소값을 산출하는peak_to_peak_ratio()
함수를 입력하였다. -
붓꽃 종별로
peak_to_peak_ratio()
의 결과 값이 잘 나타난다. -
agg()
에는 하나의 값을 산출하는 함수를 넣어주어야 한다.
# describe
iris.groupby(iris.species).describe().T
species | setosa | versicolor | virginica | |
---|---|---|---|---|
sepal_length | count | 50.000000 | 50.000000 | 50.000000 |
mean | 5.006000 | 5.936000 | 6.588000 | |
std | 0.352490 | 0.516171 | 0.635880 | |
min | 4.300000 | 4.900000 | 4.900000 | |
25% | 4.800000 | 5.600000 | 6.225000 | |
50% | 5.000000 | 5.900000 | 6.500000 | |
75% | 5.200000 | 6.300000 | 6.900000 | |
max | 5.800000 | 7.000000 | 7.900000 | |
sepal_width | count | 50.000000 | 50.000000 | 50.000000 |
mean | 3.428000 | 2.770000 | 2.974000 | |
std | 0.379064 | 0.313798 | 0.322497 | |
min | 2.300000 | 2.000000 | 2.200000 | |
25% | 3.200000 | 2.525000 | 2.800000 | |
50% | 3.400000 | 2.800000 | 3.000000 | |
75% | 3.675000 | 3.000000 | 3.175000 | |
max | 4.400000 | 3.400000 | 3.800000 | |
petal_length | count | 50.000000 | 50.000000 | 50.000000 |
mean | 1.462000 | 4.260000 | 5.552000 | |
std | 0.173664 | 0.469911 | 0.551895 | |
min | 1.000000 | 3.000000 | 4.500000 | |
25% | 1.400000 | 4.000000 | 5.100000 | |
50% | 1.500000 | 4.350000 | 5.550000 | |
75% | 1.575000 | 4.600000 | 5.875000 | |
max | 1.900000 | 5.100000 | 6.900000 | |
petal_width | count | 50.000000 | 50.000000 | 50.000000 |
mean | 0.246000 | 1.326000 | 2.026000 | |
std | 0.105386 | 0.197753 | 0.274650 | |
min | 0.100000 | 1.000000 | 1.400000 | |
25% | 0.200000 | 1.200000 | 1.800000 | |
50% | 0.200000 | 1.300000 | 2.000000 | |
75% | 0.300000 | 1.500000 | 2.300000 | |
max | 0.600000 | 1.800000 | 2.500000 |
describe()
는 기술 통계량 산출해주는 함수로 꼭groupby()
가 아니어도 사용 가능하다.
# apply
# petal_length가 가장 큰 3개 행 출력
def top3_petal_length(df):
top3_pl = df.sort_values(by="petal_length", ascending=False)[:3]
top3_pl = top3_pl[["sepal_length", "sepal_width", "petal_length", "petal_width"]]
return top3_pl
iris.groupby(iris.species).apply(top3_petal_length)
sepal_length | sepal_width | petal_length | petal_width | ||
---|---|---|---|---|---|
species | |||||
setosa | 24 | 4.8 | 3.4 | 1.9 | 0.2 |
44 | 5.1 | 3.8 | 1.9 | 0.4 | |
23 | 5.1 | 3.3 | 1.7 | 0.5 | |
versicolor | 83 | 6.0 | 2.7 | 5.1 | 1.6 |
77 | 6.7 | 3.0 | 5.0 | 1.7 | |
72 | 6.3 | 2.5 | 4.9 | 1.5 | |
virginica | 118 | 7.7 | 2.6 | 6.9 | 2.3 |
117 | 7.7 | 3.8 | 6.7 | 2.2 | |
122 | 7.7 | 2.8 | 6.7 | 2.0 |
-
apply()
는agg()
와 비슷하지만 하나의 값이 아닌 여러개의 값을 출력한다. -
위 예시에선 각 붓꽃 종별로 petal_length가 가장 큰 3개의 행을 출력하였다.
# transform
# 붓꽃 종별 petal_length의 소/중/대 구분
def q3cut(s):
return pd.qcut(s, 3, labels=["소", "중", "대"]).astype(str)
iris["petal_length_class"] = iris.groupby(iris.species)["petal_length"].transform(q3cut)
iris.tail(10)
sepal_length | sepal_width | petal_length | petal_width | species | petal_length_class | |
---|---|---|---|---|---|---|
140 | 6.7 | 3.1 | 5.6 | 2.4 | virginica | 중 |
141 | 6.9 | 3.1 | 5.1 | 2.3 | virginica | 소 |
142 | 5.8 | 2.7 | 5.1 | 1.9 | virginica | 소 |
143 | 6.8 | 3.2 | 5.9 | 2.3 | virginica | 대 |
144 | 6.7 | 3.3 | 5.7 | 2.5 | virginica | 중 |
145 | 6.7 | 3.0 | 5.2 | 2.3 | virginica | 소 |
146 | 6.3 | 2.5 | 5.0 | 1.9 | virginica | 소 |
147 | 6.5 | 3.0 | 5.2 | 2.0 | virginica | 소 |
148 | 6.2 | 3.4 | 5.4 | 2.3 | virginica | 중 |
149 | 5.9 | 3.0 | 5.1 | 1.8 | virginica | 소 |
-
transform()
은 그룹별 대표값을 만드는 것이 아니라 그룹별 계산을 통해 데이터프레임 자체를 변화시킨다. -
위 예시에선 붓꽃 종별로 petal_length를 소/중/대로 구분하였다.
-
작업 순서는 iris 그룹 데이터 생성 후, 그룹 데이터에서 petal_length를 선택, 마지막으로
transform()
이다.
연습문제 4.7.2
붓꽃(iris) 데이터에서 붓꽃 종(species)별로 꽃잎길이(sepal_length), 꽃잎폭(sepal_width) 등의 평균을 구하라.
만약 붓꽃 종(species)이 표시되지 않았을 때 이 수치들을 이용하여 붓꽃 종을 찾아낼 수 있을지 생각하라.
# 붓꽃 종별 평균
iris.groupby(iris.species).mean()
sepal_length | sepal_width | petal_length | petal_width | |
---|---|---|---|---|
species | ||||
setosa | 5.006 | 3.428 | 1.462 | 0.246 |
versicolor | 5.936 | 2.770 | 4.260 | 1.326 |
virginica | 6.588 | 2.974 | 5.552 | 2.026 |
# 붓꽃 종별 평균을 중복으로 넣음
temp = iris.groupby(iris.species).transform("mean")
temp.columns = ["mean_sl", "mean_sw", "mean_pl", "mean_pw"]
temp2 = pd.concat([iris,temp], axis=1)
temp2
sepal_length | sepal_width | petal_length | petal_width | species | petal_length_class | mean_sl | mean_sw | mean_pl | mean_pw | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | setosa | 소 | 5.006 | 3.428 | 1.462 | 0.246 |
1 | 4.9 | 3.0 | 1.4 | 0.2 | setosa | 소 | 5.006 | 3.428 | 1.462 | 0.246 |
2 | 4.7 | 3.2 | 1.3 | 0.2 | setosa | 소 | 5.006 | 3.428 | 1.462 | 0.246 |
3 | 4.6 | 3.1 | 1.5 | 0.2 | setosa | 중 | 5.006 | 3.428 | 1.462 | 0.246 |
4 | 5.0 | 3.6 | 1.4 | 0.2 | setosa | 소 | 5.006 | 3.428 | 1.462 | 0.246 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
145 | 6.7 | 3.0 | 5.2 | 2.3 | virginica | 소 | 6.588 | 2.974 | 5.552 | 2.026 |
146 | 6.3 | 2.5 | 5.0 | 1.9 | virginica | 소 | 6.588 | 2.974 | 5.552 | 2.026 |
147 | 6.5 | 3.0 | 5.2 | 2.0 | virginica | 소 | 6.588 | 2.974 | 5.552 | 2.026 |
148 | 6.2 | 3.4 | 5.4 | 2.3 | virginica | 중 | 6.588 | 2.974 | 5.552 | 2.026 |
149 | 5.9 | 3.0 | 5.1 | 1.8 | virginica | 소 | 6.588 | 2.974 | 5.552 | 2.026 |
150 rows × 10 columns
# 붓꽃 종별 평균만 알때 종류 변수 추가
temp2["species2"] = temp2.apply(lambda x: "setosa" if round(x.mean_sl,3) == 5.006
else "versicolor" if round(x.mean_sl,3) == 5.936
else "vriginica", axis=1)
temp2
sepal_length | sepal_width | petal_length | petal_width | species | petal_length_class | mean_sl | mean_sw | mean_pl | mean_pw | species2 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | setosa | 소 | 5.006 | 3.428 | 1.462 | 0.246 | setosa |
1 | 4.9 | 3.0 | 1.4 | 0.2 | setosa | 소 | 5.006 | 3.428 | 1.462 | 0.246 | setosa |
2 | 4.7 | 3.2 | 1.3 | 0.2 | setosa | 소 | 5.006 | 3.428 | 1.462 | 0.246 | setosa |
3 | 4.6 | 3.1 | 1.5 | 0.2 | setosa | 중 | 5.006 | 3.428 | 1.462 | 0.246 | setosa |
4 | 5.0 | 3.6 | 1.4 | 0.2 | setosa | 소 | 5.006 | 3.428 | 1.462 | 0.246 | setosa |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
145 | 6.7 | 3.0 | 5.2 | 2.3 | virginica | 소 | 6.588 | 2.974 | 5.552 | 2.026 | vriginica |
146 | 6.3 | 2.5 | 5.0 | 1.9 | virginica | 소 | 6.588 | 2.974 | 5.552 | 2.026 | vriginica |
147 | 6.5 | 3.0 | 5.2 | 2.0 | virginica | 소 | 6.588 | 2.974 | 5.552 | 2.026 | vriginica |
148 | 6.2 | 3.4 | 5.4 | 2.3 | virginica | 중 | 6.588 | 2.974 | 5.552 | 2.026 | vriginica |
149 | 5.9 | 3.0 | 5.1 | 1.8 | virginica | 소 | 6.588 | 2.974 | 5.552 | 2.026 | vriginica |
150 rows × 11 columns
-
나는 이 연습문제를 풀 때 붓꽃 종별 평균을 원래 데이터 프레임에 중복으로 넣었다.
-
그 후 평균 수치를 알고 있으므로 특정 값이면 species2를 만들었다.
-
풀이는 여러 방법이 가능하고, 나처럼 종별 평균을 중복으로 붙일 때도
transform()
이 아닌 일반적인mean()
적용 후pd.merge()
를 사용할 수도 있을 것이다. -
여기선 이 챕터의 함수를 활용하려고 위와 같이 풀어보았다.
PIVOT TABLE
pivot_table()
은 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: 마진 열(행)의 이름
-
df1
도시 | 연도 | 인구 | 지역 | |
---|---|---|---|---|
0 | 서울 | 2015 | 9904312 | 수도권 |
1 | 서울 | 2010 | 9631482 | 수도권 |
2 | 서울 | 2005 | 9762546 | 수도권 |
3 | 부산 | 2015 | 3448737 | 경상권 |
4 | 부산 | 2010 | 3393191 | 경상권 |
5 | 부산 | 2005 | 3512547 | 경상권 |
6 | 인천 | 2015 | 2890451 | 수도권 |
7 | 인천 | 2010 | 263203 | 수도권 |
df1.pivot_table("인구", "도시", "연도")
연도 | 2005 | 2010 | 2015 |
---|---|---|---|
도시 | |||
부산 | 3512547.0 | 3393191.0 | 3448737.0 |
서울 | 9762546.0 | 9631482.0 | 9904312.0 |
인천 | NaN | 263203.0 | 2890451.0 |
-
중복 값이 없기 때문에
pivot()
과 같은 결과가 출력된다. -
pivot()
은 행, 열, 값 순으로 입력하지만pivot_table()
은 값, 행, 열 순임을 기억하자.
# 마진열 추가
df1.pivot_table("인구", "도시", "연도", margins = True, margins_name = "행 평균")
연도 | 2005 | 2010 | 2015 | 행 평균 |
---|---|---|---|---|
도시 | ||||
부산 | 3512547.0 | 3393191.0 | 3448737.0 | 3.451492e+06 |
서울 | 9762546.0 | 9631482.0 | 9904312.0 | 9.766113e+06 |
인천 | NaN | 263203.0 | 2890451.0 | 1.576827e+06 |
행 평균 | 6637546.5 | 4429292.0 | 5414500.0 | 5.350809e+06 |
-
margins 옵션을 True로 설정하면 각 행, 열별 그룹 연산값이 추가된다.
-
위 예시에선 aggfunc 옵션이 디폴트가 mean이므로 각 행, 열별로 평균값이 추가된다.
# 행 인덱스만 넣은 경우
df1.pivot_table("인구", index=["연도", "도시"])
인구 | ||
---|---|---|
연도 | 도시 | |
2005 | 부산 | 3512547 |
서울 | 9762546 | |
2010 | 부산 | 3393191 |
서울 | 9631482 | |
인천 | 263203 | |
2015 | 부산 | 3448737 |
서울 | 9904312 | |
인천 | 2890451 |
- 열 인덱스를 지정하지 않은 경우
groupby()
결과 형태로 출력된다.
# tips 데이터
tips = sns.load_dataset("tips")
# 팁 비율 = 팁 금액 / 전체 금액
tips['tip_pct'] = tips['tip'] / tips['total_bill']
tips.tail()
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 |
-
이번엔 tips 데이터를 이용해서 여러 그룹별 연산을 시행해보자.
-
tip_pct라는 팁 비율 열을 추가해주었다.
-
나는 함수가 어떤 기능인지를 확인하는 것에 초점을 두어서 뒤에 나올 그룹 연산 결과 해석은 거의 하지 않았다.
# 기술 통계량 확인
tips.describe()
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 |
# 성별 갯수
tips.groupby("sex").count()
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 |
tips.groupby("sex").size()
sex
Male 157
Female 87
dtype: int64
-
size()
를 사용하면count()
와 달리 여러 변수로 갯수가 나타나지 않는다. -
위 예시로 생각하면 남자/여자가 몇 명인지 확인하는데 굳이 여러 변수로 볼 필요가 없다.
-
다만
size()
는 NaN을 포함해서 결과를 출력하므로 주의해야 한다.
# 성별/흡연유무별 갯수
tips.groupby(["sex", "smoker"]).size()
sex smoker
Male Yes 60
No 97
Female Yes 33
No 54
dtype: int64
# 피봇테이블 형태로 위 셀에서 unstack을 한것에 마진이 추가되있음
tips.pivot_table("tip_pct", "sex", "smoker", aggfunc="count", margins=True)
smoker | Yes | No | All |
---|---|---|---|
sex | |||
Male | 60 | 97 | 157 |
Female | 33 | 54 | 87 |
All | 93 | 151 | 244 |
# 성별 팁 비율
tips.groupby("sex")[["tip_pct"]].mean()
tip_pct | |
---|---|
sex | |
Male | 0.157651 |
Female | 0.166491 |
# 성별 팁 비율 - 피봇 테이블 이용
tips.pivot_table("tip_pct", "sex")
tip_pct | |
---|---|
sex | |
Male | 0.157651 |
Female | 0.166491 |
# 성별/흡연유무별 평균 팁 비율
tips.pivot_table("tip_pct", "sex", "smoker")
smoker | Yes | No |
---|---|---|
sex | ||
Male | 0.152771 | 0.160669 |
Female | 0.182150 | 0.156921 |
tips.groupby(["sex", "smoker"])[["tip_pct"]].describe().T
sex | Male | Female | |||
---|---|---|---|---|---|
smoker | Yes | No | Yes | No | |
tip_pct | count | 60.000000 | 97.000000 | 33.000000 | 54.000000 |
mean | 0.152771 | 0.160669 | 0.182150 | 0.156921 | |
std | 0.090588 | 0.041849 | 0.071595 | 0.036421 | |
min | 0.035638 | 0.071804 | 0.056433 | 0.056797 | |
25% | 0.101845 | 0.131810 | 0.152439 | 0.139708 | |
50% | 0.141015 | 0.157604 | 0.173913 | 0.149691 | |
75% | 0.191697 | 0.186220 | 0.198216 | 0.181630 | |
max | 0.710345 | 0.291990 | 0.416667 | 0.252672 |
연습문제 4.7.3
팁의 비율이 요일과 점심/저녁 여부, 인원수에 어떤 영향을 받는지 살펴본다.
어떤 요인이 가장 크게 작용하는지 판단할 수 있는 방법이 있는가?
# 평균 팁 비율 기술 통계량
tips[["tip_pct"]].describe()
tip_pct | |
---|---|
count | 244.000000 |
mean | 0.160803 |
std | 0.061072 |
min | 0.035638 |
25% | 0.129127 |
50% | 0.154770 |
75% | 0.191475 |
max | 0.710345 |
# 요일별로 분석
tips.groupby("day")[["tip_pct"]].describe().T
day | Thur | Fri | Sat | Sun | |
---|---|---|---|---|---|
tip_pct | count | 62.000000 | 19.000000 | 87.000000 | 76.000000 |
mean | 0.161276 | 0.169913 | 0.153152 | 0.166897 | |
std | 0.038652 | 0.047665 | 0.051293 | 0.084739 | |
min | 0.072961 | 0.103555 | 0.035638 | 0.059447 | |
25% | 0.138210 | 0.133739 | 0.123863 | 0.119982 | |
50% | 0.153846 | 0.155625 | 0.151832 | 0.161103 | |
75% | 0.192687 | 0.196637 | 0.188271 | 0.187889 | |
max | 0.266312 | 0.263480 | 0.325733 | 0.710345 |
- 금요일에 평균 팁 비율이 가장 높았으나 금요일 손님이 가장 적다.
# 시간별로 분석
tips.groupby("time")[["tip_pct"]].describe().T
time | Lunch | Dinner | |
---|---|---|---|
tip_pct | count | 68.000000 | 176.000000 |
mean | 0.164128 | 0.159518 | |
std | 0.040242 | 0.067477 | |
min | 0.072961 | 0.035638 | |
25% | 0.139147 | 0.123192 | |
50% | 0.154084 | 0.155400 | |
75% | 0.193917 | 0.188209 | |
max | 0.266312 | 0.710345 |
- 점심에 비해 저녁 손님이 더 많았고 평균 팁 비율은 점심 손님이 더 높다.
# 인원수별로 분석
tips.groupby("size")[["tip_pct"]].describe().T
size | 1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|---|
tip_pct | count | 4.000000 | 156.000000 | 38.000000 | 37.000000 | 5.000000 | 4.000000 |
mean | 0.217292 | 0.165719 | 0.152157 | 0.145949 | 0.141495 | 0.156229 | |
std | 0.080342 | 0.066848 | 0.045459 | 0.042395 | 0.067733 | 0.042153 | |
min | 0.137931 | 0.035638 | 0.056433 | 0.077459 | 0.065660 | 0.103799 | |
25% | 0.170779 | 0.135223 | 0.124758 | 0.117750 | 0.106572 | 0.131654 | |
50% | 0.202752 | 0.156104 | 0.159323 | 0.146699 | 0.121389 | 0.162891 | |
75% | 0.249265 | 0.195036 | 0.186135 | 0.169797 | 0.172194 | 0.187466 | |
max | 0.325733 | 0.710345 | 0.230742 | 0.280535 | 0.241663 | 0.195335 |
- 2인 손님이 가장 많고 평균 팁 비율도 가장 높다.
# 점심시간 요일/인원수별 평균 팁 비율
temp = tips.pivot_table("tip_pct","day",["time","size"], fill_value = 0, margins= True)
temp["Lunch"]
size | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
day | ||||||
Thur | 0.181728 | 0.164024 | 0.144599 | 0.145515 | 0.121389 | 0.173706 |
Fri | 0.223776 | 0.181969 | 0.187735 | 0.000000 | 0.000000 | 0.000000 |
Sat | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
Sun | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
All | 0.202752 | 0.165750 | 0.153226 | 0.145515 | 0.121389 | 0.173706 |
# 저녁시간 요일/인원수별 평균 팁 비율
temp["Dinner"]
size | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
day | ||||||
Thur | 0.000000 | 0.159744 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
Fri | 0.000000 | 0.162659 | 0.000000 | 0.117750 | 0.000000 | 0.000000 |
Sat | 0.231832 | 0.155289 | 0.151439 | 0.138289 | 0.106572 | 0.000000 |
Sun | 0.000000 | 0.180870 | 0.152662 | 0.153168 | 0.159839 | 0.103799 |
All | 0.231832 | 0.165704 | 0.151995 | 0.146017 | 0.146522 | 0.103799 |
# tip의 최대 - 최소
def peak_to_peak(x):
return x.max() - x.min()
tips.groupby(["sex", "smoker"])[["tip"]].agg(peak_to_peak)
tip | ||
---|---|---|
sex | smoker | |
Male | Yes | 9.00 |
No | 7.75 | |
Female | Yes | 5.50 |
No | 4.20 |
여러 연산 동시에 수행시
tips.groupby(["sex", "smoker"])[["total_bill"]].agg(["mean", peak_to_peak])
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 |
- 여러 연산을 동시에 수행하고 싶을땐 리스트를 활용한다.
열별로 다른 연산
tips.groupby(["sex", "smoker"]).agg({'tip_pct': 'count', 'total_bill': peak_to_peak})
tip_pct | total_bill | ||
---|---|---|---|
sex | smoker | ||
Male | Yes | 60 | 43.56 |
No | 97 | 40.82 | |
Female | Yes | 33 | 41.23 |
No | 54 | 28.58 |
- 만약 각 열별로 다른 연산을 하고 싶으면 dictionary를 활용한다.
# 값2, 행2, 열1
tips.pivot_table(['tip_pct', 'size'], ['sex', 'day'], 'smoker')
size | tip_pct | ||||
---|---|---|---|---|---|
smoker | Yes | No | Yes | No | |
sex | day | ||||
Male | Thur | 2.300000 | 2.500000 | 0.164417 | 0.165706 |
Fri | 2.125000 | 2.000000 | 0.144730 | 0.138005 | |
Sat | 2.629630 | 2.656250 | 0.139067 | 0.162132 | |
Sun | 2.600000 | 2.883721 | 0.173964 | 0.158291 | |
Female | Thur | 2.428571 | 2.480000 | 0.163073 | 0.155971 |
Fri | 2.000000 | 2.500000 | 0.209129 | 0.165296 | |
Sat | 2.200000 | 2.307692 | 0.163817 | 0.147993 | |
Sun | 2.500000 | 3.071429 | 0.237075 | 0.165710 |
# 값1, 행3, 열1
tips.pivot_table('size', ['time', 'sex', 'smoker'], 'day',
aggfunc='sum', fill_value=0)
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 |
연습문제 4.7.4
타이타닉 승객 데이터를 이용하여 다음 분석을 실시하라. 데이터는 다음과 같이 받을 수 있다.
titanic = sns.load_dataset("titanic")
-
qcut 명령으로 세 개의 나이 그룹을 만든다.
-
성별, 선실, 나이 그룹에 의한 생존율을 데이터프레임으로 계산한다.
행에는 성별 및 나이 그룹에 대한 다중 인덱스를 사용하고 열에는 선실 인덱스를 사용한다.
생존률은 해당 그룹의 생존 인원수를 전체 인원수로 나눈 값이다.
-
성별 및 선실에 의한 생존율을 피봇 데이터 형태로 만든다.
titanic = sns.load_dataset("titanic")
titanic.head()
survived | pclass | sex | age | sibsp | parch | fare | embarked | class | who | adult_male | deck | embark_town | alive | alone | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 3 | male | 22.0 | 1 | 0 | 7.2500 | S | Third | man | True | NaN | Southampton | no | False |
1 | 1 | 1 | female | 38.0 | 1 | 0 | 71.2833 | C | First | woman | False | C | Cherbourg | yes | False |
2 | 1 | 3 | female | 26.0 | 0 | 0 | 7.9250 | S | Third | woman | False | NaN | Southampton | yes | True |
3 | 1 | 1 | female | 35.0 | 1 | 0 | 53.1000 | S | First | woman | False | C | Southampton | yes | False |
4 | 0 | 3 | male | 35.0 | 0 | 0 | 8.0500 | S | Third | man | True | NaN | Southampton | no | True |
# age의 NaN이 포함되어 있음
titanic.count()
survived 891
pclass 891
sex 891
age 714
sibsp 891
parch 891
fare 891
embarked 889
class 891
who 891
adult_male 891
deck 203
embark_town 889
alive 891
alone 891
dtype: int64
# NaN 값은 임의로 평균 age 값으로 대체
titanic["age"] = titanic["age"].fillna(titanic.age.mean())
# 1. 세 개의 나이그룹 생성
titanic["age_g"] = pd.qcut(titanic.age, 3, labels=["age1","age2","age3"])
titanic.head()
survived | pclass | sex | age | sibsp | parch | fare | embarked | class | who | adult_male | deck | embark_town | alive | alone | age_g | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 3 | male | 22.0 | 1 | 0 | 7.2500 | S | Third | man | True | NaN | Southampton | no | False | age1 |
1 | 1 | 1 | female | 38.0 | 1 | 0 | 71.2833 | C | First | woman | False | C | Cherbourg | yes | False | age3 |
2 | 1 | 3 | female | 26.0 | 0 | 0 | 7.9250 | S | Third | woman | False | NaN | Southampton | yes | True | age2 |
3 | 1 | 1 | female | 35.0 | 1 | 0 | 53.1000 | S | First | woman | False | C | Southampton | yes | False | age3 |
4 | 0 | 3 | male | 35.0 | 0 | 0 | 8.0500 | S | Third | man | True | NaN | Southampton | no | True | age3 |
# 2. 성별, 선실, 나이 그룹에 의한 생존율을 데이터프레임으로 계산한다.
# 행에는 성별 및 나이 그룹에 대한 다중 인덱스를 사용하고 열에는 선실 인덱스를 사용한다.
# 생존률은 해당 그룹의 생존 인원수를 전체 인원수로 나눈 값이다.
def rate(x):
return x.sum() / x.count()
titanic.groupby(["sex","age_g","class"])[["survived"]].agg(rate).unstack("class")
# 같은 결과 pivot_table
# titanic.pivot_table("survived", ["sex","age_g"], "class", aggfunc = rate)
survived | ||||
---|---|---|---|---|
class | First | Second | Third | |
sex | age_g | |||
female | age1 | 0.928571 | 0.965517 | 0.507692 |
age2 | 1.000000 | 0.888889 | 0.557377 | |
age3 | 0.979167 | 0.896552 | 0.277778 | |
male | age1 | 0.500000 | 0.277778 | 0.155039 |
age2 | 0.333333 | 0.103448 | 0.133803 | |
age3 | 0.361111 | 0.093023 | 0.105263 |
# 3 성별 및 선실에 의한 생존율을 피봇 데이터 형태로 만든다.
# 방법1
titanic.groupby(["sex","class"])[["survived"]].agg(rate).unstack("class")
survived | |||
---|---|---|---|
class | First | Second | Third |
sex | |||
female | 0.968085 | 0.921053 | 0.500000 |
male | 0.368852 | 0.157407 | 0.135447 |
# 방법2
# 1,0으로 이루어져 있으므로 그냥 mean을 하면 비율이 나옴
titanic.pivot_table("survived", "sex", "class")
class | First | Second | Third |
---|---|---|---|
sex | |||
female | 0.968085 | 0.921053 | 0.500000 |
male | 0.368852 | 0.157407 | 0.135447 |
# 방법3
titanic.pivot_table("survived", "sex", "class", aggfunc = rate)
class | First | Second | Third |
---|---|---|---|
sex | |||
female | 0.968085 | 0.921053 | 0.500000 |
male | 0.368852 | 0.157407 | 0.135447 |
Leave a comment