条形图#

除了在连续范围上绘制数值数据外,您还可以使用Bokeh在分类范围上绘制分类数据。

Bokeh中的基本分类范围表示为字符串序列。例如,四个季节的列表

seasons = ["Winter", "Spring", "Summer", "Fall"]

Bokeh还可以处理分层类别。例如,您可以使用嵌套的字符串序列来表示每个年份季度内的各个月份

months_by_quarter = [
    ("Q1", "Jan"), ("Q1", "Feb"), ("Q1", "Mar"),
    ("Q2", "Apr"), ("Q2", "May"), ("Q2", "Jun"),
    ("Q3", "Jul"), ("Q3", "Aug"), ("Q3", "Sep"),
    ("Q4", "Oct"), ("Q4", "Nov"), ("Q4", "Dec"),
]

根据数据的结构,您可以使用不同类型的图表:条形图、分类热图、抖动图等等。本章将介绍几种常见的分类数据绘图类型。

条形#

处理分类数据最常见的方法之一是将其呈现为条形图。条形图具有一个分类轴和一个连续轴。当每个类别只有一个值要绘制时,条形图很有用。

与每个类别关联的值通过为该类别绘制一条条形来表示。该条形沿连续轴的长度对应于该类别的值。

条形图也可以根据分层子类别堆叠在一起或分组在一起。本节将演示如何绘制各种不同类型的分类条形图。

基本#

要创建基本条形图,请使用 hbar()(水平条形)或 vbar()(垂直条形)图形方法。以下示例显示了一系列简单的1级类别。

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']

要将这些类别分配到x轴,请将此列表作为 x_range 参数传递给 figure()

p = figure(x_range=fruits, ... )

这样做是创建 FactorRange 对象的便捷简写。等效的显式表示法是

p = figure(x_range=FactorRange(factors=fruits), ... )

当您想自定义 FactorRange 时,此形式很有用,例如,通过更改范围或类别填充。

接下来,使用水果名称列表作为 x 坐标和条形高度作为 top 坐标调用 vbar()。您还可以指定 width 或其他可选属性。

p.vbar(x=fruits, top=[5, 3, 4, 2, 4, 6], width=0.9)

组合上述操作将产生以下输出

from bokeh.plotting import figure, show

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
counts = [5, 3, 4, 2, 4, 6]

p = figure(x_range=fruits, height=350, title="Fruit Counts",
           toolbar_location=None, tools="")

p.vbar(x=fruits, top=counts, width=0.9)

p.xgrid.grid_line_color = None
p.y_range.start = 0

show(p)

您还可以将数据分配给 ColumnDataSource 并将其作为 source 参数提供给 vbar(),而不是将数据直接作为参数传递。您将在后面的示例中看到这一点。

排序#

要对给定图的条形进行排序,请根据值对类别进行排序。

以下示例根据计数对水果类别进行升序排序,并相应地重新排列条形。

from bokeh.plotting import figure, show

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
counts = [5, 3, 4, 2, 4, 6]

# sorting the bars means sorting the range factors
sorted_fruits = sorted(fruits, key=lambda x: counts[fruits.index(x)])

p = figure(x_range=sorted_fruits, height=350, title="Fruit Counts",
           toolbar_location=None, tools="")

p.vbar(x=fruits, top=counts, width=0.9)

p.xgrid.grid_line_color = None
p.y_range.start = 0

show(p)

填充#

颜色#

您可以通过几种方式为条形着色

  • 将所有颜色与其他数据一起提供给 ColumnDataSource,并将颜色列的名称分配给 vbar()color 参数。

    from bokeh.models import ColumnDataSource
    from bokeh.palettes import Bright6
    from bokeh.plotting import figure, show
    
    fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
    counts = [5, 3, 4, 2, 4, 6]
    
    source = ColumnDataSource(data=dict(fruits=fruits, counts=counts, color=Bright6))
    
    p = figure(x_range=fruits, y_range=(0,9), height=350, title="Fruit Counts",
               toolbar_location=None, tools="")
    
    p.vbar(x='fruits', top='counts', width=0.9, color='color', legend_field="fruits", source=source)
    
    p.xgrid.grid_line_color = None
    p.legend.orientation = "horizontal"
    p.legend.location = "top_center"
    
    show(p)
    

    您还可以使用颜色列与 line_colorfill_color 参数分别更改轮廓颜色和填充颜色。

  • 使用 CategoricalColorMapper 模型在浏览器中映射条形颜色。您可以使用 factor_cmap() 函数来实现这一点。

    factor_cmap('fruits', palette=Spectral6, factors=fruits)
    

    然后,您可以将此函数的结果传递给 vbar()color 参数以实现相同的结果

    from bokeh.models import ColumnDataSource
    from bokeh.palettes import Bright6
    from bokeh.plotting import figure, show
    from bokeh.transform import factor_cmap
    
    fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
    counts = [5, 3, 4, 2, 4, 6]
    
    source = ColumnDataSource(data=dict(fruits=fruits, counts=counts))
    
    p = figure(x_range=fruits, height=350, toolbar_location=None, title="Fruit Counts")
    
    p.vbar(x='fruits', top='counts', width=0.9, source=source, legend_field="fruits",
           line_color='white', fill_color=factor_cmap('fruits', palette=Bright6, factors=fruits))
    
    p.xgrid.grid_line_color = None
    p.y_range.start = 0
    p.y_range.end = 9
    p.legend.orientation = "horizontal"
    p.legend.location = "top_center"
    
    show(p)
    

    有关使用 Bokeh 的颜色映射器的更多信息,请参阅 客户端颜色映射

堆叠#

要堆叠垂直条形,请使用 vbar_stack() 函数。以下示例使用三组水果数据。每组对应一年。此示例为每组创建一个条形图,并将每个水果的条形元素堆叠在彼此之上。

from bokeh.palettes import HighContrast3
from bokeh.plotting import figure, show

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ["2015", "2016", "2017"]

data = {'fruits' : fruits,
        '2015'   : [2, 1, 4, 3, 2, 4],
        '2016'   : [5, 3, 4, 2, 4, 6],
        '2017'   : [3, 2, 4, 4, 5, 3]}

p = figure(x_range=fruits, height=250, title="Fruit Counts by Year",
           toolbar_location=None, tools="hover", tooltips="$name @fruits: @$name")

p.vbar_stack(years, x='fruits', width=0.9, color=HighContrast3, source=data,
             legend_label=years)

p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xgrid.grid_line_color = None
p.axis.minor_tick_line_color = None
p.outline_line_color = None
p.legend.location = "top_left"
p.legend.orientation = "horizontal"

show(p)

您还可以堆叠表示正值和负值的条形

from bokeh.models import ColumnDataSource
from bokeh.palettes import GnBu3, OrRd3
from bokeh.plotting import figure, show

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ["2015", "2016", "2017"]

exports = {'fruits' : fruits,
           '2015'   : [2, 1, 4, 3, 2, 4],
           '2016'   : [5, 3, 4, 2, 4, 6],
           '2017'   : [3, 2, 4, 4, 5, 3]}
imports = {'fruits' : fruits,
           '2015'   : [-1, 0, -1, -3, -2, -1],
           '2016'   : [-2, -1, -3, -1, -2, -2],
           '2017'   : [-1, -2, -1, 0, -2, -2]}

p = figure(y_range=fruits, height=350, x_range=(-16, 16), title="Fruit import/export, by year",
           toolbar_location=None)

p.hbar_stack(years, y='fruits', height=0.9, color=GnBu3, source=ColumnDataSource(exports),
             legend_label=[f"{year} exports" for year in years])

p.hbar_stack(years, y='fruits', height=0.9, color=OrRd3, source=ColumnDataSource(imports),
             legend_label=[f"{year} imports" for year in years])

p.y_range.range_padding = 0.1
p.ygrid.grid_line_color = None
p.legend.location = "top_left"
p.axis.minor_tick_line_color = None
p.outline_line_color = None

show(p)

工具提示#

Bokeh 会自动将每层的 name 属性设置为其在数据集中对应的名称。您可以使用 $name 变量在工具提示中显示名称。您还可以使用 @$name 工具提示变量从数据集中检索每层中每个项目的相应值。

以下示例演示了两种行为

from bokeh.palettes import HighContrast3
from bokeh.plotting import figure, show

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ["2015", "2016", "2017"]

data = {'fruits' : fruits,
        '2015'   : [2, 1, 4, 3, 2, 4],
        '2016'   : [5, 3, 4, 2, 4, 6],
        '2017'   : [3, 2, 4, 4, 5, 3]}

p = figure(x_range=fruits, height=250, title="Fruit counts by year",
           toolbar_location=None, tools="hover", tooltips="$name @fruits: @$name")

p.vbar_stack(years, x='fruits', width=0.9, color=HighContrast3, source=data,
             legend_label=years)

p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xgrid.grid_line_color = None
p.axis.minor_tick_line_color = None
p.outline_line_color = None
p.legend.location = "top_left"
p.legend.orientation = "horizontal"

show(p)

您可以通过手动将其传递给 vbar_stackhbar_stack 函数来覆盖 name 的值。在这种情况下,@$name 将对应于您提供的名称。

hbar_stackvbar_stack 函数将返回所有渲染器的列表(每个条形堆叠一个)。您可以使用此列表自定义每层的工具提示。

renderers = p.vbar_stack(years, x='fruits', width=0.9, color=colors, source=source,
                         legend=[value(x) for x in years], name=years)

for r in renderers:
    year = r.name
    hover = HoverTool(tooltips=[
        ("%s total" % year, "@%s" % year),
        ("index", "$index")
    ], renderers=[r])
    p.add_tools(hover)

分组#

除了堆叠之外,您还可以选择对条形进行分组。根据您的用例,您可以通过两种方式实现这一点

嵌套类别#

如果您提供多个数据子集,Bokeh 会自动将条形分组到带标签的类别中,用其代表的子集的名称标记每个条形,并在类别之间添加一个分隔符。

以下示例创建一系列水果-年份对(元组),并使用对 vbar() 的单个调用对条形进行分组,以水果名称进行分组。

from bokeh.models import ColumnDataSource, FactorRange
from bokeh.plotting import figure, show

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ['2015', '2016', '2017']

data = {'fruits' : fruits,
        '2015'   : [2, 1, 4, 3, 2, 4],
        '2016'   : [5, 3, 3, 2, 4, 6],
        '2017'   : [3, 2, 4, 4, 5, 3]}

# this creates [ ("Apples", "2015"), ("Apples", "2016"), ("Apples", "2017"), ("Pears", "2015), ... ]
x = [ (fruit, year) for fruit in fruits for year in years ]
counts = sum(zip(data['2015'], data['2016'], data['2017']), ()) # like an hstack

source = ColumnDataSource(data=dict(x=x, counts=counts))

p = figure(x_range=FactorRange(*x), height=350, title="Fruit Counts by Year",
           toolbar_location=None, tools="")

p.vbar(x='x', top='counts', width=0.9, source=source)

p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None

show(p)

要对条形应用不同的颜色,请在 vbar() 函数调用中对 fill_color 使用 factor_cmap(),如下所示

p.vbar(x='x', top='counts', width=0.9, source=source, line_color="white",

       # use the palette to colormap based on the x[1:2] values
       fill_color=factor_cmap('x', palette=palette, factors=years, start=1, end=2))

在对 factor_cmap() 的调用中,start=1end=2 使用 (fruit, year) 对中的年份进行颜色映射。

视觉偏移#

以分别有 (fruit, year) 对序列的情况为例。您可以使用对 vbar() 的单独调用来绘制这些序列。但是,由于每组中的每个条形都属于相同的 fruit 类别,因此条形会重叠。为了避免这种行为,请使用 dodge() 函数为对 vbar() 的每次调用提供偏移量。

from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
from bokeh.transform import dodge

fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
years = ['2015', '2016', '2017']

data = {'fruits' : fruits,
        '2015'   : [2, 1, 4, 3, 2, 4],
        '2016'   : [5, 3, 3, 2, 4, 6],
        '2017'   : [3, 2, 4, 4, 5, 3]}

source = ColumnDataSource(data=data)

p = figure(x_range=fruits, y_range=(0, 10), title="Fruit Counts by Year",
           height=350, toolbar_location=None, tools="")

p.vbar(x=dodge('fruits', -0.25, range=p.x_range), top='2015', source=source,
       width=0.2, color="#c9d9d3", legend_label="2015")

p.vbar(x=dodge('fruits',  0.0,  range=p.x_range), top='2016', source=source,
       width=0.2, color="#718dbf", legend_label="2016")

p.vbar(x=dodge('fruits',  0.25, range=p.x_range), top='2017', source=source,
       width=0.2, color="#e84d60", legend_label="2017")

p.x_range.range_padding = 0.1
p.xgrid.grid_line_color = None
p.legend.location = "top_left"
p.legend.orientation = "horizontal"

show(p)

堆叠和分组#

您还可以将上述技术结合起来创建堆叠和分组条形图。以下示例按季度对条形进行分组,并按区域堆叠它们

from bokeh.models import ColumnDataSource, FactorRange
from bokeh.plotting import figure, show

factors = [
    ("Q1", "jan"), ("Q1", "feb"), ("Q1", "mar"),
    ("Q2", "apr"), ("Q2", "may"), ("Q2", "jun"),
    ("Q3", "jul"), ("Q3", "aug"), ("Q3", "sep"),
    ("Q4", "oct"), ("Q4", "nov"), ("Q4", "dec"),

]

regions = ['east', 'west']

source = ColumnDataSource(data=dict(
    x=factors,
    east=[ 5, 5, 6, 5, 5, 4, 5, 6, 7, 8, 6, 9 ],
    west=[ 5, 7, 9, 4, 5, 4, 7, 7, 7, 6, 6, 7 ],
))

p = figure(x_range=FactorRange(*factors), height=250,
           toolbar_location=None, tools="")

p.vbar_stack(regions, x='x', width=0.9, alpha=0.5, color=["blue", "red"], source=source,
             legend_label=regions)

p.y_range.start = 0
p.y_range.end = 18
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None
p.legend.location = "top_center"
p.legend.orientation = "horizontal"

show(p)

混合因子#

您可以使用多级数据结构中的任何级别来定位图形。

以下示例将每个月的条形分组到财务季度,并在从 Q1Q4 的组中心坐标处添加季度平均线。

from bokeh.models import FactorRange
from bokeh.palettes import TolPRGn4
from bokeh.plotting import figure, show

quarters =("Q1", "Q2", "Q3", "Q4")

months = (
    ("Q1", "jan"), ("Q1", "feb"), ("Q1", "mar"),
    ("Q2", "apr"), ("Q2", "may"), ("Q2", "jun"),
    ("Q3", "jul"), ("Q3", "aug"), ("Q3", "sep"),
    ("Q4", "oct"), ("Q4", "nov"), ("Q4", "dec"),
)

fill_color, line_color = TolPRGn4[2:]

p = figure(x_range=FactorRange(*months), height=500, tools="",
           background_fill_color="#fafafa", toolbar_location=None)

monthly = [10, 13, 16, 9, 10, 8, 12, 13, 14, 14, 12, 16]
p.vbar(x=months, top=monthly, width=0.8,
       fill_color=fill_color, fill_alpha=0.8, line_color=line_color, line_width=1.2)

quarterly = [13, 9, 13, 14]
p.line(x=quarters, y=quarterly, color=line_color, line_width=3)
p.scatter(x=quarters, y=quarterly, size=10,
          line_color=line_color, fill_color="white", line_width=3)

p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None

show(p)

使用pandas#

pandas 是一个强大且流行的工具,用于在 Python 中分析表格和时间序列数据。虽然不是必需的,但它可以使 Bokeh 的使用更加轻松。

例如,您可以使用 pandas 提供的 GroupBy 对象来初始化一个 ColumnDataSource,并自动为许多统计参数创建列,例如组均值和计数。您也可以将这些 GroupBy 对象作为 range 参数传递给 figure

from bokeh.palettes import Spectral5
from bokeh.plotting import figure, show
from bokeh.sampledata.autompg import autompg as df
from bokeh.transform import factor_cmap

df.cyl = df.cyl.astype(str)
group = df.groupby('cyl')

cyl_cmap = factor_cmap('cyl', palette=Spectral5, factors=sorted(df.cyl.unique()))

p = figure(height=350, x_range=group, title="MPG by # Cylinders",
           toolbar_location=None, tools="")

p.vbar(x='cyl', top='mpg_mean', width=1, source=group,
       line_color=cyl_cmap, fill_color=cyl_cmap)

p.y_range.start = 0
p.xgrid.grid_line_color = None
p.xaxis.axis_label = "some stuff"
p.xaxis.major_label_orientation = 1.2
p.outline_line_color = None

show(p)

上面的例子根据 'cyl' 列对数据进行分组,这就是 ColumnDataSource 包含此列的原因。它还为非分组类别(如 'mpg')添加关联列,例如,在 'mpg_mean' 列中提供平均每加仑英里数。

这对于多级分组也有效。下面的例子按 ('cyl', 'mfr') 对相同数据进行分组,并在沿 x 轴分布的嵌套类别中显示它。在这里,索引列名 'cyl_mfr' 是通过连接分组列的名称而成的。

from bokeh.palettes import MediumContrast5
from bokeh.plotting import figure, show
from bokeh.sampledata.autompg import autompg_clean as df
from bokeh.transform import factor_cmap

df.cyl = df.cyl.astype(str)
df.yr = df.yr.astype(str)

group = df.groupby(['cyl', 'mfr'])

index_cmap = factor_cmap('cyl_mfr', palette=MediumContrast5, factors=sorted(df.cyl.unique()), end=1)

p = figure(width=800, height=300, title="Mean MPG by # Cylinders and Manufacturer",
           x_range=group, toolbar_location=None, tooltips=[("MPG", "@mpg_mean"), ("Cyl, Mfr", "@cyl_mfr")])

p.vbar(x='cyl_mfr', top='mpg_mean', width=1, source=group,
       line_color="white", fill_color=index_cmap )

p.y_range.start = 0
p.x_range.range_padding = 0.05
p.xgrid.grid_line_color = None
p.xaxis.axis_label = "Manufacturer grouped by # Cylinders"
p.xaxis.major_label_orientation = 1.2
p.outline_line_color = None

show(p)

区间#

除了使用带有共同基线的条形图之外,您还可以使用条形图来表示更多内容。如果每个类别都与一个起始值和一个结束值相关联,那么您也可以使用条形图来表示每个类别在范围内跨越的区间。

下面的例子向 hbar() 函数提供了 leftright 属性,以显示多年来奥运会短跑比赛中金牌和铜牌获得者之间的比赛时间跨度。

from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
from bokeh.sampledata.sprint import sprint

df = sprint.copy()  # since we are modifying sampledata

df.Year = df.Year.astype(str)
group = df.groupby('Year')
source = ColumnDataSource(group)

p = figure(y_range=group, x_range=(9.5,12.7), width=400, height=550, toolbar_location=None,
           title="Time Spreads for Sprint Medalists (by Year)")
p.hbar(y="Year", left='Time_min', right='Time_max', height=0.4, source=source)

p.ygrid.grid_line_color = None
p.xaxis.axis_label = "Time (seconds)"
p.outline_line_color = None

show(p)