同じ領収書で二度請求された件を見つける
目標
受け付けられた請求から、完全重複と事実上の重複を分け、同じ日に同じ病院で行われた別の診療という誤検知を除いた判定を、根拠とともに出す検知器dedup.pyを作ります。
なぜ重要なのか
同じ診療1件が、アプリ・ウェブ・窓口から何度も入ってくることは、例外ではなく日常です。請求番号まで同じ再送はまとめれば終わりですが、請求番号が違って表記だけが揺れた事実上の重複は、正規化してはじめて現れます。見逃すと、同じお金が2回出ていきます。 逆方向の事故のほうが静かです。同じ日に同じ病院で本当に2回診療を受けた人の請求を重複として止めると、顧客は理由もわからないまま、支払いを受けられません。検知器を作る仕事の半分は、この誤検知を減らす仕事であり、そのため判定には必ず根拠が付きます。 採点ツールは、判定を信用しません。一時的な請求表を用意してdedup.pyを実行し、正規化結果・ブロック・スコア・判定を、採点ツールが計算した値と突き合わせます。患者と病院、領収書番号、金額は、実行のたびに変わります。
ステップ
/root/dupclaim/gen_claims.pyを作成して実行し、/root/dupclaim/claims.dbに1日分の受付分を作ってください。- 請求番号が同じ再送を、
/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_no、patient、hospital、receipt_no、service_date(YYYY-MM-DD)、amount(整数)、channel、received_at(RFC 3339のローカル時刻)の8つです。 - 実行契約:
python3 /root/dupclaim/dedup.py --db <claims.db> --out <결과.json>(プレースホルダーは結果ファイル名です) - 再送は請求番号ごとに1行にまとめます。受付時刻が最も早い行を残します。
- 正規化:
patient_nとhospital_nは、NFKC正規化のあと、すべての空白を除去します。receipt_nは、NFKCのあと英数字だけを残して大文字にします。ブロックキーは、hospital_nとservice_dateを縦棒でつないだものです。 blocksには、構成要素が2つ以上のブロックだけを入れ、値は、請求番号を昇順に並べた配列です。- ペアはブロック内だけで作り、請求番号の昇順で
aがbより前です。 - スコア:
name_simとreceipt_simは、difflib.SequenceMatcher(None, x, y).ratio()を小数第4位で丸めた値です。scoreは、0.45 * receipt_sim + 0.35 * name_sim + 0.20 * (병원이 같으면 1.0, 아니면 0.0)(韓国語の文は「病院が同じなら1.0、そうでなければ0.0」という意味です)で、これも小数第4位で丸めます。 score_verdict: 0.92以上ならduplicate、0.80以上ならreview、それ未満ならdistinctです。- 許容誤差:
amount_gapは金額差の絶対値、day_gapは診療日の差の日数です。in_toleranceは、amount_gapが100以下でday_gapが0のときだけ真です。 serial_neighbor: 2つの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 - よくある間違いは、請求番号だけでまとめて事実上の重複を見逃すこと、ブロックキーに正規化前の値を使うこと、スコアだけを出して根拠を残さないことです。
1日分の受付データを作る
/root/dupclaim/gen_claims.pyを作成して実行し、/root/dupclaim/claims.dbを作ってください。claim表は261行で、異なる請求番号は250個です。請求番号が同じ再送10件(そのうち1件は3回入ってきました)、正規化すると患者・病院・領収書が完全に同じになるが原文の表記は違うペア12組、同じブロック内で領収書が連番のペア6組を仕込み、channelはapp・web・counterの3種類を使います。
表記の揺れは、空白を入れる、ハイフンを全角にする、数字を全角にする、小文字にする、の4つで十分です。連番ペアは、同じ患者・病院・診療日で領収書番号の末尾だけを1上げれば作れます。病院と診療日の種類を少なく絞らなければ、ブロックに2件以上が集まりません。
請求番号が同じ再送からまとめる
/root/dupclaim/exact_dup.csvに、同じ請求番号が2回以上入ってきた案件を、ヘッダーclaim_no,copies,first_received,last_receivedで抽出してください。請求番号ごとに1行です。
ステップ1で作った/root/dupclaim/claims.dbを読みます。GROUP BYとHAVING COUNT(*) > 1で終わります。first_receivedとlast_receivedは、その請求番号で入ってきた受付時刻の最小値と最大値で、再送の回数が常に2とは限りません。これは事実上の重複ではなく再送なので、重複件数には数えません。
表記の揺れを正規化でならす
/root/dupclaim/dedup.pyが、再送を請求番号ごとに1行にまとめ、各請求のpatient_n、hospital_n、receipt_n、blockを計算して、結果JSONのnormalizedに入れるようにしてください。
unicodedata.normalize("NFKC", s)が、全角の英数字と全角ハイフンを通常の文字にならします。そのあと空白を消し、領収書は英数字だけを残して大文字にそろえます。まとめるときは、受付時刻が最も早い行を残します。
ブロックで比べるペアを減らす
/root/dupclaim/dedup.pyがblocksを出すようにしてください。キーはブロックキーで、値はそのブロックに属する請求番号を昇順に並べた配列であり、構成要素が2つ以上のブロックだけを入れます。
ブロックキーに正規化前の病院名を使うと、表記が揺れた相手が別のブロックに分かれて、そもそも候補になれません。25万件をすべて比べるとペアが300億個になるので、ブロッキングは性能ではなく実現可能性の問題です。
類似度と合算スコアを付ける
/root/dupclaim/dedup.pyが、ブロック内のすべてのペアにname_sim、receipt_sim、hospital_same、score、score_verdictを付けてpairsに入れるようにしてください。
difflib.SequenceMatcher(None, x, y).ratio()は、2つの列の全要素数Tと一致した要素数Mに対して、2.0*M/Tです。小数第4位で丸めてください。ペアは請求番号の昇順で、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です。
12桁の番号で1文字だけ違うと、類似度が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には、## 판정 요약 ## 완전 중복과 사실상 중복 ## 사람이 봐야 하는 건 ## 오탐으로 뺀 것 ## 정규화 규칙の5つの節を書き(見出しは順に、韓国語で「判定の要約」「完全重複と事実上の重複」「人が見るべき案件」「誤検知として除いたもの」「正規化ルール」を意味する語です)、要約には、duplicate・review・distinct・serial_neighborのペア数を表で書きます。
前のステップで作った/root/dupclaim/dedup.pyに、/root/dupclaim/claims.dbを渡せば済みます。レビューリストは、担当者がスプレッドシートで開くファイルです。スコアだけでは何を確認すべきかわからないので、根拠も一緒に入れます。誤検知の節には、実際に除いたペアの請求番号を書いておかなければ、次の人がルールを検証できません。