Python-金融编程第二版-全-
Python 金融编程第二版(全)
原文:
annas-archive.org/md5/d2f94efd019a2e2cb5c4fa9f260d63c译者:飞龙
第一部分:Python 与金融
本部分介绍了 Python 在金融领域的应用。它包括三章:
-
第一章简要讨论了 Python 的一般情况,并论述了为什么 Python 确实非常适合应对金融行业和金融(数据)分析中的技术挑战。
-
第二章关于 Python 基础设施,旨在简要概述管理 Python 环境的重要方面,以便开始使用 Python 进行交互式金融分析和金融应用程序开发。
第一章:为什么要用 Python 进行金融?
银行本质上是科技公司。
什么是 Python?
Python 是一种高级、通用的编程语言,广泛应用于各个领域和技术领域。在 Python 网站上,您可以找到以下执行摘要(参见 https://www.python.org/doc/essays/blurb):
Python 是一种解释性的、面向对象的、高级的编程语言,具有动态语义。它的高级内置数据结构,结合动态类型和动态绑定,使其非常适合快速应用程序开发,以及用作脚本语言或粘合语言将现有组件连接在一起。Python 简单、易学的语法强调可读性,从而降低了程序维护的成本。Python 支持模块和包,这鼓励了程序的模块化和代码的重用。Python 解释器和广泛的标准库可以在所有主要平台上免费获得源代码或二进制形式,并可以自由分发。
这很好地描述了为什么 Python 已经发展成为今天的主要编程语言之一。现在,Python 不仅被初学者程序员使用,还被高技能专家开发者使用,在学校,在大学,在网络公司,在大型企业和金融机构以及在任何科学领域都有应用。
Python 具有以下特点之一:
开源
Python 及其大多数支持库和工具都是开源的,并且通常具有相当灵活和开放的许可证。
解释性的
参考 CPython 实现是一种语言的解释器,它将 Python 代码在运行时转换为可执行字节码。
多范式
Python 支持不同的编程和实现范式,如面向对象和命令式、函数式或过程式编程。
多用途
Python 可用于快速、交互式的代码开发,也可用于构建大型应用程序;它可用于低级系统操作,也可用于高级分析任务。
跨平台
Python 可用于最重要的操作系统,如 Windows、Linux 和 Mac OS;它用于构建桌面应用程序和 Web 应用程序;它可以用于最大的集群和最强大的服务器,也可以用于树莓派等小型设备(参见 http://www.raspberrypi.org)。
动态类型
Python 中的类型通常在运行时推断,而不是像大多数编译语言中静态声明的那样。
缩进感知
与大多数其他编程语言不同,Python 使用缩进来标记代码块,而不是使用括号、方括号或分号。
垃圾收集
Python 具有自动化垃圾收集,避免了程序员管理内存的需要。
当涉及到 Python 语法和 Python 的本质时,Python Enhancement Proposal 20——即所谓的“Python 之禅”提供了主要的指导原则。可以通过每个交互式 shell 的命令import this访问它:
In [1]: import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Python 的简要历史
尽管对于一些人来说,Python 可能仍然具有一些新鲜感,但它已经存在了相当长的时间。事实上,Python 的开发工作始于 1980 年代,由来自荷兰的 Guido van Rossum 负责。他仍然活跃于 Python 的开发,并且被 Python 社区授予了终身仁慈独裁者的称号(参见http://en.wikipedia.org/wiki/History_of_Python)。以下可以被视为 Python 发展的里程碑:
-
Python 0.9.0 发布于 1991 年(第一个版本)
-
Python 1.0 发布于 1994 年
-
Python 2.0 发布于 2000 年
-
Python 2.6 发布于 2008 年
-
Python 3.0 发布于 2008 年
-
Python 3.1 发布于 2009 年
-
Python 2.7 发布于 2010 年
-
Python 3.2 发布于 2011 年
-
Python 3.3 发布于 2012 年
-
Python 3.4 发布于 2014 年
-
Python 3.5 发布于 2015 年
-
Python 3.6 发布于 2016 年
令人瞩目的是,对于 Python 新手来说,Python 有两个主要版本可用,自 2008 年以来仍在开发中,并且更重要的是,同时使用。截至撰写本文时,这种情况可能会持续一段时间,因为大量的代码仍然是基于 Python 2.6/2.7 并且在生产中使用。尽管本书的第一版是基于 Python 2.7 编写的,但本书的第二版全程使用的是 Python 3.6。
Python 生态系统
Python 作为一个生态系统的一个主要特点,与仅仅是一种编程语言相比,就是有大量的包和工具可用。这些包和工具通常在需要时必须被导入(例如,绘图库),或者必须作为一个单独的系统进程启动(例如,Python 开发环境)。导入意味着使一个包对当前的命名空间和当前的 Python 解释器进程可用。
Python 本身已经带有大量的包和模块,可以增强基本的解释器。人们谈论的是Python 标准库(参见https://docs.python.org/3/library/index.html)。例如,可以进行基本的数学计算而无需任何导入,而更专业的数学函数需要通过math模块导入:
In [2]: 100 * 2.5 + 50
Out[2]: 300.0
In [3]: log(1)
----------------------------------------
NameErrorTraceback (most recent call last)
<ipython-input-3-cfa4946d0225> in <module>()
----> 1 log(1)
NameError: name 'log' is not defined
In [4]: import math
In [5]: math.log(1)
Out[5]: 0.0
虽然 math 是一个标准的 Python 库,可以在任何安装中使用,但还有许多其他可选安装的库,可以像标准库一样使用。这些库可从不同的(网络)来源获取。然而,通常建议使用一个 Python 软件包管理器,以确保所有库都与彼此一致(有关此主题的更多信息,请参见第二章)。
到目前为止所提供的代码示例都使用了IPython(请参见http://www.ipython.org),这是 Python 最受欢迎的交互式开发环境(IDE)之一。尽管它最初只是一个增强的 shell,但今天它具有许多通常在 IDE 中找到的特性(例如,支持分析和调试)。那些缺失的功能通常由高级文本/代码编辑器提供,如 Sublime Text(请参见http://www.sublimetext.com)。因此,将IPython与个人选择的文本/代码编辑器结合起来形成 Python 开发过程的基本工具集并不罕见。
IPython 在许多方面增强了标准的交互式 shell。例如,它提供了改进的命令行历史功能,并允许轻松地检查对象。例如,只需在函数名称前后添加?(添加??将提供更多信息),就可以打印函数的帮助文本(docstring)。
IPython 最初有两个流行版本:一个是 shell 版本,另一个是 基于浏览器的版本(Notebook)。Notebook 变体已被证明非常有用和受欢迎,因此它现在已成为一个独立的、与语言无关的项目和工具,现在称为 Jupyter(见http://jupyter.org)。
Python 用户光谱
Python 不仅吸引专业软件开发人员;它也对业余开发人员以及领域专家和科学开发人员有用。
专业软件开发人员可以找到他们构建大型应用程序所需的一切。几乎支持所有编程范式;有强大的开发工具可用;并且原则上任何任务都可以通过 Python 来解决。这些类型的用户通常构建自己的框架和类,也在基本的 Python 和科学堆栈上工作,并努力充分利用生态系统。
科学开发人员或领域专家通常是某些库和框架的重度用户,已经构建了自己的应用程序,并随着时间的推移对其进行了增强和优化,并根据自己的特定需求定制了生态系统。这些用户群通常参与更长时间的交互式会话,快速原型化新代码,并探索和可视化他们的研究和/或领域数据集。
业余程序员 喜欢使用 Python 通常是因为他们知道 Python 在特定问题上有优势。例如,访问 matplotlib 的画廊页面,复制那里提供的某个可视化代码片段,并根据他们的特定需求调整代码,可能是这个群体的成员的有益用例。
还有另一重要的 Python 用户群体:初学者程序员,即那些刚开始学习编程的人。如今,Python 已经成为大学、学院甚至学校介绍编程给学生的非常流行的语言之一。¹ 这主要是因为它的基本语法易于学习和理解,即使对于非开发者也是如此。此外,有帮助的是 Python 几乎支持所有的编程风格。²
科学堆栈
有一组被统称为 科学堆栈 的库。这个堆栈包括,但不限于,以下几个包:
NumPy 提供了一个多维数组对象来存储同构或异构数据;它还提供了优化的函数/方法来操作这个数组对象。
SciPy 是一个子包和函数的集合,实现了科学或金融中经常需要的重要标准功能;例如,可以找到用于三次样条插值以及数值积分的函数。
这是 Python 中最受欢迎的绘图和可视化库,提供了 2D 和 3D 可视化功能。
PyTables 是 HDF5 数据存储库的流行封装器(参见http://www.hdfgroup.org/HDF5/);它是一个用于实现基于磁盘的优化 I/O 操作的库,基于分层数据库/文件格式。
pandas 建立在 NumPy 之上,提供了更丰富的类来管理和分析时间序列和表格数据;它与 matplotlib 紧密集成用于绘图和 PyTables 用于数据存储和检索。
Scikit-Learn 是一个流行的机器学习(ML)包,为许多不同的 ML 算法提供了统一的 API,例如,用于估计、分类或聚类的算法。
根据特定的领域或问题,这个堆栈会通过额外的库进行扩展,这些库往往有一个共同点,即它们建立在一个或多个基本库的基础之上。然而,在一般情况下,最常见的最小公分母或基本构建块是 NumPy 的 ndarray 类(参见 第四章),或者现在是 pandas 的 DataFrame 类(参见 第五章)。
单纯将 Python 视为一种编程语言,还有许多其他语言可供选择,可能与其语法和优雅程度相媲美。例如,Ruby 是一种相当流行的语言,经常与 Python 进行比较。在该语言的网站 http://www.ruby-lang.org 上,您会找到以下描述:
一种以简洁和高效为重点的动态、开源编程语言。它具有优雅的语法,自然易读且易于编写。
大多数使用 Python 的人可能也同意对 Python 本身作出完全相同的声明。然而,与 Ruby 等同样吸引人的语言相比,许多用户认为 Python 的特点在于科学堆栈的可用性。这使得 Python 不仅是一种好的、优雅的语言,还能够替代类似 Matlab 或 R 这样的领域特定语言和工具集。它默认提供了你所期望的任何东西,比如一位经验丰富的 Web 开发人员或系统管理员。此外,Python 也擅长与领域特定语言(如 R)进行接口交互,因此通常决策并不是关于“要么选择 Python 要么选择其他语言”,而是关于选择哪种语言作为主要语言。
金融科技
现在我们对 Python 的大致了解有了一些想法,稍微退后一步,简要思考一下技术在金融中的作用是有意义的。这将使我们能够更好地评判 Python 已经扮演的角色,甚至更重要的是,可能会在未来的金融行业中发挥的作用。
在某种意义上,技术本身对金融机构(例如,与工业公司相比)或金融职能(例如,与物流等其他企业职能相比)并不“特别”。然而,近年来,在创新和监管的推动下,银行和其他金融机构(如对冲基金)已经越来越多地演变成了技术公司,而不仅仅是“仅仅”是金融中介。技术已经成为全球几乎所有金融机构的重要资产,具有带来竞争优势以及劣势的潜力。一些背景信息可以阐明这一发展的原因。
技术支出
银行和金融机构共同构成了年度技术支出最多的行业。因此,以下声明不仅显示了技术对金融业的重要性,而且金融业对技术行业也非常重要(www.idc.com):
…,截至 2016 年,全球金融服务 IT 支出将达到近 4800 亿美元,年复合增长率(CAGR)为 4.2%。
IDC
特别是,银行和其他金融机构正在竞相将业务和运营模式转移到数字化基础上 (www.statista.com):
预计 2017 年北美银行对新技术的支出将达到 199 亿美元。
银行开发当前系统并致力于新技术解决方案,以增强其在全球市场上的竞争力并吸引对新在线和移动技术感兴趣的客户。这对于为银行业提供新想法和软件解决方案的全球金融科技公司来说是一个巨大的机遇。
Statista
当今,大型跨国银行通常雇佣数千名开发人员来维护现有系统并构建新系统。
技术作为推动因素
技术发展也促进了金融领域的创新和效率提升:
金融服务行业在过去几年里经历了技术引领的剧变。许多高管指望他们的 IT 部门提高效率并促进游戏改变式的创新,同时以某种方式降低成本并继续支持遗留系统。与此同时,金融科技初创企业正在侵入已建立的市场,以客户友好的解决方案为首,这些解决方案从零开始开发,并且不受遗留系统的束缚。
《普华永道第 19 届全球 CEO 调查报告》(2016 年)
随着效率日益提高,寻找竞争优势往往要求在越来越复杂的产品或交易中寻找。这反过来会增加风险,并使风险管理以及监督和监管变得越来越困难。2007 年和 2008 年的金融危机讲述了由此类发展带来的潜在危险。同样,“算法和计算机失控”也代表了对金融市场的潜在风险;这在 2010 年 5 月的所谓闪电崩盘中得到了戏剧性的体现,那时自动卖出导致某些股票和股票指数在一天内大幅下跌(参见http://en.wikipedia.org/wiki/2010_Flash_Crash)。
技术和人才作为进入壁垒
一方面,技术的进步会随着时间的推移降低成本,其他条件不变。另一方面,金融机构继续大力投资于技术,以获得市场份额并捍卫其当前地位。要在今天的某些金融领域活跃起来,通常需要进行大规模的技术和人才投资。例如,考虑一下衍生品分析领域:
在总体软件生命周期中,采用内部策略进行场外[衍生品]定价的公司,仅建立、维护和增强完整的衍生品库就需要投资额在$25 百万到$36 百万之间。
Ding 2010
建立完整的衍生品分析库不仅成本高昂且耗时,你还需要足够的专家来完成这项工作。而且这些专家必须有适当的工具和技术来完成他们的任务。
另一则关于长期资本管理(LTCM)早期的报价支持了关于技术和人才的这一见解:LTCM 曾是最受尊敬的量化对冲基金之一,然而在 1990 年代末破产。
Meriwether 花费了$20 百万在格林威治,康涅狄格州建立了一套最先进的计算机系统,并雇佣了一支精英金融工程团队来运作 LTCM,这是工业级风险管理。
Patterson 2010
Meriwether 为数百万美元购买的同样计算能力,如今可能只需数千美元就能获得。另一方面,对于更大的金融机构来说,交易、定价和风险管理已变得如此复杂,以至于今天它们需要部署拥有数万计算核心的 IT 基础设施。
日益增加的速度、频率和数据量
金融行业的一个维度受到技术进步影响最大:金融交易的决策和执行速度和频率。Lewis(2014 年)的新书详细描述了所谓的闪电交易—即以最高速度进行交易。
一方面,不断增加的数据可用性在越来越小的尺度上使得实时反应变得必要。另一方面,交易速度和频率的增加使得数据量进一步增加。这导致了一些过程相互强化,并将金融交易的平均时间尺度系统性地推向下降:
Renaissance 的 Medallion 基金在 2008 年惊人地增长了 80%,利用其闪电般快速的计算机资本化市场极端波动。吉姆·西蒙斯成为当年对冲基金界的最高收入者,赚得了 25 亿美元。
Patterson 2010
单支股票的三十年日度股价数据大约包括 7,500 个报价。今天大部分金融理论基于这种数据。例如,现代投资组合理论(MPT)、资本资产定价模型(CAPM)和风险价值(VaR)理论都基于日度股价数据。
相比之下,在典型的交易日,苹果公司(AAPL)的股价约被报价 15,000 次—比过去 30 年末日报价看到的报价多两倍。这带来了一系列挑战:
数据处理
仅仅考虑和处理股票或其他金融工具的日终行情是不够的;对于某些工具来说,每天 24 小时,每周 7 天都会发生“太多”的事情。
分析速度
决策通常需要在毫秒甚至更快的时间内做出,因此需要建立相应的分析能力,并实时分析大量数据。
理论基础
尽管传统的金融理论和概念远非完美,但它们随着时间的推移已经经过了充分的测试(有时也被充分地拒绝);就当今至关重要的毫秒级时间尺度而言,仍然缺乏一致的、经过时间检验的概念和理论,这些理论已被证明在一定程度上是相对稳健的。
所有这些挑战原则上只能通过现代技术来解决。可能有点令人惊讶的是,缺乏一致性理论往往是通过技术手段来解决的,高速算法利用市场微观结构元素(例如,订单流量、买卖价差),而不是依赖某种金融推理。
实时分析的崛起
有一个学科在金融行业的重要性大大增加:金融和数据分析。这一现象与速度、频率和数据量在行业中迅速增长的认识密切相关。事实上,实时分析可以被认为是行业对这一趋势的回应。
大致而言,“金融和数据分析”是指将软件和技术与(可能是先进的)算法和方法相结合,以收集、处理和分析数据,以获取见解、做出决策或满足监管要求的学科。例如,估算银行零售业务中金融产品定价结构变化引起的销售影响。另一个例子可能是对投资银行衍生品交易的复杂投资组合进行大规模的隔夜信用价值调整(CVA)计算。
在这种情况下,金融机构面临着两个主要挑战:
大数据
即使在“大数据”这个术语被创造之前,银行和其他金融机构也不得不处理大量数据;然而,随着时间的推移,单个分析任务中需要处理的数据量已经大大增加,要求提高计算能力以及越来越大的内存和存储容量。
实时经济
在过去,决策者可以依靠结构化的、定期的规划、决策和(风险)管理流程,而今天他们面临的是需要实时处理这些功能的需求;过去通过夜间批量运行在后台处理的几项任务现在已被转移到前台,并实时执行。
再次,人们可以观察到技术进步与金融/商业实践之间的相互作用。一方面,需要不断应用现代技术来提高分析方法的速度和能力。另一方面,技术的进步使得几年甚至几个月前被认为不可能(或由于预算限制而不可行)的新分析方法成为可能。
分析领域的一个主要趋势是在 CPU(中央处理单元)端利用并行架构和在 GPGPU(通用图形处理单元)端利用大规模并行架构。当前的 GPGPU 通常拥有 1,000 多个计算核心,这使得有时需要彻底重新思考并行对不同算法可能意味着什么。在这方面仍然存在的障碍是用户通常必须学习新的范例和技术来利用这种硬件的性能。^(3)
金融中的 Python
上一节描述了金融中技术角色的一些选定方面:
-
金融业技术成本
-
技术作为新业务和创新的推动者
-
技术和人才作为金融行业进入壁垒
-
速度、频率和数据量的增加
-
实时分析的兴起
在本节中,我们想要分析 Python 如何帮助解决这些方面所暗示的几个挑战。但首先,从更基本的角度来看,让我们从语言和语法的角度来审视 Python 在金融领域的作用。
金融和 Python 语法
在金融环境中首次尝试 Python 的大多数人可能会面临算法问题。这类似于科学家想要解决微分方程、评估积分或简单地可视化一些数据的情况。一般来说,在这个阶段,很少有人会花费时间思考形式化的开发过程、测试、文档编写或部署等问题。然而,这似乎是人们开始喜爱 Python 的阶段。其中一个主要原因可能是 Python 语法通常与用于描述科学问题或金融算法的数学语法非常接近。
我们可以通过一个简单的金融算法来说明这一现象,即通过蒙特卡洛模拟对欧式看涨期权进行估值。我们将考虑一个 Black-Scholes-Merton(BSM)设置(也见[待续链接]),其中期权的基础风险因素遵循几何布朗运动。
假设我们对估值有以下数值参数值:
-
初始股指水平
-
欧式看涨期权的行权价格
-
到期时间 年。
-
常量,无风险短期利率 。
-
常量波动率 。
在 BSM 模型中,到期时的指数水平是一个随机变量,由 方程 1-1 给出,其中 z 是一个标准正态分布的随机变量。
方程 1-1. 到期时的 Black-Scholes-Merton (1973) 指数水平。
以下是蒙特卡洛估值过程的 算法描述 :
-
从标准正态分布中绘制(伪)随机数 (i) , 其中 。
-
计算给定 和 方程 1-1 的所有到期时指数水平 。
-
计算到期时期权的所有内部值为 。
-
根据 方程 1-2 中给出的蒙特卡罗估计量估算期权现值。
方程 1-2. 欧式期权的蒙特卡罗估计量。
现在我们将把这个问题和算法翻译成 Python 代码。读者可以通过使用 IPython 等工具来跟踪单个步骤,但在这个阶段并不是非常必要。
In [6]: S0 = 100. # ①
K = 105. # ①
T = 1.0 # ①
r = 0.05 # ①
sigma = 0.2 # ①
In [7]: import math
import numpy as np # ②
I = 100000
np.random.seed(1000) # ③
z = np.random.standard_normal(I) # ④
ST = S0 * np.exp((r - sigma ** 2 / 2) * T + sigma * math.sqrt(T) * z) # ⑤
hT = np.maximum(ST - K, 0) # ⑥
C0 = math.exp(-r * T) * np.mean(hT) # ⑦
In [8]: print('Value of the European Call Option %5.3f:' % C0) # ⑧
Value of the European Call Option 8.019:
①
模型参数值已定义。
②
NumPy 在这里作为主要包被使用。
③
随机数生成器的种子值是固定的。
④
这绘制了标准正态分布的随机数。
⑤
这模拟了期末值。
⑥
到期时的期权回报是通过计算得出的。
⑦
蒙特卡洛估算器被评估。
⑧
这将打印出结果值的估计。
有三个方面值得强调:
语法
Python 语法确实与数学语法非常接近,例如,在参数值赋值时。
翻译
每个数学和/或算法声明通常可以被翻译成一行单独的Python 代码。
向量化
NumPy的一个优点是其紧凑、向量化的语法,例如,允许在一行代码中进行 10 万次计算。
这段代码可以在像IPython这样的交互环境中使用。然而,通常被定期重复使用的代码会被组织成所谓的模块(或脚本),它们是具有后缀.py的单个 Python(技术上的“文本”)文件。在这种情况下,这样的一个模块可能看起来像 Example 1-1,并且可以保存为名为bsm_mcs_euro.py的文件。
示例 1-1. 欧式看涨期权的蒙特卡洛估值
#
# Monte Carlo valuation of European call option
# in Black-Scholes-Merton model
# bsm_mcs_euro.py
#
# Python for Finance
# (c) Dr. Yves J. Hilpisch
#
import math
import numpy as np
# Parameter Values
S0 = 100. # initial index level
K = 105. # strike price
T = 1.0 # time-to-maturity
r = 0.05 # riskless short rate
sigma = 0.2 # volatility
I = 100000 # number of simulations
# Valuation Algorithm
z = np.random.standard_normal(I) # pseudorandom numbers
# index values at maturity
ST = S0 * np.exp((r - 0.5 * sigma ** 2) * T + sigma * math.sqrt(T) * z)
hT = np.maximum(ST - K, 0) # inner values at maturity
C0 = math.exp(-r * T) * np.mean(hT) # Monte Carlo estimator
# Result Output
print('Value of the European Call Option %5.3f' % C0)
本小节中相当简单的算法示例说明了 Python,以其非常直观的语法,非常适合补充经典的科学语言英语和数学。似乎将Python加入到科学语言集合中使其更加完整。我们有
-
英语用于书写、讨论科学和金融问题等。
-
数学用于简洁而准确地描述和建模抽象方面、算法、复杂量等。
-
Python用于技术上建模和实现抽象方面、算法、复杂量等。
数学和 Python 语法
几乎没有任何编程语言能像 Python 一样接近数学语法。因此,数值算法从数学表示转换为Python实现非常简单。这使得在这些领域中使用 Python 进行原型设计、开发和代码维护非常高效。
在一些领域,使用伪代码并以此引入第四个语言家族成员是常见做法。伪代码的作用是更技术化地表示金融算法,这既与数学表示接近,又与技术实现接近。除了算法本身,伪代码还考虑了计算机原理。
这种做法通常源于大多数编程语言的技术实现与其正式数学表示相距甚远。大多数编程语言需要包含许多仅在技术上需要的元素,以至于很难看出数学和代码之间的等价性。
如今,Python 通常以伪代码方式使用,因为其语法几乎与数学相似,并且由于技术“开销”保持最小。这是通过语言中体现的一些高级概念实现的,这些概念不仅具有优势,而且一般都伴随着风险和/或其他成本。但是,可以肯定的是,使用 Python 可以在需要时遵循其他语言可能从一开始就需要的严格实现和编码实践。在这个意义上,Python 可以提供最好的两种世界:高级抽象和严格实现。
通过 Python 提高效率和生产力
在高层次上,使用 Python 的好处可以从三个方面衡量:
效率
Python 如何帮助更快地获得结果,节省成本和节省时间?
生产力
Python 如何帮助提高使用相同资源(人力、资产等)的效率?
质量
Python 允许我们做什么,而其他技术做不到呢?
对这些方面的讨论自然不可能穷尽。但是,它可以突出一些论据作为起点。
更短的结果时间
Python 的效率显而易见的领域之一是交互式数据分析。这是一个极大受益于诸如IPython和像pandas这样的强大工具的领域。
考虑一个金融学生,她正在写她的硕士论文,对 S&P 500 指数值感兴趣。她想要分析历史指数水平,比如说,几年来指数波动率是如何随时间波动的。她想要找到证据表明,与一些典型的模型假设相反,波动率随时间波动,并且远非恒定。结果还应该进行可视化。她主要需要做以下几件事:
-
从网络检索指数水平数据。
-
计算对数收益的年化滚动标准差(波动性)。
-
绘制指数水平数据和结果。
这些任务足够复杂,以至于不久之前人们会认为这是专业金融分析师的事情。如今,即使是金融学生也能轻松应对这些问题。让我们看看这究竟是如何运作的——在这个阶段不必担心语法细节(一切都将在后续章节中详细解释)。
In [10]: import numpy as np # ①
import pandas as pd # ①
In [11]: data = pd.read_csv('http://hilpisch.com/tr_eikon_eod_data.csv',
index_col=0, parse_dates=True) # ②
data = pd.DataFrame(data['.SPX']) # ③
data.info() # ④
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1972 entries, 2010-01-04 to 2017-10-31
Data columns (total 1 columns):
.SPX 1972 non-null float64
dtypes: float64(1)
memory usage: 30.8 KB
In [12]: data['rets'] = np.log(data / data.shift(1)) # ⑤
data['vola'] = data['rets'].rolling(252).std() * np.sqrt(252) # ⑥
In [13]: data[['.SPX', 'vola']].plot(subplots=True, figsize=(10, 6)); # ⑦
plt.savefig('../images/01_chapter/spx_volatility.png')
①
这导入了NumPy和pandas。
②
read_csv允许检索远程存储的数据集。
③
选择数据的一个子集。
④
这显示了数据集的一些元信息。
⑤
对数收益以矢量化方式计算(“无循环”)。
⑥
滚动、年化波动率是由此得出的。
⑦
最后一行将这两个时间序列绘制出来。
图 1-1 展示了这个简短交互会话的图形结果。几行代码就足以实现在金融分析中经常遇到的三个相当复杂的任务:数据收集、复杂和重复的数学计算,以及结果的可视化。这个例子说明了pandas使得处理整个时间序列几乎与对浮点数执行数学运算一样简单。

图 1-1. S&P 500 收盘价和年化波动率
将其翻译成专业的金融背景下,该示例意味着金融分析师可以——当应用正确的 Python 工具和库,提供高级抽象——专注于他们的领域,而不是技术细节。分析师可以更快地做出反应,几乎实时提供有价值的见解,并确保他们领先竞争对手一步。这个提高效率的例子很容易转化为可衡量的底线效应。
确保高性能
总的来说,人们普遍认为 Python 语法相对简洁,编码相对高效。然而,由于 Python 是一种解释语言,导致了“偏见”持续存在,即 Python 通常对金融中的计算密集型任务来说速度太慢了。事实上,根据特定的实现方法,Python 可能确实很慢。但它不一定慢— 它几乎可以在任何应用领域都表现出高性能。原则上,可以区分至少三种不同的策略来提高性能:
范式
通常情况下,Python 中有很多不同的方法可以得到相同的结果,但性能特性却大不相同;“简单地”选择正确的方式(例如特定的库)可以显著提高结果。
编译
如今,有几个性能库可用,它们提供了重要函数的编译版本,或者将 Python 代码静态或动态地(在运行时或调用时)编译为机器代码,速度可以提高数个数量级;其中流行的有Cython和Numba。
并行化
许多计算任务,特别是在金融领域,都可以从并行执行中获益;这并不是 Python 特有的,但是可以很容易地用 Python 实现。
使用 Python 进行性能计算
Python 本身不是一种高性能计算技术。然而,Python 已经发展成为访问当前性能技术的理想平台。从这个意义上说,Python 已经成为一种 性能计算的粘合语言。
后面的章节将详细介绍这三种技术。目前,我们希望使用一个简单但仍然现实的例子,涉及这三种技术。
在金融分析中,一个相当常见的任务是在大量数字数组上评估复杂的数学表达式。为此,Python 本身提供了一切所需的:
In [14]: loops = 2500000
import math
a = range(1, loops)
def f(x):
return 3 * math.log(x) + math.cos(x) ** 2
%timeit r = [f(x) for x in a]
1.52 s ± 29.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
在这种情况下,Python 解释器需要 1.5 秒来评估函数 f 250 万次。
同样的任务可以使用NumPy来实现,它提供了优化(即,预编译)的函数来处理这种基于数组的操作:
In [15]: import numpy as np
a = np.arange(1, loops)
%timeit r = 3 * np.log(a) + np.cos(a) ** 2
83.3 ms ± 1.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
使用NumPy可以大大减少执行时间至 90 毫秒。
然而,甚至有一个专门致力于这种任务的库。它被称为 numexpr,表示“数值表达式”。它将表达式编译以改进NumPy的一般功能,例如,在此过程中避免数组的内存副本:
In [16]: import numexpr as ne
ne.set_num_threads(1)
f = '3 * log(a) + cos(a) ** 2'
%timeit r = ne.evaluate(f)
78.2 ms ± 4.08 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
使用这种更专业的方法进一步将执行时间缩短到 80 毫秒。然而,numexpr 也具有内置功能来并行执行相应的操作。这使我们能够使用 CPU 的多个线程:
In [17]: ne.set_num_threads(4)
%timeit r = ne.evaluate(f)
21.9 ms ± 113 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
这将执行时间进一步缩短到约 25 毫秒,在此情况下,使用了四个线程。总体而言,这是性能提升超过 50 倍。特别注意,这种改进是可能的,而不需要改变基本的问题/算法,也不需要了解编译或并行化问题。即使是非专家也可以从高层次访问这些功能。当然,必须意识到存在哪些功能和选项。
该示例显示了 Python 提供了许多选项,以充分利用现有资源——即,提高生产力。使用顺序方法,每秒完成大约 31 百万次评估,而并行方法允许每秒进行超过 100 百万次评估——在这种情况下,只需告诉 Python 使用所有可用的 CPU 线程,而不是只使用一个线程。
从原型设计到生产
在交互式分析中的效率以及在执行速度方面的性能无疑是考虑 Python 的两个好处。然而,使用 Python 进行金融交易的另一个主要好处乍一看可能似乎有点微妙;第二次看可能会表现为一个重要的战略因素。这就是可以从原型设计到生产都可以使用 Python 的可能性。
当涉及到金融开发流程时,全球金融机构的实践通常以分离的、两步的过程为特征。一方面,有量化分析师(“量化分析师”)负责模型开发和技术原型设计。他们喜欢使用像Matlab和R这样的工具和环境,这些工具和环境允许进行快速、交互式的应用程序开发。在开发工作的这个阶段,性能、稳定性、异常管理、数据访问和分析的分离等问题并不那么重要。主要是在寻找概念验证和/或展示算法或整个应用程序主要期望功能的原型。
一旦原型完成,IT 部门与其开发人员接管并负责将现有的原型代码转换为可靠、可维护和高性能的生产代码。通常,在这个阶段会发生范式转变,使用像C++或Java这样的语言来满足生产的要求。此外,还会应用正式的开发流程,包括专业工具、版本控制等。
这种两步法的做法通常会产生一些通常意义上不期而遇的后果:
低效
原型代码不可重用;算法必须实现两次;冗余的工作需要时间和资源。
多样化的技能组合
不同部门展示出不同的技能集,并使用不同的语言来实现“相同的事物”。
遗留代码
代码可用且必须用不同的语言进行维护,通常使用不同的实现风格(例如,从架构的角度来看)。
另一方面,使用 Python 可以实现从最初的交互式原型设计步骤到高度可靠且高效可维护的生产代码的简化端到端流程。不同部门之间的沟通变得更容易。员工培训也更加简化,因为只有一种主要语言涵盖了金融应用构建的所有领域。它还避免了在开发过程的不同步骤中使用不同技术时固有的低效和冗余。总而言之,Python 几乎可以为金融应用开发和算法实现的几乎所有任务提供一致的技术框架。
人工智能优先金融
数据可用性
机器学习和深度学习
传统与人工智能优先金融
结论
Python 作为一种语言——但更多作为一个生态系统——是金融行业的理想技术框架。它具有许多优点,如优雅的语法、高效的开发方法以及适用于原型设计和生产等方面的可用性。凭借其大量可用的库和工具,Python 似乎对金融行业的最新发展所提出的大多数问题都有答案,例如分析、数据量和频率、合规性和监管,以及技术本身。它有潜力提供一个单一、强大、一致的框架,可以使端到端的开发和生产工作流程变得更加顺畅,即使是在较大的金融机构之间也是如此。
进一步阅读
以下书籍由同一作者撰写,详细介绍了本章中只是简要提及的许多方面(例如衍生品分析):
- Hilpisch, Yves (2015): Python 衍生品分析. Wiley Finance, Chichester, England. http://derivatives-analytics-with-python.com。
本章引用的语录来自以下资源:
-
Crosman, Penny (2013): “银行将如何使用其 2014 年 IT 预算的 8 种方式。” 银行技术新闻。
-
Deutsche Börse Group (2008): “全球衍生品市场——简介。” 白皮书。
-
Ding, Cubillas (2010): “优化场外交易定价和估值基础设施。” Celent 研究。
-
Lewis, Michael (2014): 闪电少年. W. W. Norton & Company, New York.
-
Patterson, Scott (2010): 量化分析师. Crown Business, New York.
¹ 例如,Python 是纽约市立大学巴鲁克学院金融工程硕士课程中使用的主要语言(请参阅http://mfe.baruch.cuny.edu)。
² 请参阅http://wiki.python.org/moin/BeginnersGuide,在那里您将找到许多对于初学者和非开发人员来说开始使用 Python 非常有价值的资源链接。
³ [Link to Come]提供了在随机数生成的背景下使用现代 GPGPU 的好处的示例。
第二章:Python 基础设施
在建造房屋时,有木材选择的问题。
重要的是,木匠的目标是携带能够良好切割的设备,并在有时间时磨削这些设备。
宫本武藏(《五轮书》)
介绍
对于新接触 Python 的人来说,Python 部署似乎一切都不简单。对于可以选择安装的丰富库和包也是如此。首先,Python 不只有 一个 版本。 Python 有许多不同的版本,如 CPython,Jython,IronPython 或 PyPy。然后仍然存在 Python 2.7 和 3.x 世界之间的差异。[¹] 接下来,本章重点关注 CPython,迄今为止最流行的 Python 编程语言版本,以及 版本 3.6。
即使专注于 CPython 3.6(以下简称Python),部署也因许多其他原因变得困难:
-
解释器(标准 CPython 安装)只带有所谓的 标准库(例如,涵盖典型的数学函数)
-
可选的 Python 包需要单独安装 —— 而且有数百个
-
由于依赖关系和操作系统特定要求,编译/构建这些非标准包可能会很棘手
-
要注意这些依赖关系并保持版本一致性随着时间的推移(即维护)通常是繁琐且耗时的。
-
对某些包的更新和升级可能导致需要重新编译大量其他包
-
更改或替换一个包可能会在(许多)其他地方引起麻烦
幸运的是,有一些工具和策略可用于帮助解决 Python 部署问题。本章介绍了以下几种有助于 Python 部署的技术类型:
-
虚拟环境管理器:虚拟环境管理器如
virtualenv或conda允许并行管理多个 Python 安装(例如,在单个计算机上同时拥有 Python 2.7 和 3.6 安装,或者在不冒风险的情况下测试最新的流行 Python 包的开发版本) -
容器:Docker容器代表包含运行某个软件所需的所有系统部件的完整文件系统,如代码、运行时或系统工具;例如,你可以在运行 Mac OS 或 Windows 10 的机器上运行一个包含 Ubuntu 16.04 操作系统、Python 3.6 安装和相应 Python 代码的 Docker 容器中运行。
-
云实例:部署用于算法交易的 Python 代码通常需要高可用性、安全性和性能;这些要求通常只能通过使用现在以有吸引力条件提供的专业计算和存储基础设施来满足,这些基础设施形式可从相对较小到真正大型和强大的云实例;与长期租用的专用服务器相比,云实例,即虚拟服务器的一个好处是,用户通常只需支付实际使用的小时数;另一个优点是,如果需要,这些云实例可以在一两分钟内立即使用,这有助于敏捷开发和可扩展性。
本章的结构如下所示
“Conda 作为包管理器”
本节介绍了conda作为 Python 的包管理器。
“作为虚拟环境管理器的 Conda”
本节重点介绍conda作为虚拟环境管理器的功能。
“使用 Docker 容器化”
本节简要介绍 Docker 作为容器化技术,并侧重于构建具有 Python 3.6 安装的基于 Ubuntu 的容器。
“使用云实例”
本节展示了如何在云中部署 Python 和 Jupyter Notebook —— 作为强大的基于浏览器的工具套件 —— 用于 Python 开发。
本章的目标是在专业基础设施上具有正确的 Python 安装,并可用最重要的数值和数据分析包,然后这种组合作为在后续章节中实现和部署 Python 代码的骨干,无论是交互式的金融分析代码还是脚本和模块形式的代码。
Conda 作为包管理器
虽然conda可以单独安装,但高效的方式是通过 Miniconda,这是一个包括conda作为包和虚拟环境管理器的最小 Python 发行版。
安装 Miniconda 3.6
您可以在Miniconda 页面上下载不同版本的 Miniconda。在接下来的内容中,假定使用 Python 3.6 64 位版本,该版本适用于 Linux、Windows 和 Mac OS。本小节的主要示例是在基于 Ubuntu 的 Docker 容器中进行会话,通过 wget 下载 Linux 64 位安装程序,然后安装 Miniconda。如所示的代码应在任何其他基于 Linux 或 Mac OS 的机器上无需修改即可正常工作。
$ docker run -ti -h py4fi -p 9999:9999 ubuntu:latest /bin/bash
root@py4fi:/# apt-get update; apt-get upgrade -y
...
root@py4fi:/# apt-get install wget bzip2 gcc
...
root@py4fi:/# wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
--2017-11-04 10:52:09-- https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
Resolving repo.continuum.io (repo.continuum.io)... 104.16.19.10, 104.16.18.10, 2400:cb00:2048:1::6810:120a, ...
Connecting to repo.continuum.io (repo.continuum.io)|104.16.19.10|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 54167345 (52M) [application/x-sh]
Saving to: 'Miniconda3-latest-Linux-x86_64.sh'
Miniconda3-latest-Lin 100%[=======================>] 51.66M 1.57MB/s in 32s
2017-11-04 10:52:41 (1.62 MB/s) - 'Miniconda3-latest-Linux-x86_64.sh' saved [54167345/54167345]
root@py4fi:/# bash Miniconda3-latest-Linux-x86_64.sh
Welcome to Miniconda3 4.3.30
In order to continue the installation process, please review the license
agreement.
Please, press ENTER to continue
>>>
简单地按下ENTER键即可开始安装过程。在审查许可协议后,通过回答yes来批准条款。
Do you approve the license terms? [yes|no]
>>> yes
Miniconda3 will now be installed into this location:
/root/miniconda3
- Press ENTER to confirm the location
- Press CTRL-C to abort the installation
- Or specify a different location below
[/root/miniconda3] >>>
PREFIX=/root/miniconda3
installing: python-3.6.3-hc9025b9_1 ...
Python 3.6.3 :: Anaconda, Inc.
installing: ca-certificates-2017.08.26-h1d4fec5_0 ...
installing: conda-env-2.6.0-h36134e3_1 ...
installing: libgcc-ng-7.2.0-h7cc24e2_2 ...
installing: libstdcxx-ng-7.2.0-h7a57d05_2 ...
installing: libffi-3.2.1-h4deb6c0_3 ...
installing: ncurses-6.0-h06874d7_1 ...
installing: openssl-1.0.2l-h077ae2c_5 ...
installing: tk-8.6.7-h5979e9b_1 ...
installing: xz-5.2.3-h2bcbf08_1 ...
installing: yaml-0.1.7-h96e3832_1 ...
installing: zlib-1.2.11-hfbfcf68_1 ...
installing: libedit-3.1-heed3624_0 ...
installing: readline-7.0-hac23ff0_3 ...
installing: sqlite-3.20.1-h6d8b0f3_1 ...
installing: asn1crypto-0.22.0-py36h265ca7c_1 ...
installing: certifi-2017.7.27.1-py36h8b7b77e_0 ...
installing: chardet-3.0.4-py36h0f667ec_1 ...
installing: idna-2.6-py36h82fb2a8_1 ...
installing: pycosat-0.6.2-py36h1a0ea17_1 ...
installing: pycparser-2.18-py36hf9f622e_1 ...
installing: pysocks-1.6.7-py36hd97a5b1_1 ...
installing: ruamel_yaml-0.11.14-py36ha2fb22d_2 ...
installing: six-1.10.0-py36hcac75e4_1 ...
installing: cffi-1.10.0-py36had8d393_1 ...
installing: setuptools-36.5.0-py36he42e2e1_0 ...
installing: cryptography-2.0.3-py36ha225213_1 ...
installing: wheel-0.29.0-py36he7f4e38_1 ...
installing: pip-9.0.1-py36h8ec8b28_3 ...
installing: pyopenssl-17.2.0-py36h5cc804b_0 ...
installing: urllib3-1.22-py36hbe7ace6_0 ...
installing: requests-2.18.4-py36he2e5f8d_1 ...
installing: conda-4.3.30-py36h5d9f9f4_0 ...
installation finished.
在您同意许可条款并确认安装位置之后,您应该允许 Miniconda 将新的 Miniconda 安装位置添加到 PATH 环境变量中,通过再次回答 yes。
Do you wish the installer to prepend the Miniconda3 install location
to PATH in your /root/.bashrc ? [yes|no]
[no] >>> yes
完成这个相当简单的安装过程后,现在既有一个基本的 Python 安装,也有conda可用。基本的 Python 安装已经包含了一些不错的功能,比如 SQLite3 数据库引擎。您可以尝试看看是否可以在新的 shell 实例中启动 Python,或者在追加相关路径到相应的环境变量后启动 Python。
root@py4fi:/# export PATH="/root/miniconda3/bin/:$PATH"
root@py4fi:/# python
Python 3.6.3 |Anaconda, Inc.| (default, Oct 13 2017, 12:02:49)
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print('Hello Algo Trading World.')
Hello Algo Trading World.
>>> exit()
root@py4fi:/#
Conda 基本操作
Conda 可以高效地处理,安装,更新和删除 Python 包,等等。以下列表提供了主要功能的概述。
安装 Python x.x
conda install python=x.x
更新 Python
conda update python
安装一个包
conda install $PACKAGE_NAME
更新一个包
conda update $PACKAGE_NAME
移除一个包
conda remove $PACKAGE_NAME
更新 conda 本身
conda update conda
搜索包
conda search $SEARCH_TERM
列出已安装的包
conda list
有了这些功能,例如,安装NumPy——作为所谓科学堆栈中最重要的库之一——只需一条命令。当在装有英特尔处理器的机器上进行安装时,该过程会自动安装英特尔数学核心库 mkl,这不仅加速了在英特尔机器上的NumPy的数值操作,还加速了其他几个科学 Python 包的数值操作。
root@py4fi:/# conda install numpy
Fetching package metadata ...........
Solving package specifications: .
Package plan for installation in environment /root/miniconda3:
The following NEW packages will be INSTALLED:
intel-openmp: 2018.0.0-h15fc484_7
mkl: 2018.0.0-hb491cac_4
numpy: 1.13.3-py36ha12f23b_0
Proceed ([y]/n)? y
intel-openmp-2 100% |######################################| Time: 0:00:00 1.78 MB/s
mkl-2018.0.0-h 100% |######################################| Time: 0:01:27 2.08 MB/s
numpy-1.13.3-p 100% |######################################| Time: 0:00:02 1.36 MB/s
root@py4fi:/#
也可以一次性安装多个包。-y 标志表示所有(可能的)问题都应回答“是”。
conda install -y ipython matplotlib pandas pytables scipy seaborn
安装完成后,除了标准库之外,一些金融分析中最重要的库也可用。
一个改进的交互式 Python shell
Python 中的标准绘图库
高效处理数值数组
管理表格数据,如金融时间序列数据
用于HDF5库的 Python 封装
一组科学类和函数(作为依赖项安装)
一个绘图库,添加了统计功能和良好的绘图默认值
这提供了一套用于一般数据分析和金融分析的基本工具集。下面的示例使用 IPython 并使用 NumPy 绘制一组伪随机数。
root@py4fi:/# ipython
Python 3.6.3 |Anaconda, Inc.| (default, Oct 13 2017, 12:02:49)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: import numpy as np
In [2]: np.random.standard_normal((5, 4))
Out[2]:
array([[-0.11906643, 0.65717731, 0.3763829 , 0.04928928],
[-0.88728758, -0.41263027, -0.70284906, -0.42948971],
[-0.43964908, -0.21453161, 0.63860994, -0.24972815],
[-0.50119552, -0.68807591, -0.03928424, 3.03200697],
[-0.19549982, 0.92946315, -1.86523627, 0.37608079]])
In [3]: exit
root@py4fi:/#
执行 conda list 可以验证安装了哪些包。
root@py4fi:/# conda list
# packages in environment at /root/miniconda3:
#
asn1crypto 0.22.0 py36h265ca7c_1
bzip2 1.0.6 h0376d23_1
ca-certificates 2017.08.26 h1d4fec5_0
certifi 2017.7.27.1 py36h8b7b77e_0
cffi 1.10.0 py36had8d393_1
chardet 3.0.4 py36h0f667ec_1
conda 4.3.30 py36h5d9f9f4_0
conda-env 2.6.0 h36134e3_1
cryptography 2.0.3 py36ha225213_1
cycler 0.10.0 py36h93f1223_0
...
matplotlib 2.1.0 py36hba5de38_0
mkl 2018.0.0 hb491cac_4
ncurses 6.0 h06874d7_1
numexpr 2.6.2 py36hdd3393f_1
numpy 1.13.3 py36ha12f23b_0
openssl 1.0.2l h077ae2c_5
pandas 0.20.3 py36h842e28d_2
...
python 3.6.3 hc9025b9_1
python-dateutil 2.6.1 py36h88d3b88_1
pytz 2017.2 py36hc2ccc2a_1
qt 5.6.2 h974d657_12
readline 7.0 hac23ff0_3
requests 2.18.4 py36he2e5f8d_1
ruamel_yaml 0.11.14 py36ha2fb22d_2
scipy 0.19.1 py36h9976243_3
seaborn 0.8.0 py36h197244f_0
setuptools 36.5.0 py36he42e2e1_0
simplegeneric 0.8.1 py36h2cb9092_0
sip 4.18.1 py36h51ed4ed_2
six 1.10.0 py36hcac75e4_1
sqlite 3.20.1 h6d8b0f3_1
statsmodels 0.8.0 py36h8533d0b_0
tk 8.6.7 h5979e9b_1
tornado 4.5.2 py36h1283b2a_0
traitlets 4.3.2 py36h674d592_0
urllib3 1.22 py36hbe7ace6_0
wcwidth 0.1.7 py36hdf4376a_0
wheel 0.29.0 py36he7f4e38_1
xz 5.2.3 h2bcbf08_1
yaml 0.1.7 h96e3832_1
zlib 1.2.11 hfbfcf68_1
root@py4fi:/#
如果一个包不再需要,则可以使用 conda remove 高效地移除它。
root@py4fi:/# conda remove seaborn
Fetching package metadata ...........
Solving package specifications: .
Package plan for package removal in environment /root/miniconda3:
The following packages will be REMOVED:
seaborn: 0.8.0-py36h197244f_0
Proceed ([y]/n)? y
root@py4fi:/#
作为包管理器的conda已经非常有用。但是,只有在添加虚拟环境管理时,其全部功能才会显现出来。
提示
conda 作为一个包管理器,使得安装、更新和移除 Python 包变得愉快。不再需要自行构建和编译包 — 有时候这可能会很棘手,因为一个包指定了一长串依赖项,而且还要考虑到在不同操作系统上的特定情况。
Conda 作为虚拟环境管理器
安装了包含 conda 的 Miniconda 后,会根据选择的 Miniconda 版本提供一个默认的 Python 安装。conda 的虚拟环境管理功能允许在 Python 3.6 默认安装中添加一个完全独立的 Python 2.7.x 安装。为此,conda 提供了以下功能。
创建一个虚拟环境
conda create --name $ENVIRONMENT_NAME
激活一个环境
source activate $ENVIRONMENT_NAME
停用一个环境
source deactivate $ENVIRONMENT_NAME
移除一个环境
conda-env remove --name $ENVIRONMENT_NAME
导出到一个环境文件
conda env export > $FILE_NAME
从文件创建一个环境
conda env create -f $FILE_NAME
列出所有环境
conda info --envs
作为一个简单的示例,接下来的示例代码创建了一个名为 py27 的环境,安装了 IPython 并执行了一行 Python 2.7.x 代码。
root@py4fi:/# conda create --name py27 python=2.7
Fetching package metadata ...........
Solving package specifications: .
Package plan for installation in environment /root/miniconda3/envs/py27:
The following NEW packages will be INSTALLED:
ca-certificates: 2017.08.26-h1d4fec5_0
certifi: 2017.7.27.1-py27h9ceb091_0
libedit: 3.1-heed3624_0
libffi: 3.2.1-h4deb6c0_3
libgcc-ng: 7.2.0-h7cc24e2_2
libstdcxx-ng: 7.2.0-h7a57d05_2
ncurses: 6.0-h06874d7_1
openssl: 1.0.2l-h077ae2c_5
pip: 9.0.1-py27ha730c48_4
python: 2.7.14-h89e7a4a_22
readline: 7.0-hac23ff0_3
setuptools: 36.5.0-py27h68b189e_0
sqlite: 3.20.1-h6d8b0f3_1
tk: 8.6.7-h5979e9b_1
wheel: 0.29.0-py27h411dd7b_1
zlib: 1.2.11-hfbfcf68_1
Proceed ([y]/n)? y
Fetching package metadata ...........
Solving package specifications: .
...
#
# To activate this environment, use:
# > source activate py27
#
# To deactivate an active environment, use:
# > source deactivate
#
root@py4fi:/#
注意,在激活环境后提示符如何变为 (py27)。²
root@py4fi:/# source activate py27
(py27) root@py4fi:/# conda install -y ipython
Fetching package metadata ...........
Solving package specifications: .
...
最后,使用 Python 2.7 语法的 IPython。
(py27) root@py4fi:/# ipython
Python 2.7.14 |Anaconda, Inc.| (default, Oct 27 2017, 18:21:12)
Type "copyright", "credits" or "license" for more information.
IPython 5.4.1 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]: print "Hello Algo Trading World!"
Hello Algo Trading World!
In [2]: exit
(py27) root@py4fi:/#
正如这个例子所示,conda 作为一个虚拟环境管理器允许在一起安装不同的 Python 版本。它还允许安装某些包的不同版本。默认 Python 安装不受这种过程的影响,也不受同一台机器上可能存在的其他环境的影响。所有可用的环境可以通过 conda info --envs 显示。
(py27) root@py4fi:/# conda info --envs
# conda environments:
#
py27 * /root/miniconda3/envs/py27
root /root/miniconda3
(py27) root@py4fi:/#
有时需要与他人共享环境信息或在多台机器上使用环境信息。为此,可以使用 conda env export 将安装的包列表导出到一个文件中。
(py27) root@py4fi:/# conda env export > py27env.yml
(py27) root@py4fi:/# cat py27env.yml
name: py27
channels:
- defaults
dependencies:
- backports=1.0=py27h63c9359_1
- backports.shutil_get_terminal_size=1.0.0=py27h5bc021e_2
- ca-certificates=2017.08.26=h1d4fec5_0
- certifi=2017.7.27.1=py27h9ceb091_0
- decorator=4.1.2=py27h1544723_0
- enum34=1.1.6=py27h99a27e9_1
- ipython=5.4.1=py27h36c99b6_1
- ipython_genutils=0.2.0=py27h89fb69b_0
- libedit=3.1=heed3624_0
- libffi=3.2.1=h4deb6c0_3
- libgcc-ng=7.2.0=h7cc24e2_2
- libstdcxx-ng=7.2.0=h7a57d05_2
- ncurses=6.0=h06874d7_1
- openssl=1.0.2l=h077ae2c_5
- pathlib2=2.3.0=py27h6e9d198_0
- pexpect=4.2.1=py27hcf82287_0
- pickleshare=0.7.4=py27h09770e1_0
- pip=9.0.1=py27ha730c48_4
- prompt_toolkit=1.0.15=py27h1b593e1_0
- ptyprocess=0.5.2=py27h4ccb14c_0
- pygments=2.2.0=py27h4a8b6f5_0
- python=2.7.14=h89e7a4a_22
- readline=7.0=hac23ff0_3
- scandir=1.6=py27hf7388dc_0
- setuptools=36.5.0=py27h68b189e_0
- simplegeneric=0.8.1=py27h19e43cd_0
- six=1.11.0=py27h5f960f1_1
- sqlite=3.20.1=h6d8b0f3_1
- tk=8.6.7=h5979e9b_1
- traitlets=4.3.2=py27hd6ce930_0
- wcwidth=0.1.7=py27h9e3e1ab_0
- wheel=0.29.0=py27h411dd7b_1
- zlib=1.2.11=hfbfcf68_1
- pip:
- backports.shutil-get-terminal-size==1.0.0
- ipython-genutils==0.2.0
- prompt-toolkit==1.0.15
prefix: /root/miniconda3/envs/py27
(py27) root@py4fi:/#
通常,虚拟环境(从技术上讲,不过是一种特定的(子)文件夹结构)是为了进行一些快速测试而创建的。在这种情况下,通过 conda env remove 轻松删除环境。
(py27) root@py4fi:/# source deactivate
root@py4fi:/# conda env remove --name py27
Package plan for package removal in environment /root/miniconda3/envs/py27:
The following packages will be REMOVED:
backports: 1.0-py27h63c9359_1
backports.shutil_get_terminal_size: 1.0.0-py27h5bc021e_2
ca-certificates: 2017.08.26-h1d4fec5_0
certifi: 2017.7.27.1-py27h9ceb091_0
...
traitlets: 4.3.2-py27hd6ce930_0
wcwidth: 0.1.7-py27h9e3e1ab_0
wheel: 0.29.0-py27h411dd7b_1
zlib: 1.2.11-hfbfcf68_1
Proceed ([y]/n)? y
#
这就结束了 conda 作为虚拟环境管理器的概述。
提示
conda 不仅帮助管理包,还是 Python 的虚拟环境管理器。它简化了不同 Python 环境的创建,允许在同一台机器上有多个 Python 版本和可选包可用,而且它们之间互不影响。conda 还允许将环境信息导出,以便在多台机器上轻松复制它或与他人共享。
使用 Docker 容器化
Docker 容器已经风靡了 IT 世界。尽管技术仍然很年轻,但它已经确立了自己作为几乎任何类型软件应用的高效开发和部署的基准之一。
对于我们的目的,将 Docker 容器视为一个分离的(“容器化的”)文件系统足以包含操作系统(例如服务器上的 Ubuntu 16.04),一个(Python)运行时,额外的系统和开发工具以及根据需要的其他(Python)库和软件包。这样的 Docker 容器可以在具有 Windows 10 的本地机器上运行,也可以在具有 Linux 操作系统的云实例上运行,例如。
本节不允许深入探讨 Docker 容器的有趣细节。它更多地是对 Docker 技术在 Python 部署上的简要说明。⁴
Docker 镜像和容器
然而,在进入说明之前,谈论 Docker 时需要区分两个基本术语。第一个是Docker 镜像,可以类比为 Python 类。第二个是Docker 容器,可以类比为相应 Python 类的实例。
在更技术层面上,在Docker 术语表中找到了对Docker 镜像的以下定义:
Docker 镜像是容器的基础。一个镜像是一组有序的根文件系统更改和相应的执行参数,用于容器运行时。一个镜像通常包含叠加在彼此上面的分层文件系统的联合。镜像没有状态,永远不会改变。
同样,您可以在Docker 术语表中找到对Docker 容器的以下定义,它将类比为 Python 类和这些类的实例:
容器是 Docker 镜像的运行时实例。Docker 容器包括:一个 Docker 镜像,一个执行环境和一组标准指令。
根据操作系统的不同,安装 Docker 的方式略有不同。这就是为什么本节不涉及相应细节的原因。详细信息请参阅安装 Docker 引擎页面。
构建一个 Ubuntu 和 Python Docker 镜像
本小节说明了基于最新版本的 Ubuntu 构建 Docker 镜像的过程,该镜像包含 Miniconda 以及一些重要的 Python 包。此外,它还通过更新 Linux 软件包索引,必要时升级软件包,并安装某些额外的系统工具来进行一些 Linux 的维护工作。为此,需要两个脚本。一个是在 Linux 级别执行所有工作的bash脚本。⁵另一个是所谓的Dockerfile,它控制镜像本身的构建过程。
示例 2-1 中的 bash 脚本负责安装,由三个主要部分组成。第一部分处理 Linux 的基本事务。第二部分安装 Miniconda,而第三部分安装可选的 Python 包。内联还有更详细的注释。
示例 2-1. 安装 Python 和可选包的脚本
#!/bin/bash
#
# Script to Install
# Linux System Tools and
# Basic Python Components
#
# Python for Finance
# (c) Dr. Yves J. Hilpisch
#
# GENERAL LINUX
apt-get update # updates the package index cache
apt-get upgrade -y # updates packages
# installs system tools
apt-get install -y bzip2 gcc git htop screen vim wget
apt-get upgrade -y bash # upgrades bash if necessary
apt-get clean # cleans up the package index cache
# INSTALL MINICONDA
# downloads Miniconda
wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O Miniconda.sh
bash Miniconda.sh -b # installs it
rm -rf Miniconda.sh # removes the installer
export PATH="/root/miniconda3/bin:$PATH" # prepends the new path
# INSTALL PYTHON LIBRARIES
conda install -y pandas # installs pandas
conda install -y ipython # installs IPython shell
示例 2-2 中的 Dockerfile 使用 示例 2-1 中的 bash 脚本构建新的 Docker 镜像。它还将其主要部分作为注释内联。
示例 2-2. 构建镜像的 Dockerfile
#
# Building a Docker Image with
# the Latest Ubuntu Version and
# Basic Python Install
#
# Python for Finance
# (c) Dr. Yves J. Hilpisch
#
# latest Ubuntu version
FROM ubuntu:latest
# information about maintainer
MAINTAINER yves
# add the bash script
ADD install.sh /
# change rights for the script
RUN chmod u+x /install.sh
# run the bash script
RUN /install.sh
# prepend the new path
ENV PATH /root/miniconda3/bin:$PATH
# execute IPython when container is run
CMD ["ipython"]
如果这两个文件在同一个文件夹中,并且已安装 Docker,则构建新的 Docker 镜像就很简单。在这里,标签 ubuntupython 用于该镜像。这个标签在需要引用镜像时很重要,例如在基于它运行容器时。
macbookpro:~/Docker$ docker build -t py4fi:basic .
Sending build context to Docker daemon 29.7kB
Step 1/7 : FROM ubuntu:latest
---> 747cb2d60bbe
...
Step 7/7 : CMD ["ipython"]
---> Running in fddb07301003
Removing intermediate container fddb07301003
---> 60a180c9cfa6
Successfully built 60a180c9cfa6
Successfully tagged py4fi:basic
macbookpro:~/Docker$
可以通过 docker images 列出现有的 Docker 镜像。新镜像应该位于列表的顶部。
macbookpro:~/Docker$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
py4fi basic 60a180c9cfa6 About a minute ago 1.65GB
ubuntu python 9fa4649d110f 4 days ago 2.46GB
<none> <none> 5ac1e7b7bd9f 4 days ago 2.46GB
ubuntu base bcee12a18154 6 days ago 2.29GB
<none> <none> 032acb2af94c 6 days ago 122MB
tpq python a9dc75f040f6 12 days ago 2.55GB
ubuntu latest 747cb2d60bbe 3 weeks ago 122MB
macbookpro:~/Docker$
成功构建 ubuntupython 镜像后,可以使用 docker run 运行相应的 Docker 容器。参数组合 -ti 用于在 Docker 容器中运行交互式进程,比如 shell 进程(参见 Docker Run 参考页)。
macbookpro:~/Docker$ docker run -ti py4fi:basic
Python 3.6.3 |Anaconda, Inc.| (default, Oct 13 2017, 12:02:49)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: import numpy as np
In [2]: a = np.random.standard_normal((5, 3))
In [3]: import pandas as pd
In [4]: df = pd.DataFrame(a, columns=['a', 'b', 'c'])
In [5]: df
Out[5]:
a b c
0 0.062129 0.040233 0.494589
1 -0.681517 -0.307187 -0.476016
2 0.861527 0.438467 -1.656811
3 -1.402893 0.611978 0.238889
4 0.876606 0.746728 0.840246
In [6]:
退出 IPython 也将退出容器,因为它是容器中唯一运行的应用程序。然而,你可以通过
Ctrl+p --> Ctrl+q
从容器中分离后,docker ps 命令显示运行的容器(可能还有其他当前正在运行的容器):
macbookpro:~/Dropbox/Platform/algobox/algobook/book/code/Docker$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
98b95440f962 py4fi:basic "ipython" 3 minutes ago Up 3 minutes wonderful_neumann
4b85dfc94780 ubuntu:latest "/bin/bash" About an hour ago Up About an hour 0.0.0.0:9999->9999/tcp musing_einstein
macbookpro:~/Docker$
通过 docker attach $CONTAINER_ID(注意,容器 ID 的几个字母就足够了)连接到 Docker 容器:
macbookpro:~/Docker$ docker attach 98b95
In [6]: df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5 entries, 0 to 4
Data columns (total 3 columns):
a 5 non-null float64
b 5 non-null float64
c 5 non-null float64
dtypes: float64(3)
memory usage: 200.0 bytes
In [7]: exit
macbookpro:~/Docker$
exit 命令终止 IPython,从而终止 Docker 容器。它可以通过 docker rm 删除。
macbookpro:~/Docker$ docker rm 98b95
d9efb
macbookpro:~/Docker$
类似地,如果不再需要,可以通过 docker rmi 删除 Docker 镜像 ubuntupython。虽然容器相对轻量级,但单个镜像可能会消耗大量存储空间。在 py4fi:basic 镜像的情况下,其大小超过 1 GB。这就是为什么你可能想定期清理 Docker 镜像列表的原因。
macbookpro:~/Docker$ docker rmi 60a180
当然,关于 Docker 容器及其在某些应用场景中的优势还有很多值得说的。对于本书和在线培训课程而言,它们提供了一种现代化的方法,用于部署 Python,在完全分离的(容器化)环境中进行 Python 开发,并为算法交易提供代码。
提示
如果你还没有使用 Docker 容器,应考虑开始使用它们。在 Python 部署和开发工作中,它们提供了许多好处,不仅在本地工作时,而且在使用远程云实例和服务器部署算法交易代码时尤其如此。
使用云实例
本节展示了如何在 DigitalOcean 云实例上设置一个完整的 Python 基础设施。还有许多其他的云服务提供商,其中包括 亚马逊网络服务(AWS)作为领先的提供商。然而,DigitalOcean 以其简单性和相对较低的价格而闻名,他们称之为 droplet 的较小的云实例。最小的 droplet,通常足够用于探索和开发目的,每月只需 5 美元或每小时 0.007 美元。用户只按小时计费,因此您可以轻松地为 2 小时启动一个 droplet,然后销毁它,仅需收取 0.014 美元。如果您还没有账户,请在这个 注册页面 上注册一个账户,这可以为您提供 10 美元的起始信用额。
本节的目标是在 DigitalOcean 上设置一个 droplet,其中包含 Python 3.6 安装和通常所需的包(例如 NumPy、pandas),以及一个密码保护和安全套接字层(SSL)加密的 Jupyter Notebook 服务器安装。作为一个基于 Web 的工具套件,Jupyter Notebook 提供了三个主要工具,可以通过常规浏览器使用:
-
Jupyter Notebook:这是目前非常流行的交互式开发环境,具有不同语言内核的选择,如 Python、R 和 Julia。
-
终端:通过浏览器访问的系统 shell 实现,允许进行所有典型的系统管理任务,但也可以使用诸如
Vim或git等有用工具。 -
编辑器:第三个主要工具是一个基于浏览器的文件编辑器,具有许多不同的编程语言和文件类型的语法突出显示以及典型的编辑功能。
在一个 droplet 上安装 Jupyter Notebook 允许通过浏览器进行 Python 开发和部署,避免了通过安全外壳(SSH)访问登录云实例的需求。
为了完成本节的目标,需要一些文件。
-
服务器设置脚本:这个脚本协调所有必要的步骤,比如将其他文件复制到 droplet 上并在 droplet 上运行它们。
-
Python 和 Jupyter 安装脚本:这个脚本安装 Python、额外的包、Jupyter Notebook 并启动 Jupyter Notebook 服务器。
-
Jupyter Notebook 配置文件:此文件用于配置 Jupyter Notebook 服务器,例如密码保护方面。
-
RSA 公钥和私钥文件:这两个文件用于对 Jupyter Notebook 服务器进行 SSL 加密。
接下来,我们将逆向处理这个文件列表。
RSA 公钥和私钥
为了通过任意浏览器安全连接到 Jupyter Notebook 服务器,需要一个由 RSA 公钥和私钥组成的 SSL 证书(参见RSA 维基百科页面)。一般来说,人们期望这样的证书来自所谓的证书颁发机构(CA)。然而,在本书的目的下,自动生成的证书已经“足够好“了。[⁶] 生成 RSA 密钥对的流行工具是OpenSSL。接下来的简要交互式会话生成了适用于 Jupyter Notebook 服务器的证书。
macbookpro:~/cloud$ openssl req -x509 -nodes -days 365 -newkey \
> rsa:1024 -out cert.pem -keyout cert.key
Generating a 1024 bit RSA private key
..++++++
.......++++++
writing new private key to 'cert.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:DE
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:Voelklingen
Organization Name (eg, company) [Internet Widgits Pty Ltd]:TPQ GmbH
Organizational Unit Name (eg, section) []:Algo Trading
Common Name (e.g. server FQDN or YOUR name) []:Jupyter
Email Address []:team@tpq.io
macbookpro:~/cloud$ ls
cert.key cert.pem
macbookpro:~/cloud$
两个文件 cert.key 和 cert.pem 需要复制到滴管上,并且需要被 Jupyter Notebook 配置文件引用。下面会介绍这个文件。
Jupyter Notebook 配置文件
可以按照运行 Notebook 服务器页面上的说明安全地部署一个公共 Jupyter Notebook 服务器。其中,Jupyter Notebook 应该受到密码保护。为此,有一个叫做 passwd 的密码哈希生成函数,可在 notebook.auth 子包中使用。下面的代码生成了一个以 jupyter 为密码的密码哈希代码。
macbookpro:~/cloud$ ipython
Python 3.6.1 |Continuum Analytics, Inc.| (default, May 11 2017, 13:04:09)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: from notebook.auth import passwd
In [2]: passwd('jupyter')
Out[2]: 'sha1:4c72b4542011:0a8735a18ef2cba12fde3744886e61f76706299b'
In [3]: exit
此哈希代码需要放置在 Jupyter Notebook 配置文件中,如示例 2-3 所示。该代码假定 RSA 密钥文件已复制到了滴管的 /root/.jupyter/ 文件夹中。
示例 2-3. Jupyter Notebook 配置文件
#
# Jupyter Notebook Configuration File
#
# Python for Finance
# (c) Dr. Yves J. Hilpisch
#
# SSL ENCRYPTION
# replace the following file names (and files used) by your choice/files
c.NotebookApp.certfile = u'/root/.jupyter/cert.pem'
c.NotebookApp.keyfile = u'/root/.jupyter/cert.key'
# IP ADDRESS AND PORT
# set ip to '*' to bind on all IP addresses of the cloud instance
c.NotebookApp.ip = '*'
# it is a good idea to set a known, fixed default port for server access
c.NotebookApp.port = 8888
# PASSWORD PROTECTION
# here: 'jupyter' as password
# replace the hash code with the one for your password
c.NotebookApp.password = 'sha1:bb5e01be158a:99465f872e0613a3041ec25b7860175f59847702'
# NO BROWSER OPTION
# prevent Jupyter from trying to open a browser
c.NotebookApp.open_browser = False
注意
将 Jupyter Notebook 部署在云中主要会引发一系列安全问题,因为它是一个通过 Web 浏览器可访问的完整的开发环境。因此,使用 Jupyter Notebook 服务器默认提供的安全措施(如密码保护和 SSL 加密)至关重要。但这只是开始,根据在云实例上具体做了什么,可能建议采取进一步的安全措施。
下一步是确保 Python 和 Jupyter Notebook 在滴管上安装。
Python 和 Jupyter Notebook 的安装脚本
用于安装 Python 和 Jupyter Notebook 的 bash 脚本类似于在“使用 Docker 容器化”一节中介绍的用于在 Docker 容器中通过 Miniconda 安装 Python 的脚本。然而,这里的脚本还需要启动 Jupyter Notebook 服务器。所有主要部分和代码行都在内联注释中。
示例 2-4. 安装 Python 并运行 Jupyter Notebook 服务器的 bash 脚本
#!/bin/bash
#
# Script to Install
# Linux System Tools and Basic Python Components
# as well as to
# Start Jupyter Notebook Server
#
# Python for Finance
# (c) Dr. Yves J. Hilpisch
#
# GENERAL LINUX
apt-get update # updates the package index cache
apt-get upgrade -y # updates packages
# install system tools
apt-get install -y bzip2 gcc git htop screen htop vim wget
apt-get upgrade -y bash # upgrades bash if necessary
apt-get clean # cleans up the package index cache
# INSTALLING MINICONDA
wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O Miniconda.sh
bash Miniconda.sh -b # installs Miniconda
rm -rf Miniconda.sh # removes the installer
# prepends the new path for current session
export PATH="/root/miniconda3/bin:$PATH"
# prepends the new path in the shell configuration
cat >> ~/.profile <<EOF
export PATH="/root/miniconda3/bin:$PATH"
EOF
# INSTALLING PYTHON LIBRARIES
conda install -y jupyter # interactive data analytics in the browser
conda install -y pytables # wrapper for HDF5 binary storage
conda install -y pandas # data analysis package
conda install -y pandas-datareader # retrieval of open data
conda install -y matplotlib # standard plotting library
conda install -y seaborn # statistical plotting library
conda install -y quandl # wrapper for Quandl data API
conda install -y scikit-learn # machine learning library
conda install -y tensorflow # deep learning library
conda install -y flask # light weight web framework
conda install -y openpyxl # library for Excel interaction
conda install -y pyyaml # library to manage yaml files
pip install --upgrade pip # upgrading the package manager
pip install q # logging and debugging
pip install plotly # interactive D3.js plots
pip install cufflinks # combining plotly with pandas
# COPYING FILES AND CREATING DIRECTORIES
mkdir /root/.jupyter
mv /root/jupyter_notebook_config.py /root/.jupyter/
mv /root/cert.* /root/.jupyter
mkdir /root/notebook
cd /root/notebook
# STARTING JUPYTER NOTEBOOK
jupyter notebook --allow-root
此脚本需要复制到滴管上,并且需要由下一小节中描述的编排脚本启动。
管理滴管设置的脚本
设置滴水筹的第二个 bash 脚本是最短的。它主要将所有其他文件复制到滴水筹中,其中预期的是相应的 IP 地址作为参数。在最后一行,它启动install.sh bash 脚本,后者又会进行安装并启动 Jupyter Notebook 服务器。
示例 2-5. 用于设置滴水筹的bash脚本
#!/bin/bash
#
# Setting up a DigitalOcean Droplet
# with Basic Python Stack
# and Jupyter Notebook
#
# Python for Finance
# (c) Dr Yves J Hilpisch
#
# IP ADDRESS FROM PARAMETER
MASTER_IP=$1
# COPYING THE FILES
scp install.sh root@${MASTER_IP}:
scp cert.* jupyter_notebook_config.py root@${MASTER_IP}:
# EXECUTING THE INSTALLATION SCRIPT
ssh root@${MASTER_IP} bash /root/install.sh
现在一切都准备好尝试设置代码。在 DigitalOcean 上,创建一个新的滴水筹,并选择类似于以下选项:
-
操作系统:Ubuntu 16.04.3 x64(截至 2017 年 11 月 4 日的默认选择)
-
规模:1 核心,512 MB,20GB SSD(最小的滴水筹)
-
数据中心区域:法兰克福(因为作者居住在德国)
-
SSH 密钥:添加(新的)SSH 密钥以实现无密码登录 ^(7)
-
滴水筹名称:您可以选择预先指定的名称,也可以选择像
py4fi这样的名称
最后,点击创建按钮启动滴水筹建过程,通常需要约一分钟。进行设置过程的主要结果是 IP 地址,例如,当您选择法兰克福作为数据中心位置时,可能为 46.101.156.199。现在设置滴水筹非常简单:
(py3) macbookpro:~/cloud$ bash setup.sh 46.101.156.199
结果过程可能需要几分钟。当 Jupyter Notebook 服务器出现类似于以下消息时,过程结束:
The Jupyter Notebook is running at: https://[all ip addresses on your system]:8888/
在任何当前浏览器中,访问以下地址即可访问正在运行的 Jupyter Notebook(注意https协议):
https://46.101.156.199:8888
可能需要添加安全例外,然后 Jupyter Notebook 登录屏幕会提示输入密码(在我们的情况下为jupyter)。现在一切准备就绪,可以通过 Jupyter Notebook 在浏览器中开始 Python 开发,通过终端窗口进行IPython或文本文件编辑器。还提供了其他文件管理功能,如文件上传,删除文件或创建文件夹。
小贴士
像 DigitalOcean 和 Jupyter Notebook 这样的云实例是算法交易员的强大组合,可以利用专业的计算和存储基础设施。专业的云和数据中心提供商确保您的(虚拟)机器在物理上安全且高度可用。使用云实例也可以使探索和开发阶段的成本相对较低,因为通常按小时收费,而无需签订长期协议。
结论
Python 是本书选择的编程语言和技术平台。然而,Python 部署可能最多时甚至令人厌烦和心神不宁。幸运的是,今天有一些技术可用 — 都不到五年的时间 — 有助于解决部署问题。开源软件conda有助于 Python 包和虚拟环境的管理。Docker 容器甚至可以进一步扩展,可以在一个技术上受到保护的"沙箱"中轻松创建完整的文件系统和运行时环境。更进一步,像 DigitalOcean 这样的云提供商在几分钟内提供由专业管理和安全的数据中心提供的计算和存储容量,并按小时计费。这与 Python 3.6 安装和安全的 Jupyter Notebook 服务器安装结合在一起,为算法交易项目的 Python 开发和部署提供了专业的环境。
进一步资源
对于Python 包管理,请参考以下资源:
对于虚拟环境管理,请参阅以下资源:
关于Docker 容器的信息请见此处:
-
Matthias、Karl 和 Sean Kane (2015): Docker: Up and Running. O’Reilly, 北京等。
Robbins (2016)提供了对bash 脚本语言的简明介绍和概述。
- Robbins, Arnold (2016): Bash Pocket Reference. 第 2 版,O’Reilly, 北京等。
如何安全运行公共 Jupyter Notebook 服务器请参阅运行笔记本服务器。
要在 DigitalOcean 上注册一个新帐户并获得 10 美元的起始信用,请访问此注册页面。这可以支付最小水滴两个月的使用费。
¹在撰写本文时,Python 3.7beta 刚刚发布。
²在 Windows 上,激活新环境的命令仅为activate py27 — 省略了source。
³在官方文档中,您会找到以下解释:“Python '虚拟环境'允许将 Python 包安装在特定应用程序的隔离位置,而不是全局安装。"参见创建虚拟环境页面。
⁴ 有关 Docker 技术的全面介绍,请参阅 Matthias 和 Kane (2015) 的书籍。
⁵ 要了解 bash 脚本的简明介绍和快速概述,请参考 Robbins (2016) 的书籍。
⁶ 使用这样一个自行生成的证书时,可能需要在浏览器提示时添加安全异常。
⁷ 如果需要帮助,请访问如何在 DigitalOcean Droplets 上使用 SSH 密钥或如何在 DigitalOcean Droplets 上使用 PuTTY(Windows 用户)。
第二部分:掌握基础知识
本书的这部分内容涉及 Python 编程的基础知识。本部分涵盖的主题对于随后部分中的所有其他章节都是基础的。
这些章节按照特定主题组织,使读者可以作为参考,查找与感兴趣的主题相关的示例和详细信息:
-
第三章 关于
Python的数据类型和数据结构 -
第四章 关于
NumPy及其ndarray类 -
第五章 关于
pandas及其DataFrame类 -
第六章 关于使用 Python 的面向对象编程(OOP)
第三章:数据类型和结构
糟糕的程序员担心代码。优秀的程序员担心数据结构及其关系。
Linus Torvalds
介绍
本章介绍了Python的基本数据类型和数据结构。尽管Python解释器本身已经带来了丰富多样的数据结构,但NumPy和其他库在其中增添了宝贵的内容。
本章组织如下:
“基本数据类型”
第一节介绍了基本数据类型,如int、float和string。
“基本数据结构”
下一节介绍了Python的基本数据结构(例如list对象)并说明了控制结构、函数式编程范式和匿名函数。
本章的精神是在涉及数据类型和结构时提供对Python特定内容的一般介绍。如果您具备来自其他编程语言(例如C或Matlab)的背景,那么您应该能够轻松掌握Python用法可能带来的差异。此处介绍的主题对于接下来的章节都是重要且基础的。
本章涵盖以下数据类型和结构:
| 对象类型 | 含义 | 用法/模型 |
|---|---|---|
int |
整数值 | 自然数 |
float |
浮点数 | 实数 |
bool |
布尔值 | 某种真或假 |
str |
字符串对象 | 字符、单词、文本 |
tuple |
不可变容器 | 固定的对象集合、记录 |
list |
可变容器 | 变化的对象集合 |
dict |
可变容器 | 键-值存储 |
set |
可变容器 | 唯一对象的集合 |
基本数据类型
Python 是一种动态类型语言,这意味着Python解释器在运行时推断对象的类型。相比之下,像C这样的编译语言通常是静态类型的。在这些情况下,对象的类型必须在编译时与对象关联。¹
整数
最基本的数据类型之一是整数,或者int:
In [1]: a = 10
type(a)
Out[1]: int
内置函数type提供了标准和内置类型的所有对象的类型信息,以及新创建的类和对象。在后一种情况下,提供的信息取决于程序员与类存储的描述。有一句话说“Python中的一切都是对象。”这意味着,例如,即使是我们刚刚定义的int对象这样的简单对象也有内置方法。例如,您可以通过调用方法bit_length来获取表示内存中的int对象所需的位数:
In [2]: a.bit_length()
Out[2]: 4
您会发现,所需的位数随我们分配给对象的整数值的增加而增加:
In [3]: a = 100000
a.bit_length()
Out[3]: 17
一般来说,有很多不同的方法,很难记住所有类和对象的所有方法。高级Python环境,如IPython,提供了可以显示附加到对象的所有方法的制表符完成功能。您只需键入对象名称,然后跟一个点(例如,a.),然后按 Tab 键,例如,a.*tab*。然后,这将提供您可以调用的对象的方法集合。或者,Python内置函数dir提供了任何对象的完整属性和方法列表。
Python的一个特殊之处在于整数可以是任意大的。例如,考虑谷歌数 10¹⁰⁰。Python对于这样的大数没有问题,这些大数在技术上是long对象:
In [4]: googol = 10 ** 100
googol
Out[4]: 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
In [5]: googol.bit_length()
Out[5]: 333
大整数
Python 整数可以是任意大的。解释器会根据需要使用尽可能多的位/字节来表示数字。
整数的算术运算易于实现:
In [6]: 1 + 4
Out[6]: 5
In [7]: 1 / 4
Out[7]: 0.25
In [8]: type(1 / 4)
Out[8]: float
浮点数
最后一个表达式返回通常期望的结果 0.25(在基本的 Python 2.7 中不同)。这使我们进入了下一个基本数据类型,即float对象。在整数值后添加一个点,如1.或1.0,会导致Python将对象解释为float。涉及float的表达式通常也返回一个float对象:^(2)
In [9]: 1.6 / 4
Out[9]: 0.4
In [10]: type (1.6 / 4)
Out[10]: float
float稍微复杂一些,因为有理数或实数的计算机化表示通常不是精确的,而是取决于所采取的具体技术方法。为了说明这意味着什么,让我们定义另一个float对象b。像这样的float对象总是内部表示为仅具有一定精度的。当向b添加 0.1 时,这变得明显:
In [11]: b = 0.35
type(b)
Out[11]: float
In [12]: b + 0.1
Out[12]: 0.44999999999999996
这是因为+float+s 在内部以二进制格式表示;也就是说,十进制数通过形式为的系列表示。对于某些浮点数,二进制表示可能涉及大量元素,甚至可能是一个无限级数。然而,给定用于表示此类数字的固定位数-即表示系列中的固定项数-不准确是其结果。其他数字可以完美表示,因此即使只有有限数量的位可用,它们也会被精确地存储。考虑以下示例:
In [13]: c = 0.5
c.as_integer_ratio()
Out[13]: (1, 2)
一半,即 0.5,被准确地存储,因为它有一个精确(有限)的二进制表示: 。然而,对于b = 0.35,我们得到的结果与期望的有理数 不同:
In [14]: b.as_integer_ratio()
Out[14]: (3152519739159347, 9007199254740992)
精度取决于用于表示数字的位数。一般来说,所有Python运行的平台都使用 IEEE 754 双精度标准(即 64 位)来进行内部表示。³ 这意味着相对精度为 15 位数字。
由于这个主题在金融等几个应用领域非常重要,有时需要确保数字的确切或至少是最佳可能的表示。例如,在对一大堆数字求和时,这个问题可能很重要。在这种情况下,某种类型和/或数量级别的表示误差可能导致与基准值的显著偏差。
模块decimal提供了一个用于浮点数的任意精度对象,以及几个选项来解决使用这些数字时的精度问题:
In [15]: import decimal
from decimal import Decimal
In [16]: decimal.getcontext()
Out[16]: Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
In [17]: d = Decimal(1) / Decimal (11)
d
Out[17]: Decimal('0.09090909090909090909090909091')
您可以通过改变Context对象的相应属性值来改变表示的精度:
In [18]: decimal.getcontext().prec = 4 # ①
In [19]: e = Decimal(1) / Decimal (11)
e
Out[19]: Decimal('0.09091')
In [20]: decimal.getcontext().prec = 50 # ②
In [21]: f = Decimal(1) / Decimal (11)
f
Out[21]: Decimal('0.090909090909090909090909090909090909090909090909091')
①
低于默认精度。
②
高于默认精度。
如有需要,可以通过这种方式调整精度以适应手头的确切问题,并且可以处理具有不同精度的浮点对象:
In [22]: g = d + e + f
g
Out[22]: Decimal('0.27272818181818181818181818181909090909090909090909')
任意精度浮点数
模块decimal提供了一个任意精度浮点数对象。在金融中,有时需要确保高精度并超越 64 位双精度标准。
布尔值
在编程中,评估比较或逻辑表达式,例如4 > 3,4.5 <= 3.25或(4 > 3) and (3 > 2),会产生True或False之一作为输出,这是两个重要的 Python 关键字。其他例如def,for或if。Python 关键字的完整列表可在keyword模块中找到。
In [23]: import keyword
In [24]: keyword.kwlist
Out[24]: ['False',
'None',
'True',
'and',
'as',
'assert',
'break',
'class',
'continue',
'def',
'del',
'elif',
'else',
'except',
'finally',
'for',
'from',
'global',
'if',
'import',
'in',
'is',
'lambda',
'nonlocal',
'not',
'or',
'pass',
'raise',
'return',
'try',
'while',
'with',
'yield']
True和False是bool数据类型,代表布尔值。以下代码展示了 Python 对相同操作数应用比较操作符后生成的bool对象。
In [25]: 4 > 3 # ①
Out[25]: True
In [26]: type(4 > 3)
Out[26]: bool
In [27]: type(False)
Out[27]: bool
In [28]: 4 >= 3 # ②
Out[28]: True
In [29]: 4 < 3 # ③
Out[29]: False
In [30]: 4 <= 3 # ④
Out[30]: False
In [31]: 4 == 3 # ⑤
Out[31]: False
In [32]: 4 != 3 # ⑥
Out[32]: True
①
更大。
②
大于或等于。
③
更小。
④
小于或等于。
⑤
相等。
⑥
不相等。
经常,逻辑运算符被应用于bool对象,从而产生另一个bool对象。
In [33]: True and True
Out[33]: True
In [34]: True and False
Out[34]: False
In [35]: False and False
Out[35]: False
In [36]: True or True
Out[36]: True
In [37]: True or False
Out[37]: True
In [38]: False or False
Out[38]: False
In [39]: not True
Out[39]: False
In [40]: not False
Out[40]: True
当然,这两种类型的运算符经常结合使用。
In [41]: (4 > 3) and (2 > 3)
Out[41]: False
In [42]: (4 == 3) or (2 != 3)
Out[42]: True
In [43]: not (4 != 4)
Out[43]: True
In [44]: (not (4 != 4)) and (2 == 3)
Out[44]: False
其一主要应用是通过其他 Python 关键字(例如if或while)来控制代码流程(本章后面将有更多示例)。
In [45]: if 4 > 3: # ①
print('condition true') # ②
condition true
In [46]: i = 0 # ③
while i < 4: # ④
print('condition true, i = ', i) # ⑤
i += 1 # ⑥
condition true, i = 0
condition true, i = 1
condition true, i = 2
condition true, i = 3
①
如果条件成立,则执行要跟随的代码。
②
如果条件成立,则执行要跟随的代码。
③
使用 0 初始化参数i。
④
只要条件成立,就执行并重复执行后续代码。
⑤
打印文本和参数i的值。
⑥
将参数值增加 1;i += 1等同于i = i + 1。
在数值上,Python 将False赋值为 0,将True赋值为 1。通过bool()函数将数字转换为bool对象时,0 给出False,而所有其他数字给出True。
In [47]: int(True)
Out[47]: 1
In [48]: int(False)
Out[48]: 0
In [49]: float(True)
Out[49]: 1.0
In [50]: float(False)
Out[50]: 0.0
In [51]: bool(0)
Out[51]: False
In [52]: bool(0.0)
Out[52]: False
In [53]: bool(1)
Out[53]: True
In [54]: bool(10.5)
Out[54]: True
In [55]: bool(-2)
Out[55]: True
字符串
现在我们可以表示自然数和浮点数,我们转向文本。在Python中表示文本的基本数据类型是string。string对象具有许多非常有用的内置方法。事实上,当涉及到处理任何类型和任何大小的文本文件时,Python通常被认为是一个很好的选择。string对象通常由单引号或双引号定义,或者通过使用str函数转换另一个对象(即使用对象的标准或用户定义的string表示)来定义:
In [56]: t = 'this is a string object'
关于内置方法,例如,您可以将对象中的第一个单词大写:
In [57]: t.capitalize()
Out[57]: 'This is a string object'
或者您可以将其拆分为其单词组件以获得所有单词的list对象(稍后会有关于list对象的更多内容):
In [58]: t.split()
Out[58]: ['this', 'is', 'a', 'string', 'object']
您还可以搜索单词并在成功情况下获得单词的第一个字母的位置(即,索引值):
In [59]: t.find('string')
Out[59]: 10
如果单词不在string对象中,则该方法返回-1:
In [60]: t.find('Python')
Out[60]: -1
用replace方法轻松替换字符串中的字符是一个典型的任务:
In [61]: t.replace(' ', '|')
Out[61]: 'this|is|a|string|object'
字符串的剥离—即删除某些前导/后置字符—也经常是必要的:
In [62]: 'http://www.python.org'.strip('htp:/')
Out[62]: 'www.python.org'
表格 3-1 列出了string对象的许多有用方法。
表格 3-1. 选择的字符串方法
| 方法 | 参数 | 返回/结果 |
|---|---|---|
capitalize |
() |
第一个字母大写的字符串副本 |
count |
(子串[, 开始[, 结束]]) |
子串出现次数的计数 |
decode |
([编码[, 错误]]) |
使用编码(例如,UTF-8)的字符串的解码版本 |
encode |
([编码+[, 错误]]) |
字符串的编码版本 |
find |
(sub[, start[, end]]) |
找到子字符串的(最低)索引 |
join |
(seq) |
将序列seq中的字符串连接起来 |
replace |
(old, new[, count]) |
将old替换为new的第一个count次 |
split |
([sep[, maxsplit]]) |
以sep为分隔符的字符串中的单词列表 |
splitlines |
([keepends]) |
如果keepends为True,则带有行结束符/断行的分隔行 |
strip |
(chars) |
删除chars中的前导/尾随字符的字符串的副本 |
upper |
() |
复制并将所有字母大写 |
注意
从 Python 2.7(本书的第一版)到 Python 3.6(本书的第二版使用的版本)的基本变化是字符串对象的编码和解码以及 Unicode 的引入(参见https://docs.python.org/3/howto/unicode.html)。本章不允许详细讨论此上下文中重要的许多细节。对于本书的目的,主要涉及包含英文单词的数字数据和标准字符串,这种省略似乎是合理的。
附录:打印和字符串替换
打印str对象或其他 Python 对象的字符串表示通常是通过print()函数完成的(在 Python 2.7 中是一个语句)。
In [63]: print('Python for Finance') # ①
Python for Finance
In [64]: print(t) # ②
this is a string object
In [65]: i = 0
while i < 4:
print(i) # ③
i += 1
0
1
2
3
In [66]: i = 0
while i < 4:
print(i, end='|') # ④
i += 1
0|1|2|3|
①
打印一个str对象。
②
打印由变量名引用的str对象。
③
打印int对象的字符串表示。
④
指定打印的最后一个字符(默认为前面看到的换行符\n)。
Python 提供了强大的字符串替换操作。有通过%字符进行的旧方法和通过花括号{}和format()进行的新方法。两者在实践中仍然适用。本节不能提供所有选项的详尽说明,但以下代码片段显示了一些重要的内容。首先,旧的方法。
In [67]: 'this is an integer %d' % 15 # ①
Out[67]: 'this is an integer 15'
In [68]: 'this is an integer %4d' % 15 # ②
Out[68]: 'this is an integer 15'
In [69]: 'this is an integer %04d' % 15 # ③
Out[69]: 'this is an integer 0015'
In [70]: 'this is a float %f' % 15.3456 # ④
Out[70]: 'this is a float 15.345600'
In [71]: 'this is a float %.2f' % 15.3456 # ⑤
Out[71]: 'this is a float 15.35'
In [72]: 'this is a float %8f' % 15.3456 # ⑥
Out[72]: 'this is a float 15.345600'
In [73]: 'this is a float %8.2f' % 15.3456 # ⑦
Out[73]: 'this is a float 15.35'
In [74]: 'this is a float %08.2f' % 15.3456 # ⑧
Out[74]: 'this is a float 00015.35'
In [75]: 'this is a string %s' % 'Python' # ⑨
Out[75]: 'this is a string Python'
In [76]: 'this is a string %10s' % 'Python' # ⑩
Out[76]: 'this is a string Python'
①
int对象替换。
②
带有固定数量的字符。
③
如果必要,带有前导零。
④
float对象替换。
⑤
带有固定数量的小数位数。
⑥
带有固定数量的字符(并填充小数)。
⑦
带有固定数量的字符和小数位数…
⑧
… 以及必要时的前导零。
⑨
str对象替换。
⑩
带有固定数量的字符。
现在以新方式实现相同的示例。请注意,输出在某些地方略有不同。
In [77]: 'this is an integer {:d}'.format(15)
Out[77]: 'this is an integer 15'
In [78]: 'this is an integer {:4d}'.format(15)
Out[78]: 'this is an integer 15'
In [79]: 'this is an integer {:04d}'.format(15)
Out[79]: 'this is an integer 0015'
In [80]: 'this is a float {:f}'.format(15.3456)
Out[80]: 'this is a float 15.345600'
In [81]: 'this is a float {:.2f}'.format(15.3456)
Out[81]: 'this is a float 15.35'
In [82]: 'this is a float {:8f}'.format(15.3456)
Out[82]: 'this is a float 15.345600'
In [83]: 'this is a float {:8.2f}'.format(15.3456)
Out[83]: 'this is a float 15.35'
In [84]: 'this is a float {:08.2f}'.format(15.3456)
Out[84]: 'this is a float 00015.35'
In [85]: 'this is a string {:s}'.format('Python')
Out[85]: 'this is a string Python'
In [86]: 'this is a string {:10s}'.format('Python')
Out[86]: 'this is a string Python '
字符串替换在多次打印操作的上下文中特别有用,其中打印的数据会更新,例如,在while循环期间。
In [87]: i = 0
while i < 4:
print('the number is %d' % i)
i += 1
the number is 0
the number is 1
the number is 2
the number is 3
In [88]: i = 0
while i < 4:
print('the number is {:d}'.format(i))
i += 1
the number is 0
the number is 1
the number is 2
the number is 3
旅行:正则表达式
在处理string对象时,使用正则表达式是一种强大的工具。Python在模块re中提供了这样的功能:
In [89]: import re
假设你面对一个大文本文件,例如一个逗号分隔值(CSV)文件,其中包含某些时间序列和相应的日期时间信息。往往情况下,日期时间信息以Python无法直接解释的格式提供。然而,日期时间信息通常可以通过正则表达式描述。考虑以下string对象,其中包含三个日期时间元素,三个整数和三个字符串。请注意,三重引号允许在多行上定义字符串:
In [90]: series = """
'01/18/2014 13:00:00', 100, '1st';
'01/18/2014 13:30:00', 110, '2nd';
'01/18/2014 14:00:00', 120, '3rd'
"""
以下正则表达式描述了提供在string对象中的日期时间信息的格式:⁴
In [91]: dt = re.compile("'[0-9/:\s]+'") # datetime
有了这个正则表达式,我们可以继续找到所有日期时间元素。通常,将正则表达式应用于string对象还会导致典型解析任务的性能改进:
In [92]: result = dt.findall(series)
result
Out[92]: ["'01/18/2014 13:00:00'", "'01/18/2014 13:30:00'", "'01/18/2014 14:00:00'"]
正则表达式
在解析string对象时,考虑使用正则表达式,这可以为此类操作带来便利性和性能。
然后可以解析生成Python datetime对象的结果string对象(参见[Link to Come],了解如何使用Python处理日期和时间数据的概述)。要解析包含日期时间信息的string对象,我们需要提供如何解析的信息 —— 再次作为string对象:
In [93]: from datetime import datetime
pydt = datetime.strptime(result[0].replace("'", ""),
'%m/%d/%Y %H:%M:%S')
pydt
Out[93]: datetime.datetime(2014, 1, 18, 13, 0)
In [94]: print(pydt)
2014-01-18 13:00:00
In [95]: print(type(pydt))
<class 'datetime.datetime'>
后续章节将提供有关日期时间数据的更多信息,以及处理此类数据和datetime对象及其方法。这只是对金融中这一重要主题的一个引子。
基本数据结构
作为一个通用规则,数据结构是包含可能大量其他对象的对象。在Python提供的内置结构中包括:
tuple
一个不可变的任意对象的集合;只有少量方法可用
list
一个可变的任意对象的集合;有许多方法可用
dict
键-值存储对象
set
用于其他唯一对象的无序集合对象
元组
tuple是一种高级数据结构,但在其应用中仍然相当简单且有限。通过在括号中提供对象来定义它:
In [96]: t = (1, 2.5, 'data')
type(t)
Out[96]: tuple
你甚至可以放弃括号,提供多个对象,只需用逗号分隔:
In [97]: t = 1, 2.5, 'data'
type(t)
Out[97]: tuple
像几乎所有的Python数据结构一样,tuple具有内置索引,借助它可以检索单个或多个tuple元素。重要的是要记住,Python使用零基编号,因此tuple的第三个元素位于索引位置 2:
In [98]: t[2]
Out[98]: 'data'
In [99]: type(t[2])
Out[99]: str
零基编号
与其他一些编程语言(如Matlab)相比,Python使用零基编号方案。例如,tuple对象的第一个元素的索引值为 0。
这种对象类型提供的特殊方法仅有两个:count和index。第一个方法统计某个对象的出现次数,第二个方法给出其第一次出现的索引值:
In [100]: t.count('data')
Out[100]: 1
In [101]: t.index(1)
Out[101]: 0
tuple对象是不可变对象。这意味着一旦定义,它们就不容易更改。
列表
类型为list的对象比tuple对象更加灵活和强大。从财务角度来看,你可以仅使用list对象就能实现很多,比如存储股价报价和添加新数据。list对象通过括号定义,其基本功能和行为与tuple对象相似:
In [102]: l = [1, 2.5, 'data']
l[2]
Out[102]: 'data'
也可以通过使用函数list来定义或转换list对象。以下代码通过转换前面示例中的tuple对象生成一个新的list对象:
In [103]: l = list(t)
l
Out[103]: [1, 2.5, 'data']
In [104]: type(l)
Out[104]: list
除了tuple对象的特性外,list对象还可以通过不同的方法进行扩展和缩减。换句话说,虽然string和tuple对象是不可变序列对象(具有索引),一旦创建就无法更改,但list对象是可变的,并且可以通过不同的操作进行更改。你可以将list对象附加到现有的list对象上,等等:
In [105]: l.append([4, 3]) # ①
l
Out[105]: [1, 2.5, 'data', [4, 3]]
In [106]: l.extend([1.0, 1.5, 2.0]) # ②
l
Out[106]: [1, 2.5, 'data', [4, 3], 1.0, 1.5, 2.0]
In [107]: l.insert(1, 'insert') # ③
l
Out[107]: [1, 'insert', 2.5, 'data', [4, 3], 1.0, 1.5, 2.0]
In [108]: l.remove('data') # ④
l
Out[108]: [1, 'insert', 2.5, [4, 3], 1.0, 1.5, 2.0]
In [109]: p = l.pop(3) # ⑤
print(l, p)
[1, 'insert', 2.5, 1.0, 1.5, 2.0] [4, 3]
①
在末尾附加list对象。
②
添加list对象的元素。
③
在索引位置之前插入对象。
④
删除对象的第一次出现。
⑤
删除并返回索引位置的对象。
切片也很容易实现。在这里,切片指的是将数据集分解为较小部分(感兴趣的部分)的操作:
In [110]: l[2:5] # ①
Out[110]: [2.5, 1.0, 1.5]
①
第三到第五个元素。
Table 3-2 提供了list对象的选定操作和方法的摘要。
表 3-2. list对象的选定操作和方法
| 方法 | 参数 | 返回/结果 |
|---|---|---|
l[i] = x |
[i] |
将第i个元素替换为x |
l[i:j:k] = s |
[i:j:k] |
用s替换从i到j-1的每个第k个元素 |
append |
(x) |
将x附加到对象 |
count |
(x) |
对象x的出现次数 |
del l[i:j:k] |
[i:j:k] |
删除索引值为i到j-1的元素 |
extend |
(s) |
将s的所有元素附加到对象 |
index |
(x[, i[, j]]) |
元素i和j-1之间x的第一个索引 |
insert |
(i, x) |
在索引i之前/后插入x |
remove |
(i) |
删除索引为i的元素 |
pop |
(i) |
删除索引为i的元素并返回它 |
reverse |
() |
将所有项目原地颠倒 |
sort |
([cmp[, key[, reverse]]]) |
原地对所有项目排序 |
专题:控制结构
虽然控制结构本身是一个专题,像for循环这样的控制结构可能最好是基于Python中的list对象介绍的。这是因为一般情况下循环是在list对象上进行的,这与其他语言中通常的标准相当不同。看下面的例子。for循环遍历list对象l的元素,索引值为 2 到 4,并打印出相应元素的平方。注意第二行缩进(空格)的重要性:
In [111]: for element in l[2:5]:
print(element ** 2)
6.25
1.0
2.25
这相比于典型的基于计数器的循环提供了非常高的灵活性。基于(标准的)list对象range也可以使用计数器进行循环:
In [112]: r = range(0, 8, 1) # ①
r
Out[112]: range(0, 8)
In [113]: type(r)
Out[113]: range
①
参数是start、end、step size。
为了比较,相同的循环使用range实现如下:
In [114]: for i in range(2, 5):
print(l[i] ** 2)
6.25
1.0
2.25
遍历列表
在Python中,你可以遍历任意的list对象,不管对象的内容是什么。这通常避免了引入计数器。
Python还提供了典型的(条件)控制元素if、elif和else。它们在其他语言中的使用方式类似:
In [115]: for i in range(1, 10):
if i % 2 == 0: # ①
print("%d is even" % i)
elif i % 3 == 0:
print("%d is multiple of 3" % i)
else:
print("%d is odd" % i)
1 is odd
2 is even
3 is multiple of 3
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is multiple of 3
①
%代表取模。
同样,while提供了另一种控制流的手段:
In [116]: total = 0
while total < 100:
total += 1
print(total)
100
Python的一个特点是所谓的list 推导式。与遍历现有list对象不同,这种方法以一种相当紧凑的方式通过循环生成list对象:
In [117]: m = [i ** 2 for i in range(5)]
m
Out[117]: [0, 1, 4, 9, 16]
从某种意义上说,这已经提供了一种生成“类似”的向量化代码的第一手段,因为循环相对来说更加隐式而不是显式的(代码的向量化将在本章后面更详细地讨论)。
专题:功能编程
Python还提供了一些功能编程支持工具,即将函数应用于整套输入(在我们的情况下是list对象)。其中包括filter、map和reduce。然而,我们首先需要一个函数定义。首先从一个非常简单的函数开始,考虑一个返回输入x的平方的函数f:
In [118]: def f(x):
return x ** 2
f(2)
Out[118]: 4
当然,函数可以是任意复杂的,具有多个输入/参数对象,甚至多个输出(返回对象)。但是,考虑以下函数:
In [119]: def even(x):
return x % 2 == 0
even(3)
Out[119]: False
返回对象是一个布尔值。这样的函数可以通过使用 map 应用于整个 list 对象:
In [120]: list(map(even, range(10)))
Out[120]: [True, False, True, False, True, False, True, False, True, False]
为此,我们还可以直接将函数定义作为 map 的参数提供,通过使用 lambda 或匿名函数:
In [121]: list(map(lambda x: x ** 2, range(10)))
Out[121]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
函数也可以用于过滤 list 对象。在下面的示例中,过滤器返回符合由 even 函数定义的布尔条件的 list 对象元素:
In [122]: list(filter(even, range(15)))
Out[122]: [0, 2, 4, 6, 8, 10, 12, 14]
列表推导、函数式编程、匿名函数
在 Python 级别尽可能避免使用循环被认为是良好的实践。list 推导和函数式编程工具如 map、filter 和 reduce 提供了编写没有(显式)循环的代码的方法,这种代码既紧凑又通常更可读。在这种情况下,lambda 或匿名函数也是强大的工具。
字典
dict 对象是字典,也是可变序列,允许通过可以是 string 对象的键来检索数据。它们被称为键值存储。虽然 list 对象是有序且可排序的,但 dict 对象是无序且不可排序的。通过示例可以更好地说明与 list 对象的进一步差异。花括号是定义 dict 对象的标志:
In [123]: d = {
'Name' : 'Angela Merkel',
'Country' : 'Germany',
'Profession' : 'Chancelor',
'Age' : 63
}
type(d)
Out[123]: dict
In [124]: print(d['Name'], d['Age'])
Angela Merkel 63
同样,这类对象具有许多内置方法:
In [125]: d.keys()
Out[125]: dict_keys(['Name', 'Country', 'Profession', 'Age'])
In [126]: d.values()
Out[126]: dict_values(['Angela Merkel', 'Germany', 'Chancelor', 63])
In [127]: d.items()
Out[127]: dict_items([('Name', 'Angela Merkel'), ('Country', 'Germany'), ('Profession', 'Chancelor'), ('Age', 63)])
In [128]: birthday = True
if birthday is True:
d['Age'] += 1
print(d['Age'])
64
有几种方法可以从 dict 对象获取 iterator 对象。当进行迭代时,这些对象的行为类似于 list 对象:
In [129]: for item in d.items():
print(item)
('Name', 'Angela Merkel')
('Country', 'Germany')
('Profession', 'Chancelor')
('Age', 64)
In [130]: for value in d.values():
print(type(value))
<class 'str'>
<class 'str'>
<class 'str'>
<class 'int'>
表 3-3 提供了 dict 对象的选定操作和方法的摘要。
表 3-3. dict 对象的选定操作和方法
| 方法 | 参数 | 返回/结果 |
|---|---|---|
d[k] |
[k] |
d 中具有键 k 的项目 |
d[k] = x |
[k] |
将项目键 k 设置为 x |
del d[k] |
[k] |
删除具有键 k 的项目 |
clear |
() |
删除所有项目 |
copy |
() |
复制一个副本 |
has_key |
(k) |
如果 k 是键,则为 True |
items |
() |
迭代器遍历所有项目 |
keys |
() |
迭代器遍历所有键 |
values |
() |
迭代器遍历所有值 |
popitem |
(k) |
返回并删除具有键 k 的项目 |
update |
([e]) |
用 e 中的项目更新项目 |
集合
我们将考虑的最后一个数据结构是 set 对象。尽管集合论是数学和金融理论的基石,但对于 set 对象的实际应用并不太多。这些对象是其他对象的无序集合,每个元素只包含一次:
In [131]: s = set(['u', 'd', 'ud', 'du', 'd', 'du'])
s
Out[131]: {'d', 'du', 'u', 'ud'}
In [132]: t = set(['d', 'dd', 'uu', 'u'])
使用 set 对象,您可以像在数学集合论中一样实现操作。例如,您可以生成并集、交集和差异:
In [133]: s.union(t) # ①
Out[133]: {'d', 'dd', 'du', 'u', 'ud', 'uu'}
In [134]: s.intersection(t) # ②
Out[134]: {'d', 'u'}
In [135]: s.difference(t) # ③
Out[135]: {'du', 'ud'}
In [136]: t.difference(s) # ④
Out[136]: {'dd', 'uu'}
In [137]: s.symmetric_difference(t) # ⑤
Out[137]: {'dd', 'du', 'ud', 'uu'}
①
s 和 t 的全部。
②
在 s 和 t 中都有。
③
在 s 中但不在 t 中。
④
在 t 中但不在 s 中。
⑤
在其中一个但不是两者都。
set对象的一个应用是从list对象中消除重复项。例如:
In [138]: from random import randint
l = [randint(0, 10) for i in range(1000)] # ①
len(l) # ②
Out[138]: 1000
In [139]: l[:20]
Out[139]: [10, 9, 2, 4, 5, 1, 7, 4, 6, 10, 9, 5, 4, 6, 10, 3, 4, 7, 0, 5]
In [140]: s = set(l)
s
Out[140]: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
①
1,000 个 0 到 10 之间的随机整数。
②
l 中的元素数量。
结论
基本的Python解释器已经提供了丰富灵活的数据结构。从金融的角度来看,以下可以被认为是最重要的:
基本数据类型
在金融中,int、float和string类提供了原子数据类型。
标准数据结构
tuple、list、dict和set类在金融领域有许多应用领域,其中list通常是最灵活的通用工作马。
进一步资源
本章重点讨论可能对金融算法和应用特别重要的问题。但是,它只能代表探索Python中数据结构和数据建模的起点。有许多宝贵的资源可供进一步深入了解。
书籍形式的良好参考资料包括:
-
Goodrich, Michael 等(2013):Python 数据结构与算法. John Wiley & Sons, Hoboken, NJ.
-
Harrison, Matt (2017): Python 3 图解指南. Treading on Python Series.
-
Ramalho, Luciano (2016): 流畅的 Python. O’Reilly, Beijing et al.
¹ Cython库将静态类型和编译功能引入Python,与C中的相似。实际上,Cython是Python和C的混合语言。
² 在这里和后续讨论中,诸如float、float 对象等术语可互换使用,承认每个float也是一个对象。对于其他对象类型也是如此。
³ 参考http://en.wikipedia.org/wiki/Double-precision_floating-point_format。
⁴ 在这里不可能详细介绍,但互联网上有大量关于正则表达式的信息,特别是针对Python。关于这个主题的介绍,请参阅 Fitzgerald, Michael (2012): 正则表达式入门. O’Reilly, Sebastopol, CA.
第四章:使用 NumPy 进行数值计算
计算机是无用的。它们只能给出答案。
巴勃罗·毕加索
介绍
本章介绍了 Python 的基本数据类型和数据结构。尽管 Python 解释器本身已经带来了丰富的数据结构,但 NumPy 和其他库以有价值的方式添加了这些数据结构。
本章组织如下:
数据数组
本节详细讨论了数组的概念,并说明了在 Python 中处理数据数组的基本选项。
NumPy 数据结构
本节致力于介绍 NumPy ndarray 类的特性和功能,并展示了该类对科学和金融应用的一些好处。
代码向量化
本节说明了,由于 NumPy 的数组类,向量化代码很容易实现,从而导致代码更紧凑,性能更好。
本章涵盖了以下数据结构:
| 对象类型 | 含义 | 用法/模型 |
|---|---|---|
ndarray(常规) |
n 维数组对象 | 大量数值数据的大数组 |
ndarray(记录) |
二维数组对象 | 以列组织的表格数据 |
本章组织如下:
“数据数组”
本节讨论了使用纯 Python 代码处理数据数组的方法。
[待添加链接]
这是关于常规 NumPy ndarray 类的核心部分;它是几乎所有数据密集型 Python 使用案例中的主要工具。
[待添加链接]
这个简短的部分介绍了用于处理带有列的表格数据的结构化(或记录)ndarray 对象。
“代码的向量化”
在本节中,讨论了代码的向量化及其好处;该部分还讨论了在某些情况下内存布局的重要性。
数据数组
前一章表明 Python 提供了一些非常有用和灵活的通用数据结构。特别是,list 对象可以被认为是一个真正的工作马,具有许多方便的特性和应用领域。在一般情况下,使用这样一个灵活的(可变的)数据结构的代价在于相对较高的内存使用量,较慢的性能或两者兼有。然而,科学和金融应用通常需要对特殊数据结构进行高性能操作。在这方面最重要的数据结构之一是数组。数组通常以行和列的形式结构化其他(基本)相同数据类型的对象。
暂时假设我们仅使用数字,尽管这个概念也可以推广到其他类型的数据。在最简单的情况下,一维数组在数学上表示为向量,通常由float对象内部表示为实数的一行或一列元素组成。在更普遍的情况下,数组表示为i × j 矩阵的元素。这个概念在三维中也可以推广为i × j × k 立方体的元素以及形状为i × j × k × l × …的一般n维数组。
线性代数和向量空间理论等数学学科说明了这些数学结构在许多科学学科和领域中的重要性。因此,设计一个专门的数据结构类来方便和高效地处理数组可能是非常有益的。这就是Python库NumPy的作用所在,其ndarray类应运而生。在下一节介绍其强大的ndarray类之前,本节展示了两种处理数组的替代方法。
使用 Python 列表的数组
在转向NumPy之前,让我们首先用上一节介绍的内置数据结构构建数组。list对象特别适用于完成这项任务。一个简单的list已经可以被视为一维数组:
In [1]: v = [0.5, 0.75, 1.0, 1.5, 2.0] # ①
①
list对象与数字。
由于list对象可以包含任意其他对象,它们也可以包含其他list对象。通过嵌套list对象,可以轻松构建二维和更高维的数组:
In [2]: m = [v, v, v] # ①
m # ②
Out[2]: [[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0]]
①
list对象与list对象…
②
… 得到一个数字矩阵。
我们还可以通过简单的索引选择行或通过双重索引选择单个元素(然而,选择整列并不那么容易):
In [3]: m[1]
Out[3]: [0.5, 0.75, 1.0, 1.5, 2.0]
In [4]: m[1][0]
Out[4]: 0.5
嵌套可以进一步推广到更一般的结构:
In [5]: v1 = [0.5, 1.5]
v2 = [1, 2]
m = [v1, v2]
c = [m, m] # ①
c
Out[5]: [[[0.5, 1.5], [1, 2]], [[0.5, 1.5], [1, 2]]]
In [6]: c[1][1][0]
Out[6]: 1
①
立方数。
请注意,刚刚介绍的对象组合方式通常使用对原始对象的引用指针。这在实践中意味着什么?让我们看看以下操作:
In [7]: v = [0.5, 0.75, 1.0, 1.5, 2.0]
m = [v, v, v]
m
Out[7]: [[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0]]
现在修改v对象的第一个元素的值,看看m对象会发生什么变化:
In [8]: v[0] = 'Python'
m
Out[8]: [['Python', 0.75, 1.0, 1.5, 2.0],
['Python', 0.75, 1.0, 1.5, 2.0],
['Python', 0.75, 1.0, 1.5, 2.0]]
通过使用copy模块的deepcopy函数,可以避免这种情况:
In [9]: from copy import deepcopy
v = [0.5, 0.75, 1.0, 1.5, 2.0]
m = 3 * [deepcopy(v), ] # ①
m
Out[9]: [[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0]]
In [10]: v[0] = 'Python' # ②
m # ③
Out[10]: [[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0]]
①
使用物理副本而不是引用指针。
②
因此,对原始对象的更改…
③
… 不再有任何影响。
Python 数组类
Python 中有一个专用的array模块可用。正如您可以在文档页面上阅读到的(参见https://docs.python.org/3/library/array.html):
该模块定义了一种对象类型,可以紧凑地表示基本值的数组:字符、整数、浮点数。数组是序列类型,并且行为非常像列表,只是存储在其中的对象类型受到限制。类型在对象创建时通过使用类型代码(一个单个字符)来指定。
考虑以下代码,将一个list对象实例化为一个array对象。
In [11]: v = [0.5, 0.75, 1.0, 1.5, 2.0]
In [12]: import array
In [13]: a = array.array('f', v) # ①
a
Out[13]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0])
In [14]: a.append(0.5) # ②
a
Out[14]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5])
In [15]: a.extend([5.0, 6.75]) # ②
a
Out[15]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75])
In [16]: 2 * a # ③
Out[16]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75, 0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75])
①
使用float作为类型代码实例化array对象。
②
主要方法的工作方式类似于list对象的方法。
③
虽然“标量乘法”原理上可行,但结果不是数学上预期的;而是元素被重复。
尝试附加与指定数据类型不同的对象会引发TypeError。
In [17]: # a.append('string') # ①
In [18]: a.tolist() # ②
Out[18]: [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75]
①
仅能附加float对象;其他数据类型/类型代码会引发错误。
②
然而,如果需要这样的灵活性,array对象可以轻松转换回list对象。
array类的一个优点是它具有内置的存储和检索功能。
In [19]: f = open('array.apy', 'wb') # ①
a.tofile(f) # ②
f.close() # ③
In [20]: with open('array.apy', 'wb') as f: # ④
a.tofile(f) # ④
In [21]: !ls -n arr* # ⑤
-rw-r--r--@ 1 503 20 32 29 Dez 17:08 array.apy
①
打开一个用于写入二进制数据的磁盘上的文件。
②
将array数据写入文件。
③
关闭文件。
④
或者,可以使用with上下文执行相同的操作。
⑤
这显示了磁盘上写入的文件。
与以前一样,从磁盘读取数据时,array对象的数据类型很重要。
In [22]: b = array.array('f') # ①
In [23]: with open('array.apy', 'rb') as f: # ②
b.fromfile(f, 5) # ③
In [24]: b # ④
Out[24]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0])
In [25]: b = array.array('d') # ⑤
In [26]: with open('array.apy', 'rb') as f:
b.fromfile(f, 2) # ⑥
In [27]: b # ⑦
Out[27]: array('d', [0.0004882813645963324, 0.12500002956949174])
①
使用类型代码float创建一个新的array对象。
②
打开文件以读取二进制数据...
③
...并在b对象中读取五个元素。
④
使用类型代码double创建一个新的array对象。
⑤
从文件中读取两个元素。
⑥
类型代码的差异导致“错误”的数字。
⑦
常规 NumPy 数组
显然,使用list对象构成数组结构有些作用。但这并不是真正方便的方式,而且list类并没有为此特定目标而构建。它的范围更广泛,更一般。array类已经稍微更专业一些,提供了一些有用的特性来处理数据数组。然而,某种“高度”专业化的类因此可能真的对处理数组类型的结构非常有益。
基础知识
这样一个专门的类就是numpy.ndarray类,它的特定目标是方便且高效地处理n维数组,即以高性能的方式。这个类的基本处理最好通过示例来说明:
In [28]: import numpy as np # ①
In [29]: a = np.array([0, 0.5, 1.0, 1.5, 2.0]) # ②
a
Out[29]: array([ 0. , 0.5, 1. , 1.5, 2. ])
In [30]: type(a) # ②
Out[30]: numpy.ndarray
In [31]: a = np.array(['a', 'b', 'c']) # ③
a
Out[31]: array(['a', 'b', 'c'],
dtype='<U1')
In [32]: a = np.arange(2, 20, 2) # ④
a
Out[32]: array([ 2, 4, 6, 8, 10, 12, 14, 16, 18])
In [33]: a = np.arange(8, dtype=np.float) # ⑤
a
Out[33]: array([ 0., 1., 2., 3., 4., 5., 6., 7.])
In [34]: a[5:] # ⑥
Out[34]: array([ 5., 6., 7.])
In [35]: a[:2] # ⑥
Out[35]: array([ 0., 1.])
①
导入numpy包。
②
通过list对象中的浮点数创建一个ndarray对象。
③
通过list对象中的字符串创建一个ndarray对象。
④
np.arange的工作方式类似于range。
⑤
然而,它接受附加输入dtype参数。
⑥
对于一维的ndarray对象,索引的工作方式与平常一样。
ndarray类的一个重要特性是内置方法的多样性。例如:
In [36]: a.sum() # ①
Out[36]: 28.0
In [37]: a.std() # ②
Out[37]: 2.2912878474779199
In [38]: a.cumsum() # ③
Out[38]: array([ 0., 1., 3., 6., 10., 15., 21., 28.])
①
所有元素的总和。
②
元素的标准偏差。
③
所有元素的累积和(从索引位置 0 开始)。
另一个重要特性是对ndarray对象定义的(向量化的)数学运算:
In [39]: l = [0., 0.5, 1.5, 3., 5.]
2 * l # ①
Out[39]: [0.0, 0.5, 1.5, 3.0, 5.0, 0.0, 0.5, 1.5, 3.0, 5.0]
In [40]: a
Out[40]: array([ 0., 1., 2., 3., 4., 5., 6., 7.])
In [41]: 2 * a # ②
Out[41]: array([ 0., 2., 4., 6., 8., 10., 12., 14.])
In [42]: a ** 2 # ③
Out[42]: array([ 0., 1., 4., 9., 16., 25., 36., 49.])
In [43]: 2 ** a # ④
Out[43]: array([ 1., 2., 4., 8., 16., 32., 64., 128.])
In [44]: a ** a # ⑤
Out[44]: array([ 1.00000000e+00, 1.00000000e+00, 4.00000000e+00,
2.70000000e+01, 2.56000000e+02, 3.12500000e+03,
4.66560000e+04, 8.23543000e+05])
①
与list对象的“标量乘法”导致元素的重复。
②
相比之下,使用ndarray对象实现了适当的标量乘法,例如。
③
这个计算每个元素的平方值。
④
这解释了ndarray的元素作为幂。
⑤
这个计算每个元素的自身的幂。
NumPy包的另一个重要功能是通用函数。它们在一般情况下对ndarray对象以及基本 Python 数据类型进行操作。然而,当将通用函数应用于 Python float对象时,需要注意与math模块中相同功能的性能降低。
In [45]: np.exp(a) # ①
Out[45]: array([ 1.00000000e+00, 2.71828183e+00, 7.38905610e+00,
2.00855369e+01, 5.45981500e+01, 1.48413159e+02,
4.03428793e+02, 1.09663316e+03])
In [46]: np.sqrt(a) # ②
Out[46]: array([ 0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131])
In [47]: np.sqrt(2.5) # ③
Out[47]: 1.5811388300841898
In [48]: import math # ④
In [49]: math.sqrt(2.5) # ④
Out[49]: 1.5811388300841898
In [50]: # math.sqrt(a) # ⑤
In [51]: %timeit np.sqrt(2.5) # ⑥
703 ns ± 17.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [52]: %timeit math.sqrt(2.5) # ⑦
107 ns ± 1.48 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
①
逐个元素计算指数值。
②
计算每个元素的平方根。
③
计算 Python float对象的平方根。
④
相同的计算,这次使用math模块。
⑤
math.sqrt不能直接应用于ndarray对象。
⑥
将通用函数np.sqrt应用于 Python float对象……
⑦
……比使用math.sqrt函数的相同操作慢得多。
多维度
切换到多维度是无缝的,并且到目前为止呈现的所有特征都适用于更一般的情况。特别是,索引系统在所有维度上保持一致:
In [53]: b = np.array([a, a * 2]) # ①
b
Out[53]: array([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 0., 2., 4., 6., 8., 10., 12., 14.]])
In [54]: b[0] # ②
Out[54]: array([ 0., 1., 2., 3., 4., 5., 6., 7.])
In [55]: b[0, 2] # ③
Out[55]: 2.0
In [56]: b[:, 1] # ④
Out[56]: array([ 1., 2.])
In [57]: b.sum() # ⑤
Out[57]: 84.0
In [58]: b.sum(axis=0) # ⑥
Out[58]: array([ 0., 3., 6., 9., 12., 15., 18., 21.])
In [59]: b.sum(axis=1) # ⑦
Out[59]: array([ 28., 56.])
①
用一维数组构造二维ndarray对象。
②
选择第一行。
③
选择第一行的第三个元素;在括号内,索引由逗号分隔。
④
选择第二列。
⑤
计算所有值的总和。
⑥
沿第一个轴计算总和,即按列计算。
⑦
沿第二轴计算总和,即按行计算。
有多种方法可以初始化(实例化)ndarray对象。一种方法如前所述,通过np.array。然而,这假定数组的所有元素已经可用。相比之下,也许我们希望首先实例化ndarray对象,以便在执行代码期间生成的结果后来填充它们。为此,我们可以使用以下函数:
In [60]: c = np.zeros((2, 3), dtype='i', order='C') # ①
c
Out[60]: array([[0, 0, 0],
[0, 0, 0]], dtype=int32)
In [61]: c = np.ones((2, 3, 4), dtype='i', order='C') # ②
c
Out[61]: array([[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]],
[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]]], dtype=int32)
In [62]: d = np.zeros_like(c, dtype='f16', order='C') # ③
d
Out[62]: array([[[ 0.0, 0.0, 0.0, 0.0],
[ 0.0, 0.0, 0.0, 0.0],
[ 0.0, 0.0, 0.0, 0.0]],
[[ 0.0, 0.0, 0.0, 0.0],
[ 0.0, 0.0, 0.0, 0.0],
[ 0.0, 0.0, 0.0, 0.0]]], dtype=float128)
In [63]: d = np.ones_like(c, dtype='f16', order='C') # ③
d
Out[63]: array([[[ 1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0]],
[[ 1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0]]], dtype=float128)
In [64]: e = np.empty((2, 3, 2)) # ④
e
Out[64]: array([[[ 0.00000000e+000, -4.34540174e-311],
[ 2.96439388e-323, 0.00000000e+000],
[ 0.00000000e+000, 1.16095484e-028]],
[[ 2.03147708e-110, 9.67661175e-144],
[ 9.80058441e+252, 1.23971686e+224],
[ 4.00695466e+252, 8.34404939e-309]]])
In [65]: f = np.empty_like(c) # ④
f
Out[65]: array([[[0, 0, 0, 0],
[9, 0, 0, 0],
[0, 0, 0, 0]],
[[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]]], dtype=int32)
In [66]: np.eye(5) # ⑤
Out[66]: array([[ 1., 0., 0., 0., 0.],
[ 0., 1., 0., 0., 0.],
[ 0., 0., 1., 0., 0.],
[ 0., 0., 0., 1., 0.],
[ 0., 0., 0., 0., 1.]])
In [67]: g = np.linspace(5, 15, 15) # ⑥
g
Out[67]: array([ 5. , 5.71428571, 6.42857143, 7.14285714,
7.85714286, 8.57142857, 9.28571429, 10. ,
10.71428571, 11.42857143, 12.14285714, 12.85714286,
13.57142857, 14.28571429, 15. ])
①
用零预先填充的ndarray对象。
②
用 1 预先填充的ndarray对象。
③
相同,但采用另一个ndarray对象来推断形状。
④
ndarray对象不预先填充任何内容(数字取决于内存中存在的位)。
⑤
创建一个由 1 填充对角线的方阵作为ndarray对象。
⑥
创建一个一维ndarray对象,其中数字之间的间隔均匀分布;所使用的参数是start、end、num(元素数量)。
使用所有这些函数,我们可以提供以下参数:
shape
要么是一个int,一个``int+s序列,或者是对另一个+numpy.ndarray的引用
dtype(可选)
一个dtype——这些是NumPy特定的numpy.ndarray对象的数据类型
order(可选)
存储元素在内存中的顺序:C表示C风格(即,逐行),或F表示Fortran风格(即,逐列)
在这里,NumPy如何通过ndarray类专门构建数组的方式,与基于list的方法进行比较变得明显:
-
ndarray对象具有内置的维度(轴)。 -
ndarray对象是不可变的,其形状是固定的。 -
它仅允许单一数据类型(
numpy.dtype)用于整个数组。
相反,array类只共享允许唯一数据类型(类型代码,dtype)的特性。
order参数的作用在本章稍后讨论。表 4-1 提供了numpy.dtype对象的概述(即,NumPy允许的基本数据类型)。
表 4-1。NumPy dtype 对象
| dtype | 描述 | 示例 |
|---|---|---|
t |
位域 | t4 (4 位) |
b |
布尔 | b(true 或 false) |
i |
整数 | i8 (64 位) |
u |
无符号整数 | u8 (64 位) |
f |
浮点数 | f8 (64 位) |
c |
复数浮点数 | c16 (128 位) |
O |
对象 | 0 (对象指针) |
S, a |
字符串 | S24 (24 个字符) |
U |
Unicode | U24 (24 个 Unicode 字符) |
V |
其他 | V12 (12 字节数据块) |
元信息
每个ndarray对象都提供访问一些有用属性的功能。
In [68]: g.size # ①
Out[68]: 15
In [69]: g.itemsize # ②
Out[69]: 8
In [70]: g.ndim # ③
Out[70]: 1
In [71]: g.shape # ④
Out[71]: (15,)
In [72]: g.dtype # ⑤
Out[72]: dtype('float64')
In [73]: g.nbytes # ⑥
Out[73]: 120
①
元素的数量。
②
用于表示一个元素所使用的字节数。
③
维度的数量。
④
ndarray对象的形状。
⑤
元素的dtype。
⑥
内存中使用的总字节数。
重塑和调整大小
虽然ndarray对象默认是不可变的,但有多种选项可以重塑和调整此类对象。一般情况下,第一个操作只是提供相同数据的另一个视图,而第二个操作一般会创建一个新的(临时)对象。
In [74]: g = np.arange(15)
In [75]: g
Out[75]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [76]: g.shape # ①
Out[76]: (15,)
In [77]: np.shape(g) # ①
Out[77]: (15,)
In [78]: g.reshape((3, 5)) # ②
Out[78]: array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [79]: h = g.reshape((5, 3)) # ③
h
Out[79]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [80]: h.T # ④
Out[80]: array([[ 0, 3, 6, 9, 12],
[ 1, 4, 7, 10, 13],
[ 2, 5, 8, 11, 14]])
In [81]: h.transpose() # ④
Out[81]: array([[ 0, 3, 6, 9, 12],
[ 1, 4, 7, 10, 13],
[ 2, 5, 8, 11, 14]])
①
原始ndarray对象的形状。
②
重塑为两个维度(内存视图)。
③
创建新对象。
④
新ndarray对象的转置。
在重塑操作期间,ndarray对象中的元素总数保持不变。在调整大小操作期间,此数字会更改,即它要么减少(“向下调整”),要么增加(“向上调整”)。
In [82]: g
Out[82]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [83]: np.resize(g, (3, 1)) # ①
Out[83]: array([[0],
[1],
[2]])
In [84]: np.resize(g, (1, 5)) # ①
Out[84]: array([[0, 1, 2, 3, 4]])
In [85]: np.resize(g, (2, 5)) # ①
Out[85]: array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
In [86]: n = np.resize(g, (5, 4)) # ②
n
Out[86]: array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 0],
[ 1, 2, 3, 4]])
①
两个维度,向下调整。
②
两个维度,向上调整。
堆叠是一种特殊操作,允许水平或垂直组合两个ndarray对象。但是,“连接”维度的大小必须相同。
In [87]: h
Out[87]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [88]: np.hstack((h, 2 * h)) # ①
Out[88]: array([[ 0, 1, 2, 0, 2, 4],
[ 3, 4, 5, 6, 8, 10],
[ 6, 7, 8, 12, 14, 16],
[ 9, 10, 11, 18, 20, 22],
[12, 13, 14, 24, 26, 28]])
In [89]: np.vstack((h, 0.5 * h)) # ②
Out[89]: array([[ 0. , 1. , 2. ],
[ 3. , 4. , 5. ],
[ 6. , 7. , 8. ],
[ 9. , 10. , 11. ],
[ 12. , 13. , 14. ],
[ 0. , 0.5, 1. ],
[ 1.5, 2. , 2.5],
[ 3. , 3.5, 4. ],
[ 4.5, 5. , 5.5],
[ 6. , 6.5, 7. ]])
①
水平堆叠两个ndarray对象。
②
垂直堆叠两个ndarray对象。
另一个特殊操作是将多维ndarray对象展平为一维对象。可以选择是按行(C顺序)还是按列(F顺序)进行展平。
In [90]: h
Out[90]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [91]: h.flatten() # ①
Out[91]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [92]: h.flatten(order='C') # ①
Out[92]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [93]: h.flatten(order='F') # ②
Out[93]: array([ 0, 3, 6, 9, 12, 1, 4, 7, 10, 13, 2, 5, 8, 11, 14])
In [94]: for i in h.flat: # ③
print(i, end=',')
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
In [95]: for i in h.ravel(order='C'): # ④
print(i, end=',')
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
In [96]: for i in h.ravel(order='F'): # ④
print(i, end=',')
0,3,6,9,12,1,4,7,10,13,2,5,8,11,14,
①
平铺的默认顺序是C。
②
用F顺序展平。
③
flat属性提供了一个平坦的迭代器(C顺序)。
④
ravel()方法是flatten()的另一种选择。
布尔数组
比较和逻辑操作通常在ndarray对象上像在标准 Python 数据类型上一样逐元素地进行。默认情况下,评估条件会产生一个布尔ndarray对象(dtype为bool)。
In [164]: h
Out[164]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [150]: h > 8 # ①
Out[150]: array([[False, False, False],
[False, False, False],
[False, False, False],
[ True, True, True],
[ True, True, True]], dtype=bool)
In [151]: h <= 7 # ②
Out[151]: array([[ True, True, True],
[ True, True, True],
[ True, True, False],
[False, False, False],
[False, False, False]], dtype=bool)
In [152]: h == 5 # ③
Out[152]: array([[False, False, False],
[False, False, True],
[False, False, False],
[False, False, False],
[False, False, False]], dtype=bool)
In [158]: (h == 5).astype(int) # ④
Out[158]: array([[0, 0, 0],
[0, 0, 1],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
In [165]: (h > 4) & (h <= 12) # ⑤
Out[165]: array([[False, False, False],
[False, False, True],
[ True, True, True],
[ True, True, True],
[ True, False, False]], dtype=bool)
①
值是否大于...?
②
值是否小于或等于...?
③
值是否等于...?
④
以整数值 0 和 1 表示True和False。
⑤
值是否大于...且小于或等于...?
此类布尔数组可用于索引和数据选择。注意以下操作会展平数据。
In [153]: h[h > 8] # ①
Out[153]: array([ 9, 10, 11, 12, 13, 14])
In [155]: h[(h > 4) & (h <= 12)] # ②
Out[155]: array([ 5, 6, 7, 8, 9, 10, 11, 12])
In [157]: h[(h < 4) | (h >= 12)] # ③
Out[157]: array([ 0, 1, 2, 3, 12, 13, 14])
①
给我所有大于...的值。
②
给我所有大于... 且小于或等于...的值。
③
给我所有大于... 或小于或等于...的值。
在这方面的一个强大工具是np.where()函数,它允许根据条件是True还是False来定义操作/操作。应用np.where()的结果是一个与原始对象相同形状的新ndarray对象。
In [159]: np.where(h > 7, 1, 0) # ①
Out[159]: array([[0, 0, 0],
[0, 0, 0],
[0, 0, 1],
[1, 1, 1],
[1, 1, 1]])
In [160]: np.where(h % 2 == 0, 'even', 'odd') # ②
Out[160]: array([['even', 'odd', 'even'],
['odd', 'even', 'odd'],
['even', 'odd', 'even'],
['odd', 'even', 'odd'],
['even', 'odd', 'even']],
dtype='<U4')
In [163]: np.where(h <= 7, h * 2, h / 2) # ③
Out[163]: array([[ 0. , 2. , 4. ],
[ 6. , 8. , 10. ],
[ 12. , 14. , 4. ],
[ 4.5, 5. , 5.5],
[ 6. , 6.5, 7. ]])
①
在新对象中,如果为True,则设置为1,否则设置为0。
②
在新对象中,如果为True,则设置为even,否则设置为odd。
③
在新对象中,如果为True,则将h元素设置为两倍,否则将h元素设置为一半。
后续章节提供了关于ndarray对象上这些重要操作的更多示例。
速度比较
在转向具有NumPy的结构化数组之前,让我们暂时保持常规数组,并看看专业化在性能方面带来了什么。
以一个简单的例子为例,假设我们想要生成一个形状为 5,000 × 5,000 元素的矩阵/数组,填充了(伪)随机的标准正态分布的数字。然后我们想要计算所有元素的总和。首先,纯Python方法,我们使用list推导来实现:
In [97]: import random
I = 5000
In [98]: %time mat = [[random.gauss(0, 1) for j in range(I)] \
for i in range(I)] # ①
CPU times: user 20.9 s, sys: 372 ms, total: 21.3 s
Wall time: 21.3 s
In [99]: mat[0][:5] # ②
Out[99]: [0.02023704728430644,
-0.5773300286314157,
-0.5034574089604074,
-0.07769332062744054,
-0.4264012594572326]
In [100]: %time sum([sum(l) for l in mat]) # ③
CPU times: user 156 ms, sys: 1.93 ms, total: 158 ms
Wall time: 158 ms
Out[100]: 681.9120404070142
In [101]: import sys
sum([sys.getsizeof(l) for l in mat]) # ④
Out[101]: 215200000
①
通过嵌套的列表推导来创建矩阵。
②
从所绘制的数字中选择一些随机数。
③
首先在列表推导中计算单个list对象的总和;然后计算总和的总和。
④
添加所有list对象的内存使用量。
现在让我们转向NumPy,看看同样的问题是如何在那里解决的。为了方便,NumPy子库random提供了许多函数来实例化一个ndarray对象,并同时填充它(伪)随机数:
In [102]: %time mat = np.random.standard_normal((I, I)) # ①
CPU times: user 1.14 s, sys: 170 ms, total: 1.31 s
Wall time: 1.32 s
In [103]: %time mat.sum() # ②
CPU times: user 29.5 ms, sys: 1.32 ms, total: 30.8 ms
Wall time: 29.7 ms
Out[103]: 2643.0006104377485
In [104]: mat.nbytes # ③
Out[104]: 200000000
In [105]: sys.getsizeof(mat) # ③
Out[105]: 200000112
①
使用标准正态分布的随机数字创建ndarray对象;速度约快 20 倍。
②
计算ndarray对象中所有值的总和;速度约快 6 倍。
③
NumPy方法也节省了一些内存,因为ndarray对象的内存开销与数据本身的大小相比微不足道。
我们观察到以下情况:
语法
尽管我们使用了几种方法来压缩纯Python代码,但NumPy版本更加紧凑和易读。
性能
生成ndarray对象的速度大约快了 20 倍,求和的计算速度大约快了 6 倍,比纯Python中的相应操作更快。
使用 NumPy 数组
使用NumPy进行基于数组的操作和算法通常会导致代码紧凑、易读,并且与纯Python代码相比具有显著的性能改进。
结构化 NumPy 数组
ndarray类的专业化显然带来了许多有价值的好处。然而,太窄的专业化可能对大多数基于数组的算法和应用程序来说是一个太大的负担。因此,NumPy提供了允许每列具有不同dtype的结构化或记录ndarray对象。什么是“每列”?考虑以下结构化数组对象的初始化:
In [106]: dt = np.dtype([('Name', 'S10'), ('Age', 'i4'),
('Height', 'f'), ('Children/Pets', 'i4', 2)]) # ①
In [107]: dt # ①
Out[107]: dtype([('Name', 'S10'), ('Age', '<i4'), ('Height', '<f4'), ('Children/Pets', '<i4', (2,))])
In [108]: dt = np.dtype({'names': ['Name', 'Age', 'Height', 'Children/Pets'],
'formats':'O int float int,int'.split()}) # ②
In [109]: dt # ②
Out[109]: dtype([('Name', 'O'), ('Age', '<i8'), ('Height', '<f8'), ('Children/Pets', [('f0', '<i8'), ('f1', '<i8')])])
In [110]: s = np.array([('Smith', 45, 1.83, (0, 1)),
('Jones', 53, 1.72, (2, 2))], dtype=dt) # ③
In [111]: s # ③
Out[111]: array([('Smith', 45, 1.83, (0, 1)), ('Jones', 53, 1.72, (2, 2))],
dtype=[('Name', 'O'), ('Age', '<i8'), ('Height', '<f8'), ('Children/Pets', [('f0', '<i8'), ('f1', '<i8')])])
In [112]: type(s) # ④
Out[112]: numpy.ndarray
①
复杂的dtype是由几部分组成的。
②
实现相同结果的替代语法。
③
结构化ndarray以两条记录实例化。
④
对象类型仍然是numpy.ndarray。
从某种意义上说,这个构造与初始化SQL数据库中的表格的操作非常接近。我们有列名和列数据类型,可能还有一些附加信息(例如,每个string对象的最大字符数)。现在可以通过它们的名称轻松访问单个列,并通过它们的索引值访问行:
In [113]: s['Name'] # ①
Out[113]: array(['Smith', 'Jones'], dtype=object)
In [114]: s['Height'].mean() # ②
Out[114]: 1.7749999999999999
In [115]: s[0] # ③
Out[115]: ('Smith', 45, 1.83, (0, 1))
In [116]: s[1]['Age'] # ④
Out[116]: 53
①
通过名称选择一列。
②
在选定的列上调用方法。
③
选择一条记录。
④
选择记录中的一个字段。
总之,结构化数组是常规numpy.ndarray对象类型的泛化,因为数据类型只需在每列上保持相同,就像在SQL数据库表格上的上下文中一样。结构化数组的一个优点是,列的单个元素可以是另一个多维对象,不必符合基本的NumPy数据类型。
结构化数组
NumPy提供了除了常规数组之外,还提供了结构化(记录)数组,允许描述和处理类似表格的数据结构,每个(命名的)列具有各种不同的数据类型。它们将SQL表格类似的数据结构带到了Python中,大部分具备常规ndarray对象的优点(语法、方法、性能)。
代码的向量化
代码的矢量化是一种获得更紧凑代码并可能更快执行的策略。其基本思想是对复杂对象进行“一次性”操作或应用函数,而不是通过循环遍历对象的单个元素。在Python中,函数式编程工具,如map和filter,提供了一些基本的矢量化手段。然而,NumPy在其核心深处内置了矢量化。
基本矢量化
正如我们在上一节中学到的,简单的数学运算,如计算所有元素的总和,可以直接在ndarray对象上实现(通过方法或通用函数)。还可以进行更一般的矢量化操作。例如,我们可以按元素将两个NumPy数组相加如下:
In [117]: np.random.seed(100)
r = np.arange(12).reshape((4, 3)) # ①
s = np.arange(12).reshape((4, 3)) * 0.5 # ②
In [118]: r # ①
Out[118]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
In [119]: s # ②
Out[119]: array([[ 0. , 0.5, 1. ],
[ 1.5, 2. , 2.5],
[ 3. , 3.5, 4. ],
[ 4.5, 5. , 5.5]])
In [120]: r + s # ③
Out[120]: array([[ 0. , 1.5, 3. ],
[ 4.5, 6. , 7.5],
[ 9. , 10.5, 12. ],
[ 13.5, 15. , 16.5]])
①
具有随机数的第一个ndarray对象。
②
具有随机数的第二个ndarray对象。
③
逐元素加法作为矢量化操作(无循环)。
NumPy还支持所谓的广播。这允许在单个操作中组合不同形状的对象。我们之前已经使用过这个功能。考虑以下示例:
In [121]: r + 3 # ①
Out[121]: array([[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [122]: 2 * r # ②
Out[122]: array([[ 0, 2, 4],
[ 6, 8, 10],
[12, 14, 16],
[18, 20, 22]])
In [123]: 2 * r + 3 # ③
Out[123]: array([[ 3, 5, 7],
[ 9, 11, 13],
[15, 17, 19],
[21, 23, 25]])
①
在标量加法期间,标量被广播并添加到每个元素。
②
在标量乘法期间,标量也广播并与每个元素相乘。
③
此线性变换结合了两个操作。
这些操作也适用于不同形状的ndarray对象,直到某个特定点为止:
In [124]: r
Out[124]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
In [125]: r.shape
Out[125]: (4, 3)
In [126]: s = np.arange(0, 12, 4) # ①
s # ①
Out[126]: array([0, 4, 8])
In [127]: r + s # ②
Out[127]: array([[ 0, 5, 10],
[ 3, 8, 13],
[ 6, 11, 16],
[ 9, 14, 19]])
In [128]: s = np.arange(0, 12, 3) # ③
s # ③
Out[128]: array([0, 3, 6, 9])
In [129]: # r + s # ④
In [130]: r.transpose() + s # ⑤
Out[130]: array([[ 0, 6, 12, 18],
[ 1, 7, 13, 19],
[ 2, 8, 14, 20]])
In [131]: sr = s.reshape(-1, 1) # ⑥
sr
Out[131]: array([[0],
[3],
[6],
[9]])
In [132]: sr.shape # ⑥
Out[132]: (4, 1)
In [133]: r + s.reshape(-1, 1) # ⑥
Out[133]: array([[ 0, 1, 2],
[ 6, 7, 8],
[12, 13, 14],
[18, 19, 20]])
①
长度为 3 的新一维ndarray对象。
②
r(矩阵)和s(向量)对象可以直接相加。
③
另一个长度为 4 的一维ndarray对象。
④
新s(向量)对象的长度现在与r对象的第二维长度不同。
⑤
再次转置r对象允许进行矢量化加法。
⑥
或者,s的形状可以更改为(4, 1)以使加法起作用(但结果不同)。
通常情况下,自定义的Python函数也适用于numpy.ndarray。如果实现允许,数组可以像int或float对象一样与函数一起使用。考虑以下函数:
In [134]: def f(x):
return 3 * x + 5 # ①
In [135]: f(0.5) # ②
Out[135]: 6.5
In [136]: f(r) # ③
Out[136]: array([[ 5, 8, 11],
[14, 17, 20],
[23, 26, 29],
[32, 35, 38]])
①
实现对参数x进行线性变换的简单 Python 函数。
②
函数f应用于 Python 的float对象。
③
同一函数应用于ndarray对象,导致函数的向量化和逐个元素的评估。
NumPy所做的是简单地将函数f逐个元素地应用于对象。在这种意义上,通过使用这种操作,我们并不避免循环;我们只是在Python级别上避免了它们,并将循环委托给了NumPy。在NumPy级别上,对ndarray对象进行循环处理是由高度优化的代码来完成的,其中大部分代码都是用C编写的,因此通常比纯Python快得多。这解释了在基于数组的用例中使用NumPy带来性能优势的“秘密”。
内存布局
当我们首次使用np.zero初始化numpy.ndarray对象时,我们提供了一个可选参数用于内存布局。这个参数大致指定了数组的哪些元素会被连续地存储在内存中。当处理小数组时,这几乎不会对数组操作的性能产生任何可测量的影响。然而,当数组变大并且取决于要在其上实现的(财务)算法时,情况可能会有所不同。这就是内存布局发挥作用的时候(参见,例如多维数组的内存布局)。
要说明数组的内存布局在科学和金融中的潜在重要性,考虑以下构建多维ndarray对象的情况:
In [137]: x = np.random.standard_normal((1000000, 5)) # ①
In [138]: y = 2 * x + 3 # ②
In [139]: C = np.array((x, y), order='C') # ③
In [140]: F = np.array((x, y), order='F') # ④
In [141]: x = 0.0; y = 0.0 # ⑤
In [142]: C[:2].round(2) # ⑥
Out[142]: array([[[-1.75, 0.34, 1.15, -0.25, 0.98],
[ 0.51, 0.22, -1.07, -0.19, 0.26],
[-0.46, 0.44, -0.58, 0.82, 0.67],
...,
[-0.05, 0.14, 0.17, 0.33, 1.39],
[ 1.02, 0.3 , -1.23, -0.68, -0.87],
[ 0.83, -0.73, 1.03, 0.34, -0.46]],
[[-0.5 , 3.69, 5.31, 2.5 , 4.96],
[ 4.03, 3.44, 0.86, 2.62, 3.51],
[ 2.08, 3.87, 1.83, 4.63, 4.35],
...,
[ 2.9 , 3.28, 3.33, 3.67, 5.78],
[ 5.04, 3.6 , 0.54, 1.65, 1.26],
[ 4.67, 1.54, 5.06, 3.69, 2.07]]])
①
一个在两个维度上具有较大不对称性的ndarray对象。
②
对原始对象数据进行线性变换。
③
这将创建一个二维ndarray对象,其顺序为C(行优先)。
④
这将创建一个二维ndarray对象,其顺序为F(列优先)。
⑤
内存被释放(取决于垃圾收集)。
⑥
从C对象中获取一些数字。
让我们看一些关于两种类型的ndarray对象的基本示例和用例,并考虑它们在不同内存布局下执行的速度:
In [143]: %timeit C.sum() # ①
4.65 ms ± 73.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [144]: %timeit F.sum() # ①
4.56 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [145]: %timeit C.sum(axis=0) # ②
20.9 ms ± 358 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [146]: %timeit C.sum(axis=1) # ③
38.5 ms ± 1.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [147]: %timeit F.sum(axis=0) # ②
87.5 ms ± 1.37 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [148]: %timeit F.sum(axis=1) # ③
81.6 ms ± 1.66 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [149]: F = 0.0; C = 0.0
①
计算所有元素的总和。
②
每行计算和(“许多”)。
③
计算每列的总和(“少”)。
我们可以总结性能结果如下:
-
当计算所有元素的总和时,内存布局实际上并不重要。
-
对
C-orderedndarray对象的求和在行和列上都更快(绝对速度优势)。 -
使用
C-ordered(行优先)ndarray对象,对行求和相对比对列求和更快。 -
使用
F-ordered(列优先)ndarray对象,对列求和相对比对行求和更快。
结论
NumPy 是 Python 中数值计算的首选包。ndarray 类是专门设计用于处理(大)数值数据的高效方便的类。强大的方法和 NumPy 的通用函数允许进行向量化的代码,大部分避免了在 Python 层上的慢循环。本章介绍的许多方法也适用于 pandas 及其 DataFrame 类(见 第五章)
更多资源
有用的资源提供在:
优秀的 NumPy 介绍书籍包括:
-
McKinney, Wes(2017):Python 数据分析。第 2 版,O’Reilly,北京等。
-
VanderPlas, Jake(2016):Python 数据科学手册。O’Reilly,北京等。
第五章:数据分析与 pandas
数据!数据!数据!没有数据,我无法制造砖头!
夏洛克·福尔摩斯
简介
本章讨论的是pandas,这是一个专注于表格数据的数据分析库。pandas在最近几年已经成为一个强大的工具,不仅提供了强大的类和功能,还很好地封装了来自其他软件包的现有功能。结果是一个用户界面,使得数据分析,特别是金融分析,成为一项便捷和高效的任务。
在pandas的核心和本章中的是DataFrame,一个有效处理表格形式数据的类,即以列为组织的数据。为此,DataFrame类提供了列标签以及对数据集的行(记录)进行灵活索引的能力,类似于关系数据库中的表或 Excel 电子表格。
本章涵盖了以下基本数据结构:
| 对象类型 | 意义 | 用途/模型为 |
|---|---|---|
DataFrame |
带有索引的二维数据对象 | 表格数据以列组织 |
Series |
带有索引的一维数据对象 | 单一(时间)数据系列 |
本章组织如下:
“DataFrame 类”
本章从使用简单且小的数据集探索pandas的DataFrame类的基本特征和能力开始;然后通过使用NumPy的ndarray对象并将其转换为DataFrame对象来进行处理。
“基本分析” 和 “基本可视化”
本章还展示了基本的分析和可视化能力,尽管后面的章节在这方面更深入。
“Series 类”
本节简要介绍了pandas的Series类,它在某种程度上代表了DataFrame类的一个特殊情况,只包含单列数据。
“GroupBy 操作”
DataFrame类的一大优势在于根据单个或多个列对数据进行分组。
“复杂选择”
使用(复杂)条件允许从DataFrame对象中轻松选择数据。
“串联、连接和合并”
将不同数据集合并为一个是数据分析中的重要操作。pandas提供了多种选项来完成这样的任务。
“性能方面”
与 Python 一般一样,pandas在一般情况下提供了多种选项来完成相同的目标。本节简要讨论潜在的性能差异。
DataFrame 类
本节涵盖了DataFrame类的一些基本方面。这个类非常复杂和强大,这里只能展示其中一小部分功能。后续章节提供更多例子并揭示不同的方面。
使用 DataFrame 类的第一步
从相当基本的角度来看,DataFrame类被设计用来管理带索引和标签的数据,与SQL数据库表或电子表格应用程序中的工作表并没有太大的不同。考虑以下创建DataFrame对象的示例:
In [1]: import pandas as pd # ①
In [2]: df = pd.DataFrame([10, 20, 30, 40], # ②
columns=['numbers'], # ③
index=['a', 'b', 'c', 'd']) # ④
In [3]: df # ⑤
Out[3]: numbers
a 10
b 20
c 30
d 40
①
导入pandas。
②
将数据定义为list对象。
③
指定列标签。
④
指定索引值/标签。
⑤
显示DataFrame对象的数据以及列和索引标签。
这个简单的例子已经展示了当涉及到存储数据时DataFrame类的一些主要特性:
数据
数据本身可以以不同的形状和类型提供(list、tuple、ndarray和dict对象都是候选对象)。
标签
数据以列的形式组织,可以具有自定义名称。
索引
存在可以采用不同格式(例如,数字、字符串、时间信息)的索引。
与此类DataFrame对象一起工作通常非常方便和高效,例如,与常规的ndarray对象相比,当您想要像扩大现有对象一样时,后者更为专业和受限。以下是展示在DataFrame对象上进行典型操作的简单示例:
In [4]: df.index # ①
Out[4]: Index(['a', 'b', 'c', 'd'], dtype='object')
In [5]: df.columns # ②
Out[5]: Index(['numbers'], dtype='object')
In [6]: df.loc['c'] # ③
Out[6]: numbers 30
Name: c, dtype: int64
In [7]: df.loc[['a', 'd']] # ④
Out[7]: numbers
a 10
d 40
In [8]: df.iloc[1:3] # ⑤
Out[8]: numbers
b 20
c 30
In [9]: df.sum() # ⑥
Out[9]: numbers 100
dtype: int64
In [10]: df.apply(lambda x: x ** 2) # ⑦
Out[10]: numbers
a 100
b 400
c 900
d 1600
In [11]: df ** 2 # ⑧
Out[11]: numbers
a 100
b 400
c 900
d 1600
①
index属性和Index对象。
②
columns属性和Index对象。
③
选择与索引c对应的值。
④
选择与索引a和d对应的两个值。
⑤
通过索引位置选择第二行和第三行。
⑥
计算单列的总和。
⑦
使用apply()方法以向量化方式计算平方。
⑧
直接应用向量化,就像使用ndarray对象一样。
与NumPy的ndarray对象相反,可以在两个维度上扩大DataFrame对象:
In [12]: df['floats'] = (1.5, 2.5, 3.5, 4.5) # ①
In [13]: df
Out[13]: numbers floats
a 10 1.5
b 20 2.5
c 30 3.5
d 40 4.5
In [14]: df['floats'] # ②
Out[14]: a 1.5
b 2.5
c 3.5
d 4.5
Name: floats, dtype: float64
①
使用提供的float对象作为tuple对象添加新列。
②
选择此列并显示其数据和索引标签。
整个DataFrame对象也可以用来定义新列。在这种情况下,索引会自动对齐:
In [15]: df['names'] = pd.DataFrame(['Yves', 'Sandra', 'Lilli', 'Henry'],
index=['d', 'a', 'b', 'c']) # ①
In [16]: df
Out[16]: numbers floats names
a 10 1.5 Sandra
b 20 2.5 Lilli
c 30 3.5 Henry
d 40 4.5 Yves
①
基于DataFrame对象创建另一个新列。
数据附加工作方式类似。但是,在以下示例中,我们看到通常应避免的副作用——索引被简单的范围索引替换:
In [17]: df.append({'numbers': 100, 'floats': 5.75, 'names': 'Jil'},
ignore_index=True) # ①
Out[17]: numbers floats names
0 10 1.50 Sandra
1 20 2.50 Lilli
2 30 3.50 Henry
3 40 4.50 Yves
4 100 5.75 Jil
In [18]: df = df.append(pd.DataFrame({'numbers': 100, 'floats': 5.75,
'names': 'Jil'}, index=['y',])) # ②
In [19]: df
Out[19]: floats names numbers
a 1.50 Sandra 10
b 2.50 Lilli 20
c 3.50 Henry 30
d 4.50 Yves 40
y 5.75 Jil 100
In [20]: df = df.append(pd.DataFrame({'names': 'Liz'}, index=['z',])) # ③
In [21]: df
Out[21]: floats names numbers
a 1.50 Sandra 10.0
b 2.50 Lilli 20.0
c 3.50 Henry 30.0
d 4.50 Yves 40.0
y 5.75 Jil 100.0
z NaN Liz NaN
In [22]: df.dtypes # ④
Out[22]: floats float64
names object
numbers float64
dtype: object
①
通过dict对象添加新行;这是一个临时操作,在此期间索引信息会丢失。
②
这基于具有索引信息的DataFrame对象附加行;原始索引信息被保留。
③
这将不完整的数据行附加到DataFrame对象中,导致NaN值。
④
单列的不同dtypes;这类似于带有NumPy的记录数组。
尽管现在存在缺失值,但大多数方法调用仍将起作用。例如:
In [23]: df[['numbers', 'floats']].mean() # ①
Out[23]: numbers 40.00
floats 3.55
dtype: float64
In [24]: df[['numbers', 'floats']].std() # ②
Out[24]: numbers 35.355339
floats 1.662077
dtype: float64
①
对指定的两列求平均值(忽略具有NaN值的行)。
②
对指定的两列计算标准差(忽略具有NaN值的行)。
DataFrame 类的第二步
本小节中的示例基于具有标准正态分布随机数的ndarray对象。它探索了进一步的功能,如使用DatetimeIndex来管理时间序列数据。
In [25]: import numpy as np
In [26]: np.random.seed(100)
In [27]: a = np.random.standard_normal((9, 4))
In [28]: a
Out[28]: array([[-1.74976547, 0.3426804 , 1.1530358 , -0.25243604],
[ 0.98132079, 0.51421884, 0.22117967, -1.07004333],
[-0.18949583, 0.25500144, -0.45802699, 0.43516349],
[-0.58359505, 0.81684707, 0.67272081, -0.10441114],
[-0.53128038, 1.02973269, -0.43813562, -1.11831825],
[ 1.61898166, 1.54160517, -0.25187914, -0.84243574],
[ 0.18451869, 0.9370822 , 0.73100034, 1.36155613],
[-0.32623806, 0.05567601, 0.22239961, -1.443217 ],
[-0.75635231, 0.81645401, 0.75044476, -0.45594693]])
尽管可以更直接地构造DataFrame对象(如前所示),但通常使用ndarray对象是一个很好的选择,因为pandas将保留基本结构,并且“只”会添加元信息(例如,索引值)。它还代表了金融应用和一般科学研究的典型用例。例如:
In [29]: df = pd.DataFrame(a) # ①
In [30]: df
Out[30]: 0 1 2 3
0 -1.749765 0.342680 1.153036 -0.252436
1 0.981321 0.514219 0.221180 -1.070043
2 -0.189496 0.255001 -0.458027 0.435163
3 -0.583595 0.816847 0.672721 -0.104411
4 -0.531280 1.029733 -0.438136 -1.118318
5 1.618982 1.541605 -0.251879 -0.842436
6 0.184519 0.937082 0.731000 1.361556
7 -0.326238 0.055676 0.222400 -1.443217
8 -0.756352 0.816454 0.750445 -0.455947
①
从ndarray对象创建DataFrame对象。
表 5-1 列出了DataFrame函数接受的参数。在表中,“array-like”意味着类似于ndarray对象的数据结构,例如list。Index是pandas Index类的一个实例。
表 5-1. DataFrame 函数的参数
| 参数 | 格式 | 描述 |
|---|---|---|
data |
ndarray/dict/DataFrame |
DataFrame的数据;dict可以包含Series,ndarray,list等 |
index |
Index/array-like |
要使用的索引;默认为range(n) |
columns |
Index/array-like |
要使用的列标题;默认为range(n) |
dtype |
dtype,默认为None |
要使用/强制的数据类型;否则,它会被推断 |
copy |
bool,默认为None |
从输入复制数据 |
与结构化数组一样,正如我们已经看到的那样,DataFrame对象具有可以直接通过分配具有正确数量元素的list来定义的列名。这说明您可以在需要时定义/更改DataFrame对象的属性:
In [31]: df.columns = ['No1', 'No2', 'No3', 'No4'] # ①
In [32]: df
Out[32]: No1 No2 No3 No4
0 -1.749765 0.342680 1.153036 -0.252436
1 0.981321 0.514219 0.221180 -1.070043
2 -0.189496 0.255001 -0.458027 0.435163
3 -0.583595 0.816847 0.672721 -0.104411
4 -0.531280 1.029733 -0.438136 -1.118318
5 1.618982 1.541605 -0.251879 -0.842436
6 0.184519 0.937082 0.731000 1.361556
7 -0.326238 0.055676 0.222400 -1.443217
8 -0.756352 0.816454 0.750445 -0.455947
In [33]: df['No2'].mean() # ②
Out[33]: 0.70103309414564585
①
通过list对象指定列标签。
②
现在选择列变得很容易。
要高效处理金融时间序列数据,必须能够处理时间索引。这也可以被视为pandas的一项重要优势。例如,假设我们的四个列中的九个数据条目对应于从 2019 年 1 月开始的每月末数据。然后,可以使用date_range()函数生成DatetimeIndex对象,如下所示:
In [34]: dates = pd.date_range('2019-1-1', periods=9, freq='M') # ①
In [35]: dates
Out[35]: DatetimeIndex(['2019-01-31', '2019-02-28', '2019-03-31', '2019-04-30',
'2019-05-31', '2019-06-30', '2019-07-31', '2019-08-31',
'2019-09-30'],
dtype='datetime64[ns]', freq='M')
①
创建一个DatetimeIndex对象。
表 5-2 列出了date_range函数的参数。
表 5-2。date_range函数的参数
| 参数 | 格式 | 描述 |
|---|---|---|
start |
string/datetime |
生成日期的左边界 |
end |
string/datetime |
生成日期的右边界 |
periods |
integer/None |
期数(如果start或end为None) |
freq |
string/DateOffset |
频率字符串,例如,5D代表 5 天 |
tz |
string/None |
本地化索引的时区名称 |
normalize |
bool,默认为None |
规范化start和end为午夜 |
name |
string,默认为None |
结果索引的名称 |
以下代码将刚刚创建的DatetimeIndex对象定义为相关的索引对象,从而使原始数据集生成时间序列:
In [36]: df.index = dates
In [37]: df
Out[37]: No1 No2 No3 No4
2019-01-31 -1.749765 0.342680 1.153036 -0.252436
2019-02-28 0.981321 0.514219 0.221180 -1.070043
2019-03-31 -0.189496 0.255001 -0.458027 0.435163
2019-04-30 -0.583595 0.816847 0.672721 -0.104411
2019-05-31 -0.531280 1.029733 -0.438136 -1.118318
2019-06-30 1.618982 1.541605 -0.251879 -0.842436
2019-07-31 0.184519 0.937082 0.731000 1.361556
2019-08-31 -0.326238 0.055676 0.222400 -1.443217
2019-09-30 -0.756352 0.816454 0.750445 -0.455947
在使用date_range函数生成DatetimeIndex对象时,频率参数freq有多种选择。表 5-3 列出了所有选项。
表 5-3。date_range函数的频率参数值
| 别名 | 描述 |
|---|---|
B |
工作日频率 |
C |
自定义工作日频率(实验性的) |
D |
日历日频率 |
W |
周频率 |
M |
月度末频率 |
BM |
工作月末频率 |
MS |
月初频率 |
BMS |
工作月初频率 |
Q |
季度末频率 |
BQ |
工作季度末频率 |
QS |
季度初频率 |
BQS |
工作季度初频率 |
A |
年度末频率 |
BA |
工作年度末频率 |
AS |
年度初频率 |
BAS |
工作年度初频率 |
H |
每小时频率 |
T |
分钟频率 |
S |
每秒频率 |
L |
毫秒 |
U |
微秒 |
在某些情况下,以ndarray对象的形式访问原始数据集是值得的。例如,values属性直接提供了对它的访问。
In [38]: df.values
Out[38]: array([[-1.74976547, 0.3426804 , 1.1530358 , -0.25243604],
[ 0.98132079, 0.51421884, 0.22117967, -1.07004333],
[-0.18949583, 0.25500144, -0.45802699, 0.43516349],
[-0.58359505, 0.81684707, 0.67272081, -0.10441114],
[-0.53128038, 1.02973269, -0.43813562, -1.11831825],
[ 1.61898166, 1.54160517, -0.25187914, -0.84243574],
[ 0.18451869, 0.9370822 , 0.73100034, 1.36155613],
[-0.32623806, 0.05567601, 0.22239961, -1.443217 ],
[-0.75635231, 0.81645401, 0.75044476, -0.45594693]])
In [39]: np.array(df)
Out[39]: array([[-1.74976547, 0.3426804 , 1.1530358 , -0.25243604],
[ 0.98132079, 0.51421884, 0.22117967, -1.07004333],
[-0.18949583, 0.25500144, -0.45802699, 0.43516349],
[-0.58359505, 0.81684707, 0.67272081, -0.10441114],
[-0.53128038, 1.02973269, -0.43813562, -1.11831825],
[ 1.61898166, 1.54160517, -0.25187914, -0.84243574],
[ 0.18451869, 0.9370822 , 0.73100034, 1.36155613],
[-0.32623806, 0.05567601, 0.22239961, -1.443217 ],
[-0.75635231, 0.81645401, 0.75044476, -0.45594693]])
数组和数据框
通常情况下,您可以从ndarray对象中生成DataFrame对象。但是,您也可以通过使用DataFrame类的values属性或NumPy的np.array()函数轻松地从DataFrame生成ndarray对象。
基本分析
像NumPy的ndarray对象一样,pandas的DataFrame类内置了许多便利方法。作为入门,考虑info()方法和+describe()。
In [40]: df.info() # ①
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 9 entries, 2019-01-31 to 2019-09-30
Freq: M
Data columns (total 4 columns):
No1 9 non-null float64
No2 9 non-null float64
No3 9 non-null float64
No4 9 non-null float64
dtypes: float64(4)
memory usage: 360.0 bytes
In [41]: df.describe() # ②
Out[41]: No1 No2 No3 No4
count 9.000000 9.000000 9.000000 9.000000
mean -0.150212 0.701033 0.289193 -0.387788
std 0.988306 0.457685 0.579920 0.877532
min -1.749765 0.055676 -0.458027 -1.443217
25% -0.583595 0.342680 -0.251879 -1.070043
50% -0.326238 0.816454 0.222400 -0.455947
75% 0.184519 0.937082 0.731000 -0.104411
max 1.618982 1.541605 1.153036 1.361556
①
提供有关数据、列和索引的元信息。
②
为每列提供有用的摘要统计信息(针对数值数据)。
此外,您可以轻松地按列或按行获取和累积和,平均值,如下所示:
In [42]: df.sum() # ①
Out[42]: No1 -1.351906
No2 6.309298
No3 2.602739
No4 -3.490089
dtype: float64
In [43]: df.mean() # ②
Out[43]: No1 -0.150212
No2 0.701033
No3 0.289193
No4 -0.387788
dtype: float64
In [44]: df.mean(axis=0) # ②
Out[44]: No1 -0.150212
No2 0.701033
No3 0.289193
No4 -0.387788
dtype: float64
In [45]: df.mean(axis=1) # ③
Out[45]: 2019-01-31 -0.126621
2019-02-28 0.161669
2019-03-31 0.010661
2019-04-30 0.200390
2019-05-31 -0.264500
2019-06-30 0.516568
2019-07-31 0.803539
2019-08-31 -0.372845
2019-09-30 0.088650
Freq: M, dtype: float64
In [46]: df.cumsum() # ④
Out[46]: No1 No2 No3 No4
2019-01-31 -1.749765 0.342680 1.153036 -0.252436
2019-02-28 -0.768445 0.856899 1.374215 -1.322479
2019-03-31 -0.957941 1.111901 0.916188 -0.887316
2019-04-30 -1.541536 1.928748 1.588909 -0.991727
2019-05-31 -2.072816 2.958480 1.150774 -2.110045
2019-06-30 -0.453834 4.500086 0.898895 -2.952481
2019-07-31 -0.269316 5.437168 1.629895 -1.590925
2019-08-31 -0.595554 5.492844 1.852294 -3.034142
2019-09-30 -1.351906 6.309298 2.602739 -3.490089
①
逐列求和。
②
逐列平均值。
③
逐行平均值。
④
逐列累积和(从第一个索引位置开始)。
DataFrame对象也按预期理解NumPy通用函数:
In [47]: np.mean(df) # ①
Out[47]: No1 -0.150212
No2 0.701033
No3 0.289193
No4 -0.387788
dtype: float64
In [48]: np.log(df) # ②
/Users/yves/miniconda3/envs/base/lib/python3.6/site-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in log
"""Entry point for launching an IPython kernel.
Out[48]: No1 No2 No3 No4
2019-01-31 NaN -1.070957 0.142398 NaN
2019-02-28 -0.018856 -0.665106 -1.508780 NaN
2019-03-31 NaN -1.366486 NaN -0.832033
2019-04-30 NaN -0.202303 -0.396425 NaN
2019-05-31 NaN 0.029299 NaN NaN
2019-06-30 0.481797 0.432824 NaN NaN
2019-07-31 -1.690005 -0.064984 -0.313341 0.308628
2019-08-31 NaN -2.888206 -1.503279 NaN
2019-09-30 NaN -0.202785 -0.287089 NaN
In [49]: np.sqrt(abs(df)) # ③
Out[49]: No1 No2 No3 No4
2019-01-31 1.322787 0.585389 1.073795 0.502430
2019-02-28 0.990616 0.717091 0.470297 1.034429
2019-03-31 0.435311 0.504977 0.676777 0.659669
2019-04-30 0.763934 0.903796 0.820196 0.323127
2019-05-31 0.728890 1.014757 0.661918 1.057506
2019-06-30 1.272392 1.241614 0.501876 0.917843
2019-07-31 0.429556 0.968030 0.854986 1.166857
2019-08-31 0.571173 0.235958 0.471593 1.201340
2019-09-30 0.869685 0.903578 0.866282 0.675238
In [50]: np.sqrt(abs(df)).sum() # ④
Out[50]: No1 7.384345
No2 7.075190
No3 6.397719
No4 7.538440
dtype: float64
In [51]: 100 * df + 100 # ⑤
Out[51]: No1 No2 No3 No4
2019-01-31 -74.976547 134.268040 215.303580 74.756396
2019-02-28 198.132079 151.421884 122.117967 -7.004333
2019-03-31 81.050417 125.500144 54.197301 143.516349
2019-04-30 41.640495 181.684707 167.272081 89.558886
2019-05-31 46.871962 202.973269 56.186438 -11.831825
2019-06-30 261.898166 254.160517 74.812086 15.756426
2019-07-31 118.451869 193.708220 173.100034 236.155613
2019-08-31 67.376194 105.567601 122.239961 -44.321700
2019-09-30 24.364769 181.645401 175.044476 54.405307
①
逐列平均值。
②
逐元素自然对数;会发出警告,但计算会继续进行,导致多个NaN值。
③
绝对值的逐元素平方根 …
④
… 以及结果的逐列平均值。
⑤
数值数据的线性变换。
NumPy 通用函数
通常情况下,您可以将NumPy通用函数应用于pandas的DataFrame对象,只要它们可以应用于包含相同类型数据的ndarray对象。
pandas相当容错,以捕获错误并在相应的数学运算失败时仅放置NaN值。不仅如此,正如之前简要展示的那样,您还可以在许多情况下像处理完整数据集一样处理这些不完整数据集。这非常方便,因为现实往往被不完整的数据集所表征,这比人们希望的更常见。
基本可视化
通常情况下,一旦数据存储在DataFrame对象中,数据的绘制就只需一行代码即可(参见图 5-1):
In [52]: from pylab import plt, mpl # ①
plt.style.use('seaborn') # ①
mpl.rcParams['font.family'] = 'serif' # ①
%matplotlib inline
In [53]: df.cumsum().plot(lw=2.0, figsize=(10, 6)); # ②
# plt.savefig('../../images/ch05/pd_plot_01.png')
①
自定义绘图样式。
②
将四列的累积和绘制成折线图。

图 5-1。DataFrame对象的折线图
基本上,pandas 提供了一个围绕 matplotplib(参见第七章)的包装器,专门设计用于 DataFrame 对象。表 5-4 列出了 plot 方法接受的参数。
表 5-4。plot 方法的参数
| 参数 | 格式 | 描述 |
|---|---|---|
x |
标签/位置,默认为 None |
仅当列值为 x 刻度时使用 |
y |
标签/位置,默认为 None |
仅当列值为 y 刻度时使用 |
subplots |
布尔值,默认为 False |
在子图中绘制列 |
sharex |
布尔值,默认为 True |
x 轴共享 |
sharey |
布尔值,默认为 False |
y 轴共享 |
use_index |
布尔值,默认为 True |
使用 DataFrame.index 作为 x 刻度 |
stacked |
布尔值,默认为 False |
堆叠(仅用于柱状图) |
sort_columns |
布尔值,默认为 False |
绘图前按字母顺序排序列 |
title |
字符串,默认为 None |
绘图标题 |
grid |
布尔值,默认为 False |
水平和垂直网格线 |
legend |
布尔值,默认为 True |
标签的图例 |
ax |
matplotlib axis 对象 |
用于绘图的 matplotlib axis 对象 |
style |
字符串或列表/字典 | 线绘图风格(对每列) |
kind |
"line"/"bar"/"barh"/"kde"/"density" |
绘图类型 |
logx |
布尔值,默认为 False |
x 轴的对数缩放 |
logy |
布尔值,默认为 False |
y 轴的对数缩放 |
xticks |
序列,默认为 Index |
绘图的 x 刻度 |
yticks |
序列,默认为 Values |
绘图的 y 刻度 |
xlim |
2-元组,列表 | x 轴的边界 |
ylim |
2-元组,列表 | y 轴的边界 |
rot |
整数,默认为 None |
x 刻度的旋转 |
secondary_y |
布尔值/序列,默认为 False |
次要 y 轴 |
mark_right |
布尔值,默认为 True |
次要轴的自动标记 |
colormap |
字符串/colormap 对象,默认为 None |
用于绘图的色图 |
kwds |
关键字 | 传递给 matplotlib 的选项 |
作为另一个示例,考虑绘制相同数据的柱状图(参见图 5-1)。
In [54]: df.plot(kind='bar', figsize=(10, 6)); # ①
# plt.savefig('../../images/ch05/pd_plot_02.png')
①
使用 kind 参数来改变绘图类型。

图 5-2。DataFrame 对象的柱状图
Series 类
到目前为止,我们主要使用 pandas 的 DataFrame 类。Series 类是另一个与 pandas 一起提供的重要类。它的特点是只有一列数据。从这个意义上说,它是 DataFrame 类的一个特化,共享许多但不是所有的特征和功能。通常,当从多列 DataFrame 对象中选择单列时,会得到一个 Series 对象:
In [55]: type(df)
Out[55]: pandas.core.frame.DataFrame
In [56]: s = df['No1']
In [57]: s
Out[57]: 2019-01-31 -1.749765
2019-02-28 0.981321
2019-03-31 -0.189496
2019-04-30 -0.583595
2019-05-31 -0.531280
2019-06-30 1.618982
2019-07-31 0.184519
2019-08-31 -0.326238
2019-09-30 -0.756352
Freq: M, Name: No1, dtype: float64
In [58]: type(s)
Out[58]: pandas.core.series.Series
主要的DataFrame方法也适用于Series对象。举例来说,考虑mean()和plot()方法(见图 5-3):
In [59]: s.mean()
Out[59]: -0.15021177307319458
In [60]: s.plot(lw=2.0, figsize=(10, 6));
# plt.savefig('../../images/ch05/pd_plot_03.png')

图 5-3。一个 Series 对象的线性图
分组操作
pandas具有强大且灵活的分组功能。它们与SQL中的分组以及 MicrosoftExcel中的数据透视表类似。为了有东西可以分组,我们添加了一列,指示相应数据所属的季度:
In [61]: df['Quarter'] = ['Q1', 'Q1', 'Q1', 'Q2', 'Q2',
'Q2', 'Q3', 'Q3', 'Q3']
df
Out[61]: No1 No2 No3 No4 Quarter
2019-01-31 -1.749765 0.342680 1.153036 -0.252436 Q1
2019-02-28 0.981321 0.514219 0.221180 -1.070043 Q1
2019-03-31 -0.189496 0.255001 -0.458027 0.435163 Q1
2019-04-30 -0.583595 0.816847 0.672721 -0.104411 Q2
2019-05-31 -0.531280 1.029733 -0.438136 -1.118318 Q2
2019-06-30 1.618982 1.541605 -0.251879 -0.842436 Q2
2019-07-31 0.184519 0.937082 0.731000 1.361556 Q3
2019-08-31 -0.326238 0.055676 0.222400 -1.443217 Q3
2019-09-30 -0.756352 0.816454 0.750445 -0.455947 Q3
现在,我们可以按Quarter列进行分组,并且可以输出单个组的统计信息:
In [62]: groups = df.groupby('Quarter') # ①
In [63]: groups.size() # ②
Out[63]: Quarter
Q1 3
Q2 3
Q3 3
dtype: int64
In [64]: groups.mean() # ③
Out[64]: No1 No2 No3 No4
Quarter
Q1 -0.319314 0.370634 0.305396 -0.295772
Q2 0.168035 1.129395 -0.005765 -0.688388
Q3 -0.299357 0.603071 0.567948 -0.179203
In [65]: groups.max() # ④
Out[65]: No1 No2 No3 No4
Quarter
Q1 0.981321 0.514219 1.153036 0.435163
Q2 1.618982 1.541605 0.672721 -0.104411
Q3 0.184519 0.937082 0.750445 1.361556
In [66]: groups.aggregate([min, max]).round(2) # ⑤
Out[66]: No1 No2 No3 No4
min max min max min max min max
Quarter
Q1 -1.75 0.98 0.26 0.51 -0.46 1.15 -1.07 0.44
Q2 -0.58 1.62 0.82 1.54 -0.44 0.67 -1.12 -0.10
Q3 -0.76 0.18 0.06 0.94 0.22 0.75 -1.44 1.36
①
根据Quarter列进行分组。
②
给出组中的行数。
③
给出每列的均值。
④
给出每列的最大值。
⑤
给出每列的最小值和最大值。
也可以通过多个列进行分组。为此,引入另一列,指示索引日期的月份是奇数还是偶数:
In [67]: df['Odd_Even'] = ['Odd', 'Even', 'Odd', 'Even', 'Odd', 'Even',
'Odd', 'Even', 'Odd']
In [68]: groups = df.groupby(['Quarter', 'Odd_Even'])
In [69]: groups.size()
Out[69]: Quarter Odd_Even
Q1 Even 1
Odd 2
Q2 Even 2
Odd 1
Q3 Even 1
Odd 2
dtype: int64
In [70]: groups[['No1', 'No4']].aggregate([sum, np.mean])
Out[70]: No1 No4
sum mean sum mean
Quarter Odd_Even
Q1 Even 0.981321 0.981321 -1.070043 -1.070043
Odd -1.939261 -0.969631 0.182727 0.091364
Q2 Even 1.035387 0.517693 -0.946847 -0.473423
Odd -0.531280 -0.531280 -1.118318 -1.118318
Q3 Even -0.326238 -0.326238 -1.443217 -1.443217
Odd -0.571834 -0.285917 0.905609 0.452805
这就是对pandas和DataFrame对象的介绍。后续部分将使用这个工具集来处理真实世界的金融数据。
复杂选择
数据选择通常通过在列值上制定条件来完成,并可能逻辑地组合多个这样的条件。考虑以下数据集。
In [71]: data = np.random.standard_normal((10, 2)) # ①
In [72]: df = pd.DataFrame(data, columns=['x', 'y']) # ②
In [73]: df.info() # ②
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10 entries, 0 to 9
Data columns (total 2 columns):
x 10 non-null float64
y 10 non-null float64
dtypes: float64(2)
memory usage: 240.0 bytes
In [74]: df.head() # ③
Out[74]: x y
0 1.189622 -1.690617
1 -1.356399 -1.232435
2 -0.544439 -0.668172
3 0.007315 -0.612939
4 1.299748 -1.733096
In [75]: df.tail() # ④
Out[75]: x y
5 -0.983310 0.357508
6 -1.613579 1.470714
7 -1.188018 -0.549746
8 -0.940046 -0.827932
9 0.108863 0.507810
①
具有标准正态分布随机数的ndarray对象。
②
具有相同随机数的DataFrame对象。
③
通过head()方法获得前五行。
④
通过tail()方法获得最后五行。
下面的代码说明了 Python 的比较运算符和逻辑运算符在两列值上的应用。
In [76]: df['x'] > 0.5 # ①
Out[76]: 0 True
1 False
2 False
3 False
4 True
5 False
6 False
7 False
8 False
9 False
Name: x, dtype: bool
In [77]: (df['x'] > 0) & (df['y'] < 0) # ②
Out[77]: 0 True
1 False
2 False
3 True
4 True
5 False
6 False
7 False
8 False
9 False
dtype: bool
In [78]: (df['x'] > 0) | (df['y'] < 0) # ③
Out[78]: 0 True
1 True
2 True
3 True
4 True
5 False
6 False
7 True
8 True
9 True
dtype: bool
①
检查x列中的值是否大于 0.5。
②
检查x列中的值是否为正且y列中的值是否为负。
③
检查x列中的值是否为正或y列中的值是否为负。
使用结果布尔Series对象,复杂数据(行)的选择很简单。
In [79]: df[df['x'] > 0] # ①
Out[79]: x y
0 1.189622 -1.690617
3 0.007315 -0.612939
4 1.299748 -1.733096
9 0.108863 0.507810
In [80]: df[(df['x'] > 0) & (df['y'] < 0)] # ②
Out[80]: x y
0 1.189622 -1.690617
3 0.007315 -0.612939
4 1.299748 -1.733096
In [81]: df[(df.x > 0) | (df.y < 0)] # ③
Out[81]: x y
0 1.189622 -1.690617
1 -1.356399 -1.232435
2 -0.544439 -0.668172
3 0.007315 -0.612939
4 1.299748 -1.733096
7 -1.188018 -0.549746
8 -0.940046 -0.827932
9 0.108863 0.507810
①
所有x列的值大于 0.5 的行。
②
所有x列的值为正且y列的值为负的行。
③
所有列中 x 的值为正或列中 y 的值为负的所有行(这里通过各自的属性访问列)。
比较运算符也可以一次应用于完整的 DataFrame 对象。
In [82]: df > 0 # ①
Out[82]: x y
0 True False
1 False False
2 False False
3 True False
4 True False
5 False True
6 False True
7 False False
8 False False
9 True True
In [83]: df[df > 0] # ②
Out[83]: x y
0 1.189622 NaN
1 NaN NaN
2 NaN NaN
3 0.007315 NaN
4 1.299748 NaN
5 NaN 0.357508
6 NaN 1.470714
7 NaN NaN
8 NaN NaN
9 0.108863 0.507810
①
DataFrame 对象中哪些值是正数?
②
选择所有这样的值,并在所有其他位置放置 NaN。
连接、合并和拼接
本节介绍了在形式上为 DataFrame 对象的两个简单数据集组合的不同方法。这两个简单数据集是:
In [84]: df1 = pd.DataFrame(['100', '200', '300', '400'],
index=['a', 'b', 'c', 'd'],
columns=['A',])
In [85]: df1
Out[85]: A
a 100
b 200
c 300
d 400
In [86]: df2 = pd.DataFrame(['200', '150', '50'],
index=['f', 'b', 'd'],
columns=['B',])
In [87]: df2
Out[87]: B
f 200
b 150
d 50
拼接
拼接或附加基本上意味着将行从一个 DataFrame 对象添加到另一个 DataFrame 对象。这可以通过 append() 方法或 pd.concat() 函数完成。一个主要问题是如何处理索引值。
In [88]: df1.append(df2) # ①
Out[88]: A B
a 100 NaN
b 200 NaN
c 300 NaN
d 400 NaN
f NaN 200
b NaN 150
d NaN 50
In [89]: df1.append(df2, ignore_index=True) # ②
Out[89]: A B
0 100 NaN
1 200 NaN
2 300 NaN
3 400 NaN
4 NaN 200
5 NaN 150
6 NaN 50
In [90]: pd.concat((df1, df2)) # ③
Out[90]: A B
a 100 NaN
b 200 NaN
c 300 NaN
d 400 NaN
f NaN 200
b NaN 150
d NaN 50
In [91]: pd.concat((df1, df2), ignore_index=True) # ④
Out[91]: A B
0 100 NaN
1 200 NaN
2 300 NaN
3 400 NaN
4 NaN 200
5 NaN 150
6 NaN 50
①
将来自 df2 的数据附加为 df1 的新行。
②
做同样的事情,但忽略了索引。
③
具有与第一个相同的效果,并且…
④
第二个追加操作,分别。
连接
在连接这两个数据集时,DataFrame 对象的顺序也很重要,但方式不同。只使用第一个 DataFrame 对象的索引值。这种默认行为称为左连接。
In [92]: df1.join(df2) # ①
Out[92]: A B
a 100 NaN
b 200 150
c 300 NaN
d 400 50
In [93]: df2.join(df1) # ②
Out[93]: B A
f 200 NaN
b 150 200
d 50 400
①
df1 的索引值相关。
②
df2 相关的索引值。
一共有四种不同的连接方法可用,每种方法都会导致索引值和相应数据行的处理方式不同。
In [94]: df1.join(df2, how='left') # ①
Out[94]: A B
a 100 NaN
b 200 150
c 300 NaN
d 400 50
In [95]: df1.join(df2, how='right') # ②
Out[95]: A B
f NaN 200
b 200 150
d 400 50
In [96]: df1.join(df2, how='inner') # ③
Out[96]: A B
b 200 150
d 400 50
In [97]: df1.join(df2, how='outer') # ④
Out[97]: A B
a 100 NaN
b 200 150
c 300 NaN
d 400 50
f NaN 200
①
左连接是默认操作。
②
右连接与颠倒 DataFrame 对象的顺序相同。
③
内连接仅保留那些在两个索引中都找到的索引值。
④
外连接保留来自两个索引的所有索引值。
也可以基于空的 DataFrame 对象进行连接。在这种情况下,列会被顺序创建,导致行为类似于左连接。
In [98]: df = pd.DataFrame()
In [99]: df['A'] = df1 # ①
In [100]: df
Out[100]: A
0 NaN
1 NaN
2 NaN
3 NaN
In [101]: df['B'] = df2 # ②
In [102]: df
Out[102]: A B
0 NaN NaN
1 NaN NaN
2 NaN NaN
3 NaN NaN
①
df1 作为第一列 A。
②
df2 作为第二列 B。
利用字典组合数据集的方式产生了类似外连接的结果,因为列是同时创建的。
In [103]: df = pd.DataFrame({'A': df1['A'], 'B': df2['B']}) # ①
In [104]: df
Out[104]: A B
a 100 NaN
b 200 150
c 300 NaN
d 400 50
f NaN 200
①
DataFrame 对象的列被用作 dict 对象中的值。
合并
虽然连接操作是基于要连接的 DataFrame 对象的索引进行的,但合并操作通常是在两个数据集之间共享的列上进行的。为此,将新列 C 添加到原始的两个 DataFrame 对象中:
In [105]: c = pd.Series([250, 150, 50], index=['b', 'd', 'c'])
df1['C'] = c
df2['C'] = c
In [106]: df1
Out[106]: A C
a 100 NaN
b 200 250.0
c 300 50.0
d 400 150.0
In [107]: df2
Out[107]: B C
f 200 NaN
b 150 250.0
d 50 150.0
默认情况下,此情况下的合并操作基于单个共享列 C 进行。然而,还有其他选项可用。
In [108]: pd.merge(df1, df2) # ①
Out[108]: A C B
0 100 NaN 200
1 200 250.0 150
2 400 150.0 50
In [109]: pd.merge(df1, df2, on='C') # ①
Out[109]: A C B
0 100 NaN 200
1 200 250.0 150
2 400 150.0 50
In [110]: pd.merge(df1, df2, how='outer') # ②
Out[110]: A C B
0 100 NaN 200
1 200 250.0 150
2 300 50.0 NaN
3 400 150.0 50
①
默认在列 C 上合并。
②
外部合并也是可能的,保留所有数据行。
还有许多其他类型的合并操作可用,以下代码示例了其中的一些:
In [111]: pd.merge(df1, df2, left_on='A', right_on='B')
Out[111]: A C_x B C_y
0 200 250.0 200 NaN
In [112]: pd.merge(df1, df2, left_on='A', right_on='B', how='outer')
Out[112]: A C_x B C_y
0 100 NaN NaN NaN
1 200 250.0 200 NaN
2 300 50.0 NaN NaN
3 400 150.0 NaN NaN
4 NaN NaN 150 250.0
5 NaN NaN 50 150.0
In [113]: pd.merge(df1, df2, left_index=True, right_index=True)
Out[113]: A C_x B C_y
b 200 250.0 150 250.0
d 400 150.0 50 150.0
In [114]: pd.merge(df1, df2, on='C', left_index=True)
Out[114]: A C B
f 100 NaN 200
b 200 250.0 150
d 400 150.0 50
In [115]: pd.merge(df1, df2, on='C', right_index=True)
Out[115]: A C B
a 100 NaN 200
b 200 250.0 150
d 400 150.0 50
In [116]: pd.merge(df1, df2, on='C', left_index=True, right_index=True)
Out[116]: A C B
b 200 250.0 150
d 400 150.0 50
性能方面
本章中的许多示例说明了使用 pandas 可以实现相同目标的多个选项。本节比较了用于逐元素添加两列的此类选项。首先,使用 NumPy 生成的数据集。
In [117]: data = np.random.standard_normal((1000000, 2)) # ①
In [118]: data.nbytes # ①
Out[118]: 16000000
In [119]: df = pd.DataFrame(data, columns=['x', 'y']) # ②
In [120]: df.info() # ②
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000000 entries, 0 to 999999
Data columns (total 2 columns):
x 1000000 non-null float64
y 1000000 non-null float64
dtypes: float64(2)
memory usage: 15.3 MB
①
带有随机数字的 ndarray 对象。
②
带有随机数字的 DataFrame 对象。
第二,一些完成任务的性能值的选项。
In [121]: %time res = df['x'] + df['y'] # ①
CPU times: user 5.68 ms, sys: 14.5 ms, total: 20.1 ms
Wall time: 4.06 ms
In [122]: res[:3]
Out[122]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [123]: %time res = df.sum(axis=1) # ②
CPU times: user 44 ms, sys: 14.9 ms, total: 58.9 ms
Wall time: 57.6 ms
In [124]: res[:3]
Out[124]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [125]: %time res = df.values.sum(axis=1) # ③
CPU times: user 16.1 ms, sys: 1.74 ms, total: 17.8 ms
Wall time: 16.6 ms
In [126]: res[:3]
Out[126]: array([ 0.3872424 , -0.96934273, -0.86315944])
In [127]: %time res = np.sum(df, axis=1) # ④
CPU times: user 39.7 ms, sys: 8.91 ms, total: 48.7 ms
Wall time: 47.7 ms
In [128]: res[:3]
Out[128]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [129]: %time res = np.sum(df.values, axis=1) # ⑤
CPU times: user 16.1 ms, sys: 1.78 ms, total: 17.9 ms
Wall time: 16.6 ms
In [130]: res[:3]
Out[130]: array([ 0.3872424 , -0.96934273, -0.86315944])
①
直接操作列(Series对象)是最快的方法。
②
这通过在 DataFrame 对象上调用 sum() 方法来计算总和。
③
这通过在 ndarray 对象上调用 sum() 方法来计算总和。
④
这通过在 DataFrame 对象上调用 np.sum() 方法来计算总和。
⑤
这通过在 ndarray 对象上使用通用函数 np.sum() 方法来计算总和。
最后,更多基于 eval() 和 apply() 方法的选项。
In [131]: %time res = df.eval('x + y') # ①
CPU times: user 13.3 ms, sys: 15.6 ms, total: 28.9 ms
Wall time: 18.5 ms
In [132]: res[:3]
Out[132]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [133]: %time res = df.apply(lambda row: row['x'] + row['y'], axis=1) # ②
CPU times: user 22 s, sys: 71 ms, total: 22.1 s
Wall time: 22.1 s
In [134]: res[:3]
Out[134]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
# tag::PD_34[]
①
eval() 是专门用于评估(复杂)数值表达式的方法;可以直接访问列。
②
最慢的选项是逐行使用 apply() 方法;这就像在 Python 级别上循环遍历所有行。
注意
pandas 通常提供多种选项来实现相同的目标。如果不确定,应该比较一些选项,以确保在时间紧迫时获得最佳性能。在简单示例中,执行时间相差数个数量级。
结论
pandas 是数据分析的强大工具,并已成为所谓 PyData 栈的核心包。它的 DataFrame 类特别适用于处理任何类型的表格数据。对这种对象的大多数操作都是矢量化的,这不仅使代码简洁,而且通常性能很高,与 NumPy 的情况一样。此外,pandas 还使得处理不完整的数据集变得方便,例如,使用 NumPy 并不那么方便。在本书的许多后续章节中,pandas 和 DataFrame 类将是核心,当需要时还将使用和说明其他功能。
进一步阅读
pandas 是一个文档齐全的开源项目,既有在线文档,也有可供下载的 PDF 版本。¹。以下页面提供了所有资源:
至于 NumPy,在书籍形式上推荐的参考资料是:
-
McKinney, Wes (2017): Python 数据分析. 第二版, O’Reilly, 北京等地。
-
VanderPlas, Jake (2016): Python 数据科学手册. O’Reilly, 北京等地。
¹ 在撰写本文时,PDF 版本共有 2,207 页(版本 0.21.1)。
第六章:面向对象编程
软件工程的目的是控制复杂性,而不是创建它。
Pamela Zave
介绍
面向对象编程(OOP)是当今最流行的编程范式之一。正确使用时,它与过程式编程相比提供了许多优势。在许多情况下,OOP 似乎特别适用于金融建模和实施金融算法。然而,也有许多对 OOP 持批评态度的人,对 OOP 的单个方面甚至整个范式表示怀疑。本章对此持中立态度,认为 OOP 是一个重要的工具,可能不是每个问题的最佳解决方案,但应该是程序员和从事金融工作的量化人员的手头工具之一。
随着 OOP 的出现,一些新的术语也随之而来。本书和本章的最重要术语是(更多细节如下):
类
对象类的抽象定义。例如,一个人类。
属性
类的特性(类属性)或类的实例(实例属性)的一个特征。例如,是哺乳动物或眼睛的颜色。
方法
可以在类上实现的操作。例如,行走。
参数
一个方法接受的输入参数以影响其行为。例如,三个步骤。
对象
类的一个实例。例如,有蓝眼睛的 Sandra。
实例化
创建基于抽象类的特定对象的过程。
转换为 Python 代码,实现人类示例的简单类可能如下所示。
In [1]: class HumanBeing(object): # ①
def __init__(self, first_name, eye_color): # ②
self.first_name = first_name # ③
self.eye_color = eye_color # ④
self.position = 0 # ⑤
def walk_steps(self, steps): # ⑥
self.position += steps # ⑦
①
类定义语句。
②
在实例化时调用的特殊方法。
③
名字属性初始化为参数值。
④
眼睛颜色属性初始化为参数值。
⑤
位置属性初始化为 0。
⑥
使用steps作为参数的步行方法定义。
⑦
给定steps值后改变位置的代码。
根据类定义,可以实例化并使用一个新的 Python 对象。
In [2]: Sandra = HumanBeing('Sandra', 'blue') # ①
In [3]: Sandra.first_name # ②
Out[3]: 'Sandra'
In [4]: Sandra.position # ②
Out[4]: 0
In [5]: Sandra.walk_steps(5) # ③
In [6]: Sandra.position # ④
Out[6]: 5
①
实例化。
②
访问属性值。
③
调用方法。
④
访问更新后的position值。
有几个人类方面可能支持使用 OOP:
自然的思考方式
人类思维通常围绕着现实世界或抽象对象展开,比如汽车或金融工具。面向对象编程适合模拟具有其特征的这类对象。
降低复杂性
通过不同的方法,面向对象编程有助于降低问题或算法的复杂性,并逐个特征进行建模。
更好的用户界面
在许多情况下,面向对象编程可以实现更美观的用户界面和更紧凑的代码。例如,当查看NumPy的ndarray类或pandas的DataFrame类时,这一点变得显而易见。
Python 建模的方式
独立于面向对象编程的优缺点,它只是 Python 中的主导范式。这也是“在 Python 中一切皆为对象。”这句话的由来。面向对象编程还允许程序员构建自定义类,其实例的行为与标准 Python 类的任何其他实例相同。
也有一些技术方面可能支持面向对象编程:
抽象化
使用属性和方法可以构建对象的抽象、灵活的模型——重点放在相关的内容上,忽略不需要的内容。在金融领域,这可能意味着拥有一个以抽象方式模拟金融工具的通用类。这种类的实例将是由投资银行设计和提供的具体金融产品,例如。
模块化
面向对象编程简化了将代码拆分为多个模块的过程,然后将这些模块链接起来形成完整的代码基础。例如,可以通过一个类或两个类来建模股票上的欧式期权,一个用于基础股票,另一个用于期权本身。
继承
继承指的是一个类可以从另一个类继承属性和方法的概念。在金融领域,从一个通用的金融工具开始,下一个级别可能是一个通用的衍生金融工具,然后是一个欧式期权,再然后是一个欧式看涨期权。每个类都可以从更高级别的类中继承属性和方法。
聚合
聚合指的是一个对象至少部分由多个其他对象组成,这些对象可能是独立存在的。模拟欧式看涨期权的类可能具有其他对象的属性,例如基础股票和用于贴现的相关短期利率。表示股票和短期利率的对象也可以被其他对象独立使用。
组合
组合与聚合类似,但是这里的单个对象不能独立存在。考虑一个定制的固定利率互换合同和一个浮动利率互换合同。这两个腿不能独立于互换合同本身存在。
多态性
多态性可以呈现多种形式。在 Python 上下文中特别重要的是所谓的鸭子类型。这指的是可以在许多不同类及其实例上实现标准操作,而不需要准确知道正在处理的特定对象是什么。对于金融工具类,这可能意味着可以调用一个名为 get_current_price() 的方法,而不管对象的具体类型是什么(股票、期权、互换等)。
封装
此概念指的是仅通过公共方法访问类内部数据的方法。模拟股票的类可能有一个属性 current_stock_price。封装将通过方法 get_current_stock_price() 提供对属性值的访问,并将数据隐藏(使其私有化)。这种方法可能通过仅使用和可能更改属性值来避免意外效果。但是,对于如何使数据在 Python 类中私有化存在限制。
在更高层面上,软件工程中的两个主要目标可以总结如下:
可重用性
继承和多态等概念提高了代码的可重用性,增加了程序员的效率和生产力。它们还简化了代码的维护。
非冗余性
与此同时,这些方法允许构建几乎不冗余的代码,避免双重实现工作,减少调试和测试工作以及维护工作量。它还可能导致更小的总体代码基础。
本章按如下方式组织:
“Python 对象概览”
下一节将通过面向对象编程的视角简要介绍一些 Python 对象。
“Python 类基础”
本节介绍了 Python 中面向对象编程的核心要素,并以金融工具和投资组合头寸为主要示例。
“Python 数据模型”
本节讨论了 Python 数据模型的重要元素以及某些特殊方法所起的作用。
Python 对象概览
本节通过面向对象编程程序员的眼光简要介绍了一些标准对象,这些对象在前一节中已经遇到过。
int
为了简单起见,考虑一个整数对象。即使对于这样一个简单的 Python 对象,主要的面向对象编程(OOP)特征也是存在的。
In [7]: n = 5 # ①
In [8]: type(n) # ②
Out[8]: int
In [9]: n.numerator # ③
Out[9]: 5
In [10]: n.bit_length() # ④
Out[10]: 3
In [11]: n + n # ⑤
Out[11]: 10
In [12]: 2 * n # ⑥
Out[12]: 10
In [13]: n.__sizeof__() # ⑦
Out[13]: 28
①
新实例 n。
②
对象的类型。
③
一个属性。
④
一个方法。
⑤
应用 + 运算符(加法)。
⑥
应用 * 运算符(乘法)。
⑦
调用特殊方法__sizeof__()以获取内存使用情况(以字节为单位)。¹
列表
list对象有一些额外的方法,但基本上表现方式相同。
In [14]: l = [1, 2, 3, 4] # ①
In [15]: type(l) # ②
Out[15]: list
In [16]: l[0] # ③
Out[16]: 1
In [17]: l.append(10) # ④
In [18]: l + l # ⑤
Out[18]: [1, 2, 3, 4, 10, 1, 2, 3, 4, 10]
In [19]: 2 * l # ⑥
Out[19]: [1, 2, 3, 4, 10, 1, 2, 3, 4, 10]
In [20]: sum(l) # ⑦
Out[20]: 20
In [21]: l.__sizeof__() # ⑧
Out[21]: 104
①
新实例l。
②
对象的类型。
③
通过索引选择元素。
④
一个方法。
⑤
应用+运算符(连接)。
⑥
应用*运算符(连接)。
⑦
应用标准 Python 函数sum()。
⑧
调用特殊方法__sizeof__()以获取内存使用情况(以字节为单位)。
ndarray
int和list对象是标准的 Python 对象。NumPy的ndarray对象是一个来自开源包的“自定义”对象。
In [22]: import numpy as np # ①
In [23]: a = np.arange(16).reshape((4, 4)) # ②
In [24]: a # ②
Out[24]: array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
In [25]: type(a) # ③
Out[25]: numpy.ndarray
①
导入numpy。
②
新实例a。
③
对象的类型。
尽管ndarray对象不是标准对象,但在许多情况下表现得就像是一个标准对象——这要归功于下文中解释的 Python 数据模型。
In [26]: a.nbytes # ①
Out[26]: 128
In [27]: a.sum() # ②
Out[27]: 120
In [28]: a.cumsum(axis=0) # ③
Out[28]: array([[ 0, 1, 2, 3],
[ 4, 6, 8, 10],
[12, 15, 18, 21],
[24, 28, 32, 36]])
In [29]: a + a # ④
Out[29]: array([[ 0, 2, 4, 6],
[ 8, 10, 12, 14],
[16, 18, 20, 22],
[24, 26, 28, 30]])
In [30]: 2 * a # ⑤
Out[30]: array([[ 0, 2, 4, 6],
[ 8, 10, 12, 14],
[16, 18, 20, 22],
[24, 26, 28, 30]])
In [31]: sum(a) # ⑥
Out[31]: array([24, 28, 32, 36])
In [32]: np.sum(a) # ⑦
Out[32]: 120
In [33]: a.__sizeof__() # ⑧
Out[33]: 112
①
一个属性。
②
一个方法(聚合)。
③
一个方法(没有聚合)。
④
应用+运算符(加法)。
⑤
应用*运算符(乘法)。
⑥
应用标准 Python 函数sum()。
⑦
应用NumPy通用函数np.sum()。
⑧
调用特殊方法__sizeof__()以获取内存使用情况(以字节为单位)。
DataFrame
最后,快速查看pandas的DataFrame对象,因为其行为大多与ndarray对象相同。首先,基于ndarray对象实例化DataFrame对象。
In [34]: import pandas as pd # ①
In [35]: df = pd.DataFrame(a, columns=list('abcd')) # ②
In [36]: type(df) # ③
Out[36]: pandas.core.frame.DataFrame
①
导入pandas。
②
新实例df。
③
对象的类型。
其次,查看属性、方法和操作。
In [37]: df.columns # ①
Out[37]: Index(['a', 'b', 'c', 'd'], dtype='object')
In [38]: df.sum() # ②
Out[38]: a 24
b 28
c 32
d 36
dtype: int64
In [39]: df.cumsum() # ③
Out[39]: a b c d
0 0 1 2 3
1 4 6 8 10
2 12 15 18 21
3 24 28 32 36
In [40]: df + df # ④
Out[40]: a b c d
0 0 2 4 6
1 8 10 12 14
2 16 18 20 22
3 24 26 28 30
In [41]: 2 * df # ⑤
Out[41]: a b c d
0 0 2 4 6
1 8 10 12 14
2 16 18 20 22
3 24 26 28 30
In [42]: np.sum(df) # ⑥
Out[42]: a 24
b 28
c 32
d 36
dtype: int64
In [43]: df.__sizeof__() # ⑦
Out[43]: 208
①
一个属性。
②
一个方法(聚合)。
③
一个方法(无聚合)。
④
应用+运算符(加法)。
⑤
应用*运算符(乘法)。
⑥
应用NumPy通用函数np.sum()。
⑦
调用特殊方法__sizeof__()以获取以字节为单位的内存使用情况。
Python 类的基础知识
本节涉及主要概念和具体语法,以利用 Python 中的 OOP。当前的背景是构建自定义类来模拟无法轻松、高效或适当地由现有 Python 对象类型建模的对象类型。在金融工具的示例中,只需两行代码即可创建一个新的 Python 类。
In [44]: class FinancialInstrument(object): # ①
pass # ②
In [45]: fi = FinancialInstrument() # ③
In [46]: type(fi) # ④
Out[46]: __main__.FinancialInstrument
In [47]: fi # ④
Out[47]: <__main__.FinancialInstrument at 0x10a21c828>
In [48]: fi.__str__() # ⑤
Out[48]: '<__main__.FinancialInstrument object at 0x10a21c828>'
In [49]: fi.price = 100 # ⑥
In [50]: fi.price # ⑥
Out[50]: 100
①
类定义语句。²
②
一些代码;这里只是pass关键字。
③
一个名为fi的类的新实例。
④
每个 Python 对象都带有某些特殊属性和方法(来自object);这里调用了用于检索字符串表示的特殊方法。
⑤
所谓的数据属性 —— 与常规属性相对 —— 可以为每个对象即时定义。
⑥
一个重要的特殊方法是__init__,它在每次实例化对象时被调用。它以对象自身(按照惯例为self)和可能的多个其他参数作为参数。除了实例属性之外
In [51]: class FinancialInstrument(object):
author = 'Yves Hilpisch' # ①
def __init__(self, symbol, price): # ②
self.symbol = symbol # ③
self.price = price # ③
In [52]: FinancialInstrument.author # ①
Out[52]: 'Yves Hilpisch'
In [53]: aapl = FinancialInstrument('AAPL', 100) # ④
In [54]: aapl.symbol # ⑤
Out[54]: 'AAPL'
In [55]: aapl.author # ⑥
Out[55]: 'Yves Hilpisch'
In [56]: aapl.price = 105 # ⑦
In [57]: aapl.price # ⑦
Out[57]: 105
①
类属性的定义(=每个实例都继承的)。
②
在初始化期间调用特殊方法__init__。
③
实例属性的定义(=每个实例都是个别的)。
④
一个名为fi的类的新实例。
⑤
访问实例属性。
⑥
访问类属性。
⑦
更改实例属性的值。
金融工具的价格经常变动,金融工具的符号可能不会变动。为了向类定义引入封装,可以定义两个方法get_price()和set_price()。接下来的代码还额外继承了之前的类定义(不再继承自object)。
In [58]: class FinancialInstrument(FinancialInstrument): # ①
def get_price(self): # ②
return self.price # ②
def set_price(self, price): # ③
self.price = price # ④
In [59]: fi = FinancialInstrument('AAPL', 100) # ⑤
In [60]: fi.get_price() # ⑥
Out[60]: 100
In [61]: fi.set_price(105) # ⑦
In [62]: fi.get_price() # ⑥
Out[62]: 105
In [63]: fi.price # ⑧
Out[63]: 105
①
通过从上一个版本继承的方式进行类定义。
②
定义get_price方法。
③
定义set_price方法……
④
……并根据参数值更新实例属性值。
⑤
基于新的类定义创建一个名为fi的新实例。
⑥
调用get_price()方法来读取实例属性值。
⑦
通过set_price()更新实例属性值。
⑧
直接访问实例属性。
封装通常的目标是隐藏用户对类的操作中的数据。添加相应的方法,有时称为getter和setter方法,是实现此目标的一部分。然而,这并不阻止用户直接访问和操作实例属性。这就是私有实例属性发挥作用的地方。它们由两个前导下划线定义。
In [64]: class FinancialInstrument(object):
def __init__(self, symbol, price):
self.symbol = symbol
self.__price = price # ①
def get_price(self):
return self.__price
def set_price(self, price):
self.__price = price
In [65]: fi = FinancialInstrument('AAPL', 100)
In [66]: fi.get_price() # ②
Out[66]: 100
In [67]: fi.__price # ③
----------------------------------------
AttributeErrorTraceback (most recent call last)
<ipython-input-67-74c0dc05c9ae> in <module>()
----> 1 fi.__price # ③
AttributeError: 'FinancialInstrument' object has no attribute '__price'
In [68]: fi._FinancialInstrument__price # ④
Out[68]: 100
In [69]: fi._FinancialInstrument__price = 105 # ④
In [70]: fi.set_price(100) # ⑤
①
价格被定义为私有实例属性。
②
方法get_price()返回其值。
③
尝试直接访问属性会引发错误。
④
通过在类名前添加单个下划线,仍然可以直接访问和操作。
⑤
将价格恢复到其原始值。
注意
尽管封装基本上可以通过私有实例属性和处理它们的方法来实现 Python 类,但无法完全强制隐藏数据不让用户访问。从这个意义上说,这更像是 Python 中的一种工程原则,而不是 Python 类的技术特性。
考虑另一个模拟金融工具投资组合头寸的类。通过两个类,聚合的概念很容易说明。PortfolioPosition类的一个实例将FinancialInstrument类的一个实例作为属性值。添加一个实例属性,比如position_size,然后可以计算出例如头寸价值。
In [71]: class PortfolioPosition(object):
def __init__(self, financial_instrument, position_size):
self.position = financial_instrument # ①
self.__position_size = position_size # ②
def get_position_size(self):
return self.__position_size
def update_position_size(self, position_size):
self.__position_size = position_size
def get_position_value(self):
return self.__position_size * \
self.position.get_price() # ③
In [72]: pp = PortfolioPosition(fi, 10)
In [73]: pp.get_position_size()
Out[73]: 10
In [74]: pp.get_position_value() # ③
Out[74]: 1000
In [75]: pp.position.get_price() # ④
Out[75]: 100
In [76]: pp.position.set_price(105) # ⑤
In [77]: pp.get_position_value() # ⑥
Out[77]: 1050
①
基于FinancialInstrument类的实例的实例属性。
②
PortfolioPosition类的私有实例属性。
③
根据属性计算位置值。
④
附加到实例属性对象的方法可以直接访问(也可能被隐藏)。
⑤
更新金融工具的价格。
⑥
根据更新后的价格计算新位置值。
Python 数据模型
前一节的示例已经突出了所谓的 Python 数据或对象模型的一些方面(参见https://docs.python.org/3/reference/datamodel.html)。Python 数据模型允许设计与 Python 基本语言构造一致交互的类。除其他外,它支持(参见 Ramalho(2015),第 4 页)以下任务和结构:
-
迭代
-
集合处理
-
属性访问
-
运算符重载
-
函数和方法调用
-
对象创建和销毁
-
字符串表示(例如,用于打印)
-
受管理的上下文(即
with块)。
由于 Python 数据模型非常重要,本节专门介绍了一个示例,探讨了其中的几个方面。示例可在 Ramalho(2015)的书中找到,并进行了微调。它实现了一个一维,三元素向量的类(想象一下欧几里德空间中的向量)。首先,特殊方法__init__:
In [78]: class Vector(object):
def __init__(self, x=0, y=0, z=0): # ①
self.x = x # ①
self.y = y # ①
self.z = z # ①
In [79]: v = Vector(1, 2, 3) # ②
In [80]: v # ③
Out[80]: <__main__.Vector at 0x10a245d68>
①
三个预初始化的实例属性(想象成三维空间)。
②
名为v的类的新实例。
③
默认字符串表示。
特殊方法__str__允许定义自定义字符串表示。
In [81]: class Vector(Vector):
def __repr__(self):
return 'Vector(%r, %r, %r)' % (self.x, self.y, self.z)
In [82]: v = Vector(1, 2, 3)
In [83]: v # ①
Out[83]: Vector(1, 2, 3)
In [84]: print(v) # ①
Vector(1, 2, 3)
①
新的字符串表示。
abs()和bool()是两个标准的 Python 函数,它们在Vector类上的行为可以通过特殊方法__abs__和__bool__来定义。
In [85]: class Vector(Vector):
def __abs__(self):
return (self.x ** 2 + self.y ** 2 +
self.z ** 2) ** 0.5 # ①
def __bool__(self):
return bool(abs(self))
In [86]: v = Vector(1, 2, -1) # ②
In [87]: abs(v)
Out[87]: 2.449489742783178
In [88]: bool(v)
Out[88]: True
In [89]: v = Vector() # ③
In [90]: v # ③
Out[90]: Vector(0, 0, 0)
In [91]: abs(v)
Out[91]: 0.0
In [92]: bool(v)
Out[92]: False
①
返回给定三个属性值的欧几里德范数。
②
具有非零属性值的新Vector对象。
③
仅具有零属性值的新Vector对象。
如多次显示的那样,+和*运算符几乎可以应用于任何 Python 对象。其行为是通过特殊方法__add__和__mul__定义的。
In [93]: class Vector(Vector):
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
z = self.z + other.z
return Vector(x, y, z) # ①
def __mul__(self, scalar):
return Vector(self.x * scalar,
self.y * scalar,
self.z * scalar) # ①
In [94]: v = Vector(1, 2, 3)
In [95]: v + Vector(2, 3, 4)
Out[95]: Vector(3, 5, 7)
In [96]: v * 2
Out[96]: Vector(2, 4, 6)
①
在这种情况下,两个特殊方法都返回自己的类型对象。
另一个标准的 Python 函数是 len(),它给出对象的长度,即元素的数量。当在对象上调用时,此函数访问特殊方法 __len__。另一方面,特殊方法 __getitem__ 使通过方括号表示法进行索引成为可能。
In [97]: class Vector(Vector):
def __len__(self):
return 3 # ①
def __getitem__(self, i):
if i in [0, -3]: return self.x
elif i in [1, -2]: return self.y
elif i in [2, -1]: return self.z
else: raise IndexError('Index out of range.')
In [98]: v = Vector(1, 2, 3)
In [99]: len(v)
Out[99]: 3
In [100]: v[0]
Out[100]: 1
In [101]: v[-2]
Out[101]: 2
In [102]: v[3]
----------------------------------------
IndexErrorTraceback (most recent call last)
<ipython-input-102-0f5531c4b93d> in <module>()
----> 1 v[3]
<ipython-input-97-eef2cdc22510> in __getitem__(self, i)
7 elif i in [1, -2]: return self.y
8 elif i in [2, -1]: return self.z
----> 9 else: raise IndexError('Index out of range.')
IndexError: Index out of range.
①
Vector 类的所有实例都有长度为三。
最后,特殊方法 __iter__ 定义了对对象元素进行迭代的行为。定义了此操作的对象称为可迭代的。例如,所有集合和容器都是可迭代的。
In [103]: class Vector(Vector):
def __iter__(self):
for i in range(len(self)):
yield self[i]
In [104]: v = Vector(1, 2, 3)
In [105]: for i in range(3): # ①
print(v[i]) # ①
1
2
3
In [106]: for coordinate in v: # ②
print(coordinate) # ②
1
2
3
①
使用索引值进行间接迭代(通过__getitem__)。
②
对类实例进行直接迭代(使用__iter__)。
提示
Python 数据模型允许定义与标准 Python 操作符、函数等无缝交互的 Python 类。这使得 Python 成为一种相当灵活的编程语言,可以轻松地通过新类和对象类型进行增强。
总之,子节 “向量类” 在单个代码块中提供了 Vector 类的定义。
结论
本章从理论上和基于 Python 示例介绍了面向对象编程(OOP)的概念和方法。OOP 是 Python 中使用的主要编程范式之一。它不仅允许建模和实现相当复杂的应用程序,还允许创建自定义对象,这些对象由于灵活的 Python 数据模型,与标准 Python 对象表现得相似。尽管有许多批评者反对 OOP,但可以肯定地说,当达到一定复杂程度时,它为 Python 程序员和量化人员提供了强大的手段和工具。在 [Link to Come] 中讨论和展示的衍生定价包呈现了这样一个情况,其中 OOP 似乎是唯一合理的编程范式,以处理固有的复杂性和对抽象的需求。
更多资源
关于面向对象编程以及特别是 Python 编程和 Python OOP 的一般和宝贵的在线资源:
有关 Python 面向对象编程(OOP)和 Python 数据模型的书籍资源:
- Ramalho, Luciano(2016):流畅的 Python。 O’Reilly,北京等。
Python 代码
向量类
In [107]: class Vector(object):
def __init__(self, x=0, y=0, z=0):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return 'Vector(%r, %r, %r)' % (self.x, self.y, self.z)
def __abs__(self):
return (self.x ** 2 + self.y ** 2 + self.z ** 2) ** 0.5
def __bool__(self):
return bool(abs(self))
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
z = self.z + other.z
return Vector(x, y, z)
def __mul__(self, scalar):
return Vector(self.x * scalar,
self.y * scalar,
self.z * scalar)
def __len__(self):
return 3
def __getitem__(self, i):
if i in [0, -3]: return self.x
elif i in [1, -2]: return self.y
elif i in [2, -1]: return self.z
else: raise IndexError('Index out of range.')
def __iter__(self):
for i in range(len(self)):
yield self[i]
¹ Python 中的特殊属性和方法以双下划线开头和结尾,例如 __XYZ__。
² 类名采用驼峰命名法是推荐的方式。然而,如果没有歧义,也可以采用小写命名,比如financial_instrument。
第七章:数据可视化
使用图片。一图胜千言。
阿瑟·布里斯班(1911 年)
本章介绍了matplotlib和plotly库的基本可视化能力。
尽管有许多其他可用的可视化库,但matplotlib已经确立了自己作为基准,并且在许多情况下是一个强大而可靠的可视化工具。在标准绘图方面易于使用,在更复杂的绘图和定制方面灵活。此外,它与NumPy和pandas及它们提供的数据结构紧密集成。
matplotlib仅允许以位图形式(例如 PNG 或 JPG 格式)生成图。另一方面,现代网络技术允许基于数据驱动文档(D3.js)标准创建漂亮的交互式图表,例如,可以缩放以更详细地检查某些区域。一个非常方便的库,可以使用 Python 创建这样的 D3.js 图表,是plotly。一个小的附加库,称为Cufflinks,将plotly与pandas的DataFrame对象紧密集成,可以创建最受欢迎的金融图表(如蜡烛图)
本章主要涵盖以下主题:
“静态 2D 绘图”
本节介绍了matplotlib,并呈现了一些典型的 2D 绘图,从最简单的到具有两个比例尺或不同子图的更高级的绘图。
“静态 3D 绘图”
基于matplotlib,介绍了一些在特定金融应用中有用的 3D 绘图。
“交互式 2D 绘图”
本节介绍了plotly和Cufflinks,用于创建交互式 2D 绘图。利用Cufflinks的QuantFigure功能,本节还涉及典型的金融绘图,例如在技术股票分析中使用的绘图。
本章无法全面涵盖使用Python、matplotlib或plotly进行数据可视化的所有方面,但它提供了这些包在金融领域的基本和重要功能的一些示例。其他示例也可以在后面的章节中找到。例如,第八章更深入地介绍了如何使用pandas库可视化金融时间序列数据。
静态 2D 绘图
在创建样本数据并开始绘图之前,首先进行一些导入和自定义:
In [1]: import matplotlib as mpl # ①
In [2]: mpl.__version__ # ②
Out[2]: '2.0.2'
In [3]: import matplotlib.pyplot as plt # ④
In [4]: plt.style.use('seaborn') # ⑤
In [5]: mpl.rcParams['font.family'] = 'serif' # ③
In [6]: %matplotlib inline
①
使用常见缩写mpl导入了matplotlib。
②
使用的matplotlib版本。
③
将所有图的字体设置为serif。
④
使用常见缩写plt导入了主要的绘图(子)包。
⑤
将绘图样式设置为seaborn(请参阅,例如,此处的概述)。
一维数据集
在接下来的所有内容中,我们将绘制存储在NumPy的ndarray对象或pandas的DataFrame对象中的数据。然而,matplotlib当然也能够绘制存储在不同Python格式中的数据,比如list对象。最基本但相当强大的绘图函数是plt.plot()。原则上,它需要两组数字:
-
x值:包含x坐标(横坐标值)的列表或数组 -
y值:包含y坐标(纵坐标值)的列表或数组
提供的x和y值的数量必须相匹配,当然了。考虑下面的代码,其输出如图 7-1 所示。
In [7]: import numpy as np
In [8]: np.random.seed(1000) # ①
In [9]: y = np.random.standard_normal(20) # ②
In [10]: x = np.arange(len(y)) # ③
plt.plot(x, y); # ④
# plt.savefig('../../images/ch07/mpl_01')
①
为了可重复性,设置随机数生成器的种子。
②
绘制随机数(y 值)。
③
固定整数(x 值)。
④
使用x和y对象调用plt.plot()函数。

图 7-1. 绘制给定的 x 和 y 值
plt.plot()注意到当您传递一个ndarray对象时。在这种情况下,无需提供x值的“额外”信息。如果您只提供y值,则plot将索引值视为相应的x值。因此,以下单行代码生成完全相同的输出(参见图 7-2):
In [11]: plt.plot(y);
# plt.savefig('../../images/ch07/mpl_02')

图 7-2. 绘制给定的ndarray对象的数据
NumPy 数组和 matplotlib
您可以简单地将NumPy的ndarray对象传递给matplotlib函数。matplotlib能够解释数据结构以简化绘图。但是,请注意不要传递过大和/或复杂的数组。
由于大多数ndarray方法再次返回一个ndarray对象,因此您还可以通过附加方法(甚至在某些情况下可以是多个方法)来传递您的对象。通过在样本数据上调用cumsum()方法,我们得到了这些数据的累积和,正如预期的那样,得到了不同的输出(参见图 7-3):
In [12]: plt.plot(y.cumsum());
# plt.savefig('../../images/ch07/mpl_03')

图 7-3. 绘制给定一个带有附加方法的ndarray对象
通常,默认的绘图样式不能满足报告、出版物等的典型要求。例如,您可能希望自定义使用的字体(例如,与LaTeX字体兼容),在轴上标记标签,或者绘制网格以提高可读性。这就是绘图样式发挥作用的地方(见上文)。此外,matplotlib提供了大量函数来自定义绘图样式。有些函数很容易访问;对于其他一些函数,需要深入挖掘。例如,很容易访问的是那些操作轴的函数以及与网格和标签相关的函数(参见图 7-4):
In [13]: plt.plot(y.cumsum())
plt.grid(False); # ①
# plt.savefig('../../images/ch07/mpl_04')
①
关闭网格。

图 7-4。没有网格的图
plt.axis()的其他选项在表 7-1 中给出,其中大多数必须作为string对象传递。
表 7-1。plt.axis()的选项
| 参数 | 描述 |
|---|---|
| 空 | 返回当前轴限制 |
off |
关闭轴线和标签 |
equal |
导致等比例缩放 |
scaled |
通过尺寸变化实现等比例缩放 |
tight |
使所有数据可见(紧缩限制) |
image |
使所有数据可见(带有数据限制) |
[xmin, xmax, ymin, ymax] |
设置给定(列表的)值的限制 |
此外,您可以直接使用plt.xlim()和plt.ylim()设置每个轴的最小和最大值。以下代码提供了一个示例,其输出显示在图 7-5 中:
In [14]: plt.plot(y.cumsum())
plt.xlim(-1, 20)
plt.ylim(np.min(y.cumsum()) - 1,
np.max(y.cumsum()) + 1);
# plt.savefig('../../images/ch07/mpl_05')

图 7-5。带有自定义轴限制的图
为了更好地可读性,图表通常包含许多标签,例如标题和描述x和y值性质的标签。这些分别通过函数plt.title、plt.xlabel和plt.ylabel添加。默认情况下,plot绘制连续线条,即使提供了离散数据点。通过选择不同的样式选项来绘制离散点。图 7-6 叠加了(红色)点和(蓝色)线,线宽为 1.5 点:
In [15]: plt.figure(figsize=(10, 6)) # ①
plt.plot(y.cumsum(), 'b', lw=1.5) # ②
plt.plot(y.cumsum(), 'ro') # ③
plt.xlabel('index') # ④
plt.ylabel('value') # ⑤
plt.title('A Simple Plot'); # ⑥
# plt.savefig('../../images/ch07/mpl_06')
①
增加图的大小。
②
将数据绘制为蓝色线条,线宽为 1.5 点。
③
将数据绘制为红色(粗)点。
④
在 x 轴上放置一个标签。
⑤
在 y 轴上放置一个标签。
⑥
放置一个标题。

图 7-6。具有典型标签的图
默认情况下,plt.plot()支持表 7-2 中的颜色缩写。
表 7-2。标准颜色缩写
| 字符 | 颜色 |
|---|---|
b |
蓝色 |
g |
绿色 |
r |
红色 |
c |
青色 |
m |
紫红色 |
y |
黄色 |
k |
黑色 |
w |
白色 |
在线和/或点样式方面,plt.plot()支持表 7-3 中列出的字符。
表 7-3. 标准样式字符
| 字符 | 符号 |
|---|---|
- |
实线型 |
-- |
虚线型 |
-. |
短划线-点线型 |
: |
点线型 |
. |
点标记 |
, |
像素标记 |
o |
圆形标记 |
v |
向下三角形标记 |
0 |
向上三角形标记 |
< |
向左三角形标记 |
> |
向右三角形标记 |
1 |
向下三角形标记 |
2 |
向上三角形标记 |
3 |
向左三角形标记 |
4 |
向右三角形标记 |
s |
正方形标记 |
p |
五边形标记 |
0 |
星形标记 |
h |
六边形 1 标记 |
H |
六边形 2 标记 |
0 |
加号标记 |
x |
X 标记 |
D |
菱形标记 |
d |
窄菱形标记 |
| `pass:[ | ]` |
| 垂直线标记 | 0 |
任何颜色缩写都可以与任何样式字符组合。通过这种方式,您可以确保不同的数据集易于区分。正如我们将看到的,绘图样式也将反映在图例中。
二维数据集
绘制一维数据可以被视为一种特殊情况。一般来说,数据集将由多个单独的数据子集组成。与 matplotlib 一维数据一样,处理这样的数据集遵循相同的规则。但是,在这种情况下可能会出现一些额外的问题。例如,两个数据集的缩放可能有如此之大的不同,以至于不能使用相同的 y 轴和/或 x 轴缩放绘制它们。另一个问题可能是您可能希望以不同的方式可视化两个不同的数据集,例如,通过线图绘制一个数据集,通过条形图绘制另一个数据集。
以下代码生成一个具有 20×2 形状的标准正态分布(伪随机)数字的NumPy ndarray对象的二维样本数据集。对这个数组,调用cumsum()方法来计算样本数据沿轴 0(即第一个维度)的累积和:
In [16]: y = np.random.standard_normal((20, 2)).cumsum(axis=0)
一般来说,您也可以将这样的二维数组传递给 plt.plot。然后,它将自动解释包含的数据为单独的数据集(沿着轴 1,即第二个维度)。相应的图示显示在图 7-7 中:
In [17]: plt.figure(figsize=(10, 6))
plt.plot(y, lw=1.5)
plt.plot(y, 'ro')
plt.xlabel('index')
plt.ylabel('value')
plt.title('A Simple Plot');
# plt.savefig('../../images/ch07/mpl_07')

图 7-7. 带有两个数据集的图
在这种情况下,进一步的注释可能有助于更好地阅读图表。您可以为每个数据集添加单独的标签,并在图例中列出它们。 plt.legend() 接受不同的位置参数。0 代表最佳位置,意味着图例尽可能少地遮挡数据。图 7-8 展示了两个数据集的图表,这次有了图例。在生成的代码中,我们现在不再将 ndarray 对象作为一个整体传递,而是分别访问两个数据子集(y[:, 0] 和 y[:, 0]),这样可以为它们附加单独的标签:
In [18]: plt.figure(figsize=(10, 6))
plt.plot(y[:, 0], lw=1.5, label='1st') # ①
plt.plot(y[:, 1], lw=1.5, label='2nd') # ①
plt.plot(y, 'ro')
plt.legend(loc=0) # ②
plt.xlabel('index')
plt.ylabel('value')
plt.title('A Simple Plot');
# plt.savefig('../../images/ch07/mpl_08')
①
为数据子集定义标签。
②
将图例放在最佳位置。
进一步的 plt.legend() 位置选项包括 表 7-4 中介绍的选项。
表 7-4. plt.legend() 的选项
| 位置 | 描述 |
|---|---|
| 空 | 自动 |
0 |
最佳位置 |
1 |
右上角 |
2 |
左上角 |
3 |
左下角 |
4 |
右下角 |
5 |
右 |
6 |
左中 |
7 |
右中 |
8 |
底部中心 |
9 |
上部中心 |
10 |
中心 |

图 7-8. 带标记数据集的图表
具有相似缩放的多个数据集,例如相同财务风险因素的模拟路径,可以使用单个 y 轴绘制。然而,通常数据集显示的缩放相差较大,并且使用单个 y 轴绘制此类数据通常会导致视觉信息的严重丢失。为了说明效果,我们将两个数据子集中的第一个缩放因子放大了 100 倍,并再次绘制数据(参见 图 7-9):
In [19]: y[:, 0] = y[:, 0] * 100 # ①
In [20]: plt.figure(figsize=(10, 6))
plt.plot(y[:, 0], lw=1.5, label='1st')
plt.plot(y[:, 1], lw=1.5, label='2nd')
plt.plot(y, 'ro')
plt.legend(loc=0)
plt.xlabel('index')
plt.ylabel('value')
plt.title('A Simple Plot');
# plt.savefig('../../images/ch07/mpl_09')
①
重新调整第一个数据子集的比例。

图 7-9. 具有两个不同缩放数据集的图表
检查 图 7-9 发现,第一个数据集仍然“视觉可读”,而第二个数据集现在看起来像是直线,因为 y 轴的新缩放。在某种意义上,第二个数据集的信息现在“视觉上丢失了”。解决这个问题有两种基本方法:
-
使用两个 y 轴(左/右)
-
使用两个子图(上/下,左/右)
让我们先将第二个 y 轴引入图表中。图 7-10 现在有了两个不同的 y 轴。左侧的 y 轴用于第一个数据集,而右侧的 y 轴用于第二个数据集。因此,也有了两个图例:
In [21]: fig, ax1 = plt.subplots() # ①
plt.plot(y[:, 0], 'b', lw=1.5, label='1st')
plt.plot(y[:, 0], 'ro')
plt.legend(loc=8)
plt.xlabel('index')
plt.ylabel('value 1st')
plt.title('A Simple Plot')
ax2 = ax1.twinx() # ②
plt.plot(y[:, 1], 'g', lw=1.5, label='2nd')
plt.plot(y[:, 1], 'ro')
plt.legend(loc=0)
plt.ylabel('value 2nd');
# plt.savefig('../../images/ch07/mpl_10')
①
定义 figure 和 axis 对象。
②
创建共享 x 轴的第二个 axis 对象。

图 7-10. 具有两个数据集和两个 y 轴的图表
关键代码行是帮助管理坐标轴的代码行。这些是接下来的代码行:
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
通过使用 plt.subplots() 函数,我们直接访问基础绘图对象(图形、子图等)。例如,它允许生成一个共享 x 轴的第二个子图。在图 7-10 中,我们实际上有两个子图叠加在一起。
接下来,考虑两个分离子图的情况。这个选项给予了更多处理两个数据集的自由,就像图 7-11 所示:
In [22]: plt.figure(figsize=(10, 6))
plt.subplot(211) # ①
plt.plot(y[:, 0], lw=1.5, label='1st')
plt.plot(y[:, 0], 'ro')
plt.legend(loc=0)
plt.ylabel('value')
plt.title('A Simple Plot')
plt.subplot(212) # ②
plt.plot(y[:, 1], 'g', lw=1.5, label='2nd')
plt.plot(y[:, 1], 'ro')
plt.legend(loc=0)
plt.xlabel('index')
plt.ylabel('value');
# plt.savefig('../../images/ch07/mpl_11')
①
定义了上方子图 1。
②
定义了下方子图 2。

图 7-11. 具有两个子图的绘图
matplotlib figure 对象中子图的放置是通过使用特殊的坐标系统来完成的。plt.subplot() 接受三个整数作为参数,分别为 numrows、numcols 和 fignum(用逗号分隔或不分隔)。numrows 指定行的数量,numcols 指定列的数量,而 fignum 指定子图的数量,从 1 开始,以 numrows * numcols 结束。例如,具有九个等大小子图的图形将具有 numrows=3,numcols=3 和 fignum=1,2,...,9。右下方的子图将具有以下“坐标”:plt.subplot(3, 3, 9)。
有时,选择两种不同的图表类型来可视化这样的数据可能是必要的或者是期望的。通过子图的方法,您可以自由组合 matplotlib 提供的任意类型的图表。1 图 7-12 结合了线条/点图和柱状图:
In [23]: plt.figure(figsize=(10, 6))
plt.subplot(121)
plt.plot(y[:, 0], lw=1.5, label='1st')
plt.plot(y[:, 0], 'ro')
plt.legend(loc=0)
plt.xlabel('index')
plt.ylabel('value')
plt.title('1st Data Set')
plt.subplot(122)
plt.bar(np.arange(len(y)), y[:, 1], width=0.5,
color='g', label='2nd') # ①
plt.legend(loc=0)
plt.xlabel('index')
plt.title('2nd Data Set');
# plt.savefig('../../images/ch07/mpl_12')
①
创建一个 bar 子图。

图 7-12. 将线条/点子图与柱状子图组合的绘图
其他绘图样式
在二维绘图方面,线条和点图可能是金融中最重要的图表之一;这是因为许多数据集包含时间序列数据,通常通过这些图表进行可视化。第八章详细讨论了金融时间序列数据。然而,目前这一节还是采用了一个随机数的二维数据集,并且展示了一些备用的、对于金融应用有用的可视化方法。
第一种是散点图,其中一个数据集的值作为另一个数据集的x值。图 7-13 展示了这样一个图。例如,此类图用于绘制一个金融时间序列的回报与另一个金融时间序列的回报。对于此示例,我们将使用一个新的二维数据集以及一些更多的数据:
In [24]: y = np.random.standard_normal((1000, 2)) # ①
In [25]: plt.figure(figsize=(10, 6))
plt.plot(y[:, 0], y[:, 1], 'ro') # ②
plt.xlabel('1st')
plt.ylabel('2nd')
plt.title('Scatter Plot');
# plt.savefig('../../images/ch07/mpl_13')
①
创建一个包含随机数的较大数据集。
②
通过 plt.plot() 函数绘制散点图。

图 7-13. 通过 plot 函数绘制散点图
matplotlib 还提供了一个特定的函数来生成散点图。它基本上工作方式相同,但提供了一些额外的功能。图 7-14 展示了使用 plt.scatter() 函数生成的相应散点图,这次与 图 7-13 对应,:
In [26]: plt.figure(figsize=(10, 6))
plt.scatter(y[:, 0], y[:, 1], marker='o') # ①
plt.xlabel('1st')
plt.ylabel('2nd')
plt.title('Scatter Plot');
# plt.savefig('../../images/ch07/mpl_14')
①
通过 plt.scatter() 函数绘制的散点图。

图 7-14. 通过散点函数生成的散点图
例如,plt.scatter() 绘图函数允许添加第三个维度,可以通过不同的颜色来可视化,并且可以通过使用颜色条来描述。图 7-15 展示了一个散点图,其中第三个维度通过单个点的不同颜色来说明,并且有一个颜色条作为颜色的图例。为此,以下代码生成了一个具有随机数据的第三个数据集,这次是介于 0 到 10 之间的整数:
In [27]: c = np.random.randint(0, 10, len(y))
In [28]: plt.figure(figsize=(10, 6))
plt.scatter(y[:, 0], y[:, 1],
c=c, # ①
cmap='coolwarm', # ②
marker='o') # ③
plt.colorbar()
plt.xlabel('1st')
plt.ylabel('2nd')
plt.title('Scatter Plot');
# plt.savefig('../../images/ch07/mpl_15')
①
包含了第三个数据集。
②
选择了颜色映射。
③
将标记定义为粗点。

图 7-15. 具有第三维的散点图
另一种类型的图表,直方图,在金融收益的背景下也经常被使用。图 7-16 将两个数据集的频率值放在同一个图表中相邻位置:
In [29]: plt.figure(figsize=(10, 6))
plt.hist(y, label=['1st', '2nd'], bins=25) # ①
plt.legend(loc=0)
plt.xlabel('value')
plt.ylabel('frequency')
plt.title('Histogram');
# plt.savefig('../../images/ch07/mpl_16')
①
通过 plt.hist() 函数绘制直方图。

图 7-16. 两个数据集的直方图
由于直方图在金融应用中是如此重要的图表类型,让我们更近距离地看一下 plt.hist 的使用。以下示例说明了支持的参数:
plt.hist(x, bins=10, range=None, normed=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', orientation='vertical', rwidth=None, log=False, color=None, label=None, stacked=False, hold=None, **kwargs)
表 7-5 提供了 plt.hist 函数的主要参数的描述。
表 7-5. plt.hist() 的参数
| 参数 | 描述 |
|---|---|
x |
list 对象(s),ndarray 对象 |
bins |
柱子数量 |
range |
柱的下限和上限 |
normed |
规范化,使积分值为 1 |
weights |
x 中每个值的权重 |
cumulative |
每个柱包含低位柱的计数 |
histtype |
选项(字符串):bar,barstacked,step,stepfilled |
align |
选项(字符串):left,mid,right |
orientation |
选项(字符串):horizontal,vertical |
rwidth |
条柱的相对宽度 |
log |
对数刻度 |
color |
每个数据集的颜色(类似数组) |
label |
用于标签的字符串或字符串序列 |
stacked |
堆叠多个数据集 |
图 7-17 展示了类似的图表;这次,两个数据集的数据在直方图中堆叠:
In [30]: plt.figure(figsize=(10, 6))
plt.hist(y, label=['1st', '2nd'], color=['b', 'g'],
stacked=True, bins=20, alpha=0.5)
plt.legend(loc=0)
plt.xlabel('value')
plt.ylabel('frequency')
plt.title('Histogram');
# plt.savefig('../../images/ch07/mpl_17')

图 7-17. 两个数据集的堆叠直方图
另一种有用的绘图类型是箱线图。类似于直方图,箱线图既可以简明地概述数据集的特征,又可以轻松比较多个数据集。图 7-18 展示了我们数据集的这样一个图:
In [31]: fig, ax = plt.subplots(figsize=(10, 6))
plt.boxplot(y) # ①
plt.setp(ax, xticklabels=['1st', '2nd']) # ②
plt.xlabel('data set')
plt.ylabel('value')
plt.title('Boxplot');
# plt.savefig('../../images/ch07/mpl_18')
①
通过plt.boxplot()函数绘制箱线图。
②
设置各个 x 标签。
最后一个示例使用了函数plt.setp(),它为一个(组)绘图实例设置属性。例如,考虑由以下代码生成的线图:
line = plt.plot(data, 'r')
下面的代码:
plt.setp(line, linestyle='--')
将线条样式更改为“虚线”。这样,您可以在生成绘图实例(“艺术家对象”)之后轻松更改参数。

图 7-18. 两个数据集的箱线图
作为本节的最后一个示例,请考虑一个在matplotlib 画廊中也可以找到的受数学启发的绘图。它绘制了一个函数并在图形上突出显示了函数下方的区域,从下限到上限 — 换句话说,函数在下限和上限之间的积分值突出显示为一个区域。要说明的积分值是,其中,,。图 7-19 显示了结果图,并演示了matplotlib如何无缝处理数学公式的LaTeX类型设置以将其包含到绘图中。首先,函数定义,积分限制作为变量以及 x 和 y 值的数据集。
In [32]: def func(x):
return 0.5 * np.exp(x) + 1 # ①
a, b = 0.5, 1.5 # ②
x = np.linspace(0, 2) # ③
y = func(x) # ④
Ix = np.linspace(a, b) # ⑤
Iy = func(Ix) # ⑥
verts = [(a, 0)] + list(zip(Ix, Iy)) + [(b, 0)] # ⑦
①
函数定义。
②
积分限制。
③
用于绘制函数的 x 值。
④
用于绘制函数的 y 值。
⑤
积分限制内的 x 值。
⑥
积分限制内的 y 值。
⑦
包含多个表示要绘制的多边形的坐标的list对象。
其次,由于需要明确放置许多单个对象,绘图本身有点复杂。
In [33]: from matplotlib.patches import Polygon
fig, ax = plt.subplots(figsize=(10, 6))
plt.plot(x, y, 'b', linewidth=2) # ①
plt.ylim(ymin=0) # ②
poly = Polygon(verts, facecolor='0.7', edgecolor='0.5') # ③
ax.add_patch(poly) # ③
plt.text(0.5 * (a + b), 1, r'$\int_a^b f(x)\mathrm{d}x$',
horizontalalignment='center', fontsize=20) # ④
plt.figtext(0.9, 0.075, '$x$') # ⑤
plt.figtext(0.075, 0.9, '$f(x)$') # ⑤
ax.set_xticks((a, b)) # ⑥
ax.set_xticklabels(('$a$', '$b$')) # ⑥
ax.set_yticks([func(a), func(b)]) # ⑦
ax.set_yticklabels(('$f(a)$', '$f(b)$')) # ⑦
# plt.savefig('../../images/ch07/mpl_19')
Out[33]: [<matplotlib.text.Text at 0x1066af438>, <matplotlib.text.Text at 0x10669ba20>]
①
将函数值绘制为蓝线。
②
定义纵坐标轴的最小 y 值。
③
以灰色绘制多边形(积分区域)。
④
将积分公式放置在图中。
⑤
放置轴标签。
⑥
放置 x 标签。
⑦
放置 y 标签。

图 7-19。指数函数、积分区域和 LaTeX 标签
静态 3D 绘图
在金融领域,确实没有太多领域真正受益于三维可视化。然而,一个应用领域是同时显示一系列到期时间和行权价的隐含波动率的波动率曲面。接下来,代码人工生成类似于波动率曲面的图形。为此,请考虑:
-
50 到 150 之间的行权价值
-
0.5 到 2.5 年的到期时间
这提供了一个二维坐标系。NumPy的np.meshgrid()函数可以从两个一维ndarray对象生成这样的系统:
In [34]: strike = np.linspace(50, 150, 24) # ①
In [35]: ttm = np.linspace(0.5, 2.5, 24) # ②
In [36]: strike, ttm = np.meshgrid(strike, ttm) # ③
In [37]: strike[:2].round(1) # ③
Out[37]: array([[ 50. , 54.3, 58.7, 63. , 67.4, 71.7, 76.1, 80.4, 84.8,
89.1, 93.5, 97.8, 102.2, 106.5, 110.9, 115.2, 119.6, 123.9,
128.3, 132.6, 137. , 141.3, 145.7, 150. ],
[ 50. , 54.3, 58.7, 63. , 67.4, 71.7, 76.1, 80.4, 84.8,
89.1, 93.5, 97.8, 102.2, 106.5, 110.9, 115.2, 119.6, 123.9,
128.3, 132.6, 137. , 141.3, 145.7, 150. ]])
In [38]: iv = (strike - 100) ** 2 / (100 * strike) / ttm # ④
In [39]: iv[:5, :3] # ④
Out[39]: array([[1. , 0.76695652, 0.58132045],
[0.85185185, 0.65333333, 0.4951989 ],
[0.74193548, 0.56903226, 0.43130227],
[0.65714286, 0.504 , 0.38201058],
[0.58974359, 0.45230769, 0.34283001]])
①
ndarray对象中的行权价值。
②
ndarray 对象中的时间至到期值。
③
创建的两个二维ndarray对象(网格)。
④
虚拟的隐含波动率值。
由以下代码生成的图形显示在图 7-20 中:
In [40]: from mpl_toolkits.mplot3d import Axes3D # ①
fig = plt.figure(figsize=(10, 6))
ax = fig.gca(projection='3d') # ②
surf = ax.plot_surface(strike, ttm, iv, rstride=2, cstride=2,
cmap=plt.cm.coolwarm, linewidth=0.5,
antialiased=True) # ③
ax.set_xlabel('strike') # ④
ax.set_ylabel('time-to-maturity') # ⑤
ax.set_zlabel('implied volatility') # ⑥
fig.colorbar(surf, shrink=0.5, aspect=5); # ⑦
# plt.savefig('../../images/ch07/mpl_20')
①
导入相关的 3D 绘图特性。
②
为 3D 绘图设置画布。
③
创建 3D 图。
④
设置 x 标签。
⑤
设置 y 标签。
⑥
设置 z 标签。
⑦
这将创建一个色标。

图 7-20。 (虚拟) 隐含波动率的三维曲面图
表 7-6 提供了plt.plot_surface()函数可以接受的不同参数的描述。
表 7-6。plot_surface的参数
| 参数 | 描述 |
|---|---|
X, Y, Z |
数据值为 2D 数组 |
rstride |
数组行跨度(步长) |
cstride |
数组列跨度(步长) |
color |
表面补丁的颜色 |
cmap |
表面补丁的颜色映射 |
facecolors |
各个补丁的面颜色 |
norm |
用于将值映射到颜色的 Normalize 的实例 |
vmin |
要映射的最小值 |
vmax |
要映射的最大值 |
shade |
是否着色面颜色 |
与二维图相似,线型可以由单个点或如下所示的单个三角形替代。图 7-21 将相同的数据绘制为 3D 散点图,但现在还使用 view_init() 方法设置了不同的视角:
In [41]: fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111, projection='3d')
ax.view_init(30, 60) # ①
ax.scatter(strike, ttm, iv, zdir='z', s=25,
c='b', marker='^') # ②
ax.set_xlabel('strike')
ax.set_ylabel('time-to-maturity')
ax.set_zlabel('implied volatility');
# plt.savefig('../../images/ch07/mpl_21')
①
设置视角。
②
创建 3D 散点图。

图 7-21. (虚拟的) 隐含波动率的 3D 散点图
交互式 2D 绘图
matplotlib 允许创建静态位图对象或 PDF 格式的绘图。如今,有许多可用于基于 D3.js 标准创建交互式绘图的库。这些绘图可以实现缩放和悬停效果以进行数据检查,还可以轻松嵌入到网页中。
一个流行的平台和绘图库是Plotly。它专门用于数据科学的可视化,并在全球范围内广泛使用。Plotly 的主要优点是其与 Python 生态系统的紧密集成以及易于使用 — 特别是与 pandas 的 DataFrame 对象和包装器包 Cufflinks 结合使用时。
对于某些功能,需要使用 Plotly 的免费帐户,用户可以在平台本身 http://plot.ly 下注册。一旦授予凭据,它们应该被本地存储以供以后永久使用。所有相关详细信息都在使用 Plotly for Python 入门 中找到。
本节仅关注选定的方面,因为 Cufflinks 专门用于从存储在 DataFrame 对象中的数据创建交互式绘图。
基本绘图
要从 Jupyter Notebook 上下文开始,需要一些导入,并且应该打开 notebook 模式。
In [42]: import pandas as pd
In [43]: import cufflinks as cf # ①
In [44]: import plotly.offline as plyo # ②
In [45]: plyo.init_notebook_mode(connected=True) # ③
①
导入 Cufflinks。
②
导入 Plotly 的离线绘图功能。
③
打开笔记本绘图模式。
提示
使用Plotly,还可以选择在Plotly服务器上呈现绘图。然而,笔记本模式通常更快,特别是处理较大数据集时。但是,像Plotly的流图服务之类的一些功能仅通过与服务器通信才可用。
后续示例再次依赖随机数,这次存储在具有DatetimeIndex的DataFrame对象中,即作为时间序列数据。
In [46]: a = np.random.standard_normal((250, 5)).cumsum(axis=0) # ①
In [47]: index = pd.date_range('2019-1-1', # ②
freq='B', # ③
periods=len(a)) # ④
In [48]: df = pd.DataFrame(100 + 5 * a, # ⑤
columns=list('abcde'), # ⑥
index=index) # ⑦
In [49]: df.head() # ⑧
Out[49]: a b c d e
2019-01-01 109.037535 98.693865 104.474094 96.878857 100.621936
2019-01-02 107.598242 97.005738 106.789189 97.966552 100.175313
2019-01-03 101.639668 100.332253 103.183500 99.747869 107.902901
2019-01-04 98.500363 101.208283 100.966242 94.023898 104.387256
2019-01-07 93.941632 103.319168 105.674012 95.891062 86.547934
①
标准正态分布的(伪)随机数。
②
DatetimeIndex对象的开始日期。
③
频率(“business daily“)。
④
所需周期数。
⑤
原始数据进行线性转换。
⑥
将列标题作为单个字符。
⑦
DatetimeIndex对象。
⑧
前五行的数据。
Cufflinks为DataFrame类添加了一个新方法:df.iplot()。此方法在后台使用Plotly创建交互式图。本节中的代码示例都利用了将交互式图下载为静态位图的选项,然后将其嵌入到文本中。在 Jupyter Notebook 环境中,创建的绘图都是交互式的。下面代码的结果显示为<<>>。
In [50]: plyo.iplot( # ①
df.iplot(asFigure=True), # ②
# image ='png', # ③
filename='ply_01' # ④
)
①
这利用了Plotly的离线(笔记本模式)功能。
②
使用参数asFigure=True调用df.iplot()方法以允许本地绘图和嵌入。
③
image选项还提供了绘图的静态位图版本。
④
指定要保存的位图的文件名(文件类型扩展名会自动添加)。

图 7-22. 使用Plotly、pandas和Cufflinks绘制时间序列数据的线图
与matplotlib一般或pandas绘图功能一样,可用于自定义此类绘图的多个参数(参见图 7-23):
In [51]: plyo.iplot(
df[['a', 'b']].iplot(asFigure=True,
theme='polar', # ①
title='A Time Series Plot', # ②
xTitle='date', # ③
yTitle='value', # ④
mode={'a': 'markers', 'b': 'lines+markers'}, # ⑤
symbol={'a': 'dot', 'b': 'diamond'}, # ⑥
size=3.5, # ⑦
colors={'a': 'blue', 'b': 'magenta'}, # ⑧
),
# image ='png',
filename='ply_02'
)
①
选择绘图的主题(绘图样式)。
②
添加标题。
③
添加 x 标签。
④
添加 y 标签。
⑤
按列定义绘图模式(线条、标记等)。
⑥
按列定义要用作标记的符号。
⑦
为所有标记固定大小。
⑧
按列指定绘图颜色

图 7-23. DataFrame 对象的两列线图及自定义
与 matplotlib 类似,Plotly 允许使用多种不同的绘图类型。通过 Cufflinks 可用的绘图有:chart, scatter, bar, box, spread, ratio, heatmap, surface, histogram, bubble, bubble3d, scatter3d, scattergeo, ohlc, candle, pie 和 choroplet。作为与线图不同的绘图类型的示例,请考虑直方图(参见[链接即将到来]):
In [52]: plyo.iplot(
df.iplot(kind='hist', # ①
subplots=True, # ②
bins=15, # ③
asFigure=True),
# image ='png',
filename='ply_03'
)
①
指定绘图类型。
②
每列需要单独的子图。
③
设置 bins 参数(要使用的桶=要绘制的条形图)。

图 7-24. DataFrame 对象的每列直方图
金融图表
当处理金融时间序列数据时,Ploty、Cufflinks 和 pandas 的组合特别强大。Cufflinks 提供了专门的功能,用于创建典型的金融图,并添加典型的金融图表元素,例如相对强度指标(RSI),这只是一个例子。为此,创建了一个持久的 QuantFig 对象,可以像使用 Cufflinks 的 DataFrame 对象一样绘制。
此子部分使用真实的金融数据集:欧元/美元汇率的时间序列数据(来源:FXCM Forex Capital Markets Ltd.)。
In [53]: # data from FXCM Forex Capital Markets Ltd.
raw = pd.read_csv('../../source/fxcm_eur_usd_eod_data.csv',
index_col=0, parse_dates=True) # ①
In [54]: raw.info() # ②
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2820 entries, 2007-06-03 to 2017-05-31
Data columns (total 10 columns):
Time 2820 non-null object
OpenBid 2820 non-null float64
HighBid 2820 non-null float64
LowBid 2820 non-null float64
CloseBid 2820 non-null float64
OpenAsk 2820 non-null float64
HighAsk 2820 non-null float64
LowAsk 2820 non-null float64
CloseAsk 2820 non-null float64
TotalTicks 2820 non-null int64
dtypes: float64(8), int64(1), object(1)
memory usage: 242.3+ KB
In [55]: quotes = raw[['OpenAsk', 'HighAsk', 'LowAsk', 'CloseAsk']] # ③
quotes = quotes.iloc[-60:] # ④
quotes.tail() # ⑤
Out[55]: OpenAsk HighAsk LowAsk CloseAsk
Date
2017-05-27 1.11808 1.11808 1.11743 1.11788
2017-05-28 1.11788 1.11906 1.11626 1.11660
2017-05-29 1.11660 1.12064 1.11100 1.11882
2017-05-30 1.11882 1.12530 1.11651 1.12434
2017-05-31 1.12434 1.12574 1.12027 1.12133
①
从逗号分隔值(CSV)文件中读取财务数据。
②
结果 DataFrame 对象包含多列和超过 2,800 行数据。
③
从 DataFrame 对象中选择四列(开-高-低-收)。
④
仅用于可视化的少量数据行。
⑤
结果数据集 quotes 的最后五行。
在实例化期间,QuantFig 对象将 DataFrame 对象作为输入,并允许进行一些基本的自定义。然后使用 qf.iplot() 方法绘制存储在 QuantFig 对象 qf 中的数据(参见图 7-25)。
In [56]: qf = cf.QuantFig(
quotes, # ①
title='EUR/USD Exchange Rate', # ②
legend='top', # ③
name='EUR/USD' # ④
)
In [57]: plyo.iplot(
qf.iplot(asFigure=True),
# image ='png',
filename='qf_01'
)
①
DataFrame 对象传递给 QuantFig 构造函数。
②
添加图标题。
③
图例放置在图的顶部。
④
这给数据集起了个名字。

图 7-25. EUR/USD 数据的 OHLC 图
添加典型的金融图表元素,如 Bollinger 带,通过 QuantFig 对象的不同可用方法进行 (见图 7-26)。
In [58]: qf.add_bollinger_bands(periods=15, # ①
boll_std=2) # ②
In [59]: plyo.iplot(qf.iplot(asFigure=True),
# image='png',
filename='qf_02')
①
Bollinger 带的周期数。
②
用于带宽的标准偏差数。

图 7-26. EUR/USD 数据的 OHLC 图,带有 Bollinger 带
添加了某些金融指标,如 RSI,作为一个子图 (见图 7-27)。
In [60]: qf.add_rsi(periods=14, # ①
showbands=False) # ②
In [61]: plyo.iplot(
qf.iplot(asFigure=True),
# image='png',
filename='qf_03'
)
①
修复了 RSI 周期。
②
不显示上限或下限带。

图 7-27. EUR/USD 数据的 OHLC 图,带有 Bollinger 带和 RSI
结论
当涉及到 Python 中的数据可视化时,matplotlib 可以被认为是基准和工作马。它与 NumPy 和 pandas 紧密集成。基本功能易于方便地访问。然而,另一方面,matplotlib 是一个相当强大的库,具有一种相对复杂的 API。这使得在本章中无法对 matplotlib 的所有功能进行更广泛的概述。
本章介绍了在许多金融背景下有用的 matplotlib 的 2D 和 3D 绘图的基本功能。其他章节提供了如何在可视化中使用这个基本库的更多示例。
除了 matplotlib,本章还涵盖了 Plotly 与 Cufflinks 的组合。这种组合使得创建交互式 D3.js 图表成为一件方便的事情,因为通常只需在 DataFrame 对象上进行一次方法调用。所有的技术细节都在后端处理。此外,Cufflinks 通过 QuantFig 对象提供了一种创建带有流行金融指标的典型金融图表的简单方法。
进一步阅读
matplotlib 的主要资源可以在网络上找到:
-
matplotlib的主页,当然,是最好的起点:http://matplotlib.org。 -
有许多有用示例的画廊:http://matplotlib.org/gallery.html。
-
一个用于 2D 绘图的教程在这里:http://matplotlib.org/users/pyplot_tutorial.html。
-
另一个用于 3D 绘图的是:http://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html。
现在已经成为一种标准的例程去参考画廊,寻找合适的可视化示例,并从相应的示例代码开始。
Plotly 和 Cufflinks 的主要资源也可以在线找到:
-
matplotlib的主页:http://matplotlib.org -
一个 Python 入门教程:https://plot.ly/python/getting-started/
-
Cufflinks的 Github 页面:https://github.com/santosjorge/cufflinks
¹ 想了解可用的绘图类型概述,请访问matplotlib gallery。
第八章:金融时间序列
时间的唯一目的是让一切不是同时发生。
阿尔伯特·爱因斯坦
金融时间序列数据是金融领域最重要的数据类型之一。这是按日期和/或时间索引的数据。例如,随着时间的推移,股票价格代表金融时间序列数据。类似地,随时间变化的欧元/美元汇率代表金融时间序列;汇率在较短的时间间隔内报价,一系列这样的报价则是汇率的时间序列。
没有任何金融学科能够不考虑时间因素而存在。这与物理学和其他科学基本相同。在 Python 中处理时间序列数据的主要工具是pandas。pandas的原始和主要作者 Wes McKinney 在 AQR Capital Management,一家大型对冲基金的分析员时开始开发这个库。可以肯定地说pandas是从头开始设计用于处理金融时间序列数据的。
本章主要基于两个以逗号分隔值(CSV)文件形式的金融时间序列数据集。它沿以下线路进行:
“金融数据”
这一部分介绍了使用pandas处理金融时间序列数据的基础知识:数据导入,导出摘要统计信息,计算随时间变化的数据和重采样。
“滚动统计”
在金融分析中,滚动统计起着重要作用。这些统计数据通常在一个固定的时间间隔内进行计算,并在整个数据集上“滚动前进”。简单移动平均线(SMAs)是一个流行的例子。这一部分说明了pandas如何支持计算这种统计数据。
“相关性分析”
这一部分提供了一个基于标普 500 股票指数和 VIX 波动率指数的金融时间序列数据的案例研究。它为两个指数都呈现负相关的模式提供了一些支持。
“高频数据”
高频数据或者说 tick 数据在金融领域已经变得司空见惯。这一部分处理 tick 数据。pandas再次在处理这样的数据集时表现强大。
金融数据
这一部分处理的是一个以 CSV 文件形式存储的本地金融数据集。从技术上讲,这些文件只是由逗号分隔单个值的数据行结构的文本文件。在导入数据之前,首先进行一些软件包导入和自定义。
In [1]: import numpy as np
import pandas as pd
from pylab import mpl, plt
plt.style.use('seaborn')
mpl.rcParams['font.family'] = 'serif'
%matplotlib inline
数据导入
pandas提供了许多不同的函数和DataFrame方法来导入以不同格式存储的数据(CSV、SQL、Excel 等)以及将数据导出为不同的格式(详见第九章)。下面的代码使用pd.read_csv()函数从 CSV 文件中导入时间序列数据集。¹
In [2]: filename = '../../source/tr_eikon_eod_data.csv' # ①
In [3]: !head -5 $filename # ②
Date,AAPL.O,MSFT.O,INTC.O,AMZN.O,GS.N,SPY,.SPX,.VIX,EUR=,XAU=,GDX,GLD
2010-01-04,30.57282657,30.95,20.88,133.9,173.08,113.33,1132.99,20.04,1.4411,1120.0,47.71,109.8
2010-01-05,30.625683660000004,30.96,20.87,134.69,176.14,113.63,1136.52,19.35,1.4368,1118.65,48.17,109.7
2010-01-06,30.138541290000003,30.77,20.8,132.25,174.26,113.71,1137.14,19.16,1.4412,1138.5,49.34,111.51
2010-01-07,30.082827060000003,30.452,20.6,130.0,177.67,114.19,1141.69,19.06,1.4318,1131.9,49.1,110.82
In [4]: data = pd.read_csv(filename, # ③
index_col=0, # ④
parse_dates=True) # ⑤
In [5]: data.info() # ⑥
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1972 entries, 2010-01-04 to 2017-10-31
Data columns (total 12 columns):
AAPL.O 1972 non-null float64
MSFT.O 1972 non-null float64
INTC.O 1972 non-null float64
AMZN.O 1972 non-null float64
GS.N 1972 non-null float64
SPY 1972 non-null float64
.SPX 1972 non-null float64
.VIX 1972 non-null float64
EUR= 1972 non-null float64
XAU= 1972 non-null float64
GDX 1972 non-null float64
GLD 1972 non-null float64
dtypes: float64(12)
memory usage: 200.3 KB
①
指定路径和文件名。
②
显示原始数据的前五行(Linux/Mac)。
③
传递给pd.read_csv()函数的文件名。
④
这指定第一列将被处理为索引。
⑤
这另外指定索引值的类型为日期时间。
⑥
结果的DataFrame对象。
在这个阶段,金融分析师可能会通过检查数据或将其可视化(参见图 8-1)来首次查看数据。
In [6]: data.head() # ①
Out[6]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX .VIX \
Date
2010-01-04 30.572827 30.950 20.88 133.90 173.08 113.33 1132.99 20.04
2010-01-05 30.625684 30.960 20.87 134.69 176.14 113.63 1136.52 19.35
2010-01-06 30.138541 30.770 20.80 132.25 174.26 113.71 1137.14 19.16
2010-01-07 30.082827 30.452 20.60 130.00 177.67 114.19 1141.69 19.06
2010-01-08 30.282827 30.660 20.83 133.52 174.31 114.57 1144.98 18.13
EUR= XAU= GDX GLD
Date
2010-01-04 1.4411 1120.00 47.71 109.80
2010-01-05 1.4368 1118.65 48.17 109.70
2010-01-06 1.4412 1138.50 49.34 111.51
2010-01-07 1.4318 1131.90 49.10 110.82
2010-01-08 1.4412 1136.10 49.84 111.37
In [7]: data.tail() # ②
Out[7]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX .VIX \
Date
2017-10-25 156.41 78.63 40.78 972.91 241.71 255.29 2557.15 11.23
2017-10-26 157.41 78.76 41.35 972.43 241.72 255.62 2560.40 11.30
2017-10-27 163.05 83.81 44.40 1100.95 241.71 257.71 2581.07 9.80
2017-10-30 166.72 83.89 44.37 1110.85 240.89 256.75 2572.83 10.50
2017-10-31 169.04 83.18 45.49 1105.28 242.48 257.15 2575.26 10.18
EUR= XAU= GDX GLD
Date
2017-10-25 1.1812 1277.01 22.83 121.35
2017-10-26 1.1650 1266.73 22.43 120.33
2017-10-27 1.1608 1272.60 22.57 120.90
2017-10-30 1.1649 1275.86 22.76 121.13
2017-10-31 1.1644 1271.20 22.48 120.67
In [8]: data.plot(figsize=(10, 12), subplots=True) # ③
# plt.savefig('../../images/ch08/fts_01.png');
①
前五行 …
②
… 最后五行显示。
③
这通过多个子图可视化完整数据集。

图 8-1. 金融时间序列数据的线图
使用的数据来自汤森路透(TR)Eikon 数据 API。在 TR 世界中,金融工具的符号称为“路透社仪器代码”或RICs。单个RICs代表的金融工具是:
In [9]: instruments = ['Apple Stock', 'Microsoft Stock',
'Intel Stock', 'Amazon Stock', 'Goldman Sachs Stock',
'SPDR S&P 500 ETF Trust', 'S&P 500 Index',
'VIX Volatility Index', 'EUR/USD Exchange Rate',
'Gold Price', 'VanEck Vectors Gold Miners ETF',
'SPDR Gold Trust']
In [10]: for pari in zip(data.columns, instruments):
print('{:8s} | {}'.format(pari[0], pari[1]))
AAPL.O | Apple Stock
MSFT.O | Microsoft Stock
INTC.O | Intel Stock
AMZN.O | Amazon Stock
GS.N | Goldman Sachs Stock
SPY | SPDR S&P 500 ETF Trust
.SPX | S&P 500 Index
.VIX | VIX Volatility Index
EUR= | EUR/USD Exchange Rate
XAU= | Gold Price
GDX | VanEck Vectors Gold Miners ETF
GLD | SPDR Gold Trust
摘要统计
下一步,金融分析师可能会采取的步骤是查看数据集的不同摘要统计信息,以了解它的“感觉”。
In [11]: data.info() # ①
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1972 entries, 2010-01-04 to 2017-10-31
Data columns (total 12 columns):
AAPL.O 1972 non-null float64
MSFT.O 1972 non-null float64
INTC.O 1972 non-null float64
AMZN.O 1972 non-null float64
GS.N 1972 non-null float64
SPY 1972 non-null float64
.SPX 1972 non-null float64
.VIX 1972 non-null float64
EUR= 1972 non-null float64
XAU= 1972 non-null float64
GDX 1972 non-null float64
GLD 1972 non-null float64
dtypes: float64(12)
memory usage: 200.3 KB
In [12]: data.describe().round(2) # ②
Out[12]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX .VIX \
count 1972.00 1972.00 1972.00 1972.00 1972.00 1972.00 1972.00 1972.00
mean 86.53 40.59 27.70 401.15 163.61 172.84 1727.54 17.21
std 34.04 14.39 5.95 257.12 37.17 42.33 424.35 5.92
min 27.44 23.01 17.66 108.61 87.70 102.20 1022.58 9.19
25% 57.57 28.12 22.23 202.66 144.23 132.64 1325.53 13.25
50% 84.63 36.54 26.41 306.42 162.09 178.80 1783.81 15.65
75% 111.87 50.08 33.74 559.45 184.11 208.01 2080.15 19.20
max 169.04 83.89 45.49 1110.85 252.89 257.71 2581.07 48.00
EUR= XAU= GDX GLD
count 1972.00 1972.00 1972.00 1972.00
mean 1.25 1352.47 34.50 130.60
std 0.12 195.38 15.44 19.46
min 1.04 1051.36 12.47 100.50
25% 1.13 1214.56 22.22 116.77
50% 1.29 1288.82 26.59 123.90
75% 1.35 1491.98 49.77 145.43
max 1.48 1897.10 66.63 184.59
①
.info()提供有关DataFrame对象的一些元信息。
②
.describe() 提供每列有用的标准统计数据。
提示
pandas提供了许多方法来快速查看新导入的金融时间序列数据集的概述,例如.info()和.describe()。它们还允许快速检查导入过程是否按预期工作(例如,DataFrame对象是否确实具有DatetimeIndex作为索引)。
当然,也有选项来自定义要推导和显示的统计信息类型。
In [13]: data.mean() # ①
Out[13]: AAPL.O 86.530152
MSFT.O 40.586752
INTC.O 27.701411
AMZN.O 401.154006
GS.N 163.614625
SPY 172.835399
.SPX 1727.538342
.VIX 17.209498
EUR= 1.252613
XAU= 1352.471593
GDX 34.499391
GLD 130.601856
dtype: float64
In [14]: data.aggregate(min, ![2 np.mean, # ③
np.std, # ④
np.median, # ⑤
max] # ⑥
).round(2)
Out[14]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX .VIX EUR= \
min 27.44 23.01 17.66 108.61 87.70 102.20 1022.58 9.19 1.04
mean 86.53 40.59 27.70 401.15 163.61 172.84 1727.54 17.21 1.25
std 34.04 14.39 5.95 257.12 37.17 42.33 424.35 5.92 0.12
median 84.63 36.54 26.41 306.42 162.09 178.80 1783.81 15.65 1.29
max 169.04 83.89 45.49 1110.85 252.89 257.71 2581.07 48.00 1.48
XAU= GDX GLD
min 1051.36 12.47 100.50
mean 1352.47 34.50 130.60
std 195.38 15.44 19.46
median 1288.82 26.59 123.90
max 1897.10 66.63 184.59
①
每列的均值。
②
每列的最小值。
③
每列的均值。
④
每列的标准偏差。
⑤
每列的最大值。
⑥
使用.aggregate()方法还允许传递自定义函数。
随时间变化
大多数统计分析方法,例如,通常基于时间序列随时间的变化,而不是绝对值本身。有多种选项可以计算时间序列随时间的变化,包括:绝对差异、百分比变化和对数(对数)收益。
首先是绝对差异,对于这个pandas提供了一个特殊的方法。
In [15]: data.diff().head() # ①
Out[15]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX .VIX EUR= \
Date
2010-01-04 NaN NaN NaN NaN NaN NaN NaN NaN NaN
2010-01-05 0.052857 0.010 -0.01 0.79 3.06 0.30 3.53 -0.69 -0.0043
2010-01-06 -0.487142 -0.190 -0.07 -2.44 -1.88 0.08 0.62 -0.19 0.0044
2010-01-07 -0.055714 -0.318 -0.20 -2.25 3.41 0.48 4.55 -0.10 -0.0094
2010-01-08 0.200000 0.208 0.23 3.52 -3.36 0.38 3.29 -0.93 0.0094
XAU= GDX GLD
Date
2010-01-04 NaN NaN NaN
2010-01-05 -1.35 0.46 -0.10
2010-01-06 19.85 1.17 1.81
2010-01-07 -6.60 -0.24 -0.69
2010-01-08 4.20 0.74 0.55
In [16]: data.diff().mean() # ②
Out[16]: AAPL.O 0.070252
MSFT.O 0.026499
INTC.O 0.012486
AMZN.O 0.492836
GS.N 0.035211
SPY 0.072968
.SPX 0.731745
.VIX -0.005003
EUR= -0.000140
XAU= 0.076712
GDX -0.012801
GLD 0.005515
dtype: float64
①
.diff()提供了两个索引值之间的绝对变化。
②
当然,还可以应用聚合操作。
从统计学的角度来看,绝对变化不是最佳选择,因为它们依赖于时间序列数据本身的比例。因此,通常更喜欢百分比变化。以下代码推导了金融背景下的百分比变化或百分比收益(也称为:简单收益),并可视化其每列的均值(参见 图 8-2)。
In [17]: data.pct_change().round(3).head() # ①
Out[17]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX .VIX EUR= \
Date
2010-01-04 NaN NaN NaN NaN NaN NaN NaN NaN NaN
2010-01-05 0.002 0.000 -0.000 0.006 0.018 0.003 0.003 -0.034 -0.003
2010-01-06 -0.016 -0.006 -0.003 -0.018 -0.011 0.001 0.001 -0.010 0.003
2010-01-07 -0.002 -0.010 -0.010 -0.017 0.020 0.004 0.004 -0.005 -0.007
2010-01-08 0.007 0.007 0.011 0.027 -0.019 0.003 0.003 -0.049 0.007
XAU= GDX GLD
Date
2010-01-04 NaN NaN NaN
2010-01-05 -0.001 0.010 -0.001
2010-01-06 0.018 0.024 0.016
2010-01-07 -0.006 -0.005 -0.006
2010-01-08 0.004 0.015 0.005
In [18]: data.pct_change().mean().plot(kind='bar', figsize=(10, 6)); # ②
# plt.savefig('../../images/ch08/fts_02.png');
①
.pct_change()计算两个索引值之间的百分比变化。
②
结果的均值作为条形图可视化。

图 8-2. 百分比变化的均值作为条形图
作为百分比收益的替代,可以使用对数收益。在某些情况下,它们更容易处理,因此在金融背景下通常更受欢迎。² 图 8-3 展示了单个金融时间序列的累积对数收益。这种类型的绘图导致某种形式的归一化。
In [19]: rets = np.log(data / data.shift(1)) # ①
In [20]: rets.head().round(3) # ②
Out[20]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX .VIX EUR= \
Date
2010-01-04 NaN NaN NaN NaN NaN NaN NaN NaN NaN
2010-01-05 0.002 0.000 -0.000 0.006 0.018 0.003 0.003 -0.035 -0.003
2010-01-06 -0.016 -0.006 -0.003 -0.018 -0.011 0.001 0.001 -0.010 0.003
2010-01-07 -0.002 -0.010 -0.010 -0.017 0.019 0.004 0.004 -0.005 -0.007
2010-01-08 0.007 0.007 0.011 0.027 -0.019 0.003 0.003 -0.050 0.007
XAU= GDX GLD
Date
2010-01-04 NaN NaN NaN
2010-01-05 -0.001 0.010 -0.001
2010-01-06 0.018 0.024 0.016
2010-01-07 -0.006 -0.005 -0.006
2010-01-08 0.004 0.015 0.005
In [21]: rets.cumsum().apply(np.exp).plot(figsize=(10, 6)); # ③
# plt.savefig('../../images/ch08/fts_03.png');
①
这以向量化方式计算对数收益。
②
结果的子集。
③
这绘制了随时间累积的对数收益;首先调用.cumsum()方法,然后将np.exp()应用于结果。

图 8-3. 随时间累积的对数收益
重采样
对于金融时间序列数据,重采样是一项重要的操作。通常,这采取上采样的形式,意味着例如,具有每日观测的时间序列被重采样为具有每周或每月观测的时间序列。这也可能意味着将金融 Tick 数据系列重采样为一分钟间隔(也称为:柱)。
In [22]: data.resample('1w', label='right').last().head() # ①
Out[22]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX .VIX \
Date
2010-01-10 30.282827 30.66 20.83 133.52 174.31 114.57 1144.98 18.13
2010-01-17 29.418542 30.86 20.80 127.14 165.21 113.64 1136.03 17.91
2010-01-24 28.249972 28.96 19.91 121.43 154.12 109.21 1091.76 27.31
2010-01-31 27.437544 28.18 19.40 125.41 148.72 107.39 1073.87 24.62
2010-02-07 27.922829 28.02 19.47 117.39 154.16 106.66 1066.19 26.11
EUR= XAU= GDX GLD
Date
2010-01-10 1.4412 1136.10 49.84 111.37
2010-01-17 1.4382 1129.90 47.42 110.86
2010-01-24 1.4137 1092.60 43.79 107.17
2010-01-31 1.3862 1081.05 40.72 105.96
2010-02-07 1.3662 1064.95 42.41 104.68
In [23]: data.resample('1m', label='right').last().head() # ②
Out[23]: AAPL.O MSFT.O INTC.O AMZN.O GS.N SPY .SPX \
Date
2010-01-31 27.437544 28.1800 19.40 125.41 148.72 107.3900 1073.87
2010-02-28 29.231399 28.6700 20.53 118.40 156.35 110.7400 1104.49
2010-03-31 33.571395 29.2875 22.29 135.77 170.63 117.0000 1169.43
2010-04-30 37.298534 30.5350 22.84 137.10 145.20 118.8125 1186.69
2010-05-31 36.697106 25.8000 21.42 125.46 144.26 109.3690 1089.41
.VIX EUR= XAU= GDX GLD
Date
2010-01-31 24.62 1.3862 1081.05 40.72 105.960
2010-02-28 19.50 1.3625 1116.10 43.89 109.430
2010-03-31 17.59 1.3510 1112.80 44.41 108.950
2010-04-30 22.05 1.3295 1178.25 50.51 115.360
2010-05-31 32.07 1.2267 1213.81 49.86 118.881
In [24]: rets.cumsum().resample('1m', label='right').last(
).plot(figsize=(10, 6)); # ③
# plt.savefig('../../images/ch08/fts_04.png');
①
EOD 数据被重采样为
②
结果的子集。
③
这绘制了随时间累积的对数收益;首先调用.cumsum()方法,然后将np.exp()应用于结果。

图 8-4. 随时间重采样的累积对数收益(每月)
注意
在重新取样时,默认情况下,pandas采用区间的左标签(或索引值)。为了保持金融一致性,请确保使用右标签(索引值)和通常是区间中最后一个可用数据点。否则,可能会在金融分析中引入预见偏差。^(3)
滚动统计
在金融传统中,通常称为滚动统计,也称为金融指标或金融研究。这样的滚动统计是金融图表分析师和技术交易员的基本工具,例如。本节仅处理单个金融时间序列。
In [25]: sym = 'AAPL.O'
In [26]: data = pd.DataFrame(data[sym])
In [27]: data.tail()
Out[27]: AAPL.O
Date
2017-10-25 156.41
2017-10-26 157.41
2017-10-27 163.05
2017-10-30 166.72
2017-10-31 169.04
概览
使用pandas很容易得出标准滚动统计。
In [28]: window = 20 # ①
In [29]: data['min'] = data[sym].rolling(window=window).min() # ②
In [30]: data['mean'] = data[sym].rolling(window=window).mean() # ③
In [31]: data['std'] = data[sym].rolling(window=window).std() # ④
In [32]: data['median'] = data[sym].rolling(window=window).median() # ⑤
In [33]: data['max'] = data[sym].rolling(window=window).max() # ⑥
In [34]: data['ewma'] = data[sym].ewm(halflife=0.5, min_periods=window).mean() # ⑦
①
定义窗口,即要包含的索引值的数量。
②
计算滚动最小值。
③
计算滚动均值。
④
计算滚动标准差。
⑤
计算滚动中位数。
⑥
计算滚动最大值。
⑦
这将计算指数加权移动平均值,衰减以半衰期0.5来计算。
要推导出更专业的金融指标,通常需要使用其他软件包(例如,使用Cufflinks进行金融图表,参见“交互式二维绘图”)。也可以通过.apply()方法轻松地应用自定义指标。
下面的代码显示了部分结果,并可视化了计算的滚动统计的选择部分(参见图 8-5)。
In [35]: data.dropna().head()
Out[35]: AAPL.O min mean std median max \
Date
2010-02-01 27.818544 27.437544 29.580892 0.933650 29.821542 30.719969
2010-02-02 27.979972 27.437544 29.451249 0.968048 29.711113 30.719969
2010-02-03 28.461400 27.437544 29.343035 0.950665 29.685970 30.719969
2010-02-04 27.435687 27.435687 29.207892 1.021129 29.547113 30.719969
2010-02-05 27.922829 27.435687 29.099892 1.037811 29.419256 30.719969
ewma
Date
2010-02-01 27.805432
2010-02-02 27.936337
2010-02-03 28.330134
2010-02-04 27.659299
2010-02-05 27.856947
In [36]: ax = data[['min', 'mean', 'max']].iloc[-200:].plot(
figsize=(10, 6), style=['g--', 'r--', 'g--'], lw=0.8) # ①
data[sym].iloc[-200:].plot(ax=ax, lw=2.0); # ②
# plt.savefig('../../images/ch08/fts_05.png');
①
绘制最后 200 个数据行的三个滚动统计。
②
将原始时间序列数据添加到图表中。

图 8-5。最小、平均、最大值的滚动统计
技术分析示例
与基本分析相比,滚动统计是所谓的技术分析中的主要工具,基本分析侧重于财务报告和被分析股票公司的战略位置等方面。
基于技术分析的几十年历史的交易策略基于两个简单移动平均线(SMAs)。这个想法是,当短期 SMA 高于长期 SMA 时,交易员应该持有一支股票(或者一般的金融工具),当情况相反时,应该空仓。这些概念可以通过pandas和DataFrame对象的功能来精确描述。
当给定了window参数规范并且有足够的数据时,通常才会计算滚动统计。如图 8-6 所示,SMAs 时间序列仅从有足够数据的那天开始。
In [37]: data['SMA1'] = data[sym].rolling(window=42).mean() # ①
In [38]: data['SMA2'] = data[sym].rolling(window=252).mean() # ②
In [39]: data[[sym, 'SMA1', 'SMA2']].tail()
Out[39]: AAPL.O SMA1 SMA2
Date
2017-10-25 156.41 157.610952 139.862520
2017-10-26 157.41 157.514286 140.028472
2017-10-27 163.05 157.517619 140.221210
2017-10-30 166.72 157.597857 140.431528
2017-10-31 169.04 157.717857 140.651766
In [40]: data[[sym, 'SMA1', 'SMA2']].plot(figsize=(10, 6)); # ③
# plt.savefig('../../images/ch08/fts_06.png');
①
计算短期 SMA 的值。
②
计算长期 SMA 的值。
③
可视化股价数据和两条 SMAs 时间序列。

图 8-6. 苹果股价和两条简单移动平均线
在这个背景下,简单移动平均线(SMAs)仅仅是实现目标的手段。它们被用来推导出实施交易策略的定位。图 8-7 通过数值1来可视化多头头寸,数值-1来可视化空头头寸。头寸的变化(在视觉上)由表示 SMAs 时间序列的两条线的交叉触发。
In [41]: data.dropna(inplace=True) # ①
In [42]: data['positions'] = np.where(data['SMA1'] > data['SMA2'], # ②
1, # ③
-1) # ④
In [43]: ax = data[[sym, 'SMA1', 'SMA2', 'positions']].plot(figsize=(10, 6),
secondary_y='positions')
ax.get_legend().set_bbox_to_anchor((0.25, 0.85));
# plt.savefig('../../images/ch08/fts_07.png');
①
仅保留完整的数据行。
②
如果短期 SMA 值大于长期 SMA 值…
③
…买入股票(放置1)…
④
…否则卖空股票(放置-1)。

图 8-7. 苹果股价,两条简单移动平均线和头寸
在这里隐含地推导出的交易策略本质上只导致了很少的交易:只有当头寸价值变化(即发生交叉)时,才会进行交易。包括开仓和平仓交易,在总计中这将只增加到六次交易。
相关性分析
作为如何使用pandas和金融时间序列数据的进一步说明,考虑标准普尔 500 股指数和 VIX 波动率指数的情况。一个事实是,一般情况下,当标准普尔 500 上涨时,VIX 下降,反之亦然。这涉及相关性而不是因果关系。本节展示了如何为标准普尔 500 和 VIX 之间(高度)负相关的事实提供支持性统计证据。⁴
数据
数据集现在包含两个金融时间序列,都在图 8-8 中可视化。
In [44]: # EOD data from Thomson Reuters Eikon Data API
raw = pd.read_csv('../../source/tr_eikon_eod_data.csv',
index_col=0, parse_dates=True)
In [45]: data = raw[['.SPX', '.VIX']]
In [46]: data.tail()
Out[46]: .SPX .VIX
Date
2017-10-25 2557.15 11.23
2017-10-26 2560.40 11.30
2017-10-27 2581.07 9.80
2017-10-30 2572.83 10.50
2017-10-31 2575.26 10.18
In [47]: data.plot(subplots=True, figsize=(10, 6));
# plt.savefig('../../images/ch08/fts_08.png');

图 8-8. S&P 500 和 VIX 时间序列数据(不同的缩放)
当在单个图中绘制(部分)两个时间序列并进行调整缩放时,两个指数之间的负相关的事实通过简单的视觉检查就已经变得明显。
In [48]: data.loc[:'2012-12-31'].plot(secondary_y='.VIX', figsize=(10, 6));
# plt.savefig('../../images/ch08/fts_09.png');

图 8-9. S&P 500 和 VIX 时间序列数据(相同的缩放)
对数收益率
如上所述,一般的统计分析依赖于收益而不是绝对变化或甚至绝对值。因此,在进行任何进一步分析之前,首先计算对数收益。图 8-10 显示了随时间变化的对数收益的高变异性。对于两个指数都可以发现所谓的波动率集群。总的来说,股票指数波动率高的时期伴随着波动率指数的同样现象。
In [49]: rets = np.log(data / data.shift(1))
In [50]: rets.head()
Out[50]: .SPX .VIX
Date
2010-01-04 NaN NaN
2010-01-05 0.003111 -0.035038
2010-01-06 0.000545 -0.009868
2010-01-07 0.003993 -0.005233
2010-01-08 0.002878 -0.050024
In [51]: rets.dropna(inplace=True)
In [52]: rets.plot(subplots=True, figsize=(10, 6));
# plt.savefig('../../images/ch08/fts_10.png');

图 8-10。S&P 500 和 VIX 的对数收益随时间变化
在这种情况下,pandas的scatter_matrix()绘图函数非常方便用于可视化。它将两个系列的对数收益相互绘制,可以在对角线上添加直方图或核密度估计器(KDE)(请参见图 8-11)。
In [53]: pd.plotting.scatter_matrix(rets, # ①
alpha=0.2, # ②
diagonal='hist', # ③
hist_kwds={'bins': 35}, # ④
figsize=(10, 6));
# plt.savefig('../../images/ch08/fts_11.png');
①
要绘制的数据集。
②
点的不透明度参数为alpha。
③
放置在对角线上的内容;这里是列数据的直方图。
④
这些是要传递给直方图绘图函数的关键字。

图 8-11。S&P 500 和 VIX 的对数收益作为散点矩阵
OLS 回归
通过所有这些准备,普通最小二乘(OLS)回归分析很方便实现。图 8-12 显示了对数收益的散点图和通过点云的线性回归线。斜率明显为负,支持了关于两个指数之间负相关的事实。
In [54]: reg = np.polyfit(rets['.SPX'], rets['.VIX'], deg=1) # ①
In [55]: ax = rets.plot(kind='scatter', x='.SPX', y='.VIX', figsize=(10, 6)) # ②
ax.plot(rets['.SPX'], np.polyval(reg, rets['.SPX']), 'r', lw=2); # ③
# plt.savefig('../../images/ch08/fts_12.png');
①
这实现了线性 OLS 回归。
②
这将对数收益绘制为散点图…
③
… 添加了线性回归线。

图 8-12。S&P 500 和 VIX 的对数收益作为散点矩阵
相关性
最后,直接考虑相关性度量。考虑到两种度量,一种是考虑完整数据集的静态度量,另一种是显示一定时间内相关性的滚动度量。图 8-13 说明了相关性确实随时间变化,但鉴于参数设置,始终为负。这确实强有力地支持了 S&P 500 和 VIX 指数之间甚至强烈的负相关的事实。
In [56]: rets.corr() # ①
Out[56]: .SPX .VIX
.SPX 1.000000 -0.808372
.VIX -0.808372 1.000000
In [57]: ax = rets['.SPX'].rolling(window=252).corr(
rets['.VIX']).plot(figsize=(10, 6)) # ②
ax.axhline(rets.corr().iloc[0, 1], c='r'); # ③
# plt.savefig('../../images/ch08/fts_13.png');
①
整个DataFrame的相关矩阵。
②
这会绘制随时间变化的滚动相关性……
③
… 并将静态值添加到绘图中作为水平线。

图 8-13. 标普 500 和 VIX 之间的相关性(静态和滚动)
高频数据
本章介绍了使用pandas进行金融时间序列分析。金融时间序列的一个特殊情况是 tick 数据集。坦率地说,它们可以更多或更少地以与本章迄今为止使用的 EOD 数据集相同的方式处理。一般来说,使用pandas导入这些数据集也是相当快的。所使用的数据集包含 17,352 行数据(另见图 8-14)。
In [58]: %%time
# data from FXCM Forex Capital Markets Ltd.
tick = pd.read_csv('../../source/fxcm_eur_usd_tick_data.csv',
index_col=0, parse_dates=True)
CPU times: user 23 ms, sys: 3.35 ms, total: 26.4 ms
Wall time: 25.1 ms
In [59]: tick.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 17352 entries, 2017-11-10 12:00:00.007000 to 2017-11-10 14:00:00.131000
Data columns (total 2 columns):
Bid 17352 non-null float64
Ask 17352 non-null float64
dtypes: float64(2)
memory usage: 406.7 KB
In [60]: tick['Mid'] = tick.mean(axis=1) # ①
In [61]: tick['Mid'].plot(figsize=(10, 6));
# plt.savefig('../../images/ch08/fts_14.png');
①
计算每一行数据的Mid价格。

图 8-14. 欧元/美元汇率的 tick 数据
处理 tick 数据通常需要对金融时间序列数据进行重新采样。以下代码将 tick 数据重新采样为一分钟的条形数据。例如,这样的数据集(另见图 8-15)用于回测算法交易策略或实施技术分析。
In [62]: tick_resam = tick.resample(rule='1min', label='right').last()
In [63]: tick_resam.head()
Out[63]: Bid Ask Mid
2017-11-10 12:01:00 1.16406 1.16407 1.164065
2017-11-10 12:02:00 1.16396 1.16397 1.163965
2017-11-10 12:03:00 1.16416 1.16418 1.164170
2017-11-10 12:04:00 1.16417 1.16417 1.164170
2017-11-10 12:05:00 1.16425 1.16427 1.164260
In [64]: tick_resam['Mid'].plot(figsize=(10, 6));
# plt.savefig('../../images/ch08/fts_15.png');

图 8-15. 欧元/美元汇率的一分钟条形数据
结论
本章涉及金融时间序列,可能是金融领域最重要的数据类型。pandas是处理这种数据集的强大工具包,不仅可以进行高效的数据分析,而且还可以轻松进行可视化。pandas还有助于从不同来源读取此类数据集,并将此类数据集导出为不同的技术文件格式。这在随后的第九章第九章中进行了说明。
进一步阅读
本章涵盖的主题在书籍形式上有很好的参考资料:
-
McKinney, Wes (2017): Python for Data Analysis. 2nd ed., O’Reilly, 北京等地.
-
VanderPlas, Jake (2016): Python Data Science Handbook. O’Reilly, 北京等地。
¹ 该文件包含了从汤姆森路透 Eikon 数据 API 检索的不同金融工具的每日结束数据(EOD 数据)。
² 其中一个优点是随时间的可加性,这对于简单的百分比变化/回报并不成立。
³ 预见偏见 ——或者,在其最强形式下,完美预见 ——意味着在金融分析的某个点上使用的数据仅在以后才可用。结果可能是“太好了”的结果,例如,在回测交易策略时。
⁴ 其背后的一个推理是,当股指下跌时——例如在危机期间——交易量会上升,因此波动性也会上升。当股指上涨时,投资者通常会保持冷静,并且不怎么有动力进行大量交易。特别是,仅持有多头头寸的投资者会试图进一步跟进趋势。
第九章:输入/输出操作
在有数据之前进行理论推断是一个大错误。
福尔摩斯
作为一般规则,无论是在金融环境中还是在其他任何应用程序领域,大多数数据都存储在硬盘驱动器(HDDs)或其他形式的永久存储设备上,如固态硬盘(SSDs)或混合硬盘驱动器。多年来,存储容量一直在稳步增长,而存储单元的成本(例如,每兆字节)一直在稳步下降。
与此同时,存储数据的容量增长速度远远快于即使是最大型机器中可用的典型随机访问内存(RAM)。这使得不仅需要将数据存储到磁盘上以进行永久存储,而且需要通过将数据从 RAM 交换到磁盘,然后再交换回来来弥补 RAM 不足的情况。
因此,在金融应用程序和数据密集型应用程序中,输入/输出(I/O)操作通常是重要的任务。通常,它们代表了性能关键计算的瓶颈,因为 I/O 操作通常无法将数据快速地从 RAM 移动到 RAM¹,然后再从 RAM 移动到磁盘。从某种意义上说,CPU 经常由于 I/O 操作慢而“饥饿”。
尽管如今的大部分金融和企业分析工作都面临着大数据(例如,PB 级别),但单个分析任务通常使用的数据子集属于中等数据类别。微软研究的一项研究得出了结论:
我们的测量以及其他最近的工作显示,大多数现实世界的分析任务处理的输入量不超过 100GB,但流行的基础设施,如 Hadoop/MapReduce 最初是为 PB 级处理而设计的。
Appuswamy 等人(2013 年)
就频率而言,单个金融分析任务通常处理不超过几 GB 大小的数据,并且这是 Python 及其科学堆栈库(如NumPy,pandas和PyTables)的甜蜜点。这样大小的数据集也可以在内存中进行分析,利用今天的 CPU 和 GPU 通常会获得较高的速度。但是,必须将数据读入 RAM 并将结果写入磁盘,同时确保满足今天的性能要求。
本章涉及以下主题:
“使用 Python 进行基本 I/O”
Python 具有内置函数,可以对任何对象进行序列化并将其存储到磁盘上,然后从磁盘中读取到 RAM 中;除此之外,在处理文本文件和SQL数据库时,Python 也很强大。NumPy还提供了专用函数,用于快速二进制存储和检索ndarray对象。
“使用 pandas 进行 I/O”
pandas库提供了丰富的便利函数和方法,用于读取存储在不同格式中的数据(例如,CSV,JSON)并将数据写入不同格式的文件。
“使用 PyTables 进行快速 I/O”
PyTables 使用具有分层数据库结构和二进制存储的 HDF5 标准来实现对大数据集的快速 I/O 操作;速度通常仅受使用的硬件限制。
“TsTables 的 I/O”
TsTables 是一个构建在 PyTables 之上的包,允许快速存储和检索时间序列数据。
Python 的基本 I/O
Python 本身具有多种 I/O 功能,有些针对性能进行了优化,而其他一些则更注重灵活性。然而,总的来说,它们既可以在交互式环境下使用,也可以在生产环境中轻松应用。
将对象写入磁盘
以供以后使用、文档化或与他人共享,有人可能想要将 Python 对象存储在磁盘上。一个选项是使用 pickle 模块。该模块可以序列化大多数 Python 对象。序列化 指的是将对象(层次结构)转换为字节流;反序列化 是相反的操作。
通常情况下,首先进行一些与绘图相关的导入和自定义:
In [1]: from pylab import plt, mpl
plt.style.use('seaborn')
mpl.rcParams['font.family'] = 'serif'
%matplotlib inline
接下来的示例使用(伪)随机数据,这次存储在 list 对象中:
In [2]: import pickle # ①
import numpy as np
from random import gauss # ②
In [3]: a = [gauss(1.5, 2) for i in range(1000000)] # ③
In [4]: path = '/Users/yves/Documents/Temp/data/' # ④
In [5]: pkl_file = open(path + 'data.pkl', 'wb') # ⑤
①
从标准库导入 pickle 模块。
②
导入 gauss 以生成正态分布的随机数。
③
创建一个更大的 list 对象,并填充随机数。
④
指定存储数据文件的路径。
⑤
以二进制模式打开文件进行写入(wb)。
用于序列化和反序列化 Python 对象的两个主要函数是 pickle.dump()(用于写入对象)和 pickle.load()(用于将对象加载到内存中):
In [6]: %time pickle.dump(a, pkl_file) # ①
CPU times: user 23.4 ms, sys: 10.1 ms, total: 33.5 ms
Wall time: 31.9 ms
In [7]: pkl_file.close() # ②
In [8]: ll $path* # ③
-rw-r--r-- 1 yves staff 9002006 Jan 18 10:05 /Users/yves/Documents/Temp/data/data.pkl
-rw-r--r-- 1 yves staff 163328824 Jan 18 10:05 /Users/yves/Documents/Temp/data/tstb.h5
In [9]: pkl_file = open(path + 'data.pkl', 'rb') # ④
In [10]: %time b = pickle.load(pkl_file) # ⑤
CPU times: user 28.7 ms, sys: 15.2 ms, total: 43.9 ms
Wall time: 41.9 ms
In [11]: a[:3]
Out[11]: [3.0804166128701134, -0.6586387748854099, 3.3266248354210206]
In [12]: b[:3]
Out[12]: [3.0804166128701134, -0.6586387748854099, 3.3266248354210206]
In [13]: np.allclose(np.array(a), np.array(b)) # ⑥
Out[13]: True
①
序列化对象 a 并将其保存到文件中。
②
关闭文件。
③
显示磁盘上的文件及其大小(Mac/Linux)。
④
以二进制模式打开文件进行读取(rb)。
⑤
从磁盘读取对象并进行反序列化。
⑥
将 a 和 b 转换为 ndarrary 对象,np.allclose() 验证两者包含相同的数据(数字)。
使用 pickle 存储和检索单个对象显然非常简单。那么两个对象呢?
In [14]: pkl_file = open(path + 'data.pkl', 'wb')
In [15]: %time pickle.dump(np.array(a), pkl_file) # ①
CPU times: user 26.6 ms, sys: 11.5 ms, total: 38.1 ms
Wall time: 36.3 ms
In [16]: %time pickle.dump(np.array(a) ** 2, pkl_file) # ②
CPU times: user 35.3 ms, sys: 12.7 ms, total: 48 ms
Wall time: 46.8 ms
In [17]: pkl_file.close()
In [18]: ll $path* # ③
-rw-r--r-- 1 yves staff 16000322 Jan 18 10:05 /Users/yves/Documents/Temp/data/data.pkl
-rw-r--r-- 1 yves staff 163328824 Jan 18 10:05 /Users/yves/Documents/Temp/data/tstb.h5
①
序列化 a 的 ndarray 版本并保存。
②
序列化 a 的平方 ndarray 版本并保存。
③
文件现在的大小大约是之前的两倍。
那么如何将两个ndarray对象读回内存?
In [19]: pkl_file = open(path + 'data.pkl', 'rb')
In [20]: x = pickle.load(pkl_file) # ①
x[:4]
Out[20]: array([ 3.08041661, -0.65863877, 3.32662484, 0.77225328])
In [21]: y = pickle.load(pkl_file) # ②
y[:4]
Out[21]: array([ 9.48896651, 0.43380504, 11.0664328 , 0.59637513])
In [22]: pkl_file.close()
①
这检索到了存储的对象第一个。
②
这检索到了存储的对象第二个。
很明显,pickle根据先进先出(FIFO)原则存储对象。但这存在一个主要问题:用户事先无法获得关于存储在pickle文件中的内容的元信息。
有时一个有用的解决方法是不存储单个对象,而是存储包含所有其他对象的dict对象:
In [23]: pkl_file = open(path + 'data.pkl', 'wb')
pickle.dump({'x': x, 'y': y}, pkl_file) # ①
pkl_file.close()
In [24]: pkl_file = open(path + 'data.pkl', 'rb')
data = pickle.load(pkl_file) # ②
pkl_file.close()
for key in data.keys():
print(key, data[key][:4])
x [ 3.08041661 -0.65863877 3.32662484 0.77225328]
y [ 9.48896651 0.43380504 11.0664328 0.59637513]
In [25]: !rm -f $path*
①
存储包含两个ndarray对象的dict对象。
②
检索dict对象。
然而,这种方法要求一次性写入和读取所有对象。考虑到它带来的更高便利性,这可能是一个可以接受的折中方案。
读写文本文件
文本处理可以被视为 Python 的一个优势。事实上,许多公司和科学用户正是用 Python 来完成这项任务的。使用 Python,你有多种选择来处理str对象,以及一般的文本文件。
假设有一个相当大的数据集要共享为逗号分隔值(CSV)文件。尽管这些文件具有特殊的内部结构,但它们基本上是纯文本文件。以下代码创建一个虚拟数据集作为ndarray对象,一个DatetimeIndex对象,将两者组合并将数据存储为 CSV 文本文件。
In [26]: import pandas as pd
In [27]: rows = 5000 # ①
a = np.random.standard_normal((rows, 5)).round(4) # ②
In [28]: a # ②
Out[28]: array([[-0.9627, 0.1326, -2.012 , -0.299 , -1.4554],
[ 0.8918, 0.8904, -0.3396, -2.3485, 2.0913],
[-0.1899, -0.9574, 1.0258, 0.6206, -2.4693],
...,
[ 1.4688, -1.268 , -0.4778, 1.4315, -1.4689],
[ 1.1162, 0.152 , -0.9363, -0.7869, -0.1147],
[-0.699 , 0.3206, 0.3659, -1.0282, -0.4151]])
In [29]: t = pd.date_range(start='2019/1/1', periods=rows, freq='H') # ③
In [30]: t # ③
Out[30]: DatetimeIndex(['2019-01-01 00:00:00', '2019-01-01 01:00:00',
'2019-01-01 02:00:00', '2019-01-01 03:00:00',
'2019-01-01 04:00:00', '2019-01-01 05:00:00',
'2019-01-01 06:00:00', '2019-01-01 07:00:00',
'2019-01-01 08:00:00', '2019-01-01 09:00:00',
...
'2019-07-27 22:00:00', '2019-07-27 23:00:00',
'2019-07-28 00:00:00', '2019-07-28 01:00:00',
'2019-07-28 02:00:00', '2019-07-28 03:00:00',
'2019-07-28 04:00:00', '2019-07-28 05:00:00',
'2019-07-28 06:00:00', '2019-07-28 07:00:00'],
dtype='datetime64[ns]', length=5000, freq='H')
In [31]: csv_file = open(path + 'data.csv', 'w') # ④
In [32]: header = 'date,no1,no2,no3,no4,no5\n' # ⑤
In [33]: csv_file.write(header) # ⑤
Out[33]: 25
In [34]: for t_, (no1, no2, no3, no4, no5) in zip(t, a): # ⑥
s = '{},{},{},{},{},{}\n'.format(t_, no1, no2, no3, no4, no5) # ⑦
csv_file.write(s) # ⑧
In [35]: csv_file.close()
In [36]: ll $path*
-rw-r--r-- 1 yves staff 284621 Jan 18 10:05 /Users/yves/Documents/Temp/data/data.csv
①
定义数据集的行数。
②
创建具有随机数的ndarray对象。
③
创建一个适当长度的DatetimeIndex对象(每小时间隔)。
④
打开一个供写入的文件(w)。
⑤
定义标题行(列标签)并将其写为第一行。
⑥
数据以行为单位组合…
⑦
…转换为str对象…
⑧
…并逐行写入(追加到 CSV 文本文件中)。
另一种方法也类似。首先,打开现有的CSV文件。其次,使用file对象的.readline()或.readlines()方法逐行读取其内容:
In [37]: csv_file = open(path + 'data.csv', 'r') # ①
In [38]: for i in range(5):
print(csv_file.readline(), end='') # ②
date,no1,no2,no3,no4,no5
2019-01-01 00:00:00,-0.9627,0.1326,-2.012,-0.299,-1.4554
2019-01-01 01:00:00,0.8918,0.8904,-0.3396,-2.3485,2.0913
2019-01-01 02:00:00,-0.1899,-0.9574,1.0258,0.6206,-2.4693
2019-01-01 03:00:00,-0.0217,-0.7168,1.7875,1.6226,-0.4857
In [39]: csv_file.close()
In [40]: csv_file = open(path + 'data.csv', 'r') # ①
In [41]: content = csv_file.readlines() # ③
In [42]: content[:5] # ④
Out[42]: ['date,no1,no2,no3,no4,no5\n',
'2019-01-01 00:00:00,-0.9627,0.1326,-2.012,-0.299,-1.4554\n',
'2019-01-01 01:00:00,0.8918,0.8904,-0.3396,-2.3485,2.0913\n',
'2019-01-01 02:00:00,-0.1899,-0.9574,1.0258,0.6206,-2.4693\n',
'2019-01-01 03:00:00,-0.0217,-0.7168,1.7875,1.6226,-0.4857\n']
In [43]: csv_file.close()
①
打开文件以供读取(r)。
②
逐行读取文件内容并打印。
③
一次性读取文件内容…
④
… 其结果是一个包含所有行的list对象,每行作为单独的str对象。
CSV文件如此重要且常见,以至于 Python 标准库中有一个csv模块,简化了 CSV 文件的处理。csv模块的两个有用的读取器(迭代器)对象都返回一个list对象的list对象,或者一个list对象的dict对象。
In [44]: import csv
In [45]: with open(path + 'data.csv', 'r') as f:
csv_reader = csv.reader(f) # ①
lines = [line for line in csv_reader]
In [46]: lines[:5] # ①
Out[46]: [['date', 'no1', 'no2', 'no3', 'no4', 'no5'],
['2019-01-01 00:00:00', '-0.9627', '0.1326', '-2.012', '-0.299', '-1.4554'],
['2019-01-01 01:00:00', '0.8918', '0.8904', '-0.3396', '-2.3485', '2.0913'],
['2019-01-01 02:00:00', '-0.1899', '-0.9574', '1.0258', '0.6206', '-2.4693'],
['2019-01-01 03:00:00', '-0.0217', '-0.7168', '1.7875', '1.6226', '-0.4857']]
In [47]: with open(path + 'data.csv', 'r') as f:
csv_reader = csv.DictReader(f) # ②
lines = [line for line in csv_reader]
In [48]: lines[:3] # ②
Out[48]: [OrderedDict([('date', '2019-01-01 00:00:00'),
('no1', '-0.9627'),
('no2', '0.1326'),
('no3', '-2.012'),
('no4', '-0.299'),
('no5', '-1.4554')]),
OrderedDict([('date', '2019-01-01 01:00:00'),
('no1', '0.8918'),
('no2', '0.8904'),
('no3', '-0.3396'),
('no4', '-2.3485'),
('no5', '2.0913')]),
OrderedDict([('date', '2019-01-01 02:00:00'),
('no1', '-0.1899'),
('no2', '-0.9574'),
('no3', '1.0258'),
('no4', '0.6206'),
('no5', '-2.4693')])]
In [49]: !rm -f $path*
①
csv.reader()将每一行都返回为一个list对象。
②
csv.DictReader()将每一行都返回为OrderedDict,它是dict对象的一种特殊情况。
SQL 数据库
Python 可以与任何类型的SQL数据库一起工作,并且通常也可以与任何类型的NoSQL数据库一起工作。在这种情况下,SQL代表结构化查询语言。Python 默认提供的一个SQL或关系数据库是SQLite3。借助它,可以轻松地说明 Python 对SQL数据库的基本方法:²
In [50]: import sqlite3 as sq3
In [51]: con = sq3.connect(path + 'numbs.db') # ①
In [52]: query = 'CREATE TABLE numbs (Date date, No1 real, No2 real)' # ②
In [53]: con.execute(query) # ③
Out[53]: <sqlite3.Cursor at 0x1054efb20>
In [54]: con.commit() # ④
In [55]: q = con.execute # ⑤
In [56]: q('SELECT * FROM sqlite_master').fetchall() # ⑥
Out[56]: [('table',
'numbs',
'numbs',
2,
'CREATE TABLE numbs (Date date, No1 real, No2 real)')]
①
打开数据库连接;如果不存在,则创建一个文件。
②
这是一个创建包含三列的表的SQL查询。³
③
执行查询…
④
… 并提交更改。
⑤
这为con.execute()方法定义了一个简短的别名。
⑥
这获取关于数据库的元信息,将刚创建的表显示为单个对象。
现在有了一个带有表的数据库文件,可以使用数据填充该表。每行由一个datetime对象和两个float对象组成:
In [57]: import datetime
In [58]: now = datetime.datetime.now()
q('INSERT INTO numbs VALUES(?, ?, ?)', (now, 0.12, 7.3)) # ①
Out[58]: <sqlite3.Cursor at 0x1054efc70>
In [59]: np.random.seed(100)
In [60]: data = np.random.standard_normal((10000, 2)).round(4) # ②
In [61]: %%time
for row in data: # ③
now = datetime.datetime.now()
q('INSERT INTO numbs VALUES(?, ?, ?)', (now, row[0], row[1]))
con.commit()
CPU times: user 111 ms, sys: 3.22 ms, total: 115 ms
Wall time: 116 ms
In [62]: q('SELECT * FROM numbs').fetchmany(4) # ④
Out[62]: [('2018-01-18 10:05:24.043286', 0.12, 7.3),
('2018-01-18 10:05:24.071921', -1.7498, 0.3427),
('2018-01-18 10:05:24.072110', 1.153, -0.2524),
('2018-01-18 10:05:24.072160', 0.9813, 0.5142)]
In [63]: q('SELECT * FROM numbs WHERE no1 > 0.5').fetchmany(4) # ⑤
Out[63]: [('2018-01-18 10:05:24.072110', 1.153, -0.2524),
('2018-01-18 10:05:24.072160', 0.9813, 0.5142),
('2018-01-18 10:05:24.072257', 0.6727, -0.1044),
('2018-01-18 10:05:24.072319', 1.619, 1.5416)]
In [64]: pointer = q('SELECT * FROM numbs') # ⑥
In [65]: for i in range(3):
print(pointer.fetchone()) # ⑦
('2018-01-18 10:05:24.043286', 0.12, 7.3)
('2018-01-18 10:05:24.071921', -1.7498, 0.3427)
('2018-01-18 10:05:24.072110', 1.153, -0.2524)
In [66]: rows = pointer.fetchall() # ⑧
rows[:3]
Out[66]: [('2018-01-18 10:05:24.072160', 0.9813, 0.5142),
('2018-01-18 10:05:24.072184', 0.2212, -1.07),
('2018-01-18 10:05:24.072202', -0.1895, 0.255)]
①
将单行(或记录)写入numbs表。
②
创建一个较大的虚拟数据集作为ndarray对象。
③
迭代ndarray对象的行。
④
从表中检索多行。
⑤
相同但在no1列的值上有条件。
⑥
定义一个指针对象…
⑦
…它的行为类似于生成器对象。
⑧
.fetchall()检索所有剩余的行。
最后,如果不再需要,可能会想要删除数据库中的表对象。
In [67]: q('DROP TABLE IF EXISTS numbs') # ①
Out[67]: <sqlite3.Cursor at 0x1054eff80>
In [68]: q('SELECT * FROM sqlite_master').fetchall() # ②
Out[68]: []
In [69]: con.close() # ③
In [70]: !rm -f $path* # ④
①
从数据库中删除表格。
②
此操作后不再存在表格对象。
③
关闭数据库连接。
④
从磁盘中删除数据库文件。
SQL数据库是一个相当广泛的主题;事实上,在本章中无法对其进行任何重要的涵盖,因为它太广泛且复杂了。基本信息如下:
-
Python 与几乎所有的数据库技术都能很好地集成。
-
基本的
SQL语法主要由所使用的数据库确定;其余部分如我们所说,都是Pythonic的。
接下来会有几个基于SQLite3的示例。
写入和读取 NumPy 数组
NumPy本身有函数可以以方便和高效的方式写入和读取ndarray对象。在某些情况下,这节省了很多工作,比如当你必须将NumPy的dtype对象转换为特定的数据库类型时(例如对于SQLite3)。为了说明NumPy有时可以有效替代基于SQL的方法,以下代码复制了之前使用NumPy的示例。
代码使用NumPy的np.arange()函数生成一个存储了datetime对象的ndarray对象,而不是使用pandas:⁴
In [71]: dtimes = np.arange('2019-01-01 10:00:00', '2025-12-31 22:00:00',
dtype='datetime64[m]') # ①
In [72]: len(dtimes)
Out[72]: 3681360
In [73]: dty = np.dtype([('Date', 'datetime64[m]'),
('No1', 'f'), ('No2', 'f')]) # ②
In [74]: data = np.zeros(len(dtimes), dtype=dty) # ③
In [75]: data['Date'] = dtimes # ④
In [76]: a = np.random.standard_normal((len(dtimes), 2)).round(4) # ⑤
In [77]: data['No1'] = a[:, 0] # ⑥
data['No2'] = a[:, 1] # ⑥
In [78]: data.nbytes # ⑦
Out[78]: 58901760
①
创建一个带有datetime作为dtype的ndarray对象。
②
用于记录数组的特殊dtype对象。
③
用特殊dtype实例化的ndarray对象。
④
这将填充Date列。
⑤
假数据集……
⑥
…这填充了No1和No2列。
⑦
记录数组的大小(以字节为单位)。
保存ndarray对象是高度优化的,因此非常快速。大约 60 MB 的数据在磁盘上保存约 0.1 秒(这里使用 SSD)。大小为 480 MB 的较大ndarray对象在磁盘上保存大约需要 1 秒钟。
In [79]: %time np.save(path + 'array', data) # ①
CPU times: user 4.06 ms, sys: 99.3 ms, total: 103 ms
Wall time: 107 ms
In [80]: ll $path* # ②
-rw-r--r-- 1 yves staff 58901888 Jan 18 10:05 /Users/yves/Documents/Temp/data/array.npy
In [81]: %time np.load(path + 'array.npy') # ③
CPU times: user 1.81 ms, sys: 47.4 ms, total: 49.2 ms
Wall time: 46.7 ms
Out[81]: array([('2019-01-01T10:00', 1.51310003, 0.69730002),
('2019-01-01T10:01', -1.722 , -0.4815 ),
('2019-01-01T10:02', 0.8251 , 0.3019 ), ...,
('2025-12-31T21:57', 1.37199998, 0.64459997),
('2025-12-31T21:58', -1.25419998, 0.1612 ),
('2025-12-31T21:59', -1.1997 , -1.097 )],
dtype=[('Date', '<M8[m]'), ('No1', '<f4'), ('No2', '<f4')])
In [82]: %time data = np.random.standard_normal((10000, 6000)).round(4) # ④
CPU times: user 2.81 s, sys: 354 ms, total: 3.17 s
Wall time: 3.23 s
In [83]: data.nbytes # ④
Out[83]: 480000000
In [84]: %time np.save(path + 'array', data) # ④
CPU times: user 23.9 ms, sys: 878 ms, total: 902 ms
Wall time: 964 ms
In [85]: ll $path* # ④
-rw-r--r-- 1 yves staff 480000080 Jan 18 10:05 /Users/yves/Documents/Temp/data/array.npy
In [86]: %time np.load(path + 'array.npy') # ④
CPU times: user 1.95 ms, sys: 441 ms, total: 443 ms
Wall time: 441 ms
Out[86]: array([[ 0.3066, 0.5951, 0.5826, ..., 1.6773, 0.4294, -0.2216],
[ 0.8769, 0.7292, -0.9557, ..., 0.5084, 0.9635, -0.4443],
[-1.2202, -2.5509, -0.0575, ..., -1.6128, 0.4662, -1.3645],
...,
[-0.5598, 0.2393, -2.3716, ..., 1.7669, 0.2462, 1.035 ],
[ 0.273 , 0.8216, -0.0749, ..., -0.0552, -0.8396, 0.3077],
[-0.6305, 0.8331, 1.3702, ..., 0.3493, 0.1981, 0.2037]])
In [87]: !rm -f $path*
①
这将记录的ndarray对象保存到磁盘上。
②
磁盘上的大小几乎与内存中的大小相同(由于二进制存储)。
③
这会从磁盘加载记录的ndarray对象。
④
一个较大的普通ndarray对象。
这些示例说明,在这种情况下,写入磁盘主要受硬件限制,因为 480 MB/s 大致代表了标准 SSD 在撰写本文时的宣传写入速度(512 MB/s)。
无论如何,可以预期,与使用标准 SQL 数据库或使用标准 pickle 库进行序列化相比,这种形式的数据存储和检索速度要快得多。有两个原因:首先,数据主要是数字;其次,NumPy 实现了二进制存储,几乎将开销降低到零。当然,使用这种方法不具备 SQL 数据库的功能,但是随后的部分将显示 PyTables 将在这方面提供帮助。
pandas 中的 I/O
pandas 的一个主要优势之一是它可以原生地读取和写入不同的数据格式,其中包括:
-
CSV(逗号分隔值) -
SQL(结构化查询语言) -
XLS/XSLX(微软Excel文件) -
JSON(JavaScript对象表示法) -
HTML(超文本标记语言)
表 9-1 列出了 pandas 和 DataFrame 类的支持格式以及相应的导入和导出函数/方法。导入函数所接受的参数在 [Link to Come] 中列出并描述(根据函数,可能适用其他约定)。
表 9-1. 导入导出函数和方法
| 格式 | 输入 | 输出 | 备注 |
|---|---|---|---|
CSV |
pd.read_csv() |
.to_csv() |
文本文件 |
XLS/XLSX |
pd.read_excel() |
.to_excel() |
电子表格 |
HDF |
pd.read_hdf() |
.to_hdf() |
HDF5 数据库 |
SQL |
pd.read_sql() |
.to_sql() |
SQL 表 |
JSON |
pd.read_json() |
.to_json() |
JavaScript 对象表示法 |
MSGPACK |
pd.read_msgpack() |
.to_msgpack() |
可移植二进制格式 |
HTML |
pd.read_html() |
.to_html() |
HTML 代码 |
GBQ |
pd.read_gbq() |
.to_gbq() |
Google Big Query 格式 |
DTA |
pd.read_stata() |
.to_stata() |
格式 104, 105, 108, 113-115, 117 |
| 任何 | pd.read_clipboard() |
.to_clipboard() |
例如,从 HTML 页面 |
| 任何 | pd.read_pickle() |
.to_pickle() |
(结构化的)Python 对象 |
测试案例再次是一个较大的 float 对象集合:
In [88]: data = np.random.standard_normal((1000000, 5)).round(4)
In [89]: data[:3]
Out[89]: array([[ 0.4918, 1.3707, 0.137 , 0.3981, -1.0059],
[ 0.4516, 1.4445, 0.0555, -0.0397, 0.44 ],
[ 0.1629, -0.8473, -0.8223, -0.4621, -0.5137]])
为此,我们还将重新审视 SQLite3 并将其性能与使用 pandas 的替代格式进行比较。
SQL 数据库
至于 SQLite3 的一切,现在应该都很熟悉了。
In [90]: filename = path + 'numbers'
In [91]: con = sq3.Connection(filename + '.db')
In [92]: query = 'CREATE TABLE numbers (No1 real, No2 real,\
No3 real, No4 real, No5 real)' # ①
In [93]: q = con.execute
qm = con.executemany
In [94]: q(query)
Out[94]: <sqlite3.Cursor at 0x1054e2260>
①
一张具有五列实数(float 对象)的表格。
这次,可以应用 .executemany() 方法,因为数据在一个单一的 ndarray 对象中可用。读取和处理数据与以前一样工作。查询结果也可以轻松可视化(参见 图 9-1)。
In [95]: %%time
qm('INSERT INTO numbers VALUES (?, ?, ?, ?, ?)', data) # ①
con.commit()
CPU times: user 7.16 s, sys: 147 ms, total: 7.3 s
Wall time: 7.39 s
In [96]: ll $path*
-rw-r--r-- 1 yves staff 52633600 Jan 18 10:05 /Users/yves/Documents/Temp/data/numbers.db
In [97]: %%time
temp = q('SELECT * FROM numbers').fetchall() # ②
print(temp[:3])
[(0.4918, 1.3707, 0.137, 0.3981, -1.0059), (0.4516, 1.4445, 0.0555, -0.0397, 0.44), (0.1629, -0.8473, -0.8223, -0.4621, -0.5137)]
CPU times: user 1.86 s, sys: 138 ms, total: 2 s
Wall time: 2.07 s
In [98]: %%time
query = 'SELECT * FROM numbers WHERE No1 > 0 AND No2 < 0'
res = np.array(q(query).fetchall()).round(3) # ③
CPU times: user 770 ms, sys: 73.9 ms, total: 844 ms
Wall time: 854 ms
In [99]: res = res[::100] # ④
plt.figure(figsize=(10, 6))
plt.plot(res[:, 0], res[:, 1], 'ro') # ④
plt.savefig('../../images/ch09/io_01.png');
①
将整个数据集一次性插入表中。
②
以单步操作从表中检索所有行。
③
检索行的选择并将其转换为 ndarray 对象。
④
绘制查询结果的子集。

图 9-1. 查询结果的散点图(选择)
从 SQL 到 pandas
一个通常更高效的方法,然而,是使用 pandas 读取整个表或查询结果。当您能够将整个表读入内存时,分析查询通常可以比使用 SQL 基于磁盘的方法执行得快得多。
使用 pandas 读取整个表与将其读入 NumPy ndarray 对象大致需要相同的时间。在这里和那里,瓶颈是 SQL 数据库。
In [100]: %time data = pd.read_sql('SELECT * FROM numbers', con) # ①
CPU times: user 2.11 s, sys: 175 ms, total: 2.29 s
Wall time: 2.33 s
In [101]: data.head()
Out[101]: No1 No2 No3 No4 No5
0 0.4918 1.3707 0.1370 0.3981 -1.0059
1 0.4516 1.4445 0.0555 -0.0397 0.4400
2 0.1629 -0.8473 -0.8223 -0.4621 -0.5137
3 1.3064 0.9125 0.5142 -0.7868 -0.3398
4 -0.1148 -1.5215 -0.7045 -1.0042 -0.0600
①
将表的所有行读入名为 data 的 DataFrame 对象中。
数据现在在内存中。这样可以进行更快的分析。加速通常是一个数量级或更多。pandas 也可以处理更复杂的查询,尽管它既不意味着也不能替代 SQL 数据库,当涉及复杂的关系数据结构时。多个条件组合的查询结果显示在 图 9-2 中。
In [102]: %time data[(data['No1'] > 0) & (data['No2'] < 0)].head() # ①
CPU times: user 19.4 ms, sys: 9.56 ms, total: 28.9 ms
Wall time: 27.5 ms
Out[102]: No1 No2 No3 No4 No5
2 0.1629 -0.8473 -0.8223 -0.4621 -0.5137
5 0.1893 -0.0207 -0.2104 0.9419 0.2551
8 1.4784 -0.3333 -0.7050 0.3586 -0.3937
10 0.8092 -0.9899 1.0364 -1.0453 0.0579
11 0.9065 -0.7757 -0.9267 0.7797 0.0863
In [103]: %%time
res = data[['No1', 'No2']][((data['No1'] > 0.5) | (data['No1'] < -0.5))
& ((data['No2'] < -1) | (data['No2'] > 1))] # ②
CPU times: user 20.6 ms, sys: 9.18 ms, total: 29.8 ms
Wall time: 28 ms
In [104]: plt.figure(figsize=(10, 6))
plt.plot(res['No1'], res['No2'], 'ro');
plt.savefig('../../images/ch09/io_02.png');
①
两个条件逻辑上组合。
②
逻辑上组合了四个条件。

图 9-2. 查询结果的散点图(选择)
预期地,使用 pandas 的内存分析能力会显著加速,只要 pandas 能够复制相应的 SQL 语句。
使用 pandas 的另一个优点不仅仅是这个,因为 pandas 与 PyTables 等紧密集成 — 后续部分的主题。在这里,知道它们的组合可以显著加速 I/O 操作就足够了。如下所示:
In [105]: h5s = pd.HDFStore(filename + '.h5s', 'w') # ①
In [106]: %time h5s['data'] = data # ②
CPU times: user 33 ms, sys: 43.3 ms, total: 76.3 ms
Wall time: 85.8 ms
In [107]: h5s # ③
Out[107]: <class 'pandas.io.pytables.HDFStore'>
File path: /Users/yves/Documents/Temp/data/numbers.h5s
In [108]: h5s.close() # ④
①
打开 HDF5 数据库文件进行写入;在 pandas 中创建一个 HDFStore 对象。
②
完整的 DataFrame 对象通过二进制存储存储在数据库文件中。
③
HDFStore 对象的信息。
④
关闭数据库文件。
与使用 SQLite3 相比,整个来自原始 SQL 表的所有数据的 DataFrame 写入速度快得多。读取甚至更快:
In [109]: %%time
h5s = pd.HDFStore(filename + '.h5s', 'r') # ①
data_ = h5s['data'] # ②
h5s.close() # ③
CPU times: user 8.24 ms, sys: 21.2 ms, total: 29.4 ms
Wall time: 28.5 ms
In [110]: data_ is data # ④
Out[110]: False
In [111]: (data_ == data).all() # ⑤
Out[111]: No1 True
No2 True
No3 True
No4 True
No5 True
dtype: bool
In [112]: np.allclose(data_, data) # ⑤
Out[112]: True
In [113]: ll $path* # ⑥
-rw-r--r-- 1 yves staff 52633600 Jan 18 10:05 /Users/yves/Documents/Temp/data/numbers.db
-rw-r--r-- 1 yves staff 48007192 Jan 18 10:05 /Users/yves/Documents/Temp/data/numbers.h5s
①
打开 HDF5 数据库文件进行读取。
②
DataFrame 被读取并存储在内存中作为 data_。
③
关闭数据库文件。
④
这两个 DataFrame 对象不相同。
⑤
然而,它们现在包含相同的数据。
⑥
与 SQL 表相比,二进制存储通常具有更小的大小开销。
CSV 文件中的数据
交换金融数据最广泛使用的格式之一是 CSV 格式。尽管它并没有真正标准化,但它可以被任何平台处理,并且绝大多数与数据和金融分析有关的应用程序都可以处理。前一节展示了如何使用标准 Python 功能将数据写入 CSV 文件并从 CSV 文件中读取数据(参见“读写文本文件”)。pandas 使得整个过程更加方便,代码更加简洁,并且总体执行更快(还可以参见图 9-3):
In [114]: %time data.to_csv(filename + '.csv') # ①
CPU times: user 6.82 s, sys: 277 ms, total: 7.1 s
Wall time: 7.54 s
In [115]: ll $path
total 282184
-rw-r--r-- 1 yves staff 43834157 Jan 18 10:05 numbers.csv
-rw-r--r-- 1 yves staff 52633600 Jan 18 10:05 numbers.db
-rw-r--r-- 1 yves staff 48007192 Jan 18 10:05 numbers.h5s
In [116]: %time df = pd.read_csv(filename + '.csv') # ②
CPU times: user 1.4 s, sys: 124 ms, total: 1.53 s
Wall time: 1.58 s
In [117]: df[['No1', 'No2', 'No3', 'No4']].hist(bins=20, figsize=(10, 6));
plt.savefig('../../images/ch09/io_03.png');
①
.to_csv() 方法将 DataFrame 数据以 CSV 格式写入磁盘。
②
然后 pd.read_csv() 以新的 DataFrame 对象的形式将其再次读入内存。

图 9-3. 选定列的直方图
Excel 文件中的数据
尽管处理 Excel 电子表格是本书的后续章节的主题,但以下代码简要地演示了 pandas 如何以 Excel 格式写入数据并从 Excel 电子表格中读取数据。在这种情况下,我们将数据集限制为 100,000 行:
In [118]: %time data[:100000].to_excel(filename + '.xlsx') # ①
CPU times: user 23.2 s, sys: 498 ms, total: 23.7 s
Wall time: 23.9 s
In [119]: %time df = pd.read_excel(filename + '.xlsx', 'Sheet1') # ②
CPU times: user 5.47 s, sys: 74.7 ms, total: 5.54 s
Wall time: 5.57 s
In [120]: df.cumsum().plot(figsize=(10, 6));
plt.savefig('../../images/ch09/io_04.png');
In [121]: ll $path*
-rw-r--r-- 1 yves staff 43834157 Jan 18 10:05 /Users/yves/Documents/Temp/data/numbers.csv
-rw-r--r-- 1 yves staff 52633600 Jan 18 10:05 /Users/yves/Documents/Temp/data/numbers.db
-rw-r--r-- 1 yves staff 48007192 Jan 18 10:05 /Users/yves/Documents/Temp/data/numbers.h5s
-rw-r--r-- 1 yves staff 4032639 Jan 18 10:06 /Users/yves/Documents/Temp/data/numbers.xlsx
In [122]: rm -f $path*
①
.to_excel() 方法将 DataFrame 数据以 XLSX 格式写入磁盘。
②
然后 pd.read_excel() 以新的 DataFrame 对象的形式将其再次读入内存,同时指定要从中读取的工作表。

图 9-4. 所有列的线性图
生成包含较小数据子集的 Excel 电子表格文件需要相当长的时间。这说明了电子表格结构所带来的额外开销。
对生成的文件进行检查后发现,DataFrame 与 HDFStore 结合是最紧凑的选择(使用压缩,正如本章后面所述,进一步增加了优势)。与文本文件相比,作为 CSV 文件的相同数量的数据的大小要大一些。这是处理 CSV 文件时性能较慢的另一个原因,另一个原因是它们只是“普通”文本文件。
使用 PyTables 进行快速 I/O
PyTables是HDF5数据库标准的 Python 绑定(参见http://www.hdfgroup.org)。它专门设计用于优化 I/O 操作的性能,并充分利用可用的硬件。库的导入名称是tables。与pandas类似,当涉及到内存分析时,PyTables既不能也不意味着是对SQL数据库的完全替代。然而,它带来了一些进一步缩小差距的特性。例如,一个PyTables数据库可以有很多表,它支持压缩和索引以及对表的非平凡查询。此外,它可以有效地存储NumPy数组,并具有其自己的数组数据结构的风格。
首先,一些导入:
In [123]: import tables as tb # ①
import datetime as dt
①
包名是PyTables,导入名称是tables。
与表格一起工作
PyTables提供了一种基于文件的数据库格式,类似于SQLite3。⁵。以下是打开数据库文件并创建表格的示例:
In [124]: filename = path + 'pytab.h5'
In [125]: h5 = tb.open_file(filename, 'w') # ①
In [126]: row_des = {
'Date': tb.StringCol(26, pos=1), # ②
'No1': tb.IntCol(pos=2), # ③
'No2': tb.IntCol(pos=3), # ③
'No3': tb.Float64Col(pos=4), # ④
'No4': tb.Float64Col(pos=5) # ④
}
In [127]: rows = 2000000
In [128]: filters = tb.Filters(complevel=0) # ⑤
In [129]: tab = h5.create_table('/', 'ints_floats', # ⑥
row_des, # ⑦
title='Integers and Floats', # ⑧
expectedrows=rows, # ⑨
filters=filters) # ⑩
In [130]: type(tab)
Out[130]: tables.table.Table
In [131]: tab
Out[131]: /ints_floats (Table(0,)) 'Integers and Floats'
description := {
"Date": StringCol(itemsize=26, shape=(), dflt=b'', pos=0),
"No1": Int32Col(shape=(), dflt=0, pos=1),
"No2": Int32Col(shape=(), dflt=0, pos=2),
"No3": Float64Col(shape=(), dflt=0.0, pos=3),
"No4": Float64Col(shape=(), dflt=0.0, pos=4)}
byteorder := 'little'
chunkshape := (2621,)
①
以HDF5二进制存储格式打开数据库文件。
②
用于日期时间信息的date列(作为str对象)。
③
用于存储int对象的两列。
④
用于存储float对象的两列。
⑤
通过Filters对象,可以指定压缩级别等。
⑥
表的节点(路径)和技术名称。
⑦
行数据结构的描述。
⑧
表的名称(标题)。
⑨
预期的行数;允许进行优化。
⑩
用于表格的Filters对象。
为了用数字数据填充表格,生成两个具有随机数字的ndarray对象。一个是随机整数,另一个是随机浮点数。通过一个简单的 Python 循环来填充表格。
In [132]: pointer = tab.row # ①
In [133]: ran_int = np.random.randint(0, 10000, size=(rows, 2)) # ②
In [134]: ran_flo = np.random.standard_normal((rows, 2)).round(4) # ③
In [135]: %%time
for i in range(rows):
pointer['Date'] = dt.datetime.now() # ④
pointer['No1'] = ran_int[i, 0] # ④
pointer['No2'] = ran_int[i, 1] # ④
pointer['No3'] = ran_flo[i, 0] # ④
pointer['No4'] = ran_flo[i, 1] # ④
pointer.append() # ⑤
tab.flush() # ⑥
CPU times: user 8.36 s, sys: 136 ms, total: 8.49 s
Wall time: 8.92 s
In [136]: tab # ⑦
Out[136]: /ints_floats (Table(2000000,)) 'Integers and Floats'
description := {
"Date": StringCol(itemsize=26, shape=(), dflt=b'', pos=0),
"No1": Int32Col(shape=(), dflt=0, pos=1),
"No2": Int32Col(shape=(), dflt=0, pos=2),
"No3": Float64Col(shape=(), dflt=0.0, pos=3),
"No4": Float64Col(shape=(), dflt=0.0, pos=4)}
byteorder := 'little'
chunkshape := (2621,)
In [137]: ll $path*
-rw-r--r-- 1 yves staff 100156248 Jan 18 10:06 /Users/yves/Documents/Temp/data/pytab.h5
①
创建了一个指针对象。
②
具有随机int对象的ndarray对象。
③
具有随机float对象的ndarray对象。
④
datetime对象,两个int和两个float对象被逐行写入。
⑤
新行被附加。
⑥
所有写入的行都会被刷新,即作为永久更改提交。
⑦
更改反映在 Table 对象描述中。
在这种情况下,Python 循环相当慢。 有一种更高效和 Pythonic 的方法可以实现相同的结果,即使用 NumPy 结构化数组。 使用存储在结构化数组中的完整数据集,表的创建归结为一行代码。 请注意,不再需要行描述; PyTables 使用结构化数组的 dtype 对象来推断数据类型:
In [138]: dty = np.dtype([('Date', 'S26'), ('No1', '<i4'), ('No2', '<i4'),
('No3', '<f8'), ('No4', '<f8')]) # ①
In [139]: sarray = np.zeros(len(ran_int), dtype=dty) # ②
In [140]: sarray[:4] # ③
Out[140]: array([(b'', 0, 0, 0., 0.), (b'', 0, 0, 0., 0.), (b'', 0, 0, 0., 0.),
(b'', 0, 0, 0., 0.)],
dtype=[('Date', 'S26'), ('No1', '<i4'), ('No2', '<i4'), ('No3', '<f8'), ('No4', '<f8')])
In [141]: %%time
sarray['Date'] = dt.datetime.now() # ④
sarray['No1'] = ran_int[:, 0] # ④
sarray['No2'] = ran_int[:, 1] # ④
sarray['No3'] = ran_flo[:, 0] # ④
sarray['No4'] = ran_flo[:, 1] # ④
CPU times: user 82.7 ms, sys: 37.9 ms, total: 121 ms
Wall time: 133 ms
In [142]: %%time
h5.create_table('/', 'ints_floats_from_array', sarray,
title='Integers and Floats',
expectedrows=rows, filters=filters) # ⑤
CPU times: user 39 ms, sys: 61 ms, total: 100 ms
Wall time: 123 ms
Out[142]: /ints_floats_from_array (Table(2000000,)) 'Integers and Floats'
description := {
"Date": StringCol(itemsize=26, shape=(), dflt=b'', pos=0),
"No1": Int32Col(shape=(), dflt=0, pos=1),
"No2": Int32Col(shape=(), dflt=0, pos=2),
"No3": Float64Col(shape=(), dflt=0.0, pos=3),
"No4": Float64Col(shape=(), dflt=0.0, pos=4)}
byteorder := 'little'
chunkshape := (2621,)
①
定义特殊的 dtype 对象。
②
使用零(和空字符串)创建结构化数组。
③
来自 ndarray 对象的几条记录。
④
ndarray 对象的列一次性填充。
⑤
这将创建 Table 对象,并用数据填充它。
这种方法快了一个数量级,代码更简洁,且实现了相同的结果。
In [143]: type(h5)
Out[143]: tables.file.File
In [144]: h5 # ①
Out[144]: File(filename=/Users/yves/Documents/Temp/data/pytab.h5, title='', mode='w', root_uep='/', filters=Filters(complevel=0, shuffle=False, bitshuffle=False, fletcher32=False, least_significant_digit=None))
/ (RootGroup) ''
/ints_floats (Table(2000000,)) 'Integers and Floats'
description := {
"Date": StringCol(itemsize=26, shape=(), dflt=b'', pos=0),
"No1": Int32Col(shape=(), dflt=0, pos=1),
"No2": Int32Col(shape=(), dflt=0, pos=2),
"No3": Float64Col(shape=(), dflt=0.0, pos=3),
"No4": Float64Col(shape=(), dflt=0.0, pos=4)}
byteorder := 'little'
chunkshape := (2621,)
/ints_floats_from_array (Table(2000000,)) 'Integers and Floats'
description := {
"Date": StringCol(itemsize=26, shape=(), dflt=b'', pos=0),
"No1": Int32Col(shape=(), dflt=0, pos=1),
"No2": Int32Col(shape=(), dflt=0, pos=2),
"No3": Float64Col(shape=(), dflt=0.0, pos=3),
"No4": Float64Col(shape=(), dflt=0.0, pos=4)}
byteorder := 'little'
chunkshape := (2621,)
In [145]: h5.remove_node('/', 'ints_floats_from_array') # ②
①
带有两个 Table 对象的 File 对象的描述。
②
这会删除具有冗余数据的第二个 Table 对象。
Table 对象在大多数情况下的行为与 NumPy 结构化的 ndarray 对象非常相似(另见 图 9-5):
In [146]: tab[:3] # ①
Out[146]: array([(b'2018-01-18 10:06:28.516235', 8576, 5991, -0.0528, 0.2468),
(b'2018-01-18 10:06:28.516332', 2990, 9310, -0.0261, 0.3932),
(b'2018-01-18 10:06:28.516344', 4400, 4823, 0.9133, 0.2579)],
dtype=[('Date', 'S26'), ('No1', '<i4'), ('No2', '<i4'), ('No3', '<f8'), ('No4', '<f8')])
In [147]: tab[:4]['No4'] # ②
Out[147]: array([ 0.2468, 0.3932, 0.2579, -0.5582])
In [148]: %time np.sum(tab[:]['No3']) # ③
CPU times: user 64.5 ms, sys: 97.1 ms, total: 162 ms
Wall time: 165 ms
Out[148]: 88.854299999999697
In [149]: %time np.sum(np.sqrt(tab[:]['No1'])) # ③
CPU times: user 59.3 ms, sys: 69.4 ms, total: 129 ms
Wall time: 130 ms
Out[149]: 133349920.36892509
In [150]: %%time
plt.figure(figsize=(10, 6))
plt.hist(tab[:]['No3'], bins=30); # ④
plt.savefig('../../images/ch09/io_05.png');
CPU times: user 244 ms, sys: 67.6 ms, total: 312 ms
Wall time: 340 ms
①
通过索引选择行。
②
仅通过索引选择列值。
③
应用 NumPy 通用函数。
④
从 Table 对象绘制列。

图 9-5. 列数据的直方图
PyTables 还提供了通过典型的 SQL-like 语句查询数据的灵活工具,如下例所示(其结果如 图 9-6 所示;与 图 9-2 相比,基于 pandas 查询):
In [151]: query = '((No3 < -0.5) | (No3 > 0.5)) & ((No4 < -1) | (No4 > 1))' # ①
In [152]: iterator = tab.where(query) # ②
In [153]: %time res = [(row['No3'], row['No4']) for row in iterator] # ③
CPU times: user 487 ms, sys: 128 ms, total: 615 ms
Wall time: 637 ms
In [154]: res = np.array(res) # ④
res[:3]
Out[154]: array([[ 0.7694, 1.4866],
[ 0.9201, 1.3346],
[ 1.4701, 1.8776]])
In [155]: plt.figure(figsize=(10, 6))
plt.plot(res.T[0], res.T[1], 'ro');
plt.savefig('../../images/ch09/io_06.png');
①
查询作为 str 对象,由逻辑运算符组合的四个条件。
②
基于查询的迭代器对象。
③
通过列表推导收集查询结果的行…
④
… 并转换为 ndarray 对象。

图 9-6. 列数据的直方图
快速查询
pandas和PyTables都能够处理相对复杂的、类似SQL的查询和选择。它们在执行此类操作时都进行了速度优化。但是,与关系型数据库相比,这些方法当然存在限制。但对于大多数数值和金融应用程序,它们通常并不决定性。
正如以下示例所示,使用存储在PyTables中的数据作为Table对象让您感觉就像是在NumPy或pandas中工作且是内存中的,从语法和性能方面都是如此:
In [156]: %%time
values = tab[:]['No3']
print('Max %18.3f' % values.max())
print('Ave %18.3f' % values.mean())
print('Min %18.3f' % values.min())
print('Std %18.3f' % values.std())
Max 5.224
Ave 0.000
Min -5.649
Std 1.000
CPU times: user 88.9 ms, sys: 70 ms, total: 159 ms
Wall time: 156 ms
In [157]: %%time
res = [(row['No1'], row['No2']) for row in
tab.where('((No1 > 9800) | (No1 < 200)) \
& ((No2 > 4500) & (No2 < 5500))')]
CPU times: user 78.4 ms, sys: 38.9 ms, total: 117 ms
Wall time: 80.9 ms
In [158]: for r in res[:4]:
print(r)
(91, 4870)
(9803, 5026)
(9846, 4859)
(9823, 5069)
In [159]: %%time
res = [(row['No1'], row['No2']) for row in
tab.where('(No1 == 1234) & (No2 > 9776)')]
CPU times: user 58.9 ms, sys: 40.1 ms, total: 99 ms
Wall time: 133 ms
In [160]: for r in res:
print(r)
(1234, 9841)
(1234, 9821)
(1234, 9867)
(1234, 9987)
(1234, 9849)
(1234, 9800)
使用压缩表
使用PyTables的一个主要优势是它采用的压缩方法。它不仅使用压缩来节省磁盘空间,还利用了在某些硬件场景下改善 I/O 操作性能的压缩。这是如何实现的?当 I/O 成为瓶颈,而 CPU 能够快速(解)压缩数据时,压缩在速度方面的净效果可能是积极的。由于以下示例基于标准 SSD 的 I/O,因此观察不到压缩的速度优势。但是,使用压缩也几乎没有缺点:
In [161]: filename = path + 'pytabc.h5'
In [162]: h5c = tb.open_file(filename, 'w')
In [163]: filters = tb.Filters(complevel=5, # ①
complib='blosc') # ②
In [164]: tabc = h5c.create_table('/', 'ints_floats', sarray,
title='Integers and Floats',
expectedrows=rows, filters=filters)
In [165]: query = '((No3 < -0.5) | (No3 > 0.5)) & ((No4 < -1) | (No4 > 1))'
In [166]: iteratorc = tabc.where(query) # ③
In [167]: %time res = [(row['No3'], row['No4']) for row in iteratorc] # ④
CPU times: user 362 ms, sys: 55.3 ms, total: 418 ms
Wall time: 445 ms
In [168]: res = np.array(res)
res[:3]
Out[168]: array([[ 0.7694, 1.4866],
[ 0.9201, 1.3346],
[ 1.4701, 1.8776]])
①
压缩级别(complevel)可以取 0(无压缩)到 9(最高压缩)的值。
②
使用了经过优化的Blosc压缩引擎(Blosc),该引擎旨在提高性能。
③
给定前面查询的迭代器对象。
④
通过列表推导收集查询结果行。
使用原始数据生成压缩的Table对象并对其进行分析比使用未压缩的Table对象稍慢一些。那么将数据读入ndarray对象呢?让我们来检查一下:
In [169]: %time arr_non = tab.read() # ①
CPU times: user 42.9 ms, sys: 69.9 ms, total: 113 ms
Wall time: 117 ms
In [170]: tab.size_on_disk
Out[170]: 100122200
In [171]: arr_non.nbytes
Out[171]: 100000000
In [172]: %time arr_com = tabc.read() # ②
CPU times: user 123 ms, sys: 60.5 ms, total: 184 ms
Wall time: 191 ms
In [173]: tabc.size_on_disk
Out[173]: 40612465
In [174]: arr_com.nbytes
Out[174]: 100000000
In [175]: ll $path* # ③
-rw-r--r-- 1 yves staff 200312336 Jan 18 10:06 /Users/yves/Documents/Temp/data/pytab.h5
-rw-r--r-- 1 yves staff 40647761 Jan 18 10:06 /Users/yves/Documents/Temp/data/pytabc.h5
In [176]: h5c.close() # ④
①
从未压缩的Table对象tab中读取。
②
从压缩的Table对象tabc中读取。
③
压缩表的大小显着减小了。
④
关闭数据库文件。
例子表明,与未压缩的Table对象相比,使用压缩的Table对象工作时几乎没有速度差异。但是,磁盘上的文件大小可能会根据数据的质量而显着减少,这有许多好处:
-
存储成本:存储成本降低了
-
备份成本:备份成本降低了
-
网络流量:网络流量减少了
-
网络速度:存储在远程服务器上并从中检索的速度更快
-
CPU 利用率:为了克服 I/O 瓶颈而增加了 CPU 利用率
使用数组
“Python 基本 I/O”演示了NumPy对于ndarray对象具有内置的快速写入和读取功能。当涉及到存储和检索ndarray对象时,PyTables也非常快速和高效。由于它基于分层数据库结构,因此提供了许多便利功能:
In [177]: %%time
arr_int = h5.create_array('/', 'integers', ran_int) # ①
arr_flo = h5.create_array('/', 'floats', ran_flo) # ②
CPU times: user 3.24 ms, sys: 33.1 ms, total: 36.3 ms
Wall time: 41.6 ms
In [178]: h5 # ③
Out[178]: File(filename=/Users/yves/Documents/Temp/data/pytab.h5, title='', mode='w', root_uep='/', filters=Filters(complevel=0, shuffle=False, bitshuffle=False, fletcher32=False, least_significant_digit=None))
/ (RootGroup) ''
/floats (Array(2000000, 2)) ''
atom := Float64Atom(shape=(), dflt=0.0)
maindim := 0
flavor := 'numpy'
byteorder := 'little'
chunkshape := None
/integers (Array(2000000, 2)) ''
atom := Int64Atom(shape=(), dflt=0)
maindim := 0
flavor := 'numpy'
byteorder := 'little'
chunkshape := None
/ints_floats (Table(2000000,)) 'Integers and Floats'
description := {
"Date": StringCol(itemsize=26, shape=(), dflt=b'', pos=0),
"No1": Int32Col(shape=(), dflt=0, pos=1),
"No2": Int32Col(shape=(), dflt=0, pos=2),
"No3": Float64Col(shape=(), dflt=0.0, pos=3),
"No4": Float64Col(shape=(), dflt=0.0, pos=4)}
byteorder := 'little'
chunkshape := (2621,)
In [179]: ll $path*
-rw-r--r-- 1 yves staff 262344490 Jan 18 10:06 /Users/yves/Documents/Temp/data/pytab.h5
-rw-r--r-- 1 yves staff 40647761 Jan 18 10:06 /Users/yves/Documents/Temp/data/pytabc.h5
In [180]: h5.close()
In [181]: !rm -f $path*
①
存储ran_int ndarray对象。
②
存储ran_flo ndarray对象。
③
更改反映在对象描述中。
将这些对象直接写入HDF5数据库比遍历对象并逐行将数据写入Table对象或使用结构化ndarray对象的方法更快。
基于 HDF5 的数据存储
当涉及到结构化的数值和金融数据时,HDF5分层数据库(文件)格式是一个强大的替代方案,例如,关系数据库。无论是在直接使用PyTables还是与pandas的功能结合使用时,您都可以期望获得几乎达到可用硬件允许的最大 I/O 性能。
内存外计算
PyTables支持内存外操作,这使得可以实现不适合内存的基于数组的计算。为此,请考虑以下基于EArray类的代码。这种类型的对象允许在一维(按行)中扩展,而列数(每行的元素)需要固定。
In [182]: filename = path + 'earray.h5'
In [183]: h5 = tb.open_file(filename, 'w')
In [184]: n = 500 # ①
In [185]: ear = h5.create_earray('/', 'ear', # ②
atom=tb.Float64Atom(), # ③
shape=(0, n)) # ④
In [186]: type(ear)
Out[186]: tables.earray.EArray
In [187]: rand = np.random.standard_normal((n, n)) # ⑤
rand[:4, :4]
Out[187]: array([[-1.25983231, 1.11420699, 0.1667485 , 0.7345676 ],
[-0.13785424, 1.22232417, 1.36303097, 0.13521042],
[ 1.45487119, -1.47784078, 0.15027672, 0.86755989],
[-0.63519366, 0.1516327 , -0.64939447, -0.45010975]])
In [188]: %%time
for _ in range(750):
ear.append(rand) # ⑥
ear.flush()
CPU times: user 728 ms, sys: 1.11 s, total: 1.84 s
Wall time: 2.03 s
In [189]: ear
Out[189]: /ear (EArray(375000, 500)) ''
atom := Float64Atom(shape=(), dflt=0.0)
maindim := 0
flavor := 'numpy'
byteorder := 'little'
chunkshape := (16, 500)
In [190]: ear.size_on_disk
Out[190]: 1500032000
①
这定义了固定的列数。
②
EArray对象的路径和技术名称。
③
单个值的原子dtype对象。
④
用于实例化的形状(没有行,n列)。
⑤
具有随机数的ndarray对象…
⑥
… 多次附加。
对于不会导致聚合的内存外计算,需要另一个相同形状(大小)的EArray对象。 PyTables+有一个特殊模块可以高效处理数值表达式。它称为Expr,基于数值表达式库numexpr。接下来的代码使用Expr计算之前整个EArray对象中的方程式 9-1 的数学表达式。
方程式 9-1. 示例数学表达式
结果存储在out EArray对象中,表达式评估以块方式进行。
In [191]: out = h5.create_earray('/', 'out',
atom=tb.Float64Atom(),
shape=(0, n))
In [192]: out.size_on_disk
Out[192]: 0
In [193]: expr = tb.Expr('3 * sin(ear) + sqrt(abs(ear))') # ①
In [194]: expr.set_output(out, append_mode=True) # ②
In [195]: %time expr.eval() # ③
CPU times: user 2.98 s, sys: 1.38 s, total: 4.36 s
Wall time: 3.28 s
Out[195]: /out (EArray(375000, 500)) ''
atom := Float64Atom(shape=(), dflt=0.0)
maindim := 0
flavor := 'numpy'
byteorder := 'little'
chunkshape := (16, 500)
In [196]: out.size_on_disk
Out[196]: 1500032000
In [197]: out[0, :10]
Out[197]: array([-1.73369462, 3.74824436, 0.90627898, 2.86786818, 1.75424957,
-0.91108973, -1.68313885, 1.29073295, -1.68665599, -1.71345309])
In [198]: %time out_ = out.read() # ④
CPU times: user 879 ms, sys: 1.11 s, total: 1.99 s
Wall time: 2.18 s
In [199]: out_[0, :10]
Out[199]: array([-1.73369462, 3.74824436, 0.90627898, 2.86786818, 1.75424957,
-0.91108973, -1.68313885, 1.29073295, -1.68665599, -1.71345309])
①
这将基于str对象的表达式转换为Expr对象。
②
这定义了输出为 out EArray 对象。
③
这启动了表达式的评估。
④
这将整个 EArray 读入内存。
考虑到整个操作是在内存之外进行的,可以认为是相当快的,尤其是在标准硬件上执行。作为基准,可以考虑 numexpr 模块的内存性能(也见[Link to Come])。它更快,但并不是很大的优势:
In [200]: import numexpr as ne # ①
In [201]: expr = '3 * sin(out_) + sqrt(abs(out_))' # ②
In [202]: ne.set_num_threads(1) # ③
Out[202]: 4
In [203]: %time ne.evaluate(expr)[0, :10] # ④
CPU times: user 1.72 s, sys: 529 ms, total: 2.25 s
Wall time: 2.38 s
Out[203]: array([-1.64358578, 0.22567882, 3.31363043, 2.50443549, 4.27413965,
-1.41600606, -1.68373023, 4.01921805, -1.68117412, -1.66053597])
In [204]: ne.set_num_threads(4) # ⑤
Out[204]: 1
In [205]: %time ne.evaluate(expr)[0, :10] # ⑥
CPU times: user 2.29 s, sys: 804 ms, total: 3.09 s
Wall time: 1.56 s
Out[205]: array([-1.64358578, 0.22567882, 3.31363043, 2.50443549, 4.27413965,
-1.41600606, -1.68373023, 4.01921805, -1.68117412, -1.66053597])
In [206]: h5.close()
In [207]: !rm -f $path*
①
导入用于 内存中 评估数值表达式的模块。
②
数值表达式作为 str 对象。
③
将线程数设置为仅一个。
④
使用一个线程在内存中评估数值表达式。
⑤
将线程数设置为四。
⑥
使用四个线程在内存中评估数值表达式。
通过 TsTables 进行 I/O 操作。
TsTables 包使用 PyTables 构建了一个高性能的时间序列数据存储。主要的使用场景是“一次写入,多次检索”。这是金融分析中的典型场景,因为数据是在市场上创建的,可能是实时或异步检索,并存储在磁盘上以供以后使用。这样的使用场景可能是一个较大的交易策略回测程序,需要反复使用历史金融时间序列的不同子集。因此,数据检索速度很重要。
示例数据
通常情况下,首先生成一些足够大的示例数据集,以说明 TsTables 的好处。以下代码基于几何布朗运动的模拟生成了三个相当长的金融时间序列(见[Link to Come])。
In [208]: no = 5000000 # ①
co = 3 # ②
interval = 1. / (12 * 30 * 24 * 60) # ③
vol = 0.2 # ④
In [209]: %%time
rn = np.random.standard_normal((no, co)) # ⑤
rn[0] = 0.0 # ⑥
paths = 100 * np.exp(np.cumsum(-0.5 * vol ** 2 * interval +
vol * np.sqrt(interval) * rn, axis=0)) # ⑦
paths[0] = 100 # ⑧
CPU times: user 932 ms, sys: 204 ms, total: 1.14 s
Wall time: 1.2 s
①
时间步数。
②
时间序列的数量。
③
年份间隔作为年分数。
④
波动率。
⑤
标准正态分布的随机数。
⑥
初始随机数设为 0。
⑦
基于 Euler 离散化的模拟。
⑧
将路径的初始值设为 100。
由于TsTables与pandas DataFrame对象很好地配合,因此数据被转换为这样的对象(另见图 9-7)。
In [210]: dr = pd.date_range('2019-1-1', periods=no, freq='1s')
In [211]: dr[-6:]
Out[211]: DatetimeIndex(['2019-02-27 20:53:14', '2019-02-27 20:53:15',
'2019-02-27 20:53:16', '2019-02-27 20:53:17',
'2019-02-27 20:53:18', '2019-02-27 20:53:19'],
dtype='datetime64[ns]', freq='S')
In [212]: df = pd.DataFrame(paths, index=dr, columns=['ts1', 'ts2', 'ts3'])
In [213]: df.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 5000000 entries, 2019-01-01 00:00:00 to 2019-02-27 20:53:19
Freq: S
Data columns (total 3 columns):
ts1 float64
ts2 float64
ts3 float64
dtypes: float64(3)
memory usage: 152.6 MB
In [214]: df.head()
Out[214]: ts1 ts2 ts3
2019-01-01 00:00:00 100.000000 100.000000 100.000000
2019-01-01 00:00:01 100.018443 99.966644 99.998255
2019-01-01 00:00:02 100.069023 100.004420 99.986646
2019-01-01 00:00:03 100.086757 100.000246 99.992042
2019-01-01 00:00:04 100.105448 100.036033 99.950618
In [215]: df[::100000].plot(figsize=(10, 6));
plt.savefig('../../images/ch09/io_07.png')

图 9-7. 金融时间序列的选定数据点
数据存储
TsTables基于特定的基于块的结构存储金融时间序列数据,该结构允许根据某个时间间隔快速检索任意数据子集。为此,该软件包将create_ts()函数添加到PyTables中。以下代码使用了来自PyTables的class基于描述方法,基于tb.IsDescription类。
In [216]: import tstables as tstab
In [217]: class ts_desc(tb.IsDescription):
timestamp = tb.Int64Col(pos=0) # ①
ts1 = tb.Float64Col(pos=1) # ②
ts2 = tb.Float64Col(pos=2) # ②
ts3 = tb.Float64Col(pos=3) # ②
In [218]: h5 = tb.open_file(path + 'tstab.h5', 'w') # ③
In [219]: ts = h5.create_ts('/', 'ts', ts_desc) # ④
In [220]: %time ts.append(df) # ⑤
CPU times: user 692 ms, sys: 403 ms, total: 1.1 s
Wall time: 1.12 s
In [221]: type(ts)
Out[221]: tstables.tstable.TsTable
In [222]: ls -n $path
total 306720
-rw-r--r-- 1 501 20 157037368 Jan 18 10:07 tstab.h5
①
时间戳的列。
②
存储数字数据的列。
③
为写入(w)打开HDF5数据库文件。
④
基于ts_desc对象创建TsTable对象。
⑤
将DataFrame对象中的数据附加到TsTable对象。
数据检索
使用TsTables编写数据显然非常快,即使与硬件有关。对数据的块的读取也是如此。方便的是,TaTables返回一个DataFrame对象(另见图 9-8)。
In [223]: read_start_dt = dt.datetime(2019, 2, 1, 0, 0) # ①
read_end_dt = dt.datetime(2019, 2, 5, 23, 59) # ②
In [224]: %time rows = ts.read_range(read_start_dt, read_end_dt) # ③
CPU times: user 80.5 ms, sys: 36.2 ms, total: 117 ms
Wall time: 116 ms
In [225]: rows.info() # ④
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 431941 entries, 2019-02-01 00:00:00 to 2019-02-05 23:59:00
Data columns (total 3 columns):
ts1 431941 non-null float64
ts2 431941 non-null float64
ts3 431941 non-null float64
dtypes: float64(3)
memory usage: 13.2 MB
In [226]: rows.head() # ④
Out[226]: ts1 ts2 ts3
2019-02-01 00:00:00 52.063640 40.474580 217.324713
2019-02-01 00:00:01 52.087455 40.471911 217.250070
2019-02-01 00:00:02 52.084808 40.458013 217.228712
2019-02-01 00:00:03 52.073536 40.451408 217.302912
2019-02-01 00:00:04 52.056133 40.450951 217.207481
In [227]: h5.close()
In [228]: (rows[::500] / rows.iloc[0]).plot(figsize=(10, 6));
plt.savefig('../../images/ch09/io_08.png')
①
时间间隔的开始时间。
②
时间间隔的结束时间。
③
函数ts.read_range()返回时间间隔的DataFrame对象。
④
DataFrame对象有几十万行数据。

图 9-8. 金融时间序列的特定时间间隔(归一化)
为了更好地说明基于TsTables的数据检索性能,考虑以下基准,该基准检索由三天的一秒钟柱状图组成的 100 个数据块。检索包含 345,600 行数据的DataFrame仅需不到十分之一秒。
In [229]: import random
In [230]: h5 = tb.open_file(path + 'tstab.h5', 'r')
In [231]: ts = h5.root.ts._f_get_timeseries() # ①
In [235]: %%time
for _ in range(100): # ②
d = random.randint(1, 24) # ③
read_start_dt = dt.datetime(2019, 2, d, 0, 0, 0)
read_end_dt = dt.datetime(2019, 2, d + 3, 23, 59, 59)
rows = ts.read_range(read_start_dt, read_end_dt)
CPU times: user 3.51 s, sys: 1.03 s, total: 4.55 s
Wall time: 4.62 s
In [233]: rows.info() # ④
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 431941 entries, 2019-02-01 00:00:00 to 2019-02-05 23:59:00
Data columns (total 3 columns):
ts1 431941 non-null float64
ts2 431941 non-null float64
ts3 431941 non-null float64
dtypes: float64(3)
memory usage: 13.2 MB
In [234]: !rm $path/tstab.h5
①
连接到TsTable对象。
②
数据检索重复多次。
③
起始日值被随机化。
④
最后检索到的DataFrame对象。
结论
基于SQL或关系数据库的方法在处理展示了许多单个对象/表之间关系的复杂数据结构时具有优势。在某些情况下,这可能会使它们在纯NumPy ndarray或pandas DataFrame方法上的性能劣势成为合理。
金融或一般科学中的许多应用领域可以通过主要基于数组的数据建模方法取得成功。在这些情况下,通过利用原生NumPy的 I/O 功能、NumPy和PyTables功能的组合,或通过HDF5-based 存储的pandas方法,可以实现巨大的性能提升。当处理大型(金融)时间序列数据集时,尤其是在“一次写入,多次检索”的场景中,TsTables特别有用。
虽然最近的一个趋势是使用基于商品硬件的大量计算节点组成的云解决方案,特别是在金融背景下,人们应该仔细考虑哪种硬件架构最适合分析需求。微软的一项研究对这个问题有所启发:
我们声称一个“扩展”服务器可以处理这些工作中的每一个,并且在性能、成本、功耗和服务器密度等方面与集群一样好,甚至更好。
Appuswamy 等人(2013 年)
从事数据分析的公司、研究机构等应该首先分析一般情况下必须完成的具体任务,然后根据以下方面的硬件/软件架构做出决策:
扩展
使用具有标准 CPU 和相对较低内存的许多商品节点的集群
扩展
使用一台或多台强大的服务器,配备多核 CPU,可能还有 GPU 甚至 TPU,当机器学习和深度学习发挥作用时,并拥有大量内存。
扩展硬件规模并应用适当的实现方法可能会显著影响性能。下一章将更多地涉及性能。
进一步阅读
本章开头引用的论文以及“结论”部分是一篇不错的文章,也是思考金融分析硬件架构的良好起点:
- Appuswamy,Raja 等人(2013 年):“
没有人因为购买集群而被解雇。”微软研究,英格兰剑桥,http://research.microsoft.com/apps/pubs/default.aspx?id=179615。
通常情况下,网络提供了许多有关本章涵盖主题的宝贵资源:
-
对于使用
pickle对 Python 对象进行序列化,请参阅文档:http://docs.python.org/3/library/pickle.html。 -
关于
NumPy的 I/O 功能概述可在SciPy网站上找到:http://docs.scipy.org/doc/numpy/reference/routines.io.html。 -
对于使用
pandas进行 I/O,请参阅在线文档中的相应部分:http://pandas.pydata.org/pandas-docs/stable/io.html。 -
PyTables首页提供了教程和详细文档:http://www.pytables.org。 -
TsTables的 Github 页面位于https://github.com/afiedler/tstables。
¹ 这里,我们不区分不同级别的 RAM 和处理器缓存。当前内存架构的最佳使用是一个独立的主题。
² 要了解 Python 可用的数据库连接器的概述,请访问https://wiki.python.org/moin/DatabaseInterfaces。与直接使用关系型数据库不同,对象关系映射器,例如SQLAlchemy,通常非常有用。它们引入了一个抽象层,允许更加 Pythonic、面向对象的代码。它们还允许更容易地在后端将一个关系型数据库更换为另一个。
³ 请参阅https://www.sqlite.org/lang.html以了解 SQLite3 语言方言的概述。
⁴ 请参阅http://docs.scipy.org/doc/numpy/reference/arrays.datetime.html。
⁵ 许多其他数据库需要服务器-客户端架构。对于交互式数据和金融分析,基于文件的数据库在一般情况下会更加方便,也足够满足大多数目的。


浙公网安备 33010602011771号