Python:将计算线添加到具有嵌套分类 x 轴的散点图

Python: Add calculated lines to a scatter plot with a nested categorical x-axis

交叉-post:https://discourse.bokeh.org/t/add-calculated-horizontal-lines-corresponding-to-categories-on-the-x-axis/5544

我想在 Python 中复制这个情节:

这是我的尝试,使用 pandasbokeh:

进口:

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。这正是您所看到的在你的散景图上。

有点不相关但仍然是一个错误 - 请注意您的 X 轴与您想要的有何不同。您必须先指定主要类别,因此将 list(zip(df['fruit'], df['year'])) 替换为 list(zip(df['year'], df['fruit']))

现在,进入 "how to" 部分。由于您需要两个独立的波段,因此您不能为它们提供相同的数据源。这样做的方法是拥有两个额外的数据源——每个波段一个。它最终是这样的:

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 class 的实例)不会考虑波段的高度。所以乐队很容易越界并被情节裁剪。这里的解决方案是使用将 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 版本一起使用。