0

I have the following method of calculating the mean and standard deviation of a stream of numbers

import numpy as np

class RunningStatisticsVar:
    def __init__(self, ddof=0):
        self.mean = 0
        self.var = 0
        self.std = 0

        self._n = 0
        self._s = 0
        self._ddof = ddof

    def update(self, values):
        values = np.array(values, ndmin=1)
        n = len(values)

        self._n += n

        old_mean = self.mean
        delta = values - self.mean
        self.mean += (delta / self._n).sum()

        self._s += (delta * (values - self.mean)).sum()
        self.var = self._s / (self._n - self._ddof) if self._n > self._ddof else 0
        self.std = np.sqrt(self.var)

    def update_single(self, value):
        self._n += 1

        old_mean = self.mean
        self.mean += (value - old_mean) / self._n

        self._s += (value - old_mean) * (value - self.mean)
        self.var = self._s / (self._n - self._ddof) if self._n > self._ddof else 0
        self.std = np.sqrt(self.var)

    def __str__(self):
        if self.std:
            return f"(\u03BC \u00B1 \u03C3): {self.mean} \u00B1 {self.std}"
        else:
            return f"{self.mean}"

Since I'd like to use this class in general applications, I want to make it as robust as possible. I've done my best to make the code numerically stable by trying to always place the summations and multiplications (i.e. the operations which can blow up numbers) as late as possible.

I have the below code, which I wrote to test the correctness of my implementation of the batch update in update from the update_single, which I knew to be correct:

samples = np.random.uniform(0, 100, [100000]) #
s1 = RunningStatisticsVar()
s2 = RunningStatisticsVar()

s1.update(samples)
print(s1)
for i in samples:
    s2.update_single(i)
print(s2)

To try and test its numerical stability, I tried using some extremely small and some extremely large numbers by replacing the #-marked line with:

samples = np.hstack((np.random.uniform(0, 1e-20, [100000]), np.random.uniform(1e20, 1e21, [100000]), np.random.uniform(0, 1e-20, [100000])))

This code prints out:

(μ ± σ): 1.8363120959956487e+20 ± 3.000290391225753e+20
(μ ± σ): 1.836312095995585e+20 ± 3.000290391225752e+20

As you can see, the numbers are very much similar, but not quite the same. Is my code numerically stable and is there a better way to test it than what I have above?

Mate de Vita
  • 1,102
  • 12
  • 32
  • 1
    With floating-point calculations, there will almost always be some tiny imperfections when trying to represent numbers with high precision – byxor Oct 31 '19 at 16:59
  • I understand that, my question is more whether the code is (as) numerically stable (as possible) and if there's a better way to test it than the small-big-small input I have above. – Mate de Vita Oct 31 '19 at 17:22

0 Answers0