# Copyright 2022-2025 The Ramble Authors
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
import statistics
[docs]
def decimal_places(value):
"""Returns the number of decimal places of a value"""
val_str = str(value)
if "." not in val_str:
return 0
else:
return len(val_str.split(".")[1])
[docs]
def max_decimal_places(list):
"""Returns the max decimal places of a list of values"""
max = 0
for val in list:
if decimal_places(val) > max:
max = decimal_places(val)
return max
[docs]
class StatsBase:
min_count = 1
name = ""
[docs]
def compute(self, values):
pass
[docs]
def get_unit(self, unit):
return unit
[docs]
def report(self, values, unit):
label = f"summary::{self.name}"
if len(values) < self.min_count:
return ("NA", "", label)
return (self.compute(values), self.get_unit(unit), label)
[docs]
class StatsMin(StatsBase):
name = "min"
[docs]
def compute(self, values):
return min(values)
[docs]
class StatsMax(StatsBase):
name = "max"
[docs]
def compute(self, values):
return max(values)
[docs]
class StatsMean(StatsBase):
name = "mean"
[docs]
def compute(self, values):
return round(statistics.mean(values), max_decimal_places(values))
[docs]
class StatsVar(StatsBase):
name = "variance"
min_count = 2
[docs]
def get_unit(self, unit):
return f"{unit}^2"
[docs]
def compute(self, values):
return round(statistics.variance(values), max_decimal_places(values))
[docs]
class StatsStdev(StatsBase):
name = "stdev"
min_count = 2
[docs]
def compute(self, values):
return round(statistics.stdev(values), max_decimal_places(values))
[docs]
class StatsCoefficientOfVariation(StatsBase):
name = "cv"
min_count = 2
[docs]
def compute(self, values):
mean = statistics.mean(values)
# Only guard against zero mean.
# While CV isn't particularly meaningful when negative values are present,
# calculate anyway and leave the interpretation to individual experiments.
if not mean:
return "NA"
return round(
statistics.stdev(values) / statistics.mean(values), max_decimal_places(values)
)
[docs]
def get_unit(self, unit):
# `unit` unused
del unit
return ""
all_stats = [
StatsMin(),
StatsMax(),
StatsMean(),
StatsMedian(),
StatsVar(),
StatsStdev(),
StatsCoefficientOfVariation(),
]