The Validator Is Green and the Numbers Still Look Wrong
한국어 원문으로 표시합니다.
목표
칼럼별 프로파일과 전달분 사이의 분포 변화, 그리고 첫 자리 숫자 분포로 형식은 맞는데 값이 이상한 자료를 찾아내는 도구 profile.py 를 만든다. 벤포드가 통하지 않는 칼럼을 반례로 넣어, 이것이 증거가 아니라 단서임을 코드가 스스로 말하게 한다.
왜 중요한가
스키마도 맞고 결측도 규칙대로인데 숫자가 이상한 일이 있다. 이때 "이상합니다" 만으로는 아무도 움직이지 않는다. 근거가 필요하고, 그 근거는 값 하나가 아니라 분포다. 프로파일 하나만으로는 판단이 어렵다. 이상은 절대적인 값이 아니라 지난 전달분과의 차이로 드러나기 때문이다. 그래서 프로파일은 전달분마다 남겨 두고 견주며, 문턱값을 코드에 적어 둔다 — 적어 두지 않은 문턱값은 다음 사람이 고칠 수 없다. 첫 자리 숫자 분포는 값싸고 잘 듣는 신호이지만 가장 오해받는 신호이기도 하다. 분포가 어긋났다는 것은 사람이 봐야 한다는 뜻이지 누가 조작했다는 뜻이 아니고, 값 범위가 좁은 칼럼에서는 정직한 자료도 늘 어긋난다. 그래서 적용 가능성을 먼저 판정해야 한다. 채점기는 여러분의 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 전달분을 차려 놓고 여러분의 도구를 실제로 실행해 프로파일과 판정을 채점기가 직접 계산한 값과 대조한다. 거래처와 금액은 실행마다 바뀐다.
단계
- /root/prof/gen_feed.py 를 만들어 실행해 /root/prof/feed 아래
2026-04.csv·2026-05.csv·2026-06.csv를 만드세요. 셋 다 형식 검증은 통과합니다. - /root/prof/profile.py 에
profile <파일>을 만들어 칼럼마다 타입·결측·고유 비율·값 길이·최빈값·범위를 내게 하세요. digits <파일> <칼럼>을 더해 값 길이 분포와 최빈 길이에서 벗어난 값을 내게 하세요.drift <파일A> <파일B>를 더해 전달분 사이의 분포 변화를 문턱값으로 판정하게 하세요.benford <파일> <칼럼>을 더해 첫 자리 숫자 분포와 기대 분포의 차이를 내게 하고, 두 전달분을 비교해 /root/prof/benford.json 에 적으세요.benford에 적용 가능성 판정을 더해, 값 범위가 좁은 칼럼에applicable을 false 로 하고 verdict 를not_applicable로 하세요. 결과는 /root/prof/narrow.json 에 적습니다.- 여러 신호를 겹쳐 조사 대상 목록을 만들고 칼럼마다 다음에 확인할 것을 /root/prof/leads.json 에 적으세요.
- 전체를 한 장으로 정리해 /root/prof/prof_report.json 과 /root/prof/prof_report.md 를 만드세요.
참고
- 실행 계약:
python3 /root/prof/profile.py <명령> <파일> [인자]. 명령은 profile·digits·drift·benford 넷입니다. 성공하면 종료 코드 0, 파일이 없으면 3, 명령이나 인자 수가 틀리면 2 입니다. - 없음으로 볼 표기는 빈 칸과
NULL·NA·N/A·-이고 대소문자는 구별하지 않습니다. profile응답:{"rows": 정수, "columns": {칼럼: {"type":…, "nulls":…, "null_rate":…, "distinct":…, "unique_ratio":…, "len_min":…, "len_max":…, "modal_length":…, "top": [[값, 건수], …3개]}}}. 숫자 칼럼에는min과max를 더합니다. 비율은 소수 넷째 자리에서 반올림합니다.top은 건수 내림차순이고 건수가 같으면 값의 사전 순입니다. 비율의 분모는 전체 행 수이고, 길이와 고유값은 없음을 뺀 값들로만 셉니다.digits응답:column·rows·lengths(길이를 문자열 키로 한 건수)·modal_length·outliers·sample_outliers(서로 다른 값을 정렬해 앞 3개).drift응답:{"columns": {칼럼: {…_before, …_after, "flags": [...]}}, "flagged": [칼럼...]}. 네 가지를 봅니다 — 결측 비율 차이 0.05 이상이면null_rate, 고유 비율 차이 0.20 이상이면unique_ratio, 최빈 길이가 다르면modal_length, 최빈값 점유율 차이 0.10 이상이면top_share. 이 문턱값들은 이 실습의 가정입니다.benford응답:column·n·observed·expected·max_deviation·deviation_digit·verdict. 6단계부터span과applicable이 더해집니다. 기대 비율은 자릿수 d 에 대해log10(1 + 1/d)이고 소수 넷째 자리에서 반올림합니다.- 판정 규칙도 이 실습의 가정입니다 — 값이 200건 이상이고 최대/최소 비율이 100 이상이어야 적용 가능하고, 적용 가능할 때 최대 차이가 0.05 이상이면
suspect, 아니면ordinary이며, 적용 불가면not_applicable입니다. - 첫 자리는 부호와 앞쪽 0 과 소수점을 건너뛰고 처음 만나는 0 이 아닌 숫자입니다. 값이 0 이면 세지 않습니다.
- 공식 문서: NIST 카이제곱 적합도 검정 · python statistics · python math
- 배경 설명(표준 문서가 아닙니다): 벤포드 법칙 — 이름과 역사를 훑는 용도이며, 판정 절차의 근거는 위의 NIST 문서입니다.
- 흔한 실수: 벤포드 결과를 증거처럼 적기, 값 범위가 좁은 칼럼에 그대로 적용하기, 문턱값을 코드에만 두고 문서에 안 적기, 신호 하나로 결론 내기.
세 달치 전달분 만들기
/root/prof/gen_feed.py 를 만들어 실행해 /root/prof/feed 아래 2026-04.csv·2026-05.csv·2026-06.csv 를 만드세요. 두 달은 평범하고 한 달은 형식은 맞는데 값이 이상합니다.
금액은 자릿수가 고르게 퍼지도록 만드세요 — 지수를 균등하게 뽑아 10의 거듭제곱으로 만들면 첫 자리 분포가 자연스러워집니다. 손댄 달에는 사람이 적어 넣은 듯한 금액 몇 가지를 섞고, 거래처가 비는 줄과 전화번호 자릿수가 다른 줄을 함께 넣으세요. 표준 라이브러리 난수 대신 고정 씨앗 생성기를 쓰면 누가 돌려도 같은 파일이 나옵니다.
칼럼마다 자기 소개를 시키기
/root/prof/profile.py 에 profile <파일> 을 만들어 rows 와 columns 를 내게 하세요. 칼럼마다 type·nulls·null_rate·distinct·unique_ratio·len_min·len_max·modal_length·top 을 담고, 숫자 칼럼에는 min·max 를 더합니다.
비율의 분모는 전체 행 수이고, 길이와 고유값은 없음을 뺀 값들로만 셉니다. top 은 건수 내림차순이되 건수가 같으면 값의 사전 순으로 정해야 실행마다 같은 답이 나옵니다.
값 길이에서 벗어난 것 꺼내 보기
digits <파일> <칼럼> 을 더해 lengths·modal_length·outliers·sample_outliers 를 내게 하세요. sample_outliers 는 서로 다른 값을 정렬해 앞 3개입니다.
형식 검사를 통과하는 값이라도 길이가 다르면 만든 쪽이 달라진 것입니다. 벗어난 값을 몇 개 직접 꺼내 보면 원인이 대개 바로 보입니다 — 전화번호라면 지역번호가 빠졌거나 하이픈이 사라진 것입니다.
지난 전달분과 견주기
drift <파일A> <파일B> 를 더해 칼럼마다 네 가지 변화를 재고 문턱값을 넘은 것을 flags 에 담게 하세요. 응답에는 flags 가 하나라도 있는 칼럼 목록 flagged 도 넣습니다.
이상은 절대값이 아니라 차이로 드러납니다. 결측 비율 0.05, 고유 비율 0.20, 최빈값 점유율 0.10, 그리고 최빈 길이가 달라졌는지를 봅니다. 문턱값을 상수로 빼 두면 나중에 자료가 달라졌을 때 고칠 자리가 한 곳입니다.
첫 자리 숫자 분포 재기
benford <파일> <칼럼> 을 더해 observed·expected·max_deviation·deviation_digit·verdict 를 내게 하고, 평범한 달과 손댄 달의 amount 결과를 /root/prof/benford.json 에 normal·suspect 로 적으세요.
기대 비율은 자릿수 d 에 대해 log10(1 + 1/d) 입니다. 첫 자리는 부호와 앞쪽 0 과 소수점을 건너뛰고 처음 만나는 0 이 아닌 숫자이고, 값이 0 이면 세지 않습니다. 사람이 손으로 지어낸 금액은 특정 자릿수에 몰리므로 차이가 크게 벌어집니다.
벤포드가 통하지 않는 칼럼
benford 에 span 과 applicable 을 더해, 건수가 200 미만이거나 최대/최소 비율이 100 미만이면 적용 불가로 보고 verdict 를 not_applicable 로 하세요. score 칼럼 결과를 /root/prof/narrow.json 에 column·n·span·applicable·verdict·why 로 적으세요.
값이 좁은 구간에만 있으면 첫 자리가 몇 가지로 제한되어 정직한 자료도 기대 분포와 크게 어긋납니다. 적용 가능성 판정 없이 돌린 벤포드는 멀쩡한 자료를 계속 신고하고, 그러면 아무도 그 신고를 읽지 않게 됩니다. why 에는 왜 적용할 수 없는지를 한두 문장으로 적으세요.
신호를 겹쳐 조사 대상 만들기
분포 변화와 벤포드 결과를 겹쳐 /root/prof/leads.json 에 leads·not_leads·note 를 적으세요. leads 의 각 항목은 column·signals·next_check 를 담고, not_leads 에는 벤포드를 적용할 수 없는 칼럼을 적습니다.
하나만 걸린 칼럼과 셋이 걸린 칼럼은 다르게 다뤄야 합니다. next_check 는 '이상하다' 가 아니라 '다음으로 무엇을 확인한다' 여야 합니다 — 손으로 적은 듯한 금액이 어느 담당자에게 몰려 있는지, 자릿수가 바뀐 전화번호가 어느 시스템에서 왔는지 같은 것입니다.
단서를 한 장으로 넘기기
/root/prof/prof_report.json 에 baseline·target·rows·flagged·benford·not_applicable·leads 를 적고, /root/prof/prof_report.md 에 ## 무엇을 봤나 ## 지난 전달분과 무엇이 달라졌나 ## 첫 자리 숫자가 말해 주는 것 ## 무엇을 더 확인해야 하나 네 절로 쓰세요.
benford 는 amount 칼럼의 column·max_deviation·verdict 를 담은 객체입니다. 보고서의 셋째 절에는 벤포드 결과와 함께 그것이 왜 증거가 아닌지를 반드시 적고, 넷째 절에는 칼럼마다 다음에 확인할 것을 적으세요. 앞 단계에서 남긴 파일들을 읽어 합치면 됩니다.