原文:
zh.annas-archive.org/md5/6efde0935976ca50d877b2b5774aeade
译者:飞龙
前言
算法交易帮助你通过量化分析制定策略,从而在市场上保持领先,获取利润并减少损失。本书将帮助你理解金融理论,并自信地执行一系列算法交易策略。
本书首先介绍算法交易、pyfinance 生态系统和 Quantopian。然后你将学习使用 Python 进行算法交易和量化分析,并学习如何在 Quantopian 上构建算法交易策略。随着你的进步,你将深入了解用于分析金融数据集的 Python 库,如 NumPy 和 pandas,并探索 matplotlib、statsmodels 和 scikit-learn 等库进行高级分析。接下来,你将探索有用的金融概念和理论,如金融统计学、杠杆和套期保值以及卖空,这将帮助你了解金融市场的运作方式。最后,你将发现用于分析和理解金融时间序列数据的数学模型和方法。
通过本交易书的学习,你将能够构建预测性交易信号,采用基本和高级算法交易策略,并在 Quantopian 平台上进行组合优化。
本书面向对象
本书适用于想要使用 Python 核心库探索算法交易的数据分析师和金融交易员。如果你正在寻找一本实用指南来执行各种算法交易策略,那么本书适合你。具备 Python 编程和统计学的基本工作知识将会有所帮助。
本书内容概要
第一章,算法交易和 Python 入门,介绍了关键的金融交易概念,并解释了为什么 Python 最适合算法交易。
第二章,Python 中的探索性数据分析,提供了处理任何数据集的第一步骤,即探索性数据分析的概述。
第三章,使用 NumPy 进行高速科学计算,详细介绍了 NumPy,这是一个用于快速和可扩展结构化数组和矢量化计算的库。
第四章,使用 pandas 进行数据操作和分析,介绍了建立在 NumPy 之上的 pandas 库,该库提供了用于结构化 DataFrame 的数据操作和分析方法。
第五章,使用 Matplotlib 进行数据可视化,聚焦于 Python 中的主要可视化库之一,Matplotlib。
第六章,统计估计、推断和预测,讨论了 statsmodels 和 scikit-learn 库,用于高级统计分析技术,时间序列分析技术,以及训练和验证机器学习模型。
第七章,Python 中的金融市场数据访问,描述了 Python 中检索市场数据的替代方法。
第八章,Zipline 和 PyFolio 介绍,涵盖了 Zipline 和 PyFolio,这是 Python 库,它们摆脱了算法交易策略的实际回测和性能/风险分析的复杂性。它们允许您完全专注于交易逻辑。
第九章,基本算法交易策略,介绍了算法策略的概念,以及八种不同的交易算法代表了最常用的算法。
要充分利用本书
按照附录部分的说明,使用存储在书籍 GitHub 存储库中的environment.yml
文件重新创建conda
虚拟环境。一个命令即可还原整个环境。
如果您使用本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库(链接在下一节中提供)访问代码。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Hands-On-Financial-Trading-with-Python
。如果代码有更新,将在现有 GitHub 存储库上进行更新。
我们还提供了来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/
获取。快去看看吧!
下载彩色图片
我们还提供了一份 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:static.packt-cdn.com/downloads/9781838982881_ColorImages.pdf
。
使用的约定
本书中使用了许多文本约定。
文本中的代码
:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个示例:“让我们使用 Python 3.6 创建一个zipline_env
虚拟环境。”
代码块设置如下:
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol
from datetime import datetime
import pytz
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
from . import quandl # noqa
from . import csvdir # noqa
from . import quandl_eod # noqa
粗体:表示一个新术语、一个重要单词或在屏幕上看到的词。例如,菜单或对话框中的单词会出现在文本中,如下所示。这里是一个示例:“然后,在**环境变量…**对话框中指定变量。”
提示或重要说明
看起来像这样。
第一部分:算法交易简介
本节将向您介绍算法交易和 Python 中的重要概念。
本节包括以下章节:
- 第一章*,算法交易和 Python 简介*
第一章:算法交易简介
在本章中,我们将带您走过交易的简要历史,并解释在哪些情况下手动交易和算法交易各自有意义。此外,我们将讨论金融资产类别,这是对不同类型金融资产的分类。您将了解现代电子交易所的组成部分,最后,我们将概述算法交易系统的关键组成部分。
在本章中,我们将涵盖以下主题:
-
穿越算法交易的演变
-
了解金融资产类别
-
穿越现代电子交易所
-
了解算法交易系统的组成部分
穿越算法交易的演变
从交换一种财产换另一种财产的概念自古以来就存在。在其最早期形式中,交易对于交换较不理想的财产以换取较理想的财产很有用。随着时间的流逝,交易演变为参与者试图找到一种以低于公平价值的价格购买和持有交易工具(即产品)的方式,希望能够在未来以高于购买价格的价格出售它们。这个低买高卖的原则成为迄今所有盈利交易的基础;当然,如何实现这一点就是复杂性和竞争的关键所在。
市场由供求基本经济力量驱动。当需求增加而供应没有相应增加,或者供应减少而需求没有减少时,商品变得稀缺并增值(即,其市场价格)。相反,如果需求下降而供应没有减少,或者供应增加而需求没有增加,商品变得更容易获得并且价值更低(市场价格更低)。因此,商品的市场价格应该反映基于现有供应(卖方)和现有需求(买方)的平衡价格。
手动交易方法有许多缺点,如下所示:
-
人类交易员天生处理新市场信息的速度较慢,使他们很可能错过信息或在解释更新的市场数据时出错。这导致了糟糕的交易决策。
-
人类总体上也容易受到分心和偏见的影响,从而降低利润和/或产生损失。例如,对于失去金钱的恐惧和赚钱的喜悦也会导致我们偏离理论上理解但在实践中无法执行的最佳系统化交易方法。此外,人们也天生且非均匀地偏向于盈利交易与亏损交易;例如,人类交易员往往会在盈利交易后迅速增加风险金额,并在亏损交易后减缓降低风险金额的速度。
-
人类交易员通过经历市场条件来学习,例如通过参与和交易实时市场。因此,他们无法从历史市场数据条件中学习和进行回测——这是自动化策略的一个重要优势,我们稍后将会看到。
随着技术的发展,交易已经从通过大声呼喊和示意购买和出售订单进行的交易演变为使用复杂、高效和快速的计算机硬件和软件执行交易,通常几乎没有人为干预。复杂的算法交易软件系统已经取代了人类交易员和工程师,而构建、运行和改进这些系统的数学家,即量化交易员,已经崛起。
特别是,自动化、计算机驱动的系统化/算法交易方法的主要优势如下:
-
计算机非常擅长执行明确定义和重复的基于规则的任务。它们可以非常快速地执行这些任务,并且可以处理大规模的吞吐量。
-
此外,计算机不会分心、疲倦或犯错误(除非存在软件错误,从技术上讲,这算是软件开发者的错误)。
-
算法交易策略在交易过程中也没有情绪上的损失或利润,因此它们可以坚持系统性的交易计划。
所有这些优势使得系统性算法交易成为建立低延迟、高吞吐量、可扩展和稳健交易业务的完美选择。
然而,算法交易并不总是比手动交易更好:
-
手动交易更擅长处理极其复杂的思想和现实交易运营的复杂性,有时很难将其表达为自动化软件解决方案。
-
自动交易系统需要大量的时间和研发成本投入,而手动交易策略通常能更快地进入市场。
-
算法交易策略也容易受到软件开发/操作错误的影响,这可能会对交易业务产生重大影响。整个自动交易操作在几分钟内被清除并不罕见。
-
通常,自动量化交易系统不擅长处理被称为黑天鹅事件的极不可能发生的事件,例如 LTCM 崩盘、2010 年闪崩、Knight Capital 崩盘等。
在本节中,我们了解了交易历史以及何时自动化/算法交易优于手动交易。现在,让我们继续前往下一节,在那里我们将了解被分类为金融资产类别的实际交易主题。
了解金融资产类别
算法交易涉及金融资产的交易。金融资产是一种价值来源于合同协议的非实物资产。
主要的金融资产类别如下:
-
股票(股票):这允许市场参与者直接投资于公司并成为公司的所有者。
-
固定收益(债券):这些代表投资者向借款人(例如政府或公司)提供的贷款。每张债券都有其到期日,到期日时贷款本金应偿还,并且通常由借款人在债券寿命期间支付固定或可变的利息。
-
房地产投资信托(REITs):这些是拥有、经营或融资产生收入的房地产的上市公司。这些可以被用作直接投资于房地产市场的代理,比如通过购买一处房产。
-
大宗商品:例如金属(银、金、铜等)和农产品(小麦、玉米、牛奶等)。它们是跟踪基础大宗商品价格的金融资产。
-
交易所交易基金(ETFs):ETF 是一个在交易所上市的安全性,跟踪其他证券的集合。ETFs,例如 SPY、DIA 和 QQQ,持有股票来跟踪更大型的著名标准普尔 500、道琼斯工业平均指数和纳斯达克股票指数。ETFs,如美国石油基金(USO),通过投资于短期 WTI 原油期货来跟踪油价。ETFs 是投资者以相对较低成本投资于广泛资产类别的便利投资工具。
-
外汇(FX)在不同货币对之间交易,主要货币包括美元(USD)、欧元(EUR)、英镑(GBP)、日元(JPY)、澳大利亚元(AUD)、新西兰元(NZD)、加拿大元(CAD)、瑞士法郎(CHF)、挪威克朗(NOK)和瑞典克朗(SEK)。它们通常被称为 G10 货币。
-
主要的金融衍生品包括期权和期货——这些是复杂的杠杆衍生产品,可以放大风险和回报:
a) 期货是金融合同,以预定的未来日期和价格购买或出售资产。
b) 期权是金融合同,赋予其所有者权利,但不是义务,以在规定的价格(行权价)之前或之后的特定日期买入或卖出基础资产。
在本节中,我们了解了金融资产类别及其独特属性。现在,让我们讨论现代电子交易交易所的订单类型和交易匹配算法。
通过现代电子交易交易所进行交易
第一个交易所是阿姆斯特丹证券交易所,始于 1602 年。在这里,交易是面对面进行的。将技术应用于交易的方式包括使用信鸽、电报系统、莫尔斯电码、电话、计算机终端,以及如今的高速计算机网络和先进的计算机。随着时间的推移,交易微观结构已经发展成为我们今天所熟悉的订单类型和匹配算法。
对于算法策略的设计,现代电子交易所微观结构的了解至关重要。
订单类型
金融交易策略采用各种不同的订单类型,其中一些最常见的包括市价订单、带价格保护的市价订单、立即取消(IOC)订单、填写和取消(FAK)订单、有效至当天(GTD)订单、长效(GTC)订单、止损订单和冰山订单。
对于我们将在本书中探讨的策略,我们将专注于市价订单、IOC 和 GTC。
市价订单
市价订单是需要立即以当前市场价格执行的买入或卖出订单,当执行的即时性优于执行价格时使用。
这些订单将以订单价格执行对立方的所有可用订单,直到要求的所有数量被执行。如果没有可用的流动性可以匹配,它可以被配置为停留在订单簿中或到期。停留在订单簿中意味着订单变为待定订单,被添加到订单簿中供其他参与者进行交易。到期意味着剩余订单数量被取消,而不是被添加到订单簿中,因此新订单无法与剩余数量匹配。
因此,例如,买入市价订单将与订单簿中从最佳价格到最差价格的所有卖出订单匹配,直到整个市价订单被执行。
这些订单可能会遭受极端的滑点,滑点被定义为已执行订单价格与发送订单时市场价格之间的差异。
IOC 订单
IOC 订单无法以比发送价格更差的价格执行,这意味着买入订单无法以高于订单价格的价格执行,卖出订单也无法以低于订单价格的价格执行。这个概念被称为限价,因为价格受限于订单可以执行的最差价格。
IOC 订单将继续与订单方的订单进行匹配,直到出现以下情况之一:
-
IOC 订单的全部数量被执行。
-
对方的被动订单价格比 IOC 订单的价格差。
-
IOC 订单部分执行,剩余数量到期。
如果 IOC 订单的价格优于另一方的最佳可用订单(即,买单低于最佳卖出价,或卖单高于最佳买入价),则根本不会执行,而只会过期。
GTC 订单
GTC 订单可以无限期存在,并需要特定的取消订单。
限价订单簿
交易所接受来自所有市场参与者的订单请求,并将其保存在限价订单簿中。限价订单簿是交易所在任何时间点上所有可见订单的视图。
买单(或竞价)按照从最高价格(即,最佳价格)到最低价格(即,最差价格)的顺序排列,而卖单(即卖出或报价)则按照从最低价格(即,最佳价格)到最高价格(即,最低价格)的顺序排列。
最高竞价价格被认为是最佳竞价价格,因为具有最高买价的买单首先被匹配,而对于卖价,情况相反,即具有最低卖价的卖单首先匹配。
相同方向、相同价格水平的订单按照先进先出(FIFO)的顺序排列,也被称为优先顺序 - 优先级更高的订单排在优先级较低的订单前面,因为优先级更高的订单比其他订单先到达了交易所。其他条件相同(即,订单方向、价格和数量相同)的情况下,优先级更高的订单将在优先级较低的订单之前执行。
交易所撮合引擎
电子交易所的撮合引擎使用交易所撮合算法执行订单的撮合。撮合过程包括检查市场参与者输入的所有活跃订单,并将价格交叉的订单进行匹配,直到没有可以匹配的未匹配订单为止 - 因此,价格在或高于其他卖单的买单与之匹配,反之亦然,即价格在或低于其他买单的卖单与之匹配。剩余订单保留在交易所撮合簿中,直到新的订单流入,如果可能的话,进行新的匹配。
在 FIFO 匹配算法中,订单首先按照价格从最佳价格到最差价格进行匹配。因此,来自最佳价格的买单会尝试与摆放在最低价格到最高价格的卖单(即要价/出价)匹配,而来自最高价格的卖单会尝试与摆放在最高价格到最低价格的买单匹配。新到达的订单将根据特定的规则进行匹配。对于具有更好价格的主动订单(价格优于另一侧的最佳价格水平的订单),它们将按照先到先服务的原则进行匹配,即首先出现的订单会提取流动性,因此首先匹配。对于坐在订单簿中的被动挂单,因为它们不会立即执行,它们将根据先到先服务的优先级进行分配。这意味着同一方和相同价格的订单将根据它们到达匹配引擎的时间进行排列;时间较早的订单将获得更好的优先级,因此有资格首先匹配。
在本节中,我们了解了现代电子交易所的订单类型和交易匹配引擎。现在,让我们继续前往下一节,我们将了解算法交易系统的组件。
了解算法交易系统的组件
客户端算法交易基础设施大致可以分为两类:核心基础设施和量化基础设施。
算法交易系统的核心基础设施
核心基础设施负责使用市场数据和订单输入协议与交易所进行通信。它负责在交易所和算法交易策略之间传递信息。
它的组件还负责捕获、时间戳和记录历史市场数据,这是算法交易策略研究和开发的重中之重。
核心基础设施还包括一层风险管理组件,以防止交易系统受到错误或失控的交易策略的影响,以防止灾难性结果发生。
最后,算法交易业务中涉及的一些不太光彩的任务,如后勤协调任务、合规性等,也由核心基础设施处理。
交易服务器
交易服务器涉及一个或多个计算机接收和处理市场和其他相关数据,并处理交易所信息(例如订单簿),并发出交易订单。
从限价订单簿中,交易所匹配簿的更新通过市场数据协议传播给所有市场参与者。
市场参与者拥有接收这些市场数据更新的交易服务器。尽管技术上,这些交易服务器可以位于世界任何地方,但现代算法交易参与者将其交易服务器放置在离交易所匹配引擎非常近的数据中心。这称为共同定位或直接市场访问(DMA)设置,这保证参与者尽可能快地收到市场数据更新,因为它们尽可能接近匹配引擎。
一旦市场数据更新通过交易所提供的市场数据协议通信到每个市场参与者,它们就使用称为市场数据接收处理程序的软件应用程序解码市场数据更新并将其馈送到客户端上的算法交易策略。
一旦算法交易策略消化了市场数据更新,根据策略中开发的智能,它生成外向订单流。这可以是在特定价格和数量上添加、修改或取消订单。
订单请求通常由一个名为订单录入网关的单独客户端组件接收。订单录入网关组件使用订单录入协议与交易所通信,将策略对交易所的请求进行转换。电子交易所对这些订单请求的响应通知被发送回订单录入网关。再次,针对特定市场参与者的订单流动,匹配引擎生成市场数据更新,因此回到此信息流循环的开头。
算法交易系统的量化基础设施
量化基础设施构建在核心基础设施提供的平台之上,并尝试在其上构建组件,以研究、开发和有效利用平台以产生收入。
研究框架包括回测、交易后分析(PTA)和信号研究组件等组件。
其他在研究中使用并部署到实时市场的组件包括限价订单簿、预测信号和信号聚合器,将单个信号组合成复合信号。
执行逻辑组件使用交易信号并完成管理活动,管理各种策略和交易工具之间的活动订单、持仓和损益(PnL)。
最后,交易策略本身有一个风险管理组件,用于管理和减轻不同策略和工具之间的风险。
交易策略
有利可图的交易理念始终是由人类直觉驱动的,这种直觉是从观察市场条件的模式和不同市场条件下各种策略的结果中发展起来的。
例如,历史上观察到,大规模的市场上涨会增强投资者信心,导致更多的市场参与者进入市场购买更多;因此,反复造成更大规模的上涨。相反,市场价格大幅下跌会吓跑投资于交易工具的参与者,导致他们抛售持有的资产并加剧价格下跌。市场观察到的这些直观观念导致了趋势跟随策略的想法。
还观察到,短期内价格的波动往往倾向于恢复到其之前的市场价格,导致了均值回归为基础的投机者和交易策略。同样,历史观察到类似产品价格的移动会相互影响,这也是直觉的合理性所在,这导致了相关性和共线性为基础的交易策略的产生,如统计套利和配对交易策略。
由于每个市场参与者使用不同的交易策略,最终的市场价格反映了大多数市场参与者的观点。与大多数市场参与者观点一致的交易策略在这些条件下是有利可图的。单一的交易策略通常不可能 100%的盈利,所以复杂的参与者有一个交易策略组合。
交易信号
交易信号也被称为特征、计算器、指标、预测器或阿尔法。
交易信号是驱动算法交易策略决策的因素。信号是从市场数据、另类数据(如新闻、社交媒体动态等)甚至我们自己的订单流中获得的明确的情报,旨在预测未来某些市场条件。
信号几乎总是源自对某些市场条件和/或策略表现的直觉观察。通常,大多数量化开发人员花费大部分时间研究和开发新的交易信号,以改善在不同市场条件下的盈利能力,并全面提高算法交易策略。
交易信号研究框架
大量的人力投入到研究和发现新信号以改善交易表现。为了以系统化、高效、可扩展和科学的方式做到这一点,通常第一步是建立一个良好的信号研究框架。
这个框架有以下子组件:
-
数据生成是基于我们试图构建的信号和我们试图捕捉/预测的市场条件/目标。在大多数现实世界的算法交易中,我们使用 tick 数据,这是代表市场上每个事件的数据。正如你可以想象的那样,每天都会有大量的事件发生,这导致了大量的数据,因此您还需要考虑对接收到的数据进行子抽样。子抽样有几个优点,例如减少数据规模,消除噪音/虚假数据片段,并突出显示有趣/重要的数据。
-
对与其尝试捕捉/预测的市场目标相关的特征的预测能力或有用性进行评估。
-
在不同市场条件下维护信号的历史结果,并调整现有信号以适应不断变化的市场条件。
信号聚合器
信号聚合器是可选组件,它们从各个信号中获取输入,并以不同的方式对其进行聚合,以生成新的复合信号。
一个非常简单的聚合方法是取所有输入信号的平均值,并将平均值作为复合信号值输出。
熟悉统计学习概念的读者 - bagging 和 boosting 的集成学习 - 可能能够发现这些学习模型与信号聚合器之间的相似之处。通常,信号聚合器只是统计模型(回归/分类),其中输入信号只是用于预测相同最终市场目标的特征。
策略执行
策略执行涉及根据交易信号的输出有效地管理和执行订单,以最小化交易费用和滑点。
滑点是市场价格和执行价格之间的差异,由于订单经历了延迟才能到达市场,价格在变化之前发生了变化,以及订单的大小在达到市场后引起价格变化所致。
在算法交易策略中使用的执行策略的质量可以显著改善/降低有利交易信号的表现。
限价订单簿
限价订单簿既在交易所撮合引擎中构建,也在算法交易策略期间构建,尽管并不一定所有算法交易信号/策略都需要整个限价订单簿。
复杂的算法交易策略可以将更多的智能集成到其限价订单簿中。我们可以在限价订单簿中检测和跟踪自己的订单,并了解根据我们的优先级,我们的订单被执行的概率是多少。我们还可以利用这些信息在交易所的订单录入网关收到执行通知之前甚至执行我们自己的订单,并利用这种能力为我们谋利。通过限价订单簿和许多电子交易所的市场数据更新,还可以实现更复杂的微观结构特征,例如检测冰山订单、检测止损订单、检测大量买入/卖出订单流入或流出等。
头寸和损益管理
让我们探讨交易策略通过执行交易开仓和平仓多头和空头头寸时,头寸和损益如何演变。
当策略没有市场头寸时,即价格变动不影响交易账户价值时,这被称为持平头寸。
从持平头寸开始,如果执行买单,则被称为持有多头头寸。如果策略持有多头头寸且价格上涨,则头寸从价格上涨中获利。在这种情况下,损益也增加,即利润增加(或亏损减少)。相反,如果策略持有多头头寸且价格下跌,则头寸从价格下跌中损失。在这种情况下,损益减少,例如,利润减少(或亏损增加)。
从持平头寸开始,如果执行卖单,则被称为持有空头头寸。如果策略持有空头头寸且价格下跌,则头寸从价格下跌中获利。在这种情况下,损益增加。相反,如果策略持有空头头寸且价格上涨,则损益减少。仍然未平仓头寸的损益被称为未实现损益(unrealized PnL),因为只要头寸保持未平仓状态,损益就会随着价格变动而变化。
通过卖出等量的工具来关闭多头头寸。这被称为平仓,此时损益被称为实现损益(realized PnL),因为价格变动不再影响损益,因为头寸已关闭。
类似地,空头头寸通过买入与头寸规模相同的数量来关闭。
在任何时刻,**总损益(total PnL)**是所有已平仓头寸的实现损益和所有未平仓头寸的未实现损益的总和。
当多头或空头头寸由以不同价格和不同大小进行多次买入或卖出时,则通过计算**成交量加权平均价格(Volume Weighted Average Price,VWAP)**来计算头寸的平均价格,即根据每个价格上执行的数量加权平均。按市价计价是指获取头寸的 VWAP,并将其与当前市场价格进行比较,以了解某个多头/空头头寸的盈利或亏损情况。
回测
回测器使用历史记录的市场数据和模拟组件来模拟算法交易策略的行为和性能,就好像它在过去被部署到实时市场中一样。直到策略的表现符合预期,才会开发和优化算法交易策略。
回测器是需要模拟市场数据流、客户端和交易所端延迟的复杂组件在软件和网络组件中、准确的 FIFO 优先级、滑点、费用和市场影响来自策略订单流(即其他市场参与者将如何对策略的订单流作出反应添加到市场数据流)以生成准确的策略和投资组合绩效统计数据。
PTA
PTA 是在模拟或实时市场运行的算法交易策略生成的交易上执行的。
PTA 系统用于从历史回测策略生成性能统计,目的是了解历史策略性能期望。
当应用于由实时交易策略生成的交易时,PTA 可用于了解实时市场中的策略性能,并比较和确认实时交易性能是否符合模拟策略性能期望。
风险管理
良好的风险管理原则确保策略以最佳 PnL 表现运行,并采取措施防止失控 / 错误策略。
不良的风险管理不仅可以将有利可图的交易策略变成无利可图的策略,而且还可能由于无法控制的策略损失、失灵的策略和可能的监管后果而使投资者的整个资本面临风险。
概要
在本章中,我们学习了什么时候算法交易比手动交易更具优势,金融资产类别是什么,最常用的订单类型是什么,限价订单簿是什么,以及订单是如何由金融交易所匹配的。
我们还讨论了算法交易系统的关键组成部分 - 核心基础设施和量化基础设施,包括交易策略、执行、限价订单簿、持仓、PnL 管理、回测、交易后分析和风险管理。
在下一章中,我们将讨论 Python 在算法交易中的价值。
第二部分:深入了解用于金融数据集分析的 Python 库
本节将深入介绍核心 Python 库 NumPy 和 pandas,这些库用于分析和操作大型数据框。我们还将涵盖与 pandas 密切相关的可视化库 Matplotlib。最后,我们将介绍 statsmodels 和 scikit-learn 库,这些库允许更高级的金融数据集分析。
本节包括以下章节:
-
第二章*, Python 中的探索性数据分析*
-
第三章*, 使用 NumPy 进行高速科学计算*
-
第四章*, 使用 Pandas 进行数据操作和分析*
-
第五章*, 使用 Matplotlib 进行数据可视化*
-
第六章*, 统计估计、推断和预测*
第二章:Python 中的探索性数据分析
本章重点介绍探索性数据分析(EDA),这是处理任何数据集的第一步。EDA 的目标是将数据加载到最适合进一步分析的数据结构中,以识别和纠正任何错误/坏数据,并获得对数据的基本见解——字段的类型有哪些;它们是否是分类的;有多少缺失值;字段之间的关系等等。
这些是本章讨论的主要话题:
-
EDA 介绍
-
用于 EDA 的特殊 Python 库
技术要求
本章中使用的 Python 代码可以在书的代码库中的Chapter02/eda.ipynb
笔记本中找到。
EDA 介绍
EDA 是从感兴趣的结构化/非结构化数据中获取、理解和得出有意义的统计见解的过程。这是在对数据进行更复杂的分析之前的第一步,例如从数据中预测未来的期望。在金融数据的情况下,EDA 有助于获得后续用于构建盈利交易信号和策略的见解。
EDA 指导后续决策,包括使用或避免哪些特征/信号,使用或避免哪些预测模型,并验证和引入关于变量性质和它们之间关系的正确假设,同时否定不正确的假设。
EDA 也很重要,可以理解样本(完整数据集的代表性较小数据集)统计数据与总体(完整数据集或终极真相)统计数据之间的差异,并在绘制关于总体的结论时记住这一点,基于样本观察。因此,EDA 有助于减少后续可能的搜索空间;否则,我们将浪费更多的时间后来构建不正确/不重要的模型或策略。
必须以科学的心态来对待 EDA。有时,我们可能会基于轶事证据而不是统计证据得出不充分验证的结论。
基于轶事证据的假设受到以下问题的影响:
-
不具有统计学意义——观测数量太少。
-
选择偏见——假设只是因为它首先被观察到而产生的。
-
确认偏见——我们对假设的内在信念会偏向于我们的结果。
-
观察中的不准确性。
让我们探索 EDA 涉及的不同步骤和技术,使用真实数据集。
EDA 的步骤
以下是 EDA 涉及的步骤列表(我们将在接下来的子章节中逐个进行讨论):
-
加载必要的库并进行设置
-
数据收集
-
数据整理/整理
-
数据清洗
-
获得描述性统计
-
数据的可视化检查
-
数据清洗
-
高级可视化技术
加载必要的库并进行设置
我们将使用numpy
、pandas
和matplotlib
,这些库可以通过以下代码加载:
%matplotlib inline
import numpy as np
import pandas as pd
from scipy import stats
import seaborn as sn
import matplotlib.pyplot as plt
import mpld3
mpld3.enable_notebook()
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max_rows', 2)
我们使用mpld3
库来启用 Jupyter 的matplotlib
图表内的缩放。 前面代码块的最后一行指定了应显示pandas
DataFrame 的最大行数为两行。
数据收集
数据收集通常是 EDA 的第一步。 数据可能来自许多不同的来源(逗号分隔值(CSV)文件、Excel 文件、网页抓取、二进制文件等),通常需要正确标准化和首先正确格式化在一起。
对于这个练习,我们将使用存储在.csv
格式中的 5 年期间的三种不同交易工具的数据。 这些工具的身份故意没有透露,因为这可能泄露它们的预期行为/关系,但我们将在练习结束时透露它们的身份,以直观地评估我们对它们进行的 EDA 的表现如何。
让我们从加载我们可用的数据集开始,将其加载到三个 DataFrame(A
,B
和C
)中,如下所示:
A = pd.read_csv('A.csv', parse_dates=True, index_col=0);
A
DataFrame A
的结构如下所示:
图 2.1 – 从 A.csv 文件构造的 DataFrame
类似地,让我们加载 DataFrame B
,如下所示:
B = pd.read_csv('B.csv', parse_dates=True, index_col=0);
B
DataFrame B
的结构如下所示:
图 2.2 – 从 B.csv 文件构造的 DataFrame
最后,让我们将C
数据加载到一个 DataFrame 中,如下所示:
C = pd.read_csv('C.csv', parse_dates=True, index_col=0);
C
我们看到C
有以下字段:
图 2.3 – 从 C.csv 文件构造的 DataFrame
如我们所见,所有三个数据源的格式都是2015-05-15
和2020-05-14
。
数据整理/处理
数据很少是以可直接使用的格式提供的。 数据整理/处理指的是从初始原始来源操纵和转换数据的过程,使其成为结构化的、格式化的和易于使用的数据集。
让我们使用pandas.DataFrame.join(...)
来合并这些 DataFrame,并对齐它们以具有相同的DateTimeIndex
格式。 使用lsuffix=
和rsuffix=
参数,我们将_A
,_B
和_C
后缀分配给来自三个 DataFrame 的列,如下所示:
merged_df = A.join(B, how='outer', lsuffix='_A', sort=True).join(C, how='outer', lsuffix='_B', rsuffix='_C', sort=True)
merged_df
我们将检查我们刚刚创建的merged_df
DataFrame,并确保它具有我们从所有三个 DataFrame 中预期的所有字段(仅显示前七列)。 DataFrame 可以在这里看到:
图 2.4 – 通过合并 DataFrame A、B 和 C 构造的 DataFrame
请注意,原始三个数据框(A
、B
和 C
)分别有 1,211、1,209 和 1,206 行,但合并后的数据框有 1,259 行。这是因为我们使用了外部连接,它使用了所有三个数据框的日期的并集。当它在特定日期的特定数据框中找不到值时,它会将该数据框的字段的那个位置放置一个 NaN
值。
数据清洗
数据清洗是指处理来自缺失数据、不正确数据值和异常值的数据错误的过程。
在我们的示例中,merged_df
的许多字段都缺失原始数据集和不同日期数据框合并而来的字段。
让我们首先检查是否存在所有值都缺失(NaN
)的行,如下所示:
merged_df[merged_df.isnull().all(axis=1)]
结果表明,我们没有任何所有字段都缺失的行,如我们所见:
图 2.5 – DataFrame 表明没有所有字段都缺失的行。
现在,让我们找出有多少行存在至少一个字段缺失/NaN
的,如下所示:
merged_df[['Close_A', 'Close_B', 'Close_C']].isnull().any(axis=1).sum()
因此,结果显示我们的 1,259 行中有 148 行具有一个或多个字段缺失值,如下所示:
148
对于我们的进一步分析,我们需要有效的 Close
价格。因此,我们可以通过运行以下代码删除所有三个工具的任何 Close
价格缺失的行:
valid_close_df = merged_df.dropna(subset=['Close_A', 'Close_B', 'Close_C'], how='any')
删除缺失的 Close
价格后,我们不应该再有缺失的 Close
价格字段,如下代码段所示:
valid_close_df[['Close_A', 'Close_B', 'Close_C']].isnull().any(axis=1).sum()
结果证实,不再存在任何 Close_A
、Close_B
或 Close_C
字段为 NaN
值的行,如我们所见:
0
让我们检查新的 DataFrame,如下所示:
valid_close_df
这是结果(仅显示前七列):
图 2.6 – 没有任何收盘价缺失/NaN 值的结果 DataFrame
如预期的那样,我们删除了具有任何收盘价缺失/NaN
值的 148 行。
接下来,让我们处理任何其他字段具有 NaN
值的行,首先了解有多少这样的行。我们可以通过运行以下代码来做到这一点:
valid_close_df.isnull().any(axis=1).sum()
这是该查询的输出:
165
因此,存在 165 行至少有一些字段缺失值。
让我们快速检查一下至少有一些字段缺失值的几行,如下所示:
valid_close_df[valid_close_df.isnull().any(axis=1)]
以下显示了一些具有一些缺失值的行(仅显示前七列),如下所示:
图 2.7 – DataFrame 表明仍然有一些行存在一些缺失值
因此,我们可以看到 2015-05-18
(在前述截屏中不可见)的 Low_C
字段和 2020-05-01
的 Open_B
字段有 NaN
值(当然还有其他 163 个)。
让我们使用 pandas.DataFrame.fillna(...)
方法与一种称为 backfill
的方法 —— 这使用缺失值后的下一个有效值来填充缺失值。代码如下所示:
valid_close_complete = valid_close_df.fillna(method='backfill')
让我们看看 backfilling 的影响,如下所示:
valid_close_complete.isnull().any(axis=1).sum()
现在,这是查询的输出:
0
正如我们所看到的,在进行 backfill
操作之后,任何行的任何字段都不再有缺失或 NaN
值。
获取描述性统计数据
下一步是生成数据的关键基本统计信息,以便熟悉每个字段,使用 DataFrame.describe(...)
方法。代码如下所示:
pd.set_option('display.max_rows', None)
valid_close_complete.describe()
请注意,我们已经增加了要显示的 pandas
DataFrame 的行数。
这是运行 pandas.DataFrame.describe(…)
后的输出,仅显示了前七列:
图 2.8 – 有效关闭完整 DataFrame 的描述统计
前面的输出为我们的 DataFrame 中的每个字段提供了快速摘要统计信息。
从 图 2.8 的关键观察点可以总结如下:
-
Volume_C
的所有统计值都为0
,这意味着每一行的Volume_C
值都设置为0
。因此,我们需要移除此列。 -
Open_C
的最小值为-400
,这不太可能是真实的,原因如下:a) 其他价格字段 ——
High_C
、Low_C
、Close_C
和Adj Close_C
—— 的所有最小值都约为9
,因此Open_C
具有-400
的最小值是没有意义的。b) 考虑到
Open_C
的第 25 个百分位数为12.4
,其最小值不太可能远低于此。c) 资产的价格应为非负数。
-
Low_C
的最大值为330
,这同样不太可能,原因如下:a) 出于先前所述的相同原因,
Open_C
是不正确的。b) 此外,考虑到
Low_C
应始终低于High_C
,根据定义,一天中的最低价格必须低于当天的最高价格。
让我们将所有 pandas
DataFrame 的输出恢复为只有两行,如下所示:
pd.set_option('display.max_rows', 2)
现在,让我们移除所有三个工具的 Volume
字段,使用以下代码:
prices_only = valid_close_complete.drop(['Volume_A', 'Volume_B', 'Volume_C'], axis=1)
prices_only
而 prices_only
DataFrame 具有以下数据(仅显示前七列):
图 2.9 – 仅价格的 DataFrame
预期之中的是,我们移除了三个工具的交易量列之后,将 DataFrame 维度减少到 1111 × 15
—— 这些以前是 1111 × 18
。
数据的视觉检查
似乎没有任何明显的错误或不一致之处,所以让我们快速可视化价格,看看这是否符合我们从描述性统计中学到的内容。
首先,我们将从A
的价格开始,因为根据描述性统计摘要,我们期望这些是正确的。代码如下所示:
valid_close_complete['Open_A'].plot(figsize=(12,6), linestyle='--', color='black', legend='Open_A')
valid_close_complete['Close_A'].plot(figsize=(12,6), linestyle='-', color='grey', legend='Close_A')
valid_close_complete['Low_A'].plot(figsize=(12,6), linestyle=':', color='black', legend='Low_A')
valid_close_complete['High_A'].plot(figsize=(12,6), linestyle='-.', color='grey', legend='High_A')
输出与我们的期望一致,我们可以根据统计数据和下面截图中显示的图表得出A
的价格是有效的结论:
图 2.10 – 展示了交易工具 A 的开盘价、收盘价、最高价和最低价在 5 年内的价格
现在,让我们绘制 C 的价格图,看看图表是否提供了关于我们怀疑某些价格不正确的进一步证据。代码如下所示:
valid_close_complete['Open_C'].plot(figsize=(12,6), linestyle='--', color='black', legend='Open_C')
valid_close_complete['Close_C'].plot(figsize=(12,6), linestyle='-', color='grey', legend='Close_C')
valid_close_complete['Low_C'].plot(figsize=(12,6), linestyle=':', color='black', legend='Low_C')
valid_close_complete['High_C'].plot(figsize=(12,6), linestyle='-.', color='grey', legend='High_C')
输出证实了Open_C
和Low_C
具有一些与其他值极端相距甚远的错误值—这些是异常值。下面的截图显示了说明这一点的图表:
图 2.11 – 展示了 C 价格中正负两个方向的大异常值的图表
我们需要进行一些进一步的数据清理,以消除这些异常值,以便我们不从数据中得出不正确的统计见解。
检测和移除异常值最常用的两种方法是四分位数范围(IQR)和 Z 分数。
IQR
IQR 方法使用整个数据集上的百分位数/分位数值范围来识别和移除异常值。
在应用 IQR 方法时,我们通常使用极端百分位数值,例如 5% 到 95%,以最小化移除正确数据点的风险。
在我们的Open_C
示例中,让我们使用第 25 百分位数和第 75 百分位数,并移除所有数值超出该范围的数据点。第 25 到 75 百分位数范围是(12.4, 17.68
),因此我们将移除异常值-400
。
Z 分数
Z 分数(或标准分数)是通过从数据集中减去每个数据点的均值,并通过数据集的标准偏差进行归一化得到的。
换句话说,数据点的 Z 分数表示数据点与所有数据点的均值之间的标准偏差距离。
对于正态分布(适用于足够大的数据集),有一个68-95-99的分布规则,总结如下:
-
所有数据的 68% 将落在距离均值一个标准差的范围内。
-
所有数据的 95% 将落在距离均值两个标准差的范围内。
-
所有数据的 99% 将落在距离均值三个标准差的范围内。
因此,在计算了数据集中所有数据点的 Z 得分(足够大的数据集)之后,存在着大约 1% 的数据点具有 Z 得分大于或等于3
的概率。
因此,我们可以利用这些信息筛选出所有 Z 得分为3
或更高的观察结果以检测并移除异常值。
在我们的示例中,我们将删除所有 Z 得分小于-6
或大于6
的值的行—即,与平均值相差六个标准偏差。
首先,我们使用scipy.stats.zscore(...)
计算prices_only
DataFrame 中每列的 Z 得分,然后我们使用numpy.abs(...)
获取 Z 得分的大小。最后,我们选择所有字段的 Z 得分低于 6 的行,并将其保存在no_outlier_prices
DataFrame 中。代码如下所示:
no_outlier_prices = prices_only[(np.abs(stats.zscore(prices_only)) < 6).all(axis=1)]
让我们看看这个 Z 得分异常值移除代码对仪器C
的价格字段产生了什么影响,通过重新绘制其价格并与之前的图进行比较,如下所示:
no_outlier_prices['Open_C'].plot(figsize=(12,6), linestyle='--', color='black', legend='Open_C')
no_outlier_prices['Close_C'].plot(figsize=(12,6), linestyle='-', color='grey', legend='Close_C')
no_outlier_prices['Low_C'].plot(figsize=(12,6), linestyle=':', color='black', legend='Low_C')
no_outlier_prices['High_C'].plot(figsize=(12,6), linestyle='-.', color='grey', legend='High_C')
这是输出:
图 2.12 – 应用数据清理移除异常值后显示 C 的价格的绘图
绘图清楚地显示了Open_C
和Low_C
的早期极端值观察已被丢弃;不再有-400
的低谷。
请注意,虽然我们移除了极端异常值,但我们仍然能够保留 2015 年、2018 年和 2020 年价格的剧烈波动,因此并没有导致大量数据损失。
我们还要通过重新检查描述性统计数据来检查我们的异常值移除工作的影响,如下所示:
pd.set_option('display.max_rows', None)
no_outlier_prices[['Open_C', 'Close_C', 'Low_C', 'High_C']].describe()
这些统计数据看起来明显更好—正如我们在以下截图中看到的,所有价格的min
和max
值现在看起来符合预期,并且没有极端值,所以我们在数据清理任务中取得了成功:
图 2.13 – 选择的列的无异常价格的描述性统计
让我们将要显示的pandas
DataFrame 的行数重置回来,如下所示:
pd.set_option('display.max_rows', 5)
高级可视化技术
在本节中,我们将探讨一元和多元统计可视化技术。
首先,让我们收集三个工具的收盘价格,如下所
close_prices = no_outlier_prices[['Close_A', 'Close_B', 'Close_C']]
接下来,让我们计算每日收盘价格变动,以评估三个工具之间的每日价格变动是否存在关系。
每日收盘价格变动
我们将使用 pandas.DataFrame.shift(...)
方法将原始 DataFrame 向前移动一个周期,以便我们可以计算价格变动。这里的 pandas.DataFrame.fillna(...)
方法修复了由于 shift
操作而在第一行生成的一个缺失值。最后,我们将列重命名为 Delta_Close_A
、Delta_Close_B
和 Delta_Close_C
,以反映这些值是价格差异而不是实际价格。以下是代码示例:
delta_close_prices = (close_prices.shift(-1) - close_prices).fillna(0)
delta_close_prices.columns = ['Delta_Close_A', 'Delta_Close_B', 'Delta_Close_C']
delta_close_prices
新生成的 delta_close_prices
DataFrame 的内容如下截图所示:
图 2.14 – delta_close_prices DataFrame
从前几个实际价格和计算出的价格差异来看,这些值看起来是正确的。
现在,让我们快速检查这个新 DataFrame 的摘要统计信息,以了解价格差值的分布情况,如下所示:
pd.set_option('display.max_rows', None)
delta_close_prices.describe()
这个 DataFrame 的描述性统计如下所示截图所示:
图 2.15 – delta_close_prices DataFrame 的描述性统计
我们可以从这些统计数据中观察到,所有三个 delta 值的均值都接近于 0,仪器 A
经历了大幅价格波动,而仪器 C
则经历了明显较小的价格波动(来自 std
字段)。
直方图
让我们观察 Delta_Close_A
的分布,以更加熟悉它,使用直方图绘制。以下是代码示例:
delta_close_prices['Delta_Close_A'].plot(kind='hist', bins=100, figsize=(12,6), color='black', grid=True)
在下面的截图中,我们可以看到分布大致呈正态分布:
图 2.16 – Delta_Close_A 值的直方图大致呈正态分布,围绕着 0 值
箱线图
让我们绘制一个箱线图,这也有助于评估值的分布。以下是相应代码的示例:
delta_close_prices['Delta_Close_B'].plot(kind='box', figsize=(12,6), color='black', grid=True)
输出结果如下截图所示:
图 2.17 – 箱线图显示均值、中位数、四分位距(25th 到 75th 百分位数)和异常值
相关性图表
多元数据统计的第一步是评估 Delta_Close_A
、Delta_Close_B
和 Delta_Close_C
之间的相关性。
最方便的方法是绘制一个相关性散点矩阵,显示三个变量之间的成对关系,以及每个单独变量的分布。
在我们的示例中,我们演示了使用核密度估计(KDE)的选项,这与直方图密切相关,但在对角线上的图中提供了更平滑的分布表面。其代码如下所示:
pd.plotting.scatter_matrix(delta_close_prices, figsize=(10,10), color='black', alpha=0.75, diagonal='kde', grid=True)
这个图表表明,Delta_Close_A
和 Delta_Close_B
之间存在强烈的正相关性,以及 Delta_Close_C
与另外两个变量之间存在强烈的负相关性。对角线也显示了每个单独变量的分布,使用了 KDE。
下面是字段的散点图:
图 2.18 – Delta_Close 字段的散点图,对角线上是 KDE 直方图
接下来,让我们看一些提供变量之间关系的统计数据。DataFrame.corr(...)
为我们完成了这项工作,并且还显示了线性相关性。这可以在以下代码片段中看到:
delta_close_prices.corr()
相关矩阵证实了 Delta_Close_A
和 Delta_Close_B
之间存在强烈的正相关性(非常接近 1.0,这是最大值),这符合我们根据散点图的预期。此外,Delta_Close_C
与其他两个变量呈负相关(接近 -1.0 而不是 0.0)。
您可以在以下屏幕截图中看到相关矩阵:
图 2.19 – Delta_Close_A、Delta_Close_B 和 Delta_Close_C 的相关矩阵
成对相关热图
一种称为 seaborn.heatmap(...)
的替代可视化技术,如下面的代码片段所示:
plt.figure(figsize=(6,6))
sn.heatmap(delta_close_prices.corr(), annot=True, square=True, linewidths=2)
在下面的屏幕截图中显示的图中,最右侧的刻度显示了一个图例,其中最暗的值代表最强的负相关,最浅的值代表最强的正相关:
图 2.20 – Seaborn 热图可视化 Delta_Close 字段之间的成对相关性
热图在图表中以图形方式显示了前一节中的表格信息 —— Delta_Close_A
和 Delta_Close_B
之间存在非常高的相关性,而 Delta_Close_A
和 Delta_Close_C
之间存在非常高的负相关性。Delta_Close_B
和 Delta_Close_C
之间也存在非常高的负相关性。
A、B 和 C 的身份揭示以及 EDA 的结论
A
仪器是 B
仪器是 C
仪器是芝加哥期权交易所(CBOE)波动率指数(VIX),基本上跟踪市场在任何给定时间内的波动性(基本上,是股票指数价格波动的函数)。
从我们对神秘仪器的 EDA 中,我们得出了以下结论:
-
C
(VIX)的价格不能为负值,也不能超过 90,这在历史上一直成立。 -
A
(DJIA)和B
(SPY)在 2008 年和 2020 年都有巨大的跌幅,分别对应股市崩盘和 COVID-19 大流行。同时,C
(VIX)的价格也在同一时间上升,表明市场动荡加剧。 -
A
(DJIA)的每日价格波动最大,其次是B
(SPY),最后是C
(VIX),其每日价格波动非常小。考虑到它们所隐藏的基础工具,这些观察也是正确的。
A
(DJIA)和 B
(SPY)具有非常强的正相关性,这是有道理的,因为它们都是大型市值股票指数。C
(VIX)与 A
(DJIA)和 B
(SPY)都有很强的负相关性,这也是有道理的,因为在繁荣时期,波动性保持低位,市场上涨,在危机期间,波动性激增,市场下跌。
在下一节中,我们介绍了一个特殊的 Python 库,它可以自动生成最常见的 EDA 图表和表格。
用于 EDA 的特殊 Python 库
有多个 Python 库可以提供单行代码的 EDA。其中最先进的之一是 dtale
,如下面的代码片段所示:
import dtale
dtale.show(valid_close_df)
前面的命令生成了一个包含所有数据的表格(仅显示前七列),如下所示:
图 2.21 – dtale 组件显示对 valid_close_df DataFrame 的类似电子表格的控制
点击顶部的箭头会显示一个带有所有功能的菜单,如下面的截图所示:
图 2.22 – dtale 全局菜单显示其功能
点击列标题会显示每个特征的单独命令,如下面的截图所示:
图 2.23 – dtale 列菜单显示列功能
交互式 EDA,而不是命令驱动的 EDA,具有其优势——它直观、促进视觉创造力,并且速度更快。
摘要
EDA 的目标是了解我们处理的数据集,并纠正基本数据错误,如不太可能的异常值。我们已经描述了通过运行单独的 Python 命令构建的 EDA,以及使用特殊的 Python EDA 库进行自动化的 EDA。
下一章介绍了我们其中一个最重要的 Python 库:numpy
。
第三章:使用 NumPy 进行高速科学计算
本章介绍了 NumPy,一个用于矩阵计算的高速 Python 库。大多数数据科学/算法交易库都是基于 NumPy 的功能和约定构建的。
在本章中,我们将讨论以下关键主题:
-
NumPy 简介
-
创建 NumPy n 维数组(ndarrays)
-
与 NumPy 数组一起使用的数据类型
-
ndarray 的索引
-
基本 ndarray 操作
-
对 ndarray 进行文件操作
技术要求
本章中使用的 Python 代码在书籍代码仓库的 Chapter03/numpy.ipynb
笔记本中可用。
NumPy 简介
在 Python 中,可以使用列表表示多维异构数组。列表是一个一维数组,列表的列表是一个二维数组,列表的列表的列表是一个三维数组,依此类推。然而,这种解决方案很复杂,难以使用,并且非常慢。
NumPy Python 库的主要设计目标之一是引入高性能和可扩展的结构化数组和矢量化计算。
大多数 NumPy 中的数据结构和操作都是用 C/C++ 实现的,这保证了它们具有优越的速度。
创建 NumPy ndarrays
ndarray 是一个极其高性能和空间有效的多维数组数据结构。
首先,我们需要导入 NumPy 库,如下所示:
import numpy as np
接下来,我们将开始创建一个一维 ndarray。
创建一维 ndarray
以下代码行创建一个一维 ndarray:
arr1D = np.array([1.1, 2.2, 3.3, 4.4, 5.5]);
arr1D
这将产生以下输出:
array([1.1, 2.2, 3.3, 4.4, 5.5])
让我们用以下代码检查数组的类型:
type(arr1D)
这表明数组是一个 NumPy ndarray,如下所示:
numpy.ndarray
我们可以轻松地创建两个或更多维的 ndarray。
创建二维 ndarray
要创建一个二维 ndarray,请使用以下代码:
arr2D = np.array([[1, 2], [3, 4]]);
arr2D
结果有两行,每行有两个值,所以它是一个 2 x 2 的 ndarray,如下代码片段所示:
array([[1, 2],
[3, 4]])
创建任意维度的 ndarray
ndarray 可以构造具有任意维度的数组。以下代码创建了一个 2 x 2 x 2 x 2 维的 ndarray:
arr4D = np.array(range(16)).reshape((2, 2, 2, 2));
arr4D
数组的表示如下所示:
array([[[[ 0, 1],
[ 2, 3]],
[[ 4, 5],
[ 6, 7]]],
[[[ 8, 9],
[10, 11]],
[[12, 13],
[14, 15]]]])
NumPy ndarrays 具有描述 ndarray 维度的 shape
属性,如下代码片段所示:
arr1D.shape
以下片段显示 arr1D
是一个包含五个元素的一维数组:
(5,)
我们可以使用以下代码检查 arr2D
上的 shape
属性:
arr2D.shape
如预期的那样,输出描述它为一个 2 x 2 的 ndarray,如下所示:
(2, 2)
在实践中,有一些矩阵经常被使用,比如零矩阵、全一矩阵、单位矩阵、包含一系列数字的矩阵或随机矩阵。NumPy 提供了支持用一个命令生成这些常用 ndarray 的功能。
使用 np.zeros(…) 创建 ndarray
np.zeros(...)
方法创建一个填充有全 0 的 ndarray,如下代码片段所示:
np.zeros(shape=(2,5))
输出是全 0,维度为 2 x 5,如下代码片段所示:
array([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
使用 np.ones(…) 创建一个 ndarray
np.ones(...)
类似,但每个值都被赋予值 1,而不是 0。该方法如下所示:
np.ones(shape=(2,2))
结果是一个 2 x 2 的 ndarray,每个值都设置为 1,如下面的代码片段所示:
array([[1., 1.],
[1., 1.]])
使用 np.identity(…) 创建一个 ndarray
在矩阵运算中,我们经常需要创建一个单位矩阵,可以使用 np.identity(...)
方法,如下面的代码片段所示:
np.identity(3)
这将创建一个 3 x 3 的单位矩阵,对角线上为 1,其他位置为 0,如下面的代码片段所示:
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
使用 np.arange(…) 创建一个 ndarray
np.arange(...)
是 Python range(...)
方法的 NumPy 等价物。这生成具有起始值、结束值和增量的值,但是返回的是 NumPy ndarrays,如下所示:
np.arange(5)
返回的 ndarray 如下所示:
array([0, 1, 2, 3, 4])
默认情况下,值从 0 开始,递增为 1。
使用 np.random.randn(…) 创建一个 ndarray
np.random.randn(...)
生成一个指定维度的 ndarray,每个元素从标准正态分布中随机抽取的随机值(mean=0
,std=1
),如下所示:
np.random.randn(2,2)
输出是一个 2 x 2 的 ndarray,其值为随机值,如下面的代码片段所示:
array([[ 0.57370365, -1.22229931],
[-1.25539335, 1.11372387]])
使用 NumPy ndarrays 的数据类型
NumPy ndarrays 是同质的——即,ndarray 中的每个元素具有相同的数据类型。这与 Python 列表不同,Python 列表可以包含不同数据类型的元素(异质的)。
np.array(...)
方法接受一个显式的 dtype=
参数,允许我们指定 ndarray 应该使用的数据类型。常用的数据类型包括 np.int32
、np.float64
、np.float128
和 np.bool
。请注意,np.float128
在 Windows 上不受支持。
你应该注意各种数值类型对 ndarrays 的内存使用,主要原因是——数据类型提供的精度越高,其内存需求就越大。对于某些操作,较小的数据类型可能已经足够了。
创建一个 numpy.float64 数组
要创建一个 128 位浮点值数组,请使用以下代码:
np.array([-1, 0, 1], dtype=np.float64)
输出如下所示:
array([-1., 0., 1.], dtype=float64)
创建一个 numpy.bool 数组
我们可以通过将指定的值转换为目标类型来创建一个 ndarray。在下面的代码示例中,尽管提供了整数数据值,但由于指定了数据类型为 np.bool
,所以生成的 ndarray 的 dtype
为 bool
:
np.array([-1, 0, 1], dtype=np.bool)
值如下所示:
array([ True, False, True])
我们观察到整数值 (-1, 0, 1
) 被转换为布尔值 (True, False, True
)。0
被转换为 False
,所有其他值被转换为 True
。
ndarrays 的 dtype 属性
ndarrays 有一个 dtype
属性用于检查数据类型,如下所示:
arr1D.dtype
输出是一个 NumPy dtype
对象,其值为 float64
,如下所示:
dtype('float64')
使用 numpy.ndarrays.astype(…) 转换 ndarray 的底层数据类型
我们可以使用 numpy.ndarrays.astype(...)
方法轻松地将 ndarray 的基础数据类型转换为任何其他兼容的数据类型。例如,要将arr1D
从np.float64
转换为np.int64
,我们使用以下代码:
arr1D.astype(np.int64).dtype
这反映了新的数据类型,如下所示:
dtype('int64')
当 numpy.ndarray.astype(...)
转换为较窄的数据类型时,它将截断值,如下所示:
arr1D.astype(np.int64)
这将arr1D
转换为以下整数值 ndarray:
array([1, 2, 3, 4, 5])
原始的浮点值(1.1, 2.2, …)被转换为它们截断的整数值(1, 2, …)。
ndarray 的索引
数组索引是指访问特定数组元素或元素的方式。在 NumPy 中,所有 ndarray 索引都是从零开始的,即数组的第一个项目索引为0
。负索引被理解为从数组的末尾开始计数。
直接访问 ndarray 的元素
直接访问单个 ndarray 元素是最常用的访问形式之一。
以下代码构建了一个 3 x 3 的随机值 ndarray 供我们使用:
arr = np.random.randn(3,3);
arr
arr
ndarray 具有以下元素:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 1.65004669, -0.09589629, 0.15586867],
[ 0.39533427, 1.47193681, 0.32148741]])
我们可以使用整数索引 0
索引第一个元素,如下所示:
arr[0]
这给我们了arr
ndarray 的第一行,如下所示:
array([-0.04113926, -0.273338 , -1.05294723])
我们可以通过以下代码访问第一行的第二列元素:
arr[0][1]
结果如下所示:
-0.2733379996693689
ndarray 也支持执行相同操作的替代表示法,如下所示:
arr[0, 1]
它访问了与之前相同的元素,如下所示:
-0.2733379996693689
当访问具有非常大维度的 ndarray 时,numpy.ndarray[index_0, index_1, … index_n]
表示法尤其更简洁和有用。
负索引从 ndarray 的末尾开始,如下所示:
arr[-1]
这将返回 ndarray 的最后一行,如下所示:
array([0.39533427, 1.47193681, 0.32148741])
ndarray 切片
虽然单个 ndarray 访问很有用,但是对于批量处理,我们需要一次访问数组的多个元素(例如,如果 ndarray 包含某个资产的所有每日价格,我们可能只想处理所有星期一的价格)。
切片允许一次访问多个 ndarray 记录。ndarray 的切片工作方式与 Python 列表的切片类似。
基本切片语法是 i:j:k,其中 i 是我们想要包括的第一个记录的索引,j 是停止索引,k 是步长。
访问第一个元素之后的所有 ndarray 元素
要访问第一个元素之后的所有元素,我们可以使用以下代码:
arr[1:]
这返回了第一个元素之后的所有行,如下代码片段所示:
array([[ 1.65004669, -0.09589629, 0.15586867],
[ 0.39533427, 1.47193681, 0.32148741]])
获取所有行,从第二行开始,列为第一列和第二列
类似地,要获取从第二行开始的所有行,并且列不包括第三列,运行以下代码:
arr[1:, :2]
这是一个 2 x 2 的 ndarray,正如预期的那样,可以在这里看到:
array([[ 1.65004669, -0.09589629],
[ 0.39533427, 1.47193681]])
使用负索引进行切片
更复杂的切片表示法也是可能的,包括正负索引范围的混合,如下所示:
arr[1:2, -2:-1]
这是一种不太直观的方法,用于查找位于第二行和第二列的元素的切片,如下所示:
array([[-0.09589629]])
没有索引的切片
没有索引的切片将产生整个行/列。下面的代码生成包含第三行所有元素的切片:
arr[:][2]
输出如下所示:
array([0.39533427, 1.47193681, 0.32148741])
下面的代码生成原始arr
ndarray 的切片:
arr[:][:]
输出如下所示:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 1.65004669, -0.09589629, 0.15586867],
[ 0.39533427, 1.47193681, 0.32148741]])
将切片的值设置为 0
经常,我们需要将 ndarray 的某些值设置为给定值。
让我们生成一个包含arr
的第二行并将其分配给一个新变量arr1
的切片,如下所示:
arr1 = arr[1:2];
arr1
arr1
现在包含了最后一行,如下代码片段所示:
array([[ 1.65004669, -0.09589629, 0.15586867]])
现在,让我们将arr1
的每个元素设置为值0
,如下所示:
arr1[:] = 0;
arr1
如预期的那样,arr1
现在包含了全部为 0 的值,如下所示:
array([[0., 0., 0.]])
现在,让我们重新检查我们的原始arr
ndarray,如下所示:
arr
输出如下所示:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 0. , 0. , 0. ],
[ 0.39533427, 1.47193681, 0.32148741]])
我们看到我们对arr1
切片的操作也改变了原始的arr
ndarray。这带我们来到最重要的一点:ndarray 切片是原始 ndarrays 的视图,而不是副本。
在使用 ndarrays 时记住这一点很重要,这样我们就不会无意中改变我们不想改变的东西。这个设计纯粹是为了效率原因,因为复制大型 ndarrays 会产生巨大的开销。
要创建一个 ndarray 的副本,我们需要显式调用numpy.ndarray.copy(...)
方法,如下所示:
arr_copy = arr.copy()
现在,让我们更改arr_copy
ndarray 中的一些值,如下所示:
arr_copy[1:2] = 1;
arr_copy
我们可以在下面的代码片段中看到arr_copy
的变化:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 1. , 1. , 1. ],
[ 0.39533427, 1.47193681, 0.32148741]])
让我们也来检查一下原始的arr
ndarray,如下所示:
arr
输出如下所示:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 0. , 0. , 0. ],
[ 0.39533427, 1.47193681, 0.32148741]])
我们看到原始 ndarray 没有更改,因为arr_copy
是arr
的副本而不是引用/视图。
布尔索引
NumPy 提供了多种索引 ndarray 的方法。NumPy 数组可以通过使用求值为True
或False
的条件进行索引。让我们从重新生成一个arr
ndarray 开始,如下所示:
arr = np.random.randn(3,3);
arr
这是一个 3 x 3 的 ndarray,具有随机值,如下代码片段所示:
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 1.67500165, -0.99280199, 0.80878346],
[ 0.56937775, 0.36614928, -0.02532004]])
让我们重新审视一下运行以下代码的输出,实际上只是调用了np.less(...)
np.less(arr, 0)
方法:
arr < 0
这生成另一个包含True
和False
值的 ndarray,其中True
表示arr
中的相应元素为负数,而False
表示arr
中的相应元素不是负数,如下代码片段所示:
array([[ True, True, False],
[False, True, False],
[False, False, True]])
我们可以使用该数组作为索引到arr
来找到实际的负元素,如下所示:
arr[(arr < 0)]
如预期的那样,这会获取以下负值:
array([-0.50566069, -0.52115534, -0.99280199, -0.02532004])
我们可以使用&
(和)和|
(或)运算符组合多个条件。Python 的&
和|
布尔运算符不适用于 ndarrays,因为它们适用于标量。这里是&
运算符的一个示例:
(arr > -1) & (arr < 1)
这将生成一个值为True
的 ndarray,其中元素在-1
和1
之间,否则为False
,如下代码片段所示:
array([[ True, True, True],
[False, True, True],
[ True, True, True]])
正如我们之前看到的,我们可以使用布尔数组索引 arr
并找到实际的元素,如下所示:
arr[((arr > -1) & (arr < 1))]
以下输出是满足条件的元素数组:
array([-0.50566069, -0.52115534, 0.0757591 , -0.99280199, 0.80878346,
0.56937775, 0.36614928, -0.02532004])
使用数组进行索引
ndarray 的索引还允许我们直接传递感兴趣的索引列表。让我们首先生成一个随机值的 ndarray,如下所示:
arr
输出如下所示:
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 1.67500165, -0.99280199, 0.80878346],
[ 0.56937775, 0.36614928, -0.02532004]])
我们可以使用以下代码选择第一行和第三行:
arr[[0, 2]]
输出是一个包含两行的 2 x 3 ndarray,如下所示:
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 0.56937775, 0.36614928, -0.02532004]])
我们可以结合使用数组进行行和列索引,如下所示:
arr[[0, 2], [1]]
上述代码给出了第一和第三行的第二列,如下所示:
array([-0.52115534, 0.36614928])
我们还可以改变传递的索引的顺序,这在输出中有所体现。下面的代码按照指定的顺序挑选出第三行和第一行:
arr[[2, 0]]
输出反映了我们期望的两行的顺序(先第三行;然后第一行),如下代码片段所示:
array([[ 0.56937775, 0.36614928, -0.02532004],
[-0.50566069, -0.52115534, 0.0757591 ]])
现在我们已经学会了如何创建 ndarrays 以及各种检索其元素值的方法,让我们讨论最常见的 ndarray 操作。
基本的 ndarray 操作
在接下来的示例中,我们将使用一个 arr2D
ndarray,如下所示:
arr2D
这是一个 1
到 4
的值的 2 x 2 ndarray,如下所示:
array([[1, 2],
[3, 4]])
与 ndarray 的标量乘法
与 ndarray 的标量乘法会使 ndarray 的每个元素相乘,如下所示:
arr2D * 4
输出如下所示:
array([[ 4, 8],
[12, 16]])
ndarray 的线性组合
以下操作是标量和 ndarray 操作以及 ndarray 之间的操作的组合:
2*arr2D + 3*arr2D
输出是我们预期的,如下所示:
array([[ 5, 10],
[15, 20]])
ndarray 的指数运算
我们可以将 ndarray 的每个元素提升到某个幂,如下所示:
arr2D ** 2
输出如下所示:
array([[ 1, 4],
[ 9, 16]])
将 ndarray 与标量相加
将 ndarray 与标量相加的结果类似,如下所示:
arr2D + 10
输出如下所示:
array([[11, 12],
[13, 14]])
转置矩阵
找到矩阵的转置,这是一个常见的操作,在 NumPy 中可以使用 numpy.ndarray.transpose(...)
方法实现,如下代码片段所示:
arr2D.transpose()
这转置了 ndarray 并输出它,如下所示:
array([[1, 3],
[2, 4]])
改变 ndarray 的布局
np.ndarray.reshape(...)
方法允许我们更改 ndarray 的布局(形状),而不改变其数据为兼容的形状。
例如,要将 arr2D
从 2 x 2 重塑为 4 x 1,我们使用以下代码:
arr2D.reshape((4, 1))
新的重塑后的 4 x 1 ndarray 如下所示:
array([[1],
[2],
[3],
[4]])
以下代码示例结合了 np.random.randn(...)
和 np.ndarray.reshape(...)
来创建一个 3 x 3 的随机值 ndarray:
arr = np.random.randn(9).reshape((3,3));
arr
生成的 3 x 3 ndarray 如下所示:
array([[ 0.24344963, -0.53183761, 1.08906941],
[-1.71144547, -0.03195253, 0.82675183],
[-2.24987291, 2.60439882, -0.09449784]])
查找 ndarray 中的最小值
要查找 ndarray 中的最小值,我们使用以下命令:
np.min(arr)
结果如下所示:
-2.249872908111852
计算绝对值
所示的np.abs(...)
方法计算 ndarray 的绝对值:
np.abs(arr)
输出 ndarray 如下所示:
array([[0.24344963, 0.53183761, 1.08906941],
[1.71144547, 0.03195253, 0.82675183],
[2.24987291, 2.60439882, 0.09449784]])
计算 ndarray 的均值
np.mean(...)
方法,如下所示,计算 ndarray 中所有元素的均值:
np.mean(arr)
这里显示了arr
元素的均值:
0.01600703714906236
我们可以通过指定axis=
参数来沿列找到均值,如下所示:
np.mean(arr, axis=0)
返回以下数组,其中包含每列的均值:
array([-1.23928958, 0.68020289, 0.6071078 ])
类似地,我们可以通过运行以下代码来沿行找到均值:
np.mean(arr, axis=1)
返回以下数组,包含每行的均值:
array([ 0.26689381, -0.30554872, 0.08667602])
查找 ndarray 中最大值的索引
通常,我们有兴趣找出数组中最大值的位置。np.argmax(...)
方法可以找到 ndarray 中最大值的位置,如下所示:
np.argmax(arr)
这返回以下值,表示最大值的位置(2.60439882
):
7
np.argmax(...)
方法还接受axis=
参数,以按行或按列执行操作,如此处所示:
np.argmax(arr, axis=1)
这将找到每行中最大值的位置,如下所示:
array([2, 2, 1], dtype=int64)
计算 ndarray 元素的累积和
要计算累积总和,NumPy 提供了np.cumsum(...)
方法。np.cumsum(...)
方法如下所示,找到 ndarray 中元素的累积总和:
np.cumsum(arr)
输出提供了每个附加元素后的累积和,如下所示:
array([ 0.24344963, -0.28838798, 0.80068144, -0.91076403, -0.94271656,
-0.11596474, -2.36583764, 0.23856117, 0.14406333])
注意累积和和求和之间的差异。累积和是一个累加的数组,而求和是一个单个数字。
将axis=
参数应用于cumsum
方法的效果类似,如以下代码片段所示:
np.cumsum(arr, axis=1)
这将按行进行,并生成以下数组输出:
array([[ 0.24344963, -0.28838798, 0.80068144],
[-1.71144547, -1.743398 , -0.91664617],
[-2.24987291, 0.35452591, 0.26002807]])
查找 ndarray 中的 NaN 值
在 NumPy 中,缺失或未知值通常使用Not a Number (NaN)值表示。对于许多数值方法,必须将这些值删除或替换为插值。
首先,让我们将第二行设置为np.nan
,如下所示:
arr[1, :] = np.nan;
arr
新的 ndarray 具有 NaN 值,如以下代码片段所示:
array([[ 0.64296696, -1.35386668, -0.63063743],
[ nan, nan, nan],
[-0.19093967, -0.93260398, -1.58520989]])
np.isnan(...)
ufunc 找到 ndarray 中的值是否为 NaN,如下所示:
np.isnan(arr)
输出是一个 ndarray,其中存在 NaN 的地方为True
值,不存在 NaN 的地方为False
值,如下所示的代码片段所示:
array([[False, False, False],
[ True, True, True],
[False, False, False]])
查找两个 ndarray 的 x1>x2 的真值
布尔 ndarray 是获取感兴趣的值的索引的有效方式。使用布尔 ndarray 比逐个遍历矩阵元素要高效得多。
让我们按照以下方式构建另一个具有随机值的arr1
ndarray:
arr1 = np.random.randn(9).reshape((3,3));
arr1
结果是一个 3 x 3 的 ndarray,如下所示的代码片段中所示:
array([[ 0.32102068, -0.51877544, -1.28267292],
[-1.34842617, 0.61170993, -0.5561239 ],
[ 1.41138027, -2.4951374 , 1.30766648]])
类似地,让我们构建另一个arr2
ndarray,如下所示:
arr2 = np.random.randn(9).reshape((3,3));
arr2
输出如下所示:
array([[ 0.33189432, 0.82416396, -0.17453351],
[-1.59689203, -0.42352094, 0.22643589],
[-1.80766151, 0.26201455, -0.08469759]])
np.greater(...)
函数是一个二进制 ufunc,当 ndarray 中的左值大于 ndarray 中的右值时生成True
值。该函数如下所示:
np.greater(arr1, arr2)
输出是如前所述的True
和False
值的 ndarray,如我们在这里所见:
array([[False, False, False],
[ True, True, False],
[ True, False, True]])
>
中缀操作符,如下段代码片段所示,是numpy.greater(...)
的简写:
arr1 > arr2
输出相同,如我们在这里所见:
array([[False, False, False],
[ True, True, False],
[ True, False, True]])
对 ndarray 进行任何和所有的布尔运算
除了关系运算符外,NumPy 还支持其他方法来测试矩阵值上的条件。
以下代码生成一个 ndarray,对满足条件的元素返回True
,否则返回False
:
arr_bool = (arr > -0.5) & (arr < 0.5);
arr_bool
输出如下所示:
array([[False, False, True],
[False, False, False],
[False, True, True]])
以下numpy.ndarray.any(...)
方法在任何元素为True
时返回True
,否则返回False
:
arr_bool.any()
在这里,我们至少有一个元素为True
,因此输出为True
,如下所示:
True
再次,它接受常见的axis=
参数并且表现如预期,如我们在这里所见:
arr_bool.any(axis=1)
并且按行执行的操作生成如下所示:
array([True, False, True])
以下numpy.ndarray.all(...)
方法在所有元素都为True
时返回True
,否则返回False
:
arr_bool.all()
这返回了以下内容,因为并非所有元素都为True
:
False
它还接受axis=
参数,如下所示:
arr_bool.all(axis=1)
再次,每行至少有一个False
值,因此输出为False
,如下所示:
array([False, False, False])
对 ndarray 进行排序
在排序的 ndarray 中查找元素比处理 ndarray 的所有元素更快。
让我们生成一个 1D 随机数组,如下所示:
arr1D = np.random.randn(10);
arr1D
ndarray 包含以下数据:
array([ 1.14322028, 1.61792721, -1.01446969, 1.26988026, -0.20110113,
-0.28283051, 0.73009565, -0.68766388, 0.27276319, -0.7135162 ])
np.sort(...)
方法非常简单,如下所示:
np.sort(arr1D)
输出如下所示:
array([-1.01446969, -0.7135162 , -0.68766388, -0.28283051, -0.20110113,
0.27276319, 0.73009565, 1.14322028, 1.26988026, 1.61792721])
让我们检查原始 ndarray,看看它是否被numpy.sort(...)
操作修改了,如下所示:
arr1D
以下输出显示原始数组未改变:
array([ 1.14322028, 1.61792721, -1.01446969, 1.26988026, -0.20110113,
-0.28283051, 0.73009565, -0.68766388, 0.27276319, -0.7135162 ])
以下np.argsort(...)
方法创建一个表示每个元素在排序数组中位置的索引数组:
np.argsort(arr1D)
此操作的输出生成以下数组:
array([2, 9, 7, 5, 4, 8, 6, 0, 3, 1])
NumPy ndarray 还具有numpy.ndarray.sort(...)
方法,该方法可以就地对数组进行排序。该方法在下面的代码片段中说明:
arr1D.sort()
np.argsort(arr1D)
调用sort()
后,我们调用numpy.argsort(...)
来确保数组已排序,这将生成以下数组,确认了该行为:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
在 ndarray 中搜索
在 ndarray 上满足某个条件的元素的索引是一种基本操作。
首先,我们从一个具有连续值的 ndarray 开始,如下所示:
arr1 = np.array(range(1, 11));
arr1
这将创建以下 ndarray:
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
我们根据第一个 ndarray 创建了第二个 ndarray,不过这次第二个 ndarray 中的值乘以了1000
,如下面的代码片段所示:
arr2 = arr1 * 1000;
arr2
然后,我们知道arr2
包含以下数据:
array([ 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000,
10000])
我们定义另一个 ndarray,其中包含 10 个随机的True
和False
值,如下所示:
cond = np.random.randn(10) > 0;
cond
cond
ndarray 中的值显示如下:
array([False, False, True, False, False, True, True, True, False, True])
np.where(...)
方法允许我们根据条件是 True
还是 False
从一个 ndarray 或另一个中选择值。以下代码将生成一个 ndarray,当 cond
数组中对应的元素为 True
时,从 arr1
中选择值;否则,从 arr2
中选择值:
np.where(cond, arr1, arr2)
返回的数组如下所示:
array([1000, 2000, 3, 4000, 5000, 6, 7, 8, 9000, 10])
ndarray 的文件操作
大多数 NumPy 数组都是从文件中读取的,在处理后,再写回文件。
文本文件的文件操作
文本文件的主要优点是它们可读性强,并且与任何自定义软件兼容。
让我们从以下随机数组开始:
arr
此数组包含以下数据
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 1.67500165, -0.99280199, 0.80878346],
[ 0.56937775, 0.36614928, -0.02532004]])
numpy.savetxt(...)
方法以文本格式将 ndarray 保存到磁盘。
以下示例使用了 fmt='%0.2lf'
格式字符串并指定了逗号分隔符:
np.savetxt('arr.csv', arr, fmt='%0.2lf', delimiter=',')
让我们检查当前目录中写入磁盘的 arr.csv
文件,如下所示:
!cat arr.csv
逗号分隔值 (CSV) 文件包含以下数据:
-0.51,-0.52,0.08
1.68,-0.99,0.81
0.57,0.37,-0.03
numpy.loadtxt(...)
方法从文本文件加载 ndarray 到内存中。在这里,我们显式指定了 delimiter=','
参数,如下所示:
arr_new = np.loadtxt('arr.csv', delimiter=',');
arr_new
从文本文件中读入的 ndarray 包含以下数据:
array([[-0.51, -0.52, 0.08],
[ 1.68, -0.99, 0.81],
[ 0.57, 0.37, -0.03]])
二进制文件的文件操作
二进制文件对于计算机处理来说效率更高——它们保存和加载更快,比文本文件更小。但是,它们的格式可能不受其他软件支持。
numpy.save(...)
方法将 ndarray 存储为二进制格式,如下代码片段所示:
np.save('arr', arr)
!cat arr.npy
arr.npy
文件的输出如下:
numpy.save(...)
方法会自动为其创建的二进制文件分配 .npy
扩展名。
numpy.load(...)
方法,如下代码片段所示,用于读取二进制文件:
arr_new = np.load('arr.npy');
arr_new
新读入的 ndarray 如下所示:
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 1.67500165, -0.99280199, 0.80878346],
[ 0.56937775, 0.36614928, -0.02532004]])
二进制文件格式的另一个优点是,数据可以以极高的精度存储,特别是在处理浮点值时,这在某些情况下在文本文件中并不总是可能的,因为在某些情况下会有一些精度损失。
让我们通过运行以下代码检查旧的 arr
ndarray 和新读入的 arr_new
数组是否完全匹配:
arr == arr_new
这将生成以下数组,如果元素相等则包含 True
,否则包含 False
:
array([[ True, True, True],
[ True, True, True],
[ True, True, True]])
因此,我们看到每个元素都完全匹配。
概要
在本章中,我们学习了如何在 Python 中创建任意维度的矩阵,如何访问矩阵的元素,如何对矩阵进行基本的线性代数运算,以及如何保存和加载矩阵。
使用 NumPy 矩阵是任何数据分析的主要操作,因为向量运算经过机器优化,因此比 Python 列表上的操作要快得多——通常快 5 到 100 倍。回测任何算法策略通常包括处理庞大的矩阵,而速度差异可以转化为节省的小时或天数时间。
在下一章中,我们将介绍第二重要的用于数据分析的库:Pandas,它是建立在 NumPy 基础上的。NumPy 提供了对基于数据框架的数据操作的支持(数据框架是 Excel 工作表的 Python 版本——即,一个二维数据结构,其中每列都有自己的类型)。