news 2026/9/14 4:44:12

Bokeh 统计图绘制完全指南:直方图、金字塔图、箱线图、KDE 与 SPLOM 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bokeh 统计图绘制完全指南:直方图、金字塔图、箱线图、KDE 与 SPLOM 实战

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)等六类统计图表。读完本文,你将掌握quadhbarvbarscattervareahareacontour等核心绘图 API 的组合用法,以及Whisker注解、Label标注、范围共享(linked panning/brushing)等进阶技巧,所有示例均可在仓库examples/topics/stats/目录下直接运行验证。

该指南对应的全部示例代码位于 examples/topics/stats/,每个脚本都带有bokeh-example-metadata元数据块,标注了所用 API、关联文档章节与关键词,便于在文档与示例之间交叉检索。

前置准备:统计图绘制的通用思路

在开始之前,先明确 Bokeh 统计图的核心设计哲学:Bokeh 并不提供"直方图"或"箱线图"这类打包好的高层图表函数,而是提供原子化的 glyph 原语与注解模型,由开发者按统计语义自行组装。这意味着你需要:

  1. 用 NumPy / SciPy / scikit-learn / pandas 完成统计计算(分箱、分位数、核密度估计等);
  2. figure上的 glyph 方法(quadhbarvbarscattervareahareacontour)完成图形绘制;
  3. WhiskerLabel等注解模型补充统计图形特有的视觉元素。

上述 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.linefigure.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, ...)将男性计数取负,hbarright参数即为条形右端 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*iqrq1 - 1.5*iqr作为离群点判定的理论上下界。随后一段循环把须线端点"收敛"到实际数据点——上须取 Q3 与上界之间的最大值、下须取 Q1 与下界之间的最小值,这正是 Tukey 箱线图的标准做法,确保须线不过度延伸。
  • Whisker注解(src/bokeh/models/annotations/geometry.py)Whisker(base="kind", upper="upper", lower="lower", source=source)沿分类轴为每个类别绘制一条竖线,upper/lowerCoordinateSpec类型的数据列名,source指定ColumnDataSourcewhisker.upper_head.size = whisker.lower_head.size = 20控制两端箭头头(默认TeeHead,见geometry.pylower_head/upper_headInstanceDefault(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" 数据中hpmpg的联合密度,再用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_kdekernel(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_colorline_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 是"结合核密度信息增强的一维散点图":每个类别沿横轴展开,散点在类别内的横向偏移量正比于该处的核密度,从而同时呈现数据点位置与分布形状。指南指出它由hareascatter两个 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 加入Plotp.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并隐藏网格。
  • 工具集:每个子图统一挂载PanToolWheelZoomToolResetToolLassoSelectTool。其中LassoSelectTool的选区经共享ColumnDataSource自动传播到其他子图,实现 linked brushing——选中的点在所有子图中同步高亮。

小结:统计图绘制的模式化方法论

纵观examples/topics/stats/下的全部示例,可以总结出 Bokeh 统计图绘制的通用方法论:

图表类型统计计算(Python 端)Bokeh 绘制原语示例文件
直方图np.histogramquad+linehistogram.py
人口金字塔图np.histogram+ pandas groupbyhbar+text+Labelpyramid.py
箱线图pandasquantile+ IQRWhisker+vbar+scatterboxplot.py
二维 KDEscipy.stats.gaussian_kdecontourkde2d.py
多组密度sklearn KernelDensityvarea+Labeldensity.py
SinaPlotsklearn KernelDensity+ jitterharea+scattersinaplot.py
SPLOMpandas 预处理Plot+Scatter+ 共享 Rangesplom.py

核心结论有两点:

  1. "计算与绘制分离":所有统计量(分箱、分位数、核密度)都在 Python 侧用 NumPy/SciPy/scikit-learn/pandas 完成,Bokeh 只负责把计算结果映射为图形原语,这让图形逻辑完全透明、可测试、可复用;
  2. "原语组合出高级图":箱线图 =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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 4:44:00

知网查AI与维普降AI有何不同?论文AIGC检测原理与正确降AI实操

开头&#xff1a; 论文季一到&#xff0c;宿舍群里的画风立刻就变了。前两天有个学弟跑来问我&#xff1a;“学校规定用知网查AI率&#xff0c;室友却非让我用维普降AI&#xff0c;说维普上降到0%就能过&#xff0c;这到底听谁的&#xff1f;”我听完第一反应是&#xff1a;这俩…

作者头像 李华
网站建设 2026/9/14 4:43:41

Bitwarden server Aspire AppHost 启动时服务卡在等待状态怎么排查

Bitwarden server Aspire AppHost 启动时服务卡在等待状态怎么排查 【免费下载链接】server Bitwarden infrastructure/backend (API, database, Docker, etc). 项目地址: https://gitcode.com/GitHub_Trending/ser/server 在 Bitwarden server 仓库的 AppHost 目录执行…

作者头像 李华
网站建设 2026/9/14 4:42:28

Dify中Chatflow和Workflow怎么选?区别解析与实战搭建

我不止一次在社群里看到有人问&#xff1a;Dify 里同时有 Chatflow 和 Workflow&#xff0c;打开新建应用的时候两个按钮摆在一起&#xff0c;到底该点哪个&#xff1f;还有人说 Workflow 能做的东西&#xff0c;Chatflow 好像也能做&#xff0c;那为什么要分两种&#xff1f;这…

作者头像 李华
网站建设 2026/9/14 4:35:44

SEO优化完整流程指南:关键词、站内、外链与数据复盘

上个月有个做门窗生意的朋友老刘来找我&#xff0c;说他网站上线快一年了&#xff0c;文章也发了不少&#xff0c;可每天从百度来的访客还是个位数。他上来就问我一句&#xff1a;“SEO到底应该怎么做&#xff1f;是不是已经没用了&#xff1f;”我没急着回答&#xff0c;先让他…

作者头像 李华