Bokeh 统计图绘制完全指南:直方图、金字塔图、箱线图、KDE 与 SPLOM 实战
【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh
本文基于 Bokeh 官方用户指南中的 Statistical plots(统计图) 一节,系统讲解如何仅用基础 glyph 与注解(annotation)在浏览器中构建直方图、人口金字塔图、箱线图、核密度估计(KDE)、SinaPlot 以及散点图矩阵(SPLOM)等六类统计图表。读完本文,你将掌握quad、hbar、vbar、scatter、varea、harea、contour等核心绘图 API 的组合用法,以及Whisker注解、Label标注、范围共享(linked panning/brushing)等进阶技巧,所有示例均可在仓库examples/topics/stats/目录下直接运行验证。
该指南对应的全部示例代码位于 examples/topics/stats/,每个脚本都带有bokeh-example-metadata元数据块,标注了所用 API、关联文档章节与关键词,便于在文档与示例之间交叉检索。
前置准备:统计图绘制的通用思路
在开始之前,先明确 Bokeh 统计图的核心设计哲学:Bokeh 并不提供"直方图"或"箱线图"这类打包好的高层图表函数,而是提供原子化的 glyph 原语与注解模型,由开发者按统计语义自行组装。这意味着你需要:
- 用 NumPy / SciPy / scikit-learn / pandas 完成统计计算(分箱、分位数、核密度估计等);
- 用
figure上的 glyph 方法(quad、hbar、vbar、scatter、varea、harea、contour)完成图形绘制; - 用
Whisker、Label等注解模型补充统计图形特有的视觉元素。
上述 glyph 方法统一定义在 src/bokeh/plotting/glyph_api.py(contour位于 src/bokeh/plotting/_figure.py),它们内部都通过@glyph_method装饰器绑定到对应的 glyph 模型类,因此既可以用高层方法,也可以用底层Plot.add_glyph手动组装(SPLOM 一节会演示后者)。
直方图(Histogram):quad + np.histogram
直方图是最基础的分布可视化。官方推荐的做法是:先用np.histogram完成分箱统计,再用quadglyph 将每个箱绘制为一个矩形。完整示例见 examples/topics/stats/histogram.py:
import numpy as np from bokeh.plotting import figure, show rng = np.random.default_rng() x = rng.normal(loc=0, scale=1, size=1000) p = figure(width=670, height=400, toolbar_location=None, title="Normal (Gaussian) Distribution") # Histogram bins = np.linspace(-3, 3, 40) hist, edges = np.histogram(x, density=True, bins=bins) p.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:], fill_color="skyblue", line_color="white", legend_label="1000 random samples") # Probability density function x = np.linspace(-3.0, 3.0, 100) pdf = np.exp(-0.5*x**2) / np.sqrt(2.0*np.pi) p.line(x, pdf, line_width=2, line_color="navy", legend_label="Probability Density Function") p.y_range.start = 0 p.xaxis.axis_label = "x" p.yaxis.axis_label = "PDF(x)" show(p)关键点拆解:
np.histogram(x, density=True, bins=bins):返回(hist, edges)两个数组。density=True表示输出的是概率密度而非频数,从而可以与理论概率密度函数(PDF)叠加对比。bins可以传整数(自动分箱)或数组(显式指定箱边界),本示例用np.linspace(-3, 3, 40)在 [-3, 3] 区间内生成 40 个均匀边界。p.quad(...):quad是绘制矩形的 glyph(glyph_api.py),核心参数为top(上边 y 坐标)、bottom(下边 y 坐标)、left(左边 x 坐标)、right(右边 x 坐标)。这里利用edges[:-1]与edges[1:]错位切片,恰好把每个箱的左右边界对起来,top=hist让矩形高度等于箱内密度值。quad天然支持矢量参数,一次调用即可绘制全部 40 个箱。- 密度曲线叠加:直接在同一
figure上再调用p.line绘制标准正态 PDF,legend_label让两条曲线自动进入图例。 - 坐标轴细节:
p.y_range.start = 0强制 y 轴从 0 开始(直方图不应有负值);p.xaxis.axis_label/p.yaxis.axis_label设置轴标题。
这个示例同时演示了figure.line与figure.quad的组合使用(见脚本中的:apis:元数据),是"统计计算在 Python 端、绘制在 Bokeh 端"这一模式的最小范例。
人口金字塔图(Population Pyramid):hbar 绘制发散条形图
人口金字塔是一种发散式水平条形图(divergent horizontal bar plot),用于对比两组人群的分布。其技巧非常巧妙:将一组的计数取负值绘制在 x 轴负半区,另一组绘制在正半区,从而形成左右对称的金字塔形状。在 Bokeh 中它由hbarglyph 实现,示例见 examples/topics/stats/pyramid.py,数据来自bokeh.sampledata.titanic:
import numpy as np from bokeh.models import CustomJSTickFormatter, Label from bokeh.palettes import DarkText, Vibrant3 as colors from bokeh.plotting import figure, show from bokeh.sampledata.titanic import data as df sex_group = df.groupby("sex") female_ages = sex_group.get_group("female")["age"].dropna() male_ages = sex_group.get_group("male")["age"].dropna() bin_width = 5 bins = np.arange(0, 72, bin_width) m_hist, edges = np.histogram(male_ages, bins=bins) f_hist, edges = np.histogram(female_ages, bins=bins) p = figure(title="Age population pyramid of titanic passengers, by gender", height=400, width=600, x_range=(-90, 90), x_axis_label="count") p.hbar(right=f_hist, y=edges[1:], height=bin_width*0.8, color=colors[0], line_width=0) p.hbar(right=m_hist * -1, y=edges[1:], height=bin_width*0.8, color=colors[1], line_width=0) # add text to every other bar for i, (count, age) in enumerate(zip(f_hist, edges[1:])): if i % 2 == 1: continue p.text(x=count, y=edges[1:][i], text=[f"{age-bin_width}-{age}yrs"], x_offset=5, y_offset=7, text_font_size="12px", text_color=DarkText[5]) # customise x-axis and y-axis p.xaxis.ticker = (-80, -60, -40, -20, 0, 20, 40, 60, 80) p.xaxis.major_tick_out = 0 p.y_range.start = 3 p.ygrid.grid_line_color = None p.yaxis.visible = False # format tick labels as absolute values for the two-sided plot p.xaxis.formatter = CustomJSTickFormatter(code="return Math.abs(tick);") # add labels p.add_layout(Label(x=-40, y=70, text="Men", text_color=colors[1], x_offset=5)) p.add_layout(Label(x=20, y=70, text="Women", text_color=colors[0], x_offset=5)) show(p)实现要点:
- 数据准备:用 pandas 的
groupby("sex")分别取出男、女乘客的年龄,dropna()丢弃缺失值;np.arange(0, 72, bin_width)以 5 岁为箱宽生成边界,男女各做一次np.histogram。 - 左右镜像:
p.hbar(right=f_hist, ...)绘制女性一侧;p.hbar(right=m_hist * -1, ...)将男性计数取负,hbar的right参数即为条形右端 x 坐标(默认left=0),负值条形自然落在负半轴。y=edges[1:]把条形中心放在每个年龄箱的上边界处,height=bin_width*0.8让条形之间留出 20% 间隙。 - 坐标刻度绝对值化:由于男性侧是负值,直接用
CustomJSTickFormatter(code="return Math.abs(tick);")把刻度标签取绝对值显示,避免出现 "-40 人" 这种误导性读数。 Label注解:p.add_layout(Label(x=..., y=..., text="Men"/"Women", ...))在图中直接摆放文字标签(Label 模型),x_offset/y_offset微调偏移量。此外还用p.text为每隔一个箱添加年龄区间文字。- 视觉收敛:
p.xaxis.ticker手动指定刻度位置、p.yaxis.visible = False隐藏 y 轴、p.ygrid.grid_line_color = None去掉横向网格线,让金字塔更干净。
箱线图(Boxplot):Whisker 注解 + vbar + scatter
箱线图在 Bokeh 中由三部分组装而成:Whisker注解绘制须线、vbar绘制四分位箱体、scatter绘制离群点。完整示例见 examples/topics/stats/boxplot.py,数据来自bokeh.sampledata.autompg2:
import pandas as pd from bokeh.models import ColumnDataSource, Whisker from bokeh.plotting import figure, show from bokeh.sampledata.autompg2 import autompg2 from bokeh.transform import factor_cmap df = autompg2[["class", "hwy"]].rename(columns={"class": "kind"}) kinds = df.kind.unique() # compute quantiles grouper = df.groupby("kind") qs = grouper.hwy.quantile([0.25, 0.5, 0.75]).unstack().reset_index() qs.columns = ["kind", "q1", "q2", "q3"] # compute IQR outlier bounds iqr = qs.q3 - qs.q1 qs["upper"] = qs.q3 + 1.5*iqr qs["lower"] = qs.q1 - 1.5*iqr # update the whiskers to actual data points for kind, group in grouper: qs_idx = qs.query(f"kind=={kind!r}").index[0] data = group["hwy"] # the upper whisker is the maximum between p3 and upper q3 = qs.loc[qs_idx, "q3"] upper = qs.loc[qs_idx, "upper"] wiskhi = group[(q3 <= data) & (data <= upper)]["hwy"] qs.loc[qs_idx, "upper"] = q3 if len(wiskhi) == 0 else wiskhi.max() # the lower whisker is the minimum between q1 and lower q1 = qs.loc[qs_idx, "q1"] lower = qs.loc[qs_idx, "lower"] wisklo = group[(lower <= data) & (data<= q1)]["hwy"] qs.loc[qs_idx, "lower"] = q1 if len(wisklo) == 0 else wisklo.min() df = pd.merge(df, qs, on="kind", how="left") source = ColumnDataSource(qs) p = figure(x_range=kinds, tools="", toolbar_location=None, title="Highway MPG distribution by vehicle class", background_fill_color="#eaefef", y_axis_label="MPG") # outlier range whisker = Whisker(base="kind", upper="upper", lower="lower", source=source) whisker.upper_head.size = whisker.lower_head.size = 20 p.add_layout(whisker) # quantile boxes cmap = factor_cmap("kind", "TolRainbow7", kinds) p.vbar("kind", 0.7, "q2", "q3", source=source, color=cmap, line_color="black") p.vbar("kind", 0.7, "q1", "q2", source=source, color=cmap, line_color="black") # outliers outliers = df[~df.hwy.between(df.lower, df.upper)] p.scatter("kind", "hwy", source=outliers, size=6, color="black", alpha=0.3) p.xgrid.grid_line_color = None p.axis.major_label_text_font_size="14px" p.axis.axis_label_text_font_size="12px" show(p)技术要点:
- 统计计算(pandas 端):
groupby("kind").hwy.quantile([0.25, 0.5, 0.75]).unstack()得到每个车型类别的 Q1/Q2/Q3;再由iqr = q3 - q1计算四分位距,以q3 + 1.5*iqr与q1 - 1.5*iqr作为离群点判定的理论上下界。随后一段循环把须线端点"收敛"到实际数据点——上须取 Q3 与上界之间的最大值、下须取 Q1 与下界之间的最小值,这正是 Tukey 箱线图的标准做法,确保须线不过度延伸。 Whisker注解(src/bokeh/models/annotations/geometry.py):Whisker(base="kind", upper="upper", lower="lower", source=source)沿分类轴为每个类别绘制一条竖线,upper/lower是CoordinateSpec类型的数据列名,source指定ColumnDataSource。whisker.upper_head.size = whisker.lower_head.size = 20控制两端箭头头(默认TeeHead,见geometry.py中lower_head/upper_head的InstanceDefault(TeeHead, size=10))的尺寸。最后必须通过p.add_layout(whisker)把注解挂到图上。- 箱体(两段 vbar):
p.vbar("kind", 0.7, "q2", "q3", ...)绘制 Q2→Q3 的上半箱,p.vbar("kind", 0.7, "q1", "q2", ...)绘制 Q1→Q2 的下半箱——vbar的参数为(x, width, top, bottom),两段拼合即得完整箱体。factor_cmap按类别映射TolRainbow7调色板。 - 离群点(scatter):
df[~df.hwy.between(df.lower, df.upper)]筛出落在须线范围之外的行,p.scatter以半透明黑色圆点标出,alpha=0.3缓解重叠。
核密度估计(Kernel Density Estimation)
指南展示了 KDE 的两种形态:二维 KDE 用contour绘制等高线图,一维多组 KDE 用varea绘制填充面积图。
二维 KDE 等高线:scipy.stats.gaussian_kde + contour
示例 examples/topics/stats/kde2d.py 使用scipy.stats.gaussian_kde估计 "autompg" 数据中hp与mpg的联合密度,再用p.contour渲染等高线:
import numpy as np from scipy.stats import gaussian_kde from bokeh.palettes import Blues9 from bokeh.plotting import figure, show from bokeh.sampledata.autompg import autompg as df def kde(x, y, N): xmin, xmax = x.min(), x.max() ymin, ymax = y.min(), y.max() X, Y = np.mgrid[xmin:xmax:N*1j, ymin:ymax:N*1j] positions = np.vstack([X.ravel(), Y.ravel()]) values = np.vstack([x, y]) kernel = gaussian_kde(values) Z = np.reshape(kernel(positions).T, X.shape) return X, Y, Z x, y, z = kde(df.hp, df.mpg, 300) p = figure(height=400, x_axis_label="hp", y_axis_label="mpg", background_fill_color="#fafafa", tools="", toolbar_location=None, title="Kernel density estimation plot of HP vs MPG") p.grid.level = "overlay" p.grid.grid_line_color = "black" p.grid.grid_line_alpha = 0.05 palette = Blues9[::-1] levels = np.linspace(np.min(z), np.max(z), 10) p.contour(x, y, z, levels[1:], fill_color=palette, line_color=palette) show(p)实现细节:
- KDE 计算:
kde()函数先用np.mgrid在数据包围盒内生成N=300的二维网格点,把网格点与原始观测值np.vstack后喂给gaussian_kde,kernel(positions)得到每个网格点的密度值,再np.reshape回网格形状。 p.contour(src/bokeh/plotting/_figure.py):接受(x, y, z)三个二维数组与levels等高线层级列表。这里用np.linspace(np.min(z), np.max(z), 10)生成 10 个层级并丢弃最低层(levels[1:]),fill_color与line_color同时传入Blues9反序调色板([::-1]让高密度区使用更深蓝)。- 网格线覆盖:
p.grid.level = "overlay"把网格线置于等高线之上,配合低透明度黑色网格,便于读数。
一维多组密度:sklearn KernelDensity + varea
示例 examples/topics/stats/density.py 使用sklearn.neighbors.KernelDensity对 "cows" 数据按奶牛品种分别估计黄油脂肪含量密度,并用varea填充曲线下方区域:
import numpy as np from sklearn.neighbors import KernelDensity from bokeh.models import ColumnDataSource, Label, PrintfTickFormatter from bokeh.palettes import Dark2_5 as colors from bokeh.plotting import figure, show from bokeh.sampledata.cows import data as df breed_groups = df.groupby('breed') x = np.linspace(2, 8, 1000) source = ColumnDataSource(dict(x=x)) p = figure(title="Multiple density estimates", height=300, x_range=(2.5, 7.5), x_axis_label="butterfat contents", y_axis_label="density") for (breed, breed_df), color in zip(breed_groups, colors): data = breed_df['butterfat'].values kde = KernelDensity(kernel="gaussian", bandwidth=0.2).fit(data[:, np.newaxis]) log_density = kde.score_samples(x[:, np.newaxis]) y = np.exp(log_density) source.add(y, breed) p.varea(x="x", y1=breed, y2=0, source=source, fill_alpha=0.3, fill_color=color) # Find the highest point and annotate with a label max_idx = np.argmax(y) highest_point_label = Label( x=x[max_idx], y=y[max_idx], text=breed, text_font_size="10pt", x_offset=10, y_offset=-5, text_color=color, ) p.add_layout(highest_point_label) # Display x-axis labels as percentages p.xaxis.formatter = PrintfTickFormatter(format="%d%%") p.axis.axis_line_color = None p.axis.major_tick_line_color = None p.axis.minor_tick_line_color = None p.xgrid.grid_line_color = None p.yaxis.ticker = (0, 0.5, 1, 1.5) p.y_range.start = 0 show(p)关键点:
- KDE 计算:
KernelDensity(kernel="gaussian", bandwidth=0.2)构造高斯核估计器,score_samples返回对数密度,np.exp还原为密度值。bandwidth是核宽度,控制曲线的平滑程度。 p.varea(glyph_api.py):垂直方向面积图,参数为(x, y1, y2)——x是横坐标列,y1是上边界(密度曲线),y2=0是下边界(基线)。多个品种的密度列通过source.add(y, breed)动态追加到同一个ColumnDataSource,因此可以在循环内以列名引用。- 顶点标注:
np.argmax(y)找到密度峰值位置,用Label在该点旁标注品种名,text_color与曲线颜色一致。 - 格式化与精简:
PrintfTickFormatter(format="%d%%")把 x 轴显示为百分比;隐藏坐标轴线与刻度线、关闭 x 网格、手动设置 y 轴刻度(0, 0.5, 1, 1.5),使多曲线叠加图保持清爽。
SinaPlot:harea + scatter 组合
SinaPlot 是"结合核密度信息增强的一维散点图":每个类别沿横轴展开,散点在类别内的横向偏移量正比于该处的核密度,从而同时呈现数据点位置与分布形状。指南指出它由harea与scatter两个 glyph 组装而成,示例见 examples/topics/stats/sinaplot.py,数据为 "lincoln" 气象数据:
import numpy as np import pandas as pd from sklearn.neighbors import KernelDensity from bokeh.plotting import figure, show from bokeh.sampledata.lincoln import data as df df["DATE"] = pd.to_datetime(df["DATE"]) df["TAVG"] = (df["TMAX"] + df["TMIN"]) / 2 df["MONTH"] = df.DATE.dt.strftime("%b") months = list(df.MONTH.unique()) p = figure( height=400, width=600, x_range=months, x_axis_label="month", y_axis_label="mean temperature (F)", ) # add a non-uniform categorical offset to a given category def offset(category, data, scale=7): return list(zip([category] * len(data), scale * data)) for month in months: month_df = df[df.MONTH == month].dropna() tavg = month_df.TAVG.values temps = np.linspace(tavg.min(), tavg.max(), 50) kde = KernelDensity(kernel="gaussian", bandwidth=3).fit(tavg[:, np.newaxis]) density = np.exp(kde.score_samples(temps[:, np.newaxis])) x1, x2 = offset(month, density), offset(month, -density) p.harea(x1=x1, x2=x2, y=temps, alpha=0.8, color="#E0E0E0") # pre-compute jitter in Python, this case is too complex for BokehJS tavg_density = np.exp(kde.score_samples(tavg[:, np.newaxis])) jitter = (np.random.random(len(tavg)) * 2 - 1) * tavg_density p.scatter(x=offset(month, jitter), y=tavg, color="black") p.y_range.start = -10 p.yaxis.ticker = [0, 25, 50, 75] p.grid.grid_line_color = None show(p)实现要点:
- 数据预处理:
DATE解析为 datetime,TAVG = (TMAX + TMIN) / 2计算日均温,再按%b格式提取月份缩写作为分类轴。 offset辅助函数:返回[(category, 偏移值), ...]的坐标对列表——这是向 Bokeh 传"分类 + 数值"混合坐标的惯用写法,scale控制偏移幅度。p.harea(glyph_api.py):水平方向面积图,参数为(x1, x2, y)——y是公共纵坐标(温度),x1/x2是左右边界。这里x1=offset(month, density)、x2=offset(month, -density),即以密度值为半径在类别两侧展开对称的"密度翼",形成每个月的轮廓带。- 散点抖动:注释明确指出"this case is too complex for BokehJS",即该抖动逻辑在 Python 端预先算好:
jitter = (np.random.random(len(tavg)) * 2 - 1) * tavg_density——随机数乘以密度值,使散点横向散布范围随密度变化,最终p.scatter用与轮廓带相同的offset结构放置散点。 - 展示优化:
y_range.start = -10预留底部空间,yaxis.ticker指定刻度,关闭网格线让轮廓带更突出。
SPLOM(散点图矩阵):共享 Range 实现联动
SPLOM(Scatter Plot Matrix,散点图矩阵)把多维数据两两组合排列成网格状散点图,用于快速发现维度间的相关性。指南明确指出其关键组件是联动平移(linked panning)与联动刷选(linked brushing),更详细的机制见 docs/bokeh/source/docs/user_guide/interaction/linking.rst。示例 examples/topics/stats/splom.py 基于 Palmer 企鹅数据,采用底层模型 API(而非figure便捷接口)手工搭建,完整代码:
from itertools import product from bokeh.io import show from bokeh.layouts import gridplot from bokeh.models import (BasicTicker, ColumnDataSource, DataRange1d, Grid, LassoSelectTool, LinearAxis, PanTool, Plot, ResetTool, Scatter, WheelZoomTool) from bokeh.sampledata.penguins import data from bokeh.transform import factor_cmap df = data.copy() df["body_mass_kg"] = df["body_mass_g"] / 1000 SPECIES = sorted(df.species.unique()) ATTRS = ("bill_length_mm", "bill_depth_mm", "body_mass_kg") N = len(ATTRS) source = ColumnDataSource(data=df) xdrs = [DataRange1d(bounds=None) for _ in range(N)] ydrs = [DataRange1d(bounds=None) for _ in range(N)] plots = [] for i, (y, x) in enumerate(product(ATTRS, reversed(ATTRS))): p = Plot(x_range=xdrs[i%N], y_range=ydrs[i//N], background_fill_color="#fafafa", border_fill_color="white", width=200, height=200, min_border=5) if i % N == 0: # first column p.min_border_left = p.min_border + 4 p.width += 40 yaxis = LinearAxis(axis_label=y) yaxis.major_label_orientation = "vertical" p.add_layout(yaxis, "left") yticker = yaxis.ticker else: yticker = BasicTicker() p.add_layout(Grid(dimension=1, ticker=yticker)) if i >= N*(N-1): # last row p.min_border_bottom = p.min_border + 40 p.height += 40 xaxis = LinearAxis(axis_label=x) p.add_layout(xaxis, "below") xticker = xaxis.ticker else: xticker = BasicTicker() p.add_layout(Grid(dimension=0, ticker=xticker)) scatter = Scatter(x=x, y=y, fill_alpha=0.6, size=5, line_color=None, fill_color=factor_cmap('species', 'Category10_3', SPECIES)) r = p.add_glyph(source, scatter) p.x_range.renderers.append(r) p.y_range.renderers.append(r) # suppress the diagonal if (i%N) + (i//N) == N-1: r.visible = False p.grid.grid_line_color = None p.add_tools(PanTool(), WheelZoomTool(), ResetTool(), LassoSelectTool()) plots.append(p) show(gridplot(plots, ncols=N))原理拆解:
- 网格布局:
product(ATTRS, reversed(ATTRS))生成N×N个 (y, x) 维度组合,gridplot(plots, ncols=N)按行排列成矩阵。 - 范围共享是联动核心:
xdrs[i%N]与ydrs[i//N]是关键设计——同一列的图共享同一个 x 轴DataRange1d,同一行的图共享同一个 y 轴DataRange1d。当用户拖拽平移或缩放某张图时,共享 Range 的所有图同步变化,这就是 linked panning 的底层机制(对应交互指南中的 linked panning 章节)。 - 坐标轴与网格的布局策略:只有第一列添加
LinearAxis(左轴)和Grid(dimension=1, ...)(水平网格线),只有最后一行添加 x 轴和Grid(dimension=0, ...)(垂直网格线),其余子图使用BasicTicker保持刻度一致但不重复绘制轴——避免矩阵内部出现冗余坐标轴。 - glyph 与 Range 的关联:
p.add_glyph(source, scatter)手动把Scatterglyph 加入Plot;p.x_range.renderers.append(r)与p.y_range.renderers.append(r)是关键一步,它把该 glyph 纳入 Range 的数据边界计算,否则DataRange1d无法自动适配数据范围。 - 对角线抑制:
(i%N) + (i//N) == N-1判定对角线位置(变量自身 vs 自身,无意义),将 glyph 设为r.visible = False并隐藏网格。 - 工具集:每个子图统一挂载
PanTool、WheelZoomTool、ResetTool与LassoSelectTool。其中LassoSelectTool的选区经共享ColumnDataSource自动传播到其他子图,实现 linked brushing——选中的点在所有子图中同步高亮。
小结:统计图绘制的模式化方法论
纵观examples/topics/stats/下的全部示例,可以总结出 Bokeh 统计图绘制的通用方法论:
| 图表类型 | 统计计算(Python 端) | Bokeh 绘制原语 | 示例文件 |
|---|---|---|---|
| 直方图 | np.histogram | quad+line | histogram.py |
| 人口金字塔图 | np.histogram+ pandas groupby | hbar+text+Label | pyramid.py |
| 箱线图 | pandasquantile+ IQR | Whisker+vbar+scatter | boxplot.py |
| 二维 KDE | scipy.stats.gaussian_kde | contour | kde2d.py |
| 多组密度 | sklearn KernelDensity | varea+Label | density.py |
| SinaPlot | sklearn KernelDensity+ jitter | harea+scatter | sinaplot.py |
| SPLOM | pandas 预处理 | Plot+Scatter+ 共享 Range | splom.py |
核心结论有两点:
- "计算与绘制分离":所有统计量(分箱、分位数、核密度)都在 Python 侧用 NumPy/SciPy/scikit-learn/pandas 完成,Bokeh 只负责把计算结果映射为图形原语,这让图形逻辑完全透明、可测试、可复用;
- "原语组合出高级图":箱线图 =
Whisker+ 两段vbar+scatter,SinaPlot =harea+scatter,SPLOM = 共享 Range 的Plot矩阵——掌握quad/hbar/vbar/scatter/varea/harea/contour这几个 glyph 方法与Whisker/Label注解模型后,几乎可以组装出任意的统计图形。
若需进一步深化,建议继续阅读 交互联动指南(SPLOM 联动的完整机制)、Whisker 注解文档 以及 figure 绘图 API 参考,并结合tests/unit/bokeh/models下的测试用例验证 glyph 与注解的行为细节。
【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考