verify.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env python3
  2. """Automated verification of the reconstructed algorithms and 3h models.
  3. Checks
  4. ------
  5. 1. Algorithm scan CSVs are byte-identical to the golden reference.
  6. 2. DB full rerun of the hourly intermediate matches the cached table.
  7. 3. Pressure / vibration 3h model metrics match the documented results.
  8. 4. Dataset candidate and split counts match the documented results.
  9. Writes ``models/results/verification/verification_report.json`` and exits
  10. non-zero if any check fails.
  11. """
  12. from __future__ import annotations
  13. import csv
  14. import json
  15. import sys
  16. from pathlib import Path
  17. sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
  18. from models.common import paths
  19. ALGORITHM_FILES = {
  20. "pressure": ["润滑油压力小时等级.csv", "润滑油压力小时阶段段.csv", "润滑油压力演化周期.csv"],
  21. "vibration": ["振动异常段.csv"],
  22. "common": ["报警阈值配置.csv"],
  23. }
  24. EXPECTED = {
  25. "pressure": {
  26. "candidates_normal": 12789, "candidates_anomaly": 1016,
  27. "splits": {"train": (9095, 8439, 656), "validation": (2457, 2256, 201), "test": (2253, 2094, 159)},
  28. "threshold": 0.8625534176826477,
  29. "anomaly": {"precision": 0.9512, "recall": 0.9811, "f1": 0.9659},
  30. "normal_f1": 0.9974,
  31. "pr_auc": 0.9961,
  32. "event_recall": (10, 10),
  33. "by_unit_normal_fp_rate": {"7号机": 0.0032, "8号机": 0.0113, "9号机": 0.0},
  34. },
  35. "vibration": {
  36. "candidates_normal": 13908, "candidates_anomaly": 231,
  37. "splits": {"train": (9302, 9112, 190), "validation": (2657, 2637, 20), "test": (2180, 2159, 21)},
  38. "threshold": 0.9166295528411865,
  39. "anomaly": {"precision": 0.7143, "recall": 0.7143, "f1": 0.7143},
  40. "normal_f1": 0.9972,
  41. "pr_auc": 0.8270,
  42. "event_recall": (4, 4),
  43. "by_unit_normal_fp_rate": {"7号机": 0.0, "8号机": 0.0071, "9号机": 0.0013},
  44. },
  45. }
  46. METRIC_TOLERANCE = 1e-3
  47. THRESHOLD_TOLERANCE = 1e-6
  48. def compare_files(reference: Path, candidate: Path) -> dict:
  49. return {
  50. "reference": str(reference),
  51. "candidate": str(candidate),
  52. "identical": reference.read_bytes() == candidate.read_bytes(),
  53. }
  54. def check_algorithm():
  55. checks = []
  56. for group, files in ALGORITHM_FILES.items():
  57. for name in files:
  58. reference = paths.REFERENCE / name
  59. candidate = (paths.COMMON_RESULTS if group == "common" else paths.RESULTS / group) / name
  60. checks.append({"group": group, "file": name, **compare_files(reference, candidate)})
  61. return {"passed": all(item["identical"] for item in checks), "checks": checks}
  62. def manifest_candidate_counts(dataset_dir: Path) -> dict:
  63. counts = {"normal": 0, "anomaly": 0}
  64. with (dataset_dir / "manifest.csv").open(encoding="utf-8-sig", newline="") as handle:
  65. for row in csv.DictReader(handle):
  66. if row["label"] == "0":
  67. counts["normal"] += 1
  68. elif row["label"] == "1":
  69. counts["anomaly"] += 1
  70. return counts
  71. def check_model(model_type: str):
  72. expected = EXPECTED[model_type]
  73. dataset_dir = paths.DATAS / model_type
  74. models_dir = paths.MODEL_ARTIFACTS / model_type
  75. results_dir = paths.RESULTS / model_type
  76. candidates = manifest_candidate_counts(dataset_dir)
  77. split_report = json.loads((dataset_dir / "split_report.json").read_text(encoding="utf-8"))[model_type]
  78. metadata = json.loads((models_dir / f"{model_type}_metadata.json").read_text(encoding="utf-8"))
  79. evaluation = json.loads((results_dir / f"{model_type}_test_evaluation.json").read_text(encoding="utf-8"))
  80. summary = evaluation["window_metrics"]
  81. details = []
  82. def check(name, actual, want, tolerance):
  83. passed = abs(actual - want) <= tolerance if isinstance(want, float) else actual == want
  84. details.append({"name": name, "actual": actual, "expected": want, "tolerance": tolerance, "passed": bool(passed)})
  85. check("candidates_normal", candidates["normal"], expected["candidates_normal"], 0)
  86. check("candidates_anomaly", candidates["anomaly"], expected["candidates_anomaly"], 0)
  87. for split, (total, normal, anomaly) in expected["splits"].items():
  88. got = split_report["sample_counts"][split]
  89. check(f"{split}_total", got["samples_total"], total, 0)
  90. check(f"{split}_normal", got["samples_normal"], normal, 0)
  91. check(f"{split}_anomaly", got["samples_anomaly"], anomaly, 0)
  92. check("threshold", metadata["threshold"], expected["threshold"], THRESHOLD_TOLERANCE)
  93. check("anomaly_precision", summary["class_metrics"]["anomaly"]["precision"], expected["anomaly"]["precision"], METRIC_TOLERANCE)
  94. check("anomaly_recall", summary["class_metrics"]["anomaly"]["recall"], expected["anomaly"]["recall"], METRIC_TOLERANCE)
  95. check("anomaly_f1", summary["class_metrics"]["anomaly"]["f1"], expected["anomaly"]["f1"], METRIC_TOLERANCE)
  96. check("normal_f1", summary["class_metrics"]["normal"]["f1"], expected["normal_f1"], METRIC_TOLERANCE)
  97. check("pr_auc", summary["pr_auc"], expected["pr_auc"], METRIC_TOLERANCE)
  98. detected, total = expected["event_recall"]
  99. check("event_recall", evaluation["event_metrics"]["detected_anomaly_events"], detected, 0)
  100. check("event_total", evaluation["event_metrics"]["anomaly_events"], total, 0)
  101. for unit, want in expected["by_unit_normal_fp_rate"].items():
  102. got = 1 - evaluation["by_unit"][unit]["specificity"]
  103. check(f"{unit}_normal_fp_rate", got, want, METRIC_TOLERANCE)
  104. return {"passed": all(item["passed"] for item in details), "checks": details}
  105. def check_db_parity():
  106. report_path = paths.RESULTS / "verification" / "db_parity.json"
  107. if not report_path.is_file():
  108. return {"passed": False, "reason": "db_parity.json 缺失"}
  109. report = json.loads(report_path.read_text(encoding="utf-8"))
  110. return {"passed": bool(report.get("identical")), "report": report}
  111. def main() -> None:
  112. report = {
  113. "algorithm": check_algorithm(),
  114. "db_parity": check_db_parity(),
  115. "models": {mt: check_model(mt) for mt in ("pressure", "vibration")},
  116. }
  117. report["passed"] = (
  118. report["algorithm"]["passed"]
  119. and report["db_parity"]["passed"]
  120. and all(item["passed"] for item in report["models"].values())
  121. )
  122. output = paths.RESULTS / "verification"
  123. output.mkdir(parents=True, exist_ok=True)
  124. (output / "verification_report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  125. print(f"算法扫描结果一致性:{'通过' if report['algorithm']['passed'] else '失败'}")
  126. print(f"数据库全量重跑一致性:{'通过' if report['db_parity']['passed'] else '失败'}")
  127. for model_type, result in report["models"].items():
  128. failed = [item["name"] for item in result["checks"] if not item["passed"]]
  129. print(f"{model_type} 模型复现:{'通过' if result['passed'] else '失败'}" + (f" 差异={failed}" if failed else ""))
  130. print(f"总体:{'全部通过' if report['passed'] else '存在失败项'}")
  131. print(f"报告:{output / 'verification_report.json'}")
  132. if not report["passed"]:
  133. sys.exit(1)
  134. if __name__ == "__main__":
  135. main()