自己實現一個滑動視窗

Meet.相識發表於2018-12-12

基本概念

  1. 移動平均值:

    一個移動平均值計算常常用來在事件序列資料中消除短期波動,展示長期的趨勢。 移動平均值的平滑效果通過在計算中考慮到歷史值來實現。計算一個移動平均值可以通過少量的狀態來進行,對於一個事件序列,我們只需要記錄上次發生的時間和上次計算出來的評價值即可。
      diff = currentTime-lastEventTime
      currentAverage = (1.0-alpha)*diff+alpha*lastAverage
複製程式碼

上述計算中的alpha的值是一個0~1之間的常量,aplha值決定了一段時間內的平滑水平,alpha越趨於1,歷史值對當前的平均值的影響越大,反之亦然

  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);  


複製程式碼

相關文章