본문 바로가기
언어별 개념 정리/Python

pandas merge 전에 확인할 중복 키·결측 키와 행 수

by char_lie 2026. 9. 12.
반응형

pandas merge의 중복 키와 결측 키에 따라 결과 행 수가 달라지는 원리를 정리한 표지

 

주문 세 건에 고객 정보를 붙였는데 결과가 다섯 행으로 늘었다면, 먼저 조인 키가 한쪽에서 중복됐는지 확인해야 한다. pandas의 merge는 키가 같은 행의 조합을 만들기 때문에 왼쪽 행 수를 자동으로 보존하지 않는다.

고객 한 명당 기준 정보가 한 행이어야 한다면 validate="many_to_one"으로 그 조건을 검사하고, indicator=True로 연결되지 않은 주문을 확인할 수 있다. 행 수, 키의 유일성, 미매칭은 서로 다른 검사다.

같은 키가 몇 번씩 나타나는지 확인하기

다음 예제는 Python 3.12와 pandas 2.2.3에서 실행했다. 코드 블록은 위에서 아래로 같은 Python 세션에서 실행한다.

import pandas as pd

orders = pd.DataFrame({
    "order_id": [101, 102, 103],
    "customer_id": [1, 1, 2],
    "amount": [10000, 20000, 30000],
})
customers_bad = pd.DataFrame({
    "customer_id": [1, 1, 3],
    "name": ["A", "A-duplicate", "C"],
})

expanded = orders.merge(customers_bad, on="customer_id", how="left")
print(len(orders), len(expanded))  # 3 5
print(customers_bad["customer_id"].duplicated().sum())  # 1

고객 1의 주문은 두 건이고 오른쪽 고객 정보도 두 행이다. 이 키에서만 2×2인 네 행이 생긴다. 고객 2의 주문 한 건은 연결할 정보가 없어도 왼쪽 조인이므로 남아 총 다섯 행이다.

중복은 조인 키를 기준으로 확인한다. 이름이 다르면 두 행 전체가 서로 달라도 customer_id의 중복 문제는 그대로다. 여러 열을 키로 쓸 때는 그 열들의 조합을 검사해야 한다.

validate로 데이터 관계를 명시하기

주문은 여러 건이어도 고객 기준표의 키는 유일해야 한다. 이 조건에 맞는 검사는 many_to_one이다.

try:
    orders.merge(
        customers_bad,
        on="customer_id",
        how="left",
        validate="many_to_one",
    )
except pd.errors.MergeError as error:
    print(type(error).__name__)  # MergeError

이 오류는 의도한 결과다. 행이 늘어난 결과를 받은 다음 정리하는 대신, 관계가 맞지 않는 병합을 해당 호출에서 중단한다.

validate 값 왼쪽 키 오른쪽 키
one_to_one 또는 1:1 유일해야 함 유일해야 함
one_to_many 또는 1:m 유일해야 함 중복 허용
many_to_one 또는 m:1 중복 허용 유일해야 함
many_to_many 또는 m:m 유일성 검사 없음 유일성 검사 없음

many_to_many는 중복 문제를 해결하는 옵션이 아니다. 필요한 관계를 제한하지 않는다는 뜻이다. 또 오른쪽 키가 유일하다고 해서 왼쪽의 모든 키가 오른쪽에 존재하는 것은 아니다.

중복 행을 무조건 drop_duplicates로 지우면 어느 고객 정보를 남겨야 하는지에 대한 판단이 빠진다. 갱신 시각이나 유효 기간 등 기준을 정하고 기준표를 정리해야 한다. 아래에서는 예제의 올바른 기준표를 별도로 만든다.

indicator로 연결되지 않은 행 찾기

customers = pd.DataFrame({
    "customer_id": [1, 3],
    "name": ["A", "C"],
})
result = orders.merge(
    customers,
    on="customer_id",
    how="left",
    validate="many_to_one",
    indicator=True,
)
counts = result["_merge"].value_counts().reindex(
    ["both", "left_only", "right_only"], fill_value=0
)
print(counts.to_dict())
# {'both': 2, 'left_only': 1, 'right_only': 0}
print(result.loc[result["_merge"] == "left_only", "order_id"].tolist())
# [103]
assert len(result) == len(orders)

both는 양쪽에서 키가 연결된 행, left_only는 왼쪽에만 남은 행이다. 위 코드는 고객 정보가 없는 주문 103을 찾아낸다. 왼쪽 조인은 오른쪽에만 있는 고객 3을 결과에 남기지 않으므로 right_only는 0이다. 양쪽 누락을 모두 조사하려면 외부 조인인 how="outer"가 필요하다.

이 예제의 행 수 단언은 왼쪽 조인과 오른쪽 키 유일성을 전제로 한다. 내부 조인에서는 미매칭 주문이 제거될 수 있으므로 같은 조건을 그대로 적용하면 안 된다.

결측 키끼리도 연결되는지 확인하기

pandas는 양쪽 키가 결측이면 그 행들끼리도 연결할 수 있다. 일반적인 SQL의 NULL 비교를 그대로 예상하면 결과를 잘못 읽기 쉽다.

left_null = pd.DataFrame({
    "customer_id": pd.Series([1, pd.NA], dtype="Int64")
})
right_null = pd.DataFrame({
    "customer_id": pd.Series([1, pd.NA], dtype="Int64"),
    "name": ["A", "unknown"],
})

matched_null = left_null.merge(
    right_null, on="customer_id", how="left",
    validate="many_to_one", indicator=True,
)
print(matched_null["_merge"].astype(str).tolist())
# ['both', 'both']

right_valid = right_null.dropna(subset=["customer_id"])
unmatched_null = left_null.merge(
    right_valid, on="customer_id", how="left",
    validate="many_to_one", indicator=True,
)
print(unmatched_null["_merge"].astype(str).tolist())
# ['both', 'left_only']

결측 고객 ID를 연결하지 않기로 했다면 오른쪽 기준표의 결측 키를 제외하는 방식으로 왼쪽 주문은 보존할 수 있다. 왼쪽 결측 주문도 버릴지는 별도의 업무 규칙이다. 결측을 같은 임의 값으로 채워 양쪽을 병합하면 오히려 서로 다른 미확인 고객을 한 그룹으로 연결할 수 있다.

실제 병합에서는 키의 자료형과 공백을 먼저 맞추고, 유일성 검사와 미매칭 검사를 각각 실행한다. 마지막에는 원래 보존하려던 주문 수와 금액 합계가 유지되는지도 확인한다. 결측치 처리와 집계부터 연습하려면 빅분기 실기 작업형 1유형 50제의 CSV와 노트북을 함께 사용할 수 있다.

반응형

댓글