# 回归评测

> 用一组带 ground-truth 的样本压一遍 classifier,把准确率刻在表里,让改词典/改规则的影响可量化。

## 1. 输入格式

`samples/labeled_regression.jsonl`,每行一个 JSON 对象。首行可放 `{"_meta": "..."}` 注释,会被跳过。

```json
{
  "id": "r004",
  "title": "...",
  "content": "...",
  "occurred_at": "2026-05-27T20:00:00",
  "expected": {
    "threads": ["终端"],
    "thesis_impact": "增强",
    "min_confidence": 0.5,
    "max_confidence": null
  }
}
```

字段:

| 字段 | 必填 | 说明 |
|---|---|---|
| `id` | 推荐 | r001-r030 命名,便于在失败列表里追溯 |
| `title` / `content` | 任一 | classifier 输入(content 优先,fallback 到 title) |
| `expected.threads` | 必填 | 主线集合,集合相等才算 exact match |
| `expected.thesis_impact` | 必填 | "增强" / "削弱" / "中性" |
| `expected.min_confidence` | 可选 | 置信度下限 |
| `expected.max_confidence` | 可选 | 置信度上限(常用于 noise 样本) |
| `expected.scenarios` | 可选 | 暂未参与评估,留作 Phase 2 |
| `_note` | 可选 | 标注理由(争议 case 必填) |

## 2. 评估指标

每条样本三项独立判断:

1. **`threads_exact`** — 预测的 thread 集合 == expected 集合
2. **`impact`** — 预测的 thesis_impact == expected
3. **`confidence`** — 预测置信度落在 `[min, max]` 区间

整体通过 (`pass.overall`) 要求三项全过。

聚合指标:

| 指标 | 含义 | round 1 修复后 |
|---|---|---|
| `threads_exact_match_rate` | thread 集合精确匹配率 | **100%** |
| `thesis_impact_accuracy` | thesis 三态准确率 | **100%** |
| `confidence_in_band_rate` | 置信度落入期望区间率 | **100%** |
| `overall_pass_rate` | 三项全过通过率 | **100%** |
| `macro_f1_thread` | 4 主线 F1 的算术平均 | **1.000** |
| `avg_confidence` | 平均置信度 | 0.736 |

per-thread P/R/F1 也单独输出:

| thread | TP | FP | FN | P | R | F1 |
|---|---:|---:|---:|---:|---:|---:|
| 核心网 | 9 | 0 | 0 | 1.00 | 1.00 | 1.00 |
| 终端 | 16 | 0 | 0 | 1.00 | 1.00 | 1.00 |
| 芯片 | 8 | 0 | 0 | 1.00 | 1.00 | 1.00 |
| 运营支撑 | 8 | 0 | 0 | 1.00 | 1.00 | 1.00 |

## 3. 当前 30 条样本组成

> **重要前提**:这 30 条是**合成-领域真实**(synthetic-but-realistic)的,不是抓的真实新闻。规则与样本是一起调出来的,会有过拟合。100% 不代表上线表现。真实数据基线参见 [`regression-real.md`](./regression-real.md) — 25 条公开新闻在 rules v1 下三项全过 **64%**, macro F1 **0.875**。

主题分布:

- **核心网** (9): r001 端到端打通 / r002 中标 / r003 设备发布 / r008 D2C 标准 / r009 星座组网 / r010 地面站延期 / r011 未中标 / r027 招标重启 / r030 行业研报
- **终端** (16): r004 大额订单 / r005 芯片推迟拖累终端 / r006 在轨验证 / r008 D2C / r013 无人机机载 / r014 车载前装定点 / r015 车载召回 / r018 船载首单 / r022 专利诉讼 / r023 应急招标 / r026 IoT 模组 / r028 D2C 商用 / r007 海事被监管 / r030 研报 / r012 流片(终端 ODM 客户)/ r014 ...
- **芯片** (8): r005 / r006 / r008 / r012 / r016 / r017 / r025 / r030
- **运营支撑** (8): r001 公司反哺 / r007 频谱问题 / r019 频率许可 / r020 频谱协调失利 / r021 BOSS 升级 / r028 / r029 / r030

边角 case 覆盖:

| 边角 | 样本 ID | 用途 |
|---|---|---|
| 未中标(负向反义) | r011 | 验证 `_NEGATION_PAIRS` |
| 召回(law/risk) | r015 | 验证 NEG 扩词 |
| 诉讼 | r022 | 验证 NEG 扩词 |
| 裁员 | r025 | 验证 NEG 扩词 |
| 多主线 D2C | r008, r028, r030 | 验证多 thread 同时打 |
| 公司主线反哺 | r001 (中国卫通 → 运营支撑) | 验证 `enrich_with_company_threads` |
| 风险维度但无极性词 | r029 (短文本"被监管") | 验证 RISK fallback |
| 低信号噪音 | r024 (市场普涨) | 验证零主线 + 低 confidence |
| 标注争议 | r027 (招标重启) | 在 `_note` 字段标理由 |

## 4. Round 1 复盘

第一次跑出 **83.3% 三项全过**,5 个 failure → 复盘后定位 5 类根因,修完得到 100%:

| ID | 失败原因 | 根因 | 修复 |
|---|---|---|---|
| r010 | impact: 增强 (expected 削弱) | POS 词典 `投运` 列了 2 次,被"延期"压不过 | `sorted(set(...))` 强制 dedupe + `_hits` 用 `dict.fromkeys` |
| r012 | 多挂"运营支撑" | `频段` 太泛,毫米波频段误中 | 砍掉 `频段`,改用 `频段使用` |
| r019/r020 | 多挂"核心网" | `卫星互联网` 是产业泛词 | 从 CORE_NETWORK 移除,**产业泛词不进主线** |
| r023 | 中性 (expected 增强) | `启动` 未在 POS | 加入 POS;同步加 `启动调查 → 启动` 反义对 |
| r027 | 争议 (中性 vs 增强) | 标注边界 | 改 expected → 增强 + 加 `_note` 说理由 |

教训沉淀到 `docs/ontology.md` §7 "词典维护原则"。

## 5. 阈值断言

`tests/test_regression.py` 设的最低线(留有缓冲,接真实数据时会先掉到这里再回升):

```python
thesis_impact_accuracy ≥ 0.70
macro_f1_thread        ≥ 0.70
threads_exact_match    ≥ 0.55
confidence_in_band     ≥ 0.50
```

CI 跑 `pytest -q` 时这四条会被强制执行。如果加新样本后跌破,要么补词典,要么调阈值,**不允许"先注释掉 test 等会儿再说"**。

## 6. 已知 limitations

| 局限 | 影响 | 后续 |
|---|---|---|
| 合成数据自调 | 100% 是 round-trip,不代表上线表现 | 接 20-50 条真实新闻盲测 |
| 关键词长尾 | 没见过的新词(如新公司、新技术缩写)直接漏 | 接 LLM 兜底分类(`confidence < 0.4` 触发) |
| 无语义理解 | "公司 A 表示对 B 的诉讼**没有依据**" 会被打成削弱(命中"诉讼") | LLM 兜底 + 否定句检测 |
| 共指消解 | "该公司"指代谁要靠上下文 | LLM 必要 |
| 时态/语态 | "曾被罚款" vs "被罚款" 当前同等看待 | 暂可接受 |
| 评估缺 scenario / dimension | 当前只评 threads + thesis,scenario/dim 不参与 pass/fail | Phase 2 加入 |

## 7. 跑回归

CLI:
```bash
satagent regress samples/labeled_regression.jsonl                  # markdown
satagent regress samples/labeled_regression.jsonl --format json    # JSON
satagent regress samples/labeled_regression.jsonl --no-company-enrich  # 测纯规则
```

pytest:
```bash
pytest tests/test_regression.py -q
```

加新样本后建议先跑 `--no-company-enrich` 看原始规则表现,再开启 enrichment 看公司反哺贡献了多少。
