SciPy-数值与科学计算学习指南-全-

SciPy 数值与科学计算学习指南(全)

原文:zh.annas-archive.org/md5/8f71309a5888f7b3ef5b7ba2c6de32e9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在保持第一版主要结构的同时,学习 SciPy 进行数值和科学计算的修订版包括了一套配套的 IPython Notebooks。这将帮助学生、研究人员和从业者修改和整合到他们自己的工作中,书中提供的经过测试的代码片段,作为教学策略。这还将展示 SciPy 通过 Python 计算机语言提供的独特灵活性,为任何对通过数值计算感兴趣的人带来计算能力。

然而,我们应该提到,只有当它们与书中相应的章节一起阅读时,IPython Notebooks 才会对刚开始这个领域的人有意义,这有助于您在了解 SciPy 模块的条件和限制的同时,发展使用 SciPy 解决大规模数值问题的技能。当然,对于已经具备知识的读者来说,当他们遇到已知的内容时,会感到愉悦,但也会被挑战去设计出以书中相同清晰度完成相同任务的方法,这些任务用于说明 SciPy 的功能。

SciPy 多年来一直是许多科学家首选的计算环境的重要组成部分。我们今天面临的挑战之一是汇集来自不同背景、技术和软件(从纯数学家到资深工程师)的专业人士,让他们能够在各自的工作环境中独立贡献。

在 Python 中,SciPy 是一个完美的平台,用于在平稳、可靠和协调的环境中协调项目。它允许轻松完成大多数任务;原因在于许多专门的软件工具可以轻松集成到 SciPy 的核心功能中,因此,与非 Python 软件包和工具的接口正变得越来越简单。

总结来说,本书展示了迄今为止最稳健的编程环境。我们将向您展示如何从基本数据处理到通过不同科学和工程分支的示例进行非常详细的阐述,来使用这个系统。

本书涵盖的内容

第一章,SciPy 简介,展示了使用 Python、NumPy、SciPy 和 matplotlib 的组合作为科学目的编程环境的优势。您将学习如何安装、测试和探索这些环境,使用它们进行快速计算,并找到一些寻找帮助的好方法。本书还简要介绍了如何打开随书附带的 IPython Notebooks。

第二章,作为 SciPy 入门的 NumPy 数组操作,深入探讨了 SciPy 使用的对象数组的创建和基本操作,作为 NumPy 库的概述。

第三章,线性代数中的 SciPy,涵盖了 SciPy 在处理大型矩阵中的应用,包括求解系统或计算特征值和特征向量。

第四章,数值分析中的 SciPy,无疑是本书中最有趣的章节之一。它详细介绍了函数(一个或多个变量)的定义和操作、求根、极值(优化)、导数计算、积分、插值、回归以及应用于常微分方程求解的应用。

第五章,信号处理中的 SciPy,探讨了信号(在任何维度上)的构建、获取、质量提升、压缩和特征提取。它包含了来自图像处理领域的精美且有趣的示例。

第六章,数据挖掘中的 SciPy,涵盖了 SciPy 在数据收集、组织、分析和解释中的应用,示例取自统计学和聚类。

第七章,计算几何中的 SciPy,探讨了点三角剖分、凸包、Voronoi 图及其应用,包括在矩形网格中通过有限元方法求解二维拉普拉斯方程。在本书的这一部分,您将能够结合前几章的所有技术,轻松展示使用 SciPy 进行的尖端研究,我们将探讨来自材料科学和实验物理的一些优秀示例。

第八章,与其他语言的交互,介绍了 SciPy 的主要优势之一——与其他语言(如 C/C++、Fortran、R 和 MATLAB/Octave)交互的能力。

您需要这本书什么

要使用本书中的示例和尝试代码,您只需要安装最新的 Python(2.7 或更高版本)以及 NumPy、SciPy 和 matplotlib 库。本书中提供了安装所有这些库的食谱。

这本书面向谁

本书面向了解 Python 的科学家、工程师、程序员或分析师。对于某些章节,需要具备一定的线性代数、微积分和一些统计学知识才能理解某些概念,但除此之外,本书大部分内容都是自包含的。

习惯用法

在本书中,您将找到多种文本样式,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:"我们可以通过使用include指令来包含其他上下文。"。

代码块设置如下:

solve(A, b, sym_pos=False, lower=False, overwrite_a=False, overwrite_b=False, debug=False)
spsolve(A, b[, permc_spec, use_umfpack])

具备所需背景知识的读者应能识别出 Python 提示符>>>后跟一个空格,然后是代码字段。任何命令行输入或输出都应如下编写:

>>> from scipy import stats
>>> result=scipy.stats.bayes_mvs(scores)

新术语重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中将如下显示:"点击下一步按钮将您带到下一屏幕"。

注意

警告或重要提示将以如下框中的形式出现。

小贴士

小技巧和技巧将以如下形式出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们来说非常重要,以便我们开发出您真正能从中获得最大收益的标题。

如要向我们发送一般反馈,请简单地将电子邮件发送至 <feedback@packtpub.com>,并在邮件主题中提及书名。

如果您在某个主题领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

下载本书的彩色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/7702OS_ColoredImages.pdf下载此文件。

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入你的勘误详情。一旦你的勘误得到验证,你的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。任何现有勘误都可以通过从 www.packtpub.com/support 选择你的标题来查看。

盗版

互联网上版权材料的盗版是所有媒体中持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可证。如果你在互联网上发现任何我们作品的非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢你在保护我们作者和我们提供有价值内容的能力方面提供的帮助。

询问

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章。SciPy 简介

毫无疑问,21 世纪的科学家劳动比前几代人更为全面和跨学科。科学界的成员以更大的团队形式连接,共同致力于以任务为导向的目标,并在各自的领域内合作。这种研究范式也反映在研究人员使用的计算资源上。研究人员不再局限于一种商业软件、操作系统或供应商,而是受到研究机构和开源社区提供的开源贡献的启发和测试;研究工作经常跨越各种平台和技术。

本书介绍了迄今为止高度认可的开放源代码编程环境——一个基于计算机语言 Python 的两个库的系统:NumPySciPy。在接下来的章节中,我们将通过科学和工程领域的示例来指导您使用这个系统。

SciPy 是什么?

理想的可计算数学编程环境具有以下特点:

  • 它必须基于一种允许用户快速工作并有效集成系统的计算机语言。理想情况下,计算机语言应可移植到所有平台:Windows、Mac OS X、Linux、Unix、Android 等。这对于促进不同资源和使用能力的科学家之间的合作至关重要。它必须包含一套强大的库,允许以简单有效的方式获取、存储和处理大量数据集。这是核心——允许进行模拟和大规模使用数值计算。

  • 与其他计算机语言以及第三方软件的平滑集成。

  • 除了运行编译后的代码,编程环境还应允许进行交互式会话以及脚本能力,以便快速实验。

  • 应支持不同的编码范式——命令式、面向对象和/或函数式编码风格。

  • 它应该是一个开源软件,允许用户访问原始数据代码,并允许用户在需要时修改基本算法。在商业软件中,改进算法的包含由卖家决定,并且通常会给最终用户带来成本。在开源领域,社区通常进行这些改进,并在发布时发布新版本——无需付费。

  • 应用程序集不应仅限于简单的数值计算;它应该足够强大,允许进行符号计算。

在科学界使用的最知名数值计算环境之一是MATLAB,这是一个商业软件,价格昂贵,并且不允许对代码进行任何篡改。MapleMathematica则更侧重于符号计算,尽管它们可以匹配 MATLAB 中的许多数值计算。然而,这些也是商业软件,价格昂贵,且不开放修改。基于类似数学引擎的 MATLAB 的合理替代品是GNU Octave 系统。大多数 MATLAB 代码可以轻松移植到 Octave,它是开源的。不幸的是,其配套的编程环境并不十分用户友好,而且也非常限制于数值计算。一个结合了所有优点的是 Python,它带有开源库 NumPy 和 SciPy 进行数值操作。无疑,吸引用户使用 Python 的第一个特性是其代码的可读性。其语法极其清晰和表达性强。它支持用不同范式编写的代码:面向对象、函数式或传统的命令式。它允许打包 Python 代码,并通过py2exepyinstallercx_Freeze库将它们作为独立的可执行程序运行,但也可以用作交互式编程语言或脚本语言。这在开发符号计算工具时是一个巨大的优势。因此,Python 已经成为 Maple 和 Mathematica 的强劲竞争对手:开源数学软件Sage代数和几何实验系统)。

NumPy 是 Python 的一个开源扩展,它增加了对大型多维数组的支持。这种支持允许获取、存储和复杂操作之前提到的数据。仅凭 NumPy 本身就是一个解决许多数值计算问题的强大工具。

在 NumPy 之上,我们还有另一个开源库 SciPy。这个库包含用于操纵 NumPy 对象(具有明确的科学和工程目标)的算法和数学工具。

Python、NumPy 和 SciPy(以下简称为“SciPy”)的组合已经多年成为许多应用数学家的首选环境;我们每天与纯数学家和硬核工程师一起工作。这个行业的挑战之一是将不同视野、技术、工具和软件的专业人士的科学产出集中在一个工作站上。SciPy 是协调计算顺畅、可靠和一致性的完美解决方案。

我们经常需要编写并执行脚本,例如,使用 SciPy 本身、C/C++、Fortran 和/或 MATLAB 的组合进行实验。通常,我们会从一些信号采集设备接收大量数据。从所有这些异构材料中,我们使用 Python 来检索和处理数据,一旦分析完成,就生成高质量的文档,其中包含专业外观的图表和可视化辅助工具。SciPy 使得执行所有这些任务变得轻松。

这部分是因为许多专门的软件工具可以轻松扩展 SciPy 的核心功能。例如,尽管绘图和绘图通常由matplotlib的 Python 库处理,但还有其他可用的包,例如Biggles (biggles.sourceforge.net/)、Chaco (pypi.python.org/pypi/chaco)、HippoDraw (github.com/plasmodic/hippodraw)、用于3D渲染的MayaVi (mayavi.sourceforge.net/)、Python Imaging LibraryPIL (pythonware.com/products/pil/),以及在线分析和数据可视化工具Plotly (plot.ly/)。

与非 Python 包的接口也是可能的。例如,SciPy 与 R 统计软件包的交互可以通过RPy (rpy.sourceforge.net/rpy2.html)来实现。这允许进行更稳健的数据分析。

安装 SciPy

在本书编写时,Python 的稳定生产版本是 2.7.9 和 3.4.2。尽管如此,如果用户需要与第三方应用程序通信,Python 2.7 仍然更方便。Python 2 不再计划发布新版本;Python 3 被认为是 Python 的现在和未来。出于 SciPy 应用的目的,我们建议您保留 2.7 版本,因为还有一些使用 SciPy 的包尚未移植到 Python 3。尽管如此,本书的配套软件已在 Python 2.7 和 Python 3.4 上进行了测试。

Python 软件包可以从官方网站(www.python.org/downloads/)下载,并可以安装在所有主要系统上,如 Windows、Mac OS X、Linux 和 Unix。它也已移植到其他平台,包括 Palm OS、iOS、PlayStation、PSP、Psion 等。

以下截图显示了在 iPad 上使用 Python 进行编码的两个流行选项——PythonMathSage Math。虽然第一个应用程序仅允许使用简单的数学库,但第二个应用程序允许用户远程加载和使用 NumPy 和 SciPy。

安装 SciPy

PythonMathSage Math 将 Python 编码带到了 iOS 设备上。Sage Math 允许导入 NumPy 和 SciPy。

我们不会详细介绍如何在您的系统上安装 Python,因为我们已经假设您熟悉这门语言。如果有疑问,我们建议浏览优秀的书籍 Expert Python Programming,作者 Tarek Ziadé,由 Packt Publishing 出版,其中详细解释了在不同系统上安装许多不同实现的方法。通常,遵循官方 Python 网站上的说明是个好主意。我们还将假设您熟悉在 Python 中进行交互式会话以及编写独立脚本。

NumPy 和 SciPy 的最新库可以从官方 SciPy 网站下载 (scipy.org/)。它们都需要 Python 版本 2.4 或更高,所以我们现在应该处于良好的状态。我们可以选择从 SourceForge (sourceforge.net/projects/scipy/)、Gohlke (www.lfd.uci.edu/~gohlke/pythonlibs/) 或 Git 仓库(例如,来自 stronginference.com/ScipySuperpack/superpack)下载软件包。

在某些系统中,也可以使用预包装的可执行捆绑包来简化过程,例如 Anaconda (store.continuum.io/cshop/anaconda/) 或 Enthought (www.enthought.com/products/epd/) Python 发行版。在这里,我们将向您展示如何在最常见的情况下在各种平台上下载和安装 Scipy。

在 Mac OS X 上安装 SciPy

在 Mac OS X 上安装 SciPy 时,您在系统上安装它之前必须考虑一些标准。这有助于 SciPy 的顺利安装。以下是需要注意的事项:

  • 例如,在 Mac OS X 上,如果已安装 MacPorts,则过程将非常简单。以超级用户身份打开终端,并在提示符(%)下输入以下命令:

    % port search scipy
    
    
  • 这列出了所有安装 SciPy 或将 SciPy 作为依赖项的端口。对于 Python 2.7,我们需要安装 py27-scipy,输入以下命令:

    % port install py27-scipy
    
    

几分钟后,库就正确安装并准备好使用了。注意 macports 还会为我们安装所有需要的依赖项(包括 NumPy 库),而无需我们做任何额外的工作。

在 Unix/Linux 上安装 SciPy

在任何其他 Unix/Linux 系统中,如果没有任何端口可用或用户更喜欢从 SourceForge 或 Git 下载的软件包中安装,则只需执行以下步骤:

  1. 按照官方页面上的推荐,解压 NumPy 和 SciPy 软件包。这将创建两个文件夹,每个库一个。

    在终端会话中,切换到存储 NumPy 库的文件夹,其中包含setup.py文件。找出你正在使用的 Fortran 编译器(gnugnu95fcompiler之一),然后在提示符下输入以下命令:

    % python setup.py build –fcompiler=<compiler>
    
    
  2. 一旦构建完成,在同一文件夹下,输入安装命令。这应该就足够了:

    % python setup.py install
    
    

在 Windows 上安装 SciPy

你可以通过多种方式在 Windows 上安装 Scipy。以下是一些推荐的方法,你可能想看看:

  • 在 Microsoft Windows 下,我们建议你从 Anaconda 或 Enthought Python 发行版提供的二进制安装程序中进行安装。然而,请注意内存要求。或者,你可以下载并安装 SciPy 堆栈或库。

  • SciPy 库的安装过程与上述过程完全相同,即在 Unix/Linux 下下载和构建后再安装,或在 Microsoft Windows 下下载和运行。请注意,不同的 Python 实现可能对安装 NumPy 和 SciPy 有不同的要求。

测试 SciPy 安装

如你所知,计算机系统并非完美无缺。因此,在开始使用 SciPy 进行计算之前,需要确保其运行正常。为此,SciPy 的开发者提供了一套测试程序,任何 SciPy 用户都可以执行这些测试来确保所使用的 SciPy 运行良好。这样,在使用 SciPy 提供的任何函数时发生错误时,可以节省大量的调试时间。

要运行测试程序,在 Python 提示符下,可以运行以下命令:

>>> import scipy
>>> scipy.test()

小贴士

下载示例代码

你可以从www.packtpub.com购买的所有 Packt 书籍的账户中下载示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

读者应该知道,执行此测试需要一些时间才能完成。它应该以类似以下内容结束:

测试 SciPy 安装

这意味着在基本层面上,你的 SciPy 安装是正常的。最终,测试可能以以下形式结束:

测试 SciPy 安装

在这种情况下,需要仔细检查错误和失败的测试。获取帮助的一个地方是 SciPy 邮件列表(mail.scipy.org/pipermail/scipy-user/),你可以订阅该列表。我们包括了一个 Python 脚本,读者可以使用它来运行这些测试,这些测试可以在本书的配套软件中找到。

SciPy 组织

SciPy 被组织成一系列模块。我们喜欢将每个模块视为数学的不同领域。因此,每个模块都有其特定的技术和工具。您可以在docs.scipy.org/doc/scipy-0.14.0/reference/py-modindex.html找到包含在 SciPy 中的一些不同模块的列表。

让我们使用其中的一些函数来解决一个简单的问题。

下表显示了 31 个人的智商测试分数:

114 100 104 89 102 91 114 114
103 105 108 130 120 132 111 128
118 119 86 72 111 103 74 112
107 103 98 96 112 112 93

这些 31 个分数的茎叶图(请参考本章的 IPython 笔记本)显示,没有明显偏离正态分布,因此我们假设分数的分布接近正态分布。现在,使用 99%的置信区间来估计这个群体的平均智商分数。

我们首先将数据加载到内存中,如下所示:

>>> import numpy
>>> scores = numpy.array([114, 100, 104, 89, 102, 91, 114, 114, 103, 105, 108, 130, 120, 132, 111, 128, 118, 119, 86, 72, 111, 103, 74, 112, 107, 103, 98, 96, 112, 112, 93])

在这一点上,如果我们输入 dir(scores),然后按 return 键,接着按一个点(.),再按 tab 键;系统会列出从 NumPy 库继承的所有可能的方法,这在 Python 中是惯例。技术上,我们可以继续计算所需的 meanxmean 和相应的置信区间,根据公式 xmean ± zcrit * sigma / sqrt(n),其中 sigman 分别是数据的标准差和大小,而 zcrit 是与置信度相对应的临界值(en.wikipedia.org/wiki/Confidence_interval)。在这种情况下,我们可以在任何统计书籍的表格中查找其值的粗略近似,zcrit = 2.576。其余的值可以在我们的会话中计算并适当组合,如下所示:

>>> import scipy
>>> xmean = scipy.mean(scores)
>>> sigma = scipy.std(scores)
>>> n = scipy.size(scores)
>>> xmean, xmean - 2.576*sigma /scipy.sqrt(n), \
 xmean + 2.576*sigma / scipy.sqrt(n)

输出如下所示:

(105.83870967741936, 99.343223715529746, 112.33419563930897)

因此,我们已经计算出了估计的平均智商分数(值为 105.83870967741936)和置信区间(从大约 99.34 到约 112.33)。我们使用纯 SciPy 操作并遵循已知公式来完成这些计算。但与其手动进行所有这些计算并在表格中查找临界值,我们只需询问 SciPy。

注意,在使用 scipy.stats 模块中的任何函数之前,需要先加载该模块:

>>> from scipy import stats
>>> result=scipy.stats.bayes_mvs(scores)

变量 result 包含我们问题的解决方案和一些附加信息。请注意,根据 help 文档的说明,result 是一个包含三个元素的元组:

>>> help(scipy.stats.bayes_mvs)

这个命令的输出将取决于 SciPy 的安装版本。它可能看起来像这样(运行本章的配套 IPython 笔记本以查看您系统的实际输出,或在 Python 控制台中运行该命令):

SciPy 组织结构

我们的解决方案是元组 result 的第一个元素;要查看其内容,请输入:

>>> result[0]

输出如下所示:

(105.83870967741936, (101.48825534263035, 110.18916401220837))

注意这个输出给出了与之前相同的平均值,但由于 SciPy(输出可能取决于您计算机上可用的 SciPy 版本)的计算更准确,因此置信区间略有不同。

如何查找文档

在线有大量信息,无论是来自 SciPy 的官方页面(尽管其参考指南可能有些不完整,因为它是一个正在进行中的工作),还是来自许多其他在论坛、YouTube 或个人网站上提供教程的贡献者。一些开发者还在线发布了他们工作的详细示例。

正如我们之前看到的,从我们的交互式 Python 会话中获取帮助也是可能的。NumPy 和 SciPy 库大量使用 docstrings,这使得使用常规 Python 帮助系统请求使用说明和建议变得简单。例如,如果对 bayes_mvs 例程的使用有疑问,用户可以发出以下命令:

>>> import scipy.stats
>>> help(scipy.stats.bayes_mvs)

执行此命令后,系统将提供必要的信息。等价地,NumPy 和 SciPy 都自带了它们自己的帮助系统,info。例如,查看以下命令:

>>> import numpy
>>> numpy.info('random')

这将提供从与给定关键字关联的 NumPy 库的所有 docstrings 内容中解析出的所有信息的摘要。用户可以上下滚动输出,而不会进一步交互。

如果我们不确定函数的使用,但已经知道我们想要使用它,这很方便。但是,如果我们不知道这个程序的存在,并且怀疑它可能存在,我们应该怎么办?Python 的常规方法是调用模块上的 dir() 命令,它列出了所有可能的属性。

交互式 Python 会话使得在帮助会话的输出中进行搜索变得更容易,并且可以在输出中导航和执行进一步的搜索。例如,在提示符中输入以下命令:

>>> import scipy.stats
>>> help(scipy.stats)

此命令的输出将取决于 SciPy 的安装版本。它可能看起来像这样(运行本章的配套 IPython Notebook 以查看您系统的实际输出,或在 Python 控制台中运行该命令):

如何查找文档

注意屏幕末尾的冒号(:)——这是一个老式的提示符。系统处于待机模式,等待用户发出命令(以单个键的形式)。这也表明在给定文本之后还有几页帮助。如果我们打算阅读帮助文件的其余部分,我们可以按空格键滚动到下一页。

以这种方式,我们可以访问以下关于此主题的手册页面。也可以通过逐行滚动使用上下箭头键导航手册页面。当我们准备好退出帮助会话时,我们只需简单地按(键盘字母)Q

还可以在帮助内容中搜索给定的字符串。在这种情况下,在提示符下,我们按 (/) 斜杠键。提示符从冒号变为斜杠,然后我们输入我们想要搜索的关键字。

例如,是否存在一个 SciPy 函数可以计算给定数据集的皮尔逊峰度?在斜杠提示符下,我们输入 kurtosis 并按 enter。帮助系统带我们到该字符串的第一个出现位置。要访问字符串 kurtosis 的后续出现,我们按 N 键(表示下一个)直到找到所需的内容。在那个阶段,我们继续退出这个帮助会话(通过按 Q),并请求有关该函数的更多信息:

>>> help(scipy.stats.kurtosis)

此命令的输出将取决于 SciPy 的安装版本。它可能看起来像这样(运行本章的配套 IPython Notebook 以查看您系统的实际输出,或在 Python 控制台中运行该命令):

如何查找文档

科学可视化

在这一点上,我们想向您介绍另一个我们将使用来生成图表的资源,即 matplotlib 库。它可以从其官方网站 matplotlib.org/ 下载,并按照标准的 Python 命令进行安装。官方网站上有很好的在线文档,我们鼓励读者比本书中我们将使用的几个命令更深入地挖掘。例如,由 Sandro Tosi 编写的优秀专著《Matplotlib for Python Developers》,由 Packt Publishing 出版,提供了我们所需要的一切以及更多。其他绘图库也可用(商业或其他,旨在针对非常不同和特定的应用。matplotlib 的复杂程度和使用简便性使其成为科学计算中生成图形的最佳选择之一)。

安装完成后,可以使用 import matplotlib 来导入。在其所有模块中,我们将重点关注 pyplot,它提供了一个与绘图库的舒适接口。例如,如果我们想要绘制正弦函数的周期,我们可以执行以下代码片段:

>>> import numpy
>>> import matplotlib.pyplot as plt
>>> x=numpy.linspace(0,2*numpy.pi,32)
>>> fig = plt.figure()
>>> plt.plot(x, numpy.sin(x))
>>> plt.show()
>>> fig.savefig('sine.png')

我们得到了以下图表:

科学可视化

让我们解释上一节中的每个命令。前两个命令是按照惯例导入 numpymatplotlib.pyplot。我们定义一个名为 x 的数组,包含从 0 到 2π 的 32 个均匀分布的浮点数值,并将 y 定义为包含 x 中数值的正弦值的数组。figure 命令在内存中创建空间以存储后续的图形,并放置一个 matplotlib.figure.Figure 形式的对象。plt.plot(x, numpy.sin(x)) 命令创建一个 matplotlib.lines.Line2D 形式的对象,其中包含 xnumpy.sin(x) 的绘图数据,以及与之相连并按变量范围标记的一组坐标轴。此对象存储在之前的 Figure 对象中,并通过 plt.show() 命令在屏幕上显示。会话中的最后一个命令 fig.savefig(),将图形对象保存为我们想要的任何有效图像格式(在这种情况下,是一个 可移植 网络 图形 (PNG) 图像)。从现在起,在处理 matplotlib 命令的任何代码中,我们将保留显示/保存的选项。

当然,有一些命令可以控制坐标轴的样式、坐标轴之间的纵横比、标签、颜色、图例、同时管理多个图形(子图)的可能性(subplots),以及许多其他用于显示各种数据的特性。随着我们在本书中的进展,我们将通过示例来发现这些特性。

如何打开 IPython 笔记本

本书附带一套 IPython 笔记本,可以帮助你交互式地测试和修改或适应书中每一章中显示的代码片段。然而,我们应该警告,这些 IPython 笔记本只有在与本书一起阅读时才有意义。

在这方面,本书假设读者熟悉 Python 以及其开发环境中的 IPython 笔记本。因此,我们只会参考 IPython 笔记本官方网站上的文档(ipython.org/notebook.html)。你可以在 ipython.org/ipython-doc/stable/notebook/index.html 找到额外的帮助。请注意,IPython Notebook 也通过 Wakari (wakari.io/)、作为 Anaconda 包的一部分或通过 Enthought 提供。如果你是 IPython Notebook 的新手,可以通过查看示例集合和阅读文档开始学习。

要使用本书中的文件,打开终端并转到你想要打开的文件所在的目录(它应该具有 filename.ipynb 的形式)。在那个终端的命令行中,输入:

ipython notebook filename.ipynb

在按下回车键后,文件应该会在默认的网页浏览器中显示。如果未发生这种情况,请注意,IPython Notebook 官方支持 Chrome、Safari 和 Firefox 浏览器。有关更多详细信息,请参阅当前位于ipython.org/ipython-doc/stable/install/install.html的文档中的浏览器兼容性部分。

一旦打开.ipynb文件,请按住shift键并按下回车键,以逐个执行笔记本单元格。另一种逐个执行笔记本单元格的方法是通过位于单元格标签markdown左侧菜单附近的播放器图标。或者,您可以从单元格菜单(位于浏览器顶部)中选择几个选项来执行笔记本的内容。

要离开笔记本,您可以从浏览器顶部的文件菜单中选择关闭停止,位于笔记本标签下面的笔记本。保存笔记本的选项也可以在文件菜单下找到。要完全关闭笔记本浏览器,您需要在笔记本启动的终端上同时按下ctrlC键,然后按照之后的指示操作。

摘要

在本章中,您已经了解了使用 Python、NumPy、SciPy 和 matplotlib 组合作为任何需要数学的科学研究编程环境的益处;特别是与数值计算相关的一切。您已经探索了该环境,学习了如何下载、安装和测试所需的库,使用它们进行了一些快速计算,并找到了一些寻找帮助的好方法。

在第二章中,我们将引导您了解 SciPy 中的基本对象创建,包括操作数据或从中获取信息的最佳方法。

第二章。使用 NumPy 数组作为 SciPy 的第一步

在顶层,SciPy 基本上是 NumPy,因为这两个对象创建和基本操作都是由后者的函数执行的。这保证了计算速度更快,因为内存处理是在内部以最佳方式进行的。例如,如果必须在大型多维数组的元素上执行操作,一个新手用户可能会被诱惑使用尽可能多的 for 循环来遍历列和行。当循环以与它们在内存中存储的相同顺序访问每个连续元素时,循环运行得更快。在编码时,我们不应该被这类考虑所困扰。NumPy/SciPy 操作确保了这一点。作为一个额外的优势,NumPy/SciPy 中操作的名字直观且易于理解。以这种方式编写的代码非常容易理解和维护,在需要时更快地纠正或更改。

让我们用一个入门示例来说明这一点。

SciPy 包中的scipy.misc模块包含一个经典图像lena,该图像在图像处理社区中被用于测试和比较目的。这是一个 512 x 512 像素的标准测试图像,自 1973 年以来一直被使用,最初是从 1972 年 11 月《花花公子》杂志的封底裁剪而来。这是一张由摄影师 Dwight Hooker 拍摄的瑞典模特 Lena Söderberg 的照片。这张图像可能是所有各种图像处理算法(如压缩和降噪)和相关科学出版物中最广泛使用的测试图像。

这张图像存储为一个二维数组。请注意,该数组第n列和m行的数字测量图像像素位置(n+1, m+1)的灰度值。在以下内容中,我们通过以下命令访问这张图片并将其存储在img变量中:

>>> import scipy.misc
>>> img=scipy.misc.lena()
>>> import matplotlib.pyplot as plt 
>>> plt.gray() 
>>> plt.imshow(img) 

可以通过以下命令显示图像:

>>> plt.show() 

使用 NumPy 数组作为 SciPy 的第一步

我们可以查看一些这些值;比如说图像的 7 x 3 上角(7 列,3 行)。我们不必使用 for 循环,我们可以切片图像的相应部分。img[0:3,0:7]命令给出了以下内容:

array([[162, 162, 162, 161, 162, 157, 163],
 [162, 162, 162, 161, 162, 157, 163],
 [162, 162, 162, 161, 162, 157, 163]])

我们可以使用相同的策略来填充数组或更改它们的值。例如,让我们将上一个数组的所有条目更改为在第二行第 2 到第 6 列之间持有零:

>>> img[1,1:6]=0
>>> print (img[0:3,0:7])

输出如下所示:

[[162 162 162 161 162 157 163]
 [162   0   0   0   0   0 163]
 [162 162 162 161 162 157 163]]

对象基础

我们已经介绍了 NumPy 的主要对象——同构的多维数组,也称为ndarray。数组的所有元素都被转换为相同的数据类型(同构)。我们通过dtype属性获得数据类型,通过shape属性获得其维度,通过size属性获得数组中的元素总数,通过引用它们的位位置来引用元素:

>>> img.dtype, img.shape, img.size

输出如下所示:

(dtype('int64'), (512, 512), 262144)

现在让我们计算灰度值:

>>> img[32,67]

输出如下所示:

87

让我们解释一下输出。img的元素是 64 位整数值('int64')。这可能会根据系统、Python 安装和计算机规格而变化。数组的形状(注意它以 Python 元组的形式出现)是 512 x 512,元素数量为 262144。图像在第 33 列和第 68 行的灰度值是87(请注意,在 NumPy 中,就像在 Python 或 C 中一样,所有索引都是从 0 开始的)。

现在我们将介绍 NumPy/SciPy 对象的基本属性和方法——数据类型和索引。

使用数据类型

强制数据类型的方法有多种。例如,如果我们想将已创建数组的所有条目都设置为 32 位浮点值,我们可以按照以下方式转换:

>>> import scipy.misc
>>> img=scipy.misc.lena().astype('float32')

我们还可以通过命令使用可选参数dtype

>>> import numpy
>>> scores = numpy.array([101,103,84], dtype='float32')
>>> scores

输出如下所示:

array([ 101.,  103.,   84.], dtype=float32)

这可以通过第三种巧妙的方法进一步简化(尽管这种做法提供的代码不易理解):

>>> scores = numpy.float32([101,103,84])
>>> scores

输出如下所示:

array([ 101.,  103.,   84.], dtype=float32)

NumPy 数组的类型选择非常灵活;我们可以选择基本的 Python 类型(包括booldictlistsettuplestrunicode),尽管在数值计算中我们专注于intfloatlongcomplex

NumPy 有一组针对ndarray实例优化的数据类型,并且与之前给出的原生类型具有相同的精度。我们通过尾部下划线(_)来区分它们。例如,字符串的ndarray可以初始化如下:

>>> a=numpy.array(['Cleese', 'Idle', 'Gilliam'], dtype='str_')
>>> a.dtype

输出如下所示(这取决于您的 Python 版本):

dtype('<U7')

注意两点;与它的纯 Python 对应物不同,使用'str_'数据类型需要将名称引号括起来;我们可以使用较长的未引号版本,即numpy.str_

当系统提示数据类型时,它会返回其 C 派生的等效类型:'<U7'<U表示字符串,7表示其元素的最大大小)。

最常见的处理数值类型的方法是使用位宽命名法:boolXXintXXuintXXfloatXXcomplexXX,其中XX表示位大小(例如,uint32表示 32 位无符号整数)。

我们也可以设计自己的数据类型,这正是 NumPy 数据类型灵活性的全部潜力所在。例如,可以创建一个数据类型来表示学生的姓名和成绩,如下所示:

>>> dt = numpy.dtype([ ('name', numpy.str_, 16), ('grades',numpy.float64, (2,)) ])
>>> dt 

输出如下所示(这取决于您的 Python 版本):

dtype([('name', '<U16'), ('grades', '<f8', (2,))])

这意味着dt数据类型有两个部分:第一部分是name,它必须是一个 16 个字符的numpy.str_字符串。第二部分是grades,它是一个维度为 2 的子数组,其中的分数是 64 位浮点值。具有此数据类型元素的数组将如下所示:

>>> MA141=numpy.array([ ('Cleese', (7.0,8.0)), ('Gilliam',(9.0,10.0)) ], dtype=dt)
>>> MA141

输出如下所示(这取决于您的 Python 版本):

array([('Cleese', [7.0, 8.0]), ('Gilliam', [9.0, 10.0])],dtype=[('name', '<U16'), ('grades', '<f8', (2,))])

索引和切片数组

访问 NumPy 数组中的数据有两种基本方法;让我们称这个数组为A。两种方法都使用相同的语法,A[obj],其中obj是一个 Python 对象,它执行选择。我们已经熟悉了访问单个元素的第一种方法。第二种方法是本节的主题,即切片。这个概念正是使 NumPy 和 SciPy 如此易于管理的根本原因。

基本切片方法是一个形式为slice(start,stop,step)的 Python 对象,或者在一个更紧凑的表示法中,start:stop:step。最初,三个变量startstopstep是非负整数值,其中start小于或等于stop

这表示索引序列k = start + (i * step),其中kstart运行到最大的整数k_max = start + stepint((stop-start)/step),或者i0运行到最大的整数等于int((stop - start) / step)*。当在ndarray的任何维度上调用切片方法时,它选择该维度中由相应索引序列索引的所有元素。下面的简单示例说明了这一点:

>>> A=numpy.array([[1,2,3,4,5,6,7,8],[2,4,6,8,10,12,14,16]])
>>> print (A[0:2, 0:8:2])

输出如下所示:

[[ 1  3  5  7]
 [ 2  6 10 14]]

如果start大于stop,则使用负值的step来反向遍历序列:

>>> print (A[0:2, 8:0:-2])

输出如下所示:

[[ 8,  6,  4,  2]
 [16, 12,  8,  4]]

startstop的负值被解释为n-startn-stop(分别),其中n是相应维度的尺寸。A[0:2,-1:0:-2]命令给出与前面示例完全相同的输出。

切片对象可以通过省略start(如果step为正,则表示零,如果step为负,则表示该维度的尺寸)来缩短,省略stop(在step为正的情况下,表示相应维度的尺寸,在step为负的情况下,表示零)。省略step意味着step等于 1。::对象可以简单地缩短为:,以便于语法。然后A[:,::-2]命令再次提供与前面两个相同的输出。

从数组中访问数据的第一个非基本方法基于收集多个索引并请求具有这些索引的数组元素的思路。例如,从我们之前的数组A中,我们希望构建一个新的数组,包含位置(0, 0),(0, 3),(1, 2)和(1, 5)的元素。我们通过收集索引的xy值,分别在列表[0,0,1,1][0,3,2,5]中,并将这些列表作为索引对象传递给A,如下所示:

>>> print (A[ [0,0,1,1], [0,3,2,5] ])

输出如下所示:

[ 1  4  6 12]

注意结果丢失了原始数组的维度,并提供了一个一维数组。如果我们希望捕获A的子数组,其索引位于两个索引集的笛卡尔积中,尊重行和列的选择,并创建一个具有笛卡尔积维度的新数组,我们使用ix_命令。例如,如果我们想从之前的数组中获取维度为 2 x 2 的子数组,其索引位于索引(0, 1)和(0,3)的笛卡尔积中(这些位置是(0, 0)、(0, 3)、(1, 0)和(1, 3),我们可以这样做:

>>> print (A[ numpy.ix_( [0,1], [0,3] )])

输出如下所示:

[[1 4]
 [2 8]]

数组对象

在这一点上,我们已准备好对所有有趣的ndarray属性进行彻底研究,以用于科学计算目的。我们已经涵盖了几个,例如dtypeshapesize。其他有用的属性包括ndim(用于计算数组中的维度数量)、realimag(如果数据由复数组成,则用于获取数据的实部和虚部)或flat(它从数据创建一个一维可索引迭代器)。

例如,如果我们想将数组中的所有值相加,我们可以使用flat属性按顺序遍历所有元素,并将所有值累积到一个变量中。执行此任务的可能的代码片段如下(将此代码与稍后解释的ndarray.sum()方法进行比较):

>>> value=0; import scipy.misc; img=scipy.misc.lena()
>>> for item in img.flat: value+=item
>>> value

输出如下所示:

32518120

我们还将探索应用于数组的一些方法。这些是用于修改对象的工具;无论是它们的数据类型、形状还是通过转换的结构。这些方法可以分为三大类——数组转换形状选择/操作对象计算

数组转换

astype()方法返回数组的副本,转换为特定类型;copy方法返回数组的副本。最后,tofile()tolist()tostring()方法将数组的二进制数据写入文件,返回相同数组的分层 Python 列表版本,或返回数组数据的字符串表示。

例如,要将img数组的内容写入文本文件,确保数组中的每个条目都按整数打印,并且每两个整数之间用空格分隔,我们可以发出以下命令:

>>> img.tofile("lena.txt",sep=" ",format="%i")

注意格式化字符串遵循 C 语言约定。

形状选择/操作

这些方法不仅在我们需要重新排列(swapaxestranspose)或排序(argsortsort)数组时使用,而且在我们需要重塑(reshape)、调整大小(flattenravelresizesqueeze)或选择(choosecompressdiagonalnonzerosearchsortedtake)数组时也使用。请注意,当与切片操作结合使用时,这些方法非常强大;事实上,许多方法可以替代切片以提供更好的可读性。

我们需要谈谈flatravelflatten这些属性,它们提供非常相似的结果,但在内存管理方面却大不相同。第一个属性flat创建一个数组迭代器。一旦使用,它就会从内存中消失。属性ravel返回一个一维展平的输入数组;只有在需要时才会创建副本。最后,flatten创建一个一维输入数组,并且总是为它分配内存。我们只在需要更改展平数组的值时使用它。我们将在以下代码片段中强调排序方法的力量。当对一个整数数组进行排序时,它们的索引顺序会是什么?我们可以使用argsort()方法获得这些信息。我们甚至可以指定要使用的排序算法(而不是自己编写代码)——quicksortmergesortheapsort。我们甚至可以使用sort()方法就地排序。让我们看一下以下一系列命令:

>>> import numpy
>>> A = numpy.array([11,13,15,17,19,18,16,14,12,10])
>>> A.argsort(kind='mergesort')

输出如下所示:

array([9, 0, 8, 1, 7, 2, 6, 3, 5, 4])

现在,我们应用sort()方法:

>>> A.sort()
>>> print(A)

输出如下所示:

[10 11 12 13 14 15 16 17 18 19]

对象计算

数组计算方法用于执行计算或从我们的数据中提取信息。Python 提供了一系列统计方法来计算,例如,数据的最大值和最小值(maxmin),以及它们的对应索引方法(argmaxargmin)来计算总和、累计总和、乘积或累计乘积(sumcumsumprodcumprod),以及计算我们的数据的平均值(mean)、点扩散(ptp)、方差(var)和标准差(std)。其他方法允许我们计算复数值数组的复共轭(conj)、数组的迹(trace,即对角线元素的总和),甚至可以通过强制设置低于和高于某些阈值的最低和最高值来剪切矩阵(clip)。

注意,这些方法中的大多数都可以作用于整个数组及其每个维度:

>>> A=numpy.array([[1,1,1],[2,2,2],[3,3,3]])
>>> A.mean()

输出如下所示:

2

现在,让我们使用axis=0应用mean()方法:

>>> A.mean(axis=0)

输出如下所示:

array([ 2.,  2.,  2.])

类似地,我们使用相同的命令并设置axis=1

>>> A.mean(axis=1)

输出如下:

array([ 1.,  2.,  3.])

让我们也通过基于 Lena 图像的简单练习来说明clip命令。计算 Lena(img)的最大值和最小值,并将它们与点扩散(它应该等于这两个值之间的差)进行对比。现在,通过剪切 Lena 创建一个新的数组A,保持最小值不变,但将点扩散减少到仅 100 个值。让我们说明min()max()ptp()命令对 Lena(img)的影响:

>>> img.min(), img.max(), img.ptp()

输出如下所示:

(25, 245, 220)

此外,我们将在以下代码行中说明clip()命令对img的影响:

>>> A=img.clip(img.min(),img.min()+100)
>>> A.min(), A.max(), A.ptp()

输出如下所示:

(25, 125, 100)

数组例程

在本节中,我们将处理数组的大部分操作。我们将它们分为四个主要类别:

  • 创建新数组的例程

  • 操作单个数组的例程

  • 结合两个或更多数组的例程

  • 从数组中提取信息的例程

读者肯定会意识到,这类操作可以通过方法来实现,这再次显示了 Python 和 NumPy 的灵活性。

创建数组的例程

我们之前已经看到了创建数组并将其存储到变量A中的命令。让我们再次看看它:

>>> A=numpy.array([[1,2],[2,1]])

然而,完整的语法如下所示:

array(object,dtype=None,copy=True,order=None, subok=False,ndim=0)

让我们来看看选项:object只是我们用来初始化数组的原始数据。在先前的例子中,该对象是一个 2 x 2 的方阵;我们可以使用dtype选项来指定数据类型。结果存储在变量A中。如果copyTrue,返回的对象将是数组的副本,如果False,则返回的对象将仅是副本,如果dtype与对象的类型不同。数组按照 C 风格的行和列顺序存储。如果用户希望按照 FORTRAN 的内存风格存储数组,应使用order='Fortran'选项。subok选项非常微妙;如果为True,数组可以作为对象的子类传递,如果为False,则只能传递ndarray数组。最后,ndmin选项表示数组返回的最小维度。如果没有提供,这将从object中计算得出。

可以使用zerosonesemptyidentityeye等命令获得一组特殊数组。这些命令的名称非常有信息性:

  • zeros创建一个填充有零的数组。

  • ones创建一个填充有 1 的数组。

  • empty返回所需形状的数组,但不初始化其条目。

  • identity创建一个由单个正整数n指定的维度的方阵。除了对角线外,所有条目都填充为零,对角线填充为 1。

eye命令与identity非常相似。它也构建对角数组,但与identity不同,eye允许指定偏离传统居中对齐的对角线,因为它还可以在矩形数组上操作。在以下代码行中,我们使用 zeros、ones 和 identity 命令:

>>> Z=numpy.zeros((5,5), dtype=int)
>>> U=numpy.ones((2,2), dtype=int)
>>> I=numpy.identity(3, dtype=int)

在前两种情况下,我们指明了数组的形状(作为正整数的 Python 元组)和可选的数据类型指定。

eye 的语法如下:

numpy.eye(N,M=None,k=0,dtype=float)

整数 NM 表示数组的形状,整数 k 表示要填充的对角线索引。

索引 k=0(默认值)指向传统的对角线;正索引指向上对角线,负索引指向下对角线。为了说明这一点,以下示例展示了如何创建一个 4 x 4 的稀疏矩阵,其非零元素位于第一上对角线和次对角线上:

>>> D=numpy.eye(4,k=1) + numpy.eye(4,k=-1)
>>> print (D)

输出如下所示:

[[ 0\.  1\.  0\.  0.]
 [ 1\.  0\.  1\.  0.]
 [ 0\.  1\.  0\.  1.]
 [ 0\.  0\.  1\.  0.]]

将前四个命令与基本切片结合使用,可以非常简单地创建更复杂的数组。我们提出以下挑战。

仅使用之前定义的 UI 以及 eye 数组。读者将如何创建一个 5 x 5 的数组 A,其值为浮点数,在四个条目(0, 0)、(0, 1)、(1, 0)和(1, 1)处为 fives;在剩余的对角线条目处为 sixes;在两个其他角落处为 threes?此问题的解决方案可以通过以下命令集来解决:

>>> A=3.0*(numpy.eye(5,k=4) + numpy.eye(5,k=-4))
>>> A[0:2,0:2]=5*U; A[2:5,2:5]=6*I
>>> print (A)

输出如下所示:

[[ 5\.  5\.  0\.  0\.  3.]
 [ 5\.  5\.  0\.  0\.  0.]
 [ 0\.  0\.  6\.  0\.  0.]
 [ 0\.  0\.  0\.  6\.  0.]
 [ 3\.  0\.  0\.  0\.  6.]]

使用 NumPy 创建数组的灵活性通过 fromfunction 命令表现得更加清晰。例如,如果我们需要一个 4 x 4 的数组,其中每个条目都反映了其索引的乘积,我们可以在 fromfunction 命令中使用 lambda 函数 (lambda i,j: i*j),如下所示:

>>> B=numpy.fromfunction( (lambda i,j: i*j), (4,4), dtype=int)
>>> print (B)

输出如下所示:

[[0 0 0 0]
 [0 1 2 3]
 [0 2 4 6]
 [0 3 6 9]]

处理数组的一个非常重要的工具是掩码的概念。掩码基于选择或掩码那些满足给定条件的索引条目的想法。例如,在前面示例中显示的数组 B 中,我们可以使用 B==0 命令掩码所有零值条目,如下所示:

>>> print (B==0)

输出如下所示:

[[ True  True  True  True]
 [ True False False False]
 [ True False False False]
 [ True False False False]]

现在,读者将如何更新 B,以便将所有零替换为它们对应索引的平方和?

将一个掩码与形状相同的第二个数组相乘,会得到一个新的数组,其中每个条目要么是零(如果掩码中相应的条目是 False),要么是第二个数组的条目(如果掩码中相应的条目是 True):

>>> B += numpy.fromfunction((lambda i,j:i*i+j*j), (4,4))*(B==0)
>>> print (B)

输出如下所示:

[[0 1 4 9]
 [1 1 2 3]
 [4 2 4 6]
 [9 3 6 9]]

注意,我们创建了一个新数组,其中填充了布尔值,其大小与原始数组相同,并在每个步骤中。在这些玩具示例中,这并不是什么大问题,但在处理大型数据集时,分配过多的内存可能会严重减慢我们的计算速度并耗尽系统的内存。在创建数组的命令中,有两个命令特别重要,即 putmaskwhere,它们有助于内部资源管理,从而加快处理过程。

例如,当我们寻找 B 中所有奇数值条目时,生成的掩码大小为 16,尽管有趣的条目只有八个:

>>> print (B%2!=0)

输出如下所示:

[[False  True False  True]
 [ True  True False  True]
 [False False False False]
 [ True  True False  True]]

numpy.where()命令帮助我们更有效地收集这些条目。让我们看看以下命令:

>>> numpy.where(B%2!=0)

输出如下所示:

(array([0, 0, 1, 1, 1, 3, 3, 3], dtype=int32),array([1, 3, 0, 1, 3, 0, 1, 3], dtype=int32))

如果我们希望将这些条目(所有奇数)改为,比如说,它们是squares plus one,我们可以使用numpy.putmask()命令,同时更好地管理内存。以下是对numpy.putmask()命令的示例代码:

>>> numpy.putmask( B, B%2!=0, B**2+1)
>>> print (B)

输出如下所示:

[[ 0  2  4 82]
 [ 2  2  2 10]
 [ 4  2  4  6]
 [82 10  6 82]]

注意putmask过程如何更新B的值,而不需要显式地进行新的赋值。

有三个额外的命令可以创建网格形式的数组。arangelinspace命令在两个数字之间创建均匀分布的值。在arange中,我们指定元素之间的间隔;在linspace中,我们指定网格中所需的元素数量。logspace命令在以 10 为底的两个数字的对数之间创建对数刻度的均匀分布值。用户可以将这些输出视为单变量函数的支持。

以下是对numpy.arrange()命令的示例代码:

>>> L1=numpy.arange(-1,1,0.3)
>>> print (L1)

上述代码的输出如下所示:

[-1\.  -0.7 -0.4 -0.1  0.2  0.5  0.8]

以下是对numpy.linspace()命令的示例代码:

>>> L2=numpy.linspace(-1,1,4)
>>> print (L2)

输出如下所示:

[-1\.         -0.33333333  0.33333333  1\.        ]

以下是对numpy.logspace()命令的示例:

>>> L3= numpy.logspace(-1,1,4)
>>> print (L3)

上述代码的输出如下所示:

[  0.1          0.46415888   2.15443469  10\.        ]

最后,meshgridmgridogrid创建了两个二维数组,维度为n x m,包含两个给定的一维数组(维度为nm)的元素。它是通过重复每个数组的值来实现的。用户可以将这些输出视为二元函数的支持。

这些程序中的第一个,meshgrid,只接受数组作为输入。其他两个程序,mgridogrid,只接受索引对象(例如,切片)。这两个最后之间的区别是内存分配的问题;mgrid分配包含所有数据的完整数组,而ogrid只创建足够的集合,以便可以通过适当的笛卡尔积获得相应的mgrid命令。

让我们看看以下meshgrid命令:

>>> print (numpy.meshgrid(L2,L3))

输出如下所示:

(array([[-1\.        , -0.33333333,  0.33333333,  1\.        ],
 [-1\.        , -0.33333333,  0.33333333,  1\.        ],
 [-1\.        , -0.33333333,  0.33333333,  1\.        ],
 [-1\.        , -0.33333333,  0.33333333,  1\.        ]]), array([[ 
0.1       ,   0.1       ,   0.1       ,   0.1       ],
 [  0.46415888,   0.46415888,   0.46415888,   0.46415888],
 [  2.15443469,   2.15443469,   2.15443469,   2.15443469],
 [ 10\.        ,  10\.        ,  10\.        ,  10\.        ]]))

让我们看看以下mgrid命令:

>>> print (numpy.mgrid[0:5,0:5])

输出如下所示:

[[[0 0 0 0 0]
 [1 1 1 1 1]
 [2 2 2 2 2]
 [3 3 3 3 3]
 [4 4 4 4 4]]

 [[0 1 2 3 4]
 [0 1 2 3 4]
 [0 1 2 3 4]
 [0 1 2 3 4]
 [0 1 2 3 4]]]

让我们看看以下ogrid命令:

>>> print (numpy.ogrid[0:5,0:5])

输出如下所示:

[array([[0],
 [1],
 [2],
 [3],
 [4]]), array([[0, 1, 2, 3, 4]])]

我们希望通过展示图像处理和微分方程中最有用的程序之一——tile命令,来完成关于数组创建的小节。它的语法非常简单,如下所示:

tile(A, reps)

这个程序提供了一个非常有效的方法,通过重复模式reps(一个元组、一个列表或另一个数组)来对数组A进行平铺,从而创建更大的数组。以下棋盘练习展示了它的潜力。

从两个小的二进制数组开始——B=numpy.ones((3,3))checker2by2=numpy.zeros((6,6)),并使用 tile 和尽可能少的操作创建一个棋盘。

让我们使用这些命令执行一些操作:

>>> checker2by2[0:3,0:3]=checker2by2[3:6,3:6]=B
>>> numpy.tile(checker2by2,(4,4))

输出太长,无法在此显示。请参阅第一章中“如何打开 IPython 笔记本”部分,SciPy 简介,以运行与本章对应的 IPython 笔记本。

组合两个或更多数组的常规操作

有时,我们需要将两个或更多数组的数据组合起来以解决特定问题。核心 NumPy 库包含执行这些计算的高效常规操作,我们敦促读者熟悉它们。它们使用最先进的算法构建,并确保内存使用最小化,复杂度最优化。最相关的是将数组作为矩阵操作的常规操作。这包括矩阵乘法(outerinnerdotvdottensordotcrosskron)、数组相关性(correlateconvolve)、数组堆叠(concatenatevstackhstackcolumn_stackrow_stackdstack)以及数组比较(allclose)。

如果你精通线性代数,你一定会喜欢 NumPy 中包含的矩阵乘法。我们将推迟它们的用法和分析,直到我们在第三章中介绍 SciPy 模块的线性代数部分,SciPy 线性代数

数组相关性的一个很好的用途是基本的模式匹配。例如,以下示例(text 数组)中的图像是从维基百科关于堂吉诃德的页面中提取的段落图像,而第二个数组 letterE 包含字母 e 的图像,实际上是从 text 数组中获得的子数组,代表要匹配的模式。

首先,我们加载文本图像并在其上进行一些预处理,以便将图像转换为正确的格式(尽可能接近灰度近似),以便在此简单模式匹配方法上获得更好的性能。我们通过在 Python 控制台中执行以下代码行来完成此操作:

>>> import scipy.ndimage
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> text = scipy.ndimage.imread('Chap_02_text_image.png')
>>> text = np.mean(text.astype(float)/255,-1)*2-1

第二,识别字母 e 的模式:

>>> letterE = text[37:53,275:291]

接下来,两个数组相关性的最大值的一部分提供了数组 text 中包含的所有 e 字母的位置:

>>> corr = scipy.ndimage.correlate(text,letterE)
>>> eLocations = (corr >= 0.95 * corr.max())

找到的模式在图像中 x 的位置如下:

>>> CorrLocIndex = np.where(eLocations==True)
>>> x=CorrLocIndex[1]
>>> x 

输出如下所示:

array([ 283,  514,  583,  681,  722,  881,  929, 1023,   64,  188,  452, 
 504,  892,  921, 1059, 1087, 1102, 1133,  118,  547,  690, 1066, 
 1110,  330,  363,  519,  671,  913,  951, 1119,  120,  292,  441, 
 516,  557,  602,  649,  688,  717,  747,  783,  813,  988, 1016, 
 250,  309,  505,  691,  769,  876,  904, 1057,  224,  289,  470, 
 596,  626,  780, 1027,  112,  151,  203,  468,  596,  751,  817, 
 867,  203,  273,  369,  560,  599,  888, 1111,  159,  221,  260, 
 352,  427,  861,  901, 1034, 1146,  325,  506,  558]) 

找到的模式在图像中 y 的位置如下:

>>> y=CorrLocIndex[0] 
>>> y 

输出如下所示:

array([ 45,  45,  45,  45,  45,  45,  45,  45,  74,  74,  74,  74,  74, 
 74,  74,  74,  74,  74, 103, 103, 103, 103, 103, 132, 132, 132, 
 132, 132, 132, 132, 161, 161, 161, 161, 161, 161, 161, 161, 161, 
 161, 161, 161, 161, 161, 190, 190, 190, 190, 190, 190, 190, 190, 
 219, 219, 219, 219, 219, 219, 219, 248, 248, 248, 248, 248, 248, 
 248, 248, 277, 277, 277, 277, 277, 277, 277, 306, 306, 306, 306, 
 306, 306, 306, 306, 306, 335, 335, 335]) 

有 86 个元素,实际上是在文本图像中字母 e 出现的总数,可以通过计数来验证。是否正确匹配可以通过图形验证,将每个模式 (x,y) 对的图案叠加到文本图像上,如下所示:

>>> thefig=plt.figure()
>>> plt.subplot(211)
<matplotlib.axes._subplots.AxesSubplot object at 0x7fb9b2390110>
>>> plt.imshow(text, cmap=plt.cm.gray, interpolation='nearest')
<matplotlib.image.AxesImage object at 0x7fb9b1f29410>
>>> plt.axis('off')

plt.axis()的输出如下所示:

(-0.5, 1199.5, 359.5, -0.5) 

现在,让我们在代码中进一步深入:

>>> plt.subplot(212) 
<matplotlib.axes._subplots.AxesSubplot object at 0x7fb9b1f29890> 
>>> plt.imshow(text, cmap=plt.cm.gray, interpolation='nearest') 
<matplotlib.image.AxesImage object at 0x7fb9b1637e10> 
>>> plt.autoscale(False) 
>>> plt.plot(x,y,'wo',markersize=10) 
[<matplotlib.lines.Line2D object at 0x7fb9b1647290>] 
>>> plt.axis('off') 

plt.axis()的输出如下所示:

(-0.5, 1199.5, 359.5, -0.5) 

最后,在下面的show()命令中,我们显示一个图像,该图像将图案中的每个(x,y)对叠加到文本图像上:

>>> plt.show() 

这导致以下截图(第一幅图像是文本,下一幅图像是所有出现字母e的文本都被划掉的文本):

两个或多个数组合并的程序

关于堆叠操作的一些话;我们有一个基本的连接程序,concatenate,它沿着预定的轴将一系列数组连接在一起。当然,序列中的所有数组都必须具有相同的维度,否则显然不起作用。其余的堆叠操作是concatenate的特殊情况的语法糖——vstack用于垂直连接数组,hstack用于水平连接数组,dstack用于在第三个维度上连接数组,等等。

另一组令人印象深刻的程序是集合操作。它们允许用户将一维数组视为集合,并执行交集(intersect1d)、并集(union1d)、集合差(setdiff1d)和集合异或(setxor1d)的布尔运算。这些集合操作的输出返回排序后的数组。请注意,也可以测试一个数组中的所有元素是否属于第二个数组(in1d)。

数组操作的相关程序

有一个分割程序序列,旨在将数组分割成更小的数组,在任何给定维度上——array_splitsplit(两者都是沿指示轴的基本分割)、hsplit(水平分割)、vsplit(垂直分割)和dsplit(在第三个轴上)。让我们用一个简单的例子来说明这些:

>>> import numpy 
>>> B = numpy.ones((3,3)) 
>>> checker2by2 = numpy.zeros((6,6)) 
>>> checker2by2[0:3,0:3] = checker2by2[3:6,3:6] = B 
>>> print(checker2by2)

输出如下所示:

[[ 1\.  1\.  1\.  0\.  0\.  0.]
 [ 1\.  1\.  1\.  0\.  0\.  0.]
 [ 1\.  1\.  1\.  0\.  0\.  0.]
 [ 0\.  0\.  0\.  1\.  1\.  1.]
 [ 0\.  0\.  0\.  1\.  1\.  1.]
 [ 0\.  0\.  0\.  1\.  1\.  1.]]

现在,让我们执行垂直分割:

>>> numpy.vsplit(checker2by2,3)

输出如下所示:

[array([[ 1.,  1.,  1.,  0.,  0.,  0.],
 [ 1.,  1.,  1.,  0.,  0.,  0.]]),
 array([[ 1.,  1.,  1.,  0.,  0.,  0.],
 [ 0.,  0.,  0.,  1.,  1.,  1.]]),
 array([[ 0.,  0.,  0.,  1.,  1.,  1.],
 [ 0.,  0.,  0.,  1.,  1.,  1.]])]

在数组上应用 Python 函数通常意味着将函数应用于数组的每个元素。注意 NumPy 函数sin是如何在数组上工作的,例如:

>>> a=numpy.array([-numpy.pi, numpy.pi])
>>> print (numpy.vstack((a, numpy.sin(a))))

输出如下所示:

[[ -3.14159265e+00   3.14159265e+00]
 [ -1.22464680e-16   1.22464680e-16]]

注意,sin函数是在数组的每个元素上计算的。

这只有在函数已经被正确向量化(例如numpy.sin)的情况下才会工作。注意非向量化 Python 函数的行为。让我们定义一个函数,用于计算对于每个x值,x和 100 之间的最大值,而不使用 NumPy 库中的任何程序:

# function max100
>>> def max100(x):
 return(x)

如果我们尝试将此函数应用于前面的数组,系统将引发错误,如下所示:

>>> max100(a)

输出是一个错误,显示如下:

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

我们需要明确告诉系统,当我们想要将我们的函数应用于数组以及标量时。我们通过vectorize程序来完成,如下所示:

>>> numpy.vectorize(max100)(a)

输出如下所示:

array([100, 100])

为了我们的便利,NumPy 库提供了一大堆已经向量化了的数学函数。一些例子包括round_fix(将数组元素四舍五入到所需的十进制位数),以及angle(提供数组元素的角,前提是它们是复数)和任何基本三角函数(sincostansec)、指数函数(expexp2sinhcosh)和对数函数(loglog10log2)。

我们还有将数组视为多维函数输出的数学函数,并提供相关计算。一些有用的例子包括diff(通过执行离散差分来模拟任何指定维度的微分),gradient(计算相应函数的梯度),或者cov(用于数组的协方差)。

使用msortsort_complex例程可以根据第一轴的值对整个数组进行排序。

从数组中提取信息的例程

大多数提取信息的例程都具有统计性质,包括average(与mean方法作用相同),median(计算数组在其任何维度上的统计中位数,或整个数组),以及直方图的计算(histogramhistogram2dhistogramdd,取决于数组的维度)。在这个类别中,另一组重要的例程处理一维数组的 bin 概念。这可以通过例子更容易地解释。以数组A=numpy.array([5,1,1,2,1,1,2,2,10,3,3,4,5])为例,unique命令找到数组中的唯一值,并以排序的方式呈现:

>>> numpy.unique(A)

输出如下所示:

array([ 1, 2, 3, 4, 5, 10])

对于所有条目都是非负整数的数组,如A,我们可以将数组A可视化为从 0 到 10(数组中的最大值)的数字标签的十一个 bin 序列(bin)。每个标签为n的 bin 包含数组中n的数量:

>>> numpy.bincount(A)

输出如下所示:

array([0, 4, 3, 2, 1, 2, 0, 0, 0, 0, 1])

对于某些元素不是数字(nan)的数组,NumPy 提供了一套模拟提取信息但忽略冲突元素的例程——nanmaxnanminnanargmaxnanargminnansum等等:

>>> A=numpy.fromfunction((lambda i,j: (i+1)*(-1)**(i*j)), (4,4))
>>> print (A)

输出如下所示:

[[ 1\.  1\.  1\.  1.]
 [ 2\. -2\.  2\. -2.]
 [ 3\.  3\.  3\.  3.]
 [ 4\. -4\.  4\. -4.]]

让我们看看log2对数组A的影响:

>>> B=numpy.log2(A)
__main__:1: RuntimeWarning: invalid value encountered in log2
>>> print (B)

输出如下所示:

[[ 0\.         0\.         0\.         0\.       ]
 [ 1\.               nan  1\.               nan]
 [ 1.5849625  1.5849625  1.5849625  1.5849625]
 [ 2\.               nan  2\.               nan]]

让我们看一下以下代码行中的sumnansum命令:

>>> numpy.sum(B), numpy.nansum(B)

输出如下所示:

(nan, 12.339850002884624)

摘要

在本章中,我们深入探讨了 SciPy 使用的对象数组的创建和基本操作,作为对 NumPy 库的概述。特别是,我们看到了切片和掩码的原则,这些原则简化了算法的编码,以至于将原本难以阅读的循环和原始命令序列转换成直观且自解释的对象调用和方法集。你还了解到,NumPy 中的非基本模块在 SciPy 自身中也作为模块进行了复制。本章的结构大致与官方 NumPy 参考(读者可以在 SciPy 页面docs.scipy.org/doc/numpy/reference/上访问)相同。还有其他一些严谨地覆盖 NumPy 的优质资源,我们建议您查阅这些材料以获得对该主题更详细的了解。

在接下来的五章中,我们将探讨使 SciPy 成为数值计算强大工具的命令。这些章节的结构基本上反映了 SciPy 中不同模块的结构,这些模块按照允许在彼此之上构建应用程序的顺序排列。

第三章。SciPy 线性代数

在本章中,我们将通过有意义的示例继续探索 SciPy 的不同模块。我们将从使用线性代数模块linalgsparse处理矩阵(无论是正常还是稀疏)开始。请注意,linalg扩展了同名的 NumPy 模块。

这门数学学科研究向量空间及其之间的线性映射。矩阵以这种方式代表该领域的对象,通过在表示矩阵上执行适当的操作,可以获取底层对象的任何属性。在本章中,我们假设您至少熟悉线性代学的基础知识,特别是矩阵乘法、求矩阵的行列式和逆矩阵以及它们在向量微积分中的直接应用。

因此,在本章中,我们将探讨 Numpy/SciPy 中如何处理向量和矩阵,如何创建它们,如何编程它们之间的标准数学运算,以及如何在函数形式上表示这些运算。接下来,我们将解决以矩阵形式表示的线性方程组,涉及稠密或稀疏矩阵。相应的 IPython Notebook 将帮助您测试涉及模块的功能,并根据您的具体需求修改每个示例。

向量创建

如同第二章中提到的,作为 SciPy 入门的 NumPy 数组处理,SciPy 依赖于 NumPy 的主要对象ndarray数据结构。您可以将一维数组视为向量,反之亦然(n 维空间中的有向点)。因此,可以通过 Numpy 创建一个向量如下所示:

>>> import numpy
>>> vectorA = numpy.array([1,2,3,4,5,6,7])
>>> vectorA

输出如下所示:

array([1, 2, 3, 4, 5, 6, 7])

我们也可以使用已经定义的数组来创建一个新的候选者。前一章中已经提供了几个例子。这里我们可以将已经创建的向量反转并分配给一个新的向量:

>>> vectorB = vectorA[::-1].copy()
>>> vectorB

输出如下所示:

array([7, 6, 5, 4, 3, 2, 1])

注意,在这个例子中,我们必须复制vectorA元素的逆序并将其分配给vectorB。这样,通过改变vectorB的元素,vectorA的元素保持不变,如下所示:

>>> vectorB[0]=123
>>> vectorB

输出如下所示:

array([123,   6,   5,   4,   3,   2,   1])

让我们看看vectorA

>>> vectorA

输出如下所示:

array([1, 2, 3, 4, 5, 6, 7])

让我们通过反转其元素并将结果分配给vectorB来复制vectorA

>>> vectorB = vectorA[::-1].copy()
>>> vectorB

输出如下所示:

array([7, 6, 5, 4, 3, 2, 1])

在最后的代码语句中,我们重复了之前的赋值给vectorB的操作,使其再次回到其初始值,即vectorA的逆序。

向量运算

除了是线性代数中研究的数学实体外,向量在物理学和工程学中被广泛用作表示物理量(如位移速度加速度、力等)的便捷方式。因此,向量之间的基本运算可以通过 Numpy/SciPy 操作如下进行:

加法/减法

向量的加法/减法不需要任何显式的循环来执行。让我们看看两个向量的加法:

>>> vectorC = vectorA + vectorB
>>> vectorC

输出如下所示:

array([8, 8, 8, 8, 8, 8, 8])

进一步,我们对两个向量进行减法操作:

>>> vectorD = vectorB - vectorA
>>> vectorD

输出如下所示:

array([ 6,  4,  2,  0, -2, -4, -6])

标量/点积

Numpy 有一个内置函数dot,用于计算两个向量之间的标量(dot)积。我们展示了如何使用它来计算前一个代码片段中vectorAvectorBdot积:

>>> dotProduct1 = numpy.dot(vectorA,vectorB)
>>> dotProduct1

输出如下所示:

84

或者,为了计算这个乘积,我们可以执行向量的各分量之间的逐元素乘积,然后将相应的结果相加。这已在以下代码行中实现:

>>> dotProduct2 = (vectorA*vectorB).sum()
>>> dotProduct2

输出如下所示:

84

交叉/向量积 – 在三维空间向量上

首先,在应用 NumPy 的内置函数计算向量的叉积之前,创建了三个维度的两个向量:

>>> vectorA = numpy.array([5, 6, 7])
>>> vectorB = numpy.array([7, 6, 5])
>>> crossProduct = numpy.cross(vectorA,vectorB)
>>> crossProduct

输出如下所示:

array([-12,  24, -12])

进一步,我们对vectorBvectorA上执行cross操作:

>>> crossProduct = numpy.cross(vectorB,vectorA)
>>> crossProduct

输出如下所示:

array([ 12, -24,  12])

注意到最后一个表达式显示了预期的结果,即vectorAvectorB的叉积是vectorBvectorA叉积的相反数。

创建一个矩阵

在 SciPy 中,任何一维或二维ndarray都可以通过matrixmat命令获得矩阵结构。完整的语法如下:

numpy.matrix(data=object, dtype=None, copy=True)

创建矩阵时,数据可以以ndarray、字符串或 Python 列表(如下面的第二个示例)的形式给出,这非常方便。当使用字符串时,分号表示行变化,逗号表示列变化:

>>> A=numpy.matrix("1,2,3;4,5,6")
>>> A

输出如下所示:

matrix([[1, 2, 3],
 [4, 5, 6]])

让我们来看另一个例子:

>>> A=numpy.matrix([[1,2,3],[4,5,6]])
>>> A

输出如下所示:

matrix([[1, 2, 3],
 [4, 5, 6]])

从一个二维数组创建矩阵的另一种技术是将矩阵结构强加到新对象上,通过asmatrix例程复制前者的数据。

一个矩阵被称为稀疏矩阵 (en.wikipedia.org/wiki/Sparse_matrix),如果其大多数条目都是零。以通常的方式输入这样的矩阵是浪费内存的,尤其是如果维度很大。SciPy 提供了不同的程序来有效地在内存中存储这样的矩阵。SciPy 中的 scipy.sparse 模块考虑了大多数输入稀疏矩阵的常规方法作为例程。其中一些方法包括 块稀疏行 (bsr_matrix),坐标格式 (coo_matrix),压缩稀疏列或行 (csc_matrixcsr_matrix),具有对角存储的稀疏矩阵 (dia_matrix),基于 键排序 的字典 (dok_matrix),以及 基于行的链表 (lil_matrix)。

在这个阶段,我们想介绍其中之一:坐标格式。在这个格式中,给定一个稀疏矩阵 A,我们识别非零元素的位置,比如说有 n 个,然后我们创建两个 n 维的 ndarray 数组,包含这些条目的列和行,以及一个包含相应条目值的第三个 ndarray。例如,注意以下稀疏矩阵:

创建矩阵

创建此类矩阵的标准形式如下:

>>> A=numpy.matrix([ [0,10,0,0,0], [0,0,20,0,0], [0,0,0,30,0], [0,0,0,0,40], [0,0,0,0,0] ])
>>> A

输出如下所示:

matrix([[ 0, 10,  0,  0,  0],
 [ 0,  0, 20,  0,  0],
 [ 0,  0,  0, 30,  0],
 [ 0,  0,  0,  0, 40],
 [ 0,  0,  0,  0,  0]])

创建这些矩阵的一种更节省内存的方法是正确存储非零元素。在这种情况下,一个非零条目位于第 1 行第 2 列(或 Python 中的位置 (0, 1)),值为 10。另一个非零条目位于 (1, 2),值为 20。第三个非零条目,值为 30,位于 (2, 3)A 的最后一个非零条目位于 (3, 4),值为 40

然后,我们有行 ndarray,列 ndarray,以及另一个值 ndarray

>>> import numpy
>>> rows=numpy.array([0,1,2,3])
>>> cols=numpy.array([1,2,3,4])
>>> vals=numpy.array([10,20,30,40])

我们如下创建矩阵 A

>>> import scipy.sparse
>>> A=scipy.sparse.coo_matrix( (vals,(rows,cols)) )
>>> print (A); print (A.todense())

输出如下所示:

 (0, 1)  10
 (1, 2)  20
 (2, 3)  30
 (3, 4)  40
[[  0\.  10   0\.   0\.   0.]
 [  0\.   0\.  20   0\.   0.]
 [  0\.   0\.   0\.  30   0.]
 [  0\.   0\.   0\.   0\.  40]]

注意 todense 方法如何将稀疏矩阵转换为全矩阵。还请注意,它消除了最后一个非零元素之后的任何全零行或列。

与每种输入方法相关联,我们都有识别每种类型稀疏矩阵的函数。例如,如果我们怀疑 Acoo_matrix 格式的稀疏矩阵,我们可以使用以下命令:

>>> scipy.sparse.isspmatrix_coo(A)

输出如下所示:

True

所有数组例程都转换为矩阵,只要输入是矩阵。这对于矩阵创建非常方便,尤其是在堆叠命令(hstackvstacktile)的帮助下。除此之外,矩阵还有一个令人惊叹的堆叠命令,bmat。这个例程允许通过字符串堆叠矩阵,利用以下约定:分号用于行变化,逗号用于列变化。它还允许在字符串内部评估矩阵名称。以下示例很有启发性:

>>> B=numpy.mat(numpy.ones((3,3)))
>>> W=numpy.mat(numpy.zeros((3,3)))
>>> print (numpy.bmat('B,W;W,B'))

输出如下所示:

[[ 1\.  1\.  1\.  0\.  0\.  0.]
 [ 1\.  1\.  1\.  0\.  0\.  0.]
 [ 1\.  1\.  1\.  0\.  0\.  0.]
 [ 0\.  0\.  0\.  1\.  1\.  1.]
 [ 0\.  0\.  0\.  1\.  1\.  1.]
 [ 0\.  0\.  0\.  1\.  1\.  1.]]

数组和矩阵之间的主要区别在于相同类型两个对象的乘积行为。例如,两个数组之间的乘法意味着两个数组条目的逐元素乘法,并且需要具有相同形状的两个对象。以下是一个两个数组乘法示例的代码片段:

>>> a=numpy.array([[1,2],[3,4]])
>>> a*a

输出如下所示:

array([[ 1,  4],
 [ 9, 16]])

另一方面,矩阵乘法需要一个形状为(mn)的第一个矩阵和一个形状为(np)的第二个矩阵——第一个矩阵的列数必须与第二个矩阵的行数相同。这个操作会提供一个形状为(mp)的新矩阵,如下面的图所示:

创建矩阵

以下是一个代码片段:

>>> A=numpy.mat(a)
>>> A*A

输出如下所示:

matrix([[ 7, 10],
 [15, 22]])

或者,为了获得两个符合规范的矩阵作为ndarray对象的矩阵乘积,如果不需要,我们实际上不需要将ndarray对象转换为矩阵对象。矩阵乘积可以通过本章标量/点积部分中介绍过的numpy.dot函数直接获得。让我们看看以下numpy.dot命令的示例:

>>> b=numpy.array([[1,2,3],[3,4,5]])
>>> numpy.dot(a,b)

输出如下所示:

array([[ 7, 10, 13],
 [15, 22, 29]])

如果我们想要对两个矩阵的元素进行逐元素乘法,我们可以使用多功能的numpy.multiply命令,如下所示:

>>> numpy.multiply(A,A)

输出如下所示:

matrix([[ 1,  4],
 [ 9, 16]])

数组和矩阵之间另一个值得注意的区别在于它们的形状。虽然我们允许数组有一个维度;它们对应的矩阵必须至少有两个维度。当我们转置任一对象时,这一点非常重要。让我们看看以下实现shape()transpose()命令的代码片段:

>>> a=numpy.arange(5); A=numpy.mat(a)
>>> a.shape, A.shape, a.transpose().shape, A.transpose().shape

输出如下所示:

((5,), (1, 5), (5,), (5, 1))

如前所示,SciPy 提供了相当多的基本工具来实例化和操作矩阵,还有许多相关的方法可以继续使用。这也允许我们在使用特殊矩阵的情况下加快计算速度。

scipy.linalg模块提供了创建特殊矩阵的命令,例如从提供的数组创建块对角矩阵(block_diag)、循环矩阵(circulant)、伴随矩阵(companion)、Hadamard 矩阵hadamard)、Hankel 矩阵hankel)、Hilbert逆 Hilbert 矩阵hilbertinvhilbert)、Leslie 矩阵leslie)、平方 Pascal 矩阵pascal)、Toeplitz 矩阵toeplitz)、下三角矩阵tril)和上三角矩阵triu)。

让我们来看一个关于最优称重的例子。

假设我们给定 p 个物体需要在 n 次称重中使用双盘天平进行称重。我们创建一个 n x p 的矩阵,其中包含加号和减号,其中在 (i, j) 位置的正值表示在 i^(th) 次称重中,j^(th) 物体被放置在天平的左盘上,而负值表示相应的 j^(th) 物体在右盘上。

已知最优称重是由哈达玛矩阵的子矩阵设计的。对于设计八个物体的三次称重问题,我们可以探索八阶哈达玛矩阵的三行不同的选择。唯一的要求是矩阵行的元素之和为零(这样每个天平上放置的物体数量相同)。通过切片,我们可以实现这一点:

>>> import scipy.linalg
>>> A=scipy.linalg.hadamard(8)
>>> zero_sum_rows = (numpy.sum(A,0)==0)
>>> B=A[zero_sum_rows,:]
>>> print (B[0:3,:])

输出如下所示:

[[ 1 -1  1 -1  1 -1  1 -1]
 [ 1  1 -1 -1  1  1 -1 -1]
 [ 1 -1 -1  1  1 -1 -1  1]]

scipy.sparse 模块有其自己的一套特殊矩阵。最常见的是对角线上的矩阵(eye)、单位矩阵(identity)、从对角线生成的矩阵(diags, spdiags)、从稀疏矩阵生成的分块对角矩阵(block_diag)、从稀疏子块生成的矩阵(bmat)、按列和按行堆叠的矩阵(hstack, vstack),以及给定形状和密度的随机矩阵,其值均匀分布(rand)。

矩阵方法

除了继承所有数组方法外,矩阵还享有四个额外的属性:T 用于转置,H 用于共轭转置,I 用于求逆,以及 A 用于转换为 ndarray

>>> A = numpy.matrix("1+1j, 2-1j; 3-1j, 4+1j")
>>> print (A.T); print (A.H)

输出如下所示:

[[ 1.+1.j  3.-1.j]
 [ 2.-1.j  4.+1.j]]
[[ 1.-1.j  3.+1.j]
 [ 2.+1.j  4.-1.j]]

矩阵之间的运算

我们简要介绍了两个矩阵之间最基本的操作;矩阵乘法。对于任何其他类型的乘法,我们求助于 NumPy 库中的基本工具,如:数组或向量的点积(dot, vdot)、两个数组的内积和外积(inner, outer)、沿指定轴的张量点积tensordot)或两个数组的克罗内克积kron)。

让我们看看创建一个正交基的例子。

从三维实空间的正交归一基中创建九维实空间的正交归一基。

例如,让我们选择如下图中所示的由向量构成的正交归一基:

矩阵之间的运算

我们通过将这些向量收集到一个矩阵中并使用克罗内克积来计算所需的基,如下所示:

>>> import numpy
>>> import scipy.linalg
>>> mu = 1/numpy.sqrt(2)
>>> A = numpy.matrix([[mu,0,mu],[0,1,0],[mu,0,-mu]])
>>> B = scipy.linalg.kron(A,A)

之前显示的矩阵 B 的列直接给出了一个正交归一基。例如,奇数索引的向量将是以下子矩阵的列:

>>> print (B[:,0:-1:2])

输出如下所示:

[[ 0.5  0.5  0\.   0.5]
 [ 0\.   0\.   0\.   0\. ]
 [ 0.5 -0.5  0\.   0.5]
 [ 0\.   0\.   0\.   0\. ]
 [ 0\.   0\.   1\.   0\. ]
 [ 0\.  -0\.   0\.   0\. ]
 [ 0.5  0.5  0\.  -0.5]
 [ 0\.   0\.   0\.  -0\. ]
 [ 0.5 -0.5  0\.  -0.5]]

矩阵上的函数

scipy.linalg模块为矩阵提供了一组有用的函数。对于方阵的基本两个命令是inv(矩阵的逆)和det(行列式)。方阵的幂由标准的指数运算给出;也就是说,如果A是一个方阵,那么A**2表示矩阵乘积A*A,如下面的代码片段所示:

>>> A=numpy.matrix("1,1j;21,3")
>>> A; A*A; A**2

输出如下所示:

matrix([[  1.+0.j,   0.+1.j],
 [ 21.+0.j,   3.+0.j]])
matrix([[  1.+21.j,   0\. +4.j],
 [ 84\. +0.j,   9.+21.j]])
matrix([[  1.+21.j,   0\. +4.j],
 [ 84\. +0.j,   9.+21.j]])

应该指出的是,作为一个类型数组,A*A(或A**2)的乘积是通过平方数组的每个元素来计算的:

>>> numpy.asarray(A); numpy.asarray(A)*numpy.asarray(A); numpy.asarray(A)**2

输出如下所示:

array([[  1.+0.j,   0.+1.j],
 [ 21.+0.j,   3.+0.j]])
array([[   1.+0.j,   -1.+0.j],
 [ 441.+0.j,    9.+0.j]])
array([[   1.+0.j,   -1.+0.j],
 [ 441.+0.j,    9.+0.j]])

更高级的命令计算依赖于涉及矩阵幂的表达式的幂级数表示的矩阵函数,例如矩阵指数(有三种可能性——expmexpm2expm3),矩阵对数logm),矩阵三角函数cosmsinmtanm),矩阵双曲三角函数coshmsinhmtanhm),矩阵符号函数signm),或矩阵平方根sqrtm)。

注意矩阵上正常指数函数的应用与矩阵指数函数的结果之间的区别。

在前一种情况下,我们获得矩阵中每个元素的numpy.exp应用;在后一种情况下,我们实际上根据幂级数表示计算矩阵的指数:

矩阵上的函数

以下代码片段展示了前面的公式:

>>> import numpy
>>> import scipy.linalg
>>> a=numpy.arange(0,2*numpy.pi,1.6)
>>> A = scipy.linalg.toeplitz(a)
>>> print (A)

输出如下所示:

[[ 0\.   1.6  3.2  4.8]
 [ 1.6  0\.   1.6  3.2]
 [ 3.2  1.6  0\.   1.6]
 [ 4.8  3.2  1.6  0\. ]]

让我们对A执行exp()操作:

>>> print (numpy.exp(A))

输出如下所示:

[[   1\.            4.95303242   24.5325302   121.51041752]
 [   4.95303242    1\.            4.95303242   24.5325302 ]
 [  24.5325302     4.95303242    1\.            4.95303242]
 [ 121.51041752   24.5325302     4.95303242    1\.        ]]

让我们对A执行expm()操作:

>>> print (scipy.linalg.expm(A))

输出如下所示:

[[ 1271.76972856   916.49316549   916.63015271  1271.70874469]
 [  916.49316549   660.86560972   660.5306514    916.63015271]
 [  916.63015271   660.5306514    660.86560972   916.49316549]
 [ 1271.70874469   916.63015271   916.49316549  1271.76972856]]

对于稀疏方阵,我们有一个优化的逆函数,以及矩阵指数——scipy.sparse.linalg.invscipy.sparse.linalg.expm

对于一般矩阵,我们有基本的范数函数(norm),以及两种版本的Moore-Penrose 伪逆pinvpinv2)。

再次强调,依赖这些函数而不是手动编码它们的等效表达式是多么重要。例如,注意向量的或矩阵的norm计算,scipy.linalg.norm。让我们通过以下示例向您展示二维向量v=numpy.matrix([x,y])的 2-范数,其中至少一个xy的值非常大——大到使得x*x溢出:

>>> import numpy
>>> import scipy.linalg
>>> x=10**100; y=9; v=numpy.matrix([x,y])
>>> scipy.linalg.norm(v,2)

输出如下所示:

1e+100

现在,让我们执行sqrt()操作:

>>> numpy.sqrt(x*x+y*y)

输出是一个错误,如下所示:

Traceback (most recent call last)
 File "<stdin>", line 1, in <module>
AttributeError: 'long' object has no attribute 'sqrt'

特征值问题和矩阵分解

在矩阵上广泛使用的一组操作是计算和处理方阵的特征值和特征向量。这两个问题在我们能对方阵进行的复杂操作中排名很高,为此已经进行了大量研究,以获得低复杂度和最优内存资源使用的良好算法。SciPy 提供了实现这些想法的先进代码。

对于特征值的计算,scipy.linalg模块提供了三个例程:eigvals(用于任何普通或一般特征值问题)、eigvalsh(如果矩阵是对称的或复数的厄米特矩阵),以及eigvals_banded(如果矩阵是带状的)。为了计算特征向量,我们同样有三个相应的选择:eigeigheigh_banded

所用语法在所有情况下都非常相似。例如,对于特征值的通用情况,我们使用以下代码行,其中矩阵A必须是方阵:

eigvals(A, B=None, overwrite_a=False)

如果我们希望解决一个普通的特征值问题,那么应该只将这个参数传递给该例程。如果我们想推广这个概念,我们可以提供一个额外的方阵(与矩阵A具有相同的维度)。这个方阵通过B参数传递。

该模块还提供了一组广泛的函数,用于计算矩阵的不同分解,如下所示:

  • 主元 LU 分解:此函数允许我们使用lulufactor命令。

  • 奇异值分解:此函数允许我们使用svd命令。为了计算奇异值,我们发出svdvals。如果我们想从奇异值组成奇异值分解中的 sigma 矩阵,我们使用diagsvd例程。如果我们想使用 SVD 计算矩阵范围的正交基,我们可以使用orth命令。

  • Cholesky 分解:此函数允许我们使用choleskycholesky_bandedcho_factor命令。

  • QR 和 QZ 分解:此函数允许我们使用qrqz命令。如果我们想将矩阵与分解的矩阵 Q 相乘,我们使用语法糖qr_multiply,而不是分两步执行此过程。

  • Schur 和 Hessenberg 分解:此函数允许我们使用schurHessenberg命令。如果我们想将实 Schur 形式转换为复数,我们有rsf2csf例程。

在这一点上,我们有一个有趣的应用——图像压缩,它利用了之前解释的一些例程。

通过奇异值分解进行图像压缩

这是一个非常简单的应用,其中大小为n x n的方阵图像A,存储为ndarray被视为矩阵,并且对其执行奇异值分解(SVD)。此操作在以下图中可见:

通过奇异值分解进行图像压缩

s的所有奇异值中选择一个分数,以及它们对应的左奇异向量u和右奇异向量v。我们根据以下图表中给出的公式收集它们来计算一个新的矩阵:

通过奇异值分解进行图像压缩

例如,注意原始(512 个奇异值)和仅使用 32 个奇异值进行的近似之间的相似性:

>>> import numpy
>>> import scipy.misc
>>> from scipy.linalg import svd
>>> import matplotlib.pyplot as plt
>>> img=scipy.misc.lena()
>>> U,s,Vh=svd(img)      # Singular Value Decomposition
>>> A = numpy.dot( U[:,0:32],  # use only 32 singular values
 numpy.dot( numpy.diag(s[0:32]),
 Vh[0:32,:]))
>>> plt.subplot(121,aspect='equal'); plt.imshow(img); plt.gray()
>>> plt.subplot(122,aspect='equal'); plt.imshow(A)
>>> plt.show()

这产生了以下图像,其中左边的图片是原始图像,右边的图片是使用 32 个奇异值进行的近似:

通过奇异值分解进行图像压缩

使用svd近似,我们成功将原始图像的 262,144 个系数(512 * 512)压缩到仅有 32,800 个系数((2 * 32 * 512) + 32),即原始信息的八分之一。

求解器

线性代数的一个基本应用是求解大型线性方程组。对于形式为Ax=b的基本系统,对于任何方阵A和一般矩阵bA的行数与列数相同),我们有两种通用的方法来找到x(对于密集矩阵使用solve,对于稀疏矩阵使用spsolve),语法如下:

solve(A, b, sym_pos=False, lower=False, overwrite_a=False, overwrite_b=False, debug=False)
spsolve(A, b[, permc_spec, use_umfpack])

在 SciPy 中,有一些求解器比这更复杂,它们在矩阵A的结构已知的情况下具有增强的性能。对于密集矩阵,我们在scipy.linalg模块中有三个命令:solve_banded(用于带状矩阵),solveh_banded(如果除了带状,A是厄米特矩阵),以及solve_triangular(用于三角矩阵)。

当无法找到解(例如,如果A是一个奇异矩阵)时,仍然可以找到一个矩阵x,它在最小二乘意义上最小化b-Axnorm。我们可以使用lstsq命令来计算这样一个矩阵,其语法如下:

lstsq(A, b, cond=None, overwrite_a=False, overwrite_b=False)

此函数的输出是一个包含以下内容的元组:

  • 找到的解(作为ndarray

  • 残差的和(作为另一个ndarray

  • 矩阵A的有效秩

  • 矩阵A的奇异值(作为另一个ndarray

让我们用一个简单的例子来说明这个例程,以求解以下系统:

求解器

以下是一个代码片段:

>>> import numpy
>>> import scipy.linalg
>>> A=numpy.mat(numpy.eye(3,k=1))
>>> print(A)

输出如下所示:

[[ 0\.  1\.  0.]
 [ 0\.  0\.  1.]
 [ 0\.  0\.  0.]]

让我们进一步进入代码,并在b上执行以下操作:

>>> b=numpy.mat(numpy.arange(3) + 1).T
>>> print(b)

输出如下所示:

[[1]
 [2]
 [3]]

进一步,让我们执行lstsq操作:

>>> xinfo=scipy.linalg.lstsq(A,b)
>>> print (xinfo[0].T)      # output the solution

输出如下所示:

[[ 0\.  1\.  2.]]

overwrite_选项旨在提高算法的性能,应谨慎使用,因为它们会破坏原始数据。

SciPy 中真正最快的求解器基于矩阵分解。将系统简化为更简单的东西可以轻松解决巨大且真正复杂的线性方程组。我们可以使用本章“矩阵方法”部分的“特征值问题和矩阵分解”以及“通过奇异值分解进行图像压缩”子部分中提出的分解技术来完成这项工作,但当然,SciPy 的哲学是帮助我们内部处理所有与内存和资源相关的烦恼。为此,该模块还提供了lu_solve(基于 LU 分解的解)、cho_solvecho_solve_banded(基于 Cholesky 分解的解)。

最后,你还会找到非常复杂的矩阵方程的求解器——Sylvester方程(solve_sylvester),包括连续和离散的代数Riccati方程(solve_continuous_are, solve_discrete_are)以及连续和离散的Lyapunov方程(solve_discrete_lyapunov, solve_lyapunov)。

大多数矩阵分解和特征值问题的解都是在scipy.sparse.linalg模块中针对稀疏矩阵考虑的,命名约定相似,但计算机资源的使用和错误控制更加稳健。

摘要

本章探讨了使用线性代数模块——linalgsparse.linalg处理向量、矩阵(无论是普通还是稀疏)的方法,这些模块扩展并改进了同名的 NumPy 模块。

在第四章《SciPy 数值分析》中,我们将继续讨论 SciPy 中用于高效执行数值计算的可选项的细节,将涵盖如何评估应用数学和数学物理问题中找到的特殊函数。这将在本章“矩阵方法”部分的“特征值问题和矩阵分解”以及“通过奇异值分解进行图像压缩”子部分的细节中进行讨论。

第四章:SciPy 数值分析

几乎所有数值分析的各个领域都在某些 SciPy 模块中得到了考虑。例如,为了计算特殊函数的值,我们使用scipy.special模块。scipy.interpolate模块负责插值、外推和回归。对于优化,我们有scipy.optimize模块,最后,我们还有scipy.integrate模块用于数值积分。这个最后的模块还作为执行常微分方程数值解的接口。

因此,在本章中,我们将首先广泛探讨如何使用 SciPy 来数值评估在数学物理领域常见的特殊函数。然后,我们将讨论 SciPy 中可用于处理回归、插值和优化问题的模块。

本章以混沌洛伦兹系统的解为例,展示了 SciPy 在寻找常微分方程数值解方面的能力。相应的 IPython Notebook 将帮助您尝试涉及计算中的模块功能,并根据您的具体需求修改每个示例。

特殊函数的评估

scipy.special模块包含有用函数的数值稳定定义。通常,直接在单个值上评估函数并不非常高效。例如,我们宁愿使用 Horner 方案(en.wikipedia.org/wiki/Horner%27s_method)来找到多项式在一点的值,而不是使用原始公式。NumPy 和 SciPy 模块确保通过定义所有函数始终保证这种优化,无论是通过 Horner 方案还是更高级的技术。

便利性和测试函数

所有便利函数都是为了简化计算环境而设计的,用户不需要担心相对误差。这些函数乍一看似乎没有意义,但它们背后的代码中包含了最先进的思想,提供了更快、更可靠的结果。

我们除了在 NumPy 库中定义的函数之外,还有便利函数来找到以度为单位(cosdgsindgtandgcotdg)的三角函数的解;从度、分、秒的表达式计算弧度(radian);常见的幂(exp2用于2**xexp10用于10**x);以及针对变量小值的常见函数(log1p用于log(1 + x)expm1用于exp(x) - 1cosm1用于cos(x) - 1)。

例如,在下面的代码片段中,log1p函数计算1 + x的自然对数。为什么不是简单地将1加到1的值上,然后再取对数呢?让我们比较一下:

>>> import numpy
>>> import scipy.special
>>> a=scipy.special.exp10(-16)
>>> numpy.log(1+a)

输出如下:

0.0

现在让我们在a上使用log1p()函数:

>>> scipy.special.log1p(a)

输出如下:

9.9999999999999998e-17

虽然第一次计算的绝对误差很小,但相对误差是 100%。

与 Lena 图像被视为图像处理中的性能测试一样,我们有一些函数用于在不同的场景中测试不同的算法。

例如,通常我们会用 Rosenbrock 的香蕉函数(en.wikipedia.org/wiki/Rosenbrock_function)来测试最小化代码:

便利性和测试函数

相应的优化模块scipy.optimize有一个例程可以准确评估这个函数(rosen),其导数(rosen_der),其Hessian矩阵(rosen_hess),或者后者的向量乘积(rosen_hess_prod)。

单变量多项式

在 SciPy 中,多项式被定义为 NumPy 类poly1d。这个类有几个方法与多项式相关联,用于计算多项式的系数(coeffs或简单地c),计算多项式的根(r),计算其导数(deriv),计算其符号积分(integ),以及获取其度数(order或简单地o),以及一个方法(variable),它提供了一个字符串,表示我们希望在多项式的正确定义中使用该变量的名称(参见涉及P2的示例)。

为了定义一个多项式,我们必须指明其系数或其根:

>>> import numpy
>>> P1=numpy.poly1d([1,0,1])           # using coefficients
>>> print (P1)

输出如下:

 2
1 x + 1

现在,让我们找到P1的根、阶数和导数:

>>> print (P1.r); print (P1.o); print (P1.deriv())

输出如下:

[ 0.+1.j  0.-1.j]
2
2 x

让我们使用poly1d类:

>>> P2=numpy.poly1d([1,1,1], True)     # using roots
>>> print (P2)

输出如下:

 3     2
1 x - 3 x + 3 x – 1

让我们使用poly1d类和variable方法:

>>> P2=numpy.poly1d([1,1,1], True, variable='z')
>>> print (P2)

输出如下:

 3     2 
1 z - 3 z + 3 z - 1

我们可以通过将多项式视为(向量化)函数或使用__call__ 方法来评估多项式:

>>> P1( numpy.arange(10) )           # evaluate at 0,1,...,9

输出如下:

array([ 1,  2,  5, 10, 17, 26, 37, 50, 65, 82])

让我们发出__call__命令:

>>> P1.__call__(numpy.arange(10))    # same evaluation

输出如下:

array([ 1,  2,  5, 10, 17, 26, 37, 50, 65, 82])

这些想法的一个直接应用是验证前一个例子中使用的1 + x的自然对数的计算。当x接近零时,自然对数可以用以下公式近似:

单变量多项式

这个表达式可以用 Python 使用前面提到的想法输入并评估,如下所示:

>>> import numpy
>>> Px=numpy.poly1d([-(1./2.),1,0])
>>> print(Px)

输出如下:

 2
-0.5 x + 1 x

让我们看看变量a中存储的值:

>>> a=1./10000000000000000.
>>> print(a)

存储在a中的值的输出如下:

1e-16

我们现在在以下代码行中使用Px(包含一维多项式形式)在a上:

>>> Px(a)

输出如下:

9.9999999999999998e-17

结果与之前使用 SciPy 函数scipy.special.log1p得到的结果相同,这验证了计算的正确性。

与多项式相关的一些例程包括:roots(计算零点),polyder(计算导数),polyint(计算积分),polyadd(加多项式),polysub(减多项式),polymul(乘多项式),polydiv(多项式除法),polyval(评估多项式),以及 polyfit(计算两个给定数据数组的最佳拟合多项式)。

常用的二元运算符 +, -, * 和 / 对多项式执行相应的操作。此外,一旦创建了一个多项式,任何与之交互的值列表都会立即转换为多项式。因此,以下四个命令是等价的:

>>> P1=numpy.poly1d([1,0,1])
>>> print(P1)

上述代码行的输出如下:

 2
1 x + 1

让我们看看下面的 print() 命令:

>>> print(numpy.polyadd(P1, numpy.poly1d([2,1])))

输出如下:

 2
1 x + 2 x + 2

让我们看看下面的 print() 命令:

>>> print(numpy.polyadd(P1, [2,1]))

输出如下:

 2
1 x + 2 x + 2

让我们看看下面的 print() 命令:

>>> print(P1 + numpy.poly1d([2,1]))

输出如下:

 2
1 x + 2 x + 2

让我们看看下面的 print() 命令:

>>> print(P1 + [2,1])

输出如下:

 2
1 x + 2 x + 2

注意多项式除法提供了商和余数值,例如:

>>> P1/[2,1]

输出如下:

(poly1d([ 0.5 , -0.25]), poly1d([ 1.25]))

这也可以写成以下形式:

一元多项式

如果一个多项式族相对于内积正交,那么该族中的任意两个多项式的内积为零。这些函数的序列被用作快速求积算法(用于一般函数的数值积分)的骨干。scipy.special 模块包含了 poly1d 定义,并允许快速评估正交多项式族,例如勒让德 (legendre),切比雪夫 (chebyt, chebyu, chebyc, 和 chebys),雅可比 (jacobi),拉格朗日及其推广版本 (laguerregenlaguerre),厄米特及其归一化版本 (hermitehermitenorm),以及根式 (gegenbauer)。还有一些它们的平移版本,例如 sh_legendresh_chebyt 等。

对于正交多项式,由于其丰富的数学结构,通常的多项式评估可以得到改进。在这种情况下,我们永远不会使用之前介绍的通用调用方法来评估它们。相反,我们使用 eval_ 语法。例如,我们使用以下命令来评估雅可比多项式:

>>> eval_jacobi(n, alpha, beta, x)

为了获得 alpha = 0beta = 1 的三阶雅可比多项式在 -1 到 1 之间均匀分布的一千个 x 值的图形,我们可以发出以下命令:

>>> import numpy 
>>> import scipy.special
>>> import matplotlib.pyplot as plt
>>> x=numpy.linspace(-1,1,1000)
>>> plt.plot(x,scipy.special.eval_jacobi(3,0,1,x))
>>> plt.show()

输出如下:

一元多项式

欧拉函数

伽玛函数是一个对数、凸、光滑的复数函数,它对所有非负整数插值了阶乘函数。它在零或任何负整数处未定义。这是最常见的特殊函数,广泛应用于许多不同的应用中,要么单独使用,要么作为许多其他函数定义中的主要成分。伽玛函数在量子物理、天体物理、统计学和流体动力学等众多领域得到应用。

伽玛函数由以下的不定积分定义:

伽玛函数

在整数值处评估伽玛函数给出移位阶乘,这正是 SciPy 中阶乘函数编码的方式。

scipy.special模块包含算法,可以快速评估伽玛函数在任意允许值处的值。它还包含执行文献中出现的伽玛函数最常见组合的评估例程:gammaln用于伽玛绝对值的自然对数,rgamma用于伽玛的倒数,beta用于商,betaln用于后者的自然对数。我们还实现了其导数的对数(psi)的实现。

伽玛函数的一个明显应用是能够执行如果直接进行将几乎不可能由计算机完成的计算。例如,在统计应用中,我们经常处理阶乘的比率。如果这些阶乘对于计算机的精度来说太大,我们就求助于涉及它们的对数的表达式。即使如此,计算ln(a! / b!)也可能是一项不可能的任务(例如,用a = 10**15b = a - 10**10尝试)。一个优雅的解决方案是使用狄利克雷函数psi,通过在ln(gamma(x))函数上应用平均值定理。通过适当的估计,我们获得了出色的近似(对于这个选择的ab的情况):

伽玛函数

让我们看一下以下代码片段:

>>> import scipy.special
>>> 10**10*scipy.special.psi(10**15)

输出如下:

345387763949.10681

黎曼ζ函数

黎曼ζ函数在解析数论中非常重要,并在物理学和概率论中也有应用。它计算任何复值p的 p 级数:

黎曼ζ函数

SciPy 中编码的定义允许对这个函数进行更灵活的推广,如下所示:

黎曼ζ函数

在众多应用中,此函数在粒子物理和动力系统领域(en.wikipedia.org/wiki/Hurwitz_zeta_function)也有应用。

艾里函数和贝里函数

这些是斯托克斯方程的解,通过求解以下微分方程获得:

艾里函数和贝里函数

这个方程有两个线性无关的解,它们都定义为独立变量的实数值的不定积分。airy 命令计算这两个函数(AiBi)以及它们相应的导数(AipBip,分别)。在下面的代码中,我们利用 matplotlib.pyplot 中的 contourf 命令展示了一个 801 x 801 的复数值数组在从 -4 - 4j4 + 4j 的正方形内均匀分布的 Bairy 函数 Bi 输出的实部图像。我们还提供这个图作为使用 mpl_toolkitsmplot3d 模块的三维表面图:

>>> import numpy
>>> import scipy.special
>>> import  matplotlib.pyplot as plt
>>> import mpl_toolkits.mplot3d
>>> x=numpy.mgrid[-4:4:100j,-4:4:100j]
>>> z=x[0]+1j*x[1]
>>> (Ai, Aip, Bi, Bip) = scipy.special.airy(z)
>>> steps = range(int(Bi.real.min()), int(Bi.real.max()),6)
>>> fig=plt.figure()
>>> subplot1=fig.add_subplot(121,aspect='equal')
>>> subplot1.contourf(x[0], x[1], Bi.real, steps)
>>> subplot2=fig.add_subplot(122,projection='3d')
>>> subplot2.plot_surface(x[0],x[1],Bi.real)
>>> plt.show()

输出如下:

Airy 和 Bairy 函数

贝塞尔和斯特夫函数

贝塞尔函数是贝塞尔齐次微分方程的规范解:

贝塞尔和斯特夫函数

这些方程在圆柱坐标系中拉普拉斯方程的解中自然出现。以下图中非齐次贝塞尔微分方程的解被称为斯特夫函数:

贝塞尔和斯特夫函数

在任何情况下,方程的顺序是复数 alpha,它作为一个参数。根据规范解和顺序,贝塞尔和斯特夫函数的处理(和计算)方式不同。

对于贝塞尔函数,我们有算法来生成第一类贝塞尔函数(jv)和第二类贝塞尔函数(ynyv),第一类和第二类汉克尔函数(hankel1hankel2),以及第一类和第二类的修正贝塞尔函数(ivknkv)。它们的语法在所有情况下都是相似的:第一个参数是阶数,第二个参数是独立变量。定义中的 n 组件表示要使用整数作为阶数(因为它们针对这种情况进行了优化编码):

>>> import numpy
>>> import scipy.special
>>> scipy.special.jn(5,numpy.pi)

输出如下:

0.052141184367118461

scipy.special 模块还包含最常见的贝塞尔函数的快速版本(阶数为 0 和 1 的那些):j0(x)j1(x)(第一类 y0(x) 和第二类 y1(x)),等等。有球面贝塞尔函数的定义,如 sph_jn(n,z)sph_yn(z);里卡提-贝塞尔函数,如 riccati_jn(n,x)riccati_yn(n,x);以及所有基本函数的导数,如 jvpyvpkvpivph1vph2vp

对于斯特夫函数,我们有快速算法来计算阶数为 v 的微分方程的解:(struve(v,x)modstruve(v,x))。

其他特殊函数

scipy.special 模块中包含更多特殊函数,这些函数在纯数学和应用数学的许多应用中非常有用。由于篇幅限制,本章不可能列出详尽的列表,我鼓励您使用每个特殊函数集的不同实用工具。其中最有趣的一些包括椭圆函数、高斯超几何函数抛物柱面函数Mathieu 函数球面波函数Kelvin 函数

插值

插值是数值计算中的一个基本方法,它从一组离散的数据点中获得,目的是找到一个插值函数,该函数代表包含数据的某些更高阶结构。最著名的例子是将平面上一系列点(x_ky_k)进行插值,以获得一条通过序列顺序指定的所有点的曲线。

如果前一个序列中的点位置和顺序正确,则可以找到一个单变量函数 y = f(x),使得 y_k = f(x_k)。通常合理地要求这个插值函数是一个多项式、有理函数或更复杂的函数对象。当然,插值也可以在更高维中进行。scipy.interpolate 模块的目标是提供一套完整的、最优编码的应用程序,以解决在不同设置中的这个问题。

让我们来看看最简单的插值数据方法:拉格朗日插值。给定一个大小为 n 的不同 x 值序列和一个同样大小的任意实值序列 y,我们寻求一个 n - 1 次的多项式 p(x),它满足所有从 0 到 n - 1n 个约束条件 p(x[k]) = y[k]。以下代码演示了如何获得一个 9 次多项式,它插值了区间 (-1, 1) 内的正弦函数的 10 个均匀分布的值:

>>> import numpy
>>> import matplotlib.pyplot as plt
>>> import scipy.interpolate
>>> x=numpy.linspace(-1,1,10); xn=numpy.linspace(-1,1,1000)
>>> y=numpy.sin(x)
>>> polynomial=scipy.interpolate.lagrange(x, numpy.sin(x))
>>> plt.plot(xn,polynomial(xn),x,y,'or')
>>> plt.show()

我们将获得以下 plot 展示拉格朗日插值:

插值

拉格朗日插值存在许多问题。第一个明显的缺点是用户不能指定插值的次数;这完全取决于数据。该过程在数值上也非常不稳定,特别是对于包含超过 20 个点的数据集。可以通过让算法依赖于数据集的不同属性来解决此问题,而不仅仅是点的数量和位置。

此外,当我们需要通过添加一些更多实例来更新数据集时,这很不方便;必须从头开始重复该过程。如果数据集的大小不断增加且频繁更新,这证明是不切实际的。为了解决这个问题,BarycentricInterpolator具有add_xiset_yi方法。例如,在下一个会话中,我们首先在 1 到 10 之间插值 10 个均匀分布的正弦函数值。完成后,我们使用 1.5 到 10.5 之间的 10 个更多均匀分布的值更新插值多项式。正如预期的那样,这个操作降低了在插值点内计算的(百分比)相对误差。以下命令被使用:

>>> import numpy
>>> import scipy.interpolate
>>> x1=numpy.linspace(1,10,10); y1=numpy.sin(x1)
>>> Polynomial=scipy.interpolate.BarycentricInterpolator(x1,y1)
>>> exactValues=numpy.sin(x1+0.3)
>>> exactValues

这是exactValues的输出:

array([ 0.96355819,  0.74570521, -0.15774569, -0.91616594, 
-0.83226744,
 0.0168139 ,  0.85043662,  0.90217183,  0.12445442, 
-0.76768581])

通过以下命令来找到interpolatedValues的值:

>>> interpolatedValues=Polynomial(x1+0.3)
>>> interpolatedValues

输出如下:

array([ 0.97103132,  0.74460631, -0.15742869, -0.91631362, 
-0.83216445, 
 0.01670922,  0.85059283,  0.90181323,  0.12588718, 
-0.7825744 ])

通过以下命令来找到PercentRelativeError的值:

>>> PercentRelativeError = numpy.abs((exactValues - interpolatedValues)/interpolatedValues)*100
>>> PercentRelativeError

输出如下:

array([ 0.76960822,  0.14758101,  0.20136334,  0.01611703,  0.01237594, 
 0.62647084,  0.01836479,  0.0397652 ,  1.13812858,  1.90251374])

然后,我们找出interpolatedValues2包含的内容:

>>> x2=numpy.linspace(1.5,10.5,10); y2=numpy.sin(x2)
>>> Polynomial.add_xi(x2,y2)
>>> interpolatedValues2=Polynomial(x1+0.3)
>>> interpolatedValues2

输出如下:

array([ 0.96355818,  0.74570521, -0.15774569, -0.91616594, -0.83226744,
 0.0168139 ,  0.85043662,  0.90217183,  0.12445442, -0.76768581])

让我们找到PercentRelativeError的值,同时考虑interpolatedValues2

>>> PercentRelativeError = numpy.abs((exactValues - interpolatedValues2)/interpolatedValues2)*100
>>> PercentRelativeError

输出如下:

array([  1.26241742e-07,   2.02502252e-09,   5.95225989e-10,
 1.84438143e-11,   8.75086862e-12,   4.14359323e-10,
 1.75194631e-11,   8.52321518e-11,   9.45285176e-09,
 1.29570657e-07])

不仅可以通过点位置进行数据插值,还可以通过这些位置处的导数进行插值。KroghInterpolator命令通过包含重复的x值并按顺序在相应的y值中指示位置和连续导数来实现这一点。

例如,如果我们想要构造一个在原点为零、在x = 1处为一、在x = 2处为二,并且在每个这些三个位置处具有水平切线的一次多项式,我们将发出以下命令:

>>> import numpy
>>> import matplotlib.pyplot as plt
>>> import  scipy.interpolate
>>> x=numpy.array([0,0,1,1,2,2]); y=numpy.array([0,0,1,0,2,0])
>>> interp=scipy.interpolate.KroghInterpolator(x,y)
>>> xn=numpy.linspace(0,2,20)   # evaluate polynomial in larger set
>>> plt.plot(x,y,'o',xn,interp(xn),'r')
>>> plt.show()

这生成了以下图表:

插值

使用分段多项式(PiecewisePolynomial)可以实现更高级的一维插值。这允许控制不同部分的次数以及它们交点处的导数。scipy.interpolate模块中的其他插值选项包括PCHIP 单调三次插值pchip)或甚至是单变量样条InterpolatedUnivariateSpline)。

让我们用一个单变量样条插值的例子来检查。其语法如下:

InterpolatedUnivariateSpline(x, y, w=None, bbox=[None, None], k=3)

xy数组分别包含依赖数据和独立数据。数组w包含用于样条拟合的正权重。双序列bbox参数指定近似区间的边界。最后一个选项表示平滑多项式的次数(k)。

假设我们想要插值以下示例中的五个点。这些点按严格递增的x值排序。我们需要以这种方式执行插值,使用四个三次多项式(每个两个连续点一个),使得每个两个连续部分的至少一阶导数在其交点上一致。我们将按以下步骤进行:

>>> import numpy
>>> import matplotlib.pyplot as plt
>>>import scipy.interpolate
>>> x=numpy.arange(5); y=numpy.sin(x)
>>> xn=numpy.linspace(0,4,40)
>>> interp=scipy.interpolate.InterpolatedUnivariateSpline(x,y)
>>> plt.plot(x,y,'.',xn,interp(xn))
>>> plt.show()

这提供了以下展示使用单变量样条插值的图表:

插值

SciPy 在二维网格插值方面表现出色。它与简单的分段多项式(LinearNDInterpolator)、分段常数(NearestNDInterpolator)或更高级的样条曲线(BivariateSpline)表现良好。它能够在平面上的矩形网格(RectBivariateSpline)或球面上的表面(RectSphereBivariateSpline)上执行样条插值。对于非结构化数据,除了基本的scipy.interpolate.BivariateSpline外,它还能够计算平滑近似(SmoothBivariateSpline)或更复杂的加权最小二乘样条曲线(LSQBivariateSpline)。

以下代码创建了一个从(0, 0)到(9, 9)的正方形内 10 x 10 的均匀分布点网格,并在这些点上评估函数sin(x) * cos(y)。我们使用这些点创建一个scipy.interpolate.BivariateSpline,并在正方形上对所有值评估得到的函数:

>>> import numpy
>>> import scipy.interpolate
>>> import matplotlib.pyplot as plt
>>> from mpl_toolkits.mplot3d import Axes3D
>>> x=y=numpy.arange(10)
>>> f=(lambda i,j: numpy.sin(i)*numpy.cos(j))  # function to interpolate
>>> A=numpy.fromfunction(f, (10,10))           # generate samples
>>> spline=scipy.interpolate.RectBivariateSpline(x,y,A)
>>> fig=plt.figure()
>>> subplot=fig.add_subplot(111,projection='3d')
>>> xx=numpy.mgrid[0:9:100j, 0:9:100j]     # larger grid for plotting
>>> A=spline(numpy.linspace(0,9,100), numpy.linspace(0,9,100))
>>> subplot.plot_surface(xx[0],xx[1],A)
>>> plt.show()

输出如下,它显示了使用二变量样条曲线对 2D 数据进行插值:

插值

回归

回归与插值类似。在这种情况下,我们假设数据是不精确的,我们需要一个具有预定结构的对象来尽可能紧密地拟合数据。最基本的一个例子是将单变量多项式回归应用于一系列点。我们使用polyfit命令来获得这些,我们在本章的“单变量多项式”部分简要讨论了它。例如,如果我们想在区间(0, π/2)内计算 10 个均匀分布点的回归线,以及它们在sin函数下的值,我们将发出以下命令:

>>> import numpy
>>> import scipy
>>> import matplotlib.pyplot as plt
>>> x=numpy.linspace(0,1,10)
>>> y=numpy.sin(x*numpy.pi/2)
>>> line=numpy.polyfit(x,y,deg=1)
>>> plt.plot(x,y,'.',x,numpy.polyval(line,x),'r')
>>> plt.show()

这给出了以下图表,显示了使用polyfit进行线性回归:

回归

如果我们明智地使用参数,曲线拟合也可以使用样条曲线。例如,在之前介绍的单变量样条曲线拟合的情况下,我们可以调整权重、平滑因子、平滑样条曲线的次数等。如果我们想为之前示例中的相同数据拟合一个抛物线样条曲线,我们可以发出以下命令:

>>> import numpy
>>> import scipy.interpolate
>>> import matplotlib.pyplot as plt
>>> x=numpy.linspace(0,1,10)
>>> y=numpy.sin(x*numpy.pi/2)
>>> spline=scipy.interpolate.UnivariateSpline(x,y,k=2)
>>> xn=numpy.linspace(0,1,100)
>>> plt.plot(x,y,'.', xn, spline(xn))
>>> plt.show()

这给出了以下图表,显示了使用样条曲线进行曲线拟合:

回归

从曲线拟合的角度来看回归,有一个通用的例程:scipy.optimize模块中的curve_fit。这个例程使用Levenberg-Marquardt算法最小化一组方程的平方和,并从任何类型的函数(而不仅仅是多项式或样条曲线)中提供最佳拟合。语法很简单:

curve_fit(f, xdata, ydata, p0=None, sigma=None, **kw)

f 参数是一个可调用的函数,代表我们寻求的函数,而 xdataydata 是相同长度的数组,包含要拟合的点 xy 坐标。元组 p0 包含要找到的值的初始猜测,而 sigma 是一个权重向量,必要时可以用作数据的标准差。

我们将通过一个很好的例子来展示其用法。我们首先将在正弦波的一部分生成一些点,振幅 A=18,角频率 w=3π 和相位 h=0.5。我们在数组 y 中添加一些小的随机噪声来损坏数据:

>>> import numpy
>>> import scipy
>>> A=18; w=3*numpy.pi; h=0.5
>>> x=numpy.linspace(0,1,100); y=A*numpy.sin(w*x+h)
>>> y += 4*((0.5-scipy.rand(100))*numpy.exp(2*scipy.rand(100)**2))

我们希望从损坏的数据中估计 Awh 的值,因此从正弦波集合中技术上找到一个曲线拟合。我们首先将三个参数收集到一个列表中,并将它们初始化为一些值,例如,A = 20w = 2πh = 1。我们还构建了一个目标函数的可调用表达式(target_function):

>>> import scipy.optimize
>>> p0 = [20, 2*numpy.pi, 1]
>>> target_function = lambda x,AA,ww,hh: AA*numpy.sin(ww*x+hh)

我们将这些内容以及拟合数据一起输入到 curve_fit 中,以找到所需值:

>>> pF,pVar = scipy.optimize.curve_fit(target_function, x, y, p0)

在我们的任何实验中运行 pF 的一个样本应该会给出三个请求值的准确结果:

>>> print (pF)

前述命令的输出如下:

[ 18.13799397   9.32232504   0.54808516]

这意味着 A 的估计值约为 18.14,w 的估计值非常接近 3π,而 h 在 0.46 和 0.55 之间。以下是在后续图表中显示的原始数据(左侧图表左侧的蓝色)、损坏数据(两个图表中的红色)和计算的正弦波(右侧图表中的黑色)的输出,以及正弦波的计算:

回归

代码过长,无法在此处包含。相反,完整的代码(此处未显示产生的中间图表)可以在本章对应的电子资源 IPython Notebook 中找到。

优化

优化涉及寻找函数的极值或其根。我们已经看到了优化在曲线拟合领域中的力量,但这并不止于此。它几乎应用于工程学的每一个分支,并且执行这些任务的鲁棒算法是每位科学家工具箱中的必备品。

curve_fit 例程实际上是执行最小二乘最小化的一般算法 leastsq 的语法糖,其具有如下强制的语法:

leastsq(func, x0, args=(), Dfun=None, full_output=0,
        col_deriv=0, ftol=1.49012e-8, xtol=1.49012e-8,
        gtol=0.0, maxfev=0, epsfcn=0.0, factor=100, diag=None):

例如,curve_fit 例程可以用 leastsq 调用来代替:

leastsq(error_function,p0,args=(x,y))

在这里,error_function 等于 lambda p,x,y: target_function(x,p[0],p[1],p[2])-y

实现细节在本书相应章节的 IPython 笔记本中给出。SciPy 中的大多数优化例程都可以通过原生 Python 代码或作为相应算法的 Fortran 或 C 经典实现的包装器访问。技术上,我们仍在使用 Fortran 或 C 下使用的相同包,但现在是 Python 内部。例如,实现截断Newton方法的例程可以用fmin_ncg(这是纯 Python)或作为fmin_tnc(这是一个 C 实现的包装器)调用。

最小化

对于一般的优化问题,SciPy 有许多不同的算法。到目前为止,我们已经涵盖了最小二乘算法(leastsq),但我们还有暴力搜索(brute)、模拟退火anneal)、BrentGolden 方法(对于标量函数brentgolden)、下降单纯形算法fmin)、Powell 方法fmin_powell)、非线性共轭梯度或其牛顿版本(fmin_cgfmin_ncg),以及BFGS 算法fmin_bfgs)。

约束最小化在计算上也是可能的,SciPy 有实现L-BFGS-S 算法fmin_l_bfgs_s)、截断牛顿算法(fmin_tnc)、COBYLAfmin_cobyla)或顺序最小二乘规划(fmin_slsqp)的程序。

例如,以下代码比较了使用下降单纯形算法在原点附近寻找 Rosenbrock 函数局部最小值的所有不同方法的输出:

>>> import scipy.optimize
>>> scipy.optimize.fmin(scipy.optimize.rosen,[0,0])

输出如下:

Optimization terminated successfully.
 Current function value: 0.000000
 Iterations: 79
 Function evaluations: 146
array([ 1.00000439,  1.00001064])

自 SciPy 版本 0.11 以来,所有最小化例程都可以通过通用的scipy.optimize.minimize调用,其中method参数指向一个字符串,例如Nelder-Mead(用于下降单纯形)、PowellCGNewton-CGBFGSanneal。对于约束最小化,相应的字符串是L-BFGS-STNC(截断牛顿的)、COBYLASLSQP

minimize( fun, x0, args=(), method='BFGS', jac=None, hess=None, hessp=None, bounds=None, constraints=(),tol=None, callback=None, options=None)

对于包含在scipy.special模块中的大多数特殊函数,我们有精确的算法,允许我们找到它们的零点。例如,对于整数阶的第一类贝塞尔函数,jn_zeros提供了所需数量的根(按升序排列)。我们可以通过以下命令获得四阶贝塞尔 J 函数的前三个根:

>>> import scipy.special
>>> print (scipy.special.jn_zeros(4,3))

输出如下:

[  7.58834243  11.06470949  14.37253667]

对于非特殊标量函数,scipy.optimize模块允许通过大量不同的算法进行根的近似。对于标量函数,我们有粗略的二分法bisect)、牛顿-拉夫森的古典割线法newton),以及更精确和更快的算法,如Ridders 算法ridder)和两种 Brent 方法的版本(brentqbrenth)。

在许多方面寻找多个变量的函数的根是非常具有挑战性的;维度越大,越困难。任何这些算法的有效性都取决于问题,投入一些时间和资源去了解它们都是个好主意。自从 SciPy 的 0.11 版本以来,可以使用相同的例程scipy.optimize.root调用任何设计的方法,该例程的语法如下:

root(fun, x0, args=(), method='hybr', jac=None, tol=None, callback=None, options=None)

通过改变method参数的值到相应的方法字符串,可以得到不同的方法。我们可以选择如下方法:'hybr'用于改进的混合 Powell 方法;'lm'用于改进的最小二乘法;'broyden1''broyden2'分别用于 Broyden 的好方法和坏方法;'diagbroyden'用于对角 Broyden 雅可比近似;'anderson'用于 Anderson 扩展混合;'Krylov'用于雅可比的 Krylov 近似;'linearmixing'用于标量雅可比近似;以及'excitingmixing'用于调整的对角雅可比近似。

对于大规模问题,雅可比的 Krylov 近似或 Anderson 扩展混合通常是最佳选择。

让我们通过一个示例来展示这些技术的能力。考虑以下微分方程组:

根

我们使用 matplotlib.pyplot 库中的 quiver 绘图例程来可视化xy在-0.5 和 2.5 之间的斜率场,从而确定该区域中可能的临界点的位置:

>>> import numpy 
>>> import matplotlib.pyplot as plt 
>>> f=lambda x: [x[0]**2 - 2*x[0] - x[1] + 0.5, x[0]**2 + 4*x[1]**2 - 4]
>>> x,y=numpy.mgrid[-0.5:2.5:24j,-0.5:2.5:24j]
>>> U,V=f([x,y])
>>> plt.quiver(x,y,U,V,color='r', \
 linewidths=(0.2,), edgecolors=('k'), \
 headaxislength=5)
>>> plt.show()

输出如下:

根

注意在平面上有一个区域,其斜率非常小。由于涉及的多项式的次数,最多有四个不同的可能临界点。在这个区域,我们应该能够识别出两个这样的点(实际上只有两个非复数解)。其中一个似乎在(0, 1)附近,另一个在(2, 0)附近。我们使用这两个位置作为搜索的初始猜测:

>>> import scipy.optimize
>>> f=lambda x: [x[0]**2 - 2*x[0] - x[1] + 0.5, x[0]**2 + 4*x[1]**2 - 4]
>>> scipy.optimize.root(f,[0,1])

输出如下:

 status: 1
 success: True
qtf: array([ -4.81190247e-09,  -3.83395899e-09])
nfev: 9
 r: array([ 2.38128242, -0.60840482, -8.35489601])
 fun: array([  3.59529073e-12,   3.85025345e-12])
 x: array([-0.22221456,  0.99380842])
 message: 'The solution converged.'
fjac: array([[-0.98918813, -0.14665209],
 [ 0.14665209, -0.98918813]])

让我们看看第二个案例:

>>> scipy.optimize.root(f,[2,0])

输出如下:

 status: 1
 success: True
qtf: array([  2.08960516e-10,   8.61298294e-11])
nfev: 12
 r: array([-4.56575336, -1.67067665, -1.81464307])
 fun: array([  2.44249065e-15,   1.42996726e-13])
 x: array([ 1.90067673,  0.31121857])
 message: 'The solution converged.'
fjac: array([[-0.39612596, -0.91819618],
 [ 0.91819618, -0.39612596]])

在第一种情况下,我们成功收敛到(-0.22221456, 0.99380842)。在第二种情况下,我们收敛到(1.90067673, 0.31121857)。例程给出了收敛的细节和近似的性质。例如,nfev告诉我们执行了多少次函数调用,而fun表示在找到的位置的函数输出。输出中的其他项反映了在程序中使用的矩阵,如qtfrfjac

积分

SciPy 能够执行非常稳健的数值积分。使用scipy.special模块中的例程,可以准确地计算一组特殊函数的定积分。对于其他函数,scipy.integrate模块中有几种不同的算法可以获得可靠的近似。

指数/对数积分

在指数/对数类别的不定积分和定积分的总结如下:指数积分(expnexpiexp1)、道森积分(dawsn)、高斯误差函数(erferfc)。我们还有斯彭斯双对数(也称为斯彭斯积分)。让我们看一下以下公式:

指数/对数积分

三角函数和双曲三角函数积分

在三角函数和双曲三角函数积分类别中,我们有菲涅耳正弦和余弦积分,以及 sinc 和双曲三角函数积分。让我们看一下以下公式:

三角函数和双曲三角函数积分

在前面列出的积分定义中,伽马符号表示欧拉-马斯刻若尼常数:

三角函数和双曲三角函数积分

椭圆积分

在计算椭圆的弧长时,椭圆积分自然出现。SciPy 遵循椭圆积分的参数表示法:完全(一个参数)和不完全(两个参数)。让我们看一下以下公式:

椭圆积分

伽马和贝塔积分

在伽马和贝塔积分类别中,我们有一个不完全伽马函数、一个补全的不完全伽马积分和一个不完全贝塔积分。这些是这个类别中最有用的函数之一。让我们看一下以下公式:

伽马和贝塔积分

数值积分

对于任何其他函数,我们满足于使用求积公式来近似定积分,如quad(自适应求积)、fixed_quad(固定阶数的 Gauss 求积)、quadrature(固定容差 Gauss 求积)和romberg(龙贝格积分)。对于多于一个变量的函数,我们有dbquad(双重积分)和tplquad(三重积分)方法。在所有情况下,语法都是quad的变体:

quad(func, a, b, args=(), full_output=0, epsabs=1.49e-08, epsrel=1.49e-08, limit=50, points=None, weight=None, wvar=None, wopts=None, maxp1=50, limlst=50)

如果我们有样本而不是函数,我们可以使用trapzcumtrapz(复合梯形规则及其累积版本)、romb(再次使用龙贝格积分)和simps(辛普森规则)等例程。在这些例程中,语法更简单,并改变了参数的顺序。例如,这是调用simps的方式:

>>> simps(y, x=None, dx=1, axis=-1, even='avg')

熟悉QUADPACK库的我们将会发现类似的语法、用法和性能。

若需更多信息,请运行 scipy.integrate.quad_explain() 命令。在本章的 IPython Notebook 中,执行了替代的帮助命令 scipy.integrate.quad,其输出显示在相应的部分。这以极大的详细程度解释了模块结果中包含的所有不同类型的数值积分输出,包括绝对误差的估计和收敛性,以及如果需要的话,对所使用的权重的解释。让我们至少举一个有意义的例子,其中我们积分一个特殊函数,并将求积公式的输出与 scipy.special 中给出的更精确的例程值进行比较:

>>> f=lambda t: numpy.exp(-t)*t**4
>>> from scipy.special import gammainc
>>> from scipy.integrate import quad
>>> from scipy.misc import factorial
>>> print (gammainc(5,1))

输出如下:

0.00365984682734

让我们看看下面的 print 命令:

print('%.19f' % gammainc(5,1))

输出如下:

0.0036598468273437131

让我们进一步查看代码:

>>> import numpy
>>> result,error=quad(f,0,1)/factorial(4)
>>> result

输出如下:

0.0036598468273437122

要使用从样本积分的例程,我们可以灵活地指定数据的频率和长度。对于以下问题,我们可以在相同区间尝试使用 10,000 个样本:

>>> import numpy 
>>> import scipy.integrate
>>> x=numpy.linspace(0,1,10000)
>>> scipy.integrate.simps(f(x)/factorial(4), x)

输出如下:

0.0036598468273469071

常微分方程

与积分类似,SciPy 为一阶常微分方程组提供了一些非常精确的通用求解器:

常微分方程

对于实值函数,我们基本上有两种类型:ode(通过 set_integrator 方法传递选项)和 odeint(接口更简单)。ode 的语法如下:

ode(f,jac=None)

第一个参数 f 是要积分的函数,第二个参数 jac 指的是关于依赖变量的偏导数矩阵(雅可比矩阵)。这创建了一个 ode 对象,具有不同的方法来指示解决系统的算法(set_integrator)、初始条件(set_initial_value)以及要发送给函数或其雅可比矩阵的不同参数。

集成算法的选项有 'vode',用于实值变量系数常微分方程求解器,具有固定领先系数实现(它为非刚性问题提供 Adam 方法,为刚性问题提供 BDF);'zvode',用于复值变量系数常微分方程求解器,具有与前一个选项类似的选择;'dopri5',用于四阶(4)5 阶的 Runge-Kutta 方法;'dop853',用于八阶(5, 3)Runge-Kutta 方法。

下面的代码片段展示了使用 scipy.integrate.ode 解决初值问题的示例:

常微分方程

我们按顺序计算每个步骤,并将其与已知的实际解进行比较,您将注意到实际上没有差异:

>>> import numpy
>>> from scipy.integrate import ode
>>> f=lambda t,y: -20*y        # The ODE
>>> actual_solution=lambda t:numpy.exp(-20*t)  # actual solution
>>> dt=0.01            # time step
>>> solver=ode(f).set_integrator('dop853')  # solver
>>> solver.set_initial_value(1,0)      # initial value
>>> while solver.successful() and solver.t<=1+dt:
 # solve the equation at succesive time steps,
 # until the time is greater than 1
 # but make sure that the solution is successful
 print (solver.t, solver.y, actual_solution(solver.t))
 # We compare each numerical solution with the actual
 # solution of the ODE
 solver.integrate(solver.t + dt)    # solve next step

运行上述代码后,我们得到以下输出:

<scipy.integrate._ode.ode at 0x10eac5e50>
0 [ 1.] 1.0
0.01 [ 0.81873075] 0.818730753078
0.02 [ 0.67032005] 0.670320046036
0.03 [ 0.54881164] 0.548811636094
0.04 [ 0.44932896] 0.449328964117
0.05 [ 0.36787944] 0.367879441171
0.06 [ 0.30119421] 0.301194211912
0.07 [ 0.24659696] 0.246596963942
0.08 [ 0.20189652] 0.201896517995
0.09 [ 0.16529889] 0.165298888222
0.1 [ 0.13533528] 0.135335283237
 ...
0.9 [  1.52299797e-08] 1.52299797447e-08
0.91 [  1.24692528e-08] 1.24692527858e-08
0.92 [  1.02089607e-08] 1.02089607236e-08
0.93 [  8.35839010e-09] 8.35839010137e-09
0.94 [  6.84327102e-09] 6.84327102222e-09
0.95 [  5.60279644e-09] 5.60279643754e-09
0.96 [  4.58718175e-09] 4.58718174665e-09
0.97 [  3.75566677e-09] 3.75566676594e-09
0.98 [  3.07487988e-09] 3.07487987959e-09
0.99 [  2.51749872e-09] 2.51749871944e-09
1.0   [  2.06115362e-09] 2.06115362244e-09

完整的输出显示在本章 IPython 笔记本的相应部分。对于具有复值函数的一阶微分方程组,我们有一个ode的包装器,我们用complex_ode命令调用它。语法和用法与ode类似。

odeint的语法更加直观,并且更符合 Python 风格:

odeint(func, y0, t, args=(), Dfun=None, col_deriv=0, full_output=0, ml=None, mu=None, rtol=None, atol=None, tcrit=None, h0=0.0, hmax=0.0, hmin=0.0, ixpr=0, mxstep=0, mxhnil=0, mxordn=12, mxords=5, printmessg=0)

这个例程最令人印象深刻的部分是,不仅可以指示雅可比矩阵,还可以指示这是否是带状矩阵以及有多少非零对角线位于主对角线之下或之上(使用mlmu选项)。这可以大幅提高计算速度。odeint的另一个惊人特性是能够指示积分的临界点(tcrit)。

现在我们将介绍一个应用,使用本节中介绍的例程来分析洛伦兹吸引子。

洛伦兹吸引子

没有一本关于科学计算的书籍是不回顾洛伦兹吸引子的;SciPy 在计算解和基于微分方程组的想法展示方面都非常出色,我们将在本节中展示如何以及为什么。

考虑一个二维流体单元,从底部加热并从顶部冷却,这与地球大气层中发生的情况非常相似。这会产生可以通过单个偏微分方程建模的对流,对于这个方程,一个合理的近似形式是以下常微分方程组:

洛伦兹吸引子

变量x代表对流翻转速率。变量yz分别代表水平和垂直温度变化。这个系统依赖于四个物理参数,它们的描述远远超出了本书的范围。重要的是,我们可以用这些方程来模拟地球大气层,在这种情况下,参数的一个好选择是sigma = 10.0b = 8/3.0。对于第三参数的某些值,我们有一些解表现出混沌行为的系统。让我们在 SciPy 的帮助下探索这个效应。

在以下代码片段中,我们将使用scipy.integrate模块中的一个求解器以及绘图工具:

>>> import numpy
>>> from numpy import linspace
>>> import scipy
>>> from scipy.integrate import odeint
>>> import matplotlib.pyplot as plt
>>> from mpl_toolkits.mplot3d import Axes3D
>>> sigma=10.0
>>> b=8/3.0
>>> r=28.0
>>> f = lambda x,t: [sigma*(x[1]-x[0]), r*x[0]-x[1]-x[0]*x[2], x[0]*x[1]-b*x[2]]

让我们选择一个足够大且足够密集的时间间隔t,以及任何初始条件y0。然后,执行以下命令:

>>> t=linspace(0,20,2000); y0=[5.0,5.0,5.0]
>>> solution=odeint(f,y0,t)
>>> X=solution[:,0]; Y=solution[:,1]; Z=solution[:,2]

如果我们想要绘制所获得解的 3D 渲染图,可以按照以下方式操作:

>>> import matplotlib.pyplot as plt
>>> plt.gca(projection='3d'); plt.plot(X,Y,Z)
>>> plt.show()

这会产生以下图表,展示了洛伦兹吸引子:

洛伦兹吸引子

这是最具说明性的,精确地展示了解的混沌行为。让我们详细观察垂直温度的波动,以及与垂直温度相比的水平温度波动。执行以下命令:

>>> plt.rcParams['figure.figsize'] = (10.0, 5.0)
>>> plt.subplot(121); plt.plot(t,Z)
>>> plt.subplot(122); plt.plot(Y,Z)
>>> plt.show()

这产生了以下图表,展示了垂直温度随时间的变化(左侧图表)以及水平与垂直温度的关系(右侧图表):

洛伦兹吸引子

摘要

本章通过相应的模块(specialintegrateinterpolateoptimize)探讨了特殊函数、积分、插值和优化,并讨论了常微分方程组的解法。

在第五章《SciPy 信号处理》中,我们将描述 SciPy 模块的功能,用于分析涉及时间序列和空间信号的过程,包括如何在数值数据上执行离散傅里叶变换、如何构建信号、如何对数据进行滤波以及如何插值图像。

第五章. SciPy 信号处理

我们将信号定义为测量时间变化或空间变化现象的数据。声音或心电图是时间变化量的优秀例子,而图像则体现了典型的空间变化情况。动态图像(电影或视频)显然使用了这两种信号类型的技术。

信号处理领域处理此类数据的四个方面——其获取、质量改进、压缩和特征提取。SciPy 有许多例程可以有效地处理四个领域中的任何一项任务。所有这些都被包含在两个低级模块中(scipy.signal是主要的一个,侧重于时变数据,而scipy.ndimage用于图像)。这两个模块中的许多例程都是基于数据的离散傅里叶变换。

在本章中,我们将涵盖以下内容:

  • 背景算法的定义,scipy.fftpack

  • 信号构建的内建函数

  • 函数的展示,用于过滤空间或时间序列信号

关于该主题的更多详细信息可以在Python for Signal ProcessingUnpingco JoséSpringer Publishing中找到。

离散傅里叶变换

离散傅里叶变换(DFT)将任何信号从其时间/空间域转换到相关频率域的信号。这使我们不仅能够分析数据的不同频率,而且当正确使用时,还能实现更快的滤波操作。由于逆傅里叶变换(IFT)的存在,我们还可以将频率域中的信号转换回其时间/空间域。由于我们假设对这种理论有一定程度的熟悉,因此我们不会深入探讨这些算子背后的数学细节。我们将专注于语法和应用。

scipy.fftpack模块中的基本例程计算 DFT 及其逆变换,适用于任何维度的离散信号——fftifft(一维);fft2ifft2(二维);fftnifftn(任意维数)。所有这些例程都假设数据是复值。如果我们事先知道特定的数据集实际上是实值,并且应该提供实值频率,我们则使用rfftirfft,以获得更快的算法。所有这些例程都设计得使得与它们的逆运算组合总是得到恒等变换。所有情况下的语法都是相同的,如下所示:

fft(x[, n, axis, overwrite_x])

第一个参数,x,总是任何数组形式的信号。请注意,fft执行一维变换。这意味着如果x是二维的,例如,fft将输出另一个二维数组,其中每一行是原始每一行的变换。我们可以使用列代替,通过可选参数axis。其余参数也是可选的;n表示变换的长度,overwrite_x移除原始数据以节省内存和资源。我们通常在需要用零填充信号或截断它时玩整数n。对于更高维数,nshape(一个元组)替换,axisaxes(另一个元组)替换。

为了更好地理解输出,通常使用fftshift将零频率移到输出数组的中心是有用的。该操作的逆操作ifftshift也包含在该模块中。以下代码展示了当应用于棋盘图像时,这些例程的一些实际应用:

>>> import numpy
>>> from scipy.fftpack import fft,fft2, fftshift
>>> import matplotlib.pyplot as plt
>>> B=numpy.ones((4,4)); W=numpy.zeros((4,4))
>>> signal = numpy.bmat("B,W;W,B")
>>> onedimfft = fft(signal,n=16)
>>> twodimfft = fft2(signal,shape=(16,16))
>>> plt.figure()
>>> plt.gray()
>>> plt.subplot(121,aspect='equal')
>>> plt.pcolormesh(onedimfft.real)
>>> plt.colorbar(orientation='horizontal')
>>> plt.subplot(122,aspect='equal')
>>> plt.pcolormesh(fftshift(twodimfft.real))
>>> plt.colorbar(orientation='horizontal')
>>> plt.show()

注意一维变换的前四行是相等的(后四行也是如此),而二维变换(一旦移位)在原点处呈现峰值,并在频域中表现出良好的对称性。

在以下屏幕截图(由前面的代码获得)中,左边的图像是fft,右边的图像是 2 x 2 棋盘信号的fft2

离散傅里叶变换

scipy.fftpack模块还提供了离散余弦变换及其逆变换(dctidct),以及许多以所有这些变换为定义的微分和伪微分算子 – diff(用于导数/积分);hilbertihilbert(用于希尔伯特变换);tilbertitilbert(用于周期序列的 h-Tilbert 变换)等等。

信号构造

为了帮助构建具有预定属性的信号,scipy.signal模块提供了一系列文献中最常见的单维波形 – chirpsweep_poly(用于频率扫描余弦发生器),gausspulse(高斯调制的正弦波),sawtoothsquare(用于具有这些名称的波形)。它们都以一维ndarray作为主要参数,表示信号要评估的时间。其他参数根据频率或时间约束控制信号的设计。让我们看看以下代码片段,它说明了我们刚才讨论的这些一维波形的用法:

>>> import numpy
>>> from scipy.signal import chirp, sawtooth, square, gausspulse
>>> import matplotlib.pyplot as plt
>>> t=numpy.linspace(-1,1,1000)
>>> plt.subplot(221); plt.ylim([-2,2])
>>> plt.plot(t,chirp(t,f0=100,t1=0.5,f1=200))   # plot a chirp
>>> plt.title("Chirp signal")
>>> plt.subplot(222); plt.ylim([-2,2])
>>> plt.plot(t,gausspulse(t,fc=10,bw=0.5))      # Gauss pulse
>>> plt.title("Gauss pulse")
>>> plt.subplot(223); plt.ylim([-2,2])
>>> t*=3*numpy.pi
>>> plt.plot(t,sawtooth(t))                     # sawtooth
>>> plt.xlabel("Sawtooth signal")
>>> plt.subplot(224); plt.ylim([-2,2])
>>> plt.plot(t,square(t))                       # Square wave
>>> plt.xlabel("Square signal")
>>> plt.show()

由此代码生成的以下图表显示了chirpgausspulsesawtoothsquare的波形:

信号构造

创建信号的传统方法是从文件中导入它们。这可以通过使用纯 NumPy 例程来实现;例如,fromfile:

fromfile(file, dtype=float, count=-1, sep='')

file参数可以指向一个文件或一个字符串,count参数用于确定要读取的项目数量,而sep表示原始文件/字符串中的分隔符。对于图像,我们有通用的例程imread,在scipy.ndimagescipy.misc模块中:

imread(fname, flatten=False)

fname参数是一个包含图像位置的字符串。例程推断文件类型,并相应地将数据读入数组。如果将flatten参数设置为True,则图像被转换为灰度。请注意,为了使fromfileimread工作,需要安装Python Imaging LibraryPIL)。

还可以使用readwrite例程从scipy.io模块中的wavfile子模块加载.wav文件进行分析。例如,以下代码行使用read例程读取一个音频文件,例如audio.wav

>>> rate,data = scipy.io.wavfile.read("audio.wav")

该命令将一个整数值分配给rate变量,表示文件的采样率(以每秒样本数计),并将一个 NumPy ndarray分配给data变量,其中包含分配给不同音符的数值。如果我们希望将一些一维ndarray 数据写入这种类型的音频文件,采样率由rate变量给出,我们可以通过以下命令实现:

>>> scipy.io.wavfile.write("filename.wav",rate,data)

过滤器

滤波器是对信号的操作,要么去除特征,要么提取某些成分。SciPy 提供了一套完整的已知滤波器以及构建新滤波器的工具。SciPy 中的滤波器列表很长,我们鼓励读者探索scipy.signalscipy.ndimage模块的帮助文档以获得完整的信息。在这些页面上,我们将介绍一些在音频或图像处理中常用的滤波器。

我们首先创建一个需要过滤的信号:

>>> from numpy import sin, cos, pi, linspace
>>> f=lambda t: cos(pi*t) + 0.2*sin(5*pi*t+0.1) + 0.2*sin(30*pi*t) + 0.1*sin(32*pi*t+0.1) + 0.1*sin(47* pi*t+0.8)
>>> t=linspace(0,4,400); signal=f(t)

首先,我们测试了WienerKolmogorov的经典平滑滤波器wiener。我们在plot中展示了原始信号(黑色)和相应的滤波数据,选择 Wiener 窗口大小为 55 个样本(蓝色)。接下来,我们比较了应用具有与之前相同大小的核的中值滤波器medfilt的结果(红色):

>>> from scipy.signal import wiener, medfilt
>>> import matplotlib.pylab as plt
>>> plt.plot(t,signal,'k', label='The signal')
>>> plt.plot(t,wiener(signal,mysize=55),'r',linewidth=3, label='Wiener filtered')
>>> plt.plot(t,medfilt(signal,kernel_size=55),'b',linewidth=3, label='Medfilt filtered')
>>> plt.legend()
>>> plt.show()

这给出了以下图表,显示了平滑滤波器(红色的是Wiener,其起点正好在0.5上方,蓝色的是Medfilt,其起点在0.5下方)的比较:

过滤器

scipy.signal模块中的大多数滤波器都可以适应与任何维度的数组一起工作。但在图像的特定情况下,我们更倾向于使用scipy.ndimage模块中的实现,因为它们是针对这些对象编写的。例如,为了对图像执行中值滤波以平滑处理,我们使用scipy.ndimage.median_filter。让我们看一个例子。我们将首先将 Lena 加载为数组,并用高斯噪声(均值为零,标准差为 16)对图像进行破坏:

>>> from scipy.stats import norm     # Gaussian distribution
>>> import matplotlib.pyplot as plt
>>> import scipy.misc
>>> import scipy.ndimage
>>> plt.gray()
>>> lena=scipy.misc.lena().astype(float)
>>> plt.subplot(221);
>>> plt.imshow(lena)
>>> lena+=norm(loc=0,scale=16).rvs(lena.shape)
>>> plt.subplot(222);
>>> plt.imshow(lena)
>>> denoised_lena = scipy.ndimage.median_filter(lena,3)
>>> plt.subplot(224); 
>>> plt.imshow(denoised_lena)

图像滤波器集有两种类型——统计和形态。例如,在具有统计性质的滤波器中,我们有索贝尔算法,该算法面向边缘检测(曲线上的奇点)。其语法如下:

sobel(image, axis=-1, output=None, mode='reflect', cval=0.0)

可选参数axis表示计算所进行的维度。默认情况下,这始终是最后一个轴(-1)。mode参数,它可以是字符串'reflect''constant''nearest''mirror''wrap'之一,表示在数据不足无法进行计算时如何处理图像的边界。如果mode'constant',我们可以使用cval参数来指定用于边界的值。让我们看看以下代码片段,它说明了sobel滤波器的使用:

>>> from scipy.ndimage.filters import sobel
>>> import numpy
>>> lena=scipy.misc.lena()
>>> sblX=sobel(lena,axis=0); sblY=sobel(lena,axis=1)
>>> sbl=numpy.hypot(sblX,sblY)
>>> plt.subplot(223); 
>>> plt.imshow(sbl) 
>>> plt.show()

以下截图展示了前两个滤波器的实际应用——Lena(左上角)、噪声 Lena(右上角)、索贝尔边缘图(左下角)和中值滤波器(右下角):

滤波器

LTI 系统理论

为了研究时不变线性系统对输入信号的响应,我们在scipy.signal模块中拥有许多资源。实际上,为了简化对象的表示,我们有一个lti类(线性时不变类),以及与之相关的方法,如bode(用于计算博德幅度和相位数据)、impulseoutputstep

无论我们是在处理连续时间还是离散时间线性系统,我们都有模拟此类系统(连续的lsimlsim2,离散的dsim)、计算冲激(连续的impulseimpulse2,离散的dimpulse)和阶跃(连续的stepstep2,离散的dstep)的例程。

使用cont2discrete可以将系统从连续转换为离散,但在任何情况下,我们都能为任何系统提供其任何表示形式,以及从一种表示形式转换为另一种表示形式。例如,如果我们有传递函数的零点z、极点p和系统增益k,我们可以使用zpk2tf(z,p,k)获得多项式表示(先分子后分母)。如果我们有传递函数的分子(num)和分母(dem),我们可以使用tf2ss(num,dem)获得状态空间。这个操作可以通过ss2tf例程进行逆操作。从零极增益到/从状态空间的表示形式变化也在(zpk2ssss2zpk)模块中考虑。

过滤器设计

scipy.signal模块中有一些例程允许使用不同的方法创建不同类型的过滤器。例如,bilinear函数使用双线性变换将模拟转换为数字滤波器。有限脉冲响应FIR)滤波器可以通过firwinfirwin2例程使用窗口方法设计。无限脉冲响应IIR)滤波器可以通过iirdesigniirfilter以两种不同的方式设计。巴特沃斯滤波器可以通过butter例程设计。还有设计切比雪夫cheby1cheby2)、卡乌尔ellip)和 Bessel(bessel)滤波器的例程。

窗口函数

没有广泛列表的窗口——在特定域外为零值的数学函数——的信号处理计算系统将是不完整的。在本节中,我们将使用scipy.signal模块中实现的一些编码窗口,通过卷积设计非常简单的平滑滤波器。

我们将在之前使用的相同一维信号上测试它们,以便进行比较。

我们首先展示四个著名的窗口函数的图表——箱形、汉明、布莱克曼-哈里斯(Nuttall 版本)和三角形。我们将使用 31 个样本的大小:

>>> from scipy.signal import boxcar, hamming, nuttall, triang
>>> import matplotlib.pylab as plt
>>> windows=['boxcar', 'hamming', 'nuttall', 'triang']
>>> plt.subplot(121)
>>> for w in windows:
 eval( 'plt.plot(' + w + '(31))' )
 plt.ylim([-0.5,2]); plt.xlim([-1,32])
 plt.legend(windows)

为了绘图目的,我们需要将原始信号扩展十五个样本:

>>> plt.subplot(122)
>>> import numpy
>>> from numpy import sin, cos, pi, linspace
>>> f=lambda t: cos(pi*t) + 0.2*sin(5*pi*t+0.1) + 0.2*sin(30*pi*t) + 0.1*sin(32*pi*t+0.1) + 0.1*sin(47* pi*t+0.8)
>>> t=linspace(0,4,400); signal=f(t)
>>> extended_signal=numpy.r_[signal[15:0:-1],signal,signal[-1:-15:- 1]]
>>> plt.plot(extended_signal,'k')

最后一步是过滤器本身,我们通过简单的卷积来实现:

>>> for w in windows:
 window = eval( w+'(31)')
 output=numpy.convolve(window/window.sum(),signal)
 plt.plot(output,linewidth=2)
 plt.ylim([-2,3]); plt.legend(['original']+windows)
>>> plt.show()

这会产生以下输出,显示信号与不同窗口的卷积:

窗口函数

图像插值

对图像进行某些几何操作的过滤器集合在经典上被称为图像插值,因为这种数值技术是所有算法的根源。实际上,SciPy 将这些功能收集在子模块scipy.ndimage.interpolation下,以便于访问。本节最好通过示例来解释,涵盖几何变换中最有意义的例程。起点是图像,Lena。我们现在假设子模块中的所有函数都已导入会话中。

我们需要在图像的域上应用一个仿射变换,如下矩阵形式给出:

图像插值

要在图像域上应用变换,我们发出affine_transform命令(请注意,语法是自解释的):

>>> import scipy.misc
>>> import numpy 
>>> import matplotlib.pylab as plt 
>>> from scipy.ndimage.interpolation import affine_transform
>>> lena=scipy.misc.lena() 
>>> A=numpy.mat("0,1;-1,1.25"); b=[-400,0]
>>> Ab_Lena=affine_transform(lena,A,b,output_shape=(512*2.2,512*2.2))
>>> plt.gray() 
>>> plt.subplot(121) 
>>> plt.imshow(Ab_Lena)

对于一般变换,我们使用geometric_transform例程,其语法如下:

geometric_transform(input, mapping, output_shape=None, 
                    output=None, order=3, mode='constant',
cval=0.0, prefilter=True, extra_arguments=(),
extra_keywords={})

我们需要提供一个从元组到元组的 2 阶映射作为参数映射。例如,我们希望对复数值z(假设abcd的值已经定义,并且它们是复数值)应用莫比乌斯变换,如下公式所示:

图像插值

我们必须以以下方式编写代码:

>>> def f(z):
 temp = a*(z[0]+1j*z[1]) + b
 temp /= c*(z[0]+1j*z[1])+d
 return (temp.real, temp.imag)

在这两个函数中,无法直接用公式计算的网格值使用样条插值推断。我们可以使用order参数指定此插值的顺序。定义域外的点不进行插值,而是根据某些预定的规则填充。我们可以通过传递一个字符串到mode选项来强制执行此规则。选项有 – 'constant',使用我们可以通过cval选项施加的常数值;'nearest',在每个水平线上继续插值的最后一个值;和'reflect''wrap',这些是自解释的。

例如,对于值a = 2**15*(1+1j)b = 0c = -2**8*(1-1j*2)d = 2**18-1j*2**14,在应用reflect模式后,我们得到的结果,如代码行之后所示:

>>> from scipy.ndimage.interpolation import geometric_transform 
>>> a = 2**15*(1+1j); b = 0; c = -2**8*(1-1j*2); d = 2**18-1j*2**14
>>> Moebius_Lena = geometric_transform(lena,f,mode='reflect')
>>> plt.subplot(122); 
>>> plt.imshow(Moebius_Lena) 
>>> plt.show()

以下截图显示了仿射变换(左侧)和几何变换(右侧):

图像插值

对于旋转、平移或缩放的特殊情况,我们有语法糖例程,rotate(input,angle)shift(input, offset)zoom(input,dilation_factor)

对于任何图像,我们知道数组在域中像素值(具有整数坐标)的值。但没有整数坐标的位置对应的值是什么?我们可以使用有价值的例程map_coordinates来获取这些信息。请注意,语法可能令人困惑,特别是对于coordinates参数:

map_coordinates(input, coordinates, output=None, order=3, 
                mode='constant', cval=0.0, prefilter=True)

例如,如果我们希望评估 Lena 在位置(10.5,11.7)和(12.3,1.4),我们收集坐标作为序列的序列;第一个内部序列包含x值,第二个,包含y值。我们可以使用order指定使用的样条顺序,如果需要,在域外指定插值方案,如前例所示。让我们使用以下代码片段评估 Lena 在(我们刚才在示例中讨论的位置):

>>> import scipy.misc 
>>> from scipy.ndimage.interpolation import map_coordinates
>>> lena=scipy.misc.lena().astype(float)
>>> coordinates=[[10.5, 12.3], [11.7, 1.4]]
>>> map_coordinates(lena, coordinates, order=1)

输出如下所示:

array([ 157.2 ,  157.42])

此外,我们使用order=2评估 Lena,如下代码行所示:

>>> map_coordinates(lena, coordinates, order=2)

输出如下所示:

array([ 157.80641507,  157.6741489 ])

形态学

我们还有可能基于数学形态学创建和应用图像过滤器,这些过滤器既可以应用于二值图像,也可以应用于灰度图像。四种基本的形态学操作是开运算(binary_opening)、闭运算(binary_closing)、膨胀(binary_dilation)和腐蚀(binary_erosion)。请注意,每个这些过滤器的语法都非常简单,因为我们只需要两个成分——要过滤的信号和执行形态学操作的结构元素。让我们来看看这些形态学操作的一般语法:

binary_operation(signal, structuring_element)

我们已经展示了这些操作在获取氧化物结构模型中的应用,但我们将把这个例子推迟到我们介绍第七章中的三角剖分和 Voronoi 图概念时再进行。

我们可以使用这四种基本形态学操作的组合来创建更复杂的过滤器,用于去除孔洞、点中击或未击变换(用于在二值图像中找到特定模式的位置)、降噪、边缘检测等等。该模块甚至为我们提供了一些最常用的这种方式的过滤器。例如,对于文本中字母 e 的位置(我们已在第二章中介绍过,作为 SciPy 计算几何第一步的 NumPy 数组处理,作为相关性的应用),我们可以使用以下命令代替:

>>> binary_hit_or_miss(text, letterE)

为了比较的目的,让我们将此命令应用于第二章中的示例,作为 SciPy 计算几何第一步的 NumPy 数组处理

>>> import numpy
>>> import scipy.ndimage
>>> import matplotlib.pylab as plt
>>> from scipy.ndimage.morphology import binary_hit_or_miss
>>> text = scipy.ndimage.imread('CHAP_05_input_textImage.png')
>>> letterE = text[37:53,275:291]
>>> HitorMiss = binary_hit_or_miss(text, structure1=letterE, origin1=1) 
>>> eLocation = numpy.where(HitorMiss==True)
>>> x=eLocation[1]; y=eLocation[0]
>>> plt.imshow(text, cmap=plt.cm.gray, interpolation='nearest')
>>> plt.autoscale(False)
>>> plt.plot(x,y,'wo',markersize=10)
>>> plt.axis('off')
>>> plt.show()

这将生成以下输出,读者应该将其与第二章中的对应输出进行比较,作为 SciPy 计算几何第一步的 NumPy 数组处理

形态学

对于灰度图像,我们可以使用结构元素(structuring_element)或足迹。因此,语法略有不同:

grey_operation(signal, [structuring_element, footprint, size, ...])

如果我们希望使用一个完全平坦且矩形的结构元素(所有1),那么只需要用一个元组来表示其大小即可。例如,为了在我们的经典图像 Lena 上对size (15,15)的平坦元素进行灰度膨胀,我们发出以下命令:

>>> grey_dilation(lena, size=(15,15))

scipy.ndimage 模块中编码的最后一种形态学操作执行距离和特征变换。距离变换创建一个映射,将每个像素分配到最近的对象的距离。特征变换则提供最近背景元素的索引。这些操作用于将图像分解为不同的标签。我们甚至可以选择不同的度量标准,如欧几里得距离、棋盘距离和出租车距离。使用暴力算法的距离变换(distance_transform)的语法如下:

distance_transform_bf(signal, metric='euclidean', sampling=None,
return_distances=True, return_indices=False,
                      distances=None, indices=None)

我们用字符串如 'euclidean''taxicab''chessboard' 来指示度量标准。如果我们想提供特征变换,我们将 return_distances 设置为 False,将 return_indices 设置为 True

使用更复杂的算法,也有类似的程序可用 – distance_transform_cdt(使用 chamfering 计算出租车和棋盘距离)。对于欧几里得距离,我们也有 distance_transform_edt。所有这些都使用相同的语法。

摘要

在本章中,我们探讨了信号处理(任何维度),包括通过它们的离散傅里叶变换来处理频域中的信号。这些对应于 fftpacksignalndimage 模块。

第六章, SciPy 数据挖掘,将探讨 SciPy 中包含的工具,以解决统计和数据挖掘问题。除了标准统计量之外,还将介绍特殊主题,如核估计、统计距离和大数据集的聚类。

第六章:SciPy 数据挖掘

本章涵盖了数学和统计学中处理数据收集、组织、分析和解释的分支。这些应用和操作分布在多个模块和子模块中:scipy.stats(纯统计工具)、scipy.ndimage.measurements(数据分析和组织)、scipy.spatial(空间算法和数据结构),以及最终的聚类包scipy.clusterscipy.cluster聚类包包括两个子模块:scipy.cluster.vq(矢量量化)和scipy.cluster.hierarchy(用于层次和聚合聚类)。

如前几章所述,假设读者对主题熟悉。我们的重点是向您展示一些可用于执行统计计算的 SciPy 函数,而不是教授它。因此,您可以在阅读您喜欢的相关书籍的同时阅读本章,以便您可以全面探索本章中提供的示例以及额外的数据集。

然而,我们应该提到,Python 中还有其他专门模块可以用于从不同角度探索这个主题。其中一些(本书中未涉及)是模块化数据处理工具包MDP)(mdp-toolkit.sourceforge.net/install.html)、scikit-learn(scikit-learn.org/)和Statsmodels(statsmodels.sourceforge.net/)。

在本章中,我们将涵盖以下内容:

  • 通过 SciPy 计算的标准描述性统计量

  • SciPy 中处理统计分布的内置函数

  • Scipy 查找区间估计的功能

  • 执行统计相关性和一些统计测试的计算,分布拟合和统计距离

  • 聚类示例

描述性统计

我们经常需要分析数据,其中某些特征被分组在不同的区域,每个区域具有不同的尺寸、值、形状等。scipy.ndimage.measurements子模块具有完成此任务的正确工具,最佳方式是通过详尽的示例来展示模块的功能。例如,对于零和一的二值图像,可以使用label命令标记每个连通区域(值为一的连续像素区域),并使用该命令获取这些区域的数量。如果我们希望获取这些区域的质心,可以使用center_of_mass命令。我们可以在第七章 SciPy 计算几何的应用中再次看到这些操作的实际应用。

对于非二进制数据,scipy.ndimage.measurements子模块提供了通常的基本统计测量(极值值和位置,均值,标准差,总和,方差,直方图等)。

对于更高级的统计测量,我们必须访问来自scipy.stats模块的函数。我们现在可以使用几何平均数和调和平均数(gmeanhmean),中位数,众数,偏度,各种矩,或峰度(medianmodeskewmomentkurtosis)。为了概述数据集最重要的统计特性,我们更喜欢使用describe过程。我们还可以计算项目频率(itemfreq),百分位数(scoreatpercentilepercentileofscore),直方图(histogramhistogram2),累积频率和相对频率(cumfreqrelfreq),标准误差(sem),以及信噪比(signaltonoise),这些总是很有用的。

分布

scipy.stats模块的主要优势之一是编码了大量的分布,包括连续和离散。列表令人印象深刻,至少有 80 个连续分布和 10 个离散分布。

使用这些分布最常见的方法是生成随机数。我们一直在使用这种技术来污染我们的图像,例如:

>>> import scipy.misc 
>>> from scipy.stats import signaltonoise 
>>> from scipy.stats import norm     # Gaussian distribution
>>> lena=scipy.misc.lena().astype(float)
>>> lena+= norm.rvs(loc=0,scale=16,size=lena.shape)
>>> signaltonoise(lena,axis=None)

输出如下所示:

array(2.459233897516763)

让我们看看 SciPy 处理分布的方法。首先,创建一个随机变量类(在 SciPy 中,对于连续随机变量有rv_continuous类,对于离散情况有rv_discrete类)。每个连续随机变量都有一个相关的概率密度函数(pdf),累积分布函数(cdf),生存函数及其逆函数(sfisf),以及所有可能的描述性统计。它们还关联了随机变量rvs,这是我们实际生成随机实例所用的。例如,对于一个参数b = 5的帕累托连续随机变量,为了检查这些特性,我们可以发出以下命令:

>>> import numpy
>>> from scipy.stats import pareto
>>> import matplotlib.pyplot as plt
>>> x=numpy.linspace(1,10,1000)
>>> plt.subplot(131); plt.plot(pareto.pdf(x,5))
>>> plt.subplot(132); plt.plot(pareto.cdf(x,5))
>>> plt.subplot(133); plt.plot(pareto.rvs(5,size=1000))
>>> plt.show()

这给出了以下图表,显示了概率密度函数(左),累积分布函数(中间),和随机生成(右):

分布

区间估计,相关度量,和统计测试

我们在第一章中简要介绍了 SciPy 的区间估计作为入门示例:bayes_mvs,语法非常简单,如下所示:

bayes_mvs(data, alpha=0.9)

它返回一个包含三个参数的元组,其中每个参数的形式为(center, (lower, upper))。第一个参数指的是均值;第二个指的是方差;第三个指的是标准差。所有区间都是根据alpha给出的概率计算的,默认值为0.9

我们可以使用 linregress 程序来计算二维数据 x 的回归线,或者两组一维数据,xy。我们还可以计算不同的相关系数,以及它们对应的 p 值。我们有 皮尔逊相关系数pearsonr),斯皮尔曼秩相关spearmanr),点二列相关pointbiserialr),以及 肯德尔 taukendalltau)用于有序数据。在所有情况下,语法都是相同的,因为它只需要一个二维数据数组,或者两个长度相同的一维数据数组。

SciPy 也包含了大多数已知的统计测试和程序:t 检验ttest_1samp 用于一组分数,ttest_ind 用于两个独立样本的分数,或 ttest_rel 用于两个相关样本的分数),柯尔莫哥洛夫-斯米尔诺夫检验kstestks_2samp)用于拟合优度,单因素 卡方检验chisquare),以及更多。

让我们用一个教科书上的例子来说明这个模块的一些常规操作,这个例子基于 Timothy Sturm 对控制设计的 研究。

为了转动一个通过螺丝动作移动指示器的旋钮,25 名右手使用者被要求使用他们的右手。有两种相同的仪器,一个有右手螺纹,旋钮顺时针转动,另一个有左手螺纹,旋钮逆时针转动。以下表格给出了每个受试者将指示器移动到固定距离所需的时间(秒):

受试者 1 2 3 4 5 6 7 8 9 10
右螺纹 113 105 130 101 138 118 87 116 75 96
左螺纹 137 105 133 108 115 170 103 145 78 107
受试者 11 12 13 14 15 16 17 18 19 20
右螺纹 122 103 116 107 118 103 111 104 111 89
左螺纹 84 148 147 87 166 146 123 135 112 93
受试者 21 22 23 24 25
右螺纹 78 100 89 85 88
左螺纹 76 116 78 101 123

我们可以通过一个简单的单样本 t 统计量分析来得出结论,即右手使用者发现右手螺纹更容易使用。我们将如下方式将数据加载到内存中:

>>> import numpy
>>> data = numpy.array([[113,105,130,101,138,118,87,116,75,96, \
 122,103,116,107,118,103,111,104,111,89,78,100,89,85,88], \
 [137,105,133,108,115,170,103,145,78,107, \
 84,148,147,87,166,146,123,135,112,93,76,116,78,101,123]])

每一行的差异表示哪个旋钮更快,以及快了多少时间。我们可以轻松地获得这些信息,并对它进行一些基本的统计分析。我们将从计算平均值、标准差和具有 10 个分箱的直方图开始:

>>> dataDiff = data[1,:]-data[0,:]
>>> dataDiff.mean(), dataDiff.std()

输出如下所示:

(13.32, 22.472596645692729)

让我们通过以下一系列命令绘制直方图:

>>> import matplotlib.pyplot as plt
>>> plt.hist(dataDiff)
>>> plt.show()

这产生了以下直方图:

区间估计、相关度量统计测试

鉴于这个直方图,假设数据服从正态分布并不牵强。如果我们假设这是一个合适的简单随机样本,使用 t 统计量是合理的。我们想要证明左旋螺纹的转动时间比右旋螺纹长,因此我们将dataDiff的均值设定为与零均值(这表明两种螺纹所需时间相同)进行对比。

通过简单的命令计算双样本 t 统计量和双样本检验的 p 值,如下所示:

>>> from scipy.stats import ttest_1samp
>>> t_stat,p_value=ttest_1samp(dataDiff,0.0)

然后计算单侧检验的 p 值:

>>> print (p_value/2.0)

输出如下所示:

0.00389575522747

注意,这个 p 值比通常的阈值alpha = 0.05alpha = 0.1都要小得多。因此,我们可以保证我们有足够的证据来支持以下说法:右旋螺纹比左旋螺纹转动所需时间更短。

分布拟合

在 Timothy Sturm 的例子中,我们声称某些数据的直方图似乎符合正态分布。SciPy 有几个例程可以帮助我们近似随机变量的最佳分布,以及最佳拟合的参数。例如,对于那个问题中的数据,实现最佳拟合的正态分布的均值和标准差可以通过以下方式找到:

>>> from scipy.stats import norm     # Gaussian distribution
>>> mean,std=norm.fit(dataDiff)

现在,我们可以绘制数据的(归一化)直方图,以及计算出的概率密度函数,如下所示:

>>> plt.hist(dataDiff, normed=1)
>>> x=numpy.linspace(dataDiff.min(),dataDiff.max(),1000)
>>> pdf=norm.pdf(x,mean,std)
>>> plt.plot(x,pdf)
>>> plt.show()

我们将获得以下图表,显示对dataDiff最佳拟合的正态分布的最大似然估计:

分布拟合

我们甚至可以不指定任何特定的分布来拟合最佳概率密度函数,这要归功于一种非参数技术,核密度估计。我们可以在scipy.stats.kde子模块中找到一个执行高斯核密度估计的算法。让我们通过以下示例来展示,使用与之前相同的数据:

>>> from scipy.stats import gaussian_kde
>>> pdf=gaussian_kde(dataDiff)

如前所述的稍微不同的绘图会给出以下图表,显示通过核密度估计在dataDiff上获得的概率密度函数:

分布拟合

以下为完整的代码块:

>>> from scipy.stats import gaussian_kde
>>> pdf = gaussian_kde(dataDiff)
>>> pdf = pdf.evaluate(x)
>>> plt.hist(dataDiff, normed=1)
>>> plt.plot(x,pdf,'k')
>>> plt.savefig("hist2.png")
>>> plt.show()

为了比较目的,最后两个图表可以合并成一个:

>>> plt.hist(dataDiff, normed=1)
>>> plt.plot(x,pdf,'k.-',label='Kernel fit')
>>> plt.plot(x,norm.pdf(x,mean,std),'r',label='Normal fit')
>>> plt.legend() 
>>> plt.savefig("hist3.png")
>>> plt.show()

输出是以下综合图表:

分布拟合

距离

在数据挖掘领域,经常需要确定训练集中哪些成员与未知测试实例最接近。对于任何执行搜索的算法,拥有一个好的不同距离函数集是必不可少的,SciPy 为此目的在scipy.spatial模块的距离子模块中提供了一个巨大的优化编码函数集合。列表很长。除了欧几里得、平方欧几里得或标准化欧几里得之外,我们还有许多其他函数——Bray-CurtisCanberraChebyshevManhattan、相关距离、余弦距离、dice 不相似性HammingJaccard-NeedhamKulsinskiMahalanobis等等。在大多数情况下,语法都很简单:

distance_function(first_vector, second_vector)

语法不同的只有三种情况,即闵可夫斯基、马氏和标准化欧几里得距离,其中距离函数需要整数(用于闵可夫斯基距离定义中范数的阶数),马氏距离的情况需要协方差(但这是一个可选要求),或者方差矩阵来标准化欧几里得距离。

现在我们来看一个有趣的练习,用于可视化 Minkowski 度量中的单位球:

>>> import numpy 
>>> from scipy.spatial.distance import minkowski 
>>> Square=numpy.mgrid[-1.1:1.1:512j,-1.1:1.1:512j]
>>> X=Square[0]; Y=Square[1]
>>> f=lambda x,y,p: minkowski([x,y],[0.0,0.0],p)<=1.0
>>> Ball=lambda p:numpy.vectorize(f)(X,Y,p)

我们创建了一个函数Ball,它创建一个 512 x 512 的布尔值网格。该网格代表一个以原点为中心、边平行于坐标轴的长度为 2.2 的正方形,其上的真实值代表所有在 Minkowski 度量中参数p的单位球内的网格点。我们只需将其图形化展示,如下例所示:

>>> import matplotlib.pylab as plt
>>> plt.imshow(Ball(3), cmap = plt.cm.gray)
>>> plt.axis('off')
>>> plt.subplots_adjust(left=0.0127,bottom=0.0164,\
 right=0.987,top=0.984)
>>> plt.show()

这产生了以下结果,其中Ball(3)是参数p = 3的 Minkowski 度量的单位球:

距离

我们觉得有必要发出以下四个重要警告:

  • 第一次警告:只要可能,我们必须使用这些过程而不是创建我们自己的对应距离函数的定义。它们保证了更快的结果,并优化了处理输入过大或过小的情况的编码。

  • 第二次警告:这些函数在比较两个向量时工作得很好;然而,对于许多向量的成对计算,我们必须求助于pdist过程。此命令接受一个表示m个维度为n的向量的m x n数组,并计算它们之间的距离。我们使用选项 metric 和所需的附加参数来指示要使用的距离函数。例如,对于五个随机选择的四维向量(整数值为10-1)的曼哈顿(cityblock)距离,我们可以发出以下命令:

    >>> import scipy.stats
    >>> from scipy.spatial.distance import pdist
    >>> V=scipy.stats.randint.rvs(0.4,3,size=(5,4))-1
    >>> print (V)
    
    

    输出如下所示:

    [[ 1  0  1 -1]
     [-1  0 -1  0]
     [ 1  1  1 -1]
     [ 1  1 -1  0]
     [ 0  0  1 -1]]
    
    

    让我们看看以下pdist命令:

    >>> pdist(V,metric='cityblock')
    
    

    输出如下所示:

    array([ 5.,  1.,  4.,  1.,  6.,  3.,  4.,  3.,  2.,  5.])
    
    

    这意味着,如果v1 = [1,0,1,-1]v2 = [-1,0,-1,0]v3 = [1,1,1,-1]v4 = [1,1,-1,0]v5 = [0,0,1,-1],那么v1v2的曼哈顿距离是 5。v1v3的距离是 1;到v1v4,4;到v1v5,1。从v2v3的距离是 6;从v2v4,3;从v2v5,4。从v3v4的距离是 3;从v3v5,2。最后,v4v5的距离是 5,这是输出的最后一个条目。

  • 第三次警告:在计算两个输入集合中每对之间的距离时,我们应该使用cdist过程,它具有类似的语法。例如,对于两个由随机选择的四维布尔向量组成的集合,相应的 Jaccard-Needham 不相似度计算如下:

    >>> from scipy.spatial.distance import cdist
    >>> V=scipy.stats.randint.rvs(0.4, 2, size=(3,4)).astype(bool)
    >>> W=scipy.stats.randint.rvs(0.4, 3, size=(2,4)).astype(bool)
    >>> cdist(V,W,'jaccard')
    array([[ 0.75      ,  1\.        ],
     [ 0.75      ,  1\.        ],
     [ 0.33333333,  0.5       ]])
    
    

    即,如果V中的三个向量分别标记为v1v2v3,而W中的两个向量分别标记为w1w2,那么v1w1之间的不相似度为 0.75;v1w2之间的不相似度为 1;依此类推。

  • 第四次警告:当我们有大量数据点并且需要解决最近邻问题(例如,定位数据中与新的实例点最近的元素)时,我们很少通过暴力方法来做。执行此搜索的最佳算法基于 k 维树的概念。SciPy 有两个类来处理这些对象—KDTreecKDTree。后者是前者的子集,由于它是用 C 代码包装的,所以稍微快一点,但用途非常有限。它只有query方法来查找输入的最近邻。语法简单,如下所示:

    KDTree(data, leafsize=10)
    

    这创建了一个包含二叉树的结构,非常适合快速搜索算法的设计。leafsize选项表示在什么级别上必须放弃基于二叉树结构的搜索,转而使用暴力搜索。

    KDTree类相关联的其他方法包括—count_neighbors,用于计算可以与另一个KDTree形成的邻近对的数量;query_ball_point,用于查找给定距离内的所有点;query_ball_treequery_pairs,用于查找一定距离内的所有点对;以及sparse_distance_matrix,它计算两个KDTree类之间的距离稀疏矩阵。

    让我们用一个包含 10 个随机生成的四维整数点的数据集来看看它的实际应用:

    >>> from scipy.spatial import KDTree
    >>> data=scipy.stats.randint.rvs(0.4,10,size=(10,4))
    >>> print (data)
    
    

    输出如下所示:

    [[8 6 1 1]
     [2 9 1 5]
     [4 8 8 9]
     [2 6 6 4]
     [4 1 2 1]
     [3 8 7 2]
     [1 1 3 6]
     [5 2 1 5]
     [2 5 7 3]
     [6 0 6 9]]
    >>> tree=KDTree(data)
    >>> tree.query([0,0,0,0])
    
    

    输出如下所示:

    (4.6904157598234297, 4)
    
    

这意味着,在数据集中的所有点中,到原点欧几里得距离最近的是第五个点(索引 4),距离恰好是 4.6 个单位。

我们可以有一个包含多个点的输入;输出仍然是一个元组,其中第一个条目是一个数组,指示每个输入点的最小距离。第二个条目是另一个数组,指示最近邻的索引。

聚类

数据挖掘中使用的另一种技术是聚类。SciPy 有两个模块来处理这个领域的任何问题,每个模块都针对不同的聚类工具——scipy.cluster.vq用于 k-means 聚类和scipy.cluster.hierarchy用于层次聚类。

向量量化与 k-means

我们有两个例程使用 k-means 技术将数据划分为聚类——kmeanskmeans2。它们对应于两种不同的实现。前者具有非常简单的语法:

kmeans(obs, k_or_guess, iter=20, thresh=1e-05)

obs参数是一个包含我们希望聚类的数据的ndarray。如果数组的维度是m x n,则算法将此数据解释为n维欧几里得空间中的m个点。如果我们知道这些数据应该被划分为多少个聚类,我们通过k_or_guess选项输入这个数字。输出是一个包含两个元素的元组。第一个是一个k x n维的ndarray,表示一个点的集合——与指示的聚类数量一样多。这些位置中的每一个都表示找到的聚类的质心。元组的第二个条目是一个表示传递的点与先前生成的质心之间的畸变的浮点值。

如果我们希望为聚类的质心指定一个初始猜测,我们可以再次通过k_or_guess参数这样做,通过发送一个k x nndarray

我们传递给kmeans的数据需要通过whiten例程进行归一化。

第二种选项更加灵活,正如其语法所表明的:

kmeans2(data, k, iter=10, thresh=1e-05,
minit='random', missing='warn')

datak参数分别与obsk_or_guess相同。这个例程的不同之处在于可以选择不同的初始化算法,因此,如果我们知道我们数据的一些属性,我们可以通过将一个字符串传递给minit参数来加速过程并使用更少的资源。我们可以通过传递一个k x nndarray来这样做,例如'random'(使用高斯随机构建初始化质心),'points'(通过选择属于我们数据的点进行初始化),或'uniform'(如果我们更喜欢均匀分布而不是高斯分布)。

如果我们希望使用k参数自己提供初始化质心,我们必须通过将'matrix'传递给minit选项来向算法指示我们的选择。

在任何情况下,如果我们希望通过将每个点分配到它所属的聚类来对原始数据进行分类;我们使用vq例程(用于向量量化)。语法同样简单:

vq(obs, centroids)

输出是一个包含两个条目的元组。第一个条目是一个包含n个元素的二维ndarray,对于obs中的每个点,它表示该点所属的聚类。第二个条目是另一个同样大小的二维ndarray,但它包含表示每个点到其聚类质心的距离的浮点值。

让我们用一个经典的例子来说明,即鼠标数据集。我们将创建一个包含在三个圆盘内随机生成的点的大数据集,如下所示:

>>> import numpy
>>> from scipy.stats import norm
>>> from numpy import array,vstack
>>> data=norm.rvs(0,0.3,size=(10000,2))
>>> inside_ball=numpy.hypot(data[:,0],data[:,1])<1.0
>>> data=data[inside_ball]
>>> data = vstack((data, data+array([1,1]),data+array([-1,1])))

一旦创建,我们将请求数据被分成三个簇:

>>> from scipy.cluster.vq import *
>>> centroids, distortion = kmeans(data,3)
>>> cluster_assignment, distances = vq(data,centroids)

让我们展示结果:

>>> from matplotlib.pyplot import plot
>>> import matplotlib.pyplot as plt
>>> plt.plot(data[cluster_assignment==0,0], \
 data[cluster_assignment==0,1], 'ro')
>>> plt.plot(data[cluster_assignment==1,0], \
 data[cluster_assignment==1,1], 'b+')
>>> plt.plot(data[cluster_assignment==2,0], \
 data[cluster_assignment==2,1], 'k.')
>>> plt.show()

这给出了以下从左到右显示包含三个簇的鼠标数据集的图——红色(正方形)、蓝色(加号)和黑色(点):

矢量量化与 k 均值

层次聚类

有几种不同的算法可以进行层次聚类。SciPy 提供了以下方法的例程:

  • 单/最小/最近方法single

  • 完全/最大/最远方法complete

  • 平均/UPGMA 方法average

  • 加权/WPGMA 方法weighted

  • 质心/UPGMC 方法centroid

  • 中位数/WPGMC 方法median

  • 沃德链接方法ward

在任何上述情况下,语法都是相同的;唯一的输入是数据集,它可以是表示 n 维欧几里得空间中 m 个点的 m x n ndarray,或者是从之前数据使用 scipy.spatial 中的 pdist 例程获得的压缩距离矩阵。输出始终是一个表示聚类获得的对应链接矩阵的 ndarray

或者,我们也可以使用通用的 linkage 例程进行聚类。这个例程接受一个数据集/距离矩阵,以及一个表示要使用的方法的字符串。这些字符串与之前引入的名称相匹配。linkage 相比之前的例程的优势在于,我们还可以指定不同于通常欧几里得距离的不同度量。linkage 的完整语法如下:

linkage(data, method='single', metric='euclidean')

可以使用诸如观察之间的柯芬特距离(cophenet)、不一致性统计(inconsistent)、每个非单簇及其后代的最大不一致系数(maxdists)以及每个非单簇及其后代的最大统计量(maxRstat)等例程对结果链接矩阵执行不同的统计操作。

使用二叉树来表示关联矩阵是惯例,scipy.cluster.hierachy 子模块提供了大量不同的例程来操作和从这些树中提取信息。其中最有用的例程是这些树的可视化,通常称为树状图。SciPy 中对应的例程是 dendrogram,其语法如下:

dendrogram(Z, p=30, truncate_mode=None, color_threshold=None, 
get_leaves=True, orientation='top', labels=None, 
count_sort=False, distance_sort=False, 
show_leaf_counts=True, no_plot=False, no_labels=False, 
color_list=None, leaf_font_size=None, 
leaf_rotation=None, leaf_label_func=None, 
no_leaves=False, show_contracted=False,
link_color_func=None)

第一个明显的参数,Z,是一个链接矩阵。这是唯一的非可选变量。其他选项控制输出样式(颜色、标签、旋转等),由于它们在技术上是非数学性质,我们不会在本专著中详细探讨它们,除了通过以下简单应用于动物聚类的示例。

根据牙齿排列聚类哺乳动物

哺乳动物的牙齿被分为四组,如门齿、犬齿、前臼齿和臼齿。已经收集了多种哺乳动物的牙齿数据,可在www.uni-koeln.de/themen/statistik/data/cluster/dentitio.dat下载。

此文件展示了哺乳动物的名称,以及顶门齿、底门齿、顶犬齿、底犬齿、顶前臼齿、底前臼齿、顶臼齿和底臼齿的数量。

我们希望使用层次聚类对数据集进行聚类,以评估哪些物种在这些特征上更接近。

我们将首先准备数据集并将相关数据存储在 ndarrays 中。原始数据以文本文件的形式给出,其中每行代表不同的哺乳动物。以下是前四行:

OPOSSUM                    54113344
HAIRY TAIL MOLE            33114433
COMMON MOLE              32103333
STAR NOSE MOLE            33114433

每一行的前 27 个字符包含动物的名称。第 28 至 35 位的字符表示各自种类的牙齿数量。我们需要将此数据准备成 SciPy 可以处理的形式。我们将单独收集名称,因为我们将在树状图中使用它们作为标签。其余的数据将被强制转换为整数数组:

>>> import numpy
>>> file=open("dentitio.dat","r") # open the file
>>> lines=file.readlines() # read each line in memory
>>> file.close() # close the file
>>> mammals=[] # this stores the names
>>> dataset=numpy.zeros((len(lines),8)) # this stores the data
>>> for index,line in enumerate(lines):
 mammals.append( line[0:27].rstrip(" ").capitalize() )
 for tooth in range(8):
 dataset[index,tooth]=int(line[27+tooth])

我们将继续计算连接矩阵及其后继树状图,确保使用 Python 列表 mammals 作为标签:

>>> import matplotlib.pyplot as plt
>>> from scipy.cluster.hierarchy import linkage, dendrogram
>>> Z=linkage(dataset)
>>> dendrogram(Z, labels=mammals, orientation="right")
>>> matplotlib.pyplot.show()
>>> plt.show()

这给出了以下树状图,显示了根据牙齿聚类哺乳动物:

根据牙齿聚类哺乳动物

注意所有蝙蝠都被聚在一起。老鼠也聚在一起,但离蝙蝠很远。绵羊、山羊、羚羊、鹿和驼鹿也有类似的牙齿,它们在树的底部聚集,靠近负鼠和犰狳。注意所有猫科动物也聚在一起,在树的顶部。

数据分析专家可以从树状图中获得更多信息;他们能够解释分支的长度或组成中使用的不同颜色,并给我们更多关于簇之间差异的深入解释。

摘要

本章讨论了适用于数据挖掘的工具,并探讨了stats(用于统计)、spatial(用于数据结构)和cluster(用于聚类和向量量化)等模块。在下一章中,将研究 SciPy 模块scipy.spatial中包含的附加功能,以补充前几章中已经探讨的功能。像往常一样,每个介绍的功能将通过非平凡示例进行说明,这些示例可以通过修改与本章对应的 IPython Notebook 来丰富。

第七章。SciPy 计算几何

在本章中,我们将介绍 SciPy 的基础知识,以开发涉及此非常专业主题的程序:计算几何。我们将使用两个示例来说明 SciPy 函数在该领域的应用。为了能够从第一个示例中受益,你可能需要手头有一本 Computational Geometry: Algorithms and Applications Third Edition,作者 de Berg M.Cheong O.van Kreveld M.,和 Overmars M.,由 Springer Publishing 出版。第二个示例,其中使用 有限元法来解决涉及拉普拉斯方程数值解的两个维问题,可以在了解 Introduction to the Finite Element Method,作者 Ottosen N. S.Petersson H.,由 Prentice Hall 出版的该主题的情况下无困难地跟随。

让我们先介绍 scipy.spatial 模块中处理任何维度空间中点三角剖分及其相应凸包构建的例程。

程序很简单;给定一个包含在 n- 维空间中的 m 个点(我们将其表示为一个 m x n 的 NumPy 数组),我们创建一个 scipy.spatial 类的 Delaunay,它包含由这些点形成的三角剖分:

>>> import scipy.stats 
>>> import scipy.spatial 
>>> data = scipy.stats.randint.rvs(0.4,10,size=(10,2))
>>> triangulation = scipy.spatial.Delaunay(data)

任何 Delaunay 类都有基本的搜索属性,例如 points(用于获取三角剖分中的点集),vertices(提供构成三角剖分单纯形的顶点的索引),neighbors(用于每个单纯形的相邻单纯形的索引——约定“-1”表示边界上的单纯形没有相邻单纯形)。

更高级的属性,例如 convex_hull,指示构成给定点凸包的顶点索引。如果我们想搜索共享给定顶点的单纯形,我们可以使用 vertex_to_simplex 方法。如果我们想定位包含空间中任何给定点的单纯形,我们可以使用 find_simplex 方法。

在这个阶段,我们想指出三角剖分和 Voronoi 图之间的密切关系,并提出一个简单的编码练习。让我们首先选择一组随机点,并获取相应的三角剖分:

>>> from numpy.random import RandomState
>>> rv = RandomState(123456789)
>>> locations = rv.randint(0, 511, size=(2,8))
>>> triangulation=scipy.spatial.Delaunay(locations.T)

我们可以使用 matplotlib.pyplottriplot 程序来获取这个三角剖分的图形表示。我们首先需要获取计算出的单纯形的集合。Delaunay 提供了这个集合,但它通过顶点的索引而不是坐标来提供。因此,在将单纯形的集合输入到 triplot 程序之前,我们需要将这些索引映射到实际点上:

>>> import matplotlib.pyplot as plt 
>>> assign_vertex = lambda index: triangulation.points[index]
>>> triangle_set = map(assign_vertex, triangulation.vertices)

我们现在将以与之前类似的方式(这次使用 scipy.spatial.Voronoi 模块)获取 Voronoi 图的边图,并将其与三角剖分一起绘制。这是通过以下代码行完成的:

>>> voronoiSet=scipy.spatial.Voronoi(locations.T)
>>> scipy.spatial.voronoi_plot_2d(voronoiSet)
>>> fig = plt.figure()
>>> thefig = plt.subplot(1,1,1)
>>> scipy.spatial.voronoi_plot_2d(voronoiSet, ax=thefig)
>>> plt.triplot(locations[1], locations[0], triangles=triangle_set, color='r')

让我们看看下面的 xlim() 命令:

>>> plt.xlim((0,550))

输出如下所示:

 (0, 550)

现在,让我们看一下下面的 ylim() 命令:

>>> plt.ylim((0,550))

输出如下所示:

 (0, 550)

现在,我们在下面的 plt.show() 命令中绘制 Voronoi 图的边缘图以及三角剖分:

>>> plt.show()

输出如下所示:

SciPy for Computational Geometry

注意到三角剖分和相应的 Voronoi 图是彼此的补集;三角剖分中的每条边(红色)与 Voronoi 图中的边(白色)垂直。我们应该如何利用这个观察结果来为点云编码实际的 Voronoi 图?实际的 Voronoi 图是由组成它的顶点和边构成的集合。

注意

可以在stackoverflow.com/questions/10650645/python-calculate-voronoi-tesselation-from-scipys-delaunay-triangulation-in-3d找到寻找 Voronoi 图的有意思的方法。

让我们以两个科学计算的应用来结束这一章,这两个应用广泛使用了这些技术,并结合了其他 SciPy 模块的例程。

氧化物的结构模型

在本例中,我们将介绍从青铜型氧化铌分子的HAADF-STEM显微照片中提取结构模型(关于此主题的更多背景信息可以在书籍《电子显微镜中的纳米尺度成像建模》的第五章通过应用于高角环形暗场扫描透射电子显微镜(HAADF-STEM)的非局部均值方法形成高质量图像中找到,作者:Vogt T.Dahmen W.,和Binev P.Springer Publishing)。

下面的图显示了青铜型氧化铌的 HAADF-STEM 显微照片(取自www.microscopy.ethz.ch/BFDF-STEM.htm):

氧化物的结构模型

感谢:ETH Zurich

为了教学目的,我们采取了以下方法来解决此问题:

  • 通过阈值和形态学操作对原子进行分割。

  • 通过连接组件标记来提取每个单独的原子,以便进行后续检查。

  • 计算每个被识别为原子的标签的质量中心。这向我们展示了一个平面上的点阵,它展示了氧化物结构模型的第一手资料。

  • 计算前一个点阵的 Voronoi 图。将信息与上一步的输出相结合将使我们得到一个相当好的(实际结构模型的近似)样品结构模型。

让我们继续这个方向。

一旦检索并保存在当前工作目录中,我们的 HAADF-STEM 图像将在 Python 中读取,并默认存储为float32float64精度的大的矩阵(取决于您的计算机架构)。对于这个项目,只需要从scipy.ndimage模块检索一些工具,以及从matplotlib库中的一些过程。然后,前导代码如下:

>>> import numpy
>>> import scipy
>>> from scipy.ndimage import *
>>> from scipy.misc import imfilter
>>> import matplotlib.pyplot as plt
>>> import matplotlib.cm as cm

该图像使用imread(filename)命令加载。这将以dtype = float32将图像存储为numpy.array。请注意,图像被缩放,使得最大值和最小值分别为1.00.0。有关图像的其他有趣信息可以通过以下方式检索:

>>> img=imread('./NbW-STEM.png')
>>> minVal = numpy.min(img) 
>>> maxVal = numpy.max(img) 
>>> img = (1.0/(maxVal-minVal))*(img - minVal) 
>>> plt.imshow(img, cmap = cm.Greys_r)
>>> plt.show()
>>> print "Image dtype: %s"%(img.dtype)
>>> print "Image size: %6d"%(img.size)
>>> print "Image shape: %3dx%3d"%(img.shape[0],img.shape[1])
>>> print "Max value %1.2f at pixel %6d"%(img.max(),img.argmax())
>>> print "Min value %1.2f at pixel %6d"%(img.min(),img.argmin())
>>> print "Variance: %1.5f\nStandard deviation: \ 
 %1.5f"%(img.var(),img.std())

这提供了以下输出:

Image dtype: float64
Image size:  87025
Image shape: 295x295
Max value 1.00 at pixel  75440
Min value 0.00 at pixel   5703
Variance: 0.02580
Standard deviation: 0.16062

我们通过在包含数据的数组中施加不等式来进行阈值处理。输出是一个布尔数组,其中True(白色)表示不等式已被满足,而False(黑色)则表示未满足。我们可以在这一点上执行多个阈值处理操作,并可视化它们以获得用于分割的最佳阈值。以下图像显示了几个示例(对不同氧化物图像应用的不同阈值):

氧化物的结构模型

以下代码行生成了该氧化物图像:

>>> plt.subplot(1, 2, 1)
>>> plt.imshow(img > 0.2, cmap = cm.Greys_r)
>>> plt.xlabel('img > 0.2')
>>> plt.subplot(1, 2, 2) 
>>> plt.imshow(img > 0.7, cmap = cm.Greys_r)
>>> plt.xlabel('img > 0.7')
>>> plt.show()

通过对几个不同的阈值进行视觉检查,我们选择0.62作为给我们提供良好映射的阈值,该映射显示了我们需要用于分割的内容。尽管如此,我们需要去除异常值:可能满足给定阈值但足够小以至于不被视为实际原子的微小颗粒。因此,在下一步中,我们执行开运算来去除这些小颗粒。我们决定,任何小于 2 x 2 大小的正方形都将从阈值处理的输出中消除:

>>> BWatoms = (img> 0.62)
>>> BWatoms = binary_opening(BWatoms,structure=numpy.ones((2,2)))

我们已经准备好进行分割,这将使用来自scipy.ndimage模块的label例程来完成。它为每个分割的原子收集一个切片,并提供了计算出的切片数量。我们需要指出连接类型。例如,在下面的玩具示例中,我们是否希望将这种情况视为两个原子还是一个原子?

氧化物的结构模型

这取决于情况;我们宁愿现在将其视为两个不同的连通组件,但对于某些其他应用,我们可能认为它们是一个。我们将连接信息指示给label例程的方式是通过一个定义特征连接的结构元素。例如,如果我们的两个像素之间的连接标准是它们的边缘是相邻的,那么结构元素看起来就像在下一张图中左侧显示的图像。如果我们的两个像素之间的连接标准是它们也可以共享一个角落,那么结构元素看起来就像在右侧显示的图像。

对于每个像素,我们施加选定的结构元素并计算交点;如果没有交点,则这两个像素不相连。否则,它们属于同一个连通分量。

氧化物的结构模型

我们需要确保那些对角线距离太近的原子被计为两个,而不是一个,所以我们选择了左侧的结构元素。然后脚本如下所示:

>>> structuring_element = [[0,1,0],[1,1,1],[0,1,0]]
>>> segmentation,segments = label(BWatoms,structuring_element)

segmentation 对象包含一个切片列表,每个切片都有一个布尔矩阵,包含每个找到的氧化物原子。对于每个切片,我们可以获得大量有用的信息。例如,可以使用以下命令检索每个原子的质心坐标(centers_of_mass):

>>> coords = center_of_mass(img, segmentation, range(1,segments+1))
>>> xcoords = numpy.array([x[1] for x in coords])
>>> ycoords = numpy.array([x[0] for x in coords])

注意,由于矩阵在内存中的存储方式,像素位置的 xy 坐标发生了转置。我们需要考虑这一点。

注意到计算的点阵与原始图像(下一张图中左侧的图像)的重叠。我们可以使用以下命令获得它:

>>> plt.imshow(img, cmap = cm.Greys_r) 
>>> plt.axis('off') 
>>> plt.plot(xcoords,ycoords,'b.') 
>>> plt.show() 

我们已经成功找到了大多数原子的质心,尽管仍有大约十几个区域我们对结果不太满意。现在是时候通过改变一些变量的值来微调,比如调整阈值、结构元素、不同的形态学操作等等。我们甚至可以添加关于这些变量的广泛信息,并过滤掉异常值。以下是一个优化分割的示例(请看右侧图像):

氧化物的结构模型

为了本演示的目的,我们很高兴保持简单,并继续使用我们已经计算出的坐标集。现在我们将提供一个关于氧化物晶格的近似,它是通过计算晶格的沃罗诺伊图的边缘图得到的:

>>> L1,L2 = distance_transform_edt(segmentation==0, return_distances=False, return_indices=True)
>>> Voronoi = segmentation[L1,L2]
>>> Voronoi_edges= imfilter(Voronoi,'find_edges')
>>> Voronoi_edges=(Voronoi_edges>0)

让我们将 Voronoi_edges 的结果叠加到找到的原子位置上:

>>> plt.imshow(Voronoi_edges); plt.axis('off'); plt.gray()
>>> plt.plot(xcoords,ycoords,'r.',markersize=2.0)
>>> plt.show()

这给出了以下输出,它代表了我们所寻找的结构模型(回想一下,我们是从一个想要找到分子结构模型的图像开始的):

氧化物的结构模型

拉普拉斯方程的有限元求解器

当数据的大小如此之大以至于其结果禁止使用有限差分法处理时,我们使用有限元。为了说明这种情况,我们想探索在特定边界条件下的拉普拉斯方程的数值解。

我们将首先定义计算域并使用三角形作为局部有限元来划分这个域。这将是我们使用有限元方法解决这个问题的起点,因为我们将在计算域上放置一个分段连续函数,其部分是线性的,并且在每个三角形上都有支撑。

我们首先调用必要的模块来构建网格(其他模块将在需要时调用):

>>> import numpy
>>> from numpy import linspace
>>> import scipy
>>> import matplotlib.pyplot as plt
>>> from scipy.spatial import Delaunay

首先,我们定义区域:

>>> xmin = 0 ; xmax = 1 ; nXpoints = 10
>>> ymin = 0 ; ymax = 1 ; nYpoints = 10
>>> horizontal = linspace(xmin,xmax,nXpoints)
>>> vertical = linspace(ymin,ymax,nYpoints)
>>> y, x = numpy.meshgrid(horizontal, vertical)
>>> vertices = numpy.array([x.flatten(),y.flatten()])

现在,我们可以创建三角剖分:

>>> triangulation = Delaunay(vertices.T)
>>> index2point = lambda index: triangulation.points[index]
>>> all_centers = index2point(triangulation.vertices).mean(axis=1)
>>> trngl_set=triangulation.vertices

然后,我们有以下三角剖分:

>>> plt.triplot(vertices[0],vertices[1],triangles=trngl_set)
>>> plt.show()

这产生了以下图:

拉普拉斯方程的有限元求解器

在这种情况下,我们选择的问题是在物理学和工程数学中的标准问题,它包括在单位正方形区域内求解二维拉普拉斯方程,三边上的Dirichlet边界条件为零,在第四边上的条件是常数。从物理学的角度来看,这个问题可以代表二维板上的温度扩散。从数学的角度来看,问题表述如下:

拉普拉斯方程的有限元求解器

这种形式的解可以用傅里叶级数表示如下:

拉普拉斯方程的有限元求解器

这很重要,因为在你尝试使用你的数值方案来解决复杂计算域中的更复杂问题之前,你可以检查所获得的数值解的正确性。然而,应该提到的是,Python 中存在其他实现有限元方法来解决偏微分方程的替代方案。在这方面,读者可以参考Fenics项目(fenicsproject.org/book/)和SfePy项目(sfepy.org/doc-devel/index.html)。

我们按照常规方式编写代码求解。我们首先计算刚度矩阵A(由于显而易见的原因,它是sparse的)。然后,定义包含全局边界条件的向量R的构造(我们构建网格的方式使得定义这个向量变得简单)。有了它们,系统的解来自于通过求解形式为AX=R的矩阵方程获得的解X,使用与边界上的节点不同的节点对应的矩阵AR的子集。这对 SciPy 来说应该不成问题。让我们从刚度矩阵开始:

>>> from numpy import  cross 
>>> from scipy.sparse import dok_matrix 
>>> points=triangulation.points.shape[0]
>>> stiff_matrix=dok_matrix((points,points))
>>> for triangle in triangulation.vertices:
 helper_matrix=dok_matrix((points,points))
 pt1,pt2,pt3=index2point(triangle)
 area=abs(0.5*cross(pt2-pt1,pt3-pt1))
 coeffs=0.5*numpy.vstack((pt2-pt3,pt3-pt1,pt1-pt2))/area
 #helper_matrix[triangle,triangle] = \ 
 array(mat(coeffs)*mat(coeffs).T)
 u=None 
 u=numpy.array(numpy.mat(coeffs)*numpy.mat(coeffs).T) 
 for i in range(len(triangle)):
 for j in range(len(triangle)):
 helper_matrix[triangle[i],triangle[j]] = u[i,j] 
 stiff_matrix=stiff_matrix+helper_matrix

注意,这是更新stiff_matrix矩阵的繁琐方式。这是由于矩阵是sparse的,并且当前的选择在索引方面表现不佳。

为了计算全局边界向量,我们首先需要收集边界上的所有边,然后将函数值为 1 分配给x=1的节点,将函数值为 0 分配给其他节点。由于我们设置网格的方式,这很容易,因为函数值为 1 的节点总是全局边界向量的最后几个条目。这是通过以下代码行实现的:

>>> allNodes = numpy.unique(trngl_set) 
>>> boundaryNodes = numpy.unique(triangulation.convex_hull) 
>>> NonBoundaryNodes = numpy.array([]) 
>>> for x in allNodes: 
 if x not in boundaryNodes: 
 NonBoundaryNodes = numpy.append(NonBoundaryNodes,x) 
 NonBoundaryNodes = NonBoundaryNodes.astype(int) 
 nbnodes = len(boundaryNodes) # number of boundary nodes 
 FbVals=numpy.zeros([nbnodes,1]) # Values on the boundary 
 FbVals[(nbnodes-nXpoints+1):-1]=numpy.ones([nXpoints-2, 1])

我们已经准备好使用在前一步骤中获得的数据来寻找问题的数值解:

>>> totalNodes = len(allNodes) 
>>> stiff_matrixDense = stiff_matrix.todense() 
>>> stiffNonb = \ 
 stiff_matrixDense[numpy.ix_(NonBoundaryNodes,NonBoundaryNodes)] 
>>> stiffAtb = \ 
 stiff_matrixDense[numpy.ix_(NonBoundaryNodes,boundaryNodes)] 
>>> U=numpy.zeros([totalNodes, 1]) 
>>> U[NonBoundaryNodes] = numpy.linalg.solve( - stiffNonb , \
 stiffAtb * FbVals ) 
>>> U[boundaryNodes] = FbVals 

这产生了以下图像,展示了方形内部温度的扩散:

拉普拉斯方程的有限元求解器

该图是通过以下方式获得的:

>>> X = vertices[0] 
>>> Y = vertices[1] 
>>> Z = U.T.flatten() 
>>> from mpl_toolkits.mplot3d import axes3d
>>> fig = plt.figure() 
>>> ax = fig.add_subplot(111, projection='3d') 
>>> surf = ax.plot_trisurf(X, Y, Z, cmap=cm.jet, linewidth=0) 
>>> fig.colorbar(surf) 
>>> fig.tight_layout() 
>>> ax.set_xlabel('X',fontsize=16)
>>> ax.set_ylabel('Y',fontsize=16)
>>> ax.set_zlabel(r"$\phi$",fontsize=36)
>>> plt.show() 

数值分析中的一个重要点是评估任何问题获得的数值解的质量。在这种情况下,我们选择了一个具有解析解的问题(参见前面的代码),因此可以检查(而不是证明)用于解决我们问题的数值算法的有效性。在这种情况下,解析解可以以以下方式编码:

>>> from numpy import pi, sinh, sin, cos, sum
>>> def f(x,y): 
 return sum( 2*(1.0/(n*pi) - \
 cos(n*pi)/(n*pi))*(sinh(n*pi*x)/ \
 sinh(n*pi))*sin(n*pi*y) 
 for n in range(1,200)) 
>>> Ze = f(X,Y) 
>>> ZdiffZe = Ze - Z 
>>> plt.plot(ZdiffZe) 
>>> plt.show() 

这产生了以下图表,显示了精确解(评估到 200 项)与问题的数值解之间的差异(通过相应的 IPython 笔记本,你可以对数值解进行进一步分析,以更加确信获得的结果是正确的):

拉普拉斯方程的有限元求解器

摘要

在本书的七个章节中,我们以结构化的方式详细介绍了 SciPy 库中包含的所有不同模块,这些模块是从数学不同分支的逻辑划分中得出的。

我们还见证了该系统以最少的编码和最优的资源使用,在不同科学领域的科学研究问题中实现最先进应用的能力。

在第八章中,我们将介绍 SciPy 的主要优势之一:与其他语言交互的能力。

第八章。与其他语言的交互

我们经常需要将不同语言的代码整合到我们的工作流程中;主要是 C/C++ 或 Fortran,以及来自 R、MATLAB 或 Octave 的代码。Python 优秀地允许所有这些其他来源的代码在内部运行;必须注意将不同的数值类型转换为 Python 可以理解的形式,但这几乎是我们遇到唯一的难题。

如果您正在使用 SciPy,那是因为您的 Python 生态系统中有可用的 C 和 Fortran 程序编译器。否则,SciPy 就无法安装到您的系统上。鉴于其流行程度,您的计算机环境很可能有 MATLAB/Octave。因此,这导致了本章后面列出主题的选择。我们将留给感兴趣的读者去了解如何与 R 和许多其他软件进行接口,这些软件可用于数值计算。使用 R 的两种替代方案是包 PypeR (bioinfo.ihb.ac.cn/softwares/PypeR/) 和 rpy2 (rpy.sourceforge.net/)。其他替代方案可以在 stackoverflow.com/questions/11716923/python-interface-for-r-programming-language 找到。

在本章中,我们将涵盖以下内容:

  • 简要讨论如何使用 Python 运行 Fortran、C/C++ 和 MATLAB/Octave 的代码

  • 我们将首先了解实用程序 f2py 的基本功能,以通过 SciPy 处理在 Python 中包含 Fortran 代码。

  • 使用 scipy.weave 模块提供的工具在 Python 代码中包含 C/C++ 代码的基本用法

通过简单的示例来展示这些例程,您可以通过修改与本章对应的 IPython Notebook 来丰富这些示例。

与 Fortran 的交互

SciPy 提供了一种简单的方法来包含 Fortran 代码——f2py。这是一个与 NumPy 库一起提供的实用程序,当 SciPy 的 distutils 可用时才会生效。当我们安装 SciPy 时,这总是成立的。

f2py 实用程序应该在 Python 之外运行,它用于从任何 Fortran 文件创建一个可以在我们的会话中轻松调用的 Python 模块。在任何 *nix 系统中,我们从终端调用它。在 Windows 上,我们建议您在原生终端中运行它,或者更好的是,通过 cygwin 会话运行。

在使用 f2py 编译之前,任何 Fortran 代码都需要进行以下三个基本更改:

  • 移除所有分配

  • 将整个程序转换为一个子程序

  • 如果需要将任何特殊内容传递给 f2py,我们必须使用注释字符串 "!f2py""cf2py" 来添加它。

让我们用一个简单的例子来说明这个过程。以下存储在 primefactors.f90 文件中的简单子程序,对任何给定的整数进行质因数分解:

SUBROUTINE PRIMEFACTORS(num, factors, f)
  IMPLICIT NONE
  INTEGER, INTENT(IN) :: num  !input number
  INTEGER,INTENT(OUT), DIMENSION((num/2))::factors 
  INTEGER, INTENT(INOUT) :: f
  INTEGER :: i, n
  i = 2  
  f = 1  
  n = num
  DO
    IF (MOD(n,i) == 0) THEN 
      factors(f) = i
      f = f+1
      n = n/i
    ELSE
      i = i+1
    END IF
    IF (n == 1) THEN    
      f = f-1    
      EXIT
    END IF
  END DO

由于代码中没有进行任何分配,并且我们直接收到一个子例程,我们可以跳到第三步,但暂时我们不会修改 f2py 命令,我们满足于尝试从它创建一个 Python 模块。将此 primefactors 子例程包装起来的最快方式是发出以下命令(在由 % 指示的 shell 或终端提示符处):

% f2py –c primefactors.f90 –m primefactors

如果一切正常,将创建一个名为 primefactors.so 的扩展模块。然后我们可以从 primefactors 模块中访问 primefactors 例程:

>>> import primefactors
>>> primefactors.primefactors(6,1)

输出如下所示:

array([2, 3, 0], dtype=int32)

与 C/C++ 的交互

技术上,f2py 也可以为我们包装 C 代码,但还有更有效的方法来完成这项任务。例如,如果我们需要接口一个非常大的 C 函数库,完成此任务的首选方法是 简化包装和接口生成器SWIG)(www.swig.org/)。要包装 C++ 代码,根据所需功能和与 Python 交互的方法,我们有几种方法,如 SWIG 或再次使用 f2py,但还有 PyCXXBoost.PythonCython 或 SciPy 模块:weave。当 C 编译器不可用(因此无法以通常的方式链接大量库)时,我们使用 ctypes。每当我们将使用 NumPy/SciPy 代码,并且想要快速解决我们的包装/绑定问题时,与 C/C++ 交互的两种最常见方式通常是 Python/C API 和 weave 包。

这里简要列举的所有方法都需要一本专著来详细描述,具体来说,就是根据系统和需求绑定包装的繁琐之处的方法,以及它们实现时的注意事项。在本章中,我们想更详细地介绍的是 weave 包,更具体地说,是通过 inline 例程。此命令接收一个包含一系列命令的字符串(原始或非原始),并通过调用您的 C/C++ 编译器在 Python 中运行它。语法如下:

inline(code, arg_names, local_dict=None, global_dict=None,
           force = 0,
           compiler='',
           verbose = 0,
support_code = None,
           customize=None,
type_factories = None,
auto_downcast=1,
           **kw)

让我们来看看不同的参数:

  • code 参数是包含要运行的代码的字符串。请注意,此代码不得指定任何类型的 return 语句。相反,它应分配一些可以返回给 Python 的结果。

  • arg_names 参数是一个包含要发送到 C/C++ 代码的 Python 变量名的字符串列表。

  • local_dict 参数是可选的,它必须是一个包含用作 C/C++ 代码局部作用域的值的 Python 字典。

  • global_dict 参数也是可选的,它必须是一个包含应作为 C/C++ 代码全局作用域的值的另一个 Python 字典。

  • force 参数仅用于调试目的。它也是可选的,只能取两个值——0(默认值)或 1。如果其值设置为 1,则每次调用 inline 时都会编译 C/C++ 代码。

  • 我们可以使用compiler选项指定接管 C/C++代码的编译器。它必须是一个包含 C/C++编译器名称的字符串。

让我们以inline例程为例,其中我们使用以下方法来使用cout进行文本显示:

>>> import scipy.weave
>>> name = 'Francisco'
>>> pin = 1234
>>> code = 'std::cout << name << "---PIN: " '
>>> code+= '<<std::hex << pin <<std::endl;'
>>> arg_names = ['name','pin']
>>> scipy.weave.inline(code, arg_names)

输出如下所示:

Francisco---PIN: 4d2

这是一个非常简单的例子,其中不需要外部头文件声明。如果我们希望这样做,那些将放入support_code选项中。例如,如果我们希望在 C/C++代码中包含 R 的数学函数并通过inline传递,我们需要执行以下步骤:

  1. 将 C 函数配置为共享库。在终端会话中,在包含 R 发布的文件夹中,输入以下命令:

    % ./configure --enable-R-static-lib --enable-static --with-readline=no
    
    
  2. 切换到src/nmath中的standalone文件夹,完成库的安装。最后,我们应该有一个名为libRmath.so的文件,需要从libpath字符串指向我们的 Python 会话:

    % cd src/nmath/standalone
    % make
    
    
  3. 在我们的 Python 会话中,我们使用适当的选项准备inline调用。例如,如果我们想调用 R 例程pbinom,我们按以下步骤进行:

    >>> import scipy.weave 
    >>> support_code= 'extern "C" double pbinom(double x,\ 
     double n, double p, int lower_tail, int log_p);' 
    >>> libpath='/opt/Rlib' #IS THE LOCATION OF LIBRARY libRmath.so
    >>> library_dirs=[libpath] 
    >>> libraries=['Rmath'] 
    >>> runtime_library_dirs=[libpath] 
    >>> code='return_val=pbinom(100,20000,100./20000.,0,1);' 
    >>> res=scipy.weave.inline(code, support_code=support_code, \ 
     library_dirs=library_dirs, libraries=libraries, \ 
     runtime_library_dirs=runtime_library_dirs) 
    >>> print(res) 
    
    

    输出如下所示:

    -0.747734910363 
    
    

    注意

    注意函数声明是在support_code中传递的,而不是在代码中。还要注意,每次我们不使用 C++时,此选项都需要以extern "C"开头。

  4. 如果需要传递额外的头文件,我们使用header选项,而不是support_codecode

    >>> headers = ['<math.h>']
    
    

我们有一些建议。在将不同变量类型从其原始 C/C++格式转换为 Python 理解的形式时,必须小心谨慎。在某些情况下,这需要修改原始的 C/C++代码。但默认情况下,我们不必担心以下 C/C++类型,因为 SciPy 会自动将它们转换为以下表所示的 Python 格式:

Python int float complex string list dict tuple
C/C++ int double std::complex py::string py::list py:dict py::tuple

文件类型FILE*被发送到 Python 文件。Python 的可调用对象和实例都来自py::object。NumPy ndarrays 是从PyArrayObject*构建的。对于任何其他要使用的 Python 类型,相应的 C/C++类型必须仔细转换为前面的组合。

这样就足够了。要超越内联函数的简单使用,我们通常创建扩展模块,并将其中的函数编目以供将来使用。

与 MATLAB/Octave 的交互

由于这两个数值计算环境都提供了第四代编程语言,我们不建议直接包含这两个环境中的任何代码。在速度、资源使用或编码能力方面都没有任何优势。在极端且罕见的情况下,如果 SciPy 中没有特定的例程,将例程带到我们的会话中的首选方式是从 MATLAB/Octave 代码生成 C 代码,然后使用本章中与 C/C++交互部分中建议的任何方法进行封装。

当我们接收由 MATLAB 或 Octave 创建的数据时,情况会有所不同。SciPy 有一个专门的模块来处理这种情况——scipy.io

让我们通过示例来展示。我们从 Octave 开始,在平面上生成一个由 10 个随机点组成的Delaunay 三角剖分

我们将这些点的坐标以及三角剖分中三角形的指针保存到一个名为 data 的 MATLAB 风格文件(版本 7)中:

octave:1> x=rand(1,10);
octave:2> y=rand(size(x));
octave:3> T=delaunay(x,y);
octave:4> save -v7 data x y T

我们在这里完成了。然后我们转到我们的 Python 会话,在那里我们恢复文件数据:

>>> from scipy.io import loadmat
>>> datadict = loadmat("data")

datadict变量包含一个 Python 字典,其中变量的名称作为keys,加载的矩阵作为它们对应的值:

>>> datadict.keys()

输出如下所示:

['__header__', '__globals__', 'T', 'y', 'x', '__version__']

让我们发出datadict命令:

>>> datadict['x']

输出如下所示:

array([[0.81222999,0.51836246,0.60425982,0.23660352,0.01305779,
 0.0875166,0.77873049,0.70505801,0.51406693,0.65760987]])

让我们看看下面的datadict命令:

>>> datadict['__header__']

输出如下所示:

'MATLAB 5.0 MAT-file, written by Octave 3.2.4, 2012-11-27
 15:45:20 UTC'

我们可以将会话中的数据保存为 MATLAB 和 Octave 可以理解的格式。我们使用来自同一模块的savemat命令来完成此操作。其语法如下:

savemat(file_name, mdict, appendmat=True, format='5', 
long_field_names=False, do_compression=False,
oned_as=None)

file_name参数包含将要写入数据的 MATLAB 类型文件的名称。Python 字典mdict包含变量名称(作为键)及其对应的数组值。

如果我们希望在文件末尾附加.mat扩展名,我们可以在file_name变量中这样做,或者通过将appendmat设置为True。如果我们需要为文件提供长名称(并非所有 MATLAB 版本都接受),我们需要通过将long_field_names选项设置为True来表示这一点。

我们可以使用format选项来指定 MATLAB 的版本。对于 5.0 及以后的版本,我们将其设置为字符串'5',对于 4.0 版本,则设置为字符串'4'

我们可以压缩我们发送的矩阵,并且通过将do_compression选项设置为True来表示这一点。

最后一个选项非常有趣。它允许我们向 MATLAB/Octave 指示我们的数组是按列读取还是按行读取。将oned_as参数设置为字符串'column'将我们的数据发送到一列向量集合中。如果我们将其设置为字符串'row',它将数据作为行向量集合发送。如果设置为None,则尊重数据写入的格式。

摘要

本章介绍了 SciPy 的一个主要优势——能够与其他语言如 C/C++、Fortran、R 和 MATLAB/Octave 交互。要深入了解 Python 与其他语言的接口,您可能需要阅读更多专门的文献,如 《Cython 编程学习》,作者 Philip Herron,出版社 Packt Publishing,或者深入了解 F2PY 的资料,可在 docs.scipy.org/doc/numpy/f2py/www.f2py.com/home/references 找到。更多帮助信息可在 wiki.python.org/moin/IntegratingPythonWithOtherLanguages 找到。

如果您已经阅读到这一章,并且是从第一章开始阅读的,那么您应该知道在本章关于 SciPy 的简介中省略了许多主题。本书已经为您提供了足够的背景知识,以进一步强化您使用 SciPy 的技能和能力。要继续学习,请参考 SciPy 参考指南 (docs.scipy.org/doc/scipy/reference/) 和其他可用的文档指南 (docs.scipy.org/doc/)。

此外,我们建议您定期阅读并订阅 SciPy 邮件列表 (mail.scipy.org/mailman/listinfo/scipy-user),在那里您可以与世界各地的 SciPy 用户互动,不仅可以通过提问/回答有关 SciPy 的问题,还可以了解 SciPy 的当前趋势,甚至找到与之相关的职位。

您可以浏览该列表历史存档的帖子集合,mail.scipy.org/pipermail/scipy-user/。此外,您应该知道每年都会举办 SciPy 会议 (conference.scipy.org/),正如他们所说,这允许来自学术、商业和政府机构的参与者展示他们最新的科学 Python 项目,从熟练的用户和开发者那里学习,并在代码开发上进行合作。

第九章:索引

A

  • Airy 函数

    • 关于 / Airy 和 Bairy 函数
  • Anaconda

    • URL / 安装 SciPy
  • 数组转换 / 数组转换

  • 数组操作

    • 数组例程,用于 / 数组操作例程
  • 数组对象

    • 关于 / 数组对象

    • 数组转换 / 数组转换

    • 形状,选择 / 形状选择/操作

    • 形状,操作 / 形状选择/操作

    • 对象计算 / 对象计算

  • 数组例程

    • 关于 / 数组例程

    • 用于创建数组 / 创建数组例程

    • 用于组合多个数组 / 组合两个或更多数组的例程

    • 用于数组操作 / 数组操作例程

    • 用于从数组中提取信息 / 从数组中提取信息例程

  • 数组

    • 索引 / 数组的索引和切片

    • 切片 / 数组的索引和切片

    • 创建,使用的数组例程 / 创建数组例程

B

  • Bairy 函数

    • 关于 / Airy 和 Bairy 函数
  • 贝塞尔函数

    • 关于 / 贝塞尔和 Struve 函数
  • beta 积分 / 伽马和 beta 积分

  • Biggles

    • URL / 什么是 SciPy?
  • Bray-Curtis / 距离

  • Butterworth 滤波器 / 滤波器设计

C

  • C/C++

    • 与之交互 / 与 C/C++ 交互

    • 格式 / 与 C/C++ 交互

  • C/C++,参数

    • 代码参数 / 与 C/C++ 交互

    • arg_names 参数 / 与 C/C++ 交互

    • local_dict 参数 / 与 C/C++ 交互

    • global_dict 参数 / 与 C/C++ 交互

    • 力参数 / 与 C/C++ 交互

  • Canberra / 距离

  • Chaco

    • URL / 什么是 SciPy?
  • Chebyshev / 距离

  • 卡方检验

    • 关于 / 区间估计、相关度量及统计检验
  • 聚类

    • 关于 / 聚类

    • 向量量化 / 向量量化与 k-means

    • k-means / 向量量化与 k-means

    • 层次聚类 / 层次聚类

    • 哺乳动物,通过牙齿分类 / 通过牙齿分类聚类哺乳动物

  • 方便函数

    • 和测试函数 / 方便和测试函数

D

  • 数据类型

    • 使用 / 使用数据类型
  • 德劳内三角剖分

    • 关于 / 与 MATLAB/Octave 交互
  • 描述性统计

    • 关于 / 描述性统计
  • DFT

    • 关于 / 离散傅里叶变换
  • 距离,数据挖掘

    • 关于 / 距离
  • 分布拟合

    • 关于 / 分布拟合
  • 分布

    • 关于 / 分布
  • 文档

    • 寻找 / 如何查找文档

E

  • 特征值问题 / 特征值问题和矩阵分解

  • 椭圆积分 / 椭圆积分

  • 空命令 / 创建数组的例程

  • Enthought

    • URL / 安装 SciPy
  • 指数积分 / 指数/对数积分

F

  • 过滤器

    • 关于 / 过滤器

    • LTI 系统理论 / LTI 系统理论

    • 设计 / 滤波器设计, 窗口函数

    • 图像插值 / 图像插值

    • 形态学 / 形态学

  • 有限元求解器

    • 对于拉普拉斯方程 / 拉普拉斯方程的有限元求解器
  • 有限脉冲响应 (FIR) / 滤波器设计

  • Fortran

    • 与 Fortran 交互 / 与 Fortran 交互
  • 函数

    • 评估 / 特殊函数的评估

    • 伽马函数 / 伽马函数

    • 瑞曼ζ函数 / 瑞曼ζ函数

    • 贝塞尔函数 / 艾里和贝塞尔函数

    • Airy 函数 / Airy 和 Bairy 函数

    • 贝塞尔函数 / 贝塞尔和斯特夫函数

    • 斯特夫函数 / 贝塞尔和斯特夫函数

    • 特殊函数 / 其他特殊函数

    • 椭圆函数 / 其他特殊函数

G

  • gamma 函数

    • 关于 / 伽马函数
  • gamma 积分 / 伽马和贝塔积分

  • GNU Octave 系统

    • 关于 / 什么是 SciPy?
  • Gohlke

    • URL / 安装 SciPy

H

  • HAADF-STEM

    • 关于 / 氧化物的结构模型

    • URL / 氧化物的结构模型

  • 汉明 / 距离

  • 层次聚类 / 层次聚类

  • HippoDraw

    • URL / 什么是 SciPy?
  • Horner 算法

    • URL / 特殊函数的评价
  • 双曲三角函数积分 / 三角函数和双曲三角函数积分

I

  • 标识命令 / 创建数组的例程

  • 图像压缩

    • 通过奇异值分解 / 通过奇异值分解进行图像压缩
  • 图像插值 / 图像插值

  • 无限脉冲响应(IIR) / 滤波器设计

  • 积分

    • 关于 / 积分

    • 指数积分 / 指数/对数积分

    • 对数积分 / 指数/对数积分

    • 三角函数积分 / 三角函数和双曲三角函数积分

    • 双曲三角函数积分 / 三角函数和双曲三角函数积分

    • 椭圆积分 / 椭圆积分

    • gamma 积分 / 伽马和贝塔积分

    • beta 积分 / 伽马和贝塔积分

    • 数值积分 / 数值积分

  • 插值

    • 关于 / 插值
  • 区间估计

    • 关于 / 区间估计、相关度量统计测试
  • IPython 笔记本

    • URL / 如何打开 IPython 笔记本
  • IPython 笔记本

    • 打开 / 如何打开 IPython 笔记本

J

  • Jaccard-Needham / 距离

K

  • 核密度估计

    • 关于 / 分布拟合
  • Kolmogorov / 过滤器

  • Kolmogorov-Smirnov 检验

    • 关于 / 区间估计、相关度量统计检验
  • Krogh 插值器命令 / 插值

  • Kulsinski / 距离

L

  • 拉普拉斯方程

    • 有限元求解器,用于 / 用于拉普拉斯方程的有限元求解器
  • Levenberg-Marquardt 算法 / 回归

  • 对数积分 / 指数/对数积分

  • Lorenz 吸引子 / Lorenz 吸引子

  • LTI 系统理论 / LTI 系统理论

M

  • Mac OS X

    • SciPy,在 / 在 Mac OS X 上安装 SciPy
  • Mahalanobis / 距离

  • 哺乳动物

    • 通过牙齿分类,聚类 / 根据牙齿聚类哺乳动物
  • 曼哈顿 / 距离

  • Maple

    • 关于 / 什么是 SciPy?
  • 遮罩 / 创建数组的例程

  • Mathematica

    • 关于 / 什么是 SciPy?
  • MATLAB

    • 关于 / 什么是 SciPy?
  • MATLAB/Octave

    • 与 / 与 MATLAB/Octave 交互
  • matplotlib

    • 关于 / 什么是 SciPy?
  • 矩阵

    • 创建 / 创建矩阵
  • 矩阵分解 / 特征值问题和矩阵分解

    • 旋转 LU 分解 / 特征值问题和矩阵分解

    • 奇异值分解 / 特征值问题和矩阵分解

    • Cholesky 分解 / 特征值问题和矩阵分解

    • QR 和 QZ 分解 / 特征值问题和矩阵分解

    • Schur 分解 / 特征值问题和矩阵分解

    • Hessenberg 分解 / 特征值问题和矩阵分解

  • 矩阵方法

    • 关于 / 矩阵方法

    • 矩阵间的运算 / 矩阵间的运算

    • 矩阵上的函数 / 矩阵上的函数

    • 特征值问题 / 特征值问题和矩阵分解

    • 矩阵分解 / 特征值问题和矩阵分解

    • 通过 SVD 进行图像压缩 / 通过奇异值分解进行图像压缩

    • 求解器 / 求解器

  • MayaVi

    • URL / 什么是 SciPy?
  • 形态学 / 形态学

  • 多数组

    • 组合,使用的数组例程 / 组合两个或更多数组的例程

N

  • 钽氧化物 / 氧化物的结构模型

  • 数值积分 / 数值积分

O

  • 对象计算 / 对象计算

  • 对象基础

    • 关于 / 对象基础
  • ones 命令 / 创建数组的例程

  • 最佳称重

    • 示例 / 创建矩阵
  • 优化

    • 关于 / 优化

    • 最小化,问题 / 最小化

    • 根 / 根

  • 正交归一基

    • 创建 / 矩阵间的运算

P

  • PCHIP 单调三次插值(pchip) / 插值

  • 皮尔逊相关系数

    • 关于 / 区间估计、相关度量及统计检验
  • 皮尔逊峰度 / 如何查找文档

  • PIL

    • URL / 什么是 SciPy?
  • Plotly

    • URL / 什么是 SciPy?
  • 点二列相关

    • 关于 / 区间估计、相关度量及统计检验
  • 多项式

    • 关于 / 一元多项式
  • 可移植网络图形(PNG) / 科学可视化

  • Python

    • 格式 / 与 C/C++的交互
  • Python 图像库(PIL) / 信号构造

R

  • 排序相关

    • 关于 / 区间估计、相关度量及统计检验
  • 回归

    • 关于 / 回归
  • 黎曼ζ函数

    • 关于 / 黎曼ζ函数
  • RPy

    • URL / 什么是 SciPy?
  • 龙格-库塔方法 / 常微分方程

S

  • Sage(代数和几何实验系统) / 什么是 SciPy?

  • 科学可视化

    • 关于 / 科学可视化
  • SciPy

    • 关于 / 什么是 SciPy?

    • 特征 / 什么是 SciPy?

    • 安装 / 安装 SciPy

    • 在 Mac OS X 上安装,安装 / 在 Mac OS X 上安装 SciPy

    • 在 Unix/Linux 上安装,安装 / 在 Unix/Linux 上安装 SciPy

    • 在 Windows 上安装,安装 / 在 Windows 上安装 SciPy

    • 常微分方程 / 常微分方程

    • 方法 / 层次聚类

  • scipy.special 模块 / 一元多项式

  • SciPy 安装

    • 测试 / 测试 SciPy 安装
  • SciPy 邮件列表

    • URL / 测试 SciPy 安装
  • SciPy 组织

    • 关于 / SciPy 组织
  • 信号构造

    • 关于 / 信号构造
  • 切片

    • 关于 / 数组索引和切片
  • Sobel 算法 / 过滤器

  • SourceForge

    • URL / 安装 SciPy
  • 稀疏矩阵

    • URL / 创建矩阵
  • 斯宾塞积分 / 指数/对数积分

  • 结构模型,氧化物

    • 关于 / 氧化物的结构模型
  • 斯图夫函数

    • 关于 / 贝塞尔和斯图夫函数
  • SVD

    • 通过奇异值分解进行图像压缩 / 通过奇异值分解进行图像压缩
  • SWIG

    • 关于 / 与 C/C++交互

    • URL / 与 C/C++交互

T

  • t 检验

    • 关于 / 区间估计、相关度量统计测试
  • 测试函数

    • 和便利函数 / 便利和测试函数
  • 三角积分 / 三角和双曲三角积分

U

  • 单变量样条(InterpolatedUnivariateSpline)。 / 插值

  • Unix/Linux

    • SciPy,安装于 / 在 Unix/Linux 上安装 SciPy

V

  • 向量

    • 创建 / 向量创建
  • 向量运算

    • 关于 / 向量运算

    • 加法 / 加法/减法

    • 减法 / 加法/减法

    • 标量积 / 标量/点积

    • 点积 / 标量/点积

    • 向量积 / 叉积/向量积 – 在三维空间向量上

    • 向量积 / 叉积/向量积 – 在三维空间向量上

W

  • Wakari

    • URL / 如何打开 IPython 笔记本
  • 警告 / 距离

  • 维纳 / 滤波器

  • Windows

    • SciPy,安装于 / 在 Windows 上安装 SciPy

Z

  • 零点命令 / 创建数组的例程
posted @ 2025-10-24 09:59  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报