File size: 1,445 Bytes
590a501 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | """Factor neutralization (industry / market cap) via cross-sectional regression."""
from __future__ import annotations
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
def neutralize_factor(
factor: pd.Series,
exposures: pd.DataFrame,
date_level: str = "datetime",
) -> pd.Series:
"""
Neutralize factor against exposure columns (e.g. industry dummies, log market cap).
Expects MultiIndex (instrument, datetime) or a panel with date column.
"""
if isinstance(factor.index, pd.MultiIndex):
df = factor.to_frame("factor").join(exposures, how="left")
neutralized = []
for dt, group in df.groupby(level=date_level):
y = group["factor"].values
x = group[exposures.columns].fillna(0).values
if len(y) < x.shape[1] + 2:
neutralized.append(group["factor"])
continue
reg = LinearRegression().fit(x, y)
resid = y - reg.predict(x)
neutralized.append(pd.Series(resid, index=group.index))
return pd.concat(neutralized).sort_index()
raise NotImplementedError("neutralize_factor currently supports MultiIndex panels only")
def zscore_by_date(factor: pd.Series, date_level: str = "datetime") -> pd.Series:
def _z(x):
return (x - x.mean()) / (x.std() + 1e-8)
return factor.groupby(level=date_level, group_keys=False).apply(_z)
|