use polars::error::PolarsResult;
use polars::frame::DataFrame;
use polars::lazy::dsl::Expr;
use polars::prelude::*;
#[derive(Clone, Debug, Default)]
pub struct FeatureOperator {
feature_expressions: Vec<Expr>,
derived_feature_expressions: Vec<Expr>,
}
impl FeatureOperator {
pub fn new() -> FeatureOperator {
FeatureOperator {
feature_expressions: Vec::new(),
derived_feature_expressions: Vec::new(),
}
}
pub fn append_expression(&mut self, expression: Expr) -> &mut FeatureOperator {
self.feature_expressions.push(expression);
self
}
pub fn append_derived_expression(&mut self, expression: Expr) -> &mut FeatureOperator {
self.derived_feature_expressions.push(expression);
self
}
pub fn collect_lazy_expressions(
&self,
df: DataFrame,
n_rows: Option<u32>,
exclude_columns: Option<Vec<String>>,
) -> PolarsResult<DataFrame> {
let exclude_columns = exclude_columns.unwrap_or_default();
match n_rows {
Some(n_rows) => df
.lazy()
.with_columns(&self.feature_expressions)
.with_columns(&self.derived_feature_expressions)
.tail(n_rows)
.select([all().exclude(exclude_columns)])
.collect(),
None => df
.lazy()
.with_columns(&self.feature_expressions)
.with_columns(&self.derived_feature_expressions)
.select([all().exclude(exclude_columns)])
.collect(),
}
}
pub fn feature_expressions_count(&self) -> usize {
self.feature_expressions.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_feature_operator() -> FeatureOperator {
let mut feature_operator = FeatureOperator::new();
let addition_expression = (col("high") + col("low")).alias("addition");
let subtraction_expression = (col("high") - col("low")).alias("subtraction");
let division_expression = (col("high") / col("low")).alias("division");
let multiplication_expression = (col("high") * col("low")).alias("multiplication");
feature_operator.append_expression(addition_expression);
feature_operator.append_expression(subtraction_expression);
feature_operator.append_expression(division_expression);
feature_operator.append_expression(multiplication_expression);
feature_operator
}
fn create_df() -> DataFrame {
df!(
"ticker" => ["AAPL", "NVDA", "MSFT", "GOOG", "AMZN"],
"price" => [229.9, 138.93, 420.56, 166.41, 188.4],
"high" => [231.31, 139.6, 424.04, 167.62, 189.83],
"low" => [228.6, 136.3, 417.52, 164.78, 188.44],
)
.unwrap()
}
#[test]
fn count_features() {
let feature_operator = create_feature_operator();
assert_eq!(feature_operator.feature_expressions_count(), 4);
}
#[test]
fn collect_expressions() {
let df = create_df();
let (height, width) = df.shape();
let feature_operator = create_feature_operator();
let feature_count = feature_operator.feature_expressions_count();
let df = feature_operator
.collect_lazy_expressions(df, None, None)
.unwrap();
dbg!(&df);
assert_eq!(df.shape(), (height, width + feature_count));
assert_eq!(
df.get_column_names(),
&[
"ticker",
"price",
"high",
"low",
"addition",
"subtraction",
"division",
"multiplication"
]
);
let exclude_columns = vec![String::from("addition"), String::from("subtraction")];
let df = feature_operator
.collect_lazy_expressions(df, Some(5), Some(exclude_columns.clone()))
.unwrap();
assert_eq!(
df.shape(),
(height, width + feature_count - exclude_columns.len())
);
assert_eq!(
df.get_column_names(),
&[
"ticker",
"price",
"high",
"low",
"division",
"multiplication"
]
);
}
#[test]
fn rolling_window_expression() {
let df = create_df();
let mut feature_operator = FeatureOperator::new();
let rolling_window_expression = (col("price").rolling_mean(RollingOptionsFixedWindow {
window_size: 3,
min_periods: 3,
weights: None,
center: false,
fn_params: Default::default(),
}))
.alias("rolling_mean");
feature_operator.append_expression(rolling_window_expression);
let df = feature_operator
.collect_lazy_expressions(df, None, None)
.unwrap();
dbg!(&df);
assert!(df.get_column_names_str().to_vec().contains(&"rolling_mean"));
}
}