ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用SkillMetrics绘制泰勒图:一张图看懂模型评估三大统计量

2026/9/18 20:39:14 拓冰建站 浏览量
用SkillMetrics绘制泰勒图:一张图看懂模型评估三大统计量 从“手头一堆模式模拟结果、又被导师问‘你家模型到底行不行’”这个场景切入可能很多做气象、海洋、空气质量、水文的同学都深有体会。对着几十个站点或格点的观测与模拟数据算了一堆相关系数、标准差、RMSE却不知道怎么把“模型整体表现”讲清楚。直接贴表格太冰冷画散点图又看不出多模式之间的相对优劣。我第一次接触泰勒图Taylor Diagram的时候真是有一种“相见恨晚”的感觉——一张极坐标图把相关系数、标准差比、中心化均方根误差三个核心指标全部叠在一起哪个模式在哪方面强、哪方面弱一眼就分明。这次分享的主角是Python的SkillMetrics库。我实测下来只要数据格式准备好从导入库到出图5分钟确实是够用的。如果是第一次上手花在“理解泰勒图在画什么”上面的时间反而比写代码的时间多得多。这篇文章我把完整可跑的代码、数据准备的细节、以及我在实际项目中踩过的几个坑一并写出来希望能帮你直接把泰勒图用到自己的评估任务里。1. 泰勒图到底在画什么三个统计量如何挤进一张图很多教程上来就甩代码但如果你不清楚泰勒图每个轴代表什么跑出来也只会觉得“花花绿绿一团”根本不敢拿出去汇报。这里我拆开讲一遍用得多了你自然能一眼读出信息。泰勒图本质上是一张极坐标图它把模型评估中最常用的三个量统一到一个几何框架里相关系数R决定点的方位角。相关系数越接近1点在极坐标里的角度越接近0度也就是靠右的水平方向。标准差比σ_model / σ_obs决定点的径向距离。如果模型标准差和观测标准差完全一致这个比值是1点在半径1的圆弧上。中心化均方根误差RMSD在图中表现为该点到“观测参考点”的直线距离。这里面的几何关系其实特别优雅。把观测数据视为一个参考点通常在横轴上对应相关系数为1、标准差比值为1的位置任意一个模型点与参考点之间的距离通过余弦定理可以推导出来RMSD² σ_model² σ_obs² - 2·σ_model·σ_obs·R。所以一个二维平面内点的位置同时编码了三个统计指标。换句话说你不需要分别画三张图泰勒图就是它们的三合一投影。我记得第一次看到这个图时觉得最反直觉的地方是“为什么相关系数轴是用角度来表达”。你可以这样理解把观测序列和模拟序列先各自减去平均值变成距平序列再算它们的相关本质上是在衡量两条序列“波形是否同步”。用极坐标的角度来映射相关系数角度越小说明两条波形的相位越对齐模型模拟的“变化节律”越准。顺带提醒一点很多泰勒图教程会画“归一化”版本也就是把所有标准差都除以观测标准差。归一化之后观测参考点固定落在横轴1.0的位置图面更清爽不同变量之间也可以横向比较。SkillMetrics库两种模式都支持我后面会给出具体参数。2. 为什么选SkillMetrics而不是自己用matplotlib硬画你可能会问泰勒图又不是什么新东西matplotlib的极坐标功能也能画为什么非要引入一个SkillMetrics库我的回答是自己画不是不行但纯手搓的代码量、调试成本和易错点远比想象中多。自己实现泰勒图你需要处理的核心问题包括极坐标下的坐标轴刻度怎么标常规极坐标默认是角度等间距而泰勒图需要把相关系数的刻度映射到反余弦角度上还要在圆弧上标注0.9、0.95、0.99这类刻度。观测参考点怎么摆默认情况下参考点要放在横轴右侧所有模型点要围绕它散布轴范围需要根据最大标准差动态调整。表示RMSE的虚线弧怎么画这些弧线是以参考点为圆心、以不同RMSE值为半径的圆弧手搓要自己算角度范围和插值。图例、颜色映射、多模型标记的布局也很麻烦。SkillMetrics库通常对应GitHub上的PeterRochford/SkillMetrics项目把这些底层逻辑都封装好了。它的核心函数taylor_diagram直接接收标准差和相关系数一行就能出图。更关键是它内部还处理了掩膜数组、缺测值、坐标轴范围自适应、RMSE等值线叠加等细节这些恰恰是自绘时最容易翻车的地方。我用过之后最大的体会是这类专业可视化库的“隐形价值”不在画线本身而在它替你把该考虑的资源边界条件都处理了。就像你写爬虫可以用requests但真要处理重定向、会话保持、超时重试用requests就能省掉一堆坑。SkillMetrics扮演的就是泰勒图领域的requests。安装方面绝大多数环境走pip就能搞定pip install skillmetrics如果你的网络环境不太稳定或者pip默认源拉取较慢可以临时用国内镜像源pip install skillmetrics -i https://pypi.tuna.tsinghua.edu.cn/simple不过这里有个隐蔽的坑我后面专门用一个章节来说因为PyPI上叫skillmetrics这个名字的包不止一个。3. 5分钟跑通第一张泰勒图最简完整代码这一节直接上干货。下面这段代码是我调试过的完整版本你复制到本地只要Python环境里有numpy、matplotlib、skillmetrics就能直接运行。import numpy as np import matplotlib.pyplot as plt import skillmetrics as sm # 固定随机种子保证每次运行结果一致便于复现 np.random.seed(42) # 生成模拟观测数据100个时间点的序列 obs np.random.normal(loc5.0, scale2.0, size100) # 构造三个模拟模型结果分别叠加不同噪声 model1 obs * 0.95 np.random.normal(0, 0.8, size100) model2 obs * 1.15 np.random.normal(0, 1.5, size100) model3 obs * 0.80 np.random.normal(0, 0.3, size100) # 计算每个模型的统计量 def calc_stats(obs, model): # 去掉NaN值保证相关系数计算可靠 mask ~(np.isnan(obs) | np.isnan(model)) obs_valid obs[mask] model_valid model[mask] std_model np.std(model_valid, ddof1) corr np.corrcoef(obs_valid, model_valid)[0, 1] # 中心化均方根误差centered RMSE rmse np.sqrt(np.mean((model_valid - obs_valid) ** 2 - (np.mean(model_valid) - np.mean(obs_valid)) ** 2)) return std_model, corr, rmse # 观测标准差 std_obs np.std(obs, ddof1) # 模型统计量 stats [calc_stats(obs, model1), calc_stats(obs, model2), calc_stats(obs, model3)] std_models [s[0] for s in stats] corr_models [s[1] for s in stats] rmse_models [s[2] for s in stats] # 绘制泰勒图 fig, ax plt.subplots(figsize(8, 8)) # 关键调用SkillMetrics的taylor_diagram函数 sm.taylor_diagram(ax, std_models, corr_models, std_obs, markero, label[Model 1, Model 2, Model 3], titleTaylor Diagram Demo) ax.legend(locupper right, bbox_to_anchor(1.6, 1.0)) plt.tight_layout() plt.show() # 打印统计量备查 print(f观测标准差: {std_obs:.3f}) for i, (s, c, r) in enumerate(zip(std_models, corr_models, rmse_models)): print(fModel {i1}: std{s:.3f}, corr{c:.3f}, centered_RMSE{r:.3f})跑完这段代码你应该能看到一张典型的泰勒图最右侧那个特殊标记是观测参考点三个圆点是不同模型。稍微调整model1、model2、model3的构造公式观察点位置的变化就能很直观地理解泰勒图的几何含义。这里解释一下几个代码细节的用意ddof1计算标准差时样本自由度设为1这是样本标准差的常规做法避免小样本下有偏估计。掩膜处理np.isnan检查是为了防止数据里有缺测时np.corrcoef静默返回NaN。中心化RMSE的公式这里使用的是“去均值后的RMSE”如果只是想快速看整体误差直接用np.sqrt(np.mean((model - obs) ** 2))也可以。4. 把代码用到真实评估任务CSV数据和NetCDF数据怎么接示例代码能跑通只是第一步落到实际项目里你手里的数据大概率不是代码里现成的obs和model数组而是一张站点观测表、一份模式输出NetCDF或者一个DataFrame。这里我分享两种最常见的真实数据接入方式。4.1 从CSV表格加载站点观测与模拟数据假设你有一份CSV结构大概是第一列是站点名第二列是观测值第三列是某个模型的模拟值。import pandas as pd import numpy as np df pd.read_csv(evaluation_data.csv) obs_series df[observed].values model_series df[model_A].values然后复用3.1节里的calc_stats函数计算标准差和相关系数再调用taylor_diagram。这里有个实践细节如果CSV里有多站的观测-模拟数据直接全部塞进去算一个统计量往往会把空间差异掩盖掉。我更推荐的做法是对每个站点分别求相关系数和标准差比然后用泰勒图展示“所有站点的整体分布”这样能直观看出模型在哪些站点表现好、哪些站点表现差。4.2 从NetCDF读取格点数据以气象模式为例气象和海洋领域最常碰到的就是NetCDF格式的模式输出。以某个温度变量为例import xarray as xr ds_obs xr.open_dataset(obs.nc) ds_model xr.open_dataset(model.nc) # 假设变量名为 temp先做空间平均得到时间序列 obs_time_series ds_obs[temp].mean(dim[lat, lon]) model_time_series ds_model[temp].mean(dim[lat, lon]) # 如果两个NetCDF的时间轴不对齐需要先插值或对齐 # 一个简单粗暴的对齐方式 obs_vals obs_time_series.values model_vals model_time_series.values # 再调用之前的calc_stats std_obs np.nanstd(obs_vals, ddof1) std_model, corr, rmse calc_stats(obs_vals, model_vals)这种格点平均的处理方式适合评估“整体时空演变能力”。但如果想深入评估空间细节不能只做全局平均而是应该分成多个子区域比如中国东部、青藏高原、海洋区域每个子区域算一个点泰勒图就能看出模型在不同区域的技能差异。这就是泰勒图在科研汇报中特别好用的地方——不需要你多解释审稿人、导师看图就明白模型哪里强哪里弱。4.3 多模型、多季节对比的批量画法实际评估中很少只比较一个模型。比如做多模式集合评估时你有10个模型还想分夏季和冬季分别看那么两个季节各画一张泰勒图每张图上有10个点标记不同颜色或不同形状。fig, axes plt.subplots(1, 2, figsize(14, 6), subplot_kw{projection: polar}) season_names [Summer, Winter] for ax, season in zip(axes, season_names): obs_season ... # 取出对应季节的观测 std_obs np.nanstd(obs_season, ddof1) stds [] corrs [] for model_name in model_names: sim_season ... # 取出对应模型的对应季节 std_model, corr, _ calc_stats(obs_season, sim_season) stds.append(std_model) corrs.append(corr) sm.taylor_diagram(ax, stds, corrs, std_obs, markero, labelmodel_names, titleseason) ax.legend(locupper right, bbox_to_anchor(1.5, 1.0))小提醒两个子图共用一个legend会很乱我自己常用的办法是只在第二张图放图例或者统一在图的右侧外部放一次。另外子图的坐标轴范围如果不一致横向对比时容易误读建议固定axis_range参数强制两图的范围一致。5. 深入解读泰勒图相关系数高不等于模型更好代码跑完图出来了接下来就是更关键的一步——怎么把它“读明白”。我在几次项目汇报中发现不少人对泰勒图存在一个普遍误解觉得“相关系数最高的点就是最靠近参考点的点”其实完全不是一回事。举个例子假设模型A相关系数是0.98但标准差只有观测的0.6倍模型B相关系数0.90标准差是观测的1.05倍。从泰勒图上看模型A的“角度”非常接近0度看起来很优秀但它的径向位置明显小于1离参考点反而不近。模型B虽然相关系数略低但它的径向位置更接近1综合下来离参考点更近RMSE也更小。这就引出一个核心读图原则看一个模型的综合表现不是看相关系数大小而是看它到参考点的距离。距离越短中心化RMSE越小综合技能越好。具体场景下的读图策略如果点落在参考点正上方角度小、半径接近观测标准差说明模型不仅在“变化节奏”上和观测一致振幅也模拟得准确是最理想的情况。如果点落在半径小于1的区域说明模拟值的波动幅度偏小。常见于模型对极端事件的平滑过度比如日最高温的低估。如果点落在半径大于1的区域说明模拟值波动太剧烈常见于模式对天气系统强度的过度反应。如果相关系数低但标准差接近1说明模型的“量级和波动范围”抓到了但是“相位”不对。比如模拟的降水峰值总是偏移几个时次或者锋面系统位置偏东偏西。如果相关系数低而且标准差也偏离1这就是系统性偏差和随机误差都很大基本可以判定该模型在这个变量上不可靠。我在实际工作中还习惯把泰勒图和空间分布图搭配使用。泰勒图告诉你“模型在多站/多格点平均意义上可能低估了变率”但具体低估发生在哪里泰勒图是看不出来的。所以我通常在泰勒图选出Top1模型后再画一张偏差空间分布图或时间序列对比图来定位问题区域。6. 我踩过的几个真实的坑版本冲突、NaN、字体和图例这部分是纯经验分享。SkillMetrics库整体用起来很顺手但以下几个坑我确实都踩过有的还折腾了挺久。写出来供你避雷。6.1 同名包冲突此skillmetrics非彼skillmetrics这是最容易让人崩溃的一个坑。PyPI上有个叫skillmetrics的包是机器学习领域用来计算分类/回归指标的还有一个SkillMetrics是PeterRochford维护的气象领域泰勒图绘制库GitHub仓库名也叫SkillMetrics。两个包名完全一样但功能和接口完全不同。如果你用pip install skillmetrics之后import skillmetrics成功了但调用taylor_diagram时报AttributeError极大概率就是装错了包。遇到这种情况推荐直接卸载后从GitHub安装pip uninstall skillmetrics -y pip install githttps://github.com/PeterRochford/SkillMetrics.git如果你的网络环境访问GitHub不顺畅也可以先下载源码zip到本地然后pip install /path/to/SkillMetrics-master.zip判断装没装对最直接的方法是导入后检查import skillmetrics as sm print(dir(sm))如果里面有taylor_diagram、target_diagram这些函数那就没问题了。6.2 数据里的NaN悄悄污染统计量泰勒图画出来很漂亮但如果原始数据里有NaN很多统计函数会静默处理结果就是相关系数莫名其妙地偏低或直接NaN。SkillMetrics内部虽然对掩膜数组做了处理但你自己的numpy计算要先清洗好。我封装过一个更健壮的统计函数这里直接给你用def robust_corr_std(obs, model): mask ~(np.isnan(obs) | np.isnan(model)) if np.sum(mask) 3: return np.nan, np.nan obs_v obs[mask] model_v model[mask] corr np.corrcoef(obs_v, model_v)[0, 1] std_model np.std(model_v, ddof1) return std_model, corr如果数据量特别大建议先用xarray或pandas检查各变量的缺测比例超过30%的站点或格点建议直接剔除避免污染整体统计量。6.3 不加掩膜数组时SkillMetrics内部可能报奇怪错误这是我唯一一次看到SkillMetrics直接抛异常的场景传入的数组是普通numpy数组但数据中有inf或特大异常值导致坐标轴范围计算炸了。解决办法有两种一是把异常值手动过滤成NaN二是直接把数组转成numpy.ma.MaskedArray类型再传入。如果你手里的数据本身是xarray.DataArray转起来很简单import numpy.ma as ma obs_mask ma.masked_invalid(obs_vals) model_mask ma.masked_invalid(model_vals)然后统计量计算全部基于mask数组进行这样传给SkillMetrics的数据也天然带掩膜信息里面的NaN问题就迎刃而解了。6.4 中文字体跑到泰勒图里全变方块中文用户最普遍的问题。taylor_diagram函数内部新建的文本对象默认字体族不一定支持中文。如果你的标题、图例希望用中文显示需要在调用前全局设置matplotlib字体import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] matplotlib.rcParams[axes.unicode_minus] False注意axes.unicode_minus必须设成False否则负号会显示成方块。如果你在服务器上没有中文字体那就别硬显示中文了图例和标题用英文最省事。6.5 图例位置容易跑出画布SkillMetrics的默认布局会为图例预留空间但当模型数量多、标记类型复杂时自动布局经常打架。我的做法是显式调整legend位置和画布边距fig, ax plt.subplots(figsize(8, 8)) sm.taylor_diagram(ax, stds, corrs, std_obs, markero, labelmodel_names) ax.legend(locupper right, bbox_to_anchor(1.6, 1.0), fontsize9) plt.tight_layout()如果还是出界可以再画布外留白加宽或者用fig.subplots_adjust(right0.7)手动调整。6.6 归一化与非归一化泰勒图的选取SkillMetrics默认画的图横纵轴是标准差的原始物理单位参考点位置由观测标准差决定。如果你同时评估降水和温度两个完全不同的量物理单位不同没法画在同一张图里。这时候归一化泰勒图就很有用taylor_diagram函数内通过normalize参数或相关参数开启所有标准差都除以观测标准差参考点固定为1.0不同变量之间就可以横向对比了。我个人经验是单变量评估用非归一化图更直观因为你能直接读出标准差的实际数值多变量或者多区域横向对比时优先用归一化图不然图面会因某个变量方差过大而严重拉伸。7. 我的个人用法泰勒图不是终点而是评估流程的起点最后聊点偏个人经验的东西。用了相当长时间的SkillMetrics和泰勒图我逐渐形成了一套自己的评估流程大概是这样第一拿到模式数据后不要直接画泰勒图先做数据清洗和区域划分。我一般会先把研究区域按气候特征分成几个子区域然后按季节性切成多个子集。第二每个子集先快速算一遍相关系数和标准差比做一个简单的排序大致淘汰掉明显偏差过大的模型。第三用SkillMetrics画泰勒图选出每张图里离参考点最近的前三模型作为候选。第四回到其他诊断图比如空间分布图、偏差图、时间序列图针对候选模型做更深入的误差来源分析。这样做的好处是泰勒图帮你节省了大量的粗筛时间但它不能替代其他诊断手段。它更像是一个“体检报告”告诉你是该去心内科还是骨科但具体要做什么手术还得靠更精细的检查。关于SkillMetrics本身它还有一个姊妹功能叫target_diagram靶图专门用来区分随机误差和系统性偏差的。如果泰勒图里模型点离参考点较远你再画一张靶图就能判断这个偏离主要是“系统性偏差”还是“随机扰动”这个信息对改进模型非常有价值。我强烈建议你把这两个图配合使用模式评估的完整度会提高一个档次。另外有个小建议如果你需要在论文里展示泰勒图记得在方法部分写清楚标准差用的是“样本标准差还是总体标准差”相关系数用的是“Pearson相关系数”以及是否做了归一化。这三个细节审稿人很看重而且如果不在方法里写明读者很难直接从图上判断你的计算口径。我自己的习惯是固定用ddof1的样本标准差和Pearson相关系数并且在图注里补一句“标准差为样本标准差相关系数为Pearson相关系数”。别看这行字不起眼真能省去不少审稿阶段的来回解释。