移動平均とスライディングウィンドウ - 「プロになるJava」ボツ原稿

「プロになるJava」ボツ原稿、今回は「13章 処理の難しさの段階」の「他のデータを参照する」のもうひとつの例として、移動平均やスライディングウィンドウの話題です。
13.1.1のあとに入る想定です。

他のデータを参照する

移動平均

隣のデータを参照しないといけない処理としてもうひとつ、 移動平均 を見てみましょう。移動平均は、日々の売り上げや来店者数など、バラツキのあるデータをなめらかにして傾向を見るときによく使われます。
一定区間の要素を取り出して平均をとり、その区間を移動させながら全体のグラフを書いていくので移動平均といいます。

src/main/java/projava/MovingAverage.java

package projava;

import java.util.Arrays;
import java.util.stream.IntStream;

public class MovingAverage {

    public static void main(String[] args) {
        int[] data = {3, 6, 9, 4, 2, 1, 5};
        
        var builder = IntStream.builder();
        for (int i = 0; i < data.length; i++) {
            int sum = Arrays.stream(data, Math.max(0, i - 2), i + 1)
                    .sum();
            builder.add(sum / 3);
        }
        int[] result = builder.build().toArray();
        System.out.println(Arrays.toString(data));
        System.out.println(Arrays.toString(result));
    }
}

結果は次のようになります。

[3, 6, 9, 4, 2, 1, 5]
[1, 3, 6, 6, 5, 2, 2]

ここではIntStream.Builderを使うことでint型の配列を構築しようと思います。
IntStream.Builderを使った配列の構築は次のようになります。まずIntStream.builderメソッドでIntStream.Builderオブジェクトを得ます。

var builder = IntStream.builder();

IntStream.Builderオブジェクトに対してaddメソッドでint型の要素を追加します。

builder.add(data[i]);

要素を追加し終わったら、buildメソッドでIntStreamを得れます。ここではそのままtoArrayメソッドで配列を得ます。

int[] result = builder.build().toArray();

このような、ビルダーオブジェクトをまず用意して要素を構築してからbuildメソッドで目的のオブジェクトを得るというパターンを ビルダーパターン といいます。イミュータブルなオブジェクトを得るときによく使われています。あとで紹介するStringBuilderは文字列を構築するために使います。
「ファイルとネットワーク」の章で紹介したHttpRequestもビルダーを使って構築していました。

HttpRequest req = HttpRequest.newBuilder(uri).build();

さて、移動平均の処理をみていきましょう。 処理を要素数分くり返します。このループはdata.foriと入力して[Tab]キーを押すと補完できます。

for (int i = 0; i < data.length; i++) {

Arrays.streamメソッドは配列からStreamを得るメソッドです。int型の配列を渡すとIntStreamを得れます。また、引数を3つ与えた場合は、最初の引数に配列、2番目の引数に開始位置、3番目の引数に終了位置の次の値を指定します。

int sum = Arrays.stream(data, Math.max(0, i - 2), i + 1)
        .sum();

i番目までをStreamで処理したいので、第3引数にi + 1を渡しています。
第2引数のMath.max(0, i - 2)のようなMath.maxの使い方は、値が0より小さくならないようにするときによく使います。

また、ここで変数sumfor文の処理のブロック内で定義しています。こうすると、この変数sumfor文の処理のブロック内だけで有効な変数になります。変数の有効範囲を スコープ といいます。変数は、定義された中カッコの中でだけ使えると覚えておくといいでしょう。

そうすると、3つの要素の合計を得ることができるので、3で割って平均をとってビルダーに追加します。

builder.add(sum / 3);

処理が終わってビルダーから配列を取り出したら、元の配列と結果の配列をArrays.toStringメソッドを使って整形して表示します。

int[] result = builder.build().toArray();
System.out.println(Arrays.toString(data));
System.out.println(Arrays.toString(result));

移動平均のように、配列などの一部を取り出して処理して、そのあとは取り出す部分をずらして同じ処理を繰り返すような手続きを スライディングウィンドウ といいます。

スライディングウィンドウの処理を素直に実装すると、ウィンドウのサイズに比例した処理時間がかかります。今回はウィンドウのサイズは3件だけなのであまり問題になりませんが、28日分や180日分などウィンドウのサイズが大きくなってくると問題が出てくることがあります。
スライディングウィンドウでは、ウィンドウに入ってくる要素と出ていく要素だけを考えると処理ができることがあります。移動平均の場合は、ウィンドウに入ってくる要素を合計に足して、出ていく要素を合計から引けば、区間の合計を求めることができます。
そのような考えを取り入れたコードは次のようになります。

src/main/java/projava/MovingAverage.java

int sum = 0;
for (int i = 0; i < data.length; i++) {
    sum += data[i];
    if (i >= 3) {
        sum -= data[i - 3];
    }
    builder.add(sum / 3);
}

合計を覚えておく変数sumを用意します。

int sum = 0;

合計に、入ってくる要素を足します。

sum += data[i];

4番目以降の要素を処理するとき、つまり変数iが3以上のときはウィンドウから出ていく要素があるので合計から引きます。

if (i >= 3) {
    sum -= data[i - 3];
}

このようにすると、ウィンドウサイズに関わらず、データ件数だけに比例して処理時間がかかるようになります。