이미지 로딩 중...
AI Generated
2025. 11. 15. · 8 Views
데이터셋 이해하기 완벽 가이드
데이터 분석의 첫걸음, 데이터셋을 제대로 이해하고 다루는 방법을 배웁니다. Polars를 활용하여 데이터셋의 구조를 파악하고, 품질을 확인하며, 효율적으로 탐색하는 실전 기법을 소개합니다.
목차
- 데이터셋 구조 파악하기 - 첫 번째로 알아야 할 기본 정보
- 데이터 품질 확인하기 - 결측치와 이상치 탐지
- 데이터 타입 변환하기 - 올바른 타입으로 작업하기
- 컬럼 선택과 필터링 - 필요한 데이터만 추출하기
- 데이터 집계와 그룹화 - 인사이트 도출하기
- 컬럼 생성과 변환 - 새로운 피처 만들기
- 결측치 처리하기 - 데이터 완성도 높이기
- 데이터 정렬하기 - 순서 있는 인사이트
- 데이터 샘플링하기 - 대용량 데이터 다루기
- 데이터 조인하기 - 여러 데이터 연결하기
1. 데이터셋 구조 파악하기 - 첫 번째로 알아야 할 기본 정보
시작하며
여러분이 새로운 데이터 분석 프로젝트를 시작할 때 이런 상황을 겪어본 적 있나요? CSV 파일을 받았는데 어떤 컬럼이 있는지, 데이터가 몇 개나 되는지, 어떤 타입인지 전혀 감이 오지 않는 상황 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 데이터셋의 구조를 제대로 파악하지 않고 분석을 시작하면, 잘못된 타입으로 인한 에러, 예상치 못한 null 값, 메모리 부족 같은 문제에 부딪히게 됩니다.
바로 이럴 때 필요한 것이 데이터셋 구조 파악입니다. Polars를 사용하면 데이터셋의 전체적인 모습을 빠르게 확인하고, 어떤 방식으로 분석을 진행할지 계획을 세울 수 있습니다.
개요
간단히 말해서, 데이터셋 구조 파악은 분석을 시작하기 전에 데이터의 형태, 크기, 타입을 확인하는 첫 번째 단계입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 데이터의 규모를 모르면 메모리 문제가 발생할 수 있고, 컬럼의 타입을 모르면 연산 에러가 발생합니다.
예를 들어, 날짜 데이터가 문자열로 저장되어 있다면 시계열 분석이 불가능한 경우가 많습니다. 기존에는 pandas에서 .info(), .shape, .dtypes 등 여러 메서드를 조합해서 확인했다면, 이제는 Polars의 통합된 메서드들로 더 빠르고 명확하게 파악할 수 있습니다.
Polars는 스키마 정보를 명확하게 제공하고, Lazy 평가를 통해 대용량 데이터도 효율적으로 다룰 수 있으며, 타입 안정성이 뛰어나다는 특징이 있습니다. 이러한 특징들이 데이터 분석의 신뢰성과 성능을 크게 향상시킵니다.
코드 예제
import polars as pl
# CSV 파일 로드
df = pl.read_csv("sales_data.csv")
# 기본 정보 확인
print(f"행 수: {df.height}, 열 수: {df.width}")
# 컬럼 이름과 타입 확인
print(df.schema)
# 처음 5개 행 확인
print(df.head())
# 데이터 요약 정보
print(df.describe())
설명
이것이 하는 일: 위 코드는 CSV 파일을 로드한 후 데이터셋의 전체적인 구조와 특성을 확인합니다. 첫 번째로, pl.read_csv()로 파일을 읽어들인 후 height와 width 속성을 사용해 데이터의 규모를 파악합니다.
height는 행(row) 수, width는 열(column) 수를 나타내며, 이를 통해 데이터셋의 크기를 즉시 알 수 있습니다. 이렇게 하는 이유는 메모리 사용량을 예측하고 적절한 처리 전략을 세우기 위함입니다.
그 다음으로, schema 속성이 실행되면서 각 컬럼의 이름과 데이터 타입을 딕셔너리 형태로 보여줍니다. 내부에서는 Polars가 파일을 읽을 때 자동으로 타입 추론을 수행하며, Int64, Utf8, Float64, Date 등의 명확한 타입을 할당합니다.
head() 메서드는 기본적으로 처음 5개 행을 보여주며, 실제 데이터가 어떻게 생겼는지 직관적으로 확인할 수 있게 합니다. describe()는 수치형 컬럼에 대해 평균, 표준편차, 최소/최대값 등의 통계를 계산하여 데이터의 분포를 파악하게 해줍니다.
여러분이 이 코드를 사용하면 몇 초 만에 데이터셋의 전체 구조를 파악할 수 있고, 어떤 전처리가 필요한지, 어떤 분석이 가능한지 즉시 판단할 수 있습니다. 실무에서는 이 단계를 건너뛰지 않는 것이 중요하며, 예상치 못한 타입이나 크기로 인한 문제를 사전에 방지할 수 있고, 팀원들과 데이터 구조를 공유할 때도 유용합니다.
실전 팁
💡 대용량 파일은 pl.scan_csv()로 Lazy 모드로 읽어서 스키마만 확인한 후 필요한 컬럼만 선택하면 메모리를 절약할 수 있습니다
💡 df.null_count()로 각 컬럼의 결측치 개수를 먼저 확인하세요 - 결측치가 많은 컬럼은 별도 처리가 필요합니다
💡 타입이 예상과 다르다면 with_columns()와 cast()를 사용해 명시적으로 타입을 변환하는 것이 안전합니다
💡 df.glimpse()를 사용하면 각 컬럼의 샘플 데이터를 세로로 보기 좋게 출력해줘서 넓은 데이터셋 파악에 유용합니다
💡 스키마를 JSON으로 저장(df.schema)해두면 데이터 파이프라인 문서화나 타입 검증에 활용할 수 있습니다
2. 데이터 품질 확인하기 - 결측치와 이상치 탐지
시작하며
여러분이 데이터 분석을 진행하다가 이런 상황을 겪어본 적 있나요? 평균을 계산했는데 이상하게 큰 값이 나오거나, 특정 컬럼에서 계속 에러가 발생하는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 데이터 품질이 좋지 않으면 분석 결과가 왜곡되고, 잘못된 비즈니스 의사결정으로 이어질 수 있습니다.
결측치, 중복 데이터, 이상치는 데이터 품질을 해치는 주요 원인입니다. 바로 이럴 때 필요한 것이 체계적인 데이터 품질 확인입니다.
Polars를 활용하면 결측치 패턴을 빠르게 찾아내고, 중복을 제거하며, 이상치를 효율적으로 탐지할 수 있습니다.
개요
간단히 말해서, 데이터 품질 확인은 분석 전에 데이터의 신뢰성을 검증하고 문제가 있는 부분을 찾아내는 과정입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 결측치가 많은 데이터로 모델을 학습하면 성능이 떨어지고, 중복 데이터는 통계를 왜곡시키며, 이상치는 평균이나 분산 같은 지표를 크게 변화시킵니다.
예를 들어, 전자상거래 데이터에서 가격 컬럼에 음수 값이 있거나, 나이가 200살인 고객 데이터가 있다면 명백한 오류입니다. 기존에는 pandas에서 isnull().sum(), duplicated(), 수동 조건 필터링 등을 사용했다면, 이제는 Polars의 표현식 시스템으로 더 빠르고 선언적으로 품질 검사를 수행할 수 있습니다.
Polars는 null 처리가 명시적이고, 표현식 체이닝으로 복잡한 조건도 간결하게 작성할 수 있으며, 병렬 처리로 대용량 데이터도 빠르게 검사한다는 특징이 있습니다. 이러한 특징들이 데이터 품질 관리를 더 정확하고 효율적으로 만들어줍니다.
코드 예제
import polars as pl
df = pl.read_csv("customer_data.csv")
# 각 컬럼의 결측치 개수와 비율 확인
null_report = df.select([
(pl.col(col).null_count().alias(f"{col}_null_count"))
for col in df.columns
])
print(null_report)
# 중복 행 확인
duplicates = df.filter(pl.struct(df.columns).is_duplicated())
print(f"중복 행 수: {duplicates.height}")
# 수치형 컬럼의 이상치 탐지 (IQR 방식)
age_q1 = df["age"].quantile(0.25)
age_q3 = df["age"].quantile(0.75)
age_iqr = age_q3 - age_q1
outliers = df.filter(
(pl.col("age") < age_q1 - 1.5 * age_iqr) |
(pl.col("age") > age_q3 + 1.5 * age_iqr)
)
print(f"이상치 행 수: {outliers.height}")
설명
이것이 하는 일: 위 코드는 데이터셋에서 품질 문제가 될 수 있는 결측치, 중복 행, 이상치를 체계적으로 찾아냅니다. 첫 번째로, 리스트 컴프리헨션과 select()를 조합하여 모든 컬럼의 결측치 개수를 한 번에 계산합니다.
pl.col(col).null_count()는 각 컬럼에서 null 값의 개수를 세며, alias()로 의미 있는 컬럼명을 부여합니다. 이렇게 하는 이유는 어떤 컬럼에 결측치가 집중되어 있는지 한눈에 파악하기 위함입니다.
그 다음으로, pl.struct(df.columns).is_duplicated()가 실행되면서 모든 컬럼의 값을 하나의 구조체로 묶어 중복을 검사합니다. 내부에서는 각 행을 해시값으로 변환하여 중복을 빠르게 찾아내며, 이는 수백만 행의 데이터에서도 효율적으로 작동합니다.
이상치 탐지 부분에서는 IQR(Interquartile Range) 방식을 사용합니다. Q1(25번째 백분위수)과 Q3(75번째 백분위수)를 구한 후, IQR = Q3 - Q1을 계산하고, Q1 - 1.5 * IQR보다 작거나 Q3 + 1.5 * IQR보다 큰 값을 이상치로 판단합니다.
이는 통계적으로 널리 사용되는 방법으로, 극단적인 값을 객관적으로 식별합니다. 마지막으로, filter()로 조건을 만족하는 행만 추출하여 실제 이상치 데이터를 확인할 수 있게 합니다.
여러분이 이 코드를 사용하면 데이터의 문제점을 수치화하여 보고할 수 있고, 어떤 전처리 작업이 필요한지 우선순위를 정할 수 있으며, 데이터 수집 과정의 문제를 발견할 수 있습니다. 실무에서는 결측치가 10% 이상인 컬럼은 삭제하거나 별도 처리를 고려하고, 중복 데이터는 비즈니스 로직을 확인한 후 제거하며, 이상치는 도메인 전문가와 상의하여 처리 방법을 결정합니다.
실전 팁
💡 결측치 패턴을 시각화하려면 missingno 라이브러리와 함께 사용하세요 - Polars DataFrame을 pandas로 변환(to_pandas())하면 됩니다
💡 df.unique(maintain_order=True)로 중복을 제거할 때 순서를 유지할지 선택할 수 있으며, 순서가 중요하지 않다면 False로 설정해 성능을 높이세요
💡 여러 컬럼을 기준으로 중복을 검사할 때는 df.unique(subset=["col1", "col2"])를 사용하면 특정 컬럼 조합만 고려합니다
💡 이상치 탐지는 도메인에 따라 다르므로, IQR 외에도 Z-score, 도메인 규칙(예: 나이 0-120) 등 다양한 방법을 병행하세요
💡 df.drop_nulls()는 null이 하나라도 있는 행을 모두 삭제하므로, drop_nulls(subset=["중요컬럼"])으로 특정 컬럼만 지정하는 것이 안전합니다
3. 데이터 타입 변환하기 - 올바른 타입으로 작업하기
시작하며
여러분이 날짜 데이터로 시계열 분석을 하려는데 이런 상황을 겪어본 적 있나요? "2024-01-15"라는 문자열을 날짜로 인식하지 못해서 정렬이나 필터링이 제대로 되지 않는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. CSV 파일은 모든 데이터를 텍스트로 저장하기 때문에, 숫자나 날짜를 올바른 타입으로 변환하지 않으면 연산이 불가능하거나 잘못된 결과가 나옵니다.
예를 들어, "100"이라는 문자열은 더하기가 아니라 문자열 연결이 되어버립니다. 바로 이럴 때 필요한 것이 명시적인 데이터 타입 변환입니다.
Polars는 강력한 타입 시스템과 변환 함수를 제공하여, 안전하고 효율적으로 데이터 타입을 관리할 수 있게 해줍니다.
개요
간단히 말해서, 데이터 타입 변환은 문자열, 숫자, 날짜 등의 데이터를 분석 목적에 맞는 올바른 타입으로 바꾸는 작업입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 타입이 맞지 않으면 집계 함수가 작동하지 않고, 정렬 순서가 이상해지며, 메모리 사용량도 비효율적으로 증가합니다.
예를 들어, 카테고리형 데이터를 문자열로 저장하면 메모리를 많이 사용하지만, Categorical 타입으로 변환하면 메모리를 크게 절약할 수 있습니다. 기존에는 pandas에서 astype(), pd.to_datetime() 등을 사용했다면, 이제는 Polars의 cast() 메서드와 다양한 파싱 함수로 더 빠르고 안전하게 타입을 변환할 수 있습니다.
Polars는 타입 변환이 실패하면 명확한 에러를 발생시키고, 날짜/시간 타입을 네이티브로 지원하며, Categorical 타입으로 메모리를 최적화할 수 있다는 특징이 있습니다. 이러한 특징들이 데이터 파이프라인의 안정성과 성능을 향상시킵니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"order_id": ["1", "2", "3"],
"price": ["100.50", "200.75", "150.25"],
"order_date": ["2024-01-15", "2024-01-16", "2024-01-17"],
"category": ["A", "B", "A"]
})
# 타입 변환
df_converted = df.with_columns([
# 문자열 -> 정수
pl.col("order_id").cast(pl.Int64),
# 문자열 -> 실수
pl.col("price").cast(pl.Float64),
# 문자열 -> 날짜 (파싱 포맷 지정)
pl.col("order_date").str.strptime(pl.Date, "%Y-%m-%d"),
# 문자열 -> 카테고리 (메모리 최적화)
pl.col("category").cast(pl.Categorical)
])
print(df_converted.schema)
print(df_converted)
설명
이것이 하는 일: 위 코드는 문자열로 저장된 다양한 타입의 데이터를 실제 용도에 맞는 타입으로 변환합니다. 첫 번째로, with_columns()를 사용하여 여러 컬럼을 한 번에 변환합니다.
이 메서드는 기존 컬럼을 덮어쓰거나 새 컬럼을 추가할 수 있으며, 원본 DataFrame은 변경하지 않고 새로운 DataFrame을 반환합니다. 이렇게 하는 이유는 Polars의 불변성 원칙을 따르면서도 여러 변환을 효율적으로 수행하기 위함입니다.
그 다음으로, cast(pl.Int64), cast(pl.Float64) 같은 기본 타입 변환이 실행됩니다. 내부에서는 문자열을 파싱하여 숫자로 변환하며, 변환이 불가능한 값이 있으면 에러를 발생시킵니다.
이는 데이터 무결성을 보장하는 중요한 안전장치입니다. 날짜 변환 부분에서는 str.strptime()을 사용합니다.
이 함수는 문자열 날짜를 지정된 포맷("%Y-%m-%d")에 따라 파싱하여 Polars의 Date 타입으로 변환합니다. Date 타입으로 변환하면 날짜 간 차이 계산, 연도/월 추출, 날짜 필터링 등의 작업이 가능해집니다.
카테고리 타입 변환은 반복되는 문자열 값을 정수 인덱스로 인코딩하여 메모리를 크게 절약합니다. 예를 들어, "A", "B" 같은 값이 수백만 번 반복되는 경우, 각각을 0, 1로 저장하고 매핑 테이블만 유지하면 메모리 사용량이 현저히 줄어듭니다.
여러분이 이 코드를 사용하면 타입 관련 에러를 사전에 방지할 수 있고, 날짜나 숫자 연산을 정확하게 수행할 수 있으며, 대용량 데이터에서 메모리를 효율적으로 사용할 수 있습니다. 실무에서는 타입 변환을 데이터 로드 직후에 수행하여 파이프라인 초반에 데이터 품질을 보장하고, 변환 실패 시 로그를 남겨 데이터 소스 문제를 추적하며, 스키마를 문서화하여 팀원들과 공유합니다.
실전 팁
💡 변환 실패를 무시하고 null로 처리하려면 cast(pl.Float64, strict=False)를 사용하세요 - 데이터 정제 시 유용합니다
💡 날짜 포맷이 여러 개라면 str.strptime()을 여러 번 시도하거나, coalesce()로 여러 포맷을 순차적으로 적용할 수 있습니다
💡 대용량 데이터에서 반복 값이 많은 컬럼은 무조건 Categorical로 변환하세요 - 메모리를 최대 90%까지 절약할 수 있습니다
💡 DateTime 타입은 타임존 정보를 포함할 수 있으므로, dt.convert_time_zone()으로 명시적으로 관리하는 것이 안전합니다
💡 타입 변환 후에는 df.schema로 변환이 제대로 되었는지 반드시 확인하세요 - 예상과 다른 타입은 나중에 버그를 일으킵니다
4. 컬럼 선택과 필터링 - 필요한 데이터만 추출하기
시작하며
여러분이 100개의 컬럼이 있는 데이터셋에서 이런 상황을 겪어본 적 있나요? 실제로 분석에 필요한 건 5개 컬럼뿐인데, 전체를 로드해서 메모리가 부족하거나 처리 속도가 느려지는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 불필요한 데이터를 모두 로드하면 메모리 낭비는 물론이고, 네트워크 전송 시간, 처리 시간이 증가하며, 코드의 가독성도 떨어집니다.
특히 클라우드 환경에서는 불필요한 데이터 전송이 비용으로 직결됩니다. 바로 이럴 때 필요한 것이 효율적인 컬럼 선택과 필터링입니다.
Polars는 표현식 기반의 강력한 선택 문법을 제공하여, 필요한 데이터만 정확하게 추출할 수 있게 해줍니다.
개요
간단히 말해서, 컬럼 선택과 필터링은 데이터셋에서 분석에 필요한 컬럼과 행만 추출하여 효율성을 높이는 작업입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 작은 데이터셋을 다루면 연산 속도가 빨라지고, 메모리 사용량이 줄어들며, 코드의 의도가 명확해집니다.
예를 들어, 고객 데이터에서 이메일 마케팅을 위해 이메일 주소와 구매 이력만 필요하다면, 주소나 전화번호 같은 다른 정보는 불필요합니다. 기존에는 pandas에서 df[['col1', 'col2']], df[df['age'] > 30] 같은 인덱싱을 사용했다면, 이제는 Polars의 표현식 시스템으로 더 강력하고 조합 가능한 방식으로 데이터를 선택할 수 있습니다.
Polars는 선택 표현식을 lazy 평가로 최적화하고, 정규식이나 타입 기반 선택을 지원하며, 복잡한 조건을 체이닝으로 간결하게 작성할 수 있다는 특징이 있습니다. 이러한 특징들이 데이터 처리의 생산성과 성능을 크게 향상시킵니다.
코드 예제
import polars as pl
df = pl.read_csv("user_data.csv")
# 특정 컬럼만 선택
selected = df.select(["user_id", "name", "email", "purchase_amount"])
# 타입 기반 선택 - 모든 숫자형 컬럼
numeric_cols = df.select(pl.col(pl.NUMERIC_DTYPES))
# 정규식으로 컬럼 선택 - "date"로 끝나는 컬럼
date_cols = df.select(pl.col("^.*date$"))
# 조건 필터링 - 구매액 1000 이상인 VIP 고객
vip_customers = df.filter(
(pl.col("purchase_amount") >= 1000) &
(pl.col("status") == "active")
)
# 복합 조건 - 나이 18-65세이고 이메일 수신 동의한 고객
target_users = df.filter(
pl.col("age").is_between(18, 65) &
pl.col("email_opt_in").eq(True)
)
설명
이것이 하는 일: 위 코드는 다양한 방법으로 필요한 컬럼을 선택하고, 조건에 맞는 행만 필터링하여 효율적인 데이터셋을 만듭니다. 첫 번째로, select() 메서드에 컬럼 이름 리스트를 전달하여 명시적으로 원하는 컬럼만 선택합니다.
이는 가장 기본적인 방법이며, 어떤 데이터를 다루는지 코드만 봐도 즉시 알 수 있어 가독성이 좋습니다. 이렇게 하는 이유는 불필요한 컬럼을 제거하여 후속 연산의 부담을 줄이기 위함입니다.
그 다음으로, pl.col(pl.NUMERIC_DTYPES) 같은 타입 기반 선택이 실행됩니다. 내부에서는 스키마 정보를 참조하여 숫자형(Int, Float 등) 타입의 컬럼만 자동으로 찾아냅니다.
이는 통계 분석이나 머신러닝 전처리에서 숫자형 컬럼만 다뤄야 할 때 매우 유용합니다. 정규식 선택(pl.col("^.*date$"))은 패턴에 매칭되는 컬럼명을 찾습니다.
예를 들어, "created_date", "updated_date", "order_date" 같은 컬럼들을 일일이 나열하지 않고 한 번에 선택할 수 있습니다. 필터링 부분에서는 filter() 메서드와 조건 표현식을 조합합니다.
&(AND), |(OR) 연산자로 복잡한 조건을 결합할 수 있으며, is_between(), eq() 같은 메서드는 가독성을 높여줍니다. Polars는 이러한 조건들을 내부적으로 최적화하여 빠르게 실행합니다.
여러분이 이 코드를 사용하면 데이터 로딩 시간을 크게 줄일 수 있고, 분석에 집중할 수 있는 깔끔한 데이터셋을 얻을 수 있으며, 동적으로 컬럼을 선택하는 유연한 파이프라인을 구축할 수 있습니다. 실무에서는 Lazy API(scan_csv())와 함께 사용하면 필요한 컬럼만 디스크에서 읽어와 메모리를 극적으로 절약할 수 있고, 복잡한 비즈니스 로직도 체이닝으로 표현하여 유지보수가 쉬운 코드를 작성할 수 있습니다.
실전 팁
💡 exclude()를 사용하면 특정 컬럼만 제외하고 나머지를 모두 선택할 수 있습니다 - df.select(pl.exclude(["password", "ssn"]))
💡 여러 조건을 OR로 연결할 때는 | 대신 any_horizontal([조건1, 조건2])를 사용하면 더 명확합니다
💡 날짜 범위 필터링은 is_between()보다 dt.year() == 2024 같은 접근법이 더 읽기 쉬울 수 있습니다
💡 filter()는 여러 번 체이닝할 수 있으며, 각 단계를 명확히 분리하면 디버깅이 쉬워집니다
💡 대용량 데이터는 scan_csv().select().filter().collect()로 lazy 평가를 활용하여 실행 계획을 최적화하세요
5. 데이터 집계와 그룹화 - 인사이트 도출하기
시작하며
여러분이 전국 매장의 판매 데이터를 분석할 때 이런 상황을 겪어본 적 있나요? 지역별, 제품별로 매출을 합산하고 평균을 내야 하는데, 코드가 복잡해지고 성능도 느려지는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 원본 데이터는 개별 거래 단위로 저장되어 있지만, 의사결정을 위해서는 그룹별 통계가 필요합니다.
지역별 총 매출, 제품별 평균 가격, 고객 세그먼트별 구매 빈도 같은 집계 정보가 비즈니스 인사이트를 제공합니다. 바로 이럴 때 필요한 것이 효율적인 데이터 집계와 그룹화입니다.
Polars는 SQL의 GROUP BY와 유사하지만 더 강력한 표현식 시스템을 제공하여, 복잡한 집계도 빠르고 간결하게 수행할 수 있게 해줍니다.
개요
간단히 말해서, 데이터 집계와 그룹화는 데이터를 특정 기준으로 묶어서 합계, 평균, 개수 같은 통계를 계산하는 작업입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 수백만 개의 거래 데이터를 일일이 보는 것은 불가능하고, 그룹별 패턴을 파악해야 비즈니스 의사결정을 할 수 있습니다.
예를 들어, 전체 매출보다는 "서울 지역의 전자제품 카테고리 매출"이 실제로 유용한 정보입니다. 기존에는 pandas에서 groupby().agg()를 사용했지만 성능이 느리고 문법이 복잡했다면, 이제는 Polars의 group_by().agg()로 병렬 처리의 이점을 누리면서도 직관적인 코드를 작성할 수 있습니다.
Polars는 그룹화 연산을 자동으로 병렬화하고, 여러 집계를 한 번에 계산할 수 있으며, 표현식으로 복잡한 조건부 집계도 가능하다는 특징이 있습니다. 이러한 특징들이 데이터 분석의 속도와 유연성을 크게 향상시킵니다.
코드 예제
import polars as pl
df = pl.read_csv("sales_data.csv")
# 기본 그룹화 - 지역별 총 매출과 평균 매출
regional_sales = df.group_by("region").agg([
pl.col("sales_amount").sum().alias("total_sales"),
pl.col("sales_amount").mean().alias("avg_sales"),
pl.col("order_id").count().alias("order_count")
])
# 복수 컬럼 그룹화 - 지역별, 제품 카테고리별 집계
category_sales = df.group_by(["region", "category"]).agg([
pl.col("sales_amount").sum(),
pl.col("quantity").sum(),
pl.col("customer_id").n_unique().alias("unique_customers")
])
# 조건부 집계 - 고액 거래와 일반 거래 분리
sales_by_type = df.group_by("region").agg([
pl.col("sales_amount").filter(pl.col("sales_amount") >= 1000).sum().alias("premium_sales"),
pl.col("sales_amount").filter(pl.col("sales_amount") < 1000).sum().alias("regular_sales")
])
print(regional_sales.sort("total_sales", descending=True))
설명
이것이 하는 일: 위 코드는 데이터를 다양한 기준으로 그룹화하고, 각 그룹에 대해 여러 통계를 동시에 계산합니다. 첫 번째로, group_by("region")으로 지역별로 데이터를 묶습니다.
이 메서드는 동일한 region 값을 가진 행들을 하나의 그룹으로 만들며, 이후 agg()에서 각 그룹에 대해 집계 함수를 적용합니다. 이렇게 하는 이유는 지역이라는 비즈니스 단위로 데이터를 요약하기 위함입니다.
그 다음으로, agg() 내부에서 여러 표현식이 실행됩니다. pl.col("sales_amount").sum()은 각 그룹의 매출 합계를, mean()은 평균을, count()는 개수를 계산합니다.
내부적으로 Polars는 이러한 연산들을 병렬로 처리하여 성능을 최적화합니다. alias()로 결과 컬럼에 의미 있는 이름을 부여하면 가독성이 높아집니다.
복수 컬럼 그룹화에서는 ["region", "category"] 리스트를 전달하여 두 가지 기준으로 동시에 묶습니다. 이는 지역과 카테고리의 모든 조합에 대해 집계를 수행하며, 교차 분석에 유용합니다.
n_unique()는 고유한 값의 개수를 세어 "각 지역-카테고리 조합에서 몇 명의 고객이 구매했는지"를 알려줍니다. 조건부 집계는 filter()를 집계 표현식 내부에 사용합니다.
pl.col("sales_amount").filter(조건).sum()은 조건을 만족하는 값만 골라서 합계를 계산하므로, 하나의 그룹화로 여러 세그먼트를 동시에 분석할 수 있습니다. 여러분이 이 코드를 사용하면 비즈니스 질문에 즉시 답할 수 있는 요약 데이터를 얻을 수 있고, 수백만 행을 몇 초 만에 집계하여 대시보드를 만들 수 있으며, 복잡한 비즈니스 로직도 선언적으로 표현할 수 있습니다.
실무에서는 집계 결과를 sort()로 정렬하여 상위/하위 그룹을 파악하고, pivot()으로 와이드 포맷으로 변환하여 리포트를 작성하며, 시계열 그룹화(group_by_dynamic())로 일별/월별 추이를 분석합니다.
실전 팁
💡 maintain_order=True 옵션을 group_by에 추가하면 그룹 순서를 유지하지만, False(기본값)가 더 빠릅니다
💡 여러 집계 함수를 적용할 때는 리스트로 묶어서 한 번에 전달하면 내부 최적화가 더 잘 됩니다
💡 quantile([0.25, 0.5, 0.75])로 여러 백분위수를 동시에 계산하여 분포를 파악할 수 있습니다
💡 시계열 데이터는 group_by_dynamic(every="1mo")로 날짜 기반 동적 그룹화를 사용하세요 - 월별, 주별 집계가 쉽습니다
💡 집계 후 filter()를 다시 적용하여 "총 매출 1억 이상인 지역만" 같은 후처리를 할 수 있습니다 (SQL의 HAVING과 유사)
6. 컬럼 생성과 변환 - 새로운 피처 만들기
시작하며
여러분이 머신러닝 모델을 학습시키거나 분석을 할 때 이런 상황을 겪어본 적 있나요? 기존 데이터만으로는 부족해서 새로운 특성(feature)을 만들어야 하는데, 복잡한 계산이나 조건을 적용해야 하는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 원본 데이터는 가공되지 않은 상태이므로, 비즈니스 로직이나 도메인 지식을 반영한 파생 컬럼을 만들어야 합니다.
예를 들어, "구매 금액"과 "할인율"에서 "실제 결제 금액"을 계산하거나, "생년월일"에서 "나이"를 구하는 작업이 필요합니다. 바로 이럴 때 필요한 것이 효율적인 컬럼 생성과 변환입니다.
Polars는 표현식 기반의 강력한 변환 기능을 제공하여, 복잡한 로직도 선언적이고 빠르게 구현할 수 있게 해줍니다.
개요
간단히 말해서, 컬럼 생성과 변환은 기존 데이터를 기반으로 새로운 컬럼을 만들거나 기존 컬럼을 가공하는 작업입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 분석에 필요한 정보가 직접적으로 존재하지 않는 경우가 많고, 머신러닝에서는 피처 엔지니어링이 모델 성능을 크게 좌우합니다.
예를 들어, 전자상거래에서 "고객 생애 가치(LTV)"는 여러 거래 데이터를 조합하여 계산해야 하는 파생 지표입니다. 기존에는 pandas에서 apply(), 람다 함수, 또는 반복문을 사용했지만 성능이 느렸다면, 이제는 Polars의 벡터화된 표현식으로 수백만 행도 빠르게 처리할 수 있습니다.
Polars는 모든 연산이 벡터화되어 빠르고, 조건부 로직을 when-then-otherwise로 명확하게 표현하며, 복잡한 계산도 체이닝으로 간결하게 작성할 수 있다는 특징이 있습니다. 이러한 특징들이 피처 엔지니어링의 생산성과 성능을 극대화합니다.
코드 예제
import polars as pl
from datetime import date
df = pl.DataFrame({
"product": ["A", "B", "C", "A"],
"price": [100, 200, 150, 100],
"quantity": [2, 1, 3, 5],
"discount_rate": [0.1, 0.2, 0.0, 0.15],
"birth_year": [1990, 1985, 2000, 1995]
})
# 새로운 컬럼 생성
df_enhanced = df.with_columns([
# 총 금액 계산
(pl.col("price") * pl.col("quantity")).alias("subtotal"),
# 할인 금액 계산
(pl.col("price") * pl.col("quantity") * pl.col("discount_rate")).alias("discount_amount"),
# 최종 결제 금액
(pl.col("price") * pl.col("quantity") * (1 - pl.col("discount_rate"))).alias("final_amount"),
# 나이 계산
(pl.lit(date.today().year) - pl.col("birth_year")).alias("age"),
# 조건부 컬럼 - 고액 구매 여부
pl.when(pl.col("price") * pl.col("quantity") >= 300)
.then(pl.lit("High"))
.otherwise(pl.lit("Normal"))
.alias("purchase_tier")
])
print(df_enhanced)
설명
이것이 하는 일: 위 코드는 기존 컬럼들을 수학적으로 조합하거나 조건을 적용하여 새로운 의미 있는 컬럼들을 생성합니다. 첫 번째로, with_columns()를 사용하여 여러 파생 컬럼을 동시에 추가합니다.
이 메서드는 원본 DataFrame을 유지하면서 새 컬럼을 추가한 새로운 DataFrame을 반환하므로, 원본 데이터의 무결성을 보장합니다. 이렇게 하는 이유는 데이터 파이프라인에서 추적 가능성과 재현성을 확보하기 위함입니다.
그 다음으로, 수학적 연산 표현식들이 실행됩니다. pl.col("price") * pl.col("quantity")는 두 컬럼의 각 행을 곱하여 새로운 시리즈를 만듭니다.
내부적으로 이는 벡터화된 연산으로, 파이썬 반복문보다 수십 배 빠릅니다. 할인율을 적용한 최종 금액 계산도 한 줄의 표현식으로 명확하게 표현됩니다.
나이 계산 부분에서는 pl.lit()로 스칼라 값(현재 연도)을 모든 행에 적용합니다. pl.lit(2024) - pl.col("birth_year")는 각 사람의 나이를 계산하며, 이는 날짜 기반 피처 엔지니어링의 기본 패턴입니다.
조건부 컬럼 생성은 when-then-otherwise 구조를 사용합니다. SQL의 CASE WHEN과 유사하며, 조건을 평가하여 참이면 "High", 거짓이면 "Normal"을 할당합니다.
여러 조건을 체이닝할 수도 있어(when().then().when().then().otherwise()), 복잡한 비즈니스 로직을 깔끔하게 구현할 수 있습니다. 여러분이 이 코드를 사용하면 비즈니스 규칙을 코드로 명확하게 표현할 수 있고, 머신러닝 모델의 성능을 높이는 유용한 피처를 빠르게 생성할 수 있으며, 복잡한 도메인 로직도 유지보수하기 쉬운 선언적 코드로 작성할 수 있습니다.
실무에서는 여러 단계의 변환을 메서드 체이닝으로 연결하여 파이프라인을 구축하고, 각 변환 단계를 함수로 분리하여 재사용성을 높이며, 생성된 피처의 분포를 확인하여 이상치나 오류를 검증합니다.
실전 팁
💡 복잡한 계산은 중간 결과를 여러 컬럼으로 나누면 디버깅과 이해가 쉬워집니다
💡 when-then 체인에서 마지막 otherwise()를 생략하면 매칭되지 않는 행은 null이 되므로, 항상 명시하는 것이 안전합니다
💡 날짜 연산은 dt 네임스페이스를 활용하세요 - pl.col("date").dt.year(), dt.month() 등으로 쉽게 추출할 수 있습니다
💡 문자열 연산도 str 네임스페이스로 가능합니다 - pl.col("name").str.to_uppercase(), str.slice() 등
💡 여러 컬럼을 조합한 복잡한 피처는 struct()로 묶어서 처리한 후 struct.field()로 다시 펼칠 수 있습니다
7. 결측치 처리하기 - 데이터 완성도 높이기
시작하며
여러분이 머신러닝 모델을 학습시키려는데 이런 상황을 겪어본 적 있나요? 데이터의 일부 값이 비어있어서 모델이 에러를 발생시키거나, 분석 결과가 왜곡되는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 실제 세계의 데이터는 불완전하며, 센서 오류, 사용자 입력 누락, 시스템 장애 등 다양한 이유로 결측치가 발생합니다.
결측치를 제대로 처리하지 않으면 통계가 왜곡되고, 머신러닝 모델의 성능이 저하되며, 비즈니스 의사결정이 잘못될 수 있습니다. 바로 이럴 때 필요한 것이 체계적인 결측치 처리입니다.
Polars는 다양한 결측치 처리 전략을 제공하여, 데이터의 특성과 분석 목적에 맞게 결측치를 처리할 수 있게 해줍니다.
개요
간단히 말해서, 결측치 처리는 비어있는 데이터를 삭제하거나, 적절한 값으로 채우거나, 별도로 표시하여 데이터의 완성도를 높이는 작업입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 대부분의 분석 도구와 머신러닝 알고리즘은 결측치를 처리하지 못하고, 결측치가 많으면 샘플 크기가 줄어들어 통계적 신뢰도가 떨어집니다.
예를 들어, 고객 설문 데이터에서 연령대 정보가 30% 누락되어 있다면, 그냥 삭제하면 데이터의 70%만 사용하게 되어 비효율적입니다. 기존에는 pandas에서 fillna(), dropna()를 사용했지만 복잡한 조건부 처리가 어려웠다면, 이제는 Polars의 표현식 시스템으로 조건에 따라 다른 전략을 적용하는 유연한 처리가 가능합니다.
Polars는 null 처리가 명시적이고 안전하며, 앞/뒤 값으로 채우기(forward/backward fill), 보간(interpolation) 등 다양한 전략을 지원하고, 조건부 채우기도 표현식으로 간결하게 작성할 수 있다는 특징이 있습니다. 이러한 특징들이 데이터 품질 관리의 정확성과 효율성을 향상시킵니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"age": [25, None, 35, None, 45],
"income": [50000, 60000, None, 80000, None],
"city": ["Seoul", None, "Busan", "Seoul", None]
})
# 전략 1: 결측치가 있는 행 삭제
df_dropped = df.drop_nulls()
# 전략 2: 특정 값으로 채우기
df_filled = df.with_columns([
pl.col("age").fill_null(pl.col("age").mean()), # 평균으로 채우기
pl.col("income").fill_null(0), # 0으로 채우기
pl.col("city").fill_null("Unknown") # 고정값으로 채우기
])
# 전략 3: 앞의 값으로 채우기 (시계열 데이터에 유용)
df_forward = df.with_columns([
pl.col("city").fill_null(strategy="forward")
])
# 전략 4: 조건부 채우기
df_conditional = df.with_columns([
pl.when(pl.col("age").is_null() & (pl.col("income") > 70000))
.then(40) # 고소득자는 40세로 가정
.otherwise(pl.col("age"))
.alias("age")
])
print(df_filled)
설명
이것이 하는 일: 위 코드는 결측치를 발견하고, 데이터의 특성과 분석 목적에 맞는 여러 전략으로 처리합니다. 첫 번째로, drop_nulls()는 가장 단순한 방법으로, null이 하나라도 있는 행을 완전히 제거합니다.
이 메서드는 데이터 손실이 크지만, 결측치 비율이 낮고 데이터가 충분할 때는 가장 깔끔한 방법입니다. 이렇게 하는 이유는 불완전한 데이터로 인한 편향을 완전히 제거하기 위함입니다.
그 다음으로, fill_null() 메서드가 다양한 방식으로 결측치를 채웁니다. pl.col("age").mean()으로 계산된 평균값을 사용하면 통계적 중심 경향을 유지할 수 있으며, 수치형 데이터에 적합합니다.
내부적으로는 먼저 평균을 계산한 후, null 위치만 그 값으로 대체합니다. 고정값(0, "Unknown")으로 채우는 것은 결측치를 명시적으로 표시하거나, 비즈니스 규칙에 따른 기본값을 설정할 때 유용합니다.
Forward fill(strategy="forward")은 시계열 데이터나 순서가 있는 데이터에서 자주 사용됩니다. 이전 행의 값을 그대로 가져와 채우는 방식으로, "마지막으로 알려진 값이 계속 유지된다"는 가정을 기반으로 합니다.
예를 들어, 센서 데이터에서 측정 실패 시 이전 측정값을 사용하는 경우입니다. 조건부 채우기는 when-then-otherwise로 복잡한 비즈니스 로직을 반영합니다.
"소득이 높은 사람은 나이가 많을 것"이라는 도메인 지식을 활용하여, 결측치를 보다 정확하게 추정할 수 있습니다. 이는 단순 평균보다 더 정교한 전략입니다.
여러분이 이 코드를 사용하면 데이터 손실을 최소화하면서 분석을 진행할 수 있고, 머신러닝 모델의 학습 데이터를 충분히 확보할 수 있으며, 비즈니스 규칙을 반영한 합리적인 결측치 처리를 할 수 있습니다. 실무에서는 결측치 처리 전후의 통계를 비교하여 편향이 발생하지 않았는지 검증하고, 중요한 컬럼의 결측치는 도메인 전문가와 상의하여 처리 방법을 결정하며, 결측치 패턴을 분석하여 데이터 수집 과정의 문제를 개선합니다.
실전 팁
💡 fill_null(strategy="backward")도 가능하며, 미래 값으로 채우는 것이 더 적절한 경우에 사용하세요
💡 보간법이 필요하면 interpolate()를 사용할 수 있지만, 수치형 데이터에만 적용 가능합니다
💡 결측치 처리 전에 null_count()로 각 컬럼의 결측치 비율을 확인하여 전략을 결정하세요 - 50% 이상이면 삭제 고려
💡 머신러닝에서는 결측치 자체가 정보일 수 있으므로, col.is_null().cast(pl.Int8) 같은 이진 피처를 추가하는 것도 효과적입니다
💡 coalesce([col1, col2, default_value])를 사용하면 여러 컬럼을 순서대로 확인하여 첫 번째 non-null 값을 선택할 수 있습니다
8. 데이터 정렬하기 - 순서 있는 인사이트
시작하며
여러분이 판매 실적 리포트를 작성할 때 이런 상황을 겪어본 적 있나요? 가장 많이 팔린 제품이나 매출이 높은 지역을 찾으려는데, 데이터가 무작위로 섞여 있어서 한눈에 파악하기 어려운 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 정렬되지 않은 데이터는 패턴을 찾기 어렵고, 상위/하위 항목을 파악하기 위해 추가 작업이 필요하며, 리포트의 가독성이 떨어집니다.
특히 대용량 데이터에서는 정렬이 분석의 효율성을 크게 좌우합니다. 바로 이럴 때 필요한 것이 효율적인 데이터 정렬입니다.
Polars는 단일 컬럼은 물론 복수 컬럼 정렬, null 값 처리 등 다양한 정렬 옵션을 제공하여, 데이터를 원하는 순서로 빠르게 배열할 수 있게 해줍니다.
개요
간단히 말해서, 데이터 정렬은 하나 이상의 컬럼을 기준으로 행의 순서를 오름차순 또는 내림차순으로 재배열하는 작업입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 순위를 매기거나, 최댓값/최솟값을 찾거나, 시계열 데이터를 시간 순으로 배열하거나, 리포트를 보기 좋게 정리할 때 필수적입니다.
예를 들어, "매출 상위 10개 제품"을 찾으려면 매출 기준 내림차순 정렬 후 상위 10개를 선택하면 됩니다. 기존에는 pandas에서 sort_values()를 사용했지만 대용량 데이터에서 느렸다면, 이제는 Polars의 병렬 정렬 알고리즘으로 수백만 행도 빠르게 정렬할 수 있습니다.
Polars는 정렬을 병렬로 처리하여 빠르고, 여러 컬럼을 기준으로 복합 정렬을 지원하며, null 값의 위치(앞/뒤)를 명시적으로 제어할 수 있다는 특징이 있습니다. 이러한 특징들이 데이터 분석과 리포팅의 효율성을 크게 향상시킵니다.
코드 예제
import polars as pl
df = pl.DataFrame({
"product": ["A", "B", "C", "D", "E"],
"sales": [1000, 1500, 800, 1500, 1200],
"profit": [200, 300, 150, 350, 250],
"region": ["Seoul", "Busan", "Seoul", "Busan", "Incheon"]
})
# 단일 컬럼 정렬 - 매출 내림차순
sorted_by_sales = df.sort("sales", descending=True)
# 복수 컬럼 정렬 - 매출 내림차순, 같으면 이익 내림차순
sorted_multi = df.sort(
["sales", "profit"],
descending=[True, True]
)
# 표현식 기반 정렬 - 이익률 기준
sorted_by_ratio = df.sort(
pl.col("profit") / pl.col("sales"),
descending=True
)
# 상위 N개 추출
top_3_products = df.sort("sales", descending=True).head(3)
print("매출 상위 3개 제품:")
print(top_3_products)
설명
이것이 하는 일: 위 코드는 다양한 기준으로 데이터를 정렬하여 순위를 매기고, 중요한 항목을 빠르게 찾아냅니다. 첫 번째로, sort("sales", descending=True)는 sales 컬럼을 기준으로 내림차순 정렬을 수행합니다.
descending=False(기본값)는 오름차순이며, True는 내림차순입니다. 이렇게 하는 이유는 매출이 높은 제품부터 보기 위함이며, 비즈니스 리포트에서 가장 자주 사용되는 패턴입니다.
그 다음으로, 복수 컬럼 정렬이 실행됩니다. sort(["sales", "profit"], descending=[True, True])는 먼저 sales로 정렬하고, sales가 같은 경우 profit으로 추가 정렬합니다.
내부적으로는 안정 정렬(stable sort)을 사용하여, 동일한 값의 상대적 순서를 유지합니다. 각 컬럼마다 다른 정렬 방향을 지정할 수 있어([True, False]), 복잡한 정렬 요구사항도 만족시킬 수 있습니다.
표현식 기반 정렬은 계산된 값을 기준으로 정렬합니다. pl.col("profit") / pl.col("sales")는 이익률을 계산하며, 이 값으로 정렬하면 수익성이 높은 제품을 찾을 수 있습니다.
별도의 컬럼을 미리 만들 필요 없이, 정렬 시점에 동적으로 계산하여 메모리를 절약합니다. 마지막으로, sort().head(3)를 체이닝하여 상위 3개만 추출합니다.
이는 "top-N" 쿼리의 기본 패턴이며, 대시보드나 리포트에서 핵심 지표만 보여줄 때 유용합니다. 여러분이 이 코드를 사용하면 몇 초 만에 중요한 항목을 찾을 수 있고, 리포트를 논리적인 순서로 정리하여 가독성을 높일 수 있으며, 시계열 데이터를 시간 순으로 배열하여 추세를 파악할 수 있습니다.
실무에서는 정렬 후 with_row_count()로 순위 컬럼을 추가하여 명시적인 랭킹을 만들고, bottom(N) 메서드로 하위 항목도 분석하며, 대용량 데이터는 Lazy API에서 정렬하여 메모리 효율을 높입니다.
실전 팁
💡 null 값의 위치를 제어하려면 nulls_last=True 옵션을 사용하세요 - 기본적으로 null은 가장 작은 값으로 취급됩니다
💡 top_k(N, by="column")를 사용하면 전체 정렬 없이 상위 N개만 효율적으로 추출할 수 있어 대용량 데이터에 유용합니다
💡 문자열 정렬은 기본적으로 유니코드 순서이므로, 자연스러운 순서가 필요하면 별도 처리가 필요합니다 (예: "item1", "item10", "item2" 순서 문제)
💡 시계열 데이터는 항상 날짜 컬럼으로 먼저 정렬한 후 분석을 시작하세요 - 순서가 뒤죽박죽이면 추세 분석이 불가능합니다
💡 정렬이 자주 필요하면 인덱스를 고려하거나, Lazy API에서 maintain_order=False로 성능을 높일 수 있습니다
9. 데이터 샘플링하기 - 대용량 데이터 다루기
시작하며
여러분이 수백만 행의 데이터로 분석을 시작하려는데 이런 상황을 겪어본 적 있나요? 전체 데이터를 로드하면 메모리가 부족하거나, 간단한 탐색 작업도 몇 분씩 걸려서 답답한 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 빅데이터 시대에는 전체 데이터를 한 번에 처리하기 어려운 경우가 많고, 초기 탐색이나 프로토타이핑 단계에서는 작은 샘플로도 충분합니다.
전체 데이터로 작업하면 개발 속도가 느려지고, 반복 실험이 어려워집니다. 바로 이럴 때 필요한 것이 효율적인 데이터 샘플링입니다.
Polars는 무작위 샘플링, 비율 기반 샘플링, 계층적 샘플링 등 다양한 방법을 제공하여, 대표성 있는 부분 데이터로 빠르게 작업할 수 있게 해줍니다.
개요
간단히 말해서, 데이터 샘플링은 전체 데이터에서 일부를 추출하여 분석, 테스트, 프로토타이핑에 활용하는 작업입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 대용량 데이터의 전체 처리는 시간과 자원이 많이 들고, 초기 탐색이나 알고리즘 검증에는 샘플로도 충분하며, 균형 잡힌 샘플은 전체 데이터의 특성을 잘 대표할 수 있습니다.
예를 들어, 머신러닝 모델 개발 시 먼저 10% 샘플로 빠르게 실험한 후, 최종 모델만 전체 데이터로 학습하면 개발 시간을 크게 단축할 수 있습니다. 기존에는 pandas에서 sample()을 사용했지만 대용량 데이터에서는 전체를 로드해야 했다면, 이제는 Polars의 Lazy API와 함께 사용하여 디스크에서 샘플만 읽어올 수 있습니다.
Polars는 무작위 샘플링이 빠르고, 재현 가능한 랜덤 시드를 지원하며, 개수 또는 비율로 유연하게 샘플링할 수 있다는 특징이 있습니다. 이러한 특징들이 대용량 데이터 분석의 효율성과 실험의 재현성을 보장합니다.
코드 예제
import polars as pl
df = pl.read_csv("large_dataset.csv")
# 고정 개수 샘플링 - 1000개 행 추출
sample_n = df.sample(n=1000, seed=42)
# 비율 기반 샘플링 - 10% 추출
sample_frac = df.sample(fraction=0.1, seed=42)
# 중복 허용 샘플링 (부트스트랩)
bootstrap_sample = df.sample(n=len(df), with_replacement=True, seed=42)
# 계층적 샘플링 - 각 지역에서 동일 비율로 추출
stratified_sample = df.group_by("region").map_groups(
lambda group: group.sample(fraction=0.1, seed=42)
)
# 학습/테스트 분할
train_size = int(len(df) * 0.8)
shuffled = df.sample(fraction=1.0, shuffle=True, seed=42)
train_df = shuffled.head(train_size)
test_df = shuffled.tail(len(df) - train_size)
print(f"전체: {len(df)}, 샘플: {len(sample_frac)}")
print(f"학습: {len(train_df)}, 테스트: {len(test_df)}")
설명
이것이 하는 일: 위 코드는 다양한 샘플링 전략으로 대용량 데이터에서 대표성 있는 부분을 추출하여 효율적인 분석을 가능하게 합니다. 첫 번째로, sample(n=1000, seed=42)는 정확히 1000개의 행을 무작위로 선택합니다.
seed 매개변수는 난수 생성기의 시드를 고정하여, 같은 코드를 여러 번 실행해도 동일한 샘플을 얻을 수 있게 합니다. 이렇게 하는 이유는 실험의 재현성을 보장하고, 팀원들과 동일한 샘플을 공유하기 위함입니다.
그 다음으로, 비율 기반 샘플링(fraction=0.1)이 실행됩니다. 내부적으로는 전체 행 수의 10%를 계산한 후, 그만큼의 행을 무작위로 선택합니다.
데이터 크기가 변해도 항상 일정 비율을 유지하므로, 스크립트를 수정하지 않고 다양한 데이터셋에 재사용할 수 있습니다. 부트스트랩 샘플링(with_replacement=True)은 중복을 허용하여 샘플링합니다.
즉, 같은 행이 여러 번 선택될 수 있으며, 통계적 추정의 불확실성을 평가하거나, 앙상블 머신러닝 기법에서 활용됩니다. 계층적 샘플링은 group_by()와 map_groups()를 조합하여 각 그룹에서 동일한 비율로 샘플링합니다.
예를 들어, 지역별로 데이터 크기가 다르더라도, 각 지역에서 10%씩 추출하면 모든 지역이 샘플에 포함되어 편향을 방지할 수 있습니다. 학습/테스트 분할은 먼저 전체 데이터를 섞은 후(shuffle=True), 80%는 학습용, 20%는 테스트용으로 나눕니다.
이는 머신러닝에서 가장 기본적인 검증 전략입니다. 여러분이 이 코드를 사용하면 대용량 데이터를 로컬 환경에서도 빠르게 탐색할 수 있고, 알고리즘을 빠르게 프로토타이핑하여 개발 주기를 단축할 수 있으며, 재현 가능한 실험으로 과학적인 분석을 수행할 수 있습니다.
실무에서는 초기 EDA(탐색적 데이터 분석)는 1% 샘플로 빠르게 수행하고, 모델 개발은 10% 샘플로 반복한 후, 최종 검증만 전체 데이터로 수행하여 시간을 절약하며, 클라우드 환경에서는 샘플을 로컬로 다운받아 비용을 절감합니다.
실전 팁
💡 Lazy API와 함께 사용하면 scan_csv().sample().collect()로 샘플만 디스크에서 읽어와 메모리를 크게 절약할 수 있습니다
💡 시계열 데이터는 무작위 샘플링보다 연속된 기간을 추출하는 것이 패턴 파악에 유리할 수 있습니다
💡 샘플의 통계적 특성(describe())을 전체 데이터와 비교하여 대표성을 검증하세요
💡 클래스 불균형 데이터는 계층적 샘플링으로 소수 클래스가 충분히 포함되도록 하세요
💡 seed를 설정하지 않으면 매번 다른 샘플이 추출되므로, 실험 재현성이 중요하면 반드시 고정하세요
10. 데이터 조인하기 - 여러 데이터 연결하기
시작하며
여러분이 고객 정보와 주문 정보가 별도의 테이블에 저장되어 있을 때 이런 상황을 겪어본 적 있나요? 고객별 주문 금액을 분석하려는데, 두 테이블을 연결해야 해서 복잡하고 에러가 발생하는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 실무 데이터는 정규화된 데이터베이스처럼 여러 테이블로 분산되어 있고, 의미 있는 분석을 위해서는 이들을 결합해야 합니다.
고객 정보, 주문 내역, 제품 정보가 각각 다른 파일에 있다면, 조인 없이는 통합 분석이 불가능합니다. 바로 이럴 때 필요한 것이 효율적인 데이터 조인입니다.
Polars는 SQL과 유사하지만 더 빠른 조인 연산을 제공하여, 수백만 행의 데이터도 빠르게 결합할 수 있게 해줍니다.
개요
간단히 말해서, 데이터 조인은 공통 키(key)를 기준으로 두 개 이상의 데이터셋을 수평으로 결합하는 작업입니다. 이 과정이 왜 필요한지 실무 관점에서 설명하자면, 관계형 데이터베이스의 정규화된 테이블들을 분석 가능한 형태로 결합해야 하고, 여러 소스의 데이터를 통합하여 360도 관점의 인사이트를 얻어야 합니다.
예를 들어, 고객 테이블의 지역 정보와 주문 테이블의 금액 정보를 조인하면 "지역별 고객당 평균 주문 금액" 같은 분석이 가능해집니다. 기존에는 pandas에서 merge()를 사용했지만 대용량 데이터에서 메모리 부족이 발생했다면, 이제는 Polars의 해시 조인 알고리즘으로 효율적으로 결합할 수 있습니다.
Polars는 조인을 병렬로 처리하여 빠르고, inner/left/right/full/cross 등 모든 조인 타입을 지원하며, 여러 컬럼을 기준으로 조인할 수 있다는 특징이 있습니다. 이러한 특징들이 복잡한 데이터 통합 작업의 성능과 유연성을 크게 향상시킵니다.
코드 예제
import polars as pl
# 고객 정보
customers = pl.DataFrame({
"customer_id": [1, 2, 3, 4],
"name": ["Alice", "Bob", "Charlie", "Diana"],
"region": ["Seoul", "Busan", "Seoul", "Incheon"]
})
# 주문 정보
orders = pl.DataFrame({
"order_id": [101, 102, 103, 104, 105],
"customer_id": [1, 2, 1, 3, 99], # 99는 고객 테이블에 없음
"amount": [1000, 1500, 800, 2000, 500]
})
# Inner Join - 양쪽에 모두 있는 데이터만
inner_joined = customers.join(orders, on="customer_id", how="inner")
# Left Join - 왼쪽(customers) 모두 유지, 오른쪽은 매칭되는 것만
left_joined = customers.join(orders, on="customer_id", how="left")
# 복수 컬럼 조인
products = pl.DataFrame({
"product_id": [1, 2, 3],
"category": ["A", "B", "A"],
"price": [100, 200, 150]
})
order_details = pl.DataFrame({
"product_id": [1, 2, 1],
"category": ["A", "B", "A"],
"quantity": [2, 1, 3]
})
multi_key_join = products.join(
order_details,
on=["product_id", "category"],
how="inner"
)
print("Inner Join 결과:")
print(inner_joined)
설명
이것이 하는 일: 위 코드는 공통 키를 기준으로 여러 데이터셋을 결합하여, 분산된 정보를 하나의 통합 뷰로 만듭니다. 첫 번째로, join(orders, on="customer_id", how="inner")는 inner join을 수행합니다.
이는 양쪽 테이블에 모두 존재하는 customer_id만 결과에 포함시킵니다. customer_id가 99인 주문은 고객 테이블에 없으므로 제외되고, customer_id가 4인 Diana는 주문이 없으므로 제외됩니다.
이렇게 하는 이유는 완전한 정보가 있는 레코드만 분석하기 위함입니다. 그 다음으로, left join(how="left")이 실행됩니다.
내부적으로는 왼쪽 테이블(customers)의 모든 행을 유지하고, 오른쪽 테이블(orders)에서 매칭되는 행을 붙입니다. Diana처럼 주문이 없는 고객도 결과에 포함되지만, 주문 정보 컬럼은 null로 채워집니다.
이는 "모든 고객의 주문 현황"을 분석할 때 유용하며, 주문이 없는 고객도 식별할 수 있습니다. 복수 컬럼 조인은 on=["product_id", "category"]처럼 리스트를 전달합니다.
이는 두 컬럼의 조합이 모두 일치하는 경우만 결합하며, 단일 키로는 고유성이 보장되지 않는 경우에 사용됩니다. 예를 들어, 같은 product_id라도 다른 category면 다른 제품으로 취급합니다.
Polars의 조인은 해시 조인 알고리즘을 사용하여, 오른쪽 테이블을 해시 테이블로 변환한 후 왼쪽 테이블을 스캔하며 매칭합니다. 이는 중첩 루프보다 훨씬 빠르며, 대용량 데이터에 적합합니다.
여러분이 이 코드를 사용하면 여러 소스의 데이터를 통합하여 종합적인 분석을 할 수 있고, 관계형 데이터베이스의 정규화된 스키마를 분석 가능한 플랫 구조로 변환할 수 있으며, 데이터 품질 문제(매칭되지 않는 레코드)를 발견할 수 있습니다. 실무에서는 조인 전에 키 컬럼의 고유성을 검증하고(n_unique()), 조인 후 행 수 변화를 확인하여 예상치 못한 중복을 방지하며, 대용량 조인은 Lazy API에서 수행하여 메모리를 절약합니다.
실전 팁
💡 조인 타입 선택: inner(완전 일치만), left(왼쪽 모두 유지), right(오른쪽 모두 유지), outer(양쪽 모두 유지), cross(모든 조합)
💡 조인 키 컬럼명이 다르면 left_on과 right_on을 따로 지정할 수 있습니다 - join(df2, left_on="id", right_on="customer_id")
💡 validate="1:1" 옵션으로 일대일 관계를 강제하여, 의도치 않은 중복을 방지할 수 있습니다
💡 여러 테이블을 조인할 때는 작은 테이블부터 순차적으로 조인하면 메모리 효율이 좋습니다
💡 조인 후 select()로 필요한 컬럼만 선택하여 불필요한 데이터를 제거하세요