///|
/// Return a quantile using linear interpolation between adjacent order statistics.
/// `probability` is in the closed interval `[0, 1]`.
pub fn quantile(data : Array[Double], probability : Double) -> Double {
if data.length() == 0 {
return 0.0
}
validate_probability(probability)
let sorted = copy_and_sort(data)
if sorted.length() == 1 {
return sorted[0]
}
let position = probability * (sorted.length() - 1).to_double()
let lower = position.to_int()
let upper = if lower + 1 < sorted.length() { lower + 1 } else { lower }
let fraction = position - lower.to_double()
sorted[lower] + fraction * (sorted[upper] - sorted[lower])
}
///|
pub fn percentile(data : Array[Double], percent : Double) -> Double {
if percent < 0.0 || percent > 100.0 {
abort("percent must be in [0, 100]")
}
quantile(data, percent / 100.0)
}
///|
pub fn lower_quartile(data : Array[Double]) -> Double {
quantile(data, 0.25)
}
///|
pub fn upper_quartile(data : Array[Double]) -> Double {
quantile(data, 0.75)
}
///|
pub fn interquartile_range(data : Array[Double]) -> Double {
upper_quartile(data) - lower_quartile(data)
}
///|
pub fn quantile_range(
data : Array[Double],
lower_probability : Double,
upper_probability : Double,
) -> Double {
if lower_probability > upper_probability {
abort("lower probability must not exceed upper probability")
}
quantile(data, upper_probability) - quantile(data, lower_probability)
}
///|
/// The nearest-rank quantile is useful when a discrete order statistic is required.
pub fn nearest_rank(data : Array[Double], probability : Double) -> Double {
if data.length() == 0 {
return 0.0
}
validate_probability(probability)
let sorted = copy_and_sort(data)
let mut rank = (probability * sorted.length().to_double()).to_int()
if probability > 0.0 &&
rank.to_double() < probability * sorted.length().to_double() {
rank += 1
}
if rank < 1 {
rank = 1
}
sorted[rank - 1]
}
///|
pub fn median_low(data : Array[Double]) -> Double {
if data.length() == 0 {
0.0
} else {
let sorted = copy_and_sort(data)
sorted[(sorted.length() - 1) / 2]
}
}
///|
pub fn median_high(data : Array[Double]) -> Double {
if data.length() == 0 {
0.0
} else {
let sorted = copy_and_sort(data)
sorted[sorted.length() / 2]
}
}
///|
pub fn weighted_quantile(
data : Array[Double],
weights : Array[Double],
probability : Double,
) -> Double {
if data.length() == 0 || data.length() != weights.length() {
return 0.0
}
validate_probability(probability)
let pairs = []
for index = 0; index < data.length(); index = index + 1 {
if weights[index] < 0.0 {
abort("weights must be non-negative")
}
pairs.push((data[index], weights[index]))
}
pairs.sort()
let mut total_weight = 0.0
for pair in pairs {
total_weight += pair.1
}
if total_weight == 0.0 {
return 0.0
}
let target = probability * total_weight
let mut cumulative = 0.0
for pair in pairs {
cumulative += pair.1
if cumulative >= target {
return pair.0
}
}
pairs[pairs.length() - 1].0
}
///|
pub fn empirical_cdf(data : Array[Double], value : Double) -> Double {
if data.length() == 0 {
0.0
} else {
let mut count = 0
for item in data {
if item <= value {
count += 1
}
}
count.to_double() / data.length().to_double()
}
}
///|
pub fn rank_of(data : Array[Double], value : Double) -> Double {
if data.length() == 0 {
0.0
} else {
let mut less = 0
let mut equal = 0
for item in data {
if item < value {
less += 1
} else if item == value {
equal += 1
}
}
less.to_double() + (equal.to_double() + 1.0) / 2.0
}
}
///|
pub fn ranks(data : Array[Double]) -> Array[Double] {
let result = []
for value in data {
result.push(rank_of(data, value))
}
result
}
///|
pub fn quantile_bins(data : Array[Double], bins : Int) -> Array[Int] {
if bins <= 0 {
abort("bins must be positive")
}
let result = []
for value in data {
let mut bucket = (empirical_cdf(data, value) * bins.to_double()).to_int()
if bucket >= bins {
bucket = bins - 1
}
result.push(bucket)
}
result
}
///|
pub fn order_statistics(
data : Array[Double],
positions : Array[Int],
) -> Array[Double] {
let sorted = copy_and_sort(data)
let result = []
for position in positions {
if position < 0 || position >= sorted.length() {
abort("order statistic position out of bounds")
}
result.push(sorted[position])
}
result
}
///|
pub fn five_number_summary(data : Array[Double]) -> Array[Double] {
if data.length() == 0 {
[]
} else {
[
min_value(data),
lower_quartile(data),
median(data),
upper_quartile(data),
max_value(data),
]
}
}