///|
/// A record in a multi-athlete or multi-session batch.
pub(all) struct BatchRecord {
subject_id : String
session_id : String
date : String
rmssd : Double
sdnn : Double
mean_hr : Double
quality_ratio : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Cohort-level distribution and quality summary.
pub(all) struct CohortSummary {
record_count : Int
subject_count : Int
rmssd_mean : Double
rmssd_median : Double
rmssd_sd : Double
rmssd_q1 : Double
rmssd_q3 : Double
mean_hr : Double
mean_quality : Double
low_quality_count : Int
strongest_subject : String
weakest_subject : String
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Subject-specific aggregate used in cohort comparison.
pub(all) struct SubjectSummary {
subject_id : String
record_count : Int
rmssd_mean : Double
rmssd_sd : Double
quality_mean : Double
percentile : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Return a stable subject index in an aggregate array.
fn subject_index(subjects : Array[SubjectSummary], subject_id : String) -> Int? {
for i in 0.. Array[SubjectSummary] {
let result = []
for record in records {
match subject_index(result, record.subject_id) {
Some(index) => {
let current = result[index]
let count = current.record_count + 1
let new_mean = (
current.rmssd_mean * current.record_count.to_double() + record.rmssd
) /
count.to_double()
let delta = record.rmssd - current.rmssd_mean
let new_variance = if count <= 1 {
0.0
} else {
(
current.rmssd_sd * current.rmssd_sd * (count - 2).to_double() +
delta * (record.rmssd - new_mean)
) /
(count - 1).to_double()
}
result[index] = {
subject_id: current.subject_id,
record_count: count,
rmssd_mean: new_mean,
rmssd_sd: if new_variance < 0.0 {
0.0
} else {
new_variance.sqrt()
},
quality_mean: (
current.quality_mean * current.record_count.to_double() +
record.quality_ratio
) /
count.to_double(),
percentile: 0.0,
}
}
None =>
result.push({
subject_id: record.subject_id,
record_count: 1,
rmssd_mean: record.rmssd,
rmssd_sd: 0.0,
quality_mean: record.quality_ratio,
percentile: 0.0,
})
}
}
result
}
///|
/// Calculate percentile rank using a cohort of scalar values.
pub fn percentile_rank(values : Array[Double], value : Double) -> Double {
if values.length() == 0 {
return 0.0
}
let mut below = 0
let mut equal = 0
for candidate in values {
if candidate < value {
below += 1
}
if candidate == value {
equal += 1
}
}
(below.to_double() + 0.5 * equal.to_double()) /
values.length().to_double() *
100.0
}
///|
/// Aggregate all records and attach subject percentile ranks.
pub fn summarize_cohort(records : Array[BatchRecord]) -> CohortSummary {
let subjects = summarize_subjects(records)
let rmssd_values = []
let hr_values = []
let qualities = []
let mut low_quality = 0
let mut best_subject = ""
let mut worst_subject = ""
let mut best_value = -1.0
let mut worst_value = 1.0e18
for record in records {
rmssd_values.push(record.rmssd)
hr_values.push(record.mean_hr)
qualities.push(record.quality_ratio)
if record.quality_ratio < 0.75 {
low_quality += 1
}
}
for subject in subjects {
if subject.rmssd_mean > best_value {
best_value = subject.rmssd_mean
best_subject = subject.subject_id
}
if subject.rmssd_mean < worst_value {
worst_value = subject.rmssd_mean
worst_subject = subject.subject_id
}
}
let summary = summarize_distribution(rmssd_values)
{
record_count: records.length(),
subject_count: subjects.length(),
rmssd_mean: summary.mean,
rmssd_median: summary.median,
rmssd_sd: summary.standard_deviation,
rmssd_q1: summary.q1,
rmssd_q3: summary.q3,
mean_hr: mean_value(hr_values),
mean_quality: mean_value(qualities),
low_quality_count: low_quality,
strongest_subject: best_subject,
weakest_subject: worst_subject,
}
}
///|
/// Compare one subject's mean RMSSD with a cohort distribution.
pub fn compare_subject_to_cohort(
subject : SubjectSummary,
cohort : Array[SubjectSummary],
) -> SubjectSummary {
let values = []
for candidate in cohort {
values.push(candidate.rmssd_mean)
}
{
subject_id: subject.subject_id,
record_count: subject.record_count,
rmssd_mean: subject.rmssd_mean,
rmssd_sd: subject.rmssd_sd,
quality_mean: subject.quality_mean,
percentile: percentile_rank(values, subject.rmssd_mean),
}
}
///|
/// Return the number of records that meet a quality threshold.
pub fn count_quality_records(
records : Array[BatchRecord],
threshold : Double,
) -> Int {
let mut count = 0
for record in records {
if record.quality_ratio >= threshold {
count += 1
}
}
count
}
///|
/// Return a deterministic cohort feature vector for dashboards.
pub fn cohort_feature_vector(summary : CohortSummary) -> Array[Double] {
[
summary.record_count.to_double(),
summary.subject_count.to_double(),
summary.rmssd_mean,
summary.rmssd_median,
summary.rmssd_sd,
summary.rmssd_q1,
summary.rmssd_q3,
summary.mean_hr,
summary.mean_quality,
summary.low_quality_count.to_double(),
]
}