Py学习  »  Python

HF.139|顶刊三元图美图合集+复现教程 [Python]

走天涯徐小洋地理数据科学 • 4 小时前 • 9 次点击  


一套水文模型究竟模拟得好不好,往往取决于我们关注什么。研究洪水时,人们更重视径流;研究土壤干旱时,会关注土壤水分;研究流域耗水时,又需要检验蒸散发。当三个变量的模拟精度并不一致,只给一个综合分数,就容易掩盖评价侧重点带来的差异。三元图提供了一种直观的表达方式:把三个评价权重放进同一个三角形,再用颜色显示综合评分。在水文研究中,它可以展示径流组成,也可以表示多目标评价的权重组合。下面结合顶刊中的案例,介绍三元相图的相关内容。


01原理介绍



三元图的核心原理,是用三角形内一个点的位置,表示三个非负、且总和为 100% 的分量。例如三个分量分别为 50%、30%、20%,就能在三角形中找到唯一对应的点。


(图1 三元图原理示意)


02顶刊案例



下图的流量图是一种三元图,其中每个维度代表一个模型的径流通量,每个模型运行根据其等效最终径流通量占总模拟值的比例,投影为一个点。原文采用 SIMHYD水文模型,把超渗产流(INFexc)、壤中流与蓄满产流(INT & SATexc)、基流(BAS)的体积贡献映射到三元图,并用 NSE 着色。它提供的核心启示是:相似的拟合指标,可以对应不同的内部组分贡献。读图时,位置回答“模型如何分配径流”,颜色回答“总径流拟合得怎样”。两者不能互相替代。


(图2 图源:https://doi.org/10.1029/2018WR023750)


另一个研究以密歇根湖流域的 20 个集水区为对象,比较不同资料约束下的水文模拟。作者将径流、土壤水分和蒸散发评价权重放入三元图。研究在率定之后,对固定的非支配解集合计算不同权重下的综合效率,再以集合中位数构成性能曲面。这里三轴表示评价权重,颜色表示综合评分。它帮助读者识别:对某一变量赋予更多权重时,模型的综合评价如何变化。


(图3 图源:https://doi.org/10.1029/2022WR032064)


图示为经典的水力几何 b-f-m 三元图,用于分类河流的调整方式。图中三个顶点分别对应河宽(b)、水深(f)和流速(m)指数,三者归一化后相加为 1。原作者将 400 多个瞬时水力几何(AHG,蓝点)与 150 个下游水力几何(DHG,红点)数据投射在同一个三角空间中作对比。蓝点和红点在图中占据了明显不同的区域(蓝点偏右上,红点偏左下)。这种清晰的分类直接证明:这两种水力几何规律,对应着截然不同的河道演变过程和边界条件。


(图4 图源:https://doi.org/10.1029/2020WR029430)



03案例复现



以图3为例:

(1)准备评分:每行是一组模型方案,每列是一个评价目标,首先准备三个变量的 KGE 评分

例如,第 3 组方案的径流评分较高,但土壤水分评分低于第 6 组。这种差异体现了不同方案之间的多目标权衡。本例直接使用虚构评分。实际研究中,这张表通常由各参数方案的模拟结果与同一评价期的参考数据比较得到。


(2)生成权重:用5151个点覆盖整个三角形

三个权重分别为:

满足:

本例将步长设置为 0.01。用整数生成前两个权重,再由总和约束确定第三个:

其中:。例如,当 = 0.60 时,土壤水分权重可以从 0 到 0.40,蒸散发权重随之从 0.40 降到 0。

全部组合数量为:


(3)逐方案加权:每组权重下计算7个综合评分

对第 r 个方案,综合评分为:

例如,取:,表示评价时径流占 60%,土壤水分占 30%,蒸散发占 10%。第 1 组方案的评分为:

对 7 组方案重复计算:


(4)取中位数:将每组权重下的 7 个评分合并成一个颜色值

一个位置只能显示一个填色值,因此需要汇总该位置上不同方案的评分。本例采用中位数:

将上一步 的 7 个评分排序:0.854, 0.862, 0.863, 0.866, 0.870, 0.871, 0.873。中间的第 4 个数为 0.866,所以权重组合 (0.6, 0.3, 0.1) 对应的圆面颜色值就是 0.866。

ce = np.median(ce_each, axis=0)

这里 axis=0 表示沿方案方向取中位数,最终得到 5151 个颜色值。必须先加权,再取中位数。不能先求三个评分列各自的中位数,再进行加权。

例如,三个评分列的中位数为 0.89、0.88、0.70,直接等权平均得到:(0.89 + 0.88 + 0.70) / 3 ≈ 0.8233。但本例每组保留方案的三项评分之和均为 2.46,正确的等权综合评分中位数为:


(5)绘图:将权重变成位置,将综合评分变成颜色

先确定三个顶点:左下角:径流权重为 1;右下角:土壤水分权重为 1;上顶点:蒸散发权重为 1。之后将权重转换为普通二维坐标:

(6)复现结果图:


三个顶点分别对应径流、土壤水分和蒸散发权重为 100%,颜色表示综合评分中位数。图中底部偏蓝、顶部偏暖,说明这组虚构数据中,径流和土壤水分评分较高,蒸散发评分较低。增加蒸散发评价权重,综合评分总体下降;等权中心评分为 0.82。



04复现代码


from pathlib import Pathimport csvimport jsonimport numpy as npimport matplotlibmatplotlib.use('Agg')import matplotlib.pyplot as pltimport matplotlib.tri as mtri
OUT = Path(__file__).resolve().parent# Columns: KGE_Q, KGE_Sstar, KGE_ET. These are invented scores.scores = np.array([    [.88, .88, .70], [.90, .85, .71], [.92, .82, .72],    [.91, .86, .69], [.89, .90, .67], [.87, .91, .68],    [.86, .89, .71], [.70, .70, .60]])
def nondominated(a):    # Higher scores are better. A strict improvement in one column is required.    return np.array([not np.any(np.all(a >= row, axis=1)                     & np.any(a > row, axis=1)) for row in a])
keep = nondominated(scores)nds = scores[keep]assert len(nds) == 7with (OUT/'synthetic_scores.csv').open('w', newline='', encoding='utf-8') as f:    writer = csv.writer(f)    writer.writerow(['solution_id', 'KGE_Q', 'KGE_Sstar', 'KGE_ET', 'is_NDS'])    for k, row in enumerate(scores):        writer.writerow([k+1, *row, int(keep[k])])
# Integer grid avoids accumulated floating-point step errors.# Order: Q (left), S* (right), ET (top).N = 100weights = np.array([[i, j, N-i-j]                   for i in range(N+1) for j in range(N+1-i)], float)/Nassert len(weights) == 5151assert np.all(weights >= 0) and np.allclose(weights.sum(1), 1)ce_each = nds @ weights.T                 # shape: 7 solutions x 5151 weightsce = np.median(ce_each, axis=0)           # median across solutions
def xy(w):    w = np.asarray(w, float)    return w[..., 1]+.5*w[..., 2], np.sqrt(3)/2*w[..., 2]
x, y = xy(weights)mesh = mtri.Triangulation(x, y)np.savetxt(OUT/'weight_surface.csv', np.column_stack([weights, x, y, ce]),           delimiter=',', header='w_Q,w_Sstar,w_ET,x,y,CE_median', comments='')fig, ax = plt.subplots(figsize=(7.5, 6.4))levels = np.linspace(.60, .90, 13)filled = ax.tricontourf(mesh, ce, levels=levels, cmap='RdBu', extend='both')# Draw only thresholds actually crossed by the surface; donot fabricate lines.for level, color, style in [(.48, '#bdbdbd', '--'),                             (.65, '#bdbdbd', '-'), (.83, '#303030', '--')]:    if ce.min() < level < ce.max():        lines = ax.tricontour(mesh, ce, levels=[level], colors=[color],                              linestyles=[style], linewidths=1.8)        ax.clabel(lines, fmt='%.2f', fontsize=10)for k in range(3):    for t in [.2, .4, .6, .8]:        p = np.zeros((2, 3)); p[:, k] = t        p[0, (k+1)%3] = 1-t; p[1, (k+2)%3] = 1-t        ax.plot(*xy(p), color='gray', lw=.55, ls='--', alpha=.65)h = np.sqrt(3)/2ax.plot([0, 1, .5, 0], [0, 0, h, 0], color='#333333', lw=1.4)for t in np.linspace(0, 1, 6):    ax.text(t, -.035, f'{t:.1f}', ha='center', va='top', fontsize=10)    ax.text(.5*(1-t)-.035, h*(1-t), f'{t:.1f}', ha='right', fontsize=10)    ax.text(1-.5*t+.035, h*t, f'{t:.1f}', ha='left', fontsize=10)ax.text(.5, -.105, r'$w_{S^*}$', ha='center', fontsize=15)ax.text(.13, .45, r'$w_Q$', rotation=60, ha='center', fontsize=15)ax.text(.87, .45, r'$w_{ET}$', rotation=-60, ha='center', fontsize=15)ax.set(xlim=(-.14, 1.14), ylim=(-.13, h+.08), aspect='equal')ax.axis('off')ax.set_title('Panel b method reproduction | SYNTHETIC DATA', fontsize=13, pad=16)bar = fig.colorbar(filled, ax=ax, shrink=.8, pad=.06,                  ticks=[.60, .65, .70, .75, .80, .85, .90])bar.set_label('Median combined efficiency')fig.tight_layout()for ext in ['png', 'svg']:    fig.savefig(OUT/f'panel_b_synthetic.{ext}', dpi=300, bbox_inches='tight')plt.close(fig)check_w = np.array([[1,0,0], [0,1,0], [0,0,1], [1/3]*3, [.6,.3,.1]])values = np.median(nds @ check_w.T, axis=0)assert np.allclose(values, [.89, .88, .70, .82, .866])report = dict(synthetic=True, candidate_count=len(scores), NDS_count=len(nds),              weight_count=len(weights), grid_step=1/N,              minimum=float(ce.min()), maximum=float(ce.max()),              check_values=values.tolist())(OUT/'validation.json').write_text(json.dumps(report, indent=2), encoding= 'utf-8')print(json.dumps(report, indent=2))

一个三角形,可以呈现三种组分的相对贡献,也能展示三个评价目标之间的权衡。在水文研究中,三元图让径流组成、过程差异和模型表现变得更加直观。读懂它,关键是先看清三个顶点代表什么,再理解位置与颜色分别传递的信息。希望通过本文的原理介绍与代码复现,你不仅能看懂论文中的三元图,也能将自己的数据放进这个三角形,让复杂的水文关系得到清晰表达!


一图胜千言!水文图绘改版后致力于分享水文相关的精美图表,为读者提供作图思路和经验,帮助大家制作更漂亮丰富的图表。同时欢迎留言咨询绘图难点,我们会针对性地分享相关绘制经验。另外也期待读者踊跃来稿,分享更好的构图思维和技巧,稿件可发送至邮箱hydro90@126.com, 或者联系微信17339888901投稿。

图片

编辑:牛钰婷 孟雪 |校稿:hydro90编委团


图片


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/203359