temperatureEnvironmentAnalyst.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import os
  2. import numpy as np
  3. import pandas as pd
  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 geopy.distance import geodesic
  9. from plotly.subplots import make_subplots
  10. class TemperatureEnvironmentAnalyst(AnalystWithGoodBadLimitPoint):
  11. """
  12. 风电机组环境温度传感器分析
  13. """
  14. def typeAnalyst(self):
  15. return "temperature_environment"
  16. def turbinesAnalysis(self, outputAnalysisDir, conf: Contract, turbineCodes):
  17. dictionary = self.processTurbineData(turbineCodes, conf, [
  18. Field_DeviceCode, Field_Time,Field_EnvTemp, Field_WindSpeed, Field_ActiverPower])
  19. dataFrameOfTurbines = self.userDataFrame(
  20. dictionary, conf.dataContract.configAnalysis, self)
  21. # 检查所需列是否存在
  22. required_columns = {Field_CodeOfTurbine,Field_EnvTemp}
  23. if not required_columns.issubset(dataFrameOfTurbines.columns):
  24. raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
  25. # 环境温度分析
  26. turbineEnvTempData = dataFrameOfTurbines.groupby(Field_CodeOfTurbine).agg(
  27. {Field_EnvTemp: 'median'})
  28. turbineEnvTempData = turbineEnvTempData.reset_index()
  29. mergeData = self.mergeData(self.turbineInfo, turbineEnvTempData)
  30. # 分机型
  31. turbrineInfos = self.common.getTurbineInfos(
  32. conf.dataContract.dataFilter.powerFarmID, turbineCodes, self.turbineInfo)
  33. returnResult= self.draw(mergeData, outputAnalysisDir, conf,turbrineInfos)
  34. return returnResult
  35. # return self.draw(mergeData, outputAnalysisDir, conf)
  36. def mergeData(self, turbineInfos: pd.DataFrame, turbineEnvTempData):
  37. """
  38. 将每台机组的环境温度均值数据与机组信息,按机组合并
  39. 参数:
  40. turbineInfos (pandas.DataFrame): 机组信息数据
  41. turbineEnvTempData (pandas.DataFrame): 每台机组的环境温度均值数据
  42. 返回:
  43. pandas.DataFrame: 每台机组的环境温度均值数据与机组信息合并数据
  44. """
  45. """
  46. 合并类型how的选项包括:
  47. 'inner': 内连接,只保留两个DataFrame中都有的键的行。
  48. 'outer': 外连接,保留两个DataFrame中任一或两者都有的键的行。
  49. 'left': 左连接,保留左边DataFrame的所有键,以及右边DataFrame中匹配的键的行。
  50. 'right': 右连接,保留右边DataFrame的所有键,以及左边DataFrame中匹配的键的行。
  51. """
  52. # turbineInfos[fieldTurbineName]=turbineInfos[fieldTurbineName].astype(str).apply(confData.add_W_if_starts_with_digit)
  53. # turbineEnvTempData[Field_NameOfTurbine] = turbineEnvTempData[Field_NameOfTurbine].astype(
  54. # str)
  55. tempDataFrame = pd.merge(turbineInfos, turbineEnvTempData, on=[
  56. Field_CodeOfTurbine], how='inner')
  57. # 保留指定字段,例如 'Key' 和 'Value1'
  58. mergeDataFrame = tempDataFrame[[Field_CodeOfTurbine, Field_NameOfTurbine, Field_Latitude,Field_Longitude, Field_EnvTemp]]
  59. return mergeDataFrame
  60. # 定义查找给定半径内点的函数
  61. def find_points_within_radius(self, data, center, field_temperature_env, radius):
  62. points_within_radius = []
  63. for index, row in data.iterrows():
  64. distance = geodesic(
  65. (center[2], center[1]), (row[Field_Latitude], row[Field_Longitude])).meters
  66. if distance <= radius:
  67. points_within_radius.append(
  68. (row[Field_NameOfTurbine], row[field_temperature_env]))
  69. return points_within_radius
  70. fieldTemperatureDiff = "temperature_diff"
  71. # def draw(self, dataFrame: pd.DataFrame, outputAnalysisDir, conf: Contract, charset=charset_unify):
  72. def draw(self, dataFrame: pd.DataFrame, outputAnalysisDir, conf: Contract, turbineModelInfo: pd.Series):
  73. # +1. 去除经纬度或温度为空的数据
  74. dataFrame = dataFrame.dropna(subset=[Field_NameOfTurbine, Field_Longitude, Field_Latitude, Field_EnvTemp])
  75. # +2. 筛选合法的经纬度范围 (纬度 [-90, 90], 经度 [-180, 180]); geopy 对纬度非常敏感,超出范围会直接报错
  76. valid_lat = (dataFrame[Field_Latitude] >= -90) & (dataFrame[Field_Latitude] <= 90)
  77. valid_lon = (dataFrame[Field_Longitude] >= -180) & (dataFrame[Field_Longitude] <= 180)
  78. dataFrame = dataFrame[valid_lat & valid_lon]
  79. # +3. 如果筛选后没有数据,打印日志并返回空 DataFrame,避免后续报错
  80. if dataFrame.empty:
  81. print(f"Warning: {self.typeAnalyst()} filtered no data (invalid coordinates or missing values).")
  82. return pd.DataFrame()
  83. # 处理数据
  84. dataFrame['new'] = dataFrame.loc[:, [Field_NameOfTurbine,
  85. Field_Longitude, Field_Latitude, Field_EnvTemp]].apply(tuple, axis=1)
  86. coordinates = dataFrame['new'].tolist()
  87. # df = pd.DataFrame(coordinates, columns=[Field_NameOfTurbine, Field_Longitude, Field_Latitude, confData.field_env_temp])
  88. # 查找半径内的点
  89. points_within_radius = {coord: self.find_points_within_radius(
  90. dataFrame, coord, Field_EnvTemp, self.turbineModelInfo[Field_RotorDiameter].iloc[0]*10) for coord in coordinates}
  91. res = []
  92. for center, nearby_points in points_within_radius.items():
  93. current_temp = dataFrame[dataFrame[Field_NameOfTurbine]
  94. == center[0]][Field_EnvTemp].iloc[0]
  95. target_tuple = (center[0], current_temp)
  96. if target_tuple in nearby_points:
  97. nearby_points.remove(target_tuple)
  98. median_temp = np.median(
  99. [i[1] for i in nearby_points]) if nearby_points else current_temp
  100. res.append((center[0], nearby_points, median_temp, current_temp))
  101. res = pd.DataFrame(
  102. res, columns=[Field_NameOfTurbine, '周边机组', '周边机组温度', '当前机组温度'])
  103. res[self.fieldTemperatureDiff] = res['当前机组温度'] - res['周边机组温度']
  104. # 使用plotly进行数据可视化
  105. fig1 = make_subplots(rows=1, cols=1)
  106. # 温度差异条形图
  107. fig1.add_trace(
  108. go.Bar(x=res[Field_NameOfTurbine],
  109. y=res[self.fieldTemperatureDiff], marker_color='dodgerblue'),
  110. row=1, col=1
  111. )
  112. fig1.update_layout(
  113. title={'text': f'温度偏差', 'x': 0.5},
  114. xaxis_title='机组名称',
  115. yaxis_title='温度偏差',
  116. shapes=[
  117. {'type': 'line', 'x0': 0, 'x1': 1, 'xref': 'paper', 'y0': 5,
  118. 'y1': 5, 'line': {'color': 'red', 'dash': 'dot'}},
  119. {'type': 'line', 'x0': 0, 'x1': 1, 'xref': 'paper', 'y0': -
  120. 5, 'y1': -5, 'line': {'color': 'red', 'dash': 'dot'}}
  121. ],
  122. xaxis=dict(tickangle=-45) # 设置x轴刻度旋转角度为45度
  123. )
  124. # 确保从 Series 中提取的是具体的值
  125. engineTypeCode = turbineModelInfo.get(Field_MillTypeCode, "")
  126. if isinstance(engineTypeCode, pd.Series):
  127. engineTypeCode = engineTypeCode.iloc[0]
  128. engineTypeName = turbineModelInfo.get(Field_MachineTypeCode, "")
  129. if isinstance(engineTypeName, pd.Series):
  130. engineTypeName = engineTypeName.iloc[0]
  131. # 构建最终的JSON对象
  132. json_output = {
  133. "analysisTypeCode": "风电机组环境温度传感器分析",
  134. "engineCode": engineTypeCode,
  135. "engineTypeName": engineTypeName,
  136. "xaixs": "机组名称",
  137. "yaixs": "温度偏差(℃)",
  138. "data": [{
  139. "engineName": "", # Field_NameOfTurbine
  140. "engineCode": "", # Field_CodeOfTurbine
  141. "title": f'温度偏差',
  142. "xData": res[Field_NameOfTurbine].tolist(),
  143. "yData": res[self.fieldTemperatureDiff].tolist(),
  144. }]
  145. }
  146. result_rows = []
  147. # 保存图像
  148. # pngFileName = '{}环境温差Bias.png'.format(
  149. # self.powerFarmInfo[Field_PowerFarmName].iloc[0])
  150. # pngFilePath = os.path.join(outputAnalysisDir, pngFileName)
  151. # fig1.write_image(pngFilePath, scale=3)
  152. # 保存HTML
  153. # htmlFileName = '{}环境温差Bias.html'.format(
  154. # self.powerFarmInfo[Field_PowerFarmName].iloc[0])
  155. # htmlFilePath = os.path.join(outputAnalysisDir, htmlFileName)
  156. # fig1.write_html(htmlFilePath)
  157. # 将JSON对象保存到文件
  158. output_json_path = os.path.join(outputAnalysisDir, f"total_Bias.json")
  159. with open(output_json_path, 'w', encoding='utf-8') as f:
  160. import json
  161. json.dump(json_output, f, ensure_ascii=False, indent=4)
  162. # 如果需要返回DataFrame,可以包含文件路径
  163. result_rows.append({
  164. Field_Return_TypeAnalyst: self.typeAnalyst(),
  165. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  166. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  167. Field_CodeOfTurbine: 'total',
  168. Field_MillTypeCode: 'total_Bias',
  169. Field_Return_FilePath: output_json_path,
  170. Field_Return_IsSaveDatabase: True
  171. })
  172. # result_rows.append({
  173. # Field_Return_TypeAnalyst: self.typeAnalyst(),
  174. # Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  175. # Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  176. # Field_CodeOfTurbine: Const_Output_Total,
  177. # Field_Return_FilePath: pngFilePath,
  178. # Field_Return_IsSaveDatabase: False
  179. # })
  180. # result_rows.append({
  181. # Field_Return_TypeAnalyst: self.typeAnalyst(),
  182. # Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  183. # Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  184. # Field_CodeOfTurbine: Const_Output_Total,
  185. # Field_Return_FilePath: htmlFilePath,
  186. # Field_Return_IsSaveDatabase: True
  187. # })
  188. # 环境温度中位数条形图
  189. fig2 = make_subplots(rows=1, cols=1)
  190. fig2.add_trace(
  191. go.Bar(x=res[Field_NameOfTurbine],
  192. y=res['当前机组温度'], marker_color='dodgerblue'),
  193. row=1, col=1
  194. )
  195. fig2.update_layout(
  196. title={'text': f'平均温度', 'x': 0.5},
  197. xaxis_title='机组名称',
  198. yaxis_title=' 温度',
  199. xaxis=dict(tickangle=-45) # 为x轴也设置旋转角度
  200. )
  201. # 确保从 Series 中提取的是具体的值
  202. engineTypeCode = turbineModelInfo.get(Field_MillTypeCode, "")
  203. if isinstance(engineTypeCode, pd.Series):
  204. engineTypeCode = engineTypeCode.iloc[0]
  205. engineTypeName = turbineModelInfo.get(Field_MachineTypeCode, "")
  206. if isinstance(engineTypeName, pd.Series):
  207. engineTypeName = engineTypeName.iloc[0]
  208. # 构建最终的JSON对象
  209. json_output = {
  210. "analysisTypeCode": "风电机组环境温度传感器分析",
  211. "engineCode": engineTypeCode,
  212. "engineTypeName": engineTypeName,
  213. "xaixs": "机组名称",
  214. "yaixs": "温度(℃)",
  215. "data": [{
  216. "engineName": "", # Field_NameOfTurbine
  217. "engineCode": "", # Field_CodeOfTurbine
  218. "title": f'平均温度',
  219. "xData": res[Field_NameOfTurbine].tolist(),
  220. "yData": res['当前机组温度'].tolist(),
  221. }]
  222. }
  223. # 保存图像
  224. # pngFileName = '{}环境温度中位数.png'.format(
  225. # self.powerFarmInfo[Field_PowerFarmName].iloc[0])
  226. # pngFilePath = os.path.join(outputAnalysisDir, pngFileName)
  227. # fig2.write_image(pngFilePath, scale=3)
  228. # 保存HTML
  229. # htmlFileName = '{}环境温度中位数.html'.format(
  230. # self.powerFarmInfo[Field_PowerFarmName].iloc[0])
  231. # htmlFilePath = os.path.join(outputAnalysisDir, htmlFileName)
  232. # fig2.write_html(htmlFilePath)
  233. # 将JSON对象保存到文件
  234. output_json_path = os.path.join(outputAnalysisDir, f"total_Mid.json")
  235. with open(output_json_path, 'w', encoding='utf-8') as f:
  236. import json
  237. json.dump(json_output, f, ensure_ascii=False, indent=4)
  238. # 如果需要返回DataFrame,可以包含文件路径
  239. result_rows.append({
  240. Field_Return_TypeAnalyst: self.typeAnalyst(),
  241. Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  242. Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  243. Field_CodeOfTurbine: 'total',
  244. Field_MillTypeCode: 'total_Mid',
  245. Field_Return_FilePath: output_json_path,
  246. Field_Return_IsSaveDatabase: True
  247. })
  248. # result_rows.append({
  249. # Field_Return_TypeAnalyst: self.typeAnalyst(),
  250. # Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  251. # Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  252. # Field_CodeOfTurbine: Const_Output_Total,
  253. # Field_Return_FilePath: pngFilePath,
  254. # Field_Return_IsSaveDatabase: False
  255. # })
  256. # result_rows.append({
  257. # Field_Return_TypeAnalyst: self.typeAnalyst(),
  258. # Field_PowerFarmCode: conf.dataContract.dataFilter.powerFarmID,
  259. # Field_Return_BatchCode: conf.dataContract.dataFilter.dataBatchNum,
  260. # Field_CodeOfTurbine: Const_Output_Total,
  261. # Field_Return_FilePath: htmlFilePath,
  262. # Field_Return_IsSaveDatabase: True
  263. # })
  264. result_df = pd.DataFrame(result_rows)
  265. return result_df
  266. """
  267. fig, ax = plt.subplots(figsize=(16,8),dpi=96)
  268. # 设置x轴刻度值旋转角度为45度
  269. plt.tick_params(axis='x', rotation=45)
  270. sns.barplot(x=Field_NameOfTurbine,y=self.fieldTemperatureDiff,data=res,ax=ax,color='dodgerblue')
  271. plt.axhline(y=5,ls=":",c="red")#添加水平直线
  272. plt.axhline(y=-5,ls=":",c="red")#添加水平直线
  273. ax.set_ylabel('temperature_difference')
  274. ax.set_title('temperature Bias')
  275. plt.savefig(outputAnalysisDir +'//'+ "{}环境温差Bias.png".format(confData.farm_name),bbox_inches='tight',dpi=120)
  276. fig2, ax2 = plt.subplots(figsize=(16,8),dpi=96)
  277. # 设置x轴刻度值旋转角度为45度
  278. plt.tick_params(axis='x', rotation=45)
  279. sns.barplot(x=Field_NameOfTurbine ,y='当前机组温度',data=res,ax=ax2,color='dodgerblue')
  280. ax2.set_ylabel('temperature')
  281. ax2.set_title('temperature median')
  282. plt.savefig(outputAnalysisDir +'//'+ "{}环境温度均值.png".format(confData.farm_name),bbox_inches='tight',dpi=120)
  283. """