여러 CSV나 API 응답을 pd.concat으로 세로 결합한 뒤 중복이 보인다면, 먼저 전체 행 중복과 업무 식별자 중복, 인덱스 중복을 나눠 확인해야 합니다. subset과 keep은 업무 키를 기준으로 남길 행을 정하는 도구이고, 중복 인덱스를 오류로 취급할지는 verify_integrity로 결합 단계에서 결정할 수 있습니다. 무조건 삭제하기보다 어떤 중복을 허용할지 정한 뒤 건수와 결과를 검증하는 순서가 안전합니다.
concat과 merge를 먼저 구분하기
배치별 데이터를 같은 열 구조로 아래로 쌓는 일이라면 concat이 맞습니다. 반대로 고객 정보와 주문 정보처럼 공통 키를 기준으로 열을 연결하는 SQL식 결합이라면 merge의 문제입니다. 두 작업을 혼동하면 중복을 삭제해야 할 문제와 조인 조건을 고쳐야 할 문제를 잘못 다루게 됩니다.
반복문 안에서 한 행씩 concat하기보다 입력 DataFrame을 먼저 리스트에 모아 한 번에 결합하는 편이 구조를 확인하기 쉽습니다.
import pandas as pd
frames = [pd.read_csv(path) for path in csv_paths]
combined = pd.concat(frames, ignore_index=True)
ignore_index=True는 기존 인덱스를 결과의 새 라벨로 쓰지 않겠다는 선택입니다. 이것만으로 주문번호나 상품번호 같은 업무 키의 중복이 해결되는 것은 아닙니다.
세 가지 중복을 따로 확인하기
1. 모든 열이 같은 전체 행 중복
행의 모든 열 값이 같은지 보려면 subset을 생략합니다. 인덱스는 이 판단에 포함되지 않으므로, 인덱스만 다르고 나머지 값이 같은 행도 전체 열 기준으로는 중복일 수 있습니다.
duplicate_rows = combined[combined.duplicated(keep=False)]
이 결과가 비어 있지 않다고 바로 삭제하지는 마십시오. 같은 원본이 재수집된 것인지, 같은 업무 이벤트가 실제로 두 번 발생한 것인지 확인해야 합니다.
2. 업무 식별자만 같은 키 중복
주문번호와 상품번호처럼 한 행을 식별하는 컬럼이 있다면 업무 규칙을 subset으로 명시합니다.
key_columns = ["order_id", "product_id", "order_date"]
duplicate_keys = combined[
combined.duplicated(subset=key_columns, keep=False)
].sort_values(key_columns)
업무 키가 같아도 금액이나 상태가 다를 수 있습니다. 이 경우는 단순한 복제 행이 아니라 같은 키에 여러 버전이 들어온 식별자 충돌일 수 있으므로, 최신 자료를 남길지 원본 우선으로 둘지 먼저 정해야 합니다.
3. 결합 축의 인덱스 중복
원본 DataFrame의 인덱스를 보존한 채 결합해야 한다면 인덱스 중복은 별도 검증 대상입니다. 결합 시 verify_integrity=True를 사용하면 새로 만들어지는 결합 축의 중복을 검사하고, 중복이면 ValueError가 발생합니다.
combined = pd.concat(frames, verify_integrity=True)
인덱스가 업무 식별자가 아니라 단순한 파일 내부 행 번호라면 ignore_index=True로 새 인덱스를 부여하는 편이 자연스럽습니다. 반대로 인덱스 자체가 유일해야 하는 계약이라면 오류를 숨기지 말고 결합을 중단하는 쪽이 맞습니다.
drop_duplicates의 기준과 보존 행 선택
drop_duplicates()는 subset을 생략하면 모든 열을, 지정하면 해당 열만 중복 판단에 사용합니다. keep='first'는 첫 행을, keep='last'는 마지막 행을 남기며, keep=False는 중복 그룹의 모든 행을 제거합니다.
예를 들어 같은 업무 키에서 최신 수집 시각의 행을 남기려면, 최신 행이 마지막에 오도록 정렬한 뒤 keep='last'를 선택할 수 있습니다.
deduplicated = (
combined
.sort_values("collected_at")
.drop_duplicates(subset=key_columns, keep="last", ignore_index=True)
)
여기서 collected_at이 실제 최신성을 대표하는지, 결측값을 어떻게 볼지는 데이터 계약으로 정해야 합니다. 단순히 파일에 나중에 들어온 행이 더 정확하다고 가정하면 안 됩니다. 어느 행을 남길 근거가 없다면 keep=False로 충돌을 보류하고 사람이 확인하는 편이 낫습니다.
제거 전후에 확인할 것
운영 배치에서는 삭제 호출 자체보다 삭제 전후의 검증 기록이 중요합니다. 다음 순서로 확인하면 집계가 조용히 부풀거나 줄어드는 문제를 발견하기 쉽습니다.
- 결합 전 입력 목록과 각 입력의 행 수를 기록합니다.
- 전체 행 중복과 업무 키 중복을 각각 계산합니다.
- 보존 기준에 따라 정렬한 뒤
drop_duplicates를 적용합니다. - 결과 행 수와 업무 키의 유일성을 다시 확인합니다.
- 예상하지 못한 충돌이 남으면 저장하지 않고 원본과 함께 조사합니다.
before = len(combined)
after = len(deduplicated)
remaining_key_duplicates = deduplicated.duplicated(
subset=key_columns, keep=False
).sum()
if remaining_key_duplicates:
raise ValueError("업무 키 중복이 남아 있어 저장하지 않습니다.")
이 검사는 중복 제거가 올바른 행을 남겼다는 뜻까지 보장하지는 않습니다. 어떤 키를 유일하게 볼지와 최신성 기준은 데이터 업무 규칙으로 정의해야 합니다.
결론: 삭제보다 중복의 의미를 먼저 정하기
pd.concat 뒤에는 duplicated()로 현상을 확인하고, 전체 행 중복인지 subset 기준 업무 키 중복인지 나눠 보십시오. 인덱스 유일성이 중요하면 verify_integrity=True로 결합 단계에서 오류를 드러내고, 단순 행 번호라면 ignore_index=True를 고려합니다. 마지막으로 보존 규칙과 전후 건수 검증을 코드에 남겨야 다음 배치에서도 같은 판단을 반복할 수 있습니다.
자세한 매개변수 동작은 pandas.concat 공식 문서, DataFrame.drop_duplicates 공식 문서, pandas 결합 가이드에서 확인할 수 있습니다.