///|
/// Robust trend decomposition for longitudinal HRV series.
///|
/// Trend decomposition components.
pub(all) struct TrendDecomposition {
original : Array[Double]
baseline : Array[Double]
residual : Array[Double]
slope : Double
intercept : Double
turning_points : Int
explained_variance : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Estimate a rolling baseline with a centered median window.
pub fn trend_baseline(
values : Array[Double],
window_size : Int,
) -> Array[Double] {
rolling_median(values, if window_size < 1 { 1 } else { window_size })
}
///|
/// Count direction changes in a residual sequence.
pub fn count_turning_points(values : Array[Double]) -> Int {
if values.length() <= 2 {
return 0
}
let mut count = 0
let mut previous = values[1] - values[0]
for i in 2.. Double {
if values.length() == 0 {
return 0.0
}
let mean = mean_value(values)
let mut total = 0.0
let mut residual = 0.0
for i in 0.. TrendDecomposition {
let baseline = trend_baseline(values, window_size)
let residual = []
let n = if values.length() < baseline.length() {
values.length()
} else {
baseline.length()
}
for i in 0.. Array[Double] {
if values.length() == 0 {
return []
}
let weight = alpha.clamp(min=0.0, max=1.0)
let result = [values[0]]
let mut current = values[0]
for i in 1.. Array[Int] {
let result = []
let scale = median_absolute_deviation(residual) * 1.4826
let limit = if threshold < 0.0 { 0.0 } else { threshold }
let bound = if scale < 1.0 { limit } else { scale * limit }
for i in 0.. bound {
result.push(i)
}
}
result
}
///|
/// Return the signed change between the endpoints of a series.
pub fn endpoint_change(values : Array[Double]) -> Double {
if values.length() <= 1 {
0.0
} else {
values[values.length() - 1] - values[0]
}
}
///|
/// Calculate a trailing slope for every complete window.
pub fn rolling_trend_slopes(
values : Array[Double],
window_size : Int,
) -> Array[Double] {
if window_size <= 1 || values.length() < window_size {
return []
}
let result = []
for start in 0..<(values.length() - window_size + 1) {
let window = []
for i in start..<(start + window_size) {
window.push(values[i])
}
result.push(fit_linear_trend(window).slope)
}
result
}
///|
/// Return a trend feature vector.
pub fn trend_feature_vector(
decomposition : TrendDecomposition,
) -> Array[Double] {
[
decomposition.slope,
decomposition.intercept,
decomposition.turning_points.to_double(),
decomposition.explained_variance,
endpoint_change(decomposition.original),
standard_deviation(decomposition.residual),
mean_value(decomposition.baseline),
]
}
///|
/// Return whether the decomposition has aligned finite arrays.
pub fn trend_decomposition_is_usable(
decomposition : TrendDecomposition,
) -> Bool {
decomposition.original.length() == decomposition.baseline.length() &&
decomposition.baseline.length() == decomposition.residual.length()
}