当前位置: 首页 > news >正文

Bokeh数据结构选型指南:NumPy、Pandas与ColumnDataSource实战解析

1. 为什么用 Bokeh 做数据可视化?——不是“又一个绘图库”,而是交互式叙事的起点

我带过十几期 Python 数据分析实战训练营,每次讲到可视化环节,总有人问:“Matplotlib 不够用吗?Seaborn 不香吗?”——这话没错,但当你真正需要把一张图放进周报、嵌进内部系统、或者发给业务方看时,问题就来了:Matplotlib 生成的是静态 PNG,缩放模糊;Seaborn 封装虽好,但定制交互逻辑几乎要重写底层;而你花半小时调好的双轴折线图,业务同事点开后第一句是:“能不能让我拖动看看某段具体日期的价格?”——这时候,Bokeh 就不是“可选项”,而是“不得不选”的工具。

Bokeh 的核心价值,从来不在“画得美不美”,而在“数据能不能说话”。它把 Python 的数据处理能力(NumPy/Pandas)和浏览器的交互能力(JavaScript 渲染、缩放、悬停、联动)无缝缝合在一起。它的 ColumnDataSource 不是简单的数据容器,而是一个活的数据中枢:你改一行数据,所有关联图表实时响应;你加一个滑块控件,整张时间序列图自动过滤;你点中散点图里某个异常点,右侧表格立刻高亮对应原始记录。这种“数据-视图-交互”三位一体的能力,在金融监控、IoT 设备看板、A/B 测试仪表盘等真实场景里,省下的不是代码行数,而是反复导出截图、手动标注、再发邮件解释的时间。

这篇教程聚焦的三个数据结构——NumPy 数组、Pandas DataFrame、ColumnDataSource——不是随意罗列的“语法清单”,而是你实际工作中最常踩坑的三道门槛。比如,新手直接用plot.line(x_array, y_array)画股价,结果 x 轴显示成 0,1,2,3…(因为 NumPy 索引被当成了坐标);又比如用 Pandas 读取 CSV 后直接传df['date']给 Bokeh,发现日期全挤在左下角变成一坨黑线(没转 datetime);再比如多个子图共用同一份股票数据,却反复pd.read_csv()加载三次,Jupyter 内存直接爆掉……这些都不是“不会写”,而是没理解 Bokeh 的数据契约:它不接受“裸数据”,只认“有结构、有类型、有语义”的数据对象。

所以,这不是一篇教你“怎么画线”的教程,而是一份帮你建立 Bokeh 数据思维的操作手册。我会拆解每种结构背后的原理、实操中必踩的坑、以及如何用最少的代码写出最稳健的交互式图表。无论你是刚学完 Pandas 的分析师,还是想给爬虫脚本加个实时监控页的工程师,只要你的数据还在 Python 里流动,这篇内容就能让你少走三个月弯路。

2. 核心数据结构设计与选型逻辑:为什么不是“随便选一个”,而是“必须分三层”

2.1 三层数据结构的本质差异:从“临时快照”到“持久中枢”

Bokeh 的数据处理模型,本质上是三层递进关系。很多教程把它们并列讲解,导致初学者混淆使用场景。我用一个真实案例说明:上周帮一家电商公司做促销效果复盘,他们需要同时展示三张图——(1)近30天GMV趋势线、(2)各品类销量散点分布、(3)点击率与转化率的联动热力图。如果强行用同一种数据结构实现,会怎样?

  • NumPy 数组层:适合“一次性、无上下文”的数学计算结果。比如你用np.polyfit()拟合出一条趋势线的系数,想快速画出来验证效果。它的优势是内存极轻、索引极快;劣势是零元数据——没有列名、没有时间类型、没有缺失值标记。你传x_array = [1,2,3],Bokeh 只知道这是三个数字,不知道它们代表“第1/2/3天”,更不知道该不该按时间排序。所以,它只该出现在“中间计算结果”的输出端,绝不该作为原始数据源。

  • Pandas DataFrame 层:这是大多数人的默认选择,也是最容易误用的一层。DataFrame 天然带列名、索引、dtypes,看起来很“完整”。但问题在于:Bokeh 并不直接消费 DataFrame。当你写plot.line(x=df['date'], y=df['price'])时,Bokeh 实际上是在每次绘图时,偷偷把这两列抽出来转成 NumPy 数组再渲染。这意味着:(1)如果你画5张图都用同一个 df,内存里其实存了5份独立副本;(2)DataFrame 的索引(比如日期索引)在传递过程中可能丢失;(3)无法实现跨图表联动——你点中A图的某天,B图不会高亮对应数据。它适合“单图快览”,不适合“多图协同”。

  • ColumnDataSource 层:这才是 Bokeh 的心脏。它本质是一个带类型声明的字典,但关键在于:所有引用它的图表,共享同一块内存地址。你改source.data['price'][10] = 99.99,所有用这个 source 的图立刻刷新。它强制你为每列数据声明类型('date': np.array([...], dtype='datetime64[ns]')),让 Bokeh 在渲染前就知道该怎么处理时间轴、怎么格式化数字、怎么处理 NaN。更重要的是,它内置了 Bokeh 的事件系统——你可以绑定source.selected.on_change(...)监听用户点击,这是 DataFrame 永远做不到的。

提示:一个简单判断标准——如果你的图表只需要“自己看”,用 DataFrame 最省事;如果要“给别人用”或“和其他图联动”,必须升维到 ColumnDataSource。别省那两行代码,后期维护成本差十倍。

2.2 为什么 ColumnDataSource 是“唯一正确起点”?——从内存管理到交互架构

很多人觉得 ColumnDataSource “多此一举”,毕竟figure().line()直接传数组也能出图。但我在生产环境部署过27个 Bokeh 应用,所有崩溃事故里,73% 源于数据源管理混乱。举个典型例子:某风控系统要实时显示交易延迟分布,开发同学用pd.read_csv('log.csv')每秒重读一次文件,然后plot.quad()画直方图。结果运行三天后服务器 OOM——不是因为数据量大,而是每次读取都创建新 DataFrame,旧对象没被及时 GC,内存碎片越积越多。

ColumnDataSource 从根本上解决了这个问题。它的设计哲学是:“数据只加载一次,视图按需订阅”。你只需:

# 一次加载,永久持有 source = ColumnDataSource(data={ 'timestamp': np.array([], dtype='datetime64[ns]'), 'latency_ms': np.array([]), 'status': np.array([]) }) # 后续更新,只替换数据,不重建对象 new_data = { 'timestamp': np.append(source.data['timestamp'], new_timestamps), 'latency_ms': np.append(source.data['latency_ms'], new_latencies), 'status': np.append(source.data['status'], new_statuses) } source.data = new_data # 关键!不是 source = ColumnDataSource(new_data)

这段代码实现了真正的流式更新:内存地址不变,Bokeh 自动 diff 差异并局部重绘。而 DataFrame 方案必须df = pd.concat([df, new_df]),触发完整内存拷贝。

更深层的价值在于交互架构。ColumnDataSource 是 Bokeh 事件系统的唯一入口。比如你想实现“点击散点图,下方表格显示详情”,代码是:

# 表格数据源 table_source = ColumnDataSource(df_details) # 散点图数据源 scatter_source = ColumnDataSource(df_scatter) # 绑定点击事件 def on_selection_change(attr, old, new): if new.indices: # 有选中项 selected_rows = scatter_source.data['id'][new.indices] table_source.data = df_details[df_details['id'].isin(selected_rows)] scatter_source.selected.on_change('indices', on_selection_change)

这里selected.indices是 ColumnDataSource 的专属属性,DataFrame 根本没有这个概念。没有 ColumnDataSource,所谓“交互式可视化”就是一句空话。

注意:ColumnDataSource 的data字典键名,就是你在 glyph 函数里用的x='key_name'中的 key。命名必须严格匹配,且不能含空格或特殊字符(如'high price'会报错,必须用'high_price')。这是新手最常犯的拼写错误。

3. NumPy 数组绘图:从“能画出来”到“画得准确”的硬核细节

3.1 线图陷阱:为什么你的 x 轴总是从 0 开始?

看这段官方示例代码:

x_array = np.array([10,20,30,40,50,60]) y_array = np.array([50,60,70,80,90,100]) plot.line(x_array, y_array)

它确实能出图,但如果你把x_array换成[20230101, 20230102, ...]这样的数值型日期,结果会非常诡异:x 轴显示为2.023e7,刻度间隔巨大,根本看不出日期变化。原因在于:Bokeh 默认将纯数字数组视为“无单位序号”,它不会主动推断20230101是“2023年1月1日”,只会当成普通浮点数处理。

解决方案分三步:

  1. 明确数据语义:先用pd.to_datetime()np.datetime64转换类型;
  2. 强制指定轴类型:在figure()中设置x_axis_type='datetime'
  3. 确保数组 dtype 正确x_array.dtype必须是datetime64[ns],不能是object

实操对比:

# ❌ 错误示范:字符串数组(dtype=object) x_str = np.array(['2023-01-01', '2023-01-02']) # Bokeh 会报错:TypeError: expected an array of dates... # ❌ 错误示范:整数数组(dtype=int64) x_int = np.array([20230101, 20230102]) # 渲染为科学计数法,无日期格式 # ✅ 正确示范:datetime64 数组 x_dt = np.array(['2023-01-01', '2023-01-02'], dtype='datetime64[ns]') plot = figure(x_axis_type='datetime') plot.line(x_dt, y_array) # 自动识别为日期,显示"Jan 1", "Jan 2"

这里的关键是dtype='datetime64[ns]'[ns]表示纳秒精度,这是 Pandas 和 Bokeh 共同约定的标准。如果你用dtype='datetime64[D]'(天精度),Bokeh 可能无法正确解析。

3.2 散点图避坑指南:尺寸、透明度与性能的三角平衡

散点图看似简单,但sizealpha参数的取值逻辑常被误解。看这行代码:

plot.circle(x_arr, y_arr, size=30, alpha=0.5)

新手以为size=30是“直径30像素”,其实不然。Bokeh 的size参数单位是屏幕像素(screen pixels),但它受两个因素影响:(1)图表整体尺寸(plot.width);(2)数据范围(x_range/y_range)。当你放大图表时,size=30的圆点不会变大,因为它锚定在屏幕坐标系。

更隐蔽的问题是性能。如果你有10万点,size=30会让每个圆渲染成一个大圆盘,GPU 负载飙升。实测数据:10万点size=5时帧率 60fps,size=30时跌至 8fps。解决方案是动态缩放:

# 根据数据点数量自动调整 size n_points = len(x_arr) base_size = 5 if n_points > 5000: base_size = 2 elif n_points > 100000: base_size = 1 plot.circle(x_arr, y_arr, size=base_size, alpha=0.7)

alpha(透明度)同样有陷阱。alpha=0.5不是“半透明”,而是“50%不透明”。当大量点重叠时,叠加区域会变深(类似 Photoshop 的 Normal 混合模式)。如果你想让密集区颜色更深以体现密度,alpha=0.1配合size=1是更好的选择——100个点重叠处会比单点深100倍,自然形成热力图效果。

实操心得:画大规模散点图前,务必用datashader预聚合。Bokeh 本身不支持大数据降采样,直接传100万点必然卡死。正确姿势是:import datashader as ds; agg = ds.Canvas().points(df, 'x', 'y'); img = ds.transfer_functions.shade(agg),再把img作为image_url绘制。这是工业级方案,比硬扛强十倍。

3.3 数组绘图的终极技巧:用multi_line画多条趋势线

NumPy 数组最被低估的能力是multi_line——它能一次性画出几十条线,比循环调用line()快50倍。比如你要对比10只股票的日线:

# 假设 data_2d 是 shape=(10, 1000) 的数组,每行一只股票 # x_coords 是 shape=(1000,) 的日期数组 plot.multi_line( xs=[x_coords for _ in range(10)], # 10次重复 x 坐标 ys=data_2d.tolist(), # 转为列表,每行一个 y 数组 line_color=['red','blue','green'] * 4, # 循环配色 line_width=2 )

注意xsys必须是相同长度的列表,且每个元素是长度一致的数组。multi_line内部用 WebGL 批量渲染,避免 Python 循环开销。我在回测系统中用它画200只股票的5年均线,耗时从12秒降到0.3秒。

4. Pandas DataFrame 绘图:从“能跑通”到“生产就绪”的七道关卡

4.1 时间序列的生死线:pd.to_datetime()的四个致命参数

DataFrame 绘图失败,90% 源于日期处理。看这个常见错误:

df = pd.read_csv('stocks.csv') df['date'] = pd.to_datetime(df['date']) # ❌ 危险! plot.line(x=df['date'], y=df['close'])

表面看没问题,但当 CSV 中存在'2023-13-01'这样的非法日期时,pd.to_datetime()默认会设为NaT(Not a Time),而 Bokeh 遇到NaT会直接崩溃,报错ValueError: cannot convert NaT to datetime64

必须显式处理:

# ✅ 安全写法 df['date'] = pd.to_datetime( df['date'], errors='coerce', # 非法日期转为 NaT(不是抛异常) infer_datetime_format=True, # 加速解析,假设格式统一 cache=True # 缓存转换结果,避免重复计算 ) # 过滤掉 NaT df = df.dropna(subset=['date'])

errors='coerce'是底线,infer_datetime_format=True能提速3倍(对百万行数据),cache=True在 Jupyter 中尤其重要——你反复运行单元格时不会重复解析。

另一个陷阱是时区。如果数据来自不同时区(如美股+港股),pd.to_datetime()默认返回 naive datetime(无时区信息),Bokeh 渲染时会按本地时区偏移,导致时间轴错位。正确做法:

df['date'] = pd.to_datetime(df['date']).dt.tz_localize('UTC') # 或者转换为本地时区 df['date'] = pd.to_datetime(df['date']).dt.tz_convert('Asia/Shanghai')

4.2 多数据源关联:merge的隐式陷阱与concat的替代方案

教程中用pd.merge()关联 Google 和 Apple 股价,这在小数据集上可行,但生产环境有严重隐患。merge默认how='inner',会丢弃任一表中缺失的日期。比如苹果某天停牌无数据,那天谷歌数据也被删,导致相关性计算失真。

更鲁棒的做法是outer join+ 前向填充:

# 保留所有日期,缺失值用前值填充 df_merged = pd.merge(df_goog, df_apple, on='date', how='outer') df_merged = df_merged.sort_values('date').fillna(method='ffill')

merge本身有性能问题:它会创建新 DataFrame,内存翻倍。对于高频数据(如每秒行情),推荐用pd.concat()拼接后pivot

# 将两只股票数据堆叠成长表 df_long = pd.concat([ df_goog[['date','high']].assign(stock='GOOG'), df_apple[['date','high']].assign(stock='AAPL') ]) # 转为宽表:date 为索引,列名为 stock df_wide = df_long.pivot(index='date', columns='stock', values='high') # 直接传给 Bokeh plot.line(x=df_wide.index, y=df_wide['GOOG'], legend_label='GOOG') plot.line(x=df_wide.index, y=df_wide['AAPL'], legend_label='AAPL')

pivotmerge内存占用低40%,且天然支持多只股票扩展。

4.3 DataFrame 到 Bokeh 的“最后一公里”:.values还是.to_numpy()

当你写plot.line(x=df['date'], y=df['close']),Bokeh 内部会调用df['date'].values获取 NumPy 数组。但 Pandas 0.24+ 推荐用.to_numpy(),因为:

  • .values可能返回object数组(如混合类型列),Bokeh 无法处理;
  • .to_numpy()强制转换为同质数组,且支持dtype参数。

安全写法:

# 显式指定 dtype,避免 object 类型 x_data = df['date'].to_numpy(dtype='datetime64[ns]') y_data = df['close'].to_numpy(dtype='float64') plot.line(x=x_data, y=y_data)

4.4 性能杀手:df.plot()和 Bokeh 的混用误区

很多教程教“用 Pandas 自带的df.plot()生成 Bokeh 图”,这是重大误导。df.plot(backend='bokeh')是 Pandas 的封装,它:

  • 会忽略你自定义的figure设置(如x_axis_type);
  • 无法访问底层ColumnDataSource,丧失交互能力;
  • 渲染速度比原生 Bokeh 慢2-3倍(多一层抽象)。

正确姿势永远是:Pandas 只负责数据清洗,Bokeh 只负责渲染。把清洗后的干净数组传给figure().line(),而不是依赖df.plot()

5. ColumnDataSource 实战:从“数据容器”到“交互引擎”的深度开发

5.1 构建健壮数据源:dict初始化的五个黄金法则

ColumnDataSource 的data参数看着像普通字典,但有严格约束。我总结出初始化时的五条铁律:

  1. 所有值必须是同长度的序列len(data['x']) == len(data['y']) == len(data['color'])。Bokeh 不检查长度,但运行时报Length mismatch
  2. 禁止嵌套结构data={'users': [{'name':'a'}, {'name':'b'}]}会失败。必须展平:data={'user_name':['a','b']}
  3. 日期列必须是datetime64[ns]pd.Timestamp对象可以,但datetime.datetime不行(需先转np.datetime64)。
  4. 字符串列避免objectdtype:用astype('string')替代astype('object'),防止 Bokeh 解析失败。
  5. 数值列显式声明dtype'price': np.array([1.1,2.2]).astype('float64'),避免float32导致精度丢失。

安全初始化模板:

def safe_cds(df): data = {} for col in df.columns: s = df[col] if pd.api.types.is_datetime64_any_dtype(s): data[col] = s.astype('datetime64[ns]').to_numpy() elif pd.api.types.is_string_dtype(s): data[col] = s.astype('string').to_numpy() else: # 强制转 float64,避免 int32 溢出 data[col] = pd.to_numeric(s, downcast='float').astype('float64').to_numpy() return ColumnDataSource(data) source = safe_cds(df_goog)

5.2 动态更新策略:stream()patch()data=的适用场景

ColumnDataSource 更新有三种方式,选错会导致性能灾难:

  • source.data = new_dict:全量替换。适合数据量小(<1万行)或结构变更(如增减列)。缺点:触发全图重绘,旧数据状态丢失(如用户已选中的点)。

  • source.stream(new_data, rollover=1000):追加流式数据。rollover控制最大行数,超限自动删除最早行。适合实时监控(如传感器数据),内存恒定。

  • source.patch({'column_name': [(row_index, new_value), ...]}):局部更新。只改指定位置的值,不触发重绘,仅更新对应图形元素。适合高频更新(如每秒改10个点的颜色)。

实测性能对比(1万行数据):

方法内存增长CPU 占用是否保留选择状态
data=+8MB12%
stream()+0.1MB3%是(新增行可选)
patch()+0.01MB<1%

工业级建议:实时系统用stream(),交互式分析用patch(),批量导入用data=

5.3 跨图表联动:用CustomJS实现“点击即联动”的底层逻辑

ColumnDataSource 的真正威力,在于它能让不同图表“看到彼此”。比如点击左侧散点图的点,右侧直方图自动聚焦该点所在区间。这需要CustomJS(Bokeh 的 JavaScript 回调):

# 创建两个数据源 scatter_source = ColumnDataSource(df_scatter) hist_source = ColumnDataSource(df_hist) # 散点图 scatter = figure() scatter.circle('x', 'y', source=scatter_source, size=10) # 直方图 hist = figure() hist.quad('top', 'bottom', 'left', 'right', source=hist_source) # JavaScript 回调:当 scatter_source 被选中时... callback = CustomJS(args=dict(scatter_source=scatter_source, hist_source=hist_source), code=""" // 获取选中的行索引 const indices = scatter_source.selected.indices; if (indices.length === 0) return; // 计算 x 值范围 const x_data = scatter_source.data['x']; const min_x = Math.min(...indices.map(i => x_data[i])); const max_x = Math.max(...indices.map(i => x_data[i])); // 更新直方图数据源(伪代码,实际需计算 bin) hist_source.data['top'] = compute_histogram(x_data, min_x, max_x); hist_source.change.emit(); // 通知 Bokeh 更新 """) scatter_source.selected.js_on_change('indices', callback)

这段代码的核心是js_on_change——它把 Python 的事件监听,桥接到浏览器端的 JavaScript。CustomJS中可以直接访问source.data,执行复杂计算,再调用change.emit()触发重绘。这是 Bokeh 区别于其他库的“核武器”。

注意:CustomJS里不能调用 Python 函数,所有逻辑必须用 JS 实现。复杂计算建议提前在 Python 端算好,存入source的额外列中,JS 只做索引映射。

6. 常见问题与排查技巧实录:那些文档里不会写的血泪教训

6.1 “图出来了,但点不动”——交互失效的四大元凶

问题现象:图表正常显示,但鼠标悬停无提示、点击无反应、缩放无效。

排查清单

  1. 检查output_file()show()顺序:必须先output_file('plot.html'),再show(plot)。如果漏掉output_file(),Bokeh 会尝试用output_notebook(),但在某些 Jupyter 环境(如 VS Code)中失效。
  2. 确认HoverTool已添加:悬停提示不是默认开启的!必须显式添加:
    from bokeh.models import HoverTool hover = HoverTool(tooltips=[("Date", "@date{%F}"), ("Price", "@close{$0,0.00}")], formatters={'@date': 'datetime'}) plot.add_tools(hover)
  3. 检查source是否被覆盖:如果后续代码写了source = ColumnDataSource(new_data),旧的source引用就失效了,绑定的事件全部丢失。
  4. 浏览器控制台报错:按 F12 打开开发者工具,切换到 Console 标签。常见错误Uncaught ReferenceError: Bokeh is not defined表示 Bokeh JS 未加载,需检查网络或换用output_file()

6.2 “日期轴乱码”——从2023-01-01Jan 1的格式化秘籍

问题现象:x 轴显示2023-01-01 00:00:00,而非简洁的Jan 1

解决方案:用DatetimeTickFormatter精确控制:

from bokeh.models import DatetimeTickFormatter plot.xaxis.formatter = DatetimeTickFormatter( hours=["%d %b"], days=["%d %b"], months=["%b %Y"], years=["%Y"] )

hours控制小时级刻度,days控制天级,依此类推。%b是英文缩写(Jan),%d是日期,%Y是四位年份。这样当图表宽度变化时,Bokeh 会自动选择最合适的格式。

6.3 “内存爆炸”——Jupyter 中反复运行单元格的隐形杀手

问题现象:第一次运行show(plot)正常,第二次运行后浏览器卡死,内存占用飙升。

根本原因show()每次都创建新文档,旧文档未销毁。Jupyter 中尤其严重。

解决方法

  • 方案1(推荐):用curdoc().clear()清理当前文档:
    from bokeh.io import curdoc curdoc().clear() # 运行前加这一行 show(plot)
  • 方案2:改用output_file()输出 HTML 文件,避免 Jupyter 渲染器冲突。
  • 方案3:重启内核(治标不治本)。

6.4 “中文乱码”——字体缺失的终极修复

问题现象:标题、坐标轴标签显示为方框 □□□。

原因:Bokeh 默认用 Sans-Serif 字体,系统无中文字体时 fallback 失败。

三步修复

  1. 下载思源黑体(免费开源):https://github.com/adobe-fonts/source-han-sans
  2. SourceHanSansSC-Regular.otf放入项目目录
  3. 在代码中指定字体路径:
    from bokeh.resources import INLINE from bokeh.util.dependencies import export # 注册字体(需在 show() 前) export('SourceHanSansSC', 'SourceHanSansSC-Regular.otf') plot.title.text_font = 'SourceHanSansSC' plot.xaxis.axis_label_text_font = 'SourceHanSansSC'

6.5 “部署失败”——从本地到服务器的环境鸿沟

问题现象:本地show(plot)正常,部署到服务器后白屏。

关键检查点

  • Python 版本:Bokeh 3.x 要求 Python 3.8+,服务器可能是 3.6。
  • 依赖版本冲突pip install bokeh可能装错版本。生产环境必须锁定:
    pip install "bokeh>=3.0,<3.1"
  • 缺少nodejs:Bokeh 服务端渲染(bokeh serve)需要 nodejs 编译 JS。Ubuntu 上:sudo apt install nodejs npm
  • 防火墙bokeh serve默认端口 5006,需开放。

我踩过的最深的坑:某次部署到阿里云 ECS,一切配置正确,但始终 502 错误。最后发现是安全组规则里,只放行了 80/443,忘了加 5006。花了6小时查日志,教训深刻——部署前先 telnet 测试端口连通性。

7. 从入门到落地:一个完整的股票分析仪表盘实战

7.1 需求拆解:业务方真正要什么?

我们常陷入技术细节,忘了业务本质。以股票分析为例,业务方要的从来不是“一张漂亮的图”,而是:

  • 快速定位异常:哪天价格突变?哪个指标偏离均值?
  • 交叉验证假设:成交量放大时,价格是否一定上涨?
  • 分享结论:一键导出带标注的图片,或生成可交互链接。

所以,仪表盘必须包含:

  • 主趋势图(带移动平均线)
  • 成交量柱状图(与主图联动)
  • 相关性散点图(支持筛选时间段)
  • 实时统计面板(当前价、涨跌幅、RSI)

7.2 代码骨架:用 ColumnDataSource 统一数据流

# 1. 数据加载与预处理(一次) df = pd.read_csv('stocks.csv') df['date'] = pd.to_datetime(df['date']).dt.tz_localize('UTC') df['ma20'] = df['close'].rolling(20).mean() df['volume_ma'] = df['volume'].rolling(10).mean() # 2. 构建统一数据源 source = ColumnDataSource(df) # 3. 创建主图(价格+均线) p1 = figure(x_axis_type='datetime', width=800, height=400) p1.line('date', 'close', source=source, color='blue', legend_label='Close') p1.line('date', 'ma20', source=source, color='red', legend_label='MA20') # 4. 创建成交量图(共享 x 轴) p2 = figure(x_axis_type='datetime', x_range=p1.x_range, width=800, height=200) p2.vbar('date', 0.5, 'volume', source=source, color='gray', alpha=0.5) # 5. 创建散点图(支持时间筛选) def make_scatter(start_date, end_date): mask = (df['date'] >= start_date) & (df['date'] <= end_date) scatter_source = ColumnDataSource(df[mask]) p = figure(width=500, height=400) p.circle('volume', 'close', source=scatter_source, size=5, alpha=0.7) return p # 6. 添加交互工具 p1.add_tools(HoverTool(tooltips=[("Date", "@date{%F}"), ("Close", "@close{$0,0.00}")])) p1.add_tools(BoxSelectTool()) # 框选区域,触发散点图更新

这个骨架体现了 Bokeh 的核心思想:数据是中心,视图是投影,交互是连接线。所有图表都指向source,修改source.data,所有图同步更新;添加BoxSelectTool,用户框选主图区域,即可提取对应数据生成新视图。

7.3 部署上线:bokeh serve的生产级配置

本地开发用show(),生产必须用bokeh serve。创建app.py

from bokeh.io import curdoc from bokeh.layouts import column from bokeh.plotting import figure # 复制上面的图表创建代码 # ... # 将所有图表放入布局 layout = column(p1, p2, make_scatter('2023-01-01', '2023-12-31')) curdoc().add_root(layout) curdoc().title = "Stock Dashboard"

启动命令:

bokeh serve app.py --port 8080 --allow-websocket-origin=localhost:8080 --num-procs 4
  • --port:指定端口
  • --allow-websocket-origin:允许跨域(前端页面可嵌入)
  • --num-procs:多进程,提升并发能力

访问 `http://localhost:8

http://www.jsqmd.com/news/1134291/

相关文章:

  • AI大模型职业指南:从核心岗位到实战技能,助你成功转型
  • WebUnity迁移:前端协作范式与契约驱动工程化
  • Django救场实战:18天交付企业级库存系统
  • Stop Asking for Time:技术债治理的工程化操作系统
  • WarcraftHelper终极指南:解决魔兽争霸3在现代Windows系统下的完整兼容性方案
  • AWS S3 Presigned Post字段顺序导致InvalidAccessKeyId错误解析
  • ISO 15739 Visual Noise 实战:3步量化人眼感知噪声,对比SNR差异
  • GTA5线上小助手:3分钟解锁你的洛圣都终极游戏体验
  • 深度解析高危钓鱼链:从攻击原理到企业纵深防御实战
  • Antigravity:Agent驱动的浏览器原生AI开发范式
  • 基于SpringBoot的国密群签名区块链教学模拟系统(含源码、文档与可运行工程)
  • 2009开源桥笔记:一份协作人类学的活体切片
  • 基于Vue2和ElementUI开发的航空订票系统前端工程包,含完整页面与构建配置
  • STM32与FRAM实现嵌入式数据持久化存储方案
  • 魔兽争霸III终极优化指南:免费解锁宽屏高帧率完整方案
  • R语言数据子集化:原理、陷阱与高性能实践
  • Bokeh交互可视化实战:从环境配置到生产级仪表盘
  • Claude Code:面向工程落地的开发者操作系统
  • Plone高并发扩展实战:ZODB锁、Zope进程与缓存分层优化
  • Excel COUNTBLANK函数深度解析:识别真空白与数据质量断点
  • Web安全面试核心:从SQL注入到内网渗透的攻防原理与实战
  • 5分钟快速上手:XUnity.AutoTranslator游戏翻译完整指南
  • R语言生存分析实战:从Kaplan-Meier到Cox模型
  • WebUnity:设计系统即代码的工程化落地实践
  • 数据库原子性原理与工程实践:从WAL日志到分布式事务
  • 高可用、负载均衡与故障转移的三层协同设计
  • 如何利用爆款评判核心参考维度进行选品
  • Plone内容管理系统入门与高校数字化迁移实践
  • R语言数据子集化:性能、内存与方法选择实战指南
  • MongoDB博客数据建模实战:嵌入、引用与混合策略