회계사의 직관을 수치화하다, Python 이상거래/부정 탐지
40단계까지 정제된 회계 데이터베이스를 바탕으로, 이제부터는 회계사에게 가장 중요한 "어떤 거래를 우선적으로 검토(Audit)해야 하는가?"에 대한 답을 구합니다.
수만 건의 거래 데이터 속에서 고액, 심야, 중복 패턴 등 Rule 기반 탐지는 물론 Z-score, 벤포드의 법칙, 머신러닝을 활용하여 '검토 우선순위(Risk Score)'를 자동으로 산출하는 강력한 감사(Audit) 툴을 구축합니다.
이상거래 ≠ 부정거래 (중요한 마인드셋)
프로그램이 "이것은 부정(Fraud)입니다!"라고 확정 짓는 것은 매우 위험합니다. 평균 거래가 100만 원인데 특정일 5,000만 원이 지출되었다면 이상거래(Anomaly)이긴 하지만, 장비 구입 등 정당한 사유가 있을 수 있습니다.
우리가 만드는 파이썬 시스템의 역할은 "부정거래 확정"이 아니라, 회계사가 수만 건의 데이터 중 "가장 먼저 눈으로 확인하고 증빙을 대조해야 할 고위험 거래(검토 대상) 리스트를 추려주는 것"입니다.
Rule 기반 조건 탐지 (고액, 심야, 주말)
가장 직관적이고 강력한 방법입니다. 사전에 정의된 '의심스러운 조건'을 Pandas 필터링으로 걸러냅니다.
# 1. 특정 금액 이상 + 계정과목 필터링 condition = (df["amount"] >= 5_000_000) & (df["account"] == "접대비") high_risk_rule = df[condition] # 2. 심야(23시~06시) 및 주말 거래 탐지 df["hour"] = pd.to_datetime(df["transaction_date"]).dt.hour df["weekday"] = pd.to_datetime(df["transaction_date"]).dt.weekday # 5:토, 6:일 night_weekend = df[(df["hour"] < 6) | (df["hour"] >= 23) | (df["weekday"] >= 5)]
중복 거래와 분할 거래 패턴 탐지
동일한 날짜, 거래처에 동일한 금액이 실수로 두 번 계상되었는지, 혹은 내부 규정을 피하기 위해 금액을 쪼개어 결제(분할 결제)했는지 찾습니다.
# 1. 완전 동일한 거래 찾기 (날짜, 거래처, 금액이 모두 같음) duplicates = df[df.duplicated(subset=["transaction_date", "customer", "amount"], keep=False)] # 2. 거래처별 특정 기간(7일) Rolling Sum (단기간 결제 집중 탐지) df = df.sort_values("transaction_date") df["rolling_sum_7d"] = df.groupby("customer")["amount"].rolling(7).sum().reset_index(level=0, drop=True)
통계적 이상치 (Z-score & IQR)
특정 거래처나 계정과목의 '평소 거래' 대비 과도하게 튀는 값을 찾습니다.
# 1. 계정과목 내 평소 대비 몇 배나 큰가? account_mean = df.groupby("account")["amount"].transform("mean") df["ratio_to_mean"] = df["amount"] / account_mean # 예: 10배 이상이면 의심 # 2. Z-score (표준편차 이용) mean, std = df["amount"].mean(), df["amount"].std() df["z_score"] = (df["amount"] - mean) / std outlier_z = df[df["z_score"].abs() > 3] # |Z| > 3 이면 통계적으로 드문 값 # 3. IQR (사분위수 범위 이용 - 비정규 분포인 회계데이터에 더 적합) Q1 = df["amount"].quantile(0.25) Q3 = df["amount"].quantile(0.75) IQR = Q3 - Q1 outlier_iqr = df[df["amount"] > (Q3 + 1.5 * IQR)]
감사의 클래식: 벤포드의 법칙 (Benford's Law)
자연적으로 발생한 숫자들의 첫 번째 자리는 1이 약 30%, 9가 약 5%로 균등하지 않게 나타난다는 통계적 성질입니다. 장부를 조작해 임의로 숫자를 지어내면 이 분포가 깨지게 됩니다.
# 금액에서 첫 번째 숫자(1~9)만 추출 df["first_digit"] = df["amount"].astype(str).str.replace(r"\D", "", regex=True).str[0] # 빈도 분포 확인 distribution = df["first_digit"].value_counts(normalize=True).sort_index() # -> 이 분포가 이론적 수치(1: 30.1%, 2: 17.6%...)와 현저히 다르면 인위적 조작 의심!
단, 급여나 정액 임차료처럼 인위적으로 고정된 금액 데이터에는 맞지 않으므로 추가 검토를 위한 힌트로만 사용합니다.
비지도학습(ML) 기반 이상탐지: Isolation Forest
규칙(Rule)으로 다 걸러낼 수 없는 다차원적인 복합 패턴(예: 금액은 크지 않으나, 특정 요일에 낯선 거래처와 거래 등)은 머신러닝 알고리즘에 맡깁니다.
from sklearn.ensemble import IsolationForest # Feature: 거래금액, 요일, 시간대 등 features = df[["amount", "weekday", "hour"]] # 전체 데이터의 1% 정도를 이상치로 간주하도록 모델 설정 model = IsolationForest(contamination=0.01, random_state=42) df["anomaly_ml"] = model.fit_predict(features) # -1 반환 시 이상치로 판별
종합 위험점수(Risk Score) 모델 구축
위의 모든 탐지 기법들을 하나의 '점수(Max 100점)'로 합산하여 HIGH / MEDIUM / LOW 등급을 부여합니다.
df["risk_score"] = 0 df["risk_reason"] = "" # 조건별 점수 가산 df.loc[df["amount"] >= 10_000_000, ["risk_score", "risk_reason"]] += [30, "고액거래; "] df.loc[df["ratio_to_mean"] >= 10, ["risk_score", "risk_reason"]] += [20, "평균대비 10배 초과; "] df.loc[df["anomaly_ml"] == -1, ["risk_score", "risk_reason"]] += [20, "ML이상치 탐지; "] df.loc[df["hour"] < 6, ["risk_score", "risk_reason"]] += [10, "심야거래; "] # 위험 등급 판정 def set_risk_level(score): if score >= 70: return "HIGH" elif score >= 40: return "MEDIUM" return "LOW" df["risk_level"] = df["risk_score"].apply(set_risk_level)
AI 종합 요약 및 검토대상 선정
최종적으로 `HIGH` 등급을 받은 거래 내역과 `risk_reason`(위험 사유) 데이터를 AI(LLM)에게 전달하여 회계사가 읽기 좋은 자연어 보고서를 작성하게 합니다.
[우선 검토 권고]
XYZ 거래처에서 15,000,000원의 거래가 발생했습니다. (위험점수: 80점)
해당 거래는 평소 평균 거래금액 대비 14.2배 높으며(20점), 동일한 날짜와 금액의 거래가 중복으로 확인(30점)되어 우선적인 원본 증빙(세금계산서) 대조가 필요합니다.
이렇게 만들어진 `HIGH / MEDIUM / LOW` 결과셋을 39단계에서 배운 xlsxwriter를 활용하여 다중 시트가 포함된 `audit_risk_report.xlsx` 파일로 자동 생성해 내면 이번 단계가 완성됩니다.
42단계 예고: 데이터 시각화와 재무 대시보드
이제 우리는 완벽하게 정제되고 필터링된 회계 데이터를 손에 쥐었습니다.
다음 42단계에서는 숫자만 가득한 엑셀과 터미널 창을 벗어나, Pandas와 시각화 라이브러리(Matplotlib, Plotly)를 활용하여 화려하고 직관적인 '회계/재무 시각화 대시보드'를 그려보겠습니다.