简介:在气象与水文学研究中,风速数据的连续性和可比性直接影响天气预报、气候分析与环境影响评估的可靠性。这份以最大风速为例的均一化订正资源包,面向需要处理历史观测数据的气象学者、环境评估人员及数据分析学习者,提供一套从原始数据到订正结果的完整代码方案。包内共含3个文件:Python脚本覆盖数据读取、缺失值插补、订正因子计算、结果验证与趋势图绘制等环节;CSV文件存放1971-2015年的最大风速观测记录,方便直接测试;PDF文档详解代码逻辑与均一化订正原理,便于快速上手。整体体积仅377KB,轻巧但信息密度高。目前已有592人浏览学习,适合作为学习Thorne-Wyatt、HOM等均一化算法的入门实操素材。运行代码后,读者既能掌握处理非均一化数据的通用思路,也能将方法迁移至降水、温度等其他气象要素,为气候研究奠定更坚实的数据基础。
1. 以最大风速为例的风速均一化订正:这套代码和数据能解决哪个实际问题
做风电场微观选址或者桥梁抗风设计时,常会遇到一个尴尬场景:同一站点不同年份的最大风速序列,在某一年突然整体跳高一截,或者毫无征兆地跌下去,看起来像气候变了,实际上很可能是那一年前后站址搬迁、测风仪器换型,或者周围盖了高楼。遇到这种数据,直接用原始序列算极值重现期,结果很容易偏离真实情况几十个百分点。风速均一化订正,就是针对这种非气候突变做的一套数据处理流程,它把序列里站址、仪器、环境变化造成的系统偏差识别出来并消除,同时保留真实的气候波动。这份以最大风速为例的代码包,能帮你完成断点检测、分段订正、结果输出这条完整链路,适合气象水文资料整编、风工程评估和相关专业的学生直接参照复现。
2. 风速均一化订正的原理和选型:为什么先从年最大风速开刀
2.1 站点序列里的非气候信号:迁站、换仪器和环境变化如何混进来
国家气象站的历史风速序列,很少是真正“同址、同仪器、同环境”测出来的。最常见的干扰是站点迁移:一个站从城区楼顶迁到郊区开阔地,测风高度、周边粗糙度全变了,年最大风速可能整体抬高或压低一到两米每秒。第二个常见干扰是仪器换型:早期机械式风杯测风仪的启动风速偏高,弱风时捕捉不到,真正的最大风速也容易因为风杯惯性被低估;换到新型自动站之后,采样频率提高,同样的一场大风,记录值可能比旧仪器高出不少。第三个干扰是台站环境变化:周边盖楼、树木长高、工地围挡出现,都会改变局地的气流绕行特征。
这些信号混进原始序列之后,外在表现往往就是“均值突变”或者“方差突变”。做风速均一化订正,第一步就是判断哪些突变属于非气候信号,哪些属于真实的气候事件。常见误区是把所有突然的变化都当成气候突变,这是做订正时第一个要克服的本能。区分两者的一个基本思路是:气候信号通常是区域性的,附近多个站点会有相似变化;而台站本身的非气候信号是局地的,往往只影响这一个站。这就是后面“参考站法”的出发点,也是为什么纯单站订正永远只能算次优解。
2.2 年最大风速的统计特性:极值段对突变比均值段更敏感
这套示例代码选择“最大风速”而不是“月平均风速”,不是随意为之。第一个原因很实际:工程上关心的是极值。风机高度处的极端风、建筑风荷载设计、防洪防风标准,全部直接落在年最大风速和重现期的换算上。如果月平均风速序列被订正得很好,但年最大风速一端仍然带着系统偏差,那对设计参数毫无帮助。第二个原因是统计上的:年最大风速序列每年只取一个最大值,样本之间的相关性很弱,基本满足独立同分布假设,而逐月或逐时风速序列存在强自相关,会干扰变点检验的显著性判断。
第三个原因容易被忽略:极值段对均值突变更敏感。一个站点的年平均风速如果偏移 0.5 m/s,可能完全看不出来;但年最大风速整体偏移 2 m/s,在 50 年一遇重现期上可能就是 28 m/s 和 32 m/s 的区别。换句话说,极值序列的均一性问题,最终会直接放大到设计参数里。当然,用年最大值做样本也有代价:样本量等于年数,通常只有 30 到 50 个点,订正常数本身有估计误差。所以后续代码里对分段样本量做了严格限制,低于 10 年的分段不建议单独做订正。
以最大风速为例还有一个数学上的灵活性:风速可以近似看成乘性误差占主导。旧仪器、低高度、高粗糙度带来的往往是“同样的大风测出来偏低一截”,而不是“固定少 2 m/s”。所以订正常用比值法而不是差值法,这也是标题这个例子最容易扩展成通用流程的原因。理解了这一点,再看后面的代码,就不会把订正常数误当成一个简单的减法偏移。
2.3 变点检测方法怎么选:SNHTT、Pettitt 与参考序列的取舍
做风速均一化订正,断点检测是核心环节。常用方法里,标准正态同质性检验也就是 SNHTT,主要思路是把原始序列标准化,然后遍历每个可能的断点位置,计算前段均值与后段均值的偏离统计量;Pettitt 检验则是基于秩的 Mann-Whitney 统计量,不需要数据服从正态分布;MASH 则是荷兰皇家气象研究所发展起来的多站联合同质化方法,适合有完整站网的业务环境。
| 场景 | 推荐做法 | 局限与注意 |
|---|---|---|
| 单站、无邻近可靠站 | SNHTT 绝对订正 | 无法分离气候变率,检测结论要谨慎 |
| 有 3 到 5 个邻近站 | 差值序列 SNHTT | 差值序列要先做相关性筛选 |
| 长序列、疑似多断点 | 迭代 SNHTT | 每次订正后重新检测,控制迭代次数 |
| 业务整编、站网齐全 | MASH / 同类多站方法 | 需要历史沿革元数据支撑 |
单站绝对订正的问题在于,它把真实气候波动也当成候选断点来处理,断点位置检测出来之后,很难判断是迁站还是气候转折。所以我在实际处理中,只要周围有相关性达标的参考站,就优先做差值序列检测。所谓差值序列,就是把目标站年最大风速减去参考站年最大风速,得到一条新的序列,再对这条序列做 SNHTT。区域性气候波动在差值里被抵消,剩下更大的部分才是目标站的局地非气候突变。这套流程在后面的代码主流程里会体现出来。
方法选型还有一个前置条件:台站历史沿革资料。断点检测给出的是统计提示,不是最终结论。迁站年份、换仪器年份如果没有元数据支撑,统计上再显著的断点也只能标注为“待核实”。这也是为什么所有正规的同质化订正业务里,统计结果都要和台站沿革记录交叉验证。
3. 把订正流程写成 Python 脚本:最小可跑版本和参数说明
3.1 读入风速 CSV:先统一列名、单位和缺测标记
压缩包里的示例数据是 CSV 格式,每一行代表一个年份。运行代码之前先确认三件事:列名、单位、缺测标记。我一般会先用一段极简代码把数据读进来,打印表头和数据分布,再继续往下走。
import pandas as pd df = pd.read_csv("ws_max.csv", encoding="utf-8-sig") print(df.head()) print(df.dtypes) print(df.isna().sum())这段代码里encoding="utf-8-sig"是为了兼容中文列名,防止在 Windows 下用 Excel 打开时出现乱码。df.isna().sum()用来统计缺测年份,后续断点检测不允许序列里出现 NaN,这一步能提前暴露问题。
如果原始数据里的列名是中文,比如“年份”“最大风速”,先做重命名,避免后面代码反复用中文索引:
df = df.rename(columns={"年份": "year", "最大风速": "ws_max"})单位统一放在读入阶段处理。示例数据采用 m/s,如果你的数据来自统计年鉴且单位是 km/h,直接除以 3.6 即可。这里要特别提醒:不要忽略“最大风速”口径。日最大风速、10 分钟平均最大风速、瞬时极大风速是三个不同概念,混合使用会让订正结果完全失去意义。
3.2 SNHTT 断点检测:一个函数找出序列断点
SNHTT 的代码量不大,但参数处理有不少细节。函数输入是一维风速序列,输出是最可能断点的位置和对应统计量。
import numpy as np def snhtt_detect(series, min_sep=0.1): """SNHTT 变点检测。 series: 一维数值序列,不允许含 NaN。 min_sep: 断点不允许落在序列首尾的占比。 """ series = np.asarray(series, dtype=float) if np.any(np.isnan(series)): raise ValueError("序列存在 NaN,请先插补或删除缺测年") n = len(series) z = (series - series.mean()) / series.std(ddof=1) k_start = max(1, int(n * min_sep)) k_end = min(n - 1, int(n * (1 - min_sep))) best_k, best_T = None, -np.inf for k in range(k_start, k_end): z1 = z[:k].mean() z2 = z[k:].mean() T = k * z1 ** 2 + (n - k) * z2 ** 2 if T > best_T: best_T, best_k = T, k return best_k, best_T逻辑说明:先把原始序列标准化成 z 序列,消除量纲影响;然后从第k_start个观测点开始,逐个假设“第 k 个观测之后发生了断点”,分别计算前后两段标准化均值;T值越大,说明该位置的断点越显著。best_k返回的是索引位置,含义是第best_k个观测之后出现断点,对应到年份序列时要注意索引和年份的对应关系。
参数说明:min_sep=0.1表示断点不允许落在序列最前和最后的 10% 区间内,这是为了避免样本量过小导致订正常数不可靠。如果站点序列只有 20 年,那么首尾各 2 年被禁止作为断点,程序只会检测中间约 80% 的范围。
这个函数里我选择了直接抛错而不是静默处理 NaN,原因是缺测没有统一处理方案时产生的错误很难被发现。实际业务中,缺测年份应该先插补再进入检测,具体插补策略在第 5 章会展开。
3.3 分段订正常构建:比值订正与基准段选择
检测到断点之后,下一步是构建订正常。风速偏差以乘性为主,所以示例代码采用比值订正而不是差值订正。订正常等于两段均值的比值,订正目标是把两段均值拉齐到同一水平。
def segment_adjust(series, break_k, base_segment="second"): """分段比值订正。 base_segment='second' 表示以后段为基准,把前段向下调整; base_segment='first' 表示以前段为基准,把后段向下调整。 """ seg1 = series[:break_k] seg2 = series[break_k:] if base_segment == "second": coef = seg1.mean() / seg2.mean() adjusted = series.copy() adjusted[:break_k] = series[:break_k] / coef else: coef = seg2.mean() / seg1.mean() adjusted = series.copy() adjusted[break_k:] = series[break_k:] / coef return adjusted, coef逻辑说明:假设某站 1980 年迁站,新站址比旧站址测得的平均最大风速整体偏大 10%,那么后段均值约等于前段均值乘以 1.1。以更可靠的新站址为基准时,应该把前段风速整体除以 1.1,而不是把后段风速乘以 0.9。中括号切片adjusted[:break_k]处理的是断点前的年份,adjusted[break_k:]处理的是断点后的年份,边界要检查清楚,否则会出现偏移一年。
参数说明:base_segment的默认值是"second",这并不是算法最优解,而是业务上的常见选择。通常认为新站址、新仪器的观测更接近现代标准,所以把历史旧段向新段靠拢。如果你的台站元数据表明旧段更可信,把参数改成"first"即可。这个参数的选择,本质上是元数据判断,不是统计判断。
3.4 单站订正主流程:断点迭代直到序列平稳
单次断点检测订正一遍通常不够,长序列里可能存在两个以上的断点。标准做法是迭代处理:检测出最强断点,订正,再对订正后序列重新检测,直到没有显著断点或达到最大迭代次数。
def adjust_station(df, min_sep=0.1, threshold=8.0, max_iter=3): """单站订正主流程,返回订正后序列和断点记录。""" years = df["year"].to_numpy() series = df["ws_max"].to_numpy(dtype=float) adjusted = series.copy() records = [] for _ in range(max_iter): k, T = snhtt_detect(adjusted, min_sep=min_sep) if T < threshold: break adjusted, coef = segment_adjust(adjusted, k, base_segment="second") records.append({ "break_year": years[k - 1], "statistic_T": round(T, 2), "coef": round(coef, 4), }) result = pd.DataFrame({ "year": years, "ws_max_raw": series, "ws_max_adj": adjusted, }) return result, records逻辑说明:循环里每次重新调用snhtt_detect,等价于在第 3.2 节中把订正后的序列再做一次标准化检测。threshold=8.0是 SNHTT 统计量的经验阈值,当样本量约 30 到 50 年时,95% 置信度的临界值大致落在这个量级,正式业务应查阅对应样本量的临界值表。max_iter=3是防过度订正的开关,限制最多迭代 3 次,避免把序列拆得过于零碎。
参数说明:break_year的记录用的是years[k - 1],表示断点发生在前段最后一个观测年之后;如果希望记录成新段起始年,改成years[k]即可。coef是比值订正常数,大于 1 说明旧段均值偏高被向下调整,小于 1 说明旧段均值偏低被向上调整。输出表格里ws_max_raw保留原始值,这一列永远不要删除,后面对比和验收全都要靠它。
4. 让代码吃进正确的数据:最大风速抽样与文件组织
4.1 年最大风速还是月最大风速:抽样时次决定断点位置
这份代码的检测对象是“年最大风速”,但“年最大风速”本身有三种常见取法:直接取日历年内 12 个月的月最大风速中的最大值;取日最大风速序列的全年最大值;取定时观测记录里的瞬时极大风速。三种取法得到的数字差异很大,断点检测的敏感度也不一样。
在气象水文业务里,最常见的是第一种取法,因为月最大风速报表齐全,缺测少,跨年问题也容易处理。但要注意台风影响明显的区域,一次台风过程可能横跨 12 月到次年 1 月,如果按日历年来切,同一场台风的风速被分到两个年份,会人为压低两个年份的极值。研究这类区域时,我会改用“滑动年”或“气象年”,比如从上年 7 月 1 日到当年 6 月 30 日作为一个统计年,这样一次台风过程不会跨年。代码本身不关心你给的是日历年还是气象年,但抽样口径不同,断点位置会明显变化,这一点在报告里必须交代清楚。
另外要区分“最大风速”和“极大风速”。国内观测规范里,最大风速通常指 10 分钟平均风速的最大值,极大风速指瞬时风最大值。两种序列的统计特性不同,订正常数也不同。如果你手里的数据文件没有写清楚口径,建议先看数据说明或原始报表,不要直接跑代码。
4.2 数据文件怎么排:站号、年份、风速和缺测的约定
压缩包里的示例数据按一个站点一个 CSV 文件组织,表头字段约定如下。
| 字段 | 类型 | 说明 |
|---|---|---|
| station_id | 字符串 | 区站号或自定义编号 |
| year | 整数 | 统计年份 |
| ws_max | 浮点数 | 年最大风速,单位 m/s |
| flag | 字符串或整数 | 0 正常,1 可疑,2 缺测 |
如果需要做参考站差值检测,建议把参考站放在另一个 CSV 文件里,字段同上,并保证年份范围与目标站一致。示例数据里给出了一个单站序列和对应的参考站序列,两个文件按站号区分,读取后先按年份对齐再进入检测流程。
缺测用NaN表示,不要用-9999或0填充。原因是 3.2 节的snhtt_detect会直接拒绝含 NaN 的序列,而如果用-9999充当数值,程序不会报错,但会把缺测年当成一个极小的风速值参与统计,标准差和均值全部被污染。这是最容易出现但最难排查的问题。
两个文件是否要对齐成同一个年份列表?我一般会做一次pd.merge(df_target, df_ref, on="year", how="inner"),只保留双方都有观测的年份。缺测年太多时,先插补再做差值序列,插补方法放到第 5 章集中说明。
4.3 三个必调参数:断点搜索范围、显著性阈值、最小分段长度
代码能跑起来是一回事,参数调得对是另一回事。这套流程里三个参数直接影响结果质量。
第一个是min_sep,断点搜索范围。示例值 0.1,配合 30 年序列,相当于断点年份限制在第 3 年到第 27 年之间。如果站点有过两次迁站,且第二次迁站发生在第 28 年,这个参数会漏掉它。这时可以把min_sep调小到 0.05,但要接受首尾分段样本量变小、订正常数方差变大的代价。
第二个是threshold,显著性阈值。示例值 8.0,这是一个经验值而非标准值。SNHTT 临界值随样本量变化,30 年序列和 60 年序列的 95% 置信度临界值并不相同。代码里把这个值做成函数参数,就是希望你在正式业务中查阅对应文献,而不是直接沿用示例数字。
第三个是base_segment,基准段选择。这个参数调整的是订正方向。判断依据来自台站沿革:迁站后、换新仪器后的记录段通常更可信,就以新段为基准;如果你手里的序列恰好相反,早期用的是进口高精度设备,后期换了国产设备且明显偏低,那基准段就应该选在早期。示例默认"second",但正式的订正报告里,这一选择必须写理由。
三个参数之间互相影响:调小min_sep会增加可检测断点范围,但显著性阈值不变时容易把噪声当断点;调低threshold会让迭代更容易提前终止,适合短序列;基准段选错则会让整个订正方向反转。所以参数调完,一定要回到第 6 章的验证环节看整体效果,不能只盯检测统计量。
5. 风速均一化订正避坑:5 条现象到原因再到解决
5.1 订正完强台风记录被压没了
现象:订正后的序列里,历史上几次强台风年份的风速值明显变小,极值序列看起来被“削平”了。
原因:比值订正常数按整段均值计算,会把一个统一的倍率套用到该段所有年份上。但强台风年份的风速往往不完全服从这种倍率关系,比如旧仪器在 10 m/s 以下偏低明显,而在 40 m/s 以上因为风杯飞转反而接近真实值,套用同一个系数就会把本应保留的极值压下去。
解决:订正后单独检查历史前 5% 的风速记录。如果某些极端年份被过度修正,可以只对非极值部分做订正,或者用分段分位点订正来替代单纯的均值比值订正。更稳妥的做法是保留原始序列,在报告里把订正结果和原始极值并列展示,不要用订正结果覆盖原始记录。记住订正的目的是消系统偏差,不是抹平极端年份。
5.2 订正方向反了,序列比原来更不齐
现象:订正后的序列均值方差反而更大,断点位置依旧一眼可见,甚至比订正前更刺眼。
原因:基准段选错了方向。如果旧站址周边全是建筑,测得的风速系统性偏低,新站址开阔、风速偏高,而你把旧段默认成了基准,就会把后期偏高的记录向下压,等于把真实偏差越纠越偏。
解决:查看台站历史沿革,确认哪一段观测环境更符合规范要求。代码里segment_adjust的base_segment参数存在的原因,就是让方向判断显式化为一个可审查的决策。如果元数据不可靠,还有一个交叉验证手段:把订正后的序列分别与参考站做差值方差比较,方差显著增大的那一版,大概率是方向选反了。
5.3 多个断点叠在一起,订正完段间不衔接
现象:站点 40 年里经历过两次迁站,代码只检测出一个断点,订正完成后,序列里还剩一个明显台阶。
原因:SNHTT 统计量遍历所有断点位置时,多个断点会干扰彼此的显著性。最强断点被找到后,次级断点的统计量大幅下降,单次检测容易漏掉;而如果试图一次性把两个断点都写进模型,又容易把两个断点的位置都估计偏移。
解决:用第 3.4 节的迭代流程,每次只检测最强断点,订正后重新标准化再检测下一断点。同时限制最小分段长度,比如每个分段至少 10 年,防止迭代把序列拆得过于零碎。每轮迭代的断点年份、订正常数都要记录到表格里,否则报告阶段说不清“为什么在这个年份断开”。
5.4 参考站选错,差值序列检出一堆假断点
现象:加了参考站做差值序列之后,原本单站检测还算合理的结果,反而冒出来三四个显著断点,断点年份和台站沿革完全对不上。
原因:参考站和目标站的气候相关性不足。比如目标站靠海,参考站在内陆,它们对同一场台风的响应差别很大;或者两站海拔差了三四百米,地形抬升效应让风速变化并不同步。差值序列把这两部分不相关的信号叠加起来,信噪比反而下降。
解决:做参考站差值之前,先算两个站原始序列的相关系数,通常要求相关系数不低于 0.6,同时距离不宜过远。如果没有满足条件的参考站,老实回到单站绝对订正,并在结果表里标记“未做参考站验证”。参考站数量也不宜贪多,选择 3 到 5 个相关性高、分布在目标站不同方向的站点会更稳健。
5.5 缺测年份处理不当,SNHTT 统计量失真
现象:某站有 5 年缺测,代码没报错,但检测出的断点年份落在缺测年附近,订正常数大得离谱。
原因:如果缺测被填充成 0 或空字符串,序列会变成“正常值 + 一堆 0”的组合,标准化后均值被拉低,标准差被放大,断点统计量会在缺测边缘出现虚假峰值。如果缺测被直接跳过,年份和序列索引错位,断点年份的定位也会出错。
解决:先插补,再检测。常见做法是取相邻年份的线性内插,或者用该站多年同期平均风速做气候值替换。插补完成后,在数据表里保留一个flag列标记哪些年份是插补值,订正报告里也要注明缺测年份数量。如果缺测年超过总年数的 20%,就不建议做严格意义上的均一化订正了,样本量不足以支撑可靠的订正常数。
6. 订正结果怎么验收:从对比图到重现期变化
6.1 订正效果的三张图与一张表
结果不是算完就结束的,订正质量要用图和表交代清楚。第一张图是订正前后年最大风速时间序列对比,原始值画灰线,订正值画黑线,断点年份用竖直虚线标出来;第二张图是订正后的序列减去参考站序列的差值图,看差值是否围绕 0 附近波动,方差是否比订正前明显缩小;第三张图是订正前后的风重现期对比曲线,直接展示工程关心的参数变化。一张表对应输出年份、原始风速、订正风速、断点编号和订正常数,原始值永远保留。
import matplotlib.pyplot as plt plt.plot(result["year"], result["ws_max_raw"], label="raw", color="gray") plt.plot(result["year"], result["ws_max_adj"], label="adjusted", color="black") plt.axvline(records[0]["break_year"], linestyle="--", color="red") plt.legend() plt.savefig("adjusted_series.png", dpi=200)绘图这段代码没什么特殊逻辑,唯一要提醒的是records[0]只有在至少存在一个断点时才能取到。实际运行时先判断records是否为空,否则画图会直接报错。这张图也是后续核对台站沿革的记录:断点年份有没有落在迁站年份前后,一眼就能判断。
6.2 五十年级别风速是最终验收标准
验收时,我最看重的是订正前后“50 年一遇最大风速”的变化幅度。用经验频率法能快速得到近似结果,适合初验:把序列按大小排序,第 i 大的观测对应重现期约 (n+1)/i 年,再在排序序列里找到最接近 50 年的位置。
def emp_return_period(series, return_level=50): n = len(series) sorted_series = np.sort(series)[::-1] rp = (n + 1) / np.arange(1, n + 1) idx = np.argmin(np.abs(rp - return_level)) return sorted_series[idx]经验法只适合快速对比。如果差异小于 10% 到 15%,属于合理范围,说明订正没有过度干预;如果差异超过这个范围,就要倒回去检查参数选择。我自己就吃过一次亏:第一次跑通流程时,把订正后的序列单独存了一版,原始值被覆盖,后来复核时才发现某年台风风速被压掉了,翻原始报表才找回来。那之后我养成了两个习惯:原始列永远保留,订正列单独放;每次交付都附一张断点观测图。希望帮到你。
本文还有配套的精品资源,点击获取