最近有一个程序需要做一些数据分析,遇见一个求平均值的需求。数据序列由传感器输出类似如下:[10,12,11,25,9,10,9,45,13,12,10,11,78,12,12,13,10,9]。在这个序列中很明显的25,45,78都是要远远大于其他一些数据的,而我们认为3个数据应该是异常数据。如果是求平均值,这三个大数会拉高平均值,会让我们的结果有一定的偏差。如果数据序列很大,个别异常数据不太会影响平均值,但是为了使结果更加准确,我们就需要对这些异常数据进行过滤。
通常我们会使用程序判断滤波的方式来过滤异常数据,比如说先对一个序列求平均值,方差等等,然后对每个数据和平均值或方差的偏差,设置一个阈值,差超过这个阈值就认为是异常数据,然后过滤。当然这个阈值只能是经验值,有时候不一定准确,或当异常数据变成常态的时候,异常数据就不再是异常数据,这时候如果还是使用阈值过滤就会有一些问题。当然程序判断滤波的方式还是适用于一些场景,并且实现比较方便。
在做数据统计,分析以及图像处理中,为了防止噪声对数据结果的影响,除了采用更加科学的采样技术外,我们还要采用一些必要的技术手段对原始数据进行整理、统计。数字滤波技术是最基本的处理方法,它可以剔除数据中的噪声,提高数据的代表性。常用的滤波技术有:程序判断滤波,均值滤波,中值滤波,加权平均,滤波,众数滤波,一阶滞后滤波,移动滤波,复合滤波等。
由于上面例子的需求对平均值这个具体的数字不是要求特别准,只是一个大概的数字,所以我们使用中值滤波的原理来处理。均值滤波或其他方式也可以使用,但就这个例子来说,中值滤波原理的效果会比较好一些。
中值滤波的原理,来自百度,比较容易理解:中值滤波是基于排序统计理论的一种能有效抑制噪声的非线性信号处理技术,中值滤波的基本原理是把数字图像或数字序列中一点的值用该点的一个邻域中各点值的中值代替,让周围的像素值接近的真实值,从而消除孤立的噪声点。方法是用某种结构的二维滑动模板,将板内像素按照像素值的大小进行排序,生成单调上升(或下降)的为二维数据序列。二维中值滤波输出为g(x,y)=med{f(x-k,y-l),(k,l∈W)} ,其中,f(x,y),g(x,y)分别为原始图像和处理后图像。W为二维模板,通常为2*2,3*3区域,也可以是不同的的形状,如线状,圆形,十字形,圆环形等。
对于上面的数字序列,我们使用的方法是,对于每个数据,用它周围邻域一定数量的数据的中值替代。如果我们设置邻域的数量为7。那么对于第一个数据10来说,这个邻域数列就是10左边的3个数字和10后边的3个数字,再加上本身,就是7个数字:[13,10,9,10,12,11,25]。因为10是第1个数字,左边3个就要从数组的最后3个去获取。就像下图标识的次序获取。
那对于第一个数字10来说,邻域就是13,10,9,10,12,11,25。对这个新序列进行排序,取中值。排序后的结果是9,10,10,11,12,13,25。中值就是11。那在原始的数列中第一个数字10,就用11来代替。
再一个例子,对于45来说,邻域就是9,10,9,45,13,12,10。如下图:
同样排序后取中值,那么原始队列中的45,就用10代替。这样就过滤了45。
下面直接上java代码:
public static List<Long> getSampleByMedianFilter(List<Long> samples) { //小于三个就不做了 if(samples == null || samples.size() < 3) { return samples; } else { try { //邻域的个数 int medianSampleCount = samples.size() / 2 + 1; List<Long> newSamples = new ArrayList<Long>(); for(int i=0;i<samples.size();i++) { //定义邻域 List<Long> medianSample = new ArrayList<Long>(); int count = medianSampleCount; int step = 1; //先取左边的,再取右边的 boolean left = true; medianSample.add(samples.get(i)); while(count-- > 1) { int index = 0; if(left) { index = i - step; if(index < 0) { index = samples.size() - Math.abs(index); } } else { index = i + step; if (index >= samples.size()) { index = index - samples.size(); } step++; } left = !left; medianSample.add(samples.get(index)); } //排序 Collections.sort(medianSample); //取中值 if(medianSampleCount % 2 == 0) //偶数 { long avg = (medianSample.get(medianSampleCount / 2 - 1) + medianSample.get(medianSampleCount / 2)) / 2; newSamples.add(avg); } else //基数 { newSamples.add(medianSample.get(medianSampleCount / 2)); } } return newSamples; } catch(Exception e) { e.printStackTrace(); return samples; } } }
测试上面的例子:
List<Long> samples = new ArrayList<Long>(); samples.add(Long.valueOf(10)); samples.add(Long.valueOf(12)); samples.add(Long.valueOf(11)); samples.add(Long.valueOf(25)); samples.add(Long.valueOf(9)); samples.add(Long.valueOf(10)); samples.add(Long.valueOf(9)); samples.add(Long.valueOf(45)); samples.add(Long.valueOf(13)); samples.add(Long.valueOf(12)); samples.add(Long.valueOf(10)); samples.add(Long.valueOf(11)); samples.add(Long.valueOf(78)); samples.add(Long.valueOf(12)); samples.add(Long.valueOf(12)); samples.add(Long.valueOf(13)); samples.add(Long.valueOf(10)); samples.add(Long.valueOf(9)); List<Long> newSamples = algorithmManager.getSampleByMedianFilter(samples); for(Long l : newSamples) { System.out.print(l.longValue() + ","); }
结果:
从结果可以看出,异常数据25,45和78都已经被过滤掉了。这样再求平均值就会准确一些。
在上面的这个算法中,邻域的个数,和获取的方式都可以变的,并不是固定的方式,大家可以选择不同的阈值或者邻域获取方式,邻域的个数也不是越多也好,看测试结果而定。
算法比较简单,给大家提供了一个过滤异常数据的思路,大家可以尝试其他一些算法,了解各种算法的优劣和适用场景,在实际项目中使用。