use crate::error::SciRS2Error;
#[cfg(feature = "series")]
use crate::series::PyTimeSeries;
use pyo3::prelude::*;
use pyo3::types::PyDict;
#[cfg(feature = "series")]
use pyo3::types::PyList;
use scirs2_numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods};
#[cfg(feature = "series")]
#[pyfunction]
pub fn pandas_to_timeseries(py: Python, series: Py<PyAny>) -> PyResult<PyTimeSeries> {
let values_obj = series.getattr(py, "values")?;
let values_array = values_obj.cast_bound::<PyArray1<f64>>(py)?;
let index = series.getattr(py, "index")?;
let timestamps = if let Ok(index_values) = index.getattr(py, "values") {
if index.getattr(py, "to_numpy").is_ok() {
let timestamp_method = index.getattr(py, "astype")?;
let timestamps_ns = timestamp_method.call1(py, ("int64",))?;
let ts_values = timestamps_ns.getattr(py, "values")?;
let timestamps_array = ts_values.cast_bound::<PyArray1<i64>>(py)?;
let binding = timestamps_array.readonly();
let ts_arr = binding.as_array();
let ts_vec: Vec<f64> = ts_arr.iter().map(|&ns| ns as f64 / 1e9).collect();
Some(scirs2_core::Array1::from_vec(ts_vec))
} else if let Ok(index_array) = index_values.cast_bound::<PyArray1<f64>>(py) {
let binding = index_array.readonly();
let idx_arr = binding.as_array();
Some(idx_arr.to_owned())
} else {
None
}
} else {
None
};
let binding = values_array.readonly();
let values_arr = binding.as_array();
let values_owned = values_arr.to_owned();
Ok(PyTimeSeries::from_arrays(values_owned, timestamps))
}
#[cfg(feature = "series")]
#[pyfunction]
pub fn timeseries_to_pandas(py: Python, ts: &PyTimeSeries) -> PyResult<Py<PyAny>> {
let pandas = py.import("pandas")?;
let values = ts.values_owned().into_pyarray(py).unbind();
let series = if let Some(timestamps) = ts.timestamps_owned() {
let timestamps_ns: Vec<i64> = timestamps.iter().map(|&s| (s * 1e9) as i64).collect();
let datetime_index = pandas
.getattr("DatetimeIndex")?
.call1((PyList::new(py, ×tamps_ns)?,))?;
let kwargs = PyDict::new(py);
kwargs.set_item("index", datetime_index)?;
pandas.getattr("Series")?.call((values,), Some(&kwargs))
} else {
pandas.getattr("Series")?.call1((values,))
}?;
Ok(series.into())
}
#[pyfunction]
pub fn dataframe_to_array(py: Python, df: Py<PyAny>) -> PyResult<Py<PyArray2<f64>>> {
let values = df.getattr(py, "values")?;
let array = values.cast_bound::<PyArray2<f64>>(py)?;
Ok(array.to_owned().unbind())
}
#[pyfunction]
#[pyo3(signature = (array, columns=None, index=None))]
pub fn array_to_dataframe(
py: Python,
array: &Bound<'_, PyArray2<f64>>,
columns: Option<Vec<String>>,
index: Option<Py<PyAny>>,
) -> PyResult<Py<PyAny>> {
let pandas = py.import("pandas")?;
let kwargs = PyDict::new(py);
if let Some(cols) = columns {
kwargs.set_item("columns", cols)?;
}
if let Some(idx) = index {
kwargs.set_item("index", idx)?;
}
let df = pandas.getattr("DataFrame")?.call((array,), Some(&kwargs))?;
Ok(df.into())
}
#[pyfunction]
pub fn apply_to_dataframe(py: Python, df: Py<PyAny>, func: Py<PyAny>) -> PyResult<Py<PyAny>> {
let pandas = py.import("pandas")?;
let columns = df.getattr(py, "columns")?;
let col_list: Vec<String> = columns.extract(py)?;
let results = PyDict::new(py);
for col_name in col_list {
let column = df.call_method1(py, "__getitem__", (&col_name,))?;
let values = column.getattr(py, "values")?;
let result = func.call1(py, (values,))?;
results.set_item(&col_name, result)?;
}
let series = pandas.getattr("Series")?.call1((results,))?;
Ok(series.into())
}
#[pyfunction]
#[pyo3(signature = (df, func, axis=0))]
pub fn apply_along_axis(
py: Python,
df: Py<PyAny>,
func: Py<PyAny>,
axis: usize,
) -> PyResult<Py<PyAny>> {
let pandas = py.import("pandas")?;
if axis == 0 {
apply_to_dataframe(py, df, func)
} else {
let values = df.getattr(py, "values")?;
let array = values.cast_bound::<PyArray2<f64>>(py)?;
let binding = array.readonly();
let arr = binding.as_array();
let results: Vec<f64> = arr
.rows()
.into_iter()
.map(|row| {
let row_array = row.to_owned().into_pyarray(py);
func.call1(py, (row_array,))
.and_then(|r| r.extract::<f64>(py))
})
.collect::<Result<Vec<_>, _>>()?;
let series = pandas.getattr("Series")?.call1((results,))?;
Ok(series.into())
}
}
#[pyfunction]
pub fn rolling_apply(
py: Python,
series: Py<PyAny>,
window: usize,
func: Py<PyAny>,
) -> PyResult<Py<PyAny>> {
let pandas = py.import("pandas")?;
let values = series.getattr(py, "values")?;
let array = values.cast_bound::<PyArray1<f64>>(py)?;
let binding = array.readonly();
let arr = binding.as_array();
if arr.len() < window {
return Err(SciRS2Error::ValueError(format!(
"Window size {} is larger than array length {}",
window,
arr.len()
))
.into());
}
let mut results = Vec::with_capacity(arr.len() - window + 1);
for i in 0..=(arr.len() - window) {
let window_slice = arr.slice(ndarray::s![i..i + window]);
let window_array = window_slice.to_owned().into_pyarray(py);
let result: f64 = func.call1(py, (window_array,))?.extract(py)?;
results.push(result);
}
let mut padded = vec![f64::NAN; window - 1];
padded.extend(results);
let series_result = pandas.getattr("Series")?.call1((padded,))?;
Ok(series_result.into())
}
pub fn register_pandas_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
#[cfg(feature = "series")]
m.add_function(wrap_pyfunction!(pandas_to_timeseries, m)?)?;
#[cfg(feature = "series")]
m.add_function(wrap_pyfunction!(timeseries_to_pandas, m)?)?;
m.add_function(wrap_pyfunction!(dataframe_to_array, m)?)?;
m.add_function(wrap_pyfunction!(array_to_dataframe, m)?)?;
m.add_function(wrap_pyfunction!(apply_to_dataframe, m)?)?;
m.add_function(wrap_pyfunction!(apply_along_axis, m)?)?;
m.add_function(wrap_pyfunction!(rolling_apply, m)?)?;
Ok(())
}