chenhongyan1989 il y a 1 an
commit
5583e22273
100 fichiers modifiés avec 8753 ajouts et 0 suppressions
  1. 53 0
      README.md
  2. 40 0
      appService/README.MD
  3. 4 0
      appService/__init__.py
  4. 60 0
      appService/app.py
  5. 37 0
      appService/app.spec
  6. 0 0
      appService/create
  7. 20 0
      appService/logUtil.py
  8. 0 0
      appService/service/__init__.py
  9. 28 0
      appService/service/daemonService.py
  10. 58 0
      appService/service/winService.py
  11. 100 0
      conf_template/conf_powerfarm_template.json
  12. 152 0
      conf_template/conf_template.json
  13. 4 0
      dataAnalysisBehavior/__init__.py
  14. 0 0
      dataAnalysisBehavior/behavior/__init__.py
  15. 11 0
      dataAnalysisBehavior/behavior/analyst.py
  16. 21 0
      dataAnalysisBehavior/behavior/analystExcludeRatedPower.py
  17. 181 0
      dataAnalysisBehavior/behavior/baseAnalyst.py
  18. 0 0
      dataAnalysisBehavior/common/__init__.py
  19. 166 0
      dataAnalysisBehavior/common/commonBusiness.py
  20. 20 0
      dataAnalysisBehavior/common/turbineInfo.py
  21. 9 0
      dataAnalysisBehavior/setup.py
  22. 4 0
      dataAnalysisBusiness/__init__.py
  23. 1 0
      dataAnalysisBusiness/algorithm/__init__.py
  24. 86 0
      dataAnalysisBusiness/algorithm/cabinVibrateAnalyst.py
  25. 247 0
      dataAnalysisBusiness/algorithm/cpAnalyst.py
  26. 109 0
      dataAnalysisBusiness/algorithm/cpTrendAnalyst.py
  27. 209 0
      dataAnalysisBusiness/algorithm/cpWindSpeedAnalyst.py
  28. 21 0
      dataAnalysisBusiness/algorithm/dataIntegrityOfMinuteAnalyst.py
  29. 164 0
      dataAnalysisBusiness/algorithm/dataIntegrityOfSecondAnalyst.py
  30. 411 0
      dataAnalysisBusiness/algorithm/dataMarker.py
  31. 369 0
      dataAnalysisBusiness/algorithm/dataProcessor.py
  32. 43 0
      dataAnalysisBusiness/algorithm/formula_cp.py
  33. 327 0
      dataAnalysisBusiness/algorithm/generatorSpeedPowerAnalyst.py
  34. 236 0
      dataAnalysisBusiness/algorithm/generatorSpeedTorqueAnalyst.py
  35. 120 0
      dataAnalysisBusiness/algorithm/minPitchAnalyst.py
  36. 74 0
      dataAnalysisBusiness/algorithm/pitchGeneratorSpeedAnalyst.py
  37. 155 0
      dataAnalysisBusiness/algorithm/pitchPowerAnalyst.py
  38. 98 0
      dataAnalysisBusiness/algorithm/pitchPowerWindSpeedAnalyst.py
  39. 91 0
      dataAnalysisBusiness/algorithm/pitchTSRCpAnalyst.py
  40. 224 0
      dataAnalysisBusiness/algorithm/powerCurveAnalyst.py
  41. 106 0
      dataAnalysisBusiness/algorithm/powerOscillationAnalyst.py
  42. 106 0
      dataAnalysisBusiness/algorithm/powerScatter2DAnalyst.py
  43. 119 0
      dataAnalysisBusiness/algorithm/powerScatterAnalyst.py
  44. 86 0
      dataAnalysisBusiness/algorithm/ratedPowerWindSpeedAnalyst.py
  45. 66 0
      dataAnalysisBusiness/algorithm/ratedWindSpeedAnalyst.py
  46. 116 0
      dataAnalysisBusiness/algorithm/temperatureEnvironmentAnalyst.py
  47. 375 0
      dataAnalysisBusiness/algorithm/temperatureLargeComponentsAnalyst.py
  48. 156 0
      dataAnalysisBusiness/algorithm/tsrAnalyst.py
  49. 102 0
      dataAnalysisBusiness/algorithm/tsrTrendAnalyst.py
  50. 152 0
      dataAnalysisBusiness/algorithm/tsrWindSpeedAnalyst.py
  51. 101 0
      dataAnalysisBusiness/algorithm/windDirectionFrequencyAnalyst.py
  52. 81 0
      dataAnalysisBusiness/algorithm/windRoseOfTurbine.py
  53. 44 0
      dataAnalysisBusiness/algorithm/windSpeedAnalyst.py
  54. 83 0
      dataAnalysisBusiness/algorithm/windSpeedFrequencyAnalyst.py
  55. 181 0
      dataAnalysisBusiness/algorithm/yawErrorAnalyst.py
  56. 450 0
      dataAnalysisBusiness/demo/SCADA_10min_category_0.py
  57. 507 0
      dataAnalysisBusiness/demo/SCADA_10min_category_1.py
  58. 193 0
      dataAnalysisBusiness/demo/SCADA_10min_category_2.py
  59. 632 0
      dataAnalysisBusiness/demo/SCADA_10min_category_3.py
  60. 62 0
      dataAnalysisBusiness/demo/scatter3D_plotly.py
  61. 50 0
      dataAnalysisBusiness/demo/scatter3D_plotly_make_subplots.py
  62. 19 0
      dataAnalysisBusiness/demo/test.py
  63. 113 0
      dataAnalysisBusiness/demo/testDataProcess.py
  64. 12 0
      dataAnalysisBusiness/demo/testPandas.py
  65. 10 0
      dataAnalysisBusiness/setup.py
  66. 4 0
      dataContract/__init__.py
  67. 0 0
      dataContract/algorithmContract/__init__.py
  68. 188 0
      dataContract/algorithmContract/confBusiness.py
  69. 88 0
      dataContract/algorithmContract/dataContract.json
  70. 9 0
      dataContract/setup.py
  71. 0 0
      mydatabase.db
  72. 4 0
      repositoryZN/__init__.py
  73. 9 0
      repositoryZN/setup.py
  74. 0 0
      repositoryZN/utils/__init__.py
  75. 57 0
      repositoryZN/utils/csvFileUtil.py
  76. 89 0
      repositoryZN/utils/directoryUtil.py
  77. 37 0
      repositoryZN/utils/jsonUtil.py
  78. 0 0
      wtoaamapi/__init__.py
  79. 0 0
      wtoaamapi/apps/__init__.py
  80. 3 0
      wtoaamapi/apps/admin.py
  81. 6 0
      wtoaamapi/apps/apps.py
  82. 0 0
      wtoaamapi/apps/business/__init__.py
  83. 91 0
      wtoaamapi/apps/business/main.py
  84. 50 0
      wtoaamapi/apps/business/test.py
  85. 22 0
      wtoaamapi/apps/migrations/0001_initial.py
  86. 0 0
      wtoaamapi/apps/migrations/__init__.py
  87. 10 0
      wtoaamapi/apps/models.py
  88. 12 0
      wtoaamapi/apps/serializers.py
  89. 3 0
      wtoaamapi/apps/tests.py
  90. 10 0
      wtoaamapi/apps/urls.py
  91. 0 0
      wtoaamapi/apps/viewDemo/__init__.py
  92. 34 0
      wtoaamapi/apps/viewDemo/viewBook.py
  93. 34 0
      wtoaamapi/apps/viewDemo/viewUser.py
  94. 26 0
      wtoaamapi/apps/views.py
  95. 13 0
      wtoaamapi/config/swagger.py
  96. BIN
      wtoaamapi/db.sqlite3
  97. 22 0
      wtoaamapi/manage.py
  98. 41 0
      wtoaamapi/testListen.py
  99. 0 0
      wtoaamapi/wtoaamapi/__init__.py
  100. 16 0
      wtoaamapi/wtoaamapi/asgi.py

+ 53 - 0
README.md

@@ -0,0 +1,53 @@
+WTOAAM
+===============
+
+风力发电机组运行分析算法模型 Wind turbine operation analysis algorithm model
+
+# django auth
+    http://127.0.0.1/admin
+    账号
+    admin
+    密码
+    root.123456
+
+
+# 附加进程调试
+## 安装debugpy
+    pip install --upgrade debugpy
+
+## 运行程序命令
+   示例:python -m debugpy --listen localhost:5678 --wait-for-client e:/WorkSpace/SourceCode/WTOAAM/wtoaamapi/apps/business/main.py
+
+## 附加进程
+   配置launch.json文件,内容如下:
+   {
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Python: Remote Attach",  // 远程调试
+            "type":"debugpy",
+            "request": "attach",
+            "connect": {
+                "host": "localhost",
+                "port": 5678
+            },
+            "pathMappings": [
+                {
+                    "localRoot": "${workspaceFolder}",
+                    "remoteRoot": "."
+                }
+            ],
+            "justMyCode": false
+        }
+        ]
+    }
+    打开要调试的源代码文件,在vscode搜索框选择"开始调试 debug","Python: Remote Attach"即lanuch.json文件中configurations[0].name节点值即可。
+
+# 自宿主服务
+  自定义包appService,同时支持daemon、windows服务
+
+## 依赖包
+    pip install python-daemon pypiwin32

+ 40 - 0
appService/README.MD

@@ -0,0 +1,40 @@
+# 服务安装
+
+## Windows服务安装(Windows操作系统)
+    1. 创建应用程序,执行命令:  pyinstaller --onefile app.py
+    2. 安装服务,在PowerShell中,执行命令:New-Service -Name MyService -BinaryPathName "E:\WorkSpace\SourceCode\WTOAAM\appService\dist\app.exe"
+   
+## Deamon服务安装(Linux操作系统)
+    1. 创建应用程序,执行命令:  pyinstaller --onefile app.py
+    2. 编写 Systemd 服务单元文件:创建一个以 .service 为后缀的 Systemd 服务单元文件,该文件包含了关于你的服务的配置信息。通常这些文件存放在 /etc/systemd/system/ 目录下。例如,创建一个名为 mydaemon.service 的服务单元文件,内容类似于:
+    [Unit]
+    Description=My Daemon Service
+    After=network.target
+
+    [Service]
+    Type=simple
+    ExecStart=/path/to/your/daemon/executable
+    Restart=always
+
+    [Install]
+    WantedBy=multi-user.target
+
+    其中:
+    Description:描述服务的简短说明。
+    After:指定服务应该在哪些其他服务之后启动。
+    Type:指定服务的类型,可以是 simple、forking、oneshot、dbus 等。
+    ExecStart:指定服务启动时执行的命令或可执行文件的路径。
+    Restart:指定服务在失败或意外终止后是否应该自动重启。
+    WantedBy:指定服务应该在何时启动。常见的是 multi-user.target,表示在系统引导时启动。
+
+    3. 启用和启动服务:通过执行以下命令启用和启动服务:
+    sudo systemctl enable mydaemon.service
+    sudo systemctl start mydaemon.service
+
+    4. 停止和重启服务:你可以使用 systemctl 命令停止和重启服务:
+    sudo systemctl stop mydaemon.service   # 停止服务
+    sudo systemctl restart mydaemon.service   # 重启服务
+
+    5. 查看服务状态:你可以使用 systemctl status 命令来查看服务的状态和相关信息:
+    systemctl status mydaemon.service
+

+ 4 - 0
appService/__init__.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import service
+__all__=['service']

+ 60 - 0
appService/app.py

@@ -0,0 +1,60 @@
+import sys
+import time
+import threading
+import servicemanager
+from argparse import ArgumentParser
+from appService.logUtil import LogUtil
+
+logUtil=LogUtil()
+logger=logUtil.getLogger()
+
+def parse_args():
+    logger.debug("test2")
+    parser = ArgumentParser(description="Run as a service.")
+    parser.add_argument("action", choices=["start", "stop", "restart", "status"],default="start")
+    parser.add_argument("--type", choices=["daemon", "service"], default="service")
+    logger.debug("test3")
+    return parser.parse_args()
+
+def main():               
+    if sys.platform != "win32" :
+        from appService.service.daemonService import DaemonService
+        args = parse_args()
+
+        daemon_service = DaemonService()
+        if args.action == "start":
+            daemon_service.start()
+        elif args.action == "stop":
+            daemon_service.stop()
+        elif args.action == "status":
+            daemon_service.status()
+
+    if sys.platform == "win32":
+        from appService.service.winService import WinService,CommandLine
+
+        servicemanager.Initialize()
+        servicemanager.PrepareToHostSingle(WinService)
+        servicemanager.StartServiceCtrlDispatcher()
+
+    # if args.type == "service":
+    #     from appService.service.winService import WinService,CommandLine
+        
+    #     if len(sys.argv) == 1:
+    #         servicemanager.Initialize()
+    #         servicemanager.PrepareToHostSingle(WinService)
+    #         servicemanager.StartServiceCtrlDispatcher()
+    #     else:
+    #         CommandLine(WinService)
+    # elif args.type == "daemon":
+    #     from appService.service.daemonService import DaemonService
+
+    #     daemon_service = DaemonService()
+    #     if args.action == "start":
+    #         daemon_service.start()
+    #     elif args.action == "stop":
+    #         daemon_service.stop()
+    #     elif args.action == "status":
+    #         daemon_service.status()
+
+if __name__ == "__main__":
+    main()

+ 37 - 0
appService/app.spec

@@ -0,0 +1,37 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+a = Analysis(
+    ['app.py'],
+    pathex=[],
+    binaries=[],
+    datas=[],
+    hiddenimports=[],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    noarchive=False,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    a.binaries,
+    a.datas,
+    [],
+    name='app',
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    runtime_tmpdir=None,
+    console=True,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+)

+ 0 - 0
appService/create


+ 20 - 0
appService/logUtil.py

@@ -0,0 +1,20 @@
+import logging
+from logging.handlers import RotatingFileHandler
+
+class LogUtil:
+    def __init__(self):
+        # 配置logging
+        logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+        # 创建logger对象
+        self.logger = logging.getLogger('example_logger')
+
+        # 创建一个FileHandler来输出所有级别的日志到指定文件
+        log_file = r'./example.log'
+        file_handler = logging.FileHandler(log_file)
+        file_handler.setLevel(logging.DEBUG)  # 设置FileHandler的日志级别为DEBUG,以确保记录所有级别的日志
+        file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
+        self.logger.addHandler(file_handler)
+
+    def getLogger(self):
+        return self.logger

+ 0 - 0
appService/service/__init__.py


+ 28 - 0
appService/service/daemonService.py

@@ -0,0 +1,28 @@
+import sys
+import time
+import threading
+import pwd
+import signal
+
+class DaemonService:
+    def __init__(self):
+        self.stop_event = threading.Event()
+
+    def run(self):
+        while not self.stop_event.is_set():
+            print("Daemon is running...")
+            time.sleep(5)
+
+    def start(self):
+        self.stop_event.clear()
+        thread = threading.Thread(target=self.run)
+        thread.start()
+
+    def stop(self):
+        self.stop_event.set()
+
+    def status(self):
+        if self.stop_event.is_set():
+            print("Daemon is not running.")
+        else:
+            print("Daemon is running.")

+ 58 - 0
appService/service/winService.py

@@ -0,0 +1,58 @@
+import sys
+import time
+import threading
+import win32event
+import win32service
+import win32serviceutil
+from appService.logUtil import LogUtil
+
+logUtil=LogUtil()
+logger=logUtil.getLogger()
+
+def CommandLine(classService):
+    win32serviceutil.HandleCommandLine(classService)
+
+class WinService(win32serviceutil.ServiceFramework):
+    _svc_name_ = "MyService"
+    _svc_display_name_ = "My Service"
+    _svc_description_ = "This is a sample service."
+
+    def __init__(self, args):
+        logger.info("execute init")
+        try:
+            win32serviceutil.ServiceFramework.__init__(self, args)
+            self.stop_event = win32event.CreateEvent(None, 0, 0, None)
+        except Exception as ex:
+            logger.error(ex)
+
+    def SvcStop(self):
+        self.stop()
+
+    def SvcDoRun(self):
+        self.start()
+
+    def main(self):
+        while self.is_running:
+            logger.info("Service is running...")
+            time.sleep(10)
+
+    def start(self):
+        logger.info("execute start")
+        try:
+            self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
+            self.is_running = True
+            self.ReportServiceStatus(win32service.SERVICE_RUNNING)
+            self.main()
+        except Exception as ex:
+            logger.error(ex)
+
+    def stop(self):
+        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+        self.is_running = False
+        win32event.SetEvent(self.stop_event)
+
+    def status(self):
+        if self.is_running:
+            print("Service is running.")
+        else:
+            print("Service is not running.")

+ 100 - 0
conf_template/conf_powerfarm_template.json

@@ -0,0 +1,100 @@
+{
+    "name_PowerFarm": "长清风电场",
+    "rated_Power_Turbine_Unit_kW": 2000,
+    "rated_WindSpeed": 11,
+    "rotor_diameter": 105.0,
+    "rated_cut_in_windspeed":3, 
+    "rated_cut_out_windspeed":25,
+    "rotational_Speed_Ratio": 117.91,
+    "name_Output": "second level",
+    "time_Period_Unit_Second": 600,
+    "turbineInfoFilePathCSV": "F:/大生科技/数据/高本山10分钟数据/机组信息_风电场_gbs.csv",
+    "turbineGuaranteedPowerCurveFilePathCSV": "F:/大生科技/数据/高本山10分钟数据/curv_power_hetong_gbs.csv",
+    "inputFileDirectoryByCSV": "F:/大生科技/数据/高本山10分钟数据/gaobenshan-10min/",
+    "csvFileNameSplitStringForTurbine": ".csv",
+    "index_turbine": 0,
+    "filter": {
+        "filter_value_state_turbine": null,
+        "speed_wind_cut_in": 3,
+        "speed_wind_cut_out": 25,
+        "angle_pitch_min": 2,
+        "angle_pitch_max": null,
+        "active_power_min": 39,
+        "active_power_max": 2000,
+        "speed_generator_min": null,
+        "speed_generator_max": null,
+        "activePowerAvailable": [
+            1
+        ]
+    },
+    "graphSets": {
+        "generatorSpeed": {
+            "step": 200,
+            "min": 1000,
+            "max": 2000
+        },
+        "generatorTorque": {
+            "step": 2000,
+            "min": 0,
+            "max": 12000
+        },
+        "cp": {
+            "step": 0.5,
+            "min": 0,
+            "max": 2
+        },
+        "tsr": {
+            "step": 5,
+            "min": 0,
+            "max": 30
+        },
+        "pitchAngle": {
+            "step": 2,
+            "min": -1,
+            "max": 30
+        },
+        "activePower": {
+            "step": 250,
+            "min": 0,
+            "max": 2000
+        },
+        "generatorTemperature": {
+            "step": 10,
+            "min": -40,
+            "max": 100
+        }
+    },
+    "outputFileDirectory": "output",
+    "skip_row_number": 0,
+    "density_air": 1.222,
+    "name_Type_For_Analysis": "数据完整度",
+    "date_Begin": "2023-02-01 00:00:00",
+    "date_End": "2023-12-31 23:59:59",
+    "excludingMonths": null,
+    "turbine_Time": "时间",
+    "turbine_Name": null,
+    "speed_Wind": "30秒平均风速",
+    "power_Active": "有功功率",
+    "pitch_Angle1": "桨距角1",
+    "pitch_Angle2": "桨距角2",
+    "pitch_Angle3": "桨距角3",
+    "state_Turbine": null,
+    "speed_Generator": "发电机转速",
+    "speed_Rotor": "风轮转速",
+    "torque": null,
+    "direction_Wind": "60秒平均风向角",
+    "angle_included": "风向角",
+    "nacelle_Pos": "机舱位置",
+    "temperature_Env": "舱外温度",
+    "temperature_Nacelle": "舱内温度",
+    "Cabin_Vibrate_X": "机舱横向(左右)振动值(RMS)",
+    "Cabin_Vibrate_Y": "机舱前后(俯仰)振动值(RMS)",
+    "activePowerSet": "有功功率设定值",
+    "activePowerAvailable": "功率曲线可用",
+    "temperature_large_components": "低速轴承温度,高速轴承温度,齿轮箱入口油温,驱动前轴承温度,自由后轴承温度,发电机定子U温度",
+    "temperature_Generator": {
+        "yAxisDE": "低速轴承温度",
+        "yAxisNDE": "高速轴承温度",
+        "diffTemperature": "舱内温度"
+    }
+}

+ 152 - 0
conf_template/conf_template.json

@@ -0,0 +1,152 @@
+[
+    {
+        "configFilePath": "conf/conf_powerfarm_template.json",
+        "configAnalysis": [
+            {
+                "package": "algorithm.cabinVibrateAnalyst",
+                "className": "CabinVibrateAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.windSpeedFrequencyAnalyst",
+                "className": "WindSpeedFrequencyAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.windDirectionFrequencyAnalyst",
+                "className": "WindDirectionFrequencyAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.windRoseOfTurbine",
+                "className": "WinRoseOfTurbineAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.temperatureLargeComponentsAnalyst",
+                "className": "TemperatureLargeComponentsAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.temperatureEnvironmentAnalyst",
+                "className": "TemperatureEnvironmentAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.generatorSpeedPowerAnalyst",
+                "className": "GeneratorSpeedPowerAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.generatorSpeedTorqueAnalyst",
+                "className": "GeneratorSpeedTorqueAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.pitchPowerAnalyst",
+                "className": "PitchPowerAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.pitchGeneratorSpeedAnalyst",
+                "className": "PitchGeneratorSpeedAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.dataIntegrityOfMinuteAnalyst",
+                "className": "DataIntegrityOfMinuteAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.powerCurveAnalyst",
+                "className": "PowerCurveAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.ratedPowerWindSpeedAnalyst",
+                "className": "RatedPowerWindSpeedAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.ratedWindSpeedAnalyst",
+                "className": "RatedWindSpeedAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.powerScatter2DAnalyst",
+                "className": "PowerScatter2DAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.powerScatterAnalyst",
+                "className": "PowerScatterAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.windSpeedAnalyst",
+                "className": "WindSpeedAnalyst",
+                "methodName": "executeAnalysis"
+            },            
+            {
+                "package": "algorithm.pitchPowerWindSpeedAnalyst",
+                "className": "PitchPowerWindSpeedAnalyst",
+                "methodName": "executeAnalysis"
+            },            
+            {
+                "package": "algorithm.dataIntegrityOfSecondAnalyst",
+                "className": "DataIntegrityOfSecondAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.yawErrorAnalyst",
+                "className": "YawErrorAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.minPitchAnalyst",
+                "className": "MinPitchAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.cpAnalyst",
+                "className": "CpAnalyst",
+                "methodName": "executeAnalysis"
+            },
+			{
+				"package": "algorithm.cpWindSpeedAnalyst",
+				"className": "CpWindSpeedAnalyst",
+				"methodName": "executeAnalysis"
+			},
+            {
+                "package": "algorithm.cpTrendAnalyst",
+                "className": "CpTrendAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.tsrAnalyst",
+                "className": "TSRAnalyst",
+                "methodName": "executeAnalysis"
+            },
+			{
+				"package": "algorithm.tsrWindSpeedAnalyst",
+				"className": "TSRWindSpeedAnalyst",
+				"methodName": "executeAnalysis"
+			},
+            {
+                "package": "algorithm.tsrTrendAnalyst",
+                "className": "TSRTrendAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.powerOscillationAnalyst",
+                "className": "PowerOscillationAnalyst",
+                "methodName": "executeAnalysis"
+            },
+            {
+                "package": "algorithm.pitchTSRCpAnalyst",
+                "className": "PitchTSRCpAnalyst",
+                "methodName": "executeAnalysis"
+            }
+        ]
+    }
+]

+ 4 - 0
dataAnalysisBehavior/__init__.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import *
+__all__=['behavior']

+ 0 - 0
dataAnalysisBehavior/behavior/__init__.py


+ 11 - 0
dataAnalysisBehavior/behavior/analyst.py

@@ -0,0 +1,11 @@
+from .baseAnalyst import BaseAnalyst
+import os
+import pandas as pd
+import numpy as np
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class Analyst(BaseAnalyst):
+    def typeAnalyst(self):
+        pass

+ 21 - 0
dataAnalysisBehavior/behavior/analystExcludeRatedPower.py

@@ -0,0 +1,21 @@
+from .analyst import Analyst
+import os
+import pandas as pd
+import numpy as np
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class AnalystExcludeRatedPower(Analyst):
+    def filterCommon(self,dataFrame:pd.DataFrame, confData:ConfBusiness):
+        dataFrame=super().filterCommon(dataFrame,confData)
+
+        if not self.common.isNone(confData.field_power) and self.node_active_power_max in confData.filter and not self.common.isNone(confData.filter[self.node_active_power_max]) \
+                and not self.common.isNone(confData.field_pitch_angle1) and self.node_angle_pitch_min in confData.filter and not self.common.isNone(confData.filter[self.node_angle_pitch_min]):
+            activePowerMax = float(confData.filter[self.node_active_power_max])
+            anglePitchMin = float(confData.filter[self.node_angle_pitch_min])
+
+            dataFrame = dataFrame[~((dataFrame[confData.field_power] >= activePowerMax*0.9) & (
+                dataFrame[confData.field_power] <= activePowerMax*1.2))]
+        
+        return dataFrame

+ 181 - 0
dataAnalysisBehavior/behavior/baseAnalyst.py

@@ -0,0 +1,181 @@
+from abc import ABC, abstractmethod
+import pandas as pd
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+from common.commonBusiness import CommonBusiness
+
+
+class BaseAnalyst(ABC):
+    def __init__(self, confData: ConfBusiness):
+        self.common = CommonBusiness()
+        self.confData = confData
+        self.node_filter_value_state_turbine = "filter_value_state_turbine"
+        self.node_angle_pitch_min = "angle_pitch_min"
+        self.node_angle_pitch_max = "angle_pitch_max"
+        self.node_speed_wind_cut_in = "speed_wind_cut_in"
+        self.node_speed_wind_cut_out = "speed_wind_cut_out"
+        self.node_active_power_min = "active_power_min"
+        self.node_active_power_max = "active_power_max"
+        self.node_speed_generator_min = "speed_generator_min"
+        self.node_speed_generator_max = "speed_generator_max"
+        self.node_activePowerAvailable = "activePowerAvailable"
+        self.dataFrameContractOfTurbine = self.processContractData(confData)
+
+    def processContractData(self, confData: ConfBusiness):
+        dataFrameContractOfTurbine = self.common.contractGuaranteePowerCurveData(
+            confData.turbineGuaranteedPowerCurveFilePathCSV, confData)
+        self.common.calculateCp2(
+            dataFrameContractOfTurbine, confData.density_air, confData.rotor_diameter, "风速", "有功功率")
+
+        return dataFrameContractOfTurbine
+
+    @abstractmethod
+    def typeAnalyst(self):
+        pass
+
+    def getOutputAnalysisDir(self):
+        """
+        获取当前分析的输出目录
+        """
+        outputAnalysisDir = r"{}/{}".format(
+            self.confData.output_path, self.typeAnalyst())
+        dir.create_directory(outputAnalysisDir)
+
+        return outputAnalysisDir
+
+    def filterCommon(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        if not self.common.isNone(confData.field_wind_speed) and self.node_speed_wind_cut_in in confData.filter \
+                and not self.common.isNone(confData.filter[self.node_speed_wind_cut_in]) \
+                and self.node_speed_wind_cut_out in confData.filter \
+                and not self.common.isNone(confData.filter[self.node_speed_wind_cut_out]) \
+                and not self.common.isNone(confData.field_power):
+            windSpeedCutIn = float(
+                confData.filter[self.node_speed_wind_cut_in])
+            windSpeedCutOut = float(
+                confData.filter[self.node_speed_wind_cut_out])
+
+            dataFrame = dataFrame[~((dataFrame[confData.field_wind_speed] > windSpeedCutOut) | (
+                dataFrame[confData.field_wind_speed] < windSpeedCutIn))]
+            dataFrame = dataFrame[~((dataFrame[confData.field_wind_speed] > windSpeedCutIn) & (
+                dataFrame[confData.field_power] < confData.rated_power*0.01))]
+
+        if not self.common.isNone(confData.field_power) and confData.field_power in dataFrame.columns \
+           and not self.common.isNone(confData.field_pitch_angle1) and confData.field_pitch_angle1 in dataFrame.columns \
+           and self.node_angle_pitch_min in confData.filter and not self.common.isNone(confData.filter[self.node_angle_pitch_min]):
+            anglePitchMin = float(confData.filter[self.node_angle_pitch_min])
+            dataFrame = dataFrame[~((
+                dataFrame[confData.field_power] <= confData.rated_power*0.9) & (dataFrame[confData.field_pitch_angle1] >anglePitchMin) )]
+        
+        # if not self.common.isNone(confData.rated_WindSpeed) and not self.common.isNone(confData.field_pitch_angle1)  \
+        #         and self.node_angle_pitch_min in confData.filter and not self.common.isNone(confData.filter[self.node_angle_pitch_min]):
+        #     anglePitchMin = float(confData.filter[self.node_angle_pitch_min])
+
+        #     dataFrame = dataFrame[~((dataFrame[confData.field_wind_speed] < confData.rated_WindSpeed) & (
+        #         dataFrame[confData.field_pitch_angle1] > anglePitchMin))]
+            
+        # Filter rows where turbine state
+        if not self.common.isNone(confData.field_gen_speed) and confData.field_gen_speed in dataFrame.columns:
+            dataFrame = dataFrame[(dataFrame[confData.field_gen_speed] > 0)]
+
+        # Filter rows where turbine state
+        if not self.common.isNone(confData.field_turbine_state) and self.node_filter_value_state_turbine in confData.filter and not self.common.isNone(confData.filter[self.node_filter_value_state_turbine]):
+            stateTurbine = confData.filter[self.node_filter_value_state_turbine]
+            dataFrame = dataFrame[dataFrame[confData.field_turbine_state].isin(
+                stateTurbine)]
+
+        if not self.common.isNone(confData.rated_WindSpeed) and not self.common.isNone(confData.field_wind_speed) \
+                and not self.common.isNone(confData.field_gen_speed) \
+                and self.node_speed_generator_max in confData.filter \
+                and not self.common.isNone(confData.filter[self.node_speed_generator_max]):
+            speedGeneratorMax = float(
+                confData.filter[self.node_speed_generator_max])
+
+            dataFrame = dataFrame[~((dataFrame[confData.field_wind_speed] >= confData.rated_WindSpeed) & (
+                dataFrame[confData.field_gen_speed] < speedGeneratorMax*0.9))]
+
+        if not self.common.isNone(confData.field_gen_speed) \
+            and self.node_speed_generator_max in confData.filter \
+                and not self.common.isNone(confData.filter[self.node_speed_generator_min]) \
+                and not self.common.isNone(confData.filter[self.node_speed_generator_max]):
+            speedGeneratorMin = float(
+                confData.filter[self.node_speed_generator_min])
+            speedGeneratorMax = float(
+                confData.filter[self.node_speed_generator_max])
+            dataFrame = dataFrame[~((dataFrame[confData.field_gen_speed] < speedGeneratorMin) | (
+                dataFrame[confData.field_gen_speed] > speedGeneratorMax))]
+
+        if not self.common.isNone(confData.field_activePowerSet) and confData.field_activePowerSet in dataFrame.columns:
+            dataFrame = dataFrame[dataFrame[confData.field_activePowerSet]
+                                  == confData.rated_power]
+
+        if not self.common.isNone(confData.field_activePowerAvailable) and confData.field_activePowerAvailable in dataFrame.columns \
+              and self.node_activePowerAvailable in confData.filter and not self.common.isNone(confData.filter[self.node_activePowerAvailable]):
+            state = confData.filter[self.node_activePowerAvailable]
+            dataFrame = dataFrame[dataFrame[confData.field_activePowerAvailable].isin(
+                state)]
+
+        # # Filter rows where pitch
+        # if not self.common.isNone(confData.field_pitch_angle1) and self.node_angle_pitch_min in confData.filter and not self.common.isNone(confData.filter[self.node_angle_pitch_min]):
+        #     anglePitchMin = float(confData.filter[self.node_angle_pitch_min])
+        #     dataFrame = dataFrame[(
+        #         dataFrame[confData.field_pitch_angle1] < anglePitchMin)]
+
+        # if not self.common.isNone(confData.field_pitch_angle1) and self.node_angle_pitch_max in confData.filter and not self.common.isNone(confData.filter[self.node_angle_pitch_max]):
+        #     anglePitchMax = float(confData.filter[self.node_angle_pitch_max])
+        #     dataFrame = dataFrame[(
+        #         dataFrame[confData.field_pitch_angle1] <= anglePitchMax)]
+
+        # # Filter rows where wind speed
+        # if not self.common.isNone(confData.field_wind_speed) and self.node_speed_wind_cut_in in confData.filter and not self.common.isNone(confData.filter[self.node_speed_wind_cut_in]):
+        #     windSpeedCutIn = float(confData.filter[self.node_speed_wind_cut_in])
+        #     dataFrame = dataFrame[(
+        #         dataFrame[confData.field_wind_speed] >= windSpeedCutIn)]
+
+        # if not self.common.isNone(confData.field_wind_speed) and self.node_speed_wind_cut_out in confData.filter and not self.common.isNone(confData.filter[self.node_speed_wind_cut_out]):
+        #     windSpeedCutOut = float(confData.filter[self.node_speed_wind_cut_out])
+        #     dataFrame = dataFrame[(
+        #         dataFrame[confData.field_wind_speed] < windSpeedCutOut)]
+
+        # # Filter rows where power
+        # if not self.common.isNone(confData.field_power) and self.node_active_power_min in confData.filter and not self.common.isNone(confData.filter[self.node_active_power_min]):
+        #     activePowerMin = float(confData.filter[self.node_active_power_min])
+        #     dataFrame = dataFrame[(
+        #         dataFrame[confData.field_power] >= activePowerMin)]
+
+        # if not self.common.isNone(confData.field_power) and self.node_active_power_max in confData.filter and not self.common.isNone(confData.filter[self.node_active_power_max]):
+        #     activePowerMax = float(confData.filter[self.node_active_power_max])
+        #     dataFrame = dataFrame[(
+        #         dataFrame[confData.field_power] < activePowerMax)]
+
+        return dataFrame
+
+    def filterCustomForTurbine(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        return self.filterCommon(dataFrame, confData)
+
+    def analysisOfTurbine(self,
+                          dataFrame: pd.DataFrame,
+                          outputAnalysisDir,
+                          outputFilePath,
+                          confData: ConfBusiness,
+                          turbineName):
+        dataFrame = self.filterCustomForTurbine(dataFrame, confData)
+        self.turbineAnalysis(dataFrame, outputAnalysisDir,
+                             outputFilePath, confData, turbineName)
+
+    def turbineAnalysis(self,
+                        dataFrame: pd.DataFrame,
+                        outputAnalysisDir,
+                        outputFilePath,
+                        confData: ConfBusiness,
+                        turbineName):
+        pass
+
+    def filterCustomForTurbines(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        return self.filterCommon(dataFrame, confData)
+
+    def analysisOfTurbines(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        dataFrameMerge = self.filterCustomForTurbines(dataFrameMerge, confData)
+        self.turbinesAnalysis(dataFrameMerge, outputAnalysisDir, confData)
+
+    def turbinesAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        pass

+ 0 - 0
dataAnalysisBehavior/common/__init__.py


+ 166 - 0
dataAnalysisBehavior/common/commonBusiness.py

@@ -0,0 +1,166 @@
+import pandas as pd
+import numpy as np
+import chardet  
+from algorithmContract.confBusiness import *
+
+
+class CommonBusiness:
+    def getFloat32(self):
+        return "float32"
+
+    def isNone(self, value):
+        return value is None
+
+    def getUseColumns(self, confData: ConfBusiness):
+        useColumns = []
+
+        if not self.isNone(confData.field_turbine_time):
+            useColumns.append(confData.field_turbine_time)
+
+        if not self.isNone(confData.field_turbine_name):
+            useColumns.append(confData.field_turbine_name)
+
+        if not self.isNone(confData.field_wind_speed):
+            useColumns.append(confData.field_wind_speed)
+
+        if not self.isNone(confData.field_power):
+            useColumns.append(confData.field_power)
+
+        if not self.isNone(confData.field_pitch_angle1):
+            useColumns.append(confData.field_pitch_angle1)
+
+        if not self.isNone(confData.field_rotor_speed):
+            useColumns.append(confData.field_rotor_speed)
+
+        if not self.isNone(confData.field_gen_speed):
+            useColumns.append(confData.field_gen_speed)
+
+        if not self.isNone(confData.field_torque):
+            useColumns.append(confData.field_torque)
+
+        if not self.isNone(confData.field_wind_dir):
+            useColumns.append(confData.field_wind_dir)
+
+        if not self.isNone(confData.field_angle_included):
+            useColumns.append(confData.field_angle_included)
+
+        if not self.isNone(confData.field_nacelle_pos):
+            useColumns.append(confData.field_nacelle_pos)
+
+        if not self.isNone(confData.field_env_temp):
+            useColumns.append(confData.field_env_temp)
+
+        if not self.isNone(confData.field_nacelle_temp):
+            useColumns.append(confData.field_nacelle_temp)
+
+        if not self.isNone(confData.field_turbine_state):
+            useColumns.append(confData.field_turbine_state)
+
+        if not self.isNone(confData.field_Cabin_Vibrate_X):
+            useColumns.append(confData.field_Cabin_Vibrate_X)
+
+        if not self.isNone(confData.field_Cabin_Vibrate_Y):
+            useColumns.append(confData.field_Cabin_Vibrate_Y)
+
+        if not self.isNone(confData.field_activePowerSet):
+            useColumns.append(confData.field_activePowerSet)
+
+        if not self.isNone(confData.field_activePowerAvailable):
+            useColumns.append(confData.field_activePowerAvailable)
+
+        if not self.isNone(confData.field_temperature_large_components):
+            temperature_cols = confData.field_temperature_large_components.split(
+                ',')
+            for temperatureColumn in temperature_cols:
+                useColumns.append(temperatureColumn)
+
+        return useColumns
+
+    def recalculationOfGeneratorSpeedforShow(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        if not self.isNone(confData.value_gen_speed_multiple) and confData.field_gen_speed in dataFrame.columns:
+            dataFrame[confData.field_gen_speed] = dataFrame[confData.field_gen_speed] * \
+                confData.value_gen_speed_multiple
+            dataFrame[Field_GeneratorTorque] = dataFrame[confData.field_gen_speed]
+
+    def contractGuaranteePowerCurveData(self, csvPowerCurveFilePath, confData: ConfBusiness):
+        with open(csvPowerCurveFilePath, 'rb') as f:  
+            result = chardet.detect(f.read())  
+        
+        dataFrameGuaranteePowerCurve = pd.read_csv(csvPowerCurveFilePath, encoding= result['encoding'] )
+
+        return dataFrameGuaranteePowerCurve
+
+    def calculateTSR(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        """
+        使用叶轮/主轴转速、风速、叶轮直径,计算叶尖速比(TSR)
+        """
+        # Calculate tsr
+        if not self.isNone(confData.rotor_diameter) \
+           and not self.isNone(confData.field_wind_speed) and confData.field_wind_speed in dataFrame.columns \
+           and not self.isNone(confData.field_rotor_speed) and confData.field_rotor_speed in dataFrame.columns:
+            dataFrame[confData.field_rotor_speed] = pd.to_numeric(
+                dataFrame[confData.field_rotor_speed], errors='coerce')
+            dataFrame[confData.field_wind_speed] = pd.to_numeric(
+                dataFrame[confData.field_wind_speed], errors='coerce')
+
+            rotor_diameter = pd.to_numeric(
+                confData.rotor_diameter, errors='coerce')
+            
+            dataFrame[Field_PowerFloor] = dataFrame[confData.field_power].apply(  
+                lambda x: int(x / 10) * 10 if pd.notnull(x) else np.nan  # 保留NaN值  
+            )
+
+            dataFrame[Field_TSR] = (dataFrame[confData.field_rotor_speed] * 0.104667 *
+                                    (rotor_diameter / 2)) / dataFrame[confData.field_wind_speed]
+            
+    def calculateCp2(self,dataFrame: pd.DataFrame,airDensity,rotorDiameter,fieldWindSpeed,fieldActivePower):
+        """
+        使用有功功率、风速、空气密度、叶轮直径,计算风能利用系数(Cp)
+        """
+        # Calculate cp
+        if not self.isNone(airDensity) and not self.isNone(rotorDiameter) \
+           and not self.isNone(fieldWindSpeed) and fieldWindSpeed in dataFrame.columns \
+           and not self.isNone(fieldActivePower) and fieldActivePower in dataFrame.columns:
+            dataFrame[fieldActivePower] = pd.to_numeric(
+                dataFrame[fieldActivePower], errors='coerce')
+            dataFrame[fieldWindSpeed] = pd.to_numeric(
+                dataFrame[fieldWindSpeed], errors='coerce')
+
+            rotor_diameter = pd.to_numeric(
+                rotorDiameter, errors='coerce')
+            air_density = pd.to_numeric(airDensity, errors='coerce')
+
+            dataFrame[Field_PowerFloor] = dataFrame[fieldActivePower].apply(  
+                lambda x: int(x / 10) * 10 if pd.notnull(x) else np.nan  # 保留NaN值  
+            )
+
+            dataFrame[Field_Cp] = dataFrame[fieldActivePower] * 1000 / \
+                (0.5 * np.pi * air_density *
+                 (rotor_diameter ** 2) / 4 * dataFrame[fieldWindSpeed] ** 3)
+
+    def calculateCp(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        """
+        使用有功功率、风速、空气密度、叶轮直径,计算风能利用系数(Cp)
+        """
+        self.calculateCp2(dataFrame,confData.density_air,confData.rotor_diameter,confData.field_wind_speed,confData.field_power)
+        # # Calculate cp
+        # if not self.isNone(confData.density_air) and not self.isNone(confData.rotor_diameter) \
+        #    and not self.isNone(confData.field_wind_speed) and confData.field_wind_speed in dataFrame.columns \
+        #    and not self.isNone(confData.field_power) and confData.field_power in dataFrame.columns:
+        #     dataFrame[confData.field_power] = pd.to_numeric(
+        #         dataFrame[confData.field_power], errors='coerce')
+        #     dataFrame[confData.field_wind_speed] = pd.to_numeric(
+        #         dataFrame[confData.field_wind_speed], errors='coerce')
+
+        #     rotor_diameter = pd.to_numeric(
+        #         confData.rotor_diameter, errors='coerce')
+        #     air_density = pd.to_numeric(confData.density_air, errors='coerce')
+
+        #     dataFrame[Field_PowerFloor] = dataFrame[confData.field_power].apply(  
+        #         lambda x: int(x / 10) * 10 if pd.notnull(x) else np.nan  # 保留NaN值  
+        #     )
+
+        #     dataFrame[Field_Cp] = dataFrame[confData.field_power] * 1000 / \
+        #         (0.5 * np.pi * air_density *
+        #          (rotor_diameter ** 2) / 4 * dataFrame[confData.field_wind_speed] ** 3)
+            

+ 20 - 0
dataAnalysisBehavior/common/turbineInfo.py

@@ -0,0 +1,20 @@
+from algorithmContract.confBusiness import charset_unify,Field_NameOfTurbine,ConfBusiness
+import pandas as pd
+
+
+def loadTurbineInfo(turbineInfoFilePathCSV,charset=charset_unify):
+    """
+    通过机组信息csv文件,载入机组信息
+
+    参数:
+    turbineInfoFilePathCSV (str): 机组信息csv文件路径
+    charset (str): 读取文件编码字符集,默认为utf-8
+
+    返回:
+    pandas.DataFrame : 机组信息
+    """
+    dataFrameOfTurbine= pd.read_csv(turbineInfoFilePathCSV,encoding=charset)
+
+    return dataFrameOfTurbine
+    
+    

+ 9 - 0
dataAnalysisBehavior/setup.py

@@ -0,0 +1,9 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='dataAnalysisBehavior',
+    version='1.0.202403261044',
+    description='Data Analysis Behavior Package', # 描述信息
+    author='Xie Zhou Yang', # 作者
+    packages=find_packages()
+)

+ 4 - 0
dataAnalysisBusiness/__init__.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import *
+__all__=['algorithm','common']

+ 1 - 0
dataAnalysisBusiness/algorithm/__init__.py

@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-

+ 86 - 0
dataAnalysisBusiness/algorithm/cabinVibrateAnalyst.py

@@ -0,0 +1,86 @@
+import os
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import plotly.express as px  
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+from windrose import WindroseAxes
+import matplotlib as mpl
+
+class CabinVibrateAnalyst(Analyst):
+    '''
+    风电机组机舱振动分析
+    '''
+    def typeAnalyst(self):
+        return "cabin_vibrate"
+    
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.CabinVibrateAnalysis(dataFrameMerge, outputAnalysisDir, confData)
+    
+    def CabinVibrateAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 检查所需列是否存在
+        required_columns = {confData.field_wind_dir, confData.field_wind_speed, confData.field_Cabin_Vibrate_X, confData.field_Cabin_Vibrate_Y}
+        # Cabin_Vibrate_X为左右振动 Cabin_Vibrate_Y为前后振动
+
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+        
+        dataFrameMerge = dataFrameMerge.dropna(axis=0, how='any')
+        
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            # 风向- 左右振动 - 风速
+            fig = plt.figure(figsize=(6,6))
+            pointsize = 6 # 散点的大小
+            ax = WindroseAxes.from_ax()
+            ax.set_theta_zero_location("N")
+            ax.set_theta_direction('clockwise')
+            ax.set_xticks([(i/8)*np.pi for i in range(16)])
+            ax.set_xticklabels(['N', 'NNE', 'NE', 'ENE', 'E','ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'])
+            #ax.set_rlim(0, 0.6, 0.1, auto=False)
+            # 设定极轴的范围,即设定图片显示振动大小的范围
+            ax.set_rlabel_position(34)
+            # 将显示极轴刻度的位置设在34°方向上,即位于NNE与NE的中间
+            plt.scatter(np.radians(group[confData.field_wind_dir]), group[confData.field_Cabin_Vibrate_Y],
+                        c=group[confData.field_wind_speed], cmap='viridis_r', alpha=1, s=pointsize)
+            plt.title('WindDirecton-Cabin_VibrationX-Windspeed')
+            locator = mpl.ticker.MultipleLocator(1)
+            # 设置颜色条上的风速间隔为1
+            cb = plt.colorbar(ticks=locator, pad=0.05, shrink=0.65) 
+            # 颜色条与图像保持一定距离,防止重叠现象
+            cb.ax.tick_params()
+            cb.ax.set_title('Wind Speedd(m/s)')
+            # 保存图像
+            plt.savefig(os.path.join(outputAnalysisDir, "{}Cabin_Vibrate_X.png".format(name)), bbox_inches='tight', dpi=120)
+            
+            # 风向- 前后振动 - 风速
+            fig = plt.figure(figsize=(6,6))
+            pointsize = 6 # 散点的大小
+            ax = WindroseAxes.from_ax()
+            ax.set_theta_zero_location("N")
+            ax.set_theta_direction('clockwise')
+            ax.set_xticks([(i/8)*np.pi for i in range(16)])
+            ax.set_xticklabels(['N', 'NNE', 'NE', 'ENE', 'E','ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'])
+            #ax.set_rlim(0, 0.6, 0.1, auto=False)
+            # 设定极轴的范围,即设定图片显示振动大小的范围
+            ax.set_rlabel_position(34)
+            # 将显示极轴刻度的位置设在34°方向上,即位于NNE与NE的中间
+            plt.scatter(np.radians(group[confData.field_wind_dir]), group[confData.field_Cabin_Vibrate_Y],
+                        c=group[confData.field_wind_speed], cmap='viridis_r', alpha=1, s=pointsize)
+            plt.title('WindDirecton-Cabin_VibrationX-Windspeed')
+            locator = mpl.ticker.MultipleLocator(1)
+            # 设置颜色条上的风速间隔为1
+            cb = plt.colorbar(ticks=locator, pad=0.05, shrink=0.65) 
+            # 颜色条与图像保持一定距离,防止重叠现象
+            cb.ax.tick_params()
+            cb.ax.set_title('Wind Speedd(m/s)')
+            # 保存图像
+            plt.savefig(os.path.join(outputAnalysisDir, "{}Cabin_Vibrate_Y.png".format(name)), bbox_inches='tight', dpi=120) 
+
+

+ 247 - 0
dataAnalysisBusiness/algorithm/cpAnalyst.py

@@ -0,0 +1,247 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import seaborn as sns
+import matplotlib.pyplot as plt
+from matplotlib.ticker import MultipleLocator
+from behavior.analystExcludeRatedPower import AnalystExcludeRatedPower
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class CpAnalyst(AnalystExcludeRatedPower):
+    """
+    风电机组风能利用系数分析
+    """
+
+    def typeAnalyst(self):
+        return "cp"        
+
+    def turbinesAnalysis(self, dataFrameMerge:pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):        
+        # 检查所需列是否存在
+        required_columns = {confData.field_wind_speed,
+                            Field_Cp, Field_PowerFloor}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+        
+        self.drawLineGraphForTurbine(dataFrameMerge,outputAnalysisDir,confData)
+
+    def drawLineGraphForTurbine(self,dataFrameMerge:pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        sns.set_palette('deep')
+
+        upLimitOfPower = confData.rated_power*0.9
+        value_step = 0.5
+        
+        grouped = dataFrameMerge.groupby([Field_NameOfTurbine,Field_PowerFloor]).agg(
+            cp=('cp', 'median'),
+            cp_max=('cp', 'max'),
+            cp_min=('cp', 'min'),
+        ).reset_index()
+
+        # Rename columns post aggregation for clarity
+        grouped.columns = [Field_NameOfTurbine,Field_PowerFloor,  Field_CpMedian, 'cp_max', 'cp_min']
+        # Sort by power_floor
+        grouped = grouped.sort_values(by=[Field_NameOfTurbine,Field_PowerFloor])
+
+        fig, ax = plt.subplots()
+
+        ax = sns.lineplot(x=Field_PowerFloor, y=Field_CpMedian, data=grouped,
+                          hue=Field_NameOfTurbine)
+        # 绘制合同功率曲线  
+        ax.plot(self.dataFrameContractOfTurbine[Field_PowerFloor], self.dataFrameContractOfTurbine[Field_Cp], marker='o',  
+                c='red', label='Contract Guarantee Cp')  
+
+        ax.xaxis.set_major_locator(MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                        confData.graphSets["activePower"]) and not self.common.isNone(
+                        confData.graphSets["activePower"]["step"]) else 250))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_xlim(0, upLimitOfPower)
+
+        ax.yaxis.set_major_locator(MultipleLocator(
+            confData.graphSets["cp"]["step"] if not self.common.isNone(confData.graphSets["cp"]["step"]) else value_step))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_ylim(confData.graphSets["cp"]["min"] if not self.common.isNone(confData.graphSets["cp"]["min"])
+                    else 0, confData.graphSets["cp"]["max"] if not self.common.isNone(confData.graphSets["cp"]["max"]) else 2)
+
+        ax.set_title('Cp-Distribution')
+        # plt.legend(ncol=4)
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.legend(title='Turbine', bbox_to_anchor=(1.02, 0.5),
+                   ncol=2, loc='center left', borderaxespad=0.)
+        plt.savefig(os.path.join(
+            outputAnalysisDir, "{}-Cp-Distribution.png".format(confData.farm_name)), bbox_inches='tight', dpi=300)
+        plt.close()
+
+        groupedX=grouped.groupby(Field_NameOfTurbine)
+
+        for name,group in groupedX:
+            color = ["lightgrey"] * len(dataFrameMerge[Field_NameOfTurbine].unique())
+            fig, ax = plt.subplots(figsize=(8, 8))
+            ax = sns.lineplot(x=Field_PowerFloor, y=Field_CpMedian, data=grouped, hue=Field_NameOfTurbine,
+                              palette=sns.color_palette(color), legend=False)
+            ax = sns.lineplot(x=Field_PowerFloor, y=Field_CpMedian, data=group,
+                              color='darkblue', legend=False)
+            
+            # 绘制合同功率曲线  
+            ax.plot(self.dataFrameContractOfTurbine[Field_PowerFloor], self.dataFrameContractOfTurbine[Field_Cp], marker='o',  
+                c='red', label='Contract Guarantee Cp')  
+
+            ax.xaxis.set_major_locator(
+                MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                        confData.graphSets["activePower"]) and not self.common.isNone(
+                        confData.graphSets["activePower"]["step"]) else 250))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_xlim(0, upLimitOfPower)
+
+            ax.yaxis.set_major_locator(MultipleLocator(
+                confData.graphSets["cp"]["step"] if not self.common.isNone(confData.graphSets["cp"]["step"]) else value_step))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_ylim(confData.graphSets["cp"]["min"] if not self.common.isNone(confData.graphSets["cp"]["min"])
+                        else 0, confData.graphSets["cp"]["max"] if not self.common.isNone(confData.graphSets["cp"]["max"]) else 1)
+
+            ax.set_title('turbine name={}'.format(name))
+            plt.xticks(rotation=45)  # 旋转45度
+            plt.legend(title='Turbine', bbox_to_anchor=(1.02, 0.5),
+                   ncol=2, loc='center left', borderaxespad=0.)
+            plt.savefig(os.path.join(outputAnalysisDir, "{}.png".format(
+                name)), bbox_inches='tight', dpi=120)
+            plt.close()
+
+
+    def generate_cp_distribution(self, csvFileDirOfCp, confData: ConfBusiness, encoding='utf-8'):
+        """
+        Generates Cp distribution plots for turbines in a wind farm.
+
+        Parameters:
+        - csvFileDirOfCp: str, path to the directory containing input CSV files.
+        - farm_name: str, name of the wind farm.
+        - encoding: str, encoding of the input CSV files. Defaults to 'utf-8'.
+        """
+
+        output_path = csvFileDirOfCp
+
+        field_Name_Turbine = "turbine_name"
+        x_name = 'power_floor'
+        y_name = 'cp'
+        upLimitOfPower = confData.rated_power*0.9
+        value_step = 0.5
+
+        sns.set_palette('deep')
+        res = pd.DataFrame()
+        for root, dir_names, file_names in dir.list_directory(csvFileDirOfCp):
+            for file_name in file_names:
+
+                if not file_name.endswith(CSVSuffix):
+                    continue
+
+                file_path = os.path.join(root, file_name)
+                print(file_path)
+                frame = pd.read_csv(file_path, encoding=encoding)
+                frame = frame[(frame[x_name] > 0)]
+                turbine_name = file_name.split(CSVSuffix)[0]
+                frame[field_Name_Turbine] = turbine_name
+
+                res = pd.concat(
+                    [res, frame.loc[:, [field_Name_Turbine, x_name, y_name]]], axis=0)
+
+        ress = res.reset_index()
+
+        fig, ax = plt.subplots()
+
+        ax = sns.lineplot(x=x_name, y=y_name, data=ress,
+                          hue=field_Name_Turbine)
+
+        ax.xaxis.set_major_locator(MultipleLocator(200))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_xlim(0, upLimitOfPower)
+
+        ax.yaxis.set_major_locator(MultipleLocator(
+            confData.graphSets["cp"]["step"] if not self.common.isNone(confData.graphSets["cp"]["step"]) else value_step))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_ylim(confData.graphSets["cp"]["min"] if not self.common.isNone(confData.graphSets["cp"]["min"])
+                    else 0, confData.graphSets["cp"]["max"] if not self.common.isNone(confData.graphSets["cp"]["max"]) else 2)
+
+        ax.set_title('Cp-Distribution')
+        # plt.legend(ncol=4)
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.legend(title='turbine', bbox_to_anchor=(1.02, 0.5),
+                   ncol=2, loc='center left', borderaxespad=0.)
+        plt.savefig(os.path.join(
+            output_path, "{}-Cp-Distribution.png".format(confData.farm_name)), bbox_inches='tight', dpi=300)
+        plt.close()
+
+        grouped = ress.groupby(field_Name_Turbine)
+        for name, group in grouped:
+            color = ["lightgrey"] * len(ress[field_Name_Turbine].unique())
+            fig, ax = plt.subplots(figsize=(8, 8))
+            ax = sns.lineplot(x=x_name, y=y_name, data=ress, hue=field_Name_Turbine,
+                              palette=sns.color_palette(color), legend=False)
+            ax = sns.lineplot(x=x_name, y=y_name, data=group,
+                              color='darkblue', legend=False)
+
+            ax.xaxis.set_major_locator(
+                MultipleLocator(200))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_xlim(0, upLimitOfPower)
+
+            ax.yaxis.set_major_locator(MultipleLocator(
+                confData.graphSets["cp"]["step"] if not self.common.isNone(confData.graphSets["cp"]["step"]) else value_step))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_ylim(confData.graphSets["cp"]["min"] if not self.common.isNone(confData.graphSets["cp"]["min"])
+                        else 0, confData.graphSets["cp"]["max"] if not self.common.isNone(confData.graphSets["cp"]["max"]) else 1)
+
+            ax.set_title('turbine name={}'.format(name))
+            plt.xticks(rotation=45)  # 旋转45度
+            plt.savefig(os.path.join(output_path, "{}.png".format(
+                name)), bbox_inches='tight', dpi=120)
+            plt.close()
+
+    def plot_cp_distribution(self, csvFileDir,  farm_name):
+        field_Name_Turbine = "设备名"
+        x_name = 'power_floor'
+        y_name = 'cp'
+        # Create the output path based on the farm name
+        output_path = csvFileDir  # output_path_template.format(farm_name)
+        # Ensure the output directory exists
+        os.makedirs(output_path, exist_ok=True)
+        print(csvFileDir)
+        # Initialize a DataFrame to store results
+        res = pd.DataFrame()
+
+        # Walk through the input directory to process each file
+        for root, _, file_names in dir.list_directory(csvFileDir):
+            for file_name in file_names:
+                full_path = os.path.join(root, file_name)
+                frame = pd.read_csv(full_path, encoding='gbk')
+                turbine_name = file_name.split(CSVSuffix)[0]
+                print("turbine_name={}".format(turbine_name))
+                frame[field_Name_Turbine] = turbine_name
+                res = pd.concat(
+                    [res, frame.loc[:, [field_Name_Turbine, x_name, y_name]]], axis=0)
+
+        # Reset index for plotting
+        ress = res.reset_index(drop=True)
+
+        # Plot combined Cp distribution for all turbines
+        fig = make_subplots(rows=1, cols=1)
+        for name, group in ress.groupby(field_Name_Turbine):
+            fig.add_trace(go.Scatter(
+                x=group[x_name], y=group[y_name], mode='lines', name=name))
+
+        fig.update_layout(title_text='{} Cp分布'.format(
+            farm_name), xaxis_title=x_name, yaxis_title=y_name)
+        fig.write_image(os.path.join(
+            output_path, "{}Cp分布.png".format(farm_name)), scale=3)
+
+        # Plot individual Cp distributions
+        unique_turbines = ress[field_Name_Turbine].unique()
+        for name in unique_turbines:
+            individual_fig = make_subplots(rows=1, cols=1)
+            # Add all turbines in grey
+            for turbine in unique_turbines:
+                group = ress[ress[field_Name_Turbine] == turbine]
+                individual_fig.add_trace(go.Scatter(
+                    x=group[x_name], y=group[y_name], mode='lines', name=turbine, line=dict(color='lightgrey')))
+
+            # Highlight the current turbine in dark blue
+            group = ress[ress[field_Name_Turbine] == name]
+            individual_fig.add_trace(go.Scatter(
+                x=group[x_name], y=group[y_name], mode='lines', name=name, line=dict(color='darkblue')))
+
+            individual_fig.update_layout(title_text='设备名={}'.format(name))
+            individual_fig.write_image(os.path.join(
+                output_path, "all-{}.png".format(name)), scale=2)

+ 109 - 0
dataAnalysisBusiness/algorithm/cpTrendAnalyst.py

@@ -0,0 +1,109 @@
+import os
+import pandas as pd
+import numpy as np
+from plotly.subplots import make_subplots
+import plotly.graph_objects as go
+import matplotlib.pyplot as plt
+from matplotlib.ticker import MultipleLocator
+from behavior.analystExcludeRatedPower import AnalystExcludeRatedPower
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class CpTrendAnalyst(AnalystExcludeRatedPower):
+    """
+    风电机组风能利用系数时序分析
+    """
+
+    def typeAnalyst(self):
+        return "cp_trend"
+
+    def turbinesAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        self.drawCpTrend(dataFrameMerge, outputAnalysisDir, confData)
+
+    def drawCpTrend(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 检查所需列是否存在
+        required_columns = {Field_Cp, Field_YearMonthDay}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            # # 计算四分位数和IQR
+            Q1 = group[Field_Cp].quantile(0.25)
+            Q3 = group[Field_Cp].quantile(0.85)
+            IQR = Q3 - Q1
+            # 定义离群值的范围
+            lower_bound = Q1 - 1.5 * IQR
+            upper_bound = Q3 + 1.5 * IQR
+
+            # 筛选掉离群值
+            filtered_group = group[(group[Field_Cp] >= lower_bound) & (
+                group[Field_Cp] <= upper_bound)]
+
+            # 创建箱线图
+            fig = go.Figure()
+
+            fig.add_trace(go.Box(
+                x=filtered_group[Field_YearMonthDay],  # 设置x轴数据为日期
+                y=filtered_group[Field_Cp],  # 设置y轴数据为风能利用系数
+                # boxpoints='outliers',  # 显示异常值(偏离值),不显示数据的所有点(只显示异常值)
+                boxpoints=False,  # 不显示偏离值
+                marker=dict(color='lightgoldenrodyellow',
+                            size=1),  # 设置偏离值的颜色和大小
+                line=dict(color='lightgray', width=2),  # 设置箱线和须线的颜色为灰色,粗细为2
+                fillcolor='rgba(200, 200, 200, 0.5)',  # 设置箱体的填充颜色和透明度
+                name='Cp'  # 图例名称
+            ))
+
+            # 对于每个箱线图的中位数,绘制一个蓝色点
+            medians = filtered_group.groupby(filtered_group[Field_YearMonthDay])[
+                Field_Cp].median()
+            fig.add_trace(go.Scatter(
+                x=medians.index,
+                y=medians.values,
+                mode='markers',
+                marker=dict(color='orange', size=3),
+                name='Median Cp'  # 中位数标记的图例名称
+            ))
+
+            # 设置图表的标题和轴标签
+            fig.update_layout(
+                title={
+                    'text': f'Cp Trend Turbine Name {name}',
+                    # 'y': 1,
+                    'x': 0.5,
+                    # 'xanchor': 'center',
+                    # 'yanchor': 'top'
+                },
+                xaxis_title='Time',
+                yaxis_title='Cp',
+                xaxis=dict(
+                    tickmode='auto',  # 自动设置x轴刻度,以适应日期数据
+                    tickformat='%Y-%m-%d',  # 设置x轴时间格式
+                    showgrid=True,  # 显示网格线
+                    gridcolor='lightgray',  # setting y-axis gridline color to black
+                    tickangle=-45,
+                    linecolor='black',  # 设置y轴坐标系线颜色为黑色
+                    ticklen=5,  # 设置刻度线的长度
+                ),
+                yaxis=dict(
+                    dtick=confData.graphSets["cp"]["step"] if not self.common.isNone(
+                        confData.graphSets["cp"]["step"]) else 0.5,  # 设置y轴刻度间隔为0.1
+                    range=[confData.graphSets["cp"]["min"] if not self.common.isNone(
+                        confData.graphSets["cp"]["min"]) else 0, confData.graphSets["cp"]["max"] if not self.common.isNone(confData.graphSets["cp"]["max"]) else 2],  # 设置y轴的范围从0到1
+                    showgrid=True,  # 显示网格线
+                    gridcolor='lightgray',  # setting y-axis gridline color to black
+                    linecolor='black',  # 设置y轴坐标系线颜色为黑色
+                    ticklen=5,  # 设置刻度线的长度
+                ),
+                paper_bgcolor='white',  # 设置纸张背景颜色为白色
+                plot_bgcolor='white',  # 设置图表背景颜色为白色
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+
+            # 保存图像
+            output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            fig.write_image(output_file, scale=2)

+ 209 - 0
dataAnalysisBusiness/algorithm/cpWindSpeedAnalyst.py

@@ -0,0 +1,209 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import seaborn as sns
+import matplotlib.pyplot as plt
+from matplotlib.ticker import MultipleLocator
+from behavior.analystExcludeRatedPower import AnalystExcludeRatedPower
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class CpWindSpeedAnalyst(AnalystExcludeRatedPower):
+    """
+    风电机组风能利用系数分析
+    """
+
+    def typeAnalyst(self):
+        return "cp_windspeed"
+
+    def turbineAnalysis(self,
+                        dataFrame,
+                        outputAnalysisDir,
+                        outputFilePath,
+                        confData: ConfBusiness,
+                        turbineName):
+
+        self.cp(dataFrame, outputFilePath,
+                confData.field_wind_speed, confData.field_rotor_speed, confData.field_power, confData.field_pitch_angle1, confData.rotor_diameter, confData.density_air)
+
+    def cp(self, dataFrame, output_path, wind_speed_col, field_rotor_speed, power_col, pitch_col, rotor_diameter, air_density):
+        print('rotor_diameter={}  air_density={}'.format(
+            rotor_diameter, air_density))
+
+        dataFrame = dataFrame.dropna(subset=[power_col])
+
+        dataFrame['power'] = dataFrame[power_col]  # Alias the power column
+        # Floor division by 10 and then multiply by 10
+        dataFrame['wind_speed_floor'] = (
+            dataFrame[wind_speed_col] / 1).astype(int) + 0.5
+        dataFrame['wind_speed'] = dataFrame[wind_speed_col].astype(float)
+        dataFrame['rotor_speed'] = dataFrame[field_rotor_speed].astype(float)
+
+        # Power coefficient calculation
+        dataFrame['power'] = pd.to_numeric(dataFrame['power'], errors='coerce')
+        dataFrame['wind_speed'] = pd.to_numeric(
+            dataFrame['wind_speed'], errors='coerce')
+        # rotor_diameter = pd.to_numeric(rotor_diameter, errors='coerce')
+        # air_density = pd.to_numeric(air_density, errors='coerce')
+
+        # # Calculate cp
+        # dataFrame['cp'] = dataFrame['power'] * 1000 / \
+        #     (0.5 * np.pi * air_density *
+        #      (rotor_diameter ** 2) / 4 * dataFrame['wind_speed'] ** 3)
+
+        # Group by wind_speed_floor and calculate mean, max, and min of the specified columns
+        grouped = dataFrame.groupby('wind_speed_floor').agg(
+            power=('power', 'mean'),
+            rotor_speed=('rotor_speed', 'mean'),
+            cp=('cp', 'mean'),
+            cp_max=('cp', 'max'),
+            cp_min=('cp', 'min'),
+        ).reset_index()
+
+        # Rename columns post aggregation for clarity
+        grouped.columns = ['wind_speed_floor', 'power',
+                           'rotor_speed', 'cp', 'cp_max', 'cp_min']
+
+        # Sort by wind_speed_floor
+        grouped = grouped.sort_values('wind_speed_floor')
+
+        # Write the dataframe to a CSV file
+        grouped.to_csv(output_path, index=False)
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.generate_cp_distribution(outputAnalysisDir, confData)
+
+    def generate_cp_distribution(self, csvFileDirOfCp, confData: ConfBusiness, encoding='utf-8'):
+        """
+        Generates Cp distribution plots for turbines in a wind farm.
+
+        Parameters:
+        - csvFileDirOfCp: str, path to the directory containing input CSV files.
+        - farm_name: str, name of the wind farm.
+        - encoding: str, encoding of the input CSV files. Defaults to 'utf-8'.
+        """
+
+        output_path = csvFileDirOfCp
+        value_step = 0.5
+
+        field_Name_Turbine = "turbine_name"
+        x_name = 'wind_speed_floor'
+        y_name = 'cp'
+
+        sns.set_palette('deep')
+        res = pd.DataFrame()
+
+        for root, dir_names, file_names in dir.list_directory(csvFileDirOfCp):
+            for file_name in file_names:
+
+                if not file_name.endswith(CSVSuffix):
+                    continue
+
+                file_path = os.path.join(root, file_name)
+                print(file_path)
+                frame = pd.read_csv(file_path, encoding=encoding)
+                turbine_name = file_name.split(CSVSuffix)[0]
+                frame[field_Name_Turbine] = turbine_name
+
+                res = pd.concat(
+                    [res, frame.loc[:, [field_Name_Turbine, x_name, y_name]]], axis=0)
+
+        ress = res.reset_index()
+
+        fig, ax = plt.subplots()
+        ax = sns.lineplot(x=x_name, y=y_name, data=ress,
+                          hue=field_Name_Turbine)
+
+        ax.xaxis.set_major_locator(MultipleLocator(1))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_xlim(0, 26)
+        ax.yaxis.set_major_locator(MultipleLocator(
+            confData.graphSets["cp"]["step"] if not self.common.isNone(confData.graphSets["cp"]["step"]) else value_step))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_ylim(confData.graphSets["cp"]["min"] if not self.common.isNone(confData.graphSets["cp"]["min"])
+                    else 0, confData.graphSets["cp"]["max"] if not self.common.isNone(confData.graphSets["cp"]["max"]) else 1)
+        ax.set_title('Cp-Distribution')
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.legend(ncol=4)
+        plt.savefig(os.path.join(
+            output_path, "{}-Cp-Distribution.png".format(confData.farm_name)), bbox_inches='tight', dpi=300)
+        plt.close()
+
+        grouped = ress.groupby(field_Name_Turbine)
+        for name, group in grouped:
+            color = ["lightgrey"] * len(ress[field_Name_Turbine].unique())
+            fig, ax = plt.subplots(figsize=(8, 8))
+            ax = sns.lineplot(x=x_name, y=y_name, data=ress, hue=field_Name_Turbine,
+                              palette=sns.color_palette(color), legend=False)
+            ax = sns.lineplot(x=x_name, y=y_name, data=group,
+                              color='darkblue', legend=False)
+
+            ax.xaxis.set_major_locator(
+                MultipleLocator(1))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_xlim(0, 26)
+            ax.yaxis.set_major_locator(MultipleLocator(
+                confData.graphSets["cp"]["step"] if not self.common.isNone(confData.graphSets["cp"]["step"]) else value_step))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_ylim(confData.graphSets["cp"]["min"] if not self.common.isNone(confData.graphSets["cp"]["min"])
+                        else 0, confData.graphSets["cp"]["max"] if not self.common.isNone(confData.graphSets["cp"]["max"]) else 1)
+            ax.set_title('turbine name={}'.format(name))
+            plt.xticks(rotation=45)  # 旋转45度
+            plt.savefig(os.path.join(output_path, "{}.png".format(
+                name)), bbox_inches='tight', dpi=120)
+            plt.close()
+
+    def plot_cp_distribution(self, csvFileDir,  farm_name):
+        field_Name_Turbine = "设备名"
+        x_name = 'wind_speed_floor'
+        y_name = 'cp'
+        # Create the output path based on the farm name
+        output_path = csvFileDir  # output_path_template.format(farm_name)
+        # Ensure the output directory exists
+        os.makedirs(output_path, exist_ok=True)
+        print(csvFileDir)
+        # Initialize a DataFrame to store results
+        res = pd.DataFrame()
+
+        # Walk through the input directory to process each file
+        for root, _, file_names in dir.list_directory(csvFileDir):
+            for file_name in file_names:
+                full_path = os.path.join(root, file_name)
+                frame = pd.read_csv(full_path, encoding='gbk')
+                turbine_name = file_name.split(CSVSuffix)[0]
+                print("turbine_name={}".format(turbine_name))
+                frame[field_Name_Turbine] = turbine_name
+                res = pd.concat(
+                    [res, frame.loc[:, [field_Name_Turbine, x_name, y_name]]], axis=0)
+
+        # Reset index for plotting
+        ress = res.reset_index(drop=True)
+
+        # Plot combined Cp distribution for all turbines
+        fig = make_subplots(rows=1, cols=1)
+        for name, group in ress.groupby(field_Name_Turbine):
+            fig.add_trace(go.Scatter(
+                x=group[x_name], y=group[y_name], mode='lines', name=name))
+
+        fig.update_layout(title_text='{} Cp分布'.format(
+            farm_name), xaxis_title=x_name, yaxis_title=y_name)
+        fig.write_image(os.path.join(
+            output_path, "{}Cp分布.png".format(farm_name)), scale=3)
+
+        # Plot individual Cp distributions
+        unique_turbines = ress[field_Name_Turbine].unique()
+        for name in unique_turbines:
+            individual_fig = make_subplots(rows=1, cols=1)
+            # Add all turbines in grey
+            for turbine in unique_turbines:
+                group = ress[ress[field_Name_Turbine] == turbine]
+                individual_fig.add_trace(go.Scatter(
+                    x=group[x_name], y=group[y_name], mode='lines', name=turbine, line=dict(color='lightgrey')))
+
+            # Highlight the current turbine in dark blue
+            group = ress[ress[field_Name_Turbine] == name]
+            individual_fig.add_trace(go.Scatter(
+                x=group[x_name], y=group[y_name], mode='lines', name=name, line=dict(color='darkblue')))
+
+            individual_fig.update_layout(title_text='设备名={}'.format(name))
+            individual_fig.write_image(os.path.join(
+                output_path, "all-{}.png".format(name)), scale=2)

+ 21 - 0
dataAnalysisBusiness/algorithm/dataIntegrityOfMinuteAnalyst.py

@@ -0,0 +1,21 @@
+import os
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import seaborn as sns
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from geopy.distance import geodesic
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+from .dataIntegrityOfSecondAnalyst import DataIntegrityOfSecondAnalyst
+
+
+class DataIntegrityOfMinuteAnalyst(DataIntegrityOfSecondAnalyst):
+    """
+    风电机组秒级数据完整度分析
+    """
+
+    def typeAnalyst(self):
+        return "data_integrity_minute"

+ 164 - 0
dataAnalysisBusiness/algorithm/dataIntegrityOfSecondAnalyst.py

@@ -0,0 +1,164 @@
+import os
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import seaborn as sns
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from geopy.distance import geodesic
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+import calendar
+import random 
+
+
+class DataIntegrityOfSecondAnalyst(Analyst):
+    """
+    风电机组秒级数据完整度分析
+    """
+
+    def typeAnalyst(self):
+        return "data_integrity_second"
+    
+    def filterCommon(self,dataFrame:pd.DataFrame, confData:ConfBusiness):        
+        return dataFrame
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        groupedDataFrame = self.dataIntegrityByMonth(
+            dataFrameMerge,confData, Field_NameOfTurbine )
+        print("groupedDataFrame : \n {}".format(groupedDataFrame.head()))
+        self.plotByAllMonth(groupedDataFrame, outputAnalysisDir,
+                  confData.farm_name, Field_NameOfTurbine)
+        
+    def generate_weighted_random(self):  
+        # 首先,尝试生成一个在91至100之间的随机数,这样大部分值都会落在这个区间  
+        if random.random() < 0.8:  # 假设80%的随机数应该在91至100之间  
+            return random.randint(91, 100)  
+        else:  # 剩下的20%则均匀分布在79至100之间(包括79但不包括100)  
+            return random.randint(79, 100)  
+        
+    def fullMonthIndex(self,start_time,end_time,turbine_name,new_frame):
+        months = (end_time.year - start_time.year)*12 + end_time.month - start_time.month
+        month_range = ['%04d-%02d' % (int(start_time.year + mon//12), int(mon%12+1)) for mon in range(start_time.month-1, start_time.month+months)]
+        month_index = pd.DataFrame(month_range,columns=[Field_YearMonth])
+
+        plot_res = pd.DataFrame()
+        grouped = new_frame.groupby(turbine_name)
+        for name,group in grouped:
+            group = pd.merge(group,month_index,on=Field_YearMonth,how='outer')
+            group['数据完整度%'] = group['数据完整度%'].fillna(0)
+            group[turbine_name] = name
+            group['year'] = group[Field_YearMonth].apply(lambda x:str(x).split('-')[0])
+            group['month'] = group[Field_YearMonth].apply(lambda x:str(x).split('-')[1])
+            plot_res = pd.concat([plot_res,group],axis=0,sort=False)
+
+        return plot_res
+
+    def dataIntegrityByMonth(self, dataFrameMerge:pd.DataFrame, confData:ConfBusiness,fieldTurbineName):
+        grouped = dataFrameMerge.groupby([dataFrameMerge.loc[:, confData.field_turbine_time].dt.year.rename('year'),
+                                          dataFrameMerge.loc[:, confData.field_turbine_time].dt.month.rename(
+                                              'month'),
+                                          dataFrameMerge.loc[:, fieldTurbineName]]).agg({'count'})[confData.field_turbine_time].rename({'count': '长度'}, axis=1)
+        
+        new_frame = grouped.reset_index('month')
+        new_frame = new_frame.assign(数据完整度=(100 * new_frame['长度'] / (
+            new_frame['month'].map(lambda x: calendar.mdays[x] * 24 * 3600 / confData.time_period))).round(decimals=0))
+        
+        # new_frame['数据完整度'] = [self.generate_weighted_random() for _ in range(len(new_frame))] 
+        new_frame = new_frame.rename(columns={'数据完整度': '数据完整度%'})
+
+        new_frame = new_frame.reset_index()
+        new_frame['month'] = new_frame['month'].astype(
+            str).apply(lambda x: x.zfill(2))
+        new_frame[Field_YearMonth] = new_frame['year'].astype(
+            str) + '-' + new_frame['month'].astype(str)
+        
+        new_frame = self.fullMonthIndex(confData.start_time,confData.end_time,fieldTurbineName,new_frame)
+
+        return new_frame
+
+    def plotByAllMonth(self, groupedDataFrame, outputAnalysisDir, farmName, fieldTurbineName):
+        title = 'time integrity check(%)'
+        fig, ax = plt.subplots(figsize=(18, 15), dpi=300)
+        # 风机数量小于月份
+        if len(set(groupedDataFrame.loc[:, Field_YearMonth])) > len(set(groupedDataFrame.loc[:, fieldTurbineName])):
+            result = pd.pivot(groupedDataFrame, index=fieldTurbineName,
+                              columns=Field_YearMonth, values="数据完整度%")
+            ax = sns.heatmap(data=result, square=True, annot=True,
+                             linewidths=0.3, cbar=False, fmt='g',)
+            bottom, top = ax.get_ylim()
+            ax.set_ylim(bottom + 0.5, top - 0.5)
+            ax.set_title(title)
+            plt.setp(ax.get_yticklabels(), rotation=0)
+            plt.setp(ax.get_xticklabels(), rotation=90)
+            plt.savefig(outputAnalysisDir +
+                        r'/{}数据完整度分析.png'.format(farmName), bbox_inches='tight')
+            plt.close()
+        else:
+            result = pd.pivot(groupedDataFrame, index=Field_YearMonth,
+                              columns=fieldTurbineName, values="数据完整度%")
+            ax = sns.heatmap(data=result, square=True, annot=True,
+                             linewidths=0.3, cbar=False, fmt='g',)
+            bottom, top = ax.get_ylim()
+            ax.set_ylim(bottom + 0.5, top - 0.5)
+            ax.set_title(title)
+            plt.setp(ax.get_yticklabels(), rotation=0)
+            plt.setp(ax.get_xticklabels(), rotation=90)
+            # 设置x轴标签斜向展示
+            plt.xticks(rotation=45)  # 旋转45度
+            plt.savefig(outputAnalysisDir +
+                        r'/{}数据完整度分析.png'.format(farmName), bbox_inches='tight')
+            plt.close()
+
+    def draw(self, groupedDataFrame, outputAnalysisDir, farmName, fieldTurbineName):
+        fig = make_subplots(rows=1, cols=1)
+        if len(set(groupedDataFrame[Field_YearMonth])) > len(set(groupedDataFrame[fieldTurbineName])):
+            result = groupedDataFrame.pivot(
+                index=fieldTurbineName, columns=Field_YearMonth, values="数据完整度%")
+            fig.add_trace(
+                go.Heatmap(
+                    z=result.values,
+                    x=result.columns,
+                    y=result.index,
+                    colorscale='Viridis',
+                    colorbar=dict(title='数据完整度%'),
+                    text=[[f"{value}" for value in row]
+                        for row in result.values],  # 显示的文本(百分比)
+                    texttemplate="%{text}",  # 使用文本模板
+                    yaxis=dict(  
+                        tickformat="%Y-%m",  # 设置y轴刻度的格式  
+                        tickmode="array",    # 如果需要,可以指定自定义的刻度标签  
+                        tickvals=result.columns.strftime("%Y-%m").tolist()  # 如果需要自定义刻度位置  
+                    )  
+                )
+            )
+        else:
+            result = groupedDataFrame.pivot(
+                index=Field_YearMonth, columns=fieldTurbineName, values="数据完整度%")
+            fig.add_trace(
+                go.Heatmap(
+                    z=result.values,
+                    x=result.columns,
+                    y=result.index,
+                    colorscale='Viridis',
+                    colorbar=dict(title='数据完整度%'),
+                    text=[[f"{value}" for value in row]
+                        for row in result.values],  # 显示的文本(百分比)
+                    texttemplate="%{text}",  # 使用文本模板
+                    xaxis=dict(  
+                        tickformat="%Y-%m",  # 设置y轴刻度的格式  
+                        tickmode="array",    # 如果需要,可以指定自定义的刻度标签  
+                        tickvals=result.index.strftime("%Y-%m").tolist()  # 如果需要自定义刻度位置  
+                    )
+                )
+            )
+
+        fig.update_layout(
+            title_text='{}-time integrity check(%)'.format(farmName),
+            xaxis_nticks=36
+        )
+
+        fig.write_image(outputAnalysisDir + '/' +
+                        '{}数据完整度分析.png'.format(farmName))

+ 411 - 0
dataAnalysisBusiness/algorithm/dataMarker.py

@@ -0,0 +1,411 @@
+import os
+import re
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.pyplot import MultipleLocator
+import math
+import pdb
+from algorithmContract.confBusiness import *  #将这个包里的全部加载
+
+intervalPower = 25
+intervalWindspeed = 0.25
+
+class DataMarker:
+    
+    #选取时间、风速、功率数据
+    def preprocessData(self,dataFrame:pd.DataFrame,confData:ConfBusiness):
+        timeStamp = dataFrame[confData.field_turbine_time]
+        activePower = dataFrame[confData.field_power]
+        windSpeed = dataFrame[confData.field_wind_speed]
+        dataFramePartOfSCADA = pd.concat([timeStamp,activePower,windSpeed], axis=1)
+        return dataFramePartOfSCADA
+    
+    #计算分仓数目
+    def calculateIntervals(self,activePowerMax, ratedPower, windSpeedCutOut):
+        binNumOfPower = math.floor((activePowerMax) / intervalPower) + 1 if (activePowerMax) >= ratedPower else math.floor(ratedPower / intervalPower)
+        binNumOfWindSpeed = math.ceil(windSpeedCutOut / intervalWindspeed)
+        return binNumOfPower, binNumOfWindSpeed
+
+    def calculateTopP(self,activePowerMax,ratedPower):
+        
+        TopP = 0   
+        if activePowerMax >= ratedPower: 
+            TopP = math.floor((activePowerMax - ratedPower) / intervalPower) + 1  
+        else:  
+            TopP = 0
+        return TopP
+
+    def chooseData(self,dataFramePartOfSCADA,dataFrame:pd.DataFrame,confData:ConfBusiness):
+        
+        # 初始化标签列
+        SM1 = dataFramePartOfSCADA.shape 
+        AA1 = SM1[0]  
+        lab = [[0] for _ in range(AA1)]
+        lab = pd.DataFrame(lab,columns=['lab'])
+        dataFramePartOfSCADA = pd.concat([dataFramePartOfSCADA,lab],axis=1)  #在tpv后加一列标签列
+        dataFramePartOfSCADA = dataFramePartOfSCADA.values
+        SM = dataFramePartOfSCADA.shape #(52561,4)
+        AA = SM[0] 
+        nCounter1 = 0 
+        DzMarch809_0 = np.zeros((AA, 3)) 
+        Point_line = np.zeros(AA, dtype=int)  
+        APower = dataFrame[confData.field_power].values
+        WSpeed = dataFrame[confData.field_wind_speed].values
+
+        for i in range(AA):
+            if (APower[i] > 10.0) & (WSpeed[i] > 0.0):
+                nCounter1 += 1  
+                DzMarch809_0[nCounter1-1, 0] = WSpeed[i]  
+                DzMarch809_0[nCounter1-1, 1] = APower[i] 
+                Point_line[nCounter1-1] = i+1  
+            if APower[i] <= 10: 
+                dataFramePartOfSCADA[i,SM[1]-1] = -1
+                
+            DzMarch809 = DzMarch809_0[:nCounter1, :] 
+            
+        return DzMarch809,nCounter1,dataFramePartOfSCADA,Point_line,SM
+
+    def gridCount(self,binNumOfWindSpeed,binNumOfPower,nCounter1,DzMarch809):  
+        # 遍历有效数据
+        XBoxNumber = np.ones((binNumOfPower, binNumOfWindSpeed),dtype=int) 
+        for i in range(nCounter1):             
+            for m in range(1, binNumOfPower + 1):  
+                if (DzMarch809[i,1] > (m - 1) * intervalPower) and (DzMarch809[i,1] <= m * intervalPower):  
+                    nWhichP = m  
+                    break  
+            for n in range(1, binNumOfWindSpeed + 1):  
+                if (DzMarch809[i, 0] > (n - 1) * intervalWindspeed) and (DzMarch809[i, 0] <= n * intervalWindspeed):  
+                    nWhichV = n  
+                    break  
+            if (nWhichP > 0) and (nWhichV > 0):  
+                XBoxNumber[nWhichP - 1][nWhichV - 1] += 1
+        for m in range(1,binNumOfPower+1):
+            for n in range(1,binNumOfWindSpeed+1):
+                XBoxNumber[m-1,n-1] = XBoxNumber[m-1,n-1] - 1
+        
+        return XBoxNumber
+
+    def percentageDots(self,XBoxNumber, binNumOfPower, binNumOfWindSpeed,axis):
+        
+        BoxPercent = np.zeros((binNumOfPower, binNumOfWindSpeed), dtype=float)     
+        BinSum = np.zeros((binNumOfPower if axis == 'power' else binNumOfWindSpeed, 1), dtype=int)
+        for i in range(1,1+(binNumOfPower if axis == 'power' else binNumOfWindSpeed)):
+            for m in range(1,(binNumOfWindSpeed if axis == 'power' else binNumOfPower)+1):  
+                BinSum[i-1] = BinSum[i-1] + (XBoxNumber[i-1,m-1] if axis == 'power' else XBoxNumber[m-1,i-1])
+            for m in range(1,(binNumOfWindSpeed if axis == 'power' else binNumOfPower)+1):  
+                if BinSum[i-1]>0:
+                    if axis == 'power':
+                        BoxPercent[i-1,m-1] = (XBoxNumber[i-1,m-1] / BinSum[i-1])*100
+                    else:
+                        BoxPercent[m-1,i-1] = (XBoxNumber[m-1,i-1] / BinSum[i-1])*100
+                        
+        return BoxPercent,BinSum
+
+    def maxBoxPercentage(self,BoxPercent, binNumOfPower, binNumOfWindSpeed, axis):
+        
+        BoxMaxIndex = np.zeros((binNumOfPower if axis == 'power' else binNumOfWindSpeed,1),dtype = int) 
+        BoxMax = np.zeros((binNumOfPower if axis == 'power' else binNumOfWindSpeed,1),dtype = float)  
+        for m in range(1,(binNumOfPower if axis == 'power' else binNumOfWindSpeed)+1):
+            BoxMaxIndex[m-1] = (np.argmax(BoxPercent[m-1, :])) if axis == 'power' else (np.argmax(BoxPercent[:, m-1]))
+            BoxMax[m-1] = (np.max(BoxPercent[m-1, :]))if axis == 'power' else (np.max(BoxPercent[:, m-1]))
+
+        return BoxMaxIndex, BoxMax
+
+    def extendBoxPercent(self,m, BoxMax,TopP,BoxMaxIndex,BoxPercent,binNumOfPower,binNumOfWindSpeed):
+        
+        DotDense = np.zeros(binNumOfPower)  
+        DotDenseLeftRight = np.zeros((binNumOfPower,2))
+        DotValve = m 
+        PDotDenseSum = 0
+        for i in range(binNumOfPower - TopP):
+            PDotDenseSum = BoxMax[i] 
+            iSpreadRight = 1  
+            iSpreadLeft = 1         
+            while PDotDenseSum < DotValve:  
+                if (BoxMaxIndex[i] + iSpreadRight) < binNumOfWindSpeed-1-1:  
+                    PDotDenseSum += BoxPercent[i, BoxMaxIndex[i] + iSpreadRight] 
+                    iSpreadRight += 1  
+                else:
+                    break             
+                if (BoxMaxIndex[i] - iSpreadLeft) > 0:  
+                    PDotDenseSum += BoxPercent[i, BoxMaxIndex[i] - iSpreadLeft] 
+                    iSpreadLeft += 1  
+                else:  
+                    break  
+            iSpreadRight = iSpreadRight-1
+            iSpreadLeft = iSpreadLeft-1
+        
+            DotDenseLeftRight[i, 0] = iSpreadLeft 
+            DotDenseLeftRight[i, 1] = iSpreadRight 
+            DotDense[i] = iSpreadLeft + iSpreadRight + 1    
+
+        return DotDenseLeftRight
+
+    def calculatePWidth(self,binNumOfPower,TopP,DotDenseLeftRight,PBinSum):
+        
+
+        PowerLimit = np.zeros(binNumOfPower, dtype=int)  
+        WidthAverage = 0    
+        WidthAverage_L = 0 
+        nCounter = 0  
+        PowerLimitValve = 6    
+        N_Pcount = 20  
+        for i in range(binNumOfPower - TopP):   
+            if (DotDenseLeftRight[i, 1] > PowerLimitValve) and (PBinSum[i] > N_Pcount):  
+                PowerLimit[i] = 1  
+            
+            if DotDenseLeftRight[i, 1] <= PowerLimitValve:  
+                WidthAverage += DotDenseLeftRight[i, 1]
+                WidthAverage_L += DotDenseLeftRight[i,1] 
+                nCounter += 1  
+        WidthAverage /= nCounter if nCounter > 0 else 1  
+        WidthAverage_L /= nCounter if nCounter > 0 else 1   
+
+        return WidthAverage, WidthAverage_L,PowerLimit
+
+    def amendMaxBox(self,binNumOfPower,TopP,PowerLimit,BoxMaxIndex):
+        
+
+        for i in range(1, binNumOfPower - TopP+1):  
+            if (PowerLimit[i] == 1) and (abs(BoxMaxIndex[i] - BoxMaxIndex[i - 1]) > 5):  
+                BoxMaxIndex[i] = BoxMaxIndex[i - 1] + 1  
+
+        return BoxMaxIndex
+
+    def markBoxLimit(self,binNumOfPower,binNumOfWindSpeed,TopP,CurveWidthR,CurveWidthL,BoxMaxIndex):
+        
+        BBoxRemove = np.zeros((binNumOfPower, binNumOfWindSpeed), dtype=int)  
+        for m in range(binNumOfPower - TopP): 
+            for n in range(int(BoxMaxIndex[m]) + int(CurveWidthR), binNumOfWindSpeed):
+                BBoxRemove[m, n] = 1  
+            for n in range(int(BoxMaxIndex[m]) - int(CurveWidthL)+1, 0, -1):   
+                BBoxRemove[m, n-1] = 2 
+        return BBoxRemove
+
+    def markBoxPLimit(self,binNumOfPower,binNumOfWindSpeed,TopP,CurveWidthR,PowerLimit,BoxPercent,BoxMaxIndex,mm_value:int,BBoxRemove,nn_value:int):
+        
+        BBoxLimit = np.zeros((binNumOfPower, binNumOfWindSpeed), dtype=int)  
+        for i in range(2, binNumOfPower - TopP):  
+            if PowerLimit[i] == 1:
+                BBoxLimit[i, int(BoxMaxIndex[i] + CurveWidthR + 1):binNumOfWindSpeed] = 1
+        IsolateValve = 3
+        for m in range(binNumOfPower - TopP):    
+            for n in range(int(BoxMaxIndex[m]) + int(CurveWidthR), binNumOfWindSpeed):    
+                if BoxPercent[m, n] < IsolateValve:   
+                    BBoxRemove[m, n] = 1
+
+        for m in range(binNumOfPower - TopP, binNumOfPower):   
+            for n in range(binNumOfWindSpeed):  
+                BBoxRemove[m, n] = 3
+        
+        # 标记功率主带拐点左侧的欠发网格  
+        for m in range(mm_value - 1, binNumOfPower - TopP): 
+            for n in range(int(nn_value) - 2):
+                BBoxRemove[m, n] = 2
+        
+        return BBoxLimit
+        
+    def markData(self,binNumOfPower, binNumOfWindSpeed,DzMarch809,BBoxRemove,nCounter1):
+        
+        DzMarch809Sel = np.zeros(nCounter1, dtype=int)
+        nWhichP = 0  
+        nWhichV = 0  
+        for i in range(nCounter1):   
+            for m in range( binNumOfPower ):   
+                if ((DzMarch809[i,1])> m * intervalPower) and ((DzMarch809[i,1]) <= (m+1) * intervalPower):  
+                    nWhichP = m  #m记录的是index
+                    break  
+            for n in range( binNumOfWindSpeed ):    
+                if DzMarch809[i,0] > ((n+1) * intervalWindspeed - intervalWindspeed/2) and DzMarch809[i,0] <= ((n+1) * intervalWindspeed + intervalWindspeed / 2):  
+                    nWhichV = n 
+                    break  
+            if nWhichP >= 0 and nWhichV >= 0:  
+                if BBoxRemove[nWhichP, nWhichV] == 1:   
+                    DzMarch809Sel[i] = 1  
+                elif BBoxRemove[nWhichP, nWhichV] == 2:  
+                    DzMarch809Sel[i] = 2  
+                elif BBoxRemove[nWhichP , nWhichV] == 3:  
+                    DzMarch809Sel[i] = 0  
+
+        return DzMarch809Sel
+        
+
+    def windowFilter(self,nCounter1,ratedPower,DzMarch809,DzMarch809Sel,Point_line):
+        
+
+        PVLimit = np.zeros((nCounter1, 3)) 
+        nLimitTotal = 0  
+        nWindowLength = 6  
+        LimitWindow = np.zeros(nWindowLength)
+        UpLimit = 0   
+        LowLimit = 0  
+        PowerStd = 30  
+        nWindowNum = np.floor(nCounter1/nWindowLength)
+        PowerLimitUp = ratedPower - 100  
+        PowerLimitLow = 100  
+
+        # 循环遍历每个窗口  
+        for i in range(int(nWindowNum)):  
+            start_idx = i * nWindowLength  
+            end_idx = start_idx + nWindowLength  
+            LimitWindow = DzMarch809[start_idx:end_idx, 1]  
+            
+            bAllInAreas = np.all(LimitWindow >= PowerLimitLow) and np.all(LimitWindow <= PowerLimitUp)  
+            if not bAllInAreas:  
+                continue  
+            
+            UpLimit = LimitWindow[0] + PowerStd  
+            LowLimit = LimitWindow[0] - PowerStd  
+            
+            bAllInUpLow = np.all(LimitWindow >= LowLimit) and np.all(LimitWindow <= UpLimit)  
+            if bAllInUpLow: 
+                DzMarch809Sel[start_idx:end_idx] = 4  
+    
+                for j in range(nWindowLength):  
+                    PVLimit[nLimitTotal, :2] = DzMarch809[start_idx + j, :2]  
+                    PVLimit[nLimitTotal, 2] = Point_line[start_idx + j]  # 对数据进行标识  
+                    nLimitTotal += 1  
+        return PVLimit,nLimitTotal
+
+    def store_points(self,DzMarch809, DzMarch809Sel,Point_line, nCounter1):  
+          
+        PVDot = np.zeros((nCounter1, 3))
+        PVBad = np.zeros((nCounter1, 3))  
+
+        nCounterPV = 0  
+        nCounterBad = 0 
+        for i in range(nCounter1):
+            if DzMarch809Sel[i] == 0:   
+                nCounterPV += 1 
+                PVDot[nCounterPV-1, :2] = DzMarch809[i, :2]
+                PVDot[nCounterPV-1, 2] = Point_line[i]  
+            elif DzMarch809Sel[i] in [1, 2, 3]:  
+                nCounterBad += 1  
+                PVBad[nCounterBad-1, :2] = DzMarch809[i, :2]  
+                PVBad[nCounterBad-1, 2] = Point_line[i]
+                    
+        return PVDot, nCounterPV,PVBad,nCounterBad  
+
+    def markAllData(self,nCounterPV,nCounterBad,dataFramePartOfSCADA,PVDot,PVBad,SM,nLimitTotal,PVLimit):
+
+        for i in range(nCounterPV):
+            dataFramePartOfSCADA[int(PVDot[i, 2] - 1), (SM[1]-1)] = 1   
+        #坏点  
+        for i in range(nCounterBad):  
+            dataFramePartOfSCADA[int(PVBad[i, 2] - 1),(SM[1]-1)] = 5  # 坏点标识  
+
+        # 对所有数据中的限电点进行标注   
+        for i in range(nLimitTotal):  
+            dataFramePartOfSCADA[int(PVLimit[i, 2] - 1),(SM[1]-1)] = 4  # 限电点标识  
+
+        return dataFramePartOfSCADA
+    
+    # 4. 数据可视化
+    def plotData(self,turbineName:str,ws:list, ap:list):
+        fig = plt.figure()
+        plt.scatter(ws, ap, s=1, c='black', marker='.')
+        ax = plt.gca()
+        ax.xaxis.set_major_locator(MultipleLocator(5))
+        ax.yaxis.set_major_locator(MultipleLocator(500))
+        plt.title(turbineName)
+        plt.xlim((0, 30))
+        plt.ylim((0, 2200))
+        plt.tick_params(labelsize=8)
+        plt.xlabel("V/(m$·$s$^{-1}$)", fontsize=8)
+        plt.ylabel("P/kW", fontsize=8)
+        plt.show()
+    
+    
+    def main(self,confData: ConfBusiness,dataFrame:pd.DataFrame):
+        dataFramePartOfSCADA = self.preprocessData(dataFrame, confData)
+        powerMax = dataFrame[confData.field_power].max()
+
+        binNumOfPower, binNumOfWindSpeed = self.calculateIntervals(powerMax,confData.rated_power,confData.rated_cut_out_windspeed)
+        TopP = self.calculateTopP(powerMax,confData.rated_power)
+        # 根据功率阈值对数据进行标签分配
+        DzMarch809,nCounter1,dataFramePartOfSCADA,Point_line,SM = self.chooseData(dataFramePartOfSCADA,dataFrame,confData)
+        XBoxNumber = self.gridCount(binNumOfWindSpeed,binNumOfPower,nCounter1,DzMarch809)
+        PBoxPercent,PBinSum = self.percentageDots(XBoxNumber, binNumOfPower, binNumOfWindSpeed, 'power')
+        VBoxPercent,VBinSum = self.percentageDots(XBoxNumber, binNumOfPower, binNumOfWindSpeed, 'speed')
+
+        PBoxMaxIndex, PBoxMaxP = self.maxBoxPercentage(PBoxPercent, binNumOfPower, binNumOfWindSpeed, 'power')
+        VBoxMaxIndex, VBoxMaxV = self.maxBoxPercentage(VBoxPercent, binNumOfPower, binNumOfWindSpeed, 'speed')
+        if PBoxMaxIndex[0] > 14: PBoxMaxIndex[0] = 9
+        DotDenseLeftRight = self.extendBoxPercent(90, PBoxMaxP,TopP,PBoxMaxIndex,PBoxPercent,binNumOfPower,binNumOfWindSpeed)
+        WidthAverage, WidthAverage_L,PowerLimit = self.calculatePWidth(binNumOfPower,TopP,DotDenseLeftRight,PBinSum)
+        PBoxMaxIndex = self.amendMaxBox(binNumOfPower,TopP,PowerLimit,PBoxMaxIndex)
+        # 计算功率主带的左右边界  
+        CurveWidthR = np.ceil(WidthAverage) + 2  
+        CurveWidthL = np.ceil(WidthAverage_L) + 2 
+        #确定功率主带的左上拐点,即额定风速位置的网格索引
+        CurveTop = np.zeros((2, 1), dtype=int)  
+        BTopFind = 0  
+        mm_value = None
+        nn_value = None
+        for m in range(binNumOfPower - TopP, 0, -1):
+            for n in range(int(np.floor(int(confData.rated_cut_in_windspeed) / intervalWindspeed)), binNumOfWindSpeed - 1):   
+                if (VBoxPercent[m, n - 1] < VBoxPercent[m, n]) and (VBoxPercent[m, n] <= VBoxPercent[m, n + 1]) and (XBoxNumber[m, n] >= 3):   
+                    CurveTop[0] = m  
+                    CurveTop[1] = n  #[第80个,第40个]
+                    BTopFind = 1
+                    mm_value = m
+                    nn_value = n
+                    break 
+            if BTopFind == 1:  
+                break 
+        #标记网格
+        BBoxRemove = self.markBoxLimit(binNumOfPower,binNumOfWindSpeed,TopP,CurveWidthR,CurveWidthL,PBoxMaxIndex)
+        if mm_value is not None and nn_value is not None:
+            BBoxLimit = self.markBoxPLimit(binNumOfPower,binNumOfWindSpeed,TopP,CurveWidthR,PowerLimit,PBoxPercent,PBoxMaxIndex,mm_value,BBoxRemove,nn_value)
+        DzMarch809Sel = self.markData(binNumOfPower, binNumOfWindSpeed,DzMarch809,BBoxRemove,nCounter1)
+        PVLimit,nLimitTotal = self.windowFilter(nCounter1,confData.rated_power,DzMarch809,DzMarch809Sel,Point_line)
+        #将功率滑动窗口主带平滑化
+        nSmooth = 0   
+        for i in range(binNumOfPower - TopP - 1):  
+            PVLeftDown = np.zeros(2)  
+            PVRightUp = np.zeros(2)   
+            if PBoxMaxIndex[i + 1] - PBoxMaxIndex[i] >= 1:  
+                # 计算左下和右上顶点的坐标  
+                PVLeftDown[0] = (PBoxMaxIndex[i]+1 + CurveWidthR) * 0.25 - 0.125  
+                PVLeftDown[1] = (i) * 25  
+                PVRightUp[0] = (PBoxMaxIndex[i+1]+1 + CurveWidthR) * 0.25 - 0.125  
+                PVRightUp[1] = (i+1) * 25  
+                    
+                for m in range(nCounter1):  
+                    # 检查当前点是否在锯齿区域内  
+                    if (DzMarch809[m, 0] > PVLeftDown[0]) and (DzMarch809[m, 0] < PVRightUp[0]) and (DzMarch809[m, 1] > PVLeftDown[1]) and (DzMarch809[m, 1] < PVRightUp[1]):
+                        # 检查斜率是否大于对角连线  
+                        if ((DzMarch809[m, 1] - PVLeftDown[1]) / (DzMarch809[m, 0] - PVLeftDown[0])) > ((PVRightUp[1] - PVLeftDown[1]) / (PVRightUp[0] - PVLeftDown[0])):
+                            # 如果在锯齿左上三角形中,则选中并增加锯齿平滑计数器  
+                            DzMarch809Sel[m] = 0  
+                            nSmooth += 1  
+        # DzMarch809Sel 数组现在包含了锯齿平滑的选择结果,nSmooth 是选中的点数
+        PVDot, nCounterPV,PVBad,nCounterBad = self.store_points(DzMarch809, DzMarch809Sel,Point_line, nCounter1)
+        #标注   
+        dataFramePartOfSCADA = self.markAllData(nCounterPV,nCounterBad,dataFramePartOfSCADA,PVDot,PVBad,SM,nLimitTotal,PVLimit)
+        A = dataFramePartOfSCADA[:,-1]
+        A=pd.DataFrame(A,columns=['lab'])
+
+        dataFrame = pd.concat([dataFrame,A],axis=1) 
+
+        """
+        标识	说明
+        5	坏点
+        4	限功率点
+        1	好点
+        0	null
+        -1	P<=10
+        """
+        print("lab unique :",dataFrame['lab'].unique())
+        # data=dataFrame[dataFrame['lab']==1]        
+        # self.plotData(data[Field_NameOfTurbine].iloc[0],data[confData.field_wind_speed],data[confData.field_power])
+
+        return dataFrame
+        
+
+    if __name__ == '__main__':
+        main()
+
+
+

+ 369 - 0
dataAnalysisBusiness/algorithm/dataProcessor.py

@@ -0,0 +1,369 @@
+import os
+from datetime import datetime
+import concurrent.futures
+import numpy as np
+import pandas as pd
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+from behavior.baseAnalyst import BaseAnalyst
+from behavior.analyst import Analyst
+from common.commonBusiness import CommonBusiness
+from algorithm.dataMarker import DataMarker
+
+
+class DataProcessor:
+    def __init__(self):
+        self.common = CommonBusiness()
+        self._baseAnalysts = []
+        self._analysts = []
+
+    def attachBaseAnalyst(self, analyst: BaseAnalyst):
+        if analyst not in self._analysts:
+            self._baseAnalysts.append(analyst)
+
+    def detachBaseAnalyst(self, analyst: BaseAnalyst):
+        try:
+            self._baseAnalysts.remove(analyst)
+        except ValueError:
+            pass
+
+    def attach(self, analyst: Analyst):
+        if analyst not in self._analysts:
+            self._analysts.append(analyst)
+
+    def detach(self, analyst: Analyst):
+        try:
+            self._analysts.remove(analyst)
+        except ValueError:
+            pass
+
+    def turbineNotify(self,
+                      dataFrameOfTurbine: pd.DataFrame,
+                      confData: ConfBusiness,
+                      turbineName):
+        for analyst in self._analysts:
+            outputAnalysisDir = analyst.getOutputAnalysisDir()
+
+            outputFilePath = r"{}/{}{}".format(
+                outputAnalysisDir, turbineName, CSVSuffix)
+
+            analyst.analysisOfTurbine(
+                dataFrameOfTurbine, outputAnalysisDir, outputFilePath, confData, turbineName)
+
+    def turbinesNotify(self, dataFrameOfTurbines: pd.DataFrame,  confData: ConfBusiness):
+        for analyst in self._analysts:
+            outputAnalysisDir = analyst.getOutputAnalysisDir()
+            analyst.analysisOfTurbines(
+                dataFrameOfTurbines, outputAnalysisDir, confData)
+
+    def baseAnalystTurbineNotify(self,
+                                 dataFrameOfTurbine: pd.DataFrame,
+                                 confData: ConfBusiness,
+                                 turbineName):
+        for analyst in self._baseAnalysts:
+            outputAnalysisDir = analyst.getOutputAnalysisDir()
+
+            outputFilePath = r"{}/{}{}".format(
+                outputAnalysisDir, turbineName, CSVSuffix)
+
+            analyst.analysisOfTurbine(
+                dataFrameOfTurbine, outputAnalysisDir, outputFilePath, confData, turbineName)
+
+    def baseAnalystNotify(self, dataFrameOfTurbines: pd.DataFrame,  confData: ConfBusiness):
+        for analyst in self._baseAnalysts:
+            outputAnalysisDir = analyst.getOutputAnalysisDir()
+            analyst.analysisOfTurbines(
+                dataFrameOfTurbines, outputAnalysisDir, confData)
+
+    def calculateAngleIncluded(self, array1, array2):
+        """
+        计算两个相同长度角度数组中两两对应角度值的偏差。
+        结果限制在-90°到+90°之间,并保留两位小数。
+
+        参数:
+        array1 (list): 第一个角度数组
+        array2 (list): 第二个角度数组
+
+        返回:
+        list: 两两对应角度的偏差列表
+        """
+        deviations = []
+        for angle1, angle2 in zip(array1, array2):
+            # 计算原始偏差
+            deviation = angle1 - angle2
+
+            # 调整偏差,使其位于-180°到+180°范围内
+            if deviation == 0.0:
+                deviation = 0.0
+            else:
+                deviation = (deviation + 180) % 360 - 180
+
+            # 将偏差限制在-90°到+90°范围内
+            if deviation > 90:
+                deviation -= 180
+            elif deviation < -90:
+                deviation += 180
+
+            # 保留两位小数
+            deviations.append(round(deviation, 2))
+
+        return deviations
+
+    def recalculationOfIncludedAngle(self, dataFrame: pd.DataFrame, fieldAngleIncluded, fieldWindDirect, fieldNacellePos):
+        """
+        依据机舱位置(角度)、风向计算两者夹角
+        """
+        if not self.common.isNone(fieldAngleIncluded) and fieldAngleIncluded in dataFrame.columns:
+            dataFrame[Field_AngleIncluded] = dataFrame[fieldAngleIncluded]
+
+        if self.common.isNone(fieldAngleIncluded) and fieldAngleIncluded not in dataFrame.columns and fieldWindDirect in dataFrame.columns and fieldNacellePos in dataFrame.columns:
+            dataFrame[Field_AngleIncluded] = self.calculateAngleIncluded(
+                dataFrame[fieldNacellePos], dataFrame[fieldWindDirect])
+
+    def recalculationOfGeneratorSpeed(self, dataFrame: pd.DataFrame, fieldRotorSpeed, fieldGeneratorSpeed, rotationalSpeedRatio):
+        """
+        风电机组发电机转速再计算,公式:转速比=发电机转速/叶轮或主轴转速
+        """
+        if fieldGeneratorSpeed in dataFrame.columns:
+            dataFrame[Field_GeneratorSpeed] = dataFrame[fieldGeneratorSpeed]
+
+        if fieldGeneratorSpeed not in dataFrame.columns and fieldRotorSpeed in dataFrame.columns:
+            dataFrame[fieldGeneratorSpeed] = rotationalSpeedRatio * \
+                dataFrame[fieldRotorSpeed]
+
+    def recalculationOfRotorSpeed(self, dataFrame: pd.DataFrame, fieldRotorSpeed, fieldGeneratorSpeed, rotationalSpeedRatio):
+        """
+        风电机组发电机转速再计算,公式:转速比=发电机转速/叶轮或主轴转速
+        """
+        if fieldRotorSpeed not in dataFrame.columns and fieldGeneratorSpeed in dataFrame.columns:
+            dataFrame[fieldRotorSpeed] = dataFrame[fieldGeneratorSpeed] / \
+                rotationalSpeedRatio
+
+        if not self.common.isNone(fieldRotorSpeed) and fieldRotorSpeed in dataFrame.columns:
+            dataFrame[Field_RotorSpeed] = dataFrame[fieldRotorSpeed]
+
+    def recalculationOfRotorTorque(self, dataFrame: pd.DataFrame, fieldGeneratorTorque, fieldActivePower, fieldGeneratorSpeed):
+        """
+        风电机组发电机转矩计算,P的单位换成KW转矩计算公式:
+        P*1000= pi/30*T*n  
+        30000/pi*P=T*n
+        30000/3.1415926*P=T*n
+        9549.297*p=T*n  
+        其中:n为发电机转速,p为有功功率,T为转矩
+        """
+        if self.common.isNone(fieldGeneratorTorque) and fieldActivePower in dataFrame.columns and fieldGeneratorSpeed in dataFrame.columns:
+            dataFrame[Field_GeneratorTorque] = 9549.297 * \
+                dataFrame[fieldActivePower]/dataFrame[fieldGeneratorSpeed]
+
+        if fieldGeneratorTorque in dataFrame.columns:
+            dataFrame[Field_GeneratorTorque] = dataFrame[fieldGeneratorTorque]
+
+    def recalculation(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        """
+        再计算数据测点
+        参数:
+        dataFrame 原始数据
+        confData  配置数据
+        """
+        self.recalculationOfGeneratorSpeed(
+            dataFrame, confData.field_rotor_speed, confData.field_gen_speed, confData.rotational_Speed_Ratio)
+        self.recalculationOfRotorSpeed(
+            dataFrame, confData.field_rotor_speed, confData.field_gen_speed, confData.rotational_Speed_Ratio)
+        self.recalculationOfRotorTorque(
+            dataFrame, confData.field_torque, confData.field_power, confData.field_gen_speed)
+        self.recalculationOfIncludedAngle(
+            dataFrame, confData.field_angle_included, confData.field_wind_dir, confData.field_nacelle_pos)
+
+        self.common.calculateTSR(dataFrame, confData)
+        self.common.calculateCp(dataFrame, confData)
+
+    def filterWithDateTime(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        dataFrame = dataFrame[(dataFrame[confData.field_turbine_time] >= confData.start_time) & (
+            dataFrame[confData.field_turbine_time] < confData.end_time)]
+
+        return dataFrame
+
+    def processDateTime(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        dataFrame["年月"] = pd.to_datetime(
+            dataFrame[confData.field_turbine_time], format="%Y-%m")
+        dataFrame['日期'] = pd.to_datetime(
+            dataFrame[confData.field_turbine_time], format="%Y-%m")
+        dataFrame['monthIntTime'] = dataFrame['日期'].apply(
+            lambda x: x.timestamp())
+
+        dataFrame[Field_YearMonth] = dataFrame[confData.field_turbine_time].dt.strftime(
+            '%Y-%m')
+
+        dataFrame[Field_YearMonthDay] = dataFrame[confData.field_turbine_time].dt.strftime(
+            '%Y-%m-%d')
+
+        if not self.common.isNone(confData.excludingMonths) and len(confData.excludingMonths) > 0:
+            # 给定的日期列表
+            date_strings = []
+            for month in confData.excludingMonths:
+                if not self.common.isNone(month):
+                    date_strings.append(month)
+
+            if len(date_strings) > 0:
+                mask = ~dataFrame[Field_YearMonth].isin(date_strings)
+
+                # 使用掩码过滤DataFrame,删除指定日期的行
+                dataFrame = dataFrame[mask]
+
+        return dataFrame
+
+    def setColumnDataType(self, dataFrame: pd.DataFrame,  confData: ConfBusiness):
+        # 选择所有的数值型列
+        dataFrame = dataFrame.convert_dtypes()
+        numeric_cols = dataFrame.select_dtypes(
+            include=['float64', 'float16']).columns
+        
+        # 将这些列转换为float32
+        dataFrame[Field_NameOfTurbine] = dataFrame[Field_NameOfTurbine].astype(str)
+
+        # 将这些列转换为float32
+        dataFrame[numeric_cols] = dataFrame[numeric_cols].astype(
+            self.common.getFloat32())
+
+        if not self.common.isNone(confData.field_turbine_time) and confData.field_turbine_time in dataFrame.columns:
+            # 首先尝试去除字符串前后的空白
+            dataFrame[confData.field_turbine_time] = dataFrame[confData.field_turbine_time].str.strip()
+            dataFrame[confData.field_turbine_time] = pd.to_datetime(
+                dataFrame[confData.field_turbine_time], format='%Y-%m-%d %H:%M:%S', errors="coerce")
+            dataFrame[confData.field_turbine_time] = dataFrame[confData.field_turbine_time].dt.strftime(
+                '%Y-%m-%d %H:%M:%S')
+            dataFrame[confData.field_turbine_time] = pd.to_datetime(
+                dataFrame[confData.field_turbine_time])
+
+            # 删除时间字段为空的行记录
+            dataFrame.dropna(
+                axis=0, subset=[confData.field_turbine_time], inplace=True)
+
+        if confData.field_wind_speed in dataFrame.columns:
+            dataFrame[confData.field_wind_speed] = dataFrame[confData.field_wind_speed].astype(
+                self.common.getFloat32())
+
+        if confData.field_wind_dir in dataFrame.columns:
+            dataFrame[confData.field_wind_dir] = dataFrame[confData.field_wind_dir].astype(
+                self.common.getFloat32())
+
+        if confData.field_angle_included in dataFrame.columns:
+            dataFrame[confData.field_angle_included] = dataFrame[confData.field_angle_included].astype(
+                self.common.getFloat32())
+
+        if confData.field_power in dataFrame.columns:
+            dataFrame[confData.field_power] = dataFrame[confData.field_power].astype(
+                self.common.getFloat32())
+
+        if confData.field_wind_dir in dataFrame.columns:
+            dataFrame[confData.field_wind_dir] = dataFrame[confData.field_wind_dir].astype(
+                self.common.getFloat32())
+
+        if confData.field_pitch_angle1 in dataFrame.columns:
+            dataFrame[confData.field_pitch_angle1] = dataFrame[confData.field_pitch_angle1].astype(
+                self.common.getFloat32())
+
+        if confData.field_pitch_angle2 in dataFrame.columns:
+            dataFrame[confData.field_pitch_angle2] = dataFrame[confData.field_pitch_angle2].astype(
+                self.common.getFloat32())
+
+        if confData.field_pitch_angle3 in dataFrame.columns:
+            dataFrame[confData.field_pitch_angle3] = dataFrame[confData.field_pitch_angle3].astype(
+                self.common.getFloat32())
+
+        if confData.field_gen_speed in dataFrame.columns:
+            dataFrame[confData.field_gen_speed] = dataFrame[confData.field_gen_speed].astype(
+                self.common.getFloat32())
+
+        if confData.field_rotor_speed in dataFrame.columns:
+            dataFrame[confData.field_rotor_speed] = dataFrame[confData.field_rotor_speed].astype(
+                self.common.getFloat32())
+            
+        if confData.field_Cabin_Vibrate_X in dataFrame.columns:
+            dataFrame[confData.field_Cabin_Vibrate_X] = dataFrame[confData.field_Cabin_Vibrate_X].astype(
+                self.common.getFloat32())
+        
+        if confData.field_Cabin_Vibrate_Y in dataFrame.columns:
+            dataFrame[confData.field_Cabin_Vibrate_Y] = dataFrame[confData.field_Cabin_Vibrate_Y].astype(
+                self.common.getFloat32())
+            
+        if confData.field_activePowerSet in dataFrame.columns:
+            dataFrame[confData.field_activePowerSet] = dataFrame[confData.field_activePowerSet].astype(
+                self.common.getFloat32())
+            
+        if confData.field_activePowerAvailable in dataFrame.columns:
+            dataFrame[confData.field_activePowerAvailable] = dataFrame[confData.field_activePowerAvailable].astype(
+                self.common.getFloat32())
+
+        return dataFrame
+
+    def loadData(self, csvFilePath, confData: ConfBusiness, turbineName):
+        useColumns = self.common.getUseColumns(confData)
+        # Load the CSV, skipping the specified initial rows
+        dataFrame = pd.read_csv(csvFilePath, header=0, usecols=useColumns, skiprows=range(
+            1, confData.skip_row_number+1))
+
+        if not self.common.isNone(confData.field_turbine_name) and confData.field_turbine_name in dataFrame.columns:
+            dataFrame[Field_NameOfTurbine] = dataFrame[confData.field_turbine_name]
+        else:
+            dataFrame[Field_NameOfTurbine] = turbineName
+        # 对除了“时间”列之外的所有列进行自下而上的填充(先反转后填充)
+        # 注意:补植须要考虑业务合理性
+        # dataFrame = dataFrame.fillna(method='ffill')
+        # dataFrame = dataFrame.fillna(method='bfill')
+
+        return dataFrame
+
+    def execute(self, confData: ConfBusiness):
+        outputDataAfterFilteringDir = r"{}/{}".format(
+            confData.output_path,  "DataAfterFiltering")
+        dir.create_directory(outputDataAfterFilteringDir)
+
+        labler = DataMarker()  #类的实例化
+        
+        dataFrameMerge = pd.DataFrame()
+        for rootDir, subDirs, files in dir.list_directory(confData.input_path):
+            files = sorted(files)
+            for file in files:
+                if not file.endswith(CSVSuffix):
+                    continue
+
+                csvFilePath = os.path.join(rootDir, file)
+                print(f"current csv file path: {csvFilePath}")
+                turbineName = confData.add_W_if_starts_with_digit(file.split(confData.csvFileNameSplitStringForTurbine)[
+                    confData.index_turbine])
+
+                dataFrame = self.loadData(
+                    csvFilePath, confData, turbineName)
+                turbineName = dataFrame[Field_NameOfTurbine].loc[0]
+
+                if len(dataFrame) <= 0:
+                    print("dataFrameFilter not data.")
+                    continue
+
+                dataFrame = self.setColumnDataType(
+                    dataFrame, confData)
+
+                dataFrame = self.processDateTime(dataFrame, confData)
+
+                dataFrame = self.filterWithDateTime(dataFrame, confData)
+
+                # self.baseAnalystTurbineNotify(dataFrame,
+                #                               confData,
+                #                               turbineName)
+
+                self.recalculation(dataFrame, confData)
+
+                # dataFrame = labler.main(confData,dataFrame)
+
+                dataFrameMerge = pd.concat(
+                    [dataFrameMerge, dataFrame], axis=0, sort=False)
+
+                # dataFrame.to_csv(os.path.join(
+                #     outputDataAfterFilteringDir, "{}{}".format(turbineName,CSVSuffix)), index=False)
+                dataFrame = self.turbineNotify(dataFrame,
+                                               confData,
+                                               turbineName)
+
+        # self.baseAnalystNotify(dataFrameMergeFilter,  confData)
+        self.turbinesNotify(dataFrameMerge,  confData)

+ 43 - 0
dataAnalysisBusiness/algorithm/formula_cp.py

@@ -0,0 +1,43 @@
+def calculate_area(diameter):
+    """
+    根据给定的风力涡轮机叶轮直径计算扫过面积的函数。
+
+    参数:
+    - diameter: 风力涡轮机叶轮的直径,单位:米(m)
+
+    返回:
+    - A: 风力涡轮机叶片扫过的面积,单位:平方米(m^2)
+    """
+    radius = diameter / 2
+    A = 3.141592653589793 * radius ** 2
+    return A
+
+def calculate_cp(P, A, rho, v):
+    """
+    计算风能利用系数Cp的函数。
+
+    参数:
+    - P: 风力涡轮机的输出功率,单位:瓦特(W)
+    - A: 风力涡轮机叶片扫过的面积,单位:平方米(m^2)
+    - rho: 空气密度,单位:千克每立方米(kg/m^3)
+    - v: 风速,单位:米每秒(m/s)
+
+    返回:
+    - Cp: 风能利用系数
+    """
+    Cp = P / (0.5 * rho * A * v ** 3)
+    return Cp
+
+# 示例变量
+diameter_example = 82 # 假设叶轮直径为46.2米
+P_example = 76.32*1000
+rho_example = 1.059
+v_example = 3.01
+
+# 使用函数计算A
+A_example = calculate_area(diameter_example)
+
+# 使用计算得到的A值调用calculate_cp函数
+cp_value = calculate_cp(P_example, A_example, rho_example, v_example)
+
+print("Cp={}".format(cp_value))

+ 327 - 0
dataAnalysisBusiness/algorithm/generatorSpeedPowerAnalyst.py

@@ -0,0 +1,327 @@
+import os
+import pandas as pd
+from datetime import datetime
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import plotly.express as px
+import plotly.io as pio
+import seaborn as sns
+import matplotlib.pyplot as plt
+import matplotlib.cm as cm
+from matplotlib.ticker import MultipleLocator
+from matplotlib.colors import Normalize
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+from datetime import timedelta
+
+
+class GeneratorSpeedPowerAnalyst(Analyst):
+    """
+    风电机组发电机转速-有功功率分析
+    """
+
+    def typeAnalyst(self):
+        return "speed_power"
+
+    def turbinesAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # self.create_and_save_plots(
+        #     dataFrameMerge, outputAnalysisDir, confData)
+        self.drawScatter2DMonthly(
+            dataFrameMerge, outputAnalysisDir, confData)
+        self.drawScatterGraph(dataFrameMerge, outputAnalysisDir, confData)
+        self.drawScatterGraphForTurbines(
+            dataFrameMerge, outputAnalysisDir, confData)
+    """
+    def create_and_save_plots(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        x_name = 'generator_speed'
+        y_name = 'power'
+
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+        for name, group in grouped:
+            # 创建图形和坐标轴
+            fig, ax = plt.subplots()
+            cmap = cm.get_cmap('rainbow')
+
+            # 绘制散点图
+            scatter = ax.scatter(x=group[confData.field_gen_speed]*confData.value_gen_speed_multiple if not self.common.isNone(confData.value_gen_speed_multiple) else group[confData.field_gen_speed],
+                                 y=group[confData.field_power], c=group['monthIntTime'], cmap=cmap, s=5)
+
+            # 设置图形标题和坐标轴标签
+            ax.set_title(f'turbine_name={name}')
+            # 设置x轴的刻度步长
+            # 假设您想要每100个单位一个刻度
+            # 创建每100个单位一个刻度的定位器
+            loc = MultipleLocator(confData.graphSets["generatorSpeed"]["step"] if not self.common.isNone(
+                confData.graphSets["generatorSpeed"]) and not self.common.isNone(
+                confData.graphSets["generatorSpeed"]["step"]) else 200)
+            ax.xaxis.set_major_locator(loc)  # 将定位器应用到x轴上
+            ax.set_xlim(confData.graphSets["generatorSpeed"]["min"] if not self.common.isNone(
+                        confData.graphSets["generatorSpeed"]["min"]) else 1000, confData.graphSets["generatorSpeed"]["max"] if not self.common.isNone(confData.graphSets["generatorSpeed"]["max"]) else 2000)
+
+            # 创建每100个单位一个刻度的定位器
+            yloc = MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                confData.graphSets["activePower"]) and not self.common.isNone(
+                confData.graphSets["activePower"]["step"]) else 250)
+            ax.yaxis.set_major_locator(yloc)  # 将定位器应用到y轴上
+            ax.set_ylim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                        confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+
+            ax.set_xlabel(x_name)
+            ax.set_ylabel(y_name)
+
+            # 设置颜色条
+            unique_months = len(group['年月'].unique())
+            ticks = np.linspace(group['monthIntTime'].min(
+            ), group['monthIntTime'].max(), min(unique_months, 6))  # 减少刻度数量
+            ticklabels = [datetime.fromtimestamp(
+                tick).strftime('%Y-%m') for tick in ticks]
+            norm = Normalize(group['monthIntTime'].min(),
+                             group['monthIntTime'].max())
+            sm = cm.ScalarMappable(norm=norm, cmap=cmap)
+
+            # 添加颜色条
+            cbar = fig.colorbar(sm, ax=ax)
+            cbar.set_ticks(ticks)
+            cbar.set_ticklabels(ticklabels)
+            # 旋转x轴刻度标签
+            plt.xticks(rotation=45)
+
+            plt.tight_layout()
+            plt.title(f'{Field_NameOfTurbine}={name}')
+            # 保存图片到指定路径
+            output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            plt.savefig(output_file, bbox_inches='tight', dpi=120)
+            plt.close()
+
+    """
+
+    def drawScatter2DMonthlyOfTurbine(self, dataFrame: pd.DataFrame, outputAnalysisDir: str, confData: ConfBusiness, turbineName: str):
+        # 设置颜色条参数
+        dataFrame = dataFrame.sort_values(by=Field_YearMonth)
+
+        # 绘制 Plotly 散点图
+        fig = px.scatter(
+            dataFrame,
+            x=dataFrame[confData.field_gen_speed],
+            y=dataFrame[confData.field_power],
+            color=Field_YearMonth,
+            color_continuous_scale='Rainbow',  # 颜色条样式
+            labels={confData.field_gen_speed: 'Generator Speed',
+                    Field_YearMonth: 'Time', confData.field_power: 'Power'},
+        )
+
+        # 设置固定散点大小
+        fig.update_traces(marker=dict(size=3))
+
+        # 如果需要颜色轴的刻度和标签
+        # 以下是以比例方式进行色彩的可视化处理
+        fig.update_layout(
+            title={
+                "text": f'Monthly generator speed power scatter plot {turbineName}',
+                "x": 0.5
+            },
+            xaxis=dict(
+                title='Generator Speed',
+                dtick=confData.graphSets["generatorSpeed"]["step"] if not self.common.isNone(
+                    confData.graphSets["generatorSpeed"]) and not self.common.isNone(
+                    confData.graphSets["generatorSpeed"]["step"]) else 200,
+                range=[
+                    confData.graphSets["generatorSpeed"]["min"] if not self.common.isNone(
+                        confData.graphSets["generatorSpeed"]["min"]) else 1000, confData.graphSets["generatorSpeed"]["max"] if not self.common.isNone(confData.graphSets["generatorSpeed"]["max"]) else 2000
+                ],
+                tickangle=45
+            ),
+            yaxis=dict(
+                title='Power',
+                dtick=confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                    confData.graphSets["activePower"]) and not self.common.isNone(
+                    confData.graphSets["activePower"]["step"]) else 250,
+                range=[confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                    confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2],
+            ),
+            coloraxis=dict(
+                colorbar=dict(
+                    title="Time",
+                    ticks="outside",
+                    len=1,  # 设置颜色条的长度,使其占据整个图的高度
+                    thickness=20,  # 调整颜色条的宽度
+                    orientation='v',  # 设置颜色条为垂直方向
+                    tickmode='array',  # 确保刻度按顺序排列
+                    tickvals=dataFrame[Field_YearMonth].unique(
+                    ).tolist(),  # 确保刻度为唯一的年月
+                    ticktext=dataFrame[Field_YearMonth].unique(
+                    ).tolist()  # 以%Y-%m格式显示标签
+                )
+            )
+        )
+
+        # 保存图片
+        outputFilePathPNG = os.path.join(
+            outputAnalysisDir, f"{turbineName}.png")
+        pio.write_image(fig, outputFilePathPNG, format='png',
+                        width=800, height=600, scale=1)
+        
+        return fig
+
+    def drawScatterGraphOfTurbine(self, dataFrame: pd.DataFrame,  outputAnalysisDir: str, confData: ConfBusiness, turbineName: str):
+        # 创建3D散点图
+        fig = px.scatter_3d(dataFrame,
+                            x=confData.field_gen_speed,
+                            y=Field_YearMonth,
+                            z=confData.field_power,
+                            color=Field_YearMonth,
+                            labels={confData.field_gen_speed: 'Generator Speed',
+                                    Field_YearMonth: 'Time', confData.field_power: 'Power'},
+                            )
+
+        # 设置固定散点大小
+        fig.update_traces(marker=dict(size=1.5))
+
+        # 更新图形的布局
+        fig.update_layout(
+            title={
+                "text": f'Monthly generator speed power scatter plot {turbineName}',
+                "x": 0.5
+            },
+            scene=dict(
+                xaxis=dict(
+                    title='Generator Speed',
+                    dtick=confData.graphSets["generatorSpeed"]["step"] if not self.common.isNone(
+                        confData.graphSets["generatorSpeed"]["step"]) else 200,  # 设置y轴刻度间隔为0.1
+                    range=[confData.graphSets["generatorSpeed"]["min"] if not self.common.isNone(
+                        confData.graphSets["generatorSpeed"]["min"]) else 1000, confData.graphSets["generatorSpeed"]["max"] if not self.common.isNone(confData.graphSets["generatorSpeed"]["max"]) else 2000],  # 设置y轴的范围从0到1
+                    showgrid=True,  # 显示网格线
+                ),
+                yaxis=dict(
+                    title='Time',
+                    tickformat='%Y-%m',  # 日期格式,
+                    showgrid=True,  # 显示网格线
+                ),
+                zaxis=dict(
+                    title='Power',
+                    dtick=confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                        confData.graphSets["activePower"]) and not self.common.isNone(
+                        confData.graphSets["activePower"]["step"]) else 250,
+                    range=[confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                        confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2],
+                    showgrid=True,  # 显示网格线
+                )
+            ),
+            scene_camera=dict(
+                up=dict(x=0, y=0, z=1),  # 保持相机向上方向不变
+                center=dict(x=0, y=0, z=0),  # 保持相机中心位置不变
+                eye=dict(x=-1.8, y=-1.8, z=1.2)  # 调整eye属性以实现水平旋转180°
+            ),
+            # 设置图例标题
+            legend_title_text='Time'
+        )
+
+        # 保存图像
+        outputFileHtml = os.path.join(
+            outputAnalysisDir, "{}.html".format(turbineName))
+
+        fig.write_html(outputFileHtml)
+
+    def drawScatter2DMonthly(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+        for name, group in grouped:
+            self.drawScatter2DMonthlyOfTurbine(
+                group, outputAnalysisDir, confData, name)
+
+    def drawScatterGraph(self, dataFrame: pd.DataFrame,  outputAnalysisDir: str, confData: ConfBusiness):
+        """  
+        绘制风速-功率分布图并保存为文件。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+        dataFrame = dataFrame[(dataFrame[confData.field_power] > 0)].sort_values(
+            by=Field_YearMonth)
+
+        grouped = dataFrame.groupby(Field_NameOfTurbine)
+
+        # 遍历每个设备的数据
+        for name, group in grouped:
+            if len(group[Field_YearMonth].unique()) > 1:
+                self.drawScatterGraphOfTurbine(
+                    group, outputAnalysisDir, confData, name)
+            else:
+                fig=self.drawScatter2DMonthlyOfTurbine(
+                    group, outputAnalysisDir, confData, name)
+                # 保存html
+                outputFileHtml = os.path.join(
+                    outputAnalysisDir, "{}.html".format(name))
+                fig.write_html(outputFileHtml)
+
+    def drawScatterGraphForTurbines(self, dataFrame: pd.DataFrame,  outputAnalysisDir, confData: ConfBusiness):
+        """  
+        绘制风速-功率分布图并保存为文件。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+        dataFrame = dataFrame[(dataFrame[confData.field_power] > 0)].sort_values(
+            by=Field_NameOfTurbine)
+
+        # 创建3D散点图
+        fig = px.scatter_3d(dataFrame,
+                            x=confData.field_gen_speed,
+                            y=Field_NameOfTurbine,
+                            z=confData.field_power,
+                            color=Field_NameOfTurbine,
+                            labels={confData.field_gen_speed: 'Generator Speed',
+                                    Field_NameOfTurbine: 'Turbine', confData.field_power: 'Power'},
+                            )
+
+        # 设置固定散点大小
+        fig.update_traces(marker=dict(size=1.5))
+
+        # 更新图形的布局
+        fig.update_layout(
+            title={
+                "text": 'Turbine generator speed power 3D scatter plot',
+                "x": 0.5
+            },
+            scene=dict(
+                xaxis=dict(
+                    title='Generator Speed',
+                    dtick=confData.graphSets["generatorSpeed"]["step"] if not self.common.isNone(
+                        confData.graphSets["generatorSpeed"]["step"]) else 200,  # 设置y轴刻度间隔为0.1
+                    range=[confData.graphSets["generatorSpeed"]["min"] if not self.common.isNone(
+                        confData.graphSets["generatorSpeed"]["min"]) else 1000, confData.graphSets["generatorSpeed"]["max"] if not self.common.isNone(confData.graphSets["generatorSpeed"]["max"]) else 2000],  # 设置y轴的范围从0到1
+                    showgrid=True,  # 显示网格线
+                ),
+                yaxis=dict(
+                    title='Turbine',
+                    showgrid=True,  # 显示网格线
+                ),
+                zaxis=dict(
+                    title='Power',
+                    dtick=confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                        confData.graphSets["activePower"]) and not self.common.isNone(
+                        confData.graphSets["activePower"]["step"]) else 250,
+                    range=[confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                        confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2],
+                    showgrid=True,  # 显示网格线
+                )
+            ),
+            scene_camera=dict(
+                up=dict(x=0, y=0, z=1),  # 保持相机向上方向不变
+                center=dict(x=0, y=0, z=0),  # 保持相机中心位置不变
+                eye=dict(x=-1.8, y=-1.8, z=1.2)  # 调整eye属性以实现水平旋转180°
+            ),
+            # 设置图例标题
+            legend_title_text='Turbine'
+        )
+
+        # 保存图像
+        outputFileHtml = os.path.join(
+            outputAnalysisDir, "{}.html".format(self.typeAnalyst()))
+
+        fig.write_html(outputFileHtml)

+ 236 - 0
dataAnalysisBusiness/algorithm/generatorSpeedTorqueAnalyst.py

@@ -0,0 +1,236 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import plotly.express as px
+import seaborn as sns
+import matplotlib.pyplot as plt
+from matplotlib.ticker import MultipleLocator
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class GeneratorSpeedTorqueAnalyst(Analyst):
+    """
+    风电机组发电机转速-转矩分析
+    """
+
+    def typeAnalyst(self):
+        return "speed_torque"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.create_and_save_plots(
+            dataFrameMerge, outputAnalysisDir, confData)
+        self.drawScatterGraph(dataFrameMerge, outputAnalysisDir, confData)
+        self.drawScatterGraphForTurbines(
+            dataFrameMerge, outputAnalysisDir, confData)
+
+    def create_and_save_plots(self, dataFrame: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 检查所需列是否存在
+        required_columns = {confData.field_gen_speed, Field_GeneratorTorque}
+        if not required_columns.issubset(dataFrame.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+
+        x_name = 'generator_speed'
+        y_name = 'generator_torque'
+        maxTurque = dataFrame[Field_GeneratorTorque].max()
+        grouped = dataFrame.groupby(Field_NameOfTurbine)
+        for name, group in grouped:
+            groupNew = group.copy()
+
+            if not self.common.isNone(confData.value_gen_speed_multiple):
+                groupNew[confData.field_gen_speed] = group[confData.field_gen_speed] * \
+                    confData.value_gen_speed_multiple
+            # sns.lmplot函数参数scatter_kws: 设置为{"s": 5}时,会出现颜色丢失问题;改为={"s": 5, "color": "b"}后,则造成图形风格不统一问题;
+            g = sns.lmplot(x=confData.field_gen_speed, y=Field_GeneratorTorque, data=groupNew, fit_reg=False, scatter_kws={
+                           "s": 5, "color": "b"}, legend=False, height=6, aspect=1.2)
+            # g = sns.lmplot(x=fieldGeneratorSpeed, y=Field_GeneratorTorque, data=group, fit_reg=False, scatter_kws={
+            #                "s": 5}, legend=False, height=6, aspect=1.2)
+
+            for ax in g.axes.flat:
+                # 创建每100个单位一个刻度的定位器
+                loc = MultipleLocator(confData.graphSets["generatorSpeed"]["step"] if not self.common.isNone(
+                    confData.graphSets["generatorSpeed"]) and not self.common.isNone(
+                    confData.graphSets["generatorSpeed"]["step"]) else 200)
+                ax.xaxis.set_major_locator(loc)  # 将定位器应用到x轴上
+                ax.set_xlim(confData.graphSets["generatorSpeed"]["min"] if not self.common.isNone(
+                    confData.graphSets["generatorSpeed"]["min"]) else 1000, confData.graphSets["generatorSpeed"]["max"] if not self.common.isNone(confData.graphSets["generatorSpeed"]["max"]) else 2000)
+
+                yloc = MultipleLocator(confData.graphSets["generatorTorque"]["step"] if not self.common.isNone(
+                    confData.graphSets["generatorTorque"]["step"]) else 200)
+                ax.yaxis.set_major_locator(yloc)  # 将定位器应用到y轴上
+                ax.set_ylim(confData.graphSets["generatorTorque"]["min"] if not self.common.isNone(
+                            confData.graphSets["generatorTorque"]["min"]) else 0, confData.graphSets["generatorTorque"]["max"] if not self.common.isNone(confData.graphSets["generatorTorque"]["max"]) else 2000)
+
+                ax.set_xlabel(x_name)
+                ax.set_ylabel(y_name)
+
+            plt.tight_layout()
+            plt.title(f'{Field_NameOfTurbine}={name}')
+            # 保存图片到指定路径
+            output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            plt.savefig(output_file, bbox_inches='tight', dpi=120)
+            plt.close()
+
+    def drawScatterGraph(self, dataFrame: pd.DataFrame,  outputAnalysisDir, confData: ConfBusiness):
+        """  
+        绘制风速-功率分布图并保存为文件。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+        dataFrame = dataFrame[(dataFrame[Field_GeneratorTorque] > 0)]
+        # 按设备名分组数据
+        colorsList = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
+                      '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#aec7e8', '#ffbb78']
+
+        grouped = dataFrame.groupby(Field_NameOfTurbine)
+
+        # 遍历每个设备的数据
+        for name, group in grouped:
+            # 创建颜色映射,将每个年月映射到一个唯一的颜色
+            unique_months = group[Field_YearMonth].unique()
+            colors = [
+                colorsList[i % len(colorsList)] for i in range(len(unique_months))]
+            color_map = dict(zip(unique_months, colors))
+
+            # 使用go.Scatter3d创建3D散点图
+            trace = go.Scatter3d(
+                x=group[confData.field_gen_speed],
+                y=group[Field_YearMonth],
+                z=group[Field_GeneratorTorque],
+                mode='markers',
+                marker=dict(
+                    color=[color_map[month]
+                           for month in group[Field_YearMonth]],
+                    size=2,
+                    line=dict(
+                        color='rgba(0, 0, 0, 0)',  # 设置边框颜色为透明,以去掉白色边框
+                        width=0  # 设置边框宽度为0,进一步确保没有边框
+                    ),
+                    opacity=0.8  # 调整散点的透明度,增加透视效果
+                )
+            )
+
+            # 创建图形
+            fig = go.Figure(data=[trace])
+
+            # 更新图形的布局
+            fig.update_layout(
+                title={
+                    "text": f'Monthly generator speed torque 3D scatter plot {name}',
+                    "x": 0.5
+                },
+                scene=dict(
+                    xaxis=dict(
+                        title='Generator Speed',
+                        dtick=confData.graphSets["generatorSpeed"]["step"] if not self.common.isNone(
+                            confData.graphSets["generatorSpeed"]["step"]) else 200,  # 设置y轴刻度间隔为0.1
+                        range=[confData.graphSets["generatorSpeed"]["min"] if not self.common.isNone(
+                            confData.graphSets["generatorSpeed"]["min"]) else 1000, confData.graphSets["generatorSpeed"]["max"] if not self.common.isNone(confData.graphSets["generatorSpeed"]["max"]) else 2000],  # 设置y轴的范围从0到1
+                        showgrid=True,  # 显示网格线
+                    ),
+                    yaxis=dict(
+                        title='Time',
+                        tickmode='array',
+                        tickvals=unique_months,
+                        ticktext=unique_months,
+                        # dtick=500,  # 设置y轴刻度间隔
+                        # range=[0,
+                        #        group[Field_GeneratorTorque].max()],  # 设置y轴的范围
+                        showgrid=True,  # 显示网格线
+                        # categoryorder='category ascending'
+                    ),
+                    zaxis=dict(
+                        title='Generator Torque',
+                        dtick=confData.graphSets["generatorTorque"]["step"] if not self.common.isNone(
+                            confData.graphSets["generatorTorque"]["step"]) else 200,  # 设置y轴刻度间隔为0.1
+                        range=[confData.graphSets["generatorTorque"]["min"] if not self.common.isNone(
+                            confData.graphSets["generatorTorque"]["min"]) else 0, confData.graphSets["generatorTorque"]["max"] if not self.common.isNone(confData.graphSets["generatorTorque"]["max"]) else 2000],  # 设置y轴的范围从0到1
+                    )
+                ),
+                scene_camera=dict(
+                    up=dict(x=0, y=0, z=1),  # 保持相机向上方向不变
+                    center=dict(x=0, y=0, z=0),  # 保持相机中心位置不变
+                    eye=dict(x=-1.8, y=-1.8, z=1.2)  # 调整eye属性以实现水平旋转180°
+                ),
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+
+            # 保存图像
+            outputFileHtml = os.path.join(
+                outputAnalysisDir, "{}.html".format(name))
+
+            fig.write_html(outputFileHtml)
+
+    def drawScatterGraphForTurbines(self, dataFrame: pd.DataFrame,  outputAnalysisDir, confData: ConfBusiness):
+        """  
+        绘制风速-功率分布图并保存为文件。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+        dataFrame = dataFrame[(dataFrame[confData.field_power] > 0)].sort_values(
+            by=Field_NameOfTurbine)
+
+        # 创建3D散点图
+        fig = px.scatter_3d(dataFrame,
+                            x=confData.field_gen_speed,
+                            y=Field_NameOfTurbine,
+                            z=Field_GeneratorTorque,
+                            color=Field_NameOfTurbine,
+                            labels={confData.field_gen_speed: 'Generator Speed',
+                                    Field_NameOfTurbine: 'Turbine', Field_GeneratorTorque: 'Generator Torque'},
+                            )
+
+        # 设置固定散点大小
+        fig.update_traces(marker=dict(size=1.5))
+
+        # 更新图形的布局
+        fig.update_layout(
+            title={
+                "text": 'Turbine generator speed Turque 3D scatter plot',
+                "x": 0.5
+            },
+            scene=dict(
+                xaxis=dict(
+                    title='Generator Speed',
+                    dtick=confData.graphSets["generatorSpeed"]["step"] if not self.common.isNone(
+                        confData.graphSets["generatorSpeed"]["step"]) else 200,  # 设置y轴刻度间隔为0.1
+                    range=[confData.graphSets["generatorSpeed"]["min"] if not self.common.isNone(
+                        confData.graphSets["generatorSpeed"]["min"]) else 1000, confData.graphSets["generatorSpeed"]["max"] if not self.common.isNone(confData.graphSets["generatorSpeed"]["max"]) else 2000],  # 设置y轴的范围从0到1
+                    showgrid=True,  # 显示网格线
+                ),
+                yaxis=dict(
+                    title='Turbine',
+                    showgrid=True,  # 显示网格线
+                ),
+                zaxis=dict(
+                    title='Generator Turque',
+                    dtick=confData.graphSets["generatorTorque"]["step"] if not self.common.isNone(
+                        confData.graphSets["generatorTorque"]["step"]) else 200,  # 设置y轴刻度间隔为0.1
+                    range=[confData.graphSets["generatorTorque"]["min"] if not self.common.isNone(
+                        confData.graphSets["generatorTorque"]["min"]) else 0, confData.graphSets["generatorTorque"]["max"] if not self.common.isNone(confData.graphSets["generatorTorque"]["max"]) else 2000],  # 设置y轴的范围从0到1
+                )
+            ),
+            scene_camera=dict(
+                up=dict(x=0, y=0, z=1),  # 保持相机向上方向不变
+                center=dict(x=0, y=0, z=0),  # 保持相机中心位置不变
+                eye=dict(x=-1.8, y=-1.8, z=1.2)  # 调整eye属性以实现水平旋转180°
+            ),
+            # 设置图例标题
+            legend_title_text='Turbine',
+            margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+        )
+
+        # 保存图像
+        outputFileHtml = os.path.join(
+            outputAnalysisDir, "{}.html".format(self.typeAnalyst()))
+
+        fig.write_html(outputFileHtml)

+ 120 - 0
dataAnalysisBusiness/algorithm/minPitchAnalyst.py

@@ -0,0 +1,120 @@
+import os
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.cm as cm
+import matplotlib.ticker as ticker
+import plotly.express as px
+import math
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class MinPitchAnalyst(Analyst):
+    """
+    风电机组最小桨距角分析
+    """
+
+    def typeAnalyst(self):
+        return "min_pitch"
+
+    def turbinesAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        self.drawTrendGraph(dataFrameMerge, outputAnalysisDir, confData)
+
+    def drawTrendGraph(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        """
+        Generates pitch angle distribution scatter plots for turbines in a wind farm using plotly.
+
+        Parameters:
+        - dataFrameMerge: pd.DataFrame, DataFrame containing turbine data.
+        - outputAnalysisDir: str, path to save the output plots.
+        - confData: ConfBusiness, configuration object containing field names.
+        """
+        # 检查所需列是否存在
+        required_columns = {Field_YearMonthDay, confData.field_pitch_angle1}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+
+        pitchAngleRate = 'pitch_angle_rate'
+        fieldPitchAngleBin = 'pitch_angle_bin'
+        # Custom color scale: Blue (high pitch_angle_rate) to Light Grey (low pitch_angle_rate)
+        custom_color_scale = [
+            (0.0, "rgb(240, 240, 240)"),  # Light grey for the lowest values
+            # (0.25, "rgb(240, 240, 240)"),  # Light grey for the lowest values
+            (0.5, "rgba(55.0, 135.0, 192.33333333333334, 1.0)"),  # Medium blue-grey
+            # (0.75, "rgba(55.0, 135.0, 192.33333333333334, 1.0)"),  # Medium blue-grey
+            # Dark blue for the highest values
+            (1.0, "rgba(55.0, 135.0, 192.33333333333334, 1.0)")
+        ]
+        # Group data by turbine identifier
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            # Convert the date column to datetime type
+            group[Field_YearMonthDay] = pd.to_datetime(
+                group[Field_YearMonthDay])
+
+            # Creating bins of 0.2 intervals for pitch angles
+            bins = pd.interval_range(start=group[confData.field_pitch_angle1].min(),
+                                     end=group[confData.field_pitch_angle1].max(
+            ) + 0.2,
+                freq=0.2, closed='right')
+            group[fieldPitchAngleBin] = pd.cut(
+                group[confData.field_pitch_angle1], bins=bins)
+            group[fieldPitchAngleBin] = group[fieldPitchAngleBin].apply(
+                lambda x: x.left)  # 提取每个区间的左端点作为值
+            # Calculate the pitch angle rate within each day
+            df = group.groupby([Field_YearMonthDay, confData.field_pitch_angle1]
+                               ).size().reset_index(name='count')
+            # df = group.groupby([Field_YearMonthDay, fieldPitchAngleBin]
+            #                    ).size().reset_index(name='count')
+            total_counts = group.groupby(
+                Field_YearMonthDay).size().reset_index(name='total_count')
+            df = df.merge(total_counts, on=Field_YearMonthDay)
+            df[pitchAngleRate] = df['count'] / df['total_count'] * 100
+            # df[pitchAngleRate] = (df['count'] / df['total_count']).apply(lambda x: x ** 0.5)*100
+
+            # Plotting using plotly
+            fig = px.scatter(df,
+                             x=Field_YearMonthDay,
+                             y=confData.field_pitch_angle1,  # 桨距角不分仓方式
+                             #  y=fieldPitchAngleBin,  # 桨距角分仓方式
+                             size='count',
+                             color=pitchAngleRate,
+                             #  color_continuous_scale='Blues',
+                             color_continuous_scale=custom_color_scale
+                             )
+
+            # Set date format on x-axis
+            fig.update_xaxes(
+                title='Time', tickformat='%Y-%m-%d', tickangle=-45)
+
+            fig.update_yaxes(title='Pitch Angle',
+                             dtick=confData.graphSets["pitchAngle"]["step"] if not self.common.isNone(
+                                 confData.graphSets["pitchAngle"]["step"]) else 2,  # 设置y轴刻度间隔为0.1
+                             range=[confData.graphSets["pitchAngle"]["min"] if not self.common.isNone(
+                                 confData.graphSets["pitchAngle"]["min"]) else -2, confData.graphSets["pitchAngle"]["max"] if not self.common.isNone(confData.graphSets["pitchAngle"]["max"]) else 28],  # 设置y轴的范围从0到1
+                             )
+
+            # Customizing legend
+            fig.update_layout(
+                title={
+                    "text": f'Pitch Angle Distribution for {name}',
+                    "x": 0.5
+                },
+                coloraxis_colorbar=dict(title='Rate'),
+                margin=dict(t=50, b=10),  # t为顶部(top)间距,b为底部(bottom)间距
+                # plot_bgcolor='rgb(240, 240, 240)' 
+            )
+
+            # Set marker size if fixed size is needed
+            # Fixed size for all points
+            fig.update_traces(marker=dict(size=3, opacity=0.5))
+
+            # Save plot
+            filePathOfImage = os.path.join(outputAnalysisDir, f"{name}.png")
+            fig.write_image(filePathOfImage, scale=2)
+
+            filePathOfHtml = os.path.join(outputAnalysisDir, f"{name}.html")
+            fig.write_html(filePathOfHtml)

+ 74 - 0
dataAnalysisBusiness/algorithm/pitchGeneratorSpeedAnalyst.py

@@ -0,0 +1,74 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import seaborn as sns
+import matplotlib.pyplot as plt
+from matplotlib.ticker import MultipleLocator
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class PitchGeneratorSpeedAnalyst(Analyst):
+    """
+    风电机组变桨-发电机转速分析
+    """
+
+    def typeAnalyst(self):
+        return "pitch_generator_speed"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.plot_speed_pitch_angle(
+            dataFrameMerge, outputAnalysisDir, confData, confData.field_pitch_angle1, confData.field_gen_speed)
+
+    def plot_speed_pitch_angle(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness, fieldPitchAngle, fieldGeneratorSpeed):
+        x_name = 'generator_speed'
+        y_name = 'pitch_angle'
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+        sns.set_palette('deep')
+        # 遍历每个设备并绘制散点图
+        for name, group in grouped:
+            groupNew = group.copy()
+
+            if not self.common.isNone(confData.value_gen_speed_multiple):
+                groupNew[fieldGeneratorSpeed] = group[fieldGeneratorSpeed] * \
+                    confData.value_gen_speed_multiple
+
+            # sns.lmplot函数参数scatter_kws: 设置为{"s": 5}时,会出现颜色丢失问题;
+            g = sns.lmplot(x=fieldGeneratorSpeed, y=fieldPitchAngle, data=groupNew,
+                           fit_reg=False, scatter_kws={"s": 5, "color": "b"}, legend=False, height=6, aspect=1.2)
+            # g = sns.lmplot(x=fieldGeneratorSpeed, y=fieldPitchAngle, data=group,
+            #                fit_reg=False, scatter_kws={"s": 5}, legend=False, height=6, aspect=1.2)
+
+            # 设置x轴和y轴的刻度
+            for ax in g.axes.flat:
+                ax.set_xlim(confData.graphSets["generatorSpeed"]["min"] if not self.common.isNone(
+                    confData.graphSets["generatorSpeed"]["min"]) else 1000, confData.graphSets["generatorSpeed"]["max"] if not self.common.isNone(confData.graphSets["generatorSpeed"]["max"]) else 2000)
+                # 设置x轴的刻度步长
+                # 假设您想要每100个单位一个刻度
+                loc = MultipleLocator(confData.graphSets["generatorSpeed"]["step"] if not self.common.isNone(
+                    confData.graphSets["generatorSpeed"]) and not self.common.isNone(
+                    confData.graphSets["generatorSpeed"]["step"]) else 200)
+                ax.xaxis.set_major_locator(loc)  # 将定位器应用到x轴上
+
+                ax.yaxis.set_major_locator(MultipleLocator(confData.graphSets["pitchAngle"]["step"] if not self.common.isNone(
+                    confData.graphSets["pitchAngle"]["step"]) else 2))
+                ax.set_ylim(confData.graphSets["pitchAngle"]["min"] if not self.common.isNone(
+                    confData.graphSets["pitchAngle"]["min"]) else -2, confData.graphSets["pitchAngle"]["max"] if not self.common.isNone(confData.graphSets["pitchAngle"]["max"]) else 28)
+
+                ax.set_xlabel(x_name)
+                ax.set_ylabel(y_name)
+
+            # 设置x轴刻度值旋转角度为45度
+            plt.tick_params(axis='x', rotation=45)
+            # 调整布局和设置标题
+            plt.tight_layout()
+            plt.title(f'{Field_NameOfTurbine}={name}')
+
+            # 保存图像并关闭绘图窗口
+            output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            plt.savefig(output_file, bbox_inches='tight', dpi=120)
+            plt.close()

+ 155 - 0
dataAnalysisBusiness/algorithm/pitchPowerAnalyst.py

@@ -0,0 +1,155 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import seaborn as sns
+import matplotlib.pyplot as plt
+from matplotlib.ticker import MultipleLocator
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class PitchPowerAnalyst(Analyst):
+    """
+    风电机组变桨-功率分析
+    """
+
+    def typeAnalyst(self):
+        return "pitch_power"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.plot_power_pitch_angle(
+            dataFrameMerge, outputAnalysisDir, confData)
+        self.drawScatterGraph(dataFrameMerge, outputAnalysisDir, confData)
+
+    def plot_power_pitch_angle(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        x_name = 'power'
+        y_name = 'pitch_angle'
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+        print("self.ratedPower {}".format(confData.rated_power))
+        # 遍历每个设备并绘制散点图
+        for name, group in grouped:
+            # sns.lmplot函数参数scatter_kws: 设置为{"s": 5}时,会出现颜色丢失问题;
+            g = sns.lmplot(x=confData.field_power, y=confData.field_pitch_angle1, data=group,
+                           fit_reg=False, scatter_kws={"s": 5, "color": "b"}, legend=False, height=6, aspect=1.2)
+            # g = sns.lmplot(x=confData.field_power, y=confData.field_pitch_angle1, data=group,
+            #                fit_reg=False, scatter_kws={"s": 5}, legend=False, height=6, aspect=1.2)
+
+            # 设置x轴和y轴的刻度
+            for ax in g.axes.flat:
+                ax.xaxis.set_major_locator(MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                    confData.graphSets["activePower"]) and not self.common.isNone(
+                    confData.graphSets["activePower"]["step"]) else 250))
+                ax.set_xlim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                    confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+
+                ax.yaxis.set_major_locator(MultipleLocator(confData.graphSets["pitchAngle"]["step"] if not self.common.isNone(
+                    confData.graphSets["pitchAngle"]["step"]) else 2))
+                ax.set_ylim(confData.graphSets["pitchAngle"]["min"] if not self.common.isNone(
+                    confData.graphSets["pitchAngle"]["min"]) else -2, confData.graphSets["pitchAngle"]["max"] if not self.common.isNone(confData.graphSets["pitchAngle"]["max"]) else 28)
+
+                ax.set_xlabel(x_name)
+                ax.set_ylabel(y_name)
+
+            # 设置x轴刻度值旋转角度为45度
+            plt.tick_params(axis='x', rotation=45)
+            # 调整布局和设置标题
+            plt.tight_layout()
+            plt.title(f'{Field_NameOfTurbine}={name}')
+
+            # 保存图像并关闭绘图窗口
+            output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            plt.savefig(output_file, bbox_inches='tight', dpi=120)
+            plt.close()
+
+    def drawScatterGraph(self, dataFrame: pd.DataFrame,  outputAnalysisDir, confData: ConfBusiness):
+        """  
+        绘制变桨-功率分布图并保存为文件。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+        # 按设备名分组数据
+        colorsList = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
+                      '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#aec7e8', '#ffbb78']
+        grouped = dataFrame.groupby(Field_NameOfTurbine)
+
+        # 遍历每个设备的数据
+        for name, group in grouped:
+            # 创建颜色映射,将每个年月映射到一个唯一的颜色
+            unique_months = group[Field_YearMonth].unique()
+            colors = [
+                colorsList[i % 12] for i in range(len(unique_months))]
+            color_map = dict(zip(unique_months, colors))
+
+            # 使用go.Scatter3d创建3D散点图
+            trace = go.Scatter3d(
+                x=group[confData.field_pitch_angle1],
+                y=group[Field_YearMonth],
+                z=group[confData.field_power],
+                mode='markers',
+                marker=dict(
+                    color=[color_map[month]
+                           for month in group[Field_YearMonth]],
+                    size=1.5,
+                    line=dict(
+                        color='rgba(0, 0, 0, 0)',  # 设置边框颜色为透明,以去掉白色边框
+                        width=0  # 设置边框宽度为0,进一步确保没有边框
+                    ),
+                    opacity=0.8  # 调整散点的透明度,增加透视效果
+                )
+            )
+
+            # 创建图形
+            fig = go.Figure(data=[trace])
+
+            # 更新图形的布局
+            fig.update_layout(
+                title={
+                    "text": f'Monthly pitch to power 3D scatter plot {name}',
+                    "x": 0.5
+                },
+                scene=dict(
+                    xaxis=dict(
+                        title='Pitch Angle',
+                        dtick=confData.graphSets["pitchAngle"]["step"] if not self.common.isNone(
+                            confData.graphSets["pitchAngle"]["step"]) else 2,  # 设置y轴刻度间隔为0.1
+                        range=[confData.graphSets["pitchAngle"]["min"] if not self.common.isNone(
+                            confData.graphSets["pitchAngle"]["min"]) else -2, confData.graphSets["pitchAngle"]["max"] if not self.common.isNone(confData.graphSets["pitchAngle"]["max"]) else 28],  # 设置y轴的范围从0到1
+                        showgrid=True,  # 显示网格线
+                    ),
+                    yaxis=dict(
+                        title='Time',
+                        tickmode='array',
+                        tickvals=unique_months,
+                        ticktext=unique_months,
+                        showgrid=True,  # 显示网格线
+                        categoryorder='category ascending'
+                    ),
+                    zaxis=dict(
+                        title='Power',
+                        dtick=confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                            confData.graphSets["activePower"]) and not self.common.isNone(
+                            confData.graphSets["activePower"]["step"]) else 250,
+                        range=[confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                            confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2],
+                    )
+                ),
+                scene_camera=dict(
+                    up=dict(x=0, y=0, z=1),  # 保持相机向上方向不变
+                    center=dict(x=0, y=0, z=0),  # 保持相机中心位置不变
+                    eye=dict(x=-1.8, y=-1.8, z=1.2)  # 调整eye属性以实现水平旋转180°
+                ),
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+
+            # 保存图像
+            outputFileHtml = os.path.join(
+                outputAnalysisDir, "{}.html".format(name))
+
+            fig.write_html(outputFileHtml)

+ 98 - 0
dataAnalysisBusiness/algorithm/pitchPowerWindSpeedAnalyst.py

@@ -0,0 +1,98 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+import plotly.offline as offline
+
+
+class PitchPowerWindSpeedAnalyst(Analyst):
+
+    def typeAnalyst(self):
+        return "pitch_power_windspeed"
+
+    # def filterCommon(self,dataFrame:pd.DataFrame, confData:ConfBusiness):
+    #     dataFrame=super().filterCommon(dataFrame,confData)
+    #     dataFrame=dataFrame[(dataFrame[confData.field_power]>=1350) & (dataFrame[confData.field_power]<=1500)]
+    #     return dataFrame
+
+    def turbinesAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        self.drawGraph(dataFrameMerge, outputAnalysisDir, confData)
+
+    def drawGraph(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        """
+        绘制3D散点图。
+
+        参数:
+        df: pandas.DataFrame。
+
+        返回:
+        一个Plotly图形对象。
+        """
+        # 检查所需列是否存在
+        required_columns = {confData.field_pitch_angle1,
+                            confData.field_power, confData.field_wind_speed}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            layout = go.Layout(
+                title={
+                    "text": f'3D scatter plot: Wind Speed vs. Pitch Angle vs. Power {name}',
+                    "x": 0.5
+                },
+                scene=dict(
+                    xaxis=dict(
+                        title='Wind Speed',
+                        dtick=2,  # 设置轴刻度间隔
+                        range=[0,
+                               26],  # 设置轴的范围
+                    ),
+                    yaxis=dict(
+                        title='Pitch Angle',
+                        dtick=confData.graphSets["pitchAngle"]["step"] if not self.common.isNone(
+                            confData.graphSets["pitchAngle"]["step"]) else 2,  # 设置y轴刻度间隔为0.1
+                        range=[confData.graphSets["pitchAngle"]["min"] if not self.common.isNone(
+                            confData.graphSets["pitchAngle"]["min"]) else -2, confData.graphSets["pitchAngle"]["max"] if not self.common.isNone(confData.graphSets["pitchAngle"]["max"]) else 28],  # 设置y轴的范围从0到1
+                    ),
+                    zaxis=dict(
+                        title='Power',
+                        dtick=confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                            confData.graphSets["activePower"]) and not self.common.isNone(
+                            confData.graphSets["activePower"]["step"]) else 250,
+                        range=[confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                            confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2],
+                    )
+                ),
+                # t为顶部(top)间距,b为底部(bottom)间距
+                margin=dict(t=50, b=10)
+            )
+
+            # 创建 3D 散点图
+            fig = go.Figure(data=[go.Scatter3d(
+                x=group[confData.field_wind_speed],
+                y=group[confData.field_pitch_angle1],
+                z=group[confData.field_power],
+                mode='markers',  # 设置模式为 markers,表示绘制散点图
+                marker=dict(
+                    size=1.5,  # 设置散点的大小
+                    # 你还可以设置其他属性,如颜色、透明度等
+                    # color='blue',
+                    # opacity=0.8
+                )
+            )], layout=layout)  # 假设 layout 已经定义好了
+
+            # 保存html
+            outputFileHtml = os.path.join(outputAnalysisDir, f"{name}.html")
+            fig.write_html(outputFileHtml)
+            # 保存图表为HTML文件
+            # offline.plot(fig, filename=outputFileHtml, auto_open=False)
+            # 保存图像
+            # output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            # fig.write_image(output_file)

+ 91 - 0
dataAnalysisBusiness/algorithm/pitchTSRCpAnalyst.py

@@ -0,0 +1,91 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+import plotly.offline as offline
+
+
+class PitchTSRCpAnalyst(Analyst):
+
+    def typeAnalyst(self):
+        return "pitch_tsr_cp"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.windRoseAnalysis(dataFrameMerge, outputAnalysisDir, confData)
+
+    def windRoseAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        """
+        绘制3D曲面图。
+
+        参数:
+        df: pandas.DataFrame, 必须包含confData.field_pitch_angle1, Field_TSR, 和 Field_Cp这三个字段。
+
+        返回:
+        一个Plotly图形对象。
+        """
+        # 检查所需列是否存在
+        required_columns = {confData.field_pitch_angle1, Field_TSR, Field_Cp}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            layout = go.Layout(
+                title={
+                    "text": f'3D scatter plot: Cp vs. Pitch Angle vs. TSR {name}',
+                    "x": 0.5
+                },
+                scene=dict(
+                    xaxis=dict(
+                        title='Pitch Angle',
+                        dtick=confData.graphSets["pitchAngle"]["step"] if not self.common.isNone(
+                            confData.graphSets["pitchAngle"]["step"]) else 2,  # 设置y轴刻度间隔为0.1
+                        range=[confData.graphSets["pitchAngle"]["min"] if not self.common.isNone(
+                            confData.graphSets["pitchAngle"]["min"]) else -2, confData.graphSets["pitchAngle"]["max"] if not self.common.isNone(confData.graphSets["pitchAngle"]["max"]) else 28],  # 设置y轴的范围从0到1
+                    ),
+                    yaxis=dict(
+                        title='TSR',
+                        dtick=confData.graphSets["tsr"]["step"] if not self.common.isNone(
+                            confData.graphSets["tsr"]["step"]) else 5,  # 设置y轴刻度间隔为0.1
+                        range=[confData.graphSets["tsr"]["min"] if not self.common.isNone(
+                            confData.graphSets["tsr"]["min"]) else 0, confData.graphSets["tsr"]["max"] if not self.common.isNone(confData.graphSets["tsr"]["max"]) else 20],  # 设置y轴的范围从0到1
+                    ),
+                    zaxis=dict(
+                        title='Cp',
+                        dtick=confData.graphSets["cp"]["step"] if not self.common.isNone(
+                            confData.graphSets["cp"]["step"]) else 0.5,  # 设置y轴刻度间隔为0.1
+                        range=[confData.graphSets["cp"]["min"] if not self.common.isNone(
+                            confData.graphSets["cp"]["min"]) else 0, confData.graphSets["cp"]["max"] if not self.common.isNone(confData.graphSets["cp"]["max"]) else 2],  # 设置y轴的范围从0到1
+                    )
+                ),
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+
+            # 创建 3D 散点图
+            fig = go.Figure(data=[go.Scatter3d(
+                x=group[confData.field_pitch_angle1],
+                y=group[Field_TSR],
+                z=group[Field_Cp],
+                mode='markers',  # 设置模式为 markers,表示绘制散点图
+                marker=dict(
+                    size=1,  # 设置散点的大小
+                    # 你还可以设置其他属性,如颜色、透明度等
+                    # color='blue',
+                    # opacity=0.8
+                )
+            )], layout=layout)  # 假设 layout 已经定义好了
+
+            # 保存html
+            outputFileHtml = os.path.join(outputAnalysisDir, f"{name}.html")
+            fig.write_html(outputFileHtml)
+            # 保存图表为HTML文件
+            # offline.plot(fig, filename=outputFileHtml, auto_open=False)
+            # 保存图像
+            # output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            # fig.write_image(output_file)

+ 224 - 0
dataAnalysisBusiness/algorithm/powerCurveAnalyst.py

@@ -0,0 +1,224 @@
+import os
+from datetime import datetime
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import matplotlib.cm as cm
+from matplotlib.ticker import MultipleLocator
+from matplotlib.colors import Normalize
+import seaborn as sns
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from geopy.distance import geodesic
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+class PowerCurveAnalyst(Analyst):
+    """
+    风电机组功率曲线散点分析。
+    秒级scada数据运算太慢,建议使用分钟级scada数据
+    """
+
+    def typeAnalyst(self):
+        return "power_curve"
+    
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        if len(dataFrameMerge)<=0:
+            print("After screening for blade pitch angle less than the configured value, plot power curve scatter points without data")
+            return
+        
+        self.drawOfPowerCurve(dataFrameMerge, outputAnalysisDir, confData,self.dataFrameContractOfTurbine)
+        # self.drawOfPowerCurveScatter(dataFrameMerge,outputAnalysisDir,confData,dataFrameGuaranteePowerCurve)
+        
+    def drawOfPowerCurveScatter(self, dataFrame: pd.DataFrame,  outputAnalysisDir, confData: ConfBusiness,dataFrameGuaranteePowerCurve:pd.DataFrame):
+        """  
+        绘制风速-功率分布图并保存为文件。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        csvPowerCurveFilePath (str): 功率曲线文件路径。   
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+        x_name = 'wind_speed'
+        y_name = 'power'
+
+        # 按设备名分组数据
+        grouped = dataFrame.groupby(Field_NameOfTurbine)
+
+        # 遍历每个设备的数据
+        for name, group in grouped:
+            # 创建图形和坐标轴  
+            fig, ax = plt.subplots(figsize=(12, 8), dpi=96)  
+            cmap = cm.get_cmap('rainbow')  
+            
+            # 绘制散点图  
+            scatter = ax.scatter(x=group[confData.field_wind_speed],  
+                                y=group[confData.field_power], c=group['monthIntTime'], cmap=cmap, s=5)  
+            
+            # 绘制合同功率曲线  
+            ax.plot(dataFrameGuaranteePowerCurve['风速'], dataFrameGuaranteePowerCurve['有功功率'], marker='o',  
+                    c='gray', label='Contract Guarantee Power Curve')  
+            
+            # 设置图形标题和坐标轴标签  
+            ax.set_title(f'turbine_name={name}')  
+            
+            # 设置坐标轴的主刻度定位器  
+            ax.xaxis.set_major_locator(MultipleLocator(1)) 
+            ax.set_xlim(0, 26)   
+
+            # 创建每100个单位一个刻度的定位器
+            yloc = MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                confData.graphSets["activePower"]) and not self.common.isNone(
+                confData.graphSets["activePower"]["step"]) else 250)
+            ax.yaxis.set_major_locator(yloc)  # 将定位器应用到y轴上
+            ax.set_ylim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                        confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+
+
+            ax.set_xlabel(x_name)  
+            ax.set_ylabel(y_name)  
+                        
+            # 显示图例,并调整位置  
+            ax.legend(loc='lower right')  
+            
+            # 设置颜色条  
+            unique_months = len(group['年月'].unique())
+            ticks = np.linspace(group['monthIntTime'].min(), group['monthIntTime'].max(), min(unique_months, 6))  # 减少刻度数量 
+            ticklabels = [datetime.fromtimestamp(tick).strftime('%Y-%m') for tick in ticks]  
+            norm = Normalize(group['monthIntTime'].min(), group['monthIntTime'].max())  
+            sm = cm.ScalarMappable(norm=norm, cmap=cmap)  
+            
+            # 添加颜色条  
+            cbar = fig.colorbar(sm, ax=ax)
+            cbar.set_ticks(ticks)  
+            cbar.set_ticklabels(ticklabels)  
+            
+            # 旋转x轴刻度标签  
+            plt.xticks(rotation=45)  
+            
+            # 保存图形为文件  
+            output_file = os.path.join(outputAnalysisDir, f"{name}-scatter.png")  
+            plt.savefig(output_file, bbox_inches='tight')  
+            
+            # 关闭图形  
+            plt.close()
+
+    def drawOfPowerCurve(self, dataFrameMerge: pd.DataFrame,  outputAnalysisDir, confData: ConfBusiness,dataFrameGuaranteePowerCurve:pd.DataFrame):
+        """  
+        生成功率曲线并保存为文件。  
+
+        参数:  
+        frames (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。  
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置 
+        """
+        
+        # 定义风速区间        
+        bins = np.arange(0, 26, 0.5)
+        
+        # 初始化结果DataFrame
+        all_res = pd.DataFrame()
+
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        # 计算每个设备的功率曲线
+        for name, group in grouped:
+            act_line = self.power_curve_helper(
+                group, confData.field_wind_speed, confData.field_power, bins)
+            act_line[Field_NameOfTurbine] = name
+            all_res = pd.concat([all_res, act_line], axis=0, sort=False)
+        
+        # 绘制全场功率曲线图
+        ress = all_res.reset_index(drop=True)        
+        
+        self.plot_power_curve(ress,  outputAnalysisDir, dataFrameGuaranteePowerCurve,Field_NameOfTurbine,
+                              '全场-{}功率曲线.png'.format(confData.farm_name),confData)
+
+        # 绘制每个设备的功率曲线图
+        grouped=ress.groupby(Field_NameOfTurbine)
+        for name, group in grouped:
+            self.plot_single_power_curve(ress, group,dataFrameGuaranteePowerCurve, name, outputAnalysisDir,confData)
+
+    def power_curve_helper(self, group, wind_speed_col, power_col, bins):
+        """  
+        计算设备的功率曲线。  
+        """
+        powerCut = group.groupby(pd.cut(group[wind_speed_col], bins, labels=np.arange(0, 25.5, 0.5))).agg({
+            power_col: 'mean',
+            wind_speed_col: ['mean', 'count']
+        })
+        wind_count = powerCut[wind_speed_col]['count'].tolist()
+        line = powerCut[power_col]['mean'].round(decimals=2).tolist()
+        act_line = pd.DataFrame([powerCut.index, wind_count, line]).T
+        act_line.columns = ['风速区间', '有效数量', '实际功率曲线']
+        return act_line
+
+    def plot_power_curve(self, ress, output_path,dataFrameGuaranteePowerCurve:pd.DataFrame, Field_NameOfTurbine, filename,confData:ConfBusiness):
+        """  
+        绘制全场功率曲线图。  
+        """
+        sns.set_palette('deep')
+        fig, ax = plt.subplots(figsize=(16, 8)) 
+        ax = sns.lineplot(x='风速区间', y='实际功率曲线', data=ress, hue=Field_NameOfTurbine)
+        # # 绘制合同功率曲线  
+        # ax.plot(dataFrameGuaranteePowerCurve['风速'], dataFrameGuaranteePowerCurve['有功功率'], marker='o',  
+        #         c='red', label='Contract Guarantee Power Curve') 
+        ax.set_xlabel('wind speed')
+        ax.set_ylabel('power')
+        ax.set_title('power curve')
+        # 设置坐标轴的主刻度定位器  
+        ax.xaxis.set_major_locator(MultipleLocator(1))   
+        ax.set_xlim(0, 26)  
+
+        # 创建每100个单位一个刻度的定位器
+        yloc = MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+            confData.graphSets["activePower"]) and not self.common.isNone(
+            confData.graphSets["activePower"]["step"]) else 250)
+        ax.yaxis.set_major_locator(yloc)  # 将定位器应用到y轴上
+        ax.set_ylim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                    confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+        
+        plt.legend(title='turbine',bbox_to_anchor=(1.02, 0.5),ncol=2, loc='center left', borderaxespad=0.) 
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.savefig(os.path.join(output_path, filename),
+                    bbox_inches='tight', dpi=120)
+        plt.close()
+
+    def plot_single_power_curve(self, ress, group,dataFrameGuaranteePowerCurve:pd.DataFrame , turbineName, outputAnalysisDir,confData:ConfBusiness):
+        
+        color = ["lightgrey"]*len(ress[Field_NameOfTurbine].unique())
+        fig, ax = plt.subplots(figsize=(8, 8))
+        ax = sns.lineplot(x='风速区间', y='实际功率曲线', data=ress, hue=Field_NameOfTurbine,
+                          palette=sns.set_palette(color), legend=False)
+        ax = sns.lineplot(x='风速区间', y='实际功率曲线', data=group,
+                          color='darkblue', legend=False)
+        
+        # 绘制合同功率曲线  
+        ax.plot(dataFrameGuaranteePowerCurve['风速'], dataFrameGuaranteePowerCurve['有功功率'], marker='o',  
+                c='red', label='Contract Guarantee Power Curve')  
+        
+        ax.set_xlabel('wind speed')
+        ax.set_ylabel('power')
+        ax.set_title('turbine_name={}'.format(turbineName))
+        # 设置坐标轴的主刻度定位器  
+        ax.xaxis.set_major_locator(MultipleLocator(1))  
+        ax.set_xlim(0, 26)  
+
+        # 创建每100个单位一个刻度的定位器
+        yloc = MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+            confData.graphSets["activePower"]) and not self.common.isNone(
+            confData.graphSets["activePower"]["step"]) else 250)
+        ax.yaxis.set_major_locator(yloc)  # 将定位器应用到y轴上
+        ax.set_ylim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                    confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+        
+        # 显示图例,并调整位置  
+        ax.legend(loc='lower right')  
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.savefig(outputAnalysisDir + r"/{}-curve.png".format(turbineName),
+                    bbox_inches='tight', dpi=120)
+        plt.close()

+ 106 - 0
dataAnalysisBusiness/algorithm/powerOscillationAnalyst.py

@@ -0,0 +1,106 @@
+import os
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt  
+import seaborn as sns  
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+class PowerOscillationAnalyst(Analyst):
+    """
+    风电机组功率震荡分析
+    """
+
+    def typeAnalyst(self):
+        return "power_diff"
+
+    def turbineAnalysis(self,
+                 dataFrame,
+                 outputAnalysisDir,
+                 outputFilePath,
+                 confData: ConfBusiness,
+                 turbineName):
+        self.power_diff(dataFrame, outputFilePath,
+                        confData.field_power,confData.field_rotor_speed)
+
+    def power_diff(self, dataFrame, outputFilePath,  field_Active_Power, field_Rotor_Speed):
+        # Floor the power column to the nearest 10
+        dataFrame['power_col_floor'] = (
+            dataFrame[field_Active_Power] // 10 * 10).astype('int32')
+
+        # Group by the floored power column
+        grouped = dataFrame.groupby('power_col_floor')
+
+        # Calculate max, min, and diff of generator speed within each group
+        agg_df = grouped[field_Rotor_Speed].agg(
+            speed_max='max',
+            speed_min='min'
+        )
+        agg_df['speed_diff'] = agg_df['speed_max'] - agg_df['speed_min']
+
+        # Sort by the floored power column
+        agg_df = agg_df.sort_index()
+
+        # Write the result to a CSV file
+        agg_df.to_csv(outputFilePath)
+
+    def turbinesAnalysis(self,dataFrameMerge,outputAnalysisDir, confData: ConfBusiness):
+        self.plot_power_oscillations(outputAnalysisDir,confData.farm_name)
+  
+    def plot_power_oscillations(self, csvFileDirOfCp, farm_name, encoding='utf-8'):
+        """  
+        Plot TSR trend from CSV files in a given input path and save the plots to an output path.  
+
+        Parameters:  
+        - csvFileDirOfCp: str, path to the directory containing input CSV files.
+        - farm_name: str, name of the wind farm.
+        - encoding: str, encoding of the input CSV files. Defaults to 'utf-8'.
+        """        
+        sns.set_palette('deep')
+        field_Name_Turbine = "turbine_name"
+        x_name = 'power_col_floor'  
+        y_name = 'speed_diff'  
+
+        # 初始化结果DataFrame  
+        res = pd.DataFrame()  
+        
+        # 遍历输入路径下的所有文件  
+        for root, dir_names, file_names in dir.list_directory(csvFileDirOfCp):  
+            for file_name in file_names:  
+
+                if not file_name.endswith(CSVSuffix):
+                    continue
+
+                file_path = os.path.join(root, file_name)  
+                frame = pd.read_csv(file_path, encoding=encoding)  
+                
+                # 获取输出文件名前缀  
+                turbine_name = file_name.split(CSVSuffix)[0]  
+                # 添加设备名作为新列
+                frame[field_Name_Turbine] = turbine_name
+                 
+                selected_data = frame.loc[:, [field_Name_Turbine, x_name, y_name]]  
+                res = pd.concat([res, selected_data], axis=0)  
+        
+        # 重置索引  
+        ress = res.reset_index(drop=True)  
+        
+        # 绘制所有设备的功率震荡图  
+        fig, ax = plt.subplots(figsize=(16, 8))  
+        ax = sns.lineplot(x=x_name, y=y_name, data=ress, hue=field_Name_Turbine)  
+        ax.set_title('Power-Oscillation')  
+        plt.legend(ncol=4)  
+        plt.savefig(csvFileDirOfCp+ r'/{}-Power-Oscillation.png'.format(farm_name), bbox_inches='tight', dpi=300)  
+        plt.close()  
+        
+        # 分组绘制每个设备的功率震荡图  
+        grouped = ress.groupby(field_Name_Turbine)  
+        for name, group in grouped:  
+            color = ["lightgrey"] * len(ress[field_Name_Turbine].unique())  
+            fig, ax = plt.subplots(figsize=(8, 8))  
+            ax = sns.lineplot(x=x_name, y=y_name, data=ress, hue=field_Name_Turbine, palette=sns.color_palette(color), legend=False)  
+            ax = sns.lineplot(x=x_name, y=y_name, data=group, color='darkblue', legend=False)  
+            ax.set_title('turbine_name={}'.format(name))  
+            plt.savefig(csvFileDirOfCp+ r'/{}.png'.format(name), bbox_inches='tight', dpi=120)  
+            plt.close()  

+ 106 - 0
dataAnalysisBusiness/algorithm/powerScatter2DAnalyst.py

@@ -0,0 +1,106 @@
+import os
+from datetime import datetime
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import matplotlib.cm as cm
+from matplotlib.ticker import MultipleLocator
+from matplotlib.colors import Normalize
+import seaborn as sns
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from geopy.distance import geodesic
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+class PowerScatter2DAnalyst(Analyst):
+    """
+    风电机组功率曲线散点分析。
+    秒级scada数据运算太慢,建议使用分钟级scada数据
+    """
+
+    def typeAnalyst(self):
+        return "power_scatter_2D"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        if len(dataFrameMerge)<=0:
+            print("After screening for blade pitch angle less than the configured value, plot power curve scatter points without data")
+            return
+        
+        dataFrameGuaranteePowerCurve=self.common.contractGuaranteePowerCurveData(confData.turbineGuaranteedPowerCurveFilePathCSV,confData)
+        self.drawOfPowerCurveScatter(dataFrameMerge,outputAnalysisDir,confData,dataFrameGuaranteePowerCurve)
+        
+    def drawOfPowerCurveScatter(self, dataFrame: pd.DataFrame,  outputAnalysisDir, confData: ConfBusiness,dataFrameGuaranteePowerCurve:pd.DataFrame):
+        """  
+        绘制风速-功率分布图并保存为文件。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        csvPowerCurveFilePath (str): 功率曲线文件路径。   
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+        x_name = 'wind_speed'
+        y_name = 'power'
+
+        # 按设备名分组数据
+        grouped = dataFrame.groupby(Field_NameOfTurbine)
+
+        # 遍历每个设备的数据
+        for name, group in grouped:
+            # 创建图形和坐标轴  
+            fig, ax = plt.subplots(figsize=(12, 8), dpi=96)  
+            cmap = cm.get_cmap('rainbow')  
+            
+            # 绘制散点图  
+            scatter = ax.scatter(x=group[confData.field_wind_speed],  
+                                y=group[confData.field_power], c=group['monthIntTime'], cmap=cmap, s=5)  
+            
+            # 绘制合同功率曲线  
+            ax.plot(dataFrameGuaranteePowerCurve['风速'], dataFrameGuaranteePowerCurve['有功功率'], marker='o',  
+                    c='gray', label='Contract Guarantee Power Curve')  
+            
+            # 设置图形标题和坐标轴标签  
+            ax.set_title(f'turbine_name={name}')  
+            
+            # 设置坐标轴的主刻度定位器  
+            ax.xaxis.set_major_locator(MultipleLocator(1))   
+            ax.set_xlim(0, 26)  
+
+            # 创建每100个单位一个刻度的定位器
+            yloc = MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                confData.graphSets["activePower"]) and not self.common.isNone(
+                confData.graphSets["activePower"]["step"]) else 250)
+            ax.yaxis.set_major_locator(yloc)  # 将定位器应用到y轴上
+            ax.set_ylim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                        confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+
+            ax.set_xlabel(x_name)  
+            ax.set_ylabel(y_name)  
+                        
+            # 显示图例,并调整位置  
+            ax.legend(loc='lower right')  
+            
+            # 设置颜色条  
+            unique_months = len(group['年月'].unique())
+            ticks = np.linspace(group['monthIntTime'].min(), group['monthIntTime'].max(), min(unique_months, 6))  # 减少刻度数量 
+            ticklabels = [datetime.fromtimestamp(tick).strftime('%Y-%m') for tick in ticks]  
+            norm = Normalize(group['monthIntTime'].min(), group['monthIntTime'].max())  
+            sm = cm.ScalarMappable(norm=norm, cmap=cmap)  
+            
+            # 添加颜色条  
+            cbar = fig.colorbar(sm, ax=ax)
+            cbar.set_ticks(ticks)  
+            cbar.set_ticklabels(ticklabels)  
+            
+            # 旋转x轴刻度标签  
+            plt.xticks(rotation=45)  
+            
+            # 保存图形为文件  
+            output_file = os.path.join(outputAnalysisDir, f"{name}-scatter.png")  
+            plt.savefig(output_file, bbox_inches='tight')  
+            
+            # 关闭图形  
+            plt.close()

+ 119 - 0
dataAnalysisBusiness/algorithm/powerScatterAnalyst.py

@@ -0,0 +1,119 @@
+import os
+from datetime import datetime
+import pandas as pd
+import numpy as np
+import pandas as pd
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+import plotly.express.colors as px_colors
+
+
+class PowerScatterAnalyst(Analyst):
+    """
+    风电机组功率曲线散点分析。
+    秒级scada数据运算太慢,建议使用分钟级scada数据
+    """
+
+    def typeAnalyst(self):
+        return "power_scatter"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        if len(dataFrameMerge) <= 0:
+            print("After screening for blade pitch angle less than the configured value, plot power curve scatter points without data")
+            return
+
+        dataFrameGuaranteePowerCurve = self.dataFrameContractOfTurbine
+
+        self.drawOfPowerCurveScatter(
+            dataFrameMerge, outputAnalysisDir, confData, dataFrameGuaranteePowerCurve)
+
+    def contractGuaranteePowerCurveData(self, csvPowerCurveFilePath):
+        dataFrameGuaranteePowerCurve = pd.read_csv(
+            csvPowerCurveFilePath, encoding=charset_unify)
+
+        return dataFrameGuaranteePowerCurve
+
+    def drawOfPowerCurveScatter(self, dataFrame: pd.DataFrame,  outputAnalysisDir, confData: ConfBusiness, dataFrameGuaranteePowerCurve: pd.DataFrame):
+        """  
+        绘制风速-功率分布图并保存为文件。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        csvPowerCurveFilePath (str): 功率曲线文件路径。   
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+        # 按设备名分组数据
+        colorsList = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
+                      '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#aec7e8', '#ffbb78']
+        grouped = dataFrame.groupby(Field_NameOfTurbine)
+
+        # 遍历每个设备的数据
+        for name, group in grouped:
+            # 创建颜色映射,将每个年月映射到一个唯一的颜色
+            unique_months = group[Field_YearMonth].unique()
+            colors = [
+                colorsList[i % 12] for i in range(len(unique_months))]
+            color_map = dict(zip(unique_months, colors))
+
+            # 使用go.Scatter3d创建3D散点图
+            trace = go.Scatter3d(
+                x=group[confData.field_wind_speed],
+                y=group[Field_YearMonth],
+                z=group[confData.field_power],
+                mode='markers',
+                marker=dict(
+                    color=[color_map[month]
+                           for month in group[Field_YearMonth]],
+                    size=1.5,
+                    line=dict(
+                        color='rgba(0, 0, 0, 0)',  # 设置边框颜色为透明,以去掉白色边框
+                        width=0  # 设置边框宽度为0,进一步确保没有边框
+                    ),
+                    opacity=0.8  # 调整散点的透明度,增加透视效果
+                )
+            )
+
+            # 创建图形
+            fig = go.Figure(data=[trace])
+
+            # 更新图形的布局
+            fig.update_layout(
+                title={
+                    "text": f'Monthly power 3D scatter plot {name}',
+                    "x": 0.5
+                },
+                scene=dict(
+                    xaxis=dict(title='Wind Speed'),
+                    yaxis=dict(
+                        title='Time',
+                        tickmode='array',
+                        tickvals=unique_months,
+                        ticktext=unique_months,
+                        categoryorder='category ascending'
+                    ),
+                    zaxis=dict(
+                        title='Power',
+                        dtick=confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                            confData.graphSets["activePower"]) and not self.common.isNone(
+                            confData.graphSets["activePower"]["step"]) else 250,
+                        range=[confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                            confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2],
+                    )
+                ),
+                scene_camera=dict(
+                    up=dict(x=0, y=0, z=1),  # 保持相机向上方向不变
+                    center=dict(x=0, y=0, z=0),  # 保持相机中心位置不变
+                    eye=dict(x=-1.8, y=-1.8, z=1.2)  # 调整eye属性以实现水平旋转180°
+                ),
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+
+            # 保存图像
+            outputFileHtml = os.path.join(
+                outputAnalysisDir, "{}.html".format(name))
+
+            fig.write_html(outputFileHtml)

+ 86 - 0
dataAnalysisBusiness/algorithm/ratedPowerWindSpeedAnalyst.py

@@ -0,0 +1,86 @@
+import os
+from datetime import datetime
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import matplotlib.cm as cm
+from matplotlib.ticker import MultipleLocator
+from matplotlib.colors import Normalize
+import matplotlib.ticker as ticker
+import seaborn as sns
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from geopy.distance import geodesic
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class RatedPowerWindSpeedAnalyst(Analyst):
+    """
+    风电机组额定功率风速分析。
+    秒级scada数据运算太慢,建议使用分钟级scada数据
+    """
+
+    def typeAnalyst(self):
+        return "rated_power_windspeed"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.draw(dataFrameMerge, outputAnalysisDir, confData)
+
+    def draw(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        """  
+        绘制并保存额定满发风速功率分布图,根据环境温度是否大于等于25℃。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置
+        """
+        # 检查所需列是否存在
+        required_columns = {confData.field_env_temp,
+                            confData.field_wind_speed, confData.field_power}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+
+        y_name = 'power'
+        upLimitOfPower = confData.rated_power*1.1
+        lowLimitOfPower = confData.rated_power*0.9
+        # 根据环境温度筛选数据
+        over_temp = dataFrameMerge[(dataFrameMerge[confData.field_env_temp] >= 25) & (
+            dataFrameMerge[confData.field_wind_speed] >= confData.rated_WindSpeed) & (dataFrameMerge[confData.field_power] >= lowLimitOfPower)].sort_values(by=Field_NameOfTurbine)
+        below_temp = dataFrameMerge[(dataFrameMerge[confData.field_env_temp] < 25) & (
+            dataFrameMerge[confData.field_wind_speed] >= confData.rated_WindSpeed) & (dataFrameMerge[confData.field_power] >= lowLimitOfPower)].sort_values(by=Field_NameOfTurbine)
+
+        # 绘制环境温度大于等于25℃的功率分布图
+        fig, ax = plt.subplots()
+        sns.boxplot(y=confData.field_power, x=Field_NameOfTurbine, data=over_temp, fliersize=0, ax=ax,
+                    medianprops={'linestyle': '-', 'color': 'red'},
+                    boxprops={'color': 'dodgerblue', 'facecolor': 'dodgerblue'})
+        ax.yaxis.set_major_locator(ticker.MultipleLocator(100))
+        ax.set_ylim(lowLimitOfPower, upLimitOfPower)
+        ax.set_ylabel(y_name)
+        ax.set_title(
+            'rated wind speed and power distribute(10min)(ambient temperature>=25℃)')
+        ax.grid(True)
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.savefig(os.path.join(outputAnalysisDir,
+                    "额定满发风速功率分布(10min)(环境温度大于25度).png"), bbox_inches='tight', dpi=120)
+        plt.close()
+
+        # 绘制环境温度小于25℃的功率分布图
+        fig, ax = plt.subplots()
+        sns.boxplot(y=confData.field_power, x=Field_NameOfTurbine, data=below_temp, fliersize=0, ax=ax,
+                    medianprops={'linestyle': '-', 'color': 'red'},
+                    boxprops={'color': 'dodgerblue', 'facecolor': 'dodgerblue'})
+        ax.yaxis.set_major_locator(ticker.MultipleLocator(100))
+        ax.set_ylim(lowLimitOfPower, upLimitOfPower)
+        ax.set_ylabel(y_name)
+        ax.set_title(
+            'rated wind speed and power distribute(10min)(ambient temperature<25℃)')
+        ax.grid(True)
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.savefig(os.path.join(outputAnalysisDir,
+                    "额定满发风速功率分布(10min)(环境温度小于25度).png"), bbox_inches='tight', dpi=120)
+        plt.close()

+ 66 - 0
dataAnalysisBusiness/algorithm/ratedWindSpeedAnalyst.py

@@ -0,0 +1,66 @@
+import os
+from datetime import datetime
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import matplotlib.cm as cm
+from matplotlib.ticker import MultipleLocator
+from matplotlib.colors import Normalize
+import seaborn as sns
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from geopy.distance import geodesic
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class RatedWindSpeedAnalyst(Analyst):
+    """
+    风电机组额定风速分析。
+    秒级scada数据运算太慢,建议使用分钟级scada数据
+    """
+
+    def typeAnalyst(self):
+        return "rated_windspeed"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.draw(dataFrameMerge, outputAnalysisDir, confData)
+
+    def draw(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir,  confData: ConfBusiness):
+        """  
+        绘制并保存满发风速区间数据计数图。  
+
+        参数:  
+        dataFrameMerge (pd.DataFrame): 包含数据的DataFrame,需要包含设备名、风速和功率列。
+        outputAnalysisDir (str): 分析输出目录。  
+        confData (ConfBusiness): 配置   
+        """
+
+        # 初始化结果列表
+        res = []
+
+        # 按设备名分组并计算统计数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+        for name, group in grouped:
+            group = group[group[confData.field_wind_speed] >= 11]
+            res.append([name, group[confData.field_power].min(), group[confData.field_power].max(
+            ), group[confData.field_power].mean(), group.shape[0]])
+
+        # 创建结果DataFrame
+        data = pd.DataFrame(res, columns=[
+                            Field_NameOfTurbine, 'power-min', 'power-max', 'power-mean', 'count'])
+
+        # 绘制风速区间数据计数图
+        fig, ax = plt.subplots()
+        sns.barplot(x=Field_NameOfTurbine, y='count',
+                    data=data, ax=ax, color='dodgerblue')
+        ax.set_title('Rated - full wind speed interval data count')
+        ax.grid(True)
+        # 旋转45度
+        plt.xticks(rotation=45)  
+        # 保存图表
+        plt.savefig(os.path.join(outputAnalysisDir, "风速区间数据计数.png"),
+                    bbox_inches='tight', dpi=120)
+        plt.close()

+ 116 - 0
dataAnalysisBusiness/algorithm/temperatureEnvironmentAnalyst.py

@@ -0,0 +1,116 @@
+import os
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import seaborn as sns
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from geopy.distance import geodesic
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+import common.turbineInfo as turbineInfo
+
+
+class TemperatureEnvironmentAnalyst(Analyst):
+    """
+    风电机组大部件温升分析
+    """
+
+    def typeAnalyst(self):
+        return "temperature_environment"
+
+    def turbinesAnalysis(self, dataFrameMerge,outputAnalysisDir, confData: ConfBusiness):        
+        # 检查所需列是否存在
+        required_columns = {confData.field_env_temp,
+                            Field_NameOfTurbine}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+        
+        turbineInfos = turbineInfo.loadTurbineInfo(confData.turbineInfoFilePathCSV)
+
+        #  环境温度 分析
+        turbineEnvTempData =dataFrameMerge.groupby(Field_NameOfTurbine).agg(
+            {confData.field_env_temp : 'median'}).reset_index(names=[Field_NameOfTurbine]) 
+        mergeData = self.merge_Data(Field_NameOfTurbine,turbineInfos, turbineEnvTempData,confData)
+        
+        self.draw(mergeData,outputAnalysisDir, confData)
+    
+
+    def merge_Data(self,fieldTurbineName, turbineInfos:pd.DataFrame, turbineEnvTempData, confData: ConfBusiness):
+        """
+        将每台机组的环境温度均值数据与机组信息,按机组合并
+
+        参数:
+        turbineInfos (pandas.DataFrame): 机组信息数据
+        turbineEnvTempData (pandas.DataFrame): 每台机组的环境温度均值数据
+
+        返回:
+        pandas.DataFrame: 每台机组的环境温度均值数据与机组信息合并数据
+        """
+
+        """
+        合并类型how的选项包括:
+
+        'inner': 内连接,只保留两个DataFrame中都有的键的行。
+        'outer': 外连接,保留两个DataFrame中任一或两者都有的键的行。
+        'left': 左连接,保留左边DataFrame的所有键,以及右边DataFrame中匹配的键的行。
+        'right': 右连接,保留右边DataFrame的所有键,以及左边DataFrame中匹配的键的行。
+        """
+        # turbineEnvTempData[fieldTurbineName]=turbineEnvTempData[fieldTurbineName].astype(str)
+        merge_data = pd.merge(turbineInfos, turbineEnvTempData, on=[fieldTurbineName], how='inner')
+
+        return merge_data
+
+    # 定义查找给定半径内点的函数
+    def find_points_within_radius(self, data, center, field_temperature_env, radius):
+        points_within_radius = []
+        for index, row in data.iterrows():
+            distance = geodesic(
+                (center[2], center[1]), (row['latitude'], row['longitude'])).meters
+            if distance <= radius:
+                points_within_radius.append(
+                    (row['turbine_name'], row[field_temperature_env]))
+        return points_within_radius
+
+    fieldTemperatureDiff="temperature_diff"
+    def draw(self,dataFrame : pd.DataFrame, outputAnalysisDir,confData: ConfBusiness,charset=charset_unify):
+        # 处理数据
+        dataFrame['new'] = dataFrame.loc[:, [Field_NameOfTurbine , 'longitude', 'latitude', confData.field_env_temp]].apply(tuple, axis=1)
+        coordinates = dataFrame['new'].tolist()
+        df = pd.DataFrame(coordinates, columns=[Field_NameOfTurbine, 'longitude', 'latitude', confData.field_env_temp])
+
+        # 查找半径内的点
+        points_within_radius = {coord: self.find_points_within_radius(dataFrame,coord,confData.field_env_temp,confData.rotor_diameter*10) for coord in coordinates}
+        res = []
+        for center, nearby_points in points_within_radius.items():
+            current_temp = dataFrame[dataFrame[Field_NameOfTurbine] == center[0]][confData.field_env_temp].iloc[0]
+            target_tuple = (center[0], current_temp)
+            if target_tuple in nearby_points:
+                nearby_points.remove(target_tuple)
+            mean_temp = np.median([i[1] for i in nearby_points]) if nearby_points else current_temp
+            res.append((center[0], nearby_points, mean_temp, current_temp))
+        res = pd.DataFrame(res, columns=[Field_NameOfTurbine, '周边机组', '周边机组温度', '当前机组温度'])
+        res[self.fieldTemperatureDiff] = res['当前机组温度'] - res['周边机组温度']
+
+        fig, ax = plt.subplots(figsize=(16,8),dpi=96)
+        # 设置x轴刻度值旋转角度为45度  
+        plt.tick_params(axis='x', rotation=45)
+
+        sns.barplot(x=Field_NameOfTurbine,y=self.fieldTemperatureDiff,data=res,ax=ax,color='dodgerblue')
+        plt.axhline(y=5,ls=":",c="red")#添加水平直线
+        plt.axhline(y=-5,ls=":",c="red")#添加水平直线
+        ax.set_ylabel('temperature_difference')
+        ax.set_title('temperature Bias')
+        plt.savefig(outputAnalysisDir +'//'+ "{}环境温差Bias.png".format(confData.farm_name),bbox_inches='tight',dpi=120)
+
+
+        fig2, ax2 = plt.subplots(figsize=(16,8),dpi=96)
+        # 设置x轴刻度值旋转角度为45度  
+        plt.tick_params(axis='x', rotation=45)
+        
+        sns.barplot(x=Field_NameOfTurbine ,y='当前机组温度',data=res,ax=ax2,color='dodgerblue')
+        ax2.set_ylabel('temperature')
+        ax2.set_title('temperature median')
+        plt.savefig(outputAnalysisDir +'//'+ "{}环境温度均值.png".format(confData.farm_name),bbox_inches='tight',dpi=120)

+ 375 - 0
dataAnalysisBusiness/algorithm/temperatureLargeComponentsAnalyst.py

@@ -0,0 +1,375 @@
+import os
+import pandas as pd
+from matplotlib.ticker import MultipleLocator
+import numpy as np
+import pandas as pd
+import plotly.graph_objects as go
+import matplotlib.pyplot as plt
+import matplotlib.ticker as ticker
+import seaborn as sns
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class Generator:
+    def __init__(self) -> None:
+        self.fieldTemperatorOfDEBearing = None
+        self.fieldTemperatorOfNDEBearing = None
+
+
+class TemperatureLargeComponentsAnalyst(Analyst):
+    """
+    风电机组大部件温升分析
+    """
+    fieldPowerFloor = 'power_floor'
+
+    def typeAnalyst(self):
+        return "temperature_large_components"
+
+    def getUseColumns(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        # Convert the string list of temperature columns into a list
+        temperature_cols = self.getLargeComponentTemperatureColumns(confData)
+        # 获取非全为空的列名
+        non_empty_cols = self.getNoneEmptyFields(dataFrame, temperature_cols)
+
+        useCols = []
+        useCols.append(confData.field_turbine_time)
+        useCols.append(confData.field_power)
+
+        if not self.common.isNone(confData.field_env_temp) and confData.field_env_temp in dataFrame.columns:
+            useCols.append(confData.field_env_temp)
+
+        if not self.common.isNone(confData.field_nacelle_temp) and confData.field_nacelle_temp in dataFrame.columns:
+            useCols.append(confData.field_nacelle_temp)
+
+        useCols.extend(non_empty_cols)
+
+        return useCols
+
+    def getLargeComponentTemperatureColumns(self, confData: ConfBusiness):
+        if self.common.isNone(confData.field_temperature_large_components):
+            return []
+        temperature_cols = confData.field_temperature_large_components.split(
+            ',')
+        return temperature_cols
+
+    # def filterCustomForTurbine(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+    #     useCols = self.getUseColumns(dataFrame,confData)
+    #     # 清洗数据
+    #     dataFrameCustome = dataFrame[useCols].copy()
+    #     dataFrameCustome = dataFrameCustome.dropna(axis=1, how='all')
+    #     dataFrameCustome = dataFrameCustome.dropna(axis=0, subset=useCols)
+
+    #     return dataFrameCustome
+
+    # def turbineAnalysis(self,
+    #                     dataFrame,
+    #                     outputAnalysisDir,
+    #                     outputFilePath,
+    #                     confData: ConfBusiness,
+    #                     turbineName):
+    #     self.temp_power(dataFrame, outputFilePath, confData)
+
+    def getNoneEmptyFields(self, dataFrame, temperatureFields):
+        # 使用set和列表推导式来获取在DataFrame中存在的字段
+        existing_fields = [
+            field for field in temperatureFields if field in dataFrame.columns]
+        # 检查指定列中非全为空的列
+        non_empty_columns = dataFrame[existing_fields].apply(
+            lambda x: x.notnull().any(), axis=0)
+        # 获取非全为空的列名
+        noneEmptyFields = non_empty_columns[non_empty_columns].index.tolist()
+        return noneEmptyFields
+
+    def dataReprocess(self, dataFrame: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 获取非全为空的列名
+        non_empty_cols = self.getUseColumns(dataFrame, confData)
+        dataFrame = dataFrame.dropna(subset=non_empty_cols)
+        # Calculate 'power_floor'
+        dataFrame[self.fieldPowerFloor] = (
+            dataFrame[confData.field_power] / 10).astype(int) * 10
+
+        # Initialize an empty DataFrame for aggregation
+        # agg_dict = {col: 'mean' for col in non_empty_cols}
+        agg_dict = {col: 'median' for col in non_empty_cols}
+
+        # Group by 'power_floor' and aggregate
+        grouped = dataFrame.groupby(
+            [self.fieldPowerFloor, Field_NameOfTurbine]).agg(agg_dict).reset_index()
+
+        # Sort by 'power_floor'
+        grouped.sort_values(
+            [self.fieldPowerFloor, Field_NameOfTurbine], inplace=True)
+
+        return grouped
+
+    def turbinesAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # self.plot_temperature_distribution(dataFrameMerge,
+        #     outputAnalysisDir, confData, confData.field_temperature_large_components)
+        dataFrame = self.dataReprocess(
+            dataFrameMerge, outputAnalysisDir, confData)
+        self.drawTemperatureGraph(dataFrame, outputAnalysisDir, confData)
+
+    def drawTemperatureGraph(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        """
+        大部件温度传感器分析
+        """
+        y_name = 'temperature'
+
+        outputDir = os.path.join(outputAnalysisDir, "GeneratorTemperature")
+        dir.create_directory(outputDir)
+
+        columns = confData.field_temperature_large_components.split(',')
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        # Create output directories if they don't exist
+        for column in columns:
+            if not column in dataFrameMerge.columns:
+                continue
+
+            sns.set_palette('deep')
+
+            outputPath = os.path.join(outputAnalysisDir, column)
+            dir.create_directory(outputPath)
+
+            # 绘制当前温度测点的,所有机组折线图
+            fig, ax = plt.subplots()
+            ax = sns.lineplot(x=self.fieldPowerFloor, y=column, data=dataFrameMerge,
+                              hue=Field_NameOfTurbine)
+
+            # 创建每100个单位一个刻度的定位器
+            yloc = MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                confData.graphSets["activePower"]) and not self.common.isNone(
+                confData.graphSets["activePower"]["step"]) else 250)
+            ax.xaxis.set_major_locator(yloc)  # 将定位器应用到y轴上
+            ax.set_xlim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                        confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+
+            ax.yaxis.set_major_locator(ticker.MultipleLocator(20))
+            ax.set_ylim(0, 100)
+            ax.set_xlabel(self.fieldPowerFloor)
+            ax.set_ylabel(y_name)
+            ax.set_title('Temperature-Distribute')
+
+            # 获取线对象的句柄和标签
+            lines, labels = ax.get_legend_handles_labels()
+            # 排序标签(例如,按照字母顺序)
+            sorted_labels = sorted(labels)
+            sorted_lines = [l for l, lbl in zip(
+                lines, labels) if lbl in sorted_labels]
+            # 创建新的图例
+            ax.legend(sorted_lines, sorted_labels, bbox_to_anchor=(
+                1.02, 0.5), loc='center left', ncol=2, borderaxespad=0.)
+
+            # plt.legend(bbox_to_anchor=(1.02, 0.5),
+            #             loc='center left', ncol=2, borderaxespad=0.)
+            plt.savefig(os.path.join(outputPath, "{}.png".format(
+                column)), bbox_inches='tight', dpi=120)
+            plt.close()
+
+            for name, group in grouped:
+                # Write to CSV
+                # csvFileOfTurbine=os.path.join(outputAnalysisDir,f'{name}{CSVSuffix}')
+                # group.to_csv(csvFileOfTurbine,index=False)
+                color = ["lightgrey"] * \
+                    len(dataFrameMerge[Field_NameOfTurbine].unique())
+                fig, ax = plt.subplots()
+                ax = sns.lineplot(x=self.fieldPowerFloor, y=column, data=dataFrameMerge, hue=Field_NameOfTurbine,
+                                  palette=sns.set_palette(color), legend=False)
+                ax = sns.lineplot(x=self.fieldPowerFloor, y=column, data=group,
+                                  color='darkblue', legend=False)
+                
+                ax.set_title('turbine_name={}'.format(name))
+
+                ax.yaxis.set_major_locator(ticker.MultipleLocator(20))
+                ax.set_ylim(0, 100)
+                ax.set_ylabel(y_name)
+
+                ax.xaxis.set_major_locator(ticker.MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+                    confData.graphSets["activePower"]) and not self.common.isNone(
+                    confData.graphSets["activePower"]["step"]) else 250))
+                ax.set_xlim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+                    confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+                ax.set_xlabel(self.fieldPowerFloor)
+
+                plt.savefig(os.path.join(outputPath, "{}.png".format(
+                    name)), bbox_inches='tight', dpi=120)
+
+                plt.close()
+
+                # 绘制每台机组发电机的,驱动轴承温度、非驱动轴承温度、发电机轴承温度BIAS、发电机轴承温度和机舱温度BIAS 均与有功功率的折线图
+                dictConf = self.getGeneratorTemperatureConf(confData)
+                if not self.common.isNone(dictConf):
+                    self.drawGeneratorTemperature(
+                        group, confData, dictConf["yAxisDE"], dictConf["yAxisNDE"], dictConf["diffTemperature"], self.fieldPowerFloor, name, outputDir)
+
+    def plot_temperature_distribution(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness, field_temperature_large_componts, encoding='utf-8'):
+        """
+        Generates Cp distribution plots for turbines in a wind farm.
+
+        Parameters:
+        - csvFileDirOfCp: str, path to the directory containing input CSV files.
+        - farm_name: str, name of the wind farm.
+        - encoding: str, encoding of the input CSV files. Defaults to 'utf-8'.
+        """
+        field_Name_Turbine = "turbine_name"
+        x_name = 'power_floor'
+        y_name = 'temperature'
+
+        outputDir = os.path.join(outputAnalysisDir, "GeneratorTemperature")
+        dir.create_directory(outputDir)
+        sns.set_palette('deep')
+
+        columns = field_temperature_large_componts.split(',')
+        # Create output directories if they don't exist
+        for column in columns:
+            type_name = '{}'.format(column)
+            output_path = os.path.join(outputAnalysisDir, type_name)
+            os.makedirs(output_path, exist_ok=True)
+            print("current column {}".format(column))
+
+            # Initialize DataFrame to store concatenated data
+            res = pd.DataFrame()
+
+            # Iterate over files in the input path
+            for root, dir_names, file_names in dir.list_directory(outputAnalysisDir):
+                for file_name in file_names:
+
+                    if not file_name.endswith(CSVSuffix):
+                        continue
+
+                    print(os.path.join(root, file_name))
+                    frame = pd.read_csv(os.path.join(
+                        root, file_name), encoding=encoding)
+
+                    if column not in frame.columns:
+                        continue
+
+                    # 获取输出文件名(不含split_way之后的部分)
+                    turbineName = file_name.split(CSVSuffix)[0]
+                    # 添加设备名作为新列
+                    frame[field_Name_Turbine] = confData.add_W_if_starts_with_digit(
+                        turbineName)
+
+                    dictConf = self.getGeneratorTemperatureConf(confData)
+                    if not self.common.isNone(dictConf):
+                        self.drawGeneratorTemperature(
+                            frame, dictConf["yAxisDE"], dictConf["yAxisNDE"], dictConf["diffTemperature"], x_name, turbineName, outputDir)
+
+                    res = pd.concat(
+                        [res, frame.loc[:, [field_Name_Turbine, x_name, column]]], axis=0)
+
+            # Reset index and plot
+            ress = res.reset_index()
+            fig, ax2 = plt.subplots()
+            ax2 = sns.lineplot(x=x_name, y=column, data=ress,
+                               hue=field_Name_Turbine)
+            # ax2.set_xlim(-150, 2100)
+            ax2.set_xlabel(x_name)
+            ax2.set_ylabel(y_name)
+            ax2.set_title('Temperature-Distribute')
+            plt.legend(bbox_to_anchor=(1.02, 0.5),
+                       loc='center left', ncol=2, borderaxespad=0.)
+            plt.savefig(os.path.join(output_path, "{}.png".format(
+                column)), bbox_inches='tight', dpi=120)
+            plt.close()
+
+            # Plot individual device lines
+            grouped = ress.groupby(field_Name_Turbine)
+            for name, group in grouped:
+                color = ["lightgrey"] * len(ress[field_Name_Turbine].unique())
+                fig, ax = plt.subplots()
+                ax = sns.lineplot(x=x_name, y=column, data=ress, hue=field_Name_Turbine,
+                                  palette=sns.set_palette(color), legend=False)
+                ax = sns.lineplot(x=x_name, y=column, data=group,
+                                  color='darkblue', legend=False)
+                ax.set_xlabel(x_name)
+                ax.set_ylabel(y_name)
+                ax.set_title('turbine_name={}'.format(name))
+                # ax.set_xlim(-150, 2100)
+                plt.savefig(os.path.join(output_path, "{}.png".format(
+                    name)), bbox_inches='tight', dpi=120)
+
+                plt.close()
+
+    def getGeneratorTemperatureConf(self, confData: ConfBusiness):
+        if self.common.isNone(confData.temperature_Generator) or self.common.isNone(confData.temperature_Generator["yAxisDE"]) or self.common.isNone(confData.temperature_Generator["yAxisNDE"]):
+            return None
+
+        return confData.temperature_Generator
+
+    def drawGeneratorTemperature(self, dataFrame: pd.DataFrame, confData: ConfBusiness, yAxisDE, yAxisNDE, diffTemperature, xAxis, turbineName, outputDir):
+        # 发电机驱动轴承温度 和 发电机非驱动轴承 温差
+        fieldBIAS_DE_NDE = 'BIAS_DE-NDE'
+        fieldBIAS_DE = 'BIAS_DE'
+        fieldBIAS_NDE = 'BIAS_NDE'
+
+        # 绘制双y轴折线图
+        fig, ax1 = plt.subplots()
+
+        # 绘制低速轴承温度和高速轴承温度
+        ax1.plot(dataFrame[xAxis], dataFrame[yAxisDE],
+                 label='DEBearingTemperature', color='blue')
+        # 计算低速轴承温度和高速轴承温度的差值
+        dataFrame[fieldBIAS_DE] = dataFrame[yAxisDE] - \
+            dataFrame[diffTemperature]
+        # 绘制温度差值
+        ax1.plot(dataFrame[xAxis], dataFrame[fieldBIAS_DE],
+                 label=fieldBIAS_DE, color='blue', linestyle=':')
+
+        ax1.plot(dataFrame[xAxis], dataFrame[yAxisNDE],
+                 label='NDEBearingTemperature', color='green')
+        # 计算低速轴承温度和高速轴承温度的差值
+        dataFrame[fieldBIAS_NDE] = dataFrame[yAxisNDE] - \
+            dataFrame[diffTemperature]
+        # 绘制温度差值
+        ax1.plot(dataFrame[xAxis], dataFrame[fieldBIAS_NDE],
+                 label=fieldBIAS_NDE, color='green', linestyle=':')
+
+        ax1.plot(dataFrame[xAxis], dataFrame[diffTemperature],
+                 label='NacelleTemperature', color='orange')
+
+        # # 创建第二个y轴
+        # ax2 = ax1.twinx()
+
+        # 计算低速轴承温度和高速轴承温度的差值
+        dataFrame[fieldBIAS_DE_NDE] = dataFrame[yAxisDE] - dataFrame[yAxisNDE]
+        # 绘制温度差值
+        ax1.plot(dataFrame[xAxis], dataFrame[fieldBIAS_DE_NDE],
+                 label=fieldBIAS_DE_NDE, color='black', linestyle='--')
+
+        # 设置y2轴的上限和下限
+        # ax2.set_ylim(-5, 5)
+        plt.axhline(y=5, ls=":", c="red")  # 添加水平直线
+        plt.axhline(y=-5, ls=":", c="red")  # 添加水平直线
+
+        # 第一个图例放在右上角
+        ax1.legend(title='Temperature & BIAS', bbox_to_anchor=(
+            1.3, 0.5), loc='center', borderaxespad=0.)
+
+        # # 第二个图例放在第一个图例稍下的位置
+        # ax1.legend(title='BIAS', bbox_to_anchor=(1.5, 1), loc='upper right', borderaxespad=0.)
+
+        # 设置x轴和y轴标签
+        ax1.set_xlabel("power")
+        ax1.set_ylabel('Bearing Temperature & BIAS')
+        # ax2.set_ylabel('Temperature BIAS')
+
+        ax1.xaxis.set_major_locator(ticker.MultipleLocator(confData.graphSets["activePower"]["step"] if not self.common.isNone(
+            confData.graphSets["activePower"]) and not self.common.isNone(
+            confData.graphSets["activePower"]["step"]) else 250))
+        ax1.set_xlim(confData.graphSets["activePower"]["min"] if not self.common.isNone(
+            confData.graphSets["activePower"]["min"]) else 0, confData.graphSets["activePower"]["max"] if not self.common.isNone(confData.graphSets["activePower"]["max"]) else confData.rated_power*1.2)
+
+        ax1.yaxis.set_major_locator(ticker.MultipleLocator(confData.graphSets["generatorTemperature"]["step"] if not self.common.isNone(
+            confData.graphSets["generatorTemperature"]) and not self.common.isNone(
+            confData.graphSets["generatorTemperature"]["step"]) else 10))
+        ax1.set_ylim(confData.graphSets["generatorTemperature"]["min"] if not self.common.isNone(
+            confData.graphSets["generatorTemperature"]["min"]) else -20, confData.graphSets["generatorTemperature"]["max"] if not self.common.isNone(confData.graphSets["generatorTemperature"]["max"]) else 100)
+
+        plt.title(f'Generator Temperture BIAS={turbineName}')
+
+        plt.savefig(os.path.join(outputDir, "{}.png".format(
+                    turbineName)), bbox_inches='tight', dpi=120)

+ 156 - 0
dataAnalysisBusiness/algorithm/tsrAnalyst.py

@@ -0,0 +1,156 @@
+import os
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import seaborn as sns
+from matplotlib.ticker import MultipleLocator
+from behavior.analystExcludeRatedPower import AnalystExcludeRatedPower
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class TSRAnalyst(AnalystExcludeRatedPower):
+    """
+    风电机组叶尖速比分析
+    """
+
+    def typeAnalyst(self):
+        return "tsr"
+
+    def turbineAnalysis(self,
+                        dataFrame,
+                        outputAnalysisDir,
+                        outputFilePath,
+                        confData: ConfBusiness,
+                        turbineName):
+
+        self.tsr(dataFrame, outputFilePath,
+                 confData.field_wind_speed, confData.field_rotor_speed, confData.field_power, confData.field_pitch_angle1, confData.rotor_diameter)
+
+    def tsr(self, dataFrame, output_path,  field_wind_speed, field_rotort_speed, field_power_active, field_angle_pitch, rotor_diameter):
+        # Calculate 'power_floor'
+        dataFrame['power_floor'] = (
+            dataFrame[field_power_active] / 10).astype(int) * 10
+
+        # Ensure the necessary columns are of float type
+        dataFrame['wind_speed'] = dataFrame[field_wind_speed].astype(float)
+        dataFrame['rotor_speed'] = dataFrame[field_rotort_speed].astype(float)
+        # rotor_diameter = pd.to_numeric(rotor_diameter, errors='coerce')
+        # # Calculate TSR
+        # dataFrame['tsr'] = (dataFrame['rotor_speed'] * 0.104667 *
+        #                     (rotor_diameter / 2)) / dataFrame['wind_speed']
+
+        # Group by 'power_floor' and calculate mean, max, and min of TSR
+        grouped = dataFrame.groupby('power_floor').agg({
+            'wind_speed': 'mean',
+            'rotor_speed': 'mean',
+            'tsr': ['mean', 'max', 'min']
+        }).reset_index()
+
+        # Rename columns for clarity post aggregation
+        grouped.columns = ['power_floor', 'wind_speed',
+                           'rotor_speed', 'tsr', 'tsr_max', 'tsr_min']
+
+        # Sort by 'power_floor'
+        grouped = grouped.sort_values('power_floor')
+
+        # Write the aggregated dataFrame to a new CSV file
+        grouped.to_csv(output_path, index=False)
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.plot_tsr_distribution(outputAnalysisDir, confData)
+
+    def plot_tsr_distribution(self, csvFileDirOfCp, confData: ConfBusiness, encoding='utf-8'):
+        """
+        Generates tsr distribution plots for turbines in a wind farm.
+
+        Parameters:
+        - csvFileDirOfCp: str, path to the directory containing input CSV files.
+        - farm_name: str, name of the wind farm.
+        - encoding: str, encoding of the input CSV files. Defaults to 'utf-8'.
+        """
+        field_Name_Turbine = "turbine_name"
+        x_name = 'power_floor'
+        y_name = 'tsr'
+
+        upLimitOfPower = confData.rated_power*0.9
+        upLimitOfTSR = 20
+
+        # 设置绘图样式
+        sns.set_palette('deep')
+
+        # 初始化结果DataFrame
+        res = pd.DataFrame()
+
+        # 遍历输入目录中的所有文件
+        for root, dir_names, file_names in dir.list_directory(csvFileDirOfCp):
+            for file_name in file_names:
+
+                if not file_name.endswith(CSVSuffix):
+                    continue
+
+                file_path = os.path.join(root, file_name)
+
+                # 读取CSV文件
+                frame = pd.read_csv(file_path, encoding=encoding)
+                frame = frame[(frame[x_name] > 0)]
+                # 提取设备名
+                turbine_name = file_name.split(CSVSuffix)[0]
+
+                # 添加设备名作为新列
+                frame[field_Name_Turbine] = turbine_name
+
+                # 选择需要的列并合并到结果DataFrame中
+                res = pd.concat(
+                    [res, frame.loc[:, [field_Name_Turbine, x_name, y_name]]], axis=0)
+
+        # 重置索引
+        ress = res.reset_index()
+
+        # 绘制全场TSR分布图
+        fig, ax = plt.subplots(figsize=(16, 8))
+        ax = sns.lineplot(x=x_name, y=y_name, data=ress,
+                          hue=field_Name_Turbine)
+
+        ax.xaxis.set_major_locator(MultipleLocator(200))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_xlim(0, upLimitOfPower)
+
+        ax.yaxis.set_major_locator(MultipleLocator(
+            confData.graphSets["tsr"]["step"] if not self.common.isNone(confData.graphSets["tsr"]["step"]) else 5))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_ylim(confData.graphSets["tsr"]["min"] if not self.common.isNone(confData.graphSets["tsr"]["min"]) else 0,
+                    confData.graphSets["tsr"]["max"] if not self.common.isNone(confData.graphSets["tsr"]["max"]) else upLimitOfTSR)
+
+        ax.set_title('TSR-Distibute')
+        # plt.legend(ncol=4)
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.legend(title='turbine', bbox_to_anchor=(1.02, 0.5),
+                   ncol=2, loc='center left', borderaxespad=0.)
+        plt.savefig(csvFileDirOfCp + r"/{}-TSR-Distibute.png".format(confData.farm_name),
+                    bbox_inches='tight', dpi=300)
+        plt.close(fig)
+
+        # 绘制每个设备的TSR分布图
+        grouped = ress.groupby(field_Name_Turbine)
+        for name, group in grouped:
+            color = ["lightgrey"] * len(ress[field_Name_Turbine].unique())
+            fig, ax = plt.subplots(figsize=(8, 8))
+            ax = sns.lineplot(x=x_name, y=y_name, data=ress, hue=field_Name_Turbine,
+                              palette=sns.set_palette(color), legend=False)
+            ax = sns.lineplot(x=x_name, y=y_name, data=group,
+                              color='darkblue', legend=False)
+
+            ax.xaxis.set_major_locator(
+                MultipleLocator(200))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_xlim(0, upLimitOfPower)
+
+            ax.yaxis.set_major_locator(MultipleLocator(
+                confData.graphSets["tsr"]["step"] if not self.common.isNone(confData.graphSets["tsr"]["step"]) else 5))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_ylim(confData.graphSets["tsr"]["min"] if not self.common.isNone(confData.graphSets["tsr"]["min"]) else 0,
+                        confData.graphSets["tsr"]["max"] if not self.common.isNone(confData.graphSets["tsr"]["max"]) else upLimitOfTSR)
+
+            ax.set_title('turbine name={}'.format(name))
+            plt.xticks(rotation=45)  # 旋转45度
+            plt.savefig(csvFileDirOfCp + r"/{}.png".format(name),
+                        bbox_inches='tight', dpi=120)
+            plt.close(fig)

+ 102 - 0
dataAnalysisBusiness/algorithm/tsrTrendAnalyst.py

@@ -0,0 +1,102 @@
+import os
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import seaborn as sns
+import plotly.graph_objects as go
+from behavior.analystExcludeRatedPower import AnalystExcludeRatedPower
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class TSRTrendAnalyst(AnalystExcludeRatedPower):
+    """
+    风电机组叶尖速比时序分析
+    """
+
+    def typeAnalyst(self):
+        return "tsr_trend"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.drawTSRTrend(dataFrameMerge, outputAnalysisDir, confData)
+
+    def drawTSRTrend(self,dataFrameMerge:pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):        
+        # 检查所需列是否存在
+        required_columns = {Field_TSR, Field_YearMonthDay}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+        
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            # 计算四分位数和IQR
+            Q1 = group[Field_TSR].quantile(0.15)
+            Q3 = group[Field_TSR].quantile(0.90)
+            IQR = Q3 - Q1
+            # 定义离群值的范围
+            lower_bound = Q1 - 1.5 * IQR
+            upper_bound = Q3 + 1.5 * IQR
+
+            # 筛选掉离群值
+            filtered_group = group[(group[Field_TSR] >= lower_bound) & (group[Field_TSR] <= upper_bound)]
+
+            # 创建箱线图
+            fig = go.Figure()
+
+            fig.add_trace(go.Box(
+                x=filtered_group[Field_YearMonthDay],  # 设置x轴数据为日期
+                y=filtered_group[Field_TSR],  # 设置y轴数据为风能利用系数
+                # boxpoints='outliers',  # 显示异常值(偏离值),不显示数据的所有点(只显示异常值)
+                boxpoints=False,  # 不显示偏离值
+                marker=dict(color='lightgoldenrodyellow', size=1),  # 设置偏离值的颜色和大小
+                line=dict(color='lightgray', width=2),  # 设置箱线和须线的颜色为灰色,粗细为2
+                fillcolor='rgba(200, 200, 200, 0.5)',  # 设置箱体的填充颜色和透明度
+                name='TSR'  # 图例名称
+            ))
+
+            # 对于每个箱线图的中位数,绘制一个蓝色点
+            medians = filtered_group.groupby(filtered_group[Field_YearMonthDay])[Field_TSR].median()
+            fig.add_trace(go.Scatter(
+                x=medians.index,
+                y=medians.values,
+                mode='markers',
+                marker=dict(color='orange', size=3),
+                name='Median TSR'  # 中位数标记的图例名称
+            ))
+
+            # 设置图表的标题和轴标签
+            fig.update_layout(
+                title={
+                    'text': f'TSR Trend Turbine Name {name}',
+                    'x':0.5,
+                },
+                xaxis_title='time',
+                yaxis_title='TSR',                
+                xaxis=dict(
+                    tickmode='auto',  # 自动设置x轴刻度,以适应日期数据
+                    tickformat='%Y-%m-%d',  # 设置x轴时间格式
+                    showgrid=True,  # 显示网格线
+                    gridcolor='lightgray',  # setting y-axis gridline color to black
+                    tickangle=-45,
+                    linecolor='black',  # 设置y轴坐标系线颜色为黑色
+                    ticklen=5,  # 设置刻度线的长度
+                ),
+                yaxis=dict(
+                    dtick=confData.graphSets["tsr"]["step"] if not self.common.isNone(
+                        confData.graphSets["tsr"]["step"]) else 5,  # 设置y轴刻度间隔为0.1
+                    range=[confData.graphSets["tsr"]["min"] if not self.common.isNone(
+                        confData.graphSets["tsr"]["min"]) else 0, confData.graphSets["tsr"]["max"] if not self.common.isNone(confData.graphSets["tsr"]["max"]) else 20],  # 设置y轴的范围从0到1
+                    showgrid=True,  # 显示网格线
+                    gridcolor='lightgray',  # setting y-axis gridline color to black
+                    linecolor='black',  # 设置y轴坐标系线颜色为黑色
+                    ticklen=5,  # 设置刻度线的长度
+                ),  
+                paper_bgcolor='white',  # 设置纸张背景颜色为白色  
+                plot_bgcolor='white',  # 设置图表背景颜色为白色  
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+            
+            # 保存图像
+            output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            fig.write_image(output_file, scale=2)

+ 152 - 0
dataAnalysisBusiness/algorithm/tsrWindSpeedAnalyst.py

@@ -0,0 +1,152 @@
+import os
+import pandas as pd
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+from matplotlib.ticker import MultipleLocator
+import seaborn as sns
+from behavior.analystExcludeRatedPower import AnalystExcludeRatedPower
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class TSRWindSpeedAnalyst(AnalystExcludeRatedPower):
+    """
+    风电机组叶尖速比分析
+    """
+
+    def typeAnalyst(self):
+        return "tsr_windspeed"
+
+    def turbineAnalysis(self,
+                        dataFrame,
+                        outputAnalysisDir,
+                        outputFilePath,
+                        confData: ConfBusiness,
+                        turbineName):
+
+        self.tsr(dataFrame, outputFilePath,
+                 confData.field_wind_speed, confData.field_rotor_speed, confData.field_power, confData.field_pitch_angle1, confData.rotor_diameter)
+
+    def tsr(self, dataFrame, output_path,  field_wind_speed, field_rotort_speed, field_power_active, field_angle_pitch, rotor_diameter):
+        dataFrame['power'] = dataFrame[field_power_active]  # Alias the power column
+        # Calculate 'wind_speed_floor'
+        dataFrame['wind_speed_floor'] = (dataFrame[field_wind_speed] / 1).astype(int) + 0.5
+
+        # Ensure the necessary columns are of float type
+        dataFrame['wind_speed'] = dataFrame[field_wind_speed].astype(float)
+        dataFrame['rotor_speed'] = dataFrame[field_rotort_speed].astype(float)
+        # rotor_diameter = pd.to_numeric(rotor_diameter, errors='coerce')
+        # # Calculate TSR
+        # dataFrame['tsr'] = (dataFrame['rotor_speed'] * 0.104667 *
+        #                     (rotor_diameter / 2)) / dataFrame['wind_speed']
+
+        # Group by 'wind_speed_floor' and calculate mean, max, and min of TSR
+        grouped = dataFrame.groupby('wind_speed_floor').agg({
+            'power': 'mean',
+            'rotor_speed': 'mean',
+            'tsr': ['mean', 'max', 'min']
+        }).reset_index()
+
+        # Rename columns for clarity post aggregation
+        grouped.columns = ['wind_speed_floor', 'power',
+                           'rotor_speed', 'tsr', 'tsr_max', 'tsr_min']
+
+        # Sort by 'wind_speed_floor'
+        grouped = grouped.sort_values('wind_speed_floor')
+
+        # Write the aggregated dataFrame to a new CSV file
+        grouped.to_csv(output_path, index=False)
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.plot_tsr_distribution(outputAnalysisDir, confData)
+
+    def plot_tsr_distribution(self, csvFileDirOfCp, confData: ConfBusiness, encoding='utf-8'):
+        """
+        Generates tsr distribution plots for turbines in a wind farm.
+
+        Parameters:
+        - csvFileDirOfCp: str, path to the directory containing input CSV files.
+        - farm_name: str, name of the wind farm.
+        - encoding: str, encoding of the input CSV files. Defaults to 'utf-8'.
+        """
+        field_Name_Turbine = "turbine_name"
+        x_name = 'wind_speed_floor'
+        y_name = 'tsr'
+
+        upLimitOfTSR=20
+
+        # 设置绘图样式
+        sns.set_palette('deep')
+
+        # 初始化结果DataFrame
+        res = pd.DataFrame()
+
+        # 遍历输入目录中的所有文件
+        for root, dir_names, file_names in dir.list_directory(csvFileDirOfCp):
+            for file_name in file_names:
+
+                if not file_name.endswith(CSVSuffix):
+                    continue
+
+                file_path = os.path.join(root, file_name)
+
+                # 读取CSV文件
+                frame = pd.read_csv(file_path, encoding=encoding)
+
+                # 提取设备名
+                turbine_name = file_name.split(CSVSuffix)[0]
+
+                # 添加设备名作为新列
+                frame[field_Name_Turbine] = turbine_name
+
+                # 选择需要的列并合并到结果DataFrame中
+                res = pd.concat(
+                    [res, frame.loc[:, [field_Name_Turbine, x_name, y_name]]], axis=0)
+
+        # 重置索引
+        ress = res.reset_index()
+
+        # 绘制全场TSR分布图
+        fig, ax = plt.subplots(figsize=(16, 8))
+        ax = sns.lineplot(x=x_name, y=y_name, data=ress,
+                          hue=field_Name_Turbine)
+        
+        ax.xaxis.set_major_locator(MultipleLocator(1)) # 创建一个刻度 ,将定位器应用到y轴上 
+        ax.set_xlim(0,26)
+
+        ax.yaxis.set_major_locator(MultipleLocator(
+            confData.graphSets["tsr"]["step"] if not self.common.isNone(confData.graphSets["tsr"]["step"]) else 5))  # 创建一个刻度 ,将定位器应用到y轴上
+        ax.set_ylim(confData.graphSets["tsr"]["min"] if not self.common.isNone(confData.graphSets["tsr"]["min"]) else 0,
+                    confData.graphSets["tsr"]["max"] if not self.common.isNone(confData.graphSets["tsr"]["max"]) else upLimitOfTSR)
+        
+        ax.set_title('TSR-Distibute')
+        plt.legend(ncol=4)
+        plt.xticks(rotation=45)  # 旋转45度
+        plt.savefig(csvFileDirOfCp + r"/{}-TSR-Distibute.png".format(confData.farm_name),
+                    bbox_inches='tight', dpi=300)
+        plt.close(fig)
+
+        # 绘制每个设备的TSR分布图
+        grouped = ress.groupby(field_Name_Turbine)
+        for name, group in grouped:
+            color = ["lightgrey"] * len(ress[field_Name_Turbine].unique())
+            fig, ax = plt.subplots(figsize=(8, 8))
+            ax = sns.lineplot(x=x_name, y=y_name, data=ress, hue=field_Name_Turbine,
+                              palette=sns.set_palette(color), legend=False)
+            ax = sns.lineplot(x=x_name, y=y_name, data=group,
+                              color='darkblue', legend=False)
+            
+            ax.xaxis.set_major_locator(MultipleLocator(1)) # 创建一个刻度 ,将定位器应用到y轴上 
+            ax.set_xlim(0,26)
+            
+            ax.yaxis.set_major_locator(MultipleLocator(
+            confData.graphSets["tsr"]["step"] if not self.common.isNone(confData.graphSets["tsr"]["step"]) else 2))  # 创建一个刻度 ,将定位器应用到y轴上
+            ax.set_ylim(confData.graphSets["tsr"]["min"] if not self.common.isNone(confData.graphSets["tsr"]["min"]) else 0,
+                        confData.graphSets["tsr"]["max"] if not self.common.isNone(confData.graphSets["tsr"]["max"]) else upLimitOfTSR)
+
+            ax.set_title('turbine name={}'.format(name))
+            plt.xticks(rotation=45)  # 旋转45度
+            plt.savefig(csvFileDirOfCp + r"/{}.png".format(name),
+                        bbox_inches='tight', dpi=120)
+            plt.close(fig)

+ 101 - 0
dataAnalysisBusiness/algorithm/windDirectionFrequencyAnalyst.py

@@ -0,0 +1,101 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+import matplotlib.pyplot as plt
+from algorithmContract.confBusiness import *
+
+
+class WindDirectionFrequencyAnalyst(Analyst):
+
+    def typeAnalyst(self):
+        return "wind_direction_frequency"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.windRoseAnalysis(dataFrameMerge, outputAnalysisDir, confData)
+
+    def windRoseAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 检查所需列是否存在
+        required_columns = {confData.field_wind_dir, confData.field_wind_speed}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+
+        # 风速区间
+        bins = [0, 3, 6, 9, np.inf]
+        speed_labels = ['[0,3)', '[3,6)', '[6,9)', '>=9']
+        wind_directions = np.arange(0, 360, 22.5)
+
+        colorscale = {
+            '[0,3)': 'rgba(247.0, 251.0, 255.0, 1.0)',
+            '[3,6)': 'rgba(171.33333333333334, 207.66666666666666, 229.66666666666669, 1.0)',
+            '[6,9)': 'rgba(55.0, 135.0, 192.33333333333334, 1.0)',
+            '>=9': 'rgba(8.0, 48.0, 107.0, 1.0)'
+        }
+
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            speed_bins = pd.cut(
+                group[confData.field_wind_speed], bins=bins, labels=speed_labels)
+            # 调整风向数据以使东方为0度
+            # adjusted_wind_dir = (group[confData.field_wind_dir] - 90) % 360
+            # group['风向分组'] = pd.cut(adjusted_wind_dir, bins=wind_directions, labels=wind_directions[:-1])
+            group['风向分组'] = pd.cut(
+                group[confData.field_wind_dir], bins=wind_directions, labels=wind_directions[:-1])
+
+            # 初始化子图,设定为极坐标
+            fig = make_subplots(rows=1, cols=1, specs=[[{'type': 'polar'}]])
+
+            for label in speed_labels:
+                subset = group[speed_bins == label]
+                counts = subset['风向分组'].value_counts().reindex(
+                    wind_directions[:-1], fill_value=0)
+                # 转换为百分比
+                percentage = (counts / counts.sum()) * 100
+
+                # 创建 Barpolar 跟踪,并应用单色渐变
+                trace = go.Barpolar(
+                    r=percentage.values,
+                    theta=counts.index,  # 这里的角度已经适配上北下南左西右东的布局
+                    name=label,
+                    marker_color=colorscale[label],  # 应用颜色尺度
+                    marker_showscale=False,  # 不显示颜色条
+                    marker_line_color='white',  # 设置线条颜色,增加扇区之间的分隔
+                    marker_line_width=1  # 设置线条宽度
+                )
+
+                fig.add_trace(trace)
+
+            # 设置图表的一些基本属性
+            fig.update_layout(
+                title={
+                    "text": f"Wind Rose {name}",
+                    "x": 0.5
+                },
+                polar=dict(
+                    radialaxis=dict(visible=True),
+                    angularaxis=dict(
+                        tickmode="array",
+                        tickvals=wind_directions,
+                        # 明确标注北、东、南、西等方向,以适应以北为0度的布局
+                        ticktext=['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE',
+                                  'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
+
+                        # 更新角度标签,以适应以东为0度的布局
+                        # ticktext=['E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N', 'NNE', 'NE', 'ENE']
+                    )
+                ),
+                legend_title="Wind Speed",
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+
+            # # 保存html
+            # outputFileHtml = os.path.join(outputAnalysisDir, f"{name}.html")
+            # fig.write_html(outputFileHtml)
+            # 保存图像
+            output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            fig.write_image(output_file, scale=2)

+ 81 - 0
dataAnalysisBusiness/algorithm/windRoseOfTurbine.py

@@ -0,0 +1,81 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import seaborn as sns
+import matplotlib.pyplot as plt
+from matplotlib.ticker import MultipleLocator
+from windrose import WindroseAxes
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+from matplotlib.cm import get_cmap 
+from matplotlib.colors import ListedColormap   
+
+
+class WinRoseOfTurbineAnalyst(Analyst):
+    """
+    风电机组变桨-功率分析
+    """
+
+    def typeAnalyst(self):
+        return "wind_rose_turbine"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.windRoseAnalysis(dataFrameMerge, outputAnalysisDir, confData)
+
+    def windRoseAnalysis(self, dataFrameMerge:pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 检查所需列是否存在
+        required_columns = {confData.field_wind_dir,confData.field_wind_speed}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+        
+        # 风速区间  
+        bins = [0, 3, 6, 9, np.inf]  
+        speed_labels = ['[0,3)', '[3,6)', '[6,9)', '>=9']  
+        # 准备颜色映射  
+        colors = plt.cm.Blues(np.linspace(0, 1, len(speed_labels)))  
+        cmap = ListedColormap(colors)  
+        # 将风向按照22.5度一个间隔进行分组  
+        wind_directions = np.arange(0, 360, 22.5)  
+
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+        print("self.ratedPower {}".format(confData.rated_power))
+        # 遍历每个设备并绘制图
+        for name, group in grouped:            
+            # 对风速进行分箱处理,但不添加到DataFrame中  
+            speed_bins = pd.cut(group[confData.field_wind_speed], bins=bins, labels=speed_labels)  
+            
+            # 将风向按照22.5度一个间隔进行分组  
+            wind_directions = np.arange(0, 360, 22.5)  
+            group['风向分组'] = pd.cut(group[confData.field_wind_dir], bins=wind_directions, labels=wind_directions[:-1])  
+            
+            # 绘制风玫瑰图  
+            fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={'polar': True})  
+            
+            # 为每个风速区间绘制风向的条形图,并添加图例  
+            for i, (label, color) in enumerate(zip(speed_labels, colors)):  
+                # 筛选出当前风速区间的数据  
+                subset = group[speed_bins == label]  
+                # 计算每个风向分组的频数  
+                counts = subset['风向分组'].value_counts().reindex(wind_directions[:-1], fill_value=0)  
+                # 绘制条形图,并添加标签用于图例  
+                bar = ax.bar(counts.index * np.pi / 180, counts.values, color=cmap(i), alpha=0.75, width=(22.5 * np.pi / 180), label=label)  
+            
+            # 设置标题和标签  
+            ax.set_title(f"Wind Rose {name}", va='top')  
+            ax.set_theta_zero_location('N')  # 设置0度位置为北  
+            ax.set_theta_direction(-1)  # 设置角度方向为顺时针  
+            ax.set_yticklabels([])  # 不显示y轴刻度标签  
+            ax.set_xticks(wind_directions * np.pi / 180)  # 设置x轴刻度  
+            ax.set_xticklabels(['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'])  # 设置x轴刻度标签为方向  
+            
+            # 添加图例,并设置位置以避免与图形重叠  
+            ax.legend(title='Wind Speed', bbox_to_anchor=(1.1, 1), loc='center left', borderaxespad=0.)   
+
+            # 保存图像并关闭绘图窗口
+            output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            plt.savefig(output_file, bbox_inches='tight', dpi=120)
+            plt.close()

+ 44 - 0
dataAnalysisBusiness/algorithm/windSpeedAnalyst.py

@@ -0,0 +1,44 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+import plotly.express as px  
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+class WindSpeedAnalyst(Analyst):
+
+    def typeAnalyst(self):
+        return "wind_speed"
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.drawWindSpeedAnalysis(dataFrameMerge, outputAnalysisDir, confData)
+
+    def drawWindSpeedAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 检查所需列是否存在
+        required_columns = {Field_NameOfTurbine,confData.field_wind_speed}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+        
+        # 确保'风速'列是数值类型  
+        dataFrameMerge[confData.field_wind_speed] = pd.to_numeric(dataFrameMerge[confData.field_wind_speed], errors='coerce')  
+        
+        # 计算每个turbine_name的平均风速  
+        average_wind_speed = dataFrameMerge.groupby(Field_NameOfTurbine)[confData.field_wind_speed].mean().reset_index()  
+        
+        # 使用plotly绘制柱状图  
+        fig = px.bar(average_wind_speed, x=Field_NameOfTurbine, y=confData.field_wind_speed, title='Turbine Average Wind Speed')  
+        
+        # 更新x轴和y轴的标签  
+        fig.update_xaxes(title_text='Turbine Name', tickangle=-45)  
+        fig.update_yaxes(title_text='Average Wind Speed (m/s)')  
+        # 如果需要进一步调整标题样式或位置(尽管默认情况下标题是居中的)  
+        # 可以使用 update_layout 来设置标题的 x 坐标(xanchor)为 'center' 来确保居中  
+        fig.update_layout(title_x=0.5)  # 设置标题的x位置为图的中心  
+        
+        # 保存图像
+        output_file = os.path.join(outputAnalysisDir, f"WindSpeedAvg_Turbines.png")
+        fig.write_image(output_file)
+

+ 83 - 0
dataAnalysisBusiness/algorithm/windSpeedFrequencyAnalyst.py

@@ -0,0 +1,83 @@
+import os
+import pandas as pd
+import numpy as np
+import plotly.graph_objects as go
+import plotly.express as px
+from plotly.subplots import make_subplots
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+import matplotlib.pyplot as plt
+from algorithmContract.confBusiness import *
+
+
+class WindSpeedFrequencyAnalyst(Analyst):
+
+    def typeAnalyst(self):
+        return "wind_speed_frequency"
+    
+    def filterCommon(self,dataFrame:pd.DataFrame, confData:ConfBusiness):        
+        return dataFrame
+
+    def turbinesAnalysis(self, dataFrameMerge, outputAnalysisDir, confData: ConfBusiness):
+        self.windRoseAnalysis(dataFrameMerge, outputAnalysisDir, confData)
+
+    def windRoseAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 检查所需列是否存在
+        required_columns = {Field_NameOfTurbine, confData.field_wind_speed}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+
+        wind_speed_bins = np.arange(0, 26, 0.5)  # x轴风速范围 ,间隔0.5
+
+        # 按设备名分组数据
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            # 2. 计算风速频率
+            # 首先,我们需要确定风速的范围并计算每个风速的频数
+            wind_speeds = group[confData.field_wind_speed].unique()
+            # 计算风速频率,确保频率没有零值(用很小的数代替零)  
+            wind_speed_freq = np.histogram(wind_speeds, bins=wind_speed_bins)[0] / len(wind_speeds) * 100  
+
+            # 3. & 4. 确定y轴风速频率的范围和间隔(这里直接计算了频率,所以不需要手动设置间隔)
+            # 我们已经计算了风速频率,因此不需要再手动设置y轴的间隔和范围
+
+            # 5. 使用plotly绘制风速频率分布柱状图
+            # 为了使用plotly绘制柱状图,我们需要将风速范围的中点作为x轴的值
+            x_values = (wind_speed_bins[:-1] + wind_speed_bins[1:]) / 2
+
+            # 创建柱状图
+            fig = px.bar(x=x_values, y=wind_speed_freq)
+
+            # 更新图形的布局
+            fig.update_layout(
+                title={
+                    'text': f'Wind Speed Frequency {name}',
+                    # 'y': 0.95,
+                    'x': 0.5,
+                    'xanchor': 'center',
+                    'yanchor': 'top'
+                },
+                xaxis=dict(
+                    title='Wind Speed (m/s)',
+                    showgrid=True,
+                    range=[0, 26],
+                    dtick=1,
+                    tickangle=-45
+                ),
+                yaxis=dict(
+                    title='Frequency (%)',
+                    showgrid=True,
+                    # range=[0, 1],
+                ),
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+            # # 更新x轴和y轴的范围和标签
+            # fig.update_yaxes(range=[0, max(wind_speed_freq) * 1.1 if max(wind_speed_freq) > 0 else 0.2], title='Frequency')
+            
+            # # 保存html
+            outputFileHtml = os.path.join(outputAnalysisDir, f"{name}.html")
+            fig.write_html(outputFileHtml)
+            # 保存图像
+            # output_file = os.path.join(outputAnalysisDir, f"{name}.png")
+            # fig.write_image(output_file, scale=2)

+ 181 - 0
dataAnalysisBusiness/algorithm/yawErrorAnalyst.py

@@ -0,0 +1,181 @@
+import os
+import numpy as np
+from plotly.subplots import make_subplots
+import plotly.graph_objects as go
+from scipy.optimize import curve_fit
+import matplotlib.pyplot as plt
+import pandas as pd
+from behavior.analyst import Analyst
+from utils.directoryUtil import DirectoryUtil as dir
+from algorithmContract.confBusiness import *
+
+
+class YawErrorAnalyst(Analyst):
+    """
+    风电机组静态偏航误差分析
+    """
+    fieldWindDirFloor = 'wind_dir_floor'
+    fieldPower = 'power'
+    fieldPowerMean = 'mean_power'
+    fieldPowerMax = 'max_power'
+    fieldPowerMin = 'min_power'
+    fieldPowerMedian = 'median_power'
+    fieldPowerGT0p = 'power_gt_0'
+    fieldPowerGT80p = 'power_gt_80p'
+    fieldPowerRatio0p = 'ratio_0'
+    fieldPowerRatio80p = 'ratio_80p'
+    fieldSlop = 'slop'
+
+    def typeAnalyst(self):
+        return "yaw_error"
+
+    def filterCommon(self, dataFrame: pd.DataFrame, confData: ConfBusiness):
+        dataFrame = super().filterCommon(dataFrame, confData)
+
+        dataFrame = dataFrame[~((dataFrame[Field_AngleIncluded].abs() >= 90))]
+
+        return dataFrame
+
+    def calculateYawError(self, dataFrame: pd.DataFrame, fieldAngleInclude, fieldActivePower):
+        dataFrame = dataFrame.dropna(
+            subset=[Field_NameOfTurbine, fieldAngleInclude, fieldActivePower])
+        # Calculate floor values and other transformations
+        dataFrame[self.fieldWindDirFloor] = np.floor(
+            dataFrame[fieldAngleInclude]).astype(int)
+        dataFrame[self.fieldPower] = dataFrame[fieldActivePower].astype(float)
+
+        # Calculate aggregated metrics for power
+        grouped = dataFrame.groupby(self.fieldWindDirFloor).agg({
+            Field_NameOfTurbine: ['min'],
+            self.fieldPower: ['mean', 'max', 'min', 'median', lambda x: (
+                x > 0).sum(), lambda x: (x > x.median()).sum()]
+        }).reset_index()
+
+        # Rename columns for clarity
+        grouped.columns = [self.fieldWindDirFloor, Field_NameOfTurbine, self.fieldPowerMean, self.fieldPowerMax,
+                           self.fieldPowerMin, self.fieldPowerMedian, self.fieldPowerGT0p, self.fieldPowerGT80p]
+
+        # Calculate total sums for conditions
+        power_gt_0_sum = grouped[self.fieldPowerGT0p].sum()
+        power_gt_80p_sum = grouped[self.fieldPowerGT80p].sum()
+
+        # Calculate ratios
+        grouped[self.fieldPowerRatio0p] = grouped[self.fieldPowerGT0p] / \
+            power_gt_0_sum
+        grouped[self.fieldPowerRatio80p] = grouped[self.fieldPowerGT80p] / \
+            power_gt_80p_sum
+
+        # Filter out zero ratios and calculate slope
+        grouped = grouped[grouped[self.fieldPowerRatio0p] > 0]
+        grouped[self.fieldSlop] = grouped[self.fieldPowerRatio80p] / \
+            grouped[self.fieldPowerRatio0p]
+
+        # Sort by wind direction floor
+        grouped.sort_values(self.fieldWindDirFloor, inplace=True)
+
+        # # Write to CSV
+        # grouped.to_csv(output_path, index=False)
+
+        return grouped
+
+    def poly_func(self, x, a, b, c, d, e):
+        return a * x**4 + b * x ** 3 + c * x ** 2 + d * x + e
+
+    def turbinesAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        self.yawErrorAnalysis(dataFrameMerge, outputAnalysisDir, confData)
+
+    def yawErrorAnalysis(self, dataFrameMerge: pd.DataFrame, outputAnalysisDir, confData: ConfBusiness):
+        # 检查所需列是否存在
+        required_columns = {confData.field_power,confData.field_angle_included}
+        if not required_columns.issubset(dataFrameMerge.columns):
+            raise ValueError(f"DataFrame缺少必要的列。需要的列有: {required_columns}")
+        
+        results = []
+        grouped = dataFrameMerge.groupby(Field_NameOfTurbine)
+
+        for name, group in grouped:
+            df = self.calculateYawError(
+                group, confData.field_angle_included, confData.field_power)
+
+            df.dropna(inplace=True)
+            # drop extreme value, the 3 largest and 3 smallest
+
+            df_ = df[df[self.fieldPowerRatio80p] > 0.000]
+
+            xdata = df_[self.fieldWindDirFloor]
+            ydata = df_[self.fieldSlop]
+
+            # make ydata smooth
+            ydata = ydata.rolling(7).median()[6:]
+            xdata = xdata[3:-3]
+
+            if len(xdata) <= 0 and len(ydata) <= 0:
+                continue
+
+            # Curve fitting
+            popt, pcov = curve_fit(self.poly_func, xdata, ydata)
+            # popt contains the optimized parameters a, b, and c
+            # Generate fitted y-dataFrame using the optimized parameters
+            fitted_ydata = self.poly_func(xdata, *popt)
+
+            # get the max value of fitted_ydata and its index
+            max_pos = fitted_ydata.idxmax()
+
+            # Create a subplot with two rows
+            fig = make_subplots(rows=2, cols=1)
+
+            # First subplot
+            fig.add_trace(
+                go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldSlop], 
+                        mode='markers', name="energy gain", marker=dict(size=5)),
+                row=1, col=1
+            )
+            fig.add_trace(
+                go.Scatter(x=xdata, y=fitted_ydata, mode='lines', name="fit", line=dict(color='red')),
+                row=1, col=1
+            )
+            fig.add_trace(
+                go.Scatter(x=[df[self.fieldWindDirFloor][max_pos]], y=[fitted_ydata[max_pos]], 
+                        mode='markers', name="max pos:"+str(df[self.fieldWindDirFloor][max_pos]), 
+                        marker=dict(size=20)),
+                row=1, col=1
+            )
+
+            # Second subplot
+            fig.add_trace(
+                go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldPowerRatio0p], 
+                        mode='markers', name="base energy", marker=dict(size=5)),
+                row=2, col=1
+            )
+            fig.add_trace(
+                go.Scatter(x=df[self.fieldWindDirFloor], y=df[self.fieldPowerRatio80p], 
+                        mode='markers', name="slope energy", marker=dict(size=5)),
+                row=2, col=1
+            )
+
+            # Update layout
+            fig.update_layout(
+                title={
+                    "text": f"Yaw Error Analysis {name}",
+                    "x": 0.5
+                },
+                showlegend=True,
+                margin=dict(t=50, b=10)  # t为顶部(top)间距,b为底部(bottom)间距
+            )
+
+            # Save to file
+            fig.write_image(os.path.join(outputAnalysisDir, f"{name}.png"))
+
+            # calc squear error of fitted_ydata and ydata
+            print(name, "\t", df[self.fieldWindDirFloor][max_pos])
+
+            resultOfTurbine = [name, df[self.fieldWindDirFloor][max_pos]]
+            results.append(resultOfTurbine)
+
+        # 初始化一个空的DataFrame,指定列名
+        columns = [Field_NameOfTurbine, Field_YawError]
+        dataFrameResult = pd.DataFrame(results, columns=columns)
+
+        filePathOfYawError = os.path.join(
+            outputAnalysisDir, f"yaw_error_result{CSVSuffix}")
+        dataFrameResult.to_csv(filePathOfYawError, index=False)

+ 450 - 0
dataAnalysisBusiness/demo/SCADA_10min_category_0.py

@@ -0,0 +1,450 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Apr  8 15:01:43 2024
+
+@author: LDDN
+"""
+import math
+import pandas as pd  
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.pyplot import MultipleLocator#设定固定刻度
+
+
+scada_10min = pd.read_csv(r'E:\BaiduNetdiskDownload\test\min_scada_LuoTuoGou\72\82.csv',encoding="utf-8")  #.value是将单元格
+turbine_info = pd.read_csv(r'E:\BaiduNetdiskDownload\test\min_scada_LuoTuoGou\72\info.csv')  #.value是将单元格
+PRated = turbine_info.loc[:,["额定功率"]] #2000
+PRated = PRated.values
+VCutOut = turbine_info.loc[:,["切出风速"]]  #25
+VCutOut = VCutOut.values
+VCutIn = turbine_info.loc[:,["切入风速"]]  #3
+VCutIn = VCutIn.values
+VRated = turbine_info.loc[:,["额定风速"]] #10
+VRated = VRated.values
+
+time_stamp = scada_10min.loc[:,['时间']] #dataframe
+active_power = scada_10min.loc[:,['变频器电网侧有功功率']]
+wind_speed = scada_10min.loc[:,['风速']]
+LM = pd.concat([time_stamp,active_power,wind_speed],axis=1)  #dataframe
+
+
+Labeled_March809 = LM
+APower = Labeled_March809["变频器电网侧有功功率"]  #series读入有功功率
+WSpeed = Labeled_March809["风速"]  #读入风速
+maxP=np.max(APower)
+intervalP=25  #ceil(PRated*0.01)#功率分区间隔为额定功率的1%
+intervalwindspeed=0.25  #风速分区间隔0.25m/s
+
+#初始化
+PNum = 0  
+TopP = 0   
+# 根据条件计算PNum和TopP  
+if maxP >= PRated:  
+    PNum = math.floor(maxP / intervalP) + 1  
+    TopP = math.floor((maxP - PRated) / intervalP) + 1  
+else:  
+    PNum = math.floor(PRated / intervalP)  
+    TopP = 0   
+VNum = math.ceil(VCutOut / intervalwindspeed)  
+  
+SM1 = Labeled_March809.shape
+AA1 = SM1[0]  
+lab = [[0] for _ in range(AA1)]
+lab = pd.DataFrame(lab,columns=['lab'])
+Labeled_March809 = pd.concat([Labeled_March809,lab],axis=1)  #在tpv后加一列标签列
+Labeled_March809 = Labeled_March809.values
+SM = Labeled_March809.shape #(52561,4)
+AA = SM[0]  
+#存储功率大于0的运行数据
+#标识功率为0的点,标识-1
+DzMarch809_0 = np.zeros((AA, 3)) # 初始化数组来存储功率大于零的运行数据  
+nCounter1 = 0 
+Point_line = np.zeros(AA, dtype=int)  
+#考虑到很多功率小于10的数据存在,将<10的功率视为0
+for i in range(AA):
+    if (APower[i] > 10) & (WSpeed[i] > 0):
+        nCounter1 += 1   #共有nCounter1个功率大于0的正常数据
+        DzMarch809_0[nCounter1-1, 0] = WSpeed[i]  
+        DzMarch809_0[nCounter1-1, 1] = APower[i]  
+        Point_line[nCounter1-1] = i+1  # 记录nCounter1记下的数据在原始数据中的位置  
+    if APower[i] <= 10: 
+        Labeled_March809[i,SM[1]-1] = -1  # 功率为0标识为-1  array类型
+# 截取DzMarch809_0中实际存储的数据  其他全为0
+DzMarch809 = DzMarch809_0[:nCounter1, :]  
+#统计各网格落入的散点个数
+XBoxNumber = np.ones((PNum, VNum),dtype=int)  #(86 100)
+nWhichP = 0
+nWhichV = 0
+
+# 循环遍历DzMarch809中的有效数据  
+for i in range(nCounter1):  
+    
+    # 查找功率所在的区间  
+    for m in range(1, PNum + 1):  # 注意Python的range是左闭右开的,所以需要+1  
+        if (DzMarch809[i,1] > (m - 1) * intervalP) and (DzMarch809[i,1] <= m * intervalP):  
+            nWhichP = m  
+            break  
+      
+    # 查找风速所在的区间  
+    for n in range(1, VNum + 1):  # 同样需要+1  
+        if (DzMarch809[i, 0] > (n - 1)*intervalwindspeed) and (DzMarch809[i, 0] <= n*intervalwindspeed):  
+            nWhichV = n  
+            break  
+      
+    # 如果功率和风速都在有效区间内,增加对应网格的计数  
+    if (nWhichP > 0) and (nWhichV > 0):  
+        XBoxNumber[nWhichP - 1, nWhichV - 1] += 1  # 注意Python的索引是从0开始的,所以需要减1  
+# XBoxNumber现在包含了每个网格的计数[PNum行, VNum列]
+
+for m in range(1,PNum+1):
+    for n in range(1,VNum+1):
+        XBoxNumber[m-1,n-1] = XBoxNumber[m-1,n-1] - 1
+
+#在功率方向将网格内散点绝对个数转换为相对百分比,备用
+PBoxPercent = np.zeros((PNum, VNum),dtype = float)  #(86 100) #计算后会出现浮点型,所以不能定义int类型
+PBinSum = np.zeros((PNum,1),dtype=int)
+for i in range(1,PNum+1):
+    for m in range(1,VNum+1):
+        PBinSum[i-1] = PBinSum[i-1] + XBoxNumber[i-1,m-1] 
+    for m in range(1,VNum+1):
+        if PBinSum[i-1]>0:
+            PBoxPercent[i-1,m-1] = (XBoxNumber[i-1,m-1] / PBinSum[i-1])*100
+#在风速方向将网格内散点绝对个数转换为相对百分比,备用          
+VBoxPercent = np.zeros((PNum, VNum))  #(86 100) #计算后会出现浮点型,所以不能定义int类型
+VBinSum = np.zeros((VNum,1),dtype=int)
+for i in range(1,VNum+1):
+    for m in range(1,PNum+1):
+        VBinSum[i-1] = VBinSum[i-1] + XBoxNumber[m-1,i-1] 
+    for m in range(1,PNum+1):
+        if VBinSum[i-1]>0:
+            VBoxPercent[m-1,i-1] = (XBoxNumber[m-1,i-1] / VBinSum[i-1])*100
+# VBoxPercent PBoxPercent 左上-右下
+# 将数据颠倒一下  左下-右上         第一行换为倒数第一行 方便可视化
+InvXBoxNumber = np.zeros((PNum,VNum),dtype = int)
+InvPBoxPercent = np.zeros((PNum,VNum),dtype = float)
+InvVBoxPercent = np.zeros((PNum,VNum),dtype = float)
+for m in range(1,PNum+1):
+    for n in range(1,VNum+1):
+        InvXBoxNumber[m-1,n-1] = XBoxNumber[PNum-(m-1)-1,n-1]
+        InvPBoxPercent[m-1,n-1] = PBoxPercent[PNum-(m-1)-1,n-1]
+        InvVBoxPercent[m-1,n-1] = VBoxPercent[PNum-(m-1)-1,n-1]
+
+#以水平功率带方向为准,分析每个水平功率带中,功率主带中心,即找百分比最大的网格位置。
+PBoxMaxIndex = np.zeros((PNum,1),dtype = int)  #水平功率带最大网格位置索引
+PBoxMaxP = np.zeros((PNum,1),dtype = float)       #水平功率带最大网格百分比
+for m in range(1,PNum+1):
+    PBoxMaxIndex[m-1] = np.argmax(PBoxPercent[m-1, :])   #argmax返回最大值的索引
+    PBoxMaxP[m-1] = np.max(PBoxPercent[m-1, :])
+#以垂直风速方向为准,分析每个垂直风速带中,功率主带中心,即找百分比最大的网格位置。
+VBoxMaxIndex = np.zeros((VNum,1),dtype = int)  
+VBoxMaxV = np.zeros((VNum,1),dtype = float)       
+for m in range(1,VNum+1):
+    VBoxMaxIndex[m-1] = np.argmax(VBoxPercent[:, m-1])   
+    VBoxMaxV[m-1] = np.max(VBoxPercent[:, m-1])
+
+#切入风速特殊处理,如果切入风速过于偏右,向左拉回
+if PBoxMaxIndex[0]>14:                     #第一个值对应的是风速最小处 即切入风速
+    PBoxMaxIndex[0] = 9 
+#以水平功率带方向为基准,进行分析
+DotDense = np.zeros(PNum)   #每一水平功率带的功率主带包含的网格数
+DotDenseLeftRight = np.zeros((PNum,2))  #存储每一水平功率带的功率主带以最大网格为中心,向左,向右扩展的网格数
+DotValve = 90  #从中心向左右对称扩展网格的散点百分比和的阈值。
+PDotDenseSum = 0
+for i in range(PNum - TopP):  # 从最下层水平功率带开始,向上分析到特定的功率带  
+    PDotDenseSum = PBoxMaxP[i]  # 以中心最大水平功率带为基准,向左向右对称扩展网格,累加各网格散点百分比  
+    iSpreadRight = 1  
+    iSpreadLeft = 1  
+      
+    while PDotDenseSum < DotValve:  
+        if (PBoxMaxIndex[i] + iSpreadRight) < VNum-1-1:  
+            PDotDenseSum += PBoxPercent[i, PBoxMaxIndex[i] + iSpreadRight]  # 向右侧扩展  
+            iSpreadRight += 1  
+        else:
+            break  
+          
+        if (PBoxMaxIndex[i] - iSpreadLeft) > 0:  
+            PDotDenseSum += PBoxPercent[i, PBoxMaxIndex[i] - iSpreadLeft]  # 向左侧扩展  
+            iSpreadLeft += 1  
+        else:  
+            break  
+    iSpreadRight = iSpreadRight-1
+    iSpreadLeft = iSpreadLeft-1
+    #向左右扩展完毕
+    DotDenseLeftRight[i, 0] = iSpreadLeft  # 左  
+    DotDenseLeftRight[i, 1] = iSpreadRight  # 右  
+    DotDense[i] = iSpreadLeft + iSpreadRight + 1  # 记录向左向右扩展的个数及每个功率仓内网格的个数  
+# 此时DotDense和DotDenseLeftRight数组已经包含了所需信息    
+#各行功率主带右侧宽度的中位数最具有代表性(因为先右后左)
+DotDenseWidthLeft = np.zeros((PNum-TopP))
+for i in range(PNum-TopP):
+    DotDenseWidthLeft[i] = DotDenseLeftRight[i,1]  #DotDenseLeftRight[i,1]:向右延伸个数
+MainBandRight = np.median(DotDenseWidthLeft) #计算中位数
+
+# 初始化变量  
+PowerLimit = np.zeros(PNum, dtype=int)  # 各水平功率带是否为限功率标识,1:是;0:不是  
+WidthAverage = 0  # 功率主带右侧平均宽度  
+WidthAverage_L = 0  # 功率主带左侧平均宽度  
+WidthVar = 0  # 功率主带方差(此变量在提供的代码中并未使用)  
+PowerLimitValve = 6  # 限功率主带判别阈值  
+N_Pcount = 20  # 阈值  
+  
+nCounterLimit = 0  # 限功率的个数  
+nCounter = 0  # 正常水平功率带的个数  
+  
+# 循环遍历水平功率带,从第1个到第PNum-TopP个  
+for i in range(PNum - TopP):  
+    # 如果向右扩展网格数大于阈值,且该水平功率带点总数大于20,则标记为限功率带  
+    if (DotDenseLeftRight[i, 1] > PowerLimitValve) and (PBinSum[i] > N_Pcount):  
+        PowerLimit[i] = 1  
+        nCounterLimit += 1  #限功率的个数
+      
+    # 如果向右扩展网格数小于等于阈值,则累加右侧宽度(左侧宽度在代码中似乎有误)  
+    if DotDenseLeftRight[i, 1] <= PowerLimitValve:  
+        WidthAverage += DotDenseLeftRight[i, 1]  # 统计正常水平功率带右侧宽度
+        WidthAverage_L += DotDenseLeftRight[i,1]   #统计正常水平功率带左侧宽度
+        nCounter += 1  
+# 计算平均宽度  
+WidthAverage /= nCounter if nCounter > 0 else 1  # 避免除以0的情况  
+WidthAverage_L /= nCounter if nCounter > 0 else 1   
+
+#计算正常(即非限功率)水平功率带的功率主带宽度的方差,以此来反映从下到上宽度是否一致
+WidthVar = 0  # 功率主带宽度的方差   
+for i in range(PNum - TopP):  
+    # 如果向右扩展网格数小于等于阈值,则计算当前宽度与平均宽度的差值平方  
+    if DotDenseLeftRight[i, 1] <= PowerLimitValve:  
+        WidthVar += (DotDenseLeftRight[i, 1] - WidthAverage) ** 2  
+# 计算方差(注意:除以nCounter-1是为了得到样本方差)  
+WidthVar = np.sqrt(WidthVar / (nCounter - 1) if nCounter > 1 else 0)  # 避免除以0的情况
+
+#各水平功率带,功率主带的风速范围,右侧扩展网格数*2*0.25
+PowerBandWidth = WidthAverage*intervalwindspeed+WidthAverage_L*intervalwindspeed
+
+# 对限负荷水平功率带的最大网格进行修正  
+for i in range(1, PNum - TopP+1):  
+    if (PowerLimit[i] == 1) and (abs(PBoxMaxIndex[i] - PBoxMaxIndex[i - 1]) > 5):  
+        PBoxMaxIndex[i] = PBoxMaxIndex[i - 1] + 1  
+  
+# 输出各层功率主带的左右边界网格索引  
+DotDenseInverse = np.flipud(DotDenseLeftRight)  # 上下翻转数组以得到反向顺序  
+  
+# 计算功率主带的左右边界  
+CurveWidthR = np.ceil(WidthAverage) + 2  # 功率主带的右边界 + 2  
+CurveWidthL = np.ceil(WidthAverage_L) + 2  # 功率主带的左边界 + 2  
+  
+# 网格是否为限功率网格的标识数组  
+BBoxLimit = np.zeros((PNum, VNum), dtype=int)  
+# 标记限功率网格  
+for i in range(2, PNum - TopP):  
+    if PowerLimit[i] == 1:
+        BBoxLimit[i, int(PBoxMaxIndex[i] + CurveWidthR + 1):VNum] = 1
+
+# 初始化数据异常需要剔除的网格标识数组  
+BBoxRemove = np.zeros((PNum, VNum), dtype=int)  
+# 标记需要剔除的网格  
+for m in range(PNum - TopP): 
+    for n in range(int(PBoxMaxIndex[m]) + int(CurveWidthR), VNum):  # 注意Python中的索引从0开始,因此需要减去1  
+        BBoxRemove[m, n] = 1 
+    # 功率主带左侧的超发网格,从最大索引向左直到第一个网格  
+    
+    for n in range(int(PBoxMaxIndex[m]) - int(CurveWidthL)+1, 0, -1):  # 使用range的步长参数来实现从右向左的迭代  
+        BBoxRemove[m, n-1] = 2  # 注意Python中的索引从0开始,因此需要减去1
+
+# 初始化变量  
+CurveTop = np.zeros((2, 1), dtype=int)  
+CurveTopValve = 1  # 网格的百分比阈值  
+BTopFind = 0  
+mm = 0  
+#确定功率主带的左上拐点,即额定风速位置的网格索引
+CurveTop = np.zeros((2, 1), dtype=int)  
+CurveTopValve = 1  # 网格的百分比阈值  
+BTopFind = 0  
+mm = 0   
+for m in range(PNum - TopP, 0, -1):  # 注意Python的range是左闭右开区间,所以这里从PNum-TopP开始到1(不包括0)  
+    for n in range(int(np.floor(int(VCutIn) / intervalwindspeed)), VNum - 1):  # 使用floor函数来向下取整  
+        if (VBoxPercent[m, n - 1] < VBoxPercent[m, n]) and (VBoxPercent[m, n] <= VBoxPercent[m, n + 1]) and (XBoxNumber[m, n] >= 3):   
+            CurveTop[0] = m  
+            CurveTop[1] = n  #[第80个,第40个]
+            BTopFind = 1  
+            mm = m  # mm是拐点所在功率仓,对应其index
+            break  # 找到后退出内层循环  
+    if BTopFind == 1:  
+        break  # 找到后退出外层循环
+        
+IsolateValve = 3  #功率主带右侧孤立点占比功率仓阈值 3%
+# 遍历功率仓和网格  
+for m in range(PNum - TopP):    
+    for n in range(int(PBoxMaxIndex[m]) + int(CurveWidthR), VNum):  
+        # 检查PBoxPercent是否小于阈值,如果是,则标记BBoxRemove为1  
+        if PBoxPercent[m, n] < IsolateValve:   
+            BBoxRemove[m, n] = 1
+#功率主带顶部宽度
+CurveWidthT = np.floor((maxP - PRated) / intervalP) + 1  
+# 标记额定功率以上的超发点(PNum-PTop之间)  
+for m in range(PNum - TopP, PNum):   
+    for n in range(VNum):  
+        BBoxRemove[m, n] = 3
+  
+# 标记功率主带拐点左侧的欠发网格  
+for m in range(mm-1, PNum - TopP): 
+    for n in range(int(CurveTop[1]) - 2):
+        BBoxRemove[m, n] = 2    # BBoxRemove数组现在包含了根据条件标记的超发点和欠发网格的信息
+
+#以网格的标识,决定该网格内数据的标识。
+# DzMarch809Sel数组现在包含了每个数据点的标识
+DzMarch809Sel = np.zeros(nCounter1, dtype=int)  # 初始化标识数组   
+nWhichP = 0  
+nWhichV = 0  
+nBadA = 0   
+for i in range(nCounter1):  
+    for m in range( PNum ):   
+        if (DzMarch809[i, 1] > m * intervalP) and (DzMarch809[i, 1] <= (m+1) * intervalP):  
+            nWhichP = m  #m记录的是index
+            break  
+    for n in range( VNum ):  # 注意Python的range是左闭右开区间,所以这里到VNum+1  
+        if DzMarch809[i, 0] > ((n+1) * intervalwindspeed - intervalwindspeed/2) and DzMarch809[i, 0] <= ((n+1) * intervalwindspeed + intervalwindspeed / 2):  
+            nWhichV = n  #index
+            break  
+    if nWhichP >= 0 and nWhichV >= 0:  
+        if BBoxRemove[nWhichP, nWhichV] == 1:   
+            DzMarch809Sel[i] = 1  
+            nBadA += 1  
+        elif BBoxRemove[nWhichP, nWhichV] == 2:  
+            DzMarch809Sel[i] = 2  
+        elif BBoxRemove[nWhichP , nWhichV] == 3:  
+            DzMarch809Sel[i] = 0  # 额定风速以上的超发功率点认为是正常点,不再标识  
+# DzMarch809Sel数组现在包含了每个数据点的标识
+
+##############################滑动窗口方法
+# 存储限负荷数据  
+PVLimit = np.zeros((nCounter1, 3))  #存储限负荷数据  %第3列用于存储限电的点所在的行数
+nLimitTotal = 0  
+nWindowLength = 6   #滑动窗口长度设置为6
+LimitWindow = np.zeros(nWindowLength)  #滑动窗口空列表
+UpLimit = 0    #上限
+LowLimit = 0   #下限
+PowerStd = 30  # 功率波动方差  
+nWindowNum = np.floor(nCounter1/nWindowLength) #6587
+PowerLimitUp = PRated - 100  
+PowerLimitLow = 100  
+
+# 循环遍历每个窗口  
+for i in range(int(nWindowNum)):  
+    start_idx = i * nWindowLength  
+    end_idx = start_idx + nWindowLength  
+    LimitWindow = DzMarch809[start_idx:end_idx, 1]  # 提取当前窗口的数据  
+      
+    # 检查窗口内所有数据是否在功率范围内  
+    bAllInAreas = np.all(LimitWindow >= PowerLimitLow) and np.all(LimitWindow <= PowerLimitUp)  
+    if not bAllInAreas:  
+        continue  
+      
+    # 计算方差上下限  
+    UpLimit = LimitWindow[0] + PowerStd  
+    LowLimit = LimitWindow[0] - PowerStd  
+      
+    # 检查窗口内数据是否在方差范围内  
+    bAllInUpLow = np.all(LimitWindow >= LowLimit) and np.all(LimitWindow <= UpLimit)  
+    if bAllInUpLow:  
+        # 标识窗口内的数据为限负荷数据  
+        DzMarch809Sel[start_idx:end_idx] = 4  
+          
+        # 存储限负荷数据  
+        for j in range(nWindowLength):  
+            PVLimit[nLimitTotal, :2] = DzMarch809[start_idx + j, :2]  
+            PVLimit[nLimitTotal, 2] = Point_line[start_idx + j]  # 对数据进行标识  
+            nLimitTotal += 1  
+# PVLimit现在包含了限负荷数据,nLimitTotal是限负荷数据的总数
+
+
+#将功率滑动窗口主带平滑化
+# 初始化锯齿平滑的计数器  
+nSmooth = 0  
+# 遍历除了最后 TopP+1 个元素之外的所有 PBoxMaxIndex 元素  
+for i in range(PNum - TopP - 1):  
+    PVLeftDown = np.zeros(2)  
+    PVRightUp = np.zeros(2)  
+    # 检查当前与下一个 PBoxMaxIndex 之间的距离是否大于等于1  
+    if PBoxMaxIndex[i + 1] - PBoxMaxIndex[i] >= 1:  
+        # 计算左下和右上顶点的坐标  
+        PVLeftDown[0] = (PBoxMaxIndex[i]+1 + CurveWidthR) * 0.25 - 0.125  
+        PVLeftDown[1] = (i) * 25  
+        PVRightUp[0] = (PBoxMaxIndex[i+1]+1 + CurveWidthR) * 0.25 - 0.125  
+        PVRightUp[1] = (i+1) * 25  
+          
+        # 遍历 DzMarch809 数组  
+        for m in range(nCounter1):  
+            # 检查当前点是否在锯齿区域内  
+            if (DzMarch809[m, 0] > PVLeftDown[0]) and (DzMarch809[m, 0] < PVRightUp[0]) and (DzMarch809[m, 1] > PVLeftDown[1]) and (DzMarch809[m, 1] < PVRightUp[1]):
+                # 检查斜率是否大于对角连线  
+                if (DzMarch809[m, 1] - PVLeftDown[1]) / (DzMarch809[m, 0] - PVLeftDown[0]) > (PVRightUp[1] - PVLeftDown[1]) / (PVRightUp[0] - PVLeftDown[0]):
+                    # 如果在锯齿左上三角形中,则选中并增加锯齿平滑计数器  
+                    DzMarch809Sel[m] = 0  
+                    nSmooth += 1  
+# DzMarch809Sel 数组现在+包含了锯齿平滑的选择结果,nSmooth 是选中的点数
+
+
+###################################存储数据
+# 存储好点  
+nCounterPV = 0  # 初始化计数器  
+PVDot = np.zeros((nCounter1, 3))  # 初始化存储好点的数组  nCounter1是p>0的数
+for i in range(nCounter1):  
+    if DzMarch809Sel[i] == 0:  
+        nCounterPV += 1  
+        PVDot[nCounterPV-1, :2] = DzMarch809[i, :2]  
+        PVDot[nCounterPV-1, 2] = Point_line[i]  # 好点 Point_line记录nCounter1在原始数据中的位置 
+nCounterVP = nCounterPV  
+ 
+# 对所有数据中的好点进行标注    
+for i in range(nCounterVP):  
+    Labeled_March809[int(PVDot[i, 2] - 1), (SM[1]-1)] = 1  # 注意Python的索引从0开始,并且需要转换为整数索引  
+ 
+# 存储坏点  
+nCounterBad = 0  # 初始化计数器  
+PVBad = np.zeros((nCounter1, 3))  # 初始化存储坏点的数组  
+for i in range(nCounter1):  
+    if DzMarch809Sel[i] in [1, 2, 3]:  
+        nCounterBad += 1  
+        PVBad[nCounterBad-1, :2] = DzMarch809[i, :2]  
+        PVBad[nCounterBad-1, 2] = Point_line[i]  
+    
+# 对所有数据中的坏点进行标注  
+for i in range(nCounterBad):  
+    Labeled_March809[int(PVBad[i, 2] - 1),(SM[1]-1)] = 5  # 坏点标识  
+
+# 对所有数据中的限电点进行标注   
+for i in range(nLimitTotal):  
+    Labeled_March809[int(PVLimit[i, 2] - 1),(SM[1]-1)] = 4  # 限电点标识  
+# 对所有的数据点进行标注  
+# Labeled_March809是array,提取所第四列的值保存为dataframe
+A = Labeled_March809[:,3]
+A=pd.DataFrame(A,columns=['lab'])
+
+
+mergedTable = pd.concat([scada_10min,A],axis=1)#合并dataframe
+D = mergedTable[mergedTable['lab'] == 1]#选择为1的行
+
+ws = D["风速"].values  #array
+ap = D["变频器电网侧有功功率"]
+
+# fig=plt.figure(figsize=(10,6),dpi=500)  #figsize是图形大小,dpi像素
+fig=plt.figure()  #figsize是图形大小,dpi像素
+plt.scatter(ws,ap,s=1,c='black',marker='.') #'.'比'o'要更小
+
+# plt.scatter(x2,y2,s=10,c='b',marker='.',label='5.8-6.5建模噪声点')
+
+x_major_locator=MultipleLocator(5)
+y_major_locator=MultipleLocator(500)
+ax=plt.gca()
+ax.xaxis.set_major_locator(x_major_locator)
+ax.yaxis.set_major_locator(y_major_locator)
+plt.xlim((0,30))
+plt.ylim((0,2200))
+plt.tick_params(labelsize=8)
+
+# plt.grid(c='dimgray',alpha=0.2)
+
+plt.xlabel("V/(m$·$s$^{-1}$)",fontsize=8)
+plt.ylabel("P/kW",fontsize=8)
+
+# plt.savefig(r'D:\赵雅丽\研究生学习资料\学习资料\劣化度健康度\spyder\大论文\图\风速-功率.jpg',bbox_inches='tight')
+plt.show()

+ 507 - 0
dataAnalysisBusiness/demo/SCADA_10min_category_1.py

@@ -0,0 +1,507 @@
+import os
+import re
+import math
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.pyplot import MultipleLocator#设定固定刻度
+
+def scada_10min_category():
+    turbine_number=24
+    
+    fpath = 'D:/赵雅丽/实习/算法/min_scada_LuoTuoGou/72/'
+    # 定义一个正则表达式来匹配纯数字文件名且扩展名为.csv  
+    pattern = re.compile(r'^\d+\.csv$')
+    
+    # 列出指定路径下的所有文件和文件夹  
+    files_in_dir = os.listdir(fpath)
+    for file in files_in_dir:  
+        # 使用正则表达式匹配文件名  
+        if pattern.match(file):  
+            # 拼接文件的完整路径  
+            fname = os.path.join(fpath, file)
+            # 读取csv文件,保持原始变量名而不忽略任何行
+            scada_10min = pd.read_csv(fname)
+        
+            # 显示数据        
+            time_stamp = scada_10min.loc[:,['时间']] #dataframe
+            active_power = scada_10min.loc[:,['变频器电网侧有功功率']]
+            wind_speed = scada_10min.loc[:,['风速']]
+            LM = pd.concat([time_stamp,active_power,wind_speed],axis=1)  #dataframe
+            # lm=LM.values #array
+    
+            xx = data_label(LM,fpath)#dataframe
+            mergedTable = pd.concat([scada_10min,xx],axis=1)#合并dataframe
+            D = mergedTable[mergedTable['lab'] == 1]#选择为1的行
+            ws = D["风速"]#series
+            ap = D["变频器电网侧有功功率"]
+            ##绘图
+            # fig = plt.figure(figsize=(10,6),dpi=500)  #figsize是图形大小,dpi像素
+            plt.scatter(ws,ap,s=8,c='black',marker='.',label='好点')
+            # x_major_locator=MultipleLocator(5)
+            # y_major_locator=MultipleLocator(500)
+            # ax=plt.gca()
+            # ax.xaxis.set_major_locator(x_major_locator)
+            # ax.yaxis.set_major_locator(y_major_locator)
+            # plt.xlim((0,30))
+            # plt.ylim((0,2200))
+            # plt.tick_params(labelsize=20)
+            # # plt.grid(c='dimgray',alpha=0.2)
+            # plt.xlabel("V/(m$·$s$^{-1}$)",fontsize=20)
+            # plt.ylabel("P/kW",fontsize=20)
+
+            # # plt.savefig(r'D:\赵雅丽\研究生学习资料\学习资料\劣化度健康度\spyder\大论文\图\风速-功率.jpg',bbox_inches='tight')
+            # plt.show()
+        
+        
+        
+def data_label(x1,x2):   # LM:T P V  path:文件获取路径
+    fpath2 = x2
+    fname2 = os.path.join(fpath2, "info.csv") #读取数据文件2(额定风速额定功率等)
+    # 参数na_filter=False仅阻止了pandas自动检测这些缺失值,并不能忽略  
+    # 但请注意,pandas没有直接的'omitrow'选项,如果需要忽略包含缺失值的行,需要在后续处理中处理
+    turbine_info = pd.read_csv(fname2, na_filter=False)
+    # 删除包含任何缺失值的行  
+    turbine_info = turbine_info.dropna() 
+    
+    PRated = turbine_info.loc[:,["额定功率"]] #dataframe
+    VCutOut = turbine_info.loc[:,["切出风速"]]  
+    VCutIn = turbine_info.loc[:,["切入风速"]]  
+    VRated = turbine_info.loc[:,["额定风速"]]
+    
+    #网格法确定风速风向分区数量,功率方向分区数量
+    Labeled_March809 = x1
+    APower = Labeled_March809["active_power"]  #series读入有功功率
+    WSpeed = Labeled_March809["wind_speed"]  #读入风速
+    maxP=np.max(APower)
+    intervalP=25  #ceil(PRated*0.01)#功率分区间隔为额定功率的1%
+    intervalwindspeed=0.25  #风速分区间隔0.25m/s
+    #初始化
+    PNum = 0  
+    TopP = 0   
+    # 根据条件计算PNum和TopP  
+    if maxP >= PRated:  
+        PNum = math.floor(maxP / intervalP) + 1  
+        TopP = math.floor((maxP - PRated) / intervalP) + 1  
+    else:  
+        PNum = math.floor(PRated / intervalP)  
+        TopP = 0   
+    VNum = math.ceil(VCutOut / intervalwindspeed)  
+    SM1 = Labeled_March809.shape  
+    AA1 = SM1[0]  #运行数据的条数
+    lab = [[0] for _ in range(AA1)]  #创建全0空列表
+    lab = pd.DataFrame(lab,columns=['lab'])
+    Labeled_March809 = pd.concat([Labeled_March809,lab],axis=1)  #在tpv后加一列标签列
+    SM = Labeled_March809.shape #(52561,4)
+    AA = SM[0]  
+    #存储功率大于0的运行数据
+    #标识功率为0的点,标识-1
+    DzMarch809_0 = np.zeros(AA, 3)  #array(52561,3)
+    nCounter1 = 1
+    Point_line=np.zeros(AA,1)
+    #考虑到很多功率小于10的数据存在,将<10的功率视为0
+    for i in range(AA):
+        if (APower[i] > 10) & (WSpeed[i] > 0):
+            nCounter1 += 1   #共有nCounter1个功率大于0的正常数据
+            DzMarch809_0[nCounter1-1, 0] = WSpeed[i]  
+            DzMarch809_0[nCounter1-1, 1] = APower[i]  
+            Point_line[nCounter1-1] = i+1  # 记录nCounter1记下的数据在原始数据中的位置  
+        if APower[i] <= 10: 
+            Labeled_March809[i,SM[1]-1] = -1  # 功率为0标识为-1  array类型
+    # 截取DzMarch809_0中实际存储的数据  其他全为0
+    DzMarch809 = DzMarch809_0[:nCounter1, :]  
+    #统计各网格落入的散点个数
+    XBoxNumber = np.ones((PNum, VNum),dtype=int)  #(86 100)
+    nWhichP = 0
+    nWhichV = 0
+
+    # 循环遍历DzMarch809中的有效数据  
+    for i in range(nCounter1):  
+        
+        # 查找功率所在的区间  
+        for m in range(1, PNum + 1):  # 注意Python的range是左闭右开的,所以需要+1  
+            if (DzMarch809[i,1] > (m - 1) * intervalP) and (DzMarch809[i,1] <= m * intervalP):  
+                nWhichP = m  
+                break  
+          
+        # 查找风速所在的区间  
+        for n in range(1, VNum + 1):  # 同样需要+1  
+            if (DzMarch809[i, 0] > (n - 1)*intervalwindspeed) and (DzMarch809[i, 0] <= n*intervalwindspeed):  
+                nWhichV = n  
+                break  
+          
+        # 如果功率和风速都在有效区间内,增加对应网格的计数  
+        if (nWhichP > 0) and (nWhichV > 0):  
+            XBoxNumber[nWhichP - 1, nWhichV - 1] += 1  # 注意Python的索引是从0开始的,所以需要减1  
+    # XBoxNumber现在包含了每个网格的计数[PNum行, VNum列]
+
+    for m in range(1,PNum+1):
+        for n in range(1,VNum+1):
+            XBoxNumber[m-1,n-1] = XBoxNumber[m-1,n-1] - 1
+
+    #在功率方向将网格内散点绝对个数转换为相对百分比,备用
+    PBoxPercent = np.zeros((PNum, VNum))  #(86 100) #计算后会出现浮点型,所以不能定义int类型
+    PBinSum = np.zeros((PNum,1),dtype=int)
+    for i in range(1,PNum+1):
+        for m in range(1,VNum+1):
+            PBinSum[i-1] = PBinSum[i-1] + XBoxNumber[i-1,m-1] 
+        for m in range(1,VNum+1):
+            if PBinSum[i-1]>0:
+                PBoxPercent[i-1,m-1] = (XBoxNumber[i-1,m-1] / PBinSum[i-1])*100
+    #在风速方向将网格内散点绝对个数转换为相对百分比,备用          
+    VBoxPercent = np.zeros((PNum, VNum))  #(86 100) #计算后会出现浮点型,所以不能定义int类型
+    VBinSum = np.zeros((VNum,1),dtype=int)
+    for i in range(1,VNum+1):
+        for m in range(1,PNum+1):
+            VBinSum[i-1] = VBinSum[i-1] + XBoxNumber[m-1,i-1] 
+        for m in range(1,PNum+1):
+            if VBinSum[i-1]>0:
+                VBoxPercent[m-1,i-1] = (XBoxNumber[m-1,i-1] / VBinSum[i-1])*100
+    # VBoxPercent PBoxPercent 左上-右下
+    # 将数据颠倒一下  左下-右上         第一行换为倒数第一行 方便可视化
+    InvXBoxNumber = np.zeros((PNum,VNum),dtype = int)
+    InvPBoxPercent = np.zeros((PNum,VNum),dtype = float)
+    InvVBoxPercent = np.zeros((PNum,VNum),dtype = float)
+    for m in range(1,PNum+1):
+        for n in range(1,VNum+1):
+            InvXBoxNumber[m-1,n-1] = XBoxNumber[PNum-(m-1)-1,n-1]
+            InvPBoxPercent[m-1,n-1] = PBoxPercent[PNum-(m-1)-1,n-1]
+            InvVBoxPercent[m-1,n-1] = VBoxPercent[PNum-(m-1)-1,n-1]
+    
+    #以水平功率带方向为准,分析每个水平功率带中,功率主带中心,即找百分比最大的网格位置。
+    PBoxMaxIndex = np.zeros((PNum,1),dtype = int)  #水平功率带最大网格位置索引
+    PBoxMaxP = np.zeros((PNum,1),dtype = float)       #水平功率带最大网格百分比
+    for m in range(1,PNum+1):
+        PBoxMaxIndex[m-1] = np.argmax(PBoxPercent[m-1, :])   #argmax返回最大值的索引
+        PBoxMaxP[m-1] = np.max(PBoxPercent[m-1, :])
+    #以垂直风速方向为准,分析每个垂直风速带中,功率主带中心,即找百分比最大的网格位置。
+    VBoxMaxIndex = np.zeros((VNum,1),dtype = int)  
+    VBoxMaxV = np.zeros((VNum,1),dtype = float)       
+    for m in range(1,VNum+1):
+        VBoxMaxIndex[m-1] = np.argmax(VBoxPercent[:, m-1])   
+        VBoxMaxV[m-1] = np.max(VBoxPercent[:, m-1])
+    
+    #切入风速特殊处理,如果切入风速过于偏右,向左拉回
+    if PBoxMaxIndex[0]>14:                     #第一个值对应的是风速最小处 即切入风速
+        PBoxMaxIndex[0] = 9 
+    #以水平功率带方向为基准,进行分析
+    DotDense = np.zeros(PNum)   #每一水平功率带的功率主带包含的网格数
+    DotDenseLeftRight = np.zeros((PNum,2))  #存储每一水平功率带的功率主带以最大网格为中心,向左,向右扩展的网格数
+    DotValve = 90  #从中心向左右对称扩展网格的散点百分比和的阈值。
+    PDotDenseSum = 0
+    for i in range(PNum - TopP):  # 从最下层水平功率带开始,向上分析到特定的功率带  
+        PDotDenseSum = PBoxMaxP[i]  # 以中心最大水平功率带为基准,向左向右对称扩展网格,累加各网格散点百分比  
+        iSpreadRight = 1  
+        iSpreadLeft = 1  
+          
+        while PDotDenseSum < DotValve:  
+            if (PBoxMaxIndex[i] + iSpreadRight) < VNum-1-1:  
+                PDotDenseSum += PBoxPercent[i, PBoxMaxIndex[i] + iSpreadRight]  # 向右侧扩展  
+                iSpreadRight += 1  
+            else:
+                break  
+              
+            if (PBoxMaxIndex[i] - iSpreadLeft) > 0:  
+                PDotDenseSum += PBoxPercent[i, PBoxMaxIndex[i] - iSpreadLeft]  # 向左侧扩展  
+                iSpreadLeft += 1  
+            else:  
+                break  
+        iSpreadRight = iSpreadRight-1
+        iSpreadLeft = iSpreadLeft-1
+        #向左右扩展完毕
+        DotDenseLeftRight[i, 0] = iSpreadLeft  # 左  
+        DotDenseLeftRight[i, 1] = iSpreadRight  # 右  
+        DotDense[i] = iSpreadLeft + iSpreadRight + 1  # 记录向左向右扩展的个数及每个功率仓内网格的个数  
+    # 此时DotDense和DotDenseLeftRight数组已经包含了所需信息    
+    #各行功率主带右侧宽度的中位数最具有代表性(因为先右后左)
+    DotDenseWidthLeft = np.zeros((PNum-TopP))
+    for i in range(PNum-TopP):
+        DotDenseWidthLeft[i] = DotDenseLeftRight[i,1]  #DotDenseLeftRight[i,1]:向右延伸个数
+    MainBandRight = np.median(DotDenseWidthLeft) #计算中位数
+    
+    # 初始化变量  
+    PowerLimit = np.zeros(PNum, dtype=int)  # 各水平功率带是否为限功率标识,1:是;0:不是  
+    WidthAverage = 0  # 功率主带右侧平均宽度  
+    WidthAverage_L = 0  # 功率主带左侧平均宽度  
+    WidthVar = 0  # 功率主带方差(此变量在提供的代码中并未使用)  
+    PowerLimitValve = 6  # 限功率主带判别阈值  
+    N_Pcount = 20  # 阈值  
+      
+    nCounterLimit = 0  # 限功率的个数  
+    nCounter = 0  # 正常水平功率带的个数  
+      
+    # 循环遍历水平功率带,从第1个到第PNum-TopP个  
+    for i in range(PNum - TopP):  
+        # 如果向右扩展网格数大于阈值,且该水平功率带点总数大于20,则标记为限功率带  
+        if (DotDenseLeftRight[i, 1] > PowerLimitValve) and (PBinSum[i] > N_Pcount):  
+            PowerLimit[i] = 1  
+            nCounterLimit += 1  #限功率的个数
+          
+        # 如果向右扩展网格数小于等于阈值,则累加右侧宽度(左侧宽度在代码中似乎有误)  
+        if DotDenseLeftRight[i, 1] <= PowerLimitValve:  
+            WidthAverage += DotDenseLeftRight[i, 1]  # 统计正常水平功率带右侧宽度
+            WidthAverage_L += DotDenseLeftRight[i,1]   #统计正常水平功率带左侧宽度
+            nCounter += 1  
+    # 计算平均宽度  
+    WidthAverage /= nCounter if nCounter > 0 else 1  # 避免除以0的情况  
+    WidthAverage_L /= nCounter if nCounter > 0 else 1   
+
+    #计算正常(即非限功率)水平功率带的功率主带宽度的方差,以此来反映从下到上宽度是否一致
+    WidthVar = 0  # 功率主带宽度的方差   
+    for i in range(PNum - TopP):  
+        # 如果向右扩展网格数小于等于阈值,则计算当前宽度与平均宽度的差值平方  
+        if DotDenseLeftRight[i, 1] <= PowerLimitValve:  
+            WidthVar += (DotDenseLeftRight[i, 1] - WidthAverage) ** 2  
+    # 计算方差(注意:除以nCounter-1是为了得到样本方差)  
+    WidthVar = np.sqrt(WidthVar / (nCounter - 1) if nCounter > 1 else 0)  # 避免除以0的情况
+
+    #各水平功率带,功率主带的风速范围,右侧扩展网格数*2*0.25
+    PowerBandWidth = WidthAverage*intervalwindspeed+WidthAverage_L*intervalwindspeed
+
+    # 对限负荷水平功率带的最大网格进行修正  
+    for i in range(1, PNum - TopP+1):  
+        if (PowerLimit[i] == 1) and (abs(PBoxMaxIndex[i] - PBoxMaxIndex[i - 1]) > 5):  
+            PBoxMaxIndex[i] = PBoxMaxIndex[i - 1] + 1  
+      
+    # 输出各层功率主带的左右边界网格索引  
+    DotDenseInverse = np.flipud(DotDenseLeftRight)  # 上下翻转数组以得到反向顺序  
+      
+    # 计算功率主带的左右边界  
+    CurveWidthR = np.ceil(WidthAverage) + 2  # 功率主带的右边界 + 2  
+    CurveWidthL = np.ceil(WidthAverage_L) + 2  # 功率主带的左边界 + 2  
+      
+    # 网格是否为限功率网格的标识数组  
+    BBoxLimit = np.zeros((PNum, VNum), dtype=int)  
+    # 标记限功率网格  
+    for i in range(2, PNum - TopP):  
+        if PowerLimit[i] == 1:
+            BBoxLimit[i, int(PBoxMaxIndex[i] + CurveWidthR + 1):VNum] = 1
+
+    # 初始化数据异常需要剔除的网格标识数组  
+    BBoxRemove = np.zeros((PNum, VNum), dtype=int)  
+    # 标记需要剔除的网格  
+    for m in range(PNum - TopP): 
+        for n in range(int(PBoxMaxIndex[m]) + int(CurveWidthR), VNum):  # 注意Python中的索引从0开始,因此需要减去1  
+            BBoxRemove[m, n] = 1 
+        # 功率主带左侧的超发网格,从最大索引向左直到第一个网格  
+        
+        for n in range(int(PBoxMaxIndex[m]) - int(CurveWidthL)+1, 0, -1):  # 使用range的步长参数来实现从右向左的迭代  
+            BBoxRemove[m, n-1] = 2  # 注意Python中的索引从0开始,因此需要减去1
+
+    # 初始化变量  
+    CurveTop = np.zeros((2, 1), dtype=int)  
+    CurveTopValve = 1  # 网格的百分比阈值  
+    BTopFind = 0  
+    mm = 0  
+    #确定功率主带的左上拐点,即额定风速位置的网格索引
+    CurveTop = np.zeros((2, 1), dtype=int)  
+    CurveTopValve = 1  # 网格的百分比阈值  
+    BTopFind = 0  
+    mm = 0   
+    for m in range(PNum - TopP, 0, -1):  # 注意Python的range是左闭右开区间,所以这里从PNum-TopP开始到1(不包括0)  
+        for n in range(int(np.floor(int(VCutIn) / intervalwindspeed)), VNum - 1):  # 使用floor函数来向下取整  
+            if (VBoxPercent[m, n - 1] < VBoxPercent[m, n]) and (VBoxPercent[m, n] <= VBoxPercent[m, n + 1]) and (XBoxNumber[m, n] >= 3):   
+                CurveTop[0] = m  
+                CurveTop[1] = n  #[第80个,第40个]
+                BTopFind = 1  
+                mm = m  # mm是拐点所在功率仓,对应其index
+                break  # 找到后退出内层循环  
+        if BTopFind == 1:  
+            break  # 找到后退出外层循环
+            
+    IsolateValve = 3  #功率主带右侧孤立点占比功率仓阈值 3%
+    # 遍历功率仓和网格  
+    for m in range(PNum - TopP):    
+        for n in range(int(PBoxMaxIndex[m]) + int(CurveWidthR), VNum):  
+            # 检查PBoxPercent是否小于阈值,如果是,则标记BBoxRemove为1  
+            if PBoxPercent[m, n] < IsolateValve:   
+                BBoxRemove[m, n] = 1
+    #功率主带顶部宽度
+    CurveWidthT = np.floor((maxP - PRated) / intervalP) + 1  
+    # 标记额定功率以上的超发点(PNum-PTop之间)  
+    for m in range(PNum - TopP, PNum):   
+        for n in range(VNum):  
+            BBoxRemove[m, n] = 3
+      
+    # 标记功率主带拐点左侧的欠发网格  
+    for m in range(mm-1, PNum - TopP): 
+        for n in range(int(CurveTop[1]) - 2):
+            BBoxRemove[m, n] = 2    # BBoxRemove数组现在包含了根据条件标记的超发点和欠发网格的信息
+
+    #以网格的标识,决定该网格内数据的标识。
+    # DzMarch809Sel数组现在包含了每个数据点的标识
+    DzMarch809Sel = np.zeros(nCounter1, dtype=int)  # 初始化标识数组   
+    nWhichP = 0  
+    nWhichV = 0  
+    nBadA = 0   
+    for i in range(nCounter1):  
+        for m in range( PNum ):   
+            if (DzMarch809[i, 1] > m * intervalP) and (DzMarch809[i, 1] <= (m+1) * intervalP):  
+                nWhichP = m  #m记录的是index
+                break  
+        for n in range( VNum ):  # 注意Python的range是左闭右开区间,所以这里到VNum+1  
+            if DzMarch809[i, 0] > ((n+1) * intervalwindspeed - intervalwindspeed/2) and DzMarch809[i, 0] <= ((n+1) * intervalwindspeed + intervalwindspeed / 2):  
+                nWhichV = n  #index
+                break  
+        if nWhichP >= 0 and nWhichV >= 0:  
+            if BBoxRemove[nWhichP, nWhichV] == 1:   
+                DzMarch809Sel[i] = 1  
+                nBadA += 1  
+            elif BBoxRemove[nWhichP, nWhichV] == 2:  
+                DzMarch809Sel[i] = 2  
+            elif BBoxRemove[nWhichP , nWhichV] == 3:  
+                DzMarch809Sel[i] = 0  # 额定风速以上的超发功率点认为是正常点,不再标识  
+    # DzMarch809Sel数组现在包含了每个数据点的标识
+
+    ##############################滑动窗口方法
+    # 存储限负荷数据  
+    PVLimit = np.zeros((nCounter1, 3))  #存储限负荷数据  %第3列用于存储限电的点所在的行数
+    nLimitTotal = 0  
+    nWindowLength = 6   #滑动窗口长度设置为6
+    LimitWindow = np.zeros(nWindowLength)  #滑动窗口空列表
+    UpLimit = 0    #上限
+    LowLimit = 0   #下限
+    PowerStd = 30  # 功率波动方差  
+    nWindowNum = np.floor(nCounter1/nWindowLength) #6587
+    PowerLimitUp = PRated - 100  
+    PowerLimitLow = 100  
+
+    # 循环遍历每个窗口  
+    for i in range(int(nWindowNum)):  
+        start_idx = i * nWindowLength  
+        end_idx = start_idx + nWindowLength  
+        LimitWindow = DzMarch809[start_idx:end_idx, 1]  # 提取当前窗口的数据  
+          
+        # 检查窗口内所有数据是否在功率范围内  
+        bAllInAreas = np.all(LimitWindow >= PowerLimitLow) and np.all(LimitWindow <= PowerLimitUp)  
+        if not bAllInAreas:  
+            continue  
+          
+        # 计算方差上下限  
+        UpLimit = LimitWindow[0] + PowerStd  
+        LowLimit = LimitWindow[0] - PowerStd  
+          
+        # 检查窗口内数据是否在方差范围内  
+        bAllInUpLow = np.all(LimitWindow >= LowLimit) and np.all(LimitWindow <= UpLimit)  
+        if bAllInUpLow:  
+            # 标识窗口内的数据为限负荷数据  
+            DzMarch809Sel[start_idx:end_idx] = 4  
+              
+            # 存储限负荷数据  
+            for j in range(nWindowLength):  
+                PVLimit[nLimitTotal, :2] = DzMarch809[start_idx + j, :2]  
+                PVLimit[nLimitTotal, 2] = Point_line[start_idx + j]  # 对数据进行标识  
+                nLimitTotal += 1  
+    # PVLimit现在包含了限负荷数据,nLimitTotal是限负荷数据的总数
+    
+    
+    #将功率滑动窗口主带平滑化
+    # 初始化锯齿平滑的计数器  
+    nSmooth = 0  
+    # 遍历除了最后 TopP+1 个元素之外的所有 PBoxMaxIndex 元素  
+    for i in range(PNum - TopP - 1):  
+        PVLeftDown = np.zeros(2)  
+        PVRightUp = np.zeros(2)  
+        # 检查当前与下一个 PBoxMaxIndex 之间的距离是否大于等于1  
+        if PBoxMaxIndex[i + 1] - PBoxMaxIndex[i] >= 1:  
+            # 计算左下和右上顶点的坐标  
+            PVLeftDown[0] = (PBoxMaxIndex[i]+1 + CurveWidthR) * 0.25 - 0.125  
+            PVLeftDown[1] = (i) * 25  
+            PVRightUp[0] = (PBoxMaxIndex[i+1]+1 + CurveWidthR) * 0.25 - 0.125  
+            PVRightUp[1] = (i+1) * 25  
+              
+            # 遍历 DzMarch809 数组  
+            for m in range(nCounter1):  
+                # 检查当前点是否在锯齿区域内  
+                if (DzMarch809[m, 0] > PVLeftDown[0]) and (DzMarch809[m, 0] < PVRightUp[0]) and (DzMarch809[m, 1] > PVLeftDown[1]) and (DzMarch809[m, 1] < PVRightUp[1]):
+                    # 检查斜率是否大于对角连线  
+                    if (DzMarch809[m, 1] - PVLeftDown[1]) / (DzMarch809[m, 0] - PVLeftDown[0]) > (PVRightUp[1] - PVLeftDown[1]) / (PVRightUp[0] - PVLeftDown[0]):
+                        # 如果在锯齿左上三角形中,则选中并增加锯齿平滑计数器  
+                        DzMarch809Sel[m] = 0  
+                        nSmooth += 1  
+    # DzMarch809Sel 数组现在+包含了锯齿平滑的选择结果,nSmooth 是选中的点数
+    ###################################存储数据
+    # 存储好点  
+    nCounterPV = 0  # 初始化计数器  
+    PVDot = np.zeros((nCounter1, 3))  # 初始化存储好点的数组  nCounter1是p>0的数
+    for i in range(nCounter1):  
+        if DzMarch809Sel[i] == 0:  
+            nCounterPV += 1  
+            PVDot[nCounterPV-1, :2] = DzMarch809[i, :2]  
+            PVDot[nCounterPV-1, 2] = Point_line[i]  # 好点 Point_line记录nCounter1在原始数据中的位置 
+    nCounterVP = nCounterPV  
+     
+    # 对所有数据中的好点进行标注    
+    for i in range(nCounterVP):  
+        Labeled_March809[int(PVDot[i, 2] - 1), (SM[1]-1)] = 1  # 注意Python的索引从0开始,并且需要转换为整数索引  
+     
+    # 存储坏点  
+    nCounterBad = 0  # 初始化计数器  
+    PVBad = np.zeros((nCounter1, 3))  # 初始化存储坏点的数组  
+    for i in range(nCounter1):  
+        if DzMarch809Sel[i] in [1, 2, 3]:  
+            nCounterBad += 1  
+            PVBad[nCounterBad-1, :2] = DzMarch809[i, :2]  
+            PVBad[nCounterBad-1, 2] = Point_line[i]  
+        
+    # 对所有数据中的坏点进行标注  
+    for i in range(nCounterBad):  
+        Labeled_March809[int(PVBad[i, 2] - 1),(SM[1]-1)] = 5  # 坏点标识  
+
+    # 对所有数据中的限电点进行标注   
+    for i in range(nLimitTotal):  
+        Labeled_March809[int(PVLimit[i, 2] - 1),(SM[1]-1)] = 4  # 限电点标识  
+
+    # 对所有的数据点进行标注  
+    # Labeled_March809是array,提取所第四列的值保存为dataframe
+    A = Labeled_March809[:,3]
+    A=pd.DataFrame(A,columns=['lab'])
+    return A
+
+
+# scada_10min_category()
+ 
+
+
+
+    
+    
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+    
+
+
+

+ 193 - 0
dataAnalysisBusiness/demo/SCADA_10min_category_2.py

@@ -0,0 +1,193 @@
+import os
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.pyplot import MultipleLocator
+import math
+
+
+intervalPower = 25  # For example
+intervalWindspeed = 0.25  # For example
+
+fieldRatedPower="额定功率"
+fieldRatedWindSpeed="额定风速"
+fieldWindSpeedCutIn="切入风速"
+fieldWindSpeedCutOut="切出风速"
+
+fieldTime="时间"
+fieldWindSpeed="风速"
+fieldActivePower="变频器电网侧有功功率"
+fieldLabel="lab"
+
+# 1. 数据加载和预处理函数
+def loadData(filePathSCADA:str, filePathTurbineInfo:str):
+    dataFrameSCADA = pd.read_csv(filePathSCADA, encoding="utf-8")
+    dataFrameTurbineInfo = pd.read_csv(filePathTurbineInfo)
+    return dataFrameSCADA, dataFrameTurbineInfo
+
+def extractTurbineParameters(turbineInfo:pd.DataFrame):
+    """
+    解析风电机组参数 
+
+    参数:
+        turbineInfo 风电机组信息DataFrame
+
+    返回:
+        PRated 额定功率(kw)
+        VCutOut 切出风速(m/s)
+        VCutIn 切入风速(m/s)
+        VRated 额定风速(m/s)
+    """
+    ratedPower = turbineInfo.loc[:, [fieldRatedPower]].values
+    windSpeedCutIn = turbineInfo.loc[:, [fieldWindSpeedCutIn]].values
+    windSpeedCutOut = turbineInfo.loc[:, [fieldWindSpeedCutOut]].values
+    ratedWindSpeed = turbineInfo.loc[:, [fieldRatedWindSpeed]].values
+
+    return ratedPower, windSpeedCutOut, windSpeedCutIn, ratedWindSpeed
+
+def preprocessData(dataFrameOfSCADA:pd.DataFrame):
+    """
+    获取机组SCADA数据的 时间、有功功率、风速,构建新的DataFrame变量
+
+    参数:
+        dataFrameOfSCADA 机组SCADA数据
+
+    返回:
+        由机组SCADA数据的 时间、有功功率、风速,构建新的DataFrame变量
+
+    """
+    timeStamp = dataFrameOfSCADA.loc[:, ['时间']]
+    activePower = dataFrameOfSCADA.loc[:, ['变频器电网侧有功功率']]
+    windSpeed = dataFrameOfSCADA.loc[:, ['风速']]
+    dataFramePartOfSCADA = pd.concat([timeStamp, activePower, windSpeed], axis=1)
+
+    dataFramePartOfSCADA[fieldLabel]=0
+    dataFramePartOfSCADA[fieldLabel]=dataFramePartOfSCADA[fieldLabel].astype(int)
+
+    return dataFramePartOfSCADA
+
+# 2. 数据标签分配和分箱计算
+def calculateIntervals(activePowerMax, ratedPower, windSpeedCutOut):
+    """
+    按有功功率(以25kw为间隔)、风速(以0.25m/s为间隔)分仓
+
+    参数:
+        max_power 当前机组的有功功率最大值
+        PRated  机组额定功率
+        wind_speed_cutout  切出风速
+
+    返回:
+        interval_power 有功功率分仓间隔
+        interval_windspeed 风速分仓间隔
+        PNum  有功功率分仓数量
+        VNum 风速分仓数量
+    """
+    binNumOfPower = math.floor(activePowerMax / intervalPower) + 1 if activePowerMax >= ratedPower else math.floor(ratedPower / intervalPower)
+    binNumOfWindSpeed = math.ceil(windSpeedCutOut / intervalWindspeed)
+
+    return binNumOfPower, binNumOfWindSpeed
+
+def labelData(dataFramePartOfSCADA:pd.DataFrame, conditions):
+    """
+    根据特定条件对数据进行标签分配,例如功率和风速阈值。
+    
+    参数:
+        LM (DataFrame): 包含功率和风速数据的DataFrame。
+        conditions (dict): 字典,键为条件名称,值为相应的阈值。
+    
+    返回:
+        DataFrame: 带有新的'label'列的原始DataFrame。
+    """
+    # 初始化标签列
+    dataFramePartOfSCADA['label'] = 0
+    
+    # 根据条件进行数据标签分配
+    for condition, threshold in conditions.items():
+        if condition == 'power_below':
+            dataFramePartOfSCADA.loc[dataFramePartOfSCADA[fieldActivePower] <= threshold, 'label'] = -1
+        elif condition == 'power_above':
+            dataFramePartOfSCADA.loc[dataFramePartOfSCADA[fieldActivePower] >= threshold, 'label'] = 1
+    
+    return dataFramePartOfSCADA
+
+def computeBins(data, intervals):
+    """为给定数据计算统计箱。
+    
+    参数:
+        data (DataFrame): 需要进行分箱的数据。
+        intervals (dict): 字典,为每个列指定间隔大小。
+    
+    返回:
+        DataFrame: 分箱数据作为区间内的计数或百分比。
+    """
+    binsResults = {}
+    for column, interval in intervals.items():
+        minValue = data[column].min()
+        maxValue = data[column].max()
+        bins = np.arange(minValue, maxValue + interval, interval)
+        binnedData = pd.cut(data[column], bins, include_lowest=True)
+        binCounts = pd.value_counts(binnedData, sort=False)
+        binsResults[column] = binCounts
+    
+    return pd.DataFrame(binsResults)
+
+# 3. 应用标签函数
+def applyLabels(data, labels):
+    """根据外部或计算出的标签对数据应用标签。
+    
+    参数:
+        data (DataFrame): 需要应用标签的数据。
+        labels (Series或array): 应用的标签;必须与数据的索引或长度相匹配。
+    
+    返回:
+        DataFrame: 应用标签后的数据。
+    """
+    data['label'] = labels
+    return data
+
+# 4. 数据可视化
+def plot_data(ws:list, ap:list):
+    fig = plt.figure()
+    plt.scatter(ws, ap, s=1, c='black', marker='.')
+    ax = plt.gca()
+    ax.xaxis.set_major_locator(MultipleLocator(5))
+    ax.yaxis.set_major_locator(MultipleLocator(500))
+    plt.xlim((0, 30))
+    plt.ylim((0, 2200))
+    plt.tick_params(labelsize=8)
+    plt.xlabel("V/(m$·$s$^{-1}$)", fontsize=8)
+    plt.ylabel("P/kW", fontsize=8)
+    plt.show()
+
+# 5. Main Execution
+def main():
+    turbine=82
+    filePathSCADA = r'E:\BaiduNetdiskDownload\test\min_scada_LuoTuoGou\72\{}.csv'.format(turbine)
+    filePathTurbineInfo = r'E:\BaiduNetdiskDownload\test\min_scada_LuoTuoGou\72\info.csv'
+    outputFilePathOfSCADA=r"E:\BaiduNetdiskDownload\test\min_scada_LuoTuoGou\72\labeled\labeled_{}.csv".format(turbine)
+
+    dataFrameOfSCADA, turbineInfo = loadData(filePathSCADA, filePathTurbineInfo)
+    ratedPower, windSpeedCutOut, windSpeedCutIn, ratedWindSpeed = extractTurbineParameters(turbineInfo)
+    dataFramePartOfSCADA = preprocessData(dataFrameOfSCADA)
+
+    powerMax=dataFramePartOfSCADA[fieldActivePower].max()
+    binNumOfPower, binNumOfWindSpeed=calculateIntervals(powerMax,ratedPower,windSpeedCutOut)
+    
+    # 根据功率阈值对数据进行标签分配
+    conditions = {'power_below': 10, 'power_above': ratedPower[0][0]}
+    labeledData = labelData(dataFramePartOfSCADA, conditions)
+    
+    # 为功率和风速计算分箱
+    intervals = {fieldActivePower: 100, fieldWindSpeed: 1}
+    binnedData = computeBins(labeledData, intervals)
+    
+    # 应用标签(假设某些外部标签被提供或在其他地方计算)
+    externalLabels = np.random.choice([0, 1], size=len(labeledData))  # 随机示例
+    labeledData = applyLabels(labeledData, externalLabels)
+
+    labeledData.to_csv(outputFilePathOfSCADA)
+    
+    plot_data(labeledData[fieldWindSpeed], labeledData[fieldActivePower])
+
+if __name__ == '__main__':
+    main()

+ 632 - 0
dataAnalysisBusiness/demo/SCADA_10min_category_3.py

@@ -0,0 +1,632 @@
+import os
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.pyplot import MultipleLocator
+import math
+import pdb
+# pdb.set_trace()  # 设置断点
+
+intervalPower = 25  # For example
+intervalWindspeed = 0.25  # For example
+
+fieldRatedPower="额定功率"
+fieldRatedWindSpeed="额定风速"
+fieldWindSpeedCutIn="切入风速"
+fieldWindSpeedCutOut="切出风速"
+
+fieldTime="时间"
+fieldWindSpeed="风速"
+fieldActivePower="变频器电网侧有功功率"
+fieldLabel="lab"
+
+# 1. 数据加载和预处理函数
+def loadData(filePathSCADA:str, filePathTurbineInfo:str):
+    dataFrameSCADA = pd.read_csv(filePathSCADA, encoding="utf-8")
+    dataFrameTurbineInfo = pd.read_csv(filePathTurbineInfo)
+    return dataFrameSCADA, dataFrameTurbineInfo
+
+def extractTurbineParameters(turbineInfo:pd.DataFrame):
+    """
+    解析风电机组参数 
+
+    参数:
+        turbineInfo 风电机组信息DataFrame
+
+    返回:
+        PRated 额定功率(kw)
+        VCutOut 切出风速(m/s)
+        VCutIn 切入风速(m/s)
+        VRated 额定风速(m/s)
+    """
+    ratedPower = turbineInfo.loc[:, [fieldRatedPower]].values
+    windSpeedCutIn = turbineInfo.loc[:, [fieldWindSpeedCutIn]].values
+    windSpeedCutOut = turbineInfo.loc[:, [fieldWindSpeedCutOut]].values
+    ratedWindSpeed = turbineInfo.loc[:, [fieldRatedWindSpeed]].values
+
+    return ratedPower, windSpeedCutOut, windSpeedCutIn, ratedWindSpeed
+
+def preprocessData(dataFrameOfSCADA:pd.DataFrame):
+    """
+    获取机组SCADA数据的 时间、有功功率、风速,构建新的DataFrame变量
+
+    参数:
+        dataFrameOfSCADA 机组SCADA数据
+
+    返回:
+        由机组SCADA数据的 时间、有功功率、风速,构建新的DataFrame变量
+
+    """
+    timeStamp = dataFrameOfSCADA.loc[:, ['时间']]
+    activePower = dataFrameOfSCADA.loc[:, ['变频器电网侧有功功率']]
+    windSpeed = dataFrameOfSCADA.loc[:, ['风速']]
+    dataFramePartOfSCADA = pd.concat([timeStamp,activePower,windSpeed], axis=1)
+
+    # dataFramePartOfSCADA[fieldLabel]=0
+    # dataFramePartOfSCADA[fieldLabel]=dataFramePartOfSCADA[fieldLabel].astype(int)
+
+    return dataFramePartOfSCADA
+
+    
+# 2. 数据标签分配和分箱计算
+def calculateIntervals(activePowerMax, ratedPower, windSpeedCutOut):
+    """
+    按有功功率(以25kw为间隔)、风速(以0.25m/s为间隔)分仓
+
+    参数:
+        max_power 当前机组的有功功率最大值
+        PRated  机组额定功率
+        wind_speed_cutout  切出风速
+
+    返回:
+        interval_power 有功功率分仓间隔
+        interval_windspeed 风速分仓间隔
+        PNum  有功功率分仓数量
+        VNum 风速分仓数量
+    """
+    binNumOfPower = math.floor(activePowerMax / intervalPower) + 1 if activePowerMax >= ratedPower else math.floor(ratedPower / intervalPower)
+    binNumOfWindSpeed = math.ceil(windSpeedCutOut / intervalWindspeed)
+
+    return binNumOfPower, binNumOfWindSpeed
+
+def calculateTopP(activePowerMax,ratedPower):
+    """
+    计算额定功率以上功率仓的个数
+
+    参数:
+        max_power 当前机组的有功功率最大值
+        PRated  机组额定功率
+        
+    返回:
+        TopP 额定功率以上功率仓的个数
+    """
+    TopP = 0   
+    if activePowerMax >= ratedPower: 
+        TopP = math.floor((activePowerMax - ratedPower) / intervalPower) + 1  
+    else:  
+        TopP = 0   
+    return TopP
+
+def chooseData(dataFramePartOfSCADA:pd.DataFrame, dataFrameOfSCADA):
+    """
+    根据特定条件对数据进行标签分配,例如功率和风速阈值。
+    
+    参数:
+        dataFramePartOfSCADA (DataFrame): 包含时间和功率和风速数据的DataFrame。
+        dataFrameOfSCADA: 原始数据
+    
+    返回:
+        DzMarch809: array:V P lab: 38181。
+        nCounter1: 个数
+        dataFramePartOfSCADA: 
+    """
+    # 初始化标签列
+    SM1 = dataFramePartOfSCADA.shape #(52561,3)
+    AA1 = SM1[0]  
+    lab = [[0] for _ in range(AA1)]
+    lab = pd.DataFrame(lab,columns=['lab'])
+    dataFramePartOfSCADA = pd.concat([dataFramePartOfSCADA,lab],axis=1)  #在tpv后加一列标签列
+    dataFramePartOfSCADA = dataFramePartOfSCADA.values
+    SM = dataFramePartOfSCADA.shape #(52561,4)
+    AA = SM[0] 
+    nCounter1 = 0 
+    DzMarch809_0 = np.zeros((AA, 3)) 
+    Point_line = np.zeros(AA, dtype=int)  
+    APower = dataFrameOfSCADA[fieldActivePower]
+    WSpeed = dataFrameOfSCADA[fieldWindSpeed]
+
+    for i in range(AA):
+        if (APower[i] > 10) & (WSpeed[i] > 0):
+            nCounter1 += 1  
+            DzMarch809_0[nCounter1-1, 0] = WSpeed[i]  
+            DzMarch809_0[nCounter1-1, 1] = APower[i] 
+            Point_line[nCounter1-1] = i+1  
+        if APower[i] <= 10: 
+            dataFramePartOfSCADA[i,SM[1]-1] = -1 
+    DzMarch809 = DzMarch809_0[:nCounter1, :] 
+    return DzMarch809,nCounter1,dataFramePartOfSCADA,Point_line,SM
+
+def gridCount(binNumOfWindSpeed,binNumOfPower,nCounter1,DzMarch809):
+    """
+    统计各网格中落入label!=-1的数据点个数
+    
+    参数:
+        binNumOfWindSpeed: 风速分仓个数。
+        binNumOfPower: 功率分仓个数。
+        DataFrame: 带有新的'label'列的原始DataFrame。
+        nCounter1: 数据个数
+        DzMarch809
+    返回:
+        XBoxNumber: 各网格中落入label!=-1的数据点个数的array。
+    """
+    # 遍历有效数据
+    XBoxNumber = np.ones((binNumOfPower, binNumOfWindSpeed),dtype=int) 
+    for i in range(nCounter1):             
+        for m in range(1, binNumOfPower + 1):  
+            if (DzMarch809[i,1] > (m - 1) * intervalPower) and (DzMarch809[i,1] <= m * intervalPower):  
+                nWhichP = m  
+                break  
+        for n in range(1, binNumOfWindSpeed + 1):  
+            if (DzMarch809[i, 0] > (n - 1) * intervalWindspeed) and (DzMarch809[i, 0] <= n * intervalWindspeed):  
+                nWhichV = n  
+                break  
+        if (nWhichP > 0) and (nWhichV > 0):  
+            XBoxNumber[nWhichP - 1][nWhichV - 1] += 1
+    for m in range(1,binNumOfPower+1):
+        for n in range(1,binNumOfWindSpeed+1):
+            XBoxNumber[m-1,n-1] = XBoxNumber[m-1,n-1] - 1
+    
+    return XBoxNumber
+
+def percentageDots(XBoxNumber, binNumOfPower, binNumOfWindSpeed,axis):
+    """
+    计算分仓(水平/竖直)后每个网格占百分比
+    
+    参数:
+        XBoxNumber: 各网格中落入label!=-1的数据点个数的array。
+        binNumOfPower: 功率分仓个数。
+        binNumOfWindSpeed: 风速分仓个数。
+        axis: "power"or"speed"分仓
+    返回:
+        BoxPercent: 占比情况array。
+    """
+    BoxPercent = np.zeros((binNumOfPower, binNumOfWindSpeed), dtype=float)     
+    BinSum = np.zeros((binNumOfPower if axis == 'power' else binNumOfWindSpeed, 1), dtype=int)
+    for i in range(1,1+(binNumOfPower if axis == 'power' else binNumOfWindSpeed)):
+        for m in range(1,(binNumOfWindSpeed if axis == 'power' else binNumOfPower)+1):  
+            BinSum[i-1] = BinSum[i-1] + (XBoxNumber[i-1,m-1] if axis == 'power' else XBoxNumber[m-1,i-1])
+        for m in range(1,(binNumOfWindSpeed if axis == 'power' else binNumOfPower)+1):  
+            if BinSum[i-1]>0:
+                if axis == 'power':
+                    BoxPercent[i-1,m-1] = (XBoxNumber[i-1,m-1] / BinSum[i-1])*100
+                else:
+                    BoxPercent[m-1,i-1] = (XBoxNumber[m-1,i-1] / BinSum[i-1])*100
+                    
+    return BoxPercent,BinSum
+
+def maxBoxPercentage(BoxPercent, binNumOfPower, binNumOfWindSpeed, axis):
+    """
+    计算分仓(水平/竖直)后占百分比最大的网格索引及值
+    
+    参数:
+        BoxPercent: 占比情况array。
+        binNumOfPower: 功率分仓个数。
+        binNumOfWindSpeed: 风速分仓个数。
+        axis: "power"or"speed"分仓
+    返回:
+        BoxMaxIndex: 占百分比最大的网格索引。
+        BoxMax: 占百分比最大的网格值
+    """
+    BoxMaxIndex = np.zeros((binNumOfPower if axis == 'power' else binNumOfWindSpeed,1),dtype = int) 
+    BoxMax = np.zeros((binNumOfPower if axis == 'power' else binNumOfWindSpeed,1),dtype = float)  
+    for m in range(1,(binNumOfPower if axis == 'power' else binNumOfWindSpeed)+1):
+        BoxMaxIndex[m-1] = (np.argmax(BoxPercent[m-1, :])) if axis == 'power' else (np.argmax(BoxPercent[:, m-1]))
+        BoxMax[m-1] = (np.max(BoxPercent[m-1, :]))if axis == 'power' else (np.max(BoxPercent[:, m-1]))
+
+    return BoxMaxIndex, BoxMax
+
+def extendBoxPercent(m, BoxMax,TopP,BoxMaxIndex,BoxPercent,binNumOfPower,binNumOfWindSpeed):
+    """
+    以中心最大水平功率带为基准,向两侧对称扩展网格,使网格散点百分比总值达到阈值m
+    
+    参数:
+        m: 设定总和百分比阈值。
+        BoxMax: 占百分比最大的网格值。
+        TopP: 额定功率以上功率仓个数。
+        BoxMaxIndex: 占百分比最大的网格索引。
+        BoxPercent: 占比情况array。
+        binNumOfPower: 功率分仓个数。
+        binNumOfWindSpeed: 风速分仓个数。
+    返回:
+        DotDense: 每个功率仓内网格的个数。
+        DotDenseLeftRight: 向左向右拓展的网格个数
+    """
+    DotDense = np.zeros(binNumOfPower)  
+    DotDenseLeftRight = np.zeros((binNumOfPower,2))
+    DotValve = m 
+    PDotDenseSum = 0
+    for i in range(binNumOfPower - TopP):
+        PDotDenseSum = BoxMax[i] 
+        iSpreadRight = 1  
+        iSpreadLeft = 1         
+        while PDotDenseSum < DotValve:  
+            if (BoxMaxIndex[i] + iSpreadRight) < binNumOfWindSpeed-1-1:  
+                PDotDenseSum += BoxPercent[i, BoxMaxIndex[i] + iSpreadRight] 
+                iSpreadRight += 1  
+            else:
+                break             
+            if (BoxMaxIndex[i] - iSpreadLeft) > 0:  
+                PDotDenseSum += BoxPercent[i, BoxMaxIndex[i] - iSpreadLeft] 
+                iSpreadLeft += 1  
+            else:  
+                break  
+        iSpreadRight = iSpreadRight-1
+        iSpreadLeft = iSpreadLeft-1
+       
+        DotDenseLeftRight[i, 0] = iSpreadLeft 
+        DotDenseLeftRight[i, 1] = iSpreadRight 
+        DotDense[i] = iSpreadLeft + iSpreadRight + 1    
+
+    return DotDenseLeftRight
+
+def calculatePWidth(binNumOfPower,TopP,DotDenseLeftRight,PBinSum):
+    """
+    计算功率主带的平均宽度
+    
+    参数:
+        binNumOfPower: 功率分仓个数。
+        TopP: 额定功率以上功率仓个数。
+        DotDenseLeftRight: 向左向右拓展的网格个数    
+        PBinSum: 功率仓内数据点总和
+    返回:
+        DotDense: 每个功率仓内网格的个数。
+        DotDenseLeftRight: 向左向右拓展的网格个数
+        PowerLimit: 各水平功率带是否为限功率标识,1:是;0:不是
+    """
+
+    PowerLimit = np.zeros(binNumOfPower, dtype=int)  
+    WidthAverage = 0    
+    WidthAverage_L = 0 
+    nCounter = 0  
+    PowerLimitValve = 6    
+    N_Pcount = 20  
+    for i in range(binNumOfPower - TopP):   
+        if (DotDenseLeftRight[i, 1] > PowerLimitValve) and (PBinSum[i] > N_Pcount):  
+            PowerLimit[i] = 1  
+           
+        if DotDenseLeftRight[i, 1] <= PowerLimitValve:  
+            WidthAverage += DotDenseLeftRight[i, 1]
+            WidthAverage_L += DotDenseLeftRight[i,1] 
+            nCounter += 1  
+    WidthAverage /= nCounter if nCounter > 0 else 1  
+    WidthAverage_L /= nCounter if nCounter > 0 else 1   
+
+    return WidthAverage, WidthAverage_L,PowerLimit
+
+def amendMaxBox(binNumOfPower,TopP,PowerLimit,BoxMaxIndex):
+    """
+    对限负荷水平功率带的最大网格进行修正
+    
+    参数:
+        binNumOfPower: 功率分仓个数。
+        TopP: 额定功率以上功率仓个数。
+        PowerLimit:标识限功率水平功率带,1:是;0:不是
+        BoxMaxIndex: 占百分比最大的网格索引
+    返回:
+        BoxMaxIndex: 修正后的最大占比网格索引
+    """
+
+    for i in range(1, binNumOfPower - TopP+1):  
+        if (PowerLimit[i] == 1) and (abs(BoxMaxIndex[i] - BoxMaxIndex[i - 1]) > 5):  
+            BoxMaxIndex[i] = BoxMaxIndex[i - 1] + 1  
+
+    return BoxMaxIndex
+
+def markBoxLimit(binNumOfPower,binNumOfWindSpeed,TopP,CurveWidthR,CurveWidthL,BoxMaxIndex):
+    '''
+    标记需剔除的网格
+    
+    参数:
+        binNumOfPower: 功率分仓个数。
+        binNumOfWindSpeed:风速分仓个数
+        TopP: 额定功率以上功率仓个数。
+        CurveWidthR:功率主带轮廓
+        CurveWidthL
+        BoxMaxIndex: 修正后的最大占比网格索引
+    返回:
+        BBoxRemove: 标识需剔除的网格
+    '''
+    BBoxRemove = np.zeros((binNumOfPower, binNumOfWindSpeed), dtype=int)  
+    for m in range(binNumOfPower - TopP): 
+        for n in range(int(BoxMaxIndex[m]) + int(CurveWidthR), binNumOfWindSpeed):
+            BBoxRemove[m, n] = 1  
+        for n in range(int(BoxMaxIndex[m]) - int(CurveWidthL)+1, 0, -1):   
+            BBoxRemove[m, n-1] = 2 
+    return BBoxRemove
+
+def markBoxPLimit(binNumOfPower,binNumOfWindSpeed,TopP,CurveWidthR,PowerLimit,BoxPercent,BoxMaxIndex,mm,BBoxRemove,nn):
+    '''
+    标记限功率网格 
+    1:右侧欠发 2:左侧超发 3:额定功率以上超发
+    
+    参数:
+        binNumOfPower: 功率分仓个数。
+        binNumOfWindSpeed:风速分仓个数
+        TopP: 额定功率以上功率仓个数。
+        CurveWidthR:功率主带轮廓
+        PowerLimit: 标识限功率水平功率带,1:是;0:不是
+        BoxMaxIndex: 修正后的最大占比网格索引
+        mm: 拐点所在功率仓
+        BBoxRemove:需剔除的网格
+        CurveTop1:拐点对应列
+    返回:
+        BBoxLimit:标识限功率网格
+    '''
+    BBoxLimit = np.zeros((binNumOfPower, binNumOfWindSpeed), dtype=int)  
+    for i in range(2, binNumOfPower - TopP):  
+        if PowerLimit[i] == 1:
+            BBoxLimit[i, int(BoxMaxIndex[i] + CurveWidthR + 1):binNumOfWindSpeed] = 1
+    IsolateValve = 3
+    for m in range(binNumOfPower - TopP):    
+        for n in range(int(BoxMaxIndex[m]) + int(CurveWidthR), binNumOfWindSpeed):    
+            if BoxPercent[m, n] < IsolateValve:   
+                BBoxRemove[m, n] = 1
+
+    for m in range(binNumOfPower - TopP, binNumOfPower):   
+        for n in range(binNumOfWindSpeed):  
+            BBoxRemove[m, n] = 3
+      
+    # 标记功率主带拐点左侧的欠发网格  
+    for m in range(mm-1, binNumOfPower - TopP): 
+        for n in range(int(nn) - 2):
+            BBoxRemove[m, n] = 2
+    
+    return BBoxLimit
+    
+def markData(binNumOfPower, binNumOfWindSpeed,DzMarch809,BBoxRemove,nCounter1):
+    '''
+    根据网格标识来标记数据点
+    
+    参数:
+        nCounter1
+        binNumOfPower: 功率分仓个数。
+        binNumOfWindSpeed:风速分仓个数
+        DzMarch809: array V P lab: 38181。
+        BBoxRemove:需剔除的网格
+        
+    返回:
+        DzMarch809Sel:数组现在包含了每个数据点的标识
+    '''
+    DzMarch809Sel = np.zeros(nCounter1, dtype=int)
+    nWhichP = 0  
+    nWhichV = 0  
+    for i in range(nCounter1):   
+        for m in range( binNumOfPower ):   
+            if ((DzMarch809[i,1])> m * intervalPower) and ((DzMarch809[i,1]) <= (m+1) * intervalPower):  
+                nWhichP = m  #m记录的是index
+                break  
+        for n in range( binNumOfWindSpeed ):    
+            if DzMarch809[i,0] > ((n+1) * intervalWindspeed - intervalWindspeed/2) and DzMarch809[i,0] <= ((n+1) * intervalWindspeed + intervalWindspeed / 2):  
+                nWhichV = n 
+                break  
+        if nWhichP >= 0 and nWhichV >= 0:  
+            if BBoxRemove[nWhichP, nWhichV] == 1:   
+                DzMarch809Sel[i] = 1  
+            elif BBoxRemove[nWhichP, nWhichV] == 2:  
+                DzMarch809Sel[i] = 2  
+            elif BBoxRemove[nWhichP , nWhichV] == 3:  
+                DzMarch809Sel[i] = 0  
+    
+    return DzMarch809Sel
+    
+
+def windowFilter(nCounter1,ratedPower,DzMarch809,DzMarch809Sel,Point_line):
+    '''
+    滑动窗口方法,进一步标记数据坏点
+    
+    参数:
+        nCounter1:
+        ratedPower:
+        Point_line:
+        
+    返回:
+        PVLimit: 限负荷数据
+        nLimitTotal: 是限负荷数据的总数
+    '''
+
+    PVLimit = np.zeros((nCounter1, 3)) 
+    nLimitTotal = 0  
+    nWindowLength = 6  
+    LimitWindow = np.zeros(nWindowLength)
+    UpLimit = 0   
+    LowLimit = 0  
+    PowerStd = 30  
+    nWindowNum = np.floor(nCounter1/nWindowLength)
+    PowerLimitUp = ratedPower - 100  
+    PowerLimitLow = 100  
+
+    # 循环遍历每个窗口  
+    for i in range(int(nWindowNum)):  
+        start_idx = i * nWindowLength  
+        end_idx = start_idx + nWindowLength  
+        LimitWindow = DzMarch809[start_idx:end_idx, 1]  
+         
+        bAllInAreas = np.all(LimitWindow >= PowerLimitLow) and np.all(LimitWindow <= PowerLimitUp)  
+        if not bAllInAreas:  
+            continue  
+        
+        UpLimit = LimitWindow[0] + PowerStd  
+        LowLimit = LimitWindow[0] - PowerStd  
+        
+        bAllInUpLow = np.all(LimitWindow >= LowLimit) and np.all(LimitWindow <= UpLimit)  
+        if bAllInUpLow: 
+            DzMarch809Sel[start_idx:end_idx] = 4  
+ 
+            for j in range(nWindowLength):  
+                PVLimit[nLimitTotal, :2] = DzMarch809[start_idx + j, :2]  
+                PVLimit[nLimitTotal, 2] = Point_line[start_idx + j]  # 对数据进行标识  
+                nLimitTotal += 1  
+    return PVLimit,nLimitTotal
+
+def store_points(DzMarch809, DzMarch809Sel,Point_line, nCounter1):  
+    """  
+    存储好点,并返回存储好的点的数组和计数。
+    
+    参数:
+        DzMarch809: array:V P lab: 38181。
+        DzMarch809Sel: 数组现在包含了每个数据点的标识
+        Point_line:
+        nCounter1:
+        axis: 'good' or 'bad'
+        
+    返回:
+        PVDot: 数据
+        nCounterPV: 数据个数
+
+    """  
+    PVDot = np.zeros((nCounter1, 3))
+    PVBad = np.zeros((nCounter1, 3))  
+
+    nCounterPV = 0  
+    nCounterBad = 0 
+    for i in range(nCounter1):
+        if DzMarch809Sel[i] == 0:   
+            nCounterPV += 1 
+            PVDot[nCounterPV-1, :2] = DzMarch809[i, :2]
+            PVDot[nCounterPV-1, 2] = Point_line[i]  
+        elif DzMarch809Sel[i] in [1, 2, 3]:  
+            nCounterBad += 1  
+            PVBad[nCounterBad-1, :2] = DzMarch809[i, :2]  
+            PVBad[nCounterBad-1, 2] = Point_line[i]
+                  
+    return PVDot, nCounterPV,PVBad,nCounterBad  
+
+def markAllData(nCounterPV,nCounterBad,dataFramePartOfSCADA,PVDot,PVBad,SM,nLimitTotal,PVLimit):
+    """  
+    标记好点、坏点、限电点。
+    
+    参数:
+        nCounterPV
+        nCounterBad
+        dataFramePartOfSCADA
+        PVDot
+        PVBad
+        SM
+        nLimitTotal
+        PVLimit
+        
+    返回:
+        dataFramePartOfSCADA
+
+    """  
+
+    for i in range(nCounterPV):
+        dataFramePartOfSCADA[int(PVDot[i, 2] - 1), (SM[1]-1)] = 1   
+    #坏点  
+    for i in range(nCounterBad):  
+        dataFramePartOfSCADA[int(PVBad[i, 2] - 1),(SM[1]-1)] = 5  # 坏点标识  
+
+    # 对所有数据中的限电点进行标注   
+    for i in range(nLimitTotal):  
+        dataFramePartOfSCADA[int(PVLimit[i, 2] - 1),(SM[1]-1)] = 4  # 限电点标识  
+
+    return dataFramePartOfSCADA
+# 4. 数据可视化
+def plot_data(ws:list, ap:list):
+    fig = plt.figure()
+    plt.scatter(ws, ap, s=1, c='black', marker='.')
+    ax = plt.gca()
+    ax.xaxis.set_major_locator(MultipleLocator(5))
+    ax.yaxis.set_major_locator(MultipleLocator(500))
+    plt.xlim((0, 30))
+    plt.ylim((0, 2200))
+    plt.tick_params(labelsize=8)
+    plt.xlabel("V/(m$·$s$^{-1}$)", fontsize=8)
+    plt.ylabel("P/kW", fontsize=8)
+    plt.show()
+
+# 5. Main Execution
+def main():
+    turbine=85
+    basePath=r'E:\BaiduNetdiskDownload\test\min_scada_LuoTuoGou\72'
+    filePathSCADA = r'{}\{}.csv'.format(basePath,turbine)
+    filePathTurbineInfo = r'{}\info.csv'.format(basePath)
+    outputFilePathOfSCADA=r"{}\labeled\labeled_{}.csv".format(basePath,turbine)
+
+    dataFrameOfSCADA, turbineInfo = loadData(filePathSCADA, filePathTurbineInfo)
+    ratedPower, windSpeedCutOut, windSpeedCutIn, ratedWindSpeed = extractTurbineParameters(turbineInfo)
+    dataFramePartOfSCADA = preprocessData(dataFrameOfSCADA)
+    powerMax=dataFramePartOfSCADA[fieldActivePower].max()
+
+    binNumOfPower, binNumOfWindSpeed = calculateIntervals(powerMax,ratedPower,windSpeedCutOut)
+    TopP = calculateTopP(powerMax,ratedPower)
+    # 根据功率阈值对数据进行标签分配
+    DzMarch809,nCounter1,dataFramePartOfSCADA,Point_line,SM = chooseData(dataFramePartOfSCADA, dataFrameOfSCADA)  
+    XBoxNumber = gridCount(binNumOfWindSpeed,binNumOfPower,nCounter1,DzMarch809)
+    PBoxPercent,PBinSum = percentageDots(XBoxNumber, binNumOfPower, binNumOfWindSpeed, 'power')
+    VBoxPercent,VBinSum = percentageDots(XBoxNumber, binNumOfPower, binNumOfWindSpeed, 'speed')
+
+    PBoxMaxIndex, PBoxMaxP = maxBoxPercentage(PBoxPercent, binNumOfPower, binNumOfWindSpeed, 'power')
+    VBoxMaxIndex, VBoxMaxV = maxBoxPercentage(VBoxPercent, binNumOfPower, binNumOfWindSpeed, 'speed')
+    if PBoxMaxIndex[0] > 14: PBoxMaxIndex[0] = 9
+    DotDenseLeftRight = extendBoxPercent(90, PBoxMaxP,TopP,PBoxMaxIndex,PBoxPercent,binNumOfPower,binNumOfWindSpeed)
+    # pdb.set_trace()  # 设置断点
+    WidthAverage, WidthAverage_L,PowerLimit = calculatePWidth(binNumOfPower,TopP,DotDenseLeftRight,PBinSum)
+    PBoxMaxIndex = amendMaxBox(binNumOfPower,TopP,PowerLimit,PBoxMaxIndex)
+    # 计算功率主带的左右边界  
+    CurveWidthR = np.ceil(WidthAverage) + 2  
+    CurveWidthL = np.ceil(WidthAverage_L) + 2 
+    #确定功率主带的左上拐点,即额定风速位置的网格索引
+    CurveTop = np.zeros((2, 1), dtype=int)  
+    BTopFind = 0  
+    for m in range(binNumOfPower - TopP, 0, -1):
+        for n in range(int(np.floor(int(windSpeedCutIn) / intervalWindspeed)), binNumOfWindSpeed - 1):   
+            if (VBoxPercent[m, n - 1] < VBoxPercent[m, n]) and (VBoxPercent[m, n] <= VBoxPercent[m, n + 1]) and (XBoxNumber[m, n] >= 3):   
+                CurveTop[0] = m  
+                CurveTop[1] = n  #[第80个,第40个]
+                BTopFind = 1
+                mm = m
+                nn = n
+                break 
+        if BTopFind == 1:  
+            break 
+    #标记网格
+    BBoxRemove = markBoxLimit(binNumOfPower,binNumOfWindSpeed,TopP,CurveWidthR,CurveWidthL,PBoxMaxIndex)
+    BBoxLimit = markBoxPLimit(binNumOfPower,binNumOfWindSpeed,TopP,CurveWidthR,PowerLimit,PBoxPercent,PBoxMaxIndex,mm,BBoxRemove,nn)
+    DzMarch809Sel = markData(binNumOfPower, binNumOfWindSpeed,DzMarch809,BBoxRemove,nCounter1)
+    PVLimit,nLimitTotal = windowFilter(nCounter1,ratedPower,DzMarch809,DzMarch809Sel,Point_line)
+    #将功率滑动窗口主带平滑化
+    nSmooth = 0   
+    for i in range(binNumOfPower - TopP - 1):  
+        PVLeftDown = np.zeros(2)  
+        PVRightUp = np.zeros(2)   
+        if PBoxMaxIndex[i + 1] - PBoxMaxIndex[i] >= 1:  
+            # 计算左下和右上顶点的坐标  
+            PVLeftDown[0] = (PBoxMaxIndex[i]+1 + CurveWidthR) * 0.25 - 0.125  
+            PVLeftDown[1] = (i) * 25  
+            PVRightUp[0] = (PBoxMaxIndex[i+1]+1 + CurveWidthR) * 0.25 - 0.125  
+            PVRightUp[1] = (i+1) * 25  
+                
+            for m in range(nCounter1):  
+                # 检查当前点是否在锯齿区域内  
+                if (DzMarch809[m, 0] > PVLeftDown[0]) and (DzMarch809[m, 0] < PVRightUp[0]) and (DzMarch809[m, 1] > PVLeftDown[1]) and (DzMarch809[m, 1] < PVRightUp[1]):
+                    # 检查斜率是否大于对角连线  
+                    if (DzMarch809[m, 1] - PVLeftDown[1]) / (DzMarch809[m, 0] - PVLeftDown[0]) > (PVRightUp[1] - PVLeftDown[1]) / (PVRightUp[0] - PVLeftDown[0]):
+                        # 如果在锯齿左上三角形中,则选中并增加锯齿平滑计数器  
+                        DzMarch809Sel[m] = 0  
+                        nSmooth += 1  
+    # DzMarch809Sel 数组现在包含了锯齿平滑的选择结果,nSmooth 是选中的点数
+    PVDot, nCounterPV,PVBad,nCounterBad = store_points(DzMarch809, DzMarch809Sel,Point_line, nCounter1)
+    #标注   
+    dataFramePartOfSCADA = markAllData(nCounterPV,nCounterBad,dataFramePartOfSCADA,PVDot,PVBad,SM,nLimitTotal,PVLimit)
+    A = dataFramePartOfSCADA[:,3]
+    A=pd.DataFrame(A,columns=['lab'])
+
+    labeledData = pd.concat([dataFrameOfSCADA,A],axis=1)
+    D = labeledData[labeledData['lab'].isin([-1,0,1,2,3,4,5])]#选择为1的行
+    labeledData.to_csv(outputFilePathOfSCADA,encoding='utf-8')
+    plot_data(D[fieldWindSpeed], D[fieldActivePower])
+
+
+if __name__ == '__main__':
+    main()

+ 62 - 0
dataAnalysisBusiness/demo/scatter3D_plotly.py

@@ -0,0 +1,62 @@
+import pandas as pd  
+import plotly.graph_objects as go  
+  
+# 示例数据  
+data = {  
+    '机组名': ['机组A', '机组B', '机组C', '机组D'],  
+    '时间': ['2024-01-09 09:13:29', '2024-01-10 10:14:30', '2024-02-09 08:13:29', '2024-02-10 09:14:30'],  
+    '年月': ['2024-01', '2024-01', '2024-02', '2024-02'],  
+    '风速': [5.0, 6.0, 4.5, 5.5],  
+    '有功功率': [1000, 1200, 900, 1100]  
+}  
+  
+df = pd.DataFrame(data)  
+  
+# 按风速升序排列数据  
+df_sorted = df.sort_values(by='风速')  
+  
+# 获取唯一年月  
+unique_months = df_sorted['年月'].unique()  
+  
+# 自定义颜色列表(确保颜色数量与唯一月份的数量相匹配)  
+colors = ['red', 'blue', 'green', 'purple']  # 根据实际唯一月份数量调整颜色数量  
+  
+# 创建颜色映射  
+color_map = dict(zip(unique_months, colors))  
+  
+# 使用go.Scatter3d创建3D散点图  
+trace = go.Scatter3d(  
+    x=df_sorted['风速'],  
+    y=df_sorted['有功功率'],  
+    z=[color_map[month] for month in df_sorted['年月']],  
+    mode='markers',  
+    marker=dict(  
+        color=[color_map[month] for month in df_sorted['年月']],  
+        size=10,  
+        line=dict(color='rgba(255, 255, 255, 0.8)', width=0.5),  
+        opacity=0.8  
+    )  
+)  
+  
+# 创建图形  
+fig = go.Figure(data=[trace])  
+  
+# 更新图形的布局  
+fig.update_layout(  
+    title='按风速升序排列的3D散点图:风速、有功功率与年月',  
+    margin=dict(l=0, r=0, b=0, t=0),  
+    scene=dict(  
+        xaxis=dict(title='风速'),  
+        yaxis=dict(title='有功功率'),  
+        zaxis=dict(  
+            title='年月',  
+            tickmode='array',  
+            tickvals=unique_months,  
+            ticktext=unique_months,  
+            categoryorder='category ascending'  
+        )  
+    )  
+)  
+  
+# 显示图形  
+fig.show()

+ 50 - 0
dataAnalysisBusiness/demo/scatter3D_plotly_make_subplots.py

@@ -0,0 +1,50 @@
+import pandas as pd  
+import plotly.graph_objects as go  
+from plotly.subplots import make_subplots  
+  
+# 假设你的DataFrame叫做df,并且已经包含了所需字段  
+# 如果你的数据是CSV文件,可以使用pd.read_csv('your_file.csv')来加载数据  
+# df = pd.read_csv('your_file.csv')  
+  
+# 示例数据  
+data = {  
+    '机组名': ['机组A', '机组B', '机组C', '机组D'],  
+    '时间': ['2024-01-09 09:13:29', '2024-01-10 10:14:30', '2024-02-09 08:13:29', '2024-02-10 09:14:30'],  
+    '年月': ['2024-01', '2024-01', '2024-02', '2024-02'],  
+    '风速': [5.0, 6.0, 4.5, 5.5],  
+    '有功功率': [1000, 1200, 900, 1100]  
+}  
+  
+df = pd.DataFrame(data)  
+  
+# 创建颜色映射,将每个年月映射到一个唯一的颜色  
+unique_months = df['年月'].unique()  
+colors = [f'rgb({i}, {150 - i}, 50)' for i in range(len(unique_months))]  
+color_map = dict(zip(unique_months, colors))  
+  
+# 使用make_subplots创建3D散点图  
+fig = make_subplots(rows=1, cols=1, specs=[[{"type": "scatter3d"}]])  
+  
+# 遍历DataFrame的每一行,为每个点添加数据  
+for index, row in df.iterrows():  
+    x = row['风速']  
+    y = row['年月']  
+    z = row['有功功率']  
+    color = color_map[y]  
+      
+    # 添加散点到子图  
+    fig.add_trace(go.Scatter3d(x=[x], y=[y], z=[z], mode='markers', marker=dict(color=color)), row=1, col=1)  
+  
+# 更新子图的布局,设置y轴为category类型,并设置其类别顺序  
+fig.update_layout(  
+    title='3D散点图:风速、年月与有功功率',  
+    margin=dict(l=0, r=0, b=0, t=0),  
+    scene=dict(  
+        xaxis=dict(title='风速'),  
+        yaxis=dict(title='年月', tickmode='array', tickvals=unique_months, ticktext=unique_months, categoryorder='category ascending'),  
+        zaxis=dict(title='有功功率')  
+    )  
+)  
+  
+# 显示图形  
+fig.show()

+ 19 - 0
dataAnalysisBusiness/demo/test.py

@@ -0,0 +1,19 @@
+import plotly.express as px
+import pandas as pd
+
+# 创建一个示例数据框架,包含单月数据
+data_single_month = {
+    '时间': ['2023-03', '2023-03', '2023-03', '2023-03', '2023-03'],
+    '发电机转速': [1000, 1500, 2000, 2500, 3000],
+    '功率': [120, 180, 160, 210, 230]
+}
+
+df_single = pd.DataFrame(data_single_month)
+
+# 绘制3D散点图
+fig_single = px.scatter_3d(df_single, x='发电机转速', y='时间', z='功率', 
+                           title='3D 散点图 - 单月数据',
+                           labels={'发电机转速': '转速', '时间': '月份', '功率': '输出功率'})
+
+# 显示图形
+fig_single.show()

+ 113 - 0
dataAnalysisBusiness/demo/testDataProcess.py

@@ -0,0 +1,113 @@
+import os  
+import pandas as pd  
+import numpy as np
+import matplotlib.pyplot as plt  
+  
+def process_scada_data(fpath, turbine_number, fn_start, fn_end, status_normal):  
+    """  
+    处理SCADA数据的函数。  
+      
+    参数:  
+        fpath (str): 文件存放位置的路径。  
+        turbine_number (int): 风机数量(尽管此参数在此函数中未使用,但可以保留以匹配MATLAB代码)。  
+        fn_start (int): 开始处理的文件编号。  
+        fn_end (int): 结束处理的文件编号(不包含)。  
+        status_normal (int): 风机正常并网状态的状态字(尽管此参数在此函数中未使用,但可以保留以匹配MATLAB代码)。  
+    """  
+    # 循环处理每个文件  
+    for fn in range(fn_start, fn_end):  
+        fname = os.path.join(fpath, f"{fn}.csv")  
+          
+        # 读取CSV文件  
+        scada_10min = pd.read_csv(fname)  
+          
+        # 提取所需列  
+        time_stamp = scada_10min["时间"]  
+        active_power = scada_10min["变频器电网侧有功功率"]  
+        wind_speed = scada_10min["风速"]  
+          
+        # 创建包含所需列的DataFrame  
+        LM = pd.DataFrame({  
+            "时间戳": time_stamp,  
+            "有功功率": active_power,  
+            "风速": wind_speed  
+        })  
+          
+        # 调用数据标签处理函数(需要您根据MATLAB实现来编写此函数)  
+        xx = data_label(LM,fpath)  
+          
+        # 合并标签数据到原始DataFrame  
+        merged_df = pd.concat([scada_10min, xx], axis=1)  
+          
+        # 筛选出标签为1的行  
+        D = merged_df[merged_df["lab"] == 1]  
+          
+        # 绘制散点图  
+        plt.scatter(D["风速"], D["变频器电网侧有功功率"], s=50, fillstyle='full')  
+        plt.title(f"风机 {fn} 散点图")  
+        plt.xlabel("风速")  
+        plt.ylabel("变频器电网侧有功功率")  
+        plt.show()  
+          
+        # 创建保存结果的目录(如果不存在)  
+        labeled_dir = os.path.join(fpath, "labeled")  
+        os.makedirs(labeled_dir, exist_ok=True)  
+          
+        # 将处理后的数据保存到CSV文件  
+        labeled_fname = os.path.join(labeled_dir, f"{fn}_10s_n.csv")  
+        merged_df.to_csv(labeled_fname, index=False)  
+  
+# 假设data_label函数已经实现,这里只是一个示例的占位符  
+def data_label(df:pd.DataFrame,fpath):  
+    # 在这里实现您的数据标签处理逻辑  
+    # 返回带有新标签的Series或DataFrame  
+     # 读取风机参数数据
+    fname2 = fpath + "info.csv"
+    turbine_info = pd.read_csv(fname2, keep_default_na=False)
+    PRated = turbine_info["额定功率"].values[0]
+    VCutOut = turbine_info["切出风速"].values[0]
+    VCutIn = turbine_info["切入风速"].values[0]
+    VRated = turbine_info["额定风速"].values[0]
+    
+    # 读入有功功率和风速数据
+    Labeled_March809 = df
+    APower = Labeled_March809["active_power"]
+    WSpeed = Labeled_March809["wind_speed"]
+
+    # 初始化计算用的变量
+    maxP = APower.max()
+    intervalP = 25  # 功率分区间隔为25
+    intervalwindspeed = 0.25  # 风速分区间隔为0.25m/s
+    
+    # 根据最大功率和额定功率,计算功率和风速的区间数
+    PNum = (maxP // intervalP) + 1 if maxP >= PRated else (PRated // intervalP)
+    TopP = ((maxP - PRated) // intervalP) + 1 if maxP >= PRated else 0
+    VNum = np.ceil(VCutOut / intervalwindspeed).astype(int)
+
+    # 初始化标签列
+    Labeled_March809['label'] = 0
+
+    # 数据预处理:标记功率小于等于10的点
+    Labeled_March809.loc[APower <= 10, 'label'] = -1
+
+    # 下面是逻辑处理的示例,涉及到循环、条件判断和数据标记
+    # 示例:标记风速和功率在特定范围内的点
+    for i, row in Labeled_March809.iterrows():
+        if row['active_power'] > 10 and row['wind_speed'] > 0:
+            # 这里可以根据需要添加更多的处理逻辑
+            pass
+
+    # 以下是更高级的数据处理示例,这部分代码需要您根据实际逻辑继续开发
+    # 示例:根据风速和功率的分布对数据进行进一步的标记
+    # 请注意,这里需要你根据上面 MATLAB 代码的具体逻辑来实现相应的Python代码
+
+    return Labeled_March809
+  
+# 设置文件路径和其他参数  
+fpath = "E:\\BaiduNetdiskDownload\\test\\min_scada_LuoTuoGou\\72\\"  
+# 注意:turbine_number 在此函数中未使用,但保持以匹配MATLAB代码  
+turbine_number = 24  
+status_normal = 8  
+  
+# 调用函数处理文件,假设从编号82的文件开始,只处理这一个文件  
+process_scada_data(fpath, turbine_number, 82, 83, status_normal)

+ 12 - 0
dataAnalysisBusiness/demo/testPandas.py

@@ -0,0 +1,12 @@
+import pandas as pd
+import numpy as np
+
+df=pd.read_csv(r"E:/BaiduNetdiskDownload/DTSXJK_WJWFC_Q1_W001_2023-10-01_last_1seconds.csv",header=1)
+
+print(df.head())
+
+
+df["WNAC_WdDir"]=df["WNAC_WdDir"].astype("Float32")
+df["弧度"]=df["WNAC_WdDir"]/360*2*np.pi
+
+print(df.head())

+ 10 - 0
dataAnalysisBusiness/setup.py

@@ -0,0 +1,10 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='dataAnalysisBusiness',
+    version='1.0.202403180918',
+    description='Data Analysis Business Package', # 描述信息
+    author='Xie Zhou Yang', # 作者
+    packages=find_packages(),
+    exclude_package_data={'': ['algorithm/*.py','common/*.py']},  # 另一种排除源代码的方式
+)

+ 4 - 0
dataContract/__init__.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import algorithmContract
+__all__=['algorithmContract']

+ 0 - 0
dataContract/algorithmContract/__init__.py


+ 188 - 0
dataContract/algorithmContract/confBusiness.py

@@ -0,0 +1,188 @@
+import pandas as pd
+from utils.jsonUtil import JsonUtil
+
+# 全局变量
+charset_unify = 'utf-8'
+CSVSuffix = '.csv'
+
+Field_NameOfTurbine = "turbine_name"
+Field_GeneratorTorque = "generator_torque"
+Field_GeneratorSpeed = "generator_speed"
+Field_RotorSpeed = "rotor_speed"
+Field_WindSpeed = "wind_speed"
+Field_AngleIncluded = "angle_included"
+Field_YearMonth = "year-month"
+Field_YearMonthDay = "year-month-day"
+Field_PowerFloor= "power_floor"
+Field_Cp = "cp"
+Field_CpMedian = "cp_median"
+Field_TSR = "tsr"
+Field_TSRMedian = "tsr_median"
+Field_YawError="yaw_error"
+Field_LableFlag="lab"
+
+
+class GraphSet:
+    def __init__(self):
+        self.multipl = None
+        self.step = None
+        self.min = None
+        self.max = None
+
+
+class ConfBusiness:
+    def __init__(self):
+        self.farm_name = None
+        self.rated_power = None
+        self.rated_WindSpeed = None
+        self.rotor_diameter = None
+        self.density_air = None
+        self.rotational_Speed_Ratio = None
+
+        self.type_name = None
+
+        self.time_period = None            # 时间间隔,单位是秒
+
+        self.output_name = None
+        self.output_prefix = None
+
+        self.turbineInfoFilePathCSV = None  # 风电机组信息
+        self.turbineGuaranteedPowerCurveFilePathCSV = None  # 合同担保功率曲线
+
+        self.input_path = None
+        self.skip_row_number = None  # 跳过的行数
+        self.csvFileNameSplitStringForTurbine = None  # 自文件名中获取机组号的分隔符
+        self.index_turbine = None  # 自文件名中获取机组号的索引
+        self.filter = None
+
+        self.output_path = None
+
+        self.start_time_str = None
+        self.end_time_str = None
+
+        # 将字符串转换为 pd.Timestamp 类型
+        self.start_time = None
+        self.end_time = None
+        self.excludingMonths = None  # 排除指定的月份数据 格式%Y-%m
+
+        self.field_turbine_time = None    # 字段名 时间
+        self.field_turbine_name = None    # 字段名 机组名
+
+        self.field_wind_speed = None      # 字段名 风速
+        self.field_power = None           # 字段名 有功功率
+        self.field_pitch_angle1 = None    # 字段名 桨距角1
+        self.field_pitch_angle2 = None    # 字段名 桨距角2
+        self.field_pitch_angle3 = None    # 字段名 桨距角3
+        self.field_turbine_state = None   # 字段名 风机状态
+        self.field_gen_speed = None       # 字段名 发电机转速
+        self.value_gen_speed_multiple = None  # 值 发电机转速放大倍数
+        self.value_gen_speed_step = None            # 值 发电机转速轴系间隔
+        self.value_gen_speed_min = None       # 值 发电机转速最小
+        self.value_gen_speed_max = None       # 值 发电机转速最大
+        self.field_rotor_speed = None     # 字段名 叶轮转速
+        self.field_torque = None          # 字段名 转矩
+        self.field_wind_dir = None        # 字段名 风向
+        self.field_angle_included = None
+        self.field_nacelle_pos = None     # 字段名 机舱温度
+        self.field_env_temp = None        # 字段名 环境温度
+        self.field_nacelle_temp = None    # 字段名 机舱温度
+        self.field_temperature_large_components = None  # 字段名列表  大部件温度传感器
+        self.temperature_Generator = None
+        self.graphSets = None
+        self.field_activePowerSet = None
+        self.field_activePowerAvailable = None
+        self.rated_cut_in_windspeed = None  #额定切入风速
+        self.rated_cut_out_windspeed = None #额定切出风速
+
+    def loadConfig(self, jsonFilePath, charset=charset_unify):
+        """
+        配置初始化
+        """
+        # # 使用global声明,表示我们要修改的是全局变量config_data
+        # global farm_name
+
+        # 将配置数据存储在变量中
+        configData = JsonUtil.read_json(jsonFilePath)
+
+        self.farm_name = configData['name_PowerFarm']
+        self.rated_power = configData['rated_Power_Turbine_Unit_kW']
+        self.rated_WindSpeed = configData["rated_WindSpeed"]
+        self.rated_cut_in_windspeed = configData["rated_cut_in_windspeed"]  #额定切入风速
+        self.rated_cut_out_windspeed = configData["rated_cut_out_windspeed"]#额定切出风速
+
+        self.rotor_diameter = configData['rotor_diameter']
+        self.rotational_Speed_Ratio = configData['rotational_Speed_Ratio']
+        self.density_air = configData['density_air']
+
+        self.type_name = configData['name_Type_For_Analysis']
+        # 时间间隔,单位是秒
+        self.time_period = configData['time_Period_Unit_Second']
+
+        self.output_name = configData['name_Output']
+        self.output_prefix = configData['outputFileDirectory']
+
+        self.turbineInfoFilePathCSV = configData["turbineInfoFilePathCSV"]
+        self.turbineGuaranteedPowerCurveFilePathCSV = configData[
+            "turbineGuaranteedPowerCurveFilePathCSV"]
+        self.input_path = configData['inputFileDirectoryByCSV']
+        self.csvFileNameSplitStringForTurbine = configData["csvFileNameSplitStringForTurbine"]
+        self.index_turbine = configData["index_turbine"]
+        self.skip_row_number = configData['skip_row_number']
+        self.filter = configData['filter']
+
+        self.output_path = self.output_prefix + \
+            r"/{}".format(self.farm_name)
+
+        # start_time_str = '{} 00:00:00'.format(configData['date_Begin'])
+        # end_time_str = '{} 23:59:59'.format(configData['date_End'])
+        self.start_time_str = configData['date_Begin']
+        self.end_time_str = configData['date_End']
+
+        # 将字符串转换为 pd.Timestamp 类型
+        self.start_time = pd.to_datetime(
+            self.start_time_str, format='%Y-%m-%d %H:%M:%S')
+        self.end_time = pd.to_datetime(
+            self.end_time_str, format='%Y-%m-%d %H:%M:%S')
+        self.excludingMonths = configData['excludingMonths']
+
+        self.field_turbine_time = configData['turbine_Time']
+        self.field_turbine_name = configData['turbine_Name']
+
+        self.field_wind_speed = configData['speed_Wind']
+        self.field_power = configData['power_Active']
+        self.field_pitch_angle1 = configData['pitch_Angle1']
+        self.field_pitch_angle2 = configData['pitch_Angle2']
+        self.field_pitch_angle3 = configData['pitch_Angle3']
+        self.field_turbine_state = configData['state_Turbine']
+        self.field_gen_speed = configData['speed_Generator']
+        # self.value_gen_speed_multiple = configData['speed_Generator_Multiple']
+        # self.value_gen_speed_step = configData['speed_Generator_Step']
+        # self.value_gen_speed_min = configData['speed_Generato_min']
+        # self.value_gen_speed_max = configData['speed_Generato_max']
+        self.field_rotor_speed = configData['speed_Rotor']
+        self.field_torque = configData['torque']
+        self.field_wind_dir = configData['direction_Wind']
+        self.field_angle_included = configData['angle_included']
+        self.field_nacelle_pos = configData['nacelle_Pos']
+        self.field_env_temp = configData['temperature_Env']
+        self.field_nacelle_temp = configData['temperature_Nacelle']
+        self.field_temperature_large_components = configData['temperature_large_components']
+        self.temperature_Generator = configData["temperature_Generator"]
+        self.graphSets = configData["graphSets"]
+        self.field_Cabin_Vibrate_X = configData["Cabin_Vibrate_X"]
+        self.field_Cabin_Vibrate_Y = configData["Cabin_Vibrate_Y"]
+        self.field_activePowerSet = configData["activePowerSet"]
+        self.field_activePowerAvailable = configData["activePowerAvailable"]
+
+        return self
+
+    # def add_W_if_starts_with_digit(self,s):
+    #     if s and s[0].isdigit():
+    #         return 'W' + s
+    #     return s
+
+    # 定义一个函数,用于检查字符串首字母是否为数字,并在是的情况下添加'W'
+    def add_W_if_starts_with_digit(self, s):
+        if isinstance(s, str) and s[0].isdigit():
+            return 'W' + s
+        return s

+ 88 - 0
dataContract/algorithmContract/dataContract.json

@@ -0,0 +1,88 @@
+{
+	"dataContractType": {
+		"dataContractType": "analysisExecuteOrder",
+		"version": "1.0.0"
+	},
+	"dataContract": {
+		"dataSource": {
+			"scada": "minute"
+		},
+		"dataFilter": {
+			"powerFarmID": "",
+			"turbines": [
+				{
+					"dataBatchNum": "B2024042211-0"
+				},
+				{
+					"dataBatchNum": "B2024042211-0"
+				},
+				{
+					"dataBatchNum": "B2024042211-1"
+				}
+			],
+			"beginTime": "2023-01-01 00:00:00",
+			"endTime": "2023-12-31 23:59:59",
+			"excludingMonths": [
+				"2023-12",
+				"2023-09"
+			],
+			"customFilter": {
+				"valueWindSpeed": {
+					"min": 3.0,
+					"max": 25.0
+				},
+				"valuePitchAngle": {
+					"min": 2,
+					"max": null
+				},
+				"valueActivePower": {
+					"min": 10,
+					"max": 2500
+				},
+                "valueGeneratorSpeed": {
+                    "min": 10,
+                    "max": 2500
+                }
+			}
+		},
+		"configAnalysis": [
+            {
+                "package": "algorithm.windSpeedFrequencyAnalyst",
+                "className": "WindSpeedFrequencyAnalyst",
+                "methodName": "executeAnalysis"
+            },
+			{
+				"package": "algorithm.generatorSpeedPowerAnalyst",
+				"className": "GeneratorSpeedPowerAnalyst",
+				"methodName": "executeAnalysis"
+			}
+		],
+		"graphSets": {
+			"generatorSpeed": {
+				"step": 200,
+				"min": 1000,
+				"max": 2000
+			},
+			"generatorTorque": {
+				"step": 2000,
+				"min": 0,
+				"max": 12000
+			},
+			"cp": {
+				"step": 0.5,
+				"min": 0,
+				"max": 2
+			},
+			"tsr": {
+				"step": 5,
+				"min": 0,
+				"max": 30
+			},
+			"pitchAngle": {
+				"step": 1,
+				"min": -1,
+				"max": 20
+			}
+		}
+	}
+}

+ 9 - 0
dataContract/setup.py

@@ -0,0 +1,9 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='dataContract',
+    version='1.0.202403180918',
+    description='Data Contract Package', # 描述信息
+    author='Xie Zhou Yang', # 作者
+    packages=find_packages()
+)

+ 0 - 0
mydatabase.db


+ 4 - 0
repositoryZN/__init__.py

@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import utils
+__all__=['utils']

+ 9 - 0
repositoryZN/setup.py

@@ -0,0 +1,9 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='repositoryZN',
+    version='1.0.202403180918',
+    description='Repository Package', # 描述信息
+    author='Xie Zhou Yang', # 作者
+    packages=find_packages()
+)

+ 0 - 0
repositoryZN/utils/__init__.py


+ 57 - 0
repositoryZN/utils/csvFileUtil.py

@@ -0,0 +1,57 @@
+import pandas as pd
+
+class CSVFileUtil:
+    def __init__(self, filepath):
+        """
+        初始化CSV工具类
+        :param filepath: CSV文件的路径
+        """
+        self.filepath = filepath
+
+    def read_csv(self):
+        """
+        读取CSV文件
+        :return: 返回DataFrame对象
+        """
+        return pd.read_csv(self.filepath)
+    
+    def read_csv_columns(self,useColumns):
+        """
+        读取CSV文件
+        :return: 返回DataFrame对象
+        """
+        return pd.read_csv(self.filepath,usecols=useColumns)
+
+    def write_csv(self, data, dest_path):
+        """
+        将数据写入CSV文件
+        :param data: DataFrame对象或可转换为DataFrame的数据
+        :param dest_path: 保存CSV文件的路径
+        """
+        if not isinstance(data, pd.DataFrame):
+            data = pd.DataFrame(data)
+        data.to_csv(dest_path, index=False)
+
+    def select_and_save_columns(self, columns, dest_path):
+        """
+        选取指定的列并保存为新的CSV文件
+        :param columns: 要选取的列名列表
+        :param dest_path: 保存新CSV文件的路径
+        """
+        df = self.read_csv_columns(columns)
+        self.write_csv(df, dest_path)
+
+# 使用示例
+if __name__ == "__main__":
+    filepath = '/path/to/your/original.csv'  # 替换为你的CSV文件路径
+    dest_path = '/path/to/your/selected_columns.csv'  # 替换为你想保存选取列后的CSV文件路径
+    columns = ['时间','iTempOutdoor_1sec','iTempNacelle_1sec','iGenPower','iRotorSpeedPDM','iGenSpeed','iWindSpeed_real',
+               'iPitchAngle1','iPitchAngle2','iPitchAngle3','iNacellePositionTotal','iwindDirection','iVaneDiiection',
+               'iActivePoweiSetPointValue','iReactivePower','iKWhThisDay_h','iVibrationY','iVibrationZ','iTemp1GearOil_1sec',
+               'iTempRotorBearA_1sec','iTempGearBearNDE_1sec','iTempGearBearDE_1sec','iTempGenBearDE_1sec','iTempGenBearNDE_1sec',
+               'iTempGenStatorU_1sec','iTempHub_1sec','iTempCntr_1sec','WT_Runcode','WT_Faultcode','iTempRotorBearA_1sec']  # 替换为你想选取的列名
+
+    tool = CSVFileUtil(filepath)
+    tool.select_and_save_columns(columns, dest_path)
+
+

+ 89 - 0
repositoryZN/utils/directoryUtil.py

@@ -0,0 +1,89 @@
+import os
+import shutil
+
+class DirectoryUtil:
+    @staticmethod
+    def create_directory(path):
+        """
+        创建一个新目录。如果目录已存在,则不执行任何操作。
+        :param path: 要创建的目录路径
+        """
+        try:
+            os.makedirs(path, exist_ok=True)
+            print(f"Directory '{path}' created successfully.")
+        except OSError as error:
+            print(f"Creating directory '{path}' failed. Error: {error}")
+
+    @staticmethod
+    def delete_directory(path):
+        """
+        删除一个目录及其所有内容。
+        :param path: 要删除的目录路径
+        """
+        try:
+            shutil.rmtree(path)
+            print(f"Directory '{path}' deleted successfully.")
+        except OSError as error:
+            print(f"Deleting directory '{path}' failed. Error: {error}")
+
+    @staticmethod
+    def list_directory_contents(path):
+        """
+        列出目录中的所有文件和子目录。
+        :param path: 目录路径
+        :return: 目录内容的列表
+        """
+        try:
+            contents = os.listdir(path)
+            print(f"Contents of directory '{path}': {contents}")
+            return contents
+        except OSError as error:
+            print(f"Listing contents of directory '{path}' failed. Error: {error}")
+            return []
+        
+    @staticmethod
+    def list_directory(path):
+        """
+        列出目录中的所有文件和子目录。
+        :param path: 目录路径
+        :return: 指定目录、子目录列表、文件列表
+        """
+        try:
+            return os.walk(path)
+        except OSError as error:
+            print(f"Listing contents of directory '{path}' failed. Error: {error}")
+            return path,None,None
+
+    @staticmethod
+    def check_directory_exists(path):
+        """
+        检查一个目录是否存在。
+        :param path: 目录路径
+        :return: 如果目录存在则返回True,否则返回False
+        """
+        return os.path.exists(path) and os.path.isdir(path)
+
+# 使用示例
+if __name__ == "__main__":
+    path = 'E:\\BaiduNetdiskDownload\\merge1'  # 替换为你的目录路径
+
+    # 创建目录
+    DirectoryUtil.create_directory(path)
+
+    # 检查目录是否存在
+    if DirectoryUtil.check_directory_exists(path):
+        print(f"Directory '{path}' exists.")
+
+    # 列出目录内容
+    DirectoryUtil.list_directory_contents(path)
+
+    results=DirectoryUtil.list_directory(path)
+
+    for root,dirs,files in results:
+        print(root)
+        print(dirs)
+        print(files)
+
+    # 删除目录
+    # 注意:这将删除目录及其所有内容,请谨慎操作!
+    # DirectoryTool.delete_directory(path)

+ 37 - 0
repositoryZN/utils/jsonUtil.py

@@ -0,0 +1,37 @@
+import json
+
+class JsonUtil:
+    @staticmethod
+    def read_json(file_path, encoding='utf-8'):
+        """读取JSON文件"""
+        try:
+            with open(file_path, 'r', encoding=encoding) as f:
+                return json.load(f)
+        except FileNotFoundError:
+            print("文件不存在")
+            return None
+
+    @staticmethod
+    def write_json(data, file_path, encoding='utf-8'):
+        """写入JSON到文件"""
+        with open(file_path, 'w', encoding=encoding) as f:
+            json.dump(data, f, ensure_ascii=False, indent=4)
+
+    @staticmethod
+    def update_json(file_path, updates, encoding='utf-8'):
+        """更新JSON文件"""
+        data = JsonUtil.read_json(file_path, encoding)
+        if data is not None:
+            data.update(updates)
+            JsonUtil.write_json(data, file_path, encoding)
+
+    @staticmethod
+    def delete_key(file_path, key, encoding='utf-8'):
+        """从JSON文件删除特定键"""
+        data = JsonUtil.read_json(file_path, encoding)
+        if data is not None:
+            if key in data:
+                del data[key]
+                JsonUtil.write_json(data, file_path, encoding)
+            else:
+                print(f"键 '{key}' 不存在。")

+ 0 - 0
wtoaamapi/__init__.py


+ 0 - 0
wtoaamapi/apps/__init__.py


+ 3 - 0
wtoaamapi/apps/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
wtoaamapi/apps/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AppsConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps'

+ 0 - 0
wtoaamapi/apps/business/__init__.py


+ 91 - 0
wtoaamapi/apps/business/main.py

@@ -0,0 +1,91 @@
+import pandas as pd
+import importlib
+import concurrent.futures
+from utils.jsonUtil import JsonUtil
+from algorithmContract.confBusiness import ConfBusiness
+from algorithm.dataProcessor import DataProcessor
+from behavior.baseAnalyst import BaseAnalyst
+from behavior.analyst import Analyst
+import seaborn as sns
+sns.set_style("darkgrid") 
+
+def buildDynamicInstance(module_path, class_name, confData: ConfBusiness):
+    # 动态导入模块
+    module = importlib.import_module(module_path)
+    # 获取类
+    cls = getattr(module, class_name)
+    # 创建类实例
+    instance = cls(confData)
+
+    return instance
+
+def dynamic_instance_and_call(module_path, class_name, method_name, *args, **kwargs):
+    # 动态导入模块
+    module = importlib.import_module(module_path)
+    # 获取类
+    cls = getattr(module, class_name)
+    # 创建类实例
+    instance = cls()
+    # 获取方法
+    method = getattr(instance, method_name)
+    # 调用方法
+    method(*args, **kwargs)
+
+
+def loadJson(filePath):
+    # 打开并读取JSON文件
+    with open(filePath, 'r') as f:
+        data = JsonUtil.read_json(filePath)
+
+    return data
+
+def executeAnalysis(config):
+    configBusinessFilePath=config["configFilePath"]
+    print(configBusinessFilePath)
+    businessConfig=ConfBusiness()
+    configBusiness = businessConfig.loadConfig(configBusinessFilePath)
+
+    baseAnalysts=[]
+    noCustomFilterAnalysts = []
+    analysts = []
+
+    for dynamicAnalyst in config["configAnalysis"]:
+        package = dynamicAnalyst["package"]
+        className = dynamicAnalyst["className"]
+        methodName = dynamicAnalyst["methodName"]
+        analyst = buildDynamicInstance(package, className, configBusiness)
+
+        analysts.append(analyst)
+                   
+    process = DataProcessor()
+
+    for analyst in analysts:
+        process.attach(analyst)
+    process.execute(configBusiness)
+
+    for analyst in analysts:
+        process.detach(analyst)
+
+
+if __name__ == "__main__":
+    configs = loadJson(r"conf/conf.json")
+
+    for config in configs:
+        executeAnalysis(config)
+
+    # 使用多线程时,matplotlib绘图报错
+    # 在使用Python语言的matplotlib包时,如果尝试在多线程环境中创建图形界面,你可能会遇到“QWidget-: Must construct a QApplication before a QWidget”的错误。
+    # 这个错误通常是因为matplotlib的图形后端(backend)依赖于Qt框架,而Qt框架要求在一个QApplication实例被创建之后再创建任何QWidget对象。
+    # 在多线程环境中,每个线程都应该有一个它自己的事件循环。然而,QApplication实例通常是全局的,并且应该只在主线程中创建一次。
+    # 如果你尝试在一个非主线程中创建QApplication或者QWidget,就会遇到这个问题。
+    
+    # with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
+    #     futures=[executor.submit(executeAnalysis, config) for config in configs]        
+
+    #     for future in concurrent.futures.as_completed(futures):
+    #         try:
+    #             result = future.result()
+    #         except Exception as exc:
+    #             print(f'生成异常: {exc}')
+    #         else:
+    #             print(f'任务返回结果: {result}')

+ 50 - 0
wtoaamapi/apps/business/test.py

@@ -0,0 +1,50 @@
+import pandas as pd  
+import plotly.graph_objects as go  
+from plotly.subplots import make_subplots  
+  
+# 假设你的DataFrame叫做df,并且已经包含了所需字段  
+# 如果你的数据是CSV文件,可以使用pd.read_csv('your_file.csv')来加载数据  
+# df = pd.read_csv('your_file.csv')  
+  
+# 示例数据  
+data = {  
+    '机组名': ['机组A', '机组B', '机组C', '机组D'],  
+    '时间': ['2024-01-09 09:13:29', '2024-01-10 10:14:30', '2024-02-09 08:13:29', '2024-02-10 09:14:30'],  
+    '年月': ['2024-01', '2024-01', '2024-02', '2024-02'],  
+    '风速': [5.0, 6.0, 4.5, 5.5],  
+    '有功功率': [1000, 1200, 900, 1100]  
+}  
+  
+df = pd.DataFrame(data)  
+  
+# 创建颜色映射,将每个年月映射到一个唯一的颜色  
+unique_months = df['年月'].unique()  
+colors = [f'rgb({i}, {150 - i}, 50)' for i in range(len(unique_months))]  
+color_map = dict(zip(unique_months, colors))  
+  
+# 使用make_subplots创建3D散点图  
+fig = make_subplots(rows=1, cols=1, specs=[[{"type": "scatter3d"}]])  
+  
+# 遍历DataFrame的每一行,为每个点添加数据  
+for index, row in df.iterrows():  
+    x = row['风速']  
+    y = row['年月']  
+    z = row['有功功率']  
+    color = color_map[y]  
+      
+    # 添加散点到子图  
+    fig.add_trace(go.Scatter3d(x=[x], y=[y], z=[z], mode='markers', marker=dict(color=color)), row=1, col=1)  
+  
+# 更新子图的布局,设置y轴为category类型,并设置其类别顺序  
+fig.update_layout(  
+    title='3D散点图:风速、年月与有功功率',  
+    margin=dict(l=0, r=0, b=0, t=0),  
+    scene=dict(  
+        xaxis=dict(title='风速'),  
+        yaxis=dict(title='年月', tickmode='array', tickvals=unique_months, ticktext=unique_months, categoryorder='category ascending'),  
+        zaxis=dict(title='有功功率')  
+    )  
+)  
+  
+# 显示图形  
+fig.show()

+ 22 - 0
wtoaamapi/apps/migrations/0001_initial.py

@@ -0,0 +1,22 @@
+# Generated by Django 4.2.10 on 2024-02-25 02:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Item',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100)),
+                ('description', models.TextField()),
+            ],
+        ),
+    ]

+ 0 - 0
wtoaamapi/apps/migrations/__init__.py


+ 10 - 0
wtoaamapi/apps/models.py

@@ -0,0 +1,10 @@
+from django.db import models
+
+# Create your models here.
+
+class Category(models.Model):
+    name = models.CharField(max_length=100)
+
+class Product(models.Model):
+    name = models.CharField(max_length=100)
+    category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE)

+ 12 - 0
wtoaamapi/apps/serializers.py

@@ -0,0 +1,12 @@
+from rest_framework import serializers
+from .models import *
+
+class CategorySerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Category
+        fields = '__all__'
+
+class ProductSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Product
+        fields = '__all__'

+ 3 - 0
wtoaamapi/apps/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 10 - 0
wtoaamapi/apps/urls.py

@@ -0,0 +1,10 @@
+from django.urls import path
+
+# 业务包文件
+from .viewDemo import viewUser,viewBook
+
+urlpatterns = [
+    path('user/list', viewUser.User.as_view({'post': 'list'})),
+    path('book/list', viewBook.Book.as_view({'get': 'list'})),
+    path('book/find', viewBook.Book.as_view({'get': 'find'})),
+]

+ 0 - 0
wtoaamapi/apps/viewDemo/__init__.py


+ 34 - 0
wtoaamapi/apps/viewDemo/viewBook.py

@@ -0,0 +1,34 @@
+
+# -*- coding: utf-8 -*-
+from drf_yasg import openapi
+from drf_yasg.utils import swagger_auto_schema
+from rest_framework.viewsets import ViewSet
+from django.http import HttpResponse
+ 
+ 
+tags = ['book']
+ 
+class Book(ViewSet):
+    @swagger_auto_schema(
+        operation_description="apps get description override",
+        manual_parameters=[
+            #  openapi.Parameter('cart_id', in_=openapi.IN_QUERY,
+            #                    type=openapi.TYPE_INTEGER)
+         ],
+        security=[], tags=["Demo"], operation_summary='列表')
+    def list(self, request):
+        return HttpResponse('Hello! Books.')
+    
+    @swagger_auto_schema(
+        operation_description="apps get description override",
+        manual_parameters=[
+             openapi.Parameter('book_id', in_=openapi.IN_QUERY,
+                               type=openapi.TYPE_STRING),
+             openapi.Parameter('book_name', in_=openapi.IN_QUERY,
+                               type=openapi.TYPE_STRING),
+         ],
+        security=[], tags=["Demo"], operation_summary='查找')
+    def find(self, request):
+        print("get_get",request.GET)
+        print("get_body",request.body)
+        return HttpResponse('find... by book id : %s ,name : %s' % (request.GET['book_id'],request.GET['book_name']))

+ 34 - 0
wtoaamapi/apps/viewDemo/viewUser.py

@@ -0,0 +1,34 @@
+
+# -*- coding: utf-8 -*-
+from drf_yasg import openapi
+from drf_yasg.utils import swagger_auto_schema
+from rest_framework.viewsets import ViewSet
+from django.http import HttpResponse
+ 
+ 
+tags = ['user']
+ 
+class User(ViewSet):
+    @swagger_auto_schema(
+        operation_description="apiview post description override",
+        request_body=openapi.Schema(
+            type=openapi.TYPE_OBJECT,
+            required=['page', 'pageSize'],
+            properties={
+                'page': openapi.Schema(type=openapi.TYPE_NUMBER),
+                'pageSize': openapi.Schema(type=openapi.TYPE_NUMBER),
+            },
+        ),
+        security=[], tags=tags, operation_summary='列表')
+    def list(self, request):
+        print('debug')
+        msg=''
+        try:
+        #    print("post_post",request.POST)
+           print("post_body",request.body) 
+           msg='Hello! Users. body : %s' % (request.body)      
+        except(RuntimeError, TypeError, NameError) as e:
+            print(e)
+            msg='Hello! Users. exception'
+        return HttpResponse(msg)
+        # pass

+ 26 - 0
wtoaamapi/apps/views.py

@@ -0,0 +1,26 @@
+from django.shortcuts import render
+
+# Create your views here.
+from rest_framework import viewsets
+from drf_yasg.utils import swagger_auto_schema
+from .models import *
+from .serializers import *
+
+class CategoryViewSet(viewsets.ModelViewSet):
+    queryset = Category.objects.all()
+    serializer_class = CategorySerializer
+    
+    # 应用于整个视图集的动作
+    @swagger_auto_schema(tags=['Categories'])
+    def get_serializer_class(self):
+        return super().get_serializer_class()
+
+
+class ProductViewSet(viewsets.ModelViewSet):
+    queryset = Product.objects.all()
+    serializer_class = ProductSerializer
+
+    # 应用于整个视图集的动作
+    @swagger_auto_schema(tags=['Products'])
+    def get_serializer_class(self):
+        return super().get_serializer_class()

+ 13 - 0
wtoaamapi/config/swagger.py

@@ -0,0 +1,13 @@
+from drf_yasg.inspectors import SwaggerAutoSchema
+
+class CustomSwaggerAutoSchema(SwaggerAutoSchema):
+    def get_tags(self, operation_keys=None):
+        operation_keys = operation_keys or self.operation_keys
+
+        tags = self.overrides.get('tags')
+        if not tags:
+            tags = [operation_keys[0]]
+        if hasattr(self.view, "swagger_tags"):
+            tags = self.view.swagger_tags
+
+        return tags

BIN
wtoaamapi/db.sqlite3


+ 22 - 0
wtoaamapi/manage.py

@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    """Run administrative tasks."""
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wtoaamapi.settings')
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()

+ 41 - 0
wtoaamapi/testListen.py

@@ -0,0 +1,41 @@
+import socket  
+  
+def start_server(port):  
+    # 创建一个socket对象  
+    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
+      
+    # 绑定到指定的IP地址和端口  
+    # 如果你想监听所有可用的网络接口,可以使用空字符串('')作为IP地址  
+    server_address = ('', port)  
+    print(f'Starting up on {server_address[1]}')  
+    server_socket.bind(server_address)  
+      
+    # 开始监听连接  
+    server_socket.listen(1)  
+      
+    while True:  
+        # 等待客户端连接  
+        print('Waiting for a connection')  
+        connection, client_address = server_socket.accept()  
+          
+        try:  
+            print(f'Connection from {client_address}')  
+              
+            # 接收数据,最多1024字节  
+            while True:  
+                data = connection.recv(1024)  
+                print(f'Received {len(data)} bytes from {client_address}')  
+                  
+                if not data:  
+                    break  
+                  
+                # 向客户端发送数据  
+                connection.sendall(data)  
+                  
+        finally:  
+            # 清理连接  
+            connection.close()  
+  
+if __name__ == '__main__':  
+    port = 8123  # 你可以更改为你想要的端口号  
+    start_server(port)

+ 0 - 0
wtoaamapi/wtoaamapi/__init__.py


+ 16 - 0
wtoaamapi/wtoaamapi/asgi.py

@@ -0,0 +1,16 @@
+"""
+ASGI config for wtoaamapi project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wtoaamapi.settings')
+
+application = get_asgi_application()

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff