1 # Copyright (c) 2024 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Module holding AvgStdevStats class."""
21 @dataclasses.dataclass
23 """Class for statistics which include average and stdev of a group.
25 Contrary to other stats types, adding values to the group
26 is computationally light without any caching.
28 Instances are only statistics, the data itself is stored elsewhere.
32 """Number of scalar values (samples) participating in this group."""
34 """Population average of the participating sample values."""
36 """Population standard deviation of the sample values."""
41 runs: typing.Iterable[typing.Union[float, "AvgStdevStats"]],
43 """Return new stats instance describing the sequence of runs.
45 If you want to append data to existing stats object,
46 you can simply use the stats object as the first run.
48 Instead of a verb, "for" is used to start this method name,
49 to signify the result contains less information than the input data.
51 Here, run is a hypothetical abstract class, an union of float and cls.
52 Defining that as a real abstract class in Python is too much hassle.
54 :param runs: Sequence of data to describe by the new metadata.
55 :type runs: Iterable[Union[float, cls]]
56 :returns: The new stats instance.
59 # Using Welford method to be more resistant to rounding errors.
60 # Adapted from code for sample standard deviation at:
61 # https://www.johndcook.com/blog/standard_deviation/
62 # The logic of plus operator is taken from
63 # https://www.johndcook.com/blog/skewness_kurtosis/
68 if isinstance(run, (float, int)):
78 old_total_size = total_size
79 delta = run_avg - total_avg
80 total_size += run_size
81 total_avg += delta * run_size / total_size
82 moment_2 += run_stdev * run_stdev * run_size
83 moment_2 += delta * delta * old_total_size * run_size / total_size
85 # Avoid division by zero.
87 # TODO: Is it worth tracking moment_2 instead, and compute and cache
88 # stdev on demand, just to possibly save some sqrt calls?
89 total_stdev = math.sqrt(moment_2 / total_size)
90 ret_obj = cls(size=total_size, avg=total_avg, stdev=total_stdev)