数据预处理(下)之中性化(不止行业和市值中性化~)

转载自:https://www.ricequant.com/community/topic/4309/

 

  在上一个帖子中,我们总结了离群值处理和标准化,而本文将解释何为中性化以及其它的一些“中性化”定义。同样的,欢迎大家补充和讨论!!

 

(接上帖)

中性化

    当我们提及中性化时,我们往往是希望剔除待使用数据中那些多余的风险暴露。这些数据根据不同的应用场景会有不同类型,比如说我要使用因子选股,所以想对因子值进行中性化;或者想分析因子到底是否有效,所以对因子收益率进行中性化;再或者,假如已经选好了股,想使这个投资组合对行业中性化,等等。

    道理我懂,可是应该怎么实现呢?

 

    首先,我们来讲一讲和因子相关的中性化~

    在量化交易中,我们会经常使用某种指标或者多种指标来对股票池进行筛选,这些用于选股的指标一般被称为因子。在使用这些因子进行选股时,有时会因为其它因子的影响,而导致选出来的股票具有一些我们不希望看到的偏向。

    比如说,市净率会与市值有很高的相关性,这时如果我们使用未进行市值中性化的市净率,选股的结果会比较集中。同时朝阳行业和夕阳行业的市盈率在大致上也有一定的特点,也就是说行业对估值因子也有影响,那么我们得到的结果是具有一些多余偏好的。

    那我们要怎么解决这一由于不同行业和市值大小导致的误差问题呢?

    为了让我们在用某一因子时能剔除其他因素的影响,使得选出的股票更加分散,我们需要对其进行中性化处理。上一篇提到,标准化应该用于多个不同量级指标之间需要互相比较或者数据需要变得集中时,而中性化的目的在于消除因子中的偏差和不需要的影响。

    这里使用和上篇帖子相同的数据,下图是使用3sigma去极值方法后的全市场BP分布。

 

    下图是每个申万行业的BP平均值(不知道是不是因为我用的windows,图表显示不出中文...),可以看出801780.INDX的BP平均值最高,即银行业在中性化之前拥有最高的BP均值。

 

 

    根据大部分的研报对于中性化的处理,主要的方法是利用回归得到一个与风险因子线性无关的因子,即通过建立线性回归,提取残差作为中性化后的新因子。这样处理后的中性化因子与风险因子之间的相关性严格为零。

    对于因子来说,市场风险(例如牛市和熊市)和行业风险(同一行业的公司受的影响类似)是主要考虑的因素,对这两者的处理方式有两种:

    1、将市场因子和行业因子同时纳入模型

    2、仅纳入行业因子,而将市场因子包含在行业因子中。

    实际上这两种并没有什么区别:对于回归而言,前者带截距项,而后者是过原点。

    对于其他风格风险因子,以市值的影响最为明显和广泛,所以在此我用市值和行业中性化为例,具体做法是在每个时间截面上用所有股票的数据做横截面回归方程:

 

    其中,Factor_i为股票i的alpha因子,MktVal_i为股票i的总市值,Industry_j,i为行业虚拟变量,即如果股票i属于行业j则暴露度为1,否则为0,而且每个股票i仅属于一个行业,不对其所属行业进行拆分。    

    我们以上述回归方程的残差项作为原因子在中性化后的新因子。下图为市值或/与行业中性化后的BP因子值:

 

    能够看出市值+行业中性化后的BP分布相较于之前,变得更加均匀。实际上中性化后的BP和、以及中性化后的各行业BP和,都约等于零。详细计算请看下面的notebook。

 

    然而有的人提出简单的线性回归法本身不一定能彻底地剔除因子的多余信息,这与线性回归所做的前提假设有关。

    通常默认因子之间线性相关,残差正态独立同分布,但以上假设可能存在问题。第一,由于有相当大一部分的因子分布在两端极值,这会影响到残差正态分布的假设,但去极值又有可能会破坏残差分布的连续性。第二,我们常用分位数分组的方法来使用中性化后的因子值,理论上回归残差求和为0,而不能确保各分组内部是否中性。

    故提出在原模型上选择使用单调性变化处理后的因子(实际上是“对因子横截面排序百分比取正太累积分布函数反函数”),能使残差在未去极值的情况下更接近正态分布。

    我对此变换处理比较有疑虑,因为与去极值的简单的线性回归相比,很难说究竟是哪种更好或哪种损失的信息更少。若是对这个方法感兴趣,欢迎讨论~

 

    正如在上文提到的,除了因子以外,还有其他含义的中性化。

    例如想使得投资组合对行业中性化,通常的做法是根据基准中各行业的市值权重,调整投资组合相应行业的股票权重,此时的结果将对行业中性。

    直观来说,我们知道Brinson分析(如果有不清楚定义的朋友,请点击此帖:《绩效分析之Brinson模型》)中的资产配置收益的公式为Q2-Q1,即由资产配置带来的超额收益。此时如果组合资产i(在此处可理解为组合中的行业i)的权重等于基准资产i(基准的行业i)的权重,则来自资产配置收益为0,即主动收益为0,此时做到了中性化。

 

    最后,根据《主动投资》(《Active Portfolio Management》)中投资组合的构建一章中,它对中性化的解释为“去掉alpha的偏差或者不希望产生的影响‘,并且提供了四种类型的中性化:基准组合中性化、现金中性化、风险因子中性化和行业中性化。

    它这里的中性化更多的是对alpha的处理,而不是我们通常提及的因子中性化。然而既然提到了就搬运下这四种中性化的定义吧~

    1. 基准组合中性化。根据定义(尽管可能具有超额收益),基准投资组合的alpha为0。将基准组合的alpha设定为0可以确保对基准组合中性,并避免基准组合时机选择问题。

    2. 现金中性化。和基准中性化同一个思路,即alpha不含任何主动的现金头寸。

    3. 风险因子中性化。投资组合分析中的多因子分析方法可以把收益分解为几个不同的维度。我们应当使alpha相对于风险因子进行中性化,中性化的alpha值仅包括那些我们可以预见的因素的信息,以及特别资产的信息。一旦进行中性化,相对于这些风险因子的alpha就为0。

    4. 行业中性化。计算每个行业(按市值加权平均)的,然后从每一个中减去行业平均的alpha。

 

总结

    中性化在不同应用场景中有不同的意义和方法,我们在处理前需要确定到底要对什么因素进行中性化。最后的投资组合不能保证行业暴露度为零,仍需要进行组合优化。

 

 

数据预处理之中性化-Copy1.ipynb克隆研究 +75

In [1]:

#import seaborn
import numpy as np
import pandas as pd
import math
from statsmodels import regression
import statsmodels.api as sm
stocks = all_instruments(type="CS", date='2017-10-23').order_book_id.tolist()
data = get_fundamentals(query(fundamentals.eod_derivative_indicator.pb_ratio,fundamentals.eod_derivative_indicator.market_cap
).filter(fundamentals.income_statement.stockcode.in_(stocks)), '2017-10-23', '1d').major_xs('2017-10-23').dropna()
data['BP'] = 1/data['pb_ratio']

一、离群值处理

In [2]:

def filter_extreme_MAD(series,n): #MAD:中位数去极值
  median = np.percentile(series,50)
  new_median = np.percentile((series - median).abs(),50)
  max_range = median + n*new_median
  min_range = median - n*new_median
  return np.clip(series,min_range,max_range)
def filter_extreme_3sigma(series,n=3): #3 sigma
  mean = series.mean()
  std = series.std()
  max_range = mean + n*std
  min_range = mean - n*std
  return np.clip(series,min_range,max_range)
def filter_extreme_percentile(series,min = 0.025,max = 0.975): #百分位法
  series = series.sort_values()
  q = series.quantile([min,max])
  return np.clip(series,q.iloc[0],q.iloc[1])

二、标准化(见上一帖子,再此省略)

三、中性化处理

In [3]:

SHENWAN_INDUSTRY_MAP = {
      "801010.INDX": "农林牧渔",
      "801020.INDX": "采掘",
      "801030.INDX": "化工",
      "801040.INDX": "钢铁",
      "801050.INDX": "有色金属",
      "801080.INDX": "电子",
      "801110.INDX": "家用电器",
      "801120.INDX": "食品饮料",
      "801130.INDX": "纺织服装",
      "801140.INDX": "轻工制造",
      "801150.INDX": "医药生物",
      "801160.INDX": "公用事业",
      "801170.INDX": "交通运输",
      "801180.INDX": "房地产",
      "801200.INDX": "商业贸易",
      "801210.INDX": "休闲服务",
      "801230.INDX": "综合",
      "801710.INDX": "建筑材料",
      "801720.INDX": "建筑装饰",
      "801730.INDX": "电气设备",
      "801740.INDX": "国防军工",
      "801750.INDX": "计算机",
      "801760.INDX": "传媒",
      "801770.INDX": "通信",
      "801780.INDX": "银行",
      "801790.INDX": "非银金融",
      "801880.INDX": "汽车",
      "801890.INDX": "机械设备"}
def get_industry_exposure(order_book_ids):
    df = pd.DataFrame(index=SHENWAN_INDUSTRY_MAP.keys(), columns=order_book_ids)
    for stk in order_book_ids:
        try:
            df[stk][instruments(stk).shenwan_industry_code] = 1
        except:
            continue
    return df.fillna(0)#将NaN赋为0

In [4]:

# 需要传入单个因子值和总市值
def neutralization(factor,mkt_cap = False, industry = True):
  y = factor
  if type(mkt_cap) == pd.Series:
    LnMktCap = mkt_cap.apply(lambda x:math.log(x))
    if industry: #行业、市值
      dummy_industry = get_industry_exposure(factor.index)
      x = pd.concat([LnMktCap,dummy_industry.T],axis = 1)
    else: #仅市值
      x = LnMktCap
  elif industry: #仅行业
    dummy_industry = get_industry_exposure(factor.index)
    x = dummy_industry.T
  result = sm.OLS(y.astype(float),x.astype(float)).fit()
  return result.resid

In [5]:

#使用3sigma离群值处理法
no_extreme_BP = filter_extreme_3sigma(data['BP'])
 #行业市值中性
new_BP_all = neutralization(no_extreme_BP,data['market_cap'])
 #市值中性
new_BP_MC = neutralization(no_extreme_BP,data['market_cap'],industry = False)
#行业中性
new_BP_In = neutralization(no_extreme_BP) 
fig = plt.figure(figsize = (20, 8))
ax = no_extreme_BP.plot.kde(label = 'no_extreme_BP')
ax = new_BP_all.plot.kde(label = 'new_BP_all')
ax = new_BP_MC.plot.kde(label = 'new_BP_MC')
ax = new_BP_In.plot.kde(label = 'new_BP_In')
ax.legend()

Out[5]:

接下来查看原数据(去极值后)在各行业的情况

In [6]:

df = pd.DataFrame(no_extreme_BP).reset_index()
#添加申万分类
df['shenwan'] = df['index'].apply(lambda x:instruments(x).shenwan_industry_code)
df = df.set_index('index')

In [7]:

 #求出每个申万行业的BP平均值
shenwan_BP = df.groupby('shenwan')['BP'].apply(lambda x:x.mean())
fig = plt.figure(figsize = (20, 8))
ax = shenwan_BP.plot.bar()
ax.legend()

Out[7]:

In [8]:

#将中性化后的BP与df拼接
df = pd.concat([df,new_BP_all],axis = 1).rename(columns = {0:'new_BP'}) 
 #求出每个申万行业的中性化BP平均值
shenwan_new_BP = df.groupby('shenwan')['new_BP'].apply(lambda x:x.sum())
print('中性化后的BP和:',df['new_BP'].sum())
print(shenwan_new_BP)
中性化后的BP和: 4.7151615945e-11
shenwan
801010.INDX    1.325884e-12
801020.INDX    8.743006e-13
801030.INDX    4.233947e-12
801040.INDX    3.837486e-13
801050.INDX    1.498801e-12
801080.INDX    3.196110e-12
801110.INDX    7.885914e-13
801120.INDX    1.258660e-12
801130.INDX    1.367906e-12
801140.INDX    1.710937e-12
801150.INDX    3.910650e-12
801160.INDX    1.957989e-12
801170.INDX    1.353917e-12
801180.INDX    1.747491e-12
801200.INDX    1.253886e-12
801210.INDX    4.702350e-13
801230.INDX    7.120415e-13
801710.INDX    9.721390e-13
801720.INDX    1.560252e-12
801730.INDX    2.601475e-12
801740.INDX    6.176171e-13
801750.INDX    2.643663e-12
801760.INDX    1.721456e-12
801770.INDX    1.285860e-12
801780.INDX    3.917977e-13
801790.INDX    8.250067e-13
801880.INDX    2.229328e-12
801890.INDX    4.263256e-12
Name: new_BP, dtype: float64

In [9]:

#取前20%,并根据行业分类
BP_head = df[df['BP']>df['BP'].quantile(0.80)]
BP_count = BP_head.groupby('shenwan')['BP'].count()
new_BP_head = df[df['new_BP']>df['new_BP'].quantile(0.80)]
new_BP_count = new_BP_head.groupby('shenwan')['new_BP'].count()
fig = plt.figure(figsize = (20, 8))
ax = BP_count.plot.bar(color='grey', position=1, width=0.3)
ax = new_BP_count.plot.bar(position=0, width=0.3)
ax.legend()

Out[9]:

 

你可能感兴趣的:(量化)