判断峰值-可排除噪声毛刺

峰值判定算法

前言

对于疫情来说,我们有了每个时刻的现存确诊人数数据(Confirmed),也就是一个一维的数组,记为C(t)。想判断有没二次爆发,那么就需要判断这个序列是不是出现了超过一个峰值。当然啦,这只是这个算法的一个应用而已,这个算法可以用来判断其他一维数组的峰值个数及位置。

算法介绍

由于传染过程会有些波动,现存确诊人数C(t)曲线可能出现毛刺,若直接以C(t)>C(t-1)和C(t)>C(t+1)为条件的话,会出现多判,所以需要对原始的数据进行光滑处理。另外,有些小幅度的感染规模上升,是不能作为峰值的,因此,我们需要增加阈值。
具体操作步骤如下:

  • 1, 归一化/计算密度
    这一步并非求峰值的必须,可以先对数据进行归一化,或者将人口数据转化为人口密度数据。
  • 2, 光滑处理:
    采用均值滤波对原始进行处理。可根据经验设置滤波器窗口大小window_size,对t时刻的数据取其前后各window_size/2个数据(包含自身)求和,并将平均值作为C_filtered(t)的值,C_filtered(t)是光滑处理后的数据。
  • 3, 判断峰值:
    峰值需满足一下两个条件:
    1) C_filtered(t)> C_filtered(t-1)且C_filtered(t)> C_filtered(t+1)
    2) C_filtered(t)>threshold,阈值可以根据情况取,比如说感染人口大于10000。
    另外,需要避免平峰:这里通过峰值宽度peak_width来排除相近的峰值。如果有两个峰值的间隔小于peak_width,则认为他们只有一个有效。
    效果如下图所示。
    窗口大小设置为window size=20,峰值宽度peak width = 10 。(窗口大小在20~40之间均可)

效果

设置参数:

  • window_size = 20
  • threshold = 0.01,即:对于一百万的人口来说,有一万人意思感染才能作为峰值。
  • peak_width = 10

OK,让我们看看效果如何。
判断峰值-可排除噪声毛刺_第1张图片
上图是未处理的数据,横坐标是时间t,纵坐标是感染人口C(t),右边是对左边的局部放大,可以看到曲线并不平滑,如果直接取峰值的话,可能将毛刺当做峰值。我们可以看出,有两个峰值。
下图是光滑处理后的数据,纵坐标是感染密度。判断峰值-可排除噪声毛刺_第2张图片
我用红笔标注了3个位置,我们不希望把3号也当做峰值,因为它只是轻微的起伏,并不能算作爆发。所以需要阈值来排除它,当然啦,仁者见仁智者见智,也许你觉得它是峰值,那么就把阈值调小一些吧。

最后程序是按照上面的说明,程序判断出了两个峰值,t=63, 216这两个位置,也就是图中标注1,2的位置。

源码

Python写的,有一个读取csv文件的代码,不属于本文的主题,主要是读取我存的数据。而且我存的数据格式不太规则,就自己写了。啰嗦一句,我数据保存的格式是,一行一个时间序列,每一行的数据长度不相同。

import numpy as np
import matplotlib.pyplot as plt


def readData(filepath):
    """
    读取csv文件,返回所有的时间序列,该csv文件每一行存储了一个时间序列。
    :param: filepath 文件名字符串
    :return: all_data 大概有几千条时间序列,每个序列是一个一维数组,
    """
    all_data = []

    with open(filepath, encoding='UTF-8') as f:
        sdata = f.readlines()
        all_count = len(sdata)
        for i in range(all_count):
            line = sdata[i]
            odom = line.split(',')[:-1]
            total = len(odom)
            jdata = np.zeros(total)
            for j in range(total):
                jdata[j] = float(odom[j])
            all_data.append(jdata)

    return all_data


def normalData(array):
    """
    归一化处理,或者转化为感染密度
    :param array: 一维数组
    :return: 归一化的一维数组
    """
    array = np.array(array)
    # array_max = array.max()
    # array = array/array_max   # 归一化
    array = array / 1000000  # 转化为感染密度,已知条件,数据最大可能取值是1000000,即总人口为一百万。
    return array


def meanFilter(array, window_size):
    """
    均值滤波,将时间序列进行平滑处理
    :param array:时间序列
    :param window_size:滤波器的窗口大小
    :return: filtered_array:平滑处理后的时间序列
    """
    filtered_array = np.zeros(len(array))
    array_length = len(array)

    for i in range(len(array)):
        # 滤波的左右边界
        if int(i - window_size / 2) > 0:
            left = int(i - window_size / 2)
        else:
            left = 0
        if (i + window_size / 2) < array_length:
            right = int(i + window_size / 2)
        else:
            right = array_length
        filtered_array[i] = np.mean(array[left:right])

    return filtered_array


def findPeak(array, thre, peak_width):
    """
    找出序列中的峰值
    :param array: 已经平滑处理后的时间序列
    :param thre: 阈值
    :param peak_width: 峰的宽度
    :return:turePeakIndex:真正的峰值下标, turePeakCount:真正的峰值个数
    """
    T = len(array)
    peak_index = []

    for t in range(T):
        # 自身比左右两边的值大,并且大于阈值
        if array[t] > array[t - 1] and array[t] > array[t + 1] and array[t] > thre:
            peak_index.append(t)
        # 自身与周围相等,这里考虑平峰的情况
        elif array[t] == array[t - 1] and array[t] > thre:
            peak_index.append(t)

    fakeNum = 0         # 假的峰值个数
    turePeakIndex = []  # 真正的峰值坐标
    turePeakIndex.append(peak_index[0])
    peakNum = len(peak_index)

    for i in range(1, peakNum):
        # 如果与上一个峰值靠得很近,则这个不算峰值。
        if peak_index[i] - peak_index[i - 1] < peak_width:
            fakeNum += 1
        else:
            turePeakIndex.append(peak_index[i])

    turePeakCount = peakNum - fakeNum

    return turePeakIndex, turePeakCount


def main():
    """
    读取数据并找到峰值
    :return:None
    """
    PATH = r"E:\COVID-19\SIMU-DATA\data0714"
    FILENAME = "C.csv"
    file_path = PATH + "\\" + FILENAME

    window_size = 20
    peak_width = 20
    thre = float(10000 / 1000000)

    data = readData(file_path)  # 这里有很多个时间序列
    array = data[229]           # 我取了编号为229的时间序列作为说明,没有特别意思,
    							# 只是这个数据有两个峰值,适合讲解而已。

    normal_array = normalData(array)                                        # 归一化
    filtered_array = meanFilter(normal_array, window_size)                  # 均值滤波
    peak_index, peak_count = findPeak(filtered_array, thre, peak_width)     # 寻找峰值

    print("检查到峰值个数:", peak_count)
    print("峰值出现的时刻:", peak_index)

    plt.subplot(121)
    plt.plot(array)
    plt.subplot(122)
    plt.plot(filtered_array)
    plt.show()


if __name__ == '__main__':
    main()

你可能感兴趣的:(python,学习,算法)