如何在Python中基于DataFrame动态更新条形图
动态更新DataFrame条形图的正确实现方案
问题背景
我基于DataFrame d绘制了条形图,需要通过遍历第二个DataFrame new_d的每一行来更新d的数据,进而动态刷新条形图。尝试用FuncAnimation实现但效果不符合预期,求可行的解决方法。
原始数据与代码
原始DataFrame d
Position Operation Side Price Size 1 9 0 1 0.7289 -19 2 8 0 1 0.729 -427 3 7 0 1 0.7291 -267 4 6 0 1 0.7292 -18 5 5 0 1 0.7293 -16 6 4 0 1 0.7294 -16 7 3 0 1 0.7295 -429 8 2 0 1 0.7296 -23 9 1 0 1 0.7297 -31 10 0 0 0 0.7299 41 11 1 0 0 0.73 9 12 2 0 0 0.7301 10 13 3 0 0 0.7302 18 14 4 0 0 0.7303 16 15 5 0 0 0.7304 18 16 6 0 0 0.7305 429 17 7 0 0 0.7306 16 18 8 0 0 0.7307 268 19 9 0 0 0.7308 18
初始绘图代码
import matplotlib.pyplot as plt import seaborn as sns import matplotlib.ticker as ticker import pandas as pd from matplotlib.animation import FuncAnimation f, ax = plt.subplots() sns.set_color_codes('muted') sns.barplot(data = d[d.Side==0], x = 'Size', y = 'Price', color = 'b', orient = 'h', native_scale=True) sns.barplot(data = d[d.Side==1], x = 'Size', y = 'Price', color = 'r', orient = 'h', native_scale=True) ax.yaxis.set_major_locator(ticker.MultipleLocator(.0001)) sns.despine()
待遍历的更新数据new_d
Position Operation Side Price Size 34 0 1 0 0.7299 39 35 1 1 0 0.73 9 36 3 1 0 0.7302 18 37 0 1 1 0.7298 -8 38 0 1 1 0.7298 -9 39 0 1 1 0.7298 -8 40 0 1 1 0.7298 -9 41 0 1 1 0.7298 -14 42 0 1 1 0.7298 -9 43 0 2 1 0.0 0 44 9 0 1 0.7288 -17 45 0 1 1 0.7297 -29 46 1 1 1 0.7296 -23 47 9 2 1 0.0 0 48 0 0 1 0.7298 -3 49 1 1 1 0.7297 -31 50 0 1 1 0.7298 -10 51 0 1 0 0.7299 41 52 0 1 1 0.7298 -4 53 0 2 1 0.0 0 54 9 0 1 0.7288 -17 55 9 2 0 0.0 0 56 0 0 0 0.7298 2 57 0 1 0 0.7298 4 58 1 1 0 0.7299 39 59 0 1 0 0.7298 5
尝试的FuncAnimation代码(存在问题)
def init(): # f, ax = plt.subplots() sns.set_color_codes('muted') sns.barplot(data = d[d.Side==0], x = 'Size', y = 'Price', color = 'b', orient = 'h', native_scale=True) s = sns.barplot(data = d[d.Side==1], x = 'Size', y = 'Price', color = 'r', orient = 'h', native_scale=True) sns.despine() return s def update_d(row): if row.Operation == 1: d.loc[((d.Position==row.Position) & (d.Side==row.Side)), 'Size'] = row.Size elif row.Operation == 2: idx = d.loc[((d.Position==row.Position) & (d.Side==row.Side))].index d.drop(idx, inplace=True) elif row.Operation == 0: d = pd.concat([pd.DataFrame([[row.Time, row.Symbol, row.Position, row.Operation, row.Side, row.Price, row.Size]], columns=d.columns), d], ignore_index=True) d['Position'] = d.groupby('Side')['Price'].rank().astype('int').sub(1) d.sort_values('Price', inplace=True) sns.barplot(data = d[d.Side==0], x = 'Size', y = 'Price', color = 'b', orient = 'h', native_scale=True) s = sns.barplot(data = d[d.Side==1], x = 'Size', y = 'Price', color = 'r', orient = 'h', native_scale=True) return s f, ax = plt.subplots() ax.yaxis.set_major_locator(ticker.MultipleLocator(.0001)) ani = FuncAnimation(f, update_d, init_func=init, frames=new_d[20:].iterrows(), interval = 100) plt.show()
问题分析
- 图层叠加:每次调用
sns.barplot都会在原有图层上新增条形,导致画面混乱。 - DataFrame作用域:
update_d函数中Operation=0分支重新赋值了d,但这是局部变量,没有修改全局的d。 - 动画更新逻辑错误:
FuncAnimation需要更新现有绘图元素的属性,而非重新绘制整个图表;同时init和update函数返回的对象不符合要求,无法触发正确的重绘。
正确实现方案
核心思路是:初始化时保留条形图的patches对象,每次更新时先修改d的数据,再更新patches的宽度(水平条形图的长度对应x轴数值),最后调整轴范围。
import matplotlib.pyplot as plt import seaborn as sns import matplotlib.ticker as ticker import pandas as pd from matplotlib.animation import FuncAnimation # 先对原始数据d按Price排序,保证y轴顺序稳定 d.sort_values('Price', inplace=True) # 初始化画布和轴 f, ax = plt.subplots() sns.set_color_codes('muted') ax.yaxis.set_major_locator(ticker.MultipleLocator(.0001)) sns.despine() # 初始绘制两个条形图,保存patches对象 bar0 = sns.barplot(data=d[d.Side==0], x='Size', y='Price', color='b', orient='h', native_scale=True, ax=ax) bar1 = sns.barplot(data=d[d.Side==1], x='Size', y='Price', color='r', orient='h', native_scale=True, ax=ax) # 合并所有patches,方便统一更新 all_patches = list(bar0.patches) + list(bar1.patches) def init(): # 初始化时返回需要更新的元素 return all_patches def update(frame): # frame是new_d的一行,iterrows返回(index, row),取第二个元素 _, row = frame global d # 声明使用全局d # 执行数据更新逻辑 if row.Operation == 1: # 更新现有行的Size mask = (d.Position == row.Position) & (d.Side == row.Side) d.loc[mask, 'Size'] = row.Size elif row.Operation == 2: # 删除指定行 mask = (d.Position == row.Position) & (d.Side == row.Side) d.drop(d[mask].index, inplace=True) elif row.Operation == 0: # 新增行:匹配原始d的列结构 new_row = pd.DataFrame({ 'Position': [row.Position], 'Operation': [row.Operation], 'Side': [row.Side], 'Price': [row.Price], 'Size': [row.Size] }) d = pd.concat([new_row, d], ignore_index=True) # 重新计算Position:按Side分组,Price排序后排名减1 d['Position'] = d.groupby('Side')['Price'].rank(ascending=False).astype(int).sub(1) d.sort_values('Price', inplace=True) # 遍历每个patch,更新宽度 sorted_prices = d['Price'].unique() sorted_prices.sort() for patch in all_patches: # 获取条形中心对应的y轴刻度值(即Price) y_val = patch.get_y() + patch.get_height()/2 tick_idx = int(round(y_val / ax.get_yticklabels()[0].get_position()[1])) price = ax.get_yticks()[tick_idx] # 获取对应Price的Size值 size = d.loc[d['Price'] == price, 'Size'].values[0] # 更新条形宽度 patch.set_width(size) # 调整x轴范围,确保所有条形完整显示 min_size = d['Size'].min() max_size = d['Size'].max() ax.set_xlim(min_size * 1.1, max_size * 1.1) # 返回需要更新的元素 return all_patches # 创建动画,保持原代码的new_d切片范围 ani = FuncAnimation(f, update, init_func=init, frames=new_d[20:].iterrows(), interval=100, blit=True) plt.show()
关键改进点
- 保留绘图元素引用:初始化时保存
barplot返回的对象,获取其patches属性,后续直接更新这些patch的宽度,避免重复绘图。 - 修正DataFrame作用域:用
global声明使用全局的d,确保数据修改生效。 - 轴范围自适应:每次更新后调整x轴范围,保证所有条形都能完整显示。
- 精准匹配数据与绘图元素:通过y轴刻度值关联DataFrame中的Price数据,确保每个条形都对应正确的Size值。
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

