淘金『因子日历』:因子筛选与机器学习
量化投资与机器学习微信公众号,是业内垂直于量化投资、对冲基金、Fintech、人工智能、大数据等领域的主流自媒体。公众号拥有来自公募、私募、券商、期货、银行、保险、高校等行业30W+关注者,曾荣获AMMA优秀品牌力、优秀洞察力大奖,连续4年被腾讯云+社区评选为“年度最佳作者”。
这就教你如何真正使用因子数据!
因子日历数据下载
如需下载因子日历提供的数据,请大家在公众号后台进行下载获取。
操作方式如下:
前言
测试说明
def _entropy(x, bins=None):
"""
calculate Shanon Entropy
"""
N = len(x)
x_ = x[~np.isnan(x)] # drop nan
if len(x_)<1:
return np.nan
if bins is None:
_,counts = np.unique(x_, return_counts=True)
else:
counts = np.histogram(x_, bins=bins)[0]
prob = counts[np.nonzero(counts)] / N
en = -np.sum(prob * np.log2(prob))
return en
计算监督型评价指标时除了考虑因子 x 的信息外,还会同时考虑收益 y 的信息,是对因子与收益间关系的评价,常用的统计指标有 F 统计量、Cramer' V值、互信息等,测试用的收益 y 为股票未来一个月的收益率。
F统计量
此处的 F 统计量通过对单个因子 x 与收益 y 进行一元线性回归得到,具体调用的 sklearn 中的 f_regression,该方法采用如下公式计算 x 与 y 之间的回归系数:E[(X[:, i] - mean(X[:, i])) * (y - mean(y))] / (std(X[:, i]) * std(y)),该公式本质上就是计算的 x 与 y 的 pearson 相关系数(也就是因子 IC 值),可以证明得到该相关系数的平方即为该回归方程的判定系数R^2(回归平方和与总离差平方和之比值 SSR/SST),最终的 F 统计量为:
回归中的 F 统计量通常用于检测回归方程整体的显著性,由于单变量回归只涉及一个回归系数,此时的 F 统计量衡量了因子 x 和收益 y 的关联程度,F 值越高,关联性越强,对应的 p 值可用于判定因子 x 和收益 y 的关系是否显著。为了消除不同横截面样本量的影响,下图中的 F 统计量都是经过归一化后的值。
对比大类因子的平均 F 统计量情况,排名靠前的是规模因子>无形资产因子>投资因子>杠杆因子,常用的动量因子排名最后,估值因子也排名较后;基本上所有的大类因子在跨横截面后 F 统计量都有所提升(可能受样本量影响),但排名头部的大类因子提升的可能性相对更低些。在看 P 值显著占比情况,排名靠前的是规模因子>流动性因子>来自量价的技术因子、动量因子、波动率因子等,前面 F 值高的因子反倒排名靠后了,说明这些因子在不同横截面上的表现不是很稳定,有些时点上表现很突出,但大多数时间表现并不理想。
结合下面几幅图可知各大类因子中 F 统计量排名最靠前和排名最靠后的具体因子以及它们在分布图中的大致位置(最顶端的点和最底端的点)。与大类因子一致,就 F 值来看,排名靠前的因子中,基本面因子居多,排名靠后的因子中,量价因子居多,但量价因子在时序上表现的更稳定。
卡方检验
此处的卡方检验指的是 Pearson's chi squared test,它借助列联表来判断两个分类变量是否独立,所以若想用卡方检验来做因子筛选,需要对因子 x 和收益 y 做离散化处理,对于因子 x 的离散化有 2 种方式:① 离散化为 N 类:利用 qcut 等分为 N 组,组内样本量相等;② 离散化为 2 类:只取因子值排名靠前的 n% 样本作为一组和排名靠后的 n% 样本作为一组,剔除掉中间的那部分样本,只保留尾部 tail;对于收益 y 的离散化有 2 种方式:① 离散化为 N 类:利用 qcut 等分为 N 组,组内样本量相等;② 离散化为 2 类:将收益大于等于 0 的为一组,收益小于 0 的为一组。若 x 的划分选用的方式 ②,则保留相应的 y ,但 y 的分类是基于全样本确定的。对于评价指标,可以选用 Cramer'V 和 p 值:
, 为实际观测数、 为期望观测数。
, 和 为列联表的行数和列数, 为样本量。
在筛选因子时,一般用 Cramer'V ,其取值为 0-1,取值越高,关联性越强,更方便做比较。相比回归的 F 统计值,Cramer'V 能衡量因子 x 与收益 y 的非线性关系,而且因子 x 的尾部划分,能进一步知道因子的极端取值与收益的关系,而因子对收益的预测能力往往依赖于极端值。
卡方检验示例代码
def chi2(x, y, x_bin=0.1, y_bin=3) -> tuple:
'''
Chi-square test of independence between x and y
x_bin: int -> use all cases and split equally; 0<float<=0.5 -> use tails only and get two bins
y_bin: int -> split equally; 'zero' -> split at zero and get only two bins (win and lose), y=0 belong to win
return:tuple (chi_square, p_value, contingency coefficient, cramer'v)
'''
x,y = discretization_processing(x, y, x_bin, y_bin)
# get chi2
arr = pd.crosstab(index=x, columns=y).sort_index().to_numpy()
chi2_stat = chi2_contingency(arr, correction=True, lambda_=None)
chi2 = chi2_stat[0]
phi2 = chi2 / arr.sum()
n_rows, n_cols = arr.shape
cramer = np.sqrt(phi2 / min(n_cols - 1, n_rows - 1))
contingency = np.sqrt(chi2 / (chi2+arr.sum()))
return (chi2,chi2_stat[1],contingency,cramer)
下图测试,因子 x 离散化采用方式②,阈值取0.1,收益 y 离散化采用方式①,阈值取 3,更多的是测试因子极端值的表现;对比大类因子的平均 Cramer'V 统计量情况,排名靠前的有:规模因子>流动性因子>来自量价的技术因子、波动率因子、动量因子等,量价因子普遍优于基本面因子,与前面 F 统计量的 p 值占比相对一致;结合卡方检验的 p 值显著性占比情况,与 Cramer'V 统计量的排名也是保持一致的。对比跨横截面的结果,所有大类因子在跨横截面后 Cramer'V 值都有所降低,但显著时间点占比有所提高,这可能是受样本量的影响。对比 F 统计量,Cramer'V 给出的结果更一致,更稳定,而且还能捕捉非线性关系。
结合下面几幅图可知各大类因子中 Cramer'V 统计量排名最靠前和排名最靠后的具体因子以及它们在分布图中的大致位置(最顶端的点和最底端的点)。排名靠前因子中,量价因子居多,排名靠后因子中,基本面因子居多。
下图还进一步对比了因子 x 的 2 种离散化方式下大类因子 Cramer'V 均值分布情况(因子 x 采用 3 等均分 equal 和保留 10% 的尾部 tail,收益 y 同上,采用 3 等均分),极端值下因子与收益的关联关系相对更高,这个现象在财务质量因子上的表现相对要弱些。
互信息
def discretization_processing(x, y, x_bin=0.1, y_bin=3) -> tuple:
'''
x_bin: int -> use all cases and split equally; 0<float<=0.5 -> use tails only and get two bins; None -> raw x
y_bin: int -> split equally; 'zero' -> split at zero and get only two bins (win and lose), y=0 belong to win; None -> raw y
return:tuple (discretized feature, discretized target)
'''
# cut target y
if isinstance(y_bin, int):
y = pd.qcut(y, q=y_bin, duplicates='drop')
elif y_bin=='zero':
y = np.where(y>=0, 1, 0)
elif y_bin is None:
y = y
else:
raise ValueError("y_bin must be int or 'zero' or None !")
# cut predictor x
if isinstance(x_bin, float) & (x_bin<=0.5) & (x_bin>0):
x_bin_ = int(1/x_bin)
x = pd.qcut(x, q=x_bin_, duplicates='drop')
elif isinstance(x_bin, int):
x = pd.qcut(x, q=x_bin, duplicates='drop')
elif x_bin is None:
x = x
else:
raise ValueError("x_bin must be fraction in (0,0.5] or int or None !")
# only get tail
if isinstance(x_bin, float):
un = np.unique(x)
id = np.where((x==un[0])|(x==un[-1]))[0]
x = x[id]
y = y[id]
return (x,y)
总结
微信扫码关注该文公众号作者