| |
| """조인 평가 + 회귀 검증 공용 모듈. |
| |
| 각 정규화 단계 전후로 호출해 조인율을 재측정하고, (board, lineno, refpos) |
| 단위로 '이전에 조인되던 인용이 깨졌는지'(회귀)를 정확히 비교한다. |
| """ |
| import json |
| import re |
|
|
| BOARDS = ["016001", "016002", "016003", "016005", "016006"] |
| DATA = __import__("pathlib").Path(__file__).resolve().parent / "data" / "parsed" |
|
|
|
|
| def load_std_index(): |
| s3001 = [json.loads(l) for l in (DATA / "3001.jsonl").open(encoding="utf-8")] |
| s3003 = [json.loads(l) for l in (DATA / "3003.jsonl").open(encoding="utf-8")] |
| return { |
| "k_para": {r["ref_key"] for r in s3001 if r["record_type"] == "paragraph"}, |
| "k_tsec": {r["section_key"] for r in s3001 if r["record_type"] == "term"}, |
| "k_nos": {r["standard_no"] for r in s3001}, |
| "g_para": {r["ref_key"] for r in s3003}, |
| "g_ch": {re.search(r"제(\d+)장", r["standard_no"]).group(1) for r in s3003}, |
| } |
|
|
|
|
| def classify(ref): |
| """참조 유형 판정: (kind, precision). precision=True면 문단/용어/장문단(정밀).""" |
| if re.match(r"^제\d{1,2}장", ref): |
| if "문단" in ref: |
| return "장문단", True |
| return ("장제목", False) if "'" in ref else ("단독장", False) |
| if re.match(r"^제\d{3,4}호", ref): |
| if ref.endswith("용어의 정의"): |
| return "용어섹션", True |
| if "문단" in ref: |
| return "문단", True |
| return ("제목", False) if "'" in ref else ("단독호", False) |
| return "기타", False |
|
|
|
|
| def matches(ref, idx): |
| """현재 ref가 기준서 레코드와 조인되는지 (정밀은 exact, 상위는 식별).""" |
| kind, _ = classify(ref) |
| if kind == "장문단": |
| return ref in idx["g_para"] |
| if kind in ("장제목", "단독장"): |
| ch = re.match(r"제(\d+)장", ref).group(1) |
| return ch in idx["g_ch"] |
| if kind == "용어섹션": |
| return ref in idx["k_tsec"] |
| if kind == "문단": |
| return ref in idx["k_para"] |
| if kind in ("제목", "단독호"): |
| no = re.match(r"제(\d+)호", ref).group(1) |
| return no in idx["k_nos"] |
| return False |
|
|
|
|
| def evaluate(): |
| """현재 JSONL 상태로 조인 측정. 반환: dict(rates, joined_precise set, joined_any set).""" |
| idx = load_std_index() |
| prec_tot = prec_hit = any_tot = any_hit = 0 |
| joined_precise = set() |
| joined_any = set() |
| per_kind = {} |
| for b in BOARDS: |
| for ln, line in enumerate((DATA / (b + ".jsonl")).open(encoding="utf-8")): |
| refs = json.loads(line).get("standard_refs", []) |
| for pos, ref in enumerate(refs): |
| kind, precise = classify(ref) |
| ok = matches(ref, idx) |
| d = per_kind.setdefault(kind, [0, 0]) |
| d[1] += 1 |
| if ok: |
| d[0] += 1 |
| joined_any.add((b, ln, pos)) |
| any_tot += 1 |
| any_hit += ok |
| if precise: |
| prec_tot += 1 |
| prec_hit += ok |
| if ok: |
| joined_precise.add((b, ln, pos)) |
| return { |
| "prec": (prec_hit, prec_tot), "any": (any_hit, any_tot), |
| "per_kind": per_kind, |
| "joined_precise": joined_precise, "joined_any": joined_any, |
| } |
|
|
|
|
| def report(r, label=""): |
| ph, pt = r["prec"]; ah, at = r["any"] |
| print(f"[{label}] 정밀 {ph}/{pt}={100*ph/pt:.1f}% | 전체 {ah}/{at}={100*ah/at:.1f}%") |
| for k in ["문단", "용어섹션", "제목", "단독호", "장문단", "장제목", "단독장"]: |
| if k in r["per_kind"]: |
| h, t = r["per_kind"][k] |
| print(f" {k}: {h}/{t} ({100*h/t:.0f}%)") |
|
|
|
|
| def regression(before, after): |
| """이전에 조인되던 (board,lineno,refpos)가 깨진 건수.""" |
| lost_prec = before["joined_precise"] - after["joined_precise"] |
| lost_any = before["joined_any"] - after["joined_any"] |
| return lost_prec, lost_any |
|
|
|
|
| if __name__ == "__main__": |
| report(evaluate(), "현재") |
|
|