Finding the Receipt That Was Claimed Twice
한국어 원문으로 표시합니다.
목표
접수된 청구에서 완전 중복과 사실상 중복을 갈라내고, 같은 날 같은 병원의 다른 진료라는 오탐을 뺀 판정을 근거와 함께 내는 탐지기 dedup.py 를 만든다.
왜 중요한가
같은 진료 한 건이 앱·웹·창구로 여러 번 들어오는 일은 예외가 아니라 일상이다. 청구번호까지 같은 재전송은 묶으면 끝나지만, 청구번호가 다르고 표기만 흔들린 사실상 중복은 정규화해야 드러난다. 놓치면 같은 돈이 두 번 나간다. 반대 방향의 사고가 더 조용하다. 같은 날 같은 병원에서 정말로 두 번 진료받은 사람의 청구를 중복으로 막으면, 고객은 이유도 모른 채 지급을 못 받는다. 탐지기를 만드는 일의 절반은 이 오탐을 줄이는 일이고, 그래서 판정에는 반드시 근거가 붙는다. 채점기는 여러분의 판정을 믿지 않는다. 임시 청구 표를 차려 놓고 여러분의 dedup.py 를 실행해 정규화 결과·블록·점수·판정을 채점기가 계산한 값과 대조한다. 환자와 병원, 영수증 번호, 금액은 실행마다 바뀐다.
단계
/root/dupclaim/gen_claims.py를 만들어 실행해/root/dupclaim/claims.db에 하루치 접수분을 만든다.- 청구번호가 같은 재전송을
/root/dupclaim/exact_dup.csv로 뽑는다. /root/dupclaim/dedup.py가 환자·병원·영수증을 정규화하고 블록 키를 계산하게 한다.- dedup.py 가 블록을 만들어 견줄 쌍을 줄이게 한다.
- dedup.py 가 블록 안의 모든 쌍에 유사도와 합산 점수, 점수 등급을 매기게 한다.
- dedup.py 에 금액과 날짜의 허용 오차를 넣는다.
- dedup.py 가 연번 영수증을 오탐으로 빼고 최종 판정과 근거를 내게 한다.
- 자기 청구 표로 판정해
/root/dupclaim/dedup_result.json,/root/dupclaim/review.csv,/root/dupclaim/dup_report.md를 남긴다.
참고
- 표
claim의 열은claim_nopatienthospitalreceipt_noservice_date(YYYY-MM-DD)amount(정수)channelreceived_at(RFC 3339 지역시각) 여덟 개입니다. - 실행 계약:
python3 /root/dupclaim/dedup.py --db <claims.db> --out <결과.json> - 재전송은 청구번호마다 한 줄로 접습니다. 접수 시각이 가장 이른 줄을 남깁니다.
- 정규화:
patient_nhospital_n은 NFKC 정규화 뒤 모든 공백 제거.receipt_n은 NFKC 뒤 영숫자만 남기고 대문자화. 블록 키는hospital_n과service_date를 세로막대로 이은 것입니다. blocks에는 구성원이 둘 이상인 블록만 넣고, 값은 청구번호를 오름차순으로 정렬한 배열입니다.- 쌍은 블록 안에서만 만들고 청구번호 오름차순으로
a가b보다 앞입니다. - 점수:
name_sim과receipt_sim은difflib.SequenceMatcher(None, x, y).ratio()를 소수 넷째 자리에서 반올림.score=0.45 * receipt_sim + 0.35 * name_sim + 0.20 * (병원이 같으면 1.0, 아니면 0.0), 역시 넷째 자리 반올림. score_verdict: 0.92 이상 duplicate, 0.80 이상 review, 그 아래 distinct.- 허용 오차:
amount_gap은 금액 차의 절댓값,day_gap은 진료일 차의 일수.in_tolerance는amount_gap이 100 이하이고day_gap이 0 일 때만 참입니다. serial_neighbor: 두receipt_n이 서로 다르고 길이가 같으며, 마지막 숫자 덩어리를 뺀 앞부분이 같고 그 숫자 길이도 같으며, 숫자 차이가 1 이상 5 이하일 때 참입니다.- 최종
verdict:serial_neighbor면 distinct. 아니면score_verdict가 duplicate 일 때in_tolerance면 duplicate, 아니면 review. 그 밖에는score_verdict그대로입니다. reasons는 이 순서로 담습니다:receipt_exact(receipt_sim 이 1.0) 또는receipt_similar(0.8 이상),name_exact(name_sim 이 1.0),hospital_same,amount_gap(0 초과),day_gap(0 초과),serial_neighbor.- 직접 시험:
python3 /root/dupclaim/dedup.py --db /root/dupclaim/claims.db --out /tmp/r.json - 흔한 실수: 청구번호로만 묶어 사실상 중복을 놓치기, 블록 키에 정규화 전 값을 쓰기, 점수만 내고 근거를 안 남기기.
하루치 접수분 만들기
/root/dupclaim/gen_claims.py 를 만들어 실행해 /root/dupclaim/claims.db 를 만드세요. claim 표는 261행이고 서로 다른 청구번호는 250개입니다. 청구번호가 같은 재전송 10건(그중 한 건은 세 번 들어왔습니다), 정규화하면 환자·병원·영수증이 완전히 같아지지만 원문 표기는 다른 쌍 12쌍, 같은 블록 안에서 영수증이 연번인 쌍 6쌍을 심고, channel 은 app·web·counter 세 가지를 씁니다.
표기 흔들림은 공백 넣기, 하이픈을 전각으로 바꾸기, 숫자를 전각으로 쓰기, 소문자로 쓰기 네 가지면 넉넉합니다. 연번 쌍은 같은 환자·병원·진료일에 영수증 번호 끝자리만 1 올리면 됩니다. 병원과 진료일 가짓수를 좁게 잡아야 블록에 둘 이상이 모입니다.
청구번호가 같은 재전송부터 묶기
/root/dupclaim/exact_dup.csv 에 같은 청구번호가 두 번 이상 들어온 건을 머리글 claim_no,copies,first_received,last_received 로 뽑으세요. 청구번호마다 한 줄입니다.
1단계에서 만든 /root/dupclaim/claims.db 를 읽습니다. GROUP BY 와 HAVING COUNT(*) > 1 이면 끝납니다. first_received 와 last_received 는 그 청구번호로 들어온 접수 시각의 최솟값과 최댓값이고, 재전송 횟수가 늘 2 인 것은 아닙니다. 이건 사실상 중복이 아니라 재전송이라 중복 건수로 세지 않습니다.
표기 흔들림을 정규화로 접기
/root/dupclaim/dedup.py 가 재전송을 청구번호마다 한 줄로 접고, 각 청구의 patient_n hospital_n receipt_n block 을 계산해 결과 JSON 의 normalized 에 넣게 하세요.
unicodedata.normalize("NFKC", s) 가 전각 영숫자와 전각 하이픈을 보통 문자로 접습니다. 그다음 공백을 지우고, 영수증은 영숫자만 남겨 대문자로 맞춥니다. 접을 때는 접수 시각이 가장 이른 줄을 남깁니다.
블록으로 견줄 쌍 줄이기
/root/dupclaim/dedup.py 가 blocks 를 내게 하세요. 키는 블록 키이고 값은 그 블록에 속한 청구번호를 오름차순으로 정렬한 배열이며, 구성원이 둘 이상인 블록만 넣습니다.
블록 키에 정규화 전 병원명을 쓰면 표기가 흔들린 짝이 서로 다른 블록으로 갈라져 애초에 후보가 되지 못합니다. 25만 건을 전부 견주면 쌍이 300억 개라, 블로킹은 성능이 아니라 실현 가능성의 문제입니다.
유사도와 합산 점수 매기기
/root/dupclaim/dedup.py 가 블록 안의 모든 쌍에 name_sim receipt_sim hospital_same score score_verdict 를 매겨 pairs 에 넣게 하세요.
difflib.SequenceMatcher(None, x, y).ratio() 는 두 열의 전체 원소 수 T 와 일치 원소 수 M 에 대해 2.0*M/T 입니다. 소수 넷째 자리에서 반올림하세요. 쌍은 청구번호 오름차순으로 a 가 b 보다 앞입니다.
금액과 날짜의 허용 오차
/root/dupclaim/dedup.py 가 각 쌍에 amount_gap day_gap in_tolerance 를 붙이게 하세요. in_tolerance 는 금액 차가 100 이하이고 진료일 차가 0 일 때만 참입니다.
점수가 1.0 이어도 금액이 수천 원 벌어져 있으면 같은 영수증이 아닐 수 있습니다. 진료일은 datetime.date.fromisoformat 으로 읽어 빼면 일수가 나옵니다. 접수일이 아니라 진료일을 봅니다.
연번 영수증이라는 오탐 빼기
/root/dupclaim/dedup.py 가 각 쌍에 serial_neighbor 와 최종 verdict, 그리고 reasons 를 붙이게 하세요. 연번 영수증은 점수가 높아도 distinct 입니다.
열두 자리 번호에서 한 글자만 다르면 유사도가 0.93 을 넘습니다. 이름과 병원까지 같으니 합산 점수가 문턱을 넘지만, 이건 같은 서류가 아니라 이웃한 서류입니다. 마지막 숫자 덩어리만 떼어 비교하세요.
심사 담당자가 그대로 볼 수 있게 내기
자기 청구 표로 판정해 /root/dupclaim/dedup_result.json 을 남기고, 판정이 distinct 가 아닌 쌍을 /root/dupclaim/review.csv 에 머리글 a,b,verdict,score,amount_gap,day_gap,reasons 로 (reasons 는 세로막대로 이어서) 적으세요. /root/dupclaim/dup_report.md 에는 ## 판정 요약 ## 완전 중복과 사실상 중복 ## 사람이 봐야 하는 건 ## 오탐으로 뺀 것 ## 정규화 규칙 다섯 절을 쓰고, 요약에는 duplicate·review·distinct·serial_neighbor 의 쌍 수를 표로 적습니다.
앞 단계에서 만든 /root/dupclaim/dedup.py 에 /root/dupclaim/claims.db 를 물리면 됩니다. 검토 목록은 담당자가 스프레드시트로 여는 파일입니다. 점수만 있으면 무엇을 확인할지 모르니 근거를 같이 넣습니다. 오탐 절에는 실제로 뺀 쌍의 청구번호를 적어야 다음 사람이 규칙을 검증할 수 있습니다.