import_compressor_labels.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. """Import algorithm stage labels into compressor_label.
  2. Phase 2: algorithm labels are written as segment records. Manual rows are
  3. never overwritten by a re-import.
  4. """
  5. from __future__ import annotations
  6. import argparse
  7. import csv
  8. import hashlib
  9. import json
  10. import re
  11. from datetime import datetime, timedelta
  12. from pathlib import Path
  13. from typing import Any
  14. from app.db import ensure_label_table, get_connection
  15. ROOT = Path(__file__).resolve().parents[1]
  16. VIBRATION_CSV = ROOT / "models" / "datas" / "reference" / "振动异常段.csv"
  17. OIL_CSV = ROOT / "models" / "datas" / "reference" / "润滑油压力小时阶段段.csv"
  18. TEMPERATURE_CSV = ROOT / "_tmp_temperature" / "out" / "温度异常段.csv"
  19. SEVERITY = {"轻微": 1, "异常": 2, "严重异常": 3}
  20. VIBRATION_SIDE_TO_METRIC = {"联轴器端": "YSJ_10", "链轮端": "YSJ_11"}
  21. INSERT_SQL = """
  22. INSERT INTO compressor_label (
  23. source_type, origin, target_type, target_id, target_key, channel_key, segment_key,
  24. unit, device_code, device_part, device_point, metric_key, metric_label,
  25. start_time, end_time, algorithm_start, algorithm_end,
  26. label_type, label_subtype, severity, confirm_status, usable_for_train,
  27. source_file, source_row, algorithm_version, features_json
  28. ) VALUES (
  29. %s, %s, %s, %s, %s, %s, %s,
  30. %s, %s, %s, %s, %s, %s,
  31. %s, %s, %s, %s,
  32. %s, %s, %s, %s, %s,
  33. %s, %s, %s, %s
  34. )
  35. ON DUPLICATE KEY UPDATE
  36. label_type = IF(compressor_label.origin = 'manual', compressor_label.label_type, VALUES(label_type)),
  37. label_subtype = IF(compressor_label.origin = 'manual', compressor_label.label_subtype, VALUES(label_subtype)),
  38. severity = IF(compressor_label.origin = 'manual', compressor_label.severity, VALUES(severity)),
  39. confirm_status = IF(compressor_label.origin = 'manual', compressor_label.confirm_status, VALUES(confirm_status)),
  40. usable_for_train = IF(compressor_label.origin = 'manual', compressor_label.usable_for_train, VALUES(usable_for_train)),
  41. features_json = IF(compressor_label.origin = 'manual', compressor_label.features_json, VALUES(features_json)),
  42. updated_at = CURRENT_TIMESTAMP
  43. """
  44. def read_csv(path: Path) -> list[dict[str, str]]:
  45. if not path.exists():
  46. print(f"跳过:文件不存在 {path}")
  47. return []
  48. with path.open("r", encoding="utf-8-sig", newline="") as handle:
  49. return list(csv.DictReader(handle))
  50. def parse_unit(raw: str) -> str | None:
  51. match = re.match(r"\s*(\d+)\s*号机", raw or "")
  52. return match.group(1) if match else None
  53. def parse_time(raw: str) -> datetime | None:
  54. text = (raw or "").strip()
  55. if not text:
  56. return None
  57. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
  58. try:
  59. return datetime.strptime(text, fmt)
  60. except ValueError:
  61. continue
  62. return None
  63. def time_text(value: datetime) -> str:
  64. return value.strftime("%Y-%m-%d %H:%M:%S")
  65. def metric_label(site_points: dict[str, str], unit: str, metric: str) -> str:
  66. return site_points.get(f"YSJ{unit}_{metric.split('_')[-1]}", metric)
  67. def confirm_status(row: dict[str, str]) -> str:
  68. return "已确认" if (row.get("是否形成确认等级") or "").strip() == "是" else "未确认"
  69. def make_segment(
  70. *,
  71. source_type: str,
  72. unit: str,
  73. metric: str,
  74. start: datetime,
  75. end: datetime,
  76. label_type: str,
  77. label_subtype: str | None,
  78. row: dict[str, str],
  79. source_file: str,
  80. site_points: dict[str, str],
  81. ) -> tuple[Any, ...]:
  82. device_code = f"{unit}#"
  83. target_key = f"pks:{unit}:{device_code}:{metric}"
  84. source_row = (row.get("周期编号") or "").strip()
  85. segment_key = hashlib.md5(
  86. f"{source_type}|{unit}|{metric}|{source_row}|{time_text(start)}|{time_text(end)}".encode("utf-8")
  87. ).hexdigest()
  88. label = metric_label(site_points, unit, metric)
  89. return (
  90. source_type,
  91. "algorithm",
  92. "pks",
  93. None,
  94. target_key,
  95. target_key,
  96. segment_key,
  97. unit,
  98. device_code,
  99. (row.get("机组") or "").strip(),
  100. label,
  101. metric,
  102. label,
  103. time_text(start),
  104. time_text(end),
  105. time_text(start),
  106. time_text(end),
  107. label_type,
  108. label_subtype,
  109. SEVERITY.get(label_type),
  110. confirm_status(row),
  111. 1,
  112. source_file,
  113. source_row,
  114. "csv-v1",
  115. json.dumps(row, ensure_ascii=False),
  116. )
  117. def build_rows(site_points: dict[str, str]) -> list[tuple[Any, ...]]:
  118. rows: list[tuple[Any, ...]] = []
  119. for row in read_csv(VIBRATION_CSV):
  120. unit = parse_unit(row.get("机组", ""))
  121. start = parse_time(row.get("开始小时", ""))
  122. end = parse_time(row.get("结束小时", ""))
  123. side = (row.get("最强异常侧") or "").strip()
  124. metric = VIBRATION_SIDE_TO_METRIC.get(side)
  125. if not unit or not start or not end or not metric:
  126. continue
  127. rows.append(
  128. make_segment(
  129. source_type="vibration",
  130. unit=unit,
  131. metric=metric,
  132. start=start,
  133. end=end + timedelta(hours=1),
  134. label_type=(row.get("振动阶段") or "").strip(),
  135. label_subtype=side or None,
  136. row=row,
  137. source_file=str(VIBRATION_CSV.relative_to(ROOT)),
  138. site_points=site_points,
  139. )
  140. )
  141. for row in read_csv(OIL_CSV):
  142. unit = parse_unit(row.get("机组", ""))
  143. start = parse_time(row.get("开始小时", ""))
  144. end = parse_time(row.get("结束小时", ""))
  145. if not unit or not start or not end:
  146. continue
  147. metric = "YSJ_5" if unit in {"7", "8", "9"} else "YSJ_1"
  148. rows.append(
  149. make_segment(
  150. source_type="oil_pressure",
  151. unit=unit,
  152. metric=metric,
  153. start=start,
  154. end=end + timedelta(hours=1),
  155. label_type=(row.get("压力阶段") or "").strip(),
  156. label_subtype="润滑油压力",
  157. row=row,
  158. source_file=str(OIL_CSV.relative_to(ROOT)),
  159. site_points=site_points,
  160. )
  161. )
  162. for row in read_csv(TEMPERATURE_CSV):
  163. unit = parse_unit(row.get("机组", ""))
  164. start = parse_time(row.get("开始小时", ""))
  165. end = parse_time(row.get("结束小时", ""))
  166. metric = (row.get("阶段点位") or "").strip()
  167. if not unit or not start or not end or not re.fullmatch(r"YSJ_\d+", metric):
  168. continue
  169. rows.append(
  170. make_segment(
  171. source_type="temperature",
  172. unit=unit,
  173. metric=metric,
  174. start=start,
  175. end=end + timedelta(hours=1),
  176. label_type=(row.get("温度阶段") or "").strip(),
  177. label_subtype=(row.get("阶段部位") or row.get("特征类型") or "").strip() or None,
  178. row=row,
  179. source_file=str(TEMPERATURE_CSV.relative_to(ROOT)),
  180. site_points=site_points,
  181. )
  182. )
  183. return [row for row in rows if row[17] in SEVERITY]
  184. def load_site_points() -> dict[str, str]:
  185. with get_connection() as connection:
  186. with connection.cursor() as cursor:
  187. cursor.execute("SELECT ItemName, ItemDescription FROM site_point")
  188. return {str(row["ItemName"]): str(row["ItemDescription"] or "") for row in cursor.fetchall()}
  189. def main() -> None:
  190. parser = argparse.ArgumentParser(description="导入算法阶段标签到 compressor_label")
  191. parser.add_argument("--dry-run", action="store_true", help="只统计,不写库")
  192. args = parser.parse_args()
  193. ensure_label_table()
  194. site_points = load_site_points()
  195. rows = build_rows(site_points)
  196. counts: dict[str, int] = {}
  197. for row in rows:
  198. counts[row[0]] = counts.get(row[0], 0) + 1
  199. print(f"待导入 {len(rows)} 条:{counts}")
  200. if args.dry_run:
  201. return
  202. with get_connection() as connection:
  203. with connection.cursor() as cursor:
  204. cursor.executemany(INSERT_SQL, rows)
  205. cursor.execute(
  206. "SELECT source_type, COUNT(*) AS n FROM compressor_label "
  207. "WHERE origin = 'algorithm' GROUP BY source_type ORDER BY source_type"
  208. )
  209. print("库内算法标签:", {row["source_type"]: int(row["n"]) for row in cursor.fetchall()})
  210. if __name__ == "__main__":
  211. main()