#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 重建串接報告(離線,不重掃 DisposedCase) ========================================= 串接案件歷程.py 最後一段報告寫壞時,用這支從已產出的 CSV/JSON 重建, 不必再掃 5 GB 的 03_enriched。 python _pipeline\\串接報告.py 輸入:40_案件歷程/確定終局裁判.csv、案件歷程.csv、來源明細.json 輸出:00_索引/串接報告.md """ import csv, json, sys, collections, re from pathlib import Path ROOT = Path(__file__).resolve().parent.parent OUT = ROOT / "40_案件歷程" csv.field_size_limit(10 ** 8) def main(): try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass fj = list(csv.DictReader(open(OUT / "確定終局裁判.csv", encoding="utf-8-sig"))) ch = list(csv.DictReader(open(OUT / "案件歷程.csv", encoding="utf-8-sig"))) dp = OUT / "來源明細.json" det = json.load(open(dp, encoding="utf-8")) if dp.exists() else {} rows = [r for r in fj if r.get("法院")] nohit = [r for r in fj if not r.get("法院")] allcase = {r["釋憲案"] for r in fj} linked = {r["釋憲案"] for r in ch if r["審級"] != "未命中"} yr = collections.Counter(int(r["年"]) for r in rows if r["年"].isdigit()) out_of_range = sum(v for k, v in yr.items() if k < 103) lay = collections.Counter(v["審級"] for v in det.values()) if det else \ collections.Counter(r["審級"] for r in ch if r["審級"] != "未命中") ORD = {"確定終局裁判": 0, "前審": 1, "再前審": 2, "第四層": 3, "第五層": 4, "第六層": 5} layline = "、".join(f"{k} {v} 筆" for k, v in sorted(lay.items(), key=lambda x: ORD.get(x[0], 9))) # 每案的鏈數、鏈長、深度與分支 by = collections.defaultdict(list) for r in ch: by[r["釋憲案"]].append(r) chain_len, depth_ct, branch = collections.Counter(), collections.Counter(), [] for k, rs in by.items(): has_par = any("上層案號" in x for x in rs) byno = {} for x in rs: byno.setdefault(x["案號"], x) kids = collections.defaultdict(list) if has_par: for x in rs: p = (x.get("上層案號") or "").strip() if p: kids[p].append(x) else: for x in rs: p = (x.get("前審案號") or "").split(";")[0].strip() if p and p in byno: kids[x["案號"]].append(byno[p]) used = set() for x in sorted(rs, key=lambda r: ORD.get(r["審級"], 9)): if x["審級"] != "確定終局裁判" or x["案號"] in used: continue if has_par and (x.get("上層案號") or "").strip(): continue st = [0, 0] def go(y, d): if y["案號"] in used: return used.add(y["案號"]) st[0] += 1 st[1] = max(st[1], d + 1) ks = kids.get(y["案號"], []) if len(ks) > 1: branch.append((k, y["案號"], len(ks))) for c in ks: go(c, d + 1) go(x, 0) chain_len[st[0]] += 1 depth_ct[st[1]] += 1 rep = ["# 釋憲案件歷程串接報告", "", f"- 釋憲案總數:{len(allcase)}", f"- 官網可取得確定終局裁判字號者:{len(allcase) - len(nohit)}", f"- 解析出裁判字號:{len(rows)} 筆", f"- 其中年度早於 103 年(終結案件資料未涵蓋):{out_of_range} 筆", f"- 各層命中:{layline}", f"- 命中裁判合計:{len(det) or sum(lay.values())} 筆", f"- **至少串到一層的釋憲案:{len(linked)} 件**", "", "## 審級鏈結構", "", "一條鏈=一件確定終局裁判往下回溯出的整棵前審樹,**不等於一位被告**。", "一則釋憲案常合併多位聲請人,故一則案有多條鏈;", "一筆裁判也可能同時有多個前審(合併上訴、最高法院同列一二審),故鏈可能分叉。", "", "| 每條鏈的裁判數 | 鏈數 |", "|---|---|"] rep += [f"| {n} | {c} |" for n, c in sorted(chain_len.items())] rep += ["", "| 每條鏈的最大審級深度 | 鏈數 |", "|---|---|"] rep += [f"| {n} | {c} |" for n, c in sorted(depth_ct.items())] rep += ["", f"合計 {sum(chain_len.values())} 條鏈;" f"其中有 {len(branch)} 個節點同時指向兩個以上前審(分叉)。"] if branch: rep += ["", "分叉節點:", ""] rep += [f"- {k} {no}({n} 個前審)" for k, no, n in branch] rep += ["", "## 未能取得裁判字號之原因", ""] why = collections.Counter( (re.sub(r"^(未取得:|)$", "", r.get("官網原文法院", "")) or "未註明") for r in nohit) rep += [f"- {k}:{v} 件" for k, v in why.most_common()] rep += ["", "## 資料限制", "", "1. 終結案件資料為無姓名版,`c1_辯護及代理` 僅有型態類別" "(選任律師辯護/公設辯護人辯護/法律扶助/義務律師等),**無律師姓名**," "因此無法追蹤律師更換,也無法查該律師承辦之其他案件。", "2. 終結案件資料**不含當事人姓名**。當事人層級只有 `當事人序號`、`身分類別`、" "`當事人人數`,且序號僅在案內有效,跨案沒有任何個人識別碼。" "因此無法列出被告姓名,也無法以個人為單位追蹤其前案。" "一條審級鏈=一件確定終局裁判(一個案號),不等於一位被告。", "3. 法官姓名(`c0_法官名1~3`)有值,法官的縱向與橫向追蹤可行。", "4. 終結案件資料涵蓋 103–114 年度,102 年以前之裁判無法串接。", "5. 串接以「法院+年+字別+號」精確比對,不做模糊比對;" "地方法院名稱之「臺灣」前綴已正規化對齊。", "6. 前審欄位在 387 個 enriched 檔中有四種命名,串接腳本四種都認:" "`c0_原審(前審)…`(132 檔)、`c0_原審(前審或確定)…`(108 檔)、" "`c0_一審/二審…`(36 檔)、`c0_原審(前審)簡易庭別`(12 檔)。" "另有 90 檔完全沒有前審欄位(如最高法院民事其他、地院民事執行、懲戒法院)," "那批在資料上就接不了,不是比對失敗。", "7. 回溯深度不預設層數,接不動為止(上限 6 層);" "一筆同時列出一審與二審時,優先接已命中者。"] (ROOT / "00_索引").mkdir(exist_ok=True) p = ROOT / "00_索引" / "串接報告.md" p.write_text("\n".join(rep) + "\n", encoding="utf-8") print("已重建", p) print(f" 各層命中:{layline}") print(f" 鏈長分布:" + "、".join(f"{n} 層 {c} 條" for n, c in sorted(chain_len.items()))) print(f" 至少串到一層的釋憲案:{len(linked)} 件") if __name__ == "__main__": main()