Devin.KR

리스트와 집합으로 중복 찾기

75분 안팎

학습 목표

지역·날짜 키를 튜플과 집합으로 관리하고 중복 행 번호를 출력합니다.

개념

행 목록과 검색용 키를 나눕니다

한 지역의 여러 날짜를 분석하려면 여러 행을 담아야 합니다. 리스트는 입력 순서를 가진 목록이며 중복된 원소도 그대로 둡니다. 딕셔너리는 region과 date처럼 열 이름으로 값을 꺼내는 행에 적합합니다. 집합은 이미 본 키인지 확인하는 용도에 맞습니다. 출력 목록을 집합 하나로 대체하면 중복이 발생한 위치를 잃습니다. 입력 행, 검색 키, 오류 번호를 각각 다른 자료 구조에 둡니다.

이번 브라우저 입력은 region과 date를 가진 딕셔너리 목록입니다. 키 열은 비어 있지 않고 날짜는 이미 ISO 형식으로 정리되었다고 가정합니다. 날짜 문자열이 다른 표기를 가진 경우에는 중복 검사를 하기 전에 다음 레슨의 정규화를 적용해야 합니다. 가정을 생략하면 같은 날짜가 다른 키로 남거나 잘못된 날짜가 정상 키를 차지할 수 있습니다.

튜플로 복합키를 표현합니다

지역만 키로 쓰면 A의 9월 1일과 9월 2일을 같은 관측으로 잘못 취급합니다. 날짜만 키로 쓰면 같은 날의 A와 B가 충돌합니다. (row["region"],row["date"])처럼 두 문자열을 튜플로 묶으면 두 요소가 모두 같을 때 같은 키입니다. 튜플은 이 두 값을 한 묶음으로 전달하며 리스트처럼 원소를 바꾸지 않습니다. 여기서는 요소도 문자열이므로 집합에 넣을 수 있습니다.

문자열을 이어 붙이는 키는 경계를 잃을 수 있습니다. 지역과 날짜 사이의 구분자가 원문에도 등장하거나 구성 요소 길이가 달라지면 서로 다른 값이 같은 문자열이 됩니다. 튜플은 구성 요소 경계를 보존합니다. TypeError: unhashable type: list가 나오면 집합에 리스트나 딕셔너리를 넣으려 했는지 확인합니다. 튜플이라도 안에 리스트가 들어 있으면 같은 문제가 있으므로 문자열 키 두 개만 사용합니다.

이미 본 키를 순서대로 확인합니다

seen = set()으로 빈 집합을 만듭니다. {}는 빈 딕셔너리이므로 혼동하지 않습니다. 반복문은 enumerate(rows,1)로 원래 순서를 따릅니다. key in seen이면 현재 번호를 duplicates에 추가하고 처음 본 키이면 seen.add(key)를 호출합니다. 첫 등장 번호는 중복으로 반환하지 않습니다. 같은 키가 세 번 등장하면 두 번째와 세 번째 번호가 목록에 남습니다.

집합 자체의 표시 순서는 출력 계약으로 쓰지 않습니다. 이 실습에서 duplicates는 리스트이며 입력을 따라 append하므로 순서가 고정됩니다. 지역 이름의 가나다순으로 번호를 정렬하는 것은 다른 계약입니다. 키 포함 여부에 집합을 사용하는 것은 일반적인 입력에서 반복 검색 비용을 줄이는 데 도움이 되지만, 데이터 전체와 검색 집합을 메모리에 담으므로 파일 크기에 따른 한계도 있습니다.

반복 키와 완전 중복은 다릅니다

브라우저 과제는 값의 다른 열을 비교하지 않고 반복 키의 행 번호만 찾습니다. 미션에서는 같은 키라도 통행량 100과 80처럼 값이 다르면 충돌입니다. 처음 값을 남기고 뒤의 충돌 원문을 rejected.csv에 기록하되, 실제 출처 확인 전에는 최종 분석을 확정하지 않습니다. 임의의 마지막 행으로 덮어쓰면 수정 이력인지 잘못된 재전송인지 판단할 근거가 사라집니다.

완전 일치 중복은 모든 정규화 열을 비교해서 결정합니다. 공백과 날짜 표기만 달랐던 두 행이 정규화 후 같다면 한 관측으로 취급합니다. 키가 처음 등장한 행이 날짜나 숫자 오류라면 seen에 넣지 않습니다. 그렇지 않으면 뒤의 정상 행이 중복으로 제거됩니다. 검증→정규화→키 검사 순서가 중요하며 원본 번호는 정규화한 뒤에도 유지합니다.

딕셔너리 덮어쓰기를 점검합니다

seen[key] = row처럼 딕셔너리로 첫 행을 저장하면 나중에 현재 행과 값 전체를 비교할 수 있습니다. 대입 전에 key in seen을 검사합니다. 모든 행을 한 번에 딕셔너리로 만드는 표현은 같은 키의 앞 값을 뒤 값으로 덮어쓰므로 감사 기록을 만들기에 적절하지 않습니다. 건수만 줄어든 결과로는 어느 행이 왜 사라졌는지 설명하기 어렵습니다.

result = row는 복사가 아니라 같은 딕셔너리를 다른 이름으로 가리킵니다. result["date"]를 수정하면 원문 row도 바뀔 수 있습니다. 이번 미션처럼 문자열 값만 가진 평평한 행은 dict(row)로 얕은 복사한 뒤 정규화합니다. 중첩 목록까지 복사된다는 뜻은 아닙니다. 입력 바이트 보존과 메모리 원문 보존을 구분해 테스트하고, 결과 파일과 오류 파일을 대조할 수 있게 만듭니다.

이 레슨의 프로젝트 적용과 검토

앞 단계의 daily 한 행은 교통 지역 한 곳의 하루 관측입니다. 날씨 원본에는 관측소가 있어서 같은 날 같은 날씨 지역이 여러 번 등장할 수 있지만 joined.csv는 교통 지역·날짜 한 행으로 맞추어져 있습니다. 이 레슨에서 관측소를 키에 다시 넣으면 지역별 결과의 중복을 놓칠 수 있습니다. 리스트에 든 행의 의미와 키의 의미를 먼저 연결한 다음 집합을 선택합니다. 자료 구조를 배우는 목적은 이 계약을 코드로 표현하는 데 있습니다.

모듈 미션의 검색 딕셔너리는 정상화된 첫 행을 값으로 갖습니다. 키가 없으면 현재 행을 저장하고, 키가 있으면 모든 열을 비교합니다. 완전 중복은 별도 목록에 원문을 보존하여 왜 행 수가 줄었는지 보여 줍니다. 충돌은 오류 목록에 넣지만 이미 저장된 첫 정상 행까지 자동으로 지우지는 않습니다. 이 첫 정상 행 유지 정책은 수정 이력을 해석하는 정책이 아니므로 실제 충돌이 생기면 원본 담당자에게 확인할 근거를 남깁니다.

튜플과 집합을 익히면서 원본 목록을 수정하지 않는 습관도 연습합니다. 반복 중 rows.remove를 호출하면 남은 원소의 인덱스가 바뀌어 다음 행을 건너뛸 수 있습니다. 원본은 순서대로 읽고 clean과 rejected 같은 새 목록에 분류 결과를 쌓습니다. 반복문의 번호는 원래 rows의 위치이며, clean 목록의 길이가 늘어나거나 줄어들어도 오류 번호가 변하지 않습니다. 이 원칙은 파일 레코드 번호를 추적하는 미션에도 그대로 이어집니다.

출력 비교는 집합 내용의 표시와 오류 번호의 표시를 구분해서 읽습니다. print(seen)으로 확인한 키 순서는 채점 출력으로 사용하지 않습니다. 결과 번호 목록은 입력 순서로 append되며 json.dumps가 그 목록을 출력합니다. IndexError가 보이면 인덱스를 직접 세는 코드의 범위를 확인하고, KeyError가 보이면 row의 열 이름이 계약과 같은지 확인합니다. 브라우저 fixture의 필수 열은 존재한다고 가정하지만 로컬 파일에서는 헤더를 먼저 검사합니다.

검사가 빠졌을 때의 위험은 테스트 입력을 조금 바꾸어 살펴봅니다. A가 두 날짜에 나오고 B가 같은 날짜에 나오는 표본은 단일 열 키의 실수를 잡습니다. 같은 키 세 번은 최초 행까지 오류로 세는 실수를 잡습니다. 반복 키가 없는 목록과 빈 목록은 결과가 []인지 확인합니다. 데이터가 작다고 여러 값이 같은 행을 임의로 삭제하지 않으며, 관측 단위가 다른 파일에 이 함수를 쓰려면 키 정의와 테스트를 함께 바꿉니다.

따라하기

복합키의 의미

아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.

keys = [('A','2026-09-01'),('A','2026-09-02'),('B','2026-09-01')]
print(len(set(keys)))
print(len({key[0] for key in keys}))

실행 결과

3
2

입력 순서의 반복 번호

아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.

import json, sys
rows = json.load(sys.stdin)
seen = set()
duplicates = []
for number, row in enumerate(rows, 1):
    key = (row['region'], row['date'])
    if key in seen:
        duplicates.append(number)
    else:
        seen.add(key)
print(json.dumps(duplicates))

실행 결과

[3, 4]

원문 복사하기

아래 코드를 Python 3에서 실행합니다. 각 단계는 독립 예제입니다.

raw = {'region':'A','date':'2026/09/01'}
clean = dict(raw)
clean['date'] = '2026-09-01'
print(raw['date'])
print(clean['date'])

실행 결과

2026/09/01
2026-09-01

확인 문제

실습

표준 입력은 region,date 문자열을 가진 객체 목록입니다. 두 열은 비어 있지 않고 날짜는 이미 ISO 형식입니다. 지역·날짜 튜플이 앞에서 등장했으면 현재 행 번호를 반환합니다. 최초 행은 제외하며 세 번 나타나면 뒤 두 번호를 넣습니다. 결과는 입력 순서의 1부터 시작하는 번호 목록 JSON 한 줄입니다.

모범 답안
import json, sys
rows = json.load(sys.stdin)
seen = set()
duplicates = []
for number, row in enumerate(rows, 1):
    key = (row['region'], row['date'])
    if key in seen:
        duplicates.append(number)
    else:
        seen.add(key)
print(json.dumps(duplicates))

더 읽기

면접 질문

  • 조인 후 합계가 커진 상황을 어떻게 확인하는지 설명해 주시면 됩니다.