鳥の巣箱

ネトゲしたり、機械いじったり、ソフト書いたり、山篭ったり、ギャンブルしたりする人

平均の計算をループを使わずに処理時間を短縮する

タイトル通りです。
簡単に平均値の計算をする場合、大体はこんな感じの関数を使うことになるでしょう。

function Average(data : array of Double):Double;
var
  I : Integer;
begin
  Result := 0;
  for I := 0 to Length(data) -1 do
    begin
      Result  := Result + data[I];
    end;
  Result := Result / Length(data);
end;

基本的には、データ要素数分のループ処理が必要になります。
ただ、これは要素数が増えたりなんだりで当然計算回数が増えることになるので
数が多くなればなるほど負荷がかかります。
そこで、ループを使わずに平均値を出していきます。

考え方

上記の処理は、式にして考えるとこうなります。
\displaystyle \bar{x}_{n} = \frac{\displaystyle\sum_{n=1}^\infty x_{n}}{n}
nの時点での平均値\bar{x}_{n}x_{1}x_{n}までの総和を、要素数nで割ったもの。
となります。

ここで、n+1の時を考えると、x_{1}x_{n+1}までの総和が必要になりますが、
過去の平均値である\bar{x}がわかっている場合、\bar{x}_{n}×nx_{1}~x_{n}までの総和と等しくなるはずです。

これをふまえると
\displaystyle \bar{x}_{n+1} = \frac{\bar{x}_{n}×n+x_{n+1}}{n+1}
という計算でも平均値を計算することができます。

つまり、要素一つ前の平均値と、新しいデータ、ここまでの要素数がわかれば
ループを使わずとも平均値を求めることができます。
これの利点としては、当然計算回数が削減できるだけでなく、長々と配列を用意する必要もないという点です。

Delphiでの実装

type
  //  ループを使用しない加算平均値算出クラス
  TTscAverage = class(TObject)
    private
      FAverage    : Double;
      FCount      : UInt32;
      function GetAverageData:Double;
    public
      constructor Create;virtual;
      procedure Clear;

      procedure SetData(data:Double);overload;virtual;
      procedure SetData(data:Array of Double);overload;virtual;

      procedure GetAverage(data:Double ; var avg : Double);overload;
      function GetAverage(data:Double):Double;overload;
      property TotalCount : UInt32 read FCount;
      property Average    : Double read GetAverageData;
  end;

implementation

constructor TTscAverage.Create;
begin
  Clear;
end;

procedure TTscAverage.Clear;
begin
  //  変数の初期化
  TspVarInitDouble([@FAverage]);
  TspVarInitUInt16([@FCount]);
end;

procedure TTscAverage.SetData(data:Double);
begin
  FAverage  := ((Faverage * FCount) + data) / (FCount +1);
  Inc(FCount);
end;

procedure TTscAverage.SetData(data:array of Double);
var
  I: Integer;
begin
  for I := 0 to Length(data) -1 do SetData(data[I]);
end;

procedure TTscAverage.GetAverage(data:Double; var avg:Double);
begin
  avg := GetAverage(data);
end;

function TTscAverage.GetAverage(data:Double):Double;
begin
  SetData(data);
  Result    := FAverage;
end;

function TTscAverage.GetAverageData:Double;
begin
  Result  := FAverage;
end;

と、こんな具合です。

移動平均を考える

今まで考えてきたのは単純な加算平均ですが、移動平均でもこの考え方は使えます。
移動平均の場合は、一番古いデータを引く事が必要になるので、データを保持しておく配列は必要になります。
データx(n)で要素数m個の移動平均SMA(x,m)
 \displaystyle SMA(x,m)_{n+1}=\frac{SMA(x,m)_{n}×m+x_{n+1}-x_{n-m}}{m}
となります。

これを前術の平均値算出クラスを継承して書くと

type
  TTscMovingAverage = class(TTscAverage)
    private
      FBuffer : Array of Double;
      FSamples: UInt16;
      procedure SetSamples(samples:UInt16);
    public
      constructor Create(samples : UInt16);reintroduce;
      procedure SetData(data : Double);override;
      property Samples  : UInt16 read FSamples write SetSamples;
  end;

implementation

constructor TTscMovingAverage.Create(samples:UInt16);
begin
  SetSamples(samples);
  inherited Create;
end;

procedure TTscMovingAverage.SetData(data:Double);
begin
  if FCount >= FSamples then
    begin
      FAverage  := ((FAverage * FSamples) +data -FBuffer[FCount mod FSamples]) / FSamples;
    end
  else
    begin
      FAverage  := ((FAverage * FCount) +data) / (FCount+1);
    end;
  FBuffer[FCount mod FSamples]  := data;
  Inc(FCount);
end;

procedure TTscMovingAverage.SetSamples(samples:UInt16);
begin
  SetLength(FBuffer, samples);
  FSamples  := samples;
  Clear;
end;

これで算術平均と移動平均の逐次処理ができるようになります。