gsb-cli 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +431 -0
- package/dist/src/api.js +106 -0
- package/dist/src/args.js +145 -0
- package/dist/src/commands.js +1145 -0
- package/dist/src/dataset.js +378 -0
- package/dist/src/format.js +143 -0
- package/dist/src/help.js +60 -0
- package/dist/src/index.js +24 -0
- package/dist/src/issues.js +95 -0
- package/dist/src/session.js +46 -0
- package/dist/src/skill.js +141 -0
- package/dist/src/types.js +1 -0
- package/dist/src/version.js +151 -0
- package/docs/api-contract.md +78 -0
- package/package.json +36 -0
- package/scripts/postinstall.mjs +47 -0
- package/skills/gsb-eval/SKILL.md +301 -0
- package/skills/gsb-eval/references/agent-cli.md +346 -0
- package/skills/gsb-eval/references/analysis.md +1438 -0
- package/skills/gsb-eval/references/anchor-design.md +91 -0
- package/skills/gsb-eval/references/case-analysis-report.md +500 -0
- package/skills/gsb-eval/references/data-format.md +60 -0
- package/skills/gsb-eval/references/decision-report-v1.md +122 -0
|
@@ -0,0 +1,1438 @@
|
|
|
1
|
+
# GSB 评估结果统计分析指南
|
|
2
|
+
|
|
3
|
+
评估收集完成后,执行统计分析并生成 HTML 报告。本指南记录完整方法论与可复用代码模板。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 分析框架:5层递进
|
|
8
|
+
|
|
9
|
+
| 层次 | 内容 | 核心问题 |
|
|
10
|
+
|------|------|---------|
|
|
11
|
+
| **L0 数据质量** | 锚点题重测信度 + 行为信号检测 → 评估者连贯性得分 | 数据可信吗?噪声有多大? |
|
|
12
|
+
| L1 总体胜负 | 归一化 verdict(结合 left_version)→ 6类计数 + 胜出方质量分布 | V_new 整体是否优于 V_old?显著/略好比例如何? |
|
|
13
|
+
| L2 统计显著性 | 二项检验 + Bootstrap CI(可选:按连贯性加权) | 提升是真实的还是随机噪声? |
|
|
14
|
+
| L3 细粒度剖析 | 按题目 / 按评估者拆分 + 灵敏度分析 | 哪些 case 赢、哪些 case 退步?结论对噪声稳健吗? |
|
|
15
|
+
| L4 根因归纳 | 评论文本分类 + 评估者聚类 | 为什么赢?为什么输?用户群体是否存在系统性偏好分歧? |
|
|
16
|
+
|
|
17
|
+
> **L0 是前置步骤,不是可选项。** 在开始 L1 计数之前,先评估数据质量——高噪声下的胜率数字是误导性的。锚点题设计规范见 [`anchor-design.md`](anchor-design.md)。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 核心数据结构(v2 归一化格式)
|
|
22
|
+
|
|
23
|
+
每条评估记录(`results/eval_*.json`)的格式:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"Q_0001": {
|
|
28
|
+
"query_id": "Q_0001",
|
|
29
|
+
"evaluator": "张三",
|
|
30
|
+
"winner": "gpt4_turbo", // 胜出版本的实际名称,或 "similar" 表示差不多
|
|
31
|
+
"magnitude": "much_better", // much_better | slightly_better | similar
|
|
32
|
+
"quality_rating": "meets", // exceeds | meets | below(所有场次都有值)
|
|
33
|
+
"comments": {
|
|
34
|
+
"gpt4_turbo": { "pros": "回答更简洁", "cons": "" },
|
|
35
|
+
"claude_3_5": { "pros": "", "cons": "参数错误" },
|
|
36
|
+
"general": "整体评价..."
|
|
37
|
+
},
|
|
38
|
+
"left_version": "gpt4_turbo", // 盲评时左边面板的版本(审计用)
|
|
39
|
+
"right_version": "claude_3_5",
|
|
40
|
+
"mode": "blind",
|
|
41
|
+
"timestamp": "2026-02-25T16:00:57.220429"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**关键**:
|
|
47
|
+
- `winner` 已在存储时完成归一化,直接是胜出版本的实际名称,**无需再做 left/right 映射**。
|
|
48
|
+
- `winner = "similar"` 对应持平判定;通过 `quality_rating` 区分"都好"(exceeds/meets)和"都不好"(below)。
|
|
49
|
+
- `quality_rating` 对**所有**场次都有值(含 similar),反映胜出方或双方共同的绝对质量:
|
|
50
|
+
- `exceeds`(超出预期)/ `meets`(符合预期)/ `below`(低于预期,"矮子里拔高个")
|
|
51
|
+
- 评论按版本名直接读取:`comments["gpt4_turbo"]["pros"]`,无需 left/right 映射。
|
|
52
|
+
- 若启用了管理员审核,分析时应通过 API `GET /api/summary?all=1&accepted_only=1` 获取仅已接受的结果。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## L0:数据质量与锚点分析
|
|
57
|
+
|
|
58
|
+
> **前提**:需要在数据准备阶段配置锚点题(每位评估者都会遇到、少量题目重复出现)。
|
|
59
|
+
> 若本次评估未配置锚点题,跳过 L0.1 / L0.2,仅做 L0.3 行为信号检测。
|
|
60
|
+
|
|
61
|
+
### L0.1 重测信度(Test-Retest Reliability)
|
|
62
|
+
|
|
63
|
+
同一道题让同一人评两次,检验前后判断是否一致。这是最干净的连贯性信号,不依赖"正确答案"假设。
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# anchor_ids: 在数据准备时预先标注的锚点题 ID 列表
|
|
67
|
+
# 例如 anchor_ids = ["Q_0001", "Q_0005"],且这些题目出现了两次(ID 如 Q_0001 和 Q_0001_repeat)
|
|
68
|
+
ANCHOR_PAIRS = {
|
|
69
|
+
"Q_0001": "Q_0001_repeat",
|
|
70
|
+
"Q_0005": "Q_0005_repeat",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
coherence_scores = {}
|
|
74
|
+
for ev, recs in by_ev.items():
|
|
75
|
+
by_qid = {r['query_id']: r for r in recs}
|
|
76
|
+
matches, total_pairs = 0, 0
|
|
77
|
+
for q1, q2 in ANCHOR_PAIRS.items():
|
|
78
|
+
if q1 in by_qid and q2 in by_qid:
|
|
79
|
+
total_pairs += 1
|
|
80
|
+
r1, r2 = by_qid[q1], by_qid[q2]
|
|
81
|
+
# 比较方向一致性(忽略量级)
|
|
82
|
+
if r1['norm_side'] == r2['norm_side']:
|
|
83
|
+
matches += 1
|
|
84
|
+
if total_pairs > 0:
|
|
85
|
+
coherence_scores[ev] = matches / total_pairs
|
|
86
|
+
else:
|
|
87
|
+
coherence_scores[ev] = None # 未覆盖到锚点题
|
|
88
|
+
|
|
89
|
+
for ev, score in sorted(coherence_scores.items(), key=lambda x: (x[1] is None, x[1] or 0)):
|
|
90
|
+
label = f"{score:.0%}" if score is not None else "未覆盖锚点题"
|
|
91
|
+
print(f"{ev}: 重测信度 = {label}")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### L0.2 关联锚点传递性检验(可选)
|
|
95
|
+
|
|
96
|
+
若多道锚点题测量相近维度,连贯评估者在这些题上应表现出相关性。随机作答者趋近噪声,判定方向之间几乎无相关。
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import numpy as np
|
|
100
|
+
|
|
101
|
+
# 将每个评估者在锚点题上的判定编码为向量
|
|
102
|
+
# Vnew_wins=+1, Vold_wins=-1, both_good/both_bad=0
|
|
103
|
+
anchor_ids = list(ANCHOR_PAIRS.keys())
|
|
104
|
+
|
|
105
|
+
def encode_side(s):
|
|
106
|
+
return +1 if s == 'Vnew_wins' else (-1 if s == 'Vold_wins' else 0)
|
|
107
|
+
|
|
108
|
+
ev_vectors = {}
|
|
109
|
+
for ev, recs in by_ev.items():
|
|
110
|
+
by_qid = {r['query_id']: r for r in recs}
|
|
111
|
+
vec = [encode_side(by_qid[q]['norm_side']) if q in by_qid else 0 for q in anchor_ids]
|
|
112
|
+
if any(v != 0 for v in vec):
|
|
113
|
+
ev_vectors[ev] = vec
|
|
114
|
+
|
|
115
|
+
# 计算锚点题两两之间的评估者相关性
|
|
116
|
+
if len(anchor_ids) >= 2:
|
|
117
|
+
mat = np.array(list(ev_vectors.values())) # shape: [n_evaluators, n_anchors]
|
|
118
|
+
for i in range(len(anchor_ids)):
|
|
119
|
+
for j in range(i+1, len(anchor_ids)):
|
|
120
|
+
corr = np.corrcoef(mat[:, i], mat[:, j])[0, 1] if len(mat) > 1 else float('nan')
|
|
121
|
+
print(f"锚点 {anchor_ids[i]} vs {anchor_ids[j]}: 相关系数 r = {corr:.2f}")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### L0.3 行为信号检测
|
|
125
|
+
|
|
126
|
+
利用评估元数据(若 server.py 记录了 `duration_ms` 和判定分布)识别低质量作答:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
DURATION_THRESHOLD_MS = 5000 # 判断"过快"的阈值(根据题目复杂度调整)
|
|
130
|
+
|
|
131
|
+
flags = {}
|
|
132
|
+
for ev, recs in by_ev.items():
|
|
133
|
+
n = len(recs)
|
|
134
|
+
# 信号1:作答时间异常短
|
|
135
|
+
fast_count = sum(1 for r in recs if r.get('duration_ms', 999999) < DURATION_THRESHOLD_MS)
|
|
136
|
+
fast_rate = fast_count / n if n else 0
|
|
137
|
+
|
|
138
|
+
# 信号2:全部选 same(both_good 或 both_bad)
|
|
139
|
+
same_count = sum(1 for r in recs if r['norm_side'] in ('both_good', 'both_bad'))
|
|
140
|
+
same_rate = same_count / n if n else 0
|
|
141
|
+
|
|
142
|
+
# 信号3:明显位置偏好(始终选左或始终选右,且未结合 left_version 分析)
|
|
143
|
+
left_pref = sum(1 for r in recs if r.get('verdict', '').startswith('left_'))
|
|
144
|
+
right_pref = sum(1 for r in recs if r.get('verdict', '').startswith('right_'))
|
|
145
|
+
position_bias = max(left_pref, right_pref) / n if n >= 5 else 0
|
|
146
|
+
|
|
147
|
+
flags[ev] = {
|
|
148
|
+
'fast_rate': fast_rate,
|
|
149
|
+
'same_rate': same_rate,
|
|
150
|
+
'position_bias': position_bias,
|
|
151
|
+
'suspicious': fast_rate > 0.5 or same_rate > 0.8 or position_bias > 0.85,
|
|
152
|
+
}
|
|
153
|
+
print(f"{ev}: 过快={fast_rate:.0%} | 全same={same_rate:.0%} | 位置偏好={position_bias:.0%}"
|
|
154
|
+
+ (" ⚠️ 疑似低质量" if flags[ev]['suspicious'] else ""))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### L0.4 连贯性得分与加权
|
|
158
|
+
|
|
159
|
+
综合重测信度(L0.1)和行为信号(L0.3),为每位评估者计算连贯性得分,用于后续加权分析。
|
|
160
|
+
|
|
161
|
+
**不建议直接剔除**低连贯评估者——这容易误杀有独立偏好的认真评估者。推荐加权保留。
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
def compute_coherence_weight(ev):
|
|
165
|
+
"""综合重测信度与行为信号,输出 0~1 权重"""
|
|
166
|
+
base = coherence_scores.get(ev)
|
|
167
|
+
if base is None:
|
|
168
|
+
base = 0.7 # 无锚点题数据时给予中等默认值
|
|
169
|
+
|
|
170
|
+
flag = flags.get(ev, {})
|
|
171
|
+
penalty = 0.0
|
|
172
|
+
if flag.get('fast_rate', 0) > 0.5: penalty += 0.3
|
|
173
|
+
if flag.get('same_rate', 0) > 0.8: penalty += 0.3
|
|
174
|
+
if flag.get('position_bias', 0) > 0.85: penalty += 0.2
|
|
175
|
+
|
|
176
|
+
return max(0.1, base - penalty) # 最低保留 10% 权重,不完全剔除
|
|
177
|
+
|
|
178
|
+
weights = {ev: compute_coherence_weight(ev) for ev in by_ev}
|
|
179
|
+
print("\n评估者连贯性权重:")
|
|
180
|
+
for ev, w in sorted(weights.items(), key=lambda x: -x[1]):
|
|
181
|
+
print(f" {ev}: {w:.2f}")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### L0.5 位置偏倚检测(Position Bias)
|
|
185
|
+
|
|
186
|
+
盲评模式下左右位置随机分配,但评估者可能无意识地偏好某一侧。系统性地检测这一偏倚:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# 计算每位评估者的左/右偏好
|
|
190
|
+
position_stats = {}
|
|
191
|
+
for ev, recs in by_ev.items():
|
|
192
|
+
blind_recs = [r for r in recs if r.get('mode') == 'blind']
|
|
193
|
+
if len(blind_recs) < 5:
|
|
194
|
+
continue
|
|
195
|
+
# 统计:选了左边多少次(包含 much_better + slightly_better)
|
|
196
|
+
left_wins = sum(1 for r in blind_recs
|
|
197
|
+
if r.get('left_version') and r.get('winner') == r['left_version'])
|
|
198
|
+
right_wins = sum(1 for r in blind_recs
|
|
199
|
+
if r.get('right_version') and r.get('winner') == r['right_version'])
|
|
200
|
+
n = len(blind_recs)
|
|
201
|
+
left_rate = left_wins / n
|
|
202
|
+
right_rate = right_wins / n
|
|
203
|
+
|
|
204
|
+
# 二项检验:左边胜率是否偏离 50%(排除 similar 后)
|
|
205
|
+
decisive_blind = [r for r in blind_recs if r.get('winner') != 'similar'
|
|
206
|
+
and r.get('winner') in (r.get('left_version'), r.get('right_version'))]
|
|
207
|
+
n_dec = len(decisive_blind)
|
|
208
|
+
left_dec = sum(1 for r in decisive_blind if r['winner'] == r['left_version'])
|
|
209
|
+
if n_dec >= 5:
|
|
210
|
+
p_pos = stats.binomtest(left_dec, n=n_dec, p=0.5, alternative='two-sided').pvalue
|
|
211
|
+
else:
|
|
212
|
+
p_pos = None
|
|
213
|
+
|
|
214
|
+
position_stats[ev] = {
|
|
215
|
+
'left_rate': left_rate, 'right_rate': right_rate,
|
|
216
|
+
'n_blind': n, 'n_decisive': n_dec,
|
|
217
|
+
'position_pvalue': p_pos,
|
|
218
|
+
'suspicious': p_pos is not None and p_pos < 0.05,
|
|
219
|
+
}
|
|
220
|
+
flag = " ⚠️ 位置偏倚显著" if position_stats[ev]['suspicious'] else ""
|
|
221
|
+
print(f"{ev}: 左边偏好={left_rate:.0%} | 右边偏好={right_rate:.0%} | p={p_pos}{flag}")
|
|
222
|
+
|
|
223
|
+
# 全局位置偏倚检验
|
|
224
|
+
all_blind_decisive = [r for r in all_records
|
|
225
|
+
if r.get('mode') == 'blind' and r.get('winner') != 'similar'
|
|
226
|
+
and r.get('winner') in (r.get('left_version'), r.get('right_version'))]
|
|
227
|
+
global_left = sum(1 for r in all_blind_decisive if r['winner'] == r['left_version'])
|
|
228
|
+
global_n = len(all_blind_decisive)
|
|
229
|
+
if global_n >= 10:
|
|
230
|
+
global_p = stats.binomtest(global_left, n=global_n, p=0.5, alternative='two-sided').pvalue
|
|
231
|
+
print(f"\n全局位置偏倚: 左边胜 {global_left}/{global_n} ({global_left/global_n:.1%}), p={global_p:.4f}"
|
|
232
|
+
+ (" ⚠️ 存在系统性位置偏倚" if global_p < 0.05 else " ✅ 未检测到系统性位置偏倚"))
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
> 若全局位置偏倚显著(p < 0.05),说明一侧版本被系统性高估,需检查是否是盲评随机化不足、或某版本在特定侧出现频率更高。单个评估者的显著位置偏倚应作为低质量信号纳入连贯性权重。
|
|
236
|
+
|
|
237
|
+
### L0.6 评分者间信度(Inter-Rater Reliability)
|
|
238
|
+
|
|
239
|
+
原始一致率(agreement rate)不校正随机一致的影响。科学报告应使用 Cohen's Kappa(2 人)或 Fleiss' Kappa(多人),对 GSB 的三分类(Vnew 胜 / Vold 胜 / 相似)做信度评估。
|
|
240
|
+
|
|
241
|
+
**Cohen's Kappa(两两评估者配对)**:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
def cohens_kappa(ratings1, ratings2, categories):
|
|
245
|
+
"""计算两评估者间的 Cohen's Kappa。
|
|
246
|
+
ratings1/2: list of category labels
|
|
247
|
+
categories: list of possible category values
|
|
248
|
+
"""
|
|
249
|
+
n = len(ratings1)
|
|
250
|
+
if n == 0:
|
|
251
|
+
return float('nan')
|
|
252
|
+
|
|
253
|
+
# 观测一致率
|
|
254
|
+
po = sum(1 for a, b in zip(ratings1, ratings2) if a == b) / n
|
|
255
|
+
|
|
256
|
+
# 期望一致率(随机)
|
|
257
|
+
pe = 0.0
|
|
258
|
+
for cat in categories:
|
|
259
|
+
p1 = ratings1.count(cat) / n
|
|
260
|
+
p2 = ratings2.count(cat) / n
|
|
261
|
+
pe += p1 * p2
|
|
262
|
+
|
|
263
|
+
if pe == 1.0:
|
|
264
|
+
return 1.0 if po == 1.0 else float('nan')
|
|
265
|
+
return (po - pe) / (1.0 - pe)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# 找出所有评估者两两共享的题目
|
|
269
|
+
evaluator_names = list(by_ev.keys())
|
|
270
|
+
CATEGORIES = [V_NEW, V_OLD, 'similar']
|
|
271
|
+
|
|
272
|
+
pairwise_kappas = []
|
|
273
|
+
for i in range(len(evaluator_names)):
|
|
274
|
+
for j in range(i + 1, len(evaluator_names)):
|
|
275
|
+
ev1, ev2 = evaluator_names[i], evaluator_names[j]
|
|
276
|
+
by_q1 = {r['query_id']: r.get('winner', '') for r in by_ev[ev1]}
|
|
277
|
+
by_q2 = {r['query_id']: r.get('winner', '') for r in by_ev[ev2]}
|
|
278
|
+
shared = sorted(set(by_q1.keys()) & set(by_q2.keys()))
|
|
279
|
+
if len(shared) < 5:
|
|
280
|
+
continue
|
|
281
|
+
r1 = [by_q1[q] for q in shared]
|
|
282
|
+
r2 = [by_q2[q] for q in shared]
|
|
283
|
+
k = cohens_kappa(r1, r2, CATEGORIES)
|
|
284
|
+
pairwise_kappas.append((ev1, ev2, k, len(shared)))
|
|
285
|
+
|
|
286
|
+
mean_kappa = np.mean([k for _, _, k, _ in pairwise_kappas]) if pairwise_kappas else float('nan')
|
|
287
|
+
print(f"\n评分者间信度(Cohen's Kappa):")
|
|
288
|
+
for ev1, ev2, k, n_shared in pairwise_kappas:
|
|
289
|
+
level = 'Almost Perfect' if k > 0.8 else ('Substantial' if k > 0.6 else ('Moderate' if k > 0.4 else 'Poor'))
|
|
290
|
+
print(f" {ev1} ↔ {ev2}: κ = {k:.3f} ({level}), n = {n_shared}")
|
|
291
|
+
print(f" 平均 Kappa = {mean_kappa:.3f}")
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Fleiss' Kappa(多人同时评估同一题目)**:
|
|
295
|
+
|
|
296
|
+
当 ≥ 3 位评估者评估了同一题目时,Fleiss' Kappa 比两两 Cohen's Kappa 更高效:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
def fleiss_kappa(ratings_matrix):
|
|
300
|
+
"""计算 Fleiss' Kappa。
|
|
301
|
+
ratings_matrix: list of list, 每行是一道题,每列是各评估者的判定
|
|
302
|
+
每个单元格是类别标签 (str)
|
|
303
|
+
"""
|
|
304
|
+
n_items = len(ratings_matrix) # 题目数
|
|
305
|
+
n_raters = len(ratings_matrix[0]) # 评估者数(每道题必须相同)
|
|
306
|
+
|
|
307
|
+
# 统计每道题每个类别被选中的次数
|
|
308
|
+
categories = sorted(set(r for row in ratings_matrix for r in row))
|
|
309
|
+
cat_to_idx = {c: i for i, c in enumerate(categories)}
|
|
310
|
+
|
|
311
|
+
# n_ij: 题目 i 中类别 j 被选中的次数
|
|
312
|
+
n_ij = [[0] * len(categories) for _ in range(n_items)]
|
|
313
|
+
for i, row in enumerate(ratings_matrix):
|
|
314
|
+
for r in row:
|
|
315
|
+
n_ij[i][cat_to_idx[r]] += 1
|
|
316
|
+
|
|
317
|
+
# P_i: 每道题的评估者间一致度
|
|
318
|
+
P_i = []
|
|
319
|
+
for i in range(n_items):
|
|
320
|
+
s = sum(n_ij[i][j] ** 2 for j in range(len(categories)))
|
|
321
|
+
P_i.append((s - n_raters) / (n_raters * (n_raters - 1)))
|
|
322
|
+
|
|
323
|
+
P_bar = np.mean(P_i)
|
|
324
|
+
|
|
325
|
+
# p_j: 每个类别的总体比例
|
|
326
|
+
p_j = [sum(n_ij[i][j] for i in range(n_items)) / (n_items * n_raters)
|
|
327
|
+
for j in range(len(categories))]
|
|
328
|
+
P_e = sum(p ** 2 for p in p_j)
|
|
329
|
+
|
|
330
|
+
if P_e == 1.0:
|
|
331
|
+
return 1.0 if P_bar == 1.0 else float('nan')
|
|
332
|
+
return (P_bar - P_e) / (1.0 - P_e)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# 为 Fleiss' Kappa 构建评分矩阵(要求每道题评估者数一致)
|
|
336
|
+
# 先找出所有评估者均参与的题目
|
|
337
|
+
complete_qids = [qid for qid, recs in by_q.items() if len(recs) == len(by_ev)]
|
|
338
|
+
if len(complete_qids) >= 5:
|
|
339
|
+
matrix = []
|
|
340
|
+
for qid in sorted(complete_qids):
|
|
341
|
+
row = [r.get('winner', '') for r in sorted(by_q[qid], key=lambda x: x['evaluator'])]
|
|
342
|
+
matrix.append(row)
|
|
343
|
+
fk = fleiss_kappa(matrix)
|
|
344
|
+
print(f"\nFleiss' Kappa({len(complete_qids)} 道完整题目, {len(by_ev)} 位评估者): κ = {fk:.3f}")
|
|
345
|
+
else:
|
|
346
|
+
print(f"\nFleiss' Kappa: 完整评估题目不足(需 ≥5,当前 {len(complete_qids)}),使用两两 Cohen's Kappa 代替")
|
|
347
|
+
|
|
348
|
+
# 决定性判定 Kappa(排除 similar 后)
|
|
349
|
+
dec_categories = [V_NEW, V_OLD]
|
|
350
|
+
dec_pairwise = []
|
|
351
|
+
for i in range(len(evaluator_names)):
|
|
352
|
+
for j in range(i + 1, len(evaluator_names)):
|
|
353
|
+
ev1, ev2 = evaluator_names[i], evaluator_names[j]
|
|
354
|
+
by_q1 = {r['query_id']: r.get('winner', '') for r in by_ev[ev1]}
|
|
355
|
+
by_q2 = {r['query_id']: r.get('winner', '') for r in by_ev[ev2]}
|
|
356
|
+
shared = sorted(set(by_q1.keys()) & set(by_q2.keys()))
|
|
357
|
+
# 只保留两人都有明确偏好的题目
|
|
358
|
+
dec_shared = [q for q in shared
|
|
359
|
+
if by_q1[q] != 'similar' and by_q2[q] != 'similar']
|
|
360
|
+
if len(dec_shared) < 5:
|
|
361
|
+
continue
|
|
362
|
+
r1 = [by_q1[q] for q in dec_shared]
|
|
363
|
+
r2 = [by_q2[q] for q in dec_shared]
|
|
364
|
+
k = cohens_kappa(r1, r2, dec_categories)
|
|
365
|
+
dec_pairwise.append((ev1, ev2, k, len(dec_shared)))
|
|
366
|
+
|
|
367
|
+
if dec_pairwise:
|
|
368
|
+
mean_dec_kappa = np.mean([k for _, _, k, _ in dec_pairwise])
|
|
369
|
+
print(f" 决定性判定平均 Kappa = {mean_dec_kappa:.3f}")
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Kappa 判读标准(Landis & Koch)**:
|
|
373
|
+
|
|
374
|
+
| Kappa 范围 | 一致性水平 | 对分析结论的影响 |
|
|
375
|
+
|-----------|-----------|----------------|
|
|
376
|
+
| > 0.80 | Almost Perfect | 结论高度可靠 |
|
|
377
|
+
| 0.60–0.80 | Substantial | 结论可信,可正常解读 |
|
|
378
|
+
| 0.40–0.60 | Moderate | 结论存在不确定性,需标注 |
|
|
379
|
+
| 0.20–0.40 | Fair | 评估标准需重新对齐,结论仅作参考 |
|
|
380
|
+
| < 0.20 | Slight/Poor | 数据不可用于结论,需重新评估 |
|
|
381
|
+
|
|
382
|
+
> **重要区分**:全判定 Kappa(含 similar)通常低于决定性判定 Kappa(排除 similar),因为 similar 是更大的分歧空间。报告中两者都应呈现,决定性判定 Kappa 反映方向共识的核心质量。
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## L1:总体计数
|
|
387
|
+
|
|
388
|
+
v2 格式中 `winner` 已是归一化版本名,无需额外处理。
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
import json, os
|
|
392
|
+
from collections import Counter, defaultdict
|
|
393
|
+
|
|
394
|
+
RESULTS_DIR = "results" # 评估结果目录
|
|
395
|
+
V_NEW = "claude_3_5" # 新版本名(按实际调整)
|
|
396
|
+
V_OLD = "gpt4_turbo" # 旧版本名(按实际调整)
|
|
397
|
+
|
|
398
|
+
all_records = []
|
|
399
|
+
for fname in sorted(os.listdir(RESULTS_DIR)):
|
|
400
|
+
if fname.endswith('.json') and not fname.startswith('_'):
|
|
401
|
+
with open(os.path.join(RESULTS_DIR, fname)) as f:
|
|
402
|
+
for record in json.load(f).values():
|
|
403
|
+
all_records.append(record)
|
|
404
|
+
|
|
405
|
+
total = len(all_records)
|
|
406
|
+
|
|
407
|
+
# 胜负分布
|
|
408
|
+
winner_counts = Counter(r.get('winner', '') for r in all_records)
|
|
409
|
+
magnitude_counts = Counter(r.get('magnitude', '') for r in all_records)
|
|
410
|
+
quality_counts = Counter(r.get('quality_rating', '') for r in all_records if r.get('quality_rating'))
|
|
411
|
+
|
|
412
|
+
# similar 细分:用 quality_rating 区分"都好"和"都不好"
|
|
413
|
+
similar_recs = [r for r in all_records if r.get('winner') == 'similar']
|
|
414
|
+
similar_good = sum(1 for r in similar_recs if r.get('quality_rating') in ('exceeds', 'meets'))
|
|
415
|
+
similar_bad = sum(1 for r in similar_recs if r.get('quality_rating') == 'below')
|
|
416
|
+
|
|
417
|
+
print(f"总计 {total} 条")
|
|
418
|
+
print(f"winner 分布: {dict(winner_counts)}")
|
|
419
|
+
print(f" 其中 similar 细分: 都好={similar_good}, 都不好={similar_bad}")
|
|
420
|
+
print(f"magnitude 分布: {dict(magnitude_counts)}")
|
|
421
|
+
print(f"quality 分布: {dict(quality_counts)}")
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
两个维度分析:
|
|
425
|
+
- **胜负维度**(winner):版本名 or "similar" — 回答"谁更好"
|
|
426
|
+
- 有偏好:实际版本名(V_new / V_old)
|
|
427
|
+
- 持平:`"similar"`,再由 `quality_rating` 细分为"都好"(exceeds/meets)或"都不好"(below)
|
|
428
|
+
- **量级维度**(magnitude):`much_better` / `slightly_better` / `similar` — 回答"好多少"
|
|
429
|
+
- **质量维度**(quality_rating):**所有场次都有值** — 回答"胜出方绝对质量如何"
|
|
430
|
+
- `exceeds`(超出预期)/ `meets`(符合预期)/ `below`(低于预期)
|
|
431
|
+
|
|
432
|
+
> `similar + below`(都不好)≠ `winner=V_old + below`(旧版胜但绝对质量不达标)——前者是两版本共同缺陷,后者是新版退步。
|
|
433
|
+
|
|
434
|
+
### L1 补充:GSB 综合得分(Composite Score)
|
|
435
|
+
|
|
436
|
+
除了计数和胜率,可以用加权利分将 GSB 判定转化为可比较的综合得分,方便跨任务对比:
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
# 评分映射:much_better=2, slightly_better=1, similar=0
|
|
440
|
+
# 方向:正值 = V_NEW 胜, 负值 = V_OLD 胜
|
|
441
|
+
SCORE_MAP = {
|
|
442
|
+
'much_better': 2,
|
|
443
|
+
'slightly_better': 1,
|
|
444
|
+
'similar': 0,
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# 题目级聚合(多评估者对同一题的判定取平均)
|
|
448
|
+
question_scores = {}
|
|
449
|
+
for qid, recs in by_q.items():
|
|
450
|
+
scores = []
|
|
451
|
+
for r in recs:
|
|
452
|
+
w = r.get('winner', '')
|
|
453
|
+
mag = r.get('magnitude', 'similar')
|
|
454
|
+
# 计算原始分数
|
|
455
|
+
raw = SCORE_MAP.get(mag, 0)
|
|
456
|
+
if w == V_NEW:
|
|
457
|
+
s = raw
|
|
458
|
+
elif w == V_OLD:
|
|
459
|
+
s = -raw
|
|
460
|
+
else:
|
|
461
|
+
s = 0 # similar
|
|
462
|
+
# 可选:按评估者连贯性加权
|
|
463
|
+
weight = weights.get(r['evaluator'], 1.0)
|
|
464
|
+
scores.append(s * weight)
|
|
465
|
+
|
|
466
|
+
if scores:
|
|
467
|
+
avg_score = sum(scores) / sum(weights.get(r['evaluator'], 1.0) for r in recs)
|
|
468
|
+
question_scores[qid] = {
|
|
469
|
+
'score': round(avg_score, 2),
|
|
470
|
+
'verdict': V_NEW if avg_score > 0 else (V_OLD if avg_score < 0 else 'tie'),
|
|
471
|
+
'n_evaluators': len(recs),
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# 总体 GSB 综合得分
|
|
475
|
+
total_score = sum(qs['score'] for qs in question_scores.values())
|
|
476
|
+
max_possible = len(question_scores) * 2 # 理论最高分(全票 much_better for V_NEW)
|
|
477
|
+
normalized_score = total_score / max_possible if max_possible > 0 else 0
|
|
478
|
+
|
|
479
|
+
# 胜率计算(排除持平题)
|
|
480
|
+
focus_wins = sum(1 for qs in question_scores.values() if qs['verdict'] == V_NEW)
|
|
481
|
+
baseline_wins = sum(1 for qs in question_scores.values() if qs['verdict'] == V_OLD)
|
|
482
|
+
ties = sum(1 for qs in question_scores.values() if qs['verdict'] == 'tie')
|
|
483
|
+
decisive_total = focus_wins + baseline_wins
|
|
484
|
+
|
|
485
|
+
print(f"\nGSB 综合得分:")
|
|
486
|
+
print(f" 总得分: {total_score:.1f} / {max_possible}(归一化: {normalized_score:+.3f})")
|
|
487
|
+
print(f" 题目级判定: {V_NEW}胜 {focus_wins} 题, {V_OLD}胜 {baseline_wins} 题, 持平 {ties} 题")
|
|
488
|
+
if decisive_total > 0:
|
|
489
|
+
print(f" 胜率(排除持平): {focus_wins}/{decisive_total} = {focus_wins/decisive_total:.1%}")
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
> **综合得分的优势**:单次评估内的得分可用于跨任务对比(如"本模型在上次评估得 +0.35,本次得 +0.42"),但需注意不同测试集的得分不可直接对比。归一化得分 > 0 表示新版整体占优,越接近 +1 优势越大。
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## L2:统计显著性检验
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
import numpy as np
|
|
500
|
+
from scipy import stats
|
|
501
|
+
|
|
502
|
+
# 只取有明确方向的"决定性场次"(排除 winner="similar")
|
|
503
|
+
decisive = [r for r in all_records if r.get('winner') != 'similar']
|
|
504
|
+
n = len(decisive)
|
|
505
|
+
n_new = sum(1 for r in decisive if r.get('winner') == V_NEW)
|
|
506
|
+
|
|
507
|
+
print(f"决定性场次: {n},新版胜: {n_new}({n_new/n:.1%})")
|
|
508
|
+
|
|
509
|
+
# 二项检验(H₀: 两版本获胜概率相等,即 p=0.5)
|
|
510
|
+
binom = stats.binomtest(n_new, n=n, p=0.5, alternative='two-sided')
|
|
511
|
+
ci = binom.proportion_ci(confidence_level=0.95, method='wilson')
|
|
512
|
+
print(f"p-value: {binom.pvalue:.4f}")
|
|
513
|
+
print(f"95% CI (Wilson): ({ci.low:.1%}, {ci.high:.1%})")
|
|
514
|
+
|
|
515
|
+
# Bootstrap CI(更稳健,用于验证)
|
|
516
|
+
np.random.seed(42)
|
|
517
|
+
labels = np.array([1 if r.get('winner') == V_NEW else 0 for r in decisive])
|
|
518
|
+
boot_rates = [np.mean(np.random.choice(labels, size=n, replace=True)) for _ in range(10000)]
|
|
519
|
+
boot_ci = np.percentile(boot_rates, [2.5, 97.5])
|
|
520
|
+
print(f"Bootstrap 95% CI: ({boot_ci[0]:.1%}, {boot_ci[1]:.1%})")
|
|
521
|
+
|
|
522
|
+
# 额外:区分"显著好"和"略好"的信号强度
|
|
523
|
+
n_new_much = sum(1 for r in decisive if r.get('winner') == V_NEW and r.get('magnitude') == 'much_better')
|
|
524
|
+
n_new_slight = sum(1 for r in decisive if r.get('winner') == V_NEW and r.get('magnitude') == 'slightly_better')
|
|
525
|
+
print(f"新版胜中:显著好 {n_new_much},略好 {n_new_slight}")
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**结论判读规则**:
|
|
529
|
+
- `p < 0.05` → 差异显著,新版提升非偶然
|
|
530
|
+
- CI 下限 > 50% → 新版胜率置信区间完全在 50% 以上,结论稳健
|
|
531
|
+
- Wilson CI 和 Bootstrap CI 对齐 → 结论可靠
|
|
532
|
+
- 显著好占比高 → 提升更强烈、更可靠
|
|
533
|
+
|
|
534
|
+
**注意**:若样本量 n < 30,p 值仅供参考,优先看 CI 范围宽度(CI 越宽,置信度越低)。
|
|
535
|
+
|
|
536
|
+
### L2 补充:灵敏度分析(Sensitivity Analysis)
|
|
537
|
+
|
|
538
|
+
同时呈现两组结果:全量数据 vs 过滤低连贯评估者后的数据。若两者结论一致,结果稳健;若差异显著,说明噪声数据正在影响结论方向。
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
COHERENCE_THRESHOLD = 0.5 # 低于此阈值视为低连贯
|
|
542
|
+
|
|
543
|
+
high_coherence_records = [
|
|
544
|
+
r for r in all_records
|
|
545
|
+
if weights.get(r['evaluator'], 1.0) >= COHERENCE_THRESHOLD
|
|
546
|
+
]
|
|
547
|
+
|
|
548
|
+
def run_binom(records):
|
|
549
|
+
decisive = [r for r in records if r.get('winner') != 'similar']
|
|
550
|
+
n = len(decisive)
|
|
551
|
+
if n == 0: return None
|
|
552
|
+
n_new = sum(1 for r in decisive if r.get('winner') == V_NEW)
|
|
553
|
+
result = stats.binomtest(n_new, n=n, p=0.5)
|
|
554
|
+
ci = result.proportion_ci(0.95, 'wilson')
|
|
555
|
+
return {'n': n, 'rate': n_new/n, 'p': result.pvalue, 'ci': (ci.low, ci.high)}
|
|
556
|
+
|
|
557
|
+
full = run_binom(all_records)
|
|
558
|
+
filtered = run_binom(high_coherence_records)
|
|
559
|
+
|
|
560
|
+
print(f"全量数据: n={full['n']}, 新版胜率={full['rate']:.1%}, p={full['p']:.4f}, CI={full['ci'][0]:.1%}~{full['ci'][1]:.1%}")
|
|
561
|
+
print(f"高连贯评估者: n={filtered['n']}, 新版胜率={filtered['rate']:.1%}, p={filtered['p']:.4f}, CI={filtered['ci'][0]:.1%}~{filtered['ci'][1]:.1%}")
|
|
562
|
+
|
|
563
|
+
delta = abs(full['rate'] - filtered['rate'])
|
|
564
|
+
if delta > 0.05:
|
|
565
|
+
print(f"⚠️ 两组胜率差异 {delta:.1%},噪声数据影响显著,建议优先参考高连贯组结论")
|
|
566
|
+
else:
|
|
567
|
+
print(f"✅ 两组结论一致(差异 {delta:.1%}),结果稳健")
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### L2 补充2:McNemar 检验(配对二分类)
|
|
571
|
+
|
|
572
|
+
二项检验和 Bootstrap 对所有决定性判定做聚合检验,但未利用配对设计信息。McNemar's test 是配对二分类数据的行业标准检验:对每道题同一评估者的配对判定,检验新版胜 vs 旧版胜的差异。
|
|
573
|
+
|
|
574
|
+
```python
|
|
575
|
+
def mcnemar_test(records, evaluator_field='evaluator', query_field='query_id'):
|
|
576
|
+
"""对配对判定做 McNemar 检验。
|
|
577
|
+
只考虑同一评估者对同一题目的判定(自然配对)。
|
|
578
|
+
返回 (chi2_stat, p_value, n_discordant_vnew, n_discordant_vold)
|
|
579
|
+
"""
|
|
580
|
+
from collections import defaultdict
|
|
581
|
+
# 按 (evaluator, query_id) 索引
|
|
582
|
+
paired = defaultdict(dict)
|
|
583
|
+
for r in records:
|
|
584
|
+
key = (r[evaluator_field], r[query_field])
|
|
585
|
+
paired[key] = r
|
|
586
|
+
|
|
587
|
+
# 统计 discordant pairs: 同一评估者一题上新版胜、另一题上旧版胜
|
|
588
|
+
# 但 GSB 是同一道题同时比较两版,判定本身就是配对的
|
|
589
|
+
# 所以 McNemar 直接在题目级做:统计各题目多数意见
|
|
590
|
+
by_q = defaultdict(list)
|
|
591
|
+
for r in records:
|
|
592
|
+
by_q[r[query_field]].append(r)
|
|
593
|
+
|
|
594
|
+
b = 0 # 新版胜 → 旧版胜(新版退步)
|
|
595
|
+
c = 0 # 旧版胜 → 新版胜(新版提升)
|
|
596
|
+
for qid, recs in by_q.items():
|
|
597
|
+
n_new = sum(1 for r in recs if r.get('winner') == V_NEW)
|
|
598
|
+
n_old = sum(1 for r in recs if r.get('winner') == V_OLD)
|
|
599
|
+
if n_new > n_old:
|
|
600
|
+
c += 1 # 该题多数意见偏向新版
|
|
601
|
+
elif n_old > n_new:
|
|
602
|
+
b += 1 # 该题多数意见偏向旧版
|
|
603
|
+
# n_new == n_old: 归入 tie,不进入 b/c 计数
|
|
604
|
+
|
|
605
|
+
if b + c == 0:
|
|
606
|
+
return None, 1.0, 0, 0
|
|
607
|
+
|
|
608
|
+
# McNemar chi-squared statistic (with continuity correction)
|
|
609
|
+
chi2 = (abs(b - c) - 1) ** 2 / (b + c) if (b + c) > 0 else 0
|
|
610
|
+
from scipy.stats import chi2 as chi2_dist
|
|
611
|
+
p = 1.0 - chi2_dist.cdf(chi2, 1)
|
|
612
|
+
return chi2, p, b, c
|
|
613
|
+
|
|
614
|
+
chi2, p_mc, n_disc_old, n_disc_new = mcnemar_test(all_records)
|
|
615
|
+
if chi2 is not None:
|
|
616
|
+
print(f"\nMcNemar 检验: χ² = {chi2:.3f}, p = {p_mc:.4f}")
|
|
617
|
+
print(f" 新版胜→旧版胜: {n_disc_new} 题, 旧版胜→新版胜: {n_disc_old} 题")
|
|
618
|
+
if p_mc < 0.05:
|
|
619
|
+
print(f" ✅ 新版显著占优(p < 0.05)")
|
|
620
|
+
else:
|
|
621
|
+
print(f" → 差异不显著")
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
> McNemar 和 Binomial test 的关系:Binomial test 检验「胜率是否偏离 50%」,McNemar 检验「在 discordant pairs 中新版胜 vs 旧版胜是否对称」。两者互补,报告中建议同时呈现。当两检验结论一致时,结论更稳健。
|
|
625
|
+
|
|
626
|
+
### L2 补充3:效应量(Effect Size)
|
|
627
|
+
|
|
628
|
+
p 值只回答「是否存在差异」,效应量回答「差异有多大」。对于比例数据,使用 **Cohen's h**(两比例差别的标准化度量):
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
import math
|
|
632
|
+
|
|
633
|
+
def cohens_h(p1, p2):
|
|
634
|
+
"""计算两比例的 Cohen's h 效应量。
|
|
635
|
+
h = 2 * (arcsin(sqrt(p1)) - arcsin(sqrt(p2)))
|
|
636
|
+
判读: |h| < 0.2 = negligible, 0.2-0.5 = small, 0.5-0.8 = medium, > 0.8 = large
|
|
637
|
+
"""
|
|
638
|
+
return 2.0 * (math.asin(math.sqrt(p1)) - math.asin(math.sqrt(p2)))
|
|
639
|
+
|
|
640
|
+
# 新版胜率 vs 随机基线 (50%)
|
|
641
|
+
n_decisive = len(decisive)
|
|
642
|
+
p_new = n_new / n_decisive if n_decisive > 0 else 0.5
|
|
643
|
+
p_null = 0.5
|
|
644
|
+
|
|
645
|
+
h = cohens_h(p_new, p_null)
|
|
646
|
+
h_level = 'Large' if abs(h) > 0.8 else ('Medium' if abs(h) > 0.5 else ('Small' if abs(h) > 0.2 else 'Negligible'))
|
|
647
|
+
|
|
648
|
+
# 新版胜率 vs 旧版胜率
|
|
649
|
+
p_old = (n_decisive - n_new) / n_decisive if n_decisive > 0 else 0.5
|
|
650
|
+
h_vs_old = cohens_h(p_new, p_old)
|
|
651
|
+
|
|
652
|
+
print(f"\n效应量 (Cohen's h):")
|
|
653
|
+
print(f" 新版 vs 随机: h = {h:+.3f} ({h_level})")
|
|
654
|
+
print(f" 新版 vs 旧版: h = {h_vs_old:+.3f}")
|
|
655
|
+
print(f" 新版胜率: {p_new:.1%}, 旧版胜率: {p_old:.1%}, 差值: {p_new - p_old:+.1%}")
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
> **报告规则**:当 p < 0.05 但 |h| < 0.2 时,差异虽统计显著但实际意义微弱(大样本下常见),应标注"统计显著但效应量忽略不计"。
|
|
659
|
+
|
|
660
|
+
### L2 补充4:多重比较校正(Multiple Comparison Correction)
|
|
661
|
+
|
|
662
|
+
当按题目类型、难度、评估者等多个维度做分层分析时,同时进行多次统计检验会膨胀第一类错误率(Family-Wise Error Rate)。使用 **Benjamini-Hochberg** 方法控制错误发现率(FDR):
|
|
663
|
+
|
|
664
|
+
```python
|
|
665
|
+
def benjamini_hochberg(pvalues, alpha=0.05):
|
|
666
|
+
"""Benjamini-Hochberg FDR 校正。
|
|
667
|
+
pvalues: list of (name, p_value)
|
|
668
|
+
返回: list of (name, p_value, is_significant_after_correction)
|
|
669
|
+
"""
|
|
670
|
+
sorted_p = sorted(pvalues, key=lambda x: x[1])
|
|
671
|
+
n = len(sorted_p)
|
|
672
|
+
results = []
|
|
673
|
+
for rank, (name, p) in enumerate(sorted_p, 1):
|
|
674
|
+
bh_threshold = (rank / n) * alpha
|
|
675
|
+
results.append((name, p, p <= bh_threshold, bh_threshold))
|
|
676
|
+
return results
|
|
677
|
+
|
|
678
|
+
# 示例:按题目类型分层检验
|
|
679
|
+
type_pvalues = []
|
|
680
|
+
for qtype, qids in type_groups.items():
|
|
681
|
+
type_recs = [r for r in all_records if r['query_id'] in qids]
|
|
682
|
+
type_dec = [r for r in type_recs if r.get('winner') != 'similar']
|
|
683
|
+
if len(type_dec) >= 10:
|
|
684
|
+
n_t = len(type_dec)
|
|
685
|
+
n_new_t = sum(1 for r in type_dec if r.get('winner') == V_NEW)
|
|
686
|
+
p_t = stats.binomtest(n_new_t, n=n_t, p=0.5, alternative='two-sided').pvalue
|
|
687
|
+
type_pvalues.append((qtype, p_t))
|
|
688
|
+
|
|
689
|
+
if type_pvalues:
|
|
690
|
+
corrected = benjamini_hochberg(type_pvalues)
|
|
691
|
+
print("\n分层显著性(BH 校正后):")
|
|
692
|
+
for name, p, sig, threshold in corrected:
|
|
693
|
+
flag = "✅" if sig else "→"
|
|
694
|
+
print(f" {name}: p={p:.4f}, BH阈值={threshold:.4f} {flag}")
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
> 仅在分层 ≥ 3 组时触发校正。校正后不显著的组别结论应从「确定」降级为「趋势」,避免过度解读。
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## L3:细粒度拆分
|
|
702
|
+
|
|
703
|
+
### 按评估者
|
|
704
|
+
|
|
705
|
+
```python
|
|
706
|
+
by_ev = defaultdict(list)
|
|
707
|
+
for r in all_records: by_ev[r['evaluator']].append(r)
|
|
708
|
+
|
|
709
|
+
for ev, recs in sorted(by_ev.items()):
|
|
710
|
+
n_ev = len(recs)
|
|
711
|
+
decisive_ev = [r for r in recs if r.get('winner') != 'similar']
|
|
712
|
+
n_dec = len(decisive_ev)
|
|
713
|
+
n_new_wins = sum(1 for r in decisive_ev if r.get('winner') == V_NEW)
|
|
714
|
+
v_rate = n_new_wins / n_dec if n_dec > 0 else None
|
|
715
|
+
|
|
716
|
+
similar_ev = [r for r in recs if r.get('winner') == 'similar']
|
|
717
|
+
similar_rate = len(similar_ev) / n_ev
|
|
718
|
+
similar_bad = sum(1 for r in similar_ev if r.get('quality_rating') == 'below')
|
|
719
|
+
below_rate = sum(1 for r in decisive_ev if r.get('quality_rating') == 'below') / n_dec if n_dec > 0 else 0
|
|
720
|
+
|
|
721
|
+
print(f"{ev}: n={n_ev}, 新版胜率={v_rate:.0%}, similar率={similar_rate:.0%}, "
|
|
722
|
+
f"similar中都不好={similar_bad}, 低于预期率={below_rate:.0%}")
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
> **similar 率高**(≥30%)的评估者,倾向于认为两版本差不多;再看 `similar + below` 的数量区分"都好"还是"都不好"。
|
|
726
|
+
> **低于预期率高**的评估者,绝对质量标准更严苛。
|
|
727
|
+
> **新版胜率差异大**(>30% 的差距)的评估者之间,需检查评估标准是否一致。
|
|
728
|
+
|
|
729
|
+
### 按题目
|
|
730
|
+
|
|
731
|
+
```python
|
|
732
|
+
by_q = defaultdict(list)
|
|
733
|
+
for r in all_records: by_q[r['query_id']].append(r)
|
|
734
|
+
|
|
735
|
+
q_stats = {}
|
|
736
|
+
for qid, recs in sorted(by_q.items()):
|
|
737
|
+
n_ev = len(recs)
|
|
738
|
+
n_new = sum(1 for r in recs if r.get('winner') == V_NEW)
|
|
739
|
+
n_old = sum(1 for r in recs if r.get('winner') == V_OLD)
|
|
740
|
+
n_sim = sum(1 for r in recs if r.get('winner') == 'similar')
|
|
741
|
+
n_sim_bad = sum(1 for r in recs if r.get('winner') == 'similar' and r.get('quality_rating') == 'below')
|
|
742
|
+
majority = V_NEW if n_new > n_old else (V_OLD if n_old > n_new else 'split')
|
|
743
|
+
consensus = max(n_new, n_old) / n_ev if n_ev else 0
|
|
744
|
+
qc = Counter(r.get('quality_rating', '') for r in recs if r.get('quality_rating'))
|
|
745
|
+
q_stats[qid] = {
|
|
746
|
+
V_NEW: n_new, V_OLD: n_old, 'similar': n_sim, 'similar_bad': n_sim_bad,
|
|
747
|
+
'quality': dict(qc), 'n': n_ev, 'majority': majority, 'consensus': consensus,
|
|
748
|
+
}
|
|
749
|
+
print(f"{qid}: 新={n_new} 旧={n_old} similar={n_sim}(其中都不好={n_sim_bad}) → {majority} ({consensus:.0%})")
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
重点关注:
|
|
753
|
+
- **高共识新版胜**(consensus ≥ 60%)→ 新版真正有提升的案例
|
|
754
|
+
- **高共识旧版胜**(consensus ≥ 50%)→ 新版退步,需 bad case 分析
|
|
755
|
+
- **`similar + below` 占比高**(在 n≥3 的题目中 ≥ 50%)→ 该 query 两个版本都不好
|
|
756
|
+
- **低于预期集中的题目** → 揭示系统性质量问题
|
|
757
|
+
|
|
758
|
+
### winner × quality_rating 交叉分析
|
|
759
|
+
|
|
760
|
+
```python
|
|
761
|
+
cross = Counter((r.get('winner', ''), r.get('quality_rating', '')) for r in all_records)
|
|
762
|
+
print("\nwinner × quality_rating 交叉表:")
|
|
763
|
+
for w in [V_NEW, V_OLD, 'similar']:
|
|
764
|
+
for qr in ['exceeds', 'meets', 'below']:
|
|
765
|
+
cnt = cross.get((w, qr), 0)
|
|
766
|
+
if cnt:
|
|
767
|
+
print(f" winner={w:15s} + {qr:8s}: {cnt}")
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
> 交叉分析揭示重要组合:
|
|
771
|
+
> - `winner=V_new + below`:新版虽然相对更好但绝对质量仍不达标("矮子里拔高个")
|
|
772
|
+
> - `winner=V_new + exceeds`:新版胜出且质量很好——最强的提升信号
|
|
773
|
+
> - `winner=V_old + meets/exceeds`:旧版更好且质量尚可——新版退步的 bad case
|
|
774
|
+
> - `winner=similar + below`:两版都不好——共同系统性缺陷
|
|
775
|
+
|
|
776
|
+
### 评估者间一致性
|
|
777
|
+
|
|
778
|
+
```python
|
|
779
|
+
shared_qs = [qid for qid, recs in by_q.items() if len(recs) >= 2]
|
|
780
|
+
|
|
781
|
+
all_pairs, dec_pairs = [], []
|
|
782
|
+
for qid in shared_qs:
|
|
783
|
+
winners = [r.get('winner', '') for r in by_q[qid]]
|
|
784
|
+
pairs = [(winners[i], winners[j]) for i in range(len(winners)) for j in range(i+1, len(winners))]
|
|
785
|
+
all_pairs.extend([1 if a == b else 0 for a, b in pairs])
|
|
786
|
+
dec = [w for w in winners if w != 'similar']
|
|
787
|
+
if len(dec) >= 2:
|
|
788
|
+
dpairs = [(dec[i], dec[j]) for i in range(len(dec)) for j in range(i+1, len(dec))]
|
|
789
|
+
dec_pairs.extend([1 if a == b else 0 for a, b in dpairs])
|
|
790
|
+
|
|
791
|
+
print(f"全判定一致率: {np.mean(all_pairs):.1%}(含 similar 的分歧)")
|
|
792
|
+
print(f"决定性判定一致率: {np.mean(dec_pairs):.1%}(有明确偏好时的方向一致率)")
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
> 全判定一致率通常较低(30-40%),因为 similar 选择分散。
|
|
796
|
+
> 决定性判定一致率是更有意义的指标,60% 以上说明方向判断有基本共识。
|
|
797
|
+
|
|
798
|
+
### 按题目类型 / 难度分层(Stratified Analysis)
|
|
799
|
+
|
|
800
|
+
若每道题有类型标签(如商品知识、商品对比、值不值得买)或难度标签,应做分层胜率分析。分层结论比总胜率更有指导意义——不同题型的表现差异可能指向模型在特定能力上的强弱。
|
|
801
|
+
|
|
802
|
+
```python
|
|
803
|
+
# 假设题目元数据来自 _config.json 或外部标注文件
|
|
804
|
+
# question_meta[qid] = {"type": "值不值得买", "difficulty": "hard", ...}
|
|
805
|
+
# 实际使用时按数据来源读取
|
|
806
|
+
|
|
807
|
+
# 方法1:若元数据来自 _config.json
|
|
808
|
+
import json
|
|
809
|
+
config_path = os.path.join(RESULTS_DIR, '_config.json')
|
|
810
|
+
question_meta = {}
|
|
811
|
+
if os.path.exists(config_path):
|
|
812
|
+
with open(config_path) as f:
|
|
813
|
+
config = json.load(f)
|
|
814
|
+
# 假设 config["question_types"] = {"Q_0001": "商品知识", ...}
|
|
815
|
+
question_meta = config.get('question_types', {})
|
|
816
|
+
|
|
817
|
+
# 方法2:若题目 ID 中包含类型信息(如 Q_type_qid),从 ID 中解析
|
|
818
|
+
# 或由 Agent 读取题目原文件后人工标注类型
|
|
819
|
+
|
|
820
|
+
# 按类型分组统计
|
|
821
|
+
type_groups = defaultdict(list)
|
|
822
|
+
for qid, qs in question_scores.items():
|
|
823
|
+
qtype = question_meta.get(qid, '未分类')
|
|
824
|
+
type_groups[qtype].append(qid)
|
|
825
|
+
|
|
826
|
+
print("\n=== 按题目类型分层 ===")
|
|
827
|
+
type_stats = []
|
|
828
|
+
for qtype, qids in sorted(type_groups.items(), key=lambda x: -len(x[1])):
|
|
829
|
+
type_scores = [question_scores[qid]['score'] for qid in qids]
|
|
830
|
+
type_focus = sum(1 for qid in qids if question_scores[qid]['verdict'] == V_NEW)
|
|
831
|
+
type_base = sum(1 for qid in qids if question_scores[qid]['verdict'] == V_OLD)
|
|
832
|
+
type_tie = sum(1 for qid in qids if question_scores[qid]['verdict'] == 'tie')
|
|
833
|
+
n_dec_type = type_focus + type_base
|
|
834
|
+
|
|
835
|
+
# 对该类型做统计检验
|
|
836
|
+
if n_dec_type >= 5:
|
|
837
|
+
type_p = stats.binomtest(type_focus, n=n_dec_type, p=0.5, alternative='two-sided').pvalue
|
|
838
|
+
type_ci = stats.binomtest(type_focus, n=n_dec_type, p=0.5).proportion_ci(0.95, 'wilson')
|
|
839
|
+
type_win_rate = type_focus / n_dec_type
|
|
840
|
+
type_h = cohens_h(type_win_rate, 0.5)
|
|
841
|
+
hlevel = 'Large' if abs(type_h) > 0.8 else ('Medium' if abs(type_h) > 0.5 else ('Small' if abs(type_h) > 0.2 else 'Negligible'))
|
|
842
|
+
else:
|
|
843
|
+
type_p = None
|
|
844
|
+
type_ci = None
|
|
845
|
+
type_win_rate = n_dec_type / n_dec_type if n_dec_type > 0 else None
|
|
846
|
+
type_h = None
|
|
847
|
+
hlevel = None
|
|
848
|
+
|
|
849
|
+
# below 分析:该类型中 below 的题目
|
|
850
|
+
type_below_questions = set()
|
|
851
|
+
for qid in qids:
|
|
852
|
+
for r in by_q.get(qid, []):
|
|
853
|
+
if r.get('quality_rating') == 'below':
|
|
854
|
+
type_below_questions.add(qid)
|
|
855
|
+
|
|
856
|
+
stat = {
|
|
857
|
+
'type': qtype,
|
|
858
|
+
'n': len(qids),
|
|
859
|
+
'score_mean': np.mean(type_scores),
|
|
860
|
+
'focus_wins': type_focus,
|
|
861
|
+
'baseline_wins': type_base,
|
|
862
|
+
'ties': type_tie,
|
|
863
|
+
'focus_rate': round(type_win_rate, 3) if type_win_rate else None,
|
|
864
|
+
'pvalue': round(type_p, 4) if type_p else None,
|
|
865
|
+
'ci_low': round(type_ci.low, 3) if type_ci else None,
|
|
866
|
+
'ci_high': round(type_ci.high, 3) if type_ci else None,
|
|
867
|
+
'cohens_h': round(type_h, 3) if type_h else None,
|
|
868
|
+
'h_level': hlevel,
|
|
869
|
+
'below_questions': len(type_below_questions),
|
|
870
|
+
'low_sample': n_dec_type < 5, # 标记小样本
|
|
871
|
+
}
|
|
872
|
+
type_stats.append(stat)
|
|
873
|
+
|
|
874
|
+
sample_warning = " ⚠️ 样本不足" if stat['low_sample'] else ""
|
|
875
|
+
h_str = f"h={type_h:+.3f}" if type_h else "h=N/A"
|
|
876
|
+
print(f"{qtype}: n={len(qids)}, 得分均值={np.mean(type_scores):+.2f}, "
|
|
877
|
+
f"胜率={stat['focus_rate']}, {h_str}, p={type_p}{sample_warning}")
|
|
878
|
+
|
|
879
|
+
# 对分层做多重比较校正(≥ 3 层且有 p 值)
|
|
880
|
+
type_pvalues = [(s['type'], s['pvalue']) for s in type_stats if s['pvalue'] is not None]
|
|
881
|
+
if len(type_pvalues) >= 3:
|
|
882
|
+
corrected = benjamini_hochberg(type_pvalues)
|
|
883
|
+
print("\n分层 BH 校正后:")
|
|
884
|
+
for name, p, sig, threshold in corrected:
|
|
885
|
+
flag = "✅ 显著" if sig else "→ 不显著"
|
|
886
|
+
print(f" {name}: p={p:.4f}, BH阈值={threshold:.4f} {flag}")
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
> **分层报告原则**:
|
|
890
|
+
> - 每层必须带效应量和 CI,不可仅报告胜率。
|
|
891
|
+
> - 样本量 < 5 的分层标注"样本不足,仅供趋势参考"。
|
|
892
|
+
> - ≥ 3 层时必须做 BH 校正。
|
|
893
|
+
> - 若某题型新版显著胜出(p < 0.05 且 |h| > 0.5),这是最强的提升信号。
|
|
894
|
+
> - 若某题型新版显著退步,这是高优修复目标。
|
|
895
|
+
> - 分层结果应作为报告第 3 节"核心结果 + 题型拆分"的数据源。
|
|
896
|
+
|
|
897
|
+
### 按难度 / 其他维度分层
|
|
898
|
+
|
|
899
|
+
若有难度标签,同理可做按难度的分层。代码结构相同,替换分组 key 即可:
|
|
900
|
+
|
|
901
|
+
```python
|
|
902
|
+
# 按难度分层
|
|
903
|
+
difficulty_groups = defaultdict(list)
|
|
904
|
+
for qid, qs in question_scores.items():
|
|
905
|
+
diff = question_meta.get(qid, {}).get('difficulty', 'normal')
|
|
906
|
+
difficulty_groups[diff].append(qid)
|
|
907
|
+
|
|
908
|
+
for diff, qids in sorted(difficulty_groups.items()):
|
|
909
|
+
# ... 同上分层统计逻辑
|
|
910
|
+
pass
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
分层维度建议(按数据可用性选择):
|
|
914
|
+
- **题目类型**(最常用):商品知识 / 商品对比 / 值不值得买 / 商品推荐 / 多轮对话 / ...
|
|
915
|
+
- **难度**:简单 / 中等 / 困难
|
|
916
|
+
- **是否多轮**:单轮 / 多轮
|
|
917
|
+
- **领域/行业**:家电 / 数码 / 服装 / 美妆 / 食品 / ...
|
|
918
|
+
|
|
919
|
+
> 报告中选择 1-2 个最重要的分层维度展开,不宜过多。通常题目类型是必做维度。
|
|
920
|
+
|
|
921
|
+
---
|
|
922
|
+
|
|
923
|
+
从评论中归纳根因。评估格式提供 5 个评论字段,信息丰富。
|
|
924
|
+
|
|
925
|
+
### 分类标签
|
|
926
|
+
|
|
927
|
+
| 类别 | 对应判定 | 改进方向 |
|
|
928
|
+
|------|---------|---------|
|
|
929
|
+
| **新版取胜根因** | winner=V_new(显著+略好) | 保持并加强这些优势 |
|
|
930
|
+
| **新版退步根因** | winner=V_old(显著+略好) | 高优修复 |
|
|
931
|
+
| **都不好根因** | winner="similar" + quality_rating="below" | 两版共同问题 |
|
|
932
|
+
| **低于预期根因** | quality_rating="below"(含有明确偏好的场次) | 虽然相对更好但绝对质量不达标,需策略层改进 |
|
|
933
|
+
|
|
934
|
+
### 根因归纳方法
|
|
935
|
+
|
|
936
|
+
L4 是定性分析,没有固定代码——需要阅读每条有评论的记录,从中提炼模式。步骤:
|
|
937
|
+
|
|
938
|
+
**第一步:提取所有非空评论**
|
|
939
|
+
|
|
940
|
+
```python
|
|
941
|
+
def collect_comments(r):
|
|
942
|
+
"""汇总一条记录的所有评论(v2 格式:按版本名直接读取)"""
|
|
943
|
+
parts = []
|
|
944
|
+
comments = r.get('comments', {})
|
|
945
|
+
for version_name, c in comments.items():
|
|
946
|
+
if version_name == 'general':
|
|
947
|
+
if c and str(c).strip():
|
|
948
|
+
parts.append(f" 共同评价: {c}")
|
|
949
|
+
elif isinstance(c, dict):
|
|
950
|
+
pros = c.get('pros', '').strip()
|
|
951
|
+
cons = c.get('cons', '').strip()
|
|
952
|
+
if pros: parts.append(f" {version_name} 优点: {pros}")
|
|
953
|
+
if cons: parts.append(f" {version_name} 缺点: {cons}")
|
|
954
|
+
return '\n'.join(parts)
|
|
955
|
+
|
|
956
|
+
for r in all_records:
|
|
957
|
+
comments = collect_comments(r)
|
|
958
|
+
if comments:
|
|
959
|
+
winner = r.get('winner', '')
|
|
960
|
+
magnitude = r.get('magnitude', '')
|
|
961
|
+
qr = r.get('quality_rating', '')
|
|
962
|
+
qr_label = {'exceeds': '🌟超出预期', 'meets': '✅符合预期', 'below': '⚠️低于预期'}.get(qr, '')
|
|
963
|
+
label = f"similar({qr_label})" if winner == 'similar' else f"{winner}胜({magnitude}) {qr_label}"
|
|
964
|
+
print(f"[{label}] {r['evaluator']} / {r['query_id']}:")
|
|
965
|
+
print(comments)
|
|
966
|
+
print()
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
**第二步:阅读评论,按 4 类归纳模式**
|
|
970
|
+
|
|
971
|
+
对每类(Vnew 赢 / Vold 赢 / both_bad / 低于预期):
|
|
972
|
+
- 找出被多人提及的共同批评或赞扬(出现 ≥2 次的点优先)
|
|
973
|
+
- 归纳为一句话标签(如"新版使用了用户无法理解的术语")
|
|
974
|
+
- 摘录 1-2 条最有代表性的原话作为佐证
|
|
975
|
+
- 区分"显著好"和"略好"的评论——显著好的评论通常揭示更核心的差异
|
|
976
|
+
|
|
977
|
+
**第三步:识别任务相关的根因维度**
|
|
978
|
+
|
|
979
|
+
不同类型的 GSB 任务,根因会有所不同,但通常围绕以下几个通用维度展开:
|
|
980
|
+
|
|
981
|
+
| 维度 | 新版可能的优势 | 新版可能的退步 |
|
|
982
|
+
|------|--------------|--------------|
|
|
983
|
+
| **准确性** | 旧版存在幻觉/事实错误 | 新版引入新的错误 |
|
|
984
|
+
| **信息完整性** | 新版覆盖更多关键维度 | 新版遗漏重要信息 |
|
|
985
|
+
| **语言质量** | 表达更流畅/口语化 | 用词更复杂/生硬 |
|
|
986
|
+
| **相关性** | 更精准回应用户意图 | 答非所问或偏题 |
|
|
987
|
+
| **冗余度** | 去除了无效内容 | 引入了新的冗余 |
|
|
988
|
+
| **格式/结构** | 组织更清晰 | 结构混乱或过长 |
|
|
989
|
+
|
|
990
|
+
`similar + below`(都不好)和低于预期案例的常见共性根因(跨任务通用):
|
|
991
|
+
- **任务本身超出合理范围**:两个版本都没有识别出这是一个不该直接回答/需要拒绝的 case
|
|
992
|
+
- **两版本共同遗漏了核心信息**:在这个维度上版本差异无意义
|
|
993
|
+
- **领域知识缺失**:两版都不了解该领域的关键约束(如法规、时效性等)
|
|
994
|
+
|
|
995
|
+
### L4 补充:自然语言定性反馈的结构化分析
|
|
996
|
+
|
|
997
|
+
定量统计(胜率、p 值、效应量)告诉我们"谁赢了多少",但评估者写下的自然语言 comment 告诉我们"为什么这么判断"。定性与定量结合才能形成完整的分析。
|
|
998
|
+
|
|
999
|
+
#### 反馈分析方法论
|
|
1000
|
+
|
|
1001
|
+
```
|
|
1002
|
+
定量统计(L1-L3) 定性反馈(L4)
|
|
1003
|
+
↓ ↓
|
|
1004
|
+
胜率 + CI + 效应量 ←→ comment 归纳的模式
|
|
1005
|
+
↓ ↓
|
|
1006
|
+
数字告诉"谁赢" 评论告诉"为什么赢/输"
|
|
1007
|
+
↓ ↓
|
|
1008
|
+
综合 → 结论 + 行动建议
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
#### 结构化 comment 提取
|
|
1012
|
+
|
|
1013
|
+
```python
|
|
1014
|
+
def extract_structured_comments(all_records, V_NEW, V_OLD):
|
|
1015
|
+
"""将所有评估者的 comment 按判定类型归类,输出结构化反馈表。"""
|
|
1016
|
+
structured = {
|
|
1017
|
+
'vnew_wins': [], # V_NEW 胜出场次的 comment
|
|
1018
|
+
'vold_wins': [], # V_OLD 胜出场次的 comment
|
|
1019
|
+
'both_bad': [], # similar + below 的 comment
|
|
1020
|
+
'vnew_but_below': [], # V_NEW 胜但质量 below 的 comment
|
|
1021
|
+
'exceeds': [], # 质量 exceeds 的 comment(亮点)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
for r in all_records:
|
|
1025
|
+
entry = {
|
|
1026
|
+
'query_id': r['query_id'],
|
|
1027
|
+
'evaluator': r['evaluator'],
|
|
1028
|
+
'winner': r.get('winner', ''),
|
|
1029
|
+
'magnitude': r.get('magnitude', ''),
|
|
1030
|
+
'quality_rating': r.get('quality_rating', ''),
|
|
1031
|
+
'pros': {},
|
|
1032
|
+
'cons': {},
|
|
1033
|
+
'general': '',
|
|
1034
|
+
}
|
|
1035
|
+
comments = r.get('comments', {})
|
|
1036
|
+
for version_name, c in comments.items():
|
|
1037
|
+
if version_name == 'general':
|
|
1038
|
+
entry['general'] = str(c).strip() if c else ''
|
|
1039
|
+
elif isinstance(c, dict):
|
|
1040
|
+
entry['pros'][version_name] = c.get('pros', '').strip()
|
|
1041
|
+
entry['cons'][version_name] = c.get('cons', '').strip()
|
|
1042
|
+
|
|
1043
|
+
w = r.get('winner', '')
|
|
1044
|
+
qr = r.get('quality_rating', '')
|
|
1045
|
+
if w == V_NEW and qr == 'below':
|
|
1046
|
+
structured['vnew_but_below'].append(entry)
|
|
1047
|
+
elif w == V_NEW:
|
|
1048
|
+
structured['vnew_wins'].append(entry)
|
|
1049
|
+
elif w == V_OLD:
|
|
1050
|
+
structured['vold_wins'].append(entry)
|
|
1051
|
+
elif w == 'similar' and qr == 'below':
|
|
1052
|
+
structured['both_bad'].append(entry)
|
|
1053
|
+
if qr == 'exceeds':
|
|
1054
|
+
structured['exceeds'].append(entry)
|
|
1055
|
+
|
|
1056
|
+
return structured
|
|
1057
|
+
|
|
1058
|
+
feedback = extract_structured_comments(all_records, V_NEW, V_OLD)
|
|
1059
|
+
for cat, entries in feedback.items():
|
|
1060
|
+
print(f"\n{cat}: {len(entries)} 条")
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
#### Comment 聚合与模式归纳
|
|
1064
|
+
|
|
1065
|
+
不要罗列所有原始 comment。按以下步骤聚合:
|
|
1066
|
+
|
|
1067
|
+
**第 1 步:按主题聚类**
|
|
1068
|
+
阅读每个类别(V_NEW 赢 / V_OLD 赢 / both_bad / below)中的 comment,识别 ≥ 2 次出现的共同赞扬或批评。例如:
|
|
1069
|
+
|
|
1070
|
+
```
|
|
1071
|
+
V_NEW 赢的 comment 中反复出现的正向点:
|
|
1072
|
+
- "回答更简洁,直接给结论"(出现 8 次)
|
|
1073
|
+
- "商品推荐更贴合用户场景"(出现 5 次)
|
|
1074
|
+
- "表格对比让选择更清晰"(出现 4 次)
|
|
1075
|
+
|
|
1076
|
+
V_OLD 赢的 comment 中反复出现的负向点:
|
|
1077
|
+
- "新版本没有先分析用户需求就直接推荐"(出现 6 次)
|
|
1078
|
+
- "遗漏了关键约束条件"(出现 4 次)
|
|
1079
|
+
- "商品卡不相关或过时"(出现 3 次)
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
**第 2 步:提炼问题簇 / 能力簇**
|
|
1083
|
+
将同主题合并为可命名的问题簇。问题簇命名要具体、可指导改进:
|
|
1084
|
+
|
|
1085
|
+
| 推荐命名 | 不推荐命名 |
|
|
1086
|
+
|---------|-----------|
|
|
1087
|
+
| "决策题没有真正接住用户纠结" | "用户意图与场景" |
|
|
1088
|
+
| "对比题缺少稳定的差异框架" | "信息完整性" |
|
|
1089
|
+
| "商品推荐和约束匹配不稳定" | "准确性" |
|
|
1090
|
+
| "高风险场景信息深度不足" | "其他问题" |
|
|
1091
|
+
|
|
1092
|
+
**第 3 步:摘录代表性原话**
|
|
1093
|
+
每个问题簇摘录 1-2 条最具有代表性的评估者原话作为佐证。原话摘录原则:
|
|
1094
|
+
- 保留能直接看出问题的片段
|
|
1095
|
+
- 标注评估者和题目 ID
|
|
1096
|
+
- 合并同义 comment
|
|
1097
|
+
|
|
1098
|
+
```python
|
|
1099
|
+
# 辅助:按关键词搜索 comment
|
|
1100
|
+
def search_comments(feedback_category, keywords):
|
|
1101
|
+
"""在指定类别的 feedback 中搜索包含关键词的 comment。"""
|
|
1102
|
+
matches = []
|
|
1103
|
+
for entry in feedback_category:
|
|
1104
|
+
text = entry['general'] + ' '.join(entry['cons'].values()) + ' '.join(entry['pros'].values())
|
|
1105
|
+
if any(kw in text for kw in keywords):
|
|
1106
|
+
matches.append(entry)
|
|
1107
|
+
return matches
|
|
1108
|
+
|
|
1109
|
+
# 示例:找出 V_OLD 胜出场次中提到"需求理解"的 comment
|
|
1110
|
+
need_understanding = search_comments(feedback['vold_wins'], ['需求', '意图', '理解'])
|
|
1111
|
+
for e in need_understanding[:5]:
|
|
1112
|
+
print(f" {e['evaluator']} / {e['query_id']}: {e['general'][:120]}...")
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
#### 定性反馈在报告中的呈现
|
|
1116
|
+
|
|
1117
|
+
在 HTML 报告中,定性与定量应交织呈现而非割裂:
|
|
1118
|
+
|
|
1119
|
+
```
|
|
1120
|
+
第 7 节「根因分析」的推荐结构:
|
|
1121
|
+
|
|
1122
|
+
定量骨架 定性血肉
|
|
1123
|
+
───────── ─────────
|
|
1124
|
+
新版胜率 58% ←→ "回答更简洁直接"(8位评估者提及)
|
|
1125
|
+
效应量 h=+0.32 ←→ "表格对比让选择更清晰"(5位)
|
|
1126
|
+
p=0.012 ←→ "商品推荐贴合场景"(4位)
|
|
1127
|
+
|
|
1128
|
+
新版退步 25% ←→ "没有先分析需求就推荐"(6位)
|
|
1129
|
+
旧版胜率高共识题 ←→ 具体 case 的 comment 原文
|
|
1130
|
+
←→ "遗漏关键约束条件"(4位)
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
> 详细的 case 分析报告写作规范见 [`references/case-analysis-report.md`](references/case-analysis-report.md),其中包括问题簇命名、case 选择原则、双版本对比展示、comment 使用规范和可执行改进动作的写法。
|
|
1134
|
+
|
|
1135
|
+
---
|
|
1136
|
+
|
|
1137
|
+
锚点题最有战略价值的用法——用评估者在锚点题上的判定模式做聚类,识别系统性偏好分歧。
|
|
1138
|
+
|
|
1139
|
+
**适用条件**:锚点题 ≥ 3 道,且评估者 ≥ 6 人。
|
|
1140
|
+
|
|
1141
|
+
```python
|
|
1142
|
+
from sklearn.cluster import KMeans
|
|
1143
|
+
from sklearn.preprocessing import StandardScaler
|
|
1144
|
+
import numpy as np
|
|
1145
|
+
|
|
1146
|
+
# 将每位评估者在所有题目上的判定编码为特征向量
|
|
1147
|
+
all_qids = sorted(by_q.keys())
|
|
1148
|
+
|
|
1149
|
+
def encode_side(s):
|
|
1150
|
+
return +1 if s == 'Vnew_wins' else (-1 if s == 'Vold_wins' else 0)
|
|
1151
|
+
|
|
1152
|
+
ev_feature_matrix = {}
|
|
1153
|
+
for ev, recs in by_ev.items():
|
|
1154
|
+
by_qid = {r['query_id']: r for r in recs}
|
|
1155
|
+
vec = [encode_side(by_qid[q]['norm_side']) if q in by_qid else 0 for q in all_qids]
|
|
1156
|
+
ev_feature_matrix[ev] = vec
|
|
1157
|
+
|
|
1158
|
+
ev_names = list(ev_feature_matrix.keys())
|
|
1159
|
+
X = np.array([ev_feature_matrix[ev] for ev in ev_names])
|
|
1160
|
+
|
|
1161
|
+
# K-Means 聚类(K=2 适合多数场景;可尝试 K=3)
|
|
1162
|
+
K = 2
|
|
1163
|
+
km = KMeans(n_clusters=K, random_state=42, n_init=10)
|
|
1164
|
+
labels = km.fit_predict(X)
|
|
1165
|
+
|
|
1166
|
+
# 输出分群结果
|
|
1167
|
+
for k in range(K):
|
|
1168
|
+
group_evs = [ev_names[i] for i, l in enumerate(labels) if l == k]
|
|
1169
|
+
group_recs = [r for r in all_records if r['evaluator'] in group_evs]
|
|
1170
|
+
decisive = [r for r in group_recs if r['norm_side'] in ('Vnew_wins', 'Vold_wins')]
|
|
1171
|
+
n_new = sum(1 for r in decisive if r['norm_side'] == 'Vnew_wins')
|
|
1172
|
+
rate = n_new / len(decisive) if decisive else 0
|
|
1173
|
+
print(f"群体 {k+1}({len(group_evs)} 人): {group_evs}")
|
|
1174
|
+
print(f" 新版胜率 = {rate:.1%}(决定性场次 {len(decisive)} 条)")
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
**报告分群结论时的建议格式**:
|
|
1178
|
+
|
|
1179
|
+
```
|
|
1180
|
+
群体 A(n=4):倾向于重视准确性,新版胜率 72%
|
|
1181
|
+
群体 B(n=3):倾向于重视表达流畅性,新版胜率 41%
|
|
1182
|
+
|
|
1183
|
+
→ 若目标用户更接近群体 A,新版表现显著更好;
|
|
1184
|
+
若目标用户更接近群体 B,新版可能不如旧版。
|
|
1185
|
+
建议对照目标用户画像选择参考哪一组结论。
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
与其报告一个被平均掉的总胜率,不如明确偏好分歧——这对产品决策更有指导意义。
|
|
1189
|
+
|
|
1190
|
+
---
|
|
1191
|
+
|
|
1192
|
+
## HTML 报告生成
|
|
1193
|
+
|
|
1194
|
+
### 报告结构(推荐)
|
|
1195
|
+
|
|
1196
|
+
```
|
|
1197
|
+
0. 元数据页眉:任务名称、对比版本、参与人数、评估时间、报告生成时间(方便回溯)
|
|
1198
|
+
1. 页眉 + 6个核心指标卡(新版胜率 / p值 / 效应量Cohen's h / both_bad率 / 评估者一致率 / 平均Kappa)
|
|
1199
|
+
2. 统计显著性:CI 可视化(全量 vs 高连贯组)+ Bootstrap 分布直方图 + 6类判定分布卡
|
|
1200
|
+
3. 总体分布:verdict donut chart(6类)+ 决定性场次 bar chart(区分显著/略好)
|
|
1201
|
+
4. 胜出方质量分布:超出预期/符合预期/低于预期 三档 + 判定×质量交叉表
|
|
1202
|
+
5. 评估者画像:stacked bar chart + 详细表格(含连贯性得分 + Kappa配对矩阵 + both_bad率 + 质量评价分布)
|
|
1203
|
+
6. 题目粒度:card grid(按多数意见着色 + 共识度标注)
|
|
1204
|
+
7. 根因分析:4列卡片(新版赢 / 新版输 / both_bad / 低于预期)+ 评论引用
|
|
1205
|
+
8. 评估者聚类(若适用):分群胜率 + 偏好解读
|
|
1206
|
+
9. 信度与偏倚说明:评分者间信度(Cohen's Kappa / Fleiss' Kappa)+ 位置偏倚检测结果
|
|
1207
|
+
10. 结论与行动建议:优先级列表(高优修复退步 > 中优系统性改进 > 低优巩固优势)
|
|
1208
|
+
11. 局限性说明:样本量不足的分层、低信度维度、已知偏倚
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
### 技术选型
|
|
1212
|
+
|
|
1213
|
+
- 纯 HTML + Chart.js(CDN),无构建步骤,单文件可直接分发或邮件附件
|
|
1214
|
+
- 所有数据内联到 `<script>` 中,Chart.js 加载成功后离线可用
|
|
1215
|
+
- Agent 直接生成完整 HTML 文件,**不依赖预置模板**(每次任务数据结构不同,按需生成)
|
|
1216
|
+
|
|
1217
|
+
### 数据对象约定
|
|
1218
|
+
|
|
1219
|
+
Agent 生成 HTML 前,先在 Python 中整理好数据对象,再内联到 `<script>` 中:
|
|
1220
|
+
|
|
1221
|
+
```python
|
|
1222
|
+
from datetime import datetime
|
|
1223
|
+
|
|
1224
|
+
report_data = {
|
|
1225
|
+
"meta": {
|
|
1226
|
+
"title": f"{V_OLD} vs {V_NEW} 评估报告", # ← 按实际版本语义填写
|
|
1227
|
+
"v_new": V_NEW,
|
|
1228
|
+
"v_old": V_OLD,
|
|
1229
|
+
"total": total,
|
|
1230
|
+
"n_evaluators": len(by_ev),
|
|
1231
|
+
"n_questions": len(by_q),
|
|
1232
|
+
# ⚠️ 必填元数据(每次报告必须记录,方便回溯)
|
|
1233
|
+
"task_name": "", # 评估任务名称(如 "P2-RL0420 vs P2-RL0413 上线前评估")
|
|
1234
|
+
"evaluator_names": list(by_ev.keys()), # 参与评估者名单
|
|
1235
|
+
"evaluation_period": { # 评估进行的时间范围
|
|
1236
|
+
"start": "", # 最早 timestamp
|
|
1237
|
+
"end": "", # 最晚 timestamp
|
|
1238
|
+
},
|
|
1239
|
+
"report_generated_at": datetime.now().isoformat(), # 报告生成时间
|
|
1240
|
+
"analysis_version": "2.0", # 分析方法版本号
|
|
1241
|
+
},
|
|
1242
|
+
"winner_counts": dict(winner_counts), # {V_NEW: n, V_OLD: n, "similar": n}
|
|
1243
|
+
"magnitude_counts": dict(magnitude_counts), # {much_better, slightly_better, similar}
|
|
1244
|
+
"quality_counts": dict(quality_counts), # {exceeds, meets, below}
|
|
1245
|
+
"similar_breakdown": {"good": similar_good, "bad": similar_bad},
|
|
1246
|
+
"decisive": {
|
|
1247
|
+
"n": n,
|
|
1248
|
+
"n_new": n_new,
|
|
1249
|
+
"n_new_much": n_new_much,
|
|
1250
|
+
"n_new_slight": n_new_slight,
|
|
1251
|
+
"rate": round(n_new / n, 3) if n else 0,
|
|
1252
|
+
},
|
|
1253
|
+
"significance": {
|
|
1254
|
+
"pvalue": round(binom.pvalue, 4),
|
|
1255
|
+
"ci_low": round(ci.low, 3),
|
|
1256
|
+
"ci_high": round(ci.high, 3),
|
|
1257
|
+
"boot_ci_low": round(boot_ci[0], 3),
|
|
1258
|
+
"boot_ci_high": round(boot_ci[1], 3),
|
|
1259
|
+
# 新增:效应量与 McNemar
|
|
1260
|
+
"cohens_h": round(h, 3),
|
|
1261
|
+
"cohens_h_level": h_level,
|
|
1262
|
+
"win_rate_delta": round(p_new - p_old, 3), # 胜率差值
|
|
1263
|
+
"mcnemar_chi2": round(chi2, 3) if chi2 else None,
|
|
1264
|
+
"mcnemar_pvalue": round(p_mc, 4) if chi2 else None,
|
|
1265
|
+
},
|
|
1266
|
+
"reliability": { # 新增:评分者间信度
|
|
1267
|
+
"mean_cohens_kappa": round(mean_kappa, 3) if not np.isnan(mean_kappa) else None,
|
|
1268
|
+
"mean_decisive_kappa": round(mean_dec_kappa, 3) if dec_pairwise else None,
|
|
1269
|
+
"fleiss_kappa": round(fk, 3) if 'fk' in dir() else None,
|
|
1270
|
+
"kappa_level": ( # Landis-Koch 判读
|
|
1271
|
+
"Almost Perfect" if mean_kappa > 0.8 else
|
|
1272
|
+
"Substantial" if mean_kappa > 0.6 else
|
|
1273
|
+
"Moderate" if mean_kappa > 0.4 else
|
|
1274
|
+
"Fair" if mean_kappa > 0.2 else "Poor"
|
|
1275
|
+
) if not np.isnan(mean_kappa) else None,
|
|
1276
|
+
},
|
|
1277
|
+
"position_bias": { # 新增:位置偏倚检测
|
|
1278
|
+
"global_left_rate": round(global_left / global_n, 3) if global_n >= 10 else None,
|
|
1279
|
+
"global_pvalue": round(global_p, 4) if global_n >= 10 else None,
|
|
1280
|
+
"has_global_bias": global_p < 0.05 if global_n >= 10 else None,
|
|
1281
|
+
},
|
|
1282
|
+
"cross_table": { # winner × quality_rating 交叉计数
|
|
1283
|
+
f"{w}_{qr}": cross.get((w, qr), 0)
|
|
1284
|
+
for w in [V_NEW, V_OLD, 'similar']
|
|
1285
|
+
for qr in ['exceeds', 'meets', 'below']
|
|
1286
|
+
},
|
|
1287
|
+
"evaluators": { # name → 胜负计数 + similar率 + 质量分布
|
|
1288
|
+
ev: {
|
|
1289
|
+
"total": len(recs),
|
|
1290
|
+
"v_new": sum(1 for r in recs if r.get('winner') == V_NEW),
|
|
1291
|
+
"v_old": sum(1 for r in recs if r.get('winner') == V_OLD),
|
|
1292
|
+
"similar": sum(1 for r in recs if r.get('winner') == 'similar'),
|
|
1293
|
+
"similar_bad": sum(1 for r in recs if r.get('winner') == 'similar' and r.get('quality_rating') == 'below'),
|
|
1294
|
+
"quality": dict(Counter(r.get('quality_rating','') for r in recs if r.get('quality_rating'))),
|
|
1295
|
+
}
|
|
1296
|
+
for ev, recs in by_ev.items()
|
|
1297
|
+
},
|
|
1298
|
+
"questions": { # qid → 判定计数 + 质量分布 + 多数意见 + 共识度
|
|
1299
|
+
qid: s for qid, s in q_stats.items()
|
|
1300
|
+
},
|
|
1301
|
+
"agreement": {
|
|
1302
|
+
"all_winners": round(np.mean(all_pairs), 3),
|
|
1303
|
+
"decisive_only": round(np.mean(dec_pairs), 3),
|
|
1304
|
+
},
|
|
1305
|
+
}
|
|
1306
|
+
import json
|
|
1307
|
+
print(json.dumps(report_data, ensure_ascii=False, indent=2))
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
---
|
|
1311
|
+
|
|
1312
|
+
## 分析结论表述规范
|
|
1313
|
+
|
|
1314
|
+
输出结论时建议按以下结构组织,确保有分析深度而非只是数字展示:
|
|
1315
|
+
|
|
1316
|
+
```
|
|
1317
|
+
结论 1(核心问题):新版是否显著优于旧版?
|
|
1318
|
+
→ 给出胜率 + p值 + CI + 效应量(Cohen's h),明确说"显著"还是"不显著"
|
|
1319
|
+
→ 区分"显著好"和"略好"的比例,量化提升强度
|
|
1320
|
+
→ 若效应量 |h| < 0.2 但 p < 0.05,标注"统计显著但效应量微弱"
|
|
1321
|
+
|
|
1322
|
+
结论 2(质量达标):胜出方的绝对质量如何?
|
|
1323
|
+
→ 质量评价三档分布(超出预期/符合预期/低于预期)
|
|
1324
|
+
→ "Vnew_wins + below"说明相对更好但绝对不达标
|
|
1325
|
+
|
|
1326
|
+
结论 3(新版优势):新版在哪些维度/案例上赢?
|
|
1327
|
+
→ 列举高共识题目 + 根因标签 + 评论引用
|
|
1328
|
+
→ 区分显著好和略好的案例,显著好更值得分析
|
|
1329
|
+
|
|
1330
|
+
结论 4(新版退步):新版有哪些 bad case?
|
|
1331
|
+
→ 重点!列举 V_old 胜出的题目,分析退步根因
|
|
1332
|
+
|
|
1333
|
+
结论 5(系统性问题):similar+below 和低于预期标记揭示了什么?
|
|
1334
|
+
→ similar+below(都不好)说明两版共同的系统性不足
|
|
1335
|
+
|
|
1336
|
+
结论 6(置信度说明):评估者一致性和信度如何?结论可靠吗?
|
|
1337
|
+
→ Kappa 值和判读水平(Almost Perfect / Substantial / Moderate / Fair / Poor)
|
|
1338
|
+
→ McNemar 和 Binomial test 结论是否一致
|
|
1339
|
+
→ 低 Kappa 需说明原因(标准不统一?主观性强?)
|
|
1340
|
+
→ 位置偏倚是否影响结论
|
|
1341
|
+
|
|
1342
|
+
行动建议:按优先级排列(高优=修复退步 > 中优=系统性问题 > 低优=巩固优势)
|
|
1343
|
+
每项建议附带效应量参考——大效应量的提升/退步值得更高优先级
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
---
|
|
1347
|
+
|
|
1348
|
+
## 局限性说明模板
|
|
1349
|
+
|
|
1350
|
+
每份报告必须包含局限性说明,不粉饰不回避。模板如下:
|
|
1351
|
+
|
|
1352
|
+
```
|
|
1353
|
+
### 局限性说明
|
|
1354
|
+
|
|
1355
|
+
**样本量**:
|
|
1356
|
+
- 总评估记录 {total} 条,{n_questions} 道题目,{n_evaluators} 位评估者
|
|
1357
|
+
- [若 n_evaluators < 3] 仅 {n_evaluators} 人参与,结论仅作参考
|
|
1358
|
+
- [若某分层 n < 10] 分层"{name}"样本量不足,结论标注为"趋势"
|
|
1359
|
+
|
|
1360
|
+
**信度**:
|
|
1361
|
+
- 平均 Cohen's Kappa = {kappa}({level})
|
|
1362
|
+
- [若 Kappa < 0.4] 评估者间一致性较低,结论存在不确定性
|
|
1363
|
+
- [若 Fleming' Kappa < 0.4] 多人一致性不足,建议对齐评估标准后重新评估
|
|
1364
|
+
|
|
1365
|
+
**偏倚**:
|
|
1366
|
+
- [若存在位置偏倚] 检测到全局位置偏倚(p={p_val}),可能影响结论方向
|
|
1367
|
+
- [若低连贯评估者影响] 全量 vs 高连贯组胜率差异 {delta},[描述差异]
|
|
1368
|
+
|
|
1369
|
+
**覆盖度**:
|
|
1370
|
+
- 测试集覆盖 {n_domains} 个领域 / {n_types} 种题型
|
|
1371
|
+
- [未覆盖的场景] 本次评估未覆盖 [列举],结论不适用于这些场景
|
|
1372
|
+
- [测试集代表性] 测试集来源于 [来源],[说明是否代表真实用户分布]
|
|
1373
|
+
|
|
1374
|
+
**评估者代表性**:
|
|
1375
|
+
- 评估者背景:[描述评估者专业背景]
|
|
1376
|
+
- [若评估者同质化] 评估者来自同一团队,可能存在群体偏好偏倚
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
---
|
|
1380
|
+
|
|
1381
|
+
## 科学报告检查清单
|
|
1382
|
+
|
|
1383
|
+
生成每份 GSB 评估报告前,逐项确认:
|
|
1384
|
+
|
|
1385
|
+
### 必须包含(不可省略)
|
|
1386
|
+
|
|
1387
|
+
- [ ] **元数据完整**:任务名称、对比版本、参与人数与名单、评估时间范围、报告生成时间
|
|
1388
|
+
- [ ] **全量数据概览**:总记录数、题目数、评估者数、有效 vs 剔除记录
|
|
1389
|
+
- [ ] **GSB 分布**:新版胜(显著/略好)、旧版胜(显著/略好)、相似(都好/都不好)
|
|
1390
|
+
- [ ] **统计检验**:p 值 + 95% CI(Wilson + Bootstrap)+ 效应量(Cohen's h)+ McNemar 检验
|
|
1391
|
+
- [ ] **质量交叉分析**:winner × quality_rating 交叉表
|
|
1392
|
+
- [ ] **评分者间信度**:Cohen's Kappa(两两平均)+ Fleiss' Kappa(若多人评同题)
|
|
1393
|
+
- [ ] **灵敏度分析**:全量 vs 高连贯组对比
|
|
1394
|
+
- [ ] **位置偏倚检测**:全局位置偏倚 p 值
|
|
1395
|
+
- [ ] **结论与行动建议**:按优先级排序,带效应量参考
|
|
1396
|
+
- [ ] **局限性说明**:样本量、信度、偏倚、覆盖度
|
|
1397
|
+
|
|
1398
|
+
### 建议包含(提升完备性)
|
|
1399
|
+
|
|
1400
|
+
- [ ] **分层分析**:按题目类型/领域/难度的胜率 + CI(若 ≥ 3 层,做 BH 校正)
|
|
1401
|
+
- [ ] **Bootstrap 分布直方图**:展示胜率估计的不确定性形态
|
|
1402
|
+
- [ ] **Kappa 配对矩阵热图**:可视化评估者间信度结构
|
|
1403
|
+
- [ ] **评估者聚类**:若 ≥ 6 人 + ≥ 3 道锚点题,做偏好分群
|
|
1404
|
+
- [ ] **根因分类**:按 4 类归纳评论模式
|
|
1405
|
+
|
|
1406
|
+
### 不应出现的错误
|
|
1407
|
+
|
|
1408
|
+
- [ ] 未校正随机一致的"一致率"代替 Kappa
|
|
1409
|
+
- [ ] 仅报告 p 值不报告效应量和 CI
|
|
1410
|
+
- [ ] 分层检验未做多重比较校正
|
|
1411
|
+
- [ ] 小样本(n < 30)结论语气过于确定
|
|
1412
|
+
- [ ] 忽略评估者间系统性差异
|
|
1413
|
+
- [ ] 缺少元数据导致报告无法回溯
|
|
1414
|
+
|
|
1415
|
+
---
|
|
1416
|
+
|
|
1417
|
+
## 常见陷阱
|
|
1418
|
+
|
|
1419
|
+
| 陷阱 | 说明 |
|
|
1420
|
+
|------|------|
|
|
1421
|
+
| 把 `similar` 当单一类别统计 | `similar` 中 `quality_rating="below"` 是"都不好",`exceeds/meets` 是"都好",语义截然不同,必须分开 |
|
|
1422
|
+
| 只看判定不看质量评价 | 高"低于预期"率意味着虽然"新版更好"但绝对质量仍不达标 |
|
|
1423
|
+
| 忽略 winner × quality 交叉分析 | `winner=V_new + below` 是"矮子里拔高个",不能当作真正的提升 |
|
|
1424
|
+
| 错误读取评论字段 | v2 格式评论在 `comments[版本名]["pros"]`,不是 `comment_left_pros`;读错字段会得到空数据 |
|
|
1425
|
+
| 忽略量级差异 | `much_better` 和 `slightly_better` 的信号强度不同,`much_better` 更可靠 |
|
|
1426
|
+
| 忽略评估者差异 | 一个超严苛评估者会拉高 `similar+below` 率,稀释真实的胜率信号 |
|
|
1427
|
+
| 用全判定一致率评判可靠性 | 决定性判定一致率(排除 similar)才是方向共识的有效指标 |
|
|
1428
|
+
| 样本量过小时过度解读 p 值 | n < 30 的题目级结论,说"趋势"而非"结论" |
|
|
1429
|
+
| 遗漏 `_config.json` / `_reviews.json` | 读取 results 目录时应跳过 `_` 开头的配置文件 |
|
|
1430
|
+
| 直接剔除低连贯评估者 | 应使用加权(权重 ≥ 0.1),避免误杀有独立偏好的认真评估者 |
|
|
1431
|
+
| 用整体胜率代替分群胜率 | 当聚类分析发现两个群体胜率差异 >20% 时,总胜率会掩盖偏好分歧,产生误导 |
|
|
1432
|
+
| 无锚点题时跳过 L0 | 仍可做行为信号检测(作答时长、全 same 率、位置偏好)作为质量辅助判据 |
|
|
1433
|
+
| 把高"全same率"评估者直接标为噪声 | same 偏好也可能是真实的"差不多"判断,需结合作答时长综合判断 |
|
|
1434
|
+
| 只看 p 值不关注效应量 | 大样本下微小差异也显著(p < 0.05 但 Cohen's h < 0.2),应标注"效应量忽略不计" |
|
|
1435
|
+
| 分层分析不做多重比较校正 | 按 ≥ 3 组维度分层时,逐组检验不校正会膨胀第一类错误,应使用 BH FDR 校正 |
|
|
1436
|
+
| 用原始一致率代替 Kappa | 原始一致率不校正随机一致,GSS 3 分类中随机一致期望 ≈ 33%,需使用 Cohen's Kappa 或 Fleiss' Kappa |
|
|
1437
|
+
| 忽略位置偏倚 | 盲评中位置偏倚可能系统性高估一侧版本,应在 L0 阶段检测 |
|
|
1438
|
+
| 报告缺少元数据 | 无任务名称、参与人数、评估时间、报告时间的报告无法回溯,严重影响长期使用价值 |
|