基本概念
-
移動平均值:
一個移動平均值計算常常用來在事件序列資料中消除短期波動,展示長期的趨勢。 移動平均值的平滑效果通過在計算中考慮到歷史值來實現。計算一個移動平均值可以通過少量的狀態來進行,對於一個事件序列,我們只需要記錄上次發生的時間和上次計算出來的評價值即可。
diff = currentTime-lastEventTime
currentAverage = (1.0-alpha)*diff+alpha*lastAverage
複製程式碼
上述計算中的alpha的值是一個0~1之間的常量,aplha值決定了一段時間內的平滑水平,alpha越趨於1,歷史值對當前的平均值的影響越大,反之亦然
-
滑動視窗
- 某些情況下,我們需要降低歷史值對當前移動平均值的影響,例如當兩次事件之間的間隔時間較長時,需要重置平滑作用。如果有一個較小的alpha值,可能不需要這麼做,因為平滑效果已經很好。但是,如果aplha值很大時,需要適當地降低平滑效果的影響.
- 考慮下面的例子。 我們有一個事件(比如說網路錯誤) 很少發生。偶爾出現小的峰值,通常是設什麼問 題的。所以我們]想平滑這些小的峰值。只有當連續的峰值州現時,我們才需要發出通知。 如果事件平均一週才發生一次(達不到通知的閾值),但是某一天一小時內出現了多 個峰值(超過了通知閾值),alpha 值較大的平滑效果可能抵消了峰值,導致事件一直無法 觸發。 為了中和這種影響,我們可以在計算移動平均值時引人滑動視窗的概念。因為我們已 經保留了上一個事件的時間戳以及當前的平均值,實現一個滑動視窗非常簡單,如下面偽 程式碼所示:
f(cur rent Time last BventT ime) > s1idingWindowInterval
currentAverage = 0
end if ....
複製程式碼
一個完整的例項程式碼如下
import java.io.Serializable;
public class EWMA implements Serializable {
private static final long serialVersionUID = -6408346318181111576L;
// 和UNIX系統計算負載時使用的標準alpha值相同
public static final double ONE_MINUTE_ALPHA = 1-Math.exp(-5d / 60d / 1d);
public static final double FIVE_MINUTE_ALPHA = 1-Math.exp(-5d / 60d / 5d);
public static final double FIFTEEN_MINUTE_ALPHA = 1-Math.exp(-5d / 60d / 15d);
public static enum Time {
MILLISECONDS(1),
SECONDS(1000),
MINUTES(SECONDS.getMillis() * 60),
HOURS(MINUTES.getMillis() * 60),
DAYS(HOURS.getMillis() * 24),
WEEKS(DAYS.getMillis() * 7);
private long millis;
Time(long millis) {
this.millis = millis;
}
public long getMillis() {
return millis;
}
}
private long window; //滑動視窗大小
private long alphaWindow;
private long last; //記錄上一次的時間
private double average; //移動平均值
private double alpha = -1D; //平滑水平
private boolean sliding = false; //是否移動
public EWMA() {
}
/**
* 建立指定時間的滑動視窗
*/
public EWMA sliding(double count,Time time){
return this.sliding((long)(time.getMillis()*count));
}
private EWMA sliding(long window){
this.sliding = true;
this.window = window;
return this;
}
/**
* 指定alpha值
* @param alpha
* @return
*/
public EWMA withAlpha(double alpha){
if(!(alpha>0.0D)&&alpha<=1.0D){
throw new IllegalArgumentException("Alpha must be between 0.0 and 1.0");
}
this.alpha = alpha;
return this;
}
/**
* 作為一個alphaWindow視窗的函式
* alpha = 【1-Math.exp(-5d / 60d / alphaWindow)】
* @param alphaWindow
* @return
*/
public EWMA withAlphaWindow(long alphaWindow){
this.alpha = -1;
this.alphaWindow = alphaWindow;
return this;
}
public EWMA withAlphaWindow(double count,Time time){
return this.withAlphaWindow((long)(time.getMillis()*count));
}
/**
* 預設使用當前時間更新移動平均值
*/
public void mark(){
mark(System.currentTimeMillis());
}
/**
* 更新移動平均值
* @param time
*/
public synchronized void mark(long time){
if(this.sliding){
//如果發生時間間隔大於視窗,則重置滑動視窗
if(time-this.last > this.window){
this.last = 0;
}
}
if(this.last == 0){
this.average = 0;
this.last = time;
}
// 計算上一次和本次的時間差
long diff = time-this.last;
// 計算alpha
double alpha = this.alpha != -1.0 ? this.alpha : Math.exp(-1.0*((double)diff/this.alphaWindow));
// 計算當前平均值
this.average = (1.0-alpha)*diff + alpha*this.average;
this.last = time;
}
/**
* mark()方法多次呼叫的平均間隔時間(歷史平均水平)
* @return
*/
public double getAverage(){
return this.average;
}
/**
* 按照指定的時間返回平均值
* @param time
* @return
*/
public double getAverageIn(Time time){
return this.average == 0.0?this.average:this.average/time.getMillis();
}
/**
* 返回特定時間度量內呼叫mark()的頻率
* @param time
* @return
*/
public double getAverageRatePer(Time time){
return this.average == 0.0?this.average:time.getMillis()/this.average;
}
}
複製程式碼
使用例項
//指定一個1分鐘的滑動視窗
EWMA ewma = new EWMA().sliding(1.0, EWMA.Time.MINUTES).withAlpha(EWMA.ONE_MINUTE_ALPHA);
複製程式碼