风电机组 SCADA 功率曲线数据清洗工具:用「迭代分箱 + MAD(中位数绝对偏差)」把正常运行数据从原始 SCADA 数据中初筛出来。
输入一台风机的 SCADA 数据(至少要含风速、有功功率两列,通常为 10 分钟统计记录)。
PowerCurveFiltering.process() 会把数据按行切分成两份返回:
normal_df:风机正常运行的数据点——功率落在该风速区间应有的功率带内。后续做功率曲线建模、工况分析、机器学习等时主要用这一份。abnormal_df:其余所有被剔除的点。⚠️ 它不是"严格异常集",而是混了多种情况:停机、故障、限电/降载、桨距控制等其它工况、以及真正的传感器/数据异常。想区分异常类型,需要后续用更高阶方法(如 GAM/sigmoid 拟合、时间维度分析)处理。
想识别限电/降载/停机成片出现、单点看却落在正常带内的"隐藏异常",以及逐点打上
normal/abnormal标签并区分异常类型,请用 DailyAnomalyDetector(v2,时间维度 + 基准曲线对比)。PowerCurveFiltering(v1)是无模型的 bin+MAD 粗洗,适合作为对照或初筛。
单台风机的整条清洗流水线分三步:
- 去停机:风速 ≥ 切入风速、但功率 ≤ 1 kW 的点(风机已停机)→ 剔除。
- 高风低功率粗洗:风速 ≥ 额定风速、但功率 <
low_power_ratio × 额定功率的点(限电、降载、桨距/故障等非正常发电)→ 剔除。 - 迭代 bin+MAD 精洗:按
bin_interval把风速分箱,对每箱算功率中位数与 MAD,箱内功率落在中位数 ± z_coeff × MAD带外的点 → 剔除。剔除后重新分箱再剔,重复filter_cycle轮(每轮带会随数据变干净而收窄)。
几个内置的判定约定(避免误杀正常点):
- 箱内样本 < 3,或功率离散度为 0(典型如额定功率平台),该箱不判定、直接保留;
- 风速低于切入风速的点默认保留(近零功率属正常待机);
- 恒定功率的"水平聚集"类异常(如某段时间被固定在某一功率)bin+MAD 识别不出来,这是本方法的能力上限,应交给后续更精细的方案。
核心思路(来自 2020 国家电投"绿动未来"大赛 top1 方案):降维 + 对比。先把单台风机的正常点压成一条基准功率曲线,再按天(或未来按小时)把每天的运行段压缩成"中位残差",与整机曲线对比——某天整体偏移明显,就把该天判为异常,并给逐点打标签。它比纯 bin+MAD 能抓到水平聚集 / 限电 / 降载这类单点统计标不出的异常。
import pandas as pd
from wtclean import DailyAnomalyDetector
df = pd.read_csv("data.csv")
turbine = df[df["Wind_turbine_name"] == "R80721"] # 单台风机
detector = DailyAnomalyDetector(
windspeed_label="Ws_avg", # 风速列名
power_label="P_avg", # 有功功率列名
timestamp_label="Date_time", # 时间戳列名(datetime 或可解析字符串)
df=turbine, # 单台风机数据
# —— 以下物理参数缺省时会从 SCADA 自动反推(见下)——
cut_in_speed=3.5, # 切入风速 m/s
rated_wind_speed=14.5, # 额定风速 m/s
rated_power=2050, # 额定功率 kW
cut_out_speed=25.0, # 切出风速 m/s(可选;给定后切出以上近零功率视为正常停机)
)
normal_df, abnormal_df = detector.detect()
detector.labels_ # pd.Series:每行一个标签
detector.abnormal_days_ # set:被判异常的天(日期)
detector.removal_counts_ # dict:各类异常点数统计detect() 返回 (normal_df, abnormal_df) 两个 DataFrame,按行切分原始 df(normal_df + abnormal_df == 原始 df,且索引不交叠)。每个点的异常类型看 detector.labels_:
| 标签 | 含义 |
|---|---|
normal |
正常 |
nan |
缺失:风速或功率为 NaN |
range |
越界:功率/风速为负,或功率 > 1.2×额定功率(切出风速以上功率非零也算) |
downtime |
停机:切入风速以上、功率却近零(≤ downtime_power) |
curtailment |
限电:额定风速以上、功率 < low_power_ratio×额定功率(高风低功率) |
temporal |
时间维度异常:整天的运行段整体偏离基准曲线(限电/降载/停机方向) |
residual |
逐点残差异常:单点相对基准曲线的残差超出该风速 bin 的 z_coeff×MAD 带 |
优先级:粗洗四类(nan/range/downtime/curtailment)> temporal > residual > normal,即时间维度与残差只作用于尚未被粗洗标异常的点。
- 粗洗掩码:
nan/range/downtime/curtailment直接判异常,不参与后续建模。 - 整机基准曲线:用
base_method(默认gam单调 GAM,也可logistic_5_parametric/iec)在粗洗后的点上拟合。 - 残差:
resid = power - base(wind_speed)。 - 天级判定:每天"切入风速以上"点的中位残差 = 该天系统性偏移;跨天用
median + day_z_coeff×MAD自适应阈值标异常天(默认只标"低于曲线"方向)。 - 逐点残差 bin-MAD 精洗:按
bin_width风速分箱,对残差做z_coeff×MAD带外判定(异方差)。 - 输出
(normal_df, abnormal_df)。
| 参数 | 默认 | 含义 |
|---|---|---|
windspeed_label / power_label / timestamp_label |
— | 风速/功率/时间戳列名(必填)。 |
df |
— | 单台风机的 SCADA DataFrame(必填)。 |
cut_in_speed / rated_wind_speed / rated_power |
自动反推 | 厂商物理参数;缺省时调用 estimate_machine_parameters 反推。 |
cut_out_speed |
None |
切出风速 m/s;给定后切出风速以上功率归零视为正常停机,未给则把高风零功率当停机异常。 |
low_power_ratio |
0.9 |
限电粗洗阈值:风速 ≥ 额定风速时功率 < 该比例×额定功率即判限电。 |
downtime_power |
1.0 |
功率 ≤ 该值(kW)且风速在切入以上视为停机。 |
bin_width |
0.5 |
残差分箱宽度 m/s。 |
min_day_points |
10 |
参与天级判定的最少样本数(每天约 144 点,正常数据下几乎不触发)。 |
min_bin_points |
3 |
残差 bin 参与判定的最少样本数(同上,兜底用)。 |
z_coeff |
3.5 |
逐点残差 MAD 倍数,控制残差精洗的误杀率:越大保留越多、误杀越少。 |
day_z_coeff |
2.5 |
天级分数 MAD 倍数,控制"整天判异常"的松紧;真实数据上天级偏移无天然分界、标出天数随该值平滑变化,建议按站点校准。 |
base_method |
"gam" |
基准曲线后端:gam(默认,单调 GAM)/ logistic_5_parametric / iec。 |
**base_kwargs |
— | 透传给 fit_power_curve 的额外参数。 |
z_coeff:控制逐点残差精洗的误杀率。合成数据 + 真实数据(La Haute Borne R80721)一致显示2.5偏激进(残差点占比约 9.6%),故默认3.5(约 4%);若更看重"少动正常点"可放宽到4.0(约 3%)。day_z_coeff:不是稳健旋钮,需按站点校准。合成数据(正常天无系统偏移)上 2.0–6.0 结果一致,但真实数据(La Haute Borne R80721)上天级中位残差呈重尾连续分布(中心 0 kW、MAD 12 kW、尾部 -30~-68 kW),标出的异常天数随它平滑变化:2.0→38 天、2.5→23 天、3.0→14 天、4.0→2 天。建议结合已知干净时段或可视化 QC 选定,不要指望换个站点仍适用。base_method:默认gam带单调约束 + 网格搜样条数,无需调;logistic_5_parametric的默认 bounds 假设 1.2–1.8 MW,其它机型需显式传 bounds。- 其它参数(
bin_width/min_day_points/min_bin_points)对结果不敏感,正常数据量下可保持默认。
git clone <this-repo>
cd wtclean
pip install -r requirements.txt
pip install .import pandas as pd
from wtclean import PowerCurveFiltering
df = pd.read_csv("data.csv")
turbine = df[df["Wind_turbine_name"] == "R80721"] # 取单台风机
pc_filter = PowerCurveFiltering(
windspeed_label="Ws_avg", # 风速列名
power_label="P_avg", # 有功功率列名
df=turbine, # 单台风机数据
cut_in_speed=3.5, # 切入风速(厂商参数,必填)
rated_wind_speed=14.5, # 额定风速(厂商参数,必填)
rated_power=2050, # 额定功率 kW(厂商参数,必填)
low_power_ratio=0.9, # 高风低功率粗洗阈值(默认 0.9)
bin_interval=0.5, # 风速分箱宽度(默认 0.5)
z_coeff=2.5, # 正常带宽度系数(默认 2.5)
filter_cycle=3, # 迭代轮数(默认 3)
return_fig=False, # 是否保存功率曲线图
image_path="", # 出图时的完整输出文件路径
)
normal_df, abnormal_df = pc_filter.process()运行时会逐轮打印删除情况,例如:
iteration 1: removed 3864 (7.21%), remaining 49724
iteration 2: removed 1655 (3.33%), remaining 48069
iteration 3: removed 904 (1.88%), remaining 47165
每轮删除数也会记录在 pc_filter.removal_history(list[int]),方便你审计或画收敛曲线。
PowerCurveFiltering 一次只处理一台;多台由调用方 groupby 后循环调用:
from wtclean import PowerCurveFiltering, estimate_machine_parameters
results = {}
for name, group in df.groupby("Wind_turbine_name"):
params = estimate_machine_parameters(group, "Ws_avg", "P_avg") # 先反推厂商参数
pcf = PowerCurveFiltering(
"Ws_avg", "P_avg", group,
cut_in_speed=params["cut_in_speed"],
rated_wind_speed=params["rated_wind_speed"],
rated_power=params["rated_power"],
)
results[name] = pcf.process() # (normal_df, abnormal_df)cut_in_speed / rated_wind_speed / rated_power 是风机机型物理参数,必填。若拿不到厂商技术参数,可先从 SCADA 数据反推(estimate_machine_parameters 会估计全部三个值),再把结果传给构造函数:
from wtclean import estimate_machine_parameters
params = estimate_machine_parameters(turbine, "Ws_avg", "P_avg")
# -> {"cut_in_speed": 3.78, "rated_wind_speed": 13.59, "rated_power": 1985.3}注意:反推值来自(通常是 10 分钟统计的)SCADA 数据,是"软"值——例如反推的额定风速约 13 m/s,会低于厂商标称的约 14.5 m/s,因为时间平均会把功率曲线拐点往左拉。生产使用前建议与厂商功率曲线表交叉核对;正式项目优先填厂商标称值。
La Haute Borne 四台 Senvion MM82,厂商参数 3.5 / 14.5 / 2050(切入/额定风速/额定功率),estimate_machine_parameters 反推结果与影响如下:
| 风机 | 反推 cut_in | 反推额定风速 | 反推额定功率 | 用反推 vs 厂商的判定差异 |
|---|---|---|---|---|
| R80711 | 3.79 | 12.54 | 1930 | 0.40% 行 |
| R80721 | 3.79 | 12.72 | 1985 | 0.51% 行 |
| R80736 | 3.78 | 12.71 | 2005 | 0.61% 行 |
| R80790 | 3.67 | 13.01 | 1989 | 0.40% 行 |
两点值得注意:
- 额定风速反推值(约 12.5–13.0)确实比厂商标称(14.5)低约 1.5–2 m/s,这是 10 分钟平均口径的固有"软"偏差。但在这批数据上它几乎不影响最终结果:因为接近额定风速后真实功率已基本贴满 0.9×额定功率,粗洗规则在高风速段的触发对额定风速不敏感。
- 反推与厂商两种做法判定的差异行只有约 0.4–0.6%,且几乎全部集中在切入风速附近的功率≈0 边界点(差异行中位风速 ≈3.6 m/s、中位功率 = 0)——本质是反推切入风速偏高一点点(3.7~3.8 vs 3.5)导致"停机"判定边界挪了几十厘米风速。对
normal_df的功率曲线主体毫无影响。
结论:反推参数在这类"正常运行占比高"的数据上完全可用,对结果影响很小。但若数据里限电/降载占比高,额定风速偏低的反推值会让"高风低功率粗洗"提前到功率爬坡段触发,可能多删一批合法的低功率点——这种情况下务必用厂商标称参数,或把反推额定风速往上修正。
| 参数 | 默认 | 含义 |
|---|---|---|
windspeed_label |
— | 风速列名(必填)。 |
power_label |
— | 有功功率列名(必填)。 |
df |
— | 单台风机的 SCADA DataFrame(必填)。 |
cut_in_speed |
— | 切入风速 m/s(厂商参数,必填)。低于该风速默认保留;停机判定从该风速起算。 |
rated_wind_speed |
— | 额定风速 m/s(厂商参数,必填)。风速 ≥ 它时风机应接近额定功率,是"高风低功率粗洗"的起点。 |
rated_power |
— | 额定功率 kW(厂商参数,必填)。高风区各类比例阈值都以此为基准。 |
low_power_ratio |
0.9 |
高风低功率粗洗阈值:风速 ≥ 额定风速时,功率 < 该比例×额定功率 即剔除。 |
bin_interval |
0.5 |
风速分箱宽度 m/s。越小分箱越细(样本少的区段统计越不稳),越大越粗。 |
z_coeff |
2.5 |
正常带宽度 = 中位数 ± z_coeff × MAD。见下方调参建议。 |
filter_cycle |
3 |
bin+MAD 迭代精洗的轮数上限。见下方调参建议。 |
return_fig |
False |
是否保存一张功率曲线清洗结果散点图(蓝=Normal / 橙=Abnormal)。 |
image_path |
"" |
return_fig=True 时输出图片的完整文件路径(含文件名,如 ./images/turbine_pc.png);目录不存在会自动创建。 |
"保留多少 / 洗得多纯"没有绝对正确的值,取决于你拿到 normal_df 后要干什么——拿去给高阶模型精洗可以放宽一点(尽量保点),指望初筛结果直接用就得收紧。每次调参都是"保点 vs 纯净"的权衡,建议结合输出图与逐轮打印来判断。以下给出方向和量级参考。
含义:功率偏离该箱中位数多少个 MAD 算正常。MAD 用原始值、未乘 1.4826,所以不能直接当"σ 倍数"读;换算成高斯直觉(原始 MAD ≈ 0.675σ):
| z_coeff | 正常带约 | 高斯下保留比例 | 档位 |
|---|---|---|---|
2.0 |
±1.35σ | ~82% | 激进(洗得纯,易误删) |
2.5(默认) |
±1.69σ | ~91% | 居中 |
3.0 |
±2.02σ | ~96% | 偏宽松 |
4.0 |
±2.70σ | ~99% | 很宽松(基本只剔粗洗) |
- 越大 → 带越宽 → 保留越多(正常点误删少,但漏进
normal_df的真异常变多); - 越小 → 带越窄 → 删得越多(
normal_df更纯,但可能误删边界正常点)。
实测参考(La Haute Borne,MM82):filter_cycle=3 时,z_coeff=2.5 → abnormal ≈ 13%;z_coeff=4.0 → ≈ 3%(此时 MAD 层每轮只删 ~1%,第 2、3 轮基本空转,接近"纯粗洗")。z_coeff 与 filter_cycle 是强耦合的,怎么配看下面的网格实测。
下表为"abnormal 占比",横轴 z_coeff、纵轴 filter_cycle:
| cycle\z | 2.0 |
2.5 |
3.0 |
4.0 |
|---|---|---|---|---|
1 |
15.0% | 8.7% | 5.1% | 2.6% |
2 |
22.6% | 11.7% | 6.1% | 2.7% |
3 |
27.7% | 13.4% | 6.5% | 2.7% |
5 |
34.3% | 14.7% | 6.7% | 2.7% |
8 |
40.7% | 15.0% | 6.7% | 2.7% |
从这张表能直接读出三条规律(这也是"该不该加迭代轮数"的判据):
- 带越宽,收敛越快:
z=4第 2 轮后删除就降到 0.14%→0.02%→0,第 2 轮即可停;z=3约第 3–4 轮收敛;z=2.5到第 5 轮才基本收敛(继续到第 8 轮 abnormal 只从 13.4% 涨到 15.0%,也就是多删了约 900 个正常/边界点)。 z=2是危险区:每轮删除量不衰减(13.7%→8.9%→6.6%→5.1%→4.2%……),跑到 8 轮时 abnormal 已到 40%。这说明带太窄、装不下数据真实的离散度,加轮数只会不断"吃"正常点。遇到这种逐轮删除不衰减的情况,别再加大filter_cycle,应调大z_coeff(或换doc/下的 GAM/sigmoid 方法)。- 看逐轮打印做决定:每轮都打印
removed X (Y%), remaining Z。某轮删除已 < 0.5% → 再加轮意义不大;连跑好几轮删除都 > 1% 不衰减 → 带太窄,往回调z_coeff。
- 只想粗洗、后续交给高阶方法精洗(
normal_df允许混少量离散点):z_coeff=3~4、filter_cycle=2~3。La Haute Borne 下 abnormal 约 3–7%,基本只剔停机/高风低功率/明显离群点,正常点保留最全。 - 想让 bin+MAD 这一层尽量洗纯(
normal_df直接用于建模,能接受少量正常点损失):z_coeff=2.5、filter_cycle=5。abnormal 约 15%,注意第 4–5 轮起每轮删 <1%,若不想误删边界点可提前手动用小轮次。 - 数据很干净 / 风机台数多样本足:可收紧
z_coeff=2.0~2.5;反之样本少、高风速 bin 稀疏时统计不稳,应放宽z_coeff≥3并加大bin_interval。 - 数据污染重 / 工况杂(大量限电、降载、停机混在正常里,如部分比赛风机):bin+MAD 会把"偏离主体"的正常工况也算成异常,别指望靠调小
z_coeff或加filter_cycle洗得更准——这类数据优先保证normal_df不被误杀,用z_coeff=4只剔粗洗,真正的异常分类交给doc/下的按天拟合/模型方案。
low_power_ratio:只影响额定风速以上的"限电/降载"粗洗。想多剔除降载工况就调小(如 0.85),想更保险保留就调大。注意别设太低,否则额定平台上的轻微降载会漏掉。bin_interval:高风速区样本稀的话可适当调大(如 1.0)提高该区段 MAD 稳定性;低风速区样本极多时调小能更精细。默认 0.5 对多数 10 分钟数据够用。cut_in_speed/rated_wind_speed/rated_power:这三个是物理参数,不要拿来当清洗旋钮调。设错会直接让粗洗规则失效(比如额定风速设太低会把正常爬坡段误当"高风低功率"整段删掉)。拿不准就反推,再和厂商表核对。
会画一张风速-功率散点图:蓝色 = Normal(normal_df),橙色 = Abnormal(abnormal_df),图例已标注。图片保存到 image_path 指定的完整文件路径。建议每次调参都出一张图,肉眼确认"边界处"是否删得合理。
python -m unittest discover -s test -v当前测试覆盖:正常/异常切分、重复索引不膨胀、粗洗(停机、高风低功率)、非法参数与空数据校验、出图、迭代删除记录。