カイ二乗検定でソフトウェアの確率的な振る舞いをテストする

乱数を使った確率的な事象をモデル化したいことはよくあるかもしれない。(自分はしょっちゅうあるよくある。)

例えばおみくじのようなものを以下のように設定したいとする。

  • 大吉 20%
  • 吉 30%
  • 末吉 30%
  • 凶 20%

で,これが正しく実装できたかをテストするときにはよくカイ二乗検定を使っている。 めんどくさいのでおみくじのコードはわざわざ書かないが, drawLottery()がおみくじを引く実装だとして,大吉を0,吉を1, 末吉を2, ...というように表現したとする。 すると以下のような感じで評価できる。

import org.apache.commons.math3.stat.inference.TestUtils;
import org.junit.Assert;
import org.junit.Test;

public class TestStatsUtilsTest {

    @Test
    public void test() {

        // 有意水準(0.1%とする)
        double significanceLevel = 0.01;

        // 期待する発生確率(合計が1)
        double[] expectedProbabilities = new double[] {0.2, 0.3, 0.3, 0.2};

        // 観測結果
        long[] observedCounts = new long[expectedProbabilities.length];

        // 実際にはテストしたい確率的な処理を十分な回数実行して計測する
        for (int i = 0; i < 1000; ++i) {
            var i = drawLottery();
            observedCounts[i]++;
        }

        // カイ二乗検定により、「観測値は、期待する発生確率に従う」という帰無仮説が棄却できないことを確認する
        Assert.assertFalse(
                TestUtils.chiSquareTest(expectedProbabilities, observedCounts, significanceLevel));
    }
}

帰無仮説を棄却できないからといって,帰無仮説が成立することは証明できず, この場合だと,「期待する分布にしたがっていない,とは言えない」であって,「期待する分布にしたがっている」と言えるわけではない。 ただ,帰無仮説が棄却できてしまうのはまぁバグの可能性高いですよね,ということで。

あとCI/CDのパイプラインに組み込む場合は,確率的に失敗するテストが混ざることになるので,その辺は注意が必要そう。 CDのパイプランの実行条件に含める場合は,有意水準を厳しめ(小さめ)に設定しておいたほうがよさそうだけれど,そもそもどうするのがよいんだろうか。