feat: add example plugins (ai_image, calendar, music, rss, weather), fix .gitignore, move gengskill to tools/

This commit is contained in:
root
2026-07-21 12:45:17 +08:00
parent 75ae2b4692
commit 52dc22f86f
54 changed files with 13650 additions and 202 deletions

View File

@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""
GRIM 测试 (Granularity-Related Inconsistency of Means)
========================================================
原理对于整数取值的数据如李克特量表1-5、年龄等
给定样本量 n合法的平均值只能取有限集合中的值。
如果报告的平均值不在合法集合中,则数据存在不一致性。
n=25数据取值为整数则平均值只能是 k/25 的形式,
小数部分只能是 .00, .04, .08, .12, ..., .96
参考Brown & Heathers (2017). The GRIM Test. SPPS.
致敬"耿同学讲故事" — 用数据说话,让造假无所遁形。
"""
import argparse
import sys
import json
import math
from decimal import Decimal, ROUND_HALF_UP
def grim_test_single(mean, n, decimals=2, scale_min=None, scale_max=None):
"""
对单个均值执行 GRIM 测试
Parameters
----------
mean : float or str
报告的平均值
n : int
样本量
decimals : int
报告的小数位数
scale_min : int, optional
量表最小值(用于范围检查)
scale_max : int, optional
量表最大值(用于范围检查)
Returns
-------
dict : 检测结果
"""
try:
mean_val = Decimal(str(mean))
n = int(n)
except (ValueError, TypeError) as e:
return {'status': 'error', 'message': f'无效输入: {e}'}
if n <= 0:
return {'status': 'error', 'message': '样本量必须大于0'}
# 范围检查
if scale_min is not None and scale_max is not None:
if float(mean_val) < scale_min or float(mean_val) > scale_max:
return {
'status': 'range_error',
'message': f'均值 {mean} 超出量表范围 [{scale_min}, {scale_max}]',
'consistent': False
}
# 计算总和 = mean * n
total = mean_val * n
# 对于整数取值数据,总和必须是整数
# 考虑四舍五入误差:检查 total 是否足够接近某个整数
granularity = Decimal(1) / Decimal(10 ** decimals)
# 在四舍五入精度范围内检查
# mean 可能是真实值四舍五入到 decimals 位的结果
# 真实 mean 在 [mean - 0.5*granularity, mean + 0.5*granularity) 范围内
lower_total = (mean_val - granularity / 2) * n
upper_total = (mean_val + granularity / 2) * n
# 检查这个范围内是否包含整数
lower_int = math.ceil(float(lower_total))
upper_int = math.floor(float(upper_total))
consistent = lower_int <= upper_int
# 计算最近的合法均值
nearest_total = round(float(mean_val * n))
nearest_mean = nearest_total / n
# 格式化到指定小数位
fmt = f"%.{decimals}f"
nearest_mean_str = fmt % nearest_mean
reported_mean_str = fmt % float(mean_val)
result = {
'reported_mean': str(mean),
'sample_size': n,
'decimals': decimals,
'consistent': consistent,
'computed_sum': float(mean_val * n),
'nearest_valid_mean': nearest_mean_str,
'difference': round(abs(float(mean_val) - nearest_mean), decimals + 2)
}
if scale_min is not None and scale_max is not None:
result['scale_range'] = f"[{scale_min}, {scale_max}]"
return result
def grim_test_batch(items):
"""
批量 GRIM 测试
Parameters
----------
items : list of dict
每个字典包含 'mean', 'n', 可选 'decimals', 'label'
Returns
-------
dict : 批量检测结果
"""
results = []
n_inconsistent = 0
for i, item in enumerate(items):
mean = item.get('mean')
n = item.get('n')
decimals = item.get('decimals', 2)
label = item.get('label', f'Item {i+1}')
scale_min = item.get('scale_min')
scale_max = item.get('scale_max')
res = grim_test_single(mean, n, decimals, scale_min, scale_max)
res['label'] = label
results.append(res)
if not res.get('consistent', True):
n_inconsistent += 1
# 整体评估
total = len(results)
inconsistency_rate = n_inconsistent / total if total > 0 else 0
# 风险评分
if inconsistency_rate > 0.5:
risk_level = 'high'
risk_score = 70 + inconsistency_rate * 30
elif inconsistency_rate > 0.25:
risk_level = 'medium-high'
risk_score = 50 + inconsistency_rate * 40
elif inconsistency_rate > 0:
risk_level = 'medium'
risk_score = 30 + inconsistency_rate * 40
else:
risk_level = 'low'
risk_score = 0
summary = {
'test_name': 'GRIM Test (均值粒度一致性检验)',
'status': 'completed',
'total_items': total,
'inconsistent_items': n_inconsistent,
'inconsistency_rate': round(inconsistency_rate, 4),
'risk_level': risk_level,
'risk_score': round(float(risk_score), 1),
'details': results,
'interpretation': _interpret_grim(n_inconsistent, total, inconsistency_rate)
}
return summary
def _interpret_grim(n_inconsistent, total, rate):
"""生成可读的解释"""
if n_inconsistent == 0:
return f"✅ 全部 {total} 个均值通过 GRIM 检验,未发现数值不一致。"
elif rate > 0.5:
return (
f"⚠️ {total} 个均值中有 {n_inconsistent} 个({rate:.0%})未通过 GRIM 检验。"
f"超过半数均值与样本量不兼容,这是严重的数据不一致信号。"
f"强烈建议核查原始数据。"
)
elif rate > 0.25:
return (
f"⚠️ {total} 个均值中有 {n_inconsistent} 个({rate:.0%})未通过 GRIM 检验。"
f"建议仔细核查这些不一致的数据点。"
)
else:
return (
f"{total} 个均值中有 {n_inconsistent} 个({rate:.0%})未通过 GRIM 检验。"
f"少量不一致可能是四舍五入方式不同导致,建议结合其他检测综合判断。"
)
def main():
parser = argparse.ArgumentParser(
description='GRIM测试 - 检测报告均值与样本量的一致性'
)
parser.add_argument('--mean', type=str, help='报告的平均值')
parser.add_argument('--n', type=int, help='样本量')
parser.add_argument('--decimals', type=int, default=2, help='小数位数')
parser.add_argument('--scale', type=str, help='量表范围,如 "1-5"')
parser.add_argument('--input', '-i', help='输入JSON文件批量测试')
parser.add_argument('--output', '-o', help='输出JSON文件路径')
args = parser.parse_args()
scale_min, scale_max = None, None
if args.scale:
parts = args.scale.split('-')
if len(parts) == 2:
scale_min, scale_max = int(parts[0]), int(parts[1])
if args.input:
# 批量模式
with open(args.input, 'r', encoding='utf-8') as f:
items = json.load(f)
result = grim_test_batch(items)
elif args.mean and args.n:
# 单项模式
res = grim_test_single(args.mean, args.n, args.decimals, scale_min, scale_max)
result = {
'test_name': 'GRIM Test (均值粒度一致性检验)',
'status': 'completed',
'result': res,
'interpretation': (
f"✅ 均值 {args.mean} 与样本量 {args.n} 一致" if res.get('consistent')
else f"⚠️ 均值 {args.mean} 与样本量 {args.n} 不一致!最近合法均值为 {res.get('nearest_valid_mean')}"
)
}
else:
parser.print_help()
sys.exit(1)
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()