4. 数据质量控制(QC):野点剔除、时间一致性检查、相关性分析、卡塔与冰冻数据处理
各位好,我是老张。干风资源评估这行十几年了,要说哪个环节最磨人,我肯定投数据质量控制一票。你想想看,测风塔往那一立,风吹日晒雨淋,什么稀奇古怪的数据都能给你整出来。要是前期QC没做好,后面画出来的风图谱就是空中楼阁,根本没法用。
今天咱们就聊聊数据质量控制这四大块:野点剔除、时间一致性检查、相关性分析,还有卡塔和冰冻数据的处理。嗯,都是实打实的硬功夫。
4.1 野点剔除(Range Check)—— 先把明显离谱的干掉
野点剔除,说白了就是把那些明显不符合物理常识的数据直接扔掉。风速出现负值?风速超过80m/s?温度显示零下80度?这些数据留着只会污染你的分析结果。
我个人习惯的做法是分两步走:
- 硬阈值过滤:根据仪器量程和当地气候特征,设定绝对不可逾越的上下限。比如风速传感器量程是0-60m/s,那超过60m/s的直接标记为无效。
- 软阈值过滤:结合当地气候统计特征,比如该地区历史最大风速从未超过35m/s,那突然冒出来一个50m/s的数据,大概率是传感器故障或雷击干扰。
核心原则:宁可错杀一千,不可放过一个。野点对平均风速、风功率密度的影响是灾难性的,尤其是对湍流强度的影响,一个野点就能把整年的湍流强度拉高好几个百分点。
我在内蒙古一个项目上遇到过,测风塔10m高度风速数据里,连续三天出现40m/s以上的记录。当时现场人员说是大风天气,我一看风向数据——完全没变化,而且相邻塔同高度风速只有8m/s。后来一查,是传感器轴承卡死了,风杯被吹得乱转。这就是典型的野点,必须剔除。
# Python 示例:野点剔除的简单实现
import pandas as pd
import numpy as np
def range_check(df, col='ws', lower=0, upper=60):
"""
野点剔除函数
lower: 风速下限(m/s)
upper: 风速上限(m/s)
"""
mask = (df[col] >= lower) & (df[col] <= upper)
df['qc_flag'] = np.where(mask, 1, 0)
return df
# 使用示例
data = pd.read_csv('wind_data.csv')
data = range_check(data, 'ws_80m', lower=0, upper=50)
print(f"有效数据占比: {data['qc_flag'].mean()*100:.1f}%")
4.2 时间一致性检查(Trend Check)—— 揪出那些"抽风"的数据
野点剔除解决的是"明显错误",但有些数据看着在合理范围内,实际上却有问题。比如风速从5m/s突然跳到25m/s,下一秒又回到6m/s。这种"跳变"数据,你想想看,自然界里风的变化再剧烈,也不可能在10分钟内完成这种幅度的变化。
时间一致性检查,就是通过相邻时间点的数据对比,找出那些变化速率异常的数据点。我常用的方法有:
- 一阶差分法:计算相邻两个时间点的差值,超过设定阈值(比如3m/s/10min)则标记为可疑。
- 滑动窗口法:以当前点为中心,取前后各2-3个点,计算局部均值和标准差,当前点偏离均值超过3倍标准差则标记。
- 持续异常检测:如果连续多个时间点数据都异常(比如连续6小时风速为0),那可能是传感器结冰或停机了。
我的小技巧:阈值设定不要一刀切。强风天气下风速变化本来就大,弱风天气下变化幅度小。我一般会根据风速大小动态调整阈值——风速越大,允许的变化幅度也越大。
嗯,这里要注意一点:时间一致性检查不能做得太激进。我曾经在一个山地项目上,把变化超过2m/s/10min的数据全标记了,结果发现大量有效数据被误删。后来一分析,山地的阵风效应本来就强,10分钟内风速变化3-4m/s是正常现象。所以阈值一定要结合地形和气候特征来定。
# 时间一致性检查示例
def trend_check(df, col='ws', max_change=5):
"""
max_change: 10分钟内允许的最大风速变化(m/s)
"""
df['ws_diff'] = df[col].diff().abs()
df['trend_flag'] = np.where(df['ws_diff'] <= max_change, 1, 0)
return df
# 动态阈值版本
def trend_check_dynamic(df, col='ws', base_change=3, factor=0.2):
"""
根据风速大小动态调整阈值
"""
df['threshold'] = base_change + factor * df[col].shift(1)
df['ws_diff'] = df[col].diff().abs()
df['trend_flag'] = np.where(df['ws_diff'] <= df['threshold'], 1, 0)
return df
4.3 相关性分析(相邻塔对比)—— 让数据互相"作证"
单塔的数据再干净,你也得有个参照物来验证。相关性分析就是利用相邻测风塔的数据,来交叉验证当前塔的数据质量。说白了,就是让数据互相"作证"。
我一般会这样做:
- 选择参考塔:距离目标塔10-30公里范围内,地形和气候条件相似的测风塔。
- 时间对齐:确保两座塔的数据时间戳完全对齐,10分钟平均数据要一一对应。
- 计算相关系数:用皮尔逊相关系数R来衡量两塔数据的线性相关程度。R² > 0.8通常认为数据质量合格。
- 残差分析:如果相关系数高但个别点偏差很大,这些点可能就是问题数据。
注意:相关性分析不能替代前两步的QC。如果参考塔本身数据就有问题,那对比结果也会误导你。我习惯先对参考塔也做一遍野点剔除和时间一致性检查,再用干净的数据来做相关性分析。
我记得在新疆一个项目上,两座塔直线距离只有8公里,但相关系数只有0.3。一开始我以为是数据有问题,后来实地考察才发现,两座塔中间隔着一道山梁,气流被完全阻断了。所以相关性低不一定就是数据质量差,也可能是地形导致的局地气候差异。
# 相关性分析示例
from scipy import stats
def correlation_check(df_target, df_ref, col='ws_80m'):
"""
计算目标塔与参考塔的相关性
"""
merged = pd.merge(df_target, df_ref, on='timestamp', suffixes=('_target', '_ref'))
r, p_value = stats.pearsonr(merged[f'{col}_target'], merged[f'{col}_ref'])
return r, p_value
# 残差分析:找出偏差大的点
def residual_analysis(df_target, df_ref, col='ws_80m', threshold=3):
merged = pd.merge(df_target, df_ref, on='timestamp', suffixes=('_target', '_ref'))
merged['residual'] = abs(merged[f'{col}_target'] - merged[f'{col}_ref'])
mean_res = merged['residual'].mean()
std_res = merged['residual'].std()
merged['outlier'] = merged['residual'] > (mean_res + threshold * std_res)
return merged
4.4 卡塔(Calm)与冰冻数据处理—— 特殊工况不能一刀切
卡塔和冰冻,是风资源评估里两个让人头疼的问题。处理不好,要么低估了风资源,要么高估了发电量。
4.4.1 卡塔数据(Calm)
卡塔指的是风速低于传感器启动风速(通常0.5-1.0m/s)的情况。这时候风速计不转了,记录的是0或者一个很小的值。但实际风并没有完全停止,只是传感器测不到了。
我的处理原则是:
- 保留卡塔数据:不要直接删除,因为卡塔时间占比虽然小(通常1-5%),但对年平均风速有影响。
- 标记但不剔除:在数据标记中注明"calm",后续计算时按0风速处理。
- 检查卡塔比例:如果某个高度卡塔比例超过10%,那就要怀疑传感器是不是出问题了。
4.4.2 冰冻数据(Icing)
冰冻比卡塔麻烦得多。传感器结冰后,风杯转不动,记录的数据要么是0,要么是异常低值。而且冰冻往往持续几个小时甚至几天,对数据完整性的影响很大。
我处理冰冻数据的经验是:
- 识别冰冻事件:结合温度数据(低于0°C)、风速骤降、风向锁定等特征来判断。
- 标记冰冻时段:将冰冻期间的数据标记为"icing",不参与正常统计。
- 插值或替代:如果冰冻时间短(< 6小时),可以用前后数据线性插值;如果时间长,需要用相邻塔或再分析数据来替代。
- 记录冰冻损失:在报告中明确说明冰冻导致的数据缺失比例,这对后续发电量评估很重要。
避坑指南:我曾经在东北一个项目上,冬天连续20天的数据都是0风速。当时以为是传感器坏了,后来才发现是结冰导致的。但因为处理不及时,这20天的数据被直接删除了,导致冬季平均风速被严重低估,发电量预测偏差了15%以上。所以,冰冻数据一定要单独处理,不能简单删除。
# 冰冻数据识别与处理示例
def icing_detection(df, temp_col='temp', ws_col='ws_80m', temp_threshold=0):
"""
基于温度识别冰冻事件
"""
df['icing'] = (df[temp_col] < temp_threshold) & (df[ws_col] < 0.5)
# 连续冰冻标记:如果连续3个点都满足条件,认为是冰冻事件
df['icing_event'] = df['icing'].rolling(window=3, min_periods=1).sum() >= 3
return df
def icing_imputation(df, ws_col='ws_80m', max_gap=6):
"""
冰冻数据插值:仅对短时间冰冻进行插值
"""
df[ws_col + '_imputed'] = df[ws_col].copy()
# 对冰冻事件内的数据进行插值
df.loc[df['icing_event'], ws_col + '_imputed'] = np.nan
# 线性插值,限制最大插值长度
df[ws_col + '_imputed'] = df[ws_col + '_imputed'].interpolate(
method='linear', limit=max_gap
)
return df
4.5 数据质量报告—— 让QC结果可追溯
QC做完了,不能拍拍屁股走人。你得把整个过程记录下来,形成数据质量报告。我一般会包含以下内容:
| QC项目 | 剔除/标记数量 | 占比 | 备注 |
|---|---|---|---|
| 原始数据总量 | 52560 | 100% | 10分钟数据,全年 |
| 野点剔除 | 128 | 0.24% | 风速>50m/s或<0 |
| 时间一致性 | 256 | 0.49% | 变化>5m/s/10min |
| 卡塔数据 | 1051 | 2.00% | 风速<0.5m/s |
| 冰冻数据 | 2102 | 4.00% | 温度<0°C且风速<0.5m/s |
| 有效数据 | 49023 | 93.27% | 满足QC要求 |
数据质量报告不仅是给别人看的,更是给自己留个底。万一后续分析结果有问题,你可以回溯到QC环节,看看是不是哪个阈值设得太严或太松了。
好了,数据质量控制这部分就聊到这儿。记住一句话:垃圾进,垃圾出。QC花的时间越多,后面的分析就越省心。别嫌麻烦,这是风资源评估的根基。