월말·분기말이면 점검 대상과 실제 점검 기록을 맞춰 봐야 합니다. 손으로 대조하면 빠뜨리기 쉽습니다. 이 안내서는 두 목록을 대조해 미점검 대상·재점검 임박 대상·불량 분류를 찾는 로컬 스크립트입니다. 우선순위를 자동으로 정하지 않고, 담당자가 확인할 후보를 찾아 줍니다.
- 점검 대상 목록과 점검 기록을 식별자로 연결할 수 있습니다.
- anti-join과 양방향 대조가 찾는 차이를 설명할 수 있습니다.
- 날짜 변환 실패와 업무상 제외 대상을 누락 후보와 구분할 수 있습니다.
- 자동 대조 결과를 사람 확인용 후보로 정리할 수 있습니다.
왜 손으로 하면 조용히 틀리는가
가상의 상황을 들어 보겠습니다. 월말, 점검해야 할 대상은 수백 곳이고 기록은 여러 엑셀 파일에 흩어져 있습니다. 담당자는 두 목록을 나란히 놓고 눈으로 훑으며 빠진 곳을 찾습니다. 문제는 이 방식이 틀려도 티가 안 난다는 데 있습니다.
출석을 부르는 장면을 떠올리면 쉽습니다. 반 전체 명단(점검 대상)에서 대답한 학생(점검 기록)을 하나씩 지워 나가면, 끝에 남는 이름이 결석자(미점검)입니다. 그런데 명단엔 소방-001로 적혀 있고 출석부엔 소방_001로 적혀 있다면 어떻게 될까요. 사람 눈에는 같은 대상이지만, 하이픈과 밑줄 한 글자 차이로 기계에는 '다른 대상'이 되어 조용히 어긋납니다. 앞자리 0이 빠진 1과 001도 마찬가지입니다. 이런 표기 흔들림을 미리 맞추는 방법은 지저분한 데이터 정제에 정리돼 있습니다.
함정이 하나 더 있습니다. '대상 수 − 점검 수 = 미점검 수'라는 단순 빼기입니다. 대상 300, 점검 280이니 미점검 20이라고 셈하면 깔끔해 보입니다. 하지만 점검 기록에 대상 목록에 없는 곳(관할이 바뀐 곳, 중복 기록)이 섞여 있으면, 이 빼기는 어긋난 채로 그럴듯한 숫자를 냅니다. 그래서 빼기가 아니라, 어느 대상이 기록에 있고 없는지를 하나하나 대조해야 합니다. 사람(교육 이수) 명단에도 똑같은 함정이 있어 교육 명단 대조에서 양방향으로 다룹니다.
입력 두 가지
facility_register.csv—facility_id, facility_type, district, public_label, statusinspection_log.csv—facility_id, inspected_at, items_checked, items_failed, violation_category, result, reinspection_due등
동작 원리
대조의 기준은 행 수가 아니라 식별자의 포함 관계입니다. 먼저 대상 목록에서 현재 비교할 상태만 고르고, 점검 기록의 facility_id를 집합으로 만듭니다. 대상 ID가 그 집합에 없으면 anti-join 결과에 남습니다. 반대 방향으로도 확인하면 기록에는 있지만 대상 목록에는 없는 ID를 따로 찾을 수 있습니다. 두 방향은 각각 다른 오류를 보여 줍니다.
날짜 조건은 식별자 대조 뒤에 별도로 적용합니다. reinspection_due를 날짜형으로 바꾸고, 기준일과의 차이가 정한 창 안에 있는 행만 후보로 남깁니다. 아래 최소 예제는 변환 실패와 빈 날짜를 모두 NaT로 만든 뒤 필터 결과에서 제외할 뿐, 별도 검토 목록을 만들지 않습니다. 실제 적용에서는 날짜 필터 전에 NaT 행을 따로 확인해 "기한이 없음"과 "읽을 수 없음"을 구분하는 검토 단계를 추가해야 합니다.
마지막 불량 분류 집계는 후보의 개수를 묶어 보여 줄 뿐 심각도를 정하지 않습니다. 결국 한 파이프라인 안에도 세 종류의 연산이 있습니다. 포함 여부는 누락 후보, 날짜 차이는 일정 후보, 그룹별 행 수는 현황 요약을 만듭니다. 각 결과의 질문이 다르므로 한 점수로 합쳐 자동 우선순위를 만들지 않습니다.
무엇을 찾는가
세 가지를 만듭니다. 판정 규칙도 단순합니다.
- 미점검 (
not_inspected.csv) —status가 active인 대상 중 점검 기록이 없는 것 - 재점검 확인 대상 (
reinspection_due.csv) —reinspection_due날짜가 기준일로부터 30일 이내인 기록 - 불량 분류 집계 (
violation_summary.csv) —items_failed가 0보다 큰 행을 분류별로 합산
세 결과는 서로 다른 질문에 답합니다. 미점검은 '빠진 곳은 어디인가', 재점검은 '곧 다시 봐야 할 곳은 어디인가', 불량 집계는 '어떤 항목이 자주 걸리는가'입니다. 한 표에 뭉뚱그리지 않고 셋으로 나누는 이유는, 각각 다음 행동이 다르기 때문입니다 — 미점검은 방문 계획으로, 재점검은 일정 관리로, 불량 집계는 점검·교육 표준을 손보는 일로 이어집니다.
선택지와 트레이드오프
대조 방법은 결과를 누가 읽고 어떤 오류를 추적해야 하는지에 따라 고릅니다.
| 선택지 | 맞는 상황 | 장점 | 주의할 점 |
|---|---|---|---|
isin 기반 anti-join | 대상에 있으나 기록에 없는 ID만 빠르게 찾을 때 | 의도가 짧고 직접적임 | 반대 방향 불일치는 별도 검사 필요 |
merge(..., indicator=True) | 양쪽 존재 여부를 한 표에서 설명해야 할 때 | 왼쪽만·오른쪽만·양쪽을 함께 구분 | 중복 키가 있으면 행이 불어나므로 키 검산 필요 |
| 날짜 차이 필터 | 기준일에서 일정 창 안의 후보를 찾을 때 | 기준일을 바꿔 재현 가능 | 형식 실패와 빈 날짜를 따로 관리해야 함 |
분류별 groupby | 불량 유형의 건수 분포를 볼 때 | 반복 유형을 한눈에 요약 | 건수가 심각도나 우선순위를 뜻하지 않음 |
대조는 코드로 — 핵심 로직
손 대조의 실수를 없애는 핵심은 세 가지 연산입니다. 표준 pandas만 씁니다.
import pandas as pd reg = pd.read_csv("facility_register.csv") log = pd.read_csv("inspection_log.csv") asof = pd.Timestamp("2025-08-01") # ① 미점검: active 대상 중 점검 기록에 없는 것 (단방향 anti-join) active = reg[reg["status"] == "active"] not_inspected = active[~active["facility_id"].isin(set(log["facility_id"]))] # ① 역방향 확인: 점검 기록 중 active 대상에 없는 것 (양방향 대조) active_ids = set(active["facility_id"]) record_only_ids = set(log["facility_id"]) - active_ids record_only_rows = log[log["facility_id"].isin(record_only_ids)] # 대상 목록에 없는 기록들 # ② 재점검 임박: reinspection_due 가 기준일로부터 0~30일 이내 log["reinspection_due"] = pd.to_datetime(log["reinspection_due"], errors="coerce") due_soon = log[(log["reinspection_due"] - asof).dt.days.between(0, 30)] # ③ 불량 분류 집계 violations = log[log["items_failed"] > 0].groupby("violation_category").size()
핵심은 ①의 단방향(~...isin(...) anti-join)과 역방향 확인입니다:
- 첫 번째 ①: active 대상에 있지만 점검 기록에 없는 ID → 미점검 후보
- 두 번째 ①: 점검 기록에 있지만 active 대상에 없는 ID → 관할 변경·폐업·중복 기록 확인 필요
양방향을 함께 보아야 대조가 완성됩니다. ②의 errors="coerce"는 깨진 날짜를 NaT로 만들고 현재 필터에서 제외하므로, 필터 전 NaT 검토도 별도 단계입니다.
실행과 결과
python script.py --as-of 2025-08-01
[입력] 점검 대상 10개, 점검 기록 8건, 기준일 2025-08-01 [검산] 미점검 2개, 재점검 확인 대상 3개, 불량 분류 4종 [주의] 재점검 대상은 담당자 원자료 확인 후 확정합니다. [OK] 저장: output/not_inspected.csv [OK] 저장: output/reinspection_due.csv [OK] 저장: output/violation_summary.csv
합성 데이터로 눈에 익히기
동봉된 합성 데이터로 세 결과를 확인합니다. 대상 10곳이 모두 active이고 8곳에만 기록이 있으면:
- 미점검:
F-002,F-007— active이지만 기록 없음 - 재점검 확인 대상:
reinspection_due가 기준일 ±30일 안의 기록들 (날짜 형식 오류는NaT로 제외) - 불량 분류 집계:
items_failed > 0인 기록을 분류별로 집계
대상이 모두 active라 '10 − 8 = 2'가 우연히 맞지만, 폐업·관할 변경이 섞이면 단순 빼기는 어긋납니다. 코드가 status == "active" 필터와 anti-join을 함께 쓰는 이유입니다.
가상 사례: 예방업무 담당자의 관할 변경 대조
다음은 설명을 위한 가상 사례입니다. 예방업무 담당자가 월말 누락 후보를 확인하는데, 대상 목록에는 최근 관할이 바뀐 시설이 남아 있고 점검 기록에는 새 식별자로 등록된 행이 있습니다. 입력은 최신이라고 표시된 대상표와 여러 시점의 점검 기록이며, 제약은 공개용 결과에 시설명·상세주소를 내보내지 않는 것입니다.
담당자는 누락 후보를 곧바로 미점검으로 확정하지 않습니다. 현재 최소 코드 밖의 record_only_ids 검사를 함께 실행해 "기록에만 있는 ID"를 찾고, 같은 대상을 가리킬 가능성이 있으면 승인된 매핑 자료와 원등록부를 사람이 확인합니다. 일치가 확인돼도 원본 ID를 덮어쓰지 않고 매핑 근거와 적용 시점을 남깁니다.
실패 가능성은 이름이 비슷하다는 이유만으로 자동 결합하는 것입니다. 서로 다른 시설을 하나로 합치면 실제 누락이 숨습니다. 최종 확인자는 식별자 변경 근거와 적용 날짜를 확인한 뒤 후보 상태만 갱신합니다.
가상 사례: 데이터 품질 담당자의 날짜 실패 행 검토
이 사례도 전부 가상입니다. 데이터 품질 담당자가 재점검 후보 파일을 받았는데 예상보다 행이 적습니다. 입력에는 빈 reinspection_due, 형식이 깨진 문자열, 정상 날짜가 섞여 있습니다. 목적은 재점검 대상을 정하는 것이 아니라 어떤 행이 날짜 비교에서 제외됐는지 설명하는 것입니다.
담당자는 먼저 이 최소 예제가 NaT 행을 별도 파일로 만들지 않고 필터에서 제외한다는 한계를 확인합니다. 실제 적용 절차를 설계할 때는 날짜 필터 전에 NaT 원본 행을 따로 검토하고, 날짜 없음과 형식 오류를 원자료 담당자가 구분하도록 별도 단계를 둡니다. 이 검토와 정비가 끝난 입력으로 기준일 창 계산을 다시 실행합니다.
실패 가능성은 최소 예제의 due_soon 결과만 보고 NaT 행까지 검토가 끝났다고 간주하는 것입니다. 사람은 필터 전 원본에서 제외 사유를 따로 확인하고, 형식 오류가 해결되기 전에는 후보 수를 완전한 현황처럼 공유하지 않습니다.
검산 — 먼저 기대값을 정해두기
요리를 손님상에 내기 전에 간을 보듯, 코드를 실제 자료에 쓰기 전에 답을 아는 데이터로 먼저 돌려 봅니다. 동봉된 합성 데이터로 먼저 돌려, 기대값이 그대로 나오는지 확인합니다. 하나라도 다르면 결과를 업무에 쓰지 않습니다.
- active 대상 10개, 점검 기록 보유 8개
- 미점검 후보 2개, 재점검 확인 후보 3개
- 불량 분류 4종 (소화설비·피난구조설비·소화활동설비·경보설비)
대상 수는 status=active 행 수와 맞아야 하고, 미점검은 점검 기록이 없는 active 대상만, 재점검은 날짜 형식이 올바른 기록만 집계됩니다.
이 대조가 답하지 못하는 것
이 스크립트는 '두 목록이 어긋나는 지점'을 찾아 줄 뿐, 왜 어긋났는지는 모릅니다. 미점검으로 뜬 곳이 정말 점검을 빠뜨린 것인지, 아니면 폐업·관할 변경·점검 면제 대상이라 애초에 기록이 없는 게 맞는 것인지는 사람이 원자료로 가려야 합니다.
- 미점검 ≠ 위반: 기록이 없는 이유는 여러 가지입니다. 대상 목록이 최신이 아닐 수도, 점검 주기가 아직 안 됐을 수도 있습니다.
- 재점검 임박 ≠ 확정: 30일이라는 창은 기준일에 따라 달라집니다.
--as-of날짜를 바꾸면 대상도 바뀌므로, 어느 날 기준인지를 결과에 함께 적어 둡니다. - 불량 집계 ≠ 심각도: 건수는 많고 적음만 보여 줄 뿐, 어느 불량이 더 위험한지는 판단하지 않습니다. 소화설비 한 건과 경보설비 한 건은 똑같은 '1'이지만 무게가 다릅니다.
요컨대 이 도구는 담당자의 눈을 넓혀 주는 것이지 판단을 대신하는 것이 아닙니다. 후보를 빠르고 정확하게 추리는 일까지가 코드의 몫이고, 최종 확정은 사람이 합니다.
결과를 공유하기 전
다음 중 하나라도 보이면 공유하지 않고 원자료 확인으로 되돌립니다.
- 실제 시설명·상세주소가 출력에 남음 → 출력 컬럼을 줄이고 합성 ID만
- 대상 목록과 점검 기록의 ID 체계가 다름 → 매핑표로 샘플 5행 대조
reinspection_due날짜 형식이 섞임 → 최소 예제에는 별도 저장이 없으므로 필터 전NaT검토 단계를 추가- 결과로 바로 조치 문안을 만들려 함 → 담당자 검토 질문 목록으로만 전환
- [ ] 실제 시설명, 상세주소, 취약점 설명을 공개 예시에 넣지 않았습니다. - [ ] 대상과 기록의 ID 표기(하이픈·밑줄·앞자리 0)를 통일한 뒤 대조했습니다. - [ ] 점검 우선순위나 위험도 최종 판단을 AI 또는 스크립트가 단정하지 않습니다. - [ ] reinspection_due.csv는 담당자 원자료 확인 후 확정합니다. - [ ] 미점검 대상은 점검 제외, 폐업, 관할 변경 같은 업무 규칙을 확인합니다. - [ ] 공개 문서에는 집계 수준의 결과만 사용합니다.
문제 해결
| 증상 | 가능한 원인 | 처방 | 확인 |
|---|---|---|---|
| 거의 모든 대상이 미점검으로 나옴 | 양쪽 ID 형식·자료형이 다름 | 공백·구분자·앞자리 0의 규칙을 승인된 방식으로 맞춤 | 양쪽 샘플 ID와 고유값 수를 전후 비교 |
| 대조 뒤 행 수가 갑자기 늘어남 | 한쪽 또는 양쪽 키가 중복된 상태로 병합됨 | 키별 행 수를 먼저 세고 중복의 업무 의미를 확인 | 병합 전후 행 수와 키별 최대 반복 수 확인 |
| 재점검 후보가 예상보다 적음 | NaT 행이 현재 날짜 필터에서 제외됨 | 필터 전 원본의 NaT를 별도 검토하고 빈값·형식 오류를 구분 | 검토한 원본 행과 정비 후 재실행 결과를 대조 |
| 누락 후보에 제외 대상이 반복됨 | 대상 목록의 상태·관할 정보가 오래됨 | 최신 원등록부와 적용 시점을 사람이 확인 | 후보별 제외 근거와 확인자를 기록 |
자주 묻는 질문
Q. anti-join 결과가 곧 미점검 확정 목록인가요?
아닙니다. 두 표에서 ID가 연결되지 않은 후보입니다. 관할 변경, 제외 상태, ID 변경, 기록 지연 같은 이유를 원자료에서 확인해야 합니다.
Q. 시설명을 비슷하게 맞춰 자동 결합해도 되나요?
이름 유사도만으로는 다른 대상을 잘못 합칠 수 있습니다. 승인된 식별자와 매핑 근거가 없으면 자동 확정하지 않고 검토 후보로 남깁니다.
Q. 기준일 창은 왜 결과와 함께 기록해야 하나요?
같은 날짜도 기준일이 바뀌면 창 안팎이 달라집니다. 결과를 다시 만들고 설명하려면 기준일과 사용한 조건이 함께 있어야 합니다.
Q. 불량 분류 건수가 많으면 먼저 점검해야 하나요?
건수만으로 심각도나 우선순위를 정할 수 없습니다. 이 표는 반복 분류를 보여 주는 요약이며, 실제 조치는 업무 기준과 담당자 검토가 결정합니다.
핵심 정리
- 현재 코드는 대상→기록 누락만 찾으므로, 기록→대상 차이는 별도 집합 차이로 확인합니다.
- 현재 최소 코드는
NaT를 필터에서 제외하므로, 실제 적용 전 필터 앞에 별도 검토 단계를 둡니다. - 누락·일정·불량 집계는 서로 다른 질문이므로 한 자동 점수로 합치지 않습니다.
- 결과는 처분이나 위험 판단이 아니라 담당자가 원자료를 확인할 작업 목록입니다.
다음 단계
- 지저분한 출동 데이터 깔끔하게 정제하기 — 대조 전에 ID·날짜·상태 표기를 일관되게 만드는 단계입니다.
- 교육 이수 명단, 두 목록이 맞는지 확인하기 — 같은 양방향 대조 원리를 사람 명단에서 연습할 수 있습니다.
- AI 결과, 제출 전 1분 점검 — 후보 목록을 공유하기 전 개인정보·단정 표현·검산 근거를 확인합니다.