python科学计算第二版(可编辑)
python科学计算第二版(可编辑)
python科学计算第二版(可编辑)
清华大学出版社
P y th o n 科 学 计 算
(第2版)
张若愚著
清华大学出版社
北京
Preface
Python is rightfully viewed as a general purpose language , well suited for web development,
system administration, and general puipose business applications. It^s has earned this reputation well
by powering web sites such as YouTube, installation tools integral to Red Hat’ s operating system, and
large coiporate IT systems from cloud cluster management to investment banking . Python has also
established itself firmly in the world o f scientific computing covering a wide range o f applications
from seismic processing for oil exploration to quantum physics . This breadth o f applicability is
significant because these seemingly disparate uses often overlap in important w ays .Applications that
can easily connect to databases publish information to the web, and efficiently carry out complex
calculations are now critical in many industries.Python’ s primary strength is that it allows developers
to build such tools quickly .
Python’ s scientific computing roots actually go quite deep . Guido van Rossum created the
language while at C W I , the Center for Mathematics and Computer Science , in the Netherlands. As
interest developed outside the center,others began to contribute.The first several Python workshops ,
starting in 1994, were held an ocean away at scientific institutions such as NIST (National Institute o f
Instruments and Technology ), the US Geological Society , and L LN L (Lawrence Livemiore National
Laboratories), all science centric institutions. At the time, Python 1.0 had recently been released and
the attendees were just beginning to hammer out the design o f its mathematical tools .A decade and a
half later,it is gratifying to see how far we have come both in the amazing capabilities o f the tool set
and the diversity o f the community . It is somehow fitting that the first comprehensive book (that I
know o 〇 covering the primaiy scientific computing tools for Python is composed and published,
another ocean away , in Chinese . Looking forward a decade and a half, I can hardly wait to see what
we will all build together.
Guido ,himself,was not a scientist or engineer.He sat squarely in the computer science branch o f
CW I and created Python to ease the pain o f building system administration tools for the Amoeba
operating system . At the time, the tools were being written in C . Python was to be the tool that
“bridged the gap between shell scripting and C .”Operating system tools are not even in the same
neighborhood as matrix inversions or fast Fourier transforms,but, as the language emerged ,scientists
around the world were some o f its earliest adopters. Guido had succeeded in creating an elegantly
expressive language that coupled nicely with their existing C and Fortran code . And , in Guido , they
had a language designer willing to listen and add critical features, such as complex numbers,
specifically for the scientific community . With the creation o f Numeric , the precursor to NumPy ,
Python gained a fast and powerful number crunching tool that solidified Python ^ role as a leading
computational language in the coming decades .
Python 科学计算(第2 版)
For some, the term “ scientific programming”conjures up visions o f intricate algorithms described
from “Numerical Recipes in C ”or forged in late night programming sessions by graduate students.
But the reality is the domain encompasses a much wider range o f programming tasks from low level
algorithms to GUI development with advanced graphics.This latter topic is too often underestimated
in temis o f importance and effort. Fortunately, Ruoyu Zhang has done us the service o f covering all
facets o f the scientific programming in this book .Beginning with the foundational Numpy libraiy the
algorithmic toolboxes in SciPy he provides the fundamental tools for any scientific application . He
then aptly covers the 2D plotting and 3D visualization libraries provided by matplotlib, chaco , and
mayavi . Application and GUI development with Traits and Traits UI , and coupling to legacy C
libraries through Cython ,W eave ,ctypes, and SW IG are well covered as w ell . These core tools are
rounded out by coverage o f symbolic mathematics with Sym Py and various other useful topics .
Preface
It's truly gratifying to see all o f these topics aggregated into a single volume . It provides a
one-stop shop that can lead you from the beginning steps to a polished and full featured application for
analysis and simulation.
Eric Jones
2011/12/8
第 1 版序
Python 理所当然地被视为一门通用的程序设计语言,非常适合于网站开发、系统管理以及
通用的业务应用程序。它为诸如 Y o u T u b e 这样的网站系统 、 Red H a t 操作系统中不可或缺的
安装工具以及从云管理到投资银行等大型企业的I T 系统提供技术支持,从而贏得了如此商的
声誉。Python 还在科学计算领域建立了牢固的基础,覆盖了从石汕勘探的地震数椐处理到M 子
物理等范围广泛的应用场景。Python 这种广泛的适用性在于,这些看似不同的应用领域通常在
菜些重要的方面是重S 的。易于与数据库连接、在网络上发布信息并高效地进行复杂计算的应
用程序对于许多行业是至关重要的,
而 Python 最主要的长处就在于它能让开发者迅速地创建这
样的工具。
实际上,Python 与利•学计箅的关系源远流长。吉多•范罗苏姆创建这门语言,还是在他在
荷兰阿姆斯特丹的国家数学和计算机科学研究学会(C W I )的时候。当时只是作为“课余”的开
发,但是很快其他人也开始为之做出贡献。从 1994年开始的头几次 Python 研讨会,都是在大
洋彼岸的科研机构举行的。例如国家标准技术研究所(N IST )、美国地质学会以及分伦斯利福摩
尔国家实验室(L L N L ) , 所有这些都是以科研为中心的机构。当时 Python 1.0刚刚发布,与会者
们就已经开始打造 P y th o n 的数学计算工具。1 0 多年过去了,我们欣喜地看到,我们在开发具
有惊人能力的工具集以及建设多彩的社区方而做出了如此多的成绩。很合时宜的是,就我所知
的第一本涵盖了 P yth on 的主要科学计算工具的综合性著作,在另一个海洋之遥的中国编著并出
版了。展望今后的十儿年,我迫不及待地想看到我们能共同创建出怎样的未来。
吉多他本人并不是科学家或工程师。他 在 C W I 的计算机科学部门时,为了缓解为阿米巴
(Amoeba )操作系统创建系统管理工具的痛苦,他创建了 Python 。当时那些系统管理工具都是用
C 语言编写的。于 是 Python 就成了填补 sh e ll 脚本和 C 语言之间空白的工具。操作系统工具与
计兑逆矩阵或快速傅立叶变换是完全不同的领域,但是从 Python 诞生开始,世界各地的许多科
学家就成了它最早期的采)W者。吉多成功地创建了一门能与他们的C 和 Fortran 代码完美结合
的、具有优雅表现力的程序语言。并且,吉多是一位愿意听取建议并添加关键功能的语言设计
师,例如支持复数就足专门针对科学领域的。随着 N u m P y 的前身--- N um eric 的诞生, Python
获得了一个高效且强大的数值运算工具,它巩固了在未来儿丨•年中,Python 作为领先的科学计
算语 g 的地位。
对于一些人来说, “科学计算编程”会让人联想起 M y〃
w /c〃/ /〃C 中描述的那些S
杂算法,或是研究生们在深夜中努力打造程序的场景。但是真实情况所涵盖的范围更广泛—
—
从底层的算法设计到具有高级绘图功能的用户界而开发。而后者的重要性却常常被忽视了。幸
运的是在本书中,作者为我们介绍了科学计算编程所需的各个方面。从 N u m P y 库 和 S d P y 算
法工具库的莶础开始,介绍了任何科学计兑应用程序所需的基本工具。然后,本书很恰当地介
绍了二维绘图以及三维可视化库--- matplotlib 、Chaco 、M ayavi 。用 T r a its 和 T raitsU I 进行应
用程序和界面开发,以及MJCython 、W eave 、ctyp es 和 S W I G 等与传统的 C 语言库相互结合等
Python 世界的发展日新月异,在本书第1 版出版之后,Python 在数据分析、科学计算领域
又出现了许多令人兴奋的进展:
• IPython 从增强的交互式解释器发展到Jupyter N otebook 项 P1,它己经成为 Python 科学
计算界的标准配置。
• Pandas 经过几个版本的更新,目前已经成为数据清洗、处理和分析的不二选择。
• O p e n C V 官方的扩展库 cv 2 已经正式发布,它的众多图像处理函数能直接对N u m P y 数
组进行处理,编写图像处理、计兑机视觉程序变得更方便、简洁。
• matplotlib 2.0即将发布,它将使用更美观的默认样式。
• C y th o n 内置支持 N u m P y 数组,它已经逐渐成为编写高效运算扩展库的首选工具。
• N um Py 、S c iP y 等也经历了儿个版本的更新,许多计算变得更快捷,功能也更加丰富。
• W inPython 、A naconda 等新兴的 Python 集成环境无须安装,使得开发与共享 Python 程
序更方便快捷。
木书第2 版紧随各个扩展库的发展,将最新、最实用的内容呈现给读者。除了数值计算之
外,本书还包含了界而制作、三维可视化、阁像处理、提高运算效率等方而的内容。最后一章
综合使用本书介绍的各个扩展库,完成儿个有趣的实例项目。
本书完全采用 IPython N otebook 编写,保证了书中所有代码及输出的正确性。附盘中附带
所有章节的 N oteb o ok 以及便携式运行环境W in P y th o m 以方便读者运行书中所有实例。
本书适合于工科高年级本科生、研究生、工程技术人员以及计算机开发人员阅读,也适合
阅读过第1 版的读者了解各个扩展库的最新进展,进一步深入学习。
阅读本书的读者需要掌握P yth on 语言的一些栽础知识,C y th o n 章节需要读者能够阅读 C
语言代码。
除封面署名的作者外,参加本书编写的人员还有张佑林、张东等人,在 此 _•并表示感谢。
2.2.2比较运算和布尔运算..... 59
第1章 Python 科学计算环境的安装与
2 . 2 . 3 自定义ufunc函数........ 61
齡 ....................................... 1
2.2.4 广播................... 62
1.1 Python 简介................. 1
2.2.5 ufunc 的方法............. 66
1.1.1 Python 2 还是 Python 3 ...... 1
2 . 3 多维数组的下标存収........ 68
1.1.2开发环境................. 2
2.3.1下标对象................68
1.1.3集成开发环境(IDE)........ 5
2.3.2整数数组作为下标....... 70
1.2 IPython Notebook 入 门 ........9
2.3.3 —个M 获的例子......... 72
1.2.1基木操作................ 10
2.3.4布尔数组作为下标........ 73
1.2.2 魔法(Magic)命令..........12
2 . 4 庞大的闲数厍.............. 74
1.2.3 Notebook 的显示系统.....20
2.4.1随机数................74
1.2.4 定制 IPython Notebook.... 24
2.4.2求和、平均值、方差...... 77
1 . 3 扩展库介绍................ 27
2.4.3大小与排序............ 81
1.3.1数值计算库............ 27
2.4.4统计函数................86
1.3.2符号计算;$............ 28
2.4.5分段函数................89
1.3.3绘图与可视化.......... 28
2.4.6操作多维数组............ 92
1.3.4数据处理和分析......... 29
2.4.7多项式函数.............. 96
1.3.5界面设计.............. 30
2.4.8多项式函数类............ 98
1.3.6图像处理和计算机视觉………31
2.4.9各种乘积运算.......... 103
1.3.7提高运算速度............31
2.4.10 广义 ufunc 函数......... 106
第2 章 NumPy-快速处理数据.......33 2 . 5 实用技巧................. 110
2.1 ndarray 对象............... 33 2.5.1动态数组...............110
2.1.1 创建................. 34 2.5.2和其他对象共享内存..... 112
2.1.2 7G素类型.............. 35 2.5.3与结构数组共李内存..... 115
2.1.3 自动生成数组.......... 37
第 3 章 SciPy-数值计算库......... 117
2.1.4存収元素.............. 40
3 . 1 常数和特殊函数............ 117
2.1.5多维数组.............. 43
3.2 拟合与优化-optim ize ....... 119
2.1.6结构数组.............. 47
3.2.1非线性方程组求解....... 120
2.1.7内存结构.............. 50 3.2.2最小二乘拟合........... 121
2.2 ufunc 函数................ 56 3.2.3计算W数局域最小值..... 125
2.2.1四则运算.............. 58 3.2.4计舒全域最小值......... 127
Python 科学计算(第2 版)
VIII
4.6.4添加 GUI 而板.......... 288 第 6 章 SymPy-符号运算好帮手•…•…359
6 . 1 从例子开始 .................359
第5 章 Pandas-方 便 的 数 据 分 析 库 291
6.1.1封而上的经典公式....... 359
5.1 Pandas 中 的 数 据 对 象 ....... 291
6.1.2球体体积.............. 361
5.1.1 Series 对象............. 291
6.1.3数值微分.............. 362
5.1.2 DataFrame 对象.........293
6 . 2 数学表达式 .................365
5.1.3 Index 对象............. 297
6.2.1 符号.................. 365
5.1.4 Multiindex 对象......... 298
6.2.2 数值.................. 367
5.1.5常用的函数参数......... 300
6.2.3运算符和函数........... 368
5.1.6 DataF丨
•amc 的内部结构... 301
6.2.4通配符................ 371
5 . 2 下 标 存 収 ................... 303
6 . 3 符号运算 ................... 373
5.2.1 [】
操作符............... 304
6.3.1 表达式变换和化简....... 373
5.2.2 .loc□和.iloc□存取器...... 304
6.3.2 方程.................. 376
5.2.3获収单个值............ 306
6.3.3 微分.................. 377
5.2.4多级标签的存取......... 306
6.3.4微分方程.............. 378
5.2.5 query()方法............. 307
6.3.5 积分..................379
5 . 3 文 件 的 输 入 输 出 ............ 307
6 . 4 输出符号表达式............ 380
5.3.1 CSV 文件.............. 308
6.4.1 lambdify............... 38 i
5.3.2 HDF5 文件............. 309
6.4.2 ”j autowmpO编译表达式... 381
5.3.3读写数据库............ 313
6.4.3使ilj cse()分步输出表达式…•384
5.3.4 使fll Pickle 序列化....... 314
6 . 5 机械运动模拟 .............. 385
5 . 4 数 值 运 算 函 数 .............. 315
6.5.1推导系统的微分方程..... 386
5.5 时 间 序 列 ................... 323
6.5.2将符号表达式转换为程序……388
5.5.1时间点、时间段、时间
6.5.3动画演示.............. 389
1、
_ .................. 323
5.5.2吋间序歹ij...............326 第 7 章 T ra its & TraitsUI-轻松制作
5.5.3与 NaN 相关的函数...... 329 图形界面................. 393
5.5.4 改变 DataFrame 的 ... 333 7.1 Traits 类型入门 ............. 393
5 . 6 分 组 运 算 ................... 338 7.1.1什么是Traits屈性....... 393
5.6.1 gro叩by()方法...........339 7.1.2 Trait屈性的功能........ 396
5.6.2 GroupBy 对象...........340 7.1.3 Trait 类型对象.......... 399
5.6.3分组一运算一合并....... 341 7.1.4 Trait 的元数据.......... 401
5 . 7 数 据 处 理 和 可 视 化 实 例 ..... 347 7.2 Trait 类型 ...................403
5.7.1分析 Pandas项目的提交 7.2.1预定义的Trait类型...... 403
■ .................. 347 7.2.2 Property 属性........... 406
5.7.2分析空气质量数据....... 354 7.2.3 Trait 屈性监听.......... 408
7.2.4 Event 和 Button 屈性..... 411
Python 科学计算(第2 版)
X
9 . 5 形状与结构分析........... 564 10.6.1 创建 ufunc 函数....... 613
9.5.1轮腐检测............. 565 10.6.2快速调用D L L 中的
9.5.2轮廓匹配............. 568 函数................ 617
9 . 6 类型转换................. 569 10.6.3 调用 BLAS 函数...... 620
9.6.1 分析 cv2 的源程序....... 570
第 1 1 章 实 例 .................... 627
9.6.2 Mat 对象...............572
1 1 . 1 使用泊松混合合成图像………627
9.3.3在 c v 和 cv2 之间转换阁像
11.1.1泊松混合算法.........627
对象.................. 574
11.1.2编写代码............ 629
第 1 0 章 Cython-编译 Python 程 序 575 11.1.3演示程序............ 632
1 0 . 1 配置编译器.............. 575 1 1 . 2 经典力学模拟 ............. 632
10.2 Cython 入 门 ..........577 11.2.1 悬链线.............. 633
10.2.1计算欠量集的距离矩阵••…577 11.2.2敁速降线............ 638
10.2.2将 Cython程序编译成 11.2.3单摆模拟............ 641
扩展模块............ 579 1 1 . 3 推荐算法 ..................644
10.2.3 C 语言屮的Python对象 11.3.1读入数据............ 645
类型 .............. 581 11.3.2推荐性能评价标准..... 646
10.2.4使用 cdef 关键字声明变量 11.3.3矩阵分解............ 647
类型 .............. 582 11.3.4使爪最小二乘法实现
10.2.5使用 def 定义函数.....585 矩阵分解............ 648
10.2.6使用 cdef 定义 C 语言 11.3.5使爪 Cython迭代实现
函数.............. 586 矩阵分解............ 651
1 0 . 3 高效处理数组............ 587 1 1 . 4 频域信号处理 ............. 654
10.3.1 Cython 的内存视图.... 587 11.4.1 FFT 知识尨习.........654
10.3.2川?¥采样提高绘图速度•••••592 11.4.2合成吋域信号.........657
1 0 . 4 使j Python 标准对象和 11.4.3观察信号的频谱.......660
A P I .................... 596 11.4.4卷积运算............ 671
10.4.1 操作 list 对象......... 596 11 . 5 布尔可满足性问题求解器•••• 675
10.4.2 创逑 tuple 对象........ 597 11.5.1 用 Cython 包裝 PicoSAT…"678
10.4.3 用 array.array 作为动态 11.5.2数独游戏............ 682
数组 .............. 598 11.5.3扫雷游戏............ 686
1 0 . 5 扩展类型................ 600 11.6 分形...................... 693
10.5.1扩展类型的基本结构…•…600 11.6.1 Mandelbrot 集合...... 693
10.5.2 —维浮点数向量类型•……601 11.6.2迭代函数系统.........699
10.5.3 包装 ahocorasick 席 .... 606 11.6.3 L-System 分形........ 706
10.6 Cython 技巧集........... 612 11.6.4 分形山 ............ 710
Python科学计算环境的安装与简介
1.1 Python 简介
自从2008年发布以来,Pyth〇
n3 经历了 5 个小版本的更迭,无论是语法还是标准库都发展
得十分成熟。许多重要的扩展库也已经逐渐同时支持Pyth〇
n2 和 Pylhon3。但是由于 Python3 不向
Python 科学计算(第 2 版)
1 . 1 . 2 开发环境
1. Win Python
Pytho科
n 学计算环境的安装与简介
- https://winpython.github.io/
WinPython 的下载地址。
W in P yth on 只 能 在 W in d o w s 系统中运行,其安装包不会修改系统的任何配置,各种扩
展库的用户配置文件也保存在 W in P yth o n 的文件夹之下。因此可将整个运行环境复制到 U
盘中,在任•何安装了 W in d o w s 操作系统的计算机上运行。W in P yth on 提供了一个安装扩展
库 的 WinPython Control P a n e l 界面程序,通过它可以安装 P y th o n 的各种扩展库。可以通过
下丨ffl的链接下载已经编译好的二进制扩展库安装包,然 后 通 过 W inPython Control P a n e l 来
安装。
http://www .lfd.uci.edu/~gohlke/pythonlibs/
从该网址可以下载各种Python 扩展库的 W indows 安装文件。
所有扩展库。
i l l 然手动安装扩展库有些麻烦,不过这种方式适合没有网络连接或者网速较慢的计算机。
例如在笔者的工作环境中,有大量的实验用计箅机不允许连接互联网。
「一
图 1-1通过 WinPython Control Panel安装扩展阵
2. Anaconda
一 https://store.continuum.io/cshop/anaconda/
C 交 Anaconda 的下载地址。
叩 *7 说明
conda list 列出所有的扩展厍
conda update扩展沛•名 升级扩展库
conda install扩展序名 安装扩展砟
conda search 模板 搜索符合模板的扩展库
Python 科学计算(第 2 版)
还 可 以 使 用 p i p 命 令 安 装 下 载 的 扩 展 库 文 件 ,例 如 从 前 而 介 绍 的 网 址 下 载 文 件
opencv_python-2.4.11-cp 27-none-win32.whl 之 后 ,切换到该文件所在的路径并输入 pip install
opencv_python-2.4.1 l ~cp 27-none-win32.whl 即可安装该扩展库。
3.使用附赠光盘中的开发环境
本书的附赠光盘中包含了能运行本书所有实例程序的WinPython Hi缩包:winpython.zip 。
Pytho科
1 . 1 . 3 集成开发环境(IDE)
1 •Spyder
丨
冬| 1-2在 Spyder中执行图像处理的程序
Spyder 的界面由许多泊坞窗口构成,用户可以根据自己的喜好调整它们的位置和大小。当
多个窗口在一个区域屮吋,将使用标签页的形式显示。例 如 在 图 1-2屮,可 以 看 到 “Editor”、
Python 科学计算(第 2 版)
表 1-2 Spyder的主要窗口及其作用
窗口名 功能
Editor 编辑程序,以标签页的形式编辑多个程序文件
Console 在别的进程中运行的Python控制台
Variable explorer 显示 Python控制台中的变量列表
Object inspector 査看对象的说明文档和源程序
File explorer 文件浏览器,用来打开程序文件或者切换当前路径
按 F 5 键将在另外的控制台进程中运行当前编辑器中的程序。第一次运行程序时,将弹出
一个如图1-3所示的运行配置对话框。在此对话框中可以对程序的运行进行如下配置:
rrsa
Python
iOVUMisVRtVCl»fipb〇KVciC^>bMk2Vc«dMl«cp)r2ln1rol«cvd«rj
科学计算环境的安装与简介
n PirtoncrPylhon
l(«dfV^onrA*f〇
r〇
t〇
r
ExKwtov*«ni *stcmWS)
0fnr4l »«4ir««
ComiMnSim
图 1-3运行配咒对话框
Python
科学计算环境的安装与简介
图 1 4 使 用 “Variablecxplorcr”杏看■和编钳变S 内容
7
Python科学计算(第2 版)
Spyder的功|拒比较多,这里仅介绍一些常用的功|拒和技巧:
• 默 认 配 置 下 , “Variableexpbrer”中不显示大写字母幵头的变量,可以单击工具栏中的
配置按钮(最后一个按钮),在菜单中取消 “ Exclude capitalized references” 的勾选状态。
•在 控 制 台 中 ,可以按 Tab 键自动补全。在变量名之后输入“?”,可以在“Objectinspector”
窗口中杏看对象的说明文档。此窗口的 Options菜 单 中 的 “Showsource”选项可以开泊
显示函数的源程序。
• 可 以 通 过 “ Working directory”工具栏修改工作路径,用户程序运行时,将以此工作路
径作为当前路径。只需要修改工作路径,就可以用同一个程序处理不同文件夹下的数
据文件。
• 在 程 序 编 辑 窗 口 中 按 住 C tr l 按键,并单击变量名、函数名、类名或模块名,可以快速
跳转到其定义位置。如果是在别的程序文件中定义的,将打开此文件。在学习一个新
的模块库的用法时,经常需要查看模块屮的某个函数或某个类是如何定义的,使用此
功能可以帮助我们快速查看和分析各个库的源程序。
2. PyCharm
Pytho科
动完成、重构、自动导入、调试等功能。虽然专业版价格比较高,但是提供的免费社区版具有
幵 发 Python程序所需的所有功能。如果读者需要幵发较大的应用程序,使用它可以提高开发效
率,保证代码的质f f l 。
http://www .
jetbrains.com/pychaim
◎ PyCharm 的官方网站。
如果读者使用本书提供的便携WinPython 版本,
那么需要在 PyCharm 1丨
1设H Python 解释器。
通 过 菜 单 “f ile ”— “Settings”打开配置对话框,在左栏中找到 “ Project Interpreter”,然后通过
右侧的齿轮按钮,并选择弹出菜单中的“AddLocal ”选项,即可打开如图1-6所示的对话框。
图1~6配 S Python解释器的路径
>
由于本书提供的代码没有复制到 P yth o n 的库搜索路径中,可 以 将 scpy2 的路径添加进
PYTH O N PATH 环境变量,
或者在 PyCharm 中将 scpy2 所在的路径添加进 Python 的库搜索路径。
单击上面提到的齿轮按钮,并 选 择 “Morc ...”, 将 打 开 图 1-7中左侧的对话框,选择解释器之
后,
单击右侧工具栏中最下方的按钮,
打开路径配置对话框,
通过此对话框添加木书提供的scpy2
庳所在的路径。
OK Cancel
Pytho科
图 1-7添加库搜索路径
n 学计算环境的安装与简介
1.2 IPython Notebook 人门
http://nbviewer.ipython.org/
Q 通过这个网站可以快速查看网络上任何Notebook 的内容《
在 IPython 的官方网站上收集了许多开发者发布的Notebook:
減 https://github.com/ipython/ipython/wiki/A -galleiy-of-interesting-IPython-Notebooks
上面有许多有趣的Notebook。
1 . 2 . 1 基本操作
1.运行丨Python Notebook
常退出,也不会丢失任何州户输入的数据。在关闭服务器进程之前,确保所有的 Notebook 都已
保存。
n 学计算环境的安装与简介
is PIW
■Notetx
ook
& nm m
服务器逬程
峨鼷
►
运®核逬程
2.操作单元
N otebook 由多个竖向排列的单元构成,每个单元可以有以下两种样式:
• Code : C o d e 单元中的文本将被作为代码执行,执行代码时按 Shift+ Entei•快捷键,即N
时按下 Shift 和 Enter键 。
• Markdown : 使j|j Markdown 的格式化文本,可以通过简单的标记表示各种显示格式。
单元的样式可以通过工具栏中的下拉框或快捷键来选杼。为了快速操作这些单元格,需要
掌握一些快捷键,完整的快捷键列表可以通过菜单Help — Keyboard Shortcuts杏看。
Notebook 有两种编辑模式:命令模式和单元编辑模式。在命令模式中,被选中的单元格的
边框为灰色。该模式用来对整个单元格进行操作,例如删除、添加、修改格式等。按 Enter 键
进入单元编辑模式,边框的颜色变为绿色,并且上方菜单条的右侧会出现铅笔阁标,表示 H 前
处于编辑状态。按 E sc 键可返回命令模式。
3. 安装 MathJax
Python
序 ,它将会下载 MathJax 到本地硬盘:
科学计算环境的安装与简介
from IPython.external.mathjax import install_mathjax, default_dest
install—mathjax()
$eA{i \p i > + 1 = 0$
到数学公式。
4. 操作运算进程
在代码单元中输入的代码都将在运算核进程的运行环境中执行。当执行某些代码出现问题
时,可以通过 Kernel菜单中的选项操作该进程:
• im e ir u p h 中断运行当前的程序,当程序进入死循环时可以通过它中断程序运行。
Python 科学计算(第 2 版)
• Restart:当运算核进程在扩展模块的程序中进入死循环,
无法通过 Interrupt菜单中断时,
可以通过此选项重新启动运算核进程。
一旦运算核进程被关闭,运行环境中的对象将不复存在,此时可以通过 Cell — Rim A l l 菜
单再次执行所有单元中的代码。代码将按照从上到下的顺序执行。由于用户在编写 Notebook
时,可以按照任意顺序执行单元,冈此为了保证能再现运行环境中的所有对象,请记住调整单
元的先后顺序。
1. 2 . 2 魔法(Magic)命令
1.显 示 matplotlib 图表
科学计算环境的安装与简介
%matplotlib inline
import pylab as pi
pl.seed(l)
data = pl.randn(100)
pi.plot(data)
%config InlineBackend.figure_format="svg"
pi.plot(data)
内嵌阁表很适合制作图文并茂的Notebook, 然而它们是静态的,无法进行交互。可以将图
表输出模式修改为使用G U I 界面库,下面的 qt4 表示使用 QT 4 界面库显示图表。请读者根据自
己系统的配置,选择合适的界面库:gtk 、osx 、qt、qt4、tk 、w x 。
执行下面的语句将弹出一个W 口显示图表,可以通过鼠标和键盘与此图表交互。请注意该
功能只能在运行 IPython Kernel 的机器上显示图表。
%matplotlib qt4
pi.plot(data)
2.性能分析
a = [1,2,3]
%timeit a[l] = 10
Python
10000000 loops, best of 3: 69.3 ns per loop
科学计算环境的安装与简介
则用于测试整个单元中代码的执行时间。下面的代码测试空列表中循环添加10
个元素所需的时间:
%%timeit
a = []
for i in xrange(10):
a.append(i)
1000000 loops, best of 3: 1.82 \xs per loop
%%time
a = []
for i in xrange(100000):
a.append(i)
Wall time: 18 ms
time 和 timeit命令都使用print输出倍息,
如果希望用程序分析这些仏'息,
可以使用% %capture
命令,将单元格的输出保存为一个对象。下面的程序对不同长度的列表调叫 mnd〇m .shuffie()以
打乱顺序,用%tim e 记录下 shuffieO的运行时间:
%%capture time一results
import random
Python 科学计算(第 2 版)
time_results.stdout屈性保存标准输出管道中的输出信息:
print time_results.stdout
n=1000
Wall time: 1 ms
n=5000
Wall time: 5 ms
n=10000
Wall time: 10 ms
n=50000
Wall time: 40 ms
n=100000
Python
Wall time: 62 ms
科学计算环境的安装与简介
n=500000
Wall time: 400 ms
如 果 在 调 用 命 令 时 添 加 •〇参数,则返回一个表示运行时间信息的对象。下面的程序
对不同长度的列表调用sorted()排序,并 使 命 令 统 计 排 序 所 需 的 时 间 :
timeit_results = []
for n in [5000, 10000, 20000, 40000, 80000, 160000, 320000]:
alist = [random.random() for i in xrange(n)]
res = %timeit -o sorted(alist)
timeit_results.append((n, res))
1000 loops, best of 3: 1.56 ms per loop
100 loops^ bestof 3: 3.32 ms per loop
100 loops, bestof 3: 7.57 ms per loop
100 loops, bestof 3: 16.4 ms per loop
10 loops, best of 3: 35.8 ms per loop
10 loops, best of 3: 81 ms per loop
10 loops, best of 3: 185 ms per loop
图 1-9显示了排序的耗时结果。横坐标为对数坐标轴,表示数组的长度;纵坐标为平均每
个元素所需的排序时间。可以看出每个元素所需的平均排序时间与数组长度的对数成正比,因
此可以计算出排序函数sortedO的时间复杂度为:0(nlogn)。
阁 1-9 sortedO函数的时间复杂度
Pytho科
%%prun
n 学计算环境的安装与简介
def fib(n):
if n < 2:
return 1
else:
return fib(n-l) + fib(n-2)
fib(20)
fib_fast(20)
21913 function calls (4 primitive calls) in 0.007 seconds
3.代码调试
% d e b u g 命令用于调试代码,它有两种用法:一•种是在执行代码之前设置断点进行调试;
另一种则是在代码抛出兄常之后,执 S % debug 命令查看调j |j 堆栈。下面先演示第二种州法:
import math
def sinc(x):
return math.sin(x) / x
<ipython-input-28-9b69eaad97fe> in sinc(x)
2
3 def sinc(x):
--- >4 return math.sin(x) / x
5
6 [sinc(x) for x in range(5)]
%debug
><ipython-input-28-9b69eaad97fe>(4)sinc()
3 def sinc(x):
--- >4 return math.sin(x) / x
5
ipdb> p x
0
ipdb> q
%%func_debug np.unique
np.unique([l, 2, S, A, 2])
Breakpoint 1 at
c :\winpython-32bit-2.7.9.2\python-2.7.9\lib\site-packages\numpy\lib\anraysetops.py:96
NOTE: Enter 'c' at the ipdb> prompt to continue execution.
> c:\winpython-32bit-2.7.9.2\python-2.7.9\lib\site-packages\numpy\lib\anraysetops.py(173)
unique()
172 ""
--> 173 ar = np.asanyarray(ar).flatten()
174
ipdb> n
>c:\winpython-32bit-2.7.9.2\python-2.7.9\lib\site-packages\numpy\lib\anraysetops.py(175)
unique()
174
--> 175 optional_indices = return_index on retunn_inverse
176 optional一returns = optional—indices or return—counts
ipdb> c
4.自定义的魔法命令
scpy2.utils.nbmagics: 该模块中定义了本书提供的魔法命令,如果读者使用本书提供的
批处理运行 Notebook, 则该模块已经载入。notebooks\01-intro\scpy2-magics.ipynb 是这些
魔法命令的使用说明。
^register一line—magic
def find(line):
from IPython.core.getipython import get_ipython
from fnmatch import fnmatch
items = line.split() O
patterns, target = items[:-l], items[-1]
ipython = get_ipython() ©
names = dir(ipython.ev(target)) ©
Python 科学计算(第 2 版)
re su lts = []
fo r pattern in p attern s :
fo r name in names:
i f fnmatch(name, pattern ):
r e s u lt s .append(name)
return re su lts
import numpy as np
科学计算环境的安装与简介
@magic_arguments()
@argument(■-1', '--lin e s ', help='max lin e s 、 type =in t , d efau lt =100)
^argumentC -c ', '--chars ', help='max chars '^ type =in t ^ d efau lt =10000)
@re g iste r _cell_magic
def cu t (lin e , c e l l ):
from IPython .co re. getipython import get_ipython
from sys import stdout
args = parse_argstring(cut, line) O
max_lines = args.lines
max_chars = args.chars
def write(string):
counters ["lines"] += string, count ("W.)
counters["chars"] += len(string)
Python
old_write( string)
科学计算环境的安装与简介
try:
old_write, stdout.write = stdout.write, write ©
ipython = get_ipython()
ipython.run_cell(cell) ©
finally:
del stdout.write O
〇调 用 parse_argstring()分析行参数,它的第一个参数是使用 argument装饰器修饰过的魔法
命令函数,第二个参数为行命令字符苹。© 在调用单元代码之前,将 stdout.writeO替换为限制输
出行数和字符数的 write〇函数。© 调用运算核对象的 mn_Cd l 〇来运行肀元代码。O 运行完毕之
后 将 stdout.write〇删除,恢复到原始状态。
下面是使用%% cu t 限制输出行数的例子:
%%cut - 1 5
for i in range(10000):
print "I am line", i
I am line 0
I am line 1
I am line 2
I am line 3
I am line 4
<ipython-input-8-e0ddfb5el8b6> in write(string)
20
21 if counters["lines"] >= max一lines:
— > 22 raise IOError("Too many lines")
23 elif counters["chars"] >= max_chars:
24 raise IOError("Too many characters")
1.2.3 Notebook 的 显 示 系 统
若单元中代码的最后一行没有缩进,并且不以分号结尾,则在单元的输出栏中显示运行该
代码后得到的对象。此外运算核的标准输出被重定M 到单元的输出框中,冈此可以使用 print
语句输出任何信息。例如在下面的程序中,使用循环进行累加计算,在循环体中使用 print输出
Python
中间结果,而最后一行的运算结果就是变S s 的值:
科学计算环境的安装与简介
s = 0
for i in range(4):
s += i
print "i={}, s={}".format(i, s)
s
i=0, s=0
i=l, s=l
i=2, s=3
i=3j s=6
6
1•display 模块
由于 Notebook 采用浏览器作为界而,因此除了可以显示文木之外,
还可以显示阁像、
动画、
H T M L 等多种形式的数掘。有关显示方而的功能均在 IPython.display 模块中定义。其中提供了
表 1-3所示的对象,用于显示各种格式的数据。
表 1-3 IPython.display模块提供的用于显示各种格式的数据的类
类名 说明
Audio 将二进制数据、文件或网址显示为播放声音的控件
FileLink 将文件夹路径显示为一个超链接
FileLinks 将文件夹路径显示为一姐超链接
HTML 将字符卑、文件或网址显示为HTML
Image 将表示图像的二进制字符串、文件或网址显示为图像
20
(赚 )
类名 说明
Javascript 将字符率作为Javascript代码在浏览器屮运行
Latex 将字符串作为LaTeX代码显示,主耍用于显示数学公式
SVG 将字符卑、文件或网址显示为SVG 图形
当对单元中程序的最后一行求值并得到上述类型的对象吋,将在单元的输出栏中显示对应
的格式。
也可以使用 display模块中的 displayO闲数在程序中输出这些对象。
下而的程序使用Latex
对象输出了 3 个数学公式,其中前两个使用 displayO输出,而由于最后一行的求值结果为Latex()
对象,因此它也会被显示为数学公式。
x2 + y2 5
学
x3 + y3 计
算
x4 + y 4 环
Im age 对象可以用于显示阁像,当 用 u r l 参数时,它会从指定的网址获取阁像,并显示在 境
的
Notebook中。如 果 embed参数为 True, 图像的数据将直接嵌入到Notelxx)k之中,这样此后打开 安
装
此 Notelxx)k时,即使没有联网也可以显示该图像。 与
简
介
logourl = "https://www.python.org/static/community_logos/python-logo-masten-v3-TM.png"
display•Image(url=logourl, embed=True)
21
Python 科学计算(第 2 版)
import numpy as np
y, x = np.mgrid[-3:3:300j, -6:6:600j]
z = np.sin(x**2 + 2*y**2 + x*y)
png = as_png(z, cmap="Blues", vmin=-2, vmax=2)
print repr(png[:10])
display.Image(png)
'\x89PNG\r\n\xla\n\x00\x00'
2.自定义对象的显示格式
它们分别使用 H T M L 和 P N G 图像显示颜色信总。
class Color(object):
def html_color(self):
return '#{:02x}{:02x}{:02x}'.format(*self.rgb)
def invert(self):
r, g, b = self.rgb
return Color(255-r, 255-g, 255-b)
def _repr_html_(self):
color = self.html_color()
inv_color = self.invert().html_color()
template = '<span
style="background-color:{c};colon:{ic};padding:5px;">{c}</span>'
return template.format(c=color, ic=inv_color)
def _repr_png_(self):
img = np.empty((50, 50, 3), dtype=np.uint8)
i m g [ ] = self.rgb
return as_png(img)
下UU创 連 Color对象,并直接查看它,IPython会自动选择最合适的显示格式。lil 于 Notebook
是基于 HTML 的,HTML 格式的优先级别最高,因此查看 Color对象时,_repr_html_()方法将被
调用:
display.display j3ng(c)
shell = get_ipython()
Python
调 用 Formatter.for_type_by_name〇可以为该输出格式添加指定的格式显示函数,其前两个参
科学计算环境的安装与简介
数分别为模块名和类名。由于使用字符串指定类,因此添加格式显示函数时不需要载入IeI标类。
下而的代码为 NumPy的数组添加品示函数as_png():
下而查看前而创建的数组z ,它将以图像的形式呈现,结果如图1-10(右)所示。
如果目标类已被载入,可以使用 f〇
r_type()方法为其添加格式显示函数。下而的代码将表示
分数的 Fmction类使用 LaTeX 的数学公式进行显示:
latex 一
formatter.for_type(Fraction, fraction_formatter)
Fraction(3, 4) ** 4 / 3
27
25 6
虽 然 I P y t h o n 只提供了最堪本的编辑、
运行N o te b o o k 的功能,
但楚它具有丰富的可定制性,
用户可以根据自己的需要打造出独特的N o te b o o k 幵发环境。如 图 1 -8 所示,I P y t h o n N o te b o o k 系
统由浏览器、服务器和运算核三部分组成。I P y l h o n 分别提供了这三部分的定制方法。
1•用户配置(profile)
N o te b o o k H O M E
舄j 将配置文件夹和 N o te b o o k 文件一起打包。
n 学计算环境的安装与简介
import os
ipython = get_ipython()
print "HOME 环境变量: " ,os.environ["HOME"]
当前的用户配贾文件夹:
C :\Users\RY\Dropbox\scipybook2\settings\.ipython\profile_scipybook2
可以在命令行中输入如_K 命令来创建新的用户配置:
2 .服务器扩展插件和 N o te b o o k 扩展插件
h ttp s ://g i t h u b .c o m /i p y t h o n -c o n t r i b /l P y t h o n -n o t e b o o k -e x t e n s i o n s /w i k i /c o n f i g - e x t e n s i o n
安 装 I P y t h o n 扩展程序的说明。
import I P y t h o n .h t m l .nbextensions as nb
ext= 'https://github.eom/ipython-contrib/IPython-notebook-extensions/archive/3.x.zip'
nb.install_nbextension(ext, user=True)
Pytho科
上面的程序将在 n b e x t e n s i o n s 文件夹下创建 E P y t h o n -n o t e b o o k -e x t e n s i o n s -3 .x 文件夹,其中包
n 学计算环境的安装与简介
含了许多客户端扩展程序。接下来按照如下步骤完成安装:
⑴将 \
n b e x te n s io n s I P y th o n -n o t e b o o k -e x t e n s i o n s -3 .x \ c o n f i g 移到 n b e x t e n s i o n s 文件夹之下。
(2 ) 将 n b e x t e n s i o n s \c o n f i g \n b e x t e n s i o n s . p y 移到 e x t e n s i o n s 文件夹之下。
(3 ) 布.i p y t h o n 之下创建 t e m p la te s 文件夹。
⑷将 \ \
n b e x t e n s io n s c o n f ig n b e x te n s io n s .h tm l 移到 t e m p l a t e s 文件夹之下。
⑶将 \
n b e x te n s io n s c o n fig \i p y t h o n —n o t e b ⑴k —c o n f i g . p y 中的代码添加到 p r o file _d e f a u l t \i p y t h o n _
n o te b o o k _c o n f i g . p y 中。
(6 )访 问 h U p ://l o c a l h o s t :8 8 8 8 /n b e x t e n s i o n s /,在该页而上可以管理n b e x t e n s i o n s 文件夹下安装的
客户端扩展程序。
当N o t e b x x :
)k 服务器启动时,
会运行用户配寬(p r o f i l e )文件夹之下的i p y t h o n _n o t e l x x )k _c o n f i g . p y
文件,并使用其中的配置。
下 面 是 i p y t h o n _n o t e b o o k _c o n f i g .p y 中的配背代码。 0 |v j*先 将 e x t e n s i o n s 文件夹添加到 P y t h o n
的模块搜索路径之下,
因此该路径之下的n b e x t e n s i o n s .p y 文件可以通过i m p o r t n b e x te n s io n s 载入。
©指定服务器扩展程序的模块名,由于之前添加了搜索路径,因此 P y t h o n 可以直接通过模块名
'n b e x t e n s i o n s '找到对应的文件 n b e x t e n s i o n s .p y 。© 将 te m p la te s 文件夹添加到服务器扩展程序的网
页模板的搜索路径,让服务器可以找到 n b e x t e n s i 〇
n s.
h t m l 文件。
import os
import sys
ipythondir = get_ipython_dir()
Python 科学计算(第 2 版)
extensions = os.path.join(ipythondir,'extensions')
sys.pat h .a p p e n d ( extensions ) O
c = get_config()
c.NotebookApp.server— extensions = [ 'n b e x t e n s i o n s '] ©
c .N o t e b o o k A p p .extra_template_paths = [os.path.join(ipythondir^'templates')] ©
3.添加新的运算核
由于执行用户代码的运兑核与Notebook服务器是独立的进程,H 此不 N 的 Notebook可以
使川不同版本的 Python,甚至是其他语言的运算核。IPython的下一个版本将改名为Jupyter,其
Python
目标是创建通用的科学计算的开发环境,
支持 Julia、
Pyth〇
n 和 R 等在数据处理领域流行的语言。
科学计算环境的安装与简介
下面以 Python3-64bit为例介绍如何添加新的运箅核。
首先从 WinPython 的网址下载 WinPython-64bit-3.4.3.3.exe ,并安装在 C 盘 根 录 之 K 。然后
运行下面的代码来创建运算核配置文件:
import os
from os import path
import json
ipython = get_ipython()
kernel_SGttings = {
" a r g v " : [python3j3ath,
"IPython.kernel"-f", "{connection_file}"],
>
with open(kernel一
fn, "w") as f:
json.dump(kernel_settings, f, indent=4)
上而的代码创建.ipyth〇
n\kemels^>ython3-64bit\kemel.j son 文件•,它是一个 JSON 格式的字典,
其中"a r g v t 为运算核的启动命令,"display_namen为 运 算 核 的 示 名 称 ,"language"为运算核的
飞九、 •
=
I口口〇
刷 新 Notebook的索引页面之后,可 以 在 “New ”下拉菜单中找到“Python3-64bit”选项,
单击它将打开•个以 Python3 64bit解释器为运算核的 Notebook炎面。在 Notebook:贝面中也可以
使 用 “Kernel”菜单更改当前的运算核。运兑核的配置保存在 Notebook文件中,因此下一次开
启 Notebook时,将自动使用最后一次选择的运算核。
感兴趣的读者可以试试添加更多的运算核,笔 者 在 Windows系统下成功地安装了 PyPy、
Julia、R 、 NodeJS 等运算核。
1 . 3 扩展库介绍
Python 科学计算方面的内容由许多扩展库构成。本书将对编写科学计算软件时常用的一些
扩展库进行详细介绍,这里先简要介绍本书涉及的扩展库。
1 . 3 . 1 数值计算库
N um Py 为 Python 带来了真正的多维数组功能,
并且提供了丰富的函数库来处理这些数组。
在下而的例子中,使用如下公式计算ti, 可以看到在 N um Py 中使用数组运算替代通常需要借助
循环的运算:
1 3 5 7 9 11 13
import numpy as np
n = 100000
3.141572653589833
S d P y 则 在 N um Py 基础上添加了众多的科学计算所需的各种工具,它的核心计算部分都是
一些久经考验的 Fortran数值计算库,例如:
Python 科学计算(第 2 版)
•线 性 代 数 使 用 L A P A C K 库
•快 速 傅 立 叶 变 换 使 用 F F T P A C K 库
•常 微 分 方 程 求 解 使 ) O D E P A C K 库
•非线性方程组求解以及最小值求解等使用 M 1N P A C K 库
在下面的例子中,使 用 S d P y 中提供的数值积分函数quad〇计算;r:
r1 ______
TI = 2 I y / l x*
2 dx
—
3.141592653589797
1 . 3 . 2 符号计算库
Sym P y 是一套数学符号运算的扩展库,虽然与一些专门的符号运算软件相比,S ym P y 的功
Python
下 面 用 S ym P y 提供的符号积分函数 imegrateO对上面的公式进行积分运算,可以看到运算
的结果为符号表示的TI:
Pi
1 . 3 . 3 绘图与可视化
分适合编写短小的脚本程序进行快速绘图。此外 , matplotlib采用面向对象的技术来实现,因此
组成图表的各个元素都是对象,在编写较大的应用程序吋通过面向对象的方式使用 matplotlib
将更加有效。
下面的程序绘制隐函数(x 2 + y 2 _ I )3 —x 2y 3 = 0 的曲线,结果如图 M l 所示。
x, y = np.mgrid[-2:2:500j> -2:2:500j]
Pytho科
(x 2 + - y 2 -h z 2 — l )3 —x 2z 3 —— y 2z 3 = 0
4 80
n 学计算环境的安装与简介
%%mlab_plot
from mayavi import mlab
x , y , z = np.mgrid[-3:3:100j , -1:1:100〕
、 -3:3:100j ]
f = (x**2 + 9.0/4*y**2 + z**2 _ 1)**3 - x**2 * z**3 - 9.0/80 * y**2 * z**3
图 1-12使用Mayavi绘制心形隐函数曲而
1 . 3 . 4 数据处理和分析
import pandas as pd
columns = 'user_id', 'age、 'sex', 'occupation、 'zip_code'
df = pcLread—csv("../data/ml-100k/u.user",
delimiter」 ’|", header=None, names=columns)
print d f . h e a d ()
1 2 53 F other 94043
2 3 23 M writer 32067
3 4 24 M technician 43537
4 5 33 F other 15213
下面使州职业栏对用户数据进行分组,计算每组的平均年龄,按年龄排序之后将结果显示
Python
(0
l
mi
ws—
w
£
5»s
p
utut ll
ICMSJtuJ
IMU,
Idumi-Jsold
v
n
f
s
JB
k
J^
J
.
\
使
户
图
打
分
•nu
t/
—J
nlpi
r
L_____________
3
—J J
n
h
^ix
pall das Ji
l#
/
I
;h
1. 3 . 5 界面设计
1 . 3 . 6 图像处理和计算机视觉
O p e n C V 是一套开源的跨平台计算机视觉库,可用于开发实时的阁像处理、计算机视觉以
及模式识别程序。它 提 供 的 Python 包装模块可调用 O p e iiC V 提供的大部分功能。由于它采用
N um Py 数组表示图像,因此能很方便地与其他扩展库共享图像数据。
在下而的例子中,读入图像 moon.j p g,并转换为二值图像。找到二值图像中黑白区域相交
的边线,并计算周长和面积。然后通过这两个参数计算71。
import cv2
img = cv2.imread("moon.jpg", cv2.IMREAD— GRAYSCALE)
Python
contour = c v2.approxPolyDP(contour[0], e p s ilon=2> closed=False)
科学计算环境的安装与简介
area = cv2.contourArea(contour)
penimetGr**2 / (4 * area)
3.176088313869952
1 . 3 . 7 提高运算速度
Python 的动态特性虽然方便了程序的幵发,
但也会极大地降低程序的运行速度。
使用 Cython
可以将添加了类型声明的Python 程序编译成 C 语言源代码,再编译成扩展模块,从而提高程序
的运算速度。使 用 Cython 既能实现 C 语言的运算速度,也能使用 Python 的所有动态特性,极
大地方便了扩展库的编写。
下面是按照前面介绍的公式使用循环计算tt的源程序,使 用 o l e f 关键字定义变量的类型,
从而提高程序的运行效率:
%%cython
import cython
@cython.cdivision(True)
def calc_pi(int n ) :
cdef double pi = 0
cdef int i
for i in r a n g e (1, n, 4):
pi += 4.0 / i
for i in r a n g e (3, n, 4):
pi -= 4.0 / i
return pi
Python 科学计算(第 2 版)
调 用 calc_pi()来 计 的 近 似 值 :
calc_pi(1000000)
3.141590653589821
n = 1000000
%timeit calc_pi(n)
import numpy
numpy._ version
2.1 ndarray 对象
与本节内容对应的Notebook 为: 02-numpy/numpy-100-ndairay.ipynb〇
本书的示例程序假设用以下推荐的方式导入N um Py 函数库:
import numpy as np
高效地存储大量的数值元素,从而提高数组计兑的运兑速度,还能用它与各种扩展库进行数据
交换。本节的内容可能会有些枯燥,但是为了打下一个SL好的基础,让我们从深入理解ndairay
对象开始学习 Python 科学计算之旅。
2 . 1 . 1 创建
a = np.array([lj 2, 3, 4])
b = np.array((5J 6, 7, 8))
c = np^arrayGCl, 2, 3, 4], [4, 5, 6, 7], [7, 8, 9, 10]])
b c
[5, 6, 7, 8] [[ 1, 2, 3, 4],
[4, 5, 6, 7],
[7, 8, 9, 10]]
Numpy快
数组的形状可以通过其s h a p e 屈性获得,它是一个描述数组各个轴的长度的元组(tuple):
—速 处 理 数 据
数组 a 的 shape 属性只有一个元素,因此它是一维数组。
而数组 c 的 shape 属性有两个元素,
因此它是二维数组,其中第0 轴的长度为3 , 第 1轴的长度为4。还可以通过修改数组的shape
屈性,在保持数组元素个数不变的情况下,
改变数组每个轴的长度。
下面的例子将数组 c 的 shape
屈性改为(4,3),注意从(3,4)改为(4,3)并不是对数组进行转置,而只是改变每个轴的大小,数组元
素在内存中的位置并没有改变:
c.shape = 4, 3
array([[ 1, 2, 3],
[ 4, 4, 5],
[ 6, 7, 7],
[ 8, 9, 10]])
c.shape = 2 , -1
array([[ 1, 2} 3, 4, 4, 5],
[ 6, 7, 7, 8, 9, 10]])
使用数组的 reshapeO方法,可以创建指定形状的新数组,而原数组的形状保持不变:
数 组 a 和 d 其实共享数椐存储空间,因此修改其中任怠一个数组的元素都会同时修改另一
个数组的内容。注意在下而的例子中,数 组 d 中的2 也被改成了 100:
[ 1, 100, 3, 4] [[ 1, 100],
[ 3, 4]]
NumPy快
2 . 1 . 2 元素类型
数组的元素类型可以通过 d ty p e 属性获得。在前而的例子中,创建数组所用的序列的元
—速 处 理 数 据
素都是整数,因此所创建的数组的元素类型是整型,并且是32位的长整型。这是因为笔者所
使 用 的 Python 是 32位的,如果使用6 4 位的操作系统和 Python , 那么默认整数类型的长度为
64位。
c.dtype
dtype('int32')
完整的类型列表可以通过下面的语句得到,
它 将 typeDict字典中所有的值转换为一个集合,
从而去除其中的重复项:
set(np.typeDict.values())
{numpy.boolj numpy. object 」 numpy.string 一, n u m p y •Unicode
得与其对应的数值类型:
c.dtype.type
numpy.int32
通 过 N um Py 的数值类型也可以创建数值对象,下面创建一个16位的符号整数对象,它与
Python 的整数对象不同的是,它的収值范围有限,因此计算200*200会溢出,得到一个负数,
这一点与 C 语 言 的 16位整数的结果相同:
-25536
vl = 3.14
v2 = np.float64(vl)
%timeit vl*vl
%timeit v2*v2
tl = np.array([l, 2 , 3, A] , dtype=np.float)
t2 = n p . a r r a y ([1, 2, 3, 4], d t y p e = n p .c o m p l e x )
t3 = tl.astype(np.int32)
t 4 = t2.astype(np.complex64)
2 . 1 . 3 自动生成数组
数组,注意所得到的结果中不包含终值。例如下面的程序创建幵始值为0 、终 值 为 1、步长为
0.1的等差数组,注意终值1不在数组中:
Numpy快
np.arange(0, 1, 0.1)
—速 处 理 数 据
array([ 0. , 0.7,
linspaceO通过指定幵始值、终值和元素个数来创建表示等差数列的一维数组,可以通过
endpoint参数指定是否包含终值,默认值为 Tm e ,即包含终值。下而两个例子分别演示了 endpoint
为 True 和 False 时的结果,注 意 endpoint的值会改变数组的等差步长:
logspace()和 linspace()类似,
不过它所创建的数组是等比数列。
下 面 的 例 子 产 生 从 到 102、
有 5 个元素的等比数列,注意起始值0 表示10(>,而 终 值 2 表 示 102:
np.logspace(0, 2, 5)
雄数可以通过base 参数指定,
其默认值为10。
下而通过将 base 参数设置为2,并设置 endpoint
参 数 为 False , 仓ij建一个比例为21/12的等比数组,此等比数组的比值是音乐中相差半音的两个
音阶之间的频率比值,因此可以用它计算一个八度中所有半音的频率:
zeros〇、ones()、empty〇可以创建指定形状和类型的数组。其 中 empty〇只分亂i数组所使用的
内存,不对数组元素进行初始化操作,因此它的运行速度是最快的。下面的程序创建一个形状
为(2,3)、元素类型为整数的数组,注意其中的元素值没有被初始化:
np.empty((2_j3), np.int)
而 zemsO将数组元素初始化为0, onesO将数组元素初始化为1。下而创建一个长度为4 、
元素类型为整数的一维数组,并且元素全部被初始化为0:
n p . z e r o s (4, np.int)
Numpy快
array([0, d, d, 0])
M l 〇将数组元素初始化为指足的值:
—速 处 理 数 据
np.full(4, np.pi)
s = "abcdefgh"
Python 的字符串实际上是一个字节序列,每个字符占一个字节。因此如果从字符串 s 创建
一 个 8 位的整数数组,所得到的数组正好就是字符串中每个字符的A S C II 编码:
n p .fromstring(sJ dtype=np.int8)
array([ 97, 98, 99, 100, 101, 102, 103, 104], dtype=int8)
如果从字符串 s 创 建 16位的整数数组,那么两个相邻的字节就表示一个整数,把 字 节 98
和字节9 7 当作一•个16位的整数,它的值就是98*256+97 = 25185。可以看出,16位的整数是以
低位字节在前(little-endian)的方式保存在内存中的。
print 98*256+97
np.fromstring(s., dtype=np.intl6)
25185
np.fromstring(s, dtype=np.float)
a r r a y ([ 8 . 5 4 0 8 8 3 22e+194])
buf[l] = 10
Numpy快
1 buf = n p .frombuffen(s<) dtype=np.intl6)
— > 2 buf[l] = 10
—速 处 理 数 据
ValueError: assignment destination is read-only
我们会介绍如何使用这些对象实现动态数组的功能。
还可以先定义一个从下标计算数值的函数,然后用 fVomfunctionO通过此函数创建数组:
def func(i):
return i % 4 + 1
arnay([ 1,, 2., 3., A., 1., 2,, l., A,, 1., 2.])
fromfunctionO的第一个参数是计算每个数组元素的函数,第二个参数指定数组的形状。因
为它支持多维数组,所以第二个参数必须是一个序列。上例中第二个参数是长度为1 的元组
(10,
),因此创建了一个有10个元素的一维数组。
下面的例子创建一个表示九九乘法表的二维数组,输出的数组 a 中的每个元素a [i,
j ]都等于
func2(i,
j ):
def func2(i, j ) :
return (i + 1) * (j + 1)
n p .fromfunction(func2, (9,9))
[ 3 ., 6 ., 9 ., 1 2 ., 1 5 ., 18。 2 1 ., 2 4 ., 2 7 .],
[ 4 ., 8 ., 1 2 ., 1 6 ., 2 0 .j 2 4 ., 2 8 ., 3 2 ., 3 6 .],
[ 6 ., 1 2 ., 1 8 ., 2 4 ., 3 0 ., 3 6 ., 4 2 ., 4 8 ., 5 4 .],
[ 7 ., 1 4 ., 2 1 ., 28” 3 5 ., 4 2 ., 4 9 ., 5 6 ., 6 3 .],
[ 8 ., 1 6 ., 2 4 ., 3 2 ., 4 0 ., 48。 5 6 ., 6 4 ., 7 2 .],
2 . 1 . 4 存取兀素
可以使用和列表相同的方式对数组的元素进行存取:
a = np.arange(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
• a [5]: 用整数作为下标可以获取数组中的某个元素。
Numpy快
• a [:-l ]: 下标可以使用负数,表示从数组最后往前数。
• a [h -l :2 1 : 切片中的第三个参数表示步长,2 表示隔一个元素取一个元素。
• a 卜 11:省略切片的开始下标和结束下标,步长为- 1 , 整个数组头尾颠倒。
• a [5:l >2]: 步长为负数时,开始下标必须大于结束下标。
下标还可以用来修改元素的值:
和列表不同的是,通过切片获取的新的数组是原始数组的一个视阁。它与原始数组共享同
一块数椐存储空间。下而的程序将 b 的第2 个元素修改为-10, a 的 第 5 个元素也同时被修改为
-10,因为它们在内存中的地址相同。
40
b = a[3:7] # 通过切片产生一个新的数组b, b 和 e3 共享同一块数据存储空间
b[2] = -10 # 将 b 的第2 个元素修改为-10
b a
除了使用切片下标存取元素之外,N um Py 还提供了整数列表、整数数组和布尔数组等儿种
高级下标存取方法。
当使用整数列表对数组元素进行存取时,将使用列表中的每个元素作为下标。使用列表作
为下标得到的数组不和原始数组共享数据:
x = np.arange(10, 1, -1)
x
array([10, 9, 8, 7, 6, 5, 4, 3, 2])
NumPy快
• x [[3,3,-3,8]]:下标可以是负数,-3表示取倒数第3 个元素(从1开始计数)。
a = x[[3, 3,
—速 处 理 数 据
b = x[[3, 3,
[7, 7, 9, 2] [7, 7, 4, 2]
b[2] = 100
b x
7, 7, 100, 2] [10, 9, 8, 7, 6, 5, 4, 3, 2]
整数序列下标也可以用来修改元素的值:
当使用整数数组作为数组下标时,将得到•个形状和下标数组相同的新数组,新数组的每
个元素都是用下标数组中对应位置的值作为下标从原数组获得的值。当下标数组是一维数组时,
结果和用列表作为下标的结果相同:
x = n p . a n a n ge(10jl,-1)
x[np.array([3,3,l,8])]
41
Python 科学计算(第 2 版)
array([7, 7, 9, 2])
而当下标是多维数组时,得到的也是多维数组:
x[np.array([[3,3,l,8],[3,3,-3,8]])]
array([[7, 7, 9, 2],
[7, 7, 4, 2]])
可以将上述操作理解为:先将下标数组展平为一维数组,并作为下标获得一个新的一•维数
组,然后将其形状修改为下标数组的形状:
x[[3,3,l,8,3,3,-3,8]].reshape(2,4) # 改变数组形状
array([[7, 7, 9, 2],
[7土7, 4, 2]])
于布尔数组,不能使用布尔列表。
—速 处 理 数 据
x = n p . a r a n g e( 5 J0 J -1)
x
array([5, 4, 3, 2, 1])
array([5, 3])
array([4, 5, 4, 5, 5])
在 NumPy 1.10之后的版本中,布尔列表会被当作布尔数组,因此上面的运行结果会变
A 成 army([5,3])。
布尔数组的长度不够时,不够的部分都当作 False:
arnay([5, 3, 2])
布尔数组的下标也可以丨lj 来修改元素:
x[np.array([True, False, True, True])] = -1, -2, -3
NumPy快
—速 处 理 数 据
多维数组的存取和一维数组类似,因为多维数组有多个轴,所以它的下标需要用多个值来
表示。N um Py 采用元组作为数组的下标,元组中的每个元素和数组的每个轴对应。图 2 - 1 ® 示
了一个 shape 为(6,6)的数组 a ,图中用不N 颜色和线型标出各个下标所对应的选择区域。
第1轴
a [0,3:5] 0 1 2 3 4 5
pM
M 10 11 12 13 14 15
a [4:,4:] n n
| < —
■ •_ _ • 1
20
21 22 23 2 4 25
f s ss
30 31 32 33 34 35
a [:,2]
\ ft
40 42 44 45 1
ir-------------------- 41 1 43 1 |
a [2::2,::2] 52 54
r 50 51 5 3 ! • 55
_ i1
图2-1 使用数组切片语法访问多维数组中的元素
为什么使用元组作为下标
Python 的下标语法(用[】
存取序列中的元素)本身并不支持多维,但是可以使用任何对象作为
下标,因 此 N u m P y 使用元组作为下标存取数组中的元素,使用元组可以很方便地表示多个轴
的下标。虽然在 Python 程序中经常用圆括号将元组的元素括起来,但其实元组的语法只需要用
逗号隔开元素即可,例 如 x, y = y, x 就是用元组叉换变量值的一个例子。因此 a[l ,
2]和 a[(l ,
2)]冗全
相同,都是使用元组(1,
2)作为数组a 的下标。
Python 科学计算(第2 版)
读 者 也 许 会 对 如 何 创 建 图 中 的 二 维 数 组 感 到 好 奇 。它 实 际 上 是 一 个 加 法 表 ,由纵向量(0,10,
20,30,40,50)和 横 向 量 (0, 1,2,3,4,5)的 元 素 相 加 而 得 。可 以 用 下 面 的 语 句 创 建 它 ,至 于 其 原 现 ,
将在后面的章节进行讨论。
5 55 5 5 5
3 3 3 3 3 3夕
a = np.anange(0, 60, 1 0 ) .reshape(-1, 1) + np.arange(0, 6)
5 4 3
5 4 3
4 4 4
54 3
5 4 3夕
11
a
111
1r
3
4
02 1
52 1
/
22 1
r L
T J
V
J
2 1
2 1 ,
L
ay
3 2 1
•
N
0
2
r
TJ
夕
L
*
\
0
2
r
TJ
L
*
,
\
4
0 n0
2
r L
TJ
J
*
,
\
,
2
r L
u
J
J
*
\
,—I I
L
J
0
4
'
2
n
r L
J夕
T J
,
*
,
\
,
阁 2-1中 的 下 标 都 是 有 两 个 元 素 的 元 组 ,其 中 的 第 0 个 元 素 与 数 组 的 第 0 轴(纵轴)对 应 ,而
第 1 个 元 素 与 数 组 的 第 1 轴(横轴)对 应 。下 而 是 图 中 各 种 多 维 数 组 切 片 的 运 算 结 果 :
Numpy快
[3, 4] [[44, 45], [ 2, 12, 22, 32, 42, 52] [[20, 22, 24],
—速 处 理 数 据
如 果 K 标元组中只包含整数和切片,那么得到的数组和原始数组共享数据,它是原数组的
视图。下面的例子中,数 组 b 是 a 的视图,它们共享数据,因此修改 b[0]时,数 组 a 中对应的
元素也被修改:
b = a[0, 3:5]
b[0] = -b[0]
a[0, 3:5]
arnay([-3, 4])
因 为 数 组 的 下 标 萣 一 个 元 组 ,所 以 我 们 可 以 将 下 标 元 组 保 存 起 来 ,用 同 一 个 元 组 存 取 多 个
数 组 。在 下 而 的 例 子 中 ,a[idx]和 a[::2,2:]相 同 ,a[idx][idx]和 a[::2,2:][::2»2:]相 同 。
a[idx] a[idx][idx]
切片(slice)对象
根 据 Python 的语法,在 u 中可以使用以冒号隔开的两个或三个整数表示切片,但是单独生
成切片对象时需要使用 slice()来创建。它有三个参数,分别为开始值、结束值和间隔步长,当
这些值需要省略时可以使用1^01^。例如,3[51^(1^011^,
1^0116,1^01^),
2』和3[:,
2]相同。
np.s 」:
:2_» 2:]
3_为什么不是函数
根 据 Python 的语法,只有在中括号□中才能使用以冒号隔开的切片语法,如 果 s j 函数,
那么这些切片必须使用slice〇创建。类似的对象还有 mgrid 和 ogrid 等,后面我们会学习它们的
用法。Python 的下标语法实际上会调用_ getitem_ ()方法,因此我们可以很容易自己实现s_对象
的功能:
class S(object):
Numpy快
def _ getitem_ (self, i n d e x ) :
return index
—速 处 理 数 据
在多维数组的下标元组中,也可以使用整数元组或列表、整数数组和布尔数组,如 阁 2-2
所示。当下标中使用这些对象时,所获得的数椐是原始数据的副木,因此修改结果数组不会改
变原始数组。
第1轴
a [(0,l ,2,3〉
,(l ,2,3,4〉
】 e 0 0 3 4 5
10 11 回 13 14 15
f --------------------
3(3:, [0,2,5]]
w mm mm 20 21 回 > j 24 25
• np.drray([l ,0>l fe (e (l ], 〇 * " l i _ i
dtype«np.bool) l 3 e i 31 i 32, 33 B4 ,35
I i i i i 1
a [mask, 2] ,40 41 43 44 ,45
1 1 i42丨
1J - 1
► i t 0 , 51 回53 S4 j 55
图2 - 2 使用整数序列和布尔数组访问多维数组中的元素
在3[(0,
1,
2,3),
(1,
2,3,4)]中,下标仍然是一个有两个元素的元组,元组中的每个元素都是一个
整数元组,分别对应数组的第0 轴 和 第 1 轴。从两个序列的对应位置取出两个整数组成下标,
于是得到的结果是:a[0,
l ]、a[l ,
2]、a[2,3]、a[3,4]。
a[(0,l,2,3),(l,2,3,4)]
array([ 1, 12, 23, 34])
Python 科学计算(第2 版)
a[3:, [0,2,5]]
mask = n p . a r r a y ([1,0 ,
1,0,0)1], dtype=np.bool)
a[mask, 2]
mask2 = [True,False^True,False,False^True]
a[maskl_» 2] a[mask2_» 2]
当下标的长度小于数组的维数时,剩余的各轴所对应的下标是“:”,即选取它们的所有
数据:
a [ [ l , 2 ] , :] a[[l,2]]
[[10 ,11, 12, 13, 14, 15], [[10, 11, 12, 13, 14, 15],
[20, 21, 22, 23, 24, 25]] [20, 21, 22, 23, 24, 25]]
当所有轴都用形状相冋的整数数组作为下标时,得到的数组和 K标数组的形状相同:
x = np.array([[0,l],[2,3]])
y = np.array([[-l,-2],[-3,-4]])
a[x,y]
array([[ 5, 14],
[23, 32]])
效果和下面的程序相同:
a[(0,l,2,3),(-l,-2,-3,-4)].reshape(2,2)
array([[ 5, 14],
[23, 3 2 ]])
当没有指定第1轴的下标时,使 用 “:”作为下标,因此得到了一个三维数组:
a[x]
可以使用这种以整数数组作为下标的方式快速替换数组中的毎个元素,例如有一个表示索
引阁像的数组 image, 以及一个调色板数组palette, 则 palettefimage〗
可以得到通过调色板着色之
后的彩色阁像:
palette = n p . a r r a y ( [ [0,0,0],
[255,0,0],
NumPy快
[0,255,0],
[0,0,255],
—速 处 理 数 据
[255,255,255]])
image = n p . a r r a y ( [ [ 0, 1^ 2, 0 ],
[0, 3, 4, 0 ] ] )
palette[image]
array([[[ 0, 0, 0],
[255, 0, 0],
[0 , 255, 0],
[0, 0, 0]],
[[0, 0, 0],
[0 , 0, 255],
[255, 255, 255],
[0, 0, 0]]])
2 . 1 . 6 结构数组
在 C 语言中我们可以通过struct关键字定义结构类型,
结构中的字段占据连续的内存空间。
类型相同的两个结构所占用的内荐大小相同,因此可以很容易定义结构数组。和 C 语言一样,
在 N um Py 中也很容易对这种结构数组进行操作。只 要 N um Py 中的结构定义和C 语言中的结构
定义相同,就可以很方便地读取 C 语言的结构数组的二进制数掘,将其转换为 N um P y 的结构
数组。
假设我们需要定义一个结构数组,它的每个元素都有 name、a g e 和 weight 字段。在 NumPy
中可以如下定义:
Python 科学计算(第2 版)
persontype = np.dtype({ O
■names’ :
[ ’name', 'a g e ', 'weight'],
•formats.:[.S30., ,
i., 呷1]}, align=True)
a = np.array([("Zhang", 32, 75.5), ("Wang", 24, 65.2)], ©
dty p e = p e rs o n t y p e )
〇我们先创建一个dtype 对象 persontype,
它的参数是一个描述结构类型的各个字段的字典。
字典有两个键:’
names^ nTormats、 每个键对应的值都是一个列表。hame^定义结构中每个字段
的名称,而Tormats侧定义每个字段的类型。这里我们使用类型字符串定义字段类型:
• S 30’
:长 度 为 3 0 个字节的字符串类型,由于结构中的每个元素的大小必须同定,因此
需要指定字符串的长度。
• T : 32位的整数类型,相当于 np.int32。
• 32位的单精度浮点数类型,相当于 np.float32。
© 然后调用 airayO 以创連数红L,
通过 dtype 参数指定所创連的数纟J1的元素类型为persontype。
下面查看数组 a 的元素类型:
Numpy快
a .dtype
•offsets':[0,32,36], 'itemsize’ :
40}, align=True)
还可以用包含多个元纟11的列表来描述结构的类型:
其中形如“(字段名,
类型描述)”的元组描述了结构中的每个字段。类型字符串前面的丫、'<’
、
V 等字符表示字段值的字节顺序:
• h 忽视字节顺序。
• < : 低位字节在前,即小端模式(little endian)。
• > : ? 这位字节在前, 大端模式(big endian)。
结构数组的荐取方式和一般数组相同,通过下标能够取得其中的元素,注怠元素的值看上
去像是元组,实际上是结构:
print a [0]
a [ 0 ] .dtype
我们可以使用字段名作为下标获取对应的字段值:
a [0]["name"]
'Zhang'
a[0]是-个结构元素,它 和 数 组 a 共享内存数据,因此可以通过修改它的子段来改变原始
数组中对应元素的字段:
c = a[l]
c["name"] = "Li"
a[l]["name"]
•Li’
我们不但可以获得结构元素的某个字段,而且可以直接获得结构数组的字段,返回的是原
始数组的视图,因此可以通过修改b [0]来改变 a[0]["age"]:
b=a["age"]
b[0] = 40
print a[0]["age"]
40
Numpy快
a . t o f i l e ( "test.bin")
—速 处 理 数 据
利用下而的 C 语言程序可以将 test.bin文件中的数据读取出来。 为 I P y t h o n 的魔法命
令 ,它将该申.元格中的文本保存成文件read_stnict_array.c:
%%file rea d _ s t r uc t _ a n r a y .c
#include <stdio.h>
struct person
{
char n a m e [30];
int age;
float weight;
};
void main ()
{
FILE * f p ;
int i;
fp=fopen("test.bin","rb");
f r e a d ( p J sizeof(struct person), 2, fp);
fclose(fp);
for(i=0;i<2;i++)
Python 科学计算(第2 版)
Zhang 4 0 75.500000
Li 24 65.199997
内存对齐
为了内存寻址方便,C 语言的结构类型会自动添加一些填充用的字节,这叫做内存对齐。
例如上面 C 语言中定义的结构的name 字段虽然是30个字节长,
但是由于内存对齐问题,
在 name
和 a g e 中间会填补两个字节。因此,如果数组中所配置的内存大小不符合C 语言的对齐规范,
Numpy快
结构类型中可以包括其他的结构类型,下面的语句创建一个有一个字段f l 的结构,f l 的值
是另一个结构,它有字段12,类 型 为 16位整数:
dtype([( ;
fr, [(*f2*, *0 2 ^)])])
当某个字段类型为数组时,用元组的第三个元素表示其形状。在下面的结构体中,f l 字段
是一个形状为(2,3)的双精度浮点数组:
dtype([Cf0_ , ,
<i4,
),(' f l 1, ' f S 1, (2, 3))])
用下面的字典参数也可以定义结构类型,字典的键为结构的字段名,值为字段的类型描述。
但是由于字典的键是没有顺序的,冈此字段的顺序需要在类型描述中给出。类型描述楚一个元
组,它的第二个值给出字段的以字节为单位的偏移S ,例如下例中的 a g e 字段的偏移M 为 25个
字节:
2 . 1 . 7 内存结构
下面让我们看看数组对象是如何在内存中存储的。如 图 2-3所示,数组的描述信息保存在
一个数据结构中,这个结构引用两个对象:用于保存数据的存储区域和用于描述元素类型的
dtype 对象。
ndarray数据结构
f l o a t 3 2 描述数组的元素类型
l
dtype • Q -------- f lo a t 3 2
dia count 2
dimensions 3 3
4 字节 数据存储区域
strides 12 4
/ A N
d»t«i • d>— 0 1 2 3 4 5 6 7 8
>
12字节
图 2-3 ndarray数组对象在内存屮的存储方式
Numpy快
数据存储区域保存着数组中所有元素的二进制数据,d ty p e 对象则知道如何将元素的二进
制数据转换为可用的值。数组的维数和形状等信息都保存在ndamiy 数组对象的数据结构中。图
2-3中显示的是下面的数组a 的内存结构:
—速 处 理 数 据
a = n p . a r n a y ( [ [0,1,2],[3,4,5],[6,7,8]], dtype=np.float32)
数组对象使用 strides屈性保存每个轴上相邻两个元素的地址差,即当某个轴的下标增加1
时,数据存储区中的指针所増加的字节数。例 如 图 2-3中 的 strides为(12,4),即 第 0 轴的下标增
加 1 时,数据的地址增加12个字节。也就是 aU ,
〇j 的地址比 a [0,0j 的地址大1 2 , 正好是3 个单
精度浮点数的总字节数。第 1轴的下标增加1时,数据的地址增加4 个字节,正好是一个单精
度浮点数的字节数。
如 果 strides属性中的数值正好和对应轴所占据的字节数相同,那么数据在内存中是连续存
储的。通过切片下标得到的新数组是原始数组的视阁,即它和原始数组共享数椐存储区域,但
是新数组的 strides属性会发生变化:
b = a [ ::2, ::2]
b b.strides
[ 6。 8.]]
维数组的第0 轴是最上位的,
即第0 轴的下标增加1时,
元素的地址增加的字节数最多;而Fortran
语言中的多维数组的第0 轴是最下位的,即 第 0 轴的下标增加1 时,地址只增加一个元素的字
节数。在 N um Py 中默认以 C 语言格式存储数据,如果希望改为 Fortran格式,只需要在创建数
组时,设 置 order参数为MF ":
c.strides
(4, 12)
了解了数组的内存结构,就可以解释使用下标収得数据时的复制和引用间题.•
• 当 下 标 使 W 整数和切片时,所取得的数据在数据存储区域中是等间隔分布的。因为只
需要修改图2-3所示的数据结构中的dim count、dimensions、stride等属性以及指向数据
存储区域的指针 data, 就能实现整数和切片 K 标,所以新数组和原始数组能够共享数据
存储区域。
* 当使用整数序列、整数数组和布尔数组时,不能保证所取得的数椐在数据存储区域中
Numpy快
是等间隔的,因此无法和原始数组共享数裾,只能对数据进行复制。
数 组 的 flags 属性描述了数据存储E 域的一些屈性,直接查看 flags 属性将输出各个标志的
—速 处 理 数 据
值 ,也可以单独获得其中的某个标志值:
print a.flags
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
c _ c o n t i g u o u s : True
下面是几个比较重要的标志:
• C _C 0 N T 1GU 0 U S : 数据存储区域是否是C 语言格式的连续区域。
• F_CON TIGU OUS : 数据存储区域适否是Fortran语言格式的连续区域。
• O W N D A T A : 数组是否拥有此数据存储区域,当一个数组萣其他数组的视图时,它不
拥有数据存储区域。
由于数组 a 是 通 过 amiy()直接创建的,因此它的数椐荐储区域是C 语言格式的连续区域,
并且它拥有数据存储K 域 。下面我们看看数组 a 的转置标志,数组的转置可以通过其 T 属性
获得,转置数组将其数据存储K 域 看 作 Fortnur语言格式的连续K 域 ,并且它不拥有数据存储
区域。
a.T.flags
C_CONTIGUOUS : False
F_CONTIGUOUS : True
OWNDATA : False
WRITEABLE : True
A LIGNED : True
UPDATEIFCOPY : False
下面查看数组 b 的标志,它不拥有数据存储区域,其数据也不是连续符储的。通过视图数
组 的 base 属性可以获得保存数据的原始数组:
b.flags
C_CONTIGUOUS : False
F_CONTIGUOUS : False
OWN D A T A : False
WRITEABLE : True
A LIGNED : True
UPDATEIFCOPY : False
Numpy快
id(b.base) id(a)
—速 处 理 数 据
119627272 119627272
b = a.view(np.uint32)
c = a.view(np.uint8)
b c
a[0, 0] = 3.14
float 〇
Lrsqrt( float number )
/
i
long i;
float y;
x2 = number * 0.5F;
Numpy快
y = number;
i = * ( long * ) &y; / / 对浮点数的邪恶位级hack
—速 处 理 数 据
i = 0x5f3759df - ( i » 1 ); / / 这到底是怎么回事?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x 2 * y * y ) ); / / 第一次牛顿迭代
return y;
下而我们用 N u m P y 实现同样的计算:
y = y * (1.5 - x2 * y * y) O
0.0050456140410597428
O 丨
:t ]于 l i n s p a c e ()创述的数纟J1的类型为双精度浮点数,因此这里首先通过a s t y p e ()方法将J l :转
换成单精度浮点数数组y 。© 通 过 v i e w ()方法创建一个与 y 共享内存的32位整数数组 i 。€)对整
数数组 i 进行那段完全摸不着头脑的运算,
并且将结果重新写入数组i 中。1±1于i 和 y 共享内存,
此 时 y 中的值也发生了变化。注意这里的赋值不能使用 i = 0x 5B 759d f _ (i » 1),如果这样写,
那么数组 i 就是一个全新的数组了。O 进行一次牛顿迭代运算,这里由于使用 y = ...的写法,因
此 y 将变成一个全新的数组,和原来的 i 不再共享内荐。在这段代码中有很多数组运算,关于
这方而的内容将在下一节进行详细说明。© 最后输出奥实值和近似值之间的最大误差。图 2 4
品示了绝对误差与变量的关系,当 number很小吋绝对误差较大,但此时的函数值也较大,
因此相对误差的变化并不大。
图 2~4《
雷祌之锤》中计筇平方根倒数算法的绝对误差
Numpy快
除了使用切片从同一块数据K 创建不同的 shape和 strides的数组对象之外,还可以直接设
置这些屈性,从而得到用切片实现不了的效果,例如:
—速 处 理 数 据
from numpy.lib.stride_tricks import as_strided
a = np.anange(6)
b = as 一strided(a, shape=(4, 3), strides=(4, 4))
a b
[1, 2, 3],
[2, 3, 4],
[3, 4, 5]]
a[2] = 20
array([[ 0, 1, 20],
在对数据进行处理时,可能经常耑要对数据进行分块处理,而且为了保持平滑,每块数据
之间需要有一定的重叠部分。这时可以使用上面介绍的方法对数据进行带重叠的分块。需要注
Python 科学计算(第2 版)
2.2 ufunc 函数
y = np.sin(x)
Numpy快
y
array([ 0.00000000e+00, 6.42787610e-01, 9.84807753e-01,
—速 处 理 数 据
-2.44929360e-16])
先用丨impaceO产生一个从0 到2 tt的等差数组,
然后将其传递给叩.如〇函数计算每个元素的
正弦值。由于 np.Sin()是一个 iiflinc 函数,因此在其内部对数组x 的每个元素进行循环,分别计
算它们的正弦值,并返回一个保荐各个计算结果的数组。运算之后数组 x 中的值并没有改变,
而是新创建了一个数组来保存结果。也可以通过 o u t 参数指定保存计算结果的数组。冈此如果
希望直接在数组 x 中保存结果,可以将它传递给 out 参数:
True
标准庵的 math.sin〇的计算速度:
import math
def sin_math(x):
for i., t in e n u m e r a t e ( x ) :
x [ i ] = m a th .s in (t)
def s i n _ n u m p y ( x ) :
np.sin(x, x)
def s i n _ n u m p y_ l o o p ( x ) :
for i, t in e n u m e r a t e ( x ) :
x[i] = np.sin(t)
xl = x[:]
%time sin_math(x)
xa = np.array(x)
%time sin_numpy(xa)
xl = x[:]
Numpy快
%time sin— numpy 一loop(x)
—速 处 理 数 据
Wall time: 30 ms
列表推导式比循环更快
事实上,标 准 Python 中有比 fo r 循环更快的方案:使用列表推导式 x = [math.sin⑴ for t in x ]。
但是列表推导式将产生一个新的列表,而不是直接修改原列表中的元素。
np .sin〇同样也支持计算单个数值的正弦值。不过值得注意的楚,对单个数值的计算,
math.sin〇则比 np.sin〇快很多。
在 Python 级别进行循环时,
np.sin〇的计算速度只有math.sin〇的 1/6。
这是因为:np.Sin〇为了同时支持数组和单个数值的计算,其 C 语言的内部实现要比math.sin〇复杂
很多。此外,对于中.个数值的计算,np.sin()的返回值类型和math.sin()的小同,math.sin()返回的
足 Python 的标准 float 类型,而 np.sin()返 回 float64类型:
t y p e ( m a t h .sin(0.5)) type(np.sin(0.5))
float numpy.float64
a = np.arange(6.0).reshape(2, 3)
2 . 2 . 1 四则运算
a = np.arange(0j 4)
b = np.arange(lj 5)
np.add(aj b)
array([l, 3, 5, 7])
Numpy快
add()返冋一个数组,它的每个元素都是两个参数数组的对应元素之和。如果没有指定 out
参数,那么它将创建一个新的数组来保存计算结果。如果指定了第三个参数 out, 则不产生新
—速 处 理 数 据
的数组,而直接将结果保存进指定的数组。
np.add(a, a)
a
array([l, 3, 5, 7])
NumPy为数组定义了各种数学运兑操作符,
因此计算两个数组相加可以简单地写为a + b ,
而叩.add(a,b,
a)则可以川 a += b 来表示。
表 2-1列出了数组的运算符以及与之对应的uftmc函数,
注意除号的意义根据是否激活_ future_ .division有所不同。
表 2-1 数组的运算符以及对应的ufunc函数
t = a * b
x = t + c
del t
也就是说,需要产生…个临时数组 t 来保存乘法的运算结果,然后再产生最后的结果数组
X。 可以将兑式分解为下面的两行语句,以减少一次内存分配:
x = a*b
x += c
2 . 2 . 2 比较运算和布尔运算
使用= 、>等比较运算符对两个数组进行比较,将返回一个布尔数组,它的每个元素值都
Numpy快
是两个数组对应元素的比较结果。例如:
—速 处 理 数 据
np.array([lj 2, 3]) < np.anray([3., 2, 1])
表 2 - 2 比较运算符与相应的ufunc 函数
表达式 对应的ufunc 函数
y = x 1 = x2 equal(x l, y])
x2[,
y = xl !=x2 not_equal(x l,x2 [,y])
y = xl < x2 less(x l,x2, [,y])
y = xl < = x2 less一equal(x l,x2, [,
y])
y = xl > x2 greater(x l,x2, [,y ])
y = xl > = x2 greater一equal(x l,x2, [,
yj)
下面是一个使用 l〇
gical_〇
r()进 行 “或运算”的例子:
5
9
Python 科学计算(第2 版)
a = np.arange(5)
b = np.anange(4., ~1, -1)
print a == b
print a > b
a == b and a > b
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any()
—速 处 理 数 据
or a.all()
错误信息告诉我们可以使用数组的any〇或 all()方法,
在 NumPy 1M 司吋也定义了 any()和 all〇
函数,它们的用法和 Python 内置的 any()和 all〇类似。只要数组中有一个元素值为 True , any()
就返回 True ; 而只有当数组的全部元素都为T ru e 时,all()才返回 True 。
True True
(a == b) | (a > b)
整数数组的位运算和C 语言的位运算相同,在使用时要注意元素类型的符号,例如下面的
amngeO所创建的数组的元素类型为32位符号整数,因此对正数按位取反将得到负数。以整数0
为例,按位取反的结果是OxFFWFFFF , 在 32位符号整数中,这个值表示-1。
〜 np.arange(5)
而如果对8 位无符号整数数组进行按位収反运兑:
^ np.arange(5., dtype=np.uint8)
2 . 2 . 3 自定义 ufunc 函数
NumPy快
—速 处 理 数 据
根据图2-5,我们很容易写出计算三角波上某点的Y 坐标的函数。显 然 triangle_wave 〇只能
计算单个数值,不能对数组直接进行处理。
def triangle 一
wave(x, c, c0, he):
x = x - int(x) # 三角波的周期为1 ,因此只取x 坐标的小数部分进行计算
if x >= c: r = 0.0
我们可以用下面的程序,先使用列表推导式计算出一个列表,然后用 amiyO将列表转换为
数组。这利順法每次都需要使用列表推导式语法调用函数,这对于多维数组很麻烦。
x = n p . l inspace(0> 2, 1000)
yl = np.array([triangle 一wave(t, 0.6, 0.4, 1.0) for t in x])
通过 fmmpyflmcO可以将计兑单个值的函数转换为能对数组的每个元素进行计兑的uflm c 函
Python 科学计算(第2 版)
数。frompyfunc()的调用格式为:
值得注怠的是,triangle_Uftmcl()所返回的数组的元素类型是object, 因此还需要调用数组的
astype()方法,以将其转换为双精度浮点数组:
y 2 .dtype y 2 .a s t y p e (n p .f l o a t ).dtype
dtype('0') dtype('float64')
Numpy快
使用列表可以描述多个返丨Hi数组的元素类型。下面的程序使用vectorizeO计算三角波:
最后我们验证一下结果:
True True
2 . 2 . 4 广播
当使用uftinc函数对两个数组进行计算时,ullinc函数会对这两个数组的对应元素进行计算,
因此它要求这两个数组的形状相同。如果形状不同,会进行如下广播(broadcasting)处理:
1) 让所有输入数姐都向其中维数最多的数绀看齐,
shape属性中不足的部分都通过在前面加
1补齐。
2) 输出数组的 shape属性是输入数组的 shape屈性的各个轴上的最大值。
3) 如果输入数组的某个轴的L<:度 为 1或与输出数组的对应轴的长度相同,这个数组能够用
来计算,否则出错。
4) 当输入数组的某个轴的长度为1 吋,沿着此轴运算时都用此轴上的第一组值。
上述 4 条规则理解起来可能比较费劲, K面让我们看一个实际的例子。
先创建一个二维数组a , 其形状为(6,
1):
a = np.arange(0, 60, 10).reshape(-l, 1)
a a.shape
[[0], (6, 1)
[10],
[20],
[30],
[40],
[50]]
再创建一维数组 b ,其形状为(5,):
b = np.arange(0j 5)
b b.shape
[0, 1, 2, 3, 4] (5,)
计 算 a 与 b 的和,得到一个加法表,它相当于计算两个数组中所有元素对的和,得到一个
形状为(6,5)的数组:
c = a + b
c c.shape
[ [ 0, 1, 2, 3, 4], (6, 5)
b.shape = 1 , 5
b b.shape
这样,加法运算的两个输入数组的shape 属性分別为(6,
1)和(1,
5),根据规则2),输出数组的
各个轴的长度为输入数组各个轴的长度的最大值,可知输出数组的 shape 属性为(6,5)。
由于 b 的 第 0 轴的长度为1,而 a 的 第 0 轴的长度为6 , 为了让它们在第0 轴上能够相加,
需要将 b 的第0 轴的长度扩展为6 , 这相当于:
Python 科学计算(第2 版)
b = b . r e p e a t (6, axis=0)
b b.shape
a = a . r e p e a t (5, axis=l)
a
a.shape
Numpy快
0
[
T J
0
0
濟
(6, 5)
•
•
\ *
V *
N
1
0
0
0
0
r L
T J
J
,
—速 处 理 数 据
\ *
\ *
0
0
2
2
0
0
r L
T J
夕
\ *
\ *
3
0
0
3
3
0
0
r L
T J
\
\,
4
0
0
0
0
r L
u
5
0
0
5
5
0
0
r L
u
T
J
Xj y = np.ognid[:5, :5]
x y
[ 1],
[ 2 ],
[3] ,
[4] ]
X, y = np.mgrid[:5, :5]
x y
[[0, 0, 0, 0, 0], [[0, 1, 2, 3, 4],
[1, 1, 1, 1, 1], [0, 1, 2, 3, 4],
[2, 2, 2, 2, 2], [0, 1, 2, 3, 4],
[3, 3, 3, 3, 3], [0, 1, 2, 3, 4],
[4, 七 4, 4丄 4]] [0, 1, 2, 3, 4]]
Ogrid是一个很有趣的对象,它像多维数组一样,用切片元组作为下标,返回的是一组可以
用来广播计算的数组。其切片下标有两种形式:
•开始值:结束值:步长,和 np.amnge(开始值,结朿值,
步长)类似。
•幵 始 值 :结 朿 值 :长 度 j ,当第三个参数为虚数时,它表示所返冋的数组的长度,和
np.linspace(开始值,
结朿值,
长度)类似。
x, y = np.ogrid[:l:4j, :l:3j]
x y
Numpy快
[0.33333333],
[0.66666667],
—速 处 理 数 据
[ I- ]]
利 用 Ogrid 的返回值,我们很容易计算二元函数在等间距网格上的值。下面是绘制三维曲
面f (x ,y ) = x e x2_y2 的程序:
x, y = np.ogrid[-2:2:20j, -2:2:20j]
z = x * np.exp( - x**2 - y**2)
1.0 以 〜 -1*5
15 2.0
图2 4 使用ogrid计算二元函数的曲面
P y th o n 科学计算(第2 版)
a = np.arange(4)
a [ N o n e , :] a[:, None]
[[0, 1, 2, 3]][[0],
[1],
[2],
[3]]
K 面的例子利用 N o n e 作为下标,实现广播运算:
x = n p . a r r a y Q Q ,1, 4, 10])
y = np.array([2, 3, 8])
Numpy快
x [ N o n e , :] + y[:. None]
array([[ 2, 3, 6, 12],
—速 处 理 数 据
[3, 4, 7, 13],
还可以使用 ix_〇将两个一维数组转换成可广播的二维数组:
gy, gx = np •ix 」y , x )
gx gy gx + gy
u fu n c 函数对象本身还有一些方法函数,这些方法只对两个输入、一个输出的 u f u n c 函数有
效,其他的 u f u n c 对象调用这些方法时会抛出V a l u e E r r o i •异常。
red u ce ()方法和 P y t h o n 的 r e d u c e 〇函数类似,它沿着 axis 参数指定的轴对数组进行操作,相
当于将<〇p>运兑符插入到沿 a x is 轴的所有元素之间:< o p >.r e d u c e (a r r a y ,
axis =0, d t y p e = N o n e )〇
例如:
rl = np.add.neduce([l, 2, 3]) # 1 + 2 + 3
rl r2
6 [ 6, 15]
accumulate〇方 法 和 reduce〇类似,只是它返回的数组和输入数组的形状相同,保存所有的
中间计算结果:
al = np.add.accumulate([l, 2, 3])
a2 = np.add.accumulate([[l, 2, 3], [4, 5, 6]], axis=l)
al a2
Numpy快
它的计算有些特别,让我们通过例子详细解释一下:
—速 处 理 数 据
a = np.ar r a y ([ l ,2, 3 ,4])
result
array([ 1, 2, 3, 3, 6y 4, 10])
对 于 indices参数中的每个元素都会计算出一个值,因此最终的计算结果和indices参数的长
度相同。结果数组 result中除最后一个元素之外,都按照如下计算得出:
if indices[i] < i n d i c e s [ i + 1 ] :
而最后一个元素如下计算:
<op>.r e d uc e ( a [ i n d i c e s [ -1]:])
因此在上面的例子中,数 组 insult的每个元素按照如下计算得出:
1 : a[0] -> 1
2 : a[l] -> 2
3 : a[0] + a[l] -> 1 + 2
3 : a[2] -> 3
6 : a[0] + a[l] + a[2] -> l + 2 + 3 = 6
4 : a[3] -> 4
10 a[0] + a[l] + a[21 + a[4l - > 1 + 2 + 3 + 4 = 1 0
P y th o n 科学计算(第 2 版)
a.shape += (1^)*b.ndim
<op>(a,b)
a = a.squeeze()
a m a y ( [ [ 2, 3, 4],
[4, 6, 8],
[6, 9, 12],
[8, 12, 16],
[10, 15, 20]])
可以看出通过 outerQ计算的结果是如下乘法表:
*| 2 3 4
1| 2 3 4
2| 4 6 8
3| 6 9 12
4| 8 12 16
5| 10 15 20
如果将这两个数组按照等N 程序一•步一步地进行计算,就会发现乘法表最终是通过广播的
方式计箅出来的。
2 .3 多 维 難 的 下 締 取
与本节内容对应的Notebook 为: 02-numpy/numpy-300-mulitindex.ipynb〇
在前面的介绍中,我们通过-些实例介绍了如何对多维数组进行下标访问。实际上,NumPy
提供的下标功能十分强大,在读者掌握了“广播”相关的知识之后,让我们再回过头来系统地
学习数组的下标规则。
2 . 3 . 1 下标对象
首先,多维数组的下标应该是一个长度和数组的维数相同的元组。如栗下标元组的长度比
数组的维数大,就会出错;如果小,就 会 在 下 标 元 组 的 后 而 补 ,使得它的长度勹数组维数
相同。
如果下标对象不是元组,则 N u m P y 会首先把它转换为元组。这种转换可能会和用户所希
望的不一致,因此为了避免出现问题,请 “显式”地使)lj 元组作为下标。例如数组 a 是一个三
维数组,下 面 使 —个二维列表 lid x 和二维数组 aidx 作为下标,得到的结果就不一样。
a = np.arange(3 * 4 * 5).reshape(3, 4, 5)
lidx = [[0], [1]]
aidx = np.array(lidx)
a[lidx] a[aidx]
NumPy快
[[[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29],
—速 处 理 数 据
[30, 31, 32, 33, 34],
[35, 36, 37, 38, 39]]]]
经过各种转换和添加“:”之后得到了一个标准的下标元组。它的各个元素有如下几利1类
型:切片、整数、整数数组和布尔数组。如果元素不是这些类型,如列表或元组,就将苒转换
成整数数组。
如 果 K 标元组的所有元素都是切片和整数,那么用它作为 K标得到的是原始数组的一个视
图,即它和原始数组共享数据存储空间。
P y th o n 科学计算(第 2 版)
2 . 3 . 2 整数数组作为下标
下面看看下标元组中的元素由切片和整数数组构成的情况。假设整数数组有队个,而切几
有叱个。Nt + Ns 为数纟J1的维数 D 。
首先,这\个整数数组必须满足广播条件,假设它们进行广播之后的维数为 M , 形状为
(d 〇
,d i ,•••, 〇
如 果 \ 为 0 , 即没有切片元素时,则下标所得到的结果数组result的形状和整数数组广播
之后的形状相同。它的每个元素值按照下而的公式获得:
result[i 〇, ip •••,Im—i ] - X[ind〇[i〇,ii, ••. ,iM—i], •••,indNt—i[i 〇, i i , . •• - 1]]
其中, 为进行广播之后的整数数组。让我们看一个例子,从而加深对此公
式的理解:
若只需要沿着指定轴通过整数数组获取元素,可以使用 numpy.take()函数,其运算速度
场J 比整数数组的下标运算略快,并且支持下标越界处理。
Numpy快
11 = np.array([[[0]], [[1]]])
12 = np.array([[[2, 3, 2]]])
b = a[i0, il) i2]
b
array([[[22, 43, 22],
[2, 23, 2]],
(1 , 2 , 3)
(2, 1, 1)
(1 , 1, 3)
2 2 3
b.shape
(2 , 2, 3)
我们可以使用 broadcast_airays()查看广播之后的数组:
对 于 b 中的任意一个元素b [i j ,
k ],它是数组 a 中经过 indO、in d l 和 ind2 进行卜标转换之后
的值:
i , 〕、 k = 0, 1, 2
print b[i, j, k], a[ind0[i, j, k], indl[i, ]•, k], ind2[i, j, k]]
i , 〕、 k = 1, 1, 1
print b[i, j, k], a[ind0[i, j, k], indl[i, j, k], ind2[i, j, k]]
2 2
28 28
下 面考虑 Ns 不 为 0 的情况。当存在切片下标时,情况就变得更加复杂了。可以细分为两
种情况:下标元组中的整数数组之间没有切片,即整数数组只有一个或连续的多个整数数组。
这时结果数组的 shape 属性为:将原始数组的 shape 属性中整数数组所占据的部分替换为它们广
播之后的 shape属性。
例如假设原始数纟1U 的 shape属性为(3,4,5), iO和 i l 广播之后的形状为(2,2,3),
则 a [13,
i0,i l ]的形状为(2,2,2,3):
c.shape
(2, 2, 2, 3)
ind0.shape
print c [ : ,i ,]•,k]
[21 41]
[21 41]
d = a[i0, il]
d.shape
(2, 2, 3, 4)
i , 〕、 k = 1, 1, 2
d[i,
j , k , :] a[ind0[i,
j,k],:
,indl [ i ,
j,k]]
2.3.3 —个复杂的例子
下面让我们用所学的下标存取的知识,解 决 在 N u m P y 邮件列表中提出的一个比较经典的
—速 处 理 数 据
问题。
C .各 http://mail.scipy.org/pipeiTnail/numpy-discussion/2008-July/035764.html
歹 N um Py 邮件列表中原文的链接。
我们对问题进行一些简化,提问者想要实现的下标运算是:有一个形状为(I,J,K )的三维数
组 v 和一个形状为(I,J)的二维数组 idx , id x 的每个值都是0 到 K -L 的整数。他想通过下标运算
得到一个数组 r ,对于第〇轴 和 第 1轴的每个下标 i 和 j 都满足下而的条件:
r[i,
j , :] = v [ i ,
j,idx[i,
j]:idx[i,
j]+L]
如 图 2-7所示,左图中不透明的方块是我们希望获取的部分,通过下标运算之后将得到右
侧所示的数组。
第〇$由^ ^
第2轴
££ ltd
图 2-7二维数组下标运算问题的示意图
首先创建-•个方便调试的数组V ,它在第2 轴上每一层的值就是该层的高度,即 v [:,
:,
i]的所
有的元素值都为 i 。然后随机产生数组 idx , 它的每个元素的取值都在0 到 K -L 之间:
I, J, K, L = 6j l y 8, 3
一, v = np.mg r i d [: I , : K]
] , :
idx 一k.shape
(6, 7, 3)
Numpy快
使J+j idx_ i,
idx_j ,
idx_k 对数纟11 v 进行下标运兑即可得到结果:
—速 处 理 数 据
r = v [idx _i , i d x j 、 idx 一k]
i, j = 2, 3 # 验证结果,读者可以将之修改为使用循环验证所有的元素
r[i,j,:] v[i,j,idx[i,j]:idx[i,j]+L]
[0, 1, 2] [0, 1, 2]
2 . 3 . 4 布尔数组作为下标
当使用布尔数组直接作为下标对象或者元组下标对象中有布尔数组吋,都相当于用
nonzeroO将布尔数组转换成一组整数数组,然后使用整数数组进行下标运兑。
nonzero(a)返回数纟U a 中值不为零的元素的下标,它的返回值是一个长度为a.ndim(数纟J1 a 的
轴数)的元组,元组的每个元素都是一个整数数组,其值为非零元素的下标在对应轴上的值。例
如对于一维布尔数组b l ,nonzero(a)所得到的楚一个长度为1 的元组,它表示 b l [0]和 b l [2]的值
不 为 0。
若只需要沿着指定轴通过布尔数组获取元素,可以使用 niimpy.compress〇函数。
np.nonzero(bl)
(anray([0, 2]),)
当布尔数组直接作为下标吋,相当于使用由 nonzeroO转换之后的元组作为下标对象:
a = np.arange(3 * 4 * 5 ) .reshape(3, A, 5)
a[b2] a[np.nonzeno(b2)]
当下标对象是元组,并且其中有布尔数组时,相当于将布尔数组展开为由 n〇
nzeros()转换
NumPy快
之后的各个整数数组:
—速 处 理 数 据
2 . 4 庞大的函数库
2 . 4 . 1 随机数
木节介绍的函数如表2-3所示。
表 2-3本节要介绍的函数
函数名 功能 函数名 功能
rand 0 到 1之间的随机数 randn 标准1丨•:态分布的随机数
randint 指定范围内的随机整数 normal 正态分布
(续表)
函数名 功能 函数名 功能
uniform 均匀分布 poisson 泊松分布
permutation 随机排列 shuffle 随机打乱顺序
choice 随机抽取样本 seed 设置随机数种子
mimpy.mnd〇m 模块中提供了大量的随机数相关的闲数,为了方便后面用随机数测试各利呍
算函数,让我们首先来看看如何产生随机数:
• rand()产 生 0 到 1之间的随机浮点数,它的所有参数用于指定所产生的数组的形状。
• randn()产生标准正态分布的随机数,参数的含义与 mnd〇相同。
• randintO产生指定范围的随机整数,包括起始值,但是不包括终值,在下而的例子中,
产 生 0 到 9 的随机数,它的第三个参数用于指定数组的形状:
NumPy
rl = nr.rand(4, 3)
r2 = nr.randn(4, 3)
丨快速处理数据
n3 = nr.randint(0, 10, (4, 3))
rl r2 r3
random模块提供了许多产生符合特定随机分布的随机数的函数,它们的最后一个参数 size
都用于指定输出数组的形状,而其他参数都是分布函数的参数。例如:
• nomialO: 正态分布,前两个参数分别为期望值和标准差。
• imiformO: 均匀分布,前两个参数分别为区间的起始值和终值。
• poisson〇 : 泊松分布,第一个参数指定 A 系数,它表示单位时间(或单位而积)内随机事
件的平均发生率。由于泊松分布是一个离散分布,因此它输出的数组是一个整数数组。
rl r2 r3
机排列;当参数为一个序列吋,它返冋一个随机排列之后的序列:
print n r .p e r m u t a t io n (10)
print n r .p e r m u t a t io n (a )
[ 2 4 3 5 6 8 0 1 9 7 ]
[40 1 10 20 30]
permutationO返回一个新数组,而 shuffle〇则直接将参数数组的顺序打乱:
nr.shuffle(a)
choice〇从指定的样木中随机进行抽取:
• size 参数用于指定输出数组的形状。
Numpy快
• replace 参数为 T ru e 时,
进行可重复杣取,而 为 False 吋进行不重S 杣取,
默认值为 True 。
所以在下面的例子中,c l 中可能有重复数值,而 c 2 中的每个数值都是不N 的。
—速 处 理 数 据
• p 参数指定每个元素对应的抽収概率,如果不指定,所有的元素被杣収到的概率相同。
在下面的例子中,
值越大的元素被抽到的概率越大,
因此 c 3 中数值较大的元素比较多。
cl c2 c3
为了保证每次运行时能重现相同的随机数,可 以 通 过 seed〇函数指定随机数的种子。在下
面的例子中,计 算 r3 和 1*4之前,都使用4 2 作为种子,因此得到的随机数组是相同的:
rl = nr.randint(0, 100, 3)
n2 = n n . nandint(0J 100^ 3)
n r . s e e d (42)
r3 = nr\randint(0, 100, 3)
n r . s e e d (42)
r4 = nr.randint(0, 100, 3)
rl r2 r3 r4
[84, 14, 46] [23, 20, 66] [51, 92, 14] [51, 92, 14]
2 . 4 . 2 求和、平均值、方差
本节介绍的函数如表2~4所示。
表2 W 本节要介绍的函数
函数名 功能 函数名 功能
sum 求和 mean 求期望
average 加权平均数 std 标准差
var 方差 product 连乘积
s u m ( ) 汁算数组元素之和,也可以对列表、元组等与数组类似的序列进行求和。当数组是多
NumPy
整数数组。
丨快速处理数据
n p . r a n d o m. s e e d (42)
a = n p .r a n d o m .r a n d i n t (0^I d , size=(4,5))
a np.sum(a)
TJ u
6
6
r L
J
J
96
nJ
9
4
r L
J
TJ TJ
3
2
7
5
r L
J
1
4
5
r L
J
J
当 axis 参数是一个轴的序列时,对指定的所有轴进行求和运算。例如下面的程序对一个形
状为(2,3,4)的三维数组的第0 和 第 2 轴求和,得到的结果为一个形状为(3,
)的数组。由于数组的
所有元素都为1 , 因此求和的结果都是8:
array([ 8 。 8 。 8.])
77
P y th o n 科学计算(第2 版)
[28],
[24],
[18]]
SUm〇默认使用和数组的元素类型相同的累加变M 进行计算,如果元素类型为整数,则使用
系 统 的 默 认 整 数 类 型 作 为 累 加 变 例 如 在 32位系统中使用32位整数作为累加变量。因此对
整数数组进行累加时可能会出现溢出问题,即数组元素的总和超过了累加变量的取值范围。下
面的程序计算数组 a 中每个元素占其所在行总和的百分比。在调用 sum()函数时:
• 设 置 dtype 参数为 float, 这样得到的结果是浮点数组,能避免整数的整除运算。
• 设 置 keepdims参数为 Tm e , 这 样 sum〇得到的结果的形状为(4,
1),能够和原始数组进行
广播运算。
Numpy快
pa pa . s u m ( l J keepdims=True)
对很大的单精度浮点数类型的数组进行计算时,也可能出现精度不够的现象,这时也可以
通 过 dtype 参数指定累加变f i 的类型。在下而的例子中,我们对一个元素都为1.1的单精度数组
进行求和,比较单精度累加变量和双精度累加变量的计算结果:
np.setj3nintoptions(precision=8)
b = np.full(1000000, 1.1, dtype=np.float32) # 创建一个很大的单精度浮点数数组
b # 1 . 1 无法使用浮点数精确表示,存在一些误差
使用单精度累加变M 进行累加计算,误差将越来越人,而使用双精度浮点数则能够得到较
精确的结果:
1099999.3 1100000.0238418579
上面的例子将产生一•个新的数组来保存求和的结果,如果希望将结果直接保存到另一个
数组中,可 以 和 uftinc 函数一•样使用o u t 参数指定输出数组,它的形状必须和结果数组的形状
相同。
mean〇求数组的平均值,它的参数与 sum()相同。和 sum〇不同的是:对于整数数组它使JIJ
双精度浮点数进行计算,而对于其他类型的数组,则使用和数组元素类型相同的累加变董进行
计算:
1.0999993 1.1000000238418579
78.6153846154
相当于进行如下计算:
78.6153846154
而无偏样木方差的公式为:
S2 > (Yi - y )2
下面我们用程序演示这两种方差的差別。
首先产生-•个标准差为2.0、方差为4.0的正态分布的随机数组。我们可以认为总体样本的
方差为4.0。假设从总体样本中随机抽取10个样本,我们分別计算这10个样本的两种方差,这
里我们用一个二维数组重复上述操作1〇_次,然后计算所有这些方差的期望值:
np.mean(vl) np.mean(v2)
3.6008566906846693 4.0009518785385216
可以看到无偏样本方差的期望值接近于总体方差4.0,而偏样本方差比4.0小一些。
偏样本方差是正态分布随机变量的最大似然估计。如果有一个样本包含 n 个随机数,并且
知道它们符合正态分布,通过该样本可以估兑出正态分布的概率密度函数的参数。所估兑的那
组正态分布参数最符合给定的样本,就称为最大似然估计。
Numpy快
正态分布的概率密度函数的定义如下,其中^表示期望,CT2表示方差:
1 (x- n)2
—速 处 理 数 据
专业术语总是很难理解,下而我们还是用程序来验证:
n r . s e e d (42)
p = normal_pdf(mean, v a r _ n a n g e [ N o n e ] , data) O
p = np.product(p, axis=l) 0
import pylab as pi
pi.plot(var一range, p)
pl.axvline(var> 0, 1, c="r")
pi.show()
-8
Numpy快
—速 处 理 数 据
图2-8偏样本方差位于似然估计曲线的最大值处
2 . 4 . 3 大小与排序
木节介绍的函数如表2-5所示。
表 2-5本节要介绍的函数
函数名 功能 函数名 功能
min 最小值 max 最大值
minimum 二元最小值 maximum 二元最大值
a = np.array([l, 3, S, 7])
b = np.anray([2., A, 6])
n p .m a x i m u m ( a [None., b [ :, N o n e ] )
array([[2, 3, 5, 7],
[4, 4, 5, 7],
[6, 6, 6, 7]])
np.random.seed(42)
a = np.random.randint(0j 10j size=(4, 5))
—速 处 理 数 据
maxjDos = np.argmax(a)
maxjDos
a.ravel()[max_pos] np.max(a)
9 9
可 以 通 过 unravel_index()将一维数组的下标转换为多维数组的下标,它的第一个参数为一
维数组的下标,第二个参数是多维数组的形状:
idx a[idx]
(1, 0) 9
idx
array([2, 0, 1, 2])
使用下面的语句可以通过id x 选出每行的最大值:
a[np.arange(a.shape[0])^ idx]
array([7, 9, 7, 7])
1 4
r L
T J
J
>
[[3, 4, 6, 6, 7],
*
\ *
4
r L
T J
[2, 4, 6, 7, 9],
\ *
6
5
r L
T J
[2, 3, 5, 7, 7],
\ *
9
6
r L
T J
J
Numpy快
argsort()返回数组的排序下标,参 数 a x is 的默认值为-1:
—速 处 理 数 据
sort_axisl = np.argsort(a)
sort_axis0 = np.argsort(a., axis=0)
sort_axisl sort—axis0
然后使用这些下标数组得到排序之后的数组:
2
6
1 4
r L
T J
J
J
J
[[3, 4, 6, 6, 7],
4
4
r L
T J
J
[2, 4, 6, 7, 9],
6
5
T J
r L
3, 5, ly 7],
J
[2,
9
6
r L
T J
^
使用这种方法可以对两个相关联的数组进行排序,即从数组 a 产生排序下标数组,然后使
用它对数组 b 进行排序。排序相关的函数或方法还可以通过kind 参数指定排序算法,对于结构
P y th o n 科学计算(第2 版)
数组可以通过 order参数指定排序所使用的字段。
lexsortO类似于 Excel 中的多列排序。它的参数是一个形状为(k,N)的数组,或者包含 k 个长
度 为 N 的数组的序列,可以把它理解为 Excel 中 N 行 k 列的表格。lexsort()返回排序下标,注意
数组中最后的列为排序的主键。在下面的例子中,按 照 “姓名-年龄”的顺序对数据排序:
[2, 3, 1, 4, 0 ] [ r i r , 32],
[.wang’
,31],
[ • w a n g 、 33],
[•zhang’
,36],
[■zhang’
,37]]
Numpy快
如果需要对一个 N 行 k 列的数组以第一列为主键进行排序,可以先通过切片下标::-l 反转
—速 处 理 数 据
数组的第1轴,然后对其转置进行 lexsortO排序:
b b [ n p . l e x s o r t ( b [ :, ::-l].T)]
[15, 23, 25, 37, 47] [15, 47, 25, 37, 23]
%timeit np.sort(n)[:5]
%timeit np. sort ( r e p a r t i t i o n (r, 5)[ :5])
100 loops, best of 3: 6.02 ms per loop
1000 loops, best of 3: 348 ns per loop
用 medianO可以获得数组的中值,即对数组进行排序之后,位于数组中间位置的值。当长
度是偶数时,则得到正中间两个数的平均值。它也可以指定 axis 和 ou t 参数:
np.median(a^ axis=l)
array([ 6 。 6 ” 5” 4.])
percentileO用于计算百分位数,即将数值从小到大排列,计算处于 p% 位置上的值。下面的
程序计兑标准正态分布随机数的绝对值在68.3%、95.4%以及99.7%处的百分位数,它们应该约
等 于 1 倍、2 倍 和 3 倍的标准差:
r = np.abs(np.random.nandn(100000))
np.percentile(r, [68.3, 95.4, 99.7])
Numpy快
当数组中的元素按照从小到大的顺序排列时,可 以 使 用 searchsortedO在数组中进行二分搜
索 。在下面的例子中,a 楚一个已经排好序的列表,v 是需要搜索的数值列表。searchsorted()
—速 处 理 数 据
返回一个下标数纟II, 将 v 中对应的元素插入到a 中的位置,能够保持数据的升序排列。当 v 中
的元素在 a 中出现时,通 过 side 参数指定返回最左端的下标还是最右端的下标。在下面的例子
中,16放 到 a 的下标为3、4 、5 的位置都能保持升序排列,side 参数为默认值"left”
时返回3 , 而
为"right"时返丨Hi 5。
[0, 2, 6, 3] [0, 2, 6, 5]
searchsortedO可以用于在两个数组中查找相同的元素。下面看一个比较复杂的例子:有 x
和 y 两个一维数组,找 到 y 中每个元素在x 中的下标。若不存在,将下标设置为-1。
x = np.array([3j 5^ 7y 1, 9, 8, 6, 10])
y = np.array([2, 1, 5 , 10, 100, 6])
def get_index一searchsorted(x, y ) :
index = np.argsont(x) O
sonted_x = x[index] ©
sorted_index = np.searchsorted(sorted_x, y) ©
yindex = np.take(index, sorted— index, mode="clip") O
mask = x[yindex] != y ©
yindex[mask] = -1
Python 科学计算 (第 2 版)
return yindex
get_index_searchsorted(x, y)
O 由于 x 并不是按照升序排列,
因此先调用 argsortO获得升序排序的下标index。
© 使 用 index
获 得 将 x 排序之后的 sorted_ x 。© 使 用 searchsorted()在 sorte_x 中搜索 y 中每个元素对应的下标
soited_ index〇
〇如果搜索的值大于 x 的最大值,那么下标会越界,因此这里调用 take()函数,take(index,
sorted_ index)与 index[sorted_ index]的 含 义 相 但 是 能 处 理 下 标 越 界 的 情 况 。通过设置 m ode 参
数为"clip ”
,将下标限定在0 到 lenOO- l 之间。
© 使 用 yindex 获 取 x 中的元素并和 y 比较,若值相同则表示该元素确实存在于x 之中,否
则表示不存在。
这段算法有些复杂,但由于利用了 N um P y 提供的数组操作函数,它的运算速度比使用字
典的纯 Python 程序要快。下面我们用两个较大的数组测试运算速度。为了比较的公平性,我们
Numpy快
调 用 tdist〇方法将数组转换成列表:
—速 处 理 数 据
x = n p . r a n d om.permutation( 1 0 0 0 ) [ :100]
%timeit g Gt_index_searchsonted(xJ y)
%timeit get_index_dict(xlj yl)
True
2 . 4 . 4 统计函数
表 2-6本节要介绍的函数
函数名 功能 函数名 功能
unique 去除M 复元尜 bincount 对整数数组的元尜计数
histogram 一维直方KI统计 digitze 离敗化
uniqueO返回其参数数组中所有不同的值,并且按照从小到大的顺序排列。它有两个可选
参数:
* retum jndex : Ture 表示同时返回原始数组中的下标。
• retumjnverse : True 表示返回重建原始数组用的下标数组。
下面通过儿个例子介绍unique()的用法。首先用 mndint()创 建 有 10个元素、值 在 0 到 9 范围
之内的随机整数数组,通 过 unique(a)可以找到数组 a 中所有的整数,并按照升序排列:
n p . r a n d o m. s e e d (42)
NumPy
a = np.random.randintC©, 8, 10)
丨快速处理数据
a np.unique(a)
[6, 3, 4, 6, 2, 7, 4, 4, 6, 1] [1, 2, 3, 4, 6, 7]
x index a[index]
rindex xfrindex]
[4, 2, 3, 4, 1, 5, 3, 3, 4, 0] [6, 3, 4, 6, 2, 7, 4, 4, 6, 1]
bincountO对整数数组中各个元素所出现的次数进行统计,它要求数组中的所有元素都萣非
np.bincount(a)
array([0, 1, 1, 1, 3, 0, 3, 11)
87
Python 科学计算 (第 2 版)
面我们看一个实例:
x = np.anray([0 , 1, 2, 2, 1, b 0])
w = np.array([0.1, 0.3, Q.2, 0.4, 0.5, 0.8, 1.2])
np.bincount(x^ w)
均值,可以用求和的结果与次数相除:
np.bincount(Xj w) / np.bincount(x)
Numpy快
histogram()对一维数组进行直方图统计。其参数列表如下:
其 中 a 是保存待统计数据的数组,bins 指定统计的区间个数,即对统计范围的等分数。range
是一个长度为 2 的元组,表示统计范围的最小值和最大值,默认值为 None , 表示范围兩数据的
范围决定,即(a.min〇,
a .max())。当 density 参数为 False 时,函数返回 a 中的数掘在每个区间的个
数,参数为 T ru e 则返回每个区间的概率密度。weights 参数和 bincount〇的类似。
histogram ()返回两个一维数组---- h ist 和 bin_ edges ,第一个数组是每个区间的统计结果,
第二个数组的长度为 len (hist) + 1 , 每两个相邻的数值构成一个统计区间。下而我们看一个
例子:
a = n p . r a n d o m. r a n d (100)
(array([28, 18, 17, 19, 18]), array([ 0. , 0.2, 0.4, 0.6, 0.8, 1.]))
d = np.loadtxt("height.csv"., dGlimitGr="/')
Numpy快
此很容易计算出每个年龄段的T •均身高:
—速 处 理 数 据
sums = n p . h i s t o g n a m ( d [ 0], bins=nange(7., 21), w e i g h t s = d [ :, 1])[0]
sums / cnts
2 . 4 . 5 分段函数
本丨:T介绍的函数如表2-7所示。
表 2 -7 本节要介绍的函数
函数名 功能
where 欠量化判断表达式
piecewise 分段函数
select 多分支判断选择
在前而的小节中介绍过如何使用 frompyfuncO函数计算三角波形。由于三角波形是分段函
数,需要根据肉变M:的取值范幽决定计算函数值的公式,因此无法直接通过 u flin c 函数计算。
N um Py 提供了一些计算分段函数的方法。
Python 科学计算 (第 2 版)
x = y if condition else z
布 N u m P y 中,where〇函数可以看作判断表达式的数纟11版本:
x = where(conditionj y, z)
其 中 condition、
y 和 z 都楚数组,
它的返丨nl值是一个形状与 condition相同的数组。当 condition
中的某个元素为T m e 时,x 中对应下标的值从数组y 获取,否则从数组 z 获取:
x = np.anange(10)
np.where(x < 5, 9 - x, x)
array([9, 8, 1 } 6, 5, 5, 6, l y 8, 9])
如 果 y 和 z 是单个数值或者它们的开$状与condition 的不同,将先通过广播运算使其形状
一致:
Numpy快
—速 处 理 数 据
0,
np.where(x < c0^
x / c0 * he,
(c - x) / (c - c0) * he))
由于三角波形分为三段,因此需-要两个嵌套的wherc()进行计算。由于所有的运算和循环都
在 C 语言级別完成,因此它的计算效率比frompyflincO高。
随着分段函数的分段数量的增加,
需要嵌套更多层 whereO。
这样不便于程序的编写和阅读。
可以用 selectO解决这个问题,它的调用形式如下:
def triangle 一
wave2(x, c0_» he):
x = x - x.astype(np.int)
return np.select([x >= c ,x < c0 , True ],
[0 ,x/c0*hc, (c-x)/(c-c0)*hc])
[0 , x / c0 * he],
default=(c-x)/(c-c0)*hc)
Numpy快
x >= c, x < c0, x / c0 * he, (c - x) / (c -c0 ) * he
—速 处 理 数 据
在计算时还会产生许多保存屮间结果的数组,因此如果输入的数组X 很大,将会发生大量
内存分配和释放。
为了解决这个问题,
N u m P y 提供了 piecewiseO专门用于计算分段函数,
它的调叫参数如下:
参 数 x 是一个保存自变量值的数组,condlist楚一个长度为 M 的布尔数组列表,其中的每
个布尔数组的长度都和数组x 相同。ftm dist 是一个长度为 M 或 M + 1 的闲数列表,这些闲数的
输入和输出都是数组。它们计算分段函数中的毎个片段。如果不是函数而是数值,则相当于返
回此数值的函数。
每个函数与 condlist中下标相同的布尔数组对应,
如 果 ftmclist的长度为 M +1,
则最后一个函数计筧所有条件都为F alse 时的值。下而是使用 piecewiseO 计算三角波形的程序:
x = np.linspace(0, 2, 10000)
yl = triangle_wavel(x, 0.6, 0.4, 1.0)
y2 = triangle 一wave2(x, 0.6, 0.4, 1.0)
np.all(yl =:
= y2), np.all(yl == y3)
(True, True)
2 . 4 . 6 操作多维数组
Numpy快
本节介绍的函数如表2-8所示。
表 2-8本节要介绍的函数
函数名 功能 函数名 功能
concatenate 连接多个数组 vstack 沿第0 轴连接数组
hstack 沿 第 1轴连接数组 column一stack 按列连接多个一维数绀
split、airay_split 将数组分为多段 transpose 重新设置轴的顺序
swapaxes 交换两个轴的顺序
concatenate()是连接多个数纟11的最签本的函数,其他函数都是它的快捷版本。它的第一个参
数是包含多个数组的序列,它将沿着 a x is 参数指定的轴(默认为第0 轴)连接数纟11。所有这些数
组的形状除了第 axis 轴之外都相同。
vstack()沿 着 第 0 轴连接数组,当被连接的数组是长度为 N 的一维数组时,将其形状改为
(l ,
N )〇
hstadc()沿 着 第 1轴连接数组。当所有数组都是一维时,沿着第0 轴连接数组,因此结果数
组仍然为一维的。
column_ stack〇和 hstack〇类似,沿 着 第 1轴连接数组,但是当数组为一维时,将其形状改为
(N ,1),经常用于按列连接多个一维数组。
a = np.anange(3)
b = np.arange(10, 13)
v = np.vstack((a, b))
h = np.hstack((a, b))
此外,C_[j 对象也可以)Ij于按列连接数绀:
[ 1, 11, 12],
[2, 12, 14]])
Numpy快
split〇和 array_split〇的用法基本相同,将一个数组沿着指定轴分成多个数组,可以直接指定
切分轴上的切分点下标。下而的代码把随机数组a 切分为多个数组,保证每个数组中的元素都
—速 处 理 数 据
是升序排列的。注怠通过 diff()和 nonzeroO获得的下标是每个升序片段中最后一个元素的下标,
而切分点为每个片段第一个元素的下标,冈此需要+1。
n p . r a n d o m. s e e d (42)
7, 4, 3, 7] array([3, 7]),
array([4, 6) 9]),
array([2, 6, 7]),
a r r a y ([4]),
array([3, 7])]
array([3, 7])]
trmspose()和 swapaxes〇用于修改轴的顺序,它们得到的是原数组的视图。IransposeO通过其
第二个参数 axes 指定轴的顺序,默认时表示将整个形状翻转。而 swapaxesO通过两个整数指定
调换顺序的轴。在下面的例子中:
• transpose〇的结果数组的形状为(3,4,2,5),它们分别位于原数组形状(2,3,4,5)的(1,
2,0,3)
下标位置处。
• swapaxesO的结果数组的形状为(2, 4, 3, 5),它是通过将原数纽形状的中间两个轴对调得
到的。
print u " t r a n s p o s e :’
、 np.transpose(a, (1 ,2, 0, 3 )).shape
Numpy快
print i T s w a p a x e s : n p . s w a p a x e s ( a ^ 1, 2 ) . shape
原数组形状:(
2, 3, 4, 5)
—速 处 理 数 据
transpose: (3, A, 2, 5)
s w a p a x e s : (2, 4, 3, 5)
下而以将多个缩略图拼成一幅大图为例,帮助读者理解多维数组中变换轴的顺序。在
data/thumbnails 1e|录之下有30个 160x 90像素的 P N G 图标阁像,需要将这些阁像拼成一幅6 行 5
import glob
import numpy as np
imgs = [ ]
for fn in glob.glob("thumbnails/*.png"):
imgs.append(imread(fn^ -1))
print i m g s [ 0 ] .shape
(90, 160, 3)
img = np.concatenate(imgSj 0)
img.shape
(2700, 160, 3)
由于我们的最终目标是把它们拼成一幅如图2-9(左)所示的6 行 5 列的缩略图,因此需要
将 im g 的第0 轴分解为3 个轴,
长度分别为(6,5,90)。
下而使用 reshape()完成这个工作。
使用 im g l [i,
j ]可以获取第 i 行、第 j 列上的图像:
Numpy快
獅
—速 处 理 数 据
■ s P I
图 2-9 使用操作多维数组的函数拼接多幅缩略图
imgl[0, 1].shape
(90, 160, 3)
根据目标图像的大小,可以算出目标数组的形状为(540,800,3),即(6*90,5*160,3),也可以
把它看作形状为(6,90,5,160,3)的多维数组。与丨11屯1的形状相比,可以看出需要交换丨〇^1的第
1轴和第2 轴。这个操作可以通过 im gl .swapaxes()或 im gl .lranspose〇完成。然后再通过 reshape〇
将数组的形状改为(540,800,3)。
img = np.concatenate(imgSj 0)
•reshape(540, 800, 3)
下而的程序将每幅缩略图的边沿上的W 个像素填充为白色,效果如图2-9(右)所示。O 这M
使用一个形状与 i m g l 的前4 个轴相冋的 m ask 布尔数组,该数组的初始值为 True 。© 通过切片
将 m ask 中除去边框的部分设置为 False 。
€)将 i m g l 中与 m ask 为 T ru e 的对应像素填充为白色。
Python 科学计算(第2 版)
img = np.concatenate(imgs, 0)
imgl[mask] = 230 ©
img4 = imgl.swapaxes(l, 2).reshape(540, 800, 3)
2 . 4 . 7 多项式函数
多项式函数是变量的整数次幂与系数的乘积之和,可以用下面的数学公式表示:
由于多项式闲数只包含加法和乘法运算,冈此它很容M 计算,可用于计算其他数学闲数的
Numpy快
近似值。多项式函数的应用非常广泛,例如在嵌入式系统中经常会用它计算正弦、余弦等函数。
在 N um Py 中,多项式函数的系数可以用一维数组表示,例如可以用下而的数组表示,其 中 a[〇l
—速 处 理 数 据
p = np.polyld(a)
print type(p)
p(np.linspace(0, 1, 5))
对 p o ly ld 对象进彳」
:加减乘除运算相当于对相应的多项式函数进行计算。例如
p + [-2, 1] # 和 p + np.polyld([-2, 1 ] ) 相同
True
p.deriv()
p.integO
p . i n t e g O .deniv() == p
True
Numpy快
多项式函数的根可以使用rootsO函数计算:
—速 处 理 数 据
r = np.roots(p)
r
a r r a y ( [ -1.61803399, 1. ,0.61803399])
p(r) # 将根带入多项式计尊,得到的值近似为0
array([ 2.33146835e-15, 1.33226763e-15, 1.11022302e-16])
而 poly〇闲数可以将根转换Ini多项式的系数:
np.poly(r)
a r r a y ([ 1 .0 0 0 0 0 0 0 0 e+ 0 0 ^ -1.66533454e-15, -2.00000000e+00,
1.00000000e+00])
除了使用多项式对象之外,也可以直接使用 N u m P y 提供的多项式闲数对表示多项式系数
的数组进行运算。可以在 IPython 中使用自动补全查看函数名:
其 中 的 pdyfitO 闲数可以对一组数据使用多项式闲数进行拟合,找到与这组数据的误差平
方和最小的多项式的系数。下而的程序用它计算-ti/ 2 〜 ti/2区间勹 sin〇〇闲数最接近的多项式
的系数:
Python 科学计算 (第 2 版)
n p .set_printoptions(suppress=True, pnecision=4)
a = np.polyfit(x^ y, deg) ©
error = np.abs(np.polyval(a, x) - y) O
print "degree {}: {}".format(deg, a)
e g
2 . 4 . 8 多项式函数类
.
n u m p y p o ly n o m ia l 模块中提供了史丰富的多项式函数类,例 如 P o l y n o m ia l 、C h e b y s h e v 、
L e g e n d re 等。它们和前面介绍的 n u m .
p y p o ly ld 相反,多项式各项的系数按照¥从小到大的顺序
排歹U ,下面使用 P o l y n o m ia l 类表示多项式x 3 - 2 x + 1 ,并计算x = 2处的值:
print p(2.0)
5.0
Polynomial对象提供了众多的方法对多项式进行操作,例 如 deriv()计兑导函数:
p.deniv()
Polynomial([-2” 0 . , 3 . ] , [ - 1•, L.] , [ - 1 •,
: :L.])
切 比 雪 夫 多 项 式 是 一 个 正 交 多 项 式 序 列 —个 n 次多项式可以表示为多个切比雪夫多
项式的加 权 和 。在 NumPy中 ,使 用 Chebyshev类表示由切比雪夫多项式组成的多项式p 〇〇:
n
p (x ) = y q T i C x )
Numpy快
T i 〇〇多项式可以通过 C h e b y s h e v .b a s i s (i )获得,图 2-11显示了 0 到 4 次切比雪夫多项式。通
过多项式类的 c o n v e i t ()方法可以在不同类型的多项式之间相互转换,转换的 F I 标类型由 k i n d 参
—速 处 理 数 据
数指定。例如下而将T 4( )转换成 P o l y n o mx ia l 类 。由结果可知:T 4( ) = 1 —8 2 + 8 4。
x x x
C h e b y s h e v .b a s i s (4).c o n v e r t (k i nd=Polynomial)
切比雪夫多项式的根被称为切比雪夫节点,可以用于多项式插值。相应的插值多项式能最
大限度地降低龙格现象,并且提供多项式在连续函数的最侁一致逼近。下面以f 〇〇 = ^函
数插值为例演示切比雪夫节点与龙格现象。
Python 科学计算 (第 2 版)
插值与拟合
所谓多项式插值就是找到一个多项式经过所有的插值点。一 个 n 阶多项式有 n + 1 个系数,
因此可以通过解方程求解经过n + 1 个插值点的 n 阶多项式的系数。fit〇方法虽然计算与目标点拟
合的多项式系数,但是当使用 n 阶多项式拟合 n + 1 的目标点时,多项式将经过所有目标点,因
此其结果与多项式插值相同。
def f(x):
return 1.0/ ( 1 + 2 5 * x**2)
Numpy快
n = 11
xl = np.linspace(-l, 1, n) O
—速 处 理 数 据
x2 = Chebyshev. b a s i s ( n ) . r o o t s () ©
xd = np.linspace(-lj 1} 200)
print u "插值多项式的最大误差:",
print u"等距离取样点:" , abs(cl(xd) - f(xd)).max(),
print u"切比馬•人节点:" , abs(c2(xd) - f(xd)).max()
插值多项式的S 大误差:等距离取样点: 1.91556933029切比霄火节点: 0.109149825014
- 0.2
阁2 - 1 2 等距离插ffi点(左)、切比雪夫插值点(右)
在使用多项式逼近函数时,使用切比雪夫多项式进行插值的误差比一般多项式要小许多。
在下面的例子中,
对 g (x )在 100个切比雪夫节点之上分别使用P o l y n o m ia l 和 C h e b y s h e v 进行插值,
结果如图 2-13 所示。在使用 P o ly n o m ia l.fitO 插值时,产生了 R a n k W a m in g :T h e fit m ay b e p o o r ly
c o n d itio n e d 警告,因此其结果多项式未能经过所有插值点。
def g(x):
x = (x - 1) * 5
return np.sin(x**2) + np.sin(x)**2
n = 100
x = Chebyshev.basis(n).roots()
xd = np.linspace(-l, 1, 1000)
Numpy快
print "Max Polynomial Error:' abs(g(xd) - p_jg(xd)).max()
print "Max Chebyshev Error:'、 abs(g(xd) - c_g(xd)).max()
—速 处 理 数 据
Max Polynomial Error: 1.19560558744
Max Chebyshev Error: 6.47575726376e-09
trim()方法可以降低多项式的次数,将尾部绝对值小于参数 t d 的高次系数截断。下面使JIJ
tiim〇方法获取 c 中前68个系数,得到一个新的 Chebyshev 对 象 c_trimed,其最大误差上升到0.09
左名。
c_trimed = c_g.trim(tol=0.05)
print " d e g r e e : c 一trimed.degree()
print "error:", abs(g(xd) - c_trimed(xd)).max()
Python 科学计算 (第 2 版)
degree: 68
error: 0.0912094835458
def h(x):
x = 5 * x
n = 20
x = Chebyshev. b a s i s ( n ) . r o o t s ()
c_h = Chebyshev.fit(x, h(x), n - 1, d o m a i n K - l , 1])
多项式类支持四则运算,
下面将 c_ 5 和 c_h 相 减 得 到 并 调 用 其 rootsO计算其所有根。
Numpy快
True
图2 - 1 4 使用Chebyshev 插值计算两条曲线在[-1,1]之间的所有交点
切比雪夫多项式在区间[-1,1]上为正交多项式,因此只有在该区间才能对目标函数正确插
值。为了对任何区域的目标函数进行插值,耑要对自变量的区间进行缩放和平移变换。可以通
过 domain 参数指定拟合点的区间。在下面的例子中,对 g 2(x )在区间卜10,0]之内使用切比雪夫
多项式进行插值。〇为了产生 E丨标冈间的切比雪夫节点,在通过 basiso 方法创建几〇〇吋,通过
dom ain 参数指定目标区间。© 在 调 用 fit〇方法进行拟合时,通 过 domain 参数指定同样的区间。
© 最后输出拟合得到的c j 2 多项式在[-10,0]中与目标函数的最大误差。
def g2(x):
return np.sin(x**2) + np.sin(x)**2
n = 100
2 . 4 . 9 各种乘积运算
本节介绍的函数如表2-9所示。
表 2 - 9 本节要介绍的函数
函数名 功能 函数名 功能
dot 矩阵乘积 inner 内积
outter 外积 tensordot 张量乘积
矩阵的乘积可以使用dot〇计算。对于二维数组,它计算的是矩阵乘积;对于一维数组,它
计算的是内积。当需要将一维数组当作列矢S 或行矢S 进行矩阵运算时,先将一维数组转换为
二维数组:
a = np.array([l, 2, 3])
a[:, None] a [ N o n e , :]
[[1], [ [ U 3]]
[2L
[3]]
对于多维数组,dot〇的通用计算公式如下,即结果数组中的每个元素都萣:数 组 a 的最后
轴上的所有元素与数组b 的倒数第二轴上的所有元素的乘积和:
dot(a, b ) [ i ,
j,k,
m] = s u m ( a [ i ,
j , :] * b [ k ,:
,m])
Python 科学计算 (第 2 版)
下面以两个三维数组的乘积演示dot()的计算结果。酋先创建两个三维数组,这两个数组的
最后两轴满足矩阵乘积的条件:
a = np.arange(12).reshape(2^ 3, 2)
b = np.arange(12, 2 4 ) . r e s h a p e d 2, 3)
c = np.dot(a, b)
c.shape
(2, 3, 2, 3)
对于两个一维数组,inner()和 dot〇—样,计算两个数组对应下标元素的乘积和。而对于多
Numpy快
维数组,它讣算的结果数组中的每个元素都是:数 组 a 和 b 的最后轴的内积。因此数组 a 和 b
的最后轴的长度必须相同:
—速 处 理 数 据
inner(a, b)[ i , j , k ,
m] = sum ( a [ i ,
j,:
]*b[k,m,:])
下面是对 innerQ的演示:
a = np.arange(12).reshape(2^ 3, 2)
b = np.arange(12, 24).reshape(2, 3, 2)
c = np.inner(aj b)
c.shape
(2, 3, 2, 3)
assert c[i, k, 1] == n p . i n n e r ( a [ i ^ 〕
•],b[k_» 1])
outerO只对一维数组进行计算,如果传入的是多维数组,则先将此数组展平为一维数组之
后再进行运算。它计算列向量和行向量的矩阵乘积:
a = np.array([l,, 2, 3])
b = np.array([4, 5, 6, 7])
a = n p . r a n d o m. r a n d (3, 4)
b = n p . r a n d o m. r a n d (4, 5)
c2 = np.tensordot(a, b, axes=l) ©
c3 = np.dot(a, b)
Numpy快
中的倒数第二轴求乘积和:
—速 处 理 数 据
a = np.arange(12).reshape(2^ 3, 2)
b = np.arange(12, 2 4 ) .reshape(2^ 2, 3)
c2 = np.dot(a, b)
a = n p . r a n d o m. r a n d (4, 5^ 6, 7)
b = n p . r a n d o m. r a n d (6, 5^ 2, 3)
c.shape
(4, 7, 2, 3)
05
Python 科学计算 (第 2 版)
2 . 4 . 1 0 广 义 ufunc 函数
ainv = inv(a)
a : ( … , M)
输入数组 a 的 形 状 中 带 有 它 表 示 0 到任意多个轴。当它为空时,就是对单个矩阵
求逆,g iifu n c 函数将对这些轴进行广播运算。最后两个轴的长度为 M , 表示任意大小的方形
NumPy快
矩阵。
—速 处 理 数 据
a = n p . r a n d o m. r a n d (10, 20, 3, 3)
ainv = np.linalg.inv(a)
ainv.shape
(10, 20, 3, 3)
adet :(… )
adet = np.linalg.det(a)
adet.shape
(10, 20)
n = 10000
Numpy快
n p . r a n d o m. s e e d (0)
beta = np.random.rand(n, 3)
—速 处 理 数 据
x = np.random.rand(n, 10)
M 然使用前而介绍过的 numpy.polyfit()可以很方便地完成这个任务,下而的程序输出第42
组的实际系数以及拟合的结果:
print b e t a [42]
print np.polyfit(x[42], y[42], 2)
只需要循环调用 n 次 numpy.polyfitO即可得到所需的结果,但是它的运算速度有些慢:
n p . a llclose(betaJ beta2)
True
xx = np.column_stack(([x[42]**2 ,x [ 4 2 ],np.ones_like(x[42])]))
07
Python 科学计算 (第 2 版)
%%time
X = np.dstack([x**2, x, np.ones__like(x)])
Xt = X.swapaxes(-1, -2)
Numpy快
b = umath.matrix_multiply(Xt> y [ . . ” None]).squeeze()
beta3 = np.linalg.solve(A, b)
True
Wall time: 30 ms
在上而的运算中,\的形状为(10000,10,3),沿的形状为(10000,3,10)。11^11^_1^111办〇的
各个参数和返回值的形状如下:
c = mat r i x _ m ul t i p l y ( a J b)
a : M, N)
b : ( … ,N, K)
c M, K)
调用 matrix_multiply()对 X t 和 X 中的每对矩阵进行乘积运算,
得到的结果 A 的形状为(10000,
3,3)。
而为了计算X Ty , 需要通过 y [...,
None]将 y 变 成 形 状 为 (1_,
10,
1)的数组。
matrix_ multiply(Xt ,
y [...,
None])所 得 到 的 形 状 为 (1_,
3,1)。调用其 sqiieeze(),删除长度为1 的轴。这 样 b 的形状为
(10000,3)。
solve()的参数 b 支持两利呢状,其中第一利悄况的形状如下:
x = solve(a, b)
a : … ,M, M)
b : M)
x : M)
在二维平面上的旋转矩阵为:
co s 0 —sin 0
M (0)
sin 0 co s 0 -
Numpy快
而运算轴进行矩阵乘积之后的形状为(100,2 ) , 因此结果〖
points 的形状为(4,3,100,2)。
—速 处 理 数 据
M = np.array([[[np.cos(t), -np.sin(t)]>
[np.sin(t), np.cos(t)]]
for t in np.linspace(0_j np.pi. A, endpoint=False)])
x = np.linspace(-l, 1, 100)
points = np.array((np.c— [x, x], np.c— [x, x**3], np.c 」x**3, x]))
rpoints = umath.matrix_multiply(points ,M [ :^ N o n e , … ])
将 这 12条曲线绘制成图表之后的效果如图2-15所示。
09
Python 科学计算 (第 2 版)
2 . 5 实用技巧
2 . 5 . 1 动态数组
N u m P y 的数组对象不能像列表-•样动态地改变其大小,在做数据采集的时候,需要频繁
地往数纟J1中添加数据时很不方便。
而 Python 标准库中的 am iy 数组提供了动态分配内存的功能,
而且它和 Num Py 数组一样直接将数值的二进制数据保存在一块内存中,
因此我们可以先用_ y
数纽收集数据,然后通过 np.frombuffer〇将 array 数纟II的数据内存直接转换为N um Py 数红U 下面
Numpy快
是一个例子:
import numpy as np
—速 处 理 数 据
a m iy 数组只支持一维,如果我们需要采集多个通道的数椐,可以将这些数椐依次添加进
array 数组,然后通过 rcshape〇方 法 将 np.lVombuffer()所创建的 N um Py 数组改为二维数组。在下
而的例 子 中 ,我 们 通 过 a ir a y 数 组 b u f 采集两个通道的数椐,数据采集完毕之后,通过
np.frombuffer〇将其转换为 N um Py 数组,并通过 reshape()将典形状改为二维数组:
import math
buf = array("d")
for i in range(5):
b u f •a p p e n d ( m a t h •s i n (i * 0 •1))
b u f .a p p e n d ( m a t h .c o s (i * 0 .1))
[0.29552021 0.95533649]
[ 0 .38941834 0 .9 2 1 0 6 0 9 9 ]]
Numpy快
a = array("d")
for i in range(10):
—速 处 理 数 据
a.append(i)
if i == 2:
na = np.frombuffer(a> dtype=float)
print a.buffer_info(),
if i == 4:
可以获得数组的数裾内存的地址,
可以看出 n a 的数据内存地址仍然是a 在重新分配之前的地址,
而 n a 中的数据也变成了随机的无效数据。
print n a .c t y p e s .data
print na
83088512
由上而的分析可知,每次动态数组的长度改变时,我们都需要重新调用 np .frombuffer〇以创
建一个新的 ndarray数组对象来访问其中的数掘。
当每个通道的数据类型不同时,就不能采用 a r r a y . a r m y 对象了。这吋可以使用 b y t e a i r a y 收
集数据。b y t e a m a y 是字节数组,因此首先需要通过 s t m c t 模 块 将 P y t h o n 的数值转换成其字节表
Python 科学计算 (第 2 版)
示形式。如果数据来6丨二进制文件或硬件,那么很可能得到的已经是字节数据了,这个步骤可
以省略。下面是使用 byteamiy 进行数据采集的例子:
bytearray对象的+=运算与其 extend()方法的功能相同,
但+=的运行速度要比extend〇快许
职 多,读者可以使用%6〇^自行验证。
import struct
buf = bytearray()
for i in nange(5):
dtype = np.dtype({"names":["id",. ,
sin","cos"], "formats":["h", "d", "d"]}) e
data = np.frombuffer(buf, dtype=dtype) ©
print data
度浮点数,其类型符号为“d ”。类型 格式 字 符串 中的 表 示输 出的 字 节数 据不 进 行内 存对
齐。即一条数据的字节数为2+8+8=18,如 果 没 有 “= ”,那么一条数据的字节数为8+8+8=24。
©定 义 一 个 dtype 对象来表示一条数据的结构,dtype 对象默认不进行内存对齐。如果采集
数据用的 bytearray 中的数据是内存对齐的话,只需要设置 dtype〇的 align 参数为 T ru e 即可。
©最 后 通 过 np.frombuffer〇将 bytearray转换为 Num Py 的结构数组。
然后就可以通过data[”
id"]、
data[nsin"]和 data[”
cos "]访问这三个通道的数据了。
2 . 5 . 2 和其他对象共享内存
在前面的章节中介绍过,当其他对象提供了获取其内部数据存取区的接口时,可以是用
numpy.frombuffer〇创建一个数组与此对象共享数据内存。如果对象没有提供该接口,但是能够
获取数据存•储区的地址,可以通过 ctypes 和 numpy.ctypeslib 模块中提供的函数,创建与对象共
享内存的数组。下而以 PyQt4 中的 Q lm age 对象为例,介绍如何创建一个与Q lm age 对象共享内
存的数组。
先创連一个 Q lm age 对象,并载入’
’
lena.png”
文件中的内容。然后输出与图像相关的一些
信息,为了创建与该图像共享内存的数纟11,我们需要使用这些信息。
format: 4 4
byteCount: 804864
bytesPerLine: 2048
bits: 156041248
〇由于我们只知道数据的地址,首先需要使用 CtypeS.Cast〇将整数转换为一个指向单字节类
型的指针。© 然后使用 numpy.ctypeslib.as_array()将 ctypes 的指针指向的内存转换成N um Py 的数
纽。as_array()的第二个参数是该数绀的形状,注意数組的第0 轴为图像的高,第 1轴为图像的
宽,第 2 轴为每个像素的字节数。
Numpy快
import ctypes
addr = i n t ( i m g . bi t s ())
—速 处 理 数 据
pointer = ctypes.cast(addr^ ctypes.POINTER(ctypes.c_uint8)) O
a m = np.ctypeslib.as_array(pointer^ (img.height(), img.width()_» img.depth()//8)) ©
x, y = 100, 50
b, g, r, a = arr[y, x]
print qRgb(r, g, b)
print img.pixel(x^ y)
4289282380
4289282380
0xff563412L
使用上述方法共享内存时需注意必须保持目标对象处于可访问状态。例如在上例中,如果
执 行 del im g 语句引起 im g 对象被垃圾回收,则通过 arr 数组将访问被释放掉的内存区域。为了
解决这个问题,可以让数组的 base 屈性引用目标对象,这样只要数组不被释放,则目标对象也
不会被释放。为了能正确设置 base 屈性,需要使用数组的_ airayjnterface _ 接口。
P y th o n 科学计算(第 2 版)
表 2 - 1 0 键值及含义
键值 含义
shape 所创建数组的形状
data 数据存储E 的首地址,以及是否只读
strides 数组的strides厲性
typestr 元素类型描述符
descr 如果创逑结构数组,该键描述结构体各个字段名以及对应的数据类型
version 固定为3
interface = {
—速 处 理 数 据
>
如 果 目 标 对 象 只 读 ,无法为其添加_〇11^_丨1^池(^_属性,可以创建一个代理用的
A m iy P ro x y 对 象 ,在 该 代 理 对 象 中 引 用 H 标 对 象 ,使 其 不 会 被 垃 圾 回 收 , 同时提供
_ array_interface_ 屈性,以供创建相应的数组。
class A r n a y P r o x y ( o b j e c t ) :
def _ init_ (self, base, i n t e r f a c e ) :
self.base = base
self.— array_interface— = interface
2 . 5 . 3 与结构数组共孚内存
从结构数纟11获取某个字段时,得到的是原数组的视图,但是如果获取多个字段,将得到一
个全新的数组,不与原数组共享内存。
pensontype = np.dtype({
•names': [ ’n a m e ’, ’a g e 、 'weight 、 ’h e i g h t ’],
•formats ,
:[_S30,
,_ i _ , _ f ] } , align=True)
a = np.array([("Zhang", 32, 72.5, 1 6 7 . 0 ),
True
Numpy快
为了创建结构数组的多字段视图,可以使用下面的 fields_View 〇闲数。它通过原数组的 dtype
—速 处 理 数 据
属性创建视图数组的dtype 对象。然后通过 ndairayO创建视图数组。
def fields_view(arrj f i e l d s ) :
v["age"] + = 10
print a
True
print a.dtype.fields
print a.dtype
print v.dtype
• shape:所创連数纟11的形状。
• dtype: 数组元素类型的 dtype 对象。
• buffer: 拥 有 buffer 接口的对象,所创建的数组将与该对象共享内存。
• offset: buffer 对象的数据内荐中的起始地址的偏移量。
• strides: 所创建数组的 strides属性,即每个轴上的下标增加1 时的地址增£:。
• order: C 语言格式或 Fortran语言格式。
在 fields_view ()中,我们所创建的数组视图与原数组拥有相同的shape、data 和 strides屈性。
而 dtype 屈性中的字段与原数组拥有相同的偏移量,
显然这样的新数组能够与原数组共亨内存。
N um p y—
速
处
理
数
据 快
llr
單3 亨
SciPy-数值计算库
S d P y 在 N m n P y 的基础上增加了众多的数学计算、利•学计算以及工程计算中常)|j 的模块,
例如线性代数、常微分方程数值求解、信号处理、图像处理、稀疏矩阵等。在本章中,将通过
实例介绍 S d P y 中常用的一些模块。为了方便读者理解,在实例程序中会使用 matplotlib、 T V T K
以 及 M a y a v i 等扩展序绘制二维和三维的图表。在阅读实例的源程序时,读者可以忽赂绘图部
分,在后续的章节中,我们会对这些绘图库进行详细介绍。
木章的所有实例程序都在SciPy 0.15下调试通过,请读者在运行之前检查S ciP y 的版木:
import scipy
scipy._ _ version
•0.15.0 ,
3 . 1 常数和特殊函数
与本节内容对应的Notebook 为: 03-scipy/scipy-100-intro.ipynb〇
S ciP y 的 constants模块包含了众多的物理常数:
6.62606957e-34
在 字 典 physical_constants中,以物理常量名为键,对应的值是一个含有三个元素的元组,
分别为常数值、单位以及误差,例如下面的程序用来查看电子的质量:
除了物理常数之外,constants模块中还包括许多单位信息,它 们 是 1 单位的量转换成标准
单位时的数值:
Python 科学计算(第2 版)
S c iP y 的 special 模块是一个非常完整的函数库,其中包含了蕋本数学函数、特殊数学函数
以 及 N u m P y 中出现的所有函数。由于函数数量众多,本节仅对其进行简要介绍。至于其具体
所包含的函数列表,请读者参考 S d P y 的帮助文构。
伽玛(gamma)函数r 是概率统计学中经常出现的一•个特殊函数,它的计算公式如下:
import scipy.special as S
s c ip y数
print S . g a m m a (4)
print S . g a m m a (0.5)
—值 计 算 库
6.0
1.77245385091
(0.498015668118-0.154949828302j)
inf
函数是阶乘函数在实数和复数系上的扩展,它的增长速度非常快,lo o o 的阶乘已经超
过了双精度浮点数的表示范围,因此结果是无穷大。为了计算更大的范围,可以使用 gammalnO
计 算 的 值 ,它使用特殊的算法,能直接计算r 函数的对数值,因此可以表示更大的
范围。
S . g a m m a I n (1000)
5 9 0 5 .2204232091808
这是由于浮点数的精度有限,无法很精确地表示非常接近1 的实数。例如无法用浮点数表示1 +
le -20的值,因此 l〇
g (l + le -20)的值为0 , 而当使用 l〇
g lp ()时,则可以很精确地计算:
print 1 + le-20
print np.log(l+le-20)
print S.loglp(le-20)
1.0
0 .0
le-20
头际上当 x 非常小时,loglp (x )约 等 于 X , 这可以通过对 log(l + x )进行泰勒级数展开证明。
在后续章节我们会学习如何用符号计算库Sym Py 进行泰勒级数展开。
这些特殊函数与 N um P y 中的一般数学函数一样,都 是 uftinc 函数,支持数组的广播运算。
例 如 ellipj(u,
m )计算雅可比椭岡闲数,它有两个参数 u 和 m ,返 回 4个 值 sn 、cn 、d n 和中,其中
中满足下面的椭岡积分,而sn = sincj)、 cn = coscj)、 dn = ^/l —m sin 2cj)〇
f中 d0
u —I r —
J〇 V l —m sin 20
由于 dlipj〇支持广播运算,因此下而的程序在调用它时传递的两个参数的形状分别为(200,
1)和(1,5),于是得到的4 个结果数组的形状都为(200,5),图 3-1显示了这些丨III线。
m = np.linspace(0.1, 0.9, 4)
A
值
计
算
库
•10 S 10 -10 -5 0
图3-1使用广播计算得到的cllipjO返回值
3 . 2 拟合与优化一optimize
119
Python 科学计算 (第 2 版)
3 . 2 . 1 非线性方程组求解
进行求解:
f l (u l ,u 2, u 3) = 0, f 2(u l ,u 2, u 3) = 0, f3( u l ,u 2, u 3) = 0
那 么 fu n c 函数可以定义如下:
def func(x):
ul_»u2jU3 = x
return [fl(ul,u2^u3), f2(ul,u2Ju3)> f3(ul^u2>u3)]
下而我们看一个对下列方程组求解的例子:
5 x x + 3 = 0, 4 x 〇—2 s in (x 1x 2) = 0, x xx 2 — 1.5 = 0
s c ip y数
def f(x): O
x0, xl, x2 = x.tolist() ©
return [
5*xl+3,
4*x0*x0 - 2*sin(xl*x2),
xl*x2 - 1.5
]
# f 计算方程组的误差,[ 1 ,1 ,1 ]是未知数的初始值
result = optimize.fsolve(f, [1,1,1]) ©
print result
print f(result)
[-0.70622057 -0.6 -2.5 ]
[0.0, -9.126033262418787G-14, 5.329070518200751G-15]
雅可比矩阵
雅可比矩阵是一阶偏导数以一定方式排列的矩阵,它给出了可微:分方程与给定点的最优线
性逼近,因此类似于多元函数的导数◊ 例如前面的函数 fl 、f2、B 和 未 知 数 ul 、u2 、u3 的雅可
比矩阵如下:
r3 fl d fl a fli
aui du2 du3
d f2 d f2 d f2
s c ip y数
数是未知数的一组值,它计算非线性方程组在x 处的雅可比矩阵。© 通 过 参 数 将 j()传递
—值 计 算 库
给 fsdveO 。由于本例中的未知数很少,因此计算雅可比矩阵并不能显著地提高计算速度。
def j(x): O
x0j xl, x2 = x.tolist()
return [
[0, 5, 0],
[8*x0, -2*x2*cos(xl*x2), -2*xl*cos(xl*x2 )],
print result
print f(result)
3 . 2 . 2 最小二乘拟合
s ( p) =/ [ y i - f ( x pp)]2
P y th o n 科学计算(第2 版)
import numpy as np
from scipy import optimize
def r e s i d u a l s ( p ) : O
"计算以p 为参数的直线和原始数据之间的误差"
kj b = p
return Y - (k*X + b)
# l e a s t s q 使得residuals() 的输出数姐的平方和最小,参数的初始值为[1,0]
s c ip y数
k = 0.613495349193 b = 1.79409254326
图 3-2(左)直观地M 示了原始数据、拟合直线以及它们之间的误差。OresidualsO的参数p 是
拟合直线的参数,函数返冋的是原始数据和拟合直线之间的误差。图屮用数据点到拟合立线在
Y 轴上的距离表示误差。© leastsqO使得这些误差的平方和最小,即图中所有正方形的面积之和
最小。
由前而的闲数S 的公式可知,对于直线拟合来说,误差的平方和是直线参数_ lb 的二次多
项式闲数,因此可以用如图3-2(右)所示的曲面直观地显示误差平方和与两个参数之间的关系。
图中用红色圆点表示曲而的最小点,它 的 X -Y 轴的坐标就是 leastsqO的拟合结果。
图3 - 2 最小化正方形面积之和(左),误差曲面(右)
接下来,让我们再看一个对正弦波数据进行拟合的例子:
数据拟合所用的蜗数:A*sin(2*pi*k*x + theta)
IIMII
A, k, theta = p
return A*np.sin(2*np.pi*k*x+theta)
def residuals(p, y, x ) : ©
ii itii
return y - func(x, p)
s c ip y数
A, k, theta = 10, 0.34, np.pi/6
—值 计 算 库
# 加入噪声之后的实验数掘
np.r a n d o m. s e e d (0)
yl = y 0 + 2 * np.random.randn(len(x)) ©
# 调用l e a s t s q 进行数据拟合
# r e s i d u a l s 为计寫误差的函数
# P 0 为拟合参数的初始值
# a r g s 为需要拟合的实验数据
图 3-3 S 示了带噪声的止弦波拟合。
P y th o n 科学计算(第2 版)
O 用 leastsqO对带噪声的实验数据(x ,
y l )进行数据拟合,它可以找到数组 x 和 yO 之间的正弦
- 值计算库
return A * n p .sin(2*np.pi*k*x+theta)
print popt
如果频率的初值和寘实值的差别较人,拟合结果中的频率参数可能无法收敛于实际的频率。
在下面的例子中,Etl于频率初值的选择不当,导致 curve_fit〇未能收敛到真实的参数。这时可以
通过其他方法先估算一个频率的近似值,或者使用全局优化算法。在后面的例子中,我们会使
用全局优化算法重新对正弦波数据进行拟合。
optimize 序还提供了许多求函数似小值的}/:法:Nelder-Mead 、
Powell、
CG、
BFG S、
Newton-C G 、
L -BFGS -B 等。下面我们j|j 一个实例观察这些优化函数是如何找到函数的最小值的。在本例中,
要计算最小值的函数f (x ,
y )为:
f (x ,y ) = (1 —x )2 + 100 (y —x 2)2
这个函数叫作 Rosenbrock 函数,它经常用来测试最小化算法的收敛速度。它有一个十分平
坦的山谷区域,收敛到此山谷区域比较容易,但是在山谷区域搜索到最小点则比较困难。根据
函数的计算公式不难看出此函数的最小值是0 , 在(U )处。
为了提高运算速度和精度,有些算法带有一个 fprime 参数,它是计算目标函数 f ()对各个自
变量的偏导数的函数。f(x ,
y )对变量 x 和 y 的偏导函数为:
df 9
— = —2 + 2x —400 x (y —x 2)
df ?
— = 200 y - 200 x 2
dy
s c ip y数
而 Newton-C G 算法则需要计算海森矩阵,它是一个由自变量为以量的实值闲数的二阶偏导
—值 计 算 库
数构成的方块矩阵,对于函数 f (Xl,
x 2,
...,
xn) , 其海森矩阵的定义如下:
■ d 2f d 2f d 2f -
d 2f d 2f d 2f
.d x n dx1 d x n dx 2 dx^ .
对于本例来说,海森矩阵为一个二阶矩阵:
'2(600x 2 - 200 y -f 1) -400x
-400x 200 .
下面使用各种最小值优化算法计算 f (x ,
y )的最小值,根据其输出可知有些算法需要较长的
收敛时间,而有些算法则利用导数信息更快地找到最小点。
class T a r g e t F u n c t i o n ( o b j e c t ) :
self.fprimejDoints = [ ]
s e l f .fhess_points = [ ]
2
5
P y th o n 科学计算(第2 版)
def f(self, p ) :
x, y = p.tolist()
z = target 一function(x, y)
s e l f .f j D o i n t s .a p p e n d ( (x, y))
return z
def fprime(self, p ) :
y = p.tolist()
s e l f •fprime 一p o i n t s •a p p e n d ((x, y ))
dx = -2 + 2*x - 400*x*(y - x**2)
dy = 200*y - 200*x**2
return np.array([dx, dy])
def fhess(selfj p ) :
x, y = p.tolist()
s c ip y数
s e l f . f h e ss _ p o i n t s .a p p e n d ( (x^ y))
[-400*x, 200]])
method=method,
j a c = t a r g e t .f p r i m e ,
hess=tanget.fhess)
BFGS min= 2.31605e-16, f count= 40, fprime count= AQ, fhess count= 0
Newton-CG min= 5.22666e-10, f count= 60, fprime count= 91, fhess count= 38
L-BFGS-B min= 6.5215e-15^ f count= 33, fprime count= 33, fhess count= 0
图 3W 显示了各种优化算法的搜索路径,图中用圆点表示调用f 〇时的坐标点,圆点的颜色
表示调州顺序;叉点表示调用 fprimeO时的坐标点。图中用图像表示二维函数的值,值越大则
颜色越浅,值越小则颜色越深。为了更淸晰地显示函数的山谷区域,图中显示的实际上是通过
对数函数 loglOO对 f (x ,
y )进行处理之后的结果。
Nelder-Mead
lewton-CG
• 1 0 1 2 -10 12
图3 4 各种优化算法的搜索路径
3 . 2 . 4 计算全域最小值
def func(x, p ) :
A, k, theta = p
return A*np.sin(2*np.pi*k*x+theta)
def func_error(pj y, x ) :
np.r a n d o m. s e e d (0)
yl = y0 + 2 * np.random.randn(len(x))
P y th o n 科学计算(第2 版)
我 们 使 用 optimize.basinhoppingO全域优化函数找出正弦波的三个参数。f 的M 两个參数和
苏他求最小值的函数一样:目标函数和初始值。由于它是全局优化函数,因此初始值的选择并
不是太重要。niter参数是全域优化算法的迭代次数,迭代的次数越多,就越有可能找到全域最
优解。
在 basinhoppingO内部需要调用局域最小值蚋数,其 minimizer_kw args 参数决定了所采用的
局域最小值算法以及传递给此函数的参数。下而的程序指定使用 L -BFGS -B 算法搜索局域最小
值,并且将两个对象y l 和 x 传递给该局域最小值求解函数的args 参数,而该函数会将这两个参
数传递给 func_error〇
〇
"args":(yl, x)})
print result.x
虽然频率和相位与原系数不同,但是由于正弦闲数的周期性,其拟合|||丨线楚和原始数据重
- 值计算库
合的,如 图 3-5所示。
丨
冬I 3-5川 basinhoppingO拟合正弦波
3 . 3 线性代数- linalg
numpy.linalg.solve(A ,
b )和 scipy .linalg.solve(A ,
b )可以用来解线性方程组A x = b ,也就是计算
x = A - % 。这里,A 是m x n 的方形矩阵,x 和b 是长为m 的向量。有时候A 是固定的,需要对多
组b 进行求解,冈此第二个参数也可以是m x n 的矩阵B 。这样计算出来的X 也是m x n 的矩阵,
相当于计算A —4 。
在一些矩阵公式中经常会出现类似于八_坨的运算,它们都可以用 S〇
lve (A ,B )计算,这要比
直接计算逆矩阵然后做矩阵乘法更快捷一些,下而的程序比较 s〇
M )和逆矩阵的运算速度:
import numpy as np
f rom scipy import linalg
n = 500, 50
A = np.random.rand(m, m)
B = np.random.rand(m, n)
XI = linalg.solve(A, B)
s c ip y数
X2 = np.dot(linalg.inv(A), B)
—值 计 算 库
print np.allclose(Xl, X2)
%timeit l i n a l g .solve(A, B)
%timeit np.dot(linalg.inv(A), B)
True
100 loops, best of 3: 10.1 ms per loop
苦需要对多组B 进行求解,但是又不好将它们合并成一个矩阵,例如某些矩阵公式中可能
会有A _1B 、
A _1C 、
A _1D 等乘法,而B 、
C、D 是通过某种方式逐次计算的。这时可以采用 lu_factor〇
和 lu_solve()〇先调用 lu_fact〇
K A )对矩阵A 进 行 L U 分解,得到一个元组:〇J J 矩阵,排序数组)。
将这个元组传递给 lu_s〇M ),即可对不同的B 进行求解。由于已经对A 进行了 L U 分解,lu_solve()
能够很快得出结果。
luf = linalg.lu_factor(A)
X3 = linalg.lu_solve(luf, B)
np.allclose(Xl, X3)
True
M, N = 1000, 100
n p . r a n dom.seed(0)
A = np.random.rand(M, M)
29
P y th o n 科学计算(第2 版)
B = np.random.rand(M, N)
Ai = linalg.inv(A)
luf = linalg.lu_factor(A)
%timeit linalg.inv(A)
%timeit np.dot(Ai, B)
%timeit l i n a l g .lu_factor(A)
%timeit l i n a l g .lu__solve(l u f , B)
3 . 3 . 2 最小二乘解
的用法。
—值 计 算 库
首先简单介绍一下离散卷积的相关知识和计算方法。对于离散的线性时不变系统h , 如果
它的输入是 X , 那么其输出 y 可以用 x 和 h 的卷积表示: y = x * h 。
离散卷积的计算公式如下:
y [n ] = E h [m ] x[n - m ]
假 设 h 的长度为 n , x 的长度为 m ,贝蜷积计算所得到的 y 的长度将为 rvfm-1。它的每个
值都是按照下面的公式计算得到的:
y[0] = h[0]*x[0]
•••
y[n+m-l] = h[rrl]*x[m-l]
运算:
〇竹先 make_data()创建所需的数据,
它使用随机数函数standard_normal()初始化数组 x 和 h 。
在实际的系统中 h 通常是未知的,并且值会逐渐衰减。make_data()返回系统的输入信号 x 以及
添加了随机噪声的输出信号yn 。为了和最小二乘法的结果相比较,我们同时也输出了系统的系
数 h〇
30
© solve_h()使用最小二乘法计兑系统的参数h ,因为通常我们不知道未知系统的系数的长度,
因此这里叫 N 表示所求系数的长度。
观察前面的卷积方程组可知,在 n+ m- 1 个方程中,中间的 n-m+ 1 个方程使州了 h 的所有
系数。为了程序计算方便,我们对这 m-n+ 1 个方程进行最小二乘运算。© 根 据 h 的长度,需要
将 一 维数组 x 变换成一个形状为(m -n+1,
n)的二维数组 X ,它的每行相对于上一行都左移了一个
元素。这个二维数组可以很容易地使用第2 韋中介绍过的 as_ strided()得到。O 我们取出输出数
组 y 中与数组X 每行对应的部分,
©然 后 调 用 lstsq〇对这 m -n+1个方程进行最小二乘运算。
@ lstsq〇
返回一个元组,它的第0 个元素是最小二乘解,注怠得到的结果顺序是颠倒的,因此还需要对
其进行翻转。
n p .r a n d o m .s e e d (42)
x = np.random.standard 一normal(m)
s c ip y数
h = np.random.standard_normal(n)
—值 计 算 库
y = np.convolve(x, h)
return x, yn, h
Y = y[n-l:len(x)] O
h = linalg.lstsq(X, Y) ©
return h[0][::-l] ©
图3 - 6 实际的系统参数与最小二乘解的比较
S c iP y数
3 . 3 . 3 特征值和特征向量
- 值计算库
特征句董。
evalues evectors
[-0.3983218 , 0.60889B68]]
图 3-7显示了变换前后的向量。在图中,蓝色箭头为变换之前的向量,红色箭头为变换之
后的向量。粗箭头为变换前后的特征向量。可以看出特征向量变换前后方向没有改变,只是长
度发生了变化。长度的变化倍数[tl特征值决定:一个变为原来的1.13倍长,一个变为原来的0.77
倍长。
图3 - 7 线性变换将蓝色箭头变换为红色箭头
s c ip y数
—值 计 算 库
http://research.microsoft.com/pubs/67845/ellipse-pami.pdf
用广义特征向量计算椭圆拟合。
n p .r a n d o m .s e e d (42)
t = np.random.uniform(0, 2*np.pi, 60)
alpha = 0 . 4
a = 0.5
b = 1.0
x = 1.0 + a*np.cos(t)*np.cos(alpha) - b*np.sin(t)*np.sin(alpha)
y = 1.0 + a*np.cos(t)*np.sin(alpha) - b*np.sin(t)*np.cos(alpha)
x += n p . r andom.normal(0, 0.05, size=len(x))
y += n p . r andom.normal(0, 0.05, size=len(y))
〇当传递第二个参数时,eig()计算广义特征值和向麗。evecton;中共有6 个特征向M , © 将
这 6 个特征向M 代入椭圆方程中,计算平均误差,© 并挑选误差最小的特征向S 作为椭圆的参
数 p 。图 3-8显示了参数 p 所表示的椭圆以及数据点。
S c iP y数
A = np.dot(D.Tj D)
C = np.zeros((6, 6))
C[[0, 1, 2], [2, 1, 0]] = 2, -1, 2
evalues, evectors = linalg.eig(A, C) O
evectors = np.real(evectors)
err = np.mean(np.dot(D, e v e c tors)**2> 0) ©
p = evectors[:, np.argmin(err) ] ©
print p
i_s
图3 - 8 用广义特征向量计算的拟合椭圆
3 . 3 . 4 奇异值分解-SVD
奇异值分解是线性代数中一种重要的矩阵分解,在信号处理、统计学等领域都有重要应用。
假设M 是一个m x n 阶矩阵,存在一个分解使得 : M = U n r 。其中U 是m x m 阶酉矩阵;Z 是
半正定m x n 阶对角矩阵;而V 、 即V 的共轭转置,是n x n 阶两矩阵。这样的分解就称作M 的
奇异值分解。2 对角线上的元素为M 的奇异值。通常奇异•值按照从大到小的顺序排列。
奇异值的数学描述读起来有些难懂,让我们通过一个实例说明奇异值分解的用途。下面的
例子对一幅灰度图像进行奇异值分解,然后从三个分解矩阵中提取奇兄值较大的部分数据还原
原始图像。首先读入一幅图像,并通过其红绿蓝三个通道计算灰度图像 im g , 图像的宽为375
个像素、高 为 505个像素。
img.shape
(505, 375)
调J s c i p y .linalg.svd〇对图像矩陈进行奇•值分解,得到三个分解部分:
• U : 对应于公式中的U 。
• s : 对应于公式中的2 , 由于它是一个对角矩阵,只有对角线上的元素非零,因此 s 是一
个一维数组,保存对角线上的非零元素。
s c ip y数
• V h : 对应于公式中的\^。
下而的程序查看这三个数组的形状:
—值 计 算 库
U, s, Vh = linalg.svd(img)
pi.semilogy(s, lw=3)
图3 - 9 按从大到小排列的奇异值
下而的 composite!;
)选 择 U 和 V h 中的前 n 个向M 重新合成矩阵,当用上所有向M 时,重新
合成的矩阵和原始矩阵相同:
P y th o n 科学计算(第2 版)
True
下而演示选择前10个、20个以及5 0个 向 合 成 时 的 效 果 ,如 阁 3-10所示,可以看到使用
的向量越多,结果就越接近原始图像:
3*4 统计-stats
S d P y 的 stats 模块包含了多种概率分布的随机变量(随机变量是指概率论巾的概念,不是
Python 中的变量),随机变量分为连续和离散两种。所有的连续随机变量都是 ^ co n tin u o u s 的派
生类的对象,而所有的离散随机变量都是rv_discret e 的派生类的对象。
3 . 4 . 1 连续概率分布
可以使用下而的语句获得stats模块中所有的连续随机变量:
连续随机变量对象都有如下方法:
• rvs : 对随机变量进行随机取值,可以通过 size 参数指定输出的数纽的大小。
• pdf: 随机变量的概率密度函数。
• cdf : 随机变M 的累积分布函数,它是概率密度函数的积分。
• sf : 随机变量的生存闲数,它的值是卜 cdf(t)。
• ppf: 累积分布函数的反函数。
• stat: 计算随机变M 的期望值和方差。
• fit: 对一组随机取样进行拟合,找出最适合取样数掘的概率密度函数的系数。
下而以正规分布为例,简巾.地介绍随机变量的用法。下面的语句获得默认正规分布的随机
变量的期望值和方差,我们看到默认情况下它是一个均值为0、方 差 为 1 的随机变量:
s t a t s .n o r m .s t a t s ()
(anray(0.0), array(1.0))
于正态分布的随机变量来说,这两个参数相当于指)其期望值和标准差,标准差是方差的算术
平方根,因此标准差为2.0吋,方差为4.0:
137
P y th o n 科学计算(第 2 版)
X = stats.norm(loc=1.0, scale=2.0)
X . s t a t s ()
(array(1.0), array(4.0))
stats.norm.fit(x) # 得到随机序列的期望值和标准差
(1 .0043406567303883, 1 .9 7 2 2 9 7 4 6 26 9 2 3 4 3 3 )
s c ip y数
在下而的例子中,计算取样值 x 的直方图统计以及累积分布,并与随机变量的概率密度函
—值 计 算 库
数和累积分布函数进行比较。
O 其 屮 histogmmO对 数 组 x 进行直方图统计,它将数组 x 的収值范围分为100个区间,并
统 计 x 中的每个值落入各个R 间的次数。histogramO返冋两个数组 p d f 和 t , 其 中 p d f 表示各个区
间的取样值出现的频数,凼于 nom ied 参数为 Tm e , 因此 p d f 的值是正规化之后的结果,其:结果
应与随机变量的概率密度函数一致。
表示区间,由于其中包拈区间的起点和终点,因 此 t 的长度为101。计算每个区间的中
间值,然 后调用 X .pdf(t)和 X .cdf(t)计算随机变量的概率密度闲数和累积分布沐|数的值,并与统
计值比较。©汁算样木的累积分布时,需要与区间的大小相乘,这样才能保证其结果与累积分
布函数相同。
print "max pdf error: {}, max cdf error: { } " .format(
np.abs(p 一error) .max(), np.abs(c_error) .max 〇)
图 3-l l (左)显示了概率密度函数和直方图统计的结果,可以看出二者是一致的。右图显示
了随机变量X 的累积分布函数和数组p d f 的累加结果。
0 5
图3-11正态分布的概率密度函数(左)和尜积分布函数(右)
print stats.gamma.stats(1.0)
print stats.gamma.stats(2.0)
(array(1.0), array(1.0))
(anray(2.0), array(2.0))
s c ip y数
伽玛分布的尺度参数0 和随机事件发生的频率相关,由 scale参数指定:
—值 计 算 库
stats.gamma.stats(2.0, scale=2)
(array(4.0), array(8.0))
根据伽玛分布的数学定义可知其期望值为k0, 方差为k 0 2 。
上而的程序验证了这两个公式。
当随机分布有额外的形状参数时,它所对应的 rvs()、pdf()等方法都会增加额外的参数来接
收形状参数。例如下面的程序调用rvs〇对k = 2、 0 = 2 的伽玛分布取4 个随机值:
stats.gamma.pdf(x, 2, scale=2)
array([ 0.17948513, 0.18384555, 0.13960273, 0.02062186])
X = stats.gamma(2, scale=2)
X.pdf(x)
arnay([ 0.17948513, 0.18384555, 0.13960273, 0.02062186])
3 . 4 . 2 离散概率分布
当分布函数的值域为离散吋我们称之为离散概率分布。例如投掷有六个而的骰子时,只能
39
Python科学计算(第2 版)
获 得 1到 6 的整数,因此所得到的概率分布为离散的。对于离散随机分布,通常使用概率质量
函数(PM F )描述其分布情况。
在 sta ts 模块中所 有 描 述 离 散 分 布 的 随 机 变 量 都 从 rv_discrete 类 继 承 , 也可以直接用
w _disCret e 类自定义离散概率分布。例如假设有一个不均匀的骰子,它的各点出现的概率不相
等。我们可以用下面的数组x 保存骰子的所有可能值,数 组 p 保存每个值出现的概率:
x = range(l, 7)
p = (0.4, 0.2, 0.1, 0.1, 0.1, 0.1)
dice.rvs(size=20)
array([l, 6, 3, 1, 2, 2, 4, 1, 1, 1, 2, 5, 6, 2, 4, 2, 5, 2, 1, 4])
s c ip y数
下面我们用程序验证概率论中的中心极限定理:大量相互独立的随机变董,其均值的分布
以正态分布为极限。我们计算上而那个特殊骰子投掷5 0 次的平均值,由于每次投掷骰子都可
—值 计 算 库
n p .r a n d o m .s e e d (42)
samples = dice.rvs(size=(20000, 50))
3 . 4 . 3 核密度估计
在上面的例子中,由于每个取样都是离散的,因此其平均值也是离散的,对这样的数据进
行直方图统计很容易出现许多离散点恰巧聚集到同一区间的现象。为了更平滑地示样本的概
率密度,可以使用 kde.gaussian_ kde〇进行核密度估计。在 图 3-12中,直方图统计的结果有很大
的起伏,而核密度估计与拟合的正态分布十分接近,因此验证了中心极限定理。
一 , bins, step = p i . h i s t (
samples_mean, bins=100, norme d = T ru e ,histtype="step" ,label=u" 直方图统计••)
kde = s t a t s . k d e . gaussian_kde(samples_mean)
p i . l e g e n d ()
1 i
2.5 3.<
图3 - 1 2 核密度估计能更准确地表示随机变量的概率密度函数
核密度估计算法在每个数据点处放置一条核函数曲线,最终的核密度估计就是所有这些核
函数曲线的®加。g a u s s i a n _k d e ()的核函数为高斯曲线,其 b w _ m e th o d 参数决定核函数的宽度,
即高斯丨
1丨
I 线的方差。b w _ m e th o d 参数可以是如下几种情况:
•当 为 '吋将采用相应的公式根据数据个数和维数决定核函数的宽度系数。
s c ip y数
k o ttV s ilv e r m a n
•当为函数时将调用此函数计算曲线宽度系数,闲数的参数为g a u s s i a n _ k d e 对象。
—值 计 算 库
•当 为 数 值 时 ,将直接使用此数值作为宽度系数。
核函数的方差由数据的方差和宽度系数决定。
下面的程序比较宽度系数对核密度估计的影响。当宽度系数较小时,可以看到在三个数据
点处的高斯曲线的峰值,而当宽度逐渐变大时,这些峰值就合并成一个统一的峰值了。
y = kde(x)
图 3-13显示 b w _ m e th o d 参数越大,核密度估计曲线越平滑。
3 . 4 . 4 二项分布、泊松分布、伽玛分布
本节州几个实例程序对概率论中的二项分布、泊松分布以及伽玛分布进行一些实验和
讨论。
二项分布是最重要的离散概率分布之一。假设有一种只有两个结果的试验,其成功概率为
p , 那么二项分布描述了进行n 次这样的独立试验,成 功 k 次的概率。二项分布的概率质量闲数
公式如下:
n! ■k
f (k ;n ,p ) Pk( l - P )n'
k !(n —k )!
s t a t s .b i n o m .p m f (r a n g e (6)^ 5, 1/6.0)
lambda_ = 1 0 . 0
x = np.arange(20)
42
print np.max(np.abs(y_binom_nl possion))
print np.max(np.abs(y_bin 〇
fn_n2 possion))
0.00675531110335
0.000630175404978
n =100 •1000
10
次败
3 - 1 4 当n 足够大时二项分布和泊松分布近似相等
s c ip y数
泊松分布适合描述单位时间内随机事件发生的次数的分布情况。例如茶个设施在一足时间
—值 计 算 库
内的使用次数、机器出现故障的次数、商然灾害发生的次数等。
为了加深读者对泊松分布概念的理解,下面我们使用随机数模拟泊松分布,并与概率质量
函数进行比较,结果如图3-15所示。图中,每秒内事件的〒均发生次数为1 0 , 即入= 1 0 。其
中左图的观察时间为1000秒 ,而右图的观察时间为50000秒。可以看出观察时间越长,每秒内
事件发生的次数越符合泊松分布。
n p .r a n d o m .s e e d (42)
def simj3 〇
isson(lambda_, t i m e ) :
t = np.random.uniform(0, time, size=lambda_ * time) O
x = c o u n t _ e d g e s [ :-1]
poisson = s t a t s .p o i s s o n .pmf(x, lambda 」
return x, poisson, dist
lambda_ = 10
times = 1000, 50000
43
Python 科学计算(第2 版)
t i m e = 1 0 0 0 , m a x _ e r r o r = 0 .019642302016
time=50000, m a x _ e r r o r = 0 .00179801289496
图3 - 1 5 模拟泊松分布
下面的程序模拟了事件的时间间隔的伽玛分布,结 果 如 图 3-16所示。图屮的观察时间为
1000秒,〒均每秒产生10个事件。左图中k = 1,它表示相邻两个事件间的间隔的分布,而k = 2
则表示相隔一个事件的两个事件间的间隔的分布,可以看出它们都符合伽玛分布。
def s i m _ g a m m a ( l a m b d a t i m e , k):
t = np.random.uniform(0> time, size=lambda_ * time) O
t.sort() ©
interval = t[k:] - t[:-k] ©
dist, interval 一edges = np.histogram(interval, bins=100, density=True) O
x = (interval 一e d g e s [1:] + interval_edges[:-l])/2 0
gamma = stats.gamma.pdf(x, k, s c a l e = l .0/lambda_) ©
lambda 10.0
time = 1000
ks = 1, 2
xl, gammal, distl = sim_gamma(lambda_, time, ks[0])
时间间n
阁3 - 1 6 模拟伽玛分布
s c ip y数
〇首 先 在 1000秒之内产生10000个随机事件发生的时刻。因此事件的平均发生次数为每秒
—值 计 算 库
10次。©为了计算事件前后的时间间隔,需要先对随机时刻进行排序,© 然后再计算 k 个事件
之间的时间间隔。〇对该时间间隔调用 histogramO进行概率统计,设 置 density 为 T m e 可以直接
计览概率密度。histogram〇返冋的第二个值为统计区间的边界,© 接 下 来 用 ganirna.pdf()计箅伽
玛分布的概率密度时,使川各个区间的中值进行计算。pdf〇的第二个参数为 k 值 ,sca le 参数
为1/入。
接下来我们看一道关于伽玛分布的概率题:有 A 和 B 两路公交车,平均发车间隔时间分
别 是 5 分 钟 和 10分钟,某乘客在站点 S 可以任意选杼两者之一乘坐,假 设 A 和 B 到 达 S 的时
刻无法确定,计算该乘客的平均等待公交车的时间。
可以将“假 设 A 和 B 到 达 S 的时刻无法确定”理解为公交车到达 S 站点的时刻是完全随机
的,因此单位时间之内到达S 站点的公交车次数符合泊松分布,而前后两辆公交车的时间差符
合 k= l 的伽玛分布。下面我们先用随机数模拟的方法求出近似解,然后推导出解的公式。
T = 100000
A_count = T / 5
B_count = T / 10
bus_time.sort()
N = 200000
45
Python 科学计算(第2 版)
199.12512768644049
np.mean(np.diff(busjtime)) * 60
—值 计 算 库
199.98208112933918
这是因为存在观察者偏差,即会有更多的乘客出现在时间间隔较长的时间段。我们可以想
象如果公交车冈为事故晚点很长时间,那么通常车站上会挤满等待的人。在图3-17(上)中,蓝
色竖线代表公交车的到站时刻,红色竖线代表乘客的到站吋刻。可以看出,两条蓝色竖线之间
的距离越大,其间的红色竖线就会越多。阁3-17(下)的横轴是前后两辆公交车的时间差,纵轴
是这段时间差之内的等待人数,可以看出二者成正比关系。
公艾车的时间
图3 - 1 7 观察者偏控
通过以上分析,不难写出计算平均等待时间的计算公式:
46
0+〇 〇 2 xf(x) dx
.+ 〇
〇
x f (x )d x
t = 10.0 / 3 # 两辆公交车之间的平均时间间隔
bus 一interval = s t a t s . g a mm a (1, scale=t)
s c ip y数
d, _ = integrate, quad (lambda x: x * bus— interval .pdf (X), 0, 1000)
n / d * 60
—值 计 算 库
200.0
3 . 4 . 5 学生 t-分布与 t 检验
从均值为^的正态分布中,抽取有n 个值的样本,计算样本均值5和样本方差s :
n
X x H---- h X 】
x S2
n 占 2 ( Xr 幻2
之差经过正规化之后的数值,可以用来描述抽取的样本与整体样本之间的差异。
下而的程序模拟学生t-分布(参见阁3-18),O 创建一个形状为(100000,10)的正态分布的随机
数数组,© 使用上而的公式计算 t 值,© 统 计 t 值的分布情况并与 stats.t的概率密度函数进行比
较。如果我们使用10个样本计算 t 值,则它应该符合 df= 9 的学生 t-分布。
mu = 0.0
n = 10
t_dist = stats.t(n-l).pdf(x)
47
Python 科学计算(第2 版)
图3 - 1 8 模拟学生t-分布
图 3-19当df 增大时,学生t-分布趋向于正态分布
学 生 t-分布可以用于检测样本的平均值,下面我们从一个期望值为1 的正态分布的随机变
量中取30个数值:
n = 30
n p .r a n d o m .s e e d (42)
零假设
在统计学中,零假设或虚无假设(null hypothesis)是做统计检验时的一类假设。零假设的内
容一般是希望被证明为错误的假设或是需要着重考虑的假设。
t = (np.mean(s) - 0.5) / (np.std(s, ddof=l) / np.sqrt(n))
print s t a t s .t t e s t _ l s a m p (s , 0.5)
下而我们检验期望值是杏为1.0,由于 p 值大于0.05,我们不能推翻期望值为1.0的零假设,
但这并不意味着可以接受该假设,因为期望值为0.9的假设对应的p 值也大于0.05,甚 至 比 1.0
的 t 值还大。
-1.14501736704
s c ip y数
能成立。反过来当 p 值较大吋,则不能推翻零假设,注意不能推翻假设并不代表能接受该假设。
—值 计 算 库
阁3-20红色部分为ttest_ lsamp〇计算的p 值
拿上面期望值为0.5的测试为例:如果整体样本的期望值真的是0.5,那么抽取到 t 值大于
2.65858或小于-2.65858的样本的概率为0.0126377。由于这个概率太小,因此整体样本的期望值
应该不是0.5。也可以这样理解:如果整体样本的期望值为0.5,那么随机抽収比 s 更极端的样
本的概率为0.0126377。
x = np.linspace(-5, 5^ 500)
y = stats.t(n-l).pdf(x)
pit.plot(x, y, lw=2)
p = s t a t s .t t e s t _ l s a m p (s , 0.5)
49
Python 科学计算 (第 2 版)
下 面 用 scipy.integrate.trapzO积分验证 p 值 ,由于左右两块红色而积是相等的,下面的积分
只需要计算其中一块的面积:
极端组出现的概率,可以看到它和 p 值是相同的。
m = 200000
mean =0.5
r = stats.norm.rvs(loc=mean, scale=0.8, size=(m_» n))
ts = (np.mean(s) - mean) / (np.std(s> ddof=l) / np.sqrt(n))
tr = (np.mean(r> axis=l) - mean) / (np.std(r> ddof=l^ axis=l) / np.sqrt(n))
np.mean(np.abs(tr) > np.abs(ts))
0.012695
如 果 si 和 s2 是两个独立的来自正态分布总体的样本,可以通过 ttest_ind()检验这两个总体
的均值是否存在差异。通 过 equal_var参数指定两个总体的方差是否相同。在下面的例子中, O
由于 si 和 s2 样本来自不同方差的总体,因此 eqiial_var参数为 False。由于p < 0 . 0 5 , 因此认为
两个总体的均值存在差异。© s 2 和 S3 来自相同方差的总体,因 此 equaLvar 参数为 True, 所得
到 的 p 值很大,因此无法推翻零假设,也就无法否定两个总体的均值相同的零假设。
np.random.seed(42)
(-0.59466985218561719, 0 .5 5518058758105393)
3 . 4 . 6 卡方分布和卡方检验
a = n p . random.normal(size:(300000, 4))
cs = np.sum(a**2, axis=l)
s c ip y数
—值 计 算 库
图 3-21使用随机数验证卡方分布
片方分布可以用来描述这样的概率现象:袋子里有5 种颜色的球,
杣到每种球的概率相同,
从中选 N 次,并统计每利_ 色的次数〇i 。
则下而的X2符合自由度为4 的卡方分布,
其中E = N /5为
每种球被杣选的期望次数:
2 V ( ° i ~ E)2
x = Z - T —
i=l
repeat_count = 60000
n, k = 100, 5
np.r a n d o m. s e e d (42)
ball_ids = np.random.randint(0, k, s i z e = (repeat_count^ n)) O
counts = n p .app l y _ a l on g _ a x i s (n p .bi n c o u n t , 1, ball_ids, minlength=k) ©
pl.xlim(0, 10)
s c ip y数
—值 计 算 库
图3 - 2 2 模拟卡方分布
卡方检验可以用来评估观测值与现论值的差异是否只是因为随机误差造成的。在下面的例
子中,袋子中各种颜色的球按照probabilities参数指定的概率分布,choose_balls(probabilities,
size)
从袋中选择 size 次并返回每种球被选中的次数。袋 子 1 中的球的概率分布为:0.18、0.24、0.25、
0.16、0.17。袋 子 2 中各种颜色的球的个数一样多。通过调用 choose_balls()得到两组数字:809397
6466和 89 767971 85。现在需要判断袋子中的球是否是平均分布的。
def choose_balls(probabilities, s i z e ) :
r = st a t s . r v _d i s c r e t e ( v a l u e s =( r a n g e ( l e n ( p r o b ab i l i t i e s ) L probabilities))
s = r.rvs(size=size)
counts = np.bincount(s)
return counts
n p .r a n d o m .s e e d (42)
countsl = choose— balls([0.18, 0.24, 0.25, 0.16, 0.17], 400)
countsl counts2
[80, 93, 97, 64, 66] [89, 76, 79, 71, 85]
使 用 chisqua^ O进行卡方检验,它的参数为每利镩被选中次数的列表,如果没有设置检验
的囚标概率,就测试它们是否符合T 均分布。卡方检验的苓假设为样本符合丨3标概率,由下面
的检验结果可知,第一个袋子对应的 p 值只有0.02,也就是说如果第一个袋子中的球真的符合
平均分布,那么得到的观测结果80 93 97 64 6 6 的概率只有2 % , 因此可以推翻零假设,即袋子
中的球不太可能是平均分布的。第二个袋子对应的 p 值 为 0.64,无法推翻零假设,即我们的结
论是不能否定第二个袋子中的球是平均分布的。注意,和前面介绍的 t 检验一样,零假设只能
用来否定,因此不能根据观测结果89 767971 85得出袋子中的球是符合平均分布的结论。
chil, pi = stats.chisquare(countsl)
chi2, p2 = stats.chisquare(counts2)
卡方检验是通过卡方分布进行计兑的。
图3-23显示自由度为4 的卡方分布的概率密度函数,
以及 c h i l 和 chi2 对应的位置x 〖
和x i 。P i 是X〖
右侧部分的面积,而P 2是X i 右侧的面积。
3-23卡方检验计算的概率为阴影部分的面积
卡方检验还可以用于二维数据。前面介绍的彩色球的例子中,只是按照球的颜色分组,而
二维数据则按照样本的两个属性分组,统计学上称之为列联表。例如下面是性别与惯用手的列
联表,我们希望知道这个统计结果能否说明性别与惯用手之间存在某利1联系。
右手 左手
男性 43 9
女性 44 4
下面使用 dii 2_C〇
mingenCy 〇对列联表进行卡方检验,零假设为性别与惯用手之间不存在联
Python 科学计算 (第 2 版)
系,即男性与女性惯用左右手的概率相同。由 于 p 值 为 0.3,因此不能推翻;假设,即该实验
数据中没有明显证据表明男性和女性在使阳左右手习惯上存在区別。
chi2 p
1.0724852071005921 0.30038477039056899
stats.fisher_exact(table)
(0.43434343434343436, 0 .23915695682225618)
3.5 数值积分-integrate
S c iP y数
—值 计 算 库
积分。
3 . 5 . 1 球的体积
数值积分是对定积分的数值求解,例如可以利用数值积分计兑某个形状的面积。让我们先
考虑一下如何计算半径为1 的半圆的面积。根据圆的面积公式,其面积应该等于71/2。单位半
圆的曲线方程为y = V l —x 2, 可以通过下面的 half_circle〇进行计算:
def h a l f _ c i r c l e ( x ) :
return (l-x**2)**0.5
最简单的数值积分兑法就是将要积分的面积分为许多小矩形,然后计兑这些矩形的面积之
和。下面使川这种方法,将 X 轴上-1到 1 的 区 间 分 为 1 _ 等 份 ,然后计算面积和:
N = 10000
x = np.linspace(-l, 1, N)
dx = x[l] - x[0]
y = half_circle(x)
2 * dx * np.sum(y) # 而积的两倍
3.1415893269307373
也可以用 N um Py 的 trapz()计算半圆上的各点所构成的多边形的l l 丨积,
trapz()计萬的是以(x ,y )
为顶点坐标的折线与X 轴所夹的面积:
np.trapz(y, x) * 2 # 而积的两倍
3.1415893269315975
勿I果使用 integrate.quad()进行数值积分,就能得到非常精确的结果:
pi_half * 2
3.141592653589797
计算多重走积分可以通过多次调用quad〇实现,
为了调用方便,
integrate模块提供了 dblquad()
进行二重定积分,
提供了 tplquad()进行三重定积分。
下面以计17:单位半球体积为例,
说明 dblquad()
的用法。
s c ip y数
单位半球面上的点(x ,y ,z )满兄方程x 2 + y 2 + z 2 = 1 ,因 此 下 面 的 half_sphere〇可以通过
X -Y 轴坐标计算球面上的点的Z 轴坐标值:
—值 计 算 库
def half_sphere(x, y ) :
return (l-x**2-y**2)**0.5
X -Y 轴平面与此球体的交线为一个单位[Ml,因此二重积分的计f i :
区间为此单位_ 。即对于
X 轴从- 1 到 1进行积分,而对于 Y 轴则从-half_circle(x )到 half_circle(x )进行积分。因此半球体积
的二重积分公式为:
Vi -x2 __________
n ___ J l — x 2 — y 2dy dx
V l - x2
下而的程序使用 dblqimd()计算半球体积:
dblquad〇的调用参数为:dblquad(ftmc2d,
a^b ,
gflin,
hfim)〇其中,flinc2d 是需要进行二重积分
的闲数,它有两个参数,假设分别为 x 和 y 。a 和 b 参数指定被积分闲数的第一个变量x 的积分
区间,而 gfun 和 hfun 参数指定第二个变董y 的积分区间。gflin 和 hfun 是闲数,它们通过变量 x
计算出变S y 的积分区间,这样可以对 X -Y 平而上的任何区间对func2d 进行积分。
阁 3-24是半球体积的积分的示意阁。从此示意图可以看出,X 轴的积分区间为-1.0到 1.0,
对 于 X 轴上的某点 xO, 通过对 Y 轴的积分可以计算出图中深色的垂直切面的面积,因 此 Y 轴
Python科学计算 (第 2 版)
的积分区间如图中的点线所示。
阁3-24半球体二茁积分示意阁
3 . 5 . 2 解常微分方程组
integrate模块还提供了对常微分方程组进行积分的函数odeintO。下面我们看看如何用它计
S c iP y数
算洛伦茨吸引子的轨迹。洛伦茨吸引子由下面的三个微分方程定义:
- 值计算库
dx dy dz
— = a • (y - x ); — = x • (p - z ) - y , — = x y - pz
这三个方程定义了三维空间中各个坐标点上的速度矢量。从某个坐标开始沿着速度矢量进
行积分,就可以计算出无质量点在此空间中的运动轨迹。其中a 、p 、(
3为三个常数,不同的参
数可以计算出不同的运动轨迹:x (t )、y (t )、z (t )。当参数为某些值时,轨迹出现混沌现象。即
微小的初值差别也会显著地影响运动轨迹。下而是洛伦茨吸引子的轨迹计算和绘制程序。
O 程序中先定义一个函数 lorenz(),它的任务是计算出某个坐标点的各个方向上的微分值,
它可以直接根据洛伦茨吸引子的公式得出。
©€)使用不同的位移初始值两次调用odeintO, 对微分方程求解。oddnt()有许多参数,这里
用到的4 个参数分别为:
• lorenz: 它是计算某个位置上的各个方向的速度的函数。
• (0.0,1.0,0.0):位置初始值,它是计算常微分方程所需的各个变量的初始值。
• t : 表示时间的数纽,odeintO对此数组中的每个时间点进行求解,得出所 W 时间点的
位置。
• args: 这些参数直接传递给lorenzO, 冈此它们在整个积分过程中都是常量。
图 3-25显示了 odeintO所得到的轨迹。由结果可知,即使初始值只相差0.01,两条运动轨
迹也是完全不同的。
http://bzhang.lamost.org/website/archives/lorenz_attactor
O 洛伦茨吸引子的详细介绍。
from scipy.integrate import odeint
import numpy as np
def lorenz(w, t, p, r, b ) : O
# 给 出 位 置 矢 量 w 和三个 参 数 p、 r、 b
# 计 算 出 d x / d t 、 d y / d t 、 dz/dt 的 值
x, y, z = w.tolist()
# 直 接 与 lorenz的 计 算 公 式 对 应
return p*(y-x), x*(r-z)-y, x*y-b*z
# 调 用 o d e 对 lorenz进 行 求 解 , 用 两 个 不 同 的 初 始 值
图 3-25洛 伦 茨 吸 引 了 • : 微 小 的 初 值 斧 别 也 会 M 著 地 影 响 运 动 轨 迹
3.5.3 ode 类
阁 3-26质 弹 黄 - 阻 尼 系 统
Python科学计算 (第 2 版)
该系统的微分万程为:m 5i + bk + kx = F 。其 中 x 为滑块的位移,父为位移对时间的二次
导数,即滑块的加速度,i 为滑块的速度,m 为滑块的质量,b 为阻尼系数,k 为弹簧的系数,
F 为外部施加于滑块的控制力。这是一个二次微分方程,为了使用 o d e 对系统求解,需要将其
转换成如下一阶微分方程组:
x u , u = (F —kx —bu )/m
其 中 X 为滑块的位移,U 为滑块的速度。这两个变S 构成了系统的状态,它们对时间的导
数可以通过这两个方程直接算出。
def m a ss_spring_damper(xu> t, m, k, b, F ) :
X, u = xu.tolist()
dx = u
du = (F - k*x - b*u)/m
return dx^ du
04 位移
0.5 2.
通度
0.5 1.0 .5 2.
图3-27滑块的速度和位移曲线
一样,也需要一个计算各个状态的导数的函数。
〇这里使用 MassSpringDamper炎的方法 f ()计算状态点处的导数。注意该方法的参数顺序和
odeint()所需的函数 mass_spring_damper〇不 M ,第一个参数为时间,第二个参数为系统状态。并
且该方法必须返冋•个列表来表示各个状态的导数,
不能返M 元组。
这里使用 MassSpringDamper
类将系统的各个参数M 、k 、b 以及 F 等包装在对象内部。
© 创 建 od e 对象之后,通 过 set_ integrator〇设置积分器相关的参数。它的第一个参数为积分
器的算法,其后的关键字参数设置该积分器算法的各个参数。关于各个参数的具体含义请读者
参 阅 od e 类的文档说明。然后调用 seUnitiaLvalueO 设置系统的初始状态和初始时间。
© 在 w hile 循环中,以 d t 为间隔对系统进行积分求解。o d e 对象的属性 r.t 为当前的模拟吋
间,调 用 r.integmte (r.t + dt)计 算 r.t + d t 处的状态。系统的状态保存在 r.y 中。我们用两个列表 t
和 result分别保存模拟时间和系统状态。
由 allclose〇的比较结果可知使用o d e 的结果与 odeint〇的完全相同。
class M a s s S p r i n g D a m p e r ( o b j e c t ) : O
s c ip y数
self為 self.k, self.b, self.F = m, k, b, F
—值 计 算 库
def f(self, xu):
Xj u = xu.tolist()
dx = u
dt = 0.01
r = o d e (system.f) ©
r •set 一i n t e g r a t or (•v o d e 、 m e t h o d = •b d f •)
r.set 一initial 一
value(init 一status, 0)
t =[]
result2 = [init 一status]
r.integrate(r.t + dt)
t.append(r.t)
r e s u l t 2 .a p p e n d (r .y )
result2 = np.array(result2)
n p .allclose(result, result2)
True
9
5
Python 科学计算(第2 版)
p K re{f)-
1*
t
I
•
D Mi)
人 ^ 一df
IT "
图3-28 P I D 控制系统
s c ip y数
class PID(object):
def update(selfj e r r o r ) :
p = self.kp * error
i = self.ki * self.status
if self.last_error is None:
d = 0.0
else:
d = self.kd * (error - self,last_error) / self.dt
return p + i + d
下而的程序使用 P ID 控制器对系统进行控制,让滑块更快地停在位移1.0处,为了后续调
用优化丁具自动搜索合适的P ID 参数,
这M 将整个系统模拟用函数pid_control_system()封装起来,
函数的参数为 P ID 控制器的三个参数。
程序的基本构造与前面的无控制的程序相同,只是增加了 P ID 控制器方面的运算:O 计算
13标 位 置 1.0与当前位置之间的误差,©使用该误差更新 P ID 控制器,获得控制器的输出 F , €)
更新0 标系统中的控制力。
由程序的输出可知, l:t |于 P1D 控制器的控制,系统在两秒之内就已经停在了位移1.0处,
如 图 3-29所示。
s c ip y数
system(kp, ki, kd, dt, t a r g e t = 1 . 0 ) :
—值 计 算 库
pid = PID(kp, kij kd, dt)
init_status = 0.0, 0.0
r = ode(system.f)
t = [0]
result = [init_status]
F_arr = [0]
system.F = F ©
t.append(r.t)
r e s u l t .a p p e n d (r .y )
F_arr.append(F)
result = np.array(result)
t = np.array(t)
F_arr = np.array(F_arr)
return t, F_arr, result
Python 科学计算(第 2 版)
控制力的终值:19.9434046839
1.2
1.0
0.8
0.6
0.4
0.2
0.0
0.0 0.5 1.0 1.5 2 .(
速度
s c ip y数
控制力
图3 - 2 9 使用P I D 控制器让滑块停在位移1.0处
通 过 调 节 P I D 控制器的三个参数可以获得最佳的控制效果,这里我们使用前而介绍过的
optimize 库中的函数自动寻找最优的 P I D 参数。为了使用最优化函数,需要编写一个对控制结
果进行评价的函数。由于我们的目标是让滑块尽快地停在位移1.0处,因此可以用前两秒钟滑
块位移与0 标位移差的绝对值之和作为控制结果的评价,该值越小衮示控制得越好。为了让最
优化运行得快-些,这里将控制器的吋间间隔改为0.01秒。
%%time
def e v a l _ f u n c ( k ) :
kp, ki, kd = k
return n p . s u m ( n p . a b s ( r e s u l t [ 0] - 1.0))
kwargs = {"method":"L-BFGS-B",
niter=10,
minimizer_ k w a r g s = k w a r g s )
print opt_k.x
下而使用优化器的输出作为P ID 的参数对系统进行模拟,可以看到控制幵始0.5秒之后滑
块已经■本上稳定在了位移1.0处,如 图 3-30所示。
x, u = result[idx]
s c ip y数
print "t={}, x={:g}, u={:g}".format(t[idx], x, u)
—值 计 算 库
t=0.5, x=0.979592, u=0.00828481
丨
冬I 3 - 3 0 优化P I D 的参数以降低控制响应时间
63
P y th o n 科学计算(第2 版)
3 . 6 信号处理-sig n a l
波器设计等方面的内容。
3 . 6 . 1 中值滤波
中值滤波能够比较有效地消除声音信号中的瞬间噪声或者阁像中的斑点噪声。在 signal模
块中,
medfilt()对 一 维信号进行中值滤波,
而 medfilt2d〇对二维信号进行中值滤波。
在 scipy.ndimage
模块中另有针对多维图像的中值滤波器,这里简单演示 medfiltO的效果。
s c ip y数
x = np.sin(t)
—值 计 算 库
x2 = signal.medfilt(x> 5) ©
p i .l e g e n d (l o c = "b e s t ")
True
〇酋先创建-•个带有随机的瞬间噪声的正弦波,© 然 后 调 用 medfiltO进行屮值滤波,第二
个参数为计中值的窗口大小,它必须是一个奇数。medfiltO将信号中的每个元素都替换为其窗
口内的中值。
最后绘制原始信号和滤波信号,为了便于比较,图中将滤波之后的信号统一向上偏移了 0.5,
结果如图3-31所示。中值滤波是排序滤波的一个特例。使用排序滤波可以将元素替换为芄窗U
内指定排序顺序的元素。其调用形式如下:
3 . 6 . 2 滤波器设计
signal模块提供了许多滤波器设计的函数。
在下面的实例中,
我们设计一个 IIR 带通滤波器,
s c ip y数
并查看其频率响应,最后使用它对频率扫描信号进行滤波计算。
—值 计 算 库
sampling_rate = 8000.0
f t 设计一个带通滤波器:
# 通带为 0.2*4000 - 0.5*4000
# 阻带为<0.1*4000, >0.6*4000
# 通带增益的最大衰减值为2dB
# 阻带的最小衰减值为40dB
b, a = signal. i ir d e s i g n ( [0.2, 0.5], [0.1, 0.6]., 2, 40) O
# 使用f r e q 计算滤波器的频率响应
Wj h = signal.freqz(b, a) ©
# 计算增益
power = 20*np.logl0(np.clip(np.abs(h)> le-8, lel00)) ©
65
Python 科学计算(第2 版)
# 产生两秒钟的取样频率为sampling_rate H z 的频率扫描信号
# 开始频率为0 ,结束频率为sampling_rate/2
t = np.arange(0, 2, 1/sampling 一rate) O
# 对频率扫描信号进行滤波
out = signal.Ifi!ter(b, a, sweep) 0
# 将波形转换为能ffl
out = 20*np.logl0(np.abs(out)) O
# 找到所有周部最大值的下标
index = signal.argrelmax(out^ order=3) ©
# 绘制滤波之后的波形的增益
s c ip y数
pl.figure(figsize=(8, 2.5))
〇为了调州 chhpO产生频率扫描波形的数据,首先需要产生一个表示取样时间的等差数姐,
这里产生两秒的取样频率为8k H z 的取样吋间数组。© 然后调州 chirpO得到两秒的频率扫描波形
的数据。频率扫描波的幵始频率fi)为 0H z ,结束频率 f l 为 4kH z ,到达4k H z 的时间为两秒,使
用数组 t 作为取样时间点。©最 后 调 用 lfilteK)计算频率扫描波形经过带通滤波器之后的结果。
〇为了和系统的增益特性图进行比较,需要获取输出波形的包络,因此先将输出波形数据
转换为能£1:
值。© 为了计算包络,调用 argrelmax〇找 到 out 数组中所有局部最大值的下标,order
参数指定局域最大值的范围,这里的值为3 表示所有的局域最大值都是连续7 个元素(前后各三
个元素)中的最大值。©最后将时间转换为对应的频率,绘制所有局部最大点的能量值。
图 3-32 S 示了 freqzO计算的频谱和频率扫描波得到的频率特性,可以看到结果是一致的。
!)屋旳滤波器频谓
图3 - 3 2 用频率丨描波测S 的频率响应
3 . 6 . 3 连续时间线性系统
在上一节中,我们使)|j odeintO对质量-弹簧-阻尼系统的微分方程组进行了数值积分,并且
进行了 P ID 控制模拟。该系统的微分方程为: mX + bk + kx = f 。通过拉普拉斯变换可以将微
分方程化为容易求解的代数方程:m s 2X (s ) + bsX (s ) + kX (s ) = F (s )。其 中 F (s)是f (t )的拉普拉
斯变换,X (s :
)是 的 拉 普 拉 数 变 换 ,而n 次微分变成了s n 。FCs)是输入信号,而X (X )是输出信
号,将等式改写为输入除以输出的形式,就得到了系统的传递函数PCs):
X (s ) 1
P (s ) = --- = ------------
F (s ) m s 2 + bs + k
连续时间系统的传递函数是两个s 的多项式的商。通过连续吋间系统的传递函数,很容易
计錄某输入信号对应的输出信号。在下面的例子屮,使 用 signal模块计兑质量-弹黄-阻尼系统对
阶跃信号以及正弦波信号的响应输出。〇创 建 lti 对象,可以使用控制理论中的多种形式表示连
续 时 间 线 性 系 统 ,这 里 使 用 的 是 传 递 函 数 分 子 和 分 母 多 项 式 的 系 数 。多项式的系数与
num py.polyld 的约定相同,即下标为0 的元素是最高次项的系数。© 调 用 lti.step〇方法计算系统
的阶跃响应。
T 参数为计算响应的时间数组。© 调 用 signal.lsim〇计算系统对正弦波信号的响应,
s c ip y数
它的第一个参数为 lti 对象,也可以直接传递(numerator,denominator)。 U 参数为保存输入信号的
—值 计 算 库
数组。step()和 lsim〇计算结果中的第二项为系统的输出信号,这里忽略其余的输出。
图 3-33显示阶跃响应最终稳定在x =0.05处,这时的 kx = l 。
阶跃嗚应
正弦波响应
).5 1.0
I 间 (秒
图 3-33系统的阶跃响应和正弦波响应
m, b, k = 1.0, 10, 20
numerator = [1]
denominator = [m, b, k]
67
Python科学计算 (第 2 版)
t = np.arange(0, 2, 0.01)
x_step = plant.step(T=t) O
传递函数的代数运算可以表示由多个连续时间系统组成的系统,例如两个系统的级联的传
递函数是各个系统的传递函数的乘积。而传递函数由分子和分母两个多项式构成,因此传递函
数的叫则运篇可以使用 N u m P y 的 p o ly ld 相关的函数实现。下 面 的 S Y S 类通过定义_ mul_ 、
_ add_ 、_ sadd_ 、_ div_ 等魔法方法,让它支持四则运爲。
图3 - 3 4 反馈控制系统框图
s c ip y数
OfeedbackO 方法计算与之对应的反馈系统的传递函数。
在 图 3-34中,
P 是被控制的系统,
C是
控制器,C 的输入信号是F1标信号与实际输入的差〜- X 。 我们从XlJ ijx 的传递函数就是这个反
—值 计 算 库
馈系统的传递函数。根椐阁示可以列出如下拉普拉斯变换之后的代数方程:
X (s ) = (Xr (s ) - X (s )) •C (s ) •P (s )
整理可得:
X (s ) = C (s ) • P (s )
Xr (s ) 一 1 + C (s ) • P (s )
def as_sys(s):
if isinstance(s, R e a l ) :
return SYS([s], [1])
return s
class S Y S ( o b j e c t ) :
def _ init_ (self, num, d e n ) :
self.num = num
self.den = den
def f e e d b a c k ( s e l f ) : O
return s e lf / ( s e lf + 1 )
s = as_sys(s)
num = n p .p o l y m u l (s e l f .num, s.num)
s = as_sys(s)
den = n p .p o l y m u l (s e l f .d e n , s.den)
num = n p .p o l y a d d (n p .p o l y m u l (s e l f .num, s.den),
np.polymul(s.num, self.den))
M, b, k = 1.0, 10, 20
for pi_setting in p i _ s e t t i n g s :
ax.plot(t^ Xj label=label)
ax •set_xlabel (u •_时间(秒)")
ax •set_ylabel (u __位移(米)" )
s c ip y数
—值 计 算 库
时阇(杪〉
图3 _ 3 5 使用PI 控制器的控制系统的阶跃响J、
V:
为了讣箅施加于质量的控制力,可以将误差信号传递给lsim〇计兑控制器的输出:
为 了 让 P I D 控制器的输出在限定的范围之内,可以在反馈系统之前添加一个低通滤波器,
70
一阶低通滤波器的传递函数为: 添加低通滤波器之后,P I D 控制器的输入就是连续信号
a.s+l
了,如 图 3-36所示。
图 3-36带低通滤波器的反馈控制系统框图
x3 = signal.step(lp_feedback, T=t)
由 于 P I D 控制器的传递闲数的分子阶数高于分母阶数,因此无法使用 lsim()计算。我们可
s c ip y数
以把•当作系统的输入,把 f 当作输出,通过下面的方程计算从x ^ j f 的传递闲数:
—值 计 算 库
F(s) = (Xr (s) • LP(s) - F(s) • P(s)) • C(s)
得到的传递函数为:
F (s ) _ C (s ) •LP (s )
Xr (s ) = C (s ) •P (s ) + 1
下面根据上面的公式计算带低通滤波器的控制系统中控制器的输出:
f3 = signal.step(pid_out, T=t)
目标系统的位移
图 3-37泔块的位移以及控制力
Python科学计算 (第 2 版)
插值是通过已知的离散数据求未知数据的方法。与拟合不同的是,要求曲线通过所有的已
知数据。S d P y 的 interpolate模块提供了许多对数据进行插值运算的函数。
3.7.1 一维插值
一维数据的插值运算可以通过inteipld ()完成。其调用形式如下,它实际上不是函数而是一
个类:
它给出插值的 B 样条曲线的阶数,可以有如下候选值:
—值 计 算 库
• hero'、’
nearcst’
:阶梯插值,相当于0 阶 B 样条曲线。
• 'slinear’
、’linear^ 线性插值,用一条直线连接所有的取样点,相当于一阶 B 样条|11|线,
’
slinear’
使用扩展库中的相关函数计算,
而linear’
则直接使用 Python 编写的函数进行运算,
其结果一样。
• Quadratic'、’
cubic’
:二阶和三阶 B 样条曲线,更高阶的丨11|线可以直接使用整数值指
iito p ld 对象可以计兑 x 的収值范围之内任意点的函数值。它可以像函数一样直接调用,和
N um Py 的 ufunc 函数-样能对数组中的每个元素进行计箅,并返冋一个新的数组。
下面的程序演示了 kin d 参数以及与其对应的插值曲线,结果如图3-38所示。程序中我们
使用循环对相同的数据进行4 种不同阶数的插值运兑。O 首先使用数据点创建一个interpld对象
f ,通 过 kin d 参数指定其阶数。© 调 用 f 〇计算出一系列的插值结果。本例中,决定插值曲线的
数据点一共有11个,插值之后的曲线数据点有101个。
高 次 interpld ()插值的运算量很大,因此对于点数较多的数据,建议使用后面介绍的
A Uni variateSpline()。
y = np.sin(x)
xnew = np.linspace(0, 10, 101)
p l . p l o t ( x , y , 'n o ' )
for kind in ['nearest', 'zero', 'slinear', 'quadratic']:
f = interpolate.interpld(x,y,kind=kind) O
y n e w = f(xnew) O
pi.legend(loc='lower right')
•s
.0 -
*5〇 2
图3-38 intcrpld的各阶插值
1.外推和 Spline 拟合
能够使得样条曲线接近无噪声时的波形,可以把它看作使用样条曲线对数据进行拟合运算。
sy2 = spline2(sx2)
pl.figure(figsize=(8, 5))
p i . s u b p l o t (211)
p i . l e g e n d ()
—值 计 算 库
p i . s u b p l o t (212)
p i . l e g e n d ()
o 5 10 15 20
图3 - 3 9 使用UnivariateSpline进行插值:夕卜推(上)和数据拟合(下)
当曲线为三阶曲线时,UnivariateSpline.rootsO可 以 于 计 : 曲 线 与 y = 0 的交点横坐标。下
面显示了图3-39(下)中的曲线与X 轴 的 6 个交点的横坐标:
如果需要计算丨[丨1线与任意横线的交点,可以事先将曲线的拟合数据在Y 轴方向上进行平移。
但若要计算与多条 y = c 横线的交点,则需要对同样的数据进行平移和拟合多次。
O 下 而 的 r〇
ot_ at()通过直接修改拟合曲线的参数,实现曲线的平移,从而可以计算与任意
横线的交点。© — r〇
〇ts_ at()动态添加为 UnivariateSpline炎的方法。© 对多条横线求交点,并进
行绘图,其结果如阁3^40所示。
coeff = self.get_coeffs()
coeff -= v
try:
s c ip y数
root = s e l f . r o o ts ()
—值 计 算 库
return root
finally:
coeff += v
interpolate.UnivariateSpline.roots_at = roots_at ©
ax = pl.gca()
xn = spline2.roots_at(level) ©
pl.plot(xr) spline2(xr), "ro")
l.Sr
i.o-
).5
).〇
).5
1.0 ■
1.5 ■
0
图3 ~ 4 0 计算Spline与水平线的交点
75
Python 科学计算 (第 2 版)
2.参数插值
pl.plot(x, y, "〇")
p i . l e g e n d ()
N
s.oo
图3 4 1 使用参数插值连接二维平而h 的点
3.单调插值
前面介绍的几种插值方法不能保证数据点的单调性,即曲线的最值可能出现在数据点之外
的地方。Pchiplnterpolator类(别名 pchip)使 单 调 三 次 插 值 ,能够保证丨1丨]线的所有最值都出现在
数据点之上。下面的程序用 pchip〇对数据点进行插值,并绘制其一阶导数曲线,由图3^42的导
数曲线可知,所有最值点处的导数都为0。
X = [0, 1, 2, 3, 4, 5]
ys = curve(xs)
dys = curve.derivative(xs)
s c ip y数
p i .l e g e n d (lo c = " b e s t ")
p i . g r i d ()
—值 计 算 库
pl.margins(0.1j 0.1)
图3 * 4 2 单调插值能保证两个点之间的III丨线为单调递增或速减
3 . 7 . 2 多维插值
使 用 interp2d()可以进行二维插值运算。它的调用形式如下:
interp2d(Xj y, z, kind='linear', … )
其 中 x 、y 、z 都是一维数组,如果传入的是多维数组,则先将其转为一维数组。kind 参数
指定捕值运算的阶数,可以为'linear’
、’cubic'、’
quintic'。
下面的例子对某个函数曲面上的网格点进行二维插值,效果如图3 4 3 所示。其中左图显示
插值之前的数据,而右图显示插值运算后得到的结果。
77
Python 科学计算(第2 版)
# X - Y 轴分为1 5 * 1 5 的网格
y ,x = np.mgrid[-l:l:15j, -l:l:15j] ©
# 二维插值
newfunc = interpolate.interp2d(x, y, fvals^ k i n d = ' c u b i c •) ©
# 计箅1 0 0 * 1 0 0 的网格上的插值
xnew = n p .li n s p a c e ( -1,1,100)
y n e w = n p .li n s p a c e ( -1,1,100)
fnew = newfunc(xnew, ynew) O
Fvals fnew
s c ip y数
—值 计 算 库
图3 4 3 使用inteip2d类进行二维插值
1•griddata
interp2d 类只能对网格形状的取样值进行插值运算,如果需要对随机散列的取样点进行插
值,则可以使用 griddataO。其调用形式如下:
其 中 points表 示 K 维空间中的坐标,它可以是形状为(N ,
k)的数组,也可以是一个有 k 个数
组的序列,N 为数据的点数。
values 是 points 中每个点对应的值。
;ci 是需要进行插值运算的坐标,
其形状为(M ,k )。method 有三个选项---*’
nearest’
、’linear’
、’cubic’
,分別对feZ 0 阶、1 阶以及3 阶
插值。
下面是 griddataO的演示程序,其输出如图3^44所示。左图与’
nearest^?法对应,平面上每个
点都被填充为与它最近的采样点的数据,因此图中由许多相同颜色的色块构成。’
linear’
和’cubic’
算法只对采样点构成的凸包区域进行插值,区域之外采用出1_^1此进行填充。中图和右图中的
白色区域为插值的凸包区域之外。
griddata〇使用欧几里得距离计算插值。
如果 K 维空间中每个维度的取值范围相差较大,
A 则应先将数据正规化,然后使用 griddataO进行插值运算。
# 计算随机N 个点的坐标,以及这些点对应的函数值
N = 200
n p .r a n d o m .s e e d (42)
s c ip y数
x = np.random.uniform(-l, 1, N)
—值 计 算 库
y = np.random.uniform(-lJ l y N)
z = func(x, y)
.0
人 乂 O。 抑。。 、
了。
。V 、8 。文 。久V 令 J
o
o 〇
€> 0
客 W
o
o
。
° 〇 €) 〇«
o 0
P 0
o
〇 <b 0 〇〇
0〇
〇 i8 CD
.5
> 〇没®
o
o o 0 〇
〇 O
° ^ °
〇«〇
〇〇° 。7 CO
0
〇〇°
n 〇 O •0 H 於 没 X r : 。 ^
•1i) -0.5 0.0 0.S 1. 1.0 -dS 00 0.S 1.0
inear cube
图3 4 4 使用gridata进行二维插值
2.径向基函数插值
79
Python 科学计算(第2 版)
坐标。使州这些中函数,可以近似表示 N 维空间中的函数:
N
y (x ) = ^ W i c|)(|| X - X i ||)
i=l
丨
冬I3 4 5 — 维 R B F 插值
下面的程序演示二维径向基函数插值,效果如图3 4 6 所示。
图3 > 4 6 二维径向雄函数插值
某些径向蕋函数可以通过epsilon 参数指定艽作用范围,该值越大每个插值点的作川范围越
广,所得到的曲面也就越平滑。下面的代码演示 gaussian 径向蕋函数的 epsilon 参数与插值结果
的关系,效果如图3 4 7 所示。
s c ip y数
epsilons = 0 . 1 ,0 . 1 5 ,0.3
rbfs = [Rbf(x, y, z, function="gaussian", epsilon=eps) for eps in epsilons]
—值 计 算 库
zgs = [rbf(xg^ yg).reshape(xg.shape) for rbf in rbfs]
0
o€
5
I
o
o
c
0
^ J A C
-
。
v
<6
^ 一^
〇
r
〇
s
l
x
”
H
o
0 0^00
©
v
r
o
s
00
」
000
i
c
。。
oo
$
o
o
k
•1.0 -as OlO 05 1.0 -10 -0.3 00 O.S i.o o.e o.s i.o
epi*01 cpi«0.1S eps*0J
图3 4 7 epsilon参数指定径向丛函数中数据点的作用范_丨
在科学与工程领域求解线性模型时经常出现许多大型的矩阵,这些矩阵中大部分的元素都
为 0 , 被称为稀疏矩阵。用 N u m P y 的 ndam iy 数组保存这样的矩阵会很浪费内存,由于矩阵的
181
Python 科学计算 (第 2 版)
稀疏特性,可以通过只保存非零元素的相关信息,从而节约内存的使用。此外,针对这种特殊
结构的矩阵设计运箅函数,也可以提高矩阵的运箅速度。
scipy.sparse 中提供了多种表示稀疏矩陈的格式,scipy .sparse.linalg 提供了对这些矩昨进行线
性代数运算的闲数,scipy.sparse.csgraph 提供对用稀疏矩阵表示的图进行搜索的闲数。本节首先
介绍表示稀疏矩阵的各种格式,然后介绍如何使用 csgm p h 中的闲数搜索最佳路径,而在本书
最后一章中会介绍使用稀矩阵的线性代数运算闲数解决实际问题的例子。
3 . 8 . 1 稀疏矩阵的存储形式
素(行,
列)信息的元组,其对应的值为矩阵中位于(行,
列)中的元素值。显然字典格式的稀疏矩阵
很适合单个元素的添加、删除和存取操作。通常用来逐个添加非零元素,然后转换成K 他支持
快速运算的格式。
s c ip y数
a = s p arse.dok_matrix((10, 5))
在的列。这种格式也很适合逐个添加元素,并且能快速获取行相关的数据。
b = s p arse.lil_matrix((10, 5))
b[2, 3] = 1.0
b[3, 4] = 2.0
b[3, 2] = 3.0
print b.data
print b.rows
[[][][3] [2, 4 ] [ ] [ ] [ ] [ ] [ ] [ ] ]
row = [2_> 3, 3, 2]
col = [3, 4, 2, 3]
data = [1, 2, 3, 10]
c = sparse.coo_matrix((data, (row, col)), shape=(5, 6))
print c.col, c .row, c.data
print c.toarnay()
[B 4 2 3] [2 B 3 2] [ 1 2 3 10]
[[ 0 0 0 0 0 0]
[ 00 0 0 0 0]
[0 0 0 11 0 0]
s c ip y数
[ 00 3 0 2 0]
[ 00 0 0 0 0]]
—值 计 算 库
3 . 8 . 2 最短路径
j 之间没有直接路径,则稀疏矩阵不包含该下标,因此使用稀疏矩阵可以表示权值为0 的路径。
我们对图3 4 8 中 A 、B 、C 、D 这 4 个节点分别编号为0、1、2、3 , 于是可以构造如下稀
疏矩阵。注意当将稀疏矩阵转换为数组显示时,矩阵屮未设置的值默认为0 , 这并不表示图中
有权值为0 的边。当用稀疏矩阵表示无向图时,只需要设置 w [i,
j ]或 w [j,
i]中的一个即可。
w = sparse.dok_matrix((4, 4))
for i, 〕、 v in edges:
w[i, j] = v
w.todense()
Python 科学计算 (第 2 版)
使 用 sCipy.spaiM.sCgraph模块可以在图中寻找最短路径,下而通过一个例子说明最短路径闲
数的用法。
图3>49(左)是一幅迷宫图像,其中的黑色曲线是用 scgraph 模块求得的从坐标(sx ,sy )到(ex ,
ey )的最短路径。O 为了方便计算,下面先将彩色迷宫图像通过闹值转换为黑 A 二值图像,黑
色表示墙壁,白色表示通路。© 为了避免将迷宫外部的余白当作通路,下面的程序在中部两侧
添加了两条黑色线段,将余白分隔为上下两个部分。经过上述处理之后的迷宫如图349(右)
所示。
img = pl.imnead("maze.png")
sx, sy = (400, 979)
s c ip y数
H, W = bimg.shape
我们将迷宫中所有的像素都当作图中的节点,节点序号与像素坐标(x ,
y )的关系使用 idx = y
* W + x 计f f 。O 找到所有上下相邻、左右相邻的白色像素对,将其对应的节点序号保存在形状
为(N ,
2)的 edges 数组中,N 为图所包含的边数。© 通 过 coo_ matdx()创建稀疏矩阵,所有边的权
值 均 为 1。
#上下相邻的白色像素
mask = (bimg[l:, :] & bimg[:-l,:])
idx = np.where(mask.navel())[0]
vedge = np.c」idx, idx + W]
pi.imsave("tmp.png", mask, cmap="gray")
#左右相邻的白色像素
mask = (bimg[:, 1:] & bimg[:, :
-l])
y, x = np.where(mask)
idx = y * W + x
hedge = np.c_[idXj idx + 1]
edges = np.vstack([vedge, hedge]) O
shape=(bimg.size, bimg.size))
startid = sy * W + sx
endid = ey * W + ex
d.shape p.shape
s c ip y数
(1, 801600) (1, 801600)
—值 计 算 库
dli,
j j 保存从编号为 indiceslij的节点到编号为j 的节点的距离。如果两个节点之间无路径联
通,值 为 inf。下面计算从起点无法到达的节点数,这些节点包拈迷宫中黑色像素表示的墙壁以
及被黑色像素完全包围的区域。
np.isinf(d[0]).sum()
322324
P[i,
j ]保存节点 indices[i]到节点 j 的路径中最后一个节点的编号。下面的代码从编号为 endid
的节点开始回溯,直到找到 startid节点为止。将访问过的节点保存到 path 中,将 path 反转即可
得到从起点到终点的路径。
path = []
node_id = endid
while True:
p a t h .a p p e n d (n o d e _ i d )
if node_id == startid or node_id < 0:
break
最后,在原来的迷宫图像中将path 经过的像素涂黑,得到图349(左)中的路径。
85
Python 科学计算(第2 版)
图 3 4 9 用 dijkstm计算最短路径
s c ip y数
—值 计 算 库
在上而的迷宫中,两个相邻白色像素之间的路径权值均为1 , 因此搜索到的最佳路径为最
短路径。而许多游戏地图中的路径搜索会考虑地形因素,这时可以根椐不同的地形设®不同大
小的路径权值,这样最伟路径就是使所有权值之和最小的路径。
3 . 9 图像处理-ndimage
提 供 的 P y t h o n 调用接口的用法。
• SimpleCV : 对多个计算机视觉库进行包装,提供了一套更方便、统 一 的 Python调用接口。
• -
s c ik it im a g e : 使 用 P y t h o n 开发的图像处理库,高速运算部分多采用 C y t h o n 编写。
• M a h o ta s : 采 用 P y t h o n 和 C ~ H •开发的图像处理库。
3 . 9 . 1 形态学图像处理
本节介绍如何使用 m o r p h o lo g y 模块实现二值图像处理。二值图像中每个像素的颜色只有两
种:黑色和白色。在 N u m P y 中可以用二维布尔数组表示:F a l s e 表示黑色,T r u e 表示白色。也
可以用无符号单字节整型(u i m 8)数组表示:0 表示黑色,非 0 表示白色。
下面的两个函数用于显示形态学图像处理的结果:
s c ip y数
import numpy as np
—值 计 算 库
def expand_image(img, value, out=None, size = 10):
if out is None:
w, h = img.shape
out = np.zeros((w*size, h*s i z e ) ,d t y p e = n p .uint8)
a x .s e t _ a x i s _o f f ()
pl.subplots_adjust(0.02, 0, 0.98, 1, 0.02, 0)
1.膨胀和腐蚀
二值阁像最基木的形态学运算是膨胀和腐蚀。膨胀运算是将与某物体(白色区域)接触的所
有背景像素(黑色区域)合并到该物体中的过程。简单来说,就是对于原始阁像中的每个白色像
素进行处理,将其周围的黑色像素都设置为白色像素。这 里 的 “周围”是一个模糊概念,在实
际运算时,需要明确给出“周围”的足义。图 3-50是膨胀运算的一个例子,其中左图是原始图
Python 科学计算 (第 2 版)
像,中间的图是四连通定义的“周围”的膨胀效果,右图是八连通定义的“周围”的膨胀效果,
图中用灰色方块表示丨:tl膨胀处理添加进物体的像素。
p l . i mread("scipy_morphology_demo.png")[:^:,0].astype(np.uint8)
imgl = expand 一image(a, 255)
■ H H
图 3-50 四连通和八连通的膨胀运算
四连通八连通
0 10 111
111 111
0 10 111
假设数组 a 是-个表示二值图像的数组,可以用如下语句对其进行膨胀运兑:
binary 一
dilation(a)
左中右
000 0 10 010
111 0 10 0 10
0 0 0 010 0 0 0
图3 - 5 1 不M 结构元素的膨胀效采
bim iy _er〇si〇
n()的腐蚀运算正好和膨胀相反,它 将 “周围”有黑色像素的白色像素设置为
黑色。图 3-52是四连通和八连通腐蚀的效果,图中用灰色方块表示被腐蚀的像素。
def erosion 一
demo(a, s t r u c t u r e = N o n e ) :
b = morphology.binary 一
erosion(a, structure)
img = expand_image(a, 255)
s h o w _ i m a ge (i m g l , img2, img3)
图3 - 5 2 四连通叭连通的腐蚀运算
Python 科学计算 (第 2 版)
2. Hit 和 Miss
H it 和 M iss 是二值形态学图像处理中最■本的运兑,因为几乎所有其他的运箅都可以用Hit
和 M iss 的组合推演出来。它对图像中的每个像素周围的像素进行模式判断,如果周围像素的黑
白模式符合指定的模式,将此像素设为白色,否则设置为黑色。因为它需要同时对白色和黑色
像素进行判断,因此需要指定两个结构元素。进 行 H it 和 M iss 运算的 binai7_hit_〇
r_ miss()的调用
形式如下:
闩色结构元尜黑色结构元尜
000 100
010 000
s c ip y数
111 000
—值 计 算 库
白色结构元索 黑色结构元索
0 00 10 0
000 0 10
111 0 00
使 用 H it 和 M iss 运算的组合,可以实现复杂的图像处理。例如文字识别中常用的细线化运
算就可以用一系列的 H it 和 M iss 运算实现。图 3-54显示了细线化处理的效果,实现程序如下:
def s k e l e t o n iz e (i m g ) :
s c ip y数
hit_list = [ ]
—值 计 算 库
miss— list = [ ]
for k in r a n g e (4): ©
hit 一l i s t •a p p e n d (n p •rot90(hi, k ) )
hit_list.append(np.rot90(h2, k))
m i s s _ l i s t .a p p e n d ( n p .rot90(ml, k ) )
miss 一l i s t •a p p e n d ( n p •rot90(m2, k ) )
img = i m g . c o p y ()
while True:
last = img
# 从图像中删除h i t _ o r _ m i s s 所得到的白色点
img = np.logical 一and(img, n p .l o g i c a l _ no t ( h m ) ) O
# 如果处理之后的图像和处理前的图像相同,则结朿处理
if np.all(img == last): 0
break
return img
a = pl.imread("scipy_morphology_demo2.png,,) [ :^:^0],astype(np.uint8)
b = skeletonize(a)
Python科学计算(第2 版)
白色结沟元素 白色结构元素
S c iP y数
0 0 0 .
0 0 0
0 1 0 黑 黑 黑 黑 黑 1 1 0
- 值计算库
1 1
1
0 1 0
白 白 白 黑
1 1 1 0 1
[0 0 0 0 0
白 白 白 白
0 0 0 0 0
0
黑色结构元素 深色结构元素
图3 - 5 5 细线化算法的4 个结构元索
3 . 9 . 2 图像分割
squares = pi.imread("suqares.jpg")
由于许多矩形都有一些小凸起与邻近的矩形连在一起,我们需要先将每块矩形与其周围的
矩形分离出来。可以使)lj 上节介绍的二值腐蚀函数 moiphology.binary_erosion()实现这一功能。
不过这里我们采用另外的方法。
morphology .distance_ _ sform_cdt(image)计算二值图像中每个像素到最近的黑色像素的距离,
返回一个保存所有距离的数组。阁像上两点之间的距离有很多定义方式,此函数默认采用切比
雪 夫 距 离 。 两 点 之 间 的 切 比 雪 夫 距 离 定 义 为 其 各 坐 标 数 值 差 的 最 大 值 , 即 Dehess =
m ax (|x 2 - x j ,|y 2 - y J )〇
下面调j|j distance_transform_cdt(squares)得到距离数纟J1 squares_dt, 并绘制成图。图中颜色越
红的像素表示该点距离黑色背景越远,而原图中值为0 的像素对应的距离为0 , 离黑色背景最
远的距离为27个像素。如果将距离数组当作图像输出,显示效果如图3-56(中上)所示。
squares_dt = morphology.distance_transform_cdt(squares)
各种距离值 [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
25 26 27]
s c ip y数
分割开了。
—值 计 算 库
squares_core = (squares_dt > 8 ) ,astype(np.uint8)
染上不同的“颜色”。这M 所 谓 的 “颜色”是指为每个区域指定的一个整数,而不是指图像中
像素的真正颜色。
labelsO 返 冋 的 结 果 数 组 可 以 用 于 计 算 各 个 R 域的一• 些统计信息。例 如 可 以 调 用
Center_of_masS〇计 每 个 R 域的重心。其第一个参数为各个像素的权值(可以认为是每个像素的
质量密度),第二个参数为 labelsO输出的染色数组,第三个参数为需要计兑重心的区域的标签列
表。在下面的程序中,权值数组和染色数纟11相同,因此可以计箅区域中所有內色像素的重心。
如 果 直 接 使 imshow()显 示 labelsO输出的染色数纟11,将会得到一个区域颜色逐渐变化的图
像,这样的图像不利于肉眼识别各个区域。冈此卜而的程序用 random_palette()为每个整数分配
一个随机颜色。其结果如图3-56(左下)所示,每个区域的重心使用白色小圆点表示。
palette = np.random.rand(count+1, 3)
p a l e t t e [0,:] = 0
return palette[labels]
h., w = labels.shape
centers = np.array(center 一
of 一
mass(labels, labels, index=range(l, count+1)), np.int)
我们儿乎完成了区域识别的任务,但是每个矩形区域都比原始图像中的要小一圈。下面将
染色之后的矩形区域恢复到原始图像中的大小。即对于原始图像中为白色、腐蚀之后的图像中
为黑色的每个像素,将其颜色设置为染色数组屮与之最近的区域的颜色。具体的计兑步骤如下:
® 将 腐 蚀 之 后 的 图 像 square_c o r e 反转,并调 jjj distance_transform_cdt()。这样可以找到
square_c o r e 中距离每个黑色像素最近的白色像素的坐标。为了让其:返回坐标信思,需要设置参
数 retum_indices 为 True。由于这里不斋要距离信息,因此可以设置参数retum_distances为 False。
其返 W 值 index 是一个形状为(2 , 高,宽)的三维数组。index[0]为最近像素的第0 轴(纵轴)坐細,
index[l ]为最近像素的第1轴(横轴)坐标。
© 使 用 index[0]和 index[l ]获 取 labels 中对应坐标的颜色,得 到 near_labels。
© 创連一个布尔数组 mask,其中的每个 True 值对应 squares中为白色、squares_co re 中为黑
色的像素。将 labels 复制—份 到 labels2,最后将 m ask 中 True 对应的 near_labels 中的颜色值复制
s c ip y数
到 labels2 中。
—值 计 算 库
图 3-560I1下)是使用mndom_palette〇随机着色之后的结果。
index = morphology.distance_transform_cdt(l-squares_core,
return 一distances=False,
return_indices=True) O
图3 - 5 6 矩形区域分割算法各个步骤的输出图像
3 . 1 0 空间算法库-sp a tia l
3 . 1 0 . 1 计算最近旁点
众所周知,对于一维的已排序的数值,可以使用二分法快速找到与指定数值最近的数值。
在下面的例子中,在一个升序排序的随机数数纟JI x 中,使 用 numpy.searchsorted〇搜索离 0 .5 最近
的数。排序算法的时间复杂度为〇( N lo g N ) , 而每次二分搜索的时间复杂度为O(logN )。
s c ip y数
x = np.sort(np.random.rand(100))
idx = np.searchsortGd(x, 0.5)
—值 计 算 库
print x[idx], x[idx - 1] #距离0.5最近的数是这两个数中的一个
0 .542258714465 0 .492205345391
kd = s p a t i a l .c K D T r e e (p o i n t s )
targets = np.array([(0, 0), (0.5, 0.5), (-0.5, 0.5), (0.5, -0.5), (-0.5, -0.5)])
dist idx
:KDTnee.query_ball_point〇搜索与指定点在一定距离之内的所有点,它只返回最近点的下标,
95
Python科学计算 (第 2 版)
r = 0.2
idx2 = kd.query_ball_point(targets, r)
idx2
array([[48], [37, 78], [79, 92, 22], [58, 35, 6], [7, 55, 83, 42]], dtype=object)
{(1, 46), (3, 21), (3, 82), (3, 95), (5, 16), (9, 30),
(10, 87), (11, 42), (11, 97), (18, 41), (29, 74), (32, 51),
(37, 78), (39, 61), (41, 61), (50, 84), (55, 83), (73, 81)}
在 阁 3-57中,与 target中的每个点(用五角星表示)最近的点用与其相同的颜色标识:
s c ip y数
—值 计 算 库
图3 - 5 7 用 c K D T r c e 寻找近旁点
distl[np.diag_indices(len(points))] = np.inf
nearestjDair = np.unravel_index(np.argmin(distl), distl.shape)
print nearestjDair, d i s t l [nearestjDair]
s c ip y数
(22, 92) 0.00534621024816
—值 计 算 库
用 cKDTree .query()可以快速找到这个距离最近的点对:在 K -d 树中搜索它自己包含的点,
找到勹毎个点最近的两个点,其中距离最近的点就是它木身,距 离 为 〇,而距离第二近的点就
是每个点的最近旁点,然后只需要找到这些距离中最小的那个即可:
N = 1000000
span = np.random.uniform(0.01, 1, N)
span = np.clip(span, 2, 100)
下面的 naive_count_at()采用逐个比较的方法计算指定时间的在线用户数量:
return np.sum(mask)
97
Python科学计算 (第 2 版)
so
20
〇
〇
60
40
20
s c ip y数
0
—值 计 算 库
2C
图3 - 5 8 使用二维K k !树搜索指定区间的在线用户
tree.count_ neighbors(other,
r,p=2)可以统计 tree 中到 other 的距离小于 r 的点数,其 中 p 为计算
距离的范数。距离按照如下公式计算:
当p = 2 时,上面的公式就是欧几里得空间中的向量长度。当p = 〇
〇时,该距离变为各个
轴上的最大绝对值:
N〇〇(x) =|| X ||〇〇= maxdXil, |x 2 卜 •, |xn|)
当使)Up = 〇
〇时可以计某正方形区域之内的点数。我们将该正方形的中心设置为(出1^-
m^ _ time, time + max_tiirie),正方形的边长为 2 * max_time , 即 r = max_time。其-中 max_time 为 end
中的最大值。下面的 KdSearch 类实现了该算法:
class K d S e a r c h (o b j e c t ) :
self.max_time = np.max(end)
def count 一
at(self, t i m e ) :
%timeit k s .c o u n t _ a t (40)
Wall time: 4 8 4 ms
3 . 1 0 . 2 凸包
所谓 ini包是指 N 维空间中的一个区域,该区域中任意两点之间的线段都完全被包含在该区
域之中,二维平面上的凸多边形就是典型的凸包。ConvexHull可以快速计算包含N 维空间中点
的集合的最小凸包。下面先看一个二维的例子:points2d 是—•组二维平面上的随机点, c h 2 d 是
这些点的凸包对象。ConvexHull.simplices是凸包的每条边线的两个顶点在points2d 中的下标,由
于它的形状为(5,2),因此凸包由5条线段构成。对于二维的情况,(
:〇^6识1111^111(愁是凸多边
形的每个顶点在 p〇ints2d 中的下标,按逆时针方向的顺序排列。
n p .r a n d o m .s e e d (42)
points2d = n p . r a n dom.rand(10, 2)
ch2d = spatial.ConvexHull(points2d)
ch2d.simplices ch2d.vertices
ax = pi.subplot(aspect="equal")
.6 0.7 0.8
图3 - 5 9 二维平而上的凸包
三维空间中的凸包是一个凸多而体,每个而都是一个三角形。在下面的例子中,由 simplices
的形状可知,所得到的凸包由38个三角形面构成:
n p .r a n d o m .s e e d (42)
points3d = np.r a n d o m. r a n d (40, 3)
ch3d = spatial.ConvexHull(points3d)
c h 3 d .s i m p l i c e s .shape
(38, 3)
scene.close()
s c ip y数
—值 计 算 库
如果读者希望采用三维交互界而,可以在 Notebook 中执行如下代码:
%gui qt
i v t k _ s c e ne (a c t o r s )
3 . 1 0 . 3 沃罗诺伊图
[5, 6],
[1, 3],
Python科学计算 (第 2 版)
[-1, 5],
[2, 4],
[-1, 6]]
使 用 v〇
r〇noi_ pl〇t_2d()可以将沃罗诺伊图显示为图表,效果如图3-61(左)所示。图中蓝色小
圆点为 p o in te d 指定的胞点,红色大圆点表示 Voronoi.vertices 中的点,图中为每个 vertices 点标
注了下标。山虚线和实线将空间分为7 个区域,以线为边的区域为无限大的 R 域 ,一直向外
延伸,全部由实线构成的区域为有限区域。每个区域都以 vertices 中的点为顶点。
Vorx)noi.regions 是区域列表,其中每个区域由一个列表(忽略空列表)表示,列表中的整数为
vertices 中的序号,包含- 1 的区域为无限区域。例如[6,4,3,5]为图中正中心的那块区域。
Voronoi.ridge_vertices 足区域分割线列表,每条分割线[tl vertices 中的两个序号构成,包含-1
的分割线为图中的虚线,其长度为无限长。
如果希望将每块区域以不同颜色填充,但由于外围的区域是无限大的,因此无法使用
matplodib 绘图,可以在外而添加4 个点将整个区域園起来,这样每个 p o in te d 中的胞点都对应
—个有限区域。在图3-61(右)中,黑圈点为 points2d 指定的胞点,将空间中与其最近的区域填充
S c iP y数
成胞点的颜色。
- 值计算库
图3 - 6 1 沃罗诺伊图将空间分割为多个区域
使用沃罗诺伊图可以解决最大空圆问题:找到一个半径最大的圆,使得其圆心在一组点的
凸包冈域之内,并且圆内没有任何点。根 据 图 3-61可知最大空圆必定是以 vertices 为圆心,以
与最近胞点的距离为半径的圆中的某一个。为了找到胞点与 vertices 之间的联系,可以使用
Voronoi.point_region 属性。point_region[ij 楚 与 第 i 个胞点(points2 _ ) 最近的丨以挪j 编^•。例如凼
02
下面的输出可知,下标为5 的蓝色点与下标为6 的区域对应,而 由 V 〇
1x)n〇
i.regi〇
ns[6]可知,该区
域 [1:1编号为6、2、4 的三个 vertices 点(图中红色的点)构成。因 此 vertices 中与胞点 points2d |_5J最
近的点的序号为[2,4,6]。
print v o . r e g i o n s [6]
[ 0 3 1 7 4 6 5 ]
[6, -1, 2, 4]
下 面 是 计 算 最 大 空 程 序 ,效果如图3~62所示。程序「
I1: O 使用 pylab.Polygon.contains_point〇
判断用 CoiwexHull计兑的凸包多边形是否包含vertices中的点,用以在©处剔除圆心在凸包之外
的1C
© vertice_point_ m ap 是一个字典,它的键为 vertices 点的下标,值为与其最近的几个 points2d
点的序号。
整个字典使用p〇
int_nigion和 regions构建,
注意这里剔除了所有在凸包之外的vertices点。
O 对 于 VertiCe_point_m a p 中的每一对点,找到距离最大的那对点,即可得出岡心坐标和岡
s c ip y数
的半径。
—值 计 算 库
from collections import defaultdict
n = 50
n p •r a n d o m •s e e d (42)
points2d = np.random.rand(n, 2)
vo = spatial.Voronoi(points2d)
ch = spatial.ConvexHull(points2d)
poly = pi.Polygon(points2d[ch.vertices]) O
vs = vo.vertices
vert ice__point一
map = defaultdict (list) €)
region = vo.regions[index_region]
if -1 in r e g i o n : continue
if c o n v e x h u ll _mask[index_vertice]:
v e r t i c e _ p o i n t _ m a p [index _ v e rt i c e ].a p p e n d (i n d e x _ p o in t )
return ((pl-p2)**2).sum()**0.5
20
Python 科学计算 (第 2 版)
).6
>•4 •
#
).2 • •
)〇
oo 0.4 3.6
图 3 4 2 使用沃罗进伊•图汁算 S 大空岡
3 . 1 0 . 4 德劳内三角化
德劳内三角化算法对给定的点集合的凸包进行三角形分割,使得每个三角形的外接圆都不
含任何点。下面的程序演示了德劳内三角化的效果:
Delaunay 对 象 d y 的 simplices 属性是每个三角形的顶点在points2d 中 的 K标。可以通过三角
形的顶点坐标计算其外接圆的圆心,不过这里使用同一点集合的沃罗诺伊图的 vertices 属性,
vertices 就是每个三角形的外接圆的圆心。
dy = spatial.Delaunay(points2d)
vo = spatial.Voronoi(points2d)
dy.simplices vo.vertices
下而是用 ddaimay_plot_2d〇绘制德劳内三角化的结果,此外还绘制每个外接圆及其圆心。
可以看到三角形的所有顶点都不在任何外接圆之内,效果如图3-63所示:
cx, cy = vo.vertices.T
ax = pi.subplot(aspect="equal")
spatial.delaunayjDlot_2d(dy, ax=ax)
ax.plot(cx, c y , " r . " )
for (cx, cy) in e n u m e r a t e( v o . v e r t i c e s ) :
s c ip y数
a x .add_artist(circle)
—值 计 算 库
ax.set_xlim(0, 300)
ax.set_ylim(0, 300)
图 3 4 3 德劳内三角形的外接圆与岡心
05
解4 亭
matplotlib-绘制精美的图表
matplotlib是 Python 最著名的绘图库,它提供了一整套和 M A T L A B 类似的绘图函数集,十
分适合编写短小的脚本程序以进行快速绘图。matplotlib的文档十分完备,并且其展示页而中有
上 百 幅 图 表 的 缩 略 图 及 其 源 程 序 。因 此 如 果 读 者 需 要 绘 制 某 种 类 型 的 图 表 ,只需要在
h即://matplotlib.sourceforge.net/gdlety.htird I: “浏览/复制/粘贴”一卜,猫本丨:都能快速解决。
木章在简单介绍 matplotlib 的快速绘阁功能之后,将较深入地挖掘儿个实例,让读者从中
学习和理解 matplotlib绘图的一些_ 本概念。相信读者在了解木章的内容之后,应该能够根据官
方的文裆和演示程序使用matplotlib完美地展示数据。
4 . 1 快速绘图
matplotlib采用而向对象的技术来实现,因此组成阁表的各个元素都是对象,在编写较大的
应用程序时通过而向对象的方式使用matplotlib将更加有效。
但是使用这种而向对象的调用接口
进行绘图比较烦琐,因此 matplotlib还提供了快速绘图的pyplot模块。本节首先介绍该模块的使
用方法。
为了将 matplotlib绘制的图表嵌入 Notebook 中,需要执行下面的命令:
%matplotlib inline
4 . 1 . 1 使 用 pyplot模块绘图
pylab模 块
matplotlib还提供了一个名为 pylab 的模块,其中包括了许多 N um Py 和 pyplot模块中常用的
函数,方便用户快速进行计算和绘图,十分适合在 IPython 交互式环境中使用。本书使用 import
pylab as pi 载入 pylab 模块。
y = np.sin(x)
z = np.cos(x**2)
plt.figure(figsize=(8,4)) ©
p i t .plot(x,y,label="$sin(x)$"<,color=nr e d n,linewidth=2) ©
plt.xlabel("Time(s)") ©
p i t . y l a b el ( MVolt")
- 制精美的图表
p i t . l e g e n d ()
plt.show() ©
程序的输出如图4-1所示。
〇首先载入 matplotlib的绘图模块pyplot,
并且靈命名为pit。
© 调用 figure()创連一个 Figure(阁
表)对象,并且它将成为当前 Figure 对象。也可以不创建 Figure 对象而直接调用接下来的 plot()
a
20
进行绘图,这 时 matplotlib会自动创連一个 Figure 对象。figsize 参数指定 Figure 对象的宽度和高
度,单位为英寸。此外还可以用 dp i 参数指定 Figurc 对象的分辨率,即每英寸所表示的像素数,
这里使用默认值80。因此本例中所创建的Figure 对象的宽度为8*80 = 640个像素。
© 创 建 Figure 对象之后,接 K来 调 用 plot()在当前的 Figure 对象中绘图。实际上 plot()是在
Axes (子图)对象上绘图,如果当前的 Figure 对象中没有 A x e s 对象,将会为之创建一个几乎充满
整个阁表的 A x e s 对象,并且使此 A x e s 对象成为当前的 A x e s 对象。plot〇的前两个参数是分别表
示 X 、Y 轴数据的对象,这里使用的是 N um Py 数组。使用关键字参数可以指定所绘制曲线的各
种属性:
• label:给丨1丨
1线指足 ■个标签,此标签将在图不中见不。如果标签字符$的則 j 曰有字符'$',
一
matplotlib会使用内嵌的 L a T e X 引擎将:其显示为数学公式。
• c o l o n 指定曲线的颜色,颜色可以用英文单词或以’
# 字符开头的6 位十六进制数表示,
例如’
#00000’
表示红色。或者使用值在0 到 1 范围之内的三个元素的元组来表示,例如
(1.0,0.0,0.0)也表示红色。
• linewidth: 指定曲线的宽度,可以不是整数,也 可 以 使 缩 写 形 式 的 参 数 名 lw 。
m atp lo tlib绘
使 用 LaTeX 语法绘制数学公式会极大地降低图表的描绘速度。
—制 精 美 的 图 表
〇直接通过第三个参数指定曲线的颜色和线型,它通过-•些易记的符号指定曲线的样
式。其中’
b’
表示蓝色, 表示线型为虚线。在 IPython 中 输 入 p ltp bt ?可以查看格式化字符串以
及各个参数的详细说明。
© 接下来通过一系列闲数设置当前A x e s 对象的各个属性:
• xlabel、y l a b e h 分别设置 X 、Y 轴的标题文字。
• title: 设麗子图的标题。
• xlim 、ylim : 分别设® X 、Y 轴的显示范園。
• legend:显 示 图 示 ,即图中表示每条丨III线的标签(label)和样式的矩形K 域。
© S 后调用 plt.show ()显示绘图窗口,
在 Notebcx也中可以省略此步骤。
在通常的运行情况下,
show ()将会阯塞程序的运行,直到用户关闭绘图窗口。
还可以调用 plt.savefigO将当前的 Figure 对象保存成图像文件,图像格式由图像文件的扩展
名决定。下面的程序将当前的图表保存为test.png,并且通过 d p i 参数指定图像的分辨率为120,
因此输出图像的宽度为8*120 = 960个像素。
09
P y th o n 科学计算(第2 版)
import io
'\x89PNG\r\n\xla\n\x00\x00\x00\rIHDR\x00\x00\x03 '
4 . 1 . 2 面向对象方式绘图
matplotlib实际上是一套面向对象的绘图库,它所绘制的图表屮的每个绘图元素,例如线条、
文字、刻度等在内存中都有一个对象与之对应。为了方便快速绘图,matplotlib通 过 pyplot模块
提供了一套和 M A T L A B 类似的绘图 A P I , 将众多绘图对象所构成的复杂结构隐藏在这套API
内部。
我们只需要调用 pyplot模块所提供的闲数就可以实现快速绘图以及设置图表的各种细节。
m atp lo tlib绘
p yp lot 模块虽然用法简单,但不适合在较大的应用程序中使用,冈此本章将着重介绍如何使用
matplotlib 的面叫对象方式编写绘图程序。
- 制精美的图表
fig = plt.gcf()
axes = plt.gca()
print fig, axes
•••
try:
ax.hold(washold)
4 . 1 . 3 配置属性
matplotlib所绘制图表的每个组成部分都和一个对象对应,可以通过调州这些对象的属性设
置 方 法 set_*()或 者 pyplot 模块的属性设置函数 setp〇来设置它们的属性值。例 如 plot〇返回一个
元素类型为 Line2D 的列表,下面的例子设置 Line2D 对象的属性:
plt.figure(figsize=(4j 3))
m a t p l i b绘
lines = pit.plot(x^ np.sin(x)j x, np.cos(x))
调 川 se tp (M 以同时配置多个对象的属性,这里同时设置两条曲线的颜色和线宽:
—制 精 美 的 图 表
pit.setp(lines^ col o r = " r _、 linewidth=4.0)
图本2 配® 绘图对象的屈性
print line.get_linewidth()
print plt.getp(lines[0], "color") # 返回 color 属性
2.0
r
注 意 getp()和 setp()小同,它只能对一个对象进行操作,它有两利调法:
•指 定 屈 性 名 :返冋对象的某个屈性的值。
•不 指 定 属 性 名 :输出对象的所有属性和值。
下面通过 getp()查看 Figure 对象的属性:
P y th o n 科学计算(第2 版)
f = plt.gcfO
plt.getp(f)
agg 一
filter = None
alpha = None
animated = False
通过这利1方法可以很容鉍查看对象的属性值以及各个对象之间的关系,找到需要配置的属
性。因为 matplotlib实际上是一套而向对象的绘阁库,因此也可以直接获取对象的属性,例如 :
4 . 1 . 4 绘制多子图
p i t .subplot(321+idx, axisbg=color)
如果希望某个子图占据整行或整列,可以如下调用 subplotO,程序的输出如图4-3(右)所示。
p i t . s u b p lo t (221 ) # 第一行的左图
p!t.subplot(222) # 第一行的右图
p i t . s u b p lo t (212) # 第二整行
m a t p l i b绘
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
—制 精 美 的 图 表
阁《 在 Figure对象中创逑多个子阁
将不创建新的对象,而只是让它成为当前的Figure 对象。下而的程序演示了依次在不同图表的
不同子图中绘制llll线:
plt.figure(l) # 创建图表 1
plt.figure(2) # 创建图表 2
axl = plt.subplot(121) # 在图表2 中创建子图1
ax2 = plt.subplot(122) # 在图表 2 中创建了•图 2
x = np.linspace(0^ 3, 100)
for i in xrange(5):
plt.figure(l) O 选择图表 1
pit.plot(x, np.exp(i*x/3))
plt.sca(axl) ©选择图表2 的子图1
213
Python科学计算 (第 2 版)
pit.plot(x, np.sin(i*x))
图4 4 同时在多幅图表、多个子阁中进行绘图
此 外 subpbtsO 可以一次生成多个子图,并返回图表对象和保存子图对象的数组。在下面的
例子中,axes 是一个形状为(2,3)的数纟J1,每个元素都是一个子图对象,可以利用 Python 的赋值
功能将这个数组中的每个元素用一个变蛰表示:
print b
(2, 3)
A x e s (0.398529,0.536364;0.227941x0.3636B6)
m a tp lo tlib绘
- 制精美的图表
丨
冬I4 ~ 5 使用subplot2grid〇创逑表格布厂•}
4 . 1 . 5 配置文件
绘制一•幅图需要对许多对象的屈性进行配置,例如颜色、字体、线型等。在前面的绘图程
序屮,并没有逐一对这些属性进行配置,而是直接采用 matplotlib 的默认配置。matplotlib 将这
些默认配置保存在一个名为matplotlibrc的配置文件中,通过修改配置文件,可以修改图表的默
认样式。
在 matplotlib 中可以使用多个 matplotlibrc配置文件,它们的搜索顺序如下:顺序靠前的配
置文件将会被优先采用。
•当 前 路 径 :程序的当前路径。
• 用 户 配 置 路 径 :通 常 在 用 户 文 件 夹 的 .matplotlib H 录 下 ,可 以 通 过 环 境 变 M
M A T P LO T LIB R C 修改它的位置。
•系 统 配 置 路 径 :保存在 matplotlib的安装目录下的mpl-data中。
通过下面的语句可以获収用户配置路径:
p a t h .a b s p a t h ( ma t p l o t l i b .g e t _ c o n f i g d i r ())
通过下而的语句可以获得目前使用的配置文件的路径:
Python科学计算 (第 2 版)
p a t h .a b s p a t h ( ma t p l o t l i b .m a t p l o t l ib _ f n a m e ())
如果使用文本编辑器打开此配置文件,就会发现它实际上是一个字典。为了对众多的配置
进行区分,字典的键根据配置的种类,用 分 为 多 段 。配置文件的读入可以使用rc_params〇,
它返回一个配置字典:
p r i n t ( m a tp l o t l i b .rc j D a r a m s ())
a g g .p a t h .c h u n k s i z e : 0
a n i m a t i o n •avconv 一args: [ ]
a n i m a t i o n .a v c o n v _ p a t h : avconv
a n i m a t i o n .b i t r a t e : -1
p r i n t ( m a t p l o t l i b .rcP a r a m s )
m a tp lo tlib绘
a g g .p a t h .c h u n k s i z e : 0
a n i m a t i o n .a v c o n v _ a r g s : [ ]
- 制精美的图表
a n i m a t i o n .a v c o n v _ p a t h : avconv
animation.bitrate: -1
做的改变会反映到此后创建的绘图元素。例如下而的脚本所绘制的折线将带有岡形的点标识符:
m a t p l o t l i b .r c P a r a m s ["lines.marker"] = "o"
p i t . p l o t ([1,2,3,2])
为了方便对配置字典进行设置,
可以使用 rc〇。
下面的例子同吋配置点标识符、
线宽和颜色:
m a t p l o t l i b .rcdef a u l ts ()
m a t p l o t l i b .r c P a r a m s .u p d a t e ( m a t p l o t l i b .rc j D a r a m s ( ) )
ggp 丨
o t t 丫式
m atp lo tlib绘
- 制精美的图表
图4 « 6 使用ggplot样式绘图
4 . 1 . 6 在图表中显示中文
matplotlib 的默认配置文件中所使用的字体无法正确显示中文,可以通过下面几利汸法设置
中文字体:
♦ 在程序中直接指定字体。
•在 程 序 开 头修改配置字典 rcParams。
•修 改 配 置 文 件 。
在 matplotlib 中可以通过字体名指定字体,而每个字体名都与一个字体文件相对应。通过
下面的程序可以获得所有可用字体的列表:
print fontManager.ttflist[0].name
print fontManager.ttflist[0].fname
cmssl0
C :\WinPython-32bit-2.7.9.2\python-2.7.9\lib\site-packages\matplotlib\mpl-data\fonts\ttf
\cmssl0.ttf
下面的程序使用字体列表中的字体显示中文文字,效果图4-7所示。
scpy2/matplotlib/chinese_fonts.py:显 示 系 统 中 所 有 文 件 大 于 1M B 的 T T F 字体,请读者
积使用该程序查询计算机中可使用的中文字体名。
m a t p l b绘
import os
from os import path
-
pit.subplots__adjust(0, 0, 1, 1, d, 0)
plt.xticks([])
plt.yticks([])
x, y = 0.05, 0.05
精
y += dy
x = 0.05
plt.show()
表
218
中文事冰 中文字体 中文字体
LiSu STXjhei FZShuTi OengXian
中文字体 r4 •>r4 4>
中文字体
Yu Gothic You^uan Linux Biolinum G Go br iota
中文字体 中文字 <本 中文 f 体 9770
• • • •
阁本7 显示系统中所有的中文字体名
还可以直接修改配置字典,设置默认字体,这样就不需要在每次绘制文字时设置字体了。
例如:
pit.rcParams["font.family"] = "SimHei"
p i t . p l o t ([1,2,3])
plt.xlabel(0.5 ,0.5, u " 中文字体")
名,请读者运行前面的代码来查看系统中所有可用的中文字体名。
- 制精美的图表
4.2 Artist
matplotlib是一套面向对象的绘图库,它有三个层次:
• backend_bases.FigureCanvas:绘图用的幽布。
• backend_bases.Renderer: 知道如何在 FigureCanvas 对象上绘图。
• artist.Artist: 知道如何使用 Renderer在 FigureCanvas对象上绘图。
FigureCanvas 和 Renderer⑷要处理底层的绘图操作,例如在 wxPython 界而库所生成的界而
上绘图,或者使用 PostScript在 P D F 文件中绘阁。Artist对象则处理所有的商层结构,例如处理
图表、文字和丨11|线等各利烩图元素的绘制和布局。通常我们只和 A rtist 对象打交道,而不需要
关心底层是如何实现绘图细节的。
Artist 对象分为简单类型和容器类型两种。简单类型的 Artist 对象是标准的绘图元件,例如
Line2D 、Rectangle、Text 、A xeslm age 等。而界器类型贝II可以包含多个 Artist 对象,使它们组织
成一个整体,例 如 A xis 、A xes 、Figure 等。
直接创建 Artist对象进行绘图的流程如下:
(1) 创 連 Figure 对象。
(2) 为 Figure 对象创建一个或多个 A x e s 对象。
(3) 调 用 A x e s 对象的方法来创建各种简单类型的Artist对象。
20
在下面的程序1丨
1,"S *先调用 figure()创 建 Figure 对象,figure()是一个辅助函数,帮助我们创
建 Figure 对象,它会进行许多初始化操作,因此不述议直接使j |jFigure ()创連。然后调jij Figure
对 象 的 add_axes()在其中创建一个 A x e s 对象,add_axes〇的参数是一个形如[left,bottom,width,
height]的列表,这些数值分别指定所创建的A x e s 对象在 Figure 对象中的位置和大小,各个值的
取值范围都在0 到 1之间:
fig = p i t . f i g u re ()
True
m atp lo tlib绘
A x e s 对象的 lines 属性是一个包含所有曲线的列表,
如果继续运行ax .plot〇,所创建的 Line2D
对象都会添加到此列表中。如果想删除某条曲线,直接从此列表中删除即可。
A x e s 对象还包括许多其他的 Artists 对象,例如可以通过 set_ xlabel()设置其 X 轴上的标题:
- 制精美的图表
ax.set_xlabel("timeM )
如果查看 set_Xlabel〇的源代码,就会发现它是通过下面的语句实现的:
s e l f •x a x i s •set 一label 一t e x t (x l a b e l )
print ax.xaxis
print ax.xaxis.label
print a x . x a x i s . l a b e l •一text
X A x i s (72.000000,24.000000)
T e x t ( 0 . 5 , 2 . 2 , u ,t i m e ,)
time
ax.get_xaxis().get_label().get_text()
u'time'
fig = p i t . f i g u re ()
fig.patch.set_color("g") # 设H 1/丨景色为绿
注意当代码作为单独程序运行时,调 州 set_Cd 〇
r()设置好背景色之后,并不会立即在界面
上显示出来,还需要调用 fig .canvas.dmwO才能更新界面显示。
表木1是所有 Artist对象都拥有的一些属性。
表 4 - 1 所有Artist对象都拥有的一些属性
属性 说明
alpha 透明度,值在0 到 1 之间,0 为完全透明,1 为完全不透明
animated 布尔值,在绘制动圃效果时使用
matpotlib绘
clip_on
dip—path 裁剪的路径
contains 判断指定点是否在对象之上的函数
figure 拥有此Artist对象的Figure对象,可能为None
label 文本标签
picker 控制Artist对象选取
transform 控制偏移、旋转、缩放等坐标变换
visible 控制是否可见
zorder 控制绘图顺序
l i n e •set 一a l p h a (0•5)
可以使用 set〇—次设置多个屈性:
line.set(alpha=0.5, zorder=2)
p i t .g e t p (f i g .p a t c h )
2
2
2
aa = False
agg_filter = None
alpha = None
animated = False
4.2.2 Figure 容器
类型。
在构成图表的各种 Artist对象中,最上层的 Artist对象是 Figure, 它包含组成图表的所有元
素。当调用 add_subplot()或 add_axes()方法往图表中添加子图吋,这些子图都将添加到axes 属性
列表中,同时这两个方法也返回新创連的A x e s 对象。注 意 add_subplot()和 add_ axes()所返回对象
的类型有所不同,分別为 AxesSubplot和 A xes ,AxesSubplot是 A x e s 的派生类。
m atp lo tlib绘
fig = p i t . f i g u re ()
axl = fig.add— s u b p l o t (211)
- 制精美的图表
print axl in fig.axes and ax2 in fig.axes
True
for ax in fig.axes:
ax.grid(True)
linel = Line2D(
[0, l]j [0, 1], transform=fig.transFigure^ figure=fig^ color="n")
line2 = Line2D(
f i g .l i n e s .e x t e n d ([linel, line2])
P y th o n 科学计算(第 2 版)
表 4 - 2 包含其他Artist对象的Figure对象属性
属性 说明
axes A x e s 对象列表
patches Patch对象列表
4.2.3 Axes 容器
- 制精美的图表
fig = p i t . f i g u re ()
ax = fig.add_subplot(lll)
a x .p a t c h .set_facecolor("green")
line is a x . l i n e s [0]
True
24
与 plot()类似,绘制柱状图的函数 bm<)和绘制直方统计图的函数hist()将创建一个 Patch 对象
的列表,每个元素实际上都是从Patch 类派生的 Rectangle 对象,所创建的 Patch 对象都被添加进
了 A x e s 对象的 patches属性中:
fig, ax = p i t . s u b p lo t s ()
n, bins, rects = a x .h i s t (n p .r a n d o m .r a n d n (1000)y 50, f a c e c o l o r= " b l u e " )
rects[0] is a x . p a t c h e s [0]
True
fig^ ax = p i t . s u b p lo t s ()
rect = pit.Rectangle((l ,
l), width=5, height=12)
m a t p l b绘
然后通过 add_patch()将 rect 添加进 a x 中:
- 制精美的图表
rect.get 一a x e s () is ax
True
(0.0, 1.0)
(1.0, 1.0, 5.0, 12.0)
(1.0, 6.0)
22
:
P y th o n 科学计算(第2 版)
(续表)
属性 说明
lines Linc2D 对象列表
patches Patch对象列表
xaxis X A x i s 对象
yaxis YAx i s 对象
表 4~4列出了 A x e s 对象的各种创建其他Artist对象的方法:
fig, ax = p i t . s u b p lo t s ()
print tj t in ax.collections
4.2.4 Axis 容器
A x is 容器包括也标轴上的刻度线、亥lj度文本、猶示网格以及坐标轴标题等内容。亥度包柄
主刻度和副刻度,分别通过 get_major_ticks〇和 get_minor_ticks〇方法获得。每个刻度线都是一个
X T ic k 或 Y T ic k 对象,它包括实际的刻度线和刻度文本。为了方便访问刻度线和文本,A x is 对
象提供了 get_ticklabels〇和 get_ticklines〇方法来直接获得刻度线和刻度文本。
6
2
2
下面先创建-•个子图并获得其X 轴对象 axis :
fig_» ax = p i t . s u b p lo t s ()
axis = ax.xaxis
a x i s •get— t i c k l o c s ()
下而获得轴上表示主刻度线的列表,可以看到 X 轴 上 共 有 12条刻度线,它们是子图的上
m a t p lib绘
下两个 X 轴上的所有刻度线:
- 制精美的图表
a x i s •get 一
t i c k l i n e s ()
而由于图中没有副刻度线,因此副刻度线列表的长度为0:
axis.get_ticklines(minor=True) # 获得副刻度线列表
<a list of 0 Line2D ticklines objects>
获得刻度线或刻度标签之后,可以设置其各种属性,下面设置刻度线为绿色粗线,文本为
红色并且旋转45°。最终结果如图本8所示:
for label in a x i s . g e t _t i c k l a b e l s ( ) :
l a b e l .set_r o t a ti o n (45)
l a b e l •set— f o n tsize(16)
l i n e •set 一c o l o r ("green")
line.set_markersize(25)
l i n e •set 一
m a r k e r e d g e w i d t h (3)
fig
Python科学计算(第2 版)
•Of—
.2 •
图4>8配置X 轴的刻度线和刻度文本的样式
在前面的例子中,副刻度线列表为空,这是因为用于计算副刻度位置的对象默认为
N u llL o c a to r , 它不产生任何刻度线。而计算主刻度位置的对象为A u t o L o c a t o r , 它会根据当前的
缩放等配置自动计算刻度的位置:
def p i_formatter(x> p o s ) : ©
if frac == 0:
return "0"
22J
elif frac == 1:
return "$\pi$"
elif d == 1:
return r n$\frac{ \ pi } { % d } $ n % d
# 设贾两个坐标轴的范围
plt.ylim(-1.5>1.5)
plt.xlim(0^ np.max(x))
# 设置图的底边距
pit.subplots_adjust(bottom = 0.15)
p i t . g r i d () # 开启网格
m a t p l b绘
# 主刻度为pi/4
a x •xaxis.set— major 一l o c a t o r ( M u l t i p l e L o c a t o r ( n p .pi/4) ) ©
- 制精美的图表
# 主刻度文本用p i _ f o r m a t t e r 函数计算
ax.xaxis.set 一
major 一
formatter^ FuncFormatter( pi 一
formatter ) ) O
# 副刻度为pi/20
ax.xaxis.set_minor_locator( MultipleLocator(np.pi/20) ) ©
# 设置刻度文本的大小
for tick in ax.xaxis .g e t _ m a j o r _ t i c k s () :
t i c k .l a b e l l .set_fontsize(16)
图4 * 9 配置X 轴的刻度线的位置和文本,并开启副刻度线
29
Python科学计算 (第 2 版)
〇刻度定位和文本格式化相关的类都在matplotlib.tickei•模块中定义,程序从中载入了如下
两个类:
• MultipleLocator: €>©以指定值的整数倍为刻度放置主副刻度线。
• FuncFormatter: O 使用指定的闲数计算刻度文本,它会将刻度值和刻度的序号作为参数
传递给计算刻度文本的闲数。© 程 序 中 通 过 pi_formatter()计算出与刻度值对应的刻度
文本。
4.2.5 Artist对象的关系
为了方便读者理解图表中各种Artist对象之间的关系,本书提供了一个输出Artist对象关系
图的小程序。
为了生成关系图,
读者可以从 Graphviz 的官方网站下载Graphviz 软件包,
或者使用 Graphviz
的在线编辑器。下而看一个例子:
- 制精美的图表
fig = p i t . f i g u re ()
p i t . s u b p lo t (211)
plt.plot([l, 2, 3])
%dot G r a p h v i z M a t p l o t l i b .graphviz(fig)
30
matplotlib绘
制
精
美 -
闯4 ~ 1 0 使用GraphvizMatplotlib也成阁表对象中各个Artist对象之间的关系图
的
4 3 坐标变换和注释
图
一幅阍表中涉及多种坐标系以及坐标变换,理解各利唑标系的含义并掌握其用法才能随心
2
31
P y th o n 科学计算(第2 版)
所欲地使用 matplotlib绘制出理想效果的图表。本卞以图表屮的文字、箭头和标注为例介绍各种
坐标系及其变换。
def funcl(x): O
return 0.6*x + 0 . 3
def func2(x): O
return 0.4*x*x + 0.1*x + 0 . 2
return -dl*(x2-xl)/(d2-dl) + xl
matpotlib绘
x = np.linspace(-3,3,100) O
fl = funcl(x)
f2 = func2(x)
- 制精美的图表
fig, ax = pit.subplots(figsize=(8,4))
ax.plot(Xj fl)
ax.plot(x, f2)
fontsize = 18,
b b o x = { " f ac e c o l o r " :"red" , " a lp h a " :0.4," p a d " :10}
arrow = {"arrowstyle":"fancy,tail_width=0.6">
"face c o l or " :"gray",
ax.annotate(u" 交点", ©
xy=(x2, funcl(x2))) xycoords="data",
xm = (xl+x2)/2
ym = (funcl(xm) - func2(xm))/2+func2(xm)
o = ax.annotate(u" 直线大于曲线区域", ©
xy =(xm, ym), xycoords="data",
xytext = (30, -30), textcoords="offset points",
matplotlib绘
b b o x = { " b o x s t y l e " :" r o u n d % "facecolor":(1.0, 0.7, 0.7), "edgecolor":"none"},
fontsize=16j
arrowprops={ "arrowstyle" }
- 制精美的图表
)
程序的输出如图4-11所示。在图木11中演示了下面列出的标注效果:
图. 1 1 为图表添加各种注释元素
•用两个小圆点表示直线和 llli线的两个交点。
•对 两 个 交 点 之 间 、位于直线和曲线之间的面积进行了填充。
•使叫一个高为整个子图高度、左右边位于两个交点的矩形表示两个交点之间的区间。
• 在 图 ‘11的左上角放置了说明文字。
•对两个交点和填充面积使用了带箭头的注释说明。
首先,〇定义了两个函数ftm cl 和 ftmc2,它们分别萣计算一条直线和一条二次曲线的闲数。
Python科学计算 (第 2 版)
©然后计兑这两个函数在区间(-3,3)上的值,并且调用 plot〇绘制成曲线图。
© 为了标出两个交点,
我们用 find_curve_ intersects〇计算两条 llll线 f l 和 f2 的交点所对应的X
轴坐标 x l 和 x 2。交点处的小丨创点仍然使用plot()进行绘制,这时所传递的 X -Y 轴的数据为单一
的数值,并且以W为样式进行绘图。
如j 可计算两条曲线的交点
当两条曲线的Y 轴坐标值 y l 和 y 2 使用相同的 X 轴坐标数组 x 计算时,很容易计算它们的
交点。首先计算两条曲线在Y 轴的差值 d = y l -y 2 , 然后找到符号相反的两个连续的差值的下标
1(^和丨(^+1。计算直线以阳4,
即(划)-(\阳乂+11,即(^+1〗
)和\轴的交点就可得到两条曲线交点的
X 轴坐标 xc 。如果要计算交点的 Y 轴坐标,只需要调用 np.interp(xc ,x,y l )对曲线进行线性插值
即可。
为单个数值时,它们相当于一个长度为 N 、元素数值都相同的数组。fill_between〇将 填 充 Y 轴
在 y l 和 y 2 之间的部分。
如 果 where 参数为 None ,
就对数组 x 中的所有元素进行填充;如果where
是一个布尔数组,则只填充其中 True 所对应的部分。程序中的数组 x 的取值范围为(-3, 3 ) , 由
于设置了条件 where = fl > f 2 , 因此只绘制直线在二次曲线之上的部分。
© 绘 制 X 轴上在两个交点之间的矩形区域;© 用 textO在图表中添加说明文字;© 最后用
annotate〇为图表添加三个带箭头的注释。
为了真正理解程序的细节,首先需要了解 matplotlib中坐标变换的工作原理。
4.3.1 4 种坐标系
在 matplotlib所绘制的—*幅图表屮,有 4 种坐标系:
•数 据 坐 标 系 :它是描述数据空间中位置的坐标系,例如对于图4^11,它的数据坐标系
的范围为 X 轴在(_3,3)之间,Y 轴在(_2,5)之间。
•子 图 坐 标 系 :描述子图中位置的坐标系,子图的左下角坐标为(0,0),右上角坐标为(1,1)。
•图 表 坐 标 系 :一幅图表可以包含多个子图,并且子图周围都有一定的余白,因此还需
要用阁表坐标系描述图表显示区域中的某个点,阁表的左下角坐标为(0,0),右上角坐
标为(1,1)。
•窗 口 坐 标 系 :它是绘图窗口中以像素为单位的坐标系。左下角坐标为(0,0),右上角坐
标为(width, height)。其 中 的 width 和 height分别是以像素为单位的绘图窗口的内宽和内
高,不包括标题栏、工具条以及状态栏等部分。
A x e s 对象的 transData屈性是数据來.标变换对象,
transAxes 屈性.是子图來标变换对象。 Figure
34
对象的 transFigure屈性适图表來标变换对象。
通过上述坐标变换对象的_ sform〇方法,可以将此坐标系下的坐标转换为窗丨」坐标系中
的坐标。下面的程序计算数据坐标系中的坐标点(-3, -2)和(3,5)在绘图窗口中的坐标:
print type(ax.transData)
ax.transData.transform([(-3,-2)y (3,5)])
[576., 288.]])
下面的程序计兑子图坐标系中的坐标点(0,0)和(1,1)在绘图窗口中的位置,得到的结果和上
面的相同。即子图的左下角坐标(0,0)和数据坐标系中的坐标(-3, -2)在屏幕上是一个点。观察图
4~11可以知道这显然是正确的。
matplotlib绘
[576., 288.]])
最后计兑图表坐标系中坐标点(〇,〇)和(1,1)在绘图窗口中的位置,可以看出绘图R 域的宽为
- 制精美的图表
640个像素,高 为 320个像素:
通过坐标变换对象的 irwertedO方法,可以获得它的逆变换对象。例如下面的程序计兑绘图
窗口中的坐标点(320,160)在数据坐标系中的坐标,结果为(-0.09677419,1.5):
inv = a x . t n a nsData.inverted()
print type(inv)
inv.tra n sf o r m ( (320, 160))
请读者仔细观察程序所输出的图表,子图的上下余白相同,而左侧余白略大于右侧余白,
因此绘图区域的中心点(320, 160)并不是数据区域的中心点(0, 1.5)。
当 调 用 set」dim 〇修改子图所M 示 的 X 轴范围之后,它的数据坐标变换对象也同时发生了
变化:
(-3, 2)
[675.2 288.]
Python科学计算 (第 2 版)
下面回头看看图4-11中绘制矩形区间的程序:
矩形区间使用 fill_between〇绘制。由于所绘制矩形的左右两边要始终经过两个交点,因此
矩 形 的 X 轴坐标必须使用数据坐标系中的坐标:x l 和 x 2。而由于矩形的高度始终充满整个子
图的高度,因此矩形的 Y 轴坐标必须是子图坐标系中的坐标:0 和 1。
使 用 axvspan〇和 axhspan()可以快速绘制垂直方向和水平方向上的区间。
右边通过两个交点,而上下边位于子图边框之上。
4 . 3 . 2 坐标变换的流水线
从一个坐标系变换到另一个坐标系,中间需耍经过儿个步骤。而且数据坐标系不一定是笛
卡尔坐标系,它可能是极坐标系或对数坐标系。因此坐标系的变换并不是简单的二维仿射变换
(2D Affine Transfomation )。让我们从最简中的图表坐标变换对象transFigure开始,介 绍 matplotlib
的坐标变换是如何进行的。
通过本书提供的 G m phvizM PLTm m fcm 可以将坐标变换对象.K 示为关系图,图 ‘12显示了
fig .transFigure的内部结构。
%dot G r a p h v i z M P L T r a n s f o r m .g n a p h v i z ( f i g .t r a n s F i g u r e )
图 . 1 2 图表來标变换对象的内部结构
23(
这个坐标变换对象的内容有些复杂,它 是 一 个 BboxTransformTo 对 象 ,.其中包含一个
TransformedBbox 对象,而 TransformedBbox 对象又包含一个 B b ox 对象和一个 Affine 2D 对象:
• Bbox : 定义一个矩形区域—
— [[xO,y 〇
U x l , y lU 。在本例中,矩形的两个顶点坐标分別
为(0,0)和(8,4),它是窗口的英寸大小,通 过 figsize 参数传递给 figureO 。
• Affine 2D : 二维仿射变换对象,它是一个矩陈,通过它和齐次昀量相乘得到变换之后的
坐标。由于矩阵中只有对角线上的值不为零,因此该仿射变换只进行缩放变换。它将
坐标(X,
y )变换为(80*x ,80*y )。
仿射变换
二维空间的仿射变换矩阵的大小为3 x 3 , 为了进行仿射变换需要使用齐次坐标,即用三维
向量(x ,
y,1)表示二维平面上的点(x ,y ) » 仿射变换就是仿射矩阵和向量的乘积。由于变换矩阵最
下一行的数值始终是(0,0,1),因此有时也将它写成2 x 3 的矩阵形式。
• TransformedBbox: 将矩形区域通过仿射变换之后得到一个新的矩形区域。例子中,所
matplotlib绘
得到的矩形区域的两个顶点为(0, 0)和(640, 320)。为了避免重复运爲,它的_poimS 属性
缓存了这两个顶点的坐标。它正好萣以像素点为单位的窗口的大小,因此仿射变换矩
阵中的数值80实际上是 Figure 对象的 dpi 属性。
- 制精美的图表
• BboxTransformTo:它是一个从单位矩形区域转换到指定的矩形区域的变换。在本例中,
它是一个将矩形区域(0, 〇)-(1,1)变换到矩形区域(0, 〇)-(640,320)的坐标变换对象,因此它
能将坐标从阁表坐标系转换为窗口坐标系中的坐标。其_m tx 属性缓存了该变换矩阵。
fig.transFigiire中的仿射变换对象可以通过fig .dpi_scale_trans获得:
fig.dpi_scale_trans == fig.transFigure._boxout._transform
True
接下来我们杳看子图坐标变换对象的内容(内容结构参见图4-13):
%dot G r a p h v i z M P L T r a n s f o r m .g r a p h v i z (a x .t r a n s A x e s )
图本1 3 子图坐标变换对象的内部结构
ax.tnansAxes._boxout._transform == fig.transFigure
True
而此变换中的矩形区域(0.125,0.1M 0.9,0.9)是子图在图表坐标系中的位置:
a x .get jDosition ()
子图在窗口來标系中的矩形K 域为:
a x .t r a n s A x e s ._ b o x o u t .bounds
是 一 个 BboxTransformFrom 对象,它是一个将指定的矩形区域变换为(0,0)-(1,
1)矩形区域的变换
对象。
- 制精美的图表
阁4 ~ 1 4 数据坐标变换对象的内部结构
而 transLimits的源矩形区域为一个TransformedBbox对象,
它是一个将矩形区域(-3, 5)
通过坐标变换之后的矩形R 域 。而此处的变换由 TmnsformWrappei•对象定义,在图4~14中它
是一个恒等变换。因 此 tm nsLim its 的最终效果就是将矩形区域(_3,
_2)-(2, 5)变换为矩形区域(0,
〇)-(1,1):
[0. 0.]
23:
(-3.0, 2.0)
(-2.0, 5.0)
t = ax.transLimits + ax.transAxes
print t . t r a n s f or m ( ( 0 >0))
print ax.transData.transform((0>0))
[377.6 105.14285714]
[377.6 105.14285714]
matplotlib绘
+ transLimits + transAxes。本例中 transScale 是一个丨H 等变换,因此 ax.transLimits + ax.transAxes 和
ax .transData的变换效果一样:
- 制精美的图表
ax.transScale
T r a n sformWnapper(BlendedAffine2D(IdentityTransform()^IdentityTransform()))
图4~ 15 X 轴为对数坐标时transScale对象的内部结构
由 于 本 例 中 X 轴的取值范围包含负数,因 此 如 果 将 X 轴改为对数坐标,并且重新绘
A 图,会产生很多错误信息。
ax.set_xscale("log") # 将 X 轴改为对数坐标
%dot G r a p h v i z M P L T r a n s f o r m .g r a p h v i z (ax.tr a n s Sc a l e )
239
Python科学计算 (第 2 版)
4 . 3 . 3 制作阴影效果
下面用上节介绍的坐标变换绘制带阴影效果的曲线。完整程序如下,效果如图本16所示:
fig, ax = p i t . s u b p lo t s ()
x = np.anange(0.^ 2 ” 0.01)
y = np.sin(2*np.pi*x)
N = 7 # 阴影的条数
for i in xrange(N, d, -1):
transform=shadow_trans, ©
alpha=(N-i)/2.0/N)
ax.set_ylim((-1.5, 1.5))
1.5
- 制精美的图表
阁4 > 1 6 使用坐标变换绘制的带阴影的曲线
首先使用循环绘制N 条透明度和偏移量逐渐变化的肋线,然后绘制实际的llll线,以实现阴
影效果。
O offset 是一个 ScaledTranslation对象,它的前两个参数决定了 X 轴 和 Y 轴的偏移量,而第
三个参数是一个坐标变换对象,经过它变换之后,再进行偏移变换。由于程序中的第三个参数
是一个恒等变换,因 此 offset 实际上是一个单纯的偏移变换:对 X 轴坐标增加 i , 对 Y 轴坐标
减少i。
下面查看 i 为 1 时的 offset:
offset.transform((0,0)) # 将(0 ,
0) 变换为(1,-1)
24
©阴);如 ll线的來标变换htl shadow_trans兒成,
它由数据少标变换对象transData和 offset组成。
4 . 3 . 4 添加注释
m a t p l b绘
示了在数椐坐标系、子图坐标系以及图表坐标系中添加文字:
x = np.li n s p ac e ( -1,1,10)
- 制精美的图表
y = x**2
figj ax = plt.subplots(figsize=(8>4))
ax.plot(x^y)
图4>17三个坐标系中的文字
m a t p l b绘
绘制文字的闲数还有许多关键字参数用于设置文字、外框的样式,请 读 者 参 考 matplotlib
的用户手册,这里就不再详细介绍了。
- 制精美的图表
通 过 pyplot模块的 annotate()绘制带箭头的注释文字,其调用参数如下:
表 4 - 5 属性值与相应的坐标变换方式
属性值 坐标变换方式
figure points 以点为单位,相对于图表左下角的叱标
figure pixels 以像素为单位,相对于图表左下角的坐标
figure ftnction 图表坐标系中的坐标
axes points 以点为单位,相对于子图左下角的坐标
axes pixels 以像素为单位,相对于子图左下角的坐标
axes fraction 子阁坐标系中的坐标
data 数据坐标系中的坐标
offset points 以点为单位,相对于点x y 的坐标
polar 数据坐标系中的极坐标
/ ^
24
其 ipTig^re fi^action’、 ’axes fraction’
和'data’
分別表示使用图表來标系、子图來标系和数据剩;示
系中的坐标变换对象。由于图表和子图坐标系都是正规化之后的坐标,使用起来不太方便,因
此对于图表和子图还分別提供了以点为单位和以像素为单位的坐标变换方式。点和像素的单位
类似,但是它不会随着图表的dpi 属性值而发生变化,它始终以每英寸72个点进行计算。
上述几种坐标变换都以固定的点为原点进行变换,有时我们希望以距离箭头的偏移董指定
文字的坐标,这时可以使用'offset points’
选项。
在阁本11中,所有注释的箭头坐标都采用’
data’
,因此无论如何放大或平移绘阁区域,箭头
始终指向数据坐标系中的固定点。而注释文木“交点”的坐标变换方式采用’
axesfractk ) ^ , 因此
“交点”始终保持在子图中的同定位置。而 “直线大于 llll线区域”注释文本的坐标采用Offset
points变 换 ,因此文字和箭头的相对位置始终保持不变。
最 后 ,arrowprops 参数是一个描述箭头样式的字典。关于注释样式的详•细配置请参考
matplotlib 的相关文档。
4 4 块 、路径和集合
与本节内容对应的Notebook 为: 04-matplotlib/matplotlib-400-patch-collections.ipynb〇
本々介绍构成绘图元素的几个重要的类,熟练掌掘这些类的用法可以绘制出标准的绘图函
数无法实现的效果,并且能极大地提高绘图速度。
表 4 - 6 命令代码及说明
代码 定义 说明
0 STOP 停止绘图
1 MOVETO 将当前位贾移动到对应的坐标点
2 L1NET0 从当前位置绘制直线到对应的坐标点
3 CURVE3 使用2 个坐标点绘制III丨
线
4 CURVE4 使用3 个坐标点绘制丨III线
79 CLOSEPOLY 关闭多边形
Python科学计算(第2 版)
下面创建一个左下角位于(0,1)、宽为 2 、高 为 i 的 Rectangle矩形对象,并查看与之对应的
Path对象的 vertices和 codes属性:
[ 1。 0小
[ l.L
[0., 1小
[0., 0.]]
对照前面的命令代码表,很容鉍理解矩形萣如何绘制出来的。但萣细心的读者会发现,
vertices 中的坐标并不是我们创建矩形时指定的4 个顶点坐标。这是因为所有的矩形对象都共用
同一个 Path 对象,然后通过前而介绍过的 _ s f o r m 对象将单位矩形的 Path 对象变换到指定的
matplotlib绘
tran = rect_patch.get_patch 一t r a n s f o r m ()
t r a n .transform(rect j D a t h .v e r t i c e s )
[0.,
[0., 1.]])
scpy2.matplotlib.svg_path: 从 S V G 文件中获取简单的路径信息。可以使用该模块将矢
妒 量 绘 图 软 件 创 建 的 图 形 转 换 为 Patch 对象。
ax = plt.gca()
patches = read svg p a t h ("p y t h o n -l o g o .s v g " )
244
for patch in patches:
ax.add_patch(patch)
a x .s e t _ a s p e ct ("equal")
a x .i n v e r t _ y ax i s ()
ax.autoscale()
matplotlib绘
40 60 80
- 制精美的图表
图 4~18使用木书提供的read_svg_path()读 入 SVG文件中的路径并ffi示 为 Patch对象
4 . 4 . 2 集合
表 4-7 Collection类的列表属性及说明
属性 说明
paths 绘阁路径
transfoims 坐标变换对象
cdgecoloi^ 边线颜色
facccoloi's 填充颜色
lincwidtlis 边线宽度
offsets 坐标偏移景
Collection 对象巾的路径从路径來标到屏蒂來标要进行三次处标变换:
• transform: 主坐标变换。
• transforms: 与 Collection 中悔条路径对应的路径变换。
Python科学计算 (第 2 版)
1•曲线集合(LineCollection)
在文件"butterfly.txt"中每一行保存:一条封闭丨111线上各点的坐标: xO yO x l y l x 2 y 2 ...。O 在循
环读入这些坐标时,将第一个点的坐标添加到列表尾部,© 然后将其转换为形状为(N ,2)的数组,
其 中 N 为丨1丨|线上的点数加1。
lines 是一个保存多个数组的列表,
每个数组表示一条丨11|线。
可以通过 LineCollection 绘制 lines
保存的丨丨丨1线集合。© colors 参数设置所有曲线的颜色为黑色,0 也可以通过 cm ap 参数设置所使
用的颜色映射表,曲线的颜色由 a rra y 参数数组中的值和颜色映射表决定。这里将曲线的点数
matplotlib绘
的对数作为颜色映射表的输入值。
lines = [ ]
for line in f:
points = l i n e . s t rip().split()
point s . e xt e n d (p o i n t s [:2]) O
points = np.array(points).reshape(-l, 2) O
l i n e s .a p p e n d (p o i n t s )
a x l .add_ c o l l ec t i o n (l c l )
ax2.add_collection(lc2)
a x.set_aspect("equal")
a x . a u t o s ca l e ()
ax.axis("off")
24
图4 ~ 1 9 使用LineColleclion显示大f i 曲线
matplotlib绘
print "number o f lcl paths:", l e n ( l cl.getj3aths 〇)
print "number of lcl colors:", l e n (l c l .g e t _ e d g e co l o r s ())
print "number o f lc2 colors:", l e n (l c 2 .g G t _ e d g e co l o r s ())
- 制精美的图表
print n p .a l l (l c 2 .g e t _ e d g e c o l o r s () == l c 2 .c m a p ( l c 2 .n o r m ( l c 2 .g e t _ a r r a y ())))
True
下面显示路径变换、主变换、坐标偏移,可以看到唯一起作用的是主变换,它就是数据坐
标变换对象,它将丨11|线上各个点的坐标从数据坐标系转换到屏幕坐标系。
[]
True
使 用 LineCollection 可以绘制颜色或宽度渐变的曲线,例如下而对一个二维平面上的矢董场
积分,并将所得的路径保存到 streams列表中。它的长度为2 5 , 其中每个数组表示一条积分路
径,形状为(50,2)。
x, y = s
P y th o n 科学计算(第2 版)
return 0 . 3 * x - y , 0 . 3 * y + x
return [u, v]
X, Y = np.mgrid[-2:2:5j, -2:2:5j]
init_pos = np.c_[X.ravel(), Y . r a v e l 〇]
t = np.linspace(0, 5, 50)
streams = [ ]
for pos in init_pos:
r = odein t ( f ie l d , pos, t)
streams.append(r)
25 (50, 2)
(25*(50_1),
2,2),也就是由1225条线段组成的集合。我们使用两种数值作为颜色映射表的输入:
time_ value 和 speed_ value 。其 中 tirne_ value 为到达对应处标点所需的时间,speed_value 为对应來
标点处的速度大小,效果如图4-20所示。
- 制精美的图表
lines = np.concatenate([
n p . c o n c a te n a t e ( ( r [ :-1, None, :], r[l:,, None, :]), axis=l)
u, v = field([x, y], 0)
speed_value = np.sqrt(u ** 2 + v ** 2)
a x l .add_ c o l l ec t i o n (l c l )
ax2.add_collection(lc2)
2
41
for ax in axl, ax2:
ax.set 一a s p e c t ("equal")
ax.set_xlim(-10, 10)
ax.set_ylim(-10, 10)
45
AO
JLA
xo
J.4
•1 ZQ
)Ji
IjO
as
matpotlib绘
QO
图 《 0 使 川 LineCollection绘制颜色渐变的llli线
—制 精 美 的 图 表
2•多边形集合(PolyCollection)
def starj3 〇
lygon(x, y, r, theta, n, s ) :
xs = n * np.cos(angles)
ys = n * np.sin(angles)
xs[l::2] *= s
ys[l::2] *= s
xs += x
ys += y
stars = [ ]
for i in nange(1000):
49
Python 科学计算 (第 2 版 )
stars.append(star)
ax.autoscale()
ax.margins(0)
a x .set 一aspect (•.e q u a l 1.)
〇
〇
oo
00
m a tp lo tlib绘
00
—制 精 美 的 图 表
00
0
0 100 200 300 400 S00 600 700 80C
图1 2 1 用 PolyCollection绘制大量多边形
length of edgecolors: 1
3•路径集合(PathCollection)
N = 30
n p .r a n d o m .s e e d (42)
x = np.random.rand(N)
y = np.random.rand(N)
2
5<
size = np.random.randint(20, 60, N)
value = np.random.rand(N)
fig, ax = p i t . s u b p lo t s ()
pc = ax.scatter(x, y, s=size, c=value)
print p c .g e t _ t r a n sf o r m s ().shape
print pc.get_transforms()[0] # 下标为0 的点对应的缩放矩阵
(30, 3, 3)
m atp lo tlib绘
[[5.91607978 0. 0. ]
[0. 5.91607978 0. ]
[0. 0. 1. ]]
- 制精美的图表
散列点的中心位:1!为offsets 经 过 offsetjransform 变换之后的坐标。由下而的结果可以看出
offseuram fom q 变换就是数据坐标变换对象,它将数据空间中的坐标变换为以像素为.中.位的屏
幕坐标,因此 offsets 中保存的就是数据坐标系中的坐标偏移量。
标为0 的点对应的中心坐标
print pc.get_offsets()[0] # F
# 计算下标为0 的点对应的屏幕坐标
print pc.get_offset_transform().tnansform(pc.get_offsets())[0]
print p c .g e t _ o f fset_transform() is ax.transData
[0.37454012 0.60754485]
[212.66351729 134.74900826]
True
print p c .get_ t r a n sf o r m ( )
IdentityT r a n sform()
u'screen'
Python科学计算 (第 2 版)
4.摘圆集合(EllipseCollection)
EllipseCoUectiori用于绘制大量的椭圆,每个椭圆都可以拥有独立的长轴和短轴长度以及旋
转角度。它的参数如下:
EllipseCollection(self, widths, heights, angles, u n i t s = _p o i n t s ', **kwargs)
其中'points’
、Inches'和'dot^为解幕坐标系中的长度,’
dots怕单位为像素点,而joints '和’
inches’
则根据图表对象的D P I 属性按照不同的比例变换为’
dots’
单位。’
width 和height’
分别用子图的宽度
或高度作为长度单位,’
x •和y 则采用数据坐标系中的X 轴 或 Y 轴长度,’
x / 表示采用数据坐杯系
中 的 X 轴 和 Y 轴的长度单位。如果子图的 X 轴 和 Y 轴的长度单位不同,椭圆呈现的旋转角度
与 angles 指定的值也会有所不同。
m a t p o t lib绘
下面的程序演示了 imit为Y 和 的 区 别 ,
效果如图4-22所示,
左图中椭圆的长度单位为’
x 1,
宽度为 X 轴上的网个单位距离,高度为 X 轴上的一个中位距离。由于高度和宽度采用同样的中.
位 ,因此椭圆显示的角度与angles 指定的值相H 。当 unit 为\ y ’
吋,椭圆的高度采用 Y 轴的单位
- 制精美的图表
a x e s [1].add 一collection(eel)
axes[l].axis((-5, 5, -5, 5))
#axes[l].set_aspect("equal")
25:
〇〇
图 4-22 EllipseColletion 的 unit 参数:unit='x'(左图)、unit='xy'(右阅)
5.数据空间中的圆形集合对象
matplotlib绘
CirdeCollection 类,但是它所设置的圆的大小是屏;空间中的大小,无法控制其在数据空间中
的大小。
下而我们 A 定义一个能绘制数据空间中圆形集合的〇^<^(^0>11〇^〇11类:
- 制精美的图表
from matplotlib.collections import CircleCollection^ Collection
from matplotlib.transforms import Affine2D
class D a t a C i r c le C o llection(CirclGCollection):
self._sizes = sizes
def draw(self, r e n d e r ) :
ax = self.axes
ms = n p .z e r o s ((l e n (s e l f ._ s i z e s ), 3, 3))
m s [ :j Qj 0] = self._sizes
m s [ :^ lj 1] = self._sizes
ms[:j 2, 2] = 1
s e l f •一transforms = ms O
m = ax.transData.get_affine().get_matrix().copy()
m[:2, 2:] = 0
self.set_transform(Affine2D(m)) ©
〇设置每个丨创形对应的路径变换,它们将IM丨形缩放指定的倍数。©使用数据坐标转换对象
的缩放系数创建一个新的二维仿射变换对象,并将其设置为主变换。路径变换与主变换叠加起
来,就将半径为1 的单位_ 缩放为数据空间中半径为sizes 的1MI形。
每个岡形对应的路径变换将单位岡变换为数据坐标系中指定大小的岡形,然后通过主坐标
变换将数据坐标系中的圆形变换为屏幕坐标系中的大小,最后再由坐标偏移变换将圆形放到指
定的位置。
下而用 DataCircleColleclion 绘制一幅漂高的丨创形拼阁,效果如阁4~23所示。’
'venus-face.csvn
中的每一行有6 个数值表示一个圆形,每个数值的含义为:圆形 X 轴坐标、圆形 Y 轴坐标、半
径、红色、绿色和蓝色。
a x e .set_rasterized(True)
cc = DataCircleCollection(sizesJ facecolors=colors, e d gecolors="w"> l i newidths=0.1J
—制 精 美 的 图 表
offsets=offsetSj t r a n s O f f s e t = a x e .t r a n s D a t a )
a x e .add_ c o l l ec t i o n (c c )
axe.axis((0, 512, 512, 0))
axe.axis("off")
4 . 5 . 1 对数坐标图
I
K
.4
.2
I
.0
.8
n
.6
4n a
L
.2
.0
•
f
l
j
y
阁4 ~ 2 4 低通滤波器的频率响应:箅术坐标(左上)、X 轴对数坐
标(右上)、Y 轴对数坐标(左下)、双对数坐标(右上)
「 I 4 . 5 . 2 极坐标图
| ! 极坐标系是和笛卡尔坐标系完全不同的坐标系,极坐标系中的点由一个夹角和一段相对中
| | 心点的距离表示。下面的程序绘制极坐标图,效果如图4-25所示。
斤 ;
绘 theta = np.arange(0, 2*np.pi, 0.02)
制
f t
美 pit.subplot(121^ polar=True) O
的
图 pit.plot(theta, 1.6*np.ones__like(theta), linewidth=2) ©
表 pit.plot(3*theta, theta/3^ linewidth=2)
pit.subplot(122., polar=True)
pit.plot(theta, 1.4*np.cos(5*theta), linewidth=2)
pit.plot(theta, 1.8*np.cos(4*theta), linewidth=2)
plt.rgrids(np.arange(0.5, 2, 0.5), angle=45) ©
plt.thetagrids([0, 45]) O
SO"
图4 ~ 2 5 极坐标中的圆、螺旋线和攻瑰线
256
O 调)|j subploto创建子图时通过设置 polar 参数为 True ,创建一个极坐标子图。©然后调用
plot()在极坐标子图中绘图。也可以使叫 polar()直接创建极坐标子图并在其中绘制曲线。
© rgrids()设置同心丨创栅格的半径大小和文字标注的角度。因此右图中的虚线N 圈有三个,
半径分别为0.5、1.0和 1.5,这些文字沿着45°线排列。OthetagridsO设置放射线栅格的角度,
因此右图中只有两条放射线栅格线,角度分别为0°和 45°。
4 . 5 . 3 柱状图
柱状图用其每根柱子的长度表示值的大小,它们通常用来比较两组或多组值。下面的程序
从文件中读入中国人口的年龄的分布数据(人口分布数据由维基百科提供,仅供参考,不保证正
确性),并使用柱状图比较男性和女性的年龄分布,效果如图4-26所示。
matplotlib绘
plt.bar(data[:,0]-width, data[:,l]/le7, width, color=cl, la b e l = u " 男")©
plt.bar(data[:,0], data[:,2]/le7, width, color=c2, label=u" 女••)©
pit.xlim(-width, 100)
- 制精美的图表
plt.xlabel(u" 年龄")
pit •ylabel (u __人口(
千万)•_)
p i t . l e g e nd ()
图本2 6 中国男女人口的年龄分布图
读入的数据中,第 -列 为 年 龄 ,它将作为柱状图的横少标。〇首先计箅柱状图中每根柱子
的宽度,因为要在每个年龄段上绘制两根柱子,因此柱子的宽度应该小于年龄段的二分之一。
这里以年龄段的0.4倍作为柱子的宽度。
Python 科学计算 (第 2 版)
© 调)|j bar〇绘制男性人口分布的柱状图。它的第一个参数为每根柱子的左边缘的横坐标,
为了让男性和女性的柱子以年龄刻度为中心,这里让每根柱子左侧的横坐标为“年龄减去柱子
的宽度”。bar()的第二个参数为每根柱子的高度,第三个参数指定所有柱子的宽度。当第三个
参数为序列时,可以为每根柱子指定宽度。
© 绘制女性人口分布的柱状图,这里以年龄为柱子的左边缘横坐标,因此女性和男性的人
口分布图以年龄刻度为中心。由 于 bai<)不向动修改颜色,因此程序中通过 co lo r 参数设觉两个
柱状图的颜色。
4 . 5 . 4 散列图
使 用 pbt 〇绘图时,如果指定样式参数为只绘制数据点,那么所绘制的就是•一幅散列图。
例如:
但是这利「
方法所绘制的点无法单独指定颜色和大小。scatteit)所绘制的散列图可以指定每个
m a t p l b绘
点的颜色和大小。下面的程序演示了 scatterO的用法,效果如图本27所示。
plt.figure(figsize=(8<> 4))
- 制精美的图表
x = np.random.random(100)
y = np.random.random(100)
plt.ylim(0, 1)
合 ☆
it ☆
☆
02
图本2 7 可指定点的颜色和大小的散列
大小,其值和点的面积成正比,可以是单个数值或数组。
25:
C 参数指定每个点的颜色,也可以是数值或数组。这里使用一维数组为每个点指定了一个
数值。通过颜色映射表,每个数值都会与一个颜色相对应。默认的颜色映射表中蓝色与最小值
对应,红色与最大值对应。当 c 参数楚形状为(N ,
3)或(N ,
4)的二维数纟11时,则直接表示每个点的
R G B 颜色。
marker参数设置点的形状,可以是一个表示形状的字符串,或是表示多边形的两个元素的
元组,第一个元素表示多边形的边数,第二个元素表示多边形的样式,取值范围为0、1、2、3。
0 表示多边形,1表示星形,2 表示放射形,3 表示忽略边数显示为圆形。
最后,通 过 alpha 参数设置点的透明度,lw 参数设置线宽,它 是 linewidth 的缩写。 facecoloni
参数为nnonen表示散列点没有填充色。
4 . 5 . 5 图像
imread()和 imshow()提供了简单的图像载入和品示功能。imrcad()可以从图像文件读入数据,
得到一个表示图像的N um Py 数组。它的第一个参数是文件名或文件对象,format参数指定图像
类型,如果省略则l:tl 文件的扩展名决定图像类型。对于灰度图像,它返回一个形状为(M ,N )的
matplotlib绘
数组;对于彩色图像,它返In丨形状为(M ,
N,C )的数组。其 中 M 为图像的高度,N 为图像的宽度,
C 为 3 或 4 , 表示图像的通道数。下面的程序从 len ajp g 中读入图像数据,效果如图4-28所示。
- 制精美的图表
所得到的数组 im g 是一个形状为(393, 512, 3)的单字节无符号整数数组。这是因为通常所使用的
图像采用单字节分别保存每个像素的红、绿、蓝三个通道的分
则每个像素的颜色由各个通道的值决定。
© imshow()所绘制图表的 Y 轴的正方向是从上往下的。如果设置 imshow〇的 origin 参数为
"lower”
,则所显示图表的原点在左下角,但是整个图像就上下颠倒了。
© 如果三维数组的元素类型为浮点数,则元素值的収值范围为0.0到 1.0,与颜色值0 到 255
对应。超过这个范围可能会出现颜色异常的像素。下面的例子将数组 im g 转换为浮点数组并用
imshow〇进行显示,由于数值范围超过了 0.0〜 1.0,因此颜色显示异常。
〇而取值在0.0〜 1.0的浮点数组和原始图像完全相同。
© 使 用 clip()将超出范围的值限制在取值范围之内,可以使整个图像变亮。
© 如 果 imsh〇
w ()的参数是二维数组,则使用颜色映射表决定每个像素的颜色。这里显示图
像中的红色通道,它是一个二维数组。其显示效果比较吓人,因为默认的图像映射将最小值映
射为蓝色、将最大值映射为红色。可以使用 cobrbarO 将颜色映射表在阁表中显示出来。
© 通 过 imshow()的 cm ap 参数可以修改显示阁像时所采用的颜色映射表,使用名为 copper
的颜色映射表显示图像的红色通道。
Python 科学计算 (第 2 版 )
img = pit.imnead("lena.jpg")
fig, axes = p i t . s u b p lo t s (2, 4, figsize=(ll, 4))
axes = a x e s . r a v el ()
a x e s [ 0 ] .imshow(img) O
a x e s [1].imshow(img^ origin="lower") ©
axes[2].imshow(img * 1.0) €)
axes[3].imshow(img / 255.0) O
axes[4].imshow(np.clip(img /200.0, Q, 1)) 0
p i t •colorbar(axe_img, ax=axes[6])
for ax in axes:
- 制精美的图表
a x .s e t _ a x i s _o f f ()
import matplotlib.cm as cm
cm._cmapnames[:5]
使 用 imshow()可以显示任意的二维数据,例如卜面的程序使用图像直观地显示了二元阑数
f (x ,y ) = x e x2_y2,效果如图 4^29 所示。
y, x = np.ognid[-2:2:200j, -2:2:200j]
p i t .f i g u r e ( f i g s i z e = (10,3))
p i t . s u b p lo t (121)
plt.imshow(zJ extent=extent, origin="lower") ©
plt.colorbar()
p i t . s u b p lo t (122)
plt.colorbar()
matplotlib绘
- 制精美的图表
图4 ~ 2 9 使川imshow ()可视化二元函数
4 . 5 . 6 等值线图
还可以使用等值线图表示二元闲数。所谓等值线,是指由闲数值相等的各点连成的平滑曲
线。等值线可以直观地表示二元函数值的变化趋势,例如等值线密集的地方表示函数值在此处
的变化较大。matplodib 中可以使用 contour〇和 contourf〇描绘等值线,它们的区别是 contourf()所
得到的是带填充效果的等值线。下面的程序演示了这两个函数的用法,效果如图4~30所示:
y, x = np.ogrid[-2:2:200j, -3:3:300j] O
z = x * np.exp( - x**2 - y**2)
plt.clabel(cs) ©
p i t . s u b p lo t (122)
图4 ~ 3 0 用 contour(左)和 contourf(右)描绘等值线图
- 制精美的图表
〇为了更淸楚地区分X 轴 和 Y 轴,这里让它们的収值范围和等分次数均不拥司。这样所得
到的数组 z 的形状为(200,300),它的第0 轴对应 Y 轴 ,第 1轴对应 X 轴。
© 调 用 contourO绘制数组 z 的等值线图,第二个参数为10表示将整个函数的収值范围等分
为 10个区间,即其所显示的等值线图中将有9 条等值线。和 imshow()—样,可 以 使 extent参
数指定等值线图的 X 轴 和 Y 轴的数据范围。© contour〇所返丨El的楚一个 QuadContourSet对象,
将它传递给 dabel〇 ,为其中的等值线标上对应的值。
O 调 用 contomfO绘制带填充效果的等值线图。这里演示了另一种设置X 、Y 轴 取 范 围 的
方法。它的前两个参数分别是计算数组z 时所使用的 X 轴 和 Y 轴上的取样点,这两个数组必须
是一维数组或是形状与数组z 相同的数组。
如果需要对散列点数据绘制等值线图,可 以 先 使 用 scipy.inteipolate模块中提供的插值
妒 函数将散列点数据插值为网格数据。
还可以使用等值线绘制隐函数刖线。所谓隐函数,是指在一个方程中,若 令 x 在茶一E 间
内取任意值时总有相应的y 满足此方程,则可以说方程在该区间上确定了 x 的隐函数 y , 如隐
函数x 2 + y 2 - 1 = 0表示一个单位圆。
M 然无法像绘制一般函数那样,先创建一个等差数组表示变量x 的収值点,然后计算出数
y, x = np.ogrid[-1.5:1.5:200j, -1.5:1.5:200j]
f = (x**2 + y * *2)**4 - (x**2 _ y**2)**2
plt.figure(figsize=(9, 4))
p i t . s u b p lo t (121)
p i t . s u b p lo t (122)
for c in cs.collections: ©
data = c.getjDaths()[0].vertices
p i t •p l o t ( d a t a 0], data
matplotlib绘
c o l o r = c .g e t _ c o l o r ()[0], l i n e w i d t h = c .g e t _ l i n e wi d t h ( )[0])
- 制精美的图表
设 置 levels 参数为[0, 0.1],因此最终将绘制两条等值线。通 过 colors、linestyles、linewidths等参
数可以分别指定每条等值线的颜色、线型以及线宽。
仔细观察图4-31(左)会发现,表示隐函数f (x ,y ) = 0 的蓝色实线并不是完全连续的,在图的
中间部分它由许多孤立的小段构成。因为等值线在原点附近无限靠近,所以无论对函数 f 的取
值空间如何进行细分,总是会有无法分开的地方,最终造成了阁中的那些孤立的细小区域,而
表示隐函数f (x ,y ;
) - 0.1 = 0 的红色虛线则是闭合且连续的。
© 从等值线集合c s 中找到表示等值线的路径,并使用pbt 〇将其绘制出来,
效果如图4~31(右)
所示。
图使用等值线绘制隐函数曲线(左),获取等值线数据并绘图(右)
6
2
4/
Python科学计算 (第 2 版)
print cs
cs.collections
print c s . c o l l e ct i o n s [ 0 ] .get_color()[0]
print c s . c o l l e ct i o n s [ 0 ] .get_linewidth()[0]
[0. 0. 1. 1.]
2
在前面的章节介绍过LineCollection 对象是一组丨III线的集合,因此它可以表示蓝色实线那样
matpotlib绘
由多条线构成的等值线。它 的 get_paths()方法获得构成等值线的所有路径,本例中蓝色实线所
表示的等值线由4 2 条路径构成:
- 制精美的图表
l e n (c s .c o l l e c t i on s [0].g e t _ p a t h s ())
42
path.vertices
a r r a y ([[-0.08291457, -0.98938936],
[ - 0 . 0 9 0 3 92 6 9 , - 0 . 9 8 7 4 3 7 1 9 ] ,
[-0.09798995, -0.98513674],
• • •J
[-0.05276382, -0.99548781],
[-0.0678392 , -0.99273907],
[-0.08291457, -0.98938936]])
4 . 5 . 7 四边形网格
pcolomesh (X ,
Y ,C )绘制由 X 、
Y 和 C 三个数组定义的四边形网格。
这三个数组是二维数组,
X 和 Y 的形状相同,C 的形状可以和 X 、Y 相同,也可以比它们少一行一列。每个四边形的4
个顶点的 X 轴坐标由 X 中上下左右相邻的4 个元素决定,Y 轴坐标由 Y 中对应的4 个元素决定。
四边形的颜色由C 中对应的元素以及颜色映射表决定。
在下面的例子中,X 和 Y 的形状都是(2,3),其中有两组上下左右相邻的4 个元素,定义两
个四边形的4 个顶点:
第一个四边形的顶点第二个四边形的顶点
每个四边形的填充颜色与Z 中的一个元素对应:
X = np.array([[0, 1, 2],
[〇, 1, 2]])
Y = np.array([[0, 0.2, 0],
matplotlib绘
pit.plot(X.ravel(), Y.ravel(), "ko")
plt.pcolormesh(X, Y, Z)
p i t . m a r g in s (0.1)
- 制精美的图表
1.0 -
0.8 -
0.6 -
0.4 -
0.2 -
o.o -
图4 * 3 2 演示pcolormeshO绘制的卩q 边形及其填充颜色
def make一mesh(n):
x, y = np.mgrid[-10:0:n*lj, -5:5:n*lj]
65
Python 科学计算 (第 2 版)
s = x + lj*y
z = (2 + s) / (2 - s)
return s, z
axes = a x e s . r a v el ()
for ax in axes:
ax.set 一a s p e c t ("equal")
si, zl = make_mesh(10)
s2, z2 = make_mesh(200)
图 使 用 pcolormeshO绘制复数平面上的生标变换
66
还可以在极坐标中使用 pcolormeshO绘制网格,下面的例子使叫 mgridU创建极坐标中的等
间隔网格,然后在 projection 为 polai•的子图中绘制这个网格:
y = theta * np.sin(r)
return np.sqrt(y*y)
T, R = np.mgrid[0:2*np.pi:360j, 0:10:100j]
Z = func(T, R)
ax.pcolormesh(Tj R, Z, nasterized=True)
90-
270*
图 4-34 使川 pcolormesh()绘制极坐标中的网格
4 . 5 . 8 三角网格
在工业工程设计与分析中,经常将分析对象使用三角网格离散化,然后用有限元法进行模
拟。在 matplotlib中提供/ 下面的三角网格绘制函数:
• tiiplot〇: 绘制三角网格的边线。
• tripcolor〇: 与 pcolormesh〇类似,绘制填充颜色的三角网格。
• tricontour〇和 tricontourfO: 绘制三角网格的等高线。
diffusion.txt楚使jij F iP y 对二维稳态热传导问题进行有限元模拟的结果。该文件分为三个
部分:
• 以 # points开头的部分是一个形状为(N _points,
2)的数组,保 存 N _points 个点的坐标。
• 以 # triangles开头的部分足一个形状为(N_triangles,
3)的数组,
保存每个三角形三个顶点在
points 数组中的下标。
• 以 # v a lu e s 开头的部分是一个形状为(N 」riangles ,1)的数组,保存•每个三角形对应的
温度。
下面的程序将这些数据读入data 字典:
Python科学计算 (第 2 版)
with o p e n ("diffusion.txt") as f:
values = d a t a [ l i n e[ l : ]]
continue
然后就可以调用 trip*〇,用三角形网格品示目标区域的温度,结果如图4-35所示。
Otripcolor〇的参数从左到右分別为各点的X 轴坐标、Y 轴坐标、三角形顶点下标、标量数
matpotlib绘
组。标量数组屮的每个值可以与每个顶点对应,也可以与每个三角形对应。在本例屮由于 values
的长度与 triangles的 第 0 轴长度相同,因此每个值与三角形相对应。若标量数纟11的长度与顶点
数相同,则每个三角形对应的值丨:tl 其三个顶点的平均值决定。
- 制精美的图表
© 调 用 triplotO绘制所有三角形的边线。© 调 用 tricomourO绘制等高线。由于要求标量数组
与三角形顶点相对应,而本例中标量数组与三角形对应,因此先计算每个三角形的重心坐标
X c 和 Y c ,这 样 values 中的每个值就可以与每个三角形的重心对应。在调用 tricontourO时没有传
递三角形顶点下标信息,
这吋会调用 matplotlib A 带的三角化算法计算出每个三角形对应的顶点。
X, Y = data["points"].T
triangles = dat a [ " t r ia n g l e s " ] .astype(int)
values = d a t a ["values"].s q u e e z e ( )
Xc = X[triangles],mean(axis=l)
Yc = Y[triangles].mean(axis=l)
pit.tricontour(Xc, Yc, values, 10) €)
6
8
ao Q5 1.0 13 7.0
4 . 5 . 9 箭头图
matplotlib绘
使 用 quiverO可以用大董的箭头表示矢量场。下面的程序显示f (x ,y ) = x e x2_y 2的梯度场,
结果如图丰36所示。vec_field(f ,X,
y )近似计爲函数 f 在 x 和 y 处的偏导数。
- 制精美的图表
quiver〇的 前 5 个参数中,X 、Y 是箭头起点的 X 轴 和 Y 轴坐标,U 、V 是箭头方向和大小
的矢量,C 是箭头对应的值。
x2 = x + dx
y2 = y + dy
v = f(x, y)
vx = (f(x2, y) - v) / dx
vy = (f(x, y2) - v) / dy
return vx, vy
X, Y = np.mgrid[-2:2:20j, -2:2:20j]
C = f(X, Y)
U, V = vec 一
field(f, X, Y)
p i t •quiver(X) Y, U, V, C)
plt.colorbar();
plt.gca().set_aspect("equal")
69
P y th o n 科学计算(第2 版)
m a t p l b绘
此外,quiverO还提供许多参数来配置箭头的大小和方向:
• 箭 头 的 长 度 由 sc a le 和 scale_units 决定。其 中 s c a le 为数值,表示箭头的缩放尺度,而
- 制精美的图表
scale_units 为箭头的长度单位,可选单位有'width’
、height’
、’dots'、’
inches' ’
x '、’
y '、’
xy '
等。其中\vid thV height 为子图的宽和高,’
dots刺 inched以点和英寸为单位,\'、y 、bey’
则以数据坐标系的X 轴、Y 轴或单位矩形的对角线为单位。箭头的长度按照“U V 矢量
的长度* 箭头的长度单位/缩放尺度”计算。例如,如 果 scale 为 2,scale_units 为Y ,而
U V 矢量的长度为3 , 贝U对应的箭头的长度为1.5个 X 轴的单位长度。
• width 、headwidth、headlength和 headaxislength等参数决定箭头的杆部分粗细、箭头部分
的大小以及长度,而 units 参数决定这些参数的单位, nj'选 值 与 scale_units 相同。这些
参数的含义如图4-37所示。
阁4~37 quiver箭头的各个参数的含义
• p ivo t 参数决定箭头旋转的中心,可 以 为 ’
middle'、’
tip’
等值,在 图 中 使 用 灰 色
圆点表示这些旋转点。
o
27
• angles 参数决定箭头的方向。正方形可能由于 X 轴 和 Y 轴的缩放尺度不N 而显示为长
方形,因此方向有两利1计箅方式:W 和’
xy ’
。其•中W 只 采 用 U 和 V 的值计箅方向,因
此 若 U 和 V 的值相同,贝lj方向为45度;而’
xy ’
在使W U 和 V 计算角度时考虑 X 轴 和 Y
轴的缩放尺度。
下面通过两个例子帮助读者理解这些参数的用法,如图个38所示。首先绘制了一条参数曲
线,然后沿着该丨111线绘制了 4 0 个等分曲线的箭头,箭头的方向表示箭头处曲线的切线方…J,
颜色表示箭头所在处参数的大小。计算部分留给读者A 行分析,下而仔细分析这些参数是如何
决定箭头的大小和方向的。
箭头的长度和其他尺寸的单位由scale_units和 units 决走,在本例中均为'dots',即以像素点
为中.位。d x 和 d y 为描述箭头的矢量,长 度 为 1,将 scale 参数设置为1.0/arrow_size ,这样所有
箭头的长度均为arrow_siZe 个像素点。
箭杆的宽度由width 参数指定,
本例中的宽度为1个像素。
而 headwidth、headlength和 headaxislength等参数决定箭头部分的宽度、长度以及箭头与箭杆接
触部分的长度,这些参数为对应长度与箭杆宽度的比例系数。在本例中,nln于箭杆宽度为1个
像素,因此箭头宽度为aiT〇
w _SiZe* 0.5个像素,而箭头部分的长度和箭头的长度相同,因此图
matplotlib绘
中的箭头没有箭杆部分。
由于子图的X 轴 和 Y 轴的缩放比例不同,冈此设置 angles 参数为”
xy ”
,这样箭头的方向才
- 制精美的图表
能与曲线的切线方向相同。
n = 40
arrow 一size = 16
t = np.linspace(0, 1, 1000)
x = np.sin(3*2*np.pi*t)
y = np.cos(5*2*np.pi*t)
length = l e n g t h s [-1]
dy /= ds
p i t •quiver(x[index], y[index], dx, dy, t[index],
units="dots"> scale_units="dots _、
angles=__xy", s c a l e = l .0/arrow_size, pivot="middle",
edgecolors="black", linewidths=l,
width=l, headwi d t h= a r r o w _ s i z e * 0 .5,
headlength=arrow_size, headaxislength=arrow_size^
Python科学计算 (第 2 版)
zorder=100)
plt.colorbar()
plt.xlim([-1.5, 1.5])
plt.ylim([-1.5, 1.5])
m a t p l b绘
阁使用箭头表示参数丨III线的切线方向
还 可 以 用 quiverO绘制起点和终点的箭头集合。下面的例子绘制神经网络结构示意图,效
- 制精美的图表
levels = [4, 5, 3, 2]
x = np.linspace(0, 1, len(levels))
j = i + 1
nl, n2 = levels[i], levels[j]
headaxislength=10, headwidth=4)
xp = np.repeat(x, levels)
p i t •plot(xp, yp, " o ' ms=12)
p i t .g c a ().a x i s ("off")
p i t •m a r g i n s (0.1) 0.1)
27:
r
阁 使 用 quiverQ绘制神经网络结构示意阁
4.5.10 .维绘图
matplotlib绘
import mpl_toolkits.mplot3d O
- 制精美的图表
x, y = np.mgrid[-2:2:20j, -2:2:20j] ©
z = x * np.exp( - x**2 - y**2)
2.0 •邡
阁4 ~ 4 0 使用mplot3D 绘制的三维丨II丨而阁
Python科学计算 (第 2 版)
投影模式
投影模式决定了点从数椐坐标转换为屏幕坐标的方式。可以通过下面的语句获得当前有效
的投影模式的名称:
'mollweide’
等均为地图投影,’
polar’
为极坐标投影,’
rectilinear'则是默认的直线投影模式。
- 制精美的图表
作为本章的最后一节,让我们介绍一些比较特别的用法。
4 . 6 . 1 使 用 a gg 后台在图像上绘图
74
功能,我们可以立接在内存屮绘制图像,然后将其转换成 N um Py 数组。
下面的代码载入RendererAgg(画布),
并创述一个长宽都是250个像素的 RendererAgg 对象,
其第三个参数为 DPI , 该参数不影响画布的大小。其 buffer_ rgba〇方法获得画布中保存绘图结果
的缓存,通 过 frombuffer〇将该缓存转换为 N um Py 数组,并按照画布的大小调用reshape〇。最后
得到的数组 an*的形状为(250,250,4),其中第2 轴 的 4 表示画布有4 个通道:红、绿、蓝、透明。
import numpy as np
from matplotlib.backends.backend_agg import RendererAgg
w, h = 250, 250
renderer = RendererAgg(w, h, 90)
buf = r e n d e r e r .b u f f e r _ r gb a ()
(250, 250, 4)
matplotlib绘
RendererAgg 对象提供 T 一 些 draw_*()方法用于在I叫布上绘图,例如下面的代码首先创建一
个 Path 对象,然后调用 renderer.draw_path〇在 N 布上绘制该 Path 对象,如图4~41所示。其第一
- 制精美的图表
个参数为一个 GraphicsComextBase对象,用来设置绘图吋的一些屈性,例如线宽、线条颜色等。
第三个参数是•个來标变换对象,在本例中使用恒等变换,第 4 个参数为路径的填充颜色。
path_data = [
(Path.MOVETO, (179, 1)),
gc = r e n d e r e r .n e w _ g c ()
g c .s e t _ l inewidth(2)
gc.set_foreground((l, 0, 0))
P y th o n 科学计算(第2 版)
gc.set_antialiased(True)
text.figure = Tenderer
t e x t .draw(renderer)
m a t p l b绘
png_buf = BytesIO()
p i t .i m s a v e (png_buf, arr, format="png")
d i s p l a y j Dn g ( p n g _ b u f .g e t v a l u e (), raw=True)
图4 ~ 4 1 直接使用RendcrcrAgg绘图
本书提供了一个可以在图像上绘图的ImageDrawer类 。下 而 使 用 ImageDrawer在图像上绘
制标记、文字、直线、圆形、矩形以及椭圆,并使用木书提供的% airayjm age 魔法命令将结果
图像显示在 N otebook 中,结果如图 4*42 所示。
27(
ImageDrawer 的 reverse 参数决定 Y 轴的方向,默认值为 True , 表 示 Y 轴方向向下,和图像
的像素坐标系方向相同,False 表 示 Y 轴方向向上,和数学上的笛卡尔坐标系的定义相同。
drawer = ImageDrawe r (i m g )
d r a w e r .setj3arameters(lw=2, color="white", alpha=0.5)
%array_image drawer.to_array()
图 4*42使用木书提供的ImageDrawer在图像上绘图
4 . 6 . 2 响应鼠标与键盘事件
界面111的壤件绑定都是通过Figure.canvas.mpl_connect()进行的,
它的第一个参数为劇牛名,
第二个参数为事件响成函数,当指定的事件发生时,将调j l j 指定的函数。
P y th o n 科学计算(第 2 版)
%gui qt
%matplotlib qt
1.键盘事件
下面的程序响应键盘按键,并输出按键的值:
scpy2.matplotlib.key_event_show_key : 显示触发键盘按键事件的按键名称。
DVD
import sys
fig, ax = p i t . s u b p lo t s ()
def o n _ k e y _ p r e s s ( e v e n t ) :
print e v e n t .key
matpotlib绘
s y s .s t d o u t .f l u s h ()
control
ctrl+a
alt
alt+1
表4 - 8 支持的事件名及含义
事件名 含义
button_pixjss_cvcnt 按下鼠标按键
button一
release—event 释放鼠标按键
draw—event 界而M 新绘制
key_press_event 按下键盘上的按键
key_release_event 释放键盘按键
motion_notiiV_event 鼠标移动
pick_event 鼠标点选绘图对象
scroll_event 麵滚轴事件
figure_enter_event 鼠标移进图表
27:
(续表)
事件名 含义
d o s e 一even t 关闭图表
当前所有注册的响应函数可以通过Figure.canvas.callbacks.callbacks 查看。下而的程序输出所
有的事件响应函数,响应函数的执行按照显示的顺序进行,可以看出除了我们绑定的响应函数
之外,matplotlib还绑定了处理快捷键的键盘响应函数。
key_press_event
motion_notify_event
4:matplotlib.backend— b a s e s •〈function mouse— move at 0 x 0 9 3A4FB0>
scroll 一event
2:matplotlib.backend_bases.<function pick at 0x093A40F0>
button_press_event
1:matplotlib.backend_bases.〈function pick at 0x093A40F0>
下而的程序通过键盘按键修改曲线的颜色。由于某些按键勺默认的快捷键重复,因此这里
通 过 mpl_disconnect〇取消默认快捷键的响应函数的绑定,其参数为调用 mpl_connect〇时所返回
的整数。默认快捷键的响应函数对应的整数可以通过Figure.canvas.manager.key_press_ handler_id
获得。
O 在响应函数中通过 line.set_color〇修改肋线的颜色,© 并调用 Figure.canvas.draw_ idle〇重新
绘制整个图表。
fig, ax = p i t . s u b p lo t s ()
x = np.linspace(0, 10, 1000)
line, = ax.plot(x, np.sin(x))
Python 科学计算 (第 2 版)
def o n _ k e y _ p re s s (e v e n t ) :
if event.key in 'rgbcmyk':
line.set_color(event.key) O
fig•canvas•draw一idle() 0
fig•canvas•mpl一disconnect(fig•canvas•manager•key_press一handler一id)
fig.canvas.mpl_connect('keyj3ress_event', on_key_press)
2.鼠标事件
当鼠标在子图范围内产生动作时,将触发鼠标事件。鼠标事件分为三种:
• "buttorLpressDevenf:鼠标按键按下时触发。
• 'button_release_event’
: 鼠标按键释放时触发。
• 〗
notion_notify_evenf:鼠标移动时触发。
鼠标事件的相关信息可以通过event 对象的属性获得:
m a t p o t l i b绘
• name: 事件名。
• button: 鼠标按键,1、2、3 表示左中右按键,N one 表示无按键。
• x,
y : 鼠标在图表中的像素坐标。
- 制精美的图表
• xdata,
ydata: 鼠标在数据坐标系中的坐标。
下面的程序显示了鼠标事件的各种信总:
import sys
fig, ax = pit.s u b p lo t s ()
def o n _ m o u s e (e v e n t ):
global e
e = event
info = "{}\nButton:{}\nFig x,y:{}, {}\nData , { : 3 . 2 f } ".format(
1.0
0.8
m o tio n _ n o tify _ e v e n t
0.6
B u tto n :N o n e
0.4 Fig x ,y :213, 206.0
D a ta x ,y :0.27, 0.85
0.2
图4 4 3 显示鼠标事件信息
下而的例子通过响应上述三个鼠标事件,实现阁表中形状的移动。我们将所有的事件响应
封装到 PatchMover类中,它有三个内部使用的属性:
• selected_patch: 保存当前被选中的 Patch 对象。
matplotlib绘
• start_ mouse_pos : 保 存 Patch 对象被选1丨
1时鼠标在子图中的來标。
• start_patch_pos: 保 存 Patch 对象被选中时在子图中的坐标。
O 在 on_ press()中,对子图中的所有 Patch 对象进行循环判断,这里采用 zordei•属性相卜:序之
- 制精美的图表
后的逆序循环,保证在最上层的 Patch 对象优先被选中。© 通 过 Patch.contains_point()判断当前的
鼠标坐标是否在 Patch 对象之内,注意这里需要使用图表坐标系中的像素坐标。一旦判断鼠标
在当前的 Patch 对象之中,贝《
保存当前 Patch 对象,以及相关的坐杯信息。注意我们保存数据坐
标系的坐标,因为 Patch 对象的移动是在数据坐标系中进行的。
©在〇 11_111(^〇11()中,通过当前的鼠标坐标计算被选中 Patch 对象的当前位置,所有的计算
都在数掘坐标系中进行。O 调 用 Figure.canvas.dmw_idle()重新绘制整个阁表。
© 在 on_release〇中,取消被选中的 Patch 对象。
class P a t c h M over(object):
self.ax = ax
s e l f .selected jDatch = None
s e l f .start_mouse_pos =: None
s e l f .startjDatch_pos =: None
Python科学计算 (第 2 版)
fig = ax.figure
fig.canvas.mpl_connect('button一press_event', self.on_press)
fig.canvas•mpl_connect('button一release一event、 self.on_release)
fig.canvas.mpl_connect('motion_notify_event', self.on一
motion)
for patch in r e v e r s e d (p a t c h e s ):
if patch.containsj3 〇
i n t ( (event.x, event.y)): ©
self.selected jDatch = patch
self,start_mousej3 〇
s = n p . a r r a y ([event.xdata, event.ydata])
s e l f .start jDatchjDos = patch.xy
break
matpotlib绘
def on_motion(self, e v e n t ) : ©
if self,selected_patch is not None:
self.selected 一
patch.xy = self.startj3atch_pos + pos - self.start_mouse_pos
s e l f .a x .f i g u r e .c a n v a s .d r a w _ i d l e () O
def on_release(self, e v e n t ) : 0
s e l f .selected_patch = None
fig, ax = p i t . s u b p lo t s ()
a x •set 一a s p e c t ("equal")
for i in r a n g e (10):
poly = RegularPolygon(rand(2), randint(3, 10), rand() * 0.1 + 0.1, facecolor=rand(3),
ax.autoscale()
pm = PatchMover(ax)
plt.show()
3.点选事件
上 而 通 过 Patch.contains_point()判断鼠标的点it ;
•事件是否发生在Patch 对象内部。但是对于
illi线这样的对象,没有类似的判断方法。为了响应鼠标点选图形的事件,可以设置图形对象的
picker 屈性为 True ,然后在'pick_event寧件响应函数中加以处理。
在下面的例子中,创建了一个 Rectangle 对象和一个 Line2D 对象,并分別设置其 pickei•屈
性,表示这两个图形对象支持点选事件。跑于 Rectangle 占据一块面积,因此只需要设置为True
即可;而对于表示曲线的 Line2D 对象,为了方便点选,在 调 用 plot〇创建曲线时通过 picker 参
数设置了一个容错值,鼠标坐标到曲线的距离小于8.0就认为该曲线被点选。
在点选事件处理函数〇n_pick()中,我们修改曲线的线宽和矩形的填充颜色。
scpy2.matplotlib.pick_event_demo: 演示绘图对象的点选事件。
fig, ax = pit. s u b p lo t s ()
rect = plt.Rectangle((np.pi, -0.5), 1, 1, f c = n p .r a n d o m .random(3 ) ^ picker=True)
a x .add j D a t c h (r e c t )
def o n j D i c k ( ev e n t ):
artist = event.artist
if isinstance(antist, p l t . L i n e 2 D ) :
lw = artist.get_linewidth()
artist.set_linewidth(lw % 5 + 1)
else:
artist.set_fc(np.random.random(3))
f i g •c a n v a s .draw 一i d l e ()
4.实时高亮显示曲线
为了方便用户分辨多条曲线,可以通过响应鼠标事件,当鼠标靠近某条曲线时,高亮显示
该 llli线 。下 而 的 例 子 中 ,我 们 采 用 而 向 对 象 的 设 计 模 式 ,将鼠标事件处理函数包装在
CurveHighLighter 内部。它 的 alpha 参数为非商品显示时|11|线的透明度,alpha 为 1表示完全不透
明,0 表示完全透明;linewidth 参数为高亮显示丨III线时|1丨1线的宽度。
® 绑疋鼠标移动靡件 niotion_notify_event 到 on_move()方法之上。当鼠标在图表中移动时将
调 用 on_ move〇方法。
©所有高亮显不的逻辑判断都在 highlight〇中进彳了,
其参数 target为高亮显不的Line2D 对象,
如果为 N one 表示取消高亮显示。
•当 无 高 亮 显 示 时 ,将所街曲线的 alpha 属性和 linewidth 设 置 为 1。
•当 有 高 亮 显 示 时 ,将高亮显示的曲线的 alpha 属性设置为1,将 linewidth 设置 为 3 ; 将
非高亮显示的曲线的alpha 属性设置为0.3,将 linewidth 设置 为 1。
Python科学计算 (第 2 版)
•若有任思一条曲线的屈性.被修改,贝懦要调用 Figure.canvas.draw_idle〇重新绘制整个图
表,它会等到空闲时重绘。
© 子图中的所有曲线(Line2D 对象)都在其属性lines 列表中,对此列表中的 Line2D 对象进行
循环。Line2D .C〇
ntains()可以用于判断事件是否发生在此对象内部。当鼠标坐标离曲线的像素距
离小于其 pickradius属性时,将会判断事件发生在 Line2D 对象之.丨
:。contains〇返 N —个有两个
元素的元组,其 第 0 个元素为判断结果,第 1个元素为保符详细信息的字典。
class C u r v e H i g hL i g h t e r ( o b j e c t ) :
matpotlib绘
self.alpha = alpha
self.linewidth = 3
ax.figure.canvas.mpl_connect('motion_notify_event', self.on_move) O
def highlight(self, t a r g e t ) : O
need_nedraw = False
if target is None:
for line in self.ax.lines:
l i n e •set 一a l p h a (1•0)
if line.get_linewidth() != 1.0:
l i n e .s e t _ l inewidth(1.0)
need 一redraw = True
else:
for line in self.ax.lines:
line.set_linewidth(lw)
need 一redraw = True
line.set_alpha(alpha)
if need_redraw:
s e l f •a x •f i g u r e •c a n v a s •draw 一i d l e ()
def on_move(self^ e v t ) :
ax = self.ax
s e l f .highlight(line)
break
else:
self.highlight(None)
f i g ,ax = p i t . s u b p lo t s 〇
x = np.linspace(0, 50, 300)
matplotlib绘
ax.plot(Xj jn(i, x))
ch = CurveHighLighter(ax)
- 制精美的图表
图本44为实际运行效果,当鼠标悬停到某条曲线之上时,该曲线加粗显示,而其他曲线则
变为半透明显示。
10 20 30 40 S0
阁4 4 4 岛兑显示鼠标悬停曲线
4 . 6 . 3 动画
通过修改图形元素的各种属性并重新绘制图表,可以实现简单的动画效果。在下面的例子
中:O 首先创建一个5 0 毫秒的定时器 timer, 并调用其 add_callback()添加定时事f 丨
:,其第一个
参数为定时事件发生时调用的函数,第二个参数为传递给此函数的对象。由于我们需要对阁表
中 llll线 的 数 据 进 行 修 改 , 因 此 需 要 将 Line2D 对 象 l i n e 传 递 给 update_data()。 © 调用
Python科学计算 (第 2 版)
import numpy as np
fig, ax = p i t . s u b p lo t s ()
x = n p . l inspace(0 ,10, 1000)
line, = ax.plot(x, np,sin(x)_» lw=2)
def update 一
data(line):
x[:] += 0.1
line.set_ydata(np.sin(x)) ©
fig.canvas.draw() ©
timer = fig.canvas.new_timer(interval=50) O
timer.add_callback(update_data, line)
matpotlib绘
t i m e r . s t a r t ()
1.使用缓存快速重绘图表
- 制精美的图表
然 而 Figure.canvas.draw()的绘图速度较慢,为了提高重绘速度,可以快速重绘图表中的静
态元素,只更新有动态效果的元素。在下面的例子中:
〇创建绘图元素丨丨彳,设置其 animated屈性.为True 。© 在调用 Figure.canva.draw〇重绘整个图
表时,会 忽 略 所 有 anim ated 为 T r u e 的对象。€)这时所有的静态元素都已经绘制完毕,调JIJ
Figure.canvas.copy_from_bbox ()保存子图对象对皮区域中的图像信总到background 中。子图对象
在图表对象中的位置和大小可以通过其bbox 属性获得。
在时钟事件处理闲数中,© 首先调用 Figure.canvas.restore_region()恢复所保存的图像信息,
这相当于擦除所有动态元素,重新绘制了所有的静态元素。© 在更新 llll线 的 Y 轴数据之后,调
用子图对象的 draw_artist〇在 Canvas 对象中绘制丨II丨线,此 时 Canvas 对象中已经是一幅完整的图
表的图像了。@ 调 用 Figure.canvas.blitO将 C anvas 中指定区域的内容绘制到屏藉上。
f i g ,ax = p i t . s u b p lo t s ()
fig.canvas.draw() ©
background = f i g . c a n v a s .copy_from_bbox(ax.bbox) ©
def u p d a t e _ d at a ( l i n e ) :
x[:] += 0.1
line.set_ydata(np.sin(x))
f i g •c a n v a s •restore 一r e g i o n ( b a c k g r o u nd ) O
86
ax.draw_artist(line) 0
f i g . c a n v a s .blit(ax.bbox) ©
timer = fig.canvas.new_timer(interval=50)
t i m e r •add 一callback(update 一data, line)
t i m e r . s t a r t ()
2. animation 模 块
matplotlib绘
update_line〇的巾贞数参数将在0 到 99之间循环变化。
import numpy as np
- 制精美的图表
from matplotlib import pyplot as pit
fig, ax = p i t . s u b p lo t s ()
def u p d a t e _ l i n e ( i ) :
y = np.sin(x + i*2*np.pi/100)
line.set_ydata(y)
return [line] ©
若想将动画保存成动画文件,可以调用如下方法:
4 . 6 . 4 添 加 G U I面板
在 matplotlib 的主页中可以找到将图表嵌入各种主流界面库的演示程序,这些程序都是首
先创建一个 G U I 窗 U , 然后将图表作为控件嵌入窗口中。本节介绍一种更加简洁的方法:为
matplotlib 图表窗口添力口 G U I 控件的控制而板。
下 面 是 使 用 TkSliderPanel的一个例子。O 竹 先 调 用 matplotlib.use〇将后台界面库设置为
”
TkA gg ”
,该语句必须在matplotlib的其他函数之前被调)lj 。
© 我们希望绘制的丨11丨线函数丨:tHexp_sin〇
定义,其第一个参数为自变量,其:余参数为函数中的各个系数。© update〇函数接收新的系数,
并调用 exp_Siii()计算新系数对应的闲数值。O 然后调用 nne.set_data〇更新曲线的数据。在更新子
图的显示范围之后,© 调 用 fig .canvas.dmw_idle()重新绘制整个图表。
@创 建 一 个 T k S l i d e r P a n e l 对象,它的第一个参数为阁表对象,第二个参数定义控制而板中
的滑标名称以及取值范围,第三个参数为回调函数。当某个滑标控件的值发生变化时,将调用
此函数。
© 最后调用其 seLpam m et^O 设資各个滑标控件的初始值。
程序的运行结果如阁本45(左)
所示,而右图则是采用 Q tS lid e r P a n e l 时的界而截图。© 为了在 N o te b o o k 中显示 T K 界面库的窗
口,需要执行% g u it k 魔法命令,然后调用 f i g .s h 〇
w ()显示图表窗口。如果是在单独的进程中运行
该程序,贝IJ需要调用 p l .s h o w ()以显示窗口。
由于程序涉及G U I 库的用法,限于篇幅这里不再详细叙述,请感兴趣的读者查看本书提供
的相关源代码。
%gui tk
import numpy as np
import matplotlib
m a t p l o t l i b .use(" T k A gg " ) O
import pylab as pi
def exp_sin(x, A, z, p ) : ©
figj ax = pi.su b p l ot s ()
x = np.linspace(le-6, 1, 500)
def u p d a t e ( * * k w ) : ©
y = exp_sin(x, **kw)
line.set_data(x, y) O
ax.relim()
a x •autoscale 一view()
fig.canvas.draw 一i d l e () ©
from scpy2.matplotlib.gui 一
panel import TkSliderPanel
panel = TkSliderPanel(fig, ©
[("A", 0, 10), ("f", 0, 10), ("z", -3, 0), ("p", 0, 2*np.pi)],
matp — b绘
fig.show() Q
—制 精 美 的 图 表
Pandas- 方便的数据分析库
N um Py 虽然提供了方便的数组处理功能,但它缺少数据处理、分析所需的许多快速工具。
Pandas 堪 于 N um Py 开发,提供了众多更高级的数据处理功能。Pandas 的帮助文档十分全面,因
此本章主要介绍 Pandas 的一些;
®本概念和帮助文档中说明不够详细的部分。希望读者在阅读本
章之后能更容易阅读官方文档。
import pandas as pd
pd._ version 一
•0.16.2'
5.1 Pandas中的数据对象
常用属性,在后续章节将介绍对它们进行操作和运箅的各种函数和方法。
5.1.1 Series 对象
Series 对象的下标运算同时支持位置和标签两种形式:
位置下标 s[2]: 3
标签下标s [ ^ ] : 4
Series 对象还支持位置切片和标签切片。
位置切片遵循 Python 的切片规则,
包括起始位置,
但不包括结束位置;但标签切片则同时包括起始标签和结束标签。
s[l:3] s [ _ b W ]
b 2 b 2
c 3 c 3
Tl
an d丨
d t y p e : int64 d 4
dtype: int64
a s方便的数据分析库
和 ndairay 数组一样,还可以使用位置列表或位置数组存取元素,冋样也可以使用标签列
表和标签数组。
b 2 b 2
d 4 d 4
c 3 c 3
d t y p e : int64 d t y p e : int64
S e r ie s 对 象 N 时 具 有 数 组 和 字 典 的 功 能 , 因 此 它 也 支 持 字 典 的 一 些 方 法 ,例如
Series.iteritemsO:
l i s t (s .i t e n i t e m s ())
[(•a., 1), ( ,
b 、 2), (.c., 3), C d . , 4), (.e., 5)]
s s2 s+s2
2
9
/4
a 1 b 20 a nan
b 2 c 30 b 22
c 3 d 40 c 33
d 4 e 50 d 44
e 5 f 60 e 55
d t y p e : int64 d t y p e : int64 f nan
dtype: float64
5.1.2 DataFrame 对象
1. DataFrame 的各个组成元素
Pandas方
列为行索引,
用 parse_dates参数指定进行日期转换的列。
在指定列时可以使用列的序号或列名。
所得到的 DataFmme对象如图 5 - 1 所示,图中标识出了 DataFmme的各个组成部分的名称。
- 便的数据分析库
列索引名 列索引
行 列 奴据
第e 圾 索 引 第 1圾
阁 5-1 DataFmme的结构
由 图 5 - 1 可 知 DataFmme对象是一个二维表格。其中,每列中的元素类型必须一致,而不
同的列可以拥有不同的元素类型。在本例中,有 4 列浮点数类型、 1 列 F1期 类 型 和 1 列 object
类塑。object类型的列可以保存任何Python 对象,在 Pandas 中 字 符 $列 使 用 object类塑。dtypes
属性可以获得表示各个列类型的Series 对象:
d f _ s o i l .dtypes
Measures
pH float64
293
Python科学计算 (第 2 版)
Dens float64
Ca float64
Conduc float64
Date datetime64[ns]
Name object
dtype: object
df_soil.shape
(6, 6)
D a t a F m m e 对象拥有行索引和列索弓|, 可以通过索引标签对其中的数据进行存取。index属
性保荐行索引,而 cohmins属性保存列索引。在木例中列索引是一个Index对象,索引对象的名
称可以通过其 n a m e 厲性存取:
print df_soil.columns
Tl
an d丨
print df_soil.columns.name
a s方便的数据分析库
print df一soil•index
print df一soil.index.names
Mu!tilndex(levels=[[u'0-10•, u '10-30•], [u'Depression•, u'Slope', u'Top']],
labels=[[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 1, 2]],
names=[u'Depthu_Contour’])
[u'Depth'^ u'Contour']
94
10-30 Depression 4.9 Top 1 13
.locn 可通过行索引标签获取指定的行,
例 如 df.loc[’’0-10n/ T o p "〗
获 得 Depth 为 Contour
为’T o p " 的行,而 df.loc[” 10-30n]获 取 Depth 为” 10-30"的所有行。当结果为一行时得到的是Series
对象,结果为多行时得到的是 DataFrame对象。注意由于原数据中列的类型不统一,因此得到
的 Series 对象的类型被转换为最通用的 object 类型。.loc[]的用法非常丰富,下一节还会详细介
绍它的各种用法。
Pandas方
Ca 13 Slope 5.B 1.3 9.5 4.9 2015-02-06 Diana
Conduc 1.4 Top 4.8 1.3 10 B.6 2015-04-11 Diana
- 便的数据分析库
Date 2015-05-21 00:00:00
Name Roy
Name: (0-10, Top), dtype: object
d f _ s o i l .v a l u e s .dtype
dtype('CV)
2.将内存中的数据转换为 DataFrame 对象
col u m n s = [ Ma M:> Mb n ])
df3 = pd.DataFrame(arr) ©
a b a b name count
A 2 6 A 1 5 0 iteml 1
B 3 1 B 2 6 1 item2 2
C 5 9 C 3 7 2 item3 3
D 8 0 D 4 8 3 item4 4
Tl
an d丨
dfl = pd.DataFrame.from 一
diet(dictl, orient="index")
df2 = pd.DataFrame.from_dict(dictl, o r i e n t = ncolumns")
0 1 2 a b A B C a 1
a 1 2 3 0 1 4 a 1 2 nan A 1 3
b 4 5 6 1 2 5 b 3 nan 4 B 2 nan
2 3 6 C nan 4
items = d i c t l . i t em s ()
29(
d fl df2
A B C a b
a 1 2 3 0 1 4
b 4 5 6 1 2 5
2 3 6
Pandas方
to_ records〇方法可以将 DataFrame 对象转换为结构数纽,名:其 index 参数为 True(默认值),
- 便的数据分析库
则其返回的数组中包含行索引数据:
[ ( ’i n d e x 、 •<i8,), ( ’a 、 ,
<i8.), C b ’, .<i8’)]
[(•a., '<i8.), (.b、 ,
<i8.)]
将其输出到文件中,关于文件的输入输出将在后面详细介绍。
5.1.3 Index 对象
In d ex 对象保存索引标签数据,它可以快速找到标签对应的整数下标,这种将标签映射到
整数下标的功能5 Python 的字典类似。
其 values 属性可以获得保存标签的数组,
与 Series —样 ,
字符串使用 object类型的数组保存:
index = d f _ s o i l .columns
index.values
2
9*
Python科学计算 (第 2 版)
Index 对象也具有字典的映射功能,它将数组中的值映射到其位置:
• Index.g e tjo c (value) : 获得巾•个值 value 的下标。
• Index.get_ indexer(values):获 得 -•组 值 values 的下标,当值3、
存在时,得到-1。
2
[1 3-1]
True
5.1.4 M ultiindex 对象
mindex = df_soil.index
print mindex[l]
( • 0 - 1 0 、 .Slope.)
[5 0-1]
print m i n d e x . l e v e l s [0]
print m i n d e x . l e v e l s [1]
然后使用多个整数数组保存这些标签的下标:
9
2:
print m i n d e x . l a b e l s [0]
print mindex.labels[l]
zip(level0[label0], l e v e l l [labell])
[(•0-10,
,'Depression'),
( ■ 0 - 1 0 、 'Slope'),
(•0-10.) 'Top'),
( ■ 1 0 - 3 0 、 'Depression')
( • 1 0 - 3 0 、 •S l o p e •),
Pandas方
C 10-30', •Top.)]
- 便的数据分析库
元纽的 Index 对象,可以设置 tupleize_co ls 参数为 False:
pd.Index([("A", "x"), ("A", "y"), ("B", "x"), ("B", "y")], name=["classl", "class2"])
级索引对象:
dfl
classl A B C
class2 X y X y X y
classl class2
A X 3 0 8 2 7 3
y 9 7 6 9 2 4
B X 8 8 0 4 8 3
y 8 3 7 6 3 9
C X 2 0 4 0 6 4
y 0 7 5 6 0 5
5 . 1 . 5 常用的函数参数
表 5 - 1 常用的函数参数
丨方便的数据分析库
参数名 常用值 说明
axis 0、 1 运算对应的轴
level 整数或索引的级别名 指定运算对应的级別
fill一
value 数值 指定运箅中出现的N a N 的替代填充值
skipna 布尔值 运算是否跳过N a N
index 序列 指定行索引
columns 序列 指定列索引
numeric_oiily 布尔值 足否只针对数值进行运箅
func 可调用对象 指定回调函数
inplace 布尔值 是否原地更新,若为否,则返回新对象
encoding nutf8” 指定文木编码
dropna 布尔值 楚否删除包含N a N 的行
df_soil.mean() d f _ s o i l .m e a n (a x i s = l ) d f _ s o i l .m e a n (level=l)
dtype: float64
%dot GraphvizDataFrame.graphviz(df_soil)
pandas方
ndarray
—便 的 数 据 分 析 库
6
PyOb^ectHashTabto
ObiectEngine
FrozonNDArray
Mutblndex
•Depth.
FrozcnND Array
M
ndarray
r • Index
•
Index
rxUirray
Wop* ^ 4
FloatBlock
Index 6
ndarray OatotimeBkxA
OataFrame
6 1
BlockManager
—
6
I \ " 6
图5-2 D a t a F m m c 对象的内部结构
301
Python 科学计算 (第 2 版)
d f _ s o i l .c o l u m n s . _e n g i n e .m a p p i n g . get_item("Date")
s = d f _ s o i l [ nD e n s n ]
s.values.base is df _ s o i l . _d a t a . b l o c k s [ 0 ] .values
True
None
True
当 DataFrame对象只有一个数据块吋,
获収其行数据所得到的Series 对象也与其共享内存:
True
而 当 BlockManager 屮使用多个数组保存数据时,则返冋这呰数据的拷贝,数组的元素类型
为最通W 的元素类型,以保存各种格式的数据。
在下面的例子中,df.values 的元素类型为 object,
因为它需要同时保存浮点数、时间和字符串。
d f _ s o i l .v a l u e s .dtype
d t y p e ( '0')
5 .2 下 取
DVD
Pandas方
用.loc[]、.iloc[]、.at[]、.iat[]和.ix []等存取器存取其中的元素。
下面的表5-2总结了 DataFmme对象的各种存取方法:
- 便的数据分析库
表 5-2 D a t a F r a m e 对象的各种存取方法
方法 说明
[col一
label) 以单个标签作为下标,获取与标签对应的列,返回Series对象
[coljabels) 以标签列表作为下标,获取对应的多个列,返回DataFrame 对象
[ix)\v_slice] 整数切片或标签切片,得到指定范围之内的行
[ix)w_bool_array] 选杼布尔数组中T m e 对应的行
.s»ct(colJabcl, default) 与字典的gel()方法的用法相同
.atfindcxjabcl, coljabcl] 选择行标签和列标签对应的值,返0 单个元素
.iatfindex, col] 选择行编号和列编号对应的值,返固单个元素
.loc[index, col] 通过单个标签值、标签列表、标签数姐、布尔数组、标签切片等选择指
定行与列上的数据
.iloclindex, col] 通过单个整数值、整数列表、整数数组、布尔数俎、整数切片选择指走
行与列上的数扼
.ix[index, col] 同时拥有.locfl和.ilocn的功能,既可以使用标签下标也可以使用整数下标
•lookiip(mw_labeLs,
col一
labels) 选择行标签列表与列标签列表屮每对标签对应的元尜值
.get_vdue(ix)w_kibel, colJabel) 与.at[j的功能类似,不过速度更快
•query。 通过表达式选择满足条件的行
•head〇 获取头部N 行数据
.tail() 获取尾部N 行数据
/
»
30
Python 科学计算 (第 2 版)
n p .r a n d o m .s e e d (42)
df = pd.DataFrame(np.random.randint(0, 10, (5, 3)),
5.2.1 □操作符
df d f [2:4] :"r 4 .
df[' •r2__ .]
cl c2 c3 cl c2 c3 cl c2 c3
rl 6 3 7 r3 2 6 7 r2 4 6 9
r2 4 6 9 r4 4 3 7 r3 2 6 7
n3 2 6 7 r4 4 3 7
r4 4 3 7
rS 7 2 5
df.cl > 4 是一个布尔序列,因此 df[df.cl > 4] 获得该序列中 True 对应的行。d f > 2 是一个布尔
DataFrame对象,df[df> 2]将其中 False 对应的元素置换为 NaN:
cl c2 c3 cl c2 c3
rl 6 3 7 rl 6 3 7
r5 7 2 5 r2 4 6 9
r3 nan 6 7
r4 4 3 7
r5 7 nan 5
5.2.2 .loc[]和.ilocD存取器
.locfl的下标对象是一个元组,其中的两个元素分别与 DataFrame的两个轴相对应。若下标
不是元组,则该下标对应第0 轴,:对应第1轴。每个轴的下标对象都支持中.个标签、标签列表、
04
标签切片以及布尔数组。
df .loc [M
r2"]获得nr2"对应的行,它返回一个 Series 对象。df.loc ["r2",
"c 2"]获得"r2'1f nc 2"列的元
素,它返回单个元素值。
df.loc["r2"] df.loc[>2",__c2"]
cl 4 6
c2 6
c3 9
ctf.lcx [[”
r2n,"r3 M
]]获得nr2"和"r3"对应的行。40〇〇[["12","以],
[41",
"〇2,
’
]]则获得^
"c l M和"c 2"列上的数据,所得到的数据都是新的DataFrame对象。
cl c2 c3 cl c2 Tl
an d丨
r2 4 6 9 r2 4 6
a s方便的数据分析库
r3 2 6 7 r3 2 6
在下而的程序中,第 0 轴的下标分别为标签切片和布尔数序列:
c2 c3 cl c2
r2 6 9 rl 6 3
r3 6 7 r2 4 6
r4 3 7 r4 4 B
r5 7 2
.ilocU和 loci]类似,不过它使用整数下标:
cl 2 cl c2 c3 cl c2 c3 cl c3
c2 6 r3 2 6 7 r2 4 6 9 r2 4 9
c3 7 r5 7 2 5 r4 4 B 7 r4 4 7
Name: r3, d t y p e : int32
df.iloc[2:4, [0 ,
2]] df.iloc[df.cl.values>2 ,[0 ,
1]]
cl c3 cl c2
r3 2 7 rl 6 3
r4 4 7 r2 4 6
r4 4 3
n5 7 2
05
Python科学计算 (第 2 版)
此外.ix[]的存収器可以混用标签和位置下标,例如:
cl c3 cl c3
r3 2 7 rl 6 7
r4 4 7 r2 4 9
r3 2 7
5 . 2 . 3 获取单个值
.at□和.iat□分别使用标签和整数下标获取单个值,此 外 get_Value〇与.atD类似,不过其执行
速度要快一些:
6 6 6
a s方便的数据分析库
当.loc[]的下标对象是两个标签列表时,所获得的是这两个列表形成的网格上的元素,这与
N u m P y 的 数 组 下 标 操 作 不 样 。如果希望获収两个列表中每对标签所对应的元素,可以使用
lookupO, 它返冋一个包含指定元素的数组:
array([4, 3, 2])
5 . 2 . 4 多级标签的存取
.l〇
c [j 和.atU的下标可以指定多级索引中每级索引上的标签。这时多级索引轴对应的下标是
一个下标元组,该元组中的每个元素与索引中的每级索引对应。若下标不是元组,则将其转换
为长度为1 的元组,若元组的长度比索引的层数少,则在其后面补 sliCe(N 〇
ne)。
在下而的例子中,n10-30”
为第0 轴的标签,
根据前而的规则,
将其转换为('’
10-30’
’
,slice(None)),
即选择第0 级中"1〇~3〇"对应的行:
pH Ca
Contour
06
如果需要选择第1级中"Top ”
对应的行,
则需要把 sliceCNone)作为第0 级的下标。由于 Python
中只有直接在□中才能使用以:分隔的切片语法,
因此这里使用np.s_对象创建第0 轴对应的下标:
(slice(None),
"Top ")。
pH Ca
Depth Contour
5.2.5 query()方法
当需要根据-•定的条件对行进行过滤吋,通常可以先创建一个布尔数组,使用该数组获収
True 对应的行,例如下面的程序获得 p H 值大于5、C a 含 量 小 于 11%的行。由于 Python 中无法
自
1定 义 not、and 和 o r 等关键字的行为,因此需要改川〜、& 、I等位运算符。然而这些运算符的
pandas方
优先级比比较运算符要高,因此需要用括号将比较运算括起来:
—便 的 数 据 分 析 库
使 用 queiy 〇可以简化上述程序:
尔运算,表达式中的变量名表示与其对应的列。如果希望在表达式中使W 其他全局或局域变量
的值,可以在变量名之前添加@,例如:
pH_low = 5
Ca_hi = 11
5 . 3 文件的输入输出
DVD
07
P y th o n 科学计算(第2 版)
本卞介绍表5-3巾的输入输出函数:
表 5 - 3 输入输出函数
函数名 说明
read_csv() 从C S V 格式的文木文件读取数据
reiid_excel〇 从Excel文件读入数据
HDFStorcO 使用H D F 5 文件读写数据
rcad_sql() 从 S Q L 数据库的査询结果载入数据
read_pickle() 读入Pickle序列化之后的数据
5.3.1 CSV 文件
read_Csv 〇从文本文件读入数据,它的可选参数非常多,下面只简要介绍一些常用参数:
• sep 参数指定数据的分隔符号,可 以 使 正 则 表 达 式 ,默认值为逗号。有 时 C S V 文件为
Tl
了便于阅读,在逗号之后添加了一些空格以对齐每列的数据。如果希望忽略这些空格,
an d丨
返回一个迭代器。
•当 文 件 名 包 含 中 文 时 ,需要使用 Unicode 字符苹指定文件名。
下面使用上面介绍的各个参数读入上海市的空气质量数据文件。该文件的文字编码为
UTF -8,并且带 B 0 M 。所 谓 B O M , 是指在文件开头的3 个特殊字节表示该文件为UTF -8文件。
对于张 B 0 M 的 UTF -8文件,可以指定编码参数 encoding 为"utf-8-sig "。
该文件中有两种字符表示缺失数据:一个是减号,另一个是全角的横杠。由 于 read_csv ()
在将字节字符串转换为 Unicode 之前判断 N aN , 因此耑要使用与文件相同的编码表示这些缺失
数据的字符串。
08
://a i r .e p m a p .o r g /
h ttp
空气质量数椐来源:青悦空气质量历史数据库。
df_list = []
encoding="utf-8-sig",# 文件编码
chunksize=100, #一次读入的行数
u s ecols=[u" 时间",u" 监测点","AQI", "PM2.5", "PM10"], #只读入这些列
na_values= 这些字符串表示缺失数据
parse_dates=[0]): # 第一列为时间列
df_list •append (df) #在这里处理数据
df_ l i s t [ 0] . c o u n t () df_list[0].dtypes
Pandas方
时间 100 时间 datetime64[ns]
- 便的数据分析库
监测点 90 监测点 object
AQI 100 AQI int64
d t y p e : int64 d t y p e : object
注 意 “吋间”列 为 d a t e t i m e 64[ns]类型,而由于存在缺失数据,丨
41此 “P M 10”列被转换为
浮点数类型,其他的数值列为整数类型,而 “监测点”列中保存的是 U n ic o d e 字符串。
<type 'Unicode'>
5.3.2 HDF5 文件
H D F 5 是存储科学计兑数据的一种文件格式,支持大于2G B 的文件,可以把它看作针对科
学计算的数据库文件。关 于 H D F 5 文件格式的更多信息,请参考下面的链接:
://w w w .n s m c .c m a .g o v .c n /F E N
h ttp / /
G Y U N C a st d ocs H D F 5.0_c h i n e s e . p d f
◎ 中文的 H D F 5 使用简介。
H D F 5 文件像一个保存数据的文件系统,其中只有两利喽型的对象:资料数据(d a t a s e t )和目
录(g r o u p ):
• 资 料 数 据 :像文件系统中的文件一样用于保存各利1数据,例 如 N u m P y 数组。
09
Python 科学计算 (第 2 版)
• 目 录 :类似于文件系统中的文件夹,可以包含.其他的目录或资料数据。
使 用 P a n d a s 可以很方便地将多个Series 和 D a t a F r a m e 保存进 H D F 5 文件。H D F 5 文件采j|J二
进制格式保存数据,可以对数据进行压缩存储,比文本文件更节省空间,存取也更迅速。
卜面创建一个H D F S t o n e 对象,通 过 c o m p l i b 参数指定使用 blosc P R 缩数据,通 过 complevel
参数指定压缩级别。
si = p d.Series(np.random.rand(1000))
store["dataframes/dfl"] = dfl
store["dataframes/df2"] = df2
Pandas方
store [,,se r i e s / s l H ] = si
print s t o r e . k e ys ()
- 便的数据分析库
print d f l .e q u a l s (s t o r e ["dataframes/dfl"])
http://pytables.github.io/usersguide/libref/hierarchy_classes.html
pytables 官方文档。
root = s t o r e . get_node("//")
for node in r<x>t._f_walknodes():
print node
Pandas方
stor e . a p pe n d ( 'dataframes/df— d y n a m i c l ' , dfl, append=False) O
- 便的数据分析库
df3 = pd.DataFrame(np.random.rand(100, 4)^ columns=list("ABCD"))
(100100, 4)
A B C D
98 0.95 0.072 0.78 0.18
99 0.19 0.043 0.24 0.075
100 0.21 0.78 0.86 0.47
101 0.71 0.87 0.63 0.74
A B C D
头三个字节并与 B O M 比较,这样才能保证读入的数椐中勹第一列对应的标签不包含
BOM 。
- 便的数据分析库
■由于可能存在缺失数据,因此读入的数值列的类型可能为整数和浮点数。由 于 HDF 5
文件中的每列数据只能对应一利喽型,© 因此需要使用 dtype 参数指定这些数值列的类
型为浮点数。
• €)需要为 HDF 5 文件中的字符串列指定最大长度,否则该最大长度将由第一个被添加进
HDF 5 文件的数据对象决定。
A 据的行索引并不是唯一的 。
def r e a d _ a q i _files(fnj3attern):
UTF8_B0M = b M\ x E F\xBB\xBFM
cols = ••时间,
城市,监测点,
质量等级,A Q I , P M 2 . 5 , P M 1 0 , C O , N O 2 , O 3 , S O 2 " . s p l i t ( V )
float— dtypes = {col:float for col in " A Q I , P M 2 . 5 , P M 1 0 , C O ) N O 2 , O 3 , S O 2 " . s p l i t ( _ V ) }
names_map = {•’时间_’:
_’Time",
"质 量 等 级 Level",
••城市" :
"City",
,,PM2.5,,:,,PM2_5M>
for fn in g l o b ( f n _ p a t t e r n ) :
with open(fn, "rb") as f:
sig = f.read(3) O
if sig != UTF8_B0M:
f.seek(0, 0)
df = pd.read_csv(f,
parse_dates=[0]>
na— values=
usecols=cols^
d t y p e = f loat_dtypes) ©
df.rename_axis(names— map, axis=l_» inplace=True)
d f .d r o p n a (inpla c e = Tr u e )
yield df
Pandas方
store = p d .H D F S t o r e ( " d a t a / a q i / a q i .hdf5", c o mplib="blosc '、 complevel=9)
string 一
- 便的数据分析库
size = {"City": 12, "Position": 30, "Level":12}
store . c l os e ()
store = pd.HDFStore("data/aqi/aqi.hdf5")
df 一aqi = s t o n e .s e l e c t ("a q i ")
print len(df_aqi)
337250
下而只读取 PM 2.5值大于500的行:
print len(df_polluted)
87
5 . 3 . 3 读写数据库
用 to_sql()可以将数据写入 S Q L 数椐库,它的第一个参数为数椐库的表名,第二个参数为
表示与数据库连接的 Engine 对象,Engine 在 sqlalchemy 库 中 )义 。下面首先从 sqlalchemy 中载
入 create_engine(),并调用它使用 S Q L ite 打 开 数 据 库 文 当 该 文 件 小 存 在 时 ,
将创建新的数据库文件:
P y th o n 科学计算(第2 版)
try :
engine.execute("DROP TABLE aqi")
except:
pass
print len(df_polluted)
87
5 . 3 . 4 使 用 Pickle 序列化
d f _ a q i .equals(df_aqi2)
True
化之后的数椐,但是作为临时保存•运算的中间结果还是很方便的。
5 . 4 数值运算函数
Pandas方
print df_soil
pH Dens Ca Conduc
- 便的数据分析库
Depth Contour
df_soil.mean() d f _ s o i l .m e a n (a x i s = l ) d f _ s o i l .m e a n (l e v e l = l )
Top 5
dtype: float64
s = pd.Series(diet(Depression=0.9^ Slope=1.2))
Depth Contour
Slope 15
Top 13
Slope 11
Top 10
d t y p e : float64
函数对带脉冲噪声的正弦波进行处理的结來。它们的第二个参数为窗口包含的元素个数,而
center 参数为 True 表示移动窗口以当前元素为中心。
由 于 rolling_ median()采用了更高效的算法,因此当窗口很大吋它的运算速度比S c iP y 章节
中介绍过的 signal.order_filter〇更快。
x = np.sin(0.5*2*np.pi*t)
s = pd.Series(x^ index=t)
一 罐 声 值 号 一 中 僅 戏 法 一 移动平均
图5 - 3 中值滤波和移动平均
expandir^*() 函数对序列进行扩展窗丨」运综,例 如 expanding_max()返回到每个元素为止的
历史最大值。图 5~4显示了 expanding_max()、expandingjnean 〇和 expanding_min〇的运算结果。
np.r a n d o m. s e e d (42)
x = np.cumsum(np.random.randn(400))
x_max = pd.expanding_max(x)
x_min = pd.expanding__min(x)
x_mean = pd.expanding_mean(x)
图 5 4 用 expandingjH卜算W史最大值、平均值、最小值
字符串处理
2 C
d t y p e : object
s_unicode = s_utf8.str.decode("utf-8")
s_gb2312 = s一Unicode.str.encode("gb2312")
0 6 0 2 0 4
■o
andas
1 9 1 3 1 6
2 12 2 4 2 8
丨
串类型。在处理文本数据时,需要格外注意字符串的类型。
可以对 sti•使用整数或切片下标,相当于对 Series 对象中的每个元素进行下标运算,例如:
便
0 北京
的
1 北京
2 北京
d t y p e : object
数
字 符 串 序 列 与 字 符 样 ,支持加法和乘法运尊,例如:
+ s一
据
0 北京-aa
1 北京市-bb
分
2 北京地区-cc
d t y p e : object
析
也可以使用 str.cat〇连接两个字符串序列的对应元素:
print s _ u n i c o d e .s t r .c a t (s _ a b c , sep="-") *
1
库
0 北狀-a
1 北京市-b
318
2 北京地区-c
d t y p e : object
print s _ u n i c o d e .s t r .l e n ().astype(unicode)
0 2
1 3
2 4
d t y p e : object
s = pd.Series(["a|bc|de", "x|xyz|yz"])
Pandas方
s_list = s. str. split ( " r 1)
s一comma = s _ l ist.str.join("/')
- 便的数据分析库
s s一list s_comma
对字符串序列进行处理付,经常会得到元素类型为列表的序列。Pandas没有提供处理这种
序列的方法,不过可以通过 str[]获取其中的元素:
s一l i s t . s t r [1]
0 be
1 xyz
d t y p e : object
或者先将其转换为嵌套列表,然后再转换为 DataFrame对象:
A B C
0 a be de
1 x xyz yz
两个命名组,因此其列名为组名。
df_extractl = s.str.extract(r"(\w+)\|(\w+)\|(\w+)")
df_extract2 = s.str.extract(r"(?P<A>\w+)\|(?P<B>\w+)|")
df_extractl df_extract2
0 1 2 A B
0 a be de 0 a be
1 X xyz yz 1 X xyz
在处理数据时,经常会遇到这种以特定分隔符分隔关键字的数据,例如下而的数椐可以用
于表示有向阁,其第一列为边的起点、第二列为以T 分隔的多个终点。下而使用 r c a d _CSV()读入
该数据,得到一个两列的 D a t a F r a m e 对象:
import io
text = """A, B|C|D
■D B, E|F
andas
C, A
丨方便的数据分析库
D, B|C
I I I I II
d f = p d .r e a d _ c s v (io .B y t G S lO (t G x t )^ s k i p i n i t i a l s p a c e = T rue, h e a d e r= N o n e )
p rin t d f
0 1
0 A B|C|D
I B E|F
2 C A
3 D B|C
可以使用下而的程序将上述数椐转换为每行对应一条边的数据。O n o d e s 是一个元素类型
为列表的 S e r i e s 对象。© 调 用 N u m P y 数 组 的 r e p e a t ()方法将第一列数据重复相应的次数。由于
re p e a tO 只能接受32位整数,而血1沈〇返回的是64位整数,因此还需要进行类型转换。© 将嵌
套列表平坦化,转换为一维数组。
nodes = d f [ 1 ] . s t r.split("|") O
from_node = d f [0].values.repeat(nodes.str.len().astype(np.int32)) ©
to_node = np.concatenate(nodes) ©
1 A C
2 A D
3 B E
4 B F
5 C A
6 D B
7 D C
还可以把原始数据的第二列看作第一列数椐的标签,为了后续的数据分析,通常使用
str.get_dummies〇将这种数据转换为布尔 DataFrame 对象,每一列与一个标签对应,元 素 值 为 1
表示对应的行包含对应的标签:
df[1].map(lambda s:max(s.split("|")))
0 D
2 A
3 C
Name: 1, dtype: object
当用字符串序列表示分类信息时,其中会有大量相冋的字符串,将其转换为分类(Category)
序列 n丨以节哲内存、提高运算效率。例如在下面的 df_soil 对象中,Contour、Depth 和 G p 列都是
表示分类的数据,因此有许多重复的字符串。
下而循环调用 astypefcategoty ”
)将这三列转换为分类列:
Contour category
Depth category
Gp category
pH float64
d t y p e : object
Gp = df_soil.Gp
print Gp.cat.categories
d t y p e = ' o b j e c t ')
Gp.head(5) G p .c a t .c o d e s .h e a d (5)
丨方便的数据分析库
0 T0 0 8
1 T0 1 8
2 T0 2 8
3 T0 3 8
4 T1 4 9
Name:: Gp, d t y p e : category dtype : int8
分类数据有无序和有序两种,无序分类中的不同分类无法比较大小,例如性别;有序分类
则可以比较大小,例如年龄段。上面创建的三个分类列为无序分类,可 以 通 过 cat.as_ordered()
和 cat.as_ unordered〇在这两种分类之间相互转换。下面的程序通过 cat.as_ ord丨
'edO将深度分类列转
换为有序分类,注意最后一行分类名之间使用“< ”连接,表示是有序分类。
depth = df_soil.Depth
depth•cat•as一ordered()•head()
0 0-10
1 0-10
2 0-10
3 0-10
4 10-30
d t y p e : category
Categories (4^ object): [0-10 < 10-30 < 30-60 < 60-90]
如果需要自定义分类中的顺序,可以使用 cat.reorder_categories()指定分类的顺序:
contour = df 一soil.Contour
contour•cat•reorder'一categories(categories, ordered=True).head()
0 Top
1 Top
2 Top
3 Top
4 Top
d t y p e : category
5 . 5 时间序列
Pandas 提供了表示时间点、时间段和时间间隔等三利i与时间有关的类型,以及元素为这些
类型的索引对象,并提供了许多时间序列相关的函数。本节简要介绍一些与吋间相关的对象和
函数。在本章最后一节还会介绍一些相关的实例。
5 . 5 . 1 时间点、时间段、时间间隔
now = pd.Timestamp.now()
now_shanghai = now.tz_localize("Asia/Shanghai")
nowjtokyo = now_shanghai.tz_convert("Asia/Tokyo")
print u" 本地 时 间 :", now
本地 时 间 : 2015-07-25 11:50:46.264000
上 海 时 2015-07-25 11:50:46.264000+08:00
东 京 时 区 : 2015-07-25 12:50:46.264000+09:00
Python科学计算 (第 2 版)
不 N 时区的时间可以比较,而本地时间和时R 时间无法比较:
True
import pytz
p y t z .common_timezones
['A f r i c a / A b i d j a n '3
•Africa/Accra.,
•Africa/Addis__Ababa •,
•Africa/Algiers ’
,
期时间段。
s 方便的数据分析库
now_hour = pd.Period.now(freq="H")
freq 属性萣一个描述时间段的字符串,其可选值可以通过下面的代码获得:
now_week__sun now_week_mon
曰、星期几、一年巾的第几天、小时等信息:
2015 7 25 5 206 11
P a n d a s方
将两个时间点相减可以得到表示时间间隔的Timedelta对象,下而计算当前时刻离2015年
国庆节还有多少时间:
- 便的数据分析库
national 一day = pd.Ti m e s ta m p ("2015-10-1")
td = national_day - pd.Timestamp.now()
td
时间点和时间间隔之间可以进行加减运算:
T i m e s t a m p C 2015-10-21 10:20:30')
秒数、微秒数和纳秒数。注意这些值与对应的单位相乘并求和才是该对象表示的总时间间隔:
也可以通过关键字参数直接指定时间间隔的天数、小时数、分钟数和秒数:
print pd.Timedelta(seconds=100000)
10 days 01:02:10.500000
1 days 03:46:40
Python 科学计算 (第 2 版)
5 . 5 . 2 时间序列
return index[locations]
■D
a n d a丨
n p .r a n d o m .s e e d (42)
pd_index = ts _ i n d e x .t o j 3 e r i o d ( " M M)
td_index = pd.TimedeltaIndex(np.diff(ts_index))
'2015-08-18 13:13:00*],
dtype=' d at e t i m e 6 4 [ n s ]、 freq=None^ tz=None)
dtype='timedelta64[ns]freq=None)
这三种索引对象都提供了许多与时间相关的属性,例如:
ts 一i n d e x . s h if t (1, "H")
•2015-08-18 14:13:00'],
P a n d a s方
d t y p e = 'd a t e t i m e 64 [ n s] ', freq=Nonej tz=None)
- 便的数据分析库
而对于月份这样不精确的时间单位,则移动一个单位相当于移到月头或月底:
*2015-03-31 12:30:00', *
*2015-08-31 02:40:00',
'2015-08-31 13:13:00'],
dtype= ' d at e t i m e 6 4 [ n s] 'y freq=None^ tz=None)
Datetimelndex.normalizeO 将时刻修改为当天的凌晨零点,可以理解为按日期取整:
t s _ i n d e x .n o r m a l i z e ()
*2015-08-18 13:00:00'],
d t y p e = ' d a t e t i m e 6 4 [ n s ] f r e q = N o n e ^ tz=None)
时间序列提供一些专门用于处理吋间的方法,
例 如 between_time()返 N 所有位于指定时间范
围之内的数据:
2015-01-15 16:12:00 0
2015-02-28 12:30:00 2
2015-08-18 13:13:00 4
d t y p e : int64
而 tshiftO则将索引移动指定的时间:
ts_series.tshift(l, freq="D")
2015-01-16 16:12:00 0
2015-02-16 08:04:00 1
■D 2015-03-01 12:30:00 2
a n d a丨
2015-08-07 02:40:00 3
2015-08-19 13:13:00 4
s 方便的数据分析库
d t y p e : int64
2015-09 4 d t y p e : int64
Freq: d t y p e : int64
td_data = pd.Series(td_index)
ts 一
data.dtype pd 一
d a t a •dtype td 一
d a t a •dtype
0 16 0 1 0 30
1 8 1 2 1 13
2 12 2 2 2 158
3 2 3 8 3 12
4 13 4 8 d t y p e : int64
d t y p e : int64 d t y p e : int64
5 . 5 . 3 与 NaN 相关的函数
Pandas方
与本节内容对应的 Notebook 为:05-pandas/pandas-700~nan.ipynb。
- 便的数据分析库
Pandas 使 用 N a N 表示缺失的数据,由于整数列无法使用 NaN , 因此如果整数类型的列出
现缺失数据,则会被自动转换为浮点数类型。下面将布尔类型的 DataFmme对象传递给一个整
数类型的 DataFrame对 象 的 where()方法。该方法将 False 对象的元素设置为 N aN ,注意其结果
变成了浮点数类型,而没有 N a N 的列仍然为整数类型。
n p .r a n d o m .s e e d (41)
d f _ i n t .dtypes d f _ n a n .dtypes
A int32 A int32
B int32 B float64
C int32 C float64
d t y p e : object d t y p e : object
df__int df 一
nan
A B C A B C
0丨10 3 2 0 10 3 NaN
1 10 1 3 1 10 NaN 3
2 19 7 5 2 19 7 5
3 18 3 3 3 18 3 3
29
Python 科学计算 (第 2 版)
4 12 6 0 4 12 6 NaN
5 14 6 9 5 14 6 9
6 13 8 4 6 13 8 4
7 17 6 1 7 17 6 NaN
8 15 2 1 8 15 NaN NaN
9 15 3 2 9 15 3 NaN
df_nan.isnull() df_nan.notnull()
A B C A B C
count()返回每行或每列的非 N aN 元素的个数:
d f _ n a n . c ou n t () d f _ n a n .c o u n t (a x i s = l )
A 10 0 2
B 8 1 2
C 5 2 3
d t y p e : int64 3 3
4 2
5 3
6 3
7 2
8 1
9 2
d t y p e : int64
330
删除所有 N aN 个数大于等于该阈值的行。
df 一n a n •d r o p n a () df_nan.dropna(thresh=2)
A B C A B C
2 19 7 5 0 10 3 NaN
3 18 3 3 1 10 NaN 3
5 14 6 9 2 19 7 5
6 13 8 4 3 18 3 3
4 12 6 NaN
5 14 6 9
6 13 8 4
7 17 6 NaN
9 15 3 NaN
当行数据按照某利1物理顺序(例如时间)排列时,可以使W N a N 前后的数据对其进行填充。
ffillO 使用之前的数据填充,而 bfillO则使用之后的数据填充。interpolateO使用前后数据进行插值
Pandas方
填充:
- 便的数据分析库
df_nan.ffill() df_nan.bfill() d f _ n a n .i n t e r p o l at e ()
A B C A B C A B C
0 10 3 NaN 0 10 3 3 0 10 3.0 NaN
1 10 3 3 1 10 7 3 1 10 5.0 3
2 19 7 5 2 19 7 5 2 19 7.0 5
3 18 3 3 3 18 3 3 3 18 3.0 3
4 12 6 3 4 12 6 9 4 12 6.0 6
5 14 6 9 5 14 6 9 5 14 6.0 9
6 13 8 4 6 13 8 4 6 13 8.0 4
7 17 6 4 7 17 6 NaN 7 17 6.0 4
8 15 6 4 8 15 3 NaN 8 15 4.5 4
9 15 3 4 9 15 3 NaN 9 15 3.0 4
s.in t e r p ol a t e () s •interpolate(method="index")
8 5 8 6.555556
Python 科学计算 (第 2 版)
9 7 9 7 •000000
此外还可以使用字典参数让fillna()对不同的列使用不同的值填充NaN :
A B C
0 10 3 0
1 10 -999 3
2 19 7 5
3 18 3 3
4 12 6 0
5 14 6 9
6 13 8 4
7 17 6 0
8 15 -999 0
■D 9 15 3 0
andas
A 143 A 143 A 64
B 42 B NaN B 24
C 24 C NaN C 21
columns=["B", "C"],
index=[lj 2, 8, 9])
print d f _ n a n .combine_first(df_other)
A B C
0 10 3 NaN
1 10 4 3
2 19 7 5
3 18 3 3
4 12 6 NaN
5 14 6 9
6 13 8 4
7 17 6 NaN
8 15 4 5
9 15 3 5
5 . 5 . 4 改 变 DataFrame 的形状
本V 介绍表5 4 屮的函数:
表 5 - 4 本节要介绍的函数
函数名 功能 函数名 功能
concat 拼接多块数据 drop 删除行或列
set一index 设置索引 reset一index 将行索引转换为列
stack 将列索引转换为行索引 uastack 将行索引转换为列索引
reorder—levels 设置索引级别的顺序 swaplevel 交换索引中两个级别的顺序
sortjndcx 对索引排序 pivot 创逑透视表
melt 透视表的逆变换 assign 返回添加新列之后的数据
DataFram e 的 sh ap e 属 性 和 N u m P y 的二维数组相同,是一个有两个元素的元组。由于
DataFrame 的 index 和 columns 都支持 Multiindex 索弓丨对象,因此可以用 DataFrame表示更高
数据。
下面首先从 C S V 文件读入数据,并使用 groupbyO计算分组的〒均值。关 于 groupby 在后面
的章节还会详细介绍。注意下面的 s d m e a n 对象的行索引是多级索引:
s o i l s . h e ad () s o i l s _ m e a n .h e a d ()
1.添加删除列或行
保持不变:
def random 一
dataframe(n):
df_list = list(random_dataframe(1000))
%%time
df_resl = pd.DataFrame([])
for df in df_list:
df_resl = df_resl.append(df)
d f _ r e s 3 .l o c [30].e q u a l s (d f _ l i s t [30])
True
Pandas方
5 10-30 Top 5.1 16
2.行索引与列之间的相互转换
- 便的数据分析库
resetjndexO 可以将索引转换为列,通 过 level 参数可以指定被转换为列的级别。如来只希望
从索引中删除某个级别,可以设置 drop 参数为 True 。
print soils_mean.reset_index(level="Contour"),head()
Contour Group pH N
Depth
pH N
Depth Contour Group
3.行索引和列索引的相互转换
stack〇方法把指定级別的列索引转换为行索弓丨,而 unstackO则把行索引转换为列索弓I。下
面的程序将行索引中的第一级转换为列索引的第一级,所得到的结果中行索引为单级索引,而
列索引为多级索引:
Group pH
Contour Depression Slope Top Depression Slope Top
Depth
print soils 一
丨方便的数据分析库
mean.stack().head(10)
Depth Contour
0-10 Depression Group 9
pH 5.4
N 0.18
Slope Group 5
pH 5.5
N 0.22
Top Group 1
pH 5.3
N 0.2
10-30 Depression Group 10
dtype: float64
4.交换索引的等级
Group pH N
Contour Depth
5.透视表
df df 一pivot-pH
Pandas方
Depth Contour pH N Contour Depression Slope Top
- 便的数据分析库
0 0-10 Depression 5.4 0.18 Depth
将剩余的列都当作元素值列,得到多级列索引:
pH N
Contour Depression Slope Top Depression Slope Top
Depth
df_before_melt = df_pivot_pH.reset_index()
df_after— melt = pd.melt(df— before— melt, id— vars="Depth", value— name="pH")
5 . 6 分组运算
所谓分组运算是指使用特定的条件将数裾分为多个分组,然后对每个分组进行运算,最
后再将结果整合起来。
Pandas 中的分组运算由 DataFrame 或 Series 对象的 groupbyO 方法实现。
下而以某种药剂的实验数裾ndose.asv”
为例介绍如何使用分组运算分析数据。在该数据集中
使用了 “A B C D ”4 种不同的药剂处理方式(Tmt) , 针对+同性别(Gender)、不同年龄(A ge )的患者
进行药剂实验,记录下药剂的投药量(Dose)与两种药剂反应(Response)。
dose_df = pd.read_csv("dose.csv")
print dose_df,head(3)
0 50 9.9 10 C 60s F
如 图 5-5所示,分组操作中涉及两组数据:源数据和分组数据。将分纟11数据传递给源数据
的 groupbyO方法以完成分组。groupbyO的 a x is 参数默认为0 , 表示对源数据的行进行分组。源
数据中的每行与分组数据中的每个兀素对应,分组数据中的每个唯一值对应一个分组。由于图
中的分组数据中有两个唯一值,因此得到两个分组。
源数据 a b
索引 A B 分组数据 A B A B
0 1 2 a 0 1 2 1 7 0
瀑RJR.groopby 份 !&敗SC
b 4
Pandas方
1 7 0 2 5 6 3 8
2 5 6 a 3 9 4
3 9 4 a
- 便的数据分析库
4 3 8 b
阁 5-5 groupbyO分纟丨丨示总图
当分组用的数据在源数据中时,可以直接通过列名指定分纟11数据。当源数据是 DataFrame
类型时,groupbyO方 法 返 回 一 个 DataFrameGroupBy 对象。名:源 数 据 楚 S e rie s 类型,则返 N
SeriesGroupBy 对象。在下面的例子中使用T m t 列对源数据分组:
tmt_group = dose_df.groupby("Tmt")
print type(tmt_group)
还可以使用列表传递多组分组数据给groupbyO ,例如下面的程序使用处理方式与年龄对源
数据分组:
当分组数据不在源数据中时,可以直接传递分组数椐。在下而的例子中对长度与源数据的
行数相同、取值范围为[0,5)的随机整数数组进行分组,这样就将源数据随机分成了 5 组:
random_group = dose_df.groupby(random_values)
当分组数据可以通过源数据的行索引计箅时,可以将计算函数传递给groupbyO 。下面的例
子使川行索引值除以3 的余数进行分纟U , 因此将源数据的每行交替地分为3 组。这是因为源数
39
Python 科学计算 (第 2 版)
据的行索引为从0 开始的整数序列。
上述三种分组数据可以任意囱由组合,例如下而的例子同时使用源数据中的性别列、函数
以及数组进行分组:
5.6.2 GroupBy 对象
使 用 len()可以获取分组数:
10 20
据。当使用多列数据分组时,与每个组对应的键是一个元组:
for key, df in t m t _ a g e _ g r o u p :
- 便的数据分析库
key = ( ’B ’
,’6 0 s ’
), shape = (39, 6)
key = ('C', '40s') , shape = (13, 6)
由于 Python 的赋值语句支持迭代接口,因此可以使用下面的语句快速为每个分组数据指定
变•!£名。这是因为我们知道只有4 种药剂处理方式,并 且 GroupBy 对象默认会对分组键进行排
序。可以将 groupbyO的 sort参数设置为 F alse 以关闭排序功能,这样可以稍微提高大M 分组时的
运算速度。
34
get_^roup()力‘
法可以获得与指定的分组键对应的数据,例如:
6 1 0 0 A 50s F
17 5 0 0.003 A 50s M
34 40 11 10 A 50s M
Pandas方
式可以先使用源数据中的某些列进行分组,然后选择另一些列进行后续计算。
- 便的数据分析库
print tmt_group["Dose"]
print tmt_group[["Responsel", "Response2"]]
面的顺序操作:
•如果属性名是源数据对象的某列的名称,则相当于 GroupBy 丨
name], 获取针对该列的
GroupBy 对象。
•如果属性名是源数据对象的方法,则相当于通过 apply()对每个分组调用该方法。注意
Pandas 中走义/ 转换为 apply()的方法集合,
只有在此集合之中的方法才会被自动转换。
关 于 apply()方法将在下一小节详细介绍。
下面的程序得到对源数据屮的D ose 列进行分组的 GroupBy 对象:
print t m t _ g r o u p .Dose
5 . 6 . 3 分组一运算一合并
b
A B A B
0 1 2 1 7 0
2 5 6 4 3 8
3 9 4
g.transf〇m(l«^}d« df:
«KC<\»c«>dji d f: df - u x ( d f .A .« 1 n 〇 , d f.B .v t n ())>
d f .lo c (( d f .A df . a ) . l d u u « ( ) ) )
A B A B
A B A B 0 0 0 0 •1 0
a 9 6 a 9 4 1 4 0 1 4 •3
b 7 8 b 3 8 2 4 4 2 3 4
g.«U(np.iui) 3 8 2 3 7 2
4 0 8 4 0 5
丨
冬I5-6 agg()和transfomiO的运算示阁
1.agg()—聚合
agg()对每个分组中的数据进行聚合运算。所谓聚合运算是指将一组凼N 个数值组成的数据
转换为单个数值的运算,例如求和、平均值、中间值甚至随机取值等都是聚合运算。其回调函
数接收的数据是表示每个分组中每列数据的Series 对象,若回调函数不能处理 Series 对象,则
agg()会接着尝试将整个分组的数据作为 DataFmme 对象传递给回调函数。回调函数对其参数进
左侧第二个表格对应的程序为:
由于在回调函数中访问了屈性A 和 B , 这两个屈性在表示毎列数据的Series对象中不存•在,
因 此 传 递 S eries 对象给回调函数的尝试失败。于 是 agg()接下来尝试将表示整个分组数据的
DataFmme 对象传递给回调函数。该回调函数每次返回结果中的一行,例如图中分组 b 对应的
agg_resl = tmt_group.agg(np.mean) O
agg_res2 = tmt_group.agg(lambda df:df.loc[df.Responsel.idxmax()]) ©
agg_resl agg_res2
C 34 4 4.1 C 60 10 11 50s M
D 34 3.B 3.2 D 80 11 9.9 60s F
2. transform ()—转换
transformO对每个分组中的数据进行转换运算。与 agg()相冋,首先尝试将表示每列的Series
Pandas方
对象传递给回调函数,如果失败,将表示整个分组的 DataFmme对象传递给回调函数。回调函
数的返回结果与参数的形状相同,traasfomiO将这些结果按照源数据的顺序合并在一起。
- 便的数据分析库
阁 5-6的下方右侧两个表格为timsformO 的运算结果,它们对应的回调函数分别对Series 和
DataFmme 对象进行处理。注意这两个表格的行索引与源数据的行索引相同。
下面是对 tmLgroup 进行转换运算的例子。O 回调函数能对 Series 对象进行运算,因此运算
结果中不包含源数据中的字符串列。© 由 于 Series 对象没有 assign )方法,因 此 transformO在尝
试 S e rie s 失败之后,将 表 示 整 个 分 组 的 DataFrame 对象传递给冋调函数。该冋调函数只对
Responsel 列进行转换。
transform_res2 = tmt_group.transform(
t r a n s f o r m _ r e s l .h e a d (5) t r a n s f o r m _ r e s 2 .h e a d (5)
3. filter()— 过滤
filter()对每个分纟J1进行条件判断。它将表示每个分纟J1的 DataFrame对象传递给回调函数,该
DataFmme 对象,其行索引与源数据的行索引的顺序一致。
下面的程序保留Responsel 列的最大值小于11的分组,
注意结果中包含用于分组的列Tm t :
0 50 9.9 10 C 60s F
4. apply()—运用
Tl 功能。它会根据回调函数的返回值的类型选择恰当的合并方式,然而这种自动选择有时会得到
a n d a丨
令人费解的结果。
s方便的数据分析库
.jp p ly d M b d ^ d f : ( d f - d f . n 1 n ( ) ) ( : ] )
.^ p p ly d jw b d a d f :d f.A .$ ^ « p W (2 ))
A B A B
A B a 0 1 0 0 0 a 0 0 0
a 9 6 3 9 1 4 0 2 4 4
b 7 8 b 4 3 2 4 4 3 8 2
1 7 3 8 2 b 1 4 0
f . Apply (〇A t«F r4 a «.
4 0 8 4 0 8
g .A p p ly(X »e b d » d f : d f • d f . _ f n (〉)
图5-7 apply〇的运算示意图
p a n d a s方
—便 的 数 据 分 析 库
注意目前的版本采用 is判断索引是否相同,很容易引起混淆,未来的版本可能会对这
A 一点进行修改。
下 而 计 算 tm ugroup 的每个分组中每列的最大值和平均值。注意最大值的结果中包含字符
串列,而平均值的结果中不包含字符串列:
45
Python科学计算 (第 2 版)
Tmt Responsel 0 1
A 248 10 Tmt
164 10 A 10 10
B 113 0.19 B 10 10
26 9.4 C 0.004 9.9
C 191 10 D 0.33 11
236 1.7
D 188 0.061
8 0.001
Name : Responsel, d t y p e : float64
当冋调函数的返冋值是DataFmme对象时,根据其行标签是否与参数对象的行标签为同-
对象,会得到不同的结果:
B 9 40 11 10 B 60s F
16 30 9.8 10 B 60s F
346
下面的例子分別调用 Cython 编写的提速方法 mean〇和 使 用 applyO包装之后的 quantile〇
方法:
tmt_group.mean() t m t _ g r o u p .q u a n t i l e ( q = 0 .75)
Tmt Tmt
A 34 6.7 6.9 A 50 10 10
B 34 5.6 5.5 B 50 9.8 10
C 34 4 4.1 C 50 9.6 9.6
D 34 3.3 3.2 D 50 8.9 8.4
木节以 DataFmmeGmupBy对象为例介绍了分组运算的_木概念以及常用方法。
Pancks 提供
的分组运算功能丨•分强大,建议读者在理解了木节的内容之后,再详细阅读官方的帮助文档,
以了解更多的用法和技巧。
5;
7雖 处 理 和 可 视 化 实 例
5 . 7 . 1 分 析 Pandas 项目的提交历史
cd pandas
该文件中的一条提交记录凼多行文本构成,例如:
commit 758ca05e2eb04532b5d78331ba87c291038e2c61
A u t h o r : Garrett-R <xxxx@xx x xx . c o m>
Date: Sat Dun 27 15:11:12 2015 -0700
Python科学计算 (第 2 版)
DOC: Add warning for newbs not to edit auto-generated file, #10456
def r e a d _git_log(l o g _ f n ) :
import io
with io.open(log_fn, "r", encoding="utf8") as f :
message = [ ]
message_start = False
for line in f:
line = line.strip()
if not line:
Tl continue
a n d a丨
if line.startswith("commit"):
s方便的数据分析库
del message[:]
message_start = False
elif l i n e .s t a r t s w i th (" A u t h o r :"):
a u th o r = l i n e [ l i n e . i n d e x ( n : n) + l : l i n e . i n d e x ( ' V * ) ] . s t r i p ( )
elif l i n e . s t a r t s w i t h ( " D a t e :
datetime = l i n e [l i n e .i n d e x (":")+l :].strip()
下面将生成器的数据转换为DataFmme对象,其中包括了 12109条提交记录:
df-C 〇
mmit = p d .DataFrame(read__git_log("data/pandas.l o g " ),
(12109, 3)
为了分析时间数据,需要将提交时间的字符串转换为吋间列,使 用 t〇
_datetime()可以快速
完成这个任务,它会自动尝试各种常用的日期格式。由下面的输出可知,它进行了时区转换,
将所有的时间都转换成了世界标准时间:
d f _ c o m m i t ["Date"] = pd.to 一
datetime(df — commit.Datestring)
4
3:
DateString Date
0 Tue Dul 7 23:43:31 2015 -0500 2015-07-08 04:43:31
df _ c o m m i t ["Timezone"] = df_commit.DateString.str[- 5 :]
在提交说明的每行丌头可能会有全部大写字母的单词,该单词通常用于描述提交的分类。
在大致浏览提交记录之后,决定使用正则表达式nA([A -Z /]{2,
12})"提取这种分类信息。其中A勹
re.M U L T IU N E 配合使用,可以匹配每行的开头;(
)括起来的部分就是要提取的内容;[A -Z /]匹
配大写字母或斜杠字符;{2,
12}表示前面的匹配重复2 到 12次。
import re
Pandas方
d f _ c o m m i t ["Type"] = d f _ c o m m i t .M e s s a g e .s t r .e x t r a c t (r "A ([A - Z / ]{2,12})"^
flags=re.MULTILINE)
- 便的数据分析库
下面使用 Value_axmts ()统计时区和分类。可以看到美国所在时K 的提交数最多,修 复 BUG
的提交数最多。
tz_counts = p d .v a l u e _ c o u n t s (d f _ c o m m i t .T i m e z o n e )
type— counts = pd.value— counts(df_commit.Type)
t z _ c o u n t s •h e a d () type— c o u n t s •h e a d ()
d t y p e : int64 d t y p e : int64
为了方便后续处理,下 面 将 D a te 列设置为行索引,并且按照吋间顺序排序。注意我们设
置 drop 参数为 False,保 留 D ate 列。
49
Python 科学计算 (第 2 版)
time_delta = df_commit.Date.diff(l).dropna() O
a x .s e t _ x l a b el ("Hours")
pandas方
0
—便 的 数 据 分 析 库
图5 - 8 两次提交的时间间隔统计
下面的程序绘制每个星期的提交数,结果如图 5 - 9 所示。先调用时间序列的 r e s a m p l e O 方法
对其进行分组计算,其第•一个参数为表示分组时间周期的字符串, “W ” 表示按星期分组。h o w
参数指定对每个分组执行的运箅。"count”表示返回每个分组中值不为N a N 的元素个数。因此可
以使用不包含N a N 的任何列得到相同的结果。
ax •set_ylabel (u _•提交次数" )
图5 - 9 每个期的提交次数
351
h o w 参数还支持回调函数,例如下面的程序绘制每个月的提交人数,结果如图 5 - 1 0 所示。
这里使用 len(s.imique 〇)得到每个分组去重之后的长度:
a x .set_ylabel (u "提交人数••)
2014 2015
Dot
Pandas方
图5 - 1 0 每个月的提交人数
- 便的数据分析库
请读者思考如何使用 groupbyO 实现与上述 resample〇相同的运算。
s_authors = df_commit.Author.value_counts()
print s_authors.head()
y-p 943
Chang She 629
d t y p e : int64
print d f _ c o u n t s .shape
Python 科学计算(第2 版)
(72, 485)
d f _ c o u n t s [s _ a u t h ors.head( 5 ) .index].plot(kind="area"^
subplots=True,
figsize=(8, 6),
c o l o r = p l .r c P a r a m s ['a x e s .c o l o r _ c y c l e '][〇]>
alpha=0.5,
sharex=True,
sharey=True)
0
5 ^ 50
pandas方
<)!
0!
0 50 sO&
—便 的 数 据 分 析 库
0!
OT
OI
s
o s^w 5 ^o
o'
o'
^ 5
o oQ
os o s o^
^i
ol
o'
oi
5
o
ol
oos
必、
、
5
Month
^
5
图5-11 前 5 名作者的每月提交次数
Q
5
352
© 使 川 unstack()将 访 0 级索引转换为列索引,得到一个 DataFrame对象,获取其最后60周
的数据之后,将其中的 N aN 填充为0。这 样 就 得 到 了 于 做 图 的 active_data 数据。
注 意 daily_com m it 的索引不是按照时间先后顺序排列的,但 unstack〇返回的结果中行索弓I
和列索引都是按从小到大的顺序排列的:
daily_commit = daily_commit.sort_index()
active 一
data = daily 一c o m m i t . u n s t a c k ( 0 ) . i l o c [ - 6 0 : ] .fillna(0) ©
ax.set 一aspect("equal")
Pandas方
a x .pcolon m e sh ( a c t i v e _ d a t a .v a l u e s , cmap=.’G r e e n s ",
vmin=0, v m a x = a c t i v e _ d a t a .v a l u e s .m a x () * 0.75) O
- 便的数据分析库
tick_locs = np.arange(3, 60, 10)
ax.set_xticks(tick_locs + 0 . 5 )
ax. set_xticklabels(active_data. columns [tick_locs].to_timestamp(how=" start") .format())
ax.set_yticks(np.arange(7) + 0.5)
ax.set_yticklabels(DAYS)
ytick_format=dict(zip(range(7), DAYS)).get^
5 . 7 . 2 分析空气质量数据
表 5 - 5 空气质量数据
index T ime City Position AQI Level PM2_5 PM10 CO N〇2 03 S〇2
15:00:00
15:00:00
15:00:00
15:00:00
丨方便的数据分析库
15:00:00
store = pd.HDFStore("data/aqi/aqi.hdf5")
df_aqi = store.select("aqi")
df_aqi.head()
卜面用value_counts()杏看所有的城市名:
print df 一a q i •C i t y •value— c o u n t s ()
天津 134471
北京 109999
上海 92745
天津市 13
北京市 12
上海市 10
d t y p e : int64
print df_aqi.City.value_counts()
天津 134484
北京 110011
上海 92755
d t y p e : int64
A Q I 列为空气质量的评分,而其他的数值列为各利I成分的指标值。下面通过〇)耐)计兑这
些值之间的相关性:
c o m = df_aqi.corr()
print c o m
Pandas方
为了更直观地显示上面的相关矩陈,可以将其绘制成如图5-13所示的图表。由图可知空气
- 便的数据分析库
质量指数与 PM 2.5的相关性最大,而 与 0 3 略呈负相关性。
fig, ax = p i . s u b p l ot s ()
plot_datafname_as_colormesh(corr, ax=ax^ colorbar=True, xtick 一rot=90)
s
8
SN
a<
—sd
OUNd
£0
图5 - 1 3 空气质量参数:乙间的相关性
ax = mean J3m2_5.plot(kind="kde")
a x . s et_xlim(0J 400)
。
力H
OtOM
0U0I2
0010
0002
&000 —
Pandas方
图5 - 1 4 每座城市的日平均P M 2 . 5 的分布图
- 便的数据分析库
m e a n _ p m 2 _ 5 .c o m ()
表 5 * 6 三座城市 P M 2 . 5 之间的相关性
index 上海 北京 天津
上海 1.0 -0.146 0.0718
ax.set— xticklabels(DAYS)
6
5
图5-15 -• 星期中P M 2 . 5 的平均值
下面统计一天中每个小时的PM 2.5的平均值,结果如图5-16所示,可以看出北京市白天的
Pandas方
空气质量比夜晚要好一些:
- 便的数据分析库
hour_mean = d f _ a q i .g r o u p b y ( [df_aqi.Time.d t .hour^ "City"]).PM2_5.mean()
ax = h o u r _ m ean.unstack().plot(kind="Bar"^ figsize=(10^ 3))
■■上渴 ■■北铒 xm
下 面 计 算 北 京 市 每 个 观 测 点 的 每 个 月 的 PM 2.5的 平 均 值 ,得 到 一 个 多 级 索 引 的
month_place_ mean 序列,其 第 0 级为月份、第 1级为观测点。然后调用其 mean(level= l )方法,计
算每个观测点的 PM 2.5的平均值,结果如图5-17所示。
df 一bj = d f _ a q i . q u e r y ( " C i t y = = ’:
lt^ ’
_’
)
month_place_mean = d f _ b j .g r o u p b y ( [df_bj.Time.d t .t o _ p e r i o d ( " M " ),
"Po s i t i o n "]).P M 2 _ 5 .m e a n ()
Python科学计算(第2 版)
place_mean = nx)nth_place_mean.mean(level=l).order()
place_mean.plot(kind="bar")
SK凶w陕
雪
^
9M
W
w § s
!ft &fl
«蹤
H< HJ fr
it
®
1C
•i f
5
0
■D
阁5 - 1 7 北M 市各个观测点的P M 2 . 5 的平均值
andas
丨方便的数据分析库
下面分別选出空气质量最好和最差的两个观测点,并使用柱状图显示它们每个月的PM 2.5
平均值:
Position
Msu 解
定怀典衣
= D I
«中«
V
S^LOZ
flo
es§
s
051s
150?
5.20
ZI4 1 0 Z
lo .ssz
90- s ls
o-
in
sloz
rv
flo z
8<mo
3 . S 103
嗦
W
A
l
o
lOZ
CSI rN <N
图5 - 1 8 北京市各观测点的J j平均P M 2 . 5 值
358
解6 亭
SymPy-符号运算好帮手
S y m P y 是 P yth on 的数学符号计算库,)
|拕可以进行数学表达式的符号推导和演算。随着
SymPy 0.7.3的发布,它已经逐渐发展成熟。虽然与一些专业符号运算软件相比,S ym P y 的功能
以及运算速度都还较弱,但由于它完全采用 Python 编写,冈而能很好地与其他的Python 科学计
算厍结合使用。例如本章最后一节将介绍如何使用Sym P y 得到一个简单的单摆系统的微分方程
组,并将其丨动转换成数值运算程序,通 过 S d P y 中 的 oddm ()求解该微分方程组,最后使用
matplotlib完成动画模拟。
6.1从伊冊始
6 . 1 . 1 封面上的经典公式
下面是本书封面左上角的数学公式:
e i7T + 1 = 0
该公式被称为欧拉恒等式,其 中 e 是自然常数,i 是虚数单位,7T是圆周率。它被誉为数学
中最奇妙的公式,它 将 5 个蕋本数学常数用加法、乘法和¥运兑联系起来。下 面 Sym P y 验证
这个公式。
首 先 从 sym py 萍载入所有名字,其 中 E 表示自然常数,I 表示虚数单位,p i 表示同|周率,
因此可以用它们直接求欧拉公式的值:
式来得到:
Python科学计算 (第 2 版)
e IX cosx + isinx
在 S ym P y 中可以使j|j expand()将表达式展开,下面j|j 它展幵e lx试试看:
x = symbols("x")
expand( E * * ( I * x ) )
eix
expand(exp(I*x)j complex=True)
ie_3xsin〇
Hx) + e_3xcos 〇Rx)
需要如下重新定义X:
—号 运 算 好 帮 手
x = Symbol("x", real=True)
expand(exp(I*x), complex=True)
isin(x) + cos(x)
终于得到了欧拉公式,那么如何证明它呢?可以用泰勒多项式对其进行展开:
re(tmp)
4 2
x x6
40320 720 24 2
下面对 C〇
s(x )进行泰勒展幵,可以看到其各项与上面的结果一致:
series(cos(x), x, 0, 10)
x2 x4 x6 X
8
1- —+ + 0 ( x 10)
2 24 720 40320
60
下面获得 tm p 的虚部:
im(tmp)
5 3
x9 x7
+ — + x + 3(〇(x10))
362880 5040 120
series(sin(x), x, 0, 10)
x° x5 X X
x—-+ + 0(x 10 '
6 . 1 . 2 球体体积
sy m p y符
在 S ciP y —章中介绍了如何使用数值定积分计兑球体的体积,而 S ym P y 中的 integrate(侧可
以计算符号积分。例如下面的语句用 integrate〇做不定积分运算:
—号 运 算 好 帮 手
i n t e g r a t e (x * s i n (x ) x)
— xcos(x) + sin(x)
如果指定变量 x 的取值范围,integrateO则做定积分运总:
—2tt
为了计算球体体积,首先看看如何计算岡形面积。假设圆形的半径为 r ,则圆上任意一点
的 Y 坐标函数为:
y (x ) = yjr 2 — x2
可以直接对函数 y (x )在-r 到 r 区间上求定积分得到半圆面积。下而的程序计算该定积分:
首先需要定义运算中所需的符号,用 symbolsO可以一次创建多个符号。定义半径 r 时需要设置
positive 参数为 True , 表示圆的半径为正数:
x, y = s y m b o l s ( ’x, y ’
)
r = symbolsCr', positive=True)
circle— area = 2 * integrate(sqrt(r**2 - x**2), (x ,-i% r))
circle_area
71r 2
6
3
Python科学计算 (第 2 版)
接下来对此面积公式求定积分,就可以得到球体的体积,但是随着 X 轴坐标的变化,对应
的切面半径会发生变化。假 设 X 轴的坐标为 X,球体的半径为^•,则 X 处的切面的半径可以使
用前面的公式 y (x )计兑出。因此需要对 circle_ area 中的变量 r 进行替代:
7i(r2 — x 2)
integrate(circle 一a r e a ( x , -r , r ) )
4 tt
T r
subs()可以替换表达式中的符号,它有如下3 种调用方式:
sy m p y符
• expression.subs(x ,
y): 将算式中的 x 替换成 y 。
• expression.subs({x:y,u:v }): 使用字典进行多次替换。
—号 运 算 好 帮 手
• expression.subs([(x ,
y) ,
(u ,
v)]): 使用列表进行多次替换。
请注意多次替换是顺序执行的,因此 expression.sub([(x ,y ), (y ,x )])并不能对符号 x 和 y 进行
交换。
6 . 1 . 3 数值微分
所谓数值微分,是指根据闲数在一些离散点的函数值,推算它在某点的导数或高阶导数的
近似值的方法。例如当 h 足够接近于零时,可以使用下面的公式计算f (>〇在 x 处的导数f :
f(x + h ) - f (x )
f(x)
h
上面的公式使用两个函数值计算导数值,被称为两点公式,使用的点数越多数值微分的精
度也就越高。可以使用 S ym P y 提 供 的 as_ finite_diff〇自动计算 N 点公式。下面先使用 symbolsO
定义三个符号对象,其中定义 f 吋设置 d s 参数为 Function表示它是表示数学函数的符号。
x = symbols('x', real=True)
h = s y m b o l s ( 'h ', positive=True)
f = s y m b o l s ( ' f ', cls=Function)
导数:
f_di 幵 = 1)
f diff
362
然 后 调 用 as_fmite_diff(),将一阶导数转换为使用 f (x )、f (x -h)、f (x -2*h)、f (x _3*h)表 达 的 4
点公式:
expr 一diff = as 一
finite_diff(f_diff, [x, x-h, x-2*h, x-3*h])
expr 一diff
11 1 3 3
^ f(x) —^ f(一3h + x) + ^ f (—2h + x) —& f(一h + x)
下而以f(x) = x • e _ x 2 为例比较数值求导与符号求导的误差。
首先使用subs 〇方法将 expr_diff
中的 f(x)替换为目标函数,并调用其 doit〇方法计算导函数:
S ym P y符
sym— dexpr
- 2x 2e_x2 + e _x2
- 号运算好帮手
然后调用 l a m b d i f y O , 将上面的 s y m _ d e x p r 表达式转换为数值运算的函数。其第一个参数为
&变量列表 ,第二个参数为运兑表达式,这里将 m o d u l e s 参数设置为" n u m p y " , 因此 sym_dfunc()
可以对数组进行运算:
s y m _dfunc(np.array([-1, 0, 1]))
a r r a y ( [ -0.36787944, 1. , -0.36787944])
print expr_diff.args
上面的加法项没有按照自变量从小到大的顺序排列。
下面使用通配符 w 和 c 组成的模板 c *
f (w )对每个加法项进行匹配,提収出每项的系数和函数参数:
w = Wild("w")
c = Wild(..c")
每个匹配结果是一个以通配符为键的字典,例如下面是第一项的匹配结果,它表示该项的
系数为-3/ h , 函数 f 的参数为-h + x 。
print p a t t e r n s [0]
63
Python科学计算 (第 2 版)
{w一:-h + x, c一:-3/h}
下面使用通配符 w 的匹配结果计兑出排序用的键值,并从排序之后的列表屮选择出每个匹
配结果中与通配符C 对应的表达式:
绝对误差:
—号 运 算 好 帮 手
4.08944167418e-09
为了比较点数与误差之间的关系,在下面的 finite_diff_coefficients〇函数屮计算间隔为h 、点
数为 order的系数,并绘制 2 、3 、4 点公式对应的误差曲线,结果如图 6-1所示,注 意 Y 轴为对
数轴。
64
for t in sorted(patterns, key=lambda t:t[w])])
return coefficients
sy m p y符
阁6 ~ 1 比较不同点数的数值微分的误差
—号 运 算 好 帮 手
6.2数 学 式
本节详细介绍数学表达式的结构,虽然这部分内界比较枯燥,似只有了解表达式的结构,
才能随心所欲地对其进行处理,将 Sym P y 运州到更复杂的计算中。
6 . 2 . 1 符号
65
Python 科学计算 (第 2 版)
type(xl)
s y m p y .c o r e .s y m b o l .Symbol
x2 = S y m b o l ( Mx2")
t = X0
a, b = symbols("alpha, beta")
sin(a) + sin(b) + t
数学公式中的符号一般都有特定的假设,例 如 m 、11通常是整数,而2经常)|;
|来表示复数。
在 用 vm<)、symbols()或 Sym tol ()创 建 Sym bol 对象时,可以通过关键宁•参数指定所创述符号的假
设条件,这些假设条件会影响到它们所参与的计算。例如,下面创建了两个整数符号m 和 n 以
及一个正数符号 X:
x = Symbol("x''^ positive=True)
[•i s _ a l g e b r a i c 、
'i s _ a l g e b r a i c _ e x p r 'y
'i s _ a n t i h e r m i t i a n 'y
'i s _ b o u n d e d ',
66
在下面的判断中,X 是一个符号对象,它是一个正数,因为它可以比较大小,所以它不是
虚数。X 是一个复数,因为复数包括实数,而实数包括正数。
x.is 一
Symbol x.is_positive x.is_imaginary x.is 一complex
x.assumptions©
sy m p y符
在 S ym P y 中所有的对象都从B asic 类继承,
实际上这些丨8_*屈性和 assumptionsO属性都是在
B asic 类中定义的:
—号 运 算 好 帮 手
Symbol.mro()
[s y m p y .c o r e .s y m b o l .Symbol^
s y m p y .c o r e .e x p r .AtomicExpr^
s y m p y .c o r e .b a s i c .Atom,
s y m p y .c o r e .e x p r .Expr^
s y m p y .l o g i c .b o o l a l g .Boolean,
s y m p y .c o r e .b a s i c .Basic
s y m p y .c o r e .e v a l f .EvalfMixin,
object]
6 . 2 . 2 数值
对象:
0 5/6
67
Python科学计算(第2 版)
I 是 R a t i o n a l 对象,它由两个整数的商表示,数学上称之为有理数。也可以直接通过 Rational
6
创建:
type(S(5)/6)
s y m p y .c o r e .n u m b e r s .Rational
1
2
实数用 F l o a t 对象表示,它和标准的浮点数类似,但足它的精度(有效数字)u丨以通过参数指
定。兩于在浮点数或 F l o a t 对象内部都使用二进制方式表示数值,冈此它们都可能无法精确表
示丨•进制中的精确小数。可以使用 N ()査看浮点数的实际数值,例如下而的语句査看浮点数0.1
和 10000.1的 60位有效数字,可以看到数值的绝对精度随着数值的增大而减小:
S y m py符
0.100000000000000005551115123125782702118158340454101562500000
10000.1000000000003637978807091712951660156250000000000000000
因为浮点数的精度有限,所以在使用它创建 F lo at 对象时,即使指定精度参数也不能缩小
它与理想值之间的误差,这时可以使用字符弟表示数值:
数值:
3.1415926535897932384626433832795028841971693993751
1.4142135623730950488016887242096980785696718753769
6 . 2 . 3 运算符和函数
var("x, y, z__)
Add(x, y, z)
x + y + z
xyz + x y + sin(z)
sy m p y符
xyz + x y + sin(z)
—号 运 算 好 帮 手
在 B asic 类中泣义了两个很重要的属性:func 和 args。func 属性得到对象的类,而 args 得到
其参数。使用这两个屈性可以观察Sym P y 所创建的表达式。也许读者会对没有减法运算类感到
奇怪,下面让我们看看减法运算所得到的表达式:
t = x - y
369
Python 科学计算 (第 2 版)
图6 - 2 表达式的树状结构
sy m p y符
由 于 B a s i c 对 象 的 a r g s 属性类型是元组,因此表达式一旦创建就不能再改变。使用不可变
—号 运 算 好 帮 手
的结构表示表达式有很多优点,例如可以用表达式作为字典的键。
除了使用 Sym P y 中预先定义好的具有特殊运算含义的数学函数之外,
还可以使用 F i m c t i o n O
创建自定义的数学函数:
f = FunctionC'f")
issubclass(f, Function)
True
当 用 f 创建一个表达式时,就相当于创建它的一个实例:
t = f(x, y)
f f (x , y )
f 的实例 t 可以参与表达式运算:
t + t * t
f2(x,y) + f(x,y)
3
70
6 . 2 . 4 通配符
使用通配符可以创逑匹配特定表达式的模板。例如在下面的例子中,a 和 b 是 W i l d 通配符
对象,a 2 足由这两个通配符创建的表达式模板。调用表达式对象的 m a t c h ( ) 方法以使用指
定的模板匹配表达式。它返回一个键为通配符、值为与该通配符匹配的子表达式的字典。
执 行 S y m P y 提 供 的 init_printing〇可以使用数学符号显示运算结果,但 会 将 P y t h o n 的内
置对象也转换成 L a t e X 显示。为了编写方便,本书使用一般文本显示内置对象,而用
本书提供的 %s y m p y _ l a t e x 魔法方法将内置对象转换为 L a T e X 。
x, y = symbols("x, y__)
a = WildC'a")
b = Wild(_.b")
S ym P y符
{a: 3x, b: x + y}
而 find() 方法则在表达式的树状结构中搜索所有与模板匹配的子树。下而在x 3 +
- 号运算好帮手
3 x 2y +
3xy2 + y 3 中搜索与模板匹配的所有子表达式:
x 3 + 3 x 2y + 3 x y 2 + y 3
{2,3,x,x2,x3,y,y2,y3,3xy2,3x2y,x3 + 3 x 2y + 3 x y 2 + y 3}
整 数 2 居然也与模板匹配,这有些出乎意外,我们使用下面的 fmd_match〇输出所有子表达
式的与模板匹配的结果,如表6-1所示:
def find 一
match(expr, p a t t e r n ) :
find__match(expr, a * b**2)
表 6 - 1 与表达式的匹配结果一
表达式 匹配结果
3xy2 {a: 3x, b : y }
2 (a: 1, b: V 2 ]
3 (a: 1, b: V 3 ]
y {a: 1, b i V y ]
37
Python科学计算 (第 2 版)
(续表)
表达式 匹配结果
X {a: 1, b: V x )
x 3 + 3 x 2y + 3 x y 2 + y 3 |a: 1, b: y jx 3 + 3 x 2y + 3 x y 2 + y 3 »
X3 a: 1, b : x 2 *
3 x 2y {a: 3y, b: x}
y2 {a: 1, b : y }
y3 a:1, b : y 2|
X2 {a: 1, b : x }
出上面的结果可知,在模板与表达式匹配的过程中 S y m P y 会对表达式进行变换,找到数
学意义上正确匹配的结果。为了剔除棹这些丨表达式变换而得到的结果,可以在定义通配符时
使 用 exclu d e 参数指定不能匹配的对象列表。当与通配符匹配的表达式中包含该参数中的任何
S y m py符
a = Wild("a", exclude=[l])
b = Wild("b", exclude=[l, Pow])
表 6 - 2 与表达式的匹配结果二
表达式 匹配结果
3xy2 {a: 3x, b : y }
3 x 2y {a: 3y, b : x }
y3 {a: y, b: y}
X3 {a:x, b : x }
可以使用 r e p l a c e o 方法对表达式中的子表达式进行替换,例如下面将所有与 a * b * * 2 匹配
的子表达式替换为(a + b)**2:
372
expr = sqrt(x) / sin(y**2) + abs(exp(x) * x)
find— match(expr, f)
表 6 - 3 与表达式的匹配结果三
表达式 匹配结果
ex {WildFunction(f): ex}
sin(y2) { W i l d F u n c t i o n ( f ) : s i n ( y 2 )}
|xex | { W i l d F u n c t i o n ( 〇: |xex |}
6.3符 骑 算
DVD
S ym P y符
- 号运算好帮手
S y m P y 所提供的符号运算功能十分丰富,由于篇幅受限,本节只能简单介绍 S y m P y 的
些常用的符号运算功能。
6 . 3 . 1 表达式变换和化简
simplifyO可以对数学表达式进行化简,例如:
simplify((x + 2) ** 2 - (x + 1) ** 2)
2x + 3
simplifVO调 州 S y m P y 内部的多种表达式变换函数来对其化简。但是数学表达式的化简是一
件非常复杂的工作,对于冋一个表达式,根据其使用 FI的可以有多种化简方案。本节介绍 SymPy
提供的各种表达式变换函数,充分利用这些闲数可以实现表达式的变换和化简。
mdsimpO对表达式的分母进行有理化,结果中的分母部分不含无理数。例如:
- ( - V 5 + 2V2)
也可以对带符号的表达式进行处理:
-Vxy + Xyfy
xy(x — y)
ratsimpO对表达式中的分母进行通分运兑,即将表达式转换为分子除分母的形式:
37:
Python 科学计算 (第 2 版)
natsimp(x / (x + y) + y / (x - y))
2y2
xz — y z
分母:
%sympy_latex f r a c t i o n (r a t s i m p (1 / x + 1 / y))
(x + y, xy)
请注意 fracti〇n()不会自动对表达式进行通分运算,因此:
%sympy_latex f r a c t i o n (1 / x + 1 / y)
cancel〇对分式表达式的分子分母进行约分运算,去除它们的公因式:
S y m py符
cancel((x ** 2 - 1) / (1 + x))
—号 运 算 好 帮 手
x- 1
apart()对表达式进行部分分式分解,它将一个有理函数变为数个分子及分母次数较小的有
理函数。下 面 用 它 将 传 递 函 数 ^^分 解 为 两 个 次 数 较 小 的 传 递 函 数 之 和 :
s = symbols("s")
trans_func = l/(s**3 + s**2 + s + 1)
a p a r t(trans_func)
s - 1 1
_ 2s2 + 2 + 2s + 2
t r i g s i m p O 化简表达式1丨
1的三角函数,通 过 m e t h o d 参数可以选择化简兑法:
sin(2x) + 1
expand_trig()展开三角函数表达式:
expand 一
trig(sin(2 * x + y))
expand()根据用户设置的标志参数对表达式进行展开。默 认 惜 况 K , 表 6 4 中的标志参数为
True:
74
表 6 ~ 4 标志参数一
标志 表达式 结果 说明
mul expiind(x*(y + z)) x y + xz 展开乘法
log expiind(log(x*y**2)) log ( x y 2) 展开对数函数的参数屮的乘积
和幂运算
multinomial cxpaiid((x + y)**3) x 3 + 3 x 2y + 3 x y 2 + y 3 展开加减法表达式的整数次®
powcr_basc expand((x*y)**z) (x y)z 展开幂闲数的欣数乘积
power一exp cxpand(x**(y + z)) x^xz 展开对幕函数的指数和
x, y, z = symbols(__x,y,z__, positive=True)
expand(x * log(y * z ) , mul=False)
sy m p y符
x(log(y) + log ⑵ )
—号 运 算 好 帮 手
expand() 的表 6 - 5 中的标忐参数默认为 False:
表 6 - 5 标志参数二
标志 表达式 结果 说明
complex expand(x*y) xy 展开复数
func expand(gamma(x+ 1)) r(x + 1) 对一些^持殊函数进行展开
trig expand(sin(x + y)) sin(x + y) 展开三角函数
• c o m p l e x : 展开复数的实部和虚部。
x, y = symbols("x,y", complex=True)
expand(x * y, complex=True)
• fimc: 对一些特殊函数进行展开。
e x p a n d (g a m m a (1 + x), func=True)
x「 (x)
• t r i g : 展开三角函数。
sin(x)cos(y) + sin(y)cos(x)
75
Python 科学计算 (第 2 版)
factor(15 * x * * 2 + 2 * y - 3 * x - 1 0 * x * y )
(3x — 2y)(5x — 1)
collect〇收集表达式中指定符号的有理指数次幂的系数。下面用它获取表达式 e q 中 x 的各
次幂的系数。首先需要对表达式进行展开,得到一系列乘式之和,然后调用 cdlectO 对 x 的幂的
系数进行收集:
eq = (1 + a * X ) ** 3 + (1 + b * X ) ** 2
e q 2 = expand(eq)
collect(eq 2 > x)
p = collect(eq2, x, evaluate=False)
P[S(1)] p[x**2]
2 3*a**2 + b**2
eq 2 .coeff(x, 0 ) e q 2 .coeff(x, 2 )
2 3*a**2 + b**2
collect()也可以收集表达式的各次¥的系数,例如下面的程序收集表达式sin2x 的系数:
(a+b)sin(2x)
6 . 3 . 2 方程
a, b, c = symbols(_'a,b,c_')
%sympy_latex solve(a * x ** 2 + b * x + c, x)
76
2: ( - b + >/-4 ac + b 2) , - ^ (b + >/-4 ac + b 2
%sympy_latex solve((x ** 2 + x * y + 1 , y ** 2 + x * y + 2 ), x, y)
V 3i 2i 广、 /V5 i 2i 广、
mots〇可以计算单变量多项式的根:
6 . 3 . 3 微分
S ym P y符
Derivative 是表示导函数的类,它的第一个参数是需要进行求导的表达式,第二个参数是求
- 号运算好帮手
导的& 变量。请注意 Derivative 所得到的是一个导函数,它并不会进行求导运算:
t = D erivative(sin(x)> x)
S Sin(X)
调用其 doit〇方法可计算求导的结果:
t.doit()
cos(x)
diff(sin( 2 *x)j x)
2cos(2x)
Denivative(f(x), x)
d
f(x)
dx
37*
Python 科学计算(第 2 版)
d3
f(x)
dx3
也可以对不同的自变量符号计兑偏导数:
Denivative(f(Xj y), x, 2 y y, 3)
d5
f(x,y)
dx2 dy3
求导的结果:
di 幵(sin(x * y), x, 2, y, 3)
6 . 3 . 4 微分方程
S y m py符
ds〇M )可以对微分方程进行符号求解。它的第一个参数是带未知函数的表达式,第二个参
—号 运 算 好 帮 手
x = s y m b o l s( ' x ' )
f(x) = Cxex
dsolve(eq, f(x))
上面的解是采用泰勒级数展开之后的结果。如果使firiie ^ group•解法,则可以得到更简洁的
结果:
37:
dsolve(eq, f(x), hint="lie_group")
f W = C.e^-sinCx)
•lie_group ’
:f(x) == l/(Cl*exp(x) - sin(x)),
'order': 1 }
通 过 sympy.ode.allhints可以查看系统义持的所有解法:
sy m p y符
s y m p y .o d e .allhints
—号 运 算 好 帮 手
( 'separable 、
'l s t _ e x a c t ',
'l s t _ l i n e a r ',
'Bernoulli'^
• • 參
6 . 3 . 5 积分
integrate〇可以计算定积分和不定积分:
• integrated,x ) : 计算不定积分/ f d x
• integrated,
(x ,
a,b )): 计算定积分Jab fd x
• integrate^ ,
(x ,
a,b ),
(y ,
c,d)):计算双重定积分 Jcd Jab f dxdy
和 Derivative 对象表示微分表达式类似,
Integral对象表示积分表达式,
它的参数和 integrateO
的类似,例如:
e = Integral(x*sin(x)> x)
/ xsin(x) dx
79
Python 科学计算(第2 版)
e.doit()
—xcos(x) + sin(x)
e 2 = Integnal(sin(x)/x, (x, 0 , 1 ))
e 2 .doit()
Si⑴
print e 2 .evalf()
0 .94608307036718301494135331382317965781233795473811
—号 运 算 好 帮 手
_ 的积分被定义为一个特殊函数,它 从 0 到无穷的定积分值为tt/ 2 , 即:
X
sin (x )
dx = tt/ 2
x
但 是 S ym P y 的数值计算功能还不够强大,不能对应这利1情况的定积分:
e3 = Integnal(sin(x)/x, (x, 0, 〇
〇))
e3.evalf()
-4.0
而调用 doit()则能计算出精确的符号结果:
e3.doit()
Tt
6 . 4 输出符号表达式
80
Sym P y 可以将表达式输出成各利1格式,除了 I者如 LaTeX 、M athM L 等显示用的格式之外,
还 n j 以将表达式转换成Python、C 、Fortran 函数,从而将符号表达式转换成数值运算函数。
6.4.1 lambdify
lambdifyO可将表达式转换为数值运算闲数。它的第一个参数是作为参数的符号序列,第二
个 参 数 是 表 达 式 或 表 达 式 序 列 。例 如 下 而 用 solveO 得 到 一 元 二 次 方 程 的 两 个 符 号 解
quadmtic_roots, 然后调用 lambdify〇将其转换成数值运算函数:
[1.0, 0.5]
S ym P y符
可以得到复数结果:
- 号运算好帮手
import cmath
lam 一
quadratic_roots_complex = l a m b d i f y ( (a^ c)) quadratic 一rootsmodules=cmath)
lam_quadratic_roots_complex(2, 2 y 1 )
[(-0.5+0.5j), (-0.5-0.5j)]
[a r r a y ([-0.25000000+0.66143783j , -0.29289322+0.j ,
-1.00000000+0.j , -0.25000000+0.96824584j]),
a r r a y ([-0.25000000-0.66143783j , -1.70710678+0.j ,
-1.00000000+0.j , -0•25000000-0•96824584j ])]
6 . 4 . 2 用 autowrap()编译表达式
语言的函数,并编译为扩展模块以供P y t h o n 调用。
下面先将保存两个解表达式的列表转换为表示矩阵的M a tr ix 对 象 m a t r i x _r c x )t S, 然后调JIJ
a u to w ra p O 将m a tr ix jo o ts 转换为两个函数,注意这两个函数只能返[H丨实数解。通 过 b a c k e n d 参数
可以指定编译扩展模块的方式:
• ’
f2 p y '
: 默认值,使 用 f 2p y 将 F 〇
_ 语言程序包装成扩展模块。
• V y th o iV : 使 用 C y t h o n 将 C 语言程序包装成扩展模块。
为了让 a i i t o w r a p O 正常工作,需要通过 t e m p d ir 参数设置保存临时文件的路径,可以在该路
径下找到输出的源程序以及编译之后的扩展模块。可以通过 f l a g s 参数指定额外的编译命令,这
里通过- I 将 N um Py 头文件所在的路径添加进编译器的头文件搜索路径。
[[1. ], [[ 1.]>
[0.5]] [ 0.5]]
c o d e g e n 〇将表达式转换成源程序。它的第一个参数是一个二元元组或是一个包含多个二元
language="C",
prefix="quadratic_roots,,J
header=False)
print h_name
print * 40
print c_header
print
print c_name
print 40
print c一code
quadratic 一r o o t s •h
# ifndef P R O J E C T _ Q U A D R A T I C _ R O O T S _ H
#define PRODECT__QUADRATIC_ROOTS__H
sy m p y符
#endif
—号 运 算 好 帮 手
q u a d r a t i c _ r o o t s .c
#include <math.h>
6 . 4 . 3 使 用 cse〇分步输出表达式
通常符号运算的结果都是十分复杂的表达式,其中包含许多重复运算部分。使 用 CSe()可以
将表达式中重复的部分提取为分步运算。cse()的结果是一个由两个列表组成的元组,第一个列
表是临时变量以及与之对应的表达式,第二个列表是计算结果。
S y m py符
下面对一元二次方程的两个根的符号表达式提収公共表达式,得到两个列表 replacements
和 r e d u c e d _ e x p r s 。在 r e p l a c e m e n t s 中引入两个临吋符号x O 和 x 1 ,对 r e d u c e d _ e x p r s 中的临时符号
—号 运 算 好 帮 手
逐 步 使 r e p l a c e m e n t s 的表达式进行替换,就可以得到与 r o o t s 相同的表达式。由于根式部分使
用临时符号表示只需要运算一次,因此节省了运算时间。
replacements, reduced_exprs = cse(quadratic_roots)
%sympy_latex replacements
/ \ \ / *1
(x 〇
, 5 ), ^ - 4 a c + b 2)
(tmp〇
, ▲ ), (tmp” ^/-4ac + b2)
84
exec code
print code
def cse 一
quadratic 一roots(a, b. c ):
from math import sqrt
_ t m p 0 = 0.5/a
(3.41421356237, 0.585786437627)
由于在函数中使用 math.sqrt〇进行开方运算,因此不支持结果为复数的情况。可以通过
module 参数指定使用 cmath 模块进行计算:
import cmath
((2+2.449489742783178j ), (2-2.449489742783178j ))
6.5麵 运 纖 拟
刚体系统的运动方程。作为本章的最后一节,
我们用该模块计算如图所示系统的运动方程,
并进行数值模拟。在图中,滑动方块可以沿参照系/ 的 X 轴自由运动,小球与滑块使用无质量
连杆相连接,可以 A 由摆动。小球的的初始摆动角度为0u。我们希望计算小球释放之后的运动
轨迹。
Python 科学计算(第 2 版)
阁滑块中.摆系统的参照系示意图
6 . 5 . 1 推导系统的微分方程
首先从 sympy.physics.mechanics载 入 所 有 符 ^,
并使)I』
其中的 ReferenceFrame定义参照系/,
用 Point 定义参照点0 。最后调用 O .set_vel〇,设置点0 在参照系/ 中的运动速度为0。
http://www.pydy.org/
本 节 只 介 绍 m e c h a n i c s 模块最基本的用法,若读者对使用 S y m P y 求解多刚体系统感兴
趣 ,可 以 参 考 P y D y 扩展库。
q = dynamicsymbols("q")
u = d y n a m i c s ym b o l s ("u ")
ml = symbols("ml")
PI = Point('PI')
Pl.set_pos(0, q * I.x) # 点 P I 的位咒相对于点0 ,沿着参照系I 的X 轴偏移q
Pl.set_vel(I, u * I.x) # 点 P 1 在参照系I 中的速度为X 轴方向,大小为u
box = Particle('box', PI, ml) # 在点P 1 处放贾质m 为m l 的方块box
使 用 d y n a m i c s y m b o l s O 定义的符号足吋间的闲数:
%sympy_latex q, u
(q(t), u(t))
下面定义小球所在的参照系B ,
B 为 I 绕 Z 轴旋转0 而得,并设置 B 相对于 I 的角速度为 co,
角速度围绕 I 的 Z 轴正方叫旋转。角速度的正方丨(彳使用右手法则定义,即右手大拇指指丨⑷围绕
的轴,四指的方A 为正方向。
th = dynamicsymbols("theta")
w = dynamicsymbols("omega")
B = I.orientnewCB', 'Axis', [th, I.z]) # 将 I 围绕Z 轴旋转theta 得到参照系 B
B.set 一ang_vel(I, w * I.z) # 设置 B 的角速度
S ym P y符
1 , m 2 = s y m b o l s (" 1 ,m 2 ")
P2 = P l . l o c a t e n e w ( ,
P 2 ,, -1 * B.y) # P 2 相对于P 1 沿着B的Y 轴负方向偏移1
- 号运算好帮手
P2.v2pt_theory(Pl, I, B) # 使用二点理论设置P 2 在 I 中的速度
ball = P a r t i c l e ( ' b a i r , P2, m2) # 在 P2 处放置质量为 m2 的小球
到此为止,各个惯性参照系、坐标点、质点之间的关系已经确定。下而创建 KanesMethod
对象,使用它可以推导出系统的微分方程组。q j i i d 参数为系统中所有与位移相关的独立状态
列表,u j n d 参数为所有与速度相关的独立状态列表,而 kd_e q s 参数则是这些状态之间需要满
足的微分方程。在本例中,方块的位移 q 的导数为速度u ,细杆的旋转角度0 的导数为角速度〇
):
然后调用 kanes_equations〇方法推导微分方程。其中,particles为系统中所包含质点的列表,
fo rc e s 是系统所受外力的列表。每个作用力由作用点和矢量决定,这里定义两个质点上所受的
重力。
和等于0。
status = M a t r i x ( [ [ q ] ,
[th] ,
[u] ,
[w]])
d i s p l a y .M a t h (l a t e x (k a n e .m a s s _ m a t ri x _ f u l l ) + latex(status.diff()) +
" = " + l a t e x (kane.forcing_full))
sy m p y符
o
q
rv
ld
dt
—号 运 算 好 帮 手
0 □⑴
o
1 0 0
rv
0
ld
0 1 0 0 dt
〇)〇
:)
lm2cos(0(t))
o
ld
0 0 m x 4- m 2 lm2a)2(t)sin(0(t))
u
/lx
dt
dt
rv.
3
6 . 5 . 2 将符号表达式转换为程序
diff_status = k a n e .m a s s _ m a t r i x _ f u l l .i n v () * k a n e .forcing_full
t e m p d i r = r " .\tmp_mechanics") @
S ym P y符
from scipy.integrate import odeint
- 号运算好帮手
t = np.linspace(0, 10, 500)
fig, (axl, ax 2 ) = p i t . s u b p lo t s (2 , 1 )
0 2 4 6 8 10
图6 4 使用odeint〇计寫的运动轨迹
6 . 5 . 3 动画演示
可以通过 inatplotlib的动画制作功能,直观地显示系统的运动状况,效果如图卜5所示。关
89
Python科学计算 (第 2 版)
0.S
阁动画演示效果
ax.add j 3 atch(circle)
def animate(i):
xl, yl = q[i], 0
1 = 1.0
x 2 , y 2 = l*sin(th[i]) + xl, -l*cos(th[i]) + yl
rect.set_xy((xl-w*0.5, yl))
I i n e . s e t _ d a t a ( [ x l , x 2 ] , [ y l , y 2 ])
circle.center = x 2 , y 2
return anim
下而使用%1^吨31〇〖
1化 命令将 matplotlib的后台绘阁库改为qt,以便M 外 JT•启窗口来显示动画
演示:
%gui qt
%matplotlib qt
anim = animate一system(t, res)
sy m p y符
—号 运 算 好 帮 手
解7 亭
7.1 Traits类型入门
7 . 1 . 1 什么是 Traits 属性
下面我们通过一个简单的实例演示Tm it 属性的功能:
class C i r c l e ( H a s T r a i t s ) : O
colon = Color ©
Python科学计算(第2 版)
c = Circle()
Circle.color #Circle 类没有 color 屈性
<ipython-input-4-8335a9908186> in <module>()
1 c = Circle()
Traits & T ra its u轻
print c.color
print c.color.getRgb()
c.color = "red"
print c.color.getRgb()
c.color = 0 x 0 0 ff 0 0
print c.color.getRgb()
print c.color.getRgb()
try:
c.colon = 0 . 5
(255, 0, 0, 255)
The 'c o l o r ' trait of a Circle instance must be a string of the form (r,g,b) or (r,g 九 a)
where r, g, b, and a are integers from 0 to 255, a QColor instance, a Qt.GlobalColor., an integer
which in hex is of the form 0XRRGGBB, a string of the form #RGB, #RRGGBB, #RRRGGGBBB or
由上而的运行结果可知,我们可以将’
red’
、OxOOmX)和(0, 255, 255)等值赋给 color 属性,它
们都被正确地转换为QColoi•类型的值。而当赋值为0.5时抛出 TmitError异常,并且JS 示了一条
很详细的出错信息来说明color 属性能支持的所有值。最后看一个很酷的功能:
c .c onfigure_traits()
T r a it s & T r a i t s u 轻
执 行 configure_ traits()之后,会山现如图7-1所示的对i S 框界而以供我们修改 color 属性,任
意选择一•种颜色并单击O K 按钮,configure_traits()返 阿 True ,而 color 屈性已经变为我们通过界
面所选择的颜色了:
T 松制作图形界面
如 果 在 Notebook 中运行 c .configure_traits〇,它会立即返回 False , 而不会等待对话框关
闭◊ 当程序单独运行时,configure_traits()会等待界面关闭,并根据用户单击的按钮返
回 True 或 False。
c.color.getRgb()
^ FHir prop#»m>^
Cofcr: TO2(2SS)
Gchcel
点击弹出
颜色选择框
图7 - 1 自动生成的修改颜色屈性的对话框
Python科学计算 (第 2 版)
7.1.2 Trait属性的功能
Traits 库为对象的属性增加了类型定义的功能,此外还提供了如下额外功能:
• 初 始 化 :每个 Tm it 属性都有自己的默认值。
•验 证 : T m i t 属性都有明确的类型定义,只有满足定义的值才能赋值给屈性。
• 代 理 :T m i t 属性值可以代理给其他对象的属性。
• 监 听 :T m i t 属性值发生变化吋,可以运行事先指定的函数。
• 可 视 化 :拥 有 Trait 屈性的对象可以很方便地生成编辑T m i t 属性的界面。
下面的例子展示了 Trait 屈性的上述功能:
# 监听:当a g e 属性的值被修改时,下面的函数将被运行
def _age_changed ( self, old, new ): O
p = P a r e n t ()
c = Child()
9(
©fIJ Instance类型定义 Child 对象的 fa出er 属性楚 Pm‘
ent 类的实例,
默认值为 None 。
如果 Parent
类 在 Child 类之后定义,可以j |j 字符串表示类: father = Instance('Parent’
)。
© 通 过 D elegate 类 型 为 C h ild 对象创建了一个代理属性 last_ name。它 使 c .last_n a m e 和
c .father.last_nam e 始终拥有相R )的值。但是由于还没有设置对象 c 的 father 属性,因此无法正确
获得对象 c 的 last_ name 属性:
c •last 一name
р . last_name = "ZHANG"
T 松制作图形界面
print c .last_name
Zhang
ZHANG
© 当对象 c 的 a g e 屈性值发生变化时将调用听函数 _ a g e _ c h a n g e d ( ) :
с. age = 4
c .c o n f i g u r e _ t r a i t s ( )
从自动生成的界面可以看到,
属性按照英文名排序,垂直排为一列。由于 father 属性是 Parent
类的对象,因此界面中以一个按钮表示它,单击此按钮将会弹出一个如图7-2( 右)所示的对话框
用于编辑 father 属性所对应的对象。
单 击 Father按 钮
© Edit properties
图7-2 为Child对象自动生成的厲性修改对话框(左),单击Father按钮弹出编辑Parent对象的对话框(右)
Python科学计算 (第 2 版)
c.print_traits()
age: 4
father: < _ main_ .Parent object at 0x05D9CC90>
last_name: 'ZHANG'
c.get()
{'age1: 4j 'father': <__main_.Parent at 0x5d9cc90>, 'last^ame': 'ZHANG'}
c.set(age = 6)
Age changed from 4 to 6
<__main_ .Child at 0x5d9c600>
c2 = Child(father=p, age=3)
Age changed from 0 to 3
c.trait("age")
<tnaits.traits.CTnait at 0x9e23870>
p.trait("last_name").default
'Zhang'
9:
try:
c . t r a i t ("father").validate(c, "father", 2 )
except TraitError as ex:
print ex
The 'father' trait of a Child instance must be a Parent or None, but a value of 2 <type 'int'>
was specified.
c.trait("father").validate(c, "father", p)
T 松制作图形界面
CTrait 对象楚迩接 Trait 属性和 Trait 类别的纽带,通 过 CTm it 对象的 trait_type 属性可以获得
定义 Trait 属性时所使用的 Trait类型:
print c .t r a i t ("father").trait_type
7.1.3 T rait类型对象
class P e r s on(HasTraits):
age = Int(30)
weight = Float
99
Python科学计算 (第 2 版)
pi = P e r s o n ()
p2 = P e r s o n ()
print pl.traitC'age") is p 2 .trait("age")
print p i . t rait("weightM).trait_type is p 2 .trait("weight" ) .trait_type
True
True
Traits & T ra its u轻
也「
if 以先单独创連一个Trait 类型对象,然后j |j 它定义多个 Trait 属性:
class Qu a d r a t i c( H a s T r a i t s ):
c 2 = coefficient
cl = coefficient
c 0 = coefficient
class Q u a d r a t i c 2 ( H a s T r a i t s ) :
c2 = R ange(-1.0 ,1 . 0 ,0.0)
cl = Range(-1.0, 1.0, 0.0)
c0 = Range(-1.0, 1.0, 0.0)
q = Q u a d r a t i c ()
True
True
而 Quadratic2 对象的屈性.所对应的类型对象则楚不同的对象:
q2 = Quadratic2()
False
7.1.4 T rait的元数据
一个实例解释什么是元数据属性。
class M e t a d a t a T e s t ( H a s T r a i t s ) :
# 元素为I n t 的列表
list = List(Int) O
T 松制作图形界面
test = MetadataTest()
下面杳看所有的 Tm it 属性:
test.traits()
•trait_added': 〈
traits.traits.CTrait at 0x4fc2c38>,
•trait 一
m o d i f i e d •: <traits.traits.CTrait at 0x4fc2be0>}
通 过 traits〇方法可以得到一个包含所有C T r a i t 对象的字典。
C T r a i t 对象用于描述 T m i t 屈性,
• 内 部 属 性 :这些属性是 C T m i t 对象自带的,只读不能写。
•识 别 属 性 :这些属性可以自由设置,它们可以改变 T m i t 属性的一些行为。
•用 户 属 性 :用户自己添加的属性,需要自己编写程序来使用它们。
下而是一些内部元数据属性,可以读取它们的值,但不能修改:
Python科学计算 (第 2 版)
99
test my info
© 属 性 s 的默认值为’
’
test' 并且它有一个识别元数据属性label。在生成界面时,使用它作
为编辑器的标签。
为了在界而中使用中文,
需要使用 1 )11丨〇)也字符$。
如果运行 teSt.C〇
nfigw i _ tmits()
来显示图形界而,可以看到该厲性对应的标签文本为“字符串”。
print test.trait("s").label
字符串
test.trait("a").array
True
〇屈 性 list 是一个元素类型为整数的列表。通 过 inner_traits元数据屈性可以获得与列表元素
对应的 CTrait 属性:
print test.trait("list")
print test.trait("list").trait 一type
72 Trait 类型
T 松制作图形界面
7 . 2 . 1 预定义的 T rait类型
表 7-1 预定义的Trait类型
下面的例子比较这两种类
Python科学计算 (第 2 版)
class P e r s on(HasTraits):
cweight = C F l o a t (50.0)
weight = F l o a t (50.0)
p = P e r s o n ()
p.cweight = "90"
print p.cweight
try:
p •weight = "90"
90.0
The 'weight' trait of a Person instance must be a float, but a value of '90' <type 's t r ' >
was specified.
表 7-2 — 些常用的预定义Trait类型
类型名 参数 说明
Any Any( |value=None, **metadata]) 任何对象
Array Array( [dtype=None,
shape=None,
value=None, NumPy数姐
CArray typccode=None, **metadata])
Button Button( [lal*)cl="", imagc=Nonc, stylc="button", 按钮类型,通常用于触发事件,参数用
cHicntatioiWvcrtical", \vidlli_padding=7, liciglit_padding= 于描述界而中的按钮的样式
5, **metadata])
04
(续表)
类型名 参数 说明
Diet Dict([key_trait=None, vaIue_Dnit=None, valiic=None, 字典对象,为了方便使用,在 Traits庳
items=True, **metadata]) 中还预定义了一些键的类型为字符市
的 字 典 类 塑 , 例 如 DictStrAny,
DictSlrBool 等
Directory Directory( [value=”",
auto一set=False, entries=10, 表示架个目录的路径的字符屯
exists=False, **metadata])
Either Eilher( vall*[, *val2,valN, **metadata]) 多个 Trait类型的M合,例 如 Either(Str,
Float)表示定义的屈性可以足字符串或
浮点数
Enum Enum( values*[,料*metadata】) 枚举数据,其值可以是候选值中的任意
一个
Event Evcnt( [tmit=None,
**mctadata]) 触发事件用的对象
T 松制作图形界面
读者可以查看各个 T m it 类型的文档以了解其:具体州法。下面以枚举类型为例,介 绍 Tmit
类型的使用方法。使 用 Enum 可以定义枚举类型,在 E num 的定义中给出所有的候选值,这些
值必须是 Python 的简单数据类型,例如字符串、整数、浮点数等,候选值的类型可以不一样。
可以直接将候选值作为参数,或者将其放在列表中,第一个值为缺省值:
from traits.api import Enum, List
class I t e m s ( H a sT r a i t s ) :
count = Enum(None, Q, 1, 2 , 3, "many")
# 或者:
# count = Enum([None, 0, 1 , 2 , "many"])
下面是运行结果:
item = I t e m s ()
item.count = 2
item.count = "many"
try:
item.count = 5
except TraitError as ex:
print ex
05
Python科学计算(第2 版)
class I t e m s ( H a sT r a i t s ) :
count_list = List([None, 0, 1, 2) 3, "many"])
count = Enum(values="count_list")
item = I t e m s ()
try:
item.count = 5 # 由于候选值列表中没有5 ,因此赋值失败
except TraitError as ex:
print ex
Traits & T ra its u轻
i t e m .c o u n t _ l i s t .a p p e n d (5)
item.count = 5 #li:丨于候选值列表中有5 ,因此赋值成功
item.count
The 'c o u n t ' trait of an Items instance must be None or 0 or 1 or 2 or 3 or 'many、 but a
value of 5 <type 'int'> was specified.
T 松制作图形界面
7.2.2 Property 属性
class R e c t a n g l e( H a s T r a i t s ) :
width = F l o a t (1.0)
height = F l o a t (2.0)
O 在 Rectangle 类「
I1,使 用 PropertyO定义了一个 area 屈性。与 Python 的标准 property屈性小
同,它根据屈性名直接决定屈性所对应的方法。当 读 収 _ 屈性值时,得到的是© _ ^ t_ area〇
的返回值;而 当 设 置 a re a 属性值时,所设置的值将传递给_561_〇^()。由于在本例中没有定义
_set_area(),因此 area 属性楚只读的。此外,通 过 depends_o n 参数可以指定 Property属性的依赖
关系。本例中,当 Rectangle对象的 width 和 height属性值发生变化时斋耍重新计算area 属性。
© _ get_area〇f f i @ cached_propeity 修饰,这样_get_area〇的返III丨值将被缓存,除 非 area 属性所
依赖的 width 和 height属性值发生变化,否则将一直使用缓存值,而不会每次调用_g et_area〇。
下而看看实际的运行效果:
r = Rectangle()
print r.area # 第一次取得a r e a ,需要进行运算
r.width = 10
Traits&T1
print r.area # 修改w i d t h 之后,取得a r e a ,需要进行计算
print r.area # w i d t h 和 h e i g h t 都没有发牛.变化,因此直接返回缓存值,没有iE 新计算
recalculating
2.0
T轻 松 制 作
recalculating
20.0
1
20.0
In 【
121t
各M MOO
Hr^Kl 40
In (13Jt r . « d l t . t r « l t » ( ) 3400
O u t [ n ] t < tr « iti« il.u l.U S at
r * < a lc y la tln ( OK
r « ( 4 lc u l« t ln t
407
Python科学计算(第2 版)
由于我们弹出了两个界面,因此有两个需要通知的对象。如果再运行一次 r.ediUraitsO, 这
个列表将有 3 个元素。
Traits & T ra its u轻
7.2.3 Trait属性监听
了这两种监听方式:
# 当age 属性的值被修改时,下面的函数将被运行
def _age一changed ( self, old, new ): O
print "%s.age changed: form %s to %s" % (self, old, new)
h.on_trait_change(log_trait_changed, name="doing") O
O 当 C h ild 对 象 的 a g e 属性值发生变化时,对应的静态监听函数_8@6_(:
11〇1职(1()将被调j |J。
© _ anytrait_changed()楚一个特殊的静态监听闲数,任 何 Trait 属性发生变化时都会调用此闲数。
© 通过调用 h.on_trait_change(),动态地将©普通闲数 log_tmit_changed〇和对象 h 的 doing 属
性联系起来。当 doing 属性改变时,log^ lrait_changed〇将被调用。
下而分别改变 h 和 k 的属性:
h.doing = "sleeping"
k.doing = "playing"
T 松制作图形界面
HaiYue<8b823f0>.age changed: form 9 to 10
anytrait changed: HaiYue<8b823f0>.doing from to sleeping
log: HaiYue<8b823f0>.doing changed from to sleeping
anytrait changed: KaiWen<8b823c0>.doing from to playing
静态监听函数的参数有如下几利1形式:
一age 一changed(self)
一age 一changed(self, new)
而动态监听函数的参数有如下儿种形式:
observer。
observer^ new)
observen(name, new)
observer(obj, name, new)
09
Python科学计算(第2 版)
@on 一
trait 一c h a n g e ( names )
def any_method_name( s e l f , … ):
• • •
class H a s N a m e ( Ha s T r a i t s ) :
name = Str()
T 松制作图形界面
def — str_ ( s e l f ) :
return "<%s %s>" % (self._ class_ . 一name_ , self .name)
class I n n e n ( H a sN a m e ) :
x = Int
y = Int
class D e m o ( H a s N a m e ) :
x = Int
y = Int
z = Int(monitor=l) # 有元数据屈性 monitor 的 Int
inner = Instance(Inner)
alist = List(Int)
testl = Str()
test2 = Str()
def _ i n n e r _ d e f a u l t ( s e l f ) :
return Inner(name="innerl")
d = Demo(name="demo")
d.x = 10 与 x 匹配
#
d.y = 2 0 # 与y 匹配
d.inner.x = 1 # 与 i n n e r . [ x ,y ]匹配
d.inner.y = 2 # 与 i n n e r . [ x ,y ]匹配
<Demo demo> x 0 10
<Demo demo> y 0 20
<Inner innerl> y 0 2
T 松制作图形界面
<Demo demo> z 0 30
class Point(H a sT r a i t s ): O
x = F l o a t (0.0)
y = F l o a t (0.0)
Python科学计算 (第 2 版)
updated = Event
@on_trait_change( __x,y__ )
def _updated_fired(self): €)
self.redraw()
def r e d r a w ( s el f ) : O
改各种属性时的事件触发情况:
Traits & T ra its u轻
p = P o i n t ()
p.x = 1
p.y = 1
p.updated = True
p. updated = 0 # 给 u p d a t e d 赋任何值都能触发
redraw at 1 .0 , 0 .0
redraw at 1 .0 , 1 .0
redraw at 1 .0 , 1 .0
redraw at 1 .0 , 1 .0
7 . 2 . 5 动态添加 Trait属性
a = HasTraits()
a.add_trait("x", F l o a t (3.0))
a.x
3.0
b.a = a
时改变了。
print a.x
3.0
10.0
T 松制作图形界面
a = A()
a.x = 3
a.y = "string"
a . t r a i t s ()
只不过它们的类型为Python, 因此它们能够接收任何类型的对象,起不到校验的作用:
<traits.tnait_types.Python at 0x39399b0>
7.3 TraitsUI 入门
节需要开发者自己配置,例如控件的屈性、位置以及雜件响应等。
在开发科学计算程序时,通常希望快速实现一个够用的界面,让用户能够交互式地处理数
掘 ,而又不希望在界而制作上花费过多的精力。以 Traits 库 为 _础 、以 M V C 模式为设计思想的
T r a i t s U I 库就是实现这一理想的最佳方案。
序的修改和扩展工作,并且使程序的各个部分能够充分被重复利用。
• Model(模型):程序中存储数据以及对数据进行处理的部分。
• View (视图):程序的界面部分,实现数据的显示。
• Controller(控制器):起到视图和模型之间的组织作用,控制程序的流程,例如将界面操
作转换为对模型的处理。
7 . 3 . 1 默认界面
class Em p l o y e e (H a s T r a i t s ) :
name = Str
department = Str
制
salary = Int
bonus = Int
作
E m p l o y e e ().c onfigure_traits()
此程序创建一个 E m p l o y e e 类的对象,
然后调用 configure_lraits〇来显示出如阁 7~4( 左)所示的
图
默认界面:
在此自动生成的界面中,所有的属性都采用文本框编辑,并且每个文本框的前面都有一个
形
文字标签,上面的文字根据Tm it 属性名自动生成:第一个字母变为大写,所有的下划线变为空
格。对话框的最下面提供了 0 K 和 Cancel 按钮以确定或収消对Trait 屈性的修改。
界
由于salaiy 屈性定义为Int类型,
当输入不能转换为整数时,
输入框将以红色背景表示错误,
并 且 0 K 按钮变成无效,如图7~4(右)所示。
面
414
Edit properties ^ Edit propertile s
Bonus: 100001 Bonus: 10000
i ______________________________
Name: Name:
OK Cancel OK Cancel
没有写一行界而相关的代码,就能得到一个够实用的界而,应该还是很令人满意的。为了
手工控制界面的设计和布局,就需要添加自己的代码了。
7 . 3 . 2 用 V iew 定义界面
下面是用视图对象定义界面的完整程序,图 7-5显示了界面截图。
T 松制作图形界面
柃礼与
妊各张m
1*5 10000
X iL 2000
丨
妇7 - 5 使用视阁对象描述界而
class Employe e (H a s T r a i t s ) :
name = Str
department = Str
salary = Int
bonus = Int
view = V i e w ( ©
p = Em p l o y e e ()
p .c o n f i gure_traits()
gl = d e p a r t m e n t l a b e l = u " 部门" ,t o 〇
rtip=u" 在哪个部门干活••), ©
Item(.name', label=u._姓名_•)]
g 2 = [Item('salary’,label=u" 工资")
,
I t e m ( ’b o n u s ’,label=u" 奖金_•)]
class Employe e (H a s T r a i t s ) :
name = Str
department = Str
salary = Int
bonus = Int
traits_view = View( ©
Group(*gl, label = u •个 人 信 息 show_border = True),
global 一
view = View( 0
Group(*gl, label = u • 个 人 信 息 show_border = True),
Group(*g2, label = u •收入、 show_border = True),
p = Emp l o y e e ()
# 使用内部视图traits_view
p.edit 一t r a i t s () O
O 从 T r a i t s U I 库 载 入 G r o u p 类 ,用 G r o u p 对象可以对界面中的编樹器分组。为了后续足义
T r a it s & T r a i t s u 轻
视图对象的程序更加简沾, © 程序中定义了两个全局列表g l 和 g 2 , 它们的元素都是 I t e m 对象。
© O 在 E m p l o y e e 类内部用 V i e w ( ) 足 义 ,两个视图对象:traits_view 和 a n o t h e r _ v i e w 。 而 ©足
义了一个全局的视图对象:gl〇b a l _ v i e w 。在定义视图对象时,用 G r o u p 对 I t e m 对象分组。
值得注意的是, E m p l o y e e 类中定义的两个视图对象既不是类的屈性, 也不是实例的屈性。
T 松制作图形界面
这 些 内 部 视 图 对 象 会 放 到 E m p l o y e e 类的_ view_traits _ 属 性 中 。_ view_traits _ 属性是一个
V i e w E l e m e n t s 对象,它 的 c o n t e n t 属性是保存所有内部视图的字典:
[•another 一
v i e w 、 ■traits 一
v i e w ']
@ 当调用 edit_traits〇以显示界面时,缺省使用模型类内部定义的缺省视图对象traits_view 生
成界面。也可以使用 v i e w 参数指定显示界面时所使j|j的内部视图对象的名称:
# 使用内部视图another_view
p .edit_traits(view="another_view")
还可以直接将视图对象传递给 v ie w 参数,这样可以使在模型类之外定义的视图对象来
生成界面:
# 使用外部视图viewl
p.edit_traits(view=global 一
view)
图 7-6显示了上面三种视图对象所生成的界面,每个视图的窗口标题都不同。由于这三个
界面对应于同一个模型对象,因此无论在哪个窗口中修改模型对象的属性,另外两个窗口的内
容也会同步更新。
Python科学计算 (第 2 版)
图7 ^ 6 使用外部视图和内部视图定义界面
不进入后台界面库的消息循环,因此如果直接运行只调用 e d i U r a i t s O 的程序,界面将在显示之
后立即关闭,程序的运行也随之结朿。而 在 c 〇nfigure_traits〇中将进入消息循环,直到用户关闭
所有窗口。因此通常主界面窗口或模式对话框使用configure_traits()M 示,而无模式窗口或对话
框则用 edit_traits()M 示。在 I P y t h o n N o t e b o o k 111,可以通S % g u i q t 或%^ 11丨w x 命令启动界面消息
循环,因此无论使)1挪个方法显示界面,界面都不会阻塞N o t e b o o k 内核的运行。
T r a it s & T r a i t s u 轻
用 T m i t s U I 库创建的界面可以选择后台界面库,目前支持的有 q t 4 和 w x 两种。在启动
程序时添力D-toolkit q t 4 或 -toolkit w x 以选择使用何种界面库生成界面。本书中全邱使用
Qt 作为后台界面库。
T 松制作图形界面
引用模型对象的属性,而是通过属性名与模型对象相关联。这样模型和视图之间的耦合性将会
很弱,只需要属性名匹配,同一个视图对象可以运用到不同的模型对象之上。
有时候我们希望模型类知道如何显示自己,这时可以在模型类内部定义视图。在模型类中
定义的视图可以被其派生类继承,因此派生类能使用父类的视图。在 调 用 configure_tmits 〇时如
果不设置 v^ w 参数,将使用模型对象内部的缺宵视图对象生成界面。如果在模型类中定义了多
个视图对象,则缺省使用名为 toits_V i e w 的视阁对象。
2.多模型视图
在上节的例子中,一个模型可以对应多个视图。同样,使用一个视阁可以将多个模型对象
的数据显示在一个界面窗口中。下面是用一个视图对象同时显示多个模型对象的例子,程序的
运行界面如图7-7所示:
class Employe e (H a s T r a i t s ) :
name = Str
department = Str
salary = Int
bonus = Int
comp_view = View( O
Group(
Group(
I t e m ( ’p i . d e p a r t m e n t •, l a b e l = u "部门
Item( _p i •name •, label=u" 姓名••)
,
I t e m ( ' p i . s a l a r y l a b e l = u " 工资"),
r t e m ( _ p l . b o n u s ’,label=u" 奖金••)
,
show_border=T rue
),
Group(
Item( ’p 2 •department •, label=u" 部门")
,
I t e m ( ’p2.name', label=u" 姓名" ),
Item( •p 2 •salary •, label=u" 工资•■),
T 松制作图形界面
)>
t i tle = u" 员工对比"
r
© fliw ik
S O 男发1 部门讷嘗
妓各张三
xw*. 工 坶 咖
焚金* wo 400
阁7 - 7 用一个视图同吋显示多个校型对象
〇对于前丨fll的模型类 Employee,
创連复合视图对象comp_view ,
它能同时遥示两个Employee
对象。注 意 Item 对象的第一个参数不是简单的模型对象的属性名,它同时设置了 Item 对象的
两个属性:object 和 name。例如参数’
'p l .department”
将 设 置 Item 对 象 的 object 属性为”
p l ",设置
name 属性为'’
department''。object 属性告诉 Item 对象如何找到模型对象,而 name 属性则告诉 Item
对象如何找到模型对象中与之对应的属性。
419
Python科学计算 (第 2 版)
如果读者认为这种写法有些取巧,也可以直接调用视图对象的 ui()方法来显示界而,它的
参数就是界而所要显示的模型对象。由 于 ui()和 editjraitsO—样,不会启动界而库的消息循环,
因此需要在运行 ui()之后添加开始消息循环的代码。下而的消息循环代码支持所有的后台界
面库:
GUI() . s t ar t _ e v e n t _ l o o p 〇 # 开始后台界而库的消息循环
Traits & T ra its u轻
3. Group 对象
View(
Group(
G r o u p (… )
,
G r o u p (… )
,
orientation = 'h o r i z o n t a l '
layout = 'split'
orientation = 'horizontal'
H S p l i t 对象将它所包括的内容按照水平方向排列,并且在两块区域之间添加一个可调整位
置的分隔条,使 用 H S p l i t 和下面的代码等价:
Group( … , layout = 'split', orientation = 'horizontal')
下面的程序演示了 4 种不 N 的界面分组方式,效果如图7-8所示。
from traitsui.api import View, Item, Group, VGrid^ VGroup^ HSplit^ VSplit
class Simpl e E m pl o y e e ( H a s T r a i t s ) :
last_name = Str
department = Str
employee_number = Str
salary = Int
bonus = Int
Iten^names'firstjiame'labehirg")]
T 松制作图形界面
items2 = [Item(name = 'salary' , label=u" 工 资 " ),
viewl = View(
resizable = True
view2 = View(
VGroup(
view3 = View(
HSplit(
Python科学计算(第 2 版)
),
resizable = True, width = 400, height = 150, title = u"水 平 分 组 调 节 栏 )
view4 = View(
VSplit(
VGroup(*itemsl, show_border = True, scrollable = True),
VGroup(*items2, show_border = True, scrollable = True),
sam = SimpleEmployee()
Traits & T ra its u轻
sam•configure一
traits(view=viewl)
sam•configure一traits(view=view2)
sam.configure_traits(view=view3)
sam.configure_traits(view=view4)
T 松制作图形界面
阌 7-8 在 界 而 屮 用 Group对 象 进 行 分 组
42
表7 - 彡
ro u p 纟生类及对应属性的设置
派注 参数
H up ontal 水平排列
H N zontal、lay( :’flow’
、 水平排列,当 超 过 G
a showjabels 屈性 2
编辑器的标签文
H t ontal\ layoi ;plit' 水平分隔,中间
T. id ontal'、 layoi abbcd' 分标签页显示
V up 垂直排列
V n :al\ layout= v’
、 * 直排列,当超过 /
a
V I :al\ layout= 1’
、 _ 直排列,可折
g
XT. it、show」 ;
ls 、colum ns 等属性之夕卜,
轻松制作图形界面
其介 丨属1 〇 n Item —样,读者可以在 Group 类的涡
细 ij 1〇
:而: dsible_when 和 enabled_when 属性的用
对1 :界j 5有效,效果如图7-9所示。
Clc Shape(HasTraits):
iape_type = Enum("r tangle .、 "circle")
iitable = Bool
, y, w, h, r = [In1
Lew = V i e w (
VGroup(
HGroup(Item< iape_type"), Item("editable")),
VGroup(Itefn< ItemCY'), ItemCw"), I t e m C h ' 1),
visible. 2n "s h a p e _ t y p e = = 'r e c t a n g l e '", enable* hen
VGr )(Item("x"), I t e m C ) , Item("r"),
Python 科学计算(第 2 版)
visible 一
when="shape 一
t y p e = = ■c i r c l e ' e n a bled_when="editable ")y
) , resizable = True)
shape = Shape()
s h a p e •configure 一t r a i t s ()
时,所有编辑器都变成无效。
图7 - 9 演示visible_when和 enabled_when属性的用法
4.配置视图
表7-4 V i e w 对象的显示类型
类型 说明
modal 模式窗口,
非即时更新
live 非模式锐口,即时更新
livemalal 模式锐口,即时更新
nonmodal 非模式惊口,非即时更新
wizard 向导类型
panel, subpanel 嵌入到其他窗口中的面板,即时更新,非模式
其中'modal'、’
live'、’
livemodal’
、’nonmodal’
这 4 利唉型的 V ie w 对象都采j |J窗 U 界面显示其内
容。所谓模式窗 U , 是指在窗 U 关闭之前,程序中的艽他窗LI都不能被激活。而即时更新则是
指当窗口中的编辑器内容改变时,会立即反映到编辑器所对应的模型对象的属性值。非即时更
新的窗口则会复制模型对象,所有的改变在副本上进行,只有当用户单击0 K 或 A p p ly 按钮确
定修改时,才会修改原始模型对象的属性。
\vizard'由一系列特定的丨(彳导窗口组成,属于模式窗口,并且即时更新数据。
'panel'和’
subpanel'则楚嵌入到窗口中的面板,’
panel’
可以拥有自己的命令按钮,而’
subpanel'
则没有命令按钮。
在对话框中经常可以看到O K 、Cancel、A p p ly 之类的按钮,它们被称为命令按钮,完成所
有对话框都需要的一些共同操作。在 Tm itsUI 中,这些按钮可以通过 V ie w 对 象 的 buttons属性
设置,其值为要显示按钮的列表。
TraitsUI 中定义 J’UndoButton、ApplyButton 、RevertButton、OKButton、 CancelButton 等 6 个
标准的命令按钮,每个按钮对应••个名字,在指定 buttons屈性时,可以使用按钮的类名或对应
的名字。与按钮类对应的名字就是类名除去Button, 例 如 UndoButton对应"Undo'
traitsui.m enu 中还预定义了一些命令按钮列表,以方便川户直接使j Ij整套按钮:
7 . 4 用 Handler控制界面和模型
当界丨M事件引起控制器的方法被调用时,这 个 u n n fo 对象会作为参数传递给被调用的方法。
TraitsUI提供了三利1指定控制器的方法:
•将控制器作为视图的属性:使州视图对象的 handler属性指定控制器对象,此视图所产
生的界面都使用它进行事件处理。
•显 示 界 面 时 设 置 :调 用 edit_tmits〇、configure_tmits()或 ui()等方法显示界面时,将控制器
对象传递给 handler参数。它比视图的 handler属性有更高的优先级。
•将视阁作为控制器的一部分进行定义。
7 . 4 . 1 用 Handler处理事件
当S 示某个视图时,将会按照下面的顺序执行控制器中的方法:
(1) 创建一个 u n n fo 对象。
(2) 运行控制器的 init_info〇方法。
(3) 创建一个 U I 对象来表示实际的窗P 。
⑷运行控制器的 init()方法。
Traits & T ra its u轻
⑶运行控制器的 position〇方法。
(6)显示实际的窗口。
除了上而的 init_info〇、init()、positionO之外,当用户操作界而时,会运行如下方法:
• apply(): 用户单击窗口中的 A p p ly 按钮,模型对象的数椐更新之后。
T 松制作图形界面
• cb se 〇:用户关闭窗口,在窗口关闭之前。
• closed():命口关闭之后。
• revert():用户单击了 Revert 或 Cancel 按钮。
• setattrO: 用户通过界面修改了模型对象的某个Tm it 属性。
• show_ help〇:用户单击了窗口中的 H elp 按钮。
下面通过一个实例演示上述各个方法的用法:
class Employe e (H a s T r a i t s ) :
name = Str
department = Str
salary = Int
bonus = Int
def _ d e p a r t m en t _ c h a n g e d ( s e l f ) : O
viewl = View(
kind = "modal", ©
buttons = ModalButtons
class E m p l o y e e Ha n dler(Handler): O
T 松制作图形界面
super( EmployeeHandler, self).init 一info(info)
def position(self, i n f o ) :
super(EmployeeHandler, s e l f ) .position(info)
def apply(self, i n f o ) :
supen(EmployeeHandler, self).apply(info)
zhang = Employee(name=,,Zhang")
print "zhang is ", zhang
zhang.configure_traits(view=viewl, handler=EmployeeHandler() ) 0
zhang is < Employee at 0x91efcf0>
True
图 7 - 1 0 带标准按钮的模式对话框
42
在对话框的“部门”文本输入框中输入“开发部门”,然后单fir? A p p ly 按钮,最后单击
0 K 按钮关闭对话框。程序在命令行窗口中输出控制器的各个方法的调叫情况。
首先,对 象 zhang 所表示对象的地址为Ox91efcfO:
调用 configure_tmits()之后,在窗口显示之前,运行了 init_info〇、init〇、position()这三个方法:
接 下 来 输 入 “开发部门”,每次文本输入框内的内容发生改变时,都会修改模型对象的
depmtment属性,从 而 调 用 模 型 对 象 接 着 会 调 j |j 控 制 器 的 setattr〇。因
此控制器的 setattrO是在模型数据更新之后被调用的。
setattrO的调用参数如下:
T 松制作图形界面
s e t a t t r (s e l f , info, obj, name, value)
为 “开发部门”了:
应的模型对象和视图对象所产生的界面。但是有时我们希望通过控制器的屈性访问它们。
TraitsUI提供了从 Handler继承的 Controller类,它W 两 个 Trait 属性:model和 info ,分別保存模
型对象和 U lln fo 对象。
在下面的程序中,模型和视图采用上节的定义,为了在显示窗口之后,在 Notebook 中继续
运行命令,这 里 将 view Lkind 属性修改为非模式窗口。创建模型对象、控制器以及显示界面的
代码如下:
viewl.kind = "nonmodal"
zhang = Employee(name="Zhang")
c = Controller(zhang)
c .edit_t ra i t s (v i e w = v i e w l )
的属性编辑窗口。
由于无论是控制器类、视图类还是模型类,最终都从 HasTmits类继承,因此可以调用get()
来快速查看其内容:
T 松制作图形界面
c.get()
c.info.get()
c.info.ui.get()
{ '_active_group': 0 ,
•一checked': [ ] ,
•一context': { 'controller': <traitsui.handler.Controller at 0 x 5 6 6 5 8 7 0 、
'handler': <traitsui.handler.Controller at 0x5665870>j
c.info 是一个 im n fo 对象,而 U llnfo 对象中最重要的内容就是 U I 对•象c .in fo.u io 在 U I 对•象
中保存了叫户界面中的各利1信思。对 U I 对象的详细介绍已超出了本书的范围,请感兴趣的读
者自行查看源代码。下面简要地查看 U I 对象的几个属性:
ui = c.info.ui
u i .context
ui.control # u i 对象所表示的实际界而控件
<traitsui.qt4.ui_base._StickyDialog at 0x5658780>
ui.view
( Group(
• • •
T 松制作图形界面
ui._editors
[<traitsui.qt4.text_editor.SimpleEditor at 0x5abe480>^
<traitsui.qt4.text_editor.SimpleEditon at 0x5b00510>,
• • •
7 . 4 . 3 响应 Trait属性的事件
extended_traitname_changed(self, info)
class M y H a n d l e r ( H a n d l e r ) :
def setattr(self, info, object^ name, v a l u e ) : O
H a n d l e r .s e t a t t r (s e l f , info, object, name, value)
info.object.updated = True ©
print "setattr", name
class T e s t C l a s s ( H a s T r a i t s ) :
bl = Bool
Traits & T ra its u轻
b2 = Bool
b3 = Bool
updated = Bool(False)
h a n d l e r=MyHandler 〇,
title = "Test",
buttons = ['OK', 'Cancel'])
tc = TestClass()
t c .configure_traits(view=viewl)
setattr b 2
7.5属餓辑器
下血以几个实例简单地介绍如何使用TmitsUI提供的编辑器。
7 . 5 . 1 编辑器演示程序
本节介绍一个能显示各种编辑器效果的演示程序,阁 7-12是它的界面截图。界而的左半部
分是用来创建各种 Trait 属性的源程序列表,对于选中的茶个 T m it 属性,在界而的右半部分使
用 4 种样式创建属性编辑器。
scpy2.ti'aits.traitsui_editors: 演 示 TraitsUI提供的各种编辑器的用法。
Python 科学计算(第 2 版)
Arrty(dtyp«=*int32'. »h«p«a(5.3)) 叫 1 U
Bcol(Tru*) simple 0 2
Bwtton(*ClirIe
ChtcldjftCdit〇r<v*J〇e*=dtm c Jlit))
n
CodeCprint 1i«llo world'*)
Colorfred .〉
RGBC〇*〇f<*r«d*) Cuttorn 2.0
1r«t(ad*Trio.iitt)
Dir«ctory<os.g«tcwd〇)
Fnum(*d<'moJi*t) SI 0 0] [0 2 0] [0 0 )D
m t〇
fo*it〇
M TM lf<b» <f〇nt color^'rcd* urr**40**hrfl〇
Uft(9lr, d»m 〇.Sit)
^Ungell. 10. i)
Litt(r<fito/s 5<^Mit〇r(viUj<>»3 damo^b*tt))
图 7 - 1 2 演示 TraitsUI提供的各种编辑器
class E d i t o r D e mo I t em(HasTnaits):
code = C o d e ()
view = View(
Gnoup(
Itemf'item", style="simple", label="simple", width=-300), O
rtem(__item_、 s t y l e = V e a d o n l y % label=__readonly"),
),
class EditorDemo ( H a s T r a i t s ) :
codes = List(Stn)
selected 一item = Instance(EditorDemoItem)
selected 一code = Str
view = View(
HSplit(
Item("codes", style="custom", s h o w J a b e ^ F a l s e , O
editor=ListStrEditor(editable=False, s e l ected="selected_code")),
I t e m C ’selected— item", style="custom", show_label=False)^
T 松制作图形界面
)
employee = Em p l o y e e ()
demo— list = [u" 低通" ,u " 高通" ,u" 带通" ,u" 带阻" ]
trait__defines ="""
Array(dtype="int32", shape=(3,3))
Bool(True)
Button("Click me")
List(editor=CheckListEditor(values=demo_list))
Code("print 'hello world'")
Color("red")
RGBColor("red")
T r a i t (*demo 一list)
Python 科学计算(第2 版)
D i r e c t o r y ( o s .g e t c w d ())
Enum(*demo_list)
File()
Font()
H T M L ( ' < b x f o n t c o l o r = ,,red" siz e = " 4 0 M>hello w o r l d < / f o n t x / b > ')
demo = EditorDemo()
7 . 5 . 2 对象编辑器
随着程序开发的进行,界面中的控件数F1会逐渐增多,功能会越来越复杂,这意味着与界
面对应的模型类也会变得复杂起来。为了便于代码的理解、管理以及重用,我们需要对模型类
及其对应的界而视图对象进行重构。将程序中重复使用、相对独立的部分作为组件分离出来,
单独为其设计模型类和视图对象,最终的应用程序由一系列这样的组件构成。这些组件可以在
程序的不同地方重复使用,从而起到功能分离、代码重用等多方面的作用。Tm itsUI 的 M V C 模
式非常适合这利1组件开发方式,下而让我们通过一些实例深入理解M V C 模式所带来的便利。
下面介绍的程序创建如图7-13所示的界面,用户可以通过上方的下拉选择框选择一种形状,
选择框下面的控件会自动根据所选的形状发生变化。当通过这些控件输入形状数据时,界面下
方的信息栏会£1动更新。由于程序较长,下面将它分为几个部分进行分析。
根 据 下拉选择框 创 建 不 同 的 编 辑 界 面
Center X;
图7 - 1 3 组件演示,根据下拉选择框创建不同的编辑界面
6
3
scpy2.traits.traitsui_component: TraitsUI 的组件演示程序
class Point(H a sT r a i t s ) :
x = Int
y = Int
上面的程序定义了用于保存平面上点的坐标的P o i n t 类。
我们还为它指定了一个视图对象,
视图中 X 和 Y 轴的坐标值输入框是横向排列的。
运 行 Point 〇.configure_tmits 〇即可看到 P o i n t 对象
所创建的界面效果。
我们可以将 Point 类当作组件使用,将它嵌入更复杂的界而中。在下面的程序中定义了一
个基类 Shape 及其两个派生类 Triangle 和 Circle ,使 用 Point类定义所有表示二维坐标点的属性:
T 松制作图形界面
self.set_info() ©
class T n i a n g l e ( S h a p e ) :
a = Instance(Point, ()) ©
b = I n s t a n c e ( P o i n t , ())
c = I n s t a n c e ( P o i n t , ())
view = V i e w (
VGroup(
Item("a"j s t yle="custom"), O
Item("b", style=__custom"),
Item("c", s t yle="custom"),
)
)
def s e t _ i n f o ( s e l f ) :
BjbjC = self.a, self.b^ self.c
1 1 = ((a.x-b.x)**2+(a.y-b.y)**2)**0.5
12 = ((c.x-b.x)**2+(c.y-b.y)**2)**0.5
13 = ((a.x-c.x)**2+(a.y-c.y)**2)**0.5
3
7
Python 科学计算(第 2 版)
class C i r c l e ( S h a p e ) :
center = I n s t a n c e ( P o i n t , ())
r = Int
view = V i e w (
VGroup(
Item("center", s tyle="custom")^
Item("r"),
@ o n _ t r a i t_ c h a n g e ( "r " )
def s e t _ i n f o ( s e l f ) :
from math import pi
Traits & T ra its u轻
I n s t a n c e 的第二个参数指定创建缺省对象时所用的参数,当没有第二个参数时,它所定义的属性
的缺省值为 N o n e 。 这里用一个空元组表示与之对应的属性的缺省值是通过调用Point()得到的,
即缺省为 Point 〇创建的 P o i n t 对象。
〇如 果 T r a i t 屈 恍 是 I n s t a n c e 类型,并且它在视图中对应的编辑器为" c u s t o m ’
'样式,则屈性
对象的视图将直接嵌入当前的视图中。因此在 T r i a n g l e 和 C i r c l e 对象的编辑界面中将嵌入多个
P o i n t 对象的编辑器。在 I P y t h o n 中运行下面的程序可以看到所创建的界面效果:
Tniangle().configure_traits()
C i r c l e ( )•configure 一
t r a i t s ()
接下来,使叫上面的形状类制作最终的形状选择类S h a p e S e l e c t o r :
class S h a p e S e l ec t o r ( H a s T r a i t s ) :
shape = Instance(Shape) ©
view = V i e w (
VGroup(
Item("shape", s t y l e="custom••), €)
Item("object.shape.info", style="custom"), O
431
show labels = False
一
),
width = 350, height = 300, resizable = True
def _ s e l e c t _ c h a n g e d ( s e l f ) : 0
klass = [c for c in Shape.— subclasses— () if c.— name— == self. s e l ec t ] [0]
self.shape = klass()
scpy2.traits.traitsui_component_multi_view: 使用多个视图显示组件。
由于程序的改动不大,下面只介绍它和 t m i t s u i _ c o m p o n e n t . p y 的不同之处:
class S h a p e ( H a s T r a i t s ) :
info = Str
view = View(
VGroup(
Item("select", show_label=False),
VSplit( O
Item("shape", style="custom", editor=InstanceEditor(view="view,,))J ©
Item("shape"^ style=._custom.、 editor=InstanceEditor(view="view_info"))^
show_labels = False
Traits & T ra its u轻
〇为了和前面的例子有所区别,这里用•个垂直分隔容器将形状数据输入界面和显示形状
信息的控件分隔开。© shape 屈性的编辑器样式仍然为”
custom”
,但是为了指定编辑器所使用的
视图,需要通过 editor参数传递一个 InstanceEditor对象,而通过 InstanceEditor对象的 v ie w 参数
可以指定创建界面时所使州的视图名。实 际 上 ,Instance 类 型 的 T r a it 属性缺省就是使 JIJ
InstanceEditor作为"custom"样式的编辑器,因此前面的程序中都没有通过editor参数指定。当需
要修改 InstanceEditor对象的一些缺省值时,就;要手工创建它了。
下面总结一下本节的内容:
• 通 过 将 Instance类型的 Trait 属性的编辑器样式指定为"custom”
,可以实现界而的层层嵌
套,即组件功能。
•当模型类有多个视图对象时,通 过 InstanceEditor的 v ie w 参数可以选择其中的某个视图
来创建编辑此模型对象的控件。
TraitsU I 的组件并不局限于界面上的某一块R 域,我们可以在界面屮的不N 位置用不同的
7 . 5 . 3 自定义编辑器
import matplotlib
from traits.api import Bool
if ETSConfig.toolkit == "wx":
# m a t p l o t l i b 采用W X A g g 为后台,这样才能将绘图控件嵌入以w x 为后台界面序的traitsUI Tif口中
import wx
m a t p l o t l i b .use(" W X A gg " )
T r a it s & T r a i t s u 轻
from traitsui.wx.editor import Editor
m a t p l o t l i b .u s e ("Qt4Agg")
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
T 松制作图形界面
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as Toolbar
from traitsui.qt4.editor import Editor
class _ Q t F i g u r e E d i t o r ( E d i t o r ) :
scrollable = True
def init(self, p a r e n t ) : O
self.control = self._create_canvas(parent)
s e l f •set 一t o o l t i p ()
def u p d a t e _ e di t o r ( s e l f ) :
pass
def _create_canvas(self, p a r e n t ) :
panel = QtGui.QWidget()
def m o u s e m o v e d (e v e n t ):
Python 科学计算(第 2 版)
name = "Axes"
else:
x, y = event.x, event.y
name = "Figure"
panel.mousemoved = mousemoved
vbox = QtGui.QVBoxLayout()
p a n e l .setLayout(vbox)
mpl_control = FigureCanvas(self.value) ©
v b o x .a d dWidget(mpl_control)
m p l _ c o n t r o l .m p l _ c o n n e c t ("motion_notify_event"y mousemoved)
T 松制作图形界面
if self.factory.toolbar: ©
toolbar = Toolbar(mpl 一control, panel)
v b o x .addWidget(toolbar)
panel.info = QtGui.QLabel(panel)
v b o x .a d d W i d g e t ( p a n e l .info)
return panel
O 在初始化编辑器控件时会调用init〇方法,在该方法中调用_Create_Carwas()以创建控件。®
该编辑器对象的 value 属性保存对应的模型对象,即 matplotlib中 的 Figure 对象。在创建编辑器
时,可以根据模型对象属性执行初始化工作,这里判断模型对象是否有caiwas_evem s 属性。如
果有,就对其中定义的事件进行绑定。
© 在创建编辑器时传递的参数可以通过factoiy 属性获得,这里根据其中的 toolbar属性判断
是否创建工具栏。
最后还需要编写•个编辑器工厂类 M PLFigureE diton 它 从 BasicEditorFactory类继承:
class MPLFigureEdito r ( B a s i c E d i to r F a c t o r y ) :
IIIIII
if ETSConfig.toolkit == "wxM :
/ ^
44
klass = _WxFigureEditor
klass = _QtFigureEditor O
toolbar = Bool(True) ©
import numpy as np
class S i n W a v e ( Ha s T r a i t s ) :
T r a it s & T r a i t s u 轻
figure = I n s t a n c e ( F i g u r e , ())
view = V i e w (
width = 400,
T 松制作图形界面
height = 300,
resizable = True)
self.figure.canvas 一events = [
]
axes = s e l f .f i g u r e .a d d _ s u b p lo t (1 1 1 )
axes.plot(np.sin(t))
def f i g u r e_button_pressed(self, e v e n t ) :
model = SinWave()
m o d e l •edit 一t r a i t s ()
Python 科学计算(第 2 版)
7 . 6 函数曲线绘制工具
作为本章的最后一节,让我们用学到的内荞编写一个绘制函数丨|||线的小程序,界面如图
7-14所示。界面上方是显示肋线的图表,左下方为代码编辑器,右下方为显示数据点的表格。
^ Fun<t>0〇PtOtttf
T r a it s & T r a i t s u 轻
T 松制作图形界面
^ O O W \y © B cT
m
19 pita • oprUaist»l « M t art (yi Y
31 drf UOOSi
22 rrtum pl>a(0))
23 O.W5W 5
24 Otf r«#X_functi〇 n(x ) :
25 return 〖
A,le, 0.126933
2ft
27 p a r n c t t r i • JUct(lir>J« alohJ*#.1 )
0.1904 S.66S69
2t • p*^««eters
4.69767
(uxrm
a »0799 1 2 2 7 S7
Plot
ftTT4*t.
图7 - 1 4 函数曲线绘制工具的界面
本函数仲线绘制工具有如下功能:
•在图表上单击鼠标左键添加点,单击鼠标右键删除最后添加的点。也可以通过表格添
加点或者修改点的坐标。
•在 运 行 代 码 之 前 ,将表格中的点转换为形状为(N ,2)的二维数组,该数组可以在用户程
序中使WJ points访问。
•程 序 运 行 之 后 ,如果运行环境中有名为points、形状为(N ,
2)的二维数组,将该数组的内
容显示在左侧的表格中,在图表中这些数据点使用黑色的叉点表示。
•将运行环境中所有返回值为数组的单参数函数显示为丨111线,如果函数名以下划线幵头,
则忽略该函数。
• 如 果 曲 线 函 数 的 plot_parameters 属性为字典,该属性将作为关键字参数传递给 plot()
函数。
•如 果 图 表 的 X 轴范围发生变化,自动调用曲线对应的函数并更新曲线。
图 7-14所示的界面由三个控件组成。O figurc 属性是一个 matplotlib的 Figure 对象,它对应
的编辑器为上节介绍的MPLFigureEditor。© code 属性为 C od e 类型,它 从 Str 继承,其缺省的编
辑器为带高亮显示的代码编辑器。© points 屈 性 是 一 个 P o in t 对象的列表,其对应的编辑器为
point_ table_editor〇
T r a it s & T r a i t s u 轻
site-packages\pyface\ui\qt4\code_editor 下以覆盖原有的文件。
class FunctionPlotter(HasTnaits):
figure = Instance(Figure, ()) O
T 松制作图形界面
code = Code〇 ©
points = List(Instance(Point), []) ©
draw_button = Button("Plot")
view = View(
VSplit(
Item("figure1、 editor=MPLFigureEditor(toolbar=True)> show_label=False),
HSplit(
VGroup(
Item("code", style="custom"),
HGroup(
Item("draw一button", show一label=False),
)>
show—labels=False
class P o i n t ( H asTraits) :
x = F l o a t ()
y = F l o a t ()
editable^rue^
sortable=False,
sort_model=FalsG,
auto_size=False>
row_factory=Point
T r a it s & T r a i t s u 轻
]
self •button_press_status = None # 保存鼠标按键按下时的状态
self.lines = [] # 保存所有曲线
self.axe.set_ylim( 0 > 1 )
self .points 一line, = self .axe .plot ( [ ] , [ ] , "kx", m s = 8 , zorder= 1 0 0 0 ) # 数据点
在图表中使用鼠标进行平移和缩放时,也会触发鼠标按键按下和释放的响应函数。为了K
分这些操作与鼠标按键的单击操作,在 memory_ location〇中记^下鼠标按键按下时的状态,并
4
/
%
在 update_ location()十与鼠标释放吋的状态进行比较,如果满足:O 鼠标按键按下的时间少于0.5
秒,© 鼠标的移动距离小于4 个像素,就认为是鼠标单击操作。
© 当鼠标左键释放时,创建一个 Point对象,其 x 和 y 属性被设置为鼠标释放时在子图中的
坐标,并将该对象添加进 points 列表。© 若按下右键,贝彳调用points.popO删除列表中的最后一
个元素。
def memory_location(self, e v t ) :
else:
self,button_press_status = None
T r a it s & T r a i t s u 轻
return
if ((evt.x - last_x) ** 2 + (evt.y - last_y) ** 2) ** 0.5 > 4: ©
return
T 松制作图形界面
if evt.button == 1 :
if evt.xdata is not None and evt.ydata is not None:
s e l f .p o i n t s . p op () O
当 points列表木身或者其中的元素发生增减时,将调用_{^1^_(±1〇
1^ 6(1(),其 n ew 参数为新
添加进列表的元素。® 当通过表格编辑坐标点的位置时,Point 对 象 的 x 或 y 属性将发生变化,
为了捕捉到这些变化并更新图表中的坐标点,需要对新添加进 points列表的对象进行第件绑定。
中,从 points屈性创建二维数组,并调用 points」ine.set_data()更新图表中
的坐标点,调用© updat^ figureO重新绘制图表。由于这-•系列的触发事件可能在图表的绘图控
件创連之前发生,
因此〇在调凡I canvas.draw_ idle〇重绘制图表之前需要判断canvas 是否为 None 。
@ o n _ t r a i t_ c h a n g e ( " p o i n t s []")
def _points 一changed(self, obj, name, n e w ) :
s e l f .updat e j D oi n t s ()
def u p d a t e j D oi n t s ( s e l f ) : ©
Python 科学计算(第 2 版)
s e l f .p oints_line.set_data(arr[:^ 0 ], arr[:, 1 ])
else:
s e l f •points 一l i n e •set 一data([],[])
self.update_figure()
def u p d a t e _ f ig u r e ( s e l f ) : ©
if self.figure.canvas is not None: O
s e l f .f i g u r e .c a n v a s .d r a w _ i d l e( )
y = func(x)
line.set_data(x, y)
self.update— f i g u r e ()
T 松制作图形界面
self.plot_lines()
results = [ ]
44
for line in self.lines:
l i n e . r e m ov e ()
s e l f •a x e •set_color_cycle(None) 置颜色循环
self.functions = [ ]
self.lines = [ ]
for name, value in s e l f . e n v .i t e m s ( ) : ©
if name.startswith("__"): # 忽略以_开头的名字
continue
if c a l l a b l G ( v a l u e ) :
try:
y = value(x)
if y.shape != x.shape: #输出数组应该与输入数组的形状一致
raise ValueError("the return shape is not the same as x")
except Exception as ex:
import traceback
print "failed when call function { } \ n ".f o r m a t (n a m e )
T r a it s & T r a i t s u 轻
t r a c e b a c k .p r i n t _ e x c ()
continue
r e s u l t s .a p p e n d ( (name^ y))
s e l f .f u n c t i o n s .append(value)
T 松制作图形界面
for (name, y), function in zip(results, s e l f . f u n c t i o n s ) :
# 如果函数有p l o t _ p a r a m e t e r s 属性,
则用其作为p l o t ◦ 的参数
kw = getattr(function, "p l o tjDarameters", {}) O
label = kw.get("label", name)
s e l f .l i n e s .append(line)
self.ax e .l e g e n d ()
self.update 一f i g u r e ()
TVTK与Mayavi-数据的三维可视化
V T K 是一套功能十分强大的三维数据可视化库,它使用 C + + 编写,其中包含了近千个类。
它 在 Python 下有标准的扩展库,不过由于其 Python 扩展序的 A P I 和 C H •的A P I 相同,不能体现
出 Python 作为动态语言的优势;冈此 Enthought公司开发了一套名为T V T K 的扩展厍来对 V T K
进行包装,提供了 Python 风格的 A P I , 并支持 Trait属性和 N u m P y 数组。本韋以 T V T K 的 API
为例介绍如何在 Python 中使用 V T K 进行数据的三维可视化。
由于 T V T K 库十分庞大,为了方便用户査询文档,T V T K 库提供了一个显示T V T K 文档的
工具。可以通过下面的语句运行它:
T V T K 库提供的工具并不太好用,本书为读者提供了一个更方便的 T V T K 文档查询工具,
其界面如图8-1所示。
vJtVTOT
〇 C o m i( 〇 u r f# 秦 IConc Source
3
〇 C o r t lo u r f ih r y
〇
C c r if o u K jr id
C 〇 «m « ) ( H u 4U >〇
1
5. Co m :Source g r r t f r t t t r p o ly ^tu % ! ccrve
C u b « S o u rc e 摹
C u rto r2D
〇 C u f K K lD 篇
〇 C u rv jtu rti
9 Cpne3〇ttice • ccnc c c sc e r e tl nr » rifled i
• 10 ^ i r . a ricr direrii:-. iDy defdult, The rcr.tc
• 1 s n g i s u d th e d i r e d e n th e jt ,Depending upca t
r r ^ c l u t l c n o f c h i3 o b je c t# d i f f e r e n t r r p r r o e n t J i t t c t u i ntr
9 V: Cone rc^oS ution ^O n l t n r 4a c r r n c c d ; i f m ^ c l u t l o n ^ l , 〇 uln^S r
c t鬌 if two c t o ^ t ^ d t r m n g l ^ i i 费
第一次运行此文档工具时,它将对 T V T K 库中所有的类进行扫描,并将类的继承关系和文
木
3 全部保存在 tvtk_classes.cache 中,这个过程可能斋要等待较长的时间。界 面 的左上部分使j|J一
个树状控件显示 T V T K 中各个类之间的继承关系。在中间的文本框中输入搜索文本,其下方的
列表框中会实时显示搜索结果。输入全小写字母进行忽略大小写的搜索,而输入带大写字母的
文本则进行精确搜索。
V T K 是一个十分复杂的系统,为了方便用户使用,它使用流水线技术将 V T K 中的各个对
TV T与
象串联起来。每个对象只需要实现相对简单的任务,整个流水线则能够根据用户的需求实现十
K
分复杂的数据可视化处理。
M a y a v数
8.1.1 显示圆锥
T 据的三维可视化
作为第一例子,让我们首先看一个显示圆锥的小程序,它的运行效果如图8-2所示。
图8 -2 使用T V T K 绘制简单的圆锥
45:
%%python
#coding=utf- 8
# 创建一个圆锥数掘源,并且同时设置其高度、底面半径和底而圆的分辨率(用 3 6 边形近似)
cs = tvtk.ConeSource(height=3.0> radius=1.0, resolution=36) ©
# 使用P o l y D a t a M a p p e r 将数据转换为图形数据
m = tvtk.PolyDataMapper(input 一 connection=cs.output 一port) 0
# 创建一个Actor
a = tvtk.Actor(mapper=m) O
# 创建一个R e n d e r e r , 将 A c t o r 添加进去
ren = tvtk.Renderer(background=(l, 1, 1 )) ©
ren.add 一actor(a)
# 创建一个 RenderWindow(1$f 口)
,将 Renderer 添加进去
rw = tvtk.RenderWindow(size=(300,300)) ©
TV T与
r w .a d d _ r e n d er e r (r e n )
# 创建一个RenderWindowInteractor (窗口的交互工具)
K
rwi = tvtk.RenderWindowInteractor(render_window=rw) O
M a y a丁
# 开启交互
rwi. i n i t i a l i ze 。
v 数据的三维可视化
rw i . s t a r t ()
a = tvtk.Actor(mapper=m)
ren = tvtk.Renderer(background=(l, 1 , 1 ))
ren.add_actor(a)
cs.trait 一n a m e s ()
[•number_of_output 一ports ’
,
•abort_execute _ 、
'class 一n a m e ',
'executive 、
• • •
3.0 1.0 36
TV T与
为了将原始数椐转换为屏藉上的一幅阁像,需要经过许多处理步骤。这些步骤由众多的
K
V T K 对象分步实现,就好像生产线上加工零件一样,每位工人都负责一部分工作,整条生产线
M a y a v数
就能将原材料制作成产品。在 V T K 中,这利1在各个对象之间协调完成工作的过程被称作流水
线(Pipeline)。
T 据的三维可视化
原始数据被加工成图像要经过两条流水线:
•可 视 化 流 水 线 (Visualization Pipeline):它的工作是将原始数据加工成图形数据。一般来
说,我们耑要进行可视化展示的数据本身并不是图形数据,例如可能是某个零件内部
各个部分的温度,或是流体中各个坐标点上的速度等。
•图 形 流 水 线 (Graphics Pipeline) : 它的工作楚将图形数据加工为我们所看到的图像。⑴‘
视化流水线所产生的图形数据通常足三维空间的数据,
图形流水线将这些三维数据加
工成能在二维屏藉上显示的图像。
€)映射器( M a p p e r ) 是可视化流水线的终点、阁形流水线的起点,它的各种派生类能将众多
的数裾映射为阁形数椐以供阁形流水线加工。在木例中,C c M i e S o u r c e 对象输出一个描述圆徘:的
顶点和面的 P o l y D a t a 对象,然 后 P o l y D a t a 对象通过 P o l y D a t a M a p p e r 映射器转换为图形数据。因
此在木例中,可视化流水线由 C o n e S o u r c e 和 P o l y D a t a M a p p e r 对象组成。
可视化流水线中的对象经由 input_connection 和 output_p o rt 屈性连接起来。在本例中,
ConeSource 对象产生•个表示圆锥的 PolyData 对象,并转交给 PolyDataMappei•对象进行处理。
可以通过 output或 input属性查看在流水线中实际传递的PolyData 对象:
45
是图形流水线。O A c to r 对象代表场景中的一个实体,它 的 mappei•属性是表示图形数据的
PolyDataMapper对象,A ctor 对象还存许多属性以控制实体的位置、方向、大小等。
print a.mapper is m
True
ren.actors
©RenderWindow 对象表示包含场景的窗口,它可以同时包含多个场景。在本例中,它只有
一个 Renderer 对象。
H V T与
©RenderWindowInteractor对象为图形窗口提供一些用户交互功能,
例如平移、
旋转和缩放。
K
这些交互式操作并不改变场M 中的各个实体(Actor 对象),也不改变图形数据的属性,它们只足
修改场景中照相机(Camera)的设置,从不同的角度和距离观察场景中的实体。
M a y a v数
8 . 1 . 2 用 ivtk 观察流水线
T 据的三维可视化
为了方便对流水线进行观察和操作,木 书 在 scpy2.tvtk.tvtkhelp 模块中提供/ ivtk_scene() 和
e v e n t J o o p O 两个函数。使用它们可以交互式地对各种T V T K 对象的属性进行编辑,下而是使用
这两个函数显示圆锥的程序,运行画面如图8-3所示。
阁8 - 3 带流水线浏览器和Python命令行的ivtk界而
5
45
Python 科学计算(第 2 版)
window = ivtk_scene([a]) O
w i n d o w .s c e n e .i s o m e t r ic_view()
event 一l o o p () ©
•流 水 线 浏 览 器 :场景左边是一个表示流水线的树状控件。从 子 节 点 C o n e S o u r c e 开始
K
逐步向上层直到根节点R e n d e r W i n d o w , 是S 示圆锥的整个流水线。
M a y a v数
• P y t h o n 命令行:界面下方提供了一个 P y t h o n 命令行,方便用户直接输入命令来操作
各个对象。
例如图中显.示了通过场景对象s c e n e 获収 C o n e S o u r c e 对象所输出的P o l y D a t a
T 据的三维可视化
阁 8 4 编 辑 ConeSource对象的厲性的对话框
45(
1.照相机
在 iv tk 的窗 I」左侧的流水线浏览器中可以找到场景中的照相机对象OpenGLCamera,双击
它会弹出如图8-5所示的编辑照相机对象属性的窗口。
TV T与
K
M a y a v数
T 据的三维可视化
阁8 - 5 编辑照相机屈性的对话框
c a m e r a •e d i t _ t r a it s ( ) # 显示编辑照相机属性的窗口
[4.2227355 12.69546854]
下面列山照相机对象的一些常用属性:
• dipping^mnge: 它有两个元素,分别表示照相机到近远两个裁剪平面的距离。在这两
个〒面之外的对象将不会显示。
Python 科学计算(第 2 版)
• position:照相机在三维全间中的坐标。
• f o c a L p o i n t :照相机所聚焦的焦点坐标。
• v i e w _ u p : 照相机的上方向矢量。
2.光源
在 ivtk 窗口中,单击场景上方的工具栏中的最后一个齿轮形状的图标,将打开如图8-6所
示的编辑场景和光源的对话框。在此对话框中可以添加和删除光源以及修改它们的一些屈性。
TV T与
K
|\/^3<丁 数 据 的 三 维 可 视 化
阁 8 - 6 设®场眾和光源的对话框
lights = window.scene.renderer.lights
l ights[0].edit_traits 〇 # 显示编辑光源属性的窗口
下面的程序在照相机所在处添加一个红色的光源,它的照射方向和照相机的方向相同,朝
向 focal_point点。如果设置光源对象的positional属性为 True ,它将变成一个探照灯光源,这时
照射方昀有效。并且可以通过 c 〇ne_angle 属性设置探照灯的光锥角度,如果光锥为180度 ,它
45!
是无方向光源。
camera = window.scene.Tenderer.active_camera
light = tvtk.Light(color=(1,0,0))
light.position=camera.position
light.focalj3 〇int=camera.focal_point
window.scene.Tenderer.add_light(light)
3.实体
TV T与
K
M a y a v数
T 据的三维可视化
图8>7 Actor对象的编辑对话框
也可以使用下面的程序打开此对话框:
a •edit_traits () # a 是表示圆锥的A c t o r 对象
wi n d o w . s ce n e . T e n d e r e r . a c to r s [0 ] .edit_traits()
i. 4
►
TV T与
X
N
M a y a v数
vO
T 据的三维可视化
阁8 -8 依次修改圆锥的scale、orientation和 position屈性
请读者仔细观察每两幅图之间的变化,分析并理解前述坐細变换步骤。旋转的正方M 按照
右手法则决定:右手握拳,并伸出大拇指让它指向某个轴的正方向,则其余4 指的方向为绕此
轴旋转的正方向。
A c t o r 对象的 p r o p e r l y 厲性是一个 O p e n G L P r o p e r t y 对象,它包含了对实体进行着色时所使用
的各种配置,例 如 c o l o r 属性是实体的颜色,o p a c i t y 属性是实体的不透明度。输入下面的语句可
以打开编辑这些属性的对话框:
数据可视化的第一步是用合适的数据结构表示数据,
V T K 提供了多利嚷示不同利喽数据的
数据集(Dataset)。数据集包括点(Point)和数据(Data)两部分。点之间可以是连接的或非连接的,
多个相关的点组成单元(Cell) , 而点之间的连接可以是显式或隐式的。数据可以是标量或矢量,
数据可以属于点或单元。下面让我们通过一些实例逐步理解数据集的构造。
为了帮助读者更形象地了解数据集的结构,我们使W M a y a v i 将数据集的结构绘制成三维
图。请读者在学习的过程中运行这些程序,以加深对数据集的理解。在 学 习 M a y a v i 时,也可
以将这些程序作为实例,了解 M a y a v i 的一些高级用法。
8.2.1 ImageData
最容易理解的数椐集是 ImageData,它是表示二维或三维阁像的数据结构。可以简单地将
其理解为二维或三维数组。数组中存放的是数据,由于点位于正交且等间距的M 格之上,因此
不需要给出点的坐标,而点之间的连接关系也由它们在数组中的位置决定,因此连接也是隐
式的。
下面的程序创建了一个 Im ageD ata 对象,并且设置了它的 spacing 、o rig in 和 dimensions
属性:
的坐标:
for n in range( 6 ):
print %.lf, %.lf" % img.get_point(n)
与每个点对应的数据都保存在p〇int_data屈性_中,它是一个 PointData对象:
img.point_data
<tvtk.tvtk_classes.point_data.PointData at 0xal87780>
D onbleA m iy 数组只能以整数为下标,不支持切片以及其他高级下标运算。可以通过它的
IV la ya丁
的数据存収操作:
a = img.point一
data.scalars.to 一array()
print a
a[:2] = 10, 11
print img.point—data.scalars[0 ], img.point—data.scalars[1]
[ 0 . 1 . 2 . 3. 4. 5. 6 . 7. 8 . 9. 10 . 1 1 . 1 2 . 13. 14.
15. 16. 17. 18. 19. 20 . 2 1 . 2 2 . 23. 24. 25. 26. 27. 28. 29.
30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44.
45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59.]
10 .0 1 1 .0
i m g . p o i n t_ d a t a .s c a l a r s .number_of_tuples
60
每 个 V T K 数 组 都 有 一 个 n a m e 属性用于保存其名字,下面的语句将数组的名字设置为
'scalars':
i m g .p o i n t _ d a t a .s c a l a r s .name = 'scalars'
P o i n t D a t a 对象可以保存多个数组,它所包含的数组的个数可以通过 n u m b e r _ o f _ a i r a y s 屈性.
接下来将 T V T K 数组对象的名字设置为"zerodata":
data.name = "zerodata"
数组的序号,可以通过此序号获得数组或数组名:
T V T与
print img•point 一
data •add_array (data )
K
print repr (img.point _data .get _array (l )) # 获得第 1 个数组
M a y a v数
print img.point _data .get _array _name(l ) # 获得第 1 个数组的名字
print repr (img.point _data .get _array (0)) # 获得第 0 个数组
T 据的三维可视化
print img.point _data .get _array _name(0) # 获得第 0 个数组的名字
1
[0.0, 0.0], length = 60
zerodata
[10.0, •••, 59.0], length = 60
scalars
最后,remove_airay()方法通过数组名删除数组:
63
Python 科学计算(第 2 版)
print t y p e (i m g .p o i n t _ d a t a .v e c t o r s )
print i m g . p o i n t_data.vectors[0 ]
我们看到所创建的仍然是一个DoubleArray 对象,但它是二维数组。numbcr_of_tuples属性
获得其第0 轴的长度,而 number_of_components属性获得其第1轴的长度:
i m g .point_data.vectors.number_of_tuples i m g .point_data.vectors.number_of_components
60 3
之类似,点和单元之间的关系也是隐式定义的。单元和点之间的关系如图8-9所示。单元是由
K
8 个邻近的点构成的立方体,图中使用半透明灰色立方体标识出第0 个单元。
M a y a v数
T 据的三维可视化
点(
P o in t )
维元(C e l l 〉
图8 - 9 单元和点之间的关系
cell = img.get_cell( 0 )
print repr(cell)
8 12 6
p o i n t _ i d s 厲性可以获得构成体素的点的序号列表,而 p o i n t s 厲性则获得构成体素的点的坐
标:
print r e p r (c e l l •p o i n t _ i d s )
c e l l •points.to 一a r r a y ()
array([[ 0 .1 , 0 .2 , 0.3],
[ 0 .2 , 0 .2 , 0.3],
[ 0 .1 , 0.3, 0.3],
[ 0 .2 , 0.3, 0.3],
[ 0 .1 , 0 .2 , 0.4],
[ 0 .2 , 0 .2 , 0.4],
[ 0 .1 , 0.3, 0.4],
[ 0 .2 , 0.3, 0.4]])
数0 〇
i m g •number 一
of_cells
24
a = tvtk.IdList()
i m g .g e t j 3 〇
i n t _ c e l l s (3, a)
a = tvtk.IdList()
a • f rom_ar ray ([ 1 , 2 3 ])
a.append(4)
a . e x t e n d ([5,6])
print repr(a)
[1, 2, 3, 4, 5, 6 ]
与每个单元对应的数据都保存在cell_data属性中,
它是一个 CellData 对象,用法和 PointData
对象类似,这里就不再多做介绍了。
img.cell— data
8.2.2 RectilinearGrid
TV T与
起始处标、网格大小以及网格间距等信息就可以计兑出网格上所有点的坐标。如果要表示间距
M a y a v数
(〇
.〇.〇
)
r
X
get 一c e l l ⑴
(15,Q.〇
r
图8 ~1 0 使用RectilinearGrid创建分布不均匀的网格
x = np.array([0,3,9,15])
y = np.array([0,l,5])
z = np.array([0,2,3])
r = t v t k . R e c ti l i n e a r G r i d ()
r.x coordinates = x O
r.y 一coordinates = y
r.z_coordinates = z
r.dimensions = len(x), len(y), len(z) ©
r.point_data.scalars.name = 'scalars'
for i in xrange( 6 ):
print n.getjDoint(i)
(0 .0, 0 .0, 0 .0 )
(3.0, 0.0, 0.0)
(9.0, 0.0, 0.0)
单元和点之间的关系也和ImageData —样:
c = r.get 一c e l l (1 )
print "points of cell 1 :_、 r e p r (c .poi n t _ i d s )
print c.points.to_array()
i 3 9r3L 9 [3 9 r3L9
o
c
p
O00110011
1 J
nt el 13 14 17 18
0.
r
TJ TJ TJ TJ TJ TJ TJ TJ
0.
L
0.
0.
2.
2.
2.
2.
r L
r L
r L
r L
r L
8.2.3 StructuredGrid
比 RectilineaiGrid 更进一步,StructuredGrid需要我们指定每个点的坐标。而点和单元之间的
关系仍然由点在M 格中的位置决定。图 8-11 S 示了两种用 StructuredGrid创建的M 格结构。
Python 科学计算(第 2 版)
get^cell(3)
虏的下标《序
X
点的下择JB序
图8-11 用 StructuredGrid创建的网格结构
下而对创逑这两个网格的程序进行分析:
TV T与
def m a k e j 3 〇
ints_array(xJ y, z ) :
return np.c_[x,ravel()_» y.ravel(), z . r a v e l 〇]
T 据的三维可视化
Zj y, x = np.mgrid[:3.0, :5 為 :
4.0] O
x *= (4-z)/B ©
y *= (4-z)/3
si = tvtk.StructuredGrid()
si.points = make_points 一array(x, y, z) ©
si.dimensions = x . s h a p e [ : : -1 ] O
s i .p o i n t _ d a t a .scalars = np.arange( 0 , s i .n u m b er_of_points)
si.point_data.scalars.name = 'scalars'
0 1 2 3 20 21 22 23 40 41 42 43
4 5 6 7 24 25 26 27 44 45 46 A7
8 9 10 11 28 29 30 31 48 49 50 51
12 13 14 15 32 33 34 35 52 53 54 55
16 17 18 19 36 37 38 39 56 57 58 59
-- — —
TV T与
单元由M 格屮相邻的几个点构成,因此单元2 由图8-12 1 18 个灰色矩形表示的点构成:
K
si •get 一cell ⑵ •point 一ids
M a y a v数
[2, 3, 7, 6 , 22, 23, 27, 26]
T 据的三维可视化
由于单元的形状不是长方体,因 此 V T K 采用 H e x a h e d r o n 对象表示单元,get_face()和
g et_edg e ()方法分别用于获得构成此单元的面和边:
c = sl.get_cell( 2 )
print "cell type:"., type(c)
number_of_faces: 6
69
Python 科学计算(第 2 版)
个空心圆柱。程序屮,首先在圆柱坐标系屮创建等距网格,然后将各点坐标转换到直角來标系
中,具体的步骤留给读者自行分析。
x 2 = np.cos(theta)*r
y 2 = np.sin(theta)*r
s2 = t v t k . S t r uc t u r e d Grid(dimensions=x2.shape[::-l])
s 2 .points = make_points_array(x 2 , y 2 , z 2 )
s 2 .point_data.scalars.name = 'scalars'
8.2.4 PolyData
PolyData 数椐集由一系列的点、点之间的连线以及由点构成的多边形而组成。这些信息都
需要用户进行设置,因此用程序创建 PolyData 对象比较烦琐。T V T K 中的许多三维模型类都输
TV T与
出 PolyData 对象。例如在第一节中介绍的创建圆锥数据的ConeSource 类:
K
source = tvtk.ConeSource(resolution = 4)
M a y a v数
source.update() # 让 s o u r c e 计算其输出数据
cone = source.output
T 据的三维可视化
type(cone)
t v t k .t v t k _ c l a s s e s .p o l y _ d a t a .PolyData
[[0.5 0. 0.]
[-0.5 0. 0.5]
[-0.5-0. -0.5]]
print type(cone.polys)
print cone .polys •number_of_cells # 圆锥有 5 个面
print cone.polys.to 一a r r a y ()
47(
〈class 't v t k .t v t k _ c l a s s e s .c e l l _ a r r a y .C e l l A r r a y '>
5
[4 4 3 2 1 3 0 1 2 3 0 2 3 3 0 3 4 3 0 4 1]
4 : 4, 3, 2 , 1
3 :0 , 1 , 2
3 :0 , 2 , 3
3 : 0 , 3, 4
3 :0 , 4, 1
TV T与
下面看看如何直接创建PolyData 对象,首先是-•个简单的方锥的例子:
K
M a y a v数
pi = tvtk.PolyData()
T 据的三维可视化
faces = [
4.0. 1.2.3,
3.4.0. 1,
3.4.1.2,
3.4.2.3,
3,4,3,0
]
cells = t v t k . C e l l A r r a y () ©
pi.polys = cells
O 在介绍 StructuredGrid时,我们将一个形状为(N ,
3)的数组赋值给points屈性,这M 使用坐
标列表进行赋值,效果和使用数组相冋。© 为 了 给 p o ly s 屈性赋值,需要酋先创建-个新的
C ellA iray 对象。€)然后调用 C ellA m iy 对 象 的 set_cells()设置其内容,第-•个参数为面(单元)的个
数,第二个参数是描述各个面的构成的数组(或列表)。所创建的方锥如图8-13(左)所示,图中标
出了各个点的序号。PolyData 对象中的各个面可以通过 get_cell()方法获得,图中标出了第0 个
和 第 1个面。
Python 科学计算(第 2 版)
g e t .c e l l (l )
g e t _ c e l l (0)
阁 8-13川 PolyData创逑的多而体
下面获取第0 面 和 第 1面上的点的序号:
M a y a v数
print r e p r (p i .g e t _ c e l l (1 ).p o i n t _ i d s )
[0, 1, 2, 3]
[4, 0 , 1 ]
N = 10
a, b = np.mgrid[0:np.pi:N*lj, 0:np.pi:N*lj]
x = np.sin(a)*np.cos(b)
y = np.sin(a)*np.sin(b)
z = np.cos(a)
points = make_points_array(x, z) O
faces[:,2] = facesfi^l] + N
faces[:,3] = faces[:,0] + N
47
首先将球坐标系中的点转换为M 角坐标系中的坐标。〇然后调用 make_points_aiTay 〇将这些
坐标值转换为形状为(N ,
3)的数组。©如果每个而的点数相同,可以用一个二维数组表示面和点
之间的关系,其中第0 轴的长度为而数,第 1轴的长度为每个而的点数。可以将此二维数组直
接赋值给 p o l y s 属性,T V T K 库会帮我们完成二维数组到CellAiray 对象之间的转换。
p 2 .polys.to_array()[:2 0 ]
4, 14, 13])
请读者根据程序思考faces 数组的计兑方法,这里就不再多做解释了。
8.3 TVTK的改进
TV T与
K
与本节内容对应的 Notebook 为:08-tvtk_mayavi/tvtk_mayavi~400-tvtk_and_vtk.ipynb。
M a y a丁
v 数据的三维可视化
V T K 拥有详细的 C + + A P r 说明文档,而 Python 的 V T K 扩展库的用法和 C +4•的用法基本相
%%python
# coding: u t f - 8
import vtk
# 创建一个圆锥数据源
cone = v t k . v t k C o n e S o u r c e ( )
cone.SetHeight( 3.0 )
cone.SetRadius( 1.0 )
c o n e .S e t R e s o l ut i o n (1 0 )
# 使用P o l y D a t a M a p p e r 将数据转换为图形数据
coneMapper = v t k . v t k P o l y D a t a M a p p e r ( )
c o n e M a p p e r .Set l n p u t Co n n e c t i o n ( c o n e .G e t O u t p u t P o r t ( ) )
# 创建一个Actor
coneActor = v t k . v t k A c t o r ( )
c o n e A c t o r .SetMapper ( coneMapper )
# 用线框模式显示圆锥
Python 科学计算(第2 版)
c o n e A c t o n .G e t P r o p e r t y ( ) . S e t R e p r e s e n t a t i o n T o W i r e f r a m e ( )
# 创建R e n d e r e r 和窗口
renl = v t k . v t k R e n d e r e r ( )
renl.AddActor( coneActor )
r e n l •S e t B a c k g ro u n d ( 0.1 , 0.2 , 0.4 )
renWin = v t k . v t k R e n d e r W i n d o w ( )
r e n W i n .AddRenderer( renl )
renWin.SetSize(300 , 300)
# 创建交互工具
iren = v t k . v t k R e n d e r W i n d o w I n t e r a c t o r ( )
i r e n .SetRenderWindow( renWin )
iren.Initialize()
i r e n . S t a rt ( )
• 支 持 元 素 的 Pickle 操作
v 数据的三维可视化
• A P I 更接近 Python 风格
•能丨二
1动处理N um Py 数组或列表对象
• 流 水 线 浏 览 器 ivtk
8.3.1 TV TK 的基本用法
a = tvtk.Actor(mapper=m)
ren = tvtk.Renderer(background=(l, 1, 1 ))
ren.add_acton(a)
rw = tvtk.RenderWindow(sizG=(300,300))
rw•add renderer(ren)
一
rwi = tvtk.RenderWindowInteractor(render_window=rw)
rwi. i n i t i a l i ze 。
rw i . s t a r t ()
p .SetRepresentationToWireframe() # VTK
• T ra it 属性可以在创逑对象的同时通过关键字参数进行设置,这样更便于程序的编写
和阅读。
在 T V T K 库的内部实现中,所有的 T V T K 对象的内部都有一个 V T K 对象,对 T V T K 对象
T V T与
的函数调用将转给内部的V T K 对象执行。如朵返回值是 V T K 对象,它将被包装成 T V T K 对象
K
返回。如果方法的参数是T V T K 对象,其中的 V T K 对象将作为值进行参数传递。
M ayavT
8.3.2 Trait 属性
数据的三维可视化
所有的 T V T K 类都从 HasStrictTmits继承,HasSlrictTmits规记了它的子类的对象在创建之后
不能对不存在的屈性进行赋值。V T K 屮所有和■本状态有关的方法在T V T K 屮都使用 Trait 屈
性表示。调 用 set〇方法可以一次设置多个Trait 屈性,例如:
p = t v t k . P r o p e r t y ()
p .set(opacity=0.5, color=(l,0,0), representation="w")
p.edit_traits()
print p.representation
p _ v t k .SetRepresentationToSurface()
print p.representation
wireframe
surface
对象包装成 T V T K 对象。
8 . 3 . 3 序列化
T V T K 对象支持简单的序列化处理。单 个 T V T K 对象的状态可以被序列化:
import cPickle
p = tvtk.Property()
p. representation = "w"
s = cPickle.dumps(p)
del p
q = cPickle.loads(s)
q. representation
'wireframe'
但是序列化仅仅能保存对象的状态,对象之间的引用无法被保存。因此 T V T K 的整个流水
线无法用序列化保存。通 常 pickle.load()将创建新的对象,如果我们希望更新菜个已经存在的对
T V T与
p = tvtk.Property()
M ayavT
p. interpolation = "flat"
数据的三维可视化
d = p._ getstate— ()
del p
q = tvtk.Property()
print q .interpolation
q. 一setstate_ (d)
print q .interpolation
gouraud
flat
8 . 3 . 4 集合迭代
从 tvtk.ColleCti〇
Ii 继承的对象可以像标准的 Python 序列对象一样使叫,下面的例子演示了
ActorCollection 对象支持 len〇、append()以及 for 循环:
ac = tvtk.ActorCollection()
print len(ac)
ac.append(tvtk.Actor())
ac.append(tvtk.Actor())
print len(ac)
for a in ac:
print repr(a)
47(
del ac[0]
print len(ac)
0
2
<tvtk.tvtk_classes.open_gl_actor.OpenGLActon object at 0x0A24A690>
<tvtk.tvtk_classes.open_gl_actor.OpenGLActor object at 0x0A174ED0>
1
import vtk
ac = vtk.vtkActorCollection()
print ac.GetNumberOfItems()
ac.Addltem(vtk.vtkActor())
ac.Addltem(vtk.vtkActor())
print ac.GetNumberOfItems()
T V T与
ac.InitTraversal()
K
for i in range(ac.GetNumberOfItems()):
M ayav
print repr(ac.GetNextItem())
丁数据的三维可视化
ac.RemoveItem(0)
print ac.GetNumberOfItems()
0
2
(vtkOpenGLActor)0A24AF90
(vtkOpenGLActor)0A24AF00
1
8 . 3 . 5 数组操作
所有 DataAiray的派生类和Python的序列一样,
支持迭代接口以及_ getitem_ ( ) 、
_ setitem_ 〇、
_ repr_ 〇、append〇、extend()等。此外,还可以通过 fr〇m_array()直接j N u m P y 数组或列表进行
赋值,可以很方便地将其:中保存的数据转换为N u m P y 数组。Points和 IdList等对象也同样支持
这些特性:
pts = tvtk.Points()
p一array = np.eye(3)
pts.f rom_a m a y (p_a rray)
pts.print_traits()
pts.to_array()
_in_set: 0
_vtk_obj: (vtkPoints)0A2D82D0
Python科学计算 (第 2 版)
actual_memory_size: 1
bounds: (0 .0 , 1 .0 , 0 .0 , 1 .0 , 0 .0 , 1 .0 )
class_name: 'vtkPoints'
data: [(1 .0 , 0 .0 , 0 .0 ), (0 .0 , 1 .0 , 0 .0 ), (0 .0 , 0 .0 , 1 .0 )]
data_type: •double*
data_type_: 11
debug: 0
debug」 0
global一
warning_display:
global—warning—display— :
m一time: 44927
number_ofj3〇
ints: 3
reference_count: 1
array([[ 1., 0., 0.],
[ 0 . , 1., 0 .],
那么它也同时能够接受数组和列表:
M ayavT
t r ia n g le s = n p . a r r a y ( [ [ 0 , 1 , 3 ] ,[ 0 , 3 , 2 ] “ 1 , 2 , 3 ] “ 0 ,2 ,1 ]])
values = np.array([l.l, 1.2, 2.1, 2.2])
mesh = tvtk.PolyData(points=pointsJ polys=triangles)
mesh.point_data.scalars = values
print repr(mesh.points)
print repr(mesh.polys)
print mesh.polys.to一array()
print mesh.point_data.scalars,to_array()
[(0 .0, 0 .0, 0 .0), (1.0, 0 .0, 0 .0) , ( 0 .0, 1 .0, 0 .0), (0 .0, 0 .0, 1 .0)]
<tvtk.tvtk_classes.cell_array.CellArray object at 0x0A2E5360>
[ 3 0 1 3 3 0 3 2 3 1 2 3 3 0 2 1 ]
[ 1.1 1.2 2.1 2 .2]
8.4 TVTK可视化实例
478
由于篇幅所限,本 书 不 对 V T K 库的用法进行详细解释。在本节,我们将对几个比较典型
的实例进行分析。希望读者在学>』
这几个实例之后能够融会贯通,掌 握 V T K 幵发的一般流程
以及使用 T V T K 带来的便利。
从 V T K 的网站可以下载大盘的C ++和 T C L 的程序实例,由于 T V T K 的用法更加简洁,读
者应该能很容易将它们转换成使用T V T K 库 的 Python 程序。
在本节的可视化实例程序中,使 用 了 本 书 提 供 的 辅 助 模 块 scpy2.tvlk.tvtkhelp。其中的
ivtk_scene()使用 ivtk in[示一组 Actor 对象。
8 . 4 . 1 切面
展示三维数据的一个比较简单的方法是使用切面,对切面经过的数据进行可视化,这样就
把三维数据可视化问题转换成了二维数据的可视化。通过交互式地修改切面的位置和方向,用
户能直观地对三维数据进行观察。下面是使用切片工具观察数据的一个实例,效 果 如 图 8-14
所示。
图8 - 1 4 使用切面观察StmcturedGrid数据集
它由三个部分组成:一个曲而、一个平面和一个外框。在曲面和平而上,每个点使用颜色
表示对应数值的大小。由于我们使用ivtk 显示可视化结果,冈此可以使用界而左侧的流水线浏
览器观察组成整个场景的流水线。
# 读入数据
plot3d = tvtk.MultiBlockPL0T3DReader( O
xyz_file_name = "combxyz.bin",
q_file_name = "combq.bin",
scalar_function_number = 10 0 , vector_function_number = 200
)
plot3d.u p d a t e () ©
return plot3d
plot3d = read 一
d a t a ()
grid = plot3d.output.get_block(0) ©
# 创建颜色映射表
lut = tvtk.LookupTable() O
lut.table = pl.cm.cool(np.arange(0,256))*255
T V T与
下而先看看曲而的制作过程。〇为了对可视化的方法进行重点介绍,我们直接使用一个
K
需要调用其 i丨
pdate〇方法。运行此方法之后,© 就可以通过其 output属性获取读入的数据集对象
丁数据的三维可视化
print t y p e(plot3d.output)
print t y p e ( p l o t 3 d .o u t p u t .g e t _ b l o c k ( 0 ) )
print grid.points.to_array()
cell arrays: 0
point a r r a y s : 4
arrays name:
Density
Momentum
StagnationEnergy
Velocity
# 显示S t r u c t u r e d G r i d 中的一个N 格而
plane = tvtk.StructuredGridGeometryFilter(extent = (0, 100, Q, 100^ 6, 6 )) O
plane.set_input_data(grid) ©
plane_mapper = tvtk.PolyDataMapper(lookup_table = lut, input_connection =
plane.output_port) ©
T V T K 库 没 有 将 SetInputData()转 换 成 input_data属 性 , 因 此 需 要 调 用 与 之 对 应 的
A set_input_data〇函数来设置输入数 据 集 。
plane .update()
p = plane.output
typ e (p)
t v t k .tv tk _c la s s e s .poly _data. PolyData
卜'面查看这个PolyData 对象的一些属性:
T V T与
K
print p .number_of D
j oin ts , g r id .dimensions[0] * g r id .dimensions[1]
1881 1881
M ayavT
print g r id . dimensions
p o in tsl = g r id .p o in ts .to _array ().reshape ((25^33^57,3))
points 2 = p .p o in ts .to 一array ().reshape ((33,57,3))
np.a l l (p o in ts l [6] == points 2)
[57 33 25]
True
lut2 = tvtk.LookupTable()
lut2.table = p i .c m .c o o l ( n p .ar a n g e ( 0 >256))*255
cut_plane = tvtk.Plane(origin = grid.center, no r m a l = ( -0.287, 0 ,0.9579)) O
cut = tvtk.Cutter(cut_function = cut_plane) O
c u t •set 一input 一
data(grid)
cut_mapper = tvtk.PolyDataMapper(input_connection = cut.outputjDort, lookup_table = lut2)
cut_actor = tvtk.Actor(mapper = cut 一mapper)
T V T与
〇首先创建表示平面的 Plane 对象。它是一个经过 origin 属性表示的逆标点,法线方向为
K
normal屈性的尤限平而,通过这两个屈性可以唯一确泣平面。Plane 对象的输出是一个 PolyData
M ayav
对象:
丁数据的三维可视化
t y p e (p l a n e .o u t p u t )
cut.update()
c u t •o u t p u t •number 一
of 一points
2537
算得到的:
c u t •o u t p u t •point 一
d a t a •number 一
of— arrays
configure_input(outline, in p u t _ o b j )
outline 一
mapper = tvtk.PolyDataMapper(input 一connection = outline.output 一port)
outline 一
actor = make 一
outline(grid)
outline actor。
K
M ayavT
w i n .s c e n e .i s o metric_view()
数据的三维可视化
在界而左侧的流水线浏览器中,双击某个对象可以打开对应的编辑器,对其各种厲性进行
编辑。例如可以打开 Plane 对象的编辑器。在此编辑器中修改 Plane 对象的 original和 normal属
性 ,从 而 改 变 切 面 的 位 置 和 方 向 ,观 察 数 据 集 中 不 同 位 置 的 密 度 分 布 情 况 。而打开
StmcturedGridGeometryFiltei•对象的编辑器,可以修改|lll面切面的范围。图 8-15显示了这两个编
辑器,以及通过它们修改之后的切面。
Wm320p«nGljRtnderWWuiow ea
■ Op*nGLP»mttrO«viceAd«pter
Rtndeftr ,改鷀取的范a
• 9 Actor 双击.
*ftnictuftdGind
□ StmcturtdGfid
□ Lookuplkbl*
Op#〇
GlPf〇
p4fTy as
i Actor
P〇
^yO«taM«pp«f
* Cutttr « 改平®的方问
□ Str\Ktur«dGnd
PUn#
O tookupTabl«
Op«nGlProp«rty 兵讲o o n w U U 】
阁8-15 通过编辑器修改切而的位置和方向
8 . 4 . 2 等值面
T V T与
K
scpy2.Wtk.example_contours: 使用等值面可视化标量场
M ayavT
数据的三维可视化
contours = tvtk.ContourFilter()
c o n t o u r s .set_input_data(grid)
contours.generate 一
v a l u e s (8 , g r i d .p o i n t _ d a t a .s c a l a r s .range) O
mapper = tvtk.PolyDataMapper(input_connection = c o n t o u r s .output j D 〇
rt^
scalar_range = grid.point_data.scalars.range) ©
actor = tvtk.Actor(mapper = mapper)
actor.property.opacity = 0.3 ©
w i n .s c e n e .isometric_view()
的值:
Python 科学计算 (第 2 版)
print contours.get 一v a l u e (0 )
c o n t o u r s .set_value( 0 , 0 .2 1 )
0.197813093662
在这个例子中,同一个等值而上所有点的颜色是相同的。因为等值面上的标量值(流体密
度)相同,而对等值面进行着色时,缺省也使用标量值。有时候我们希望等值面的颜色由另外的
标量值决定。下面的程序演示了如何使用別的标量值对等值面进行着色,效果如图8-17所示。
plot3d = read_data()
p lot3d.a d d _ f u n c ti o n (153) O
p lot3d.u p d a t e ()
grid = plot3d.output.get_block(0)
contours = tvtk.ContourFilter()
contours.set— input 一
data(grid)
contours.set_value(0j 0.30) ©
T V T与
a r r a y ( 4 ) .range, €)
scalar_mode = "use 一point_field_data") O
IVlayav
m a p p e r .c olor_by_array_component("VelocityMagnitude"^ 0) ©
丁数据的三维可视化
w i n .s c e n e .isometr i c_ v i e w ( )
g r i d .point_data.get_array_name(4)
'V e l o c i t y M a g n i t u d e '
© 调 用 ContourFilter对 象 的 set_value〇,创建一个值为0.3的等值面。©设置映射器的标量
范刚属性 scdar_rmige, 将它设S 为新增加的数组的取值范丨i 彳。Oscalar_m ode 厲性决定映射器所
使用的标M 数椐类型,这里的”
uSe_ p〇im_ fidd _ data”
表示使用点数据中的数组。它有如下儿种
选择:
• "default " : 使用 point_data.scalars , 如果小存在就使用 cell_data.scalars。
T V T与
K
M ayavT
阁8 - 1 7 在等值而上丨丨]颜乜显示邛他标贵值
数据的三维可视化
8 . 4 . 3 流线
在前面的实例中,我们使用切面和等值面对流体的密度分布进行了可视化。空间中每一点
的密度可以用一个数值(标M )表示,因此可以将密度分布理解为一个标M 场。而流体在每一点
的速度是一个矢M , 因此速度的分布情况需要使用矢S 场来描述。木节介绍如何使用随机散布
的矢量箭头和流线对矢量场进行可视化,效果如图8-18所示。由此图可知,整个场景由4 个实
体构成:外框、随机散布的矢量箭头、表示流线源的球体以及流线。
图8 - 1 8 矢量场的可视化
Python 科学计算 (第 2 版)
下面的程序创建随机散布的矢量箭头。这些箭头的起始点是数据集中的点的坐标,箭头的
方向由点所对应的矢M 决定,而箭头的大小和颜色则由点所对应的标M 决定。木例中,箭头的
方向表示速度的方向,而大小和颜色则表示密度。箭头越大表示该点的标M 值(密度)越人,箭
头的颜色也同时表示标量值的大小,红色对应的标量值最小,而蓝色对应的标量值最大。
# 矢量箭头
mask = tvtk.MaskPoints(random_mode=True, on_ratio=50) O
mask.set_input_data(grid)
arrow_source = tvtk.ArrowSource() ©
arrows = tvtk.Glyph3D(input_connection = mask.output_port^ ©
scale_factor= 2 / n p . m a x ( g r i d .p o i n t _ d a t a .s c a l a r s .t o _ a r r a y ()))
scalar_range = grid.point_data.scalars.range)
arrows_actor = tvtk.Actor(mapper = arrows_mapper)
o 由于原始数据集中的点数很多,如果在所有点的位置都描绘箭头,将十分耗时并且无法
1 K 分众多的箭头。因此首先使用 M a s k P o i n t s 对象对数据集中的数据进行随机选収,某点被选中
print g r i d .number_of_points
mask.update()
print type(mask.output)
print m a s k .o u t p u t .number_of_points
print m a s k .o u t p u t .p o i n t _ d a t a .number_of_arrays
47025
〈class 't v t k .t v t k _ c l a s s e s .p o l y _ d a t a .P o l y D a t a '>
952
a r r o w s . u pd a t e ()
print arrow 一source.output .number_of_points # 个点 t"箭头有 31
还可以使用流线直观地观察矢量场。流线上每一点的切线方向就是矢量场在该点的方向。
下面是显示流线的程序:
center = grid.center
sphere = tvtk.SphereSourcG( O
T V T与
centen=( 2 > c e n t e r [1 ], center[ 2 ])> radius= 2 >
K
phi_resolution= 6 , theta_resolution= 6 )
Mayavi数
sphere_mapper = t v t k .Poly D a t a Ma p p e r (input _ c o nn e c t i o n = s p h e r e .o u t p u t j 3〇rt)
sphere_actor = tvtk.Actor(mapper=sphere_mapper)
—据 的 三 维 可 视 化
s p h e r e _ a c t o r .p r o p e r t y .s e t (
representation = "wireframe", c o l o r = (0 ,0 ,0 ))
# 流线
streamer = tvtk.StreamLine( ©
step_length= 0 .0 0 0 1 ,
integration 一
d i rection="forward _、
integraton=tvtk.RungeKutta4 〇) ©
streamer.set_input_data(grid)
s t r e a m e r .set_source_connection(s p h e r e .o u t p u t j 3 〇
rt)
tube = tvtk.TubeFilter( O
i n p u t _ c o nn e c tion=streamer.output j 3 〇
nt,
radius=0.05>
number_of_sides= 6 ,
vary_radius="vary_radius_by_scalar")
tube_mapper = t v t k . P o l yD a t a M a p p e r (
input_ c o nn e c t i o n = t u b e .output j 3 〇
nt,
s c a l a r _ r an g e = g r i d .p o i n t _ d a t a .s c a l a r s .range)
89
Python 科学计算 (第 2 版)
outline_acton = make_outline(grid)
w i n .s c e n e .isometric_view()
边(line)数 为 2 3 , 而而数为0:
M ayavT
streamer.update()
数据的三维可视化
print s t r e a m e r .o u t p u t .number_of_lines
5528
0
23
O 使)IJ TubeFiltei•对象可以将流线路径转换为有粗细的[Ml管。radius属性为呦管的粗细,而
number_of_s id e 属性指定[Ml管的切面N 的边数。丨
Ml管的粗细可以根据点的数据发生变化,这里
使用nvary_jadius _by_scalar''指定圆管的粗细由标董(密度)决定。
TubeFilter对象的输出也足 PolyData 数据集,它由众多的而构成,但足它的 number_〇L p 〇lys
属性却等于0:
tube.update()
t u b e .o u t p u t .number_of jDolys
print t u b e .o u t p u t .number_of_strips
t = tube.output.get_cell( 0 )
/i
49
\
print t y p e ⑴
138
498
上而的例子中,箭久•的大小和颜色、流线的粗细和颜色所表示的是流体的密度。有时候我
们希望用这些可视化元素表示矢量的长度,即流体的速度的大小。我们可以直接计算 vectors
数组中各个矢量的长度,并且将其写入数据集的s c a la r 数组中。例如,在读入数据之后如下添
加两行程序:
point_data = g r i d .point_data
p o i n t _ d a t a .scalars = n p .s q r t (n p .s u m ( p o i n t _ d a t a .v e c t o r s .t o _ a r r a y ()** 2 ^ axis=-l))
T V T与
K
grid 是数据集对象,而 vnorm 为 Algorithm 对象。为了让程序兼容这两种不同的输入类
M ayavT
型,可以使用 tvtk.common.configure_ input()。
数据的三维可视化
vnorm = t v t k . V e c to r N o r m ()
vnorm.set_input_data(grid)
8 . 4 . 4 计算圆柱的相贯线
图8 - 1 9 两个互相垂直的圆管(左),打通圆管并显示相贯线(右)
Python 科学计算 (第 2 版)
首 先 定 义 make_uibe()函数,它创建指定方向和大小的圆管,生成圆管的流水线如阁8-20
所示。
阁 8-20生成圆管的流水线
tr.rotate_x(rx)
tr.rotate_y(ry)
tr.rotate_z(rz)
tfl = tvtk.TransformFilter(transform=tr> input_connection=trianglel.output j 〇
rt)
3 €>
tf2 = tvtk.TransformFilter(transform=ti% input 一connection=triangle2.output_port)
bf = tvtk.BooleanOperationPolyDataFilter() O
bf.operation = "difference"
b f •set— input— c o n n e c t i on (0 , t f l .o u t p u t _ p o r t )
b f .set_input_connection(1 , t f 2 .o u t p u t j 3 〇
rt)
m = tvtk.PolyDataMapper(input_connection=bf.outputjDort^ scalar_visibility=False)
a = tvtk.Actor(mapper=m)
a.property.color = 0 . 7 , 0.7^ 0.7
tubel, tubel_actor, tubel_outer, tubel— inner = make— tube(5, [1, 0.8], 32)
w i n •s c e n e •isometric 一view()
上面的程序创逑了两个表示[Ml管 的 PolyData 对 象 tu b e l 和 tube2 , 以及构成这两个|M|管的外
_ 柱和内[Ml柱 PolyData 对象:tubel_outer、tubel_inner、tube2_outer 和 tube2_inner。
〇使 CylinderSource 创連两个丨柱面,它 的 output属性楚表示圆柱面的 PolyData 对象。该
PdyD ata 对象中的每个面都逄四边形,
为了后续的布尔运算能正确运行,
©需 要 通 过 TriangleFilter
将其转换为完全由三角形而构成的PolyData 对象。
© 使 用 TransformFilter 对 P o l y D a t a 对象进行旋转,它 的 t r a n s f o r m 属性是一个描述旋转、偏
由 图 8-19( 左)可以看到两个圆管互相并不是相通的。下面我们使用布尔运算将两个圆管相
交的部分打通。这相当于用圆管1减去圆管2 的内圆柱,以及用圆管2 减 去 圆 管 1 的内圆柱。
这 个 操 作 仍 然 使 ) B o o l e a n O p e r a t i o n P o l y D a t a F i l t e r 来完成。为了高亮显示两个N 管相交的曲线,
JIJ IntersectionPolyDataFilter 计算丨M 丨管1 和 IM丨管2 的相交线,图 8 - 2 1 是这部分的流水线:
T V T与
K
M ayavT
数据的三维可视化
阁8 -2 1 计算相货线的流水线
下面是具体的代码,〇为了淸晰显示相贯线,将颜色设置为红色,并将线宽设置为两个
像素:
def difference(pdl, pd 2 ) :
bf = tvtk.BooleanOperationPolyDataFiltGr()
bf.operation = "difference"
b f .set_input_connection(l, pd 2 .output_port)
m = t v t k . P o l y D a t a M a p p e r ( i n p u t_ c o n n e c t i o n = b f . ou t p u t _ p o r % scalar_visibility=False)
a = tvtk.Actor(mapper=m)
return bf, a
ipd = t v t k .IntersectionPolyDataFilten()
ipd.set_input_connection( 0 , p d l .o u t p u t _ p o r t )
m = tvtk.PolyDataMapper(input 一connection=ipd.output_port)
a = tvtk.Actor(mapper=m)
a.property.diffuse_color = 1 .0 , 0 , 0 O
a.property.line_width = 2 .0
return ipd, a
tubel_hole_actor.property.opacity = 0 .8
t u b e 2 _hole_actor.property.opacity = 0 .8
w i n .s c e n e .is 〇
T V T与
fnetric_view()
K
线,还需要做进一步处理。
丁数据的三维可视化
print i n t e r s e c ti n g _ l i n e .o u t p u t .l i n e s .number_of_cells
1624
1650
dist = d i s t . r a v el ()
print n p . s o r t ( di s t ) [: 1 0 ]
3.72555819e-06 3.72555819e-06]
cpd.update()
c p d .o u t p u t .p o i n t s .number_of jDoints
1578
def conn e c t _ li n e s (l i n e s ):
edges = defaultdict(set)
for s, e in lines.to_array().reshape(-l ,3 ) .tolist():
T V T与
edges[s].add(e)
K
edges[e].add(s)
M ayav
while True:
丁数据的三维可视化
if not edges:
break
e = poly[-l]
neighbours = edges[e]
if not n e i g h b o u r s :
break
n = neighbours.pop()
try:
e d g e s [n ].r e m o v e (e )
except:
pass
poly.append(n)
yield poly
points = cpd.output.points.to 一a r r a y ()
P y th o n 科学计算(第 2 版)
x, y, z = points[line].T
ax.plot(x, y, z, l a b e l = 'parametric curve')
ax.auto_scale_xyz([-l, 1 ], [- 1 , 1 ], [- 1 , 1 ])
T V T与
K
Mayavi数
图 8-22用 matplotlib绘制提収出的相贯线
- 据的三维可视化
8 . 5 用 mlab快速绘图
虽 然 V T K 可视化软件包的功能很强大,Python 的 T V T K 库也很方便简洁,但是用这些工
具快速编写实用的三维可视化程序仍然是非常具有挑战性的。因 此 甚 于 V T K 幵发出了许多可
视化软件,例如 ParaView 、VTKDesigner2、Mayavi2 等。
M a y a v i 2 完全用 P y t h o n 编写,它不但是一个方便实用的可视化软件,而且可以用 P y t h o n 编
只耍将数据准备好,通常只需要调用一次 m l a b 模块的绘图函数,就可以看到数据的三维品示
49
效果,非常适合在 IPython 中交互使用。
8 . 5 . 1 点和线
plot3d(x, y, Zj …)
plot3d(x, y, z, s, ...) # s 为保存每个点对应的标量值的数组
points3d(Xj y, z . .. )
points3d(x, y, z, s , … )# s 为保存每个点对应的标量值的数组
points3d(x, y, z, f, ...) # f 为计算每个点对应的标:lfl:值的函数
x 、y 、z 参数决定了三维空间中每个点的坐标,而每个点所对应的数值则II丨以川点的颜色、
T V T与
大小、线的粗细等直观地表现。
K
每个函数还有许多关键字参数用于设置各种绘图属性,例如点的形状、线的宽度、颜色、
Mayavi数
颜色映射等。所有这些参数能够设置的绘降丨属性,都可以在显示出阁形窗口之后,在流水线对•
话框中交互式地修改。因此木书不对这些参数进行详细讲解,读者可以阅读函数文档,了解每
—据 的 三 维 可 视 化
个关键字参数的含义。
我 们 以 plot3d〇绘制洛伦茨吸引子轨迹为例,介绍如何绘制三维空间中的llll线 ,并且使用
流水线对话框对图形的各种屈性进行调整。下面是绘制洛伦茨吸引了•的程序,算法请参照 SciPy
的相关章节,缺省的绘图结果如图8-23所示。
•1
阁8-23 plot3d〇绘制的洛伦茨吸引子,llll线使用很细的圆管绘制
def lorenz(w, t, p, r, b ) :
x, y, z = w
X, Y, Z = trackl.T
mlab.show()
标量值数组,冈此轨迹上每个点所对应的标量值就是到达此点的时间。tube_radius 参数设置曲
K
线的粗细,曲线实际.丨
:采用极细的岡管绘制。
Mayavi数
按 “C ”键切换到此模式。
•角 色 模 式 :通过鼠标可以修改场景中物体的方向和位置,按 “A ”键切换到此模式。
在照相机模式下:
♦ 旋转 场 景 :鼠标左键拖动或用键盘的方向键。
•平 移 场 景 :鼠标中键拖动或按住Shift 键并使用左键拖动,或者使用 Shift+方向键。
• 缩 放 场 景 :鼠标右键上下拖动或使用“+ ”和 按 键 。
• 滚 动 照 相 机 :按 住 C trl 按键并用左键拖动。
窗口中的工具栏还提供了从坐标轴的6 个方向观察场景、等角投影、切换平行透视和成角
透视等功能的按钮。
8.5.2 M ayavi的流水线
工具栏中最左边的图标是打开Mayavi pipeline对话框的按钮,单击此按钮将弹出如图8-24
所示的对话框。左侧用树状控件显示了构成场景的流水线。此流水线是 M ayavi 在 T V T K 的流
水线之上进行包装的结果。选中流水线中的某个对象之后,窗口右边的部分将显示设置选中对
象用的界而。让我们看看 pl〇
t3d〇生成的流水线中都有哪些对象,如 图 8-24所示:
a
49
图8-24 plot3d()绘制的洛伦茨吸引f •的流水线对话框
M a y a v i S cene: 处于树的最顶层的对象表示场景。在其配置界面中可以设置场景的背景和
T V T与
前景色、场景中的灯光以及其他一些选项。例如,将 背 景 色 “ B a c k g r o u n d ” 改为灰色,将前景
K
色 “ Foregrmmd” 改为白色。也可以用下而的程序获取场景对象的背景色:
M ayavT
s = mlab.gcf() # 首先获得当前的场景
数据的三维可视化
print s
print s .s c e n e .background
L in e S o iirc e : 线数据源。在其配置界面中,笫一项为每个点所对应的标量数据的名称,在
本例中只有一个名为s c a l a r 的标量数据,它就足我们传递给 plot3d〇的第 4 个数组:表示轨迹中
每点的时间的数组t 。下面的语句从场景中获取LineSource 对象,并且获取其中的各种数据:
stripper = sour c e . c hi l d r e n [0 ]
print s t r i p p e r .f i l t e r .maximum_length
1000
3000
<tvtk.tvtk_classes.poly_data.PolyData object at 0x0CD527B0>
并查看输出对象的类型:
K
manager = t u b e . c h i l d r e n [0 ]
m a n a g e r •scalar 一l u t _ m a n a g e r •lut_mode = 'Blues'
surface = m a n a g e r . ch i l d r e n [0 ]
s u r f a c e .a c t o r .p r o p e r t y .representation = 'wireframe'
surface.actor.property.opacity = 0 .6
修改之后的场景如图8-25所示。
图 8 - 2 5 在流水线对话框中修改了许多配咒之后的洛伦茨吸引子轨迹
T V T与
下而是用程序配置这些属性的步骤:
K
⑴ 先 获得场景对象,例如用 mlab.gcf 〇。
M ayavT
(2) 通过每个对象的 children 属性,在流水线中找到需要修改的对象。
(3) 当其配置窗口有多个选项卡或多个配置分组框时,意味着其属性可能需要一级一级地
数据的三维可视化
获得。对象的屈性名和界面上的文字之间有很简中.的转换关系:首字变大写、下划线变空格。
例如:Surface 对象的 Actor 选项卡屮的 Property分组框屮的"Line width"选项,用程序描述就是:
surface.actor.property.line_width
2.0
M ayavi 还提供了脚本录制功能,以方便我们编写配置各种屈性的程序。单击流水线对话框
的工具栏屮的红色圆形图标即可开始脚本录制,并且打幵•_•个脚本对话框。之后的界而配置操
作,都会被记录到此脚本对话框中。
8 . 5 . 3 二维图像的可视化
三维空间中的曲面可以用surf()绘制,它实际上是将二维图像(数组)绘制成三维的曲面,用
曲面的高度表示图像中每点的值。下而的程序绘制图8-26所示的曲而。
x, y = np.ogrid[-2:2:20j, -2:2:20j] O
z = x * np.exp( _ x**2 _ y**2) ©
501
Python科学计算(第 2 版)
T V T与
图8-26 surfO绘制的III丨而及流水线对话框
K
它的输出是一个T V T K 的 ImageData对象:
data = m l a b . g c f () . c h i l d r e n [0 ]
img = d a t a . o u t p u t s [0 ]
img
通过上而的分析可以看出,SUlfO的功能是将一个二维图像转换为三维空间中的曲而。因此
丨
111面上每个点的 X 、Y 轴的坐标都是通过网格配置计算出来的。
由于丨III面的高度和其 X -Y 平而上的尺寸可能相差很大,因此流水线中,在 Array2DSource
对象的下面是一个W aipSoilar 对象,它将输入数据沿着 Z 轴方向进行缩放,可以看到其配置面
板中的’’Scale factoi•”
为 2,它是由 surf()的 warp_ scale 参数决定的。WaipScalar对象的输出是一个
PolyData 对象:
d a t a . c h i ld r e n [0 ].o u t p u t s [0 ]
<tvtk.tvtk_classes.poly_data.PolyData at 0xl4263720>
流水线I 1剩 下 的 对 象 请 读 者 己 研 究 ,
通过研究流水线可以了解M ayavi 内部的组织构造,
T V T与
这有助于我们创建自己的流水线以对复杂的数据进行可视化。
K
如果数据在三个坐标轴上的范围相差很大,在进行可视化时耑要调整坐标轴的显示比例,
M ayavT
以达到更好的可视化效果。例如在下面的曲面闲数中,X 轴方叫需要更大的显示范围:
数据的三维可视化
x, y = np.ogrid[- 1 0 :1 0 :1 0 0 j, -l:l: 1 0 0 j]
z = np.sin(5*((x/10)**2+y**2))
如果直接使用数据的范围进行显示,效果如图8-27(左)所示,虽然可以很直观地看出X 轴
的显示范围是 Y 轴 的 10倍,但是很难观察曲面的一些细节信息。
mlab.surf(x, y, z)
mlab.axes()
通 过 surf()的 extent参数可以修改坐标轴的数据范围:
mlab.surf(x, y, z y e x t e n t = ( -l , l , -1 , 1 ^ -0.5^0.5))
m l a b .a x e s (n b _ l a b e l s= 5 )
mlab.surf(x, y, z, e x t e n t = ( -l , l , -1 , 1 , -0.5,0.5))
mlab.axes(ranges=(x.min(),x.max(),y.min(),y.max() ,
z.min ( ) , z. m a x ()),nb_labels=5)
03
Python 科学计算(第 2 版)
图8 - 2 7 修改坐标轴的显示比例
x, y = np.ognid[-2:2:20j, -2:2:20j]
K
mlab.imshow(x, y, z)
数据的三维可视化
mlab.show()
而 contour_surf〇和 surf〇的参数类似,但可以通过 contours参数指定等高线的数目或者等高
值的列表。下而的语句将曲而以2 0条等高线表示,如图8-28(右)所示。
mlab.contour_surf(xJy,zJwarp_scale=2Jcontours=20)
图8 - 2 8 丨
U i m s h o w 绘制图像(左),川 contour_surf绘制等高线(右)
在 用 siuf()绘制 丨
111而之后 , 在流水线对话框中对 S u r f a c e 对象进行如下配置,也可以实现和
contour_surf() —样的效来:
• 在1’
C o n t o u r s •’
选项卡中,勾选" E n a b l e C o n t o u r s "。
•勾 选 " A u t o contours" 选项,并且指定”
N u m b e r o f contours" 为 2 0 , 这样会自动产生 2 0 条
等高线。
• 也 可 以 不 勾 选 "Auto contours”
选项,然后手工添加等高线。
或者用下面的程序设置等高线,其 中 fa c e 为 surf〇返回的对象,也就是流水线中的Surface:
face.enable_contours = True
face.contour.number_of_contours = 20
8 . 5 . 4 网格面 mesh
d p h i, d th e ta = n p .p i/8 0 .0 , n p .p i/8 0 .0
p h i, th e ta = n p .m g r id [0 :n p .p i+ d p h i* 1 .5 :d p h i, 0 :2 * n p .p i+ d th e ta * 1 .5 :d th e ta ]
m0, ml, m2, m3, m4, m5, m 6 , m7 =
T V T与
y = r*cos(phi)
K
z = r*sin(phi)*sin(theta)
M ayavT
s = mlab.mesh(x, y, z) ©
数据的三维可视化
mlab.show()
图 8-29使用mesh函数绘制的3D旋转体
05
Python 科学计算(第 2 版)
X = [[-1,1,1,-1,-1],
[-1 ,1 ,1 ,-1 ,-1 ]]
y = [ 卜 1,-1,-1,-1,-1],
[ 1 , 1 , 1 , 1 , 1 ]]
z = [[1,1,-1,-1,1],
[1 ,1 ,-1 ,-1 ,1 ]]
a
T V T与
K
M ayavT
数据的三维可视化
1.001.00
阁 8-30组成立方体的各个而和顶点坐标
[
[(-1, -1, 1), (1, -1, 1), (1, -1, -1), (-1, -1, -1), (-1, -1, 1)],
[(-1 , 1 , 1 ), (1 , 1 , 1 ),(1 , 1 , -1 ), (-1 , 1 , -1 ), (-1 , 1 , 1 )]
]
为了理解方便,图中将上面的坐标点用字母表示:
[[a,b,c,d,a],
[e,f,g,h,e]]
坐标点之间的关系由其在数组中的位置决定,
因此下面的4 组坐标点构成4 个正方形平面:
a, b, f, e ->顶而
b, c, g, f ->左而
c, d, h, g ->底面
d, a, e, h ->右面
使 用 meshO 可以很方便地将二维平面上的|ll|线绕着对称轴进行旋转得到旋转面。下面的程
序 用 m e Sh〇绘制凼抛物线旋转之后得到的旋转抛物面,如 图 8-31所示。
T V T与
图8~31用 mesh〇绘制旋转抛物面
K
rho, theta = np.mgrid[0:l:40j,0:2*np.pi:40j] O
M ayavT
z = rho*rho ©
数据的三维可视化
x = rho*np.cos(theta) €
)
y = rho*np.sin(theta)
s = mlab.mesh(x,yJz)
mlab.show()
x, y = np.mgrid[-2 :2 :2 0 〕
_, -2 :2 :2 0 j] O
z = x * np.exp( - x* * 2 - y**2 )
z *= 2
c = 2 *x + y ©
pi = mlab.mesh(x, y, z } scalars=c) €
)
mlab.axes(xlabel='x、 ylabel='y', zlabel='z')
mlab.outline(pl)
mlab.show()
07
Python 科学计算(第 2 版)
图& 3 2 用 mesh〇绘制高度和颜色不同的曲而
8 . 5 . 5 修改和创建流水线
用 M f O 也可以绘制高度和颜色不同的ill酒 ,但是需要对流水线进行一些改动。首先使用
之前介绍的方法用 sinf()绘制刖面:
x, y = np.ogrid[-2 :2 :2 0 j, -2 :2 :2 0 j]
z = x * np.exp( - x* * 2 - y**2 )
我们需要为每个点添加一个新的标量值以控制它们的颜色。因此,首先获得流水线中
Array2DSource 对象内部的 ImageData 对象。Array2DSource 实际上是一个 ArraySource 对象,通
过查看其源代码可知它用image_data属性保存:所创建的 ImageData对象。
source = mlab.gcf().children[0 ]
print source
img = source.image_data
print repr(img)
<mayavi.sources.array_source.ArraySource object at 0xl27FCE70>
<tvtk.tvtk_classes.image_data.ImageData object at 0xl27C5960>
然 后 给 i m g 添加一个标量数组,并且命名为"color"。注意数组 c 的 第 0 轴 对 应 X 轴 而 第 1
轴对应 Y 轴 ,因此需要将其转置,而 mvel()方法则将二维数组变为一维数组:
c = 2 *x + y # 表示颜色的标量数组
amay_id = img.point_data.add_array (c.T .ravel ())
img.point_data.get_array(array_id).name = "color"
source.update()
source.pipeline一changed = True
T V T与
以看出二维数组z 的数据是按照第0 轴、第 1轴的顺序保存在 scalars数组中的:
K
print z[:3,:3] # 原始的二维数组中的元素
M ayavT
# ImageData中的标最值的顺序
print img.point_data.scalars.to_array()[:3] # 和数组 z 的第 0 列的数值相同
数据的三维可视化
[[-0.00067093 -0.00148987 -0.00302777]
[-0.00133304 -0.00296016 -0.00601578]
[-0.00239035 -0.00530804 -0.01078724]]
[-0.00067093 -0.00133304 -0.00239035]
通过下面的语句可以看到,PolyDataNormals输出的 PolyData对象的当前标量数组为数组z :
normals.outputs[0 ].point_data.scalars.to_array()[:3]
array([-0.00067093, -0.00133304, -0.00239035])
surf = normals.children[0 ]
del normals.children[0 ]
active一attr = mlab.pipeline.set一
active一attribute(normals, point一scalars="color")
active_attr.children.append(surf)
于是其后的颜色查询表将使用数组C 作为输入,从而使得曲面的高度和曲面的颜色分別使
用不同的数据进行描绘。最终的绘图效果和相应的流水线如图8-33所示。
V p»perip» a ft
等 灞 _ 4 丨〇奉丨丨汐 | DC ?T T « OH IT : .71 ® I Li
2 U m (t
Array^DSourt*
T V T与
d / W « rp S c* l« r
a P〇
*yO#?#N〇
rm#H
K
/
h / S«tActircAn/tbutt
M ayavT
彐办 and l»9endi
Vi SurtMe
9% Avei
数据的三维可视化
图8-33用 surf〇绘制高度和颜色不同的曲面
# 创建流水线上的后续对象
warp = mlab.pipeline.warp_scalar(src, warp_scale=2 .0 )
normals = mlab.pipeline.poly_data_normals(warp)
active一attr = mlab.pipeline.set一active一attribute(normals,
point_scalars="color")
surf = mlab.pipeline.surface(active_attr)
mlab.axes()
mlab.outline()
mlab.show()
直接创建流水线需要开发者对 M a y a v i 的流水线上的各种对象十分了解,因此建议读者首
先熟悉 M a y a v i 的界面操作以及各利对象的作用,然后通过录制脚本逐步学习。
8 . 5 . 6 标量场
前面介绍了如何对一维和二维数据进行可视化,下面看看三维数据的可视化问题。最简单
的三维数据就是三维图像,可以j|j一个三维数组表示。图像中每个点的三维坐标都细它在数组
中的下标决定,而每个点对应的标量值则是数纟11中对应元素的值。这种三维图像可以Wj 来描述
标量场,例如房间中的温度分布、材料的密度分布或者通过X 射线断层成像(C T )技术采集到的
人体的内部构造。
标量场有三种可视化方法:
• 等 值 而 :和二维阁像的等岛线类似,用标M •值相等的曲而,显示标M 场的形态。
•体 素 呈 像 :利用透明度和颜色直接呈现标量场的形态。
• 切 面 :通过对标量场进行切而处理,显示在某个切面上场的形态。
scpy2.tvtk.mlab_scalar_field: 使 用 等 值 面 、体 素 呈 像 和 切 面 可 视 化 标 量 场 。
我们用下面的程序计算一个有两个点电荷的电势场,两个点电荷分别位于(-1,0,0)和(1,0,0)
处。为了方便显示,只计算 Z 轴上(-2,0)区间的电势场:
使 用 contour3d() 可以快速绘制此电势场的等位面:
surface = mlab.contour3d(s)
这些属性也可以通过流水线对话框来修改,程序的绘制结果如图 8 - 3 4 所示。
Python 科学计算(第 2 版)
阁8-34用等值而可视化电势场
使用等值而对标量场进行可视化吋,外而的等值而可能会完全包含内部的等值而,观察不
到内部的状态。例如在本例中,如果将 Z 轴的计算范围改为(-2,2),并且不设置等值面透明度,
则无论绘制多少个等值面,都只能看到最外层的等值面。
T V T与
体素呈像法用每个点的颜色和透明度对整个标鼠场进行润色,从而能够呈现更多的信息。
K
体素呈像没有对应的函数,需要我们自己创建流水线:
M ayavT
field = mlab.pipeline.scalar一
field(s)
数
mlab.pipeline.volume(field)
据源用体素呈像进行可视化,效果如图8-35(左)所示。
由于电势强度随着距离的平方袞减,因此整体的润色效果并没有突出电势强的部分。为了
的
效果如图8-35(右)所示,它很清楚地表现出了电荷附近的电势情况。
维
可
■ ^ ||‘卜
视
化
图8-35用体素呈像可视化电势场:(
左)缺€
1'效果,(
右)通过vmin和 vmax 指定电势值的润色范围
512
还可以使用切片工具观察标量场在某个平面之上的数据,它通常和艽他的工具同时使用,
并且可以直接在三维场景中交互式地改变平面的位置和方向。下面的程序在流水线中添加一个
标量切而,在流水线中它是"〇)1〇1'5 01^咕611(^"的子节点。通过卩1〇116_01*161伽丨011参数指定切面的
法线方向为 Y 轴 ,即切面和 Y 轴垂直:
然后通过下而的程序设置切面工A 的一些属性:
切而工具的效果如图8-36所示,图中还显示了添加切而工具之后的流水线。在 3D 场景中
可以对切而工具进行如下操作:
•拖动切而的红色外框修改切而的位置。
•拖动切而的法线箭头修改切而的方向。
T V T与
•拖动法线和切面相交的灰色圆球,改变切面的旋转中心。
K
Uiysvi $ 〇
e〇e3
M ayavT
S celarfiald
^ Colors arxi l〇8»ods
9% Volume
数据的三维可视化
ScalarCulPtono
^ 7
图8-36用切面工具观察电势场
8 . 5 . 7 矢量场
如果场中每一点的属性都可以用矢量代表,那么这个场就是一个矢量•场。对于三维空间中
的场,每个点对应一个矢量,它 I±lX 、Y 、Z 轴上的三个分量组成。因此需要 3 个三维数组表
示矢量场,这些数组分别表示矢量在三个轴上的分量。下面以洛伦茨吸引子为例,介绍对矢量
场进行可视化的一些莶本方法。
scpy2.tvtk.mlab_vector_field: 使 用 矢 量 箭 头 、切 片 、等 梯 度 面 和 流 线 显 示 矢 量 场 。
Python 科学计算(第2 版)
下面的程序根据洛伦茨吸引子的公式计兑出X 、Y 、Z 轴三个方向上的速度分量u 、v 、w :
这三个速度分量构成•-个矢量场,
下面调用 quiver3d 〇将每个点的速度欠量用一个箭头表示:
vectors = mlab.quiver3d(x, y, z, u, v, w)
效果如图8-37(左)所示。由于矢量场数据的网格过密,无法看清矢量场的内部结构。此吋
可以州下丨丨11的语句修改 V e c t o r s 对象的一些属性以减少箭头的数量并增加箭头的长度,效果如
图8-37(右)所示。
^So 〇
n» 2
K
^ V«cto»«
数据的三维可视化
7,
图8-37用矢置箭头可视化矢置场
和标量场的切面工具一样,也可以对矢量场进行切面显示,这样可以观察矢量场在某个切
面上的形态:
src = mlab.pipeline.vector一ifeld(x, y, i) u, v, w)
cut_plane = mlab.pipeline.vector_cut_plane(src, scale_facton=3)
cut_plane.glyph.mask j3 〇
ints.maximum_number_ofj3 〇
ints = 1 0 0 0 0
cut_plane.glyph.maskj3 〇ints.on_ratio = 2
cut_plane.glyph.mask_input_points = True
还 可 以 通 过 欠 量 场 计 算 标 量 场 。下 L&J通 过 e x t m c t _ v e c t o r _ n o r m ( ) 在 流 水 线 111添加一个
E x t r a c t V e c t o r N o r m 对象,它将每个点所对应的欠量的长度设置为此点的标量值:
m a g n itu d e = m l a b . p i p e l i n e . e x t r a c t _ v e c t o r _ n o r m ( s r c )
于是可以对 magnitude表示的标量场绘制等值
surface = mlab.pipeline.iso_surface(magnitude)
surface.actor.property.opacity =0.3
T V T与
m la b .f lo w ( X j y , z, u , v , w)
K
图 8-38(右)是使用flow〇绘制的洛伦茨吸引子轨迹。以图中球体上的每点为初始点计算它们
Mayavi数
所对应的场线轨迹。流水线中的 Streamline对象有许多配置选项,请读者自行研究。
- 据的三维可视化
图8-38用矢量切面和等模值面可视化矢量场(左),用 flow〇观察轨迹(右)
8 . 6 将 TVTK和 Mayavi嵌入界面
M 然可以使用 T V T K 和 M a y a v i 提供的流水线控件修改流水线中各个对象的参数,但是在
完整的三维可视化程序中,除了三维场景之外,通常还需要许多界面控件用来帮助用户与场景
交互。T V T K 和 M a y a v i 都是建立在 Trait 库之上,因此可以很方便地使用T r a i t s U I 界面库。
Python 科学计算(第 2 版)
在下而的例子中,⑴户讨以通过如图8-39所示界面中的两个滚动条控制场M 中圆管的内外
半径.
^ Edit pf〇p«rt>vt |〇[}f3
T V T与
K
M ayavT
R>d4iil:04) (:
0.0 1.0 0/M26
数据的三维可视化
图 8-39将 TVTK场贵嵌入TmitsUI界血'
class TVTKSceneController(Controller):
def position(self, info):
super(TVTKSceneController, self).position(info)
self.model.plot() ©
class TubeDemoApp(HasTraits):
radiusl = Range(0, 1.0, 0.8)
radius2 = Range(0, 1.0,0.4)
scene = Instance(SceneModel^ ()) O
view = View(
VGroup(
Item(name="scene,,> editor=SceneEditor(scene_class=Scene))^ ©
HGroup("radiusl", "radius2"),
show_labels=False)>
resizable=True, height=500, width=500)
def plot(self):
rl, r2 = min(self.radiusl, self.radius2 ), max(self.radiusl, self.radius2 )
self.csl = csl = tvtk.CylinderSource(height=l^ radius=r2, resolution=32)
self.cs2 = cs2 = tvtk.CylinderSounce(height=l.lj radius=rl, resolution=32)
trianglel = tvtk.TriangleFilter(input_connection=csl.outputjD〇
rt)
triangle2 = tvtk.TriangleFilter(input_connection=cs2.outputj 3 〇
rt)
T V T与
bf = tvtk.BoolGanOperationPolyDataFilterO
K
bf.operation = "difference"
M ayav
bf•set一input_connection(0 , trianglel.output_port)
bf.set_input_connection(1 , triangle2 .output_port)
丁数据的三维可视化
m = tvtk.PolyDataMapper(input_connection=bf.output Djortj
scalar_visibility=False)
a = tvtk.Actor(mapper=m)
a.property.color =0.5, 0.5, 0.5
self.scene.add_actors([a])
self.scene.background = 1 , 1 , 1
self•scene•reset一zoom()
8.6.2 Mayavi场景的嵌入
V 3T 5C T ITi rC
Mayavi数
- 据的三维可视化
zo -i
21 b
AH
Btmm
IJ
图8^40三维标量场观察器
s c p y 2 . t v t k . e x a m p l e _ e m b e d _ f i e l d v i e w e r : 标 量 场 观 察 器 ,演 示 如 何 将 M a y a v i 的场景嵌入
D VD TraitsUI 界面。
import numpy as np
from traits.api import HasTraits, Float, Int, Bool, Range, Str, Button, Instance
from traitsui.api import View, HSplit, Item, VGroup, EnumEditor, RangeEditor
from tvtk.pyface.scene_editor import SceneEditor
from mayavi.tools.mlab_scene_model import MlabSceneModel
from mayavi.core.ui.mayavi_scene import MayaviScene
from scpy2 .tvtk import fix_mayavi_bugs
fix一mayavi_bugs()
class FieldViewer(HasTraits):
# 三个轴的取值范围
x0, xl=Float(-5), Float(5)
T V T与
y0, yl=Float(-5), Float(5)
z0, zl=Float(-5), Float(5)
K
points = Int(50) # 分割点数
Mayavi数
autocontour = Bool(True) # 是否自动计算等值而
v0 , vl = Float(0 .0 ), Float(1 .0 ) # 等值面的取值范围
- 据的三维可视化
contour = Range("v0", 0.5) # 等值面的值
function = Str("x*x*0.5 + y*y + z*z*2.0") # 标量场阴数
function_list = [
"x*x*0.5 + y*y + z*z*2 .0 ",
"x*y*0.5 + np.sin(2 *x)*y +y*z*2 .0 ",
"x*y*z",
"np.sin((x*x+y*y)/z)"
]
plot button = Button (u"描画_•)
scene = Instance(MlabSceneModel, ()) O
view = View(
HSplit(
VGroup(
"x0 "/.xl V y 0 ","yl V z 0 V z l " ,
Item(’ points’ ,label=u"点数") ,
Item( •autocontour•, label=u"自动等值•’ )
,
Item(•plotbutton、 show_label=False),
VGroup(
Item('scene',
editor=SceneEditor(scene_class=MayaviScGne)^ ©
Python 科学计算(第2 版)
resizable=True,
height=300,
width=350
Item('function',
editor=EnumEditor(name='function_list', evaluate=lambda x:x))^
Item( 'contour、
editor=RangeEditor(format="%l.2f"y
low_namG="v0", high_namG="vl")
) , show_labels=False
def _plotbutton一fired(self):
self.plot()
T V T与
K
def plot(self):
IVlayav
# 产生三维网格
丁数据的三维可视化
y, z = np.mgrid[ € )
self.x0 :self.xl:1 j*self.points,
self.y0 :self.yl:1 j*self.points,
self.z0 :self.zl:1 j*self.points]
# 根据函数计算标量场的值
scalars = eval(self.function) O
self.scene•mlab•clf() # 淸空当前场景
# 绘制等值平而
g = self.scene.mlab.contour3d(x, y, zy scalars, contours=8 , transparent=True) ©
g.contour.auto一contours = self.autocontour
self •scene •mlab •axes (figure=self •scene •mayavi一scene) # 添加坐标轴
# 添加一个X-Y 的切面
s = self.scene.mlab.pipeline.scalar_cutjDlane(g)
cutpoint = (self.x0 +self.xl)/2 > (self.y0 +self.yl)/2 j (self.z0 +self.zl) / 2
s.implicitjDlane.normal = (0 ,0 ,1 ) # x cut
s.implicit_plane.origin = cutpoint
self.g = g ©
self.scalars = scalars
# 计算标患:场的值的范刚
self.vO = np.min(scalars)
self.vl = np.max(scalars)
def _contour一changed(self): O
if hasattr(self, "g"):
if not self.g.contour.auto_contours:
self.g.contour.contours = [self.contour]
def —autocontour—changed(self): ©
if hasattr(self, "g"):
self.g.contour.auto_contours = self.autocontour
if not self.autocontour:
self.__contour_changed ()
T V T与
app = FieldViewer()
K
app•configure一traits()
M ayavT
OMlabSceneModel 表 不 Mayavi 的场];
〖
•模型,® 对应的场景控件为 MayaviScene。
数据的三维可视化
用户单击描绘按钮之后,将调用 pbt 〇绘图。© 首先计算三维标量场的网格,这里使用 mgrid
快速产生三维网格,其中的 xO 、xl 、y O 、yl 、zO 、zl 、points、function等都是模型类的 Trait属
性,可以通过界面上的控件直接修改这些属性的值。〇由于用户输入的三元函数是一个字符串,
这里用 eval〇对字符串进行求值,在字符串中可以使用x 、y 、z 等局域变量。
© 清空当前场娱之后,调用 mlab 模块中的 contour3d ()、axes()、pipeline.scalar_cut_plane〇等
在场景中添加等值而、坐标轴和切而。mlab 模块缺省对当前场景进行处理,如果应用程序有多
个场景,就需要分别在其中绘图时,通 过 figurc参数指定需要进行处理的场景,例如:
mlab.axes(figure=self.scene.mayavi_scene)
其中,
self.scene是 MlabSceneModel 对象,
mayavi_ scene属性是真正表示场以的 Scene 对象。
©最后更新模型对象的儿个属性,其中变量 g 是 c〇
ntour3d ()的返回值,它表示场景中的等
值面。self.v O 和 self.v l 是标量场的最小值和最大值,它们设置等值面滚动条的取值范围。
© 当 与 contour屈性相对应的滚动条控件(位于三维场景的下方)的值发生变化时,将调用
_contour_changed〇方法修改保存等值面数值的列表。
© 当 “自动察直”选择框控件改变时,在 autocontom•属性的事件监听函数_autocontour_changed()
中改变等值面对象g 的自动等值面选项。
解9 亭
OpenCV-图像处理和计算机视觉
OpenCV 是一套采用 C / C + + 编写的开源跨平台计算机视觉库,它提供了两套 P y t h o n 调用接
口。其 中 c v 2 模块是针对 € ^ 1 1 ( ^ 2 ^ 处 1 创 建 的 ,它 直 接 采 用 从 11111^ 的 数 组 对 象 表 示 图 像 。为
import cv2
from cv2 import cv
9 . 1 图像的输鳩出
9 . 1 . 1 读入并显不图像
让我们从读入并显示一幅阁像开始,在 I P y t h o n N o t e b o o k 中运行如下语句,将会看到一个
显 示 美 女 “lena” 的窗口 :
filename = "lena.jpg"
img = cv2 .imread( filename ) O
print type(img), img.shape, img.dtype
cv2 .namedWindow("demol") O
cv2 .imshow("demol"^ img) 0
cv2 .waitKey(0 ) O
<type 'numpy.ndarray'> (512, 512, 3) uint8
O 首 先 i m r e a d O 从指定的文件路径读入图像数据,它返回的是一个元素类型为u i n t 8 的三维
Python 科学计算(第 2 版)
数组。i m r e a d o 支持许多常用的图像格式,在不同的操作系统下它所支持的图像格式可能有所不
同。请读者阅读 C + + 文档以了解 i m r e a d 〇的详细信思。此外,O p e n C V 还提供了 imwr i t e 〇来将图
像数据写入文件。
为了方便用户快速观察图像处理的效果,O p e n C V 提供了一些简单的 G U I 功能。© 这里调
用 n a m e d W i n d o w 〇,创建一个名为nd e m 〇r' 的窗口,€)最后调用 i m s h o w ( ) 将阁像显示到所创建的
窗口中。i m s h o w O 的第一个参数是窗口名,第二个参数是表示阁像的数组。如果第一个参数指
定的窗口不存在,i m s h o w 〇会 ^ 丨动创建一个新窗口,因此这里也可以不调用n a m e d W i n d o w ()。
在 IPythonNotebook显示窗口时,waitKey(O )会阻塞运算核,因此无法再运行其他命令。
- 像处理和计
为了不阻塞运行,可以在新的线程中显示窗口,并等待窗口关闭,例如通过本书提供
的%% thread魔法命令。
IT -
机视觉
顺序的彩色图像转换为灰度图像。
9 . 1 . 2 图像类型
阁像中的每个像素点可能有多个通道,例如用单通道可以表示灰度阁像,而用红绿蓝三个
通道表示彩色图像,用 4 个通道表示带透明度(alpha)的彩色图像。通道的数值类型可以有多种
选择,例如通常的图像用8 位无符号整数表示,而医学图像可能会州16位整数表示图像数据。
因此像素点的类型跑通道数和数值类型决定。
例如前面的 i m g 是一个三维数组,形状为(512, 512, 3 ) , 第 0 轴为图像的高,第 1轴为图像
的宽,而 第 2 轴为图像的通道数。冈此图像 i m g 的宽为512个像素,高 为 512个像素,有 3 个
通道,即图像中的每个像素的颜色用三个数值表示。根 据 dtype 属性可知,每个通道的颜色值
都用一个字节表示。而灰度图像 img _g m y 则是一个二维数组,因为它只有一个通道。
在前而的例子中,没 有 设 置 imread〇的第二个参数,其缺省值为 I M R E A D _C O L O R ,使用
该参数读入的数据是三通道且每个通道8 比特的数组。第二个参数有如下候选值:
• I M R E A D _A N Y C O L O R :转 换 成 8 比特的图像,通道数由图像文件决定,注 意 4 通道图
像会被转换成三通道图像。
• M R E A D _ A N Y D E P T H : 转换为单通道,比特数由图像文件决定。
• I M R E A D _C O L O R : 转换为三通道、8 比特的图像。
• I M R E A D _G R A Y S C A L E : 转换成单通道、8 比特的图像。
o p e n c v图
• I M R E A D J J N C H A N G K ) : 使图像文件的通道数和比特数。
表 9-1显示了对各种通道数和比特数的图像文件使用上述标志读入之后的结果,其中文件
- 像处理和计筇机视觉
名的格式为: “通道比特数_通道数.png ”。
表 9-1 图像文件读入后的结果
IM R E A D _ A N Y C O L O R IM R E A D _ A N Y D E P T H IM R E A D _ C O L O R IM R E A D _ G R A Y S C A L E IM R E A D _ U N C H A N G E D
uint16 l.p n g
一 uint8、 lch uintl6、 lch uint8 、 3ch uint8、 lch uintl6、 lch
uint16_3.png uint8 、 3ch uintl6、 lch uint8 、 3ch uint8、 lch uintl6 、 3ch
uint16_4.png u intd 3ch uintl6、 lch uint8 、 3ch uint8、 lch uintl6> 4ch
uint8_1 .png uim8、 lch uint8、 lch uint8 、 3ch uint$> lch uint8、 lch
uint8_3.png uint8 、 3ch uint8、 lch uintSx 3ch uint$、 lch uinl8 、 3ch
uint8_4.png uinl8 、 3ch uint8、 lch uint8 、 3ch uint8、 lch uuu 8 n 4ch
9 . 1 . 3 图像输出
imwriteO将数组编码成指定的图像格式并写入文件,图像的格式由文件的扩展名决定。某
些格式有额外的阁像参数,例 如 J P E G 格式的文件可以指定画质参数,这些参数都以 I M W R I T E _
开头。阁像参数以[参数名,参数值,参数名,参数值,...1的形式传递给 imwriteO的第三个参数。
在下而的例子中,把 从 lenajpg读入的数椐以各种画质保存为不同的J P E G 文件:
当图像格式支持更高的通道比特数时,imwriteO会保持图像数组的精度。在下面的例子|丨I,
O 通 过 N u m P y 计箅出下面的二元函数的值:
(x2 - y 2)sin(^—
f(x, y ) = ------------ -------
d e f fu n c ( x , y y a ) :
r e tu r n ( x * x - y * y ) * n p . s in ( ( x + y ) / a ) / (x *x + y *y )
〇pencv图
img = ( i m g ^ _ r g b a [ 2 : : - 1 ] * n p .iin fo ( d ty p e ) .m a x ) .a s ty p e ( d ty p e ) ©
r e tu r n img
下 而 调 用 m a k e jm g ( ) 创 建 8 比 特 数 组 i m g j b i t 和 1 6 比 特 数 组 im g ^ l6 b it , 然 后 分 别 将 其 保
存 为 J P E G 格 式 和 P N G 格 式 的 图 像 。 由 于 J P E G 图 像 只 支 持 8 比 特 通 道 ,因 此 将 im g _ 1 6 b it 保 存
为 J P E G 图 像 吋 会 出 现 问 题 ,请 读 者 打 开 im g J 6 b it . jp g 来 查 看 结 果 。 而 P N G 格 式 则 支 持 8 比特
和 1 6 比 特 通 道 ,如 果 读 者 查 看 i m g J 6 b i t p i i g 的 文 件 属 性 ,就 会 看 到 它 的 “ 位 深 度 ” 为 4 8 比 特 ,
即三个通道并且每个通道16比特。
y , x = n p . o g r id [ - 1 0 :1 0 :2 5 0 j, -1 0 :1 0 :5 0 0 〕.]
img 一8 b i t = make一im a g e (x, y , 0 .5 ^ d ty p e = " u in t8 " )
im g _ 1 6 b it = make_image(x, y, 0 .5 , d t y p e = " u in t l6 " )
c v 2 . im w r it e ( " im g _ 8 b it.jp g " , im g _ 8 b it)
c v 2 . im w rite ( " im g J L 6 b it • j p g , im gJ3L6bit)
c v 2 . im w r it e ( " im g^_8bit. p n g ", im g _ 8 b it)
c v 2 . im w r it e ( " im g _ 1 6 b it. p n g ", im g _ 1 6 b it)
9 . 1 . 4 字节序列与图像的相互转换
im d e co d e () 「
lT 以 把 图 像 文 件 数 据 解 码 成 图 像 数 纟 11, im encode 〇则把图像数纟 11编 码 成 图 像 文 件
数 据 。 由 于 所 有 的 运 算 都 在 内 存 中 完 成 ,因 此 可 以 使 W 这 两 个 函 数 快 速 压 缩 和 解 压 图 像 。例 如
把从摄像头读入的图像编码之后通过网络传递给其他计算机。
在下面的例子中 , O |vf先 通 过 frombufferO创建一个和 y 符 中 png^ sti•共享内存的数组
png_data。© 然后调j|j imdecode()将 P N G 文件的数据解压成表示图像的数红[ img 。© 最后调川
imencodeO 将图像数组压缩成 J P G 数据,第一个参数是表示图像类型的扩展名。它返回两个值,
第一个值表示压.缩是否成功,第二个值为压缩之后的数据,它是一个形状为(N , 1)的 uim 8 数组。
O 调用数组的 tobytesO可以将数组中的二进制数据转换成字符串。
jPg-Str = jpg_data.tobytes〇 O
码之后的字符串数据传递给I m a g e 的 d a t a 参 数 ,I P y t h o n 会 将 字 符 串 数 据 直 接 嵌 入 N o t e b o o k 「
丨1,
o p e n e v图
然 后 凼 浏 览 器 将 其 显 示 为 图 像 ,效 果 如 图 9 - 1 所 示 。
—像 处 理 和 计 算 机 视 觉
res, jpg一data = cv2 .imencode("•jpg", img_8 bit)
Image(data=jpg_data.tobytes())
9 . 1 . 5 视频输出
通 过 VideoWriter类 可 以 将 多 个 N u m P y 数 组 写 入 视 频 文 件 。下 面 的 例 / •中以小同的参数调
用 m a k e 」m a g e ( ) 并 将 结 果 写 入 f m p 4 . a v i 文 件 。 O 视频文件的编码由4 个字符的F O U R C C 对象指
定 。 © 使 用 指 定 的 f o u r c c 创 建 V i d e o W r i t e r 对 象 ,其:第3 个参数为巾贞频,第 4 个参数为视频的宽
度 和 高 度 ,最 后 的 参 数 为 T m e 时 表 示 视 频 为 彩 色 。© 调 用 write 〇方 法 将 表 示 图 像 的 数 组 写 入 视
频 ,〇最 后 调 用 release〇方 法 关 闭 视 频 文 件 。
test_avi_output("fmp4.avi", "fmp4")
O p e n C V 自带的视频编码器无法设置码率之类的编码参数,如果读者希望控制视频文件的
画质,可以通过 fourcc代码指定操作系统中安装的 V F W 编码器,这些编码器通常都带有配置
界而用于设置编码参数。为了获取各种编码器对应的 fourcc代码,读者可以运行本书提供的
fburcc.py , 将弹出一个如图9-2所 示 的 “视频压缩”对话框:
264vfW configuration
teste
P r«i« t Tutiht^
o p e n c v图
曹| [ikkmT
C〇
f.¥%nho
ir〇
Lv^Mtcy fe id H co rt 144 r t f
- 像处理和计算机视觉
mu
iSm fmnttttftwj 6DQP)
C^uio1Cfv S从 MMlt _
Jd
Fp 广
f " C rttlt vtitfl
詹 mtt iKSrVI
I
rn 职 1 脱 1
Ou^qlMvU*
珍 libav
F1 财
ttJi
» I 1W R ( V •
Outputflt:
Cis«Mi4reo«ar (7
&Art IfifS
L^vOOvl^lU 40I4I<«7.13
图9-2编码选择以及x264编码设置对话框
在对话框中选择“x264vf\v ”,然后单击“配置”
按钮,
将打开图中显示的“x264vi\v configuration”
对话框,勾 选 “Zero Latency”,并 且 通 过 “Qimntizd•”设置画质。然 后 单 击 “O K ”和 “确定”
按钮完成设置。fourcc.p y 将输出与所选杼的编码器对应的fourcc代码:x264。
DVD
如果读者的计赏机中没有x264vf\v 编码器,可以通过下面的丨叫址下载安装程序:
http://sourceforge.net/projects/x264vfw /
x 264v f w 编 码 器 的 下 载 地 址 。
o p e n c v图
fsize = path.getsize(fn)
print "quantizer = {:02d}, size = {:07d} bytes".format(quantizer,fsize)
—像 处 理 和 计 笕 机 视 觉
quantizer = 01, isze = 5686272 bytes
quantizer = 10, isze = 2406912 bytes
quantizer = 20, isze = 0932864 bytes
quantizer = 30, isze = 0396288 bytes
quantizer = 40, isze = 0189952 bytes
9 . 1 . 6 视频输入
VideoCapture 类用于从视频文件或视频设备读入图像。在下而的例子中,使用它从视频文
件读入相关的属性和帧。O g e t 〇方法获得指定的属性,所有视频相关的属性名都在c v 模块中以
C V _C A P _P R O P _iT^ 。这里读入视频的帧频、总帧数、像素宽和高。© 调 用 read()方法读入一
帧图像,它返冋两个值:表示是否正确获得图像的布尔值和表示图像的数组。正常读入一帧图
像之后,当前帧自动递增。© 还可以通过 set〇方法设置当前帧,从而直接读取视频中指定位置
的图像。
video = cv2.VideoCapture("x264_ql0.avi")
print "FPS:", video.get(cv.CV__CAP_PROP_FPS) O
print "FRAMES:% video.get(cv.CV_CAP_PROP_FRAME_CCXJNT)
print "WIDTH: video•get(cv•CV 一 ACP—PROP 一
FRAME一IWDTH)
print " H E I G H T v i d e o •get(cv•CV_CAP_PROP_FRAME_HEIGHT)
print "CURRENT FRAME:% video.get(cv.CV_CAP_PROP_POS_FRAMES)
res, frame0 = video.read() ©
print "CURRENT FRAME:% video.get(cv.CV_CAP_PROP_POS_FRAMES)
video.set(cv.CV_CAP_PROP_POS_FRAMES, 50) ©
29
Python 科学计算(第 2 版)
当传递给 V i d e o C a p t u r e 的参数是整数吋,将打开该整数对应的视频设备。下面的程序从笔
者的笔记本电脑0 带的摄像头读収一帧图像:
camera = cv2.VideoCapture(0)
O penCV图
9 . 2 图像
O p e n C V 的酣象处理功能十分丰富,木节以二维卷积、形态学图像处理、颜色填充、去瑕
疵等为例简要地介绍O p e n C V 的图像处理功能。希望读者通过这些实例举一反三,能通过阅读
O p e n C V 的文档尝试更多的图像处理功能。
9 . 2 . 1 二维卷积
图像处理中最蕋本的兑法就是将图像和某个卷积核进行卷积,使用不N 的卷积核可以得到
各种不同的图像处理效果。O p e n C V 提供了 filter2D 〇来完成图像的卷积运算,调用方式如下:
其 中 src参数是原始图像,dst参数是目标图像。若 畨 略 dst参数,将创造一个新的数组来
保存图像数据。ddepth 参数用于指定 F!标图像的每个通道的数据类型,负数表示其数据类型和
原始图像相同。kernel参数设置卷积核,它将与原始图像的每个通道进行卷积计兑,并将结果
存储到目标图像的对应通道中。anchor参数指定卷积核的锚点位置,当它为默认值(-1,-1)时,
以卷积核的中心为锚点。delta参数指定在将计算结果存储到dst中之前对数值的偏移量。
filter2D 〇的卷积运算过程如下:
(1)对于图像 src中 的 每 个 像 素 点 让 它 和 卷 积 核 的 锚 点 对 齐 。
(2) 对于阁像 src中勹卷积核東抒的部分,计算像素值和卷积核的值乘积。
(3) 图像 dst中的像素点(x ,y )的值为上而所有乘积的总和。
显然当卷积核的尺寸很大时,上述方法的运算速度将会很慢。因此对于较大的卷积核,
filter2D ()将使用离散傅立叶变换相关的算法进行卷积运算。
下面的程序演示了使用不同卷积核对图像进行处理之后的效果,如 图 9-3所示。
kernels = [
(u"低通滤波器", np.array([[l, 1 , 1 ],[1 , 2 , 1 ],[1 , 1 , 1 ]])*0 .1 ),
O penCV图
(u"髙通滤波器",np.array([[0.0, -1, 0],[-1, 5, -1],[0, -1, 0]])),
(u"边缘检测",np.array([[-1 .0 , -1 , 8, -1 , -1 ]]))
- 像处理和计® 机视觉
]
index = 0
fig, axes = pi.subplots(1, 3, figsize=(12, 4.3))
for ax, (name, kernel) in zip(axes, kernels):
dst = cv2.filter2D(src, -1, kernel)
# 由于matplotlib的颜色顺序和OpenCV的顺序相反
ax.imshow(dst[:, :, ::-l])
ax.set_title(name)
ax.axis("off")
fig.subplots 一
adjust(0.02, 0, 0.98, 1, 0.02, 0)
ei邊t f j a * 纒逡:x b 螬綱
图9-3使用filtWDO制作的各种图像处理效ifi
Python 科学计算(第 2 版)
scpy2.opencv.filter2d_d e m o : 可 通 过 图 形 界 面 自 定 义 卷 积 核 ,并 实 时 查 看 处 理 结 果 。
有些特殊的卷积核可以表示成一个列矢撤和一个行矢量的乘积,这时只需要将原始閨像按
顺序与这两个矢M 进行卷积,所得到的最终结果和直接与卷积核进行卷积的结果相同。由于将
一个N x M 的矩阵分解成了两个N x 1和 lx M 的矩阵,因此对于较大的卷积核能大幅度地提高
计算速度。O p e n C V 提供了 sepFilter2D() 来进行这种分步卷积,调用参数如下:
其中 k e m e l X 和 kernel Y 分别为行卷积核和列卷积核。
下面的程序比较filter2D()和 sepFilter2D()
的计算速度:
img = np.random.rand(1 0 0 0 ,1 0 0 0 ) O
〇pencv图
〇首先随机产生一幅比较大的图像 i m g 。 © 调 用 g e t G a u s s i a n K e m e l ( ) 分别获得长度为 7 和 5
时间。
卷积核的尺寸越大,计兑时间的差别越大。请读者更改 r o w 和 col 的值,观察计箅时间的
差别。
由于卷积计算很常用,因此 O p e n C V 提供了一些高级闲数来直接完成与某种特定卷积核的
卷积计算。例如平均模糊 blur〇、高斯模糊 GaussianBlur〇、用于边缘检测的差分运算 Sobel〇和
LapladanO 等。关于这些函数的用法请读者f-|行参考 OpeiiCV 的文档,这里就不再举例了。
9 . 2 . 2 形态学运算
在 S d P y 的图像处理章节中,我们介绍过如何使用 S d P y 的图像处理模块进行形态学阁像
的处理。O p e n C V 中也提供了类似的处理功能。例 如 dilateO对图像进行膨胀处理,而 erodeO则
对图像进行腐蚀处理。另外,m 〇
iphologyEx()使用膨胀和收缩实现一些更高级的形态学处理。这
些函数都可以对多值图像进行操作,对于多通道图像,它们将对每个通道进行相同的运算。
dilate()和 erode()的调用参数相同:
其 中 src 参数是原始图像;kernel参数是结构元素,它指定针对哪些周围像素进行计算;
a n c h o i * 参数指定锚点的位置,其默认值为结构元素的中心;iterations 参数指定处理次数。
m o r p h o l o g y E x ( ) 的参数如下:
将结构元素的锚点与原始图像中的每个像素(x ,y )对齐之后,计兑所有结构元素值不为0
的像素的最大值,写入目标图像的(x ,y )像素点。而腐蚀运兑则是计算所有结构元素不为0 的像
素的最小值。
moiphologyEx ()的高级运算包姑:
• M O R P H _O P E N : 开运算,可以用来区分两个靠得很近的区域。算法为先腐蚀再膨胀:
dst= dilate(erode(src))〇
• M O R P H _C L O S E :闭运算,可以用来连接两个靠得很近的区域。算法为先膨胀再腐蚀:
dst=ercxle(dilate(src)) 〇
• M O R P H _G R A D I E N T : 形态梯度,能够找出图像区域的边缘。算法为膨胀减去腐蚀:
dst= dilate(src)- erode(src)〇
• M O R P H _T O P H A T : 顶帽运兑,兑法为原始图像减去开运算:dst = src-open(src)。
• M O R P H _B L A C K H A T : 黑帽运算,算法为闭运算减去原始图像:dst=close(src)-src。
下面的程序演示了上述形态学图像处理的效果。图 9 4 是界面截图。请读者通过此界面修
改结构元素、处理类型以及迭代次数等参数,并观察经过处理之后的图像,理解各种运兑的
公式。
阍9 4 形态学阁像处理演示界而
9.2.3 填充-floodFill
o p e n c v图
填充函数 floodFillO在图像处理中经常用于标识或分离阁像中的某些特定部分。它的调用方
式为:
- 像处理和计算机视觉
o p e n c v图
- 像处理和计® 机视觉
ISO 2CH
图 9 - 5 演示 floodFillO的填充效果
下面的程序演示了 floodFillO的用法,界面如图9-6所示。在图像上用鼠标左键点选填充的
种子点。通过界面上方的控件修改loDiff、upDiff和 flags等参数。
flnodKB CWmi
咖 撕 1
.uSliO • tt^ s II wi
TO: l 〇
F2: 10
FO:10
a細 ■
n :io
w :o
n ;255
JC抽
i9 y〇6
图 9 * 6 填充演示程序的界而截图
Python 科学计算(第 2 版)
演示程序中使用两个鮝加在一起的Axeslmage 对象显示阁像,下层显示原始图像,上层半
透明地显示填充之后的阁像,因此可以观察被填充区域的原始图像。floodFillO的 flags参数的选
项如下:
Options = {
u"以种子为标准-4 联通": cv2.FL00DFILL_FIXED_RANGE | 4,
u"以种子为标准- 8 联通": CV2.FL00DFILL_FIXED_RANGE | 8 ,
u"以邻点为标准-4 联通": 4,
u"以邻点为标准- 8 联通": 8
}
9.2.4 去瑕疵-inpaint
o p e n c v图
使 用 inpaimO可以从图像上去除指定区域中的物体,可以用于去除图像上的水印、划痕、
污渍等瑕疵。它的调用参数如下:
- 像处理和计算机视觉
今O O + ,B _
阁9-7使用i叩aim去除阁像中的物体
s c p y 2 . o p e n c v . i n p a i n t _ d e m o : 演 示 inpaint()的 用 法 ,用 户 用 鼠 标 绘 制 需 要 去 瑕 疯 的 区 域 ,
程序实时显示运算结果。
在木书提供的 i n p a i n t _ d e m o 程序中,
用鼠标绘制需要进行处理的区域之后,
可以修改“inpaint
半径”和 “inpaint 算法 ” 等设置,实时观察它们对处理结果的影响。如果选区过大,处理可能
需要较长时间,此时可以单击 “ 保存结果 ” 按钮,用当前的处理结果覆盖原始图像,并淸除选
区,以进行下一轮处理。
9.3图 像 顿
与 本 节 内 容 对 应 的 N o t e b o o k 为: 09-opencv/opencv-300-transfoims.ipynb
本节介绍一些常用的图像变换算法,其中包括:对图像中的像素坐标进行儿何变换、对像
素颜色进行转换、计算频域信息以及使用双目图像计算深度信息。
9 . 3 . 1 几何变换
我们可以对图像在二维平面上进行仿射变换,或者在三维空间中进行透视变换。仿射变换
相当于将二维平面上的每个坐标点与一个2 x 3 的矩阵相乘,得到新的坐标,而透视变换则是
与3 x 3 的矩阵相乘。原本平行的两条直线在经过仿射变换之后仍然是平行的,而经过透视变
换之后,它们就可能不再平行了。
OpenCV 中使用 w a r p A f f i n e O 对图像进行仿射变换,调用参数如下:
其 中 s r c 参数是变换的原始图像,d s i z e 参数为返回阁像的大小,返回阁像的像素类型和src
的相同。M 参数是仿射变换的矩阵,它是一个形状为 (2,3) 的数组。f l a g s 参数是内插方式,
b o r d e r M o d e 是外插方式,b o r d e r V a l u e 为诗贵颜色。关于这些参数的含义墙读者阅读O p e n C V 的
文档。
假设矩阵 M 的各个元素如下:
(aoo aoi b 0)
kaio all ^1/
那么仿射变换可以用下面的公式表示:
dst(a〇〇x + a01y + b〇/a10x + an y + bx) = src(x,y)
Python 科学计算(第 2 版)
阍 9 - 8 对阁像进行仿射变换
O s r c 为图9-8中三角形的三个顶点坐标,这三个点分别为图像的左上、右上和左下三个顶
点。© dst为这三个顶点经过仿射变换之后的坐标,图中川三个箭头连接仿射变换前后的坐标点。
© 调 用 getAffineTransformO得到仿射变换矩阵 m ,然后© 调 用 waipAffineO对 图 像 i m g 进行仿射
变换,结果图像的大小为原始图像的两倍,背景采用白色填充。
waipPerspective 〇和 w a r p A f f i n e 〇类似,也对阁像进行儿何变换,不过它是在三维空间中进行
透视变换,因此它的变换矩阵是3 x 3 的矩阵。
这个变换矩阵可以通过getPerspectiveTransform(src ,
dst)计算。
src 和 d s t 参数是变换前后的4 个点的坐标,
它们都是形状为(4,2 ) 的单精度浮点数数组。
下面的程序演示了这两个函数的用法,结果如图9-9所示。
src = np.array(
[[0, 0], [W - 1, 0], [W - 1, h - 1 ], [0 , h - 1 ]]^ dtype=np.float32)
dst = np.array(
[[300, 350], [800, 300],[900, 923], [161,923]],dtype=np.float32)
m = cv2.getPerspectiveTransform(src, dst)
result = cv2.warpPerspective(
img, m, (2 * w, 2 * h), borderValue=(255, 255, 255, 255))
O penCV图
- 像处理和计® 机视觉
图9-9对图像进行透视变换
为了便于直观地理解仿射变换和透视变换,谐读者运行下而的演示程序,界 面 如 图 9-10
所示。
scpy2.opencv.warp_d e m o : 仿射变换和透视变换的演示程序,可以通过鼠标拖茂图中
蓝色三角形和四边形的顶点,从而决定原始图像各个顶角经过变换之后的坐标。
图9-10仿射变换和透视变换演示程序
39
Python科学计算(第2 版)
9 . 3 . 2 重映身寸-remap
对于图像的各种变换都有一个共同特点:它们从原始图像上的某个位置取出一个像素点,
并把它绘制到目标图像上的另外一个位置。从原始坐标到目标坐标的映射不一定是一对一的关
系。O p e n C V 提供了一个通用的图像映射闲数remapO 来完成这种计算,其调用参数如下:
值选项。
- 像处理和计筇机视觉
def make_surf_map(funcJ r, w, h, d0 ):
"""计算曲面函数func 在[-r:r]范围之内的值,并进行透视投影。
视点高度为曲而高度的d0 倍+ 1 "•…
y, x = np.ogrid[-r:r:h * lj, -r:r:w * lj]
z = func(x, y) + 0 * (x + y) O
d = d0 * np.ptp(z) + 1 .0 ©
mapl = x * ( d - z ) / d @
map2 = y * (d - z) / d
return (mapl / ( 2 * r) + 0.5) * w, (map2 / (2 * r) + 0.5) * h O
def make_func(expr_str):
def f(x, y):
return eval(expr_str, np.—diet一,locals〇)
return f
def get_latex(expr_str):
import sympy
X,y = sympy,symbols("x,y")
env = {"x": x, "y": y}
expr = eval(expr_str, sympy.一diet一, env)
return sympy.latex(expr)
settings = [
( " s q r t ( 8 - x * * 2 - y * * 2 ) " , 2, 1 ) ,
("sin(6 *sqrt(x**2 +y**2 ))", 10 , 10 ),
( • • s in ( s q r t ( x * * 2 + y * * 2 ) ) / s q r t ( x * * 2 + y * * 2 ) " , 20, 0 .5 )
]
fig, axes = pi.subplots(1 , len(settings), figsize=(1 2 j 1 2 .0 / len(settings)))
o p e n e v图
img2 = cv2 .remap(
img, mapx.astype("f32"),mapy.astype("f32"), cv2.INTER—LINEAR)
- 像处理和计® 机视觉
ax.imshow(img2 [ ::-1 ])
ax.axis("off")
ax •set__title ("${}$". format (getJLatex (expr)))
f ig . s u b p lo t s _ a d ju s t ( 0 , 0 , 1, 1, 0 .0 2 , 0 )
图 9-11使用三维曲而和remapO对图片进行变形
程序中,先计兑指定范围内的网格x 、y , O 然后计兑出网格上每-点的曲面的高度 z 。©
通过曲面的高度范围和d O 参数决定观察点的高度。©然后利用投影变换公式,计兑出表示坐标
变换的两个数纟il m a p x 和 m a p y 。其中投影公式的示意图如图9-12所示。O 最后将 m a p x 和 mapy
的取值范围改为图像的范围之内。
P y th o n 科学计算(第2 版)
图 9-12投影公式示怠图
还 可 以 用 remapO 移动图像屮指定的区域,使用该算法可以让用户通过鼠标拖曳图像的局
部使其变形。在下面的例子中,(tx,ty)为鼠标拖曳的起始坐标,(sx ,sy)为拖曳的终点坐标,r 是
拖曳半径,半径越大被影响的像素越多。
为了方便程序的编写,我们将 remap()的 m a p l 和 m a p 2 两个参数分别分解为两个部分gridx
和 offsetx、gridy和 offsety。Ogridx 和 gridy为恒等映射,使用这两个数组不会对阁像产生任何
o p e n c v图
为坐标映射,就会将以起点坐标为中心、半 径 为 r 的圆形区域复制到终点对应的区域。O 为了
让变形更加柔和,我们使用 GaussianBlur〇对两个 offset数组进行高斯模糊处理,sigma 参数决定
模糊的程度,该值越大越模糊。
图 9-13显示了变形之后的效果,其中虚线圆表示拖曳的起始位置,实线圆表示拖曳的终点
位置。
o p e n c v图
- 像处理和计® 机视觉
图9-13使用rcmapO实现图像拖曳效果
9 . 3 . 3 直方图
for i in range(3):
hist, x = np.histogram(img[:,i],ravel(), bins=256, range=(0 , 256)) O
ax[0 ].plot(0.5 * (x[:-l] + x[l:]), hist, label=colors[i], color=colors[i])
O 通 过 histo g r a m 〇对 图 像 i m g 的三个通道分別进行一维直方图统计,丨
t丨于被统计的数姐必
须是一维的,因此这里调用数组的m v d ( ) 方法将二维数组转换为一维数组。通 过 m n g e 参数指定
O penCV图
数都变成了有两个元素的序列,
分别与两个数组相对应。
返回的统计结果 hist2 是一个二维数组,
其形状由 b i n s 决定。第 0 轴与第一个数组相对应,第 1 轴与第二个数组相对应。它是由两个一
维数组的对应元素所构成的二维矢量的分布统计结果。
观 察 图 9-14 ( 左)可知红色通道的值普遍较大,因此整个图片呈现暖色调;而从右图不但可
以得出红色通道的值比蓝色通道较大的结论,还可以看到几处分布比较密集的领域。例如其中
一块的中心坐标大约为(207, 1 2 5 ) , 这说明图像中红色值在 2 0 7 附近、蓝色值在 1 2 5 附近的像素
点较多。
O p e n C V 中的直方图统计函数为calcHist ()。它支持对多幅图像进行 N 维直方图统计,因此
其第一个参数为数组列表。下而使用 calcHist 〇对 i m g 的三个通道(0, 1,
2)进行三维直方阁统计。
每个通道的等分数分别为(30,20,10),所有通道的取值范刚都为(0,256)。返回结果 result是一个
形状为(30,20,10)的数组:
result = cv2.calcHist([img],
channels=(0 , 1 , 2 ),
mask = None,
histSize = (30, 20, 10),
ranges = (0, 256, 0, 256, 0, 256))
result.shape
(30, 20, 10)
1.直方图反向映射
计兑出直方图之后,可以用 c a l c B a c k P r q j e c t O 将图像中的每点替换为它在直方图中所对应的
值。于是在直方图中出现次数越高,图像中对应的像素就越亮。可以W 这利1方法找出图像中和
直方图相匹配的区域。下面用一个实际的例子加以说明:
o p e n c v图
img_hsv2 = cv2.cvtColor(img2> cv2.C0L0R_BGR2HSV)
- 像处理和计® 机视觉
img_bp = cv2.calcBackProject([img_hsv2], 0
channels=[0 , 1 ])
hist=result,
ranges=[0, 256,0 ,256],
scale=l)
img_th = cv2.threshold(img_bp, 180, 255, cv2.THRESH_BINARY) O
struct = np.ones((3, 3), np.uint8 )
imgjnp = cv2.morphologyEx(img_th, cv2.MORPH 一CLOSE, struct, iterations=5) O
阁9-15使用calcBackProject〇g •找图像中的橙子部分
程序的输出如图9-15所示。它在图像 fmits.jpg(上中图)中寻找和图像fruits_section.jpg(上左
图)的颜色近似的部分。
45
Python 科学计算(第 2 版)
〇首先载入颜色匹配的模板图像,并通过(MColorO 将图像的三通道的数据从蓝绿红变换为
色相、饱和度和明度。© 调 用 calcHist()对模板图像的色相与饱和度进行二维直方图计算。在色
相与饱和度空间进行颜色匹配,能够得到较好的匹配结果。€>为了后续的 calcBackPrqjectO计兑
不越界,这里将直方图的最大值缩小到255。图 上 右 )为 所 计 算 的 直 方 图 。
〇载入鬥标图像,然后也通过 cvtCdorO 进行颜色转换。© 调 用 calcBackProjectO将 F1标图像
中的每个像素的颜色变换为其在直方图中所对应的值。它的第一个参数是一个图像列表,hist
参数指定直方图,返回值是一幅单通道的形状和数值类型勹输入图像相同的阁像。channels、
ranges参数和 calcHist()的参数含义相同。图9-15(下左)为calcBackProject()的汁算结果。
© 调 用 threshold!:
)对 calcBackProject 〇的输出图像进行二值化处理,参 数 T H R E S H _ B I N A R Y
指定了二值化处理的方法,它将图像中值小于等于180的点都设置为0 , 将 大 于 180的设置为
255。图9-15(下中)为二值化的结果。
© 最后对二值化之后的图像进行形态学图像处理。使 用 5 次开运兑将图像十的分散的K 域
连接成一个大区域。图9-15(下右)为最终结果,它的内色区域正好对应目标图像中的橙子部分。
还可以对这个结果进行一些处理:例如使用闭运算消除一些杂点,然后找出图像中面积最大的
〇pencv图
区域。
这种方法也可以用于在视频中跟踪一个颜色鲜明的物体。在跟踪物体之前,首先对一幅物
- 像处理和计筇机视觉
体充满整个画而的图像进行直方图统计,然后对视频后续的帧进行calcBackPrqjectO计算。
2.直方图匹配
直方阁可以表示图像的颜色分布情况,而通过直方阁匹配算法可以将一幅阁像的直方阁分
布复制给另一幅阁像,从而让 H 标图像拥有原图像的直方图信息。-签本步骤如下:
〇计算原图像 src与目标图像 dst的归一化之后的直方图统计,
得到的结果为概率密度分布。
© 用 a m m m i O 对概率密度分布进行累加,得到累计分布。€)在原图像的累计分布中搜索目标图
像的累计分布所对应的下标index, 由于累计分布是递增函数,因此可以用 searchsortedO进行二
分查找。
〇调用 dip()将 index的取值范围限制在0〜 255之间, © 然后用它对0 标图像进行映射,
即将目标图像中每个像素值v 替换为 i n d e x M 。
for ch in (0 , 1 , 2 ):
hist一src, _ = np.histogram(src[:, ch], **kw) O
hist一dst, _ = np.h is t o g ra m(d st [c h], **kw)
cdf_src[ch] = np.cumsum(hist_src) ©
cdf_dst[ch] = np.cumsum(hist_dst)
index = np.searchsorted(cdf_src[ch] cdf_dst[ch], side="left") ©
np.clip(indexj 0, 255, out=index) O
res[:, :^ ch] = i n d e x [ d s t [ c h ] ] 0
hist_res, _ = np.histogram(res[:^ ^ ch], **kw)
cdf 一
res[ch] = np.cumsum(hist_res)
下而调用 hist〇
gram _ matCh ()将一幅秋景的直方图复制给夏景图像。图 9-16显示了对夏景图
进行直方图匹配处理前后的图像,图中下层的三个图表S 示图像的蓝绿红三个通道的累计分布
丨||)线。其中点线为处理之前S 景图的累计分布,实线为处理之后的累计分布,而虚线为秋最图
的累计分布。可以看到图屮实线与虚线几乎完全重合,因此完美地将秋景图的直方图复制给了
夏景图:
9 . 3 . 4 二维离散傅立叶变换
图像数据可以看作二维离散信号,对其进行二维离散傅立叶变换,能将K •转换为频域信号,
将原始图像分解为众多二维正弦波的鮝加。由 于 N u m P y 己经提供了二维离散傅立叶变换的函
数,冈此本节主耍使用N u m P y 的相关函数进行说明。
为了更好地理解本节所介绍的内容,需要读者掌握离散傅立叶变换相关的知识。在木书最
后一章有一维离散傅立叶变换的详细论述。
对一维的 N 点实数信号 x 进行快速傅立叶变换(F F T )之后,得到表示频域信号的 N 个复数
Python 科学计算(第 2 版)
的数组 X 。似是数据的信息量并没有增加,这是因为:
• 下 标 为 0 和 N /2的两个复数的虚数部分为0。
• 下 标 为 i和 N -i 的两个复数共轭,也就是虚数部分数值和同、符号相反。
同样,对于一 个 N *N 的二维实数信号 x 进行二维快速傅立叶变换之后,得到表示频域信
号 的 N * N 个复数元素的数组X 。其 中 X [i,j]和 X [N -i,
N -j]共辄,并 且 X [0,0]、
X [0, N /2]、
X [N /2,0]、
X [N /2, N /2]这 4 个元素的虚部为0。下面我们用程序验证一下:
x2 = fft.ifft2(X) # 将频域信号转换回空域信号
np.allclose(x, x2 ) # 和原始信号进行比较
True
频域信号中的每个元素都对应空域信号中的一个二维正弦波,如果只选择频域信号中的一
部分转换冋空域信号,就相当于对空域信号进行了滤波处理。下面演示将频域信号中的不冋K
域转换回空域信号之后的滤波效果,输出如图9-17所示。
首先载入一幅彩色图像,并将其转换为灰度图像。!
±1于F F T 运箅的最佳大小为2 的整数次
幕,因此使用 resizeO将图像的大小改为256*256。
然后计算图像 i m g 的频域信号 img _freq,由于它楚一个复数数组,为了能将其作为图像显
示,计算它的每个元素的模值,并取对数,得 到 数 组 imgjnag , 如图9-17(左上)所示。模值图
像 的 4 个角与低频信号对应,中心与高频信号对应。由于4 个角附近较亮,这说明原始图像的
低频成分较多,这符合一般阁像信号的规律。
为了更好地观察频域信号,
我们使用 ffishiftO对 img^ m a g 进行移位,
得到数组 img^ mag ^ shift。
图9-17(中上)为移位之后的模值图像。ftehiftO将两个对角线上的方块对调,即 1、3 象限对调,
2、4 象限对调。这样图像的中部与低频对应,而 4 角与高频信号对应。
N = 256
img = cv2.imread("lena.jpg", cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, (N, N))
img_freq = fft.fft2 (img)
img_mag = np.logl0 (np.abs(img_fneq))
54!
img_mag_shift = fft.fftshift(img_mag)
最后选择频域信号中的一部分,将其转换回空域信号。图 9-17的右上图和下排的图,分别
显示中上图中4 个矩形领域所对应的空域图像。
O m a s k 是-•个布尔数组,其形状和频域信号数组一样。© 将其中坐标在指定的矩形范围之
内的元素设置为 T m e 。© N 吋选择共轭对称的部分,否 则 通 过 iffi2()转换冋空域信号时虚部将
不会为0。〇通 过 ffishift〇对 m a s k 数纟11进行移位,使得它和频域信号 img _freq匹配。
© 接下来将频域信兮img^ freq与 m a s k 相乘,
得到在频域进行滤波之后的频域信号img _freq2
@ 然 后 调 用 将 imgJVeq 2 转换回空域信号。
scpy2.opencv.ffi2d_d e m o : 演示二维离散傅立叶变换,用户在左侧的频域模值图像上用
鼠标绘制遮罩区域,右侧的图像为频域信号经过遮罩处理之后转换成的空域信号。
filtered一results = []
for i, (x0 , yQ, xl, yl) in enumerate(rects):
mask = np.zeros((N, N), dtype=np.bool) O
mask[x0:xl + 1, y0:yl + 1] = True O
mask[N - xl:N - x0 + 1, N - yl:N - y0 + 1] = True @
mask = fft.fftshift(mask) O
img_freq2 = img_freq * mask 6
img_filtered = fft.ifft2 (img_freq).real ©
filtered_results.append(img_filtered)
9 . 3 . 5 用双目视觉图像计算深度信息
图9-18双0 视觉图像计箅深度示意图
- 像处理和计算机视觉
图 9-18中,两台照相机的焦距均为f,距离为B 。场景中目标点在左相机图像中的位置为x ,
在右相机图像中的位置为A 同一点在两幅图中的视差(disparity)为x - A 巾相似三角形町知两
照相机的连线的中心点到目标点的距离为:
B •f
由上而的公式可知视差越人,目标点到照相机的距离越小,读者可以试着轮流使用左右单
眼观察场景,可以发现距离越远的物体偏移量越小。
视差信息可甶 O p e n C V 提 供 的 S t e r e o S G B M 类计算。它有相当多的参数可供调节,为了获
得 较 好 的 效 果 ,需 要 仔 细 调 节 这 些 参 数 。下 面 的 程 序 屮 的 参 数 可 用 于 通 常 情 况 。
StereoSGBM .computeO 计算视差信息,得到的是•_•个双字节的整型数组,将其中的值除以16就
可以得到以像素为单位的视差数据。
img_left = cv2.pyrDown(cv2.imread('aloeL.jpg'))
img^right = cv2.pyrDown(cv2.imread('aloeR.jpg'))
stereojDarameters = dict(
SADWindowSize = 5,
numDisparities = 192,
preFilterCap = A,
minDisparity = -24,
uniquenessRatio = 1,
speckleWindowSize = 150,
speckleRange = 2,
displ2MaxDiff = 10,
fullDP = False,
PI = 600,
P2 = 2400)
stereo = cv2.StereoSGBM(**stereo_parameters)
disparity = stereo.compute(imgjeft^ img_right) ,astype(np.float32) / 16
若能完美地计兑视差数纽disparity,则能满兄等式:left_img[y ,
x〗= right_img[y ,
x+disparity[y,
x ]]。下面我们用前面介绍过的r e m a p O 将右眼图像中的像素移到与其对应的左眼图像的坐标之上,
结果如图9-19所示。其中,左图为直接将左右两幅图像叠加之后的结果,可以看出不同的位置
有不同的视差错位。中图以灰度阁像显示视差信息,颜色越浅表示视差越大,距离越近。右图
是将右眼图像经过 r e m a p O 处理之后再咎加到左眼图像之上,可以看到两幅阁儿乎完美地重合了。
o p e n c v图
h, w = img_left.shape[:2 ]
ygridj xgrid = np.mgrid[:h, :w]
- 像处理和计® 机视觉
ygrid = ygnid.astype(np.float32)
xgrid = xgrid.astype(np.float32)
res = cv2.remap(img_right, xgrid - disparity, ygrid, cv2.INTER_LINEAR)
下面设置焦距与相机间距的乘积B f , 根据视差信息计兑每个点在三维空间中的位置。该三
维坐标以两台照相机的连线为原点,连线方向为 X 轴 ,图像的上方为 Y 轴 ,照相机的前方为
Z _〇
Bf = w * 0.8
x = (xgrid - w * 0.5)
y = (ygrid - h * 0.5)
d = (disparity + le-6 )
z = (Bf / d).ravel()
x = (x / d).ravel()
y = -(y / d).ravel()
为了剔除一些噪声数据,我 们 只 示 距 离 相 机 在 3 0 以内的坐标点,每个点对应的颜色从
imgjeft图像获取。
poly = tvtk.PolyData()
poly.points = points #所有坐标点
poly.verts = np.arange(len(points)).reshape(-l, 1 ) #所有顶点与坐标点对应
poly.point_data.scalars = colors.astype(np.uint8 ) #坐标点的颜色,必须使用 uint8
m = tvtk.PolyDataMapper()
m.set—input一data(poly)
a = tvtk.Actor(mapper=m)
O penCV图
- 像处理和计® 机视觉
s c p y 2 . o p e n c v . s t e r e o _ d e m o : 使用双目视觉图像计算深度信息的演示程序
%gui qt
from scpy2 .tvtk.tvtkhelp import ivtk_scene
scene = ivtk_scene([a])
scene•scene.isometric一 view()
scene.scene.camera.position = (0 , 20, -50)
scene.scene.camera.view_up = 0 ^ 1 ^ 0
9 . 4 图像识别
O p e n C V 除了能够对图像进行各种处理和变换之外,还提供了大量的图像识别函数。本节
介绍一些常用的图像识别和分割算法。
9.4.1 用霍夫变换检测直线和圆
换ffl关的函数:
• H o u g h L i n e s : 检测图像中的直线。
• H o u g h L i n e s P : 检测图像中的直线段。
• H o u g h C i r c l e s : 检测图像中的N 。
K 而的程序演示了使用 H o u g h L i n e s P 〇和 H o u g h C i r c l e s 〇进行线段和岡的检测。必们的各种
参数均可以在控制而板中进行调整,运行界而如阁9-21所示。
s c p y 2 . o p e n c v . h o u g h _ d e m o : 霍夫变换演示程序,可通过界面调节函数的所有参数。
63
irnto
o p e n c v图
- 像处理和计算机视觉
丨繼蠢) 1丨 ICO
露0 1 鼸)u
m 1 !•0
c
鸶騰羣I 釁t i l I JIM
110
t«0
SAM
VI ' 八O O + • B _
llHm
图 9 - 2 1 用霍夫变换寻找图像中的直线和圆
.检 测 线 段
为了了解 H o u g h L i n e s P O 的每个参数的含义,让我们先学习直线霍夫变换的原理。
对于图像中的每条直线都可以用方程y = kx + m 表示。由于参数k 和直线与 X 轴的夹角之
间并不是线性关系,因此我们将直线方程改写为以直线到原点的距离r 和直线与 X 轴的夹角0 为
参数,如 图 9-22所示。
( c o s u0'
COS \ / r \
y = r ^ eJx + ^ J
图9-22用 1*和〇表示的直线
经过图像中某个白色的点(x 〇
,y 。
)的直线参数「
和0满足下面的关系:
o p e n c v图
r = x 〇•cos 0 4- y 〇•sin0
它是一条0 - r 空间中的正弦llll线。所谓霍夫变换,就是指对于原始图像中的每个白色点
- 像处理和计® 机视觉
(x 0,y 〇),绘制它们在e - r 空间中所对应的正弦曲线。众多正弦曲线的相交点(e w 。
)就是原始
图像中的一条直线。图 9-23是一个简单的例子。其中,左图中的4 个圆点构成一条直线,右图
中与它们对应的正弦曲线(图中的实线)相交于一点,而与三角点对应的正弦丨I丨彳线(虚线)则不经过
此点。
在实际计算时,我们使用一幅表示e - r空间的灰度图像作为累加器,用其中每个点对经
过此点的正弦曲线进行计数。然后通过阈值找出累加器中的所有峰值点,这些峰值点所对应的
0 - r坐标就是原始图像中的直线参数。
图9-23稂夫变换示意阁
HoughLinesP ()的调用参数如下:
越大,最后找出的直线的参数的精度越高,但是运算时间也越长。t h r c s h d d 参数是在累加器中
寻找峰值时所使用的阈值,即只有大于此值的峰值点才被当作与某条直线相对应。 由于
H o u g h L i n e s P 〇检测的是图像中的线段,冈 此 m i n L i n e L e n g t h 参数指定线段的最小长度,而
m a x L i n e G a p 参数则指定线段的最大间隙。当有多条线段共线时,间隙小于此值的线段将被合并
为一条线段。
H o u g h L i n a s P 〇的返回值是一个形状为(1,
N,4)的数组,其 中 N 为线段数,第二轴的4 个元素
为线段的起点和终点:x O 、y O 、x l 、yl 。在下而的程序中使用matplotlib 的 L i n e C o I l e c t i o n 绘制这
些线段,效果如图9-24所示。
O penCV图
- 像处理和计
IT -
机视觉
图9-24使用HoughLinesP〇检测阁像屮的直线
fig, ax = pl.subplots(figsize=(8 , 6 ))
pl.imshow(img, cmap=,,gray")
from matplotlib.collections import LineCollection
lc = LineCollection(lines.reshape(-1, 2, 2))
ax.add_collection(lc)
ax.axis("off")
2.仏测圆形
检测圆形的 HoughCircles()的参数如下:
其 :中 m e t h o d 参 数 为 圆 形 检 测 的 算 法 , 目 前 O p e n C V 中 只 实 现 了 一 种 检 测 算 法 :
C V _ H 0 U G H _G R A D 1E N T P 。d p 参数和直线检测中的 r h o 参数类似,决定了检测的精度,dp 二1
时累加器的分辨率和输入图像相同,而 dp= 2 时累加器的分辨率为输入图像的一半。minDist参
数是检测到的所有閼的圆心之间的最小距离,当它过小时会检测出很多近似的岡形,若过大则
可能会漏掉一些结果。
p a r a m l 和 p a r a m 2 参数是和检测算法相关的参数。在 H o u g h C i r c l e s ( ) 内部会进行边缘检测,
o p e n c v图
由于圆形有三个参数一 圆心坐标和半径,如果直接使用三维累加器,贝IJ计兑效率太低。
并且由于累加器中每个点的累计次数不够多,会出现很多局部峰值,也会影响检测结果。因此
- 像处理和计算机视觉
HoughCirclesO使用一•种被称作霍夫梯度的算法进行圆形检测。它的计算步骤如下:
(1) 首先将原始图像经过边缘检测兑法获得一张边缘图像,这里使用 C a n n y ( ) 进行边缘检测,
并使用 p a r a m l 参数指定阈值。
(2) 对于边缘图像中每个白色的点(xO ,yO )计算其局部梯度,
这里使用 Sobel〇进行梯度计算。
假设白色点为圆周上的某点,经过(xO ,
yO )沿着梯度方丨⑷的直线将通过圆心。
(3) 对梯度直线上离点(xO ,yO )的距离在 minRadius 和 maxRadius 之间的所有点,在累加器中
进行计数。
(4) 累加器中大于阈值param2 的局部峰值为图像中所检测出的圆形的中心。
然后对于每个检测出的圆心,在边缘图像中寻找离它距离相同的白色点的集合,并计算出
半径。如果此圆心有足够多的白色点支持,那么它就是真正的圆心。
HoughCirclesO返冋一个形状为(1,
N,3)的数组,其 中 N 为圆形的个数,第 2 轴上的三个元素
分别为圆心的 X 轴坐标、Y 轴坐标和圆形的半径。
在下面的程序中(效果如图9-25所示),O 为了获得较好的边缘检测结果,
调 用 GaussianBlmO
对图像进行模糊处理。© 使 用 EllipseCollection快速绘制多个|M |形。
y, r = circles[0].T
P y th o n 科学计算(第 2 版)
fig, ax = pi.subplots(figsize=(8 , 6 ))
pl.imshow(img, cmap="gray")
from matplotlib.collections import EllipseCollection
ec = EllipseCollection(widths=2*r, heights=2*r, angles=0^ units="xy", ©
facecolors="none", edgecolors="red'、
transOffset=ax.transData, offsets=np.c_[x, y])
ax•add一collection(ec)
ax.axis("off")
雜⑩馨參鲁
U © (d • :
勤 0 :
O penCV图
- 像处理和计筇机视觉
阁 9 - 2 5 使用 H o u g h C i r c M ) 检测阁像屮的El 形
9 . 4 . 2 图像分割
一般的图像中颜色丰富、信息繁杂,不利于计算机进行图像识别。因此通常会使用图像分
割技术,将图像屮相似的区域进行合并,使得图像更荇易理解和分析。本节将介绍 O p e n C V 中
提供的两种常见的图像分割算法。
1. Mean-Shift 算法
pyrMeanShiftFilteringO使jij M e a n -Shift算法对图像进行分割。它的调j|j参数如下:
O penCV图
ax.imshow(img2 [ :-1 ])
ax•set一axis—off()
- 像处理和计® 机视觉
ax.set_title("sr = {}".format(sr))
sr= 20 sr = 40 sr = 80
图9-26使用pyiMcanShiftFilteringO进行图像分割,从左到右参数sr分別为20、40、80
2.分水岭算法
分水岭箅法(Watershed)的基本思想是将图像的梯度当作地形图。图像中变化小的区域相当
于地形图中的山谷,而变化大的区域相当于山峰。从指定的几个初始区域同吋开始向地形灌不
同颜色的水,水面上升逐渐淹没山谷,并且范PI逐渐扩大。请注意这里所说的“颜色”是用来
区分不同区域的一个数值,和图像的颜色没有任何关系。当所有区域的水面连接到一起时,所
得到的不同颜色的灌溉区域就是最终的阁像分割结果,最终的分割区域数和初始区域数相同。
watershed()实现此算法,它的调用形式如下:
watershed(imagemarkers)
59
Python 科学计算(第 2 版)
scpy2.opencv.watershed_d e m o : 分水岭算法的演示程序。用鼠标在图像上绘制初始区
域,初始区域将使用“当前标签”填充,按鼠标右键切换到下一个标签。每次绘制
初始区域之后,将显示分割结果。
图 9-27使用 watershed分割药丸
S U R F 是一利收速提取阁像特征点的算法,它所提取的阁像特征具有缩放和旋转的不变性,
而且它对图像的灰度变化也不敏感。对 S U R F 算法的详细介绍超出了本书的范围,这里只简单
地介绍 O p e n C V 中 S U R F 类的用法。
O penCV图
在下面的程序中,〇首先读入一幅灰度图像 i n i w a y ,© 然 后 创 建 S U R F 0 对象,并设置
hessianThreshold和 nOctaves 参数,这两个参数为计兑关键点的参数,hessianThreshold越大则关
- 像处理和计® 机视觉
键点的个数越少,nOctaves越大则关键点的尺寸范围越大。€)调 用 detect()方法从灰度图像中检
测出关键点。它返回一个列表 key_poims, 其中每个元素都是一个保存关键点信息的 KeyPoint
对象。
©根 掘 关 键 点 的 size属性按照从人到小的顺序排列关键点。© 调 用 dmwKeypoints ()可以在
图 像 I:绘制关键点,这里为了使用红色绘制关键点,将灰度图像通过 cvtColor()转换成灰色的
R G B 格式的图像,结果如图9-28(左)所示。图中红色岡降丨的大小显示关键点的size厲性,而半
径线段的方向显示了关键点的方向属性angle。
图9-28(右)显示了前25个最大的关键点,每个关键点对应的小图块已经根据angle屈性旋
转过。由此可以大致观察S U R F 算法检测出的关键点对应的图像模式。
通过比对关键点对应的图块,可以在两幅图像中寻找相似的点。但楚直接比较阁块的像素
O penCV图
值并不明智,S U R F 算法为每个关键点计算128个時征,这些特征与关键点的大小和方向无关。
下面调用 S U R F .compute()计算关键点列表 key_pointsl对应的特征向M featuresl:
- 像处理和计筇机视觉
还可以调用 S U R F . d e t e c t A n d C o m p u t e ( ) 直接计算关键点以及与之对应的特征叫量:
http://docs.opencv.org/modules/flann/doc/flarm.html
O p e n C V 关于 F L A N N 的文档。
算法名 编号 说明
FLANN_INDEX_LINEAR 0 穷举法
FLANN_INDEX_KDTREE 1 使用多棵随机K d 树
FLANN_INDEX_KMEANS 2 使用分层k-means树
F L A N N J N D E X C一
OMPOSITE 3 结合使用上述两种算法
FLANN_INDEX_LSH 6 使用 mulli-probe LSH 算法
FLANN_INDEX_AUTOTUNED 255 向动选择合适的算法
每种算法都有一些可调节的参数,在本例中使用多棵随机K d 树 ,通 过 trees参数设置树的
个数为5。在搜索参数中 checks参数决定搜索次数,该值越大结果越精确。
© 调 用 knnMatch〇对 featuresl中的每个特征向量在 featured中搜索 k 个最近的特征向量。
这里设置参数 k 为 1,只搜索最接近的特征向量。
Qpencv图
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE> trees=5)
searchjDarams = dict(checks=1 0 0 )
- 像处理和计筇机视觉
fbm = cv2 .FlannBasedMatcher(index_params, search_params) O
match_list = fbm.knnMatch(featuresl, features2 , k=l) ©
match_list是一个嵌套列表,
它的长度等于featuresl .shape[0],
其中每个子列表的长度等于k 。
子列表中的元素类型为D M a t c h 对象,其 distance屈性保持两个关键点特征之间的距离,queiyldx
屈性保存 features 1中的下标,
irainidx属性保存 featured中的下标。由下面的结果可知与features 1[0]
最近的向量为 features2[21],距离为0.41472:
m = match一list[0 ][0 ]
m.distance m.queryldx m.trainldx
0.414721816778183021
下而的程序通过列表推导式将关键点的处标、匹配的下标以及距离都转换成数组,o 然后
获取距离最小的50对关键点的坐标,在 图 9-29中用直线连接这些匹配的关键点。由图9-29可
以看出大多数匹配点是正确的,似也有少数匹配错误的关键点。© 为了找到两幅图像之间的变
换矩阵,可 以 使 用 fmdHomographyO , 它的前两个参数为原坐标点和变换之后的坐标点,第三
个 参 数 选 择 算 法 。这 里 使 用 R A N S A C 算 法 ,它 可 以 将 匹 配 错 误 的 关 键 点 £ | 动 剔 除 。
findHomographyO 返回变换矩阵和遮罩用的数组m a s k ,
其中0 表示被剔除的点,
注意形状为(N ,1),
在实际使州时还耑要将K 转换成一维数组。在 图 9-29中州蓝色直线显示被剔除的匹配点。
63
Python 科学计算(第 2 版)
请读者思考如何利用如下程序得到的matrix矩阵将变形之后的图像还原成原始图像<
图9-29 © 示特征匹配的关键点
9 . 5 職与结构分析
9 . 5 . 1 轮廓检测
f m d C o i i t o u r s O 用于在二值图像中寻找黑白区域的边界轮廊,并返 In丨描述轮廊的多边形,它
的调用形式如下:
o p e n c v图
可以指定多种近似方法来计箅更平滑、点数更少的轮廓多边形。有以下几利1选项:
• C H A 1 N _ A P P R 0 X _ N 0 N E : 获取轮廓上的所有点,不做任何近似处理
- 像处理和计筇机视觉
• CHA1N_APPR0X_S1MPLE: 对水平线、垂直线以及对角线做近似处理
• C H A I N _ A P P R O X _ T C 8 9 _ L 1,
CHAIN_APPROX_TC89_KCOS :对整个轮廊进行近似处理,
从而获得点数更少的多边形。
scpy2.opencv.findcontours_d e m o : 轮廓检测演示程序。
在下面的例子中,首先读入一幅灰度图像,经过高斯模糊、边缘检测和形态学闭运算之后
得到如图9-30(左)所示的二值图像。
由于图像中没有嵌套结构,在下面的程序中使用 R E T R _ E X T E R N A L 模式搜索轮廓,并比
较各种轮廓近似选项所得到的多边形的总点数:
65
Python 科学计算(第 2 版)
cv2.RETR_EXTERNAL, approx_flag)
print _•{}: {} format(approx, sum(contour.shape[0 ] for contour in coin_contours))^
NONE:3179 SIMPLE:1579 TC89_KC0S: 849 TC89_L1:802
fmdContoursO返回两个值:表示轮廓的多边形列表和轮廓的嵌套信息。其中每个多边形都
用一•个形状为(N ,l,
2)的 32位整数数组表示。其 中 N 为多边形的点数,第 1轴为多余的轴。
图9-30(左)中右上下角处的噪声也会被检测出轮腐,可以通过下面的公式计算每个轮腐的
_ 度C ,从而删除噪声轮廓,其中p 为轮廓的周长,a 为轮廓所包围的面积:
2
P
C
4ira
轮廊的周长和面积可以通过 arcLength〇和 contourArea〇计算,为了计算封闭轮廊的周长,
需要设置 arcLengthO的第二个参数 d o s e d 为 True。下面的程序找到所有_ 度 在 0.8〜 1.2之间的
轮廓,并调用 drawContours()将这些轮廊绘制在彩色阁像img^ coin之上。其第三个参数为所绘制
轮廓的序号,负数表示绘制所有的轮廓。
O penCV图
def circularity(contour):
perimeter = cv2.arcLength(contour, True)
area = cv2.contourArea(contour) + le- 6
- 像处理和计筇机视觉
阁9-30显示所荷圆度在0.8到 1.2之间的轮廊
使 用 R E T R _T R E E 搜索模式可以获取轮廓的嵌蕓信息。在下面的程序中,fmdContoursO返
回的第二个数纟J1中保存轮廓的嵌套信息,
它是一个形状为(1,
N,4)的数组,
其 第 0 轴的长度为1,
为多余的轴,N 为轮廓的个数。hierarchy[0,
i,:]中保存与轮廓contours[i]对应的嵌套信息。其最后
一个轴的4 个数据的含义为:下一个同级别轮廊的下标、上一个同级别轮廊的下标、第一个子
轮廓的下标、父轮廓的下标。其中-1表示无效下标。
所有父轮廓下标为- 1 的轮廓就是阁像中最外层的轮廓,下而的程序找到所有最外层轮廓的
下标:
o p e n c v图
而 g e U d e s c e i K l a m O 则可以获得所有嵌套轮廓的层次和下标。下而显示下标为 0 的轮廓的所有
嵌套轮廓的级别和下标。图 9-31 示了所有的轮廓,并用颜色映射表示各个轮廓对应的
- 像处理和计筇机视觉
层次。
print list(get—descendant(hierarchy, 0 ))
[(1, 1), (2, 2), (3, 3), (2, 4), (3, 5), (3, 6 )]
67
P y th o n 科学计算(第2 版)
阁9-31显示轮廊的层次结构
9 . 5 . 2 轮廓匹配
O penCV图
通 过 findContours〇获取轮廊之后,可 以 使 用 approxPolyDP〇对其进行前化,然后通过
matchShapes〇比较两个简化之后的轮廊之间的近似程度。
- 像处理和计筇机视觉
matchShapesO 比较两个形状的近似程度。
它的第二个参数指定比较箅法,
有三利囌法可选:
C V C O N T O U R S M A T C H II、C V C O N T O U R S M A T C H 12 和 C V C O N T O U R S M A T C H B ,
这些算法都比较轮廊的7 个 H u 不变M , 可以通过 HuMoments 〇壺看这些不变量的值。其 中 13
的比较公式选择各个不变S 之间的最大误差作为近似程度的评分,在木例中使用该方法能得到
最佳匹配效果。具体的公式请读者参照O p e n C V 的帮助文档。
下面调用 matchShapesO计兑 targets_simple〖
Oj和 pattems_simple中的所有轮廊之间的近似;^51度<
图 9-32显示了每两组轮廓之间的近似评分,并用红色标出最佳匹配。图中用黑色粗线描绘近似
之后的轮廓,州填充图形显示原始轮廓。凼结果可知,形状的旋转方向和大小不影响轮廓匹配
4r±m
荃口米。
O penCV图
0.8628 0 0221 875 939
丨 1144
- 像处理和计® 机视觉
□ 0.9225 0.5160 034 1407 0282
0 t 2
f
3 4
閱 9-32使用 matchShapes()比较丨丨丨approxPolyDP()近似之后的轮廓
9 . 6 类型转换
9 . 6 . 1 分 析 cv2 的源程序
c o d e s \ p y o p e n c v _ s r c : 为了方便读者查看 c v 2 模块的源代码,本书提供了自动生成的源
代码。若读者& 到参数类型不确定的情况,可以查看这些文件中相应的函数。
O penCV图
所有的包装闲数都在自动生成的p y 〇p e n c v _ g e n e m t e d _ f u n c s . h 中定义,而这些包装闲数会调
用 c v 2 . c p p 中的众多p y o p e n c v _ t o 〇和 p y o p e n c v _ f r o m 〇函数以实现 P y t h o n 和 O p e n C V 的各种类型转
- 像处理和计筇机视觉
换工作。若不能确定包装函数使用何种P y t h o n 数椐类型,可以查看包装函数的内容。例如下而
是 O p e n C V 文档中关于 line()的说明文档:
C++: void line(Mat& img, Point ptl, Point pt2, const Scalar& color,
int thickness=l, int lineType=8 ^ int shift=0)
Python: cv2.1ine(imgj ptl, pt2, color[, thickness^ lineTypef^ shift]]]) -> None
pyopencv_generated_funcs.h 中该函数的源代码:
return NULL;
}
O penCV图
pyopencv_to()将 Python 的数据转换为这些类型。pyopencv_ to〇有众多重载函数,例如上述类型转
换实际上会调用 cv 2.c p p 中的如下三个函数:
- 像处理和计算机视觉
static int pyopencv_to(const PyObject* Mat& const Arglnfo info^ bool allowND=true);
static inline bool pyopencv_to(PyObject* obj, Points p, const char* name = "〈 unknown〉");
static bool pyopencv_to(PyObject *〇 , ScalarS s, const char *name = "〈 unknown〉");
下面是其中 Point类型对应的转换函数:
分析该程序可知它能将Python 的复数和元组转换为Point对象,
例 如 100f 200j 或(100,200)。
Python 科学计算(第 2 版)
在 调 用 C + + 的 函 数 之 后 ,还 需 要 将 其 返 冋 值 转 换 为 P y th o n 的 对 象 。这种转换由
pyopencv_from〇函数实现。例 如 minAreaRect()函数找到一个包含所有点的最小矩开帮助文档
中的调用说明如下:
9.6.2 M at 对 象
200 10 0 3 18 600
572
在 cv 2 模块屮,可以通过其中的全局变量获得表示数值类型和像素类型的常数值。数值类
型名由三部分组成:
•固 定 部 分 CV_
•数 值 的 比 特 数 :8、16、32、64
• 一个描述类型的字母:U 表示无符号整数、S 表示符号整数、F 表示浮点数
像素类型则在数值类型的基础上,添 加 C l 、C 2、C 3、C 4 等,分别表示1 到 4 个通道的像
素类型。因 此 C V _16U 表 示 16位的无符号整数,而 C V _16U C 3 表示三个通道的16位无符号
整数:
cv2•CV_16U cv2•CV一16UC3
2 18
o p e n c v图
类型与 M a t 的数值类型的对应关系如表9-3所示。
表 9-3 dtype类型与Mat数值类型的对应关系
- 像处理和计筇机视觉
dtype类型 Mat数值类型 说明
uint8 CV_8U 单字节无符号整数
int8 CV_8S 单字节符号整数
uintl6 CV_16U 双字节无符号整数
inti 6 CV_16S 双字节符号整数
cv2 .normalize(l)
Python 科学计算(第 2 版)
a r r a y ( [ [ 1.],
[
[0.],
[0.]])
在 c v 模 块 中 使 用 c v m a t 和 i p l i m a g e 对 象 表 示 图 像 。如 果 需 要 混 用 c v 2 和 c v 两 套 A P I 中的函
数 ,就 需 要 在 它 们 与 N u m P y 数 组 之 间 进 行 转 换 。表 9 ~ 4 列出了这些类型之间的转换方法:
表 9 - 4 类型转换方法
类型转换 方法
array—>cvmat cv.iiomarray(array)
cvmat—>iuray np.asarray(cvmat)
iplimage—K:
vmat iplimage [:]或 cv.GelMat(ipl image)
〇pencv图
import numpy as np
np.all(img == np.asarray(iplimage[:]))
True
由于 i p l i m a g e 类型需要数据保存在连续的内存空间「
卜,因此使用切片获得的数组需要复制
之后才能转换为 i p l i m a g e 对象:
iplimage2 = cv.GetImage(cv.fromarray(img[::2,::2^:].copy()))
等 10雩
Cython-编译 Python 程序
Python 的动态特性虽然方便了程序的幵发,但也会极大地降低运行速度,特别是对于计算
密集型的程序。
用 Python 开发这类程序时,
通常会调用编译型语言编写的扩展序,
例 如 NumPy 、
S d P y 等,尽量避免直接在 Python 中进行大量的循环和数值计算。如果这些现成的序无法完成
计算要求,就需要用更高效的语言编写核心计算部分,并为之提供 Python 的调用接口,从而同
时实现高效开发和高效运算。
然而如果采用 C 语言编写扩展库,就会儿乎失去 P y t h o n 带来的所有便利:函数的参数需
要手工解析、对象的引用计数需要手工维护、大量的 P y t h o n API 需要记忆。所有这些困难使得
我们无法把注意力集中到解决实际问题之上。
C ython 是为了减轻用 C 语言编写 Python 扩展模块的负担而开发出来的一•种编程语言。它
的语法基本与 Python 相同,但增加了直接定义和调j |j C 语言函数、定义变量类型等功能。通过
Cython 的编译器可以将 Cython 的源程序编译成 C 语言的源程序,再通过 C 语言编译器编译成
扩展模块。Cython 程序既能实现 C 语言的运算速度,也 能 使 Python 的所有动态特性,极大地
方便了扩展库的编写。
1 0 . 1 配置编译器
[build]
compiler = mingw32
读者可以使用本书提供的 sh〇
w _C〇mpiler()显示默认的 C 语言编译器。使 用 set_C〇
mpiler()可
以快速设S distutils.c f g 中的 compiler选项:
Python 科学计算(第 2 版)
如果未通过 distutils.cfg文 件 指 编 译 器 ,
distutils会尝试使用编译 Python 2.7的编译器 Visual
C + + 2008,如果操作系统中未安装该编译器,会出现如下错误:
http://www .microsoft.com/en-us/download/conflrmation.aspx?id=44266
% ^ 微 软 提 供 的 编 译 Python 2.7的编译器。
cython编
行下而的命令来更新setuptools模块:
Python
%%cython
76
此外,使用新版本的 Visual O h ■也可以编译扩展模块,在编译-•呰较新的函数库(例如笔者
flj C y t h o n 包 装 K i n e c t 2 的 A P I ) 时可能需要最新版本的Visual C + + 编译器。但 distutils只搜索编译
P y t h o n 时所使用的编译器,因此无法使用最新版本的编译器。
修改该信息,从而实现选杼新版本的编译器:
set_msvc_version(9)
set_compiler("mingw32")
cython编
10.2 Cython 入门
Python—译
程序
与本节内容对应的 Notebook 为: 1 0 ~cython /cython -2 0 0 ~intro.ipynb 。
10.2 . 1 计算矢量集的距离矩阵
我们要实现计算一组矢量中每两对矢量的距离的函数。下面的数组 X 的形状为(200,3),可
以把它看作200个三维空间中的点:
im p o rt numpy as np
n p . random. seed(4 2 )
X = n p .ra n d o m .ra n d (200, 3)
为 了 计 算 每 对 点 之 间 的 距 离 ,可 以 用 N u m P y 的 广 播 功 能 ,或 者 使 用 S d P y 中的
scipy.spatial.distance.pdist〇来实现。不过这里为了比较 P y t h o n 和 C y t h o n 的性能,我们用三层循环
逐个元素进行计算:
77
Python 科学计算(第 2 版)
def pairwise_dist_python(X):
m, n = X.shape
D = np.empty((m, m), dtype=np.float)
for i in xrange(m):
for j in xrange(i,m):
d = 0.0
for k in xrange(n):
tmp = X[i, k] - X[j, k]
d += tmp * tmp
D[i, j] = D[j, i] = d ** 0.5
return D
%timeit pairwise_dist_python(X)
np.allclose(squareform(pdist(X))J pairwise_distjDython(X))
—译
True
程序
%%cython
import numpy as np
def pairwise_dist_cython(X):
m, n = X.shape
D = np.empty((m, m), dtype=np.float)
for i in xrange(m):
for j in xrange(i> m):
d = 0 .0
for k in xrange(n):
tmp = X[i, k] - X[j, k]
d += tmp * tmp
D[i, j] = D[j, i] = d ** 0.5
return D
下面测试其运兑速度:
%timeit pairwise一dist一cython(X)
np•allclose(pairwise一dist—cython(X), pairwise—dist_python(X))
10 loops, best of 3: 72.9 ms pen loop
True
%%cython
import numpy as np
import cython
from libc.math cimport sqrt
@cython.boundscheck(False)
c y t h o n编
@cython.wraparound(False)
def painwise—dist—cython2(double[:, ::1] X):
—译
cdef int m, n, i, j, k
cdef double tmp, d
P y th o n程 序
m ,n = X.shape[0], X.shape[l]
cdef double[:, ::1] D = np.empty((m, m)^ dtype=np.float64)
for i in range(m):
for j in range(ij m):
d = 0.0
for k in range(n):
tmp = X[i, k] - X[j, k]
d += tmp * tmp
D[i, ]•] = D[j, i] = sqrt(d)
return np.asarray(D)
%timeit pairwise_dist_cython2(X)
np.allclose(pairwise_dist_cython2(X), pairwise_distjDython(X))
10000 loops, best of 3: 196 \xs per loop
True
1 0 . 2 . 2 将 Cython 程序编译成扩展模块
79
Python 科学计算(第 2 版)
%%file setup_fast_pdist.py
程序
ext_modules = [
Extension("fast_pdist", ["fastjDdist.pyx"],
include_dirs = [np.get_include()]),
]
setup(
name = 'a faster version of pdist',
cmdclass = {•build一ext•: build一ext},
ext_modules = ext_modules
)
接下来即可载入该扩展模块,并调jlj其中的函数:
80
import fast_pdist
np•allclose(fast_pdist•pairwise_dist_cython2(X), pairwise_dist_python(X))
True
@echo off
python -m Cython.Build.Cythonize %*
C y t h o n编
型进行处理。为了让读者更深入地理解Cython 语言,本节先介绍在 C 语言中是如何表示 Python
对象的。
-译
Python 采 用 C 语言编写,它的所有对象都采用 C 语言的结构体表示,无论何种对象的结构
P y th o n
体,其久•两个字段的含义是固定的:
♦ 〇b_refCm : 该对象的引用次数,当引用次数为0 时,该结构体所占据的内存将被释放。
程序
• ob_type: 指向类型对象的指针。
在 Python 的 C 语言程序中定了 PyObject 结构体,它仅拥有上述两个字段。而其他对象类型
的结构体则在之后添加新的字段,例 如 float 类型对应的结构体如下:
typedef struct {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
double ob_fval;
} PyFloatObject;
import sys
sys.getsizeof(1 .0 )
16
_l>yx.G0lftCr(_pyx.t_2);
C y t h o n编
Pyx^GOlRtl ( _ p y x . t _ 3 ) ;
-译
_Pyx_t)F.CREF(_pyx_t_l); _pyx_t_l = 0 ;
. 』 y x J )E C R F (— p y x j _ 2 > ; — pyx 一 t . ? , 0;
P y th o n
图 K M 使用“
-a ”参数査看编译之后的C 语言程序
%%cython -a
a = 1 .0
b = 2 .0
c = a + b
对对象的引用计数器进行操作。
_ pyx_n_s_a 为字符弟对象"a" 。_ P y x _ G e t M o d u l e G l o b a l N a m e ( _ p y x _ n _ s _ a ) 从全局字典中获
1 0 . 2 . 4 使 用 edef关键字声明变量类型
static double a, b, c;
• ♦镛
PyMODINIT_FUNC init(){
• • •
C y t h o n编
a = 1 .0 ;
b = 2 .0 ;
-译
c = a + b;
P y th o n
程序
该段代码创建了三个类型为double 的全局变量,并在模块初始化函数中为这三个全局变量
赋值。加法运算和变撒赋值均为 C 语言中的操作,与上节中调用 A P I 闲数的代码相比要简洁
许多。
%%cython -a
cdef double a = 1 . 0
cdef double b = 2 . 0
cdef double c = a + b
© 使 用 < 也 此 1〇 将 P y th o n 对 象 强 制 转 换 为 C 语 言 的 d o u b le 类 型 ,因 此 这 里 的 加 法 运 算 采 用
C 语 言 的 加 法 操 作 符 , 大 致 相 当 于 运 行 如 下 C 语 言 代 码 ,其 中 丄 1 为 P y th o n 对 象 指 针 ,丄 2 为
double 类 型 :
_ t _ l = _ Pyx_GetModuleGlobalName(_s_a) ;
_ t_ 2 = _ p y x _ P y F lo a t_ A s D o u b le (_ t_ l);
一
v s = v s + t
一 一 一 一 一
2 ;
%%cython -a
c d e f d o u b le s = 0
a = 3 .0
s = s + a O
s = s + <double>a ©
C y t h o n编
除了 C 语 言 的 各 种 数 据 类 型 之 外 , C y th o n 还 能 识 别 P y th o n 的 许 多 内 置 的 数 据 类 型 ,例 如
-译
列 表 、字 典 、元 组 等 。在 下 而 的 例 子 中 ,使 用 l i s t 声 明 d i s t 变 显 是 一 个 列 表 对 象 , 并 且 使 用 一
个 i n t 类 型 的 d n d e x 变 M 作 为 下 标 获 取 列 表 中 的 值 。 为 了 进 行 比 较 , 也 使 用 两 个 动 态 变 S p ylist
P y th o n
和 p y in d e x 进 行 同 样 的 操 作 。
程序
%%cython -a
c d e f l i s t c l i s t = [1000, 2, 3]
c d e f i n t c in d e x = 0
c lis t[c in d e x ] O
p y l i s t = [1000, 2, 3]
p yin d e x = 0
p y lis t [ p y in d e x ] ©
〇 由 于 声 明 了 c lis t 和 c in d e x 的 类 型 , 因 此 d is t[d n d e x | 被 编 译 成 如 下 代 码 ,直 接 调 川 辅 助 函
数 _ P y x _ G e tIte m ln t_ L is t() 获 取 列 表 对 象 中 某 个 F 标 对 应 的 对 象 :
© 由 于 p y lis t 和 p y in d e x 变 量 没 有 类 型 声 明 , 因 此 把 它 们 当 作 P y th o n 的 对 象 进 行 处 理 。将
p y lis t[p y in d e x] 编 译 为 如 卜 代 码 :
_ p y x _ t_ l = 一 Pyx_GetModuleGlobalName (__pyx_n_s_py l i s t ) ;
一 Pyx一GOTREF ( 一 p y x j i - l ) ;
_ p yx_ t_ 2 = _ Pyx_GetModuleGlobalName ( 一 pyx_n_s_py in d e x ) ;
_ P y x _ G 0 T R E F (_ p y x _ t_ 2 );
84
_Pyx_t_3 = PyObject_GetItem(_pyx_t_l, _pyx_t_2);;
_Pyx_G0TREF(_pyx_t_3);
_Pyx_DECREF(_pyx_t_l); _pyx_t_l = 0;
_Pyx_DECREF(_pyx_t_2); _pyx_t_2 = 0;
_Pyx_DECREF(_pyx_t_3); _pyx_t_3 = 0;
数据类型 可识别的运算
bytes、str、 Unicode join()、 in
C y t h o n编
tuple in
list in、insertO、reverse()、append〇、exlend〇
-译
diet in、gel()、has_key()、keys()、values()、iter*〇、clear〇、copy()
set in、clear〇、add〇、pop〇
P y th o n
slice start、stop、 step
程序
complex eval、real、 imag
读者可以在前面的程序中添加clistappend(pyindex)来查看 append〇编译之后的程序。如果遇
到未知的屈性或方法,将调用 Python 对象提供的通用 A P I 接口,例 如 clist.count(O)。
1 0 . 2 . 5 使 用 d e f定义函数
PyObject * _r;
_r = PyFloat_FromDouble(((_v_x * _v_x) + (_v_y * _v_y)));
%%cython -a
%%cython -a
for i in range(len(alist)):
s += <double>alist[i]
-译
return s
P y th o n
1 0 . 2 . 6 使 用 cdef定 义 C 语言函数
程序
_ pyx_v_a = _ pyx_f_c_square_add(1 .0 , 2 .0 );
%%cython -a
cdef double c_square_add(double x, double y):
return x*x + y*y
86
函数时会比 cd e f 函数稍微耗时一些,但要比调用 d e f 函数快得多。
%%cython -a
cpdef double cp square_add(double x, double y):
一
cp_square_add(1.0, 2.0)
1 0 .3 高效处® ^
与本节内容对应的Notebook 为: 10-cython/cython-300-memoryview.ipynb。
Cython 屮的内存视图采用如下语法进行声明:
其中维数声明的形式如下,使用:代表轴,:
:1表示对应轴上的元素被连续存储:
•[:]: 一维数组
• 一维连续存储的数会J1
• [:,
:]:二维数组
• [:,
::1]: 二维数组,第 1轴的元素被连续存储
内存视图和N um Py 数组的结构类似,保存有 shape、base、stride等信息,它本身并不拙有
数裾存储区,而是从其他的 Python 对象或 C 语言数组获取数据存储区。
在 C y t h o n 中内存•视阁有两种形式,根据使用方式的不同,C y t h o n 会在这两利呢式之间|二
|
动切换。在 C 语言级别它是一个结构体,当需要将其作为P y t h o n 对象使用时,C y t h o n 会自动将
其转换成一个 M e m o i y V i e w 对象。下面我们通过一些例子演示内存视图的用法。
在下面的程序中,O b u f 是 C 语言中的一个全局二维数组,© v ie w 是一个内存视图,并将
其数据存储区初始化为全局二维数组buf。
© numpy.asarray()楚 Python 的一个函数,它 n j 以把 M em oryView 对象转换成 N um Py 数姐,
然后就可以调数组的方法和函数了。® 此外 , Memory V ie w 对象也可以直接传递给N um Py 的
闲数,因为在这些函数内部都会调用类似asarnyO 的闲数来将参数转换为数组。
Python 科学计算(第 2 版)
%%cython
import numpy as np
def sum_view():
C y t h o n编
return np.sum(view) o
-译
def get_view():
return view 0
P y th o n
def square_view(): ©
程序
cdef int i, j
for i in range(view.shape[0 ]):
for j in range(view.shape[1 ]):
view[i, j] *= view[i, j]
view = get_view()
view.shape view.strides view.itemsize view.nbytes
(3, 4) (32, 8 ) 8 96
fill_value(3.0)
print sum 一 ivew()
square_view()
print sum—view()
view[l, 2 ] = 1 0
print sum 一 ivew()
8
8
36.0
108.0
109.0
a m = np.asarray(view)
arr[ly 0 ]= 11
print sum 一
ivew()
print a m
1 1 1 .0
[[ 9. 9. 9. 9.]
[11. 9. 10 . 9.]
[ 9. 9. 9. 9.]]
C y t h o n编
全局数纟JI中,因 此 Cython 创建了一个 array 对象来表示该全局数纟11,而通过其唯一的 memview
属性可获取一个新的M em oryView 对象。
-译
print view.base.—class—
P y th o n
print view.base.memview
<type '_cython_magic_1118ba0f43cl5alb4al6015476c9c6fa.array'>
程序
〈
MemoryView of 'array' object>
下面看看使用内存视图操作N um Py 数姐:
O 使)lj doublel:〗
声 明 x 为一维内存视图,凼于没有指定其元素为连续存储,因此 x 〖
ij 被编译
为 x.data + i * x .strides[0],其 中 x .data 为 x 的数据存储区的首地址,类型为单字节指针,x .strides[0]
为第0 轴上元素之间的字节间隔数。
©由于声明了 re s 的元素为连续存储,因此 res[i]被编译为((double *) res.data) + i 。因为省略
了下标变S i 与元素间隔字节数的乘法运算,所以运算速度更快。
© 通 过 res.base返 回 N um Py 数组,注 怠 base 属性不是表示内#视阁的 C 语言结构体中的字
段,因此 Cython 会先将其转换成 M em oryView 对象,然后通过 Python 的 getattr〇函数获収其 base
属性。
如果在函数1丨
1对数组的元素进行逐个循环,可 以 将 boundscheck 和 wraparound两个编译选
项关闭。这样所生成的 C 语言代码不会对数组下标进行越界检查,也不支持负数下标,从而提
高数组元素的访问速度。
%%cython -a
import numpy as np
import cython
89
Python 科学计算(第 2 版)
@cython.boundscheck(False)
@cython.wraparound(False)
def square(double[:] x): O
cdef int i
cdef doublet::1 ] res = np.empty 一
like(x)
for i in range(x.shape[0 ]):
res[i] = x[i] * x[i] ©
return res.base ©
此外,内存视图支持与 N u m P y 数组相同的切片下标存取功能,当使用切片存取内存视图
时,将创建一个与原内稃视阁共享内存的新结构体,因此会有一定的运算损耗,但是比数组的
切片操作要快许多。
在下而的例子中用cpdef 定义了一个可在Cylhon 中快速调用,
且能在 Python 中凋用的norm〇
函数,它对一维向量进行原地归一化。而 norm_axis()则可以对二维数组的指定轴进行归一化,
如 果 inplace 参数为 True , 则进行原地归一化,否则返回一个新的归一化之后的数组。
〇内存视图支持切片赋值运算,这吋会创建一个临时的内存视图结构体,然后在两个内存
C y t h o n编
视图结构体之间进行数据复制。© 对内存视图使用切片下标读収时将创建一个新的视图,并将
-译
此视图传递给 nomi〇函数。
P y th o n
%%cython
#cython: boundscheck=False, wraparound=False
import numpy as np
import cython
90
data = np.empty一like(x)
data[:] = x O
else:
data = x
if axis == 1 :
for i in range(data.shape[0 ]):
norm(data[i, :]) ©
elif axis == 0 :
for i in range(data.shape[l]):
norm(data[i])
return data.base
还可以通过地址运算符获得内存视图中数据的地址,将其作为指针传递给 C 语言的函数。
在下面的例子屮,O 通 过 c i m p o r t 从 C 语言的标准头文件string.h 111载 入 m e m c p y O 函数,其函数
c y t h o n编
原型如下:它将由 src 指向地址为起始地址的连续n 个字卞的数据复制到以dst 指向地址为起始
地址的空间内。
—译
void *memcpy(void *dst, const void *src^ size_t n);
P y t h o n程 序
© 通过地址运算符& 获得指向内存视图第一个元素的指针,& dst[0j 得到的是一个 double *
类型的指针。由于 memcpyO接收的是字节长度,因此这里使用 size〇
f (d〇
Uble)计算双精度浮点数
的字节数并乘以内存视图第0 轴的长度。
请注意这里的内存视图是连续的,冈此可以直接使用 memcpyO 进行内存的复制。如果不
是连续的,需要将 strides 属性一起传递给 C 语言函数,这样才能在其中正确访问内存视阁的
兀素。
%%cython
from libc.string cimport memcpy O
a = np.random.rand(1 0 )
b = np.zeros一like(a)
copy_memview(a, b)
assert np.all(a == b)
造成整个程序崩溃。
1 0 . 3 . 2 用降釆样提高绘图速度
当 使 用 m atpbtlib 显示一条拥有大量数据点的曲线时,绘图速度会明显降低。由于屏笳的
分辨率有限,绘制大量的线段并不能增加阁表显示的信息,冈此一般在显示大量数据时都会对
其进行降采样运算。由于这种运算需要对数组中的每个元素进行迭代,因此需要使用 Cython
提高运算速度。
下面演示如何使用 C y th o n 进行快速降采样运算。首先创建测试数据,在正弦波信号中夹
杂 着 1% 的脉冲信号。
import numpy as np
x = np.random.uniform(0 ^ 2 *np.pi*periodj n)
x.sont()
-译
y = np.sin(x)
m = int(n*0 .0 1 )
P y th o n
x, y = make一noise一sin一wave(1 0 , 10 0 0 0 )
无论怎样降低取样频率,我们都希望能够E 示这些脉冲信号,因此不能简中.地使用每N 个
点取一个点的方法。图10~2显示了一种能尽量保持所有局域最值的方法。将 X 轴的范围等分为
N 个 K 间,找到每个K 间的最小值和最大值的X 轴來标,对这些坐.标按照从小到大的顺序排序
之后,就得到了降采样之后的X 轴坐标。
阁 1 0 - 2 降低収样频率示S 阁
i = index0
x_min = x 一max = x[i]
y_min = y_max = y[i]
x_next = x0 + dx
c y t h o n编
j = 0
—译
while True:
xc, yc = x[i], y[i]
P y th o n
if xc >= x_next or i == indexl:
if x_min > x一amx:
程序
xMMmin. xw max = xMMmax. x min
Z Z
x_min = x_max = xc
y_min = y_max = yc
x_next += dx
if i 二二indexl:
break
else:
if y_min > yc:
x_min, y_min = xc, yc
elif y_max < yc:
x_max, y_max = xc, yc
i += 1
59:
Python 科学计算(第 2 版)
数组进行切片运算。
%%cython
import numpy as np
import cython
@cython.wraparound(False) O
@cython.boundscheck(False)
C y t h o n编
if x0 is None:
程序
x0 = x[0 ]
if xl is None:
xl = x[len(x) - 1 ] ©
i = index©
x_min = x_max = x[i]
y_min = y_max = y[i]
x_next = x0 + dx
xr = np.empty( 2 * n)
yr = np.empty( 2 * n)
j = 0
while True:
xc, yc = x[i], y[i]
if xc >= x_next or i == indexl:
94
if x_min > x_max:
x_min, x_max = x_max, x_min
y_min, y_max = y_max, y_min
xr[j], xr[j + 1 ] = x_min, x一max
yr[j], yr[j + 1 ] = y_min, y_max
j += 2
x_min = x_max = xc
y_min = y_max = yc
x 一
next += dx
if i == indexl:
break
else:
if y_min > yc:
x_min, y_min
elif y_max < yc:
x_max, y_max = xc, yc
g e t _ p e a k s 〇的运行速度提高了 1 0 0 多倍。
xr, yr = get_peaks_py(x, y, 2 0 0 )
xr2j yn2 = get_peaks(x, y, 2 0 0 )
print np.allclose(xr, xr2 ), np.allclose(yr^ yr2 )
scpy2.cython.fast_curve_draw 使〗 matplotlib 演示 g e t _ p e a k s 〇的 效 果 ,在 子 图 对 象 的
x l i m _ c h a n g e d 事件中对曲线位于当前 X 轴的显示范围内的部分进行降取样,并更新曲线对象的
数据,即可提高曲线的绘制速度。读者可以用鼠标右键拖动缩放显示范刚时可以看到响应速度
提高了很多。
Python 科学计算(第 2 版)
1 0 .4 使用Python标 麟 象 和 API
1 0 . 4 . 1 操 作 lis t 对象
在 P y th o n 中可先创建一个空列表,然 后 通 过 append〇方法往其中添加元素,或者通过
[None]*n 创建一个拥有 n 个元素的列表,然后通过下細设置列表的内容。在 Cython 中可以通过
C y t h o n编
调用操作列表的 A P I 函数,提高创建列表的速度。
-译
%%cython
#cython: boundscheck=False, wraparound=False
from cpython.list cimport PyList_New, PyList_SET_ITEM O
from cpython.ref cimport Py_INCREF
96
for i in range(n):
obj = i
PyList—SET_ITEM(result, i, obj)
Py_INCREF(obj)
return result
® 使 用 c i m p o r t 从 C y t h o n 头文件中载入 P y L i s t _ N e w 和 P y L i s t _ S E T _ I T E M 这两个函数声明。
c y t h o n编
可 以 在 C y t h o n 的安装 E1 录之下的 Includes EI 录下找到所有的 C y t h o n 头文件。其 中 n u m p y
—译
的声明,c p y t h o n 目;
^下 包 括 P y t h o n / C API 的函数声明。
变量用于临吋保存将C 语言整数变量 i 转换成的 P y t h o n 整数对象。
P y t h o n程 序
©obj
%timeit range(1 0 0 )
%timeit my_range(1 0 0 )
%timeit my_range2 (1 0 0 )
1000000 loops, best of 3: 1.24 [is per loop
1000000 loops, best of 3: 1.04 fis per loop
1 0 0 0 0 0 loops, best of 3: 2.29 per loop
1 0 . 4 . 2 创 建 tuple 对象
用法和列表的相同,这里不再重复了。
%%cython
#cython: boundscheck=False, wraparound=False
from cpython.list cimport PyList_New, PyList_SET_ITEM
from cpython.tuple cimport PyTuple_New> PyTuple_SET_ITEM
97
Python 科学计算(第 2 版)
n = arr.shapefO], arr.shape[l]
result = PyList_New(m)
for i in range(m):
t = PyTuple_New(n)
for j in range(n):
obj = arr[i, j]
PyTuple_SET_ITEM(t, j, obj)
Py_INCREF(obj)
C y t h o n编
PyList_SET_ITEM(result, i, t)
Py_INCREF(t)
-译
return result
P y th o n
import numpy as np
arr = np.random.randint(0> 10, (5, 2)).astype(np.double)
print to_tuple_list(arr)
arr = np.random.rand(100, 5)
%timeit to_tuple_list(arr)
%timeit arr.tolist()
[(0.0, 4.0), (5.0, 7.0), (7.0, 0.0), (5.0, 5.0), (5.0, 9.0)]
1 0 0 0 0 0 loops, best of 3: 13 per loop
1 0 0 0 0 loops, best of 3: 20.5 \xs per loop
1 0 . 4 . 3 用 array.array作为动态数组
def in_circle(double[ :> :] points, double cx^ double cy^ double r):
cdef array.array[double] res = array.array("d") O
cdef double n2 = r * r
cdef double p[2 ] ©
cdef int i
for i in nange(points.shape[0 ]):
p[0 ] = points[i, 0 ]
p[l] = points[i, 1 ]
if (p[0 ] - cx) * * 2 + (p[l] - cy) * * 2 < r2 :
a r r a y .extend_buffer(res, <char*>p, 2 ) €
)
C y t h o n编
return np.frombuffer(res, np.double).copy().reshape(-l> 2 ) O
-译
变 量 rcs , 并创建一个新的 arniy .airay 对象给它,”
d”表示元素类型为双精度浮点数。© p 是有两
P y th o n
个元素的 C 语言数组,我们用它临时保存当前处理的点的坐标。© 调 用 airay.extencLbufferO将 p
添加进 re s 中。extend_buffer〇的第一个参数是array .array 对象,第二个参数是一个char*指针,它
程序
指向待添加数据的首地址,第三个参数是待添加元素的个数(注怠不是字节数)。〇最后,通过
numpy.frombuffer〇创建A res 共享内存的 N um Py 数组,我们复制该数组以便 Python 垃圾回收 res
对象。
下而比较 in_circle〇和 使 用 N um Py 相关方法的运算速度,当大多数点都位于圆形之外时,
in_circle〇的运算速度将更快一些。
本例的目的是为了演示 array.array动态妒容,实际上使用布尔数组有可能得到更快的
职运算速度。
points = np.random.rand(1 0 0 0 0 , 2 )
cx, cy, r = 0.3, 0.5, 0.05
99
Python 科学计算(第 2 版)
1 0 3 扩展类型
与本节内容对应的Notebook 为: 10-cython/cylhon-500-cdef-class.ipynb〇
1 0 . 5 . 1 扩展类型的基本结构
卜
‘而的程序使用cdefclass 定义扩展类型Point2D ,
并使用 cd ef 定义属性 x 和 y 。
注意和 Python
的类不同,扩展类型的属性在类中定义,而不是在_ init_()方法中生成。
%%cython
struct _ pyx_obj_Point2D {
PyObject_HEAD
double x;
double y;
};
print type(Point2D.x)
print Point2D.x.一get
print Point2D.x._ set
<type 'getset_descriptor'>
〈method-wrapper ' _ get_ ' of getset_descripton object at 0x097AB260>
〈method-wrapper ' _ set_ ' of getset_descripton object at 0x097AB260>
和定义函数相同,扩展类型中可以使用def 、o ie f 和 q x le f 定义对象的方法。所有方法都可
以在 Cython 中调用,
而只有 d e f 和 cpdef 定义的方法可以在 Python 中调用。
在 Cython 中调用 cdef
和 cpdef 方法时,直接调用对应的 C 语言函数,因此效率比 d e f 方法要高很多。
扩展类型支持从其他扩展类型继承,例如下面的 P〇int3D 从 Point2D 继承,并增加了字段z :
%%cython -a
struct —pyx_obj_Point3D {
struct _ pyx_obj_Point2D —pyx一base;
double z;
};
10.5.2 —维浮点数向量类型
for i in range(self.count):
self.data[i] = data[i]
-译
c y t h o n编
if not isinstance(self, Vector): O
self, other = other, self
—译
_self = <Vector>self ©
P y th o n程 序
if isinstance(other, Vector): ©
_other = <Vector>other
if _self.count != 一
other.count:
raise ValueError("Vector size not equal")
new = Vector(一self.count) O
add_array(一self.data, _other.data, new.data^ _self.count)
return new
new = Vector(_self.count)
add—number (—self .data <double>other, new. data, _self. count )G
return new
60
Python 科学计算(第 2 版)
下面是+ = 操作符对应的魔法方'?i_iadd_0:
cpdef norm(self):
cdef double *p
cdef double s
cdef int i
s = 0
p = self.data
for i in range(self.count):
s += p[i] * p[i]
return s**0.5
04
res[i] = opl[i] + op2 [i]
print 20 + v2
print vl.norm(), v2 .norm()
c y t h o n编
print [x* * 2 for x in vl]
5
—译
Vector[232.637056378](100.0, 102.0, 104.0, 106.0, 108.0)
P y th o n程 序
Vector[9.48683298051](2.0, 3.0, 4.0, 5.0, 6.0)
Vector[272.818621065](120.0, 121.0, 122.0, 123.0, 124.0)
5.47722557505 228.100854887
[0.0, 1.0, 4.0, 9.0, 16.0]
vl = Vector(range(10000))
v2 = Vecton(range(10000))
%timeit vl + v2
al = np.arange(1 0 0 0 0 ^ dtype=float)
a2 = np.arange(1 0 0 0 0 ^ dtype=float)
%timeit al + a2
10 0 0 0 0 loops, best of 3: 8.04 [is per loop
100000 loops, best of 3: 9.68 [is per loop
%timeit vl[1 0 0 ]
%timeit vl[1 0 0 ] = 2 .0
%timeit al[1 0 0 ]
%timeit al[1 0 0 ] = 2 .0
05
Python 科学计算(第 2 版)
10.5.3 包装 ahocorasick 库
ms = MultiSearch(["abc", "xyz"])
print ms.isin("123abcdef")
print ms.isin("123uvwxyz")
print ms.isin("123456789")
True
False
search()方法可用于在目标字符串中搜索关键字所在的位置,它的第二个参数为一个回调函
数,每找到一个匹配位置就将该位置和匹配的关键字传递给该回调函数。回调函数返回0 表示
继续搜索,返 回 1表示结束搜索。
ms•search(__123abc456xyz789abc", process)
found abc at 3
found xyz at 9
found abc at 15
还可以使用 iter_search〇方法返回一个选代器:
for pos, pattern in ms.iter_search("12Babc456xyz789abc"):
print "found {0 } at {1 }".format(pattern,pos)
found abc at 3
found xyz at 9
found abc at 15
在开始编写扩展类型之前,让我们先看看在 C 语言中如何使用该库:
#include <stdio.h>
#include "ahocorasick.h"
/ * 搜索关键字列表*/
AC_ALPHABET_t * allstr[] = {
"recent% •'from% "college"
};
C y t h o n编
/ * 搜索文本V
-译
AC一LAPHABET—
t * input—text = {"She recently graduated from college"};
P y th o n程 序
/ / * * * 匹配时的回调函数
int match_handler(AC_MATCH_t * m, void * param)
{
unsigned int j;
AC_AUTOMATA_t * acap;
AC一PATTERN—t tmp_patt;
AC一ETXT一ttmp一 text;
//*** 创建AC_AUTOMATA_t结构体,并传递回调函数
acap = ac_automata一init();
/ / * * * 添加关键字
60
Python 科学计算(第 2 版)
//***结束添加关键字
ac—automata—ifnalize (acap);
/ / * * * 设置待搜索字符出
tmp_text.astring = input_text;
tmp_text.length = strlen(tmp_text.astning);
/ / * * * 搜索
ac_automata_search (acap, &tmp_text, match_handler, NULL);
C y t h o n编
Q,
//***释放内存
-译
ac_automata_release (acap);
P y th o n程 序
return 0 ;
}
由上面的程序可以看出,整个函数库都是围绕 A C _A U T O M A TA _t 结构体进行处理的。这
是 C 语言封装数据的一种常用方式。在 C yth on 中使扩展类型对这种函数库进行包装时,通
常会创建一个指向此结构体的指针属性,并在_ cinit_〇和_ dealloc_ ()中分配和释放此结构体。
然后定义一些 d e f 方法,调 用 C 语言函数库提供的各个A P I 函数以实现封装。
下面我们分段介绍如何将C 语言的闲数厍使用扩展类型进行包装。
60
ctypedef struct AC_AUTOMATA_t:
AC_MATCH_t match
0
AC_AUTOMATA_t * ac_automata_init()
AC_STATUS_t ac_automata_add(AC_AUTOMATA_t * thiz, AC_PATTERN_t * pattern)
void ac_automata_finalize(AC_AUTOMATA_t * thiz)
int ac_automata_search(AC_AUTOMATA_t * thiz, AC_TEXT_t * text, int keep,
AC一MATCH_CALBACK_f callback, void * param)
c y t h o n编
void ac_automata_settext (AC_AUTOMATA_t * thiz, AC_TEXT_t * text^ int keep)
AC__MATCH__t * ac__automata_findnext (AC_AUTOMATA__t * thiz)
—译
void ac_automata_release(AC_AUTOMATA_t * thiz)
P y th o n程 序
0 * 0 * 先滞要通过c d e f extern f r o m ...告诉 C y t h o n : 编译之后的 C 语言程序滞要包含ahocorasick.h
头文件。由 于 C y t h o n 不会自动解析 C 语言的头文件,因此还需要将其中用到的类型、常量和
函数原型都用C y t h o n 的语法声明一遍。
© 定义闲数指针类型 M A T C H _ C A L B A C K _ f ,它是指肉丨Hi 调函数的指针类型。其第一个参
数为指向保存匹配数据的结构体的指针,第二个参数是可以指向任何额外数据的指针。C 语言
中通常用这种 void *类型的指针传递用户自定义的数裾。
©〇 定义枚举类型和结构体类型,只需要定义在 C y t h o n 程序中用到的枚举成员和结构体的
字段即可。
如果在 C y t h o n 中不访问茶结构体的任何字段,
可以使用 p a s s 关键字代替字段的定义。
© 定 义 C y t h o n 程序中将要调用的函数原型。
将上面这一大段程序编译成C 语言程序之后,只有#include " a h o c o m s i c k . h ”一句,而其余的
类型声明则告诉 C y t h o n 如何编译对这些类型进行操作的语句。例如结构体 A C _ P A T T E R N _ t 中
的 l e n g t h 字段被声明为 u n s i g n e d int 类型,因此在必要的时候 C y t h o n 会调j |j P y t h o n / C A P I 以在
P y t h o n 的整数对象和 u n s i g n e d int 类型之间进行转换。
接下来是 M u l t i S e a r c h 扩展类型的定义:
09
Python 科学计算(第 2 版)
pattern.length = len(keyword)
err = ac_automata_add(self,_auto, Spattern)
P y th o n程 序
if err != ACERR_SUCCESS:
raise ValueError("Error Code:%d" % err)
ac_automata_finalize(self._auto)
0 _ a u t o 属 性 是 一 个 指 向 A C _ A U TO M A TA _t 结 构 体 的 指 针 。 在 _ cinit_ ( ) 中调用
ac_ automata_ init〇为其分配内存,而在_ dealloc_ {)中调用 ac_automata_release()以释放内存。
© AC _AU TO M ATA _t 结构体分配成功之后,
调用 cd ef 函数 add()将所有关键字添加进该结构体1 1。
© 在 add()内部对 keywords 参数进行迭代,将其每个兀素都当作bytes 类型处理,通过<char
将其转换成 C 语言的字符指针类型,和芄长度一起使j |j A C _PATTERN _t 结构体打包之后传
递 给 ac_automata_add()。凼于在该函数返N 之后,ahocorasick 内部的函数不会再使字符指针指
向的内容,因此这种做法是安全的。如果在后续的函数调用中需要使用字符指针指向的内容,
则需要对 keywords 中的每个字符串对象进行引用,保证它们不会被提前垃圾回收。
接下来是 isin〇方法的定义:
isin_callback〇的第一个参数是描述匹配信息的结构体指针,第二个参数为指向 MultiSearch
对象的指针。〇首 先 将 void *类型的指针转换为 M ultiSearch 对象,然后就可以通过 m s 访问
MulUSearch扩展类中定义的属性和各种方法了。© 设置 MultiSearch 对 象 的 found 厲性为 True ,
C y t h o n编
表示找到一个匹配位置。© 由于 isin〇只需要找到一个匹配位置即可,因此函数返回1,表示不
需要继续搜索了。
-译
下面定义 searchO, 它的第一个参数为搜索的目标字符串,第二个参数是 Python 的可调用
P y th o n程 序
对象,每找到一个匹配位置就调用该对象进行处理:
是直接保存捕获的异常,这样才能保证异常的回溯信息能正确指示出错的位置。
ahocorasick 库中还提供 / ac_automata_settext 〇和 ac _aut o m a t a _ f i n d n e x t 〇, 使用这两个函数可
—译
以编写如下生成器函数iter_search〇:
P y th o n程 序
10.6.1 创 建 ufu n c 函数
N u m P y 的 u f t m c 函数是一种能对数组的每个元素进行操作的函数,而 N u m P y 的 C - A P I 提供
了通过 C 语言创建 uftinc 函数的方法,请感兴趣的读者访问下而的网址来阅读相关的教程:
http://docs.scipy.org/doc/numpy-dev/user/c-info.ufunc-tutorial.html
由于 C y t h o n 最终被翻译成 C 语言程序,
因此我们可以使W C y t h o n 程序调用 N u m P y 的C - A P I
来创建 u f u n c 函数。创 建 uflinc 函数需要三个全局变量:
• fiinctions:保存对一维数组进行循环计算的函数指针,
如果 uflinc 函数支持处理多种dtype
数组,则每种 d t y p e 对应一个涵数指针。
• s i g n a t u r e s : 表 示 u f u n c 闲数的参数和返In丨值的字符数组。
%%cython
from libc.math cimport exp
from numpy cimport (PyUFuncGenericFunction^ npy_intp, import_ufunc,
NPY_D0UBLE, PyUFunc_None, PyUFunc_FromFuncAndData) O
import一ufunc() ©
double x,y
for i in range(n):
x = (〈 double *>in_ptr)[0 ]
y = 1 . 0 / (1 . 0 + exp(-x))
Python 科学计算(第 2 版)
(<double *>out_ptr)[0 ] = y
in_ptr += in_step
outjDtr += out_step
cdef:
PyUFuncGenericFunction *functions = [&double_logistic] ®
char *signatures = [NPY_DOUBLE, NPY_DOUBLE] O
void **data = [NULL] ©
1 , #nout
PyUFunc_None, #identity
"logistic"^ #name
"a sigmoid function: y = 1 / ( 1 + exp(-x))_、 #doc string
c y t h o n编
0 ) # unused
—译
用 void *类型的指针。
由上而的参数可知,PyUFuncGenericFunction类型的函数可以处理各种元素类型的数组,而
数组元素无须连续存储,可以处理使用切片获得的数组视图,并且输入数组的个数和输出数组
的个数是任意的。
Osignatiu^ 中保存表示 ufim c 函数的输入和输出数组的元素类型,本例中输入和输出都是
双精度浮点数,因此值为[N PY _D OUBLE ,
N PY _ DOUBLE ]。注意这里使用的是 Cython 中初始化
数组的语法。它相当于:
61
signatures [0] = NPY_CXXJBLE
signatures[1] = NPY_DOUBLE
C y t h o n编
函数。只需将 double_ logistic()中的所有 double 都替换为 float 即可。
-译
然后如下定义 functions、signatures、data 参数:
cdef:
P y th o n程 序
PyUFuncGenericFunction *functions = [&double一logistic, &float一logistic]
char *signatures = [NPY一DOUBLE, NPY一DOUBLE, NPY_FLOAT, NPY一FLOAT]
void **data = [NULL, NULL]
logisticl([-l, 0 , 1 ])
array([ 0.26894142, 0.5 ,0.73105858])
上 面 的 d 〇uble_bgistiC ()虽然不难编写,但是对每个数学函数都进行类似的包装却是令人厌
烦的重复劳动。幸 好 可 以 利 N u m P y 提供的一些辅助函数将单个数值的运总函数转换成u f t m c
函数。在下面的例子中,Oscalar_logistic() 是一个输入和输出都为d o u b l e 的单数值运算闲数。©
我们将该闲数通过额外参数d a t a 传递给 N u m P y 提供的 P y U F u n c _ d _ d 〇闲 数 。
© P y U F u n c _ d _ d 〇对作为输入和输出的两个一维 d o u b l e 数组进行循环, 将输入数纟J1 中的每个
元素传给 d a t a 指向的函数进行计算,并将结果写入输出数组中。
%%cython
from libc.math cimport exp
Python 科学计算(第 2 版)
impont_ufunc()
cdef:
PyUFuncGenericFunction *functions = [PyUFunc_d_d] ©
char *signatures = [NPYJXXJBLE, NPYJXXJBLE]
void **data = [&scalar_logistic] €)
0)
x = np.linspace(-6 , 6 , 10 0 0 0 )
%timeit logisticl(x)
%timeit logistic2 (x)
10000 loops, best of 3: 201 ns per loop
1 0 0 0 loops, best of 3: 208 [xs per loop
%%cython
include "numpy_ufuncs.pxi"
from libc.math cimport exp
logistic3 = register一ufunc_d(scalar_logistic,
"logistic", "logistic function' PyUFunc—None)
peaks = register_ufunc一
dd(scalar_peaks,
"peaks", "peaks function"^ PyUFunc_None)
Y, X = np.ogrid[-2:2:100j, -2:2:100j]
pl.pcolormesh(X> Y, peaks(X, Y))
1 0 . 6 . 2 快速调用 D LL 中的函数
C y t h o n编
通 过 Python 的标准碎 ctypes 可以很方便地调用动态链接Pp中的闲数。但是由于 ctypes 在调
-译
用实际的函数之前需要进行许多预处理工作,因此函数的调用效率并不商。如果需要在循环中
大M 调用,这种预处理带来的损耗会极大地影响程序的执行速度。木节介绍如何通过ctypes 找
P y th o n
到动态链接库中的函数的地址,然后将地址传递给 Cython 的函数进行循环调用,提高函数的调
程序
用效率。
首先用 C 语言编写函数peak〇,为了确认是否能正确获取该函数的地址,
我们通过 get_addr〇
返 M peaks()的地址:
%%file peaks.c
#include <math.h>
double peaks(double x, double y)
{
return x * exp(-x*x - y*y);
}
调用该函数:
import ctypes
lib = ctypes.CDLL("peaks.dll")
lib.peaks.argtypes = [ctypes.c_double, ctypes.c_double]
lib.peaks•restype = ctypes.c一double
lib.peaks(1 .0 , 2 .0 )
0.006737946999085465
〇首先通过 ctypedef关键字声明二元双精度抒点数函数指针类型Function。
© 在 vectorize_2d 〇
中首先通过 cast〇获得函数的地址,然后©将该地址转换成 Function类型的函数指针,接下来就
程序
可以在双重循环中通过该函数指针快速调用其指向的C 语言函数了。
%%cython
import cython
import numpy as np
from ctypes import cast, c_void_p
@cython.wraparound(False)
@cython.boundscheck(False)
def vectorize_2 d(func, d o u b l e t : : 1 ] x, d o u b l e t : : 1 ] y):
cdef d o u b l e [ ::1 ] res = np.zeros_like(x.base)
cdef size_t addr = cast(func, c一 void_p).value ©
cdef Function funcjDtr = <Functionxvoid *>addr ©
cdef int j
return res.base
下面通过 vectorize_2d 〇调 用 lib.peaks 〇, 并与通过 vectorizeO 创建的 u f i m c 函数的结果比较:
Y , X = np.mgrid[-2:2:200j , -2:2:200j]
True
PyUFuncGenericFunction 函数指针,因此滞要同时修改这两个函数的额外参数。
C y t h o n编
%%cython
-译
from ctypes import cast, c一void_p
P y th o n程 序
cdef class UFunc_dd:
cdef public object ufunc O
Y, X = np.ogrid[-2:2:200j, -2:2:200j]
u f u n c _ d d .s e t _ f u n c (c t y p e s .c d l l .m s v c r t .a t a n 2 )
1 0 . 6 . 3 调 用 BLAS 函数
1.包 装 saxpy()函数
import numpy as np
blas.saxpy(x, y, a=0.5)
-译
blas.saxpy
<fortran o b j e c t >
import ctypes
saxpy_addr = ctypes.pythonapi.PyCObject_AsVoidPtr(
ctypes•py一object(bias•saxpy•一cpointer))
saxpy一addr
196931082
获得函数的地址之后,还需要知道函数的调用原型。通过下面的网址查看 saxpy()的帮助文
档,可知其调用参数如下:
20
subroutine saxpy(
integer
real SA,
real, dimension(*) sx,
integer INCX,
http://www.netlib.org/lapack/explore-html/d8/daf/saxpy_8f.html
,
‘
:B L A S 库 中 s a x p y 〇的函数原型。
注 意 F o r t r a n 语言采用传址调用,即调用函数时传递的参数和函数中接收的参数是相同的内
存地址。因此其 C 语言的函数原型为:
C y t h o n编
void saxpy(int *N, float *SA, float *SX, int *INCX, float *SY, int *INCY);
-译
在 C y t h o n 程序中声明指卩彳s a x p y 〇的闲数指针类麼 s a x p y _ p t r , 并通过前面介绍的方法获得
P y th o n程 序
Cython 中循环的 cython_saxpy():
%%cython
import cython
from cpython cimport PyCObject_AsVoidPtr
ctypedef void (*saxpy_ptr) (const int *N, const float *alp h a >
const float *X, const int *incX, float *Y, const int *incY) nogil
cdef saxpy_ptr _ s a x p y =<sa x p y j 3 t r > P yC0bject_AsVoidPtr(blas.saxpy,_cpointer)
@ c y t h o n .w r a p a r o u n d (False)
@cython.boundscheck(False)
def cython_saxpy(float[:] y, float a, f l o a t [:] x ) :
cdef int i
for i in range(y . sh a p e [ 0 ] ) :
y[i] += a * x[i]
62
Python 科学计算(第 2 版)
a = np.arange(1 0 0 0 0 0 , dtype=np.float32)
b = np.zeros_like(a)
%timeit blas_saxpy(b, 0 .2 , a)
%timeit cython一saxpy(b, 0 .2 , a)
10000 loops, best of 3: 28.6 ns per loop
10000 loops, best of 3: 98.3 \xs per loop
2. dgemm 〇高速矩阵乘积
http://www.netlib.org/lapack/explore-html/d7/d2b/dgeinm_8f.html
D G E M M 的说明文档。
C = alpha*op(A)*op(B) + beta*C
-译
果 C 都 是 Foitmn 格式的数组。
Fortran 闲数的 dgemm ()的参数如 K :
subroutine dgemm (
character TRANSA,
character TRANSB,
integer
integer N,
integer K,
double precision ALPHA,
double precision. dimension(lda,*) A,
integer LDA,
double precision. dimension(ldb,*) B,
integer LDB,
double precision BETA,
double precision. dimension(ldc>*) C,
integer LDC
下面为其 C 语言的调用函数原型:
62 :
double *alpha,
double *a, int *lda,
double *b, int *ldb,
double *beta,
double *c, int *ldc)
在下而的 C y t h o n 函数 dge_ ( A ,
B ,index)中,A 和 B 为两个 C 语言格式的三维数组,形状分
别为(La ,M ,K )和(Lb ,K ,N )。index 是一个形状为(Lc ,2)的整型数组。该函数对 index 中的每对整
数 j 、k 计 算 C [i]= A [j]* B [k],其 中 i 为该对整数的下标。因此函数的返回值C 是形状为(Lc ,
N,M)
的三维数组。可以将 C 看 作 L c 个 F o r t r a n 格式的二维数组。
由于存収内存视图的元素吋不涉及任何与P y t h o n 有关的操作,而每对矩阵的乘积运算相对
独立,因此可以对这部分进行并行运总:。C y t h o n 使 用 O p e n M P 实现并行运兑,因此在编译时需
要设置编译和连接参数- f o p e n m p 。
〇载入并行运箅的 prangeO函数,该函数会被编译成使)|j 并行运箅的循环。© 设 置 其 nogil
参数为 T o ie ,表示在并行运算时释放Python 的全局锁。
C y t h o n编
%%cython -c-Ofast -c-fopenmp --link-args=-fopenmp
-译
from cython.parallel import prange O
P y th o n程 序
import cython
import numpy as np
from cpython cimport PyCObject_AsVoidPtr
from scipy.linalg import bias
@cython.wraparound(False)
@cython.boundscheck(False)
def dgemm(double[ , :] A, doublet:
, :] B, int[: ::1 ] index):
cdef int n, k, i, length, idx_a, idx_b
cdef double[:, :^ :] C
cdef char ta^ tb
cdef double alpha = 1 . 0
cdef double beta = 0 . 0
62:
P y th o n 科学计算(第2 版)
length = index.shape[0 ]
m, k, n = A.shape[l], A.shape[2], B.shape[2]
C = np.zeros((length, n, m))
ta = "T"
tb = ta
return C.base
-译
a = np.ascontiguousarray(a).astype(np.floaty copy=False)
b = np.ascontiguousarray(b).astype(np.floaty copy=False)
if a.ndim == 2 :
a = a [ N o n e , :]
624
if b.ndim == 2 :
b = b[None,
shape_a = a.shape[:-2 ]
shape_b = b.shape[:-2 ]
len—a = np.prod(shape一a)
len_b = np.prod(shape_b)
C y t h o n编
if a.ndim > 3:
a = a.reshape(-l, a.shape[-2 ], a.shape[-l])
-译
if b.ndim > 3:
P y th o n程 序
b = b.reshape(-l^ b.shapef-2 ], b.shape[-l])
if a.shape[-l] != b.shape[-2 ]:
raise ValueError("can't do matrix multiply because k isn't the same")
c = dgemm(a<, b, index)
c = np.swapaxes(c> -2 , -1 )
c.shape = bshape + c.shape[-2 :]
return c
62:
Python 科学计算(第 2 版)
运算速度相差近6 倍。
%timeit matrix_multiply(a, b)
%timeit umath.matrix—multiply(a, b)
10 loops, best of 3: 47.8 ms per loop
1 loops, best of 3: 313 ms per loop
Mi l ^
实例
作为本书的最后一章,让我们综合前面章节介绍的各个扩展库,编写一些有趣的实际程序。
1 1 . 1使用泊松混合合成图像
图 1 1 -1 泊松混合示意图
11.1.1泊松混合算法
纹理信息通过拉普拉斯算子计算,可以直接通过 O p e n C V 中 的 Laplacian〇来计算。对于
3 x 3 的情况,LaplacianO实际上就是使用如下卷积核与图像进行卷积运算:
0 1 0
结果相冋。图 11-1显示了泊松混合运兑过程。从左侧幵始分別为:
• 源 图 像 ,苒中指定的区域采用浅灰色表示。
•对源图像进行拉普拉斯运兑的结果,这里只显示指定区域内的值。
• n 标图像,其 中 用 “?”表示未知值。这些未知值要保证对于指定的区域内, FI标图像
的拉普拉斯运算结果与源图像相同。
• 对 n 标图像中的未知值进行编号,以便生成方程组。
如果用未知数 xO 到 x 8 表 示 閤 11-1(左3)中的9 个未知的像素点,那么它们的值需要满足如
下方程组:
-4*x0 + 85 + 77 + xl + x3 = -7
-4*xl + 79 + x0 + x2 + x4 = -32
•••
-4*x8 + 86 + 96 + x5 + x7 = 14
毎个方程的右边的值就是毎个未知像素对应的源阁像的拉普拉斯运算值,而方程左边则是
通过拉普拉斯卷积核展开的公式。每个未知像素的上下左右4 个相邻的像素可能是已知的值或
未知的像素。
因此计算泊松混合最终是求解这样一个方程组。我们知道 numpy .linalg.s d Ve〇可以用来解线
性方程组,但是由于未知数的个数就是区域中像素的个数,因此通常泊松混合所涉及的未知数
是非常多的。例如,对 于 一 个 100X 100的区域进行混合,需要创建-•个10000X 10000的系数
矩阵A 。
显然需要采丨lj S c iP y 的稀疏矩昨运算相关函数:scipy.sparse.linalg.spsolve()。剩下的问题就是
如何创連 spsolve()所需的两个系数矩陈 A 和 b 。
从上面的例子可以看出,所有的方程都是一利•形式,但是根据邻接像素是已知像素还是未
知像素,实际的方程是不冋的。这样我们需要对每个未知像素的邻接像素进行判断,从而增加
了创建矩阵A 的难度。
既然使用稀疏矩阵求解方程组,
冈此不在乎再多一些未知数。
如 图 11-1(右)
所示,我们将指定的区域使用膨胀运算扩展一圈像素。阁中浅灰色是原始的混合区域,深灰色
为浅灰色区域膨胀后增加的区域。我们把灰色区域内的所有像素都当作未知数,其中浅灰色像
素对应的方程由拉普拉斯运算决定:
-4*x4 + X0 + x3 + x5 + x9 = -7
-4*x5 + xl + x4 + x6 + xl0 = -32
而深灰色像素对应的方程就是对应的H 标阁像中的值:
X0 = 85
xl = 79
x2 = 78
只需要对上述两种方程编写程序即可创建稀疏矩阵A 。
11.1.2编写代码
我们要编写的函数的参数如下:
import cv2
〇甶于遮罩图像在目标图像和源图像中存在位置偏移,因此先创建固标图像的一个遮罩数
Python 科学计算(第 2 版)
variable_count = len(dst_x2 )
variable—index = np.arange(variable_count) O
x0 = variables[dst_y , dst_x ] ©
xl = variables[dst_y-l, dst_x ]
x2 = variables[dst_y+lj dst_x ]
x3 = variables[dst_y , dst一 x-1]
S x4 = variables[dst_y , dst一 x+1]
x_edge = variables[dst_ye, dst一xe] ©
0( clst_x 2,
dst_y 2)中的每个像素都与一个未知丨数对应,我们为这些未知丨数编上序号
variable_ index, © 并得到(dst_ x ,
dst_ y )中每个未知数及其上下左右4 个相邻的未知数的序号:xO、
x l 、x 2、x 3、x4 ,© 而 x_ed ge 则是与(dst_ xe ,
dst_ ye)中的未知数对应的序号。
接下来计箅线性方程组中各个未知数的系数矩阵A :
与(dst_x ,
dst_j )屮的每个未知数对应的方程有5 个系数,与(dst_ xe ,
dst_j e )中的未知数对应的
方 程 有 1个系数,因此最终的系数矩卩彳:中有丨11此1*_(:
〇111^*5+€(^6_(:
〇11丨11个非零值。与\0中的每
个未知数对应的方程中,x O 的系数为- 4 , 而其上下左右4 个未知数的系数为1。r 和 c 保存的
是 A 中非零值对应的 K 标,而 v 中保存的是系数,这些系数中除了下标为(xO, xO)的元素为-4
之外,其 余 的 都 为 1。最 后 通 过 coo_matrk ()创建稀疏矩阵,并且转换成求解方程组时使用的
C S C 格式。
630
from scipy.sparse.linalg import spsolve
order = np.argsort(np.r_[variables[dst__y, dst一
x], variables[dst_ye, dst—xe]]) O
result = dst.copy()
for ch in (0 , 1, 2 ): ©
b = np.r_[src_laplacian[:,ch], dst[dst_ye, dst一xe) ch]] ©
u = spsolve(Aj b[order]) O
u = np.clip(Uj 0j 255)
result[dst_y2 , dst_x2 , ch] = u ©
〇由于方程组是按照未知数的下标顺序排列的,因此我们计算方程组的b 时也需要按照未
知 数 的 K标排列。但是为了方便计算 b ,我们按照(dst_x ,
dst_ y )和(dst_xe ,
dst_ ye)的顺序来计算,
因此需要事先计算order用于对常数项 b 中的值进行排序。© 对三个通道的数组进行循环,© 计
算常数项 b , 其中与(dst_x ,
dst_y )对应的未知数的方程的常数项从源图像的拉普拉斯算子的输出
数组获得,而勹(dst_xe, dst je )对应的米知数的方程的常数项则为H 标阁像对应的像素值。O 调
用 spsolve()对线性方程组进行求解,© 最后将未知数的解写到结果数组中,(
dst_x2, dst_y 2)中的
未知数是按照下标顺序排列的。
图11-2(右)为泊松混合的计算结果。
for ax in axes.ravel():
ax.axis("off")
fig.subplots_adjust(wspace=0.05)
图 1 1 -2 使用泊松混合算法将吉内薇拉•班琪肖像中的眼睛和鼻子部分复制到蒙娜丽莎的肖像之上
Python 科学计算(第 2 版)
11.1.3演示程序
图 11-3泊松混合演示程序的界面截图
1L 2 经 典 力 報 拟
与本节内容对应的Notebook 为: 11-examples/examples-200-physics-simulation.ipynb〇
632
本节以悬链线、最速降线和单摆为例介绍如何使 S c i p y 中的 integrate 和 o p t i m i z e 库模拟简
单的经典力学现象。
11.2.1悬链线
将绳子的两端固定在同一水平高度,绳子因重力作用而垂下所形成的形状被称为悬链线,
它的曲线函数为:
x
y = acosh -
a
x = np.linspace(0 j 1, 10 0 )
for a in [0.35, 0.5, 0.8]:
pi.plot(Xj catenary(Xj a), label="$a={:g}$".format(a))
ax = pl.gca()
ax.set_aspect("equal")
ax.legend(loc="best")
pl.margins(0 .1 )
0.8
图 114 各种长度的悬链线
曲线的长度可以使用如下定积分来计算:
S=1j1+(^)2dX
K面我们先利用前而的 catena^ ) 闲数大致计算曲线的长度:
y = catenary(xJ 0.35)
np.sqrt(np.diff(x) * * 2 + np.diff(y)**2 ).sum()
Python 科学计算(第 2 版)
1.37655226488
def catenary_length(a):
return integrate.quad(lambda x:fs(x, a), 0, 1)[0]
length = catenary一length(0.35)
length
S 1.3765789965
1.使用运动方程模拟悬链线
为了使用牛顿力学中的运动方程模拟悬链线,我们可以把悬链看成由多个弹黄•连接的质点
系统,每个质点受到重力以及左右两个弹簧力。当质点运动时,它还会受到大小与速度成正比
的阻力。为了使悬链两端的质点保持静止,可以不计箅其受力,因此这两个质点的加速度始终
为 0。每个质点有 X 和 Y 方向上的速度与加速度共4 个状态,对于由 N 个质点构成的系统,共
有 4 X N 个状态。
下而的 diff_status(staRiM) 计算状态为status 时的微分,
然后使用 odeint 〇对该系统进行积分,
即可计算该系统在不同吋刻的状态。当时间足够长时,由于阻力作用,各个质点最终会处于平
衡位置,效果如图11-5所示。
N = 31
dump = 0.2 # 阻尼系数
k = 100.0 #弹簧系数
1 = length / (N - 1) #弹黄原长度
g = 0.01 #重力加速度
x0 = np.linspace(0, 1, N)
y0 = np.zeros一like(x0)
vx0 = np.zeros_like(x0)
vy0 = np.zeros_like(x0)
634
dvx = np.zeros一like(x)
dvy = np.zeros一like(x)
dx = vx
dy = vy
s = np.s_[l:-l]
dll = ( 1 1 - 1 ) / 1 1
dl2 = ( 1 2 - 1 ) / 1 2
dvx[s] = -vx[s] * dump - (x[s] - x[:-2 ]) * k * dll - (x[s] - x[2 :]) * k * dl2
dvy[s] = -vy[s] * dump - (y[s] - y[:-2 ]) * k * dll - (y[s] - y[2 :]) * k * dl2 + g
return np.r_[dx, dy, dvx, dvy]
tS
t = np.linspace(0 j 50, 1 0 0 )
n = integrate.odeint(diff_status, status©, t)
x, y ,vx, vy = r[-l].reshape(4, -1)
r, e = optimize.curve_fit(catenary, x, -y, [1 ])
print "a =",r[0 ], "length =_•,catenary_length(r[0 ])
x2 = np.linspace(0 , 1, 1 0 0 )
pi.plot(x2, catenary(x2, 0.B5))
pi.plot(x2 , catenary(x2 , r))
pi.plot(x, -y, "o")
pl.margins(0 .1 )
a = 0.336992602016 length = 1.40946777721
阁 11-5使⑴运动方程校拟悬链线,啦于弹黄会被拉伸,因此悬链线略比原始K:度
635
Python 科学计算(第 2 版)
在 阁 11-5中,圆点表示各个质点的最终位罝,红色丨11|线为使用悬链线方程对质点位®进行
拟合后得到的最佳拟合悬链线,而蓝色丨11|线为弹簧保持原长时的悬链线。为了使得最终状态接
近原长时的悬链线,需要尽量大的弹黄系数和尽量小的重力加速度,这样能保证每根弹簧接近
原长。读者可以试着修改前面的系数,使最终状态尽量接近蓝色|11|线。
2.通过能量最小值计算悬链线
当质点之间为刚性连接时,弹黄不存储彺何弹性势能,重力使得整个系统的重力势能降为
最低,因此可以通过最小化势能计兑各个质点的最终状态。由于悬链线的两端固定,而质点之
间的距离固定,因此该最小化问题带有许多约束条件。为了尽量减少约朿条件,我们以每个连
接杆的角度为变量表示整条悬链线的状态。悬链线的一端固定在(〇,〇)处,经过每个连接杆最终
到达坐标(1,
0)处。因此满足如下两个约束条件,其中^为每根杆的方向,1为杆的长度。整个系
I 统 如 图 11-6所示。
21 cos0j = l J l s i n A = 0
第 i 个质点的 Y 轴位置为
yi = 2 ^ lsini9k
k=0
而势能P 可以用下式表示:
P = Syi
■ T 1
3.0
图 11名把姑链线分为多个质点并用无质f i 的连接杆相连
因此最小化的问题就是找到一组卟它们满足两个约束条件,并且使得P 最小, 取值
范围为f < 0丨< ^ 。这种带等式约束条件的最小化问题可以使用scipy.optimize.fmin_jdsqp ()进行
求解。
636
在下面的程序中,g l ()计贷:最右端点的横坐标需要满足的条件,g 2()为纵坐标需要满足的条
件,这两个函数返回0 时,表示满妃约朿条件。P (theta)计算状态为 them 时的势能。O 为了提高
优化的计算速度,我们让初始值满疋两个约朿条件,如 图 11-7中的叉点所示。© fmiii_slsqp()的
eqcons 参数是计算等式约束条件的闲数列表。© bounds 参数是每个变量的取值范围列表。此外,
如果最小化问题中存在不等式约束条件,可以通过 ieqcons 参数指定。当约朿条件很多时,为了
减少函数的调用参数,可以使用 f_eqC〇
n s 和 U eq co n s 参数指定一个计算约束条件的闲数,这两
个函数返回的数组表示各个约束条件。
N = 30
1 = length / N
def gl(theta):
def g2(theta):
return np.sum(l * np.sin(theta)) - 0 . 0
s
def P(theta):
y = 1 * np.sin(theta)
cy = np.cumsum(y)
return np.sum(cy)
bounds=[(-np.pi/2, np.pi/2)]*N) ©
Iterations: 9
Function evaluations: 288
Gradient evaluations: 9
x _ i n i t = n p .r _ [0 , np.cum sum (l * n p . c o s ( t h e t a _ in it ) ) ]
y _ i n i t = n p .r _ [0 , np.cum sum (l * n p . s i n ( t h e t a _ i n i t ) ) ]
637
Python 科学计算(第2 版)
x2 = np.linspace(0, 1, 100)
pi.plot(x2, catenary(x2, 0.35))
pl.plot(x, y, _•〇")
pl.plot(x_init, y_init, "x")
pl.margins(0.1)
11.2.2最速降线
所谓最速降线问题,是指在两点之间建立一条无摩擦的轨道,使得小球从高点到低点所需
的时间最短。考虑两点高度相同的极端情况,显然这条llll线不是直线。根据维S 百科的相关介
绍,下降高度为D 的最速降线满足如下方程:
y
dx = dy
D-y
X, _ = integrate.quad(lambda y:np.sqrt( y / ( 1 . 0 - y ) ) , 0, 1)
print x
1.57079632679
可以看出曲线终点和起点之间X 轴的差为tt/2。
1.使 用 〇deint()计算最速降线
使JljodeimO 对下面的微分方程进行积分即可得到最速降线的曲线:
dy _ D-y
dx A y
638
下面是计算最速降线的程序,效 果 如 图 11-8所示。当y = 0 时,曲线的切线为垂直方向,
d y / d x 为无穷大。O 限制y 的值必须大于极小值ep s 并 小 丁 这 样 才 能 保 证 积 分 能 正 常 进 行 。
© 使 W 微分方程计算的曲线只是左半边的曲线,完整的曲线相对于x = Dti/2对称。为了
odeintO能计算完整的曲线,需要根据x 的值判断£的符号。
x0 = np.linspace(0, D * np.pi, N)
y = integrate.odeint(f, 0 , x0 )
return x0 , y.ravel()
tS
x, y = brachistochrone_curve(2 .0 )
pi.plot(x, -y)
图 11-8使 用 odeint〇计 算 速 降 线
2.使用优化算法计算最速降线
639
Python 科学计算(第 2 版)
N = 100.0
target = 1 0 . 0
x = np.linspace(0, target, N)
tmp = np.linspace(0, -1, N // 2)
y0 = np.r_[tmp, tmp[::-l]]
g = 9.8
def total一time(y):
s = np.hypot(np.diff(x)> np.diff(y)) O
v = np.sqrt( 2 * g * np.abs(y)) O
avg_v = np.maximum((v[l:] + v[:-l])*0.5, le-10) ©
t = s / avg_v
return t.sum()
def fix_two_ends(y):
return y[[0 , -1 ]]
I y一o p t = o p t i m i z G . f m i n _ s l s q p ( t o t a l _ t i m e J y0, e q c o n s = [ f ix _ t w o _ e n d s ] ) O
pl.plot(x, y0 , "k—", label=u"初始值•■ )
pl.plot(x, y_opt, label=u"优化结果")
x 2 } y2 = brachistochrone_curve(target / np.pi)
初始ffi
优化rsm
( 10
图 11-9使川优化笕法计算iii速降线
O 调 用 hyp〇
t()计算每条线段的长度,© 使用能量守恒公式计算小球到达每点的速度,© 汁
算每:个线段的平均速度,为了防止平均速度为0 导致无法计算时间,这里使用 maximumO将速
640
度的下限设置为10 一10。
〇由于需要保证曲线两个端点的Y 轴坐标为0 , 因此我们使用fmin_slsqp〇优化函数,并用
fix_two_end()保证两个端点的 Y 轴坐标始终为0。
11.2.3单摆模拟
如 阁 11-10所示,由一根不可伸长、质量不计的绳子,上端固定,下端系一质点,这样的
装置叫作单摆。
根据牛顿力学定律,可以列出如下微分方程:
d 20
+ | sin 0 = 0
d t2
其中0为单摆的摆角,^为单摆的长度,g 为重力加速度。此微分方程的符号解无法直接求
出,因此只能调用〇
ddm 〇对其求数值解。
odeimO要求每个微分方程只包含一阶导数,因此我们需要对上面的微分方程做如下变形:
d 0(t ) d v (t )
= v (t ); - | sin 0(t )
dt
下面是利用 odeintO计兑单摆轨迹的程序,摆角和吋间的关系如图11-11所示:
g = 9.8
def pendulum_equations(w, t, 1 ):
th, v = w
dth = v
dv = - g / 1 * sin(th)
return dth, dv
t = np.arange(0 , 10 , 0 .0 1 )
641
Python 科学计算(第2 版)
时间(妙)
图 1 1 -1 1 初始角度为1 弧度的单摆摆动角度和时间的关系
I
1.小角度时的摆动周期
高中物理课介绍过当® 大摆动角度很小时,单摆的摆动周期可以使用如下公式计像
i
T 〇= 2 ti
g
此微分方程的解是一个简谐振动方程,很容易计算其摆动周期。下而我们用 S y m P y 对这
个微分方程进行符号求解:
可以看到简谐振动方程的解是由两个频率相同的三角函数构成的,周期为2 ti
2.大角度时的摆动周期
但是当初始摆角增大时,上述近似处理会带来无法忽视的误差。下面让我们看看如何用数
642
值计兑的方法求出单摆在任意初始摆角时的摆动周期。
g = 9.8
d e f pendulum 一t h ( t , 1, t h 0 ):
t r a c k = in t e g r a t e .o d e in t ( p e n d u lu m _ e q u a t io n s , ( t h 0 , 0 )^ [0 , t ] ^ a r g s = ( l ,))
re tu rn t r a c k [ -l, 0]
th0):
d e f p e n d u lu m jD G r io d ( l>
t0 = 2*np.pi*(l / g)**0.5 / 4
t = fsolve( pendulum—th,t0 ) args = (1 , th0 ) )
return t*4
要计算摆动周期,
只耑要计兑从最大摆角到0 摆角所耑的时间,
摆动周期是此时间的4 倍。
为了计算出这个时间值,首先需要定义闲数pendulurrUhO来计算长度为1、初始角度为 thO 的单
摆在时刻 t 的摆角:
fsdveO 求解时需要一个初始值尽量接近真实的解,用小角度单摆的周期的^作为这个初始
4
643
Python 科学计算(第 2 版)
t
一 K
g
其中的函数K 为第一类完全椭岡积分函数,定义如下:
r /2 de
K (k ) = 「__________
J〇 V l —k 2sin 20
可以用 scipy.special.ellipkO汁算此函数的值:
图 11-12比较两种计算方法,可以看到结果是完全一致的:
图 1 1 - 1 2 单摆的摆动周期和初始角度的关系
1 1 3 推荐算法
与本节内容对应的Notebook 为: ll-examples/examples-300-movielens.ipynbo
644
推荐兑法是指利用用户的-•些行为,通过一些数学兑法,推测出用户可能喜欢的东两。本
节介绍如何通过 M ovieLens 提供的)丨』
户对电影的评分数据进行一些推卷算法方面的研究。
11.3.1读入数据
下面将评分次数少于1 0 的)户和电影的评分删除,这有助于推荐兑法将运算集中在有足
够数据可以分析的用户和电影之上:
#删除评分数少于1 0 的用户和电影
u一count = pd.value_counts(u)
v 一
count = pd.value_counts(v)
mask = (u_count >= 1 0 )[u].values & (v一count >= 10 )[v].values
u, v, r = u[mask], v[mask], r[mask]
为了评价推荐算法的性能,需要将评分数据分为训练用数据和测试用数据两部分。这样才
能防止推荐兑法对训练用数据过度学习造成实际的性能下降。下 面 的 traiiu est_split()完成这个
任务,其 t e s t jiz e 参数为测试用数据占全部数据的比例。这里通过产生一个0 到 1之间的随机
数数组对数据进行分组,得 到 6 个一维数组:u_train、v_train、r_tmin、u_test、v_test、r_test、它
们分别为训练用的用户ID 、训练用的电影 ID 、训练用的电影评分、测试用的用户 ID 、测试用
的电影 I D 和测试用的电影评分。
下面检测学习用的数据包含所有的电影和用户,如果下面的程序抛出异常,则建议修改
seed〇的系数:
11.3.2推荐性能评价标准
S 在讨论具体的推荐算法之前,还需要明确如何评价推荐性能。下面萣两个常用的性能评价
公式:
S ( y t - y P) 2
RMSE = ---------- 5—
a] N
2_ , E (y t - y P) 2
r = Z (y t - n(yt) ) 2
.其中R M S E 的值越接近0 性能越好,而r 2的值通常在0 到 1之间,越 接 近 1越好。
movies_mean = pd.Series(r_train),groupby(v_train).mean()
r_pred = movies_mean[v_test]
646
nmse_avg n2 _avg
1.0122857327818662 0.17722524926979077
通过计算每个电影的平均评分值,可以为所有用户推荐整体最受欢迎的电影,但是无法根
据每位用户的喜好推荐他最可能喜欢的电影。为了实现为不同的用户推荐不同的电影,需要对
电影评分进行矩阵分解。
11.3.3矩阵分解
准备好测试数据以及评价标准之后,下面正式幵始实现推荐算法。该兑法应该能从训练数
据中找到更多的规律,使得其对于测试数据的推荐性能超过上面以均值为预测值的性能。
可以把用户 i 对电影 j 的评分1^按照下面的公式分解成4 部分的和:
r ”= + Ui + Vj + z UikVjk
k=l
r一avg = r_train.mean()
647
Python 科学计算(第 2 版)
ub = x[:nu]
vb = x[nu:]
下面按照公式q ^ + Ui + Vj计算测试数据的预测评分,并与实际评分进行比较:
0.93056259728693425 0.30471011860212072
接下来我们将注意力集中到计算u 和V 的矩阵分解算法上。矩阵分解的自标是尽量接近下
面的 r_train2:
11.3.4使用最小二乘法实现矩阵分解
由于每个方程都包含U 和V 中未知数的乘积,因此它不是一个线性方程组,无法使州前面
的最小二乘法函数 lsqr〇。对于这种方程组的最优化问题,可以假设苏中的一部分未知数已知,
从而将其转换成线性方程组。
如果V 已知,
那么U 为未知数,
个 数 为 & •K 。可以使用最小二乘法对这些未知数进行求解。
这时最小二乘法中的矩阵A 的大小为、 x Nu •K 。同样若U 已知,则V 为未知数,个 数 为 ^ • K ,
这时最小二乘法的矩阵A 的大小为心 x Nv •K 。
648
具体的运兑步骤如下:
(1) 通过随机数产生U 和V 。
(2) 假设V 为已知数,使用最小二乘法对U 进行求解。使用新的解更新U 。
(3) 假设U 为已知数,使用最小二乘法对V 进行求解。使用新的解更新V 。
(4) 使用U 和V 对测试数据进行推荐性能评价。
(5) 转到步骤(2)重复执行,直到推荐性能的评价不再提升。
下而的 m decom paseO 实现上述运算步骤,arrays参数为包括训练数椐和测试数据的6 个数
组。其他参数均用于控制训练的参数。〇通过随机数产生U 和V ,为了保证这两个矩阵相乘后得
到的结果大小基本一致,需要以^为缩放冈子。
A 乂j .K .. .j K + K - l = Vj
A ? ,i . K . .. i K + K - l = U i
U = np.random.rand(nu, k) * 0 . 1 / k**0.5 O
V = np.random.rand(nv, k) * 0.1 / k**0.5
Python 科学计算(第 2 版)
for i in range(loop_count):
U. ravel()[:] = linalg.lsmr(Av, retrain, maxiter=maxitei% damp=damp)[0] ©
实 : Au.data[:] = Au.data[:]*(l-mu) + U[u_train].ravel()*mu O
例丨 -
V. ravel()[:] = linalg.lsmn(Au> r_train, maxiter=maxiter, damp=damp)[0]
; Av.dataf:] = Av.data[:]*(l-mu) + V[v__train] .ravel ()*mu
650
p i . p l o t ( n p . a ra n g e (l^ l e n ( r m s e s l ) + l ) , rm s e s l, la b e l= "d a m p = 3 .5 ")
p l . p l o t ( n p . a r a n g e ( l , le n (r m s e s 2 )+ l), rm ses2, la b e l= "d a m p = 3 .0 ")
p i . le g e n d ( lo c = " b e s t " )
卩 1.父 13匕 61(1]"迭 代 次 数 ")
pl.ylabel("RMSE")
S 10 5 2
A 代次R
还可以使用随机梯度下降法对矩阵进行分解。下面是具体的计算公式。和前而的方法相同,
首先用随机数初始化用户矩阵U 和电影矩阵V 。随机挑选一个评分% , 它是用户 i 对电影 j 的评
分。计算预测评分值
K
^ = 2 U i k . Vjk
k=l
然后计算评分误差 e :
e = Tij - fjj
%%cython
#cython: boundscheck=False
#cython: wnaparound=False
import numpy as np
651
Python 科学计算(第 2 版)
for j in range(ratecount):
uid = userid[j]
mid = movieid[j]
rvalue = rating[j]
pvalue = dot(uservalue, movievalue, uid, mid)
error = rvalue - pvalue
for k in range(nk):
tmp = uservalue[uid, k]
uservalue[uid, k] += eta * (error * movievalue[mid, k]
- beta * uservalue[uid, k])
movievalue[mid, k]+= eta * (error * tmp - beta * movievalue[mid^ k])
rmses =[]
idx = np.arange(nr)
for i in range(iter_count):
np.random.shuffle(idx)
uv_update(u_train[idx], v_train[idx]^ r_train2 [idx],U, V ,eta,beta)
t = U.dot(V.T)
rjDred2 = t[u_test, v_test]
rmse = nmse_score(r_test, r_pred2 )
rmses.append(rmse)
if best_rmse > rmse:
best一rmse = rmse
best_U, best_V = U.copy(), V.copy()
np.random.seed(2 )
U3, V3, best_rmse3, rmses3 = uv一decompose2(arrays, 30, 0.008, 0.08, 100)
pl.plot(rmses3) tS
idx = np.argmin(rmses3)
pl.axvline(idx, lw=l, ls="--")
pi.ylabel("RMSEH)
pl.xlabel(u"迭代次数••)
pi.text(idx^ best_rmse3 + 0.002, "%g" % best_nmse3)
图 11-14随机梯度下降法的收敛曲线
将 U V 分解得到的评分预测加上前面的r_pred, 就得到了最终的评分预测r_pred3。下面使
用 Pandas 的 boxplotO绘制每个评分等级对应的预测评分的箱形图,结果如 图 11-15所示。图中
横坐标为实际评分的5 个等级,每个盒子表示每个等级对应的预测评分的分布情况。
653
Python 科学计算(第 2 版)
阁 1 M 5 以实际评分对〖
獅!If评分分纟丨I,绘制每姐的分布惜况
I 11.4 频 域 信 号 麵
与本节内容对应的Notebook 为: 1l -examples/examples-400-fft.ipynb〇
FFT (快速傅立叶变换)能将时域信号转换为频域信号。转换为频域信号之后,可以很方便
地分析出信号的频率成分,在频域上进行处理,最终还可以将处理完毕的频域信号通过IFFT (逆
变换)转换为时域信号,实现许多在时域无法完成的信号处理算法。木章将通过许多实例,简单
地介绍有关频域信号处理的一些_础知识。
等。数值可以是实数或复数,通常的时域信号都是实数,因此下面都以实数为例。可以把这一
组实数想象成对某个连续信号按照一定収样周期进行収样而得,如果对有 N 个实数的数组进行
F F T 变换,将得到一个有 N 个复数的数组,它的元素有如下规律:
• 下 标 为 0 和 N/2的两个复数的虚数部分为0。
• 下 标 为 i 和 N -i 的两个复数共辄,也就是其:虚数部分数值相同、符号相反。
下面的例子演示了这一规律,先 以 rand〇随机产生有8 个元素的数组 X,然后用 ffi〇对其运
算之后,观察其结果为8 个复数,可以看出结果满足上面两条规律:
x = np.random.rand(8 )
xf = np.fft.fft(x)
654
print x
print xf
[0.361 0.419 0.499 0.558 0.031 0.705 0.419 0.314]
[3.307+0.j -0.044-0.051j -0.526-0.252j 0.706+0.Illj -0.686+0.j 0.706-0.Illj
-0.526+0.252j -0.044+0.051j]
F F T 变 换 的 结 果 可 以 通 过 IF F T 变 换 还 原 为 原 来 的 值 :
n p .fft.ifft( x f)
a r r a y ( [ 0 .361 + 0 .0 0 0 e + 0 0 j, 0 .4 1 9 -1 .0 3 2 e -1 7 j, 0 .499 - 1 .3 8 8 e -1 7 j, 0 .5 5 8 -1 .0 7 6 e -1 6 j,
0.0B1 + 0 .0 0 0 e + 0 0 j,0 .7 0 5 + 1 .1 4 6 e -1 6 j, 0 .4 1 9 + 1 .3 8 8 e -1 7 j, 0 .3 1 4 + 3 .3 7 9 e - 1 8 j])
注 意 iffiO 的 运 算 结 果 实 际 上 和 数 组 X 相 同 , 由 于 浮 点 数 的 运 算 误 差 , 出 现 了 一 些 非 常 小 的
虚 数 部 分 ,可 以 调 用 np.rcal〇获 取 其 中 的 实 数 部 分 。
F F T 和 IF F T 变 换 并 没 有 增 加 或 减 少 数 据 的 个 数 : 数 组 x 中 有 8 个 实 数 数 值 , 而 数 组 x f 中
其 实 也 只 有 8 个 有 效 的 数 值 。 由 于 复 数 共 轭 和 虚 数 部 分 为 0 等 规 律 ,真 正 有 用 的 信 息 保 存 在 下
标 从 0 到 N / 2 的 N /2 + 1 个 复 数 中 ,又 由 于 下 标 为 0 和 N / 2 的 值 的 虚 数 部 分 为 0 , 因 此 只 有 N 个
有效的实数值。
下 面 看 看 FFT变换所得到的复数的含义:
• 下 标 为 0 的实数表不时域信号中的直流成分。
• 下 标 为 i 的 复 数 a + b j表 示 时 域 信 号 中 周 期 为 N /i 个 取 样 值 的 正 弦 波 和 余 弦 波 的 成 分 ,其
中 a 表 示 余 弦 波 形 的 成 分 ,b 表 示 正 弦 波 形 的 成 分 。
让我们通过几个例子验证上述规律,下面对一个直流信号进行F F T 变换:
x = np.ones(8 )
np.fft.fft(x)/len(x) # 为了计算各个成分的能量,需要将 FFT 的结果除以FFT 的长度
array([ l.+0 .j, 0 .+0 .j, 0 .+0 .j, 0 .+0 .j, 0 .+0 .j, 0 .+0 .j, 0 .+0 .j, 0 .+0 .j])
为了便 P 观察结果,
这里丨 array_str〇将数纟11转换字符串,
并设置 s u p p r e s s _ s m a l l 参数为 T r u e ,
将一些很小的数值显示为0。现在观察正弦波的 F F T 的计算结果:下 标 为 1 的复数的虚数部分
Python 科学计算(第 2 版)
为-0.5,而我们产生的正弦波的振幅为1,它们之间的关系是-0.5*(-2)=1。接下来看余弦信号的
F F T 结果:
tmp = np.fft.fft(np.cos(x))/len(x)
print np.array_str(tmp, suppress一small=True)
[-0 .0 +0 .j 0.5-0.j 0 .0 +0 .j 0 .0 +0 .j 0 .^+0 .j -0 .0 +0 .j 0 .0 +0 .j 0.5-0.j]
只 有 下 标 为 1 的复数的实数部分为0.5,和余弦波振幅之间的关系是0.5*2=1。再看两个
例子:
这 里 np.angle()计算复数的福角,得到的是弧度,通 过 np.md2deg()将弧度变换为角度值。在
这个例子中产生了三个频率、振幅和相位各不相同的余弦波:
• 周 期 为 128个取样点的余弦波的相位为0,振幅为0.3。
• 周 期 为 64个取样点的余弦波的相位为45度(tt/4),振幅为0.5。
• 周 期 为 42.66(128/3.0)个取样点的余弦波的相位为-60(-tt/3)度 ,振幅为0.8。
对 照 y flU 、yfl 2〗
、y f p 〗
的复数振幅和辐角,读者应该对 F F T 结果中的每个数值都有很清晰
的认识。
F F T 的运算效率由 F F T 长 度 N 的质因子决定,N 能被分解得越小,运算速度越快。例如当
N 为素数时,F F T 的运算效率达到最低。下面的程序比较4096点 F F T 和 4093点 F F T 运算的吋
间,由于4 0 % 是 2 的整数次幂,而 4093是一个素数,因此它们的运算时间相差非常大:
xl = np.random.random(4096)
x2 = np.random.random(4093)
%timeit np.fft.fft(xl)
%timeit np.fft.fft(x2 )
10000 loops^ best of 3: 183 per loop
10 loops, best of 3: 69.6 ms per loop
11.4.2合成时域信号
在上节的演示中,通 过 iffiO可以将频域信号转换回时域信号,这种转换是精确的。下面的
程序完成类似的频域信号转时域信号的计算。不过可以啦用户选杼一部分频域信号转换为时域
信号,这样转换的结果和原始的时域信号会有误差,使用的频率信息越多,此误差越小。通过
此程序可以直观地观察到多个余弦波的叠加是如何逐步逼近任意时域信号的,图 1M 6 显示了
使 用 F F T 计算的三角波频谱。
def triangle_wave(size): O
x = np.arange(0 , 1, 1 .0 /size)
y = np.where(x<0.5, x, 0)
y = np.where(x>=0.5> 1-x^ y)
return x, y
# 计箅三角波及其FFT
x, y = tniangle_wave(fft_size)
fy = np.fft.fft(y) / fft_size
# 绘制原始的三角波和用正弦波逐级合成的结果,使用的取样点为x 轴坐标
axes[l].plot(y, label=u"原始三角波" , linewidth=2 )
for i in [0,1,3,5,7,9]:
i n dex, d a t a = f f t _ c o m b i n e ( f y , i+1, 2) # 计算两个周期的合成波形
a x e s [ 1 ] . p l o t ( d a t a , l a b e l = " N = % s " % i, a l p h a = 0 . 6 )
I axes[l].legend(loc="best")
图 1 1 - 1 6 三角波的频谱(上),使用频谱中的部分频率重建的三角波(下)
658
接下来再看看合成方波信号。由于方波的波形中存在跳变,因此叫有限个正弦波合成的方
波在跳变处出现抖动现象,如 图 1M 7 所示,片』
正弦波合成的方波的收敛速度比三角波慢得多。
计銘方波的波形可以采用下面的square_wave ():
def squane_wave(size):
x = np.arange(0 > l y 1 .0 /size)
y = np.where(x<0.5, 1 .0 , 0 )
return x, y
x, y = square_wave(fft一size)
fy = np.fft.fft(y) / fft_size
阁 1 M 7 方波的频谱,合成方波在跳变处出现抖动
本书提供了三角波和方波的F F T 演示程序,使用它们可以交互式地观察各种三角波和方波
的频谱及其正弦合成的近似波形。制作界面是一件很费工夫的事情,幸好有 TmitsUI 庳的帮忙,
200多行代码就可以制作出如图11-18所示的效果。
Python科学计算(第 2 版)
ill
o
HUM
,•辠I
II 131
21 II
sn
M
0
«
1
lJSJ
t
M
t
l
m
lf
»
_
t
t
n
s n
t
7
?asi
i
l
«
lffl.
?
»
l
l
i i i i l
sr
SI
u
l
l
l
1 )ft
)l
M»u
t
l
l
)
t
l
t
l itJI
K R
l}
iJ
«}
ll 图 11-18波形频谱观察器界面
u n
li
I scpy2.examples.ffi_demo :使用该程序可以交互式地观察各种三角波和方波的频谱及其
DVD 正弦合成的近似波形。
程序中已经给出了详细的注释,相信读者能够读懂并掌握这类程序的写法。
11.4.3观察信号的频谱
将时域信号通过 F F T 转换为频域信号之后,将其各个频率分量的幅值绘制成图,可以很直
观地观察信号的频谱。下面的程序能完成这一任务:
def show_fft(x):
xs = x[:fft_size]
xf = np.fft.rfft(xs)/fft_size O
freqs = np.linspace(0 _» sampling_rate/2 J fft_size/2 +l) ©
xfp = 2 0 *np.logl0 (np.clip(np.abs(xf)> le-2 0 , lel0 0 )) ®
pl.figure(figsize=(8^4))
pi.subplot(2 1 1 )
pi.plot(t[:fft_size], xs)
pl.xlabel(u__ 时间(秒)")
pi.subplot(2 1 2 )
pi.plot(freqs, xfp)
pl.xlabel(u"频率(Hz)")
660
pi•subplots一adj ust(hspace=0 •4)
show_fft(x)
图 11-19为程序的输出,可以看到频谱中除了两个峰值之外,其余的频率成分都接近于0。
如果放大频谱中的两个峰值,可以看到其值分别为:
661
Python 科学计算(第 2 版)
® 最后计算每个频率分f i 的幅值,
并将其转换为以d B 度 s 的值。
为了防止〇幅值造成 loglOO
无法计算,调 用 np.dipO对 x f 的幅值进行上下限处理。
下面看看不能在 ffL size 个取样中形成整数个周期的波形的频谱:
x = np.sin(2*np.pi*200*t) + 2*np.sin(2*np.pi*300*t)
show_fft(x)
得到的结果如阁11-20所示。这次得到的频谱不再是两个完美的峰值,而是两个峰值频率
周围的频率都有能量。这 然 和 W 个正弦波的咎加波形的频谱有区别。本来应该屈于200H z 和
300H z 的能量分散到了周围的频率中,这种现象被称为频谱泄漏。出现频谱泄漏的原因在于
fft_size 个取样点无法放下整数个200H z 和 300H z 的波形。
时蝴秒1
662
我们只能在有限的吋间段屮对信号进行测量,无法知道测量范围之外的信号。因此只能对
测量范围之外的信号进行假设。而傅立叶变换的假设很简单:测量范围之外的信号是所测量到
的信号的重复。
现在考虑 5 1 2 点 F F T , 从信号中取出的 5 1 2 个数椐就是 F F T 的测M 范園,它计算的是这 5 1 2
个数据一直重复的波形的频谱。显然,如 果 5 1 2 个数据包含整数个周期,那么得到的结果就是
原始信号的频谱;而如果不是整数周期,得到的频谱就是如图11-21所示的波形的频谱。由于
波形的前后不是连续的,存在跳变,而跳变处有着非常广泛的频谱,因此 F F T 结果中出现频谱
泄漏。
pl.figure(figsize=(6 , 2 ))
t = np.arange(0 , 1 .0 , 1.0/8000)
x = np.sin(2*np.pi*50*t)[:512]
pi.plot(np.hstack([x, x, x]))
1.窗函数
为了减少 F F T 所截取的数椐段前后的跳变,可以把数据勹一个窗函数相乘,使得其前后数
据能平滑过渡。例如,常用的 Hann 窗函数的定义如下:
27111
w (n ) = 0.5 1 —cos
‘
N
663
Python 科学计算(第2 版)
阁 11-22 H a n n 窗函数
print signal.hann(8 )
print signal.hann(8 sym=0 )
[0. 0.188 0.611 0.95 0.95 0.611 0.188 0 . ]
[0. 0.146 0.5 0.854 1. 0.854 0.5 0.146]
50H z 正弦波与窗函数乘积之后的周期重复波形如图11-23所示:
pi.figure(figsize=(6, 2 ))
t = np.arange(0 , 1 .0 , 1.0/8000)
x = np.sin(2*np.pi*50*t)[:512] * signal.hann(512> sym=0)
pi.plot(np.hstack([x, x, x]))
200 1200
t = np.anange(0 , 1 .0 , 1 .0 /sampling一rate)
x = np.sin(2*np.pi*200*t) + 2*np.sin(2*np.pi*300*t)
xs = x[:fft_size]
ys = xs * signal.hann(fft size, sym=0)
一
xf = np.fft.rfft(xs)/fft_size
yf = np.fft.rfft(ys)/fft_size
freqs = np.linspace(0 , sampling_rate/2 <, fft_size/2 +l)
xfp = 2 0 *np.logl0 (np.clip(np.abs(xf)> le-2 0 , lel0 0 ))
yfp = 2 0 *np.logl0 (np.clip(np.abs(yf), le-2 0 , lel0 0 ))
pl.figure(figsize=(8,4))
pi .plot (freqs, xfp, label=u"矩形窗••)
pl.plot(freqs, yfp, label=u"hann 窗")
pi.legend()
pl.xlabel(u"频率(Hz)1')
np.mean(signal.hann(512, sym=0 ))
0.5
2.频谱平均
对于频谱特性不随时间变化的信号,例如引擎、压缩机等机器噪声,可以对其进行长时间
的采样,然后分段进行 F F T 计算,最后对每个频率分量的幅值求平均值,就可以准确地测量信
665
Python 科学计算(第 2 版)
号的频谱。下面的程序完成这一计算:
x = np.random.randn(100000)
xf = average_fft(x, 512)
pi.figure(figsize=(7,3.5))
pi.plot(xf)
pi •xlabel(u"频率窗U (Frequency Bin)")
pl.ylabel(u"幅值(dB)")
pi.xlim([0,257])
pi•subplots一 adj ust(bottom=0•15)
可以看到随机噪声的频谱接近一条水平直线,也就是说每个频率窗口的能S 都相同,这种
噪声被称作白噪声。如果让白噪声通过一个 IIR 低通滤波器,绘制其输出信号的平均频谱,就
666
能够观察到1I R 滤波器的频率响应特性。下面的程序利j|j iirdesign()设计一个8k H z 取 样 的 1kHz
的 ChebyshevI 型低通滤波器,
ihdesignO需要用正规化的频率(取值范围为0〜 1),然后调用 filtfiltO
对 A 色噪声信号进行低通滤波。如果用 aVerage_ffi〇计算滤波器输出信号的平均频潜•,将得到如
图 11-26所示的频谱图。
20
ffl
psil
图 11-26经 过 低 通 滤 波 器 的 白 噪 声 的 频 谱
3.谱图
虽然使用 F F T 能够观察信号的频域特性,但却完全丧失了信号在时间轴上的信息。因此前
面所介绍的观察信号频谱的方法只适合于频率特性不随时间变化的情况。当信号频率随时间变
化时,为了既能观察信号频率又能观察其随时间的变化,可以使用短时距傅里叶变换(STFT )。
S T F T 算法所得到的结果被称为谱图(Spectrogram)。谱图的横轴表示时间而纵轴表示频率,
谱图上每点的值表示信号在此点的能量。S T F T 兑法其实很简单:对信号分段进行 F F T 处理,
每一次处理的结果都是谱图中的一列。每段信号的长度越短,时间轴上的精度越高,而频率轴
上的精度就越低,反过来也楚一样。时间轴和频率轴上的分辨率是一对不可调和的矛盾,根据
傅立叶变换的不确定原理,我们不能指望同时获得频率和时间的高分辨率。
下而是绘制频率扫描波的谱图的程序,效 果 如 图 11-27所示。通过此图可以很直观地观察
到信号的频率随着时间而逐渐变高,并且是呈指数增长的。
667
P y th o n 科学计算(第2 版)
sampling_rate = 8000.0
fft_size = 1024
step = fft_size/16
time = 2
图 11-27频率扫描波的谱图
scpy2.examples.spectrogram_realtime:实时观察声音信号谱图的演示程序,
使用 TraitsUI、
Py A udio 等库来实现。
668
1
l
u
10
CM
t_
o o o + ^Ba«r
图 11-28使 用 TraitsUI制 作 的 实 时 观 察 声音信号谱 阁 的 界 而
4.精确测量信号频率 I
F F T 的频率分辨率可以通过“取样频率/F F T 长度”计算,若仅根据频谱峰值的位置测:f fi 信
号频率,则为了精确测M 只能提高 F F T 的长度。木小节介绍一种使用 F F T 结果中的相位信息,
在不增加 F F T 长度的情况下精确测量频率的方法。
下而首先创建一个包含三个频率44H z 、150H z 和 330H z , 以及白色噪声的测试信号X 。 其
収 样 频 率 为 8000H z ,长 度 为 2400点。如果使用频谱峰值测量频率,则频率的最高分辨率为
8000/2400=3.33H z 〇
RATE = 8000
t, x = make_wave([l, 2, 0.5], [44, 150, 330], [1, 1.4, 1.8], 0.3, RATE)
x += np.random.randn(len(x))
669
Python 科学计算(第 2 版)
FFT_SIZE = 1024
spectl = np.fft.rfft(x[:FFT_SIZE] * np.hanning(FFT_SIZE))
freqs = np.fft.fftfreq(FFT_SIZE, 1.0/RATE)
bin一
width = freqs[l] - freqs[0]
amp_spectl = np.abs(spectl)
loc, = signal.argrelmax(amp_spectl, order=3) O
mask = amp_spectl[loc] > amp_spectl.mean() * 3 ©
loc = loc[mask]
peak_freqs = freqs[loc]
print "bin width:% bin_width
print "Peak Frequencies:", peak_freqs
bin width: 7.8125
Peak Frequencies: [ 46.875 148.438 328.125]
可以看出峰值的频率与实阮的频率相比有近3H z 的误差。利用相位信息可以减小检测频率
S 的误差。为了利用相位信息,我们延时256个取样之后再次计算信号的频谱。256个取样值相
当于延时了 dt = 256 / 8000 = 0.032秒。计兑两次 F F T 结果中频谱峰值对应的相位,并计兑相位
差 值 phase_delta。如果在A t 时间之内,相位变化了A 0 , 则频率可以根据如下公式计兑:
△0 + 2 n7i
f ”= -------- n = 0 co
n 2 . '
由于旋转一_ 之后,相位完全相同,因此在A t 时间内,相位变化A 0 的频率有无数个,它是
一个等差数列。只需要找出该等差数列中与根据频谱峰值获得的频率最接近的那个频率即可。
下而首先计算峰值处前后两次F F T 的相位差:
COUNT = FFT_SIZE//4
dt = COUNT / 8000.0
phasel = np.angle(spectl[loc])
phase2 = np.angle(spect2[loc])
然后利用相位差计算信号中各个峰值的频率。〇为了减少运算次数,首先利用频谱峰值中
的最高频率计算 II 的最大可能取值 max_n 。© 利用广播运算,计算各个相位差值所对应的可能
的频率 p〇ssible_ freqs。€)找到可能频率中与峰值频率最接近的那个作为信号的频率。
可以看到结果非常接近信号的真实频率,由于存在白噪声,相位差也存在较小的误差,因
此最终计算的信号频率也存在误差。
670
max一n = (peak一freqs.max() + 3*bin_width) * dt O
n = np.arange(max_n)
possible一
freqs = (phase_delta + 2*np.pi*n[:, None]) / (2 * np.pi * dt) ©
11.4.4卷积运算
1.快速卷积
出于运兑可以高效地将吋域倍号转换为频域倍号,其运兑的复杂度为O (NlogN ) , 因此
三 次 F F T 运兑加-次乘积运兑的总复杂度仍然为O (NlogN )级別,
而卷积运兑的复杂度为0( N 2),
显然通过 F F T 计算卷积要比直接计算快得多。这里假设需要卷积的两个信号的长度都为N 。
但是有一个问题:F F T 运算假设其所计算的信号为周期信号,因此通过上述方法计算出的
结果实际上是两个信号的循环卷积,而不是线性卷积。为了州 F F T 计兑线性卷积,需要对信号
进行补零扩展,使得其长度长于线性卷积结果的长度。
例如,如果要计算数组 a 和 b 的卷积,a 和 b 的长度都为128,那么它们的卷积结果的长度
为 len(a)+ len(b )- 1 = 255。
为了让 F F T 能够计算其线性卷积,需要将 a 和 b 的长度都扩展到256。
下而的程序演示了这个计算过程。
〇找到大于 n 的最小的2 的整数次幂。© fft()的第二个参数为F F T 的长度,当输入数据的长
度不够时,自动对其进行补零。€)最后对两个频域信号的乘积调用ifft(),得到时域信号的卷积。
其结果比实际的卷积结果多一个数,这个多出来的数值应该接近于0 , 请读者自行验证。
def f f t _ c o n v o l v e ( a , b ) :
n = len(a) + len(b) - 1
N = 2**(int(np.log2(n)) + 1 ) O
A = np.fft.fft(a, N) ©
B = N)
return np.fft.ifft(A * B)[:n] ©
a = np.r a n d o m. r a n d (128)
b = np.r a n d o m. r a n d (128)
c = np.convolve(aJb)
P y th o n 科学计算(第 2 版)
由于立接计兑卷积和使用F F T 的快速卷积的复杂度级別不同,因此当卷积数据很长时,可
以观察到明显的速度差别。下面的程序比较两种卷积算法的运算时间:
a=np.random.rand(10000)
b=np.random.rand(10000)
print np.allclose(np.convolve(a> b), fft_convolve(a, b))
%timeit np.convolve(a, b)
%timeit fft_convolve(a, b)
True
10 loops, best of 3: 36.5 ms per loop
100 loops, best of 3: 6.43 ms per loop
显然计算两个很长的数组的卷积时,F F T 快速卷积要比直接卷积快很多。但是对于较短的
数组,直接卷积运算还是更快一些。图 U -29显示了直接卷积和快速卷积的平均计算时间和长
I
度之间的关系。其 中 Y 轴显示的是每个数椐的平均计算时间,因此直接卷积对应的丨111线是线性
的:0( N)。由阁可知对于长度大于1024的卷积,快速卷积显示出明显的优势。
图 11-29比较直接卷积和FFT卷积的运箅速度
r e s u lt s = [ ]
f o r n in xrange(4^ 1 4 ):
N = 2 **n
a = n p .ra n d o m .ra n d(N )
b = n p .ra n d o m .ra n d(N )
t r l = % tim e it -r 1 -o -q n p .c o n v o lv e (a , b)
t r 2 = % tim e it - r 1 -o -q f ft_ c o n v o lv e ( a , b)
672
tl = tnl.best * 1g 6 / N
t2 = tn2.best * le6 / N
results•append((N, tl, t2))
results = np.array(results)
pl.figure(figsize=(8,4))
pl.plot(resul1;s[:, 0], results[:, 1], label=u"直接卷积")
pl.plot(results[:, 0], results[:, 2], label=u"FFT 卷积")
pi.legend()
pi •ylabel (u "计算时间(us/point)••)
卩
1^13匕
6 1 ( ^ 长度_')
pi.xlim(min(n_list)^max(n_list))
2.卷积的分段运算
图 11-30使川oveiiap~add法进行分段益积的过程演示
Python科学计算(第 2 版)
x = np.random.rand(1 0 0 0 )
h = np.random.rand(1 0 1 )
y = np.convolve(x, h)
N = 50 # 分段大小
M = len(h) # 滤波器的长度
output =[]
#缓存初始化为0
buffer = np.zeros(M+N-l,dtype=np.float64)
for i in xnange(len(x)/N):
#从输入信号中读取N 个数据
xslice = x[i*N:(i+l)*N]
#i|-算卷积
yslice = np.convolve(xslice, h)
#将卷积的结果加入到缓存中
buffer += yslice
#输出缓存屮的前N 个数据,注意使用copy, 否则输出的是buffer的一个视图
output.append( buffer[:N].copy() ) O
_ 存中的数据左移N 个元素
buffer[0:-N] = buffer[N:]
#后而的补0
buffer[-N:] = 0
#将输出的数据组合为数组
y2 = np.hstack(output)
#计算和直接卷积的结果之间的误差
674
print "error:", np.max(np.abs( y2 - y[:len(x)]))
error: 7.1054273576e-15
11. 5布尔可满足性问题求解器
与本节内容对应的Notebook 为: 1l -examples/examples-500-picosat.ipynb〇
可满足性用来解决给定的布尔方程式,寻找一组变量赋值,使问题成为可满足。布尔可满
足性问题(以下简称S A T )属于决定性问题,是第一个被证明 N P 完全问题。它是计算机科学中
许多领域的重要问题,包括计算机科学-签础理论、算法、人工智能、硬件设计等。本章介绍如
何 用 Cython 包 装 C 语 言 的 S A T 问题求解器 PicoSAT , 并使用该扩展模块解决数独游戏和扫雷
游戏。
http://frnv.j ku .at/picosat/
免, PicoSA T 的下滅地址。
让我们先通过一个例子解释什么是布尔方程式,以及如何将其.转换为S A T 求解器所需的
合取范式(CN F ) , 并调用 cycosat 对其求解。
一道逻辑推理题
有 4 名嫌疑犯,他们做了如下供述:
•曱: 不是我作的案。
♦ 乙:丁就是罪犯。
籲 丙 : 乙是罪犯。
• 丁: 乙有意诬陷我。
已知4 人当中只有一人说了真话。请推理出罪犯是谁?
675
Python科学计算(第 2 版)
表 1 1 -1 布尔表达式
嫌疑犯 供述 布尔表达式
甲 不逛我作的案 Sl=〜A
乙 丁就是罪犯 S2=D
丙 乙是罪犯 S3=B
丁 乙有意诬陷我 S4=〜D
由 于 4 个 人 中 只 有 一 人 说 了 真 话 ,因 此 有 如 下 4 种 可 能 ,即 列 举 出 只 有 一 个 表 达 式 为 真 的
所 有 组 合 ,其 中 & 表 示 与 运 算 ,丨表示或运算。
在 上 面 的 表 达 式 中 ,每 行 的 表 达 式 是 一 个 与 运 算 ,而 行 之 间 是 或 运 算 。这 种 逻 辑 公 式 被 称
为 析 取 范 式 (D N F )。然 而 S A T 求 解 器 只 能 对 C N F 表 达 式 求 解 。 一 个 满 足 C N F 的 布 尔 表 达 式 由
多 个 子 表 达 式 的 与 运 兑 构 成 ,而 每 个 子 表 达 式 则 山 逻 辑 变 量 的 或 运 兑 构 成 。因 此 需 要 使 用 另 外
的 方 法 来 表 示 S I 、S 2 、S 3 、S 4 中 只 有 一 个 为 真 :
在 上 而 的 表 达 式 中 ,列 举 S I 、 S 2 、 S 3 、 S 4 中 所 有 的 两 两 组 合 ,任 何 一 个 组 合 都 不 可 能 同
时 为 真 ,最 后 一 个 子 句 〜 ( 〜 S1 & 〜 S2 & 〜 S3 & ~S 4) 表 示 4 个 供 述 不 可 能 都 为 假 。 使 用 布 尔 公 式 :
〜 ( A & B ) = 〜 A 卜 B , 可 得 到 下 面 左 侧 的 C N F 公 式 。 把 S l 、 S 2 、S 3 、 S 4 转 换 成 A 、B 、C 、 D 之
后得到右侧的表示该逻辑问题的C N F公式:
si, 〜 S2 A, 〜D
si, 〜 S3 A, 〜 B
si, 〜 S4 A, D
S2, ~S3 〜 D, 〜 B
S2, 〜 S4 ~Dj D
S3, -S4 D
SI, S2, S3, S4 〜A, D, B , 〜D
A, B, C, D = symbols("A:D") O
51 = -A
52 = D
53 = B
54 = -D
dnf = ((SI & ~S2 & ~S3 & ~S4) | O
(〜SI & S2 & ~S3 & ~S4) |
(~S1 & ~S2 & S3 & ~S4) |
(〜SI & ~S2 & ~S3 & S4)) s
cnf = to_cnf(dnf) ©
%sympy_latex cnf
A 八—
iB 八(
A V D) 八(
AV — iD)八(
iB) A (A V — >B)八(
DV — »D)八(
DV — —iB V —
»D)
使用 satisfmbleO可以对逻辑表达式进行推导,
下面的结果显示逻辑变量A 为真,
即¥ 是罪犯:
sat = CoSAT()
problem = [[1, -4], [1, -2], [1, 4], [-4, -2])
[-4, 4], [-2, 4], [-1, 4, 2, -4]]
677
Python 科学计算(第 2 版)
sat.add_clauses(problem) O
print sat.solve() O
[1, -1, -1, -1]
#define PICOSAT_UNKNOWN 0
#define PICOSAT_SATISFIABLE 10
#define PICOSAT_UNSATISFIABLE 20
picosat_add(sat, 1 );
picosat_add(sat, -4);
picosat_add(sat^ 0 );
或者:
调 用 picosat_sat()进 行 求 解 , 返 冋 PICOSAT _ S A T IS F IA B L E 表 示 求 解 成 功 。 调用
picosat_variables()得到逻辑变量的个数,picosat_deref()得到第 lit 个逻辑变量的解,它返丨Hi 1表示
该逻辑变量取值为Tm e ,-1表示取值为 False, 0 表示不确定。
下面在 Cython 中对 picosat.h 文件中定义的常量宏、结构体以及闲数进行声明。细于不需要
在 Cython 屮存取 PicoSAT 结构体的字段内容,因此无须对其各个字段进行声明。
PicoSAT * picosat一init ()
void picosat_reset (PicoSAT *)
int picosat_add (PicoSAT int lit)
int picosat—add—lits(PicoSAT *, int * lits)
int picosat一sat (PicoSAT int decision_limit)
int picosat—variables (PicoSAT *)
int picosat_deref (PicoSAT int lit) s
void picosat_assume (PicoSAT int lit)
679
Python 科学计算(第 2 版)
cdef close_sat(self):
if self.sat is not NULL:
picosat_reset(self.sat)
self.sat = NULL
在每次调用 pic〇Sat_sat()求解之前,
都需要将缓存中位于bulLpos之后的子句添加进 PicoSAT
结构体,这个丁作由 build_causes()完成。O 在 Cython 中可以通过 array 对 象 的 array.data.as_ints
屈性获得指向数组对象的数据缓存区,从而通过 C 语言的指针对数组中的数据进行快速操作。
© 而 当 buf_pos为 时 ,需要重新创建 PicoSAT 结构体,这个工作由 build_sat〇完成。
cd e f in t count = le n (s e lf.c la u s e s )
if c o u n t - 1 == s e l f . b u f j D o s :
re tu rn
s p = s e lf.c la u s e s .d a t a .a s _ in t s O
fo r i in ra n g e ( s e l f . b u fj d o s , count - 1 ):
if p [i] == 0 :
p i c o s a t _ a d d _ l i t s ( s e l f . s a t^ p + i+ 1 )
s e l f .buf__pos = c o u n t - 1
s e lf,c lo s e _ s a t ()
s e lf .s a t = p ic o s a t _ in it ()
if s e lf.s a t is N U LL:
r a i s e M e m o r y E r r o r ()
s e lf .b u f jd o s = 0
s e l f . b u ild _ c la u s e s ( )
接下来是往缓存中添加单个子句的add_clause()和添加多个子句的 add_clauses(),它们都楚
通过调用内部方法_add_clause〇来实现的。
c d e f _ a d d _ c la u s e (s e lf> c la u s e ) :
s e lf.c la u s e s .e x t e n d (c la u s e )
s e l f . c l a u s e s . a p p e n d (0 )
d e f add 一c l a u s e ( s e l f , c la u s e ):
s e lf._ a d d _ c la u s e (c la u s e )
d e f a d d _ c la u s e s (s e lf, c la u s e s ):
f o r c la u s e i n c la u s e s :
s e l f •一add 一c l a u s e ( c l a u s e )
680
get_ solution()返111丨保存当前解的列表,O 竹 先 调 用 picosat_ variables()获得布尔变盘的个数,
©然 后 调 州 picosat_deref()获得第 i 个变量的解。注意在 P ico S A T 中,布尔变量的序号从1 开始,
因此返回列表中下标为0 的元素表示的是序号为1 的布尔变量的解。
在 solve()中€)首先调用 build__sat〇方法更新结构体,€>然后调用 picosat_sat()求解,若返回值
为 P I C O S A T _S A T I S F I A B L E , 则返回 get_solution()得到的解。
def get一solution(self):
cdef list solution =[]
cdef int i, v
cdef int max—index
max一index = picosat_variables(self.sat) O
for i in range(max_index):
v = picosat_deref(self.sat, i+1 ) ©
solution.append(v)
return solution
def iter一solve(self):
cdef int res, i
cdef list solution
self.build一sat〇
while True:
res = picosat_sat(self.sat, -1 )
if res == PICOSAT SATISFIABLE:
681
Python 科学计算(第 2 版)
solution = self.get_solution()
yield solution
for i in nange(len(solution)):
picosat_add(self.sat, -solution[i] * (i+1 )) O
picosat_add(self.sat, 0 )
else:
break
self•iter一reset()
def iter一reset(self): O
self.bufjDos = - 1
11.5.2数独游戏
数独是一种数字填充游戏,玩家需要把从1到 9 的数字填进每一格,保证每行、每列和每
个宫(3X 3 的方块)都 有 1至 9 所有数字。如 阁 11-31所示,黑色数字为游戏设计者提供的部分数
字,它们使该谜题只有一个答案,灰色数字显示该游戏的解答。
图 11-31数独游戏示例
根据数独的约束条件:
1) 每个中.元只能填写一个数字,即 bools 中第2 轴上的每组布尔变量只有一个为真。
2) 每行中不能有重复的数字,第 1轴上的每组布尔变量只有一个为真。例如,bcx)ls[0,
:,2]
为第一行屮的每个数字为3 的布尔变量,由于每行中有且只有一个数字为3,因此 bools[0, :,2]
中只有一个为Tm e 。
3) 每列中不能有重复的数字:第 〇轴上的每纟J1布尔变量只有一个为真。
4) 每块中不能有重复的数字:这个条件稍微复來,耑要对 bools 中变量的位置进行一些调
整,稍后再做分析。
用一句话说明数独的约束条件就是:对布尔变量进行不同的分组,保证每个分组中只有一
个布尔变S 为真。下而我们看看如何用S A T 表示一组布尔变S 中只有一个为真这个条件。
S A T 采用合取范式,其中每条表达式中的布尔变量采用或运算连接,而表达式之间采用与
运算连接。我们需要使用这种逻辑表达式表示“一组布尔变S 中只有一个为真”。例如,对于
布尔变量 A 、B 、C , 下面两条表达式的与运箅能表示A 、B 、C 中只有一个为真:
1) A I B I C : 表 示 A 、B 、C 中至少有一个为真。
2) 〜 (A & B) & 〜 (A & C ) & 〜 (B & C ) : 任意两个布尔变量都不同时为真。
第二个条件可以重写为(〜A 卜B ) & ( 〜 A I 〜〇 & (〜B I 〜C ) , 对应三条 S A T 的表达式,因此最
终的或与表达式为:
(A | B | C) & (〜A | 〜B) & (〜A | -C) & 卜B | -C)
根据上而的表达式,我们需要从9 个元素中取两个元素的所有组合,这种组合运算可以使
用 itertookcombinations来实现。下而的 get_conditions〇返丨Hi使 得 b o d s 中最终轴上的所有布尔数
组只有一个为真的 S A T 表达式。
〇首 先 将 从 N 个 数 中 选 取 两 个 数 的 所 有 组 合 转 换 成 一 个 二 维 数 组 index , 其形状为
(N *(N - l )/2, 2)。© 创建第一个条件对应的逻辑表达式,即最终轴上每组逻辑变量的序号。 ©创
建第二个条件对应的逻辑表达式,即最终轴上每两个逻辑变量序号的负值。
def get一conditions(bools):
conditions =[]
n = bools.shape[-l]
index = np. array(list(combinations(range(n)^ 2 ))) O
# 以最后一轴为组
# 第一个条件:每组只能有一个为W.
conditions.extend(bools. reshape(-1 , n) .tolist〇) 0
# 第二个条件:每组中没有两个同时为真
conditions .extend ((-bools[ " ” index] .reshaped 2 )) .tolist〇) ©
return conditions
Python科学计算(第 2 版)
下 面 使 用 geUconditionsO计算数独的前三个约朿条件,由于它只针对最终轴进行运算,因
此 对 于 “每行的数字不能重复”和 “每列的数字不能重复”这两个条件,需要将条件对应的轴
调换为最终轴。
I 对 于 约 朿 条 件 4 ) , 需要将每块的布尔变量调换为最终轴,这 需 要 交 替 使 用 reshape)和
swapaxes()。最 后 将 c l 、c 2、c 3、c4 连接成一个列表 conditions,就得到了数独游戏所有约朿条
! 件 的 S A T 表达式。
实| __________________________________________________________________________________
例 ; tmp = np.swapaxes(bools.reshape(3, 3,3, 3, 9), 1 3 2).reshape(9, 9, 9)
; c4 = get_conditions(np.swapaxes(tmp, 1, 2)) # 每块的数字不能重复
conditions = cl + c2 + c3 + c4
def format_solution(solution):
solution = np.array(solution),reshape(9j % 9) O
return (np.where(solution == 1)[2] + 1).reshape(9, 9) ©
; sat = CoSAT()
sat•add—clauses(conditions)
solution = sat.solve()
fonmat_solution(solution)
: array([[9, 8 , 7, 6 , 5, 4, 3, 2, 1],
: [6 , 5, 4, 3, 1, 2, 9, 8 , 7],
| [3, 2, 1, 9, 8 , 7, 6 , 5, 4],
: [8 , 9, 6 ,l y 4, 5, 2, 1, 3],
: [7, 4, 5, 2, 3, 1, 8 , 9, 6 ],
: [2, 1, 3, 8 , 9, 6 , 7, 4, 5],
! [5, 7, 9, 4, 6 , 8, 1, 3, 2],
: [4, 6 , 8 , 1, 2, 3, 5, 7, 9],
: [1, 3, 2, 5, 7, 9, 4, 6 , 8 ]])
684
下面用 C o S A T 和 conditions解决一个实际的数独游戏。
游戏的初始状态由 sudoku_ str 指定,
其 中 0 表示需要填写数字的空內格。〇对于初始状态中的每个非0 数字,都创建一个与其:对应
的布尔变量为真的布尔表达式,得 到 conditions〕。© 该游戏的解同时满足conditions和 conditions2
条件。
sudoku_str = """
000000185
007030000
000021400
800000020
003905600
050000004
004860000
000040300
931000000"
我们注意到 conditions〕中的每条或表示式只包含一个布尔变量,对于这种简单的表达式,
可以使川 void picosat_assume(PicoSAT *, int lit)添加假设条件。其 中 lit 为布尔变量的序号,正数
表示假设该布尔变量为真,
负数表示假。
下面楚 C o S A T 类 的 assume_ solve〇方法,
它在调j|j solve〇
方法求解之前添加 assumes指定的所有假设条件。下面是使用 assume_solve()求解的程序:
, , , , , , , , ,
, , , , , , , , ,
sat = CoSAT()
sat.add_clauses(conditions)
solution = sat.assume_solve(bools[r, c, v] .tolist〇)
4
1
2
9
7
5
3
6
7
5
1
6
2
8
format_solution(solution)
7
5
8
1
6
3
2
9
1
4
2
3
7
5
2
1
3
5
9
4
8
6
2
8
4
7
6
1
3
8
3
1
5
7
6
2
9
4
6
8
5
1
2
3
9
/V
8
2
5
3
6r
9
实例
r L
T J
L
ay
r L
TJ
r L
TJ
r L
TJ
1 J
[
r L
TJ
TJ
[
r L
TJ
r L
T J
TJ
使 用 a s s m m s o M )的一个优点是在求解完成之后所有的假设条件将被清空,因此可以重复
使用同一个 sat 对象对多个数独游戏求解。
本书提供了一个交互式数独游戏求解器程序,可以使用如下键盘按键来操作:
•使用方向键改变当前单元格的位置。
• 使 用 数 字 键 1-9填写当前单元格,0 清除当前单元格,用户输入的数字采用黑色显示,
空白单元格的解采用浅灰色显示。
• 使 用 Ctrl+ E 淸除所有中.元格的内容。
11.5.3扫雷游戏
686
或没有地雷。
扫雷游戏也可以使用 S A T 进行求解。可以用一个逻辑变量表示每个方块是否有地雷,而
每个已经打开的方块中的数字则对其周围地雷的分布提供了约朿条件,可以使州 S A T 所耑的或
与逻辑表达式表示这些约束条件。与前面介绍的逻辑游戏不冋的萣,自动扫雷的n 标不萣找到
一组可能的解,而是要找到绝对不是雷的方块,从而进一步打开它们。
1.识别雷区中的数字
下而让我们先使用两幅扫雷游戏的界而截图找出所有数字及其坐标,然后讨论如何使用
S A T 找到所有可以打开的方块的坐标。
m inejnit.png 为妇雷初始状态时的界面截图,mineOl.png为打幵了一些方格之后的截图,
mine_number$.png为已打开的方格的所有可能图像。
下面还定义了•_•呰与捕捉的扫雷图像相关的数据:(XO,Y 0)为雷区左上角像素的來标, SIZE
为每个正方形方格的边长,C O L S 为雷区中方格的列数,R O W S 为行数。
O 通 过 np.s_ nj*以定义一个切片元纟II以方便提取雷区部分的图像。® mine_ numbers.p n g 是由
8 幅 12X 12的方格图像在垂直方向上合并而成的,因此其形状为(8*12,12,3)。
import cv2
img 一
init = cv2 .imread("mine_init.png")[mine_area]
img_mine = cv2 .imread("mine0 1 .png")[mine_area]
img_numbers = cv2 .imread("mine_numbers.png") ©
img 一
numbers•shape
(96, 12, 3)
下面找出 im g jn it 和 两 幅 图 像 中 像 素 值 不 同 的 点 ,得到一个形状为(H,SIZE ,W ,
SIZE ,3)的数组 mask 。该数组中的第1、第 3、第 4 轴对应原始图像中的某个方块,例 如 mask[2,
:,
6,:,
:]与第3 行 第 7 列的方块对应。对 m ask 中这三个轨1:.的数据求均值,就得到了 img_m ine 与
im g jn it 中每个方块的差异。当差异大于某个阈伽付,我们认为 img_m ine 中对应的方块被打升
了。阁 11-32显示了 block_m ask 与原始图像,可以看出 block_m ask 中的 True 与已打开方块是一
一对应的。
图 11-32计算已打开方块的位置
img 一
imne2 = np.swapaxes(img一imne.reshape(SHAPE)j 1, 2)
img 一
numbers.shape = 8 , - 1
numbers = np.argmin(distance.cdist(blocks, img_numbers), axis=l)
rows, cols = np.where(block_mask)
下而用本书提供的 draw_grid〇函数绘制识别出的数字:
2.用 S A T 扫雷
根据方块周围的相邻方块数,可以分为如下三类:
• 角 方 块 :位 于 4 个角的方块,它们只有3 个与之相邻的方块。
• 边 方 块 :位 于 4 边上的方块,它们有5 个相邻的方块。
• 一般方块:其他的方块均有8 个相邻的方块。
每个方块中的数字对其相邻方块中地雷的可能组合提供了约束条件,如果能用或勹表达式
表示所有这些约束条件,就可以用 S A T 对扫雷问题进行求解。下而以8 个相邻方块为例,介绍
如何用合取范式表示这8 个方块中有三个地雷。
我们用序号为1到 8 的逻辑变量表示某个方块周围的8 个方块中是否有雷。可以用下面的
逻辑表示8 个逻辑变量中有3 个为真:
• 任 意 取 4 个变量,这 4 个变量不可能同时为真。例如,如果选择 A 、B 、C 、D 这 4 个
变量,贝1j 〜(A & B & C & D )表示它们不全是真,展开之后得到〜A 卜B 卜C I 〜D 。
• 任 意 取 6 个变量,至少有一个为真。如果用 A 、B 、C 、D 、E 、F 表 示 这 6 个变量,则
A 丨
B IC 丨
D丨E丨
F 表示它们中至少有一个为真。
上而所有条件之间都用与运算连接,而每个子句都是用或运算表示,因此整个表达式为合
取范式。
下面的程序中用 c _ bimitiom()穷举所有4 个变量和6 个变量的组合,并添加相应的逻辑表
达 式 。可 以 看 到 解 中 包 括 3 个 1 , 表示有三个逻辑变量为真,即有三个地雷。如果调用
sat.iter_solve(),可穷举所有三个地雷的组合。
variables = range(l, 9)
from itertools import combinations
clauses =[]
for vs in combinations(variables, 4):
Python 科学计算(第 2 版)
for vs in combinations(variables, 6 ):
clauses.append(vs)
sat = CoSAT()
sat.add_clauses(clauses)
sat.solve()
[-1, -1, -1, -1, -1, 1, 1, 1]
在生成 用 于 S A T 求解的逻辑表达式之前,我们先为每个方格设置一个逻辑变量的编号。
编号从左上角的方格为1开始,右下角的方格的编号为CO LS * R O W S 。根据下面的算式可以
将编号转换为方格所在的行和列:
• 行号=(编号- l)//CO LS
• 列 号 =(编号-1) % COLS
二维数组 variables 中保存逻輯变量的编号,而 variable_ neighbors字典保存与每个方格编兮
相邻的方格的编号。
variable_neighbors = defaultdict(list)
下而查看勹编号为5 0 的方格相邻的方格的编号:
variable_neighbors[50]
[19, 20, 21, 49, 51, 79, 80, 81]
扫雷游戏与前面介绍的逻辑问题不同,它并不是要找到一组可能的解,而足要找到绝对足
雷以及绝对不是雷的方块。下面是 C o S A T 中实现这一功能的 get_failed_assumes〇方法。它对每
个 逻 辑 变 显 进 行 循 环 并 调 用 picosalassum eO , 假 设 该 逻 辑 变量为假或为真 ,并分别调用
picasat_sat〇汁算实施假设之后是否有解。如果无解,则表示该逻辑变M 不可能为假或真。使用
picosat_assume()的好处足每次调用picosat_sat()之后假设将自动被重置。
get_ failed_asksiimes()返回所有失败的假设,因此返回值为负数表示该逻辑变量必须为真,而
正数表示必须为假。
def get一afiled一assumes(self):
cdef int max_index
cdef int netl, ret0
cdef list assumes =[]
self.build一sat()
max_index = picosat_variables(self.sat)
for i in range(1 , max_index+l):
picosat_assume(self.sat, i)
retl = picosat_sat(self.sat, -1 )
picosat_assume(self.sat, -i)
ret0 = picosat_sat(self.sat, -1 )
if ret0 == PICOSAT_UNSATISFIABLE:
assumes.append(-i)
if retl == PICOSAT_UNSATISFIABLE:
assumes.append(i)
return assumes
最 后 对 所 有 已 打 开 的 方 格 计 算 逻 辑 表 达 式 , 并 添 加 进 S A T 求 解 器 ,然后利用
get_ failed_assumes()找到所有失败的假设 failed_assumes。对于其中未打开的方格,正数表示不是
sat = CoSAT()
for var'一id, num in zip(variables[rows, cols], numbers):
sat•add—clauses(get一clauses(var一id, num))
failed_assumes = sat.get_failed_assumes()
Python 科学计算(第 2 版)
for v in failed一
assumes:
av = abs(v)
col = (av - 1) % COLS
row = (av - 1) // COLS
if table [row, col] == u"
if v > 0 :
table [row, col] = u''^
else:
table [row, col] = u" 0
draw_gnid(table, fontsize=1 2 )
图 11-34使用SAT 求解器推断方格中足否有地茁
3.自动扫雷
本书提供了一个自动扫笛实例程序,它能够自动启动扫笛游戏,并自动操纵鼠細进行扫雷。
置为高级(99个雷),并 且 关 闭 “
显示动画”、“
播放声音”以 及 “
显示提示”等选项。
当程序无法确定可以打幵的方块时,将随机点开任意的方块。为了实现更高的成功率,读
者可以尝试在剩余非常少的雷时,穷举所有的可能解,并计兑每个方块是雷的概率,选择点击
概率最小的那个方块。
除了本节介绍的 cycosat 扩展库之外,程序中还使用了如下扩展库:
• scpy2.urils.autoit: 使 用 ctypes 标准库对 Autolt 提供的 D L L 进行包装,提供了自动扫雷所
潘的$ 本 W indow s 自动化功能。
692
win 32gui : 获取游戏界面所在的位置。
pillow : 读 取 P N G 图像文件以及捕捉屏幕中的游戏界面。
1 1 . 6 分形
与本节内容对应的Notebook 为: 1l -examples/examples-600-fi*actal.ipynb〇
自然界中的很多事物,例如树木、云彩、山脉、闪电、雪花以及海岸线等都呈现出传统几
何学难以描述的形状。这些形状都有如下特性:
•有着十分精细的不规则的结构。
♦ 整体勹局部相似,例如一根树杈的形状和一棵树很像。
S
分形几何学就是用来研究这样一类几何形状的科学,借助计算机的高速计算和图像显示,
我们可以更深入、更直观地研究分形几何。作为本书的最后一节,我们看看如何使用 Python 绘
制一些经典的分形图形。
11.6.1 Mandelbrot 集合
Mandelbrot(曼德布洛特)集合是在复平面上构成分形图案的点的集合。它可以W 下面的复二
次多项式定义:
fc (z ) = z 2 4- c
693
Python科学计算(第2 版)
1.纯 P yth o n 实现
下面是完整的绘制 Mandelbrot集合的程序,它所绘制的图案如图11-35所示。
def iter_point(c): O
z = c
for i in xrange(l, 10 0 ): # 最多迭代 1 0 0 次
if abs(z) > 2 : break # 半径大于2 则认为逃逸
z = z * z + c
return i # 返回迭代次数
pi•gca()•set_axis一
off()
x, y = 0.27322626, 0.595153338
pl.figure(figsize=(9, 6 ))
pi.subplot(231)
draw_mandelbrot(-0.5, 0, 1.5)
for i in range(2,7):
pi.subplot(230+i)
draw_mandelbrot(x, y) 0 .2 **(i-l))
pl.subplots__adjust(0 , 0 , 1 , 1 , 0 .0 , 0 )
阁 11-35 Mandelbrot集合,以 5 倍的倍率放大点(0.273,0.595)附近 i
2.用 Cython 提速 |
使 用 Python 绘 制 Mandelbrot集合,最大的问题就是运算速度太慢: 丨
而 hid于 iter_point〇函数中存在迭代,
无法将其转换成N um Py 的数组运算。
下面我们用 Cython :
重新编写 iter_point()。 丨
首先为 Python 的 iter_point〇中的各个变量--- c 、z 、i 添加类型声明,然后调用%timeit查看 |
运行速度,发现速度提高得不是很明显。丨
lj % % cyth 〇
n -a 查看编译之后的 C 语言源代码,你会 |
发 现 abs⑵调用 Python 的 abs〇涵数,该函数限制了运行速度。 |
%%cython 丨
def iterjD〇 int(complex c):
cdef complex z = c
cdef int i :
for i in range(l, 1 0 0 ):
if z.real*z.real + z.imag*z.imag >4: break O
i 695
Python 科学计算(第 2 版)
z = z * z + c
return i
〇将计算S 数绝对值的代码修改为实数部分的平方与虚数部分的平方之和后,运行速度提
高了近4 0 倍:
3.连续的逃逸时间
%%cython
from libc.math cimport log2
def iterj3 〇
int(complex c):
cdef complex z = c
cdef int i
cdef double rl, mu
for i in range(1 , 20 ):
r2 = z.real*z.real + z.imag*z.imag
if r2 > 10 0 : break
z = z * z + c
if r2 > 4.0:
mu = i - log2(0.5 * log2 (r2 ))
else:
mu = i
return mu
如果逃逸半径设置得很小,例 如 2.0,那么有可能结果不够〒滑,这时可以在迭代循环之
后添加几次迭代,这能保证 z 的模值足够大。例如:
Z = Z * Z + C
Z = Z * Z + C
i += 2
图 11-36是逃逸半径为10、最大迭代次数为2 0 的结果。
pl.figure(figsize=(8 , 8 ))
draw_mandelbrot(-0.5, 0, 1.5, n=600)
c
阁 11-36平滑处理后的Mandelbrot集合:逃逸半径=10, M 大迭代次数=20
4. M andelbrot 演示程序
为了实时计兑 Mandelbrot集合的图像,我们需要更快的运兑速度,可以将所有的循环都使
用 Cython 编写。本书提供了用 matplotlib制作的实时绘制 Mandelbrot集合的演示程序,界面截
图如图11-37所示。
阁 11-37实时绘制Mandelbrot犯合的演示程序
697
Python 科学计算(第 2 版)
下 而 是 计 算 M andelbrot 集 合 图 像 的 C y th o n 函 数 。该 函 数 所 在 模 块 的 完 整 路 径 为
scpy2.examples.fractal.fastfractal。参 数 c x 和 c y 是复平而上的计算范刚的中心点,参 数 d 是中心点
到计算边界的实轴上的长度,参 数 out 是保存计算结果的二维数组。如果不指定 out, 则需通过
h 和 w 参数指定返回数组的大小。参 数 n 为最大迭代次数,R 为逃逸半径。
S cdef complex z = c
cdef int i
cdef double r2, mu
cdef double R2 = R*R
for i in range(1 , n):
r2 = z.real*z.real + z.imag*z.imag
if r2 > R2: break
z = z * z + c
if r2 > 4.0:
mu = i - log2(0.5 * log2 (r2 ))
else:
mu = i
return mu
def mandelbrot(double cx, double cy, double d, int h=0 , int w=0 ,
double[:, ::1] out=None, int n=20, double R=10.0):
cdef double x0 , xl, y0 , yl, dx, dy
cdef double[:, **1 ] r
cdef int i, j
cdef complex z
x0 , xl, yQ, yl = cx - d, cx + d, cy - d, cy + d
if out is not None:
n = out
else:
r = np.zeros((h, w))
698
hj w = r.shape[0 ], n.shape[l]
dx = (xl - x0 ) / (w - 1 )
dy = (yl - y0 ) / (h - 1 )
for i in range(h):
for j in range(w):
z.imag = y0 + i * dy
z.real = x0 + j * dx
r[i, j] = iter_point(z., n, R)
return r.base
11.6.2迭代函数系统
迭代函数系统是一种创逑分形图案的简单算法,它所创建的分形图永远是绝对自相似的。
下面以绘制某种蕨类植物叶子的图案为例,介绍迭代函数系统兑法以及如何用 Python 实现。
有下面4 组线性函数用于对二维平面上的坐标进行线性变换:
x(n+l)= 0
所谓迭代函数系统,是指将函数的输出再次当作输入进行迭代计算,因此上面的公式都是
通过坐标 x (n),y (n)计算变换后的坐标x (n+ l ),y (n+ l )。问题是有4 个迭代函数,迭代时选择哪个
函数进行计算呢?我们为每个函数指定一个概率值,它们依次为1 % 、7 % 、7 % 和 8 5 % 。通过每
个闲数的概率随机选杼一个函数进行迭代。在上面的例子中,第 4 个闲数被选杼进行迭代的概
率最高。
最后我们从坐标原点(0,0)开始迭代,绘制每次迭代后得到的坐标点,就得到了迭代函数系
统的分形图案。下面的程序演示了这一计兑过程:
eq2 = np.array([[0.2,-0.26,0],[0.23,0.22,1.6]])
p2 = 0.07
进行函数迭代
p : 每个函数的选抒概率列表
eq : 迭代函数列表
init: 迭代初始点
s n : 迭代次数
# 迭代向量的初始化
pos = np.ones(3, dtype=np.float) O
pos[:2 ] = init
# 通过函数概率,计算函数的选择序列
p = np.cumsum(p)
rands = np.random.rand(n)
select = np.searchsorted(p, rands) ©
# 结果的初始化
result = np,zeros((n,2 ), dtype=np.float)
c = np.zeros(n, dtype=np.float)
for i in xrange(n):
eqidx = select[i] # 所选的函数下标
tmp = np.dot(eq[eqidx], pos) # 进行迭代
pos[:2 ] = tmp # 更新迭代向量
# 保存结果
result[i] = tmp
700
c [i ] = eqidx
x,y ,c = ifs([pl,
p2,
p3,
p4],[eql,
eq2,
eq3,
eq4],[0,
0], 100000)
figj axes = pi.subplots(1, 2} figsize=(6 , 5))
axes[0 ].scatter(x, y, s=l, c="g", marker="s"> linewidths=0 ) ©
axes[l].scatter(x, y, s=l, c=c, marker="s", linewidths=0 ) O
for ax in axes:
ax.set_aspect("equal")
ax.set_ylim(0,10.5)
ax.axis("off")
pi.subplots_adjust(left=0 >right=l^ bottom=0 ,top=l,wspace=0 Jhspace=0 )
将乘法向量扩充为三维:这样每次和迭代函数系数进行矩阵乘积运算的向量就变成了 x (n),
y (n),1.0〇
© 为了减少计算时间,不在迭代循环中计总随机数选择迭代方程,而是事先通过每个函数
的概率,计算出函数选择数组 select。注意这里使用 cumsumO先将概率累加,然后产生-•组0
到 1之间的随机数,通过判断随机数所在的概率区间选择+同的方程下标。
€)最后调用 scatterO将得到的坐标绘制成散列图,其中每个关键字参数的含义如下:
• s : 每个散列点的大小,因为要绘制10万个点,为了提高绘图速度,我们选择点的大小
为 1个像素。
• c : 点的颜色,这里选择绿色。
• marker: 点的形状,"sn表示正方形,方形的绘制是最快的。
• linewidths:点的边框宽度,0 表示没有边框。
O 此外,参 数 c 还可以传入一个数组,作为每个点的颜色值。我们将迭代用的函数下标传
入,这样可以直观地看出哪个点是哪个函数迭代产生的。
图 11-38是程序的输出,观察右图的4 利■色的部分可以发现:概 率 为 1 % 的函数1所计算
算的是整片叶子的迭代,即最下面的三种颜色的点通过此闲数的迭代产生上面所有的深红色的
点。可以看出整片叶子呈现出完美的自相似特性,任意取其中的一片子叶,将其旋转放大之后
都和整片叶子相同。
Python科学计算(第 2 版)
图 11-38函数迭代系统所绘制的蕨类植物的叶,
•
1.2D 仿射变换
I
上面所介绍的4 个变换方程的一般形式如下:
从红色三角形的每个顶点变换到绿色三角形的对应顶点,正好能够决定仿射变换中的6 个
702
参数。这样我们可使用 N + 1 个三角形,决 定 N 个仿射变换,其中的每个变换的参数都是由第0
个三角形和其他的三角形决定的。第 0 个三角形被称为■础三角形,其余的三角形被称为变换
三角形。
为了绘制迭代闲数系统的图像,还需要给每个仿射变换方程指定迭代概率。此参数也可以
使用三角形直观地表达出来:迭代概率和变换三角形的而积成正比,即迭代概率为变换三角形
的而积除以所有变换三角形的而积之和。
如 图 11, 所示,前而介绍的蕨类植物的分形阁案的迭代方程5 个三角形决定,可以很
直观地看出紫色的小三角形决定了叶子的暮而两个蓝色的三角形决定了左右两片子叶;绿色
的三角形将茎和两片子叶往上复制,形成整片叶子。
^ Ch * ^ 1 P
图 11*40 5 个三角形的仿射方程绘制蕨类植物的叶子
2.迭代函数系统设计器
按照上节所介绍的三角形法,我们可以编写一个迭代函数系统的设计工具。用户通过程序
界面绘制或修改一组三角形,程序计算这组三角形所对应的迭代方程组的系数,并实时地绘制
迭代图案。图 1140是本书提供的设计迭代函数分形系统的程序界面截图。
scpy2.examples.fractal.ifs_demo : 迭代函数分形系统的演示程序,通过修改左侧三角形
的顶点实时地计算坐标变换矩阵,并在右侧显示迭代结果。
下而简要地介绍该演示程序中用到的一些函数和类。首先通过两个三角形求解仿射方程的
系数,相当于求六元线性方程组的解,这个计算通过 solV e _ eq 〇完成,它先计算出线性方程组的
矩 阵 a 和 b ,然;C 调 用 N um Py 的 linalg.solve〇对线性.方程组 a • x = b 求解:
Python科学计算(第2 版)
解方程,从trianglel变换到triangle2 的变换系数
trianglel、triangle2 是二维数组:
x0 ,y0
xl,yl
x2 ,y2
it it ii
x0 , y0 = trianglel[0 ]
xl, yl = trianglel[l]
\2, y2 = trianglel[2 ]
a = np.zeros((6 , 6 ), dtype=np.float)
b = triangle2 .reshape(-l)
a[0, 0:3] = x0, y0, 1
a[l, 3:6] = x0, y0, 1
x = np.linalg.solve(a, b)
x.shape = {2, 3)
return x
每个仿射方程的迭代概率与对应三角形的面积成正比,三角形的面积通过 triangle_area()i-f
算,它使用 N um Py 的 crossO计算三角形的两个边的矢量的叉积:
def triangle_area(triangle):
ii ii ii
计算三角形的面积
ii ii ii
A, B, C = triangle
AB = A - B
AC = A - C
return np.abs(np.cross(AB> AC)) / 2.0
704
后两个三角形为变换三角形。© 调 用 triangle_area()计I?:每个变换三角形的面积,并用面积和归
一化得到每个三角形的迭代概率p 。© 调 用 s d v e _ eq 〇得到从基础三角形到变换三角形的仿射变
换矩阵,并将所有反射变换矩阵按照第0 轴连接成一个形状为(4, 3 ) 的数组 e q s 。 数组中的每两
行表示一个迭代方程的系数。
〇创 建 IFS 〇对象,其前两个参数分别为三角形的迭代概率和迭代系数,第 3 个参数为每次
调 用 update()方法的迭代次数,size 参数为二维直方图统计结果数组的长轴的长度。©每次调用
updateO方法都迭代指定的次数,并返回更新后的直方图统计结果。
counts 是一个形状为(600,477)
的整数数组。© 为了更清晰地显示统计结果,这里使用对数正规化对象LogNorm 对 counts 中的
值进行正规化。由于0 的对数值为负无穷,因此 counts 中保存的值实际上是直方图统计值加1。
程序的输出如图11~41所示。
triangles = n p . a r r a y ([
[-1.945392491467576,
[6.109215017064848,
O
-5.331010452961673],
-0.8710801393728236],
s
[-1.1945392491467572, 5.400696864111497]^
[-2.5597269624573373, -4.21602787456446],
[5•426621160409557, -2•125435540069687],
[0.5119453924914676, 4.912891986062718],
[3.5836177474402735, 8.397212543554005],
[4.0614334470989775, 5.121951219512194],
[8.56655290102389, 4.7038327526132395]])
base_triangle = triangles[:3]
trianglel = t r i a n g l e s [3:6]
triangle2 = t r i a n g l e s [ 6 : ]
areal = triangle_area(trianglel) ©
area2 = triangle_area(triangle2)
counts = i f s . u p d a te () ©
705
Python 科学计算(第 2 版)
ax.axis("off")
11.6.3 L-System 分形
F+F--F+F
F+F--F+F+F+F--F+F--F+F--F+F+F+F--F+F
如此替换迭代下去,并根据字串进行绘图(符号+和-分別正负旋转60度),可得到如图1142
右下方的分形图案:
706
图 11>42使用 F+F-F+F迭代的分形图案
除了 F 、+ 、_之外我们再定义如下几个符号:
• f : 与 F 的含义相同,向前走固定单位,为了定义不同的迭代公式
• [:将当前的位置入堆栈
• ] : 从堆栈中读取坐标,修改当前位置
• S : 初始迭代符号
所有的符号(包括上面未定义的)都可以用来定义迭代,通过引入两个方拈号符号,可以描
I
述分岔的图案。例如下而的符号迭代能够绘制出一棵植物:
S -> X
X -> F-[[X]+X]+F[+FX]-X
F -> FF
下面用一个字典定义所有的迭代公式和其他的一些绘图信息:
rules = [
{
"F":"F+F--F+F", "S":"F",
"direct":180,
"angle":60,
"iter":5,
"title":"Koch"
h
{
■_X":"X+YF+", "Y":"-FX-Y", "S":"FX",
"direct":0,
"angle":90,
"iter":13,
"title":"Dragon"
}>
{
"direct":0,
707
Python 科学计算(第 2 版)
.•angle": 60,
"iter":7,
"title":"Triangle"
"title":"Plant"
"title":"Hilbert"
{
"S":"L— F--L--F", "L":"+R-F-R+", "R":"-L+F+L-",
"direct":0,
"angle":45,
"iter":10,
},
其中:
• direct: 绘阁的初始角度,通过指定不同的值可以旋转整个阁案
• angle: 定义符号+和-旋转时的角度,不同的值能产生完全不同的图案
• iter: 迭代次数
下面的程序将上述字典转换为需要绘制的线段坐标:
class L一S y s t e m ( o bj e c t ) :
for c in info:
if c in rule:
ninfo.append(rule[c])
else:
ninfo.append(c)
info = "".join(ninfo)
self.rule = rule
self.info = info
def get_lines(self):
from math import sin, cos, pi
d = self.rule['direct']
a = self.rule['angle']
p = (0 .0 ^ 0 .0 )
1 = 1.0
lines =[]
stack =[]
for c in self.info:
if c in "Ff":
r = d * pi / 180
t = p[0 ] + l*cos(r), p[l] + l*sin(r)
lines.append(((p[0 ], p[l]), (t[0 ], t[l])))
s
elif c == "+":
d += a
elif c
d -= a
elif c == __[_•:
stack.append((p,d))
elif c =="]":
p, d = stack[-l]
del stack[-l]
return lines
卜面的dmw()完成迭代计算和绘图工作:
709
Python科学计算(第 2 版)
for i in xrange(6 ):
ax = fig.add一 subplot(231+i)
draw(ax, rules[i])
图 1 1 4 3 儿种 L - S y s t c m 的迭代图案
1 1 . 6 . 4 分形山脉
前而介绍的分形图案都是严格按照指定的规则迭代生成的,然而向然界中的山川、云彩、
树木等都不是精确的自相似图形,而是在统计怠义上的A 相似阁形。木节将介绍儿种经典的山
脉地形的生成算法,以及如何用Python 快速实现这些算法。
1. 一维中点移位法
710
每次迭代时,随机移位的最大幅度都成比例地衰减。迭代足够多次之后,将所得到的点连
接起来,就得到了一条随机的分形曲线。
下面是实现此算法的源程序,程序所绘制的山脉曲线如图11^44所示。
a = np.zeros(2 **n+l) O
scale = 1 . 0
for i in xrange(n, 0 , -1 ): ©
s = 2 **(i-l) ©
s2 = 2 *s
tmp = a[::s2 ]
a[s::s2 ] += (tmp[:-l] + tmp[l:]) * 0.5 O
a[s::s2 ] += np.random.normal(size=len(tmp)-1 , scale=scale) 0
scale *= d ©
return a
pl.figure(figsize=(8,4))
for i., d in enumerate([0.4, 0.5, 0.6]):
np.random.seed(8 ) O
a = hillld(9, d)
pi.plot(a, label="d=%s" % d, linewidth=3-i)
pl.xlim(0 j len(a))
pi.legend()
图丨144 一维分形山脉曲线,袞减值越小,最大幅度的袞减越快,曲线越平滑
711
Python 科学计算(第 2 版)
X 轴坐标必须是整数。显然当数组长度为2n+ l 时满足这个要求。
©由 于 数 组 a 的长度为2"+1, 因此需要循环 n 次才能够计算到数纽上所W 的点。每次循环
时,都要对数组中的某些点计算中值。例如对 P n = 8 , 即数组长度为257时,循环变量 i 和数组
中需要计算中点的下标如表11-2所示:
表 1 1 -2 循环变量 i 与对应的数组的下标
i 需要计算中点的数组下标
8 128
7 64、 192
6 32、%、 160、 224
5 16、48、80、112、 144、176、208、240
2.二维中点移位法
屮点移位法很容易扩展到二维,可以用它计算山脉丨11|面,算法如图11~45所示。左图中,
从白色圆点的值(值为0、2、4 和 8 的 4 个点)计算5 个用灰色圆点表示的中值点。边 上 4 个中值
点的计算和一维的情况相同,而正中间的中值则是4 个角上的值的平均值。
右图以计总5 X 5 的方格为例,演示了每少迭代时所计兑的点。方格中的数字表示计算此
点的值的迭代次数。初期情况下4 个角上的点已知,标记它们的迭代次数为0。根据左图的中
值计算方式,计算出标记为1 的 5 个方格的值。然后对于由迭代0 和 迭 代 1 的点组成的4 个方
块,再次进行中值计算,计算出所有标记为2 的 16个方格的值。
阁 1145 二维屮点移位法示意阁
完整的计算程序如下,程序的显示效果如图11~46所示。
绘 制 山 脉 曲 面 , 曲 面 是 一 个 (2**n + l)*(2**n + 1) 的 图 像 , d 为 衰 减 系 数
II M II
return a
np.random.seed(42)
a = hill2d(8, 0.5)
a/= np.ptp(a) / (0.5*2**8) O
a = convolve(a, np.ones((3,3))/9) ©
mlab.options.offscreen = True
scene = mlab.figure(size=(800, 600))
scene.scene.background = 1, lj 1
mlab.surf(a)
img = vtk_scene_to_array(scene.scene)
%array_image img
713
Python科学计算(第2 版)
图 11*46二维中点移位法计算山脉曲面
hiI12d〇程序的算法和一维的情况类似,这里就不多解释了。O 在计算出表示山脉曲面的二
维数组 a 之后,调 用 np.ptpO得到数组 a 中的最大值和最小值之间的差,并将其值放大到数组形
状 的 0.5倍 ,以便调)ij mlab.surf()绘制曲面。© 使 用 S c iP y 的多维数姐卷积函数convolve()X寸二维
数 纽 a 进行平滑处理。
3.菱形方形算法
I
每次迭代都是通过正方形四个角上的点的值,计算其边上4 点和中心点的值。这种计算方
法有很多种,上节介绍的是最简单的一种方法。但是如果读者放大它所生成的曲面,就会发现
它上而有一些大人小小的正方形的痕迹。菱形方形算法(Diamond-square algorithm)通过两种中值
计算方法的交替使用,能够有效地消除这种正方形痕迹,算法如阁1M 7 所示。
图 11*47菱形方形算法
首先如左图所示,通过正方形的四个角点计算位于其中心的点的平均值。然后如中图所示,
通过菱形的四个角点计算位于其中心的点的平均值。图中没有一个完整的菱形,而楚4 个半边
菱形。也可以把正方形平均看作左上、右上、左下和右下四个方向上的点的平均值,而菱形平
均看作上下左右四个方向上的点的平均值。右图显示了采)lj 菱形正方形计算5 X 5 的数组时的
运算顺序。从标记为0 的方格开始,以方形平均计算标记为I s 的方格值,然后以菱形平均计算
标•记为I d 的 4 个方格的值。接下来重复上面的步骤,方形平均计算2s 方格,最后菱形平均计
算 2d 方格。
使用菱形方形算法绘制山脉曲而的程序如下。观察图11~47(右)中的标记为2d 方格,可以
发现菱形平均所对应的方格无法用一个数组表示,因此程序中将它们分为水平和垂直方向上的
两个数组分别计算。程序中大量使用数组切片,从而提高程序的运算速度。如果读者觉得较难
714
现解,可以如下修改程序:
(1)注释禅随机数部分,并在迭代之前为数红L的 4 个角上的元素赋值为不为零的数值。
⑵使州较小的数组,并且输出每次赋值之后的数纟U a 的值。通过观察数纟11 a 的变化可以帮
助理解程序和菱形方形算法。
for i in xrange(n, 0 , -1 ):
s = 2 **(i-l)
s2 = 2 *s
# 方形平均
t = a[::s2 ,::s2 ]
t2 = (t[:-l,:-l] + t[l:,l:] + t[l:,:-l] + t[:-l,l:])/4
tmp = a[s::s2 ,s::s2 ]
tmp[… ] = t2 + normal(0 , scale, tmp.shape)
# 菱形平均分两步,分别计箅水平和®直方向上的点
t = a[::s2 ,s::s2 ]
= buf[:,:-l] + buf[:,l:]
t[:-l] += tmp
t[l:] += tmp
/= 3 # 边上是3 个值的平均
/= 4 # 中间的是4 个值的平均
t [… ] += np.random.normal(0 , scale, t.shape)
t = a[s::s2 ,::s2 ]
t[".] = buf[:-l,:] + buf[l:,:]
t [ : - l ] += tmp
t[:,l:] += tmp
t[:,[0,-l]] /= 3
t[:,l:-l] /= 4
t [… ] += np.random.normal(0 , scale, t.shape)
scale *= d
return a
Python 科学计算(第 2 版)
np.random.seed(42)
a = hill2d一ds(8, 0.5)
a/= np.ptp(a) / (0.5*2**8)
a = convolve(a, np.ones((3,3))/9)
mlab.options.offscreen = True
scene = mlab.figure(size=(800, 600))
scene.scene.background = 1, 1, 1
mlab.surf(a)
img = vtk_scene_to_array(scene.scene)
%array_image img
图 I M S 使用菱形方形算法计算山脉曲面
716
请尽量支持正版
如果资金充裕,
请购买纸质版。