使用 Matplotlib 进行交互式散点图突出显示和删除_数据可视化

Matplotlib和散点图

Matplotlib是一个用于绘制数据可视化图形的Python库。学习Matplotlib是探索数据可视化领域的重要一步。

散点图是指在回归分析中,数据点在直角坐标系平面上的分布图,散点图表示因变量随 自变量而变化的大致趋势,据此可以选择合适的函数对数据点进行拟合。

交互式散点图,指用户可以通过多种方式与之交互。这包括能够批量选择和突出显示组中的单个散点,以及对高亮显示的点执行操作(例如删除、复制、移动)等功能。

Highlighter Class

首先,在单独的文件中创建 Highlighter 类。将逐一讨论每种方法的创建。

import pandas as pd
import numpy as np
from matplotlib.widgets import RectangleSelector
import keyboard

class Highlighter(object):
    def __init__(self, canvas, ax, x, y):
        self.canvas = canvas
        self.ax = ax
        self.x, self.y = x, y
        self.mask = np.zeros(x.shape, dtype=bool)

        self._highlight = self.ax.scatter([], [], s=50, color='yellow', zorder=10)
        self.selector = RectangleSelector(self.ax, self.select, useblit=True, state_modifier_keys={"center": "alt"})

Highlighter 类的核心部分是 Rectangle Selector,它是一个 matplotlib 功能,用于创建拖动工具。这仅在为拖动工具创建 UI 时有用,但是,实际的选择、突出显示和其他功能将来自我们。

使用 Matplotlib 进行交互式散点图突出显示和删除_散点图_02

此函数的另一个重要部分是self.mask属性,它将是一个 0 和 1 值的列表,其长度与散点图中的点数相同。这将跟踪我们的积分当前是否被选中。此列表的 index-2 处的值为 1 表示我们图中的第三个散点当前突出显示。可以同时选择多个点。

def update(self, x, y):
        self.x, self.y = x, y
        self.mask = np.zeros(x.shape, dtype=bool)

接下来是update函数,每当我们想用新值更新散点图(或删除一些值)时,都会调用该函数。我们更新 highlighter 类中的 x 和 y 值,并将蒙版的大小调整为散点图中的新点数。请注意,这不会影响实际的散点图,其代码将位于主应用程序中的其他位置。

def select(self, event1, event2):
        prevOffsets = []
        prevMask = None

        if keyboard.is_pressed('ctrl'):
            prevOffsets = self._highlight.get_offsets()
            prevMask = self.mask

        self.clear_highlights()
        self.mask |= self.inside(event1, event2)
        xy = np.column_stack([self.x[self.mask], self.y[self.mask]])

        if len(prevOffsets) > 0:
            xy = np.concatenate((xy, prevOffsets))
            self.mask |= prevMask

        if len(xy):
            self._highlight.set_offsets(xy)
            self.canvas.draw_idle()

最重要的功能是select方法,每次使用矩形选择器创建的拖动工具时,矩形选择器都会调用该方法。之所以调用它,是因为我们将Highlighter类的select函数作为第二个参数传递给了矩形选择器的构造函数。

此函数执行以下操作:

1.跟踪以前突出显示的值,以便在按下CTRL键时可以进行多阶段拖动选择。

2.调用inside方法,该方法返回一个布尔值列表,表示哪些点在所选区域内,哪些点不在。掩码属性被分配了这些值。inside方法使用两个事件计算此列表,第一个事件是拖动的开始位置(按住鼠标的位置)和结束位置(释放鼠标的地方)。

def inside(self, event1, event2):
        """Returns a boolean mask of the points inside the rectangle defined by
        event1 and event2."""
        x0, x1 = sorted([event1.xdata, event2.xdata])
        y0, y1 = sorted([event1.ydata, event2.ydata])
        mask = ((self.x > x0) & (self.x < x1) &
                (self.y > y0) & (self.y < y1))
        return mask

3.使用掩码过滤x和y值,并将其存储在xy变量中,该变量将成为需要突出显示的点的坐标。最后,调用set_offsets方法,该方法将处理实际的突出显示。

def set_offsets(self, xy):
        self._highlight.remove()
        self._highlight = self.ax.scatter([], [], s=50, color='yellow', zorder=10)

        if len(xy):
            self._highlight.set_offsets(xy)
        self.canvas.draw_idle()

这是通过在要选择的点的位置上绘制黄色散点来实现的。或者,您可以修改代码,单独更新选定的散点,并将其颜色更改为黄色。

def clear_highlights(self):
        self._highlight.remove()
        self._highlight = self.ax.scatter([], [], s=50, color='yellow', zorder=10)
        self.mask = np.zeros(self.x.shape, dtype=bool)
        self.canvas.draw_idle()

此类中还使用了clear_highlights方法来清除现有的高光。

highlights类-完整代码

import pandas as pd
import numpy as np
from matplotlib.widgets import RectangleSelector
import keyboard

class Highlighter(object):
    def __init__(self, canvas, ax, x, y, ):
        self.canvas = canvas
        self.ax = ax
        self.x, self.y = x, y
        self.mask = np.zeros(x.shape, dtype=bool)

        self._highlight = self.ax.scatter([], [], s=50, color='yellow', zorder=10)
        self.selector = RectangleSelector(self.ax, self.select, useblit=True, state_modifier_keys={"center": "alt"})

    def update(self, x, y):
        self.x, self.y = x, y
        self.mask = np.zeros(x.shape, dtype=bool)

    def select(self, event1, event2):
        prevOffsets = []
        prevMask = None

        if keyboard.is_pressed('ctrl'):
            prevOffsets = self._highlight.get_offsets()
            prevMask = self.mask

        self.clear_highlights()
        self.mask |= self.inside(event1, event2)
        xy = np.column_stack([self.x[self.mask], self.y[self.mask]])

        if len(prevOffsets) > 0:
            xy = np.concatenate((xy, prevOffsets))
            self.mask |= prevMask

        if len(xy):
            self._highlight.set_offsets(xy)
            self.canvas.draw_idle()
    
    def clear_highlights(self):
        self._highlight.remove()
        self._highlight = self.ax.scatter([], [], s=50, color='yellow', zorder=10)
        self.mask = np.zeros(self.x.shape, dtype=bool)
        self.canvas.draw_idle()

    def set_offsets(self, xy):
        self._highlight.remove()
        self._highlight = self.ax.scatter([], [], s=50, color='yellow', zorder=10)

        if len(xy):
            self._highlight.set_offsets(xy)
        self.canvas.draw_idle()

    def check_inside(self, point1, point2):
        """Returns a boolean mask of the points inside the rectangle defined by
        event1 and event2."""
        x0, x1 = sorted([point1[0], point2[0]])
        y0, y1 = sorted([point1[1], point2[1]])
        mask = ((self.x > x0) & (self.x < x1) &
                (self.y > y0) & (self.y < y1))
        return mask

    def inside(self, event1, event2):
        """Returns a boolean mask of the points inside the rectangle defined by
        event1 and event2."""
        x0, x1 = sorted([event1.xdata, event2.xdata])
        y0, y1 = sorted([event1.ydata, event2.ydata])
        mask = ((self.x > x0) & (self.x < x1) &
                (self.y > y0) & (self.y < y1))
        return mask

GUI 应用程序

这是我们的驱动程序应用程序的代码。我们使用 Tkinter GUI 开发了一个应用程序来包装我们的 Matplotlib 图形。您不需要执行此操作,并且可以相应地修改代码(不会进行一些小的更改)。但是,预计任何需要此类高级功能的图形应用程序也将有一个 GUI 库(因为 matplotlib GUI 是有限的)。

这里唯一需要注意的方法是 delete 方法,它处理所选点的删除。您可以使用 delete 方法作为执行其他操作(如复制、粘贴、移动等)的模板。

import tkinter as tk
from tkinter import ttk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from Highlighter import Highlighter
import pandas as pd
import random 

class MatplotlibApp:
    def __init__(self, master):
        self.master = master
        self.master.title("Matplotlib App")
        self.data = pd.DataFrame(columns=["x", "y"])
        self.points = []
        self.create_widgets()

    def create_widgets(self):
        self.fig = Figure(figsize=(5, 4), dpi=100)
        self.ax = self.fig.add_subplot(111)
        self.ax.set_xlim(-1, 11)
        self.ax.set_ylim(-1, 11)    

        self.canvas = FigureCanvasTkAgg(self.fig, master=self.master)
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
        self.canvas.get_tk_widget().bind("<Delete>", self.delete)
        self.canvas.draw()

        tk.Button(self.master, text="Add Random Point", command=self.add).pack(padx=20, pady=20)
        self.highlighter = Highlighter(self.canvas, self.ax, self.data["x"], self.data["y"])

    def add(self):
        x, y = [random.randint(0, 10), random.randint(0, 10)]
        df = pd.DataFrame([[x, y]], columns=["x", "y"])

        self.data = pd.concat([self.data, df], ignore_index=True)
        self.points.append(self.ax.scatter(x, y, color="blue"))
        self.highlighter.update(self.data["x"], self.data["y"])
        self.canvas.draw()

    def delete(self, event):
        self.selected_regions = self.highlighter.mask
        self.data = self.data[~self.selected_regions].reset_index(drop=True)

        for i, artist in enumerate(self.points):
            if self.selected_regions[i]:
                artist.remove()
        self.points = [artist for artist, m in zip(self.points, self.selected_regions) if m != 1]

        self.highlighter.update(self.data["x"], self.data["y"])
        self.highlighter.clear_highlights()
        self.canvas.draw()

def main():
    root = tk.Tk()
    app = MatplotlibApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()