Python-金融编程第二版-全-

Python 金融编程第二版(全)

原文:annas-archive.org/md5/d2f94efd019a2e2cb5c4fa9f260d63c

译者:飞龙

协议:CC BY-NC-SA 4.0

第一部分: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

NumPy 提供了一个多维数组对象来存储同构或异构数据;它还提供了优化的函数/方法来操作这个数组对象。

SciPy

SciPy 是一个子包和函数的集合,实现了科学或金融中经常需要的重要标准功能;例如,可以找到用于三次样条插值以及数值积分的函数。

matplotlib

这是 Python 中最受欢迎的绘图和可视化库,提供了 2D 和 3D 可视化功能。

PyTables

PyTablesHDF5 数据存储库的流行封装器(参见http://www.hdfgroup.org/HDF5/);它是一个用于实现基于磁盘的优化 I/O 操作的库,基于分层数据库/文件格式。

pandas

pandas 建立在 NumPy 之上,提供了更丰富的类来管理和分析时间序列和表格数据;它与 matplotlib 紧密集成用于绘图和 PyTables 用于数据存储和检索。

Scikit-Learn

Scikit-Learn 是一个流行的机器学习(ML)包,为许多不同的 ML 算法提供了统一的 API,例如,用于估计、分类或聚类的算法。

根据特定的领域或问题,这个堆栈会通过额外的库进行扩展,这些库往往有一个共同点,即它们建立在一个或多个基本库的基础之上。然而,在一般情况下,最常见的最小公分母或基本构建块是 NumPyndarray 类(参见 第四章),或者现在是 pandasDataFrame 类(参见 第五章)。

单纯将 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)设置(也见[待续链接]),其中期权的基础风险因素遵循几何布朗运动。

假设我们对估值有以下数值参数值

  • 初始股指水平 S 0 = 100

  • 欧式看涨期权的行权价格 K = 105

  • 到期时间 T = 1 年。

  • 常量,无风险短期利率 r = 0 . 05

  • 常量波动率 σ = 0 . 2

在 BSM 模型中,到期时的指数水平是一个随机变量,由 方程 1-1 给出,其中 z 是一个标准正态分布的随机变量。

方程 1-1. 到期时的 Black-Scholes-Merton (1973) 指数水平。

S T = S 0 exp r - 1 2 σ 2 T + σ T z

以下是蒙特卡洛估值过程的 算法描述

  1. 从标准正态分布中绘制(伪)随机数 I(i) , 其中 z ( i ) , i i n { 1 , 2 , . . . , I }

  2. 计算给定 z ( i ) 和 方程 1-1 的所有到期时指数水平 S T ( i )

  3. 计算到期时期权的所有内部值为 h T ( i ) = max ( S T ( i ) - K , 0 )

  4. 根据 方程 1-2 中给出的蒙特卡罗估计量估算期权现值。

方程 1-2. 欧式期权的蒙特卡罗估计量。

C 0 e -rT 1 I I h T ( i )

现在我们将把这个问题和算法翻译成 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')

这导入了NumPypandas

read_csv允许检索远程存储的数据集。

选择数据的一个子集。

这显示了数据集的一些元信息。

对数收益以矢量化方式计算(“无循环”)。

滚动、年化波动率是由此得出的。

最后一行将这两个时间序列绘制出来。

图 1-1 展示了这个简短交互会话的图形结果。几行代码就足以实现在金融分析中经常遇到的三个相当复杂的任务:数据收集、复杂和重复的数学计算,以及结果的可视化。这个例子说明了pandas使得处理整个时间序列几乎与对浮点数执行数学运算一样简单。

spx 波动率

图 1-1. S&P 500 收盘价和年化波动率

将其翻译成专业的金融背景下,该示例意味着金融分析师可以——当应用正确的 Python 工具和库,提供高级抽象——专注于他们的领域,而不是技术细节。分析师可以更快地做出反应,几乎实时提供有价值的见解,并确保他们领先竞争对手一步。这个提高效率的例子很容易转化为可衡量的底线效应。

确保高性能

总的来说,人们普遍认为 Python 语法相对简洁,编码相对高效。然而,由于 Python 是一种解释语言,导致了“偏见”持续存在,即 Python 通常对金融中的计算密集型任务来说速度太慢了。事实上,根据特定的实现方法,Python 可能确实很慢。但它不一定慢— 它几乎可以在任何应用领域都表现出高性能。原则上,可以区分至少三种不同的策略来提高性能:

范式

通常情况下,Python 中有很多不同的方法可以得到相同的结果,但性能特性却大不相同;“简单地”选择正确的方式(例如特定的库)可以显著提高结果。

编译

如今,有几个性能库可用,它们提供了重要函数的编译版本,或者将 Python 代码静态或动态地(在运行时或调用时)编译为机器代码,速度可以提高数个数量级;其中流行的有CythonNumba

并行化

许多计算任务,特别是在金融领域,都可以从并行执行中获益;这并不是 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 的可能性。

当涉及到金融开发流程时,全球金融机构的实践通常以分离的、两步的过程为特征。一方面,有量化分析师(“量化分析师”)负责模型开发和技术原型设计。他们喜欢使用像MatlabR这样的工具和环境,这些工具和环境允许进行快速、交互式的应用程序开发。在开发工作的这个阶段,性能、稳定性、异常管理、数据访问和分析的分离等问题并不那么重要。主要是在寻找概念验证和/或展示算法或整个应用程序主要期望功能的原型。

一旦原型完成,IT 部门与其开发人员接管并负责将现有的原型代码转换为可靠、可维护和高性能的生产代码。通常,在这个阶段会发生范式转变,使用像C++或Java这样的语言来满足生产的要求。此外,还会应用正式的开发流程,包括专业工具、版本控制等。

这种两步法的做法通常会产生一些通常意义上不期而遇的后果:

低效

原型代码不可重用;算法必须实现两次;冗余的工作需要时间和资源。

多样化的技能组合

不同部门展示出不同的技能集,并使用不同的语言来实现“相同的事物”。

遗留代码

代码可用且必须用不同的语言进行维护,通常使用不同的实现风格(例如,从架构的角度来看)。

另一方面,使用 Python 可以实现从最初的交互式原型设计步骤到高度可靠且高效可维护的生产代码的简化端到端流程。不同部门之间的沟通变得更容易。员工培训也更加简化,因为只有一种主要语言涵盖了金融应用构建的所有领域。它还避免了在开发过程的不同步骤中使用不同技术时固有的低效和冗余。总而言之,Python 几乎可以为金融应用开发和算法实现的几乎所有任务提供一致的技术框架

人工智能优先金融

数据可用性

机器学习和深度学习

传统与人工智能优先金融

结论

Python 作为一种语言——但更多作为一个生态系统——是金融行业的理想技术框架。它具有许多优点,如优雅的语法、高效的开发方法以及适用于原型设计生产等方面的可用性。凭借其大量可用的库和工具,Python 似乎对金融行业的最新发展所提出的大多数问题都有答案,例如分析、数据量和频率、合规性和监管,以及技术本身。它有潜力提供一个单一、强大、一致的框架,可以使端到端的开发和生产工作流程变得更加顺畅,即使是在较大的金融机构之间也是如此。

进一步阅读

以下书籍由同一作者撰写,详细介绍了本章中只是简要提及的许多方面(例如衍生品分析):

本章引用的语录来自以下资源:

  • 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 部署的技术类型:

  • 包管理器:像pipconda这样的包管理器有助于安装、更新和删除 Python 包;它们还有助于不同包的版本一致性

  • 虚拟环境管理器:虚拟环境管理器如virtualenvconda允许并行管理多个 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

安装完成后,除了标准库之外,一些金融分析中最重要的库也可用。

IPython

一个改进的交互式 Python shell

matplotlib

Python 中的标准绘图库

NumPy

高效处理数值数组

pandas

管理表格数据,如金融时间序列数据

PyTables

用于HDF5库的 Python 封装

SciPy

一组科学类和函数(作为依赖项安装)

Seaborn

一个绘图库,添加了统计功能和良好的绘图默认值

这提供了一套用于一般数据分析和金融分析的基本工具集。下面的示例使用 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 安装和通常所需的包(例如 NumPypandas),以及一个密码保护和安全套接字层(SSL)加密的 Jupyter Notebook 服务器安装。作为一个基于 Web 的工具套件,Jupyter Notebook 提供了三个主要工具,可以通过常规浏览器使用:

  • Jupyter Notebook:这是目前非常流行的交互式开发环境,具有不同语言内核的选择,如 Python、R 和 Julia。

  • 终端:通过浏览器访问的系统 shell 实现,允许进行所有典型的系统管理任务,但也可以使用诸如 Vimgit 等有用工具。

  • 编辑器:第三个主要工具是一个基于浏览器的文件编辑器,具有许多不同的编程语言和文件类型的语法突出显示以及典型的编辑功能。

在一个 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.keycert.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 容器的信息请见此处:

  • 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和其他库在其中增添了宝贵的内容。

本章组织如下:

“基本数据类型”

第一节介绍了基本数据类型,如intfloatstring

“基本数据结构”

下一节介绍了Python的基本数据结构(例如list对象)并说明了控制结构、函数式编程范式和匿名函数。

本章的精神是在涉及数据类型和结构时提供对Python特定内容的一般介绍。如果您具备来自其他编程语言(例如CMatlab)的背景,那么您应该能够轻松掌握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 在内部以二进制格式表示;也就是说,十进制数0 < n < 1通过形式为n = x 2 + y 4 + z 8 + . . .的系列表示。对于某些浮点数,二进制表示可能涉及大量元素,甚至可能是一个无限级数。然而,给定用于表示此类数字的固定位数-即表示系列中的固定项数-不准确是其结果。其他数字可以完美表示,因此即使只有有限数量的位可用,它们也会被精确地存储。考虑以下示例:

In [13]: c = 0.5
         c.as_integer_ratio()
Out[13]: (1, 2)

一半,即 0.5,被准确地存储,因为它有一个精确(有限)的二进制表示:\(</mi> <mn>0</mn> <mo>.</mo> <mn>5</mn> <mo>=</mo> <mfrac><mn>1</mn> <mn>2</mn></mfrac> <mi>\) 。然而,对于b = 0.35,我们得到的结果与期望的有理数\(</mi> <mn>0</mn> <mo>.</mo> <mn>35</mn> <mo>=</mo> <mfrac><mn>7</mn> <mn>20</mn></mfrac> <mi>\) 不同:

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 > 34.5 <= 3.25(4 > 3) and (3 > 2),会产生TrueFalse之一作为输出,这是两个重要的 Python 关键字。其他例如defforif。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']

TrueFalsebool数据类型,代表布尔值。以下代码展示了 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 关键字(例如ifwhile)来控制代码流程(本章后面将有更多示例)。

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中表示文本的基本数据类型是stringstring对象具有许多非常有用的内置方法。事实上,当涉及到处理任何类型和任何大小的文本文件时,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]) 如果keependsTrue,则带有行结束符/断行的分隔行
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。

这种对象类型提供的特殊方法仅有两个:countindex。第一个方法统计某个对象的出现次数,第二个方法给出其第一次出现的索引值:

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对象还可以通过不同的方法进行扩展和缩减。换句话说,虽然stringtuple对象是不可变序列对象(具有索引),一旦创建就无法更改,但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替换从ij-1的每个第k个元素
append (x) x附加到对象
count (x) 对象x的出现次数
del l[i:j:k] [i:j:k] 删除索引值为ij-1的元素
extend (s) s的所有元素附加到对象
index (x[, i[, j]]) 元素ij-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

参数是startendstep size

为了比较,相同的循环使用range实现如下:

In [114]: for i in range(2, 5):
              print(l[i] ** 2)

          6.25
          1.0
          2.25

遍历列表

Python中,你可以遍历任意的list对象,不管对象的内容是什么。这通常避免了引入计数器。

Python还提供了典型的(条件)控制元素ifelifelse。它们在其他语言中的使用方式类似:

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对象)。其中包括filtermapreduce。然而,我们首先需要一个函数定义。首先从一个非常简单的函数开始,考虑一个返回输入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 推导和函数式编程工具如 mapfilterreduce 提供了编写没有(显式)循环的代码的方法,这种代码既紧凑又通常更可读。在这种情况下,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'}

st 的全部。

st 中都有。

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解释器已经提供了丰富灵活的数据结构。从金融的角度来看,以下可以被认为是最重要的:

基本数据类型

在金融中,intfloatstring类提供了原子数据类型。

标准数据结构

tuplelistdictset类在金融领域有许多应用领域,其中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中的相似。实际上,CythonPythonC的混合语言。

² 在这里和后续讨论中,诸如floatfloat 对象等术语可互换使用,承认每个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维数组。

线性代数和向量空间理论等数学学科说明了这些数学结构在许多科学学科和领域中的重要性。因此,设计一个专门的数据结构类来方便和高效地处理数组可能是非常有益的。这就是PythonNumPy的作用所在,其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对象,其中数字之间的间隔均匀分布;所使用的参数是startendnum(元素数量)。

使用所有这些函数,我们可以提供以下参数:

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对象(dtypebool)。

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 表示TrueFalse

值是否大于...且小于或等于...?

此类布尔数组可用于索引和数据选择。注意以下操作会展平数据。

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中,函数式编程工具,如mapfilter,提供了一些基本的矢量化手段。然而,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。如果实现允许,数组可以像intfloat对象一样与函数一起使用。考虑以下函数:

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-ordered ndarray 对象的求和在行和列上都更快(绝对速度优势)。

  • 使用 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 类”

本章从使用简单且小的数据集探索pandasDataFrame类的基本特征和能力开始;然后通过使用NumPyndarray对象并将其转换为DataFrame对象来进行处理。

“基本分析” 和 “基本可视化”

本章还展示了基本的分析和可视化能力,尽管后面的章节在这方面更深入。

“Series 类”

本节简要介绍了pandasSeries类,它在某种程度上代表了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类的一些主要特性:

数据

数据本身可以以不同的形状和类型提供(listtuplendarraydict对象都是候选对象)。

标签

数据以列的形式组织,可以具有自定义名称。

索引

存在可以采用不同格式(例如,数字、字符串、时间信息)的索引。

与此类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对应的值。

选择与索引ad对应的两个值。

通过索引位置选择第二行和第三行。

计算单列的总和。

使用apply()方法以向量化方式计算平方。

直接应用向量化,就像使用ndarray对象一样。

NumPyndarray对象相反,可以在两个维度上扩大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对象的数据结构,例如listIndexpandas Index类的一个实例。

表 5-1. DataFrame 函数的参数

参数 格式 描述
data ndarray/dict/DataFrame DataFrame的数据;dict可以包含Seriesndarraylist
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 期数(如果startendNone
freq string/DateOffset 频率字符串,例如,5D代表 5 天
tz string/None 本地化索引的时区名称
normalize bool,默认为None 规范化startend为午夜
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属性或NumPynp.array()函数轻松地从DataFrame生成ndarray对象。

基本分析

NumPyndarray对象一样,pandasDataFrame类内置了许多便利方法。作为入门,考虑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通用函数应用于pandasDataFrame对象,只要它们可以应用于包含相同类型数据的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')

自定义绘图样式。

将四列的累积和绘制成折线图。

pd plot 01

图 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 参数来改变绘图类型。

pd plot 02

图 5-2。DataFrame 对象的柱状图

Series 类

到目前为止,我们主要使用 pandasDataFrame 类。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')

pd plot 03

图 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

这就是对pandasDataFrame对象的介绍。后续部分将使用这个工具集来处理真实世界的金融数据。

复杂选择

数据选择通常通过在列值上制定条件来完成,并可能逻辑地组合多个这样的条件。考虑以下数据集。

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 并不那么方便。在本书的许多后续章节中,pandasDataFrame 类将是核心,当需要时还将使用和说明其他功能。

进一步阅读

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:

自然的思考方式

人类思维通常围绕着现实世界或抽象对象展开,比如汽车或金融工具。面向对象编程适合模拟具有其特征的这类对象。

降低复杂性

通过不同的方法,面向对象编程有助于降低问题或算法的复杂性,并逐个特征进行建模。

更好的用户界面

在许多情况下,面向对象编程可以实现更美观的用户界面和更紧凑的代码。例如,当查看NumPyndarray类或pandasDataFrame类时,这一点变得显而易见。

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

intlist对象是标准的 Python 对象。NumPyndarray对象是一个来自开源包的“自定义”对象。

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

最后,快速查看pandasDataFrame对象,因为其行为大多与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()更新实例属性值。

直接访问实例属性。

封装通常的目标是隐藏用户对类的操作中的数据。添加相应的方法,有时称为gettersetter方法,是实现此目标的一部分。然而,这并不阻止用户直接访问和操作实例属性。这就是私有实例属性发挥作用的地方。它们由两个前导下划线定义。

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 年)

本章介绍了matplotlibplotly库的基本可视化能力。

尽管有许多其他可用的可视化库,但matplotlib已经确立了自己作为基准,并且在许多情况下是一个强大而可靠的可视化工具。在标准绘图方面易于使用,在更复杂的绘图和定制方面灵活。此外,它与NumPypandas及它们提供的数据结构紧密集成。

matplotlib仅允许以位图形式(例如 PNG 或 JPG 格式)生成图。另一方面,现代网络技术允许基于数据驱动文档(D3.js)标准创建漂亮的交互式图表,例如,可以缩放以更详细地检查某些区域。一个非常方便的库,可以使用 Python 创建这样的 D3.js 图表,是plotly。一个小的附加库,称为Cufflinks,将plotlypandasDataFrame对象紧密集成,可以创建最受欢迎的金融图表(如蜡烛图)

本章主要涵盖以下主题:

“静态 2D 绘图”

本节介绍了matplotlib,并呈现了一些典型的 2D 绘图,从最简单的到具有两个比例尺或不同子图的更高级的绘图。

“静态 3D 绘图”

基于matplotlib,介绍了一些在特定金融应用中有用的 3D 绘图。

“交互式 2D 绘图”

本节介绍了plotlyCufflinks,用于创建交互式 2D 绘图。利用CufflinksQuantFigure功能,本节还涉及典型的金融绘图,例如在技术股票分析中使用的绘图。

本章无法全面涵盖使用Pythonmatplotlibplotly进行数据可视化的所有方面,但它提供了这些包在金融领域的基本和重要功能的一些示例。其他示例也可以在后面的章节中找到。例如,第八章更深入地介绍了如何使用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(请参阅,例如,此处的概述)。

一维数据集

在接下来的所有内容中,我们将绘制存储在NumPyndarray对象或pandasDataFrame对象中的数据。然而,matplotlib当然也能够绘制存储在不同Python格式中的数据,比如list对象。最基本但相当强大的绘图函数是plt.plot()。原则上,它需要两组数字:

  • x:包含x坐标(横坐标值)的列表或数组

  • y:包含y坐标(纵坐标值)的列表或数组

提供的xy值的数量必须相匹配,当然了。考虑下面的代码,其输出如图 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 值)。

使用xy对象调用plt.plot()函数。

mpl 01

图 7-1. 绘制给定的 x 和 y 值

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

In [11]: plt.plot(y);
         # plt.savefig('../../images/ch07/mpl_02')

mpl 02

图 7-2. 绘制给定的ndarray对象的数据

NumPy 数组和 matplotlib

您可以简单地将NumPyndarray对象传递给matplotlib函数。matplotlib能够解释数据结构以简化绘图。但是,请注意不要传递过大和/或复杂的数组。

由于大多数ndarray方法再次返回一个ndarray对象,因此您还可以通过附加方法(甚至在某些情况下可以是多个方法)来传递您的对象。通过在样本数据上调用cumsum()方法,我们得到了这些数据的累积和,正如预期的那样,得到了不同的输出(参见图 7-3):

In [12]: plt.plot(y.cumsum());
         # plt.savefig('../../images/ch07/mpl_03')

mpl 03

图 7-3. 绘制给定一个带有附加方法的ndarray对象

通常,默认的绘图样式不能满足报告、出版物等的典型要求。例如,您可能希望自定义使用的字体(例如,与LaTeX字体兼容),在轴上标记标签,或者绘制网格以提高可读性。这就是绘图样式发挥作用的地方(见上文)。此外,matplotlib提供了大量函数来自定义绘图样式。有些函数很容易访问;对于其他一些函数,需要深入挖掘。例如,很容易访问的是那些操作轴的函数以及与网格和标签相关的函数(参见图 7-4):

In [13]: plt.plot(y.cumsum())
         plt.grid(False);  # ①
         # plt.savefig('../../images/ch07/mpl_04')

关闭网格。

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')

mpl 05

图 7-5。带有自定义轴限制的图

为了更好地可读性,图表通常包含许多标签,例如标题和描述xy值性质的标签。这些分别通过函数plt.titleplt.xlabelplt.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 轴上放置一个标签。

放置一个标题。

mpl 06

图 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')

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 中心

mpl 08

图 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')

重新调整第一个数据子集的比例。

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')

定义 figureaxis 对象。

创建共享 x 轴的第二个 axis 对象。

mpl 10

图 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。

mpl 11

图 7-11. 具有两个子图的绘图

matplotlib figure 对象中子图的放置是通过使用特殊的坐标系统来完成的。plt.subplot() 接受三个整数作为参数,分别为 numrowsnumcolsfignum(用逗号分隔或不分隔)。numrows 指定的数量,numcols 指定的数量,而 fignum 指定子图的数量,从 1 开始,以 numrows * numcols 结束。例如,具有九个等大小子图的图形将具有 numrows=3numcols=3fignum=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 子图。

mpl 12

图 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() 函数绘制散点图。

mpl 13

图 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() 函数绘制的散点图。

mpl 14

图 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')

包含了第三个数据集。

选择了颜色映射。

将标记定义为粗点。

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() 函数绘制直方图。

mpl 16

图 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 选项(字符串):barbarstackedstepstepfilled
align 选项(字符串):leftmidright
orientation 选项(字符串):horizontalvertical
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')

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='--')

将线条样式更改为“虚线”。这样,您可以在生成绘图实例(“艺术家对象”)之后轻松更改参数。

mpl 18

图 7-18. 两个数据集的箱线图

作为本节的最后一个示例,请考虑一个在matplotlib 画廊中也可以找到的受数学启发的绘图。它绘制了一个函数并在图形上突出显示了函数下方的区域,从下限到上限 — 换句话说,函数在下限和上限之间的积分值突出显示为一个区域。要说明的积分值是 a b f ( x ) d x,其中f ( x ) = 1 2 · e x + 1a = 1 2b = 3 2。图 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 标签。

mpl 19

图 7-19。指数函数、积分区域和 LaTeX 标签

静态 3D 绘图

在金融领域,确实没有太多领域真正受益于三维可视化。然而,一个应用领域是同时显示一系列到期时间和行权价的隐含波动率的波动率曲面。接下来,代码人工生成类似于波动率曲面的图形。为此,请考虑:

  • 50 到 150 之间的行权价值

  • 0.5 到 2.5 年的到期时间

这提供了一个二维坐标系。NumPynp.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 标签。

这将创建一个色标。

mpl 20

图 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 散点图。

mpl 21

图 7-21. (虚拟的) 隐含波动率的 3D 散点图

交互式 2D 绘图

matplotlib 允许创建静态位图对象或 PDF 格式的绘图。如今,有许多可用于基于 D3.js 标准创建交互式绘图的库。这些绘图可以实现缩放和悬停效果以进行数据检查,还可以轻松嵌入到网页中。

一个流行的平台和绘图库是Plotly。它专门用于数据科学的可视化,并在全球范围内广泛使用。Plotly 的主要优点是其与 Python 生态系统的紧密集成以及易于使用 — 特别是与 pandasDataFrame 对象和包装器包 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的流图服务之类的一些功能仅通过与服务器通信才可用。

后续示例再次依赖随机数,这次存储在具有DatetimeIndexDataFrame对象中,即作为时间序列数据。

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对象。

前五行的数据。

CufflinksDataFrame类添加了一个新方法:df.iplot()。此方法在后台使用Plotly创建交互式图。本节中的代码示例都利用了将交互式图下载为静态位图的选项,然后将其嵌入到文本中。在 Jupyter Notebook 环境中,创建的绘图都是交互式的。下面代码的结果显示为<<>>。

In [50]: plyo.iplot(  # ①
             df.iplot(asFigure=True),  # ②
             # image ='png', # ③
             filename='ply_01'  # ④
         )

这利用了Plotly的离线(笔记本模式)功能。

使用参数asFigure=True调用df.iplot()方法以允许本地绘图和嵌入。

image选项还提供了绘图的静态位图版本。

指定要保存的位图的文件名(文件类型扩展名会自动添加)。

ply 01

图 7-22. 使用PlotlypandasCufflinks绘制时间序列数据的线图

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 标签。

按列定义绘图模式(线条、标记等)。

按列定义要用作标记的符号。

为所有标记固定大小。

按列指定绘图颜色

ply 02

图 7-23. DataFrame 对象的两列线图及自定义

matplotlib 类似,Plotly 允许使用多种不同的绘图类型。通过 Cufflinks 可用的绘图有:chart, scatter, bar, box, spread, ratio, heatmap, surface, histogram, bubble, bubble3d, scatter3d, scattergeo, ohlc, candle, piechoroplet。作为与线图不同的绘图类型的示例,请考虑直方图(参见[链接即将到来]):

In [52]: plyo.iplot(
             df.iplot(kind='hist',  # ①
                      subplots=True,  # ②
                      bins=15,  # ③
                      asFigure=True),
             # image ='png',
             filename='ply_03'
         )

指定绘图类型。

每列需要单独的子图。

设置 bins 参数(要使用的桶=要绘制的条形图)。

ply 03

图 7-24. DataFrame 对象的每列直方图

金融图表

当处理金融时间序列数据时,PlotyCufflinkspandas 的组合特别强大。Cufflinks 提供了专门的功能,用于创建典型的金融图,并添加典型的金融图表元素,例如相对强度指标(RSI),这只是一个例子。为此,创建了一个持久的 QuantFig 对象,可以像使用 CufflinksDataFrame 对象一样绘制。

此子部分使用真实的金融数据集:欧元/美元汇率的时间序列数据(来源: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 构造函数。

添加图标题。

图例放置在图的顶部。

这给数据集起了个名字。

qf 01

图 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 带的周期数。

用于带宽的标准偏差数。

qf 02

图 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 周期。

不显示上限或下限带。

qf 03

图 7-27. EUR/USD 数据的 OHLC 图,带有 Bollinger 带和 RSI

结论

当涉及到 Python 中的数据可视化时,matplotlib 可以被认为是基准和工作马。它与 NumPypandas 紧密集成。基本功能易于方便地访问。然而,另一方面,matplotlib 是一个相当强大的库,具有一种相对复杂的 API。这使得在本章中无法对 matplotlib 的所有功能进行更广泛的概述。

本章介绍了在许多金融背景下有用的 matplotlib 的 2D 和 3D 绘图的基本功能。其他章节提供了如何在可视化中使用这个基本库的更多示例。

除了 matplotlib,本章还涵盖了 PlotlyCufflinks 的组合。这种组合使得创建交互式 D3.js 图表成为一件方便的事情,因为通常只需在 DataFrame 对象上进行一次方法调用。所有的技术细节都在后端处理。此外,Cufflinks 通过 QuantFig 对象提供了一种创建带有流行金融指标的典型金融图表的简单方法。

进一步阅读

matplotlib 的主要资源可以在网络上找到:

现在已经成为一种标准的例程去参考画廊,寻找合适的可视化示例,并从相应的示例代码开始。

PlotlyCufflinks 的主要资源也可以在线找到:

¹ 想了解可用的绘图类型概述,请访问matplotlib gallery

第八章:金融时间序列

时间的唯一目的是让一切不是同时发生。

阿尔伯特·爱因斯坦

金融时间序列数据是金融领域最重要的数据类型之一。这是按日期和/或时间索引的数据。例如,随着时间的推移,股票价格代表金融时间序列数据。类似地,随时间变化的欧元/美元汇率代表金融时间序列;汇率在较短的时间间隔内报价,一系列这样的报价则是汇率的时间序列。

没有任何金融学科能够不考虑时间因素而存在。这与物理学和其他科学基本相同。在 Python 中处理时间序列数据的主要工具是pandaspandas的原始和主要作者 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');

前五行 …

… 最后五行显示。

这通过多个子图可视化完整数据集。

fts 01

图 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()计算两个索引值之间的百分比变化。

结果的均值作为条形图可视化。

fts 02

图 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()应用于结果。

fts 03

图 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()应用于结果。

fts 04

图 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 个数据行的三个滚动统计。

将原始时间序列数据添加到图表中。

fts 05

图 8-5。最小、平均、最大值的滚动统计

技术分析示例

与基本分析相比,滚动统计是所谓的技术分析中的主要工具,基本分析侧重于财务报告和被分析股票公司的战略位置等方面。

基于技术分析的几十年历史的交易策略基于两个简单移动平均线(SMAs)。这个想法是,当短期 SMA 高于长期 SMA 时,交易员应该持有一支股票(或者一般的金融工具),当情况相反时,应该空仓。这些概念可以通过pandasDataFrame对象的功能来精确描述。

当给定了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 时间序列。

fts 06

图 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)。

fts 07

图 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');

fts 08

图 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');

fts 09

图 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');

fts 10

图 8-10。S&P 500 和 VIX 的对数收益随时间变化

在这种情况下,pandasscatter_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

放置在对角线上的内容;这里是列数据的直方图。

这些是要传递给直方图绘图函数的关键字。

fts 11

图 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 回归。

这将对数收益绘制为散点图…

… 添加了线性回归线。

fts 12

图 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的相关矩阵。

这会绘制随时间变化的滚动相关性……

… 并将静态值添加到绘图中作为水平线。

fts 13

图 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价格。

fts 14

图 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');

fts 15

图 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 及其科学堆栈库(如NumPypandasPyTables)的甜蜜点。这样大小的数据集也可以在内存中进行分析,利用今天的 CPU 和 GPU 通常会获得较高的速度。但是,必须将数据读入 RAM 并将结果写入磁盘,同时确保满足今天的性能要求。

本章涉及以下主题:

“使用 Python 进行基本 I/O”

Python 具有内置函数,可以对任何对象进行序列化并将其存储到磁盘上,然后从磁盘中读取到 RAM 中;除此之外,在处理文本文件和SQL数据库时,Python 也很强大。NumPy还提供了专用函数,用于快速二进制存储和检索ndarray对象。

“使用 pandas 进行 I/O”

pandas库提供了丰富的便利函数和方法,用于读取存储在不同格式中的数据(例如,CSVJSON)并将数据写入不同格式的文件。

“使用 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)。

从磁盘读取对象并进行反序列化。

ab 转换为 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

序列化 andarray 版本并保存。

序列化 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对象。在某些情况下,这节省了很多工作,比如当你必须将NumPydtype对象转换为特定的数据库类型时(例如对于SQLite3)。为了说明NumPy有时可以有效替代基于SQL的方法,以下代码复制了之前使用NumPy的示例。

代码使用NumPynp.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作为dtypendarray对象。

用于记录数组的特殊dtype对象。

用特殊dtype实例化的ndarray对象。

这将填充Date列。

假数据集……

…这填充了No1No2列。

记录数组的大小(以字节为单位)。

保存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 文件)

  • JSONJavaScript 对象表示法

  • HTML(超文本标记语言)

表 9-1 列出了 pandasDataFrame 类的支持格式以及相应的导入和导出函数/方法。导入函数所接受的参数在 [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 对象。

绘制查询结果的子集。

io 01

图 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

将表的所有行读入名为 dataDataFrame 对象中。

数据现在在内存中。这样可以进行更快的分析。加速通常是一个数量级或更多。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');

两个条件逻辑上组合。

逻辑上组合了四个条件。

io 02

图 9-2. 查询结果的散点图(选择)

预期地,使用 pandas 的内存分析能力会显著加速,只要 pandas 能够复制相应的 SQL 语句。

使用 pandas 的另一个优点不仅仅是这个,因为 pandasPyTables 等紧密集成 — 后续部分的主题。在这里,知道它们的组合可以显著加速 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 对象的形式将其再次读入内存。

io 03

图 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 对象的形式将其再次读入内存,同时指定要从中读取的工作表。

io 04

图 9-4. 所有列的线性图

生成包含较小数据子集的 Excel 电子表格文件需要相当长的时间。这说明了电子表格结构所带来的额外开销。

对生成的文件进行检查后发现,DataFrameHDFStore 结合是最紧凑的选择(使用压缩,正如本章后面所述,进一步增加了优势)。与文本文件相比,作为 CSV 文件的相同数量的数据的大小要大一些。这是处理 CSV 文件时性能较慢的另一个原因,另一个原因是它们只是“普通”文本文件。

使用 PyTables 进行快速 I/O

PyTablesHDF5数据库标准的 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 对象绘制列。

io 05

图 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 对象。

io 06

图 9-6. 列数据的直方图

快速查询

pandasPyTables都能够处理相对复杂的、类似SQL的查询和选择。它们在执行此类操作时都进行了速度优化。但是,与关系型数据库相比,这些方法当然存在限制。但对于大多数数值和金融应用程序,它们通常并不决定性。

正如以下示例所示,使用存储在PyTables中的数据作为Table对象让您感觉就像是在NumPypandas中工作且是内存中的,从语法性能方面都是如此:

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. 示例数学表达式

y = 3 sin ( x ) + | x |

结果存储在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。

由于TsTablespandas 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')

io 07

图 9-7. 金融时间序列的选定数据点

数据存储

TsTables基于特定的基于块的结构存储金融时间序列数据,该结构允许根据某个时间间隔快速检索任意数据子集。为此,该软件包将create_ts()函数添加到PyTables中。以下代码使用了来自PyTablesclass基于描述方法,基于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对象有几十万行数据。

io 08

图 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 ndarraypandas DataFrame方法上的性能劣势成为合理。

金融或一般科学中的许多应用领域可以通过主要基于数组的数据建模方法取得成功。在这些情况下,通过利用原生NumPy的 I/O 功能、NumPyPyTables功能的组合,或通过HDF5-based 存储的pandas方法,可以实现巨大的性能提升。当处理大型(金融)时间序列数据集时,尤其是在“一次写入,多次检索”的场景中,TsTables特别有用。

虽然最近的一个趋势是使用基于商品硬件的大量计算节点组成的云解决方案,特别是在金融背景下,人们应该仔细考虑哪种硬件架构最适合分析需求。微软的一项研究对这个问题有所启发:

我们声称一个“扩展”服务器可以处理这些工作中的每一个,并且在性能、成本、功耗和服务器密度等方面与集群一样好,甚至更好。

Appuswamy 等人(2013 年)

从事数据分析的公司、研究机构等应该首先分析一般情况下必须完成的具体任务,然后根据以下方面的硬件/软件架构做出决策:

扩展

使用具有标准 CPU 和相对较低内存的许多商品节点的集群

扩展

使用一台或多台强大的服务器,配备多核 CPU,可能还有 GPU 甚至 TPU,当机器学习和深度学习发挥作用时,并拥有大量内存。

扩展硬件规模并应用适当的实现方法可能会显著影响性能。下一章将更多地涉及性能。

进一步阅读

本章开头引用的论文以及“结论”部分是一篇不错的文章,也是思考金融分析硬件架构的良好起点:

通常情况下,网络提供了许多有关本章涵盖主题的宝贵资源:

¹ 这里,我们不区分不同级别的 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

⁵ 许多其他数据库需要服务器-客户端架构。对于交互式数据和金融分析,基于文件的数据库在一般情况下会更加方便,也足够满足大多数目的。

posted @ 2025-11-18 09:33  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报