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.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  else:
59    return 0.0
60
61
62def _Mean(values):
63  return float(sum(values)) / len(values) if len(values) > 0 else 0.0
64
65
66class ListOfScalarValues(summarizable.SummarizableValue):
67  """ ListOfScalarValues represents a list of numbers.
68
69  By default, std is the standard deviation of all numbers in the list. Std can
70  also be specified in the constructor if the numbers are not from the same
71  population.
72  """
73  def __init__(self, page, name, units, values,
74               important=True, description=None,
75               tir_label=None, none_value_reason=None,
76               std=None, 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    if values is not None and std is None:
92      std = StandardDeviation(values)
93    assert std is None or std >= 0, (
94        'standard deviation cannot be negative: %s' % std)
95    self._std = std
96
97  @property
98  def std(self):
99    return self._std
100
101  @property
102  def variance(self):
103    return self._std ** 2
104
105  def __repr__(self):
106    if self.page:
107      page_name = self.page.display_name
108    else:
109      page_name = 'None'
110    return ('ListOfScalarValues(%s, %s, %s, %s, '
111            'important=%s, description=%s, tir_label=%s, std=%s, '
112            'improvement_direction=%s, grouping_keys=%s)') % (
113                page_name,
114                self.name,
115                self.units,
116                repr(self.values),
117                self.important,
118                self.description,
119                self.tir_label,
120                self.std,
121                self.improvement_direction,
122                self.grouping_keys)
123
124  def GetBuildbotDataType(self, output_context):
125    if self._IsImportantGivenOutputIntent(output_context):
126      return 'default'
127    return 'unimportant'
128
129  def GetBuildbotValue(self):
130    return self.values
131
132  def GetRepresentativeNumber(self):
133    return _Mean(self.values)
134
135  def GetRepresentativeString(self):
136    return repr(self.values)
137
138  @staticmethod
139  def GetJSONTypeName():
140    return 'list_of_scalar_values'
141
142  def AsDict(self):
143    d = super(ListOfScalarValues, self).AsDict()
144    d['values'] = self.values
145    d['std'] = self.std
146
147    if self.none_value_reason is not None:
148      d['none_value_reason'] = self.none_value_reason
149
150    return d
151
152  @staticmethod
153  def FromDict(value_dict, page_dict):
154    kwargs = value_module.Value.GetConstructorKwArgs(value_dict, page_dict)
155    kwargs['values'] = value_dict['values']
156    kwargs['std'] = value_dict['std']
157
158    if 'improvement_direction' in value_dict:
159      kwargs['improvement_direction'] = value_dict['improvement_direction']
160    if 'none_value_reason' in value_dict:
161      kwargs['none_value_reason'] = value_dict['none_value_reason']
162
163    return ListOfScalarValues(**kwargs)
164
165  @classmethod
166  def MergeLikeValuesFromSamePage(cls, values):
167    assert len(values) > 0
168    v0 = values[0]
169
170    return cls._MergeLikeValues(values, v0.page, v0.name, v0.grouping_keys)
171
172  @classmethod
173  def MergeLikeValuesFromDifferentPages(cls, values):
174    assert len(values) > 0
175    v0 = values[0]
176    return cls._MergeLikeValues(values, None, v0.name, v0.grouping_keys)
177
178  @classmethod
179  def _MergeLikeValues(cls, values, page, name, grouping_keys):
180    v0 = values[0]
181    merged_values = []
182    list_of_samples = []
183    none_value_reason = None
184    pooled_std = None
185    for v in values:
186      if v.values is None:
187        merged_values = None
188        merged_none_values = [v for v in values if v.values is None]
189        none_value_reason = (none_values.MERGE_FAILURE_REASON +
190            ' None values: %s' % repr(merged_none_values))
191        break
192      merged_values.extend(v.values)
193      list_of_samples.append(v.values)
194    if merged_values and page is None:
195      # Pooled standard deviation is only used when merging values comming from
196      # different pages. Otherwise, fall back to the default computation done
197      # in the cosntructor of ListOfScalarValues.
198      pooled_std = PooledStandardDeviation(
199          list_of_samples, list_of_variances=[v.variance for v in values])
200    return ListOfScalarValues(
201        page, name, v0.units,
202        merged_values,
203        important=v0.important,
204        description=v0.description,
205        tir_label=value_module.MergedTirLabel(values),
206        std=pooled_std,
207        none_value_reason=none_value_reason,
208        improvement_direction=v0.improvement_direction,
209        grouping_keys=grouping_keys)
210