Python中的Fama Macbeth回归(使用Pandas或Statsmodels)

13

计量经济学背景

Fama Macbeth 回归是一种针对面板数据(其中有 N 个不同的个体,每个个体对应多个时期 T,例如日、月、年)。因此总共有 N x T 个观测值)运行回归的程序。请注意,即使面板数据不平衡也没有关系。

Fama Macbeth 回归首先在每个时期交叉地运行回归,即在给定时期 t 中将 N 个个体汇总起来。并且对于 t=1,...T 进行这样做。因此总共运行 T 次回归。然后我们得到了每个自变量的系数的时间序列。然后,可以使用系数的时间序列执行假设检验。通常,我们将平均值作为每个自变量的最终系数。然后使用 t 统计量测试其显著性。

我的问题

我的问题是如何在 pandas 中实现这一过程。从 pandas 的源代码中,我注意到有一个叫做 fama_macbeth 的过程。但我找不到任何相关文档。

这个操作也可以通过 groupby 轻松完成。目前我正在这样做:

def fmreg(data,formula):
    return smf.ols(formula,data=data).fit().params[1]

res=df.groupby('date').apply(fmreg,'ret~var1')

这是可行的,res 是一个按 date 索引的 Series ,Series 的值是 params[1],即 var1 的系数。但现在我想有更多的自变量,我需要提取所有这些自变量的系数,但我无法解决这个问题。我尝试了以下方法:

def fmreg(data,formula):
    return smf.ols(formula,data=data).fit().params

res=df.groupby('date').apply(fmreg,'ret~var1+var2+var3')

这样做是行不通的。期望的结果是,res 应该是一个以 date 为索引的 dataframe,每列包含每个变量 interceptvar1var2var3 的系数。

我也查了一下 statsmodels,他们也没有这样的内置过程。

是否有任何可以生成出版质量回归表的软件包?像 Stata 中的 outreg2 和 R 中的 texreg 一样?感谢您的帮助!


回答问题的最后一部分:statsmodels有一个summary_col函数,但它缺少一些选项(和花里胡哨的功能)https://github.com/statsmodels/statsmodels/issues/1637 - Josef
谢谢!但是它看起来很粗糙,远不如R的“texreg”或stata的“outreg2”那么整洁。我想我将不得不转向R来输出。 - user3576212
3个回答

13
更新至2018年秋季的Fama-MacBeth法中的库情况。 fama_macbeth 函数已经从pandas中去除了一段时间。那么你有哪些选择呢?
  1. 如果您使用的是Python 3,则可以在LinearModels中使用Fama-MacBeth方法: https://github.com/bashtage/linearmodels/blob/master/linearmodels/panel/model.py

  2. 如果您使用的是Python 2或者不想使用LinearModels,那么您最好自己实现。

例如,假设您拥有以下面板数据中的Fama-French行业组合(您还计算了一些变量,如过去的beta或过去的回报作为x变量):

In [1]: import pandas as pd
        import numpy as np
        import statsmodels.formula.api as smf

In [4]: df = pd.read_csv('industry.csv',parse_dates=['caldt'])
        df.query("caldt == '1995-07-01'")

In [5]: Out[5]: 
      industry      caldt    ret    beta  r12to2  r36to13
18432     Aero 1995-07-01   6.26  0.9696  0.2755   0.3466
18433    Agric 1995-07-01   3.37  1.0412  0.1260   0.0581
18434    Autos 1995-07-01   2.42  1.0274  0.0293   0.2902
18435    Banks 1995-07-01   4.82  1.4985  0.1659   0.2951

Fama-MacBeth主要涉及每个月计算相同的横截面回归模型,因此可以使用groupby实现。您可以创建一个函数,该函数接受dataframe(它将来自groupby)和patsy公式;然后拟合模型并返回参数估计值。以下是如何实现它的简单版本(请注意,这是几年前原提问者尝试做的...不确定为什么不起作用,虽然可能当时statsmodels结果对象方法params没有返回pandasSeries,因此需要显式地将返回值转换为Series...在当前版本的pandas 0.23.4中可以正常工作):
def ols_coef(x,formula):
    return smf.ols(formula,data=x).fit().params

In [9]: gamma = (df.groupby('caldt')
                .apply(ols_coef,'ret ~ 1 + beta + r12to2 + r36to13'))
        gamma.head()

In [10]: Out[10]: 
            Intercept      beta     r12to2   r36to13
caldt                                               
1963-07-01  -1.497012 -0.765721   4.379128 -1.918083
1963-08-01  11.144169 -6.506291   5.961584 -2.598048
1963-09-01  -2.330966 -0.741550  10.508617 -4.377293
1963-10-01   0.441941  1.127567   5.478114 -2.057173
1963-11-01   3.380485 -4.792643   3.660940 -1.210426

然后,只需计算平均值、平均标准误和t检验(或您想要的任何统计数据)。类似以下内容:

def fm_summary(p):
    s = p.describe().T
    s['std_error'] = s['std']/np.sqrt(s['count'])
    s['tstat'] = s['mean']/s['std_error']
    return s[['mean','std_error','tstat']]

In [12]: fm_summary(gamma)
Out[12]: 
               mean  std_error     tstat
Intercept  0.754904   0.177291  4.258000
beta      -0.012176   0.202629 -0.060092
r12to2     1.794548   0.356069  5.039896
r36to13    0.237873   0.186680  1.274230

提高速度

使用statsmodels进行回归分析会有很大的开销(尤其是在你只需要估计系数时)。如果你想要更好的效率,那么可以从statsmodels切换到numpy.linalg.lstsq。编写一个新的函数来进行OLS估计,就像以下示例一样(请注意,我没有做任何像检查这些矩阵的秩之类的事情...):

def ols_np(data,yvar,xvar):
    gamma,_,_,_ = np.linalg.lstsq(data[xvar],data[yvar],rcond=None)
    return pd.Series(gamma)

如果您仍在使用较旧版本的pandas,则以下内容可行:

以下是在pandas中使用fama_macbeth函数的示例:

>>> df

                y    x
date       id
2012-01-01 1   0.1  0.4
           2   0.3  0.6
           3   0.4  0.2
           4   0.0  1.2
2012-02-01 1   0.2  0.7
           2   0.4  0.5
           3   0.2  0.1
           4   0.1  0.0
2012-03-01 1   0.4  0.8
           2   0.6  0.1
           3   0.7  0.6
           4   0.4 -0.1

请注意结构。 fama_macbeth 函数期望 y-var 和 x-vars 具有多索引,其中日期为第一个变量,股票 / 公司 / 实体 ID 为索引的第二个变量:

>>> fm  = pd.fama_macbeth(y=df['y'],x=df[['x']])
>>> fm


----------------------Summary of Fama-MacBeth Analysis-------------------------

Formula: Y ~ x + intercept
# betas :   3

----------------------Summary of Estimated Coefficients------------------------
     Variable          Beta       Std Err        t-stat       CI 2.5%      CI 97.5%
          (x)       -0.0227        0.1276         -0.18       -0.2728        0.2273
  (intercept)        0.3531        0.0842          4.19        0.1881        0.5181

--------------------------------End of Summary---------------------------------

请注意,仅打印fm将调用fm.summary
>>> fm.summary

----------------------Summary of Fama-MacBeth Analysis-------------------------

Formula: Y ~ x + intercept
# betas :   3

----------------------Summary of Estimated Coefficients------------------------
     Variable          Beta       Std Err        t-stat       CI 2.5%      CI 97.5%
          (x)       -0.0227        0.1276         -0.18       -0.2728        0.2273
  (intercept)        0.3531        0.0842          4.19        0.1881        0.5181

--------------------------------End of Summary---------------------------------

请注意,fama_macbeth函数会自动添加一个截距(与statsmodels例程不同)。此外,x变量必须是dataframe,因此如果您只传递一个列,则需要将其作为df[['x']]传递。

如果您不想要一个截距,您需要执行以下操作:

>>> fm  = pd.fama_macbeth(y=df['y'],x=df[['x']],intercept=False)

你介意向我展示如何在 groupby 中手动执行此操作吗?也就是说,我自己的代码第二部分犯了什么错误? - user3576212
我的Statsmodels已经更新了。我想把每个系数作为一行中不同的列,并以“日期”为索引。我将尝试添加pd.Series,看看是否有效。 - user3576212
你的代码对我的简单示例有效:res = df.groupby('date').apply(fmreg,'y~1+x')。当我添加另一个x变量时,它也能正常工作:df.groupby('date').apply(fmreg,'y~x + x2') - Karl D.
没错,我使用你的代码并得到了你所期望的输出结果,没有进行任何实质性的修改。除非你提供一个小的可重现问题的示例,否则我不确定我能真正帮上忙。 - Karl D.
它是否被指定为DateTimeIndexfama_macbeth函数不需要一个常规频率,但它确实需要level_0是一个DateTimeIndex - Karl D.
显示剩余5条评论

4

编辑:新库

存在一个更新的库,可以通过以下命令安装:

pip install finance-byu

文档在这里:https://fin-library.readthedocs.io/en/latest/

新库包括 Fama Macbeth 回归实现和 Regtable 类,可用于报告结果。

此页面的文档概述了 Fama Macbeth 函数:https://fin-library.readthedocs.io/en/latest/fama_macbeth.html

有一个实现与 Karl D. 上面的实现非常相似,使用了 numpy 的线性代数函数,一个利用 joblib 进行并行化以增加数据中大量时间段时的性能,以及一个使用 numba 进行优化的实现,可以在小数据集上节省一个数量级。

以下是一个类似于文档中的小模拟数据集的示例:

>>> from finance_byu.fama_macbeth import fama_macbeth, fama_macbeth_parallel, fm_summary, fama_macbeth_numba
>>> import pandas as pd
>>> import time
>>> import numpy as np
>>> 
>>> n_jobs = 5
>>> n_firms = 1.0e2
>>> n_periods = 1.0e2
>>> 
>>> def firm(fid):
>>>     f = np.random.random((int(n_periods),4))
>>>     f = pd.DataFrame(f)
>>>     f['period'] = f.index
>>>     f['firmid'] = fid
>>>     return f
>>> df = [firm(i) for i in range(int(n_firms))]
>>> df = pd.concat(df).rename(columns={0:'ret',1:'exmkt',2:'smb',3:'hml'})
>>> df.head()

        ret     exmkt       smb       hml  period  firmid
0  0.766593  0.002390  0.496230  0.992345       0       0
1  0.346250  0.509880  0.083644  0.732374       1       0
2  0.787731  0.204211  0.705075  0.313182       2       0
3  0.904969  0.338722  0.437298  0.669285       3       0
4  0.121908  0.827623  0.319610  0.455530       4       0

>>> result = fama_macbeth(df,'period','ret',['exmkt','smb','hml'],intercept=True)
>>> result.head()

        intercept     exmkt       smb       hml
period                                         
0        0.655784 -0.160938 -0.109336  0.028015
1        0.455177  0.033941  0.085344  0.013814
2        0.410705 -0.084130  0.218568  0.016897
3        0.410537  0.010719  0.208912  0.001029
4        0.439061  0.046104 -0.084381  0.199775

>>> fm_summary(result)

               mean  std_error      tstat
intercept  0.506834   0.008793  57.643021
exmkt      0.004750   0.009828   0.483269
smb       -0.012702   0.010842  -1.171530
hml        0.004276   0.010530   0.406119

>>> %timeit fama_macbeth(df,'period','ret',['exmkt','smb','hml'],intercept=True)
123 ms ± 117 µs per loop (mean ± std. dev. of 7 runs, 10 loops each  
>>> %timeit fama_macbeth_parallel(df,'period','ret',['exmkt','smb','hml'],intercept=True,n_jobs=n_jobs,memmap=False)  
146 ms ± 16.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
>>> %timeit fama_macbeth_numba(df,'period','ret',['exmkt','smb','hml'],intercept=True)
5.04 ms ± 5.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

注意:关闭memmap可实现公平比较,无需在每次运行时生成新数据。使用memmap时,并行实现将仅提取缓存的结果。
这里有几个使用模拟数据的表类简单实现:
>>> from finance_byu.regtables import Regtable
>>> import pandas as pd
>>> import statsmodels.formula.api as smf
>>> import numpy as np
>>> 
>>> 
>>> nobs = 1000
>>> df = pd.DataFrame(np.random.random((nobs,3))).rename(columns={0:'age',1:'bmi',2:'hincome'})
>>> df['age'] = df['age']*100
>>> df['bmi'] = df['bmi']*30
>>> df['hincome'] = df['hincome']*100000
>>> df['hincome'] = pd.qcut(df['hincome'],16,labels=False)
>>> df['rich'] = df['hincome'] > 13
>>> df['gender'] = np.random.choice(['M','F'],nobs)
>>> df['race'] = np.random.choice(['W','B','H','O'],nobs)
>>> 
>>> regformulas =  ['bmi ~ age',
>>>                 'bmi ~ np.log(age)',
>>>                 'bmi ~ C(gender) + np.log(age)',
>>>                 'bmi ~ C(gender) + C(race) + np.log(age)',
>>>                 'bmi ~ C(gender) + rich + C(gender)*rich + C(race) + np.log(age)',
>>>                 'bmi ~ -1 + np.log(age)',
>>>                 'bmi ~ -1 + C(race) + np.log(age)']
>>> reg = [smf.ols(f,df).fit() for f in regformulas]
>>> tbl = Regtable(reg)
>>> tbl.render()

产生以下结果: 输入图像描述
>>> df2 = pd.DataFrame(np.random.random((nobs,10)))
>>> df2.columns = ['t0_vw','t4_vw','et_vw','t0_ew','t4_ew','et_ew','mktrf','smb','hml','umd']
>>> regformulas2 = ['t0_vw ~ mktrf',
>>>                't0_vw ~ mktrf + smb + hml',
>>>                't0_vw ~ mktrf + smb + hml + umd',
>>>                't4_vw ~ mktrf',
>>>                't4_vw ~ mktrf + smb + hml',
>>>                't4_vw ~ mktrf + smb + hml + umd',
>>>                'et_vw ~ mktrf',
>>>                'et_vw ~ mktrf + smb + hml',
>>>                'et_vw ~ mktrf + smb + hml + umd',
>>>                't0_ew ~ mktrf',
>>>                't0_ew ~ mktrf + smb + hml',
>>>                't0_ew ~ mktrf + smb + hml + umd',
>>>                't4_ew ~ mktrf',
>>>                't4_ew ~ mktrf + smb + hml',
>>>                't4_ew ~ mktrf + smb + hml + umd',
>>>                'et_ew ~ mktrf',
>>>                'et_ew ~ mktrf + smb + hml',
>>>                'et_ew ~ mktrf + smb + hml + umd'
>>>                ]
>>> regnames = ['Small VW','','',
>>>             'Large VW','','',
>>>             'Spread VW','','',
>>>             'Small EW','','',
>>>             'Large EW','','',
>>>             'Spread EW','',''
>>>             ]
>>> reg2 = [smf.ols(f,df2).fit() for f in regformulas2]
>>> 
>>> tbl2 = Regtable(reg2,orientation='horizontal',regnames=regnames,sig='coeff',intercept_name='alpha',nobs=False,rsq=False,stat='se')
>>> tbl2.render()

产生以下结果:

Regtable类的文档在此处:https://byu-finance-library-finance-byu.readthedocs.io/en/latest/regtables.html

这些表格可以导出为LaTeX格式,方便写作中的使用:

tbl.to_latex()

这些byu链接还有效吗? - Martien Lubberink
@Martien 我刚刚更新了链接和安装信息。文档已经迁移到readthedocs的新位置,并从测试PyPi迁移到正常的PyPI。 - D.J. P.

0

一个快速而简单的解决方案,以解决问题并继续使用您之前使用的同一事物。

这对我起了作用。

def fmreg(data,formula):
    return smf.ols(formula,data=data).fit().params[:]

res = df.groupby('date').apply(fmreg,'ret~var1+var2+var3')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接