资讯动态

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

发布时间:2026/9/14 4:44:15 来源:尧图企业网站定制
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.pycontour位于 src/bokeh/plotting/_figure.py它们内部都通过glyph_method装饰器绑定到对应的 glyph 模型类因此既可以用高层方法也可以用底层Plot.add_glyph手动组装SPLOM 一节会演示后者。直方图Histogramquad np.histogram直方图是最基础的分布可视化。官方推荐的做法是先用np.histogram完成分箱统计再用quadglyph 将每个箱绘制为一个矩形。完整示例见 examples/topics/stats/histogram.pyimport numpy as np from bokeh.plotting import figure, show rng np.random.default_rng() x rng.normal(loc0, scale1, size1000) p figure(width670, height400, toolbar_locationNone, titleNormal (Gaussian) Distribution) # Histogram bins np.linspace(-3, 3, 40) hist, edges np.histogram(x, densityTrue, binsbins) p.quad(tophist, bottom0, leftedges[:-1], rightedges[1:], fill_colorskyblue, line_colorwhite, legend_label1000 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_width2, line_colornavy, legend_labelProbability Density Function) p.y_range.start 0 p.xaxis.axis_label x p.yaxis.axis_label PDF(x) show(p)关键点拆解np.histogram(x, densityTrue, binsbins)返回(hist, edges)两个数组。densityTrue表示输出的是概率密度而非频数从而可以与理论概率密度函数PDF叠加对比。bins可以传整数自动分箱或数组显式指定箱边界本示例用np.linspace(-3, 3, 40)在 [-3, 3] 区间内生成 40 个均匀边界。p.quad(...)quad是绘制矩形的 glyphglyph_api.py核心参数为top上边 y 坐标、bottom下边 y 坐标、left左边 x 坐标、right右边 x 坐标。这里利用edges[:-1]与edges[1:]错位切片恰好把每个箱的左右边界对起来tophist让矩形高度等于箱内密度值。quad天然支持矢量参数一次调用即可绘制全部 40 个箱。密度曲线叠加直接在同一figure上再调用p.line绘制标准正态 PDFlegend_label让两条曲线自动进入图例。坐标轴细节p.y_range.start 0强制 y 轴从 0 开始直方图不应有负值p.xaxis.axis_label/p.yaxis.axis_label设置轴标题。这个示例同时演示了figure.line与figure.quad的组合使用见脚本中的:apis:元数据是统计计算在 Python 端、绘制在 Bokeh 端这一模式的最小范例。人口金字塔图Population Pyramidhbar 绘制发散条形图人口金字塔是一种发散式水平条形图divergent horizontal bar plot用于对比两组人群的分布。其技巧非常巧妙将一组的计数取负值绘制在 x 轴负半区另一组绘制在正半区从而形成左右对称的金字塔形状。在 Bokeh 中它由hbarglyph 实现示例见 examples/topics/stats/pyramid.py数据来自bokeh.sampledata.titanicimport 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, binsbins) f_hist, edges np.histogram(female_ages, binsbins) p figure(titleAge population pyramid of titanic passengers, by gender, height400, width600, x_range(-90, 90), x_axis_labelcount) p.hbar(rightf_hist, yedges[1:], heightbin_width*0.8, colorcolors[0], line_width0) p.hbar(rightm_hist * -1, yedges[1:], heightbin_width*0.8, colorcolors[1], line_width0) # add text to every other bar for i, (count, age) in enumerate(zip(f_hist, edges[1:])): if i % 2 1: continue p.text(xcount, yedges[1:][i], text[f{age-bin_width}-{age}yrs], x_offset5, y_offset7, text_font_size12px, text_colorDarkText[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(codereturn Math.abs(tick);) # add labels p.add_layout(Label(x-40, y70, textMen, text_colorcolors[1], x_offset5)) p.add_layout(Label(x20, y70, textWomen, text_colorcolors[0], x_offset5)) show(p)实现要点数据准备用 pandas 的groupby(sex)分别取出男、女乘客的年龄dropna()丢弃缺失值np.arange(0, 72, bin_width)以 5 岁为箱宽生成边界男女各做一次np.histogram。左右镜像p.hbar(rightf_hist, ...)绘制女性一侧p.hbar(rightm_hist * -1, ...)将男性计数取负hbar的right参数即为条形右端 x 坐标默认left0负值条形自然落在负半轴。yedges[1:]把条形中心放在每个年龄箱的上边界处heightbin_width*0.8让条形之间留出 20% 间隙。坐标刻度绝对值化由于男性侧是负值直接用CustomJSTickFormatter(codereturn Math.abs(tick);)把刻度标签取绝对值显示避免出现 -40 人 这种误导性读数。Label注解p.add_layout(Label(x..., y..., textMen/Women, ...))在图中直接摆放文字标签Label 模型x_offset/y_offset微调偏移量。此外还用p.text为每隔一个箱添加年龄区间文字。视觉收敛p.xaxis.ticker手动指定刻度位置、p.yaxis.visible False隐藏 y 轴、p.ygrid.grid_line_color None去掉横向网格线让金字塔更干净。箱线图BoxplotWhisker 注解 vbar scatter箱线图在 Bokeh 中由三部分组装而成Whisker注解绘制须线、vbar绘制四分位箱体、scatter绘制离群点。完整示例见 examples/topics/stats/boxplot.py数据来自bokeh.sampledata.autompg2import 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(fkind{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, onkind, howleft) source ColumnDataSource(qs) p figure(x_rangekinds, tools, toolbar_locationNone, titleHighway MPG distribution by vehicle class, background_fill_color#eaefef, y_axis_labelMPG) # outlier range whisker Whisker(basekind, upperupper, lowerlower, sourcesource) 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, sourcesource, colorcmap, line_colorblack) p.vbar(kind, 0.7, q1, q2, sourcesource, colorcmap, line_colorblack) # outliers outliers df[~df.hwy.between(df.lower, df.upper)] p.scatter(kind, hwy, sourceoutliers, size6, colorblack, alpha0.3) p.xgrid.grid_line_color None p.axis.major_label_text_font_size14px p.axis.axis_label_text_font_size12px 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.pyWhisker(basekind, upperupper, lowerlower, sourcesource)沿分类轴为每个类别绘制一条竖线upper/lower是CoordinateSpec类型的数据列名source指定ColumnDataSource。whisker.upper_head.size whisker.lower_head.size 20控制两端箭头头默认TeeHead见geometry.py中lower_head/upper_head的InstanceDefault(TeeHead, size10)的尺寸。最后必须通过p.add_layout(whisker)把注解挂到图上。箱体两段 vbarp.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调色板。离群点scatterdf[~df.hwy.between(df.lower, df.upper)]筛出落在须线范围之外的行p.scatter以半透明黑色圆点标出alpha0.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(height400, x_axis_labelhp, y_axis_labelmpg, background_fill_color#fafafa, tools, toolbar_locationNone, titleKernel 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_colorpalette, line_colorpalette) show(p)实现细节KDE 计算kde()函数先用np.mgrid在数据包围盒内生成N300的二维网格点把网格点与原始观测值np.vstack后喂给gaussian_kdekernel(positions)得到每个网格点的密度值再np.reshape回网格形状。p.contoursrc/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(xx)) p figure(titleMultiple density estimates, height300, x_range(2.5, 7.5), x_axis_labelbutterfat contents, y_axis_labeldensity) for (breed, breed_df), color in zip(breed_groups, colors): data breed_df[butterfat].values kde KernelDensity(kernelgaussian, bandwidth0.2).fit(data[:, np.newaxis]) log_density kde.score_samples(x[:, np.newaxis]) y np.exp(log_density) source.add(y, breed) p.varea(xx, y1breed, y20, sourcesource, fill_alpha0.3, fill_colorcolor) # Find the highest point and annotate with a label max_idx np.argmax(y) highest_point_label Label( xx[max_idx], yy[max_idx], textbreed, text_font_size10pt, x_offset10, y_offset-5, text_colorcolor, ) 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(kernelgaussian, bandwidth0.2)构造高斯核估计器score_samples返回对数密度np.exp还原为密度值。bandwidth是核宽度控制曲线的平滑程度。p.vareaglyph_api.py垂直方向面积图参数为(x, y1, y2)——x是横坐标列y1是上边界密度曲线y20是下边界基线。多个品种的密度列通过source.add(y, breed)动态追加到同一个ColumnDataSource因此可以在循环内以列名引用。顶点标注np.argmax(y)找到密度峰值位置用Label在该点旁标注品种名text_color与曲线颜色一致。格式化与精简PrintfTickFormatter(format%d%%)把 x 轴显示为百分比隐藏坐标轴线与刻度线、关闭 x 网格、手动设置 y 轴刻度(0, 0.5, 1, 1.5)使多曲线叠加图保持清爽。SinaPlotharea 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( height400, width600, x_rangemonths, x_axis_labelmonth, y_axis_labelmean temperature (F), ) # add a non-uniform categorical offset to a given category def offset(category, data, scale7): 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(kernelgaussian, bandwidth3).fit(tavg[:, np.newaxis]) density np.exp(kde.score_samples(temps[:, np.newaxis])) x1, x2 offset(month, density), offset(month, -density) p.harea(x1x1, x2x2, ytemps, alpha0.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(xoffset(month, jitter), ytavg, colorblack) p.y_range.start -10 p.yaxis.ticker [0, 25, 50, 75] p.grid.grid_line_color None show(p)实现要点数据预处理DATE解析为 datetimeTAVG (TMAX TMIN) / 2计算日均温再按%b格式提取月份缩写作为分类轴。offset辅助函数返回[(category, 偏移值), ...]的坐标对列表——这是向 Bokeh 传分类 数值混合坐标的惯用写法scale控制偏移幅度。p.hareaglyph_api.py水平方向面积图参数为(x1, x2, y)——y是公共纵坐标温度x1/x2是左右边界。这里x1offset(month, density)、x2offset(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 实现联动SPLOMScatter 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(datadf) xdrs [DataRange1d(boundsNone) for _ in range(N)] ydrs [DataRange1d(boundsNone) for _ in range(N)] plots [] for i, (y, x) in enumerate(product(ATTRS, reversed(ATTRS))): p Plot(x_rangexdrs[i%N], y_rangeydrs[i//N], background_fill_color#fafafa, border_fill_colorwhite, width200, height200, min_border5) if i % N 0: # first column p.min_border_left p.min_border 4 p.width 40 yaxis LinearAxis(axis_labely) yaxis.major_label_orientation vertical p.add_layout(yaxis, left) yticker yaxis.ticker else: yticker BasicTicker() p.add_layout(Grid(dimension1, tickeryticker)) if i N*(N-1): # last row p.min_border_bottom p.min_border 40 p.height 40 xaxis LinearAxis(axis_labelx) p.add_layout(xaxis, below) xticker xaxis.ticker else: xticker BasicTicker() p.add_layout(Grid(dimension0, tickerxticker)) scatter Scatter(xx, yy, fill_alpha0.6, size5, line_colorNone, fill_colorfactor_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, ncolsN))原理拆解网格布局product(ATTRS, reversed(ATTRS))生成N×N个 (y, x) 维度组合gridplot(plots, ncolsN)按行排列成矩阵。范围共享是联动核心xdrs[i%N]与ydrs[i//N]是关键设计——同一列的图共享同一个 x 轴DataRange1d同一行的图共享同一个 y 轴DataRange1d。当用户拖拽平移或缩放某张图时共享 Range 的所有图同步变化这就是 linked panning 的底层机制对应交互指南中的 linked panning 章节。坐标轴与网格的布局策略只有第一列添加LinearAxis左轴和Grid(dimension1, ...)水平网格线只有最后一行添加 x 轴和Grid(dimension0, ...)垂直网格线其余子图使用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并隐藏网格。工具集每个子图统一挂载PanTool、WheelZoomTool、ResetTool与LassoSelectTool。其中LassoSelectTool的选区经共享ColumnDataSource自动传播到其他子图实现 linked brushing——选中的点在所有子图中同步高亮。小结统计图绘制的模式化方法论纵观examples/topics/stats/下的全部示例可以总结出 Bokeh 统计图绘制的通用方法论图表类型统计计算Python 端Bokeh 绘制原语示例文件直方图np.histogramquadlinehistogram.py人口金字塔图np.histogram pandas groupbyhbartextLabelpyramid.py箱线图pandasquantile IQRWhiskervbarscatterboxplot.py二维 KDEscipy.stats.gaussian_kdecontourkde2d.py多组密度sklearn KernelDensityvareaLabeldensity.pySinaPlotsklearn KernelDensity jitterhareascattersinaplot.pySPLOMpandas 预处理PlotScatter 共享 Rangesplom.py核心结论有两点计算与绘制分离所有统计量分箱、分位数、核密度都在 Python 侧用 NumPy/SciPy/scikit-learn/pandas 完成Bokeh 只负责把计算结果映射为图形原语这让图形逻辑完全透明、可测试、可复用原语组合出高级图箱线图 Whisker 两段vbarscatterSinaPlot hareascatterSPLOM 共享 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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价