这是我的尝试,使用pandas
和bokeh
:
进口:
import pandas as pd
from bokeh.io import output_notebook, show, reset_output
from bokeh.palettes import Spectral5, Turbo256
from bokeh.plotting import figure
from bokeh.transform import factor_cmap
from bokeh.models import Band, Span, FactorRange, ColumnDataSource
创建数据:
fruits = ['Apples', 'Pears']
years = ['2015', '2016']
data = {'fruit' : fruits,
'2015' : [2, 1],
'2016' : [5, 3]}
fruit_df = pd.DataFrame(data).set_index("fruit")
tidy_df = (pd.DataFrame(data)
.melt(id_vars=["fruit"], var_name="year")
.assign(fruit_year=lambda df: list(zip(df['fruit'], df['year'])))
.set_index('fruit_year'))
创建bokeh
图:
p = figure(x_range=FactorRange(factors=tidy_df.index.unique()),
plot_height=400,
plot_width=400,
tooltips=[('Fruit', '@fruit'), # first string is user-defined; second string must refer to a column
('Year', '@year'),
('Value', '@value')])
cds = ColumnDataSource(tidy_df)
index_cmap = factor_cmap("fruit",
Spectral5[:2],
factors=sorted(tidy_df["fruit"].unique())) # this is a reference back to the dataframe
p.circle(x='fruit_year',
y='value',
size=20,
source=cds,
fill_color=index_cmap,
line_color=None,
)
# how do I add a median just to one categorical section?
median = Span(location=tidy_df.loc[tidy_df["fruit"] == "Apples", "value"].median(), # median value for Apples
#dimension='height',
line_color='red',
line_dash='dashed',
line_width=1.0
)
p.add_layout(median)
# how do I add this standard deviation(ish) band to just the Apples or Pears section?
band = Band(
base='fruit_year',
lower=2,
upper=4,
source=cds,
)
p.add_layout(band)
show(p)
输出:
我可以解决这个问题吗?https://github.com/bokeh/bokeh/issues/8592是否还有其他适用于Python的数据可视化库可以实现此目的? Altair,Holoviews,Matplotlib,Plotly ...?
Band
是连接的区域,但是所需输出的图像具有两个断开的区域。意思是,您实际上需要两个频段。请看以下示例,以更好地了解乐队:https://docs.bokeh.org/en/latest/docs/user_guide/annotations.html#bands
[通过使用Band(base='fruit_year', lower=2, upper=4, source=cds)
,您要求Bokeh绘制一个频带,其中对于fruit_year
的每个值,下坐标为2,上坐标为4。这正是在Bokeh图上看到的。
有点不相关,但仍然是一个错误-请注意您的X轴与所需的轴有何不同。您必须首先指定主要类别,因此将list(zip(df['fruit'], df['year']))
替换为list(zip(df['year'], df['fruit']))
。
现在,进入“操作方法”部分。由于需要两个单独的频段,因此无法为它们提供相同的数据源。这样做的方法是拥有两个额外的数据源-每个频段一个。最终结果是这样的:
for year, sd in [('2015', 0.3), ('2016', 0.5)]:
b_df = (tidy_df[tidy_df['year'] == year]
.drop(columns=['year', 'fruit'])
.assign(lower=lambda df: df['value'].min() - sd,
upper=lambda df: df['value'].max() + sd)
.drop(columns='value'))
p.add_layout(Band(base='fruit_year', lower='lower', upper='upper',
source=ColumnDataSource(b_df)))
但是还有两个问题。第一个是琐碎的-自动Y范围(默认为DataRange1d
类的实例)将不考虑波段的高度。因此,乐队可以轻松超越界限并被剧情裁剪。这里的解决方案是使用考虑到SD值的手动量程。
第二个问题是,带的宽度限制为X范围因子,这意味着圆圈将部分位于带的外部。这个不是那么容易解决。通常的解决方案是使用transform
在边缘稍微移动坐标。但是,由于这是分类轴,因此我们无法做到这一点。一种可能的解决方案是创建一个自定义Band
模型,该模型添加偏移量:
class MyBand(Band):
# language=TypeScript
__implementation__ = """
import {Band, BandView} from "models/annotations/band"
export class MyBandView extends BandView {
protected _map_data(): void {
super._map_data()
const base_sx = this.model.dimension == 'height' ? this._lower_sx : this._lower_sy
if (base_sx.length > 1) {
const offset = (base_sx[1] - base_sx[0]) / 2
base_sx[0] -= offset
base_sx[base_sx.length - 1] += offset
}
}
}
export class MyBand extends Band {
__view_type__: MyBandView
static init_MyBand(): void {
this.prototype.default_view = MyBandView
}
}
"""
只需在上面的代码中将Band
替换为MyBand
,它就可以工作。一个警告-您将需要安装Node.js,并且启动时间将持续一到两秒,因为自定义模型代码需要编译。另一个警告-自定义模型代码了解BokehJS的内部结构。意思是,虽然它可以与Bokeh 2.0.2一起使用,但我不能保证它可以与任何其他Bokeh版本一起使用。