mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 14:38:04 +00:00
177 lines
5.0 KiB
Python
177 lines
5.0 KiB
Python
"""Arrow-to-DataFrame conversion utilities.
|
|
|
|
Supports pandas and polars with automatic backend detection.
|
|
All conversions are zero-copy where possible (via PyArrow).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
|
|
|
|
def detect_backend() -> str:
|
|
"""Auto-detect the best available DataFrame backend.
|
|
|
|
Returns:
|
|
``"pandas"``, ``"polars"``, or ``"arrow"`` (fallback).
|
|
"""
|
|
try:
|
|
import pandas # noqa: F401
|
|
return "pandas"
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
import polars # noqa: F401
|
|
return "polars"
|
|
except ImportError:
|
|
pass
|
|
return "arrow"
|
|
|
|
|
|
def _resolve_backend(backend: str) -> str:
|
|
if backend == "auto":
|
|
return detect_backend()
|
|
return backend
|
|
|
|
|
|
def arrow_to_df(
|
|
batch: Any,
|
|
backend: str = "auto",
|
|
) -> Any:
|
|
"""Convert a PyArrow RecordBatch or Table to a DataFrame.
|
|
|
|
Args:
|
|
batch: A ``pyarrow.RecordBatch`` or ``pyarrow.Table``.
|
|
backend: ``"pandas"``, ``"polars"``, or ``"auto"`` (detect).
|
|
|
|
Returns:
|
|
A pandas DataFrame or polars DataFrame.
|
|
|
|
Raises:
|
|
ImportError: If the requested backend is not installed.
|
|
"""
|
|
backend = _resolve_backend(backend)
|
|
|
|
if backend == "pandas":
|
|
import pandas as pd
|
|
import pyarrow as pa
|
|
|
|
if isinstance(batch, pa.RecordBatch):
|
|
batch = pa.Table.from_batches([batch])
|
|
return batch.to_pandas()
|
|
|
|
if backend == "polars":
|
|
import polars as pl
|
|
import pyarrow as pa
|
|
|
|
if isinstance(batch, pa.RecordBatch):
|
|
batch = pa.Table.from_batches([batch])
|
|
return pl.from_arrow(batch)
|
|
|
|
# Fallback: return as-is
|
|
return batch
|
|
|
|
|
|
def arrow_to_series(
|
|
array: Any,
|
|
name: str = "value",
|
|
backend: str = "auto",
|
|
) -> Any:
|
|
"""Convert a PyArrow Array to a pandas Series or polars Series.
|
|
|
|
Args:
|
|
array: A ``pyarrow.Array``, ``pyarrow.ChunkedArray``, or ``pyarrow.Float64Array``.
|
|
name: Name for the resulting Series.
|
|
backend: ``"pandas"``, ``"polars"``, or ``"auto"`` (detect).
|
|
|
|
Returns:
|
|
A pandas Series or polars Series.
|
|
"""
|
|
backend = _resolve_backend(backend)
|
|
|
|
if backend == "pandas":
|
|
import pandas as pd
|
|
|
|
if hasattr(array, "to_pandas"):
|
|
return pd.Series(array.to_pandas(), name=name)
|
|
return pd.Series(array, name=name)
|
|
|
|
if backend == "polars":
|
|
import polars as pl
|
|
|
|
try:
|
|
import pyarrow as pa
|
|
except ImportError:
|
|
pa = None
|
|
# Zero-copy: hand the Arrow buffers straight to polars instead of boxing
|
|
# every value into a Python object via to_pylist() (copies the whole
|
|
# column). pl.from_arrow shares the underlying buffers.
|
|
if pa is not None and isinstance(array, (pa.Array, pa.ChunkedArray)):
|
|
return pl.from_arrow(array).rename(name)
|
|
return pl.Series(name=name, values=list(array))
|
|
|
|
return array
|
|
|
|
|
|
def results_to_df(
|
|
results: Sequence[Any],
|
|
param_grid: Optional[Dict[str, List[Any]]] = None,
|
|
backend: str = "auto",
|
|
) -> Any:
|
|
"""Convert a list of BacktestResult (or Result) objects to a metrics DataFrame.
|
|
|
|
Each row contains all performance metrics plus parameter values (if provided).
|
|
|
|
Args:
|
|
results: Sequence of BacktestResult or Result objects.
|
|
param_grid: Optional parameter grid dict (used to label rows with param values).
|
|
When provided, the Cartesian product is expanded to match result order.
|
|
backend: ``"pandas"``, ``"polars"``, or ``"auto"``.
|
|
|
|
Returns:
|
|
A DataFrame with one row per result and columns for each metric + parameter.
|
|
"""
|
|
import itertools
|
|
|
|
backend = _resolve_backend(backend)
|
|
|
|
rows: List[Dict[str, Any]] = []
|
|
|
|
# Expand parameter grid into list of param dicts
|
|
param_combos: Optional[List[Dict[str, Any]]] = None
|
|
if param_grid:
|
|
keys = list(param_grid.keys())
|
|
values = [param_grid[k] for k in keys]
|
|
param_combos = [dict(zip(keys, combo)) for combo in itertools.product(*values)]
|
|
|
|
for i, result in enumerate(results):
|
|
# Support both raw BacktestResult and Result wrapper
|
|
metrics = result.metrics if hasattr(result, "metrics") else {}
|
|
row: Dict[str, Any] = {}
|
|
|
|
# Add parameters
|
|
if param_combos is not None and i < len(param_combos):
|
|
for k, v in param_combos[i].items():
|
|
row[f"param_{k}"] = v
|
|
|
|
# Flatten metrics dict
|
|
if isinstance(metrics, dict):
|
|
for k, v in metrics.items():
|
|
if isinstance(v, dict):
|
|
# Nested (e.g. trade_stats)
|
|
for sub_k, sub_v in v.items():
|
|
row[sub_k] = sub_v
|
|
else:
|
|
row[k] = v
|
|
|
|
rows.append(row)
|
|
|
|
if backend == "pandas":
|
|
import pandas as pd
|
|
return pd.DataFrame(rows)
|
|
|
|
if backend == "polars":
|
|
import polars as pl
|
|
return pl.DataFrame(rows)
|
|
|
|
return rows
|