double のストリームの平均を計算したい。これは、double と int を格納するだけの単純なタスクです。私はApache CommonsのSummaryStatisticsクラスを使用してこれを行っていました。ただし、テスト中に、SummaryStatistics 平均値に浮動小数点エラーがあり、自分の Python 実装にはなかったことがわかりました。さらに調べてみると、コモンズが次のアルゴリズムのバージョンを使用していることがわかりました。
static double incMean(double[] data) {
double mean = 0;
int number = 0;
for (double val : data) {
++number;
mean += (val - mean) / number;
}
return mean;
}
これにより、小さな浮動小数点エラーが発生することがあります。
System.out.println(incMean(new double[] { 10, 9, 14, 11, 8, 12, 7, 13 }));
// Prints 10.500000000000002
これは、guava ユーティリティ DoubleMath.mean で使用される平均アルゴリズムでもあります。どちらも単純なアルゴリズムではなく、上記のアルゴリズムを使用しているのは奇妙に思えます。
static double cumMean(double[] data) {
double sum = 0;
int number = 0;
for (double val : data) {
++number;
sum += val;
}
return sum / number;
}
System.out.println(cumMean(new double[] { 10, 9, 14, 11, 8, 12, 7, 13 }));
// Prints 10.5
前者のアルゴリズムが好まれる理由として、2 つの理由が考えられます。1 つは、ストリーミング中に平均を何度もクエリする場合、除算を行うよりも値をコピーするだけでよい場合があることです。 、私は実際に違いを測定していません)。
他の説明は、前者がオーバーフローの問題を防ぐということです。これは実際には浮動小数点数には当てはまらないようで、せいぜいこれは平均値の低下につながるはずです。このエラーが発生した場合、結果を BigDecimal クラスで行われた同じ cumMean と比較できるはずです。その結果、次の関数が得られます。
public static double accurateMean(double[] data) {
BigDecimal sum = new BigDecimal(0);
int num = 0;
for (double d : data) {
sum = sum.add(new BigDecimal(d));
++num;
}
return sum.divide(new BigDecimal(num)).doubleValue();
}
これは、私たちが得ることができる最も正確な平均であるはずです。次のコードのいくつかの逸話的な実行から、平均値と最も正確な値の間に大きな違いはないようです。逸話的に、それらは桁の正確な平均とは異なる傾向があり、どちらも常に他方より近いとは限りません。
Random rand = new Random();
double[] data = new double[1 << 29];
for (int i = 0; i < data.length; ++i)
data[i] = rand.nextDouble();
System.out.println(accurateMean(data)); // 0.4999884843826727
System.out.println(incMean(data)); // 0.49998848438246
System.out.println(cumMean(data)); // 0.4999884843827622
apache commons と guava の両方が後者ではなく前者の方法を選択した理由について、正当な理由がある人はいますか?
編集:私の質問に対する答えは明らかです。答えは、Knuth が Art of Programming Vol II 4.2.2 (15) でそれを提案したことです (guava ソースを見るためのヒントを提供してくれた Louis Wasserman に感謝します)。ただし、本の中で、Knuth は、標準偏差のロバストな計算をブートストラップするために平均を計算するこの方法を提案していますが、必ずしもこれが最適な平均計算であるとは言いません。この章をさらに読んだことに基づいて、4番目の手段を実装しました。
static double kahanMean(double[] data) {
double sum = 0, c = 0;
int num = 0;
for (double d : data) {
++num;
double y = d - c;
double t = sum + y;
c = (t - sum) - y;
sum = t;
}
return sum / num;
}
上記と同じテストを実行すると (ほんの数回、統計的に有意なものは何もありません)、BigDecimal の実装とまったく同じ結果が得られます。knuth 平均更新は、より複雑な合計法を使用するよりも高速であると想像できますが、経験的に、より複雑な方法は平均の推定においてより正確であるように思われます。より高速である可能性が高い以外に、knuth メソッドを使用する他の理由はありますか?