집계(Aggregation)와 그룹화(Grouping)

데이터 분석의 핵심은 방대한 데이터를 효율적으로 요약하는 것입니다. 즉, ‘합계’, ‘평균’, ‘중앙값’, ‘최소값’, ‘최대값’과 같은 집계를 계산합니다. 즉, ’합계’, ‘평균’, ‘중앙값’ 등의 집계 연산을 통해 대규모 데이터 세트의 특징을 하나의 숫자로 나타내는 작업입니다. 이번 장에서는 NumPy 배열에서 살펴본 것과 비슷한 간단한 연산부터 groupby 개념을 기반으로 한 보다 정교한 작업까지 Pandas의 집계를 살펴보겠습니다.

편의상 이전 장에서 사용한 것과 동일한 display 마법 함수를 사용하겠습니다.

import numpy as np
import pandas as pd


class display(object):
    """Display HTML representation of multiple objects"""

    template = """<div style="float: left; padding: 10px;">
    <p style='font-family:"Courier New", Courier, monospace'>{0}</p>{1}
    </div>"""

    def __init__(self, *args):
        self.args = args

    def _repr_html_(self):
        return "\n".join(
            self.template.format(a, eval(a)._repr_html_()) for a in self.args
        )

    def __repr__(self):
        return "\n\n".join(a + "\n" + repr(eval(a)) for a in self.args)

예제: 행성(Planets) 데이터

여기서는 Seaborn 패키지(Visualization With Seaborn 참조)를 통해 제공되는 Planets 데이터 세트를 사용합니다. 이는 천문학자들이 다른 별 주위에서 발견한 행성(외계 행성(Exoplanets)으로 불림)에 대한 정보를 제공합니다. 간단한 Seaborn 명령으로 다운로드합니다.

import seaborn as sns

planets = sns.load_dataset("planets")
planets.shape
(1035, 6)
planets.head()
method number orbital_period mass distance year
0 Radial Velocity 1 269.300 7.10 77.40 2006
1 Radial Velocity 1 874.774 2.21 56.95 2008
2 Radial Velocity 1 763.000 2.60 19.84 2011
3 Radial Velocity 1 326.030 19.40 110.62 2007
4 Radial Velocity 1 516.220 10.50 119.47 2009

여기에는 2014년까지 발견된 1,000개 이상의 외계 행성에 대한 세부 정보가 포함되어 있습니다.

Pandas의 기본 집계 연산

“집계: 최소, 최대 및 그 사이의 모든 것”에서 NumPy 배열에 사용할 수 있는 데이터 집계 중 일부를 살펴보았습니다. 1차원 NumPy 배열처럼, Pandas Series에서도 집계 연산은 단일 값을 반환합니다.

rng = np.random.RandomState(42)
ser = pd.Series(rng.rand(5))
ser
0    0.374540
1    0.950714
2    0.731994
3    0.598658
4    0.156019
dtype: float64
ser.sum()
2.811925491708157
ser.mean()
0.5623850983416314

DataFrame의 경우 집계는 각 열 내에서 결과를 반환합니다.

df = pd.DataFrame({"A": rng.rand(5), "B": rng.rand(5)})
df
A B
0 0.155995 0.020584
1 0.058084 0.969910
2 0.866176 0.832443
3 0.601115 0.212339
4 0.708073 0.181825
df.mean()
A    0.477888
B    0.443420
dtype: float64

axis 인수를 지정하면 대신 각 행 내에서 집계합니다.

df.mean(axis="columns")
0    0.088290
1    0.513997
2    0.849309
3    0.406727
4    0.444949
dtype: float64

Pandas ‘Series’ 및 ‘DataFrame’ 개체에는 집계: 최소, 최대 및 그 사이의 모든 것에 언급된 모든 공통 집계가 포함되어 있습니다. 또한 각 열에 대한 여러 공통 집계를 계산하고 결과를 반환하는 편의 메서드 describe가 있습니다. 이제 누락된 값이 있는 행을 삭제하기 위해 Planets 데이터에서 이를 사용해 보겠습니다.

planets.dropna().describe()
number orbital_period mass distance year
count 498.00000 498.000000 498.000000 498.000000 498.000000
mean 1.73494 835.778671 2.509320 52.068213 2007.377510
std 1.17572 1469.128259 3.636274 46.596041 4.167284
min 1.00000 1.328300 0.003600 1.350000 1989.000000
25% 1.00000 38.272250 0.212500 24.497500 2005.000000
50% 1.00000 357.000000 1.245000 39.940000 2009.000000
75% 2.00000 999.600000 2.867500 59.332500 2011.000000
max 6.00000 17337.500000 25.000000 354.000000 2014.000000

이 방법은 데이터 세트의 전반적인 속성을 이해하는 데 도움이 됩니다. 예를 들어 ‘연도’ 열에서는 외계 행성이 1989년에 발견되었지만 데이터 세트에 있는 모든 행성의 절반은 2010년 이후까지 발견되지 않았음을 확인합니다. 이는 특별히 설계된 우주 망원경을 사용하여 다른 별 주위에서 식행성을 찾는 것을 목표로 한 케플러 임무 덕분이었습니다.

다음 표에는 기타 기본 제공 Pandas 집계가 요약되어 있습니다.

집계 반품
카운트 총 품목수
첫번째, 마지막 첫 번째 및 마지막 항목
평균, 중앙값 평균과 중앙값
최소, 최대 최소 및 최대
std, var 표준편차 및 분산
미친 평균 절대 편차
프로드 모든 품목의 제품
모든 항목의 합계

이들은 모두 DataFrameSeries 개체의 메서드입니다.

그러나 데이터에 더 깊이 들어가려면 단순한 집계만으로는 충분하지 않은 경우가 많습니다. 데이터 요약의 다음 단계는 ‘groupby’ 작업으로, 이를 통해 데이터 하위 집합에 대한 집계를 빠르고 효율적으로 계산합니다.

GroupBy: 분할(Split), 적용(Apply), 결합(Combine)

간단한 집계는 데이터 세트의 특징을 제공할 수 있지만 종종 일부 레이블이나 인덱스에 대해 조건부 집계를 선호합니다. 이는 소위 ‘groupby’ 작업에서 구현됩니다. “그룹화 기준”이라는 이름은 SQL 데이터베이스 언어의 명령에서 유래했지만 R 데이터 과학자로 유명한 해들리 위컴(Hadley Wickham)이 만든 용어인 분할, 적용, 결합을 생각하면 더 이해하기 쉬울 것입니다.

분할-적용-결합 단계

“적용”이 합계 집계인 이 분할-적용-결합 작업의 정식 예가 다음 그림에 나와 있습니다.

(부록의 그림 출처)

이는 groupby 작업이 수행하는 작업을 보여줍니다.

  • 분할 단계에는 지정된 키 값에 따라 DataFrame을 분할하고 그룹화하는 작업이 포함됩니다.
  • 적용 단계에는 개별 그룹 내에서 일반적으로 집계, 변환 또는 필터링과 같은 일부 기능을 계산하는 작업이 포함됩니다.
  • 결합 단계는 이러한 작업의 결과를 출력 배열로 병합합니다.

앞서 설명한 마스킹, 집계 및 병합 명령을 조합하여 수동으로 수행할 수도 있지만 중요한 사실은 중간 분할을 명시적으로 인스턴스화할 필요가 없다는 점입니다. 오히려 ’groupby’는 (종종) 데이터에 대한 단일 전달로 이 작업을 수행하여 각 그룹에 대한 합계, 평균, 개수, 최소 또는 기타 집계를 업데이트합니다. ’groupby’의 장점은 이러한 단계를 추상화한다는 것입니다. 사용자는 내부적으로 계산이 어떻게 수행되는지 생각할 필요가 없고 오히려 작업 전체에 대해 생각합니다.

구체적인 예로, 다음 그림에 표시된 계산에 Pandas를 사용하는 방법을 살펴보겠습니다. 입력 DataFrame을 생성하는 것부터 시작하겠습니다.

df = pd.DataFrame(
    {"key": ["A", "B", "C", "A", "B", "C"], "data": range(6)}, columns=["key", "data"]
)
df
key data
0 A 0
1 B 1
2 C 2
3 A 3
4 B 4
5 C 5

가장 기본적인 분할-적용-결합 작업은 원하는 키 열의 이름을 전달하여 DataFramegroupby 메서드로 계산합니다.

df.groupby("key")
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x11d241e20>

반환되는 것은 DataFrame 개체 집합이 아니라 DataFrameGroupBy 개체입니다. 이 개체는 마법이 있는 곳입니다. 그룹을 파헤칠 준비가 되어 있지만 집계가 적용될 때까지 실제 계산을 수행하지 않는 DataFrame의 특수 보기로 생각합니다. 이런 “지연 평가(Lazy Evaluation)” 방식 덕분에 사용자에게 거의 투명한 방식으로 공통 집계를 효율적으로 구현할 수 있음을 의미합니다.

결과를 생성하기 위해 이 DataFrameGroupBy 개체에 집계를 적용하면 적절한 적용/결합 단계를 수행하여 원하는 결과를 생성합니다.

df.groupby("key").sum()
data
key
A 3
B 5
C 7

여기서 sum 방법은 단지 하나의 가능성일 뿐입니다. 다음 설명에서 볼 수 있듯이 대부분의 Pandas 또는 NumPy 집계 함수는 물론 대부분의 DataFrame 작업을 적용합니다.

GroupBy 객체

GroupBy 개체는 유연한 추상화입니다. 여러 면에서 이는 단순히 DataFrame의 컬렉션으로 처리될 수 있지만 내부적으로는 더 정교한 작업을 수행합니다. Planets 데이터를 사용한 몇 가지 예를 살펴보겠습니다.

아마도 GroupBy를 통해 제공되는 가장 중요한 작업은 집계, 필터, 변환적용일 것입니다. 다음 섹션에서 이들 각각에 대해 더 자세히 논의할 것입니다. 하지만 그 전에 기본 GroupBy 작업과 함께 사용할 수 있는 다른 기능 중 일부를 살펴보겠습니다.

열 인덱싱

GroupBy 개체는 DataFrame과 동일한 방식으로 열 인덱싱을 지원하고 수정된 GroupBy 객체를 반환합니다. 예를 들어:

planets.groupby("method")
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x11d1bc820>
planets.groupby("method")["orbital_period"]
<pandas.core.groupby.generic.SeriesGroupBy object at 0x11d1bcd60>

여기서는 열 이름을 참조하여 원래 DataFrame 그룹에서 특정 Series 그룹을 선택했습니다. GroupBy 개체와 마찬가지로 개체에 대한 집계를 호출할 때까지 계산이 수행되지 않습니다.

planets.groupby("method")["orbital_period"].median()
method
Astrometry                         631.180000
Eclipse Timing Variations         4343.500000
Imaging                          27500.000000
Microlensing                      3300.000000
Orbital Brightness Modulation        0.342887
Pulsar Timing                       66.541900
Pulsation Timing Variations       1170.000000
Radial Velocity                    360.200000
Transit                              5.714932
Transit Timing Variations           57.011000
Name: orbital_period, dtype: float64

이는 각 방법이 민감한 궤도 기간(일)의 일반적인 규모에 대한 아이디어를 제공합니다.

그룹에 대한 반복

GroupBy 객체는 그룹에 대한 직접 반복을 지원하여 각 그룹을 Series 또는 DataFrame으로 반환합니다.

for method, group in planets.groupby("method"):
    print("{0:30s} shape={1}".format(method, group.shape))
Astrometry                     shape=(2, 6)
Eclipse Timing Variations      shape=(9, 6)
Imaging                        shape=(38, 6)
Microlensing                   shape=(23, 6)
Orbital Brightness Modulation  shape=(3, 6)
Pulsar Timing                  shape=(5, 6)
Pulsation Timing Variations    shape=(1, 6)
Radial Velocity                shape=(553, 6)
Transit                        shape=(397, 6)
Transit Timing Variations      shape=(4, 6)

이는 디버깅을 위해 그룹을 수동으로 검사하는 데 유용할 수 있지만 내장된 ‘적용’ 기능을 사용하는 것이 훨씬 더 빠른 경우가 많습니다. 이에 대해서는 잠시 논의하겠습니다.

메서드 디스패치(Method Dispatch)

파이썬의 클래스 매직(Magic Method)을 활용해 GroupBy 객체에 의해 명시적으로 구현되지 않은 모든 메서드는 DataFrame 또는 Series 객체인지 여부에 관계없이 그룹을 통해 전달되고 호출됩니다. 예를 들어 describe 메서드를 사용하는 것은 각 그룹을 나타내는 DataFrame에서 describe를 호출하는 것과 같습니다.

planets.groupby("method")["year"].describe().unstack()
       method                       
count  Astrometry                          2.0
       Eclipse Timing Variations           9.0
       Imaging                            38.0
       Microlensing                       23.0
       Orbital Brightness Modulation       3.0
                                         ...  
max    Pulsar Timing                    2011.0
       Pulsation Timing Variations      2007.0
       Radial Velocity                  2014.0
       Transit                          2014.0
       Transit Timing Variations        2014.0
Length: 80, dtype: float64

이 표를 보면 데이터를 더 잘 이해하는 데 도움이 됩니다. 예를 들어 2014년까지 행성의 대다수는 Radial Velocity 및 Transit 방법으로 발견되었지만 후자의 방법은 최근에 더 일반적이 되었습니다. 최신 방법은 통과 타이밍 변화(Transit Timing Variation)와 궤도 밝기 변조(Orbital Brightness Modulation)인 것으로 보이며, 이는 2011년까지 새로운 행성을 발견하는 데 사용되지 않았습니다.

이런 방식의 메서드들은 각 개별 그룹에 적용되고 결과는 GroupBy 내에서 결합되어 반환됩니다. 다시 말하지만, 유효한 DataFrame/Series 메소드는 해당 GroupBy 객체에서 비슷한 방식으로 호출될 수 있습니다.

집계, 필터링, 변환, 적용 연산

이전 논의에서는 결합 작업의 집계에 중점을 두었지만 더 많은 옵션을 사용합니다. 특히 GroupBy 객체에는 그룹화된 데이터를 결합하기 전에 다양하고 유용한 작업을 효율적으로 구현하는 aggregate, filter, transformapply 메서드가 있습니다.

다음 하위 섹션에서는 이 DataFrame을 사용합니다.

rng = np.random.RandomState(0)
df = pd.DataFrame(
    {
        "key": ["A", "B", "C", "A", "B", "C"],
        "data1": range(6),
        "data2": rng.randint(0, 10, 6),
    },
    columns=["key", "data1", "data2"],
)
df
key data1 data2
0 A 0 5
1 B 1 0
2 C 2 3
3 A 3 3
4 B 4 7
5 C 5 9

집계

이제 sum, median 등과 같은 GroupBy 집계에 익숙하지만 aggregate 방법을 사용하면 훨씬 더 많은 유연성을 얻을 수 있습니다. 문자열, 함수 또는 이들의 목록을 가져와서 모든 집계를 한 번에 계산합니다. 다음은 이 모든 것을 결합한 간단한 예입니다.

df.groupby("key").aggregate(["min", np.median, max])
data1 data2
min median max min median max
key
A 0 1.5 3 3 4.0 5
B 1 2.5 4 0 3.5 7
C 2 3.5 5 3 6.0 9

또 다른 일반적인 패턴은 해당 열에 적용할 작업에 사전 매핑 열 이름을 전달하는 것입니다.

df.groupby("key").aggregate({"data1": "min", "data2": "max"})
data1 data2
key
A 0 5
B 1 7
C 2 9

필터링

필터링 작업을 사용하면 그룹 속성을 기반으로 데이터를 삭제합니다. 예를 들어 표준 편차가 일부 임계값보다 큰 모든 그룹을 유지하려고 합니다.

def filter_func(x):
    return x["data2"].std() > 4


display("df", "df.groupby('key').std()", "df.groupby('key').filter(filter_func)")

df

key data1 data2
0 A 0 5
1 B 1 0
2 C 2 3
3 A 3 3
4 B 4 7
5 C 5 9

df.groupby('key').std()

data1 data2
key
A 2.12132 1.414214
B 2.12132 4.949747
C 2.12132 4.242641

df.groupby('key').filter(filter_func)

key data1 data2
1 B 1 0
2 C 2 3
4 B 4 7
5 C 5 9

필터 함수는 그룹이 필터링을 통과하는지 여부를 지정하는 부울 값을 반환해야 합니다. 여기서 A그룹은 표준편차가 4보다 크지 않으므로 결과에서 제외됩니다.

변환

집계는 데이터의 축소된 버전을 반환해야 하지만 변환은 재결합을 위해 전체 데이터의 일부 변환된 버전을 반환합니다. 이러한 변환의 경우 출력은 입력과 모양이 동일합니다. 일반적인 예는 그룹별 평균을 빼서 데이터를 중앙에 배치하는 것입니다.

def center(x):
    return x - x.mean()


df.groupby("key").transform(center)
data1 data2
0 -1.5 1.0
1 -1.5 -3.5
2 -1.5 -3.0
3 1.5 -1.0
4 1.5 3.5
5 1.5 3.0

적용 방법

apply 메소드를 사용하면 그룹 결과에 임의의 함수를 적용합니다. 함수는 DataFrame을 가져와 Pandas 객체(예: DataFrame, Series) 또는 스칼라를 반환해야 합니다. 결합 단계의 동작은 반환된 출력 유형에 맞게 조정됩니다.

예를 들어 다음은 두 번째 열의 합계로 첫 번째 열을 정규화하는 적용 작업입니다.

def norm_by_data2(x):
    # x is a DataFrame of group values
    x["data1"] /= x["data2"].sum()
    return x


df.groupby("key").apply(norm_by_data2)
key data1 data2
0 A 0.000000 5
1 B 0.142857 0
2 C 0.166667 3
3 A 0.375000 3
4 B 0.571429 7
5 C 0.416667 9

GroupBy 내의 apply는 유연합니다. 유일한 기준은 함수가 DataFrame을 사용하고 Pandas 객체 또는 스칼라를 반환한다는 것입니다. 그 사이에 무엇을 하느냐는 당신에게 달려 있습니다!

분할 키 지정

이전에 제시된 간단한 예에서는 ’DataFrame’을 단일 열 이름으로 분할했습니다. 이는 그룹을 정의할 수 있는 많은 옵션 중 하나일 뿐이며 여기서는 그룹 지정에 대한 몇 가지 다른 옵션을 살펴보겠습니다.

그룹화 키를 제공하는 목록, 배열, 시리즈 또는 인덱스

키는 DataFrame의 길이와 일치하는 모든 시리즈 또는 목록이 될 수 있습니다. 예를 들어:

L = [0, 1, 0, 1, 2, 0]
df.groupby(L).sum()
data1 data2
0 7 17
1 4 3
2 4 7

물론 이는 이전에 df.groupby('key')를 수행하는 또 다른 더 자세한 방법이 있음을 의미합니다.

df.groupby(df["key"]).sum()
data1 data2
key
A 3 8
B 5 7
C 7 12

그룹에 대한 사전 또는 시리즈 매핑 색인

또 다른 방법은 인덱스 값을 그룹 키에 매핑하는 사전을 제공하는 것입니다.

df2 = df.set_index("key")
mapping = {"A": "vowel", "B": "consonant", "C": "consonant"}
display("df2", "df2.groupby(mapping).sum()")

df2

data1 data2
key
A 0 5
B 1 0
C 2 3
A 3 3
B 4 7
C 5 9

df2.groupby(mapping).sum()

data1 data2
key
consonant 12 19
vowel 3 8

모든 파이썬(Python) 함수

매핑과 유사하게 인덱스 값을 입력하고 그룹을 출력하는 파이썬(Python) 함수를 전달합니다.

df2.groupby(str.lower).mean()
data1 data2
key
a 1.5 4.0
b 2.5 3.5
c 3.5 6.0

유효한 키 목록

또한 이전 주요 선택 사항 중 하나를 결합하여 다중 인덱스로 그룹화합니다.

df2.groupby([str.lower, mapping]).mean()
data1 data2
key key
a vowel 1.5 4.0
b consonant 2.5 3.5
c consonant 3.5 6.0

그룹화 예

이에 대한 예로서, 몇 줄의 파이썬(Python) 코드로 이 모든 것을 하나로 묶어 발견된 행성의 수를 방법 및 10년 단위로 계산합니다.

decade = 10 * (planets["year"] // 10)
decade = decade.astype(str) + "s"
decade.name = "decade"
planets.groupby(["method", decade])["number"].sum().unstack().fillna(0)
decade 1980s 1990s 2000s 2010s
method
Astrometry 0.0 0.0 0.0 2.0
Eclipse Timing Variations 0.0 0.0 5.0 10.0
Imaging 0.0 0.0 29.0 21.0
Microlensing 0.0 0.0 12.0 15.0
Orbital Brightness Modulation 0.0 0.0 0.0 5.0
Pulsar Timing 0.0 9.0 1.0 1.0
Pulsation Timing Variations 0.0 0.0 1.0 0.0
Radial Velocity 1.0 52.0 475.0 424.0
Transit 0.0 0.0 64.0 712.0
Transit Timing Variations 0.0 0.0 0.0 9.0

이는 현실적인 데이터 세트를 볼 때 지금까지 논의한 많은 작업을 결합하는 것의 힘을 보여줍니다. 우리는 첫 번째 발견 후 몇 년 동안 외계 행성이 언제 어떻게 발견되었는지에 대한 대략적인 이해를 빠르게 얻습니다.

이 몇 줄의 코드를 자세히 살펴보고 개별 단계를 평가하여 결과에 대해 정확히 무엇을 하는지 이해하는 것이 좋습니다. 확실히 다소 복잡한 예이지만 이러한 부분을 이해하면 자신의 데이터를 유사하게 탐색할 수 있는 수단을 얻을 수 있습니다.