23. 데이터 그룹화#

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

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

23.1. 시리즈와 데이터프레임 그룹화#

groupby() 메서드는 지정된 키를 기준으로 데이터를 그룹으로 쪼갠 다음에 특정 집계 함수를 그룹별로 적용해서 그룹별로 하나의 값을 계산한다. 그리고 생성된 그룹과 그룹별로 계산된 값을 조합해서 새로운 데이터프레임 또는 시리즈를 생성한다. 이 과정을 간략하게 쪼개고, 적용하고, 조합하기split-apply-combine라 부른다.

집계 함수

집계 함수는 리스트 또는 1차원 어레이가 주어졌을 때 항목들을 대상으로 하나의 값을 계산하는 함수를 가리킨다. 예를 들어, 평균값을 계산하거나, 항목의 개수 등을 계산하는 함수 등이 대표적인 집계 함수다.

23.1.1. 쪼개고, 적용하고, 조합하기#

다음 데이터프레임을 이용하여 쪼개고, 적용하고, 조합하기 과정을 설명한다.

df = pd.DataFrame({"Key" : ["A", "B", "C", "A", "B", "C", "A", "B", "C"],
                   "Data" : [0, 5, 10, 5, 10, 15, 10, 15, 20]})
df
Key Data
0 A 0
1 B 5
2 C 10
3 A 5
4 B 10
5 C 15
6 A 10
7 B 15
8 C 20

아래 코드는 Key 열에 속한 "A", "B", "C" 세 값을 기준으로 3 개의 그룹으로 쪼갠 후 각 그룹에 속합 값들을 열별로 더한 결과를 이용하애 새로운 데이터프레임을 생성한다.

df.groupby("Key").sum()
Data
Key
A 15
B 30
C 45

위 코드의 실행 과정에 포함된 쪼개고, 적용하고, 조합하기를 잘 묘사하면 다음과 같다.

<그림 출처: Python for Data Analysis>

다음 데이터프레임을 활용한다.

df = pd.DataFrame({"key1" : ["a", "a", None, "b", "b", "a", None],
                   "key2" : pd.Series(["one", "two", "one", "two", "one", None, "one"]),
                   "data1" : np.random.standard_normal(7),
                   "data2" : np.random.standard_normal(7)})
df
key1 key2 data1 data2
0 a one -0.204708 0.281746
1 a two 0.478943 0.769023
2 None one -0.519439 1.246435
3 b two -0.555730 1.007189
4 b one 1.965781 -1.296221
5 a None 1.393406 0.274992
6 None one 0.092908 0.228913

시리즈 그룹화

아래 시리즈를 이용하여 먼저 시리즈 그룹화를 설명한다.

s = df["data1"]
s
0   -0.204708
1    0.478943
2   -0.519439
3   -0.555730
4    1.965781
5    1.393406
6    0.092908
Name: data1, dtype: float64

아래 코드는 df["key1"] 시리즈의 항목을 기준으로 그룹화를 진행하며, 동일한 행에 위치한 값들을 그룹화 한다. 하지만 groupby() 메서드는 GroupBy 객체를 생성하며 실제로 그룹화를 진행하지는 않고 그룹화에 필요한 정보만 계산해 놓는다.

k1 = df["key1"]

s.groupby(k1)
<pandas.core.groupby.generic.SeriesGroupBy object at 0x000001D3F460F5E0>

실제 그룹화는 집계 메서드를 실행할 때 이뤄진다.

  • valut_counts() 메서드: 그룹별 항목 및 항목 수 확인

s.groupby(k1).value_counts()
key1  data1    
a     -0.204708    1
       0.478943    1
       1.393406    1
b     -0.555730    1
       1.965781    1
Name: data1, dtype: int64
  • mean() 메서드: 그룹별 평균값 계산

grouped = s.groupby(k1).mean()
grouped
key1
a    0.555881
b    0.705025
Name: data1, dtype: float64

두 개 이상의 키를 기준으로 그룹화 진행 가능. 아래 코드는 df["key1"]df["key2"] 열에 속한 값들을 기준으로 그룹화 진행.

k2 = df["key2"]

means = s.groupby([k1, k2]).mean()
means
key1  key2
a     one    -0.204708
      two     0.478943
b     one     1.965781
      two    -0.555730
Name: data1, dtype: float64

여러 개의 키를 이용하여 그룹화를 한 경우 언스택을 활용하면 보다 보기 좋은 데이터프레임을 생성한다.

means.unstack()
key2 one two
key1
a -0.204708 0.478943
b 1.965781 -0.555730

어레이 또는 리스트를 키로 활용하는 것도 가능하다. 시리즈와 마찬가지로 각 값의 인덱스에 맞춰 그룹화가 이뤄진다.

states = np.array(["OH", "CA", "CA", "OH", "OH", "CA", "OH"])
years = [2005, 2005, 2006, 2005, 2006, 2005, 2006]
s.groupby([states, years]).mean()
CA  2005    0.936175
    2006   -0.519439
OH  2005   -0.380219
    2006    1.029344
Name: data1, dtype: float64

데이터프레임 그룹화

그룹별 집계 연산은 열 별로 독립적으로 적용된다.

  • numeric_only=True 키워드 인자: key1 열을 기준으로 그룹화 하면 key2 열에 대해서는 mean() 함수를 적용할 수 없기에 수치형 데이터를 담고 있는 열에 대해서만 평균값을 구하라고 지정한다.

df.groupby("key1").mean(numeric_only=True)
data1 data2
key1
a 0.555881 0.441920
b 0.705025 -0.144516

다중 키 활용 그룹화

여러 개의 키를 이용한 그룹화도 동일하게 작동한다.

df.groupby(["key1", "key2"]).mean()
data1 data2
key1 key2
a one -0.204708 0.281746
two 0.478943 0.769023
b one 1.965781 -1.296221
two -0.555730 1.007189
  • dropna=True 키워드 인자: 결측치 자동 제거

df.groupby("key1", dropna=True).size()
key1
a    3
b    2
dtype: int64
df.groupby(["key1", "key2"]).size() # dropna = True 가 기본값
key1  key2
a     one     1
      two     1
b     one     1
      two     1
dtype: int64
  • dropna=False 로 지정하면 결측치를 그대로 둚

df.groupby("key1", dropna=False).size()
key1
a      3
b      2
NaN    2
dtype: int64
df.groupby(["key1", "key2"], dropna=False).size()
key1  key2
a     one     1
      two     1
      NaN     1
b     one     1
      two     1
NaN   one     2
dtype: int64
  • count() 집계 함수: 그룹별 항목 수 계산

df.groupby("key1").count()
key2 data1 data2
key1
a 2 3 3
b 2 2 2

23.1.2. 그룹 확인#

for 반복문 활용

집계 함수를 적용하기 이전의 그룹들의 상태를 확인하기 위해 for 반복문을 이용할 수 있다.

for name, group in df.groupby("key1"):
    print(name)
    print("---")
    print(group)
    print() # 한 칸 띄우기 용도
a
---
  key1  key2     data1     data2
0    a   one -0.204708  0.281746
1    a   two  0.478943  0.769023
5    a  None  1.393406  0.274992

b
---
  key1 key2     data1     data2
3    b  two -0.555730  1.007189
4    b  one  1.965781 -1.296221
for (k1, k2), group in df.groupby(["key1", "key2"]):
    print((k1, k2))
    print("---")
    print(group)
    print() # 한 칸 띄우기 용도
('a', 'one')
---
  key1 key2     data1     data2
0    a  one -0.204708  0.281746

('a', 'two')
---
  key1 key2     data1     data2
1    a  two  0.478943  0.769023

('b', 'one')
---
  key1 key2     data1     data2
4    b  one  1.965781 -1.296221

('b', 'two')
---
  key1 key2    data1     data2
3    b  two -0.55573  1.007189

사전 활용

사전 형식으로 저장하면 쉽게 그룹 단위로 확인할 수 있다.

df
key1 key2 data1 data2
0 a one -0.204708 0.281746
1 a two 0.478943 0.769023
2 None one -0.519439 1.246435
3 b two -0.555730 1.007189
4 b one 1.965781 -1.296221
5 a None 1.393406 0.274992
6 None one 0.092908 0.228913
pieces = {name: group for name, group in df.groupby("key1")}
pieces["b"]
key1 key2 data1 data2
3 b two -0.555730 1.007189
4 b one 1.965781 -1.296221
pieces = {name: group for name, group in df.groupby(["key1", "key2"])}
pieces[("b", "two")]
key1 key2 data1 data2
3 b two -0.55573 1.007189

23.1.3. 열 선택#

그룹화 후 집계를 진행하기 전에 열 라벨을 선택하면 해당 열에 대해서만 집계 함수가 적용된다.

  • 열 라벨의 리스트를 이용할 때: 데이터프레임 생성

df.groupby(["key1", "key2"])[["data2"]].mean()
data2
key1 key2
a one 0.281746
two 0.769023
b one -1.296221
two 1.007189
  • 열 라벨을 하나만 지정할 때: 시리즈 생성

df.groupby(["key1", "key2"])["data2"].mean()
key1  key2
a     one     0.281746
      two     0.769023
b     one    -1.296221
      two     1.007189
Name: data2, dtype: float64

23.1.4. 열 기준 그룹화#

people = pd.DataFrame(np.random.standard_normal((5, 5)),
                      columns=["a", "b", "c", "d", "e"],
                      index=["Joe", "Steve", "Wanda", "Jill", "Trey"])

people
a b c d e
Joe 1.352917 0.886429 -2.001637 -0.371843 1.669025
Steve -0.438570 -0.539741 0.476985 3.248944 -1.021228
Wanda -0.577087 0.124121 0.302614 0.523772 0.000940
Jill 1.343810 -0.713544 -0.831154 -2.370232 -1.860761
Trey -0.860757 0.560145 -1.265934 0.119827 -1.063512

연습을 위해 결측치 두 개를 의도적으로 추가한다.

people.iloc[2:3, [1, 2]] = np.nan
people
a b c d e
Joe 1.352917 0.886429 -2.001637 -0.371843 1.669025
Steve -0.438570 -0.539741 0.476985 3.248944 -1.021228
Wanda -0.577087 NaN NaN 0.523772 0.000940
Jill 1.343810 -0.713544 -0.831154 -2.370232 -1.860761
Trey -0.860757 0.560145 -1.265934 0.119827 -1.063512

사전 활용 열 기준 그룹화

사전을 이용하여 열 인덱스의 라벨을 "red", "blue" 로 구분한다. 즉, 키는 열 인덱스의 라벨을, 키의 값은 그룹을 지정한다. 단, "f" 라벨은 존재하지 않기에 무시된다.

mapping = {"a": "red", "b": "red", "c": "blue",
           "d": "blue", "e": "red", "f" : "orange"}

아래 코드는 지정된 열로 구성된 그룹별로 sum() 메서드를 각 행에 대해 적용한다.

by_column = people.groupby(mapping, axis="columns")
by_column.sum()
blue red
Joe -2.373480 3.908371
Steve 3.725929 -1.999539
Wanda 0.523772 -0.576147
Jill -3.201385 -1.230495
Trey -1.146107 -1.364125

다음은 그룹별, 행별 평균값을 계산한다.

by_column.mean()
blue red
Joe -1.186740 1.302790
Steve 1.862964 -0.666513
Wanda 0.523772 -0.288074
Jill -1.600693 -0.410165
Trey -0.573054 -0.454708

시리즈 활용 열 기준 그룹화

map_series = pd.Series(mapping)
map_series
a       red
b       red
c      blue
d      blue
e       red
f    orange
dtype: object

시리즈를 활용하면 행의 라벨이 해당 값에 따라 그룹화된다.

people.groupby(map_series, axis="columns").count()
blue red
Joe 2 3
Steve 2 3
Wanda 1 2
Jill 2 3
Trey 2 3

23.1.5. 함수 활용 그룹화#

groupby() 메서드의 첫째 인자는 by 키워드 인자로 지정된다. by 키워드의 인자가 함수이면 지정된 축에 따라 행 또는 열 인덱스 라벨에 해당 함수를 적용한 결과를 기준으로 그룹화한다.

예를 들어 아래 코드는 행 인덱스의 라벨의 길이를 기준으로 그룹화한다.

people
a b c d e
Joe 1.352917 0.886429 -2.001637 -0.371843 1.669025
Steve -0.438570 -0.539741 0.476985 3.248944 -1.021228
Wanda -0.577087 NaN NaN 0.523772 0.000940
Jill 1.343810 -0.713544 -0.831154 -2.370232 -1.860761
Trey -0.860757 0.560145 -1.265934 0.119827 -1.063512
people.groupby(len).sum()
a b c d e
3 1.352917 0.886429 -2.001637 -0.371843 1.669025
4 0.483052 -0.153399 -2.097088 -2.250405 -2.924273
5 -1.015657 -0.539741 0.476985 3.772716 -1.020287

그룹화의 기준으로 함수와 함께 어레이, 사전, 시리즈, 또는 열 인덱스 라벨 등을 함께 사용할 수 있다. 연습을 위해 먼저 새로운 열을 추가한다.

people["School"] = ["A", "B", "C", "A", "B"]
people
a b c d e School
Joe 1.352917 0.886429 -2.001637 -0.371843 1.669025 A
Steve -0.438570 -0.539741 0.476985 3.248944 -1.021228 B
Wanda -0.577087 NaN NaN 0.523772 0.000940 C
Jill 1.343810 -0.713544 -0.831154 -2.370232 -1.860761 A
Trey -0.860757 0.560145 -1.265934 0.119827 -1.063512 B

아래 리스트도 그룹화에 사용한다.

key_list = ["one", "one", "two", "three", "two"]

아래 코드는 행 인덱스의 라벨의 길이, key_list, "School" 열을 기준으로 그룹하를 진행한 다음에 그룹별 최소값을 계산한다.

people.groupby([len, key_list, "School"]).min()
a b c d e
School
3 one A 1.352917 0.886429 -2.001637 -0.371843 1.669025
4 three A 1.343810 -0.713544 -0.831154 -2.370232 -1.860761
two B -0.860757 0.560145 -1.265934 0.119827 -1.063512
5 one B -0.438570 -0.539741 0.476985 3.248944 -1.021228
two C -0.577087 NaN NaN 0.523772 0.000940

23.1.6. 멀티 인덱스 레벨 활용#

인덱스의 레벨에 포함된 라벨을 기준으로 그룹화를 진행할 수 있다.

행 인덱스 레벨 활용

df
key1 key2 data1 data2
0 a one -0.204708 0.281746
1 a two 0.478943 0.769023
2 None one -0.519439 1.246435
3 b two -0.555730 1.007189
4 b one 1.965781 -1.296221
5 a None 1.393406 0.274992
6 None one 0.092908 0.228913
df2 = df.set_index(["key1", "key2"])
df2
data1 data2
key1 key2
a one -0.204708 0.281746
two 0.478943 0.769023
NaN one -0.519439 1.246435
b two -0.555730 1.007189
one 1.965781 -1.296221
a NaN 1.393406 0.274992
NaN one 0.092908 0.228913
  • 행 인덱스의 0-레벨 기준 그룹화

df2.groupby(level=0, axis="index").count()
data1 data2
key1
a 3 3
b 2 2
  • 행 인덱스의 1-레벨 기준 그룹화

df2.groupby(level=1).count() # axis="index" 가 기본값
data1 data2
key2
one 4 4
two 2 2
df2.groupby(level=1).mean() # axis="index" 가 기본값
data1 data2
key2
one 0.333636 0.115218
two -0.038393 0.888106

열 인덱스 레벨 활용

columns = pd.MultiIndex.from_arrays([["Foo", "Foo", "Foo", "Bar", "Bar"],
                                    [1, 3, 5, 1, 3]])

hier_df = pd.DataFrame(np.random.standard_normal((4, 5)), columns=columns)
hier_df
Foo Bar
1 3 5 1 3
0 0.332883 -2.359419 -0.199543 -1.541996 -0.970736
1 -1.307030 0.286350 0.377984 -0.753887 0.331286
2 1.349742 0.069877 0.246674 -0.011862 1.004812
3 1.327195 -0.919262 -1.549106 0.022185 0.758363
  • 열 인덱스의 0-레벨 기준 그룹화

hier_df.groupby(level=0, axis="columns").count()
Bar Foo
0 2 3
1 2 3
2 2 3
3 2 3
  • 열 인덱스의 1-레벨 기준 그룹화

hier_df.groupby(level=1, axis="columns").mean()
1 3 5
0 -0.604556 -1.665077 -0.199543
1 -1.030458 0.308818 0.377984
2 0.668940 0.537344 0.246674
3 0.674690 -0.080449 -1.549106

23.2. 데이터 집계#

23.2.1. 집계 함수#

그룹화 집계 메서드

아래 함수가 GroupBy 객체의 집계 메서드로 최적화되어 있다.

집계 메서드

기능

any, all

최소 하나의 항목이 또는 모든 항목이 참인지 여부 확인

count

그룹별 항목 수. NaN 제외.

cummin, cummax

누적 최소값 또는 최대값. NaN 제외

cumsum

누적합. NaN 제외

cumprod

누적곱, NaN 제외

first, last

처음 또는 마지막 항목. NaN 제외

mean

평균값. NaN 제외

median

중앙값. NaN 제외

min, max

최소값 또는 최대값. NaN 제외

nth

정렬했을 때 n 번째 값

ohlc

시계열 데이터의 “open-high-low-close” 값 네 개 계산

prod

모든 항목의 곱. NaN 제외

quantile

백분위수 계산

rank

오름차순으로 정렬했을 때의 항목별 순서. NaN 제외

size

그룹 크기

sum

모든 항목의 합. NaN 제외

std, var

표준편차 또는 분산

기타 시리즈 집계 메서드

언급된 집계 메서드 이외에 시리즈의 메서드를 모두 집계 함수로 사용할 수 있다. 단, 속도가 좀 느릴 수 있다.

df
key1 key2 data1 data2
0 a one -0.204708 0.281746
1 a two 0.478943 0.769023
2 None one -0.519439 1.246435
3 b two -0.555730 1.007189
4 b one 1.965781 -1.296221
5 a None 1.393406 0.274992
6 None one 0.092908 0.228913
  • Series.nsmallest(n=5) 메서드: 가장 작은 n 개의 항목 반환. n=5가 기본값.

grouped = df.groupby("key1")
grouped["data1"].nsmallest(2)
key1   
a     0   -0.204708
      1    0.478943
b     3   -0.555730
      4    1.965781
Name: data1, dtype: float64

사용자 정의 집계 함수

1차원 어레이를 집계하는 임의의 함수를 그룹화 집계 함수로 이용할 수 있다. 예를 들어, 아래 함수는 어레이에 포함된 최대값과 최소값의 차이, 즉, 값들의 범위를 계산한다.

def peak_to_peak(arr):
    return arr.max() - arr.min()
  • agg() 메서드: 사용자 정의 집계 함수를 그룹화 집계 함수로 사용하려면 agg() 메서드의 인자로 지정한다.

grouped[["data1", "data2"]].agg(peak_to_peak)
data1 data2
key1
a 1.598113 0.494031
b 2.521511 2.303410

비집계 함수

DataFrame.describe() 처럼 집계 함수가 아닌 경우도 작동하기도 한다.

  • DataFrame.describe() 메서드: 수치 데이터셋의 분포를 요약한다. 여기서는 그룹별로 작동한다.

이와같은 비집계 함수가 작동하는 원리를 이해하려면 먼저 apply() 메서드를 이해해야 한다.

grouped.describe()
data1 data2
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
key1
a 3.0 0.555881 0.801830 -0.204708 0.137118 0.478943 0.936175 1.393406 3.0 0.441920 0.283299 0.274992 0.278369 0.281746 0.525384 0.769023
b 2.0 0.705025 1.782977 -0.555730 0.074647 0.705025 1.335403 1.965781 2.0 -0.144516 1.628757 -1.296221 -0.720368 -0.144516 0.431337 1.007189

23.2.2. 열별로 여러 함수 적용하기#

팁 데이터를 다시 이용한다.

base_url = "https://raw.githubusercontent.com/codingalzi/datapy/master/jupyter-book/examples/"
file = "tips.csv"
tips = pd.read_csv(base_url + file)

tips.head()
total_bill tip smoker day time size
0 16.99 1.01 No Sun Dinner 2
1 10.34 1.66 No Sun Dinner 3
2 21.01 3.50 No Sun Dinner 3
3 23.68 3.31 No Sun Dinner 2
4 24.59 3.61 No Sun Dinner 4

전체 수입에 대한 팁이 비율을 새로운 열로 추가한다. 열의 라벨은 "tip_pct"이다.

tips["tip_pct"] = tips["tip"] / tips["total_bill"]
tips.head()
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808

"day""smoker" 기준으로 그룹을 짓는다.

grouped = tips.groupby(["day", "smoker"])

그룹별 팁 비율의 평균값을 계산한다. agg() 메서드에 GroupBy 객체의 집계 메서드를 인자로 사용할 수 있으며 이때 함수의 이름을 문자열로 지정한다.

grouped_pct = grouped["tip_pct"]
grouped_pct.agg("mean")
day   smoker
Fri   No        0.151650
      Yes       0.174783
Sat   No        0.158048
      Yes       0.147906
Sun   No        0.160113
      Yes       0.187250
Thur  No        0.160298
      Yes       0.163863
Name: tip_pct, dtype: float64

여러 개의 집계 함수를 사용하면 사용된 집계 함수별로 열이 생성된다. 아래 코드는 그룹별로 팁 비율의 평균값, 표준편차, 최대-최소 오차 값을 계산한다.

grouped_pct.agg(["mean", "std", peak_to_peak])
mean std peak_to_peak
day smoker
Fri No 0.151650 0.028123 0.067349
Yes 0.174783 0.051293 0.159925
Sat No 0.158048 0.039767 0.235193
Yes 0.147906 0.061375 0.290095
Sun No 0.160113 0.042347 0.193226
Yes 0.187250 0.154134 0.644685
Thur No 0.160298 0.038774 0.193350
Yes 0.163863 0.039389 0.151240

집계 함수와 생성되는 열의 라벨을 쌍으로 구성하면 지정된 라벨이 사용된다.

  • np.std() 함수: 집계 함수 "std" 대신 사용됨

grouped_pct.agg([("average", "mean"), ("stdev", np.std)])
average stdev
day smoker
Fri No 0.151650 0.028123
Yes 0.174783 0.051293
Sat No 0.158048 0.039767
Yes 0.147906 0.061375
Sun No 0.160113 0.042347
Yes 0.187250 0.154134
Thur No 0.160298 0.038774
Yes 0.163863 0.039389

여러 개의 함수를 여러 개의 열에 적용하면 멀티 인덱스가 열의 인덱스로 사용된다.

functions = ["count", "mean", "max"]
result = grouped[["tip_pct", "total_bill"]].agg(functions)
result
tip_pct total_bill
count mean max count mean max
day smoker
Fri No 4 0.151650 0.187735 4 18.420000 22.75
Yes 15 0.174783 0.263480 15 16.813333 40.17
Sat No 45 0.158048 0.291990 45 19.661778 48.33
Yes 42 0.147906 0.325733 42 21.276667 50.81
Sun No 57 0.160113 0.252672 57 20.506667 48.17
Yes 19 0.187250 0.710345 19 24.120000 45.35
Thur No 45 0.160298 0.266312 45 17.113111 41.19
Yes 17 0.163863 0.241255 17 19.190588 43.11

열의 라벨을 지정하는 방식도 동일하게 작동한다.

ftuples = [("Average", "mean"), ("Variance", np.var)]
grouped[["tip_pct", "total_bill"]].agg(ftuples)
tip_pct total_bill
Average Variance Average Variance
day smoker
Fri No 0.151650 0.000791 18.420000 25.596333
Yes 0.174783 0.002631 16.813333 82.562438
Sat No 0.158048 0.001581 19.661778 79.908965
Yes 0.147906 0.003767 21.276667 101.387535
Sun No 0.160113 0.001793 20.506667 66.099980
Yes 0.187250 0.023757 24.120000 109.046044
Thur No 0.160298 0.001503 17.113111 59.625081
Yes 0.163863 0.001551 19.190588 69.808518

열별로 다른 집계 함수를 적용하려면 사전을 이용한다.

grouped.agg({"tip" : np.max, "size" : "sum"})
tip size
day smoker
Fri No 3.50 9
Yes 4.73 31
Sat No 9.00 115
Yes 10.00 104
Sun No 6.00 167
Yes 6.50 49
Thur No 6.70 112
Yes 5.00 40

열별로 여러 함수를 적용할 수도 있다.

grouped.agg({"tip_pct" : ["min", "max", "mean", "std"],
             "size" : "sum"})
tip_pct size
min max mean std sum
day smoker
Fri No 0.120385 0.187735 0.151650 0.028123 9
Yes 0.103555 0.263480 0.174783 0.051293 31
Sat No 0.056797 0.291990 0.158048 0.039767 115
Yes 0.035638 0.325733 0.147906 0.061375 104
Sun No 0.059447 0.252672 0.160113 0.042347 167
Yes 0.065660 0.710345 0.187250 0.154134 49
Thur No 0.072961 0.266312 0.160298 0.038774 112
Yes 0.090014 0.241255 0.163863 0.039389 40

23.3. Apply: 다목적 데이터 집계#

지금까지 groupby를 이용한 그룹화 이후에 사용한 집계 함수는 모두 그룹별로 하나의 값만 생성하였다. 반면에 apply() 메서드를 이용하면 그런 제한 없이 임의의 함수를 그룹별로 적용할 수 있다. 실행 결과는 그룹별 결과를 합친 데이터프레임이다.

서빙 팁 데이터 활용

tips
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808
... ... ... ... ... ... ... ...
239 29.03 5.92 No Sat Dinner 3 0.203927
240 27.18 2.00 Yes Sat Dinner 2 0.073584
241 22.67 2.00 Yes Sat Dinner 2 0.088222
242 17.82 1.75 No Sat Dinner 2 0.098204
243 18.78 3.00 No Thur Dinner 2 0.159744

244 rows × 7 columns

아래 top() 함수는 지정된 데이터프레임을 특정 열을 기준으로 내림차순으로 정렬한 다음에 처음 n 개의 행을 반환한다.

def top(df, n=5, column="tip_pct"):
    return df.sort_values(column, ascending=False)[:n]

서빙팁을 가장 많이 받은 6일에 대한 정보는 다음과 같다.

top(tips, n=6)
total_bill tip smoker day time size tip_pct
172 7.25 5.15 Yes Sun Dinner 2 0.710345
178 9.60 4.00 Yes Sun Dinner 2 0.416667
67 3.07 1.00 Yes Sat Dinner 1 0.325733
232 11.61 3.39 No Sat Dinner 2 0.291990
183 23.17 6.50 Yes Sun Dinner 4 0.280535
109 14.31 4.00 Yes Sat Dinner 2 0.279525

흡연 여부를 기준으로 그룹을 나눈 뒤 흡연 그룹과 비흡연 그룹에 대해 서빙팁이 가장 많았던 5일에 대한 정보를 top() 함수를 이용하여 구한다.

tips.groupby("smoker").apply(top)
total_bill tip smoker day time size tip_pct
smoker
No 232 11.61 3.39 No Sat Dinner 2 0.291990
149 7.51 2.00 No Thur Lunch 2 0.266312
51 10.29 2.60 No Sun Dinner 2 0.252672
185 20.69 5.00 No Sun Dinner 5 0.241663
88 24.71 5.85 No Thur Lunch 2 0.236746
Yes 172 7.25 5.15 Yes Sun Dinner 2 0.710345
178 9.60 4.00 Yes Sun Dinner 2 0.416667
67 3.07 1.00 Yes Sat Dinner 1 0.325733
183 23.17 6.50 Yes Sun Dinner 4 0.280535
109 14.31 4.00 Yes Sat Dinner 2 0.279525

top() 함수의 키워드 인자를 변경하려면 apply() 함수의 키워드 인자로 지정하면 된다. 아래 코드는 흡연 여부와 요일 기준에 따른 그룹화 후에 그룹별로 가장 많은 수입을 올린 날에 대한 정보를 보여준다.

tips.groupby(["smoker", "day"]).apply(top, n=1, column="total_bill")
total_bill tip smoker day time size tip_pct
smoker day
No Fri 94 22.75 3.25 No Fri Dinner 2 0.142857
Sat 212 48.33 9.00 No Sat Dinner 4 0.186220
Sun 156 48.17 5.00 No Sun Dinner 6 0.103799
Thur 142 41.19 5.00 No Thur Lunch 5 0.121389
Yes Fri 95 40.17 4.73 Yes Fri Dinner 4 0.117750
Sat 170 50.81 10.00 Yes Sat Dinner 3 0.196812
Sun 182 45.35 3.50 Yes Sun Dinner 3 0.077178
Thur 197 43.11 5.00 Yes Thur Lunch 4 0.115982
result = tips.groupby("smoker")["tip_pct"].describe()
result
count mean std min 25% 50% 75% max
smoker
No 151.0 0.159328 0.039910 0.056797 0.136906 0.155625 0.185014 0.291990
Yes 93.0 0.163196 0.085119 0.035638 0.106771 0.153846 0.195059 0.710345
result.unstack("smoker")
       smoker
count  No        151.000000
       Yes        93.000000
mean   No          0.159328
       Yes         0.163196
std    No          0.039910
       Yes         0.085119
min    No          0.056797
       Yes         0.035638
25%    No          0.136906
       Yes         0.106771
50%    No          0.155625
       Yes         0.153846
75%    No          0.185014
       Yes         0.195059
max    No          0.291990
       Yes         0.710345
dtype: float64

23.3.1. 그룹 키 제거#

tips.groupby("smoker", group_keys=False).apply(top)
total_bill tip smoker day time size tip_pct
232 11.61 3.39 No Sat Dinner 2 0.291990
149 7.51 2.00 No Thur Lunch 2 0.266312
51 10.29 2.60 No Sun Dinner 2 0.252672
185 20.69 5.00 No Sun Dinner 5 0.241663
88 24.71 5.85 No Thur Lunch 2 0.236746
172 7.25 5.15 Yes Sun Dinner 2 0.710345
178 9.60 4.00 Yes Sun Dinner 2 0.416667
67 3.07 1.00 Yes Sat Dinner 1 0.325733
183 23.17 6.50 Yes Sun Dinner 4 0.280535
109 14.31 4.00 Yes Sat Dinner 2 0.279525
tips.groupby("smoker").apply(top)
total_bill tip smoker day time size tip_pct
smoker
No 232 11.61 3.39 No Sat Dinner 2 0.291990
149 7.51 2.00 No Thur Lunch 2 0.266312
51 10.29 2.60 No Sun Dinner 2 0.252672
185 20.69 5.00 No Sun Dinner 5 0.241663
88 24.71 5.85 No Thur Lunch 2 0.236746
Yes 172 7.25 5.15 Yes Sun Dinner 2 0.710345
178 9.60 4.00 Yes Sun Dinner 2 0.416667
67 3.07 1.00 Yes Sat Dinner 1 0.325733
183 23.17 6.50 Yes Sun Dinner 4 0.280535
109 14.31 4.00 Yes Sat Dinner 2 0.279525
tips.groupby("smoker", as_index=False).apply(top)
total_bill tip smoker day time size tip_pct
0 232 11.61 3.39 No Sat Dinner 2 0.291990
149 7.51 2.00 No Thur Lunch 2 0.266312
51 10.29 2.60 No Sun Dinner 2 0.252672
185 20.69 5.00 No Sun Dinner 5 0.241663
88 24.71 5.85 No Thur Lunch 2 0.236746
1 172 7.25 5.15 Yes Sun Dinner 2 0.710345
178 9.60 4.00 Yes Sun Dinner 2 0.416667
67 3.07 1.00 Yes Sat Dinner 1 0.325733
183 23.17 6.50 Yes Sun Dinner 4 0.280535
109 14.31 4.00 Yes Sat Dinner 2 0.279525

23.3.2. 분위/구간 분석#

frame = pd.DataFrame({"data1": np.random.standard_normal(1000),
                      "data2": np.random.standard_normal(1000)})
frame.head()
data1 data2
0 -0.660524 -0.612905
1 0.862580 0.316447
2 -0.010032 0.838295
3 0.050009 -1.034423
4 0.670216 0.434304
quartiles = pd.cut(frame["data1"], 4)
quartiles.head(10)
0     (-1.23, 0.489]
1     (0.489, 2.208]
2     (-1.23, 0.489]
3     (-1.23, 0.489]
4     (0.489, 2.208]
5     (0.489, 2.208]
6     (-1.23, 0.489]
7     (-1.23, 0.489]
8    (-2.956, -1.23]
9     (-1.23, 0.489]
Name: data1, dtype: category
Categories (4, interval[float64, right]): [(-2.956, -1.23] < (-1.23, 0.489] < (0.489, 2.208] < (2.208, 3.928]]
def get_stats(group):
    return pd.DataFrame(
        {"min": group.min(), "max": group.max(),
        "count": group.count(), "mean": group.mean()}
    )
grouped = frame.groupby(quartiles)
grouped.apply(get_stats)
min max count mean
data1
(-2.956, -1.23] data1 -2.949343 -1.230179 94 -1.658818
data2 -3.399312 1.670835 94 -0.033333
(-1.23, 0.489] data1 -1.228918 0.488675 598 -0.329524
data2 -2.989741 3.260383 598 -0.002622
(0.489, 2.208] data1 0.489965 2.200997 298 1.065727
data2 -3.745356 2.954439 298 0.078249
(2.208, 3.928] data1 2.212303 3.927528 10 2.644253
data2 -1.929776 1.765640 10 0.024750
grouped.agg(["min", "max", "count", "mean"])
data1 data2
min max count mean min max count mean
data1
(-2.956, -1.23] -2.949343 -1.230179 94 -1.658818 -3.399312 1.670835 94 -0.033333
(-1.23, 0.489] -1.228918 0.488675 598 -0.329524 -2.989741 3.260383 598 -0.002622
(0.489, 2.208] 0.489965 2.200997 298 1.065727 -3.745356 2.954439 298 0.078249
(2.208, 3.928] 2.212303 3.927528 10 2.644253 -1.929776 1.765640 10 0.024750
quartiles_samp = pd.qcut(frame["data1"], 4, labels=False)
quartiles_samp.head()
0    1
1    3
2    2
3    2
4    3
Name: data1, dtype: int64
grouped = frame.groupby(quartiles_samp)
grouped.apply(get_stats)
min max count mean
data1
0 data1 -2.949343 -0.685484 250 -1.212173
data2 -3.399312 2.628441 250 -0.027045
1 data1 -0.683066 -0.030280 250 -0.368334
data2 -2.630247 3.260383 250 -0.027845
2 data1 -0.027734 0.618965 250 0.295812
data2 -3.056990 2.458842 250 0.014450
3 data1 0.623587 3.927528 250 1.248875
data2 -3.745356 2.954439 250 0.115899

23.3.3. 예제: 그룹별 결측치 채우기#

s = pd.Series(np.random.standard_normal(6))
s[::2] = np.nan
s
0         NaN
1    0.227290
2         NaN
3   -2.153545
4         NaN
5   -0.375842
dtype: float64
s.fillna(s.mean())
0   -0.767366
1    0.227290
2   -0.767366
3   -2.153545
4   -0.767366
5   -0.375842
dtype: float64
states = ["Ohio", "New York", "Vermont", "Florida",
          "Oregon", "Nevada", "California", "Idaho"]
group_key = ["East", "East", "East", "East",
             "West", "West", "West", "West"]
data = pd.Series(np.random.standard_normal(8), index=states)
data
Ohio          0.329939
New York      0.981994
Vermont       1.105913
Florida      -1.613716
Oregon        1.561587
Nevada        0.406510
California    0.359244
Idaho        -0.614436
dtype: float64
data[["Vermont", "Nevada", "Idaho"]] = np.nan
data
Ohio          0.329939
New York      0.981994
Vermont            NaN
Florida      -1.613716
Oregon        1.561587
Nevada             NaN
California    0.359244
Idaho              NaN
dtype: float64
data.groupby(group_key).size()
East    4
West    4
dtype: int64
data.groupby(group_key).count()
East    3
West    2
dtype: int64
data.groupby(group_key).mean()
East   -0.100594
West    0.960416
dtype: float64
def fill_mean(group):
    return group.fillna(group.mean())
data.groupby(group_key, group_keys=False).apply(fill_mean)
Ohio          0.329939
New York      0.981994
Vermont      -0.100594
Florida      -1.613716
Oregon        1.561587
Nevada        0.960416
California    0.359244
Idaho         0.960416
dtype: float64
data.groupby(group_key, group_keys=True).apply(fill_mean)
East  Ohio          0.329939
      New York      0.981994
      Vermont      -0.100594
      Florida      -1.613716
West  Oregon        1.561587
      Nevada        0.960416
      California    0.359244
      Idaho         0.960416
dtype: float64
fill_values = {"East": 0.5, "West": -1}
def fill_func(group):
    return group.fillna(fill_values[group.name])
data.groupby(group_key, group_keys=False).apply(fill_func)
Ohio          0.329939
New York      0.981994
Vermont       0.500000
Florida      -1.613716
Oregon        1.561587
Nevada       -1.000000
California    0.359244
Idaho        -1.000000
dtype: float64

23.3.4. 예제: 무작위 샘플링#

suits = ["H", "S", "C", "D"]  # Hearts, Spades, Clubs, Diamonds
card_val = (list(range(1, 11)) + [10] * 3) * 4
base_names = ["A"] + list(range(2, 11)) + ["J", "K", "Q"]
cards = []
for suit in suits:
    cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val, index=cards)

deck.head(13)
AH      1
2H      2
3H      3
4H      4
5H      5
6H      6
7H      7
8H      8
9H      9
10H    10
JH     10
KH     10
QH     10
dtype: int64
def draw(deck, n=5):
    return deck.sample(n)
draw(deck)
4D     4
QH    10
8S     8
7D     7
9C     9
dtype: int64
def get_suit(card):
    # last letter is suit
    return card[-1]
deck.groupby(get_suit).apply(draw, n=2)
C  6C     6
   KC    10
D  7D     7
   3D     3
H  7H     7
   9H     9
S  2S     2
   QS    10
dtype: int64
deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
AC      1
3C      3
5D      5
4D      4
10H    10
7H      7
QS     10
7S      7
dtype: int64

23.3.5. 그룹별 가중치 합#

df = pd.DataFrame({"category": ["a", "a", "a", "a",
                                "b", "b", "b", "b"],
                   "data": np.random.standard_normal(8),
                   "weights": np.random.uniform(size=8)})
df
category data weights
0 a -1.691656 0.955905
1 a 0.511622 0.012745
2 a -0.401675 0.137009
3 a 0.968578 0.763037
4 b -1.818215 0.492472
5 b 0.279963 0.832908
6 b -0.200819 0.658331
7 b -0.217221 0.612009
grouped = df.groupby("category")
def get_wavg(group):
    return np.average(group["data"], weights=group["weights"])
grouped.apply(get_wavg)
category
a   -0.495807
b   -0.357273
dtype: float64
base_url = "https://raw.githubusercontent.com/codingalzi/datapy/master/jupyter-book/examples/"
file = "stock_px.csv"
close_px = pd.read_csv(base_url+file, parse_dates=True,
                       index_col=0)
close_px.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 5472 entries, 1990-02-01 to 2011-10-14
Data columns (total 9 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   AA      5472 non-null   float64
 1   AAPL    5472 non-null   float64
 2   GE      5472 non-null   float64
 3   IBM     5472 non-null   float64
 4   JNJ     5472 non-null   float64
 5   MSFT    5472 non-null   float64
 6   PEP     5471 non-null   float64
 7   SPX     5472 non-null   float64
 8   XOM     5472 non-null   float64
dtypes: float64(9)
memory usage: 427.5 KB
close_px.tail(4)
AA AAPL GE IBM JNJ MSFT PEP SPX XOM
2011-10-11 10.30 400.29 16.14 185.00 63.96 27.00 60.95 1195.54 76.27
2011-10-12 10.05 402.19 16.40 186.12 64.33 26.96 62.70 1207.25 77.16
2011-10-13 10.10 408.43 16.22 186.82 64.23 27.18 62.36 1203.66 76.37
2011-10-14 10.26 422.00 16.60 190.53 64.72 27.27 62.24 1224.58 78.11
def spx_corr(group):
    return group.corrwith(group["SPX"])
rets = close_px.pct_change().dropna()
def get_year(x):
    return x.year
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)
AA AAPL GE IBM JNJ MSFT PEP SPX XOM
1990 0.595024 0.545067 0.752187 0.738361 0.801145 0.586691 0.783168 1.0 0.517586
1991 0.453574 0.365315 0.759607 0.557046 0.646401 0.524225 0.641775 1.0 0.569335
1992 0.398180 0.498732 0.632685 0.262232 0.515740 0.492345 0.473871 1.0 0.318408
1993 0.259069 0.238578 0.447257 0.211269 0.451503 0.425377 0.385089 1.0 0.318952
1994 0.428549 0.268420 0.572996 0.385162 0.372962 0.436585 0.450516 1.0 0.395078
... ... ... ... ... ... ... ... ... ...
2007 0.642427 0.508118 0.796945 0.603906 0.568423 0.658770 0.651911 1.0 0.786264
2008 0.781057 0.681434 0.777337 0.833074 0.801005 0.804626 0.709264 1.0 0.828303
2009 0.735642 0.707103 0.713086 0.684513 0.603146 0.654902 0.541474 1.0 0.797921
2010 0.745700 0.710105 0.822285 0.783638 0.689896 0.730118 0.626655 1.0 0.839057
2011 0.882045 0.691931 0.864595 0.802730 0.752379 0.800996 0.592029 1.0 0.859975

22 rows × 9 columns

def corr_aapl_msft(group):
    return group["AAPL"].corr(group["MSFT"])
by_year.apply(corr_aapl_msft)
1990    0.408271
1991    0.266807
1992    0.450592
1993    0.236917
1994    0.361638
          ...   
2007    0.417738
2008    0.611901
2009    0.432738
2010    0.571946
2011    0.581987
Length: 22, dtype: float64

23.3.6. 예제: 그룹 단위 선형 회귀#

import statsmodels.api as sm

def regress(data, yvar=None, xvars=None):
    Y = data[yvar]
    X = data[xvars]
    X["intercept"] = 1.
    result = sm.OLS(Y, X).fit()
    return result.params
by_year.apply(regress, yvar="AAPL", xvars=["SPX"])
SPX intercept
1990 1.512772 0.001395
1991 1.187351 0.000396
1992 1.832427 0.000164
1993 1.390470 -0.002657
1994 1.190277 0.001617
... ... ...
2007 1.198761 0.003438
2008 0.968016 -0.001110
2009 0.879103 0.002954
2010 1.052608 0.001261
2011 0.806605 0.001514

22 rows × 2 columns

23.4. 그룹 변환#

df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4,
                   'value': np.arange(12.)})
df
key value
0 a 0.0
1 b 1.0
2 c 2.0
3 a 3.0
4 b 4.0
5 c 5.0
6 a 6.0
7 b 7.0
8 c 8.0
9 a 9.0
10 b 10.0
11 c 11.0
g = df.groupby('key', group_keys=False)['value']
g.mean()
key
a    4.5
b    5.5
c    6.5
Name: value, dtype: float64
def get_mean(group):
    return group.mean()
g.transform(get_mean)
0     4.5
1     5.5
2     6.5
3     4.5
4     5.5
5     6.5
6     4.5
7     5.5
8     6.5
9     4.5
10    5.5
11    6.5
Name: value, dtype: float64
g.transform('mean')
0     4.5
1     5.5
2     6.5
3     4.5
4     5.5
5     6.5
6     4.5
7     5.5
8     6.5
9     4.5
10    5.5
11    6.5
Name: value, dtype: float64
def times_two(group):
    return group * 2
g.transform(times_two)
0      0.0
1      2.0
2      4.0
3      6.0
4      8.0
5     10.0
6     12.0
7     14.0
8     16.0
9     18.0
10    20.0
11    22.0
Name: value, dtype: float64
def get_ranks(group):
    return group.rank(ascending=False)
g.transform(get_ranks)
0     4.0
1     4.0
2     4.0
3     3.0
4     3.0
5     3.0
6     2.0
7     2.0
8     2.0
9     1.0
10    1.0
11    1.0
Name: value, dtype: float64
def normalize(x):
    return (x - x.mean()) / x.std()
g.transform(normalize)
0    -1.161895
1    -1.161895
2    -1.161895
3    -0.387298
4    -0.387298
5    -0.387298
6     0.387298
7     0.387298
8     0.387298
9     1.161895
10    1.161895
11    1.161895
Name: value, dtype: float64
g.apply(normalize)
0    -1.161895
1    -1.161895
2    -1.161895
3    -0.387298
4    -0.387298
5    -0.387298
6     0.387298
7     0.387298
8     0.387298
9     1.161895
10    1.161895
11    1.161895
Name: value, dtype: float64
g.transform('mean')
0     4.5
1     5.5
2     6.5
3     4.5
4     5.5
5     6.5
6     4.5
7     5.5
8     6.5
9     4.5
10    5.5
11    6.5
Name: value, dtype: float64
normalized = (df['value'] - g.transform('mean')) / g.transform('std')
normalized
0    -1.161895
1    -1.161895
2    -1.161895
3    -0.387298
4    -0.387298
5    -0.387298
6     0.387298
7     0.387298
8     0.387298
9     1.161895
10    1.161895
11    1.161895
Name: value, dtype: float64

23.5. 피벗 테이블#

tips.head()
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808
tips.pivot_table(index=["day", "smoker"], values=["size", "tip", "tip_pct", "total_bill"])
size tip tip_pct total_bill
day smoker
Fri No 2.250000 2.812500 0.151650 18.420000
Yes 2.066667 2.714000 0.174783 16.813333
Sat No 2.555556 3.102889 0.158048 19.661778
Yes 2.476190 2.875476 0.147906 21.276667
Sun No 2.929825 3.167895 0.160113 20.506667
Yes 2.578947 3.516842 0.187250 24.120000
Thur No 2.488889 2.673778 0.160298 17.113111
Yes 2.352941 3.030000 0.163863 19.190588
tips.pivot_table(index=["day", "smoker", "time"])
size tip tip_pct total_bill
day smoker time
Fri No Dinner 2.000000 2.750000 0.139622 19.233333
Lunch 3.000000 3.000000 0.187735 15.980000
Yes Dinner 2.222222 3.003333 0.165347 19.806667
Lunch 1.833333 2.280000 0.188937 12.323333
Sat No Dinner 2.555556 3.102889 0.158048 19.661778
Yes Dinner 2.476190 2.875476 0.147906 21.276667
Sun No Dinner 2.929825 3.167895 0.160113 20.506667
Yes Dinner 2.578947 3.516842 0.187250 24.120000
Thur No Dinner 2.000000 3.000000 0.159744 18.780000
Lunch 2.500000 2.666364 0.160311 17.075227
Yes Lunch 2.352941 3.030000 0.163863 19.190588
tips.pivot_table(index=["time", "day"], columns="smoker",
                 values=["tip_pct", "size"])
size tip_pct
smoker No Yes No Yes
time day
Dinner Fri 2.000000 2.222222 0.139622 0.165347
Sat 2.555556 2.476190 0.158048 0.147906
Sun 2.929825 2.578947 0.160113 0.187250
Thur 2.000000 NaN 0.159744 NaN
Lunch Fri 3.000000 1.833333 0.187735 0.188937
Thur 2.500000 2.352941 0.160311 0.163863
tips.pivot_table(index=["time", "day"], columns="smoker",
                 values=["tip_pct", "size"], margins=True)
size tip_pct
smoker No Yes All No Yes All
time day
Dinner Fri 2.000000 2.222222 2.166667 0.139622 0.165347 0.158916
Sat 2.555556 2.476190 2.517241 0.158048 0.147906 0.153152
Sun 2.929825 2.578947 2.842105 0.160113 0.187250 0.166897
Thur 2.000000 NaN 2.000000 0.159744 NaN 0.159744
Lunch Fri 3.000000 1.833333 2.000000 0.187735 0.188937 0.188765
Thur 2.500000 2.352941 2.459016 0.160311 0.163863 0.161301
All 2.668874 2.408602 2.569672 0.159328 0.163196 0.160803
tips.pivot_table(index=["time", "smoker"], columns="day",
                 values="tip_pct", aggfunc=len, margins=True)
day Fri Sat Sun Thur All
time smoker
Dinner No 3.0 45.0 57.0 1.0 106
Yes 9.0 42.0 19.0 NaN 70
Lunch No 1.0 NaN NaN 44.0 45
Yes 6.0 NaN NaN 17.0 23
All 19.0 87.0 76.0 62.0 244
tips.pivot_table(index=["time", "size", "smoker"], columns="day",
                 values="tip_pct", fill_value=0)
day Fri Sat Sun Thur
time size smoker
Dinner 1 No 0.000000 0.137931 0.000000 0.000000
Yes 0.000000 0.325733 0.000000 0.000000
2 No 0.139622 0.162705 0.168859 0.159744
Yes 0.171297 0.148668 0.207893 0.000000
3 No 0.000000 0.154661 0.152663 0.000000
... ... ... ... ... ... ...
Lunch 3 Yes 0.000000 0.000000 0.000000 0.204952
4 No 0.000000 0.000000 0.000000 0.138919
Yes 0.000000 0.000000 0.000000 0.155410
5 No 0.000000 0.000000 0.000000 0.121389
6 No 0.000000 0.000000 0.000000 0.173706

21 rows × 4 columns

23.5.1. Cross-Tabulations: Crosstab#

from io import StringIO

data = """Sample  Nationality  Handedness
1   USA  Right-handed
2   Japan    Left-handed
3   USA  Right-handed
4   Japan    Right-handed
5   Japan    Left-handed
6   Japan    Right-handed
7   USA  Right-handed
8   USA  Left-handed
9   Japan    Right-handed
10  USA  Right-handed"""
data = pd.read_table(StringIO(data), sep="\s+")

data
Sample Nationality Handedness
0 1 USA Right-handed
1 2 Japan Left-handed
2 3 USA Right-handed
3 4 Japan Right-handed
4 5 Japan Left-handed
5 6 Japan Right-handed
6 7 USA Right-handed
7 8 USA Left-handed
8 9 Japan Right-handed
9 10 USA Right-handed
pd.crosstab(data["Nationality"], data["Handedness"], margins=True)
Handedness Left-handed Right-handed All
Nationality
Japan 2 3 5
USA 1 4 5
All 3 7 10
pd.crosstab([tips["time"], tips["day"]], tips["smoker"], margins=True)
smoker No Yes All
time day
Dinner Fri 3 9 12
Sat 45 42 87
Sun 57 19 76
Thur 1 0 1
Lunch Fri 1 6 7
Thur 44 17 61
All 151 93 244

23.6. 연습문제#

참고: (실습) 데이터 그룹화