76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""Feature computation and validation."""
|
||
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import Iterable
|
||
|
||
import pandas as pd
|
||
from pandas.api.types import is_numeric_dtype
|
||
|
||
from pipeline.features.registry import get_feature
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
FEATURE_KEY_COLUMNS = ["symbol_id", "date"]
|
||
|
||
|
||
def validate_feature_frame(features: pd.DataFrame) -> pd.DataFrame:
|
||
"""Validate and normalize a daily feature frame.
|
||
|
||
A valid feature frame is keyed by unique ``symbol_id,date`` rows and has at
|
||
least one numeric feature column beyond those keys.
|
||
"""
|
||
duplicated = features.columns[features.columns.duplicated()].tolist()
|
||
if duplicated:
|
||
raise ValueError(f"Feature output has duplicate columns: {duplicated}")
|
||
|
||
missing = [col for col in FEATURE_KEY_COLUMNS if col not in features.columns]
|
||
if missing:
|
||
raise ValueError(f"Feature output missing required columns: {missing}")
|
||
|
||
out = features.copy()
|
||
out["date"] = pd.to_datetime(out["date"])
|
||
|
||
if out.duplicated(FEATURE_KEY_COLUMNS).any():
|
||
raise ValueError("Feature output has duplicate symbol_id,date rows")
|
||
|
||
feature_cols = [col for col in out.columns if col not in FEATURE_KEY_COLUMNS]
|
||
if not feature_cols:
|
||
raise ValueError("Feature output must include at least one feature column")
|
||
|
||
non_numeric = [col for col in feature_cols if not is_numeric_dtype(out[col])]
|
||
if non_numeric:
|
||
raise ValueError(f"Feature columns must be numeric: {non_numeric}")
|
||
|
||
out = out[FEATURE_KEY_COLUMNS + feature_cols].copy()
|
||
return out.sort_values(FEATURE_KEY_COLUMNS).reset_index(drop=True)
|
||
|
||
|
||
def compute_feature(
|
||
minute: pd.DataFrame,
|
||
feature_type: str,
|
||
daily: pd.DataFrame | None = None,
|
||
**params,
|
||
) -> pd.DataFrame:
|
||
"""Compute one registered feature from raw minute bars."""
|
||
feature = get_feature(feature_type, **params)
|
||
result = validate_feature_frame(feature.compute(minute=minute, daily=daily))
|
||
feature_cols = [col for col in result.columns if col not in FEATURE_KEY_COLUMNS]
|
||
logger.info(
|
||
"Feature '%s' (%r): %d symbols × %d dates, columns=%s",
|
||
feature_type,
|
||
feature,
|
||
result["symbol_id"].nunique(),
|
||
result["date"].nunique(),
|
||
feature_cols,
|
||
)
|
||
return result
|
||
|
||
|
||
def read_feature_frames(feature_paths: Iterable[str | Path]) -> list[pd.DataFrame]:
|
||
"""Read and validate feature parquet files."""
|
||
return [
|
||
validate_feature_frame(pd.read_parquet(path))
|
||
for path in feature_paths
|
||
]
|