crosperf: replace statistics stdev with pstdev

Behavior and results of statistics.stdev(data) are slightly different from
numpy.std(data) with default "ddof".
The main difference is the divisor which is "N - 1" in stdev vs. "N" in
numpy.std. As a consequence stdev fails with "N=1".

The change replaces stdev with pstdev (population standard deviation)
which is equivalent to numpy.std with default arguments that we were
using.
Added unittest with StdResult testing.

BUG=None
TEST=unittest and crosperf with one iteration passes.

Change-Id: I70c7105e6cabc27437504de16ea27afdd719e552
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/2376880
Reviewed-by: George Burgess <gbiv@chromium.org>
Tested-by: Denis Nikitin <denik@chromium.org>
Commit-Queue: Denis Nikitin <denik@chromium.org>
diff --git a/cros_utils/tabulator.py b/cros_utils/tabulator.py
index 27b1c64..1a3fd4a 100644
--- a/cros_utils/tabulator.py
+++ b/cros_utils/tabulator.py
@@ -610,7 +610,7 @@
   def _ComputeFloat(self, cell, values, baseline_values):
     if self.ignore_min_max:
       values = _RemoveMinMax(cell, values)
-    cell.value = statistics.stdev(values)
+    cell.value = statistics.pstdev(values)
 
 
 class CoeffVarResult(NumericalResult):
@@ -624,7 +624,7 @@
     if self.ignore_min_max:
       values = _RemoveMinMax(cell, values)
     if statistics.mean(values) != 0.0:
-      noise = abs(statistics.stdev(values) / statistics.mean(values))
+      noise = abs(statistics.pstdev(values) / statistics.mean(values))
     else:
       noise = 0.0
     cell.value = noise
@@ -1498,40 +1498,40 @@
 
 if __name__ == '__main__':
   # Run a few small tests here.
-  runs = [
-      [{
-          'k1': '10',
-          'k2': '12',
-          'k5': '40',
-          'k6': '40',
-          'ms_1': '20',
-          'k7': 'FAIL',
-          'k8': 'PASS',
-          'k9': 'PASS',
-          'k10': '0'
-      }, {
-          'k1': '13',
-          'k2': '14',
-          'k3': '15',
-          'ms_1': '10',
-          'k8': 'PASS',
-          'k9': 'FAIL',
-          'k10': '0'
-      }],
-      [{
-          'k1': '50',
-          'k2': '51',
-          'k3': '52',
-          'k4': '53',
-          'k5': '35',
-          'k6': '45',
-          'ms_1': '200',
-          'ms_2': '20',
-          'k7': 'FAIL',
-          'k8': 'PASS',
-          'k9': 'PASS'
-      }],
-  ]
+  run1 = {
+      'k1': '10',
+      'k2': '12',
+      'k5': '40',
+      'k6': '40',
+      'ms_1': '20',
+      'k7': 'FAIL',
+      'k8': 'PASS',
+      'k9': 'PASS',
+      'k10': '0'
+  }
+  run2 = {
+      'k1': '13',
+      'k2': '14',
+      'k3': '15',
+      'ms_1': '10',
+      'k8': 'PASS',
+      'k9': 'FAIL',
+      'k10': '0'
+  }
+  run3 = {
+      'k1': '50',
+      'k2': '51',
+      'k3': '52',
+      'k4': '53',
+      'k5': '35',
+      'k6': '45',
+      'ms_1': '200',
+      'ms_2': '20',
+      'k7': 'FAIL',
+      'k8': 'PASS',
+      'k9': 'PASS'
+  }
+  runs = [[run1, run2], [run3]]
   labels = ['vanilla', 'modified']
   t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
   print(t)
diff --git a/cros_utils/tabulator_test.py b/cros_utils/tabulator_test.py
index 227e2d7..9dd4828 100755
--- a/cros_utils/tabulator_test.py
+++ b/cros_utils/tabulator_test.py
@@ -33,6 +33,20 @@
     result.Compute(cell, table[2], table[1])
     self.assertTrue(cell.value == float(table[2][0]))
 
+  def testStdResult(self):
+    table = ['k1', [], ['1', '2']]
+    result = tabulator.StdResult()
+    cell = tabulator.Cell()
+    result.Compute(cell, table[2], table[1])
+    self.assertTrue(cell.value == 0.5)
+
+  def testStdResultOfSampleSize1(self):
+    table = ['k1', [], ['1']]
+    result = tabulator.StdResult()
+    cell = tabulator.Cell()
+    result.Compute(cell, table[2], table[1])
+    self.assertTrue(cell.value == 0.0)
+
   def testStringMean(self):
     smr = tabulator.StringMeanResult()
     cell = tabulator.Cell()