Source code for orion_finance_sdk_py.stats.factors

"""Principal-component analysis of overlapping daily returns."""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np
import pandas as pd
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

from orion_finance_sdk_py.stats.series import ReturnSeries


[docs] @dataclass(frozen=True) class PCAResult: """Standardized PCA of an overlapping return panel.""" explained_variance_ratio: np.ndarray loadings: pd.DataFrame scores: pd.DataFrame
[docs] def pca( returns: ReturnSeries | pd.DataFrame, *, n_components: int | None = None, standardize: bool = True, ) -> PCAResult: """Fit PCA on overlapping contiguous daily returns. Requires at least three overlapping rows and two columns. Default ``standardize=True`` matches the universe-research notebook (``StandardScaler`` then ``PCA``). """ frame = returns.returns if isinstance(returns, ReturnSeries) else returns overlap = frame.dropna(how="any") if overlap.shape[0] < 3 or overlap.shape[1] < 2: raise ValueError("PCA needs at least 3 overlapping observations and 2 assets") matrix = overlap.to_numpy(dtype=float) if standardize: matrix = StandardScaler().fit_transform(matrix) k = overlap.shape[1] if n_components is None else n_components k = min(k, overlap.shape[0], overlap.shape[1]) model = PCA(n_components=k) scores_arr = model.fit_transform(matrix) components = pd.Index([f"PC{i + 1}" for i in range(model.n_components_)]) loadings = pd.DataFrame( model.components_.T, index=overlap.columns, columns=components, ) scores = pd.DataFrame(scores_arr, index=overlap.index, columns=components) return PCAResult( explained_variance_ratio=np.asarray(model.explained_variance_ratio_), loadings=loadings, scores=scores, )