Bokeh:在 ColumnDataSource 中编辑数据的某些方法之间有什么区别

Bokeh: Whats the differences between certain methods of editing data in a ColumnDataSource

我对 Bokeh 2.3.0 服务器应用程序中的 ColumnDataSource 有疑问。
下面是一个试图说明我的问题的例子。尽管它有点长,但我花了很多精力让它尽可能少但完整。

因此,我知道至少有两种编辑 ColumnDataSource 中的数据的主要方法可行。
第一个是通过使用'index_way'(我不知道如何正确调用此方法)通过使用source.data['my_column_name'][<numpy_like_array_indexing>] = 'my_new_value'其中<numpy_like_array_indexing>可以导致类似于 [0:10][[True,False,True]] 等。像 numpy 数组一样对数据进行子集化。这样,例如可以使用 source.selected.indices 来索引数据。

second 方法正在使用 ColumnDataSource.patch() function。参考调用描述为 在特定位置有效更新数据源列

我在代码中遇到的 第三种 方法是 editing/changing ColumnDataSource 中的完整列,如 source.data['my_data_column_1'] = source.data['my_data_column_2']。这样,我可以将数据列设置为已经存在的数据列。

我的问题是: 它们的设计行为是否不同?我发现使用 'index' 方法所做的更改不会传播或更新到 HoverTool,而对于其他两种方法,这似乎有效。
在以下代码示例中可以看到此行为。当更改绘图中的前几个样本时,通过使用选择工具选择它们并通过 label_selected_via_index() 编辑 source.data['Label'],HoverTool 不会显示 'Label' 的正确和更新值。但是,数据的更改是实际执行的,check_label() 可以看到它访问并打印了 source.data['Label'].
的前几个样本 将鼠标悬停在数据上时,使用其他方法之一更改 Label 值确实会显示正确和更新的值。

import pandas as pd
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, LinearColorMapper, Dropdown, Button, HoverTool
from bokeh.layouts import layout
import random
import time

plot_data = 'Value1'
LEN = 1000
df = pd.DataFrame({"ID":[i for i in range(LEN)], 
                   "Value1":[random.random() for i in range(LEN)], 
                   "Value2":[random.random() for i in range(LEN)], 
                   "Color": [int(random.random()*10) for i in range(LEN)] })

df['plot_data'] = df[plot_data]
df['Label'] = "No Label Set"
df['Label_new_col'] = "Label was added"

source = ColumnDataSource(df)

cmap = LinearColorMapper(palette="Turbo256", low = 0, high = 3)

def make_tooltips():
    return [('ID', '@ID'), 
            ('Label', '@Label'), 
            (plot_data, f'@{plot_data}')]

tooltips = make_tooltips()

hover_tool = HoverTool(tooltips=tooltips)
plot1 = figure(plot_width=800, plot_height=250, tooltips=tooltips, tools='box_select')
plot1.add_tools(hover_tool)                  
circle = plot1.circle(x='ID', y='plot_data', source=source, 
                      fill_color={"field":'Color', "transform":cmap}, 
                      line_color={"field":'Color', "transform":cmap})

def update_plot_data(event):
    global plot_data
    plot_data = event.item
    source.data['plot_data'] = source.data[plot_data]
    hover_tool.tooltips = make_tooltips()

dropdown = Dropdown(label='Change Value', menu=["Value1","Value2"])
dropdown.on_click(update_plot_data)

def label_selected_via_index(event):
    t0 = time.time()
    selected = source.selected.indices
    source.data['Label'][0:10] = 'Label was added'
    hover_tool.tooltips = make_tooltips()
    source.selected.indices = []
    print(f"Time needed for label_selected_via_index: {time.time()-t0:.5f}")

button_set_label1 = Button(label='Set Label via Index')
button_set_label1.on_click(label_selected_via_index)

def label_selected_via_patch(event):
    t0 = time.time()
    selected = source.selected.indices
    patches = [(ind, 'Label was added') for ind in selected]
    source.patch({'Label': patches})
    hover_tool.tooltips = make_tooltips()
    source.selected.indices = []
    print(f"Time needed for label_selected_via_patch: {time.time()-t0:.5f}")

button_set_label2 = Button(label='Set Label via Patch')
button_set_label2.on_click(label_selected_via_patch)

def label_selected_via_new_col(event):
    t0 = time.time()
    selected = source.selected.indices
    source.data['Label'] = source.data['Label_new_col']
    hover_tool.tooltips = make_tooltips()
    source.selected.indices = []
    print(f"Time needed for label_selected_via_new_col: {time.time()-t0:.5f}")

button_set_label3 = Button(label='Set Label via New Column ')
button_set_label3.on_click(label_selected_via_new_col)

def check_label(event):
    print(f"first 10 labels: {[l for l in source.data['Label'][0:10]]}")

button_label_check = Button(label='Check Label')
button_label_check.on_click(check_label)

layout_ = layout([[plot1],
                  [dropdown],
                  [button_set_label1 ,button_set_label2, button_set_label3],
                  [button_label_check]])
curdoc().add_root(layout_)

在我的应用程序中,我有大量数据并观察到,使用 .patch() 确实比索引版本或替换完整列花费的时间要长得多。在我的应用程序中,索引方法需要不到一毫秒,而补丁方法需要一秒以上,这使得在交互更改值时一切都更加滞后。基本上,我的应用程序在某种程度上类似于上面关于在一个图中选择样本并通过多个按钮分配标签的过程的示例。这些标签也通过工具提示显示在多个图中,所以这个更新对我来说是必要的。

有没有办法 A) 使索引版本也更新 Hovertool?我更喜欢这种方法,因为它在视觉上更快,或者 B) 以某种方式使 .patch() 版本更快?

我希望我能让我的问题以某种方式被理解,并感谢您的任何建议。

在 Bokeh 服务器应用程序的上下文中,值得牢记的是,“真正需要发生什么才能让更改显示在浏览器中?”答案大致是:

  • 在 Python
  • 中检测到(或发出信号)变化
  • 更改事件被序列化并通过网络发送到浏览器
  • 更改事件由 BokehJS 反序列化
  • 更改已应用,浏览器中的视图是更新

几乎 Bokeh 总是处理最后三个步骤(模数任何实际错误或待定功能)。所以这个问题真的归结为“有什么方法可以发出变化信号”到 Bokeh? 让我们从描述可用和预期的位置开始(而不是从差异或非预期的开始)。

直接赋值给属性

为了在浏览器中看到变化而更新 Bokeh 对象的第一个主要方法是 为 Bokeh 属性 分配一个全新的值。如果你这样做,例如.prop_name = new_value,字面上包括“点”和“等号”,然后 Bokeh 可以自动神奇地检测到更改并将其发送到浏览器。这里有几个例子:

plot.title.text = "New title"  # updates the title
glyph.line_color = "red"       # change a glyph's line color
slider.value = 10              # sets a slider's value

上面的示例都显示了基本的标量(字符串、数字)值,但这同样适用于更复杂的值。这种通用机制的另一个极其常见的例子是更新 ColumnDataSource

的整个 .data 字典
source.data = {'x': [...], 'y': [...]} # new data for a glyph or table

这会更新 所有 CDS 中的数据,例如线条字形可能会自行重新绘制。

根据您在做什么、数据的大小等,更新整个 .data dict 可能很昂贵(由于序列化、反序列化、网络传输等)。因此,在特定情况下,还有一些其他方法可能更有效。

“就地”特例

上面的显着特征是一切都是“整体”分配,即没有变异或就地修改。在少数情况下,Bokeh 可以自动神奇地处理对可变值的就地更新。在不深入杂草的情况下,到目前为止,最重要的示例是通过使用标准 Python dict 索引分配在 ColumnDataSource 中设置 单个 新列.data:

source.data['x'] = [...]  # Bokeh will automatically handle this

这是您上面的第三种方法。它工作正常,但 用于更新 CDS .data 字典中的列。此方法仅通过线路发送一列新数据。只要您只需要更新大型 CDS 中的一个或几个列,它可能比分配一个新的整个 .data 值更快。


什么 不起作用 基本上是任何其他类型的就地赋值:

source.data['x'][100:200] = [...] # Bokeh does not automatically handle this

这是您上面的 First ("index") 方法,它是一个非启动器。这种用法不会触发浏览器的任何变化。

TLDR 是将每个 CDS 序列包装在某些覆盖标准 getitem/setitem 机制的自定义 class 中,这只会使通用使用效率太低,并且无法证明权衡取舍。 Bokeh 不会自动神奇地注意到或对这样的变异分配做任何事情。 (如果你是 在 BokehJS JavaScript 端,那么你可以像这样进行就地赋值,然后手动调用一个 change.emit() 来手动触发更新,但这只是 纯 JS 方面的事情)。

针对优化案例的专用 API

认识到有时甚至将 CDS 更新限制在单个列中仍然不够有效,patch and stream 方法已添加到 ColumnDataSource

这些方法适用于以下情况:

  • 我想在所有列的末尾附加一些新值,而不是再次发送所有数据。 (例如,用于有效地传输新股票代码或其他传感器数据)

  • 我想更新我的大型时间序列或图像中间的一些特定值,但只发送更新后的值而不重新发送其他所有值。

这是您的第二种 方法,对于相对于总数据大小的小更新,它通常要快得多。至于 patch 具体,你可以看到例如patch_app.py example in the examples folder:

此示例以 20Hz 的更新速率同时更新三个独立的散点图、多线图和图像图。签入版本的数据大小相当适中,但我通过将所有数据增加 10-100 倍,在本地再次测试它,它仍然保持不变。如果您看到与补丁不同的东西(即多秒更新时间),那么需要一个完整的 Minimal Reproducing Example 来实际调查。