数据源#
任何数据可视化的基础都是底层数据。本节描述了向Bokeh提供数据的各种方法,从直接传递数据值到创建ColumnDataSource (CDS)并使用CDSView过滤数据。
使用Python列表提供数据#
使用标准的Python数据列表直接将值传递到绘图函数中。
在这个例子中,列表 x_values 和 y_values 将数据传递给 circle() 函数(更多示例请参见 绘图函数):
from bokeh.plotting import figure
x_values = [1, 2, 3, 4, 5]
y_values = [6, 7, 2, 3, 6]
p = figure()
p.circle(x=x_values, y=y_values)
提供NumPy数据#
与使用Python列表和数组类似,你也可以在Bokeh中使用NumPy数据结构:
import numpy as np
from bokeh.plotting import figure
x = [1, 2, 3, 4, 5]
random = np.random.standard_normal(5)
cosine = np.cos(x)
p = figure()
p.circle(x=x, y=random)
p.line(x=x, y=cosine)
将数据作为ColumnDataSource提供#
ColumnDataSource (CDS) 是大多数 Bokeh 图的核心。它为您的图的图形提供数据。
当你将像Python列表或NumPy数组这样的序列传递给Bokeh渲染器时,Bokeh会自动为你创建一个包含这些数据的ColumnDataSource。然而,自己创建ColumnDataSource可以让你访问更多高级选项。
例如:创建你自己的ColumnDataSource允许你在多个图表和小部件之间共享数据。如果你使用一个单一的ColumnDataSource与多个渲染器一起使用,这些渲染器也会共享你使用Bokeh工具栏中的选择工具选择的数据信息(参见链接选择)。
将ColumnDataSource视为一系列数据的集合,每个数据都有其自己唯一的列名。
创建ColumnDataSource#
要创建一个基本的ColumnDataSource对象,你需要一个Python字典来传递给对象的data参数:
Bokeh 使用字典的键作为列名。
字典的值用作您的ColumnDataSource的数据值。
你作为字典的一部分传递的数据可以是任何非字符串的有序值序列,例如列表或数组(包括NumPy数组和pandas Series):
data = {'x_values': [1, 2, 3, 4, 5],
'y_values': [6, 7, 2, 3, 6]}
source = ColumnDataSource(data=data)
注意
ColumnDataSource中的所有列具有相同的长度。因此,传递给单个ColumnDataSource的所有值序列也必须具有相同的长度。如果您尝试传递不同长度的序列,Bokeh将无法创建您的ColumnDataSource。
使用ColumnDataSource绘图#
要将ColumnDataSource与渲染器函数一起使用,您至少需要传递以下三个参数:
x: ColumnDataSource 列的名称,该列包含绘图中 x 值的数据y: ColumnDataSource 列的名称,该列包含绘图中 y 值的数据source: 包含您刚刚为x和y参数引用的列的 ColumnDataSource 的名称。
例如:
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
# create a Python dict as the basis of your ColumnDataSource
data = {'x_values': [1, 2, 3, 4, 5],
'y_values': [6, 7, 2, 3, 6]}
# create a ColumnDataSource by passing the dict
source = ColumnDataSource(data=data)
# create a plot using the ColumnDataSource's two columns
p = figure()
p.circle(x='x_values', y='y_values', source=source)
修改ColumnDataSource#
要修改现有ColumnDataSource的数据,请更新ColumnDataSource对象的.data属性:
向现有的ColumnDataSource添加新列:
new_sequence = [8, 1, 4, 7, 3] source.data["new_column"] = new_sequence
注意
您添加的列的长度必须与现有列的长度匹配。
要替换现有ColumnDataSource中的所有数据,请将
.data属性分配一个全新的字典:source.data = new_dict
注意
替换ColumnDataSource的全部内容也是更新其列长度的唯一方法。当您以改变任何列长度的方式更新数据时,必须通过传递一个新的字典同时更新所有列。无法一次更新一列的列长度。
使用pandas DataFrame#
data 参数也可以是 pandas 的 DataFrame 或 GroupBy 对象:
source = ColumnDataSource(df)
如果您使用pandas的DataFrame,Bokeh中的结果ColumnDataSource将具有与DataFrame列对应的列。列的命名遵循以下规则:
如果
DataFrame有一个命名的索引列,ColumnDataSource 也会有一个同名的列。如果索引名称为
None,则ColumnDataSource将具有一个通用名称: 要么是index(如果该名称可用),要么是level_0。
使用pandas的MultiIndex#
如果您使用 pandas 的 MultiIndex 作为 Bokeh 的 ColumnDataSource 的基础,Bokeh 在创建 ColumnDataSource 之前会将列和索引展平。对于索引,Bokeh 会创建一个元组索引,并将 MultiIndex 的名称用下划线连接。列名也会用下划线连接。例如:
df = pd.DataFrame({('a', 'b'): {('A', 'B'): 1, ('A', 'C'): 2},
('b', 'a'): {('A', 'C'): 7, ('A', 'B'): 8},
('b', 'b'): {('A', 'D'): 9, ('A', 'B'): 10}})
cds = ColumnDataSource(df)
这将生成一个名为 index 的列,其值为 [(A, B), (A, C), (A, D)],
以及名为 a_b、b_a 和 b_b 的列。
此过程仅适用于字符串类型的列名。如果您使用的是非字符串列名,您需要在使用它作为Bokeh ColumnDataSource的基础之前手动展平DataFrame。
使用pandas的GroupBy#
group = df.groupby(('colA', 'ColB'))
source = ColumnDataSource(group)
如果你使用一个pandas GroupBy对象,ColumnDataSource的列对应于调用group.describe()的结果。describe方法为所有未分组的原始列生成统计度量(如mean和count)的列。
生成的 DataFrame 具有 MultiIndex 列,包含原始列名和计算出的度量。Bokeh 使用上述规则对数据进行扁平化处理。
例如:如果一个DataFrame有列'year'和'mpg',将df.groupby('year')传递给ColumnDataSource将生成诸如'mpg_mean'的列。
注意
适应GroupBy对象需要pandas版本0.20.0或以上。
将数据附加到ColumnDataSource#
ColumnDataSource 流式传输是一种将新数据追加到 ColumnDataSource 的高效方式。当你使用 stream() 方法时,Bokeh 只会将新数据发送到浏览器,而不是发送整个数据集。
stream() 方法接受一个
new_data 参数。此参数期望一个字典,该字典将列名映射到您想要附加到相应列的数据序列。
该方法接受一个额外的可选参数rollover。这是要保留的数据的最大长度。当数据量超过您定义的最大值时,Bokeh将丢弃列开头的数据。rollover的默认值为None。此默认值允许数据无限增长。
source = ColumnDataSource(data=dict(foo=[], bar=[]))
# has new, identical-length updates for all columns in source
new_data = {
'foo' : [10, 20],
'bar' : [100, 200],
}
source.stream(new_data)
有关使用流处理的示例,请参见 examples/server/app/ohlc。
替换ColumnDataSource中的数据#
ColumnDataSource 修补是一种高效更新数据源切片的方法。通过使用 patch() 方法,Bokeh 只向浏览器发送新数据,而不是整个数据集。
patch() 需要一个字典,该字典将列名映射到表示要应用的补丁更改的元组列表。
你可以与patch()一起使用的元组示例:
(index, new_value) # replace a single column value
# or
(slice, new_values) # replace several column values
有关完整示例,请参见 examples/server/app/patch_app.py。
数据转换#
到目前为止,您已经向ColumnDataSource添加了数据以控制Bokeh图表。
然而,您也可以直接在浏览器中执行一些数据操作。
在浏览器中动态计算颜色映射,例如,可以减少Python代码的数量。如果颜色映射的必要计算直接在浏览器中进行,您还需要发送更少的数据。
本节概述了可用的不同转换对象。
客户端颜色映射#
通过颜色映射,您可以将数据序列中的值编码为特定颜色。
Bokeh 提供了三个函数,可以直接在浏览器中执行颜色映射:
linear_cmap()函数用于线性颜色映射log_cmap()函数用于对数颜色映射eqhist_cmap()函数用于均衡直方图颜色映射
所有三个函数的操作方式相似,并接受以下参数:
ColumnDataSource列的名称,包含要映射颜色的数据调色板(可以是Bokeh的预定义调色板之一或自定义颜色列表)
min和max颜色映射范围的值。
颜色映射函数将数据源中的数值映射到调色板的颜色中,从min到max值。
例如,使用linear_cmap()函数,范围为[0,99]
和颜色['red', 'green', 'blue']将导致以下
值到颜色的映射:
x < 0 : 'red' # values < low are clamped
0 >= x < 33 : 'red'
33 >= x < 66 : 'green'
66 >= x < 99 : 'blue'
99 >= x : 'blue' # values > high are clamped
例如:
fill_color=linear_cmap('counts', 'Viridis256', min=0, max=10)
使用颜色映射与绘图对象的颜色属性,例如fill_color。
from numpy.random import standard_normal
from bokeh.plotting import figure, show
from bokeh.transform import linear_cmap
from bokeh.util.hex import hexbin
x = standard_normal(50000)
y = standard_normal(50000)
bins = hexbin(x, y, 0.1)
p = figure(tools="", match_aspect=True, background_fill_color='#440154')
p.grid.visible = False
p.hex_tile(q="q", r="r", size=0.1, line_color=None, source=bins,
fill_color=linear_cmap('counts', 'Viridis256', 0, max(bins.counts)))
show(p)
映射函数返回的数据规范包括一个bokeh.transform。
你可以访问这些数据,以便在不同的上下文中使用映射函数的结果。例如,创建一个ColorBar:
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
from bokeh.transform import linear_cmap
x = list(range(1, 11))
y = list(range(1, 11))
source = ColumnDataSource(dict(x=x,y=y))
p = figure(width=300, height=300, title="Linear color map based on Y")
# use the field name of the column source
cmap = linear_cmap(field_name='y', palette="Spectral6", low=min(y), high=max(y))
r = p.scatter(x='x', y='y', color=cmap, size=15, source=source)
# create a color bar from the scatter glyph renderer
color_bar = r.construct_color_bar(width=10)
p.add_layout(color_bar, 'right')
show(p)
映射标记类型#
当你使用分类数据时,你可以为数据中的每个类别使用不同的标记。使用 factor_mark() 函数自动为不同的类别分配不同的标记:
from bokeh.plotting import figure, show
from bokeh.sampledata.penguins import data
from bokeh.transform import factor_cmap, factor_mark
SPECIES = sorted(data.species.unique())
MARKERS = ['hex', 'circle_x', 'triangle']
p = figure(title = "Penguin size", background_fill_color="#fafafa")
p.xaxis.axis_label = 'Flipper Length (mm)'
p.yaxis.axis_label = 'Body Mass (g)'
p.scatter("flipper_length_mm", "body_mass_g", source=data,
legend_group="species", fill_alpha=0.4, size=12,
marker=factor_mark('species', MARKERS, SPECIES),
color=factor_cmap('species', 'Category10_3', SPECIES))
p.legend.location = "top_left"
p.legend.title = "Species"
show(p)
此示例还使用 factor_cmap() 来为这些相同的类别进行颜色映射。
注意
factor_mark() 转换通常仅在 scatter 图形方法中有用,因为通过标记类型进行参数化仅在散点图中有意义。
使用CustomJSTransform包含JavaScript代码#
除了上述内置的转换函数外,您还可以使用自己的JavaScript代码。使用CustomJSTransform()函数来添加在浏览器中执行的自定义JavaScript代码。
下面的示例使用了CustomJSTransform()函数,并带有参数v_func。v_func是“向量化函数”的缩写。您提供给v_func的JavaScript代码需要期望在变量xs中接收一个输入数组,并返回一个包含转换值的JavaScript数组:
v_func = """
const first = xs[0]
const norm = new Float64Array(xs.length)
for (let i = 0; i < xs.length; i++) {
norm[i] = xs[i] / first
}
return norm
"""
normalize = CustomJSTransform(v_func=v_func)
plot.line(x='aapl_date', y=transform('aapl_close', normalize), line_width=2,
color='#cf3c4d', alpha=0.6,legend="Apple", source=aapl_source)
此示例中的代码将原始价格数据转换为相对于第一个数据点的归一化收益序列:
数据过滤#
Bokeh 使用一个称为“视图”的概念来选择数据的子集。视图由 Bokeh 的 CDSView 类表示。当你使用视图时,你可以使用一个或多个过滤器来选择特定的数据点,而无需更改底层数据。你还可以在不同的图表之间共享这些视图。
要使用过滤后的数据子集进行绘图,请将CDSView传递给Bokeh绘图上任何渲染器方法的view参数。
一个 CDSView 有一个属性,filter:
filter是Filter模型的一个实例,下面列出并描述了该模型。
在这个例子中,你创建了一个名为 view 的 CDSView。view 使用了 ColumnDataSource source 和两个过滤器 filter1 和 filter2 的交集。view 然后被传递给一个 circle() 渲染函数:
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CDSView
filter1 = ... # IndexFilter(), BooleanFilter(), etc.
filter2 = ...
source = ColumnDataSource(some_data)
view = CDSView(filter=filter1 & filter2)
p = figure()
p.circle(x="x", y="y", source=source, view=view)
索引过滤器#
IndexFilter 是最简单的过滤器类型。它有一个 indices 属性,
这是一个整数列表,表示你想要包含在图表中的数据索引。
from bokeh.layouts import gridplot
from bokeh.models import CDSView, ColumnDataSource, IndexFilter
from bokeh.plotting import figure, show
source = ColumnDataSource(data=dict(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5]))
view = CDSView(filter=IndexFilter([0, 2, 4]))
TOOLS = "box_select,hover,reset"
p1 = figure(height=300, width=300, tools=TOOLS)
p1.scatter(x="x", y="y", size=10, hover_color="red", source=source)
p2 = figure(height=300, width=300, tools=TOOLS)
p2.scatter(x="x", y="y", size=10, hover_color="red", source=source, view=view)
show(gridplot([[p1, p2]]))
布尔过滤器#
一个BooleanFilter使用其booleans属性中的True或False值列表从数据源中选择行。
from bokeh.layouts import gridplot
from bokeh.models import BooleanFilter, CDSView, ColumnDataSource
from bokeh.plotting import figure, show
source = ColumnDataSource(data=dict(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5]))
bools = [True if y_val > 2 else False for y_val in source.data['y']]
view = CDSView(filter=BooleanFilter(bools))
TOOLS = "box_select,hover,reset"
p1 = figure(height=300, width=300, tools=TOOLS)
p1.scatter(x="x", y="y", size=10, hover_color="red", source=source)
p2 = figure(height=300, width=300, tools=TOOLS,
x_range=p1.x_range, y_range=p1.y_range)
p2.scatter(x="x", y="y", size=10, hover_color="red", source=source, view=view)
show(gridplot([[p1, p2]]))
GroupFilter#
GroupFilter 是一个用于分类数据的过滤器。使用此过滤器,您可以从数据集中选择属于特定类别的行。
GroupFilter 有两个属性:
column_name: 要应用过滤器的ColumnDataSource中的列名group: 选择类别的名称
在下面的示例中,数据集 data 包含一个名为 species 的分类变量。所有数据都属于三个物种类别之一:Adelie、Chinstrap 或 Gentoo。此示例中的第二个图使用了 GroupFilter 来仅显示属于 Adelie 类别的数据点:
from bokeh.layouts import gridplot
from bokeh.models import CDSView, ColumnDataSource, GroupFilter
from bokeh.plotting import figure, show
from bokeh.sampledata.penguins import data
source = ColumnDataSource(data)
view = CDSView(filter=GroupFilter(column_name="species", group="Adelie"))
TOOLS = "box_select,reset,help"
p1 = figure(title="Full data set", height=300, width=300, tools=TOOLS)
p1.scatter(x="bill_length_mm", y="bill_depth_mm", source=source)
p2 = figure(title="Adelie only", height=300, width=300,
tools=TOOLS, x_range=p1.x_range, y_range=p1.y_range)
p2.scatter(x="bill_length_mm", y="bill_depth_mm", size=6,
source=source, view=view, color='darkorange')
show(gridplot([[p1, p2]]))
CustomJSFilter#
你也可以使用自己的JavaScript或TypeScript代码来创建自定义过滤器。要包含你的自定义过滤器代码,请使用Bokeh的CustomJSFilter类。将你的代码作为字符串传递给CustomJSFilter的参数code。
您的JavaScript或TypeScript代码需要返回一个索引列表或一个表示过滤子集的布尔值列表。您可以通过ColumnDataSource在JavaScript或TypeScript代码中使用CDSView。Bokeh通过变量source使ColumnDataSource可用:
custom_filter = CustomJSFilter(code='''
const indices = [];
// iterate through rows of data source and see if each satisfies some constraint
for (let i = 0; i < source.get_length(); i++){
if (source.data['some_column'][i] == 'some_value'){
indices.push(true);
} else {
indices.push(false);
}
}
return indices;
''')
AjaxDataSource#
更新和流式传输数据与
Bokeh服务器应用程序配合得非常好。然而,也可以在独立文档中使用类似的功能。AjaxDataSource提供了这种能力,而不需要Bokeh服务器。
要设置一个AjaxDataSource,你需要配置一个REST端点的URL和一个轮询间隔。
在浏览器中,数据源以指定的间隔从端点请求数据。然后,它使用来自端点的数据在本地更新数据。
本地更新数据可以通过两种方式进行:要么完全替换现有的本地数据,要么将新数据追加到现有数据中(直到达到可配置的max_size)。替换本地数据是默认设置。通过传递"replace"或"append"作为AjaxDataSource的mode参数来控制此行为。
您与AjaxDataSource一起使用的端点应返回一个符合标准ColumnDataSource格式的JSON字典,即一个将名称映射到值数组的JSON字典:
{
'x' : [1, 2, 3, ...],
'y' : [9, 3, 2, ...]
}
或者,如果REST API返回不同的格式,可以通过此数据源的adapter属性提供一个CustomJS回调函数,将REST响应转换为Bokeh格式。
否则,使用AjaxDataSource与使用标准的ColumnDataSource是相同的:
# setup AjaxDataSource with URL and polling interval
source = AjaxDataSource(data_url='http://some.api.com/data',
polling_interval=100)
# use the AjaxDataSource just like a ColumnDataSource
p.circle('x', 'y', source=source)
这是使用AjaxDataSource时,Bokeh中实时数据流的预览:
有关完整示例,请参阅Bokeh的GitHub仓库中的examples/basic/data/ajax_source.py。
链接选择#
如果两个图都使用相同的ColumnDataSource,你可以在两个图之间共享选择:
from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
from bokeh.sampledata.penguins import data
from bokeh.transform import factor_cmap
SPECIES = sorted(data.species.unique())
TOOLS = "box_select,lasso_select,help"
source = ColumnDataSource(data)
left = figure(width=300, height=400, title=None, tools=TOOLS,
background_fill_color="#fafafa")
left.scatter("bill_length_mm", "body_mass_g", source=source,
color=factor_cmap('species', 'Category10_3', SPECIES))
right = figure(width=300, height=400, title=None, tools=TOOLS,
background_fill_color="#fafafa", y_axis_location="right")
right.scatter("bill_depth_mm", "body_mass_g", source=source,
color=factor_cmap('species', 'Category10_3', SPECIES))
show(gridplot([[left, right]]))
使用过滤数据进行链接选择#
使用ColumnDataSource,您还可以创建两个基于相同数据但各自使用不同数据子集的图表。这两个图表仍然通过它们所基于的ColumnDataSource共享选择和悬停检查。
以下示例展示了这种行为:
第二个图表是第一个图表数据的子集。第二个图表使用了
CDSView来仅包含大于250或小于100的y值。如果您在任一图表中使用
BoxSelect工具进行选择,该选择也会自动反映在另一个图表中。如果你在一个图表上悬停一个点,另一个图表中的对应点也会自动高亮显示,如果存在的话。
from bokeh.layouts import gridplot
from bokeh.models import BooleanFilter, CDSView, ColumnDataSource
from bokeh.plotting import figure, show
x = list(range(-20, 21))
y0 = [abs(xx) for xx in x]
y1 = [xx**2 for xx in x]
# create a column data source for the plots to share
source = ColumnDataSource(data=dict(x=x, y0=y0, y1=y1))
# create a view of the source for one plot to use
view = CDSView(filter=BooleanFilter([True if y > 250 or y < 100 else False for y in y1]))
TOOLS = "box_select,lasso_select,hover,help"
# create a new plot and add a renderer
left = figure(tools=TOOLS, width=300, height=300, title=None)
left.scatter("x", "y0", size=10, hover_color="firebrick", source=source)
# create another new plot, add a renderer that uses the view of the data source
right = figure(tools=TOOLS, width=300, height=300, title=None)
right.scatter("x", "y1", size=10, hover_color="firebrick", source=source, view=view)
p = gridplot([[left, right]])
show(p)
其他数据类型#
你也可以使用Bokeh来渲染网络图数据和地理数据。有关如何为这些类型的图表设置数据的更多信息,请参见 网络图 和 地理数据。