增量浮点平均值算法的选择(java)

增量浮点平均值算法的选择(java),java,statistics,guava,mean,apache-commons-math,Java,Statistics,Guava,Mean,Apache Commons Math,我想计算双倍流的平均值。这是一个简单的任务,只需要存储一个double和一个int。然而,在测试时,我注意到SummaryStatistics means有浮点错误,而我自己的python实现没有。经过进一步检查,我发现commons正在使用以下算法的一个版本: static double incMean(double[] data) { double mean = 0; int number = 0; for (double val : data) {

我想计算双倍流的平均值。这是一个简单的任务,只需要存储一个double和一个int。然而,在测试时,我注意到SummaryStatistics means有浮点错误,而我自己的python实现没有。经过进一步检查,我发现commons正在使用以下算法的一个版本:

static double incMean(double[] data) {
    double mean = 0;
    int number = 0;
    for (double val : data) {
        ++number;
        mean += (val - mean) / number;
    }
    return 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
这有时会导致小的浮点错误,例如

System.out.println(incMean(new double[] { 10, 9, 14, 11, 8, 12, 7, 13 }));
// Prints 10.500000000000002
这也是guava实用程序DoubleMath.mean使用的平均算法。我觉得奇怪的是,他们都使用上述算法,而不是更简单的算法:

static double incMean(double[] data) {
    double mean = 0;
    int number = 0;
    for (double val : data) {
        ++number;
        mean += (val - mean) / number;
    }
    return 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
有两个原因可以解释为什么人们更喜欢前一种算法。一个是,如果在流式处理过程中多次查询平均值,那么只需复制一个值可能比除法更有效,但更新步骤似乎要慢得多,这几乎总是会超过此成本(请注意,我没有实际计算差异的时间)

另一种解释是,前者可以防止溢出问题。浮点数的情况似乎并非如此,这最多会导致平均值下降。如果是这种错误,我们应该能够将结果与BigDecimal类的相同平均值进行比较。这将产生以下功能:

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

执行与上面相同的测试(几次,没有统计意义),我得到了与BigDecimal实现完全相同的结果。我可以想象,knuth均值更新比使用更复杂的求和方法更快,但从经验上看,更复杂的方法在估计均值时似乎更准确,我天真地认为这也会导致更好的标准偏差更新。除了可能更快之外,还有其他原因使用knuth方法吗?

简短回答:增量更新方法是首选的默认方法,因为它避免了数值错误,并且不会比和除法占用更多的时间/空间

当取大量样本的平均值时,增量更新方法在数值上更稳定。您可以看到,在
incMean
中,所有变量始终是典型数据值的顺序;但是,在求和版本中,变量
sum
的顺序为
N*mean
,由于浮点数学的有限精度,这种比例差异可能会导致问题

float
(16位)的情况下,可以构建人工问题案例:例如,少数稀有样本是
O(10^6)
,其余样本是
O(1)
(或更小),或者通常,如果您有数百万个数据点,那么增量更新将提供更准确的结果

使用
double
s(这就是为什么您的测试用例都给出了几乎相同的结果)这些有问题的情况不太可能发生,但是对于具有较大值分布的非常大的数据集,同样的数值问题可能会突然出现,因此使用增量方法获取平均值是一种普遍接受的良好做法(还有其他时刻!)

这种方法的优点是:

  • 只有一个分区操作(增量方法需要
    N
    分区)

  • 时髦的、几乎是循环的数学是一种技术,可以减少强力求和中出现的浮点错误;可以将变量
    c
    视为应用于下一次迭代的“修正”


  • 但是,编写(和阅读)增量方法更容易。

    您提供的“朴素算法”从不计算平均值。我假设您错误地忽略了这一点,但这使问题无法回答。我认为您的意思是使用
    mean=sum/number
    ?而且我不会说一个比另一个慢,它们都是O(n).可能在实践中,但理论上它们都应该在线性时间内运行。感谢排错。我不是指渐近时间,我指的是在某些使用场景下的操作数,但你是对的。我不认为在任何情况下都会有显著的时间差。我知道将朴素公式扩展到计算机ng标准差是一个,但是,我不知道这是否也适用于计算平均值。关于速度,两者显然都是线性的,但这不会阻止我说朴素的公式快得多。番石榴代码有一个。你读过吗?另见