Files
2026-07-13 12:07:23 +08:00

168 lines
5.7 KiB
Python

"""
Math Functions
"""
import polars as pl
from .utility import DataProxy
def less(feature1: DataProxy, feature2: DataProxy | float) -> DataProxy:
"""Return the minimum value between two features"""
if isinstance(feature2, DataProxy):
df_merged: pl.DataFrame = feature1.df.join(feature2.df, on=["datetime", "vt_symbol"])
else:
df_merged = feature1.df.with_columns(pl.lit(feature2).alias("data_right"))
df: pl.DataFrame = df_merged.select(
pl.col("datetime"),
pl.col("vt_symbol"),
pl.min_horizontal("data", "data_right").over("vt_symbol").alias("data")
)
return DataProxy(df)
def greater(feature1: DataProxy, feature2: DataProxy | float) -> DataProxy:
"""Return the maximum value between two features"""
if isinstance(feature2, DataProxy):
df_merged: pl.DataFrame = feature1.df.join(feature2.df, on=["datetime", "vt_symbol"])
else:
df_merged = feature1.df.with_columns(pl.lit(feature2).alias("data_right"))
df: pl.DataFrame = df_merged.select(
pl.col("datetime"),
pl.col("vt_symbol"),
pl.max_horizontal("data", "data_right").over("vt_symbol").alias("data")
)
return DataProxy(df)
def log(feature: DataProxy) -> DataProxy:
"""Calculate the natural logarithm of the feature"""
df: pl.DataFrame = feature.df.select(
pl.col("datetime"),
pl.col("vt_symbol"),
pl.col("data").log().over("vt_symbol")
)
return DataProxy(df)
def abs(feature: DataProxy) -> DataProxy:
"""Calculate the absolute value of the feature"""
df: pl.DataFrame = feature.df.select(
pl.col("datetime"),
pl.col("vt_symbol"),
pl.col("data").abs().over("vt_symbol")
)
return DataProxy(df)
def sign(feature: DataProxy) -> DataProxy:
"""Calculate the sign of the feature"""
df: pl.DataFrame = feature.df.select(
pl.col("datetime"),
pl.col("vt_symbol"),
pl.when(pl.col("data") > 0).then(1).when(pl.col("data") < 0).then(-1).otherwise(0).alias("data")
)
return DataProxy(df)
def quesval(threshold: float, feature1: DataProxy, feature2: DataProxy | float | int, feature3: DataProxy | float | int) -> DataProxy:
"""Return feature2 if threshold < feature1, otherwise feature3"""
df_merged = feature1.df
if isinstance(feature2, DataProxy):
df_merged = df_merged.join(feature2.df, on=["datetime", "vt_symbol"], suffix="_true")
else:
df_merged = df_merged.with_columns(pl.lit(feature2).alias("data_true"))
if isinstance(feature3, DataProxy):
df_merged = df_merged.join(feature3.df, on=["datetime", "vt_symbol"], suffix="_false")
else:
df_merged = df_merged.with_columns(pl.lit(feature3).alias("data_false"))
df: pl.DataFrame = df_merged.with_columns(
pl.when(threshold < pl.col("data"))
.then(pl.col("data_true"))
.otherwise(pl.col("data_false"))
.alias("data")
).select(["datetime", "vt_symbol", "data"])
return DataProxy(df)
def quesval2(threshold: DataProxy, feature1: DataProxy, feature2: DataProxy | float | int, feature3: DataProxy | float | int) -> DataProxy:
"""Return feature2 if threshold < feature1, otherwise feature3 (DataProxy threshold version)"""
df_merged: pl.DataFrame = threshold.df.join(feature1.df, on=["datetime", "vt_symbol"], suffix="_cond")
if isinstance(feature2, DataProxy):
df_merged = df_merged.join(feature2.df, on=["datetime", "vt_symbol"], suffix="_true")
else:
df_merged = df_merged.with_columns(pl.lit(feature2).alias("data_true"))
if isinstance(feature3, DataProxy):
df_merged = df_merged.join(feature3.df, on=["datetime", "vt_symbol"], suffix="_false")
else:
df_merged = df_merged.with_columns(pl.lit(feature3).alias("data_false"))
df: pl.DataFrame = df_merged.with_columns(
pl.when(pl.col("data_cond") < pl.col("data"))
.then(pl.col("data_true"))
.otherwise(pl.col("data_false"))
.alias("data")
).select(["datetime", "vt_symbol", "data"])
return DataProxy(df)
def pow1(base: DataProxy, exponent: float) -> DataProxy:
"""Safe power operation for DataProxy (handles negative base values)"""
df: pl.DataFrame = base.df.with_columns(
pl.when(pl.col("data") > 0)
.then(pl.col("data").pow(exponent))
.when(pl.col("data") < 0)
.then(pl.lit(-1) * pl.col("data").abs().pow(exponent))
.otherwise(0)
.alias("data")
)
return DataProxy(df)
def pow2(base: DataProxy, exponent: DataProxy) -> DataProxy:
"""Power operation between two DataProxy objects (base^exponent)
handle logic:
- base > 0: calculate base^exponent
- base < 0 and exponent is integer: calculate -1 * |base|^exponent
- other cases (base = 0, exponent is NaN, negative base and non-integer exponent): return 0
Note: use floor method to check integer rather than cast(Int64) method, because NaN cannot be converted to integer will report an error
"""
base_renamed = base.df.rename({"data": "base_data"})
exp_renamed = exponent.df.rename({"data": "exp_data"})
df_merged: pl.DataFrame = base_renamed.join(exp_renamed, on=["datetime", "vt_symbol"], how="left")
df: pl.DataFrame = df_merged.with_columns(
pl.when(pl.col("base_data") > 0)
.then(pl.col("base_data").pow(pl.col("exp_data")))
.when(
(pl.col("base_data") < 0) &
(~pl.col("exp_data").is_nan()) &
(pl.col("exp_data").floor() == pl.col("exp_data"))
)
.then((-1) * pl.col("base_data").abs().pow(pl.col("exp_data")))
.otherwise(pl.lit(None))
.fill_nan(None)
.fill_null(0)
.alias("data")
).select(["datetime", "vt_symbol", "data"])
return DataProxy(df)