RNAQwen / scripts /_scratch /_qa_quality_metrics.py
RomanCohort's picture
add scripts/_scratch/_qa_quality_metrics.py
7690297 verified
Raw History Blame Contribute Delete
4.61 kB
"""系统性质量度量:QA 到底在问"这条 RNA"还是"那篇论文"。
背景
----
换掉 NCBI 链路的根本原因,是那条路生成的问题**问的不是那条 RNA**
(`What is the purpose of the PhyloFish database?`)。所以"换源之后这个问题修好了没有"
必须**量化**,不能靠看两条样例拍脑袋。
判据
----
`ent_id`(如 `MIR423` / `SNORD1B` / `LINC00471`)是 RNA 的名字。
一条 QA 只要在**问题或答案**里提到这个名字,就说明它至少锚定在正确的分子上。
按 variant(ais=文献 / dc=RNAcentral 官方综述)分别统计,差距就是这两个变体的性质差异。
另外统计闭式问答的形式(yes/no vs 四选一)—— `12_generate_qa.py` 的 kind 字段
在模型没返回 `kind` 时会回落成 "closed",所以这里用文本形态判断,不信任该字段。
"""
from __future__ import annotations
import argparse
import collections
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from rnaqwen.data.records import read_jsonl # noqa: E402
_MC_RE = re.compile(r"^\s*[A-D][\).\:]\s*\S", re.M)
def name_forms(ent_id: str) -> list[str]:
"""一个基因名可能的写法:原样 / 去横线 / 小写变体交给大小写无关匹配。"""
e = (ent_id or "").strip()
if not e:
return []
forms = {e, e.replace("-", ""), e.replace("-", " ")}
# MIR423 -> miR-423 这类写法也常见;做一个宽松的数字+前后缀形式
m = re.match(r"^([A-Za-z]+)[-_]?(\d+)$", e)
if m:
forms.add(f"{m.group(1)}-{m.group(2)}")
forms.add(f"{m.group(1)}{m.group(2)}")
return [f.lower() for f in forms if f]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--in", dest="inp", type=Path, required=True)
ap.add_argument("--out", type=Path, default=ROOT / "reports" / "qa_quality_metrics.json")
ap.add_argument("--examples", type=int, default=5)
args = ap.parse_args()
recs = list(read_jsonl(args.inp))
per = collections.defaultdict(lambda: collections.Counter())
overall = collections.Counter()
miss_examples: list[dict] = []
for r in recs:
ent = r.provenance.get("ent_id", "") if isinstance(r.provenance, dict) else ""
forms = name_forms(ent)
for p in r.qa:
var = p.get("variant", "?")
kind = p.get("kind", "?")
q, a = p.get("question", ""), p.get("answer", "")
blob = (q + " " + a).lower()
hit = any(f in blob for f in forms)
both = bool(forms)
overall["qa"] += 1
overall["has_ent_id"] += int(both)
overall["mentions_rna"] += int(hit)
per[var]["qa"] += 1
per[var]["mentions_rna"] += int(hit)
per[var + "/" + kind]["qa"] += 1
per[var + "/" + kind]["mentions_rna"] += int(hit)
if kind in ("closed", "yes_no", "multiple_choice"):
is_mc = bool(_MC_RE.search(a)) or "A." in a[:8] or "B." in a[:8]
per[var]["closed_mc"] += int(is_mc)
per[var]["closed_yn"] += int(not is_mc)
if not hit and len(miss_examples) < 400:
miss_examples.append({"uid": r.uid, "ent_id": ent, "variant": var,
"kind": kind, "q": q[:170], "a": a[:170]})
n = max(overall["qa"], 1)
report = {
"records": len(recs), "qa": overall["qa"],
"mentions_rna_pct": round(100 * overall["mentions_rna"] / n, 1),
"has_ent_id_pct": round(100 * overall["has_ent_id"] / n, 1),
"by_variant": {k: dict(v) for k, v in sorted(per.items())},
"by_variant_pct": {
k: round(100 * v["mentions_rna"] / max(v["qa"], 1), 1)
for k, v in sorted(per.items()) if "mentions_rna" in v
},
"miss_examples": miss_examples[: args.examples * 4],
}
print(json.dumps({k: v for k, v in report.items() if k != "miss_examples"},
indent=2, ensure_ascii=False))
print("\n--- 没提到 RNA 名字的样例(抽查用)---")
for e in miss_examples[: args.examples]:
print(f" [{e['variant']}/{e['kind']}] ent={e['ent_id']}")
print(f" Q: {e['q']}")
print(f" A: {e['a']}")
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\n[metrics] -> {args.out}")
return 0
if __name__ == "__main__":
sys.exit(main())