[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")

  1. qcut 명령으로 세 개의 나이 그룹을 만든다.

  2. 성별, 선실, 나이 그룹에 의한 생존율을 데이터프레임으로 계산한다.

    행에는 성별 및 나이 그룹에 대한 다중 인덱스를 사용하고 열에는 선실 인덱스를 사용한다.

    생존률은 해당 그룹의 생존 인원수를 전체 인원수로 나눈 값이다.

  3. 성별 및 선실에 의한 생존율을 피봇 데이터 형태로 만든다.

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