1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import numbers
6import math
7
8from telemetry import value as value_module
9from telemetry.value import none_values
10from telemetry.value import summarizable
11
12
13def Variance(sample):
14  """ Compute the population variance.
15
16    Args:
17      sample: a list of numbers.
18  """
19  k = len(sample) - 1  # Bessel correction
20  if k <= 0:
21    return 0
22  m = _Mean(sample)
23  return sum((x - m)**2 for x in sample)/k
24
25
26def StandardDeviation(sample):
27  """ Compute standard deviation for a list of numbers.
28
29    Args:
30      sample: a list of numbers.
31  """
32  return math.sqrt(Variance(sample))
33
34
35def PooledStandardDeviation(list_of_samples, list_of_variances=None):
36  """ Compute standard deviation for a list of samples.
37
38  See: https://en.wikipedia.org/wiki/Pooled_variance for the formula.
39
40  Args:
41    list_of_samples: a list of lists, each is a list of numbers.
42    list_of_variances: a list of numbers, the i-th element is the variance of
43      the i-th sample in list_of_samples. If this is None, we use
44      Variance(sample) to get the variance of the i-th sample.
45  """
46  pooled_variance = 0.0
47  total_degrees_of_freedom = 0
48  for i in xrange(len(list_of_samples)):
49    l = list_of_samples[i]
50    k = len(l) - 1  # Bessel correction
51    if k <= 0:
52      continue
53    variance = list_of_variances[i] if list_of_variances else Variance(l)
54    pooled_variance += k * variance
55    total_degrees_of_freedom += k
56  if total_degrees_of_freedom:
57    return (pooled_variance/total_degrees_of_freedom) ** 0.5
58  return 0
59
60
61def _Mean(values):
62  return float(sum(values)) / len(values) if len(values) > 0 else 0.0
63
64
65class ListOfScalarValues(summarizable.SummarizableValue):
66  """ ListOfScalarValues represents a list of numbers.
67
68  By default, std is the standard deviation of all numbers in the list. Std can
69  also be specified in the constructor if the numbers are not from the same
70  population.
71  """
72  def __init__(self, page, name, units, values,
73               important=True, description=None,
74               tir_label=None, none_value_reason=None,
75               std=None, same_page_merge_policy=value_module.CONCATENATE,
76               improvement_direction=None, grouping_keys=None):
77    super(ListOfScalarValues, self).__init__(page, name, units, important,
78                                             description, tir_label,
79                                             improvement_direction,
80                                             grouping_keys)
81    if values is not None:
82      assert isinstance(values, list)
83      assert len(values) > 0
84      assert all(isinstance(v, numbers.Number) for v in values)
85      assert std is None or isinstance(std, numbers.Number)
86    else:
87      assert std is None
88    none_values.ValidateNoneValueReason(values, none_value_reason)
89    self.values = values
90    self.none_value_reason = none_value_reason
91    self.same_page_merge_policy = same_page_merge_policy
92    if values is not None and std is None:
93      std = StandardDeviation(values)
94    assert std is None or std >= 0, (
95        'standard deviation cannot be negative: %s' % std)
96    self._std = std
97
98  @property
99  def std(self):
100    return self._std
101
102  @property
103  def variance(self):
104    return self._std ** 2
105
106  def __repr__(self):
107    if self.page:
108      page_name = self.page.display_name
109    else:
110      page_name = 'None'
111    if self.same_page_merge_policy == value_module.CONCATENATE:
112      merge_policy = 'CONCATENATE'
113    else:
114      merge_policy = 'PICK_FIRST'
115    return ('ListOfScalarValues(%s, %s, %s, %s, '
116            'important=%s, description=%s, tir_label=%s, std=%s, '
117            'same_page_merge_policy=%s, improvement_direction=%s, '
118            'grouping_keys=%s)') % (
119                page_name,
120                self.name,
121                self.units,
122                repr(self.values),
123                self.important,
124                self.description,
125                self.tir_label,
126                self.std,
127                merge_policy,
128                self.improvement_direction,
129                self.grouping_keys)
130
131  def GetBuildbotDataType(self, output_context):
132    if self._IsImportantGivenOutputIntent(output_context):
133      return 'default'
134    return 'unimportant'
135
136  def GetBuildbotValue(self):
137    return self.values
138
139  def GetRepresentativeNumber(self):
140    return _Mean(self.values)
141
142  def GetRepresentativeString(self):
143    return repr(self.values)
144
145  def IsMergableWith(self, that):
146    return (super(ListOfScalarValues, self).IsMergableWith(that) and
147            self.same_page_merge_policy == that.same_page_merge_policy)
148
149  @staticmethod
150  def GetJSONTypeName():
151    return 'list_of_scalar_values'
152
153  def AsDict(self):
154    d = super(ListOfScalarValues, self).AsDict()
155    d['values'] = self.values
156    d['std'] = self.std
157
158    if self.none_value_reason is not None:
159      d['none_value_reason'] = self.none_value_reason
160
161    return d
162
163  @staticmethod
164  def FromDict(value_dict, page_dict):
165    kwargs = value_module.Value.GetConstructorKwArgs(value_dict, page_dict)
166    kwargs['values'] = value_dict['values']
167    kwargs['std'] = value_dict['std']
168
169    if 'improvement_direction' in value_dict:
170      kwargs['improvement_direction'] = value_dict['improvement_direction']
171    if 'none_value_reason' in value_dict:
172      kwargs['none_value_reason'] = value_dict['none_value_reason']
173
174    return ListOfScalarValues(**kwargs)
175
176  @classmethod
177  def MergeLikeValuesFromSamePage(cls, values):
178    assert len(values) > 0
179    v0 = values[0]
180
181    if v0.same_page_merge_policy == value_module.PICK_FIRST:
182      return ListOfScalarValues(
183          v0.page, v0.name, v0.units,
184          values[0].values,
185          important=v0.important,
186          same_page_merge_policy=v0.same_page_merge_policy,
187          none_value_reason=v0.none_value_reason,
188          improvement_direction=v0.improvement_direction,
189          grouping_keys=v0.grouping_keys)
190
191    assert v0.same_page_merge_policy == value_module.CONCATENATE
192    return cls._MergeLikeValues(values, v0.page, v0.name, v0.tir_label,
193                                v0.grouping_keys)
194
195  @classmethod
196  def MergeLikeValuesFromDifferentPages(cls, values):
197    assert len(values) > 0
198    v0 = values[0]
199    return cls._MergeLikeValues(values, None, v0.name, v0.tir_label,
200                                v0.grouping_keys)
201
202  @classmethod
203  def _MergeLikeValues(cls, values, page, name, tir_label, grouping_keys):
204    v0 = values[0]
205    merged_values = []
206    list_of_samples = []
207    none_value_reason = None
208    pooled_std = None
209    for v in values:
210      if v.values is None:
211        merged_values = None
212        none_value_reason = none_values.MERGE_FAILURE_REASON
213        break
214      merged_values.extend(v.values)
215      list_of_samples.append(v.values)
216    if merged_values:
217      pooled_std = PooledStandardDeviation(
218          list_of_samples, list_of_variances=[v.variance for v in values])
219    return ListOfScalarValues(
220        page, name, v0.units,
221        merged_values,
222        important=v0.important,
223        description=v0.description,
224        tir_label=tir_label,
225        same_page_merge_policy=v0.same_page_merge_policy,
226        std=pooled_std,
227        none_value_reason=none_value_reason,
228        improvement_direction=v0.improvement_direction,
229        grouping_keys=grouping_keys)
230