///|
/// Reference-range helpers for population and personal comparisons.
///|
/// A reference range with a label and sample provenance.
pub(all) struct ReferenceRange {
name : String
lower : Double
upper : Double
center : Double
sample_count : Int
source : String
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Position of a value within a reference range.
pub(all) struct RangePosition {
value : Double
percentile : Double
z_score : Double
band : String
in_range : Bool
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Build a robust reference range from observed values.
pub fn make_reference_range(
values : Array[Double],
name : String,
source : String,
) -> ReferenceRange {
let distribution = summarize_distribution(values)
{
name,
lower: distribution.q1 - 1.5 * (distribution.q3 - distribution.q1),
upper: distribution.q3 + 1.5 * (distribution.q3 - distribution.q1),
center: distribution.median,
sample_count: values.length(),
source,
}
}
///|
/// Calculate a range position using a supplied standard deviation.
pub fn position_in_reference_range(
value : Double,
range : ReferenceRange,
standard_deviation_value : Double,
population_values : Array[Double],
) -> RangePosition {
let scale = if standard_deviation_value <= 0.0 {
1.0
} else {
standard_deviation_value
}
let z = (value - range.center) / scale
let percentile = percentile_rank(population_values, value)
let band = if value < range.lower {
"below"
} else if value > range.upper {
"above"
} else if value < range.center {
"lower_half"
} else {
"upper_half"
}
{
value,
percentile,
z_score: z,
band,
in_range: value >= range.lower && value <= range.upper,
}
}
///|
/// Return a normalized distance from the center of a range.
pub fn range_center_distance(value : Double, range : ReferenceRange) -> Double {
let width = range.upper - range.lower
if width <= 0.0 {
0.0
} else {
(value - range.center) / width
}
}
///|
/// Clamp a value into a reference range.
pub fn clamp_to_reference_range(
value : Double,
range : ReferenceRange,
) -> Double {
if value < range.lower {
range.lower
} else if value > range.upper {
range.upper
} else {
value
}
}
///|
/// Calculate a robust personal reference range from a history.
pub fn personal_rmssd_range(
history : Array[MorningMeasurement],
) -> ReferenceRange {
let values = []
for measurement in history {
if measurement.rmssd > 0.0 {
values.push(measurement.rmssd)
}
}
make_reference_range(values, "personal_rmssd", "morning_history")
}
///|
/// Count observations inside a reference range.
pub fn count_in_reference_range(
values : Array[Double],
range : ReferenceRange,
) -> Int {
let mut count = 0
for value in values {
if value >= range.lower && value <= range.upper {
count += 1
}
}
count
}
///|
/// Calculate the coverage ratio of a range.
pub fn reference_range_coverage(
values : Array[Double],
range : ReferenceRange,
) -> Double {
if values.length() == 0 {
0.0
} else {
count_in_reference_range(values, range).to_double() /
values.length().to_double()
}
}
///|
/// Return the widest of several reference ranges.
pub fn merge_reference_ranges(
ranges : Array[ReferenceRange],
) -> ReferenceRange? {
if ranges.length() == 0 {
return None
}
let mut lower = ranges[0].lower
let mut upper = ranges[0].upper
let mut center = ranges[0].center
let mut sample_count = 0
let mut source = ranges[0].source
for range in ranges {
if range.lower < lower {
lower = range.lower
}
if range.upper > upper {
upper = range.upper
}
center += range.center
sample_count += range.sample_count
source = source + "+" + range.source
}
Some({
name: ranges[0].name,
lower,
upper,
center: center / ranges.length().to_double(),
sample_count,
source,
})
}
///|
/// Create a range feature vector.
pub fn reference_range_feature_vector(
position : RangePosition,
) -> Array[Double] {
[
position.value,
position.percentile,
position.z_score,
if position.in_range {
1.0
} else {
0.0
},
]
}
///|
/// Return a plain-language range interpretation.
pub fn reference_range_interpretation(position : RangePosition) -> String {
if position.band == "below" {
"value is below the reference range"
} else if position.band == "above" {
"value is above the reference range"
} else {
"value is inside the reference range"
}
}