Ich möchte den Mittelwert eines Doppelstroms berechnen. Dies ist eine einfache Aufgabe, die nur das Speichern eines double und eines int erfordert. Ich habe dies mit der Apache Commons SummaryStatistics Klasse getan. Beim Testen habe ich jedoch gemerkt, dass die ZusammenfassungStatistiken Gleitkommafehler hatten, die meine eigene Python-Implementierung nicht hatte. Bei einer weiteren Untersuchung fand ich heraus, dass Gemeingut werden mit einer Version des folgenden Algorithmus:Wahl des Algorithmus für inkrementelle Gleitkomma-Mittelwert (Java)
static double incMean(double[] data) {
double mean = 0;
int number = 0;
for (double val : data) {
++number;
mean += (val - mean)/number;
}
return mean;
}
Dies führt manchmal zu kleinen Fließkommafehlern z.B.
System.out.println(incMean(new double[] { 10, 9, 14, 11, 8, 12, 7, 13 }));
// Prints 10.500000000000002
Dies ist auch der mittlere Algorithmus, der vom Guava-Dienstprogramm DoubleMath.mean verwendet wird. Es scheint mir seltsam, dass sie beide den obigen Algorithmus verwenden anstelle dem naiven Algorithmus:
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
Es gibt zwei Gründe, warum ich für von begreifen kann, warum man könnte den früheren Algorithmus bevorzugen. Eine davon ist, dass wenn man den Mittelwert während des Streamings abfragt, es effizienter ist, nur einen Wert zu kopieren, als eine Division durchzuführen, außer es scheint, dass der Aktualisierungsschritt signifikant langsamer ist, was diese Kosten fast immer überwiegen würde (Anmerkung Ich habe den Unterschied nicht wirklich gemessen.
Die andere Erklärung ist, dass ersteres Überlaufprobleme verhindert. Bei Gleitkommazahlen scheint dies nicht der Fall zu sein, dies sollte allenfalls zu einer Verschlechterung des Mittelwerts führen. Wenn dieser Fehler der Fall ist, sollten wir in der Lage sein, die Ergebnisse mit demselben cumMean zu vergleichen, der mit der BigDecimal-Klasse durchgeführt wurde. Daraus ergibt sich die folgende Funktion:
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();
}
Dies sollte vernünftigerweise das genaueste Mittel sein, das wir bekommen konnten. Von einigen anekdotischen Läufen des folgenden Codes scheint es keinen signifikanten Unterschied zwischen entweder dem gemeinen und dem genauesten zu geben. Anekdotisch neigen sie dazu, sich von dem genauen Mittelwert an der Ziffer zu unterscheiden, und keiner ist immer näher als der andere.
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
Hat jemand eine Begründung, warum beide Apache Commons und Guave die frühere Methode anstelle des letzteren entschieden?
Edit: Die Antwort auf meine Frage scheint klar zu sein, die Antwort ist, dass Knuth es in der Kunst der Programmierung Bd. II vorgeschlagen hat. 4.2.2 (15) (Dank Louis Wasserman für den Tipp auf die Guava-Quelle zu sehen). In dem Buch schlägt Knuth diese Methode vor, um das Mittel zu berechnen, um eine robuste Berechnung der Standardabweichung zu starten, nicht unbedingt, dass dies die optimale Durchschnittsberechnung ist. Basierend auf mehr des Kapitels lesen ich ein viertes Mittel umgesetzt:
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;
}
Durchführen der gleichen Tests wie oben (ein paar Mal, nichts statistisch signifikant), erhalte ich genau das gleiche Ergebnis wie die BigDecimal Umsetzung. Ich kann mir vorstellen, dass die knuth mittlere Aktualisierung schneller ist als die Verwendung der komplizierteren Summationsmethode, aber die kompliziertere Methode scheint empirisch genauer bei der Schätzung des Mittelwerts zu sein, was ich naiverweise erwarten würde, dass auch bessere Standardabweichungsaktualisierungen resultieren. Gibt es einen anderen Grund, die knuth-Methode zu verwenden, als dass sie wahrscheinlich schneller ist?
Der von Ihnen angegebene "naive Algorithmus" berechnet nie einen Mittelwert. Ich gehe davon aus, dass Sie das fälschlicherweise vergessen haben, aber es macht die Frage unwiderlegbar. Ich glaube, du wolltest "gemein = Summe/Nummer" haben? Ich würde auch nicht sagen, dass einer langsamer ist als der andere, beide sind O (n). Vielleicht in der Praxis, aber theoretisch sollten beide in linearer Zeit laufen. – turbo
Danke für den Tippfehler. Ich habe keine asymptotische Zeit angegeben, ich bezog mich auf die Anzahl der Operationen in bestimmten Anwendungsszenarien, aber Sie haben Recht. Ich erwarte keinen signifikanten Zeitunterschied. – Erik
Ich weiß, dass die Erweiterung der naiven Formel auf die Computer-Standard-Variation eine [schlechte Idee] ist (http://www.johndcook.com/standard_deviation.html), aber ich weiß nicht, ob dies auch für das Computing-Mittel gilt. Bezüglich der Geschwindigkeit sind beide offensichtlich linear, aber das würde mich nicht davon abhalten zu sagen, dass die naive Formel viel schneller ist. – maaartinus