Java Streams - 标准差

12

我想要事先澄清的是,我正在寻找使用Streams计算标准偏差的方法(目前有一种工作方法可以计算并返回标准偏差,但不使用Streams)。

我正在处理的数据集与链接中所示非常相似。如此链接所示,我能够对我的数据进行分组并获得平均值,但无法弄清楚如何获取标准偏差。

代码

outPut.stream()
            .collect(Collectors.groupingBy(e -> e.getCar(),
                    Collectors.averagingDouble(e -> (e.getHigh() - e.getLow()))))
            .forEach((car,avgHLDifference) -> System.out.println(car+ "\t" + avgHLDifference));

我还查看了关于DoubleSummaryStatistics的链接,但似乎对SD没有帮助。

2个回答

18

您可以使用自定义的收集器来计算平方和。内置的DoubleSummaryStatistics收集器不会跟踪它。这个问题由专家组在这个线程中讨论过,但最终没有实现。计算平方和时的困难在于,在计算中间结果的平方时可能会溢出。

static class DoubleStatistics extends DoubleSummaryStatistics {

    private double sumOfSquare = 0.0d;
    private double sumOfSquareCompensation; // Low order bits of sum
    private double simpleSumOfSquare; // Used to compute right sum for non-finite inputs

    @Override
    public void accept(double value) {
        super.accept(value);
        double squareValue = value * value;
        simpleSumOfSquare += squareValue;
        sumOfSquareWithCompensation(squareValue);
    }

    public DoubleStatistics combine(DoubleStatistics other) {
        super.combine(other);
        simpleSumOfSquare += other.simpleSumOfSquare;
        sumOfSquareWithCompensation(other.sumOfSquare);
        sumOfSquareWithCompensation(other.sumOfSquareCompensation);
        return this;
    }

    private void sumOfSquareWithCompensation(double value) {
        double tmp = value - sumOfSquareCompensation;
        double velvel = sumOfSquare + tmp; // Little wolf of rounding error
        sumOfSquareCompensation = (velvel - sumOfSquare) - tmp;
        sumOfSquare = velvel;
    }

    public double getSumOfSquare() {
        double tmp =  sumOfSquare + sumOfSquareCompensation;
        if (Double.isNaN(tmp) && Double.isInfinite(simpleSumOfSquare)) {
            return simpleSumOfSquare;
        }
        return tmp;
    }

    public final double getStandardDeviation() {
        return getCount() > 0 ? Math.sqrt((getSumOfSquare() / getCount()) - Math.pow(getAverage(), 2)) : 0.0d;
    }

}

然后,您可以使用这个类

Map<String, Double> standardDeviationMap =
    list.stream()
        .collect(Collectors.groupingBy(
            e -> e.getCar(),
            Collectors.mapping(
                e -> e.getHigh() - e.getLow(),
                Collector.of(
                    DoubleStatistics::new,
                    DoubleStatistics::accept,
                    DoubleStatistics::combine,
                    d -> d.getStandardDeviation()
                )
            )
        ));

这将把输入列表收集到一个映射中,其中值对应于相同键的 high-low 的标准差。


非常感谢。我已经成功获取了SD。现在我正在检查是否可以在同一个stream()调用中收集averagingDouble和SD(例如- car,averageHL,SD),而不是使用2个streams。 - iCoder
1
@iCoder 这个答案中的 DoubleStatistics 收集了标准差和平均值。你可以使用一个 Map<String, DoubleStatistics> 存储所有信息。 - Tunaki
3
关于溢出的有趣事实:关于LongSummaryStatistics实际上会发生溢出导致LongStream.of(Long.MAX_VALUE, Long.MAX_VALUE).summaryStatistics().getAverage()结果为-1.0,然而没有人关心。我认为遇到这种溢出的机率比遇到平方和溢出的机率更高。 - Tagir Valeev
1
@Tunaki 不太确定我犯了什么错误,但是当我将Map<String,Double>更改为Map<String,DoubleStatistics>时,我会收到一个错误消息,在groupingBy中无法解决getScrip()。我想我可能犯了一些初级错误,需要更加深思熟虑。 - iCoder
在Java 1.8.0_92中,该示例使用会抛出错误:类型不匹配:无法将Map<Object,Object>转换为Map<String,Double>。 - simpleuser
显示剩余3条评论

5
您可以使用这个自定义收集器:
private static final Collector<Double, double[], Double> VARIANCE_COLLECTOR = Collector.of( // See https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
        () -> new double[3], // {count, mean, M2}
        (acu, d) -> { // See chapter about Welford's online algorithm and https://math.stackexchange.com/questions/198336/how-to-calculate-standard-deviation-with-streaming-inputs
            acu[0]++; // Count
            double delta = d - acu[1];
            acu[1] += delta / acu[0]; // Mean
            acu[2] += delta * (d - acu[1]); // M2
        },
        (acuA, acuB) -> { // See chapter about "Parallel algorithm" : only called if stream is parallel ...
            double delta = acuB[1] - acuA[1];
            double count = acuA[0] + acuB[0];
            acuA[2] = acuA[2] + acuB[2] + delta * delta * acuA[0] * acuB[0] / count; // M2
            acuA[1] += delta * acuB[0] / count;  // Mean
            acuA[0] = count; // Count
            return acuA;
        },
        acu -> acu[2] / (acu[0] - 1.0), // Var = M2 / (count - 1)
        UNORDERED);

然后只需在您的流上调用此收集器:
double stdDev = Math.sqrt(outPut.stream().boxed().collect(VARIANCE_COLLECTOR));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接