You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Bokeh实现TP/TN/FP/FN可调的交互式动态ROC曲线

基于Bokeh的动态ROC曲线实现方案

你之前的交互框架无法联动ROC曲线,核心问题是CustomJS回调里只写了正弦曲线的坐标更新逻辑,没有嵌入混淆矩阵到ROC坐标的计算规则,同时缺少混淆矩阵的数值约束(TP+FN需固定为总正样本量、FP+TN需固定为总负样本量,否则指标计算会出现逻辑矛盾)。

实现要点

  • 采用纯前端CustomJS回调实现计算逻辑,最终可导出为单HTML文件,离线即可演示,不需要启动Python服务
  • 滑块做联动约束:调整TP时自动计算并更新FN值,调整FP时自动计算并更新TN值,避免出现混淆矩阵总和不固定的问题
  • ROC曲线采用标准三点折线结构:从原点(0,0)出发,经过当前混淆矩阵对应的(FPR, TPR)点,最终到(1,1)点,和单阈值下分类器的ROC形态完全一致
  • 同步计算并展示AUC、灵敏度、特异度三个核心指标,方便演示时对照数值变化

如果需要展示平滑的多段ROC曲线,只需要在JS回调里扩展roc_src.data的fpr、tpr数组,加入多个模拟阈值对应的坐标点即可,核心的数据源更新逻辑不需要改动。

可直接运行的完整代码

from bokeh.plotting import figure, show, output_file
from bokeh.models import Slider, CustomJS, ColumnDataSource, Span, Label
from bokeh.layouts import column, row

# 基础配置:固定总正/负样本量,避免混淆矩阵总和变动
TOTAL_POS = 100
TOTAL_NEG = 100
init_tp, init_fn, init_fp, init_tn = 75, 25, 20, 80

# 初始化ROC坐标点
init_fpr = init_fp / (init_fp + init_tn)
init_tpr = init_tp / (init_tp + init_fn)
roc_source = ColumnDataSource(data={
    "fpr": [0, init_fpr, 1],
    "tpr": [0, init_tpr, 1]
})
# 初始化指标数据源
init_auc = 0.5*(0+init_tpr)*init_fpr + 0.5*(init_tpr+1)*(1-init_fpr)
metric_source = ColumnDataSource(data={
    "auc": [init_auc],
    "sensitivity": [init_tpr],
    "specificity": [1 - init_fpr]
})

# 画布配置
p = figure(width=550, height=500, title="动态ROC曲线演示", x_range=(0,1), y_range=(0,1))
p.xaxis.axis_label = "假阳性率 (FPR)"
p.yaxis.axis_label = "真阳性率 (TPR)"
# 随机分类基准线
p.line([0,1], [0,1], line_dash="dashed", color="gray", legend_label="随机分类基准")
# 绘制ROC曲线
roc_line = p.line(x="fpr", y="tpr", source=roc_source, line_width=3, color="#1f77b4", legend_label="当前模型ROC")
# 指标显示标签
auc_label = Label(x=0.6, y=0.2, text=f"AUC: {init_auc:.3f}", text_font_size="12pt")
sens_label = Label(x=0.6, y=0.15, text=f"灵敏度: {init_tpr:.3f}", text_font_size="12pt")
spec_label = Label(x=0.6, y=0.1, text=f"特异度: {1-init_fpr:.3f}", text_font_size="12pt")
p.add_layout(auc_label)
p.add_layout(sens_label)
p.add_layout(spec_label)
p.legend.location = "bottom_right"

# 四个混淆矩阵滑块
tp_slider = Slider(start=0, end=TOTAL_POS, value=init_tp, step=1, title="真阳性(TP)")
fn_slider = Slider(start=0, end=TOTAL_POS, value=init_fn, step=1, title="假阴性(FN)", disabled=True)
fp_slider = Slider(start=0, end=TOTAL_NEG, value=init_fp, step=1, title="假阳性(FP)")
tn_slider = Slider(start=0, end=TOTAL_NEG, value=init_tn, step=1, title="真阴性(TN)", disabled=True)

# 纯JS回调逻辑,无需后端支持
callback = CustomJS(args=dict(
    tp=tp_slider, fn=fn_slider, fp=fp_slider, tn=tn_slider,
    roc_src=roc_source,
    total_pos=TOTAL_POS, total_neg=TOTAL_NEG,
    auc_lbl=auc_label, sens_lbl=sens_label, spec_lbl=spec_label
), code="""
// 数值约束:保证TP+FN=总正样本,FP+TN=总负样本
const tp_val = tp.value;
const fn_val = total_pos - tp_val;
fn.value = fn_val;
const fp_val = fp.value;
const tn_val = total_neg - fp_val;
tn.value = tn_val;

// 计算当前阈值下的FPR、TPR
const fpr = fp_val / (fp_val + tn_val);
const tpr = tp_val / (tp_val + fn_val);

// 更新ROC曲线坐标
roc_src.data = {
    fpr: [0, fpr, 1],
    tpr: [0, tpr, 1]
};
roc_src.change.emit();

// 梯形法计算AUC,更新指标显示
const auc = 0.5 * (0 + tpr) * fpr + 0.5 * (tpr + 1) * (1 - fpr);
auc_lbl.text = `AUC: ${auc.toFixed(3)}`;
sens_lbl.text = `灵敏度: ${tpr.toFixed(3)}`;
spec_lbl.text = `特异度: ${(1-fpr).toFixed(3)}`;
""")

# 绑定滑块事件
tp_slider.js_on_change('value', callback)
fp_slider.js_on_change('value', callback)

# 页面布局
layout = column(
    p,
    row(tp_slider, fn_slider),
    row(fp_slider, tn_slider)
)

output_file("dynamic_roc.html")
show(layout)

效果说明

运行代码后会自动生成HTML文件,拖动滑块即可观察曲线变化规律:

  • 当TP拉满、FP为0时,ROC曲线贴左上角,AUC=1,对应完美分类器
  • 当TP=FN、FP=TN时,ROC曲线和对角基准线重合,AUC=0.5,对应随机猜测的分类器
  • 当TP很低、FP很高时,ROC曲线会贴右下角,AUC<0.5,对应预测结果完全反向的分类器

如果需要支持FN、TN滑块也可手动调整,只要给两个滑块绑定回调,在回调里增加反向约束逻辑即可(比如调整FN时自动更新TP=总正样本-FN,调整TN时自动更新FP=总负样本-TN)。


内容的提问来源于stack exchange,提问作者Chris

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 02:21:28