Python实现:基于Matplotlib库,可视化鼠标交互删除数据集中的异常点

任务描述:

最近大三暑假,在某公司测试组实习,被布置了一个小任务:

有二维数据集绘散点图如下:

图中红圈部分表示异常点。这些异常点会对数据分析造成影响,所以必须对这些异常点进行删除,删除完毕以后再开展进一步分析工作。

常规删除的方法可以采用DBSCAN、LOF、KNN等聚类方法,但对于特定数据集,采用人工交互删除的方法可以使结果更准确。主要采用Python的Matplotlib库进行实现。

实现思路

1. 根据给定数据集,采用matplotlib.pyplot进行绘图,绘图方法参考资料https://zhuanlan.zhihu.com/p/93423829。(强烈推荐阅读,讲解较为详细。)

2. 利用matplotlib的鼠标响应事件,分别规定鼠标左键按下、鼠标左键按下状态下移动、鼠标左键释放等响应事件。可以设置为(1)鼠标左键按下后触发flag为True。(2)鼠标左键按下状态下(flag为True时)移动时将光标位置坐标保存,从而可以框选数据点。(3)鼠标左键释放后关闭flag为False。从而可以将想要删除的数据点框选,获取此多边形边框的边线点集合。参考资料https://blog.csdn.net/weixin_40400335/article/details/127192570?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-8-127192570-blog-89040722.235^v38^pc_relevant_anti_t3_base&spm=1001.2101.3001.4242.5&utm_relevant_index=11

3. 利用matplotlib.path库,根据得到的边线点集合,构建多边形,将原始数据集每个点遍历判断是否在多边形内(判断方法:参考资料https://blog.csdn.net/u011119817/article/details/119986727)将在多边形内的数据点从数据集中删除。

4. 要求实时展现删除异常点后的当前数据集。得到删除异常点后的数据集后,先使用 ax.cla() 函数将画布清空,再重新绘制当前数据集的散点图,再使用 fig.canvas.draw() 更新画布即可。

5. 要求可以连续框选、删除。采用matplotlib的按钮功能,按钮功能参考资料:https://blog.csdn.net/weixin_40400335/article/details/127192570?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-8-127192570-blog-89040722.235^v38^pc_relevant_anti_t3_base&spm=1001.2101.3001.4242.5&utm_relevant_index=11设置一个clean按钮,每次框选后,点击clean按钮,可以将数据集中选中的部分点删除,同时将多边形边框的边线点集合清空,以便继续进行框选、删除的操作。

6. 为了功能便捷,设置stepback和repaint按钮。分别为撤销一步框选删除操作,以及恢复原始数据集操作。

代码实现:

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from matplotlib.backend_bases import MouseButton
from matplotlib.widgets import Button
import matplotlib.path as mplPath


class removeAnomalyPoints:
    def __init__(self, fig, ax, dataset):
        self.fig = fig
        self.ax = ax
        self.dataset_orig = dataset #原始数据集
        self.dataset_last = dataset #保存上一步的数据集,便于stepback操作
        self.dataset_now = dataset #用于操作的数据集,实时维护
        self.dataset_now_withLabel = []
        self.roi = []
        self.press = False
        self.ax.scatter(dataset[:, 0], dataset[:, 1])
        self.button1_axes = plt.axes([0.5, 0.9, 0.1, 0.075])
        self.button1 = Button(self.button1_axes, 'clean')
        self.button2_axes = plt.axes([0.8, 0.9, 0.1, 0.075])
        self.button2 = Button(self.button2_axes, 'repaint')
        self.button3_axes = plt.axes([0.65, 0.9, 0.1, 0.075])
        self.button3 = Button(self.button3_axes, 'stepback')

    def on_press(self, event):
        if event.inaxes:  # 判断鼠标是否在axes内
            if event.button == MouseButton.LEFT:  # 判断按下的是否为鼠标左键
                # print("Start drawing")
                self.press = True

    def on_move(self,event):
        if event.inaxes:
            if self.press == True:
                x = event.xdata
                y = event.ydata
                self.roi.append([x,y])
                self.ax.plot(x, y, '.', c='r')  # 画点
                self.fig.canvas.draw()  # 更新画布

    def on_release(self,event):
        if self.press == True:
            self.press = False

    def get_dataset_now(self):
        return self.dataset_now

    def get_roi_now(self):
        return self.roi

    def get_deleted_dataset_now(self): #获取删除区域点后的dataset_now
        label_now = np.zeros(len(self.dataset_now)) #label_now长度始终与self.dataset_now保持一致
        self.roi = self.get_roi_now()
        poly_path = mplPath.Path(self.roi)

        for i in range(len(self.dataset_now)):
            if poly_path.contains_point(self.dataset_now[i]):
                label_now[i] = 1
            else:
                label_now[i] = 0

        self.dataset_now_withLabel = np.c_[self.dataset_now, label_now]
        deleted_dataset_now_withLabel = np.delete(self.dataset_now_withLabel, np.where(self.dataset_now_withLabel[:, 2] == 1), axis=0)
        self.dataset_last = self.dataset_now
        self.dataset_now = deleted_dataset_now_withLabel[:,:2]
        return self.dataset_now

    def renew_img(self,event):
        try:
            self.get_deleted_dataset_now() #执行此函数,完成对self.dataset_now的更新
            self.ax.cla()
            self.ax.scatter(self.dataset_now[:, 0], self.dataset_now[:, 1])
            self.roi = []
            self.fig.canvas.draw()  # 更新画布

        except:
            print("Haven't selected area yet!") #异常处理

    def clean_to_orig(self,event):
        self.ax.cla()
        self.dataset_now = self.dataset_orig
        self.dataset_last = self.dataset_orig
        self.ax.scatter(self.dataset_now[:, 0], self.dataset_now[:, 1])
        self.roi = []
        self.fig.canvas.draw()  # 更新画布

    def stepback(self,event):
        self.dataset_now = self.dataset_last
        self.dataset_last = self.dataset_now
        self.ax.cla()
        self.ax.scatter(self.dataset_now[:, 0], self.dataset_now[:, 1])
        self.roi = []
        self.fig.canvas.draw()  # 更新画布
    
    def connect(self):
        self.fig.canvas.mpl_connect("button_press_event", self.on_press)
        self.fig.canvas.mpl_connect("button_release_event", self.on_release)
        self.fig.canvas.mpl_connect("motion_notify_event", self.on_move)
        self.fig.canvas.mpl_connect("key_press_event", self.clean_to_orig)
        self.button1.on_clicked(self.renew_img)
        self.button2.on_clicked(self.clean_to_orig)
        self.button3.on_clicked(self.stepback)

代码简介:

removeAnomalyPoints类中,可以对传入的数据集dataset进行上述处理。

初始化时:

        (1)保存三个数据集,dataset_orig为原始数据集,在整个流程中都不更改;dataset_now为当前数据集(用于操作,删除异常点);dataset_last用于存上一步的数据集,每次dataset_now被修改之前,将dataset_now存入dataset_last。

        (2)self.roi用于存每次框选时的边界点集合。

        (3)原始数据集绘制散点图。

        (4)设置按钮位置、大小,初始化按钮对象。

成员函数:

        (1)on_press,on_move,on_release函数为鼠标触发响应的回调函数,在on_move中对鼠标按下状态下移动选的框保存边界点集合,并进行绘图(散点)。

        (2)get_dataset_now和get_roi_now分别为获取当前类内成员的函数,便于外部调用。

        (3)get_deleted_dataset_now用于接收框选的边界点集合,进行数据集点是否在框内的判断并写入label,根据label删除异常点得到新数据集dataset_now。

        (4)renew_img是clean按钮的回调函数,此函数中执行get_deleted_dataset_now获取最新的数据集,将当前数据集绘制散点图。

        (5)clean_to_orig和stepback分别是repaint和stepback按钮的回调函数,分别执行重置和撤销一步的操作。

        (6)connect定义了各种鼠标、按钮响应事件的触发。

功能展示:

1. 生成数据集:由以下generate_data函数生成规模为1000的二维数据集,其中800个点符合二次函数关系,另外200个点为异常点。

def generate_data():
    # 生成二次函数数据点
    num_quadratic = 800  # 二次函数数据点数量
    x_quadratic = np.linspace(-10, 10, num_quadratic)
    y_quadratic = x_quadratic ** 2 + np.random.randn(num_quadratic) * 10

    # 生成异常点
    num_outliers = 200  # 异常点数量
    x_outliers = np.random.uniform(-10, 10, num_outliers)
    y_outliers = np.random.uniform(-300, 300, num_outliers)

    # 合并数据点
    x_data = np.concatenate((x_quadratic, x_outliers))
    y_data = np.concatenate((y_quadratic, y_outliers))

    # 打乱数据点顺序
    indices = np.random.permutation(x_data.shape[0])
    x_data = x_data[indices]
    y_data = y_data[indices]

    return x_data, y_data

2. 编写如下的主程序:

if __name__ == '__main__':
    ##1.以下部分进行异常点去除
    data_x, data_y = generate_data()  # 生成一个含异常点的二次函数数据集
    data = np.c_[data_x, data_y]
    dataset_orig = np.array(data)  # dataset_orig是n*2, array类
    fig1, ax1 = plt.subplots()
    img = removeAnomalyPoints(fig1, ax1, dataset_orig)
    img.connect()
    plt.show()
    print("The removal of the anomaly has been completed!")

注意:创建removeAnomalyPoints类对象img后,需要调用img.connect()函数,使能通过监听对鼠标键盘的操作做出响应。

3. 效果如下:

        (1)每次框选最好画为闭合多边形框,而且每画一个框就需要点一次clean进行删除。

        (2)stepback只可以回退一步。

        (3)repaint可以将程序恢复至刚运行未做任何操作的状态。

补充说明:

1. 参考链接中判断点在多边形内部有两种python方法,第一种是利用matplotlib.path库相关函数,第二种是利用shapely库相关函数,第一种速度更快。

2. 由于matplotlib.path库生成框选多边形时的限制,删除异常点时,约定需要每次画一个闭合多边形框后就点按钮清除一次。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值