"""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 ]