01_clean_medical_dataset.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. """清洗原始数据中的明确非医疗推理题和严重乱码,不覆盖原文件。"""
  2. import json
  3. import re
  4. import unicodedata
  5. from collections import Counter
  6. from pathlib import Path
  7. ROOT = Path(__file__).resolve().parent
  8. INPUT_PATH = ROOT / "medical_o1_sft_mix_Chinese.json"
  9. OUTPUT_PATH = ROOT / "medical_o1_sft_mix_Chinese_cleaned.json"
  10. REMOVED_PATH = ROOT / "medical_o1_sft_mix_Chinese_cleaned_removed.json"
  11. REPORT_PATH = ROOT / "medical_o1_sft_mix_Chinese_cleaned_report.json"
  12. FIELDS = ("Question", "Complex_CoT", "Response")
  13. # 这些私有区字符的含义可由上下文确定,优先修复而不是删除医学样本。
  14. CHAR_REPLACEMENTS = {
  15. "\ue06b": "牙合",
  16. "\ue02b": "+",
  17. "\ue02c": "-",
  18. "\ue022": "²",
  19. "\ue000": "1",
  20. "\ue001": "2",
  21. "\ue81f": "歪",
  22. "\ue13b": "拇趾",
  23. }
  24. MEDICAL_WORDS = re.compile(
  25. r"患者|患儿|疾病|症状|诊断|治疗|检查|药物|医院|临床|血|尿|肝|肾|心|肺|"
  26. r"生物等效|受试制剂|参比制剂|药代|AUC|"
  27. r"patient|disease|symptom|diagnos|treat|medicine|medical|clinical|blood|liver|"
  28. r"kidney|heart|lung|cancer|syndrome|infection|therapy",
  29. re.I,
  30. )
  31. STATISTICS_PATTERNS = [
  32. r"random sample", r"sample mean", r"standard deviation", r"probability that",
  33. r"normal distribution", r"exponential distribution", r"binomial distribution",
  34. r"confidence interval", r"null hypothesis", r"population mean", r"expected value",
  35. r"variance of", r"correlation coefficient", r"regression line", r"z[- ]?score",
  36. r"随机样本", r"样本均值", r"标准差", r"概率为", r"概率是多少", r"正态分布",
  37. r"指数分布", r"二项分布", r"置信区间", r"原假设", r"总体均值", r"数学期望",
  38. r"相关系数", r"回归方程",
  39. ]
  40. GEOMETRY_PATTERNS = [
  41. r"\bheptagon\b", r"\bpolygon\b", r"\btriangle\b", r"\brectangle\b",
  42. r"\bparallelogram\b", r"\bperimeter\b", r"\bvertices\b", r"area of (?:a|the)",
  43. r"多边形", r"三角形的面积", r"长方形的面积", r"正方形的面积", r"周长是多少",
  44. r"内角和", r"几何图形",
  45. ]
  46. TIME_PATTERNS = [
  47. r"what time", r"at what time", r"how many (?:hours|minutes)",
  48. r"(?<![A-Za-z])(?:a\.?m\.?|p\.?m\.?)(?![A-Za-z])",
  49. r"minutes (?:before|after)", r"hours (?:before|after)", r"arriv(?:e|es|ed).{0,50}leav",
  50. r"几点钟", r"星期几", r"钟表", r"时钟", r"出发.{0,40}到达", r"多少分钟后",
  51. ]
  52. def normalize_text(text):
  53. text = unicodedata.normalize("NFC", text)
  54. text = text.replace("正�?", "正常")
  55. text = text.replace("\u00a0", " ").replace("\u3000", " ")
  56. for old, new in CHAR_REPLACEMENTS.items():
  57. text = text.replace(old, new)
  58. return text
  59. def pattern_count(text, patterns):
  60. return sum(bool(re.search(pattern, text, re.I)) for pattern in patterns)
  61. def removal_reason(question):
  62. medical = bool(MEDICAL_WORDS.search(question))
  63. statistics = pattern_count(question, STATISTICS_PATTERNS)
  64. geometry = pattern_count(question, GEOMETRY_PATTERNS)
  65. time_reasoning = pattern_count(question, TIME_PATTERNS)
  66. # 只删除无医疗语境的纯计算题,保留医学统计、遗传概率和药代问题。
  67. if statistics >= 1 and not medical:
  68. return "pure_math_or_statistics"
  69. if geometry >= 1 and not medical:
  70. return "pure_geometry"
  71. if time_reasoning >= 1 and not medical:
  72. return "pure_time_reasoning"
  73. return None
  74. def has_unrecoverable_character(row):
  75. for field in FIELDS:
  76. for char in row[field]:
  77. category = unicodedata.category(char)
  78. if char == "�" or category == "Co":
  79. return True
  80. if category == "Cc" and char not in "\n\r\t":
  81. return True
  82. return False
  83. def main():
  84. for path in (OUTPUT_PATH, REMOVED_PATH, REPORT_PATH):
  85. if path.exists():
  86. raise FileExistsError(f"为避免覆盖已有结果,请先确认并处理:{path}")
  87. data = json.loads(INPUT_PATH.read_text(encoding="utf-8"))
  88. cleaned = []
  89. removed = []
  90. reasons = Counter()
  91. repaired_records = 0
  92. for original_index, original_row in enumerate(data):
  93. row = dict(original_row)
  94. changed = False
  95. for field in FIELDS:
  96. normalized = normalize_text(row[field])
  97. changed |= normalized != row[field]
  98. row[field] = normalized
  99. reason = removal_reason(row["Question"])
  100. if reason is None and has_unrecoverable_character(row):
  101. reason = "unrecoverable_character"
  102. if reason:
  103. removed.append({"original_index": original_index, "reason": reason, **row})
  104. reasons[reason] += 1
  105. else:
  106. cleaned.append(row)
  107. repaired_records += int(changed)
  108. OUTPUT_PATH.write_text(json.dumps(cleaned, ensure_ascii=False, indent=4), encoding="utf-8")
  109. REMOVED_PATH.write_text(json.dumps(removed, ensure_ascii=False, indent=4), encoding="utf-8")
  110. report = {
  111. "input_file": INPUT_PATH.name,
  112. "output_file": OUTPUT_PATH.name,
  113. "removed_file": REMOVED_PATH.name,
  114. "original_records": len(data),
  115. "cleaned_records": len(cleaned),
  116. "removed_records": len(removed),
  117. "repaired_records_kept": repaired_records,
  118. "removed_by_reason": dict(reasons),
  119. "original_file_overwritten": False,
  120. }
  121. REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=4), encoding="utf-8")
  122. print(json.dumps(report, ensure_ascii=False, indent=2))
  123. if __name__ == "__main__":
  124. main()