///|
pub struct Sample {
timestamp : Float
value : Float
} derive(Debug, Eq)
///|
pub fn sample(timestamp : Float, value : Float) -> Sample {
{ timestamp, value }
}
///|
pub fn Sample::is_finite(self : Sample) -> Bool {
self.value == self.value && self.timestamp == self.timestamp
}
///|
pub struct TimeSeries {
name : String
samples : Array[Sample]
} derive(Debug, Eq)
///|
pub fn time_series(name : String, samples : Array[Sample]) -> TimeSeries {
{ name, samples }
}
///|
pub fn TimeSeries::length(self : TimeSeries) -> Int {
self.samples.length()
}
///|
pub fn TimeSeries::at(self : TimeSeries, index : Int) -> Sample? {
self.samples.get(index)
}
///|
pub fn TimeSeries::timestamps(self : TimeSeries) -> Array[Float] {
self.samples.map(fn(item) { item.timestamp })
}
///|
pub fn TimeSeries::values(self : TimeSeries) -> Array[Float] {
self.samples.map(fn(item) { item.value })
}
///|
pub fn TimeSeries::summary(self : TimeSeries) -> Statistics {
summarize(self.values())
}
///|
pub fn TimeSeries::append(self : TimeSeries, point : Sample) -> TimeSeries {
let samples = self.samples.copy()
samples.push(point)
{ ..self, samples, }
}
///|
pub fn TimeSeries::valid(self : TimeSeries) -> TimeSeries {
let samples : Array[Sample] = []
for point in self.samples {
if point.is_finite() {
samples.push(point)
}
}
{ ..self, samples, }
}
///|
pub fn TimeSeries::between(
self : TimeSeries,
start : Float,
stop : Float,
) -> TimeSeries {
let samples : Array[Sample] = []
for point in self.samples {
if point.timestamp >= start && point.timestamp <= stop {
samples.push(point)
}
}
{ ..self, samples, }
}
///|
pub fn TimeSeries::outliers(
self : TimeSeries,
threshold : Float,
) -> Array[Sample] {
let result : Array[Sample] = []
let stats = self.summary()
let limit = threshold.abs() * stats.standard_deviation
for point in self.samples {
if stats.count > 1 && (point.value - stats.mean).abs() > limit {
result.push(point)
}
}
result
}
///|
pub fn TimeSeries::normal(
self : TimeSeries,
threshold : Float,
) -> Array[Sample] {
let result : Array[Sample] = []
let outliers = self.outliers(threshold)
for point in self.samples {
if !outliers.contains(point) {
result.push(point)
}
}
result
}
///|
pub fn TimeSeries::moving_average(
self : TimeSeries,
window : Int,
) -> TimeSeries {
let averages = moving_average(self.values(), window)
let samples : Array[Sample] = []
for index, point in self.samples {
samples.push({ timestamp: point.timestamp, value: averages[index] })
}
{ ..self, samples, }
}
///|
pub fn TimeSeries::ewma(self : TimeSeries, alpha : Float) -> TimeSeries {
let samples : Array[Sample] = []
let safe_alpha = clamp(alpha, 0.0, 1.0)
let mut previous : Float = 0.0
for index, point in self.samples {
if index == 0 {
previous = point.value
} else {
previous = safe_alpha * point.value + (1.0 - safe_alpha) * previous
}
samples.push({ timestamp: point.timestamp, value: previous })
}
{ ..self, samples, }
}
///|
pub fn TimeSeries::first_derivative(self : TimeSeries) -> TimeSeries {
let samples : Array[Sample] = []
for index, point in self.samples {
if index == 0 {
samples.push({ timestamp: point.timestamp, value: 0.0 })
} else {
let previous = self.samples[index - 1]
let delta_time = point.timestamp - previous.timestamp
let slope : Float = if delta_time == 0.0 {
0.0
} else {
(point.value - previous.value) / delta_time
}
samples.push({ timestamp: point.timestamp, value: slope })
}
}
{ ..self, samples, }
}
///|
pub fn TimeSeries::trend(self : TimeSeries) -> Float {
if self.samples.length() < 2 {
0.0
} else {
let first = self.samples[0]
let last = self.samples[self.samples.length() - 1]
let delta = last.timestamp - first.timestamp
if delta == 0.0 {
0.0
} else {
(last.value - first.value) / delta
}
}
}
///|
pub fn TimeSeries::range(self : TimeSeries) -> Float {
let stats = self.summary()
stats.maximum - stats.minimum
}
///|
pub fn TimeSeries::integral(self : TimeSeries) -> Float {
let mut total : Float = 0.0
for index, point in self.samples {
if index > 0 {
let previous = self.samples[index - 1]
total = total +
(point.value + previous.value) /
2.0 *
(point.timestamp - previous.timestamp)
}
}
total
}
///|
pub fn TimeSeries::time_weighted_mean(self : TimeSeries) -> Float {
let duration : Float = if self.samples.length() < 2 {
0.0
} else {
self.samples[self.samples.length() - 1].timestamp -
self.samples[0].timestamp
}
if duration == 0.0 {
0.0
} else {
self.integral() / duration
}
}
///|
pub fn TimeSeries::interpolate(self : TimeSeries, timestamp : Float) -> Float? {
if self.samples.length() == 0 {
None
} else if timestamp <= self.samples[0].timestamp {
Some(self.samples[0].value)
} else if timestamp >= self.samples[self.samples.length() - 1].timestamp {
Some(self.samples[self.samples.length() - 1].value)
} else {
for index in 1.. TimeSeries {
let samples : Array[Sample] = []
for timestamp in timestamps {
match self.interpolate(timestamp) {
Some(value) => samples.push(sample(timestamp, value))
None => ()
}
}
{ ..self, samples, }
}
///|
pub struct TrendSummary {
slope : Float
intercept : Float
r_squared : Float
direction : String
} derive(Debug, Eq)
///|
pub fn trend_summary(series : TimeSeries) -> TrendSummary {
let n = series.samples.length()
if n < 2 {
{ slope: 0.0, intercept: 0.0, r_squared: 0.0, direction: "flat" }
} else {
let x = series.timestamps()
let y = series.values()
let x_stats = summarize(x)
let y_stats = summarize(y)
let mut numerator : Float = 0.0
let mut denominator : Float = 0.0
for index, value in x {
let dx = value - x_stats.mean
numerator = numerator + dx * (y[index] - y_stats.mean)
denominator = denominator + dx * dx
}
let slope : Float = if denominator == 0.0 {
0.0
} else {
numerator / denominator
}
let intercept : Float = y_stats.mean - slope * x_stats.mean
let direction = if slope > 0.000001 {
"rising"
} else if slope < -0.000001 {
"falling"
} else {
"flat"
}
{ slope, intercept, r_squared: correlation_squared(x, y), direction }
}
}
///|
pub fn correlation_squared(left : Array[Float], right : Array[Float]) -> Float {
if left.length() == 0 || left.length() != right.length() {
0.0
} else {
let left_stats = summarize(left)
let right_stats = summarize(right)
let mut numerator : Float = 0.0
let mut left_square : Float = 0.0
let mut right_square : Float = 0.0
for index, value in left {
let left_delta = value - left_stats.mean
let right_delta = right[index] - right_stats.mean
numerator = numerator + left_delta * right_delta
left_square = left_square + left_delta * left_delta
right_square = right_square + right_delta * right_delta
}
if left_square == 0.0 || right_square == 0.0 {
0.0
} else {
let correlation = numerator / (left_square * right_square).sqrt()
correlation * correlation
}
}
}
///|
pub fn TimeSeries::trend_report(self : TimeSeries) -> ReportSection {
let summary = trend_summary(self)
{
title: self.name,
kind: Method,
body: "direction=\{summary.direction}, slope=\{summary.slope}, r_squared=\{summary.r_squared}",
}
}
///|
pub fn TimeSeries::to_table(self : TimeSeries) -> ReportTable {
let rows : Array[Array[String]] = []
for point in self.samples {
rows.push(["\{point.timestamp}", "\{point.value}"])
}
table(["timestamp", self.name], rows)
}
///|
pub fn TimeSeries::to_markdown(self : TimeSeries) -> String {
self.to_table().to_markdown()
}
///|
pub fn TimeSeries::quality_gate(
self : TimeSeries,
minimum_count : Int,
maximum_outliers : Int,
threshold : Float,
) -> Bool {
self.valid().length() >= minimum_count &&
self.outliers(threshold).length() <= maximum_outliers
}