본문 바로가기

개발/파이썬

Polars로 데이터 그룹화와 집계 📊 - 자주쓰는 명령어로 배우는 Polars #4

반응형

안녕하세요! '자주 쓰는 명령어로 배우는 Polars' 시리즈의 네 번째 글입니다. 지난 글에서는 데이터 필터링과 정렬에 대해 알아보았는데요. 오늘은 데이터 분석에서 매우 중요한 그룹화(Grouping)집계(Aggregation) 기능에 대해 알아보도록 하겠습니다.

데이터를 그룹화하고 집계하는 과정을 통해 의미있는 인사이트를 얻을 수 있습니다. 예를들어, '지역별 매출 합계', '연령대별 고객 수' 등의 정보를 파악할 수 있습니다. Polars는 이러한 작업을 매우 효율적이고 직관적으로 수행할 수 있도록 다양한 기능을 제공하고 있습니다.

오늘도 포켓몬 데이터 예시를 통해 그룹화와 집계에 대해 자세히 살펴보도록 하겠습니다. 😊

💡 여기서 사용하는 Polars 버전은 1.5입니다. polars 버전이 1 미만이면 예시 코드가 제대로 동작하지 않을 수 있습니다.

먼저 필요한 라이브러리를 임포트하고 예제 데이터를 불러와 보겠습니다. (아직 설치를 안 하신 분들이 있다면 지난 글을 참고해서 먼저 설치해 주세요 🛠️)

예제 데이터는 아래 파일을 다운받아주세요.

Pokemon.csv
0.04MB

import polars as pl

# 데이터 불러오기
df = pl.read_csv("Pokemon.csv")

1. 기본 그룹화 작업 🎯

단일 컬럼 그룹화

가장 기본적인 그룹화는 group_by 메서드를 사용합니다. 하나의 컬럼을 기준으로 데이터를 그룹화하고, 각 그룹에 대해 집계 함수를 적용할 수 있습니다.

# 타입별 포켓몬 수 계산
type_counts = df.group_by("Type 1").agg(
    pl.len().alias("count")
)

print("타입별 포켓몬 수:")
print(type_counts.sort("count", descending=True))

실행결과

타입별 포켓몬 수

다중 컬럼 그룹화

하나의 기준으로는 부족할 때 여러 기준으로 한 번에 그룹화 할 수 있어요. 이는 더 세분화된 분석이 필요할 때 유용합니다.
예를들어, '지역별 + 상품별 매출분석', '연령대별 + 성별 고객 통계' 등에 활용할 수 있어요.

# 세대와 타입별 평균 능력치 계산
gen_type_stats = df.group_by(["Generation", "Type 1"]).agg([
    pl.col("HP").mean().alias("avg_HP"),
    pl.col("Attack").mean().alias("avg_Attack"),
])

print("\n세대와 타입별 평균 능력치:")
print(gen_type_stats.head())

실행결과

세대와 타입별 평균 능력치

다만 그룹이 기하급수적으로 늘어나거나 데이터가 너무 잘게 쪼개질 수 있으니 주의가 필요합니다!

2. 다양한 집계 함수 활용하기 📊

기본 집계 함수

Polars는 다양한 내장 집계 함수를 제공합니다. 이러한 함수들은 그룹화된 데이터에 대해 통계적 계산을 수행합니다.

type_stats = df.group_by("Type 1").agg([
    pl.len().alias("count"),
    pl.col("HP").mean().alias("avg_HP"),
    pl.col("Attack").sum().alias("total_Attack"),
    pl.col("Defense").std().alias("std_Defense"),
    pl.col("Speed").max().alias("max_Speed"),
])

print("타입별 상세 통계:")
print(type_stats.head())

실행결과

타입별 상세 통계

이 외에도 polars는 아래와 같은 집계함수들을 제공합니다.

수치형 데이터용:

  • mean(): 평균값 (가장 기본적인 통계)
  • sum(): 합계 (전체 규모 파악할 때)
  • std(): 표준편차 (데이터 분포 확인)
  • var(): 분산
  • min(), max(): 최소/최대값
  • median(): 중앙값 (이상치에 강함)
  • quantile(): 분위수

카운트 계열:

  • count(): 개수 세기
  • n_unique(): 고유값 개수
  • first(), last(): 첫/마지막 값
  • value_counts(): 값별 빈도수

조건부 집계

단순 집계를 넘어서 조건을 걸어서 더 세밀한 분석이 가능합니다. '특정 조건 만족하는 케이스만 분석할 때', 'A/B 테스트 결과 분석', '이상치 비율 계산' 등을 위해 조건부 집계를 활용할 수 있어요.

# 타입별로 HP가 100 이상인 포켓몬의 수와 비율 계산
type_hp_stats = df.group_by("Type 1").agg([
    pl.col("HP").count().alias("total"),
    (pl.col("HP") >= 100).sum().alias("high_hp_count"),
    ((pl.col("HP") >= 100).sum() / pl.col("HP").count() * 100).alias("high_hp_ratio")
])

print("타입별 고능력치 포켓몬 비율:")
print(type_hp_stats.sort("high_hp_ratio", descending=True))

실행결과

타입별 고능력치 포켓몬 비율

3. 윈도우 함수와 고급 집계 🚀

윈도우 함수 활용하기

윈도우 함수는 그룹 내에서 행별로 계산을 수행하는 강력한 기능입니다. '매출 순위 매기기', '전월 대비 증감률'등을 계산하는데 활용할 수 있어요.

# 타입별 순위 매기기
ranked_pokemon = df.with_columns([
    pl.col("Attack").rank()
    .over("Type 1")
    .alias("attack_rank_in_type")
])

print("\n타입 내 공격력 순위:")
print(ranked_pokemon.select(["Name", "Type 1", "Attack", "attack_rank_in_type"]).head())

실행결과

타입 내 공격력 순위

복수 컬럼 기반 집계

여러 컬럼을 동시에 집계할 수 있습니다:

# 타입별 전체 능력치 통계
type_stats_all = df.group_by("Type 1").agg(
    [
        pl.col(["HP", "Attack", "Defense", "Speed"]).mean().name.suffix("_avg"),
        pl.col(["HP", "Attack", "Defense", "Speed"]).std().name.suffix("_std"),
    ]
)

print("\n타입별 종합 능력치 통계:")
print(type_stats_all.head())

실행결과

타입별 종합 능력치 통계

4. Polars 그룹화/집계 팁 💪

마지막으로 큰 데이터셋을 다룰 때 활용할 수 있는 팁을 알려드리겠습니다.

  1. 성능 최적화
    • 그룹화 전에 필요한 컬럼만 선택하면 메모리 사용량과 처리 속도를 개선할 수 있습니다.
    • 큰 데이터셋의 경우 lazy 평가를 활용하면 좋습니다.
# 필요한 컬럼만 선택하여 그룹화
efficient_group_by = (
    df
    .select(["Type 1", "HP", "Attack"])  # 필요한 컬럼만 선택
    .group_by("Type 1")
    .agg([
        pl.col("HP").mean(),
        pl.col("Attack").mean()
    ])
)
print(efficient_group_by)

실행결과

필요한 컬럼만 선택하여 그룹화

# Lazy 평가 활용
lazy_group_by = (
   df.lazy()
   .group_by("Type 1")
   .agg([
       pl.col("HP").mean(),
       pl.col("Attack").mean()
   ])
   .collect()
)
print(lazy_group_by)
  1. 가독성 향상
    • 복잡한 집계 로직은 별도의 함수로 분리하면 코드 관리가 쉬워집니다.
    • 결과 컬럼명에 alias를 적절히 사용하면 결과를 이해하기 쉬워집니다.
    • 체이닝 방식으로 코드를 작성하면 데이터 처리 흐름을 파악하기 쉽습니다.
  2. 순서가 중요한 경우 🚨
    • 기본적으로 group_by는 결과의 순서를 보장하지 않습니다.
    • 원본 데이터의 순서를 유지하고 싶다면 maintain_order=True 옵션을 써야 합니다.
    • 단, 이 옵션을 쓰면 성능이 조금 떨어질 수 있어요
# 기본 group_by (순서 보장 안됨)
basic_group = df.group_by("Type 1").agg([
    pl.col("HP").mean().alias("avg_HP")
])
print("기본 그룹화:")
print(basic_group.head())

# 순서 유지하면서 group_by
ordered_group = df.group_by("Type 1", maintain_order=True).agg([
    pl.col("HP").mean().alias("avg_HP")
])
print("\n순서 유지한 그룹화:")
print(ordered_group.head())

실행결과

기본 그룹화 / 순서를 유지한 그룹화


이번 글에서는 Polars의 그룹화와 집계 기능에 대해 자세히 알아보았습니다. 다음 글에서는 데이터 병합과 재구조화에 대해 알아보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다! 🙇‍♂️

📚 참고자료

반응형