如何在Seaborn多网格图中用mplcursors显示完整DataFrame行注释
问题:Seaborn多维度散点图(分面+色调)搭配mplcursors时Tooltip索引错误
问题描述
- 需求:绘制包含分面(col/row)、色调(hue)、x/y轴的多维散点图,实现鼠标悬停显示数据点的完整属性Tooltip
- 异常:添加全部视觉修饰符后,mplcursors返回的
sel.index是子图内的局部索引,而非原DataFrame的全局索引,导致Tooltip显示错误;仅保留部分修饰符(如仅x/y轴)时功能正常
复现代码
import seaborn as sns import matplotlib.pyplot as plt import mplcursors df = sns.load_dataset("tips") sns.relplot(data=df, x="total_bill", y="tip", hue="day", col="time", row="sex") def show_hover_panel(get_text_func=None): cursor = mplcursors.cursor( hover=2, # Transient annotation_kwargs=dict( bbox=dict( boxstyle="square,pad=0.5", facecolor="white", edgecolor="#ddd", linewidth=0.5, ), linespacing=1.5, arrowprops=None, ), highlight=True, highlight_kwargs=dict(linewidth=2), ) if get_text_func: cursor.connect( event="add", func=lambda sel: sel.annotation.set_text(get_text_func(sel.index)), # <- 返回的是子图局部索引,非全局索引 ) return cursor def on_add(index): item = df.iloc[index] parts = [ f"total_bill: {item.total_bill}", f"tip: {item.tip}", f"day: ${item.day}", f"time: ${item.time}", f"sex: ${item.sex}", ] return "\n".join(parts) show_hover_panel(on_add) plt.show()
问题原因
Seaborn的relplot是分面绘图函数,会将原DataFrame按col/row/hue规则拆分为多个子集,每个子图仅绘制对应子集的数据。此时mplcursors返回的sel.index是当前子图内数据点的局部索引,而非原DataFrame的全局索引,直接用df.iloc[index]会取到错误的行数据。
解决方法
核心是获取每个数据点对应的原DataFrame全局索引,而非子图局部索引,以下两种方案可选:
方案1:从绘图对象提取全局索引(高效推荐)
利用relplot返回的FacetGrid对象,获取每个子图散点数据对应的全局索引,绑定到绘图元素上:
import seaborn as sns import matplotlib.pyplot as plt import mplcursors df = sns.load_dataset("tips") # 保存FacetGrid对象,用于获取子图数据 g = sns.relplot(data=df, x="total_bill", y="tip", hue="day", col="time", row="sex") def show_hover_panel(): cursor = mplcursors.cursor( hover=2, annotation_kwargs=dict( bbox=dict( boxstyle="square,pad=0.5", facecolor="white", edgecolor="#ddd", linewidth=0.5, ), linespacing=1.5, arrowprops=None, ), highlight=True, highlight_kwargs=dict(linewidth=2), ) # 遍历所有子图,为散点绑定全局索引 for ax in g.axes.flat: scatter = ax.collections[0] # 获取当前子图数据对应的原DataFrame全局索引 global_indices = scatter.get_offsets()._offsets.index # 将全局索引存储到散点对象的gid属性中 scatter.set_gid(global_indices) def on_add(sel): # 通过子图局部索引获取对应的全局索引 global_index = sel.artist.get_gid()[sel.index] item = df.iloc[global_index] parts = [ f"total_bill: {item.total_bill}", f"tip: {item.tip}", f"day: {item.day}", f"time: {item.time}", f"sex: {item.sex}", ] sel.annotation.set_text("\n".join(parts)) cursor.connect("add", on_add) return cursor show_hover_panel() plt.show()
方案2:坐标匹配全局索引(兼容性强)
为原DataFrame添加全局索引列,通过鼠标悬停点的坐标匹配对应行的全局索引:
import seaborn as sns import matplotlib.pyplot as plt import mplcursors import numpy as np df = sns.load_dataset("tips") # 添加全局索引列,确保每个数据点有唯一标识 df["global_idx"] = df.index g = sns.relplot(data=df, x="total_bill", y="tip", hue="day", col="time", row="sex") def show_hover_panel(): cursor = mplcursors.cursor( hover=2, annotation_kwargs=dict( bbox=dict( boxstyle="square,pad=0.5", facecolor="white", edgecolor="#ddd", linewidth=0.5, ), linespacing=1.5, arrowprops=None, ), highlight=True, highlight_kwargs=dict(linewidth=2), ) def on_add(sel): # 获取悬停点的坐标 x, y = sel.target # 获取当前子图的数据集 ax_data = sel.artist.get_offsets()._offsets # 匹配坐标(考虑浮点精度误差),找到对应行的局部索引 match_mask = (np.isclose(ax_data["total_bill"], x)) & (np.isclose(ax_data["tip"], y)) local_idx = np.where(match_mask)[0][0] # 获取全局索引 global_index = ax_data["global_idx"].iloc[local_idx] item = df.iloc[global_index] parts = [ f"total_bill: {item.total_bill}", f"tip: {item.tip}", f"day: {item.day}", f"time: {item.time}", f"sex: {item.sex}", ] sel.annotation.set_text("\n".join(parts)) cursor.connect("add", on_add) return cursor show_hover_panel() plt.show()
说明
- 方案1直接从绘图对象提取全局索引,效率更高,适合大多数场景
- 方案2通过坐标匹配,适合无法直接从绘图对象获取索引的特殊场景
内容的提问来源于stack exchange,提问作者diyer0001
相关产品推荐
相关产品推荐

