在 Python 中处理股票收益率
翻译自
https://www.tidy-finance.org/python/working-with-stock-returns.html
你正在阅读 Tidy Finance with Python。你可以在 这里 找到与之对应的 Tidy Finance with R 的章节。
本章的主要目标是让你熟悉如何使用 tidyverse
来处理股票市场数据。我们专注于从数据提供商雅虎财经下载和可视化股票数据。
在每次会话开始时,我们会加载所需的 Python 包。在本书的整个过程中,我们始终使用 pandas
(McKinney,2010)和 numpy
(Harris 等,2020)包。在本章中,我们还会加载 yfinance
(Aroussi,2023)包来下载股票价格数据。
如果你尚未安装某个包,那么在将其加载到你的 Python 会话之前,你通常需要先安装一次。例如,你可以在终端中调用 pip install pandas
。
1 | import pandas as pd |
下载数据
请注意,import pandas as pd
意味着我们可以在后续使用 pd.function()
的方式调用所有 pandas 函数。相反,使用 from pandas import *
是不被推荐的,因为它会导致命名空间污染。该语句会将 pandas
中的所有函数和类导入到你的当前命名空间中,可能会与你自己定义的函数或其他已导入库中的函数产生冲突。使用 pd
缩写是一种非常方便的方式来避免这种情况。
我们首先从雅虎财经直接下载一只股票的每日价格,例如苹果公司股票(AAPL)。要下载数据,你可以使用 yf.download()
函数。
在以下代码中,我们请求从 2000 年初到上一年年底的数据,这是一个超过 20 年的时期。
1 | prices = (yf.download( |
date | adjusted | close | high | low | open | volume | symbol | |
---|---|---|---|---|---|---|---|---|
0 | 2000-01-03 | 0.842 | 0.999 | 1.004 | 0.908 | 0.936 | 535796800 | AAPL |
1 | 2000-01-04 | 0.771 | 0.915 | 0.988 | 0.903 | 0.967 | 512377600 | AAPL |
2 | 2000-01-05 | 0.782 | 0.929 | 0.987 | 0.920 | 0.926 | 778321600 | AAPL |
3 | 2000-01-06 | 0.715 | 0.848 | 0.955 | 0.848 | 0.948 | 767972800 | AAPL |
4 | 2000-01-07 | 0.749 | 0.888 | 0.902 | 0.853 | 0.862 | 460734400 | AAPL |
yf.download()
从雅虎财经下载股票市场数据。上述代码块返回一个包含八列的 DataFrame,这些列的含义都很明确:symbol
、date
、每日 volume
(以交易股票数量表示)、开盘、最高、最低、收盘的市场价格以及以美元为单位的 adjusted
价格。调整后的价格会纠正任何可能在市场收盘后影响股票价格的因素,例如股票拆分和股息。这些行为会影响报价价格,但对持有股票的投资者没有直接影响。因此,在分析投资者通过持续持有股票所获得的收益时,我们通常依赖于调整后的价格。
接下来,我们使用 plotnine
包(Kibirige,2023)来可视化调整后价格的时间序列(见图 1)。该包基于图形语法(Wilkinson,2012)的原则来处理可视化任务。请注意,我们通常不推荐使用 *
导入方式。然而,我们在这里仅将其用于绘图函数,这些函数是 plotnine
所特有的,并且名称与绘图密切相关。因此,由于命名空间污染而导致误用的风险很小。
使用图形语法创建图表非常直观,如下述代码块所示。
1 | ( |
图 1:价格以美元为单位,已调整股息支付和股票拆分。
计算收益率
我们不分析价格,而是计算每日收益率,定义为 ( \frac{P_t - P_{t-1}}{P_{t-1}} ),其中 ( P_t ) 是第 ( t ) 天末的调整后价格。在这种情况下,lag()
函数很有帮助,它可以返回之前的值。
1 | returns = (prices |
symbol | date | ret | |
---|---|---|---|
0 | AAPL | 2000-01-03 | NaN |
1 | AAPL | 2000-01-04 | -0.084310 |
2 | AAPL | 2000-01-05 | 0.014633 |
3 | AAPL | 2000-01-06 | -0.086538 |
4 | AAPL | 2000-01-07 | 0.047369 |
… | … | … | … |
6032 | AAPL | 2023-12-22 | -0.005548 |
6033 | AAPL | 2023-12-26 | -0.002841 |
6034 | AAPL | 2023-12-27 | 0.000518 |
6035 | AAPL | 2023-12-28 | 0.002226 |
6036 | AAPL | 2023-12-29 | -0.005424 |
6037 行 × 3 列
得到的 DataFrame 包含三列,最后一列包含每日收益率(ret
)。请注意,第一行自然包含一个缺失值(NaN
),因为没有前一个价格。显然,如果时间序列未按升序日期排序,使用 pct_change()
将毫无意义。sort_values()
函数提供了一种方便的方式来按正确的方式排序观测值。如果你想按降序日期排序观测值,可以使用参数 ascending=False
。
对于接下来的示例,我们移除缺失值,因为这些值在许多应用中需要单独处理。例如,缺失值可能会影响总和和平均值,因为如果未正确处理,它们会减少有效数据点的数量。一般来说,始终要确保你明白 NaN
值出现的原因,并仔细检查是否可以简单地移除这些观测值。
1 | returns = returns.dropna() |
接下来,我们在图 2 中用直方图可视化每日收益率的分布。此外,我们在直方图中画了一条虚线,表示每日收益率的历史 5% 分位数,这是最坏可能收益率的粗略代理,其概率最多
为 5%。这个分位数与(历史)风险价值密切相关,这是一种通常由监管机构监控的风险度量。我们参考 Tsay(2010)以更全面地介绍金融收益率的典型事实。
1 | from mizani.formatters import percent_format |
图 2:虚线垂直线表示每日收益率的历史 5% 分位数。
这里,bins=100
决定了用于插图的箱数,因此隐含地设置了箱的宽度。在继续之前,请确保你理解如何使用 geom_vline()
添加一条虚线,以表示每日收益率的历史 5% 分位数。在处理任何数据之前,一个典型的任务是计算并分析主要变量的描述性统计量。
1 | pd.DataFrame(returns["ret"].describe()).round(3).T |
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
ret | 6036.0 | 0.001 | 0.025 | -0.519 | -0.01 | 0.001 | 0.013 | 0.139 |
我们看到,最大 日 收益率为 13.9%。也许并不令人惊讶的是,平均日收益率接近但略高于 0。与上述插图一致的是,最低收益率当天的巨大损失表明收益率分布存在显著的不对称性。
你还可以通过调用 groupby(returns["date"].dt.year)
为每个单独的年份计算这些描述性统计量,其中 dt.year
返回年份。更具体地说,下面的几行代码为由列 year
的值定义的各个数据组计算上述描述性统计量。因此,描述性统计量允许对日收益率分布的时间序列动态进行初步分析。
1 | (returns["ret"] |
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
date | ||||||||
— | — | — | — | — | — | — | — | — |
2000 | 251.0 | -0.003 | 0.055 | -0.519 | -0.034 | -0.002 | 0.027 | 0.137 |
2001 | 248.0 | 0.002 | 0.039 | -0.172 | -0.023 | -0.001 | 0.027 | 0.129 |
2002 | 252.0 | -0.001 | 0.031 | -0.150 | -0.019 | -0.003 | 0.018 | 0.085 |
2003 | 252.0 | 0.002 | 0.023 | -0.081 | -0.012 | 0.002 | 0.015 | 0.113 |
2004 | 252.0 | 0.005 | 0.025 | -0.056 | -0.009 | 0.003 | 0.016 | 0.132 |
2005 | 252.0 | 0.003 | 0.024 | -0.092 | -0.010 | 0.003 | 0.017 | 0.091 |
2006 | 251.0 | 0.001 | 0.024 | -0.063 | -0.014 | -0.002 | 0.014 | 0.118 |
2007 | 251.0 | 0.004 | 0.024 | -0.070 | -0.009 | 0.003 | 0.018 | 0.105 |
2008 | 253.0 | -0.003 | 0.037 | -0.179 | -0.024 | -0.001 | 0.019 | 0.139 |
2009 | 252.0 | 0.004 | 0.021 | -0.050 | -0.009 | 0.002 | 0.015 | 0.068 |
2010 | 252.0 | 0.002 | 0.017 | -0.050 | -0.006 | 0.002 | 0.011 | 0.077 |
2011 | 252.0 | 0.001 | 0.017 | -0.056 | -0.009 | 0.001 | 0.011 | 0.059 |
2012 | 250.0 | 0.001 | 0.019 | -0.064 | -0.008 | 0.000 | 0.012 | 0.089 |
2013 | 252.0 | 0.000 | 0.018 | -0.124 | -0.009 | -0.000 | 0.011 | 0.051 |
2014 | 252.0 | 0.001 | 0.014 | -0.080 | -0.006 | 0.001 | 0.010 | 0.082 |
2015 | 252.0 | 0.000 | 0.017 | -0.061 | -0.009 | -0.001 | 0.009 | 0.057 |
2016 | 252.0 | 0.001 | 0.015 | -0.066 | -0.006 | 0.001 | 0.008 | 0.065 |
2017 | 251.0 | 0.002 | 0.011 | -0.039 | -0.004 | 0.001 | 0.007 | 0.061 |
2018 | 251.0 | -0.000 | 0.018 | -0.066 | -0.009 | 0.001 | 0.009 | 0.070 |
2019 | 252.0 | 0.003 | 0.016 | -0.100 | -0.005 | 0.003 | 0.012 | 0.068 |
2020 | 253.0 | 0.003 | 0.029 | -0.129 | -0.010 | 0.002 | 0.017 | 0.120 |
2021 | ||||||||
252.0 | 0.001 | 0.016 | -0.042 | -0.008 | 0.001 | 0.012 | 0.054 | |
2022 | 251.0 | -0.001 | 0.022 | -0.059 | -0.016 | -0.001 | 0.014 | 0.089 |
2023 | 250.0 | 0.002 | 0.013 | -0.048 | -0.006 | 0.002 | 0.009 | 0.047 |
扩展分析
下一步,我们将之前的代码进行扩展,以便能够处理任意数量的股票代码(例如某个指数的所有成分股)。遵循整洁数据原则,将之前的计算扩展到任意数量的资产或分组变得非常容易。以下代码可以处理任意数量的股票代码,例如 symbol = ["AAPL", "MMM", "BA"]
,并自动化下载以及绘制价格时间序列的图表。最后,我们一次性为所有资产创建描述性统计量的表格。在这个例子中,我们分析道琼斯工业平均指数所有当前成分股的数据。
我们首先从一个外部网站下载一个包含道琼斯成分股的表格。请注意,当你直接从网络读取成分股时,需要临时修改 Python 的 ssl
模块处理 SSL 证书的行为。这种方法应该谨慎使用,因此我们在数据下载成功后将设置恢复为默认行为。
1 | import ssl |
幸运的是,yf.download()
提供了在单次调用中获取某个时间点指数中所有股票价格的功能。
1 | prices_daily = (yf.download( |
得到的 DataFrame 包含 177,925 个每日观测值,涵盖 30 种不同的股票。图 3 展示了道琼斯指数成分股的调整后价格的时间序列。请确保你理解代码的每一行!aes()
的参数是什么?你可以使用哪些替代的 geoms
来可视化时间序列?提示:如果你不知道答案,尝试更改代码,看看你的干预会有什么不同。
1 | from mizani.breaks import date_breaks |
图 3:价格以美元为单位,已调整股息支付和股票拆分。
你是否注意到与之前使用的代码的细微差别?为了同时绘制所有股票代码,我们只需要在 ggplot
美学中包含 color = symbol
。这样,我们就可以为每个股票代码生成一条单独的线。当然,图上有太多条线,无法正确识别各个股票,但它很好地说明了如何将特定分析扩展到任意数量的主题。
同样的逻辑也适用于股票收益率。在计算收益率之前,我们使用 groupby("symbol")
,以便 assign()
命令针对每个股票代码分别执行。同样的逻辑也适用于描述性统计量的计算:groupby("symbol")
是将时间序列聚合为符号特定变量的关键。
1 | returns_daily = (prices_daily |
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
symbol | ||||||||
— | — | — | — | — | — | — | — | — |
AAPL | 6036.0 | 0.001 | 0.025 | -0.519 | -0.010 | 0.001 | 0.013 | 0.139 |
AMGN | 6036.0 | 0.000 | 0.019 | -0.134 | -0.009 | 0.000 | 0.009 | 0.151 |
AMZN | 6036.0 | 0.001 | 0.032 | -0.248 | -0.012 | 0.000 | 0.014 | 0.345 |
AXP | 6036.0 | 0.001 | 0.023 | -0.176 | -0.009 | 0.000 | 0.010 | 0.219 |
BA | 6036.0 | 0.001 | 0.022 | -0.238 | -0.010 | 0.001 | 0.011 | 0.243 |
CAT | 6036.0 | 0.001 | 0.020 | -0.145 | -0.010 | 0.001 | 0.011 | 0.147 |
CRM | 4914.0 | 0.001 | 0.027 | -0.271 | -0.012 | 0.001 | 0.014 | 0.260 |
CSCO | 6036.0 | 0.000 | 0.023 | -0.162 | -0.009 | 0.000 | 0.010 | 0.244 |
CVX | 6036.0 | 0.001 | 0.018 | -0.221 | -0.008 | 0.001 | 0.009 | 0.227 |
DIS | 6036.0 | 0.000 | 0.019 | -0.184 | -0.009 | 0.000 | 0.009 | 0.160 |
GS | 6036.0 | 0.001 | 0.023 | -0.190 | -0.010 | 0.000 | 0.011 | 0.265 |
HD | 6036.0 | 0.001 | 0.019 | -0.287 | -0.008 | 0.001 | 0.009 | 0.141 |
HON | 6036.0 | 0.000 | 0.019 | -0.174 | -0.008 | 0.001 | 0.009 | 0.282 |
IBM | ||||||||
6036.0 | 0.000 | 0.016 | -0.155 | -0.007 | 0.000 | 0.008 | 0.120 | |
JNJ | 6036.0 | 0.000 | 0.012 | -0.158 | -0.005 | 0.000 | 0.006 | 0.122 |
JPM | 6036.0 | 0.001 | 0.024 | -0.207 | -0.009 | 0.000 | 0.010 | 0.251 |
KO | 6036.0 | 0.000 | 0.013 | -0.101 | -0.005 | 0.000 | 0.006 | 0.139 |
MCD | 6036.0 | 0.001 | 0.015 | -0.159 | -0.006 | 0.001 | 0.007 | 0.181 |
MMM | 6036.0 | 0.000 | 0.015 | -0.129 | -0.007 | 0.000 | 0.008 | 0.126 |
MRK | 6036.0 | 0.000 | 0.017 | -0.268 | -0.007 | 0.000 | 0.008 | 0.130 |
MSFT | 6036.0 | 0.001 | 0.019 | -0.156 | -0.008 | 0.000 | 0.009 | 0.196 |
NKE | 6036.0 | 0.001 | 0.019 | -0.198 | -0.008 | 0.001 | 0.009 | 0.155 |
NVDA | 6036.0 | 0.002 | 0.038 | -0.352 | -0.016 | 0.001 | 0.018 | 0.424 |
PG | 6036.0 | 0.000 | 0.013 | -0.302 | -0.005 | 0.000 | 0.006 | 0.120 |
SHW | 6036.0 | 0.001 | 0.018 | -0.208 | -0.008 | 0.001 | 0.009 | 0.153 |
TRV | 6036.0 | 0.001 | 0.018 | -0.208 | -0.007 | 0.001 | 0.008 | 0.256 |
UNH | 6036.0 | 0.001 | 0.020 | -0.186 | -0.008 | 0.001 | 0.010 | 0.348 |
V | 3973.0 | 0.001 | 0.019 | -0.136 | -0.008 | 0.001 | 0.009 | 0.150 |
VZ | 6036.0 | 0.000 | 0.015 | -0.118 | -0.007 | 0.000 | 0.007 | 0.146 |
WMT | 6036.0 | 0.000 | 0.015 | -0.114 | -0.007 | 0.000 | 0.007 | 0.117 |
不同频率的数据
金融数据通常由于不同的报告时间表、交易日历和经济数据发布时间而以不同的频率存在。例如,股票价格通常每天记录一次,而宏观经济指标(如 GDP 或通货膨胀率)则每月或每季度报告一次。此外,一些数据集仅在发生交易时记录,导致时间戳不规则。为了有意义地比较数据,我们需要适当地对齐不同频率的数据。例如,为了比较不同频率的收益率,我们使用年化技术。
到目前为止,我们一直在处理日收益率,但我们可以轻松地将数据转换为其他频率。让我们从日数据中创建月收益率:
1 | returns_monthly = (returns_daily |
在这段代码中,我们首先按股票代码和月份对数据进行分组,然后通过复合日收益率来计算月收益率:( \text{ret}{\text{monthly}} = \prod{i=1}^{n} (1 + \text{ret}_{\text{daily}, i}) - 1 )。为了可视化收益率特征如何随不同频率变化,我们可以比较直方图,如图 4 所示:
1 | apple_daily = (returns_daily |
图 4:收益率基于调整后的价格,已调整股息支付和股票拆分。
其他形式的数据聚合
当然,除了按 symbol
或 date
进行聚合之外,按其他变量进行聚合也可能有意义。例如,假设你想回答这样的问题:交易量高的日子是否通常会跟随交易量高的日子?为了对这个问题进行初步分析,我们取下载的数据并计算所有道琼斯指数成分股的每日总交易量(以美元为单位)。回想一下,volume
列是以交易股票数量表示的。因此,我们将交易量乘以每日调整后的收盘价,得到一个以美元为单位的总交易量的代理值。乘以 1e-9
(Python 可以处理科学计数法)表示每日交易量以十亿美元为单位。
1 | trading_volume = (prices_daily |
图 5:每日总交易量(以十亿美元为单位)。
图 5 显示出总交易量有明显的上升趋势。特别是自新冠疫情爆发以来,市场处理了大量的交易量,正如 Goldstein、Koijen 和 Mueller(2021)所分析的那样。一种展示交易量持续性的方式是绘制第 ( t ) 天的交易量与第 ( t-1 ) 天的交易量之间的关系,如下面的例子所示。在图 6 中
,我们添加了一条虚线的 45° 线,以表示假设的一比一关系(通过 geom_abline()
),以解决轴刻度可能不同的问题。
1 | ( |
图 6:每日总交易量(以十亿美元为单位)。
仅凭肉眼观察就可以发现,交易量高的日子通常会跟随交易量高的日子。
重点总结
在本章中,你学习了如何使用 Python 和整洁数据原则来下载、分析和可视化股票市场数据:
- 整洁数据原则可以高效地分析金融数据。
- 调整后的价格考虑了股票拆分和股息等公司行为。
- 描述性统计量有助于识别金融数据中的关键模式。
- 可视化技术可以揭示收益率的趋势和分布。
- 使用
tidyverse
进行的数据操作可以轻松扩展到多个资产。 - 一致的工作流程是高级金融分析的基础。
练习
- 使用
yf.download()
从雅虎财经下载你选择的另一个股票市场的每日价格。绘制该股票的未调整收盘价和调整后收盘价的时间序列图。解释两者之间的任何可见差异。 - 计算你选择的资产的日净收益率,并使用 100 个箱体在直方图中可视化日收益率的分布。同时,使用
geom_vline()
添加一条红色虚线,表示日收益率的 5% 分位数。计算日收益率的描述性统计量(均值、标准差、最小值和最大值)。 - 将你在前面练习中的代码进行扩展,以便你可以对任意数量的股票代码(例如
symbol = ["AAPL", "MMM", "BA"]
)进行所有计算。自动化下载、价格时间序列的绘制,并为这些任意数量的资产创建收益率描述性统计量的表格。 - 为了便于计算年化因子,编写一个函数,输入为收益率日期向量,确定频率后返回适当的年化因子。
- 交易量高的日子是否通常也是绝对收益率大的日子?使用
AAPL
股票找到一个合适的可视化方法来分析这个问题。