Completed
Pull Request — master (#59)
by
unknown
01:30
created

BenchmarkStats   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 62
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 62
rs 10
wmc 16

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 12 1
A __bool__() 0 2 1
A has_error() 0 3 1
A __getitem__() 0 5 2
A __nonzero__() 0 2 1
A get() 0 5 2
A update() 0 2 1
C as_dict() 0 23 7
1
from __future__ import division
2
from __future__ import print_function
3
4
import statistics
5
from bisect import bisect_left
6
from bisect import bisect_right
7
8
from .utils import cached_property
9
from .utils import funcname
10
from .utils import get_cprofile_functions
11
12
13
class Stats(object):
14
    fields = (
15
        "min", "max", "mean", "stddev", "rounds", "median", "iqr", "q1", "q3", "iqr_outliers", "stddev_outliers",
16
        "outliers", "ld15iqr", "hd15iqr"
17
    )
18
19
    def __init__(self):
20
        self.data = []
21
22
    def __bool__(self):
23
        return bool(self.data)
24
25
    def __nonzero__(self):
26
        return bool(self.data)
27
28
    def as_dict(self):
29
        return dict(
30
            (field, getattr(self, field))
31
            for field in self.fields
32
        )
33
34
    def update(self, duration):
35
        self.data.append(duration)
36
37
    @cached_property
38
    def sorted_data(self):
39
        return sorted(self.data)
40
41
    @cached_property
42
    def total(self):
43
        return sum(self.data)
44
45
    @cached_property
46
    def min(self):
47
        return min(self.data)
48
49
    @cached_property
50
    def max(self):
51
        return max(self.data)
52
53
    @cached_property
54
    def mean(self):
55
        return statistics.mean(self.data)
56
57
    @cached_property
58
    def stddev(self):
59
        if len(self.data) > 1:
60
            return statistics.stdev(self.data)
61
        else:
62
            return 0
63
64
    @property
65
    def stddev_outliers(self):
66
        """
67
        Count of StdDev outliers: what's beyond (Mean - StdDev, Mean - StdDev)
68
        """
69
        count = 0
70
        q0 = self.mean - self.stddev
71
        q4 = self.mean + self.stddev
72
        for val in self.data:
73
            if val < q0 or val > q4:
74
                count += 1
75
        return count
76
77
    @cached_property
78
    def rounds(self):
79
        return len(self.data)
80
81
    @cached_property
82
    def median(self):
83
        return statistics.median(self.data)
84
85
    @cached_property
86
    def ld15iqr(self):
87
        """
88
        Tukey-style Lowest Datum within 1.5 IQR under Q1.
89
        """
90
        if len(self.data) == 1:
91
            return self.data[0]
92
        else:
93
            return self.sorted_data[bisect_left(self.sorted_data, self.q1 - 1.5 * self.iqr)]
94
95
    @cached_property
96
    def hd15iqr(self):
97
        """
98
        Tukey-style Highest Datum within 1.5 IQR over Q3.
99
        """
100
        if len(self.data) == 1:
101
            return self.data[0]
102
        else:
103
            pos = bisect_right(self.sorted_data, self.q3 + 1.5 * self.iqr)
104
            if pos == len(self.data):
105
                return self.sorted_data[-1]
106
            else:
107
                return self.sorted_data[pos]
108
109
    @cached_property
110
    def q1(self):
111
        rounds = self.rounds
112
        data = self.sorted_data
113
114
        # See: https://en.wikipedia.org/wiki/Quartile#Computing_methods
115 View Code Duplication
        if rounds == 1:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
116
            return data[0]
117
        elif rounds % 2:  # Method 3
118
            n, q = rounds // 4, rounds % 4
119
            if q == 1:
120
                return 0.25 * data[n - 1] + 0.75 * data[n]
121
            else:
122
                return 0.75 * data[n] + 0.25 * data[n + 1]
123
        else:  # Method 2
124
            return statistics.median(data[:rounds // 2])
125
126
    @cached_property
127
    def q3(self):
128
        rounds = self.rounds
129
        data = self.sorted_data
130
131
        # See: https://en.wikipedia.org/wiki/Quartile#Computing_methods
132 View Code Duplication
        if rounds == 1:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
133
            return data[0]
134
        elif rounds % 2:  # Method 3
135
            n, q = rounds // 4, rounds % 4
136
            if q == 1:
137
                return 0.75 * data[3 * n] + 0.25 * data[3 * n + 1]
138
            else:
139
                return 0.25 * data[3 * n + 1] + 0.75 * data[3 * n + 2]
140
        else:  # Method 2
141
            return statistics.median(data[rounds // 2:])
142
143
    @cached_property
144
    def iqr(self):
145
        return self.q3 - self.q1
146
147
    @property
148
    def iqr_outliers(self):
149
        """
150
        Count of Tukey outliers: what's beyond (Q1 - 1.5IQR, Q3 + 1.5IQR)
151
        """
152
        count = 0
153
        q0 = self.q1 - 1.5 * self.iqr
154
        q4 = self.q3 + 1.5 * self.iqr
155
        for val in self.data:
156
            if val < q0 or val > q4:
157
                count += 1
158
        return count
159
160
    @cached_property
161
    def outliers(self):
162
        return "%s;%s" % (self.stddev_outliers, self.iqr_outliers)
163
164
165
class BenchmarkStats(object):
166
    def __init__(self, fixture, iterations, options):
167
        self.name = fixture.name
168
        self.fullname = fixture.fullname
169
        self.group = fixture.group
170
        self.param = fixture.param
171
        self.params = fixture.params
172
        self.cprofile_stats = fixture.cprofile_stats
173
174
        self.iterations = iterations
175
        self.stats = Stats()
176
        self.options = options
177
        self.fixture = fixture
178
179
    def __bool__(self):
180
        return bool(self.stats)
181
182
    def __nonzero__(self):
183
        return bool(self.stats)
184
185
    def get(self, key, default=None):
186
        try:
187
            return getattr(self.stats, key)
188
        except AttributeError:
189
            return getattr(self, key, default)
190
191
    def __getitem__(self, key):
192
        try:
193
            return getattr(self.stats, key)
194
        except AttributeError:
195
            return getattr(self, key)
196
197
    @property
198
    def has_error(self):
199
        return self.fixture.has_error
200
201
    def as_dict(self, include_data=True, flat=False, stats=True, cprofile_sort_by="cumtime"):
202
        result = {
203
            "group": self.group,
204
            "name": self.name,
205
            "fullname": self.fullname,
206
            "params": self.params,
207
            "param": self.param,
208
            "options": dict(
209
                (k, funcname(v) if callable(v) else v) for k, v in self.options.items()
210
            )
211
        }
212
        if self.cprofile_stats:
213
            result["cprofile"] = get_cprofile_functions(self.cprofile_stats, cprofile_sort_by)[:10]
214
        if stats:
215
            stats = self.stats.as_dict()
216
            if include_data:
217
                stats["data"] = self.stats.data
218
            stats["iterations"] = self.iterations
219
            if flat:
220
                result.update(stats)
221
            else:
222
                result["stats"] = stats
223
        return result
224
225
    def update(self, duration):
226
        self.stats.update(duration / self.iterations)
227