labeler.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. """
  2. 独立打标模块:基于预先生成的机型阈值 Excel 对风机数据打标签。
  3. 功能:
  4. apply_labels(df, model_name, threshold_file=None) -> DataFrame
  5. 输入原始数据 DataFrame 和机型名称,返回打标后的 DataFrame,只包含原始列和 label 列、sensor_tag列。
  6. 输出新增列:
  7. - label : 最终业务状态(运行/停机/限功率/传感器异常-xxx)
  8. 状态优先级(高→低):传感器异常 > 停机 > 限功率 > 运行
  9. """
  10. from typing import Dict, Optional
  11. import numpy as np
  12. import pandas as pd
  13. # ==================== 打标参数 ====================
  14. # 功率越界倍数(相对额定功率)
  15. STATUS_POWER_UPPER_RATIO = 1.25 # 功率上限倍数
  16. STATUS_POWER_LOWER_RATIO = -0.10 # 功率下限倍数
  17. STATUS_SHUTDOWN_RATIO = 0.005 # 停机功率阈值(额定功率的比例),最小取 50kW
  18. STATUS_CURTAIL_LOW_RATIO = 0.02 # 限功率下限(额定功率比例)
  19. STATUS_CURTAIL_HIGH_RATIO = 0.95 # 限功率上限(额定功率比例)
  20. STATUS_CURTAIL_PITCH_OFFSET = 3.0 # 限功率桨距角偏移量(基准桨距角 + 此值)
  21. # 风速越界阈值(m/s)
  22. STATUS_WIND_MAX = 75.0
  23. STATUS_WIND_MIN = -2.0
  24. # 变桨越界阈值(°)
  25. STATUS_PITCH_MAX = 105.0
  26. STATUS_PITCH_MIN = -10.0
  27. # 转速/扭矩越界倍数
  28. STATUS_SPD_UPPER_RATIO = 1.25
  29. STATUS_TORQUE_UPPER_RATIO = 1.25
  30. STATUS_TORQUE_LOWER_ABS = -2000.0
  31. # 逻辑悖论阈值
  32. STATUS_LOGIC_WIND_MIN = 0.1 # 风速低于此值但功率大则为逻辑异常
  33. STATUS_LOGIC_POWER_MIN = 100.0 # 配合上面风速阈值的功率下限
  34. # 机型特殊规则(可扩展)
  35. STATUS_MODEL_SPECIAL_RULES = {
  36. "EN156-3300": [(2.7, 450)], # 示例:风速 < 2.7 且功率 > 450 时追加逻辑异常
  37. }
  38. # 传感器异常列与对应可读标签(内部使用,不输出)
  39. _SENSOR_COL_LABEL: Dict[str, str] = {
  40. "d_val_power": "功率值异常",
  41. "d_val_wind": "风速值异常",
  42. "d_val_pitch": "变桨值异常",
  43. "d_val_spd": "转速值异常",
  44. "d_val_torque": "扭矩值异常",
  45. "d_logic_wind_pwr": "风速功率逻辑异常",
  46. "d_logic_torque_pwr": "转速扭矩逻辑异常",
  47. }
  48. # ==================== 阈值加载(静态数据) ====================
  49. # 静态阈值数据(从机型参数阈值表.xlsx转换)
  50. _THRESHOLD_CACHE: Dict[str, dict] = {
  51. "CCWE1500-82.DF": {
  52. "p_max_observed": 1554.0,
  53. "torque_limit": 17924.0,
  54. "spd_limit": 1861.59,
  55. "baseline_pitch": 0.04
  56. },
  57. "CCWE3000-122.HD": {
  58. "p_max_observed": 3047.0,
  59. "torque_limit": None,
  60. "spd_limit": 362.3,
  61. "baseline_pitch": 0.19
  62. },
  63. "DEW-D10000-185": {
  64. "p_max_observed": 10135.3,
  65. "torque_limit": 10841040.0,
  66. "spd_limit": 10.45,
  67. "baseline_pitch": -0.5
  68. },
  69. "DEW-D3800-148": {
  70. "p_max_observed": 3508.8,
  71. "torque_limit": 3320535.25,
  72. "spd_limit": 11.42,
  73. "baseline_pitch": 0.0
  74. },
  75. "DEW-G3600-165": {
  76. "p_max_observed": 3730.5,
  77. "torque_limit": 21010.0,
  78. "spd_limit": 1838.41,
  79. "baseline_pitch": 0.15
  80. },
  81. "DEW-G5500-183": {
  82. "p_max_observed": 5548.6,
  83. "torque_limit": 32636.0,
  84. "spd_limit": 1746.57,
  85. "baseline_pitch": 2.85
  86. },
  87. "DEW-G6250-186": {
  88. "p_max_observed": 6314.0,
  89. "torque_limit": 92110.56,
  90. "spd_limit": 710.82,
  91. "baseline_pitch": 0.1
  92. },
  93. "DEW-H8350-A-212": {
  94. "p_max_observed": 7085.1,
  95. "torque_limit": 125065.52,
  96. "spd_limit": 8.82,
  97. "baseline_pitch": 1.89
  98. },
  99. "DF103-2000": {
  100. "p_max_observed": 2024.2,
  101. "torque_limit": 1264956.0,
  102. "spd_limit": 16.37,
  103. "baseline_pitch": -0.5
  104. },
  105. "DF131-2500": {
  106. "p_max_observed": 2527.9,
  107. "torque_limit": 2058254.75,
  108. "spd_limit": 13.25,
  109. "baseline_pitch": 0.0
  110. },
  111. "EN141-2650": {
  112. "p_max_observed": 2744.6,
  113. "torque_limit": 15705.3,
  114. "spd_limit": 1778.21,
  115. "baseline_pitch": -0.79
  116. },
  117. "EN156-3300": {
  118. "p_max_observed": 3359.5,
  119. "torque_limit": 18575.35,
  120. "spd_limit": 1835.56,
  121. "baseline_pitch": -1.45
  122. },
  123. "EN171-4500": {
  124. "p_max_observed": 4559.4,
  125. "torque_limit": 25390.95,
  126. "spd_limit": 1795.83,
  127. "baseline_pitch": -0.85
  128. },
  129. "EN182-6250": {
  130. "p_max_observed": 6326.0,
  131. "torque_limit": 34601.38,
  132. "spd_limit": 1802.05,
  133. "baseline_pitch": -0.25
  134. },
  135. "FD77-1500": {
  136. "p_max_observed": 1534.2,
  137. "torque_limit": 8058.38,
  138. "spd_limit": 1846.84,
  139. "baseline_pitch": 0.0
  140. },
  141. "G52-850": {
  142. "p_max_observed": 851.5,
  143. "torque_limit": None,
  144. "spd_limit": 1653.3,
  145. "baseline_pitch": -2.2
  146. },
  147. "G58-850": {
  148. "p_max_observed": 813.5,
  149. "torque_limit": None,
  150. "spd_limit": 1670.4,
  151. "baseline_pitch": 0.48
  152. },
  153. "GW140-2500": {
  154. "p_max_observed": 2575.0,
  155. "torque_limit": None,
  156. "spd_limit": 12.31,
  157. "baseline_pitch": 3.63
  158. },
  159. "GW140-3300": {
  160. "p_max_observed": 3486.0,
  161. "torque_limit": None,
  162. "spd_limit": None,
  163. "baseline_pitch": 4.25
  164. },
  165. "GW191-5000": {
  166. "p_max_observed": 5004.0,
  167. "torque_limit": None,
  168. "spd_limit": 705.6,
  169. "baseline_pitch": 0.0
  170. },
  171. "GW191-6250": {
  172. "p_max_observed": 6258.6,
  173. "torque_limit": None,
  174. "spd_limit": 686.2,
  175. "baseline_pitch": 2.2
  176. },
  177. "GW82-1500": {
  178. "p_max_observed": 1570.0,
  179. "torque_limit": None,
  180. "spd_limit": 18.12,
  181. "baseline_pitch": 0.2
  182. },
  183. "GWH191-5270": {
  184. "p_max_observed": 4916.6,
  185. "torque_limit": None,
  186. "spd_limit": 709.99,
  187. "baseline_pitch": 0.29
  188. },
  189. "GWH191-6700": {
  190. "p_max_observed": 5905.7,
  191. "torque_limit": None,
  192. "spd_limit": 685.9,
  193. "baseline_pitch": 0.08
  194. },
  195. "MY2.0se-121": {
  196. "p_max_observed": 2079.0,
  197. "torque_limit": None,
  198. "spd_limit": 1798.0,
  199. "baseline_pitch": 0.03
  200. },
  201. "MY3.0se-145": {
  202. "p_max_observed": 3212.0,
  203. "torque_limit": None,
  204. "spd_limit": 316.0,
  205. "baseline_pitch": 0.07
  206. },
  207. "MY4.0se-166": {
  208. "p_max_observed": 4160.0,
  209. "torque_limit": None,
  210. "spd_limit": 1095.0,
  211. "baseline_pitch": -0.45
  212. },
  213. "MY5.0se-155": {
  214. "p_max_observed": 5551.0,
  215. "torque_limit": None,
  216. "spd_limit": 284.6,
  217. "baseline_pitch": 0.0
  218. },
  219. "MY5.0se-166": {
  220. "p_max_observed": 5162.0,
  221. "torque_limit": 102.4,
  222. "spd_limit": 1031.1,
  223. "baseline_pitch": -0.49
  224. },
  225. "MY5.2se-166": {
  226. "p_max_observed": 5242.0,
  227. "torque_limit": None,
  228. "spd_limit": None,
  229. "baseline_pitch": -0.37
  230. },
  231. "MY6.25se-172": {
  232. "p_max_observed": 6346.0,
  233. "torque_limit": None,
  234. "spd_limit": 228.6,
  235. "baseline_pitch": -0.49
  236. },
  237. "S48-750": {
  238. "p_max_observed": 780.0,
  239. "torque_limit": None,
  240. "spd_limit": 1524.0,
  241. "baseline_pitch": 0.0
  242. },
  243. "SL1500-82": {
  244. "p_max_observed": 1544.3,
  245. "torque_limit": None,
  246. "spd_limit": 1841.5,
  247. "baseline_pitch": 0.01
  248. },
  249. "SL3000-113": {
  250. "p_max_observed": 3055.7,
  251. "torque_limit": None,
  252. "spd_limit": 1229.26,
  253. "baseline_pitch": 0.02
  254. },
  255. "SWT7000-154": {
  256. "p_max_observed": 7163.4,
  257. "torque_limit": None,
  258. "spd_limit": 10.8,
  259. "baseline_pitch": -1.69
  260. },
  261. "UP1500-82": {
  262. "p_max_observed": 1502.4,
  263. "torque_limit": 103.87,
  264. "spd_limit": 1777.1,
  265. "baseline_pitch": 0.03
  266. },
  267. "UP1500-86": {
  268. "p_max_observed": 1558.4,
  269. "torque_limit": 103.53,
  270. "spd_limit": 1773.6,
  271. "baseline_pitch": 0.03
  272. },
  273. "V100-2000": {
  274. "p_max_observed": 1999.8,
  275. "torque_limit": None,
  276. "spd_limit": 1734.83,
  277. "baseline_pitch": -2.7
  278. },
  279. "V110-2200": {
  280. "p_max_observed": 2204.1,
  281. "torque_limit": None,
  282. "spd_limit": 1757.7,
  283. "baseline_pitch": 0.9
  284. },
  285. "V52-850": {
  286. "p_max_observed": 854.8,
  287. "torque_limit": None,
  288. "spd_limit": 1693.8,
  289. "baseline_pitch": -1.97
  290. },
  291. "V60-850": {
  292. "p_max_observed": 853.2,
  293. "torque_limit": None,
  294. "spd_limit": 1673.9,
  295. "baseline_pitch": -3.06
  296. },
  297. "V80-2000": {
  298. "p_max_observed": 2043.9,
  299. "torque_limit": None,
  300. "spd_limit": 1748.7,
  301. "baseline_pitch": -1.74
  302. },
  303. "V90-2000": {
  304. "p_max_observed": 2005.7,
  305. "torque_limit": None,
  306. "spd_limit": 1730.4,
  307. "baseline_pitch": -2.16
  308. },
  309. "W2000-111": {
  310. "p_max_observed": 2111.0,
  311. "torque_limit": 100.04,
  312. "spd_limit": 1872.0,
  313. "baseline_pitch": 0.18
  314. },
  315. "W2500-135": {
  316. "p_max_observed": 2566.0,
  317. "torque_limit": None,
  318. "spd_limit": 1504.6,
  319. "baseline_pitch": 0.0
  320. },
  321. "W3450-146": {
  322. "p_max_observed": 3533.0,
  323. "torque_limit": None,
  324. "spd_limit": 1604.8,
  325. "baseline_pitch": 0.2
  326. },
  327. "XE122-2500": {
  328. "p_max_observed": 2568.0,
  329. "torque_limit": None,
  330. "spd_limit": 14.32,
  331. "baseline_pitch": 0.5
  332. },
  333. "XE72-2000": {
  334. "p_max_observed": 2084.0,
  335. "torque_limit": None,
  336. "spd_limit": 23.58,
  337. "baseline_pitch": -0.27
  338. }
  339. }
  340. def get_model_stats(model_name: str) -> dict:
  341. """根据机型名称获取对应的阈值字典"""
  342. thresh = _THRESHOLD_CACHE
  343. if model_name not in thresh:
  344. raise ValueError(f"未找到机型 {model_name} 的阈值配置")
  345. return thresh[model_name]
  346. # ==================== 核心打标函数 ====================
  347. def _label_dataframe(
  348. df_input: pd.DataFrame,
  349. stats: Optional[dict] = None,
  350. model_name: Optional[str] = None
  351. ) -> pd.DataFrame:
  352. """
  353. 内部打标函数,返回包含所有中间列(d_val_* 等)的完整 DataFrame。
  354. 参数含义同 apply_labels。
  355. """
  356. if stats is None:
  357. if model_name is None:
  358. raise ValueError("必须提供 stats 或 model_name")
  359. stats = get_model_stats(model_name)
  360. df = df_input.copy()
  361. P_MAX = stats["p_max_observed"]
  362. PITCH_BASE = stats["baseline_pitch"]
  363. # ── 1. 传感器异常列 ────────────────────────────────────────────────────────
  364. df["d_val_power"] = (
  365. (df["p_active"] > P_MAX * STATUS_POWER_UPPER_RATIO) |
  366. (df["p_active"] < P_MAX * STATUS_POWER_LOWER_RATIO)
  367. )
  368. if "wind_spd" in df.columns:
  369. df["d_val_wind"] = (df["wind_spd"] > STATUS_WIND_MAX) | (df["wind_spd"] < STATUS_WIND_MIN)
  370. else:
  371. df["d_val_wind"] = False
  372. pitch_cols = [c for c in df.columns if "pitch_ang_act" in c]
  373. df["d_val_pitch"] = False
  374. for col in pitch_cols:
  375. df["d_val_pitch"] |= (df[col] > STATUS_PITCH_MAX) | (df[col] < STATUS_PITCH_MIN)
  376. df["d_val_spd"] = False
  377. if stats["spd_limit"] and "gen_spd" in df.columns:
  378. df["d_val_spd"] = (
  379. (df["gen_spd"] > stats["spd_limit"] * STATUS_SPD_UPPER_RATIO) |
  380. (df["gen_spd"] < -200)
  381. )
  382. df["d_val_torque"] = False
  383. if stats["torque_limit"] and "actual_torque" in df.columns:
  384. df["d_val_torque"] = (
  385. (df["actual_torque"] > stats["torque_limit"] * STATUS_TORQUE_UPPER_RATIO) |
  386. (df["actual_torque"] < STATUS_TORQUE_LOWER_ABS)
  387. )
  388. # ── 2. 逻辑悖论列 ──────────────────────────────────────────────────────────
  389. if "wind_spd" in df.columns:
  390. df["d_logic_wind_pwr"] = (
  391. (df["wind_spd"] < STATUS_LOGIC_WIND_MIN) &
  392. (df["p_active"] > STATUS_LOGIC_POWER_MIN)
  393. )
  394. # 风速冻结检测:滑动窗口内风速几乎不变但功率明显不为0(传感器卡死)
  395. wind_std = df["wind_spd"].rolling(window=60, min_periods=2).std()
  396. df["d_logic_wind_pwr"] |= (
  397. (wind_std < 0.05) &
  398. (df["p_active"] > STATUS_LOGIC_POWER_MIN)
  399. )
  400. # 机型特殊规则
  401. if model_name is not None:
  402. for wind_thresh, pwr_thresh in STATUS_MODEL_SPECIAL_RULES.get(model_name, []):
  403. df["d_logic_wind_pwr"] |= (
  404. (df["wind_spd"] < wind_thresh) & (df["p_active"] > pwr_thresh)
  405. )
  406. else:
  407. df["d_logic_wind_pwr"] = False
  408. df["d_logic_torque_pwr"] = False
  409. if stats["torque_limit"] and "actual_torque" in df.columns:
  410. df["d_logic_torque_pwr"] = (
  411. (df["p_active"] > P_MAX * 0.1) &
  412. (df["actual_torque"].abs() < stats["torque_limit"] * 0.01)
  413. )
  414. # ── 3. 汇总传感器异常标签 ───────────────────────────
  415. all_sensor_cols = list(_SENSOR_COL_LABEL.keys())
  416. existing = [c for c in all_sensor_cols if c in df.columns]
  417. if existing:
  418. tag_matrix = np.where(
  419. df[existing].values,
  420. np.array([_SENSOR_COL_LABEL[c] for c in existing]),
  421. "",
  422. )
  423. df["sensor_anomaly_tags"] = pd.Series(
  424. [",".join(t for t in row if t) for row in tag_matrix],
  425. index=df.index,
  426. )
  427. else:
  428. df["sensor_anomaly_tags"] = ""
  429. # ── 4. 业务状态打标 ────────────────────────────────────────────────────────
  430. any_sensor_anomaly = df[[c for c in existing if c in df.columns]].any(axis=1)
  431. df["label"] = "运行"
  432. shutdown_thresh = max(50.0, P_MAX * STATUS_SHUTDOWN_RATIO)
  433. mask_shutdown = (~df["d_val_power"]) & (df["p_active"] <= shutdown_thresh)
  434. df.loc[mask_shutdown, "label"] = "停机"
  435. if "pitch_ang_act_1" in df.columns:
  436. mask_curtail = (
  437. (df["label"] != "停机") &
  438. (~df["d_val_power"]) &
  439. (~df["d_val_pitch"]) &
  440. (df["p_active"] > P_MAX * STATUS_CURTAIL_LOW_RATIO) &
  441. (df["p_active"] < P_MAX * STATUS_CURTAIL_HIGH_RATIO) &
  442. (df["pitch_ang_act_1"] > (PITCH_BASE + STATUS_CURTAIL_PITCH_OFFSET))
  443. )
  444. df.loc[mask_curtail, "label"] = "限功率"
  445. # 传感器异常覆盖
  446. mask_sensor = any_sensor_anomaly
  447. df.loc[mask_sensor, "label"] = "传感器异常"
  448. # 新增 sensor_tag 列:7位整数,每位1=正常,2=异常
  449. # 7位整数从左至右 分别为功率值异常(最高位)、风速值异常、变桨值异常、转速值异常、扭矩值异常、风速功率逻辑异常、转速扭矩逻辑异常(最低位)
  450. sensor_order = [
  451. 'd_val_power', 'd_val_wind', 'd_val_pitch', 'd_val_spd', 'd_val_torque', 'd_logic_wind_pwr',
  452. 'd_logic_torque_pwr'
  453. ]
  454. # 确保这些列都存在
  455. tag_str_series = df[sensor_order].apply(
  456. lambda row: ''.join('2' if x else '1' for x in row), axis=1
  457. )
  458. df['sensor_tag'] = tag_str_series.astype(int)
  459. return df
  460. def apply_labels(
  461. df: pd.DataFrame,
  462. model_name: str
  463. ) -> pd.DataFrame:
  464. """
  465. 便捷打标函数:传入原始 DataFrame 和机型名称,返回打标后的 DataFrame。
  466. 返回的 DataFrame 包含原始数据的所有列,以及新增的 `label` 列(最终业务状态)。
  467. 参数
  468. ----------
  469. df : pd.DataFrame
  470. 原始数据,必须包含 p_active 列,可选 wind_spd, pitch_ang_act_1, gen_spd, actual_torque 等。
  471. model_name : str
  472. 机型名称(需与静态数据中的机型名称一致)。
  473. 返回
  474. -------
  475. pd.DataFrame
  476. 包含原始数据列 + label 列的 DataFrame。
  477. """
  478. # 调用内部打标函数,获取完整结果(包含中间列)
  479. full_labeled = _label_dataframe(df, model_name=model_name)
  480. # 只保留原始列和 label 列
  481. result_cols = list(df.columns) + ['label', 'sensor_tag']
  482. return full_labeled[result_cols]