4720c10f3d4978455e8b5f9d2182530aee799666
[csit.git] / resources / libraries / python / jumpavg / AvgStdevStats.py
1 # Copyright (c) 2021 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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """Module holding AvgStdevStats class."""
15
16 import math
17
18
19 class AvgStdevStats:
20     """Class for statistics which include average and stdev of a group.
21
22     Contrary to other stats types, adding values to the group
23     is computationally light without any caching.
24
25     Instances are only statistics, the data itself is stored elsewhere.
26     """
27
28     def __init__(self, size=0, avg=0.0, stdev=0.0):
29         """Construct the stats object by storing the values needed.
30
31         Each value has to be numeric.
32         The values are not sanitized depending on size, wrong initialization
33         can cause delayed math errors.
34
35         :param size: Number of values participating in this group.
36         :param avg: Population average of the participating sample values.
37         :param stdev: Population standard deviation of the sample values.
38         :type size: int
39         :type avg: float
40         :type stdev: float
41         """
42         self.size = size
43         self.avg = avg
44         self.stdev = stdev
45
46     def __str__(self):
47         """Return string with human readable description of the group.
48
49         :returns: Readable description.
50         :rtype: str
51         """
52         return f"size={self.size} avg={self.avg} stdev={self.stdev}"
53
54     def __repr__(self):
55         """Return string executable as Python constructor call.
56
57         :returns: Executable constructor call.
58         :rtype: str
59         """
60         return (
61             f"AvgStdevStats(size={self.size!r},avg={self.avg!r}"
62             f",stdev={self.stdev!r})"
63         )
64
65     @classmethod
66     def for_runs(cls, runs):
67         """Return new stats instance describing the sequence of runs.
68
69         If you want to append data to existing stats object,
70         you can simply use the stats object as the first run.
71
72         Instead of a verb, "for" is used to start this method name,
73         to signify the result contains less information than the input data.
74
75         Here, Run is a hypothetical abstract class, an union of float and cls.
76         Defining that as a real abstract class in Python 2 is too much hassle.
77
78         :param runs: Sequence of data to describe by the new metadata.
79         :type runs: Iterable[Union[float, cls]]
80         :returns: The new stats instance.
81         :rtype: cls
82         """
83         # Using Welford method to be more resistant to rounding errors.
84         # Adapted from code for sample standard deviation at:
85         # https://www.johndcook.com/blog/standard_deviation/
86         # The logic of plus operator is taken from
87         # https://www.johndcook.com/blog/skewness_kurtosis/
88         total_size = 0
89         total_avg = 0.0
90         moment_2 = 0.0
91         for run in runs:
92             if isinstance(run, (float, int)):
93                 run_size = 1
94                 run_avg = run
95                 run_stdev = 0.0
96             else:
97                 run_size = run.size
98                 run_avg = run.avg
99                 run_stdev = run.stdev
100             old_total_size = total_size
101             delta = run_avg - total_avg
102             total_size += run_size
103             total_avg += delta * run_size / total_size
104             moment_2 += run_stdev * run_stdev * run_size
105             moment_2 += delta * delta * old_total_size * run_size / total_size
106         if total_size < 1:
107             # Avoid division by zero.
108             return cls(size=0)
109         # TODO: Is it worth tracking moment_2 instead, and compute and cache
110         # stdev on demand, just to possibly save some sqrt calls?
111         total_stdev = math.sqrt(moment_2 / total_size)
112         ret_obj = cls(size=total_size, avg=total_avg, stdev=total_stdev)
113         return ret_obj