yawErrorDensityAnalyst.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import os
  2. import pandas as pd
  3. import numpy as np
  4. import plotly.graph_objects as go
  5. from algorithmContract.confBusiness import *
  6. from algorithmContract.contract import Contract
  7. from behavior.analystWithGoodBadLimitPoint import AnalystWithGoodBadLimitPoint
  8. from scipy.stats import binned_statistic_2d
  9. from scipy.stats import skew, kurtosis
  10. from utils.jsonUtil import JsonUtil
  11. from scipy.stats import norm, gaussian_kde
  12. class YawErrorDensityAnalyst(AnalystWithGoodBadLimitPoint):
  13. """
  14. 风电机组动态偏航策略分析
  15. """
  16. def typeAnalyst(self):
  17. return "yaw_error_density"
  18. def selectColumns(self):
  19. return [Field_DeviceCode,Field_Time,Field_WindSpeed,Field_ActiverPower,Field_YawError]
  20. def turbinesAnalysis(self, outputAnalysisDir, conf: Contract, turbineCodes):
  21. dictionary = self.processTurbineData(turbineCodes,conf,self.selectColumns())
  22. dataFrameMerge = self.userDataFrame(dictionary,conf.dataContract.configAnalysis,self)
  23. turbineInfos = self.common.getTurbineInfos(conf.dataContract.dataFilter.powerFarmID, turbineCodes, self.turbineInfo)
  24. results=self.yawErrorAnalysis(dataFrameMerge, turbineInfos,outputAnalysisDir, conf)
  25. return results
  26. def yawErrorAnalysis(self, dataFrameMerge: pd.DataFrame, turbineModelInfo: pd.Series, outputAnalysisDir, conf: Contract):
  27. # 检查所需列是否存在
  28. required_columns = {Field_ActiverPower, Field_YawError,Field_WindSpeed}
  29. if not required_columns.issubset(dataFrameMerge.columns):
  30. raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
  31. result_rows = []
  32. grouped = dataFrameMerge.groupby(
  33. [Field_NameOfTurbine, Field_CodeOfTurbine])
  34. # 定义固定的颜色映射列表
  35. fixed_colors = [
  36. "#3E409C",
  37. "#476CB9",
  38. "#3586BF",
  39. "#4FA4B5",
  40. "#52A3AE",
  41. "#60C5A3",
  42. "#85D0AE",
  43. "#A8DCA2",
  44. "#CFEE9E",
  45. "#E4F39E",
  46. "#EEF9A7",
  47. "#FBFFBE",
  48. "#FDF1A9",
  49. "#FFE286",
  50. "#FFC475",
  51. "#FCB06C",
  52. "#F78F4F",
  53. "#F96F4A",
  54. "#E4574C",
  55. "#CA3756",
  56. "#AF254F"
  57. ]
  58. # 将 fixed_colors 转换为 Plotly 的 colorscale 格式
  59. fixed_colorscale = [
  60. [i / (len(fixed_colors) - 1), color] for i, color in enumerate(fixed_colors)
  61. ]
  62. for name, group in grouped:
  63. dataFrame=group[Field_YawError].abs() <= 45
  64. # yawerror=np.mean(dataFrame[Field_YawError])
  65. df = self.calculateYawError(group)
  66. # 这里的 dropna 会修改 df
  67. df.dropna(inplace=True)
  68. # 1. 之前加的防护:如果是空的,跳过
  69. if df.empty:
  70. # print(f"Warning: Turbine {name[0]} has no valid data after screening, skip the analysis.")
  71. continue
  72. counts = df['density'].value_counts()
  73. count_0 = counts.get(0, 0) # 获取 density 为 0 的数量,如果没有 0 则返回 0
  74. count_1 = counts.get(1, 0) # 获取 density 为 1 的数量,如果没有 1 则返回 0
  75. # 2. 这里的筛选可能再次导致 df 变空
  76. df = df[df["density"]>0]
  77. # 【重要】再次检查是否为空,或者数据量太少
  78. # KDE 至少需要 2 个点才能计算,且方差不能为 0
  79. if df.empty or len(df) < 2:
  80. # print(f"Warning: Turbine {name[0]} has insufficient data for density plot.")
  81. continue
  82. mean_X = np.mean(df["x"])
  83. std_X = np.std(df["x"])
  84. mediann_X= np.median(df["x"])
  85. skewness_X = skew(df["x"])
  86. kurtosis_X = kurtosis(df["x"])
  87. max_X = np.max(df["x"])
  88. min_X = np.min(df["x"])
  89. result={
  90. 'mean_X':[mean_X],
  91. 'std_X': [std_X],
  92. 'mediann_X':[mediann_X],
  93. 'skewness_X':[skewness_X],
  94. 'kurtosis_X':[kurtosis_X],
  95. 'max_X':[max_X],
  96. 'min_X':[min_X]
  97. }
  98. result = pd.DataFrame(result)
  99. # 用密度作为颜色绘制散点图
  100. fig = go.Figure()
  101. fig.add_trace(go.Scattergl(
  102. x=df["x"],
  103. y=df["y"],
  104. mode='markers',
  105. marker=dict(
  106. size=3,
  107. opacity=0.7,
  108. color=df["density"],
  109. colorscale=fixed_colorscale,
  110. showscale=True,
  111. )
  112. ))
  113. fig.update_layout(
  114. xaxis_title='对风角度',
  115. yaxis_title='风速',
  116. title=f'动态偏航误差分析-{name[0]}',
  117. xaxis=dict(range=[-20, 20]), # 限制横坐标范围为 -20 到 20
  118. yaxis=dict(range=[0, 25])
  119. )
  120. # 确保从 Series 中提取的是具体的值
  121. engineTypeCode = turbineModelInfo.get(Field_MillTypeCode, "")
  122. if isinstance(engineTypeCode, pd.Series):
  123. engineTypeCode = engineTypeCode.iloc[0]
  124. engineTypeName = turbineModelInfo.get(Field_MachineTypeCode, "")
  125. if isinstance(engineTypeName, pd.Series):
  126. engineTypeName = engineTypeName.iloc[0]
  127. # 构建最终的JSON对象
  128. json_output = {
  129. "analysisTypeCode": "动态偏航误差",
  130. "engineCode": engineTypeCode,
  131. "engineTypeName": engineTypeName,
  132. "xaixs": "对风角度(°)",
  133. "yaixs": "风速(m/s)",
  134. "data": [{
  135. "engineName": name[0],
  136. "engineCode": name[1],
  137. "title":f'动态偏航误差分析-{name[0]}',
  138. "xData": df["x"].tolist(),
  139. "yData": df["y"].tolist(),
  140. "colorbar": df["density"].tolist(),
  141. "colorbartitle": "密度"
  142. }]
  143. }
  144. x_range = np.linspace(-30, 30, 1000) # 先定义 x 轴
  145. try:
  146. # 尝试计算 KDE
  147. # 要求:数据点 > 1 且 方差 != 0
  148. if df["x"].nunique() > 1:
  149. kde = gaussian_kde(df["x"])
  150. pdf_data = kde(x_range)
  151. else:
  152. # 如果所有数据都一样(方差为0),KDE 无法计算
  153. # 直接给 0 或者抛出异常进入 except
  154. raise ValueError("Data variance is zero")
  155. except Exception as e:
  156. # print(f"KDE calculation failed for {name[0]}: {e}. Using zero distribution.")
  157. # 如果失败,生成全 0 的平直线,保证 JSON 结构完整
  158. pdf_data = np.zeros_like(x_range)
  159. # 构建最终的JSON对象2
  160. json_output2 = {
  161. "analysisTypeCode": "动态偏航误差",
  162. "engineCode": engineTypeCode,
  163. "engineTypeName": engineTypeName,
  164. "xaixs": "对风角度(°)",
  165. "yaixs": "概率密度函数",
  166. "data": [{
  167. "engineName": name[0],
  168. "engineCode": name[1],
  169. "title":f'概率密度函数-{name[0]}',
  170. "xData": x_range.tolist(),
  171. "yData": pdf_data.tolist(),
  172. "xrange":[-30,30]
  173. }]
  174. }
  175. # Save to file
  176. # filePathOfImage = os.path.join(outputAnalysisDir, f"{name[0]}.png")
  177. # fig.write_image(filePathOfImage, scale=3)
  178. # filePathOfHtml = os.path.join(outputAnalysisDir, f"{name[0]}.html")
  179. # fig.write_html(filePathOfHtml)
  180. # 将JSON对象保存到文件np.mean(dataFrame[Field_YawError])
  181. output_json_path = os.path.join(outputAnalysisDir, f"{name[0]}.json")
  182. with open(output_json_path, 'w', encoding='utf-8') as f:
  183. import json
  184. json.dump(json_output, f, ensure_ascii=False, indent=4)
  185. # 将JSON对象2保存到文件
  186. output_json_path2 = os.path.join(outputAnalysisDir, f"PDF-{name[0]}.json")
  187. with open(output_json_path2, 'w', encoding='utf-8') as f:
  188. import json
  189. json.dump(json_output2, f, ensure_ascii=False, indent=4)
  190. # 如果需要返回DataFrame,可以包含文件路径
  191. result_rows.append({
  192. Field_Return_TypeAnalyst: self.typeAnalyst(),
  193. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  194. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  195. Field_CodeOfTurbine: name[1],
  196. Field_Return_FilePath: output_json_path,
  197. Field_Return_IsSaveDatabase: True
  198. })
  199. result_rows.append({
  200. Field_Return_TypeAnalyst: self.typeAnalyst(),
  201. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  202. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  203. Field_CodeOfTurbine: name[1],
  204. Field_Return_FilePath: output_json_path2,
  205. Field_Return_IsSaveDatabase: True
  206. })
  207. # result_rows.append({
  208. # Field_Return_TypeAnalyst: self.typeAnalyst(),
  209. # Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  210. # Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  211. # Field_CodeOfTurbine: name[1],
  212. # Field_Return_FilePath: filePathOfImage,
  213. # Field_Return_IsSaveDatabase: False
  214. # })
  215. # result_rows.append({
  216. # Field_Return_TypeAnalyst: self.typeAnalyst(),
  217. # Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  218. # Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  219. # Field_CodeOfTurbine: name[1],
  220. # Field_Return_FilePath: filePathOfHtml,
  221. # Field_Return_IsSaveDatabase: True
  222. # })
  223. result_df = pd.DataFrame(result_rows)
  224. return result_df
  225. def calculateYawError(self, dataFrame: pd.DataFrame):
  226. dataFrame = dataFrame.dropna(
  227. subset=[Field_NameOfTurbine, Field_YawError, Field_ActiverPower,Field_WindSpeed])
  228. filtered_dataFrame = dataFrame[(dataFrame[Field_YawError].abs() <= 30)&(dataFrame[Field_WindSpeed] >= 0)&(dataFrame[Field_WindSpeed] <= 25)]
  229. # 如果筛选结果为空,直接返回 None,不再进行后续计算,防止 binned_statistic_2d 报错
  230. if filtered_dataFrame.empty:
  231. return pd.DataFrame()
  232. x=filtered_dataFrame[Field_YawError]
  233. y=filtered_dataFrame[Field_WindSpeed]
  234. # data = np.column_stack((x, y)) # 合并为两列数组
  235. # 使用 binned_statistic_2d 来计算散点的密度分布
  236. binSize_x = 60
  237. binSize_y = 50
  238. counts, x_edges, y_edges, binnumber = binned_statistic_2d(x, y, values=None, statistic='count', bins=[binSize_x, binSize_y])
  239. # 将数据密度转化为颜色值
  240. binX = np.digitize(x, x_edges) - 1
  241. binY = np.digitize(y, y_edges) - 1
  242. # 删除超出范围的下标
  243. validIdx = (binX >= 0) & (binX < binSize_x) & (binY >= 0) & (binY < binSize_y)
  244. # 获取有效下标的密度
  245. density = np.zeros(len(x))
  246. density[validIdx] = counts[binX[validIdx], binY[validIdx]]
  247. # 将结果保存到 result 中
  248. result = {
  249. 'x': x,
  250. 'y': y,
  251. 'density': density
  252. }
  253. # 将 result 转换为 DataFrame
  254. result_df = pd.DataFrame(result)
  255. return result_df