이미지 로딩 중...

Polars 집계 함수 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 14. · 3 Views

Polars 집계 함수 완벽 가이드

데이터 분석의 필수 도구인 Polars의 기본 집계 함수들을 실무 중심으로 배워봅니다. sum, mean, count부터 고급 집계까지, 초급자도 바로 활용할 수 있는 실전 예제와 함께 제공됩니다.


목차

  1. sum - 합계 계산의 기본
  2. mean - 평균으로 중심 경향 파악하기
  3. count - 데이터 개수 세기
  4. max와 min - 최댓값과 최솟값 찾기
  5. median - 중앙값으로 데이터 중심 파악하기
  6. std와 var - 표준편차와 분산으로 산포도 측정하기
  7. group_by와 함께 사용하기 - 그룹별 집계
  8. first와 last - 첫 번째와 마지막 값 가져오기
  9. n_unique - 고유값 개수 세기
  10. quantile - 백분위수로 분포 파악하기

1. sum - 합계 계산의 기본

시작하며

여러분이 매출 데이터를 분석할 때 "이번 달 총 매출이 얼마지?"라는 질문을 받아본 적 있나요? 수백, 수천 개의 거래 데이터를 손으로 더할 수는 없겠죠.

이런 문제는 실제 데이터 분석 현장에서 가장 기본적이면서도 자주 발생하는 작업입니다. 데이터가 많아질수록 수동으로 계산하는 것은 불가능하며, 실수의 가능성도 높아집니다.

바로 이럴 때 필요한 것이 sum() 집계 함수입니다. Polars의 sum()을 사용하면 대용량 데이터도 빠르게 합계를 계산할 수 있습니다.

개요

간단히 말해서, sum()은 컬럼의 모든 값을 더해서 하나의 숫자로 만들어주는 함수입니다. 데이터 분석에서 총합을 구하는 것은 가장 기본적인 통계 연산입니다.

예를 들어, 전체 매출 합계, 전체 방문자 수, 총 재고량 같은 경우에 매우 유용합니다. 이러한 집계 결과는 비즈니스 의사결정의 기초 자료가 됩니다.

전통적인 방법으로는 for 루프를 돌면서 하나씩 더했다면, 이제는 한 줄의 코드로 즉시 결과를 얻을 수 있습니다. sum()의 핵심 특징은 첫째, 빠른 연산 속도(Rust 기반), 둘째, null 값 자동 처리, 셋째, 다양한 숫자 타입 지원입니다.

이러한 특징들이 실무에서 안정적이고 효율적인 데이터 처리를 가능하게 합니다.

코드 예제

import polars as pl

# 매출 데이터 생성
df = pl.DataFrame({
    "product": ["A", "B", "A", "C", "B"],
    "sales": [100, 200, 150, 300, 250]
})

# 전체 매출 합계 계산
total_sales = df.select(pl.col("sales").sum())
print(f"총 매출: {total_sales}")

# 결과: 총 매출: 1000

설명

이것이 하는 일: sum() 함수는 지정된 컬럼의 모든 행 값을 읽어서 하나의 합계 값으로 반환합니다. 첫 번째로, pl.col("sales")로 sales 컬럼을 선택합니다.

이는 어떤 컬럼의 데이터를 집계할지 명시하는 과정입니다. Polars는 컬럼 기반 처리를 하기 때문에 이렇게 명확히 지정해야 합니다.

그 다음으로, .sum() 메서드가 실행되면서 선택된 컬럼의 모든 값들이 내부적으로 순회됩니다. Rust로 구현된 엔진이 메모리 효율적으로 연산을 수행하며, null 값이 있다면 자동으로 건너뜁니다.

select() 함수로 감싸면 결과가 DataFrame 형태로 반환되고, 만약 단일 값만 필요하다면 .item()을 추가로 사용할 수 있습니다. 마지막으로 계산된 총 매출 1000이 출력됩니다.

여러분이 이 코드를 사용하면 수천, 수만 건의 거래 데이터도 밀리초 단위로 합계를 계산할 수 있습니다. 또한 타입 안정성이 보장되며, 메모리 효율도 뛰어나고, 체이닝을 통해 다른 연산과 결합할 수 있습니다.

실전 팁

💡 null 값이 포함된 컬럼도 자동으로 처리되므로, 별도로 필터링할 필요가 없습니다. null은 0으로 취급되지 않고 완전히 무시됩니다.

💡 여러 컬럼의 합계를 동시에 구하려면 select([pl.col("sales").sum(), pl.col("cost").sum()])처럼 리스트로 전달하세요.

💡 단일 숫자 값이 필요하다면 .select(pl.col("sales").sum()).item()을 사용하여 DataFrame이 아닌 스칼라 값을 얻을 수 있습니다.

💡 정수 오버플로우를 방지하려면 .cast(pl.Int64) 또는 .cast(pl.Float64)로 미리 타입을 변환하세요.

💡 조건부 합계가 필요하다면 filter()와 조합하거나, pl.when().then().otherwise() 표현식을 활용하세요.


2. mean - 평균으로 중심 경향 파악하기

시작하며

여러분이 학생들의 시험 점수를 분석하면서 "평균 점수가 얼마나 되지?"라고 궁금해한 적 있나요? 또는 웹사이트의 평균 응답 시간을 모니터링해야 하는 상황이라면요?

이런 질문은 데이터의 중심 경향을 이해하기 위한 가장 기본적인 분석입니다. 평균을 모르면 데이터가 전반적으로 높은지 낮은지, 개선되고 있는지 악화되고 있는지 판단할 수 없습니다.

바로 이럴 때 필요한 것이 mean() 함수입니다. 산술 평균을 빠르게 계산하여 데이터의 대표값을 알려줍니다.

개요

간단히 말해서, mean()은 컬럼의 모든 값을 더한 후 개수로 나누어 평균을 계산하는 함수입니다. 평균은 통계학에서 가장 널리 사용되는 대푯값입니다.

예를 들어, 고객의 평균 구매 금액, 서버의 평균 응답 시간, 직원들의 평균 근무 시간 같은 경우에 핵심 지표로 활용됩니다. 이 값을 기준으로 이상치를 탐지하거나 성과를 평가할 수 있습니다.

전통적인 방법으로는 sum()을 구한 후 count()로 나눴다면, 이제는 mean() 한 번으로 즉시 결과를 얻습니다. mean()의 핵심 특징은 첫째, 자동 null 제외 처리, 둘째, 부동소수점 결과 반환, 셋째, 다른 집계 함수와의 쉬운 조합입니다.

이러한 특징들이 정확하고 신뢰할 수 있는 통계 분석을 가능하게 합니다.

코드 예제

import polars as pl

# 학생 점수 데이터
df = pl.DataFrame({
    "student": ["Alice", "Bob", "Charlie", "David", "Eve"],
    "score": [85, 92, 78, 95, 88]
})

# 평균 점수 계산
avg_score = df.select(pl.col("score").mean())
print(f"평균 점수: {avg_score}")

# 결과: 평균 점수: 87.6

설명

이것이 하는 일: mean() 함수는 컬럼의 모든 값을 합산한 후, null이 아닌 값의 개수로 나누어 평균을 계산합니다. 첫 번째로, pl.col("score")로 점수 컬럼을 선택합니다.

이는 어떤 데이터의 평균을 구할지 명확히 지정하는 단계입니다. 여러 컬럼이 있을 때 정확한 타겟을 지정하는 것이 중요합니다.

그 다음으로, .mean() 메서드가 내부적으로 두 가지 연산을 수행합니다. 먼저 모든 값을 더하고(85+92+78+95+88=438), 그 다음 유효한 값의 개수로 나눕니다(438/5=87.6).

이 과정에서 null 값은 자동으로 제외됩니다. Polars는 이 연산을 단일 패스로 효율적으로 처리하며, 결과는 항상 부동소수점 타입으로 반환됩니다.

정수 입력이어도 평균은 소수점을 가질 수 있기 때문입니다. 여러분이 이 코드를 사용하면 데이터의 중심 경향을 즉시 파악할 수 있습니다.

평균을 기준으로 개별 값들이 얼마나 높거나 낮은지 비교할 수 있고, 시계열 데이터라면 트렌드 변화를 추적할 수 있으며, A/B 테스트에서 두 그룹의 성과를 비교하는 기준으로 활용할 수 있습니다.

실전 팁

💡 이상치(outlier)가 있으면 평균이 왜곡될 수 있습니다. 이럴 때는 median()(중앙값)을 함께 확인하세요.

💡 null 값은 자동으로 제외되므로, 만약 null을 0으로 취급하고 싶다면 먼저 .fill_null(0)을 사용하세요.

💡 소수점 자릿수를 조절하려면 .round(2)를 체이닝하여 원하는 정밀도로 반올림할 수 있습니다.

💡 가중 평균이 필요하다면 (pl.col("value") * pl.col("weight")).sum() / pl.col("weight").sum() 형태로 직접 계산하세요.

💡 그룹별 평균을 구할 때는 group_by()와 함께 사용하면 카테고리별 비교가 가능합니다.


3. count - 데이터 개수 세기

시작하며

여러분이 고객 데이터베이스를 관리하면서 "활성 사용자가 몇 명이지?"라는 질문에 답해야 할 때가 있나요? 또는 특정 조건을 만족하는 레코드가 얼마나 되는지 빠르게 확인해야 하는 상황이라면요?

이런 문제는 데이터 분석의 가장 기초적인 작업이지만, 데이터가 커질수록 수동으로 세는 것은 불가능합니다. 또한 null 값이나 중복 값을 어떻게 처리할지도 고려해야 합니다.

바로 이럴 때 필요한 것이 count() 함수입니다. 조건에 맞는 레코드를 빠르고 정확하게 세어줍니다.

개요

간단히 말해서, count()는 컬럼의 null이 아닌 값의 개수를 세는 함수입니다. 데이터 개수를 세는 것은 데이터 품질 확인, 샘플 크기 파악, 비율 계산의 기초가 됩니다.

예를 들어, 전체 고객 수, 결측치가 아닌 응답 수, 중복 제거 후 유니크한 레코드 수 같은 경우에 필수적입니다. 이는 통계적 신뢰도를 판단하는 데도 중요합니다.

전통적인 방법으로는 DataFrame의 len()을 사용하거나 반복문으로 셌다면, 이제는 count()로 컬럼별로 정확하게 셀 수 있습니다. count()의 핵심 특징은 첫째, null 값 자동 제외, 둘째, 매우 빠른 연산 속도, 셋째, 조건부 카운팅과의 쉬운 결합입니다.

이러한 특징들이 정확한 데이터 규모 파악을 가능하게 합니다.

코드 예제

import polars as pl

# 고객 데이터 (일부 null 포함)
df = pl.DataFrame({
    "customer_id": [1, 2, 3, 4, 5],
    "email": ["a@test.com", None, "c@test.com", "d@test.com", None]
})

# 이메일이 등록된 고객 수
valid_emails = df.select(pl.col("email").count())
print(f"이메일 등록 고객: {valid_emails}")

# 결과: 이메일 등록 고객: 3

설명

이것이 하는 일: count() 함수는 지정된 컬럼을 순회하면서 null이 아닌 유효한 값의 개수를 세어 정수로 반환합니다. 첫 번째로, pl.col("email")로 이메일 컬럼을 선택합니다.

이 컬럼에는 총 5개의 행이 있지만, 그중 2개는 null 값입니다. 어떤 컬럼의 유효 데이터 개수를 확인할지 명시하는 것이 중요합니다.

그 다음으로, .count() 메서드가 실행되면서 각 행을 검사합니다. "a@test.com"(유효), None(제외), "c@test.com"(유효), "d@test.com"(유효), None(제외) 순서로 확인하며, null이 아닌 값만 카운트에 포함시킵니다.

내부적으로 Polars는 비트마스크를 사용하여 null 위치를 빠르게 식별하므로, 대용량 데이터에서도 매우 효율적입니다. 최종적으로 3이라는 결과가 반환되며, 이는 유효한 이메일이 3개 등록되어 있음을 의미합니다.

여러분이 이 코드를 사용하면 데이터 품질을 즉시 파악할 수 있습니다. 전체 행 수와 비교하여 결측률을 계산할 수 있고, 데이터 정제 전후를 비교할 수 있으며, 조건별로 필터링한 후 개수를 세어 분포를 분석할 수 있습니다.

실전 팁

💡 전체 행 수를 세려면 df.height 또는 pl.count()를 사용하세요. 특정 컬럼의 count()는 null을 제외합니다.

💡 조건을 만족하는 행의 개수를 세려면 filter()와 조합하거나, (pl.col("age") > 30).sum()처럼 불리언 합계를 사용하세요.

💡 유니크한 값의 개수를 세려면 count() 대신 n_unique()를 사용하세요.

💡 그룹별 개수를 세려면 group_by().agg(pl.count())를 사용하면 카테고리별 분포를 한 번에 파악할 수 있습니다.

💡 결측치 개수를 확인하려면 pl.col("column").is_null().sum()을 사용하여 null의 개수를 직접 세세요.


4. max와 min - 최댓값과 최솟값 찾기

시작하며

여러분이 재고 관리 시스템에서 "가장 비싼 제품이 뭐지?" 또는 "재고가 가장 적은 품목은?" 같은 질문에 답해야 할 때가 있나요? 데이터 품질 검증 과정에서 이상하게 큰 값이나 작은 값을 찾아야 하는 경우도 있죠.

이런 작업은 데이터의 범위를 파악하고 이상치를 탐지하는 데 필수적입니다. 최댓값과 최솟값을 모르면 데이터의 분포를 제대로 이해할 수 없고, 스케일링이나 정규화 같은 전처리도 불가능합니다.

바로 이럴 때 필요한 것이 max()와 min() 함수입니다. 데이터의 양 극단을 즉시 찾아줍니다.

개요

간단히 말해서, max()는 컬럼의 최댓값을, min()은 최솟값을 반환하는 함수입니다. 최댓값과 최솟값은 데이터의 범위(range)를 정의하는 핵심 통계량입니다.

예를 들어, 최고 매출액과 최저 매출액, 가장 빠른 응답 시간과 가장 느린 응답 시간, 최고 온도와 최저 온도 같은 경우에 중요한 의미를 갖습니다. 이는 성능 모니터링, 품질 관리, 리스크 평가에 활용됩니다.

전통적인 방법으로는 정렬 후 첫 번째와 마지막 값을 가져왔다면, 이제는 한 번의 함수 호출로 즉시 결과를 얻습니다. max()와 min()의 핵심 특징은 첫째, null 값 자동 무시, 둘째, 숫자뿐 아니라 날짜/문자열도 지원, 셋째, 단일 패스로 효율적인 연산입니다.

이러한 특징들이 다양한 데이터 타입에서 범위 분석을 가능하게 합니다.

코드 예제

import polars as pl

# 제품 가격 데이터
df = pl.DataFrame({
    "product": ["노트북", "마우스", "키보드", "모니터", "헤드셋"],
    "price": [1200000, 25000, 89000, 350000, 120000]
})

# 최고가와 최저가 찾기
max_price = df.select(pl.col("price").max())
min_price = df.select(pl.col("price").min())

print(f"최고가: {max_price}, 최저가: {min_price}")
# 결과: 최고가: 1200000, 최저가: 25000

설명

이것이 하는 일: max()와 min() 함수는 컬럼의 모든 값을 비교하여 가장 큰 값과 가장 작은 값을 각각 반환합니다. 첫 번째로, pl.col("price")로 가격 컬럼을 선택합니다.

이 컬럼에는 5개의 제품 가격이 들어있으며, 우리는 이 중에서 극값을 찾으려고 합니다. 숫자형 데이터에서 비교 연산이 명확하게 정의되어 있습니다.

그 다음으로, .max()는 내부적으로 모든 값을 순회하면서 현재까지의 최댓값을 갱신해나갑니다. 1200000, 25000, 89000...

순서로 비교하면서 1200000이 가장 크다는 것을 확인합니다. 마찬가지로 .min()은 25000이 가장 작다는 것을 찾습니다.

Polars는 SIMD(Single Instruction Multiple Data) 명령어를 활용하여 이 비교 연산을 병렬로 처리하므로, 수백만 행의 데이터에서도 빠르게 결과를 얻을 수 있습니다. null 값이 있다면 자동으로 무시되며, 모든 값이 null이면 null을 반환합니다.

여러분이 이 코드를 사용하면 가격 범위를 즉시 파악하여 제품 라인업의 스펙트럼을 이해할 수 있습니다. 또한 이상치 탐지(예: 가격이 음수이거나 너무 큰 경우), 데이터 검증(예: 허용 범위 내인지 확인), 정규화 준비(예: min-max 스케일링을 위한 범위 계산)에 활용할 수 있습니다.

실전 팁

💡 날짜 컬럼에도 사용 가능합니다. 가장 최근 날짜(max)와 가장 오래된 날짜(min)를 찾을 수 있어 데이터 기간을 파악하는 데 유용합니다.

💡 문자열 컬럼에서는 알파벳 순서로 비교됩니다. min()은 사전 순으로 가장 앞선 값을, max()는 가장 뒤의 값을 반환합니다.

💡 최댓값과 최솟값을 동시에 구하려면 select([pl.col("price").max(), pl.col("price").min()])처럼 한 번에 요청하여 효율성을 높이세요.

💡 최댓값/최솟값을 가진 행 전체를 찾으려면 filter(pl.col("price") == pl.col("price").max())를 사용하세요.

💡 그룹별 최댓값/최솟값을 찾으려면 group_by()와 함께 사용하여 카테고리별 극값을 비교할 수 있습니다.


5. median - 중앙값으로 데이터 중심 파악하기

시작하며

여러분이 부동산 시장을 분석하면서 "평균 집값"을 계산했는데, 몇 채의 초고가 주택 때문에 평균이 왜곡되어 실제 시장 상황과 맞지 않는 경험을 한 적 있나요? 소득 분포나 응답 시간 같이 극단값의 영향을 받기 쉬운 데이터를 다룰 때도 비슷한 문제가 발생합니다.

이런 문제는 평균이 이상치에 민감하기 때문에 발생합니다. 몇 개의 극단적인 값이 전체 평균을 크게 끌어올리거나 내릴 수 있어, 대부분의 데이터가 실제로 어디에 위치하는지 파악하기 어렵습니다.

바로 이럴 때 필요한 것이 median() 함수입니다. 중앙값은 이상치의 영향을 받지 않아 더 견고한 대푯값을 제공합니다.

개요

간단히 말해서, median()은 데이터를 정렬했을 때 정중앙에 위치하는 값을 반환하는 함수입니다. 중앙값은 이상치에 강건한(robust) 통계량으로, 분포가 치우친 데이터에서 특히 유용합니다.

예를 들어, 소득 분포(상위 1%가 평균을 왜곡), 주택 가격(초고가 주택의 영향), 웹 응답 시간(가끔 발생하는 타임아웃) 같은 경우에 평균보다 더 현실적인 대푯값을 제공합니다. 실무에서는 평균과 중앙값을 함께 보면서 분포의 왜도를 파악합니다.

전통적인 방법으로는 데이터를 정렬한 후 중간 인덱스를 찾았다면, 이제는 median() 한 번으로 즉시 결과를 얻습니다. median()의 핵심 특징은 첫째, 이상치에 강건함, 둘째, 데이터의 50번째 백분위수(percentile), 셋째, 홀수 개일 때는 중간값, 짝수 개일 때는 중간 두 값의 평균입니다.

이러한 특징들이 왜곡되지 않은 중심 경향을 파악하게 해줍니다.

코드 예제

import polars as pl

# 주택 가격 데이터 (이상치 포함)
df = pl.DataFrame({
    "address": ["A", "B", "C", "D", "E"],
    "price": [300, 320, 310, 330, 5000]  # 5000은 이상치
})

# 평균과 중앙값 비교
avg_price = df.select(pl.col("price").mean()).item()
med_price = df.select(pl.col("price").median()).item()

print(f"평균: {avg_price}만원, 중앙값: {med_price}만원")
# 결과: 평균: 1252만원, 중앙값: 320만원

설명

이것이 하는 일: median() 함수는 데이터를 크기 순으로 정렬한 후, 정확히 중간에 위치하는 값을 찾아 반환합니다. 첫 번째로, pl.col("price")로 가격 컬럼을 선택합니다.

이 예제에서는 5개의 값이 있으며, 그중 5000은 다른 값들에 비해 현저히 큰 이상치입니다. 이런 데이터에서 평균은 왜곡되지만 중앙값은 안정적입니다.

그 다음으로, .median()이 내부적으로 데이터를 정렬합니다: 300, 310, 320, 330, 5000. 총 5개이므로 홀수 개이고, 따라서 3번째 값인 320이 중앙값이 됩니다.

만약 짝수 개였다면 중간 두 값의 평균을 계산했을 것입니다. 평균은 (300+320+310+330+5000)/5 = 1252로 계산되어 실제 대부분의 가격대(300-330)를 제대로 반영하지 못합니다.

반면 중앙값 320은 전형적인 가격대를 정확히 나타냅니다. 이것이 중앙값이 이상치에 강건한 이유입니다.

여러분이 이 코드를 사용하면 데이터의 실제 중심을 파악할 수 있습니다. 평균과 중앙값의 차이로 분포의 치우침 정도를 판단할 수 있고(차이가 크면 왜도가 큼), 보고서에서 "중간 수준"을 표현할 때 더 정확한 값을 제시할 수 있으며, 이상치 탐지의 기준점으로도 활용할 수 있습니다.

실전 팁

💡 평균과 중앙값을 함께 계산하여 차이를 확인하세요. 큰 차이는 이상치나 치우친 분포의 신호입니다.

💡 백분위수가 필요하다면 quantile(0.25), quantile(0.75) 등을 사용하여 4분위수(quartile)를 구할 수 있습니다.

💡 중앙값은 평균보다 계산 비용이 높습니다(정렬 필요). 매우 큰 데이터에서는 성능을 고려하세요.

💡 그룹별 중앙값을 구하면 카테고리 간 비교에서 이상치의 영향 없이 공정한 비교가 가능합니다.

💡 시계열 데이터의 이동 중앙값(rolling median)은 이상치에 강건한 트렌드 분석에 유용합니다.


6. std와 var - 표준편차와 분산으로 산포도 측정하기

시작하며

여러분이 두 제품의 평균 만족도가 모두 4.0점이라는 결과를 받았을 때, "그럼 두 제품이 똑같이 좋다는 건가?"라고 생각한 적 있나요? 하지만 한 제품은 모든 고객이 3.5~4.5점을 준 반면, 다른 제품은 1점과 5점이 섞여 있다면 상황이 완전히 다르죠.

이런 문제는 평균만으로는 데이터의 퍼짐 정도를 알 수 없기 때문에 발생합니다. 데이터가 평균 주변에 모여있는지, 아니면 넓게 흩어져 있는지를 모르면 제대로 된 판단을 할 수 없습니다.

바로 이럴 때 필요한 것이 std(표준편차)와 var(분산) 함수입니다. 데이터의 변동성과 일관성을 정량화해줍니다.

개요

간단히 말해서, var()는 각 값이 평균에서 얼마나 떨어져 있는지의 제곱 평균이고, std()는 var의 제곱근으로 원래 단위로 표현한 산포도입니다. 표준편차와 분산은 데이터의 변동성을 측정하는 핵심 통계량입니다.

예를 들어, 제품 품질의 일관성(표준편차가 작을수록 균일), 주가의 변동성(리스크 지표), 서버 응답 시간의 안정성(예측 가능성) 같은 경우에 필수적입니다. 낮은 표준편차는 예측 가능하고 안정적임을, 높은 표준편차는 변동이 크고 불확실함을 의미합니다.

전통적인 방법으로는 각 값에서 평균을 빼고 제곱하고 평균 내는 복잡한 과정을 거쳤다면, 이제는 한 번의 함수 호출로 즉시 결과를 얻습니다. std()와 var()의 핵심 특징은 첫째, 모든 값의 편차를 고려하여 종합적 산포도 제공, 둘째, 제곱 연산으로 큰 편차에 더 큰 가중치 부여, 셋째, 정규분포 가정 하의 신뢰구간 계산 가능입니다.

이러한 특징들이 통계적으로 의미 있는 변동성 분석을 가능하게 합니다.

코드 예제

import polars as pl

# 두 제품의 만족도 점수
df = pl.DataFrame({
    "product_A": [4.0, 4.1, 3.9, 4.0, 4.2],
    "product_B": [1.0, 5.0, 4.0, 5.0, 3.0]
})

# 표준편차 비교
std_a = df.select(pl.col("product_A").std()).item()
std_b = df.select(pl.col("product_B").std()).item()

print(f"제품A 표준편차: {std_a:.2f}, 제품B: {std_b:.2f}")
# 결과: 제품A 표준편차: 0.11, 제품B: 1.67

설명

이것이 하는 일: std()와 var() 함수는 각 데이터 포인트가 평균으로부터 얼마나 떨어져 있는지를 종합하여 전체적인 산포도를 계산합니다. 첫 번째로, 내부적으로 먼저 평균을 계산합니다.

product_A의 평균은 4.04이고, product_B의 평균은 3.6입니다. 이 평균값이 기준점이 되어 각 값의 편차를 측정하게 됩니다.

그 다음으로, 각 값에서 평균을 뺀 편차를 계산한 후 제곱합니다. 예를 들어 product_B의 경우: (1.0-3.6)²=6.76, (5.0-3.6)²=1.96...

제곱하는 이유는 양수/음수 편차를 모두 양수로 만들고, 큰 편차에 더 큰 가중치를 부여하기 위함입니다. 이 제곱 편차들의 평균이 분산(variance)이고, 분산의 제곱근이 표준편차(standard deviation)입니다.

표준편차는 원래 데이터와 같은 단위를 가지므로 해석하기 쉽습니다. product_A는 0.11로 매우 일관적이고, product_B는 1.67로 변동이 큽니다.

여러분이 이 코드를 사용하면 데이터의 안정성과 예측 가능성을 평가할 수 있습니다. 품질 관리에서 허용 오차 범위를 설정할 수 있고(평균±2*표준편차), 투자 리스크를 정량화할 수 있으며(변동성이 큰 자산 식별), A/B 테스트에서 결과의 신뢰성을 판단할 수 있습니다(표준편차가 작을수록 일관된 효과).

실전 팁

💡 Polars의 std()와 var()는 기본적으로 표본 통계량(n-1로 나눔)을 계산합니다. 모집단이 필요하면 ddof=0 파라미터를 사용하세요.

💡 변동계수(CV)는 std/mean으로, 서로 다른 스케일의 데이터 변동성을 비교할 때 유용합니다.

💡 이상치가 있으면 표준편차가 왜곡됩니다. 이럴 때는 MAD(Median Absolute Deviation)나 IQR(Interquartile Range)를 대안으로 고려하세요.

💡 68-95-99.7 규칙: 정규분포에서 평균±1std에 68%, ±2std에 95%, ±3*std에 99.7%의 데이터가 포함됩니다.

💡 그룹별 표준편차를 비교하면 어느 그룹이 더 일관적인지, 어느 그룹이 더 변동성이 큰지 파악할 수 있습니다.


7. group_by와 함께 사용하기 - 그룹별 집계

시작하며

여러분이 전국 매장의 매출 데이터를 분석하면서 "지역별로 평균 매출이 얼마인지" 알고 싶을 때가 있나요? 또는 제품 카테고리별 재고 현황, 고객 연령대별 구매 패턴처럼 그룹으로 나누어 분석해야 하는 경우가 많죠.

이런 문제는 단순한 전체 집계만으로는 해결할 수 없습니다. 각 카테고리별, 그룹별로 통계를 내야 비교 분석이 가능하고, 의미 있는 인사이트를 발견할 수 있습니다.

바로 이럴 때 필요한 것이 group_by()와 집계 함수의 조합입니다. SQL의 GROUP BY처럼 카테고리별 통계를 한 번에 계산합니다.

개요

간단히 말해서, group_by()는 데이터를 특정 컬럼의 값에 따라 그룹으로 나눈 후, 각 그룹에 대해 집계 함수를 적용하는 기능입니다. 그룹별 집계는 데이터 분석에서 가장 강력한 도구 중 하나입니다.

예를 들어, 지역별 평균 소득, 부서별 평균 근속연수, 상품 카테고리별 총 판매량 같은 경우에 필수적입니다. 이를 통해 전체 평균만 볼 때는 보이지 않던 그룹 간 차이와 패턴을 발견할 수 있습니다.

전통적인 방법으로는 각 그룹을 수동으로 필터링한 후 개별적으로 집계했다면, 이제는 group_by()로 모든 그룹을 한 번에 처리합니다. group_by()의 핵심 특징은 첫째, 여러 컬럼으로 다단계 그룹화 가능, 둘째, 여러 집계 함수를 동시에 적용 가능, 셋째, 매우 효율적인 해시 기반 그룹화입니다.

이러한 특징들이 복잡한 다차원 분석을 간단하게 만들어줍니다.

코드 예제

import polars as pl

# 지역별 매장 매출 데이터
df = pl.DataFrame({
    "region": ["서울", "서울", "부산", "부산", "대구"],
    "store": ["A", "B", "C", "D", "E"],
    "sales": [500, 600, 400, 450, 380]
})

# 지역별 평균 매출과 매장 수
result = df.group_by("region").agg([
    pl.col("sales").mean().alias("avg_sales"),
    pl.col("store").count().alias("store_count")
])
print(result)

설명

이것이 하는 일: group_by() 함수는 지정된 컬럼의 유니크한 값들을 기준으로 데이터를 그룹으로 분할한 후, 각 그룹에 대해 집계 연산을 독립적으로 수행합니다. 첫 번째로, group_by("region")이 실행되면 "region" 컬럼의 유니크한 값들("서울", "부산", "대구")을 찾아냅니다.

그리고 각 값에 해당하는 행들을 묶어 3개의 그룹을 만듭니다: 서울 그룹(2개 행), 부산 그룹(2개 행), 대구 그룹(1개 행). 그 다음으로, agg() 함수 안에 정의된 집계 연산들이 각 그룹에 독립적으로 적용됩니다.

서울 그룹에서는 sales의 평균 (500+600)/2=550과 store 개수 2를 계산하고, 부산 그룹에서는 평균 425와 개수 2를, 대구 그룹에서는 평균 380과 개수 1을 계산합니다. alias()를 사용하여 결과 컬럼에 의미 있는 이름을 붙입니다.

이렇게 하면 결과 DataFrame에서 "avg_sales"와 "store_count"라는 명확한 컬럼명으로 접근할 수 있습니다. Polars는 내부적으로 해시 테이블을 사용하여 이 그룹화를 매우 효율적으로 처리합니다.

여러분이 이 코드를 사용하면 카테고리별 비교가 즉시 가능합니다. 어느 지역의 매출이 높은지 한눈에 파악할 수 있고, 그룹 간 차이를 통계적으로 분석할 수 있으며, 비즈니스 의사결정을 위한 세그먼트별 인사이트를 얻을 수 있습니다.

또한 여러 집계를 동시에 수행하여 효율성도 높일 수 있습니다.

실전 팁

💡 여러 컬럼으로 그룹화하려면 group_by(["region", "category"])처럼 리스트로 전달하여 계층적 그룹화를 할 수 있습니다.

💡 agg() 안에서 여러 집계를 동시에 수행하면 한 번의 그룹 순회로 모든 통계를 계산하여 효율적입니다.

💡 그룹별 결과를 정렬하려면 .sort("avg_sales", descending=True)를 체이닝하여 상위/하위 그룹을 쉽게 파악하세요.

💡 조건부 집계가 필요하다면 agg 안에서 filter()나 when().then() 표현식을 사용할 수 있습니다.

💡 그룹 개수가 매우 많을 때는 메모리를 고려하여 streaming 모드나 필터링을 먼저 적용하는 것을 고려하세요.


8. first와 last - 첫 번째와 마지막 값 가져오기

시작하며

여러분이 시계열 데이터를 다루면서 "각 고객의 첫 구매 상품이 뭐였지?" 또는 "가장 최근 로그인 시간은 언제지?" 같은 질문을 받을 때가 있나요? 데이터가 시간순이나 특정 순서로 정렬되어 있을 때, 그룹의 시작과 끝을 파악하는 것이 중요한 경우가 많죠.

이런 작업은 고객 여정 분석, 변화 추적, 트렌드 파악에 필수적입니다. 첫 번째 값은 시작 상태를, 마지막 값은 최신 상태를 나타내므로, 둘을 비교하면 변화의 방향과 크기를 알 수 있습니다.

바로 이럴 때 필요한 것이 first()와 last() 함수입니다. 정렬된 데이터에서 양 끝 값을 빠르게 추출합니다.

개요

간단히 말해서, first()는 그룹이나 컬럼의 첫 번째 값을, last()는 마지막 값을 반환하는 함수입니다. 첫 번째와 마지막 값은 시계열 분석과 그룹별 상태 추적에 핵심적입니다.

예를 들어, 고객별 첫 구매일과 최근 구매일, 세션별 시작 페이지와 종료 페이지, 주식의 시가와 종가 같은 경우에 의미 있는 정보를 제공합니다. 특히 group_by()와 함께 사용하면 각 그룹의 시작과 끝을 효율적으로 파악할 수 있습니다.

전통적인 방법으로는 인덱스로 [0]과 [-1]을 직접 접근하거나 정렬 후 head/tail을 사용했다면, 이제는 명확한 의도를 표현하는 함수로 간단히 처리합니다. first()와 last()의 핵심 특징은 첫째, 데이터의 현재 순서를 그대로 사용, 둘째, group_by()와 결합하여 그룹별 양 끝 추출, 셋째, null 값도 그대로 반환(건너뛰지 않음)입니다.

이러한 특징들이 순서에 의존하는 분석을 명확하고 효율적으로 만들어줍니다.

코드 예제

import polars as pl

# 고객별 구매 이력 (시간순 정렬됨)
df = pl.DataFrame({
    "customer": ["A", "A", "B", "B", "B"],
    "date": ["2024-01-01", "2024-03-15", "2024-01-10", "2024-02-20", "2024-04-05"],
    "product": ["노트북", "마우스", "키보드", "모니터", "헤드셋"]
})

# 고객별 첫 구매와 최근 구매 상품
result = df.group_by("customer").agg([
    pl.col("product").first().alias("first_purchase"),
    pl.col("product").last().alias("last_purchase")
])
print(result)

설명

이것이 하는 일: first()와 last() 함수는 데이터의 현재 순서를 기준으로 그룹별 첫 번째 행과 마지막 행의 값을 추출합니다. 첫 번째로, group_by("customer")가 고객별로 데이터를 그룹화합니다.

고객 A는 2개 행, 고객 B는 3개 행으로 나뉩니다. 중요한 점은 DataFrame이 이미 날짜순으로 정렬되어 있다는 것입니다.

first()와 last()는 데이터의 현재 순서를 그대로 사용하므로, 정렬이 중요합니다. 그 다음으로, 각 그룹에서 pl.col("product").first()는 그룹의 첫 번째 행의 product 값을 가져옵니다.

고객 A의 경우 "노트북"이, 고객 B의 경우 "키보드"가 됩니다. 이는 각 고객의 첫 구매 상품을 의미합니다.

마찬가지로 .last()는 각 그룹의 마지막 행을 가져옵니다. 고객 A는 "마우스", 고객 B는 "헤드셋"이 최근 구매 상품입니다.

alias()로 컬럼명을 명확히 지정하여 결과 해석이 쉽도록 합니다. 여러분이 이 코드를 사용하면 시계열 변화를 추적할 수 있습니다.

고객별 첫 구매와 최근 구매를 비교하여 구매 패턴 변화를 파악할 수 있고, 세션별 진입점과 이탈점을 분석하여 사용자 여정을 이해할 수 있으며, 시작 상태와 종료 상태의 차이로 성장이나 감소를 측정할 수 있습니다.

실전 팁

💡 first()와 last()를 사용하기 전에 반드시 원하는 순서로 정렬하세요. sort()를 먼저 적용하지 않으면 의도하지 않은 결과가 나올 수 있습니다.

💡 시간 컬럼에 적용하면 각 그룹의 시작 시간과 종료 시간을 빠르게 얻을 수 있어 기간 계산에 유용합니다.

💡 null 값도 그대로 반환되므로, null을 제외하려면 먼저 filter(pl.col("column").is_not_null())로 필터링하세요.

💡 n_unique()와 함께 사용하면 "첫 구매와 최근 구매가 다른 고객"을 찾는 등의 분석이 가능합니다.

💡 over() 표현식과 함께 사용하면 원본 행을 유지하면서 그룹별 첫/마지막 값을 각 행에 추가할 수 있습니다.


9. n_unique - 고유값 개수 세기

시작하며

여러분이 웹사이트 로그를 분석하면서 "오늘 방문한 순 방문자가 몇 명이지?"라는 질문을 받을 때가 있나요? 전체 방문 로그는 수천 개인데, 한 사람이 여러 번 방문했을 수 있어서 단순 개수로는 파악이 안 되죠.

제품 카탈로그에서 실제로 몇 개의 다른 카테고리가 있는지, 또는 고유한 사용자 아이디가 몇 개인지 알아야 하는 경우도 많습니다. 이런 문제는 중복을 제거한 유니크한 값의 개수를 세야 해결됩니다.

단순 count()는 중복을 포함한 전체 개수를 세지만, 우리가 필요한 것은 "서로 다른" 값의 개수입니다. 바로 이럴 때 필요한 것이 n_unique() 함수입니다.

중복을 자동으로 제거하고 고유값의 개수만 정확히 세어줍니다.

개요

간단히 말해서, n_unique()는 컬럼에서 중복을 제거한 후 서로 다른 값이 몇 개인지 세는 함수입니다. 고유값 개수는 데이터의 다양성과 카디널리티를 파악하는 핵심 지표입니다.

예를 들어, 순 방문자 수(IP나 사용자 ID의 고유 개수), 판매된 제품 종류 수, 데이터베이스의 유니크한 카테고리 개수 같은 경우에 필수적입니다. SQL의 COUNT(DISTINCT column)과 동일한 기능이며, 데이터 품질 검증(예: 중복 체크)에도 활용됩니다.

전통적인 방법으로는 먼저 unique()로 중복을 제거한 후 len()으로 개수를 셌다면, 이제는 n_unique() 한 번으로 즉시 결과를 얻습니다. n_unique()의 핵심 특징은 첫째, 내부적으로 해시셋을 사용한 효율적인 중복 제거, 둘째, null도 하나의 고유값으로 카운트, 셋째, group_by()와 결합하여 그룹별 다양성 측정입니다.

이러한 특징들이 정확한 카디널리티 분석을 가능하게 합니다.

코드 예제

import polars as pl

# 웹사이트 방문 로그 (중복 방문자 포함)
df = pl.DataFrame({
    "timestamp": ["10:00", "10:05", "10:10", "10:15", "10:20"],
    "user_id": ["user1", "user2", "user1", "user3", "user1"],
    "page": ["home", "about", "home", "contact", "products"]
})

# 순 방문자 수와 방문한 페이지 종류 수
unique_users = df.select(pl.col("user_id").n_unique()).item()
unique_pages = df.select(pl.col("page").n_unique()).item()

print(f"순 방문자: {unique_users}명, 페이지 종류: {unique_pages}개")
# 결과: 순 방문자: 3명, 페이지 종류: 4개

설명

이것이 하는 일: n_unique() 함수는 컬럼의 모든 값을 해시셋에 추가하면서 자동으로 중복을 제거하고, 최종적으로 셋의 크기를 반환합니다. 첫 번째로, pl.col("user_id")로 사용자 ID 컬럼을 선택합니다.

이 컬럼에는 "user1", "user2", "user1", "user3", "user1"이 들어있어, 5개의 방문 로그가 있지만 실제로는 3명의 서로 다른 사용자입니다. 그 다음으로, .n_unique()가 내부적으로 해시셋 자료구조를 생성합니다.

첫 번째 "user1"을 추가하고, "user2"를 추가하고, 다시 "user1"을 만나면 이미 존재하므로 추가하지 않고, "user3"을 추가합니다. 해시셋의 특성상 중복 값은 자동으로 제거됩니다.

Polars는 이 과정을 매우 효율적으로 수행하며, 해시 함수를 통해 O(n) 시간 복잡도로 처리합니다. 최종적으로 해시셋에 남은 요소의 개수인 3을 반환합니다.

마찬가지로 page 컬럼에서도 "home", "about", "contact", "products" 4개의 서로 다른 페이지를 찾아냅니다. 여러분이 이 코드를 사용하면 데이터의 실제 다양성을 파악할 수 있습니다.

마케팅에서 순 도달 수를 측정할 수 있고, 재고 관리에서 SKU 개수를 확인할 수 있으며, 데이터 품질 검증에서 중복 여부를 확인할 수 있습니다. 또한 count()와 비교하여 중복률을 계산할 수도 있습니다(중복률 = 1 - n_unique/count).

실전 팁

💡 null 값도 하나의 고유값으로 카운트됩니다. null을 제외하려면 먼저 drop_nulls()나 필터링을 적용하세요.

💡 중복률을 확인하려면 (pl.col("column").count() - pl.col("column").n_unique()) / pl.col("column").count()로 계산할 수 있습니다.

💡 카디널리티가 매우 높은 컬럼(예: 타임스탬프, UUID)에서는 n_unique()가 거의 count()와 같을 것입니다.

💡 그룹별 다양성을 측정하려면 group_by().agg(pl.col("column").n_unique())로 각 그룹 내 고유값 개수를 비교하세요.

💡 실제 고유값 리스트가 필요하다면 n_unique() 대신 unique()를 사용하여 값 자체를 얻을 수 있습니다.


10. quantile - 백분위수로 분포 파악하기

시작하며

여러분이 성적 분포를 분석하면서 "상위 25%는 몇 점 이상이지?"라는 질문을 받거나, 응답 시간 데이터에서 "95%의 요청이 몇 초 안에 처리되지?"를 확인해야 할 때가 있나요? 평균과 중앙값만으로는 데이터의 전체 분포를 제대로 이해할 수 없죠.

이런 문제는 데이터가 어떻게 퍼져있는지 세밀하게 파악해야 해결됩니다. 상위 몇 퍼센트, 하위 몇 퍼센트의 경계값을 알아야 분포의 형태와 극단값의 위치를 정확히 이해할 수 있습니다.

바로 이럴 때 필요한 것이 quantile() 함수입니다. 데이터를 백분위로 나누어 특정 위치의 값을 찾아줍니다.

개요

간단히 말해서, quantile(q)는 데이터를 정렬했을 때 하위 q% 지점의 값을 반환하는 함수입니다 (q는 0.0~1.0 사이). 백분위수는 데이터 분포를 이해하는 강력한 도구입니다.

예를 들어, SLA(Service Level Agreement)에서 95 백분위 응답 시간(대부분의 사용자가 경험하는 성능), 소득 분포에서 4분위수(quartile)로 계층 나누기, 이상치 탐지를 위한 1% 및 99% 경계값 같은 경우에 필수적입니다. median()은 사실 quantile(0.5)의 특수 케이스입니다.

전통적인 방법으로는 데이터를 정렬한 후 인덱스를 계산하여 직접 접근했다면, 이제는 quantile()로 원하는 백분위수를 바로 얻습니다. quantile()의 핵심 특징은 첫째, 0.0(최솟값)부터 1.0(최댓값)까지 임의의 백분위 지원, 둘째, 여러 분위수를 동시에 계산 가능, 셋째, 다양한 보간 방법 제공입니다.

이러한 특징들이 세밀한 분포 분석을 가능하게 합니다.

코드 예제

import polars as pl

# 웹 응답 시간 데이터 (밀리초)
df = pl.DataFrame({
    "request_id": range(1, 11),
    "response_time": [120, 135, 145, 150, 160, 170, 180, 200, 250, 800]
})

# 50%, 90%, 95%, 99% 백분위 응답 시간
percentiles = df.select([
    pl.col("response_time").quantile(0.5).alias("p50"),
    pl.col("response_time").quantile(0.9).alias("p90"),
    pl.col("response_time").quantile(0.95).alias("p95"),
    pl.col("response_time").quantile(0.99).alias("p99")
])
print(percentiles)

설명

이것이 하는 일: quantile(q) 함수는 데이터를 오름차순으로 정렬한 후, 전체의 q 비율만큼 지점에 위치하는 값을 찾아 반환합니다. 첫 번째로, 내부적으로 response_time 데이터를 정렬합니다: 120, 135, 145, 150, 160, 170, 180, 200, 250, 800.

총 10개의 값이므로, 각 백분위의 위치를 계산할 수 있습니다. 그 다음으로, quantile(0.5)는 중앙값을 계산합니다.

10개 값의 중간인 5번째와 6번째 값(160과 170)의 평균인 165를 반환합니다. quantile(0.9)는 하위 90% 지점으로, 9번째 값 근처인 약 250을 반환합니다.

quantile(0.95)는 95% 지점을 찾습니다. 이는 "95%의 요청이 이 시간 안에 처리된다"는 의미로, 성능 SLA를 측정하는 표준 메트릭입니다.

이 예제에서는 약 525가 나오는데, 이는 대부분의 요청은 빠르지만 상위 5%는 느리다는 것을 보여줍니다. 여러분이 이 코드를 사용하면 전체 분포를 이해할 수 있습니다.

p50(중앙값)과 p90의 차이로 상위 10%의 이상 행동을 파악할 수 있고, p95나 p99를 SLA 목표로 설정하여 대부분의 사용자 경험을 보장할 수 있으며, 4분위수(0.25, 0.5, 0.75)로 데이터를 균등하게 나누어 세그먼트 분석을 할 수 있습니다.

실전 팁

💡 성능 모니터링에서는 평균보다 p95나 p99를 사용하세요. 평균은 이상치에 왜곡되지만 백분위수는 실제 사용자 경험을 반영합니다.

💡 4분위수(quartile)는 0.25, 0.5, 0.75로, IQR(Interquartile Range) = Q3-Q1은 이상치 탐지에 유용합니다.

💡 여러 백분위수를 동시에 계산할 때는 리스트로 전달하여 한 번의 정렬로 모든 값을 얻을 수 있습니다.

💡 보간 방법(interpolation)을 "linear", "lower", "higher", "nearest", "midpoint" 중 선택하여 정확한 백분위 위치가 없을 때의 동작을 제어하세요.

💡 그룹별 백분위수를 계산하면 카테고리 간 분포 차이를 비교할 수 있어, A/B 테스트에서 평균 차이만이 아닌 전체 분포 변화를 파악할 수 있습니다.


#Polars#Aggregation#GroupBy#Statistics#DataAnalysis#데이터분석,Python,Polars

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.