Files
homeagent-sdk/tools/gengskill/scripts/grim_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
"""
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()