///|
/// A column-oriented feature matrix for one numeric signal.
pub struct FeatureFrame {
source : Array[Double]
columns : Array[Array[Double]]
names : Array[String]
row_count : Int
}
///|
/// Configuration for deterministic feature construction.
pub struct FeatureConfig {
lags : Array[Int]
windows : Array[Int]
include_differences : Bool
include_ratios : Bool
include_robust_scale : Bool
}
///|
pub fn feature_default_config() -> FeatureConfig {
{
lags: [1, 2, 7],
windows: [3, 7, 14],
include_differences: true,
include_ratios: true,
include_robust_scale: true,
}
}
///|
pub fn feature_config(
lags : Array[Int],
windows : Array[Int],
include_differences : Bool,
include_ratios : Bool,
include_robust_scale : Bool,
) -> FeatureConfig {
{ lags, windows, include_differences, include_ratios, include_robust_scale }
}
///|
pub fn feature_lag(
data : Array[Double],
lag : Int,
fill : Double,
) -> Array[Double] {
let result = []
let distance = if lag < 0 { 0 } else { lag }
for index = 0; index < data.length(); index = index + 1 {
if index < distance {
result.push(fill)
} else {
result.push(data[index - distance])
}
}
result
}
///|
pub fn feature_lead(
data : Array[Double],
lead : Int,
fill : Double,
) -> Array[Double] {
let result = []
let distance = if lead < 0 { 0 } else { lead }
for index = 0; index < data.length(); index = index + 1 {
let source = index + distance
if source >= data.length() {
result.push(fill)
} else {
result.push(data[source])
}
}
result
}
///|
pub fn feature_difference(data : Array[Double], lag : Int) -> Array[Double] {
let result = []
let distance = if lag <= 0 { 1 } else { lag }
for index = 0; index < data.length(); index = index + 1 {
if index < distance {
result.push(0.0)
} else {
result.push(data[index] - data[index - distance])
}
}
result
}
///|
pub fn feature_absolute_difference(
data : Array[Double],
lag : Int,
) -> Array[Double] {
let differences = feature_difference(data, lag)
let result = []
for value in differences {
result.push(abs_double(value))
}
result
}
///|
pub fn feature_second_difference(
data : Array[Double],
lag : Int,
) -> Array[Double] {
feature_difference(feature_difference(data, lag), lag)
}
///|
pub fn feature_percent_change(data : Array[Double], lag : Int) -> Array[Double] {
let result = []
let distance = if lag <= 0 { 1 } else { lag }
for index = 0; index < data.length(); index = index + 1 {
if index < distance || abs_double(data[index - distance]) <= 1.0e-12 {
result.push(0.0)
} else {
result.push(
(data[index] - data[index - distance]) /
abs_double(data[index - distance]),
)
}
}
result
}
///|
pub fn feature_log_change(data : Array[Double], lag : Int) -> Array[Double] {
let result = []
let distance = if lag <= 0 { 1 } else { lag }
for index = 0; index < data.length(); index = index + 1 {
if index < distance || data[index] <= 0.0 || data[index - distance] <= 0.0 {
result.push(0.0)
} else {
result.push(drift_log(data[index]) - drift_log(data[index - distance]))
}
}
result
}
///|
pub fn feature_ratio(
data : Array[Double],
lag : Int,
fallback : Double,
) -> Array[Double] {
let result = []
let distance = if lag <= 0 { 1 } else { lag }
for index = 0; index < data.length(); index = index + 1 {
if index < distance || abs_double(data[index - distance]) <= 1.0e-12 {
result.push(fallback)
} else {
result.push(data[index] / data[index - distance])
}
}
result
}
///|
pub fn feature_rolling_mean(
data : Array[Double],
window : Int,
) -> Array[Double] {
rolling_mean(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_rolling_median(
data : Array[Double],
window : Int,
) -> Array[Double] {
rolling_median(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_rolling_mad(data : Array[Double], window : Int) -> Array[Double] {
rolling_mad(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_rolling_iqr(data : Array[Double], window : Int) -> Array[Double] {
rolling_iqr(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_rolling_min(data : Array[Double], window : Int) -> Array[Double] {
rolling_min(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_rolling_max(data : Array[Double], window : Int) -> Array[Double] {
rolling_max(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_rolling_range(
data : Array[Double],
window : Int,
) -> Array[Double] {
rolling_range(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_rolling_slope(
data : Array[Double],
window : Int,
) -> Array[Double] {
let result = []
let width = if window <= 1 { 2 } else { window }
for index = 0; index < data.length(); index = index + 1 {
let start = if index + 1 < width { 0 } else { index + 1 - width }
let values = []
let coordinates = []
for cursor = start; cursor <= index; cursor = cursor + 1 {
values.push(data[cursor])
coordinates.push((cursor - start).to_double())
}
if values.length() < 2 {
result.push(0.0)
} else {
result.push(linear_regression(coordinates, values).slope)
}
}
result
}
///|
pub fn feature_rolling_signal_to_noise(
data : Array[Double],
window : Int,
) -> Array[Double] {
robust_moving_signal_to_noise(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_rolling_z(data : Array[Double], window : Int) -> Array[Double] {
rolling_z_scores(data, if window <= 0 { 1 } else { window })
}
///|
pub fn feature_robust_standardize(data : Array[Double]) -> Array[Double] {
robust_standardize(data)
}
///|
pub fn feature_rank_normalize(data : Array[Double]) -> Array[Double] {
rank_normalize(data)
}
///|
pub fn feature_min_max(
data : Array[Double],
lower : Double,
upper : Double,
) -> Array[Double] {
robust_min_max_scale(data, lower~, upper~)
}
///|
pub fn feature_clip(
data : Array[Double],
lower : Double,
upper : Double,
) -> Array[Double] {
clip_range(data, lower, upper)
}
///|
pub fn feature_centered(data : Array[Double]) -> Array[Double] {
center_by_median(data)
}
///|
pub fn feature_residual_from_rolling(
data : Array[Double],
window : Int,
) -> Array[Double] {
let baseline = feature_rolling_median(data, window)
difference_from_baseline(data, median(baseline))
}
///|
pub fn feature_deviation_from_level(
data : Array[Double],
level : Double,
) -> Array[Double] {
let result = []
for value in data {
result.push(value - level)
}
result
}
///|
pub fn feature_abs_deviation_from_level(
data : Array[Double],
level : Double,
) -> Array[Double] {
let result = []
for value in data {
result.push(abs_double(value - level))
}
result
}
///|
pub fn feature_sign(data : Array[Double]) -> Array[Double] {
let result = []
for value in data {
result.push(sign_double(value))
}
result
}
///|
pub fn feature_is_positive(data : Array[Double]) -> Array[Bool] {
let result = []
for value in data {
result.push(value > 0.0)
}
result
}
///|
pub fn feature_is_zero(data : Array[Double], tolerance : Double) -> Array[Bool] {
let result = []
let limit = if tolerance < 0.0 { -tolerance } else { tolerance }
for value in data {
result.push(abs_double(value) <= limit)
}
result
}
///|
pub fn feature_threshold_distance(
data : Array[Double],
threshold : Double,
) -> Array[Double] {
let result = []
for value in data {
result.push(value - threshold)
}
result
}
///|
pub fn feature_threshold_flag(
data : Array[Double],
lower : Double,
upper : Double,
) -> Array[Bool] {
let result = []
for value in data {
result.push(value < lower || value > upper)
}
result
}
///|
pub fn feature_quantile_distance(
data : Array[Double],
probability : Double,
) -> Array[Double] {
let level = quantile(data, probability)
feature_deviation_from_level(data, level)
}
///|
pub fn feature_quantile_flag(
data : Array[Double],
probability : Double,
multiplier : Double,
) -> Array[Bool] {
let level = quantile(data, probability)
let scale = mad(data)
let result = []
for value in data {
result.push(abs_double(value - level) > multiplier * scale)
}
result
}
///|
pub fn feature_autocorrelation(data : Array[Double], lag : Int) -> Double {
robust_autocorrelation(data, if lag <= 0 { 1 } else { lag })
}
///|
pub fn feature_autocorrelation_series(
data : Array[Double],
max_lag : Int,
) -> Array[Double] {
autocorrelation_series(data, if max_lag < 0 { 0 } else { max_lag })
}
///|
pub fn feature_energy(data : Array[Double]) -> Double {
sum_squared(data)
}
///|
pub fn feature_absolute_energy(data : Array[Double]) -> Double {
sum_absolute(data)
}
///|
pub fn feature_mean_abs_change(data : Array[Double]) -> Double {
mean_absolute_error(data, feature_lag(data, 1, 0.0))
}
///|
pub fn feature_median_abs_change(data : Array[Double]) -> Double {
median_absolute_error(data, feature_lag(data, 1, 0.0))
}
///|
pub fn feature_total_variation(data : Array[Double]) -> Double {
total_variation(data)
}
///|
pub fn feature_range(data : Array[Double]) -> Double {
range(data)
}
///|
pub fn feature_iqr(data : Array[Double]) -> Double {
interquartile_range(data)
}
///|
pub fn feature_mad(data : Array[Double]) -> Double {
mad(data)
}
///|
pub fn feature_skewness(data : Array[Double]) -> Double {
skewness(data)
}
///|
pub fn feature_kurtosis(data : Array[Double]) -> Double {
excess_kurtosis(data)
}
///|
pub fn feature_entropy(data : Array[Double], bins : Int) -> Double {
let snapshot = drift_snapshot_with_bins(data, data, bins)
let mut result = 0.0
for probability in snapshot.probabilities {
if probability > 0.0 {
result -= probability * drift_log(probability)
}
}
result
}
///|
pub fn feature_missing_fraction(data : Array[Double]) -> Double {
if data.length() == 0 {
0.0
} else {
quality_missing_indices(data).length().to_double() /
data.length().to_double()
}
}
///|
pub fn feature_duplicate_fraction(data : Array[Double]) -> Double {
duplicate_fraction(data)
}
///|
pub fn feature_outlier_fraction(
data : Array[Double],
threshold : Double,
) -> Double {
outlier_fraction(outlier_indices_z(data, threshold~), data.length())
}
///|
pub fn feature_quality_score(
data : Array[Double],
rule : QualityRule,
) -> Double {
quality_report(data, rule).quality_score
}
///|
pub fn feature_frame(
data : Array[Double],
config : FeatureConfig,
) -> FeatureFrame {
let columns = []
let names = []
for lag in config.lags {
columns.push(feature_lag(data, lag, 0.0))
names.push("lag_" + lag.to_string())
}
for window in config.windows {
columns.push(feature_rolling_median(data, window))
names.push("rolling_median_" + window.to_string())
columns.push(feature_rolling_mad(data, window))
names.push("rolling_mad_" + window.to_string())
columns.push(feature_rolling_slope(data, window))
names.push("rolling_slope_" + window.to_string())
}
if config.include_differences {
for lag in config.lags {
columns.push(feature_difference(data, lag))
names.push("difference_" + lag.to_string())
}
}
if config.include_ratios {
for lag in config.lags {
columns.push(feature_ratio(data, lag, 1.0))
names.push("ratio_" + lag.to_string())
}
}
if config.include_robust_scale {
columns.push(feature_robust_standardize(data))
names.push("robust_standardized")
}
{ source: data, columns, names, row_count: data.length() }
}
///|
pub fn FeatureFrame::column_count(self : FeatureFrame) -> Int {
self.columns.length()
}
///|
pub fn FeatureFrame::row_count(self : FeatureFrame) -> Int {
self.row_count
}
///|
pub fn FeatureFrame::names(self : FeatureFrame) -> Array[String] {
self.names.copy()
}
///|
pub fn FeatureFrame::source(self : FeatureFrame) -> Array[Double] {
self.source.copy()
}
///|
pub fn FeatureFrame::column(self : FeatureFrame, index : Int) -> Array[Double] {
if index < 0 || index >= self.columns.length() {
[]
} else {
self.columns[index].copy()
}
}
///|
pub fn FeatureFrame::column_by_name(
self : FeatureFrame,
name : String,
) -> Array[Double] {
for index = 0; index < self.names.length(); index = index + 1 {
if self.names[index] == name {
return self.columns[index].copy()
}
}
[]
}
///|
pub fn FeatureFrame::row(self : FeatureFrame, index : Int) -> Array[Double] {
let result = []
if index < 0 || index >= self.row_count {
return result
}
for column in self.columns {
result.push(column[index])
}
result
}
///|
pub fn FeatureFrame::column_means(self : FeatureFrame) -> Array[Double] {
let result = []
for column in self.columns {
result.push(mean(column))
}
result
}
///|
pub fn FeatureFrame::column_mads(self : FeatureFrame) -> Array[Double] {
let result = []
for column in self.columns {
result.push(mad(column))
}
result
}
///|
pub fn FeatureFrame::quality(
self : FeatureFrame,
rule : QualityRule,
) -> Array[Double] {
let result = []
for column in self.columns {
result.push(quality_report(column, rule).quality_score)
}
result
}
///|
pub fn FeatureFrame::standardize(self : FeatureFrame) -> Array[Array[Double]] {
let result = []
for column in self.columns {
result.push(robust_standardize(column))
}
result
}
///|
pub fn feature_flatten(frame : FeatureFrame) -> Array[Double] {
let result = []
for column in frame.columns {
for value in column {
result.push(value)
}
}
result
}
///|
pub fn feature_column_ranges(frame : FeatureFrame) -> Array[Double] {
let result = []
for column in frame.columns {
result.push(range(column))
}
result
}
///|
pub fn feature_column_scores(frame : FeatureFrame) -> Array[Double] {
let result = []
for column in frame.columns {
result.push(robust_summary_score(column))
}
result
}
///|
pub fn feature_frame_score(frame : FeatureFrame) -> Double {
if frame.columns.length() == 0 {
0.0
} else {
mean(feature_column_scores(frame))
}
}