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)