// Box plot statistics: computes five-number summary from raw data.
///|
/// Statistics for a single box plot: min, Q1, median, Q3, max, IQR, fences, whiskers, and outliers.
pub(all) struct BoxStats {
min : Double
q1 : Double
median : Double
q3 : Double
max : Double
iqr : Double
lowerFence : Double
upperFence : Double
whiskerLow : Double
whiskerHigh : Double
outliers : Array[Double]
}
///|
/// Compute box plot statistics (five-number summary, IQR, fences, whiskers, outliers) from an array of values.
pub fn computeBoxStats(values : Array[Double]) -> BoxStats {
let sorted = values.copy()
// Sort ascending
for i = 0; i < sorted.length(); i = i + 1 {
for j = i + 1; j < sorted.length(); j = j + 1 {
if sorted[i] > sorted[j] {
let tmp = sorted[i]
sorted[i] = sorted[j]
sorted[j] = tmp
}
}
}
let n = sorted.length()
let min = sorted[0]
let max = sorted[n - 1]
let median = percentile(sorted, 0.5)
let q1 = percentile(sorted, 0.25)
let q3 = percentile(sorted, 0.75)
let iqr = q3 - q1
let lowerFence = q1 - 1.5 * iqr
let upperFence = q3 + 1.5 * iqr
// Find whiskers (closest values within fences)
let mut whiskerLow = min
let mut whiskerHigh = max
for i = 0; i < n; i = i + 1 {
if sorted[i] >= lowerFence {
whiskerLow = sorted[i]
break
}
}
let mut i = n - 1
while i >= 0 {
if sorted[i] <= upperFence {
whiskerHigh = sorted[i]
break
}
i = i - 1
}
// Collect outliers
let outliers : Array[Double] = []
for i = 0; i < n; i = i + 1 {
if sorted[i] < lowerFence || sorted[i] > upperFence {
outliers.push(sorted[i])
}
}
{
min,
q1,
median,
q3,
max,
iqr,
lowerFence,
upperFence,
whiskerLow,
whiskerHigh,
outliers,
}
}
// Compute a percentile using linear interpolation between neighboring sorted elements.
///|
fn percentile(sorted : Array[Double], p : Double) -> Double {
let n = sorted.length()
let idx = p * (n.to_double() - 1.0)
let lo = idx.floor().to_int()
let hi = idx.ceil().to_int()
if lo == hi {
sorted[lo]
} else {
let frac = idx - lo.to_double()
sorted[lo] + frac * (sorted[hi] - sorted[lo])
}
}