evaluate_extractor.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. from __future__ import annotations
  2. import json
  3. from collections import defaultdict
  4. from dataclasses import dataclass
  5. from typing import Any
  6. from app.extractor import RuleBasedLaborExtractor
  7. def _norm(v: Any) -> Any:
  8. if v is None:
  9. return None
  10. if isinstance(v, str):
  11. return v.strip()
  12. return v
  13. def _as_set(v: Any) -> set[str]:
  14. if v is None:
  15. return set()
  16. if isinstance(v, list):
  17. return set([str(x).strip() for x in v if str(x).strip()])
  18. return {str(v).strip()}
  19. @dataclass
  20. class FieldPRF:
  21. tp: int = 0
  22. fp: int = 0
  23. fn: int = 0
  24. def add(self, gold: Any, pred: Any) -> None:
  25. gset = _as_set(gold)
  26. pset = _as_set(pred)
  27. self.tp += len(gset & pset)
  28. self.fp += len(pset - gset)
  29. self.fn += len(gset - pset)
  30. def metrics(self) -> dict[str, float]:
  31. p = self.tp / (self.tp + self.fp) if (self.tp + self.fp) else 0.0
  32. r = self.tp / (self.tp + self.fn) if (self.tp + self.fn) else 0.0
  33. f1 = 2 * p * r / (p + r) if (p + r) else 0.0
  34. return {"precision": round(p, 4), "recall": round(r, 4), "f1": round(f1, 4)}
  35. def run_eval(samples: list[dict[str, Any]]) -> dict[str, Any]:
  36. extractor = RuleBasedLaborExtractor()
  37. stats: dict[str, FieldPRF] = defaultdict(FieldPRF)
  38. for s in samples:
  39. pred = extractor.extract(s["text"])
  40. gold = s["gold"]
  41. for field in gold.keys():
  42. if field not in pred:
  43. continue
  44. if field == "claims":
  45. # claims 是结构体:只评 items 和 amount_total
  46. stats["claims.items"].add(gold["claims"].get("items"), pred["claims"].get("items"))
  47. stats["claims.amount_total"].add(gold["claims"].get("amount_total"), pred["claims"].get("amount_total"))
  48. else:
  49. stats[field].add(_norm(gold.get(field)), _norm(pred.get(field)))
  50. report = {k: v.metrics() | {"tp": v.tp, "fp": v.fp, "fn": v.fn} for k, v in sorted(stats.items())}
  51. # micro 平均(合计)
  52. total = FieldPRF()
  53. for v in stats.values():
  54. total.tp += v.tp
  55. total.fp += v.fp
  56. total.fn += v.fn
  57. report["_micro_avg"] = total.metrics() | {"tp": total.tp, "fp": total.fp, "fn": total.fn}
  58. return report
  59. def demo_samples() -> list[dict[str, Any]]:
  60. """
  61. 至少 10 条模拟样例(可替换为你的真实标注集)。
  62. """
  63. return [
  64. {
  65. "text": "申请人:张三\n被申请人:北京某某有限公司\n单位性质:企业\n岗位:销售\n入职时间:2020年1月1日\n离职时间:2022-03-01\n月工资:8000元\n仲裁请求:1. 支付拖欠工资8000元;2. 支付加班费2000元。\n依据《劳动合同法》第三十条。",
  66. "gold": {
  67. "applicant_name": "张三",
  68. "respondent_name": "北京某某有限公司",
  69. "employer_nature": "企业",
  70. "worker_position": "销售",
  71. "case_cause": "工资报酬",
  72. "entry_date": "2020-01-01",
  73. "leave_date": "2022-03-01",
  74. "month_salary": 8000.0,
  75. "overtime_desc": "支付加班费2000元",
  76. "termination_reason": None,
  77. "claims": {"items": ["支付拖欠工资8000元", "支付加班费2000元"], "amount_total": 10000.0},
  78. "law_refs": ["《劳动合同法》第三十条", "《劳动合同法》"],
  79. },
  80. },
  81. {
  82. "text": "申请人:李四,被申请人:上海某医院。岗位:护士。于2020.02.02入职,于2021.12.31解除。请求经济补偿八千元。引用《劳动合同法》。",
  83. "gold": {
  84. "applicant_name": "李四",
  85. "respondent_name": "上海某医院",
  86. "employer_nature": "事业单位",
  87. "worker_position": "护士",
  88. "case_cause": "经济补偿",
  89. "entry_date": "2020-02-02",
  90. "leave_date": "2021-12-31",
  91. "month_salary": None,
  92. "overtime_desc": None,
  93. "termination_reason": None,
  94. "claims": {"items": [], "amount_total": None},
  95. "law_refs": ["《劳动合同法》"],
  96. },
  97. },
  98. {
  99. "text": "申请人:王五\n被申请人:某某科技股份有限公司\n入职日期2021/6/1,离职日期2023/1/15。\n月薪为8,000元。\n因绩效不达标被辞退。\n请求:违法解除劳动合同赔偿金96000元。\n依据劳动合同法第八十七条。",
  100. "gold": {
  101. "applicant_name": "王五",
  102. "respondent_name": "某某科技股份有限公司",
  103. "employer_nature": "企业",
  104. "worker_position": None,
  105. "case_cause": "违法解除劳动合同",
  106. "entry_date": "2021-06-01",
  107. "leave_date": "2023-01-15",
  108. "month_salary": 8000.0,
  109. "overtime_desc": None,
  110. "termination_reason": "绩效不达标",
  111. "claims": {"items": ["违法解除劳动合同赔偿金96000元"], "amount_total": 96000.0},
  112. "law_refs": ["劳动合同法第八十七条"],
  113. },
  114. },
  115. {
  116. "text": "申请人:赵六\n被申请人:某个体工商户\n单位性质:个体工商户\n岗位:厨师\n入职时间:2019-5-10\n离职时间:2020-6-1\n主张加班费3000元,休息日加班无调休。\n法律依据:《劳动争议调解仲裁法》。",
  117. "gold": {
  118. "applicant_name": "赵六",
  119. "respondent_name": "某个体工商户",
  120. "employer_nature": "个人",
  121. "worker_position": "厨师",
  122. "case_cause": "加班费",
  123. "entry_date": "2019-05-10",
  124. "leave_date": "2020-06-01",
  125. "month_salary": None,
  126. "overtime_desc": "主张加班费3000元,休息日加班无调休",
  127. "termination_reason": None,
  128. "claims": {"items": [], "amount_total": None},
  129. "law_refs": ["《劳动争议调解仲裁法》"],
  130. },
  131. },
  132. {
  133. "text": "申请人:孙七\n被申请人:某某有限公司\n工伤待遇:一次性伤残补助金50000元。\n于2022年7月7日入职,2023年7月7日离职。\n依据《工伤保险条例》。",
  134. "gold": {
  135. "applicant_name": "孙七",
  136. "respondent_name": "某某有限公司",
  137. "employer_nature": "企业",
  138. "worker_position": None,
  139. "case_cause": "工伤待遇",
  140. "entry_date": "2022-07-07",
  141. "leave_date": "2023-07-07",
  142. "month_salary": None,
  143. "overtime_desc": None,
  144. "termination_reason": None,
  145. "claims": {"items": [], "amount_total": None},
  146. "law_refs": ["《工伤保险条例》"],
  147. },
  148. },
  149. # 其余 5 条:覆盖不同日期/金额/案由写法
  150. {
  151. "text": "申请人:周八 被申请人:某某集团有限公司 岗位:工程师 入职:2020年12月5日 离职:2022年2月1日 月薪12000元 请求加班费10000元。",
  152. "gold": {
  153. "applicant_name": "周八",
  154. "respondent_name": "某某集团有限公司",
  155. "employer_nature": "企业",
  156. "worker_position": "工程师",
  157. "case_cause": "加班费",
  158. "entry_date": "2020-12-05",
  159. "leave_date": "2022-02-01",
  160. "month_salary": 12000.0,
  161. "overtime_desc": "请求加班费10000元",
  162. "termination_reason": None,
  163. "claims": {"items": [], "amount_total": None},
  164. "law_refs": [],
  165. },
  166. },
  167. {
  168. "text": "申请人:钱九\n被申请人:某某学校\n单位性质:事业单位\n请求:支付拖欠工资二万元。\n入职时间2021.01.01,解除时间2021.06.30。",
  169. "gold": {
  170. "applicant_name": "钱九",
  171. "respondent_name": "某某学校",
  172. "employer_nature": "事业单位",
  173. "worker_position": None,
  174. "case_cause": "工资报酬",
  175. "entry_date": "2021-01-01",
  176. "leave_date": "2021-06-30",
  177. "month_salary": None,
  178. "overtime_desc": None,
  179. "termination_reason": None,
  180. "claims": {"items": ["支付拖欠工资二万元"], "amount_total": 20000.0},
  181. "law_refs": [],
  182. },
  183. },
  184. {
  185. "text": "申请人:吴十\n被申请人:某某有限责任公司\n解除原因:未签订劳动合同。\n请求经济补偿12000元。\n法律条款:《劳动合同法》第四十六条。",
  186. "gold": {
  187. "applicant_name": "吴十",
  188. "respondent_name": "某某有限责任公司",
  189. "employer_nature": "企业",
  190. "worker_position": None,
  191. "case_cause": "经济补偿",
  192. "entry_date": None,
  193. "leave_date": None,
  194. "month_salary": None,
  195. "overtime_desc": None,
  196. "termination_reason": "未签订劳动合同",
  197. "claims": {"items": ["经济补偿12000元"], "amount_total": 12000.0},
  198. "law_refs": ["《劳动合同法》第四十六条", "《劳动合同法》"],
  199. },
  200. },
  201. {
  202. "text": "申请人:郑十一\n被申请人:某某公司\n岗位:司机\n加班事实:长期每天加班至22点,未支付加班工资。\n请求加班费三千元。\n",
  203. "gold": {
  204. "applicant_name": "郑十一",
  205. "respondent_name": "某某公司",
  206. "employer_nature": "企业",
  207. "worker_position": "司机",
  208. "case_cause": "加班费",
  209. "entry_date": None,
  210. "leave_date": None,
  211. "month_salary": None,
  212. "overtime_desc": "加班事实:长期每天加班至22点,未支付加班工资",
  213. "termination_reason": None,
  214. "claims": {"items": ["加班费三千元"], "amount_total": 3000.0},
  215. "law_refs": [],
  216. },
  217. },
  218. {
  219. "text": "申请人:冯十二\n被申请人:某某厂\n请求:支付工资差额5000元;支付经济补偿5000元。\n依据《劳动合同法》。",
  220. "gold": {
  221. "applicant_name": "冯十二",
  222. "respondent_name": "某某厂",
  223. "employer_nature": "企业",
  224. "worker_position": None,
  225. "case_cause": "工资报酬",
  226. "entry_date": None,
  227. "leave_date": None,
  228. "month_salary": None,
  229. "overtime_desc": None,
  230. "termination_reason": None,
  231. "claims": {"items": ["支付工资差额5000元", "支付经济补偿5000元"], "amount_total": 10000.0},
  232. "law_refs": ["《劳动合同法》"],
  233. },
  234. },
  235. ]
  236. if __name__ == "__main__":
  237. # 可选:用 --dataset 读取你的真实标注集(JSON)
  238. import argparse
  239. parser = argparse.ArgumentParser()
  240. parser.add_argument("--dataset", default="", help="可选:标注集 JSON 文件路径")
  241. args = parser.parse_args()
  242. if args.dataset:
  243. samples = json.loads(open(args.dataset, "r", encoding="utf-8").read())
  244. else:
  245. samples = demo_samples()
  246. report = run_eval(samples)
  247. print(json.dumps(report, ensure_ascii=False, indent=2))