pitchPowerAnalyst.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. import os
  2. from datetime import datetime
  3. import numpy as np
  4. import pandas as pd
  5. import plotly.express as px
  6. import plotly.graph_objects as go
  7. from algorithmContract.confBusiness import *
  8. from algorithmContract.contract import Contract
  9. from behavior.analystWithGoodBadPoint import AnalystWithGoodBadPoint
  10. class PitchPowerAnalyst(AnalystWithGoodBadPoint):
  11. """
  12. 风电机组变桨-功率分析
  13. """
  14. def typeAnalyst(self):
  15. return "pitch_power"
  16. def selectColumns(self):
  17. return [Field_DeviceCode, Field_Time,Field_WindSpeed, Field_ActiverPower, Field_PitchAngel1]
  18. def turbinesAnalysis(self, outputAnalysisDir, conf: Contract, turbineCodes):
  19. dictionary = self.processTurbineData(turbineCodes, conf,self.selectColumns())
  20. dataFrame = self.userDataFrame(dictionary,conf.dataContract.configAnalysis,self)
  21. result_df1 = self.plot_power_pitch_angle(dataFrame, outputAnalysisDir, conf)
  22. result_df2 = self.drawScatterGraph(dataFrame, outputAnalysisDir, conf)
  23. result_df = pd.concat([result_df1, result_df2], ignore_index=True)
  24. return result_df
  25. def plot_power_pitch_angle(self, dataFrame:pd.DataFrame, outputAnalysisDir:str, conf: Contract):
  26. # 按设备名分组数据
  27. dataFrameMerge = dataFrame[(dataFrame[Field_ActiverPower] > 0)].sort_values(by=Field_YearMonth)
  28. grouped = dataFrameMerge.groupby([Field_NameOfTurbine, Field_CodeOfTurbine])
  29. # 定义固定的颜色映射列表
  30. fixed_colors = [
  31. "#3E409C",
  32. "#476CB9",
  33. "#3586BF",
  34. "#4FA4B5",
  35. "#52A3AE",
  36. "#60C5A3",
  37. "#85D0AE",
  38. "#A8DCA2",
  39. "#CFEE9E",
  40. "#E4F39E",
  41. "#EEF9A7",
  42. "#FBFFBE",
  43. "#FDF1A9",
  44. "#FFE286",
  45. "#FFC475",
  46. "#FCB06C",
  47. "#F78F4F",
  48. "#F96F4A",
  49. "#E4574C",
  50. "#CA3756",
  51. "#AF254F"
  52. ]
  53. # 将 fixed_colors 转换为 Plotly 的 colorscale 格式
  54. fixed_colorscale = [
  55. [i / (len(fixed_colors) - 1), color] for i, color in enumerate(fixed_colors)
  56. ]
  57. # 遍历每个设备并绘制散点图
  58. result_rows1 = []
  59. for name, group in grouped:
  60. # 创建图形
  61. fig = go.Figure()
  62. # 添加散点图
  63. fig.add_trace(go.Scatter(
  64. x=group[Field_ActiverPower],
  65. y=group[Field_PitchAngel1],
  66. mode='markers',
  67. # marker=dict(color='blue', size=3.5)
  68. marker=dict(
  69. color=group[Field_UnixYearMonth],
  70. colorscale=fixed_colorscale,
  71. size=3,
  72. opacity=0.7,
  73. colorbar=dict(
  74. tickvals=np.linspace(
  75. group[Field_UnixYearMonth].min(), group[Field_UnixYearMonth].max(), 6),
  76. ticktext=[datetime.fromtimestamp(ts).strftime('%Y-%m') for ts in np.linspace(
  77. group[Field_UnixYearMonth].min(), group[Field_UnixYearMonth].max(), 6)],
  78. thickness=18,
  79. len=1, # 设置颜色条的长度,使其占据整个图的高度
  80. outlinecolor='rgba(255,255,255,0)'
  81. ),
  82. showscale=True
  83. ),
  84. showlegend=False
  85. ))
  86. # 设置图形布局
  87. fig.update_layout(
  88. title=f'机组: {name[0]}',
  89. xaxis=dict(
  90. title='功率',
  91. range=[self.axisLowerLimitActivePower,
  92. self.axisUpperLimitActivePower],
  93. dtick=self.axisStepActivePower,
  94. tickangle=-45 # 设置x轴刻度值旋转角度为45度,如果需要
  95. ),
  96. yaxis=dict(
  97. title='桨距角',
  98. range=[self.axisLowerLimitPitchAngle,
  99. self.axisUpperLimitPitchAngle],
  100. dtick=self.axisStepPitchAngle
  101. ),
  102. coloraxis=dict(
  103. colorbar=dict(
  104. title="时间",
  105. ticks="outside",
  106. len=1, # 设置颜色条的长度,使其占据整个图的高度
  107. thickness=20, # 调整颜色条的宽度
  108. orientation='v', # 设置颜色条为垂直方向
  109. tickmode='array', # 确保刻度按顺序排列
  110. tickvals=dataFrameMerge[Field_YearMonth].unique(
  111. ).tolist(), # 确保刻度为唯一的年月
  112. ticktext=dataFrameMerge[Field_YearMonth].unique(
  113. ).tolist() # 以%Y-%m格式显示标签
  114. )
  115. )
  116. )
  117. # 保存图像
  118. filePathOfImage = os.path.join(outputAnalysisDir, f"{name[0]}.png")
  119. fig.write_image(filePathOfImage, width=800, height=600, scale=3)
  120. filePathOfHtml = os.path.join(outputAnalysisDir, f"{name[0]}.html")
  121. fig.write_html(filePathOfHtml)
  122. result_rows1.append({
  123. Field_Return_TypeAnalyst: self.typeAnalyst(),
  124. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  125. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  126. Field_CodeOfTurbine: name[1],
  127. Field_Return_FilePath: filePathOfImage,
  128. Field_Return_IsSaveDatabase: False
  129. })
  130. result_rows1.append({
  131. Field_Return_TypeAnalyst: self.typeAnalyst(),
  132. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  133. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  134. Field_CodeOfTurbine: name[1],
  135. Field_Return_FilePath: filePathOfHtml,
  136. Field_Return_IsSaveDatabase: True
  137. })
  138. result_df1 = pd.DataFrame(result_rows1)
  139. return result_df1
  140. def drawScatterGraph(self, dataFrame: pd.DataFrame, outputAnalysisDir: str, conf: Contract):
  141. dataFrame = dataFrame[(dataFrame[Field_ActiverPower] > 0)].sort_values(
  142. by=Field_YearMonth)
  143. grouped = dataFrame.groupby([Field_NameOfTurbine, Field_CodeOfTurbine])
  144. '''
  145. # 遍历每个设备的数据
  146. result_rows2 = []
  147. for name, group in grouped:
  148. if len(group[Field_YearMonth].unique()) > 1:
  149. fig = px.scatter_3d(dataFrame,
  150. x=Field_PitchAngel1,
  151. y=Field_YearMonth,
  152. z=Field_ActiverPower,
  153. color=Field_YearMonth,
  154. labels={Field_PitchAngel1: '桨距角',
  155. Field_YearMonth: '时间', Field_ActiverPower: '功率'},
  156. )
  157. # 设置固定散点大小
  158. fig.update_traces(marker=dict(size=1.5))
  159. # 更新图形的布局
  160. fig.update_layout(
  161. title={
  162. "text": f'月度桨距角功率3D散点图: {name[0]}',
  163. "x": 0.5
  164. },
  165. scene=dict(
  166. xaxis=dict(
  167. title='桨距角',
  168. dtick=self.axisStepPitchAngle,
  169. range=[self.axisLowerLimitPitchAngle,
  170. self.axisUpperLimitPitchAngle],
  171. ),
  172. yaxis=dict(
  173. title='时间',
  174. tickformat='%Y-%m', # 日期格式,
  175. dtick='M1', # 每月一个刻度
  176. showgrid=True, # 显示网格线
  177. ),
  178. zaxis=dict(
  179. title='功率',
  180. dtick=self.axisStepActivePower,
  181. range=[self.axisLowerLimitActivePower,
  182. self.axisUpperLimitActivePower],
  183. showgrid=True, # 显示网格线
  184. )
  185. ),
  186. scene_camera=dict(
  187. up=dict(x=0, y=0, z=1), # 保持相机向上方向不变
  188. center=dict(x=0, y=0, z=0), # 保持相机中心位置不变
  189. eye=dict(x=-1.8, y=-1.8, z=1.2) # 调整eye属性以实现水平旋转180°
  190. ),
  191. # 设置图例标题
  192. # legend_title_text='Time',
  193. legend=dict(
  194. orientation="h",
  195. itemsizing="constant", # Use constant size for legend items
  196. itemwidth=80 # Set the width of legend items to 50 pixels
  197. )
  198. )
  199. '''
  200. # 假设 colorsList 已经在代码的其他部分定义
  201. colorsList = [
  202. "#3E409C",
  203. "#3586BF",
  204. "#52A3AE",
  205. "#85D0AE",
  206. "#A8DCA2",
  207. "#FBFFBE",
  208. "#FDF1A9",
  209. "#FFE286",
  210. "#FCB06C",
  211. "#F96F4A",
  212. "#E4574C",
  213. "#AF254F"
  214. ]
  215. # 遍历每个设备的数据
  216. result_rows2 = []
  217. for name, group in grouped:
  218. if len(group[Field_YearMonth].unique()) > 1:
  219. fig = px.scatter_3d(
  220. group,
  221. x=Field_PitchAngel1,
  222. y=Field_YearMonth,
  223. z=Field_ActiverPower,
  224. color=Field_YearMonth,
  225. color_discrete_sequence=colorsList, # 使用 colorsList 作为颜色映射
  226. labels={
  227. Field_PitchAngel1: '桨距角',
  228. Field_YearMonth: '时间',
  229. Field_ActiverPower: '功率'
  230. },
  231. )
  232. # 设置固定散点大小
  233. fig.update_traces(marker=dict(size=1.5))
  234. # 更新图形的布局
  235. fig.update_layout(
  236. title={
  237. "text": f'月度桨距角功率3D散点图: {name[0]}',
  238. "x": 0.5
  239. },
  240. scene=dict(
  241. xaxis=dict(
  242. title='桨距角',
  243. dtick=self.axisStepPitchAngle,
  244. range=[self.axisLowerLimitPitchAngle, self.axisUpperLimitPitchAngle],
  245. ),
  246. yaxis=dict(
  247. title='时间',
  248. tickformat='%Y-%m', # 日期格式,
  249. dtick='M1', # 每月一个刻度
  250. showgrid=True, # 显示网格线
  251. ),
  252. zaxis=dict(
  253. title='功率',
  254. dtick=self.axisStepActivePower,
  255. range=[self.axisLowerLimitActivePower, self.axisUpperLimitActivePower],
  256. showgrid=True, # 显示网格线
  257. )
  258. ),
  259. scene_camera=dict(
  260. up=dict(x=0, y=0, z=1), # 保持相机向上方向不变
  261. center=dict(x=0, y=0, z=0), # 保持相机中心位置不变
  262. eye=dict(x=-1.8, y=-1.8, z=1.2) # 调整eye属性以实现水平旋转180°
  263. ),
  264. legend=dict(
  265. orientation="h",
  266. itemsizing="constant", # Use constant size for legend items
  267. itemwidth=80 # Set the width of legend items to 50 pixels
  268. )
  269. )
  270. # 保存图像
  271. filePathOfHtml = os.path.join(
  272. outputAnalysisDir, f"{name[0]}_3D.html")
  273. fig.write_html(filePathOfHtml)
  274. result_rows2.append({
  275. Field_Return_TypeAnalyst: self.typeAnalyst(),
  276. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  277. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  278. Field_CodeOfTurbine: name[1],
  279. Field_Return_FilePath: filePathOfHtml,
  280. Field_Return_IsSaveDatabase: True
  281. })
  282. result_df2 = pd.DataFrame(result_rows2)
  283. return result_df2
  284. # self.drawScatterGraphOfTurbine(
  285. # group, outputAnalysisDir, conf, name)
  286. # def drawScatterGraphOfTurbine(self, dataFrame: pd.DataFrame, outputAnalysisDir: str, conf: Contract, turbineName: str):
  287. # # 创建3D散点图
  288. # fig = px.scatter_3d(dataFrame,
  289. # x=Field_PitchAngel1,
  290. # y=Field_YearMonth,
  291. # z=Field_ActiverPower,
  292. # color=Field_YearMonth,
  293. # labels={Field_PitchAngel1: 'Pitch Angle',
  294. # Field_YearMonth: 'Time', Field_ActiverPower: 'Power'},
  295. # )
  296. # # 设置固定散点大小
  297. # fig.update_traces(marker=dict(size=1.5))
  298. # # 更新图形的布局
  299. # fig.update_layout(
  300. # title={
  301. # "text": f'Monthly Pitch-Power 3D Scatter Plot: {turbineName}',
  302. # "x": 0.5
  303. # },
  304. # scene=dict(
  305. # xaxis=dict(
  306. # title='Pitch Angle',
  307. # range=[conf.dataContract.graphSets["pitchAngle"]["min"] if not self.common.isNone(conf.dataContract.graphSets["pitchAngle"]["min"]) else -2,
  308. # conf.dataContract.graphSets["pitchAngle"]["max"] if not self.common.isNone(conf.dataContract.graphSets["pitchAngle"]["max"]) else 28],
  309. # dtick=conf.dataContract.graphSets["pitchAngle"]["step"] if not self.common.isNone(conf.dataContract.graphSets["pitchAngle"]["step"]) else 2,
  310. # ),
  311. # yaxis=dict(
  312. # title='Time',
  313. # tickformat='%Y-%m', # 日期格式,
  314. # dtick='M1', # 每月一个刻度
  315. # showgrid=True, # 显示网格线
  316. # ),
  317. # zaxis=dict(
  318. # title='Power',
  319. # dtick=conf.dataContract.graphSets["activePower"]["step"] if not self.common.isNone(
  320. # conf.dataContract.graphSets["activePower"]) and not self.common.isNone(
  321. # conf.dataContract.graphSets["activePower"]["step"]) else 250,
  322. # range=[conf.dataContract.graphSets["activePower"]["min"] if not self.common.isNone(
  323. # conf.dataContract.graphSets["activePower"]["min"]) else 0, conf.dataContract.graphSets["activePower"]["max"] if not self.common.isNone(conf.dataContract.graphSets["activePower"]["max"]) else conf.rated_power*1.2],
  324. # showgrid=True, # 显示网格线
  325. # )
  326. # ),
  327. # scene_camera=dict(
  328. # up=dict(x=0, y=0, z=1), # 保持相机向上方向不变
  329. # center=dict(x=0, y=0, z=0), # 保持相机中心位置不变
  330. # eye=dict(x=-1.8, y=-1.8, z=1.2) # 调整eye属性以实现水平旋转180°
  331. # ),
  332. # # 设置图例标题
  333. # legend_title_text='Time'
  334. # )
  335. # # 保存图像
  336. # outputFileHtml = os.path.join(
  337. # outputAnalysisDir, "{}_3D.html".format(turbineName))
  338. # fig.write_html(outputFileHtml)
  339. """"
  340. def drawScatterGraph(self, dataFrame: pd.DataFrame, outputAnalysisDir, conf: Contract):
  341. ## 绘制变桨-功率分布图并保存为文件。
  342. ## 参数:
  343. ## dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
  344. ## outputAnalysisDir (str): 分析输出目录。
  345. ## conf (ConfBusiness): 配置
  346. ## 按设备名分组数据
  347. colorsList = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
  348. '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#aec7e8', '#ffbb78']
  349. grouped = dataFrame.groupby(Field_NameOfTurbine)
  350. # 遍历每个设备的数据
  351. for name, group in grouped:
  352. # 创建颜色映射,将每个年月映射到一个唯一的颜色
  353. unique_months = group[Field_YearMonth].unique()
  354. colors = [
  355. colorsList[i % 12] for i in range(len(unique_months))]
  356. color_map = dict(zip(unique_months, colors))
  357. # 使用go.Scatter3d创建3D散点图
  358. trace = go.Scatter3d(
  359. x=group[Field_PitchAngel1],
  360. y=group[Field_YearMonth],
  361. z=group[Field_ActiverPower],
  362. mode='markers',
  363. marker=dict(
  364. color=[color_map[month]
  365. for month in group[Field_YearMonth]],
  366. size=1.5,
  367. line=dict(
  368. color='rgba(0, 0, 0, 0)', # 设置边框颜色为透明,以去掉白色边框
  369. width=0 # 设置边框宽度为0,进一步确保没有边框
  370. ),
  371. opacity=0.8 # 调整散点的透明度,增加透视效果
  372. )
  373. )
  374. # 创建图形
  375. fig = go.Figure(data=[trace])
  376. # 更新图形的布局
  377. fig.update_layout(
  378. title={
  379. "text": f'三维散点图{name}',
  380. "x": 0.5
  381. },
  382. scene=dict(
  383. xaxis=dict(
  384. title='桨距角',
  385. dtick=conf.dataContract.graphSets["pitchAngle"]["step"] if not self.common.isNone(
  386. conf.dataContract.graphSets["pitchAngle"]["step"]) else 2, # 设置y轴刻度间隔为0.1
  387. range=[conf.dataContract.graphSets["pitchAngle"]["min"] if not self.common.isNone(
  388. conf.dataContract.graphSets["pitchAngle"]["min"]) else -2, conf.dataContract.graphSets["pitchAngle"]["max"] if not self.common.isNone(conf.dataContract.graphSets["pitchAngle"]["max"]) else 28], # 设置y轴的范围从0到1
  389. showgrid=True, # 显示网格线
  390. ),
  391. yaxis=dict(
  392. title='时间',
  393. tickmode='array',
  394. tickvals=unique_months,
  395. ticktext=unique_months,
  396. showgrid=True, # 显示网格线
  397. categoryorder='category ascending'
  398. ),
  399. zaxis=dict(
  400. title='功率',
  401. dtick=conf.dataContract.graphSets["activePower"]["step"] if not self.common.isNone(
  402. conf.dataContract.graphSets["activePower"]) and not self.common.isNone(
  403. conf.dataContract.graphSets["activePower"]["step"]) else 250,
  404. range=[conf.dataContract.graphSets["activePower"]["min"] if not self.common.isNone(
  405. conf.dataContract.graphSets["activePower"]["min"]) else 0, conf.dataContract.graphSets["activePower"]["max"] if not self.common.isNone(conf.dataContract.graphSets["activePower"]["max"]) else conf.rated_power*1.2],
  406. )
  407. ),
  408. scene_camera=dict(
  409. up=dict(x=0, y=0, z=1), # 保持相机向上方向不变
  410. center=dict(x=0, y=0, z=0), # 保持相机中心位置不变
  411. eye=dict(x=-1.8, y=-1.8, z=1.2) # 调整eye属性以实现水平旋转180°
  412. ),
  413. margin=dict(t=50, b=10) # t为顶部(top)间距,b为底部(bottom)间距
  414. )
  415. # 保存图像
  416. outputFileHtml = os.path.join(
  417. outputAnalysisDir, "{}.html".format(name))
  418. fig.write_html(outputFileHtml)
  419. """