Files
homeagent-sdk/tools/gengskill/scripts/benford_test.py

257 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()