///|
/// Sort a copy of the input array in ascending order using selection sort.
pub fn sort_doubles(values : Array[Double]) -> Array[Double] {
let sorted = values.copy()
let n = sorted.length()
for i = 0; i < n; i = i + 1 {
let mut min_idx = i
for j = i + 1; j < n; j = j + 1 {
if sorted[j] < sorted[min_idx] {
min_idx = j
}
}
if min_idx != i {
let temp = sorted[i]
sorted[i] = sorted[min_idx]
sorted[min_idx] = temp
}
}
sorted
}
///|
/// Compute the median of a non-empty array of doubles.
pub fn median_doubles(values : Array[Double]) -> Double {
let sorted = sort_doubles(values)
let n = sorted.length()
if n == 0 {
return 0.0
}
if n % 2 == 1 {
sorted[n / 2]
} else {
(sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
}
}
///|
/// Compute the p-th percentile (0-100) using linear interpolation.
pub fn percentile_doubles(values : Array[Double], p : Double) -> Double {
let sorted = sort_doubles(values)
let n = sorted.length()
if n == 0 {
return 0.0
}
if p <= 0.0 {
return sorted[0]
}
if p >= 100.0 {
return sorted[n - 1]
}
let rank = p / 100.0 * (n - 1).to_double()
let lower = rank.to_int()
let upper = lower + 1
if upper >= n {
return sorted[lower]
}
let frac = rank - lower.to_double()
sorted[lower] + frac * (sorted[upper] - sorted[lower])
}
///|
/// A comprehensive summary of metric values across multiple runs.
///
/// This extends `MetricStats` with median, standard deviation, and
/// percentile support for richer statistical analysis.
pub struct MetricSummary {
priv key : String
priv count : Int
priv min_val : Double
priv max_val : Double
priv mean_val : Double
priv median_val : Double
priv stddev_val : Double
priv values : Array[Double]
} derive(Debug)
///|
/// Return the metric key.
pub fn MetricSummary::key(self : MetricSummary) -> String {
self.key
}
///|
/// Return the number of data points.
pub fn MetricSummary::count(self : MetricSummary) -> Int {
self.count
}
///|
/// Return the minimum value.
pub fn MetricSummary::min_val(self : MetricSummary) -> Double {
self.min_val
}
///|
/// Return the maximum value.
pub fn MetricSummary::max_val(self : MetricSummary) -> Double {
self.max_val
}
///|
/// Return the mean value.
pub fn MetricSummary::mean_val(self : MetricSummary) -> Double {
self.mean_val
}
///|
/// Return the median value.
pub fn MetricSummary::median_val(self : MetricSummary) -> Double {
self.median_val
}
///|
/// Return the standard deviation.
pub fn MetricSummary::stddev_val(self : MetricSummary) -> Double {
self.stddev_val
}
///|
/// Return a detached copy of all values.
pub fn MetricSummary::values(self : MetricSummary) -> Array[Double] {
self.values.copy()
}
///|
/// Return the range (max - min).
pub fn MetricSummary::range(self : MetricSummary) -> Double {
self.max_val - self.min_val
}
///|
/// Return the variance (stddev^2).
pub fn MetricSummary::variance(self : MetricSummary) -> Double {
self.stddev_val * self.stddev_val
}
///|
/// Return the coefficient of variation (stddev / mean).
/// Returns 0.0 if mean is zero.
pub fn MetricSummary::coefficient_of_variation(self : MetricSummary) -> Double {
if self.mean_val == 0.0 {
return 0.0
}
self.stddev_val / self.mean_val
}
///|
/// Return the p-th percentile (0-100) of the values.
pub fn MetricSummary::percentile(self : MetricSummary, p : Double) -> Double {
percentile_doubles(self.values, p)
}
///|
/// Return the first quartile (25th percentile).
pub fn MetricSummary::q1(self : MetricSummary) -> Double {
percentile_doubles(self.values, 25.0)
}
///|
/// Return the third quartile (75th percentile).
pub fn MetricSummary::q3(self : MetricSummary) -> Double {
percentile_doubles(self.values, 75.0)
}
///|
/// Return the interquartile range (Q3 - Q1).
pub fn MetricSummary::iqr(self : MetricSummary) -> Double {
self.q3() - self.q1()
}
///|
/// Compute a comprehensive metric summary for a specific metric key
/// across all runs in an experiment.
///
/// Only the latest metric value for each run is considered. Runs without
/// the specified metric are skipped.
pub fn TrackingStore::metric_summary(
self : TrackingStore,
experiment_id : String,
metric_key : String,
) -> Result[MetricSummary, TrackingError] {
match self.get_experiment(experiment_id) {
Err(err) => return Err(err)
Ok(_) => ()
}
let runs = self.runs_for_experiment(experiment_id)
let values : Array[Double] = []
for run in runs {
match run.latest_metric(metric_key) {
Some(m) => values.push(m.value())
None => ()
}
}
if values.is_empty() {
return Err(RunNotFound(metric_key))
}
let sorted = sort_doubles(values)
let count = sorted.length()
let min_v = sorted[0]
let max_v = sorted[count - 1]
let mut sum = 0.0
for v in sorted {
sum += v
}
let mean = sum / count.to_double()
let median = median_doubles(sorted)
let mut sum_sq = 0.0
for v in sorted {
let diff = v - mean
sum_sq += diff * diff
}
let variance = if count < 2 { 0.0 } else { sum_sq / (count - 1).to_double() }
let stddev = variance.sqrt()
Ok({
key: metric_key,
count,
min_val: min_v,
max_val: max_v,
mean_val: mean,
median_val: median,
stddev_val: stddev,
values,
})
}
///|
/// Compute metric summaries for all metric keys in an experiment.
pub fn TrackingStore::all_metric_summaries(
self : TrackingStore,
experiment_id : String,
) -> Result[Array[MetricSummary], TrackingError] {
match self.get_experiment(experiment_id) {
Err(err) => return Err(err)
Ok(_) => ()
}
let runs = self.runs_for_experiment(experiment_id)
let all_keys : Array[String] = []
for run in runs {
for m in run.metrics() {
if !array_contains_string(all_keys, m.key()) {
all_keys.push(m.key())
}
}
}
let summaries : Array[MetricSummary] = []
for key in all_keys {
match self.metric_summary(experiment_id, key) {
Ok(s) => summaries.push(s)
Err(_) => ()
}
}
Ok(summaries)
}
///|
/// Compute the Pearson correlation coefficient between two metrics
/// across all runs in an experiment.
///
/// Only runs that have both metrics are considered. At least 2 data
/// points are required.
pub fn TrackingStore::metric_correlation(
self : TrackingStore,
experiment_id : String,
key_a : String,
key_b : String,
) -> Result[Double, TrackingError] {
match self.get_experiment(experiment_id) {
Err(err) => return Err(err)
Ok(_) => ()
}
let runs = self.runs_for_experiment(experiment_id)
let xs : Array[Double] = []
let ys : Array[Double] = []
for run in runs {
match run.latest_metric(key_a) {
Some(ma) =>
match run.latest_metric(key_b) {
Some(mb) => {
xs.push(ma.value())
ys.push(mb.value())
}
None => ()
}
None => ()
}
}
let n = xs.length()
if n < 2 {
return Err(RunNotFound(key_a))
}
let mut sum_x = 0.0
let mut sum_y = 0.0
for i = 0; i < n; i = i + 1 {
sum_x += xs[i]
sum_y += ys[i]
}
let mean_x = sum_x / n.to_double()
let mean_y = sum_y / n.to_double()
let mut numerator = 0.0
let mut sum_sq_x = 0.0
let mut sum_sq_y = 0.0
for i = 0; i < n; i = i + 1 {
let dx = xs[i] - mean_x
let dy = ys[i] - mean_y
numerator += dx * dy
sum_sq_x += dx * dx
sum_sq_y += dy * dy
}
let denominator = (sum_sq_x * sum_sq_y).sqrt()
if denominator == 0.0 {
return Err(RunNotFound("zero variance"))
}
Ok(numerator / denominator)
}
///|
/// Generate a comprehensive statistics report in Markdown format.
///
/// Includes metric summaries (count, min, max, mean, median, std dev)
/// and pairwise metric correlations.
pub fn TrackingStore::statistics_report(
self : TrackingStore,
experiment_id : String,
) -> Result[String, TrackingError] {
match self.get_experiment(experiment_id) {
Err(err) => return Err(err)
Ok(exp) => {
let out = StringBuilder()
out <+ "# Statistics Report\n\n"
out <+ "## Experiment: \{exp.name()}\n\n"
out <+ "- **ID**: `\{exp.id()}`\n"
out <+ "- **Runs**: \{exp.run_count()}\n\n"
match self.all_metric_summaries(experiment_id) {
Ok(summaries) => {
if summaries.is_empty() {
out <+ "_No metrics recorded._\n\n"
} else {
out <+ "### Metric Summaries\n\n"
out <+ "| Metric | Count | Min | Max | Mean | Median | Std Dev |\n"
out <+ "|--------|-------|-----|-----|------|--------|---------|\n"
for s in summaries {
out <+ "| \{s.key()} | \{s.count()} | "
out <+ "\{s.min_val()} | \{s.max_val()} | "
out <+ "\{s.mean_val()} | \{s.median_val()} | "
out <+ "\{s.stddev_val()} |\n"
}
out <+ "\n"
if summaries.length() >= 2 {
out <+ "### Metric Correlations\n\n"
out <+ "| Metric A | Metric B | Correlation |\n"
out <+ "|----------|----------|-------------|\n"
for i = 0; i < summaries.length(); i = i + 1 {
for j = i + 1; j < summaries.length(); j = j + 1 {
let key_a = summaries[i].key()
let key_b = summaries[j].key()
match self.metric_correlation(experiment_id, key_a, key_b) {
Ok(corr) => out <+ "| \{key_a} | \{key_b} | \{corr} |\n"
Err(_) => ()
}
}
}
out <+ "\n"
}
}
Ok(out.to_string())
}
Err(err) => Err(err)
}
}
}
}