이미지 로딩 중...
AI Generated
2025. 11. 14. · 0 Views
Polars 데이터 변환 및 파생 변수 생성 완벽 가이드
Polars를 활용한 효율적인 데이터 변환과 파생 변수 생성 방법을 배웁니다. select, with_columns, 표현식 체이닝 등 실무에서 바로 활용할 수 있는 핵심 기법들을 다룹니다.
목차
- select로 필요한 컬럼만 선택하기 - 효율적인 데이터 추출의 시작
- with_columns로 새로운 컬럼 추가하기 - 원본 유지하며 확장하기
- alias로 컬럼명 지정하기 - 명확한 이름으로 가독성 향상
- cast로 데이터 타입 변환하기 - 안전하고 정확한 타입 관리
- when-then-otherwise로 조건부 로직 구현하기 - SQL CASE문의 강력한 대안
- filter로 조건에 맞는 행 필터링하기 - 원하는 데이터만 정확히 추출
- str 네임스페이스로 문자열 처리하기 - 텍스트 데이터 변환의 핵심
- 표현식 체이닝으로 복잡한 변환 연결하기 - 코드 간결성과 성능의 조화
- agg로 그룹별 집계하기 - 그룹 통계의 핵심
- sort로 데이터 정렬하기 - 순서를 통한 인사이트 발견
1. select로 필요한 컬럼만 선택하기 - 효율적인 데이터 추출의 시작
시작하며
여러분이 수백 개의 컬럼을 가진 대용량 데이터를 분석할 때 이런 상황을 겪어본 적 있나요? 필요한 건 고작 5개 컬럼인데, 전체 데이터를 메모리에 올리느라 시스템이 느려지고 분석 속도가 답답할 정도로 느린 상황 말입니다.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 대용량 CSV 파일이나 데이터베이스에서 데이터를 가져올 때, 불필요한 컬럼까지 모두 로드하면 메모리 낭비는 물론이고 처리 시간도 기하급수적으로 증가합니다.
바로 이럴 때 필요한 것이 select 메서드입니다. Polars의 select를 사용하면 필요한 컬럼만 정확히 선택하여 메모리 효율을 극대화하고, 쿼리 최적화를 통해 처리 속도를 획기적으로 개선할 수 있습니다.
개요
간단히 말해서, select는 DataFrame에서 원하는 컬럼만 골라내는 강력한 도구입니다. SQL의 SELECT 문과 비슷하지만, 훨씬 더 유연하고 표현력이 풍부합니다.
왜 이 기능이 필요한지 실무 관점에서 설명하자면, 데이터 분석 프로젝트에서는 전체 데이터의 10-20% 정도만 실제로 사용하는 경우가 대부분입니다. 예를 들어, 고객 구매 데이터에서 매출 분석을 할 때 고객의 상세 주소나 연락처 정보는 필요 없는 경우가 많습니다.
기존 Pandas에서는 df[['col1', 'col2']] 형태로 컬럼을 선택했다면, Polars의 select는 표현식(expression)을 활용하여 컬럼 선택과 동시에 변환까지 수행할 수 있습니다. 이는 코드의 가독성과 성능을 동시에 향상시킵니다.
select의 핵심 특징은 첫째, Lazy Evaluation을 지원하여 실제 필요할 때까지 연산을 지연시킬 수 있고, 둘째, 표현식 체이닝으로 여러 변환을 한 번에 처리할 수 있으며, 셋째, 자동 최적화로 쿼리 플랜을 개선한다는 점입니다. 이러한 특징들이 대용량 데이터 처리에서 Polars를 Pandas보다 월등히 빠르게 만드는 핵심 요소입니다.
코드 예제
import polars as pl
# 샘플 데이터 생성
df = pl.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"name": ["김철수", "이영희", "박민수", "정수진", "최동훈"],
"age": [25, 30, 35, 28, 42],
"city": ["서울", "부산", "대구", "인천", "광주"],
"purchase_amount": [50000, 75000, 120000, 45000, 98000]
})
# 필요한 컬럼만 선택
result = df.select([
pl.col("customer_id"), # 고객 ID
pl.col("age"), # 나이
pl.col("purchase_amount") # 구매 금액
])
print(result)
설명
이것이 하는 일: select 메서드는 DataFrame에서 지정한 컬럼들만 추출하여 새로운 DataFrame을 생성합니다. 원본 데이터는 변경되지 않으며, 선택된 컬럼들로만 구성된 새로운 뷰를 만들어냅니다.
첫 번째로, pl.col("컬럼명") 표현식을 사용하여 각 컬럼을 지정합니다. 이 방식은 단순히 문자열로 컬럼명을 전달하는 것보다 훨씬 강력한데, 왜냐하면 pl.col()은 표현식 객체를 반환하기 때문에 이후에 다양한 메서드를 체이닝하여 변환을 추가할 수 있기 때문입니다.
그 다음으로, select 메서드에 컬럼 표현식들의 리스트를 전달하면 Polars는 내부적으로 쿼리 최적화를 수행합니다. Lazy 모드에서는 실제로 데이터를 읽기 전에 어떤 컬럼이 필요한지 미리 파악하여, 파일이나 데이터베이스에서 해당 컬럼만 로드하는 최적화를 진행합니다.
마지막으로, 선택된 컬럼들로만 구성된 새로운 DataFrame이 반환되어 메모리 사용량이 크게 줄어듭니다. 예를 들어 100개 컬럼 중 3개만 선택하면 메모리 사용량이 거의 1/30 수준으로 감소할 수 있습니다.
여러분이 이 코드를 사용하면 대용량 데이터셋에서도 빠른 응답 속도를 유지할 수 있고, 메모리 부족 에러를 방지할 수 있으며, 코드의 의도가 명확해져 유지보수가 쉬워집니다. 특히 데이터 파이프라인에서 초기 단계에 select를 적용하면 이후 모든 연산의 성능이 향상되는 효과를 얻을 수 있습니다.
실전 팁
💡 문자열로도 컬럼을 선택할 수 있지만(df.select(["col1", "col2"])), pl.col()을 사용하면 이후 변환 메서드를 체이닝할 수 있어 더 강력합니다.
💡 pl.col("*")를 사용하면 모든 컬럼을 선택할 수 있고, pl.col("^regex$")로 정규표현식 패턴 매칭도 가능합니다.
💡 대용량 CSV 파일을 읽을 때는 pl.scan_csv()와 select를 조합하여 필요한 컬럼만 로드하면 메모리를 90% 이상 절약할 수 있습니다.
💡 select 안에서 pl.col("amount").sum() 같은 집계 함수를 사용하면 컬럼 선택과 집계를 동시에 수행할 수 있습니다.
💡 여러 컬럼에 같은 변환을 적용할 때는 pl.col(["col1", "col2", "col3"]).cast(pl.Float64) 형태로 한 번에 처리할 수 있습니다.
2. with_columns로 새로운 컬럼 추가하기 - 원본 유지하며 확장하기
시작하며
여러분이 데이터 분석을 하다가 기존 컬럼들을 조합해서 새로운 지표를 만들어야 할 때 이런 고민을 해본 적 있나요? "원본 데이터는 그대로 두고 싶은데, 새로운 컬럼을 추가하려면 어떻게 해야 하지?" 이런 문제는 실제 개발 현장에서 매우 빈번하게 발생합니다.
예를 들어, 매출 데이터에 '전월 대비 증가율', '카테고리별 순위', '이상치 플래그' 같은 파생 변수를 추가해야 하는데, 원본 컬럼들은 모두 유지해야 하는 경우입니다. 바로 이럴 때 필요한 것이 with_columns 메서드입니다.
기존 DataFrame의 모든 컬럼을 유지하면서 새로운 컬럼을 효율적으로 추가할 수 있어, 데이터 파이프라인 구축의 핵심 도구가 됩니다.
개요
간단히 말해서, with_columns는 기존 DataFrame에 새로운 컬럼을 추가하거나 기존 컬럼을 업데이트하는 메서드입니다. 원본의 모든 컬럼은 그대로 유지되며, 지정한 컬럼만 추가되거나 수정됩니다.
왜 이 메서드가 필요한지 실무 관점에서 설명하자면, 데이터 분석에서는 단계적으로 파생 변수를 만들어가는 경우가 대부분입니다. 예를 들어, 먼저 일별 매출을 계산하고, 그 다음 주간 평균을 구하고, 마지막으로 이상치를 탐지하는 식으로 점진적으로 컬럼을 추가해 나갑니다.
Pandas에서는 df['new_col'] = ... 형태로 컬럼을 추가했다면, Polars의 with_columns는 불변성(immutability) 원칙을 따르면서도 체이닝 가능한 API를 제공합니다.
이는 코드의 예측 가능성을 높이고 버그를 줄여줍니다. with_columns의 핵심 특징은 첫째, 한 번에 여러 컬럼을 동시에 추가할 수 있고, 둘째, 메서드 체이닝으로 여러 단계의 변환을 연결할 수 있으며, 셋째, 표현식 내에서 기존 컬럼을 자유롭게 참조하여 계산할 수 있다는 점입니다.
이러한 특징들이 복잡한 데이터 변환 로직을 간결하고 읽기 쉽게 만들어줍니다.
코드 예제
import polars as pl
# 매출 데이터
df = pl.DataFrame({
"product": ["노트북", "마우스", "키보드", "모니터", "헤드셋"],
"price": [1200000, 35000, 89000, 450000, 120000],
"quantity": [5, 50, 30, 10, 25]
})
# 여러 파생 변수를 동시에 생성
result = df.with_columns([
# 총 매출액 계산
(pl.col("price") * pl.col("quantity")).alias("total_sales"),
# 가격 구간 분류
pl.when(pl.col("price") >= 1000000).then("고가")
.when(pl.col("price") >= 100000).then("중가")
.otherwise("저가").alias("price_category"),
# 재고 가치
(pl.col("price") * pl.col("quantity") * 1.1).alias("inventory_value")
])
print(result)
설명
이것이 하는 일: with_columns 메서드는 원본 DataFrame의 구조를 유지하면서 새로운 컬럼들을 추가한 확장된 DataFrame을 반환합니다. 원본 데이터는 변경되지 않으며, 모든 기존 컬럼과 새로 추가된 컬럼을 포함하는 새로운 객체가 생성됩니다.
첫 번째로, 각 컬럼 표현식에 .alias("컬럼명")을 붙여 새 컬럼의 이름을 지정합니다. 예를 들어 (pl.col("price") * pl.col("quantity")).alias("total_sales")는 가격과 수량을 곱한 값을 "total_sales"라는 이름의 새 컬럼으로 추가합니다.
alias를 생략하면 Polars가 자동으로 이름을 생성하지만, 명시적으로 지정하는 것이 가독성 면에서 훨씬 좋습니다. 그 다음으로, pl.when().then().otherwise() 구조를 사용하여 조건부 로직을 구현할 수 있습니다.
이는 SQL의 CASE WHEN 문과 유사하며, 가격 범위에 따라 "고가", "중가", "저가"로 분류하는 등의 복잡한 비즈니스 로직을 표현할 수 있습니다. 여러 조건을 체이닝하여 다단계 분류도 가능합니다.
마지막으로, with_columns는 한 번의 호출로 여러 컬럼을 동시에 생성하므로 성능이 최적화됩니다. Polars는 내부적으로 병렬 처리를 통해 각 컬럼 계산을 동시에 수행하며, 전체 DataFrame을 한 번만 스캔하여 효율성을 극대화합니다.
여러분이 이 코드를 사용하면 복잡한 파생 변수 생성 로직을 선언적이고 읽기 쉬운 형태로 작성할 수 있고, 원본 데이터의 무결성을 유지하면서 안전하게 데이터를 확장할 수 있으며, 대용량 데이터에서도 빠른 처리 속도를 보장받을 수 있습니다. 특히 여러 단계의 Feature Engineering이 필요한 머신러닝 프로젝트에서 매우 유용합니다.
실전 팁
💡 같은 이름의 컬럼을 with_columns에 전달하면 해당 컬럼이 업데이트되므로, 기존 컬럼 수정도 가능합니다.
💡 with_columns를 여러 번 체이닝하는 것보다 한 번에 여러 컬럼을 전달하는 것이 성능상 유리합니다(내부 최적화 때문).
💡 복잡한 계산은 먼저 변수로 빼내어 가독성을 높이세요: sales_expr = pl.col("price") * pl.col("quantity")
💡 pl.col("*").suffix("_old")를 사용하면 모든 기존 컬럼에 접미사를 붙여 백업 컬럼을 만들 수 있습니다.
💡 에러가 발생할 수 있는 계산(예: 0으로 나누기)은 pl.when(pl.col("divisor") != 0).then(...).otherwise(None)로 안전하게 처리하세요.
3. alias로 컬럼명 지정하기 - 명확한 이름으로 가독성 향상
시작하며
여러분이 복잡한 계산식을 작성했는데, 결과 컬럼의 이름이 "literal"이나 "column_0" 같은 의미 없는 이름으로 나타나서 당황한 적 있나요? 나중에 코드를 다시 봤을 때 어떤 컬럼이 무엇을 의미하는지 전혀 알 수 없어서 다시 분석해야 했던 경험 말입니다.
이런 문제는 실제 개발 현장에서 심각한 유지보수 이슈로 이어집니다. 특히 팀 프로젝트에서 다른 개발자가 작성한 코드를 이해해야 할 때, 컬럼명이 불명확하면 전체 로직을 파악하는 데 몇 배의 시간이 소요됩니다.
바로 이럴 때 필요한 것이 alias 메서드입니다. 모든 표현식의 결과에 의미 있는 이름을 부여하여 코드의 자기 문서화를 실현하고, 협업 효율을 극대화할 수 있습니다.
개요
간단히 말해서, alias는 표현식의 결과로 생성되는 컬럼에 원하는 이름을 지정하는 메서드입니다. 계산식이 아무리 복잡하더라도 최종 결과물에는 명확하고 의미 있는 이름을 부여할 수 있습니다.
왜 이 기능이 필요한지 실무 관점에서 설명하자면, 데이터 분석 코드는 한 번 작성하고 끝나는 것이 아니라 계속해서 수정되고 재사용됩니다. 예를 들어, 3개월 전에 작성한 매출 분석 코드를 다시 실행해야 하는데, 컬럼명이 "column_0", "column_1" 같은 식이라면 각 컬럼이 무엇을 의미하는지 다시 추적해야 하는 불편함이 생깁니다.
기존에는 계산 후 df.rename()을 사용하여 컬럼명을 변경했다면, Polars에서는 표현식을 작성하는 시점에 바로 .alias()를 붙여 이름을 지정할 수 있습니다. 이는 코드의 응집도를 높이고, 계산 로직과 이름 지정이 한 곳에 있어 이해하기 쉽습니다.
alias의 핵심 특징은 첫째, 모든 표현식에 체이닝하여 사용할 수 있고, 둘째, 중복된 이름을 지정하면 해당 컬럼을 덮어쓰게 되며, 셋째, 한글 컬럼명도 지원하여 비즈니스 용어를 그대로 사용할 수 있다는 점입니다. 이러한 특징들이 코드를 비개발자도 이해할 수 있는 수준으로 만들어줍니다.
코드 예제
import polars as pl
# 고객 구매 데이터
df = pl.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"purchase_count": [3, 7, 2, 15, 5],
"total_amount": [150000, 420000, 80000, 1200000, 350000],
"days_since_join": [30, 180, 15, 720, 90]
})
# 의미 있는 이름으로 파생 변수 생성
result = df.select([
pl.col("customer_id"),
# 평균 구매 금액
(pl.col("total_amount") / pl.col("purchase_count")).alias("avg_purchase_amount"),
# 일 평균 구매 빈도
(pl.col("purchase_count") / pl.col("days_since_join")).alias("daily_purchase_rate"),
# 고객 등급 (VIP, 일반, 신규)
pl.when(pl.col("total_amount") >= 1000000).then("VIP")
.when(pl.col("days_since_join") >= 60).then("일반")
.otherwise("신규").alias("customer_grade")
])
print(result)
설명
이것이 하는 일: alias 메서드는 표현식이 생성하는 컬럼의 이름을 지정합니다. 복잡한 계산식의 결과를 단순히 "계산 결과"가 아닌 비즈니스 의미를 담은 이름으로 표현할 수 있게 해줍니다.
첫 번째로, 수학적 연산의 결과에 alias를 붙이면 해당 계산이 무엇을 의미하는지 명확해집니다. 예를 들어 (pl.col("total_amount") / pl.col("purchase_count"))만 보면 단순한 나눗셈이지만, .alias("avg_purchase_amount")를 붙이면 "평균 구매 금액"이라는 비즈니스 의미가 즉시 드러납니다.
이는 코드 리뷰나 유지보수 시 이해도를 크게 향상시킵니다. 그 다음으로, 조건부 로직의 결과에도 alias를 사용하여 분류나 플래그의 의미를 명확히 할 수 있습니다.
pl.when().then().otherwise() 체인의 결과가 무엇을 나타내는지 alias 없이는 알 수 없지만, .alias("customer_grade")를 붙이면 이것이 고객 등급을 나타낸다는 것을 코드만 보고도 즉시 파악할 수 있습니다. 마지막으로, alias는 표현식 체이닝의 마지막에 위치하여 전체 계산 과정의 최종 결과물을 명명합니다.
여러 메서드를 체이닝한 복잡한 표현식이라도 마지막에 .alias()를 붙이면 전체 계산의 목적이 한눈에 들어오게 됩니다. 여러분이 이 코드를 사용하면 몇 개월 후에 코드를 다시 봐도 각 컬럼의 의미를 즉시 이해할 수 있고, 팀원들과의 코드 리뷰가 훨씬 수월해지며, 버그를 발견하고 수정하는 시간이 크게 단축됩니다.
특히 데이터 분석 보고서를 작성할 때 컬럼명을 그대로 사용할 수 있어 추가 변환 작업이 필요 없습니다.
실전 팁
💡 컬럼명은 snake_case(예: avg_purchase_amount)를 사용하면 Python 변수명 규칙과 일치하여 일관성이 높아집니다.
💡 한글 컬럼명도 가능하지만, SQL 연동이나 파일 저장 시 인코딩 문제가 있을 수 있으니 영문을 권장합니다.
💡 기존 컬럼과 같은 이름으로 alias를 지정하면 해당 컬럼을 덮어쓰므로, 원본 보존이 필요하면 다른 이름을 사용하세요.
💡 너무 긴 이름보다는 명확하면서도 간결한 이름이 좋습니다(30자 이내 권장).
💡 팀 내에서 컬럼 네이밍 컨벤션을 정해두면(예: is_로 시작하면 boolean, _at으로 끝나면 timestamp) 일관성이 높아집니다.
4. cast로 데이터 타입 변환하기 - 안전하고 정확한 타입 관리
시작하며
여러분이 CSV 파일을 불러왔는데 숫자 컬럼이 문자열로 인식되어서 계산이 안 되거나, 날짜 컬럼이 텍스트로 저장되어 날짜 연산이 불가능했던 경험이 있나요? 혹은 모델 학습을 위해 정수형 데이터를 실수형으로 변환해야 하는데 어떻게 해야 할지 막막했던 순간 말입니다.
이런 문제는 실제 개발 현장에서 데이터 파이프라인의 초기 단계에서 가장 빈번하게 발생하는 이슈 중 하나입니다. 잘못된 데이터 타입은 계산 오류, 성능 저하, 심지어 런타임 에러까지 야기할 수 있어 반드시 초기에 해결해야 합니다.
바로 이럴 때 필요한 것이 cast 메서드입니다. 명시적으로 데이터 타입을 변환하여 예측 가능하고 안전한 데이터 처리를 보장하며, 메모리 효율까지 최적화할 수 있습니다.
개요
간단히 말해서, cast는 컬럼의 데이터 타입을 다른 타입으로 명시적으로 변환하는 메서드입니다. 문자열을 숫자로, 정수를 실수로, 날짜를 문자열로 등 다양한 타입 변환을 안전하게 수행합니다.
왜 이 메서드가 필요한지 실무 관점에서 설명하자면, 외부 데이터를 가져올 때 타입 추론이 항상 정확한 것은 아닙니다. 예를 들어, "123"이라는 값이 있는 컬럼을 Polars가 문자열로 인식했지만 실제로는 제품 코드가 아닌 수량이라면, 명시적으로 정수형으로 변환해야 합니다.
또한 머신러닝 모델에 데이터를 입력할 때 타입이 정확해야 오류가 발생하지 않습니다. 기존 Pandas에서는 df.astype()을 사용했다면, Polars의 cast는 표현식 체이닝의 일부로 사용되어 더 유연합니다.
또한 lazy 모드에서는 타입 변환을 쿼리 최적화 단계에서 처리하여 성능을 향상시킵니다. cast의 핵심 특징은 첫째, 변환 실패 시 에러를 발생시키거나 null로 처리하는 옵션을 제공하고, 둘째, 다양한 데이터 타입을 지원하며(정수, 실수, 문자열, 날짜, Boolean 등), 셋째, 타입 변환 시 메모리 최적화를 자동으로 수행한다는 점입니다.
이러한 특징들이 안전하고 효율적인 데이터 처리를 가능하게 합니다.
코드 예제
import polars as pl
# 타입이 섞인 데이터
df = pl.DataFrame({
"product_id": ["101", "102", "103", "104", "105"], # 문자열이지만 실제는 숫자
"price": ["1200.50", "350.00", "890.75", "450.25", "1200.00"], # 문자열 실수
"is_available": ["1", "0", "1", "1", "0"], # 문자열 boolean
"stock_count": [50.0, 120.0, 30.0, 75.0, 200.0] # 실수지만 실제는 정수
})
# 적절한 타입으로 변환
result = df.select([
# 문자열 → 정수
pl.col("product_id").cast(pl.Int64).alias("product_id"),
# 문자열 → 실수
pl.col("price").cast(pl.Float64).alias("price"),
# 문자열 → Boolean
pl.col("is_available").cast(pl.Int8).cast(pl.Boolean).alias("is_available"),
# 실수 → 정수 (소수점 버림)
pl.col("stock_count").cast(pl.Int32).alias("stock_count")
])
print(result)
print(result.dtypes) # 타입 확인
설명
이것이 하는 일: cast 메서드는 컬럼의 데이터 타입을 지정한 타입으로 변환합니다. 변환이 가능하면 새로운 타입의 데이터를 반환하고, 불가능하면 에러를 발생시키거나(strict 모드) null을 반환합니다(lenient 모드).
첫 번째로, 문자열로 저장된 숫자 데이터를 실제 숫자 타입으로 변환하는 것이 가장 흔한 사용 케이스입니다. CSV 파일은 모든 데이터를 텍스트로 저장하기 때문에, pl.col("price").cast(pl.Float64)처럼 명시적으로 실수형으로 변환해야 수학 연산이 가능합니다.
변환 시 Polars는 내부적으로 파싱을 수행하여 "1200.50" 같은 문자열을 1200.5라는 실수값으로 변환합니다. 그 다음으로, 정밀도 조정을 위한 타입 변환도 중요합니다.
예를 들어 재고 수량은 소수점이 없는 정수이므로 Float64보다 Int32를 사용하면 메모리를 절반으로 줄일 수 있습니다. 또한 0과 1로만 구성된 컬럼은 Boolean 타입으로 변환하면 메모리를 1/8로 줄일 수 있어 대용량 데이터에서 큰 효과를 냅니다.
마지막으로, 체이닝을 통해 다단계 변환도 가능합니다. 예를 들어 "1"이라는 문자열을 Boolean으로 변환하려면 먼저 .cast(pl.Int8)로 정수로 변환한 후 .cast(pl.Boolean)으로 불린으로 변환하는 2단계 과정이 필요합니다.
이렇게 명시적으로 단계를 나누면 변환 과정이 투명해지고 디버깅이 쉬워집니다. 여러분이 이 코드를 사용하면 데이터 타입 불일치로 인한 런타임 에러를 사전에 방지할 수 있고, 메모리 사용량을 최적화하여 대용량 데이터도 효율적으로 처리할 수 있으며, 코드의 의도가 명확해져 다른 개발자도 쉽게 이해할 수 있습니다.
특히 데이터 검증 단계에서 cast를 사용하면 잘못된 데이터를 조기에 발견할 수 있습니다.
실전 팁
💡 strict=False 옵션을 사용하면 변환 실패 시 에러 대신 null을 반환하여 안전하게 처리할 수 있습니다: cast(pl.Int64, strict=False)
💡 날짜 문자열을 날짜 타입으로 변환할 때는 pl.col("date").str.strptime(pl.Date, "%Y-%m-%d")를 사용하세요.
💡 큰 숫자가 필요 없다면 Int64 대신 Int32, Float64 대신 Float32를 사용하여 메모리를 절약할 수 있습니다.
💡 타입 변환 전에 pl.col("column").is_null().sum()으로 null 값을 확인하면 변환 오류를 예방할 수 있습니다.
💡 카테고리형 데이터는 pl.Categorical로 변환하면 메모리를 크게 줄이고 그룹 연산 속도도 향상됩니다.
5. when-then-otherwise로 조건부 로직 구현하기 - SQL CASE문의 강력한 대안
시작하며
여러분이 데이터를 분석하다가 특정 조건에 따라 다른 값을 할당해야 하는 상황을 만난 적 있나요? 예를 들어, 나이에 따라 연령대를 분류하거나, 매출액에 따라 등급을 매기거나, 여러 조건을 조합하여 복잡한 비즈니스 로직을 구현해야 하는 경우 말입니다.
이런 문제는 실제 개발 현장에서 거의 모든 데이터 분석 프로젝트에서 발생합니다. 단순한 if-else로는 벡터화된 연산을 수행할 수 없고, 반복문을 사용하면 성능이 너무 느려서 대용량 데이터 처리가 불가능합니다.
바로 이럴 때 필요한 것이 when-then-otherwise 표현식입니다. SQL의 CASE WHEN 문처럼 조건부 로직을 벡터화하여 수백만 행의 데이터도 빠르게 처리하며, 가독성까지 뛰어난 코드를 작성할 수 있습니다.
개요
간단히 말해서, pl.when().then().otherwise()는 조건에 따라 다른 값을 반환하는 조건부 표현식입니다. 여러 조건을 체이닝하여 복잡한 분기 로직도 명확하게 표현할 수 있습니다.
왜 이 표현식이 필요한지 실무 관점에서 설명하자면, 데이터 분석에서는 범주화, 등급 부여, 플래그 생성 등 조건부 로직이 매우 빈번하게 사용됩니다. 예를 들어, RFM 분석에서 고객의 구매 빈도와 금액을 기준으로 VIP, 우수, 일반, 이탈 위험 등으로 분류하는 것이 대표적입니다.
기존 Pandas에서는 np.where()나 apply() 함수를 사용했다면, Polars의 when-then-otherwise는 더 직관적이고 성능도 우수합니다. SQL을 사용해본 분이라면 CASE WHEN 문과 거의 동일한 구조라 쉽게 이해할 수 있습니다.
when-then-otherwise의 핵심 특징은 첫째, 여러 when을 체이닝하여 다중 조건을 처리할 수 있고, 둘째, 조건식 내에서 복잡한 논리 연산(AND, OR, NOT)을 사용할 수 있으며, 셋째, 벡터화된 연산으로 대용량 데이터도 빠르게 처리한다는 점입니다. 이러한 특징들이 복잡한 비즈니스 로직을 효율적으로 구현할 수 있게 해줍니다.
코드 예제
import polars as pl
# 고객 데이터
df = pl.DataFrame({
"customer_id": [1, 2, 3, 4, 5, 6],
"age": [25, 35, 45, 55, 65, 28],
"total_purchase": [500000, 3000000, 1500000, 800000, 5000000, 200000],
"purchase_count": [5, 30, 15, 10, 50, 2]
})
# 복잡한 조건부 로직으로 고객 세그먼트 분류
result = df.with_columns([
# 연령대 분류
pl.when(pl.col("age") < 30).then("20대")
.when(pl.col("age") < 40).then("30대")
.when(pl.col("age") < 50).then("40대")
.when(pl.col("age") < 60).then("50대")
.otherwise("60대 이상").alias("age_group"),
# 고객 등급 (다중 조건 조합)
pl.when((pl.col("total_purchase") >= 5000000) & (pl.col("purchase_count") >= 30)).then("VIP")
.when((pl.col("total_purchase") >= 2000000) | (pl.col("purchase_count") >= 20)).then("골드")
.when(pl.col("total_purchase") >= 1000000).then("실버")
.otherwise("일반").alias("customer_grade")
])
print(result)
설명
이것이 하는 일: when-then-otherwise 표현식은 각 행을 순회하면서 조건을 평가하고, 조건이 참인 경우 then에 지정된 값을, 모든 조건이 거짓이면 otherwise의 값을 반환합니다. 이 과정이 벡터화되어 매우 빠르게 처리됩니다.
첫 번째로, pl.when(조건)으로 첫 번째 조건을 평가합니다. 예를 들어 pl.when(pl.col("age") < 30)은 나이가 30 미만인 행들을 찾아냅니다.
조건식에는 비교 연산자(<, >, ==, !=), 논리 연산자(&는 AND, |는 OR, ~는 NOT)를 모두 사용할 수 있어 복잡한 조건도 표현 가능합니다. 그 다음으로, .then(값)으로 조건이 참일 때 반환할 값을 지정합니다.
이 값은 리터럴("20대" 같은 고정값)일 수도 있고, 다른 컬럼의 값이나 계산식일 수도 있습니다. 여러 when을 체이닝하면 if-elif-else 구조를 만들 수 있으며, Polars는 위에서부터 순차적으로 조건을 평가하여 첫 번째로 참인 조건의 값을 반환합니다.
마지막으로, .otherwise(값)로 모든 when 조건이 거짓일 때의 기본값을 지정합니다. otherwise를 생략하면 조건에 맞지 않는 행은 null이 됩니다.
복잡한 비즈니스 로직의 경우, 여러 조건을 AND(&)나 OR(|)로 결합할 수 있는데, 예를 들어 (pl.col("total_purchase") >= 5000000) & (pl.col("purchase_count") >= 30)는 두 조건을 모두 만족해야 합니다. 여러분이 이 코드를 사용하면 복잡한 분류 로직을 명확하고 읽기 쉬운 형태로 작성할 수 있고, 대용량 데이터에서도 빠른 처리 속도를 보장받으며, SQL 경험이 있다면 즉시 익숙하게 사용할 수 있습니다.
특히 고객 세그멘테이션, A/B 테스트 그룹 할당, 이상치 탐지 플래그 생성 등에서 매우 유용합니다.
실전 팁
💡 논리 연산자를 사용할 때는 각 조건을 괄호로 감싸야 합니다: (조건1) & (조건2), 그렇지 않으면 연산 우선순위 오류가 발생합니다.
💡 and, or 대신 &, | 연산자를 사용해야 합니다(Python의 and/or는 벡터 연산에서 작동하지 않음).
💡 여러 when 중 첫 번째로 참인 조건만 적용되므로, 조건의 순서가 중요합니다. 더 구체적인 조건을 먼저 배치하세요.
💡 복잡한 조건은 미리 변수로 빼서 가독성을 높이세요: vip_condition = (pl.col("amount") >= 5000000) & (pl.col("count") >= 30)
💡 otherwise를 생략하면 null이 되므로, 명시적으로 기본값을 지정하는 것이 안전합니다.
6. filter로 조건에 맞는 행 필터링하기 - 원하는 데이터만 정확히 추출
시작하며
여러분이 전체 데이터셋에서 특정 조건을 만족하는 데이터만 분석해야 하는 상황을 만난 적 있나요? 예를 들어, 전체 판매 데이터 중 특정 지역의 데이터만, 특정 기간의 데이터만, 또는 특정 금액 이상의 거래만 추출해야 하는 경우 말입니다.
이런 문제는 실제 개발 현장에서 데이터 분석의 가장 기본이 되는 작업입니다. 전체 데이터를 대상으로 분석하면 왜곡된 결과가 나올 수 있고, 불필요한 데이터까지 처리하느라 성능이 크게 저하됩니다.
바로 이럴 때 필요한 것이 filter 메서드입니다. SQL의 WHERE 절처럼 조건을 지정하여 원하는 데이터만 정확히 추출하고, 이후 모든 연산의 효율을 극대화할 수 있습니다.
개요
간단히 말해서, filter는 조건식을 만족하는 행만 남기고 나머지는 제거하는 메서드입니다. DataFrame의 크기를 줄여 메모리와 연산 속도를 모두 개선합니다.
왜 이 메서드가 필요한지 실무 관점에서 설명하자면, 대부분의 분석은 전체 데이터가 아닌 특정 부분집합을 대상으로 수행됩니다. 예를 들어, 월별 매출 분석을 할 때 해당 월의 데이터만 필터링하거나, 특정 제품 카테고리의 성과만 분석하거나, 이상치를 제거한 정상 데이터만 사용하는 경우가 대표적입니다.
Pandas에서는 df[df['column'] > value] 형태의 Boolean 인덱싱을 사용했다면, Polars의 filter는 더 명시적이고 최적화된 방식을 제공합니다. Lazy 모드에서는 필터 조건을 쿼리 최적화에 활용하여 파일에서 데이터를 읽을 때부터 필요한 행만 로드합니다.
filter의 핵심 특징은 첫째, 여러 조건을 AND/OR로 결합하여 복잡한 필터링이 가능하고, 둘째, 메서드 체이닝으로 여러 단계의 필터를 순차적으로 적용할 수 있으며, 셋째, Predicate Pushdown으로 데이터 소스 레벨에서 필터링하여 성능을 극대화한다는 점입니다. 이러한 특징들이 대용량 데이터 처리를 가능하게 합니다.
코드 예제
import polars as pl
from datetime import date
# 판매 데이터
df = pl.DataFrame({
"order_id": [1, 2, 3, 4, 5, 6, 7, 8],
"product": ["노트북", "마우스", "키보드", "모니터", "노트북", "헤드셋", "키보드", "마우스"],
"amount": [1200000, 35000, 89000, 450000, 1500000, 120000, 95000, 42000],
"region": ["서울", "부산", "서울", "대구", "서울", "인천", "부산", "서울"],
"date": [date(2024, 1, 15), date(2024, 1, 20), date(2024, 2, 5),
date(2024, 2, 10), date(2024, 2, 15), date(2024, 2, 20),
date(2024, 3, 1), date(2024, 3, 5)]
})
# 복잡한 필터링: 서울 지역 + 100만원 이상 + 2월 데이터
result = df.filter(
(pl.col("region") == "서울") & # 서울 지역
(pl.col("amount") >= 1000000) & # 100만원 이상
(pl.col("date").dt.month() == 2) # 2월 데이터
)
print(result)
print(f"전체 {len(df)}건 중 {len(result)}건 필터링됨")
설명
이것이 하는 일: filter 메서드는 각 행에 대해 조건식을 평가하여 True를 반환하는 행만 유지하고, False인 행은 제거합니다. 결과적으로 조건을 만족하는 부분집합 DataFrame이 생성됩니다.
첫 번째로, 조건식은 Boolean 결과를 반환하는 표현식이어야 합니다. 예를 들어 pl.col("amount") >= 1000000은 각 행의 amount 값이 100만 이상인지 확인하여 True/False를 반환합니다.
비교 연산자(==, !=, <, >, <=, >=)를 모두 사용할 수 있으며, 문자열 비교도 가능합니다. 그 다음으로, 여러 조건을 결합할 때는 &(AND), |(OR), ~(NOT) 연산자를 사용합니다.
각 조건은 반드시 괄호로 감싸야 연산 우선순위 오류를 방지할 수 있습니다. 예를 들어 (pl.col("region") == "서울") & (pl.col("amount") >= 1000000)은 두 조건을 모두 만족하는 행만 선택합니다.
마지막으로, dt 네임스페이스를 활용하면 날짜/시간 컬럼에 대한 복잡한 필터링도 가능합니다. pl.col("date").dt.month() == 2는 날짜 컬럼에서 월을 추출하여 2월 데이터만 필터링합니다.
이외에도 .dt.year(), .dt.day(), .dt.weekday() 등 다양한 날짜 메서드를 사용할 수 있습니다. 여러분이 이 코드를 사용하면 분석에 필요한 데이터만 정확히 추출하여 결과의 정확성을 높일 수 있고, 불필요한 데이터를 제거하여 메모리와 연산 시간을 크게 절약할 수 있으며, 복잡한 비즈니스 요구사항도 명확한 조건식으로 표현할 수 있습니다.
특히 대용량 데이터에서 초기 단계에 filter를 적용하면 이후 모든 연산의 성능이 향상됩니다.
실전 팁
💡 여러 filter를 체이닝하는 것보다 하나의 filter에 조건을 결합하는 것이 성능상 유리합니다: filter((조건1) & (조건2))
💡 문자열 패턴 매칭은 pl.col("name").str.contains("패턴")을 사용하세요(정규표현식 지원).
💡 null 값을 필터링하려면 pl.col("column").is_null() 또는 pl.col("column").is_not_null()을 사용하세요.
💡 리스트 안의 값에 포함되는지 확인하려면 pl.col("category").is_in(["전자", "가전", "컴퓨터"])를 사용하세요.
💡 Lazy 모드에서 filter를 먼저 적용하면 Predicate Pushdown으로 파일 읽기 단계부터 필터링되어 성능이 크게 향상됩니다.
7. str 네임스페이스로 문자열 처리하기 - 텍스트 데이터 변환의 핵심
시작하며
여러분이 고객 이름 데이터를 정제하거나, URL에서 도메인을 추출하거나, 이메일 주소의 유효성을 검증해야 하는 상황을 만난 적 있나요? 혹은 대소문자가 섞여 있는 텍스트를 통일하거나, 앞뒤 공백을 제거하거나, 특정 패턴을 찾아 치환해야 하는 경우 말입니다.
이런 문제는 실제 개발 현장에서 텍스트 데이터를 다룰 때 반드시 직면하게 됩니다. 특히 외부에서 수집한 데이터는 형식이 일관되지 않아 정제 작업이 필수적이며, 이 과정에서 문자열 처리 기능이 핵심적인 역할을 합니다.
바로 이럴 때 필요한 것이 str 네임스페이스입니다. Polars의 str 네임스페이스는 Python의 문자열 메서드와 유사하면서도 벡터화되어 있어, 수백만 행의 텍스트 데이터도 빠르게 처리할 수 있습니다.
개요
간단히 말해서, str 네임스페이스는 문자열 컬럼에 대한 다양한 변환 메서드를 제공하는 특수한 접근자입니다. pl.col("문자열컬럼").str.메서드() 형태로 사용하며, 대소문자 변환, 치환, 추출, 분할 등 모든 문자열 작업을 수행할 수 있습니다.
왜 이 네임스페이스가 필요한지 실무 관점에서 설명하자면, 실제 데이터는 거의 항상 정제가 필요합니다. 예를 들어, 사용자가 입력한 이름 데이터는 "홍길동", " 홍길동", "홍길동 ", "HONG" 등 다양한 형태로 존재할 수 있어, 분석 전에 소문자 변환, 공백 제거 등의 정규화 작업이 필수입니다.
Pandas에서는 df['column'].str.메서드() 형태를 사용했다면, Polars도 거의 동일한 인터페이스를 제공하여 학습 곡선이 낮습니다. 하지만 Polars는 내부적으로 Rust로 구현되어 있어 Pandas보다 훨씬 빠른 성능을 보여줍니다.
str 네임스페이스의 핵심 특징은 첫째, 50개 이상의 다양한 문자열 메서드를 제공하고, 둘째, 정규표현식을 완벽히 지원하여 복잡한 패턴 매칭이 가능하며, 셋째, 모든 연산이 벡터화되어 대용량 데이터도 빠르게 처리한다는 점입니다. 이러한 특징들이 텍스트 데이터 처리를 효율적으로 만들어줍니다.
코드 예제
import polars as pl
# 정제가 필요한 고객 데이터
df = pl.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"name": [" 홍길동", "김철수 ", " 이영희 ", "PARK MIN SU", "정수진"],
"email": ["hong@example.com", "KIM@EXAMPLE.COM", "lee@example.co.kr",
"park@company.com", "invalid-email"],
"phone": ["010-1234-5678", "01012345678", "010 1234 5678", "010-9876-5432", "010.1111.2222"]
})
# 문자열 정제 및 변환
result = df.with_columns([
# 앞뒤 공백 제거 + 대문자로 변환
pl.col("name").str.strip_chars().str.to_uppercase().alias("name_clean"),
# 이메일 소문자 변환
pl.col("email").str.to_lowercase().alias("email_clean"),
# 이메일 도메인 추출
pl.col("email").str.extract(r"@(.+)$", 1).alias("email_domain"),
# 전화번호 정규화 (숫자만 추출)
pl.col("phone").str.replace_all(r"[^0-9]", "").alias("phone_clean"),
# 이메일 유효성 검증
pl.col("email").str.contains(r"^[\w\.-]+@[\w\.-]+\.\w+$").alias("is_valid_email")
])
print(result)
설명
이것이 하는 일: str 네임스페이스는 문자열 컬럼의 각 값에 대해 지정한 문자열 연산을 벡터화하여 수행합니다. Python의 문자열 메서드와 유사하지만, 전체 컬럼에 대해 한 번에 적용되어 반복문보다 수백 배 빠릅니다.
첫 번째로, 기본적인 정제 메서드들이 있습니다. .str.strip_chars()는 앞뒤 공백을 제거하고, .str.to_lowercase()와 .str.to_uppercase()는 대소문자를 변환합니다.
이러한 메서드들은 체이닝하여 연속적으로 적용할 수 있어, pl.col("name").str.strip_chars().str.to_uppercase()처럼 여러 정제 작업을 한 줄로 처리할 수 있습니다. 그 다음으로, 정규표현식 기반 메서드들이 매우 강력합니다.
.str.extract(r"패턴", 그룹번호)는 정규표현식 패턴에 매칭되는 부분을 추출하며, .str.replace_all(r"패턴", "치환값")은 패턴에 맞는 모든 부분을 치환합니다. 예를 들어 .str.replace_all(r"[^0-9]", "")는 숫자가 아닌 모든 문자를 제거하여 전화번호를 정규화하는 데 사용됩니다.
마지막으로, .str.contains(r"패턴")는 문자열이 패턴을 포함하는지 Boolean으로 반환하여 유효성 검증에 활용됩니다. 예를 들어 이메일 형식 검증을 위해 ^[\w.-]+@[\w.-]+.\w+$ 패턴을 사용하면, 올바른 이메일 형식인지 확인할 수 있습니다.
이를 filter와 결합하면 유효하지 않은 데이터를 자동으로 걸러낼 수 있습니다. 여러분이 이 코드를 사용하면 일관되지 않은 텍스트 데이터를 표준화하여 분석 정확도를 높일 수 있고, 수백만 행의 텍스트를 몇 초 안에 처리하여 생산성을 극대화할 수 있으며, 정규표현식으로 복잡한 패턴도 정확하게 추출하거나 검증할 수 있습니다.
특히 데이터 정제(Data Cleaning) 단계에서 str 네임스페이스는 필수 도구입니다.
실전 팁
💡 정규표현식 패턴은 raw string(r"패턴")으로 작성하면 백슬래시 이스케이프 문제를 방지할 수 있습니다.
💡 .str.lengths()로 문자열 길이를 계산하여 이상치(너무 짧거나 긴 값)를 탐지할 수 있습니다.
💡 .str.split("구분자")로 문자열을 분할하면 리스트 컬럼이 생성되며, .list.get(0)으로 첫 번째 요소를 추출할 수 있습니다.
💡 null 값이 있는 컬럼에 str 메서드를 사용하면 null은 그대로 유지되므로 안전합니다.
💡 .str.slice(시작, 길이)로 부분 문자열을 추출할 수 있습니다(예: 전화번호 앞 3자리 추출).
8. 표현식 체이닝으로 복잡한 변환 연결하기 - 코드 간결성과 성능의 조화
시작하며
여러분이 데이터를 변환할 때 여러 단계의 작업을 수행해야 하는 상황을 만난 적 있나요? 예를 들어, 문자열을 정제하고, 숫자로 변환하고, 조건에 따라 분류하고, 결과에 이름을 붙이는 등 5-6단계의 변환을 순차적으로 적용해야 하는 경우 말입니다.
이런 문제는 실제 개발 현장에서 복잡한 Feature Engineering을 할 때 매우 흔합니다. 각 단계를 별도의 코드로 작성하면 중간 결과를 저장하느라 메모리가 낭비되고, 코드가 길어져 가독성이 떨어집니다.
바로 이럴 때 필요한 것이 표현식 체이닝입니다. Polars의 표현식은 메서드를 연속으로 체이닝하여 여러 변환을 하나의 흐름으로 표현할 수 있어, 코드가 간결해지고 성능도 향상됩니다.
개요
간단히 말해서, 표현식 체이닝은 하나의 표현식에 여러 메서드를 .으로 연결하여 순차적으로 변환을 적용하는 기법입니다. pl.col("column").메서드1().메서드2().메서드3() 형태로 작성하며, 각 메서드의 결과가 다음 메서드의 입력이 됩니다.
왜 이 기법이 필요한지 실무 관점에서 설명하자면, 데이터 변환은 대부분 여러 단계로 구성됩니다. 예를 들어, 가격 문자열 "$1,200.50"을 숫자로 변환하려면 1) $ 기호 제거, 2) 콤마 제거, 3) 실수형 변환의 3단계가 필요합니다.
이를 별도의 컬럼으로 만들면 중간 결과가 메모리를 차지하고 코드도 복잡해집니다. Pandas에서는 각 단계를 별도로 실행하거나 apply() 함수를 사용했다면, Polars의 체이닝은 선언적이면서도 성능이 우수합니다.
Polars는 체이닝된 표현식을 분석하여 최적화된 실행 계획을 생성하므로, 단계별로 나눈 것보다 오히려 빠릅니다. 표현식 체이닝의 핵심 특징은 첫째, 중간 결과를 메모리에 저장하지 않아 효율적이고, 둘째, 코드가 선언적이어서 읽기 쉬우며, 셋째, Polars의 쿼리 최적화가 자동으로 적용된다는 점입니다.
이러한 특징들이 복잡한 변환을 우아하게 표현할 수 있게 해줍니다.
코드 예제
import polars as pl
# 정제가 필요한 판매 데이터
df = pl.DataFrame({
"product_id": [1, 2, 3, 4, 5],
"product_name": [" LAPTOP ", "mouse", " KEYBOARD ", "Monitor", " headset "],
"price_str": ["$1,200.50", "$35.00", "$89.99", "$450.00", "$120.75"],
"stock_str": ["50개", "120개", "30개", "75개", "200개"],
"category": ["전자|컴퓨터", "전자|주변기기", "전자|주변기기", "전자|컴퓨터", "전자|주변기기"]
})
# 복잡한 변환을 체이닝으로 연결
result = df.select([
pl.col("product_id"),
# 제품명 정제: 공백제거 → 소문자 → 첫글자 대문자
pl.col("product_name")
.str.strip_chars()
.str.to_lowercase()
.str.to_titlecase()
.alias("product_name_clean"),
# 가격 변환: $ 제거 → 콤마 제거 → 실수형 변환
pl.col("price_str")
.str.replace_all(r"[$,]", "")
.cast(pl.Float64)
.alias("price"),
# 재고 변환: 숫자만 추출 → 정수형 변환 → 재고 상태 분류
pl.col("stock_str")
.str.replace_all(r"[^0-9]", "")
.cast(pl.Int32)
.pipe(lambda col:
pl.when(col >= 100).then("충분")
.when(col >= 50).then("보통")
.otherwise("부족"))
.alias("stock_status"),
# 카테고리에서 서브카테고리 추출
pl.col("category")
.str.split("|")
.list.get(1)
.alias("sub_category")
])
print(result)
설명
이것이 하는 일: 표현식 체이닝은 각 메서드의 출력이 다음 메서드의 입력으로 전달되는 파이프라인을 만듭니다. 전체 과정이 하나의 표현식으로 표현되어 Polars가 최적화할 수 있는 여지가 커집니다.
첫 번째로, 문자열 변환 체이닝의 예시를 보면 .str.strip_chars().str.to_lowercase().str.to_titlecase()처럼 세 단계의 변환이 연결됩니다. 먼저 앞뒤 공백이 제거되고, 그 결과가 소문자로 변환되며, 마지막으로 각 단어의 첫 글자가 대문자로 변환됩니다.
이 모든 과정이 한 번의 컬럼 스캔으로 처리되어 효율적입니다. 그 다음으로, 타입 변환과 조건부 로직을 결합할 수도 있습니다.
.str.replace_all(r"[^0-9]", "").cast(pl.Int32)로 문자열에서 숫자만 추출하여 정수로 변환한 후, .pipe()를 사용하여 해당 값을 조건부 로직에 전달할 수 있습니다. pipe는 현재까지의 표현식 결과를 함수에 전달하는 고급 기법으로, 복잡한 로직을 모듈화할 때 유용합니다.
마지막으로, 리스트 타입 컬럼의 처리도 체이닝으로 가능합니다. .str.split("|")로 문자열을 분할하면 리스트 컬럼이 생성되고, .list.get(1)로 두 번째 요소(인덱스 1)를 추출할 수 있습니다.
이렇게 str, list, dt 등 다양한 네임스페이스를 체이닝하여 복잡한 변환도 간결하게 표현할 수 있습니다. 여러분이 이 코드를 사용하면 복잡한 데이터 변환을 5-10줄로 간결하게 작성할 수 있고, 중간 변수 없이 한 번의 흐름으로 처리하여 메모리 효율이 높아지며, 코드의 의도가 명확하게 드러나 유지보수가 쉬워집니다.
특히 ETL(Extract, Transform, Load) 파이프라인에서 체이닝은 필수 기법입니다.
실전 팁
💡 체이닝이 길어지면 가독성을 위해 백슬래시()로 여러 줄로 나누거나 괄호로 감싸세요.
💡 .pipe(함수)를 사용하면 커스텀 변환 로직을 체이닝에 삽입할 수 있어 재사용성이 높아집니다.
💡 체이닝 중간에 .print()를 넣으면 디버깅 시 중간 결과를 확인할 수 있습니다(개발 모드에서만 사용).
💡 너무 복잡한 체이닝은 오히려 가독성을 해치므로, 5-6단계 이상이면 with_columns를 여러 번 사용하는 것을 고려하세요.
💡 각 메서드가 null을 어떻게 처리하는지 확인하세요. 대부분 null을 유지하지만, 일부는 에러를 발생시킬 수 있습니다.
9. agg로 그룹별 집계하기 - 그룹 통계의 핵심
시작하며
여러분이 카테고리별 매출 합계, 지역별 평균 나이, 제품별 판매 수량 등 그룹별로 집계된 통계를 계산해야 하는 상황을 만난 적 있나요? 전체 데이터의 평균이나 합계가 아니라, 특정 기준으로 그룹을 나눈 후 각 그룹의 통계를 구해야 하는 경우 말입니다.
이런 문제는 실제 개발 현장에서 데이터 분석의 가장 핵심적인 작업 중 하나입니다. 비즈니스 인사이트는 대부분 세분화된 그룹별 지표에서 나오며, "전체 매출"보다 "제품 카테고리별 매출 증감"이 훨씬 가치 있는 정보입니다.
바로 이럴 때 필요한 것이 group_by()와 agg() 조합입니다. SQL의 GROUP BY와 유사하지만 훨씬 유연하며, 한 번에 여러 집계를 동시에 수행할 수 있어 강력합니다.
개요
간단히 말해서, agg는 group_by()로 그룹화된 데이터에 대해 집계 함수를 적용하는 메서드입니다. 각 그룹별로 합계, 평균, 최댓값, 최솟값, 개수 등 다양한 통계를 계산할 수 있습니다.
왜 이 메서드가 필요한지 실무 관점에서 설명하자면, 비즈니스 질문은 대부분 그룹별 비교 형태입니다. 예를 들어, "어떤 지역의 매출이 가장 높은가?", "어떤 연령대가 가장 많이 구매하는가?", "요일별 평균 주문 금액은 얼마인가?" 같은 질문들은 모두 그룹별 집계로 답할 수 있습니다.
Pandas에서는 df.groupby().agg() 형태를 사용했다면, Polars도 거의 동일한 인터페이스를 제공하지만 성능은 월등히 빠릅니다. 특히 대용량 데이터에서 멀티 스레드 병렬 처리로 그룹별 집계를 수행하여 Pandas보다 10-100배 빠른 경우도 있습니다.
agg의 핵심 특징은 첫째, 한 번의 agg 호출로 여러 집계를 동시에 수행할 수 있고, 둘째, 각 컬럼에 서로 다른 집계 함수를 적용할 수 있으며, 셋째, 표현식을 활용하여 집계 전후로 변환을 추가할 수 있다는 점입니다. 이러한 특징들이 복잡한 그룹별 분석을 간결하게 만들어줍니다.
코드 예제
import polars as pl
# 판매 데이터
df = pl.DataFrame({
"date": ["2024-01-15", "2024-01-15", "2024-01-16", "2024-01-16", "2024-01-17", "2024-01-17"],
"category": ["전자", "가전", "전자", "가전", "전자", "가전"],
"product": ["노트북", "냉장고", "마우스", "세탁기", "키보드", "TV"],
"quantity": [5, 2, 50, 3, 30, 1],
"price": [1200000, 1500000, 35000, 800000, 89000, 2000000],
"region": ["서울", "부산", "서울", "대구", "서울", "부산"]
})
# 카테고리별 다양한 집계
result = df.group_by("category").agg([
pl.col("quantity").sum().alias("total_quantity"), # 총 판매 수량
pl.col("price").mean().alias("avg_price"), # 평균 가격
pl.col("price").max().alias("max_price"), # 최고 가격
pl.col("product").count().alias("product_count"), # 제품 수
(pl.col("price") * pl.col("quantity")).sum().alias("total_sales") # 총 매출
])
print(result)
# 여러 컬럼으로 그룹화
multi_group = df.group_by(["category", "region"]).agg([
pl.col("quantity").sum().alias("total_quantity"),
(pl.col("price") * pl.col("quantity")).sum().alias("total_sales")
])
print("\n카테고리 + 지역별 집계:")
print(multi_group)
설명
이것이 하는 일: group_by()로 지정한 컬럼의 값에 따라 데이터를 그룹으로 나눈 후, agg()로 각 그룹에 대해 집계 함수를 적용합니다. 결과는 그룹별로 하나의 행을 가진 DataFrame이 됩니다.
첫 번째로, group_by("컬럼명")으로 그룹화 기준을 지정합니다. 예를 들어 group_by("category")는 category 컬럼의 고유한 값("전자", "가전")별로 데이터를 분리합니다.
여러 컬럼으로 그룹화하려면 group_by(["col1", "col2"])처럼 리스트로 전달하면 됩니다. 그 다음으로, agg()에 집계 표현식 리스트를 전달합니다.
pl.col("quantity").sum()은 각 그룹의 quantity 값들을 모두 더하고, pl.col("price").mean()은 평균을 계산합니다. 각 표현식에 .alias()를 붙여 결과 컬럼의 이름을 지정할 수 있습니다.
한 번의 agg 호출에 여러 집계를 넣으면 Polars가 내부적으로 최적화하여 데이터를 한 번만 스캔합니다. 마지막으로, 집계 함수는 sum, mean, max, min, count, std, var 등 다양하게 제공됩니다.
또한 (pl.col("price") * pl.col("quantity")).sum()처럼 집계 전에 계산을 수행할 수도 있어, 총 매출(가격 × 수량의 합)같은 복잡한 지표도 쉽게 계산할 수 있습니다. 여러분이 이 코드를 사용하면 복잡한 비즈니스 질문에 대한 답을 몇 줄의 코드로 얻을 수 있고, 대용량 데이터에서도 빠른 그룹별 분석이 가능하며, 여러 지표를 동시에 계산하여 다각도로 데이터를 이해할 수 있습니다.
특히 대시보드나 리포트 생성 시 group_by + agg는 필수입니다.
실전 팁
💡 agg 안에서 여러 집계를 한 번에 수행하는 것이 별도로 나누는 것보다 훨씬 빠릅니다(단일 패스 최적화).
💡 pl.col("*").sum()처럼 와일드카드를 사용하면 모든 숫자 컬럼에 대해 집계를 수행할 수 있습니다.
💡 .n_unique()로 그룹별 고유값 개수를 세거나, .first()와 .last()로 첫/마지막 값을 가져올 수 있습니다.
💡 그룹 개수가 많으면 .sort("집계컬럼", descending=True)를 체이닝하여 상위 그룹을 먼저 확인하세요.
💡 null 값은 집계에서 자동으로 무시되지만, .fill_null(0)으로 명시적으로 처리하면 더 안전합니다.
10. sort로 데이터 정렬하기 - 순서를 통한 인사이트 발견
시작하며
여러분이 매출 데이터를 분석할 때 "어떤 제품이 가장 많이 팔렸는가?", "최근 구매 고객은 누구인가?" 같은 질문에 답하기 위해 데이터를 정렬해야 하는 상황을 만난 적 있나요? 혹은 시계열 데이터를 날짜 순으로 정렬하여 트렌드를 파악해야 하는 경우 말입니다.
이런 문제는 실제 개발 현장에서 데이터의 순서가 의미를 가지는 모든 상황에서 발생합니다. 상위 N개를 추출하거나, 순위를 매기거나, 시간 순서대로 분석하는 등 정렬은 기본이면서도 필수적인 작업입니다.
바로 이럴 때 필요한 것이 sort 메서드입니다. 하나 또는 여러 컬럼을 기준으로 데이터를 정렬하여, 순위 기반 분석과 시계열 분석의 기초를 마련합니다.
개요
간단히 말해서, sort는 지정한 컬럼의 값을 기준으로 DataFrame의 행을 오름차순 또는 내림차순으로 재배열하는 메서드입니다. 여러 컬럼을 기준으로 정렬할 수도 있으며, 각 컬럼마다 정렬 방향을 다르게 지정할 수 있습니다.
왜 이 메서드가 필요한지 실무 관점에서 설명하자면, 데이터의 순서는 그 자체로 중요한 정보입니다. 예를 들어, 고객 목록을 최근 구매일 기준으로 정렬하면 활성 고객과 이탈 고객을 쉽게 구분할 수 있고, 제품을 매출 순으로 정렬하면 핵심 제품과 저조한 제품을 한눈에 파악할 수 있습니다.
Pandas에서는 df.sort_values()를 사용했다면, Polars의 sort는 거의 동일한 기능을 제공하지만 더 빠릅니다. 특히 대용량 데이터의 정렬에서 멀티 스레드 병렬 정렬 알고리즘을 사용하여 성능이 우수합니다.
sort의 핵심 특징은 첫째, 여러 컬럼을 우선순위대로 정렬할 수 있고, 둘째, null 값의 위치를 지정할 수 있으며(앞 또는 뒤), 셋째, stable sort를 지원하여 동일한 값의 원래 순서를 유지한다는 점입니다. 이러한 특징들이 정확하고 예측 가능한 정렬을 보장합니다.
코드 예제
import polars as pl
from datetime import date
# 판매 데이터
df = pl.DataFrame({
"product_id": [1, 2, 3, 4, 5, 6],
"product_name": ["노트북", "마우스", "키보드", "모니터", "헤드셋", "웹캠"],
"sales": [1200000, 350000, 890000, 1500000, 420000, 250000],
"quantity": [5, 50, 30, 3, 25, 40],
"last_sale_date": [date(2024, 3, 15), date(2024, 3, 10), date(2024, 3, 18),
date(2024, 3, 20), date(2024, 3, 12), date(2024, 3, 8)]
})
# 단일 컬럼 정렬: 매출액 내림차순
top_products = df.sort("sales", descending=True)
print("매출 상위 제품:")
print(top_products.head(3))
# 다중 컬럼 정렬: 수량 내림차순, 같으면 가격 오름차순
multi_sort = df.sort(["quantity", "sales"], descending=[True, False])
print("\n수량 많고 가격 낮은 순:")
print(multi_sort)
# 날짜 정렬: 최근 판매일 기준
recent_sales = df.sort("last_sale_date", descending=True)
print("\n최근 판매 제품:")
print(recent_sales.head(3))
설명
이것이 하는 일: sort 메서드는 지정한 컬럼의 값을 비교하여 DataFrame의 모든 행을 재배열합니다. 정렬 기준 컬럼의 값이 작은 것부터(오름차순) 또는 큰 것부터(내림차순) 순서대로 행을 배치합니다.
첫 번째로, 단일 컬럼 정렬은 .sort("컬럼명", descending=True/False) 형태로 사용합니다. descending=True면 내림차순(큰 값부터), False면 오름차순(작은 값부터)입니다.
예를 들어 매출액 기준 내림차순 정렬 후 .head(10)을 사용하면 매출 상위 10개 제품을 즉시 확인할 수 있습니다. 그 다음으로, 여러 컬럼으로 정렬할 때는 리스트로 전달합니다.
.sort(["col1", "col2"], descending=[True, False])는 먼저 col1을 내림차순으로 정렬하고, col1 값이 같은 행들은 col2를 오름차순으로 정렬합니다. 이는 SQL의 ORDER BY col1 DESC, col2 ASC와 동일한 동작입니다.
마지막으로, 날짜 컬럼 정렬도 자연스럽게 지원됩니다. Date나 Datetime 타입 컬럼을 정렬하면 시간 순서대로 배치되어, 시계열 분석의 전처리 단계로 활용할 수 있습니다.
null 값이 있는 경우 nulls_last=True 옵션으로 null을 마지막에 배치할지 결정할 수 있습니다. 여러분이 이 코드를 사용하면 상위/하위 N개 데이터를 쉽게 추출하여 핵심 인사이트를 빠르게 발견할 수 있고, 시계열 데이터를 시간 순으로 정렬하여 트렌드 분석을 준비할 수 있으며, 여러 기준으로 정렬하여 복잡한 우선순위를 표현할 수 있습니다.
특히 리포트나 대시보드에서 "Top 10" 같은 순위 기반 시각화를 만들 때 필수입니다.
실전 팁
💡 정렬 후 .head(N)이나 .tail(N)을 사용하면 상위/하위 N개만 추출할 수 있어 메모리를 절약할 수 있습니다.
💡 대용량 데이터에서는 정렬이 비용이 큰 연산이므로, 필요한 경우에만 사용하고 가능하면 filter로 먼저 데이터를 줄이세요.
💡 문자열 정렬은 사전순(lexicographical order)으로 이루어지며, 한글은 유니코드 순서로 정렬됩니다.
💡 .sort()는 stable sort이므로, 정렬 기준 값이 같은 행들은 원래 순서를 유지합니다.
💡 Lazy 모드에서 정렬은 쿼리 최적화에 활용되어, 불필요한 정렬을 자동으로 제거하거나 순서를 조정합니다.