简介:这份水文频率分析配套资料包面向水文学、水利工程及数据分析初学者,聚焦从观测数据读取、缺失值与异常值预处理,到概率分布拟合、参数估计和设计频率计算的完整分析链路。压缩包为rar格式,共14个文件、约1011KB,包含txt说明与数据记录、xls表格、exe可执行程序、dll动态库、chm帮助文档,以及html、gif、db、ini等辅助文件,体量轻便,适合本地解压后对照查看与练习。已有4376人学习下载,说明其在相关课程与自学场景中具有一定参考价值。读者可借此梳理皮尔逊III型、正态、对数正态、伽马等常见分布的适用条件,理解最大似然估计、拟合优度检验、逆累积分布函数求设计值,以及交叉验证评估模型泛化能力的思路,并借助曲线拟合相关内容加深对频率分析流程的认识。
1. 水文频率分析:CV 和 CS 一动,百年一遇就换了答案
同一份实测洪峰序列,两个人各跑一遍水文频率分析,一个人给出百年一遇设计洪峰 8200 m³/s,另一个给出 6900 m³/s。样本完全相同,用的都是 P-III 型分布,甚至代码可能是同一份,差只差在 CV 和 CS 这两个数上。CV 是变差系数,衡量序列的相对离散程度;CS 是偏态系数,衡量分布往哪边偏、尾巴有多长。在水文频率分析里,这两个参数不是从数据里“读”出来的客观事实,而是带有判断成分的估计量,从读取数据那一刻起,选样方式、经验频率公式、参数估计方法、适线时看哪一段,每一步都在往最终那条曲线上加自己的权重。
值得把这套机制搞懂,是因为它直接对应工程上的钱和安全。设计洪水算大了,溢洪道白修几十米;算小了,汛期就是悬在头上的一笔账。而整个过程里最容易翻车的,恰恰不是那句“P-III 分布”的结论,是 CV、CS 怎么来的、为什么这么来、什么时候它会骗你。
2. 读取数据与经验频率:分歧在排序那一刻就注定了
一份逐日流量台账和一条频率曲线之间,隔着的不是某个公式,而是一连串会改变结论的转换。年最大值怎么挑、缺测怎么处理、超定量要不要并进来,这些动作发生在任何参数估计之前,却决定了后面所有数字的地基。
2.1 年最大值选样:一年只留一个数的理由
水文频率分析的样本单位通常是“年”,每年取一个最大洪峰,构成年最大值序列(Annual Maximum Series,AMS)。这个做法背后是一条硬假设:年最大值的分布年际独立、同分布。只有在这个假设下,把 n 年的数据当成 n 个独立观测去推总体分布才站得住。
一年只留一个数,代价是丢掉大量信息。某一年汛期来了三场量级接近的大洪水,AMS 只记住最大的那场,另外两场对参数估计毫无贡献。于是有了超定量选样(Peak Over Threshold,POT),设一个阈值,凡是超过阈值的洪峰都进样本。POT 用上了更多数据,但引入了新的麻烦:同一次降雨过程造成的双峰、洪水过程线的退水段,都会让样本不再独立,得做超定量门限筛选和去聚类。工程实践中 AMS 仍是主流,原因不复杂——它的假设更强但更干净,出了争议好解释。
选样还牵扯到“年”怎么定义。用日历年 1 月 1 日到 12 月 31 日,还是水文年 4 月 1 日到次年 3 月 31 日?北方某些流域的冰凌洪水、南方某些流域的前汛期洪水,都可能因为年界切得不对被劈成两半,或者让某一年的最大值其实来自上一年的汛末。这个细节在数据读取阶段如果不处理,后面再精细的适线都是白费。
2.2 经验频率公式:m/(n+1) 里的无偏直觉
把样本从大到小排好,第 m 位的经验频率怎么定,是个看似琐碎、实则影响曲线两端的问题。最直觉的做法是 m/n,但它在端点会出问题:最大值 m=1 给出 1/n,意思是“超过它的概率是 1/n”,可这个最大值恰恰是样本里最大的,说它还有 1/n 的超越概率,逻辑上别扭;最小值 m=n 给出 1,等于说“必然超过最小值”,而样本里明明有观测。
数学期望公式 m/(n+1) 相当于在样本两端各补了一个虚拟观测点,让排在最前和最后的实测值都不落在 0 和 1 上。用期望值的角度理解:如果把同样的抽样重复无数次,第 m 个次序统计量对应的超越概率的期望,正是 m/(n+1)。这也是它被称为“无偏”经验频率的原因。海森公式 (m-0.5)/n 是另一套思路,认为样本经验点应该落在概率格纸的区间中点;切戈达耶夫公式在 n 较大时和 m/(n+1) 差别很小,n 小于 30 时能差出一两个百分点,在曲线头部这个差别会被放大。
注意:经验频率公式的选择必须在同一批计算里保持一致。绘制经验点用一套公式,优化适线时又用另一套,曲线会被人为拉偏,而且这种偏很难从残差上看出来。
2.3 读取数据阶段最容易埋雷的三处
逐日流量台账通常长这样:一行一个日期,一列流量,缺测填 -9999 或空字符串,偶尔还有单位混用、日期格式不统一。下面这段是常见的读取加选样骨架:
import pandas as pd import numpy as np def load_annual_max(path, date_col="date", q_col="Q", missing=(-9999, -999.0), min_days=300): """读取逐日流量,返回按水文年(4月1日起)统计的年最大流量序列""" df = pd.read_csv(path, parse_dates=[date_col]) # 流量列强制转数值,非数字(如 "--"、"缺测")一律变 NaN df[q_col] = pd.to_numeric(df[q_col], errors="coerce") df.loc[df[q_col].isin(missing), q_col] = np.nan df = df.dropna(subset=[q_col]) # 水文年:4 月及以后归当年,1-3 月归上一年 d = df[date_col].dt hyear = np.where(d.month >= 4, d.year, d.year - 1) df = df.assign(hyear=hyear) # 年最大 + 当年有效观测天数,天数不足的年份剔除 g = df.groupby("hyear")[q_col] annual = pd.DataFrame({"qmax": g.max(), "ndays": g.count()}) annual = annual[annual["ndays"] >= min_days] return annual["qmax"].sort_values(ascending=False) def empirical_freq(x_desc): """数学期望公式,返回超过概率 P(从大到小排序后)""" n = len(x_desc) m = np.arange(1, n + 1) return pd.Series(m / (n + 1.0), index=x_desc.index)missing参数里塞的是台账里出现过的缺测标记,各家数据源不一样,有的是 -9999,有的是 -999,有的是空。min_days卡的是“这一年到底测够了没有”,一个只测了汛期三个月的年份,最大值可能确实很大,但它不代表全年,直接进样本会把 CV 抬高。这个阈值取 300 还是 330,取决于流域和数据完整度,没有标准答案,但必须显式写出来并记录在案。
还有一个常被忽略的点:特大值。历史洪水调查得到的、比实测序列最大值还大的那些点,不能简单地当普通样本塞进去排序。它们的重现期不是靠 m/(n+1) 算出来的,而是靠历史调查证据定出来的,处理方式通常是作为“历史洪水”单独赋经验频率,或者在有条件时用历史洪水信息校正参数。把这些点当普通样本处理,等于骗自己样本量比实际大。
3. 矩估计的三次方:CV 为什么总被算小
排序完了,接下来就是从样本推参数。最直接的路子是矩法——用样本矩去匹配总体矩。这条路在很多领域够用,但在偏态重、样本短的洪水序列上,它有一个结构性的弱点。
3.1 矩法公式与它的三次方放大
矩法的三个估计量写出来是这样:
import numpy as np def moment_estimates(x): """矩法估计 P-III 的三个参数""" n = len(x) mean = x.mean() s = x.std(ddof=1) # 样本标准差 cv = s / mean dev3 = ((x - mean) ** 3).sum() # 规范推荐的矩法偏度系数(无偏化处理) cs = n * dev3 / ((n - 1) * (n - 2) * s ** 3) return mean, cv, cs关键在cs那一行。偏度系数是样本离差的三次方和,这意味着任何一个远离均值的观测,对 CS 的贡献是它离均值距离的立方。样本里最大的那场洪水,离均值可能是 3 倍标准差,它的三次方贡献是 27 个单位,而一个离均值 1 倍标准差的观测只贡献 1 个单位。换句话说,CS 的估计值高度依赖极少数几个点,序列头部的两三场大水一改,CS 就能跳一大截。
CV 的问题不同。样本标准差的期望小于总体标准差(这是贝塞尔校正存在的原因,用 n-1 而不是 n 作为分母就是在补这一点),但更关键的是,洪水序列的分布是右偏的,从右偏总体里抽出来的样本,标准差估计的抽样分布本身也是偏的,中位数位置偏低。结果是 CV 被系统性低估,序列越短、偏态越重,低估越明显。n=30、CS=1.5 的序列上,矩法算出的 CV 平均比真值低百分之十几,这个量级足够让百年一遇设计值偏小。
提示:短序列上直接用矩法 CS,经常算出大于 4 甚至更大的数。这不是数据有问题,是三次方对极端值太敏感。规范里对 CS 的取值范围有经验约束,通常按 CV 的倍数取值或限定在某个区间内,原因就在这里。
3.2 线性矩法:把三次方换成排序后的线性组合
线性矩(L-moments)的思路是绕开高次幂。它不用离差的三次方,改用排序后样本的线性组合来构造与分布形状对应的量。定义上,前四阶线性矩可以这样算:
import numpy as np def l_moments(x_asc): """x_asc 为升序排列的样本,返回 l1, l2, t3, t4""" n = len(x_asc) i = np.arange(1, n + 1) b0 = x_asc.mean() b1 = ((i - 1) / (n - 1) * x_asc).mean() b2 = ((i - 1) * (i - 2) / ((n - 1) * (n - 2)) * x_asc).mean() l1 = b0 l2 = 2 * b1 - b0 l3 = 6 * b2 - 6 * b1 + b0 t3 = l3 / l2 # L-偏度,对应分布的形状 return l1, l2, t3每个b都是一个“按秩加权的均值”。秩越大的观测(值越大)权重越高,但权重是线性的,不存在立方那种爆炸式放大。一个极端大值在线性矩里就是权重最高的一项,贡献有限;在矩法里它是三次方,贡献被放大一两个数量级。这就是线性矩对异常值稳健的根源,不是靠什么巧妙的修正系数,是靠把运算从幂次换成线性。
从 L-偏度反推 P-III 的形状参数,需要用到 P-III 分布 L-偏度与形状参数 α 的关系:τ₃ = 6·I_{1/3}(α, 2α) − 3,其中 I 是正则化不完全贝塔函数。给定样本算出的 t3,反解出 α,再由 CS = 2/√α 得到偏态系数。
from scipy.special import betainc from scipy.optimize import brentq from scipy.special import gamma as gamma_fn def t3_of_alpha(alpha): return 6 * betainc(alpha, 2 * alpha, 1.0 / 3.0) - 3 def lmoment_fit(x): x_asc = np.sort(np.asarray(x, dtype=float)) l1, l2, t3 = l_moments(x_asc) # 由 L-偏度反解形状参数 alpha,再换算 CS = 2/sqrt(alpha) alpha = brentq(lambda a: t3_of_alpha(a) - t3, 1e-3, 200.0) cs = 2.0 / np.sqrt(alpha) # CV 同样从 L-CV 精确反解,而不是直接拿 l2/l1 近似 t2 = l2 / l1 cv = np.sqrt(t2 * np.sqrt(np.pi) * gamma_fn(alpha) / gamma_fn(alpha + 0.5)) return l1, cv, cst3_of_alpha里用到的betainc(a, b, x)是正则化不完全贝塔函数,返回 I_x(a,b)。brentq在 [1e-3, 200] 上做一维求根,这个区间对应 CS 从约 0.14 到 63,覆盖了工程实际可能的全部范围。CV 的反解用了 Gamma 分布的 L-CV 与形状参数的精确关系,早期文献里常直接拿 l2/l1 当 CV 用,在 CS 小于 1 时误差不大,CS 达到 2 以上时误差能到百分之几。
3.3 同一序列上的数值对比
拿一条 n=45 的虚构洪峰序列(均值 2400 m³/s)走一遍两种方法,会看到很典型的差异形态:
| 统计量 | 矩法 | 线性矩法 | 差异来源 |
|---|---|---|---|
| 均值 | 2400 | 2400 | 一阶矩,两者相同 |
| CV | 0.61 | 0.68 | 矩法标准差被极值主导,短序列低估 |
| CS | 2.35 | 1.42 | 三次方放大极值,矩法偏高 |
| 百年一遇设计值 | 9860 | 8720 | CS 偏高把尾部拉长,设计值被推大 |
这个对比里有个反直觉的地方:矩法的 CS 偏高,设计值反而偏大,看上去“保守是好事”。但保守不是免费的,CS 偏高的同时 CV 被低估,两个偏差方向相反,谁压过谁取决于序列的具体形状。换一条序列,很可能得到 CV 偏低、CS 偏低、设计值偏小的组合,那才是真的危险。所以不能靠“反正偏保守”来给方法选择找理由,必须知道偏差的方向和量级。
线性矩法也不是没有问题。它对序列头部的信息利用得比矩法充分,代价是对序列尾部的低值区不敏感,如果样本量很小(n 小于 20),L-偏度的抽样误差同样可观。
4. P-III 适线:CS 才是那个真正定不下来的参数
参数估计完成,接下来是适线——把理论频率曲线画到经验点据上,看它贴不贴。这一步是水文频率分析里最像“手艺”的部分,也是最容易被当成玄学的地方。实际上它有一套清晰的目标函数和几何直觉。
4.1 P-III 的三个参数与 CV、CS 的映射
P-III 型分布(皮尔逊 III 型)是三参数 Gamma 分布,密度函数为
f(x) = β^α / Γ(α) · (x − a₀)^(α−1) · exp[−β(x − a₀)]
三个参数 α(形状)、β(尺度)、a₀(位置)与水文统计量之间是一一映射:
import numpy as np from scipy.stats import pearson3 def p3_quantile(p_exceed, mean, cv, cs): """给定超过概率 p_exceed,返回 P-III 分位数""" if cs <= 0: raise ValueError("P-III 的 CS 必须为正,否则下界无意义") alpha = 4.0 / cs ** 2 beta = 2.0 / (mean * cv * cs) a0 = mean * (1.0 - 2.0 * cv / cs) # 标准 pearson3 的标准差为 1、偏度为 cs,其 ppf 接收累积概率 z = pearson3.ppf(1.0 - p_exceed, cs) # 模比系数形式:K = 1 + CV·Φ return mean * (1.0 + cv * z)三个换算关系是alpha = 4/CS²、beta = 2/(x̄·CV·CS)、a0 = x̄(1 − 2CV/CS)。从这三个式子能直接读出两件事。第一,CS 必须为正,α 才有意义;CS 趋近 0 时 α 趋于无穷,分布退化成正态,而正态在水文里是不够用的,因为洪水不可能取负值。第二,下界 a0 由 CV 和 CS 的比值决定,CS 相对 CV 越大,a0 越负,也就是分布的下界离均值越远,左尾越平坦,右尾越厚。
这就是为什么 CS 的取值如此关键。CV 主要控制曲线在中段的陡缓,CS 控制的是两头,尤其是头部。同样一条样本,CS 从 1.5 改到 2.5,中段几乎看不出变化,百年一遇那个点上能差出百分之十几。
4.2 目估适线与优化适线:目标函数决定曲线往哪偏
适线有两派做法。目估适线是人眼看经验点据,手动调 CV 和 CS,让曲线“大致穿过点群中段、头和尾都照顾到”。优化适线是定义一个目标函数,用数值方法求最小值。两派的差别不在精度,在权重分配。
最小二乘适线是最常见的优化形式,残差通常在对数域里算:
from scipy.optimize import least_squares def fit_curve(sample_desc, p_emp, x0=(2400, 0.6, 1.5)): """最小二乘适线,残差取对数域,抑制大洪水段主导""" x = np.asarray(sample_desc, dtype=float) def resid(theta): mean, cv, cs = theta model = p3_quantile(p_emp, mean, cv, cs) return np.log(x) - np.log(model) res = least_squares( resid, x0, bounds=([1.0, 1e-3, 0.05], [np.inf, 2.0, 8.0]), xtol=1e-10, ftol=1e-10 ) return res.xbounds里的上下界不是形式主义。CS 允许到 8 是因为短序列上确实可能解出这么大的值;CS 下界卡在 0.05 是因为 P-III 要求 CS 为正,接近 0 时alpha会趋于无穷,数值上会炸。残差取对数域,等于让每一段相对误差权重相同,避免几千流量级的洪水把几十流量级的小值完全压掉——用原始值做残差,最小二乘会几乎只关心最大的那几个点。
优化的结果高度依赖两件事:初值和经验频率公式。初值给 (2400, 0.6, 1.5) 和给 (2400, 1.0, 3.0),在样本短的序列上可能收敛到不同的局部极小。这就是“同一份数据两次跑出不同结果”的常见来源,不是代码有 bug,是优化问题本身有多个局部解。
规范里更常见的做法是:CV 由矩法或线性矩法确定后不再调整,只调 CS,而且 CS 按 CV 的倍数取值(比如 CS = 2CV 到 4CV 之间试几档)。这个做法牺牲了拟合精度,换来了参数的可解释性和地区一致性——相邻流域用同一套 CV、CS 关系,成果能横向比较。
4.3 海森概率格纸:为什么要把概率轴拉弯
把 P-III 曲线画在普通坐标纸上,是一条明显上翘的曲线,头部翘得几乎竖直,肉眼判断“贴不贴”非常困难。海森概率格纸做的事,是把横轴从“概率 P”换成“标准正态分位数 Φ”,也就是纵轴不变、横轴做一次概率积分变换。
变换之后,正态分布的频率曲线变成一条直线,P-III 曲线变成一条缓和的曲线,头部被拉开、尾部被压缩。这个几何变形带来的直接好处是:曲线头部(小概率、大流量)在图上占据的横向宽度变大了,目估适线时看到的偏差被放大,人眼更容易判断。同时,经验点据在图上的横向间距变得均匀,不会出现“所有点挤在左边、右边空空荡荡”的情况。
这背后是一个朴素的道理:判断拟合好坏靠的是视觉,而视觉对均匀分布的偏差最敏感。把概率轴拉成等间距,等于给判读装了一个放大镜。
注意:格纸上的直线不代表拟合好,只代表数据和正态分布一致。洪水序列几乎不可能服从正态,看到近似直线的拟合反而要怀疑是不是 CS 被压得太小。
5. 常见问题排查:参数不像话时先看这四处
参数估出来不对劲,绝大多数时候问题不在优化算法,在更前面的环节。下面四条是从实际排查里攒下来的,按出现频率排。
5.1 CS 估出负值或大于 6:先查排序和特大值
现象:线性矩法或矩法算出的 CS 为负,或者大得离谱,比如 8 以上。
原因有两个方向。负偏在洪水序列上几乎不可能出现,因为洪水分布天然右偏,出现负值基本意味着样本里有个别极端值的方向不对,比如单位记录错误(把 m³/s 记成了 L/s,差三个数量级),或者某一年混进了非洪水期的数据(比如水库泄流的恒定大流量)。CS 过大则是短序列加上极值的典型结果,n 只有二十几年,头部却有一两场远高于其余样本的大水,L-偏度被推到接近上限。
解决:先把序列按从大到小打出来,看前三名和第四名的比值。如果第一名是第四名的两倍以上,先怀疑数据错误而不是分布特性。确认数据无误后,特大值要么按历史洪水单独处理、赋经验频率,要么在参数估计时用区域信息约束 CS,不要硬让样本自己说话。
5.2 中段贴合、头部大幅偏离:目标函数和权重的问题
现象:适线出来的曲线,中间那段几乎完美穿过经验点,但最上面几个点(重现期 50 年以上的位置)和曲线差得很远,要么全在曲线上方,要么全在下方。
原因:优化适线用的残差如果是原始值,最小二乘会优先照顾数值大的样本,也就是头部,导致中段被牺牲;如果残差取了相对误差但经验频率公式在头部又不准,就会反过来。还有一种情况是样本本身在头部确实有系统性偏离,说明该流域的洪水可能不服从单一 P-III 分布,或者序列存在非一致性(比如上游修了水库之后的下游站)。
解决:先确认残差域选对了,对数域或相对误差域在头部和中段之间更平衡。然后单独看头部几个点的来水背景,如果它们来自同一场天气过程或者同一年份的上下游遭遇,考虑这是不是样本独立性问题。非一致性序列用单一分布适线,本质上是拿一个平均值去描述两段不同的物理过程。
5.3 同一份数据两次跑出不同结果:初值、公式、精度三重嫌疑
现象:同一份数据,两次运行给出的 CV、CS 在小数点后第二位就不一样,设计值差出几百。
原因:最常见的三个来源是——优化初值不同导致落到不同局部极小;经验频率公式在两次运行里用了不同的(一次 m/(n+1),一次 (m−0.5)/n);数据精度不同,一次用了原始值,一次做了四舍五入或者补了插值。
解决:把整个流程做成一条不可分割的管线,从读取数据到参数输出中间不手工干预。经验频率公式写成常量配置,不要在两处各写一遍。优化器设固定初值,或者用网格搜索加局部优化的两段式,避免初值敏感性。做完这些,同一份数据的结果应该是逐位可复现的。
5.4 频率与重现期换算错位:P 是超过概率
现象:算出来的百年一遇设计值比五十年一遇还小,或者经验点据的位置整体偏了一格。
原因:P 的定义混了。水文频率分析里 P 是超过概率,重现期 T = 1/P,P=1% 对应百年一遇。而scipy.stats里的ppf接收的是累积概率,也就是不超过概率 1−P。两者差一个补运算,写代码时漏掉1 -就是系统性错位。类似的坑还有经验频率的方向,从大到小排序后第 m 位对应的是超过概率,从小到大排序后第 m 位对应的是不超过概率,两种排序混用会得到完全镜像的结果。
解决:在函数签名里把参数名写成p_exceed,不要写p,从命名上堵住混淆。所有分位数调用统一在一个函数里完成,不做散落的ppf调用。输出结果时同时打印对应的重现期,用 T=100 这个数做一次人工核对。
6. 外延与取舍:把 30 年样本推到 100 年一遇的底气在哪
频率分析本质上是外推。样本通常只有 30 到 50 年,要回答的却是百年一遇甚至千年一遇的问题,外推倍数在三倍以上。这个动作在任何统计领域都算激进,水文能这么做,靠的是三条支撑:分布形式有物理和经验的依据、参数估计有稳健方法、结果有区域一致性可校验。
三条支撑的分量并不相同。分布形式的选择,也就是为什么用 P-III,很大程度上是历史惯性和工程规范的产物,它在下界非负、右尾厚重这两点上符合洪水的物理直觉,但没有任何理论证明洪水必须服从 P-III。参数估计这一环,线性矩法确实比矩法稳健,代价是对样本尾部的低值不敏感,在 n 小于 20 时同样不可靠。真正能提供独立校验的,是区域一致性——把邻近流域的 CV、CS 拿来对比,如果某个站算出的 CS 比周边高一倍,那多半不是这个站特殊,是样本里有故事。
我自己的习惯是,在给出任何一个设计值之前,先做三件事。第一,把 CS 固定成 CV 的两倍、三倍、四倍各算一次,看设计值对 CS 的敏感度,如果三档之间差出百分之二十以上,说明这份样本对头部信息本来就不足,任何单点结果都不能单独用。第二,把样本去掉最大的那一年重算一遍,看参数跳多少,跳得厉害说明结论被单点绑架。第三,拿区域综合的 CV、CS 范围对一下,超出范围就回头查数据,而不是急着解释“这个流域特殊”。
这三步不产生任何新信息,只是把不确定性显式地暴露出来。它们花的时间不到半小时,但能挡掉大部分低级翻车。
参数取值的取舍最终是个工程判断,不是统计判断。规范给的 CS/CV 倍比范围、短期样本的修正系数、历史洪水的权重,这些都是把统计不确定性折算成工程裕度的机制。理解这层机制之后,看到一条拟合得很漂亮的曲线,第一反应不该是满意,而是问这条曲线对哪几个数字最敏感、那几个数字是怎么来的。
从那以后我每次做完适线,都会强制走一遍“去掉最大年、固定 CS 三档、对照区域范围”这套流程,哪怕结果看起来毫无问题。有几次就是在这套流程里发现了数据里的单位错误和年份错位,那些问题在拟合图上一点都看不出来。希望帮到你。
本文还有配套的精品资源,点击获取