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)집계(Aggregation)와 그룹화(Grouping)
데이터 분석의 핵심은 방대한 데이터를 효율적으로 요약하는 것입니다. 즉, ‘합계’, ‘평균’, ‘중앙값’, ‘최소값’, ‘최대값’과 같은 집계를 계산합니다. 즉, ’합계’, ‘평균’, ‘중앙값’ 등의 집계 연산을 통해 대규모 데이터 세트의 특징을 하나의 숫자로 나타내는 작업입니다. 이번 장에서는 NumPy 배열에서 살펴본 것과 비슷한 간단한 연산부터 groupby 개념을 기반으로 한 보다 정교한 작업까지 Pandas의 집계를 살펴보겠습니다.
편의상 이전 장에서 사용한 것과 동일한 display 마법 함수를 사용하겠습니다.
예제: 행성(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))
ser0 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 |
표준편차 및 분산 |
미친 |
평균 절대 편차 |
프로드 |
모든 품목의 제품 |
합 |
모든 항목의 합계 |
이들은 모두 DataFrame 및 Series 개체의 메서드입니다.
그러나 데이터에 더 깊이 들어가려면 단순한 집계만으로는 충분하지 않은 경우가 많습니다. 데이터 요약의 다음 단계는 ‘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 |
가장 기본적인 분할-적용-결합 작업은 원하는 키 열의 이름을 전달하여 DataFrame의 groupby 메서드로 계산합니다.
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, transform 및 apply 메서드가 있습니다.
다음 하위 섹션에서는 이 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 |
이는 현실적인 데이터 세트를 볼 때 지금까지 논의한 많은 작업을 결합하는 것의 힘을 보여줍니다. 우리는 첫 번째 발견 후 몇 년 동안 외계 행성이 언제 어떻게 발견되었는지에 대한 대략적인 이해를 빠르게 얻습니다.
이 몇 줄의 코드를 자세히 살펴보고 개별 단계를 평가하여 결과에 대해 정확히 무엇을 하는지 이해하는 것이 좋습니다. 확실히 다소 복잡한 예이지만 이러한 부분을 이해하면 자신의 데이터를 유사하게 탐색할 수 있는 수단을 얻을 수 있습니다.