[태그:] groupby

  • [판다스] 가중평균 상관관계 선형회귀

    이번 포스팅에서 다룰 주제는 판다스에서 그룹별 가중평균weighted average과 상관관계correlation 구하기, 그리고 그룹별 선형회귀regression입니다.

    데이터프레임에서 열 간의 연산이나 두 Series간의 연산은 일상적으로 일어나는 일이기 때문에 익혀두고 있으면 편리하겠죠? 간단한 데이터부터 예를 들어 보겠습니다.

    간단한 가중평균 구하기

    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별 가중평균을 구하는 함수를 작성해 보겠습니다.
    넘파이의 average와 이전 포스팅링크들에서 계속 연습한 apply를 결합한 방식입니다.

    grouped = df.groupby("category")
    
    
    def get_wavg(group):
        return np.average(group["data"], weights=group["weights"])
    
    
    grouped.apply(get_wavg, include_groups=False)

    상관관계 구하기

    이번에는 조금 더 복잡한 예로, 야후 파이낸스에서 가져온 몇몇 주식들과 S&P500 지수(SPX)의 종가 데이터를 살펴보겠습니다.

    1. 데이터 불러오기

    데이터는 저자의 깃헙링크에서 다운받으실 수 있습니다.
    판다스의 read_csv 명령어를 사용해 데이터를 불러오고, 정보까지 확인해 보겠습니다.

    close_px = pd.read_csv("stock_px.csv", parse_dates=True, index_col=0)
    close_px.info()

    info 명령어를 써서 데이터프레임의 내용을 간략하게 살펴봅니다. 총 4개의 컬럼과 2,214개의 컬럼으로 이루어져 있고, 결측치는 없네요.

    참고로 SPX를 제외한 나머지 3개 열은 순서대로 애플(AAPL), 마이크로소프트(MSFT), 엑손모빌(XOM)을 나타냅니다. 그리고 저렇게 종목명을 짧은 알파벳, 혹은 숫자 코드로 표시한 것을 티커라고 부릅니다.

    close_px.tail()

    tail() 명령어를 쓰면 데이터의 마지막 5행을 불러옵니다. info에서 확인했던 대로 2011년 10월 14일까지의 데이터가 잘 들어가 있네요.

    2. 상관관계를 계산하는 함수 작성하기

    이제 연간 SPX 지수와 개별 종목들간의 상관관계를 살펴보겠습니다. SPX 열을 기준으로 다른 열들과의 상관관계를 계산하는 함수를 작성합니다.

    def spx_corr(group):
        return group.corrwith(group["SPX"])

    3. 각 열의 퍼센트 변화율 계산하기

    이제 pct_change 함수를 이용해 close_px의 퍼센트 변화율을 계산합니다.

    rets = close_px.pct_change().dropna()

    4. 연도별 퍼센트 변화율 구하고 상관관계 구하기

    마지막으로, 각 datetime 열에서 년도만 반환하는 함수를 이용해 연도별 퍼센트 변화율을 구해봅니다.

    def get_year(x):
        return x.year
    
    
    by_year = rets.groupby(get_year)
    by_year.apply(spx_corr)

    실행한 결과는 위와 같습니다.
    각 주식의 일자별 가격 변화율을 계산한 다음, 그 변화율이 주가지수와 어느 정도의 상관관계가 있는지를 년도별로 나타낸 결과입니다.

    상관계수가 0.5에서 1 사이에 분포되어 있는 경우가 많은 것으로 보아, 대체로 주가지수의 변동과 높은 상관관계를 보이는 종목들임을 알 수 있습니다.

    5. 서로 다른 두 열끼리의 상관계수 구하기

    위의 예에서는 주가지수(SPX)와 다른 열 사이의 상관계수를 계산했지만, 서로 다른 두 종목끼리의 상관계수도 간단히 계산할 수 있습니다.

    예를 들어 애플과 마이크로소프트의 주가 변화율의 상관관계를 보고 싶다면, 아래와 같이 함수를 작성하고 apply하면 됩니다.

    def corr_aapl_msft(group):
        return group["AAPL"].corr(group["MSFT"])
    
    
    by_year.apply(corr_aapl_msft)

    결과는 위와 같습니다.

    (상관관계 심화) 그룹별 선형회귀

    함수가 판다스 객체가 스칼라를 반환하기만 한다면, groupby 객체를 좀 더 복잡한 통계분석을 위해 사용할 수도 있습니다.

    예컨대 계량경제 라이브러리인 statsmodels를 사용하면 각 데이터 묶음에 대해 최소자승법으로 그룹별 선형회귀를 수행할 수도 있는데요.

    위에서 봤던 주가 데이터를 다시 한 번 예로 들어 보겠습니다.
    먼저 statsmodels 모듈을 sm이라는 이름으로 불러오고, 이를 활용해 회귀를 수행하는 함수를 만들어 보겠습니다.

    import statsmodels.api as sm
    
    
    def regress(data, yvar=None, xvars=None):
        Y = data[yvar]
        X = data[xvars]
        X["intercept"] = 1.0
        result = sm.OLS(Y, X).fit()
        return result.params

    sm.OLS(Y, X)를 통해 OLS(Ordinary Least Squares), 즉 최소제곱법 회귀 모델을 생성했습니다. fit() 메서드는 데이터를 모델에 맞춰 추정한다는 의미를 갖고 있습니다.

    결과물 result의 param 속성은 각 독립변수와 상수항에 대한 추정회귀계수를 담고 있습니다. 이제 이 모델을 SPX 수익률에 대한 애플(AAPL) 주식의 선형회귀는 아래와 같이 수행할 수 있습니다.

    by_year.apply(regress, yvar="AAPL", xvars=["SPX"])

    각 연도별로 AAPL과 SPX간의 선형 관계를 분석한 회귀 계수를 보여주는 결과가 생성되었습니다. 이렇게 groupby 객체의 두 열 내지는 Series를 가지고 여러 가지 복잡한 연산을 수행할 수 있습니다.

  • 판다스 groupby 객체에서 표본 추출하기

    이번 포스팅에서는 판다스 groupby 객체에서 표본 추출하기에 대해 알아보겠습니다.

    어느덧 판다스와 데이터 분석 공부에 관련된 포스팅도 몇 개가 생겨서, 보시기 편하게 내부 메뉴를 개편할 예정입니다.

    데이터셋으로부터 랜덤으로 표본을 뽑아내는 방법에는 여러 가지가 있는데요. 이 중에서 Series의 sample 메서드를 사용하겠습니다.

    예를 들기 위해 트럼프 카드를 코드로 만들어 보겠습니다.

    # 하트, 스페이드, 클로버, 다이아몬드
    suits = ["H", "S", "C", "D"]
    
    # 한 문양당 1~10까지 있고, J,Q,K는 똑같이 10으로 취급 * 문양은 총 4개
    card_val = (list(range(1, 11)) + [10] * 3) * 4
    
    # 1번은 A(에이스)로 표기 + 2~10까지는 숫자 + 그 다음은 J,Q,K
    base_names = ["A"] + list(range(2, 11)) + ["J", "Q", "K"]
    cards = []
    
    # for문을 순회하면서 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)

    카드 이름과 값을 색인으로 하는 트럼프 카드를 Series 객체로 만들어 보았습니다. 이제 5장의 카드를 무작위로 뽑기 위해 다음 코드를 작성해 봅니다.

    def draw(deck, n=5):
        return deck.sample(n)
    
    
    draw(deck)

    스페이드 J, 클로버 2, 스페이드 3, 클로버 Q, 하트 10이 뽑혔네요.

    sample 메서드는 무작위이므로 코드를 실행할 때마다 결과가 바뀝니다. 아울러 비복원추출이기 때문에 카드가 중복으로 뽑히지는 않습니다.

    이제 각 문양별로 2장씩의 카드를 무작위로 뽑고 싶으면 어떻게 해야 할까요? 카드 이름의 끝 글자가 문양을 나타내므로, 이를 이용해 그룹을 나누고 apply를 이용해 보겠습니다.

    def get_suit(card):
        return card[-1]  # 마지막 글자가 모양을 나타냄
    
    
    deck.groupby(get_suit).apply(draw, n=2)

    문양별로 2개씩의 카드가 랜덤으로 추출되었습니다.

    이 때도 마찬가지로 draw 함수 안의 sample 메서드 때문에 코드를 실행할 때마다 결과가 바뀌는 점에 주의합시다.

    만약 계층적 색인이 적용되어 있는 점이 보기 불편하다면 group_keys = false 옵션을 통해 선택된 카드만 남길 수도 있습니다. 즉,

    deck.groupby(get_suit, group_keys=False).apply(draw, n=2)

    이렇게 표현할 수도 있습니다.


    참고 링크 : pandas.core.groupby.DataFrameGroupBy.sample

  • 판다스 groupby 객체의 결측치 채우기

    이번 포스팅에서는 판다스 groupby 객체에 결측치가 있을 경우, 간단한 함수와 apply 메서드의 조합으로 이를 채우는 방법을 살펴보겠습니다.

    각 그룹별 평균처럼 데이터로부터 도출되는 값을 채우는 방법도 있고, 아니면 이미 지정한 스칼라 값을 일괄적으로 결측치에 채우는 방법도 있습니다.

    먼저 결측치를 채우는 기본적인 방법부터 살펴보겠습니다.
    예를 들어 아래와 같은 Series가 있다고 가정하겠습니다.
    0번 인덱스부터 2칸씩 건너뛰며 결측치를 넣어줍니다.

    s = pd.Series(np.random.standard_normal(6))
    s[::2] = np.nan
    
    s

    이제 이 결측치들을 s의 평균값으로 채워넣어 보겠습니다. 이럴 때는 fillna 메서드를 쓰면 됩니다.

    s.fillna(s.mean())

    s.mean(), 즉 s의 평균값으로 결측치가 채워졌습니다.
    그런데 만약 groupby 객체에서 그룹별로 결측치에 채워넣고 싶은 값이 다르다면 어떻게 해야 할까요?

    1. 데이터에서 도출한 특정한 값으로 결측치 채우기

    예를 들어보겠습니다.
    아래는 미국 동부와 서부의 데이터입니다.

    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

    다음으로 데이터의 몇몇 값들을 결측치로 만들어 보겠습니다.

    data[["Vermont", "Nevada", "Idaho"]] = np.nan
    data

    버몬트, 네바다, 아이다호의 값이 결측치로 바뀌었습니다.
    이제 그룹별로 통계치를 간단히 살펴보겠습니다.

    data.groupby(group_key).size()
    data.groupby(group_key).count()
    data.groupby(group_key).mean()

    결측치는 집계에서 제외되므로 size는 4지만 count는 그보다 줄어듦에 유의하셔야 합니다. 이제 각 그룹의 결측치를 해당 그룹의 평균값으로 채워 보겠습니다. 앞선 포스팅링크에서 살펴본 groupby 객체에 apply를 적용하는 방법입니다.

    def fill_mean(group):
        return group.fillna(group.mean())
    
    
    data.groupby(group_key).apply(fill_mean)

    East에 속한 버몬트, West에 속한 네바다와 아이다호에 각각 위에서 구한 평균값이 채워진 것을 확인할 수 있습니다.

    2. 그룹 속성을 활용해 미리 정의된 값으로 결측치 채우기

    두 번째 방법은 그룹의 속성을 활용해, 미리 정의된 값으로 결측치를 채우는 방법입니다. East, West라는 그룹은 내부적으로 name이라는 속성을 가지는데, 이를 활용하는 것입니다.

    fill_values = {"East": 0.5, "West": -1}
    
    
    def fill_func(group):
        return group.fillna(fill_values[group.name])
    
    
    data.groupby(group_key).apply(fill_func)

    group.name이라는 속성을 활용해 East의 결측치에는 0.5, West의 결측치에는 -1을 할당하는 함수를 만들었습니다. 이 함수를 groupby 객체에 적용하면 그룹별로 원하는 값을 결측치에 채워넣을 수 있게 됩니다.


    공식 문서 : 링크 pandas.core.groupby.DataFrameGroupBy.fillna

  • 판다스 groupby 객체를 cut, qcut으로 분류하고 apply 적용하기

    지난 포스팅링크에서는 apply 메서드의 기본적인 활용법을 살펴봤습니다. 이번 포스팅에서는 판다스의 cutqcut을 사용해 데이터셋을 그루핑하고, apply 메서드를 적용해 통계치를 뽑는 부분까지 다뤄보겠습니다. 예전에 다른 블로그에서 다뤘던 내용링크이기도 합니다:)

    판다스에 cutqcut이라는 함수가 있습니다. 선택한 크기만큼, 혹은 표본 사분위수에 따라 데이터를 나눌 수 있게 해 주는 함수들인데요.

    이 함수들을 groupby와 조합하면, 데이터셋에 대한 사분위수 분석이나 버킷 분석을 쉽게 수행할 수 있습니다. 예를 하나 들어보겠습니다.

    frame = pd.DataFrame(
        {"data1": np.random.standard_normal(1000), "data2": np.random.standard_normal(1000)}
    )
    
    frame.head()

    data1 열과 data2 열에 정규분포를 따르는 1,000개의 난수가 들어가 있습니다. 이제 이 데이터셋을 cut을 사용해 등간격으로 나눠 보겠습니다.

    quartiles = pd.cut(frame["data1"], 4)
    quartiles.head(10)

    data1 컬럼을 4개의 구간으로 쪼갰습니다. 각각의 데이터값은 나타나지 않고, 어떤 구간에 속해 있는지만 표시됩니다.

    이렇게 cut에서 반환된 Categorical 객체는 바로 groupby로 넘길 수 있습니다. quartiles에 대한 그룹 통계는 아래와 같이 계산합니다.

    def get_stats(group):
        return pd.DataFrame(
            {
                "min": group.min(),
                "max": group.max(),
                "count": group.count(),
                "mean": group.mean(),
            }
        )
    
    
    grouped = frame.groupby(quartiles, observed=False)
    grouped.apply(get_stats)

    data1과 data2열 모두에 대해 구간별 통계량이 구해졌습니다. 등간격 버킷이므로 각 구간별 모수(count)는 모두 다릅니다.

    grouped를 계산할 때 observed=False 옵션을 넣는 것은 다른 포스팅에서도 다룬 적이 있는데요. 쉽게 말해 데이터가 있는 범주만 결과에 포함할 것인지 여부를 묻는 옵션입니다. (앞으로는 디폴트가 observed = True로 바뀔 거라고 하네요)

    등간격 버킷에 대한 통계량은 아래와 같이 좀 더 단순한 형태로 구할 수도 있습니다.

    grouped.agg(["min", "max", "count", "mean"])

    이 경우 결과는 계층적 열을 갖는 데이터프레임으로 반환됩니다.


    앞서 살펴본 등간격 버킷 말고 표준 사분위수에 기반해 크기가 동일한 버킷을 계산해 보겠습니다. 이 때는 판다스의 qcut을 씁니다.

    quartiles_samp = pd.qcut(frame["data1"], 4, labels=False)
    quartiles_samp.head()

    샘플을 사분위수로 나누기 위해 버킷 수를 4로 넘기고, labels=False를 전달해 각 사분위수 색인을 구했습니다. 앞서 cut을 수행했을 때는 데이터값 대신 어떤 구간에 속해있는지가 표시되었다면, 지금은 몇 번 버킷에 속해있는지가 표시됩니다.

    grouped = frame.groupby(quartiles_samp)
    grouped.apply(get_stats)

    framequartiles_samp를 기준으로 groupby한 다음,
    다시 apply를 사용해 get_stats 함수를 적용해 준 결과입니다.

    qcut을 사용해 크기가 동일한 버킷을 구했으므로, 각 구간의 데이터 수가 정확히 250개씩임을 확인할 수 있습니다.

  • 판다스 groupby 객체에 apply 메서드로 함수 적용하기

    오늘 포스팅에서는 apply 메서드를 사용해 groupby 객체에 함수를 적용하는 방법을 알아보겠습니다.

    apply 메서드는 groupby 메서드 중에서도 가장 일반적인 메서드입니다. 작동하는 방식은 다음과 같습니다.

    1. 먼저 객체를 여러 조각으로 나눕니다.
    2. 그리고 전달된 함수를 각 조각에 일괄적으로 적용합니다.
    3. 마지막으로 이를 다시 합칩니다.

    글로만 봐서는 이해가 잘 되지 않으니 예시를 통해 살펴보겠습니다. 지난 포스팅링크에 이어 오늘도 팁 데이터셋을 불러오겠습니다.

    데이터셋 불러오기

    tips

    이 데이터셋에서 그룹별로 상위 5개의 tip_pct 값을 골라내 봅시다.

    우선 특정 열에서 가장 큰 값을 갖는 상위 n개 행을 선택하는 함수부터 작성해 보겠습니다.

    함수 작성하기

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

    top 함수를 정의하고 tips 데이터셋에 적용한 결과입니다. tip_pct 컬럼을 기준으로 상위 5개에 해당하는 데이터가 출력되었습니다.

    이제 이 함수를 smoker 그룹에 대해 적용해 보겠습니다. 즉, smoker 그룹별로 tip_pct 상위 5개의 데이터를 추출하는 것입니다. 적용은 간단합니다.

    apply 메서드로 groupby 객체에 함수 적용하기

    tips.groupby("smoker").apply(top)

    tips 데이터셋을 smoker 컬럼 기준으로 groupby한 다음, apply 메서드를 써서 top 함수를 적용한 결과입니다.

    이 결과를 보면 제일 처음 설명한 apply 메서드의 작동 방식에 대해 이해할 수 있는데요.

    1. 먼저 tips 데이터프레임이 smoker 값에 따라 여러 그룹(조각)으로 나뉩니다.
    2. 그리고 나뉜 데이터프레임 조각에 top 함수가 일괄 적용되었습니다.
    3. 마지막으로 2번의 결과물이 pandas.concat으로 하나로 합쳐진 뒤 그룹 이름이 붙었습니다. 따라서, 결과물은 위에서 보듯 계층적 색인을 갖게 됩니다.

    이제 제일 처음 설명드린 apply 메서드의 작동 방식이 이해되셨을 겁니다.

    주의) apply 메서드의 include_groups

    그런데 위 결과물에서 빨간 박스 + 경고 문구가 떠 있는 게 보이시나요? 전문은 아래와 같이 되어 있습니다.

    /var/folders/lg/3qsc7sh14n70l8gc2tm282jr0000gn/T/ipykernel_60117/2530541573.py:1: DeprecationWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
    tips.groupby("smoker").apply(top)

    저 경고 문안이 뜨는 이유는 groupby 객체에 연산을 수행하는 동작이 미래의 판다스 버전에서 변경될 예정이기 때문인데요.

    위에서 groupby 객체에 top 함수를 apply할 때 아무 옵션도 주지 않았기 때문에, 결과물에도 그루핑의 기준인 smoker 열이 포함되어 있습니다. 하지만 미래의 판다스 버전에서는 기준 열이 자동으로 제외될 예정이므로, 사용자가 명시적으로 포함 여부를 선택해 주어야 한다는 내용입니다. 즉,

    tips.groupby("smoker").apply(top, include_groups=False)

    이렇게 include_groups = False 옵션을 써 주면 오류 메시지가 뜨지 않게 됩니다. 외우고 있을 필요는 없고, 그냥 ‘이런 게 있구나’ 정도로만 생각하고 넘어가겠습니다.

    apply 메서드에 추가 인수나 예약어 붙이기

    만약 apply 메서드에 넘길 함수가 추가적인 인수나 예약어를 받는다면 함수 이름 뒤에 붙여서 넘겨주면 됩니다.

    tips.groupby(["smoker", "day"]).apply(
        top, n=1, column="total_bill", include_groups=False
    )

    tips 데이터셋을 smokerday를 기준으로 그룹화한 뒤, total_bill을 기준으로 각 그룹별 1위 데이터만 뽑은 결과입니다. 이 때 헷갈리지 말아야 할 부분이 하나 있습니다.

    apply 메서드가 top에 n=1, columns = total_bill을 전달한다는 점입니다. apply로 함수를 실행하긴 하지만, 함수의 기본값이 아닌 apply가 전달한 값이 적용된다는 점에 주의해야 합니다.

    이번 포스팅에서 소개한 방법 외에도 apply 메서드를 활용할 수 있는 방법은 다양합니다. 다음 포스팅부터는 주로 groupby를 사용해 다양한 문제를 해결하는 방법을 알아보겠습니다.


    참고 : 공식문서 링크pandas.core.groupby.DataFrameGroupBy.apply

  • 판다스 groupby 객체의 색인 비활성화하기

    지난 포스팅링크에서는 groupby로 집계한 데이터의 열에 여러 가지 함수를 적용하는 방법에 대해 알아봤습니다.

    그런데 모든 그루핑의 결과물이 항상 유일한 키 조합을 갖고 있는 건 아니죠. 이럴 경우에는 어떻게 데이터를 처리할 수 있을까요?

    먼저 지난 포스팅에서 살펴본 팁 데이터셋을 다시 불러와 보겠습니다.

    이제 이 데이터셋을 day와 smoker 컬럼을 기준으로 그루핑해 보겠습니다.

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

    표대로 groupby를 수행한 결과물입니다. 아직 아무 연산도 안 된 상태이므로 DataFrameGroupBy라는 객체 정보만 뜨고 있습니다.

    만약 이 상태에서 곧장 컬럼별 평균을 구한다고 가정해 보겠습니다.

    grouped.mean(numeric_only=True)

    이렇게 계층적 색인이 적용된 채로 컬럼별 평균이 구해집니다. (수치형 컬럼에만 계산이 적용될 수 있도록 numeric_only를 설정해 주었습니다)

    위에서 언급한 대로 색인을 원치 않을 경우에는 두 가지 방법이 있습니다.

    집계를 수행하는 단계에서 색인 비활성화하기

    첫 번째 방법은 집계를 수행하는 시점에서 색인을 비활성화하는 것입니다. 즉,

    # 집계를 수행하는 시점에서 색인 없애기
    grouped_a = tips.groupby(["day", "smoker"], as_index=False)
    grouped_a.mean(numeric_only=True)

    이렇게 as_index = False를 써 줌으로써 색인을 비활성화할 수 있습니다. 결과는 어떻게 나올까요?

    계층적 색인이 적용되지 않고 모든 행이 숫자 인덱스로 분리되어 표시되는 것을 확인할 수 있습니다.

    집계가 수행된 후, 연산 단계에서 색인 비활성화하기

    두 번째 방법은 집계 자체는 (이 경우 계층적 색인이 적용된 채로) 그대로 수행하되, mean과 같은 연산을 수행하면서 색인을 초기화하는 방법입니다. 코드로는 아래와 같이 표현할 수 있습니다.

    # 집계는 동일하게 수행하되, 연산 단계에서 색인 초기화
    grouped_b = tips.groupby(["day", "smoker"])
    grouped_b.mean(numeric_only=True).reset_index()

    첫 번째 방법과의 차이가 느껴지시나요?
    grouped_b는 groupby를 수행한 뒤, mean 연산의 결과물에 대해 reset_index()를 적용해 색인을 초기화했습니다.

    결과는 첫 번째 방법과 동일합니다.

    다만, 첫 번째 방법처럼 집계를 하는 단계에서 as_index = False 옵션을 사용하면 불필요한 계산을 피할 수 있다는 장점이 있습니다. 즉, groupbyreset_index를 하면 두 번의 계산을 해야 하는데 비해 as_index를 쓰면 곧바로 결과가 반환되므로 조금 더 효율적일 수 있는 것이죠.

  • 판다스 groupby 객체의 열에 함수 적용하기

    이번 포스팅에서는 판다스에서 groupby 객체를 만들어 데이터를 집계할 때, 함수를 적용하는 다양한 방법을 살펴봅니다.

    데이터 불러오기 및 준비

    먼저 예전 Velog에 올렸던 포스팅에서 사용한 적이 있는
    팁(tips) 데이터셋을 다시 불러오고,
    tip_pct라는 컬럼까지 추가해 보겠습니다.

    tips = pd.read_csv("tips.csv")
    tips["tip_pct"] = tips["tip"] / (tips["total_bill"] - tips["tip"])
    tips.head()
    첫 번째 코드블록 실행 결과

    단일 열/여러 열에 집계함수 적용하기

    다음으로는 day와 smoker 열을 써서 tips를 묶어봅시다.

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

    그런 후에 특정 열만 들고 와서
    집계함수를 써서 평균을 구해보겠습니다.

    grouped_pct = grouped["tip_pct"]
    grouped_pct.agg("mean")

    결과는 위와 같이 ‘day와 smoker로 그루핑된 결과’에 대한 평균이 나오게 됩니다. 위 결과는 단일 열에 대해 계산한 결과이므로 Series로 반환되지만, 만약 함수 목록이나 함수 이름을 넘기면 함수 이름을 열 이름으로 하는 DataFrame이 반환됩니다. 즉,

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

    이 코드를 실행하면

    이와 같은 결과를 얻습니다.
    tip_pct의 평균과 표준편차가 각각 계산되었고, 결과물이 DataFrame으로 반환된 것이 보입니다. (peak_to_peak 컬럼은 최댓값과 최솟값의 차를 구하기 위해 임의로 만든 함수이고, 이 포스팅에서 만들었습니다.)


    집계함수 결과를 나타낼 열 이름 지정하기

    groupby 객체가 자동으로 지정한 함수 이름을 열 이름으로 쓰고 싶지 않을 경우는 어떻게 할까요? 이럴 때는 이름과 함수가 담긴 튜플의 리스트를 넘기는 것으로 해결할 수 있습니다. 이 경우, 각 튜플의 첫 번째 원소가 DataFrame의 열 이름으로 사용됩니다. 예를 들어 아래 코드를 실행하면,

    grouped_pct.agg([("average", "mean"), ("stdev", "std")])

    이런 결과를 얻게 됩니다.
    mean이 아니라 average로, std가 아니라 stdev로 열 이름이 설정되었습니다.


    여러 개의 함수를 모든 열에 적용하기

    DataFrame에서 열마다 다른 함수를 적용하거나,
    여러 개의 함수를 모든 열에 적용하는 것도 가능합니다.
    예컨대 tip_pcttotal_bill 컬럼 각각에 세 가지 통계치를 내야 하는 상황이라고 가정하겠습니다.

    functions = ["count", "mean", "max"]
    result = grouped[["tip_pct", "total_bill"]].agg(functions)
    
    result

    결과는 아래와 같습니다.

    tip_pcttotal_bill 컬럼 각각에 count와 mean, max 이렇게 세 개의 함수가 적용되었습니다. 이 결과는 계층적인 열을 가지며, 각 열을 따로 계산한 다음 concat 메서드를 사용해 이어붙인 것과 동일합니다. keys 인수로 열 이름을 넘겨야겠죠.

    위에서는 열 이름을 functions라는 리스트에 담아서 전달했지만,
    앞에서 살펴봤던 것처럼 열 이름이 담긴 튜플 리스트를 넘기는 것도 가능합니다. 이 경우는 아까와 마찬가지로 튜플의 첫 번째 원소가 열 이름이 됩니다. 즉,

    ftuples = [("Average", "mean"), ("Variance", "var")]
    grouped[["tip_pct", "total_bill"]].agg(ftuples)

    위 코드를 실행한 결과입니다.
    아까처럼 튜플의 첫 원소가 열 이름으로 잘 들어가 있네요:)


    열마다 서로 다른 함수 적용하기

    열마다 다른 함수를 적용하고 싶다면 agg 메서드에 딕셔너리를 넘깁니다.
    역시 말만 봐서는 이해가 어려우니 예시를 보겠습니다.

    grouped.agg({"tip": "max", "size": "sum"})

    tip 열에는 그룹별 최대값이, size 열에는 그룹의 합계가 적용되었습니다.
    지금 예에서는 tip과 size 컬럼에 각각 한 개씩의 서로 다른 함수가 적용되었는데요. 만약 한 쪽 컬럼에 여러 개의 함수가 적용되면 어떻게 될까요?

    grouped.agg({"tip_pct": ["min", "max", "mean", "std"], "size": "sum"})

    결과는 위와 같습니다. 단 하나의 열에라도 여러 개의 함수가 적용된다면 (위의 예에서는 tip_pct) 데이터프레임은 계층적인 열을 갖습니다.