이미지 로딩 중...
AI Generated
2025. 11. 15. · 3 Views
Polars 데이터 결합 완벽 가이드
Polars에서 여러 데이터프레임을 효율적으로 결합하는 방법을 배웁니다. join, concat, merge 등 다양한 결합 방법과 실무 활용 팁을 다룹니다.
목차
- join - 키 기반 데이터 결합
- concat - 수직/수평 데이터 결합
- vstack - 수직 결합의 최적화 버전
- hstack - 수평 결합으로 컬럼 추가
- cross join - 모든 조합 생성
- join_asof - 시간 기반 근접 결합
- semi join - 존재 여부로 필터링
- anti join - 존재하지 않는 행 찾기
1. join - 키 기반 데이터 결합
시작하며
여러분이 고객 정보와 주문 내역이 각각 다른 테이블에 저장되어 있는데, 이 둘을 합쳐서 분석해야 하는 상황을 겪어본 적 있나요? 예를 들어, 고객 ID를 기준으로 고객의 이름, 연령과 주문 금액, 주문 날짜를 함께 보고 싶을 때 말이죠.
이런 문제는 실제 데이터 분석 현장에서 가장 자주 발생합니다. 데이터가 정규화되어 여러 테이블로 나뉘어 있기 때문에, 분석을 위해서는 관련된 데이터를 하나로 합쳐야 하는데, 이 과정에서 성능과 정확성 모두 중요합니다.
바로 이럴 때 필요한 것이 Polars의 join입니다. join은 공통 키를 기준으로 두 데이터프레임을 연결하여, SQL의 JOIN처럼 강력하면서도 Polars의 빠른 성능을 그대로 활용할 수 있습니다.
개요
간단히 말해서, join은 두 데이터프레임을 공통 컬럼(키)을 기준으로 연결하는 작업입니다. 데이터 분석에서 정보가 여러 소스에 분산되어 있을 때, join을 사용하면 이를 하나의 통합된 뷰로 만들 수 있습니다.
예를 들어, 사용자 정보 테이블과 구매 이력 테이블을 user_id로 연결하여 "어떤 사용자가 무엇을 구매했는지" 한눈에 볼 수 있습니다. 기존 pandas에서는 merge() 메서드를 사용했다면, Polars에서는 join() 메서드로 더 빠르고 메모리 효율적으로 같은 작업을 수행할 수 있습니다.
Polars의 join은 inner, left, outer, cross 등 다양한 join 타입을 지원하며, 여러 컬럼을 기준으로 결합할 수도 있습니다. 이러한 유연성 덕분에 복잡한 데이터 관계도 쉽게 표현할 수 있습니다.
코드 예제
import polars as pl
# 고객 정보 데이터프레임
customers = pl.DataFrame({
"customer_id": [1, 2, 3, 4],
"name": ["김철수", "이영희", "박민수", "정수진"],
"age": [28, 34, 45, 29]
})
# 주문 정보 데이터프레임
orders = pl.DataFrame({
"order_id": [101, 102, 103, 104],
"customer_id": [1, 2, 1, 5],
"amount": [50000, 75000, 30000, 20000]
})
# customer_id를 기준으로 left join 수행
result = customers.join(orders, on="customer_id", how="left")
print(result)
설명
이것이 하는 일: 이 코드는 고객 정보와 주문 정보를 customer_id라는 공통 키를 기준으로 연결하여, 각 고객이 어떤 주문을 했는지 보여주는 통합 테이블을 만듭니다. 첫 번째로, 두 개의 데이터프레임을 생성합니다.
customers는 고객의 기본 정보를, orders는 주문 내역을 담고 있습니다. 실무에서는 이들이 데이터베이스의 서로 다른 테이블이거나 다른 CSV 파일일 수 있습니다.
그 다음으로, join() 메서드가 실행됩니다. on="customer_id" 파라미터는 어떤 컬럼을 기준으로 매칭할지 지정하고, how="left"는 왼쪽 테이블(customers)의 모든 행을 유지하면서 오른쪽 테이블(orders)의 매칭되는 정보를 가져옵니다.
customer_id가 5인 주문은 고객 정보에 없으므로 결과에 나타나지 않습니다. 마지막으로, 결과 데이터프레임이 생성되어 각 고객의 이름, 나이와 함께 주문 정보가 하나의 행에 표시됩니다.
김철수(customer_id=1)는 두 번 주문했으므로 두 행으로 나타나며, 주문하지 않은 고객은 order_id와 amount가 null로 표시됩니다. 여러분이 이 코드를 사용하면 분산된 데이터를 통합하여 분석할 수 있으며, Polars의 병렬 처리 덕분에 대용량 데이터에서도 빠른 성능을 얻을 수 있습니다.
특히 수백만 행 이상의 데이터를 다룰 때 pandas 대비 10배 이상 빠른 속도를 경험할 수 있습니다.
실전 팁
💡 how 파라미터를 상황에 맞게 선택하세요: inner(양쪽 모두 있는 것만), left(왼쪽 전체 유지), outer(양쪽 전체), cross(카테시안 곱) 중 분석 목적에 맞는 것을 사용하세요.
💡 조인 전에 키 컬럼의 중복과 null 값을 확인하세요. 예상치 못한 중복이나 null이 있으면 결과 행 수가 폭발적으로 증가하거나 누락될 수 있습니다.
💡 대용량 데이터는 lazy evaluation을 사용하세요: lazy().join().collect()로 실행하면 Polars가 쿼리를 최적화하여 더 빠른 성능을 제공합니다.
💡 여러 컬럼으로 조인할 때는 on=["col1", "col2"] 형태로 리스트를 전달하세요. 복합 키 조인이 필요한 경우 유용합니다.
💡 조인 후 suffix 파라미터로 중복 컬럼명을 구분하세요: suffix="_order"처럼 지정하면 같은 이름의 컬럼이 있을 때 혼란을 방지할 수 있습니다.
2. concat - 수직/수평 데이터 결합
시작하며
여러분이 월별로 나뉜 판매 데이터 파일들을 하나로 합쳐서 연간 분석을 해야 하는 상황을 겪어본 적 있나요? 1월부터 12월까지 각각의 CSV 파일이 있고, 모두 같은 구조인데 단지 기간만 다른 경우 말이죠.
이런 문제는 데이터 수집 과정에서 매우 흔합니다. 로그 파일이 일별로 분리되어 있거나, 여러 지점의 데이터가 개별 파일로 관리될 때, 전체 데이터를 분석하려면 이들을 하나로 합쳐야 합니다.
바로 이럴 때 필요한 것이 concat입니다. concat은 같은 구조의 데이터프레임들을 위아래로 쌓거나(수직 결합) 좌우로 붙이는(수평 결합) 간단하면서도 필수적인 작업을 수행합니다.
개요
간단히 말해서, concat은 여러 데이터프레임을 하나로 연결하는 작업으로, 행 방향(vertical) 또는 열 방향(horizontal)으로 결합할 수 있습니다. 데이터 분석에서 같은 형식의 데이터가 여러 조각으로 나뉘어 있을 때, concat을 사용하면 이를 하나의 큰 데이터셋으로 만들 수 있습니다.
예를 들어, 각 분기별 매출 데이터를 하나로 합쳐 연간 트렌드를 분석하거나, 여러 센서의 측정값을 옆으로 붙여 종합 모니터링 테이블을 만들 수 있습니다. 기존 pandas에서는 pd.concat()을 사용했다면, Polars에서는 pl.concat()으로 더 빠르게 같은 작업을 수행할 수 있습니다.
특히 대용량 데이터에서 메모리 복사를 최소화하여 효율적입니다. concat의 핵심 특징은 단순함과 속도입니다.
join처럼 복잡한 매칭 로직 없이 그냥 데이터를 쌓거나 붙이기만 하므로 빠르며, how 파라미터로 컬럼이 다를 때의 동작도 제어할 수 있습니다.
코드 예제
import polars as pl
# 1분기 매출 데이터
q1_sales = pl.DataFrame({
"month": ["1월", "2월", "3월"],
"sales": [1000, 1200, 1100]
})
# 2분기 매출 데이터
q2_sales = pl.DataFrame({
"month": ["4월", "5월", "6월"],
"sales": [1300, 1400, 1250]
})
# 수직 결합 (행 방향으로 쌓기)
yearly_sales = pl.concat([q1_sales, q2_sales], how="vertical")
print(yearly_sales)
# 수평 결합 예시 (열 방향으로 붙이기)
result_horizontal = pl.concat([q1_sales, q2_sales], how="horizontal")
설명
이것이 하는 일: 이 코드는 분기별로 나뉜 매출 데이터를 하나의 연간 데이터프레임으로 통합하여, 전체 기간의 매출을 한눈에 볼 수 있게 만듭니다. 첫 번째로, 두 개의 분기별 데이터프레임을 생성합니다.
q1_sales와 q2_sales는 완전히 동일한 구조(같은 컬럼명과 타입)를 가지고 있으며, 단지 데이터의 기간만 다릅니다. 실무에서는 이들이 서로 다른 파일이나 데이터베이스 쿼리 결과일 수 있습니다.
그 다음으로, pl.concat() 함수가 실행됩니다. 첫 번째 인자로 결합할 데이터프레임들의 리스트를 받고, how="vertical"은 행 방향으로 쌓겠다는 의미입니다.
이렇게 하면 q1_sales의 3개 행 아래에 q2_sales의 3개 행이 추가되어 총 6개 행의 데이터프레임이 만들어집니다. 마지막으로, yearly_sales라는 통합된 데이터프레임이 생성되어 1월부터 6월까지의 모든 매출 데이터를 담게 됩니다.
how="horizontal"을 사용하면 옆으로 붙여서 각 분기의 데이터를 나란히 비교할 수도 있습니다. 여러분이 이 코드를 사용하면 흩어진 데이터를 빠르게 통합할 수 있으며, 수십 개의 파일도 리스트 컴프리헨션과 함께 사용하여 한 번에 합칠 수 있습니다.
Polars의 메모리 효율성 덕분에 대용량 데이터도 안전하게 처리됩니다.
실전 팁
💡 how 파라미터를 이해하세요: "vertical"은 행 쌓기(기본값), "horizontal"은 열 붙이기, "diagonal"은 컬럼이 달라도 모두 포함하여 쌓기입니다.
💡 컬럼 순서와 타입이 일치하는지 확인하세요. 컬럼명이 다르거나 타입이 다르면 예상치 못한 결과가 나올 수 있으므로, 사전에 스키마를 확인하세요.
💡 여러 파일을 한 번에 결합할 때는 리스트 컴프리헨션을 활용하세요: pl.concat([pl.read_csv(f) for f in file_list])처럼 간결하게 작성할 수 있습니다.
💡 rechunk=True 옵션을 사용하면 메모리를 더 효율적으로 사용할 수 있습니다. concat 후 데이터가 여러 청크로 나뉘어 있을 때 하나로 합쳐줍니다.
💡 수평 결합 시 행 개수가 다르면 오류가 발생합니다. 이런 경우 join을 사용하거나, 행 개수를 맞춘 후 concat하세요.
3. vstack - 수직 결합의 최적화 버전
시작하며
여러분이 실시간으로 들어오는 센서 데이터를 기존 데이터프레임에 계속 추가해야 하는 상황을 겪어본 적 있나요? 예를 들어, 매 시간마다 새로운 측정값이 들어올 때마다 이를 누적된 데이터에 추가해야 하는 경우 말이죠.
이런 문제는 스트리밍 데이터나 배치 처리에서 자주 발생합니다. 데이터가 순차적으로 도착하므로, 기존 데이터셋에 새 데이터를 효율적으로 추가하는 것이 성능의 핵심입니다.
바로 이럴 때 필요한 것이 vstack입니다. vstack은 concat의 수직 결합과 같은 역할을 하지만, 더 빠르고 메모리 효율적으로 데이터프레임을 위아래로 쌓을 수 있습니다.
개요
간단히 말해서, vstack은 두 데이터프레임을 수직으로(행 방향) 쌓는 작업으로, concat보다 더 직접적이고 빠른 방법입니다. 동일한 스키마를 가진 데이터프레임을 반복적으로 결합할 때, vstack을 사용하면 최소한의 오버헤드로 데이터를 추가할 수 있습니다.
예를 들어, 일별 로그를 누적하거나, 여러 소스의 동일 형식 데이터를 하나로 모을 때 유용합니다. 기존 concat(..., how="vertical")과 기능은 같지만, vstack은 메서드 형태로 제공되어 더 직관적이며, 내부적으로 최적화되어 있어 대량의 데이터를 다룰 때 성능 차이가 납니다.
vstack의 핵심은 단순성과 성능입니다. 별도의 옵션 없이 그냥 쌓기만 하므로 코드가 간결하고, 컬럼 구조가 정확히 일치해야 하므로 데이터 무결성도 보장됩니다.
코드 예제
import polars as pl
# 기존 고객 데이터
existing_customers = pl.DataFrame({
"id": [1, 2, 3],
"name": ["김철수", "이영희", "박민수"],
"city": ["서울", "부산", "대구"]
})
# 새로 가입한 고객 데이터
new_customers = pl.DataFrame({
"id": [4, 5],
"name": ["정수진", "최민호"],
"city": ["인천", "광주"]
})
# vstack으로 수직 결합
all_customers = existing_customers.vstack(new_customers)
print(all_customers)
# 결과: 5개 행을 가진 통합 데이터프레임
설명
이것이 하는 일: 이 코드는 기존 고객 목록에 새로 가입한 고객 데이터를 추가하여, 전체 고객 데이터베이스를 업데이트합니다. 첫 번째로, 두 데이터프레임을 생성합니다.
existing_customers는 이미 있던 고객 정보를, new_customers는 새로 추가할 고객 정보를 담고 있습니다. 중요한 점은 두 데이터프레임이 완전히 동일한 컬럼(id, name, city)과 타입을 가져야 한다는 것입니다.
그 다음으로, vstack() 메서드가 실행됩니다. 이는 기존 데이터프레임의 메서드로 호출되며, 인자로 받은 new_customers를 아래에 쌓습니다.
내부적으로 Polars는 메모리를 효율적으로 재사용하여 불필요한 복사를 최소화합니다. 마지막으로, all_customers라는 통합된 데이터프레임이 생성되어 기존 3명의 고객에 새로운 2명이 추가된 총 5명의 고객 정보를 담게 됩니다.
ID는 1부터 5까지 순차적으로 이어지며, 모든 정보가 하나의 테이블에 정리됩니다. 여러분이 이 코드를 사용하면 데이터 추가 작업을 매우 빠르게 수행할 수 있으며, 반복문 안에서 여러 번 호출해도 성능 저하가 적습니다.
특히 ETL 파이프라인에서 배치 단위로 데이터를 누적할 때 효과적입니다.
실전 팁
💡 스키마가 정확히 일치해야 합니다. 컬럼 순서, 이름, 타입이 모두 같아야 하므로, 사전에 select()나 cast()로 맞춰주세요.
💡 여러 데이터프레임을 한 번에 쌓을 때는 for 루프보다 리스트를 만들어 한 번에 처리하세요: df = df.vstack(pl.concat([df1, df2, df3]))가 더 효율적입니다.
💡 in-place 연산이 아닙니다. vstack은 새로운 데이터프레임을 반환하므로 결과를 변수에 할당해야 합니다: df = df.vstack(new_df)
💡 대용량 데이터는 lazy mode에서 사용하세요: df.lazy().vstack(new_df.lazy()).collect()로 최적화된 실행 계획을 얻을 수 있습니다.
💡 빈 데이터프레임도 vstack 가능합니다. 초기화 시 빈 데이터프레임을 만들고 반복문에서 데이터를 추가하는 패턴으로 활용할 수 있습니다.
4. hstack - 수평 결합으로 컬럼 추가
시작하며
여러분이 고객 기본 정보 테이블에 새로 계산한 통계 컬럼들을 추가해야 하는 상황을 겪어본 적 있나요? 예를 들어, 각 고객의 구매 횟수, 평균 구매액, 최근 방문일 같은 파생 정보를 옆에 붙여서 하나의 종합 테이블을 만들어야 할 때 말이죠.
이런 문제는 피처 엔지니어링이나 데이터 보강 작업에서 매우 흔합니다. 원본 데이터는 그대로 두고 새로운 컬럼만 추가하고 싶은데, join을 쓰기엔 키가 명확하지 않거나 단순히 옆으로 붙이기만 하면 되는 경우가 많습니다.
바로 이럴 때 필요한 것이 hstack입니다. hstack은 데이터프레임에 새로운 컬럼들을 수평으로(열 방향) 추가하여, 여러 소스의 정보를 하나의 넓은 테이블로 통합할 수 있습니다.
개요
간단히 말해서, hstack은 두 데이터프레임을 수평으로(열 방향) 결합하여, 기존 데이터에 새로운 컬럼을 추가하는 작업입니다. 데이터 분석에서 기존 데이터셋에 계산된 피처나 외부 데이터를 추가할 때, hstack을 사용하면 간단하게 컬럼을 확장할 수 있습니다.
예를 들어, 고객 테이블에 행동 분석 결과를 추가하거나, 센서 데이터에 파생된 통계값을 붙이는 경우에 유용합니다. 기존 concat(..., how="horizontal")과 같은 기능이지만, hstack은 메서드 형태로 제공되어 체이닝이 쉽고, with_columns()보다 여러 컬럼을 한 번에 추가할 때 더 직관적입니다.
hstack의 핵심은 행 개수가 일치해야 한다는 것입니다. 두 데이터프레임의 행 수가 같아야 하며, 순서도 의미가 있으므로 인덱스나 정렬 상태를 확인해야 합니다.
코드 예제
import polars as pl
# 고객 기본 정보
customers = pl.DataFrame({
"customer_id": [1, 2, 3],
"name": ["김철수", "이영희", "박민수"]
})
# 고객별 구매 통계 (같은 순서, 같은 행 개수)
purchase_stats = pl.DataFrame({
"purchase_count": [5, 3, 8],
"avg_amount": [45000, 32000, 58000],
"last_visit": ["2025-01-10", "2025-01-05", "2025-01-12"]
})
# hstack으로 수평 결합
customer_profile = customers.hstack(purchase_stats)
print(customer_profile)
# 결과: customer_id, name, purchase_count, avg_amount, last_visit 컬럼을 가진 데이터프레임
설명
이것이 하는 일: 이 코드는 고객의 기본 정보와 구매 통계를 옆으로 붙여서, 각 고객의 모든 정보를 하나의 행에서 볼 수 있는 종합 프로필을 만듭니다. 첫 번째로, 두 개의 데이터프레임을 생성합니다.
customers는 고객의 식별 정보를, purchase_stats는 각 고객의 구매 행동 통계를 담고 있습니다. 매우 중요한 점은 두 데이터프레임이 정확히 같은 행 개수(3개)를 가지며, 각 행이 같은 고객을 나타내야 한다는 것입니다.
그 다음으로, hstack() 메서드가 실행됩니다. 이는 customers 데이터프레임의 오른쪽에 purchase_stats의 모든 컬럼을 추가합니다.
첫 번째 고객(김철수)의 구매 통계가 첫 번째 행에, 두 번째 고객(이영희)의 통계가 두 번째 행에 정확히 매칭됩니다. 마지막으로, customer_profile이라는 통합된 데이터프레임이 생성되어 각 고객의 이름과 함께 구매 횟수, 평균 구매액, 최근 방문일을 한눈에 볼 수 있게 됩니다.
이제 5개의 컬럼을 가진 넓은 테이블이 만들어져, 고객 분석에 필요한 모든 정보를 제공합니다. 여러분이 이 코드를 사용하면 서로 다른 소스의 정보를 빠르게 통합할 수 있으며, 머신러닝을 위한 피처 테이블을 구성할 때 특히 유용합니다.
계산된 피처들을 별도로 준비한 후 원본 데이터에 추가하는 워크플로우에 적합합니다.
실전 팁
💡 행 개수가 정확히 일치하는지 확인하세요. len()으로 미리 체크하지 않으면 런타임 오류가 발생합니다.
💡 행의 순서가 의미를 가집니다. 정렬 상태가 다르면 잘못된 매칭이 발생하므로, 가능하면 키 기반 join을 사용하거나 인덱스를 확인하세요.
💡 중복 컬럼명이 있으면 오류가 발생합니다. rename()으로 미리 컬럼명을 변경하여 충돌을 피하세요.
💡 여러 데이터프레임을 한 번에 추가할 때는 체이닝하세요: df.hstack(df1).hstack(df2)처럼 연속 호출 가능합니다.
💡 with_columns()와의 차이를 이해하세요: with_columns()는 표현식 기반으로 계산하면서 추가하지만, hstack()은 이미 준비된 데이터프레임을 그대로 붙입니다.
5. cross join - 모든 조합 생성
시작하며
여러분이 여러 제품과 여러 지역의 모든 조합에 대해 판매 예측을 준비해야 하는 상황을 겪어본 적 있나요? 예를 들어, 10개 제품과 5개 지역이 있을 때, 총 50개의 모든 조합을 만들어서 각각에 대해 분석을 수행해야 할 때 말이죠.
이런 문제는 시나리오 분석이나 그리드 검색에서 자주 발생합니다. 모든 가능한 조합을 생성해야 하는데, 수동으로 하기엔 너무 많고 복잡하며, 실수하기 쉽습니다.
바로 이럴 때 필요한 것이 cross join(또는 cartesian join)입니다. cross join은 두 데이터프레임의 모든 행 조합을 생성하여, 완전한 조합 테이블을 만들어줍니다.
개요
간단히 말해서, cross join은 두 데이터프레임의 각 행을 서로 모든 행과 결합하여, 가능한 모든 조합을 만드는 작업입니다. 데이터 분석에서 파라미터 조합, 시나리오 생성, A/B 테스트 설계 등을 할 때 cross join을 사용하면 빠뜨림 없이 모든 경우를 생성할 수 있습니다.
예를 들어, 모든 제품 × 모든 가격대 × 모든 프로모션 조합을 만들어 수익 시뮬레이션을 돌릴 수 있습니다. SQL의 CROSS JOIN과 동일한 개념이며, 결과 행 수는 두 데이터프레임의 행 수를 곱한 값이 됩니다.
주의할 점은 행 수가 폭발적으로 증가할 수 있다는 것입니다. Polars에서는 join(..., how="cross")로 사용하며, 키 컬럼 없이 모든 조합을 생성합니다.
이는 매우 강력하지만 신중하게 사용해야 하는 도구입니다.
코드 예제
import polars as pl
# 제품 목록
products = pl.DataFrame({
"product": ["노트북", "마우스", "키보드"]
})
# 지역 목록
regions = pl.DataFrame({
"region": ["서울", "부산"]
})
# cross join으로 모든 조합 생성
product_region_combinations = products.join(regions, how="cross")
print(product_region_combinations)
# 결과: 3 × 2 = 6개 행
# (노트북, 서울), (노트북, 부산), (마우스, 서울), ...
설명
이것이 하는 일: 이 코드는 모든 제품과 모든 지역의 조합을 생성하여, 각 제품을 각 지역에서 판매하는 시나리오를 나타내는 테이블을 만듭니다. 첫 번째로, 두 개의 작은 데이터프레임을 생성합니다.
products는 3개의 제품을, regions는 2개의 지역을 담고 있습니다. 이들은 서로 연관성이 없지만, 우리는 모든 가능한 조합을 원합니다.
그 다음으로, join() 메서드가 how="cross" 파라미터와 함께 실행됩니다. 이때 on 파라미터가 없다는 점을 주목하세요.
cross join은 매칭 키 없이 단순히 모든 조합을 만들기 때문입니다. products의 각 행이 regions의 모든 행과 결합됩니다.
마지막으로, 6개 행을 가진 결과 데이터프레임이 생성됩니다. 노트북은 서울과 부산 두 번 나타나고, 마우스와 키보드도 마찬가지입니다.
이렇게 만들어진 조합 테이블에 나중에 판매 예측값이나 재고 정보를 추가할 수 있습니다. 여러분이 이 코드를 사용하면 복잡한 조합 생성을 자동화할 수 있으며, 실험 설계나 시뮬레이션에서 빠뜨린 조합 없이 완전한 분석을 수행할 수 있습니다.
그러나 대규모 테이블에는 주의해서 사용해야 합니다.
실전 팁
💡 결과 크기를 미리 계산하세요: rows1 × rows2가 결과 행 수이므로, 메모리 부족을 방지하기 위해 사전에 확인하세요.
💡 필터링은 cross join 전에 하세요. 불필요한 행을 미리 제거하면 조합 수를 크게 줄일 수 있습니다.
💡 실험 설계(DoE)나 그리드 검색에 활용하세요. 모든 파라미터 조합을 생성한 후 각각에 대해 모델을 학습시킬 수 있습니다.
💡 join(..., how="cross") 대신 실제로는 드물게 사용됩니다. 정말 필요한지 다시 확인하고, 대부분의 경우 inner join이나 left join으로 해결 가능합니다.
💡 결과에 중복 제거가 필요할 수 있습니다. unique()나 distinct()로 후처리하여 의미 있는 조합만 남기세요.
6. join_asof - 시간 기반 근접 결합
시작하며
여러분이 주식 거래 시간과 뉴스 발표 시간을 매칭해야 하는데, 정확히 같은 시간이 아니라 가장 가까운 이전 시점을 찾아야 하는 상황을 겪어본 적 있나요? 예를 들어, 오후 2시 35분에 거래가 발생했을 때, 그 이전 가장 최근의 뉴스(오후 2시 30분 발표)와 연결해야 하는 경우 말이죠.
이런 문제는 시계열 데이터 분석에서 매우 흔합니다. 센서 데이터, 금융 데이터, 로그 데이터처럼 타임스탬프가 있지만 정확히 일치하지 않는 데이터를 결합할 때, 일반 join으로는 매칭이 안 됩니다.
바로 이럴 때 필요한 것이 join_asof입니다. join_asof는 시간이나 순서 기반으로 가장 가까운 값을 찾아 결합하는, 시계열 분석의 필수 도구입니다.
개요
간단히 말해서, join_asof는 정확히 일치하지 않는 키 값에 대해 가장 가까운(근접한) 값을 찾아 결합하는 작업으로, 주로 시간 기반 데이터에 사용됩니다. 시계열 데이터 분석에서 이벤트 간 관계를 파악할 때, join_asof를 사용하면 정확한 타임스탬프 매칭 없이도 시간적으로 가까운 데이터를 연결할 수 있습니다.
예를 들어, 거래 시점 직전의 가격 정보를 가져오거나, 센서 측정 시점에 가장 가까운 온도 데이터를 매칭할 수 있습니다. pandas의 merge_asof()와 유사하지만, Polars는 더 빠르고 메모리 효율적이며, strategy 파라미터로 방향(backward, forward, nearest)을 제어할 수 있습니다.
join_asof의 핵심은 "asof" 컬럼이 정렬되어 있어야 한다는 것입니다. 시간 컬럼이 오름차순으로 정렬되어 있어야 정확한 매칭이 가능합니다.
코드 예제
import polars as pl
from datetime import datetime
# 거래 데이터
trades = pl.DataFrame({
"trade_time": ["2025-01-14 14:35:00", "2025-01-14 14:42:00", "2025-01-14 14:50:00"],
"ticker": ["AAPL", "AAPL", "AAPL"],
"quantity": [100, 200, 150]
}).with_columns(pl.col("trade_time").str.strptime(pl.Datetime, "%Y-%m-%d %H:%M:%S"))
# 뉴스 발표 데이터
news = pl.DataFrame({
"news_time": ["2025-01-14 14:30:00", "2025-01-14 14:45:00"],
"headline": ["실적 발표", "CEO 인터뷰"]
}).with_columns(pl.col("news_time").str.strptime(pl.Datetime, "%Y-%m-%d %H:%M:%S"))
# asof join으로 거래 직전 뉴스 매칭 (backward strategy)
result = trades.join_asof(news, left_on="trade_time", right_on="news_time", strategy="backward")
print(result)
설명
이것이 하는 일: 이 코드는 각 거래 시점에 가장 가까운 이전 뉴스를 찾아 연결하여, 뉴스가 거래에 미친 영향을 분석할 수 있는 데이터를 만듭니다. 첫 번째로, 두 데이터프레임을 생성합니다.
trades는 거래 정보를, news는 뉴스 발표 정보를 담고 있으며, 각각 타임스탬프 컬럼을 가지고 있습니다. with_columns()를 사용하여 문자열을 Datetime 타입으로 변환하는데, 이는 asof join이 정확한 시간 비교를 하기 위해 필요합니다.
그 다음으로, join_asof() 메서드가 실행됩니다. left_on="trade_time"은 왼쪽 테이블의 시간 컬럼을, right_on="news_time"은 오른쪽 테이블의 시간 컬럼을 지정합니다.
strategy="backward"는 각 거래 시점 이전의 가장 가까운 뉴스를 찾으라는 의미입니다. 마지막으로, 결과 데이터프레임이 생성됩니다.
14:35 거래는 14:30 뉴스와, 14:42와 14:50 거래는 14:45 뉴스와 매칭됩니다. 각 거래가 어떤 뉴스의 영향권에 있었는지 파악할 수 있게 됩니다.
여러분이 이 코드를 사용하면 시계열 이벤트 간의 관계를 쉽게 분석할 수 있으며, 금융, IoT, 로그 분석 등 다양한 분야에서 활용 가능합니다. 정확한 타임스탬프 일치를 요구하지 않으므로 실제 데이터에 매우 유용합니다.
실전 팁
💡 asof 컬럼은 반드시 정렬되어 있어야 합니다. sort()로 미리 정렬하지 않으면 잘못된 결과가 나올 수 있습니다.
💡 strategy를 상황에 맞게 선택하세요: "backward"(이전 값), "forward"(이후 값), "nearest"(가장 가까운 값) 중 분석 목적에 맞는 것을 사용하세요.
💡 tolerance 파라미터로 최대 허용 거리를 설정하세요. 너무 먼 값과 매칭되는 것을 방지하려면 tolerance="5m" 같이 시간 간격을 지정할 수 있습니다.
💡 by 파라미터로 그룹별 매칭을 할 수 있습니다. 예를 들어, by="ticker"를 추가하면 각 주식 종목별로 독립적인 asof join이 수행됩니다.
💡 타임존 처리에 주의하세요. Datetime 컬럼의 타임존이 일치하지 않으면 예상치 못한 결과가 나올 수 있으므로, 사전에 통일하세요.
7. semi join - 존재 여부로 필터링
시작하며
여러분이 전체 고객 목록에서 실제로 구매 이력이 있는 고객만 필터링해야 하는 상황을 겪어본 적 있나요? 예를 들어, 10만 명의 가입자 중에서 최근 1년간 한 번이라도 구매한 사람만 선별하여 타겟 마케팅을 하고 싶을 때 말이죠.
이런 문제는 데이터 필터링과 세그먼테이션에서 자주 발생합니다. 다른 테이블에 존재하는지 여부만 확인하고 싶은데, inner join을 쓰면 중복 행이 생기고, 직접 필터링하려면 코드가 복잡해집니다.
바로 이럴 때 필요한 것이 semi join입니다. semi join은 오른쪽 테이블에 매칭되는 행이 있는 왼쪽 테이블의 행만 반환하며, 중복 없이 깔끔하게 필터링합니다.
개요
간단히 말해서, semi join은 오른쪽 테이블에 존재하는 키를 가진 왼쪽 테이블의 행만 선택하는 작업으로, 존재 여부 기반 필터링입니다. 데이터 분석에서 특정 조건을 만족하는 레코드만 추출할 때, semi join을 사용하면 중복 없이 효율적으로 필터링할 수 있습니다.
예를 들어, 특정 이벤트에 참여한 사용자 목록을 추출하거나, 에러가 발생한 세션만 선별할 수 있습니다. SQL의 WHERE EXISTS와 유사하며, inner join과 달리 오른쪽 테이블의 컬럼은 결과에 포함되지 않고, 왼쪽 테이블의 각 행은 최대 한 번만 나타납니다.
semi join의 핵심은 필터링에 특화되어 있다는 것입니다. 데이터를 결합하는 것이 아니라, 조건을 만족하는 행을 선택하는 도구입니다.
코드 예제
import polars as pl
# 전체 고객 목록
all_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"]
})
# 구매 이력 (고객 2와 4만 구매함)
purchases = pl.DataFrame({
"customer_id": [2, 4, 2], # 고객 2는 2번 구매
"amount": [50000, 75000, 30000]
})
# semi join으로 구매 이력이 있는 고객만 필터링
active_customers = all_customers.join(purchases, on="customer_id", how="semi")
print(active_customers)
# 결과: 고객 2와 4만 포함, 각각 한 번씩만 나타남
설명
이것이 하는 일: 이 코드는 전체 고객 목록에서 실제로 구매한 적이 있는 고객만 추출하여, 활성 고객 목록을 만듭니다. 첫 번째로, 두 데이터프레임을 생성합니다.
all_customers는 5명의 전체 고객을, purchases는 일부 고객의 구매 내역을 담고 있습니다. 고객 2는 두 번 구매했으므로 purchases 테이블에 두 번 나타납니다.
그 다음으로, join() 메서드가 how="semi" 파라미터와 함께 실행됩니다. 이는 purchases 테이블에 customer_id가 존재하는 all_customers의 행만 선택합니다.
중요한 점은 purchases의 어떤 컬럼도 결과에 포함되지 않으며, 고객 2가 두 번 구매했어도 결과에는 한 번만 나타난다는 것입니다. 마지막으로, active_customers라는 필터링된 데이터프레임이 생성됩니다.
고객 2(이영희)와 고객 4(정수진)만 포함되며, 각각의 name과 email 정보가 그대로 유지됩니다. 구매하지 않은 고객 1, 3, 5는 제외됩니다.
여러분이 이 코드를 사용하면 복잡한 필터링 로직을 간결하게 표현할 수 있으며, 대용량 데이터에서도 효율적으로 동작합니다. inner join 후 unique()를 하는 것보다 성능이 좋고 의도가 명확합니다.
실전 팁
💡 inner join과의 차이를 이해하세요: inner join은 매칭되는 모든 조합을 만들지만, semi join은 존재 여부만 확인하고 중복 없이 반환합니다.
💡 오른쪽 테이블의 컬럼은 필요 없을 때 사용하세요. 단순히 필터링 조건으로만 사용하고 실제 데이터는 왼쪽 테이블만 필요한 경우 적합합니다.
💡 성능이 중요한 필터링에 활용하세요. 큰 테이블에서 일부 ID만 선택할 때, in 연산자보다 semi join이 더 빠를 수 있습니다.
💡 반대 개념인 anti join도 있습니다: how="anti"를 사용하면 오른쪽 테이블에 없는 행만 선택할 수 있습니다(예: 구매하지 않은 고객).
💡 lazy mode에서 최적화가 잘 됩니다. df.lazy().join(..., how="semi").collect()로 실행하면 쿼리 최적화 효과를 얻을 수 있습니다.
8. anti join - 존재하지 않는 행 찾기
시작하며
여러분이 전체 고객 목록에서 한 번도 구매하지 않은 고객을 찾아야 하는 상황을 겪어본 적 있나요? 예를 들어, 가입만 하고 실제 활동이 없는 휴면 고객을 찾아서 재활성화 캠페인을 진행하고 싶을 때 말이죠.
이런 문제는 이탈 분석이나 누락 데이터 파악에서 매우 흔합니다. 존재하지 않는 것을 찾는 것은 직관적이지 않아서, 직접 구현하면 복잡한 서브쿼리나 조건문이 필요합니다.
바로 이럴 때 필요한 것이 anti join입니다. anti join은 오른쪽 테이블에 매칭되지 않는 왼쪽 테이블의 행만 반환하여, "없는 것"을 효율적으로 찾아줍니다.
개요
간단히 말해서, anti join은 오른쪽 테이블에 존재하지 않는 키를 가진 왼쪽 테이블의 행만 선택하는 작업으로, semi join의 정반대입니다. 데이터 분석에서 빠진 값, 미완료 작업, 비활성 사용자 등을 찾을 때, anti join을 사용하면 간단하게 "없는 것"을 식별할 수 있습니다.
예를 들어, 초대는 받았지만 가입하지 않은 사람, 주문했지만 결제하지 않은 건 등을 추출할 수 있습니다. SQL의 WHERE NOT EXISTS 또는 LEFT JOIN ...
WHERE right_key IS NULL과 유사하지만, anti join은 의도를 더 명확히 표현하고 성능도 우수합니다. anti join의 핵심은 부정 필터링입니다.
특정 조건을 만족하지 않는 데이터를 찾는 데 특화되어 있습니다.
코드 예제
import polars as pl
# 전체 초대 목록
invitations = pl.DataFrame({
"email": ["kim@example.com", "lee@example.com", "park@example.com", "jung@example.com", "choi@example.com"],
"invited_date": ["2025-01-01", "2025-01-02", "2025-01-03", "2025-01-04", "2025-01-05"]
})
# 실제 가입한 사용자
signups = pl.DataFrame({
"email": ["lee@example.com", "jung@example.com"],
"signup_date": ["2025-01-03", "2025-01-05"]
})
# anti join으로 초대받았지만 가입하지 않은 사람 찾기
pending_invitations = invitations.join(signups, on="email", how="anti")
print(pending_invitations)
# 결과: kim, park, choi만 포함 (초대받았지만 가입 안 함)
설명
이것이 하는 일: 이 코드는 초대를 보낸 사람 중에서 실제로 가입하지 않은 사람을 찾아서, 재초대나 팔로업이 필요한 목록을 만듭니다. 첫 번째로, 두 데이터프레임을 생성합니다.
invitations는 5명에게 보낸 초대 목록을, signups는 그중 실제로 가입한 2명의 정보를 담고 있습니다. 목표는 나머지 3명을 찾는 것입니다.
그 다음으로, join() 메서드가 how="anti" 파라미터와 함께 실행됩니다. 이는 signups 테이블에 email이 존재하지 않는 invitations의 행만 선택합니다.
lee@example.com과 jung@example.com은 가입했으므로 제외되고, 나머지만 남습니다. 마지막으로, pending_invitations라는 필터링된 데이터프레임이 생성됩니다.
kim, park, choi의 초대 정보만 포함되어, 이들에게 리마인더를 보내거나 추가 조치를 취할 수 있습니다. 초대 날짜 정보도 함께 유지되어 얼마나 오래 방치되었는지 파악할 수 있습니다.
여러분이 이 코드를 사용하면 누락된 데이터나 미완료 프로세스를 쉽게 추적할 수 있으며, 고객 이탈 방지나 데이터 품질 관리에 유용합니다. 복잡한 서브쿼리 없이 의도를 명확히 표현할 수 있습니다.
실전 팁
💡 semi join과 쌍으로 이해하세요: semi는 있는 것, anti는 없는 것을 찾습니다. 상황에 맞게 선택하세요.
💡 데이터 품질 검사에 활용하세요. 예를 들어, 주문 테이블에는 있지만 결제 테이블에 없는 건을 찾아 누락된 결제를 파악할 수 있습니다.
💡 left join 후 filter(col("right_key").is_null())보다 anti join이 더 명확하고 빠릅니다. 의도를 표현하는 데도 우수합니다.
💡 여러 조건으로 필터링할 때는 on 파라미터에 여러 컬럼을 지정하세요: on=["col1", "col2"]로 복합 조건 확인 가능합니다. �� 결과가 비어있으면 모두 매칭되었다는 의미입니다. 데이터 완전성 검증에 사용할 수 있으며, 빈 결과는 성공을 나타낼 수 있습니다.