이미지 로딩 중...

Polars 데이터 통합 및 조인 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 15. · 4 Views

Polars 데이터 통합 및 조인 완벽 가이드

Polars를 활용한 데이터 결합과 조인 기법을 초급자 눈높이에서 실무 중심으로 설명합니다. 다양한 조인 방식과 데이터 통합 전략을 실제 코드 예제와 함께 배워보세요.


목차

  1. Inner Join - 두 데이터의 공통 부분만 가져오기
  2. Left Join - 왼쪽 데이터는 모두 유지하며 결합하기
  3. Outer Join - 양쪽 데이터 모두 빠짐없이 결합하기
  4. Cross Join - 모든 조합을 생성하기
  5. Concat - DataFrame을 수직/수평으로 연결하기
  6. Join with Multiple Keys - 복합 키로 정밀하게 결합하기
  7. Asof Join - 시계열 데이터를 가장 가까운 시점으로 결합하기
  8. Join with Suffix - 중복 컬럼명 충돌 해결하기
  9. Semi Join과 Anti Join - 존재 여부 기반 필터링
  10. Diagonal Concat - 스키마가 다른 데이터 통합하기

1. Inner Join - 두 데이터의 공통 부분만 가져오기

시작하며

여러분이 고객 주문 데이터를 분석할 때 이런 상황을 겪어본 적 있나요? 고객 정보 테이블과 주문 내역 테이블이 따로 있어서, 실제 주문한 고객의 상세 정보를 함께 보고 싶은 경우요.

이런 문제는 실제 데이터 분석 현장에서 매일같이 발생합니다. 여러 시스템에서 수집된 데이터는 대부분 분리되어 있고, 의미 있는 인사이트를 얻기 위해서는 이들을 적절히 결합해야 합니다.

바로 이럴 때 필요한 것이 Inner Join입니다. 두 데이터셋에서 공통된 키를 기준으로 일치하는 행만 선택하여 통합된 뷰를 만들어냅니다.

개요

간단히 말해서, Inner Join은 두 DataFrame에서 키 값이 양쪽 모두에 존재하는 행만 결합하는 방식입니다. 실무에서 이 개념이 필요한 이유는 명확합니다.

예를 들어, 회원 가입은 했지만 한 번도 구매하지 않은 고객을 제외하고, 실제 구매 이력이 있는 고객만 분석하고 싶을 때 Inner Join을 사용하면 됩니다. 양쪽 테이블에 모두 존재하는 데이터만 남기므로 누락된 값 걱정 없이 분석할 수 있습니다.

기존 Pandas에서는 merge() 함수를 사용했다면, Polars에서는 join() 메서드로 훨씬 빠르고 메모리 효율적으로 처리할 수 있습니다. Inner Join의 핵심 특징은 첫째, 양쪽 테이블에 공통으로 존재하는 키만 결과에 포함된다는 점, 둘째, 결과 크기가 원본보다 작거나 같다는 점, 셋째, NULL 값 처리 고민이 적다는 점입니다.

이러한 특징들이 데이터 품질을 높이고 분석 결과의 신뢰성을 보장하는 이유입니다.

코드 예제

import polars as pl

# 고객 정보 DataFrame
customers = pl.DataFrame({
    "customer_id": [1, 2, 3, 4],
    "name": ["김철수", "이영희", "박민수", "정수진"]
})

# 주문 정보 DataFrame
orders = pl.DataFrame({
    "order_id": [101, 102, 103],
    "customer_id": [1, 1, 3],
    "amount": [50000, 30000, 75000]
})

# Inner Join 수행 - 주문한 고객만 조회
result = customers.join(orders, on="customer_id", how="inner")
print(result)

설명

이것이 하는 일: Inner Join은 두 DataFrame을 비교하여 지정된 키 컬럼의 값이 양쪽에 모두 존재하는 행만 결합합니다. 첫 번째로, customers와 orders 두 DataFrame을 준비합니다.

customers는 4명의 고객 정보를, orders는 3건의 주문 정보를 담고 있습니다. 주목할 점은 customer_id 2번(이영희)과 4번(정수진)은 주문 내역이 없고, customer_id 1번(김철수)은 2건의 주문이 있다는 것입니다.

그 다음으로, join() 메서드가 실행되면서 Polars는 customer_id 컬럼을 기준으로 양쪽 DataFrame을 매칭합니다. how="inner" 파라미터가 Inner Join 방식을 지정하며, 이는 양쪽에 모두 존재하는 키만 선택하라는 의미입니다.

내부적으로 Polars는 해시 기반 알고리즘을 사용하여 매우 빠르게 이 작업을 수행합니다. 마지막으로, 결과 DataFrame에는 customer_id 1번의 2개 행과 customer_id 3번의 1개 행, 총 3개의 행만 포함됩니다.

김철수는 2건의 주문이 있으므로 2개 행으로 확장되고, 이영희와 정수진은 주문 내역이 없어서 결과에서 제외됩니다. 여러분이 이 코드를 사용하면 실제 거래가 발생한 고객만 필터링하여 매출 분석, 고객 세그먼트 분류, RFM 분석 등을 정확하게 수행할 수 있습니다.

불필요한 NULL 값이 없어서 후속 집계 작업도 훨씬 간단해지고, 메모리 사용량도 최소화됩니다.

실전 팁

💡 여러 컬럼을 기준으로 조인하려면 on=["col1", "col2"] 형태로 리스트를 전달하세요. 복합 키 조인 시 매우 유용합니다.

💡 조인 전 중복 키가 있는지 확인하세요. df.group_by("key").count()로 체크하면 예상치 못한 행 폭증을 방지할 수 있습니다.

💡 대용량 데이터 조인 시 lazy API(pl.scan_csv().join().collect())를 사용하면 쿼리 최적화로 10배 이상 빨라질 수 있습니다.

💡 조인 키의 데이터 타입이 양쪽에서 일치하는지 확인하세요. Int64와 Int32가 섞여 있으면 에러가 발생합니다.

💡 조인 결과가 예상보다 적다면 키 값의 공백이나 대소문자 차이를 의심해보세요. str.strip()과 str.to_lowercase()로 전처리하면 해결됩니다.


2. Left Join - 왼쪽 데이터는 모두 유지하며 결합하기

시작하며

여러분이 전체 고객 리스트를 보면서 각 고객의 최근 구매 이력을 함께 확인하고 싶을 때 어떻게 하시나요? 구매하지 않은 고객도 포함해서 전체 현황을 파악해야 하는 상황이죠.

이런 문제는 마케팅 캠페인 타겟팅이나 고객 이탈 분석에서 필수적입니다. 모든 고객을 대상으로 하되, 구매 이력이 있으면 표시하고 없으면 NULL로 남겨두어야 전체 그림을 볼 수 있습니다.

바로 이럴 때 필요한 것이 Left Join입니다. 왼쪽 테이블의 모든 행을 유지하면서 오른쪽 테이블의 일치하는 데이터를 가져오되, 일치하지 않으면 NULL로 채웁니다.

개요

간단히 말해서, Left Join은 왼쪽(첫 번째) DataFrame의 모든 행을 유지하고, 오른쪽 DataFrame에서 일치하는 데이터를 가져오는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 마스터 데이터(고객, 상품 등)를 기준으로 트랜잭션 데이터(주문, 클릭 등)를 붙일 때 마스터 데이터가 누락되면 안 되기 때문입니다.

예를 들어, 전체 100명의 고객 중 80명만 구매했다면, 구매하지 않은 20명도 분석 대상에 포함되어야 이탈 방지 전략을 세울 수 있습니다. 기존에는 Inner Join으로 구매자만 분석했다면, 이제는 Left Join으로 비구매자까지 함께 분석하여 전환율 개선 포인트를 찾을 수 있습니다.

Left Join의 핵심 특징은 첫째, 왼쪽 테이블의 행 수가 보존된다는 점, 둘째, 오른쪽 테이블에 일치하는 값이 없으면 NULL이 채워진다는 점, 셋째, 결과 크기가 왼쪽 테이블보다 크거나 같다는 점입니다. 이러한 특징들이 전체 데이터의 완전성을 유지하면서도 보강 정보를 추가할 수 있게 해줍니다.

코드 예제

import polars as pl

# 전체 고객 정보 (마스터 데이터)
customers = pl.DataFrame({
    "customer_id": [1, 2, 3, 4, 5],
    "name": ["김철수", "이영희", "박민수", "정수진", "최영수"]
})

# 주문 정보 (트랜잭션 데이터)
orders = pl.DataFrame({
    "customer_id": [1, 1, 3],
    "order_id": [101, 102, 103],
    "amount": [50000, 30000, 75000]
})

# Left Join 수행 - 모든 고객 포함, 주문 없으면 NULL
result = customers.join(orders, on="customer_id", how="left")
print(result)

설명

이것이 하는 일: Left Join은 왼쪽 DataFrame을 기준으로 하여 모든 행을 결과에 포함시키고, 오른쪽 DataFrame에서 매칭되는 데이터를 추가합니다. 첫 번째로, customers DataFrame에는 5명의 고객이 있고, orders DataFrame에는 3건의 주문이 있습니다.

customer_id 1번과 3번만 주문 이력이 있고, 2번, 4번, 5번은 주문한 적이 없는 상황입니다. 그 다음으로, join() 메서드가 how="left"로 실행되면서 Polars는 customers의 모든 5개 행을 유지합니다.

customer_id를 기준으로 orders를 매칭할 때, 일치하는 데이터가 있으면 order_id와 amount를 채우고, 없으면 NULL(Polars에서는 null)을 채웁니다. 1번 고객은 주문이 2건이므로 행이 2개로 확장됩니다.

마지막으로, 결과 DataFrame에는 총 6개의 행이 생성됩니다. 김철수(1번) 2행, 이영희(2번) 1행(주문 정보는 null), 박민수(3번) 1행, 정수진(4번) 1행(null), 최영수(5번) 1행(null)입니다.

이렇게 하여 전체 고객 목록을 유지하면서도 주문 정보를 함께 볼 수 있습니다. 여러분이 이 코드를 사용하면 비구매 고객을 식별하여 리타겟팅 캠페인을 기획하거나, 전체 고객 대비 활성 고객 비율을 계산하거나, 고객별 생애 가치(LTV)를 계산할 때 누락 없이 분석할 수 있습니다.

NULL 값을 fill_null(0)로 처리하면 집계 작업도 간편해집니다.

실전 팁

💡 Left Join 후 NULL 값을 바로 처리하려면 .join().fill_null(0) 체이닝을 사용하세요. 집계 작업이 훨씬 편해집니다.

💡 오른쪽 테이블에 중복 키가 있으면 왼쪽 행이 배수로 늘어납니다. 의도하지 않았다면 먼저 오른쪽을 group_by().agg()로 집계하세요.

💡 NULL 값이 얼마나 있는지 확인하려면 result.null_count()를 사용하면 컬럼별 NULL 개수를 즉시 볼 수 있습니다.

💡 여러 테이블을 순차적으로 Left Join할 때는 가장 큰 마스터 테이블을 왼쪽에 두고 작은 테이블들을 차례로 붙이면 효율적입니다.

💡 조인 키가 NULL인 행은 매칭되지 않으니 주의하세요. 조인 전 filter(pl.col("key").is_not_null())로 정제하는 것이 안전합니다.


3. Outer Join - 양쪽 데이터 모두 빠짐없이 결합하기

시작하며

여러분이 두 개의 서로 다른 시스템에서 수집한 고객 데이터를 통합할 때 이런 고민을 해보셨나요? 시스템 A에만 있는 고객, 시스템 B에만 있는 고객, 양쪽 모두에 있는 고객을 한 번에 파악하고 싶은 상황이요.

이런 문제는 데이터 통합이나 마이그레이션 프로젝트에서 매우 흔합니다. 두 시스템의 데이터가 완벽히 일치하지 않을 때, 어느 한쪽을 기준으로 하면 다른 쪽의 데이터를 놓치게 됩니다.

바로 이럴 때 필요한 것이 Outer Join(Full Outer Join)입니다. 양쪽 테이블의 모든 행을 포함하되, 일치하지 않는 부분은 NULL로 채워서 전체 데이터 현황을 완벽히 파악할 수 있게 해줍니다.

개요

간단히 말해서, Outer Join은 두 DataFrame의 모든 행을 결과에 포함시키며, 한쪽에만 존재하는 데이터는 반대편을 NULL로 채우는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 데이터 품질 검증이나 시스템 간 불일치 분석에서 필수적이기 때문입니다.

예를 들어, 온라인 주문 시스템과 재고 관리 시스템의 상품 목록을 Outer Join하면, 주문은 가능한데 재고가 없는 상품이나 재고는 있는데 주문 시스템에 등록되지 않은 상품을 즉시 발견할 수 있습니다. 기존에는 Left Join과 Right Join을 각각 수행하고 수동으로 합쳤다면, 이제는 Outer Join 한 번으로 양쪽의 모든 데이터를 통합할 수 있습니다.

Outer Join의 핵심 특징은 첫째, 양쪽 테이블의 모든 행이 결과에 포함된다는 점, 둘째, 결과 크기가 항상 양쪽 테이블 중 큰 쪽보다 크거나 같다는 점, 셋째, NULL 값이 양쪽 모두에서 발생할 수 있다는 점입니다. 이러한 특징들이 데이터 불일치를 발견하고 통합 전략을 수립하는 데 핵심적입니다.

코드 예제

import polars as pl

# 온라인 쇼핑몰 고객 DB
online_customers = pl.DataFrame({
    "customer_id": [1, 2, 3],
    "email": ["kim@example.com", "lee@example.com", "park@example.com"]
})

# 오프라인 매장 고객 DB
offline_customers = pl.DataFrame({
    "customer_id": [2, 3, 4, 5],
    "phone": ["010-1111-2222", "010-3333-4444", "010-5555-6666", "010-7777-8888"]
})

# Outer Join 수행 - 모든 고객을 하나로 통합
result = online_customers.join(offline_customers, on="customer_id", how="outer")
print(result)

설명

이것이 하는 일: Outer Join은 두 DataFrame을 완전히 통합하여 어느 한쪽에라도 존재하는 모든 행을 결과에 포함시킵니다. 첫 번째로, online_customers에는 customer_id 1, 2, 3이 있고, offline_customers에는 2, 3, 4, 5가 있습니다.

양쪽에 공통으로 존재하는 것은 2번과 3번뿐이고, 1번은 온라인 전용, 4번과 5번은 오프라인 전용 고객입니다. 그 다음으로, join() 메서드가 how="outer"로 실행되면서 Polars는 모든 고유한 customer_id(1, 2, 3, 4, 5)에 대해 행을 생성합니다.

customer_id를 기준으로 양쪽 데이터를 매칭하며, 한쪽에만 존재하면 반대편 컬럼은 null로 채웁니다. 내부적으로는 두 테이블의 키를 모두 포함하는 전체 집합을 만든 후 각각 매칭하는 방식입니다.

마지막으로, 결과 DataFrame에는 5개의 행이 생성됩니다. customer_id 1번은 email만 있고 phone은 null, 2번과 3번은 email과 phone 모두 존재, 4번과 5번은 phone만 있고 email은 null입니다.

이렇게 하여 두 시스템의 모든 고객을 통합적으로 볼 수 있습니다. 여러분이 이 코드를 사용하면 채널별 고객 분포를 파악하고, 옴니채널 전략을 수립하거나, 데이터 마이그레이션 시 누락된 레코드를 찾거나, 시스템 간 데이터 동기화 상태를 점검할 수 있습니다.

NULL 패턴을 분석하면 어느 시스템이 더 완전한 데이터를 갖고 있는지도 알 수 있습니다.

실전 팁

💡 Outer Join 결과의 NULL 패턴을 분석하려면 result.select([pl.all().is_null().sum()])로 컬럼별 NULL 개수를 확인하세요.

💡 대용량 데이터의 Outer Join은 메모리를 많이 사용하므로, 가능하면 필요한 컬럼만 선택한 후 조인하세요(select() 먼저 실행).

💡 조인 후 어느 쪽에서 온 데이터인지 구분하려면 suffix 파라미터를 사용하세요. join(..., suffix="_offline")처럼요.

💡 NULL 값을 의미 있는 기본값으로 채우려면 fill_null() 대신 coalesce()를 사용하면 컬럼별로 다른 전략을 적용할 수 있습니다.

💡 Outer Join이 느리다면 양쪽 DataFrame을 먼저 정렬(sort())해보세요. 정렬된 데이터는 조인 성능이 크게 향상됩니다.


4. Cross Join - 모든 조합을 생성하기

시작하며

여러분이 A/B 테스트를 설계하거나 시뮬레이션 데이터를 생성할 때 이런 경험 있으신가요? 모든 제품과 모든 지역의 조합, 또는 모든 광고 소재와 모든 타겟 그룹의 조합을 만들어야 하는 상황이요.

이런 문제는 실험 설계, 시나리오 분석, 마스터 데이터 생성에서 자주 발생합니다. 두 집합의 모든 가능한 조합(카르테시안 곱)을 만들어야 완전한 테스트나 분석이 가능합니다.

바로 이럴 때 필요한 것이 Cross Join입니다. 왼쪽 테이블의 각 행과 오른쪽 테이블의 모든 행을 조합하여 가능한 모든 경우의 수를 생성합니다.

개요

간단히 말해서, Cross Join은 두 DataFrame의 카르테시안 곱(Cartesian Product)을 계산하여 모든 가능한 행 조합을 만드는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 완전한 시나리오 매트릭스를 만들거나 참조 테이블을 생성할 때 필수적이기 때문입니다.

예를 들어, 3개 제품과 4개 지역의 판매 계획을 세울 때 Cross Join으로 12개 조합을 자동 생성하면, 각 조합별로 목표를 설정하거나 예측 모델을 적용할 수 있습니다. 기존에는 이중 for 루프로 수동으로 조합을 만들었다면, 이제는 Cross Join 한 번으로 벡터화된 방식으로 즉시 생성할 수 있습니다.

Cross Join의 핵심 특징은 첫째, 결과 행 수가 두 테이블 행 수의 곱이라는 점(m×n), 둘째, 조인 조건(키)이 필요 없다는 점, 셋째, 결과가 매우 빠르게 커질 수 있다는 점입니다. 이러한 특징들이 완전한 조합 생성을 가능하게 하지만, 대용량 데이터에는 주의가 필요합니다.

코드 예제

import polars as pl

# 제품 목록
products = pl.DataFrame({
    "product_id": [1, 2, 3],
    "product_name": ["노트북", "마우스", "키보드"]
})

# 판매 지역 목록
regions = pl.DataFrame({
    "region_id": ["A", "B", "C", "D"],
    "region_name": ["서울", "부산", "대구", "광주"]
})

# Cross Join 수행 - 모든 제품×지역 조합 생성
result = products.join(regions, how="cross")
print(f"총 {len(result)}개 조합 생성 (3 제품 × 4 지역 = 12)")
print(result)

설명

이것이 하는 일: Cross Join은 조인 키 없이 두 DataFrame의 모든 행을 서로 조합하여 완전한 카르테시안 곱을 생성합니다. 첫 번째로, products DataFrame에는 3개의 제품이 있고, regions DataFrame에는 4개의 지역이 있습니다.

일반적인 조인과 달리 Cross Join에는 공통 키가 필요하지 않으며, 단순히 모든 조합을 만듭니다. 그 다음으로, join() 메서드가 how="cross"로 실행되면서 Polars는 각 제품에 대해 모든 지역을 하나씩 매칭합니다.

노트북과 서울, 노트북과 부산, ..., 키보드와 광주까지 3×4=12개의 모든 조합이 생성됩니다. 내부적으로는 중첩 루프와 유사하지만 벡터화되어 매우 빠르게 처리됩니다.

마지막으로, 결과 DataFrame에는 정확히 12개의 행이 생성되며, 각 행은 하나의 제품-지역 조합을 나타냅니다. 모든 컬럼(product_id, product_name, region_id, region_name)이 포함되며, NULL 값은 발생하지 않습니다.

이 완전한 매트릭스를 기반으로 각 조합별 판매 목표, 재고 계획, 가격 전략 등을 설정할 수 있습니다. 여러분이 이 코드를 사용하면 A/B 테스트의 모든 변형 조합을 자동 생성하거나, 그리드 검색을 위한 하이퍼파라미터 조합을 만들거나, 시계열 예측을 위한 날짜×제품 매트릭스를 생성할 수 있습니다.

특히 실험 설계나 시뮬레이션에서 빠뜨린 조합 없이 완전한 분석을 보장합니다.

실전 팁

💡 Cross Join은 결과가 폭발적으로 커지므로, 먼저 작은 샘플로 테스트해보고 예상 크기를 계산하세요(len(df1) * len(df2)).

💡 불필요한 조합을 제거하려면 Cross Join 후 filter()를 체이닝하세요. 예: .join(..., how="cross").filter(pl.col("price") > 10000)

💡 시간 범위와 제품 목록의 Cross Join으로 시계열 분석용 완전한 날짜×제품 그리드를 만들 수 있습니다. 빠진 날짜를 0으로 채울 때 유용합니다.

💡 대용량 Cross Join은 메모리 부족을 유발할 수 있으니, 청크 단위로 나누어 처리하거나 lazy API로 스트리밍하세요.

💡 Cross Join을 그리드 검색에 활용하려면 하이퍼파라미터 값들을 각각 DataFrame으로 만들고 순차적으로 Cross Join하면 됩니다.


5. Concat - DataFrame을 수직/수평으로 연결하기

시작하며

여러분이 여러 달의 월별 매출 데이터나 여러 지점의 판매 데이터를 하나로 합쳐야 할 때 어떻게 하시나요? 같은 구조의 데이터가 여러 파일이나 테이블에 분산되어 있는 상황이죠.

이런 문제는 데이터 수집과 통합 과정에서 매우 흔합니다. 월별 배치로 생성된 데이터, 지역별로 분리된 데이터, 또는 여러 소스에서 수집한 동일 형식의 데이터를 하나의 분석용 데이터셋으로 만들어야 합니다.

바로 이럴 때 필요한 것이 Concat입니다. 동일한 스키마를 가진 여러 DataFrame을 수직으로 쌓거나(행 추가), 동일한 행 수를 가진 DataFrame을 수평으로 붙여서(열 추가) 통합된 데이터셋을 만듭니다.

개요

간단히 말해서, Concat은 여러 DataFrame을 행 방향(수직) 또는 열 방향(수평)으로 연결하여 하나의 큰 DataFrame을 만드는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 분할된 데이터를 분석 가능한 단일 뷰로 통합하는 것이 데이터 분석의 첫 단계이기 때문입니다.

예를 들어, 2024년 1월부터 12월까지의 월별 주문 데이터를 Concat으로 수직 연결하면, 연간 트렌드 분석이나 YoY 비교를 할 수 있는 완전한 데이터셋이 만들어집니다. 기존에는 여러 DataFrame을 반복문으로 하나씩 append 했다면, 이제는 pl.concat()으로 한 번에 최적화된 방식으로 결합할 수 있습니다.

Concat의 핵심 특징은 첫째, 수직 연결 시 컬럼 구조가 동일해야 한다는 점(또는 how="diagonal"로 불일치 허용), 둘째, 수평 연결 시 행 수가 동일해야 한다는 점, 셋째, Join보다 훨씬 빠르고 간단하다는 점입니다. 이러한 특징들이 대용량 데이터 통합을 효율적으로 만듭니다.

코드 예제

import polars as pl

# 1월 매출 데이터
jan_sales = pl.DataFrame({
    "date": ["2024-01-15", "2024-01-20"],
    "amount": [100000, 150000]
})

# 2월 매출 데이터
feb_sales = pl.DataFrame({
    "date": ["2024-02-10", "2024-02-25"],
    "amount": [120000, 180000]
})

# 수직 연결 - 행을 아래로 쌓기
combined = pl.concat([jan_sales, feb_sales], how="vertical")
print("수직 연결 결과:")
print(combined)

# 추가 정보 컬럼을 수평으로 붙이기
additional_info = pl.DataFrame({"category": ["식품", "의류", "식품", "전자"]})
full_data = pl.concat([combined, additional_info], how="horizontal")
print("\n수평 연결 결과:")
print(full_data)

설명

이것이 하는 일: Concat은 여러 DataFrame을 하나로 합치는데, 수직 연결은 행을 아래로 쌓고 수평 연결은 열을 옆으로 붙입니다. 첫 번째로, jan_sales와 feb_sales 두 DataFrame을 준비합니다.

둘 다 date와 amount 컬럼을 가지고 있어서 스키마가 동일합니다. 각각 2개의 행을 가지고 있습니다.

그 다음으로, pl.concat([jan_sales, feb_sales], how="vertical")이 실행되면서 Polars는 두 DataFrame을 수직으로 쌓습니다. 1월 데이터 2개 행 아래에 2월 데이터 2개 행이 추가되어 총 4개 행이 됩니다.

컬럼 구조가 동일하므로 문제없이 결합되며, 인덱스는 자동으로 재설정됩니다. 내부적으로 메모리를 효율적으로 재할당하여 복사 비용을 최소화합니다.

세 번째로, 수평 연결에서는 combined(4개 행)와 additional_info(4개 행)를 how="horizontal"로 결합합니다. 행 수가 일치하므로 각 행에 category 컬럼이 추가되어 3개 컬럼의 DataFrame이 만들어집니다.

수평 연결은 행의 순서를 기준으로 매칭되므로 순서가 중요합니다. 여러분이 이 코드를 사용하면 월별/분기별 데이터를 연간 데이터로 통합하거나, 여러 CSV 파일을 하나의 분석용 데이터셋으로 만들거나, 피처 엔지니어링으로 생성한 여러 컬럼 그룹을 원본 데이터에 추가할 수 있습니다.

대용량 데이터도 lazy API와 함께 사용하면 메모리 효율적으로 처리됩니다.

실전 팁

💡 수직 연결 시 컬럼이 일부 다르면 how="diagonal"을 사용하세요. 없는 컬럼은 자동으로 NULL로 채워집니다.

💡 수백 개의 DataFrame을 concat할 때는 리스트 컴프리헨션으로 한 번에 전달하세요. 반복문으로 하나씩 concat하면 매우 느립니다.

💡 수평 연결 전에 행 수를 확인하세요. 불일치 시 에러가 발생하므로 len(df1) == len(df2)로 체크하는 것이 안전합니다.

💡 여러 파일을 concat할 때는 pl.scan_csv("*.csv").collect()로 패턴 매칭하면 자동으로 모든 파일이 통합됩니다.

💡 concat 후 중복 행을 제거하려면 .unique()를 체이닝하세요. 특히 여러 소스에서 온 데이터는 중복이 있을 수 있습니다.


6. Join with Multiple Keys - 복합 키로 정밀하게 결합하기

시작하며

여러분이 날짜와 제품을 함께 고려해야 하는 판매 데이터를 분석할 때 이런 어려움을 겪어본 적 있나요? 단일 컬럼만으로는 고유하게 식별되지 않아서, 여러 컬럼을 조합해야 정확한 매칭이 가능한 경우요.

이런 문제는 시계열 데이터, 다차원 데이터, 복합 식별자를 사용하는 데이터베이스에서 매우 흔합니다. 날짜+제품, 지역+카테고리, 사용자+세션 같은 복합 키가 있어야 데이터를 정확히 구분할 수 있습니다.

바로 이럴 때 필요한 것이 Multiple Keys Join입니다. 두 개 이상의 컬럼을 동시에 매칭 조건으로 사용하여 정밀하고 정확한 데이터 결합을 수행합니다.

개요

간단히 말해서, Multiple Keys Join은 여러 컬럼을 동시에 조인 키로 사용하여 복합 조건으로 데이터를 결합하는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 실제 비즈니스 데이터는 대부분 단일 키로 고유성이 보장되지 않기 때문입니다.

예를 들어, 일별 제품별 판매량 데이터와 일별 제품별 재고 데이터를 결합할 때, 날짜만으로도 안 되고 제품만으로도 안 되며, (날짜, 제품) 조합이 있어야 정확히 매칭됩니다. 기존에는 복합 키를 만들기 위해 문자열 연결로 임시 컬럼을 만들었다면, 이제는 on=["col1", "col2"] 형태로 직접 지정하여 깔끔하고 효율적으로 처리할 수 있습니다.

Multiple Keys Join의 핵심 특징은 첫째, 여러 컬럼의 값이 모두 일치해야 매칭된다는 점, 둘째, 단일 키보다 정밀한 매칭이 가능하다는 점, 셋째, 복합 인덱스를 활용하여 성능도 우수하다는 점입니다. 이러한 특징들이 복잡한 비즈니스 로직을 정확히 구현할 수 있게 해줍니다.

코드 예제

import polars as pl

# 일별 제품별 판매 데이터
sales = pl.DataFrame({
    "date": ["2024-01-01", "2024-01-01", "2024-01-02", "2024-01-02"],
    "product_id": [101, 102, 101, 102],
    "sales_qty": [50, 30, 45, 35]
})

# 일별 제품별 재고 데이터
inventory = pl.DataFrame({
    "date": ["2024-01-01", "2024-01-01", "2024-01-02"],
    "product_id": [101, 102, 101],
    "stock_qty": [100, 80, 55]
})

# 날짜와 제품 ID 두 키로 조인
result = sales.join(inventory, on=["date", "product_id"], how="left")
print(result)

설명

이것이 하는 일: Multiple Keys Join은 지정된 여러 컬럼의 값이 모두 일치하는 행만 결합하여 복합 조건 매칭을 수행합니다. 첫 번째로, sales DataFrame에는 4개의 행이 있고, 각 행은 (날짜, 제품) 조합으로 고유하게 식별됩니다.

inventory DataFrame에는 3개의 행이 있습니다. 2024-01-02의 제품 102는 inventory에 없는 상황입니다.

그 다음으로, join() 메서드가 on=["date", "product_id"]로 실행되면서 Polars는 두 컬럼을 동시에 비교합니다. 예를 들어, sales의 첫 행(2024-01-01, 101)은 inventory에서 date도 2024-01-01이고 product_id도 101인 행을 찾습니다.

두 조건이 모두 만족해야 매칭되며, 하나라도 다르면 매칭되지 않습니다. 내부적으로는 복합 해시 키를 생성하여 효율적으로 처리합니다.

마지막으로, 결과 DataFrame에는 4개의 행이 모두 유지됩니다(Left Join이므로). 앞의 3개 행은 inventory와 매칭되어 stock_qty 값이 채워지고, 마지막 행(2024-01-02, 102)은 inventory에 없어서 stock_qty가 null로 표시됩니다.

이를 통해 어느 날짜에 어느 제품의 재고가 부족한지 즉시 파악할 수 있습니다. 여러분이 이 코드를 사용하면 시계열 분석에서 정확한 시점의 데이터를 매칭하거나, 다차원 큐브 데이터를 결합하거나, 사용자별 세션별 이벤트를 정밀하게 추적할 수 있습니다.

복합 키를 사용하면 데이터 무결성이 향상되고 분석 결과의 신뢰도가 크게 높아집니다.

실전 팁

💡 복합 키 조인 전에 각 키 컬럼의 데이터 타입이 일치하는지 확인하세요. 한쪽이 문자열이고 다른 쪽이 정수면 에러가 발생합니다.

💡 조인 성능을 높이려면 복합 키 컬럼들로 먼저 정렬(sort())하세요. 정렬된 데이터는 조인 속도가 훨씬 빠릅니다.

💡 복합 키의 고유성을 검증하려면 df.group_by(["key1", "key2"]).count().filter(pl.col("count") > 1)로 중복을 찾으세요.

💡 세 개 이상의 키도 가능합니다. on=["date", "product_id", "region"] 형태로 필요한 만큼 추가하세요.

💡 복합 키 조인 후 NULL 분석으로 데이터 품질 문제를 발견할 수 있습니다. 어떤 조합이 한쪽에만 있는지 확인하세요.


7. Asof Join - 시계열 데이터를 가장 가까운 시점으로 결합하기

시작하며

여러분이 주식 거래 데이터와 호가 데이터를 결합할 때 이런 고민을 해보셨나요? 거래 시점과 정확히 일치하는 호가가 없어서, 거래 시점 직전의 가장 가까운 호가를 찾아야 하는 상황이요.

이런 문제는 시계열 데이터, 특히 타임스탬프가 정확히 일치하지 않는 센서 데이터, 금융 데이터, IoT 데이터에서 매우 흔합니다. 정확한 일치를 요구하면 대부분의 데이터가 매칭되지 않아서 분석이 불가능합니다.

바로 이럴 때 필요한 것이 Asof Join입니다. 시간 순서를 고려하여 각 시점에서 가장 가까운(보통은 직전) 데이터를 찾아서 결합하는 특수한 조인 방식입니다.

개요

간단히 말해서, Asof Join은 시계열 데이터에서 정확히 일치하지 않아도 시간상 가장 가까운 행을 찾아 결합하는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 실시간 데이터 스트림에서는 타임스탬프가 정확히 일치하는 경우가 거의 없기 때문입니다.

예를 들어, 주식 체결 시각이 14:30:15.123이고 호가 갱신 시각이 14:30:15.089라면 정확히 일치하지 않지만, 체결 시점의 호가는 직전 호가를 사용해야 합니다. 기존에는 복잡한 윈도우 함수나 self-join으로 구현했다면, 이제는 Asof Join으로 간단하고 효율적으로 처리할 수 있습니다.

Asof Join의 핵심 특징은 첫째, 시간 컬럼을 기준으로 정렬이 필수라는 점, 둘째, 왼쪽 각 행에 대해 오른쪽에서 가장 가까운(또는 직전) 행을 찾는다는 점, 셋째, 방향을 지정할 수 있다는 점(backward, forward, nearest)입니다. 이러한 특징들이 시계열 데이터 분석을 혁신적으로 간편하게 만듭니다.

코드 예제

import polars as pl
from datetime import datetime

# 주식 거래 데이터 (체결 시각)
trades = pl.DataFrame({
    "time": ["2024-01-01 09:00:05", "2024-01-01 09:00:15", "2024-01-01 09:00:25"],
    "trade_price": [50000, 50100, 49900]
}).with_columns(pl.col("time").str.strptime(pl.Datetime, "%Y-%m-%d %H:%M:%S"))

# 호가 데이터 (호가 갱신 시각)
quotes = pl.DataFrame({
    "time": ["2024-01-01 09:00:00", "2024-01-01 09:00:10", "2024-01-01 09:00:20"],
    "bid_price": [49900, 50000, 49850]
}).with_columns(pl.col("time").str.strptime(pl.Datetime, "%Y-%m-%d %H:%M:%S"))

# Asof Join - 각 거래 시점 직전의 호가를 매칭
result = trades.join_asof(quotes, on="time", strategy="backward")
print(result)

설명

이것이 하는 일: Asof Join은 시간 컬럼을 기준으로 왼쪽 각 행에 대해 오른쪽에서 시간상 가장 가까운 행을 찾아 결합합니다. 첫 번째로, trades와 quotes 두 DataFrame을 준비하고 time 컬럼을 Datetime 타입으로 변환합니다.

trades의 시각은 05초, 15초, 25초이고, quotes의 시각은 00초, 10초, 20초입니다. 정확히 일치하는 시각이 하나도 없습니다.

그 다음으로, join_asof() 메서드가 on="time", strategy="backward"로 실행됩니다. 이는 각 거래(왼쪽) 시각에 대해 그 시각 이전의 가장 가까운 호가(오른쪽)를 찾으라는 의미입니다.

09:00:05의 거래는 09:00:00의 호가를, 09:00:15의 거래는 09:00:10의 호가를, 09:00:25의 거래는 09:00:20의 호가를 매칭합니다. Polars는 양쪽 데이터가 시간순으로 정렬되어 있다고 가정하며, 이진 탐색 같은 효율적인 알고리즘을 사용합니다.

마지막으로, 결과 DataFrame에는 3개의 거래 행 각각에 해당 시점 직전의 호가 정보가 추가됩니다. 첫 거래(09:00:05, 50000)는 bid_price 49900과 매칭되고, 두 번째 거래(09:00:15, 50100)는 bid_price 50000과 매칭되며, 세 번째 거래(09:00:25, 49900)는 bid_price 49850과 매칭됩니다.

이를 통해 각 체결가와 해당 시점의 호가를 비교 분석할 수 있습니다. 여러분이 이 코드를 사용하면 금융 데이터에서 체결가-호가 스프레드를 계산하거나, IoT 센서 데이터를 동기화하거나, 로그 데이터와 메트릭 데이터를 시간상 매칭하거나, 이벤트 기반 분석을 수행할 수 있습니다.

정확한 타임스탬프 일치를 강요하지 않아서 실무 시계열 분석이 훨씬 유연해집니다.

실전 팁

💡 Asof Join 전에 반드시 양쪽 DataFrame을 시간 컬럼으로 정렬(sort())하세요. 정렬되지 않으면 잘못된 결과가 나올 수 있습니다.

💡 strategy 옵션은 "backward"(직전), "forward"(직후), "nearest"(최근접) 세 가지입니다. 용도에 맞게 선택하세요.

💡 tolerance 파라미터로 최대 허용 시간 차이를 지정할 수 있습니다. 예: tolerance="1h"는 1시간 이내만 매칭합니다.

💡 by 파라미터로 추가 그룹 키를 지정하면 종목별, 센서별로 독립적인 Asof Join을 수행할 수 있습니다. 매우 강력한 기능입니다.

💡 Asof Join 후 시간 차이를 계산하려면 (pl.col("time") - pl.col("time_right")).alias("time_diff")로 매칭 품질을 확인하세요.


8. Join with Suffix - 중복 컬럼명 충돌 해결하기

시작하며

여러분이 두 테이블을 조인할 때 이런 에러를 본 적 있으신가요? "duplicate column name" 또는 결과에서 어느 테이블에서 온 컬럼인지 구분이 안 되는 상황이요.

이런 문제는 실무에서 매우 자주 발생합니다. 여러 테이블이 name, value, status 같은 일반적인 컬럼명을 공통으로 사용하기 때문에, 조인하면 컬럼명이 충돌하거나 혼란스러워집니다.

바로 이럴 때 필요한 것이 Suffix 파라미터입니다. 조인 시 오른쪽 테이블의 중복 컬럼명에 자동으로 접미사를 붙여서 명확하게 구분할 수 있게 해줍니다.

개요

간단히 말해서, Suffix는 조인 시 중복되는 컬럼명에 접미사를 추가하여 왼쪽 테이블과 오른쪽 테이블의 컬럼을 구분하는 기능입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 조인 키가 아닌 일반 컬럼들도 이름이 같을 수 있기 때문입니다.

예를 들어, 고객 테이블과 주문 테이블 모두 created_at, updated_at 같은 메타데이터 컬럼을 가지고 있을 때, 조인 후 어느 것이 고객 생성일이고 어느 것이 주문 생성일인지 구분해야 합니다. 기존에는 조인 전에 수동으로 rename()을 했다면, 이제는 suffix 파라미터로 자동으로 처리할 수 있습니다.

Suffix의 핵심 특징은 첫째, 기본값은 "_right"라는 점, 둘째, 조인 키는 중복되지 않으므로 suffix가 적용되지 않는다는 점, 셋째, 명시적으로 설정하여 가독성을 높일 수 있다는 점입니다. 이러한 특징들이 조인 결과의 명확성과 코드의 유지보수성을 향상시킵니다.

코드 예제

import polars as pl

# 고객 정보
customers = pl.DataFrame({
    "id": [1, 2, 3],
    "name": ["김철수", "이영희", "박민수"],
    "created_at": ["2023-01-01", "2023-02-01", "2023-03-01"]
})

# 주문 정보
orders = pl.DataFrame({
    "order_id": [101, 102, 103],
    "customer_id": [1, 2, 3],
    "name": ["노트북 주문", "마우스 주문", "키보드 주문"],
    "created_at": ["2024-01-01", "2024-01-05", "2024-01-10"]
})

# Suffix를 사용하여 중복 컬럼명 구분
result = customers.join(
    orders,
    left_on="id",
    right_on="customer_id",
    how="inner",
    suffix="_order"  # 오른쪽 테이블 컬럼에 _order 접미사
)
print(result)

설명

이것이 하는 일: Suffix 파라미터는 조인 시 오른쪽 DataFrame의 중복 컬럼명에 지정한 문자열을 자동으로 붙여줍니다. 첫 번째로, customers와 orders 두 DataFrame 모두 name과 created_at 컬럼을 가지고 있습니다.

이 상태로 조인하면 컬럼명이 충돌하여 어느 것이 고객명이고 어느 것이 주문명인지 알 수 없게 됩니다. 그 다음으로, join() 메서드가 suffix="_order"와 함께 실행되면서 Polars는 오른쪽 테이블(orders)의 중복 컬럼에 자동으로 "_order"를 추가합니다.

name은 왼쪽 것은 그대로 "name", 오른쪽 것은 "name_order"가 되고, created_at도 마찬가지로 "created_at"과 "created_at_order"로 구분됩니다. 조인 키로 사용된 컬럼(id, customer_id)은 한쪽만 결과에 포함되므로 suffix가 적용되지 않습니다.

마지막으로, 결과 DataFrame에는 명확히 구분된 컬럼들이 포함됩니다. name은 고객명, name_order는 주문명, created_at은 고객 가입일, created_at_order는 주문일로 명확히 식별됩니다.

이렇게 하여 후속 분석에서 혼란 없이 각 컬럼을 정확히 참조할 수 있습니다. 여러분이 이 코드를 사용하면 여러 테이블을 순차적으로 조인할 때 각 테이블의 컬럼을 명확히 추적하거나, 같은 테이블을 self-join할 때 왼쪽과 오른쪽을 구분하거나, 데이터 계보(lineage)를 유지하면서 통합할 수 있습니다.

코드 가독성이 크게 향상되고 버그 발생 가능성이 줄어듭니다.

실전 팁

💡 의미 있는 suffix를 사용하세요. "_order", "_inventory", "_user" 같이 어느 테이블에서 왔는지 명확한 이름이 좋습니다.

💡 여러 테이블을 체인으로 조인할 때는 각 조인마다 다른 suffix를 사용하여 출처를 명확히 하세요.

💡 suffix 대신 조인 전에 select()로 필요한 컬럼만 선택하고 rename()하는 방법도 고려하세요. 더 명시적일 수 있습니다.

💡 Left Join에서 suffix를 사용하면 NULL 분석 시 어느 테이블에 데이터가 없는지 쉽게 파악할 수 있습니다.

💡 조인 후 컬럼이 너무 많다면 select()로 필요한 것만 선택하세요. suffix가 붙은 컬럼도 정확한 이름으로 선택 가능합니다.


9. Semi Join과 Anti Join - 존재 여부 기반 필터링

시작하며

여러분이 "주문한 적이 있는 고객만" 또는 "한 번도 주문하지 않은 고객만" 필터링하고 싶을 때 어떻게 하시나요? 조인으로는 행이 늘어나거나 중복이 생기는 문제가 있죠.

이런 문제는 필터링 목적의 조인에서 매우 흔합니다. 다른 테이블의 데이터를 가져오는 것이 아니라, 단지 존재 여부를 확인하여 필터링하고 싶을 때 일반 조인은 적합하지 않습니다.

바로 이럴 때 필요한 것이 Semi Join과 Anti Join입니다. Semi Join은 오른쪽 테이블에 일치하는 행이 있는 왼쪽 행만, Anti Join은 일치하지 않는 왼쪽 행만 반환하되, 오른쪽 테이블의 컬럼은 가져오지 않습니다.

개요

간단히 말해서, Semi Join은 "오른쪽에 존재하는 왼쪽 데이터만" 선택하고, Anti Join은 "오른쪽에 없는 왼쪽 데이터만" 선택하는 필터링 전용 조인입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 타겟 세그먼트 필터링이나 예외 케이스 추출에 최적화되어 있기 때문입니다.

예를 들어, 최근 30일 이내 구매 고객 목록을 추출할 때 Semi Join을 사용하면 고객 정보는 유지하면서 구매 여부만 필터링할 수 있고, 중복 걱정도 없습니다. Anti Join은 미구매 고객을 찾아 리타겟팅하는 데 완벽합니다.

기존에는 Inner Join 후 distinct()로 중복을 제거하거나, Left Join 후 NULL 필터링으로 구현했다면, 이제는 Semi/Anti Join으로 한 번에 정확하고 효율적으로 처리할 수 있습니다. Semi/Anti Join의 핵심 특징은 첫째, 결과에 왼쪽 테이블의 컬럼만 포함된다는 점, 둘째, 중복이 발생하지 않는다는 점(오른쪽에 여러 매칭이 있어도 왼쪽 행은 한 번만), 셋째, 메모리와 성능이 우수하다는 점입니다.

이러한 특징들이 필터링 작업을 매우 효율적으로 만듭니다.

코드 예제

import polars as pl

# 전체 고객 목록
customers = pl.DataFrame({
    "customer_id": [1, 2, 3, 4, 5],
    "name": ["김철수", "이영희", "박민수", "정수진", "최영수"],
    "email": ["kim@example.com", "lee@example.com", "park@example.com",
              "jung@example.com", "choi@example.com"]
})

# 최근 주문 이력
recent_orders = pl.DataFrame({
    "customer_id": [1, 1, 3],  # 김철수 2건, 박민수 1건
    "order_date": ["2024-01-01", "2024-01-05", "2024-01-10"]
})

# Semi Join - 주문한 적 있는 고객만 (고객 정보만 반환, 중복 없음)
active_customers = customers.join(recent_orders, on="customer_id", how="semi")
print("활성 고객 (Semi Join):")
print(active_customers)

# Anti Join - 주문한 적 없는 고객만
inactive_customers = customers.join(recent_orders, on="customer_id", how="anti")
print("\n미활성 고객 (Anti Join):")
print(inactive_customers)

설명

이것이 하는 일: Semi Join과 Anti Join은 오른쪽 테이블을 필터 조건으로만 사용하여 왼쪽 테이블의 행을 선택합니다. 첫 번째로, customers에는 5명의 고객이 있고, recent_orders에는 customer_id 1번(2건)과 3번(1건) 총 3개 주문이 있습니다.

2번, 4번, 5번 고객은 주문 이력이 없습니다. 그 다음으로, Semi Join(how="semi")이 실행되면 Polars는 recent_orders에 customer_id가 존재하는 customers 행만 선택합니다.

1번과 3번 고객이 해당되며, 1번 고객은 주문이 2건이지만 customers에서는 한 번만 나타나므로 결과에도 1개 행만 포함됩니다. 결과 DataFrame에는 customers의 모든 컬럼(customer_id, name, email)이 포함되지만, recent_orders의 컬럼(order_date)은 포함되지 않습니다.

세 번째로, Anti Join(how="anti")이 실행되면 반대로 recent_orders에 customer_id가 없는 customers 행만 선택합니다. 2번, 4번, 5번 고객이 해당되며, 역시 customers의 컬럼만 결과에 포함됩니다.

이는 마치 NOT IN 또는 NOT EXISTS SQL 쿼리와 같은 효과입니다. 여러분이 이 코드를 사용하면 활성/비활성 사용자 세그먼트를 깔끔하게 분리하거나, 특정 이벤트를 경험한 사용자만 추출하거나, 데이터 품질 검증에서 참조 무결성 위반 레코드를 찾거나, 마케팅 캠페인 타겟/제외 리스트를 생성할 수 있습니다.

Inner Join보다 훨씬 빠르고 메모리 효율적이며, 결과도 명확합니다.

실전 팁

💡 Semi Join은 SQL의 EXISTS, Anti Join은 NOT EXISTS와 동일합니다. SQL에 익숙하다면 이렇게 이해하면 쉽습니다.

💡 큰 테이블을 작은 테이블로 필터링할 때 Semi/Anti Join이 Inner/Left Join보다 10배 이상 빠를 수 있습니다.

💡 Anti Join으로 두 데이터셋의 차이를 빠르게 찾을 수 있습니다. 데이터 동기화 검증에 매우 유용합니다.

💡 여러 조건으로 필터링하려면 오른쪽 테이블을 미리 filter()한 후 Semi/Anti Join을 수행하세요.

💡 Semi/Anti Join 결과가 비어있는지 확인하여 데이터 존재 여부를 검증할 수 있습니다. len(result) == 0이면 조건을 만족하는 데이터가 없는 것입니다.


10. Diagonal Concat - 스키마가 다른 데이터 통합하기

시작하며

여러분이 여러 버전의 로그 파일이나 진화하는 스키마의 데이터를 통합할 때 이런 문제를 겪어본 적 있나요? 버전 1에는 5개 컬럼, 버전 2에는 7개 컬럼이 있어서 일반 concat으로는 에러가 발생하는 상황이요.

이런 문제는 시간에 따라 스키마가 변경되는 시스템, 여러 소스의 이질적인 데이터, 또는 선택적 필드가 있는 API 응답 데이터에서 매우 흔합니다. 완벽히 일치하는 스키마를 요구하면 통합 자체가 불가능해집니다.

바로 이럴 때 필요한 것이 Diagonal Concat입니다. 각 DataFrame의 컬럼이 다르더라도 모든 컬럼을 포함하는 통합 스키마를 만들고, 없는 컬럼은 NULL로 채워서 유연하게 통합합니다.

개요

간단히 말해서, Diagonal Concat은 컬럼 구조가 서로 다른 여러 DataFrame을 통합하되, 모든 컬럼을 보존하고 없는 값은 NULL로 채우는 방식입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 실제 데이터는 시간과 소스에 따라 스키마가 다양하기 때문입니다.

예를 들어, 초기 버전의 사용자 테이블에는 name과 email만 있었는데, 나중에 phone과 address가 추가되었다면, 전체 히스토리를 분석하려면 두 스키마를 통합해야 합니다. 일반 concat은 컬럼 불일치로 에러가 나지만, Diagonal Concat은 자동으로 처리합니다.

기존에는 수동으로 누락된 컬럼을 NULL로 채우는 전처리를 했다면, 이제는 how="diagonal"로 Polars가 자동으로 처리해줍니다. Diagonal Concat의 핵심 특징은 첫째, 모든 DataFrame의 모든 컬럼이 결과에 포함된다는 점, 둘째, 특정 DataFrame에 없는 컬럼은 NULL로 채워진다는 점, 셋째, 컬럼 순서는 첫 번째 DataFrame을 기준으로 한 후 새로운 컬럼이 추가된다는 점입니다.

이러한 특징들이 이질적인 데이터의 통합을 가능하게 합니다.

코드 예제

import polars as pl

# 구버전 사용자 데이터 (초기 스키마)
old_users = pl.DataFrame({
    "user_id": [1, 2],
    "name": ["김철수", "이영희"],
    "email": ["kim@example.com", "lee@example.com"]
})

# 신버전 사용자 데이터 (확장된 스키마)
new_users = pl.DataFrame({
    "user_id": [3, 4],
    "name": ["박민수", "정수진"],
    "email": ["park@example.com", "jung@example.com"],
    "phone": ["010-1111-2222", "010-3333-4444"],
    "address": ["서울", "부산"]
})

# Diagonal Concat - 컬럼이 달라도 통합 가능
all_users = pl.concat([old_users, new_users], how="diagonal")
print("통합된 사용자 데이터:")
print(all_users)
print(f"\n총 {len(all_users)}명, {len(all_users.columns)}개 컬럼")

설명

이것이 하는 일: Diagonal Concat은 서로 다른 스키마의 DataFrame들을 모든 컬럼을 포함하는 슈퍼셋 스키마로 통합합니다. 첫 번째로, old_users는 3개 컬럼(user_id, name, email)을 가지고 있고, new_users는 5개 컬럼(user_id, name, email, phone, address)을 가지고 있습니다.

일반적인 vertical concat은 컬럼 수와 이름이 정확히 일치해야 하므로 이 경우 에러가 발생합니다. 그 다음으로, pl.concat()이 how="diagonal"로 실행되면서 Polars는 두 DataFrame의 모든 고유 컬럼을 수집합니다.

결과 스키마는 user_id, name, email, phone, address 5개 컬럼이 됩니다. 그런 다음 각 DataFrame을 이 스키마에 맞춰 변환하는데, old_users에는 phone과 address가 없으므로 해당 셀을 null로 채웁니다.

마지막으로, 결과 DataFrame에는 4개의 행이 모두 포함되며, 처음 2개 행(구버전)은 phone과 address가 null이고, 나중 2개 행(신버전)은 모든 컬럼에 값이 있습니다. 이렇게 하여 데이터 손실 없이 전체 히스토리를 통합하고, NULL 패턴을 분석하면 스키마 진화 과정도 파악할 수 있습니다.

여러분이 이 코드를 사용하면 여러 API 버전의 응답 데이터를 통합하거나, 시간에 따라 진화한 로그 데이터를 분석하거나, 여러 소스의 이질적인 CSV 파일을 하나로 병합하거나, A/B 테스트에서 다른 변형의 이벤트 데이터를 통합할 수 있습니다. 스키마 불일치로 인한 데이터 손실을 방지하고 완전한 분석을 가능하게 합니다.

실전 팁

💡 Diagonal Concat 후 각 컬럼의 NULL 비율을 확인하면 스키마 변경 시점이나 데이터 품질 이슈를 파악할 수 있습니다.

💡 동일한 의미이지만 다른 이름의 컬럼(예: "phone"과 "mobile")이 있다면 미리 rename()으로 통일하세요.

💡 결과 DataFrame의 컬럼이 너무 많아지면 select()로 핵심 컬럼만 선택하거나, drop_nulls()로 NULL이 많은 컬럼을 제거하세요.

💡 대용량 파일을 Diagonal Concat할 때는 lazy API(pl.scan_csv())를 사용하면 메모리 효율이 크게 향상됩니다.

💡 Diagonal Concat은 vertical concat의 특수 케이스입니다. 컬럼이 일치하면 vertical과 동일하게 동작하므로 안전하게 항상 사용 가능합니다.


#Polars#Join#DataFrame#DataIntegration#DataAnalysis#데이터분석,Python,Polars

댓글 (0)

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