Python-机器学习算法交易实用指南-全-

Python 机器学习算法交易实用指南(全)

原文:zh.annas-archive.org/md5/fcb09c483bdb21866eb6782158d1f8d5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

多样化数据的可用性增加了对算法交易策略专业知识的需求。通过本书,您将选择并应用机器学习ML)到广泛的数据源,并创建强大的算法策略。

本书将首先介绍一些基本要素,如评估数据集、使用 Python 访问数据 API、使用 Quandl 访问金融数据以及管理预测误差。然后我们将涵盖各种机器学习技术和算法,这些技术和算法可用于使用 pandas、Seaborn、StatsModels 和 sklearn 构建和训练算法模型。然后我们将使用 StatsModels 构建、估计和解释 AR(p)、MA(q) 和 ARIMA(p, d, q) 模型。您将应用贝叶斯先验、证据和后验的概念,以区分使用 PyMC3 的不确定性概念。然后我们将利用 NLTK、sklearn 和 spaCy 为财经新闻分配情感得分,并对文档进行分类以提取交易信号。我们将学习设计、构建、调整和评估前馈神经网络、循环神经网络RNNs)和卷积神经网络CNNs),使用 Keras 设计复杂的算法。您将应用迁移学习到卫星图像数据,以预测经济活动。最后,我们将应用强化学习来获得最佳交易结果。

通过本书,您将能够采用算法交易来实现智能投资策略。

本书的受众

本书适用于数据分析师、数据科学家和 Python 开发人员,以及在金融和投资行业工作的投资分析师和投资组合经理。如果您想通过开发智能调查策略使用 ML 算法来执行高效的算法交易,那么这就是您需要的书!对 Python 和 ML 技术的一些理解是必需的。

本书内容

第一章,用于交易的机器学习,通过概述 ML 在生成和评估信号以设计和执行交易策略中的重要性来确定本书的重点。它从假设生成和建模、数据选择和回测到评估和执行在投资组合背景下的策略过程进行了概述,包括风险管理。

第二章,市场与基础数据,介绍了数据来源以及如何处理原始交易所提供的 tick 和财务报告数据,以及如何访问许多本书中将依赖的开源数据提供商。

第三章,金融替代数据,提供了评估不断增加的数据来源和供应商的分类和标准。它还演示了如何通过网站爬取创建替代数据集,例如收集用于第二部分书籍中的自然语言处理NLP)和情感分析算法的收益电话转录。

第四章,Alpha 因子研究,提供了理解因子工作原理以及如何衡量其绩效的框架,例如使用信息系数IC)。它演示了如何使用 Python 库离线和在 Quantopian 平台上工程化数据生成 alpha 因子。它还介绍了使用zipline库对因子进行回测和使用alphalens库评估其预测能力。

第五章,战略评估,介绍了如何利用历史数据使用zipline离线和在 Quantopian 平台上建立、测试和评估交易策略。它展示并演示了如何使用pyfolio库计算投资组合绩效和风险指标。它还讨论了如何处理策略回测的方法论挑战,并介绍了从投资组合风险角度优化策略的方法。

第六章,机器学习工作流,通过概述如何构建、训练、调整和评估 ML 模型的预测性能作为系统化工作流程,为后续章节做好铺垫。

第七章,线性模型,展示了如何使用线性和逻辑回归进行推断和预测,以及如何使用正则化来管理过拟合的风险。它介绍了 Quantopian 交易平台,并演示了如何构建因子模型并预测资产价格。

第八章,时间序列模型,涵盖了单变量和多变量时间序列,包括向量自回归模型和协整检验,以及它们如何应用于配对交易策略。

第九章,贝叶斯机器学习,介绍了如何制定概率模型以及如何使用马尔可夫链蒙特卡罗MCMC)采样和变分贝叶斯来进行近似推断。它还说明了如何使用 PyMC3 进行概率编程以深入了解参数和模型的不确定性。

第十章,决策树和随机森林,展示了如何构建、训练和调整非线性基于树的模型以进行洞察和预测。它介绍了基于树的集成模型,并展示了随机森林如何使用自举聚合来克服决策树的一些弱点。第十一章,梯度提升机,展示了如何使用库xgboostlightgbmcatboost进行高性能训练和预测,并深入审查了如何调整众多超参数。

第十一章,梯度提升机,演示了如何使用库xgboostlightgbmcatboost进行高性能训练和预测,并深入审查了如何调整众多超参数。

第十二章,无监督学习,介绍了如何使用降维和聚类进行算法交易。它使用主成分和独立成分分析来提取数据驱动的风险因素。它提出了几种聚类技术,并演示了如何使用层次聚类进行资产配置。

第十三章,处理文本数据,演示了如何将文本数据转换为数值格式,并将第二部分中的分类算法应用于大型数据集的情感分析。

第十四章,主题建模,应用贝叶斯无监督学习来提取能够总结大量文档的潜在主题,并提供更有效地探索文本数据或将主题用作分类模型特征的方法。它演示了如何将这一技术应用于第三章,金融替代数据,中来源的盈利电话交易摘要和向证券交易委员会SEC)提交的年度报告。

第十五章,词嵌入,使用神经网络学习形式的最新语言特征,即捕获语义上下文比传统文本特征更好的词向量,并代表从文本数据中提取交易信号的一个非常有前途的途径。

第十六章,下一步,是对所有前面章节的总结

第十七章深度学习,介绍了 Keras、TensorFlow 和 PyTorch,这是我们将在第四部分中使用的最流行的深度学习框架。它还介绍了训练和调整的技术,包括正则化,并提供了常见架构的概述。要阅读此章节,请访问链接www.packtpub.com/sites/default/files/downloads/Deep_Learning.pdf

第十八章循环神经网络,展示了 RNN 在序列到序列建模中的用途,包括用于时间序列。它演示了 RNN 如何在较长时间段内捕捉非线性模式。要阅读此章节,请访问链接www.packtpub.com/sites/default/files/downloads/Recurrent_Neural_Networks.pdf

第十九章卷积神经网络,涵盖了 CNN 在大规模非结构化数据分类任务中的强大性能。我们将介绍成功的架构设计,例如在卫星数据上训练 CNN,以预测经济活动,并使用迁移学习加速训练。要阅读此章节,请访问链接www.packtpub.com/sites/default/files/downloads/Convolutions_Neural_Networks.pdf

第二十章自编码器和生成对抗网络,介绍了无监督深度学习,包括用于高维数据非线性压缩的自编码器和生成对抗网络GANs),这是生成合成数据的最重要的最近创新之一。要阅读此章节,请访问链接www.packtpub.com/sites/default/files/downloads/Autoencoders_and_Generative_Adversarial_Nets.pdf

第二十一章强化学习,介绍了允许设计和训练代理程序以随着时间响应其环境优化决策的强化学习。您将看到如何构建一个通过 Open AI gym 响应市场信号的代理。要阅读此章节,请访问链接www.packtpub.com/sites/default/files/downloads/Reinforcement_Learning.pdf

要充分利用本书

本书所需的全部内容只需要基本的 Python 和机器学习技术的理解。

下载示例代码文件

您可以从 www.packt.com 的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问 www.packt.com/support 并注册,以便直接将文件发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packt.com 登录或注册。

  2. 选择 支持 选项卡。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名并按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的软件解压缩文件夹:

  • Windows 系统使用 WinRAR/7-Zip

  • Mac 系统使用 Zipeg/iZip/UnRarX

  • Linux 系统使用 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Hands-On-Machine-Learning-for-Algorithmic-Trading。如果代码有更新,将在现有的 GitHub 仓库上进行更新。

我们还提供了来自我们丰富的图书和视频目录的其他代码包,可在 github.com/PacktPublishing/ 上获取。快去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789346411_ColorImages.pdf

使用的约定

本书中使用了一些文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 句柄。例如:“调用run_algorithm()函数后,算法继续执行,并返回相同的回测性能DataFrame。”

代码块设置如下:

interesting_times = extract_interesting_date_ranges(returns=returns)
interesting_times['Fall2015'].to_frame('pf') \
    .join(benchmark_rets) \
    .add(1).cumprod().sub(1) \
    .plot(lw=2, figsize=(14, 6), title='Post-Brexit Turmoil')

粗体:表示新术语、重要单词或屏幕上看到的单词。

警告或重要说明显示如下。

提示和技巧显示如下。

第一章:交易机器学习

算法交易依赖于执行算法的计算机程序,以自动化交易策略的某些或全部元素。算法是实现目标的一系列步骤或规则,可以采用许多形式。在机器学习ML)的情况下,算法追求学习其他算法的目标,即根据数据学习规则,例如最小化预测误差。

这些算法编码了组合经理的各种活动,组合经理观察市场交易并分析相关数据,以决定是否下达买入或卖出订单。订单的顺序定义了组合持有的投资组合,该投资组合的目标是随时间产生对资本提供者有吸引力的回报,考虑到他们对风险的偏好。

最终,主动投资管理的目标在于实现阿尔法,即超过用于评估的基准的回报。主动管理的基本法则将信息比率IR)应用于表达主动管理的价值,即投资组合回报超过基准回报(通常是指数)与这些回报的波动性之比。它将信息比率近似为信息系数IC)的乘积,信息系数衡量了预测的质量,即它们与结果的相关性,以及策略的广度,表达为赌注数量的平方根。

因此,产生阿尔法的关键是预测。成功的预测反过来又需要优越的信息或者对公共信息的优越处理能力。算法促进了整个投资过程的优化,从资产配置到构思、交易执行和风险管理。特别是利用机器学习进行算法交易,旨在更有效地利用传统和替代数据,以产生更好且更具可行性的预测,从而提高主动管理的价值。

从历史上看,算法交易曾被更狭义地定义为执行交易自动化以尽量降低成本,这是由卖方提供的,但我们将采用更全面的视角,因为算法的使用,尤其是机器学习,已经开始影响更广泛的活动,从构思和阿尔法因子设计到资产配置、头寸规模和策略的测试和评估。

本章探讨了如何将 ML 的使用作为投资行业竞争优势的关键来源,并将其纳入投资过程,以实现算法交易策略。

我们将在本章中涵盖以下主题:

  • 本书的组织方式及适合阅读本书的读者

  • 机器学习如何在算法交易中发挥战略作用

  • 如何设计和执行交易策略

  • 机器学习如何为算法交易策略增加价值

如何阅读本书

如果您正在阅读本文,那么您可能已经意识到机器学习已经成为许多行业,包括投资行业的战略能力。驱动机器学习兴起的数字数据爆炸对投资产生了特别强大的影响,这在处理信息方面已经有很长的历史了。跨资产类别的交易范围意味着除了曾经是分析工作重点的市场和基本数据之外,还可能涉及广泛的新型替代数据。

你可能也注意到,成功应用机器学习或数据科学需要个人或团队在统计知识、计算能力和领域专业知识方面的整合。换句话说,关键是提出正确的问题,识别和理解可能提供答案的数据,使用各种工具获取结果,并以能够做出正确决策的方式解释它们。

因此,本书从整体上探讨了将机器学习应用于投资和交易领域。在本节中,我们将介绍预期会发生什么,它如何实现其目标,以及您需要什么来实现自己的目标并在此过程中享受乐趣。

预期

本书旨在为您提供将机器学习应用于交易和投资过程中增加价值的战略视角、概念理解和实用工具。为此,它将机器学习作为流程中的重要组成部分而不是独立的练习。

首先,它涵盖了一系列有用于从与不同资产类别相关的多样化数据源中提取信号的监督、无监督和强化学习算法。它介绍了一个机器学习工作流,并专注于具有相关数据和大量代码示例的实际用例。然而,它还发展了数学和统计背景,以便调整算法或解释结果。

该书认识到投资者可以从第三方数据中提取的价值超过其他行业。因此,它不仅涵盖了如何处理市场和基本数据,还涵盖了如何获取、评估、处理和建模另类数据源,例如非结构化文本和图像数据。

它将机器学习的应用与研究和评估阿尔法因子、定量和基于因子的策略联系起来,并将投资组合管理作为部署结合多个阿尔法因子的策略的背景。它还强调了机器学习可以为除了与个别资产价格相关的预测之外的价值,例如资产配置,并解决了使用大型数据集开发交易策略时的错误发现风险。

这本书没有提供投资建议或现成的交易算法,这应该不足为奇。相反,它提供了识别、评估和组合适用于任何特定投资目标的数据集所需的基本构建模块,选择并应用 ML 算法到这些数据,以及基于结果开发和测试算法交易策略的方法。

谁应该阅读这本书

如果您是分析师、数据科学家或具有对金融市场的了解和对交易策略感兴趣的 ML 工程师,您应该会发现这本书很有价值。如果您是一名投资专业人士,希望利用 ML 做出更好的决策,您也应该会受益。

如果您的背景是软件和 ML,您可能只需浏览或跳过 ML 的一些入门材料。同样,如果您的专业是投资,您可能会熟悉一些或所有的金融背景。您可能会发现这本书最有用,因为它涵盖了关键算法、构建模块和用例的概述,而不是针对特定算法或策略的专业覆盖。然而,这本书假设您对继续学习这个非常动态的领域感兴趣。为此,它引用了许多资源来支持您朝着利用和建立基本方法和工具的定制交易策略的旅程。

您应该熟悉使用 Python 3 以及各种科学计算库,如numpypandasscipy,并有兴趣途中学习更多。一些 ML 和 scikit-learn 的经验可能会有所帮助,但我们简要介绍了基本的工作流程,并参考了各种资源来填补或深入研究。

书的组织结构

本书全面介绍了 ML 如何增加交易策略的设计和执行的价值。它分为四个部分,涵盖了数据采集和策略开发过程的不同方面,以及解决各种 ML 挑战的不同解决方案。

第一部分 - 框架 - 从数据到策略设计

第一部分提供了算法交易策略开发的框架。它重点介绍了驱动 ML 算法和本书讨论的策略的数据,概述了如何使用 ML 来推导交易信号,以及如何将策略部署和评估为投资组合的一部分。

本章剩余部分概述了 ML 如何成为投资的核心,描述了交易流程,并概述了 ML 如何增加价值。第二章,市场和基本数据,涵盖了源头以及如何使用原始交易所提供的 tick 和财务报告数据,以及如何访问我们在本书中将依赖的众多开源数据提供者。

第三章,金融的另类数据,提供了评估不断增加的来源和提供者的类别和标准。它还演示了如何通过网站抓取来创建另类数据集,例如收集用于本书第二部分中的自然语言处理NLP)和情感分析算法的盈利电话转录。

第四章,Alpha 因子研究,提供了理解因子如何工作以及如何衡量其绩效的框架,例如使用信息系数IC)。它演示了如何使用 Python 库离线和在 Quantopian 平台上从数据中设计 alpha 因子。它还介绍了使用zipline库对因子进行回测以及使用alphalens库评估其预测能力的方法。

第五章,策略评估,介绍了如何使用历史数据通过zipline离线和在 Quantopian 平台上构建、测试和评估交易策略。它介绍并演示了如何使用pyfolio库计算投资组合绩效和风险指标。它还介绍了如何应对策略回测的方法论挑战,并介绍了从投资组合风险角度优化策略的方法。

第二部分 - ML 基础知识

第二部分涵盖了基本的监督和无监督学习算法,并说明了它们在交易策略中的应用。它还介绍了 Quantopian 平台,您可以在该平台上利用本书中开发的数据和 ML 技术来实施在实时市场中执行交易的算法策略。

第六章,机器学习流程,通过概述如何制定、训练、调整和评估 ML 模型的预测性能作为系统工作流程来铺设舞台。

第七章,线性模型,展示了如何使用线性和逻辑回归进行推断和预测,以及如何使用正则化来管理过拟合的风险。它介绍了 Quantopian 交易平台,并演示了如何构建因子模型和预测资产价格。

第八章,时间序列模型,涵盖了单变量和多变量时间序列,包括向量自回归模型和共整合测试,以及它们如何应用于成对交易策略。第九章,贝叶斯机器学习,介绍了如何制定概率模型以及马尔科夫链蒙特卡洛MCMC)采样和变分贝叶斯如何促进近似推理。它还说明了如何使用 PyMC3 进行概率编程,以深入了解参数和模型的不确定性。

第十章,决策树和随机森林,展示了如何构建、训练和调整非线性基于树的模型以进行洞察和预测。它介绍了基于树的集成模型,并说明了随机森林如何使用自举聚合来克服决策树的一些弱点。第十一章,梯度提升机,集成模型,并演示了如何使用库xgboostlightgbmcatboost进行高性能训练和预测,并深入审查了如何调整众多的超参数。

第十二章,无监督学习,介绍了如何使用降维和聚类进行算法交易。它使用主成分和独立成分分析来提取数据驱动的风险因子。它介绍了几种聚类技术,并演示了如何使用分层聚类进行资产配置。

第三部分 – 自然语言处理

第三部分专注于文本数据,并介绍了最先进的无监督学习技术,以从这一关键的替代数据源中提取高质量的信号。

第十三章,处理文本数据,演示了如何将文本数据转换为数值格式,并应用了第二部分中的分类算法进行大规模数据集的情感分析。第十四章,主题建模,应用了贝叶斯无监督学习来提取能够总结大量文档的潜在主题,并提供了更有效地探索文本数据或将主题用作分类模型特征的方法。它演示了如何将这一技术应用到 第三章,金融替代数据,中获取的收益电话文本和提交给证券交易委员会 (SEC) 的年度报告。

第十五章,词嵌入,使用神经网络学习最先进的语言特征,以单词向量的形式捕获语义上下文,比传统的文本特征更好地表示语义内容,是从文本数据中提取交易信号的一个非常有前景的途径。

第四部分 – 深度学习和强化学习

第四部分介绍了深度学习和强化学习。

  • 第十七章深度学习,介绍了 Keras、TensorFlow 和 PyTorch,这三个最流行的深度学习框架,并说明了如何训练和调整各种架构。

  • 第十八章循环神经网络,为时间序列数据提供了 RNNs。

  • 第十九章卷积神经网络,说明了如何使用 CNN 处理图像和文本数据

  • 第二十章自编码器和生成对抗网络,展示了如何使用深度神经网络进行无监督学习,使用自编码器生成合成数据

  • 第二十一章强化学习,演示了使用强化学习构建动态代理程序,根据奖励使用 OpenAI gym 平台学习策略函数

您成功所需的条件

该书内容围绕着将机器学习算法应用于不同数据集。书中还在 GitHub 上提供了大量额外内容,以便于审阅并通过书中讨论的示例进行实验。它包含了额外的细节和说明,以及众多参考资料。

数据来源

我们将使用来自市场、基本和替代来源的免费历史数据。 第二章,市场和基本数据 和 第三章,金融替代数据 讲述了这些数据来源的特点和获取方式,并介绍了我们在整本书中将使用的关键提供者。 伴随的 GitHub 仓库(见下文)包含了如何获取或创建我们在整个书中将使用的一些数据集的说明,并包括一些较小的数据集。

我们将要获取和处理的一些示例数据源包括但不限于:

  • 纳斯达克 ITCH 订单簿数据

  • 电子数据收集、分析和检索(EDGAR)美国证券交易委员会申报

  • 来自 Seeking Alpha 的盈利电话转录

  • Quandl 每日价格以及超过 3,000 只美国股票的其他数据点

  • 来自美联储等机构的各种宏观基本数据

  • 大量 Yelp 商业评论和 Twitter 数据集

  • 油轮图像数据

一些数据非常庞大(例如纳斯达克和 SEC 申报)。 笔记本会指示是否是这种情况。

GitHub 仓库

GitHub 仓库包含了更多细节中涉及的许多概念和模型的 Jupyter Notebooks。 在书中使用时会引用这些笔记本。 每个章节都有自己的目录,需要时有单独的说明,并且包括与章节内容相关的参考资料。

Jupyter Notebooks 是创建可重现计算叙事的绝佳工具,它使用户能够创建和共享结合了实时代码、叙述文本、数学方程、可视化、交互控件和其他丰富输出的文档。 它还提供了用于与数据进行交互计算的构建模块,例如文件浏览器、终端和文本编辑器。

您可以在以下位置找到代码文件:

github.com/PacktPublishing/Hands-On-Machine-Learning-for-Algorithmic-Trading

Python 库

该书采用 Python 3.7,并推荐使用 miniconda 安装 conda 包管理器,并创建一个 conda 环境来安装必要的库。为此,GitHub 仓库包含一个 environment.yml 文件。请参考 GitHub 仓库的 README 文件中引用的安装说明。

投资行业中机器学习的崛起

在过去几十年中,投资行业已经发生了巨大变化,并在竞争加剧、技术进步和复杂的经济环境中继续发展。本节将回顾塑造投资环境总体和算法交易特别情境的几个关键趋势,以及将在本书中反复出现的相关主题。

推动算法交易和机器学习成为当前主流的趋势包括:

  • 市场微观结构的变化,如电子交易的普及和跨资产类别和地理区域的市场整合

  • 以风险因子暴露为框架的投资策略的发展,而不是资产类别

  • 计算能力、数据生成和管理以及分析方法的革命

  • 算法交易先驱相对于人类、离散投资者的表现优异

此外,2001 年和 2008 年的金融危机影响了投资者对分散化和风险管理的看法,并催生了以 交易所交易基金 (ETF) 形式的低成本被动投资工具。在 2008 年危机后的低收益和低波动性中,成本意识强的投资者将 2 万亿美元从主动管理型共同基金转移到被动管理的 ETF。竞争压力还体现在对冲基金费用的降低,从传统的 2% 年度管理费和 20% 利润份额降至 2017 年的平均 1.48% 和 17.4%。

从电子交易到高频交易

电子交易在能力、交易量、资产类别覆盖范围和地域覆盖范围方面取得了长足的进步,自 20 世纪 60 年代网络开始将价格路由到计算机终端以来。

股票市场已经在全球范围内引领了这一趋势。美国证券交易委员会(SEC)于 1997 年颁布的订单处理规则通过电子通讯网络ECN)向交易所引入了竞争。ECN 是自动化的替代交易系统ATS),它们以指定价格匹配买卖订单,主要用于股票和货币,并注册为经纪人。它允许不同地理位置的大型经纪公司和个人交易者在交易所和交易时间之后直接进行交易,无需中间商。暗池是另一种类型的 ATS,它允许投资者下订单和交易,而无需公开透露其信息,就像交易所维护的订单簿中一样。自 2007 年 SEC 裁定以来,暗池已经增长,通常设在大型银行内,并受到 SEC 监管。

随着电子交易的兴起,成本效益执行的算法迅速发展,并且采用情况从卖方迅速扩展到买方和各种资产类别。自 2000 年左右,自动交易就作为卖方工具出现,旨在实现成本效益的交易执行,通过将订单分散到时间上来限制市场影响。这些工具扩展到买方,并且通过考虑交易成本和流动性以及短期价格和成交量预测等因素变得越来越复杂。

直接市场访问DMA)通过允许交易者直接将订单发送到交易所,使用经纪人的基础设施和市场参与者标识,为交易者提供了更大的执行控制权。赞助访问通过经纪人去除了预先交易风险控制,并形成了高频交易HFT)的基础。

HFT 指的是金融工具的自动化交易,以微秒为单位的极低延迟执行,并且参与者持有很短时间的头寸。其目标是发现和利用市场微观结构的不有效率,即交易场所的机构基础设施。在过去的十年中,HFT 已经大幅增长,据估计,在美国股票市场中占据大约 55%的交易量,在欧洲股票市场中约占 40%。HFT 也在期货市场上增长到大约 80%的外汇期货交易量,以及两三分之二的利率和国债 10 年期期货交易量(FAS 2016)。

高频交易策略旨在利用被动或激进策略每笔交易获得小额利润。被动策略包括套利交易,以从不同交易场所交易的同一资产或其衍生品的微小价格差异中获利。激进策略包括订单预期或动量引爆。订单预期,也称为流动性检测,涉及提交小型探索性订单以侦测大型机构投资者的隐藏流动性,并在大订单之前交易,以从随后的价格波动中获益。动量引爆意味着一个算法执行并取消一系列订单,以欺骗其他高频交易算法更积极地买入(或卖出),并从由此产生的价格变化中获益。

监管机构对某些激进的高频交易策略与市场脆弱性和波动性增加之间的潜在联系表示担忧,比如 2010 年 5 月的闪电崩盘、2014 年 10 月的国债市场波动以及 2015 年 8 月 24 日道琼斯工业平均指数暴跌超过 1000 点。与此同时,由于高频交易的存在,市场流动性随着交易量的增加而增加,从而降低了整体交易成本。

交易量减少、波动性降低以及技术成本和获取数据和交易场所的准入成本上升的组合导致了财务压力。美国股票的高频交易收入据估计首次下降到 2008 年以来的 10 亿美元以下,从 2009 年的 79 亿美元下降。

这一趋势导致了行业的整合,例如,最大的上市专有交易公司 Virtu Financial 进行了各种收购,并共享基础设施投资,例如芝加哥和东京之间的新 Go West 超低延迟路线。同时,创业公司如 Alpha Trading Lab 提供高频交易基础设施和数据,以通过众包算法来使高频交易民主化,并获得利润的一部分。

因子投资和智能贝塔基金

资产提供的回报是与金融投资相关的不确定性或风险的函数。例如,股权投资意味着承担公司的经营风险,而债券投资意味着承担违约风险。

在特定风险特征预测回报的程度上,识别和预测这些风险因素的行为成为设计投资策略时的主要关注点。这产生了有价值的交易信号,并是优秀主动管理结果的关键。随着时间的推移,该行业对风险因素的理解发生了很大变化,并影响了机器学习在算法交易中的应用。

现代投资组合理论MPT)引入了对于给定资产的特异风险和系统性风险的区分。特异风险可以通过多元化消除,但系统性风险则不能。在 1960 年代初,资本资产定价模型CAPM)确定了驱动所有资产回报的单一因素:市场投资组合相对于国库券的回报。市场投资组合由所有可交易证券组成,按其市值加权。资产对市场的系统性暴露由贝塔(beta)来衡量,即资产回报与市场投资组合之间的相关性。

认识到资产风险不取决于孤立的资产,而是取决于其相对于其他资产和整个市场的运动方式,是一项重大的概念突破。换句话说,资产并不因其特定的特异特征而赚取风险溢价,而是因其暴露于基础因素风险而获利。

然而,大量的学术文献和长期的投资经验已经证明,CAPM 的预测是错误的,即资产风险溢价仅取决于其暴露于由资产贝塔测量的单一因素。相反,后来发现了许多额外的风险因素。因子是一种可量化的信号、属性或任何与未来股票回报历史上相关的变量,并且预计未来仍将相关。

这些风险因素被标记为异常,因为它们与有效市场假说EMH)相矛盾,后者认为市场均衡将始终根据 CAPM 定价证券,因此其他因素不应具有预测能力。因素背后的经济理论可以是理性的,即因素风险溢价弥补了不良时期低回报的情况,也可以是行为的,即代理人未能套利超出的回报。

知名的异常包括价值、规模和动量效应,这些效应有助于预测回报,同时控制 CAPM 市场因素。规模效应基于小型公司系统性地胜过大型公司,由 Banz(1981)和 Reinganum(1981)发现。价值效应(Basu 1982)指出,具有低估价指标的公司表现更好。它暗示着具有低价格倍数的公司,如市盈率或市净率低的公司,表现比它们更昂贵的同行更好(正如价值投资的创始人本杰明·格雷厄姆和大卫·多德所建议,并被沃伦·巴菲特所普及)。

动量效应是由克利福德·阿斯尼斯等人于 1980 年代末发现的,阿斯尼斯是 AQR 的创始合伙人之一。该效应表明,具有良好动量的股票,即近 6-12 个月的回报较高的股票,其未来的回报高于市场风险相似的劣质动量股票。研究人员还发现,价值和动量因子解释了美国以外股票的回报,以及其他资产类别,如债券、货币和大宗商品,以及额外的风险因素。

在固定收益领域,价值策略被称为乘坐收益曲线,是一种利率曲线溢价形式。在大宗商品中,它被称为卷曲回报,对于上升期货曲线,它有正回报,否则则为负回报。在外汇市场中,价值策略被称为利差

还有一项流动性溢价。更不流动的证券以低价格交易,并且相对于更流动的对应品具有较高的平均超额回报。具有较高违约风险的债券平均回报较高,反映了信用风险溢价。由于当回报往往下跌时投资者愿意为高波动性购买保险,因此期权市场中的波动性保护卖方往往会获得较高的回报。

多因子模型将风险定义为比市场投资组合更广泛和多样化的内容。1976 年,斯蒂芬·罗斯提出了套利定价理论,该理论主张投资者获得了多个系统性风险来源的补偿,这些风险是不可分散的。最重要的三个宏观因素是增长、通胀和波动性,除了生产力、人口结构和政治风险。1992 年,尤金·法玛和肯尼斯·弗伦奇将股权风险因素的大小和价值与市场因素结合到一个单一模型中,更好地解释了横截面股票回报。后来,他们添加了一个同时解释两种资产类别回报的模型,该模型还包括债券风险因素。

风险因素特别吸引人的一个方面是它们之间的低或负相关性。例如,价值和动量风险因素呈负相关,降低了风险,并且使风险调整后的回报超出了风险因素所隐含的收益。此外,利用杠杆和多空策略,因子策略可以组合成市场中性方法。在将暴露于正风险的证券的多头头寸与暴露于负风险的证券的低配头寸或空头头寸相结合后,可以收集到动态风险溢价。

因此,解释超越 CAPM 的回报的因素被纳入了倾向于支持一个或多个因素的投资风格中,资产开始流入基于因子的投资组合。 2008 年的金融危机强调了当投资者不看基础因子风险时,资产类别标签可能会极具误导性并导致虚假的分散化感,因为资产类别同时崩盘。

在过去几十年中,量化因子投资已经从基于两三种风格的简单方法发展为多因子智能或异类β产品。 智能β基金在 2017 年突破了 1 万亿美元资产管理规模,证明了这种混合投资策略的受欢迎程度,该策略结合了主动和被动管理。 智能β基金采取被动策略,但根据一个或多个因素进行修改,例如选择更便宜的股票或根据股息支付进行筛选,以获得更好的回报。 这种增长与对传统主动管理者收取高费用的批评以及对其业绩加剧的审查同时发生。

持续发现和成功预测风险因素,无论是单独还是与其他风险因素结合,都会显著影响未来资产回报,跨资产类别,这是投资行业中机器学习激增的关键推动力,并且将是本书的一个关键主题。

算法先驱在规模上超越了人类

引领算法交易的公司的资产管理规模AUM)的业绩和增长在引起投资者兴趣以及随后行业努力复制其成功方面发挥了关键作用。系统性基金与高频交易(HFT)不同之处在于,交易可以持有更长时间,同时寻求利用套利机会,而不是单纯依靠速度优势。

主要或完全依赖算法决策的系统性策略最著名的是由数学家 James Simons 引入的,他于 1982 年创立了文艺复兴技术公司,并将其打造成为首屈一指的量化公司。 其神秘的 Medallion 基金,对外关闭,自 1982 年以来年化收益估计为 35%。

DE Shaw,Citadel 和 Two Sigma 是最著名的三家使用基于算法的系统性策略的量化对冲基金,在 2017 年首次以总收益为标准进入前 20 名表现者之列,扣除费用,并自成立以来。

DE Shaw 成立于 1988 年,2018 年资产管理规模为 470 亿美元,在榜单上排名第 3。 Citadel 由 Kenneth Griffin 于 1990 年创立,管理着 290 亿美元,排名第 5 位。 Two Sigma 仅于 2001 年由 DE Shaw 的校友 John Overdeck 和 David Siegel 创立,从 2011 年的 80 亿美元资产管理规模增长到 2018 年的 520 亿美元。 Bridgewater 成立于 1975 年,资产管理规模超过 1500 亿美元,由于其也融合了系统性策略的纯阿尔法基金,仍然处于领先地位。

同样,在机构投资者 2017 年对冲基金 100 强榜单上,排名前六位的五家公司在很大程度上或完全依靠计算机和交易算法做出投资决策——所有这些公司都在一个充满挑战的环境中增长了资产。几家定量型公司上升了几个等级,并在某些情况下将其资产增长了两位数的百分比。排名第二的应用量化研究AQR)在 2017 年将其对冲基金资产增长了 48%,达到了 697 亿美元,并且公司总资产管理规模达到了 1876 亿美元。

在过去三年中,根据复合绩效排名,文艺复兴技术运营的基于量化的对冲基金排名分别为 6 和 24,Two Sigma 排名第 11,D.E. Shaw 分别排名 18 和 32,Citadel 排名 30 和 37。除了表现最好的之外,算法策略在过去几年中表现良好。在过去五年中,量化对冲基金的年均收益率约为 5.1%,而同期平均对冲基金的年均涨幅为 4.3%。

由机器学习驱动的基金吸引了 1 万亿美元的资产管理规模。

计算能力、数据和机器学习方法的三次革命使得系统化、数据驱动的策略的采用不仅更具吸引力和成本效益,而且是竞争优势的关键来源。

因此,算法方法不仅在开创这些策略的对冲基金行业中找到了更广泛的应用,而且在更广泛的资产管理公司以及 ETF 等被动管理的车辆中也找到了应用。特别是,利用机器学习和算法自动化的预测分析在投资过程的所有步骤中发挥着越来越重要的作用,从构思和研究到策略制定和投资组合构建,再到交易执行和风险管理,跨资产类别都是如此。

由于没有量化或算法基金的客观定义,行业规模的估计各不相同,许多传统对冲基金甚至包括共同基金和交易所交易基金(ETF)正在引入计算机驱动的策略或将其整合到人加机的自主环境中。

摩根士丹利在 2017 年估计,过去六年中,算法策略以每年 15%的速度增长,并在对冲基金、共同基金和智能贝塔 ETF 之间控制着约 1.5 万亿美元。其他报告表明,量化对冲基金行业即将超过 1 万亿美元的资产管理规模,在传统对冲基金出现资金流出的背景下,其规模自 2010 年以来几乎翻了一番。相比之下,根据最新的全球对冲基金研究报告,对冲基金行业的总资本达到了 3.21 万亿美元。

市场研究公司 Preqin 估计,几乎有 1,500 家对冲基金中的大多数基金都借助于计算机模型进行交易。量化对冲基金现在负责美国所有股票交易的 27%,而在 2013 年仅占 14%。但许多基金使用数据科学家或量化分析师,他们反过来又使用机器来构建大型统计模型(WSJ)。

然而,近年来,基金已经转向了真正的机器学习,人工智能系统可以以高速分析大量数据,并通过这些分析改进自身。最近的例子包括 Rebellion Research、Sentient 和 Aidyia,它们依赖于进化算法和深度学习来设计完全自动化的人工智能AI)驱动的投资平台。

从核心对冲基金行业开始,算法策略的采用已经扩展到了共同基金,甚至是以智能贝塔基金的形式管理的被动交易基金,以及以量化方法的形式管理的自由基金。

量化基金的出现

主动投资管理出现了两种不同的方法:系统化(或量化)和自由裁量投资。系统化方法依赖于算法,以重复可行且数据驱动的方式来识别跨多个证券的投资机会;相比之下,自由裁量方法涉及对少量证券进行深入分析。随着基本面经理采用更多的数据科学驱动方法,这两种方法变得越来越相似。

根据巴克莱的数据,现在连基本面交易员也使用量化技术,占系统资产的 550 亿美元。量化基金对特定公司不持偏见,而是交易各种证券的模式和动态。巴克莱编制的数据显示,量化交易员现在占总对冲基金资产的约 17%。

资产规模达 120 亿美元的 Point72 资产管理公司已经将大约一半的投资组合经理转向了人加机器的方法。Point72 还向一个分析大量替代数据并将结果传递给交易员的团队投资了数千万美元。

投资战略能力

对相关能力的投资——技术、数据,以及最重要的是熟练的人才——凸显了使用机器学习进行算法交易对于竞争优势有多么重要,尤其是考虑到自 2008 年金融危机以来,被动指数投资工具(如 ETF)的日益普及。

摩根士丹利指出,其量化客户中仅有 23% 的客户表示不考虑使用或尚未使用机器学习,而在 2016 年这一比例为 44%。

Guggenheim Partners LLC 在加利福尼亚州劳伦斯·伯克利国家实验室建造了一个价值 100 万美元的超级计算集群,用于帮助 Guggenheim 的量化投资基金进行数据分析。计算机的电费每年还要再花费 100 万美元。

AQR 是一个量化投资集团,依靠学术研究来识别并系统地交易那些长期以来证明能够击败整个市场的因素。该公司过去不喜欢完全依靠计算机策略的量化同行,如文艺复兴技术或 DE Shaw。然而,最近,AQR 开始利用机器学习在市场中寻找利润模式,以解析新型数据集,例如油井和油轮所投下的阴影的卫星图片。

领先的公司黑石集团,拥有超过 5 万亿美元的资产管理规模,也通过大量投资于 SAE,一个在金融危机期间收购的系统性交易公司,来打败裁量基金经理的算法。富兰克林·坦普尔顿公司以未披露的金额收购了 Random Forest Capital,这是一家以债务为重点、以数据为导向的投资公司,希望其技术能够支持更广泛的资产管理者。

机器学习和另类数据

对冲基金长期以来一直通过信息优势和发现新的不相关信号来寻找 Alpha。在历史上,这包括专有调查购物者、选民以及选举或公投前的情况。偶尔,利用公司内部人员、医生和专家网络来扩展对行业趋势或公司的了解越过了法律界限:2010 年后,一系列交易员、投资组合经理和分析师因使用内幕信息而受到起诉,动摇了这一行业。

相比之下,利用机器学习开发传统和另类数据源的信息优势与专家和行业网络或接触公司管理层无关,而是与收集大量数据并实时分析它们的能力相关。

三个趋势彻底改变了算法交易策略中数据的使用方式,并可能进一步将投资行业从裁量性转变为量化风格:

  • 数字数据量的指数增长

  • 计算能力和数据存储容量的成本下降带来了增长。

  • 分析复杂数据集的机器学习方法的进步

传统数据包括经济统计数据、交易数据或企业报告。而另类数据则更为广泛,包括卫星图像、信用卡销售、情绪分析、移动地理位置数据和网站抓取等来源,以及将业务中生成的数据转化为有价值的情报。原则上,它包括任何包含可使用机器学习提取交易信号的数据源。

例如,来自保险公司的数据,关于新汽车保险政策的销售,不仅代表了新汽车销售的量,还可以细分为品牌或地理区域。许多供应商从网站上抓取有价值的数据,范围从应用下载和用户评论到航空公司和酒店预订。社交媒体网站也可以被抓取,以获取有关消费者观点和趋势的线索。

通常,数据集很大,需要使用可扩展的数据解决方案进行存储、访问和分析,以进行并行处理,例如 Hadoop 和 Spark;根据德意志银行的数据,目前全球有超过 10 万亿个网页的 10 亿多个网站,数据量达到 500 涵字节(或 5000 亿吉字节)。每年新增的网站超过 1 亿个。

在公司公布业绩之前,可以通过其网站上职位列表的减少、员工在招聘网站 Glassdoor 上对其首席执行官的内部评价,或者其网站上服装平均价格的下降来获取对公司前景的实时见解。这可以与汽车停车场的卫星图像和手机定位数据结合使用,这些数据显示有多少人正在访问商店。另一方面,对特定功能区域或特定地理区域的职位发布量增加可以得出战略性动向。

最有价值的数据之一是直接揭示消费支出的数据,信用卡信息是其主要来源。这些数据仅提供了销售趋势的部分视图,但与其他数据结合使用时可以提供关键见解。例如,Point72 每天分析 8 千万笔信用卡交易。我们将在《第三章》详细探讨各种数据来源、使用案例以及如何评估它们,金融的替代数据

在过去两年中,投资集团在替代数据集和数据科学家方面的支出已经翻了一番,因为资产管理行业试图重振其日渐衰落的运势。2018 年 12 月,alternativedata.org 上列出了 375 家替代数据提供商(由提供商 Yipit 赞助)。

资产管理者去年在数据集和招聘新员工进行解析方面的总支出为 3.73 亿美元,比 2016 年增长了 60%,今年预计总支出将达到 6.16 亿美元,根据 alternativedata.org 对投资者的调查。该机构预测,到 2020 年,整体支出将上升到超过 10 亿美元。一些估计甚至更高:Optimus,一家咨询公司,估计投资者每年在替代数据上的支出约为 50 亿美元,并预计未来几年行业将以 30% 的速度增长。

随着对有价值数据源的竞争日益激烈,独家安排成为数据源合同的一个关键特征,以保持信息优势。同时,隐私问题日益严重,监管机构已经开始关注目前主要没有受到监管的数据提供者行业。

交易算法的众包

最近,几家算法交易公司开始提供投资平台,提供数据访问和编程环境,以众包方式获取成为投资策略或整个交易算法的风险因素。关键示例包括 WorldQuant、Quantopian 和 2018 年推出的 Alpha Trading Labs。

WorldQuant 自 2007 年以来为 Millennium Management 管理了超过 50 亿美元,资产管理规模为 346 亿美元,并于 2018 年宣布将推出其首个公共基金。它在全球的α工厂中雇佣了数百名科学家和更多的兼职工人,这个工厂将投资过程组织成量化的生产线。该工厂声称已经生产了 400 万个成功测试的α因子,用于更复杂的交易策略,并且目标是 1 亿个。每个α因子都是一个算法,旨在预测未来资产价格的变化。其他团队将α因子组合成策略,将策略组合成投资组合,在投资组合之间分配资金,并在避免相互 cannibalize 的策略的同时管理风险。

设计和执行交易策略

机器学习可以在交易策略的生命周期的多个步骤中增加价值,并依赖于关键的基础设施和数据资源。因此,这本书旨在探讨机器学习技术如何融入更广泛的设计、执行和评估策略的过程中。

一个算法交易策略由一系列α因子驱动,这些因子将一个或多个数据源转化为信号,进而预测未来的资产回报并触发买入或卖出订单。第二章,市场和基本数据和第三章,金融替代数据涵盖了数据的采集和管理,这是成功交易策略的最重要的驱动因素。

第四章,α因子研究概述了一个方法论上合理的过程,用于管理随着数据量增加而增加的虚假发现的风险。第五章,策略评估为交易策略的执行和绩效评估提供了背景:

让我们简要地了解一下这些步骤,我们将在接下来的章节中深入讨论。

数据采集和管理

数据在体积、多样性和速度方面的戏剧性演变既是将机器学习应用于算法交易的必要条件,也是驱动力。数据的不断增加需要积极管理,以发掘潜在价值,包括以下步骤:

  1. 识别和评估包含不会太快衰减的 Alpha 信号的市场、基本和替代数据来源。

  2. 部署或访问基于云的可扩展数据基础设施和分析工具,如 Hadoop 或 Spark,以促进快速、灵活的数据访问

  3. 谨慎管理和策划数据,避免通过点时(PIT)基础进行向前看偏差调整它。这意味着数据可能只反映给定时间可用和已知的信息。在扭曲的历史数据上训练的 ML 算法几乎肯定会在实时交易中失败。

Alpha 因子研究和评估

Alpha 因子的设计旨在从数据中提取信号,以预测给定投资宇宙在交易周期内的资产回报。当评估时,一个因子对每个资产都有一个值,但可能结合一个或多个输入变量。该过程涉及下图中概述的步骤:

交易策略工作流的研究阶段包括 Alpha 因子的设计、评估和组合。机器学习在这个过程中起着重要作用,因为投资者对较简单因子的信号衰减以及当今可用的数据更加丰富而作出反应,因素的复杂性增加了。

预测性 Alpha 因子的开发需要探索输入数据与目标回报之间的关系,创造性的特征工程,并测试和微调数据转换以优化输入的预测能力。

数据转换范围从简单的非参数排名到复杂的集成模型或深度神经网络,取决于输入中的信号量和输入与目标之间关系的复杂性。许多较简单的因子已经从学术研究中出现,并且在过去几十年中在工业中越来越广泛地使用。

为了最小化由数据挖掘导致的假发现的风险,并且因为金融领域已经经历了几十年的研究,并产生了几个诺贝尔奖,投资者更喜欢依赖与金融市场和投资者行为理论相一致的因子。阐明这些理论不在本书的范围之内,但参考资料将突出显示进一步探讨这种算法交易策略重要框架方面的途径。

要验证 alpha 因子候选的信号内容,就需要在代表市场体制的环境中获得其预测能力的稳健估计。可靠的估计需要避免许多方法论和实践上的陷阱,包括使用导致存活或前瞻性偏差的数据,因为它们不反映真实的 PIT 信息,或者由于对同一数据进行多次测试而未能纠正偏差。

从 alpha 因子中得出的信号通常单独很弱,但与其他因子或数据源结合起来,例如,根据市场或经济环境的情况调节信号,就足够强大了。

投资组合优化与风险管理

Alpha 因子发出入场和退出信号,导致买入或卖出订单,订单执行结果导致投资组合持仓。各个持仓的风险配置相互作用,形成特定的投资组合风险配置。投资组合管理涉及优化持仓权重,以实现所需的投资组合风险和回报配置,该过程高度动态,可以不断纳入不断演化的市场数据。

在此过程中的交易执行需要平衡交易员的困境:快速执行往往会由于市场影响而增加成本,而慢速执行可能会在实现价格偏离决策时的价格时引起执行差距。风险管理贯穿整个投资组合管理过程,以根据观察到或预测到的市场环境变化来调整持仓或承担对冲,从而影响投资组合风险配置。

策略回测

将投资理念纳入算法策略中需要进行广泛的测试,采用科学方法试图根据其在备用样本市场情景中的表现来拒绝该理念。测试可能涉及使用模拟数据来捕捉被认为可能但在历史数据中未反映出来的情景。

一个策略回测引擎需要逼真地模拟策略的执行,以实现无偏的性能和风险估计。除了数据或统计学使用上的缺陷可能引入的潜在偏差之外,回测引擎还需要准确地表示与市场条件相一致的交易信号评估、订单下达和执行等实践方面的内容。

机器学习和算法交易策略

量化策略已经在三个阶段中演变并变得更加复杂:

  1. 在 1980 年代和 1990 年代,信号通常源自学术研究,并使用从市场和基本数据中衍生出的单一或极少量的输入。这些信号现在在很大程度上被商品化,并且作为 ETF 可得,例如基本的均值回归策略。

  2. 在 2000 年代,基于因子的投资大量涌现。基金使用算法识别暴露于价值或动量等风险因子的资产,以寻求套利机会。金融危机早期的赎回引发了 2007 年 8 月的量化颤抖,这一现象在基于因子的基金行业中蔓延开来。现在,这些策略也以长期智能贝塔基金的形式出现,根据一组给定的风险因子倾斜投资组合。

  3. 第三个时代由对 ML 能力和替代数据的投资驱动,以生成可重复交易策略的利润信号。因子衰减是一个主要挑战:从发现到出版,新异常的超额收益已经显示出下降了四分之一,并且在出版后因竞争和拥挤导致下降超过 50%。

有几种使用算法执行交易规则的交易策略类别:

  • 旨在从小的价格波动中获利的短期交易,例如由于套利。

  • 旨在利用预测其他市场参与者行为的行为策略。

  • 旨在优化交易执行的程序,以及

  • 基于预测定价的大量交易群体。

上述 HFT 基金主要依赖短期持有期从基于买卖盘套利或统计套利的微小价格波动中获利。行为算法通常在较低流动性环境中运作,并旨在预测大型参与者可能会对价格产生重大影响的动向。价格影响的期望基于嗅探算法,这些算法产生有关其他市场参与者策略或市场模式(例如 ETF 强制交易)的见解。

交易执行程序旨在限制交易的市场影响,并从简单的切片交易以匹配时间加权平均价格TWAP)或成交量加权平均价格VWAP)到更复杂的算法。这些算法可以在证券或投资组合级别操作,例如,实施多腿衍生品或跨资产交易。

交易 ML 的用例

ML 从各种市场、基本和替代数据中提取信号,并可以应用于算法交易策略过程的所有步骤。关键应用包括:

  • 数据挖掘以识别模式和提取特征。

  • 通过监督学习生成风险因子或 Alpha,并提出交易思路。

  • 将个体信号聚合成策略。

  • 根据算法学到的风险配置资产。

  • 通过使用合成数据来测试和评估策略。

  • 使用强化学习进行交易策略的交互式自动优化。

我们简要介绍了其中一些应用,并确定我们将在后面的章节中演示它们的使用。

用于特征提取的数据挖掘

对大型复杂数据集的成本效益评估需要大规模检测信号。全书中有几个示例:

  • 信息论是一个有用的工具,用于提取捕捉潜在信号的特征,并可用于 ML 模型。在第四章,Alpha Factor Research中,我们使用互信息来评估单个特征对监督学习算法预测资产收益的潜在价值。

  • 在第十二章,无监督学习中,我们介绍了从高维数据集中创建特征的各种技术。在第十四章,主题建模中,我们将这些技术应用于文本数据。

  • 我们强调了获取有关单个变量预测能力的模型特定方法。我们使用一种称为SHapley Additive exPlanationsSHAP)的新颖博弈论方法来将预测性能归因于具有大量输入变量的复杂梯度增强机器中的单个特征。

监督学习用于α因子的创建和聚合

将 ML 应用于交易的主要理由是获得关于资产基本面、价格走势或市场状况的预测。策略可以利用多个 ML 算法相互构建。下游模型可以通过整合关于个别资产前景、资本市场预期和证券之间相关性的预测,生成组合水平的信号。或者,ML 预测可以像上面概述的量化基本方法那样通知自主交易。ML 预测还可以针对特定风险因素,如价值或波动性,或者实施技术方法,如趋势跟踪或均值回归:

  • 在第三章,金融替代数据中,我们说明了如何处理基本数据以创建 ML 驱动的估值模型的输入

  • 在第十三章,处理文本数据,第十四章,主题建模,和第十五章,词嵌入中,我们使用企业评论的替代数据,该数据可用于为公司的收入制定预测,作为估值练习的输入。

  • 在第八章,时间序列模型中,我们演示了如何预测宏观变量作为市场预期的输入以及如何预测波动性等风险因素

  • 第十八章递归神经网络中,我们介绍了递归神经网络RNNs),它们在非线性时间序列数据上表现出优越的性能。

资产配置

ML 已被用于基于决策树模型进行投资组合配置,该模型计算一种层次化的风险平衡形式。因此,风险特征是由资产价格模式驱动的,而不是由资产类别驱动的,实现了优越的风险收益特征。

在第五章,策略评估和第十二章,无监督学习中,我们说明了层次聚类如何提取数据驱动的风险类别,这些类别比传统资产类别定义更好地反映了相关性模式。

测试交易想法

回测是选择成功的算法交易策略的关键步骤。使用合成数据的交叉验证是生成可靠的样本外结果的关键 ML 技术,当结合适当的方法来纠正多次测试时。金融数据的时间序列性质要求对标准方法进行修改,以避免前瞻性偏差或以其他方式污染用于训练、验证和测试的数据。此外,历史数据的有限可用性已经促使出现了使用合成数据的替代方法:

  • 我们将演示使用市场、基本和替代数据来测试 ML 模型的各种方法,以获得对样本外误差的合理估计。

  • 第二十章自动编码器和生成对抗网络中,我们介绍了能够生成高质量合成数据的 GAN。

强化学习

交易发生在竞争激烈、互动式市场中。强化学习旨在训练代理以学习基于奖励的策略函数。

  • 第二十一章强化学习中,我们介绍了关键的强化学习算法,如 Q-Learning 和 Dyna 架构,并演示了使用 OpenAI 的 gym 环境对交易进行强化学习算法的训练。

概要

在本章中,我们介绍了算法交易策略以及 ML 如何成为设计和组合 alpha 因子的关键因素,这些因素又是投资组合表现的关键驱动因素。我们涵盖了围绕算法交易策略的各种行业趋势,替代数据的出现以及利用 ML 来利用这些新信息优势来源的情况。

此外,我们介绍了算法交易策略设计过程,重要的 alpha 因子类型,以及我们将如何使用 ML 来设计和执行我们的策略。在接下来的两章中,我们将更仔细地研究任何算法交易策略的推动力—市场、基本和替代数据来源—使用 ML。

第二章:市场和基本数据

数据一直是交易的重要驱动因素,交易员长期以来一直在努力通过获取优越信息来获取优势。这些努力至少可以追溯到有关罗斯柴尔德家族利用鸽子跨越海峡携带的关于滑铁卢战役英军胜利的提前消息进行债券购买的传闻。

如今,对更快数据访问的投资采取了由领先的高频交易HFT)公司组成的 Go West 联盟的形式,该联盟将芝加哥商品交易所CME)与东京连接起来。在 CME 和纽约 BATS 交易所之间的往返延迟已降至接近理论极限的 8 毫秒,因为交易员竞相利用套利机会。

传统上,投资策略主要依赖于公开可用的数据,有限地努力创建或获取私有数据集。在股票的情况下,基本策略使用基于报告的财务数据构建的金融模型,可能结合行业或宏观数据。由技术分析驱动的策略从市场数据中提取信号,例如价格和成交量。

机器学习ML)算法可以更有效地利用市场和基本数据,特别是当与替代数据结合使用时,这是下一章的主题。我们将在后续章节中介绍几种侧重于市场和基本数据的技术,例如经典和现代时间序列技术,包括循环神经网络RNNs)。

本章介绍市场和基本数据源以及它们被创建的环境。熟悉各种订单类型和交易基础设施很重要,因为它们影响交易策略的回测模拟。我们还说明了如何使用 Python 访问和处理交易和财务报表数据。

特别是,本章将涵盖以下主题:

  • 市场微观结构如何塑造市场数据

  • 如何利用 Nasdaq ITCH 从 tick 数据重建订单簿

  • 如何使用各种类型的条形图总结 tick 数据

  • 如何处理可扩展业务报告语言XBRL)编码的电子申报

  • 如何解析和结合市场和基本数据以创建 P/E 序列

  • 如何使用 Python 访问各种市场和基本数据源

如何处理市场数据

市场数据是在众多市场交易中买卖订单的提交和处理过程中产生的。这些数据反映了交易场所的机构环境,包括管理订单、交易执行和价格形成的规则和法规。

算法交易员使用机器学习算法分析买卖订单的流动以及由此产生的成交量和价格统计数据,提取能捕捉到例如需求-供给动态或某些市场参与者行为的交易信号或特征。

我们将首先审查影响在回测期间模拟交易策略的机构特征。然后,我们将介绍如何从订单簿来源重建 Tick 数据。接下来,我们将重点介绍几种规范化 Tick 数据的方法,并旨在最大化信息内容。最后,我们将说明如何访问各种市场数据提供商接口,并突出几个提供商。

市场微观结构

市场微观结构是金融经济学的一个分支,它研究交易过程以及相关市场的组织。机构细节在不同资产类别及其衍生品、交易场所和地理位置上相当复杂和多样化。在我们深入研究交易产生的数据之前,我们将对关键概念进行简要概述。GitHub 上的参考链接指向了几个对这一主题进行了详细处理的来源。

市场场所

金融工具的交易发生在有组织的、主要是电子交易所和场外市场上。交易所是买方和卖方相遇的中央市场,在这里买方竞相出价,而卖方竞相报出最低的报价。

在美国和国外有许多交易所和替代交易场所。以下表格列出了一些较大的全球交易所以及截至 2018 年 03 月各种资产类别(包括衍生品)的 12 个月交易量。通常,少数金融工具占据了大部分交易量:

股票
交易所 市值(美元百万)
纽约证券交易所 23,138,626
纳斯达克-美国 10,375,718
日本交易所集团 6,287,739
上海证券交易所 5,022,691
欧洲交易所 4,649,073
香港交易及结算所 4,443,082
伦敦证券交易所集团 3,986,413
深圳证券交易所 3,547,312
德国交易所 2,339,092
印度孟买证券交易所有限公司 2,298,179
印度国家证券交易所有限公司 2,273,286
BATS 全球市场 - 美国
芝加哥期权交易所
国际证券交易所

交易所可能依赖双边交易或以订单为驱动的系统,根据某些规则匹配买入和卖出订单。价格形成可能通过拍卖进行,例如在纽约证券交易所(NYSE)中,最高出价和最低报价被匹配,或通过买家从卖家购买然后卖给买家的交易商进行。

许多交易所使用提供流动性的中间人,即通过在某些证券中做市来进行交易的能力。例如,纽约证券交易所通常有一个指定的市场主持人,他确保每个证券的交易有序进行,而全国证券交易商自动报价系统Nasdaq)有几个。中间人可以充当作为自己代理人交易的交易商,也可以充当作为他人代理人交易的经纪人。

交易所过去是成员所有的,但随着市场改革的加剧,它们经常转为公司所有。纽约证券交易所可以追溯到 1792 年,而纳斯达克于 1971 年开始,是世界上第一个电子股票交易市场,并接管了大多数曾在场外交易的股票交易。仅在美国股票市场,交易分散在 13 个交易所和 40 多个另类交易场所,每个都向综合带报告交易,但延迟不同。

订单类型

交易员可以提交各种类型的买入或卖出订单。一些订单保证立即执行,而另一些可能规定价格阈值或其他触发执行的条件。订单通常仅在同一交易日有效,除非另有规定。

市场订单保证在到达交易场所时立即执行订单,以那一刻的价格为准。相比之下,限价订单仅在市场价格高于(低于)卖出(买入)限价订单的限价时执行。转而停止订单仅在市场价格上升到(下跌到)买入(卖出)停止订单的指定价格时才变为活动状态。购买停止订单可用于限制空头交易的损失。停止订单也可能有限制。

订单可能附加有许多其他条件——全或无订单防止部分执行,仅在指定数量的股票可用时才会被填充,并且可以在当天或更长时间内有效。它们需要特殊处理,对市场参与者不可见。立即成交或取消订单也防止部分执行,但如果未立即执行则取消。立即取消订单立即买入或卖出可用的股票数量并取消剩余部分。不持有订单允许经纪人决定执行的时间和价格。最后,市场开盘/收盘订单在市场开盘或收盘时或附近执行。允许部分执行。

处理订单簿数据

市场数据的主要来源是订单簿,它在整天实时不断更新以反映所有交易活动。 交易所通常将此数据作为实时服务提供,并可能免费提供一些历史数据。

交易活动体现在市场参与者发送的大量有关交易订单的消息中。 这些消息通常符合用于实时交换证券交易和市场数据的电子金融信息交换FIX)通信协议或本地交换协议的语法。

FIX 协议

就像 SWIFT 是后台(例如,用于交易结算)消息传递的消息协议一样,FIX 协议是交易执行之前和之间的默认消息标准,在交易所、银行、经纪商、结算公司和其他市场参与者之间的通信中。 富达投资和所罗门兄弟公司于 1992 年引入了 FIX,以促进经纪商和机构客户之间的电子通信,当时他们通过电话交换信息。

它在全球股票市场上变得流行,然后扩展到外汇、固定收益和衍生品市场,进一步扩展到后交易以支持直通处理。 交易所提供对 FIX 消息的访问作为实时数据提要,由算法交易员解析以跟踪市场活动,并且例如识别市场参与者的踪迹并预测其下一步行动。

消息序列允许重建订单簿。 跨多个交易所的交易规模产生了大量(~10 TB)难以处理的非结构化数据,因此可以成为竞争优势的来源。

FIX 协议目前版本为 5.0,是一个具有庞大相关行业专业人士社区的免费开放标准。 它像更近期的 XML 一样是自描述的,FIX 会话由底层的传输控制协议TCP)层支持。 社区不断添加新功能。

该协议支持管道分隔的键值对,以及基于标签的 FIXML 语法。 请求服务器登录的示例消息如下所示:

8=FIX.5.0|9=127|35=A|59=theBroker.123456|56=CSERVER|34=1|32=20180117- 08:03:04|57=TRADE|50=any_string|98=2|108=34|141=Y|553=12345|554=passw0rd!|10=131|

有一些用于制定和解析 FIX 消息的 Python 开源 FIX 实现可以使用。 交互经纪商提供了用于自动交易的基于 FIX 的计算机对计算机接口CTCI)(请参见 GitHub 存储库中本章的资源部分)。

纳斯达克 TotalView-ITCH 订单簿数据

虽然 FIX 具有主导的大市场份额,但交易所也提供本地协议。 纳斯达克提供了一种 TotalView ITCH 直接数据提要协议,允许订阅者跟踪股票工具的单个订单,从下单到执行或取消。

因此,它允许重建订单簿,跟踪特定证券或金融工具的活跃限价买入和卖出订单列表。订单簿通过列出每个价格点上投标或报价的股份数量来显示全天市场深度。它还可能识别特定买入和卖出订单的市场参与者,除非它是匿名下达的。市场深度是流动性的关键指标,以及大额市场订单的潜在价格影响。

除了匹配市价和限价订单,纳斯达克还进行市场开盘和闭市时执行大量交易的拍卖或交叉交易。随着被动投资继续增长,交叉交易变得越来越重要,交易员寻找执行更大块的股票的机会。TotalView 还发布了纳斯达克开市和闭市以及纳斯达克 IPO/停牌交叉交易的净订单失衡指示器(NOII)。

解析二进制 ITCH 消息

ITCH v5.0 规范声明了与系统事件、股票特征、限价订单的下达和修改以及交易执行相关的 20 多种消息类型。它还包含有关开市和闭市前净订单失衡的信息。

纳斯达克提供了数月的每日二进制文件样本。本章的 GitHub 存储库包含一个名为 build_order_book.ipynb 的笔记本,演示了如何解析 ITCH 消息的示例文件,并重建任何给定 tick 的执行交易和订单簿。

以下表格显示了样本文件日期为 2018 年 3 月 29 日时最常见消息类型的频率:

消息类型 订单簿影响 消息数量
A 新的未归属限价订单 136,522,761
D 订单取消 133,811,007
U 订单取消并替换 21,941,015
E 完全或部分执行;可能有多条针对同一原始订单的消息 6,687,379
X 部分取消后修改 5,088,959
F 添加归属订单 2,718,602
P 交易消息(非交叉) 1,120,861
C 在不同于初始显示价格的价格上全部或部分执行 157,442
Q 交叉交易消息 17,233

对于每条消息,规范列出了组件及其相应的长度和数据类型:

名称 偏移量 长度 注释
消息类型 0 1 F 添加订单 MPID 归属消息
股票定位 1 2 整数 标识证券的定位代码
跟踪编号 3 2 整数 纳斯达克内部跟踪编号
时间戳 5 6 整数 自午夜以来的纳秒数
订单参考编号 11 8 整数 新订单的唯一参考编号
买卖指示符 19 1 字母 订单类型:B = 买入订单,S = 卖出订单
股票 20 4 整数 添加到订单簿的订单股票数量
股票 24 8 Alpha 股票符号,右侧填充空格
价格 32 4 价格(4) 新订单的显示价格
归属 36 4 Alpha 与订单相关联的纳斯达克市场参与者标识符

Python 提供了 struct 模块,通过格式字符串来解析二进制数据,该格式字符串通过指定字节字符串的各个组件的长度和类型来标识消息元素,按照规范中的排列方式。

让我们逐步解析交易消息并重建订单簿的关键步骤:

  1. ITCH 解析器依赖于作为.csv文件提供的消息规范(由create_message_spec.py创建),并根据formats字典组装格式字符串:
formats = {
    ('integer', 2): 'H',  # int of length 2 => format string 'H'
    ('integer', 4): 'I',
    ('integer', 6): '6s', # int of length 6 => parse as string, 
      convert later
    ('integer', 8): 'Q',
    ('alpha', 1)  : 's',
    ('alpha', 2)  : '2s',
    ('alpha', 4)  : '4s',
    ('alpha', 8)  : '8s',
    ('price_4', 4): 'I',
    ('price_8', 8): 'Q',
}
  1. 解析器将消息规范转换为格式字符串和捕获消息内容的namedtuples
# Get ITCH specs and create formatting (type, length) tuples
specs = pd.read_csv('message_types.csv')
specs['formats'] = specs[['value', 'length']].apply(tuple, 
                           axis=1).map(formats)

# Formatting for alpha fields
alpha_fields = specs[specs.value == 'alpha'].set_index('name')
alpha_msgs = alpha_fields.groupby('message_type')
alpha_formats = {k: v.to_dict() for k, v in alpha_msgs.formats}
alpha_length = {k: v.add(5).to_dict() for k, v in alpha_msgs.length}

# Generate message classes as named tuples and format strings
message_fields, fstring = {}, {}
for t, message in specs.groupby('message_type'):
    message_fields[t] = namedtuple(typename=t, field_names=message.name.tolist())
    fstring[t] = '>' + ''.join(message.formats.tolist())
  1. alpha 类型的字段需要按照format_alpha函数中定义的进行后处理:
def format_alpha(mtype, data):
    for col in alpha_formats.get(mtype).keys():
        if mtype != 'R' and col == 'stock': # stock name only in 
                                              summary message 'R'
            data = data.drop(col, axis=1)
            continue
        data.loc[:, col] = data.loc[:, col].str.decode("utf-
                                    8").str.strip()
        if encoding.get(col):
            data.loc[:, col] = data.loc[:, 
                     col].map(encoding.get(col)) # int encoding
    return data
  1. 单日的二进制文件包含超过 3 亿条消息,总计超过 9 GB。该脚本会将解析的结果迭代地附加到以快速的 HDF5 格式存储的文件中,以避免内存限制(有关此格式的更多信息,请参阅本章的最后一节)。以下(简化的)代码处理二进制文件,并生成按消息类型存储的解析订单:
with (data_path / file_name).open('rb') as data:
    while True:
        message_size = int.from_bytes(data.read(2), byteorder='big', 
                       signed=False)
        message_type = data.read(1).decode('ascii')
        message_type_counter.update([message_type])
        record = data.read(message_size - 1)
        message = message_fields[message_type]._make(unpack(fstring[message_type],  
                                   record))
        messages[message_type].append(message)

        # deal with system events like market open/close
        if message_type == 'S':
            timestamp = int.from_bytes(message.timestamp, 
                                       byteorder='big')
            if message.event_code.decode('ascii') == 'C': # close
                store_messages(messages)
                break
  1. 预料之中,这一天交易的 8500 多只股票中的一小部分占了大部分交易:
with pd.HDFStore(hdf_store) as store:
    stocks = store['R'].loc[:, ['stock_locate', 'stock']]
    trades = store['P'].append(store['Q'].rename(columns=
                        {'cross_price': 'price'}).merge(stocks)
trades['value'] = trades.shares.mul(trades.price)
trades['value_share'] = trades.value.div(trades.value.sum())
trade_summary = 
    trades.groupby('stock').value_share.sum().sort_values
                            (ascending=False)
trade_summary.iloc[:50].plot.bar(figsize=(14, 6), color='darkblue', 
                                 title='% of Traded Value')
plt.gca().yaxis.set_major_formatter(FuncFormatter(lambda y, _: 
                                    '{:.0%}'.format(y)))

我们得到了以下图形的绘制:

重建交易和订单簿

解析的消息允许我们重建给定日期的订单流。'R' 消息类型包含在给定日期内交易的所有股票列表,包括有关首次公开募股IPOs)和交易限制的信息。

在一天的交易过程中,会添加新订单,并删除执行和取消的订单。对于引用前一日期放置的订单的消息,进行适当的会计处理需要跟踪多天的订单簿,但我们在此忽略了此方面的内容。

get_messages() 函数说明了如何收集影响交易的单一股票的订单(有关每个消息的详细信息,请参阅 ITCH 规范,略有简化,请参阅笔记本):

def get_messages(date, stock=stock):
    """Collect trading messages for given stock"""
    with pd.HDFStore(itch_store) as store:
        stock_locate = store.select('R', where='stock = 
                                     stock').stock_locate.iloc[0]
        target = 'stock_locate = stock_locate'

        data = {}
        # relevant message types
        messages = ['A', 'F', 'E', 'C', 'X', 'D', 'U', 'P', 'Q']
        for m in messages:
            data[m] = store.select(m,  
              where=target).drop('stock_locate', axis=1).assign(type=m)

    order_cols = ['order_reference_number', 'buy_sell_indicator', 
                  'shares', 'price']
    orders = pd.concat([data['A'], data['F']], sort=False,  
                        ignore_index=True).loc[:, order_cols]

    for m in messages[2: -3]:
        data[m] = data[m].merge(orders, how='left')

    data['U'] = data['U'].merge(orders, how='left',
                                right_on='order_reference_number',
                                left_on='original_order_reference_number',
                                suffixes=['', '_replaced'])

    data['Q'].rename(columns={'cross_price': 'price'}, inplace=True)
    data['X']['shares'] = data['X']['cancelled_shares']
    data['X'] = data['X'].dropna(subset=['price'])

    data = pd.concat([data[m] for m in messages], ignore_index=True, 
                      sort=False)

重建成功的交易,即作为与被取消的订单相对的订单的交易相关消息,CEPQ,相对比较简单:

def get_trades(m):
    """Combine C, E, P and Q messages into trading records"""
    trade_dict = {'executed_shares': 'shares', 'execution_price': 
                  'price'}
    cols = ['timestamp', 'executed_shares']
    trades = pd.concat([m.loc[m.type == 'E', cols + 
             ['price']].rename(columns=trade_dict),
             m.loc[m.type == 'C', cols + 
             ['execution_price']].rename(columns=trade_dict),
             m.loc[m.type == 'P', ['timestamp', 'price', 'shares']],
             m.loc[m.type == 'Q', ['timestamp', 'price', 
             'shares']].assign(cross=1),
             ], sort=False).dropna(subset=['price']).fillna(0)
    return trades.set_index('timestamp').sort_index().astype(int)

订单簿跟踪限价订单,并且买入和卖出订单的各种价格水平构成了订单簿的深度。要重建给定深度级别的订单簿,需要以下步骤:

  1. add_orders() 函数累积卖单按升序排列,买单按降序排列,以给定时间戳为基础直至达到所需的深度级别:
def add_orders(orders, buysell, nlevels):
    new_order = []
    items = sorted(orders.copy().items())
    if buysell == -1:
        items = reversed(items)  
    for i, (p, s) in enumerate(items, 1):
        new_order.append((p, s))
        if i == nlevels:
            break
    return orders, new_order
  1. 我们遍历所有 ITCH 消息,并根据规范要求处理订单及其替换:
for message in messages.itertuples():
    i = message[0]
    if np.isnan(message.buy_sell_indicator):
        continue
    message_counter.update(message.type)

    buysell = message.buy_sell_indicator
    price, shares = None, None

    if message.type in ['A', 'F', 'U']:
        price, shares = int(message.price), int(message.shares)

        current_orders[buysell].update({price: shares})
        current_orders[buysell], new_order = 
          add_orders(current_orders[buysell], buysell, nlevels)
        order_book[buysell][message.timestamp] = new_order

    if message.type in ['E', 'C', 'X', 'D', 'U']:
        if message.type == 'U':
            if not np.isnan(message.shares_replaced):
                price = int(message.price_replaced)
                shares = -int(message.shares_replaced)
        else:
            if not np.isnan(message.price):
                price = int(message.price)
                shares = -int(message.shares)

        if price is not None:
            current_orders[buysell].update({price: shares})
            if current_orders[buysell][price] <= 0:
                current_orders[buysell].pop(price)
            current_orders[buysell], new_order = 
              add_orders(current_orders[buysell], buysell, nlevels)
            order_book[buysell][message.timestamp] = new_order

不同价格水平上的订单数量,在以下截图中使用不同强度的颜色突出显示买入和卖出订单的深度流动性。左侧面板显示了限价订单价格分布偏向于更高价格的买单。右侧面板绘制了交易日内限价订单和价格的演变:深色线跟踪了市场交易小时内的执行交易价格,而红色和蓝色点表示每分钟的限价订单(详见笔记本):

规范化 tick 数据

交易数据按纳秒索引,噪音很大。例如,出现买卖市价订单交替引发的买卖跳动,导致价格在买入和卖出价格之间波动。为了提高噪声-信号比并改善统计性能,我们需要重新采样和规范化 tick 数据,通过聚合交易活动来实现。

通常,我们收集聚合期间的开盘(第一个)、最低、最高和收盘(最后)价格,以及成交量加权平均价格VWAP)、交易的股数和与数据相关的时间戳。

在此章节的 GitHub 文件夹中查看名为normalize_tick_data.ipynb的笔记本,以获取额外的细节。

Tick 柱状图

AAPL的原始 tick 价格和成交量数据的图表如下:

stock, date = 'AAPL', '20180329'
title = '{} | {}'.format(stock, pd.to_datetime(date).date()

with pd.HDFStore(itch_store) as store:
    s = store['S'].set_index('event_code') # system events
    s.timestamp = s.timestamp.add(pd.to_datetime(date)).dt.time
    market_open = s.loc['Q', 'timestamp'] 
    market_close = s.loc['M', 'timestamp']

with pd.HDFStore(stock_store) as store:
    trades = store['{}/trades'.format(stock)].reset_index()
trades = trades[trades.cross == 0] # excluding data from open/close crossings
trades.price = trades.price.mul(1e-4)

trades.price = trades.price.mul(1e-4) # format price
trades = trades[trades.cross == 0]    # exclude crossing trades
trades = trades.between_time(market_open, market_close) # market hours only

tick_bars = trades.set_index('timestamp')
tick_bars.index = tick_bars.index.time
tick_bars.price.plot(figsize=(10, 5), title=title), lw=1)

我们得到了前述代码的下列图表:

由于scipy.stats.normaltest的 p 值较低,可以看出 tick 返回远非正态分布:

from scipy.stats import normaltest
normaltest(tick_bars.price.pct_change().dropna())

NormaltestResult(statistic=62408.76562431228, pvalue=0.0)

时间柱状图

时间柱状图涉及按周期聚合交易:

def get_bar_stats(agg_trades):
    vwap = agg_trades.apply(lambda x: np.average(x.price, 
           weights=x.shares)).to_frame('vwap')
    ohlc = agg_trades.price.ohlc()
    vol = agg_trades.shares.sum().to_frame('vol')
    txn = agg_trades.shares.size().to_frame('txn')
    return pd.concat([ohlc, vwap, vol, txn], axis=1)

resampled = trades.resample('1Min')
time_bars = get_bar_stats(resampled)

我们可以将结果显示为价格-成交量图:

def price_volume(df, price='vwap', vol='vol', suptitle=title):
    fig, axes = plt.subplots(nrows=2, sharex=True, figsize=(15, 8))
    axes[0].plot(df.index, df[price])
    axes[1].bar(df.index, df[vol], width=1 / (len(df.index)), 
                color='r')

    xfmt = mpl.dates.DateFormatter('%H:%M')
    axes[1].xaxis.set_major_locator(mpl.dates.HourLocator(interval=3))
    axes[1].xaxis.set_major_formatter(xfmt)
    axes[1].get_xaxis().set_tick_params(which='major', pad=25)
    axes[0].set_title('Price', fontsize=14)
    axes[1].set_title('Volume', fontsize=14)
    fig.autofmt_xdate()
    fig.suptitle(suptitle)
    fig.tight_layout()
    plt.subplots_adjust(top=0.9)

price_volume(time_bars)

我们得到了前述代码的下列图表:

或者使用bokeh绘图库绘制蜡烛图:

resampled = trades.resample('5Min') # 5 Min bars for better print
df = get_bar_stats(resampled)

increase = df.close > df.open
decrease = df.open > df.close
w = 2.5 * 60 * 1000 # 2.5 min in ms

WIDGETS = "pan, wheel_zoom, box_zoom, reset, save"

p = figure(x_axis_type='datetime', tools=WIDGETS, plot_width=1500, title = "AAPL Candlestick")
p.xaxis.major_label_orientation = pi/4
p.grid.grid_line_alpha=0.4

p.segment(df.index, df.high, df.index, df.low, color="black")
p.vbar(df.index[increase], w, df.open[increase], df.close[increase], fill_color="#D5E1DD", line_color="black")
p.vbar(df.index[decrease], w, df.open[decrease], df.close[decrease], fill_color="#F2583E", line_color="black")
show(p)

请看以下截图:

绘制 AAPL 蜡烛图

成交量柱状图

时间柱状图平滑了原始 tick 数据中的一些噪音,但可能未能解决订单碎片化的问题。以执行为中心的算法交易可能旨在在给定期间内匹配成交量加权平均价格VWAP),并将单个订单分成多个交易,并根据历史模式下订单。时间柱状图会对相同的订单进行不同处理,即使市场没有新的信息到达。

成交量柱状图提供了一种根据成交量聚合交易数据的替代方法。我们可以按以下方式实现:

trades_per_min = trades.shares.sum()/(60*7.5) # min per trading day
trades['cumul_vol'] = trades.shares.cumsum()
df = trades.reset_index()
by_vol = 
   df.groupby(df.cumul_vol.div(trades_per_min).round().astype(int))
vol_bars = pd.concat([by_vol.timestamp.last().to_frame('timestamp'), 
                      get_bar_stats(by_vol)], axis=1)
price_volume(vol_bars.set_index('timestamp'))

我们得到了前述代码的下列图表:

美元柱状图

当资产价格发生显着变化或股票拆分后,给定数量股票的价值也会发生变化。 Volume bars 不会正确反映这一点,并且可能妨碍对反映这些变化的不同期间的交易行为进行比较。 在这些情况下,应调整 volume bar 方法,以利用股票和价格的乘积来生成美元 bars。

市场数据的 API 访问

有几种选项可以使用 Python 通过 API 访问市场数据。 我们首先介绍了内置于 pandas 库中的几个数据源。 然后,我们简要介绍了交易平台 Quantopian,数据提供商 Quandl 以及本书稍后将使用的回测库,并列出了访问各种类型市场数据的几种其他选项。 在 GitHub 上的文件夹目录 data_providers 包含了几个示例笔记本,演示了这些选项的使用方法。

使用 pandas 进行远程数据访问

pandas 库使用 read_html 函数访问网站上显示的数据,并通过相关的 pandas-datareader 库访问各种数据提供商的 API 端点。

阅读 HTML 表格

下载一个或多个 html 表格的内容的方法如下,例如从 Wikipedia 获取 S&P500 指数的成分:

sp_url = 'https://en.wikipedia.org/wiki/List_of_S%26P_500_companies'
sp = pd.read_html(sp_url, header=0)[0] # returns a list for each table
sp.info()

RangeIndex: 505 entries, 0 to 504
Data columns (total 9 columns):
Ticker symbol             505 non-null object
Security                  505 non-null object
SEC filings               505 non-null object
GICS Sector               505 non-null object
GICS Sub Industry         505 non-null object
Location                  505 non-null object
Date first added[3][4]    398 non-null object
CIK                       505 non-null int64
Founded                   139 non-null object

用于市场数据的 pandas-datareader

pandas 曾用于直接便捷访问数据提供商的 API,但该功能已迁移到相关的 pandas-datareader 库。 API 的稳定性因提供商政策而异,在 2018 年 6 月的版本 0.7 中,以下数据源可用:

来源 范围 注释
Yahoo! Finance 股票和外汇对的 EOD 价格、分红、拆分数据 不稳定
Tiingo 股票、共同基金和交易所交易基金的 EOD 价格 需要免费注册
投资者交易所IEX 历史股票价格,订单簿数据 限制为五年
Robinhood EOD 股票价格 限制为一年
Quandl 各种资产价格的市场 高级数据需要订阅
纳斯达克 最新在纳斯达克交易的股票代码以及一些额外信息
Stooq 一些股票市场指数数据
MOEX 莫斯科证券交易所数据
Alpha Vantage EOD 股票价格和外汇对
Fama/French 来自 FF 数据库的因子收益和研究组合

访问和检索数据的方式对所有数据源都是相似的,如 Yahoo! Finance 所示:

import pandas_datareader.data as web
from datetime import datetime

start = '2014'              # accepts strings
end = datetime(2017, 5, 24) # or datetime objects

yahoo= web.DataReader('FB', 'yahoo', start=start, end=end)
yahoo.info()

DatetimeIndex: 856 entries, 2014-01-02 to 2017-05-25
Data columns (total 6 columns):
High         856 non-null float64
Low          856 non-null float64
Open         856 non-null float64
Close        856 non-null float64
Volume       856 non-null int64
Adj Close    856 non-null float64

dtypes: float64(5), int64(1)

投资者交易所

IEX 是作为对高频交易争议的回应而启动的另一种交易所,出现在迈克尔·刘易斯有争议的《闪 Boys》中。 它旨在减缓交易速度,创造一个更公平的竞争环境,并自 2016 年推出以来一直在迅速增长,但在 2018 年 6 月仍然很小,市场份额约为 2.5%。

除了历史结束日价格和成交量数据外,IEX 还提供实时的订单簿深度报价,通过价格和方向聚合订单的规模。该服务还包括最后成交价和规模信息:

book = web.get_iex_book('AAPL')
orders = pd.concat([pd.DataFrame(book[side]).assign(side=side) for side in ['bids', 'asks']])
orders.sort_values('timestamp').head()

  price  size timestamp      side
4 140.00  100  1528983003604 bids
3 175.30  100  1528983900163 bids
3 205.80  100  1528983900163 asks
1 187.00  200  1528996876005 bids
2 186.29  100  1528997296755 bids

datareader.ipynb笔记本中查看更多示例。

Quantopian

Quantopian 是一家投资公司,提供一个研究平台来集体开发交易算法。免费注册后,它使会员能够使用各种数据源研究交易想法。它还提供了一个环境,用于对算法进行历史数据的回测,以及使用实时数据进行样本外测试。对于表现最佳的算法,它授予投资额度,其作者有权获得 10%的利润份额(在撰写本文时)。

Quantopian 研究平台包括用于 Alpha 因子研究和绩效分析的 Jupyter Notebook 环境。还有一个用于编写算法策略和使用自 2002 年以来带有分钟柱频率的历史数据回测结果的交互式开发环境IDE)。

用户还可以使用实时数据模拟算法,这称为纸上交易。Quantopian 提供各种市场数据集,包括美国股票和期货价格和成交量数据,频率为一分钟,以及美国股票公司基本面数据,并集成了众多替代数据集。

我们将在第四章中更详细地介绍 Quantopian 平台,Alpha 因子研究并且在整本书中依赖其功能,所以随时打开一个账户(有关更多详细信息,请参阅 GitHub repo)。

Zipline

Zipline 是算法交易库,为 Quantopian 回测和实时交易平台提供支持。它也可以离线使用,使用有限数量的免费数据包来开发策略,这些数据包可以被摄取并用于测试交易想法的表现,然后将结果转移到在线 Quantopian 平台进行纸上和实时交易。

以下代码说明了zipline允许我们访问一系列公司的每日股票数据。您可以在 Jupyter Notebook 中使用相同名称的魔术函数运行zipline脚本。

首先,您需要使用所需的安全符号初始化上下文。我们还将使用一个计数器变量。然后zipline调用handle_data,在其中我们使用data.history()方法回顾一个单一周期,并将上一天的数据附加到.csv文件中:

%load_ext zipline
%%zipline --start 2010-1-1 --end 2018-1-1 --data-frequency daily
from zipline.api import order_target, record, symbol

def initialize(context):
 context.i = 0
 context.assets = [symbol('FB'), symbol('GOOG'), symbol('AMZN')]

def handle_data(context, data):
 df = data.history(context.assets, fields=['price', 'volume'], 
                   bar_count=1, frequency="1d")
 df = df.to_frame().reset_index()

 if context.i == 0:
 df.columns = ['date', 'asset', 'price', 'volumne']
 df.to_csv('stock_data.csv', index=False)
 else:
     df.to_csv('stock_data.csv', index=False, mode='a', header=None)
                context.i += 1

df = pd.read_csv('stock_data.csv')
df.date = pd.to_datetime(df.date)
df.set_index('date').groupby('asset').price.plot(lw=2, legend=True, 
       figsize=(14, 6));

我们得到了上述代码的下列图表:

我们将在接下来的章节中更详细地探讨zipline的功能,特别是在线 Quantopian 平台。

Quandl

Quandl 提供广泛的数据来源,包括免费和订阅,使用 Python API。注册并获取免费 API 密钥,以进行 50 次以上的调用。Quandl 数据涵盖除股票外的多种资产类别,包括外汇、固定收益、指数、期货和期权以及商品。

API 的使用简单直观,文档完善,灵活性强,除了单个数据系列下载外,还有许多其他方法,例如批量下载或元数据搜索。以下调用获取了自 1986 年以来由美国能源部报价的石油价格:

import quandl
oil = quandl.get('EIA/PET_RWTC_D').squeeze()
oil.plot(lw=2, title='WTI Crude Oil Price')

我们通过前述代码得到了这个图表:

其他市场数据提供商

各种各样的提供商为各种资产类别提供市场数据。相关类别中的示例包括:

  • 交易所从数据服务中获得越来越广泛的收入,通常使用订阅方式。

  • 彭博和汤姆森路透一直是领先的数据聚合商,在 285 亿美元的金融数据市场中占有超过 55%的份额。较小的竞争对手,如 FactSet,正在增长,或者新兴的,如 money.net 和 Quandl 以及 Trading Economics 或 Barchart.

  • 专业数据提供商层出不穷。LOBSTER 就是一个例子,它实时聚合纳斯达克订单簿数据。

  • 免费数据提供商包括 Alpha Vantage,该公司提供 Python API 用于实时股票、外汇和加密货币市场数据,以及技术指标。

  • 提供数据访问的众包投资公司包括 Quantopian 以及于 2018 年 3 月推出的 Alpha Trading Labs,它们提供 HFT 基础设施和数据。

如何处理基本数据

基本数据涉及确定证券价值的经济驱动因素。数据的性质取决于资产类别:

  • 对于股票和公司信用,它包括公司财务数据以及行业和全球范围的数据。

  • 对于政府债券,它包括国际宏观数据和外汇。

  • 对于商品,它包括特定资产的供需决定因素,例如作物的天气数据。

我们将重点关注美国的股票基本面,因为数据更容易获取。全球有约 13,000 多家上市公司,每年产生 2 百万页的年度报告和 3 万多小时的收益电话。在算法交易中,基本数据和从这些数据中衍生的特征可能被直接用于推导交易信号,例如作为价值指标,并且是预测模型(包括机器学习模型)的重要输入。

财务报表数据

证券交易委员会SEC)要求美国发行人,即上市公司和证券,包括共同基金,提交三份季度财务报表(表格 10-Q)和一份年度报告(表格 10-K),以及其他各种监管文件要求。

自 20 世纪 90 年代初,美国证券交易委员会通过其电子数据收集、分析和检索EDGAR)系统提供这些报告。它们构成了股权和其他证券基本分析的主要数据来源,例如企业信用,其价值取决于发行者的业务前景和财务状况。

自动处理 – XBRL

自从 SEC 引入了 XBRL 以来,对监管提交的自动分析变得更加容易,XBRL 是一种免费、开放和全球标准,用于电子报告的表示和交换。XBRL 基于 XML;它依赖于定义报告元素含义的分类法,并映射到在报告的电子版本中突出显示相应信息的标签。其中一种分类法代表了美国普遍公认的会计准则GAAP)。

为了应对会计丑闻,SEC 于 2005 年引入了自愿的 XBRL 提交,之后从 2009 年开始要求所有提交者使用此格式,并继续将强制覆盖范围扩展到其他监管提交。SEC 维护一个网站,列出了塑造不同提交内容的当前分类法,并可用于提取特定项目。

以下数据集提供了从提交给委员会的 EX-101 附件中提取的信息,以扁平化数据格式提供,以帮助用户消化数据进行分析。数据反映了 XBRL 标记的财务报表的选定信息。目前,它包括季度和年度财务报表的数字数据,以及某些额外字段(例如标准工业分类SIC))。

有几种途径可跟踪和访问向 SEC 报告的基本数据:

  • 作为 EDGAR 公共 传播服务PDS)的一部分,接受的提交的电子订阅需要付费。

  • SEC 每 10 分钟更新一次RSS订阅,其中列出了结构化披露提交。

  • 有用于通过 FTP 检索所有提交的公共索引文件,以进行自动处理。

  • 财务报表(和附注)数据集包含了来自所有财务报表和相关附注的解析 XBRL 数据。

SEC 还发布了包含通过 SEC.gov 提交的 EDGAR 提交的互联网搜索流量的日志文件,尽管有六个月的延迟。

建立基本数据时间序列

财务报表和附注数据集中的数据范围包括从主要财务报表(资产负债表、利润表、现金流量表、权益变动表和全面收益表)和这些报表的附注中提取的数字数据。该数据最早可追溯至 2009 年。

提取财务报表和附注数据集

以下代码下载并提取了给定季度范围内财务报表和附注FSN)数据集中包含的所有历史提交(有关更多详细信息,请参阅edgar_xbrl.ipynb):

SEC_URL = 'https://www.sec.gov/files/dera/data/financial-statement-and-notes-data-sets/'

first_year, this_year, this_quarter = 2014, 2018, 3
past_years = range(2014, this_year)
filing_periods = [(y, q) for y in past_years for q in range(1, 5)]
filing_periods.extend([(this_year, q) for q in range(1, this_quarter + 
                                                     1)])
for i, (yr, qtr) in enumerate(filing_periods, 1):
    filing = f'{yr}q{qtr}_notes.zip'
    path = data_path / f'{yr}_{qtr}' / 'source'
    response = requests.get(SEC_URL + filing).content
    with ZipFile(BytesIO(response)) as zip_file:
        for file in zip_file.namelist():
            local_file = path / file
            with local_file.open('wb') as output:
                for line in zip_file.open(file).readlines():
                    output.write(line)

数据相当庞大,为了实现比原始文本文件更快的访问,最好将文本文件转换为二进制、列式 parquet 格式(请参见本章中关于与 pandas DataFrames兼容的各种数据存储选项的性能比较的使用 pandas 进行高效数据存储一节):

for f in data_path.glob('**/*.tsv'):
    file_name = f.stem  + '.parquet'
    path = Path(f.parents[1]) / 'parquet'
    df = pd.read_csv(f, sep='\t', encoding='latin1', low_memory=False)
    df.to_parquet(path / file_name)

对于每个季度,FSN 数据组织成包含有关提交、数字、税项标签、演示等信息的八个文件集。每个数据集由行和字段组成,并以制表符分隔的文本文件形式提供:

文件 数据集 描述
SUB 提交 根据公司、表格、日期等识别每个 XBRL 提交
TAG 标签 定义和解释每个税项标签
DIM 尺寸 对数字和纯文本数据进行详细描述
NUM 数字 每个备案中每个不同数据点的一行
TXT 纯文本 包含所有非数字 XBRL 字段
REN 渲染 在 SEC 网站上呈现的信息
PRE 展示 主要报表中标签和数字展示的详细信息
CAL 计算 显示标签之间的算术关系

检索所有季度苹果备案

提交数据集包含检索备案所需的唯一标识符:中央索引键CIK)和接入号(adsh)。以下显示了有关苹果 2018Q1 10-Q 备案的一些信息:

apple = sub[sub.name == 'APPLE INC'].T.dropna().squeeze()
key_cols = ['name', 'adsh', 'cik', 'name', 'sic', 'countryba',  
            'stprba', 'cityba', 'zipba', 'bas1', 'form', 'period', 
            'fy', 'fp', 'filed']
apple.loc[key_cols]

name                    APPLE INC
adsh                    0000320193-18-000070
cik                     320193
name                    APPLE INC
sic                     3571
countryba               US
stprba                  CA
cityba                  CUPERTINO
zipba                   95014
bas1                    ONE APPLE PARK WAY
form                    10-Q
period                  20180331
fy                      2018
fp                      Q2
filed                   20180502

使用中央索引键,我们可以识别出适用于Apple的所有历史季度备案,并将此信息结合起来获得 26 份10-Q表格和 9 份年度10-K表格:

aapl_subs = pd.DataFrame()
for sub in data_path.glob('**/sub.parquet'):
    sub = pd.read_parquet(sub)
    aapl_sub = sub[(sub.cik.astype(int) == apple.cik) & (sub.form.isin(['10-Q', '10-K']))]
    aapl_subs = pd.concat([aapl_subs, aapl_sub])

aapl_subs.form.value_counts()
10-Q    15
10-K     4

有了每个备案的接入号,我们现在可以依靠税项分类来从NUMTXT文件中选择适当的 XBRL 标签(在TAG文件中列出)以获取感兴趣的数字或文本/脚注数据点。

首先,让我们从 19 份苹果备案中提取所有可用的数字数据:

aapl_nums = pd.DataFrame()
for num in data_path.glob('**/num.parquet'):
    num = pd.read_parquet(num)
    aapl_num = num[num.adsh.isin(aapl_subs.adsh)]
    aapl_nums = pd.concat([aapl_nums, aapl_num])

aapl_nums.ddate = pd.to_datetime(aapl_nums.ddate, format='%Y%m%d')    
aapl_nums.shape
(28281, 16)

建立一个价格/收益时间序列

总共,九年的备案历史为我们提供了超过 28,000 个数字值。我们可以选择一个有用的字段,例如每股稀释收益EPS),将其与市场数据结合起来计算流行的市盈率P/E)估值比率。

但是,我们需要考虑到,苹果于 2014 年 6 月 4 日进行了 7:1 的股票分割,并且调整了分割前的每股收益,以使收益可比,如下图所示:

field = 'EarningsPerShareDiluted'
stock_split = 7
split_date = pd.to_datetime('20140604')

# Filter by tag; keep only values measuring 1 quarter
eps = aapl_nums[(aapl_nums.tag == 'EarningsPerShareDiluted')
                & (aapl_nums.qtrs == 1)].drop('tag', axis=1)

# Keep only most recent data point from each filing
eps = eps.groupby('adsh').apply(lambda x: x.nlargest(n=1, columns=['ddate']))

# Adjust earnings prior to stock split downward
eps.loc[eps.ddate < split_date,'value'] = eps.loc[eps.ddate < 
        split_date, 'value'].div(7)
eps = eps[['ddate', 'value']].set_index('ddate').squeeze()
eps = eps.rolling(4, min_periods=4).sum().dropna() # create trailing 
                  12-months eps from quarterly data

我们可以使用 Quandl 来获取自 2009 年以来的苹果股价数据:

import pandas_datareader.data as web
symbol = 'AAPL.US'
aapl_stock = web.DataReader(symbol, 'quandl', start=eps.index.min())
aapl_stock = aapl_stock.resample('D').last() # ensure dates align with 
                                               eps data

现在我们有了数据来计算整个期间的滚动 12 个月 P/E 比率:

pe = aapl_stock.AdjClose.to_frame('price').join(eps.to_frame('eps'))
pe = pe.fillna(method='ffill').dropna()
pe['P/E Ratio'] = pe.price.div(pe.eps)
axes = pe.plot(subplots=True, figsize=(16,8), legend=False, lw=2);

对于前述代码,我们得到了以下的绘图:

其他基本数据来源

还有许多其他的基础数据来源。许多可通过早期介绍的pandas_datareader模块访问。还有其他数据可以直接从某些组织获得,例如 IMF、世界银行或世界各地的主要国家统计机构(请参阅 GitHub 上的参考文献)。

pandas_datareader – 宏观和行业数据

pandas_datareader库根据上一节市场数据末尾介绍的约定简化了访问。它覆盖了许多全球基础宏观和行业数据源的 API,包括以下内容:

  • 肯尼斯·弗伦奇的数据库:捕捉规模、价值和动量因素、分解行业的投资组合的市场数据

  • 圣路易斯联邦储备银行(FRED):美国经济和金融市场的联邦储备数据

  • 世界银行:长期、低频经济和社会发展以及人口统计数据库

  • 经济合作与发展组织(OECD):类似于 OECD 国家

  • Enigma:各种数据集,包括替代来源

  • Eurostat:欧盟经济、社会和人口统计数据

用 pandas 进行高效的数据存储

在本书中我们将使用许多不同的数据集,值得比较主要格式的效率和性能。特别是,我们比较以下内容:

  • CSV: 逗号分隔,标准的纯文本文件格式。

  • HDF5:分层数据格式,最初在国家超级计算中心开发,是一种用于数值数据的快速可扩展的存储格式,可以使用PyTables库在 pandas 中使用。

  • Parquet: 二进制的,列式存储格式,是 Apache Hadoop 生态系统的一部分,提供了高效的数据压缩和编码,并由 Cloudera 和 Twitter 开发。通过由 pandas 的原始作者 Wes McKinney 领导的pyarrow库可用于 pandas。

storage_benchmark.ipynb笔记本使用一个可配置为包含数值数据、文本数据或两者的测试DataFrame来比较上述库的性能。对于HDF5库,我们测试fixedtable格式。table格式允许查询并且可以追加。

以下图表说明了具有随机浮点数的 100,000 行和 1000 个随机 10 个字符字符串列或仅有 2,000 个浮点列的 100,000 行的读写性能:

  • 对于纯数值数据,HDF5 格式性能最佳,并且表格式与 CSV 共享 1.6 GB 的最小内存占用。固定格式使用了两倍的空间,parquet 格式使用了 2 GB。

  • 对于数字和文本数据的混合,parquet显着更快,而 HDF5 则利用其相对于 CSV 的读取优势(在两种情况下写入性能都很低):

该笔记本演示了如何使用%%timeit单元格魔法配置、测试和收集时间,并同时演示了使用相关的 pandas 命令来使用这些存储格式所需的用法。

总结

本章介绍了构成大多数交易策略骨干的市场和基本数据来源。您了解了访问这些数据的多种方式,以及如何预处理原始信息,以便您可以开始使用我们即将介绍的机器学习技术提取交易信号。

在我们进入交易策略的设计和评估以及使用 ML 模型之前,我们需要涵盖近年来出现的替代数据集,这些数据集对算法交易的 ML 流行程度有着重要推动作用。

第三章:金融领域的替代数据

在互联网和移动网络的爆炸性增长推动下,数字数据在新数据源的处理、存储和分析技术进步的同时呈指数级增长。数字数据的可用性和管理能力的指数级增长,反过来又成为驱动创新的机器学习(ML)在包括投资行业在内的各行业的戏剧性性能提升背后的关键力量。

数据革命的规模是非凡的:仅过去两年就创造了今天世界上所有数据的 90%,到 2020 年,全球 77 亿人口预计每天每秒产生 1.7 MB 的新信息。另一方面,回到 2012 年,仅有 0.5% 的数据被分析和使用,而到 2020 年,有 33% 被认为具有价值。随着全球对分析的投资预计将于 2020 年超过 2100 亿美元,价值创造潜力将倍增,数据的可用性和使用之间的差距可能会迅速缩小。

本章解释了个人、业务流程和传感器如何产生替代数据。它还提供了一个框架,用于为投资目的导航和评估不断增长的替代数据供应。它演示了工作流程,从获取到预处理和使用 Python 存储通过网络抓取获得的数据,为 ML 应用铺平了道路。最后,它通过提供来源、供应商和应用程序的示例来结束。

本章将涵盖以下主题:

  • 替代数据革命如何释放新的信息来源

  • 个人、业务流程和传感器如何生成替代数据

  • 如何评估用于算法交易的不断增长的替代数据供应

  • 如何在 Python 中处理替代数据,例如通过抓取互联网

  • 替代数据的重要类别和提供商

替代数据革命

由数字化、网络化和存储成本的暴跌驱动的数据洪流已经导致可用于预测分析的信息性质发生了深刻的定性变化,通常由五个 V 总结:

  • 容量:由在线和离线活动、交易、记录和其他来源产生、收集和存储的数据量是数量级更大的副产品,随着分析和存储能力的增长,这些来源和数量也在不断增加。

  • 速度:数据生成、传输和处理以接近实时的速度变得可用。

  • 多样性:数据的组织格式不再局限于结构化、表格形式,如 CSV 文件或关系数据库表。相反,新的来源产生半结构化格式,如 JSON 或 HTML,以及非结构化内容,包括原始文本、图像和音频或视频数据,为使数据适用于机器学习算法增加了新的挑战。

  • 真实性:来源和格式的多样性使得验证数据信息内容的可靠性变得更加困难。

  • 价值:确定新数据集的价值可能会比以往更加耗时和耗资源,并且更加不确定。

对于算法交易而言,如果新数据来源提供了无法从传统来源获取的信息,或者提供了更早的获取机会,则它们将提供信息优势。随着全球趋势,投资行业正迅速扩展到超越市场和基本数据的替代来源,以通过信息优势实现阿尔法。数据、技术能力和相关人才的年度支出预计将从当前的 30 亿美元以每年 12.8%的速度增加到 2020 年。

如今,投资者可以实时获取宏观或公司特定的数据,而这些数据在历史上只能以更低的频率获得。新数据来源的用例包括以下内容:

  • 代表性商品和服务的在线价格数据可用于衡量通货膨胀

  • 商店访问或购买次数可以允许对公司或行业特定销售或经济活动进行实时估计

  • 卫星图像可以揭示农业产量,或者矿山或石油钻井平台上的活动,而这些信息在其他地方得到之前是不可用的

随着大数据集的标准化和采用的推进,传统数据中包含的信息可能会失去大部分预测价值。

此外,处理和整合多样化数据并应用机器学习的能力允许获得复杂的见解。过去,定量方法依赖于简单的启发式方法来使用历史数据对公司进行排名,例如按照价格/账面价值比进行排名,而机器学习算法则综合新指标,并学习和适应这些规则,考虑到市场数据的演变。这些见解创造了捕捉经典投资主题的新机会,如价值、动量、质量或情绪:

  • 动量:机器学习可以识别资产暴露于市场价格波动、行业情绪或经济因素

  • 价值:算法可以分析大量经济和行业特定的结构化和非结构化数据,超越财务报表,以预测公司的内在价值

  • 质量:集成数据的复杂分析使得能够评估顾客或员工评论、电子商务或应用流量,以确定市场份额或其他基本收益质量驱动因素的增益

然而,在实践中,有用的数据通常不是免费提供的,而是需要进行彻底的评估、昂贵的获取、谨慎的管理和复杂的分析才能提取可交易的信号。

替代数据的来源

替代数据由许多来源生成,但可以在高层次上分类为主要由以下来源产生的:

  • 在社交媒体上发布帖子、评论产品或使用搜索引擎的个人

  • 记录商业交易的企业,特别是信用卡支付,或作为中间商捕获供应链活动

  • 传感器,它们除了其他功能外,通过诸如卫星或安全摄像头的图像或通过诸如手机基站的移动模式捕获经济活动

替代数据的性质继续迅速发展,因为新的数据来源变得可用,而以前标记为替代的来源成为主流的一部分。例如,波罗的海干散货运价指数BDI),现在通过 Bloomberg 终端可用,汇集了数百家航运公司的数据以近似干散货运输船的供需情况。

替代数据包括原始数据以及已汇总或以某种形式加工以增加价值的数据。例如,一些提供商旨在提取可交易的信号,如情绪分数。我们将在第四章,Alpha 因子研究中讨论各种类型的提供商。

替代数据来源在关键方面有所不同,这些方面决定了它们对算法交易策略的价值或信号内容。我们将在下一节关于评估替代数据集中讨论这些方面。

个人

个人通过在线活动以及离线活动而自动生成电子数据,因为后者被电子捕获并经常与在线身份关联。由个人生成的数据通常是文本、图像或视频格式的非结构化数据,通过多个平台传播,包括:

  • 社交媒体帖子,例如在 Twitter、Facebook 或 LinkedIn 等综合性网站上的意见或反应,或在 Glassdoor 或 Yelp 等商业评论网站上

  • 在网站上反映对产品的兴趣或感知的电子商务活动,例如 Amazon 或 Wayfair

  • 利用诸如 Google 或 Bing 之类的平台的搜索引擎活动

  • 移动应用程序的使用情况、下载量和评论

  • 个人数据,如消息流量

社交媒体情感分析变得非常流行,因为它可以应用于个别股票、行业篮子或市场指数。最常见的来源是 Twitter,其次是各种新闻供应商和博客网站。由于通常是通过日益商品化的网络抓取获得,供应是竞争性的,价格更低。可靠的社交媒体数据集通常包括博客、推文或视频,在大规模消费者最近采用这些工具的情况下,历史不足五年。相比之下,搜索历史可追溯至 2004 年。

业务流程

企业和公共实体生产和收集许多有价值的替代数据来源。由业务流程产生的数据通常比个人生成的数据更有结构。它非常有效地作为活动的领先指标,这种活动通常以更低的频率可用。

由业务流程产生的数据包括:

  • 由处理器和金融机构提供的支付卡交易数据

  • 公司排放的数据由普通数字化活动或记录生成,例如银行记录、收银机扫描数据或供应链订单

  • 贸易流动和市场微观结构数据(如 L-2 和 L-3 订单簿数据,在第二章中有图示,市场与基本数据

  • 由信用评级机构或金融机构监测的公司支付,以评估流动性和信用状况

信用卡交易和公司排放数据,如销售点数据,是最可靠和最具预测性的数据集之一。信用卡数据的历史可追溯到约十年前,并且在不同的滞后期几乎可以实时获得,而公司盈利报告的滞后期为 2.5 周。公司排放数据的时间范围和报告滞后期因来源而异。市场微观结构数据的历史超过 15 年,而与之相比,销售方流量数据通常只有不到五年的一致历史。

传感器

嵌入在广泛范围设备中的网络传感器生成的数据是增长最快的数据来源之一,其增长受到智能手机普及和卫星技术成本降低的推动。

这一类替代数据通常非常无结构,通常比个人或业务流程生成的数据体积大得多,并且具有更高的处理挑战。该类别中的关键替代数据来源包括:

  • 卫星成像用于监测经济活动,例如建筑、航运或商品供应

  • 用于跟踪零售店铺交通的地理位置数据,例如使用志愿者智能手机数据,或者在运输路线上,例如在船只或卡车上

  • 在感兴趣的位置设置摄像头

  • 天气和污染传感器

物联网(IoT)将通过将网络微处理器嵌入个人和商业电子设备,如家用电器、公共空间和工业生产过程,进一步加速这类替代数据的大规模收集。

基于传感器的替代数据包括卫星图像、移动应用程序使用情况或蜂窝位置跟踪,通常具有三到四年的历史。

卫星

发射地理空间成像卫星所需的资源和时间表已经大幅降低;成本从数千万美元和数年的准备时间降至约 10 万美元,将小型卫星作为低地球轨道的辅助有效载荷发射。因此,公司可以使用整个卫星机队获得对特定位置的更高频率覆盖(目前约每天一次)。

应用案例包括监测经济和商业活动,可以通过航拍覆盖范围捕捉到,例如农业和矿产生产和货运、房地产或船舶的建造、工业事故如火灾,或位置感兴趣处的汽车和人流量。相关的传感器数据由用红外线光监视农作物的农业无人机贡献。

在卫星图像数据能够可靠用于 ML 模型之前,可能需要解决一些挑战。这些挑战包括考虑天气条件,特别是云层覆盖和季节效应,节假日期间,以及可能影响预测信号质量的特定位置的不规则覆盖。

地理位置数据

地理位置数据是传感器产生的另一类迅速增长的替代数据。一个熟悉的来源是智能手机,个人通过应用程序或 GPS、CDMA 或 WiFi 等无线信号自愿共享他们的地理位置,以测量周围地点(如商店、餐厅或活动场所)的人流量。

此外,越来越多的机场、购物中心和零售店安装了传感器,跟踪顾客的数量和移动。尽管最初部署这些传感器的动机通常是为了衡量营销活动的影响,但由此产生的数据也可以用于估算人流量或销售情况。用于捕捉地理位置的传感器包括 3D 立体视频和热成像,这降低了隐私顾虑,但对移动对象效果很好。还有安装在天花板上的传感器,以及压力敏感的垫子。一些供应商结合使用多个传感器,包括视觉、音频和手机定位,全面了解购物者的旅程,这不仅包括访问次数和时长,还延伸到转化和重复访问的测量。

评估替代数据集

替代数据的最终目标是在寻找产生 alpha 的交易信号的竞争中提供信息优势,即正的、不相关的投资回报。在实践中,从替代数据集中提取的信号可以作为独立基础使用,也可以作为定量策略的一部分与其他信号组合使用。如果基于单一数据集的策略产生的夏普比率足够高,独立使用是可行的,但在实践中很少见(有关信号测量和评估的详细信息,请参阅 第四章 Alpha 因子研究)。

量化公司正在建立可以单独是弱信号但组合后可能产生吸引人回报的 alpha 因子库。正如 第一章 用于交易的机器学习 中所强调的,投资因素应基于基本的经济理念,否则,它们更可能是对历史数据过拟合的结果,而不是对新数据持续产生 alpha 的结果。

由于竞争导致的信号衰减是一个严重问题,随着替代数据生态系统的发展,很少有数据集将保留有意义的夏普比率信号。延长替代数据集信号内容的半衰期的有效策略包括独家协议或专注于提高处理挑战以提高进入门槛的数据集。

评估标准

可以基于其信号内容的质量、数据的定性方面以及各种技术方面来评估替代数据集。

信号内容的质量

信号内容可以根据目标资产类别、投资风格、与传统风险溢价的关系以及最重要的,其 alpha 内容来评估。

资产类别

大多数替代数据集包含与股票和商品直接相关的信息。自 2006 年 Zillow 成功地开创了价格估算以后,针对房地产投资的有趣数据集也在增加。

随着用于监测企业支付的替代来源的发展,包括针对中小企业的,企业信用的替代数据正在增长。围绕固定收益和利率预测的替代数据是一个较新的现象,但随着更多的产品销售和价格信息被大规模地获取,它还在增加。

投资风格

大多数数据集关注特定行业和股票,因此自然吸引长短股权投资者。随着替代数据收集的规模和范围不断扩大,替代数据可能也会变得与宏观主题的投资者相关,例如消费信贷、新兴市场的活动和商品趋势。

一些替代数据可用作市场风险传统度量的代理,而其他信号则更相关于使用量化策略的高频交易者,这些策略在短期内进行。

风险溢价

一些替代数据集,如信用卡支付或社交媒体情绪,已被证明产生的信号与传统股票市场的风险溢价(如价值、动量和波动性质量)之间的相关性较低(低于 5%)。因此,将来自这类替代数据的信号与基于传统风险因素的算法交易策略结合起来,可以成为更多元化的风险溢价投资组合的重要组成部分。

Alpha 内容和质量

正当投资于替代数据集所需的信号强度自然取决于其成本,而替代数据的价格差异很大。评分社交情绪的数据可以以几千美元或更少的价格获得,而涵盖全面和及时的信用卡支付的数据则可能每年成本数百万美元。

我们将详细探讨如何使用历史数据,即所谓的回测,评估由替代数据驱动的交易策略,以估计数据集中包含的 Alpha 量。在个别情况下,一个数据集可能包含足够的 Alpha 信号来驱动一个独立的策略,但更典型的是结合各种替代和其他数据源的使用。在这些情况下,一个数据集允许提取出产生小正夏普比率的弱信号,这些信号在自身上不会获得资本分配,但当与类似的其他信号集成时可以提供投资组合级别的策略。然而,并不保证这一点,因为也有许多替代数据集不包含任何 Alpha 内容。

除了评估数据集的 Alpha 内容之外,还重要评估信号的增量或正交程度,即唯一于一个数据集还是已被其他数据捕获,在后一种情况下比较这种类型信号的成本。

最后,评估依赖于给定数据的策略的潜在容量是至关重要的,即可以分配的资本量而不会损害其成功,因为容量限制会使数据成本的回收变得更加困难。

数据的质量

数据集的质量是另一个重要的标准,因为它影响分析和货币化所需的工作量,以及它包含的预测信号的可靠性。质量方面包括数据频率和其可用历史长度、其所含信息的可靠性或准确性、它是否符合当前或潜在未来的法规,以及其使用的独家性。

法律和声誉风险

使用替代数据可能带有法律或声誉风险,特别是当它们包括以下项目时:

  • 重要非公开信息MNPI),因为它意味着侵犯了内幕交易法规

  • 个人身份信息PII),主要因为欧盟已经颁布了通用数据保护条例GDPR

因此,法律和合规要求需要彻底审查。当数据提供者也是积极基于数据集进行交易的市场参与者时,也可能存在利益冲突。

排他性

替代数据集包含的信号是否足够预测,能够在一段有意义的时间内以高夏普比率单独驱动策略的可能性与其可用性和处理的便利性成反比。换句话说,数据越独占,处理难度越大,数据越有可能具有阿尔法内容,而不会遭受快速信号衰减的影响。

提供标准财务比率的公共基本数据包含很少的阿尔法,不适合独立策略,但可能有助于多样化风险因素的投资组合。大型复杂数据集需要更长的时间被市场吸收,并且新数据集继续频繁出现。因此,评估其他投资者对数据集的熟悉程度以及提供商是否是此类信息的最佳来源至关重要。

当企业开始销售其为其他目的生成的废弃数据时,排他性或成为新数据集的早期采用者可能会带来额外的好处,因为可能可以影响数据的收集或策划方式,或者协商限制竞争对手访问的条件,至少在一定时间段内。

时间范围

对于在不同情景下测试数据集的预测能力而言,更广泛的历史记录非常理想。可用性在几个月到几十年之间变化很大,并且对基于数据构建和测试的交易策略的范围有重要影响。我们在介绍主要来源的主要类型时提到了一些不同数据集的时间范围。

频率

数据的频率决定了新信息多久可用一次以及在给定时期内预测信号可以多么差异化。它还影响投资策略的时间范围,范围从每日,到每周,甚至更低的频率。

可靠性

当然,数据准确反映其意图的程度以及这可以得到多好的验证是非常重要的关注点,并且应通过彻底的审计进行验证。这适用于原始数据和处理过的数据,其中提取或聚合信息的方法需要进行分析,考虑到提议收购的成本效益比。

技术方面

技术方面关注报告的延迟或延迟以及数据提供的格式。

延迟

数据提供商通常提供批量资源,延迟可能来自数据的收集方式、后续处理和传输,以及法规或法律约束。

格式

数据的可用格式范围广泛,取决于来源。处理后的数据将以用户友好的格式提供,并可通过强大的 API 轻松集成到现有系统或查询中。另一方面,体积庞大的数据源,如视频、音频或图像数据,或专有格式,需要更多的技能来准备分析,但也为潜在竞争者提供了更高的准入壁垒。

替代数据市场

投资行业预计在 2018 年将花费约 2,000,000,000-3,000,000,000 美元用于数据服务,预计这一数字将与其他行业保持两位数的增长。这些支出包括替代数据的获取、相关技术的投资以及合格人才的聘用。

安永的一项调查显示,2017 年替代数据的使用得到了广泛的应用;例如,有 43%的基金使用了网络抓取的数据,几乎 30%的基金正在尝试卫星数据。根据迄今为止的经验,基金经理认为网络抓取的数据和信用卡数据最具洞察力,而地理定位和卫星数据约 25%的人认为不够信息丰富:

反映这个新兴行业的快速增长,替代数据提供商市场相当分散。根据摩根大通的数据,有超过 500 家专业数据公司,而AlternativeData.org列出了 300 多家。供应商扮演着多种角色,包括咨询公司、数据聚合商和技术解决方案;卖方支持以各种格式提供数据,从原始数据到半加工数据或从一个或多个来源提取的信号形式。

我们将重点介绍主要类别的规模,并概述一些突出的例子,以说明它们的多样性。

数据提供商和使用案例

AlternativeData.org(由供应商 Yipit 支持)列出了几个类别,可以作为各种数据提供商领域活动的大致代理。社交情绪分析是迄今为止最大的类别,而卫星和地理定位数据近年来增长迅速:

产品类别 供应商数量 目标
社交情绪 48 原始或加工后的社交媒体数据;短期趋势
卫星 26 中期经济活动的航拍监测
地理定位 22 追踪零售、商业地产或活动人流量
网络数据和流量 22 监控搜索兴趣、品牌流行度和事件
信用卡和借记卡使用情况 14 追踪短期消费者支出和企业收入
应用使用情况 7 监控应用销售或收集二手数据
电子邮件和消费者收据 6 通过连锁店、品牌、行业或地理位置跟踪消费者支出
天气 4 与作物和商品相关的长期趋势
其他 87

以下简要示例旨在说明服务提供商的广泛范围和潜在用例。

社会情感数据

社会情感分析与 Twitter 数据最密切相关。Gnip 是一个早期的社交媒体聚合器,通过 API 从许多网站提供数据,并于 2014 年以 1.34 亿美元的价格被 Twitter 收购。搜索引擎是另一个来源,当研究人员发表在《自然》杂志上时,基于 Google Trends 的投资策略(例如债务)可以用于一个较长时期的有利交易策略时,它变得显著(参见 GitHub repo github.com/PacktPublishing/Hands-On-Machine-Learning-for-Algorithmic-Trading 的参考资料)。

Dataminr

Dataminr 成立于 2009 年,根据与 Twitter 的独家协议提供社会情感和新闻分析。该公司是较大的替代性提供商之一,并于 2018 年 6 月由富达领投筹集了额外的 3.92 亿美元,估值达到 16 亿美元,使其总融资达到 569 亿美元。它强调使用机器学习从社交媒体提取的实时信号,并为广泛的客户提供服务,包括不仅仅是买卖双方的投资公司,还有新闻组织和公共部门。

StockTwits

StockTwits 是一个社交网络和微博平台,几十万投资专业人士在其中分享信息和交易想法,这些信息以 StockTwits 的形式被广泛的金融网络和社交媒体平台的大量观众所关注。这些数据可以被利用,因为它可能反映了投资者的情绪或本身驱动了交易,反过来又影响了价格。GitHub 上的参考资料包含了一篇建立在选定特征上的交易策略的链接。

RavenPack

RavenPack 分析大量不同的非结构化基于文本的数据,生成包含对投资者相关信息的结构化指标,包括情感评分。底层数据来源包括高级新闻线和监管信息,以及新闻稿和超过 19,000 个网上出版物。摩根大通测试了基于情感评分的多头主权债券和股票策略,并取得了与传统风险溢价低相关的积极结果(见参考资料)。

卫星数据

RS Metrics 成立于 2010 年,通过卫星、无人机和飞机三角测量地理空间数据,重点关注金属和商品、房地产和工业应用。该公司基于自己的高分辨率卫星提供信号、预测分析、警报和终端用户应用。使用案例包括估算针对特定连锁店或商业地产的零售流量,以及某些常见金属的生产和储存或相关生产地点的就业情况。

地理位置数据

Advan 成立于 2015 年,为对冲基金客户提供源自手机流量数据的信号,目标是美国和欧洲各个领域的 1600 个股票。该公司使用应用程序收集数据,在明确获得用户同意的情况下,在智能手机上安装地理位置代码,并使用多个通道(如 WiFi、蓝牙和蜂窝信号)跟踪位置以提高准确性。使用案例包括估算实体店位置的客流量,进而作为预测交易公司收入的模型的输入。

电子邮件收据数据

Eagle Alpha 提供了一系列服务,其中包括利用电子邮件收据的大量在线交易数据,涵盖了 5000 多个零售商,包括在 53 个产品组中分类的项目和 SKU 级交易数据。摩根大通分析了一个时间序列数据集,从 2013 年开始,涵盖了整个样本期间内始终活跃的用户群。数据集包含每个时期的总体花费、订单数量和独立买家数量。

处理替代数据

我们将通过网络爬虫说明替代数据的获取,首先针对 OpenTable 餐厅数据,然后转移到 Seeking Alpha 托管的盈利电话转录。

爬取 OpenTable 数据

替代数据的典型来源是评价网站,如 Glassdoor 或 Yelp,通过员工评论或客户评论传达内部见解。这些数据为旨在预测企业前景或直接其市值以获取交易信号的 ML 模型提供了宝贵的输入。

数据需要从 HTML 源中提取,除非有法律障碍。为了说明 Python 提供的网络爬虫工具,我们将从 OpenTable 检索有关餐厅预订的信息。此类数据可用于预测地理位置的经济活动、房地产价格或餐厅连锁收入。

使用 requests 和 BeautifulSoup 从 HTML 中提取数据

在本节中,我们将请求和解析 HTML 源代码。我们将使用 requests 库进行 超文本传输协议 (HTTP) 请求和检索 HTML 源代码,使用 BeautifulSoup 解析和提取文本内容。

然而,我们将遇到一个常见的障碍:网站可能只在初始页面加载后使用 JavaScript 请求某些信息。因此,直接的 HTTP 请求将不会成功。为了规避这种类型的保护,我们将使用一个无界面浏览器,以浏览器的方式检索网站内容:

from bs4 import BeautifulSoup
import requests

# set and request url; extract source code
url = "https://www.opentable.com/new-york-restaurant-listings"
html = requests.get(url)
html.text[:500]

' <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE"/> <title>Restaurant Reservation Availability</title> <meta name="robots" content="noindex" > </meta> <link rel="shortcut icon" href="//components.otstatic.com/components/favicon/1.0.4/favicon/favicon.ico" type="image/x-icon"/><link rel="icon" href="//components.otstatic.com/components/favicon/1.0.4/favicon/favicon-16.png" sizes="16x16"/><link rel='

现在我们可以使用 BeautifulSoup 解析 HTML 内容,然后查找我们通过检查源代码获得的所有与餐厅名称相关的span 标签,即 rest-row-name-text(请参阅 GitHub 仓库中链接的指令以检查网站源代码):

# parse raw html => soup object
soup = BeautifulSoup(html.text, 'html.parser')

# for each span tag, print out text => restaurant name
for entry in soup.find_all(name='span', attrs={'class':'rest-row-name-
text'}):
    print(entry.text)

Wade Coves
Alley
Dolorem Maggio
Islands
...

一旦你识别出感兴趣的页面元素,BeautifulSoup 将很容易地检索其中包含的文本。如果你想要获取每个餐厅的价格类别,你可以使用:

# get the number of dollars signs for each restaurant
for entry in soup.find_all('div', {'class':'rest-row-pricing'}):
    price = entry.find('i').text

当你尝试获取预订数量时,然而,你只会得到一个空列表,因为该网站使用 JavaScript 代码在初始加载完成后请求此信息:

soup.find_all('div', {'class':'booking'})
[]

介绍 Selenium - 使用浏览器自动化

我们将使用浏览器自动化工具 Selenium 操作一个无头 FireFox 浏览器,它将为我们解析 HTML 内容。

以下代码打开了 FireFox 浏览器:

from selenium import webdriver

# create a driver called Firefox
driver = webdriver.Firefox()

让我们关闭浏览器:

# close it
driver.close()

要使用 selenium 和 Firefox 检索 HTML 源代码,请执行以下操作:

import time, re

# visit the opentable listing page
driver = webdriver.Firefox()
driver.get(url)

time.sleep(1) # wait 1 second

# retrieve the html source
html = driver.page_source
html = BeautifulSoup(html, "lxml")

for booking in html.find_all('div', {'class': 'booking'}):
    match = re.search(r'\d+', booking.text)
    if match:
        print(match.group())

建立一个餐厅预订数据集

现在,你只需要将网站中所有有趣的元素结合起来,创建一个特性,你可以将其用于模型中,以预测地理区域的经济活动或特定社区的人流量。

使用 Selenium,你可以跟随链接到下一页,并快速构建纽约市超过 10,000 家餐厅的数据集,然后定期更新以跟踪时间序列。首先,我们设置一个函数来解析我们计划爬取的页面的内容:

def parse_html(html):
    data, item = pd.DataFrame(), {}
    soup = BeautifulSoup(html, 'lxml')
    for i, resto in enumerate(soup.find_all('div', class_='rest-row-
           info')):
        item['name'] = resto.find('span', class_='rest-row-name-
                                   text').text

        booking = resto.find('div', class_='booking')
        item['bookings'] = re.search('\d+', booking.text).group() if 
                                       booking else 'NA'

        rating = resto.select('div.all-stars.filled')
        item['rating'] = int(re.search('\d+', 
                rating[0].get('style')).group()) if rating else 'NA'

        reviews = resto.find('span', class_='star-rating-text--review-
                              text')
        item['reviews'] = int(re.search('\d+', reviews.text).group()) if reviews else 'NA'

        item['price'] = int(resto.find('div', class_='rest-row-
                            pricing').find('i').text.count('$'))
        item['cuisine'] = resto.find('span', class_='rest-row-meta--
                                      cuisine').text
        item['location'] = resto.find('span', class_='rest-row-meta--
                                       location').text
        data[i] = pd.Series(item)
    return data.T

然后,我们启动一个无界面浏览器,它将继续为我们点击“下一页”按钮,并捕获每个页面显示的结果:

restaurants = pd.DataFrame()
driver = webdriver.Firefox()
url = "https://www.opentable.com/new-york-restaurant-listings"
driver.get(url)
while True:
    sleep(1)
    new_data = parse_html(driver.page_source)
    if new_data.empty:
        break
    restaurants = pd.concat([restaurants, new_data], ignore_index=True)
    print(len(restaurants))
    driver.find_element_by_link_text('Next').click()
driver.close()

网站仍在不断变化,因此这段代码可能在某些时候停止工作,并需要更新以跟随最新的站点导航和机器人检测。

进一步的一步 - Scrapy 和 splash

Scrapy 是一个强大的库,用于构建跟随链接、检索内容并以结构化方式存储解析结果的机器人。结合无头浏览器 splash 使用,它还可以解释 JavaScript,并成为 Selenium 的高效替代方案。你可以在 01_opentable 目录中使用 scrapy crawl opentable 命令运行蜘蛛,结果将记录在 spider.log 中:

from opentable.items import OpentableItem
from scrapy import Spider
from scrapy_splash import SplashRequest

class OpenTableSpider(Spider):
    name = 'opentable'
    start_urls = ['https://www.opentable.com/new-york-restaurant-
                   listings']

    def start_requests(self):
        for url in self.start_urls:
            yield SplashRequest(url=url,
                                callback=self.parse,
                                endpoint='render.html',
                                args={'wait': 1},
                                )

    def parse(self, response):
        item = OpentableItem()
        for resto in response.css('div.rest-row-info'):
            item['name'] = resto.css('span.rest-row-name-
                                      text::text').extract()
            item['bookings'] = 
                  resto.css('div.booking::text').re(r'\d+')
            item['rating'] = resto.css('div.all-
                  stars::attr(style)').re_first('\d+')
            item['reviews'] = resto.css('span.star-rating-text--review-
                                         text::text').re_first(r'\d+')
            item['price'] = len(resto.css('div.rest-row-pricing > 
                                i::text').re('\$'))
            item['cuisine'] = resto.css('span.rest-row-meta--
                                         cuisine::text').extract()
            item['location'] = resto.css('span.rest-row-meta--
                               location::text').extract()
            yield item

从这些数据中提取信息的方法有很多,超出了个别餐厅或连锁店的评论和预订。

我们还可以进一步收集并对餐馆的地址进行地理编码,以将餐馆的物理位置与其他感兴趣的区域(如热门零售店或街区)联系起来,以获取有关经济活动特定方面的见解。如前所述,这样的数据结合其他信息将最有价值。

收益电话转录

文本数据是一个重要的替代数据来源。一个例子是收益电话的转录,执行人员不仅展示最新的财务结果,还回答金融分析师的问题。投资者利用这些转录来评估情绪变化、特定主题的强调或沟通风格。

我们将说明如何从流行的交易网站www.seekingalpha.com爬取和解析收益电话的转录:

import re
from pathlib import Path
from time import sleep
from urllib.parse import urljoin
from bs4 import BeautifulSoup
from furl import furl
from selenium import webdriver

transcript_path = Path('transcripts')

SA_URL = 'https://seekingalpha.com/'
TRANSCRIPT = re.compile('Earnings Call Transcript')

next_page = True
page = 1
driver = webdriver.Firefox()
while next_page:
    url = f'{SA_URL}/earnings/earnings-call-transcripts/{page}'
    driver.get(urljoin(SA_URL, url))
    response = driver.page_source
    page += 1
    soup = BeautifulSoup(response, 'lxml')
    links = soup.find_all(name='a', string=TRANSCRIPT)
    if len(links) == 0:
        next_page = False
    else:
        for link in links:
            transcript_url = link.attrs.get('href')
            article_url = furl(urljoin(SA_URL, 
                           transcript_url)).add({'part': 'single'})
            driver.get(article_url.url)
            html = driver.page_source
            meta, participants, content = parse_html(html)
            meta['link'] = link

driver.close()

使用正则表达式解析 HTML

为了从非结构化的转录中收集结构化数据,我们可以使用正则表达式以及 BeautifulSoup

它们让我们不仅可以收集有关收益电话公司和时间的详细信息,还可以记录谁在场,并将声明归因于分析师和公司代表:

def parse_html(html):
    date_pattern = re.compile(r'(\d{2})-(\d{2})-(\d{2})')
    quarter_pattern = re.compile(r'(\bQ\d\b)')
    soup = BeautifulSoup(html, 'lxml')

    meta, participants, content = {}, [], []
    h1 = soup.find('h1', itemprop='headline').text
    meta['company'] = h1[:h1.find('(')].strip()
    meta['symbol'] = h1[h1.find('(') + 1:h1.find(')')]

    title = soup.find('div', class_='title').text
    match = date_pattern.search(title)
    if match:
        m, d, y = match.groups()
        meta['month'] = int(m)
        meta['day'] = int(d)
        meta['year'] = int(y)

    match = quarter_pattern.search(title)
    if match:
        meta['quarter'] = match.group(0)

    qa = 0
    speaker_types = ['Executives', 'Analysts']
    for header in [p.parent for p in soup.find_all('strong')]:
        text = header.text.strip()
        if text.lower().startswith('copyright'):
            continue
        elif text.lower().startswith('question-and'):
            qa = 1
            continue
        elif any([type in text for type in speaker_types]):
            for participant in header.find_next_siblings('p'):
                if participant.find('strong'):
                    break
                else:
                    participants.append([text, participant.text])
        else:
            p = []
            for participant in header.find_next_siblings('p'):
                if participant.find('strong'):
                    break
                else:
                    p.append(participant.text)
            content.append([header.text, qa, '\n'.join(p)])
    return meta, participants, content

我们将结果存储在几个 .csv 文件中,以便在使用 ML 处理自然语言时轻松访问:

def store_result(meta, participants, content):
    path = transcript_path / 'parsed' / meta['symbol']
    if not path.exists():
        path.mkdir(parents=True, exist_ok=True)
    pd.DataFrame(content, columns=['speaker', 'q&a', 
              'content']).to_csv(path / 'content.csv', index=False)
    pd.DataFrame(participants, columns=['type', 'name']).to_csv(path / 
                 'participants.csv', index=False)
    pd.Series(meta).to_csv(path / 'earnings.csv'

在 GitHub 仓库中的 README 中查看其他详细信息和引用,以获取进一步开发网络爬虫应用程序的资源。

总结

在本章中,我们介绍了作为大数据革命结果而可用的新型替代数据来源,包括个人、业务流程和传感器,例如卫星或 GPS 位置设备。我们提出了一个框架来从投资的角度评估替代数据集,并列出了帮助您导航这个提供关键输入的庞大且迅速扩张的领域的关键类别和提供者,这些输入用于使用 ML 的算法交易策略。

我们探索了强大的 Python 工具,以大规模收集您自己的数据集,这样您就有可能通过网络爬取来获得您的私人信息优势,成为一个算法交易员。

我们现在将在接下来的章节中进行设计和评估产生交易信号的 alpha 因子,并研究如何在投资组合背景下将它们结合起来。

第四章:阿尔法因子研究

算法交易策略受信号驱动,该信号指示何时购买或出售资产以产生相对于基准的正收益。资产收益的一部分不受基准暴露的影响被称为阿尔法,因此这些信号也被称为阿尔法因子

阿尔法因子旨在基于可用的市场、基本或替代数据预测投资领域资产的价格走势。一个因子可能结合一个或多个输入变量,但在每次策略评估因子时为每个资产假定一个单一值。交易决策通常依赖于跨资产的相对值。交易策略通常基于多个因子发出的信号,我们将看到机器学习ML)模型特别适合有效地整合各种信号以进行更准确的预测。

阿尔法交易策略工作流程研究阶段的设计、评估和结合阿尔法因子是关键步骤,如下图所示。我们将在这个第四章中专注于研究阶段,策略评估,并在下一章中关注执行阶段。本书的其余部分将着重于使用 ML 来发现和结合阿尔法因子。请看下面的图:

本章将使用简单的均值回归因子介绍用 Python 编写的算法交易模拟器zipline,并促进对给定投资领域的阿尔法因子进行测试。在下一章中,我们还将在组合上回测交易策略时使用zipline。接下来,我们将讨论评估阿尔法因子预测性能的关键指标,包括信息系数和信息比率,这导致了主动管理的基本定律。

特别是,本章将讨论以下主题:

  • 如何表征、证明和衡量关键类型的阿尔法因子

  • 如何使用金融特征工程创建阿尔法因子

  • 如何离线使用zipline测试单个阿尔法因子

  • 如何在 Quantopian 上使用zipline结合阿尔法因子并识别更复杂的信号

  • 信息系数IC)如何衡量阿尔法因子的预测性能

  • 如何使用alphalens评估预测性能和换手率

工程化阿尔法因子

阿尔法因子是包含预测信号的市场、基本和替代数据的转换。它们旨在捕获驱动资产收益的风险。一组因子描述了基本的、全面的变量,如增长、通货膨胀、波动性、生产力和人口风险。另一组包括可交易的投资风格,如市场组合、价值增长投资和动量投资。

还有一些因素可以根据金融市场的经济学或机构设置,或者投资者行为来解释价格的变动,包括这种行为的已知偏差。因素背后的经济理论可以是合理的,其中因子在长期内具有高回报,以弥补它们在不好的时期的低回报,或者是行为的,其中因子风险溢价来自于可能偏见的、或者不完全理性的代理人行为,这种行为不会被套利掉。

不断寻找和发现可能更好地捕捉已知或反映新收益驱动因素的新因素。Research Affiliates 的联合创始人、管理近 2000 亿美元的 Jason Hsu 在 2015 年确定了约 250 个因素,在知名期刊中发表了经验证据,并估计该数字每年可能增加 40 个因素。为了避免假发现并确保因子提供一致的结果,它应该有一个有意义的经济直觉,使得它可信地反映市场会补偿的风险。

数据转换包括简单的算术,如某一变量随时间的绝对或相对变化、数据系列之间的比率,或者在时间窗口内的聚合,例如简单或指数移动平均。它们还包括从价格模式的技术分析中出现的计算,比如需求与供应的相对强度指数和从证券基本分析中熟悉的众多指标。

重要的因子类别

在理想化的世界中,风险因子的类别应该彼此独立(正交),产生正的风险溢价,并形成一个完整的集合,涵盖了资产在给定类别中的所有风险维度,并解释了系统风险。实际上,这些要求只会近似成立。我们将介绍如何使用无监督学习来推导合成的、数据驱动的风险因子,特别是在 第十二章,无监督学习 中的主成分和独立成分分析。

我们将审查从市场、基本和替代数据衍生的因子的关键类别,以及用于捕捉它们的典型指标。我们还将演示如何在 Quantopian 平台上测试算法时实现这些因子,使用内置因子、使用 numpypandas 进行自定义计算,或者使用 talib 库进行技术分析。

动量和情绪因子

动量投资遵循这样的格言:趋势是你的朋友,或者让你的赢家继续奔跑。动量风险因子旨在做多表现良好的资产,同时做空表现不佳的资产,而且要在一定期间内表现良好。

依赖这一因素的策略的前提是资产价格表现出趋势,反映在正的串行相关性中。这种价格动量会违背有效市场假设的假设,即过去的价格回报本身不能预测未来的表现。尽管存在相反的理论论据,但价格动量策略在各类资产中产生了正的回报,并且是许多交易策略的重要组成部分。

理由

动量效应的原因指向投资者行为、持续的供求失衡、风险资产和经济之间的正反馈循环,或者市场微观结构。

行为原因反映了投资者对市场新闻的反应偏差,因为投资者以不同的速度处理新信息。在对新闻的初始反应之后,投资者经常会推断过去的行为,并产生价格动量。上世纪九十年代晚期科技股市场泡沫期间的上涨就是一个极端例子。恐惧和贪婪心理也促使投资者增加对获胜资产的曝光,并继续出售亏损资产。

动量也可以有基本的驱动因素,比如风险资产与经济之间的正反馈循环。经济增长推动了股票市场,而由此产生的财富效应通过更高的支出再次反馈到经济中,从而推动增长。价格和经济之间的正反馈往往会使股票和信贷的动量延伸到比债券、外汇和商品更长的时间跨度,其中负反馈会引发反转,需要更短的投资视野。动量的另一个原因可能是由于市场摩擦造成的持续的供需失衡,例如,当商品生产需要较长时间来调整以适应需求趋势时。石油生产可能会滞后于经济繁荣引起的需求增加多年,持续的供应短缺可能会引发并支持价格上涨的动量。

市场微观结构效应也可能会产生与行为模式相关的价格动量,这些行为模式促使投资者购买产品并实施模仿其偏见的策略。例如,根据交易智慧割损和让利润增长的原则,投资者使用交易策略,如止损、恒定比例组合保险(CPPI)、动态对冲、或者基于期权的策略,如保护性买入期权。这些策略会产生动量,因为它们意味着在资产表现不佳时进行预先承诺的出售,并在其表现优异时进行购买。同样,风险平价策略(见下一章)倾向于购买低波动率资产,这些资产通常表现出积极的业绩,并出售高波动率资产,这些资产通常表现不佳。使用这些策略自动平衡投资组合会加强价格动量。

关键指标

动量因子通常是通过识别趋势和模式来推导价格时间序列的变化得到的。它们可以基于绝对回报或相对回报构建,通过比较资产的横截面或分析资产的时间序列,在传统资产类别内或跨越传统资产类别,并在不同的时间范围内进行。

下表列出了一些流行的示意性指标:

因子 描述
相对强度指标 (RSI) RSI 比较股票的最近价格变动,以识别股票是否被过度买入或过度卖出。高 RSI(例如,超过 70)表示过度买入,低 RSI(例如低于 30)表示过度卖出。它使用给定数量的前几个交易日的平均价格变动与正价格变动  和负价格变动  计算: 
价格动量 此因子计算给定数量的前几个交易日的总回报。在学术文献中,通常使用过去 12 个月,但排除最近的一个月,因为最近价格波动经常观察到短期反转效应,但也广泛使用较短的周期。
12 个月价格动量 Vol Adj 考虑波动率因子的 12 个月价格动量调整将前 12 个月的总回报归一化,方法是将其除以这些回报的标准偏差。
价格加速度 价格加速度使用线性回归在较长和较短周期的每日价格上计算趋势的梯度(根据波动率调整),例如,一年和三个月的交易日,并比较斜率的变化作为价格加速度的衡量。
百分比偏离 52 周最高价 此因子使用最近 52 周的最高价格与最近价格之间的百分差异。

其他情绪指标包括以下内容:

因子 描述
盈利预测数量 此度量指标根据共识估计数量对股票进行排名,作为分析师覆盖范围和信息不确定性的代理。较高的值更可取。
推荐 N 个月变化 此因子根据前 N 个月的共识推荐变化对股票进行排名,改善是可取的(无论是从强卖出到卖出还是从买入到强买入等)。
流通股份 12 个月变化 此因子衡量公司上市股份计数在过去 12 个月的变化,其中负变化意味着股份回购,这是可取的,因为它表明管理层认为股票相对于其内在价值和未来价值而言便宜。
目标价格变动的 6 个月变化 此指标跟踪平均分析师目标价格的 6 个月变化,较高的正变化自然更可取。
净收益修订 这一因素表示盈利预测的上调和下调之间的差异,以修订总数的百分比表示。
流通股份的卖空量 这个指标是目前被卖空的流通股份的百分比,即由借入股份并在以后的某一天回购它的投资者出售的股份,而这个投资者在猜测其价格将下跌。因此,高水平的卖空量表明负面情绪,并预示着未来的表现不佳。

价值因素

相对于其基本价值而言价格较低的股票往往会产生超过市值加权基准的回报。价值因素反映了这种相关性,并旨在提供购买被低估资产的信号,即那些相对便宜的资产,并卖出那些被高估和昂贵的资产。因此,任何价值策略的核心都是一个估计或代理资产公平或基本价值的估值模型。公平价值可以定义为绝对价格水平、相对于其他资产的价差,或者资产应该交易的范围(例如,两个标准偏差)。

价值策略依赖于价格回归到资产的公平价值。它们假设价格仅因行为效应(例如过度反应或群体行为)或流动性效应(例如临时市场影响或长期供求摩擦)而暂时偏离公平价值。由于价值因素依赖于价格回归,它们通常表现出与动量因素相反的性质。对于股票来说,价值股的对立面是由于增长预期而具有高估值的成长股。

价值因素可以实现广泛的系统性策略,包括基本和市场估值、统计套利和跨资产相对价值。它们通常被实施为无暴露于其他传统或替代风险因素的多空投资组合。

基本价值策略从取决于目标资产类别的经济和基本指标中得出公平资产价值。在固定收益、货币和大宗商品方面,指标包括资本账户余额水平和变动、经济活动、通货膨胀或资金流动等。在股票和企业信用方面,价值因素可以追溯到格雷厄姆和多德在上世纪三十年代的《证券分析》中,后来被沃伦·巴菲特所著名化。股票价值方法将股票价格与基本指标(例如账面价值、销售额、净利润或各种现金流量指标)进行比较。

市值策略使用统计学或机器学习模型来识别由流动性供给不足引起的错价。统计套利和指数套利是突出的例子,捕捉短期时间范围内临时市场影响的回归(我们将在下一章中讨论配对交易)。在较长的时间范围内,市值交易还利用股票和商品的季节效应。

跨资产相对价值策略侧重于不同资产的相对错价。例如,可转换债券套利涉及对单个公司的股票、信用和波动性之间的相对价值进行交易。相对价值还包括信用和股票波动性之间的交易,利用信用信号交易股票或商品和股票之间的相对价值交易。

理性

存在价值效应的理性和行为学解释都有。我们将引用一些来自丰富研究的著名例子,并在 GitHub 存储库中列出更多参考资料。

在理性、高效的市场观点中,价值溢价是对较高的实际或感知风险的补偿。研究人员提出了证据,表明价值公司比精益和更灵活的成长公司在适应不利的经济环境方面的灵活性更小,或者价值股的风险与高财务杠杆和更不确定的未来收益有关。研究还表明,价值和小市值投资组合对宏观冲击的敏感性也比成长和大市值投资组合更高。

从行为学的角度来看,价值溢价可以通过损失规避和心理会计偏见来解释。投资者可能对具有强劲近期表现的资产的损失较少担心,因为之前收益提供了保障。这种损失规避偏见导致投资者认为股票的风险较以前较低,并以较低的利率贴现其未来现金流。相反,较差的近期表现可能导致投资者提高资产的贴现率。不同的回报预期导致了价值溢价,因为相对于基本面的高价格倍数的成长股在过去表现良好,但是在未来,投资者将要求较低的平均回报,因为他们对风险的偏见认知较低,而价值股则相反。

关键指标

从基本数据计算出了大量的估值代理。这些因素可以作为机器学习估值模型的输入,用于预测价格。在接下来的章节中,我们将看到一些这些因素在实践中如何被使用的例子:

因素 描述
现金流收益 该比率将每股运营现金流除以股价。较高的比率意味着股东获得更好的现金回报(如果通过股息或股票回购支付,或者利润投资于业务中)。
自由现金流收益率 该比率将每股自由现金流(反映必要费用和投资后可分配的现金金额)除以股价。较高且增长的自由现金流收益率通常被视为超额表现的信号。
投入资本的现金流回报率 (CFROIC) CFROIC 衡量公司的现金流盈利能力。它将经营现金流除以投入资本,定义为总债务加净资产。更高的回报意味着企业在给定的投入资本量下有更多现金,为股东创造更多价值。
现金流与总资产比 该比率将经营现金流除以总资产,表示公司相对于其资产可以生成多少现金,较高的比率类似于 CFROIC。
企业价值的自由现金流量 该比率衡量公司相对于其企业价值(以股权和债务的合计价值衡量)生成的自由现金流量。
EBITDA 与企业价值比 该比率衡量了公司的 EBITDA(利润前利息、税项、折旧和摊销),即相对于其企业价值的现金流量代理。
收益率(1 年滞后) 该比率将过去 12 个月的收益总和除以最后市场(收盘)价格。
收益率(1 年预测) 该比率将滚动 12 个月的分析师盈利预期除以最后价格,而不是实际历史盈利,其中共识是预测的(可能是加权的)平均值。
PEG 比率 价格/盈利增长(PEG)比率将一定时期内的股票价格盈利(P/E)比率除以公司的盈利增长率。该比率通过公司的盈利增长调整支付的价格(由 P/E 比率衡量)。
预测的 1 年前 PE 相对于行业的 预测的市盈率相对于相应的行业市盈率。它旨在通过考虑行业在估值上的差异来减轻通用市盈率的行业偏见。
销售收益率 该比率衡量股票的估值相对于其产生收入的能力。其他条件相等,具有较高历史销售价格比率的股票预计会表现出色。
销售收益率 FY1 前瞻性销售价格比率使用分析师销售预测,与(加权的)平均值结合。
帐面价值收益率 该比率将历史帐面价值除以股价。
股息收益率 当前年化股息除以最近收盘价。贴现现金流量估值假设公司的市值等于其未来现金流的现值。

波动性和规模因素

低波动因子捕捉到具有低于平均水平的波动率、贝塔或特有风险的股票的超额回报。具有较大市值的股票往往具有较低的波动性,因此传统的size因子通常与较新的波动性因子结合使用。

低波动性异常是一种与金融基本原理相矛盾的实证难题。资本资产定价模型CAPM)和其他资产定价模型断言,高风险应该获得更高的回报,但在许多市场和长时间内,相反情况发生,较低风险的资产表现优于其风险较高的同行。

理由

低波动性异常与有效市场假说和 CAPM 假设相矛盾。相反,已提出了几种行为解释。

彩票效应建立在个人承担类似彩票的投注的经验证据上,这些投注存在小额预期损失但可能有较大潜在赢利,即使这种大赢利可能性相当低。如果投资者认为低价格、波动性大的股票的风险-回报特征类似于彩票,那么它可能是一种有吸引力的投注。因此,投资者可能会为高波动性股票支付过高的价格,并由于其偏见偏好而为低波动性股票支付过低的价格。代表性偏差表明,投资者将一些广为人知的高波动性股票的成功推广到所有高波动性股票,而忽视了这些股票的投机性质。

投资者可能也过于自信地认为他们能够预测未来,对于风险更大、结果更不确定的波动性股票,他们的意见差异更大。由于通过持有资产而不是通过做空来表达积极观点更容易,即持有一个资产,乐观主义者可能会超过悲观主义者,继续推动波动性股票的价格上涨,导致较低的回报。

此外,投资者在牛市和危机期间的行为不同。在牛市期间,贝塔的离散度要低得多,因此低波动性股票甚至不会表现得差,而在危机期间,投资者寻求或保持低波动性股票,贝塔的离散度增加。因此,低波动性资产和投资组合在长期内表现更好。

关键指标

用于识别低波动性股票的度量覆盖了广泛的领域,其中一个端是实现的波动率(标准差),另一个端是预测(隐含)波动率和相关性。一些人将低波动性操作化为低贝塔。对于不同的度量,支持波动性异常的证据似乎是强有力的。

质量因素

质量因子旨在捕捉高盈利、运营高效、安全、稳定和良好治理的公司相对于市场的超额回报。市场似乎也会奖励相对盈利的确定性,并惩罚盈利波动性较高的股票。向高质量企业倾斜的投资组合长期以来一直被依靠基本分析的股票选手所提倡,但在量化投资中是一个相对较新的现象。主要挑战在于如何使用定量指标一致和客观地定义质量因子,考虑到质量的主观性。

基于独立质量因子的策略往往表现出逆周期性,因为投资者支付溢价以减小下行风险并推高估值。因此,质量因子经常与其他风险因子结合在一起,最常见的是与价值因子结合以产生合理价格的质量策略。长短期质量因子往往具有负市场β值,因为它们做多质量股票,这些股票也是低波动性的,同时做空更具波动性、低质量的股票。因此,质量因子通常与低波动性和动量因子呈正相关,与价值和广泛的市场暴露呈负相关。

理由

质量因子可能会暗示超额表现,因为持续的盈利能力、现金流量稳定增长、审慎的杠杆使用、对资本市场融资的低需求或长期内的低金融风险支撑了对股票的需求,并在长期内支撑了这些公司的股价。从公司财务的角度来看,质量公司通常会谨慎管理其资本,减少过度杠杆或过度资本化的风险

行为解释表明,投资者对质量信息存在反应不足,类似于动量交易的理由,投资者追逐赢家并卖出输家。另一个支持质量溢价的论点是类似于成长股的群体效应。基金经理可能会发现,即使一家公司的基本面强劲但价格昂贵,也比购买更具波动性(风险性)的价值股更容易被合理化。

关键指标

质量因子依赖于从资产负债表和利润表计算出的指标,这些指标表明了高利润或现金流边际、经营效率、财务实力和更广泛的竞争力,因为这意味着公司能够持续一段时间内保持盈利能力。

因此,质量常常通过毛利率(最近已添加到法玛—法朗奇因子模型中,参见第七章,线性模型)、投入资本回报率、低盈利波动性或各种盈利能力、盈利质量和杠杆指标的组合进行衡量,以下表格列出了一些选项。

盈余管理主要通过操纵应计项目来实施。因此,应计项目的大小通常被用作盈余质量的代理:相对于资产较高的总应计项目会使低盈余质量更有可能。然而,这并不一定清楚,因为应计项目既可以反映盈余操纵,也可以像未来业务增长的会计估计一样反映出来。

因子 描述
资产周转率 此因子衡量公司使用资产(需要资本)生产收入的效率,并通过将销售额除以总资产来计算;更高的周转率更好。
资产周转率 12 个月变化 此因子衡量管理层在过去一年中利用资产产生收入的效率变化。通常预计,效率改善水平最高的股票将表现优异。
流动比率 流动比率是衡量公司偿付短期债务能力的流动性指标。它将公司的流动资产与流动负债进行比较,从质量的角度来看,流动比率较高更好。
利息保障 此因子衡量公司支付债务利息的能力。它是通过将公司的息税前利润(EBIT)除以其利息支出来计算的。较高的比率是可取的。
杠杆 与股本相比,负债显著多于权益的公司被认为是高度杠杆的。负债权益比通常与前景呈负相关,杠杆越低越好。
分红比率 向股东支付的股息金额。分红比率较高的股票被分配到前十分位,而分红比率较低的股票则被分配到底十分位。
净资产收益率ROE 根据历史净资产收益率对股票进行排名,并将净资产收益率最高的股票分配到前十分位。

如何将数据转化为因子

基于对关键因子类别、它们的原理和流行指标的概念理解,一个关键任务是确定可能更好地捕捉之前所述的回报驱动因素所包含的风险的新因子,或者找到新因子。在任何情况下,比较创新因子与已知因子的表现将是重要的,以确定增量信号增益。

有用的 pandas 和 NumPy 方法

NumPy 和 pandas 是自定义因子计算的关键工具。数据目录中的 Notebook 00-data-prep.ipynb 包含了如何创建各种因子的示例。该笔记本使用由 GitHub 仓库根目录中的 data 文件夹中的 get_data.py 脚本生成并以 HDF5 格式存储的数据以提供更快的访问。请参阅 GitHub 仓库中的 Chapter 2 目录下的笔记本 storage_benchmarks.ipynb,以比较 pandas DataFramesparquetHDF5csv 存储格式。

以下示例说明了从原始股票数据计算选定因子的一些关键步骤。有关详细信息和我们在此处省略的可视化效果,请参见笔记本。

加载数据

我们加载了 Quandl 股价数据集,涵盖了 2000 年至 2018 年的美国股票市场,使用 pd.IndexSlicepd.MultiIndex 执行切片操作,选择调整后的收盘价,并将列进行转置,将 DataFrame 转换为宽格式,其中列中包含股票代码,行中包含时间戳:

idx = pd.IndexSlice
with pd.HDFStore('../../data/assets.h5') as store:
    prices = store['quandl/wiki/prices'].loc[idx['2000':'2018', :], 
                   'adj_close'].unstack('ticker')

prices.info()
DatetimeIndex: 4706 entries, 2000-01-03 to 2018-03-27
Columns: 3199 entries, A to ZUMZ

从日频率重新采样为月频率

为了减少训练时间并尝试针对更长时间范围的策略,我们使用可用的调整后的收盘价将商业日数据转换为月末频率:

monthly_prices = prices.resample('M').last()

计算动量因子

为了捕捉捕捉时间序列动态,例如动量模式,我们使用 pct_change(n_periods) 计算历史回报,即由 lags 指定的各种月期的回报。然后,我们使用 .stack() 将宽格式结果转换回长格式,使用 .pipe() 应用 .clip() 方法到结果的 DataFrame 并将回报截尾到 [1%, 99%] 的水平;即我们在这些百分位数上限制异常值。

最后,我们使用几何平均数对回报进行归一化处理。使用 .swaplevel() 更改 MultiIndex 索引级别的顺序后,我们获得了从 1 到 12 个月的六个周期的合并月回报:

outlier_cutoff = 0.01
data = pd.DataFrame()
lags = [1, 2, 3, 6, 9, 12]
for lag in lags:
    data[f'return_{lag}m'] = (monthly_prices
                           .pct_change(lag)
                           .stack()
                           .pipe(lambda x: x.clip(lower=x.quantile(outlier_cutoff),
                        upper=x.quantile(1-outlier_cutoff)))
                           .add(1)
                           .pow(1/lag)
                           .sub(1)
                           )
data = data.swaplevel().dropna()
data.info()

MultiIndex: 521806 entries, (A, 2001-01-31 00:00:00) to (ZUMZ, 2018-03-
                             31 00:00:00)
Data columns (total 6 columns):
return_1m 521806 non-null float64
return_2m 521806 non-null float64
return_3m 521806 non-null float64
return_6m 521806 non-null float64
return_9m 521806 non-null float64
return_12m 521806 non-null float6

我们可以使用这些结果来计算动量因子,这些因子基于较长周期的回报与最近一个月的回报之间的差异,以及基于312个月回报之间的差异,如下所示:

for lag in [2,3,6,9,12]:
    data[f'momentum_{lag}'] = data[f'return_{lag}m'].sub(data.return_1m)
data[f'momentum_3_12'] = data[f'return_12m'].sub(data.return_3m)

使用滞后回报和不同持有期

为了将滞后值用作当前观察到的输入变量或特征,我们使用 .shift() 方法将历史回报移至当前期间:

for t in range(1, 7):
    data[f'return_1m_t-{t}'] = data.groupby(level='ticker').return_1m.shift(t)

类似地,为了计算各种持有期的回报,我们使用之前计算的标准化期回报,并将其向后移动以与当前金融特征对齐:

for t in [1,2,3,6,12]:
    data[f'target_{t}m'] = data.groupby(level='ticker')[f'return_{t}m'].shift(-t)

计算因子贝塔系数

我们将在第八章 时间序列模型 中介绍 Fama—French 数据,使用线性回归估计资产对常见风险因子的暴露。已经通过实证研究表明,Fama—French 的五个因子,即市场风险、规模、价值、经营利润能力和投资,在解释资产回报方面是有效的,并且常用于评估投资组合的风险/回报特征。因此,在旨在预测未来回报的模型中,自然而然地包括过去的因子暴露作为金融特征。

我们可以使用 pandas-datareader 访问历史因子回报,并使用 pyfinance 库中的 PandasRollingOLS 滚动线性回归功能来估计历史暴露,具体如下:

factors = ['Mkt-RF', 'SMB', 'HML', 'RMW', 'CMA']
factor_data = web.DataReader('F-F_Research_Data_5_Factors_2x3', 
              'famafrench', start='2000')[0].drop('RF', axis=1)
factor_data.index = factor_data.index.to_timestamp()
factor_data = factor_data.resample('M').last().div(100)
factor_data.index.name = 'date'
factor_data = factor_data.join(data['return_1m']).sort_index()

T = 24
betas = (factor_data
         .groupby(level='ticker', group_keys=False)
         .apply(lambda x: PandasRollingOLS(window=min(T, x.shape[0]-1), y=x.return_1m, x=x.drop('return_1m', axis=1)).beta))

我们将在第七章中更详细地探讨 Fama—French 因子模型和线性回归,线性模型。请参阅笔记本以获取更多示例。

内置 Quantopian 因子

附带的笔记本factor_library.ipynb包含了许多示例因子,这些因子要么由 Quantopian 平台提供,要么从 Jupyter Notebook 使用的研究 API 可用的数据源计算而来。

有内置因子可以与量化 Python 库一起使用,特别是numpypandas,从各种相关数据源中导出更复杂的因子,例如美国股票价格、Morningstar 基本面和投资者情绪。

例如,销售收益率的倒数价格销售比是 Morningstar 基本面数据集的一部分。它可以作为一种管道的一部分,随着我们介绍zipline库而进一步描述。

TA-Lib

TA-Lib 库包括许多技术因素。Python 实现可用于本地使用,例如与ziplinealphalens,也可在 Quantopian 平台上使用。笔记本还演示了使用 TA-Lib 可用的几个技术指标。

寻找信号 - 如何使用 zipline

历史上,Alpha 因子使用单一输入和简单的启发式方法、阈值或分位数截止值来识别买入或卖出信号。ML 已被证明可以从更多样化和更大量的输入数据中提取信号,包括基于历史模式分析的其他 Alpha 因子。因此,算法交易策略今天利用了大量的 Alpha 信号,其中许多单独可能较弱,但在与 ML 算法通过其他模型驱动或传统因子相结合时可以产生可靠的预测。

开源zipline库是由众包量化投资基金 Quantopian(www.quantopian.com/)维护和用于生产中的事件驱动回测系统,以便促进算法开发和实时交易。它自动化算法对交易事件的反应,并为其提供当前和历史的点时间数据,避免了前瞻偏差。

您可以离线使用它与数据包一起研究和评估 alpha 因子。在 Quantopian 平台上使用它时,您将获得更广泛的基本和替代数据。我们还将在本章中演示 Quantopian 研究环境,并在下一章中演示回测 IDE。本节的代码位于此章节的 GitHub 存储库文件夹的01_factor_research_evaluation子目录中。

安装完成并在执行第一个算法之前,您需要摄入一个数据包,默认情况下由 Quandl 社区维护的数据,涵盖了 3000 家美国上市公司的股价、股息和拆股信息。您需要一个 Quandl API 密钥来运行下面存储数据的代码,该代码将数据存储在您的主目录下的 ~/.zipline/data/<bundle> 文件夹中:

$ QUANDL_API_KEY=<yourkey> zipline ingest [-b <bundle>]

架构 - 事件驱动交易模拟

zipline 算法会在初始设置后的指定期间内运行,并在特定事件发生时执行其交易逻辑。这些事件由交易频率驱动,也可以由算法进行调度,并导致 zipline 调用某些方法。算法通过一个 context 字典维护状态,并通过包含 PIT(point-in-time) 当前和历史数据的 data 变量接收可执行的信息。如果有任何交易,算法将返回一个包含组合绩效指标的 DataFrame,以及可以用于记录因子值等用户定义的指标。

您可以通过命令行、Jupyter Notebook 和使用 run_algorithm() 函数来执行算法。

算法需要一个 initialize() 方法,在模拟开始时调用一次。该方法可用于向所有其他算法方法可用的 context 字典中添加属性,或注册执行更复杂数据处理的 pipelines,例如基于 alpha 因子逻辑对证券进行过滤。

算法执行通过可选方法进行,这些方法可以由 zipline 自动调度或在用户定义的间隔内调用。方法 before_trading_start() 在市场开盘前每日调用,主要用于识别算法可能在当天交易的一组证券。方法 handle_data() 每分钟调用一次。

Pipeline API 便于从历史数据中为一组证券定义和计算 alpha 因子。Pipeline 定义了在一张表中为一组证券生成 PIT 值的计算。它需要使用 initialize() 方法进行注册,然后可以按照自动或自定义的时间表执行。该库提供了许多内置的计算方法,如移动平均线或布林带,可以快速计算标准因子,同时也允许创建自定义因子,下面我们将进行说明。

最重要的是,Pipeline API 使得 alpha 因子研究模块化,因为它将 alpha 因子计算与算法的其余部分(包括交易订单的下达和执行、以及组合持仓、价值等的簿记)分开。

市场数据中的单一 alpha 因子

我们首先将在离线环境中说明 zipline alpha 因子研究工作流程。特别是,我们将开发和测试一个简单的均值回归因子,该因子衡量了最近表现与历史平均水平的偏离程度。短期反转是一种常见策略,利用了股价上涨很可能会在从少于一分钟到一个月的时间跨度内回归到均值的弱预测模式。有关详细信息,请参阅 Notebook single_factor_zipline.ipynby

为此,该因子计算了相对于过去一年的滚动月收益率的最后一个月收益率的 z 得分。此时,我们不会下达任何订单,只是为了简单说明如何实现 CustomFactor 并在模拟过程中记录结果。

在一些基本设置之后,MeanReversionCustomFactor 的子类,并定义了一个 compute() 方法。它创建了默认输入的月度收益率,其默认窗口也是一年,因此 monthly_return 变量将在给定日的 Quandl 数据集中有 252 行,每个证券一个列。

compute_factors() 方法创建了一个 MeanReversion 因子实例,并创建了 longshortranking 流水线列。前两个包含布尔值,可以用来下订单,而后者反映了整体排名,以评估整体因子表现。此外,它使用内置的 AverageDollarVolume 因子来限制计算范围,以更流动的股票:

from zipline.api import attach_pipeline, pipeline_output, record
from zipline.pipeline import Pipeline, CustomFactor
from zipline.pipeline.factors import Returns, AverageDollarVolume
from zipline import run_algorithm

MONTH, YEAR = 21, 252
N_LONGS = N_SHORTS = 25
VOL_SCREEN = 1000

class MeanReversion(CustomFactor):
    """Compute ratio of latest monthly return to 12m average,
       normalized by std dev of monthly returns"""
    inputs = [Returns(window_length=MONTH)]
    window_length = YEAR

    def compute(self, today, assets, out, monthly_returns):
        df = pd.DataFrame(monthly_returns)
        out[:] = df.iloc[-1].sub(df.mean()).div(df.std())

def compute_factors():
    """Create factor pipeline incl. mean reversion,
        filtered by 30d Dollar Volume; capture factor ranks"""
    mean_reversion = MeanReversion()
    dollar_volume = AverageDollarVolume(window_length=30)
    return Pipeline(columns={'longs'  : mean_reversion.bottom(N_LONGS),
                             'shorts' : mean_reversion.top(N_SHORTS),
                             'ranking': 
                          mean_reversion.rank(ascending=False)},
                          screen=dollar_volume.top(VOL_SCREEN))

这一结果将使我们能够下达多空订单。我们将在下一章看到如何通过选择重新平衡周期和根据新信号调整投资组合持仓来构建投资组合。

initialize() 方法注册了 compute_factors() 流水线,而 before_trading_start() 方法确保该流水线每天都会运行一次。record() 函数将流水线的 ranking 列以及当前资产价格添加到 run_algorithm() 函数返回的性能 DataFrame 中:

def initialize(context):
    """Setup: register pipeline, schedule rebalancing,
        and set trading params"""
    attach_pipeline(compute_factors(), 'factor_pipeline')

def before_trading_start(context, data):
    """Run factor pipeline"""
    context.factor_data = pipeline_output('factor_pipeline')
    record(factor_data=context.factor_data.ranking)
    assets = context.factor_data.index
    record(prices=data.current(assets, 'price'))

最后,以 UTC 时间定义开始和结束 Timestamp 对象,设置资本基础,并使用对关键执行方法的引用执行 run_algorithm()performance DataFrame 包含嵌套数据,例如,prices 列由每个单元格的 pd.Series 组成。因此,当以 pickle 格式存储时,后续数据访问更容易:

start, end = pd.Timestamp('2015-01-01', tz='UTC'), pd.Timestamp('2018-
             01-01', tz='UTC')
capital_base = 1e7

performance = run_algorithm(start=start,
                            end=end,
                            initialize=initialize,
                            before_trading_start=before_trading_start,
                            capital_base=capital_base)

performance.to_pickle('single_factor.pickle')

我们将在下一节使用存储在 performance DataFrame 中的因子和定价数据,评估不同持有期的因子表现,但首先,我们将看一下如何通过组合 Quantopian 平台上多样化的数据源中的几个 alpha 因子来创建更复杂的信号。

结合来自多样化数据源的因子

Quantopian 研究环境专门用于快速测试预测性 Alpha 因子。该过程非常相似,因为它基于 zipline,但提供了更丰富的数据源访问。下面的代码示例说明了如何计算 Alpha 因子,不仅来自以前的市场数据,还来自基本和替代数据。有关详细信息,请参阅笔记本 multiple_factors_quantopian_research.ipynb

Quantopian 免费提供了几百个 MorningStar 基本变量,还包括 stocktwits 信号作为替代数据源的示例。还有自定义的宇宙定义,例如 QTradableStocksUS,它应用了几个过滤器来限制回测宇宙,使其仅包含可能在现实市场条件下可交易的股票:

from quantopian.research import run_pipeline
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data.morningstar import income_statement, 
     operation_ratios, balance_sheet
from quantopian.pipeline.data.psychsignal import stocktwits
from quantopian.pipeline.factors import CustomFactor, 
     SimpleMovingAverage, Returns
from quantopian.pipeline.filters import QTradableStocksUS

我们将使用自定义的 AggregateFundamentals 类来使用最后报告的基本数据点。这旨在解决基本面每季度报告一次的事实,并且 Quantopian 目前没有提供一种轻松的方法来聚合历史数据,比如按照滚动基础获取最后四个季度的总和:

class AggregateFundamentals(CustomFactor):
    def compute(self, today, assets, out, inputs):
        out[:] = inputs[0]

我们将再次使用上述代码中的自定义 MeanReversion 因子。我们还将使用 rank() 方法的 mask 参数为给定的宇宙定义计算几个其他因子:

def compute_factors():
    universe = QTradableStocksUS()

    profitability = (AggregateFundamentals(inputs=
                     [income_statement.gross_profit],
                                           window_length=YEAR) /
                     balance_sheet.total_assets.latest).rank(mask=universe)

    roic = operation_ratios.roic.latest.rank(mask=universe)
    ebitda_yield = (AggregateFundamentals(inputs=
                             [income_statement.ebitda],
                                          window_length=YEAR) /
                    USEquityPricing.close.latest).rank(mask=universe)
    mean_reversion = MeanReversion().rank(mask=universe)
    price_momentum = Returns(window_length=QTR).rank(mask=universe)
    sentiment = SimpleMovingAverage(inputs=
                            [stocktwits.bull_minus_bear],

                            window_length=5).rank(mask=universe)

    factor = profitability + roic + ebitda_yield + mean_reversion + 
             price_momentum + sentiment

    return Pipeline(
            columns={'Profitability'      : profitability,
                     'ROIC'               : roic,
                     'EBITDA Yield'       : ebitda_yield,
                     "Mean Reversion (1M)": mean_reversion,
                     'Sentiment'          : sentiment,
                     "Price Momentum (3M)": price_momentum,
                     'Alpha Factor'       : factor})

该算法使用了一种简单的方法来结合六个个体因子,只需为这些因子的每个因子的资产排名简单相加。我们不想使用等权重,而是希望考虑相对重要性和预测未来收益的增量信息。下一章的机器学习算法将允许我们完全这样做,使用相同的回测框架。

执行也依赖于 run_algorithm(),但 Quantopian 平台上的返回 DataFrame 只包含由 Pipeline 创建的因子值。这很方便,因为此数据格式可用作 alphalens 的输入,这是用于评估 Alpha 因子预测性能的库。

分离信号和噪音 - 如何使用 alphalens

Quantopian 开源了 Python 库 alphalens,用于预测股票因子的性能分析,与我们将在下一章中探讨的回测库 zipline 和投资组合绩效与风险分析库 pyfolio 集成得很好。

alphalens 便于分析 Alpha 因子的预测能力,涉及:

  • 信号与随后收益的相关性

  • 基于信号的等权或因子加权组合的盈利能力(基于信号的子集)

  • 因子周转率以指示潜在交易成本

  • 特定事件期间的因子表现

  • 通过行业细分的前述分解

分析可以使用泪水图或单独的计算和绘图进行。在线存储库中有泪水图以节省一些空间。

创建前瞻性收益和因子分位数

要使用alphalens,我们需要为资产的一组信号提供信号,例如MeanReversion因子的排名,以及投资于给定持有期的资产的前向回报。有关详细信息,请参阅笔记本03_performance_eval_alphalens.ipynb

我们将从single_factor.pickle文件中恢复prices(相应的factor_data)如下:

performance = pd.read_pickle('single_factor.pickle')

prices = pd.concat([df.to_frame(d) for d, df in performance.prices.items()],axis=1).T
prices.columns = [re.findall(r"\[(.+)\]", str(col))[0] for col in 
                  prices.columns]
prices.index = prices.index.normalize()
prices.info()

<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 755 entries, 2015-01-02 to 2017-12-29
Columns: 1661 entries, A to ZTS
dtypes: float64(1661)

GitHub 存储库中的alpha factor evaluation笔记本详细介绍了如何以特定于行业的方式进行评估。

我们可以使用get_clean_factor_and_forward_returns实用程序函数创建所需格式的alphalens输入数据,该函数还返回给定持有期的信号分位数和前向回报:

HOLDING_PERIODS = (5, 10, 21, 42)
QUANTILES = 5
alphalens_data = get_clean_factor_and_forward_returns(factor=factor_data,
                                     prices=prices,
                                     periods=HOLDING_PERIODS,
                                     quantiles=QUANTILES)

Dropped 14.5% entries from factor data: 14.5% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions). max_loss is 35.0%, not exceeded: OK!

alphalens_data数据框包含给定资产在给定日期的投资的回报,以及因子值,即该日期上资产的MeanReversion排名,以及相应的分位数值:

日期 资产 5 天 10 天 21 天 42 天 因子 因子分位数
01/02/15 A 0.07% -5.70% -2.32% 4.09% 2618 4
AAL -3.51% -7.61% -11.89% -10.23% 1088 2
AAP 1.10% -5.40% -0.94% -3.81% 791 1
AAPL 2.45% -3.05% 8.52% 15.62% 2917 5
ABBV -0.17% -2.05% -6.43% -13.70% 2952 5

对于评估信号的预测能力,前向回报和信号分位数是基础。通常,一个因子应该对不同分位数的回报产生明显不同的结果,例如对于因子值的底部五分位数应该产生负回报,而对于顶部分位数应该产生正回报。

因子分位数的预测性能

首先,我们想要可视化因子分位数的平均期间回报。我们可以使用performance模块中的内置函数mean_return_by_quantileplotting模块中的plot_quantile_returns_bar

from alphalens.performance import mean_return_by_quantile
from alphalens.plotting import plot_quantile_returns_bar
mean_return_by_q, std_err = mean_return_by_quantile(alphalens_data)
plot_quantile_returns_bar(mean_return_by_q);

结果是一张条形图,根据因子信号的分位数,将四种不同持有期的前向回报均值进行了分解。正如您所见,底部五分位数产生的结果明显更为负面,而顶部五分位数除了最长持有期外产生的结果更为正面:

对于第一和第四分位数,10 天的持有期提供了稍微更好的结果。我们还想看到由每个信号分位数驱动的投资随时间的表现。我们将计算每日收益率,而不是 5 天持有期的平均收益率,alphalens将调整期间收益率以解决每日信号和较长持有期之间的不匹配(有关详细信息,请参阅文档):

from alphalens.plotting import plot_cumulative_returns_by_quantile
mean_return_by_q_daily, std_err =     
     mean_return_by_quantile(alphalens_data, by_date=True)
plot_cumulative_returns_by_quantile(mean_return_by_q_daily['5D'], 
     period='5D');

结果线图显示,在这三年的大部分时间里,前两个五分位显着优于后两个五分位。然而,如前图所示,第四个五分位的信号产生的性能优于第一个五分位的性能:

对于交易策略有用的因子显示出前述模式,其中累积收益沿着明显不同的路径发展,因为这允许具有更低资本要求和相应更低整体市场暴露的多空策略。

然而,我们还需要考虑期间收益的分散而不仅仅是平均值。为此,我们可以依靠内置的plot_quantile_returns_violin

from alphalens.plotting import plot_quantile_returns_violin
plot_quantile_returns_violin(mean_return_by_q_daily);

这个分布图突出显示每日收益的范围相当广泛,并且尽管均值不同,但分布之间的分离非常有限,因此,在任何给定的日子里,不同五分位之间的性能差异可能相当有限:

虽然我们专注于单一 alpha 因子的评估,但我们在忽略与交易执行相关的实际问题时,会简化问题,我们将在下一章中详细介绍适当的回测时再放松这些问题。其中一些包括:

  • 交易成本

  • 滑点,即决策价格与交易执行价格之间的差异,例如,由于市场影响引起的

信息系数

本书大部分内容都关于使用 ML 模型设计 alpha 因子。ML 是关于优化某些预测目标的,本节中,我们将介绍用于衡量 alpha 因子性能的关键指标。我们将定义 alpha 为超过基准的平均收益。

这导致了信息比率IR),它通过将 alpha 除以跟踪风险来衡量每单位风险带来的平均超额收益。当基准是无风险利率时,IR 对应于众所周知的夏普比率,并且我们将强调在典型情况下收益不服从正态分布时出现的关键统计测量问题。我们还将解释主动管理的基本法则,将 IR 分解为预测技巧的组合和策略有效利用预测技巧的能力。

alpha 因子的目标是准确地预测未来收益的方向。因此,一个自然的性能度量是 alpha 因子预测与目标资产未来收益之间的相关性。

更好地使用非参数的 Spearman 等级相关系数来衡量两个变量之间关系的好坏,该系数衡量使用单调函数描述关系的能力,与测量线性关系强度的 Pearson 相关系数相对。

我们可以使用 alphalens 获取信息系数,alphalens 底层依赖于 scipy.stats.spearmanr(请参阅存储库以获取有关如何直接使用 scipy 获取 p 值的示例)。factor_information_coefficient 函数计算周期性的相关性,plot_ic_ts 创建一个带有一个月移动平均线的时间序列图:

from alphalens.performance import factor_information_coefficient
from alphalens.plotting import plot_ic_ts
ic = factor_information_coefficient(alphalens_data)
plot_ic_ts(ic[['5D']])

这个时间序列图显示了具有显著正移动平均 IC 的延长时期。如果有足够的机会应用这种预测技能,即使 IC 为 0.05 或甚至 0.1,也可以实现显著的超额收益,因为主动管理的基本定律将说明:

年均 IC 的图表突出显示了因子的性能历史上的不均匀性:

ic = factor_information_coefficient(alphalens_data)
ic_by_year = ic.resample('A').mean()
ic_by_year.index = ic_by_year.index.year
ic_by_year.plot.bar(figsize=(14, 6))

这会产生以下图表:

如本例中所示,信息系数低于 0.05 的情况下,虽然低但显著,可以相对于基准产生正的残差收益,我们将在下一节中看到。create_summary_tear_sheet(alphalens_data) 创建 IC 摘要统计信息,其中风险调整 IC 的结果是将平均 IC 除以 IC 的标准差得到的,这也是通过使用 scipy.stats.ttest_1sampIC = 0 的双侧 t 检验进行的:

5 天 10 天 21 天 42 天
IC 平均值 0.01 0.02 0.01 0.00
IC 标准差 0.14 0.13 0.12 0.12
风险调整 IC 0.10 0.13 0.10 0.01
2.68 3.53 2.53 0.14
p-value(IC) 0.01 0.00 0.01 0.89
IC 偏度 0.41 0.22 0.19 0.21
IC 峰度 0.18 -0.33 -0.42 -0.27

因子周转率

因子周转率衡量与给定分位数相关联的资产经常变化的频率,即调整投资组合以适应信号序列需要多少交易。更具体地说,它衡量了当前处于因子分位数中的资产份额,在上个期间不处于该分位数的资产份额。以下表格是通过此命令生成的:

create_turnover_tear_sheet(alphalens_data)

加入基于五分位数的投资组合的资产份额相当高,表明交易成本对利用预测性能构建收益的挑战:

平均周转率 5 天 10 天 21 天 42 天
分位数 1 59% 83% 83% 41%
分位数 2 74% 80% 81% 65%
分位数 3 76% 80% 81% 68%
分位数 4 74% 81% 81% 64%
分位数 5 57% 81% 81% 39%

对因子周转率的另一种观点是资产排名与因子之间的相关性在各种持有期间的情况,也是撕裂单的一部分:

5 天 10 天 21 天 42 天
平均因子排名自相关 0.711 0.452 -0.031 -0.013

一般来说,更稳定性更好,以保持交易成本可控。

Alpha 因子资源

研究过程需要设计和选择与其信号预测能力相关的阿尔法因子。算法交易策略通常会基于多个为每个资产发送信号的阿尔法因子。这些因子可以使用 ML 模型进行聚合,以优化各种信号如何转化为关于个别仓位的时间和大小决策,我们将在后续章节中看到。

替代算法交易库

其他用于算法交易和数据收集的开源 Python 库包括(在 GitHub 上查看链接):

  • QuantConnect 是 Quantopian 的竞争对手。

  • WorldQuant 提供在线竞赛,并招募社区贡献者加入众包对冲基金。

  • Alpha Trading Labs 提供以 Quantopian 类似的商业模式为基础的高频重点测试基础设施。

  • Python 算法交易库(PyAlgoTrade)专注于回测,并支持模拟交易和实时交易。它允许您使用历史数据评估交易策略的想法,并力图以最小的努力完成此任务。

  • pybacktest 是一个使用 pandas 的矢量化回测框架,旨在简洁、简单和快速(该项目目前暂停)。

  • ultrafinance 是一个较旧的项目,结合了实时金融数据收集、分析和交易策略的回测。

  • 使用 Python 进行交易提供了量化交易课程以及一系列用于量化交易的函数和类。

  • 交互经纪商提供了一个用于在其平台上进行实时交易的 Python API。

总结

在本章中,我们介绍了 zipline 库用于事件驱动的交易算法模拟,既可以离线进行,也可以在 Quantopian 在线平台上进行。我们已经说明了从市场、基本和替代数据中设计和评估单个阿尔法因子以推导出用于算法交易策略的信号,并展示了结合多个因子的一种简单方法。我们还介绍了允许对信号的预测性能和交易换手率进行全面评估的 alphalens 库。

投资组合构建过程反过来采用更广泛的视角,旨在从风险和回报的角度来看最佳仓位大小。我们现在将转向平衡投资组合过程中风险和回报的各种策略。我们还将更详细地探讨在有限历史数据集上进行回测交易策略的挑战以及如何解决这些挑战。

第五章:策略评估

Alpha 因子驱动一种算法策略,该策略转化为交易,进而产生一个投资组合。由产生的投资组合的回报和风险决定了策略的成功。测试策略需要模拟由算法生成的投资组合,以验证其在市场条件下的表现。策略评估包括针对历史数据进行回测以优化策略参数,并进行前向测试以验证样本内性能与新的、样本外数据,并避免根据特定过去情况定制策略而产生的错误发现。

在投资组合背景下,正的资产回报可以以非线性方式抵消负的价格波动,使得投资组合回报的整体变化小于投资组合头寸变化的加权平均值,除非它们的回报完全且正相关。哈里·马科维茨于 1952 年发展了现代投资组合管理理论,该理论基于多元化,从而产生了均值-方差优化:对于给定的资产组合,可以优化投资组合权重以减少风险,该风险以给定预期回报水平的回报标准差来衡量。

资本资产定价模型CAPM)将风险溢价引入为持有资产的均衡报酬,以补偿对单一风险因子——市场的暴露,该风险无法分散。随着额外的风险因子和更细粒度的暴露选择的出现,风险管理已经发展得更加复杂。凯利规则是一种流行的动态投资组合优化方法,即在一段时间内选择一系列持仓;它已经被 1968 年的爱德华·索普从其原始应用于赌博的形式成功地改编为股票市场。

因此,有几种优化投资组合的方法,包括将机器学习ML)应用于学习资产之间的层次关系,并将其持有视为相互补充或替代品,以便于投资组合风险配置。

在本章中,我们将涵盖以下主题:

  • 如何基于 Alpha 因子构建和测试投资组合使用zipline

  • 如何衡量投资组合的风险和回报

  • 如何使用pyfolio评估投资组合绩效

  • 如何使用均值-方差优化和替代方法管理投资组合权重

  • 如何使用机器学习在投资组合背景下优化资产配置

本章的代码示例位于配套 GitHub 存储库的05_strategy_evaluation_and_portfolio_management目录中。

如何使用 zipline 构建和测试投资组合

在上一章中,我们介绍了 zipline 来模拟从跨时间序列市场、基本面和替代数据中计算 alpha 因子。现在我们将利用 alpha 因子来推导并执行买入和卖出信号。我们将延迟优化投资组合权重到本章稍后,并且现在,只将相同价值的仓位分配给每个持仓。本节代码在 01_trading_zipline 子目录中。

计划交易和投资组合再平衡

我们将使用上一章开发的自定义 MeanReversion 因子,可以在 alpha_factor_zipline_with_trades.py 中查看其实现。

compute_factors() 方法创建的 Pipeline 返回一个表格,其中包含最后一个月回报与年均值的标准差归一化之间差异最大的 25 支股票的长列和短列。它还将股票范围限制在过去 30 个交易日平均交易量最高的 500 支股票上。before_trading_start() 确保每天执行管道并记录结果,包括当前价格。

新的 rebalance() 方法向 exec_trades() 方法提交了交易订单,这些订单由管道标记为长头寸和短头寸的资产,权重相等。它还清除了不再包含在因子信号中的任何当前持仓:

def exec_trades(data, assets, target_percent):
    """Place orders for assets using target portfolio percentage"""
    for asset in assets:
        if data.can_trade(asset) and not get_open_orders(asset):
            order_target_percent(asset, target_percent)

def rebalance(context, data):
    """Compute long, short and obsolete holdings; place trade orders"""
    factor_data = context.factor_data
    assets = factor_data.index

    longs = assets[factor_data.longs]
    shorts = assets[factor_data.shorts]
    divest = context.portfolio.positions.keys() - longs.union(shorts)

    exec_trades(data, assets=divest, target_percent=0)
    exec_trades(data, assets=longs, target_percent=1 / N_LONGS)
    exec_trades(data, assets=shorts, target_percent=-1 / N_SHORTS)

rebalance() 方法根据一周开始时由 schedule_function() 实用程序设置的 date_rulestime_rules 运行,在 market_open 之后立即运行,根据内置的 US_EQUITIES 日历规定(有关规则的详细信息,请参阅文档)。您还可以指定交易佣金,既以相对比例,也以最低金额。还有一个定义滑点的选项,滑点是交易决策和执行之间价格不利变化的成本:

def initialize(context):
    """Setup: register pipeline, schedule rebalancing,
        and set trading params"""
    attach_pipeline(compute_factors(), 'factor_pipeline')
    schedule_function(rebalance,
                      date_rules.week_start(),
                      time_rules.market_open(),
                      calendar=calendars.US_EQUITIES)

    set_commission(us_equities=commission.PerShare(cost=0.00075, min_trade_cost=.01))
    set_slippage(us_equities=slippage.VolumeShareSlippage(volume_limit=0.0025, price_impact=0.01))

算法在调用 run_algorithm() 函数后继续执行,并返回相同的回测性能 DataFrame。现在我们将转向常见的投资组合回报和风险度量,以及如何使用 pyfolio 库计算它们。

如何使用 pyfolio 衡量绩效

机器学习是关于优化目标函数的。在算法交易中,目标是整体投资组合的回报和风险,通常相对于一个基准(可能是现金或无风险利率)。

有几个指标可以评估这些目标。我们将简要回顾最常用的指标以及如何使用 pyfolio 库计算它们,该库也被 zipline 和 Quantopian 使用。我们还将回顾如何在 Quantopian 上应用这些指标来测试算法交易策略。

我们将使用一些简单的符号:R 表示一个周期简单组合回报的时间序列,R=(r[1], ..., r[T]),从日期 1 到 T,*R^f =(r^f[1], ..., r^f[T])** 为相匹配的无风险利率的时间序列,使得 R([e])=R-R([f]) =(r[1]-r^f[1],..., r[T]-r^f[T]) 是超额回报。

夏普比率

Ex-ante 夏普比率SR)比较了投资组合预期超额回报与该超额回报的波动性之间的关系,其波动性由其标准差来衡量。它衡量了单位风险带来的平均超额回报:

预期回报和波动性不可观测,但可以通过历史数据进行如下估计:

除非无风险利率波动(如新兴市场),否则超额回报和原始回报的标准差将相似。当夏普比率与除无风险利率以外的基准一起使用时,例如标准普尔 500 指数,称为信息比率。在这种情况下,它衡量了投资组合的超额回报,也称为阿尔法,相对于跟踪误差,即投资组合回报与基准回报的偏差。

对于独立同分布iid)的回报,夏普比率估计量的分布的推导用于统计显著性检验,是根据大样本统计理论将 μ̂ 和 σ̂² 应用于中心极限定理的结果。

然而,金融回报往往违反 iid 假设。安德鲁·罗(Andrew Lo)已推导出对于平稳但具有自相关的回报,需要调整分布和时间聚合的必要调整。这很重要,因为投资策略的时间序列特性(例如,均值回归、动量和其他形式的串行相关)对于夏普比率估计本身可能有非常重要的影响,特别是当将夏普比率从高频数据年化时(Lo 2002)。

主动管理的基本定律

信息比率IR)意味着相对于额外承担的风险而言表现优异。主动管理的基本定律将 IR 分解为信息系数IC)作为预测技能的衡量标准,以及运用这种技能进行独立投注的能力。它总结了频繁参与(高广度)和出色表现(高 IC)的重要性:

IC 衡量了 alpha 因子与其信号产生的前向回报之间的相关性,并捕捉了经理的预测技能的准确性。策略的广度通过投资者在给定时间段内进行的独立投注数量来衡量,两个值的乘积与 IR 成正比,也称为评估风险(Treynor and Black)。

此框架已扩展以包括转移系数(TC),以反映可能限制信息比率低于其他情况下可实现水平的投资组合约束(例如,对卖空的限制)。 TC 代表了管理者将见解转化为投资组合投注的效率(Clarke 等人,2002 年)。

基本定律很重要,因为它突出了超额表现的关键驱动因素:准确的预测和能够进行独立预测并据此采取行动都很重要。在实践中,具有广泛投资决策集合的管理者可以在信息系数在 0.05 到 0.15 之间时获得显著的风险调整超额回报(如果有空间,可能包括模拟图表)。

在实践中,由于预测之间的横截面和时间序列相关性,估算策略的广度是困难的。

使用 pyfolio 进行样本内外表现分析

Pyfolio 通过许多标准指标方便地进行样本内外投资组合表现和风险分析。 它生成涵盖收益、头寸和交易分析的泪水图,以及在市场压力时期使用几种内置场景的事件风险分析,并包括贝叶斯样本外表现分析。

它依赖于投资组合收益和头寸数据,并且还可以考虑交易活动的交易成本和滑点损失。这些指标是使用可以独立使用的 empyrical 库计算的。

zipline 回测引擎生成的性能 DataFrame 可以转换为所需的 pyfolio 输入。

从 alphalens 获取 pyfolio 输入数据

然而,pyfolio 也可以直接与 alphalens 集成,并允许使用 create_pyfolio_input 创建 pyfolio 输入数据:

from alphalens.performance import create_pyfolio_input

qmin, qmax = factor_data.factor_quantile.min(), 
             factor_data.factor_quantile.max()
input_data = create_pyfolio_input(alphalens_data,   
                                  period='1D',
                                  capital=100000,
                                  long_short=False,
                                  equal_weight=False,
                                  quantiles=[1, 5],
                                  benchmark_period='1D')
returns, positions, benchmark = input_data

有两种选项可以指定如何生成投资组合权重:

  • long_short: 如果为 False,则权重将对应于因子值除以其绝对值,以便负因子值生成空头。 如果为 True,则首先对因子值进行均值化,以使多头和空头相互抵消,投资组合是市场中性的。

  • equal_weight: 如果为 True,且 long_shortTrue,则资产将被分成两个相等大小的组,前半部分组成多头头寸,后半部分组成空头头寸。

如果 factor_data 包含每个资产的行业信息,则还可以为组创建多头-空头投资组合。

从 zipline 回测获取 pyfolio 输入数据

通过 extract_rets_pos_txn_from_zipline 可将 zipline 回测结果转换为所需的 pyfolio 输入:

returns, positions, transactions = 
         extract_rets_pos_txn_from_zipline(backtest)

步进式测试样本外收

测试交易策略涉及对历史数据进行回测以微调 alpha 因子参数,以及对新市场数据进行前向测试以验证策略在样本外的表现良好或参数是否过于针对特定历史情况。

Pyfolio 允许指定一个样本外期间来模拟前向测试。在测试策略以获得统计上可靠的结果时,有许多方面需要考虑,我们将在这里讨论。

plot_rolling_returns 函数显示累计样本内和样本外回报与用户定义的基准(我们使用标准普尔 500 指数)的对比:

from pyfolio.plotting import plot_rolling_returns
plot_rolling_returns(returns=returns,
                     factor_returns=benchmark_rets,
                     live_start_date='2017-01-01',
                     cone_std=(1.0, 1.5, 2.0))

图中包含一个锥形,显示扩展的置信区间,以指示基于随机游走假设的情况下不太可能出现的样本外收益。在模拟的 2017 年样本外期间,我们的策略表现不佳:

总体表现统计

pyfolio 提供了几个分析函数和图表。perf_stats 摘要显示了年度和累计回报、波动率、偏度和回报的峰度以及 SR。以下附加指标(也可以单独计算)最重要:

  • 最大回撤: 从先前峰值的最高百分比损失

  • Calmar 比率: 年度组合回报相对于最大回撤

  • Omega 比率: 针对回报目标的概率加权收益与损失的比率,默认为零

  • Sortino 比率: 相对于下行标准差的超额回报

  • 尾部比率: 右尾(收益,第 95 百分位数的绝对值)与左尾(损失,第 5 百分位数的绝对值)的比率

  • 每日风险价值 (VaR): 对应于每日平均值以下两个标准偏差的损失

  • Alpha: 由基准回报未解释的投资组合回报

  • 贝塔: 对基准的暴露

from pyfolio.timeseries import perf_stats
perf_stats(returns=returns, 
           factor_returns=benchmark_rets, 
           positions=positions, 
           transactions=transactions)

对于从 MeanReversion 因子派生的模拟的多空投资组合,我们获得以下性能统计数据:

指标 全部 样本内 样本外 指标 全部 样本内 样本外
年度回报率 1.80% 0.60% 4.20% 偏度 0.34 0.40 0.09
累计回报 5.40% 1.10% 4.20% 峰度 3.70 3.37 2.59
年度波动率 5.80% 6.30% 4.60% 尾部比率 0.91 0.88 1.03
夏普比率 0.33 0.12 0.92 每日风险价值 -0.7% -0.8% -0.6%
Calmar 比率 0.17 0.06 1.28 总杠杆 0.38 0.37 0.42
稳定性 0.49 0.04 0.75 每日换手率 4.70% 4.40% 5.10%
最大回撤 -10.10% -10.10% -3.30% Alpha 0.01 0.00 0.04
Omega 比率 1.06 1.02 1.18 贝塔 0.15 0.16 0.03
Sortino 比率 0.48 0.18 1.37

有关计算和解释投资组合风险和回报指标的详细信息,请参见附录。

回撤期和因子暴露

plot_drawdown_periods(returns)函数绘制了投资组合的主要回撤期,而其他几个绘图函数显示了滚动夏普比率和滚动因子暴露度,包括对市场贝塔或法玛-法国大小、增长和动量因子的暴露度:

fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))
axes = ax.flatten()

plot_drawdown_periods(returns=returns, ax=axes[0])
plot_rolling_beta(returns=returns, factor_returns=benchmark_rets, 
                  ax=axes[1])
plot_drawdown_underwater(returns=returns, ax=axes[2])
plot_rolling_sharpe(returns=returns)

此图突出显示了各种撕裂表中包含的可视化子集,说明了 pyfolio 如何让我们深入了解绩效特征和对风险和收益基本驱动因素的暴露:

建模事件风险

Pyfolio 还包括各种事件的时间线,您可以使用它来比较投资组合在此期间与基准的表现,例如,在 2015 年秋天的英国脱欧公投后的抛售期间:

interesting_times = extract_interesting_date_ranges(returns=returns)
interesting_times['Fall2015'].to_frame('pf') \
 .join(benchmark_rets) \
 .add(1).cumprod().sub(1) \
 .plot(lw=2, figsize=(14, 6), title='Post-Brexit Turmoil')

结果绘图如下所示:

如何避免回测的陷阱

回测是使用历史数据模拟算法策略,目的是识别适用于新市场条件的模式。除了在不断变化的市场中预测不确定未来的一般挑战外,许多因素使得将样本内表现误认为发现真实模式变得非常可能。这些因素包括数据的方面、策略模拟的实施以及与统计测试及其解释相关的缺陷。使用更多计算资源、更大数据集和更复杂算法的风险使在噪声中发现明显模式的可能性增加。

我们将列出最严重和最常见的方法论错误,并参考多重测试的文献以获取更多详细信息。我们还将介绍通货紧缩夏普比率,以说明在分析中使用相同一组财务数据时如何调整重复试验导致的指标。

数据挑战

由于数据问题对回测有效性的挑战包括前瞻性偏差、生存偏差和异常值控制。

前瞻性偏差

从过去数据导出的交易规则的测试将在用于制定规则的样本数据中包含了实际上在数据所指代的时间点上并不可用或已知的信息时产生偏见结果。

此偏差的典型来源是未考虑报告的财务数据的常见事后校正。股票拆分或反向拆分也可能产生前瞻性偏差。当计算收益率时,每股收益数据来自公司财务报表,其频率较低,而市场价格至少每天都可获得。因此,EPS 和价格数据都需要同时调整拆分。

解决方法在于对进入回测的所有数据关联的时间戳进行仔细分析,以确保仅使用点时间数据。高质量的数据提供商,如 Compustat,确保满足这些标准。当点时间数据不可用时,需要假设关于报告滞后的情况。

生存者偏差

生存者偏差是在仅包含当前活跃证券数据并省略随着时间消失的资产(例如破产、退市或收购)时出现的。通常情况下,不再是投资范围一部分的证券表现不佳,包含这些情况可能会正向偏移回测结果。

自然的解决方法是验证数据集是否包括随时间可用的所有证券,而不仅仅是在运行测试时仍然可用的证券。

离群值控制

在分析之前的数据准备通常包括处理极端值,例如通过 Winsorize 或修剪。挑战在于确定真正不代表分析期的离群值,而不是当时市场环境的组成部分的极端值。许多市场模型假设在观察到极端值更频繁时的正常分布数据,如脂尾分布所暗示的那样。

解决方法涉及对极端值的概率进行仔细分析,并根据这一现实调整策略参数。

代表性时期

如果使用的时间段不足以充分反映当前环境,缺乏相关市场体制方面,并且不包含足够的数据点或捕获到不太可能重复出现的极端历史事件,则回测将不会产生代表性结果,无法推广到未来时期。

解决方法包括使用包括重要市场现象的样本期间,或生成反映相关市场特征的合成数据(有关实施指南,请参见资源部分)。

实施问题

与历史模拟实施相关的实际问题包括未能标记至市场,即准确反映基础市场价格并考虑到回撤,对交易的可用性、成本或市场影响,或者信号和交易执行的时间做出不切实际的假设。

标记至市场表现

尽管这种策略在回测过程中表现良好,但随着时间的推移可能导致无法接受的损失或波动。

解决方法包括随时间绘制性能或计算(滚动)风险度量,如风险价值VaR)或 Sortino 比率(有关详细信息,请参见附录)。

交易成本

该策略可能假设需要对手方的空头交易,持有市场流动性较低的资产可能会在交易时引起市场波动,或者低估由于经纪费或滑点(即决定交易的市场价格与随后执行价格之间的差异)而产生的成本。

解决方案包括将宇宙限制在高度流动性并且对交易和滑点成本进行现实参数假设(如前述zipline示例所示)。 这还可以防止包括高衰减和因此周转率高的不稳定因子信号。

交易时机

模拟可能对α因子信号评估和结果交易的时间进行不切实际的假设。 例如,当下一笔交易仅在通常相差很大的开盘价时,可能会在收盘价时评估信号。 结果,如果使用收盘价来评估交易绩效,则回测将会有相当大的偏差。

解决方案涉及对信号到达顺序,交易执行和绩效评估的精心编排。

数据窥视和回测过拟合

对于回测有效性,包括已发表结果的最突出挑战与在策略选择过程中进行多次测试而发现虚假模式相关。 在同一数据上测试不同候选者之后选择策略可能会导致选择偏差,因为正面结果更可能是由于性能度量本身的随机性质。 换句话说,策略过于定制,或者过度拟合,以使得数据产生欺骗性的积极结果。

因此,回测表现并不具有信息性,除非报告试验次数以便评估选择偏差的风险。 在实际或学术研究中很少会出现这种情况,这引发了对许多已发表声明有效性的怀疑。

将回测过度拟合到特定数据集的风险不仅来自直接运行大量测试,还包括基于已知什么有效什么无效的先验知识设计的策略,也就是,他人在相同数据上运行的不同回测的知识。 结果,在实践中很难避免回测过拟合。

解决方案包括基于投资或经济理论而不是广泛的数据挖掘工作来选择进行测试的测试。 这还意味着在各种情境和场景中进行测试,包括可能在合成数据上进行测试。

最小回测长度和通胀后的夏普比率

Marcos Lopez de Prado (www.quantresearch.info/) 广泛发表了关于回测风险以及如何检测或避免其的文章。 这包括回测过拟合的在线模拟器 (datagrid.lbl.gov/backtest/)。

另一个结果包括估算投资者应该要求的回测最小长度,考虑到尝试的试验次数,以避免选择在给定试验次数期间具有预期外样本 SR 为零的给定策略的情况。这意味着,例如,如果只有两年的每日回测数据可用,则不应尝试超过七种策略变化,如果只有五年的每日回测数据可用,则不应尝试超过 45 种策略变化。有关实现细节,请参阅参考文献。

De Lopez Prado 和 Bailey (2014) 还推导出一个通货膨胀的 SR,用于计算 SR 在控制多重测试、非正常收益和较短样本长度的通货膨胀效应的情况下具有统计显著性的概率(有关03_multiple_testing子目录的deflated_sharpe_ratio.py的 Python 实现和相关公式的推导,请参阅参考文献)。

回测的最优停止

除了将回测限制在可以从理论上证明的策略上,而不仅仅是数据挖掘练习上,一个重要的问题是何时停止进行额外的测试。

根据最优停止理论中秘书问题的解决方案,建议根据以下经验法则进行决策:测试一组合理策略的随机样本的 1/e(大约 37%) 的性能。然后,继续测试,直到找到一种优于之前测试的策略。

此规则适用于测试几种替代方案的情况,目标是尽快选择接近最佳方案,同时最小化假阳性风险。

如何管理投资组合风险和收益

投资组合管理旨在在金融工具中建立仓位,以实现与基准相关的所需风险回报平衡。在每个周期中,经理选择优化多样化以降低风险并实现目标收益的仓位。在不同周期内,将重新平衡仓位,以考虑由价格变动导致的权重变化,以实现或维持目标风险配置。

多样化使我们能够通过利用价格变动之间的相互作用来降低给定预期收益的风险,因为一种资产的收益可以弥补另一种资产的损失。哈里·马科维茨于 1952 年发明了现代投资组合理论MPT),并提供了通过选择适当的投资组合权重来优化多样化的数学工具。马科维茨展示了投资组合风险,以投资组合收益的标准偏差来衡量,如何取决于所有资产的收益之间的协方差和它们的相对权重。这种关系意味着存在一种有效的投资组合边界,该边界最大化了投资组合收益,同时给定最大水平的投资组合风险。

然而,均值-方差前沿对于计算所需的输入估计非常敏感,例如预期回报、波动率和相关性。在实践中,将这些输入约束以减少抽样误差的均值-方差组合表现得更好。这些受限制的特殊情况包括等权、最小方差和风险均摊组合。

资本资产定价模型(CAPM)是建立在 MPT 风险-回报关系基础上的资产估值模型。它引入了一个概念,即投资者可以在市场均衡状态下期望持有风险资产的风险溢价;该溢价补偿了货币时间价值和无法通过分散化消除的整体市场风险(与特定资产的特异风险相对)。不可分散风险的经济基础包括宏观驱动因素对业务风险的影响,从而影响股票回报或债券违约。因此,资产的预期回报 E[r[i]]是无风险利率 r[f]和与资产暴露于市场组合预期超额回报 r[m]的风险溢价的总和,超额回报以无风险利率为基础:

理论上,市场组合包含所有可投资资产,并且在均衡状态下将由所有理性投资者持有。在实践中,广义价值加权指数近似于市场,例如,用于美国股票投资的标普 500 指数。β[i]测量对市场组合超额回报的暴露。如果 CAPM 有效,则截距分量α[i]应为零。实际上,CAPM 的假设通常不成立,α值捕捉了未通过广义市场暴露解释的回报。

随着时间的推移,研究发现了非传统的风险溢价来源,例如动量或股票价值效应,解释了一些原始α值。经济上的合理性,例如投资者对新信息的过度反应或反应不足的行为偏见,为暴露于这些替代风险因素的风险溢价提供了理论支持。它们演变成了旨在捕捉这些替代贝塔的投资风格,这些投资风格也以专门的指数基金的形式可交易。在分离了这些替代风险溢价的贡献之后,真正的α值仅限于特异资产回报和管理者调整风险敞口的能力。

过去几十年来,有效市场假说(EMH)已经得到完善,以纠正 CAPM 的许多原始缺陷,包括信息不完全以及与交易、融资和代理相关的成本。许多行为偏见具有相同的效果,而一些摩擦被建模为行为偏见。

机器学习在使用市场、基本面和先前章节讨论的替代数据源的监督和无监督学习技术推导新的阿尔法因子方面发挥着重要作用。机器学习模型的输入包括原始数据和经过工程化处理以捕获信息信号的特征。机器学习模型还用于组合单个预测信号并提供更高聚合预测能力。

近几十年来,现代投资组合理论和实践已经发生了重大变化。我们将介绍:

  • 均值-方差优化及其缺陷

  • 诸如最小风险和 1/n 配置之类的替代方案

  • 风险平价方法

  • 风险因子方法

均值-方差优化

MPT 解决了最小化波动性以实现给定预期回报,或者在给定波动水平下最大化回报的最优投资组合权重。关键的前提输入是预期资产回报、标准差和协方差矩阵。

工作原理

多样化的原因在于投资组合回报的方差取决于资产的协方差,并且可以通过包括具有不完全相关性的资产将其降低到低于资产方差的加权平均值。特别是,给定投资组合权重向量 ω 和协方差矩阵 Σ,投资组合方差 σ[PF] 定义为:

马科维茨表明,最大化期望投资组合回报以满足目标风险的问题具有等价的对偶表示,即最小化投资组合风险以满足目标期望回报水平。因此,优化问题变为:

Python 中的有效前沿

我们可以使用 scipy.optimize.minimize 和资产回报、标准差和协方差矩阵的历史估计来计算有效前沿。 代码可以在本章的 repo 的 efficient_frontier 子文件夹中找到,并实现以下步骤序列:

  1. 模拟使用狄利克雷分布生成随机权重,并使用历史回报数据计算每个样本投资组合的均值、标准差和夏普比:
def simulate_portfolios(mean_ret, cov, rf_rate=rf_rate, short=True):
    alpha = np.full(shape=n_assets, fill_value=.01)
    weights = dirichlet(alpha=alpha, size=NUM_PF)
    weights *= choice([-1, 1], size=weights.shape)

    returns = weights @ mean_ret.values + 1
    returns = returns ** periods_per_year - 1
    std = (weights @ monthly_returns.T).std(1)
    std *= np.sqrt(periods_per_year)
    sharpe = (returns - rf_rate) / std

    return pd.DataFrame({'Annualized Standard Deviation': std,
                         'Annualized Returns': returns,
                         'Sharpe Ratio': sharpe}), weights
  1. 设置二次优化问题,以求解给定回报的最小标准差或最大夏普比。为此,定义衡量关键指标的函数:
def portfolio_std(wt, rt=None, cov=None):
    """Annualized PF standard deviation"""
    return np.sqrt(wt @ cov @ wt * periods_per_year)

def portfolio_returns(wt, rt=None, cov=None):
    """Annualized PF returns"""
    return (wt @ rt + 1) ** periods_per_year - 1

def portfolio_performance(wt, rt, cov):
    """Annualized PF returns & standard deviation"""
    r = portfolio_returns(wt, rt=rt)
    sd = portfolio_std(wt, cov=cov)
    return r, sd 
  1. 定义一个目标函数,表示 scipyminimize 函数要优化的负夏普比,考虑到权重被限制在 [-1, 1] 范围内,并且绝对值之和为一:
def neg_sharpe_ratio(weights, mean_ret, cov):
    r, sd = portfolio_performance(weights, mean_ret, cov)
    return -(r - rf_rate) / sd

weight_constraint = {'type': 'eq',
                     'fun': lambda x: np.sum(np.abs(x)) - 1}

def max_sharpe_ratio(mean_ret, cov, short=True):
    return minimize(fun=neg_sharpe_ratio,
                    x0=x0,
                    args=(mean_ret, cov),
                    method='SLSQP',
                    bounds=((-1 if short else 0, 1),) * n_assets,
                    constraints=weight_constraint,
                    options={'tol':1e-10, 'maxiter':1e4})
  1. 通过迭代一系列目标回报范围并解决相应的最小方差投资组合来计算有效前沿。投资组合风险和回报的优化问题以及权重作为函数的约束可以如下制定:
def neg_sharpe_ratio(weights, mean_ret, cov):
    r, sd = pf_performance(weights, mean_ret, cov)
    return -(r - RF_RATE) / sd

def pf_volatility(w, r, c):
    return pf_performance(w, r, c)[1]

def efficient_return(mean_ret, cov, target):
    args = (mean_ret, cov)
    def ret_(weights):
        return pf_ret(weights, mean_ret)

    constraints = [{'type': 'eq', 'fun': lambda x: ret_(x) - 
                     target},
                   {'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]
    bounds = ((0.0, 1.0),) * n_assets
    return minimize(pf_volatility,
                    x0=x0,
                    args=args, method='SLSQP',
                    bounds=bounds,
                    constraints=constraints)
  1. 解决方案需要在可接受值范围内进行迭代,以识别最优的风险收益组合:
def min_vol_target(mean_ret, cov, target, short=True):

    def ret_(wt):
        return portfolio_returns(wt, mean_ret)

    constraints = [{'type': 'eq', 'fun': lambda x: ret_(x) - target},
                     weight_constraint]

    bounds = ((-1 if short else 0, 1),) * n_assets
    return minimize(portfolio_std, x0=x0, args=(mean_ret, cov),
                    method='SLSQP', bounds=bounds,
                    constraints=constraints,
                    options={'tol': 1e-10, 'maxiter': 1e4})

def efficient_frontier(mean_ret, cov, ret_range):
    return [min_vol_target(mean_ret, cov, ret) for ret in ret_range]

模拟产生了可行投资组合的子集,有效前沿确定了在样本内数据可行的最优收益风险组合。下图显示了结果,包括最小方差投资组合和最大化夏普比率的投资组合,以及我们在以下章节讨论的几种替代优化策略产生的若干投资组合。

投资组合优化可以在交易策略的每一次评估步骤中运行,以优化仓位。

挑战与缺陷

前述的均值方差前沿示例说明了样本内、向后看的优化。实际上,投资组合优化需要前瞻性输入。预期收益难以准确估计。

协方差矩阵的估计可能更加可靠,这导致了几种替代方法的出现。然而,具有相关资产的协方差矩阵会引发计算挑战,因为优化问题需要求矩阵的逆。高条件数导致数值不稳定性,从而产生马尔科维茨诅咒:需要更多分散化(通过相关投资机会),算法产生的权重越不可靠。

许多投资者更喜欢使用具有较少繁琐输入要求的投资组合优化技术。我们现在介绍几种旨在解决这些缺点的替代方法,包括基于机器学习的较新方法。

均值方差优化的替代方案

对于均值方差优化问题的准确输入存在挑战,这导致采用了几种实用的替代方法,限制均值、方差或两者,或者省略更具挑战性的回报估计,如风险平价方法。

均等权重投资组合

简单投资组合提供了有用的基准,以衡量生成过度拟合风险的复杂模型的增值。最简单的策略——等权重投资组合——已被证明是表现最佳的策略之一。

2009 年,德米格尔、加拉皮和乌帕尔(de Miguel, Garlappi, and Uppal)比较了各种均值方差优化器产生的投资组合的样本外表现,包括鲁棒贝叶斯估计器、投资组合约束和投资组合的最优组合,与简单的 1/N 规则相比。他们发现,1/N 投资组合的夏普比率高于每个资产类别的头寸,这是由于样本外复杂优化的估计误差成本通常超过了其收益的好处所致。

均等权重投资组合也包括在上述有效前沿图中。

最小方差投资组合

另一个选择是全球最小方差GMV)组合,它优先考虑风险最小化。它显示在有效边界图中,并可以通过使用均值-方差框架最小化投资组合标准差来计算如下:

def min_vol(mean_ret, cov, short=True):
    return minimize(fun=portfolio_std,
                    x0=x0,
                    args=(mean_ret, cov),
                    method='SLSQP',
                    bounds=bounds = ((-1 if short else 0, 1),) * 
                          n_assets,
                          constraints=weight_constraint,
                          options={'tol': 1e-10, 'maxiter': 1e4})

相应的min.波动性组合位于上述有效边界上。

全球投资组合优化 - 黑-利特曼方法

Black 和 Litterman(1992)的全球投资组合优化方法将经济模型与统计学习相结合,因此在许多情况下生成的预期收益估计是合理的,因此很受欢迎。

这种技术背离了市场是由 CAPM 均衡模型所隐含的均值-方差投资组合的假设,并且建立在这样一个事实之上,即观察到的市场资本化可以被视为市场分配的最优权重。市场权重反映了市场价格,而市场价格又体现了市场对未来收益的预期。

因此,这种方法可以从市场足够接近均衡的假设开始逆向工程出未来预期收益,并且允许投资者使用缩小估计器将这些估计值调整到他们自己的信念中。该模型可以解释为是投资组合优化的贝叶斯方法。我们将在第九章中介绍贝叶斯方法,贝叶斯机器学习

如何确定下注的大小 - 凯利法则

凯利法则在赌博中有着悠久的历史,因为它提供了在一系列变化的(但是有利的)赔率下,为了最大化期末财富而在每一次下注上押注多少的指导。这个法则于 1956 年由约翰·凯利发布,他是贝尔实验室的克劳德·香农的同事,作品名为《信息率的新解释》。他被新晋问答节目《$64,000 问题》上观众对候选人下注的情况所吸引,这些观众利用西海岸的三小时延迟获取关于获胜者的内部消息。

凯利将其法则与香农的信息理论联系起来,以求解在赔率有利但存在不确定性时最优的长期资本增长下注。他的法则将每场比赛成功的几率的对数财富最大化,并且包含了隐含的破产保护,因为 log(0)是负无穷,所以凯利赌徒自然会避免失去一切。

下注的最佳大小

凯利开始分析具有二元胜负结果的游戏。关键变量有:

  • b:赔率定义了每 1 美元下注的赢得金额。赔率=5/1 意味着如果下注赢了,将获得 5 美元的收益,加上 1 美元的本金。

  • p:概率定义了有利结果的可能性。

  • f:要下注的当前资本份额。

  • V:由于下注而产生的资本价值。

凯利法则的目标是最大化无限次重复下注的价值增长率,G

WL分别代表赢和输的次数时,那么:

我们可以通过对f最大化来最大化增长率G,如下所示,使用sympy进行说明:

from sympy import symbols, solve, log, diff

share, odds, probability = symbols('share odds probability')
Value = probability * log(1 + odds * share) + (1 - probability) * log(1 
        - share)
solve(diff(Value, share), share)

[(odds*probability + probability - 1)/odds]

我们得到了要投注的资本的最佳份额:

最优投资 - 单一资产

在金融市场的背景下,结果和替代方案都更加复杂,但凯利规则的逻辑仍然适用。这一规则由首次将其成功应用于赌博(在《击败庄家》中描述)的埃德·索普推广,后来他开始了成功的对冲基金普林斯顿/纽波特合伙企业。

对于连续结果,资本的增长率由不同回报的概率分布的积分定义,可以进行数值优化:

我们可以使用scipy.optimize模块来解决这个表达式以得到最优f^* 的值:

def norm_integral(f, m, st):
    val, er = quad(lambda s: np.log(1+f*s)*norm.pdf(s, m, st), m-3*st, 
                   m+3*st)
    return -val

def norm_dev_integral(f, m, st):
    val, er = quad(lambda s: (s/(1+f*s))*norm.pdf(s, m, st), m-3*st, 
                   m+3*st)
    return val

m = .058
s = .216
# Option 1: minimize the expectation integral
sol = minimize_scalar(norm_integral, args=(
                m, s), bounds=[0., 2.], method='bounded')
print('Optimal Kelly fraction: {:.4f}'.format(sol.x))

最优投资 - 多资产

我们将使用一个涉及各种股票的例子。E. Chan(2008)阐述了如何到达凯利规则的多资产应用,并且结果等同于(可能是杠杆的)从均值方差优化中获得的最大夏普比率组合。

计算涉及精度矩阵(协方差矩阵的逆矩阵)和回报矩阵的点积:

mean_returns = monthly_returns.mean()
cov_matrix = monthly_returns.cov()
precision_matrix = pd.DataFrame(inv(cov_matrix), index=stocks, columns=stocks)
kelly_wt = precision_matrix.dot(mean_returns).values

凯利组合也显示在有效前沿图表中(通过标准化使绝对权重总和为一)。许多投资者倾向于减少凯利权重以减少该策略的波动性,半凯利已经变得特别流行。

风险平价

过去 15 年的两次全球股票市场危机,持续上升的收益曲线以及利率的普遍下降使风险平价看起来像是一个特别引人注目的选择。许多机构为风险平价分配了战略性的资产配置,以进一步实现投资组合的多样化。

一个简单的风险平价分配方案根据它们方差的倒数来分配资产,忽略相关性,特别是回报预测:

var = monthly_returns.var()
risk_parity_weights = var / var.sum()

风险平价投资组合也显示在本节开头的有效前沿图表中。

风险因素投资

估计输入的另一种框架是逐步解析推动资产风险和回报的基础决定因素或因子。如果我们了解因子如何影响回报,并且我们了解这些因子,我们将能够构建更稳健的投资组合。

因子投资的概念超越了资产类别标签,以最大化分散化的好处来看待潜在的因子风险。与区分投资工具的标签(例如对冲基金或私募股权)不同,因子投资旨在根据基本风险因素的暴露差异来识别不同的风险-回报配置文件。均值-方差投资的天真方法将(人为的)分组视为不同的资产类别,并将其插入均值-方差优化器。因子投资认识到这些分组与传统资产类别共享许多相同的因子风险。分散化的好处可能被夸大,正如投资者在上一次危机中发现的那样,当有风险资产类别之间的相关性增加时,由于暴露于相同的潜在因子风险。

分层风险平价

均值-方差优化对预期收益和这些收益的协方差的估计非常敏感。当收益高度相关时,协方差矩阵的求逆也变得更加具有挑战性和不准确,这在实践中经常发生。其结果被称为马科维茨诅咒:当分散投资更重要时,因为投资相关,传统的投资组合优化器可能会产生不稳定的解决方案。分散化的好处可能会被错误的估计所抵消。正如讨论的那样,即使是天真的等权重投资组合也可以在样本外击败均值-方差和基于风险的优化。

更健壮的方法已经包括了额外的约束(Clarke et al., 2002),贝叶斯先验(Black and Litterman, 1992),或者使用缩小估计器使精度矩阵更加稳定(Ledoit 和 Wolf [2003],在 scikit-learn 中可用(scikit-learn.org/stable/modules/generated/sklearn.covariance.LedoitWolf.html)。与此相反,分层风险平价(HRP)利用无监督机器学习实现了更优异的样本外投资组合配置。

最近的投资组合优化创新利用图论和分层聚类构建投资组合的三个步骤(Lopez de Prado, 2015):

  1. 定义一个距离度量,使相关资产彼此接近,并应用单链接聚类来识别分层关系。

  2. 利用分层相关结构来准对角化协方差矩阵。

  3. 使用自上而下的反方差加权,利用递归二分搜索将聚类资产视为投资组合建设中的互补而不是替代品,并减少自由度的数量。

Raffinot(2016)提出了构建分层聚类投资组合HCP)的相关方法。从概念上讲,金融市场等复杂系统往往具有结构,并且通常以层次方式组织,而层次结构中元素之间的相互作用塑造了系统的动态。相关矩阵也缺乏层次结构的概念,这使得权重可以自由变化,并可能以意想不到的方式变化。

JPM 已经在各种股票市场上测试了 HRP 和 HCP。特别是,与天真的多样化、最大多样化投资组合或 GMV 投资组合相比,HRP 产生了相等或更高的风险调整回报和夏普比率。

我们将在第十二章中介绍 Python 实现,无监督学习

摘要

在本章中,我们涵盖了投资组合管理的重要主题,这涉及将投资头寸组合起来以管理风险与回报的权衡目标。我们介绍了pyfolio来计算和可视化关键的风险和回报指标,并比较了各种算法的表现。

我们看到准确预测对于优化投资组合权重和最大化分散效益是多么重要。我们还探讨了 ML 如何通过从资产收益协方差矩阵中学习层次关系来促进更有效的投资组合构建。

现在我们将继续本书的第二部分,重点介绍 ML 模型的使用。这些模型将通过更有效地利用更多样化的信息来捕捉比目前更突出的简单 Alpha 因子更复杂的模式,从而产生更准确的预测。

我们将开始通过使用交叉验证来训练、测试和调整线性回归和分类模型,以实现稳健的样本外表现。我们还将把这些模型嵌入到定义和回测算法交易策略的框架中,这是我们在过去两章中介绍的。

第六章:机器学习过程

在本章中,我们将开始说明您可以如何使用广泛的监督和非监督机器学习ML)模型进行算法交易。我们将在演示与各种 Python 库相关的相关应用之前,解释每个模型的假设和用例。模型的分类将包括:

  • 用于截面、时间序列和面板数据的回归和分类的线性模型

  • 广义加法模型,包括非线性基于树的模型,如决策树

  • 集成模型,包括随机森林和梯度提升机

  • 用于降维和聚类的非监督线性和非线性方法

  • 神经网络模型,包括循环和卷积架构

  • 强化学习模型

我们将这些模型应用到本书第一部分介绍的市场、基本和替代数据来源中。我们将进一步建立在迄今为止涵盖的内容之上,向您展示如何将这些模型嵌入到算法交易策略中,以生成或结合阿尔法因子,或优化投资组合管理流程,并评估其性能。

许多这些模型及其用途具有共同的几个方面。本章涵盖了这些共同的方面,以便我们可以在后续章节中专注于模型特定的用法。它们包括通过优化目标或损失函数从数据中学习功能关系的总体目标。它们还包括密切相关的测量模型性能的方法。

我们区分了无监督和监督学习,以及监督回归和分类问题,并概述了算法交易的用例。我们将使用监督学习进行统计推断输入和输出数据之间关系与使用未来输入进行未来输出预测之间的对比。我们还说明了预测错误是由于模型的偏差或方差,或者数据中的噪声信号比高而引起的。最重要的是,我们提供了诊断错误来源并改善模型性能的方法。

在本章中,我们将涵盖以下主题:

  • 使用数据进行监督和非监督学习的原理

  • 如何应用机器学习工作流程

  • 如何为回归和分类制定损失函数

  • 如何训练和评估监督学习模型

  • 偏差-方差权衡如何影响预测错误

  • 如何诊断和解决预测错误

  • 如何使用交叉验证训练模型来管理偏差-方差权衡

  • 如何使用 scikit-learn 实现交叉验证

  • 为什么金融数据的性质需要不同的样本外测试方法

如果您对机器学习已经非常熟悉,请随意跳过,直接开始学习如何使用线性模型为算法交易策略生成和组合阿尔法因子。

从数据中学习

ML 有许多定义,它们都围绕数据中有意义的模式的自动检测。两个著名的例子包括:

  • AI 先驱亚瑟·塞缪尔森在 1959 年将 ML 定义为计算机科学的一个子领域,赋予计算机在没有明确编程的情况下学习的能力。

  • 托尼·米切尔,该领域的一位当前领军人物,1998 年更加具体地确定了一个明确定义的学习问题:一个计算机程序根据任务和绩效指标从经验中学习,看任务的绩效是否随经验提高而提高。

经验以训练数据的形式呈现给算法。与以往试图构建解决问题的机器的尝试的主要区别在于,算法用于做出决策的规则是从数据中学习的,而不是被编程或硬编码——这是上世纪 80 年代突出的专家系统的情况。

自动学习的关键挑战在于识别训练数据中的模式,在将模型的学习泛化到新数据时这些模式是有意义的。模型可能识别的潜在模式数量庞大,而训练数据仅构成了算法未来执行任务所需的更大现象集合的一部分样本。可能从给定输入生成给定输出的函数数量无限,这使得搜索过程在没有对可接受函数集合施加限制的情况下无法解决。

算法能够学习的模式类型受到其假设空间的大小以及样本数据中包含的信息量的限制。假设空间的大小在算法之间有很大的变化。一方面,这种限制使得成功的搜索成为可能,另一方面,它暗示了归纳偏见,因为算法从训练样本推广到新数据。

因此,关键挑战变成了如何选择具有足够大的假设空间以包含学习问题解决方案的模型,同时又足够小以确保在给定训练数据大小的情况下可靠的泛化。随着越来越多的信息化数据,具有更大假设空间的模型将会成功。

无免费午餐定理指出没有通用的学习算法。相反,学习者的假设空间必须根据关于任务领域的先验知识来定制,以便搜索有意义的模式成功。在本章中,我们将密切关注模型对特定任务的数据关系所做的假设,并强调通过数据探索获得的经验证据与这些假设匹配的重要性。掌握任务所需的过程可以区分为监督学习、无监督学习和强化学习。

监督学习

监督学习是最常用的机器学习类型。本书的大部分章节将致力于学习该类别中模型的各种应用。术语监督意味着有一个结果变量指导学习过程,即,它教会算法任务的正确解决方案正在学习。监督学习旨在将从个别样本学到的输入和输出数据之间的功能关系泛化,并将其应用于新数据。

输出变量也可根据领域不同交替称为标签、目标、结果、内生或左手变量。我们将对观测值i = 1, ..., N使用y[i],或者在向量表示中使用y。某些任务由多个结果表示,也称为多标签问题。监督学习问题的输入数据也称为特征、外生和右手变量,用x[i ]表示i = 1, ..., N的一系列特征,或者在矩阵表示中使用X

解决监督学习问题的解决方案是一个函数(),它表示模型从样本中学到的输入-输出关系,并逼近真实关系,用表示。这个函数可以用来推断变量之间的统计关系,甚至可能是因果关系,超出样本范围,或者用来预测新输入数据的输出。

两个目标面临着一个重要的权衡:更复杂的模型有更多移动部件,能够表示更微妙的关系,但也可能更难以检查。它们也可能过度拟合并学习到训练样本特有的随机噪音,而不是代表输入-输出关系一般模式的系统信号。另一方面,过于简单的模型会忽略信号并产生偏倚的结果。这种权衡在监督学习中被称为偏差-方差权衡,但概念上这也适用于其他形式的机器学习,过于复杂的模型可能在训练数据之外表现不佳。

无监督学习

在解决无监督学习问题时,我们只观察特征,没有测量结果。任务不是预测未来结果或推断变量之间的关系,而是在没有任何结果信息指导搜索过程的情况下,在数据中找到结构。

通常,无监督算法旨在学习输入数据的新表示,这对其他任务是有用的。这包括提出标签以识别观察结果之间的共性,或者是一个总结性描述,捕捉到相关信息,同时需要数据点或特征。无监督学习算法在他们试图发现的结构的性质方面也不同于监督学习算法所做出的假设。

应用

无监督学习有几种有用的用途可应用于算法交易,包括以下内容:

  • 将具有相似风险和回报特征的证券分组在一起(请参阅本章的分层风险平价(这是关于投资组合优化的))

  • 找到驱动大量证券表现的少数风险因素

  • 识别系统地不同的交易和价格模式,可能具有较高的风险

  • 识别文档体系(例如,收益电话转录)中的潜在主题,这些主题包括那些文档中最重要的方面

在高层次上,这些应用依赖于识别聚类的方法和降低数据维度的方法。

聚类算法

聚类算法使用相似性度量来识别包含相似信息的观察或数据属性。它们通过将大量数据点分配给较少数量的聚类来概括数据集,以使聚类成员彼此之间的关系比与其他聚类成员更密切。

聚类算法主要在于它们将产生何种类型的聚类,这意味着对数据生成过程有不同的假设,列举如下:

  • K 均值聚类:数据点属于相等大小的k个椭圆形聚类之一

  • 高斯混合模型:数据点是由任何各种多元正态分布生成的

  • 基于密度的聚类:聚类具有任意形状,并且仅由附近数据点的最小数量的存在来定义

  • 层次聚类:数据点属于由逐渐合并较小聚类形成的各种超集组

降维

降维生成新的数据,捕捉源数据中包含的最重要信息。这些算法不是将现有数据分组成聚类,而是将现有数据转换为使用显著较少的特征或观察来表示原始信息的新数据集。

算法在生成新数据集的性质上存在差异,如下列表所示:

  • 主成分分析(PCA):找到捕获现有数据集中大部分方差的线性变换

  • 流形学习:确定一个非线性转换,产生数据的低维表示

  • 自编码器:使用神经网络以最小的信息损失对数据进行非线性压缩

我们将在接下来的几章中更深入地探讨线性、非线性和基于神经网络的无监督学习模型,包括自然语言处理NLP)在主题建模和 Word2vec 特征提取形式的重要应用。

强化学习

强化学习是第三种 ML 类型。它旨在选择在给定描述上下文或环境的输入数据集的情况下产生最高奖励的行动。它既动态又交互:正面和负面奖励的流影响算法的学习,现在采取的行动可能影响环境和未来奖励。

在已学习到可以产生某种奖励的行动的开发和可能增加未来奖励的新行动之间的权衡导致了一种试错方法。强化学习使用动态系统理论,特别是使用马尔可夫决策过程的最优控制来优化代理的学习,其信息不完全。

强化学习与监督学习不同,监督学习中训练数据既提供了上下文又提供了算法的正确决策。它专为交互式设置设计,其中结果只在一段时间后才可用,并且学习必须以在线或连续的方式进行,因为代理获取新经验。然而,人工智能AI中一些最显著的进展涉及使用深度学习来近似动作、环境和未来奖励之间的功能关系的强化学习。它还与无监督学习不同,因为反馈的后果将可用,尽管会有延迟。

强化学习特别适用于算法交易,因为在不确定、动态环境中追求最大化回报的代理概念与与金融市场互动的投资者或交易策略有很多共同之处。这种方法已成功应用于游戏代理,尤其是围棋,但也用于复杂的视频游戏。它还用于机器人技术——例如,自动驾驶汽车——或根据用户交互个性化服务,例如网站提供的内容。我们将在第二十一章介绍强化学习方法,用于构建算法交易策略,强化学习

机器学习工作流程

为算法交易策略开发一个机器学习解决方案需要系统化的方法,以最大化成功的机会,同时节约资源。使过程透明且可复制非常重要,以便促进协作、维护和后续的改进。

下图概述了从问题定义到部署预测解决方案的关键步骤:

整个过程在整个序列中是迭代的,不同阶段需要的工作量会根据项目而变化,但这个过程通常应包括以下步骤:

  1. 确定问题框架,确定目标指标,并定义成功。

  2. 源,清理和验证数据。

  3. 理解你的数据并生成信息性特征。

  4. 选择一个或多个适合你的数据的机器学习算法。

  5. 训练,测试和调整你的模型。

  6. 使用你的模型解决原始问题。

我们将在接下来的章节中通过一个简单的例子来演示这些步骤,以阐明一些关键点。

基本步骤- k 最近邻。

本章书籍的 GitHub 存储库中的machine_learning_workflow.ipynb笔记本包含了几个示例,演示了使用房价数据集进行机器学习工作流程。

我们将使用相当简单的k 最近邻KNN)算法,它允许我们解决回归和分类问题。

在其默认的sklearn实现中,它识别出与欧几里德距离最近的k个数据点来进行预测。在分类或回归情况下,它分别预测邻居中最频繁的类别或平均结果。

确定问题框架-目标和指标。

任何机器学习练习的起点都是其旨在解决的最终用例。有时,这个目标将是统计推断,以识别变量之间的关联或甚至因果关系。然而,最常见的目标通常是直接预测结果以产生交易信号。

推断和预测都使用指标来评估模型实现其目标的程度。我们将重点放在常见的目标函数和相应的误差度量上,用于可以通过输出变量类型来区分的预测模型:连续输出变量意味着回归问题,分类变量意味着分类问题,而有序分类变量的特殊情况则意味着排名问题。

问题可能是多个 alpha 因子的高效组合,并且可以构建为一个回归问题,旨在预测回报,一个旨在预测未来价格走势方向的二元分类问题,或者一个旨在将股票分配到不同绩效类别的多类问题。在接下来的部分中,我们将介绍这些目标,并探讨如何衡量和解释相关的误差度量。

预测与推断的对比

由监督学习算法产生的功能关系可用于推断——即获得有关结果生成方式的见解——或用于预测——即为未知或未来输入生成准确的输出估计(由表示),用X表示未来输入)。

对于算法交易,推断可用于估计资产回报对风险因素的因果或统计依赖性,而预测可用于预测风险因素。将两者结合起来可以产生资产价格的预测,进而可以转化为交易信号。

统计推断涉及从样本数据中推断出关于潜在概率分布或总体参数的结论。可能的结论包括有关个别变量分布特征的假设检验,或关于变量之间数值关系的存在或强度的假设检验。它们还包括统计指标的点估计或区间估计。

推断取决于首次生成数据的过程的假设。我们将审查这些假设以及用于线性模型推断的工具,其中它们被很好地确立。更复杂的模型对输入和输出之间的结构关系做出较少的假设,而是更开放地处理函数逼近任务,同时将数据生成过程视为黑盒。这些模型,包括决策树、集成模型和神经网络,在用于预测任务时专注并经常优于其他模型。然而,随机森林最近获得了一种我们将在后面介绍的推断框架。

因果推断

因果推断旨在识别关系,以便某些输入值暗示某些输出值,例如,一定数量的宏观变量组合导致特定资产价格以某种方式变化,假设所有其他变量保持不变。

关于两个或多个变量之间关系的统计推断会产生相关性度量,这些度量只有在满足几个其他条件时才能解释为因果关系——例如,当已经排除了替代解释或逆向因果关系时。满足这些条件需要一个实验设置,其中所有感兴趣的相关变量都可以完全控制,以隔离因果关系。或者,准实验性设置以随机方式使观察单位暴露于输入变化,以排除其他可观察或不可观察的特征对环境变化观察效果的影响。

这些条件很少被满足,因此推断性结论需要谨慎对待。同样适用于预测模型的性能,后者也依赖于特征和输出之间的统计关联,该关联可能会随着不属于模型一部分的其他因素而改变。

KNN 模型的非参数性质不利于推断,因此我们将推迟工作流程中的这一步,直到我们在下一章遇到线性模型为止。

回归问题

回归问题旨在预测连续变量。均方根误差RMSE)是最流行的损失函数和误差度量,其中之一是因为它是可微的。损失是对称的,但在计算中较大的误差权重更重。使用平方根的优势在于以目标变量的单位度量误差。与RMSE 对数误差RMSLE)结合使用相同的度量在目标受指数增长影响时是适当的,因为其不对称惩罚使得负误差权重小于正误差。您也可以先对目标进行对数转换,然后使用 RMSE,正如我们在本节后面的示例中所做的那样。

平均绝对误差MAE)和中位数绝对误差MedAE)是对称的,但不会使较大的误差权重更重。MedAE 对异常值具有鲁棒性。

解释的方差分数计算模型解释的目标方差所占比例,介于 0 和 1 之间。R²分数或决定系数产生相同的结果,即残差的平均值为 0,但在其他情况下可能不同。特别地,当在样本外数据上计算时(或者对于没有截距的线性回归),它可以为负。

下表定义了用于计算的公式以及可从metrics模块导入的相应sklearn函数。scoring参数与自动化的训练-测试函数(例如cross_val_scoreGridSearchCV)结合使用,我们稍后将在本节介绍,并在附带的笔记本中进行演示:

名称 公式 sklearn 评分参数
均方误差 mean_squared_error neg_mean_squared_error
平均平方对数误差 mean_squared_log_error neg_mean_squared_log_error
平均绝对误差 mean_absolute_error neg_mean_absolute_error
中位数绝对误差 median_absolute_error neg_median_absolute_error
解释的方差 explained_variance_score explained_variance
R2 分数 r2_score r2

下图显示了笔记本中房价回归的各种误差指标:

sklearn函数还支持多标签评估——即将多个结果值分配给单个观测值;有关更多详细信息,请参阅 GitHub 上引用的文档(github.com/PacktPublishing/Hands-On-Machine-Learning-for-Algorithmic-Trading/tree/master/Chapter06)。

分类问题

分类问题具有分类结果变量。 大多数预测器将输出一个分数,指示观测是否属于某个类。 在第二步中,这些分数然后被转换为实际预测。

在二元情况下,我们将标记类为正面和负面,分数通常在零之间变化或相应地进行归一化。 一旦将分数转换为 0-1 预测,就可能有四种结果,因为两个现有类别中的每一个都可以被正确或错误地预测。 如果有超过两个类别,则可以有更多的情况,如果您区分了几种潜在的错误。

所有的误差指标都是从预测在 2 x 2 混淆矩阵的四个领域中进行的,该矩阵将实际类与预测类相关联。 表中列出的指标,如准确性,评估了给定阈值的模型:

分类器不一定需要输出校准过的概率,而应该产生相对于彼此的分数,以区分正面和负面案例。 因此,阈值是一个决策变量,可以和应该优化,考虑到正确和错误预测的成本和收益。 更低的阈值意味着更多的正面预测,可能会出现日益上升的假阳性率,而更高的阈值则可能相反。

接收操作特性和曲线下面积

接收器操作特性 (ROC) 曲线允许我们根据其性能可视化、组织和选择分类器。它计算了使用任何预测分数作为阈值产生的所有真正例率 (TPR) 和假正例率 (FPR) 的组合。然后将这些对绘制在一个边长为一的正方形上。

进行随机预测(考虑类别不平衡)的分类器平均产生相等的 TPR 和 FPR,使得组合位于对角线上,这成为基准情况。由于性能不佳的分类器将受益于重新标记预测,因此该基准也成为最低水平。

曲线下面积 (AUC) 被定义为 ROC 图中的曲线下面积,其范围在 0.5 到最大值 1 之间变化。它是分类器的得分能够如何排列数据点的类成员身份的摘要度量。更具体地说,分类器的 AUC 具有重要的统计性质,表示分类器将随机选择的正实例排在随机选择的负实例之前的概率,这等价于 Wilcoxon 秩和检验。此外,AUC 不敏感于类别不平衡,这是它的好处。

精确度-召回率曲线

当对其中一个类别的预测特别感兴趣时,精确度和召回率曲线可视化这些误差度量之间的权衡,以及不同阈值的情况。两种度量都评估了对特定类别的预测质量。以下列表显示了它们如何应用于正类:

  • 召回率 衡量了对于给定阈值,分类器预测为正的实际正类成员的比例。它源于信息检索,衡量了搜索算法成功识别的相关文档的比例。

  • 精确度 相反,衡量了正确的正预测的比例。

召回率通常随着阈值降低而增加,但精确度可能会降低。精确度-召回率曲线可视化可达到的组合,并允许根据错过大量相关案例或产生质量较低的预测的成本和收益优化阈值。

F1 分数是给定阈值下精确度和召回率的调和平均数,并可用于在考虑这两个度量应承担的相对权重的情况下进行阈值的数值优化。

以下图表说明了 ROC 曲线及其相应的 AUC 以及精确度-召回率曲线和 F1 分数,使用精确度和召回率的相等权重,产生阈值为 0.37。图表取自附带笔记本,您可以在其中找到作用于二值化房价的 KNN 分类器的代码:

收集和准备数据

我们已经讨论了市场、基本和替代数据的采购的重要方面,并将继续使用各种这些来源的示例来说明各种模型的应用。

除了我们将通过 Quantopian 平台访问的市场和基本数据外,当我们探索自然语言处理时,我们还将获取和转换文本数据,当我们查看图像处理和识别时,我们也会获取和转换图像数据。除了获取、清理和验证数据以将其与通常以时间序列格式可用的交易数据相关联之外,将其存储在能够快速访问以便快速探索和迭代的格式中也很重要。我们推荐使用 HDF 和 parquet 格式。对于更大的数据量,Apache Spark 是最佳解决方案。

探索、提取和工程化特征

理解个体变量的分布以及结果和特征之间的关系是选择合适算法的基础。这通常从诸如散点图之类的可视化开始,如伴随笔记本中所示(并在以下图片中显示),但也包括从线性指标,如相关性,到非线性统计量,如我们介绍信息系数时遇到的 Spearman 秩相关系数的数字评估。它还包括信息论量度,如相互信息,如下一小节所示:

散点图

系统性的探索性分析也是通常成功预测模型的最重要的一个组成部分的基础:工程化的特征,这些特征提取了数据中包含的信息,但这些信息在原始形式下不一定能被算法访问到。特征工程受益于领域专业知识、统计学和信息论的应用,以及创造力。

它依赖于巧妙选择的数据转换,这些转换能有效地挖掘出输入和输出数据之间的系统关系。有许多选择,包括异常值检测和处理、功能转换以及多个变量的组合,包括无监督学习。我们将在整个过程中举例说明,但强调这个特性最好通过经验学习。Kaggle 是一个很好的地方,可以从其他数据科学家那里学习,他们与 Kaggle 社区分享了他们的经验。

使用信息论评估特征

特征和结果之间的相互信息MI)是两个变量之间相互依赖的一种度量。它将相关性的概念扩展到非线性关系。更具体地说,它量化了通过另一个随机变量获取的关于一个随机变量的信息。

MI 的概念与随机变量的基本熵概念密切相关。熵量化了随机变量中包含的信息量。形式上,两个随机变量XY的互信息—I(X, Y)—被定义如下:

sklearn 函数实现了 feature_selection.mutual_info_regression,计算所有特征与连续结果之间的互信息,以选择最有可能包含预测信息的特征。还有一个分类版本(请参阅文档以了解更多详情)。笔记本 mutual_information.ipynb 包含了对我们在第四章中创建的金融数据的应用,Alpha 因子研究

选择一个 ML 算法

本书的剩余部分将介绍几个模型族,从线性模型,其对输入和输出变量之间的功能关系的性质做出了相当强的假设,到深度神经网络,其假设极少。如前言中提到的,更少的假设将需要更多具有关于关系的显著信息的数据,以便学习过程能够成功。

我们将在引入这些模型时概述关键假设及其测试方法。

设计和调整模型

ML 过程包括基于模型泛化误差估计的诊断和管理模型复杂性的步骤。一个无偏估计需要一个统计上合理和有效的程序,以及与输出变量类型一致的错误度量,这也决定了我们是否处理回归、分类或排名问题。

偏差-方差的权衡

ML 模型在预测新输入数据的结果时所产生的错误可以分解为可减少和不可减少的部分。不可减少的部分是由数据中未测量的随机变化(噪音)引起的,比如相关但缺失的变量或自然变化。广义误差的可减少部分又可以分解为偏差和方差。二者都是由机器学习算法对真实功能关系和假设之间的差异造成的,如下列表所详述的:

  • 偏差引起的错误:假设过于简单,无法捕捉真实功能关系的复杂性。因此,每当模型试图学习真实功能时,它都会犯系统性错误,平均而言,预测也将受到类似的偏差影响。这也被称为欠拟合

  • 方差导致的错误:算法过于复杂,以至于忽视了真实关系。它不是捕捉真实关系,而是过拟合数据并从噪声中提取模式。结果,它从每个样本中学习到不同的函数关系,并且外样预测将有很大的变化。

欠拟合与过拟合

以下图表说明了通过使用越来越复杂的多项式来近似余弦函数并测量样本内误差而产生的过拟合。更具体地说,我们绘制 10 个带有一些添加噪声的随机样本(n = 30)以学习不同复杂度的多项式(请参阅附带笔记本中的代码)。每次,模型预测新数据点,我们捕获这些预测的均方误差,以及这些误差的标准差。

以下图表中的左侧面板显示了一个一次多项式;一条直线明显地欠拟合真实函数。然而,估计的直线不会从真实函数的一个抽样到下一个抽样有明显的差异。中间面板显示了一个五次多项式在[0, 1]区间上合理地近似了真实关系。另一方面,一个十五次多项式几乎完美地拟合了小样本,但提供了对真实关系的很差估计:它对样本数据点的随机变化过拟合,而且学习的函数会随着每个抽样而变化。

管理权衡

让我们通过尝试学习带有一些添加噪声的九次余弦函数的泰勒级数近似来进一步说明过拟合与欠拟合的影响。在以下图表中,我们绘制真实函数的随机样本,并拟合欠拟合、过拟合和提供近似正确灵活度的多项式。然后我们进行外样预测并测量 RMSE。

三次多项式的高偏差但低方差与第一个面板中可见的各种预测误差的低偏差但极高方差相比。左侧面板显示了由减去真实函数值产生的误差分布。直线欠拟合情况下产生了糟糕的样本内拟合,并且在样本外明显偏离目标。过拟合模型显示了最佳的样本内拟合以及最小的误差分散,但代价是样本外的大方差。与真实模型的函数形式匹配的适当模型明显在样本外表现最佳。

以下截图的右侧面板显示了实际预测而不是错误,以演示不同类型的拟合在实践中的样子:

学习曲线

学习曲线绘制了根据用于学习函数关系的数据集大小演变的训练和测试错误。它是诊断给定模型的偏差-方差权衡的有用工具,因为误差会表现出不同的行为。具有高偏差的模型将具有高但相似的训练误差,无论是样本内还是样本外,而过度拟合的模型将具有非常低的训练误差。

外样本错误的下降说明过拟合模型可能会受益于额外的数据或限制模型复杂性的工具,如正则化,而不拟合的模型需要使用更多特征或以其他方式增加模型的复杂性,如下图所示:

如何使用交叉验证进行模型选择

当您的用例中有多个候选模型(即算法)可用时,选择其中一个的行为称为模型选择问题。模型选择旨在确定在给定新数据的情况下产生最低预测误差的模型。

对这种泛化误差的无偏估计需要在未参与模型训练的数据上进行测试。因此,我们只使用部分可用数据来训练模型,并将另一部分数据留出来测试模型。为了获得对预测误差的无偏估计,绝对不能泄漏有关测试集的任何信息到训练集中,如下图所示:

有几种可用于拆分可用数据的方法,这些方法在用于训练的数据量、误差估计的方差、计算强度以及在拆分数据时是否考虑数据的结构方面不同。

如何在 Python 中实现交叉验证

我们将通过展示一个具有十个观察值的模拟数据集的索引如何分配到训练集和测试集(详见cross_validation.py),来说明将数据拆分为训练集和测试集的各种选项,如下所示:

data = list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

基本的训练测试拆分

对于将数据拆分为训练集和测试集的单次拆分,请使用sklearn.model_selection.train_test_split,其中shuffle参数默认确保观察值的随机选择,而这又可以通过设置random_state来复制。还有一个stratify参数,对于分类问题,确保训练集和测试集将包含大致相同的每个类的份额,如下图所示:

train_test_split(data, train_size=.8)
[[8, 7, 4, 10, 1, 3, 5, 2], [6, 9]]

在这种情况下,我们使用除行号69之外的所有数据来训练模型,这些数据将用于生成预测并测量给定know标签上的错误。这种方法对于快速评估很有用,但对拆分很敏感,并且测试误差估计的标准误差将更高。

交叉验证

交叉验证 (CV) 是一种常用的模型选择策略。CV 的主要思想是将数据拆分一次或多次,以便每个拆分都被用作一次验证集,剩余部分用作训练集:数据的一部分(训练样本)用于训练算法,剩余部分(验证样本)用于估计算法的风险。然后,CV 选择具有最小估计风险的算法。

虽然数据拆分启发式方法非常一般化,但 CV 的一个关键假设是数据是 独立同分布 (IID) 的。在接下来的章节中,我们将看到,对于时间序列数据,通常情况并非如此,需要采用不同的方法。

使用保留测试集

在基于验证分数选择超参数时,请注意,由于多次测试,此验证分数存在偏差,并不再是泛化误差的良好估计。为了对误差率进行无偏估计,我们必须从新数据集中估计分数,如下图所示:

因此,我们使用数据的三向拆分,如前图所示:一部分用于交叉验证,并反复拆分为训练集和验证集。剩余部分作为保留集保留,只有在交叉验证完成后才使用它生成无偏的测试误差估计。我们将在下一章开始构建机器学习模型时说明这种方法。

KFold 迭代器

sklearn.model_selection.KFold 迭代器生成几个不相交的拆分,并将这些拆分中的每一个分配给验证集,如下面的代码所示:

kf = KFold(n_splits=5)
for train, validate in kf.split(data):
    print(train, validate)

[2 3 4 5 6 7 8 9] [0 1]
[0 1 4 5 6 7 8 9] [2 3]
[0 1 2 3 6 7 8 9] [4 5]
[0 1 2 3 4 5 8 9] [6 7]
[0 1 2 3 4 5 6 7] [8 9]

除了拆分数量外,大多数 CV 对象还接受一个 shuffle 参数,以确保随机性。为了使结果可重现,设置 random_state 如下所示:

kf = KFold(n_splits=5, shuffle=True, random_state=42)
for train, validate in kf.split(data):
    print(train, validate)

[0 2 3 4 5 6 7 9] [1 8]
[1 2 3 4 6 7 8 9] [0 5]
[0 1 3 4 5 6 8 9] [2 7]
[0 1 2 3 5 6 7 8] [4 9]
[0 1 2 4 5 7 8 9] [3 6]

留一出 CV

原始 CV 实现使用留一出方法,每次将每个观测值一次用作验证集,如下面的代码所示:

loo = LeaveOneOut()
for train, validate in loo.split(data):
    print(train, validate)

[1 2 3 4 5 6 7 8 9] [0]
[0 2 3 4 5 6 7 8 9] [1]
...
[0 1 2 3 4 5 6 7 9] [8]
[0 1 2 3 4 5 6 7 8] [9]

这样做可以最大化训练的模型数量,增加计算成本。虽然验证集不重叠,但是训练集的重叠被最大化,推高了模型及其预测误差的相关性。因此,对于具有更多拆分的模型,预测误差的方差更高。

留-P-出 CV

与留一出 CV 类似的版本是留-P-出 CV,它生成所有可能的 p 数据行的组合,如下面的代码所示:

lpo = LeavePOut(p=2)
for train, validate in lpo.split(data):
    print(train, validate)

[2 3 4 5 6 7 8 9] [0 1]
[1 3 4 5 6 7 8 9] [0 2]
...
[0 1 2 3 4 5 6 8] [7 9]
[0 1 2 3 4 5 6 7] [8 9]

ShuffleSplit

sklearn.model_selection.ShuffleSplit 对象创建具有潜在重叠验证集的独立拆分,如下面的代码所示:

ss = ShuffleSplit(n_splits=3, test_size=2, random_state=42)
for train, validate in ss.split(data):
    print(train, validate)
[4 9 1 6 7 3 0 5] [2 8]
[1 2 9 8 0 6 7 4] [3 5]
[8 4 5 1 0 6 9 7] [2 3]

使用 scikit-learn 进行参数调整

模型选择通常涉及对使用不同算法(如线性回归和随机森林)或不同配置的模型的外样本性能进行重复交叉验证。不同的配置可能涉及对超参数的更改或包含或排除不同变量。

yellowbricks 库扩展了 sklearn API,生成诊断可视化工具,以便于模型选择过程。这些工具可用于调查特征之间的关系,分析分类或回归错误,监视聚类算法性能,检查文本数据的特性,并帮助进行模型选择。我们将演示提供有关参数调整阶段的有价值信息的验证和学习曲线—有关实现细节,请参阅 machine_learning_workflow.ipynb 笔记本。

使用 yellowbricks 的验证曲线

验证曲线(参见下图左侧面板)可视化单个超参数对模型交叉验证性能的影响。这有助于确定模型是否对给定数据集欠拟合或过拟合。

在我们的示例中,只有一个超参数的KNeighborsRegressor,我们可以清楚地看到,当k的值高于 20 时,模型欠拟合,其中验证错误随着减少邻居数量而下降,从而使我们的模型更复杂,因为它为更多不同的邻居组或特征空间中的区域进行预测。对于小于 20 的值,模型开始过拟合,因为训练和验证错误背离,平均外样本表现迅速恶化,如下图所示:

学习曲线

学习曲线(参见前述图表右侧面板,我们的房价回归示例)有助于确定模型的交叉验证性能是否会受益于更多数据,以及预测错误是更多地受偏差还是方差驱动。

如果训练和交叉验证性能趋于一致,那么更多的数据不太可能改善性能。在这一点上,重要的是评估模型性能是否符合人类基准的预期。如果不是这种情况,那么您应该修改模型的超参数设置,以更好地捕捉特征和结果之间的关系,或者选择一个具有更高复杂性捕捉能力的不同算法。

此外,阴影置信区间显示的训练和测试错误的变化提供了有关预测错误的偏差和方差来源的线索。交叉验证错误周围的变异性是方差的证据,而对训练集的变异性则暗示偏差,这取决于训练错误的大小。

在我们的例子中,交叉验证性能一直在下降,但增量改进已经减少,错误已经趋于稳定,因此从更大的训练集中很少会有很多好处。另一方面,鉴于验证错误的范围与训练错误相比显示出的范围相比,数据显示出相当大的方差。

使用 GridSearchCV 和 pipeline 进行参数调优

由于超参数调优是机器学习工作流的关键组成部分,有一些工具可以自动化这个过程。sklearn库包括一个GridSearchCV接口,该接口可以并行交叉验证所有参数组合,捕获结果,并自动使用在完整数据集上交叉验证中表现最佳的参数设置来训练模型。

实际上,在交叉验证之前,训练集和验证集通常需要进行一些处理。Scikit-learn 提供了Pipeline,以便在由GridSearchCV便利的自动化超参数调优中自动执行任何必要的特征处理步骤。

您可以查看所包含的machine_learning_workflow.ipynb笔记本中的实现示例,以查看这些工具的运行情况。

金融交叉验证中的挑战

迄今为止讨论的交叉验证方法的一个关键假设是训练样本的独立且相同(iid)分布。

对于金融数据,情况通常并非如此。相反,金融数据既不独立也不同分布,因为存在序列相关性和时变标准差,也称为异方差性(有关更多详细信息,请参阅接下来的两章)。sklearn.model_selection模块中的TimeSeriesSplit旨在解决时间序列数据的线性顺序。

使用 sklearn 进行时间序列交叉验证

数据的时间序列性意味着交叉验证会产生这样一种情况,即未来的数据将用于预测过去的数据。这充其量是不现实的,最坏的情况下是数据窥探,因为未来数据反映了过去事件的程度。

为了解决时间依赖性,sklearn.model_selection.TimeSeriesSplit对象实现了一个具有扩展训练集的前向测试,其中后续训练集是过去训练集的超集,如下面的代码所示:

tscv = TimeSeriesSplit(n_splits=5)
for train, validate in tscv.split(data):
    print(train, validate)

[0 1 2 3 4] [5]
[0 1 2 3 4 5] [6]
[0 1 2 3 4 5 6] [7]
[0 1 2 3 4 5 6 7] [8]
[0 1 2 3 4 5 6 7 8] [9]

你可以使用max_train_size参数来实现前向交叉验证,其中训练集的大小随时间保持不变,类似于zipline测试交易算法的方式。Scikit-learn 通过子类化方便地设计自定义交叉验证方法,我们将在接下来的章节中实现。

清除、禁止和组合交叉验证

对于金融数据,标签通常是从重叠的数据点派生的,因为收益率是从多个期间的价格计算出来的。在交易策略的背景下,模型的预测结果,可能意味着对某种资产的持仓,只有在稍后,当这个决定被评估时,才能知道,例如,当一个头寸被平仓时。

由此产生的风险包括信息从测试集泄漏到训练集,可能导致人为膨胀的性能,需要通过确保所有数据都是时点性的来解决,也就是说,在用作模型输入时真正可用和已知的时间。马科斯·洛佩斯·德·普拉多在《金融机器学习进展》中提出了几种方法来解决金融数据在交叉验证中的这些挑战,如下所示:

  • 清洗:消除训练数据点,其中评估发生在验证集中一个时间点数据点的预测之后,以避免前瞻性偏差。

  • 禁运:进一步消除跟随测试期间的训练样本。

  • 组合交叉验证:向前步进 CV 严重限制了可以测试的历史路径。相反,给定 T 个观测值,为 N<T 个组计算所有可能的训练/测试拆分,每个组都保持其顺序,并清洗和禁运可能重叠的组。然后,在所有 N-k 组合上训练模型,同时在剩余的 k 组上测试模型。结果是可能历史路径的数量大大增加。

普拉多的《金融机器学习进展》包含了实现这些方法的示例代码;该代码也可以通过新库timeseriescv获得。

总结

在本章中,我们介绍了从数据中学习的挑战,并将监督、无监督和强化模型作为我们将在本书中研究的主要学习形式,以构建算法交易策略。我们讨论了监督学习算法需要对它们试图学习的功能关系进行假设,以限制搜索空间,同时产生归纳偏差,可能导致过度泛化误差。

我们介绍了 ML 工作流程的关键方面,介绍了回归和分类模型的最常见错误度量标准,解释了偏差-方差折衷,并说明了使用交叉验证管理模型选择过程的各种工具。

在接下来的章节中,我们将深入探讨线性模型用于回归和分类,以开发我们的第一个使用 ML 的算法交易策略。

第七章:线性模型

线性模型家族代表了最有用的假设类之一。许多在算法交易中广泛应用的学习算法依赖于线性预测器,因为它们在许多情况下可以被有效地训练,在金融数据嘈杂的情况下相对稳健,并且与金融理论有着密切的联系。线性预测器也直观易懂,易于解释,并且通常能够很好地拟合数据,或者至少提供一个良好的基线。

线性回归已经有 200 多年的历史了,当勒让德和高斯将其应用于天文学并开始分析其统计性质时。此后,许多扩展都改编了线性回归模型和基线普通最小二乘法OLS)方法来学习其参数:

  • 广义线性模型GLM)通过允许响应变量表示除正态分布之外的误差分布来扩展应用范围。GLM 包括用于分类问题中的分类响应变量的 probit 或 logistic 模型。

  • 更多稳健估计方法使得在数据违反基线假设的情况下进行统计推断成为可能,例如,随时间或跨观测存在相关性。这在包含对相同单位的重复观测的面板数据中经常发生,例如,对一组资产的历史回报。

  • 收缩方法旨在改善线性模型的预测性能。它们使用一个复杂度惩罚,偏向于通过减少模型的方差并提高样本外预测性能来改善模型学习的系数。

在实践中,线性模型被应用于推断和预测的回归和分类问题。学术界和工业界研究人员开发的众多资产定价模型利用了线性回归。应用包括识别驱动资产回报的重要因素,例如,作为风险管理的基础,以及对各种时间范围内的回报进行预测。另一方面,分类问题包括方向性价格预测。

在本章中,我们将涵盖以下主题:

  • 线性回归的工作原理及其假设条件

  • 如何训练和诊断线性回归模型

  • 如何使用线性回归预测未来收益

  • 如何使用正则化来提高预测性能

  • logistic 回归的工作原理

  • 如何将回归转换为分类问题

对于代码示例、额外资源和参考资料,请参阅在线 GitHub 仓库中本章的目录。

用于推断和预测的线性回归

顾名思义,线性回归模型假设输出是输入的线性组合的结果。该模型还假设存在随机误差,允许每个观察值偏离预期的线性关系。模型不能完美描述输入和输出之间关系的原因包括,例如,缺少变量,测量或数据收集问题。

如果我们想根据从样本估计的回归参数来对人口中的真实(但未观察到的)线性关系进行统计推断,我们需要对这些误差的统计性质添加假设。基线回归模型做出了这样一个强假设,即错误的分布在错误之间是相同的,并且错误是彼此独立的,也就是说,知道一个错误不会有助于预测下一个错误。独立同分布iid)错误的假设意味着它们的协方差矩阵是由一个代表错误方差的常数与单位矩阵相乘得到的。

这些假设保证了 OLS 方法提供的估计值不仅是无偏的,而且是有效的,即它们具有最低的抽样误差学习算法。然而,在实践中很少满足这些假设。在金融领域,我们经常遇到具有给定截面上重复观察的面板数据。试图估计一组资产对一组风险因素的系统性暴露随时间变化的努力通常会导致时间维度或截面维度中的相关性,或者两者兼有。因此,出现了更多假设与单位矩阵的多个倍数不同的错误协方差矩阵的替代学习算法。

另一方面,学习为线性模型学习有偏参数的方法可能会产生方差较低的估计值,从而提高预测性能。收缩方法通过在线性目标函数中添加惩罚项来降低模型复杂性。惩罚与系数的绝对值大小正相关,因此相对于基线情况,这些系数被收缩。更大的系数意味着更复杂的模型,对输入的变化反应更强烈。正确校准的惩罚可以限制模型系数的增长,超出了最佳偏差-方差权衡建议的范围。

我们将介绍线性模型的基线截面和面板技术以及在关键假设被违反时产生准确估计的重要增强技术。然后,我们将通过估计在算法交易策略开发中无处不在的因子模型来说明这些方法。最后,我们将重点介绍正则化方法。

多元线性回归模型

我们将介绍模型的规范和目标函数、学习其参数的方法、允许推断和诊断这些假设的统计假设,以及扩展以适应这些假设失败情况的模型。

如何构建模型

多元回归模型定义了一个连续的结果变量与p个输入变量之间的线性函数关系,这些输入变量可以是任何类型,但可能需要预处理。相比之下,多元回归是指多个输出在多个输入变量上的回归。

在总体中,线性回归模型对于输出y的单个实例、输入向量 和误差ε具有以下形式:

系数的解释很简单:系数  的值是变量x[i]对输出的部分平均影响,在所有其他变量保持不变的情况下。

该模型也可以更简洁地以矩阵形式书写。在这种情况下,yN个输出观测值的向量,X是设计矩阵,其中有N行观测值和p个变量,另外还有一列 1 作为截距,而  是包含p+1个系数的向量 

该模型在其p+1个参数中是线性的,但可以通过相应地选择或转换变量来建模非线性关系,例如通过包含多项式基扩展或对数项。它还可以使用具有虚拟编码的分类变量,并通过创建形如x[i] . x[j]的新输入来对变量之间的交互进行建模。

从统计学角度完善模型的构建,以便我们可以测试关于参数的假设,我们需要对误差项进行具体的假设。我们将在首先介绍学习参数的替代方法之后做这个。

如何训练模型

有几种方法可以从数据中学习模型参数  :普通最小二乘法OLS)、最大似然估计MLE)和随机梯度下降SGD)。

最小二乘法

最小二乘法是学习最能够近似输入数据输出的超平面参数的原始方法。顾名思义,最佳近似将最小化输出值与模型表示的超平面之间的平方距离之和。

给定数据点的模型预测与实际结果之间的差异是残差(而真实模型与真实输出在总体中的偏差被称为误差)。因此,在正式术语中,最小二乘估计方法选择系数向量  以最小化残差平方和RSS):

因此,最小二乘系数  计算如下:

最小化 RSS 的最优参数向量是通过将前述表达式对  的导数置零而得到的。这产生了一个唯一的解,假设 X 具有完整的列秩,即输入变量不是线性相关的,如下所示:

y 和 X 通过减去它们各自的均值而被去均值时, 表示输入和输出  之间协方差的比率和输出方差 。还有一种几何解释:最小化 RSS 的系数确保  的向量与由 X 的列张成的子空间  是正交的,并且  是该子空间的正交投影。

最大似然估计

MLE 是估计统计模型参数的重要一般方法。它依赖于似然函数,该函数计算观察到的输出值样本在给定一组输入数据的情况下作为模型参数的函数的可能性。似然性与概率的不同之处在于它不被标准化为从 0 到 1 的范围。

我们可以通过假设误差项的分布(如标准正态分布)为线性回归示例设置似然函数:

.

这使我们能够计算给定输出  在相应输入向量 x[i] 和参数  的条件概率:

假设输出值在给定输入条件下是独立的,样本的似然性与单个输出数据点的条件概率的乘积成比例。由于使用和比使用乘积更容易,我们应用对数来获得对数似然函数:

MLE 的目标是通过选择模型参数最大化实际观察到的输出样本的概率,将观察到的输入视为给定。因此,MLE 参数估计结果来自于最大化(对数)似然函数:

由于正态分布的假设,最大化对数似然函数产生与最小二乘相同的参数解,因为仅有一个表达式与参数有关,即指数中的平方残差。对于其他分布假设和模型,MLE 将产生不同的结果,在许多情况下,最小二乘不适用,我们稍后将在逻辑回归中看到。

梯度下降(Gradient descent)

梯度下降是一种通用的优化算法,将找到平滑函数的稳定点。如果目标函数是凸的,则解将是全局最优解。梯度下降的变体被广泛用于训练复杂的神经网络,也用于计算 MLE 问题的解决方案。

算法使用目标函数的梯度,其中包含相对于参数的偏导数。这些导数指示了在相应参数方向上的微小步长中目标变化的程度。结果表明,函数值的最大变化来自于梯度方向的步长。

因此,当最小化描述例如预测误差成本的函数时,该算法使用训练数据为当前参数值计算梯度,并根据对应梯度分量的负值修改每个参数。结果,目标函数将取得较低的值,并使参数更接近解。当梯度变得很小时,参数值几乎不再变化时,优化停止。

这些步长的大小是学习率,这是一个可能需要调整的关键参数;许多实现包括逐渐增加学习率的选项,随着迭代次数的增加。根据数据的大小,算法可能会多次迭代整个数据集。每次迭代称为一个周期(epoch)。您可以调整的超参数包括周期数和用于停止进一步迭代的容差。

随机梯度下降(Stochastic gradient descent)随机选择一个数据点,并针对该数据点计算梯度,而不是对更大样本的平均值,以实现加速。还有批量版本,每个步骤使用一定数量的数据点。

高斯-马尔科夫定理

要评估模型的统计特性并进行推断,我们需要对残差(即输入未解释部分的属性)做出假设。高斯-马尔科夫定理GMT)定义了 OLS 需要的假设,以产生模型参数的无偏估计!,并且当这些估计在横截面数据的所有线性模型中具有最低标准误差时。

基线多元回归模型做出以下 GMT 假设:

  1. 在总体中,线性成立,,其中是未知但恒定的,而是一个随机误差

  2. 输入变量的数据是来自总体的随机样本

  3. 没有完美的共线性 - 输入变量之间不存在确切的线性关系

  4. 错误在给定任何输入时的条件均值为零:

  5. 同方差性,误差项在给定输入时具有恒定方差:

第四个假设意味着不存在与任何输入变量相关的缺失变量。在前四个假设下,OLS 方法提供无偏估计:包括一个无关变量不会使截距和斜率估计产生偏差,但忽略一个相关变量会使 OLS 估计产生偏差。然后 OLS 也是一致的:随着样本量的增加,估计值收敛到真实值,标准误差变得任意。不幸的是,反之亦然:如果误差的条件期望不为零,因为模型遗漏了一个相关变量或者功能形式错误(即,缺少二次项或对数项),那么所有参数估计都是有偏的。如果误差与任何输入变量相关,则 OLS 也不一致,即添加更多数据不会消除偏差。

如果添加了第五个假设,则 OLS 也会产生最佳线性无偏估计(BLUE),其中最佳意味着估计在所有线性估计器中具有最低标准误差。因此,如果五个假设成立,并且统计推断是目标,那么 OLS 估计是正确的选择。然而,如果目标是预测,那么我们将看到在许多情况下存在其他估计器,它们在一定的偏差情况下换取更低的方差以实现更好的预测性能。

现在我们已经介绍了基本的 OLS 假设,我们可以看看小样本和大样本推断。

如何进行统计推断

在线性回归背景下的推断旨在从样本数据中得出关于总体真实关系的结论。这包括关于总体关系的显著性或特定系数值的假设检验,以及置信区间的估计。

统计推断的关键要素是一个具有已知分布的检验统计量。我们可以假设零假设成立,并计算在样本中观察到该统计量值的概率,这个概率被称为 p 值。如果 p 值低于显著性阈值(通常为五百分之一),那么我们拒绝该假设,因为它使得实际样本值非常不可能。同时,我们接受 p 值反映了我们在拒绝实际上是正确的假设时错误的概率。

除了五个 GMT 假设之外,经典线性模型还假设正态性—总体误差服从正态分布且与输入变量独立。这一假设意味着在输入变量条件下,输出变量服从正态分布。这个强假设允许导出系数的精确分布,进而意味着在小样本中需要的测试统计的精确分布。这一假设通常会失败—例如,资产回报不服从正态分布—但是,幸运的是,正态性下使用的方法也是近似有效的。

我们拥有以下分布特征和测试统计量,近似地满足 GMT 假设 1–5,当正态性成立时则完全满足:

  • 参数估计服从多元正态分布:  。

  • 在 GMT 1–5 下,参数估计已经是无偏的,我们可以使用  对  进行无偏估计,即常数误差方差。

  • 对于关于个别系数的假设检验,t 统计量为 ,并且服从于自由度为 N-p-1t 分布,其中  是  对角线的第 j 个元素。

  • t 分布收敛于正态分布,而正态分布的 97.5 分位数为 1.96,因此,围绕参数估计的 95% 置信区间的一个有用的经验法则是 。包含零的区间意味着我们无法拒绝真实参数为零的零假设,因此对模型无关。

  • F统计量允许对多个参数的限制进行检验,包括整个回归是否显著。它测量了 RSS 的变化(减少),这是由于添加额外变量而导致的。

  • 最后,拉格朗日乘数(LM)测试是对F测试的替代,以限制多个限制。

如何诊断和解决问题

诊断验证模型假设,并在解释结果和进行统计推断时防止错误的结论。它们包括拟合优度的测量和关于误差项假设的各种测试,包括残差与正态分布的吻合程度。此外,诊断测试残差方差是否确实恒定,或者是否存在异方差,并且错误是否有条件地不相关或者存在串联相关性,即,知道一个错误是否有助于预测连续的错误。

除了以下概述的测试之外,始终重要的是可视化检查残差,以检测是否存在系统模式,因为这些指示模型缺少一个或多个驱动结果的因素。

拟合优度

拟合优度测量评估模型对结果变异的解释程度。它们有助于评估模型规范的质量,例如,在不同的模型设计之间进行选择。它们在评估拟合度时有所不同。在这里讨论的测量提供样本内信息;当我们在下一节专注于预测模型时,我们将使用样本外测试和交叉验证。

突出的拟合优度测量包括(调整后的),应该最大化,基于最小二乘估计:

  • 度量了模型解释结果数据变异的份额,并计算为,其中 TSS 是结果与其均值的平方偏差之和。它还对应于实际结果值与模型估计(拟合)值之间的平方相关系数。目标是最大化,但随着模型增加更多变量,它永远不会减少,因此会鼓励过度拟合。

  • 调整后的对于增加更多变量对进行惩罚;每个额外变量都需要显著减少 RSS,以产生更好的拟合优度。

或者,需要最小化的是Akaike(AIC)贝叶斯信息准则(BIC),它们基于最大似然估计:

  • ,其中是最大化似然函数的值,k 是参数个数

  • ,其中N是样本量

这两个指标都对复杂性进行了惩罚,BIC 施加了更高的惩罚,因此可能会欠拟合,而 AIC 可能会过拟合相对于 BIC 而言。从概念上讲,AIC 旨在找到最佳描述未知数据生成过程的模型,而 BIC 试图在候选模型集中找到最佳模型。在实践中,当目标是样本内拟合时,可以联合使用这两个标准来引导模型选择;否则,基于交叉验证和泛化误差估计的选择更可取。

异方差性

GMT 假设 5 要求残差协方差采用形状 ,即,一个对角矩阵,其条目等于误差项的恒定方差。异方差性发生在残差方差不恒定但在观察之间不同的情况下。如果残差方差与输入变量呈正相关,即,当误差较大时,与其平均值相差较远的输入值,则 OLS 标准误差估计将过低,因此 t 统计量将被夸大,导致在实际上不存在关系的情况下发现虚假关系。

诊断从对残差进行视觉检查开始。残差中的系统模式表明了针对误差同方差性的零假设进行统计检验的多种替代方案。这些测试包括 Breusch—Pagan 和 White 测试。

有几种方法可以校正 OLS 估计的异方差性:

  • 鲁棒标准误差(有时称为白色标准误差)在计算误差方差时考虑了异方差性,使用所谓的三明治估计器。

  • 聚类标准误差假设您的数据中存在不同的组,这些组在同方差但错误方差在组之间不同。这些组可以是不同的资产类别或来自不同行业的股票。

有几种替代方法可使用不同的假设来估计误差协方差矩阵,当时。以下是statsmodels中提供的选项:

  • 加权最小二乘法 (WLS):适用于异方差误差,其中协方差矩阵仅具有对角元素,如 OLS,但现在允许元素变化。

  • 可行广义最小二乘法 (GLSAR),用于遵循自回归 AR(p)过程的自相关误差(请参阅线性时间序列模型章节)

  • 广义最小二乘法 (GLS) 适用于任意协方差矩阵结构;在存在异方差性或序列相关性时产生高效且无偏的估计。

序列相关性

串行相关意味着线性回归产生的连续残差是相关的,这违反了第四个 GMT 假设。正串行相关意味着标准误差被低估,t-统计量将被夸大,如果忽略这一点,可能导致错误的发现。然而,在计算标准误差时,有纠正串行相关的程序。

Durbin-Watson 统计量诊断串行相关。它检验 OLS 残差不自相关的假设,而不是按照自回归过程(我们将在下一章中探讨)的替代。测试统计量的范围从 0 到 4,接近 2 的值表明非自相关,较低的值表示正,较高的值表示负的自相关。确切的阈值值取决于参数和观测值的数量,需要在表中查找。

多重共线性

当两个或更多自变量高度相关时,就会出现多重共线性。这带来了几个挑战:

  • 很难确定哪些因素影响因变量。

  • 单个 p 值可能会误导——即使变量很重要,p 值可能也很高。

  • 回归系数的置信区间可能过大,甚至可能包括零,这使得无法确定自变量对结果的影响。

没有正式的或基于理论的解决方案来纠正多重共线性。而是尝试删除一个或多个相关的输入变量,或增加样本量。

如何实践运行线性回归

附带的笔记本 linear_regression_intro.ipynb 展示了简单线性回归和多重线性回归,后者使用 OLS 和梯度下降两种方法。对于多元回归,我们生成两个随机输入变量 x[1]x[2],范围从-50 到+50,并计算结果变量作为输入的线性组合加上随机高斯噪声以满足正态性假设 GMT 6:

使用 statsmodels 进行 OLS

我们使用 statsmodels 来估计准确反映数据生成过程的多元回归模型,如下所示:

from statsmodels.api import 
X_ols = add_constant(X)
model = OLS(y, X_ols).fit()
model.summary()

下面是OLS 回归结果的摘要:

OLS 回归结果摘要

摘要的上半部分显示了数据集的特征,即估计方法、观测值和参数的数量,并指出标准误差估计不考虑异方差性。中间面板显示了与人工数据生成过程密切相关的系数值。我们可以确认,摘要结果中间显示的估计值可以使用先前推导的 OLS 公式获得:

beta = np.linalg.inv(X_ols.T.dot(X_ols)).dot(X_ols.T.dot(y))
pd.Series(beta, index=X_ols.columns)

const   50.94
X_1      1.08
X_2      2.93

以下图示了模型对随机生成的数据点拟合的超平面:

超平面

面板的右上部显示了刚讨论过的拟合优度指标,以及拒绝所有系数为零且不相关的假设的 F 检验。同样,t 统计量表明截距和斜率系数都是非常显著的,这并不令人意外。

摘要底部包含了残差诊断。左侧面板显示了用于检验正态性假设的偏度和峰度。Omnibus 和 Jarque—Bera 测试都未能拒绝残差正态分布的零假设。Durbin—Watson 统计量检验残差的序列相关性,并且在值接近 2 的情况下,考虑到 2 个参数和 625 个观测值,未能拒绝无序列相关性的假设。

最后,条件数提供了关于多重共线性的证据:它是包含输入数据的设计矩阵的最大和最小特征值的平方根的比值。值超过 30 表明回归可能存在显著的多重共线性。

statsmodels包含了与笔记本中链接的其他诊断测试。

使用 sklearn 的随机梯度下降

sklearn库在其linear_models模块中包含了一个SGDRegressor模型。要使用该方法学习相同模型的参数,我们需要首先标准化数据,因为梯度对尺度敏感。我们使用StandardScaler()来计算每个输入变量的平均值和标准差,并在拟合步骤中减去平均值并除以标准差,在转换步骤中进行方便的单个fit_transform()命令:

scaler = StandardScaler()
X_ = scaler.fit_transform(X)

然后,我们使用默认值实例化SGDRegressor,除了设置random_state以便复制:

sgd = SGDRegressor(loss='squared_loss', fit_intercept=True, 
                   shuffle=True, random_state=42,  # shuffle training data for better gradient estimates
                   learning_rate='invscaling',     # reduce learning rate over time
                   eta0=0.01, power_t=0.25)        # parameters for learning rate path

现在我们可以拟合sgd模型,为 OLS 模型和sgd模型创建样本内预测,并计算每个模型的均方根误差:

sgd.fit(X=X_, y=y)
resids = pd.DataFrame({'sgd': y - sgd.predict(X_),
                      'ols': y - model.predict(sm.add_constant(X))})
resids.pow(2).sum().div(len(y)).pow(.5)

ols   50.06
sgd   50.06

如预期的那样,两个模型产生相同的结果。我们现在将承担一个更雄心勃勃的项目,使用线性回归来估计多因子资产定价模型。

如何构建线性因子模型

算法交易策略使用线性因子模型来量化资产收益与代表这些收益主要驱动因素的风险来源之间的关系。每个因子风险都带有一个风险溢价,总资产收益可以预期对应于这些风险溢价的加权平均。

因子模型在投资组合管理过程中有多个实际应用,从构建和资产选择到风险管理和绩效评估。随着常见风险因素现在可以交易,因子模型的重要性不断增长:

  • 许多资产的回报摘要通过少量因子来减少在优化投资组合时估算协方差矩阵所需的数据量

  • 对资产或投资组合对这些因子的敞口的估计允许管理由此产生的风险,例如当风险因子本身被交易时,可以通过输入适当的对冲来管理风险

  • 因子模型还允许评估新α因子的增量信号内容。

  • 因子模型还可以帮助评估经理相对于基准的业绩是否确实是由于选择资产和市场时机的技能,或者是否业绩可以解释为投资组合倾向于已知回报驱动因素,这些因素今天可以以低成本、被动管理的基金形式复制,而无需支付主动管理费用。

以下示例适用于股票,但已为所有资产类别确定了风险因子(请参阅 GitHub 存储库中的参考资料)。

从资本资产定价模型到法玛-法国五因子模型

自从资本资产定价模型(CAPM)解释了所有N资产的预期回报,即使用它们对单一因子的各自暴露!到整体市场相对于无风险利率的预期超额回报!以来,风险因子一直是定量模型的关键因素。该模型采用以下线性形式:

这与道德和格雷厄姆经典的基本分析有所不同,后者依赖于公司特征来确定回报。理由是,在总体上,投资者无法通过分散化消除这种所谓的系统风险。因此,在均衡状态下,他们要求持有资产的补偿与其系统风险相称。该模型暗示,在市场有效的情况下,价格立即反映所有公开信息,就不应该有优越的风险调整回报,也就是说,的价值应该为零。

该模型的实证测试使用线性回归,并一直失败,促使人们争论是市场有效还是联合假设的单一因子方面有问题。事实证明,这两个前提都可能是错误的:

  • 约瑟夫·斯蒂格利茨在部分地获得了 2001 年诺贝尔经济学奖,因为他表明市场通常并非完全有效:如果市场有效,那么收集数据就没有价值,因为这些信息已经反映在价格中。但是,如果没有收集信息的动机,很难看到它如何已经反映在价格中。

  • 另一方面,对 CAPM 的理论和实证改进表明,额外因素有助于解释一些不依赖于整体市场敞口的超常风险调整回报的异常,例如较小公司的较高回报。

斯蒂芬·罗斯于 1976 年提出了套利定价理论APT),作为一种允许多种风险因素存在的替代方案,同时避免了市场效率。与 CAPM 相比,它假设由于定价错误而产生的超常回报机会可能存在,但会迅速被套利消除。该理论并未明确规定这些因素,但作者的研究表明,最重要的因素是通货膨胀和工业生产的变化,以及风险溢价或利率期限结构的变化。

肯尼斯·弗伦奇和尤金·法玛(获得 2013 年诺贝尔奖)确定了取决于公司特征的额外风险因素,并且今天被广泛使用。1993 年,法玛—弗伦奇三因子模型在单一 CAPM 风险的基础上增加了公司的相对规模和价值。2015 年,五因子模型进一步扩展了该集合,包括在介入年份已被证明显著的公司盈利能力和投资水平。此外,许多因子模型包括价格动量因子。

法玛—弗伦奇风险因素的计算是指根据反映给定风险因素的度量标准对多样化投资组合的回报差异。通过根据这些度量标准对股票进行排序,然后对超过某一百分位数的股票做多头,对低于某一百分位数的股票做空头,获得这些回报。与风险因素相关的度量标准定义如下:

  • 大小: 市场权益ME

  • 价值: 股权账面价值BE)除以 ME

  • 运营盈利能力(OP):收入减去销售成本/资产

  • 投资: 投资/资产

还有无监督学习技术用于基于数据的风险因素的发现,包括基于因子和主成分分析的方法,我们将在第十二章,无监督学习中进行探讨。

获取风险因素

法玛和弗伦奇通过他们的网站提供了更新的风险因素和研究投资组合数据,您可以使用pandas_datareader库获取这些数据。对于此应用,请参考fama_macbeth.ipynb笔记本以获取更多详细信息。

特别是,我们将使用从首先将股票分成三个规模组,然后对剩余三个公司特定因素中的每个进行两次排序的五个法玛—弗伦奇因子。因此,这些因素涉及根据大小和账面市值、大小和经营盈利能力以及大小和投资进行的 3 x 2 排序形成的三组加权价值组合。风险因素值计算为如下表所述的投资组合PF)的平均回报:

概念 标签 名称 风险因子计算
规模 SMB 小减大 九个小型股票 PF 减去九个大型股票 PF
HML 高减低 两个价值 PF 减去两个增长 PF(具有低 BE/ME 值)
盈利能力 RMW 强大减弱 两个强劲 OP PF 减去两个弱 OP PF
投资 CMA 保守减激进 两个保守投资组合减去两个激进投资组合
市场 Rm-Rf 市场的超额回报 所有在主要美国交易所上市和上市的公司的价值加权回报,具有良好数据减去一个月期国库券利率

我们将使用从 2010 年至 2017 年期间获得的月频率的回报如下:

import pandas_datareader.data as web
ff_factor = 'F-F_Research_Data_5_Factors_2x3'
ff_factor_data = web.DataReader(ff_factor, 'famafrench', start='2010', end='2017-12')[0]
ff_factor_data.info()

PeriodIndex: 96 entries, 2010-01 to 2017-12
Freq: M
Data columns (total 6 columns):
Mkt-RF 96 non-null float64
SMB 96 non-null float64
HML 96 non-null float64
RMW 96 non-null float64
CMA 96 non-null float64
RF 96 non-null float64

Fama 和 French 还提供了许多我们可以用来说明因子暴露估计的投资组合,以及市场上某个特定时间段可用的风险溢价的价值。我们将使用一个包含 17 个行业投资组合的面板,并以月频率进行计算。我们将从回报中减去无风险利率,因为因子模型使用超额回报:

ff_portfolio = '17_Industry_Portfolios'
ff_portfolio_data = web.DataReader(ff_portfolio, 'famafrench', start='2010', end='2017-12')[0]
ff_portfolio_data = ff_portfolio_data.sub(ff_factor_data.RF, axis=0)
ff_factor_data = ff_factor_data.drop('RF', axis=1)
ff_portfolio_data.info()

PeriodIndex: 96 entries, 2010-01 to 2017-12
Freq: M
Data columns (total 17 columns):
Food     96 non-null float64
Mines    96 non-null float64
Oil      96 non-null float64
...
Rtail    96 non-null float64
Finan    96 non-null float64
Other    96 non-null float64

现在,我们将基于这些面板数据构建一个线性因子模型,使用一种解决一些基本线性回归假设失败的方法。

Fama—Macbeth 回归

鉴于风险因子和投资组合回报的数据,估计投资组合的暴露是有用的,即风险因子驱动投资组合回报的程度,以及给定因子的暴露值的价值,即市场的风险因子溢价是多少。然后,风险溢价可以估算任何投资组合的回报,前提是已知或可以假定因子暴露。

更正式地说,我们将对t=1, ..., T期间的i=1, ..., N资产或投资组合的回报进行测试,每个资产的超额期间回报将被表示为。目标是测试j=1, ..., M因子是否解释了超额回报以及与每个因子相关的风险溢价。在我们的案例中,我们有N=17个投资组合和M=5个因子,每个因子都有 96 个数据周期。

因子模型是针对给定期间内许多股票进行估计的。在这种横截面回归中可能会出现推断问题,因为经典线性回归的基本假设可能不成立。潜在的违规行为包括测量误差,由异方差和串行相关引起的残差协方差,以及多重共线性。

为了解决残差相关引起的推断问题,Fama 和 MacBeth 提出了一种两步法的方法,用于对因子上的横截面回归。两阶段的 Fama—Macbeth 回归旨在估计市场对特定风险因子暴露所奖励的溢价。两个阶段包括:

  • 第一阶段N时间序列回归,对于每个资产或投资组合,它的超额收益对因子进行回归,以估计因子载荷。以矩阵形式,对于每个资产:

  • 第二阶段:T 个横截面回归,每个时间段一个,以估计风险溢价。以矩阵形式,我们获得每个时间段的风险溢价向量

现在我们可以将因子风险溢价计算为时间平均值,并使用假设风险溢价估计在时间上独立来获取 t 统计量,评估它们的个体显著性:

如果我们拥有一个非常大且具代表性的交易风险因子数据样本,我们可以将样本均值用作风险溢价估计。然而,我们通常没有足够长的历史记录,而且样本均值周围的误差范围可能相当大。法马—麦克贝斯方法利用因子与其他资产的协方差来确定因子溢价。资产收益的二阶矩比第一阶矩更容易估计,并且获得更细粒度的数据会显着改善估计,这在均值估计中并不成立。

我们可以实现第一阶段,以获取 17 个因子载荷估计,如下所示:

betas = []
for industry in ff_portfolio_data:
    step1 = OLS(endog=ff_portfolio_data[industry],
                exog=add_constant(ff_factor_data)).fit()
    betas.append(step1.params.drop('const'))

betas = pd.DataFrame(betas,
                     columns=ff_factor_data.columns,
                     index=ff_portfolio_data.columns)
betas.info()
Index: 17 entries, Food  to Other
Data columns (total 5 columns):
Mkt-RF    17 non-null float64
SMB       17 non-null float64
HML       17 non-null float64
RMW       17 non-null float64
CMA       17 non-null float64

对于第二阶段,我们对横截面投资组合的期间收益进行 96 次回归,以估计因子载荷:

lambdas = []
for period in ff_portfolio_data.index:
    step2 = OLS(endog=ff_portfolio_data.loc[period, betas.index],
                exog=betas).fit()
    lambdas.append(step2.params)

lambdas = pd.DataFrame(lambdas,
                       index=ff_portfolio_data.index,
                       columns=betas.columns.tolist())
lambdas.info()
PeriodIndex: 96 entries, 2010-01 to 2017-12
Freq: M
Data columns (total 5 columns):
Mkt-RF    96 non-null float64
SMB       96 non-null float64
HML       96 non-null float64
RMW       96 non-null float64
CMA       96 non-null float64

最后,我们计算 96 个周期的平均值,以获取我们的因子风险溢价估计值:

lambdas.mean()
Mkt-RF    1.201304
SMB       0.190127
HML      -1.306792
RMW      -0.570817
CMA      -0.522821

linear_models库通过各种面板数据模型扩展了statsmodels,并且还实现了两阶段法马—麦克贝斯程序:

model = LinearFactorModel(portfolios=ff_portfolio_data, 
                          factors=ff_factor_data)
res = model.fit()

这为我们提供了相同的结果:

线性因子模型估计摘要

随附的笔记本通过在估计较大的个别股票风险溢价时使用行业虚拟变量来说明了分类变量的使用。

缩小方法:线性回归的正则化

当高斯—马尔可夫假设得到满足时,最小二乘法用于训练线性回归模型将会产生最佳、线性和无偏的系数估计。即使 OLS 关于误差协方差矩阵的假设被违反,类似 GLS 的变种也会表现得相当好。然而,有一些估计器会产生偏斜的系数,以减少方差以达到更低的总体泛化误差。

当线性回归模型包含许多相关变量时,它们的系数将被较差地确定,因为大正系数对 RSS 的影响可能会被相关变量上的同样大的负系数抵消。因此,由于系数的这种摆动空间,模型将具有高方差的倾向,增加模型过度拟合样本的风险。

如何对抗过度拟合

控制过拟合的一种流行技术是正则化,它涉及将惩罚项添加到误差函数中,以阻止系数达到较大的值。换句话说,对系数的大小施加约束可以缓解结果对样本外预测的潜在负面影响。由于过拟合是如此普遍的问题,我们将在所有模型中遇到正则化方法。

在本节中,我们将介绍缩小方法,以解决改进迄今为止讨论的线性模型方法的两个动机:

  • 预测准确性:最小二乘估计的低偏差但高方差表明,通过收缩或将某些系数设置为零,可以减少泛化误差,从而在略微增加偏差的同时减少模型的方差。

  • 解释性:大量的预测变量可能会使结果的解释或传达变得复杂化。牺牲一些细节以限制模型只包括具有最强效果的参数子集可能更可取。

收缩模型通过对其大小施加惩罚来限制回归系数。这些模型通过向目标函数添加一个项来实现此目标,使得收缩模型的系数最小化 RSS 加上一个与系数(绝对值)大小正相关的惩罚项。添加的惩罚将线性回归系数的查找转变为一个约束最小化问题,通常采用以下拉格朗日形式:

正则化参数 λ 决定了惩罚效果的大小,即正则化的强度。一旦 λ 为正,系数将与无约束的最小二乘参数不同,这意味着一个有偏的估计。超参数 λ 应该通过交叉验证来自适应地选择,以最小化预测误差的估计。

收缩模型的区别在于它们如何计算惩罚,即 S 的函数形式。最常见的版本是岭回归,它使用了系数平方的和,而套索模型则基于系数绝对值的和来设置惩罚。

岭回归的工作原理

岭回归通过将惩罚添加到目标函数中来缩小回归系数,该惩罚等于系数的平方和,进而对应于系数向量的 L² 范数:

因此,岭回归系数定义如下:

截距已从惩罚中排除,以使程序独立于为输出变量选择的原点——否则,将常数添加到所有输出值将改变所有斜率参数而不是平行移位。

重要的是通过从每个输入中减去相应的均值并将结果除以输入的标准差来标准化输入,因为岭解对输入的尺度敏感。岭估计器也有一个类似 OLS 情况的闭式解:

在求逆之前,解决方案将缩放后的单位矩阵λI添加到XTX*中,这保证了问题是非奇异的,即使*XT**X没有满秩。这是最初引入该估计量时的动机之一。

岭惩罚导致所有参数的成比例收缩。在正交输入的情况下,岭估计只是最小二乘估计的缩放版本,即:

使用输入矩阵X奇异值分解SVD),我们可以深入了解在更常见的情况下,收缩如何影响非正交输入。中心矩阵的 SVD 表示矩阵的主成分(参见第十一章,梯度提升机,有关无监督学习的内容),按方差降序捕获数据的列空间中的不相关方向。

岭回归会收缩与数据中方差较小的方向相关联的输入变量的系数,而不是收缩与展现更多方差的方向相关联的输入变量的系数。因此,岭回归的隐含假设是,在数据中变化最大的方向在预测输出时最有影响力或最可靠。

套索回归的工作原理

套索,在信号处理中称为基 Pursuit,通过向残差的平方和添加惩罚来缩小系数,但套索惩罚具有稍微不同的效果。套索惩罚是系数向量的绝对值的总和,对应于其 L¹范数。因此,套索估计由以下方式定义:

与岭回归类似,输入需要被标准化。套索惩罚使解决方案非线性,并且与岭回归不同,系数没有闭式表达式。相反,套索解是一个二次规划问题,有可用的高效算法可以计算出对于不同λ值产生的系数整个路径,其计算成本与岭回归相同。

Lasso 惩罚的效果是随着正则化的增加逐渐将一些系数减少到零。因此,Lasso 可用于连续选择一组特征。

如何使用线性回归预测回报

笔记本 linear_regression.ipynb 包含使用 statsmodelssklearn 进行 OLS 的股价预测的示例,以及岭回归和 Lasso 模型。它旨在在 Quantopian 研究平台上作为笔记本运行,并依赖于 第四章 中介绍的 factor_libraryAlpha Factors Research

准备数据

我们需要选择一组股票和一个时间范围,构建和转换我们将用作特征的 alpha 因子,计算我们希望预测的前瞻回报,并可能清理我们的数据。

宇宙创建和时间范围

我们将使用 2014 年和 2015 年的股票数据,来自使用内置过滤器、因子和分类器选择最后 200 个交易日的平均美元成交量最高的 100 支股票的自定义 Q100US 宇宙,根据额外的默认标准进行筛选(请参阅 GitHub 上链接的 Quantopian 文档以获取详细信息)。该宇宙会根据筛选标准动态更新,因此,在任何给定点上可能有 100 支股票,但样本中可能有超过 100 个不同的股票:

def Q100US():
    return filters.make_us_equity_universe(
        target_size=100,
        rankby=factors.AverageDollarVolume(window_length=200),
        mask=filters.default_us_equity_universe_mask(),
        groupby=classifiers.fundamentals.Sector(),
        max_group_weight=0.3,
        smoothing_func=lambda f: f.downsample('month_start'),
    )

目标回报计算

我们将测试不同 lookahead 期间的预测,以确定产生最佳可预测性的最佳持有期,该期间由信息系数衡量。更具体地说,我们使用内置的 Returns 函数计算 1、5、10 和 20 天的收益,结果是在两年内(每年大约有 252 个交易日)对 100 支股票的宇宙产生超过 50,000 个观察值:

lookahead = [1, 5, 10, 20]
returns = run_pipeline(Pipeline({'Returns{}D'.format(i): Returns(inputs=[USEquityPricing.close],
                                          window_length=i+1, mask=UNIVERSE) for i in lookahead},
                                screen=UNIVERSE),
                       start_date=START,
                       end_date=END)
return_cols = ['Returns{}D'.format(i) for i in lookahead]
returns.info()

MultiIndex: 50362 entries, (2014-01-02 00:00:00+00:00, Equity(24 [AAPL])) to (2015-12-31 00:00:00+00:00, Equity(47208 [GPRO]))
Data columns (total 4 columns):
Returns10D    50362 non-null float64
Returns1D     50362 non-null float64
Returns20D    50360 non-null float64
Returns5D     50362 non-null float64

Alpha 因子选择和转换

我们将使用超过 50 个涵盖市场、基本和替代数据的各种因素的特征。笔记本还包括自定义转换,将通常以季度报告频率提供的基本数据转换为滚动年度总额或平均值,以避免过度季节波动。

一旦通过 第四章 中概述的各种流水线计算出因子,Alpha 因子研究,我们使用 pd.concat() 将它们组合起来,分配索引名称,并创建一个用于识别每个数据点的资产的分类变量:

data = pd.concat([returns, value_factors, momentum_factors,
                  quality_factors, payout_factors, growth_factors,
                  efficiency_factors, risk_factors], axis=1).sortlevel()
data.index.names = ['date', 'asset']
data['stock'] = data.index.get_level_values('asset').map(lambda x: x.asset_name)

数据清洗 - 缺失数据

接下来,我们删除缺少超过 20% 观察值的行和列,导致损失 6% 的观察值和 3 列:

rows_before, cols_before = data.shape
data = (data
        .dropna(axis=1, thresh=int(len(data) * .8))
        .dropna(thresh=int(len(data.columns) * .8)))
data = data.fillna(data.median())
rows_after, cols_after = data.shape
print('{:,d} rows and {:,d} columns dropped'.format(rows_before - rows_after, cols_before - cols_after))
2,985 rows and 3 columns dropped

此时,我们有 51 个特征和股票的分类标识符:

data.sort_index(1).info()

MultiIndex: 47377 entries, (2014-01-02, Equity(24 [AAPL])) to (2015-12-
                            31, Equity(47208 [GPRO]))
Data columns (total 52 columns):
AssetToEquityRatio             47377 non-null float64
AssetTurnover                  47377 non-null float64
CFO To Assets                  47377 non-null float64
...
WorkingCapitalToAssets         47377 non-null float64
WorkingCapitalToSales          47377 non-null float64
stock                          47377 non-null object
dtypes: float64(51), object(1)

数据探索

对于线性回归模型,重要的是探索特征之间的相关性,以识别多重共线性问题,并检查特征与目标之间的相关性。笔记本包含一个 seaborn clustermap,显示特征相关矩阵的层次结构。它识别出少量高度相关的集群。

对分类变量进行虚拟编码

我们需要将分类变量stock转换为数字格式,以便线性回归可以处理它。为此,我们使用创建每个类别级别的单独列并使用1标记此级别在原始分类列中存在,否则标记为0的虚拟编码。 pandas 函数get_dummies()自动执行虚拟编码。它检测并正确转换类型为对象的列,如下所示。如果您需要对包含整数的列获取虚拟变量,例如,您可以使用columns关键字标识它们:

df = pd.DataFrame({'categories': ['A','B', 'C']})

  categories
0          A
1          B
2          C

pd.get_dummies(df)

   categories_A  categories_B  categories_C
0             1             0             0
1             0             1             0
2             0             0             1

当将所有类别转换为虚拟变量并估计模型时,使用截距(通常情况下)会无意中创建多重共线性:矩阵现在包含冗余信息,不再具有完整的秩,即,变得奇异。通过删除新指标列中的一个来简单避免这种情况。缺失类别级别上的系数现在将由截距(当其他每个类别虚拟为0时始终为1)捕获。使用drop_first关键字相应地更正虚拟变量:

pd.get_dummies(df, drop_first=True)

   categories_B  categories_C
0             0             0
1             1             0
2             0             1

应用于我们的综合特征和回报,我们获得了 181 列,因为有超过 100 只股票作为宇宙定义,它会自动更新股票选择:

X = pd.get_dummies(data.drop(return_cols, axis=1), drop_first=True)
X.info()

MultiIndex: 47377 entries, (2014-01-02 00:00:00+00:00, Equity(24 [AAPL])) to (2015-12-31 00:00:00+00:00, Equity(47208 [GPRO]))
Columns: 181 entries, DividendYield to stock_YELP INC
dtypes: float64(182)
memory usage: 66.1+ MB

创建前瞻回报

目标是预测给定持有期内的回报。因此,我们需要将特征与回报值与相应的未来 1、5、10 或 20 天的回报数据点对齐,对于每个股票。我们通过将 pandas 的.groupby()方法与.shift()方法结合使用来实现这一点:

y = data.loc[:, return_cols]
shifted_y = []
for col in y.columns:
    t = int(re.search(r'\d+', col).group(0))
    shifted_y.append(y.groupby(level='asset')['Returns{}D'.format(t)].shift(-t).to_frame(col))
y = pd.concat(shifted_y, axis=1)
y.info()

MultiIndex: 47377 entries, (2014-01-02, Equity(24 [AAPL])) to (2015-12-31, Equity(47208 [GPRO]))
Data columns (total 4 columns):
Returns1D     47242 non-null float64
Returns5D     46706 non-null float64
Returns10D    46036 non-null float64
Returns20D    44696 non-null float64
dtypes: float64(4)

现在每个回报系列的观察次数不同,因为前向移位在每个股票的尾部创建了缺失值。

使用 statsmodels 进行线性 OLS 回归

我们可以像之前演示的那样使用statsmodels估计线性回归模型的 OLS。我们选择一个前瞻回报,例如一个 10 天的持有期,删除低于 2.5%和高于 97.5%百分位数的异常值,然后相应地拟合模型:

target = 'Returns10D'
model_data = pd.concat([y[[target]], X], axis=1).dropna()
model_data = model_data[model_data[target].between(model_data[target].quantile(.025), 
                                                   model_data[target].quantile(.975))]

model = OLS(endog=model_data[target], exog=model_data.drop(target, axis=1))
trained_model = model.fit()
trained_model.summary()

诊断统计

摘要可在笔记本中保存一些空间,因为变量数量很多。诊断统计显示,鉴于 Jarque—Bera 统计量的高 p 值,不能拒绝残差服从正态分布的假设。

然而,杜宾-沃森统计量为 1.5,因此我们可以在 5%的水平上舒适地拒绝无自相关的零假设。因此,标准误差可能呈正相关。如果我们的目标是了解哪些因素与前向收益显著相关,我们需要使用稳健标准误差重新运行回归(在statsmodels .fit()方法中的一个参数),或者完全使用不同的方法,如允许更复杂误差协方差的面板模型。

使用 sklearn 进行线性 OLS 回归

由于 sklearn 专门用于预测,我们将根据其预测性能使用交叉验证评估线性回归模型。

自定义时间序列交叉验证

我们的数据包括分组的时间序列数据,需要一个自定义的交叉验证函数来提供训练和测试索引,以确保测试数据立即跟随每个股票的训练数据,我们不会无意中产生前瞻性偏差或泄漏。

我们可以使用以下函数实现这一点,该函数返回一个generator,产生训练和测试日期的对。确保训练期的最小长度的训练日期集。对数nfolds取决于参数。不同的测试期不重叠,位于数据中可用的周期末。在使用测试期后,它将成为相应增长长度的训练数据的一部分:

def time_series_split(d=model_data, nfolds=5, min_train=21):
    """Generate train/test dates for nfolds 
    with at least min_train train obs
    """
    train_dates = d[:min_train].tolist()
    n = int(len(dates)/(nfolds + 1)) + 1
    test_folds = [d[i:i + n] for i in range(min_train, len(d), n)]
    for test_dates in test_folds:
        if len(train_dates) > min_train:
            yield train_dates, test_dates
        train_dates.extend(test_dates)

选择特征和目标

我们需要选择适当的回报系列(我们将再次使用 10 天的持有期),并去除异常值。我们还将将回报转换为对数回报,如下所示:

target = 'Returns10D'
outliers = .01
model_data = pd.concat([y[[target]], X], axis=1).dropna().reset_index('asset', drop=True)
model_data = model_data[model_data[target].between(*model_data[target].quantile([outliers, 1-outliers]).values)]

model_data[target] = np.log1p(model_data[target])
features = model_data.drop(target, axis=1).columns
dates = model_data.index.unique()

DatetimeIndex: 45114 entries, 2014-01-02 to 2015-12-16
Columns: 183 entries, Returns10D to stock_YELP INC
dtypes: float64(183)

对模型进行交叉验证

我们将使用 250 个折叠来通常预测历史训练数据后大约 2 天的前向收益。每次迭代从我们的自定义交叉验证函数中获得适当的训练和测试日期,选择相应的特征和目标,然后进行训练和预测。我们捕获根均方误差以及实际值和预测值之间的 Spearman 等级相关性:

nfolds = 250
lr = LinearRegression()

test_results, result_idx, preds = [], [], pd.DataFrame()
for train_dates, test_dates in time_series_split(dates, nfolds=nfolds):
    X_train = model_data.loc[idx[train_dates], features]
    y_train = model_data.loc[idx[train_dates], target]
    lr.fit(X=X_train, y=y_train)

    X_test = model_data.loc[idx[test_dates], features]
    y_test = model_data.loc[idx[test_dates], target]
    y_pred = lr.predict(X_test)

    rmse = np.sqrt(mean_squared_error(y_pred=y_pred, y_true=y_test))
    ic, pval = spearmanr(y_pred, y_test)

    test_results.append([rmse, ic, pval])
    preds = preds.append(y_test.to_frame('actuals').assign(predicted=y_pred))
    result_idx.append(train_dates[-1])

测试结果-信息系数和 RMSE

我们已经从 250 个折叠中捕获了测试预测,并可以计算整体和 21 天滚动平均值:

fig, axes = plt.subplots(nrows=2)
rolling_result = test_result.rolling(21).mean()
rolling_result[['ic', 'pval']].plot(ax=axes[0], title='Information Coefficient')
axes[0].axhline(test_result.ic.mean(), lw=1, ls='--', color='k')
rolling_result[['rmse']].plot(ax=axes[1], title='Root Mean Squared Error')
axes[1].axhline(test_result.rmse.mean(), lw=1, ls='--', color='k')

我们得到以下图表,突显了 IC 和 RMSE 的负相关及其各自的值:

突显 IC 和 RMSE 的负相关的图表

对于整个时期,我们看到信息系数通过实际值和预测值的等级相关性来测量,呈弱正相关且具有统计学显著性:

使用 sklearn 进行岭回归

对于岭回归,我们需要使用关键字alpha来调整正则化参数,该参数对应于我们之前使用的λ。 我们将尝试从 10^(-5)到 10⁵的 21 个值以对数步长进行尝试。

岭惩罚的尺度敏感性要求我们使用StandardScaler对输入进行标准化。 请注意,我们始终从训练集中学习均值和标准差,然后使用.fit_transform()方法将这些学习参数应用于测试集,然后使用.transform()方法。

使用交叉验证调整正则化参数

然后,我们继续使用250个折叠交叉验证超参数值:

nfolds = 250
alphas = np.logspace(-5, 5, 21)
scaler = StandardScaler()

ridge_result, ridge_coeffs = pd.DataFrame(), pd.DataFrame()
for i, alpha in enumerate(alphas):
    coeffs, test_results = [], []
    lr_ridge = Ridge(alpha=alpha)
    for train_dates, test_dates in time_series_split(dates, nfolds=nfolds):
        X_train = model_data.loc[idx[train_dates], features]
        y_train = model_data.loc[idx[train_dates], target]
        lr_ridge.fit(X=scaler.fit_transform(X_train), y=y_train)
        coeffs.append(lr_ridge.coef_)

        X_test = model_data.loc[idx[test_dates], features]
        y_test = model_data.loc[idx[test_dates], target]
        y_pred = lr_ridge.predict(scaler.transform(X_test))

        rmse = np.sqrt(mean_squared_error(y_pred=y_pred, y_true=y_test))
        ic, pval = spearmanr(y_pred, y_test)

        test_results.append([train_dates[-1], rmse, ic, pval, alpha])
    test_results = pd.DataFrame(test_results, columns=['date', 'rmse', 'ic', 'pval', 'alpha'])
    ridge_result = ridge_result.append(test_results)
    ridge_coeffs[alpha] = np.mean(coeffs, axis=0)

交叉验证结果和岭回归系数路径

我们现在可以绘制每个超参数值获得的信息系数,并可视化随着正则化增加而系数值如何变化。 结果显示,我们在λ=10\时获得了最高的 IC 值。 对于这个正则化水平,右侧面板显示,与(几乎)不受约束的模型相比,系数已经显着收缩,λ=10^(-5):

交叉验证结果和岭回归系数路径

前 10 个系数

系数的标准化允许我们通过比较它们的绝对值来得出关于它们相对重要性的结论。 最相关的 10 个系数是:

前 10 个系数

使用 sklearn 的套索回归

套索实现看起来与我们刚刚运行的岭回归模型非常相似。 主要区别在于,套索需要使用迭代坐标下降来找到解决方案,而岭回归可以依赖于闭合形式的解决方案:

nfolds = 250
alphas = np.logspace(-8, -2, 13)
scaler = StandardScaler()

lasso_results, lasso_coeffs = pd.DataFrame(), pd.DataFrame()
for i, alpha in enumerate(alphas):
    coeffs, test_results = [], []
    lr_lasso = Lasso(alpha=alpha)
    for i, (train_dates, test_dates) in enumerate(time_series_split(dates, nfolds=nfolds)):
        X_train = model_data.loc[idx[train_dates], features]
        y_train = model_data.loc[idx[train_dates], target]
        lr_lasso.fit(X=scaler.fit_transform(X_train), y=y_train)

        X_test = model_data.loc[idx[test_dates], features]
        y_test = model_data.loc[idx[test_dates], target]
        y_pred = lr_lasso.predict(scaler.transform(X_test))

        rmse = np.sqrt(mean_squared_error(y_pred=y_pred, y_true=y_test))
        ic, pval = spearmanr(y_pred, y_test)

        coeffs.append(lr_lasso.coef_)
        test_results.append([train_dates[-1], rmse, ic, pval, alpha])
    test_results = pd.DataFrame(test_results, columns=['date', 'rmse', 'ic', 'pval', 'alpha'])
    lasso_results = lasso_results.append(test_results)
    lasso_coeffs[alpha] = np.mean(coeffs, axis=0)

交叉验证信息系数和套索路径

与以前一样,我们可以绘制在交叉验证期间使用的所有测试集的平均信息系数。 我们再次看到,正则化可以提高 IC 超出不受约束模型,以λ=10^(-5)为水平获得最佳的样本外结果。 最优的正则化值与岭回归不同,因为惩罚是相对较小的系数值的绝对值之和,而不是平方值。 我们还可以看到,对于这个正则化水平,系数已经类似地收缩,就像岭回归案例中一样:

交叉验证信息系数和套索路径

总的来说,岭回归和套索将产生类似的结果。 岭回归通常计算速度更快,但套索也通过逐渐将系数减小到零来产生连续的特征子集选择,从而消除了特征。

线性分类

到目前为止讨论的线性回归模型假设定量响应变量。 在本节中,我们将重点讨论对推断和预测建模定性输出变量的方法,这个过程被称为分类,在实践中甚至比回归更频繁地发生。

针对数据点预测定性响应被称为分类该观察,因为它涉及将观察分配到一个类别或类别。 在实践中,分类方法通常为定性变量的每个类别预测概率,然后使用此概率来决定适当的分类。

我们可以忽略输出变量取离散值的事实,并应用线性回归模型尝试使用多个输入变量来预测分类输出。然而,很容易构造出这种方法表现非常糟糕的例子。 此外,当我们知道y ∈ [0, 1]时,模型产生大于 1 或小于 0 的值并不直观。

有许多不同的分类技术或分类器可用于预测定性响应。 在本节中,我们将介绍广泛使用的逻辑回归,它与线性回归密切相关。 在接下来的章节中,我们将介绍更复杂的方法,包括决策树和随机森林的广义可加模型,以及梯度提升机和神经网络。

逻辑回归模型

逻辑回归模型源于希望对输出类别的概率建模,给定一个在x中线性的函数,就像线性回归模型一样,同时确保它们总和为一,并保持在[0, 1],正如我们从概率期望的那样。

在本节中,我们介绍逻辑回归模型的目标和函数形式,并描述培训方法。 然后,我们说明如何使用 statsmodels 对宏观数据进行统计推断,并使用 sklearn 实现的正则化逻辑回归来预测价格走势。

目标函数

为了说明,我们将使用输出变量 y,如果在给定时间跨度 d 内股票收益为正,则取值为 1,否则为 0:

我们可以轻松地将 y 扩展到三个类别,其中 0 和 2 反映超过某个阈值的负面和正面价格变动,否则为 1。 然而,与其对y建模,逻辑回归模型对y属于给定一向量α因子或特征的类别的概率进行建模。 换句话说,逻辑回归模型了解包含在模型中的变量值时,股票价格上涨的概率:

逻辑函数

为了防止模型生成超出 [0, 1] 区间的值,我们必须使用一个只在整个 x 的定义域上产生 0 和 1 之间输出的函数来对 p(x) 建模。逻辑函数满足这一要求,并且始终产生一个 S 形曲线(见笔记本示例),因此,无论 X 的值如何,我们都将得到一个合理的预测:

在这里,向量 x 包括一个由 第一个分量捕获的截距,。我们可以转换此表达式以隔离类似线性回归的部分,得到:

p(x)/[1−p(x)] 称为 几率,是表达概率的另一种方式,可能与赌博中熟悉的方式相似,可以取 0 到 ∞ 之间的任意值,其中较低的值也意味着较低的概率,而较高的值意味着较高的概率。

对数几率也称为对数几率(因为它是几率的对数)。因此,逻辑回归表示的对数线性回归 x 看起来与前述的线性回归非常相似。

最大似然估计

系数向量 必须使用可用的训练数据进行估计。虽然我们可以使用(非线性)最小二乘法来拟合 logistic 回归模型,但更一般的最大似然方法更受欢迎,因为它具有更好的统计特性。正如我们刚刚讨论的,使用最大似然拟合 logistic 回归模型的基本直觉是寻找估计值 ,使得预测的概率 尽可能接近实际结果。换句话说,我们试图找到 这样这些估计值在股价上涨的所有情况下都接近 1,否则接近 0 的情况。更正式地说,我们正在寻求最大化似然函数:

使用总和比乘积更容易,因此让我们两边都取对数,得到对数似然函数和 logistic 回归系数的相应定义:

通过将  对  的导数设为零来通过最大化此方程获得 p+1 个所谓的得分方程,在参数中是非线性的,可以使用迭代数值方法来解决凹的对数似然函数。

如何使用 statsmodels 进行推理

我们将演示如何使用 statsmodels 进行逻辑回归,基于一个包含从 1959 年至 2009 年的季度美国宏观数据的简单内置数据集(详见笔记本 logistic_regression_macro_data.ipynb)。

变量及其转换列于以下表格中:

变量 描述 转换
realgdp 实际国内生产总值 年增长率
realcons 实际个人消费支出 年增长率
realinv 实际私人国内投资总额 年增长率
realgovt 实际联邦支出和总投资 年增长率
realdpi 实际私人可支配收入 年增长率
m1 M1 名义货币存量 年增长率
tbilrate 月度 3 个国库券利率 水平
unemp 季节性调整后的失业率(%) 水平
infl 通货膨胀率 水平
realint 实际利率 水平

为了得到一个二元目标变量,我们计算季度实际 GDP 年增长率的 20 季度滚动平均值。然后,如果当前增长超过移动平均值,则分配 1,否则分配 0。最后,我们将指标变量移位,以使下一季度的结果与当前季度对齐。

我们使用一个截距,并将季度值转换为虚拟变量,并按以下方式训练逻辑回归模型:

import statsmodels.api as sm

data = pd.get_dummies(data.drop(drop_cols, axis=1), columns=['quarter'], drop_first=True).dropna()
model = sm.Logit(data.target, sm.add_constant(data.drop('target', axis=1)))
result = model.fit()
result.summary()

这产生了我们的模型摘要,其中包含截距的 198 个观测值和 13 个变量:

逻辑回归结果

摘要显示,该模型已使用最大似然进行训练,并在 -67.9 处提供了对数似然函数的最大化值。

LL-Null 值为 -136.42 是只包括截距时对数似然函数的最大化结果。它形成了伪 R² 统计量和 Log-似然比LLR)测试的基础。

伪 R^(2 ) 统计量是最小二乘法下可用的熟悉 R² 的替代品。它是基于空模型 m[0] 和完整模型 m[1] 的最大化对数似然函数的比率计算的,如下所示:

值从 0 变化(当模型不改善似然时)到 1(模型完美拟合时)且对数似然在 0 处最大化。因此,较高的值表明拟合效果更好。

LLR 测试通常比较一个更受限制的模型,并计算如下:

原假设是受限制模型的性能更好,但低的 p 值表明我们可以拒绝这一假设,并更喜欢完整模型而不是空模型。这类似于线性回归的 F 检验(当我们使用 MLE 估计模型时,也可以使用 LLR 测试)。

Z 统计量在线性回归输出中起着与 t 统计量相同的作用,并且与系数估计和其标准误差的比率一样计算。p 值还指示了在假设 H[0]:β = 0(总体系数为零)的情况下观察到测试统计量的概率。我们可以拒绝这个假设,对于截距realconsrealinvrealgovtrealdpiunemp

如何使用逻辑回归进行预测

套索 L[1] 惩罚和岭 L[2] 惩罚都可以与逻辑回归一起使用。它们具有我们刚刚讨论的相同收缩效应,而套索可以再次用于任何线性回归模型的变量选择。

与线性回归一样,对输入变量进行标准化非常重要,因为正则化模型对比例敏感。正则化超参数还需要使用交叉验证进行调整,就像线性回归的情况一样。

如何使用 sklearn 预测价格变动

我们继续价格预测示例,但现在我们将结果变量二值化,以便在 10 天回报为正时取值为 1,否则为 0;请参阅子目录 stock_price_prediction 中的笔记本 logistic_regression.ipynb

target = 'Returns10D'
label = (y[target] > 0).astype(int).to_frame(target)

有了这个新的分类结果变量,我们现在可以使用默认的 L[2]正则化训练逻辑回归。对于逻辑回归,正则化与线性回归相反:λ的值越高,正则化越少,反之亦然。我们使用交叉验证评估 11 个参数值如下:

nfolds = 250
Cs = np.logspace(-5, 5, 11)
scaler = StandardScaler()

logistic_results, logistic_coeffs = pd.DataFrame(), pd.DataFrame()
for C in Cs:
    coeffs = []
    log_reg = LogisticRegression(C=C)
    for i, (train_dates, test_dates) in enumerate(time_series_split(dates, nfolds=nfolds)):
        X_train = model_data.loc[idx[train_dates], features]
        y_train = model_data.loc[idx[train_dates], target]
        log_reg.fit(X=scaler.fit_transform(X_train), y=y_train)

        X_test = model_data.loc[idx[test_dates], features]
        y_test = model_data.loc[idx[test_dates], target]
        y_pred = log_reg.predict_proba(scaler.transform(X_test))[:, 1]

        coeffs.append(log_reg.coef_.squeeze())
        logistic_results = (logistic_results
                            .append(y_test
                                    .to_frame('actuals')
                                    .assign(predicted=y_pred, C=C)))
    logistic_coeffs[C] = np.mean(coeffs, axis=0)

然后我们使用前一章讨论的 roc_auc_score 来比较各种正则化参数的预测准确度:

auc_by_C = logistic_results.groupby('C').apply(lambda x: roc_auc_score(y_true=x.actuals.astype(int), 
                                                         y_score=x.predicted))

我们可以再次绘制 AUC 结果,以显示超参数值范围以及系数路径,该路径显示系数在最佳正则化值 10² 处略微收缩时预测精度的提高:

AUC 和 Logistic Ridge 路径

摘要

在本章中,我们介绍了使用线性模型进行回归和分类的重要基线案例的第一个机器学习模型。我们探讨了两个任务的目标函数的制定,学习了各种训练方法,并学习了如何将模型用于推理和预测。

我们将这些新的机器学习技术应用于估计线性因子模型,这些模型对于管理风险、评估新的阿尔法因子和归因绩效非常有用。我们还应用线性回归和分类来完成第一个预测任务,即在绝对和方向性方面预测股票收益。

在下一章中,我们将讨论重要的线性时间序列模型主题,这些模型旨在捕捉单变量和多变量情况下的串行相关模式。我们还将学习关于新的交易策略,因为我们将探讨基于协整概念的配对交易,该概念捕捉了两个股价序列之间的动态相关性。

第八章:时间序列模型

在上一章中,我们重点关注了适用于横截面数据的线性模型,其中输入数据属于与其旨在解释或预测的输出相同的时间段。在本章中,我们将关注时间序列数据,其中观察值按周期而不同,这也创建了自然的排序。我们的目标是识别数据中的历史模式,并利用这些模式来预测时间序列在未来的行为。

在上一章中,我们已经遇到了既有横截面又有时间序列维度的面板数据,并了解了法马-麦克白斯回归如何估计随时间和跨资产采取某些因子风险的价值。然而,随时间的回报之间的关系通常相当低,因此这个过程可能会大部分忽视时间维度。本章的模型侧重于时间序列模型,其中过去的价值包含关于未来发展的预测信号。时间序列模型还可以预测随后用于横截面模型的特征。

具体来说,在本章中,我们将重点关注从先前观察到的数据中提取信号以预测同一时间序列未来值的模型。交易的时间维度使得将时间序列模型应用于市场、基本和替代数据变得非常流行。随着越来越广泛的连接设备收集可能包含预测信号的定期测量,时间序列数据将变得更加普遍。关键应用包括资产回报、相关性或协方差、或波动率的预测。

在本章中,我们将重点关注线性时间序列模型,作为本书第四部分应用于时间序列数据的非线性模型(如递归或卷积神经网络)的基线。我们首先介绍诊断时间序列特征的工具,包括平稳性,并提取捕捉潜在模式的特征。然后,我们介绍单变量和多变量时间序列模型,并将其应用于宏观数据和波动性模式的预测。最后,我们介绍协整概念以及如何应用它来开发配对交易策略。

具体来说,我们将涵盖以下主题:

  • 如何使用时间序列分析来诊断通知建模过程的诊断统计数据

  • 如何估计和诊断自回归和移动平均时间序列模型

  • 如何构建自回归条件异方差(ARCH)模型来预测波动性

  • 如何构建向量自回归模型

  • 如何利用协整性进行配对交易策略

诊断和特征提取的分析工具

时间序列数据是一系列由离散时间间隔分隔的值,通常是均匀间隔的(除了缺失值)。

两个不同时间点 t[i]、t[j]之间的周期数Δt= t[i] - t[j]称为滞后,对于每个时间序列有 T-1 个滞后。

如果一个时间序列是独立同分布的随机变量ε[t]的序列,并且具有有限的均值和方差,则称其为白噪声。

如果一个时间序列可以写成过去扰动ε[t]的加权和,这些扰动也称为创新,这里假设它们代表白噪声,以及序列的均值μ,则该时间序列是线性的。

时间序列分析的一个关键目标是理解由系数 a[i]驱动的动态行为。

在本章的大多数示例中,我们使用由美联储提供的数据,您可以使用我们在第二章中介绍的pandas datareader进行访问,市场和基本数据。 本节的代码示例可在笔记本tsa_and_arima中找到。

如何分解时间序列模式

时间序列数据通常包含各种模式的混合,可以分解为几个组成部分,每个部分代表一种基本模式类别。特别是,时间序列通常由系统组件趋势、季节性和周期性以及非系统噪声组成。这些组件可以在加法线性模型中组合,特别是当波动不依赖于系列水平时,或者在非线性乘法模型中组合。

这些组件可以自动拆分。statsmodels包括一种简单的方法,将时间序列拆分为趋势、季节和残差分量,使用移动平均。我们可以将其应用于工业制造生产的月度数据,该数据具有强烈的趋势和季节性分量,如下所示:

import statsmodels.tsa.api as tsa
industrial_production = web.DataReader('IPGMFN', 'fred', '1988', '2017-12').squeeze()
components = tsa.seasonal_decompose(industrial_production, model='additive')
ts = (industrial_production.to_frame('Original')
      .assign(Trend=components.trend)
      .assign(Seasonality=components.seasonal)
      .assign(Residual=components.resid))
ts.plot(subplots=True, figsize=(14, 8));

结果图显示了加法组件。假设趋势和季节性组件更确定且更适合简单外推,则残差组件将成为额外建模的焦点:

还有更复杂的基于模型的方法,这些方法包含在 GitHub 上可用的参考资料中。

如何计算滚动窗口统计量

考虑到时间序列数据的顺序排列,自然而然地计算给定长度周期的熟悉描述性统计量,以检测稳定性或行为变化,并获得捕获系统方面的平滑表示,同时过滤噪声。

滚动窗口统计为此过程服务:它们生成一个新的时间序列,其中每个数据点表示原始数据的一定期间内计算的摘要统计量。移动平均是最熟悉的例子。原始数据点可以以相等的权重进入计算,或者使用权重来,例如,强调最近的数据点。指数移动平均递归地计算权重,使得远离现在的数据点权重缩小或衰减。新的数据点通常是所有前面数据点的摘要,但也可以从周围的窗口计算。

pandas库包括非常灵活的功能来定义各种窗口类型,包括滚动窗口、指数加权窗口和扩展窗口。在第二步中,您可以对由窗口捕获的每个数据应用计算。这些计算包括针对单个系列的内置标准计算,例如平均值或总和,对于几个系列的相关性或协方差,以及用户定义的函数。以下部分中的移动平均和指数平滑示例使用了这些工具。

移动平均和指数平滑

早期的预测模型包括带有指数权重的移动平均模型,称为指数平滑模型。我们将在后面的线性时间序列中再次遇到移动平均作为关键的构建块。

依赖指数平滑方法的预测使用过去观测的加权平均值,其中随着观测时间的推移,权重呈指数衰减。因此,更近期的观测获得更高的关联权重。这些方法在时间序列不具有非常复杂或突然的模式时很受欢迎。

指数平滑是一种流行的技术,基于过去观测的加权平均,随着观测时间的推移,权重呈指数衰减。换句话说,观测越近,相关的权重越高。这个框架可以快速生成可靠的预测,适用于广泛的时间序列,这是一个巨大的优势,对于工业应用至关重要。

如何测量自相关

自相关(也称为串行相关)将相关概念调整为时间序列环境:正如相关系数衡量两个变量之间线性关系的强度一样,自相关系数ρ[k]衡量相隔给定滞后 k 的时间序列值之间的线性关系的程度:

因此,我们可以计算时间序列中 T-1 个滞后的每个自相关系数;T 是序列的长度。自相关函数(ACF)根据滞后计算相关系数。

对于大于 1 的滞后自相关(即,超过一个时间步的观测之间的),反映了这些观测之间的直接相关以及中间数据点的间接影响。偏自相关消除了这种影响,只测量给定滞后距离的数据点之间的线性依赖关系。偏自相关函数(PACF)提供了一旦较短滞后的相关性效应被移除后产生的所有相关性。

有算法根据 PACF 与 ACF 之间的确切理论关系来估计样本自相关的偏自相关。

自相关图简单地是顺序滞后 k=0,1,...,n 的 ACF 或 PACF 的图。它允许我们一眼就检查各个滞后的相关结构。自相关图的主要用途是在去除确定性趋势或季节性影响后检测任何自相关。ACF 和 PACF 都是线性时间序列模型设计的关键诊断工具,我们将在时间序列转换的以下部分中审查 ACF 和 PACF 图的示例。

如何诊断并实现平稳性

稳定时间序列的统计特性,如均值、方差或自相关,与时期无关,即它们随时间不变化。因此,稳态意味着时间序列没有趋势或季节效应,而且当为不同的滚动窗口计算描述统计量(如均值或标准差)时,它们是常数或随时间变化不大的。它回归到其均值,偏差具有恒定的幅度,而短期波动在统计意义上总是相同的。

更正式地说,严格稳态要求任何时间序列观测子集的联合分布与时间无关,而与所有时刻的矩一致。因此,除了均值和方差外,高阶矩,如偏度和峰度,也需要保持不变,而不考虑不同观测之间的滞后。在大多数应用中,我们将稳态限制为一阶和二阶矩,以使时间序列具有恒定的均值、方差和自相关。

请注意,我们特别允许不同滞后观测之间的依赖关系,就像我们希望线性回归的输入数据与结果相关联一样。稳态意味着这些关系是稳定的,这有助于预测,因为模型可以专注于学习发生在稳定统计属性内的系统模式。这很重要,因为经典统计模型假设时间序列输入数据是稳态的。

以下部分介绍了帮助检测数据非稳态的诊断方法,以及帮助满足这些假设的转换方法。

时间序列转换

要满足线性时间序列模型的稳态假设,我们需要转换原始时间序列,通常需要几个步骤。常见的转换包括将(自然)对数应用于将指数增长模式转换为线性趋势并稳定方差。通货紧缩意味着将一个时间序列除以另一个导致趋势行为的系列,例如将名义系列除以价格指数以将其转换为实际测量。

如果一个系列恢复到一个稳定的长期线性趋势,则它是趋势稳定的。通常可以通过使用线性回归拟合趋势线并使用残差,或者通过将时间指数包括为回归或 AR(I)MA 模型的独立变量(请参阅下一节有关单变量时间序列模型的内容)来使其稳定,可能结合对数化或通货紧缩。

在许多情况下,去趋势化不足以使序列平稳。相反,我们需要将原始数据转换为周期间和/或季节间差异的系列。换句话说,我们使用相邻数据点或季节滞后值相互减去的结果。请注意,当将这种差分应用于对数变换的序列时,结果代表金融背景下的瞬时增长率或收益率。

如果单变量序列在差分 d 次后变为平稳,则称其为 d 阶整合,如果 d=1,则称其为简单整合。这种行为是由所谓的单位根引起的。

如何诊断和解决单位根问题

单位根对确定将时间序列变为平稳的转换提出了特殊问题。时间序列通常被建模为我们将更详细地探讨的具有以下自回归形式的随机过程,作为 ARIMA 模型的构建块:

其中当前值是过去值的加权和加上随机扰动。这样的过程具有以下形式的特征方程:

如果此方程的根之一等于 1,则该过程被称为具有单位根。它将是非平稳的,但不一定需要趋势。如果特征方程的剩余根的绝对值小于 1,则过程的一阶差分将是平稳的,并且过程是整合的(阶数为 1)或 I(1)。如果附加根的绝对值大于 1,则积分的阶数较高,并且将需要额外的差分。

在实践中,利率或资产价格的时间序列通常不是平稳的,例如,因为不存在价格水平使序列回归。非平稳序列最突出的例子是给定起始价格 p[0](例如,股票的首次公开发行价格)和满足以下条件的白噪声扰动 ε 的价格时间序列的随机游走:

重复代换表明,当前值 p[t] 是所有先前扰动或创新 ε 以及初始价格 p[0] 的总和。如果方程包括一个常数项,则随机游走被称为具有漂移。因此,随机游走是以下形式的自回归随机过程:

具有特征方程 ,该方程具有单位根并且既不稳定也不是一阶积分。一方面,鉴于 ε 的 i.i.d. 性质,时间序列的方差等于 σ²,这不是二阶稳定的,并且暗示着原则上,该系列随时间可能采用任何变量。另一方面,进行第一次差分,Δp[t]=p[t]-p[t-1],留下 Δp[t]=ε [t],这是稳定的,鉴于对 ε 的统计假设。

单位根非平稳系列的定义特征是长期记忆:因为当前值是过去扰动的总和,所以大的创新持续的时间比均值回归、平稳的系列长得多。

除了使用相邻数据点之间的差异来消除恒定变化模式之外,还可以应用季节性差分来消除季节性变化的模式。这涉及到以表示季节模式长度的滞后距离为单位,相隔四个季度或 12 个月的值之间的差异,以消除季节性和线性趋势。

识别正确的转换,尤其是差分的适当数量和滞后期并不总是明确的。一些规则已经被提出,总结如下:

  • 10+滞后期的正自相关: 可能需要更高阶的差分。

  • 滞后期接近零或为负,或一般较小且无规律: 不需要更高阶的差分。

  • 滞后期-1 的自相关< -0.5: 系列可能存在过多的差分。

  • 轻微的过度或不足的差分可以通过 AR 或 MA 项进行修正。

  • 最优差分通常会产生最低的标准差,但并非总是如此。

  • 无差分的模型假设原始系列是平稳的,包括均值回归。通常包括一个常数项,以允许非零均值。

  • 具有一次差分次序的模型假设原始系列具有恒定的平均趋势,并且应包括一个常数项。

  • 具有两个差分次序的模型假设原始系列具有时变趋势,不应包括一个常数。

一些作者推荐分数差分作为使集成系列保持稳定的更灵活的方法,可能能够在离散间隔中保留比简单或季节性差异更多的信息或信号(参见 GitHub 上的参考资料)。

单位根检验

统计单位根检验是一种常见的客观确定(额外)差分是否必要的方法。这些是用于确定是否需要差分的平稳性的统计假设检验。

增广 Dickey-Fuller (ADF) 检验评估一个时间序列样本是否具有单位根的零假设,对比平稳性的备择假设。它对时间趋势、第一滞后和所有滞后差异的差异化时间序列进行回归,并根据滞后时间序列值的系数值计算一个检验统计量。 statsmodels 使得实现变得容易(参见伴随的笔记本)。

形式上,时间序列 y[t] 的 ADF 测试运行线性回归:

其中 α 是一个常数,β 是时间趋势的系数,p 是模型中使用的滞后数。α=β =0 约束意味着一个随机游走,而只有 β=0 意味着有漂移的随机游走。滞后顺序通常使用在第七章 中介绍的 AIC 和 BIC 信息标准来决定,线性模型

ADF 检验统计量使用样本系数 γ,在单位根非平稳的零假设下等于零,在其他情况下为负。它旨在证明,对于一个积分序列,滞后的系列值不应该在预测超出滞后差异的第一差异时提供有用的信息。

如何应用时间序列变换

以下图表显示了纳斯达克股票指数和工业生产在原始形式下的 30 年时间序列,以及应用对数和随后应用第一和季节性差异(在滞后 12 )之后的转换版本。图表还显示了 ADF p 值,这使得我们可以在两种情况下拒绝单位根非平稳的假设:

我们可以进一步分析转换后时间序列的相关特征,使用一个 Q-Q 图来比较时间序列观测值的分布的分位数和正态分布的分位数,以及基于 ACF 和 PACF 的自相关图。

对于纳斯达克图表,我们注意到虽然没有趋势,但方差不是恒定的,而是在 1980 年代末、2001 年和 2008 年的市场动荡时期附近显示出集群的尖峰。Q-Q 图突出显示了分布的尾部肥大,极端值比正态分布所暗示的更频繁。ACF 和 PACF 显示出类似的模式,多个滞后处的自相关显著:

对于工业生产的月度时间序列,我们注意到 2008 年危机后出现了一个大的负异常值,以及 Q-Q 图中相应的偏斜。自相关远高于纳斯达克回报,并且平稳下降。PACF 在滞后 1 和 13 处显示出明显的正自相关模式,并且在滞后 3 和 4 处有显著的负系数:

单变量时间序列模型

多元线性回归模型将感兴趣的变量表达为预测变量或输入变量的线性组合。单变量时间序列模型将感兴趣时间点的时间序列值与系列滞后值的线性组合以及可能的过去干扰项相关联。

虽然指数平滑模型是基于数据中的趋势和季节性的描述,但 ARIMA 模型旨在描述数据中的自相关性。ARIMA(p, d, q) 模型需要稳定性,并利用两个构建块:

  • 自回归 (AR) 项由时间序列的 p 滞后值组成

  • 移动平均 (MA) 项包含 q 滞后干扰

I 代表积分,因为模型可以通过对系列进行 d 次微分来考虑单位根非平稳性。自回归术语强调 ARIMA 模型意味着时间序列对其自身值的回归。

我们将介绍 ARIMA 构建块,简单的自回归 (AR) 和移动平均 (MA) 模型,并解释如何将它们结合在自回归移动平均 (ARMA) 模型中,该模型可以作为 ARIMA 模型考虑系列集成,或者包含外生变量作为 AR(I)MAX 模型。此外,我们将说明如何包含季节性 AR 和 MA 项以扩展工具箱,以便还包括 SARMAX 模型。

如何构建自回归模型

顺序 p 的 AR 模型旨在捕捉不同滞后时间序列值之间的线性相关性,并可写为:

这与 y[t] 的滞后值的多重线性回归非常相似。该模型具有以下特征方程:

解此方程的 x 的解的倒数是特征根,如果这些根在绝对值上都小于 1,则 AR(p) 过程是稳定的,否则是不稳定的。对于稳态系列,多步预测将收敛到系列的平均值。

我们可以使用熟悉的最小二乘法估计模型参数,使用 p+1,..., T 观测数据以确保每个滞后项和结果都有数据。

如何确定滞后数量

实际上,挑选适当的滞后项顺序 p 是一个挑战。用于串行相关性的时间序列分析工具起着关键作用。ACF 估计不同滞后观测之间的自相关性,这反过来是直接和间接线性相关性的结果。

因此,对于阶数为 k 的 AR 模型,ACF 将显示出一直到滞后 k 的显著串行相关,并且由于线性关系间接影响所导致的惯性,将延伸到后续滞后,最终因效应减弱而逐渐消失。另一方面,PACF 仅度量观察值之间的直接线性关系,间隔给定滞后,因此不会反映超过 k 的滞后的相关性。

如何诊断模型拟合度

如果模型捕获了滞后之间的线性依赖关系,则残差应该类似于白噪声。

除了检查 ACF 以验证自相关系数的显著性缺失外,Ljung-Box Q 统计量还允许我们测试残差序列是否遵循白噪声的假设。零假设是所有 m 个串行相关系数都为零,而备择假设是一些系数不为零。测试统计量是从不同滞后 k 的样本自相关系数 ρ[k] 计算得到,并且遵循一个 Χ² 分布:

正如我们将看到的,statsmodels 提供了关于不同滞后的系数显著性的信息,不显著的系数应予以删除。如果 Q 统计量拒绝了无自相关的原假设,则应考虑额外的 AR 项。

如何构建移动平均模型

阶数为 q 的 MA 模型使用过去 q 个扰动,而不是时间序列的滞后值,如下:

由于我们没有观察到白噪声扰动值 ε[t],MA(q) 不像我们迄今为止见过的那样是一个回归模型。MA(q) 模型不是使用最小二乘法估计的,而是使用最大似然估计(MLE),或者在系列开始时初始化或估计扰动,然后递归和迭代地计算剩余部分。

MA(q)模型的名称源于将每个 y[t] 的值表示为过去 q 个创新的加权移动平均。换句话说,当前的估计值代表了相对于模型过去错误的修正。与指数平滑或估计季节性时间序列分量不同,MA(q)模型中移动平均的使用旨在预测未来值,而不是去噪声或估计过去值的趋势周期。

MA(q) 过程始终是平稳的,因为它们是白噪声变量的加权和,而这些变量本身是平稳的。

如何确定滞后数

由 MA(q) 过程产生的时间序列由前 q 个模型预测的残差驱动。因此,MA(q) 过程的 ACF 将显示出到滞后 q 的值的显著系数,然后急剧下降,因为这是假定系列值生成的方式。

AR 和 MA 模型之间的关系

AR(p) 模型可以使用重复替代表示为 MA(∞) 过程。当对其系数的大小施加约束时,一个 MA(q) 过程,它变得可逆,并且可以表示为 AR(∞) 过程。

如何构建 ARIMA 模型及其扩展

自回归积分移动平均 ARIMA(p, d, q) 模型结合了 AR(p) 和 MA(q) 过程,利用这些构建模块的互补性,并通过使用更紧凑的形式简化模型开发,并减少参数的数量,从而减少过拟合的风险。

该模型还通过使用时间序列值的 d^(th) 差分来消除单位根非平稳性。ARIMA(p, 1, q) 模型与使用系列的第一次差分的 ARMA(p, q) 模型相同。使用 y' 表示非季节性差分 d 次后的原始系列,ARIMA(p, d, q) 模型就是:

ARIMA 模型也是使用最大似然估计的。根据实现的不同,高阶模型通常会包含低阶模型。例如,statsmodels 包括所有低阶 p 和 q 项,并且不允许移除小于最高值的滞后项系数。在这种情况下,高阶模型总是拟合得更好。小心不要通过使用过多的项过度拟合模型。

如何确定 AR 和 MA 项的数量

由于 AR(p) 和 MA(q) 项相互作用,ACF 和 PACF 提供的信息不再可靠,只能作为起点使用。

传统上,AIC 和 BIC 信息准则一直被用来在选择模型设计时依赖于样本内拟合。或者,我们可以依赖于样本外测试来交叉验证多个参数选择。

以下摘要提供了在考虑 AR 和 MA 模型时选择模型阶数的一些通用指导:

  • 超过 PACF 截断的滞后是所示的 AR 项的数量。如果差分序列的 PACF 显示出明显的截断和/或滞后 1 的自相关是正的,则添加一个或多个 AR 项。

  • 超过 ACF 截断的滞后是所示的 MA 项的数量。如果差分序列的 ACF 显示出明显的截断和/或滞后 1 的自相关是负的,则考虑向模型中添加一个 MA 项。

  • AR 和 MA 项可能会相互抵消彼此的影响,因此如果您的模型同时包含两者,尝试总是将 AR 和 MA 项的数量减少 1,以避免过度拟合,特别是如果更复杂的模型需要超过 10 次迭代才能收敛。

  • 如果 AR 系数之和接近于 1,并且表明模型的 AR 部分存在单位根,则应消除 1 个 AR 项并再次进行差分。

  • 如果 MA 系数之和接近于 1,并且表明模型的 MA 部分存在单位根,则应消除 1 个 MA 项,并将差分阶数降低 1。

  • 不稳定的长期预测表明模型的 AR 或 MA 部分可能存在单位根。

添加特征 - ARMAX

ARMAX 模型在 ARMA 时间序列模型的右侧添加输入变量或协变量(假设序列是平稳的,因此我们可以跳过差分):

这类似于线性回归模型,但很难解释,因为 β 对 y[t] 的影响不是像线性回归中 x[t] 增加一个单位那样简单。相反,在方程的右侧存在 y[t] 的滞后值意味着系数只能在考虑到响应变量的滞后值时进行解释,这几乎是不直观的。

添加季节性差分 - SARIMAX

对于具有季节效应的时间序列,我们可以包括捕捉季节性周期性的 AR 和 MA 项。例如,当使用月度数据且季节效应长度为一年时,季节性 AR 和 MA 项将反映这个特定的滞后长度。

然后 ARIMAX(p, d, q) 模型变成了 SARIMAX(p, d, q) x (P, D, Q)[s] 模型,这样写起来稍微复杂一些,但是 GitHub 上的参考资料,包括 statsmodels 的文档,提供了详细信息。

现在我们将使用宏观数据构建一个季节性 ARMA 模型来说明实施过程。

如何预测宏观基本面

我们将为 1988-2017 年间的工业生产时间序列构建一个 SARIMAX 模型。正如在关于分析工具的第一部分中所说明的,数据已经进行了对数变换,我们正在使用季节性(滞后-12)差分。我们使用一个包括常规 AR 和 MA 参数范围的滚动窗口对 10 年的训练数据进行模型估计,并评估 1 步预测的 RMSE,如下所示的简化代码(详情请参阅 GitHub):

for p1 in range(4):                # AR order
    for q1 in range(4):            # MA order
        for p2 in range(3):        # seasonal AR order
            for q2 in range(3):    # seasonal MA order
                y_pred = []
                for i, T in enumerate(range(train_size, len(data))):
                    train_set = data.iloc[T - train_size:T]
                    model = tsa.SARIMAX(endog=train_set,            # model specification
                                        order=(p1, 0, q1),
                                        seasonal_order=(p2, 0, q2, 12)).fit()

                    preds.iloc[i, 1] = model.forecast(steps=1)[0]    # 1-step ahead forecast

                mse = mean_squared_error(preds.y_true, preds.y_pred)
                test_results[(p1, q1, p2, q2)] = [np.sqrt(mse),
                                                  preds.y_true.sub(preds.y_pred).std(),
                                                  np.mean(aic)]

我们还收集了 AIC 和 BIC 标准,显示了 0.94 的非常高的秩相关系数,其中 BIC 倾向于比 AIC 稍少参数的模型。RMSE 最佳的五个模型为:

                 RMSE         AIC         BIC
p1 q1 p2 q2                                  
2  3  1  0   0.009323 -772.247023 -752.734581
3  2  1  0   0.009467 -768.844028 -749.331586
2  2  1  0   0.009540 -770.904835 -754.179884
   3  0  0   0.009773 -760.248885 -743.523935
   2  0  0   0.009986 -758.775827 -744.838368

我们重新估计了一个 SARIMA(2, 0, 3) x (1, 0, 0) 模型,如下所示:

best_model = tsa.SARIMAX(endog=industrial_production_log_diff, order=(2, 0, 3),
                         seasonal_order=(1, 0, 0, 12)).fit()
print(best_model.summary())

我们得到以下摘要:

系数是显著的,并且 Q 统计量拒绝了进一步自相关的假设。相关图表同样表明我们已成功消除了序列的自相关性:

如何使用时间序列模型来预测波动性

单变量时间序列模型的一个特别重要的应用领域是波动率的预测。金融时间序列的波动率通常随时间而变化,但是波动率的变化会聚集在一起。方差的变化对于使用经典的 ARIMA 模型进行时间序列预测构成挑战。为了解决这一挑战,我们现在将对波动率进行建模,以便我们可以预测方差的变化。

异方差性是变量方差变化的技术术语。自回归条件异方差(ARCH)模型将误差项的方差表达为前期误差的函数。更具体地说,它假定误差方差遵循一个 AR(p) 模型。

广义自回归条件异方差(GARCH)模型将范围扩展到 ARMA 模型。时间序列预测通常将 ARIMA 模型用于时间序列的预期均值,并将 ARCH/GARCH 模型用于时间序列的预期方差。2003 年诺贝尔经济学奖授予了罗伯特·恩格尔和克莱夫·格兰杰,以表彰他们开发了这类模型。前者还在纽约大学斯特恩商学院(参见 GitHub 引用)运营着波动性实验室,提供了许多关于我们将讨论的模型及其众多扩展的在线示例和工具。

自回归条件异方差(ARCH)模型

ARCH(p) 模型简单地是将 AR(p) 模型应用于时间序列模型的残差方差,该模型使得时间 t 的方差条件于方差的滞后观测。更具体地说,误差项 ε[t] 是原始时间序列上线性模型(如 ARIMA)的残差,并分为一个时间相关的标准差 σ[t] 和一个扰动 z[t],如下所示:

图片

ARCH(p) 模型可以使用 OLS 进行估计。Engle 提出了一种使用拉格朗日乘子检验来确定适当的 ARCH 阶数的方法,该检验对应于线性回归中所有系数为零的假设的 F 检验(参见 第七章,线性模型)。

该模型的一个优点是它产生波动率,并估计正的超额峰度——即,相对于正态分布有厚尾,这与关于回报的经验观察一致。弱点包括模型假设正和负波动冲击具有相同的效应,因为它依赖于前一冲击的平方,而资产价格已知对正和负冲击有不同的反应。ARCH 模型也没有提供关于金融时间序列变化源的新见解,因为它只是机械地描述条件方差。最后,ARCH 模型很可能会过度预测波动性,因为它对回报系列的大规模孤立冲击反应缓慢。

对于正确规范的 ARCH 模型,标准化残差(除以标准偏差期间的模型估计)应类似于白噪声,并且可以进行 Ljung-Box Q 检验。

泛化 ARCH —— GARCH 模型

ARCH 模型相对简单,但通常需要许多参数来捕获资产收益系列的波动性模式。广义 ARCHGARCH)模型适用于对数收益系列 r[t],具有扰动项 ε[t] = r[t ]- μ,如果遵循 GARCH(p, q) 模型:

GARCH(p, q) 模型假设误差项 ε[t] 的方差服从 ARMA(p, q) 模型。

类似于 ARCH 模型,GARCH(1,1) 过程的尾部分布比正态分布更重。该模型面临与 ARCH 模型相同的弱点。例如,它对正负冲击的反应一样。

选择滞后阶数

要为 ARCH 和 GARCH 模型配置滞后阶数,使用训练以预测原始系列均值的时间序列的平方残差。残差是零中心化的,因此它们的平方也是方差。然后检查平方残差的 ACF 和 PACF 图以识别时间序列方差中的自相关模式。

如何构建波动率预测模型

为资产收益序列开发波动率模型包括四个步骤:

  1. 基于 ACF 和 PACF 显示的序列依赖性构建金融时间序列的 ARMA 时间序列模型。

  2. 再次依靠残差序列的 ACF 和 PACF 检查模型的残差是否具有 ARCH/GARCH 效应。

  3. 如果序列相关效应显著,则指定一个波动率模型,并联合估计均值和波动率方程。

  4. 仔细检查拟合的模型,如有必要,进行精细调整。

当将波动率预测应用于回报序列时,序列依赖性可能有限,因此可以使用恒定均值代替 ARMA 模型。

arch 库提供了几个选项来估计波动率预测模型。它提供了几个选项来建模预期均值,包括一个常数均值,上面关于单变量时间序列模型的 AR(p) 模型以及更近期的异质自回归过程(HAR),它使用每日(1 天)、每周(5 天)和每月(22 天)的滞后来捕捉短期、中期和长期投资者的交易频率。

可以与几个条件异方差模型一起定义和估计均值模型,除了 ARCH 和 GARCH 外,还包括允许正负收益之间的非对称效应的指数 GARCHEGARCH)模型,以及异质 ARCHHARCH)模型,它补充了 HAR 均值模型。

我们将使用 1998 年至 2017 年的每日纳斯达克收益来演示 GARCH 模型的使用情况(有关详细信息,请参阅笔记本 arch_garch_models):

nasdaq = web.DataReader('NASDAQCOM', 'fred', '1998', '2017-12-31').squeeze()
nasdaq_returns = np.log(nasdaq).diff().dropna().mul(100) # rescale to facilitate optimization

重新缩放的日收益率系列仅具有有限的自相关性,但与均值的平方偏差具有显着的记忆,反映在缓慢衰减的 ACF 和 PACF 上,前两者的值很高,并且仅在前六个滞后之后才截断:

plot_correlogram(nasdaq_returns.sub(nasdaq_returns.mean()).pow(2), lags=120, title='NASDAQ Daily Volatility')

函数plot_correlogram生成了以下输出:

因此,我们可以估计一个 GARCH 模型来捕捉过去波动率的线性关系。我们将使用滚动 10 年窗口来估计一个 GARCH(p, q)模型,其中 p 和 q 的范围为 1-4,以生成 1 步的外样本预测。然后,我们将比较预测波动性的 RMSE 与实际收益偏离其均值的平方之间的 RMSE,以确定最具预测性的模型。我们使用修剪数据来限制极端收益值的影响,这些值反映在波动性的非常高的正偏度中:

trainsize = 10 * 252  # 10 years
data = nasdaq_returns.clip(lower=nasdaq_returns.quantile(.05),
                           upper=nasdaq_returns.quantile(.95))
T = len(nasdaq_returns)
test_results = {}
for p in range(1, 5):
    for q in range(1, 5):
        print(f'{p} | {q}')
        result = []
        for s, t in enumerate(range(trainsize, T-1)):
            train_set = data.iloc[s: t]
            test_set = data.iloc[t+1]  # 1-step ahead forecast
            model = arch_model(y=train_set, p=p, q=q).fit(disp='off')
            forecast = model.forecast(horizon=1)
            mu = forecast.mean.iloc[-1, 0]
            var = forecast.variance.iloc[-1, 0]
            result.append([(test_set-mu)**2, var])
        df = pd.DataFrame(result, columns=['y_true', 'y_pred'])
        test_results[(p, q)] = np.sqrt(mean_squared_error(df.y_true, df.y_pred))

GARCH(2, 2)模型实现了最低的 RMSE(与 GARCH(4, 2)的值相同,但参数较少),因此我们继续估计此模型以检查摘要:

am = ConstantMean(nasdaq_returns.clip(lower=nasdaq_returns.quantile(.05),
                                      upper=nasdaq_returns.quantile(.95)))
am.volatility = GARCH(2, 0, 2)
am.distribution = Normal()
model = am.fit(update_freq=5)
print(model.summary())

输出显示了最大化的对数似然以及通常在选择基于样本内表现的模型时要最小化的 AIC 和 BIC 准则(见第七章,线性模型)。它还显示了均值模型的结果,本例中只是一个常数估计,以及常数 omega 的 GARCH 参数、AR 参数α和 MA 参数β,所有这些参数都具有统计学意义:

现在让我们探讨多个时间序列模型和协整概念,这将实现新的交易策略。

多元时间序列模型

多元时间序列模型旨在同时捕捉多个时间序列的动态,并利用这些序列之间的依赖关系进行更可靠的预测。

方程组

像我们刚刚讨论的 ARMA 方法这样的单变量时间序列模型仅限于目标变量与其滞后值或滞后扰动以及在 ARMAX 情况下的外生序列之间的统计关系。相比之下,多元时间序列模型还允许其他时间序列的滞后值影响目标。这种效应适用于所有系列,导致复杂的相互作用,如下图所示:

除了可能更好的预测之外,多变量时间序列还用于获得跨系列依赖关系的洞察。例如,在经济学中,多变量时间序列用于了解对某一变量的政策变化,例如利率,可能在不同的视角下如何影响其他变量。我们将要看的多变量模型产生的冲击-响应函数就是为此目的而服务的,并且允许我们模拟一个变量如何对其他变量的突然变化做出响应。格兰杰因果分析的概念分析了一个变量是否对另一个变量的预测有用(在最小二乘意义上)。此外,多变量时间序列模型允许对预测误差方差进行分解,以分析其他系列的贡献。

向量自回归 (VAR) 模型

我们将看到向量自回归 VAR(p) 模型如何通过创建包含所有 k 系列的 p 拖后值的 k 方程系统来扩展 AR(p) 模型。在最简单的情况下,k=2 的 VAR(1) 模型如下所示:

图片

这个模型可以用矩阵形式更加简洁地表示:

图片

自身滞后的系数提供了有关系列动态的信息,而交叉变量系数则提供了有关系列间交互的一些见解。此符号扩展到 k 系列和阶数 p,如下所示:

图片

VAR(p) 模型还需要平稳性,以便从单变量时间序列建模的初始步骤延续下来。首先,探索系列并确定必要的转换,然后应用增广迪基-富勒检验来验证每个系列是否满足平稳性标准,并在否则应用进一步的转换。它可以通过有条件的 OLS 进行估计,初始信息或最大似然,对于正态分布的误差是等效的,但对于其他情况不是。

如果一些或所有的 k 系列是单位根非平稳的,则它们可能是协整的。将单位根概念扩展到多个时间序列意味着两个或多个系列的线性组合是平稳的,因此是均值回归的。VAR 模型没有能力处理这种情况而不进行差分,而应使用向量误差校正模型 (VECM,请参阅 GitHub 上的参考资料)。我们将进一步探讨协整性,因为如果存在并且被假定为持续存在,它可以用于配对交易策略。

滞后阶数的确定也从每个系列的 ACF 和 PACF 中获得线索,但受到同一滞后阶数适用于所有系列的约束。在模型估计之后,残差诊断也要求产生类似白噪声的结果,并且模型选择可以使用样本内信息准则或更好地使用样本外预测性能来交叉验证备选模型设计,如果最终目标是使用模型进行预测。

如在单变量情况中提到的那样,对原始时间序列的预测要求我们在训练模型之前撤消应用于使系列稳态的转换。

如何使用 VAR 模型进行宏观基本面预测

我们将扩展单变量示例,将一系列工业生产的月度数据和美联储数据服务提供的一系列消费者情绪的月度时间序列加入,我们将使用熟悉的pandas-datareader库从 1970 年到 2017 年检索数据:

df = web.DataReader(['UMCSENT', 'IPGMFN'], 'fred', '1970', '2017-12').dropna()
df.columns = ['sentiment', 'ip']

对工业生产系列进行对数转换,并使用这两个系列的滞后 12 个月进行季节性差分产生稳态结果:

df_transformed = pd.DataFrame({'ip': np.log(df.ip).diff(12),
                              'sentiment': df.sentiment.diff(12)}).dropna()

test_unit_root(df_transformed) # see notebook for details and additional plots

          p-value
ip          0.0003
sentiment   0.0000

这使我们得到以下系列:

为了限制输出大小,我们将仅使用前 480 个观测值使用statsmodelsVARMAX实现(允许使用可选的外生变量)来估计一个 VAR(1)模型,使用恒定的趋势:

model = VARMAX(df_transformed.iloc[:480], order=(1,1), trend='c').fit(maxiter=1000)

这导致以下摘要:

输出包含两个时间序列方程的系数,如前述的 VAR(1)示例所述。statsmodels 提供了诊断图来检查残差是否满足白噪声假设,在这种简单情况下并不完全符合:

外样本预测可按以下方式生成:

preds = model.predict(start=480, end=len(df_transformed)-1)

实际值和预测值的可视化显示了预测滞后于实际值,并且无法很好地捕捉非线性的外样本模式:

共整合 - 具有共同趋势的时间序列

综合多变量系列的概念因该过程的所有组成系列可能是单独集成的,但该过程在一个或多个线性组合产生新的稳态系列的意义上并非联合集成而复杂化。

换句话说,两个共整合系列的组合具有一个稳定的均值,这个线性组合会恢复到该均值。具有这种特性的多变量系列被称为共整合。当个别系列积分的次序较高且线性组合减少了积分的总次序时,这也适用。

协整与相关性不同:两个系列可能高度相关,但不一定是协整的。例如,如果两个增长系列是彼此的恒定倍数,它们的相关性将很高,但任何线性组合也将增长而不是回归到均值。

仍然可以将 VAR 分析应用于使用 VAR 模型的误差校正形式的集成过程,该模型使用各个系列的一阶差异加上水平的误差校正项。

测试协整

测试协整有两种主要方法:

  • Engle-Granger 两步法

  • Johansen 程序

Engle-Granger 方法涉及将一个系列回归到另一个系列,然后对回归残差应用 ADF 单位根检验。如果能拒绝原假设,假设残差是平稳的,那么这些系列就是协整的。这种方法的一个关键优势是,回归系数表示使组合平稳的乘数,即均值回归。当利用协整进行配对交易策略时,我们将回到这个方面。另一方面,这种方法仅限于识别成对系列的协整,而不是更大范围的系列组合。

相比之下,Johansen 程序测试了前一节讨论的矢量自回归(VAR)模型受协整约束的限制。更具体地说,从通用 VAR(p) 前述方程两边减去目标向量后,我们得到误差校正模型(ECM)公式

结果修改后的 VAR(p) 方程只有一个向量项在水平上,即不使用算子 Δ 表示差异。协整的性质取决于系数矩阵 Π 的特性,特别是其秩。虽然这个方程在结构上与 ADF 测试设置相似,但现在存在多个共同趋势和积分阶数的潜在星座,因为涉及到多个系列。有关详细信息,请参阅 GitHub 上列出的参考文献,包括关于个别系列尺度化的实际挑战。

如何利用协整进行配对交易策略

配对交易依赖于两个资产价格之间的稳定、均值回归关系。换句话说,两个价格之间的比率或差异,也称为价差,随时间可能会发散,但最终应返回到相同水平。鉴于这样的一对,该策略包括做多(即购买)表现不佳的资产,因为它需要一段时间的表现优异来填补差距。同时,应做空价格向正方向移动而远离价格锚点的资产,以筹集购买资产的资金。

协整正是由一个共同均值锚定的两个价格系列之间的这种稳定关系。假设协整持续存在,最终必须发生收敛,无论是表现不佳的股票上涨还是表现良好的股票下跌。该策略无论如何都将是有利可图的,这具有对一般市场动态进行对冲的额外优势。

然而,价差将不断变化,有时扩大,有时缩小,或者保持不变,因为两种资产同时移动。配对交易的挑战在于通过调整相对持仓来维持对冲头寸,因为价差变化。

在实践中,鉴于一组资产,配对交易策略将通过对每对运行统计测试来寻找协整对。这里的关键挑战是考虑到多重测试偏差,如第六章,机器学习工作流中概述的那样。statsmodels库实现了 Engle-Granger 协整测试和 Johansen 测试。

为了估算价差,运行线性回归以获得两个集成资产价格系列的线性组合的系数,以产生一个平稳的组合系列。如前所述,使用线性回归来估算系数被称为协整的 Engle-Granger 检验。

总结

在本章中,我们探讨了用于单个系列的单变量情况的线性时间序列模型以及用于几个交互系列的多变量模型。我们遇到了预测宏观基本面的应用,用于风险管理的预测资产或组合波动性的模型以及捕获多个宏观系列动态的多元 VAR 模型,以及协整的概念,这是支撑流行的配对交易策略的基础。

类似于前一章,我们看到线性模型为模型增加了许多结构,即它们做出了强有力的假设,这些假设可能需要转换和广泛测试来验证这些假设是否成立。如果是这样,模型的训练和解释就是直接的,而且模型提供了一个很好的基准案例,更复杂的模型可能会改进,我们将在接下来的章节中看到。

第九章:贝叶斯机器学习

在本章中,我们将介绍贝叶斯方法在机器学习中的应用,以及它们对开发和评估算法交易策略时的不同不确定性视角的增值。

贝叶斯统计学使我们能够量化对未来事件的不确定性,并以原则性的方式在新信息到来时优化我们的估计。这种动态方法很好地适应了金融市场的发展性质。当存在较少相关数据且我们需要系统地整合先验知识或假设时,它特别有用。

我们将看到,贝叶斯方法使得对统计指标、参数估计和预测周围的不确定性有更丰富的见解。应用范围从更精细的风险管理到动态更新的预测模型,其中包含了市场环境的变化。资产配置的黑-利特曼方法(见第五章,《策略评估》)可以解释为贝叶斯模型。它计算预期收益,作为市场均衡和投资者观点的加权平均值,每个资产的波动性,跨资产的相关性以及对每个预测的信心。

具体来说,在本章中,我们将涵盖以下主题:

  • 贝叶斯统计如何应用于机器学习

  • 如何使用 PyMC3 进行概率编程

  • 如何定义和训练机器学习模型

  • 如何运行最先进的抽样方法进行近似推断

  • 如何应用贝叶斯机器学习来计算动态夏普比率,构建贝叶斯分类器和估计随机波动性

本章的参考文献、附加材料链接和代码示例位于 GitHub 存储库相应目录中。请按照第一章提供的安装说明进行操作,《交易的机器学习》。

贝叶斯机器学习的工作原理

经典统计学也被称为频率派,因为它将概率解释为长期内事件的相对频率,即在观察了大量试验之后。在概率的背景下,一个事件是一个实验的一个或多个基本结果的组合,比如两个骰子掷出六个相等的结果中的任何一个,或者某个资产价格在某一天下跌 10%或更多。

贝叶斯统计学相反,将概率视为事件发生的信心或信念的度量。贝叶斯概率的观点为主观观点留下了更多的空间,因此,与频率派解释相比,意见之间的差异更大。这种差异在很少发生的事件中最为显著,以至于无法得出客观的长期频率度量。

换句话说,频率学派统计假设数据是来自人群的随机样本,并旨在识别生成数据的固定参数。相反,贝叶斯统计将数据视为已知的,并认为参数是随机变量,其分布可以从数据中推断出来。因此,频率学派方法要求的数据点至少与要估计的参数一样多。另一方面,贝叶斯方法与较小的数据集兼容,并且非常适合逐个样本进行在线学习。

贝叶斯观点对于许多在某些重要方面罕见或独特的现实事件非常有用。例如,下次选举的结果或市场是否会在三个月内崩溃的问题。在每种情况下,既有相关的历史数据,又有随着事件临近而展开的独特情况。

首先,我们将介绍贝叶斯定理,该定理通过将先验假设与新的经验证据相结合,并将得到的参数估计与频率学派的对应估计进行比较,以晶化通过更新信念来更新概念的过程。然后,我们将演示两种贝叶斯统计推断的方法,这些方法能够揭示潜在参数的后验分布,即未观察到的参数,在不同情况下的预期值等:

  1. 共轭先验通过提供闭合形式的解决方案来促进更新过程,但确切的分析方法并不总是可用。

  2. 近似推断模拟了由假设和数据组合而成的分布,并使用该分布的样本来计算统计洞察。

如何从经验证据更新假设

牧师托马斯·贝叶斯在 250 多年前提出的定理利用基本的概率理论规定了概率或信念在相关新信息到达时应该如何变化。以下约翰·梅纳德·凯恩斯的引述体现了贝叶斯主义的思维方式:

“当事实发生变化时,我改变我的想法。先生,你会怎么做?”

它依赖于条件概率和全概率以及链式法则;有关这些概念的评论,请参阅 GitHub 上的参考资料。

信念涉及单个或一组参数 θ(也称为假设)。每个参数可以是离散的或连续的。θ 可以是一个一维统计量,比如(离散的)分类变量的模式,或者(连续的)均值,也可以是一个更高维度的值集,比如一个协方差矩阵或深度神经网络的权重。

频率学派统计的一个关键区别在于,贝叶斯假设被表达为概率分布,而不是参数值。因此,虽然频率学派的推断关注点估计,贝叶斯推断则产生概率分布。

贝叶斯定理通过计算从以下输入中得到的后验概率分布来更新对感兴趣参数的信念,如下图所示:

  • 先验分布指示我们考虑每个可能的假设的可能性有多大。

  • 似然函数输出在给定θ参数的某些值的情况下观察到数据集的概率。

  • 证据度量观察到的数据在所有可能的假设下的可能性。因此,它对所有参数值都是相同的,用于将分子标准化:

贝叶斯定理

后验是先验和似然的乘积,除以证据,反映了假设的更新概率分布,同时考虑了先前的假设和数据。从不同的角度看,先验和似然的乘积来自于将数据和参数的联合分布因子分解的链规则的应用。

对于高维、连续变量,制定变得更加复杂,涉及到(多个)积分。一种替代的制定方法使用赔率来表示后验赔率,作为先验赔率乘以似然比的乘积(有关更多细节,请参见参考资料)。

精确推理:最大后验估计

将贝叶斯规则的实际应用于准确计算后验概率的情况非常有限,因为计算分母中的证据项非常具有挑战性。证据反映了在所有可能的参数值上观察到的数据的概率。它也被称为边际似然,因为它需要通过添加或积分参数的分布来对参数的分布进行边际化。这通常只在具有少量假设值的少量离散参数的简单情况下才可能。

最大后验概率(MAP)估计利用了证据是一个常数因子,将后验缩放以满足概率分布的要求。由于证据不依赖于θ,后验分布与似然和先验的乘积成比例。因此,MAP 估计选择使后验最大化的θ的值,考虑到观察到的数据和先验信念,即后验的模态。

MAP 方法与最大似然估计MLE)不同,MLE 定义了概率分布。MLE 选择使观察到的训练数据的似然函数最大化的参数值θ。

从定义的角度看,MAP 与 MLE 的不同之处在于包括了先验分布。换句话说,除非先验是一个常数,否则 MAP 估计θ将与其 MLE 对应物不同:

最大似然估计解往往反映了频率主义的概率估计应该反映观察到的比例的概念。另一方面,先验对 MAP 估计的影响通常相当于将反映先验假设的数据添加到 MLE 中。例如,一个强烈的先验,即硬币有偏的先验可以通过添加偏斜的试验数据来融入 MLE 背景。

先验分布是贝叶斯模型的重要组成部分。我们现在将介绍一些方便的选择,以便进行分析推断。

如何选择先验

先验应反映参数分布的知识,因为它影响 MAP 估计。如果先验不确定,我们需要进行选择,通常从几个合理的选项中选择。一般来说,证明先验的合理性并通过测试替代是否得出相同结论是一个好的做法。

有几种类型的先验:

  • 客观先验最大化数据对后验的影响。如果参数分布未知,我们可以选择一个无信息的先验,比如在参数值的相关范围内称为平坦先验的均匀分布。

  • 相反,主观先验旨在将模型外部的信息纳入估计中。

  • 经验性先验结合了贝叶斯和频率主义方法,利用历史数据消除主观性,例如通过估计各种时刻以适应标准分布。

在机器学习模型的背景下,先验可以被视为一种正则化器,因为它限制了后验可以假设的值。例如,具有零先验概率的参数不是后验分布的一部分。一般来说,更多的好数据可以得出更强的结论并减少先验的影响。

如何保持推断简单 - 共轭先验

当结果后验与先验具有相同类型的分布,只是参数不同时,先验分布与似然的共轭性。当先验和似然都是正态分布时,后验也是正态分布的。

先验和似然的共轭性暗示了后验的闭合形式解,从而便于更新过程并避免使用数值方法来近似后验。此外,由此产生的后验可以用作下一个更新步骤的先验。

让我们使用一个股价波动的二元分类示例来说明这个过程。

如何动态估计资产价格波动的概率

当数据由具有某种成功概率的二元伯努利随机变量组成时,重复试验中的成功次数遵循二项分布。共轭先验是支持区间[0, 1]上的 Beta 分布,并具有两个形状参数,用于对成功概率进行任意先验分布建模。因此,后验分布也是一个 Beta 分布,我们可以通过直接更新参数来得到。

我们将收集不同大小的二元化日度标准普尔 500 指数收益率样本,其中正面结果是价格上涨。从一个不含信息的先验开始,该先验将每个可能的成功概率在区间[0, 1]内分配相等的概率,我们计算不同证据样本的后验概率。

下面的代码示例显示了更新只是简单地将观察到的成功和失败数量添加到先验分布的参数中以获得后验分布:

n_days = [0, 1, 3, 5, 10, 25, 50, 100, 500]
outcomes = sp500_binary.sample(n_days[-1])
p = np.linspace(0, 1, 100)

# uniform (uninformative) prior
a = b = 1
for i, days in enumerate(n_days):
    up = outcomes.iloc[:days].sum()
    down = days - up
    update = stats.beta.pdf(p, a + up , b + down)

以下图表中绘制了结果后验分布。它们说明了从将所有成功概率视为同等可能的均匀先验到越来越尖峰的分布的演变。

经过 500 个样本,概率集中在 2010 年至 2017 年间正面走势的实际概率约为 54.7%。它还显示了 MLE 和 MAP 估计之间的小差异,后者倾向于稍微朝向均匀先验的期望值,如下图所示:

后验概率

在实践中,共轭先验的使用仅限于低维情况。此外,简化的 MAP 方法避免了计算证据项,但即使在其可用时也具有几个缺点;它不返回分布,因此我们无法推导出不确定性的度量,或将其用作先验。因此,我们需要采用数值方法和随机模拟而不是精确推理,我们将在下文介绍。

近似推理:随机与确定性方法

对于大多数实际相关的模型,将无法通过分析方法推导出精确的后验分布并计算潜在参数的期望值。模型可能具有太多的参数,或者后验分布可能对于分析解而言过于复杂。对于连续变量,积分可能没有封闭形式的解,而空间的维数和被积函数的复杂性可能会阻止数值积分。对于离散变量,边缘化涉及对隐藏变量的所有可能配置求和,虽然原则上这总是可能的,但在实践中,我们经常发现可能存在指数多个隐藏状态,因此精确计算是非常昂贵的。

尽管对于某些应用而言,对未观察到的参数的后验分布可能是感兴趣的,但通常主要是要评估期望,例如,进行预测。在这种情况下,我们可以依赖近似推断:

  • 基于马尔可夫链蒙特卡罗(MCMC)抽样的随机技术已经在许多领域推广了贝叶斯方法的使用。它们通常具有收敛到精确结果的能力。在实践中,抽样方法可能需要大量计算,并且通常仅限于小规模问题。

  • 确定性方法,即变分推断或变分贝叶斯,基于对后验分布的解析近似,并且可以很好地扩展到大型应用程序。它们做出简化假设,例如,后验在特定方式上因子化,或者具有特定的参数形式,例如高斯分布。因此,它们不会生成精确结果,并且可以用作抽样方法的补充。

基于抽样的随机推断

抽样是关于从给定分布p(x)中抽取样本,X=(x[1], ..., x[n])。假设样本是独立的,大数定理确保对于增长的样本数量,给定实例x[i]在样本中的比例(对于离散情况)对应于其概率,p(x=x[i])。在连续情况下,类似的推理适用于样本空间的给定区域。因此,对样本的平均值可以用作分布参数的期望值的无偏估计。

实际挑战在于确保独立抽样,因为分布是未知的。相关样本可能仍然是无偏的,但倾向于增加估计的方差,因此将需要更多的样本来获得与独立样本一样精确的估计。

从多元分布中抽样在计算上是具有挑战性的,因为随着维数的增加,状态数量呈指数增长。许多算法简化了这个过程(请参阅参考文献以获取概述)。现在,我们将介绍几种基于 MCMC 的流行变体方法。

马尔可夫链蒙特卡罗抽样

马尔可夫链是描述随机漫步的动态随机模型,由转移概率连接的一组状态组成。马尔可夫性质规定该过程没有记忆,并且下一步仅取决于当前状态。换句话说,它在于当前状态的条件下,过去、现在和未来是独立的,即过去状态的信息不会帮助预测未来超出我们从现在所知道的内容。

蒙特卡罗方法依赖于重复的随机抽样来近似可能是确定性的结果,但不允许解析的精确解。它是在曼哈顿计划期间开发的,用于估计原子级别的能量,并获得了其持久的代号以确保保密性。

许多算法将蒙特卡洛方法应用于马尔可夫链,并通常按以下方式进行:

  1. 从当前位置开始。

  2. 从提议分布中抽取一个新的位置。

  3. 在考虑数据和先验分布的情况下评估新位置的概率:

    1. 如果足够可能,移动到新的位置。

    2. 否则,保持当前位置不变。

  4. 从步骤 1 开始重复。

  5. 经过一定数量的迭代后,返回所有接受的位置。

MCMC 旨在识别和探索后验的有趣区域,这些区域集中在显著的概率密度上。当它持续移动到后验的附近高概率状态时,无记忆的过程被认为是收敛的,其中接受率增加。一个关键挑战是平衡对样本空间的随机探索的需要和降低接受率的风险。

此过程的初始步骤可能更反映出起始位置而不是后验,并且通常被丢弃为burn-in样本。 MCMC 的一个关键特性是在一定数量的迭代后,过程应该忘记其初始位置。

剩余的样本被称为过程的轨迹。假设收敛,则样本的相对频率近似于后验,可以根据大数定律计算期望值。

正如之前所指出的,估计的精度取决于随机游走收集的样本的串行相关性,每个样本设计上仅取决于前一个状态。更高的相关性限制了对后验的有效探索,并需要经过诊断测试。

设计这样的马尔可夫链的一般技术包括 Gibbs 采样、Metropolis-Hastings 算法和更近期的哈密顿 MCMC 方法,这些方法往往表现更好。

Gibbs 采样

Gibbs 采样将多变量采样简化为一系列一维抽样。从一个起始点开始,它迭代地将 n-1 个变量保持不变,同时抽样第 n 个变量。它将这个样本合并并重复。

该算法非常简单易实现,但产生高度相关的样本,导致收敛速度减慢。其顺序性也阻止了并行化。

Metropolis-Hastings 采样

Metropolis-Hastings 算法基于其当前状态随机提出新的位置,以有效地探索样本空间并相对于 Gibbs 采样减少样本的相关性。为了确保它从后验中采样,它使用先验和似然的乘积来评估提议,这与后验成比例。它根据结果接受的概率来接受,这与当前样本的相应值相关。

提议评估方法的一个关键优点是它使用比后验的确切评估更比例的评估。但是,它可能需要很长时间才能收敛,因为与后验无关的随机移动可能会降低接受率,以至于大量步骤仅产生少量(可能相关的)样本。接受率可以通过减小提议分布的方差来调整,但是由此产生的较小步骤意味着较少的探索。

哈密顿蒙特卡洛 – 走 NUTS

哈密顿蒙特卡洛(HMC)是一种混合方法,利用似然梯度的一阶导数信息来提出新的状态以进行探索,并克服了 MCMC 的一些挑战。此外,它还融合了动量以有效地在后验分布中跳跃。因此,与简单的随机游走 Metropolis 或 Gibbs 采样相比,它更快地收敛到高维目标分布。

无转弯采样器是自调节的 HMC 扩展,它自适应地调节在选择提议之前围绕后验的大小和数量。它在高维和复杂的后验分布上表现良好,并且允许拟合许多复杂的模型,而无需对拟合算法本身具有专门的知识。正如我们将在下一节中看到的,它是 PyMC3 中的默认采样器。

变分推断

变分推断(VI)是一种通过优化来近似概率密度的机器学习方法。在贝叶斯背景下,它近似后验分布如下:

  1. 选择一个参数化的概率分布族

  2. 找到该族中距目标最近的成员,以 Kullback-Leibler 散度为度量

与 MCMC 相比,变分贝叶斯往往收敛更快,并且更适用于大型数据。虽然 MCMC 通过链中的样本来近似后验,最终将收敛到任意接近目标,但变分算法通过优化结果来近似后验,这不能保证与目标重合。

变分推断更适合于大型数据集和快速探索多个模型。相比之下,当数据集较小或时间和计算资源的约束较少时,MCMC 将提供更准确的结果。

自动微分变分推断(ADVI)

变分推断的缺点是需要模型特定的导数和实现一个定制的优化例程,这减慢了广泛采用的速度。

最近的自动微分变分推断(ADVI)算法自动化了这个过程,用户只需指定模型,以程序形式表达,并且 ADVI 自动生成相应的变分算法(有关实现细节请参阅 GitHub 上的参考资料)。

我们将看到 PyMC3 支持各种变分推断技术,包括 ADVI。

使用 PyMC3 进行概率编程

概率编程提供了一种描述和拟合概率分布的语言,以便我们可以设计、编码和自动估计和评估复杂模型。它旨在抽象掉一些计算和分析复杂性,以使我们能够专注于贝叶斯推理和推断的概念上更为直观和简单的方面。

由于新语言的出现,该领域变得非常动态。Uber 开源了基于 PyTorch 的 Pyro,并且 Google 最近为 TensorFlow 添加了一个概率模块(请参阅 GitHub 上链接的资源)。

结果是,贝叶斯方法在机器学习中的实际相关性和使用可能会增加,以生成关于不确定性的洞见,特别是对于需要透明而不是黑盒模型的用例。

在本节中,我们将介绍流行的 PyMC3 库,该库使用 Python 实现了高级 MCMC 采样和变分推断,用于机器学习模型。与 Stan 一起,以 Monte Carlo 方法的发明者 Stanislaw Ulam 命名,并由哥伦比亚大学的 Andrew Gelman 自 2012 年以来开发,它是最受欢迎的概率编程语言之一。

使用 Theano 的贝叶斯机器学习

PyMC3 于 2017 年 1 月发布,以向 PyMC2(2012 年发布)中使用的 Metropolis-Hastings 采样器添加 Hamiltonian MC 方法。PyMC3 使用 Theano 作为其计算后端,用于动态 C 编译和自动微分。Theano 是一个以矩阵为重点且启用 GPU 的优化库,是由 Yoshua Bengio 的蒙特利尔机器学习算法研究所(MILA)开发的,并受到 TensorFlow 的启发。由于新的深度学习库的成功(有关详细信息,请参阅第十六章《深度学习》),MILA 最近停止进一步开发 Theano。PyMC4 计划于 2019 年使用 TensorFlow,对 API 的影响可能有限。

PyMC3 工作流程

PyMC3 的目标是直观且可读,但功能强大的语法,反映了统计学家描述模型的方式。建模过程通常遵循以下五个步骤:

  1. 通过定义以下内容来编码概率模型:

    1. 量化关于潜变量的知识和不确定性的先验分布

    2. 条件参数在观察数据上的似然函数

  2. 使用上一节中描述的选项之一分析后验:

    1. 使用 MAP 推断获得一个点估计

    2. 使用 MCMC 方法从后验中采样

  3. 使用变分贝叶斯近似后验。

  4. 使用各种诊断工具检查您的模型。

  5. 生成预测。

生成的模型可用于推断,以获取参数值的详细洞察,以及预测新数据点的结果。

我们将使用简单的 logistic 回归来说明这个工作流程(参见笔记本 bayesian_logistic_regression)。随后,我们将使用 PyMC3 来计算和比较贝叶斯夏普比率,估计动态配对交易比率,并实现贝叶斯线性时间序列模型。

模型定义 - 贝叶斯 logistic 回归

如第六章所讨论的,机器学习工作流程, logistic 回归估计一组特征与二进制结果之间的线性关系,通过 S 形函数进行调节,以确保模型产生概率。 频率方法导致参数的点估计,这些参数测量了每个特征对数据点属于正类的概率的影响,并且置信区间基于关于参数分布的假设。

相比之下,贝叶斯 logistic 回归估计参数本身的后验分布。 后验允许对每个参数的贝叶斯可信区间进行更健壮的估计,其优点在于更透明地了解模型的不确定性。

概率程序由观察到的和未观察到的随机变量(RVs)组成。正如我们所讨论的,我们通过似然分布定义观察到的 RVs,通过先验分布定义未观察到的 RVs。PyMC3 包含许多用于此目的的概率分布。

我们将使用一个简单的数据集,使用每年$50K 的收入门槛将 30,000 个个体分类。此数据集将包含关于年龄、性别、工作小时和教育年限的信息。因此,我们正在使用这些特征对一个人的收入是否超过$50K 的概率进行建模。

PyMC3 库使得对 logistic 回归执行近似贝叶斯推断非常简单。 logistic 回归模型根据以下图表左侧的方式对个体 i 基于 k 个特征的高收入的概率进行建模:

我们将使用上下文管理器 with 来定义一个 manual_logistic_model,以便稍后将其作为概率模型参考:

  1. 未观察到的截距和两个特征的参数的随机变量使用假设的先验进行表达,该先验假设为正态分布,均值为 0,标准差为 100。

  2. 似然函数根据 logistic 回归的规范将参数与数据结合起来。

  3. 结果被建模为 Bernoulli 随机变量,其成功概率由似然函数给出:

with pm.Model() as manual_logistic_model:
    # coefficients as rvs with uninformative priors
    intercept = pm.Normal('intercept', 0, sd=100)
    b1 = pm.Normal('beta_1', 0, sd=100)
    b2 = pm.Normal('beta_2', 0, sd=100)

    # Likelihood transforms rvs into probabilities p(y=1)
    # according to logistic regression model.    
    likelihood = pm.invlogit(intercept + b1 * data.hours + b2 * data.educ)

    # Outcome as Bernoulli rv with success probability 
    # given by sigmoid function conditioned on actual data 
    pm.Bernoulli(name='logit', p=likelihood, observed=data.income)

可视化和板符号

命令 pm.model_to_graphviz(manual_logistic_model) 生成在右侧图中显示的 plate 符号。它显示未观察到的参数为浅色,观察到的元素为深色圆圈。矩形表示由模型定义中包含的数据暗示的观察模型元素的重复次数。

广义线性模型模块

PyMC3 包含许多常见的模型,因此我们通常可以留下自定义应用程序的手动规范。以下代码使用受统计语言 R 启发的公式格式,并由 patsy 库移植到 Python,将相同的逻辑回归定义为 广义线性模型 (GLM) 家族的成员:

with pm.Model() as logistic_model:  
    pm.glm.GLM.from_formula('income ~ hours + educ', 
                            data, 
                            family=pm.glm.families.Binomial())

MAP 推断

我们使用刚刚定义的模型的 .find_MAP() 方法为三个参数获得点 MAP 估计值:

with logistic_model:
    map_estimate = pm.find_MAP()
print_map(map_estimate)
Intercept   -6.561862
hours        0.040681
educ         0.350390

PyMC3 使用拟牛顿 Broyden-Fletcher-Goldfarb-Shanno (BFGS) 算法解决了找到具有最高密度的后验点的优化问题,但提供了几种替代方案,这些替代方案由 sciPy 库提供。结果几乎与相应的 statsmodels 估计相同(有关更多信息,请参阅笔记本)。

近似推断 - MCMC

我们将使用稍微复杂的模型来说明马尔可夫链蒙特卡洛推断:

formula = 'income ~ sex + age+ I(age ** 2) + hours + educ'

Patsy 的函数 I() 允许我们使用常规 Python 表达式动态创建新变量。在这里,我们将 age 平方以捕获更多经验在生活后期增加收入的非线性关系。

请注意,测量尺度非常不同的变量可能会减慢采样过程。因此,我们首先对 agehourseduc 变量进行标准化,应用 sklearn 的 scale() 函数。

一旦我们用新公式定义了我们的模型,我们就可以执行推断以近似后验分布。通过 pm.sample() 函数可用 MCMC 采样算法。

默认情况下,PyMC3 自动选择最有效的采样器,并初始化采样过程以实现有效的收敛。对于连续模型,PyMC3 选择我们在前一节中讨论的 NUTS 采样器。它还通过 ADVI 运行变分推断,以找到采样器的良好起始参数。其中一个选择是使用 MAP 估计。

为了查看收敛情况,我们首先在调整了采样器 1000 次迭代后仅绘制 100 个样本。此后将丢弃这些样本。采样过程可以使用 cores 参数并行化多个链(除非使用 GPU):

with logistic_model:
    trace = pm.sample(draws=100, tune=1000,
                      init='adapt_diag', # alternative initialization
                      chains=4, cores=2,
                      random_seed=42)

结果跟踪包含每个随机变量的采样值。我们可以通过提供先前运行的跟踪作为输入来继续采样(有关更多信息,请参阅笔记本)。

置信区间

我们可以计算可信区间—贝叶斯的置信区间—作为跟踪的百分位数。结果的边界反映了对于给定概率阈值的参数值范围的信心,而不是参数将在大量试验中多少次在此范围内的数量。笔记本演示了计算和可视化。

近似推断 - 变分贝叶斯

变分推断的界面与 MCMC 实现非常相似。我们只需使用fit()函数而不是sample()函数,还可以选择包括一个早期停止的CheckParametersConvergence回调,如果分布拟合过程收敛到给定的容差:

with logistic_model:
    callback = CheckParametersConvergence(diff='absolute')
    approx = pm.fit(n=100000, 
                    callbacks=[callback])

我们可以从近似分布中抽取样本,以获得类似于之前对 MCMC 采样器所做的跟踪对象:

trace_advi = approx.sample(10000)

检查跟踪摘要显示结果略微不准确。

模型诊断

贝叶斯模型诊断包括验证采样过程是否收敛,并且始终从后验的高概率区域中采样,并确认模型是否很好地代表了数据。

收敛

我们可以随时间和它们的分布可视化样本,以检查结果的质量。下面的图表显示了初始 100 和额外的 100,000 个样本后的后验分布,并说明了收敛意味着多个链识别相同的分布。pm.trace_plot()函数也显示了样本的演变(更多信息请参见笔记本):

后验分布

PyMC3 为采样器生成各种摘要统计信息。这些信息可以作为 stats 模块中的各个函数提供,或者通过将跟踪提供给pm.summary()函数获取:

statsmodels 均值 标准差 hpd_2.5 hpd_97.5 n_eff Rhat
截距 -1.97 -1.97 0.04 -2.04 -1.89 69,492.17 1.00
性别[T.男性] 1.20 1.20 0.04 1.12 1.28 72,374.10 1.00
年龄 1.10 1.10 0.03 1.05 1.15 68,446.73 1.00
I(年龄 ** 2) -0.54 -0.54 0.02 -0.58 -0.50 66,539.66 1.00
小时 0.32 0.32 0.02 0.28 0.35 93,008.86 1.00
教育 0.84 0.84 0.02 0.80 0.87 98,125.26 1.00

前面的表格中在第一列包括了(分别计算的)statsmodelslogit系数,以显示在这个简单案例中,两个模型是一致的,因为样本均值非常接近系数。

剩下的列包含了最高后验密度HPD)的估计,用于最小宽度可信区间,这是置信区间的贝叶斯版本,在这里是以 95% 水平计算的。n_eff统计信息总结了 ~100K 绘制结果的有效(未拒绝)样本数量。

R-hat,也称为 Gelman-Rubin 统计量,通过比较链之间的方差与每个链内的方差来检查收敛性。如果采样器收敛,则这些方差应该相同,即链应该看起来相似。因此,统计量应该接近 1。pm.forest_plot() 函数还为多个链总结了此统计量(有关更多信息,请参见笔记本)。

对于具有许多变量的高维模型,检查大量轨迹变得繁琐。使用 NUTS 时,能量图有助于评估收敛问题。它总结了随机过程如何有效地探索后验分布。图表显示了能量和能量转移矩阵,它们应该是相匹配的,如下面的示例所示(有关概念细节,请参见参考资料):

后验预测检查

后验预测检查PPCs)非常有用,用于检查模型与数据的拟合程度。它们通过使用来自后验分布的参数生成模型数据来实现此目的。我们使用 pm.sample_ppc 函数进行此操作,并为每个观测值获取 n 个样本(GLM 模块自动将结果命名为 'y'):

ppc = pm.sample_ppc(trace_NUTS, samples=500, model=logistic_model)
ppc['y'].shape
(500, 29170)

我们可以使用 auc 分数来评估样本内拟合,例如,比较不同模型:

roc_auc_score(y_score=np.mean(ppc['y'], axis=0), 
              y_true=data.income)
0.8294958565103577

预测

在运行后验预测检查之前,预测使用 Theano 的共享变量将训练数据替换为测试数据。为了方便可视化,我们创建一个带有单个预测器小时的变量,创建训练和测试数据集,并将前者转换为共享变量。请注意,我们需要使用 numPy 数组并提供列标签列表(有关详细信息,请参见笔记本):

X_shared = theano.shared(X_train.values
with pm.Model() as logistic_model_pred:
    pm.glm.GLM(x=X_shared, labels=labels,
               y=y_train, family=pm.glm.families.Binomial())

然后我们像之前一样运行采样器,并在用测试数据替换训练数据后对结果的迹线应用 pm.sample_ppc 函数:

X_shared.set_value(X_test)
ppc = pm.sample_ppc(pred_trace, model=logistic_model_pred,
                    samples=100)

单特征模型的 AUC 分数为 0.65。以下图表显示了每个采样预测器值的实际结果和预测周围的不确定性:

我们现在将说明如何将贝叶斯分析应用于与交易相关的用例。

实际应用

贝叶斯机器学习方法在投资领域有许多应用。概率估计产生的透明度对风险管理和绩效评估自然非常有用。我们将说明如何计算和比较诸如夏普比率之类的指标。GitHub 仓库还包括下面引用的两个笔记本,展示了将贝叶斯 ML 用于建模线性时间序列和随机波动性的用法。

这些笔记本已经改编自 Quantopian 上创建的教程,Thomas Wiecki 领导数据科学,并且在推广贝叶斯方法的使用方面做出了重大贡献。参考资料还包括有关使用贝叶斯 ML 估计配对交易套期保值比率的教程。

贝叶斯夏普比率和表现比较

在本节中,我们将说明如何将夏普比率定义为概率模型,并比较不同收益序列的后验分布。对两组的贝叶斯估计提供了完整的可信值分布,包括效应大小、组均值及其差异、标准差及其差异以及数据的正态性。

主要用例包括分析替代策略之间的差异,或者分析策略的样本内收益与样本外收益之间的差异(详见bayesian_sharpe_ratio笔记本)。贝叶斯夏普比率也是 pyfolio 贝叶斯分析表的一部分。

模型定义

为了将夏普比率建模为概率模型,我们需要关于收益分布和控制此分布的参数的先验。学生 t 分布相对于低自由度df)的正态分布具有较厚的尾部,是捕捉收益这一方面的合理选择。

因此,我们需要对这个分布的三个参数进行建模,即收益的均值和标准差,以及自由度。我们假设均值和标准差分别服从正态和均匀分布,并且自由度服从具有足够低期望值的指数分布,以确保有厚尾。收益基于这些概率输入,并且年化夏普比率是通过标准计算得出的,忽略了无风险利率(使用每日收益):

mean_prior = data.stock.mean()
std_prior = data.stock.std()
std_low = std_prior / 1000
std_high = std_prior * 1000

with pm.Model() as sharpe_model:
    mean = pm.Normal('mean', mu=mean_prior, sd=std_prior)
    std = pm.Uniform('std', lower=std_low, upper=std_high)
    nu = pm.Exponential('nu_minus_two', 1 / 29, testval=4) + 2.
    returns = pm.StudentT('returns', nu=nu, mu=mean, sd=std, observed=data.stock)

    sharpe = returns.distribution.mean / returns.distribution.variance ** .5 * np.sqrt(252)
    pm.Deterministic('sharpe', sharpe)

该笔记本包含有关对单个股票进行采样和评估夏普比率的详细信息。

表现比较

为了比较两个收益序列的表现,我们分别对每个组的夏普比率建模,并将效应大小计算为波动率调整后收益之间的差异。通过可视化轨迹,可以深入了解每个指标的分布情况,如下图所示:

用于配对交易的贝叶斯线性回归

在最后一章中,我们介绍了配对交易作为一种流行的算法交易策略,它依赖于两个或更多资产的协整性。给定这样的资产,我们需要估计对冲比率以决定多头和空头仓位的相对大小。基本方法使用线性回归。

linear_regression笔记本说明了贝叶斯线性回归如何跟踪两个资产之间随时间变化的关系。

贝叶斯时间序列模型

PyMC3 包括允许我们对参数不确定性进行类似洞察的 AR(p)模型,与先前的模型相同。bayesian_time_series笔记本说明了一个或多个滞后的时间序列模型。

随机波动模型

正如上一章所讨论的,资产价格具有时变波动性。在某些时期,回报变动很大,而在其他时期则非常稳定。随机波动模型使用潜在波动性变量来建模,该变量被建模为随机过程。无 U 转折采样器是使用这种模型引入的,并且stochastic_volatility笔记本展示了这种用法。

总结

在本章中,我们探讨了机器学习的贝叶斯方法。我们发现它们具有几个优点,包括能够编码先验知识或观点、更深入地了解模型估计和预测周围的不确定性,以及适用于在线学习,在这种情况下,每个训练样本逐渐影响模型的预测。

我们学会了从模型规范到估计、诊断和预测应用贝叶斯工作流程,使用 PyMC3 并探索了几个相关应用。我们将在第十四章中遇到更多贝叶斯模型,主题建模,以及在第十九章中介绍无监督深度学习,在那里我们将介绍变分自动编码器。

接下来的两章介绍基于树的、非线性的集成模型,即随机森林和梯度提升机。

第十章:决策树和随机森林

在本章中,我们将学习两种新的机器学习模型类:决策树和随机森林。我们将看到决策树如何从数据中学习规则,这些规则编码了输入和输出变量之间的非线性关系。我们将说明如何训练决策树并用于回归和分类问题的预测,可视化和解释模型学习到的规则,并调整模型的超参数以优化偏差-方差的权衡并防止过拟合。决策树不仅是重要的独立模型,而且经常被用作其他模型的组成部分。

在本章的第二部分,我们将介绍集成模型,这些模型将多个个体模型组合起来,产生一个具有较低预测误差方差的单一聚合预测。我们将说明自助聚合,通常称为 bagging,作为随机化构建个体模型和减少集成组件预测误差相关性的几种方法之一。

提升是一个非常强大的替代方法,值得拥有自己的章节来讨论一系列最近的发展。我们将说明如何有效地降低方差,并学习如何配置、训练和调整随机森林。我们将看到随机森林作为大量决策树的集合,可以大幅减少预测误差,但会以一定的解释损失为代价。

简而言之,在本章中,我们将涵盖以下内容:

  • 如何使用决策树进行回归和分类

  • 如何从决策树中获得见解,并可视化从数据中学到的决策规则

  • 为什么集成模型往往能够产生更优异的结果

  • 自助聚合如何解决决策树的过拟合挑战

  • 如何训练、调整和解释随机森林

决策树

决策树是一种机器学习算法,它根据从训练数据中学到的决策规则来预测目标变量的值。该算法可以通过改变控制树学习决策规则的目标函数来应用于回归和分类问题。

我们将讨论决策树如何使用规则进行预测,如何训练它们以预测(连续)收益以及(分类)价格走势的方向,以及如何有效地解释、可视化和调整它们。

树如何学习和应用决策规则

我们在第七章和第八章中学习的线性模型通过学习一组参数来预测结果,使用输入变量的线性组合,可能在 logistic 回归的情况下通过 S 形链函数进行转换。

决策树采取了不同的方法:它们学习并顺序地应用一组规则,将数据点分割为子集,然后为每个子集进行一次预测。预测基于应用给定规则序列后产生的训练样本子集的结果值。正如我们将在后面更详细地看到的那样,分类树根据相对类频率或直接的多数类值来预测概率,而回归模型根据可用数据点的结果值均值计算预测。

这些规则中的每一个都依赖于一个特定的特征,并使用阈值将样本分为两组,其值要么低于要么高于该特征的阈值。二叉树自然地表示模型的逻辑:根是所有样本的起点,节点表示决策规则的应用,数据沿着边移动,分割成更小的子集,直到到达一个叶节点,模型在这里做出预测。

对于线性模型,参数值允许解释输入变量对输出和模型预测的影响。相比之下,对于决策树,从根到叶子的路径显示了特征及其值如何通过模型导致特定决策的透明度。

以下图突出显示了模型学习规则的过程。在训练期间,算法扫描特征,并且对于每个特征,试图找到一个分割数据的截断,以最小化由分割产生的损失,加权每个子集中的样本数:

图

要在训练期间构建整个树,学习算法重复这个划分特征空间的过程,即可能值集合为p个输入变量X[1], X[2], ..., X[p],划分为互斥且集合完备的区域,每个区域由一个叶节点表示。不幸的是,由于特征和阈值序列的可能组合数量爆炸性增长,算法将无法评估特征空间的每种可能分区。基于树的学习采用自顶向下、贪婪的递归二分拆分方法来克服这种计算限制。

这个过程是递归的,因为它使用了由先前分割产生的数据子集。它是自顶向下的,因为它从树的根节点开始,所有观察结果仍然属于单个区域,然后通过向预测器空间添加一个更多的分割来连续创建树的两个新分支。它是贪婪的,因为算法选择了基于对目标函数的直接影响的最佳规则,而不是展望未来并评估几步之后的损失。我们将在回归和分类树的更具体的上下文中返回到分割逻辑,因为这代表了主要的区别。

随着递归分割向树中添加新节点,训练样本的数量将继续减少。如果规则均匀地分割样本,导致完全平衡的树,每个节点在水平n处将有 2^(n )个节点,每个节点包含总观察数量的相应部分。在实践中,这是不太可能的,因此沿着某些分支的样本数量可能会迅速减少,并且树往往沿着不同路径增长到不同的深度。

为了对新观察结果进行预测,模型使用在训练期间推断出的规则来决定数据点应分配到哪个叶节点,然后使用对应特征空间区域的训练观察结果的均值(用于回归)或众数(用于分类)。特征空间中给定区域的训练样本数较少,即在给定叶节点中,会降低对预测的信心,并可能反映出过度拟合。

递归分割将继续,直到每个叶节点只包含单个样本,并且训练误差已降低到零。我们将介绍几个准则来限制分割并防止决策树产生极端过拟合的自然倾向。

如何在实践中使用决策树

在本节中,我们说明如何使用基于树的模型来获得洞察并进行预测。为了演示回归树,我们预测回报,对于分类情况,我们回到了正面和负面资产价格变动的示例。本节的代码示例在笔记本decision_trees中,除非另有说明。

如何准备数据

我们使用了在 Chapter 4 中构建的数据集的简化版本,Alpha Factor Research。它包含了 Quandl 提供的 2010-2017 年期间的每日股票价格以及各种工程特征。具体详情可以在本章的 GitHub 存储库中的data_prep笔记本中找到。本章中的决策树模型不能处理缺失或分类变量,因此我们将在丢弃任何缺失值后对后者应用虚拟编码。

如何编写自定义交叉验证类

我们还构建了一个针对刚刚创建的数据格式的自定义交叉验证类,该数据具有两个级别的 pandas MultiIndex,一个用于股票代码,另一个用于数据:

class OneStepTimeSeriesSplit:
    """Generates tuples of train_idx, test_idx pairs
    Assumes the index contains a level labeled 'date'"""

    def __init__(self, n_splits=3, test_period_length=1, shuffle=False):
        self.n_splits = n_splits
        self.test_period_length = test_period_length
        self.shuffle = shuffle
        self.test_end = n_splits * test_period_length

    @staticmethod
    def chunks(l, chunk_size):
        for i in range(0, len(l), chunk_size):
            yield l[i:i + chunk_size]

    def split(self, X, y=None, groups=None):
        unique_dates = (X.index
                        .get_level_values('date')
                        .unique()
                        .sort_values(ascending=False)[:self.test_end])

        dates = X.reset_index()[['date']]
        for test_date in self.chunks(unique_dates, self.test_period_length):
            train_idx = dates[dates.date < min(test_date)].index
            test_idx = dates[dates.date.isin(test_date)].index
            if self.shuffle:
                np.random.shuffle(list(train_idx))
            yield train_idx, test_idx

OneStepTimeSeriesSplit 确保了训练和验证集的分割,通过仅使用每只股票的数据直到T-1期进行训练,当使用T月份的数据进行验证时避免了前瞻性偏差。我们将只使用一步预测。

如何构建回归树

回归树基于分配给给定节点的训练样本的平均结果值进行预测,并且通常依赖于均方误差来在递归二分割过程中选择最佳规则。

给定一个训练集,该算法迭代遍历预测变量 X[1], X[2], ..., X[p] 和可能的切分点 s[1], s[1], ..., s[N],以找到最佳组合。最佳规则将特征空间分成两个区域,{X|X[i] < s[j]}{X|X[i] > s[j]**},其中 X[i] 特征的值要么低于要么高于 s[j] 阈值,以便基于训练子集的预测最大化相对于当前节点的平方残差的减少。

让我们从一个简化的例子开始,以便于可视化,并仅使用两个月的滞后收益来预测以下月份,类似于上一章的 AR(2) 模型:

使用sklearn,配置和训练回归树非常简单:

from sklearn.tree import DecisionTreeRegressor

# configure regression tree
regression_tree = DecisionTreeRegressor(criterion='mse', # default
                                        max_depth=4,     # up to 4 splits
                                        random_state=42)
# Create training data
y = data.returns
X = data.drop('returns', axis=1)
X2 = X.loc[:, ['t-1', 't-2']]

# fit model
regression_tree.fit(X=X2, y=y)

# fit OLS model
ols_model = sm.OLS(endog=y, exog=sm.add_constant(X2)).fit()

OLS 摘要和决策树的前两个层级的可视化揭示了模型之间的显著差异。OLS 模型提供了三个参数用于截距和两个特征,与该模型对 f 函数的线性假设一致。

相反,回归树图显示了前两个层级的每个节点的特征和阈值用于分割数据(请注意,特征可以重复使用),以及当前均方误差MSE)的值、样本数量和基于这些训练样本的预测值:

回归树图

该树图还突出显示了样本在节点之间的不均匀分布,只经过两次分裂后,样本数量在 28,000 到 49,000 之间变化。

为了进一步说明关于输入变量与输出之间功能形式关系的不同假设,我们可以将当前收益的预测可视化为特征空间的函数,即作为滞后收益值范围的函数。下图显示了线性回归和回归树的当前期收益作为一期和两期前收益的函数:

右侧的线性回归模型结果突显了滞后和当前收益之间的线性关系,而左侧的回归树图表说明了特征空间的递归划分中编码的非线性关系。

如何构建分类树

分类树的工作方式与回归版本相同,只是结果的分类性质需要不同的方法来进行预测和测量损失。虽然回归树使用相关训练样本的平均结果来预测分配给叶子节点的观测值的响应,但分类树则使用模式,即相关区域中训练样本中最常见的类别。分类树还可以基于相对类频率生成概率预测。

如何优化节点纯度

在构建分类树时,我们也使用递归二分分裂,但是,我们不是使用减少均方误差来评估决策规则的质量,而是可以使用分类错误率,它简单地是给定(叶子)节点中不属于最常见类别的训练样本的比例。

然而,更倾向于使用替代测量方法,如基尼指数或交叉熵,因为它们对节点纯度的敏感性更高,而不是分类错误率。节点纯度是指节点中单一类别的主导程度。一个只包含属于单一类别结果的样本的节点是纯净的,并且暗示着该特征空间区域的成功分类。对于一个分类结果取K个值,0,1,…,K-1,对于表示特征空间区域的给定节点m,其中p[mk]是节点m中第k类结果的比例,它们的计算如下:

当类别比例接近零或一时,基尼不纯度和交叉熵测量值较小,也就是说,当子节点由于分裂而变得纯净时,它们的值最高,并且在二分类情况下,当类别比例均匀或为 0.5 时,它们的值最高。本节末尾的图表显示了这两个测量值和误分类错误率在比例区间 [0, 1] 内的取值情况。

如何训练分类树

我们现在将使用 80% 的样本进行训练,以预测剩余的 20%,训练、可视化和评估一个具有连续 5 次分割的分类树。我们在这里采取了一种简化说明的快捷方式,并使用内置的 train_test_split,它不会防止前瞻性偏差,作为我们的自定义迭代器。树的配置意味着最多有 2⁵=32 个叶节点,平均情况下在平衡的情况下,将包含超过 4,300 个训练样本。看一下以下代码:

# randomize train-test split
X_train, X_test, y_train, y_test = train_test_split(X, y_binary, test_size=0.2, random_state=42)

# configure & train tree learner
classifier = DecisionTreeClassifier(criterion='gini',
                                    max_depth=5,
                                    random_state=42)
classifier.fit(X=X_train, y=y_train)

# Output:
DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=5,
            max_features=None, max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, presort=False, random_state=42,
            splitter='best')

在训练模型后,输出显示了我们将在下一节讨论参数调整时更详细讨论的所有 DecisionTreeClassifier 参数。

如何可视化决策树

您可以使用 graphviz 库来可视化树(请参阅 GitHub 获取安装说明),因为 sklearn 可以输出描述树的 .dot 语言,该语言由该库使用。您可以配置输出以包括特征和类别标签,并限制级别数量以使图表可读,如下所示:

dot_data = export_graphviz(classifier,
                           out_file=None, # opt. save to file and convert to png
                           feature_names=X.columns,
                           class_names=['Down', 'Up'],
                           max_depth=3,
                           filled=True,
                           rounded=True,
                           special_characters=True)

graphviz.Source(dot_data)

结果显示,模型使用了各种不同的特征,并指示了连续和分类(虚拟)变量的分割规则。图表显示,在标签值下,每个类别的样本数量,以及在标签类别下,最常见的类别(在样本期间,上升的月份更多):

如何评估决策树预测结果

为了评估我们的第一个分类树的预测准确性,我们将使用测试集生成预测的类别概率,如下所示:

y_score = classifier.predict_proba(X=X_test)[:, 1] # only keep probabilities for pos. class

.predict_proba() 方法为每个类别生成一个概率。在二分类中,这些概率是互补的,并且总和为 1,因此我们只需要正类的值。为了评估泛化误差,我们将使用基于我们在第六章介绍的接收器操作特性的曲线下面积。结果表明,与基线值 0.5(随机预测)相比,存在显著的改进:

roc_auc_score(y_score=y_score, y_true=y_test)
0.5941

特征重要性

决策树不仅可以被可视化以检查给定特征的决策路径,还可以提供每个特征对拟合到训练数据的模型的贡献的摘要度量。

特征重要性捕获了特征产生的分裂有助于优化模型用于评估分裂质量的度量标准,我们的情况下是基尼不纯度指数。特征的重要性计算为该度量的(归一化)总减少量,并考虑到受分裂影响的样本数量。因此,在树的较早节点使用的特征,其中节点倾向于包含更多样本,通常被认为具有更高的重要性。

下图显示了前 15 个特征的重要性:

过拟合和正则化

决策树有过拟合的倾向,特别是当数据集相对于样本数量具有大量特征时。正如前几章所讨论的,过拟合会增加预测误差,因为模型不仅学习了训练数据中包含的信号,还学习了噪声。

有几种方法可以解决过拟合的风险:

  • 降维(第十二章,无监督学习)通过用更少、更具信息性和更少噪声的特征代表现有特征来改善特征与样本的比例。

  • 集成模型,例如随机森林,结合了多个树,同时在树构建过程中进行随机化,这将在本章的第二部分中介绍。

  • 决策树提供了几个正则化超参数来限制树的增长和相关的复杂性。每个分裂增加节点数,但也减少了每个节点可用于支持预测的样本数量。对于每个额外级别,需要两倍的样本来填充具有相同样本密度的新节点。

  • 树修剪是减少树复杂性的附加工具,通过消除添加了很少价值但增加了模型方差的节点或整个树的部分来实现。例如,成本复杂度剪枝从一个大树开始,通过用叶子替换节点来递归地减小其大小,基本上是将树构建过程反向运行。各个步骤产生的树序列然后可以使用交叉验证进行比较,以选择理想大小。

如何正则化决策树

以下表格列出了 sklearn 决策树实现中可用于此目的的关键参数。在介绍了最重要的参数之后,我们将说明如何使用交叉验证来优化超参数设置,以达到偏差-方差的折衷和降低预测误差:

<tdDefault

参数 选项 描述
**max_depth** None int

| **max_features** | None | None:所有特征;int float:分数

auto, sqrt: sqrt(n_features)

log2: log2(n_features) | 用于分割的特征数量。 |

**max_leaf_nodes** None None:叶节点数量无限制 int 分割节点直到创建这么多个叶子。
**min_impurity_decrease** 0 float 如果不纯度减少至少这个值,则分割节点。
**min_samples_leaf** 1 int;float (as a percentage of N) 必须在叶节点处的最小样本数。仅当每个左右分支中至少有 min_samples_leaf 训练样本时才考虑分割。对于回归问题,可能会平滑模型。
**min_samples_split** 2 int; float (percent of N) 分割内部节点所需的最小样本数:
**min_weight_fraction_leaf** 0 叶节点所需的所有样本权重总和的最小加权分数。除非在 fit 方法中提供了 sample_weight,否则样本权重相等。

max_depth 参数对连续分割数量施加硬性限制,并且代表了限制树生长的最直接方法。

min_samples_splitmin_samples_leaf 参数是限制树生长的替代、数据驱动的方法。与对连续分割数量施加硬性限制不同,这些参数控制进一步分割数据所需的最小样本数。后者保证每个叶子节点有一定数量的样本,而前者在分割导致分布非常不均匀时可能会创建非常小的叶子。小的参数值有助于过度拟合,而高的值可能会阻止树学习数据中的信号。默认值通常相当低,您应该使用交叉验证来探索一系列潜在值。您还可以使用浮点数表示百分比,而不是绝对数值。

sklearn 文档包含有关如何在不同情况下使用各种参数的额外细节;请参阅 GitHub 引用。

决策树剪枝

递归二分法在训练集上可能会产生良好的预测,但往往会过度拟合数据并产生较差的泛化性能,因为它导致了过于复杂的树,表现为大量的叶节点或特征空间的分割。较少的分割和叶节点意味着总体上更小的树,通常也会导致更好的预测性能以及可解释性。

限制叶节点数量的一种方法是避免进一步分割,除非它们对目标度量的改进显著。然而,这种策略的缺点是,有时产生微小改进的分割会在样本的组成不断变化时为后续更有价值的分割创造条件。

相比之下,树剪枝首先通过生长一个非常大的树,然后移除或修剪节点以将大树减少到一个不太复杂且过度拟合的子树。成本复杂度剪枝通过对添加叶节点到树模型的惩罚和调节影响惩罚的正则化参数(类似于套索和岭线性回归模型)生成一系列子树。应用于大树,增加的惩罚将自动产生一系列子树。通过正则化参数的交叉验证可以用来识别最优的修剪子树。

这种方法在 sklearn 中尚不可用;有关进一步详细信息和手动实现剪枝的方法,请参见 GitHub 上的参考资料。

如何调整超参数

决策树提供了一系列超参数来控制和调整训练结果。交叉验证是获取泛化误差无偏估计的最重要工具,这反过来又允许在各种配置选项之间做出知情选择。sklearn 提供了几个工具来简化交叉验证众多参数设置的过程,即我们将在下一节中介绍的GridSearchCV便利类。学习曲线还允许进行诊断,评估收集额外数据以减少泛化误差的潜在益处。

决策树的网格搜索 CV

sklearn提供了一种方法来定义多个超参数的值范围。它自动化了交叉验证这些参数值的各种组合以确定最佳配置的过程。让我们走一遍自动调整模型的过程。

第一步是实例化一个模型对象,并定义一个字典,其中关键词命名超参数,值列出要测试的参数设置:

clf = DecisionTreeClassifier(random_state=42)
param_grid = {'max_depth': range(10, 20),
              'min_samples_leaf': [250, 500, 750],
              'max_features': ['sqrt', 'auto']
              }

然后,实例化GridSearchCV对象,提供评估器对象和参数网格,以及评分方法和交叉验证选择给初始化方法。我们将使用我们自定义的OneStepTimeSeriesSplit类的对象,初始化为使用十个折叠的cv参数,并将评分设置为roc_auc度量。我们可以使用n_jobs参数并行搜索,并通过设置refit=True自动获得使用最佳超参数的训练模型。

所有设置就绪后,我们可以像任何其他模型一样拟合GridSearchCV

gridsearch_clf = GridSearchCV(estimator=clf,
                          param_grid=param_grid,
                          scoring='roc_auc',
                          n_jobs=-1,
                          cv=cv,  # custom OneStepTimeSeriesSplit
                          refit=True,
                          return_train_score=True)

gridsearch_clf.fit(X=X, y=y_binary)

训练过程为我们的GridSearchCV对象生成一些新属性,最重要的是关于最佳设置和最佳交叉验证分数的信息(现在使用正确的设置以避免前瞻性偏差)。

max_depth设置为13min_samples_leaf设置为500,并且在决定分裂时仅随机选择与总特征数的平方根相对应的数量,可以产生最佳结果,AUC 为0.5855

gridsearch_clf.best_params_
{'max_depth': 13, 'max_features': 'sqrt', 'min_samples_leaf': 500}

gridsearch_clf.best_score_
0.5855

自动化非常方便,但我们也想检查性能如何随不同参数值的变化而变化。完成此过程后,GridSearchCV对象会提供详细的交叉验证结果,以获得更多见解。

如何检查树结构

笔记本还说明了如何手动运行交叉验证以获得自定义树属性,例如与某些超参数设置相关联的总节点数或叶节点数。以下函数访问内部的.tree_属性,以检索有关总节点数以及其中多少节点是叶节点的信息:

def get_leaves_count(tree):
    t = tree.tree_
    n = t.node_count
    leaves = len([i for i in range(t.node_count) if t.children_left[i]== -1])
    return leaves

我们可以将这些信息与训练和测试分数结合起来,以获得有关模型在整个交叉验证过程中行为的详细知识,如下所示:

train_scores, val_scores, leaves = {}, {}, {}
for max_depth in range(1, 26):
    print(max_depth, end=' ', flush=True)
    clf = DecisionTreeClassifier(criterion='gini', 
                                 max_depth=max_depth,
                                 min_samples_leaf=500,
                                 max_features='auto',
                                 random_state=42)
    train_scores[max_depth], val_scores[max_depth], leaves[max_depth] = [], [], []
    for train_idx, test_idx in cv.split(X):
        X_train, y_train,  = X.iloc[train_idx], y_binary.iloc[train_idx]
        X_test, y_test = X.iloc[test_idx], y_binary.iloc[test_idx]
        clf.fit(X=X_train, y=y_train)

        train_pred = clf.predict_proba(X=X_train)[:, 1]
        train_score = roc_auc_score(y_score=train_pred, y_true=y_train)
        train_scores[max_depth].append(train_score)

        test_pred = clf.predict_proba(X=X_test)[:, 1]
        val_score = roc_auc_score(y_score=test_pred, y_true=y_test)
        val_scores[max_depth].append(val_score)    
        leaves[max_depth].append(get_leaves_count(clf))

结果显示在以下图表的左侧面板上。它突出显示了在max_depth设置范围内的样本内外性能,以及围绕误差指标的置信区间。它还显示了在 13 次连续分割中最佳表现的设置,如垂直黑线所示。

学习曲线

学习曲线是一种有用的工具,显示验证和训练分数随训练样本数量的变化而演变。

学习曲线的目的是找出模型是否以及在多大程度上会因为在训练过程中使用更多数据而受益。它还有助于诊断模型的泛化误差更可能是由偏差还是方差驱动。

例如,如果验证分数和训练分数都趋于类似低的值,尽管训练集大小增加,但错误更可能是由于偏差,而额外的训练数据不太可能有所帮助。

请看以下可视化结果:

决策树的优势和劣势

当与迄今为止我们所探索的线性模型相比,回归和分类树在预测时采用非常不同的方法。您如何确定哪个模型更适合手头的问题?请考虑以下内容:

  • 如果结果和特征之间的关系大致是线性的(或者可以相应地进行转换),那么线性回归可能会优于更复杂的方法,例如不利用这种线性结构的决策树。

  • 如果关系呈现高度非线性和更复杂,决策树可能会优于经典模型。

决策树具有几个优点,使其非常受欢迎:

  • 它们相当容易理解和解释,部分因为它们可以很容易地可视化,因此更容易让非技术人员理解。决策树也被称为白盒模型,因为它们在如何得出预测方面具有很高的透明度。黑盒模型,如集成和神经网络,可能会提供更好的预测精度,但是决策逻辑往往更难理解和解释。

  • 与对数据做出更强假设或对数据更敏感的模型(如正则化回归)相比,决策树需要更少的数据准备。

  • 一些决策树实现处理分类输入,不需要创建虚拟变量(提高内存效率),并且可以处理缺失值,就像我们将在第十一章中看到的梯度提升机,但这并不适用于 sklearn。

  • 预测速度快,因为它与叶子节点的数量呈对数关系(除非树变得极不平衡)。

  • 可以使用统计测试验证模型并考虑其可靠性(请参阅 GitHub 参考资料)。

决策树也有一些关键的缺点:

  • 决策树内置了对训练集的过度拟合倾向,并产生高泛化误差。解决这个弱点的关键步骤包括修剪(尚未由 sklearn 支持),以及使用前一节中概述的各种早停准则进行正则化。

  • 与决策树相关的是其高方差,这是由于它们能够紧密地适应训练集,因此数据的微小变化可能会导致决策树结构和因此模型生成的预测产生很大波动。解决决策树高方差的关键机制是使用具有低偏差且产生不相关预测误差的随机决策树集成。

  • 决策树学习的贪婪方法基于局部标准进行优化,即减少当前节点的预测误差,并且不能保证全局最优结果。再次,由随机树组成的集成有助于减轻这个问题。

  • 决策树对不平衡的类权重也很敏感,并可能产生偏倚的树。一种选择是对不平衡的类进行过采样或对更频繁的类进行欠采样。通常最好使用类权重并直接调整目标函数。

随机森林

决策树不仅因其透明度和可解释性而有用,而且是更强大的集成模型的基本构建模块,它将许多个体树与策略结合起来,以随机变化其设计,以解决前一节讨论的过拟合和高方差问题。

集成模型

集成学习涉及将多个机器学习模型组合成一个新模型,旨在比任何单个模型做出更好的预测。更具体地说,一个集成将使用一个或多个给定的学习算法训练的多个基础估计器的预测整合起来,以减少这些模型可能单独产生的泛化错误。

要使集成学习实现这个目标,个体模型必须是:

  • 准确性:它们胜过一个天真的基准(例如样本均值或类比例)

  • 独立:它们的预测是通过不同方式生成的,以产生不同的错误

集成方法是最成功的机器学习算法之一,特别适用于标准的数值数据。大型集成在机器学习竞赛中非常成功,可能由许多不同的个体模型组成,这些模型通过手工或使用另一个机器学习算法组合在一起。

将不同模型的预测结合起来有几个缺点。这些包括降低的可解释性,以及训练、预测和模型维护的更高复杂性和成本。因此,在实践中(除了竞赛之外),从大规模集成中获得的小幅准确性增益可能不值得增加的成本。

根据它们如何优化组成模型并将结果整合为单个集成预测,通常可以区分两组集成方法:

  • 平均方法独立训练几个基础估计器,然后对它们的预测进行平均。如果基础模型没有偏差并且产生的不高度相关的不同预测错误,那么组合预测可能具有更低的方差,并且可能更可靠。这类似于从具有不相关回报的资产构建投资组合,以减少波动性而不牺牲回报。

  • 提升方法相反,是按顺序训练基础估计器,其特定目标是减少组合估计器的偏差。其动机是将几个弱模型组合成一个强大的集合。

在本章剩余部分,我们将专注于自动平均方法,并在第十一章中讨论梯度提升机的提升方法。

如何降低模型方差

我们发现决策树可能会由于方差较高而做出不良预测,这意味着树结构对训练样本的组成非常敏感。我们还看到低方差的模型,例如线性回归,尽管给定特征数量足够的样本不同,但产生的估计相似。

对于给定的一组独立观察结果,每个都具有σ²的方差,样本均值的标准误差由σ/n给出。 换句话说,对更多的观察结果进行平均会减少方差。 因此,减少模型方差和其泛化误差的自然方法将是从总体中收集许多训练集,对每个数据集训练不同的模型,并对产生的预测进行平均。

在实践中,我们通常没有许多不同的训练集的奢侈条件。 这就是装袋,即自助聚合的缩写,发挥作用的地方。 装袋是减少机器学习模型方差的通用方法,特别是在应用于决策树时,特别有用和流行。

袋装是指对自助采样进行聚合,自助采样是带替换的随机样本。 这样的随机样本与原始数据集具有相同数量的观察结果,但可能由于替换而包含重复项。

装袋提高了预测准确性,但降低了模型的可解释性,因为不再可能可视化树来理解每个特征的重要性。 作为一种集成算法,装袋方法对这些自助采样训练给定数量的基本估计器,然后将它们的预测聚合成最终的集合预测。

装袋通过随机化基本估计器的方法,例如,每棵树的生长方式,然后对预测进行平均,从而减少它们的泛化误差,从而降低了基本估计器的方差。 它通常是改进给定模型的直接方法,而无需更改底层算法。 它在具有低偏差和高方差的复杂模型中效果最好,例如深度决策树,因为它的目标是限制过拟合。 相比之下,提升方法最适合弱模型,例如浅决策树。

有几种装袋方法,它们的不同之处在于它们对训练集应用的随机抽样过程:

  • 粘贴从训练数据中抽取随机样本而不进行替换,而装袋则进行替换

  • 随机子空间无需替换地从特征(即列)中随机抽样

  • 随机补丁通过随机抽样观察和特征来训练基本估计器

袋装决策树

要将装袋应用于决策树,我们从训练数据中创建自助样本,通过反复采样来训练一个决策树,然后在这些样本中的每一个上创建一个决策树,通过对不同树的预测进行平均来创建一个集合预测。

袋装决策树通常生长较大,即具有许多层和叶子节点,并且不进行修剪,以使每棵树具有低偏差但高方差。 然后,平均它们的预测的效果旨在减少它们的方差。 研究表明,通过构建组合了数百甚至数千棵树的集合的装袋,可以显着提高预测性能。

为了说明装袋对回归树方差的影响,我们可以使用sklearn提供的BaggingRegressor元估计器。它基于指定抽样策略的参数来训练用户定义的基估计器:

  • max_samplesmax_features控制从行和列中抽取的子集的大小,分别

  • bootstrapbootstrap_features确定每个样本是有放回还是无放回抽样

以下示例使用指数函数生成单个DecisionTreeRegressor和包含十棵树的BaggingRegressor集成的训练样本,每棵树都生长十层深。这两个模型都是在随机样本上训练的,并为添加了噪声的实际函数预测结果。

由于我们知道真实函数,我们可以将均方误差分解为偏差、方差和噪声,并根据以下分解比较两个模型的这些分量的相对大小:

对于分别包含 250 和 500 个观测值的 100 个重复随机训练和测试样本,我们发现单个决策树的预测方差几乎是基于自举样本的10个装袋树的预测方差的两倍:

noise = .5  # noise relative to std(y)
noise = y.std() * noise_to_signal

X_test = choice(x, size=test_size, replace=False)

max_depth = 10
n_estimators=10

tree = DecisionTreeRegressor(max_depth=max_depth)
bagged_tree = BaggingRegressor(base_estimator=tree, n_estimators=n_estimators)
learners = {'Decision Tree': tree, 'Bagging Regressor': bagged_tree}

predictions = {k: pd.DataFrame() for k, v in learners.items()}
for i in range(reps):
    X_train = choice(x, train_size)
    y_train = f(X_train) + normal(scale=noise, size=train_size)
    for label, learner in learners.items():
        learner.fit(X=X_train.reshape(-1, 1), y=y_train)
        preds = pd.DataFrame({i: learner.predict(X_test.reshape(-1, 1))}, index=X_test)
        predictions[label] = pd.concat([predictions[label], preds], axis=1)

对于每个模型,以下图表显示了上半部分的平均预测值和平均值周围两个标准差的带状区域,以及下半部分基于真实函数值的偏差-方差-噪声分解:

查看笔记本random_forest以获取实现细节。

如何构建随机森林

随机森林算法通过对由装袋生成的自举样本引入的随机化进行扩展,进一步减少方差并提高预测性能。

除了对每个集成成员使用自举训练数据外,随机森林还会对模型中使用的特征进行随机抽样(不重复)。根据实现方式,随机样本可以针对每棵树或每次分裂进行抽取。因此,算法在学习新规则时面临不同选择,无论是在树的级别上还是每次分裂时。

特征样本的大小对回归和分类树有所不同:

  • 对于分类,样本量通常是特征数量的平方根。

  • 对于回归,可以选择从三分之一到所有特征,并应基于交叉验证进行选择。

以下图示说明了随机森林如何随机化单个树的训练,然后将它们的预测聚合成一个集成预测:

除了训练观察值之外,随机化特征的目标是进一步去相关化个体树的预测误差。并非所有特征都是平等的,少量高度相关的特征将在树构建过程中更频繁和更早地被选择,使得决策树在整个集合中更相似。然而,个体树的泛化误差越不相关,整体方差就会减少。

如何训练和调整随机森林

关键的配置参数包括在如何调整超参数部分介绍的各个决策树的各种超参数。以下表格列出了两个RandomForest类的附加选项:

关键字 默认 描述
bootstrap True 训练期间使用自举样本。
n_estimators 10 森林中的树的数量。
oob_score False 使用包外样本来估计在未见数据上的 R²。

bootstrap参数激活了前面提到的装袋算法大纲,这反过来又启用了包外分数(oob_score)的计算,该分数使用未包含在用于训练给定树的自举样本中的样本来估计泛化准确度(有关详细信息,请参见下一节)。

n_estimators参数定义了要作为森林一部分生长的树的数量。更大的森林表现更好,但构建时间也更长。重要的是监视交叉验证错误作为基本学习者数量的函数,以确定预测误差的边际减少何时下降,以及额外训练的成本开始超过收益。

max_features参数控制在学习新的决策规则并分裂节点时可用的随机选择特征子集的大小。较低的值会降低树的相关性,从而降低集成的方差,但可能会增加偏差。对于回归问题,良好的起始值是n_features(训练特征的数量),对于分类问题是sqrt(n_features),但取决于特征之间的关系,并且应该使用交叉验证进行优化。

随机森林设计为包含深度完全生长的树,可以使用max_depth=Nonemin_samples_split=2来创建。然而,这些值未必是最优的,特别是对于具有许多样本和因此可能非常深的树的高维数据,这可能会导致非常计算密集和内存密集的情况。

sklearn 提供的 RandomForest 类支持并行训练和预测,通过将 n_jobs 参数设置为要在不同核心上运行的 k 个作业数来实现。值 -1 使用所有可用核心。进程间通信的开销可能限制速度提升的线性,因此 k 个作业可能需要超过单个作业的 1/k 时间。尽管如此,在数据庞大且分割评估变得昂贵时,对于大型森林或深度个体树,速度提升通常相当显著,并且可能需要训练相当长时间。

如常,应使用交叉验证来确定最佳参数配置。以下步骤说明了该过程:

  1. 我们将使用 GridSearchCV 来识别一组最佳参数,以用于分类树的集成:
rf_clf = RandomForestClassifier(n_estimators=10,
                                criterion='gini',
                                max_depth=None,
                                min_samples_split=2,
                                min_samples_leaf=1,
                                min_weight_fraction_leaf=0.0,
                                max_features='auto',
                                max_leaf_nodes=None,
                                min_impurity_decrease=0.0,
                                min_impurity_split=None,
                                bootstrap=True, oob_score=False,
                                n_jobs=-1, random_state=42)
  1. 我们将使用 10 倍自定义交叉验证,并为关键配置设置的值填充参数网格:
cv = OneStepTimeSeriesSplit(n_splits=10)
clf = RandomForestClassifier(random_state=42, n_jobs=-1)
param_grid = {'n_estimators': [200, 400],
              'max_depth': [10, 15, 20],
              'min_samples_leaf': [50, 100]}
  1. 使用前述输入配置 GridSearchCV
gridsearch_clf = GridSearchCV(estimator=clf,
                          param_grid=param_grid,
                          scoring='roc_auc',
                          n_jobs=-1,
                          cv=cv,
                          refit=True,
                          return_train_score=True,
                          verbose=1)
  1. 训练由参数网格定义的多个集成模型:
gridsearch_clf.fit(X=X, y=y_binary)
  1. 获得最佳参数如下:
gridsearch_clf.bestparams{'max_depth': 15,
 'min_samples_leaf': 100,
 'n_estimators': 400}
  1. 最佳分数比单棵树基线略有提高,但具有显著改进:
gridsearch_clf.bestscore_0.6013

随机森林的特征重要性

随机森林集成可能包含数百棵独立的树,但仍然可以从装袋模型中获得关于特征重要性的整体摘要度量。

对于给定的特征,重要性得分是基于该特征进行分割导致的目标函数值的总减少量,平均分布在所有树上。由于目标函数考虑了分割影响的特征数量,所以这个度量隐含地是加权平均的,因此在树的顶部附近使用的特征会由于较少的可用节点中包含的观察次数较多而获得更高的得分。通过对以随机方式生长的许多树进行平均,特征重要性估计失去了一些变化,并且变得更加准确。

基于用于学习决策规则的不同目标,分类和回归树的计算有所不同,分别以回归树的均方误差和分类树的基尼指数或熵来衡量。

sklearn 进一步规范化特征重要性度量,使其总和为 1。因此,计算得出的特征重要性也用作特征选择的一种替代方法,而不是我们在 第六章 中看到的互信息度量(见 sklearn.feature_selection 模块中的 SelectFromModel)。

在我们的示例中,前 20 个特征的重要性值如下所示:

特征重要性值

包外测试

随机森林提供了内置的交叉验证优势,因为每个树都是在训练数据的自助版本上训练的。因此,每棵树平均使用了仅有的两三分之一可用的观测结果。要了解原因,请考虑自助样本的大小与原始样本的大小相同,每个观测结果被抽取的概率为 1/n。因此,不进入自助样本的概率是(1-1/n)^n,它收敛(迅速)到 1/e,或者大约三分之一。

在用于生长袋装树的训练集中未包括的剩余三分之一的观察结果被称为袋外袋OOB)观察结果,并且可以作为验证集。正如交叉验证一样,我们对于每棵树都预测未使用此观察结果构建的 OOB 样本的响应,然后对每个 OOB 样本进行单个集合预测的平均预测响应(如果目标是回归)或采取多数投票或预测概率(如果目标是分类)。这些预测产生了关于泛化误差的无偏估计,方便在训练期间计算。

由于预测是在不考虑此观察结果的情况下学习的决策规则产生的,因此结果的 OOB 误差是该观察结果的泛化误差的有效估计。一旦随机森林足够大,OOB 误差就会接近留一交叉验证误差。对于大型数据集,OOB 方法估计测试误差非常高效,而交叉验证可能计算成本高昂。

随机森林的优缺点

袋装集成模型既有优势又有劣势。随机森林的优势包括:

  • 预测性能可以与最佳监督学习算法竞争

  • 它们提供可靠的特征重要性估计

  • 它们提供了测试错误的有效估计,而不需要承担与交叉验证相关的重复模型训练的成本

另一方面,随机森林也有一些缺点:

  • 集合模型本质上比单个决策树不太可解释

  • 训练大量深树可能具有高计算成本(但可以并行化)并且使用大量内存

  • 预测速度较慢,这可能对需要低延迟的应用程序造成挑战

摘要

在本章中,我们学习了一种能够捕捉非线性关系的新模型类,与我们迄今探索过的经典线性模型形成对比。我们看到决策树是如何学习规则来将特征空间划分为产生预测的区域,从而将输入数据分割成特定区域的。

决策树非常有用,因为它们提供了有关特征和目标变量之间关系的独特见解,我们看到了如何可视化树结构中编码的一系列决策规则。

不幸的是,决策树容易过拟合。我们了解到集成模型和自举聚合方法设法克服了决策树的一些缺点,并使它们成为更强大的复合模型的组成部分。

在下一章中,我们将探讨另一个集成模型,它已经被认为是最重要的机器学习算法之一。

第十一章:梯度提升机

在上一章中,我们了解到随机森林通过将它们组合成一个减少个体树高方差的集合来提高个别决策树的预测。随机森林使用装袋(bootstrap aggregation)来将随机元素引入到生长个别树的过程中。

更具体地说,装袋(bagging)从数据中有替换地抽取样本,以便每棵树都在不同但大小相等的数据随机子集上进行训练(其中一些观测重复)。随机森林还随机选择一些特征子集,以便用于训练每棵树的数据的行和列都是原始数据的随机版本。然后,集成通过对个别树的输出进行平均来生成预测。

个别树通常生长较深,以确保低偏差,同时依赖随机化训练过程来产生不同的、不相关的预测错误,这些错误在聚合时具有较低的方差,比个体树的预测更可靠。换句话说,随机化训练旨在使个别树产生的错误彼此不相关或多样化,使集合对过拟合的敏感性大大降低,具有较低的方差,并且对新数据的泛化能力更好。

在本章中,我们将探讨提升(boosting)这种替代机器学习ML)算法,用于决策树集成,通常能够产生更好的结果。其关键区别在于,提升根据模型在添加新树之前累积的错误来修改用于训练每棵树的数据。与随机森林不同,随机森林独立地使用训练集的不同版本训练许多树,而提升则使用重加权版本的数据进行顺序处理。最先进的提升实现也采用了随机森林的随机化策略。

在本章中,我们将看到提升是如何在过去三十年中演变成最成功的 ML 算法之一的。在撰写本文时,它已经成为结构化数据的机器学习竞赛中的主导者(例如,与高维图像或语音不同,在这些领域中输入和输出之间的关系更加复杂,深度学习表现出色)。具体来说,本章将涵盖以下主题:

  • 提升的工作原理以及与装袋的比较

  • 如何从自适应提升到梯度提升的演变

  • 如何使用 sklearn 使用和调整 AdaBoost 和梯度提升模型

  • 最先进的 GBM 实现如何大幅加速计算

  • 如何防止梯度提升模型过拟合

  • 如何使用xgboostlightgbmcatboost构建、调整和评估大型数据集上的梯度提升模型

  • 如何解释并从梯度提升模型中获得洞见

自适应提升

像装袋一样,增强是一种集成学习算法,它将基本学习器(通常是决策树)组合成一个集成。增强最初是为分类问题开发的,但也可用于回归,并且已被称为过去 20 年中引入的最有效的学习思想之一(如 Trevor Hastie 等人所述的《统计学习要素》;请查看 GitHub 获取参考链接)。与装袋类似,它是一种通用方法或元方法,可应用于许多统计学习模型。

Boosting 的发展动机是寻找一种方法来将许多 模型的输出(当一个预测器仅比随机猜测稍好时,称为弱预测器)结合成更强大的,即增强的联合预测。一般来说,Boosting 学习一种形式类似于线性回归的可加假设,H[M]。然而,现在求和的每个元素 m= 1,..., M 都是一个称为 h[t] 的弱基本学习器,它本身需要训练。下面的公式总结了这种方法:

正如上一章所讨论的那样,装袋在不同的训练数据随机样本上训练基本学习器。相比之下,增强是通过在数据上顺序训练基本学习器,该数据反复修改以反映累积学习结果。目标是确保下一个基本学习器补偿当前集合的缺陷。我们将在本章中看到,增强算法在定义缺陷的方式上有所不同。集合使用弱模型的预测的加权平均值进行预测。

第一个带有数学证明,证明它增强了弱学习器性能的提升算法是由罗伯特·舍皮尔(Robert Schapire)和约阿夫·弗洛伊德(Yoav Freund)在 1990 年左右开发的。1997 年,作为分类问题的一个实际解决方案出现了自适应增强AdaBoost)算法,该算法于 2003 年获得了哥德尔奖。大约又过了五年,该算法在连续梯度下降的方法上得到了推广,利奥·布雷曼(发明了随机森林)将该方法与梯度下降相连接,杰罗姆·弗里德曼在 1999 年提出了梯度增强算法。近年来,出现了许多优化实现,如 XGBoost、LightGBM 和 CatBoost,这些实现牢固地确立了梯度增强作为结构化数据的首选解决方案。

在接下来的几节中,我们将简要介绍 AdaBoost,然后专注于梯度增强模型,以及这个非常强大和灵活的算法的几种最新实现。

AdaBoost 算法

AdaBoost 是第一个在拟合额外集成成员时迭代地适应累积学习进展的增强算法。特别地,AdaBoost 在拟合新的弱学习器之前,会根据当前集成在训练集上的累积错误改变训练数据的权重。AdaBoost 当时是最准确的分类算法,Leo Breiman 在 1996 年的 NIPS 会议上称其为世界上最好的现成分类器。

该算法对 ML 产生了非常重要的影响,因为它提供了理论上的性能保证。这些保证只需要足够的数据和一个可靠地预测略好于随机猜测的弱学习器。由于这种分阶段学习的自适应方法,开发准确的 ML 模型不再需要在整个特征空间上准确地表现。相反,模型的设计可以集中于找到仅仅优于抛硬币的弱学习器。

AdaBoost 与 bagging 有很大的不同,后者构建了在非常深的树上的集成以减少偏差。相比之下,AdaBoost 使用浅树作为弱学习器,通常通过使用树桩来获得更高的准确性,即由单一分裂形成的树。该算法从一个等权重的训练集开始,然后逐步改变样本分布。每次迭代后,AdaBoost 增加被错误分类的观测值的权重,并减少正确预测样本的权重,以便随后的弱学习器更多地关注特别困难的案例。一旦训练完成,新的决策树就会以反映其减少训练错误的贡献的权重被纳入集成中。

对于预测离散类别的一组基本学习器的 AdaBoost 算法,hmm=1, ..., M,以及N个训练观测值,可以总结如下:

  1. 对于观测值 i=1, ..., N,初始化样本权重 w[i]=1/N

  2. 对于每个基本分类器 h[m]m=1, ..., M,执行以下操作:

    1. w[i] 加权训练数据来拟合 hm

    2. 计算训练集上基本学习器的加权错误率 ε[m ]。

    3. 计算基本学习器的集成权重 α[m],作为其错误率的函数,如下式所示:

    1. 根据 w[i ] exp(α[m]**)* 更新错误分类样本的权重。
  1. 当集成成员的加权和为正时,预测正类;否则,预测负类,如下式所示:

AdaBoost 具有许多实际优势,包括易于实现和快速计算,它可以与任何弱学习器识别方法结合使用。除了集成的大小之外,没有需要调整的超参数。AdaBoost 还对识别异常值很有用,因为接收最高权重的样本是那些一直被错误分类且固有模糊的样本,这也是异常值的典型特征。

另一方面,AdaBoost 在给定数据集上的性能取决于弱学习器充分捕捉特征与结果之间关系的能力。正如理论所示,当数据不足或集成成员的复杂性与数据的复杂性不匹配时,提升将无法表现良好。它也容易受到数据中的噪声影响。

使用 sklearn 的 AdaBoost

作为其集成模块的一部分,sklearn 提供了一个支持两个或多个类的AdaBoostClassifier实现。本节的代码示例位于笔记本gbm_baseline中,该笔记本将各种算法的性能与始终预测最频繁类别的虚拟分类器进行比较。

我们首先需要将base_estimator定义为所有集成成员的模板,然后配置集成本身。我们将使用默认的DecisionTreeClassifier,其中max_depth=1——即一个只有一个分割的树桩。基本估算器的复杂性是关键调参参数,因为它取决于数据的性质。正如前一章所示,对于max_depth的更改应与适当的正则化约束结合使用,例如通过调整min_samples_split来实现,如下代码所示:

base_estimator = DecisionTreeClassifier(criterion='gini', 
                                        splitter='best',
                                        max_depth=1, 
                                        min_samples_split=2, 
                                        min_samples_leaf=20, 
                                        min_weight_fraction_leaf=0.0,
                                        max_features=None, 
                                        random_state=None, 
                                        max_leaf_nodes=None, 
                                        min_impurity_decrease=0.0, 
                                        min_impurity_split=None)

在第二步中,我们将设计集成。n_estimators参数控制弱学习器的数量,learning_rate确定每个弱学习器的贡献,如下代码所示。默认情况下,弱学习器是决策树树桩:

ada_clf = AdaBoostClassifier(base_estimator=base_estimator,
                             n_estimators=200,
                             learning_rate=1.0,
                             algorithm='SAMME.R',
                             random_state=42)

负责良好结果的主要调参参数是n_estimators和基本估算器的复杂性,因为树的深度控制了特征之间的相互作用程度。

我们将使用自定义的 12 折滚动时间序列拆分来交叉验证 AdaBoost 集成,以预测样本中最后 12 个月的 1 个月预测,使用所有可用的先前数据进行训练,如下代码所示:

cv = OneStepTimeSeriesSplit(n_splits=12, test_period_length=1, shuffle=True)
def run_cv(clf, X=X_dummies, y=y, metrics=metrics, cv=cv, fit_params=None):
    return cross_validate(estimator=clf,
                          X=X,
                          y=y,
                          scoring=list(metrics.keys()),
                          cv=cv,
                          return_train_score=True,
                          n_jobs=-1,                    # use all cores
                          verbose=1,
                          fit_params=fit_params)

结果显示加权测试准确度为 0.62,测试 AUC 为 0.6665,负对数损失为-0.6923,以及测试 F1 得分为 0.5876,如下截图所示:

有关交叉验证代码和处理结果的其他详细信息,请参阅配套笔记本。

梯度提升机

AdaBoost 也可以解释为在每次迭代m时通过逐步前向方法最小化指数损失函数来识别与对应权重α[m]的新基学习器h[m]添加到集成中,如下式所示:

对 AdaBoost 算法的这种解释是在其发表几年后才发现的。它将 AdaBoost 视为一种基于坐标的梯度下降算法,该算法最小化了特定的损失函数,即指数损失。

梯度提升(Gradient boosting)利用这一洞见,并将提升方法应用于更广泛的损失函数。该方法使得设计机器学习算法以解决任何回归、分类或排名问题成为可能,只要它可以使用可微分的损失函数来表述,并因此具有梯度。定制这一通用方法以适应许多特定预测任务的灵活性对于提升方法的普及至关重要。

梯度提升机GBM)算法的主要思想是训练基学习器来学习集成当前损失函数的负梯度。因此,每次添加到集成中都直接有助于减少由先前集成成员产生的总体训练误差。由于每个新成员代表数据的新函数,因此也可以说梯度提升是以加法方式优化h[m]函数。

简而言之,该算法逐步将弱学习器h[m](如决策树)拟合到当前集成的损失函数的负梯度上,如下式所示:

换句话说,在给定迭代m时,该算法计算每个观察值的当前损失的梯度,然后将回归树拟合到这些伪残差上。在第二步中,它确定每个终端节点的最佳常数预测,该预测最小化了将该新学习器添加到集成中产生的增量损失。

这与独立决策树和随机森林不同,其中预测取决于相关终端或叶节点中的训练样本的结果值:在回归的情况下是它们的平均值,或者在二元分类的情况下是正类的频率。对损失函数梯度的关注也意味着梯度提升使用回归树来学习回归和分类规则,因为梯度始终是连续函数。

最终集成模型根据个别决策树预测的加权和进行预测,每个决策树都已经训练以在给定一组特征值的情况下最小化集成损失,如下图所示:

梯度提升树在许多分类、回归和排名基准上表现出最先进的性能。它们可能是最受欢迎的集成学习算法,既作为多样化的机器学习竞赛中的独立预测器,也作为实际生产流水线中的一部分,例如,用于预测在线广告的点击率。

梯度提升成功的基础是其以增量方式学习复杂的函数关系的能力。该算法的灵活性需要通过调整约束模型固有倾向于学习训练数据中的噪声而不是信号的超参数来仔细管理过拟合的风险。

我们将介绍控制梯度提升树模型复杂性的关键机制,然后使用 sklearn 实现来说明模型调优。

如何训练和调优 GBM 模型

梯度提升性能的两个关键驱动因素是集成大小和其组成决策树的复杂性。

决策树的复杂性控制旨在避免学习高度具体的规则,这些规则通常意味着叶节点中的样本数量非常少。我们在前一章中介绍了用于限制决策树过拟合到训练数据的最有效约束条件。它们包括要求:

  • 要么分割节点或接受它作为终端节点的最小样本数,或

  • 最小改进节点质量,由纯度或熵或均方误差衡量,对于回归情况而言。

除了直接控制集成大小外,还有各种正则化技术,例如收缩,在第七章 线性模型 中我们遇到的 Ridge 和 Lasso 线性回归模型的上下文中。此外,在随机森林上下文中使用的随机化技术也常用于梯度提升机。

集成大小和提前停止

每次提升迭代旨在减少训练损失,使得对于一个大集成,训练误差可能变得非常小,增加过拟合的风险并在未见数据上表现不佳。交叉验证是找到最小化泛化误差的最佳集成大小的最佳方法,因为它取决于应用程序和可用数据。

由于集成大小需要在训练之前指定,因此监控验证集上的性能并在给定迭代次数时中止训练过程是有用的,当验证误差不再下降时。这种技术称为提前停止,经常用于需要大量迭代并且容易过拟合的模型,包括深度神经网络。

请记住,对于大量试验使用相同的验证集进行早停会导致过拟合,只是过拟合于特定的验证集而不是训练集。最好避免在开发交易策略时运行大量实验,因为误发现的风险显著增加。无论如何,保留一个留置集以获取对泛化误差的无偏估计。

收缩和学习率

收缩技术通过对模型损失函数增加惩罚来应用于增加的模型复杂性。对于提升集合,收缩可以通过将每个新集合成员的贡献缩小一个介于 0 和 1 之间的因子来应用。这个因子被称为提升集合的学习率。降低学习率会增加收缩,因为它降低了每个新决策树对集合的贡献。

学习率与集合大小具有相反的效果,后者往往对较低的学习率增加。较低的学习率与较大的集合结合在一起,已被发现可以降低测试误差,特别是对于回归和概率估计。大量迭代在计算上更昂贵,但通常对于快速的最新实现是可行的,只要个别树保持浅层。根据实现方式,您还可以使用自适应学习率,它会根据迭代次数调整,通常降低后期添加的树的影响。我们将在本章后面看到一些示例。

子抽样和随机梯度提升

如前一章节详细讨论的,自助平均法(bagging)提高了原本噪声分类器的性能。

随机梯度提升在每次迭代中使用无替换抽样,以在训练样本的子集上生成下一棵树。好处是降低计算量,通常可以获得更好的准确性,但子抽样应与收缩结合使用。

正如您所见,超参数的数量不断增加,从而增加了在有限的训练数据上从大量参数试验中选择最佳模型时出现假阳性的风险。最好的方法是依次进行并逐个选择参数值,或者使用低基数子集的组合。

如何在 sklearn 中使用梯度提升

sklearn 的集合模块包含了梯度提升树的实现,用于回归和分类,二元和多类。以下 GradientBoostingClassifier 初始化代码展示了我们之前介绍的关键调整参数,除了我们从独立决策树模型中了解的那些外。笔记本 gbm_tuning_with_sklearn 包含了本节的代码示例。

可用的损失函数包括导致 AdaBoost 算法的指数损失和对应于概率输出的 logistic 回归的 deviance。friedman_mse 节点质量度量是平均平方误差的变化,其中包括一个改进分数(请参阅 GitHub 引用以获取原始论文链接),如以下代码所示:

gb_clf = GradientBoostingClassifier(loss='deviance',                # deviance = logistic reg; exponential: AdaBoost
                                    learning_rate=0.1,              # shrinks the contribution of each tree
                                    n_estimators=100,               # number of boosting stages
                                    subsample=1.0,                  # fraction of samples used t fit base learners
                                    criterion='friedman_mse',       # measures the quality of a split
                                    min_samples_split=2,            
                                    min_samples_leaf=1, 
                                    min_weight_fraction_leaf=0.0,   # min. fraction of sum of weights
                                    max_depth=3,                    # opt value depends on interaction
                                    min_impurity_decrease=0.0, 
                                    min_impurity_split=None, 
                                    max_features=None, 
                                    max_leaf_nodes=None, 
                                    warm_start=False, 
                                    presort='auto',
                                    validation_fraction=0.1, 
                                    tol=0.0001)

AdaBoostClassifier 类似,该模型无法处理缺失值。我们将再次使用 12 折交叉验证来获取用于分类滚动 1 个月持有期的方向回报的错误,如下图所示:

gb_cv_result = run_cv(gb_clf, y=y_clean, X=X_dummies_clean)
gb_result = stack_results(gb_cv_result)

我们将解析和绘制结果,以发现与 AdaBoostClassifier 相比略有改进的情况,如下图所示:

如何使用 GridSearchCV 调整参数

model_selection 模块中的 GridSearchCV 类促进了对我们想要测试的所有超参数值组合的系统评估。在下面的代码中,我们将说明此功能,用于七个调整参数,当定义时将产生总共 2⁴ x 3² x 4 = 576 种不同的模型配置:

cv = OneStepTimeSeriesSplit(n_splits=12)

param_grid = dict(
        n_estimators=[100, 300],
        learning_rate=[.01, .1, .2],
        max_depth=list(range(3, 13, 3)),
        subsample=[.8, 1],
        min_samples_split=[10, 50],
        min_impurity_decrease=[0, .01],
        max_features=['sqrt', .8, 1]
)

.fit() 方法使用自定义的 OneStepTimeSeriesSplitroc_auc 分数执行交叉验证以评估 12 折。Sklearn 允许我们使用 joblib pickle 实现持久化结果,就像对任何其他模型一样,如以下代码所示:

gs = GridSearchCV(gb_clf,
                   param_grid,
                   cv=cv,
                   scoring='roc_auc',
                   verbose=3,
                   n_jobs=-1,
                   return_train_score=True)
gs.fit(X=X, y=y)

# persist result using joblib for more efficient storage of large numpy arrays
joblib.dump(gs, 'gbm_gridsearch.joblib')

GridSearchCV 对象在完成后具有几个额外的属性,我们可以在加载拾取的结果后访问,以了解哪种超参数组合效果最佳以及其平均交叉验证 AUC 分数,这导致了比默认值略有改进。以下代码显示了这一点:

pd.Series(gridsearch_result.best_params_)
learning_rate              0.01
max_depth                  9.00
max_features               1.00
min_impurity_decrease      0.01
min_samples_split         10.00
n_estimators             300.00
subsample                  0.80

gridsearch_result.best_score_
0.6853

参数对测试分数的影响

GridSearchCV 结果存储了平均交叉验证分数,以便我们分析不同的超参数设置如何影响结果。

下图左侧面板中的六个 seaborn swarm 图显示了所有参数值的 AUC 测试分数分布。在这种情况下,最高的 AUC 测试分数需要较低的 learning_rate 和较大的 max_features 值。某些参数设置,例如较低的 learning_rate,会产生一系列取决于其他参数互补设置的结果范围。其他参数与实验中使用的所有设置的高分数兼容:

我们现在将探讨超参数设置如何共同影响平均交叉验证分数。 为了深入了解参数设置是如何相互作用的,我们可以使用DecisionTreeRegressor训练,将平均测试分数作为结果,将参数设置编码为一组独热或虚拟格式的分类变量(详情请参阅笔记本)。 树结构突显了使用所有特征(max_features_1),低learning_ratemax_depth超过 3 导致了最佳结果,如下图所示:

在本节第一个图表的右侧面板中的条形图显示了超参数设置对产生不同结果的影响,其特征重要性由决策树产生,决策树生长到其最大深度为止。 自然,出现在树顶部附近的特征也累积了最高的重要性得分。

如何在留置集上进行测试

最后,我们希望评估我们从GridSearchCV练习中排除的留置集上最佳模型的性能。 它包含了样本期的最后六个月(截至 2018 年 2 月;详情请参阅笔记本)。 我们基于 AUC 分数0.6622获得了一个泛化性能估计,使用以下代码:

best_model = gridsearch_result.best_estimator_
preds= best_model.predict(test_feature_data)
roc_auc_score(y_true=test_target, y_score=preds)
0.6622

sklearn 梯度提升实现的缺点是计算速度有限,这使得快速尝试不同的超参数设置变得困难。 在接下来的部分中,我们将看到在过去的几年中出现了几种优化实现,大大减少了训练甚至大规模模型所需的时间,并且极大地扩展了这种高效算法的应用范围。

快速可扩展的 GBM 实现

过去几年中,出现了几种新的梯度提升实现,采用了各种创新,加速了训练,提高了资源效率,并允许算法扩展到非常大的数据集。 新的实现及其来源如下:

  • XGBoost(极端梯度提升),2014 年由华盛顿大学的 Tianqi Chen 发起

  • LightGBM,于 2017 年 1 月由 Microsoft 首次发布

  • CatBoost,于 2017 年 4 月由 Yandex 首次发布

这些创新解决了训练梯度提升模型的特定挑战(请参阅本章在 GitHub 上的README以获取详细参考)。 XGBoost 的实现是第一个获得流行的新实现:在 2015 年 Kaggle 发布的 29 个获奖解决方案中,有 17 个解决方案使用了 XGBoost。 其中 8 个仅依赖于 XGBoost,而其他的则将 XGBoost 与神经网络结合使用。

在说明其实现之前,我们将首先介绍随着时间推移而出现并最终收敛的关键创新(以便大多数功能对所有实现都可用)。

算法创新如何推动性能

随机森林可以通过在独立的自助样本上生长单独的树来并行训练。相比之下,梯度提升的顺序方法会减慢训练速度,这反过来会使得需要适应任务和数据集性质的大量超参数的实验变得更加复杂。

为了通过树扩展整体,训练算法以增量方式最小化相对于整体损失函数的负梯度的预测误差,类似于传统的梯度下降优化器。因此,在训练过程中评估每个特征对决策树与当前梯度拟合的潜在分割点的影响的计算成本与时间成正比。

二阶损失函数近似

最重要的算法创新通过使用依赖于二阶导数的近似来降低评估损失函数的成本,类似于牛顿法找到稳定点。因此,在贪婪树扩展过程中评分潜在分割的速度相对于使用完整损失函数更快。

正如先前提到的,梯度提升模型是以增量方式训练的,其目标是最小化整体 H[M] 的预测误差和正则化惩罚的组合。用 m 步后集成对结果 y[i] 的预测表示为 ŷ[i]^((m)),l 是一个可微的凸损失函数,用来衡量结果和预测之间的差异,Ω 是一个随着整体 H[M] 复杂性增加而增加的惩罚项,增量假设 h[m] 的目标是最小化以下目标函数:

正则化惩罚有助于通过偏好选择使用简单且有预测性的回归树的模型来避免过拟合。例如,在 XGBoost 的情况下,回归树 h 的惩罚取决于每棵树的叶子数 T,每个终端节点的回归树得分 w,以及超参数 γ 和 λ。这在以下公式中总结如下:

因此,在每一步,算法都会贪婪地添加最能改进正则化目标的假设 h[m]。基于泰勒展开的损失函数的二阶近似加速了目标函数的评估,如以下公式所总结:

这里,g[i] 是添加新的学习器前给定特征值的损失函数的一阶梯度,而 h[i] 是相应的二阶梯度(或者海森矩阵)值,如下式所示:

XGBoost 算法是第一个利用损失函数的这种近似来计算给定树结构的最优叶子分数和相应损失函数值的开源算法。分数由终端节点中样本的梯度和 Hessian 的和的比率组成。它使用此值来评分分裂所导致的信息增益,类似于我们在前一章中看到的节点不纯度度量,但适用于任意损失函数(详细推导请参阅 GitHub 上的参考文献)。

简化的分裂查找算法

sklearn 的梯度提升实现找到枚举连续特征所有选项的最优拆分。这种精确的贪婪算法在计算上非常耗费资源,因为它必须首先按特征值对数据进行排序,然后对潜在的非常大数量的拆分选项进行评分和决策。当数据不适合内存或在多台机器上的分布式设置中进行训练时,此方法会面临挑战。

一种近似的分裂查找算法通过将特征值分配给用户确定的一组箱子来减少分裂点的数量,这也可以在训练期间大大减少内存需求,因为每个箱子只需要存储一个分裂。XGBoost 引入了一种分位数草图算法,也能将加权训练样本分成百分位箱子,以实现均匀分布。XGBoost 还引入了处理稀疏数据的能力,这些数据由缺失值、频繁的零梯度统计和一位有效编码引起,并且还可以为给定的分裂学习一个最优的默认方向。因此,该算法只需评估非缺失值。

相比之下,LightGBM 使用基于梯度的单侧采样GOSS)来排除具有小梯度的大部分样本,并且仅使用其余部分来估算信息增益,并相应地选择分裂值。具有较大梯度的样本需要更多的训练,并且倾向于更多地对信息增益做出贡献。LightGBM 还使用排他性特征绑定来组合那些彼此互斥的特征,它们很少同时取非零值,以减少特征数量。因此,LightGBM 在发布时是最快的实现。

深度优先与叶子优先增长

LightGBM 与 XGBoost 和 CatBoost 在优先拆分哪些节点方面有所不同。LightGBM 以叶子方式决定分裂,即拆分最大化信息增益的叶子节点,即使这会导致不平衡树。相比之下,XGBoost 和 CatBoost 以深度方式扩展所有节点,并在添加更多级别之前首先拆分给定深度的所有节点。这两种方法以不同的顺序扩展节点,除了完整的树之外,它们将产生不同的结果。下图说明了这两种方法:

LightGBM 的叶子优先分割往往增加模型复杂性,并且可能加快收敛速度,但也增加了过拟合的风险。一个深度为 n 的以节点为优先的树最多有 2^(n )个叶子节点,而一个以叶子为优先的树具有 2^n 个叶子,可能会有更多的层次,并且某些叶子节点中包含相对较少的样本。因此,调整 LightGBM 的 num_leaves 设置需要额外的注意,库也允许我们同时控制 max_depth 以避免不必要的节点不平衡。LightGBM 的更近期版本还提供了深度为优先的树增长。

基于 GPU 的训练

所有新的实现支持在一个或多个 GPU 上进行训练和预测,以实现显著的加速。它们与当前的 CUDA 启用的 GPU 兼容。安装要求各不相同,并且正在快速发展。XGBoost 和 CatBoost 的实现适用于几个当前版本,但是 LightGBM 可能需要本地编译(请参阅 GitHub 获取相关文档链接)。

加速取决于库和数据类型,并且范围从低的单位数字倍增到数十倍的因子。仅需更改任务参数并且不需要其他超参数修改就能激活 GPU。

DART – 树的 dropout

2015 年,Rashmi 和 Gilad-Bachrach 提出了一个新模型来训练梯度提升树,旨在解决他们称之为过度专业化的问题:在后续迭代中添加的树往往只影响少数实例的预测,而对于其余实例的贡献较小。然而,该模型的样本外性能可能会受到影响,并且可能对先前在过程中添加的少数树的贡献过于敏感。

新的算法采用了 dropouts,成功地用于学习更准确的深度神经网络,其中 dropouts 在学习过程中静音了部分神经连接。因此,高层节点不能依赖少数连接来传递预测所需的信息。这种方法对于深度神经网络的成功做出了重要贡献,也已与其他学习技术一起使用,例如逻辑回归,以静音特征的随机份额。随机森林和随机梯度提升也会静音随机特征子集。

DART 在树的层次上运作,并且对整棵树进行静音,而不是对个别特征进行操作。使用 DART 生成的集成树的目标是更均匀地对最终预测作出贡献。在某些情况下,这已被证明对排名、回归和分类任务产生更准确的预测。该方法首先在 LightGBM 中实现,并且也适用于 XGBoost。

分类特征的处理

CatBoost 和 LightGBM 实现直接处理分类变量,无需虚拟编码。

CatBoost 的实现(其命名源自对分类特征的处理)包括处理这些特征的几个选项,除了自动独热编码外,还为几个特征的单独分类或多个特征组合分配数字值。换句话说,CatBoost 可以从现有特征的组合中创建新的分类特征。与个别特征或特征组合的分类水平相关的数字值取决于它们与结果值的关系。在分类情况下,这与观察正类别的概率有关,该概率在样本上累积计算,基于先验和平滑系数。有关更详细的数值示例,请参阅文档。

LightGBM 的实现将分类特征的水平分组以最大化(或最小化)与结果值相对于组内的均匀性。

XGBoost 的实现不直接处理分类特征,需要进行独热(或虚拟)编码。

其他功能和优化

XGBoost 通过在内存中保留压缩的列块来优化计算,在几个方面使计算多线程化,其中每个列都按相应特征值排序。XGBoost 在训练前仅计算一次此输入数据布局,并在整个过程中重复使用它以摊销额外的前期成本。使用可以并行完成的分位数时,对列的拆分统计信息的搜索变为线性扫描,并且易于支持列子抽样。

后来发布的 LightGBM 和 CatBoost 库构建在这些创新之上,并通过优化线程和减少内存使用量进一步加速了训练。由于它们的开源性质,这些库随着时间的推移往往会趋于一致。

XGBoost 还支持单调性约束。这些约束确保给定特征的值仅在其整个范围内与结果呈正相关或负相关。它们对于合并已知为真的模型的外部假设非常有用。

如何使用 XGBoost、LightGBM 和 CatBoost

XGBoost、LightGBM 和 CatBoost 提供多种语言的接口,包括 Python,并且具有与其他 sklearn 特性兼容的 sklearn 接口,例如 GridSearchCV,以及它们自己的方法来训练和预测梯度提升模型。gbm_baseline.ipynb 笔记本展示了每个实现的 sklearn 接口的使用。这些库方法通常文档更好,而且也更容易使用,因此我们将使用它们来说明这些模型的使用。

该过程涉及创建特定于库的数据格式,调整各种超参数以及评估我们将在接下来的章节中描述的结果。 附带的笔记本包含gbm_tuning.pygbm_utils.pygbm_params.py文件,共同提供以下功能,并生成相应的结果。

如何创建二进制数据格式

所有库都有自己的数据格式,用于预先计算特征统计信息以加速搜索分割点,如前所述。 这些也可以持久化以加速后续训练的开始。

以下代码为要与OneStepTimeSeriesSplit一起使用的每个模型构造了二进制训练和验证数据集:

cat_cols = ['year', 'month', 'age', 'msize', 'sector']
data = {}
for fold, (train_idx, test_idx) in enumerate(kfold.split(features)):
    print(fold, end=' ', flush=True)
    if model == 'xgboost':
        data[fold] = {'train': xgb.DMatrix(label=target.iloc[train_idx],
                                           data=features.iloc[train_idx],
                                           nthread=-1),                  # use avail. threads
                      'valid': xgb.DMatrix(label=target.iloc[test_idx],
                                           data=features.iloc[test_idx],
                                           nthread=-1)}
    elif model == 'lightgbm':
        train = lgb.Dataset(label=target.iloc[train_idx],
                            data=features.iloc[train_idx],
                            categorical_feature=cat_cols,
                            free_raw_data=False)

        # align validation set histograms with training set
        valid = train.create_valid(label=target.iloc[test_idx],
                                   data=features.iloc[test_idx])

        data[fold] = {'train': train.construct(),
                      'valid': valid.construct()}

    elif model == 'catboost':
        # get categorical feature indices
        cat_cols_idx = [features.columns.get_loc(c) for c in cat_cols]
        data[fold] = {'train': Pool(label=target.iloc[train_idx],
                                    data=features.iloc[train_idx],
                                    cat_features=cat_cols_idx),

                      'valid': Pool(label=target.iloc[test_idx],
                                    data=features.iloc[test_idx],
                                    cat_features=cat_cols_idx)}

可用选项略有不同:

  • xgboost 允许使用所有可用线程

  • lightgbm明确地将为验证集创建的分位数与训练集对齐

  • catboost 实现需要使用索引而不是标签来识别特征列

如何调整超参数

许多超参数在gbm_params.py中列出。 每个库都有参数设置来:

  • 指定总体目标和学习算法

  • 设计基学习器

  • 应用各种正则化技术

  • 在训练过程中处理提前停止

  • 启用 GPU 或在 CPU 上进行并行化

每个库的文档详细介绍了可能引用相同概念的各种参数,但是在库之间具有不同名称的参数。 GitHub 存储库包含指向突出显示xgboostlightgbm相应参数的网站的链接。

目标和损失函数

这些库支持几种提升算法,包括树的梯度提升和线性基学习器,以及 LightGBM 和 XGBoost 的 DART。 LightGBM 还支持我们之前描述的 GOSS 算法,以及随机森林。

梯度提升的吸引力在于有效支持任意可微损失函数,每个库都提供了用于回归,分类和排名任务的各种选项。 除了选择的损失函数之外,在训练和交叉验证期间还可以使用其他评估指标来监视性能。

学习参数

梯度提升模型通常使用决策树来捕获特征交互,个体树的大小是最重要的调整参数。 XGBoost 和 CatBoost 将max_depth默认设置为 6。 相反,LightGBM 使用默认的num_leaves值为 31,这对应于平衡树的五个级别,但对级别的数量没有约束。 为了避免过拟合,num_leaves应低于2^(max_depth)。 例如,对于表现良好的max_depth值为 7,您应将num_leaves设置为 70–80,而不是 2⁷=128,或者直接限制max_depth

树的数量或增强迭代次数定义了整体集合的规模。所有库都支持early_stopping,一旦损失函数在给定迭代次数内不再改善,就会终止训练。因此,通常最好设置大量迭代次数,并根据验证集上的预测性能停止训练。

正则化

所有库都实现了对基本学习器的正则化策略,例如样本数量的最小值或分割和叶节点所需的最小信息增益。

它们还支持通过学习速率通过收缩来在整个集合层次上实现正则化,限制新树的贡献。还可以通过回调函数实现自适应学习速率,随着训练的进行而降低学习速率,这在神经网络的上下文中已经成功使用过。此外,梯度提升损失函数可以通过* L1 L2 进行正则化,类似于通过修改Ω( h [m] *)或通过增加添加更多树的惩罚γ来描述的 Ridge 和 Lasso 线性回归模型。

这些库还允许使用装袋或列子抽样来随机化树的生长,用于随机森林,并减少整体方差以去相关预测误差。为了保护免受过拟合,对于近似分割查找的特征量化,添加更大的箱子作为另一种选择。

随机化网格搜索

为了探索超参数空间,我们为我们想要测试的关键参数指定值。sklearn 库支持RandomizedSearchCV,从指定的分布中随机抽样一部分参数组合进行交叉验证。我们将实现一个自定义版本,允许我们利用早停,同时监视当前表现最佳的组合,因此我们可以在满意结果时中止搜索过程,而不是事先指定一组迭代次数。

为此,我们根据每个库的参数指定参数网格,使用itertools库提供的内置笛卡尔product生成器生成所有组合,并随机shuffle结果。在 LightGBM 的情况下,我们会自动根据当前num_leaves值设置max_depth,如下所示:

param_grid = dict(
        # common options
        learning_rate=[.01, .1, .3],
        colsample_bytree=[.8, 1],  # except catboost

        # lightgbm
        num_leaves=[2 ** i for i in range(9, 14)],
        boosting=['gbdt', 'dart'],
        min_gain_to_split=[0, 1, 5],  # not supported on GPU

all_params = list(product(*param_grid.values()))
n_models = len(all_params) # max number of models to cross-validate
shuffle(all_params)

然后,我们执行交叉验证如下:

GBM = 'lightgbm'
for test_param in all_params:
    cv_params = get_params(GBM)
    cv_params.update(dict(zip(param_grid.keys(), test_param)))
    if GBM == 'lightgbm':
        cv_params['max_depth'] = int(ceil(np.log2(cv_params['num_leaves'])))
    results[n] = run_cv(test_params=cv_params,
                        data=datasets,
                        n_splits=n_splits,
                        gb_machine=GBM)

run_cv函数实现了三个库的交叉验证。对于light_gbm示例,该过程如下:

def run_cv(test_params, data, n_splits=10):
    """Train-Validate with early stopping"""
    result = []
    cols = ['rounds', 'train', 'valid']
    for fold in range(n_splits):
        train = data[fold]['train']
        valid = data[fold]['valid']

        scores = {}
        model = lgb.train(params=test_params,
                          train_set=train,
                          valid_sets=[train, valid],
                          valid_names=['train', 'valid'],
                          num_boost_round=250,
                          early_stopping_rounds=25,
                          verbose_eval=50,
                          evals_result=scores)

        result.append([model.current_iteration(),
                       scores['train']['auc'][-1],
                       scores['valid']['auc'][-1]])

    return pd.DataFrame(result, columns=cols)

train()方法还会生成存储在scores字典中的验证分数。当早停生效时,最后一次迭代也是最佳分数。有关更多详细信息,请参见 GitHub 上的完整实现。

如何评估结果

使用 GPU,我们可以在几分钟内训练一个模型,并在几个小时内评估数百个参数组合,而使用 sklearn 实现则需要多天。对于 LightGBM 模型,我们探索了使用库处理分类变量的因子版本和使用独热编码的虚拟版本。

结果存储在 model_tuning.h5 HDF5 存储中。模型评估代码样本在 eval_results.ipynb 笔记本中。

跨模型的交叉验证结果

当比较四次测试运行中三个库的平均交叉验证 AUC 时,我们发现 CatBoost 为表现最佳模型产生了稍高的 AUC 分数,同时也产生了最广泛的结果分布,如下图所示:

表现最佳的 CatBoost 模型使用以下参数(详见笔记本):

  • max_depth 为 12,max_bin 为 128

  • max_ctr_complexity 为 2,限制了分类特征的组合数量

  • one_hot_max_size 为 2,排除了二元特征的数值变量分配

  • random_strength 不等于 0 以随机化分裂的评估

训练相对于 LightGBM 和 XGBoost 稍慢(都使用 GPU),平均每个模型 230 秒。

对 LightGBM 和 XGBoost 模型表现最好的更详细的分析显示,LightGBM 因子模型的性能几乎与其他两个模型相当,但模型复杂度要低得多。它平均只包含 41 棵树,深度为三级,每棵树最多有八个叶子节点,并且还使用了 min_gain_to_split 形式的正则化。它在训练集上过拟合明显较少,训练 AUC 仅略高于验证 AUC。它还训练速度更快,每个模型只需 18 秒,因为它的复杂度更低。实际上,这个模型更可取,因为它更有可能产生良好的样本外表现。具体细节如下表所示:

LightGBM 虚拟 XGBoost 虚拟 LightGBM 因子
验证 AUC 68.57% 68.36% 68.32%
训练 AUC 82.35% 79.81% 72.12%
learning_rate 0.1 0.1 0.3
max_depth 13 9 3
num_leaves 8192 8
colsample_bytree 0.8 1 1
min_gain_to_split 0 1 0
轮数 44.42 59.17 41.00
时间 86.55 85.37 18.78

下图显示了不同 max_depth 设置对 LightGBM 和 XGBoost 模型的验证分数的影响:较浅的树产生了更广泛的结果范围,需要与适当的学习率和正则化设置相结合,以产生前面表格中显示的强结果:

与之前显示的 DecisionTreeRegressor 不同,我们也可以使用线性回归来评估不同特征在验证 AUC 分数方面的统计显著性。对于 LightGBM 虚拟模型,其中回归解释了结果变异的 68%,我们发现只有 min_gain_to_split 正则化参数不显著,如下图所示:

在实践中,深入了解模型如何进行预测非常重要,特别是对于投资策略,决策者通常需要合理的解释。

如何解释 GBM 结果

理解模型为什么预测某个结果对于多种原因都非常重要,包括信任、可操作性、责任和调试。通过模型发现的特征与结果之间的非线性关系以及特征之间的相互作用也在学习更多关于所研究现象的基本驱动因素时具有价值。

了解树集成方法(如梯度提升或随机森林模型)所做预测的见解的常见方法是将特征重要性值归因于每个输入变量。这些特征重要性值可以根据单个预测的情况或整个数据集(即所有样本)全局计算,以获得模型进行预测的更高级别视角。

特征重要性

有三种主要方法来计算全局特征重要性值:

  • Gain: 这种经典方法由 Leo Breiman 在 1984 年引入,使用给定特征的所有分割所贡献的损失或杂质的总减少。其动机在很大程度上是启发式的,但它是一种常用的特征选择方法。

  • 分割计数:这是一种替代方法,它计算使用特征进行分割决策的频率,根据选择特征以达到所得信息增益。

  • 排列:这种方法随机排列测试集中的特征值,并测量模型误差的变化量,假设重要特征应该会导致预测误差大幅增加。不同的排列选择导致该基本方法的替代实现。

对于单个预测计算特征重要性值的方法较少,因为可用的模型无关解释方法比树特定方法慢得多。

所有梯度提升实现在训练后都提供特征重要性得分作为模型属性。XGBoost 库提供五个版本,如下列表所示:

  • total_gaingain 作为其每个分割的平均值

  • total_cover 作为使用特征时每个分割的样本数

  • weight 作为前面值的分割计数

可使用训练模型的.get_score()方法和相应的importance_type参数获取这些值。对于性能最佳的 XGBoost 模型,结果如下(total度量具有 0.8 的相关性,covertotal_cover也是如此):

虽然不同月份和年份的指标占主导地位,但最近 1 个月的回报是从total_gain的角度来看第二重要的特征,并且根据weight度量经常被使用,但由于它平均应用于相对较少的实例而产生较低的平均收益(有关实施细节,请参见笔记本)。

偏依赖图

除了个体特征对模型预测的总体贡献之外,偏依赖图还可视化目标变量与一组特征之间的关系。梯度提升树的非线性特性导致这种关系取决于所有其他特征的值。因此,我们将对这些特征进行边际化处理。通过这样做,我们可以将偏依赖性解释为期望的目标响应。

我们只能为单个特征或特征对可视化偏依赖性。后者会产生等高线图,显示不同特征值的组合如何产生不同的预测概率,如下所示:

fig, axes = plot_partial_dependence(gbrt=gb_clf,
                                    X=X_dummies_clean,
                                    features=['month_9', 'return_1m', 'return_3m', ('return_1m', 'return_3m')],
                                    feature_names=['month_9','return_1m', 'return_3m'],
                                    percentiles=(0.01, 0.99),
                                    n_jobs=-1,
                                    n_cols=2,
                                    grid_resolution=250)

经过一些额外的格式化(请参阅配套笔记本),我们得到了以下图表:

右下角的图显示了在消除[1%,99%]百分位数处的离群值后,给定滞后 1 个月和 3 个月收益值范围的情况下,下个月产生正回报的概率的依赖性。 month_9 变量是一个虚拟变量,因此呈阶梯函数样式的图。我们还可以将依赖性可视化为 3D,如下代码所示:

targets = ['return_1m', 'return_3m']
target_feature = [X_dummies_clean.columns.get_loc(t) for t in targets]
pdp, axes = partial_dependence(gb_clf,
                               target_feature,
                               X=X_dummies_clean,
                               grid_resolution=100)

XX, YY = np.meshgrid(axes[0], axes[1])
Z = pdp[0].reshape(list(map(np.size, axes))).T

fig = plt.figure(figsize=(14, 8))
ax = Axes3D(fig)
surf = ax.plot_surface(XX, YY, Z,
                       rstride=1,
                       cstride=1,
                       cmap=plt.cm.BuPu,
                       edgecolor='k')
ax.set_xlabel(' '.join(targets[0].split('_')).capitalize())
ax.set_ylabel(' '.join(targets[1].split('_')).capitalize())
ax.set_zlabel('Partial Dependence')
ax.view_init(elev=22, azim=30)

这产生了以下 1 个月回报方向对滞后 1 个月和 3 个月回报的偏依赖的 3D 图:

SHapley Additive exPlanations

在 2017 年 NIPS 会议上,来自华盛顿大学的 Scott Lundberg 和 Su-In Lee 提出了一种新的更准确的方法来解释树集成模型对输出的贡献,称为SHapley Additive exPlanations,或SHAP值。

这个新算法基于这样一个观察:树集成的特征归因方法,如我们之前看过的方法,是不一致的——即,使特征对输出的影响增加的模型变化可能会降低该特征的重要性值(请参阅 GitHub 上有关此的详细说明)。

SHAP 值统一了协作博弈理论和局部解释的思想,并且根据期望值显示其在理论上是最优、一致和局部准确的。最重要的是,Lundberg 和 Lee 开发了一种算法,成功将计算这些与模型无关的、可加的特征归因方法的复杂度从 O(TLD^M) 减少到 O(TLD²),其中 TM 分别是树和特征的数量,DL 是树的最大深度和叶子节点数。这一重要的创新使得对先前难以处理的具有数千棵树和特征的模型的预测能够在几秒钟内解释。一个开源实现于 2017 年底发布,并且兼容 XGBoost、LightGBM、CatBoost 和 sklearn 树模型。

Shapley 值源自博弈论,是一种为协作游戏中的每个玩家分配值的技术,反映了他们对团队成功的贡献。SHAP 值是对基于树的模型的博弈论概念的一种改编,并且为每个特征和每个样本计算。它们衡量了一个特征对给定观察结果的模型输出的贡献。因此,SHAP 值提供了不同的洞察力,说明了特征的影响如何在样本间变化,这在这些非线性模型中交互作用效应的作用中非常重要。

如何按特征总结 SHAP 值

要获得对多个样本的特征重要性的高级概述,有两种绘制 SHAP 值的方法:对所有样本进行简单平均,类似于先前计算的全局特征重要性度量(如下图左侧面板所示),或者绘制散点图来显示每个样本的每个特征的影响(如下图右侧面板所示)。使用兼容库的训练模型和匹配输入数据非常容易产生,如下面的代码所示:

# load JS visualization code to notebook
shap.initjs()

# explain the model's predictions using SHAP values
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

shap.summary_plot(shap_values, X_test, show=False)

下图右侧的散点图按照所有样本的 SHAP 值对特征进行排序,然后显示了每个特征对模型输出的影响,其值由特征的值的函数表示,通过其颜色表示,红色表示高值,蓝色表示低值,相对于特征的范围:

如何使用力图解释预测

下面的力图显示了各种特征及其值对模型输出的累积影响,在这种情况下,模型输出为 0.6,比基准值 0.13(提供的数据集上的平均模型输出)高得多。突出显示为红色的特征增加了输出。十月份是最重要的特征,将输出从 0.338 增加到 0.537,而 2017 年则降低了输出。

因此,我们获得了模型如何到达特定预测的详细分解,如下图所示:

我们还可以同时计算大量数据点或预测的力图,并使用聚类可视化来深入了解数据集中某些影响模式的普遍程度。以下图表显示了前 1000 个观察结果的力图,旋转了 90 度,水平堆叠,并按照不同特征对给定观察结果的影响排序。该实现使用层次凝聚聚类对特征 SHAP 值上的数据点进行标识以识别这些模式,并以交互方式显示结果进行探索性分析(参见笔记本),如以下代码所示:

shap.force_plot(explainer.expected_value, shap_values[:1000,:], X_test.iloc[:1000])

这将产生以下输出:

如何分析特征交互

最后,SHAP 值使我们能够通过将这些相互作用与主要效应分离来获得对不同特征之间的相互作用效应的额外洞察。shap.dependence_plot 可以定义如下:

shap.dependence_plot("return_1m", shap_values, X_test, interaction_index=2, title='Interaction between 1- and 3-Month Returns')

它显示了 1 个月回报的不同值(x 轴)如何影响结果(y 轴上的 SHAP 值),并由 3 个月回报区分:

SHAP 值在每个单独预测的水平上提供了细粒度的特征归因,并通过(交互式)可视化实现了对复杂模型的更丰富检查。本节开头显示的 SHAP 摘要散点图提供了比全局特征重要性条形图更加差异化的见解。单个聚类预测的力图允许进行更详细的分析,而 SHAP 依赖图捕获了相互作用效应,并因此提供了比局部依赖图更准确和详细的结果。

SHAP 值的限制与任何当前特征重要性度量一样,涉及对高度相关的变量影响的归因,因为它们相似的影响可能以任意方式被分解。

摘要

在本章中,我们探讨了梯度提升算法,该算法用于以顺序方式构建集成,添加一个只使用非常少的特征的浅层决策树,以改善已经做出的预测。我们看到了梯度提升树如何可以非常灵活地应用于广泛的损失函数,并提供了许多机会来调整模型以适应给定的数据集和学习任务。

最近的实现大大简化了梯度提升的使用,通过加速训练过程并提供更一致和详细的特征重要性以及单个预测驱动因素的洞察。在下一章中,我们将转向贝叶斯方法来进行机器学习。

第十二章:无监督学习

在第六章,机器学习流程中,我们讨论了无监督学习如何通过发现数据中的结构而增加价值,而无需一个指导搜索过程的结果变量,例如老师。这个任务与我们在过去几章中专注于的监督学习的设置形成对比。

当数据集只包含特征而没有结果的测量值,或者当我们想要提取与结果无关的信息时,无监督学习算法可以很有用。目标不是预测未来的结果,而是研究对解决另一个任务有用的数据的信息表示,包括探索数据集。

例如,识别主题以总结文档(参见第十四章,主题建模)、减少特征数量以减少过度拟合和监督学习的计算成本,或者分组相似的观察,如本章末尾资产配置的聚类使用所示。

降维和聚类是无监督学习的主要任务:

  • 降维将现有特征转换为一个新的、更小的集合,同时尽量减少信息的损失。存在广泛的算法,它们仅在如何衡量信息损失、是否应用线性或非线性变换或对新特征集施加的约束方面有所不同。

  • 聚类算法不是识别新特征,而是识别和分组相似的观察或特征。不同的算法在如何定义观察的相似性以及对结果群组的假设上有所不同。

更具体地说,本章涵盖以下内容:

  • 主成分分析(PCA)和独立成分分析(ICA)如何进行线性降维

  • 如何应用 PCA 来识别资产回报的风险因素和特征组合

  • 如何使用非线性流形学习来总结高维数据以进行有效的可视化

  • 如何使用 t-SNE 和 UMAP 来探索高维替代图像数据

  • 如何使用 k-Means、分层和基于密度的聚类算法

  • 如何使用凝聚聚类构建根据层次风险平衡构建健壮的投资组合

每个部分的代码示例都在本章的在线 GitHub 存储库的目录中,网址为github.com/PacktPublishing/Hands-On-Machine-Learning-for-Algorithmic-Trading

降维

从线性代数的角度来看,数据集的特征创建了一个向量空间,其维数对应于线性独立列的数量(假设观测数量多于特征数量)。当两列线性相关时,它们完全相关,因此可以使用加法和乘法的线性运算从其中一个计算另一个。

换句话说,它们是表示相同而不是不同方向或轴的平行向量,并且只构成一个单一的维度。类似地,如果一个变量是其他几个变量的线性组合,则它是由这些列创建的向量空间的元素,而不是增加自己的新维度。

数据集的维数很重要,因为每个新维度都可以添加关于结果的信号。但是,还存在一个被称为维度诅咒的负面影响:随着独立特征数量的增加,同时保持观测数量不变,数据点之间的平均距离也会增加,并且特征空间的密度呈指数级下降。

对于机器学习而言,当观察结果彼此之间更远、即相异时,预测变得更加困难;下一节将讨论由此产生的挑战。

降维旨在通过使用较少的特征更有效地表示数据中的信息。为此,算法将数据投影到一个低维空间,同时舍弃不具信息性的数据变异,或者通过确定数据所在的或接近的低维子空间或流形。

流形是在局部类似于欧几里得空间的空间。一维流形包括线和圆(但不包括八的截屏,因为存在交点)。流形假设认为高维数据通常位于一个低维空间中,如果确定了这个空间,就可以在这个子空间中对数据进行忠实的表示。

因此,降维通过找到一组不同的、较小的变量来捕获原始特征中最重要的内容,从而最小化信息损失。压缩有助于对抗维度诅咒,节省内存,并允许可视化高维数据的显著特征,否则这些特征很难探索。

线性和非线性算法

降维算法在对新变量施加的约束以及它们如何旨在最小化信息损失方面存在差异:

  • 线性算法,如 PCA 和 ICA,将新变量限制为原始特征的线性组合;也就是说,在较低维度空间中的超平面。而 PCA 要求新特征是不相关的,ICA 进一步要求统计独立性——即线性和非线性关系的缺失。以下截图说明了 PCA 如何将三维特征投影到二维空间中:

  • 非线性算法不局限于超平面,并且可以捕捉数据中更复杂的结构。然而,由于选项的无限数量,算法仍然需要做出假设来得出解决方案。在本节中,我们展示了t-分布随机邻居嵌入t-SNE)和均匀流形近似和投影UMAP)对于可视化高维数据非常有用。以下截图说明了流形学习如何在三维特征空间中识别二维子空间(manifold_learning笔记本展示了使用其他算法的情况,包括局部线性嵌入):

维度的诅咒

数据集维度的增加意味着表示对应欧几里得空间中每个观测的特征向量中的条目更多。我们使用欧几里得距离,也称为L2 范数,来测量向量空间中的距离,我们将其应用于线性回归系数向量以训练正则化的岭回归模型。

两个具有笛卡尔坐标p = (p[1], p[2], ..., p[n])q = (q[1], q[2], ..., q[n])n维向量之间的欧几里得距离是用毕达哥拉斯所发展的熟知公式计算的:

因此,每个新的维度都会向总和中添加一个非负项,以使距离随着不同向量的维数增加而增加。换句话说,随着给定观测数量的特征数量增加,特征空间变得越来越稀疏;也就是说,越来越少或者更为空。

下图显示了我们需要多少数据点来维持均匀分布在一条线上的 10 个观测的平均距离。随着数据在每次添加新维度时需要扩展 10 倍,从一维中的 10¹增加到二维中的 10²和三维中的 10³,呈指数增长:

GitHub 存储库文件夹中的curse_of_dimensionality笔记本模拟了随着维度数量增加,数据点之间的平均距离和最小距离如何增加:

该模拟从不相关的均匀分布或相关的正态分布中绘制特征的范围为[0, 1],并逐渐增加特征数量到 2,500。对于从正态分布中绘制的特征,数据点之间的平均距离增加到特征范围的 11 倍以上,对于从不相关的均匀分布中绘制的特征,在(极端)情况下增加到 20 倍以上。

当观察之间的距离增加时,监督机器学习变得更加困难,因为对新样本的预测不太可能是基于对相似训练特征的学习。换句话说,随着特征数量的增加,可能的唯一行数呈指数增长,这使得从空间中高效采样变得更加困难。

同样,由灵活算法学到的函数的复杂度随着维度数量的增加呈指数增长,这些算法对实际关系的假设较少。

灵活算法包括我们在第十章,决策树和随机森林,以及第十一章,梯度提升机 中看到的基于树的模型,以及从第十七章 深度学习 开始我们将介绍的深度神经网络。这些算法的方差随着它们有更多机会在更多维度上过拟合噪声而增加,导致泛化性能较差。

在实践中,特征之间存在相关性,通常相当显著,或者不具有太多变化。出于这些原因,降维有助于压缩数据而几乎不损失信号,并且在节约内存的同时抵消了维数灾难。在这些情况下,它补充了使用正则化来管理由于方差和模型复杂度而产生的预测误差。

接下来我们要解决的关键问题是:找到一种尽可能减少信息损失的数据的低维表示的最佳方法是什么?

线性降维

线性降维算法计算线性组合,将原始特征进行平移、旋转和重新缩放,以捕捉数据中的显著变化,同时受到新特征特性的约束。

主成分分析 (PCA),由卡尔·皮尔逊于 1901 年发明,找到了反映数据中最大方差方向的新特征,同时这些特征相互不相关,或者正交。

独立成分分析 (ICA) 相反,起源于 20 世纪 80 年代的信号处理领域,其目标是在强加统计独立性的更严格约束条件下分离不同的信号。

本节介绍了这两个算法,然后说明了如何将 PCA 应用于资产收益,以从数据中学习风险因素,并构建所谓的特征投资组合来进行系统性交易策略。

主成分分析

PCA 将主成分视为现有特征的线性组合,并使用这些成分来表示原始数据。成分的数量是一个超参数,确定了目标维度,需要等于或小于观测或列的数量,以较小者为准。

PCA 旨在捕捉数据中的大部分方差,以便轻松恢复原始特征,并使每个成分都添加信息。它通过将原始数据投影到主成分空间来降低维度。

PCA 算法通过识别一系列主成分来工作,每个主成分都与数据中最大方差的方向对齐,考虑了先前计算的成分捕获的变化后。顺序优化还确保新成分与现有成分不相关,使得结果集构成向量空间的正交基础。

这个新基础对应于原始基础的旋转版本,使得新轴指向连续减小的方差方向。每个主成分解释的原始数据方差量的下降反映了原始特征之间相关性的程度。

捕获例如 95%的原始变化相对于总特征数的成分数量为原始数据中线性独立信息的洞察提供了一个见解。

在 2D 中可视化 PCA

下面的屏幕截图说明了一个二维随机数据集的 PCA 的几个方面(请参见pca_key_ideas笔记本):

  • 左侧面板显示了第一和第二主成分如何与最大方差的方向对齐,同时又是正交的。

  • 中心面板显示了第一主成分如何最小化重构误差,重构误差定义为数据点与新轴之间的距离之和。

  • 最后,右侧面板说明了监督 OLS,它通过从(单个)特征计算的(一维)超平面近似结果变量(这里我们选择 x[2])。垂直线突出显示 OLS 如何最小化沿结果轴的距离,与 PCA 相反,后者最小化与超平面正交的距离。

PCA 所做的假设

PCA 做出了几个重要的假设需要牢记。这些包括以下内容:

  • 高方差意味着高信噪比

  • 数据被标准化,使得各个特征的方差可比较

  • 线性变换捕捉了数据的相关方面

  • 超过第一和第二时刻的高阶统计量不重要,这意味着数据具有正态分布

对第一和第二时刻的强调与标准风险/回报度量相一致,但正态性假设可能与市场数据的特征相冲突。

PCA 算法的工作原理

该算法找到向量以创建目标维度的超平面,最小化重构误差,重构误差被测量为数据点到平面的平方距离之和。正如上面所示,这个目标对应于找到一个序列的向量,这些向量与给定其他分量的最大保留方差的方向对齐,同时确保所有主要成分都是相互正交的。

在实践中,该算法通过计算协方差矩阵的特征向量或使用奇异值分解来解决问题。

我们使用一个随机生成的三维椭圆来说明计算过程,其中包含 100 个数据点,显示在以下屏幕截图的左侧面板中,包括由前两个主成分定义的二维超平面(参见the_math_behind_pca笔记本中的以下代码示例):

图片

三维椭圆和二维超平面

基于协方差矩阵的 PCA

我们首先使用特征x[i], x[j]的成对样本协方差作为行i和列j的输入来计算主成分:

图片

对于一个n维的方阵M,我们定义如下的特征向量ω[i]和特征值λ[i]i=1, ..., n

图片

因此,我们可以使用特征向量和特征值表示矩阵M,其中W是一个包含特征向量的矩阵,L是一个包含λ[i]作为对角元素(其他元素为 0)的矩阵。我们将特征值分解定义如下:

图片

使用 NumPy,我们按如下方式实现这一点,其中 pandas DataFrame 包含椭圆的 100 个数据点:

# compute covariance matrix:
cov = np.cov(data, rowvar=False) # expects variables in rows by default
cov.shape
(3, 3)

接下来,我们计算协方差矩阵的特征向量和特征值。特征向量包含主成分(符号是任意的):

eigen_values, eigen_vectors = eig(cov)
eigen_vectors
array([[ 0.71409739, -0.66929454, -0.20520656],
[-0.70000234, -0.68597301, -0.1985894 ],
[ 0.00785136, -0.28545725, 0.95835928]])

我们可以将结果与从 sklearn 获得的结果进行比较,发现它们在绝对意义上是匹配的:

pca = PCA()
pca.fit(data)
C = pca.components_.T # columns = principal components
C
array([[ 0.71409739, 0.66929454, 0.20520656],
[-0.70000234, 0.68597301, 0.1985894 ],
[ 0.00785136, 0.28545725, -0.95835928]])
np.allclose(np.abs(C), np.abs(eigen_vectors))
True

我们还可以验证特征值分解,从包含特征值的对角矩阵L开始:

# eigenvalue matrix
ev = np.zeros((3, 3))
np.fill_diagonal(ev, eigen_values)
ev # diagonal matrix
array([[1.92923132, 0\. , 0\. ],
[0\. , 0.55811089, 0\. ],
[0\. , 0\. , 0.00581353]])

我们发现结果确实成立:

decomposition = eigen_vectors.dot(ev).dot(inv(eigen_vectors))
np.allclose(cov, decomposition)

使用奇异值分解的 PCA

接下来,我们将看看使用奇异值分解(SVD)进行的备用计算。当观测次数大于特征数时(典型情况),该算法速度较慢,但在一些特征强相关(通常是使用 PCA 的原因)的情况下,可以获得更好的数值稳定性。

SVD 将我们刚刚应用于方形对称协方差矩阵的特征分解推广到更一般的m x n矩形矩阵。其形式如下图中心所示。 Σ 的对角值是奇异值,V 的转置包含主成分作为列向量:

在这种情况下,我们需要确保我们的数据中心化,均值为零(在计算协方差之前已经处理了这一点):

n_features = data.shape[1]
data_ = data - data.mean(axis=0
Using the centered data, we compute the singular value decomposition:
U, s, Vt = svd(data_)
U.shape, s.shape, Vt.shape
((100, 100), (3,), (3, 3))
We can convert the vector s that only contains the singular values into an nxm matrix and show that the decomposition works:
S = np.zeros_like(data_)
S[:n_features, :n_features] = np.diag(s)
S.shape
(100, 3)

我们发现,分解确实重新生成了标准化数据:

np.allclose(data_, U.dot(S).dot(Vt))
True

最后,我们确认V的转置的列包含主成分:

np.allclose(np.abs(C), np.abs(Vt.T))
True

在下一节中,我们将展示 sklearn 如何实现 PCA。

sklearn 中的 PCA

sklearn.decomposition.PCA的实现遵循基于fit()transform()方法的标准 API,这些方法分别计算所需数量的主成分并将数据投影到组件空间。便捷方法fit_transform()在一步中完成此操作。

PCA 提供了三种不同的算法,可以使用svd_solver参数指定:

  • Full 使用 SciPy 提供的 LAPACK 求解器计算精确的 SVD

  • Arpack 运行适用于计算不到全部组件的截断版本

  • Randomized 使用基于抽样的算法,当数据集具有 500 个以上观测值和特征,并且目标是计算少于 80%的组件时,效率更高

  • Auto 使用随机化算法在效率最高的地方,否则使用完整的 SVD

有关算法实现细节的参考请参见 GitHub 上的参考资料。

PCA 对象的其他关键配置参数如下:

  • n_components:通过传递None(默认)来计算所有主成分,或者限制数量为int。对于svd_solver=full,还有两个额外的选项:在[0,1]区间内的浮点数计算需要保留数据方差的相应份额的组件数量,mle选项使用最大似然估计来估计维度数量。

  • whiten:如果为True,则将组件向量标准化为单位方差,在某些情况下,这可能对在预测模型中使用有用(默认为False)。

要计算三维椭圆的前两个主成分并将数据投影到新空间中,可以使用fit_transform()如下所示:

pca = PCA(n_components=2)
projected_data = pca.fit_transform(data)
projected_data.shape
(100, 2)

前两个成分的解释方差非常接近 100%:

pca2.explained_variance_ratio_
array([0.77381099, 0.22385721])

本节开始处的截图显示了数据投影到新的二维空间中。

独立成分分析

独立成分分析ICA)是另一种线性算法,它确定了一种新的基础,用于表示原始数据,但追求的目标不同于 PCA。

ICA 起源于信号处理,其旨在解决的问题称为盲源分离。通常将其描述为鸡尾酒会问题,即同时有多位嘉宾讲话,以至于单个麦克风会记录重叠的信号。ICA 假设存在与说话者数量相同的不同麦克风,每个麦克风放置在不同的位置,以记录不同混合信号。然后,ICA 旨在从不同的录音中恢复个别信号。

换句话说,有 n 个原始信号和一个未知的方形混合矩阵 A,产生一个 nm 观测值集合,使得:

目标是找到矩阵W=A^(-1),以解开混合信号以恢复源信号。

唯一确定矩阵 W 的能力取决于数据的非高斯分布。否则,W 可以在多元正态分布在旋转下的对称性的情况下任意旋转。

此外,ICA 假设混合信号是其组成部分的和,并且无法识别高斯分量,因为它们的和也是正态分布的。

ICA 假设

ICA 做出以下关键假设:

  • 信号的来源是统计独立的

  • 线性变换足以捕获相关信息

  • 独立分量没有正态分布

  • 混合矩阵 A 可以被反转

ICA 还要求数据被居中和白化;也就是说,彼此不相关且方差为单位。使用 PCA 对数据进行预处理如上所述可以实现所需的转换。

ICA 算法

sklearn 使用的 FastICA 是一种固定点算法,它使用高阶统计量来恢复独立源。特别是,它最大化了每个分量到正态分布的距离,作为独立性的代理。

名为InfoMax的替代算法最小化组件之间的互信息作为统计独立性的度量。

使用 sklearn 进行 ICA

sklearn 的 ICA 实现使用与 PCA 相同的接口,因此没有什么需要额外添加的。请注意,没有解释方差的度量,因为 ICA 不会逐步计算组件。相反,每个组件旨在捕获数据的独立方面。

用于算法交易的 PCA

PCA 在算法交易中有几方面的用途。这些包括将 PCA 应用于资产收益,通过数据驱动的方法导出风险因素,并基于资产收益的相关矩阵的主成分构建不相关的投资组合。

数据驱动的风险因素

在第七章 Linear Models 中,我们探讨了量化金融中用于捕捉回报主要驱动因素的风险因素模型。这些模型根据资产暴露于系统性风险因素的程度以及与这些因素相关的回报来解释资产回报的差异。

特别地,我们探索了法玛-法 rench 方法,该方法根据有关平均收益的经验行为的先验知识指定因素,将这些因素视为可观察的,然后使用线性回归估计风险模型系数。另一种方法将风险因素视为潜在变量,并使用因子分析技术(如 PCA)同时估计因素以及它们如何从历史回报中驱动回报。

在本节中,我们将审查这种方法如何以纯粹的统计或数据驱动方式推导因素,其优势在于不需要对资产回报行为的 ex-ante 知识(有关详细信息,请参见pcarisk_factor 笔记本模型)。

我们将使用 Quandl 股价数据,并选择市值最大的 500 支股票在 2010-18 期间的每日调整收盘价。然后,我们按以下方式计算每日回报:

idx = pd.IndexSlice
with pd.HDFStore('../../data/assets.h5') as store:
stocks = store['us_equities/stocks'].marketcap.nlargest(500)
returns = (store['quandl/wiki/prices']
.loc[idx['2010': '2018', stocks.index], 'adj_close']
.unstack('ticker')
.pct_change())

我们获得了 351 支股票和超过 2000 个交易日的收益:

returns.info()
DatetimeIndex: 2072 entries, 2010-01-04 to 2018-03-27
Columns: 351 entries, A to ZTS

PCA 对异常值很敏感,因此我们将数据在 2.5% 和 97.5% 分位数处进行了 winsorize 处理:

returns = returns.clip(lower=returns.quantile(q=.025),
upper=returns.quantile(q=.975),
axis=1)

PCA 不允许缺失数据,因此我们将删除至少在 95% 的时间段内没有数据的股票,并在第二步中删除至少在剩余股票中至少 95% 的交易日没有观察到的日子:

returns = returns.dropna(thresh=int(returns.shape[0] * .95), axis=1)
returns = returns.dropna(thresh=int(returns.shape[1] * .95))

我们剩下了覆盖类似时间段的 314 个股票收益系列:

returns.info()
DatetimeIndex: 2070 entries, 2010-01-05 to 2018-03-27
Columns: 314 entries, A to ZBH

我们使用给定交易日的平均收益来填补任何剩余的缺失值:

daily_avg = returns.mean(1)
returns = returns.apply(lambda x: x.fillna(daily_avg))

现在我们准备使用默认参数将主成分模型拟合到资产回报上,以使用全 SVD 算法计算所有成分:

pca = PCA()
pca.fit(returns)
PCA(copy=True, iterated_power='auto', n_components=None, random_state=None,
svd_solver='auto', tol=0.0, whiten=False)

我们发现最重要的因素解释了大约 40% 的日收益变化。主要因素通常被解释为市场,而剩余的因素可以被解释为行业或风格因素,符合我们在第五章 Strategy Evaluation 和第七章 Linear Models 中的讨论,具体取决于更近距离观察的结果(见下一个示例)。

右侧的图显示了累积解释方差,并表明大约 10 个因素解释了这一大型股票横截面的 60% 的回报:

笔记本包含对更广泛的股票横截面和更长的 2000-18 时间段的模拟。它发现,平均而言,500 支随机选择的股票的前三个成分解释了 25%,10% 和 5%。

累积图显示了典型的拐点模式,可以帮助确定适当的目标维度,因为它表明附加组件增加的解释价值较少。

我们可以选择前两个主成分来验证它们确实是不相关的:

risk_factors = pd.DataFrame(pca.transform(returns)[:, :2],
columns=['Principal Component 1', 'Principal Component 2'],
index=returns.index)
risk_factors['Principal Component 1'].corr(risk_factors['Principal Component 2'])
7.773256996252084e-15

此外,我们可以绘制时间序列以突出每个因子捕捉不同波动性模式的方式:

风险因子模型将采用主成分的子集作为特征来预测未来的回报,类似于我们在第七章线性模型 - 回归与分类 中的方法。

特征组合

PCA 的另一个应用涉及归一化回报的协方差矩阵。相关矩阵的主要成分以降序捕获资产之间的大部分协变异,并且彼此不相关。此外,我们可以使用标准化的主成分作为投资组合权重。

让我们使用 2010 年至 2018 年期间具有数据的 30 个最大股票来便于表述:

idx = pd.IndexSlice
with pd.HDFStore('../../data/assets.h5') as store:
stocks = store['us_equities/stocks'].marketcap.nlargest(30)
returns = (store['quandl/wiki/prices']
.loc[idx['2010': '2018', stocks.index], 'adj_close']
.unstack('ticker')
.pct_change())

我们再次修剪并对回报进行归一化:

normed_returns = scale(returns
       .clip(lower=returns.quantile(q=.025),
        upper=returns.quantile(q=.975),
        axis=1)
.apply(lambda x: x.sub(x.mean()).div(x.std())))

与前面的例子一样,删除资产和交易日后,我们剩下 23 个资产和超过 2,000 个交易日。我们估计所有主成分,并发现前两个分别解释了 55.9%和 15.5%的协变异:

pca.fit(cov)
pd.Series(pca.explained_variance_ratio_).head()
0 55.91%
1 15.52%
2 5.36%
3 4.85%
4 3.32%

接下来,我们选择并归一化四个最大的组件,使它们总和为1,我们可以将它们用作与由所有股票形成的等权重投资组合进行比较的投资组合的权重:

top4 = pd.DataFrame(pca.components_[:4], columns=cov.columns)
eigen_portfolios = top4.div(top4.sum(1), axis=0)
eigen_portfolios.index = [f'Portfolio {i}' for i in range(1, 5)]

权重显示出明显的重点 - 例如,投资组合 3 重点关注样本中的两个支付处理器 Mastercard 和 Visa,而 投资组合 2 则更多地暴露于科技公司:

将每个投资组合在样本期间的表现与我们的小样本组成的市场进行比较时,我们发现投资组合 1 的表现非常相似,而其他投资组合捕捉到不同的回报模式:

比较每个投资组合的表现

流形学习

线性维度降低将原始数据投影到与数据中的信息方向一致的低维超平面上。专注于线性变换简化了计算,并呼应了常见的财务指标,例如 PCA 旨在捕获最大的方差。

然而,线性方法自然会忽略数据中非线性关系反映的信号。在包含图像或文本数据的替代数据集中,这样的关系非常重要。在探索性分析期间检测到这样的关系可以提供关于数据潜在信号内容的重要线索。

相比之下,流形假设强调高维数据通常位于或接近嵌入在高维空间中的低维非线性流形上。本节开头的屏幕截图中显示的二维瑞士卷就是这样一种拓扑结构的示例。

流形学习旨在找到内在维度的流形,然后在这个子空间中表示数据。一个简化的例子是将道路视为三维空间中的一维流形,并使用房子编号作为局部坐标来标识数据点。

几种技术近似一个较低维度的流形。一个例子是局部线性嵌入LLE),它是由 Sam Roweis 和 Lawrence Saul 于 2000 年开发的,用于展开上一个屏幕截图中的瑞士卷(参见manifold_learning_lle笔记本中的示例)。

对于每个数据点,LLE 确定一定数量的最近邻居,并计算代表每个点的权重,使其表示为其邻居的线性组合。它通过将每个邻域线性投影到较低维度流形上的全局内部坐标上来找到较低维度的嵌入,并且可以被认为是一系列 PCA 应用。

可视化需要降至至少三个维度,可能低于内在维度,并且面临着忠实地表示局部和全局结构的挑战。这个挑战与与维度诅咒相关。虽然球体的体积随着维度数量的增加而呈指数级增长,但用于表示高维数据的较低维度空间却受到了极大的限制。

例如,在 12 维中,可能有 13 个等距离点,但在两个维度中只有三个点可以形成边长相等的三角形。因此,在较低维度准确反映一个点与其高维邻居的距离的情况下,可能会扭曲所有其他点之间的关系。结果就是拥挤问题:为了保持全局距离,局部点可能需要被放置得太近,反之亦然。

接下来的两个部分介绍了一些技术,这些技术在解决复杂数据集的可视化拥挤问题方面取得了进展。我们将使用时尚MNIST数据集,这是经典手写数字 MNIST 基准数据的一个更复杂的替代方案,用于计算机视觉。它包含了 60,000 张训练图像和 10,000 张测试图像,涵盖了 10 个类别的时尚物品(见下面的示例):

这些数据的流形学习算法的目标是检测类别是否位于不同的流形上,以便促进它们的识别和区分。

t-SNE

t-分布随机邻域嵌入是由劳伦斯·范德马滕(Laurens van der Maaten)和杰夫·辛顿(Geoff Hinton)于 2010 年开发的获奖算法,用于检测高维数据中的模式。它采用概率、非线性方法来定位位于几个不同但相关的低维流形上的数据。

该算法强调将低维度中的相似点放在一起,而不是保持高维度中相距较远的点之间的距离,这是由最小化平方距离的算法(如 PCA)产生的结果。

该算法通过将高维距离转换为(条件)概率来进行,其中高概率意味着低距离,并反映了基于相似性对两点进行抽样的可能性。它通过在每个点上定位一个正态分布并计算点和每个邻居的密度来实现这一点,其中困惑度参数控制有效邻居的数量。

在第二步中,它将点排列在低维中,并使用类似计算的低维概率来匹配高维度分布。它通过使用 Kullback-Leibler 散度来衡量分布之间的差异,该散度对于在低维中错误放置相似点施加了很大的惩罚。

低维概率使用具有一个自由度的学生 t 分布,因为它具有更胖的尾部,减少了在高维度中更远的点的错误放置的惩罚,以解决拥挤问题。

以下图表的上面板显示了 t-SNE 如何区分图像类别。更高的困惑度值增加了用于计算局部结构的邻居数量,并逐渐更加强调全局关系:

t-SNE 目前是高维数据可视化的最新技术。缺点包括计算复杂度随着点数n的平方增长,因为它评估所有成对距离,但随后基于树的实现已将成本降低至n log n

t-SNE 不利于将新数据点投影到低维空间中。压缩输出对于基于距离或基于密度的聚类算法并不是一个非常有用的输入,因为 t-SNE 对待小距离和大距离的方式不同。

UMAP

统一流形逼近和投影是用于可视化和一般降维的较新算法。它假设数据在局部连接的流形上均匀分布,并使用模糊拓扑来寻找最接近的低维等效物。它使用一个 neighbors 参数,与上面的困惑度类似地影响结果。

它比 t-SNE 更快,因此在大型数据集上的扩展性更好,并且有时比 t-SNE 更好地保留全局结构。它还可以使用不同的距离函数,包括例如余弦相似度,用于衡量词数向量之间的距离。

前一图的底部一行中的四个图表说明了 UMAP 确实将不同的群集分开得更远,而 t-SNE 则提供了更细粒度的对局部结构的了解。

笔记本还包含每个算法的交互式 Plotly 可视化,允许探索标签并确定彼此靠近的对象。

聚类

聚类和降维都对数据进行总结。正如刚刚详细讨论的那样,降维通过使用捕捉最相关信息的新特征来表示数据,从而压缩数据。相比之下,聚类算法将现有观察结果分配给由相似数据点组成的子组。

聚类可以通过从连续变量学习的类别的角度更好地理解数据。它还允许根据学习的标准自动对新对象进行分类。相关应用的示例包括层次分类法、医学诊断和客户细分。

或者,可以使用集群表示组,例如使用集群的中点作为学习组的最佳代表。示例应用包括图像压缩。

聚类算法在识别分组方面的策略不同:

  • 组合算法选择最一致的观察组合

  • 概率建模估计最可能生成群集的分布

  • 分层聚类找到一系列嵌套群集,优化任何给定阶段的一致性

算法还在于什么构成一个有用的对象集的概念上有所不同,这需要与数据特征、领域和应用目标相匹配。分组类型包括以下内容:

  • 明显分离的各种形状的组

  • 基于原型或基于中心的紧凑群集

  • 密度为基础的任意形状的群集

  • 基于连接性或基于图的群集

聚类算法的重要附加方面包括以下内容:

  • 是否需要独占的群集成员身份

  • 是否进行硬(二进制)或软(概率)分配

  • 是否完整并将所有数据点分配给群集

以下各节介绍了关键算法,包括 k-Means、层次和基于密度的聚类,以及高斯混合模型。clustering_algos 笔记本在不同的标记数据集上比较了这些算法的性能,以突出它们的优缺点。它使用互信息(参见第六章,机器学习过程)来衡量聚类分配和标签的一致性。

k-Means 聚类

k-Means 是最著名的聚类算法,最早由贝尔实验室的 Stuart Lloyd 在 1957 年提出。

该算法找到 K 个质心,并将每个数据点分配给恰好一个聚类,目标是最小化聚类内变异(称为惯性)。通常使用欧几里德距离,但也可以使用其他度量。k-Means 假设聚类是球形的且大小相等,并忽略特征之间的协方差。

该问题在计算上很困难(np-hard),因为有 K^N 种方式将 N 个观察结果划分为 K 个聚类。标准的迭代算法为给定的 K 交付了一个局部最优解,并按以下方式进行:

  1. 随机定义 K 个聚类中心并将点分配给最近的质心。

  2. 重复如下步骤:

    • 对于每个聚类,将质心计算为特征的平均值

    • 将每个观察分配给最近的质心

  3. 收敛:分配(或聚类内变异)不再改变。

kmeans_implementation 笔记本展示了如何使用 Python 编写该算法,并可视化算法的迭代优化。以下截图突出显示了结果质心如何将特征空间划分为称为Voronoi的区域,这些区域划分了聚类:

结果对于给定的初始化是最优的,但是不同的起始位置将产生不同的结果。因此,我们从不同的初始值计算多个聚类,并选择最小化聚类内变异的解决方案。

k-Means 需要连续或独热编码的分类变量。距离度量通常对比例尺敏感,因此标准化特征是必要的,以确保它们具有相等的权重。

k-Means 的优点包括其广泛的适用性,快速收敛以及对大型数据的线性可伸缩性,同时产生大小均匀的聚类。

缺点包括:

  • 需要调整超参数 k

  • 无法保证找到全局最优解

  • 限制性假设,即聚类是球形的且特征不相关

  • 对异常值的敏感性

评估聚类质量

聚类质量度量有助于在替代聚类结果中进行选择。kmeans_evaluation 笔记本说明了以下选项:

  1. k-Means 目标函数建议我们比较惯性或聚类内变异性的演变。

  2. 最初,额外的质心会急剧减少惯性,因为新的聚类提高了整体拟合度。

  3. 一旦找到了合适数量的聚类(假设存在),新的质心减少了集群内的方差,因为它们倾向于分裂自然的分组。

  4. 因此,当 k 均值发现数据的良好聚类表示时,惯性往往会遵循类似于 PCA 的解释方差比的拐点形状的路径,如下面的屏幕截图所示(请参阅实现细节的笔记本):

Silhouette 系数提供了对聚类质量的更详细的图像。它回答了一个问题:最近集群中的点与分配的集群中的点有多远?

为此,它比较了簇内平均距离(a)与最近簇的平均距离(b),并计算了以下分数s

分数可以从-11之间变化,但在实践中负值不太可能出现,因为它们意味着大多数点被分配到错误的聚类中。轮廓分数的一个有用的可视化将每个数据点的值与全局平均值进行比较,因为它突出显示了每个聚类相对于全局配置的一致性。经验法则是避免具有平均样本以下分数的簇。

下面的截图显示了三个和四个聚类的轮廓图节选,前者突出了聚类1的不良拟合,因为对全局轮廓分数的次优贡献,而所有四个聚类都有一些值表现出高于平均水平的分数:

总之,考虑到通常是无监督的性质,有必要变化聚类算法的超参数并评估不同的结果。调整特征的尺度也很重要,特别是当一些特征应该被赋予更高的权重并且因此应该在较大的尺度上进行测量时。

最后,为了验证结果的稳健性,使用数据子集识别是否出现特定模式是一致的。

层次聚类

层次聚类避免了需要指定目标聚类数量的需求,因为它假设数据可以成功地被合并成越来越不相似的簇。它不追求全局目标,而是逐渐决定如何产生一系列从单一簇到由单个数据点组成的簇的嵌套簇。

有两种方法:

  1. 凝聚聚类 自底向上进行,根据相似性顺序合并剩余的两个组

  2. 分裂聚类 自顶向下工作,并顺序地分裂剩余的集群以产生最独特的子群

两个组都生成N-1 个层次级别,并促进在最佳分割数据成同质组的级别上选择聚类。我们将专注于更常见的聚合聚类方法。

聚合聚类算法偏离了个别数据点,并计算包含所有相互距离的相似性矩阵。然后,它经过N-1 步,直到不再存在不同的聚类,并且每次更新相似度矩阵以替换已被新聚类合并的元素,以便矩阵逐渐缩小。

虽然分层聚类没有像 k-Means 那样的超参数,但是聚类之间(而不是个别数据点之间)的不相似度度量对聚类结果有重要影响。选项如下所示:

  • 单链接:两个聚类最近邻之间的距离

  • 完全连接:各个集群成员之间的最大距离

  • 组平均:每组平均值之间的距离

  • 沃德方法:最小化簇内方差

可视化 - 树状图

分层聚类提供了对观察之间相似程度的洞察,因为它不断合并数据。从一个合并到下一个的相似度度量的显著变化表明在此之前存在自然的聚类。

树状图将连续的合并可视化为二叉树,将个别数据点显示为叶子,将最终合并显示为树的根。它还显示了相似度如何从底部向顶部单调减少。因此,通过切割树状图选择聚类是自然的。

以下屏幕截图(有关实现细节,请参阅hierarchical_clustering笔记本)说明了具有四个类别和三个特征的经典鸢尾花数据集的树状图,使用了先前介绍的四种不同距离度量:

它使用科菲尼特相关性系数评估层次聚类的拟合度,该系数比较了点之间的成对距离和在哪个聚类相似度度量中成对合并发生了。系数为 1 意味着更接近的点总是更早合并。

不同的链接方法产生不同外观的树状图,因此我们不能使用此可视化工具跨方法比较结果。此外,沃德方法,它最小化簇内方差,可能不能恰当地反映方差的变化,而是总方差,这可能会误导。相反,其他质量度量,如科菲尼特相关性,或者与总体目标对齐的惯性等度量,可能更合适。

聚类的优势包括:

  • 您无需指定聚类的数量

  • 它通过直观的可视化提供了潜在聚类的洞察

  • 它产生可以用作分类学的聚类层次结构

  • 它可以与 k-Means 结合使用,以减少聚合过程开始时的项目数量

分层聚类的弱点包括:

  • 由于大量相似性矩阵更新而导致的计算和内存成本高昂

  • 它无法达到全局最优,因为所有合并都是最终的

  • 维度诅咒导致噪声大、高维数据困难

基于密度的聚类

基于密度的聚类算法根据与其他集群成员的接近程度来分配集群成员资格。它们追求识别任意形状和大小的密集区域的目标。它们不需要指定一定数量的集群,而是依赖于定义邻域大小和密度阈值的参数(请参阅相关代码示例的density_based_clustering笔记本)。

DBSCAN

具有噪声的基于密度的空间聚类应用DBSCAN)于 1996 年开发,并且由于在理论和实践中受到的关注,在 2014 年 KDD 会议上获得了时间测试奖。

它旨在识别核心和非核心样本,其中前者扩展一个集群,后者是集群的一部分,但没有足够的附近邻居进一步扩展集群。其他样本是异常值,不分配给任何集群。

它使用eps参数表示邻域半径和min_samples表示核心样本所需的成员数量。它是确定性的和排他的,并且在不同密度的集群和高维数据中存在困难。调整参数以满足必要密度可能具有挑战性,尤其是因为它通常不是恒定的。

分层 DBSCAN

分层 DBSCAN 是一个更近期的发展,它假设集群是潜在密度不同的岛屿,以克服刚才提到的 DBSCAN 挑战。它还旨在识别核心和非核心样本。它使用min_cluster_sizemin_samples参数来选择邻域并扩展集群。该算法在多个eps值上进行迭代,并选择最稳定的聚类。

除了识别不同密度的集群外,它还提供了数据的密度和层次结构的见解。

以下屏幕截图显示了 DBSCAN 和 HDBSCAN 如何能够识别非常不同形状的集群:

高斯混合模型

高斯混合模型GMM)是一种生成模型,假设数据是由各种多元正态分布的混合生成的。该算法旨在估计这些分布的均值和协方差矩阵。

它推广了 k-Means 算法:它添加了特征之间的协方差,以便聚类可以是椭圆而不是球体,而质心由每个分布的平均值表示。GMM 算法执行软分配,因为每个点都有可能是任何群集的成员。

期望最大化算法

GMM 使用期望最大化算法来识别高斯分布混合的组成成分。目标是从未标记的数据中学习概率分布参数。

该算法按以下方式迭代进行:

  1. 初始化——假设随机质心(例如,使用 k-Means)

  2. 重复以下步骤,直到收敛(即,分配的更改下降到阈值以下):

    • 期望步骤:软分配——为每个点从每个分布计算概率

    • 最大化步骤:调整正态分布参数以使数据点最有可能

以下屏幕截图显示了 Iris 数据集的 GMM 集群成员概率作为等高线:

分层风险平价

分层风险平价的关键思想是使用协方差矩阵上的分层聚类,以便能够将具有相似相关性的资产组合在一起,并通过在构建投资组合时仅考虑相似资产作为替代品来减少自由度的数量(有关详细信息,请参阅笔记本和hierarchical_risk_parity子文件夹中的 Python 文件)。

第一步是计算代表相关资产接近度并满足距离度量要求的距离矩阵。结果矩阵成为 SciPy 分层聚类函数的输入,该函数使用到目前为止讨论过的几种可用方法计算连续的群集:

def get_distance_matrix(corr):
"""Compute distance matrix from correlation;
0 <= d[i,j] <= 1"""
return np.sqrt((1 - corr) / 2)
distance_matrix = get_distance_matrix(corr)
linkage_matrix = linkage(squareform(distance_matrix), 'single')

linkage_matrix可用作seaborn.clustermap函数的输入,以可视化结果的分层聚类。seaborn显示的树状图显示了基于相对距离合并单个资产和资产集群的方式:

clustergrid = sns.clustermap(distance_matrix,
method='single',
row_linkage=linkage_matrix,
col_linkage=linkage_matrix,
cmap=cmap, center=0)
sorted_idx = clustergrid.dendrogram_row.reordered_ind
sorted_tickers = corr.index[sorted_idx].tolist()

热图

与原始相关矩阵的seaborn.heatmap相比,现在在排序数据中有更多结构(右侧面板)。

使用由聚类算法引起的层次结构排序的票据,HRP 现在继续计算一个自上而下的逆方差分配,根据树下的子群的方差依次调整权重:

def get_cluster_var(cov, cluster_items):
    """Compute variance per cluster"""
    cov_ = cov.loc[cluster_items, cluster_items]  # matrix slice
    w_ = get_inverse_var_pf(cov_)
    return (w_ @ cov_ @ w_).item()

为此,该算法使用二分搜索将群集的方差分配给其元素,基于它们的相对风险性:

def get_hrp_allocation(cov, tickers):
    """Compute top-down HRP weights"""

    weights = pd.Series(1, index=tickers)
    clusters = [tickers]  # initialize one cluster with all assets

    while len(clusters) > 0:
        # run bisectional search:
        clusters = [c[start:stop] for c in clusters
                    for start, stop in ((0, int(len(c) / 2)),
                                        (int(len(c) / 2), len(c)))
                    if len(c) > 1]
        for i in range(0, len(clusters), 2):  # parse in pairs
            cluster0 = clusters[i]
            cluster1 = clusters[i + 1]

            cluster0_var = get_cluster_var(cov, cluster0)
            cluster1_var = get_cluster_var(cov, cluster1)

            weight_scaler = 1 - cluster0_var / (cluster0_var + cluster1_var)
            weights[cluster0] *= weight_scaler
            weights[cluster1] *= 1 - weight_scaler
    return weights

结果的投资组合分配产生总和为1的权重,并反映出相关矩阵中存在的结构(有关详细信息,请参阅笔记本)。

摘要

在本章中,我们探讨了无监督学习方法,它们允许我们从数据中提取有价值的信号,而无需依赖标签提供的结果信息的帮助。

我们看到了如何使用线性降维方法,如 PCA 和 ICA,从数据中提取不相关或独立的组件,这些组件可以作为风险因子或投资组合权重。我们还涵盖了产生复杂替代数据的最先进可视化的高级非线性流形学习技术。

在第二部分中,我们涵盖了几种根据不同假设生成数据驱动分组的聚类方法。这些分组可以很有用,例如,用于构建将风险平价原则应用于已经按层次聚类的资产的投资组合。

在接下来的三章中,我们将学习关于一种替代数据的关键来源的各种机器学习技术,即文本文档的自然语言处理。

第十三章:处理文本数据

这是专门从文本数据中提取算法交易策略信号的三章中的第一章,使用自然语言处理NLP)和机器学习ML)。

文本数据在内容上非常丰富,但格式不结构化,因此需要更多的预处理,以便 ML 算法可以提取潜在信号。关键挑战在于将文本转换为算法可使用的数值格式,同时表达内容的语义或含义。我们将介绍几种捕捉语言细微差别的技术,这些差别对于人类来说很容易理解,因此它们可以成为 ML 算法的输入。

在本章中,我们介绍了基本的特征提取技术,重点放在单个语义单位上;即单词或称为标记的短语。我们将展示如何通过创建文档-术语矩阵将文档表示为标记计数的向量,这反过来又作为文本分类和情感分析的输入。我们还将介绍朴素贝叶斯算法,该算法在此方面很受欢迎。

在接下来的两章中,我们将借助这些技术,并使用诸如主题建模和词向量嵌入等 ML 算法来捕获更广泛上下文中包含的信息。

特别是,在本章中,我们将介绍以下内容:

  • 基本的 NLP 工作流程是什么样的

  • 如何使用spaCyTextBlob构建多语言特征提取管道

  • 如何执行诸如词性标注POS)或命名实体识别之类的 NLP 任务

  • 如何使用文档-术语矩阵将标记转换为数字

  • 如何使用朴素贝叶斯模型对文本进行分类

  • 如何执行情感分析

下一节的代码示例在本章的 GitHub 存储库中,参考文献列在主README文件中。

如何从文本数据中提取特征

鉴于人类使用自然语言进行交流和存储的信息量如此之大,文本数据可能非常有价值——与投资相关的各种数据源范围从正式文件(如公司声明、合同和专利)到新闻、观点和分析研究,甚至包括评论和各种类型的社交媒体帖子和消息。

在线上有大量且多样化的文本数据样本可供探索 NLP 算法的使用,其中许多列在本章的参考文献中。

为了指导我们通过最有效地支持实现此目标的技术和 Python 库的旅程,我们将重点介绍 NLP 挑战,介绍 NLP 工作流程的关键要素,并说明从文本数据到算法交易的 ML 应用。

NLP 的挑战

将非结构化文本转换为机器可读格式需要进行仔细的预处理,以保留数据的有价值的语义方面。人类如何从语言中获取含义,并理解其内容,目前尚不完全清楚,并且通过机器提高语言理解仍然是一个非常活跃的研究领域。

NLP 是具有挑战性的,因为有效地利用文本数据进行 ML 需要对语言的内部运作以及它所指涉的世界有一定的了解。主要挑战包括以下内容:

  • 由于多义性而产生的歧义;即,一个词或短语在不同的上下文中可能有不同的含义(例如,local high-school dropouts cut in half 可能有几种理解方式)。

  • 语言的非标准和不断发展的使用,特别是在社交媒体上。

  • 使用成语,比如扔手巾。

  • 棘手的实体名称,A Bug's Life 在哪里上映?

  • 对世界的了解——玛丽和苏是姐妹还是母亲。

NLP 工作流程

在使用文本数据进行算法交易时,ML 的一个关键目标是从文档中提取信号。文档是来自相关文本数据源的单个样本,例如公司报告、标题或新闻文章或推文。语料库则是文档的集合(复数形式:corpora)。

下图列出了将文档转换为可用于训练监督式机器学习算法的数据集的关键步骤,该算法能够进行可行的预测:

基本技术 提取称为 标记 的文本特征语义单元,并使用语言规则和词典来丰富这些标记的语言和语义注释。词袋模型 (BoW) 使用标记频率来将文档建模为标记向量,这导致了经常用于文本分类的文档-术语矩阵。

高级方法 使用 ML 来优化基本技术提取的特征,并生成更具信息量的文档模型。这些包括反映跨文档使用标记的主题模型和捕获标记使用上下文的词向量模型。

在下一节中,我们将更详细地回顾每个步骤所做的关键决策和相关的权衡,并使用 spaCy 库来说明它们的实现。以下表格总结了 NLP 流水线的关键任务:

特征 描述
分词 将文本分割为单词、标点符号等。
词性标注 将词的类型分配给标记,如动词或名词。
依存句法分析 标记句法标记依赖,如主语 <=> 宾语。
词干提取和词形还原 分配单词的基本形式:was => be, rats => rat。
句子边界检测 查找并分割单独的句子。
命名实体识别 标记现实世界对象,如人、公司和地点。
相似度 评估单词、文本段和文档之间的相似性。

解析和标记文本数据

令牌是在给定文档中出现的字符实例,并且应被视为进一步处理的语义单位。词汇是包含在被认为与进一步处理相关的语料库中的标记集。在以下决策中的一个关键权衡是以更大的词汇量准确反映文本来源,这可能会转化为更多的特征和更高的模型复杂性。

在这方面的基本选择涉及如何处理标点和大写,是否使用拼写校正,以及是否排除非常频繁的所谓停用词(如andthe)作为无意义的噪声。

另一个决策是关于将* n*个单个令牌组成的组(一个单个令牌也称为unigram)作为语义单位包含在内的。2-gram(或双字)的示例是纽约,而纽约市是 3-gram(或三字)。

目标是创建更准确反映文档含义的标记。决策可以依赖于字典或单个和联合使用的相对频率的比较。包括 n-gram 将增加特征的数量,因为唯一 n-gram 的数量往往比唯一 unigram 的数量高得多,并且可能会增加噪音,除非按频率的显著性进行过滤。

语言标注

语言标注包括应用句法和语法规则来确定句子的边界,尽管标点符号不明确,并且令牌在句子中的角色用于 POS 标注和依赖解析。它还允许识别词干和词形归并的常见根形式以组合相关词汇:

  • POS 标注: 它有助于根据它们的功能消除标记的歧义(当动词和名词具有相同的形式时可能是必要的),这会增加词汇量但可能导致更好的准确性。

  • 依赖解析: 它识别标记之间的层次关系,通常用于翻译,并且对于需要更高级语言理解的交互应用程序(如聊天机器人)非常重要。

  • 词干提取: 它使用简单的规则从标记中去除常见的结尾,例如 slyinged,并将其减少到其词干或根形式。

  • 词形归并: 它使用更复杂的规则来推导单词的规范根(词形)。它可以检测到不规则的根,例如 better 和 best,并且更有效地压缩词汇,但速度比词干提取慢。这两种方法都通过简化词汇以牺牲语义细微差别为代价。

语义标注

命名实体识别NER)旨在识别代表感兴趣对象的标记,例如人物、国家或公司。它可以进一步发展为捕获这些实体之间的语义和层次关系的知识图。它是那些旨在预测新闻事件或情感影响的应用程序的关键要素。

标签

许多 NLP 应用程序从文本中提取的有意义信息中学习预测结果。监督学习需要标签来教会算法真实的输入-输出关系。对于文本数据,建立这种关系可能具有挑战性,可能需要明确的数据建模和收集。

数据建模决策包括如何量化文本文档中隐含的情感,例如电子邮件、转录的采访或推文,或者将研究文档或新闻报道的哪些方面分配给特定结果。

使用案例

利用 ML 和文本数据进行算法交易依赖于以直接或间接预测未来价格变动的特征形式提取有意义信息。应用范围从利用新闻的短期市场影响到资产估值驱动因素的长期基本分析。示例包括以下内容:

  • 评估产品评论情绪以评估公司的竞争地位或行业趋势

  • 检测信用合同中的异常以预测违约的概率或影响

  • 新闻影响的预测,包括方向、幅度和受影响的实体

摩根大通(JP Morgan)例如,开发了一个基于 25 万份分析师报告的预测模型,该模型胜过了几个基准指数,并产生了与基于共识 EPS 和推荐变化形成的情绪因素不相关的信号。

从文本到标记 - NLP 管道

在本节中,我们将演示如何使用开源 Python 库spaCy构建 NLP 管道。textacy库构建在spaCy之上,并提供对spaCy属性和附加功能的简便访问。

有关以下代码示例、安装说明和额外详细信息,请参考nlp_pipeline_with_spaCy笔记本。

spaCy 和 textacy 的 NLP 管道

spaCy是一个广泛使用的 Python 库,具有多语言快速文本处理的全面功能集。标记化和注释引擎的使用需要安装语言模型。本章中我们将使用的特征仅需要小型模型;较大的模型还包括我们将在第十五章中介绍的词向量,Word Embeddings

安装并链接后,我们可以实例化一个 spaCy 语言模型,然后在文档上调用它。因此,spaCy 会产生一个 doc 对象,该对象对文本进行标记化并根据可配置的管道组件进行处理,默认情况下包括标签器、解析器和命名实体识别器:

nlp = spacy.load('en')
nlp.pipe_names
['tagger', 'parser', 'ner']

让我们使用一个简单的句子说明流程:

sample_text = 'Apple is looking at buying U.K. startup for $1 billion'
doc = nlp(sample_text)

解析、标记和注释句子

解析后的文档内容可迭代,每个元素由处理流程产生的多个属性组成。以下示例说明了如何访问以下属性:

  • .text: 原始词文本

  • .lemma_: 词根

  • .pos_: 基本词性标签

  • .tag_: 详细的词性标签

  • .dep_: 标记之间的句法关系或依赖关系

  • .shape_: 词的形状,即大写、标点或数字的使用情况

  • .is alpha: 检查标记是否是字母数字字符

  • .is stop: 检查标记是否在给定语言的常用词列表中

我们遍历每个标记,并将其属性分配给 pd.DataFrame

pd.DataFrame([[t.text, t.lemma_, t.pos_, t.tag_, t.dep_, t.shape_, t.is_alpha, t.is_stop] for t in doc],
             columns=['text', 'lemma', 'pos', 'tag', 'dep', 'shape', 'is_alpha', 'is_stop'])

产生以下输出:

文本 词根 词性 标签 依存关系 形状 是字母 是停用词
苹果 苹果 PROPN NNP nsubj Xxxxx TRUE FALSE
VERB VBZ 辅助动词 xx TRUE TRUE
寻找 寻找 VERB VBG ROOT xxxx TRUE FALSE
ADP IN prep xx TRUE TRUE
购买 购买 VERB VBG pcomp xxxx TRUE FALSE
英国 英国 PROPN NNP compound X.X. FALSE FALSE
创业公司 创业公司 NOUN NN dobj xxxx TRUE FALSE
for for ADP IN prep xxx TRUE TRUE
$ | $ SYM $ | 量词修饰符 | $ FALSE FALSE
1 1 NUM CD compound d FALSE FALSE
十亿 十亿 NUM CD pobj xxxx TRUE FALSE

我们可以使用以下方法在浏览器或笔记本中可视化句法依存:

displacy.render(doc, style='dep', options=options, jupyter=True)

结果是一个依存树:

依存树

我们可以使用 spacy.explain() 获取属性含义的其他见解,如下所示:

spacy.explain("VBZ")
verb, 3rd person singular present

批处理文档

现在我们将阅读一组更大的 2,225 篇 BBC 新闻文章(有关数据来源的详细信息,请参阅 GitHub),这些文章属于五个类别,并存储在单独的文本文件中。我们需要执行以下操作:

  1. 调用 pathlib 的 Path 对象的 .glob() 方法。

  2. 迭代结果路径列表。

  3. 读取新闻文章的所有行,不包括第一行的标题。

  4. 将清理后的结果附加到列表中:

files = Path('..', 'data', 'bbc').glob('**/*.txt')
bbc_articles = []
for i, file in enumerate(files):
    _, _, _, topic, file_name = file.parts
    with file.open(encoding='latin1') as f:
        lines = f.readlines()
        body = ' '.join([l.strip() for l in lines[1:]]).strip()
        bbc_articles.append(body)
len(bbc_articles)
2225

句子边界检测

我们将通过调用 NLP 对象来说明句子检测,对第一篇文章进行批处理:

doc = nlp(bbc_articles[0])
type(doc)
spacy.tokens.doc.Doc

spaCy 从句法分析树中计算句子边界,因此标点符号和大写字母起着重要但不决定性的作用。因此,边界将与从句边界重合,即使是标点符号不规范的文本也是如此。

我们可以使用.sents属性访问解析后的句子:

sentences = [s for s in doc.sents]
sentences[:3]
[Voting is under way for the annual Bloggies which recognize the best web blogs - online spaces where people publish their thoughts - of the year. ,
Nominations were announced on Sunday, but traffic to the official site was so heavy that the website was temporarily closed because of too many visitors.,
Weblogs have been nominated in 30 categories, from the top regional blog, to the best-kept-secret blog.]

命名实体识别

spaCy 使用 .ent_type_ 属性 实现命名实体识别:

for t in sentences[0]:
    if t.ent_type_:
        print('{} | {} | {}'.format(t.text, t.ent_type_, spacy.explain(t.ent_type_)))
annual | DATE | Absolute or relative dates or periods
the | DATE | Absolute or relative dates or periods
year | DATE | Absolute or relative dates or periods

textacy 简化了访问第一篇文章中出现的命名实体的过程:

from textacy.extract import named_entities
entities = [e.text for e in named_entities(doc)]
pd.Series(entities).value_counts()
year                          4
US                            2
South-East Asia Earthquake    2
annual                        2
Tsunami Blog                  2

N-grams

N-gram 结合 N 个连续的标记。在 BoW 模型中,N-gram 可以很有用,因为根据文本环境,将像数据科学家这样的内容视为一个单独的标记可能比将其视为两个不同的标记更有意义。

textacy 方便查看至少出现 min_freq 次的给定长度 nngrams

from textacy.extract import ngrams
pd.Series([n.text for n in ngrams(doc, n=2, min_freq=2)]).value_counts()
East Asia          2
Asia Earthquake    2
Tsunami Blog       2
annual Bloggies    2

spaCy 的流式 API

要通过处理管道传递更多文档,我们可以使用 spaCy 的流式 API 如下:

iter_texts = (bbc_articles[i] for i in range(len(bbc_articles)))
for i, doc in enumerate(nlp.pipe(iter_texts, batch_size=50, n_threads=8)):
      assert doc.is_parsed

多语言自然语言处理

spaCy 包括针对英语、德语、西班牙语、葡萄牙语、法语、意大利语和荷兰语的训练语言模型,以及用于 NER 的多语言模型。跨语言使用很简单,因为 API 不会改变。

我们将使用 TED Talk 字幕的平行语料库来说明西班牙语言模型(请参阅数据来源参考的 GitHub 存储库)。为此,我们实例化了两个语言模型:

model = {}
for language in ['en', 'es']:
    model[language] = spacy.load(language)

然后我们在每个模型中读取相应的小文本样本:

text = {}
path = Path('../data/TED')
for language in ['en', 'es']:
    file_name = path / 'TED2013_sample.{}'.format(language)
    text[language] = file_name.read_text()

句子边界检测使用相同的逻辑,但找到了不同的分解:

parsed, sentences = {}, {}
for language in ['en', 'es']:
    parsed[language] = modellanguage
    sentences[language] = list(parsed[language].sents)
print('Sentences:', language, len(sentences[language]))
Sentences: en 19
Sentences: es 22

POS 标记也是同样工作的:

pos = {}
for language in ['en', 'es']:
    pos[language] = pd.DataFrame([[t.text, t.pos_, spacy.explain(t.pos_)] for t in sentences[language][0]],
    columns=['Token', 'POS Tag', 'Meaning'])
pd.concat([pos['en'], pos['es']], axis=1).head()

结果是英文和西班牙文档的并排标记注释:

标记 POS 标记 含义 标记 POS 标记 含义
那里 ADV 副词 存在 VERB 动词
s VERB 动词 一个 DET 定冠词
一个 DET 定冠词 狭窄的 ADJ 形容词
紧密的 ADJ 形容词 CONJ 连词
CCONJ 并列连词 令人惊讶的 ADJ 形容词

下一节介绍如何使用解析和注释的标记构建文档-术语矩阵,该矩阵可用于文本分类。

使用 TextBlob 进行自然语言处理

TextBlob 是一个提供简单 API 用于常见 NLP 任务的 Python 库,它基于 自然语言工具包 (NLTK) 和 Pattern 网络挖掘库。TextBlob 简化了 POS 标记、名词短语提取、情感分析、分类、翻译等任务。

为了说明使用 TextBlob,我们从 BBC 体育文章中抽样一篇标题为 Robinson ready for difficult task 的文章。与 spaCy 和其他库类似,第一步是通过 TextBlob 对象表示的管道将文档传递,以分配各种任务所需的注释(请参阅此部分的 nlp_with_textblob 笔记本):

from textblob import TextBlob
article = docs.sample(1).squeeze()
parsed_body = TextBlob(article.body)

词干提取

要执行词干提取,我们从 nltk 库实例化了 SnowballStemmer,在每个标记上调用其 .stem() 方法,并显示修改后的标记:

from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer('english')
[(word, stemmer.stem(word)) for i, word in enumerate(parsed_body.words)
    if word.lower() != stemmer.stem(parsed_body.words[i])]
('Andy', 'andi'),
('faces', 'face'),
('tenure', 'tenur'),
('tries', 'tri'),
('winning', 'win'),

情感极性和主观性

TextBlob 使用 Pattern 库提供的字典为解析的文档提供极性和主观性估计。这些字典将产品评论中经常出现的形容词映射到从 -1 到 +1(负面 ↔ 正面)的情感极性分数和类似的主观性分数(客观 ↔ 主观)。

.sentiment 属性为每个相关标记提供平均值,而 .sentiment_assessments 属性列出了每个标记的基础值(请参见笔记本):

parsed_body.sentiment
Sentiment(polarity=0.088031914893617, subjectivity=0.46456433637284694)

从标记到数字 - 文档-术语矩阵

在本节中,我们首先介绍 BoW 模型如何将文本数据转换为允许使用距离比较文档的数字向量空间表示法。然后,我们继续说明如何使用 sklearn 库创建文档-术语矩阵。

BoW 模型

BoW 模型根据文档中包含的词或标记的频率来表示文档。每个文档都成为一个向量,每个向量在词汇表中都有一个条目,反映了该标记与文档的相关性。

给定词汇表,文档-术语矩阵很容易计算。然而,它也是一个粗略的简化,因为它抽象了单词顺序和语法关系。尽管如此,它通常很快就能在文本分类中取得良好的结果,因此是一个非常有用的起点。

下图(右边的那个)说明了该文档模型如何将文本数据转换为具有数字条目的矩阵,其中每行对应于一个文档,每列对应于词汇表中的一个标记。生成的矩阵通常是非常高维且稀疏的;也就是说,它包含许多零条目,因为大多数文档只包含总词汇表的一小部分:

![

结果矩阵

有几种方法可以衡量标记的向量条目以捕捉其与文档的相关性。我们将说明如何使用 sklearn 使用二进制标志,这些标志指示存在或不存在、计数以及加权计数,这些加权考虑了语料库中所有文档中的词频的差异:

测量文档的相似性

将文档表示为词向量将每个文档分配到由词汇表创建的向量空间中的位置。在该空间中将向量条目解释为笛卡尔坐标,我们可以使用两个向量之间的角度来衡量它们的相似性,因为指向相同方向的向量包含相同的词及相同的频率权重。

前面的图表(右边的那个)简化了二维中表示的文档向量 d[1] 与查询向量(搜索词组或另一个文档) q 之间距离的计算。

余弦相似度等于两个向量之间的夹角的余弦。它将角度大小转换为一个范围为 [0, 1] 的数字,因为所有向量条目都是非负的标记权重。值为 1 意味着两个文档在其标记权重方面是相同的,而值为 0 意味着两个文档只包含不同的标记。

如图所示,角的余弦等于向量的点积;也就是说,它们的坐标的和乘积,除以它们的长度的乘积,由每个向量的欧几里得范数测量。

使用 sklearn 创建文档-术语矩阵

scikit-learn 预处理模块提供了两个工具来创建文档-术语矩阵。CountVectorizer 使用二进制或绝对计数来衡量每个文档 d 和标记 t词频 tf(d, t)

相反,TfidFVectorizer 通过逆文档频率idf)对(绝对)词频进行加权。因此,出现在更多文档中的术语将获得比在给定文档中具有相同频率但在所有文档中出现频率较低的标记更低的权重。具体来说,使用默认设置,对于文档-术语矩阵的 tf-idf(d, t) 条目计算为 tf-idf(d, t) = tf(d, t) x idf(t)

这里 n[d] 是文档数,df(d, t) 是术语 t 的文档频率。每个文档的结果 tf-idf 向量相对于它们的绝对或平方总和进行了归一化(有关详细信息,请参阅 sklearn 文档)。tf-idf 度量最初用于信息检索以排名搜索引擎结果,并且随后被证明对于文本分类或聚类非常有用。

这两个工具使用相同的接口,并在向量化文本之前对文档列表执行标记化和进一步的可选预处理,以生成标记计数来填充文档-术语矩阵。

影响词汇量大小的关键参数包括以下内容:

  • stop_words:使用内置或提供要排除的(常见)单词列表

  • ngram_range:在由 (n[min], n[max]) 元组定义的 n 范围内包含 n-gram

  • lowercase:相应地转换字符(默认为 True

  • min_df / max_df:忽略在较少/较多(int)或更小/更大(如果是 float [0.0,1.0])的文档中出现的单词

  • max_features:相应地限制词汇表中的标记数

  • binary:将非零计数设置为 1 True

有关以下代码示例和额外详细信息,请参阅 document_term_matrix 笔记本。我们再次使用 2,225 篇 BBC 新闻文章进行说明。

使用 CountVectorizer

笔记本包含一个交互式可视化,探索min_dfmax_df设置对词汇量大小的影响。我们将文章读入 DataFrame,将CountVectorizer设置为生成二进制标志并使用所有标记,并调用其.fit_transform()方法生成文档-词矩阵:

binary_vectorizer = CountVectorizer(max_df=1.0,
                                    min_df=1,
                                    binary=True)

binary_dtm = binary_vectorizer.fit_transform(docs.body)
<2225x29275 sparse matrix of type '<class 'numpy.int64'>'
   with 445870 stored elements in Compressed Sparse Row format>

输出是一个scipy.sparse矩阵,以行格式有效地存储了445870个非零条目中的小部分(<0.7%),其中有2225个(文档)行和29275个(标记)列。

可视化词汇分布

可视化显示,要求标记在至少 1%且少于 50%的文档中出现将词汇限制在几乎 30,000 个标记的约 10%左右。

这留下了每个文档略多于 100 个独特标记的模式(左面板),右面板显示了剩余标记的文档频率直方图:

文档/词频分布

查找最相似的文档

CountVectorizer的结果让我们可以使用scipy.spatial.distance模块提供的pdist()函数来找到最相似的文档。它返回一个压缩的距离矩阵,其中的条目对应于方阵的上三角形。我们使用np.triu_indices()将最小距离的索引转换为相应于最接近的标记向量的行和列索引:

m = binary_dtm.todense() # pdist does not accept sparse format
pairwise_distances = pdist(m, metric='cosine')
closest = np.argmin(pairwise_distances) # index that minimizes distance
rows, cols = np.triu_indices(n_docs) # get row-col indices
rows[closest], cols[closest]
(11, 75)

文章编号1175在余弦相似性上最接近,因为它们共享了 58 个标记(请参阅笔记本):

主题 技术 技术
标题 在您工作时观看的软件 打败拨号诈骗者的 BT 程序
正文 软件不仅可以监视 PC 上执行的每个按键和操作,而且还可以作为法律约束证据使用。担心网络犯罪和破坏已促使许多雇主考虑监视员工。 BT 正在推出两项计划,以帮助打败流氓拨号诈骗,这可能会使拨号上网用户损失数千。从五月开始,拨号上网用户将能够下载免费软件,以阻止计算机使用不在用户预先批准名单上的号码。

CountVectorizerTfidFVectorizer都可以与spaCy一起使用;例如,执行词形还原并在标记化过程中排除某些字符,我们使用以下命令:

nlp = spacy.load('en')
def tokenizer(doc):
    return [w.lemma_ for w in nlp(doc) 
                if not w.is_punct | w.is_space]
vectorizer = CountVectorizer(tokenizer=tokenizer, binary=True)
doc_term_matrix = vectorizer.fit_transform(docs.body)

请查看笔记本以获取更多详细信息和更多示例。

TfidFTransformer 和 TfidFVectorizer

TfidfTransfomer从文档-词矩阵中的标记计数计算 tf-idf 权重,例如由CountVectorizer生成的矩阵。

TfidfVectorizer一次执行两个计算。它为CountVectorizerAPI 添加了一些控制平滑行为的参数。

对于一个小的文本样本,TFIDF 计算的工作方式如下:

sample_docs = ['call you tomorrow',
               'Call me a taxi',
               'please call me... PLEASE!']

我们像刚才一样计算词频:

vectorizer = CountVectorizer()
tf_dtm = vectorizer.fit_transform(sample_docs).todense()
tokens = vectorizer.get_feature_names()
term_frequency = pd.DataFrame(data=tf_dtm,
                             columns=tokens)

  call  me  please  taxi  tomorrow  you
0     1   0       0     0         1    1
1     1   1       0     1         0    0
2     1   1       2     0         0    0

文档频率是包含该标记的文档数量:

vectorizer = CountVectorizer(binary=True)
df_dtm = vectorizer.fit_transform(sample_docs).todense().sum(axis=0)
document_frequency = pd.DataFrame(data=df_dtm,
                                  columns=tokens)
   call  me  please  taxi  tomorrow  you
0     3   2       1     1         1    1

tf-idf 权重是这些值的比率:

tfidf = pd.DataFrame(data=tf_dtm/df_dtm, columns=tokens)
   call   me  please  taxi  tomorrow  you
0  0.33 0.00    0.00  0.00      1.00 1.00
1  0.33 0.50    0.00  1.00      0.00 0.00
2  0.33 0.50    2.00  0.00      0.00 0.00

平滑的效果

为了避免零除法,TfidfVectorizer对文档和词项频率使用平滑处理:

  • smooth_idf:将文档频率加1,就像额外的文档包含词汇表中的每个标记一样,以防止零除法。

  • sublinear_tf:应用次线性tf缩放;换句话说,用1 + log(tf)替换tf

与标准化权重结合使用时,结果略有不同:

vect = TfidfVectorizer(smooth_idf=True,
                      norm='l2',  # squared weights sum to 1 by 
                                    document
                      sublinear_tf=False,  # if True, use 1+log(tf)
                      binary=False)
pd.DataFrame(vect.fit_transform(sample_docs).todense(),
            columns=vect.get_feature_names())

   call   me  please  taxi  tomorrow  you
0  0.39 0.00    0.00  0.00      0.65 0.65
1  0.43 0.55    0.00  0.72      0.00 0.00
2  0.27 0.34    0.90  0.00      0.00 0.00

如何使用 TfidFVectorizer 总结新闻文章

由于它们能够分配有意义的标记权重,TFIDF 向量也用于总结文本数据。例如,Reddit 的autotldr功能基于类似的算法。请参阅笔记本,了解使用 BBC 文章的示例。

文本预处理 - 回顾

我们在本节介绍的大量处理自然语言以在机器学习模型中使用的技术是必要的,以应对这种高度非结构化数据源的复杂性质。设计良好的语言特征工程既具有挑战性又具有回报,可以说是解锁文本数据中隐藏的语义价值的最重要的一步。

在实践中,经验有助于我们选择去除噪声而不是信号的转换,但可能仍然需要交叉验证和比较不同预处理选择组合的性能。

文本分类和情感分析

一旦文本数据使用前面讨论的自然语言处理技术转换为数值特征,文本分类就像任何其他分类任务一样。

在本节中,我们将这些预处理技术应用于新闻文章、产品评论和 Twitter 数据,并教您各种分类器,以预测离散新闻类别、评论分数和情感极性。

首先,我们将介绍朴素贝叶斯模型,这是一种概率分类算法,适用于袋装词模型产生的文本特征。

本节的代码示例在text_classification笔记本中。

朴素贝叶斯分类器

朴素贝叶斯算法非常受欢迎,用于文本分类,因为低计算成本和内存需求便于在非常大的、高维的数据集上进行训练。它的预测性能可以与更复杂的模型竞争,提供了一个良好的基准,并以成功的垃圾邮件检测而闻名。

该模型依赖于贝叶斯定理(参见第九章,贝叶斯机器学习)和各种特征独立于结果类别的假设。换句话说,对于给定的结果,知道一个特征的值(例如文档中标记的存在)不会提供有关另一个特征值的任何信息。

贝叶斯定理复习

贝叶斯定理表达了一个事件的条件概率(例如,一封电子邮件是垃圾邮件而不是良性的垃圾邮件)给定另一个事件(例如,电子邮件包含某些词)如下:

后验概率,即一封电子邮件实际上是垃圾邮件的概率,给定它包含某些词,取决于三个因素的相互作用:

  • 一封电子邮件是垃圾邮件的先验概率

  • 在垃圾邮件中遇到这些词的似然性

  • 证据;即在电子邮件中看到这些词的概率

为了计算后验概率,我们可以忽略证据,因为它对所有结果(垃圾邮件与非垃圾邮件)都是相同的,而且无条件先验可能很容易计算。

然而,对于一个相当大的词汇表和一个实际的电子邮件语料库,似然性提出了不可逾越的挑战。原因在于单词的组合爆炸,这些单词在不同文档中是否共同出现,从而阻止了计算概率表并为似然性赋值所需的评估。

条件独立性假设

使得模型既可行又有理由称其为朴素的假设是,特征在结果条件下是独立的。为了说明,让我们对一个包含三个词Send money now的电子邮件进行分类,这样贝叶斯定理就变成了以下形式:

形式上,三个词条件独立的假设意味着观察到send的概率不受其他词存在的影响,假设邮件是垃圾邮件;换句话说,P(send | money, now, spam) = P(send | spam)。因此,我们可以简化似然函数:

使用朴素条件独立性假设,分子中的每个项都可以直接从训练数据的相对频率中计算得出。分母在类别间是常数,当需要比较后验概率而不是校准时可以忽略。先验概率在因素数量增加(即特征)时变得不那么相关。

总之,朴素贝叶斯模型的优势在于训练和预测速度快,因为参数数量与特征数量成线性关系,它们的估计具有封闭形式的解(基于训练数据频率),而不是昂贵的迭代优化。它还直观且具有一定的可解释性,不需要超参数调整,并且在有足够信号的情况下相对不太受无关特征的影响。

然而,当独立假设不成立,文本分类依赖于特征的组合或特征相关时,模型的表现会很差。

新闻文章分类

我们从以前阅读的 BBC 文章中使用朴素贝叶斯模型对新闻文章进行分类的示例开始,以获得一个包含来自五个类别的 2,225 篇文章的 DataFrame:

RangeIndex: 2225 entries, 0 to 2224
Data columns (total 3 columns):
topic 2225 non-null object
heading 2225 non-null object
body 2225 non-null object

训练和评估多项式朴素贝叶斯分类器

我们将数据拆分为默认的 75:25 训练-测试集,确保测试集类别与训练集类别密切相似:

y = pd.factorize(docs.topic)[0] # create integer class values
X = docs.body
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1, stratify=y)

我们继续从训练集中学习词汇,并使用默认设置的CountVectorizer转换两个数据集,以获得近 26,000 个特征:

vectorizer = CountVectorizer()
X_train_dtm = vectorizer.fit_transform(X_train)
X_test_dtm = vectorizer.transform(X_test)
X_train_dtm.shape, X_test_dtm.shape
((1668, 25919), (557, 25919))

训练和预测遵循标准的sklearn拟合/预测接口:

nb = MultinomialNB()
nb.fit(X_train_dtm, y_train)
y_pred_class = nb.predict(X_test_dtm)

我们使用准确性评估多类别预测,并发现默认分类器达到了近 98% 的准确率:

accuracy_score(y_test, y_pred_class)
0.97666068222621

情感分析

情感分析是自然语言处理和机器学习在交易中最受欢迎的用途之一,因为对资产或其他价格驱动因素的正面或负面观点可能会影响收益。

通常,情感分析的建模方法依赖于词典,例如TextBlob库,或者在特定领域的结果上训练的模型。后者更可取,因为它允许更有针对性的标记;例如,将文本特征与随后的价格变化联系起来,而不是间接的情感分数。

我们将使用具有二进制极性标签的 Twitter 数据集和具有五点结果量表的大型 Yelp 商业评论数据集说明情感分析的机器学习。

Twitter 数据

我们使用的数据集包含自 2009 年以来的 1.6 百万条训练推文和 350 条测试推文,这些推文具有经过算法分配的二进制正面和负面情感分数,分布相对均匀(有关更详细的数据探索,请参阅相关笔记本)。

多项式朴素贝叶斯

我们按照以下方式创建一个包含 934 个标记的文档-术语矩阵:

vectorizer = CountVectorizer(min_df=.001, max_df=.8, stop_words='english')
train_dtm = vectorizer.fit_transform(train.text)
<1566668x934 sparse matrix of type '<class 'numpy.int64'>'
   with 6332930 stored elements in Compressed Sparse Row format>

然后我们像以前一样训练MultinomialNB分类器,并预测测试集:

nb = MultinomialNB()
nb.fit(train_dtm, train.polarity)
predicted_polarity = nb.predict(test_dtm)

结果达到了超过 77.5% 的准确率:

accuracy_score(test.polarity, y_pred_class)
0.7768361581920904

与 TextBlob 情感分数的比较

我们还获取了推文的TextBlob情感分数,并注意到(请参见以下左侧图表),正面测试推文获得了明显更高的情感估计。然后我们使用MultinomialNB模型和.predict_proba()方法来计算预测概率,并使用相应的曲线下面积(请参见以下右侧图表)比较两个模型:

TextBlob 情感分数

在这种情况下,朴素贝叶斯模型优于TextBlob

商业评论 - Yelp 数据集挑战

最后,我们将情感分析应用于规模更大的 Yelp 商业评论数据集,其中包含五个结果类别。数据由多个文件组成,其中包含关于企业、用户、评论和 Yelp 鼓励数据科学创新的其他方面的信息。

我们将使用在 2010 年至 2018 年期间产生的约六百万条评论(详见相关笔记本)。以下图表显示每年的评论数量和平均星级:

表示每年评论数量和平均星级的图表

除了评论文本产生的文本特征外,我们还将使用提交的评论或关于用户的其他信息。

我们将在 2017 年之前的数据上训练各种模型,并使用 2018 年作为测试集。

基准准确率

使用最常见的星级数(=5)来预测测试集,我们实现了接近 52%的准确率:

test['predicted'] = train.stars.mode().iloc[0]
accuracy_score(test.stars, test.predicted)
0.5196950594793454

多项式朴素贝叶斯模型

接下来,我们使用具有默认设置的CountVectorizer生成的文档-术语矩阵来训练朴素贝叶斯分类器:

nb = MultinomialNB()
nb.fit(train_dtm,train.stars)
predicted_stars = nb.predict(test_dtm)

预测在测试集上产生了 64.7%的准确率,比基准提高了 24.4%:

accuracy_score(test.stars, predicted_stars)
0.6465164206691094

一对多逻辑回归

我们继续训练一对多逻辑回归,该模型对每个类别训练一个模型,同时将其余类别视为负类,并使用不同的模型预测每个类别的概率。

仅使用文本特征,我们按以下方式训练和评估模型:

logreg = LogisticRegression(C=1e9)
logreg.fit(X=train_dtm, y=train.stars)
y_pred_class = logreg.predict(test_dtm)

模型的准确率显著提高至 73.6%:

accuracy_score(test.stars, y_pred_class)
0.7360498864740219

结合文本和数值特征

数据集包含各种数值特征(有关实施细节,请参阅相关笔记本)。

向量化器生成scipy.sparse矩阵。要将向量化的文本数据与其他特征结合起来,我们首先需要将其转换为稀疏矩阵;许多 sklearn 对象和其他库(如 LightGBM)可以处理这些非常节省内存的数据结构。将稀疏矩阵转换为密集的 NumPy 数组会导致内存溢出的风险。

大多数变量都是分类变量,因此我们使用一位有效编码(one-hot encoding),因为我们有一个相当大的数据集来容纳特征的增加。

我们将编码的数值特征转换并与文档-术语矩阵相结合:

train_numeric = sparse.csr_matrix(train_dummies.astype(np.int8))
train_dtm_numeric = sparse.hstack((train_dtm, train_numeric))

多项式逻辑回归

逻辑回归还提供了一种多项式训练选项,比一对多实现更快且更准确。我们使用lbfgs求解器(有关详细信息,请参阅 GitHub 上链接的 sklearn 文档):

multi_logreg = LogisticRegression(C=1e9, multi_class='multinomial', 
                                  solver='lbfgs')
multi_logreg.fit(train_dtm_numeric.astype(float), train.stars)
y_pred_class = multi_logreg.predict(test_dtm_numeric.astype(float))

此模型将性能提高至 74.6%的准确率:

accuracy_score(test.stars, y_pred_class)
0.7464488070176475

在这种情况下,调整正则化参数C并没有带来非常显著的改善(请参阅笔记本)。

梯度提升机

为了说明目的,我们还训练了一个具有默认设置和multiclass目标的 LightGBM 梯度提升树集成:

param = {'objective':'multiclass', 'num_class': 5}
booster = lgb.train(params=param,
                    train_set=lgb_train,
                    num_boost_round=500,
                    early_stopping_rounds=20,
                    valid_sets=[lgb_train, lgb_test])

基本设置并没有改善多项式逻辑回归,但进一步的参数调整仍然是一个未使用的选项:

y_pred_class = booster.predict(test_dtm_numeric.astype(float))
accuracy_score(test.stars, y_pred_class.argmax(1) + 1)
0.738665855696524

摘要

在本章中,我们探讨了许多技术和选项来处理非结构化数据,目的是提取语义上有意义的数字特征,以供机器学习模型使用。

我们介绍了基本的标记化和注释流程,并使用 spaCy 和 TextBlob 在多种语言中说明了其实现方式。我们在这些结果的基础上构建了一个基于词袋模型的文档模型,以将文档表示为数值向量。我们学习了如何优化预处理流程,然后使用向量化的文本数据进行分类和情感分析。

在剩下的两章中,我们将学习如何使用无监督学习来总结文本,以识别潜在主题(在下一章中),并研究将单词表示为反映单词使用上下文的向量的技术,这些技术已经被成功地用于为各种分类任务提供更丰富的文本特征。

第十四章:主题建模

在上一章中,我们使用词袋模型将非结构化文本数据转换为数值格式。 此模型抽象出了单词顺序,并将文档表示为单词向量,其中每个条目表示单词对文档的相关性。

产生的文档-术语矩阵DTM)(您可能还会遇到转置的术语-文档矩阵)可用于基于其标记内容将文档与其他文档或查询向量进行比较,并快速找到草堆中的一根针或相应地对文档进行分类。

但是,这个文档模型既是高维的又是非常稀疏的。 因此,它几乎不能总结内容或更接近理解它是关于什么的。 在本章中,我们将使用无监督的机器学习形式的主题建模来从文档中提取隐藏的主题。 这些主题可以以自动化方式为大量文档提供详细的见解。 它们非常有用,可以了解草堆本身,并允许以主题和文档的关联程度进行简明的文档标记。

主题模型允许提取复杂的可解释文本特征,可以以各种方式用于从大量文档中提取交易信号。 它们加速了文档的审阅,帮助识别和聚类相似的文档,并且可以被注释为预测建模的基础。 应用包括在公司披露、收入电话成绩单、客户评论或合同中识别关键主题,使用例如情感分析或直接标记与随后的资产回报相关联。

更具体地说,在这一章中,我们将涵盖以下主题:

  • 主题建模实现了什么,为什么重要,以及它如何发展

  • 潜在语义索引LSI)如何降低 DTM 的维度

  • 概率隐含语义分析pLSA)如何使用生成模型提取主题

  • 潜在狄利克雷分配LDA)如何改进 pLSA 以及为什么它是最流行的主题模型

  • 如何可视化和评估主题建模结果

  • 如何使用 sklearn 和 gensim 实现 LDA

  • 如何将主题建模应用于收入电话和 Yelp 商业评论的集合

下一节的代码示例位于 GitHub 存储库的目录中,主 README 文件中列出了参考资料。

学习潜在主题:目标和方法

主题建模旨在发现跨文档的隐藏主题或主题,捕获超越个别单词的语义信息。 它旨在解决从文本数据中学习的机器学习算法的一个关键挑战,即超越已被写成的词汇水平,到预期的语义水平。 生成的主题可以用于根据它们与各种主题的关联来注释文档。

换句话说,主题建模旨在自动总结大量文档以促进组织和管理,以及搜索和推荐。同时,它可以使得人类能够理解文档的程度,以至于可以解释主题的描述。

主题模型旨在解决可能困扰词袋模型的维度诅咒。基于高维稀疏向量的文档表示可以使相似性度量变得嘈杂,导致不准确的距离测量和文本分类模型的过拟合。

此外,词袋模型忽略了词序并且丢失了语境以及语义信息,因为它无法捕捉同义词(几个词具有相同的含义)和多义词(一个词具有多个含义)。因此,当文档没有按照用于搜索或比较的术语进行索引时,文档检索或相似性搜索可能会错过要点。

这些缺陷引发了这个问题:我们如何对促进与文本数据更有生产性的交互的含义主题进行建模和学习?

从线性代数到分层概率模型

主题模型最初尝试改进矢量空间模型(在 1970 年代中期开发)的努力应用线性代数来降低文档-术语矩阵的维度。这种方法类似于我们在《第十二章》中讨论的主成分分析算法,无监督学习,关于无监督学习。虽然有效,但在缺乏基准模型的情况下评估这些模型的结果是困难的。

作为回应,出现了概率模型,假设有一个明确的文档生成过程,并提供算法来反向工程这个过程,并恢复潜在主题。

此表突出显示了我们将在接下来的章节中更详细讨论的模型演变的关键里程碑:

模型 年份 描述
潜在语义索引 (LSI) 1988 通过将单词空间的维度降低以捕捉语义文档-术语关系来减少
概率潜在语义分析 (pLSA) 1999 反向工程一个假设单词生成主题并且文档是主题的混合的过程
潜在狄利克雷分配 (LDA) 2003 为文档添加了一个生成过程:一个三层次的层次贝叶斯模型

潜在语义索引

潜在语义索引(LSI,也称为潜在语义分析)旨在改进省略了包含查询词的同义词的相关文档的查询结果。它旨在建模文档和术语之间的关系,以便能够预测一个术语应该与文档相关联,即使由于单词使用的变异性,没有观察到这样的关联。

LSI 使用线性代数来通过分解 DTM 找到给定数量k个潜在主题。更具体地说,它使用奇异值分解SVD)来找到使用 k 个奇异值和向量的最佳低秩 DTM 近似。换句话说,LSI 是我们在第十二章中遇到的无监督学习技术的应用,无监督学习,用于我们在第十三章中介绍的文本表示,处理文本数据。作者尝试了分层聚类,但发现这对于明确地建模文档-主题和主题-词关系,或者捕获文档或术语与几个主题的关联太过于限制性。

在这种情况下,SVD 的目的是识别一组不相关的索引变量或因子,使我们能够通过其因子值向量表示每个术语和文档。

以下图示了 SVD 如何将 DTM 分解为三个矩阵,其中两个包含正交奇异向量,以及一个带有奇异值的对角矩阵,用作缩放因子。假设原始数据中存在一些相关性,奇异值会衰减,因此仅选择最大的T个奇异值会产生原始 DTM 的低维近似,且信息损失相对较小。因此,在简化版本中,原先有N个项目的行或列只有T<N个条目。

这种简化的分解可以解释如下,其中第一个M x T矩阵表示文档与主题之间的关系,对角矩阵按其语料库强度缩放主题,并且第三个矩阵模拟了词-主题关系:

从第一个两个矩阵的乘积得到的矩阵的行,U[T]Σ[T][,]对应于在潜在主题空间中投影到原始文档的位置。

如何使用 sklearn 实现 LSI

我们将使用我们在上一章介绍的 BBC 文章数据来说明 LSI 的应用,因为它足够小,可以快速训练,并且允许我们将主题分配与类别标签进行比较。请参阅latent_semantic_indexing笔记本以获取其他实施细节:

  1. 我们首先加载文档并创建一个包含 50 篇文章的训练集和(分层)测试集。

  2. 然后,我们使用TfidfVectorizer对数据进行向量化,以获得加权 DTM 计数,并过滤掉在不到 1%或超过 25%的文档中出现的单词,以及通用停用词,以获得约 2,900 个单词的词汇表:

vectorizer = TfidfVectorizer(max_df=.25, min_df=.01,
stop_words='english',
binary=False)
train_dtm = vectorizer.fit_transform(train_docs.article)
test_dtm = vectorizer.transform(test_docs.article)
  1. 我们使用sklearnTruncatedSVD类,它只计算* k *个最大的奇异值,以降低文档-术语矩阵的维度。确定性 arpack 算法提供了精确的解决方案,但默认的随机实现对于大矩阵更有效。

  2. 我们计算五个主题以匹配五个类别,这仅解释了总 DTM 方差的 5.4%,因此更高的值是合理的:

svd = TruncatedSVD(n_components=5, n_iter=5, random_state=42)
svd.fit(train_dtm)
svd.explained_variance*ratio* array([0.00187014, 0.01559661, 0.01389952, 0.01215842, 0.01066485])
  1. LSI 为文档-术语矩阵确定了一个新的正交基,将秩降低到所需主题的数量。

  2. 训练好的svd对象的.transform()方法将文档投影到新的主题空间,这是通过降低文档向量的维度得到的结果,并对应于之前说明的U[T]Σ[T]变换:

train_doc_topics = svd.transform(train_dtm)
train_doc_topics.shape
(2175, 5)
  1. 我们可以对一篇文章进行采样,查看其在主题空间中的位置。我们选择了一个与主题 1 和 2 最(正相关)关联的Politics文章:
i = randint(0, len(train_docs))
train_docs.iloc[i, :2].append(pd.Series(doc_topics[i], index=topic_labels))
Category Politics
Heading What the election should really be about?
Topic 1 0.33
Topic 2 0.18
Topic 3 0.12
Topic 4 0.02
Topic 5 0.06
  1. 此示例的主题分配与每个类别的平均主题权重相一致(Politics是最左边的)。它们说明了 LSI 如何将 k 个主题表达为 k 维空间中的方向(笔记本包括每个类别的平均主题分配在二维空间中的投影)。

  2. 每个类别都有明确定义,测试任务与训练任务相匹配。然而,权重既有正数又有负数,这使得解释主题更加困难:

  1. 我们还可以显示与每个主题最密切相关的单词(绝对值)。主题似乎捕捉了一些语义信息,但没有区分开来:

优缺点

LSI 的优点包括去除噪声、减轻维度灾难,同时捕捉一些语义信息,并将文档和术语进行聚类。

然而,LSI 的结果很难解释,因为主题是具有正负条目的词向量。此外,没有基础模型可供评估拟合度,并在选择维度或主题数量时提供指导。

概率潜在语义分析

概率潜在语义分析(pLSA)从统计的角度看待 LSA,并创建了一个生成模型来解决 LSA 缺乏理论基础的问题。

pLSA 明确地对 LSA 采取了统计视角,并创建了一个生成模型,以解决 LSA 缺乏理论基础的问题。

此生成过程的对称形式假定单词-文档共现的隐含主题类别生成了单词和文档,而非对称模型假定给定文档后选择了主题,并且单词是在给定主题后进行的第二步生成:

主题数是在训练之前选择的超参数,不是从数据中学习得到的。

概率模型通常使用以下板块符号来表示依赖关系。下图编码了刚刚描述的非对称模型的关系。每个矩形代表多个项,比如外部的 M 个文档和内部块的每个文档的 N 个单词。我们只观察文档及其内容,模型推断隐藏或潜在的主题分布:

使用概率模型的好处是,我们现在可以通过评估它们在训练期间学到的参数对新文档分配的概率来比较模型。

如何使用 sklearn 实现 pLSA

pLSA 等价于使用 Kullback-Leibler 散度目标的非负矩阵分解(请参见 GitHub 上的参考资料 github.com/PacktPublishing/Hands-On-Machine-Learning-for-Algorithmic-Trading)。因此,我们可以使用 sklearn.decomposition.NM 类来实现这个模型,遵循 LSA 示例。

使用由 TfidfVectorizer 生成的 DTM 的相同的训练-测试拆分,我们按如下方式拟合 pLSA:

nmf = NMF(n_components=n_components,
random_state=42,
solver='mu',
beta_loss='kullback-leibler',
max_iter=1000)
nmf.fit(train_dtm)

我们得到一个重建误差的度量,这是以前解释的方差度量的替代品:

nmf.reconstruction_err_
316.2609400385988

由于其概率性质,pLSA 只产生正主题权重,从而使测试集和训练集的主题-类别关系更加直观:

我们还可以看到描述每个主题的词列表开始变得更有意义;例如,娱乐类别与主题 4 最直接相关,其中包括电影、明星等词语:

潜在狄利克雷分配

潜在狄利克雷分配LDA)通过为主题添加生成过程来扩展 pLSA。

它是最受欢迎的主题模型,因为它倾向于产生人类能够相关的有意义的主题,可以为新文档分配主题,并且是可扩展的。LDA 模型的变体可以包括元数据,如作者,或图像数据,或学习分层主题。

LDA 的工作原理

LDA 是一个分层贝叶斯模型,假设主题是词的概率分布,而文档是主题的分布。更具体地说,该模型假设主题遵循稀疏狄利克雷分布,这意味着文档只涵盖了一个小部分主题,并且主题仅使用了一小部分词频繁地。

狄利克雷分布

狄利克雷分布(Dirichlet distribution)生成可以与离散分布一起使用的概率向量。也就是说,它随机生成一定数量的值,这些值为正且总和为一,符合概率的期望。它有一个正实数参数,用于控制概率的集中程度。接近零的值意味着只有少数值会为正,并且获得大部分概率质量。下面的屏幕截图展示了对于 α = 0.1 进行了三次大小为 10 的抽样(dirichlet_distribution 笔记本中包含一个模拟,您可以尝试不同的参数值):

狄利克雷分配

生成模型

狄利克雷分布在 LDA 主题模型中占据重要地位,该模型假设当作者将文章添加到一系列文档中时,存在以下生成过程:

  1. 根据主题概率随机混合一小部分共享主题 K

  2. 对于每个词语,根据文档-主题概率选择其中一个主题

  3. 根据主题-词语概率从主题的词语列表中选择一个词语

因此,文章内容取决于每个主题的权重和构成每个主题的术语。狄利克雷分布控制着为文档选择主题以及为主题选择词语,并编码了这样一个观点:文档仅涵盖少数主题,而每个主题仅使用少量词语频繁出现。

LDA 模型的板符号总结了这些关系:

反向工程过程

生成过程是虚构的,但事实证明它是有用的,因为它允许恢复各种分布。LDA 算法对虚构作者的工作进行了反向工程,并得出了对文档-主题-词语关系进行简洁描述的摘要,该描述包括以下内容:

  • 每个主题对文档的贡献百分比

  • 每个词语与主题的概率关联

LDA 通过对假设的内容生成过程进行反向工程来解决从文档集合及其包含的词语中恢复分布的贝叶斯推理问题。原始论文使用变分贝叶斯VB)来近似后验分布。其他选择包括吉布斯抽样和期望传播。稍后,我们将演示使用 sklearn 和 gensim 库的实现。

如何评估 LDA 主题

无监督主题模型不能保证结果具有意义或可解释性,并且没有客观的度量标准来评估结果,就像监督学习中那样。人类主题评估被认为是金标准,但可能成本高昂,并且不易规模化。

两种更客观评估结果的选择包括困惑度,它评估模型对未见文档的表现,以及主题连贯性度量,它旨在评估发现的模式的语义质量。

困惑度

当应用于 LDA 时,困惑度衡量模型恢复的主题 - 单词概率分布对样本的预测能力,例如,未见过的文本文档。 它基于此分布 p 的熵 H(p) 并针对标记集 w 计算:

接近零的度量意味着分布更能够预测样本。

主题连贯性

主题连贯性衡量主题模型结果的语义一致性,即人类是否会认为与主题相关的单词及其概率是有意义的。

为此,它通过衡量与主题最相关的单词之间的语义相似度来评分每个主题。 更具体地说,连贯性度量基于定义主题的单词集合 W 的观察概率。

我们使用两个为 LDA 设计且已经显示与人类对主题质量的判断相吻合的连贯性度量,即 UMass 和 UCI 度量。

UCI 指标定义了一个词对的分数,该分数是两个不同对(顶部)主题词 w[i]w[j]w点间互信息PMI)之和和一个平滑因子 ε

概率是从一个外部语料库(如维基百科)上的单词共现频率中计算的,因此这个度量可以被认为是与语义基准的外部比较。

相比之下,UMass 指标使用训练语料库中的一些文档 D 中的共现来计算一致性分数:

与外部基准的比较不同,这个度量反映了内在的连贯性。 这两个度量都经过评估,与人类判断相吻合。 在这两种情况下,值越接近零意味着主题更连贯。

如何使用 sklearn 实现 LDA

与之前使用 BBC 数据不同,我们使用 sklearn.decomposition.LatentDirichletAllocation 来训练一个具有五个主题的 LDA 模型(有关参数的详细信息,请参阅 sklearn 文档,有关实现细节,请参阅笔记本 lda_with_sklearn):

lda = LatentDirichletAllocation(n_components=5, 
                                    n_jobs=-1, 
                                    max_iter=500,
                                    learning_method='batch', 
                                    evaluate_every=5,
                                    verbose=1, 
                                    random_state=42)
ldat.fit(train_dtm)
LatentDirichletAllocation(batch_size=128, doc_topic_prior=None,
             evaluate_every=5, learning_decay=0.7, learning_method='batch',
             learning_offset=10.0, max_doc_update_iter=100, max_iter=500,
             mean_change_tol=0.001, n_components=5, n_jobs=-1,
             n_topics=None, perp_tol=0.1, random_state=42,
             topic_word_prior=None, total_samples=1000000.0, verbose=1)

模型在训练期间跟踪样本内困惑度,并一旦该度量停止改善,就停止迭代。 我们可以像往常一样保存和加载 sklearn 对象的结果:

joblib.dump(lda, model_path / 'lda.pkl')
lda = joblib.load(model_path / 'lda.pkl')

如何使用 pyLDAvis 可视化 LDA 结果

主题可视化利用人类判断方便评估主题质量。 pyLDAvis 是 LDAvis 的 Python 版本,是在 R 和 D3.js 中开发的。我们将介绍关键概念;每个 LDA 实现笔记本都包含示例。

pyLDAvis 显示了话题之间的全局关系,同时通过检查与每个话题最相关的术语以及与每个术语相关的话题来促进它们的语义评估。它还解决了语料库中频繁出现的术语倾向于主导定义话题的词的多项分布的挑战。LDAVis 引入了术语w对话题t的相关性r,以使用权重参数0<=ƛ<=1生成关键术语的灵活排名。

作为模型对观察到的术语w在话题t中的概率估计,并作为语料库中术语w的边际概率:

第一个术语衡量了术语t与话题w的关联程度,第二个术语衡量了提升或显著性,即术语在话题中出现的可能性相对于语料库中的可能性有多少。

话题 14

该工具允许用户交互地更改ƛ以调整相关性,从而更新术语的排名。用户研究发现ƛ=0.6产生了最合理的结果。

如何使用 gensim 实现 LDA

gensim是一个专门的 NLP 库,具有快速的 LDA 实现和许多附加功能。我们还将在下一章中使用它来处理单词向量(有关详细信息,请参见latent_dirichlet_allocation_gensim笔记本)。

它促进了将 sklearn 生成的 DTM 转换为 gensim 数据结构,如下所示:

train_corpus = Sparse2Corpus(train_dtm, documents_columns=False)
test_corpus = Sparse2Corpus(test_dtm, documents_columns=False)
id2word = pd.Series(vectorizer.get_feature_names()).to_dict()

Gensim LDA 算法包括许多设置,如下所示:

LdaModel(corpus=None, 
         num_topics=100, 
         id2word=None, 
         distributed=False, 
         chunksize=2000, # No of doc per training chunk.
         passes=1,       # No of passes through corpus during training
         update_every=1, # No of docs to be iterated through per update
         alpha='symmetric', 
         eta=None,       # a-priori belief on word probability
         decay=0.5,    # % of lambda forgotten when new doc is examined
         offset=1.0,   # controls slow down of first few iterations.
         eval_every=10,   # how often estimate log perplexity (costly)
         iterations=50,         # Max. of iterations through the corpus
         gamma_threshold=0.001, # Min. change in gamma to continue
         minimum_probability=0.01, # Filter topics with lower 
                                     probability
         random_state=None, 
         ns_conf=None, 
         minimum_phi_value=0.01, # lower bound on term probabilities
         per_word_topics=False,  #  Compute most word-topic 
                                    probabilities
         callbacks=None, 
         dtype=<class 'numpy.float32'>)

Gensim 还提供了一个用于并行训练的LdaMulticore模型,可以使用 Python 的多进程特性加速训练。

模型训练只需要实例化LdaModel对象,如下所示:

lda = LdaModel(corpus=train_corpus,
num_topics=5,
id2word=id2word)

话题连贯性(Topic coherence)衡量话题中的单词是否倾向于一起出现。它为每对排名靠前的单词添加一个分数。该分数是包含至少一个较高排名单词实例的文档也包含至少一个较低排名单词实例的概率的对数。

大的负值表示不经常共现的单词;接近零的值表示单词倾向于更经常共现。gensim允许进行话题连贯性评估,产生话题连贯性并显示每个话题的最重要单词:

coherence = lda_gensim.top_topics(corpus=train_corpus,  coherence='u_mass')

我们可以如下显示结果:

topic_coherence = []
topic_words = pd.DataFrame()
for t in range(len(coherence)):
    label = topic_labels[t]
    topic_coherence.append(coherence[t][1])
    df = pd.DataFrame(coherence[t][0], columns=[(label, 'prob'), (label, 'term')])
    df[(label, 'prob')] = df[(label, 'prob')].apply(lambda x: '{:.2%}'.format(x))
    topic_words = pd.concat([topic_words, df], axis=1)

topic_words.columns = pd.MultiIndex.from_tuples(topic_words.columns)
pd.set_option('expand_frame_repr', False)
print(topic_words.head())
pd.Series(topic_coherence, index=topic_labels).plot.bar();

这显示了每个话题的以下顶级单词:

话题 1 话题 2 话题 3 话题 4 话题 5
概率 术语 概率 术语 概率 术语 概率 术语 概率 术语
0.55% 在线 0.90% 最佳 1.04% 移动 0.64% 市场 0.94% 劳动力
0.51% 网站 0.87% 游戏 0.98% 手机 0.53% 增长 0.72% 布莱尔
0.46% 游戏 0.62% 0.51% 音乐 0.52% 销售 0.72% 褐色
0.45% 0.61% 0.48% 影片 0.49% 经济 0.65% 选举
0.44% 0.56% 0.48% 使用 0.45% 价格 0.57% 联合

以及相应的一致性分数,突出了主题质量的衰减(至少部分原因是由于相对较小的数据集):

主题质量的衰减

收益电话的主题建模

在 第三章 中,金融的替代数据,我们学习了如何从 SeekingAlpha 网站上抓取收益电话数据。在本节中,我们将使用此来源说明主题建模。我使用了 2018 年下半年的约 500 个收益电话转录样本。对于实际应用,更大的数据集将非常理想。 earnings_calls 目录 包含了几个文件,后面提到了一些示例。

有关加载、探索和预处理数据的详细信息,以及训练和评估各个模型的详细信息,请参阅 lda_earnings_calls 笔记本,以及描述这里的实验的 run_experiments.py 文件。

数据预处理

收益电话的转录由公司代表、运营商和通常与分析师进行的问答环节组成。我们将每个声明视为单独的文档,忽略运营商的声明,以获取 22,766 个项目,平均词数为 144 个,中位数为 64 个:

documents = []
for transcript in earnings_path.iterdir():
    content = pd.read_csv(transcript / 'content.csv')
    documents.extend(content.loc[(content.speaker!='Operator') & (content.content.str.len() > 5), 'content'].tolist())
len(documents)
22766

我们使用 spaCy 对这些文档进行预处理,如 第十三章 所示,处理文本数据(参见笔记本),并将清理和词形还原的文本存储为新的文本文件。

数据探索揭示了领域特定的停用词,例如年份和季度,我们在第二步中移除这些停用词,我们还过滤掉少于十个单词的语句,以便剩下约 16,150 个语句。

模型训练和评估

为了说明,我们将创建一个包含在大约 1560 个特征中出现在 0.5% 到 50% 文档之间的术语的文档-术语矩阵。在四核 i7 上对语料库进行 25 次训练 15 个主题模型需要花费两分钟多一点的时间。

每个主题的前 10 个单词确定了几个不同的主题,从明显的财务信息到临床试验(主题 4)和供应链问题(12):

使用 pyLDAvis 的相关度指标,将无条件频率相对于提升的 0.6 加权,主题定义变得更直观,如主题 14 关于销售业绩的示例所示:

主题 14 的销售业绩

笔记本还说明了如何根据其主题关联查找文档。在这种情况下,分析师可以审查相关声明以获取细微差别,使用情感分析进一步处理特定主题的文本数据,或者分配从市场价格中导出的标签。

运行实验

为了说明不同参数设置的影响,我们对不同 DTM 约束和模型参数进行了几百次实验。更具体地说,我们让 min_dfmax_df 参数从 50 到 500 个词和 10% 到 100% 的文档,分别使用二进制和绝对计数。然后,我们使用 3 到 50 个主题训练了 LDA 模型,在语料库上进行了 1 到 25 次训练。

以下图表说明了主题一致性(越高越好)和困惑度(越低越好)的结果。一致性在 25-30 个主题后下降,困惑度同样增加:

该笔记本包含了回归结果,量化了参数和结果之间的关系。通常使用绝对计数和较小的词汇量可以获得更好的结果。

Yelp 商业评论的主题建模

lda_yelp_reviews 笔记本包含了将 LDA 应用于 Yelp 上的六百万商业评论的示例。评论的长度比从盈利电话会议抄录中提取的语句更加一致。在清理后,第 10 和第 90 百分位数的范围从 14 到 90 个词元。

我们展示了一个模型的结果,该模型使用了基于 min_df=0.1%max_df=25% 的 3,800 个词元的词汇表,使用单一传递以避免对 20 个主题进行漫长的训练时间。我们可以使用 pyldavis topic_info 属性来计算 lambda=0.6 的相关性值,从而生成以下单词列表(详细信息请参见笔记本):

Gensim 提供了 LdaMultiCore 实现,允许使用 Python 的 multiprocessing 模块进行并行训练,并且当使用四个工作进程时,性能提高了 50%。然而,更多的工作进程并不会进一步减少训练时间,因为存在 I/O 瓶颈。

摘要

在本章中,我们探讨了使用主题建模来洞察大量文档内容的用途。我们涵盖了潜在语义分析,它使用 DTM 的降维来将文档投影到潜在主题空间中。虽然在处理由高维词向量引起的维度诅咒方面有效,但它并不捕捉太多语义信息。概率模型对文档、主题和词之间的相互作用做出了明确的假设,这些假设允许算法反向工程文档生成过程,并评估新文档的模型适应度。我们看到 LDA 能够提取出合理的主题,使我们能够以自动化的方式对大量文本进行高层次理解,同时以有针对性的方式识别相关文档。

在下一章中,我们将学习如何训练神经网络,将个别单词嵌入到一个捕捉重要语义信息的高维向量空间中,并且可以使用生成的词向量作为高质量的文本特征。

第十五章:词嵌入

在前两章中,我们应用了词袋模型将文本数据转换为数值格式。结果是稀疏的、固定长度的向量,表示文档在高维词空间中的位置。这允许评估文档的相似性,并创建特征来训练机器学习算法,分类文档的内容或评估其中表达的情感。然而,这些向量忽略了术语使用的上下文,因此,例如,包含相同单词的不同句子将被编码为相同的向量。

在本章中,我们将介绍一种替代类别的算法,它使用神经网络来学习个别语义单元(如单词或段落)的向量表示。这些向量是密集的,而不是稀疏的,而且有几百个实值,而不是几万个二进制或离散条目。它们被称为嵌入,因为它们将每个语义单元分配到连续向量空间中的位置。

嵌入是通过训练模型将标记与其上下文关联起来而产生的,这样做的好处是类似的用法意味着类似的向量。此外,我们将看到嵌入如何通过它们的相对位置来编码语义方面的关系,例如词之间的关系。因此,它们是我们将在后面的章节中介绍的深度学习模型中强大的特征。

具体来说,在本章中,我们将涵盖以下主题:

  • 词嵌入是什么,以及它们如何工作和捕捉语义信息

  • 如何使用训练好的词向量

  • 哪些网络架构对训练 Word2vec 模型有用

  • 如何使用 Keras、gensim 和 TensorFlow 训练 Word2vec 模型

  • 如何可视化和评估词向量的质量

  • 如何使用 SEC 文件训练 Word2vec 模型

  • 如何扩展 Word2vec 的Doc2vec

词嵌入如何编码语义

词袋模型将文档表示为反映其所包含标记的向量。词嵌入将标记表示为较低维度的向量,使得它们的相对位置反映了它们在上下文中的使用方式的关系。它们体现了语言学中的分布假设,即单词最好通过它们的搭配来定义。

词向量能够捕捉许多语义方面;不仅是近义词彼此靠近,而且单词之间可以有多个相似度程度,例如,单词“驾驶员”可能与“汽车司机”或“导致”的相似。此外,嵌入反映了单词对之间的关系,例如类比(东京对日本就像巴黎对法国,或者 went 对 go 就像 saw 对 see),我们将在本节稍后进行说明。

嵌入是通过训练机器学习模型来预测单词与它们的上下文或反之。在下一节中,我们将介绍这些神经语言模型的工作原理,并介绍成功的方法,包括 Word2vec、Doc2vec和 fastText。

神经语言模型如何学习上下文中的用法

词嵌入是通过训练一个浅层神经网络来预测给定上下文的单词而产生的。传统语言模型将上下文定义为目标词之前的单词,而单词嵌入模型使用包围目标的对称窗口中包含的单词。相比之下,词袋模型使用整个文档作为上下文,并使用(加权)计数来捕捉单词的共现关系,而不是预测向量。

早期使用的神经语言模型包括增加了计算复杂性的非线性隐藏层。Word2vec 及其扩展简化了体系结构,以便在大型数据集上进行训练(例如,维基百科包含超过二十亿个标记;有关前馈网络的详细信息,请参阅第十七章 深度学习)。

Word2vec 模型 – 大规模学习嵌入

Word2vec 模型是一个将文本语料库作为输入并输出该语料库中单词的一组嵌入向量的两层神经网络。有两种不同的架构可以使用浅层神经网络有效地学习单词向量,如下图所示:

  • Continuous-Bag-Of-WordsCBOW)模型使用上下文词向量的平均值作为输入来预测目标词,因此它们的顺序并不重要。CBOW 模型训练速度更快,对于频繁词汇来说稍微准确一些,但对不经常出现的单词关注较少。

  • Skip-GramSG)模型与之相反,使用目标词来预测从上下文中抽样的词。它在小数据集上效果很好,并且即使对于罕见的单词或短语也能找到良好的表示:

因此,Word2vec 模型接收一个嵌入向量作为输入,并与另一个嵌入向量计算点积。请注意,假设归一化向量,当向量相等时,点积被最大化(绝对值),当它们正交时被最小化。

然后使用反向传播来根据由任何分类错误造成的目标函数计算的损失来调整嵌入权重。在下一节中,我们将看到 Word2vec 如何计算损失。

训练通过将上下文窗口滑过文档进行,通常被分成句子。每次完整迭代整个语料库都被称为一个时代。根据数据,可能需要几十个时代才能使向量质量收敛。

从技术上讲,SG 模型已被证明可以因式分解包含相应单词和上下文对的点互信息的单词-上下文矩阵(请参阅 GitHub 上的参考文献)。

模型目标 – 简化 softmax

Word2vec 模型旨在预测一个单词,而这个单词可能是非常大的词汇表中的一个。神经网络通常使用 softmax 函数,该函数将任意数量的实值映射到相等数量的概率,以实现相应的多类目标,其中h指嵌入,v指输入向量,c是单词w的上下文:

然而,softmax 的复杂性随着类别数量的增加而增加,因为分母需要计算词汇表中所有单词的点积以标准化概率。Word2vec 模型通过使用 softmax 的简化版本或基于采样的方法(详见参考资料)来提高效率:

  • 分层 softmax 将词汇表组织为具有词作为叶节点的二叉树。到每个节点的唯一路径可用于计算单词概率。

  • 噪声对比估计 (NCE) 对上下文外的“噪声词”进行采样,并将多类任务近似为二元分类问题。随着样本数量的增加,NCE 的导数接近 softmax 梯度,但只要 25 个样本就能产生与 softmax 相似的收敛速度,速度快 45 倍。

  • 负采样 (NEG)省略了噪声词样本,以逼近 NCE 并直接最大化目标词的概率。因此,NEG 优化了嵌入向量的语义质量(相似用法的相似向量),而不是测试集上的准确性。然而,它可能对频率较低的单词产生比分层 softmax 目标更差的表示。

自动短语检测

预处理通常涉及短语检测,即识别常用在一起的标记,并应该接收单一向量表示(例如,纽约市,请参阅第十三章中关于 n-gram 的讨论,处理文本数据)。

原始的 Word2vec 作者使用了一种简单的提升评分方法,该方法将两个词w[i]w[j]标识为一个二元组,如果它们的联合出现相对于每个词的单独出现超过了给定阈值,通过一个修正因子δ校正:

评分器可以重复应用以识别连续更长的短语。

另一种选择是归一化的点间互信息分数,这种方法更准确,但计算成本更高。它使用了相对词频P(w),并在+1 和-1 之间变化:

如何评估嵌入向量 - 向量运算和类比

词袋模型创建反映标记在文档中存在和相关性的文档向量。潜在语义分析减少了这些向量的维度,并在此过程中识别了可被解释为潜在概念的内容。潜在狄利克雷分配将文档和术语都表示为包含潜在主题权重的向量。

词和短语向量的维度没有明确的含义。然而,嵌入在潜在空间中编码了相似用法,以一种体现在语义关系上的接近性的方式。这导致了有趣的性质,即类比可以通过添加和减去词向量来表达。

以下图显示了连接巴黎和法国的向量(即它们的嵌入之差)如何反映了首都关系。类似的关系,伦敦:英国,对应于相同的向量,即英国与通过将首都向量添加到伦敦得到的位置非常接近:

正如单词可以在不同的语境中使用一样,它们可以以不同的方式与其他单词相关联,而这些关系对应于潜在空间中的不同方向。因此,如果训练数据允许,嵌入应该反映出几种不同类型的类比关系。

Word2vec 的作者提供了一份涵盖地理、语法和句法以及家庭关系等多个方面的几千个关系的列表,用于评估嵌入向量的质量。如上所示,该测试验证了目标词(英国)最接近的是将代表类似关系(巴黎:法国)的向量添加到目标的补充(伦敦)后得到的结果。

以下图将在维基百科语料库上训练的 Word2vec 模型的最相关类比的 300 维嵌入,具有超过 20 亿标记,通过主成分分析PCA)投影到二维。从以下类别的超过 24,400 个类比的测试实现了超过 73.5% 的准确率(参见笔记本):

使用嵌入模型

与其他无监督学习技术类似,学习嵌入向量的目标是为其他任务生成特征,如文本分类或情感分析。

获取给定文档语料库的嵌入向量有几种选项:

  • 使用从通用大语料库(如维基百科或谷歌新闻)中学到的嵌入

  • 使用反映感兴趣领域的文档来训练自己的模型

对于后续的文本建模任务,内容越专业化、越不通用,第二种方法就越可取。然而,高质量的词向量需要大量数据,并且需要包含数亿字的信息性文档。

如何使用预训练的词向量

预训练词嵌入的几个来源。流行选项包括斯坦福的 GloVE 和 spaCy 的内置向量(有关详细信息,请参见笔记本 using_trained_vectors)。

GloVe - 用于词表示的全球向量

GloVe 是在斯坦福 NLP 实验室开发的无监督算法,它从聚合的全局词-词共现统计中学习单词的向量表示(请参阅参考文献)。可用于以下网络规模来源的预训练向量:

  • Common Crawl 共有 420 亿或 840 亿令牌和词汇表或 190 万或 220 万令牌。

  • Wikipedia 2014 + Gigaword 5,共有 60 亿令牌和 40 万令牌的词汇表。

  • 推特使用了 20 亿条推文,27 亿令牌和 120 万令牌的词汇表

我们可以使用 gensim 将矢量文本文件转换并加载到 KeyedVector 对象中:

from gensim.models import Word2vec, KeyedVectors
 from gensim.scripts.glove2Word2vec import glove2Word2vec
glove2Word2vec(glove_input_file=glove_file, Word2vec_output_file=w2v_file)
 model = KeyedVectors.load_Word2vec_format(w2v_file, binary=False)

Word2vec 作者提供了包含超过 24,000 个类比测试的文本文件,gensim 用于评估词向量。

在维基百科语料库上训练的词向量涵盖了所有类比,并在各个类别中达到了 75.5% 的总体准确率:

类别 样本 准确率 类别 样本 准确率
首都-普通国家 506 94.86% 比较级 1,332 88.21%
首都-世界 8,372 96.46% 最高级 1,056 74.62%
城市-州 4,242 60.00% 现在分词 1,056 69.98%
货币 752 17.42% 国籍形容词 1,640 92.50%
家庭 506 88.14% 过去时 1,560 61.15%
形容词到副词 992 22.58% 复数 1,332 78.08%
反义词 756 28.57% 复数动词 870 58.51%

常见爬网词嵌入的 10 万个最常见标记覆盖了约 80% 的类比,并以 78% 的略高准确率,而 Twitter 的词嵌入则仅覆盖了 25%,准确率为 62%。

如何训练自己的单词向量嵌入

许多任务需要嵌入或特定领域的词汇,而基于通用语料库的预训练模型可能无法很好地或根本无法表示。标准 Word2vec 模型无法为词汇表中不存在的单词分配向量,而是使用降低其预测价值的默认向量。

例如,在处理行业特定文件时,词汇表或其使用可能随着时间推移而变化,因为新技术或产品出现。因此,嵌入也需要相应地发展。此外,企业盈利发布使用的语言不完全反映在基于维基百科文章预训练的 GloVe 向量中。

我们将使用 Keras 库来说明 Word2vec 架构,我们将在下一章中更详细地介绍,并使用 Word2vec 作者提供的代码的 gensim 适配更高效的代码。Word2vec 笔记本包含额外的实现细节,包括 TensorFlow 实现的参考。

Keras 中的 Skip-Gram 架构

为了说明 Word2vec 网络架构,我们使用 TED Talk 数据集,该数据集具有对齐的英文和西班牙文字幕,我们首次在第十三章中介绍了处理文本数据

笔记本包含将文档标记化并为词汇表中的每个项目分配唯一 ID 的代码。我们要求语料库中至少出现五次,并保留 31300 个标记的词汇表。

噪声对比估计

Keras 包括一个make_sampling_table方法,允许我们创建一个训练集,其中上下文和噪声词与相应的标签配对,根据它们的语料库频率进行采样。

结果是 2700 万个正面和负面的上下文和目标对的例子。

模型组件

Skip-Gram模型为词汇表中的每个项目包含一个 200 维嵌入向量,导致 31300 x 200 可训练参数,加上两个用于 sigmoid 输出。

在每次迭代中,模型计算上下文和目标嵌入向量的点积,通过 sigmoid 产生概率,并根据损失的梯度调整嵌入。

使用 TensorBoard 可视化嵌入

TensorBoard 是一种可视化工具,允许将嵌入向量投影到三维空间中,以探索单词和短语的位置。

使用 gensim 从 SEC 备案中学习的单词向量

在本节中,我们将使用 gensim 从美国年度证券交易委员会(SEC)备案中学习单词和短语向量,以说明单词嵌入对算法交易的潜在价值。在接下来的几节中,我们将将这些向量与价格回报结合使用作为特征,训练神经网络来根据安全备案的内容预测股票价格。

特别是,我们使用一个数据集,其中包含 2013-2016 年间上市公司提交的超过 22,000 份 10-K 年度报告,其中包含财务信息和管理评论(参见第三章,金融替代数据)。对于大约一半的公司的 11-K 备案,我们有股价以标记预测建模的数据(有关数据来源和sec-filings文件夹中笔记本的引用,请参阅详细信息)。

预处理

每个备案都是一个单独的文本文件,一个主索引包含备案元数据。我们提取最具信息量的部分,即以下部分:

  • 项目 1 和 1A:业务和风险因素

  • 项目 7 和 7A:管理对市场风险的讨论和披露

笔记本预处理显示如何使用 spaCy 解析和标记文本,类似于第十四章中采用的方法,主题建模。我们不对单词进行词形还原处理,以保留单词用法的细微差别。

自动短语检测

我们使用gensim来检测短语,就像之前介绍的那样。Phrases模块对标记进行评分,而Phraser类相应地转换文本数据。笔记本展示了如何重复此过程以创建更长的短语:

sentences = LineSentence(f'ngrams_1.txt')
phrases = Phrases(sentences=sentences,
                  min_count=25,  # ignore terms with a lower count
                  threshold=0.5,  # only phrases with higher score
                  delimiter=b'_',  # how to join ngram tokens
                  scoring='npmi')  # alternative: default
grams = Phraser(phrases)
sentences = grams[sentences]

最频繁的二元组包括common_stockunited_statescash_flowsreal_estateinterest_rates

模型训练

gensim.models.Word2vec类实现了之前介绍的 SG 和 CBOW 架构。Word2vec 笔记本包含额外的实现细节。

为了方便内存高效的文本摄取,LineSentence类从提供的文本文件中创建一个生成器,其中包含单独的句子:

sentence_path = Path('data', 'ngrams', f'ngrams_2.txt')
sentences = LineSentence(sentence_path)

Word2vec类提供了先前介绍的配置选项:

model = Word2vec(sentences,
                 sg=1,    # 1=skip-gram; otherwise CBOW
                 hs=0,    # hier. softmax if 1, neg. sampling if 0
                 size=300,      # Vector dimensionality
                 window=3,      # Max dist. btw target and context word
                 min_count=50,  # Ignore words with lower frequency
                 negative=10,  # noise word count for negative sampling
                 workers=8,     # no threads 
                 iter=1,        # no epochs = iterations over corpus
                 alpha=0.025,   # initial learning rate
                 min_alpha=0.0001 # final learning rate
                )

笔记本展示了如何持久化和重新加载模型以继续训练,或者如何将嵌入向量单独存储,例如供 ML 模型使用。

模型评估

基本功能包括识别相似的单词:

model.wv.most_similar(positive=['iphone'], 
                      restrict_vocab=15000)
                 term  similarity
0              android    0.600454
1           smartphone    0.581685
2                  app    0.559129

我们也可以使用正负贡献来验证单个类比:

model.wv.most_similar(positive=['france', 'london'], 
                      negative=['paris'], 
                      restrict_vocab=15000)

             term  similarity
0  united_kingdom    0.606630
1         germany    0.585644
2     netherlands    0.578868

参数设置的性能影响

我们可以使用类比来评估不同参数设置的影响。以下结果表现突出(详细结果请参见models文件夹):

  • 负采样优于分层 softmax,同时训练速度更快。

  • Skip-Gram 架构优于 CBOW 给定的目标函数

  • 不同的min_count设置影响较小,50 为最佳表现的中间值。

使用最佳表现的 SG 模型进行进一步实验,使用负采样和min_count为 50,得到以下结果:

  • 比五更小的上下文窗口会降低性能。

  • 更高的负采样率提高了性能,但训练速度慢了。

  • 更大的向量提高了性能,大小为 600 时的准确率最高,为 38.5%。

使用 Doc2vec 进行情感分析

文本分类需要组合多个词嵌入。一个常见的方法是对文档中每个词的嵌入向量进行平均。这使用所有嵌入的信息,并有效地使用向量加法来到达嵌入空间中的不同位置。但是,有关单词顺序的相关信息会丢失。

相比之下,用于生成文本片段(如段落或产品评论)的嵌入的最先进模型是使用文档嵌入模型Doc2vec。这个模型是 Word2vec 作者在发布其原创贡献后不久开发的。

与 Word2vec 类似,Doc2vec也有两种类型:

  • 分布式词袋DBOW)模型对应于 Word2vec 的 CBOW 模型。文档向量是通过训练网络在预测目标词的合成任务中产生的,该任务基于上下文词向量和文档的文档向量。

  • 分布式记忆DM)模型对应于 Word2vec Skip-Gram 架构。通过训练神经网络来预测目标单词,使用整个文档的文档向量。

Gensim 的 Doc2vec 类实现了这种算法。

在 Yelp 情感数据上训练 Doc2vec

我们使用 500,000 条 Yelp 评论(参见第十三章,处理文本数据)的随机样本,以及它们的相关星级评分(参见 notebook yelp_sentiment):

df = (pd.read_parquet('yelp_reviews.parquet', engine='fastparquet')
          .loc[:, ['stars', 'text']])
stars = range(1, 6)
sample = pd.concat([df[df.stars==s].sample(n=100000) for s in stars])

我们使用简单的预处理来删除停用词和标点符号,使用 NLTK 的分词器并删除少于 10 个标记的评论:

import nltk
nltk.download('stopwords')
from nltk import RegexpTokenizer
from nltk.corpus import stopwords
tokenizer = RegexpTokenizer(r'\w+')
stopword_set = set(stopwords.words('english'))

def clean(review):
    tokens = tokenizer.tokenize(review)
    return ' '.join([t for t in tokens if t not in stopword_set])

sample.text = sample.text.str.lower().apply(clean)
sample = sample[sample.text.str.split().str.len()>10]

创建输入数据

gensim.models.doc2vec 类以 TaggedDocument 格式处理文档,其中包含标记化的文档以及允许在训练后访问文档向量的唯一标记:

sentences = []
for i, (_, text) in enumerate(sample.values):
    sentences.append(TaggedDocument(words=text.split(), tags=[i]))

训练界面与 word2vec 类似,但有额外的参数来指定 Doc2vec 算法:

model = Doc2vec(documents=sentences,
                dm=1,          # algorithm: use distributed memory
                dm_concat=0,   # 1: concat, not sum/avg context vectors
                dbow_words=0,  # 1: train word vectors, 0: only doc 
                                    vectors
                alpha=0.025,   # initial learning rate
                size=300,
                window=5,
                min_count=10,
                epochs=5,
                negative=5)
model.save('test.model')

您还可以使用 train() 方法来继续学习过程,并且,例如,逐渐降低学习率:

for _ in range(10):
    alpha *= .9
    model.train(sentences,
                total_examples=model.corpus_count,
                epochs=model.epochs,
                alpha=alpha)

因此,我们可以将文档向量作为特征来访问,以训练情感分类器:

X = np.zeros(shape=(len(sample), size))
y = sample.stars.sub(1) # model needs [0, 5) labels
for i in range(len(sample)):
 X[i] = model[i]

我们将训练一个 lightgbm 梯度提升机,如下所示:

  1. 从训练集和测试集创建 lightgbm Dataset 对象:
train_data = lgb.Dataset(data=X_train, label=y_train)
test_data = train_data.create_valid(X_test, label=y_test)
  1. 定义具有五个类别的多类模型的训练参数(否则使用默认值):
params = {'objective'  : 'multiclass',
          'num_classes': 5}
  1. 对模型进行 250 次迭代的训练,并监视验证集错误:
lgb_model = lgb.train(params=params,
                      train_set=train_data,
                      num_boost_round=250,
                      valid_sets=[train_data, test_data],
                      verbose_eval=25)
  1. Lightgbm 对所有五个类别进行概率预测。我们使用 np.argmax() 来获取具有最高预测概率的列索引来获取类别预测:
y_pred = np.argmax(lgb_model.predict(X_test), axis=1)
  1. 我们计算准确率评分来评估结果,并看到相对于五个平衡类别的基线 20%,准确率提高了 100% 以上:
accuracy_score(y_true=y_test, y_pred=y_pred)
0.44955063467061984
  1. 最后,我们通过混淆矩阵来更仔细地查看每个类别的预测:
cm = confusion_matrix(y_true=y_test, y_pred=y_pred)
cm = pd.DataFrame(cm / np.sum(cm), index=stars, columns=stars)
  1. 并将结果可视化为 seaborn 热图:
sns.heatmap(cm, annot=True, cmap='Blues', fmt='.1%')

总之,doc2vec 方法使我们能够在不经过太多调整的情况下,比一个简单的基准模型取得了非常大的测试准确率改进。如果我们只选择顶部和底部的评论(分别为五星和一星),并训练一个二元分类器,则使用每个类别的 250,000 个样本时,AUC 分数可以达到 0.86 以上。

附加内容 - 用于翻译的 Word2vec

该 notebook 翻译演示了一个语言中编码的关系通常对应于另一种语言中的类似关系。

它说明了如何使用单词向量通过将单词向量从一个语言的嵌入空间投影到另一个语言的空间使用翻译矩阵来翻译单词和短语。

摘要

本章以词嵌入如何更有效地为个别标记编码语义开始,这比我们在第十三章中使用的词袋模型更为有效。我们还看到了如何通过线性向量算术验证嵌入,以评估是否正确表示了单词之间的语义关系。

学习词嵌入时,我们使用的浅层神经网络曾经在包含数十亿标记的 Web 数据规模上训练速度很慢。word2vec 模型结合了几种算法创新,显著加速了训练,并为文本特征生成确立了新标准。我们看到了如何使用spaCygensim来使用预训练的词向量,并学会了训练自己的词向量嵌入。然后,我们将word2vec模型应用于 SEC 提交文件。最后,我们介绍了doc2vec扩展,它以与单词向量类似的方式学习文档的向量表示,并将其应用于 Yelp 商家评论。

现在,我们将开始第四部分关于深度学习(如前言所述,在线可用),从介绍前馈网络、流行的深度学习框架和大规模高效训练技术开始。

第十六章:下一步

本书的目标是使您能够将机器学习ML)应用于各种数据源,并提取对投资策略的设计和执行有用的信号。为此,我们将机器学习介绍为交易策略过程的重要组成部分。我们看到,机器学习可以在设计、测试、执行和评估策略的多个步骤中添加价值。

显然,机器学习的核心价值主张在于能够比人类专家更系统地从更大量的数据中提取可行动的信息。一方面,这一价值主张随着数字数据的爆炸而变得更具吸引力和必要性,使得利用计算能力进行数据处理变得更有前景。另一方面,机器学习的应用仍然需要人类的重大干预和专业知识来定义目标、选择和筛选数据、设计和优化模型以及适当地利用结果。

在这个总结性的章节中,我们将简要总结本书中学到的关键工具、应用和经验教训,以避免在许多细节之后失去对整体情况的把握。然后,我们将确定我们未涵盖但值得关注的领域,这些领域将有助于您进一步扩展我们介绍的许多机器学习技术,并在日常使用中变得高效。我们将强调对个人生产力有价值的技能组合。

总之,在本章中,我们将讨论以下主题:

  • 回顾主要收获和经验教训,

  • 指出在本书中技术的下一步建设方向,

  • 建议将机器学习纳入您的投资过程中的方法。

主要收获和经验教训

在继续进行交易机器学习实践时要牢记的重要见解包括:

  • 数据是最重要的因素

  • 领域专业知识有助于发现数据的潜在价值,特别是在金融领域

  • 机器学习提供了许多用例的工具,应进一步发展并结合使用数据解决新问题

  • 模型目标的选择和性能诊断对于朝向最佳系统的生产力迭代至关重要

  • 回测过拟合是一个需要重视的巨大挑战

  • 黑盒模型透明化有助于建立信心并促进采用

我们将更详细地阐述这些想法。

数据是最重要的因素

交易和其他领域中机器学习的崛起很大程度上补充了我们详细介绍的数据爆炸。我们在第二章,市场和基本数据中说明了如何访问和处理这些数据源,历来是量化投资的支柱。在第三章,金融替代数据中,我们提出了一个框架,用于评估替代数据潜在价值的标准。

一个关键的见解是,像深度神经网络这样的最新 ML 技术之所以成功,是因为它们的预测性能随着数据的增加而不断提高。另一方面,模型和数据复杂性需要匹配,以平衡偏差-方差的权衡。管理数据质量和整合数据集是实现潜在价值的关键步骤。

质量控制

就像石油一样,这些天很流行的一个比较,数据经过从原始形式到能够推动交易策略的精炼产品的多个阶段的管道。仔细关注最终产品的质量至关重要,以达到预期的里程碑。

有时,您会获得原始数据,并控制所需的大量转换。更常见的情况是,您处理中间产品,并应明确了解此时数据究竟测量了什么。

不同于石油,随着数据源的不断增加,通常没有客观的质量标准。相反,质量取决于其信号内容,而信号内容又取决于您的投资目标。对新数据集的成本效益评估需要一个高效的工作流程,包括我们将在以下部分介绍的适当基础设施。

数据整合

对于投资策略的数据价值往往取决于结合市场、基本和替代数据的互补来源。我们发现,像基于树的集成或神经网络这样的 ML 算法的预测能力部分是由于它们能够检测到非线性关系,特别是变量之间的相互作用效应。

调节变量影响的能力作为其他模型特征的函数在捕获目标结果的不同方面的数据输入上得到了发展。将资产价格与宏观基本面、社会情绪、信用卡支付和卫星数据相结合,可能会在不同的经济和市场环境中产生更可靠的预测,比单独使用每个来源(假设数据足够大以学习隐藏的关系)更可靠。

从多个来源获取数据增加了正确标注的挑战。为了避免通过使用数据来测试算法,在数据实际可用之前就有了预见性偏见,分配准确的时间戳至关重要。例如,数据可能由提供者分配了时间戳,需要调整以反映数据实际可用于实时算法的时间点。

领域专业知识有助于释放数据的价值。

我们强调了数据是成功的机器学习应用的必要驱动因素,但领域专业知识也是重要的,以指导战略方向、特征工程和数据选择、模型设计。

在任何领域,从业者都对关键结果的驱动因素以及它们之间的关系有着理论。金融领域以大量相关的定量研究(理论和实证)脱颖而出。马科斯·洛佩斯·德·普拉多等人(参见 GitHub 的参考资料github.com/PacktPublishing/Hands-On-Machine-Learning-for-Trading)批评了大多数经验结果,因为普遍存在的数据挖掘可能会使研究结果无效。尽管如此,对金融市场运作方式的深刻理解是存在的,并应该指导数据的选择和使用,以及依赖机器学习的策略的正当化。我们在第四章 Alpha Factor Research 和 第五章 Strategy Evaluation 中概述了关键思想。

另一方面,新的机器学习技术可能会发现有关金融结果驱动因素的新假设,这将为机器学习理论提供信息,并应进行独立测试。

特征工程和 alpha 因子研究

与原始数据相比,特征工程通常是使信号对算法有用的关键。利用几十年的风险因素研究成果,从理论和实证的角度来驱动收益是一个很好的起点,以优先考虑更有可能反映相关信息的数据转换。

然而,只有创造性的特征工程才能带来能够随着时间竞争的创新策略。即使对于新的 alpha 因子,一个能够解释它们如何工作的引人注目的叙述,鉴于已有的市场动态和投资者行为理念,将会为资本分配提供更多信心。

假设发现的风险和对历史数据过拟合的风险使得在测试之前优先考虑策略变得更加必要,而不是让数据说话。我们讨论了如何根据实验数量调整夏普比率。

机器学习是解决数据问题的工具包

机器学习提供了可以应用于许多用例的算法解决方案和技术。本书的第 2、3 和 4 部分(如第一章,用于交易的机器学习 中所述)将机器学习作为一套多样化的工具集,可以为策略过程的各个步骤增加价值,包括:

  • 想法生成和阿尔法因子研究,

  • 信号聚合和投资组合优化,

  • 策略测试

  • 交易执行,并且

  • 策略评估

更重要的是,机器学习算法被设计成进一步发展、适应和结合以解决不同环境中的新问题。因此,除了能够将它们应用于数据以进行富有成效的实验和研究外,理解这些算法背后的关键概念和思想也非常重要,如第六章,机器学习过程 中所述。

此外,最佳结果通常是通过将人类专家与机器学习工具结合起来实现的。在第一章,用于交易的机器学习 中,我们介绍了量化投资风格,其中包括定量和算法交易。这种方法可能会变得越来越重要,并且依赖于对我们涵盖的基本工具以及将它们扩展到各种数据集的灵活和创造性应用。

模型诊断有助于加快优化速度

在第六章,机器学习过程 中,我们概述了一些最重要的概念。机器学习算法通过对功能形式做出假设来学习输入数据和目标之间的关系。如果学习基于噪声而不是信号,预测性能将受到影响。

当然,我们今天无法从明天的结果的角度分离信号和噪声。例如,使用学习曲线和优化验证测试的模型诊断可以帮助缓解这一基本挑战,并校准算法的选择或配置以适应手头的数据或任务。通过定义专注的模型目标,以及对于复杂模型,区分由于优化算法或目标本身的问题而导致的性能缺陷,可以使这项任务变得更容易。

不做免费午餐

没有系统——计算机程序或人类——有基础可靠地预测新样本的结果,超出了它在训练期间观察到的范围之外。唯一的出路是具有一些额外的先验知识或做出超出训练样本范围的假设。我们从本书的第七章,线性模型 和 第八章,时间序列模型,到非线性集成模型的广泛范围的算法进行了讨论,如第十章,决策树和随机森林 和 第十一章,梯度提升机,以及本书第四部分的各个章节中的神经网络。

我们看到,线性模型做出了一个强烈的假设,即输入和输出之间的关系具有非常简单的形式,而稍后讨论的模型旨在学习更复杂的函数。虽然显而易见简单模型在大多数情况下会失败,但复杂模型并不总是更好。如果真实关系是线性的,但数据是嘈杂的,复杂模型将会学习噪音作为其假定存在的复杂关系的一部分。这就是“没有免费午餐定理”的基本思想,该定理指出没有一种算法在所有任务中都是普遍优越的。在某些情况下的良好拟合会以在其他情况下的性能不佳为代价。

定制算法选择的关键工具是数据探索和基于对模型做出的假设的理解进行的实验。

管理偏差-方差权衡

适应算法到数据的挑战有一个不同的视角,即偏差和方差之间的权衡,这会导致除了数据的自然噪音之外的预测误差。一个简单的模型如果没有充分捕捉到数据中的关系,将会欠拟合并表现出偏差,即,会产生系统性的错误预测。一个过于复杂的模型会过拟合,并且除了任何信号外还会学习噪声,因此结果会对不同样本显示出很多方差。

在模型选择和优化过程的任何给定迭代中,诊断这种权衡的关键工具是学习曲线。它显示了训练和验证误差如何取决于样本大小。这使我们能够在不同的选项之间做出决定以改善性能:调整模型的复杂性或获取更多数据点。

训练误差越接近人类或其他基准,模型过拟合的可能性就越大。低验证误差告诉我们我们很幸运地找到了一个好模型。如果验证误差很高,我们就没有。然而,如果随着训练大小的增加而持续下降,更多数据可能有所帮助。如果训练误差很高,更多的数据不太可能有所帮助,我们应该增加特征或使用更灵活的算法。

定义目标模型目标

机器学习过程中的第一步之一是为算法定义一个优化的目标。有时,选择很简单,例如在回归问题中。分类任务可能更加困难,例如,当我们关心精确率和召回率时。将相互冲突的目标合并到一个单一指标中,如 F1 得分,有助于聚焦优化工作。我们还可以包括需要满足而不是优化的条件。我们还看到,强化学习完全取决于定义正确的奖励函数来指导代理的学习过程。

优化验证测试

Andrew Ng(请参见 GitHub 上的参考资料:github.com/PacktPublishing/Hands-On-Machine-Learning-for-Algorithmic-Trading)强调了由于学习算法或优化算法的问题而导致性能不佳之间的区别。像神经网络这样的复杂模型假设非线性关系,而优化算法的搜索过程可能会陷入局部而不是全局最优解。

例如,如果一个模型未能正确翻译一个短语,则测试会比较正确预测和搜索算法发现的解决方案的分数。如果学习算法为正确解决方案得分更高,则搜索算法需要改进。否则,学习算法正在优化错误的目标。

警惕反向测试过拟合

我们在整本书中反复讨论了由于过度拟合历史数据而产生的假发现的风险。第五章,策略评估,概述了主要驱动因素和潜在的补救措施。低噪声与信号比和相对较小的数据集(与网络规模图像或文本数据相比)使得这一挑战在交易领域尤为严重。意识至关重要,因为数据和应用 ML 工具的易获取性会将风险指数级增加。

没有逃脱,因为没有预防的方法。然而,我们提出了调整反向测试指标以考虑重复试验的方法,比如通货紧缩夏普比率。在制定实时交易策略时,阶段性纸上交易和在市场执行过程中密切监测绩效需要成为实施过程的一部分。

如何从黑匣子模型中获得见解

深度神经网络和复杂集成模型在被视为不可透明的黑匣子模型时可能会引起怀疑,特别是考虑到反向测试过拟合的风险。我们在第十一章中介绍了几种方法,以了解这些模型如何进行预测,梯度提升机

除了传统的特征重要性度量之外,最近博弈论创新的SHapley Additive exPlanationsSHAP)是理解复杂模型机制的重要一步。SHAP 值允许对特征及其值进行精确归因,从而更容易在特定投资目标的市场行为理论光下验证模型的逻辑。除了理论基础外,确切的特征重要性得分和预测的归因还允许更深入地洞察感兴趣的投资结果的驱动因素。

另一方面,围绕模型预测透明度是否应该成为目标本身存在一些争议。深度学习发明者之一 Geoffrey Hinton 认为,人类决策通常也是模糊的,机器也应该根据其结果进行评估,就像我们期望投资经理一样。

实践中的交易机器学习

当您继续将众多工具和技术整合到您的投资和交易流程中时,有许多事情可以成为您努力的焦点。如果您的目标是做出更好的决策,您应该选择与您当前技能集相符的现实而雄心勃勃的项目。这将帮助您开发一个高效的工作流程,支持您具有生产力的工具,并获得实际经验。

我们将简要列出一些有用的工具,以扩展本书涵盖的 Python 生态系统,并参考 GitHub 上列出的链接进行更深入的探讨。这些包括大数据技术,最终将有必要以规模实现用于交易策略的 ML。我们还将列出一些允许您使用 Python 实施交易策略的平台,通常具有数据源和 ML 算法和库的访问权限。

数据管理技术

数据在 ML 流程中的核心作用要求熟悉一系列技术,以便在规模上存储、转换和分析数据,包括使用云服务,如 Amazon Web Services、Azure 和 Google Cloud。

数据库系统

数据存储意味着使用数据库,历史上由使用 SQL 以商业提供者(如 Oracle 和 Microsoft)和开源实现(如 PostgreSQL 和 MySQL)的明确定义的表格格式存储和检索数据的关系数据库管理系统RDBMS)主导。最近,出现了一些替代方案,通常被集体标记为 NoSQL,但相当多样,即:

  • 键值存储:快速读/写对象。我们在第二章中介绍了 HDF5 格式,市场和基本数据,它有助于快速访问 pandas DataFrame。

  • 列存储:利用列中数据的同质性来实现压缩和更快的基于列的操作,如聚合。在流行的 Amazon Redshift 数据仓库解决方案、Apache Parquet、Cassandra 或 Google 的 Big Table 中使用。

  • 文档存储:设计用于存储不符合关系数据库所需的严格模式定义的数据。由使用 JSON 或 XML 格式的网络应用程序所普及,我们在第四章 Alpha Factor Research 中遇到过。例如,在 MongoDB 中使用。

  • 图数据库:设计用于存储具有节点和边的网络,并专注于网络指标和关系的查询。用于 Neo4J 和 Apache Giraph。

已经有一些转变朝向了关系数据库系统所建立的约定。Python 生态系统促进了与许多标准数据源的交互,并提供了快速的 HDF5 和 Parquet 格式,正如本书中所展示的。

大数据技术 - Hadoop 和 Spark

规模化的数据管理,即数百 GB 甚至更多,需要使用形成集群的多台机器来进行读取、写入和计算操作,并行进行,即分布在各种机器上。

Hadoop 生态系统已经成为一个分布式存储和处理大数据的开源软件框架,使用了 Google 开发的 MapReduce 编程模型。生态系统在 Apache 基金会的框架下变得多样化,并且今天包括了许多项目,涵盖了规模化数据管理的不同方面。Hadoop 中的关键工具包括:

  • Apache Pig:使用 MapReduce 实现大规模 提取-转换-加载 (ETL) 流水线的数据处理语言,由 Yahoo 开发

  • Apache Hive:在 Facebook 开发的交互式 SQL 查询的事实标准,可查询 PB 级数据

  • Apache HBASE:用于实时读/写访问的 NoSQL 数据库,线性扩展到数十亿行和数百万列,并可以使用各种不同的模式组合数据源。

Apache Spark 已成为集群上交互式分析最流行的平台。MapReduce 框架允许并行计算,但需要反复的磁盘读写操作以确保数据冗余。Spark 大大加速了规模化计算,原因是弹性分布式数据RDD)结构,可以进行高度优化的内存计算。这包括了像梯度下降这样的多个 ML 算法所需的迭代计算。

机器学习工具

我们在这本书中涵盖了 Python 生态系统的许多库。Python 已经发展成为数据科学和机器学习的首选语言,一系列开源库不断多样化和成熟,构建在强大的科学计算库 NumPy 和 SciPy 的稳固核心之上。显著促进了 Python 在数据科学领域的使用的流行 pandas 库正在计划其 1.0 版本的发布。scikit-learn 接口已经成为现代机器学习库(如 xgboostlightgbm)的标准,它经常与各种工作流自动化工具(如 GridSearchCVPipeline)交互,我们在整本书中都反复使用了这些工具。

有几个提供机器学习工作流的服务提供商:

  • H2O.ai (www.h2o.ai/) 提供了将云计算与机器学习自动化集成的 H2O 平台。它允许用户将数千个潜在模型与其数据拟合以探索数据中的模式。它在 Python、R 和 Java 中都有接口。

  • DataRobot 旨在通过提供一个快速构建和部署预测模型的平台,来自动化模型开发过程,可在云端或本地部署。

  • Dataiku 是一个协作数据科学平台,旨在帮助分析师和工程师探索、原型、构建和交付自己的数据产品。

还有一些由公司主导的开源倡议,它们在 Python 生态系统上构建和扩展:

  • 定量对冲基金 Two Sigma 在 beakerx 项目下向 Jupyter Notebook 环境贡献了量化分析工具。

  • 彭博社已将 Jupyter Notebook 集成到其终端,以便对其金融数据进行交互式分析。

在线交易平台

开发利用机器学习的交易策略的主要选项是在线平台,这些平台经常寻找并分配资金给成功的交易策略。流行的解决方案包括 Quantopian、Quantconnect、QuantRocket 以及最近关注高频交易的 Alpha Trading Labs。

此外,交互经纪商IB)提供了一个 Python API,您可以使用它来开发自己的交易解决方案。

Quantopian

我们介绍了 Quantopian 平台,并演示了如何使用其研究和交易环境来分析和测试交易策略与历史数据进行对比。Quantopian 使用 Python 并提供大量教育资料。

Quantopian 主办持续每日比赛,以招募算法为其众包对冲基金组合。Quantopian 为获胜算法提供资金支持。在线交易已于 2017 年 9 月停止,但该平台仍提供大量历史数据,并吸引着活跃的开发者和交易者社区,是讨论想法和向他人学习的良好起点。

QuantConnect

QuantConnect 是另一个开源、社区驱动的算法交易平台,与 Quantopian 竞争。它还提供一个 IDE,用于使用 Python 和其他语言进行算法策略的回测和实时交易。

QuantConnect 还拥有来自世界各地的动态全球社区,并提供许多资产类别的访问,包括股票、期货、外汇和加密货币。它提供与各种经纪人(如 IB、OANDA 和 GDAX)的实时交易集成。

QuantRocket

QuantRocket 是一个基于 Python 的平台,用于研究、回测和运行自动化的量化交易策略。它提供数据收集工具、多个数据供应商、研究环境、多个回测器,以及通过 IB 进行实时和模拟交易。它以支持国际股票交易而自豪,并凭借其灵活性脱颖而出。

QuantRocket 支持多个引擎 — 其自己的 Moonshot,以及用户选择的第三方引擎。虽然 QuantRocket 没有传统的集成开发环境(IDE),但与 Jupyter 集成良好,可以产生类似的功能。QuantRocket 在撰写本文时并不免费,定价从 19 美元/月开始。

结论

我们从强调数字数据的爆炸和 ML 作为投资和交易策略的战略能力的出现开始。这种动态反映了金融以外的全球商业和技术趋势,而且很可能会继续下去,而不是停滞或逆转。许多投资公司刚刚开始利用各种人工智能工具,就像个人正在获得相关的技能,业务流程正在适应这些价值创造的新机会一样,如导论章节所述。

未来还有许多令人兴奋的 ML 应用于交易的发展,可能会进一步推动当前的动能。它们可能在未来几年变得相关,并包括 ML 过程的自动化、合成训练数据的生成和量子计算的出现。这一领域的非凡活力意味着这本身就可以填满一本书,并且旅程将继续保持令人兴奋。

posted @ 2024-05-09 17:00  绝不原创的飞龙  阅读(125)  评论(0编辑  收藏  举报