最近有一個程序需要做一些數據分析,遇見一個求平均值的需求。數據序列由傳感器輸出類似如下:[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都已經被過濾掉了。這樣再求平均值就會准確一些。
在上面的這個算法中,鄰域的個數,和獲取的方式都可以變的,並不是固定的方式,大家可以選擇不同的閾值或者鄰域獲取方式,鄰域的個數也不是越多也好,看測試結果而定。
算法比較簡單,給大家提供了一個過濾異常數據的思路,大家可以嘗試其他一些算法,了解各種算法的優劣和適用場景,在實際項目中使用。