///|
pub struct DatasetSummary {
count : Int
minimum : Double
maximum : Double
mean : Double
median : Double
q1 : Double
q3 : Double
iqr : Double
mad : Double
standard_deviation : Double
skewness : Double
outlier_count : Int
outlier_fraction : Double
}
///|
pub fn summarize(
data : Array[Double],
outlier_threshold? : Double = 3.5,
) -> DatasetSummary {
let indices = outlier_indices_z(data, threshold=outlier_threshold)
{
count: data.length(),
minimum: min_value(data),
maximum: max_value(data),
mean: mean(data),
median: median(data),
q1: lower_quartile(data),
q3: upper_quartile(data),
iqr: interquartile_range(data),
mad: mad(data),
standard_deviation: sample_stddev(data),
skewness: skewness(data),
outlier_count: indices.length(),
outlier_fraction: outlier_fraction(indices, data.length()),
}
}
///|
pub fn summary_to_lines(summary : DatasetSummary) -> Array[String] {
[
"count=" + summary.count.to_string(),
"minimum=" + summary.minimum.to_string(),
"maximum=" + summary.maximum.to_string(),
"mean=" + summary.mean.to_string(),
"median=" + summary.median.to_string(),
"q1=" + summary.q1.to_string(),
"q3=" + summary.q3.to_string(),
"iqr=" + summary.iqr.to_string(),
"mad=" + summary.mad.to_string(),
"standard_deviation=" + summary.standard_deviation.to_string(),
"skewness=" + summary.skewness.to_string(),
"outlier_count=" + summary.outlier_count.to_string(),
"outlier_fraction=" + summary.outlier_fraction.to_string(),
]
}
///|
pub fn summary_to_string(summary : DatasetSummary) -> String {
summary_to_lines(summary).join("\n")
}
///|
pub fn robust_summary_score(data : Array[Double]) -> Double {
let summary = summarize(data)
if summary.mad == 0.0 {
0.0
} else {
summary.iqr / summary.mad
}
}
///|
pub fn compare_location_estimators(data : Array[Double]) -> Array[Double] {
[
mean(data),
median(data),
trimmed_mean(data, 0.1),
huber_location(data),
tukey_bisquare_location(data).location,
median_of_means(data, 4),
]
}
///|
pub fn estimator_bias_against_clean(
data : Array[Double],
clean_center : Double,
) -> Array[Double] {
let estimates = compare_location_estimators(data)
let result = []
for estimate in estimates {
result.push(estimate - clean_center)
}
result
}
///|
pub fn detect_shift(
reference : Array[Double],
current : Array[Double],
threshold : Double,
) -> Bool {
if threshold <= 0.0 {
abort("threshold must be positive")
}
if reference.length() == 0 || current.length() == 0 {
return false
}
let location_shift = abs_double(median(current) - median(reference))
let scale = mad(reference)
if scale == 0.0 {
location_shift > 0.0
} else {
location_shift / scale > threshold
}
}
///|
pub fn detect_scale_change(
reference : Array[Double],
current : Array[Double],
threshold : Double,
) -> Bool {
if threshold <= 1.0 {
abort("threshold must be greater than one")
}
let reference_scale = mad(reference)
let current_scale = mad(current)
if reference_scale == 0.0 {
current_scale > 0.0
} else {
let ratio = current_scale / reference_scale
ratio > threshold || ratio < 1.0 / threshold
}
}
///|
pub fn robust_signal_quality(data : Array[Double]) -> Double {
if data.length() == 0 {
return 0.0
}
let spread = mad(data)
let total = sum_absolute(data)
if total == 0.0 {
1.0
} else {
1.0 / (1.0 + spread / (total / data.length().to_double()))
}
}
///|
pub fn winsorized_summary(
data : Array[Double],
trim_percent : Double,
) -> DatasetSummary {
summarize(winsorize(data, trim_percent))
}
///|
pub fn robust_confidence_width(
data : Array[Double],
confidence? : Double = 0.95,
) -> Double {
validate_confidence(confidence)
if data.length() == 0 {
return 0.0
}
let interval = bootstrap_median_interval(data, 200, confidence~, seed=202608)
interval.upper - interval.lower
}
///|
pub fn stable_against_single_outlier(
data : Array[Double],
outlier : Double,
) -> Double {
let augmented = copy_array(data)
augmented.push(outlier)
let before = median(data)
let after = median(augmented)
abs_double(after - before)
}
///|
pub fn estimate_reliability(data : Array[Double]) -> Array[Double] {
let summary = summarize(data)
[
summary.mad,
summary.iqr,
summary.standard_deviation,
summary.outlier_fraction,
robust_signal_quality(data),
]
}
///|
pub fn summary_json_fields(summary : DatasetSummary) -> Array[String] {
[
"\"count\":" + summary.count.to_string(),
"\"minimum\":" + summary.minimum.to_string(),
"\"maximum\":" + summary.maximum.to_string(),
"\"mean\":" + summary.mean.to_string(),
"\"median\":" + summary.median.to_string(),
"\"q1\":" + summary.q1.to_string(),
"\"q3\":" + summary.q3.to_string(),
"\"iqr\":" + summary.iqr.to_string(),
"\"mad\":" + summary.mad.to_string(),
"\"standard_deviation\":" + summary.standard_deviation.to_string(),
"\"skewness\":" + summary.skewness.to_string(),
"\"outlier_count\":" + summary.outlier_count.to_string(),
"\"outlier_fraction\":" + summary.outlier_fraction.to_string(),
]
}
///|
pub fn summary_to_json(summary : DatasetSummary) -> String {
let fields = summary_json_fields(summary).join(",")
"{" + fields + "}"
}