make_report.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env python3
  2. """把英文中间结果整理成中文交付物(CSV + 汇总 md)。"""
  3. from __future__ import annotations
  4. import argparse
  5. import csv
  6. from collections import defaultdict
  7. from datetime import datetime
  8. from pathlib import Path
  9. import cn_names as CN
  10. from bins_common import BIN_METRICS, CYCLE_METRICS
  11. BIN_DESC = {
  12. "均值": "箱内平均压力(水平)", "极差": "箱内 max-min(幅度)", "标准差": "箱内波动",
  13. "中位绝对偏差": "稳健波动", "斜率": "箱内压力对角度的斜率",
  14. "粗糙度": "去线性趋势后的残差 RMS(平滑/毛糙)", "10分位": "箱内 10% 分位",
  15. "50分位": "箱内中位数", "90分位": "箱内 90% 分位", "四分位距": "90 分位-10 分位",
  16. "偏度": "分布不对称度", "峰度": "尖峰程度(4 阶矩)",
  17. "平均|导数|": "相邻点压力差的平均绝对值(局部抖动)", "最大|导数|": "最大单步压力跳变",
  18. "导数RMS": "一阶导数均方根(抖动强度)", "二阶导RMS": "二阶导均方根(拐角尖锐)",
  19. "导数过零数": "一阶导数变号次数(振荡次数)", "极值数": "箱内局部极值个数",
  20. "二阶导最大": "最大二阶导(最尖的拐角)", "总变差": "Σ|相邻差|(弧长/毛糙)",
  21. "过均次数": "穿过箱均值的次数", "移动度": "Hjorth 移动度(频率感)",
  22. "复杂度": "Hjorth 复杂度(形状复杂度)",
  23. }
  24. CYCLE_DESC = {
  25. "吸气压力": "Otsu 低压平台", "排气压力": "Otsu 高压平台", "压比": "排气/吸气",
  26. "吸气占比": "低压样本占比", "排气占比": "高压样本占比",
  27. "最小压力角": "压力最小处的角度", "最大压力角": "压力最大处的角度",
  28. "上升穿越角": "压力上穿中间阈值角", "下降穿越角": "压力下穿中间阈值角",
  29. "最大上升斜率": "整周期最大正斜率", "最大下降斜率": "整周期最大负斜率",
  30. "最大上升角": "最大正斜率所在角", "最大下降角": "最大负斜率所在角",
  31. }
  32. def translate_csv(src: Path, dst: Path, metric_map: dict, family_map: dict | None = None) -> list[dict]:
  33. if not src.is_file():
  34. return []
  35. raw_headers = next(csv.reader(open(src, encoding="utf-8-sig")), [])
  36. rows = list(csv.DictReader(open(src, encoding="utf-8-sig")))
  37. headers = [CN.col(name) for name in raw_headers]
  38. out = []
  39. for row in rows:
  40. new = {}
  41. for key, value in row.items():
  42. if key == "metric":
  43. value = metric_map.get(value, value)
  44. elif key == "family" and family_map:
  45. value = family_map.get(value, value)
  46. elif key == "metrics":
  47. value = CN.translate_metrics_field(value)
  48. new[CN.col(key)] = value
  49. out.append(new)
  50. dst.parent.mkdir(parents=True, exist_ok=True)
  51. with dst.open("w", encoding="utf-8-sig", newline="") as handle:
  52. writer = csv.DictWriter(handle, fieldnames=headers)
  53. writer.writeheader()
  54. writer.writerows(out)
  55. return out
  56. def num(value):
  57. try:
  58. return float(value)
  59. except (TypeError, ValueError):
  60. return None
  61. def write_summary_md(summary_rows: list[dict], bins_dir: Path, out: Path) -> None:
  62. lines = []
  63. lines.append("# 功图分角度特征 汇总报告(中文)\n")
  64. lines.append(f"> 生成时间:{datetime.now():%Y-%m-%d %H:%M}。数据:7/8/9 号机,"
  65. "每个周期按键相 0-360° 切 10° 箱,逐箱算 23 个形状/波动/分布指标,"
  66. "再加 13 个整周期阀时序指标;对比在**同一测点**上做停机前 vs 开机后。\n")
  67. lines.append("## 一、怎么读结果\n")
  68. lines.append("- **特征候选汇总.csv**:每个候选 = 一个事件 × 一个测点 × 一个角度箱,把该箱里同时变化的指标合并在一行。")
  69. lines.append(" - `总分` = 该组所有指标 |MAD倍数| 之和;`指标数` = 变化 ≥3MAD 的指标个数。")
  70. lines.append(" - `相隔天数` = 后段起点 − 前段终点;`前跨度天数` = 前段首末之差。**这两个越小越可信**(避免把跨年/跨季工况当成故障)。")
  71. lines.append("- **事件明细_X号.csv**:每个角度箱/整周期/指标一行的前后值,供细查。")
  72. lines.append("- **图片**:`plots/` 下三联图——左=平均压力随角度、中=波动随角度、右=关注段随时间(红=停机前,蓝=开机后,虚线=事件日)。\n")
  73. lines.append("## 二、特征字典\n")
  74. lines.append("| 中文名 | 英文键 | 含义 |")
  75. lines.append("|---|---|---|")
  76. for name, en in [(CN.BIN_CN[m], m) for m in BIN_METRICS]:
  77. lines.append(f"| {name} | {en} | {BIN_DESC.get(name, '')} |")
  78. for name, en in [(CN.CYCLE_CN[m], m) for m in CYCLE_METRICS]:
  79. lines.append(f"| {name} | {en} | {CYCLE_DESC.get(name, '')} |")
  80. lines.append("")
  81. # 按事件分组
  82. groups = defaultdict(list)
  83. for row in summary_rows:
  84. groups[(row["机组"], row["事件日期"], row["故障类型"])].append(row)
  85. lines.append("## 三、各事件候选\n")
  86. for key in sorted(groups, key=lambda k: (k[0], k[1])):
  87. unit, date, kind = key
  88. items = sorted(groups[key], key=lambda r: -num(r["总分"]) if num(r["总分"]) else 0)
  89. lines.append(f"### {unit}号机 {date} {kind}\n")
  90. top = items[0]
  91. lines.append(f"- 前段 {top['前段止']} ← 后段 {top['后段起']}(相隔 {top['相隔天数']} 天,前跨度 {top['前跨度天数']} 天)")
  92. lines.append(f"- 候选组 {len(items)} 个,Top5:\n")
  93. lines.append("| 测点 | 角度箱 | 范围 | 指标数 | 总分 | 指标明细 |")
  94. lines.append("|---|---|---|---|---|---|")
  95. for r in items[:5]:
  96. bs = r["角箱起点"]
  97. if bs in ("", None):
  98. binst = "整周期"
  99. elif float(bs) < 0:
  100. binst = "全箱(整体)"
  101. else:
  102. binst = f"{float(bs):g}-{float(bs) + 10:g}°"
  103. lines.append(f"| {r['测点']} | {binst} | {r['范围']} | {r['指标数']} | {r['总分']} | {r['指标明细']} |")
  104. if any(str(r.get("范围", "")).startswith("整体") for r in items):
  105. lines.append("")
  106. lines.append("> 注:该事件存在**整体水平变化**(全角度一起动,可能负荷/工况),已单列。")
  107. lines.append("")
  108. clean = [r for r in summary_rows
  109. if num(r.get("前跨度天数")) is not None and num(r["前跨度天数"]) <= 20
  110. and num(r.get("相隔天数")) is not None and num(r["相隔天数"]) <= 40]
  111. clean.sort(key=lambda r: -num(r["总分"]))
  112. lines.append("## 四、干净短名单(前跨度≤20天 且 相隔≤40天)\n")
  113. lines.append("| # | 机组 | 事件 | 测点 | 角度箱 | 范围 | 指标数 | 总分 | 指标明细 |")
  114. lines.append("|---|---|---|---|---|---|---|---|---|")
  115. for i, r in enumerate(clean[:15], 1):
  116. _bs = r["角箱起点"]
  117. if _bs in ("", None):
  118. binst = "整周期"
  119. elif float(_bs) < 0:
  120. binst = "全箱(整体)"
  121. else:
  122. binst = f"{float(_bs):g}-{float(_bs) + 10:g}°"
  123. lines.append(f"| {i} | {r['机组']}号 | {r['事件日期']} {r['故障类型']} | {r['测点']} | {binst} | {r['范围']} | "
  124. f"{r['指标数']} | {r['总分']} | {r['指标明细']} |")
  125. lines.append("")
  126. lines.append("## 五、图片索引\n")
  127. plots = sorted((bins_dir / "plots").glob("*.png"))
  128. for p in plots:
  129. lines.append(f"- `plots/{p.name}`")
  130. lines.append("")
  131. out.write_text("\n".join(lines), encoding="utf-8")
  132. def main() -> None:
  133. parser = argparse.ArgumentParser(description="生成中文交付物")
  134. parser.add_argument("--dir", default=str(Path(__file__).resolve().parents[1] / "cache" / "indicator" / "bins"))
  135. parser.add_argument("--outdir", default="")
  136. args = parser.parse_args()
  137. bins_dir = Path(args.dir)
  138. outdir = Path(args.outdir) if args.outdir else bins_dir / "results_cn"
  139. outdir.mkdir(parents=True, exist_ok=True)
  140. all_metrics = {**CN.BIN_CN, **CN.CYCLE_CN}
  141. # 汇总
  142. summary_rows = translate_csv(bins_dir / "summary_final.csv", outdir / "特征候选汇总.csv",
  143. all_metrics, CN.FAMILY_CN)
  144. # 事件明细
  145. for unit in ("7", "8", "9"):
  146. src = bins_dir / f"event_final_{unit}.csv"
  147. if src.is_file():
  148. translate_csv(src, outdir / f"事件明细_{unit}号.csv", all_metrics, CN.FAMILY_CN)
  149. ssrc = bins_dir / f"scan_final_{unit}.csv"
  150. if ssrc.is_file():
  151. translate_csv(ssrc, outdir / f"全时间轴扫描_{unit}号.csv", all_metrics, CN.FAMILY_CN)
  152. write_summary_md(summary_rows, bins_dir, outdir / "特征汇总.md")
  153. print("中文交付 ->", outdir)
  154. for p in sorted(outdir.iterdir()):
  155. print(" ", p.name)
  156. if __name__ == "__main__":
  157. main()