#!/usr/bin/env python3 """ 本福特定律检测 (Benford's Law Test) ==================================== 原理:跨越多个数量级的自然数据,首位数字遵循特定概率分布: P(d) = log10(1 + 1/d), d = 1,2,...,9 人为编造的数据往往偏离这一分布(倾向于均匀分布或集中在某些数字)。 致敬"耿同学讲故事" — 用数据说话,让造假无所遁形。 """ import argparse import sys import json import math import numpy as np from collections import Counter from scipy import stats # 本福特定律理论概率 BENFORD_PROBS = {d: math.log10(1 + 1/d) for d in range(1, 10)} def get_first_digit(value): """提取数值的首位有效数字(1-9)""" try: num = abs(float(value)) if num == 0: return None # 转为科学计数法取首位 s = f"{num:.10e}" first = int(s[0]) if 1 <= first <= 9: return first except (ValueError, TypeError): pass return None def get_first_two_digits(value): """提取前两位有效数字(10-99)""" try: num = abs(float(value)) if num == 0: return None while num < 10: num *= 10 while num >= 100: num /= 10 return int(num) except (ValueError, TypeError): pass return None def benford_test(values, order=1): """ 执行本福特定律检测 Parameters ---------- values : list 待检测的数值列表 order : int 1 = 首位数字检测, 2 = 前两位数字检测 Returns ------- dict : 检测结果 """ if order == 1: digits = [] for v in values: d = get_first_digit(v) if d is not None: digits.append(d) if len(digits) < 30: return { 'status': 'insufficient_data', 'message': f'数据量不足(仅{len(digits)}个有效值),本福特定律检测建议至少100个数据点', 'n_valid': len(digits) } # 统计观测频率 digit_counts = Counter(digits) observed = np.array([digit_counts.get(d, 0) for d in range(1, 10)]) expected = np.array([BENFORD_PROBS[d] * len(digits) for d in range(1, 10)]) # 卡方检验 chi2, p_value = stats.chisquare(observed, expected) # Kolmogorov-Smirnov 检验 observed_freq = observed / len(digits) expected_freq = np.array([BENFORD_PROBS[d] for d in range(1, 10)]) # 最大绝对偏差 (MAD) mad = np.mean(np.abs(observed_freq - expected_freq)) # MAD 阈值参考 (Nigrini 2012) # Close conformity: MAD < 0.006 # Acceptable conformity: 0.006 <= MAD < 0.012 # Marginally acceptable: 0.012 <= MAD < 0.015 # Nonconformity: MAD >= 0.015 if mad < 0.006: conformity = 'close' conformity_cn = '高度符合' elif mad < 0.012: conformity = 'acceptable' conformity_cn = '可接受' elif mad < 0.015: conformity = 'marginal' conformity_cn = '边缘' else: conformity = 'nonconforming' conformity_cn = '不符合' # 风险评分 if p_value < 0.001 and mad >= 0.015: risk_level = 'high' risk_score = 75 + min(25, mad * 500) elif p_value < 0.01: risk_level = 'medium-high' risk_score = 55 + min(20, mad * 400) elif p_value < 0.05: risk_level = 'medium' risk_score = 35 + min(20, mad * 300) else: risk_level = 'low' risk_score = max(0, mad * 200) distribution = { str(d): { 'observed': int(observed[d-1]), 'observed_freq': round(float(observed_freq[d-1]), 4), 'expected_freq': round(float(expected_freq[d-1]), 4), 'deviation': round(float(observed_freq[d-1] - expected_freq[d-1]), 4) } for d in range(1, 10) } result = { 'test_name': "Benford's Law Test (本福特定律检测)", 'status': 'completed', 'order': order, 'n_values': len(digits), 'distribution': distribution, 'chi_square': round(float(chi2), 4), 'p_value': float(p_value), 'degrees_of_freedom': 8, 'mean_absolute_deviation': round(float(mad), 6), 'conformity': conformity, 'conformity_cn': conformity_cn, 'risk_level': risk_level, 'risk_score': round(float(risk_score), 1), 'interpretation': _interpret_benford(p_value, mad, conformity_cn, len(digits)), 'note': '本福特定律适用于跨多个数量级的自然数据集。对于范围有限的数据(如百分比、pH值),该检测可能不适用。' } return result else: return {'status': 'error', 'message': '目前仅支持首位数字检测(order=1)'} def _interpret_benford(p_value, mad, conformity_cn, n): """生成可读的解释""" if p_value < 0.001 and mad >= 0.015: return ( f"⚠️ 数据首位数字分布严重偏离本福特定律(p < 0.001, MAD = {mad:.4f})。" f"符合性判定:{conformity_cn}。" f"这种偏离在{n}个数据点的样本中非常显著,强烈建议核查数据来源。" f"注意:需确认数据是否适用本福特定律(需跨越多个数量级)。" ) elif p_value < 0.01: return ( f"⚠️ 数据首位数字分布显著偏离本福特定律(p < 0.01, MAD = {mad:.4f})。" f"符合性判定:{conformity_cn}。建议进一步检查。" ) elif p_value < 0.05: return ( f"⚡ 数据首位数字分布存在一定偏离(p < 0.05, MAD = {mad:.4f})。" f"符合性判定:{conformity_cn}。可能是正常波动,建议结合其他检测综合判断。" ) else: return ( f"✅ 数据首位数字分布符合本福特定律(p = {p_value:.4f}, MAD = {mad:.4f})。" f"符合性判定:{conformity_cn}。未发现异常。" ) def load_data(input_file, column=None, delimiter=','): """从CSV文件加载数据""" import csv values = [] with open(input_file, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f, delimiter=delimiter) if column and column in reader.fieldnames: for row in reader: try: val = row[column].strip() if val: float(val) values.append(val) except (ValueError, KeyError): continue else: for row in reader: for key, val in row.items(): try: val = val.strip() if val: float(val) values.append(val) except (ValueError, AttributeError): continue return values def main(): parser = argparse.ArgumentParser( description="本福特定律检测 - 检测首位数字是否符合Benford's Law" ) parser.add_argument('--input', '-i', required=True, help='输入CSV文件路径') parser.add_argument('--column', '-c', help='要检测的列名') parser.add_argument('--order', type=int, default=1, choices=[1, 2], help='检测阶数:1=首位, 2=前两位') parser.add_argument('--delimiter', '-d', default=',', help='CSV分隔符') parser.add_argument('--output', '-o', help='输出JSON文件路径') args = parser.parse_args() values = load_data(args.input, args.column, args.delimiter) if not values: print("错误:未能加载有效数据", file=sys.stderr) sys.exit(1) result = benford_test(values, order=args.order) output_json = json.dumps(result, ensure_ascii=False, indent=2) if args.output: with open(args.output, 'w', encoding='utf-8') as f: f.write(output_json) print(f"结果已保存至: {args.output}") else: print(output_json) if __name__ == '__main__': main()