use arrow::array::ArrayRef;
use arrow::datatypes::{DataType, Field};
use datafusion::common::Result as DFResult;
use super::window::{
PartitionKernel, column_f64, constant_f64, constant_usize, exec_err, f64_array, on_positional,
on_positional_multi, optional_bool,
};
macro_rules! kernel {
(
$(#[$meta:meta])*
$name:ident, $sql:literal, [$($ty:expr),* $(,)?]
$(, optional: [$($opt:expr),* $(,)?])?
$(, returns: $ret:expr)?
, |$args:ident, $rows:ident| $body:block
) => {
$(#[$meta])*
#[derive(Debug)]
pub(super) struct $name;
impl PartitionKernel for $name {
const NAME: &'static str = $sql;
fn arg_types() -> Vec<DataType> {
vec![$($ty),*]
}
$(fn optional_arg_types() -> Vec<Vec<DataType>> {
let mut with_extra = Self::arg_types();
with_extra.extend([$($opt),*]);
vec![with_extra]
})?
$(fn return_type() -> DataType { $ret })?
fn evaluate($args: &[ArrayRef], $rows: usize) -> DFResult<ArrayRef> $body
}
};
}
kernel! {
DiffKernel, "diff", [DataType::Float64, DataType::Int64],
|args, _rows| {
let values = column_f64(args, 0)?;
let order = constant_usize(args, 1, "diff order")?;
Ok(f64_array(chronix_analytics::preprocess::diff(&values, order)))
}
}
kernel! {
PctChangeKernel, "pct_change", [DataType::Float64],
|args, _rows| {
let values = column_f64(args, 0)?;
Ok(f64_array(chronix_analytics::preprocess::pct_change(&values)))
}
}
kernel! {
RollingMeanKernel, "rolling_mean", [DataType::Float64, DataType::Int64],
|args, _rows| {
let values = column_f64(args, 0)?;
let window = constant_usize(args, 1, "rolling_mean window")?;
Ok(f64_array(chronix_analytics::preprocess::rolling_mean(&values, window)))
}
}
kernel! {
RollingStdKernel, "rolling_std", [DataType::Float64, DataType::Int64],
|args, _rows| {
let values = column_f64(args, 0)?;
let window = constant_usize(args, 1, "rolling_std window")?;
Ok(f64_array(chronix_analytics::preprocess::rolling_std(&values, window)))
}
}
kernel! {
RollingCorrKernel, "rolling_corr",
[DataType::Float64, DataType::Float64, DataType::Int64],
|args, _rows| {
let a = column_f64(args, 0)?;
let b = column_f64(args, 1)?;
let window = constant_usize(args, 2, "rolling_corr window")?;
Ok(f64_array(chronix_analytics::preprocess::rolling_corr(&a, &b, window)))
}
}
kernel! {
ZscoreKernel, "zscore", [DataType::Float64],
|args, _rows| {
let values = column_f64(args, 0)?;
Ok(f64_array(chronix_analytics::preprocess::zscore(&values)))
}
}
kernel! {
EwmKernel, "ewm", [DataType::Float64, DataType::Float64],
|args, _rows| {
let values = column_f64(args, 0)?;
let alpha = constant_f64(args, 1, "ewm alpha")?;
if !(alpha > 0.0 && alpha <= 1.0) {
return Err(datafusion::common::DataFusionError::Plan(format!(
"ewm alpha must be in (0, 1], got {alpha}"
)));
}
Ok(f64_array(chronix_analytics::preprocess::ewm(&values, alpha)))
}
}
#[derive(Debug, Clone, Copy)]
enum StlPart {
Trend,
Seasonal,
Residual,
}
fn stl_config(
period: usize,
args: &[ArrayRef],
robust_at: usize,
) -> DFResult<chronix_analytics::preprocess::StlConfig> {
use chronix_analytics::preprocess::StlConfig;
let cfg = StlConfig::new(period);
Ok(if optional_bool(args, robust_at, "stl robust")? {
cfg.robust()
} else {
cfg
})
}
fn stl_component(args: &[ArrayRef], part: StlPart) -> DFResult<ArrayRef> {
use chronix_analytics::preprocess::stl_decompose;
let values = column_f64(args, 0)?;
let period = constant_usize(args, 1, "stl period")?;
let cfg = stl_config(period, args, 2)?;
let out = on_positional(&values, |dense| {
let d = stl_decompose(dense, &cfg).map_err(exec_err)?;
Ok(match part {
StlPart::Trend => d.trend,
StlPart::Seasonal => d.seasonal,
StlPart::Residual => d.residual,
})
})?;
Ok(f64_array(out))
}
kernel! {
StlTrendKernel, "stl_trend", [DataType::Float64, DataType::Int64],
optional: [DataType::Boolean],
|args, _rows| { stl_component(args, StlPart::Trend) }
}
kernel! {
StlSeasonalKernel, "stl_seasonal", [DataType::Float64, DataType::Int64],
optional: [DataType::Boolean],
|args, _rows| { stl_component(args, StlPart::Seasonal) }
}
kernel! {
StlResidualKernel, "stl_residual", [DataType::Float64, DataType::Int64],
optional: [DataType::Boolean],
|args, _rows| { stl_component(args, StlPart::Residual) }
}
fn stl_struct_fields() -> arrow::datatypes::Fields {
vec![
Field::new("trend", DataType::Float64, true),
Field::new("seasonal", DataType::Float64, true),
Field::new("residual", DataType::Float64, true),
]
.into()
}
kernel! {
StlDecomposeKernel, "stl_decompose", [DataType::Float64, DataType::Int64],
optional: [DataType::Boolean],
returns: DataType::Struct(stl_struct_fields()),
|args, _rows| {
use std::sync::Arc;
use arrow::array::StructArray;
use chronix_analytics::preprocess::stl_decompose;
let values = column_f64(args, 0)?;
let period = constant_usize(args, 1, "stl period")?;
if period < 2 {
return Err(datafusion::common::DataFusionError::Plan(
"stl_decompose: period must be at least 2".into(),
));
}
let cfg = stl_config(period, args, 2)?;
let mut parts = on_positional_multi(&values, |dense| {
if dense.len() < period * 2 {
return Err(datafusion::common::DataFusionError::Plan(format!(
"stl_decompose: need at least 2*period ({}) rows between the first \
and last non-null value, got {}",
period * 2,
dense.len()
)));
}
let d = stl_decompose(dense, &cfg).map_err(exec_err)?;
Ok(vec![d.trend, d.seasonal, d.residual])
})?;
let residual = parts.pop().expect("three components");
let seasonal = parts.pop().expect("three components");
let trend = parts.pop().expect("three components");
let arr = StructArray::try_new(
stl_struct_fields(),
vec![f64_array(trend), f64_array(seasonal), f64_array(residual)],
None,
)
.map_err(exec_err)?;
Ok(Arc::new(arr))
}
}