Python

데이터 집계와 그룹 연산(1)

nova-unum 2022. 7. 22. 14:41

데이터 집계와 그룹 연산(1)

데이터셋을 분류하고 각 그룹에 집계나 변형 같은 함수를 적용하는 건 데이터 분석 과정에서 무척 중요한 일이다. 데이터를 불러오고 취합해서 하나의 데이터 집합을 준비하고 나면 그룹 통계를 구하거나 가능하다면 피벗데이블을 구해서 보고서를 만들거나 시각화하게 된다 

파이썬과 pandas의 강력한 표현력을 잘 이용하면 아주 복잡한 그룹 연산도 pandas 객체나 NumPy 배열을 받는 함수의 조합으로 해결할 수 있다. 

. 하나 이상의 키(함수, 배열, DataFrame의 컬럼 이름)을 이용해서 pandas 객체를 여러 조각으로 나누는 방법

. 합계, 평균, 표준편차, 사용자 정의 함수 같은 그룹 요약 통계를 계산하는 방법

. 정규화, 선형회귀, 등급 또는 부분집합 선택 같은 집단 내 변형이나 다른 조작을 적용하는 방법

. 피벗데이블과 교차일람표를 구하는 방법

. 변위치 분석과 다른 통계 집단 분석을 수행하는 방법


GroupBy 메카닉

그룹 연산의 첫 번째 단계에서는 Series, DataFrame 같은 pandas 객체나 아니면 다른 객체에 들어 있는 데이터를 하나 이상의 키를 기준으로 분리한다. 

객체는 하나의 축을 기준으로 분리하는데, 예를 들어 DataFrame은 로우(axis=0)로 분리하거나 컬럼(axis=1)으로 분리할 수 있다. 분리하고 나서는 함수를 각 그룹에 적용시켜 새로운 값을 얻어낸다. 마지막으로 함수를 적용한 결과를 하나의 객체로 결합한다. 마지막으로 함수를 적용한 결과를 하나의 객체로 결합한다.

각 그룹의 색인은 다음과 같이 다양한 형태가 될 수 있으며, 모두 같은 타입일 필요도 없다.

. 그룹으로 묶을 축과 동일한 길이의 리스트나 배열

. DataFrame의 컬럼 이름을 지칭하는 값

. 그룹으로 묶을 값과 그룹 이름에 대응하는 사전이나 Series 객체

. 축 색인 혹은 색인 내의 개별 이름에 대해 실행되는 함수

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

 

이 데이터를 key1으로 묶고 각 그룹에서 data1의 평균을 구해보자.

여러 가지 방법이 있지만 그중 하나는 data1에 대해 groupby 메서드를 호출하고 key1 컬럼을 넘기는 것이다.

grouped = df['data1'].groupby(df['key1'])
grouped

이 grouped 변수는 GroupBy 객체다. df['key1']로 참조되는 중간값에 대한 거 외에는 아무것도 계산되지 않은 객체다. 이 객체는 그룹 연산을 위해 필요한 모든 정보를 가지고 있어서 각 그룹에 어떤 연산을 적용할 수 있게 해준다. 그룹별 평균을 구하려면 GroupBy 객체의 mean 메서드를 사용하면 된다.

grouped.mean()

 

이 예제에서 중요한 점은 데이터(Series 객체)가 그룹 색인에 따라 수집되고 key1 컬럼에 있는 유일한 값으로 색인되는 새로운 Series 객체가 생성된다는 것이다. 새롭게 생성된 Series 객체의 색인은 'key1'인데, 그 이유는 DataFrame 컬럼인 df['key1] 때문이다.

데이터를 두 개의 색인으로 묶었고, 그 결과 계층적 색인을 가지는 Series를 얻을 수 있었다.

means = df['data1'].groupby([df['key1'], df['key2']]).mean()
means
key1  key2
a     one    -0.547675
      two    -1.859519
b     one    -0.176699
      two    -0.946255
Name: data1, dtype: float64

 

다음 예제는 그룹의 색인 모두 Series 객체인데, 길이만 같다면 어떤 배열이라도 상관없다.

states = np.array(['Ohio','California','California', 'Ohio','Ohio'])
years = np.array([2005, 2005, 2006, 2005, 2006])
states
years
array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'], dtype='<U10')
array([2005, 2005, 2006, 2005, 2006])
df['data1'].groupby([states, years]).mean()
California  2005   -1.859519
            2006   -0.176699
Ohio        2005   -1.005806
            2006   -0.029993
Name: data1, dtype: float64

한 그룹으로 묶을 정보는 주로 DataFrame  안에서 찾게 되는데, 이 경우 컬럼 이름(문자열, 숫자 혹은 다른 파이썬 객체)을 넘겨서 그룹의 색인으로 사용할 수 있다.

df.groupby('key1').mean()

df.groupby(['key1','key2']).mean()

 

groupby를 쓰는 목적과 별개로, 일반저으로 유용한 GroupBy 메서드는 그룹의 크기를 담고 있는 Series를 반환하는 size 메서드다.

df.groupby(['key1','key2']).size()

그룹 색인에서 누락된 값은 결과에서 제외된다는 것을 기억하자

그룹 간 순회하기

GroupyBy 객체는 이터레이션을 지원하는데, 그룹 이름과 그에 따른 데이터 묶음을 튜플로 반환한다.

for name, group in df.groupby('key1'):
    print(name)
    print(group)

이처럼 색인이 여럿 존재하는 경우 튜플의 첫 번째 원소가 색인값이 된다

for(k1, k2), group in df.groupby(['key1', 'key2']):
    print((k1, k2))
    print(group)

원하는 데이터만 골라낼 수 있다. 한 줄이면 그룹별 데이터를 사전형으로 쉽게 바꿔서 유용하게 사용할 수 있다.

pieces = dict(list(df.groupby('key1')))
pieces
{'a':   key1 key2     data1     data2
 0    a  one  0.185375 -1.761134
 1    a  two -1.545721 -0.256998
 4    a  one  1.426195 -1.783227,
 'b':   key1 key2     data1     data2
 2    b  one -0.821296  0.494849
 3    b  two  0.102631 -1.289961}

 

pieces['b']

groupby 메서드는 기본적으로 axis=0에 대해 그룹을 만든는데, 다른 축으로 그룹을 만드는 것도 가능하다. df의 컬럼을 dtype에 따라 그룹으로 묶을 수도 있다.

df.dtypes
key1      object
key2      object
data1    float64
data2    float64
dtype: object

 

for dtype, group in grouped:
    print(dtype)
    print(group)
    
a
0   -0.741722
1    0.473217
4   -0.517570
Name: data1, dtype: float64
b
2    1.679763
3    0.655320
Name: data1, dtype: float64
​

 

컬럼이나 컬럼의 일부만 선택하기

DataFrame에서 만든 GroupBy 객체를 이름이나 컬럼 이름이 담긴 배열로 색인하면 수집을 위해 해당 컬럼을 선택하게 된다.

df.groupby('key1')['data1']
df.groupby('key1')[['data2']]

위 코드는 아래 코드에 대한 신택틱 슈거로 같은 결과를 반환한다.

df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])

data2 컬럼에 대해서만 평균을 구하고 결과를 DataFrame으로 받고 싶다면 아래와 같이 작성한다.

df.groupby(['key1','key2'])[['data2']].mean()

s_grouped = df.groupby(['key1','key2'])['data2']
s_grouped.mean()

key1  key2
a     one     0.299317
      two     0.999824
b     one    -0.589477
      two     0.158376
Name: data2, dtype: float64

 

사전과 Series에서 그룹핑하기

그룹 정보는 배열이 아닌 형태로 존재하기도 한다. 

people = pd.DataFrame(np.random.randn(5,5), 
                     columns=['a','b','c','d','e'],
                     index =['Joe','Steve','Wes','Jim','Travis'])
people

 

people.iloc[2:3, [1,2]] = np.nan
people

이제 각 컬럼을 나타낼 그룹 목록이 있고, 그룹별로 칼럼의 값을 모두 더한다고 해보자

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

{'a': 'red', 'b': 'red', 'c': 'blue', 'd': 'blue', 'e': 'red', 'f': 'orange'}
by_column = people.groupby(mapping, axis=1)
by_column.sum()

Series에 대해서도 같은 기능을 수행할 수 있는데, 고정된 크기의 맵이라고 보면 된다.

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=1).count()

 

함수로 그룹핑하기

파이썬 함수를 사용하는 것은 사전이나 Series를 사용해서 그룹을 매핑하는 것보다 좀 더 일반적인 방법이다.

그룹 색인으로 넘긴 함수는 색인값 하나마다 한 번씩 호출되며, 반환값은 그 그룹의 이름으로 사용된다. 

만약 이름의 길이별로 그룹을 묶고 싶다면 이름의 길이가 담긴 배열을 만들어 넘기는 대신 len함수를 넘기면 된다.

내부적으로는 모두 배열로 변환되므로 함수를 배열, 사전 또는 Series와 섞어 쓰더라도 전혀 문제가 되지 않는다.

 

색인 단계로 그룹핑하기

계층적으로 색인된 데이터는 축 색인의 단계 중 하나를 사용해서 편리하게 집계할 수 있는 기능을 제공한다.

columns = pd.MultiIndex.from_arrays([['US','US','US','JP','JP'],
                                     [1, 3, 5, 1, 3]],
                                     names=['cty','tenor'])
columns

MultiIndex([('US', 1),
            ('US', 3),
            ('US', 5),
            ('JP', 1),
            ('JP', 3)],
           names=['cty', 'tenor'])

 

hier_df = pd.DataFrame(np.random.randn(4,5), columns=columns)
hier_df

 

level 예약어를 사용해서 레벨 번호나 이름을 넘기면 된다.

hier_df.groupby(level='cty', axis=1).count()

 

데이터 집계

데이터 집계는 배열로부터 스칼라값을 만들어내는 모든 데이터 변환 작업을 말한다. 

최적화된 groupby 메서드

함수 설명
count 그룹에서 NA가 아닌 값의 수를 반환한다.
sum NA가 아닌 값들의 합을 구한다.
mean NA가 아닌 값들의 평균을 구한다.
median NA가 아닌 값들의 산술 중간값을 구한다.
std, var 편향되지 않은(n-1을 분모로 하는) 표준편차와 분산
min, max NA가 아닌 값들 중 최소값과 최댓값
prod NA가 아닌 값들의 곱
first, last NA가 아닌 값들 중 첫째 값과 마지막 값

직접 고안한 집계함수를 사용하고 추가적으로 그룹 객체에 이미 정의된 메서드를 연결해서 사용하는 것도 가능하다. 예를 들어 quantile 메서드가 Series나 DataFrame의 컬럼의 변위치를 계산한다는 점을 생각해보자.

quantile 메서드는 GroupBy만을 위해 구현되지 않았지만 Series 메서드이기 때문에 여기서 사용할 수 있다. 내부적으로 GroupBy는 Series를 효과적으로 잘게 자르고 각 조각에 대해 piece.quantile(0.9)를 호출한다. 그리고 이 결과들을 모두 하나의 객체로 합쳐서 반환한다.

 

자신만의 데이터 집계함수를 사용하려면 배열의 aggregate나 agg 메서드에 해당 함수를 넘기면 된다.

def peak_to_peak(arr):
    return arr.max() - arr.min()

grouped.agg(peak_to_peak)

 

describe 같은 메서드는 데이터를 집계하지 않는데도 잘 작동함을 확인할 수 있다

grouped.describe()

 

사용자 정의 집계함수는 일반적으로 최적화된 groupby 메서드에 포함된 함수들에 비해 무척 느리게 동작하는데, 그 이유는 중간 데이터를 생성하는 과정에서 함수 호출이나 데이터 정렬 같은 오버헤드가 발생하기 때문이다.

칼럼에 여러 가지 함수 적용하기

Series나DataFrame의 모든 컬럼을 집계하는 것은 mean이나 std 같은 메서드를 호출하거나 원하는 함수에 aggregate를 사용하는 것이다. 하지만 컬럼에 따라 다른 함수를 사용해서 집계를 수행하거나 여러 개의 함수를 한 번에 적용하기 원한다면 이를 쉽고 간단하게 수행할 수 있다. 

기술 통계에서는 함수 이름을 문자열로 넘기면 된다.

grouped = tips.groupby(['day','smoker'])
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

만일 함수 목록이나 함수 이름을 넘기면 함수 이름을 컬럼 이름으로 하는 DataFrame을 얻게 된다.

grouped_pct.agg(['mean','std',peak_to_peak])

데이터 그룹에 대해 독립적으로 적용하기 위해 agg에 집계함수들의 리스트를 넘겼다.

GroupBy 객체에서 자동으로 지정하는 컬럼 이름을 그대로 쓰지 않아도 된다.  lambda 함수는 이름(함수 이름은 _name_ 속성으로 확인 가능하다)이 '<lambda>인데', 이를 그대로 쓸 경우 알아보기 힘들어진다. 이때 이름과 함수가 담긴 (name, function) 튜플의 리스트를 넘기면 각 튜플에서 첫 번째 원소가 DataFrame에서 컬럼 이름으로 사용된다 (2개의 튜플을 가지는 리스트가 순서대로 매핑된다).

DataFrame은 컬럼마다 다른 함수를 적용하거나 여러 개의 함수를 모든 컬럼에 적용할 수 있다. tip_pct와 total_bill 컬럼에 대해 동일한 세 가지 통계를 계산한다고 가정하자.

functions = ['count','mean','max']

result = grouped['tip_pct', 'total_bill'].agg(functions)
result

반환된 DataFrame은 계층적인 컬럼을 가지고 있으며 이는 각 컬럼을 따로 계산한 다음 concat 메서드를 이용해서 keys 인자로 칼럼 이름을 넘겨서 이어붙인 것과 동일하다.

컬럼 이름과 메서드가 담긴 튜플의 리스트를 넘기는 것도 가능하다.

* Durchschnitt은 평균, Abweichung은 편차라는 의미의 독일어다.

ftuples =[('durchschnitt','mean'), ('abweichung', np.var)]
grouped['tip_pct', 'total_bill'].agg(ftuples)

컬럼마다 다른 함수를 적용하고 싶다면 agg메서드에 컬럼 이름에 대응하는 함수가 들어 있는 사전을 넘기면 된다.

단 하나의 칼럼에라도 여러 개의 함수가 적용되었다면 DataFrame은 계층적인 컬럼을 가지게 된다.

 

색인되지 않은 형태로 집계된 데이터 반환하기

지금까지 살펴본 모든 예제에서 집계된 데이터는 유일한 그룹키 조합으로 색인(어떤 경우에는 계층적 색인)되어 반환되었다. 하지만 항상 이런 동작을 기대하는 것은 아니므로 groupby 메서드에 as_index =False를 넘겨서 색인되지 않도록 할 수 있다.

물론 이렇게 하지  않고 색인된 결과에 대해 reset_index 메서드를 호출해서 같은 결과를 얻을 수 잇다. as_index=False 옵션을 사용하면 불필요한 계산을 피할 수 있다.