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

245 lines
7.9 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
"""
末位数字检测 (Last Digit Test)
==============================
原理自然实验数据的末位数字0-9应近似均匀分布。
如果数据是人为编造的,末位数字往往会集中在某些特定值上。
使用卡方检验评估偏离均匀分布的程度。
致敬"耿同学讲故事" — 用数据说话,让造假无所遁形。
"""
import argparse
import sys
import json
import numpy as np
from collections import Counter
from scipy import stats
def extract_last_digit(value):
"""提取数值的末位有效数字"""
s = str(value).strip()
# 去除负号
s = s.lstrip('-')
# 去除科学计数法
if 'e' in s.lower():
try:
value = float(s)
s = f"{value:.10f}".rstrip('0')
except ValueError:
return None
# 找到末位有效数字
s = s.rstrip('0').rstrip('.')
if not s:
return 0
for c in reversed(s):
if c.isdigit():
return int(c)
return None
def extract_last_digit_with_decimals(value, use_decimal_last=True):
"""
提取数值的末位数字
use_decimal_last=True: 取小数点后最后一位非零数字
use_decimal_last=False: 取整数部分的末位数字
"""
s = str(value).strip()
s = s.lstrip('-')
if '.' in s and use_decimal_last:
decimal_part = s.split('.')[1]
# 取小数部分的最后一位数字
if decimal_part:
return int(decimal_part[-1])
# 取整数部分的末位
integer_part = s.split('.')[0] if '.' in s else s
if integer_part:
return int(integer_part[-1])
return None
def last_digit_test(values, method='all_digits'):
"""
执行末位数字检测
Parameters
----------
values : list of float/str
待检测的数值列表
method : str
'all_digits' - 检测最后一位有效数字
'decimal_last' - 检测小数末位
Returns
-------
dict : 检测结果
"""
# 提取末位数字
last_digits = []
for v in values:
try:
if method == 'decimal_last':
d = extract_last_digit_with_decimals(v, use_decimal_last=True)
else:
d = extract_last_digit(v)
if d is not None:
last_digits.append(d)
except (ValueError, TypeError):
continue
if len(last_digits) < 10:
return {
'status': 'insufficient_data',
'message': f'数据量不足(仅{len(last_digits)}个有效值需要至少10个数据点',
'n_valid': len(last_digits)
}
# 统计各数字出现频次
digit_counts = Counter(last_digits)
observed = np.array([digit_counts.get(i, 0) for i in range(10)])
expected = np.full(10, len(last_digits) / 10.0)
# 卡方检验
chi2, p_value = stats.chisquare(observed, expected)
# 计算集中度指标
max_digit = int(np.argmax(observed))
max_freq = observed[max_digit] / len(last_digits)
# 均匀性评分 (0=完全均匀, 1=完全集中)
uniformity_deviation = np.sqrt(np.sum((observed / len(last_digits) - 0.1) ** 2) / 10) / 0.3
uniformity_deviation = min(uniformity_deviation, 1.0)
# 风险评分
if p_value < 0.001:
risk_level = 'high'
risk_score = min(80 + (1 - p_value) * 20, 100)
elif p_value < 0.01:
risk_level = 'medium-high'
risk_score = 60 + (0.01 - p_value) / 0.009 * 20
elif p_value < 0.05:
risk_level = 'medium'
risk_score = 40 + (0.05 - p_value) / 0.04 * 20
else:
risk_level = 'low'
risk_score = max(0, 40 * (1 - p_value))
result = {
'test_name': 'Last Digit Test (末位数字检测)',
'status': 'completed',
'n_values': len(last_digits),
'method': method,
'digit_distribution': {str(i): int(observed[i]) for i in range(10)},
'chi_square': round(float(chi2), 4),
'p_value': float(p_value),
'degrees_of_freedom': 9,
'most_frequent_digit': max_digit,
'most_frequent_proportion': round(float(max_freq), 4),
'uniformity_deviation': round(float(uniformity_deviation), 4),
'risk_level': risk_level,
'risk_score': round(float(risk_score), 1),
'interpretation': _interpret_result(p_value, max_digit, max_freq, len(last_digits))
}
return result
def _interpret_result(p_value, max_digit, max_freq, n):
"""生成可读的解释"""
if p_value < 0.001:
return (
f"⚠️ 末位数字分布严重偏离均匀分布p < 0.001)。"
f"数字 {max_digit} 出现频率为 {max_freq:.1%}(期望 10%"
f"这种偏离在自然实验数据中极为罕见,强烈建议进一步核查原始数据。"
)
elif p_value < 0.01:
return (
f"⚠️ 末位数字分布显著偏离均匀分布p < 0.01)。"
f"数字 {max_digit} 出现频率为 {max_freq:.1%},建议关注并进行人工复核。"
)
elif p_value < 0.05:
return (
f"⚡ 末位数字分布存在一定偏离p < 0.05)。"
f"可能是正常波动,也可能提示数据存在问题,建议结合其他检测结果综合判断。"
)
else:
return (
f"✅ 末位数字分布与均匀分布无显著差异p = {p_value:.4f}"
f"未发现明显异常。"
)
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='末位数字检测 - 检测数据末位数字是否偏离均匀分布'
)
parser.add_argument('--input', '-i', required=True, help='输入CSV文件路径')
parser.add_argument('--column', '-c', help='要检测的列名')
parser.add_argument('--method', '-m', default='all_digits',
choices=['all_digits', 'decimal_last'],
help='检测方法all_digits=末位有效数字, decimal_last=小数末位')
parser.add_argument('--delimiter', '-d', default=',', help='CSV分隔符')
parser.add_argument('--output', '-o', help='输出JSON文件路径')
parser.add_argument('--values', nargs='+', type=str,
help='直接传入数值列表(不使用文件输入)')
args = parser.parse_args()
if args.values:
values = args.values
else:
values = load_data(args.input, args.column, args.delimiter)
if not values:
print("错误:未能加载有效数据", file=sys.stderr)
sys.exit(1)
result = last_digit_test(values, method=args.method)
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()