金融数据科学:贝叶斯统计

近年来,贝叶斯统计已经成为实证金融学的基石。本章无法覆盖该领域的所有概念。因此,在必要时应该参考Geweke(2005)的教科书来了解一般的入门知识,利益驱动者应该参考Rachev(2008)的教科书。

13.3.1 贝叶斯公式

贝叶斯公式在金融学中常见的解读是历时解读。这主要说明,随着时间的推移,我们会了解感兴趣的变量或者参数的新信息,例如时间序列的平均收益率。公式13-5正式描述了这种理论。

公式13-5 贝叶斯公式

式中H表示某个事件(假设),D代表实验或者真实世界可能提供的数据[4]。在这些定义的基础上,我们得到:

  • p(H)称作先验概率;
  • p(D)是任何假设下数据的概率,称作标准化常数;
  • p(D|H)是假设H下数据的似然度(即概率);
  • p(H|D)是后验概率,即我们看到数据之后得出的概率。

考虑一个简单的例子。有两个盒子B1和B2,B1中有20个黑球和70个红球,而B2中有40个黑球和50个红球。随机从两个盒子中取出一个球,假定这个球是黑色的。“H1:球来自B1”和“H2:球来自B2”这两个假设的概率分别是多少?

在随机取出这个球之前,两个假设的可能性是相同的。但当球明显是黑色,我们必须根据贝叶斯公式更新两个假设的概率,考虑假设H1。

  • 先验概率:p(H1) =1/2
  • 标准化常数:p(D) =
  • 似然度:p(D|H1) =

更新后的H1概率p(H1|D) =

这一结果也有直观的意义。从B2取出一个黑球的概率两倍于同一件事情发生在B1上的概率。因此,取出一个黑球之后,假设H2的更新后概率p(H2|D)=2/3,也是假设H1更新后概率的2倍。

13.3.2 贝叶斯回归

Python生态系统利用PyMC3提供了一个全面的软件包,从技术上实现了贝叶斯统计和概率规划。

考虑如下基于直线周围噪声数据的例子。[5]首先,在数据集上实施一次线性普通最小二乘回归(参见第11章),结果如图13-15所示:

图13-15 样本数据点和回归线

In [1]: import numpy as np
        import pandas as pd
        import datetime as dt
        from pylab import mpl, plt

In [2]: plt.style.use('seaborn')
        mpl.rcParams['font.family'] = 'serif'
        np.random.seed(1000)
        %matplotlib inline

In [3]: x = np.linspace(0, 10, 500)
        y = 4 + 2 * x + np.random.standard_normal(len(x)) * 2

In [4]: reg = np.polyfit(x, y, 1)

In [5]: reg
Out[5]: array([2.03384161, 3.77649234])

In [6]: plt.figure(figsize=(10, 6))
        plt.scatter(x, y, c=y, marker='v', cmap='coolwarm')
        plt.plot(x, reg[1] + reg[0] * x, lw=2.0)
        plt.colorbar()
        plt.xlabel('x')
        plt.ylabel('y')

OLS回归方法的结果是回归线上两个参数(截距和斜率)的固定值。注意,最高阶单项式因子(本例中是回归线的斜率)在指数水平0处,截距在指数水平1处。原始参数2和4没能完全恢复,这是数据中包含的噪声所致。

贝叶斯回归利用了PyMC3软件包。这里假定参数遵循某种分布。例如,考虑回归线方程

。假定现在有以下先验概率:

  • α呈正态分布,均值为0,标准差为20;
  • β呈正态分布,均值为0,标准差为10。

至于似然度,假定一个均值为

的正态分布和一个标准差为0~10之间的均匀分布。

贝叶斯回归的一个要素是(马尔科夫链)蒙特卡洛(MCMC)采样[6]。原则上,这和前一个例子中多次从盒子取出球相同——只是采用更系统性、更自动化的方式。

技术性采样有3个不同的函数可以调用:

  • find_MAP()通过求取局部最大后验点来寻找采样算法的起点;
  • NUTS()为假定先验概率的MCMC采样实现所谓的“高效双平均无回转采样器”(NUTS)算法;
  • sample()以给定起始值(来自find_MAP())和最优化步长(来自NUTS算法)提取一定数量的样本。

上列函数都包装到一个PyMC3 Model对象中,并在with语句中执行:

In [8]: import pymc3 as pm

In [9]: %%time
        with pm.Model() as model:
            # model
            alpha = pm.Normal('alpha', mu=0, sd=20) ❶
            beta = pm.Normal('beta', mu=0, sd=10) ❶
            sigma = pm.Uniform('sigma', lower=0, upper=10) ❶
            y_est = alpha + beta * x ❷
            likelihood = pm.Normal('y', mu=y_est, sd=sigma,
                                    observed=y) ❸

            # inference
            start = pm.find_MAP() ❹
            step = pm.NUTS() ❺
            trace = pm.sample(100, tune=1000, start=start,
                              progressbar=True, verbose=False) ❻
        logp = -1,067.8, ||grad|| = 60.354: 100%|     | 28/28 [00:00<00:00,
         474.70it/s]
        Only 100 samples in chain.
        Auto-assigning NUTS sampler...
        Initializing NUTS using jitter+adapt_diag...
        Multiprocess sampling (2 chains in 2 jobs)
        NUTS: [sigma, beta, alpha]
        Sampling 2 chains: 100%|     | 2200/2200 [00:03<00:00,
         690.96draws/s]

        CPU times: user 6.2 s, sys: 1.72 s, total: 7.92 s
        Wall time: 1min 28s

In [10]: pm.summary(trace) ❼
Out[10]:
             mean      sd mc_error hpd_2.5 hpd_97.5     n_eff     Rhat
    alpha 3.764027 0.174796 0.013177 3.431739 4.070091 152.446951 0.996281
    beta  2.036318 0.030519 0.002230 1.986874 2.094008 106.505590 0.999155
    sigma 2.010398 0.058663 0.004517 1.904395 2.138187 188.643293 0.998547

In [11]: trace[0] ❽
Out[11]: {'alpha': 3.9303300798212444,
          'beta': 2.0020264758995463,
          'sigma_interval__': -1.3519315719461853,
          'sigma': 2.0555476283253156}

❶ 定义先验概率。

❷ 指定线性回归。

❸ 定义似然度。

❹ 通过优化找出起始值。

❺ 实例化MCMC算法。

❻ 用NUTS取得后验样本。

❼ 展示采样的统计摘要。

❽ 从第一个样本估算。

3个估算值都相当接近原始值(4,2,2)。不过,整个过程会得到许多估算值。最好用轨迹图(如图 13-20 所示)描述它们。轨迹图展示了不同参数得到的后验分布以及每个样本的单独估算值。后验分布帮助我们直观地了解了估算值中的不确定性:

In [12]: pm.traceplot(trace, lines={'alpha': 4, 'beta': 2, 'sigma': 2});

图13-16 后验分布及轨迹图

仅从回归中取得alpha和beta值就可以绘制所有的结果回归线(见图13-17):

In [13]: plt.figure(figsize=(10, 6))
         plt.scatter(x, y, c=y, marker='v', cmap='coolwarm')
         plt.colorbar()
         plt.xlabel('x')
         plt.ylabel('y')
         for i in range(len(trace)):
             plt.plot(x, trace['alpha'][i] + trace['beta'][i] * x) ❶

❶ 绘制单条回归线。

图13-17 基于不同估算值的回归线

13.3.3 两种金融工具

用虚拟数据介绍了PyMC3贝叶斯回归之后,转向真实的金融数据很简单。示例使用了两种交易型开放式基金(GLD和GDX)的金融时间序列数据(见图13-18):

In [14]: raw = pd.read_csv('../../source/tr_eikon_eod_data.csv',
                           index_col=0, parse_dates=True)

In [15]: data = raw[['GDX', 'GLD']].dropna()

In [16]: data = data / data.iloc[0] ❶

In [17]: data.info()
         <class 'pandas.core.frame.DataFrame'>
         DatetimeIndex: 2138 entries, 2010-01-04 to 2018-06-29
         Data columns (total 2 columns):
         GDX    2138 non-null float64
         GLD    2138 non-null float64
         dtypes: float64(2)
         memory usage: 50.1 KB

In [18]: data.ix[-1] / data.ix[0] - 1 ❷
Out[18]: GDX   -0.532383
         GLD    0.080601
         dtype: float64

In [19]: data.corr() ❸
Out[19]:          GDX      GLD
         GDX  1.00000  0.71539
         GLD  0.71539  1.00000

In [20]: data.plot(figsize=(10, 6));

❶ 将数据规范化为起始值1。

❷ 计算相对绩效。

❸ 计算两种金融工具之间的相关度。

图13-18 GLD和GDX一段时间后的规范化价格

在下面的例子中,单个数据点的日期用散点图来可视化。为此,将DataFrame中的DatetimeIndex对象转换成matplotlib日期。图13-19展示了时间序列数据的散点图,并将GLD价值与GDX价值进行对照,然后以不同颜色显示每对数据的日期:[7]

In [21]: data.index[:3]
Out[21]: DatetimeIndex(['2010-01-04', '2010-01-05', '2010-01-06'],
          dtype='datetime64[ns]', name='Date', freq=None)
In [22]: mpl_dates = mpl.dates.date2num(data.index.to_pydatetime()) ❶
         mpl_dates[:3]
Out[22]: array([733776., 733777., 733778.])

In [23]: plt.figure(figsize=(10, 6))
         plt.scatter(data['GDX'], data['GLD'], c=mpl_dates,
                     marker='o', cmap='coolwarm')
         plt.xlabel('GDX')
         plt.ylabel('GLD')
         plt.colorbar(ticks=mpl.dates.DayLocator(interval=250),
                      format=mpl.dates.DateFormatter('%d %b %y')); ❷

❶ 将DatetimeIndex对象转换为matplotlib日期。

❷ 自定义日期的色卡。

图13-19 GLD价格与GDX价格散点图

接下来在这两个时间序列基础上实施贝叶斯回归,参数化本质上和前一个虚拟数据示例的过程相同。图13-20展示了在3个参数先验概率分布的假设下,MCMC采样过程的结果:

In [24]: with pm.Model() as model:
             alpha = pm.Normal('alpha', mu=0, sd=20)
             beta = pm.Normal('beta', mu=0, sd=20)
             sigma = pm.Uniform('sigma', lower=0, upper=50)
             y_est = alpha + beta * data['GDX'].values

             likelihood = pm.Normal('GLD', mu=y_est, sd=sigma,
                                    observed=data['GLD'].values)

             start = pm.find_MAP()
             step = pm.NUTS()
             trace = pm.sample(250, tune=2000, start=start,
                               progressbar=True)
        logp = 1,493.7, ||grad|| = 188.29: 100%|     | 27/27 [00:00<00:00,
         1609.34it/s]
        Only 250 samples in chain.
        Auto-assigning NUTS sampler...
        Initializing NUTS using jitter+adapt_diag...
        Multiprocess sampling (2 chains in 2 jobs)
        NUTS: [sigma, beta, alpha]
        Sampling 2 chains: 100%|     | 4500/4500 [00:09<00:00,
         465.07draws/s]
        The estimated number of effective samples is smaller than 200 for some
         parameters.

In [25]: pm.summary(trace)
Out[25]:
             mean      sd mc_error hpd_2.5 hpd_97.5     n_eff     Rhat
    alpha 0.913335 0.005983 0.000356 0.901586 0.924714 184.264900 1.001855
     beta 0.385394 0.007746 0.000461 0.369154 0.398291 215.477738 1.001570
    sigma 0.119484 0.001964 0.000098 0.115305 0.123315 312.260213 1.005246

In [26]: fig = pm.traceplot(trace)

图13-20 GDX和GLD数据的后验分布及轨迹图

图13-21将得到的所有回归线添加到之前的散点图中。但是,所有回归线相互之间很靠近:

In [27]: plt.figure(figsize=(10, 6))
         plt.scatter(data['GDX'], data['GLD'], c=mpl_dates,
                     marker='o', cmap='coolwarm')
         plt.xlabel('GDX')
         plt.ylabel('GLD')
         for i in range(len(trace)):
             plt.plot(data['GDX'],
                      trace['alpha'][i] + trace['beta'][i] * data['GDX'])
         plt.colorbar(ticks=mpl.dates.DayLocator(interval=250),
                      format=mpl.dates.DateFormatter('%d %b %y'));

图13-21 穿过GDX和GLD数据的多条贝叶斯回归线

图 13-21 揭示了所用回归方法的一个重大缺点:该方法没有考虑随时间推移发生的变化。也就是说,最近的数据和最老的数据受到同等对待。

13.3.4 随时更新估算值

正如前面指出的,金融学上的贝叶斯方法通常在历史分析时最有用——也就是说,随着时间推移揭示的新数据可以得出更好的回归和估算。

为了在当前的示例中加入上述概念,假定回归参数不只是随机和呈某种分布的,而且还会随着时间的推移随机“游走”。这和金融理论中从随机变量推广到随机过程(本质上是随机变量的有序序列)时采用相同的方式。

为此,我们定义一个新的PyMC3模型,这次指定参数值随机游走。在指定随机游走参数的分布之后,我们可以继续指定随机游走的alpha和beta值。为了使整个过程更加高效,为50个数据点使用相同的系数:

In [28]: from pymc3.distributions.timeseries import GaussianRandomWalk

In [29]: subsample_alpha = 50
         subsample_beta = 50

In [30]: model_randomwalk = pm.Model()
         with model_randomwalk:
             sigma_alpha = pm.Exponential('sig_alpha', 1. / .02, testval=.1) ❶
             sigma_beta = pm.Exponential('sig_beta', 1. / .02, testval=.1) ❶
             alpha = GaussianRandomWalk('alpha', sigma_alpha ** -2,
                                 shape=int(len(data) / subsample_alpha)) ❷
             beta = GaussianRandomWalk('beta', sigma_beta ** -2,
                                 shape=int(len(data) / subsample_beta)) ❷
             alpha_r = np.repeat(alpha, subsample_alpha) ❸
             beta_r = np.repeat(beta, subsample_beta) ❸
             regression = alpha_r + beta_r * data['GDX'].values[:2100] ❹
             sd = pm.Uniform('sd', 0, 20) ❺
             likelihood = pm.Normal('GLD', mu=regression, sd=sd,
                                    observed=data['GLD'].values[:2100]) ❻

❶ 为随机游走参数定义先验概率。

❷ 随机游走模型。

❸ 将参数向量代入时间间隔长度。

❹ 定义回归模型。

❺ 标准差的先验值。

❻ 用回归结果中的mu定义似然度。

由于使用随机游走代替单一随机变量,所以这些定义都比以前复杂一些。不过,MCMC的推导步骤本质上仍然没有变化。要注意的是,由于我们必须为每个随机游走样本计算一个参数对,共计1950/50=39个(以前只需要1个),所以计算负担明显增大:

In [31]: %%time
         import scipy.optimize as sco
         with model_randomwalk:
             start = pm.find_MAP(vars=[alpha, beta],
                                 fmin=sco.fmin_l_bfgs_b)
             step = pm.NUTS(scaling=start)
             trace_rw = pm.sample(250, tune=1000, start=start,
                                  progressbar=True)
         logp = -6,657: 2%|         | 82/5000 [00:00<00:08, 550.29it/s]
         Only 250 samples in chain.
         Auto-assigning NUTS sampler...
         Initializing NUTS using jitter+adapt_diag...
         Multiprocess sampling (2 chains in 2 jobs)
         NUTS: [sd, beta, alpha, sig_beta, sig_alpha]
         Sampling 2 chains: 100%|     | 2500/2500 [02:48<00:00, 8.59draws/s]

         CPU times: user 27.5 s, sys: 3.68 s, total: 31.2 s
         Wall time: 5min 3s

In [32]: pm.summary(trace_rw).head() ❶
Out[32]:

                 mean       sd  mc_error  hpd_2.5  hpd_97.5      n_eff \
    alpha__0  0.673846  0.040224  0.001376  0.592655  0.753034  1004.616544
    alpha__1  0.424819  0.041257  0.001618  0.348102  0.509757  804.760648
    alpha__2  0.456817  0.057200  0.002011  0.321125  0.553173  800.225916
    alpha__3  0.268148  0.044879  0.001725  0.182744  0.352197  724.967532
    alpha__4  0.651465  0.057472  0.002197  0.544076  0.761216  978.073246
                 Rhat
    alpha__0  0.998637
    alpha__1  0.999540
    alpha__2  0.998075
    alpha__3  0.998995
    alpha__4  0.998060

❶ 每个时间间隔的汇总统计(仅显示前5个和alpha)。

图13-22表现的是估算值的一个子集,说明回归参数alpha和beta随时间演变:

In [33]: sh = np.shape(trace_rw['alpha']) ❶
         sh ❶
Out[33]: (500, 42)

In [34]: part_dates = np.linspace(min(mpl_dates),
                                  max(mpl_dates), sh[1]) ❷

In [35]: index = [dt.datetime.fromordinal(int(date)) for
                  date in part_dates] ❷

In [36]: alpha = {'alpha_%i' % i: v for i, v in
                  enumerate(trace_rw['alpha']) if i < 20} ❸

In [37]: beta = {'beta_%i' % i: v for i, v in
                  enumerate(trace_rw['beta']) if i < 20} ❸

In [38]: df_alpha = pd.DataFrame(alpha, index=index) ❸

In [39]: df_beta = pd.DataFrame(beta, index=index) ❸

In [40]: ax = df_alpha.plot(color='b', style='-.', legend=False,
                            lw=0.7, figsize=(10, 6))
         df_beta.plot(color='r', style='-.', legend=False,
                      lw=0.7, ax=ax)
         plt.ylabel('alpha/beta');

❶ 包含参数估算值的对象构成。

❷ 创建日期列表以匹配时间间隔数量。

❸ 将相关的参数时间序列收集在两个DataFrame对象中。

图13-22 一段时间的选择参数估算值

 

绝对价格数据和相对收益率数据

本节的分析基于规范化价格数据。这仅仅是为了说明目的,因为对应的图形化结果更容易理解和解读(它们在视觉上“更引人注目”)。但是,现实世界中的金融应用应该依赖收益率数据,以确保时间序列数据的稳定性。

使用alpha和beta均值,我们可以说明一段时间内回归的更新。图 13-23 展示了回归随时间推移而更新的情况。此外,它还显示了从alpha和beta均值得出的39条回归线。很明显,随时间推移而更新数大大提高了回归拟合(对于当前/最新的数据)——换言之,每个时间周期都需要自己的回归:

In [41]: plt.figure(figsize=(10, 6))
         plt.scatter(data['GDX'], data['GLD'], c=mpl_dates,
                     marker='o', cmap='coolwarm')
         plt.colorbar(ticks=mpl.dates.DayLocator(interval=250),
                      format=mpl.dates.DateFormatter('%d %b %y'))
         plt.xlabel('GDX')
         plt.ylabel('GLD')
         x = np.linspace(min(data['GDX']), max(data['GDX']))
         for i in range(sh[1]): ❶
             alpha_rw = np.mean(trace_rw['alpha'].T[i])
             beta_rw = np.mean(trace_rw['beta'].T[i])
             plt.plot(x, alpha_rw + beta_rw * x, '--', lw=0.7,
                     color=plt.cm.coolwarm(i / sh[1]))

❶ 为所有长度为50的时间间隔绘制回归线。

图13-23 包含时间相关回归线(更新的估算)的散点图

关于贝叶斯回归的介绍到此告一段落,Python提供了强大的PyMC3库,以实现贝叶斯统计以及概率编程中的不同方法,尤其是贝叶斯回归,它在计量金融学中已经成为相当流行而重要的工具。

本文摘自《Python金融大数据分析 第2版》

《Python金融大数据分析 第2版》分为5部分,共21章。第1部分介绍了Python在金融学中的应用,其内容涵盖了Python用于金融行业的原因、Python的基础架构和工具,以及Python在计量金融学中的一些具体入门实例;第2部分介绍了Python的基础知识以及Python中非常有名的库NumPy和pandas工具集,还介绍了面向对象编程;第3部分介绍金融数据科学的相关基本技术和方法,包括数据可视化、输入/输出操作和数学中与金融相关的知识等;第4部分介绍Python在算法交易上的应用,重点介绍常见算法,包括机器学习、深度神经网络等人工智能相关算法;第5部分讲解基于蒙特卡洛模拟开发期权及衍生品定价的应用,其内容涵盖了估值框架的介绍、金融模型的模拟、衍生品的估值、投资组合的估值等知识。 

《Python金融大数据分析 第2版》本书适合对使用Python进行大数据分析、处理感兴趣的金融行业开发人员阅读。

猜你喜欢

转载自blog.csdn.net/epubit17/article/details/107687120