powerScatter2DAnalyst.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import os
  2. from datetime import datetime
  3. import numpy as np
  4. import pandas as pd
  5. import plotly.graph_objects as go
  6. from algorithmContract.confBusiness import *
  7. from algorithmContract.contract import Contract
  8. from behavior.analystWithGoodBadPoint import AnalystWithGoodBadPoint
  9. from plotly.subplots import make_subplots
  10. class PowerScatter2DAnalyst(AnalystWithGoodBadPoint):
  11. """
  12. 风电机组功率曲线散点分析。
  13. 秒级scada数据运算太慢,建议使用分钟级scada数据
  14. """
  15. def typeAnalyst(self):
  16. return "power_scatter_2D"
  17. def selectColumns(self):
  18. return [Field_DeviceCode, Field_Time, Field_WindSpeed, Field_ActiverPower]
  19. def addPropertyToDataFrame(self,dataFrameOfTurbine : pd.DataFrame, currTurbineInfo : pd.Series, currTurbineModelInfo : pd.Series):
  20. dataFrameOfTurbine[Field_PowerFarmCode] = self.currPowerFarmInfo[Field_PowerFarmCode]
  21. dataFrameOfTurbine[Field_MillTypeCode] = currTurbineModelInfo[Field_MillTypeCode]
  22. def turbinesAnalysis(self, outputAnalysisDir, conf: Contract, turbineCodes):
  23. dictionary = self.processTurbineData(turbineCodes, conf, self.selectColumns())
  24. dataFrame = self.userDataFrame(dictionary, conf.dataContract.configAnalysis, self)
  25. turbineInfos = self.common.getTurbineInfos(conf.dataContract.dataFilter.powerFarmID, turbineCodes, self.turbineInfo)
  26. if len(dataFrame) <= 0:
  27. print("After screening for blade pitch angle less than the configured value, plot power curve scatter points without data")
  28. return
  29. grouped = self.dataFrameContractOfTurbine.groupby(
  30. [Field_PowerFarmCode, Field_MillTypeCode])
  31. for groupByKey, contractPowerCurveOfMillType in grouped:
  32. break
  33. return self.drawOfPowerCurveScatter(dataFrame, turbineInfos,outputAnalysisDir, conf, contractPowerCurveOfMillType)
  34. def drawOfPowerCurveScatter(self, dataFrame: pd.DataFrame, turbineModelInfo: pd.Series, outputAnalysisDir, conf: Contract, dataFrameGuaranteePowerCurve: pd.DataFrame):
  35. """
  36. 绘制风速-功率分布图并保存为文件。
  37. 参数:
  38. dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
  39. csvPowerCurveFilePath (str): 功率曲线文件路径。
  40. outputAnalysisDir (str): 分析输出目录。
  41. confData (ConfBusiness): 配置
  42. """
  43. x_name = '风速'
  44. y_name = '功率'
  45. #机型切入风速 series
  46. cutInWsField = self.turbineModelInfo[Field_CutInWS]
  47. cut_in_ws = cutInWsField.min() - 1 if cutInWsField.notna().any() else 2
  48. # if not dataFrame.empty and Field_CutInWS in dataFrame.columns and dataFrame[Field_CutInWS].notna().any():
  49. # cut_in_ws = dataFrame[Field_CutInWS].min() - 1
  50. # else:
  51. # cut_in_ws = 2
  52. # 按设备名分组数据
  53. grouped = dataFrame.groupby([Field_NameOfTurbine, Field_CodeOfTurbine])
  54. result_rows = []
  55. # 定义固定的颜色映射列表
  56. fixed_colors = [
  57. "#3E409C",
  58. "#476CB9",
  59. "#3586BF",
  60. "#4FA4B5",
  61. "#52A3AE",
  62. "#60C5A3",
  63. "#85D0AE",
  64. "#A8DCA2",
  65. "#CFEE9E",
  66. "#E4F39E",
  67. "#EEF9A7",
  68. "#FBFFBE",
  69. "#FDF1A9",
  70. "#FFE286",
  71. "#FFC475",
  72. "#FCB06C",
  73. "#F78F4F",
  74. "#F96F4A",
  75. "#E4574C",
  76. "#CA3756",
  77. "#AF254F"
  78. ]
  79. # 将 fixed_colors 转换为 Plotly 的 colorscale 格式
  80. fixed_colorscale = [
  81. [i / (len(fixed_colors) - 1), color] for i, color in enumerate(fixed_colors)
  82. ]
  83. fixed_colors_points = [
  84. "#F96F4A",
  85. "#FFC475",
  86. "#FBFFBE",
  87. "#85D0AE",
  88. "#3586BF",
  89. "#3E409C"
  90. ]
  91. # 遍历每个设备的数据
  92. for name, group in grouped:
  93. fig = make_subplots()
  94. # 提取月份
  95. group['month'] = group['monthIntTime'].apply(lambda x: datetime.fromtimestamp(x).month)
  96. unique_months = group['month'].unique()
  97. # 计算时间跨度
  98. time_span_months = len(unique_months)
  99. if time_span_months >= 6:
  100. # 绘制散点图(时间跨度大于等于6个月)
  101. scatter = go.Scatter(x=group[Field_WindSpeed],
  102. y=group[Field_ActiverPower],
  103. mode='markers',
  104. marker=dict(
  105. color=group['monthIntTime'],
  106. colorscale=fixed_colorscale, # 使用自定义的 colorscale
  107. size=3,
  108. opacity=0.7,
  109. colorbar=dict(
  110. tickvals=np.linspace(
  111. group['monthIntTime'].min(), group['monthIntTime'].max(), 6),
  112. ticktext=[datetime.fromtimestamp(ts).strftime(
  113. '%Y-%m') for ts in np.linspace(group['monthIntTime'].min(), group['monthIntTime'].max(), 6)],
  114. thickness=18,
  115. len=1, # 设置颜色条的长度,使其占据整个图的高度
  116. outlinecolor='rgba(255,255,255,0)'
  117. ),
  118. showscale=True
  119. ),
  120. showlegend=False) # 不显示散点图的 legend,用 colorbar 代替
  121. fig.add_trace(scatter)
  122. else:
  123. # 绘制散点图(时间跨度小于6个月)
  124. for i, month in enumerate(unique_months):
  125. month_data = group[group['month'] == month]
  126. # 使用固定的颜色列表
  127. color = fixed_colors_points[i % len(fixed_colors_points)]
  128. scatter = go.Scatter(x=month_data[Field_WindSpeed],
  129. y=month_data[Field_ActiverPower],
  130. mode='markers',
  131. marker=dict(
  132. color=color,
  133. size=3,
  134. opacity=0.7
  135. ),
  136. name=f'{datetime.fromtimestamp(month_data["monthIntTime"].iloc[0]).strftime("%Y-%m")}',
  137. showlegend=True)
  138. fig.add_trace(scatter)
  139. # 绘制合同功率曲线
  140. line = go.Scatter(x=dataFrameGuaranteePowerCurve[Field_WindSpeed],
  141. y=dataFrameGuaranteePowerCurve[Field_ActiverPower],
  142. mode='lines+markers',
  143. marker=dict(color='gray', size=7),
  144. name='合同功率曲线')
  145. fig.add_trace(line, secondary_y=False)
  146. # 设置图形布局
  147. fig.update_layout(
  148. title=f'机组: {name[0]}',
  149. xaxis=dict(title=x_name,
  150. range=[cut_in_ws, 25],
  151. tickmode='linear', tick0=0, dtick=1,
  152. tickangle=-45),
  153. yaxis=dict(title=y_name,
  154. dtick=self.axisStepActivePower,
  155. range=[self.axisLowerLimitActivePower,
  156. self.axisUpperLimitActivePower]
  157. ),
  158. legend=dict(yanchor="bottom", y=0, xanchor="right", x=1, font=dict(
  159. size=10), bgcolor='rgba(255,255,255,0)')
  160. )
  161. # 确保从 Series 中提取的是具体的值
  162. engineTypeCode = turbineModelInfo.get(Field_MillTypeCode, "")
  163. if isinstance(engineTypeCode, pd.Series):
  164. engineTypeCode = engineTypeCode.iloc[0]
  165. engineTypeName = turbineModelInfo.get(Field_MachineTypeCode, "")
  166. if isinstance(engineTypeName, pd.Series):
  167. engineTypeName = engineTypeName.iloc[0]
  168. # 使用 apply() 对每个元素调用 datetime.fromtimestamp
  169. group['monthIntTime'] = group['monthIntTime'].apply(lambda x: datetime.fromtimestamp(x).strftime('%Y-%m'))
  170. # 定义要替换的空值类型
  171. na_values = {pd.NA, float('nan')}
  172. # 构建最终的JSON对象
  173. json_output = {
  174. "analysisTypeCode": "逐月有功功率散点2D分析",
  175. "engineCode": engineTypeCode,
  176. "engineTypeName": engineTypeName,
  177. "xaixs": "风速(m/s)",
  178. "yaixs": "有功功率(kW)",
  179. "data": [
  180. {# 提取机组数据
  181. "engineName": name[0],
  182. "engineCode": name[1],
  183. "title":f' 逐月有功功率散点2D分析-机组: {name[0]}',
  184. "xData": group[Field_WindSpeed].replace(na_values, None).tolist(),
  185. "xrange":[cut_in_ws, 25],
  186. "yData": group[Field_ActiverPower].replace(na_values, None).tolist(),
  187. "yrange":[self.axisLowerLimitActivePower,self.axisUpperLimitActivePower],
  188. "colorbar": group['monthIntTime'].tolist(),
  189. "colorbartitle": "年月",
  190. "mode":"markers"
  191. },
  192. {# 提取合同功率曲线数据
  193. "enginName": "合同功率曲线",
  194. "xData":dataFrameGuaranteePowerCurve[Field_WindSpeed].replace(na_values, None).tolist(),
  195. "yData":dataFrameGuaranteePowerCurve[Field_ActiverPower].replace(na_values, None).tolist(),
  196. "zData": [],
  197. "mode":"lines+markers"
  198. }]
  199. }
  200. # 保存图像
  201. # pngFileName = f"{name[0]}-scatter.png"
  202. # pngFilePath = os.path.join(outputAnalysisDir, pngFileName)
  203. # fig.write_image(pngFilePath, scale=3)
  204. # # 保存HTML
  205. # htmlFileName = f"{name[0]}-scatter.html"
  206. # htmlFilePath = os.path.join(outputAnalysisDir, htmlFileName)
  207. # fig.write_html(htmlFilePath)
  208. # 将JSON对象保存到文件
  209. output_json_path = os.path.join(outputAnalysisDir, f"{name[0]}-scatter.json")
  210. with open(output_json_path, 'w', encoding='utf-8') as f:
  211. import json
  212. json.dump(json_output, f, ensure_ascii=False, indent=4)
  213. # 如果需要返回DataFrame,可以包含文件路径
  214. result_rows.append({
  215. Field_Return_TypeAnalyst: self.typeAnalyst(),
  216. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  217. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  218. Field_CodeOfTurbine: name[1],
  219. Field_Return_FilePath: output_json_path,
  220. Field_Return_IsSaveDatabase: True
  221. })
  222. # result_rows.append({
  223. # Field_Return_TypeAnalyst: self.typeAnalyst(),
  224. # Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  225. # Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  226. # Field_CodeOfTurbine: name[1],
  227. # Field_Return_FilePath: pngFilePath,
  228. # Field_Return_IsSaveDatabase: False
  229. # })
  230. # result_rows.append({
  231. # Field_Return_TypeAnalyst: self.typeAnalyst(),
  232. # Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  233. # Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  234. # Field_CodeOfTurbine: name[1],
  235. # Field_Return_FilePath: htmlFilePath,
  236. # Field_Return_IsSaveDatabase: True
  237. # })
  238. result_df = pd.DataFrame(result_rows)
  239. return result_df