///|
/// Advanced time-series state used for reusable decomposition and seasonality diagnostics.
pub struct TimeSeriesPoint {
index : Int
value : Double
trend : Double
seasonal : Double
residual : Double
}
///|
pub struct TimeSeriesSummary {
count : Int
period : Int
trend_strength : Double
seasonal_strength : Double
residual_scale : Double
forecastability : Double
}
///|
pub struct TimeSeriesWindow {
start : Int
end : Int
center : Double
scale : Double
slope : Double
}
///|
fn timeseries_regression_slope(
values : Array[Double],
x_values : Array[Double],
) -> Double {
let x_center = mean(x_values)
let y_center = mean(values)
let mut numerator = 0.0
let mut denominator = 0.0
for index = 0
index < values.length() && index < x_values.length()
index = index + 1 {
let dx = x_values[index] - x_center
numerator += dx * (values[index] - y_center)
denominator += dx * dx
}
if denominator == 0.0 {
0.0
} else {
numerator / denominator
}
}
///|
fn timeseries_component_at(
data : Array[Double],
period : Int,
index : Int,
) -> Double {
let safe_period = if period < 2 { 2 } else { period }
let phase = index % safe_period
let values = []
for cursor = phase; cursor < data.length(); cursor = cursor + safe_period {
values.push(data[cursor])
}
mean(values) - mean(data)
}
///|
pub fn timeseries_points(
data : Array[Double],
period : Int,
window : Int,
) -> Array[TimeSeriesPoint] {
let result = []
let decomposition = seasonal_decompose(data, period, window)
for index = 0; index < data.length(); index = index + 1 {
let trend = if index < decomposition.trend.length() {
decomposition.trend[index]
} else {
data[index]
}
let seasonal = if index < decomposition.seasonal.length() {
decomposition.seasonal[index]
} else {
0.0
}
let residual = data[index] - trend - seasonal
result.push({ index, value: data[index], trend, seasonal, residual })
}
result
}
///|
pub fn timeseries_summary(
data : Array[Double],
period : Int,
window : Int,
) -> TimeSeriesSummary {
let points = timeseries_points(data, period, window)
let trend_values = []
let seasonal_values = []
let residual_values = []
for point in points {
trend_values.push(point.trend)
seasonal_values.push(point.seasonal)
residual_values.push(point.residual)
}
let trend_scale = mad(trend_values)
let seasonal_scale = mad(seasonal_values)
let residual_scale = mad(residual_values)
let total_scale = mad(data)
let safe_total = if total_scale == 0.0 { 1.0 } else { total_scale }
{
count: data.length(),
period: if period < 2 {
2
} else {
period
},
trend_strength: transform_clip(trend_scale / safe_total, 0.0, 1.0),
seasonal_strength: transform_clip(seasonal_scale / safe_total, 0.0, 1.0),
residual_scale,
forecastability: transform_clip(1.0 - residual_scale / safe_total, 0.0, 1.0),
}
}
///|
pub fn timeseries_window_summaries(
data : Array[Double],
window : Int,
) -> Array[TimeSeriesWindow] {
let result = []
let width = if window < 2 { 2 } else { window }
for index = 0; index < data.length(); index = index + 1 {
let start = if index + 1 > width { index + 1 - width } else { 0 }
let values = []
for cursor = start; cursor <= index; cursor = cursor + 1 {
values.push(data[cursor])
}
let slope = if values.length() < 2 {
0.0
} else {
timeseries_regression_slope(values, identity_indices(values.length()))
}
result.push({
start,
end: index,
center: median(values),
scale: mad(values),
slope,
})
}
result
}
///|
fn identity_indices(length : Int) -> Array[Double] {
let result = []
for index = 0; index < length; index = index + 1 {
result.push(index.to_double())
}
result
}
///|
pub fn timeseries_window_centers(
data : Array[Double],
window : Int,
) -> Array[Double] {
let result = []
for item in timeseries_window_summaries(data, window) {
result.push(item.center)
}
result
}
///|
pub fn timeseries_window_scales(
data : Array[Double],
window : Int,
) -> Array[Double] {
let result = []
for item in timeseries_window_summaries(data, window) {
result.push(item.scale)
}
result
}
///|
pub fn timeseries_window_slopes(
data : Array[Double],
window : Int,
) -> Array[Double] {
let result = []
for item in timeseries_window_summaries(data, window) {
result.push(item.slope)
}
result
}
///|
pub fn timeseries_forecast_baseline(
data : Array[Double],
horizon : Int,
period : Int,
window : Int,
) -> Array[Double] {
let summary = timeseries_summary(data, period, window)
let result = []
let count = if horizon < 0 { 0 } else { horizon }
let safe_period = if period < 2 { 2 } else { period }
let last = if data.length() == 0 { 0.0 } else { data[data.length() - 1] }
for step = 0; step < count; step = step + 1 {
let seasonal = if data.length() == 0 {
0.0
} else {
timeseries_component_at(data, safe_period, data.length() + step)
}
result.push(last + seasonal * summary.seasonal_strength)
}
result
}
///|
pub fn timeseries_residuals(
data : Array[Double],
period : Int,
window : Int,
) -> Array[Double] {
let result = []
for point in timeseries_points(data, period, window) {
result.push(point.residual)
}
result
}
///|
pub fn timeseries_trend(
data : Array[Double],
period : Int,
window : Int,
) -> Array[Double] {
let result = []
for point in timeseries_points(data, period, window) {
result.push(point.trend)
}
result
}
///|
pub fn timeseries_seasonal(
data : Array[Double],
period : Int,
window : Int,
) -> Array[Double] {
let result = []
for point in timeseries_points(data, period, window) {
result.push(point.seasonal)
}
result
}
///|
pub fn timeseries_change_points(
data : Array[Double],
window : Int,
threshold : Double,
) -> Array[Int] {
signal_change_indices(signal_change_candidates(data, window, threshold))
}
///|
pub fn timeseries_period_scores(
data : Array[Double],
candidates : Array[Int],
) -> Array[Double] {
let result = []
for period in candidates {
let safe_period = if period < 2 { 2 } else { period }
result.push(abs_double(signal_autocorrelation(data, safe_period)))
}
result
}
///|
pub fn timeseries_best_period(
data : Array[Double],
candidates : Array[Int],
) -> Int {
if candidates.length() == 0 {
return 1
}
let scores = timeseries_period_scores(data, candidates)
let mut best = 0
for index = 1; index < scores.length(); index = index + 1 {
if scores[index] > scores[best] {
best = index
}
}
candidates[best]
}
///|
pub fn timeseries_point_vector(point : TimeSeriesPoint) -> Array[Double] {
[
point.index.to_double(),
point.value,
point.trend,
point.seasonal,
point.residual,
]
}
///|
pub fn timeseries_summary_vector(summary : TimeSeriesSummary) -> Array[Double] {
[
summary.count.to_double(),
summary.period.to_double(),
summary.trend_strength,
summary.seasonal_strength,
summary.residual_scale,
summary.forecastability,
]
}
///|
pub fn timeseries_summary_lines(summary : TimeSeriesSummary) -> Array[String] {
[
"count=" + summary.count.to_string(),
"period=" + summary.period.to_string(),
"trend_strength=" + summary.trend_strength.to_string(),
"seasonal_strength=" + summary.seasonal_strength.to_string(),
"residual_scale=" + summary.residual_scale.to_string(),
"forecastability=" + summary.forecastability.to_string(),
]
}
///|
pub fn timeseries_summary_string(summary : TimeSeriesSummary) -> String {
timeseries_summary_lines(summary).join("\n")
}
///|
pub fn timeseries_quality(
data : Array[Double],
period : Int,
window : Int,
) -> Double {
timeseries_summary(data, period, window).forecastability
}
///|
pub fn timeseries_reconstruct(
data : Array[Double],
period : Int,
window : Int,
) -> Array[Double] {
let result = []
for point in timeseries_points(data, period, window) {
result.push(point.trend + point.seasonal + point.residual)
}
result
}
///|
pub fn timeseries_reconstruction_error(
data : Array[Double],
period : Int,
window : Int,
) -> Double {
mean_absolute_error(data, timeseries_reconstruct(data, period, window))
}
///|
pub fn timeseries_summary_batch(
data_sets : Array[Array[Double]],
period : Int,
window : Int,
) -> Array[Double] {
let result = []
for data in data_sets {
result.push(timeseries_quality(data, period, window))
}
result
}