db_parity_check.py 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. #!/usr/bin/env python3
  2. """Re-fetch the full hourly table from the DB and compare it to the cached copy.
  3. This honours the "DB full rerun" requirement without overwriting any cached
  4. artifact. The result is written to
  5. ``models/results/verification/db_parity.json``.
  6. """
  7. from __future__ import annotations
  8. import argparse
  9. import json
  10. import sys
  11. from pathlib import Path
  12. sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
  13. from models.common import db, paths, queries
  14. from models.common.constants import EXPECTED_PER_HOUR
  15. def key(row: dict):
  16. return (row["批次"], row["小时"])
  17. def normalize(row: dict):
  18. values = {field: row.get(field) for field in queries.NUMERIC_HOURLY_FIELDS}
  19. values["运行样本数"] = row.get("运行样本数")
  20. return values
  21. def main() -> None:
  22. parser = argparse.ArgumentParser(description="DB 全量小时中间表一致性校验")
  23. parser.add_argument("--start", default="2025-01-01 00:00:00")
  24. parser.add_argument("--end", default="2026-09-01 00:00:00")
  25. parser.add_argument("--chunk-days", type=int, default=14)
  26. parser.add_argument("--cached", default=str(paths.HOURLY_FULL))
  27. args = parser.parse_args()
  28. batch_to_unit = db.load_pks_batches()
  29. selected = sorted(batch_to_unit)
  30. configs = db.load_alarm_config({batch: batch_to_unit[batch] for batch in selected})
  31. start, end = queries.parse_dt(args.start), queries.parse_dt(args.end)
  32. print(f"从数据库重新聚合 {start} .. {end},批次 {selected}", flush=True)
  33. fetched = queries.fetch_all_hourly(start, end, args.chunk_days, batch_to_unit, configs, selected)
  34. print(f"数据库小时行:{len(fetched):,}", flush=True)
  35. cached = queries.load_hourly_csv(Path(args.cached))
  36. print(f"缓存小时行:{len(cached):,}", flush=True)
  37. fetched_by_key = {key(row): row for row in fetched}
  38. cached_by_key = {key(row): row for row in cached}
  39. only_db = sorted(set(fetched_by_key) - set(cached_by_key))
  40. only_cache = sorted(set(cached_by_key) - set(fetched_by_key))
  41. mismatched = []
  42. for item_key in sorted(set(fetched_by_key) & set(cached_by_key)):
  43. if normalize(fetched_by_key[item_key]) != normalize(cached_by_key[item_key]):
  44. mismatched.append({"key": item_key, "db": normalize(fetched_by_key[item_key]), "cache": normalize(cached_by_key[item_key])})
  45. report = {
  46. "db_rows": len(fetched),
  47. "cached_rows": len(cached),
  48. "only_in_db": len(only_db),
  49. "only_in_cache": len(only_cache),
  50. "mismatched_rows": len(mismatched),
  51. "identical": not only_db and not only_cache and not mismatched,
  52. "only_in_db_examples": only_db[:10],
  53. "only_in_cache_examples": only_cache[:10],
  54. "mismatched_examples": mismatched[:5],
  55. }
  56. output = paths.RESULTS / "verification"
  57. output.mkdir(parents=True, exist_ok=True)
  58. (output / "db_parity.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  59. print(json.dumps({k: v for k, v in report.items() if not k.endswith("examples")}, ensure_ascii=False, indent=2), flush=True)
  60. print(f"报告:{output / 'db_parity.json'}", flush=True)
  61. if __name__ == "__main__":
  62. main()