python科学计算第二版(可编辑)

Download as pdf or txt
Download as pdf or txt
You are on page 1of 723

张若愚

★ NumPy—快速处理数据 ★ OpenCV — 图像处理和计算机视觉


★ SciPy —数值计算库 ★ Cython _ 编译 Python 程序
★ matplotlib — 绘制精美的图表 ★使用泊松混合合成图像
★ Pandas—方便的数据分析库 ★经典力学模拟
★ SymPy — 符号运算好帮手 ★频域信号处理

★ Traits & T raitsU I—轻松制作图形界面 ★布尔可满足性问题求解器


★ TVTK 与 Mayavi—数据的三维可视化 ★分形

清华大学出版社
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 版)

3.3 线 性 代 数 -lin a lg ............ 128 4.1.1 使用 pyplot模块绘图.... 207


3.3.1解线性方程组..........129 4.1.2 |ftl向对象方式绘图.......210
3.3.2最小二乘解.............130 4.1.3配置屈性.............. 211
3.3.3特征值和特征向量....... 132 4.1.4绘制多子阁.............212
3.3.4奇异值分解-SVD ....... 134 4.1.5配置文件.............. 215
3.4 统 计 -sta ts .................136 4.1.6在图表中显示中文.......217
3.4.1连续概率分布..........136
4.2 Artist 对 象 ................. 220
3.4.2离散概率分布........... 139
4.2.1 Artist 的属性............221
3.4.3核密度估计.............140
4.2.2 Figure 容器............. 223
3.4.4二项分布、泊松分布、
4.2.3 Axes 容器.............. 224
伽玛分布............. 142
4.2.4 Axis 容器.............. 226
3.4.5学生 t-分布与t 检验...... 147
4.2.5 Artist对象的关系........230
3.4.6卡方分布和卡方检验..... 151
4 . 3 坐 标 变 换 和 注 释 ............ 231
3.5 数 值 积 分 -integrate ......... 154
4.3.1 4 种來标系.............234
3.5.1球的体积............... 154
4.3.2來标变换的流水线.......236
3.5.2解常微分方程组........ 156
4.3.3制作阴影效果........... 240
3.5.3 ode 类................. 157
4.3.4添加注释.............. 241
3.5.4 信号处理-signal......... 164
4 . 4 块 、路 径 和 集 合 ............ 243
3.5.5中值滤波............. 164
4.4.1 Path 与 Patch............ 243
3.5.6滤波器设计.............165
3.5.7连续时间线性系统....... 167 4.4.2 集合.................. 245

3.6 插 值 -interpolate ............172 4 . 5 绘 图 函 数 简 介 .............. 255

3.6.1 —维插值............... 172 4.5.1对数坐标图........... 255


3.6.2多维插值............... 177 4.5.2极坐标图............. 256
3.7 稀 疏 矩 阵 -sparse ........... 181 4.5.3柱状图...............257
3.7.1稀疏矩陈的存储形式..... 182 4.5.4散列阁...............258
3.7.2最短路径............. 183 4.5.5 _ ................ 259
3.8 图像处理 -ndim age ......... 186 4.5.6等值线I冬1............. 261
3.8.1形态学图像处理......... 187 4.5.7四边形网格........... 264
3.8.2图像分割............... 192 4.5.8三角网格............. 267
3 . 9 空 间 奠 法 库 -spatial........ 195 4.5.9箭头阁...............269
3.9.1计算M 近旁点........... 195 4.5.10 三维绘[§).............. 273
3.9.2 ini包.................. 199 4.6 matplotlib 技 巧 集 .......... 274
3.9.3沃罗诺伊图............ 201 4.6.1使用 agg 后台在图像上
3.9.4徳劳内三角化......... 204 绘图 ................ 274
4.6.2响应鼠标与键盘W 牛.... 277
第 4 章 matplotlib-绘制精美的图表••…207
4.6.3 动画 ................ 285
4.1 快 速 绘 图 ................... 207

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

7.2.5动态添加Trait屈性...... 412 8.5.2 Mayavi 的流水线........ 498


7.3 TraitsUI 入门.............. 413 8.5.3二维图像的可视化.......501
7.3.1默认界而...............414 8.5.4 网格而 mesh............505
7.3.2用 View 定义界而.......415 8.5.5修改和创建流水线.......508
7 . 4 用 Handler 控制界面和模型…•425 8.5.6 标量场................ 511
7.4.1 用 Handler 处理事件..... 426 8.5.7 矢量场................ 513
7.4.2 Controller 和 UHnfo 对象•.…429 8.6 将 T V T K 和 Mayavi 嵌入
7.4.3响应 Trait屈性的?JPf牛.... 431 界而........................ 515
7 . 5 属性编辑器 .................432 8.6.1 TV TK 场景的嵌入.......516
7.5.1编辑器淡示程序.........433 8.6.2 Mayavi场景的嵌入...... 518
7.5.2对象编辑器............ 436
第 9 章 OpenCV -图像处理和计算机
7 . 5.3 自 定 义 编 器 .......... 440
视 觉 ..................... 523
7 . 6 函数曲线绘制工具.......... 444
9 . 1 图像的输入输出............ 523
第 8 章 T V T K 与 Mayavi-数据的三维 9.1.1读入并显示阁像.........523
可视化................... 451 9.1.2阁像类型.............. 524
8.1 V TK 的流水线(Pipeline)...... 452 9.1.3图像输出.............. 525
8.1.1显示岡锥...............452 9.1.4字节序列与图像的
8.1.2用 ivtk 观察流水线....... 455 相互转换.............. 526
8 . 2 数据集..................... 461 9.1.5视频输出.............. 527
8.2.1 ImageData..............461 9.1.6视频输入.............. 529
8.2.2 RectilinearGrid.......... 466 9.2 图像处理 ...................530
8.2.3 StructuredGrid.......... 467 9.2.1 二维卷积.............. 530
8.2.4 PolyData............... 470 9.2.2形态学运算.............532
8.3 TV TK 的改进 .............. 473 9.2.3 填充-floodFill........... 534
8.3.1 T V T K 的基木用法...... 474 9.2.4 去瑕f t -inpaint.......... 536
8.3.2 Trait 屈性.............. 475 9.3 图像变换 ...................537
8.3.3 序歹ij化................ 476 9.3.1儿何变换.............. 537
8.3.4集合迭代...............476 9.3.2 重映射-remap........... 540
8.3.5数组操作...............477 9.3.3直方图................ 543
8.4 T V T K 可视化实例 .......... 478 9.3.4二维离散傅立叶变换..... 547
8.4.1 切而..................479 9.3.5 )丨]双|i|视觉丨钋像计算深度
8.4.2等值面................ 484 信息.................. 550
8.4.3 流线.................. 487 9 . 4 图像识別 ...................553
8.4.4计算圆柱的相贯线....... 491 9.4.1川植夫变换检测直线和阏……553
8 . 5 用 m lab 快速绘图 ........... 496 9.4.2图像分割.............. 558
8.5.1点和线................ 497 9.4.3 SURF 特征匹配......... 561

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 简介

Python 是一利1解释型、而向对象、动态的洽j 级程序设计语言。A 从 20世 纪 90年代初 Python


语言诞生至今,它逐渐被广泛应用于处理系统管理任务和开发W e b 系统。丨 前 Python 已经成
为最受欢迎的程序设计语言之一。
由 于 Python 语言的简洁、易读以及可扩展性,在国外用 Python 做科学计算的研究机构日
益增多 , 一 •些知名大学已经采用Python 教授程序设计课程。众多开源的科学计兑软件包都提供
了 Python 的调用接口,例如计兑机视觉库 OpenCV 、三维可视化库 V T K 、复杂网络分析库 igraph
等。而 Python 专用的科学计算扩展库就更多了,
例如三个十分经典的科学计算扩展库:NumPy 、
SciP y 和 matplotlib,
它们分别为 Python 提供了快速数纽处理、
数值运兑以及绘图功能。
因此 Python
语言及其众多的扩展庳所构成的开发环境十分适合工程技术、科研人员处理实验数据、制作图
表,甚至开发科学计算应用程序。近年随着数据分析扩展厍Pandas、机器学习扩展庳 sdkit-leam
以及 IPython Notebook 交互环境的日益成熟,Python 也逐渐成为数据分析领域的首选工A 。
说起科学计算,首先会被提到的可能是M A T L A B 。然而除了 M A T L A B 的一些专业性很强
的工具箱H 前还无法替代之外,M A T L A B 的大部分常用功能都可以在Python 世界中找到相应
的扩展库。和 M A T L A B 相比,用 Python 做科学计算有如下优点:
• 首 先 ,M A T L A B 是一款商用软件,并且价格不菲。而 Python 完全免费,众多开源的科
学计兑库都提供了 Python 的调用接口。用户可以在任何计算机上免费安装Python 及其
绝大多数扩展库。
• 其 次 ,与 M A T L A B 相比,Python 是一门更易学、更严谨的程序设计语言。它能让用户
编写出更易读、更易维护的代码。
• 最 后 ,M A T L A B 主要专注于工程和科学计算。然而即使在计兑领域,也经常会遇到文
件管理、界面设计、网络通信等各种需求。而 Python 有着丰富的扩展库,可以轻易完
成各种高级任务,开发者可以用 Python 实现完整应用程序所需的各种功能。

1.1.1 Python2 还是 Python3

自从2008年发布以来,Pyth〇
n3 经历了 5 个小版本的更迭,无论是语法还是标准库都发展
得十分成熟。许多重要的扩展库也已经逐渐同时支持Pyth〇
n2 和 Pylhon3。但是由于 Python3 不向
Python 科学计算(第 2 版)

下兼答,目前大多数开发者仍然在生产环境中使叫Python 2.7。在 PyCon 2014大会上, Python


之父宣布 Python 2.7的官方支持延长至2020年。因此本书仍然使用Python 2.7作为幵发环境。
在本书涉及的扩展库中,IPython、NumPy 、SciPy 、matplotlib、Pandas、Sym Py 、Cython 、
5卩乂〇61*和0{^11(1!\^等都已经支持1^11〇113,而丁〇1丨15、丁^此1)1、丁\^1^、^13}^丨等扩展挥则尚未
着 手 Pyth〇
n 3 的移植。虽然一些新兴的三维可视化扩展庳正朝着替代M ayavi 的方肉努力,但冃
前 Python 环境中尚未有能替代 V T K 和 M ayavi 的专业级别的三维可视化扩展库,因此木书仍保
留 第 1版中相关的章节。

1 . 1 . 2 开发环境

和 M A T L A B 等商用软件不同,Python 的众多扩展库由许多社R 分别维护和发布,因此要


一一将其收集齐全并安装到计算机中是一件十分耗费时间和精力的事情。本节介绍两个科学计
算用的 Python 集成软件包。读者只需要下载并执行一个安装程序,就能安装好本书涉及的所有
扩展库。

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 安装文件。

阁 1-1显 示 了 通 过 WinPython Control P a n d 安装木书介绍的儿个扩展库。通 过 “ Add


packages ”按钮添加扩展库的安装程序之后,单 击 “ Install packages ”按钮一次性安装勾选的

所有扩展库。
i l l 然手动安装扩展库有些麻烦,不过这种方式适合没有网络连接或者网速较慢的计算机。
例如在笔者的工作环境中,有大量的实验用计箅机不允许连接互联网。
「一
图 1-1通过 WinPython Control Panel安装扩展阵

如果读者从 WinPython 的官方网站下载 WinPython 幵发环境,为了运行本书的所有实例程


序,还需要安装如下扩展库:
• V T K 、Mayavi、pyface 、Tm its 和 TraitsUI: 在图形界而以及三维可视化章节需要用到这
些扩展库。
• OpenCV : 在图像处理章节需要用到该扩展库。

2. Anaconda

一 https://store.continuum.io/cshop/anaconda/
C 交 Anaconda 的下载地址。

由 C O N TIN U U M 开发的 Anaconda 开发环境支持 Windows、Linux 和 Mac 0 S X 。安装时会


提示是否修改 P A T H 环境变量和注册表,如果希望手工激活 Anaconda 环境,请取消选杼这两个
选项。
在命令行中运行安装路径之下的批处理文件Scripts\anaconda.bat以启动 Anaconda 环境,然
后就可以输入表1-1中的 conda 命令来管理扩展库了。

表 1-1 conda 命令及说明

叩 *7 说明
conda list 列出所有的扩展厍
conda update扩展沛•名 升级扩展库
conda install扩展序名 安装扩展砟
conda search 模板 搜索符合模板的扩展库
Python 科学计算(第 2 版)

conda 命令本身也是一个扩展库,因此通常在执行上述命令之前,可以先运行 conda update


co n d a 来尝试升级到最新版本。co n d a 默认从官方频道下载扩展库,如果未找到指定的扩展库,
还 可 以 使 anaconda 命 令 从 Anaconda 网站的其:他频道搜索指定的扩展库。例如下j l ]的命令用
于搜索可使用 conda 安装的 O penC V 扩展库:

binstar search -t conda opencv

找到包含 FI标扩展库的频道之后,输入下面的命令来从指定的频道c ig n e ll 安装:

conda install opencv-python -c rsignell

还 可 以 使 用 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科

请读者将之解压到 C 盘根0 录之下,该压缩包会创建 C :\WinPython-32bit-2.7.9.2 0 录。


n 学计算环境的安装与简介

然后将本书附赠光盘中提供的代码0 录 sdpybook 2 复制到计兑机的硬盘中,为了保证代码


正常运行,请确保该代码目朵的完整路径中不包含空格和中文字符。在 sdpybook 2 中包含三个
子目录:
• codes: 艽 中 的 scpy2 子目录下包含本书提供的示例程序,该示例程序库采用包的形式管
理,冈此需要将它添加进Python 的包搜索路径环境变量P Y T H O N P A T H 中才能正确运
行 scpy2 中 的 示 例 程 序 。在 scipybook2 录 下 的 批 处 理 文 件 run_console.bat 和
run_notebook.bat中会自动设置该环境变M 。
• notebooks: 木书完全使用 EPythonNotebook编写,该目录下的 Notebook 文件中保存了本
书所有章节的标题以及示例代码。读者可以通过 run_notebcxi.bat 批处理文件启动木卞〕
的编写环境。为了保护本书版权,除本章之外的其他所有章节的文字解说内容都已被
删除。
• settings:其中保存了各利1扩展库的配置文件。这些文件会保存在 H O M E 环境变量所设
置的 E1录之下,
默认值为 C:\UsenAffl户名。
为了避免与读者系统中的配置文件发生冲突,
在批处理文件中将 H O M E 环境变量修改为该 settings 回录。
为了确认开发环境正确安装,请读者运行 mn_consde.bat 批处理文件,然后在命令行中执
行 python -m scpy 2 , 并检查是否打印出开发环境中各个扩展厍的版本信息。

如果读者将 winpython.z ip 文件解压到别的路径之下,可以修改 env.b a t 文件中第二行中


^1 的路径。
4
!python -m scpy2
Welcome to Scpy2
Python: 2.7.9
executable: C :\WinPython-32bit-2.7.9.2\python-2.7.9\python.exe
Cython 0.22
matplotlib 1.4.3
numpy-MKL 1.9.1
opencv_python 2.4.11
pandas 0.16.0
scipy 0.15.0
sympy 0.7.6

1 . 1 . 3 集成开发环境(IDE)

本节介绍两个常州的 Python 集成开发环境,它们能实现自动完成、定义跳转、 自动重构、


调试等常用的 ID E 功能,并集成了 IPython 的交互环境以及g 看数红[、绘制图表等科学计算开
发中常用的功能。熟练使用这些工具能极大地提高编程效率。

1 •Spyder

Spyder 是 由 WinPython 的作者开发的一个简单的集成开发环境,可通过 W inPython 的安装


H 录 下 的 Spyder.e >ce 来 运 行 。如 果 读 者 希 望 在 本 书 的 开 发 环 境 中 运 行 Spyder , 可以在
run_console.bat开启的命令行中输入 spyder 命令。
和其他的 Python 开发环境相比,它最大的优点就是模仿M A T L A B 的“工作空间”的功能,
可以很方便地观察和修改数组的值。图 1-2是 Spyder 的界面截图。


冬| 1-2在 Spyder中执行图像处理的程序

Spyder 的界面由许多泊坞窗口构成,用户可以根据自己的喜好调整它们的位置和大小。当
多个窗口在一个区域屮吋,将使用标签页的形式显示。例 如 在 图 1-2屮,可 以 看 到 “Editor”、
Python 科学计算(第 2 版)

“Variable explorer ” 、 “ File explorer”、 “IPythonconsole”等窗 LI 。在 View 菜单中可以设贺-是


否显示这些窗口。表 1-2中列出了 Spydei•的主要窗口及其•作JIJ:

表 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

AWiy» ihoi*•« an•lr«1liki

图 1-3运行配咒对话框

• Command line options: 输入程序的运行参数。


• Working directory: 输入程序的运行路径。
• Execute in current Python or IPython interpreter: 在当前的 Python 控制台中运行程序。程序
可以访问此控制台中的所有全局对象,控制台屮已经载入的模块不需要重新载入,因
此程序的启动速度较快。
• Execute in a new dedicated Python interpretei•:新开一个 Python 控制台并在其中运行程序,
程序的启动速度较慢,但是由于新控制台中没有多余的全局对象,因此更接近真实运
行的情况。当选择此项时,还⑴以勾选“ Interact with the Python interpreter after execution”,
这样当程序结束运行之后,控制 f t 进程继续运行,吋以通过它查看程序运行之后的所
有全局对象。此外,还 可 以 在 “ Command line options”中输入新控制台的;t i 动参数。
• Execute in an external System terminal:选择该选项则完全脱离 Spyder 运行程序。
6
运行配置对话框只会在第一次运行程序时出现,如果想修改程序的运行配置,可 以 按 F 6
键来打开运行配置对话框。
控制台中的全局对象可以在 “ Variable explorei•”窗口中找到。此窗口支持数值、字符串、
元组、列表、字典以及 N um Py 的数组等对象的显示和编輯。图 M (左)楚“Variableexplorer”窗
口的截图,列出了当前运行环境中的变量名、类型、大小及其内容。右键单击变量名,会弹出
对此变量进行操作的菜单。在菜单中选杼 Edit 选项,弹出图1»4(右)所示的数组编辑窗口。此编
辑窗口中的单元格的背景颜色直观地显示了数值的大小。当有多个控制台运行时, “ Variable
explorer ”窗口显示当前控制台中的全局对象。

Python
科学计算环境的安装与简介
图 1 4 使 用 “Variablecxplorcr”杏看■和编钳变S 内容

选杼菜单中的 Plot 选项,将弹出如图1-5所示的绘图窗口。在绘图窗口的工具栏中单击最


右边的按钮,将弹出一个编辑绘图对象的对话框。图中使用此对话框修改了曲线的颜色和线宽。

7
Python科学计算(第2 版)

Spyder的功|拒比较多,这里仅介绍一些常用的功|拒和技巧:
• 默 认 配 置 下 , “Variableexpbrer”中不显示大写字母幵头的变量,可以单击工具栏中的
配置按钮(最后一个按钮),在菜单中取消 “ Exclude capitalized references” 的勾选状态。
•在 控 制 台 中 ,可以按 Tab 键自动补全。在变量名之后输入“?”,可以在“Objectinspector”
窗口中杏看对象的说明文档。此窗口的 Options菜 单 中 的 “Showsource”选项可以开泊
显示函数的源程序。
• 可 以 通 过 “ Working directory”工具栏修改工作路径,用户程序运行时,将以此工作路
径作为当前路径。只需要修改工作路径,就可以用同一个程序处理不同文件夹下的数
据文件。
• 在 程 序 编 辑 窗 口 中 按 住 C tr l 按键,并单击变量名、函数名、类名或模块名,可以快速
跳转到其定义位置。如果是在别的程序文件中定义的,将打开此文件。在学习一个新
的模块库的用法时,经常需要查看模块屮的某个函数或某个类是如何定义的,使用此
功能可以帮助我们快速查看和分析各个库的源程序。

2. PyCharm
Pytho科

PyChami是由 JetBmins开发的集成开发环境,具有项 R 管理、代码跳转、代码格式化、自


n 学计算环境的安装与简介

动完成、重构、自动导入、调试等功能。虽然专业版价格比较高,但是提供的免费社区版具有
幵 发 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
庳所在的路径。

Pro|ect Interpreterc Interpreter Paths

g- 7.7S (CW»iFyhcr^ 32bit-2.7.5.3,^><hen-2.7.a ,p>thon^y:e) -f /samiN +


0:
rWnPython-32blt-27^^tron-275VLib
C¥p^py-2.6.0-wh3Wpv〇
ycxc ():Wrkiyhnn ^mi Wllihik
C:VWinPython-32bit-2.75 <2Vp>>thon-2.75
:l*/»hrl «'A^yKv^n rw l: AC^
CtW«nP>thon-32bit-2.7.9^tp>tron-2.7.^Li^ ite-p^ kd2^win32
O'AVinPython 32bit 2752Vpyti-ion 2.7S¥Lib¥^itc p^:kGcc$Vwin02Vlib
C•客
WiiPylfwfrS2L丨 卜2.7.9«2^y0wfr 2.7•保L il w""ri
丨祕膽jS_ 藤 纪 臟 售 應 _
0K

OK Cancel

Pytho科
图 1-7添加库搜索路径

n 学计算环境的安装与简介
1.2 IPython Notebook 人门

与本节内容对应的 Notebook 为:01-intro/intro~200~ipython.ipynb。


DVD

III从 IPython 1.0发布以来,越来越多的科学家、研究者、教师使用 IPython N otebook 处


理数据、写研究报告,甚至编写书籍。可 以 使 用 下 面 的 n b v ie w e r 网站查看在网络上公开的
Notebook :

http://nbviewer.ipython.org/
Q 通过这个网站可以快速查看网络上任何Notebook 的内容《

在 IPython 的官方网站上收集了许多开发者发布的Notebook:

減 https://github.com/ipython/ipython/wiki/A -galleiy-of-interesting-IPython-Notebooks
上面有许多有趣的Notebook。

木节简要介绍 IPython Notebook 的■本使用方法、魔法命令以及®示系统等方而的内容。


Python科学计算(第2 版)

1 . 2 . 1 基本操作

1.运行丨Python Notebook

使用系统的命令行工具切换到保存Notebook 文档的目W ,输 入 ipython notebook 命令即可


启 动 Notebook 服务器,并通过系统的默认浏览器打开地址http://127.0.0.1:8888。建议读者最好使
用 Firefox 或 Chrome 浏览 Notebook。
木书提供的代码丨录scipybook2 中包含了一个泪动Notebook 的批处理文件run_notebook.bat<
运行该批处理文件之后,
在浏览器的 Notebook 列表中依次单it ? Ol-intro~^in tro l 00~ipython.ipynb,
就能打开与本节对应的Notebook 文档。
如 图 1-8所示,N oteb w k 采用浏览器作为界面,首页显示当前路径下的所有Notebook 文档
和文件夹。单 击 “NewNotebook ”按钮或文档名将打开一个新的央面,N 时启动一个运算核进
程与其交互。每个打开的 Notebook 页面都有单独的 Python 进程与之对应,在 Notebook 屮输入
的所有命令都将由浏览器传递到服务器程序,再转发到该进程运行。文档的读収和保存工作由
服务器进程完成,而运算核进程则负责运行用户的12序 。因此即使用户程序造成运箅核进程舁
Pytho科

常退出,也不会丢失任何州户输入的数据。在关闭服务器进程之前,确保所有的 Notebook 都已
保存。
n 学计算环境的安装与简介

is PIW
■Notetx
ook
& nm m

服务器逬程
峨鼷

运®核逬程

图 1-8 IPython Notebook 架构示S 图

Notebook 有自动存档和恢复功能,可通过 File Revert to Checkpoint菜单恢复到以前的版


本。此外为了确保安全,打开他人创建的 N otebook 时,不会运行其中的 Javascript程序和显示
S V G 图像。如果确信来源川_翁,可以通过 File — Trusted Notebook 信任该 Notebook。

2.操作单元

notebooks\01-intro\notebook-train.ipynb : Notebook 的操作教程,读者可以使用它练习


Notebook 的基本操作。

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

编写技术资料少不了输入数学公式,Notebook 使 用 MathJax将输入的 L a T eX 文本转换成数


学公式。由于 MathJax 库较大,没有集成到 IPython 中,而是直接从 MathJax 官网载入,因此如
果计兑机没有联网,就无法正确鼠示数学公式。为了解决这个问题,可以在单元中输入如下程

Python
序 ,它将会下载 MathJax 到本地硬盘:

科学计算环境的安装与简介
from IPython.external.mathjax import install_mathjax, default_dest
install—mathjax()

MathJax 完整解压之后,约 耑 100M B 空间,其中大都是为旧版浏览器准备的 P N G 字体图


像文件。执行下面的语句可以快速删除存放P N G 字体图片的文件夹:

from os import path


import shutil

png path = path.join(defaultdest, "fonts/HTML-CSS/TeX/png")


shutil.rmtree(png path)

运行完上面的命令之后,在命令模式下按 m 键将中.元样式切换到 Markdown 。然后输入如


下 L a T eX 文本:

$eA{i \p i > + 1 = 0$

按 Shift+ Enter快捷键之后,其内荇将被转换成数学公式显示:e iTt + 1 = 0 。


在本书提供的 scipybook2 下 的 settings 目下已经安装了 MathJax, 因此不必联网也「
lT以看

到数学公式。

4. 操作运算进程

在代码单元中输入的代码都将在运算核进程的运行环境中执行。当执行某些代码出现问题
时,可以通过 Kernel菜单中的选项操作该进程:
• im e ir u p h 中断运行当前的程序,当程序进入死循环时可以通过它中断程序运行。
Python 科学计算(第 2 版)

• Restart:当运算核进程在扩展模块的程序中进入死循环,
无法通过 Interrupt菜单中断时,
可以通过此选项重新启动运算核进程。
一旦运算核进程被关闭,运行环境中的对象将不复存在,此时可以通过 Cell — Rim A l l 菜
单再次执行所有单元中的代码。代码将按照从上到下的顺序执行。由于用户在编写 Notebook
时,可以按照任意顺序执行单元,冈此为了保证能再现运行环境中的所有对象,请记住调整单
元的先后顺序。

1. 2 . 2 魔法(Magic)命令

IPylhon 提 供 / 许多魔法命令,使得在 IPython 环境中的操作更加得心应手。魔法命令都以%


或%%开头,以%开头的为行命令,以% %开头的为中.元命令。行命令只对命令所在的行有效,
而单元命令则必须出现在单元的第一行,对整个单元的代码进行处理。
执行%m agic 可以查看关于各个命令的说明,而在命令之后添加?可以查看命令的详细说明。
此外扩展库可以提供自己的魔法命令,这些命令叫以通过%load_ex t 载入。例如%load_extcython
载入%%cython 命令,以该命令开头的单元将调用Cython 编译其中的代码。
Python

1.显 示 matplotlib 图表
科学计算环境的安装与简介

matplotlib是 Python 世界中最著名的绘图扩展伟,支持输出多种格式的图形图像,并且可以


使用多种 G U I 界面座交互式地显示图表。使用%matplotlib命令可以将 matplotlib 的图表直接嵌
入 到 N otebook 中,或者使用指定的界而序.显示图表,它有一个参数指定 matplotlib 图表的显示
方式。
在下而的例子中,inline表示将图表嵌入到 Notebook 中。因此由最后一行pl.plot()创建的图
表将直接显示在该单元之下:

%matplotlib inline
import pylab as pi
pl.seed(l)
data = pl.randn(100)
pi.plot(data)

内嵌图表的输出格式默认为PN G ,可以通过%〇)11(^命令修改这个配:®!。%config 命令可以


配 置 IPython 中的各可配置对象,其 中 InlineBackend对象为 matplotlib输出内嵌图表时所使用的
配置,我们配S 它 的 figure_format="svg ",这样可将内嵌阁表的输出格式修改为S V G 。

%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.性能分析

性能分析对编写处理大量数据的程序非常重要,特別是 Python 这样的动态语言,一条语句


可能会执行很多内容,有的是动态的,有的调)lj 扩展库。不做性能分析,就无法对程序进行优
化。IPython 提供了性能分析的许多魔法命令。
调 用 timeit模块对单行语句重复执行多次,计算出执行时间。下面的代码测试修改
列表的单个元素所需的时间:

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

timeit命令会重复执行代码多次,而 tim e 则只执行一次代码,输出代码的执行情况。和 timeit


命令一样,tim e 可以作为行命令和单元命令。下面的代码统计往空列表中添加10万个元素所耑
的时间:

%%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 版)

for n in [1000, 5000, 10000, 50000, 100000, 500000]:


print "n={0}".format(n)
alist = range(n)
%time random.shuffle(alist)

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函数的时间复杂度

% % pm n 命令调j|j profile 模块,对单元中的代码进行性能剖析。下面的性能剖析显示 fib()


运行了 21891次,而 fib_fast〇则只运行了 2 0 次:

Pytho科
%%prun

n 学计算环境的安装与简介
def fib(n):
if n < 2:
return 1
else:
return fib(n-l) + fib(n-2)

def fib_fast(n, a=l, b=l):


if n == 1:
return b
else:
return fib_fast(n-l, b, a+b)

fib(20)
fib_fast(20)
21913 function calls (4 primitive calls) in 0.007 seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)


21891/1 0.007 0.000 0.007 0.007 〈
string〉:
2(fib)
20/1 0.000 0.000 0.000 0.000 〈
string〉:
8(fib_fast)
1 0.000 0.000 0.007 0.007 〈
string〉:
2(<module>)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler'
objects}
Python 科学计算(第 2 版)

3.代码调试

% d e b u g 命令用于调试代码,它有两种用法:一•种是在执行代码之前设置断点进行调试;
另一种则是在代码抛出兄常之后,执 S % debug 命令查看调j |j 堆栈。下面先演示第二种州法:

import math

def sinc(x):
return math.sin(x) / x

[sinc(x) for x in range(5)]

ZeroDivisionError Tnaceback (most recent call last)


<ipython-input-28-9b69eaad97fe> in <module>()
4 return math.sin(x) / x
5
--- > 6 [sinc(x) for x in range(5)]
Python
科学计算环境的安装与简介

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

ZeroDivisionError: float division by zero

上而的程序抛出/ ZeroDivisionError异常,下面用 % (^1^ 资看调用堆栈。在调试模式下可


以使用 pdb 模块提供的调试命令,例如用命令 p x 显示变量 x 的值:

%debug
><ipython-input-28-9b69eaad97fe>(4)sinc()
3 def sinc(x):
--- >4 return math.sin(x) / x
5

ipdb> p x
0
ipdb> q

还可以先设置断点,然后运行程序。但是% debiig 的断点需要指定文件名和行号,使用起


来并不是太方便。本书提供 / % % func_debug单元命令,可以通过它指定中断运行的函数。在下
面的例子中,程序将在 numpy.uniqueO的第一行中断运行,然后通过输入命令 n 单步运行程序,
最后输入命令 C 继续运行:

%%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 是这些
魔法命令的使用说明。

IPython 提供了很方便的定义魔法命令的方法。最简单的方法就是使用 register_ line_magic


和 register_cell_ m agic 装饰器将函数转换为魔法命令。下面的例子使j|j register_line_ m agic 定义了
一个行魔法命令%find , 它在指定的对象中搜索与[1!标匹配的属性名:

from IPython.core.magic import register_line_magic

^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

当调m % fin d 行魔法命令时,魔法命令后面的所有内容都传递给 lin e 参数。O 按照空格对


lin e 进行分隔,除最后一个元素之外,其余的元素都作为搜索模板,而最后一个参数则为搜索
的丨e|标。© 通 过 get_ ipython()函数获得表示 IPython运算核的对象,
通过该对象可以操作运算核。
€)调用运算核的〜()方法对表达式 target求值以得到实际的对象,并 用 dir()获取该对象的所宵属
性名。
最后使用 fnmatch 模块对搜索模板和属性名进行匹配,将匹配结果保存到 results 并返回。
下面使用%*^1命令在 numpy 模块屮搜索所有以 array 开头或包含 m u l 的属性名:
Python

import numpy as np
科学计算环境的安装与简介

names = %find array * *mul* np


names

[•array、 ,array2string,, _array_equal', _array—equiv1,

•array一repr', 'array_split •, _array_str', •multiply、

_polymul、 'ravel _multi_index ']

下而的例子使用 register_cell_m agic 注册%% c u t 中.元命令。在调试代码时,我们经常会添加


print语句以输出中间结果。但如果输出的字符串太多,会导致浏览器的速度变慢甚至失去响应。
此时可以使用%% c u t 限制程序输出的行数和字符数。
oit ()函数有两个参数:lin e 和 cell , 其 中 lin e 为单元第一行中除了魔法命令之外的字符串,
而 c e ll 为除了单元屮第一行之外的所有字符串。line 通常为魔法命令的参数,而 c e ll 则为需要执
行的代码。IPython 提供了基于装饰器的参数分析函数。下面的例子使j|j argumentO声明了两个
参数-1和~c ,它们分别指定最大行数和最大字符数,它们的默认值分别为100和 1 _ :

from IPython .core.magic import re g iste r _cell_magic


from IPython .co re .magic一arguments import argument, magic—arguments, parse 一argstrin g

@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

counters = dict(chars=0, lines=0)

def write(string):
counters ["lines"] += string, count ("W.)
counters["chars"] += len(string)

if counters["lines"] >= max_lines:


raise IOError("Too many lines")
elif counters["chars"] >= max_chars:
raise IOError("Too many characters")
else:

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

IOErnon Traceback (most recent call last)


<ipython-input-9-5d2e5180bel8> in <module>()
1 for i in range(10000):
Python 科学计算(第 2 版)

--- > 2 print "I am line , i

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

IOError: Too many lines

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()
对象,因此它也会被显示为数学公式。

from IPython import display

for i in range(2, 4):


display.display(display.Latex("$xA{i} + yA{i}$".format(i=i)))
display.Latex("$xA4 + yA4$")

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)

在后续的章节中经常会将N um Py 数组®示为图像,这时可以使用 matplotlib中提供的函数


将数组转换成 P N G 图像的字符串,然 后 通 过 Im age 将图像数据嵌入到 Notebcx)k 中。下面的
as_ png〇使 用 matplotlib 屮的 imsave()将数组转换成 P N G 图像数据:

def asj3ng(img, **kw):


"将数组转换成PNG 格式的字符串数据"
import io
from matplotlib import image
from IPython import display
buf = io.Bytes10〇
image.imsave(buf, img, **kw)
return buf.getvalue()

下面的程序通过公式s in (x 2 + 2 •y 2 + x •y )生成二维数组 z ,并调用 as_png()将其转换为字


符 串 png , 査看该字符串的头10个字节,可以看出该字符串就是 P N G 图像文件中的数据。最

21
Python 科学计算(第 2 版)

后使用 ImageO将该字符串使用 P N G 图像M 示出来,结果如图1-10(左)所示。

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.自定义对象的显示格式

有两种方式可以f-l 定义对象在 Notebook 中的显示格式:


•给类添加相应的显示方法。
•为类注册相应的显示函数。
当我们自己编写类的代码时,使用第一利汸法最为便捷。和 Python 的_边_()方法类似,
Python

只需要定义_repr_*_〇等方法即可,这里的*可以是 html、svg 、javascript、latex、png 等格式的名称。


在下面的例子中,
C olor 类中定义了 EPython用的两个显示方法:_ repr_html_()和」•epr_png_(),
科学计算环境的安装与简介

它们分别使用 H T M L 和 P N G 图像显示颜色信总。

class Color(object):

def 一 init_ (self, r, g, b):


self.rgb = r, g, b

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_()方法将被
调用:

c = Color(255, 10, 10)

为了使用其他格式显示对象,可 以 调 用 display.display_*〇函数,这 里 调 用 display_png()将


Color对象转换成 PNG 图像显示:

display.display j3ng(c)

每种输出格式都对应一个 Formatter对象,它们被保存在 DisplayFormatter对 象 的 formatters


字典中,下而获取该字典中与PNG 格式对应的 Formatter对象:

shell = get_ipython()

png^_formatten = shell.display_formatter.formatters[u 'image/png']

Python
调 用 Formatter.for_type_by_name〇可以为该输出格式添加指定的格式显示函数,其前两个参

科学计算环境的安装与简介
数分别为模块名和类名。由于使用字符串指定类,因此添加格式显示函数时不需要载入IeI标类。
下而的代码为 NumPy的数组添加品示函数as_png():

p n g ^ f o r m a t t e r .f o r _ t y p e _by_name("numpy" "ndarray", as_png)

下而查看前而创建的数组z ,它将以图像的形式呈现,结果如图1-10(右)所示。

图 1-10使 用 as_png〇将数组显示为 PN G 图像(左),将 as_png〇}1•:册为数纽的显示格式(右)

如果目标类已被载入,可以使用 f〇
r_type()方法为其添加格式显示函数。下而的代码将表示
分数的 Fmction类使用 LaTeX 的数学公式进行显示:

from fractions import Fraction


latex_formatter = s h e l l .d i s p l a y _ f o r m a t t e r .f o r m a t t e r s [u"te x t / l at e x " ]
def fraction 一
formatter(obj):
return •$$\\frac{%d}{%d}$$, % (obj.numerator, obj.denominator)
Python 科学计算(第 2 版)

latex 一
formatter.for_type(Fraction, fraction_formatter)

Fraction(3, 4) ** 4 / 3

27
25 6

1.2.4 定制 IPython Notebook

虽 然 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)

每次启动 I P y t h o n 时都会从指定的用户配f f i (p r 〇m e )文件夹下读取配置信息。下而的代码输


出当前的用户配置文件夹的路径:该路径由 H O M E 环境变量、.i p y t h o n 和 p r o f i l e j ® 置名构成。

在本书提供的运行 I P y t h o n 的批处理文件中配置了 环境变量,因此能


Pytho科

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"]

print "IPython 配置文件夹: " ,i p y t h o n •ipython_dir


print " 当前的用户配置文件夹: " ,ipython.config.ProfileDir.location

HOME 环境变量:C :\Users\RY\Dropbox\scipybook2\settings


IPython 配置文件火:C :\Users\RY\Dropbox\scipybook2\settings\ •ipython

当前的用户配贾文件夹:
C :\Users\RY\Dropbox\scipybook2\settings\.ipython\profile_scipybook2

可以在命令行中输入如_K 命令来创建新的用户配置:

ipython profile create test

修改用户配置文件夹之下的配置文件之后,在 启 动 N o te b o o k 时通过- p r o f i l e 参数指定所采


用的用户配:1 S

ipython notebook --profile test

2 .服务器扩展插件和 N o te b o o k 扩展插件

在.i p y t h o n 文件夹之下还有两个子文件夹--- e x t e n s i o n s 和 n b e x te n s io n s , 它们分别用于保存


服务器和浏览器的扩展程序。
• e x te n s io n s : 存放用 P y t h o n 编写的服务器扩展程序。
• n b e x te n s io n s : 存放N o te b o o k 客户端的扩展祝序,通常为 J a v a S c r i p t 和 C S S 样式表文件。
N o te b o o k 的服务器于 to rn a d o 服务器框架开发,因此编写服务器的扩展程序需要了解
to rn a d o 框架,而 开 发 N o te b o o k 客户端(浏览器的界面部分)的扩展程序则耑要了解 H T M L 、
J a v a S c r ip t 和C S S 样式表等方面的内容。这些内容与本书的主题无关,就不再详细叙述了。下面
看看如何安装他人开发的扩展程序。

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 扩展程序的说明。

首先执行下面的语句来安装N o te b o o k 客户端的扩展程序,u s e r 参数为 T r u e 表示将扩展安装


在H O M E 环境变量路径之下的.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 文件。

from IPython.utils.path import get_ipython_dir

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

nbextensions 扩展程序为服务器添加 / 一个新的 URL---hUp://localhost:8888/nbextensions/,通


过 该 路 径 可 以 开 启 或 禁 止 指 定 的 客 户 端 扩 展 程 序 。nbextensions扩展程序通过递归搜索
ntextensions文件夹下的 Y A M L 文件识别客广端扩展程序,IPython-noteb(x)k~extensiorLS-3.x 目录下
只有部分扩展程序附带了 Y A M L 文件,读者可以仿照这些文件为其他的扩展程序添加相应的
Y A M L 文件,这样就可以通过 nbextensions页面管理扩展程序了。

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

kernels_folder = path.join(ipython.ipython_dir> "kernels")


if not path.exists(kernels 一
f o l d e r ):
os.mkdir(kernels— folder)

python3_path = "C:\\WinPython-64bit-3 . 4 . 3 . 3 W s c r i p t s W p y t h o n . b a t "

kernel_SGttings = {
" a r g v " : [python3j3ath,

"IPython.kernel"-f", "{connection_file}"],

" d i s p l ayjiame": "Python3-64bit",


"language": "python"

>

kernel_folder = p a t h .j o i n (kernels_folder, ker n e l _ s et t i n g s ["display_name"])


if not path.exists ( k e r n e l _ f o l d e r) :
os.mkdir(kernel_folder)
ke rn e l_ fn = p a th .jo in (k e r n e l_ fo ld e r , " k e rn e l.js o n ")

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 扩展库介绍

与本节内容对应的 Notebook 为:01-intro/intro~300~library.ipynb。

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

np.sum(4.0 / np.r_[l:n:4, -3:-n:-4])

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

from scipy.integrate import quad


quad(lambda x : ( l - x * *2)**0.5 ,-1 ,1)[0] * 2

3.141592653589797

1 . 3 . 2 符号计算库

Sym P y 是一套数学符号运算的扩展库,虽然与一些专门的符号运算软件相比,S ym P y 的功
Python

能以及运算速度都还是较弱的,但是由于它完全采用 Python 编写,因此能够很好地与其他的科


学计算库相结合。
科学计算环境的安装与简介

下 面 用 S ym P y 提供的符号积分函数 imegrateO对上面的公式进行积分运算,可以看到运算
的结果为符号表示的TI:

from sympy import symbols, integrate, sqrt


x = symbols("x")
integrate(sqrt(l-x**2), (x, -1, 1)) * 2

Pi

1 . 3 . 3 绘图与可视化

matplotlib是 PythoniS 著名的绘图库,它提供了一整套和 M A T L A B 类似的绘图函数集,十

分适合编写短小的脚本程序进行快速绘图。此外 , matplotlib采用面向对象的技术来实现,因此
组成图表的各个元素都是对象,在编写较大的应用程序吋通过面向对象的方式使用 matplotlib
将更加有效。
下面的程序绘制隐函数(x 2 + y 2 _ I )3 —x 2y 3 = 0 的曲线,结果如图 M l 所示。

x, y = np.mgrid[-2:2:500j> -2:2:500j]

2 = (x**2 + y**2 - i)**3 - x**2 * y**3


pl.contourf(x, y ,z, levels=[-l, 0], colors=["red"])
pi.gca().set_aspect("equal")

冬I 1-11 matplotlib绘制心形隐数曲线

V T K 是一套功能十分强大的三维数据可视化库,T V T K 库在标准的 V T K 库 之 上 用 Traits


库进行封装。而 Mayavi2 则在 T V T K 的蕋础上添加了一套面向应用的方便工具,它既可以单独
作为3D 可 视 化程序使也可以很方便地眹入到 TraitsUI编写的界面程序中。
在下面的例子中,
使 用 M ayavi 绘制如下隐函数的曲面,结果如图1-12所示。

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

contour = mlab.contour3d(x, y, z, i y contours=[0] ^ color=(l^ 0, 0))

图 1-12使用Mayavi绘制心形隐函数曲而

1 . 3 . 4 数据处理和分析

Pandas 在 N um Py 的雜础之.丨•.提供类似电子表格的数据结构DataFmme, 并以此为核心提供


大量的数据的输入输出、清洗、处理和分析闲数。其核心运算闲数使用Cython 编写,在不失灵
活性的前提下保证了函数库的运算速度。
Python 科学计算(第 2 版)

在下1丨1|的例子中,从屯影打分数据 M ovieLens 中读入用户数据文件 u.usei•,并显示其中的


头 5 条数据:

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

user_id age sex occupation zip_code


0 1 24 M technician 85711

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

为柱状图,如 图 M 3 所示。可以看到如此复杂的运算在Pandas 中可以使用一行代码完成:


科学计算环境的安装与简介

df.groupby("occupation").age.mean().order().plot(kind=,,bar"> figsize=(12, 4))


0

(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 界面设计

Python 可以使用多种界面庵编写 G U I 程序,例如标准库中自带的以 T K 为g 础 的 Tkinter、


以 wxW idgets 为蕋础的 wxPython 和 以 Q T 为■础的 pyQt4 等界面库。但是使用这些界而库编写
G U I 程序仍然是一件十分繁杂的工作。为了让读者不在界面设计上耗费大量精力,从而能把注
意力集中到如何处理数据上去,本书详细介绍了使用Tm its 库设计图形界面程序的方法。
Traits 库分为 Traits 和 TraitsUl 两大部分,Traits 为 Python 添加了类型定义的功能,使用它定
义的 Trait 属性具有初始化、校验、代理、事件等诸多功能。
TraitsUI库基于 Traits 厍,使 用 M V C (模型一视图一控制器)模式快速定义用户界面,在最简
单的情况下,甚至不需要写一句界面相关的代码,就可以通过 T raits 的属性定义获得一个可以
使叫的图形界面。使)I〗TraitsUI库编写的程序自动支持wxPython 和 p yQ t 两个经典的界面库。

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)

bimg = cv2.threshold(img, 50, 255, cv2.THRESH_BINARY)


contour, _ = cv2.findContours(bimg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPR0X_TC89_Ll)

Python
contour = c v2.approxPolyDP(contour[0], e p s ilon=2> closed=False)

科学计算环境的安装与简介
area = cv2.contourArea(contour)

perimeter = c v 2 .a r c L e n g t h (contour, True)

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

下面使用%出1^比较 calc_pi()和 N um Py 库来计算ti的运算时间:

n = 1000000
%timeit calc_pi(n)

%timeit np.sum(4.0 / np.r_[l:n:4, -3:-n:-4])

100 loops, best of 3: 3.9 ms per loop

100 loops, best of 3: 5.94 ms per loop


Python
科学计算环境的安装与简介
NumPy-快速处理数据
标准的 Python 中用列表(list)保存一组值,可以用来当作数组使州。但由于列表的元素可以
是任何对象,因此列表中保存的是对象的指针。这样的话,为了保存一个简单的列表,比如[1,
2,3J,
需要三个指针和三个整数对象。对于数值运算来说,这种结构显然比较浪费内存和 C P U 计算
时间。
此外,Python 还提供了 array 模块,它所提供的 array 对象和列表不同,能直接保存数值,
和 C 语言的一维数组类似。但是由于它不支持多维数组,也没有各利I运算函数,因此也不适合
做数值运算。
N um Py 的诞生弥补了这些不足,N um Py 提供了两种基本的对象:

• ndairay: 英文全称为 n-dimensionalarrayobject,它是存储1Y1.—数掘类型的多维数组,后


统一称之为数组。
• u fu n c : 英文全称为 universal function o b ject ,它是•种能够对数组进行处理的特殊
函数。
本书采用 NumPy 1.9版本,请读者运行下面的程序以查看N um Py 的版本号:

import numpy
numpy._ version

2.1 ndarray 对象

与本节内容对应的Notebook 为: 02-numpy/numpy-100-ndairay.ipynb〇

本书的示例程序假设用以下推荐的方式导入N um Py 函数库:

import numpy as np

N um Py 屮使用 ndarray对象表示数组,它是整个库的核心对象,N um Py 中所有的函数都是


围 绕 ndarray 对象进行处理的。ndarray 的结构并不复杂,但是功能却十分强大。不但可以用它
Python 科学计算(第2 版)

高效地存储大量的数值元素,从而提高数组计兑的运兑速度,还能用它与各种扩展库进行数据
交换。本节的内容可能会有些枯燥,但是为了打下一个SL好的基础,让我们从深入理解ndairay
对象开始学习 Python 科学计算之旅。

2 . 1 . 1 创建

首先需要创建数组才能对其进行运算和操作。可以通过给_ y ()闲数传递 Python 的序列对


象来创建数组,如果传递的是多层嵌套的序列,将创建多维数组(下例中的变M c ):

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 b.shape c.shape

(4,) (4,) (3, 4)

数组 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]])

当设置某个轴的元素个数为- 1 时,将自动计兑此轴的长度。由于数组 c 有 12个元素,因


此下面的程序将数组 c 的 shape 属性改成了(2,6):

c.shape = 2 , -1

array([[ 1, 2} 3, 4, 4, 5],
[ 6, 7, 7, 8, 9, 10]])
使用数组的 reshapeO方法,可以创建指定形状的新数组,而原数组的形状保持不变:

d = a.reshape((2,2)) # 也可以用 a.reshape(2,2)


d a

[[1, 2], [1, 2, 3, 4]


[3, 4]]

数 组 a 和 d 其实共享数椐存储空间,因此修改其中任怠一个数组的元素都会同时修改另一
个数组的内容。注意在下而的例子中,数 组 d 中的2 也被改成了 100:

a [l ] = 100 # 将数组a 的第1 个元素改为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')

可以通过 dtype 参数在创建数组吋指定元素类型,


注 意 float类型是64位的双精度浮点类型,
而 com plex 是 128位的双精度复数类型:

ai32 = np.array([l, 2, 3, 4]., dtype=np.int32)


af = n p . a r r a y ([1, 2, 3, A -
], dtype=float)

ac = n p . a r r a y ([1, 2, 3, 4], dtype=complex)


ai32.dtype af.dtype a c .dtype

dtype('int32') dtype('float64') dtype('complexl28')

在上而的例子中,传速给 dtype 参数的都是类型(type)对象,其 中 float 和 com plex 为 Python


内置的浮点数类型和复数类型,而 np.im3 2 是 N um P y 定义的新的数椐类型一一3 2 位符号整数
类型。
N um Py 也有自己的浮点数类型:floatl 6、float32、float64和 floatl28。当使用 float64作 为 dtype
参数时,其效果和内置的 float 类型相 N 。
Python 科学计算(第2 版)

在需要指定 dtype 参数时,也可以传递-个字符串来表示元素的数值类型。N um Py 中的每


个数值类型都有几种字符串表示方式,字符串和类型之间的对应关系都存储在typeDict字典中。
下面的程序获得与float64类型对应的所有键值:

[key for key, value in np. t y p e D ic t . i t e m s () if value is np.float64]

[12, •(!,, l o a t 6 4 、 _float— 、 •float 、 _f8,, _d o u b l e ,, ' F l o a t S T ]

完整的类型列表可以通过下面的语句得到,
它 将 typeDict字典中所有的值转换为一个集合,
从而去除其中的重复项:

set(np.typeDict.values())
{numpy.boolj numpy. object 」 numpy.string 一, n u m p y •Unicode

numpy.voidj numpy.int8j numpy.intl6j numpy.int32j

numpy.int32, numpy.int64, numpy.uint8^ numpy.uintl6j

numpy.uint32J n u m py.uint32J numpy.uint64j numpy.floatl6,

numpy.float32, numpy.float64, numpy.float64, n u m p y .d a t e t i m e 64 J


Numpy快

n u m p y .t i m e d e l t a 6 4 <> numpy •complex64, n u m p y •complexl28. n u m p y .complexl28}

上面S 示的数值类型与数组的dtype 屈性是不同的对象。


通 过 dtype 对象的 type 属性可以获
—速 处 理 数 据

得与其对应的数值类型:

c.dtype.type

numpy.int32

通 过 N um Py 的数值类型也可以创建数值对象,下面创建一个16位的符号整数对象,它与
Python 的整数对象不同的是,它的収值范围有限,因此计算200*200会溢出,得到一个负数,
这一点与 C 语 言 的 16位整数的结果相同:

-25536

另外值得注意的是,N u m P y 的数值对象的运兑速度比 Python 的内置类型的运兑速度慢很


多,如果程序中耑要大量地对单个数值运兑,应当尽量避免使用 N u m P y 的数值对象。下面比
较了 Python 内置的 float 类型与 N um Py 的双精度浮点数值float6 4 的乘法运算的速度:

vl = 3.14

v2 = np.float64(vl)

%timeit vl*vl

%timeit v2*v2

10000000 loops, best of 3: 70.1 ns per loop

10000000 loops, best of 3: 178 ns per loop


使 用 astypeO方法可以对数组的元素类型进行转换,下面将浮点数数组 t l 转换为32位整数
数组,将双精度的复数数组12转换成单精度的复数数组:

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 自动生成数组

前面的例子都是先创建一个Python 的序列对象,然后通过 arrayO将其转换为数组,这样做


显然效率不高。因 此 N u m P y 提供了很多专门川于创逑数组的函数。下面的每个函数都W —些
关键字参数,具体用法请查看函数说明。
arange〇类似于内置闲数 range〇,通过指定开始值、终值和步长来创建表示等差数列的一维

数组,注意所得到的结果中不包含终值。例如下面的程序创建幵始值为0 、终 值 为 1、步长为
0.1的等差数组,注意终值1不在数组中:

Numpy快
np.arange(0, 1, 0.1)

0 . 1 , 0 . 2 , 0.3, 0 . 4 , 0 . 5 , 0.6, 0 . 8 , 0.9])

—速 处 理 数 据
array([ 0. , 0.7,

linspaceO通过指定幵始值、终值和元素个数来创建表示等差数列的一维数组,可以通过
endpoint参数指定是否包含终值,默认值为 Tm e ,即包含终值。下而两个例子分别演示了 endpoint
为 True 和 False 时的结果,注 意 endpoint的值会改变数组的等差步长:

np.linspace(0, 1 ,10) # 步长为 1/9


arnay([ 0. , 0.11111111, 0.22222222, 0.33333333, 0.44444444,

0.55555556, 0.66666667, 0.77777778, 0.88888889, 1. ])

np.linspace(0, 1, 10, endpoint=False) # 步长为 1/10


array([ 0. , 0 . 1 , 0 . 2 ,0.3, 0 . 4 ,0 . 5 , 0 . 6 , 0 . 7 , 0 . 8 , 0.9])

logspace()和 linspace()类似,
不过它所创建的数组是等比数列。
下 面 的 例 子 产 生 从 到 102、
有 5 个元素的等比数列,注意起始值0 表示10(>,而 终 值 2 表 示 102:

np.logspace(0, 2, 5)

a r r a y ([ 1. , 3.16227766, 10. , 31.6227766 , 100. ])

雄数可以通过base 参数指定,
其默认值为10。
下而通过将 base 参数设置为2,并设置 endpoint
参 数 为 False , 仓ij建一个比例为21/12的等比数组,此等比数组的比值是音乐中相差半音的两个
音阶之间的频率比值,因此可以用它计算一个八度中所有半音的频率:

np.logspace(0, 1, 12, base=2, endpoint=False)


Python 科学计算(第2 版)

array([ 1. , 1.05946309, 1.12246205, 1.18920712, 1.2S99210S,

1.33483985, 1.41421356, 1.49830708, 1.58740105, 1.68179283,


1 .78179744, 1 .8 8 7 7 4 8 6 3 ])

zeros〇、ones()、empty〇可以创建指定形状和类型的数组。其 中 empty〇只分亂i数组所使用的
内存,不对数组元素进行初始化操作,因此它的运行速度是最快的。下面的程序创建一个形状
为(2,3)、元素类型为整数的数组,注意其中的元素值没有被初始化:

np.empty((2_j3), np.int)

a r n a y ( [ [1078523331, 1065353216, 1073741824],

[1077936128, 1082130432, 1084227584]])

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

array([ 3.14159265, 3.14159265, 3.14159265, 3.14159265])

此外,zeros_ like()、ones_ like()、empty_ like〇、full_ like()等函数创建与参数数组的形状和类


型相 N 的数组,因此 zerosjike (a)和 zeros(a.shape,
a .dtype)的效果相 N 。

frombuffer〇、iVomstringO、 fromfileO 等 函 数 可 以 从 字 节 序 列 或 文 件 创 建 数 组 。下面以


fiomstringO为例介绍它们的用法,先创建含8 个字符的字符串 s :

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

a r r a y ( [25185, 25699, 26213, 26727], dtype=intl6)


如果把整个字符审转换为一个64位的双精度浮点数数组,那么它的值是:

np.fromstring(s, dtype=np.float)

a r r a y ([ 8 . 5 4 0 8 8 3 22e+194])

显然这个结果没有什么意义,但是如果我们用 C 语言的二进制方式写了一•组double 类型的


数值到某个文件中,那就可以从此文件读収相应的数据,并通过 fromstringO将其转换为 float64
类型的数组,或者直接使用 fromfileO从二进制文件读取数据。
fromstringO会对字符串的字节序列进行复制,而 使 用 frombufferO创建的数组与原始字符串
共享内存。由于字符串萣只读的,因此无法修改所创建的数组的内容:

buf = n p .frombuffer(sJ dtype=np.intl6)

buf[l] = 10

ValueError Traceback (most recent call last)


<ipython-input-52-f523db231ae5> in <module>()

Numpy快
1 buf = n p .frombuffen(s<) dtype=np.intl6)

— > 2 buf[l] = 10

—速 处 理 数 据
ValueError: assignment destination is read-only

Python 中还有一些类型也支持buffer 接口,例 如 bytearray、 array .array 等。在后而的章节中,

我们会介绍如何使用这些对象实现动态数组的功能。
还可以先定义一个从下标计算数值的函数,然后用 fVomfunctionO通过此函数创建数组:

def func(i):
return i % 4 + 1

n p .fromfunction (func, (10.,))

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

array([[ 1., 2., 3。 4 。 5。 6。 7。 8。 9.],


[2., 4., 6., 8., 10., 12., 14., 16., 18.],
Python 科学计算(第2 版)

[ 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 .],

[ 5 ., 10” 1 5 ., 20。 2 5 ., 3 0 ., 3 5 ., 4 0 ., 4 5 .],

[ 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 .],

[ 9 ., 18。 2 7 ., 36。 4 5 ., 5 4 ., 63。 7 2 ., 8 1 .]])

2 . 1 . 4 存取兀素

可以使用和列表相同的方式对数组的元素进行存取:

a = np.arange(10)

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

• a [5]: 用整数作为下标可以获取数组中的某个元素。
Numpy快

• a [3:5]: 用切片作为下标获取数组的一部分,包 拈 a[3]但不包拈 a [5]。


• a[:51:切片中省略开始下标,表示从 a冏开始。
—速 处 理 数 据

• a [:-l ]: 下标可以使用负数,表示从数组最后往前数。

a[5] a[3:5] a [ :5] a [ :-1]

5 [3, 4] [0, 1, 2, 3, 4] [0, 1, 2, 3, 4, 5, 6, 7, 8]

• a [h -l :2 1 : 切片中的第三个参数表示步长,2 表示隔一个元素取一个元素。
• a 卜 11:省略切片的开始下标和结束下标,步长为- 1 , 整个数组头尾颠倒。
• a [5:l >2]: 步长为负数时,开始下标必须大于结束下标。

a[l:-l:2] a[::-l] a[5:l:-2]

[1, 3, 5, 7] [9, 8, 1} 6, 5, 4, 3, 2, 1, 0] [5, 3]

下标还可以用来修改元素的值:

a[2:4] = 100, 101

array([ 0, 1, 100, 101, 4, 5, 6, 7, 8, 9])

和列表不同的是,通过切片获取的新的数组是原始数组的一个视阁。它与原始数组共享同
一块数椐存储空间。下而的程序将 b 的第2 个元素修改为-10, a 的 第 5 个元素也同时被修改为
-10,因为它们在内存中的地址相同。

40
b = a[3:7] # 通过切片产生一个新的数组b, b 和 e3 共享同一块数据存储空间
b[2] = -10 # 将 b 的第2 个元素修改为-10
b a

[101, 4,-10, 6] [ 0 , 1, 100, 101, 4,-10, 6, 7, 8, 9]

除了使用切片下标存取元素之外,N um Py 还提供了整数列表、整数数组和布尔数组等儿种
高级下标存取方法。
当使用整数列表对数组元素进行存取时,将使用列表中的每个元素作为下标。使用列表作
为下标得到的数组不和原始数组共享数据:

x = np.arange(10, 1, -1)
x

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

• x [[3,3,l ,8]]: 获 取 x 中的下标为3、3、1、8 的 4 个元素,组成一个新的数组。

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]

_F 而修改 b [21的值,但是由于E 和 \不 共 ¥内 推 ,因此 x 的值不变:

b[2] = 100

b x

7, 7, 100, 2] [10, 9, 8, 7, 6, 5, 4, 3, 2]

整数序列下标也可以用来修改元素的值:

array([10, -3, 8, -1, 6, -2, 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]])

当使用布尔数组b 作为下标存取数组 x 中的元素时,将获得数组 x 中与数组 b 中 True 对应


的元素。使用布尔数组作为下标获得的数组不和原始数组共享数据内存,注意这种方式只对应
Numpy快

于布尔数组,不能使用布尔列表。
—速 处 理 数 据

x = n p . a r a n g e( 5 J0 J -1)
x

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

布尔数组中下标为0,2的元素为Tm e , 因此获収 x 中下标为0,2的元素:

x[np.array([True, False, True, False, False])]

array([5, 3])

如果是布尔列表,就 把 T m e 当 作 1 , 把 F alse 当作0 , 按照整数序列方式获取 x 中的元素:

x[[True, False, True, False, False]]

array([4, 5, 4, 5, 5])

在 NumPy 1.10之后的版本中,布尔列表会被当作布尔数组,因此上面的运行结果会变
A 成 army([5,3])。

布尔数组的长度不够时,不够的部分都当作 False:

x[np.array([True, False, True, True])]

arnay([5, 3, 2])

布尔数组的下标也可以丨lj 来修改元素:
x[np.array([True, False, True, True])] = -1, -2, -3

arnay([-l, 4, - 2, -3, 1])

布尔数组一般不是手工产生,而是使用布尔运算的uftm c 函数产生,关 于 uflm c 函数请参照


下一节的介绍。下面我们举一个简单的例子说明布尔数组下标的用法:

x = np.random.randint(0, 10, 6) # 产生一个长度为6 ,元素值为0 到 9 的随机幣数数姐


x x > 5

[8 ,1, 5, 6, 2, 7] [ True, F a lse ,False, True, False, True]

表 达 式 x > 5 将 数 组 x 中的每个元素和5 进行大小比较,得到一个布尔数组,T rue 表 示 x


中对应的值大于5。我们可以使用 x > 5 所得到的布尔数组收集x 中所有大于5 的数值:

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快

a[0, 3:5] a[4:, 4:] a[:, 2] a[2::2, ::2]

[3, 4] [[44, 45], [ 2, 12, 22, 32, 42, 52] [[20, 22, 24],
—速 处 理 数 据

[54, 55]] [40, 42, 44]]

如 果 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:]相 同 。

idx = slice(None, None, 2), slice(2,None)

a[idx] a[idx][idx]

[[2, -3, 4, 5], [[ 4, 5],

[22, 23, 24, 25], [44, 45]]


[42, 43, 44, 45]]

切片(slice)对象
根 据 Python 的语法,在 u 中可以使用以冒号隔开的两个或三个整数表示切片,但是单独生
成切片对象时需要使用 slice()来创建。它有三个参数,分别为开始值、结束值和间隔步长,当
这些值需要省略时可以使用1^01^。例如,3[51^(1^011^,
1^0116,1^01^),
2』和3[:,
2]相同。

JIJ Python 的内置函数 sliceO创建下标比较麻烦,因此 Num Py 提供了一个5_对象来帮助我们


创建数组下标,请 注 意 ^实 际 上 是 IndexExpression类的一个对象:

np.s 」:
:2_» 2:]

(slice(None) None, 2), slice(2, None, None))

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]]中,第 0 轴的下标是一个切片对象,它选取第3 行之后的所有行;第 1轴的


下标是整数列表,它选収第0、第 2 和 第 5 列。

a[3:, [0,2,5]]

array([[30, 32, 35],


[40, 42, 45],

[50, 52, 55]])

在 a[mask,2]中,第 0 轴的下标是一个布尔数组,它选取第0、第 2 和 第 5 行;第 1轴的下


标是一个整数,它选収第2 列。

mask = n p . a r r a y ([1,0 ,
1,0,0)1], dtype=np.bool)
a[mask, 2]

array([ 2, 22, 52])

注意,如 果 m ask 不是布尔数组而是整数数组、列表或元组,就按照以整数数组作为下标


的方式进行运算:
Numpy快

maskl = n p . a r r a y ([1,0 儿 0,0,1])


—速 处 理 数 据

mask2 = [True,False^True,False,False^True]

a[maskl_» 2] a[mask2_» 2]

[12, 2, 12, 2, 2, 12] [12, 2, 12, 2, 2, 12]

当下标的长度小于数组的维数时,剩余的各轴所对应的下标是“:”,即选取它们的所有
数据:

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]

array([[[ 0, 1, 2, -3, 4, 5],


[10, 11, 12, 13, 14, 15]],

[[20, 21, 22, 23, 24, 25],


[30, 31, 32, 33, 34, 35]]])

可以使用这种以整数数组作为下标的方式快速替换数组中的毎个元素,例如有一个表示索
引阁像的数组 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

dty p e ( { ,names •: name • / a g e . / weight • ] , _ formats ■: S30 • / <i4 • / < f 4 .],


—速 处 理 数 据

•offsets':[0,32,36], 'itemsize’ :
40}, align=True)

还可以用包含多个元纟11的列表来描述结构的类型:

d t y p e ( [ ( • n ame 、 •|S30'), ( ' a g e 、 '<i4'), ( ' w e i g h t 、 '<f4')])

其中形如“(字段名,
类型描述)”的元组描述了结构中的每个字段。类型字符串前面的丫、'<’

V 等字符表示字段值的字节顺序:
• h 忽视字节顺序。
• < : 低位字节在前,即小端模式(little endian)。
• > : ? 这位字节在前, 大端模式(big endian)。
结构数组的荐取方式和一般数组相同,通过下标能够取得其中的元素,注怠元素的值看上
去像是元组,实际上是结构:

print a [0]

a [ 0 ] .dtype

('Zhang', 32, 75.5)


dtype({■names•:[•n a m e a g e w e i g h t •], 1formats•:[_S 3 0 < i 4 < f 4_],
’o f f s e t s ' :[0,32,36], 'itemsize':40}, align=True)

我们可以使用字段名作为下标获取对应的字段值:

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

通 过 a.tostring()或 a.tofile()方法,可以将数组 a 以二进制的方式转换成字符$或写入文件:

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;

};

struct person p[3];

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

printf("%s %d %f\n">, p[i].name., p[i].age., p[i].weight);

在 IPython 中可以通过!执行系统命令,下而调用 g c c 编译前而的 C 语言程序并执行:

!gcc rea d _ s t r uc t _ a r r a y .c -o read_st r uc t _ a r r a y .exe

!read 一struct 一a r r a y •exe

Zhang 4 0 75.500000
Li 24 65.199997

内存对齐
为了内存寻址方便,C 语言的结构类型会自动添加一些填充用的字节,这叫做内存对齐。
例如上面 C 语言中定义的结构的name 字段虽然是30个字节长,
但是由于内存对齐问题,
在 name
和 a g e 中间会填补两个字节。因此,如果数组中所配置的内存大小不符合C 语言的对齐规范,
Numpy快

将会出现数据错位。为了解决这个问题,在创建 dtype 对象时,可以传递参数 align=True ,这样


结构数组的内存对齐就和C 语言的结构类型一致了。在前面的例子中,由于创建 persontype时
—速 处 理 数 据

指 定 align 参数为 True , 因此它占用4 0 个字节。

结构类型中可以包括其他的结构类型,下面的语句创建一个有一个字段f l 的结构,f l 的值
是另一个结构,它有字段12,类 型 为 16位整数:

np.dtype([Cfr, [Cf2*, np.intie)])])

dtype([( ;
fr, [(*f2*, *0 2 ^)])])

当某个字段类型为数组时,用元组的第三个元素表示其形状。在下面的结构体中,f l 字段
是一个形状为(2,3)的双精度浮点数组:

n p . d t y p e ( [ ( ,f0*, 'i4*), ('fl*, 'f8', (2, 3))])

dtype([Cf0_ , ,
<i4,
),(' f l 1, ' f S 1, (2, 3))])

用下面的字典参数也可以定义结构类型,字典的键为结构的字段名,值为字段的类型描述。
但是由于字典的键是没有顺序的,冈此字段的顺序需要在类型描述中给出。类型描述楚一个元
组,它的第二个值给出字段的以字节为单位的偏移S ,例如下例中的 a g e 字段的偏移M 为 25个
字节:

n p . d t y p e ( { ' s u r n a m e ' :( 'S25', 0 ) , 'age':(np.uint8,25)})

dtype([('surname', 'S25'), ('age', 'ul')])

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

[[0., 2.], (24, 8)

[ 6。 8.]]

由于数组 b 和数组 a 共享数据存储区,而数组 b 中的第0 轴 和 第 1轴都是从 a 中隔一个元


素取一个,因此数组 b 的 strides变成了(24,8 ) , 正好都是数组 a 的两倍。对照前面的图2-3很容
易看出数据0 和 2 的地址相差8 个字节,而数据0 和 6 的地址相差2 4 个字节。
兀素在数据存储区屮的排列格式有两种:C 语 B 格式和 Fortran语目‘
格式。在 C 语 中 ,多
Python 科学计算(第2 版)

维数组的第0 轴是最上位的,
即第0 轴的下标增加1时,
元素的地址增加的字节数最多;而Fortran
语言中的多维数组的第0 轴是最下位的,即 第 0 轴的下标增加1 时,地址只增加一个元素的字
节数。在 N um Py 中默认以 C 语言格式存储数据,如果希望改为 Fortran格式,只需要在创建数
组时,设 置 order参数为MF ":

c = n p . a r r a y ([[0,1,2], [3,4,5], [6,7,8]]., dtype=np.float32, order="F")

c.strides

(4, 12)

了解了数组的内存结构,就可以解释使用下标収得数据时的复制和引用间题.•
• 当 下 标 使 W 整数和切片时,所取得的数据在数据存储区域中是等间隔分布的。因为只
需要修改图2-3所示的数据结构中的dim count、dimensions、stride等属性以及指向数据
存储区域的指针 data, 就能实现整数和切片 K 标,所以新数组和原始数组能够共享数据
存储区域。
* 当使用整数序列、整数数组和布尔数组时,不能保证所取得的数椐在数据存储区域中
Numpy快

是等间隔的,因此无法和原始数组共享数裾,只能对数据进行复制。
数 组 的 flags 属性描述了数据存储E 域的一些屈性,直接查看 flags 属性将输出各个标志的
—速 处 理 数 据

值 ,也可以单独获得其中的某个标志值:

print a.flags

print "c_contiguous:", a .f l a g s .c_contiguous

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

我们还可以通过 view 〇方法从同一块数据区创建不同的 d typ e 的数组对象,也就是使用不


同的数值类型查看同一段内存中的二进制数据:

a = np.array([[0, 1], [2, 3], [A, 5]], dtype=np.float32)

b = a.view(np.uint32)

c = a.view(np.uint8)

b c

[[ 0, 1065353216], [[ 0, 0, 0, 0, 0, 0, 128, 63],


[1073741824, 1077936128], [ 0, 0, 0, 64, 0, 0, 64, 64],
[1082130432, 1084227584]] [ 0, 0, 128, 64, 0, 0, 160, 64]]

由于数组 a 的元素类型是单精度浮点数,占用4 个字节,通 过 a.view (np.uint32),我们仓键


了一个新的数组,它和数组 a 使用同一段数据内存,但是它将每4 个字节的数据当作无符号32
位整数处理。而 a.View (np.Uint8)将每个字节都当作一个单字节的无符号整数,因此得到一个形
状为(3,8)的数组。通过\也〜〇方法获得的新数组勹原数组共享内存,当3丨0,〇1被修改时,13丨0,〇1
和 c [0, :4]都会改变:

a[0, 0] = 3.14

b[0, 0] c[0, :4]

1078523331 [195, 245, 72, 64]


Python 科学计算(第2 版)

下面我们看一个使用v i e w 〇方法的有趣的例子。在 《雷神之锤 m : 竞技场》的 C 语言源代


码中有这样一个神奇的计兑平方根倒数的函数Q_r^qrt〇。代码中使flj牛顿迭代法计箅平方根倒
数,这并没有任何神奇之处,但是其中包含了一个神奇的数字 0 x 5 f i 7 5 9 d f , 并将单糈度浮点数
当作 3 2 位的整数进行了一次令人毫无头绪的运算:

h ttp ://z h .w i k i p e d i a .o r g /w i k i /平方根倒数速算法

O 维基百科关于《雷神之锤》中使用0x 5f 3759d f 计算平方根倒数算法的解释。

float 〇
Lrsqrt( float number )
/
i
long i;
float y;

const float threehalfs = 1.5F;

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 实现同样的计算:

number = np.linspace(0.1, 10, 100)


y = number.astype(np.float32) O
x2 = y * 0.5
i = y.view(np.int32) 0
i[:] = 0x5f3759df - (i » 1) ©

y = y * (1.5 - x2 * y * y) O

np.max(np.abs(l / np.sqrt(number) - y)) ©

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

[0, 1, 2, 3, 4, 5] [[0, 1, 2],

[1, 2, 3],
[2, 3, 4],
[3, 4, 5]]

这个例子中,我们从 NumPy的辅助模块中载入了 aS_strided〇蚋数,并使用它从一个长度为


6 的一维数组 a 创建了一个 shape为(4, 3)的二维数组 b 。由于通过 strWes参数直接指定了数组 b
的 strides属性,冈此不仅数组 b 和数组 a 共享数据区,而 且 b 中的前后两行有两个元素萣重合
的。例如下而修改 a[2]的值,b 中的前三行中对应的元素也发生改变:

a[2] = 20

array([[ 0, 1, 20],

[1, 20, 3],


[20, 3, 4],
[3, 4, 5]])

在对数据进行处理时,可能经常耑要对数据进行分块处理,而且为了保持平滑,每块数据
之间需要有一定的重叠部分。这时可以使用上面介绍的方法对数据进行带重叠的分块。需要注
Python 科学计算(第2 版)

意的是,使 用 as_ strided〇时 N um Py 不会进行内存越界检查,因 此 shape 和 strides设置不当可能


会发生意想不到的错误。

2.2 ufunc 函数

与本节内容对应的 Notebook 为:02-numpy/numpy-200-ufunc.ipynb<

uflinc 是 universal function 的缩写,它足一利儀对数组的每个元素进行运算的函数。NumPy

内置的许多 u fu n c 函数都是用 C 语言实现的,因此它们的计算速度非常快。让我们先看一个


例子:

x = np.linspace(0j 2*np.pi, 10)

y = np.sin(x)
Numpy快

y
array([ 0.00000000e+00, 6.42787610e-01, 9.84807753e-01,
—速 处 理 数 据

8.66025404e-01, 3.42020143e-01, -3.42020143e-01,

-8.66025404e-01, -9.84807753e-01, -6.42787610e-01,

-2.44929360e-16])

先用丨impaceO产生一个从0 到2 tt的等差数组,
然后将其传递给叩.如〇函数计算每个元素的
正弦值。由于 np.Sin()是一个 iiflinc 函数,因此在其内部对数组x 的每个元素进行循环,分别计
算它们的正弦值,并返回一个保荐各个计算结果的数组。运算之后数组 x 中的值并没有改变,
而是新创建了一个数组来保存结果。也可以通过 o u t 参数指定保存计算结果的数组。冈此如果
希望直接在数组 x 中保存结果,可以将它传递给 out 参数:

True

u llm c 函数的返回值仍然是计算的结果,只不过它就是数组 x 。下而比较叩.如〇和Python

标准庵的 math.sin〇的计算速度:

import math

x = [i * 0.001 for i in xrange(1000000)]

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: 302 ms

—速 处 理 数 据
Wall time: 30 ms

Wall time: 1.28 s

可以看出,np.sin〇比 math.sin〇快 10倍多,这得益于 np.sin〇在 C 语言级别的循环计算。

列表推导式比循环更快
事实上,标 准 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

通过下标运算获収的数组元素的类型为N um Py 中定义的类型,将其转换为 Python 的标准


类型还需要花费额外的时间。为了解决这个问题,数组提供了 itemO方法,它用来获取数组中
的单个元素,并且直接返冋标准的Python 数值类型:
Python 科学计算(第2 版)

a = np.arange(6.0).reshape(2, 3)

print a.item(l, 2 ),type(a.item(l, 2)), type(a[l, 2])

5.0 <type 'float'x t y p e 'numpy.float64 >

通过上而的例子我们了解了如何最有效率地使用math 模块和 N um Py 中的数学函数。由于


它们各有优缺点,因此在导入时+建议使用 import*全部载入,而足应该使用 importnumpyasnp
载入,这样可以根据需要选择合适的函数。

2 . 2 . 1 四则运算

N um Py 提供了许多 uftinc 函数,例如计算两个数组之和的add〇函数:

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函数

表达式 对应的 ufunc函数


y = xl +x2 add(xl,x21,y])
y = xl -x2 sublitict(xl, x2 [, y])
y = xl * x2 multiply(xl, x2 [, y])
y = xl /x2 divide(xl,
x2 [,
y]),
如果两个数组的元素为整数,那么爪整数除法
y = xl /x2 true_divide(xl,
x2 [,
y]),总是返回精确的商
y = xl //x2 floor_dividc(xl,
x2 [,
y]),总是对返回值取盤
y=—
x ncgative(x [,y])
y = xl**x2 poweKxl,x2 [, y])
y = xl %x2 remainder(xl, x2 [, y]), mod(xl, x2, [, y])
5
8
数组对象支持操作符,极大地简化了兑式的编写,不过要注意如果兑式很复杂,并且要运
算的数组很大,将会因为产生大量的中间结果而降低程序的运兑速度。例如,假 设 对 a 、b 、 c
三个数组采川算式 x = a*b+c 加以计算,那么它相当于:

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

array([ True, False, Fals e ] , dtype=bool)

每个比较运算符也与一个u f u n c 函数对应,表 2 - 2 是比较运算符与 u f u n c 闲数的对照表。

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

由 于 Python 中的布尔运兑使用 and、o r 和 not 等关键字,它们无法被重载,因此数组的布


尔运算只能通过相应的uflinc 函数进行。这些函数名都以 logical_幵头,在 IPython 中使j|j 自动补
全可以很容易地找到它们:

>>> np.logical # 按 T a b 键进行自动补全


np.logical 一and np.logical 一not np.logical_or np.logical 一
xor

下面是一个使用 l〇
gical_〇
r()进 行 “或运算”的例子:
5
9
Python 科学计算(第2 版)

a = np.arange(5)
b = np.anange(4., ~1, -1)

print a == b
print a > b

print np.logical_or(a == b, a > b) # 和 a>=b 相同


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

对两个布尔数组使用and、o r 和 not 等进行布尔运算,将抛出 ValueError异常。因为布尔数


组中有 T n ie 也 有 False, 所 以 N um Py 无法确定用户的运算R 的:

a == b and a > b

ValueError Traceback (most recent call last)


<ipython-input-13-99b8118687f0> in 〈
module〉
()
----> 1 a == b and a > b
NumPy快

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 。

np.any(a == b) np.any(a == b) and np.any(a > b)

True True

以 b it w is e jf 头的函数楚位运兑函数,包括 bitwise_and、bitwise_not、bitwise_or 和 bitwise_xor


等。也可以使用& 、〜 、丨和A等操作符进行计算。
对于布尔数组来说,位运算和布尔运算的结果相同。但在使用时要注意,位运算符的优先
级比比较运算符高,因此需要使用括号提高比较运算符的运算优先级。例如:

(a == b) | (a > b)

array([False, False, True, True, True], dtype=bool)

整数数组的位运算和C 语言的位运算相同,在使用时要注意元素类型的符号,例如下面的
amngeO所创建的数组的元素类型为32位符号整数,因此对正数按位取反将得到负数。以整数0
为例,按位取反的结果是OxFFWFFFF , 在 32位符号整数中,这个值表示-1。

〜 np.arange(5)

array([-l, -2, -3, -4, -5])

而如果对8 位无符号整数数组进行按位収反运兑:
^ np.arange(5., dtype=np.uint8)

array([255_» 254, 253) 252, 251], dtype=uint8)

同样的整数0 , 按位収反的结果是 OxFF,当它是8 位无符号整数时,它的值是255。

2 . 2 . 3 自定义 ufunc 函数

通 过 N um Py 提供的标准 uflinc 函数,可以组合出复杂的表达式,在 C 语言级別对数组的每


个元素进行计算。
但有吋这种表达式不易编写,
而对每个元素进行计算的程序却很容易用Python
实现,这时可以用 frompyftincO将计算单个元素的函数转换成uflinc 函数,这样就可以方便地坪J
所产生的 ullm c 函数对数组进行计算了。
例如,我们可以用一个分段函数描述三角波,三角波的形状如阁2-5所示,它分为三段:
上升段、下降段和平坦段。

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

elif x < c0: r = x / c0 * he


else: r = (c-x) / (c-c0) * he
return r

我们可以用下面的程序,先使用列表推导式计算出一个列表,然后用 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()的调用格式为:

frompyfunc(func, nin, nout)

其中:flmc是计算单个元素的函数,nin 是 lime 的输入参数的个数,nout是 flmc 的返冋值


的个数。下而的程序使用 fix)mpyfunc〇将 triangle_wave〇转换为 ufunc函数对象 triangle_ ufuncl:

triangle 一ufunci = np.frompyfunc(triangle 一wave, 4, 1)

y2 = triangle— ufunci(x, 0 . 6 ,0 . 4 ,1.0)

值得注怠的是,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快

使 用 vectorizeO也可以实现和 frompyfiincO类似的功能,但它可以通过 otypes参数指定返回


的数组的元素类型。otypes参数可以是一个表示元素类型的字符串,也可以是一个类型列表,
—速 处 理 数 据

使用列表可以描述多个返丨Hi数组的元素类型。下面的程序使用vectorizeO计算三角波:

triangle_ufunc2 = np.vectorize(triangle_wave_» otypes= [n p .f l o a t ])


y3 = triangle_ufunc2(x, 0.6, 0.4, 1.0)

最后我们验证一下结果:

np.all(yl == y2) np.all(y2 == y3)

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)

[10, 11, 12, 13, 14],

[20, 21, 22, 23, 24],

[30, 31, 32, 33, 34],

[40, 41, 42, 43, 44],

[50, 51, 52, 53, 54]]

由于 a 和 b 的维数不同,根据规则1),需要让 b 的 shape 属性向 a 对齐,于是在 b 的 shape


属性前加1 , 补齐为(1,
5)。相当于做了如下计算:

b.shape = 1 , 5

b b.shape

[[0, b 2, 3, 4]] (1, 5)

这样,加法运算的两个输入数组的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

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


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

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

这里的 repeatO方法沿着 axis 参数指定的轴复制数组中各个元素的值。由于 a 的 第 1轴的长


度 为 1 , 而 b 的 第 1 轴的长度为 5 , 为了让它们在第 1 轴上能够相加,需要将 a 的 第 1 轴的长度
扩展为 5 , 这相当于:

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

经过上述处理之后,3 和 b 就可以按对应元素进行相加运算了。当然,在执行 a + b 运算时,


NumPy 内部并不会真正将长度为1 的轴用 repeat()进行扩展,这样太浪费内存空间了。由于这种
广播计算很常用,因此 N u m P y 提供了 ogrid 对象,用于创建广播运算用的数组。

Xj y = np.ognid[:5, :5]

x y

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

[ 1],
[ 2 ],
[3] ,
[4] ]

此外,N um Py 还提供了 m grid 对象,它的)lj 法 和 ogrid 对象类似,但楚它所返回的楚进行


广播之后的数组:

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

[[0. ], [[ 0. , 0.5, 1.]]

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)

图 2-6为使用 ogrid 计兑的三维丨⑴面。

1.0 以 〜 -1*5
15 2.0

图2 4 使用ogrid计算二元函数的曲面
P y th o n 科学计算(第2 版)

为了充分利用 uftm c 函数的广播功能,我们经常需要调整数组的形状,因此数组支持特殊


的下标对象 None , 它表示在 N one 对应的位置创建一个长度为1 的新轴,例如对于一维数组 a ,
alNone,
:]和 a.neshape(l ,
-1)等效,而 a [:,
None]和 a.reshape(-1,1)等效:

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],

[8, 9, 12, 18]])

还可以使用 ix_〇将两个一维数组转换成可广播的二维数组:

gy, gx = np •ix 」y , x )

gx gy gx + gy

[[0, 1, 4, 10]] [[2], [[2, 3, 6, 12],

[3], [3, 、 7, 13],

[8]] [ 8, 9, 12, 18]]

在上面的例子中,通 过 ix_〇将数组 x 和 y 转换成能进行广播运算的二维数组。注意数组 y


对应广播运算结果中的第0 轴 ,而数组 x 与 第 1轴对应。ix_()的参数可以足 N 个一维数组,它
将这些数组转换成 N 维空间中可广播的 N 维数组。

2.2.5 ufunc 白勺方5去

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

r2 = n p . a d d . r ed u c e ( [ [1, 2, 3]^ [4, 5, 6]]., axis=l) # (1+2+3).,(4+5+6)

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

[1, 3, 6] [[1, 3, 6],


[4, 9, 15]]

reduceat()方法计算多纽 reduce()的结果,通 过 indices 参数指定一系列的起始和终止位置。

Numpy快
它的计算有些特别,让我们通过例子详细解释一下:

—速 处 理 数 据
a = np.ar r a y ([ l ,2, 3 ,4])

result = np.add.reduceat(a, indices=[0j 1, 0, 2, 0, 3, 0])

result

array([ 1, 2, 3, 3, 6y 4, 10])

对 于 indices参数中的每个元素都会计算出一个值,因此最终的计算结果和indices参数的长
度相同。结果数组 result中除最后一个元素之外,都按照如下计算得出:

if indices[i] < i n d i c e s [ i + 1 ] :

result[i] = < o p > . r e d uc e ( a [ i n d i c e s [ i ] :indices[i+1]])


else:
result[i] = a[indices[i]]

而最后一个元素如下计算:

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

可以卷出 result[::2]和 a 相等,而 result[l ::2]和 np.add.accumulate(a)相等。


uflinc 函数对象的 outerO方法等同于如下程序:

a.shape += (1^)*b.ndim

<op>(a,b)

a = a.squeeze()

其 中 squeeze()方法剔除数组 a 中长度为1 的轴。让我们看一个例子:

np.multiply,.outen([l, 2, 3, 4, 5], [2, 3, 4])

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]

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


[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]]],

NumPy快
[[[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29],

—速 处 理 数 据
[30, 31, 32, 33, 34],
[35, 36, 37, 38, 39]]]]

这是因为 Num Py 将列表 lidx转换成了([0],


[1]),而将数组 aidx转换成了(aidx,
:,:):

a[tuple(lidx)] a[aidx, ,:]

[[5, 6, 7, 8, 9]] [[[[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, 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快

10 = np.arnay([[l, 2 } 1], [0, 1, 0]])


—速 处 理 数 据

11 = np.array([[[0]], [[1]]])
12 = np.array([[[2, 3, 2]]])
b = a[i0, il) i2]
b
array([[[22, 43, 22],
[2, 23, 2]],

[[27, 48, 27],


[7, 28, 7]]])

首先,i0、i l 、i2 三个整数数组的 shape 属性分别为(2,3)、(


2,1,1)、(
1,1,
3)。根据广播规则,
先在长度不足3 的 shape 属性前面补1 , 使得它们的维数相同,广播之后的 shape 属性为各个轴
的最大值:

(1 , 2 , 3)
(2, 1, 1)
(1 , 1, 3)

2 2 3

即三个整数数组广播之后的 shape 属性为(2,2,3 ) , 这也就是下标运算所得到的结果数组的


维数:

b.shape
(2 , 2, 3)
我们可以使用 broadcast_airays()查看广播之后的数组:

ind0, indl, ind2 = np.broadcast_arrays(i0J il, i2)

ind0 indl ind2

[[[1, 2, 1], 0, 0], [[[2, 3, 2],

[0, 0]], [0, 0, 0]], [2, 3, 2]],

[[1, 2, 1], [[1, 1, 1], [[2, 3, 2],

[0, 1, 0]]] [1, 1, 1]]] [2, 3, 2]]]

对 于 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 = a[l:3, i0, il]

c.shape

(2, 2, 2, 3)

其中,c 的 shape 属性中的第一个2 是切片“1 3 ”的长度,后面的(2,2,3)则 是 iO 和 i l 广播之


后的数组的形状:

ind0, indl = n p .broa d c a s t_ a r r a y s (i0^ il)

ind0.shape

print c [ : ,i ,]•,k]

print a[l:3, ind0[i, j, k], indl[i, j, k]] # 和 c [ : , i , j , k ]的值相同


P y th o n 科学计算(第 2 版)

[21 41]

[21 41]

当下标元组屮的整数数组不连续时,结果数组的 shape 屈性为整数数组广播之后的形状后


面添加上切片元素所对应的形状。例 如 a [iO,
:,
i l ]的 shape 属性为(2,2,3,4),其中(2,2,3)是丨0和丨1广
播之后的形状,而 4 是数组 a 的 第 1轴的长度:

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]]

[ 1 , 6 , 11, 16] [b 6, 11, 16]


NumPy快

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 = n p . random.randint(0J K - size=(I_j J))

然后用数组 id x 创建第2 轴的下标数组 idx_k , 它是一个形状为(I,J,


L )的三维数组。它的第2
轴上的每一层的值都等于id x 数组加上层的高度,S卩idx_k[:,:,ij = idx[:,:]+ i :

idx_k = i d x [ :j None] + np.arange(3)

idx 一k.shape

(6, 7, 3)

然后分別创建第0 轴 和 第 1轴的下标数组,它们的 shape 分別为(I,


U )和(1J ,
1):

idx 一 idx」 、 _ = np.ogrid[:I, :K]

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〇函数。

bl = np.array([True, False, True, False])

np.nonzero(bl)

(anray([0, 2]),)

对于二维数组 b2,nonzero(a)所得到的是一个长度为2 的元组。它的第0 个元素是数组 a 中


P y th o n 科学计算(第2 版)

值不为0 的元素的第0 轴的下标,


第 1个元素则是第1轴的下标,
因此从下面的结果可知b2[0,0]、
b2[0,2]和 b2[l ,
0]的值不为 0:

b2 = np.array([[True, F a l s e ,True], [True, False, False]])


np.nonzero(b2)

(array([0, 0, 1]), anray([0, 2 } 0]))

当布尔数组直接作为下标吋,相当于使用由 nonzeroO转换之后的元组作为下标对象:

a = np.arange(3 * 4 * 5 ) .reshape(3, A, 5)

a[b2] a[np.nonzeno(b2)]

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

[10, 11, 12, 13, 14],[10, 11, 12, 13, 14],


[20, 21, 22, 23, 24]][20, 21, 22, 23, 24]]

当下标对象是元组,并且其中有布尔数组时,相当于将布尔数组展开为由 n〇
nzeros()转换
NumPy快

之后的各个整数数组:
—速 处 理 数 据

a[l:3, b2] a[l:3, np.nonz e ro ( b 2 ) [ 0 ],np.nonzero(b2)[l]]

[[20, 22, 25], [[20, 22, 25],

[40, 42, 45]] [40, 42, 45]]

2 . 4 庞大的函数库

与本节内容对应的 Notebook 为:(


E -numpy/numpy^ OO-functions.ipynbc
DVD

除了前面介绍的 ndarmy数组对象和 uflinc 函数之外,N um Py 还提供了大量对数组进行处理


的函数。充分利用这些函数,能够简化程序的逻辑,提高运算速度。本节通过一些较常州的例
子,说明它们的一些使用技巧和注意事项。

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 的随机数,它的第三个参数用于指定数组的形状:

from numpy import random as nr


np.set_printoptions(precision=2) # 为了节街篇幅,只显示小数点后两位数字

NumPy
rl = nr.rand(4, 3)
r2 = nr.randn(4, 3)

丨快速处理数据
n3 = nr.randint(0, 10, (4, 3))

rl r2 r3

[[0.87, 0.42, 0.34], [[-1.32, -0.03, -0.05], [[5, 9, 1],


[0.25, 0.87, 0.42], [0.34, -0.42, -0.41], [2, 9, 8],
[0.49, 0.18, 0.44], [0.59, -0.49, -0.01], [2, 6, 6 ],
[0.53, 0.23, 0.81]] [-1.92, -0.13, -1.34]] [3, 8, 1]]

random模块提供了许多产生符合特定随机分布的随机数的函数,它们的最后一个参数 size
都用于指定输出数组的形状,而其他参数都是分布函数的参数。例如:
• nomialO: 正态分布,前两个参数分别为期望值和标准差。
• imiformO: 均匀分布,前两个参数分别为区间的起始值和终值。
• poisson〇 : 泊松分布,第一个参数指定 A 系数,它表示单位时间(或单位而积)内随机事
件的平均发生率。由于泊松分布是一个离散分布,因此它输出的数组是一个整数数组。

rl = nr.norma 1(100., 10, (A, 3))

r2 = nr.uniform(10, 20, (4 ,3))


r3 = n r . p o i sson(2.0J (4, 3))

rl r2 r3

[[102.89, 103.56, 111.46],[[ 19. , 18.69, 14.38], [[3, 1, S],


[83.54, 122.36, 98.31],[ 17.97, 10.16, 12.47], [2, 2, 3],

[87.95, 106.89, 99.28], [ 19.36, 10.91, 19.65], [2, 4, 4],


[92.66, 103.13, 106.28]] [ 16.79, 16.46, 16.32]] [2, 2, 3]]
7
5
P y th o n 科学计算(第 2 版)

permutationO可以用于产生一个乱序数组,当参数为整数 n 时,它返 M [0,


n)这 n 个整数的随

机排列;当参数为一个序列吋,它返冋一个随机排列之后的序列:

a = n p . a r r a y ([1, 10, 20, 30, 40])

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)

a r r a y ([ 1, 20, 30, 10, 40])

choice〇从指定的样木中随机进行抽取:
• size 参数用于指定输出数组的形状。
Numpy快

• replace 参数为 T ru e 时,
进行可重复杣取,而 为 False 吋进行不重S 杣取,
默认值为 True 。
所以在下面的例子中,c l 中可能有重复数值,而 c 2 中的每个数值都是不N 的。
—速 处 理 数 据

• p 参数指定每个元素对应的抽収概率,如果不指定,所有的元素被杣収到的概率相同。
在下面的例子中,
值越大的元素被抽到的概率越大,
因此 c 3 中数值较大的元素比较多。

a = np.arange(10, IS, dtype=float)

cl = nr.choice(a_» size=(4, 3))

c2 = nr.ch o i c e( a J size=(4, 3 ) y replace=False)

c3 = nn.choice(a, size=(4, 3), p=a / np.sum(a))

cl c2 c3

[[ 12., 22., 17.], [[10., 14., 23.], [[ 21., 24., 2 3 :

[ 24., 13., 14.], [24., 13., 19.], [ 19., 18., 1 9 :

[ 19., 23., 23.], [11。 22., 20.], [ 24., 21., 22.],

[ 17., 19., 22.]] [15。 17., 18.]] [ 22., 21., 21.]]

为了保证每次运行时能重现相同的随机数,可 以 通 过 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 ( ) 汁算数组元素之和,也可以对列表、元组等与数组类似的序列进行求和。当数组是多

维时,它计算数组中所有元素的和。这里我们使用 random.randint 〇模块中的函数创建一个随机

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 参数,则求和运算沿着指定的轴进行。在上面的例子中,数 组 a 的第0 轴的


长度为4 ,第 1轴的长度为5。如 果 axis 参 数 为 1,则对每行上的5 个数求和,所得的结果是长
度 为 4 的一维数组。如果参数 a x is 为 0 , 则对每列上的4 个数求和,结果是长度为5 的一维数
组。即结果数组的形状是原始数组的形状除去其第axis 个元素:

np.sum(a, axis=l) np.sum(a, axis=0)

[26, 28, 24, 18] [22, 13, 27, 18, 16]

当 axis 参数是一个轴的序列时,对指定的所有轴进行求和运算。例如下面的程序对一个形
状为(2,3,4)的三维数组的第0 和 第 2 轴求和,得到的结果为一个形状为(3,
)的数组。由于数组的
所有元素都为1 , 因此求和的结果都是8:

np.sum(np.ones((2, 3, 4)), axis=(0, 2))

array([ 8 。 8 。 8.])

77
P y th o n 科学计算(第2 版)

有时我们希望能够保持原数组的维数,这时可以设置 keepdims参数为 True:

np.sum(a, 1, keepdims=True) np.sum(a, 0, keepdims=True)

[[26], [[22, 13, 27, 18, 16]]

[28],

[24],
[18]]

SUm〇默认使用和数组的元素类型相同的累加变M 进行计算,如果元素类型为整数,则使用
系 统 的 默 认 整 数 类 型 作 为 累 加 变 例 如 在 32位系统中使用32位整数作为累加变量。因此对
整数数组进行累加时可能会出现溢出问题,即数组元素的总和超过了累加变量的取值范围。下
面的程序计算数组 a 中每个元素占其所在行总和的百分比。在调用 sum()函数时:
• 设 置 dtype 参数为 float, 这样得到的结果是浮点数组,能避免整数的整除运算。
• 设 置 keepdims参数为 Tm e , 这 样 sum〇得到的结果的形状为(4,
1),能够和原始数组进行
广播运算。
Numpy快

pa = a / np. s u m ( a J 1, dtype=float_> keepdims=True) * 100


—速 处 理 数 据

pa pa . s u m ( l J keepdims=True)

[[23.08, 11.54, 26.92, 15.38, 23.08],[[ 100.],

[32.14, 7.14, 21.43, 25. , 14.29],[ 100.],


[12.5 , 29.17, 29.17, 8.33, 20.83],[ 100.],

[22.22, 5.56, 38.89, 27.78, 5.56]][ 100.]]

对很大的单精度浮点数类型的数组进行计算时,也可能出现精度不够的现象,这时也可以
通 过 dtype 参数指定累加变f i 的类型。在下而的例子中,我们对一个元素都为1.1的单精度数组
进行求和,比较单精度累加变量和双精度累加变量的计算结果:

np.setj3nintoptions(precision=8)
b = np.full(1000000, 1.1, dtype=np.float32) # 创建一个很大的单精度浮点数数组
b # 1 . 1 无法使用浮点数精确表示,存在一些误差

a r r a y ([ 1.10000002, 1.10000002, 1.10000002, 1.10000002,

1 •10000002, 1 •10000002], dtype=float32)

使用单精度累加变M 进行累加计算,误差将越来越人,而使用双精度浮点数则能够得到较
精确的结果:

np.sum(b) np.sum(b, dtype=np.double)

1099999.3 1100000.0238418579
上面的例子将产生一•个新的数组来保存求和的结果,如果希望将结果直接保存到另一个
数组中,可 以 和 uftinc 函数一•样使用o u t 参数指定输出数组,它的形状必须和结果数组的形状
相同。
mean〇求数组的平均值,它的参数与 sum()相同。和 sum〇不同的是:对于整数数组它使JIJ

双精度浮点数进行计算,而对于其他类型的数组,则使用和数组元素类型相同的累加变董进行
计算:

np.mean(a, axis=l) # 整数数组使用双精度浮点数进行计算


array([ 5.2) 5.6_» 4.8_» 3.6])

np.mean(b) np.mean(b, dtype=np.double)

1.0999993 1.1000000238418579

此外,average()也 n丨以对数纟J1进行平均计算。它没有 out 和 dtype 参数,但有一个指定每个


元素权值的 w eights 参数,可以用于计算加权平均数。例如有三个班级,number 数组中保存每
个班级的人数,score 数组中保存每个班级的平均分,下面计算所有班级的加权平均分,得到整
个年级的平均分:

score = n p . a r r a y ([83, 72, 79])

number = n p . a r r a y ([20, 1 5 ,30])

print np.average(score, weights=number)

78.6153846154

相当于进行如下计算:

print np.sum(score * number) / np.sum(number, dtype=float)

78.6153846154

std()和 var〇分别计算数组的标准差和方差,有 axis 、out、dtype 以及 keepdims 等参数。方差


有两种定义:偏样本方差(biased sample variance)和无偏样本方差(unbiased sample variance)。偏样
本方差的计算公式为:
n

而无偏样木方差的公式为:

S2 > (Yi - y )2

当 ddof 参数为0 时,计算偏样本方差;当 d d o f 为 1 时,计算无偏样本方差,默认值为0。


P y th o n 科学计算(第2 版)

下面我们用程序演示这两种方差的差別。
首先产生-•个标准差为2.0、方差为4.0的正态分布的随机数组。我们可以认为总体样本的
方差为4.0。假设从总体样本中随机抽取10个样本,我们分別计算这10个样本的两种方差,这
里我们用一个二维数组重复上述操作1〇_次,然后计算所有这些方差的期望值:

a = n r . n o r m a l (0, 2.0, (100000, 10))

vl = np.var(a, axis=l, ddof=0) #可以省略 ddof=0


v2 = np. v a r ( a J axis=l_» ddof=l)

np.mean(vl) np.mean(v2)

3.6008566906846693 4.0009518785385216

可以看到无偏样本方差的期望值接近于总体方差4.0,而偏样本方差比4.0小一些。
偏样本方差是正态分布随机变量的最大似然估计。如果有一个样本包含 n 个随机数,并且
知道它们符合正态分布,通过该样本可以估兑出正态分布的概率密度函数的参数。所估兑的那
组正态分布参数最符合给定的样本,就称为最大似然估计。
Numpy快

正态分布的概率密度函数的定义如下,其中^表示期望,CT2表示方差:
1 (x- n)2
—速 处 理 数 据

f(x I a2) e 2a2


V2Tia2
所谓最大似然佔计,就是找到一组参数使得下面的乘积最大,其中\为样本中的值:
f(Xl)f(X2)".f(Xn)

专业术语总是很难理解,下而我们还是用程序来验证:

def normal_pdf(mean, war, x):


return 1 / np.sqrt(2 * np.pi * var) * np.exp(-(x - mean) ** 2 / (2 * var))

n r . s e e d (42)

data = n r . n o r m a l (0, 2.0, size=10) O


mean, var = np.mean(data), np.var(data) ©
var— range = np.linspace(max(var - 4) 0 . 1 ),var + 4 ,100) €)

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

normal_pdf〇为计兑正态分布的概率密度的函数。〇产 生 10个正态分布的随机数。© 计算其


最大似然估计的参数。©以最大似然估计的方差为中心,产生一组方差值。〇用正态分布的概
率密度函数计兑每个样本、每个方差所对应的概率密度。由于使用了广播运算,得到的结果 p
是一个二维数组,它的第0 轴对应 var_m nge 中的各个方差,第 1轴对应 data 中的每个元素。©
沿 着 p 的 第 1轴求所有概率密度的乘积。productO和 sum〇的用法类似,用于计算数组所有元素
的乘积。
下 面 绘 制 var_mnge 中各个方差对应的似然估计值,并 用 -•条竖线表示偏样本方差。由图
2-8可以看到偏样本方差位于似然估计曲线的最大值处。

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 大小与排序

与本节内容对应的 Notebook 为:02-numpy/numpy~410-functions-sort.ipynb。


DVD

木节介绍的函数如表2-5所示。

表 2-5本节要介绍的函数

函数名 功能 函数名 功能
min 最小值 max 最大值
minimum 二元最小值 maximum 二元最大值

Ptp 最大值与域小值的差 argmin 最小值的下标


argmax 敁大值的下标 uniuveljndex 一维下标转换成多维下标
sort 数组排序 argsort 计箅数组排序的下标
lexsort 多列排序 partition 快速计算前k 位
argpartition 前 k 位的下标 median 中位数
percentile 百分位数 searchsorted 二分查找

用 min〇和 max()可以汁算数组的最小值和最大值,它们都有 axis 、out、 keepdims等参数。


Python 科学计算(第2 版)

这些参数的用法和 sum〇莶本相同,但 是 axis 参数不支持序列。此外,ptp()计兑最大值和最小值


之间的差,有 axis 和 out 参数。这里就不再多举例了,请读者自行查看函数的文档。 minimumO
和 maximum ()j |j 于比较两个数组对应下标的元素,返回数组的形状为两参数数组广播之后的
形状。

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

用 argmax()和 argmin()可以求最大值和i S 小值的下标。如果+ 指 纪 axis 参数,则返回平坦化


之后的数组下标,例如下面的程序找到 a 中最大值的下标,有多个最值时得到第一个最值的
下标:
Numpy快

np.random.seed(42)
a = np.random.randint(0j 10j size=(4, 5))
—速 处 理 数 据

maxjDos = np.argmax(a)
maxjDos

下而查看 a 平坦化之后的数组中下标为max_p o s 的元素:

a.ravel()[max_pos] np.max(a)

9 9

可 以 通 过 unravel_index()将一维数组的下标转换为多维数组的下标,它的第一个参数为一
维数组的下标,第二个参数是多维数组的形状:

idx = np.unravel 一index(max_pos, a.shape)

idx a[idx]

(1, 0) 9

当使用 axis 参数时,可以沿着指定轴计算最大值的下标。例如下而的结果表示,在 数 组 a


中第0 行的最大值的下标为2 , 第 1行的最大值的下标为0:

idx = np.argmax(a, axis=l)

idx

array([2, 0, 1, 2])
使用下面的语句可以通过id x 选出每行的最大值:

a[np.arange(a.shape[0])^ idx]
array([7, 9, 7, 7])

数组的 sort〇方法对数组进行排序,它 会 改 变 数 组 的 内 备 而 sort〇函数则返冋一个新数组,


不改变原始数组。它 们 的 axis 默认值都为- 1 , 即沿着数组的最终轴进行排序。sort〇函数的 axis
参数可以设置为 None ,此时它将得到平识化之后进行排序的新数组。在下面的例子中,np.sort(a)
对 a 中每行的数值进行排序,而 np.sort(a,
axis=0)对数组 a 每列上的数值进行排序。

np.sort(a) np.sort(aJ axis=0)


3

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

[1, 1,4, 5, 7]]


\

Numpy快
argsort()返回数组的排序下标,参 数 a x is 的默认值为-1:

—速 处 理 数 据
sort_axisl = np.argsort(a)
sort_axis0 = np.argsort(a., axis=0)
sort_axisl sort—axis0

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


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

为了使用 sort_axisO 和 sort_a x is l 计算排序之后的数纟11,即 np.sort(a)的结果,斋要产生非排


序轴的下标。下面使用 ogrid 对象产生第0 轴 和 第 1轴的下标 axisO 和 a x isl :

axis©^ axisl = np.ogrid[:a.shape[0]^ :a.shape[l]]

然后使用这些下标数组得到排序之后的数组:

a[axis0, sort_axisl] a[sor t _ a xi s 0 ,axisl]


3

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
^

[1, 1,4, 5, 7]]

使用这种方法可以对两个相关联的数组进行排序,即从数组 a 产生排序下标数组,然后使
用它对数组 b 进行排序。排序相关的函数或方法还可以通过kind 参数指定排序算法,对于结构
P y th o n 科学计算(第2 版)

数组可以通过 order参数指定排序所使用的字段。
lexsortO类似于 Excel 中的多列排序。它的参数是一个形状为(k,N)的数组,或者包含 k 个长
度 为 N 的数组的序列,可以把它理解为 Excel 中 N 行 k 列的表格。lexsort()返回排序下标,注意
数组中最后的列为排序的主键。在下面的例子中,按 照 “姓名-年龄”的顺序对数据排序:

names = ["zhang", "wang", "li", "wang", "zhang"]


ages = [37, 33, 32, 31, 36]
idx = np.lexsort([ages, names])
sorted_data = np.array(zip(names, ages), "0")[idx]
idx sorted一data

[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 = np.random.randint(0, V d } (5, 3))

b b [ n p . l e x s o r t ( b [ :, ::-l].T)]

[[4, 0, 9], [[3, 8) 2],

[5, 8, 0], [4, 0, 9],

[9, 2, 6], [4, 2, 6],


[3, 8, 2], [5, 8, 0],

[4, 2, 6]] [9, 2, 6]]

partitionO和 argpaititionO对数组进行分割,可以很快地找出排序之后的前k 个元素,由于它


不需要对整个数组进行完整排序,因此速度比调用sort〇之后再収前 k 个元素要快许多。下面从
10万个随机数中找出前5 个最小的数,
注意 paititionO得到的前5 个数值没有按照从小到大排序,
如果需要,可以再调用 sort〇对 这 5 个数进行排序即可:

n = np.random.randint(10, 1000000^ 100000)

n p . s o r t ( r ) [ :5] np.partition(r, 5)[:5]

[15, 23, 25, 37, 47] [15, 47, 25, 37, 23]

下面用% timeit测 试 sort〇和 partition〇的运行速度:

%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])

array([ 1.00029686, 1.99473003, 2.9614485 ])

Numpy快
当数组中的元素按照从小到大的顺序排列时,可 以 使 用 searchsortedO在数组中进行二分搜
索 。在下面的例子中,a 楚一个已经排好序的列表,v 是需要搜索的数值列表。searchsorted()

—速 处 理 数 据
返回一个下标数纟II, 将 v 中对应的元素插入到a 中的位置,能够保持数据的升序排列。当 v 中
的元素在 a 中出现时,通 过 side 参数指定返回最左端的下标还是最右端的下标。在下面的例子
中,16放 到 a 的下标为3、4 、5 的位置都能保持升序排列,side 参数为默认值"left”
时返回3 , 而
为"right"时返丨Hi 5。

a = [2, 4, 8, 16, 16, 32]

v = [1, 5, 33, 16]

np.searchsorted(aJ v) n p.searchsorted(a> side="right")

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

array([-l, 3, 1, 7, -1, 6])

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]

y = np.random.randint(0j 1000., 2000)


xl, yl = x.tolist(), y.tolist()

def get 一index 一diet(x, y ) :


idx_map = {v:i for i,v in enumerate(x)}

yindex = [idx_map.get(v^ -1) for v in y]


return yindex

yindexl = get 」ndex 一searchsorted(x, y)

yindex2 = get 一index_dict(xl_» yl)


print np.all(yindexl == yindex2)

%timeit g Gt_index_searchsonted(xJ y)
%timeit get_index_dict(xlj yl)

True

10000 loops ^ best of 3: 122 |is per loop

1000 loops, best of 3: 368 \xs per loop

2 . 4 . 4 统计函数

与本节内容对应的 Notebook 为:02-numpy/numpy>420-functions-count.ipynb。


本节介绍的函数如表2-6所示。

表 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]

如果参数 retum_index为 True , 则返回两个数组,第二个数组是第一个数组在原始数组中


的下标。在下面的例子中,数 组 index 保存的是数组 x 中每个元素在数组a 中的下标:

x, index = np.unique(a, return 一index=True)

x index a[index]

[1, 2, 3, 4, 6, 7] [9, 4, 1, 2, 0, 5] [1, 2, 3, 4, 6, 7]

如果参数 retum_inverse为 True , 则返回的第二个数组是原始数组a 的每个元素在数组x 中


的下标:

x, rindex = np.unique(a, return 一inverse=True)

rindex xfrindex]

[4, 2, 3, 4, 1, 5, 3, 3, 4, 0] [6, 3, 4, 6, 2, 7, 4, 4, 6, 1]

bincountO对整数数组中各个元素所出现的次数进行统计,它要求数组中的所有元素都萣非

负的。其返回数组中第 i 个元素的值表示整数i 出现的次数。

np.bincount(a)

array([0, 1, 1, 1, 3, 0, 3, 11)

87
Python 科学计算 (第 2 版)

由上面的结果可知,在数组 a 中 有 1个 1、1个 2、1个 3、3 个 4、3 个 6 和 1个 7 , 而 0、5


等数没有在数组 a 中出现。
通 过 w eigh ts 参数可以指定每个数所对应的权值。当 指 定 w eig h ts 参数时,bincount(x ,
weights= w )返回数纟JI x 中的每个整数所对应的 w 中的权值之和。j j j 文字解释比较难以理解,下

面我们看一个实例:

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)

array([ 1.3, 1.6, 0.6])

在上面的结果中,1.3是数组 x 中 0 所对应的 w 中的元素(0.1和 1.2)Z 和,1.6是 1所对奴的


w 中的元素(0.3、0.5和 0.8)之和,而 0.6是 2 所对应的 w 中的元素(0.2和 0.4)之和。如果要求平

均值,可以用求和的结果与次数相除:

np.bincount(Xj w) / np.bincount(x)
Numpy快

a r r a y ([ 0.65 ,0.53333333, 0.3 ])


—速 处 理 数 据

histogram()对一维数组进行直方图统计。其参数列表如下:

h i stogram(aJ bins=10, range=None_» weights=Nonej density=False)

其 中 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)

np.histogram(a, bins=5, range=(0, 1))

(array([28, 18, 17, 19, 18]), array([ 0. , 0.2, 0.4, 0.6, 0.8, 1.]))

首先创建了一个有100个元素的一维随机数组a ,取值范围在0 到 1之间。


然后用 histogmmO
对数组 a 中的数椐进行直方图统计。结果显示有2 8 个元素的值在0 到 0.2之间,18个元素的值
在 0.2到 0.4之间。读者可以尝试用 mnd〇创建更大的随机数组,由统计结果可知每个区间出现
的次数近似相等,因此 mnd()所创建的随机数在0 到 1 范围之间是平均分布的。
如果耑要统计的区间的长度不等,可以将表示区间分隔位置的数组传递给 b in s 参数,
例 1:
np.histogram(a, bins=[0, 0.4, 0.8, 1.0])

(array([46, 36, 18]), a r r a y ([ 0 . , 0.4, 0.8, 1.]))

结果表示0 到 0.4之间有4 6 个值,0.4到 0.8之间有36个值。


如果用 weights 参数指定了数组 a 中每个元素所对应的权值,则 histogmmO对区间中数值所
对应的权值进行求和。
下而看一个使用histogramO统计男性青少年年龄和身高的例子。
“height.csv ”
文 件 是 100名年龄在7 到 2 0 岁之间的男性青少年的身高统计数据。
首先)UloadtxtO从数据文件载入数据。在数纟J l d 中,第 0 列楚年龄,第 1 列楚身高。可以看
到年龄的范围在7 到 20之间:

d = np.loadtxt("height.csv"., dGlimitGr="/')

d.shape np.min(d[:, 0]) np.max(d[:, 0])

(100, 2) 7.0999999999999996 19.899999999999999

下面对数据进行统计,w m s 是每个年龄段的身高总和,cm s 是每个年龄段的数据个数,因

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]

cnts = n p . h i s t o g r a m ( d [ 0]^ bins=range(7^ 21))[0]

sums / cnts

array([ 125.96 , 132.06666667, 137.82857143, 143.8 ,

148.14 , 153.44 , 162.15555556, 166.86666667,

172.83636364, 173.3 , 175.275 , 174.19166667, 175.075 ])

histogrmri2 d〇和 histogramdd〇对 二 维 和 N 维数掘进行直方阁统计,我 们 将 在 第 9 章介绍


O p en C V 时对 histogram2 d 〇进行详细介绍。

2 . 4 . 5 分段函数

本丨:T介绍的函数如表2-7所示。

表 2 -7 本节要介绍的函数

函数名 功能
where 欠量化判断表达式
piecewise 分段函数
select 多分支判断选择

在前而的小节中介绍过如何使用 frompyfuncO函数计算三角波形。由于三角波形是分段函
数,需要根据肉变M:的取值范幽决定计算函数值的公式,因此无法直接通过 u flin c 函数计算。
N um Py 提供了一些计算分段函数的方法。
Python 科学计算 (第 2 版)

在 Python 2.6中新增加了如下判断表达式语法,当 condition条件为 T ru e 时,表达式的值为


y , 否则为 z :

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快
—速 处 理 数 据

def triangle_wavel(x, c } c0^ he):


x = x - x.astype(np.int) # 三角波的周期为1,因此只取x 坐标的小数部分进行计算
return np.where(x >= c,

0,
np.where(x < c0^
x / c0 * he,

(c - x) / (c - c0) * he))

由于三角波形分为三段,因此需-要两个嵌套的wherc()进行计算。由于所有的运算和循环都
在 C 语言级別完成,因此它的计算效率比frompyflincO高。
随着分段函数的分段数量的增加,
需要嵌套更多层 whereO。
这样不便于程序的编写和阅读。
可以用 selectO解决这个问题,它的调用形式如下:

select(condlist, choicelist, default=0)

其:中condlist是一个长度为 N 的布尔数组列表,choicelist是一个长度为 N 的存储候选值的


数绀列表,所有数纟11的长度都为 M 。如果列表元素不是数组而是单个数值,那么它相当于元素
值都相同、长度为 M 的数组。
对 于 从 0 到 M - 1 的数组下标 i , 从布尔数组列表中找出满足条件condlist[j][i] = T r u e 的 j 的
最小值,则 〇ut[f |=choicelist[jl |T|,其 中 ou t 是 select〇的返回数组。我们可以使用 select〇计算三角
波形:

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

由于分段函数分为三段,因此每个列表都有三个元素。c h o ic e s t 的最后一个元素为 True ,


表示前面的所有条件都不满足时,将使用 choicelist的最后一个数组中的值。也可以用 default参
数指定条件都不满足时的候选值数组:

return np.select([x>= c, x < c0 ],

[0 , x / c0 * he],
default=(c-x)/(c-c0)*hc)

但 是 where()和 select()的所有参数都需要在调用它们之前完成计算,因 此 N u m P y 会计算下


面 4 个数组:

Numpy快
x >= c, x < c0, x / c0 * he, (c - x) / (c -c0 ) * he

—速 处 理 数 据
在计算时还会产生许多保存屮间结果的数组,因此如果输入的数组X 很大,将会发生大量
内存分配和释放。
为了解决这个问题,
N u m P y 提供了 piecewiseO专门用于计算分段函数,
它的调叫参数如下:

piecewise(Xj condlist^ funclist)

参 数 x 是一个保存自变量值的数组,condlist楚一个长度为 M 的布尔数组列表,其中的每
个布尔数组的长度都和数组x 相同。ftm dist 是一个长度为 M 或 M + 1 的闲数列表,这些闲数的
输入和输出都是数组。它们计算分段函数中的毎个片段。如果不是函数而是数值,则相当于返
回此数值的函数。
每个函数与 condlist中下标相同的布尔数组对应,
如 果 ftmclist的长度为 M +1,
则最后一个函数计筧所有条件都为F alse 时的值。下而是使用 piecewiseO 计算三角波形的程序:

def triangle_wave3(Xj c, cd, he):


x = x - x.astype(np.int)
return np.piecewise(x,

[x >= c, x < C0],


[0, # x>=c
lambda x: x / c0 * he, # x<c0
lambda x: (c - x) / (c - c0) * he]) # else

使Jij piecewise()的好处在于它只计算需要计算的值。因此在上面的例子中,表达式 x/cO*hc


和(c -x )/(c -d ))*hC 只对输入数组 x 中满足条件的部分进行计算。下面运行前面定义的三个分段函
数,并 使 命 令 比 较 这 三 个 函 数 的 运 行 时 间 :
Python 科学计算(第2 版)

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)

y3 = triangle_wave3(x, d.6, 0.4, 1.0)

np.all(yl =:
= y2), np.all(yl == y3)

(True, True)

%timeit triangle_wavel(x, 0.6, 0.4, 1.0)


%timeit triangle_wave2(x, 0.6, 0.4, 1.0)

%timeit t r i a n g l e _w a v e 3 ( x J 0.6^ 0.4, 1.0)

1000 loops, best of 3: 614 [is per loop

1000 l o o p s , best of 3: 736 ^is per loop

1000 loops, best of 3: 311 |is per loop

2 . 4 . 6 操作多维数组
Numpy快

与本节内容对应的 Notebook 为:Ol-numpy/numpy^ SO-functions-aiTay-op.ipynlx


DVD
—速 处 理 数 据

本节介绍的函数如表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 = np.column 一stack((a, b))

[ [ 0, 1, 2], [ 0, 1, 2, 10, 11, 12][[ 0, 10],


[10, 11, 12]] [ 1, 11],
[2, 12]]

此外,C_[j 对象也可以)Ij于按列连接数绀:

np.c 」a, b, a+b]

array([[ 0, 10, 10],

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

a = np.random.randint(0, Id, 12)

idx = np.nonzero(np.diff(a) < 0)[0] + 1

a idx np.split(a^ idx)

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

7, 4, 3, 7] array([3, 7]),

array([4, 6) 9]),

array([2, 6, 7]),

a r r a y ([4]),

array([3, 7])]

当第二个参数为盤数时,表示分组个数。split〇只能平均分组,而 array_ split〇能尽量平均


分组:

np.split(a, 6) np.array 一split(a, 5)

[array([6, 3]), [array([6, 3, 7]),

array([7, 4]), array([4, 6, 9]),

anray([6, 9]), array([2, 6]),


Python 科学计算 (第 2 版)

array([2, 6]), a r r a y ([7, 4]),

a m a y ( [ 7 , 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),它是通过将原数纽形状的中间两个轴对调得
到的。

a = np.random.randint(0, 10, (2, 3, 4, 5))

print u "原数组形状:", a .shape

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

列的大图像。首先调用 g lo b 和 cv 2 模块中的闲数,获得一个数组列表 imgs。cv 2 萍将在第9 章


介 绍 O p en C V 时进行详细介绍。

import glob

import numpy as np

from cv2 import imread^ imwnite

imgs = [ ]

for fn in glob.glob("thumbnails/*.png"):
imgs.append(imread(fn^ -1))

print i m g s [ 0 ] .shape

(90, 160, 3)

im gs 中每个元素都楚一个多维数纟11,它的形状为(90, 160,3),其:中第0 轴的长度为图像的

高度,第 1轴的长度为图像的宽度,第 2 轴为图像的通道数,彩色图像包含红、绿、蓝三个通


道,所以第2 轴的长度为3。
调 用 concatenate()将这些数组沿第 0 轴拼成•个大数组,结 果 im g 是 • 个 宽 为 160 像素、高
为 2700像素的图像:

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 = img.reshape(6, 5, 90, 160, 3)

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

img2 = imgl.swapaxes(l, 2 ) .reshape(540^ 8 0 0 ^ 3)

请读者思考下面的 img 3 会得到怎样的图像:

img = np.concatenate(imgSj 0)

img3 = img.reshape(5, 6, 90, 160, 3) \


.tnanspose(l, 2, 0, 3, 4) \

•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 = img.reshape(6, 5 ,9 0 ,160, 3)


mask = np.ones(imgl.shape[:-l], dtype=bool) O
mask[:, 2:-2, 2:-2] = False ©

imgl[mask] = 230 ©
img4 = imgl.swapaxes(l, 2).reshape(540, 800, 3)

2 . 4 . 7 多项式函数

与本节内容对应的 Notebook 为:02-numpy/numpy~450-functions-poly.ipynb。

多项式函数是变量的整数次幂与系数的乘积之和,可以用下面的数学公式表示:

f (x ) = a nx n + aj^ x 11一1 4----f a 2 x z + a xx + a 〇

由于多项式闲数只包含加法和乘法运算,冈此它很容M 计算,可用于计算其他数学闲数的
Numpy快

近似值。多项式函数的应用非常广泛,例如在嵌入式系统中经常会用它计算正弦、余弦等函数。
在 N um Py 中,多项式函数的系数可以用一维数组表示,例如可以用下而的数组表示,其 中 a[〇l
—速 处 理 数 据

是最高次的系数,a [_l ]是常数项,注 意 x 2的系数为0。

a = np.anray([1.0, 0, -2, 1])

我们可以用 p〇ly ld ()将系数转换为 polyld (—元多项式)对象,此对象可以像函数一样调用,


它返回多项式函数的值:

p = np.polyld(a)
print type(p)

p(np.linspace(0, 1, 5))

<class 'n u m p y .l i b .p o l y n o m i a l .p o l y l d '>

array([ 1. , 0.515625, 0.125 -0.078125, 0. ])

对 p o ly ld 对象进彳」
:加减乘除运算相当于对相应的多项式函数进行计算。例如

p + [-2, 1] # 和 p + np.polyld([-2, 1 ] ) 相同

polyld([ 1., 0., -4., 2.])

p * p # 两个3 次多项式相乘得到一个6 次多项式


polyld([ 1,, Q.j -4.j 2.j 4.j -4. •])

p / [1, 1] # 除法返M 两个多项式,分别为商式和余式


( p o ly ld ( [ 1 . ] ) , p o ly ld ([ 2 . ] ) )
由于多项式的除法不一 •定能正好整除,因此它返冋除法所得到的商式和余式。在上面的例
子屮,商式为x 2 _ x - l , 余式为2。因此将商式和被除式相乘,再加上佘式就等于原来的 P :

p == np.polyld([ 1 。 -1., -1.]) * [1,1] + 2

True

多项式对象的 deriv〇和 imeg〇方法分別计兑多项式函数的微分和积分:

p.deriv()

polyld([ 3.^ Q.j -2.])

p.integO

polyld([ 0.25, 0. , -1. , 1. , 0.])

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 中使用自动补全查看函数名:

>>> np.poly # 按 Tab 键


np.poly np.polyadd np.polydiv np.polyint np.polysub

n p .polyld np.polyder np.polyfit np.polymul np.polyval

其 中 的 pdyfitO 闲数可以对一组数据使用多项式闲数进行拟合,找到与这组数据的误差平
方和最小的多项式的系数。下而的程序用它计算-ti/ 2 〜 ti/2区间勹 sin〇〇闲数最接近的多项式
的系数:
Python 科学计算 (第 2 版)

n p .set_printoptions(suppress=True, pnecision=4)

x = np.linspace(-np.pi / 2, np.pi / 2, 1000) O


y = np.sin(x) ©

for deg in [3, S y 7]:

a = np.polyfit(x^ y, deg) ©

error = np.abs(np.polyval(a, x) - y) O
print "degree {}: {}".format(deg, a)

print "max error of order %d:" % deg, np.max(error)

degree 3: [-0.145 -0. 0.9887 0. ]

max error of order 3: 0.00894699376707


degree 5: [ 0.0076 -0. -0.1658 -0. 0.9998 -0. ]

max error of order 5: 0.000157408614187


degree 7: [-0.0002 -0. 0.0083 0. -0.1667-0. 1. 0. ]

max error of order 7: 1.52682558063e-06


Numpy快

〇首 先 通 过 l i n s p a c e ()将 - / 2 〜ti ti /2区间等分为(1000-1)等份。©计 算 拟 合 H 标 函 数 s i n (x )


的值。©将表示目标函数的数组传递给 p 〇l y f i t 〇进行拟合,第三个参数 d 为多项式函数的最高
—速 处 理 数 据

e g

阶数。p o l y f i t O 所得到的多项式和目标函数在给定的1000个点之间的误差最小, p o l y f u O 返回多


项式的系数数组。〇使 用 p o l y v a l O 讣兑多项式函数的值,并计兑与0 标函数的差的绝对值。
从程序的输出可以看到,由于正弦函数是一个奇函数,因此拟合的多项式系数中偶数次项
的系数接近于0。
图2-10显示了各阶多项式与正弦函数之间的误差,
请注意图屮Y 轴为对数來标。

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处的值:

from numpy.polynomial import Polynomial^ Chebyshev


p = P o l y n o m i a l ([1^ -2, 0, 1])

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)

Polynomial([ 1 ” 0 •,-8 . , 0 ” 8.], [-1. , I . ] ,[ - 1 ., 1.])

图2-11 0 到4 次切比S 夫多项式

切比雪夫多项式的根被称为切比雪夫节点,可以用于多项式插值。相应的插值多项式能最

大限度地降低龙格现象,并且提供多项式在连续函数的最侁一致逼近。下面以f 〇〇 = ^函

数插值为例演示切比雪夫节点与龙格现象。
Python 科学计算 (第 2 版)

O 在 [- U ]R 间上等距离取 n 个収样点。© 使 用 n 阶切比雪夫多项式的根作为取样点。©


使州两利诹样点分別对f(x)进行多项式插值,即计兑一个多项式经过所有的插值点。图 2-12显
示了两种插值点所得到的插值多项式,由左图可知等距离插值多项式在两端有非常大的振荡,
这种现象被称为龙格现象,n 越大振荡也越大;而右图采用切比雪夫节点作为插值点,插值多
项式的振荡明显减小,并 且 n 越大振荡越小。

插值与拟合
所谓多项式插值就是找到一个多项式经过所有的插值点。一 个 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)

cl = Chebyshev.fit(xl, f(xl), n - 1, domain=[-l, 1]) ®

c2 = Chebyshev.fit(x2, f(x2)^ n - 1, d o m a i n = [ -l > 1])

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)

p_g = Polynomial.fit(x, g(x), n - 1, domain=[-l_» 1])


c_g = Chebyshev.fit(Xj g(x), n - 1, domain=[-l, 1])

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

阅 2-13 Chebyshev插值与 Polynomial插值比较

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

下面用同样的方法对函数h(x )进 行 19阶的切比雪夫多项式插值,得到插值多项式 c_h :

def h(x):
x = 5 * x

return np.exp(-x**2 / 10)

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

print "Max Chebyshev Error:", abs(h(xd) - c一h ( x d ) ) . m a x ()

Max Chebyshev Error: 1.66544267266e-09

多项式类支持四则运算,
下面将 c_ 5 和 c_h 相 减 得 到 并 调 用 其 rootsO计算其所有根。
Numpy快

然后找出其中所有的实数根reaU oots , 它们就是 g (x )与 h(x )交点的横坐标。图 2-14显示了这两


条函数曲线以及通过插值多项式计算的交点:
—速 处 理 数 据

c_diff = c_g - c_h


roots = c _ d i f f . r oo t s ()
real_roots = r o o t s [roots.imag == 0 ] . real

print np.allclose(c_diff(real 一roots), 0)

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

x = Chebyshev.basis(n, domain=[-10, 0]).roots() O


xd = np.linspace(-10, 0, 1000)

c_g2 = Chebyshev.fit(x, g2(x), n - 1, domain=[-10, 0]) ©

print "Max Chebyshev Error:", abs(g2(xd) - c_jg2(xd)).max() @

Max Chebyshev Error: 6.47574571744e-09

2 . 4 . 9 各种乘积运算

与本节内容对应的 Notebook 为: 02-n u m /


p y n u m p y -460-f u n c t i o n s -d o t .i p y n b 〇

本节介绍的函数如表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)

c 是数组 a 和 b 的多个子矩阵的乘积。我们可以把数组 a 看作两个形状为(3,2)的矩阵,而把


数 组 b 看作两个形状为(2,3)的矩阵。a 中的两个矩阵分别与b 中的两个矩阵进行矩阵乘积,就得
到数组 c ,c [i,
:,j ,:]是a 中第 i 个矩阵与 b 中第 j 个矩阵的乘积。

for j in np.ndindex(2, 2):

assert np.alltrue( c[i, 〕


、 :] == np.dot(a[i]^ b [ j ] ) )

对于两个一维数组,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)

for i, j, k, 1 in np.ndindex(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])

np.outer(a, b) n p . d o t ( a [ :, None], b [ N o n e , :])

[[4, 5, 6, 7]) [[4, 5, 6, 7],


[8, 10, 12, 14], [ 8, 10, 12, 14],

[12, 15, 18, 21]][12, 15, 18, 21]]


te n so rd o tO 将两个多维数组 a 和 b 指定轴上的对应元素相乘并求和,它是最一般化的乘积运
算函数。下面通过一些例子逐步介绍其用法。下面计兑两个矩阵的乘积: O a x e s 参数有两个元
素,第一个元素表示 a 中的轴,第二个元素表示 b 中的轴,这两个轴上对应的元素相乘之后求
和 。© a x e s 也可以是一个整数,它表示把 a 中的后 a x e s 个轴和 b 中的前 a x e s 个轴进行乘积和运
算 ,而对于乘积和之外的轴则保持不变。

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)

cl = np.tensordot(a, b, axes=[[l], [0]]) O

c2 = np.tensordot(a, b, axes=l) ©

c3 = np.dot(a, b)

assert n p.allclose(clJ c3)

assert np.allclose(c2^ c3)

对于多维数组的 dot〇乘积,可以用 tensordot^ b,axes=[[-l ],[-2]])表示,即将 a 的最后轴和 b

Numpy快
中的倒数第二轴求乘积和:

—速 处 理 数 据
a = np.arange(12).reshape(2^ 3, 2)

b = np.arange(12, 2 4 ) .reshape(2^ 2, 3)

cl = np.tensordot(a, b, axes=[[-l], [-2]])

c2 = np.dot(a, b)

assert np.alltrue(cl := = c2)

在下面的例子中,将 a 的 第 1、第 2 轴 与 b 的 第 1、第 0 轴求乘积和,因此(:中的每个元素


都是按照如下表达式计算的:

c[i, j, k, 1] = np.sum(a[i, :, j] * b[:, k, 1].T)

注怠由于 b 对应的 a x es 中的轴是倒序的,因此需要做转置操作。

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 = np.tensordot(a, b ,axe s = [ [ l ,2 ],[1 ,0]])

for i, j, k, 1 in np.ndindex(4, 7, 2, 3):

assert np.allclose(c[i, k, 1], np.sum(a[i^ j] * b [ :^ k, 1 ] .T))

c.shape

(4, 7, 2, 3)

05
Python 科学计算 (第 2 版)

2 . 4 . 1 0 广 义 ufunc 函数

与本节内容对应的 Notebook 为:(


^-numpy/numpy^ TO-guftincs.ipynlx

从 NumPy 1.8 开始 FF.式支持广义 uflmc 函数(generalized ufunc, 以下简称 gufianc)。 guflmc 是


对 uflm c 的推广,所 谓 ufimc 就是将对单个数值的运算通过广播运用到整个数组中的所有元素之
上,而 gu fu n c 则足将对中•个矩阵的运算通过广播运用到整个数组之上。例 如 numpy.linalg.inv〇
是求逆矩阵的 guftm c 函数。在其文档中描述其输入输出数组的形状如下:

ainv = inv(a)

a : ( … , M)

ainv : (..., M., M)

输入数组 a 的 形 状 中 带 有 它 表 示 0 到任意多个轴。当它为空时,就是对单个矩阵
求逆,g iifu n c 函数将对这些轴进行广播运算。最后两个轴的长度为 M , 表示任意大小的方形
NumPy快

矩阵。
—速 处 理 数 据

N um P y 中的线性代数模块 linalg 中提供的函数大都为广义ufunc 函数。在 S c iP y 中也提


供了线性代数模块 linalg,但其中的函数都是一般函数,只能对单个矩阵进行计算。关
于线性代数函数库的用法将在下一章进行详细介绍。

在输出数组 a iiw 中,由于逆矩阵的形状与原矩阵相同,冈 此 a in v 的最后两轴的形状也是


(M ,
M )。 表示广播运算之后的形状,而由于矩阵求逆只对一个矩阵进行运算,因此
的形状和 a 中 的 的 形 状 相 同 。
在下面的例子中,a 的形状为(10,20,3,3),其中(10,20)与 “…”对应 , 3 与 M 对应。而 inv〇
通过广播运算对10X 20个形状为(3,3)的矩阵求逆。
得到的结果 ainv 的形状与 a 相同,
也是(10,20,
3,3)〇

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)

下面的程序验证第 i 行、第 j 列的矩阵及其逆矩阵的乘积,应该近似等于3 阶单位矩阵:

numpy.linalg.det〇计算矩阵的行列式,它也是一个 gufim c 函数。它的输入输出的形状为:


adet = det(a)
a M, M)

adet :(… )

由于矩阵的行列式是将一个M x M 的矩阵映射到一个标量,因此输出 adet 的形状中只包含


u yy
♦ 參* 〇

adet = np.linalg.det(a)

adet.shape

(10, 20)

下面以多个二次函数的数据拟合为例,介绍如何使用 gufunc 函数提高运算效率。首先通过


随机数函数创建测试用的数据x 和 y , 这两个数组的形状都为〇1,
10)。其中的每行数据(x [i]和 y [i])
构成一个曲线拟合的数据集,它们的关系为:y ^ b + PiX + PoX2。现在需要计算每对数据所
对应的系数P 。

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)

y = beta[:,2, None] + x*beta[:, 1, None] + x**2*beta[:^ d, None]

M 然使用前而介绍过的 numpy.polyfit()可以很方便地完成这个任务,下而的程序输出第42

组的实际系数以及拟合的结果:

print b e t a [42]
print np.polyfit(x[42], y[42], 2)

[0.0191932 0.30157482 0.66017354]


[0.0191932 0.30157482 0.66017354]

只需要循环调用 n 次 numpy.polyfitO即可得到所需的结果,但是它的运算速度有些慢:

%time beta2 = np.vstack([np.polyfit(x[i]., y[i], 2) for i in range(n)])

Wall time: 1.52 s

n p . a llclose(betaJ beta2)

True

在 numpy.polyfitO 内部实际上足通过调用最小二乘法闲数 numpy.linalg.lstsqO来实现多项式


拟合的,我们也可以直接调用lstsq()计算系数:

xx = np.column_stack(([x[42]**2 ,x [ 4 2 ],np.ones_like(x[42])]))

print np.linalg.lstsq(xx, y[42])[0]

07
Python 科学计算 (第 2 版)

[0.0191932 0.30157482 0.66017354]

但遗憾的楚,0 前 numpy.linalg.lstsqO还不楚 guflinc 函数,因此无法]I 接使用它计算所有数


据组的拟合系数。然 而 numpy.linalg 屮对线性方程组求解的函数solve()是 一 个 gu fon c 函数。并
且根据最小二乘法的公式:
0 = (XTX )-1X Ty
只需要求出X TX 和XTy ,就可以使用 numpy.linalg.solveO计算出0 = s o lv e (XTX ,XTy )。为了
实现这个运算,还需要计算矩阵乘积的guftmc 闲数。然 而 dot〇并不是一个 gullinc 闲数,因为它
不遵循广播规则。
N um Py 中 H 前还没有正式提供计算矩阵乘积的gufunc 闲数,
不过在 umath_tests
模块中提供了一个测试用的函数:matriX_mUltiply〇。下而的程序使用它和 s〇
M )实现高速多项
式拟合运算,它所需的吋间约为 polyfit()版木的五十分之一。

%%time

X = np.dstack([x**2, x, np.ones__like(x)])
Xt = X.swapaxes(-1, -2)
Numpy快

import numpy.core.umath_tests as umath


A = umath.matrix_multiply(Xt, X)
—速 处 理 数 据

b = umath.matrix_multiply(Xt> y [ . . ” None]).squeeze()

beta3 = np.linalg.solve(A, b)

print np.allclose(beta3, beta2)

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)

因此 solve()的返M 值 beta3 的形状为(10000,3)。


前面的例子中,使用的都是最简单的广播规则。实际上 gufunc 函数支持所有的 uflinc 函数
的广‘
播规贝II。因此形状分別为(a,m ,
n)和(b ,1,
n,k )的两个数组通过 matrix_ multiply〇乘积之后得到
的数组的形状为(b,
a,m ,
k )。下面看一个使j|j guflinc 函数广播运算的例子:

在二维平面上的旋转矩阵为:
co s 0 —sin 0
M (0)
sin 0 co s 0 -

它能将平而上的某点的坐标圈绕原点旋转0 。对于形状为(N ,2)的矩阵 P , 可以表示平而上


N 个点的坐标。而矩阵乘积得到的则是将这 N 个点绕坐标原点旋转0之后的坐标。下面的程序
使 用 matrix_multiply()将 3 条丨II丨线上的坐标点分别旋转4 个角度,得 到 12条丨II丨线。
调 用 11犯11^_1111^#7〇时两个参数数组的形状分别为(3,100,2)和(4,1,
2,2),其中广播轴的形
状分别为(3,
)和(4,1),运算轴的形状分别为(100,2)和(2,2)。广播轴进行广播之后的形状为(4,3),

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 , … ])

print points.shape, M.shape^ rpoints.shape

(3, 100, 2) (4, 2, 2) (4, 3, 100, 2)

将 这 12条曲线绘制成图表之后的效果如图2-15所示。

團 2-15使用矩阵乘积的广播运箅将3 条曲线分别旋转4 个角度

09
Python 科学计算 (第 2 版)

2 . 5 实用技巧

与本节内容对应的 Notebook 为:OZ-numpy/numpy-QOO-tips.ipynb,

在编写应用程序时,可能经常会与其他的程序库交换人M 数裾。木节介绍一些通过 NumPy


数组共享内存的方法。

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
—速 处 理 数 据

from array import array


a = array("d", 创建一个 array 数组
[1,2,3,4]) #
# 通过np.frombuffer() 创建一个和a 共享内存的N u m P y 数组
na = np.frombuffer(a, dtype=np.float)
print a
print na
na[l] = 20 # 修改N u m P y 数组中下标为1 的元素
print a
array(.d., [1.0, 2.0, 3.0, 4.0])
[ 1 . 2 . 3. 4.]
array('d [1.0, 20.0, 3.0, 4.0])

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

data = np.frombuffer(buf, dtype=np.float).reshape(-1, 2)


print data
[[0. 1. ]
[0.09983342 0.99500417]
[0.19866933 0.98006658]

[0.29552021 0.95533649]
[ 0 .38941834 0 .9 2 1 0 6 0 9 9 ]]

下而是 Python 中实现 array 对象动态添加元素的算法:


* a r r a y 对象拥有一块用于保存数椐的内存•,其长度通常比数组中的所有数据的字节数
要长。
•当 往 a rra y 中添加数据时,如果数据内存中还有空余位置,则直接写入空余位置。
•当数据内存中无空佘位置时,贝種新分配一块更大的数据内存,并将当前的数据都复
制到这块新的数据内存中,而旧的数据内存则被释放掉。
根据上述兑法可知,只 要 往 a m iy 中添加元素,其数据内存的地址就可能发生改变。在此
之前通过 np.from bufM )创建的数t l 仍然引j |J 旧的数据内存,从而成为“野指针”。下面的代码
演示了这个过程。其 中 arniy.buffeiJnfoO 获得数据内存的地址以及其中有效数据的个数。

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:

print

(83088512, 1) (83088512, 2) (83088512, 3) (83088512, 4) (31531848, 5)


(31531848, 6) (31531848, 7) (31531848, 8) (34405776, 9) (34405776, 10)

由上而的结果可知,当数组 a 的长度为5 和 9 时,数椐内存被重新分配了。而 n a 数组是在


a 的长度为3 时通过 n p .f t ‘
om b u ffe i <)得到的,
因此它的数掘指针已经成为野指针。
n d arm y.
c ty p e s .d a ta

可以获得数组的数裾内存的地址,
可以看出 n a 的数据内存地址仍然是a 在重新分配之前的地址,
而 n a 中的数据也变成了随机的无效数据。

print n a .c t y p e s .data
print na

83088512

[ 2.11777767e+161 6.24020631e-085 8.82069697e+199]

由上而的分析可知,每次动态数组的长度改变时,我们都需要重新调用 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):

buf += struct.pack("=hdd", i., math.sin(i*0.1), math.cos(i*0.1)) O

dtype = np.dtype({"names":["id",. ,
sin","cos"], "formats":["h", "d", "d"]}) e
data = np.frombuffer(buf, dtype=dtype) ©

print data

[(0, 0.0, 1.0) (1, 0.09983341664682815, 0.9950041652780258)


Numpy快

(2, 0.19866933079506122, 0.9800665778412416)


(3, 0.2955202066613396, 0.955336489125606)
—速 处 理 数 据

(4, 0.3894183423086505, 0.9210609940028851)]

O 采集三个通道的数据,其中通道1是短整型数,其类型符号为“h ”,通 道 2 和 3 为双精

度浮点数,其类型符号为“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,我们需要使用这些信息。

from PyQt4.QtGui import Qlmage, qRgb


img = QImage("lena.png")

print "width & height:", img.width(), i m g . h e i g ht ()


print "depth:", i m g . d e p t h ()#每个像素的比特数
print " f o r m a t : ' i m g . f o r m a t ()_» Qlmage.Format 一RGB32
print "byteCount:", img.byteCount() # 图像的总字节数

print "bytesPerLine:", img.byt e sP e r L i n e 〇 # 每行的字节数

print "bits:", int(img.bits()) # 图像第一个字节的地址

width & height: 512 393


depth: 32

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

下面通过 an*数纟11和 im g 对象查看位于像素坐标(50,100)处的像素颜色值,可以看到二者是


完全相同的:

x, y = 100, 50
b, g, r, a = arr[y, x]

print qRgb(r, g, b)
print img.pixel(x^ y)

4289282380

4289282380

下面通过 a ir 数组修改颜色值,并 通 过 im g 对象查看修改的结果,由结果可知二者的确共


享着同一块内存:

arr[y, x, :3] = 0x12, 0x34, 0x56

print hex(img.pixel(x, y))

0xff563412L

使用上述方法共享内存时需注意必须保持目标对象处于可访问状态。例如在上例中,如果
执 行 del im g 语句引起 im g 对象被垃圾回收,则通过 arr 数组将访问被释放掉的内存区域。为了
解决这个问题,可以让数组的 base 屈性引用目标对象,这样只要数组不被释放,则目标对象也
不会被释放。为了能正确设置 base 屈性,需要使用数组的_ airayjnterface _ 接口。
P y th o n 科学计算(第 2 版)

O 在 调 用 array()将 0 标对象转换成数组时,如果0 标对象拥介_ arrayjnterface _ 屈性,则


根据该属性的描述创建数组。它是一个具有特定键值的字典,参见表2-10。

表 2 - 1 0 键值及含义
键值 含义
shape 所创建数组的形状
data 数据存储E 的首地址,以及是否只读
strides 数组的strides厲性
typestr 元素类型描述符
descr 如果创逑结构数组,该键描述结构体各个字段名以及对应的数据类型
version 固定为3

© 设 置 c o p y 参 数 为 False, 这样所创建的数组与El标对象共享内存,否则将复制0 标对象


的内存。© 在创述完数组之后,「
II以删除_ arrayjnterface _ 属性。® 所得到的数纟Jl arr2 与 arr 相
Numpy快

同,并且其 base 属性为 im g 对象。

interface = {
—速 处 理 数 据

'shape': (img.height()^ img.width()j 4)^


'd a t a ': (int(img.bits ()),False),
•strides': (img.bytesPerLine(), 4, 1),
•typestr':
'version': 3^

>

img. 一arnay_intenface 一 = interface O

arr2 = np.array(img, copy=False) O


del img.— array 一interface— ©
print np.all(arr2 == arr), arr2.base is img O
True True

如 果 目 标 对 象 只 读 ,无法为其添加_〇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

arr3 = n p .a r r a y ( A r ra y P r o x y (img, interface)^ copy=False)


print np.all(arr3 == arr)
True

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

("Wang", 24, 65.2, 170.0)], dtype=persontype)

print a ["age"] .base is a # 视图


print a[["age", "height"]]• base is None #复制
True

True

Numpy快
为了创建结构数组的多字段视图,可以使用下面的 fields_View 〇闲数。它通过原数组的 dtype

—速 处 理 数 据
属性创建视图数组的dtype 对象。然后通过 ndairayO创建视图数组。

def fields_view(arrj f i e l d s ) :

dtype2 = np.dtype({name:arn.dtype.fields[name] for name in fields})


return np.ndarray(arr.shapeJ dtype2j arr, 0j a m . s t r i d e s )

v = fields_view(a, ["age ","weight"])


print v.base is a

v["age"] + = 10
print a

True

[('Zhang', 42, 72.5, 167.0) ('Wang', 34, 65.19999694824219, 170.0)]

dtype 对象的 fields 属性是一个以字段名为键、以字段类型和字节偏移_


M 为值的字典,使用
它创建新的 dtype 对象时,可以保持字段的偏移量:

print a.dtype.fields

print a.dtype
print v.dtype

{'age': (dtype('int32'), 32), 'name': (dtype('S30'), 0),


'weight': (dtype('float3 2 ' 3 6 ) , 'height': (dtype('float32'), 40)}
{■ names •:[• n a m e ,/ age • / weight • / height ■ ] , _ formats _:[■ S30 • / <i4 _ / <f4 ■ / <f4 ■],

'offsets':[0,32,36.,40]^ 'itemsize':44, 'aligned':True}

{'names':[' a g e ' / w e i g h t * ' f o r m a t s ' :[,< i 4 ' / < f 4 , 'offsets': [ 3 2 ^ 3 6 'itemsize':40}


P y th o n 科学计算(第2 版)

如果这两个 dtype 对象的 itemsize屈性相冋,


那么可以使用数组的view 〇方法创建视图对象。
但是从上面的输出可以看到两个dtype 对象的字节数并不相同,一 个 是 4 4 个字节,另一个是 40
个字节。遇到这种情况时,可以使用 ndarrayO创建数组的视图,它的调用参数如下:

ndarray(shape_» dtype=float, b u ffen=NoneJ offset=0_» strides=None_» orden=None)

• 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模块包含了众多的物理常数:

from scipy import constants as C


print C.c # 真空中的光速
print C.h # 普朗克常数
299792458.0

6.62606957e-34

在 字 典 physical_constants中,以物理常量名为键,对应的值是一个含有三个元素的元组,
分别为常数值、单位以及误差,例如下面的程序用来查看电子的质量:

C •physical 一c o n s t a n t s ["electron m a s s " ]

(9.10938291e-31, 'kg', 4e-38)

除了物理常数之外,constants模块中还包括许多单位信息,它 们 是 1 单位的量转换成标准
单位时的数值:
Python 科学计算(第2 版)

# 1 英里等于多少米,1 英寸等于多少米,1 兑等于多少千兑,1 磅等于多少千兑


C.mile C.inch C.gnam C.pound

1609.3439999999998 0.0254 0.001 0.45359236999999997

S c iP y 的 special 模块是一个非常完整的函数库,其中包含了蕋本数学函数、特殊数学函数
以 及 N u m P y 中出现的所有函数。由于函数数量众多,本节仅对其进行简要介绍。至于其具体
所包含的函数列表,请读者参考 S d P y 的帮助文构。
伽玛(gamma)函数r 是概率统计学中经常出现的一•个特殊函数,它的计算公式如下:

显然,要通过此公式计算r (z )的值是比较麻烦的,可 以 用 special 模 块 中 的 gamma()进行


计算:

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)
—值 计 算 库

print S.gamma(l+lj) # gamma 函数支持复数


print S . g a m m a (1000)

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

special 模块中的某些函数并不是数学意义上的特殊函数,例 如 loglp (x )计 算 log (l + x )的值。

这是由于浮点数的精度有限,无法很精确地表示非常接近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)

u = np.linspace(-10, 10, 200)

results = S.ellipj(u[:, None], m [ N o n e , :])

print [y.shape for y in results]


in

[(200, 4), (200, 4), (200, 4), (200, 4)]

A



•10 S 10 -10 -5 0
图3-1使用广播计算得到的cllipjO返回值

3 . 2 拟合与优化一optimize

与本节内容对应的 Notebook 为:03-scipy/scipy-210-optimize.ipynb。

S ciP y 的 optimize 模块提供了许多数值优化兑法,本节对其中的非线性方程纟II求解、数据拟


合、函数最小值等进行简单介绍。

119
Python 科学计算 (第 2 版)

3 . 2 . 1 非线性方程组求解

fsolve 〇可以对非线性方程组进行求解,它的基本调用形式为 fsolve(fimc,xO)。其 中 firnc 是


计兑方程组误差的函数,它的参数 x 是一个数组,其值为方程组的一纟JI可能的解。ftinc 返回将
x 代入方程组之后得到的每个方程的误差,xO 为未知数的一组初始值。假设要对下而的方程组

进行求解:
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数

from math import sin, cos


from scipy import optimize
—值 计 算 库

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]

〇f 〇是计算方程组的误差的函数,X 参数是一绀可能的解。fsdveO 在调j_l_j f ()时,传递给 f ()


的参数是一个数组。© 先调州数姐的 tolist()方法,将 K 转 换 为 Python 的标准浮点数列表,然后
调 用 math 模块中的阑数进行运算。因为在进行单个数值的运算时,标准浮点类型比 N um Py 的
浮点类型要快许多,所以把数值都转换成标准浮点数类型,能缩短一些计算时间。© 调 用 fsd ve ()
时,传递计算误差的函数 f〇以及未知数的初始值。
在对方程组进行求解吋,fsdveO 会 A 动计算方程组在某点对各个未知数变M 的偏导数,这
些偏导数组成一个二维数组,数学上称之为雅可比矩阵。如果方程组中的未知数很多,而与每
个方程有关联的未知数较少,即雅可比矩阵比较稀疏吋,将计算雅可比矩阵的函数作为参数传
递 给 fcdveO , 这能大幅度提高运兑速度。笔者在一个模拟计兑的程序中需要求解有5 0 个未知
数的非线性方程纟11。
每个方程平均与6 个未知数相关,
通过传递计箅雅可比矩阵的函数使fsd ve ()
的计算速度提高了 4 倍。

雅可比矩阵
雅可比矩阵是一阶偏导数以一定方式排列的矩阵,它给出了可微:分方程与给定点的最优线
性逼近,因此类似于多元函数的导数◊ 例如前面的函数 fl 、f2、B 和 未 知 数 ul 、u2 、u3 的雅可
比矩阵如下:
r3 fl d fl a fli
aui du2 du3
d f2 d f2 d f2

dul du2 du3


d f3 d f3 d f3
-dul du2 du3-

下面使用雅可比矩阵对方程组进行求解。O 计算雅可比矩阵的函数 j()和 f〇— 样,其 x 参

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 )],

[0, x2, xl]

result = optimize.fsolve(f, [1,1,1], fprime=j) ©

print result
print f(result)

[-0.70622057 -0.6 -2.5 ]


[0.0, -9.1260332624187876-14, 5.329070518200751G-15]

3 . 2 . 2 最小二乘拟合

假设有一组实验数据(x i/y i ),我们事先知道它们之间应该满足某函数关系: yi = f (Xi)。通


过这些已知信息,需要确定函数f〇的一些参数。例如,如果函数f〇是线性函数f (x ) = k x + b ,
那么参数k 和b 就是需要确定的值。
如果用p 表示函数中需要确定的参数,则 H 标是找到一组P 使得函数S 的值最小:

s ( p) =/ [ y i - f ( x pp)]2
P y th o n 科学计算(第2 版)

这种算法被称为最小二乘拟合(least-square fitting)。在 optimize 模块中,可以使用 leastsqO对


数据进行最小二乘拟合计算。leastsqO的用法很简单,只需要将计箅误差的函数和待确定参数的
初始值传递给它即可。下面是用 leastsqO对线性函数进行拟合的程序:

import numpy as np
from scipy import optimize

X = np.array([ 8 . 1 9 , 2 . 7 2 ,6 . 3 9 , 8 . 7 1 , 4.7 , 2 . 6 6 , 3.78])

Y = np.array([ 7 . 0 1 , 2 . 7 8 ,6.47, 6 . 7 1 , 4.1 , 4 . 2 3 , 4.05])

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数

r = optimize.leastsq(residuals, [1, 0]) ©


k, b = r[0]
—值 计 算 库

print ..k =_、k, "b =.、 b

k = 0.613495349193 b = 1.79409254326

图 3-2(左)直观地M 示了原始数据、拟合直线以及它们之间的误差。OresidualsO的参数p 是
拟合直线的参数,函数返冋的是原始数据和拟合直线之间的误差。图屮用数据点到拟合立线在
Y 轴上的距离表示误差。© leastsqO使得这些误差的平方和最小,即图中所有正方形的面积之和

最小。
由前而的闲数S 的公式可知,对于直线拟合来说,误差的平方和是直线参数_ lb 的二次多
项式闲数,因此可以用如图3-2(右)所示的曲面直观地显示误差平方和与两个参数之间的关系。
图中用红色圆点表示曲而的最小点,它 的 X -Y 轴的坐标就是 leastsqO的拟合结果。

图3 - 2 最小化正方形面积之和(左),误差曲面(右)
接下来,让我们再看一个对正弦波数据进行拟合的例子:

def func(x, p): O


M M II

数据拟合所用的蜗数: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

实验数据x, y 和拟合函数之间的差,p 为拟合需要找到的系数


ii ii ii

return y - func(x, p)

x = np.linspace(0, 2*np.pi, 100)


# 真实数据的函数参数

s c ip y数
A, k, theta = 10, 0.34, np.pi/6

y0 = func(x, [A, k, theta]) # 真实数据

—值 计 算 库
# 加入噪声之后的实验数掘
np.r a n d o m. s e e d (0)
yl = y 0 + 2 * np.random.randn(len(x)) ©

p0 = [7, 0.40, 0] # 第一次猜测的函数拟合参数

# 调用l e a s t s q 进行数据拟合
# r e s i d u a l s 为计寫误差的函数

# P 0 为拟合参数的初始值
# a r g s 为需要拟合的实验数据

plsq = optimize.leastsq(residuals, p0, args=(yl, x)) O

print u" 真实参数:


" ,[A, k, theta]
print u" 拟合参数" ,plsq[0] # 实验数据拟合后的参数

pl.plot(x, yl, label=u" 带噪声的实验数据")


pl.plot(x, y0, label=u" 真实数据")

pl.plot(x, func(x, plsq[0]), lab e l = u __拟合数据")


p i .l e g e n d (l o c = "b e s t ")

真实参数:[10, 0.34, 0.5235987755982988]


拟合参数[ 10.2 52 18748 0.3423992 0.50817424]

图 3-3 S 示了带噪声的止弦波拟合。
P y th o n 科学计算(第2 版)

程序中,〇要拟合的目标函数 flincO是一个正弦函数,它的参数 p 是一个数纽,包含决定


正弦波的三个参数A 、k 、theta, 分別对应正弦函数的振幅、频率和相角。© 待拟合的实验数据
是一组包含噪声的数据(x ,
y l ) , 其中数组 y l 为标准正弦波数据 yO 加上随机噪声。
S c iP y数

O 用 leastsqO对带噪声的实验数据(x ,
y l )进行数据拟合,它可以找到数组 x 和 yO 之间的正弦
- 值计算库

关系,即确定 A 、k 、theta等参数。和前而的直线拟合程序不同的是,这里我们将(y U )传递给


args 参数。leastsqO会将这两个额外的参数传递给rcsiduals〇。 © 因此 residuals()有三个参数,p 是
正弦函数的参数,y 和 x 是表示实验数据的数组。
对于这利1一维丨111线拟合,optimize库还提供丫一个 curve_fit()函数,下而使用此函数对正弦
波数据进行拟合。它 的 0 标 函 数 与 leastsqO稍有不同,各个待优化参数直接作为函数的参数
传入。

def func2(x, A, k, theta):

return A * n p .sin(2*np.pi*k*x+theta)

popt, _ = optimize.curve_fit(func2, x, yl, p0=p0)

print popt

[10.25218748 0.3423992 0.50817424]

如果频率的初值和寘实值的差别较人,拟合结果中的频率参数可能无法收敛于实际的频率。
在下面的例子中,Etl于频率初值的选择不当,导致 curve_fit〇未能收敛到真实的参数。这时可以
通过其他方法先估算一个频率的近似值,或者使用全局优化算法。在后面的例子中,我们会使
用全局优化算法重新对正弦波数据进行拟合。

popt, _ = optimize.curve_fit(func2, x, yl, p0=[10^ 1, 0])

print u ••真 实 参 数 [A, k, theta]


print u" 拟合参数", popt

K实参数:[10, 0.34, 0 . 5 2 3 5 987755982988]


拟合参数[ 0 . 7 1 0 9 3 4 7 3 1.02074599 -0.1277666 ]
3 . 2 . 3 计算函数局域最小值

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 -

dxf dx1 dx2 dx1 dxn


d 2f d 2f d 2f
dx2 dx1 dx\ dx2 dxn
參 • 參 參
參 • 參 •
• • 參 參

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 )的最小值,根据其输出可知有些算法需要较长的

收敛时间,而有些算法则利用导数信息更快地找到最小点。

def target_function(Xj y):


return (l-x)**2 + 100*(y-x**2)**2

class T a r g e t F u n c t i o n ( o b j e c t ) :

def — init— (self):


self.fjDoints = [ ]

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

return np.array([[2*(600*x**2 - 200*y + 1 ) ^ -400*x]^


—值 计 算 库

[-400*x, 200]])

def fmin 一demo(method):


target = TargetFunction()

init_point =(-l, -1)


res = optimize .minimize (target init_pointj

method=method,
j a c = t a r g e t .f p r i m e ,

hess=tanget.fhess)

return res, [np.array(points) for points in


(target.fjDoints, target.fprime jDoints^ target.fhess 一points)]

methods = ("Nelder-Mead", "Powell", "CG", "BFGS", "Nevyfton-CG", "L-BFGS-B")

for method in methods:


res, (f_points, fprimejDoints, fhess_points) = fmin 一demo(method)

print "{:12s}: min={:12g}, f count={:3d}, fprime count={:3d},


"fhess c o u nt={:3d}".format(

method, f l o a t ( r e s[ " f u n " ] ), len(f_points),


l en(fprime_points), l e n ( fhessjDoints))

Nelder-Mead min= 5.30934e-10, f count=125, fprime count= 0, fhess count= 0


Powell min= Q, f count= 52, fprime count= 0, fhess count= 0

CG min= 7.6345e-15, f count= 34, fprime count= 34, fhess count= 0

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 计算全域最小值

前而介绍的儿种最小值优化算法都只能汁算局域的最小值,optim ize 库还提供了儿种能进


行全局优化的算法,下而以前而的正弦波拟合为例,演示全局优化函数的用法。在使用 leastsq()
对正弦波进行拟合时,误差函数 residua^ ) 返回一个数组,表示各个取样点的误差。而函数最小
值算法则只能对一个标量值进行最小化,因 此 最 小 化 的 目 标 函 数 返 回 所 有 取 样 点 的
误差的平方和。

def func(x, p ) :

A, k, theta = p
return A*np.sin(2*np.pi*k*x+theta)

def func_error(pj y, x ) :

return np.sum((y - func(x, p))**2)

x = np.linspace(0j 2*np.pi, 100)

A, k, theta = 10, 0.34, np.pi/6


y0 = func(x, [A, theta])

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〇

result = optimize.basinhopping(func_error, (1, 1, 1),


niter = 10,

m i n i m i z e r_ k w a r g s = { " m e t h o d" :"L-BFGS-B",

"args":(yl, x)})
print result.x

[10.25218694 -0.34239909 2.63341582]


S c iP y数

虽然频率和相位与原系数不同,但是由于正弦闲数的周期性,其拟合|||丨线楚和原始数据重
- 值计算库

合的,如 图 3-5所示。


冬I 3-5川 basinhoppingO拟合正弦波

3 . 3 线性代数- linalg

与本节内容对应的 Notebook 为:03-scipy/scipy-310-linalg.ipynb。

N um Py 和 S ciP y 都提供丫线性代数函数庵 linalg,S c iP y 的线性代数库比 N um Py 更加全面。


3 . 3 . 1 解线性方程组

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

10 l o o p s , best of 3: 20 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

除了使用 lu_ factor〇和 lu_ solve〇之外,可以先通过 inv()计算逆矩阵,然后通过 dot()计算矩


阵乘积。下面比较二者的速度,可以看出 lu_ factor〇比 inv()要快很多,而 lu_ solve()和 dot()的运
算速度儿乎相同:

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)

10 l o o p s , best of 3: 131 ms per loop


100 loops, best of 3: 9.65 ms per loop
10 l o o p s , best of 3: 52.6 ms per loop

100 l o o p s ^ best of 3: 13.8 ms per loop

3 . 3 . 2 最小二乘解

IstsqO比 s〇M )更一般化,它不要求矩阵 A 是正方形的,也就是说方程的个数可以少于、


等于或多于未知数的个数。它找到一组解)c,使得 ||b - A x ||最小。我们称得到的结果为最小二
乘解,即它使得所有等式的误差的平方和最小。下而以求解离散卷积的逆运算为例,介 绍 IstsqO
s c ip y数

的用法。
—值 计 算 库

首先简单介绍一下离散卷积的相关知识和计算方法。对于离散的线性时不变系统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[l] = h[0]*x[l] + h[l]*x[0]


y[2] = h[0]*x[2] + h[l]*x[l] + h[2]*x[0]
y[3] = h[0]*x[3] + h[l]*x[2] + h[2]*x[l]

•••
y[n+m-l] = h[rrl]*x[m-l]

所谓卷积的逆运算就是指:假设已知 x 和 y , 需要求解 h 。由于 h 的长度为 n , 于是有 n 个


未知数,而 由 于 y 的长度为 ii+ m -1,因此这 n 个未知数需要满足 n+ m -1个线性方程。由于方
程数比未知数多,卷积的逆运算不一定有精确解,因此问题就变成了找到一组 h ,使 得 x *h 与
y 之间的误差最小,®然它就是最小二乘解。下面的程序演示了如何使用 t o q ()计算卷积的逆

运算:
〇竹先 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 个元素是最小二乘解,注怠得到的结果顺序是颠倒的,因此还需要对
其进行翻转。

from numpy.lib.stride_tricks import as_strided

def make_data(m, n, noise— s c a l e ) : O

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)

yn = y + np.random.standard_normal(len(y)) * noise_scale * np.max(y)

return x, yn, h

def solve_h(x, y, n): ©

X = as_strided(xJ shape=(len(x)-n+1, n)^ strides=(x.itemsizG> x.itemsize)) @

Y = y[n-l:len(x)] O

h = linalg.lstsq(X, Y) ©

return h[0][::-l] ©

接下来对长度为100的未知系统系数h , 分别计算长度为80和 120的最小二乘解。由于我


们对系统的输出添加了一些噪声信号,因此二者并不完全吻合。图 3-6比较了这两个解与真实
系数。

x, yn, h = make_data(1000, 100, 0.4)

HI = solve_h(x, yn, 120)

H2 = solve 」!(x, yn, 80)

print "Average error o f HI:", np.mean(np.abs(H[:100] - h))

print "Average error of H2:", np.mean(np.abs(h[:80] - H2))

Average error of HI: 0.301548854044

Average error of H2: 0.295842215834


P y th o n 科学计算(第2 版)

-3— 实际旳系炔眷政 一 《小二乘WH 1


-4 ^ ^ ■
0 20 40 60 80 100

图3 - 6 实际的系统参数与最小二乘解的比较
S c iP y数

3 . 3 . 3 特征值和特征向量
- 值计算库

n x n 的矩阵A 可以看作n 维空间中的线性变换。若乂为11维空间中的一个向M ,那么A 与x 的


矩阵乘积就是对x 进行线性变换之后的向量。如果x 是线性变换的特征向量,那么经过这个线性.
变换之后,得到的新向量仍然与原来的x 保持在同一方向上,但其长度也许会改变。特征向量
的长度在该线性变换下缩放的比例称为其特征值。即特征向量x 满足如下等式,A 的值可以是一
个任意复数:
A x = Ax
下面以二维平面上的线性变换矩阵为例,演示特征值和特征变量的几何含义。通过
linalg.eig (A )计算矩昨A 的两个特征值 evalues 和特征向量 evectors,在 evectors 中,每一列楚一个

特征句董。

A = np.arnay([[l, -0.3], [-0.1, 0.9]])


evalues, evectors = linalg.eig(A)

evalues evectors

[1.13027756+0.]•, 0.76972244+0.j ] [[ 0.91724574, 0.79325185],

[-0.3983218 , 0.60889B68]]

图 3-7显示了变换前后的向量。在图中,蓝色箭头为变换之前的向量,红色箭头为变换之
后的向量。粗箭头为变换前后的特征向量。可以看出特征向量变换前后方向没有改变,只是长
度发生了变化。长度的变化倍数[tl特征值决定:一个变为原来的1.13倍长,一个变为原来的0.77
倍长。
图3 - 7 线性变换将蓝色箭头变换为红色箭头

numpy.linalg 模块中也有 eig 〇函数,与之不同的是,scipy.linalg 模块中的 eig ()函数支持计錄


广义特征值和广义特征向量,它们满足如下等式,其中B 是一个n x n 的矩陈:
A x = 入Bx
广义特征向M 可以用于椭圆拟合,椭岡拟合的公式与原理可以参考下面的论文:

s c ip y数
—值 计 算 库
http://research.microsoft.com/pubs/67845/ellipse-pami.pdf
用广义特征向量计算椭圆拟合。

椭圆上的点满足如下方程,其中a,b,c, d, e, f为椭圆的参数,( X,y) 为平面上的坐标点:


f(x, y) = ax2 + bxy + cy2 + dx + ey + f = 0
所谓椭圆拟合,就是指给出一组平面上的点(Xi,y i ) , 找到一组椭圆参数,使得^ ( x p y i )2最
小。显然这是一个最小化问题,可以使用上节介绍的优化综法optimize.leastsqO求解。为了避免
参数全为0 的平凡解,需要一点小技巧,请读者自行演练一下。下面给出论文中用广义特征向
量计算椭圆拟合的方法:
首先定义Xj = [x ^ Xi^ y ^ x ^ y i ,1]T , D = [X p …,x n]。D 是一个n x 6 的矩阵,其中n 为点
的个数,D 中的每一行与一个坐标点相对应。a 为拟合椭圆的系数: a = [a ,b ,c ,d ,e ,f]T 。则8满
足如下方程:
DTDa = ACa
其 中 C 是一个6 x 6 的矩阵:
「0 0 2 0 0 〇-
0 一1 0 0 0 0
2 0 0 0 0 0
C
0 0 0 0 0 0
0 0 0 0 0 0
L〇 0 0 0 0 0」
显然上式符合广义特征向量的等式,因此可以用 linalg.eigO求解。下而首先使用椭岡的参
数方程计算某个椭岡上随机的60 个点,并引入一些随机噪声:
P y th o n 科学计算(第2 版)

X (t ) = Xc -f a cost cos<p —b sint sirup


Y (t ) = Yc + a cost sincp + b sint coscp

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数

D = np.c_[x**2, x*y, y**2, x, y, np.ones 一like(x)]


- 值计算库

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

[-0.55214278 0.5580915 -0.23809922 0.54584559 -0.08350449 -0.14852803]

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个像素。

r, g, b = np.rollaxis(pl.imread(,,v i n c i _ t arget.png")J 2 ) .astype(float)


img = 0.2989 * r + 0.5870 * g + 0.1140 * b

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)

U.shape s.shape Vh.shape

(505, 505) (375,) (375, 375)

S 中的每个值与V h 的行A 量以及 U 中的列A f f l 对应,默认按照从大到小的顺序排列,它表


示与其对应的向S :的重要性。由图3-9可知 s 中的奇异值大小差别很大,
注怠 Y 轴是对数坐标系。

pi.semilogy(s, lw=3)

图3 - 9 按从大到小排列的奇异值

下而的 composite!;
)选 择 U 和 V h 中的前 n 个向M 重新合成矩阵,当用上所有向M 时,重新
合成的矩阵和原始矩阵相同:
P y th o n 科学计算(第2 版)

def composite(U, s, Vh, n):

return n p.dot(U[:> :n], s[:n, np.newaxis] * V h [ : n , :])

print np.allclose(img> composite(U, s, Vh, len(s)))

True

下而演示选择前10个、20个以及5 0个 向 合 成 时 的 效 果 ,如 阁 3-10所示,可以看到使用
的向量越多,结果就越接近原始图像:

imgl0 = composite(U, s, Vh, 10)

img20 = composite(U, s, Vh, 20)

img50 = composite(U, s, Vh, 50)

%anray_image img; imgl0; img20; img50


s c ip y数
—值 计 算 库

阁3 - 1 0 原始罔像以及使爪10 个、2 0 个、5 0 个向量合成的阍像(从左到右)

3*4 统计-stats

与本节内容对应的 Notebook 为:03-scipy/scipy~400-stats.ipynb。

S d P y 的 stats 模块包含了多种概率分布的随机变量(随机变量是指概率论巾的概念,不是
Python 中的变量),随机变量分为连续和离散两种。所有的连续随机变量都是 ^ co n tin u o u s 的派
生类的对象,而所有的离散随机变量都是rv_discret e 的派生类的对象。

3 . 4 . 1 连续概率分布

可以使用下而的语句获得stats模块中所有的连续随机变量:

from scipy import stats

[k for k, v in stats._ diet— .i t e m s () if isinstance(v^ stats.rv 一continuous)]


['g e n h a l f l o g i s t i c ', •triang 、 'r a y l e i g h 、 •b e t a p r i m e 、
•levy 、 'f o l d n o r m ', 'genlogistic', .gilbrat.,
•l o g n o r m 、 'a n g l i t 、 'truncnorm', 'erlang 、

_norm ’ 'n a k a g a m i ', 'weibulljnin 、 'cosine 、


'logistic 、 'fisk', 'genpareto', •tukeylambda 、
•d g a m m a 、 'pareto 、 'h a l f l o g i s t i c 、 'semicircular 、
'ksone'^ 'mielke', •ncx2 、 •g e n g a m m a 、

'johnsonsu'^ 'p o w e r n o r m 、 'powerlaw 、 ■burr',


•j o h n s o n s b 、 •b e t a •, 'gamma•, .wald 、
•arcsine 、 'maxwell 、 'i n v g a u s s 'y 'gausshypen'^
.rice 、 'vonmises— l i ne', 'loglaplace 、 ■levy 一stable、

'exponweib', 'p e a r s o n 3 ', •chi', 'f,


'cauchy 、 'truncexpon', 'k s t w o b i g n ', •recipinvgauss',

'f r e c h e t _ l ', 'foldcauchy■, 'wrapcauchy 、 .ncf_,


_g e n e x p o n 、 •e x p o n 、 'reciprocal'^
'l o m a x '. •l o g g a m m a 、 'i n v g a m m a ', 'p o w e r l o g n o r m 、
0)
'laplace', 'vonmises', 'frechet 一r', 'dweibull', 〇b
nd
_r d i s t •, ■g u m b e l _ r 、 'g o m p e r t z '^ •h a l f c a u c h y 、

'i n v w e i b u l l 'y •exponpow 、 'w e i b u l l _ m a x 、 •gumbel 一1、 A



'h a l f n o r m 、 'f a t i g u e l i f e ', •chi2., .net., 计
'uniform 、 'g e n e x t r e m e 'y •alpha 、 'h y p s e c a n t ', 算

'b r a d f o r d ', •levy-1,]

连续随机变量对象都有如下方法:
• 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))

norm 可以像函数一样调用,通 过 lo c 和 scale 参数可以指定随机变量的偏移和缩放参数。对

于正态分布的随机变量来说,这两个参数相当于指)其期望值和标准差,标准差是方差的算术
平方根,因此标准差为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))

下而调用随机变量 X 的 rvs()方法,得到包含一万次随机取样值的数组x ,然后调用 NumPy


的 m e a n 〇和 var〇计算此数组的均值和方差,其结果符合随机变量X 的特性:

x = X.rvs(size=10000) # 对随机变量取 10000 个值


np.mean(x), np.var(x) # 期望值和方差
(1 .0043406567303883, 3 .889 9 5 7 2 8 13 4 2 6 5 5 3 )

也可以使用 fit()方法对随机収样序列 x 进行拟合,它返回的是与随机取样值最吻合的随机


变量的参数:

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)计算随机变量的概率密度闲数和累积分布沐|数的值,并与统
计值比较。©汁算样木的累积分布时,需要与区间的大小相乘,这样才能保证其结果与累积分
布函数相同。

pdf, t = np.histogram(x, bins=100, normed=True) O


t = (t[:-l] + t[l:]) * 0.5 ©

cdf = np.cumsum(pdf) * (t[l] - t[0]) ©


p_error = pdf - X.pdf(t)
c_error = cdf - X.cdf(t)

print "max pdf error: {}, max cdf error: { } " .format(
np.abs(p 一error) .max(), np.abs(c_error) .max 〇)

max pdf error: 0.0217211429624, max cdf error: 0.0209887986472

图 3-l l (左)显示了概率密度函数和直方图统计的结果,可以看出二者是一致的。右图显示
了随机变量X 的累积分布函数和数组p d f 的累加结果。
0 5
图3-11正态分布的概率密度函数(左)和尜积分布函数(右)

有些随机分布除了 loc和 wale 参数之外,还需要额外的形状参数。例如伽玛分布可用于描


述 等 待 k 个独立随机摩件发生所需的时间,k 就是伽玛分布的形状参数。下面计算形状参数 k
为 1 和 2 时的伽玛分布的期望值和方差:

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 个随机值:

x = stats.gamma.rvs(2, scale=2, size=4)


x
arnay([ 2.47613445, 1.9B667652, 0.85723572, 9.49088092])

接下来调用 pdf()查看勹上而 4 个随机值对应的概率密度:

stats.gamma.pdf(x, 2, scale=2)
array([ 0.17948513, 0.18384555, 0.13960273, 0.02062186])

也可以先创建将形状参数和尺度参数固定的随机变M ,然后苒调用其 pdf()计算概率密度:

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)

然后创建表示这个特殊骰子的随机变M dice , 并调用其 rvs〇方法投掷此骰子2 0 次,获得符


合概率 p 的随机数:

dice = s t a t s .rv_discrete(values=(x, p))

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 次的平均值,由于每次投掷骰子都可
—值 计 算 库

以看作一个独立的随机事件,因此投掷5 0 次的平均值可以看作“大S 相互独立的随机变M ”,


其平均值的分布应该十分接近正态分布。
仍然通过 rvs〇获得取样值,其结果是一个形状为(2_,
50)的数组,沿着第一轴计算每行的平均值,得 到 samples_meam

n p .r a n d o m .s e e d (42)
samples = dice.rvs(size=(20000, 50))

samples_mean = np.mean(samples, axis=l)

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)

x = np.linspace(bins[0], bins[-l], 100)


pl.plot(x, kde(x), label=u" 核密度估计")
mean, std = s t a t s .n o r m .f i t (sampl e s _ me a n )

pl.plot(x, stats.norm(mean, std).pdf(x), alpha=0.8, label=u" 正态分布拟合")

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

—值 计 算 库
•当 为 数 值 时 ,将直接使用此数值作为宽度系数。
核函数的方差由数据的方差和宽度系数决定。
下面的程序比较宽度系数对核密度估计的影响。当宽度系数较小时,可以看到在三个数据
点处的高斯曲线的峰值,而当宽度逐渐变大时,这些峰值就合并成一个统一的峰值了。

for bw in [0.2, 0.3, 0.6, 1.0]:

kde = stats.gaussian_kde([-l, 0, 1], bw_method=bw)


x = np.linspace(-5, 5^ 1000)

y = kde(x)

pl.plot(x, y, lw=2, label="bw={}".format(bw), alpha=0.6)


p i .l e g e n d (lo c = " b e s t ")

图 3-13显示 b w _ m e th o d 参数越大,核密度估计曲线越平滑。

图3-13 bw_melhod 参数越大,核密度估计丨II丨线越平滑


P y th o n 科学计算(第2 版)

3 . 4 . 4 二项分布、泊松分布、伽玛分布

本节州几个实例程序对概率论中的二项分布、泊松分布以及伽玛分布进行一些实验和
讨论。
二项分布是最重要的离散概率分布之一。假设有一种只有两个结果的试验,其成功概率为
p , 那么二项分布描述了进行n 次这样的独立试验,成 功 k 次的概率。二项分布的概率质量闲数

公式如下:
n! ■k
f (k ;n ,p ) Pk( l - P )n'
k !(n —k )!

例如,可以通过二项分布的概率质量公式计算投掷5 次骰子出现3 次 6 点的概率。投掷一


次骰子,点 数 为 6 的概率(即试验成功的概率)为p = 1 / 6 , 试验次数为n = 5 。使用二项分布的
概率质量函数 pmf()可以很荇易计兑出现 k 次 6 点的概率。和概率密度函数 pdf〇类似,pmf()的
第一个参数为随机变量的取值,后面的参数为描述随机分布所耑的参数。对于二项分布来说,
参数分別为 n 和 p ,而取值范围则为0 到 n 之间的整数。下面的程序计算 k 为 0 到 6 时对应的概
率:
s c ip y数
—值 计 算 库

s t a t s .b i n o m .p m f (r a n g e (6)^ 5, 1/6.0)

array([ 4.01877572e-01, 4.01877572e-01, 1 . 60751029e-01,


3.21502058e-02, 3.21502058e-03, 1.2 8 6 0 0 8 23 e - 0 4 ] )

由结果可知:出现0 或 1 次 6 点的概率为40.2%,而出现3 次 6 点的概率为3.215%。


在二项分布中, 如果试验次数 n 很大,而每次试验成功的概率p 很小,乘 积 n p 比较适中,
那么试验成功次数的概率可以用泊松分布近似描述。
在泊松分布中使用X描述单位时间(或单位面积)中随机事件的平均发生率。如果将二项分布
中的试验次数n 看作单位时间中所做的试验次数,那么它和事件出现的概率p 的乘积就是事件的
平均发生率A , 即A = n •p 。泊松分布的概率质M 函数公式如下:
e ~AXk
f(k; A)
k!
下面的程序分別计算二项分布和泊松分布的概率质量函数,结果如图3-14所示。可以看出
当n 足够大时,二者是十分接近的。程序中的事件〒均发生率入恒等于10。根据二项分布的试验
次数n , 计算每次事件出现的概率p = ;
V/n 。

lambda_ = 1 0 . 0
x = np.arange(20)

nl, n2 = 100, 1000

y_binom_nl = s t a t s .binom.pmf(x, nl, lambda_ / nl)

y_binom_n2 = s t a t s .binom.pmf(x, n2, lambda 一/ n2)


yjDoisson = stats.poisson.pmf(x, lambda 」

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

count, time 一edges = np.histogran^t, bins=time, range=(0^ time)) O


dist, count 一edges = np.histogram(count, bins=20, nange=(0, 20), density=True) ©

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

xl, poissonl, distl = sim 一poisson(lambda 」 t i m e s [0])


x2, poisson2, dist2 = sim_poisson(lambda 」 times[l])
max_errorl = np.max(np.abs(distl - poissonl))
max_error2 = np.max(np.abs(dist2 - poisson2))

print "time={}, max_error={}__ .format(times[0], max_errorl)

print max— error={}__ .format(times[1], max_error2)

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

time = 1000 time = 50000

图3 - 1 5 模拟泊松分布

〇可以用 N um Py 的随机数生成函数unifomi〇产生 T 均分布于〇到 time 之间的 lambda_*time


s c ip y数

个事件所发生的时刻。© 用 histogramO可以统计数组 t 屮每秒之内的事件发生的次数count,根


据泊松分布的定义,count数组中的数值的分布情况应该符合泊松分布。© 接下来统计事件次数
—值 计 算 库

在 0 到 2 0 区间内的概率分布。当 histogram 〇的 density 参 数 为 T r u e 时,结果和概率质量函数


相等。
还可以换个角度看随机事件的分布问题。我们可以观察相邻两个事件之间的时间间隔的分
布情况,或者隔 k 个事件的时间间隔的分布情况。根据概率论,事件之间的时间间隔应符合伽
玛分布,由于时间间隔可以是任怠数值,因此伽玛分布是连续概率分布。伽玛分布的概率密度
函数公式如下,它 描 述 第 k 个事件发生所需的等待时间的概率分布。r (k :
)是伽玛函数,当k 为
整数吋,它的值和k 的阶乘k !相等。
x (k-l)A k e (-XX)
f (X;k ,A)
r⑻

下面的程序模拟了事件的时间间隔的伽玛分布,结 果 如 图 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_) ©

return x, gammas dist

lambda 10.0
time = 1000

ks = 1, 2
xl, gammal, distl = sim_gamma(lambda_, time, ks[0])

x2, gamma2, dist2 = sim_gamma(lambda— , time, ks[l])

时间间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

A_time = np.random.uniform(0, T, A_count) O

B_time = np.random.uniform(0, T, B_count)

bus— time = np.concatenate((A_time, B_time)) ©

bus_time.sort()

N = 200000

45
Python 科学计算(第2 版)

passenger_time = np.random.uniform(bus_time[0], bus 一


time[-l], N) ©

idx = np.searchsorted(bus_time, passenger 一


time) O

n p .m e a n (b u s _ t i m e [idx] - passenger— time) * 60 ©

199.12512768644049

模拟的总时间为 T 分钟,在这段之间之内,应该有八_〇)咖次A 路公交车和 B _count 次 B


路公交车到达 S 站点。O 可以用均匀分布 uniformO产生两路公交车到达 S 站点的时刻,© 将这
两个保存时刻的数组连接起来,并进行排序。
© 在第-趟和最后-•趟公交车的到达时间之间,产生乘客随机到达 S 站点的吋刻数组。〇
在已经排序的公交车到达时刻数组b u s jim e 巾使用二分法搜索每个乘客到达吋刻所在的下标数
组 idx 。© busjim elick ]就是乘客到达车站之后第一个到达车站的公交车的时刻,因此只需要计
算其差值,并求平均值即可。通过随机数模拟得出的平均等待时间约为200秒。
将 A 和 B 两路汽车一起考虑,
前后两个车次的平均间隔也为200秒。
这似乎有些不可思议,
直觉上我们可能期待一个小于平均间隔的等待时间。
s c ip y数

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

在公式中,X 是两辆公交车之间的间隔时间,f〇〇d x 是时间间隔为x 出现的概率。由于观察


者效应,乘客出现在较长时间间隔的概率也较大,因此xf 〇〇d x 可以看作与乘客出现在时间间隔
为 x 时段的概率成比例的量,分母的积分将其归一化。而分子中的 x/2是在该时间间隔段到达
车站所需的平均等待时间。下面我们计算该公式,由图3-17可知,公交车的间隔儿乎不会超过
30分钟,因此虽然公式中的积分上限为+〇〇,但在实际计算时只需要指定一个较大的数即可。
在木章后续的小节中会详细介绍数值积分quad() 的用法。

from scipy import integrate

t = 10.0 / 3 # 两辆公交车之间的平均时间间隔
bus 一interval = s t a t s . g a mm a (1, scale=t)

n, _ = integrate .quad (lambda x : 0 . 5 * x * x * bus_interval.pdf (x), 0, 1000)

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 符 合 d f = n - 1的学生 t-分布。t 值是抽选的样本的平均值与繫体样本的期望值


s/\fn

之差经过正规化之后的数值,可以用来描述抽取的样本与整体样本之间的差异。
下而的程序模拟学生t-分布(参见阁3-18),O 创建一个形状为(100000,10)的正态分布的随机
数数组,© 使用上而的公式计算 t 值,© 统 计 t 值的分布情况并与 stats.t的概率密度函数进行比
较。如果我们使用10个样本计算 t 值,则它应该符合 df= 9 的学生 t-分布。

mu = 0.0

n = 10

samples = s t a t s .norm(mu) .rvs(size=(100000, n)) O

t_samples = (np.mean(samples, axis=l) - mu) / np.std(samples, ddof=l, axis=l) n**0.5 ©

sample_dist, x = np.histogram(t 一samples, bins=100, density=True) ©

x = 0.5 * (x[:-l] + x[l:])

t_dist = stats.t(n-l).pdf(x)

print "max e r r o r : n p . m a x ( n p . a b s ( s a m p l e _ d i s t - t_dist))

47
Python 科学计算(第2 版)

max error: 0.00658734287935

图3 - 1 8 模拟学生t-分布

图 3-19绘 制 d f 为 5 和 3 9 的概率密度函数和生存函数,当 d f 增大时,学 生 t-分布趋向于正


态分布。
S c iP y数
- 值计算库

图 3-19当df 增大时,学生t-分布趋向于正态分布

学 生 t-分布可以用于检测样本的平均值,下面我们从一个期望值为1 的正态分布的随机变
量中取30个数值:

n = 30

n p .r a n d o m .s e e d (42)

s = s t a t s .norm.rvs(loc=lj scale=0.8, size=n)

我们建立整体样本的期望值为0.5的零假设,并 用 stats.ttest_ l Samp〇检验零假设是否能够被


推翻。stats.ttest_ lsamp()返回的第一个值为使用前述公式计算的t 值 ,第二个值被称为 p 值。当 p
值小于0.05时,通常我们认为零假设不成立。因此下面的测试表明我们可以拒绝整体样本的期
望值为0.5的假设。

零假设
在统计学中,零假设或虚无假设(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)

2.65858434088 (2.6585843408822241, 0.012637702257091229)

下而我们检验期望值是杏为1.0,由于 p 值大于0.05,我们不能推翻期望值为1.0的零假设,
但这并不意味着可以接受该假设,因为期望值为0.9的假设对应的p 值也大于0.05,甚 至 比 1.0
的 t 值还大。

print (np.mean(s) - 1) / (np.std(s, ddof=l) / np.sqrt(n))

print stats.ttest_lsamp(s, 1), s t a t s .t t e s t _ l s a m p (s , 0.9)

-1.14501736704

(-1.1450173670383303, 0.26156414618801477) (-0.38429702545421962, 0.70356191034252025)

通 过 ttestJsampO 计算的 p 值就是图3-20中红色部分的面积。可以这样理解 p 值的含义:


如果随机变量的期望值真的和假设的相同,那么从这个随机变量中随机抽取 n 个数值,其 t 值
比测试样本的t 值还极端(绝对值大)的可能性为p 。因此当 p 很小时,我们可以推断假设不太可

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)

mask = x > np.abs(t)


plt.fill_between(x[mask]^ y [ m a s k ] , color="red"., alpha=0.5)

49
Python 科学计算 (第 2 版)

mask = x < -np.abs(t)


pit.fill_between(x[mask], y[mask], color="red"> alpha=0.5)
plt.axhline(color="k", lw=0.5)
plt.xlim(-5, 5)

下 面 用 scipy.integrate.trapzO积分验证 p 值 ,由于左右两块红色而积是相等的,下面的积分
只需要计算其中一块的面积:

from scipy import integrate


x = np.linspace(-10, 10, 100000)
y = stats.t(n-l).pdf(x)
mask = x >= np.abs(t)
integrate.trapz(y[mask], x[mask])*2
0.012633433707685974

下面我们用随机数验证前而计算的p 值,我们创建 m 组随机数,每组都有 n 个数值,然后


计算假设总体样本期望值为0.5 时每组随机数对应的 t 值 tr,它是一个长度为 m 的数组。将 tr
s c ip y数

与样木的 t值 ts进行绝对值大小比较,当 叫 〉 |以 时 ,就说明该组随机数比样木更极端,统计


—值 计 算 库

极端组出现的概率,可以看到它和 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)

si = stats.norm.rvs(loc=l, scale=1.0, size=20)


s2 = stats.norm.rvs(loc=1.5, scale=0.5, size=20)
s3 = stats.norm.rvs(loc=l.5, scale=0.5, size=25)

print stats.ttest_ind(si, s2y equal一var=False) O


print stats.ttest一ind(s2, S3, equal一var=True) ©
(-2 .2391470627176755, 0 .0332508 6 60 8 6 7 4 3 6 6 5 )

(-0.59466985218561719, 0 .5 5518058758105393)

3 . 4 . 6 卡方分布和卡方检验

卡方分布(X2 )是概率论与统计学屮常用的一•种概率分布。k 个独立的标准正态分布变量的


平方和服从自由度为k 的卡方分布。下面通过随机数验证该分布,结果如图3-21所示:

a = n p . random.normal(size:(300000, 4))

cs = np.sum(a**2, axis=l)

sample 一distj bins = np.histogram(cs, bins=100, range=(0, 20), density=True)


x = 0.5 * (bins[:-l] + bins[l:])

chi2 一dist = s t a t s .chi2.pdf(x, 4)

print "max error:", n p .m a x (n p .a b s (sample_dist - chi2_dist))

max error: 0.00340194486328

s c ip y数
—值 计 算 库
图 3-21使用随机数验证卡方分布

片方分布可以用来描述这样的概率现象:袋子里有5 种颜色的球,
杣到每种球的概率相同,
从中选 N 次,并统计每利_ 色的次数〇i 。
则下而的X2符合自由度为4 的卡方分布,
其中E = N /5为
每种球被杣选的期望次数:

2 V ( ° i ~ E)2
x = Z - T —
i=l

下面川程序模拟这个过程,结果如图3-22所示。O 首先调州 mndintO创建从0 到 5 的随机


数,其结果 b a lljd s 的第0 轴表示实验次数,第 1轴为每次实验抽取的100个球的编号。©使用
bincountO统计每次实验中每个编号出现的次数, 由于它不支持多维数组,因此这里使用
apply_ alcmg_axiS()对 第 1轴上的数据循环调用 bincountO。
为了保证每行的统计结果的长度相同,
设置 minlength参数为5, apply_along_axis()会将所有关键字参数传递给进行实际运算的函数。©
使用上而的公式计算X 2 统计S cs2, © 并用 gaussian_kde()计 算 cs2 的分布情况。
Python 科学计算 (第 2 版)

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

cs2 = np.sum((counts - n/k)**2.0/(n/k), axis=l) ©


k = s t a t s .k d e .gaussian_kde(cs2) O
x = np.linspace(0, 10, 200)

p i •plot(x, stats .chi2.pdf(x, 4 ),lw=2, label=u"$\chi A{2}$ 分布_•)


pl.plot(x, k(x), lw=2, color="red__, alpha=0.6, label=u" 样本分布")
p i .l e g e n d (lo c = " b e s t ")

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)

counts2 = choose_balls([0.2]*5, 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)

print "chil =•、 chil, "pi =•、 pi


print "chi2 =•、 chi2, "p2 =", p2

chil = 11.375 pi = 0.0226576012398

chi2 = 2.55 p2 = 0.635705452704

卡方检验是通过卡方分布进行计兑的。
图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,因此不能推翻;假设,即该实验
数据中没有明显证据表明男性和女性在使阳左右手习惯上存在区別。

table = [[43, 9], [44, 4]]

chi2, P j dof, expected = stats.chi2_contingency(table)

chi2 p

1.0724852071005921 0.30038477039056899

对于上面的2 X 2 的数值较小的列联表,可以使用 fisher_exact()计算出精确的 p 值:

stats.fisher_exact(table)

(0.43434343434343436, 0 .23915695682225618)

3.5 数值积分-integrate
S c iP y数
—值 计 算 库

与本节内容对应的 Notebook 为:03-scipy/scipy-500"integrate.ipynb。


DVD

S c iP y 的 integrate 模块提供了儿种数值积分算法,其中包括对常微分方程组(O D E )的数值

积分。

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()进行数值积分,就能得到非常精确的结果:

from scipy import integrate


pi_half, err = integrate.quad(half 一circle, -1, 1)

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()计算半球体积:

volume, error = i n t e g r a t e .dblquad(half_sphere, -1, 1,


lambda x :-half 一c i r cle(x),
lambda x:half_circle(x))

print volume, error, np.pi*4/3/2

2.09439510239 2.32524566534e-14 2.09439510239

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

t = np.arange(0, 30, 0.02) # 创 建 时 间 点

# 调 用 o d e 对 lorenz进 行 求 解 , 用 两 个 不 同 的 初 始 值

trackl = odeint(lorenz, (0.0, 1.00, 0.0), t, args=(10.0, 28.0, 3.0)) ©

track2 = odeint(lorenz, (0.0, 1.01, 0.0), args=(10.0, 28.0, 3.0)) €)

图 3-25洛 伦 茨 吸 引 了 • : 微 小 的 初 值 斧 别 也 会 M 著 地 影 响 运 动 轨 迹

3.5.3 ode 类

使 用 oddntO可以很方便地计算微分方程组的数值解,只需调用一次 oddm 〇就能计算出一


组时间点上的系统状态。但是有时我们希望一次只前进一个时间片段,从而对求解对象进行更
精确的控制。在下面的例子中,我们通过 od e 类模拟如图 3-26 所示的弹簧系统每隔 lm s 周期的
系统状态,并通过 P ID 控制器控制滑块的位置。

阁 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

下面使用 odeintO对该系统进行求解,初值为滑块在位移0.0处,起始速度为0 , 外部控制


力 恒 为 1.0。如 阁 3-27所示,系统经过约两秒钟,最终停在了位移0.05米处。
S c iP y数
- 值计算库

nij b, k) F = 1.0, 10.0, 20.0, 1.0


init_status = 0.0^ 0.0
args = m, k, b, F

t = np.ar a n g e( 0 > 2, 0.01)

result = odeint(masS-Spring-damper, init_status, t, args)

04 位移

0.5 2.
通度

0.5 1.0 .5 2.

图3-27滑块的速度和位移曲线

我们希望通过控制外力 F , 使得滑块更迅速地停在位移1.0处,这时可以使用 P ID 控制器


进行控制。
在介绍 PID 控制器之前,
首先让我们用ode 类重写 odeintO模拟的部分。
ode 类和 odeintO

一样,也需要一个计算各个状态的导数的函数。
〇这里使用 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〇的完全相同。

from scipy.integrate import ode

class M a s s S p r i n g D a m p e r ( o b j e c t ) : O

def — init— (self, m, k, b, F ) :

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

du = (self.F - self.k*x - self,b*u)/self.m

return [dx, du]

system = MassSpringDamper(m=m, k=k, b=b, F=F)

init_status = 0.0, 0.0

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]

while r.successful^) and r.t + dt < 2: ©

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

出于在 While 循环中我们逐点对系统的状态进行模拟,因此可以在其中增加控制代码,改


变作川于滑块上的力,从而使滑块更迅速地停在目标位置。这里我们将采用控制理论中最常用
的控制方法:P ID 控制。采 用 P1D 控制器的系统框图如图3-28所示。

p K re{f)-

1*
t
I

D Mi)
人 ^ 一df
IT "

图3-28 P I D 控制系统
s c ip y数

图 3 -2 8 中,Setpoint为控制的目标状态,在本例中为滑块的 0 标位移,控 制 0 标系统


ftocess (即质量-弹黄-阻尼系统)的输出 Output为系统的输出,在本例屮为滑块的实际位移,二者
—值 计 算 库

的误差 Erroi•作为 PID 系统的输入。PID 控制器由三个独立的部分构成:


• P : 比例项,输出和误差成正比。
• I :积 分 项 ,输出和误差的积分成正比。
• D : 微分项,输出和误差的微分成正比。
三项之和作为 P ID 控制器的输出,在本例中控制器的输出为作用在滑块之上的外力F 。系
统框阁中使用积分和微分符号表示积分项和微分项,在实际的控制系统中,我们以一定的时间
间隔计算控制器的输出,这时积分项可以用累加变M sd f .StatUS表示,而微分项可以用当前的误
差以及前次的误差 self.last_error之间的差进行计算。
下 面 是 PID 控制器的程序,我们使用类把 kp、ki、kd、 d t 等参数、累加变量 status以及上
—次的误差 last_error等包装起来。

class PID(object):

def — init— (self, kp, ki, kd, d t ) :

self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt


self.last_error = None
self.status = 0.0

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

self.status += error * self.dt


self,last_error = error

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所示。

def pid 一control 一

s c ip y数
system(kp, ki, kd, dt, t a r g e t = 1 . 0 ) :

system = MassSp r i ng D a m p e n ( m = m > k=k^ b=b^ F=0.0)

—值 计 算 库
pid = PID(kp, kij kd, dt)
init_status = 0.0, 0.0

r = ode(system.f)

r .set_int e gr a t o r ('v o d e ', m e t h o d = 'b d f ')


r .s e t _ i n itial_value(i n it_status, 0)

t = [0]
result = [init_status]
F_arr = [0]

while r . s u ccessful() and r.t + dt < 2.0:


n.integrate(r.t + dt)

err = target - r.y[0] O


F = pid.update(err) ©

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

t, F_arr, result = pid 一control_syst:em(50.0, 100.0, 10.0, 0.001)


print u__控制力的终值:" ,F_arr[-1]

控制力的终值: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数

1.0 1.5 2.(


—值 计 算 库

控制力

1.0 1.5 2.(

图3 - 2 9 使用P I D 控制器让滑块停在位移1.0处

通 过 调 节 P I D 控制器的三个参数可以获得最佳的控制效果,这里我们使用前而介绍过的
optimize 库中的函数自动寻找最优的 P I D 参数。为了使用最优化函数,需要编写一个对控制结

果进行评价的函数。由于我们的目标是让滑块尽快地停在位移1.0处,因此可以用前两秒钟滑
块位移与0 标位移差的绝对值之和作为控制结果的评价,该值越小衮示控制得越好。为了让最
优化运行得快-些,这里将控制器的吋间间隔改为0.01秒。

%%time

from scipy import optimize

def e v a l _ f u n c ( k ) :

kp, ki, kd = k

t, F_arr, result = pid 一control 一system(kp, ki, kd, 0.01)

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",

" b o unds":[(10, 200), (10, 100), (1, 100)],


"options ":{"approx_ grad ":True }}

opt_k = optimize.basinhopping(eval 一func, (10^ 10, 10)^

niter=10,

minimizer_ k w a r g s = k w a r g s )

print opt_k.x

[199.81255771 100. 15.20382074]

Wall time: lmin 15s

下而使用优化器的输出作为P ID 的参数对系统进行模拟,可以看到控制幵始0.5秒之后滑
块已经■本上稳定在了位移1.0处,如 图 3-30所示。

kp, ki, kd = opt_k.x

t, F— arr, result = pid_control_system(kp, ki_» kd, 0.01)

idx = np.argmin(np.abs(t - 0.5))

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

与本节内容对应的 Notebook 为:03-scipy/scipy-600-signal.ipynb。

S ciP y 的 signal模块提供了信号处理方面的许多函数,包拈卷积运算、B 样条、滤波以及滤

波器设计等方面的内容。

3 . 6 . 1 中值滤波

中值滤波能够比较有效地消除声音信号中的瞬间噪声或者阁像中的斑点噪声。在 signal模
块中,
medfilt()对 一 维信号进行中值滤波,
而 medfilt2d〇对二维信号进行中值滤波。
在 scipy.ndimage
模块中另有针对多维图像的中值滤波器,这里简单演示 medfiltO的效果。
s c ip y数

t = np.arange(0, 20, 0.1)

x = np.sin(t)
—值 计 算 库

x[np.random.randint(0, len(t), 20)] += np.random.standard 一nomal(20)*0.6 O

x2 = signal.medfilt(x> 5) ©

x3 = signal.order 一filter(x, np.ones(5)> 2)

print np.all(x2 == x3)

pl.plot(t, x, label=u" 带噪声的信号•_)

pl.plot(t, x2 + 0.5, alpha=0.6, la b e l = u " 中值滤波之后的信号__)

p i .l e g e n d (l o c = "b e s t ")

True

〇酋先创建-•个带有随机的瞬间噪声的正弦波,© 然 后 调 用 medfiltO进行屮值滤波,第二
个参数为计中值的窗口大小,它必须是一个奇数。medfiltO将信号中的每个元素都替换为其窗
口内的中值。
最后绘制原始信号和滤波信号,为了便于比较,图中将滤波之后的信号统一向上偏移了 0.5,
结果如图3-31所示。中值滤波是排序滤波的一个特例。使用排序滤波可以将元素替换为芄窗U
内指定排序顺序的元素。其调用形式如下:

order 一filter(a, domain, rank)

其 中 a 是一个多维数组,domain 楚维数和 a 相同的数纟Jl ,它指定窗 U 的范围,rank 楚一个


非负整数,川来选择窗口中元素排序后的值,〇表示选择最小值,1 表示选择第二小的值。中
值滤波也可以用〇
rder_filter()计算,注 意 domain 参数是一个长度为5、值 全 为 1 的数组。
图3 - 3 1 使用中值滤波剔除瞬间噪声

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

freq = w / np.pi * sampling 一rate / 2

〇首先用 iirdesignO设计一个 U R 带通滤波器。这个滤波器的通带为0.2f〇


到 0.5f〇,阻带为小
于 O.lfo 和 大 于 0.6f〇,其-中f〇为信号取样频率的一半。如果取样频率为8kH z ,那么这个带通滤
波器的通带为800H z 到 2kH z 。通带的最大增益衰减为2d B , 阻带的最小增益衰减为40d B , 即
通带的增益浮动在2d B 之内,阻带至少有40d B 的衰减。
iirdesginO返回两个数组 b 和 a , 它们分别是 IIR 滤波器的分子和分母部分的系数。其 中 a [〇l
恒 等 于 1。© 调 用 freqzOi;
卜算所得到的滤波器的频率响应。freqzO返回两个数组 w 和 h ,其 中 w
是圆频率数组,通 过 (Of^/TC可以计算出与其对应的实际频率。h 是 w 中对应频率点的响应,它是
一个S :
数数组,其幅值表示滤波器的增益特性,相角表示滤波器的相位特性。

65
Python 科学计算(第2 版)

©计算11的增益特性,并使j+j d B 进行度量。由 于 h 中存在幅值几乎为0 的值,因此先用


clip()对其•裁剪之后,再调川对数函数,避免计算出错。
在实际运用中为了测量未知系统的频率特性,经常将频率扫描波输入到系统中,观察系统
的输出,从而计算其频率特性。下面让我们模拟这一过程:

# 产生两秒钟的取样频率为sampling_rate H z 的频率扫描信号
# 开始频率为0 ,结束频率为sampling_rate/2
t = np.arange(0, 2, 1/sampling 一rate) O

sweep = signal.chirp(t, f0=0, tl=2, fl=sampling_rate/2) 0

# 对频率扫描信号进行滤波
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))

pl.plot(freq, power, l a b e l = u "带通 IIR 滤波器的频率响应")


—值 计 算 库

pl.plot(t[index]/2.0*4000, out[index], label=u" 频率扫描波测® 的频诺" ,alpha=0.6) @

pi •legend (l o c = " best •_)

〇为了调州 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]

plant = s i g n a l .l t i (n u m erator, denominator) O

67
Python科学计算 (第 2 版)

t = np.arange(0, 2, 0.01)

x_step = plant.step(T=t) O

一,x_sin, _ = signal.lsim(plant, U=np.sin(np.pi*t), T=t) €)

传递函数的代数运算可以表示由多个连续时间系统组成的系统,例如两个系统的级联的传
递函数是各个系统的传递函数的乘积。而传递函数由分子和分母两个多项式构成,因此传递函
数的叫则运篇可以使用 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 )

如果将C (s ) * P (s )看作系统Y (s ) , 那么可以得出反馈系统的传递函数为:

© 为了让 S Y S 对象能作为 step〇、lsim()等函数的第一个表示系统的参数,需要定义_ iter_〇


魔法方法返回传递函数的分子与分母的多项式系数。

from numbers import Real

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 )

def _ mul 一 (self, s ) :

s = as_sys(s)
num = n p .p o l y m u l (s e l f .num, s.num)

den = np.polymul(self.den, s.den)


return SYS(num, den)

def _ add_ (self, s ) :

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

return SYS(num, den)

def _ sadd_ (self, s ) :


return self + s

def 一div_ _ (self, s):


s = as 一sys(s)
return self * SYS(s.den, s.num)

def — iter_ (self): ©


return iter((self.num, self.den))

下而我们用 S Y S 类计算使用 P I 控制器控制质量-弹簧-阻尼系统时的阶跃响应。P I 控制器的


传递函数为:

注意上节中介绍的 P I 控制器是离散时间的,使W 累加器近似计算积分器的输出,而本节


采用连续时间系统的系统响应模拟f e 制系统。
〇质量-弹簧-阻尼系统的传递函数为plant, © PI 控制器的传递闲数为 pi_ctrl, 为了 step()不
抛 出 L in A lg E n w 异常,这 里 将 P I 控制器的传递函数的分母常数项设置为一个非常小的值。©
计算反馈系统的传递函数feedback。由图3-35可以看出K i 为 0 时,系统的输出位移与H 标位移
之间存在一定的差距,K p越大差距越小,但萣会出现过冲现象。适当调节K p 与K f j _以减弱过
冲现象,但是仍然会有超过目标位移的时刻。

M, b, k = 1.0, 10, 20

plant = SYS([1], [M, b, k]) O

pi_settings = [(10, le-10), (200, le-10),


(200, 100), (50, 100)]
Python 科学计算(第 2 版)

fig, ax = pl.subplots(figsize=(8, 3))

for pi_setting in p i _ s e t t i n g s :

pi_ctrl = SYS(pi 一setting, [1, le-6]) ©


feedback = (pi_ctrl * plant). f ee d b a c k () ©
x = signal.step(feedbackj T=t)
label = "$K_p={:d}, K_i={:3 .format(*pi_setting)

ax.plot(t^ Xj label=label)

a x .l e g e n d (lo c = " b e s t", ncol=2)

ax •set_xlabel (u •_时间(秒)")
ax •set_ylabel (u __位移(米)" )
s c ip y数
—值 计 算 库

时阇(杪〉

图3 _ 3 5 使用PI 控制器的控制系统的阶跃响J、
V:

为了讣箅施加于质量的控制力,可以将误差信号传递给lsim〇计兑控制器的输出:

f, _ = signal.lsim(pi— Ctrl, U=l-x, T=t)

为了彻底消除过冲现象,需要使用 p r o 控制,p i d 控制器的传递函数为:


C — Kds2 + Kps + Kj
s
下而计算 P ID 控制器构成的反馈系统的阶跃响应。由于 P ID 控制器需要对输入信号进行微
分,而阶跃输入信号会导致P ID 的输出中包含脉冲输出,即时间无限短、值无限大的信号。

kd, kp, ki = 30, 200, 400

pid 一Ctrl = S Y S Q k d , kp, ki], [1, le-6])

feedback = (pid 一Ctrl * p l a n t ) . feedback()


x2 = signal.step(feedback, T=t)

为 了 让 P I D 控制器的输出在限定的范围之内,可以在反馈系统之前添加一个低通滤波器,

70
一阶低通滤波器的传递函数为: 添加低通滤波器之后,P I D 控制器的输入就是连续信号
a.s+l

了,如 图 3-36所示。

图 3-36带低通滤波器的反馈控制系统框图

lp = SYS([1], [0.2, 1])


lp 一feedback = lp * (pid 一Ctrl * plant).feedback()

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

下面根据上面的公式计算带低通滤波器的控制系统中控制器的输出:

pid_out = (pid_ctrl * lp) / (pid_ctrl * plant + 1 )

f3 = signal.step(pid_out, T=t)

图 3-37 M 示了上述 P I 控制、


P ID 控制以及带低通滤波的 P ID 控制等系统中滑块的位移以及
控制力。由于 P ID 控制的控制力存在脉冲信号,因此无法在图中正确显示。由位移曲线可以看
出低通+ PED控制可以有效抑制过冲现象。

目标系统的位移

图 3-37泔块的位移以及控制力
Python科学计算 (第 2 版)

3.7 插值- interpolate

与本节内容对应的 Notebook 为:03-scipy/scipy-700-inteipdate.ipynb。


DVD

插值是通过已知的离散数据求未知数据的方法。与拟合不同的是,要求曲线通过所有的已
知数据。S d P y 的 interpolate模块提供了许多对数据进行插值运算的函数。

3.7.1 一维插值

一维数据的插值运算可以通过inteipld ()完成。其调用形式如下,它实际上不是函数而是一
个类:

interpld(x, k i n d = ' l i n e a r ', …)

其中,x 和 y 参数是一系列已知的数据点,kind 参数楚插值类型,Vi丨以楚字符串或整数,


s c ip y数

它给出插值的 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()。

from scipy import interpolate

x = np.linspace(0, 10, 11)

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.plot(xnew, ynew, label=str(kind))

pi.legend(loc='lower right')

•s

.0 -

*5〇 2
图3-38 intcrpld的各阶插值

1.外推和 Spline 拟合

上节所介绍的 interpld类要求其参数 x 是一个递增的序列,


并且只能在 x 的取值范围之内进
行内插计算,不能用它进行外推运算,即 计 算 x 的取值范围之外的数据点。UnivariateSpliiie类
的插值运算比 interpld更高级,它支持外推和拟合运算,其调用形式如下:

UnivariateSpline(x, y, w=None, bbox=[None, None], k=3_» s=None)

• x 、 y 是保存数据点的 X - Y 坐标的数组,其 中 x 必须是递增序列。


• w 是为每个数据点指定的权重值。
• k 为样条刖线的阶数。
• s 是平滑系数,它 使 得 最 终 生 成 的 样 条 曲 线 满 足 条 件 :•(yi - splir^ Xi)))2 < s 。
即当 s > 0 时,样条曲线并不一定通过各个数据点。为了让曲线通过所有数据点,必须
将 s 参数设置为 0 。此夕卜,还可以使用 InterpolatedUnivariateSpline类,它 与 UnivariateSpline
的唯一区別就是它通过所有的数据点,相当于将 s 设置为0。
下面的程序演示了使用IMvariateSpline 对数据进行插值、夕卜推以及样条曲线拟合:
O 如图3-39(上)所示,UnivariateSpline 能够进行外推运算,虽然输入数据中没有 X 轴大于
10的点,但是它能计算出 X 轴 在 0 到 12的插值结果。在 X 轴 大 于 10的部分,样条曲线仍然呈
现出和正弦波类似的形状,越远离输入数椐范围,误差会越大,因此外推的范幽是有限的。由
于 s 参数为0 , 因此插值|lf|线经过所有的数据点。
©图3-39(下)则显示了 s 参数不为零时的结果,对于带噪声的输入数据,选择合适的 s 参数
Python 科学计算(第2 版)

能够使得样条曲线接近无噪声时的波形,可以把它看作使用样条曲线对数据进行拟合运算。

xl = np.linspace(0, 10, 20)


yl = np.sin(xl)
sxl = np.linspace(0, 1 2 ,100)
syl = i n t e r p o l a t e •UnivariateSpline(xl, yl, s=0)(sxl) O

x2 = np.linspace(0, 20, 200)


y2 = np.sin(x2) + n p .r a n d o m .standar d _n o r m a l (l e n ( x 2 ))*0.2

sx2 = np.linspace(0, 20, 2000)


spline2 = interpolate.UnivariateSpline(x2, y2, s=8) ©

sy2 = spline2(sx2)

pl.figure(figsize=(8, 5))
p i . s u b p l o t (211)

pl.plot(xl, yl, label=u" 数据点_•)


出1线’_)
s c ip y数

pl.plot(sxl, syl, label=u"spline

p i . l e g e n d ()
—值 计 算 库

p i . s u b p l o t (212)

pl.plot(x2, y2, label=u" 数据点_•)


pl.plot(sx2, sy2, linewidth=2, label=u"spline 帅线")
pl.plot(x2, np.sin(x2), label=u" 无噪声曲线")

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 个交点的横坐标:

print np.arnay_str( spline2.roots(), precision=B )

[3.288 6.329 9.296 12.578 15.75 18.805]

如果需要计算丨[丨1线与任意横线的交点,可以事先将曲线的拟合数据在Y 轴方向上进行平移。
但若要计算与多条 y = c 横线的交点,则需要对同样的数据进行平移和拟合多次。
O 下 而 的 r〇
ot_ at()通过直接修改拟合曲线的参数,实现曲线的平移,从而可以计算与任意
横线的交点。© — r〇
〇ts_ at()动态添加为 UnivariateSpline炎的方法。© 对多条横线求交点,并进
行绘图,其结果如阁3^40所示。

def roots_at(self, v): O

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 ©

pl.plot(sx2, sy2, linewidth=2, label=u"spline 曲线")

ax = pl.gca()

for level in [0.5, 0.75, -0.5, -0.75]:


ax.axhline(level, ls=":", color="k")

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.参数插值

前面介绍的插值函数都耑要X 轴的数据是按照递增顺序排列的,就像一般的 y = f (x )函数曲


线一样。数学上还有一利1参数曲线,它使用参数 t 和两个函数 x = f (t)、y = g (t)来定义二维平面上
的一条曲线,例如圆形、心形等曲线都楚参数曲线。参数曲线的插值可以通过splprepO和 splev〇
实现,这组函数支持高维空间的曲线的插值,这里以二维曲线为例介绍其用法。
〇首先调用 splprep〇,其第一个参数为一组一维数组,每个数组是各点在对应轴上的坐标。
s 参数为平滑系数,与 UnivariateSpline 的含义相同。splprepO返回两个对象,其 中 tc k 是一个元
组,它包含了插值曲线的所有信息。t 是自动计算出的参数曲线的参数数组。
© 调 用 splevO进行插值运箅,其第一个参数为一个新的参数数组,这M 将 t 的取值范围等
分 200份 ,第二个参数为 splprepO返回的第一个对象。实际上,参数数组 t 是正规化之后的各个
线段长度的累计,因此 t 的范围为0 到 1。
其结果如图3 4 1 所示,图中比较了平滑系数为0 和 le - 4 时的插值肋线。当平滑系数为0
时,插值曲线通过所有的数据点。
s c ip y数

X =[ 4.913, 4.913, 4.918, 4.938, 4.955, 4.949, 4.911,


—值 计 算 库

4.848, 4 . 864, 4.893, 4.935, 4.981, 5.01 , 5.021]

y = [ 5.2785, 5.2875, 5.291 , 5.289 , 5.28 5.26 5.245 ,


5.245 , 5.2615, 5.278 , 5.2775, 5.261 , 5.245 , 5.241]

pl.plot(x, y, "〇")

for s in (0, le-4):

tck^ t = interpolate.splprep([x, y], s=s) O


xi, yi = interpolate.splev(np.linspace(t[0]^ t[-l], 200)^ tck) ©

pl.plot(xij yi, lw=2j label=u"s=%g" % s)

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]

y = [1, 2, 1.5, 2.5, 3, 2.5]


xs = np.linspace(x[0], x[-l], 100)
curve = interpolate.pchip(x, y)

ys = curve(xs)
dys = curve.derivative(xs)

pi.plot(xs, ys, label=u"pchip")

pl.plot(xs, dys, label=u"— 阶导数•’



pl.plot(x, y, "〇
•’

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

def func(x, y): O

return (x+y)*np.exp(-5.0*(x**2 + y**2))

# X - Y 轴分为1 5 * 1 5 的网格
y ,x = np.mgrid[-l:l:15j, -l:l:15j] ©

fvals = func(x, y) # 计算每个网格点上的函数值

# 二维插值
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类进行二维插值

O llin c 是计算|1丨丨而上各点高度的函数。© 计 算 X 、Y 轴在- 1 到 1 范围之内,大 小 为 15X 15


的等间距网格上各点的高度。注意所得到的二维数组 fVals 的 第 0 轴 与 Y 轴对应,第 - 轴 与 X
轴对应。© 使用网格上各点的 X 、Y 和 Z 轴的坐标创建 interp2d 对象,这里我们使用二阶插值
曲面。Ointeip 2d 对象可以像函数-样调用,
我们用它计兑插值曲面在一个觅密网格屮的高度值。
注意这里的参数是两个一维数组,分別指定网格的 X -Y 轴坐标,而不耑要通过 mgrid 创建网格
坐标数组。

1•griddata

interp2d 类只能对网格形状的取样值进行插值运算,如果需要对随机散列的取样点进行插
值,则可以使用 griddataO。其调用形式如下:

griddata(points, values, xi, method: ' li n e a r ' , fill value=nan)

其 中 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)

yg, xg = np.mgrid[-l:l:100j, -l:l:100j]


xi = np.c_[xg.ravel(), y g . r a v e l 〇]

methods = 'nearest', 'linear', 'cubic'

zgs = [i n t e r p o l a t e .g r i d d a t a ((x^ y), z, xi, m e t h o d = m e t h o d ) .r e s h a p e (100, 100)

for method in methods]

.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.径向基函数插值

径向•签函数(radial basis function)插值算法也可以用于商维随机散布点的插值。所谓径向签函


数,是指函数值只与某特定点的距离相关的一类函数cKllx - x ilD,其中x i是嫉个给定取样点的

79
Python 科学计算(第2 版)

坐标。使州这些中函数,可以近似表示 N 维空间中的函数:
N
y (x ) = ^ W i c|)(|| X - X i ||)
i=l

为了方便读者理解 R B F , 下面先看一个一维插值的例子,结 果如图 3 4 5 所示。图中显示


了三利吨函数对应的插值曲线:multiquadric、gaussian 和 linear。

from scipy.interpolate import Rbf

xl = np.array([-lj Q, 2.0^ 1.0])

yl = n p . a r r a y ([1.0, 0.3, -0.5, 0.8])

funcs = [ 'multiquadric', 'gaussian 、 'linear']


nx = np.linspace(-3. A, 100)

rbfs = [Rbf(xl, yl, function=fname) for fname in funcs] O

rbf_ys = [rbf(nx) for rbf in rbfs] O


冬I3 4 5 — 维 R B F 插值

〇使用表示取样点的 x 和 y 数组创建一个 rb f 对象,并通过 function 参数指所使用的径向


基函数。
©狀 对 象 可 以 像 函 数 -样 被 调 用 ,
我们用它计兑以 n x 为横坐标对应的插值曲线的值。
rb f 对象的 nodes 属牲保存 Wi系数:

for fname, rbf in zip(funcs, r b f s ) :


print fname, rbf.nodes

multiquadric [ -3.79570791 9.82703701 5.08190777 -11.13103777]


gaussian [ 1.78016841 -1.83986382 -1.69565607 2.5266374 ]

linear [-0.26666667 0.6 0.73333333 -0.9 ]

下面的程序演示二维径向基函数插值,效果如图3 4 6 所示。

rbfs = [Rbf(x, y, z, function=fname) for fname in funcs]

rbf 一zg = [rbf(xg, y g ) .reshape(xg.shape) for rbf in rbfs]


/ S 〇° 0 ^ 0 〇
° °〇°〇〇 ^

1.0 -0l5 OA O.S \J


〇 Mi -0.5 00
muttiQuddfic linear

图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参数指定径向丛函数中数据点的作用范_丨

3.8 稀i^@ ^-sparse

与本节内容对应的 Notebook 为:03-scipy/scipy-810-sparse.ipynb。

在科学与工程领域求解线性模型时经常出现许多大型的矩阵,这些矩阵中大部分的元素都
为 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 稀疏矩阵的存储形式

scipy .sparse 中提供/ 多利1表示稀疏矩阵的格式,每利1格式都有小同的用处,其 中 dok_matrix


和 lil_matrix 适合逐渐添加元素。
d〇
k_ matrix 从 d iet 继承而来,它采用字典保存矩阵中的非零元素:字典的键是一个保存元

素(行,
列)信息的元组,其对应的值为矩阵中位于(行,
列)中的元素值。显然字典格式的稀疏矩阵
很适合单个元素的添加、删除和存取操作。通常用来逐个添加非零元素,然后转换成K 他支持
快速运算的格式。
s c ip y数

from scipy import sparse


—值 计 算 库

a = s p arse.dok_matrix((10, 5))

a[2:5, 3] = 1.0, 2.0, 3.0


print a.keys()
print a . v a l u e s ()

[(2, 3 ) , ( 3 , 3), (4, 3)]


[1.0, 2.0, 3.0]

Iil_ matrix 使用两个列表保存非零元素。data 保存每行中的非零元素,row s 保存非零元素所

在的列。这种格式也很适合逐个添加元素,并且能快速获取行相关的数据。

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

[[][] [1.0] [3.0, 2.0] [][][][][][]]

[[][][3] [2, 4 ] [ ] [ ] [ ] [ ] [ ] [ ] ]

coo_ matrix 采三个数纟 J iro w 、c o l 和 data 保存非零元素的信思。这三个数纟J1的长度相同,


ro w 保存元素的行,c d 保存元素的列,data 保存元素的值。
co 〇
_matrix 不支持元素的存取和增册IJ,
一旦创建后,除了将之转换成其他格式的矩阵,几乎无法对其做任何操作和矩阵运算。
coo_m atrix 支持重复元素,即同一行列坐标可以出现多次,当转换为其他格式的矩阵时,
将对同一行列坐标对应的多个值进行求和。在下而的例子中,(
2,3)对应两个值:1和 10。在将
-其转换为ndairay 数组时把这两个值加在一起,所以最终矩阵中(2,3)坐标上的值为11。
许多稀疏矩阵的数据都是采川这种格式保存在文件中的,例 如 某 个 C S V 文件中可能有这
样三列:“用 户 ID ,商 品 1D ,评价值”。采 用 numpy.loadtxt或 pandas.read_c s v 将数据读入之后,
可以通过 o x )_matrix 快速将其转换成稀疏矩阵:矩阵的每行对应一位用户,
每列对应一件商品,
而元素值为用户对商品的评价。

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 最短路径

稀疏矩阵 w 可以用于表示图,w [i,


j l 保存图中节点 i 和节点 j 之间路径的权值。若节点 i 与

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

edges = [(0, 1, 10), (1, 2, 5), (0, 2, 3),


(2, 3, 7 ) } (3, 0, 4), (3, 2, 6)]

for i, 〕、 v in edges:
w[i, j] = v

w.todense()
Python 科学计算 (第 2 版)

matrix([[ 0., 10。 3., 0.],

[ 0., 0., 5., 0.],

[ 0., 0., 0., 7.],

[ 4., 0., 6 ., 0.]])

使 用 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数

eXj ey = (398, 25)


bimg = np.all(img > 0.81,axis=2) O
—值 计 算 库

H, W = bimg.shape

x0, xl = np.where(bimg[H//2, :]==0)[0][[0,-1]] ©


bimg[H//2, :x0] = 0
bimg[H//2, xl:] = 0

我们将迷宫中所有的像素都当作图中的节点,节点序号与像素坐标(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

values = np.ones ( ed g e s . s h a p e [0])


w = sparse.coo_matrix((values, (edges[:, 0], e d g e s [:, 1])), 〇

shape=(bimg.size, bimg.size))

接下来导入 csgraph 模块,并调用 dijkstra〇计算从编号为 startid的节点出发到达所有其他节


点的最短路径。
directed 参数为 False 表示无向阁,
为了计算最短路径,
需要设:1! retum_predecessors
参数为 Tm e 。所返回的数组 d 和 p 的形状为(indices参数的长度,图的总节点数)。

from scipy.sparse import csgraph

startid = sy * W + sx
endid = ey * W + ex

dj p = csgraph.dijkstra(w, indices=[startid], return_predecessors=True, directed=False)

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

node 一id = p[0, node 一id]


path = np.array(path)

最后,在原来的迷宫图像中将path 经过的像素涂黑,得到图349(左)中的路径。

85
Python 科学计算(第2 版)

图 3 4 9 用 dijkstm计算最短路径
s c ip y数
—值 计 算 库

scpy 2.scipy .hrd_solver 使 用 csgrap h 计 算 华 容 道 游 戏 “


横 刀 立 马 ”布局步数最少的
解法。

在上而的迷宫中,两个相邻白色像素之间的路径权值均为1 , 因此搜索到的最佳路径为最
短路径。而许多游戏地图中的路径搜索会考虑地形因素,这时可以根椐不同的地形设®不同大
小的路径权值,这样最伟路径就是使所有权值之和最小的路径。

3 . 9 图像处理-ndimage

与本节内容对应的 Notebook 为:03-scipy/scipy-900-ndimage.ipynb。

scipy .ndimage 是一个处理多维图像的函数库,其中又包括以下儿个模块:


• f i l t e r s 图像滤波器。
• fourier:傅立叶变换。
• interpolation: 图像的插值、旋转以及仿射变换等。
• measurements:图像相关倍息的测量。
• morphology: 形态学图像处理。
更强大的图像处理库
s c ip y .n d im a g e 只提供了一些基灿的图像处理功能,下面是一些更强大的图像处理库:
• O p e n C V : 它 是 使 用 C /C ++ 开发的计算机视觉库 , 本书将用一整章的篇幅介绍 O p e n C V

提 供 的 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)

tmp = np. repeat (np. repeat (img size, 0 ) ,


size, 1)
o u t [:y :] = n p . w here(tmp> value, out)
out[::size,:] = 0
o u t [ ::size] = 0
return out

def show 一image(*imgs):


for idx, img in enumerate(imgs, 1):
ax = p i . s u b p l o t (1, len(imgs), idx)
pl.imshow(imgJ cmap=__gray")

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膨胀处理添加进物体的像素。

from scipy.ndimage import morphology

def dilation 一demo(a, s t r u c t u r e= N o n e ) :


b = morphology.binary_dilation(a> structure)

img = expand_image(a, 255)


return expand_image(np.logical_xor(a,b), 1 5 0 ,out=img)

p l . i mread("scipy_morphology_demo.png")[:^:,0].astype(np.uint8)
imgl = expand 一image(a, 255)

img2 = dilation 一demo(a)


img3 = dilatiori-demc^aj [[1,1,1], [1 山 1], [1,1,1]])
s h o w _ i m a ge (i m g l , img2, img3)
S c iP y数
- 值计算库

■ H H

图 3-50 四连通和八连通的膨胀运算

四连通包括上下左右4 个像素,而八连通则还包括4 个斜线方向上的邻接像素。它们都可


以使用下面的正方形矩阵定义,其屮正中心的元素表示当前要进行运兑的像素,而其周围的1
和 0 表示对应位置的像素是否算作其“周围”像素。这种矩阵描述了周围像素和当前像素之间
的关系,被称作结构元素(structuring element)。

四连通八连通
0 10 111
111 111
0 10 111

假设数组 a 是-个表示二值图像的数组,可以用如下语句对其进行膨胀运兑:

binary 一
dilation(a)

biiiary_dilation〇默认使川四连通进行膨胀运兑,通 过 structure参数 n丨以指定;II;


他的结构元素。
下面是进行八连通膨胀运算的语句:
b in a ry _ d ila tio n (a , s tru c tu re = [[ 1 , 1 , 1 ], [1 , 1 ,1 ] ,[ 1 ,1 , 1 ]] )

通过设置不N 的结构元素,能够实现各种不同的效果,图 3-51M 示了三种不N 结构元素的


膨胀效果。图中的结构元素分别为:

左中右
000 0 10 010
111 0 10 0 10
0 0 0 010 0 0 0

img4 = dilation 一demo(a, [[0,0,0],[1,1,1],[0,0,0]])


img5 = dilation_demo(a, [[0,1,0],[0,1,0],[0,1,0]])

img6 = dilation 一demo(a, [[0,1,0],[0,1,0],[0,0,0]])

s h o w _ i m a ge (img4, img5, img6)

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

return expand_image(np.logical 一xor(a,b), 100, out=img)

imgl = expand_image(a, 255)


img2 = erosion 一demo(a)
img3 = erosion_demo(a, [[1,1,1],[1,1,1],[1,1,1]])

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()的调用

形式如下:

b i nary_hit_or_miss(i n p u t , structurel=None, structure2=None^ …)

其 中 stnicturel 参数指定白色像素的结构元素,而 structure2 参数则指定黑色像素的结构元


素。图 3-53是 bimuy_hit_〇r_mks ()的运算结果。其中左图为原始图像,中图为使用下面两个结
构元素进行运算的结果:

闩色结构元尜黑色结构元尜
000 100
010 000
s c ip y数

111 000
—值 计 算 库

图 3-53 Hit和 Miss运 算

在这两个结构元素中,0 表示不关心其对应位置的像素的颜色,1 表示其对应位置的像素


必须为结构元素所表示的颜色。因此通过这两个结构元素可以找到“下方三个像素为白色,并
且左上像素为黑色的白色像素”。
与右阍对应的结构元素如下,通过它可以找到“下方三个像素为白色、左上像素为黑色的
黑色像素”。

白色结构元索 黑色结构元索
0 00 10 0
000 0 10
111 0 00

def hitmiss_demo(a, structure:!, s t r u c t u r e 2 ) :


b = morphology.binary 一hit 一
or 一
miss(a, structurel, structure2)

img = expand_image(a, 100)

return expand_image(b, 255, out=img)


imgl = expand_image(a, 255)

img2 = hitmiss_demo(a, [[0,0,0],[0,1,0],[1,1,1]], [[1,0>0],[0,0,0].[0,0,0]])

img3 = hitmiss— demo(a, [[0,0,0],[0,0,0],[1,1,1]], [[1,0,0],[0,1,0],[0,0,0]])

show_image(imgl, img2, img3)

使 用 H it 和 M iss 运算的组合,可以实现复杂的图像处理。例如文字识别中常用的细线化运
算就可以用一系列的 H it 和 M iss 运算实现。图 3-54显示了细线化处理的效果,实现程序如下:

def s k e l e t o n iz e (i m g ) :

hi = np.array([[0, Q, 0],[0, 1, 0]>[1> 1, 1]]) 〇


ml = np.array([[l, 1, 1],[0, 0, 0],[0, 0, 0]])

h2 = np.array([[0, 0, 0],[1, 1, 0],[0, 1, 0]])

m2 = np.array([[0, 1, 1],[0, 0, 1],[0, 0, 0]])

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

for hit, miss in zip(hit 一list, miss 一list):

hm = morphology.binary_hit_or_miss(img, hit, miss) ©

# 从图像中删除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 版)

图3 - 5 4 使用Hit和 Miss 进行细线化运箅

O 以图3-55所示的两个结构元素为基础,构造4 个形状为(3, 3)的二维数组:h i 、m l 、h2、


m2。其 中 h i 和 m l 对应图中左边的结构元素,而 h2 和 m2 对应图中右边的结构元素,h i 和 h2
对应白色结构元素,m l 和 m2 对应黑色结构元素。© 将这搜结构元素进行90°、180°、270°旋转
之后一共得到8 个结构元素。
€)依次使用这些结构元素进行H it 和 M iss 运算, © 并从图像屮删除运兑所得到的白色像素,
苒效果就是依次从8 个方向删除图像的边缘上的像素。© 重复运算直到没有像素可删除为止。

白色结沟元素 白色结构元素
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 图像分割

下面以矩形区域识别为例,介绍如何使用 measurements和 morphology 进行图像区域分割。


我们要杣取矩形信息的图像如图3-56所示。这个图像是二值图像,其:巾矩形R 域为白色,背景
为黑色。但是由于它采用 JPEG 格式储存,因此用 pyplot.imread〇读取的是一个形状为(高,宽,3)
的三通道图像。下面的程序使用芄中的第0 通道将其转换成二值数组squares,将矩形区域设置
为 1,将背M 设置为0。结果如图3-56(左上)所示。

squares = pi.imread("suqares.jpg")

squares = (squares[ :,0] < 200) ,astype(np.uint8)

由于许多矩形都有一些小凸起与邻近的矩形连在一起,我们需要先将每块矩形与其周围的
矩形分离出来。可以使)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(中上)所示。

from scipy.ndimage import morphology

squares_dt = morphology.distance_transform_cdt(squares)

print •• 各种距离值" , np.unique(squares_dt)

各种距离值 [ 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]

只需要选择合适的阈值将距离数组squares_d t 转换成二值数组,就可以实现矩形R 域的分


离,其效果和腐蚀算法类似。squares_C〇
r e 中每个矩形区域都缩得足够小,以至于没有任何两块
区域之间有连通的路径。.其效果如图3-56(右上)所示,可以看到所有的区域都和其相邻的区域

s c ip y数
分割开了。

—值 计 算 库
squares_core = (squares_dt > 8 ) ,astype(np.uint8)

下面调用 measurements中 的 label〇和 center_ofLmess()对各个独立的白色区域进行染色,并


计算各个区域的重心坐标。
labds 〇对二值阁像中每个独立的白色区域使用唯一的整数进行填充,这相当于将每块区域

染上不同的“颜色”。这M 所 谓 的 “颜色”是指为每个区域指定的一个整数,而不是指图像中
像素的真正颜色。
labelsO 返 冋 的 结 果 数 组 可 以 用 于 计 算 各 个 R 域的一• 些统计信息。例 如 可 以 调 用
Center_of_masS〇计 每 个 R 域的重心。其第一个参数为各个像素的权值(可以认为是每个像素的
质量密度),第二个参数为 labelsO输出的染色数组,第三个参数为需要计兑重心的区域的标签列
表。在下面的程序中,权值数组和染色数纟11相同,因此可以计箅区域中所有內色像素的重心。
如 果 直 接 使 imshow()显 示 labelsO输出的染色数纟11,将会得到一个区域颜色逐渐变化的图
像,这样的图像不利于肉眼识别各个区域。冈此卜而的程序用 random_palette()为每个整数分配
一个随机颜色。其结果如图3-56(左下)所示,每个区域的重心使用白色小圆点表示。

from scipy.ndimage.measurements import label, center 一


of_mass

def randomj3alette(labels, count, s e e d = l ) :


n p .r a n d o m .s e e d (s e e d )

palette = np.random.rand(count+1, 3)
p a l e t t e [0,:] = 0

return palette[labels]

labels, count = label(squares_core)


9
3
Python科学计算(第2 版)

h., w = labels.shape

centers = np.array(center 一
of 一
mass(labels, labels, index=range(l, count+1)), np.int)

cores = random_palette(labels, count)

我们儿乎完成了区域识别的任务,但是每个矩形区域都比原始图像中的要小一圈。下面将
染色之后的矩形区域恢复到原始图像中的大小。即对于原始图像中为白色、腐蚀之后的图像中
为黑色的每个像素,将其颜色设置为染色数组屮与之最近的区域的颜色。具体的计兑步骤如下:
® 将 腐 蚀 之 后 的 图 像 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

near— labels = labels[index[0], index[l]] ©

mask = (squares - squares 一core).astype(bool)


labels2 = label s . c op y ()
labels2[mask] = near_labels[mask] €)

separated = random_palette(labels2, count)

图3 - 5 6 矩形区域分割算法各个步骤的输出图像
3 . 1 0 空间算法库-sp a tia l

与本节内容对应的 Notebook 为:03-scipy/scipy-A00~spatial.ipynb。

木节介绍 scipy.spatial模块中提供的 K -d 树、凸包、沃罗诺伊图、德劳内三角化等空间算法


类的使用方法。

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

类似的兑法可以推广到N 维空间,spatial模块提供的 cK D Tree 类使用 K -d 树快速搜索 N 维


空间中的最近点。在下面的例子中,W 平面上随机的 100 个点创建 cK D Tree 对象,并对其搜索
与 targets中每个点距离最近的3 个点。cKDTree.queryO 返丨H]两个数纽 dist 和 idx, dist[i, :〗
楚距离
targets[i]最近的 3 个点的距离,而 idx[i, :]是这些最近点的K标:

from scipy import spatial


n p .r a n d o m .s e e d (42)
N = 100
points = np.random.uniform(-l> 1, (N, 2))

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 = kd.query(targets, 3)

dist idx

[[0.15188266, 0.21919416, 0.27647793], [[48, 73, 81],


[0.09595807, 0.15745334, 0.22855398], [37, 78, 43],
[0.05009422, 0.17583445, 0.1807312 ], 1;
79, 22, 92],

[0.11180181, 0.16618122, 0.18127473], [35, 58, 6],

[0.19015485, 0.19060739, 0.19361173]] [83, 7, 42]]

:KDTnee.query_ball_point〇搜索与指定点在一定距离之内的所有点,它只返回最近点的下标,

95
Python科学计算 (第 2 版)

凼于每个目标点的近旁点数不一定相同,因此 idx2 数组中的每个元素都是一个列表:

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)

cKDTree .query_pairs〇找 到 points 中距离小于指定值的每一对点,它返回的是一个下标对的


集合对象。下面的程序使用集合差集运緙找出所有距离在0.08到 0.1之间的点对:

idx3 = k d .q u e r y j D a ir s (0.1) - k d .q u e r y _ p a i r s (0.08)


idx3

{(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个 £9»由 澶 供 &颺 在 0 2 之内的硒有£ 好成 羶霣闭隽距颺 S 0 . 0 8 » C M 之网的由料

图3 - 5 7 用 c K D T r c e 寻找近旁点

cK D Tree 的所有搜索方法都有一个参数 p,用于定义计算两点之间距离的函数。读者可以


尝试使用不同的 p 参数,观察图3-57的变化:
• p = l : 绝对值之和作为距尚
• p = 2 : 欧式距尚
• p = np.inf: 最大坐标差值作为距离
此外,cKDTree.query_ball_tree〇可以在两棵K -d 树之间搜索距离小于给定值的所有点对。
distance子模块中的 pdist〇计算一组点中每对点的距离,而 cdist〇计算两组点中每对点的距
离。由于 pdist()返回的是一个压缩之后的一维数组,需要用 squareformO将其转换成二维数组。
distl[i,j]是 points中下标为 i 和 j 的两个点的距离,dist2[i,
j] 是 points[i]和 targets[j]之间的距离。

from scipy.spatial import distance


distl = distance.squareform(distance.pdist(points))

dist2 = distance.cdist(points, targets)


%
d is tl.s h a p e dist2.shape

(100, 100) (100, 5)

下而使用 np.min〇在 dist2 中搜索 points 中与 targets距离最近的点,


其结果与 cKDTree.queryO
的结果相同:

print dist[:, 0] # cKDTree.query() 返回的与 targets 最近的距离


print np.min(dist2, axis=0)

[0.15188266 0.09595807 0.05009422 0.11180181 0.19015485]


[0.15188266 0.09595807 0.05009422 0.11180181 0.19015485]

为了找到 points 中最近的点对,需要将 d is tl 对角线上的元素填充为无穷大:

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 树中搜索它自己包含的点,
找到勹毎个点最近的两个点,其中距离最近的点就是它木身,距 离 为 〇,而距离第二近的点就
是每个点的最近旁点,然后只需要找到这些距离中最小的那个即可:

dist, idx = kd.query(points, 2)


print i d x [ n p . a r g m i n ( d i s t [ 1])], n p . m i n ( d i s t [ 1])

[22 92] 0.00534621024816

让我们看一个使用 K -d 树提高搜索速度的实例。下而的 start和 end 数组保存用户登录和离


开网站的时间,对于任怠指定的时刻time, 计算该时刻在线用户的数M 。

N = 1000000

start = np.random.uniform(0, 100, N)

span = np.random.uniform(0.01, 1, N)
span = np.clip(span, 2, 100)

end = start + span

下面的 naive_count_at()采用逐个比较的方法计算指定时间的在线用户数量:

def naive_count_at(start, end, t i m e ) :


mask = (start < time) & (end > time)

return np.sum(mask)

图 3-58显示了如何使j[J K -d 树实现快速搜索。图中每点的横坐标为start, 纵坐标为 end。


由于end 大于 start,
因此所相的点都在y = x 斜线的上方。
图中阴影部分表示满兄(start < time)& (end

97
Python科学计算 (第 2 版)

> time)条件的区域。该区域中的点数为时刻tim e 时的在线人数。

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 为计算

距离的范数。距离按照如下公式计算:

Np( x ) =丨丨 x ||p= (|x」 p + |x2|p + … + |xn|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 ) :

def — init_ (self, start, end, l e a f s i z e = 1 0 ) :

self.tree = spatial.cKDTree(np.c_[start, end]_» leafsize=leafsize)

self.max_time = np.max(end)

def count 一
at(self, t i m e ) :

max time = self.max time


to_search = s p a t i a l .c K D T r e e ( [ [time - max_time, time + max_time]])

return self .tree.count_neighbors (to_search, max_time_» p=np.inf)

naive_count_at(startj end^ 40) == K dSearch(start, e n d ) .count_at(40)

下面比较运算时间,由结果可知创建 K <1树需要约0.5秒时间,K -d 树的搜索速度则为线


性搜索的17倍左右。

请读者研究点数 N 和 leafsize 参数与创建 K -d 树和搜索时间之间的关系。

%time ks = KdSearch(start, end, leafsize=100)

%timeit naive_search(start, end, 40)

%timeit k s .c o u n t _ a t (40)

Wall time: 4 8 4 ms

100 l o o p s , best of 3: 3.85 ms per loop

1000 loops, best of 3: 221 \xs per loop

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

[[2, 5], [5, 2, 6, 1, 0]


[ 2 , 6 ],
[0, 5],
[1, 6],
[1, 0]]

使 用 matplotlib中的 Polygon 对象可以绘制如图3-59所示的多边形。


Python 科学计算(第2 版)

poly = p i . P o l y g on (points2d[ch2d.vertices], fill=None, lw=2, color="r__, alpha=0.5)

ax = pi.subplot(aspect="equal")

pi.plot(points2d[:, 0], points2d[:, 1], "go")


for i, pos in e n u m e r a t e (p o i n t s 2 d ):
pl.t e x t ( po s [ 0 ],pos[l], str(i), color="blue")
ax.add_antist(poly)

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

下面的程序用 T V T K 立观地显示凸包,图 3-60中所有的绿色圆球表示 p o in te d 中的点,


由红色线段构成的三角形面表示凸多而体上的面。没有和红色线段相连的点就是凸包之内
的点:

from scpy2 import vtk_convexhull, vtk_scene, vtk_scene_t 。


一array
actors = vtk_convexhull(ch3d)

scene = vtk_scene(actors, viewangle=22)


%array_imagevtk_scene_to_array(scene)

scene.close()
s c ip y数
—值 计 算 库
如果读者希望采用三维交互界而,可以在 Notebook 中执行如下代码:

%gui qt

from scpy2 import ivtk_scene

i v t k _ s c e ne (a c t o r s )

3 . 1 0 . 3 沃罗诺伊图

沃罗诺伊图(Vomnoi Diagram)是一种空间分割算法,它根裾指定的 N 个胞点将空间分割为


N 个区域,每个区域中的所有坐标点都与该区域对应的胞点最近。

points2d = np.array([[0.2, 0.1], [0.5, 0 . 5 ],[0.8 ,0 . 1 ],


[0.5, 0.8], [0.3, 0.6], [0.7, 0.6], [0.5, 0.35]])
vo = spatial.Voronoi(points2d)

vo.vertices vo.regions v o .ridge_vertices

[[0.5 , 0.045 ], [[-1, 0, 1], [[-1, 0],


[0.245 , 0.351 ], [-1, 0, 2]> [0, 1],
[0.755 , 0.351 ], [], [-1, 1],
[0.3375, 0.425 ], [6, 4, 3, 5], [… 2],

[0.6625, 0.425 ], [5,-1, 1, 3], [-1, 2],

[0.45 , 0.65 ], [4, 2, 0, 1, 3], [3, 5],

[0.55 , 0.65 ]] [6,-1, 2, 4], [3, 4],

[6,-1, 5]] [4, 6],

[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数

成胞点的颜色。
- 值计算库

bound = np.array([[-100, -100], [-100, 100],


[100, 100], [ 100, -100]])
vo2 = spatial.Voronoi(np.vstack((points2d> bound)))

图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 vo.point 一region

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

convexhull 一mask = [poly.contains_point(p, radius=0) for p in vs] ©

vert ice__point一
map = defaultdict (list) €)

for indexjx>int, index 一region in e n u m e r a t e (v o •p o i n t _ r e gi o n ):

region = vo.regions[index_region]

if -1 in r e g i o n : continue

for index_vertice in region:

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 )

def dist(pl, p2):

return ((pl-p2)**2).sum()**0.5

max 一cicle = max((dist(points2d[pidxs[0]], vs[vidx]), vs[vidx]) O

for vidx, pidxs in v e r t i c e_point_map.iteritems 〇)


/
»

20
Python 科学计算 (第 2 版)

r, center = max 一cicle

print "r = ", r, ", center = ", center

r = 0.174278456762 , center = [ 0.46973363 0.59356531]

).6

>•4 •
#

).2 • •

)〇
oo 0.4 3.6

图 3 4 2 使用沃罗进伊•图汁算 S 大空岡

3 . 1 0 . 4 德劳内三角化

德劳内三角化算法对给定的点集合的凸包进行三角形分割,使得每个三角形的外接圆都不
含任何点。下面的程序演示了德劳内三角化的效果:
Delaunay 对 象 d y 的 simplices 属性是每个三角形的顶点在points2d 中 的 K标。可以通过三角
形的顶点坐标计算其外接圆的圆心,不过这里使用同一点集合的沃罗诺伊图的 vertices 属性,
vertices 就是每个三角形的外接圆的圆心。

x = n p . a r r a y ( [46.445, 263.251, 174.176, 280.899, 280.899,


189.358, 135.521, 29.638, 101.907, 226.665])

y = np.array([287.865> 250.891, 287.865, 160.975, 54.252,


160.975, 232.404, 179.187, 35.765, 71.361])

points2d = np.c 」x, y]

dy = spatial.Delaunay(points2d)
vo = spatial.Voronoi(points2d)

dy.simplices vo.vertices

[[8, 5, 7], [[104.58977484, 127.03566055],

[1, 5, 3], [235.1285 , 198.68143374],

[5, 6, 7], [107.83960707, 155.53682482],

[6, 0, 7], [71.22104881, 228.39479887],

[〇, 6, 2], [110.3105 , 291.17642838],

[6, 1, 2], [201.40695449, 227.68436282],

[1, 6, 5], [201.61895891, 226.21958623],


[9, 5, 8], [ 152.96231864, 93.25060083],

[4, 9, 8], [ 205.40381294, -90.5480267 ],


[5, 9, 3], [ 235.1285 ,127.45701644],

[9, 4, 3]] [ 267.91709907, 107.6135 ]]

下而是用 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 ) :

px, py = points2d[dy.simplices[i, 0]]


radius = np.hypot(cx - px, cy - py)

circle = pl.Circle((cx> cy), radius, fill=False, ls="dotted")

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 快速绘图

与本节内容对应的 Notebook 为: 04-matplotlib/matplotlib-100-fastdraw.ipynb〇

matplotlib采用而向对象的技术来实现,因此组成阁表的各个元素都是对象,在编写较大的
应用程序时通过而向对象的方式使用matplotlib将更加有效。
但是使用这种而向对象的调用接口
进行绘图比较烦琐,因此 matplotlib还提供了快速绘图的pyplot模块。本节首先介绍该模块的使
用方法。
为了将 matplotlib绘制的图表嵌入 Notebook 中,需要执行下面的命令:

%matplotlib inline

使 用 inline 模式在 Notebook 中绘制的图表会£l 动关闭,为了在 Notebook 的多个单元格内操


作同一幅图表,需要运行下面的魔法命令:

% config I n l i n e B a c k e n d .close_figures = False

4 . 1 . 1 使 用 pyplot模块绘图

matplotlib 的 pyplot 模块提供了与 M A T L A B 类似的绘图闲数调用接口,方便用户快速绘制


二维图表。我们先看一个简单的例子:
Python科学计算 (第 2 版)

pylab模 块
matplotlib还提供了一个名为 pylab 的模块,其中包括了许多 N um Py 和 pyplot模块中常用的
函数,方便用户快速进行计算和绘图,十分适合在 IPython 交互式环境中使用。本书使用 import
pylab as pi 载入 pylab 模块。

import m a t p l o t l i b .pyplot as pit O

x = np.linspace(0, 10, 1000)

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

p i t •p l o t (x , z , " b — ",l a b e l = " $ co s (x A 2 )r ) 〇


matplotlib绘

plt.xlabel("Time(s)") ©
p i t . y l a b el ( MVolt")
- 制精美的图表

plt.title("PyPlot First Example")


plt.ylim(-1.2,1.2)

p i t . l e g e n d ()

plt.show() ©

程序的输出如图4-1所示。

图 4~1 使 川 pyplot模块快速将数椐绘制成丨 III线

〇首先载入 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个像素。

p i t .s a v e f i g ( " t e s t .p n g "y dpi=120)

如果关闭了图表窗口,则无法使用 savefig〇保存图像。实际上不需要调用 show 〇显示


图表,可以直接用 savefigO将图表保存成图像文件。使用这种方法可以很容易编写批
量输出图表的程序。

09
P y th o n 科学计算(第2 版)

savefigO 的第一个参数可以是文件名,也可以是和 Python 的文件对象有相N 调用接口的对


象。例如可以将图像保存到 io.ByteslO 对象中,这样就得到了一个表示图像内容的字符串。这
里需要使用 fm t 参数指定保存的图像格式。

import io

buf = io.BytesIO() # 创逑一个用来保存图像内容的B y t e s I O 对象


plt.savefig(buf, fmt="png") # 将图像以 png 格式保存到 buf 中
buf •g e t v a l u e ( ) [ :20] # 显示图像内容的前2 0 个字节

'\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 的面叫对象方式编写绘图程序。
- 制精美的图表

为了将而向对象的绘图库包装成只使用函数的A P I ,pyplot 模块的内部保荐了当前图表以


及当前子阁等信息。可以使用 gcf ()和 gca〇获得这两个对象,它们分别是 “ Get Q u rait Figure ”
和 “ Get Current A xes ”开头字母的缩写。gcf ()获得的是表示图表的Figure 对象,而 gca 〇获得的
则是表示子图的 A x e s 对象。

fig = plt.gcf()
axes = plt.gca()
print fig, axes

Figu r e (640x320) A x e s (0.125,0.1;0.775x0.8)

在 pyplot 模块中,许多函数都是对当前的 Figure 或 A x e s 对象进行处理,例如前而介绍的


plot〇、xlabel()、 savefigO等。我们可以在 IPylhon 中输入函数名并加“??”,查看这些函数的源
代码以了解它们是如何调用各利1对象的方法进行绘图处理的。
例如下面的例子查看pl〇
t()函数的
源程序,可以看到 plot()函数实际上会通过gca〇获得当前的 A x e s 对 象 ax , 然后再调用它的 plot〇
方法来实现真正的绘图。请读者使用类似的方法查看pyplot模块的其他函数是如何对各种绘图
对象进行包装的。

def plot(*args, **kwargs):


ax = gca()

•••
try:

ret = ax.plot(*args, **kwargs)


finally:

ax.hold(washold)

4 . 1 . 3 配置属性

matplotlib所绘制图表的每个组成部分都和一个对象对应,可以通过调州这些对象的属性设
置 方 法 set_*()或 者 pyplot 模块的属性设置函数 setp〇来设置它们的属性值。例 如 plot〇返回一个
元素类型为 Line2D 的列表,下面的例子设置 Line2D 对象的属性:

plt.figure(figsize=(4j 3))

x = np.arange(0^ 5., 0.1)


line = plt.plot(x, 0.05*x*x)[0] # plot 返回一个列表
line.set_alpha(0.5 ) # 调用 Line2D 对象的 set__*()方法来设置厲性值

上面的例子中,通 过 调 用 L ine2D 对 象 的 set_alpha〇, 修改它在图表中对应曲线的透明度。


下面的语句同时绘制正弦和余弦两条曲线,lines 是一个有两个 Line2D 对象的列表:

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 配® 绘图对象的屈性

同样,可以通过调用 L i n e 2 D 对象的 get_*〇或者通过 pltgetpO 来获取对象的属性值:

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

axes = [<matplotlib.axes._subplots.AxesSubplot object at ...

Figure 对象的 axes 属性楚一个列表,它保存图表中的所有子图对象。下面的程序查看当前

图表的 axes 属性,它就是 goi ()所获得的当前子阁对象:

print plt.getp(f, "axes"), plt.getp(f, na x e s n )[0] is plt.gca()

[< m a t p l o t l i b .a x e s , _ s u b p l o t s .AxesSubplot object at 0x05DE5790>] True

用 plt.getpO可以继续获取 AxesSubplot对象的属性,例如它的 lines 属性为子图中的 Line2D


对象列表:
m a t p o t lib绘

alllines = plt.getp(plt.gca(), "lines")

print alllines, alllines[0] is line # 其中的第一条曲线就足域开始绘制的那条丨11丨线


- 制精美的图表

<a list of 3 Line2D o b j e c t s 〉True

通过这利1方法可以很容鉍查看对象的属性值以及各个对象之间的关系,找到需要配置的属
性。因为 matplotlib实际上是一套而向对象的绘阁库,因此也可以直接获取对象的属性,例如 :

print f.axes, len(f.axes[0].lines)

[<matplotlib.axes._subplots.AxesSubplot object at 0x05DE5790>] 3

4 . 1 . 4 绘制多子图

一 个 Figure 对象可以包含多个子图(Axes ) , 在 matplotlib中用 A x e s 对象表示一^绘图区域,


在本书中称之为子图。在前面的例子中,Figurc 对象只包括一个子图。可以使用 subplotO快速绘
制包含多个子图的图表,它的调用形式如下:

subplot(numRows, numCols, plotNum)

图表的整个绘图冈域被等分为num Rows 行 和 num Cols 列,然后按照从左到右、从上到下


的顺序对每个R 域进行编号,
左上 K 域的编号为 L plotN u m 参数指定所创建 A x e s 对象的R 域。
如 果 numRows、numCols 和 plotNum 三个参数都小于10,则可以把它们缩写成•个整数,例如
subplot(323)和 subplot(3,2,3)的含义相同。
如果新创建的子图和之前创建的子图区域有重叠的部分,
之前的子图将被删除。
下面的程序创建如图《 (左)所示的3 行 2 列 共 6 个子图,并通过 axisbg 参数给每个子图设
置不同的背景颜色。
for idx, color in enumerate("rgbyck"):

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对象中创逑多个子阁

在绘图窗口的工具栏屮,有一个名为 “ Configure Subplots”的按钮,单击它会弹出调节子


图间距和子图与图表边框距离的对话框。也可以在程序屮调用 subpl〇
ts_ adjUst()调节这些参数,
它 有 left、right、bottom、top、wspace 和 hspace 共•6 个参数,这些参数与对话框中的各个控件对
应。
参数的取值范围为0 到 1,
它们是以图表绘图区域的宽和高进行正规化之后的坐标或长度。
subplot〇返[H丨它所创連的A x e s 对象,我们 n丨以将这些对象川变量保存起来,然后用 sca()交
替让它们成为当前 A x e s 对象,并调用 plot〇在其中绘图。如果需要同时绘制多幅图表,可以给
figureO 传递一个整数参数来指定 Fig u r e 对象的序号。如果序号所指定的 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))

plt.sca(ax2) # 选择图表2 的子图2


pit.plot(x, np.cos(i*x))

也可以不调用 sca〇指定当前子图,而直接调用 ax 1 和 a x 2 的 plot()方法来绘图。

首先通过 figureO创建了两个图表,它们的序号分別为1和 2。然后在图表2 中创建了左右


并排的两个子图,并用变董 a x l 和 ax2 保存。
在循环中,〇先调用 figure(l )让 图 表 1成为当前图表,并在其中绘图。©然 后 调 用 sca(a x l )
和 sca(ax2)分别让子图 a x l 和 ax2 成为当前子图,并在其中绘图。当它们成为当前子图时,包含
它们的阁表2 也 A 动成为当前阁表,因此不需要调用figure(2)。这样依次在阁表1和图表2 的两
个子图之间切换,逐步在其中添加新的llll线,效果如图4 4 所示。
m a tp lo tlib绘
- 制精美的图表

10 U 2.0 2.S 10 0.0 0 ^ 1.0 1濂 20 3.0

图4 4 同时在多幅图表、多个子阁中进行绘图

此 外 subpbtsO 可以一次生成多个子图,并返回图表对象和保存子图对象的数组。在下面的
例子中,axes 是一个形状为(2,3)的数纟J1,每个元素都是一个子图对象,可以利用 Python 的赋值
功能将这个数组中的每个元素用一个变蛰表示:

figj axes = p i t . s u b p lo t s (2, 3)

[a, b, c], [dj e, f] = axes


print axes.shape

print b

(2, 3)

A x e s (0.398529,0.536364;0.227941x0.3636B6)

还可以调用 subplot2grid〇进行史复杂的表格布局。表格布局和在 Excel或 W ord 中绘制表格


十分类似,其调用参数如下:

subp l o t 2 gr i d (s h ape, loc, rowspan=l, colspan=l, **kwargs)

其中,shape 为表示表格形状的元纟JL (行数,


列数)。lo c 为子图左上角所在的坐标:(
行,列)。
rowspan 和 cd sp an 分別为子图所占据的行数和列数。在下面的例子中,在 3 X 3 的网格上创建5
个子图,在每个子图中间显示该子图对应的变量名,如图4-5所示:

fig = pit.figure(figsize=(6, 6))


axl = p i t . s u bplot2grid((3, B), (0, 0), colspan=2)
ax2 =p i t .subp l o t 2 gr i d ( (3, 3 ) , (0, 2 ) , rowspan=2)
ax3 = plt.subplot2grid((3, 3), (1 , 0), rowspan=2)

ax4 = plt.subplot2grid((3, 3), (2, 1), colspan=2)

ax5 = plt.subplot2grid((3, B), (1, 1))

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中。
通过下面的语句可以获収用户配置路径:

from os import path

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

u 'C :\\Users\\RY\\Dropbox\\scipybook2\\settings\\.m a t p l o t l i b '

通过下而的语句可以获得目前使用的配置文件的路径:
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 ())

u 'C :\\Users\\RY\\Dnopbox\\scipybook2\\settings\\.m a t p l o t l ib \ \ m a t p l o t l i b n c '

如果使用文本编辑器打开此配置文件,就会发现它实际上是一个字典。为了对众多的配置
进行区分,字典的键根据配置的种类,用 分 为 多 段 。配置文件的读入可以使用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

在 matplotlib模块载入时会调用 rc_ params(),并把得到的配S 字典保存到 rcParams变量中:

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

matplotlib将 使 用 处 _5字 典 中 的 配 置 进 行 绘 图 。用户可以直接修改此字典中的配置,所

做的改变会反映到此后创建的绘图元素。例如下而的脚本所绘制的折线将带有岡形的点标识符:

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〇。
下面的例子同吋配置点标识符、
线宽和颜色:

matplotlib.rc("lines", marker="x", linewidth=2, color="red")

如果希望恢复到 matplotlib载入时从配置文件读入的默认配置,可以调用 rcddkiltsO :

m a t p l o t l i b .rcdef a u l ts ()

如果手工修改了配S 文件,希望重新从配置文件载入最新的配f f i , 可以调用:

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

^; 通 过 pyplot模块也可以使用 rcParams、rc 和 rcdefaults。


ma中lotlib.style 模块提供绘图样式切换功能,所有可选样式可以通过 available 获得:

from matplotlib import style


print style.available
[u'dark一background', u 'bmh ', u 'grayscale', u'ggplot', u 'f ivethirtyeight']

调 用 use〇函数即可切换样式,例如下面使用 ggplot 样式绘图,效果如图4-6所示。

s t y l e .use ("ggp lot ")

ggp 丨
o t t 丫式

m atp lo tlib绘
- 制精美的图表
图4 « 6 使用ggplot样式绘图

4 . 1 . 6 在图表中显示中文

matplotlib 的默认配置文件中所使用的字体无法正确显示中文,可以通过下面几利汸法设置

中文字体:
♦ 在程序中直接指定字体。
•在 程 序 开 头修改配置字典 rcParams。
•修 改 配 置 文 件 。
在 matplotlib 中可以通过字体名指定字体,而每个字体名都与一个字体文件相对应。通过
下面的程序可以获得所有可用字体的列表:

from matplotlib.font_manager import fontManager


fontManagen.ttflist[:6]
[<Font 'cmssl0' (cmssl0.ttf) normal normal 400 normal>,
<Font 'cmbl0' (cmbl0.ttf) normal normal 400 normal>,
<Font 'cmexl0' (cmexl0.ttf) normal normal 400 normal>,
<Font 'STIXSizeFourSym' (STIXSizFounSymBol.ttf) normal normal 700 normal>j
Python科学计算(第2 版)

<Font 'Bitstream Vera Serif' (VeraSeBd.ttf) normal normal 700 normal>,


〈Font 'Bitstream Vera Sans' (Venalt.ttf) oblique normal 400 normal〉]

ttflist是 matplotlib 的系统字体列表。其屮每个兀素都是表7K 字体的 Font 对象,下丨id 的程序


显示了第一个字体文件的全路径和字体名,由路径可知它是matplotlib 自1带的字体:

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
-

fig = plt.figure(figsize=(8, 7))


ax = fig.add_subplot(lll)

pit.subplots__adjust(0, 0, 1, 1, d, 0)
plt.xticks([])
plt.yticks([])
x, y = 0.05, 0.05

fonts = [font.name for font in fontManager.ttflist if


path.exists(font.fname) and os.stat(font.fname).st_size>le6] O
font = set(fonts)

dy = (1.0 - y) / (len(fonts) // 4 + (len(fonts)%4 != 0))

for font in fonts:


t = ax.text(x, y + dy / 2, u"中文字体",

{'fontname•:font, 'fontsize':1A}, transform=ax.transAxes) ©


ax.text(Xj y, font, { 'fontsize':12}, transform=ax.transAxes)
x += 0.25
if x >= 1.0:

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
• • • •

WenQuanYi Micro Hei Yu Gothic Linux libertine G


+ 夂方緣
STCaiyun Microsoft MHei STXmgkai Arial Unicode MS
中文字硌 y•y••«
r> yy
••io •i • 4• •

STliti Linux Biolinum G Linux Libertine G Linux libertine G


中文字体 777?
••## 中文字你
WenQuanYi Micro Hei Linux Biolinum G STZhongsong STCaiyun
中 文 >体 yyyo
%h % k hr kr ky k>
^

Yu Gothic FZYaoTi Linux Libertine G Linux libertine G


中文字体 中文字体
Microsoft MHci Linux Lib«rtmc Disptd/ G FZShuTi D^ngXion
中文字休 • • • • 中文+ 7777
STZhongsonj Linux Biolinum G STXinwci Gabriola
*• >•*>i *i» 中文字体 中文字体 r>r>
• 鲁 •參

Linux Libertine G Microsoft MHei Microsoft JhengHei Linux Biclinum G


???? ???? ????
Linux Libertioe G Linux Biolinum G Linux Libertine G SimSun-ExtB
中文葶律 % % %% • • • • V • • •

STHupo Linux Ub«rtine G Linux Libertine G Maleun Gothic


屮文?体 中文李沐 中文字体 •女葶你
STSonc Yu Gochk SlmHei STHupo
^ x rm 中文字体 中文字体
STSonc Yahei Mono Arial Unicode MS STFanesong
中文李体 中文•卞沐 中文事《 中文承冰
Microsoft IhengHei STKaiti STUti LiSu
中文字体 中文字体 • • A t 中 文字体
STXihei STFmgsong Linux Libenme G Microsoft YaHei
中文舍体 中丈& 中文字体 中文字体
Microsoft MHei STKaiti DFKai-SB YoijYuan
中文字体 *ryy>
t 文孝冰 中文?沐
Microsoft YaHei Linux Libertine Dispta/ G STXinwei FZYaoTi
中 t 字沐 1• 7A 74 71 4 4 * 4 ♦ X 方砵
KaiTi Malgun Gothic Linux Libertine G STXingtcai

阁本7 显示系统中所有的中文字体名

〇利 用 0S模块中的 stat〇获収字体文件的大小,并保留字体列表屮所有大于1M B 的字体文


件。 hid于中文字体文件通常都很大,因此使用这种方法可以粗略地找出所有的中文字体文件。
©调用子图对象的 text〇在其中添加文字,注意文字必须是 Unicode 字符串。通过一个描述
字体的字典指定文字的字体:’
fontname’
键对应的值就是字体名。
由 于 matplotlib只搜索 T T F 字体文件,因此无法通过上述方法使用系统中安装的许多复合
T T C 字体文件。可以直接创建使用字体文件的FontProperties对象,并使用此对象指定图表中的
各种文字的字体。下面是一个例子:

from matplotlib.font_manager import FontProperties


font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc'、 size=14) O
t = np.linspace(0, 10, 1000)
y = np.sin(t)
plt.close("all")
pit.plot(t, y)
p!t.xlabel(u"时间",fontproperties=font) ©
p i t .y l a b e l (u "M l ® ", fontpr o p er t i e s = f o n t )
pit •title (u"正弦波",fontproperties=font)
plt.show()

〇创述一个描述字体属性的FontProperties对象,并设:S !其 fname 属性为字体文件的绝对路


Python 科学计算 (第 2 版)

径。© 通 过 fontproperties参数将 FontProperties对象传速给显示文字的函数。


还可以通过字体工具将 T T C 字体文件分解为多个 T T F 字体文件,并将其复制到系统的字
体文件夹中。为了缩短A 动时间,matplotlib不会每次A 动时都重新扫描所有的字体文件并创逑
字体列表,冈此在复制完字体文件之后,需要运行下面的语句重新创建字体列表:

from matplotlib.font_manager import 一rebuild


_rebuild()

还可以直接修改配置字典,设置默认字体,这样就不需要在每次绘制文字时设置字体了。
例如:

pit.rcParams["font.family"] = "SimHei"

p i t . p l o t ([1,2,3])
plt.xlabel(0.5 ,0.5, u " 中文字体")

或者修改上节介绍的配置文件,修改其中的 font.family配置为 SimHei, 注 意 Sim H ei 是字体


matpotlib绘

名,请读者运行前面的代码来查看系统中所有可用的中文字体名。
- 制精美的图表

4.2 Artist

与本节内容对应的 Notebook 为:04~matplotlib/matplotlib~200~aitists.ipynb


DVD

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之间:

from matplotlib import pyplot as pit

fig = p i t . f i g u re ()

ax = fig.add 一a x e s ( [0.15, 0.1, 0.7, 0.3])

然后调州 A x e s 对象的 pbt 〇来绘制曲线,并且返回表示此111丨线的Line2D 对象。

line = ax.plot([l, 2, 3], [1, 2, 1])[0] # 返回的是只有一个元素的列表


print line is a x . l i n e s [0]

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 )

如果一直跟踪下去,就会发现 A x e s 对象的 xaxis 属性是一个 X A x is 对象,其 label属性是一


个 T ex t 对象,而 T ext 对象的_text 属性为我们设:®的值:

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

A xes 、X A x is 和 T ex t 类都从 Artist 继承,也可以调JIJ它彳门的get_*()以获得相应的属性值:

ax.get_xaxis().get_label().get_text()
u'time'

4.2.1 Artist 的属性

通过前面的介绍我们G 经知道,图表中的每个绘图元素都用一个Artist 对象表示,而每个


P y th o n 科学计算(第 2 版)

Artist 对象都有许多屈性控制其显示效果。例 如 Figure 对象和 A x e s 对象都有 patch 屈性作为其背


景,它是一个 Rectangle 对象。通过设置它的属性可以修改图表的背景色或透明度,下面的例子
将图表的背景色设置为绿色:

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绘

axes 拥有此Artist对象的A x e s 对象,可能为None


clip_box 对象的裁剪框
足否裁剪
- 制精美的图表

clip_on

dip—path 裁剪的路径
contains 判断指定点是否在对象之上的函数
figure 拥有此Artist对象的Figure对象,可能为None
label 文本标签
picker 控制Artist对象选取
transform 控制偏移、旋转、缩放等坐标变换
visible 控制是否可见
zorder 控制绘图顺序

Artist 对象的所有屈性都可以通过相应的get_*〇和 set_*()方法进行读写,例如下面的语句将


新绘制的曲线对象的alpha 属性设置0.5,使它变成半透明:

line = p i t . p l o t ([1, 2, 3, 2, 1], lw=4)[0]

l i n e •set 一a l p h a (0•5)

可以使用 set〇—次设置多个屈性:

line.set(alpha=0.5, zorder=2)

使用前面介绍的 getp()可以方便地输出 Artist对象的所有屈性名以及与之对应的值:

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 对象的属性,接下来要解决的问题是如何找到指定的


A rtist 对象。前面介 绍 过 A rtist 对象有容器类型和简单类型两种,这一节让我们详细看看容器

类型。
在构成图表的各种 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)

ax2 = fig.add_axes([0.1, 0.1, 0.7, 0.3])

- 制精美的图表
print axl in fig.axes and ax2 in fig.axes

True

为了支持 gca〇等函数,Figure 对象内部保存有当前轴的信息,因此不建议立接对axes 屈性


进行列表操作,而应该使用 add_subplot〇、add_axes()、delaxes〇等方法进行子图的添加和删除操
作。但是使用 fo r 循 环 对 axes 属性中的每个元素进行操作是没有问题的,下面的语句打开所有
子图的栅格显示。

for ax in fig.axes:

ax.grid(True)

Figure 对象可以拥有自1己的文字、线条以及图像等简单类型的Artist 对象。默认的坐标系统


以像素点为单位,但是可以通过设置 Artist 对 象 的 transform属性修改其所使用的坐标系。例如
Figure 对象的坐标系萣以图表的左下角为坐标原点(〇,〇),右上角的坐标为(1,
1),关于坐标变换在
后而的章节还会进行详细介绍。下而的程序创建一个 Figure 对象,并在其中添加两条直线:

from matplotlib.lines import Line2D


fig = p i t . f i g u re ()

linel = Line2D(
[0, l]j [0, 1], transform=fig.transFigure^ figure=fig^ color="n")
line2 = Line2D(

[0, 1], [1^ 0], transform=fig.transFigurej figure=fig^ color="g")

f i g .l i n e s .e x t e n d ([linel, line2])
P y th o n 科学计算(第 2 版)

为了 LL所创建的 Line2D 对象使用 Figure 对象的來标系,我们将 Figure 对象的 transFigure屈


性赋给 Line2D 对象的 transform属性。为了让 Line2D 对象知道它是在 Figure 对象中,还设置-其
figure 属性为 fig 。最后还需要将这两个 Line2D 对象添加到 Figure 对象的 lines 属性列表中。
表4>2列出了 Figure 对象中包含其他 Artist对象的属性:

表 4 - 2 包含其他Artist对象的Figure对象属性
属性 说明
axes A x e s 对象列表

patch 作为W贵的 Rectangle对象


images Figurclmage对象列表

legends Legend 对象列表

lines Line2D 对象列表

patches Patch对象列表

texts Text 对象列表,用于显示文字


matpotlib绘

4.2.3 Axes 容器
- 制精美的图表

A x e s 容器(子图)是整个 matplotlib 的核心,它包含了组成图表的众多 Artist 对象,并且有许


多方法函数帮助我们创建和修改这些对象。和 Figure 容器一样,它有一个 patch 属性作为背景,
当它是馆卡尔坐标时,
patch 属性是一个 Rectangle 对象;而当它是极坐标时,
patch 属性则是 Circle
对象。例如下面的语句将 A x e s 对象的背景色设置为绿色:

fig = p i t . f i g u re ()
ax = fig.add_subplot(lll)

a x .p a t c h .set_facecolor("green")

当调用 A xes 对象的绘图方法 plot〇时,它将创建一组 Line2D 对象,并将它们添加进 Axes


对 象 的 lines属性中,最后返回包含所有创建的 Line2D 对象的列表。plot〇的所有关键字参数都
将传递给这些 Line2D 对象以设置它们的属性:

x, y = np.r a n d o m. r a n d (2, 100)


line = ax.plot(x, y, color="blue", linewidth=2)[0]

line is a x . l i n e s [0]

True

注 意 plot〇返回的是一个 Line2D 对象列表,因为可以传递多组X -Y 轴的数据给 plot(),同时


绘制多条llll线。
2

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

一般我们不会立接对 lines 或 patches屈性进行操作,而是调用 add_line()或 add_patch()等方


法,这些方法帮助我们完成许多属性的设置工作。下面首先创建 A x e s 对 象 a x 和 Rectangle 对
象 rect:

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

ax.add_patch(rect) # 将 rect 添加进 ax

- 制精美的图表
rect.get 一a x e s () is ax

True

接下来,为了完整M 示 rect,调 用 a x 的 autoscale_view 〇方 法 让 它 动 调 节 X -Y 轴的M 示


范围:

print ax.get_xlim() # a x 的X 轴范围为0 到 1 , 无法显示完整的rect


print ax.dataLim._get_bounds() # 数据的范[T;|和 rect 的大小一致
ax.autoscale_view() # 自动调整坐标轴的范丨韦丨

print ax.get_xlim() # 于楚X 轴可以完整显示rect

(0.0, 1.0)
(1.0, 1.0, 5.0, 12.0)

(1.0, 6.0)

表4~3列出了 A x e s 对象中可以包含其他 Artist对象的厲性:

表4~3包含其他Artist对象的A xes 对象属性


属性 说明
artists Artist对象列表

patch 作为A x e s 背贵的Patch对象,可以是Rectangle或 Circle


collections Collection对象列表

images Axeslmage 对象列表

legends Legend 对象列表

22
:
P y th o n 科学计算(第2 版)

(续表)

属性 说明
lines Linc2D 对象列表

patches Patch对象列表

texts Text 对象列丧

xaxis X A x i s 对象

yaxis YAx i s 对象

表 4~4列出了 A x e s 对象的各种创建其他Artist对象的方法:

表 4-4 Axes 对象提供的创建其他Artist对象的方法


A xes 的方法 所创建的对象 添加进的列表
annotate Annotate texts

ban; Rectangle patches


matpotlib

errorbju* Line2D、 Rectangle lines,patches

fill Polygon patches


丨绘制精美的图表

hist Rectangle patches

imshow Axeslma^e images

legend Legend legends

plot Linc2D lines

scatter PolygonColleclion collections

text Text texts

例如卜面的程序调用 scatterO绘制散列图,它返 N 的是一个 PathCdlection 对象,该对象被


添加进 ax.collections列表:

fig, ax = p i t . s u b p lo t s ()

t = a x .scatter(np.random.rand(20 ),n p . r a n dom.rand(20))

print tj t in ax.collections

< m a t p l o t l i b .c o l l e c t i o n s .PathCollection object at 0x082339B0> True

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

下面获得 axis 对象的刻度位置的列表:

a x i s •get— t i c k l o c s ()

array([ 0. , 0.2, 0.4, 0.6, 0.8, 1.])

下面获得 axis 对象的刻度标签以及标签中的文字:

print axis.get_ticklabels() # 获得刻度标签的列表


print [x.get_text() for x in axis.get_ticklabels()] # 获得刻度的文本字符串
<a list of 6 Text major ticklabel o b j e c t s 〉

[u'0.0', u'0.2', u'0.4', u'0.6', u'0.8', u'1.0']

下而获得轴上表示主刻度线的列表,可以看到 X 轴 上 共 有 12条刻度线,它们是子图的上

m a t p lib绘
下两个 X 轴上的所有刻度线:

- 制精美的图表
a x i s •get 一
t i c k l i n e s ()

<a list of 12 Line2D ticklines o b j e c t 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 一c o l o r (" r ed")

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)

for line in a x i s •get— ticklines():

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 轴的刻度线和刻度文本的样式

这个例子只是为了演示 A r t i s t 对象的各种属性,实际上使用 p y p l o t 模块中的 x t i c k s O 能够更


快地完成 X 轴上的刻度文本的配置。不过,x t i c k s ()只能设置刻度文本的属性,不能设置刻度线
的属性。感兴趣的读者可以在I P y t h o n 中输入 p l t .x t i c k s ??来查看源代码。
matplotlib绘

prt.xticks(fontsize=16, color="red", rotation=45)


—制 精 美 的 图 表

在前面的例子中,副刻度线列表为空,这是因为用于计算副刻度位置的对象默认为
N u llL o c a to r , 它不产生任何刻度线。而计算主刻度位置的对象为A u t o L o c a t o r , 它会根据当前的
缩放等配置自动计算刻度的位置:

print a x i s.get_minor_locator () # 计算副刻度位置的对象


print a x i s.get_major_locator () # 计算主刻度位置的对象

<matplotlib.ticker.NullLocator object at 0x08364F50>

<matplotlib.ticker.AutoLocator object at 0x084285D0>

m a tp lo tlib 提供了多种配置刻度线位置的L o c a t o r 类和控制刻度文本显示的F o r m a tte r 类。下


面的程序设置 X 轴的主刻度为71/4,副刻度为tt/ 2 0 , 并且主刻度上的文本用数学符号显示TT。
程序的输出如阁本9所示。

f rom fractions import Fraction


from matplotlib.ticker import MultipleLocator, FuncFormatter O
x = np.arange(0, 4*np.pi, 0.01)
figj ax = plt.subplots(figsize=(8j4))
pit.plot(x, np.sin(x), x, np.cos(x))

def p i_formatter(x> p o s ) : ©

frac = Fraction(int(np.round(x / (np.pi/4))), 4)


d, n = f r a c .denominator, f r a c .numerator

if frac == 0:
return "0"

22J
elif frac == 1:

return "$\pi$"
elif d == 1:

return r"${%d} \pi$" % n


elif n == 1:

return r n$\frac{ \ pi } { % d } $ n % d

return r"$\frac{%d \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对象关系
图的小程序。

scpy2.common.GraphvizMatplotlib: 将 matplotlib 的对象关系输出成如图 4~10 所示的关


系图。
matpotlib绘

为了生成关系图,
读者可以从 Graphviz 的官方网站下载Graphviz 软件包,
或者使用 Graphviz
的在线编辑器。下而看一个例子:
- 制精美的图表

fig = p i t . f i g u re ()
p i t . s u b p lo t (211)

plt.bar([l, 2, 3], [1, 2, 3])


p i t . s u b p lo t (212)

plt.plot([l, 2, 3])

下面调)IJ GraphvizMatplotlib.graphvizO,将 f i g 内部各个 Artist对象的关系输出为 dot 代码,


并使用%d o t 魔法命令将其转换为 S V G 图像,显 示 在 N oteb o ok 中。结果为 图 4-10所示的关
系图:

from scpy2.common import GraphvizMatplotlib

%dot G r a p h v i z M a t p l o t l i b .graphviz(fig)

图 4~10 中以灰色填充矩形表示列表,其他的矩形表示各种 Artist对象。Artist对象之间的关


系使用带箭头的细线表示,细线旁边的文本为厲性名。
例 如 从 Figure 矩 形 到 Rectangle 矩形的箭头表示 Figure 对 象 的 patch 属性是一个 Rectangle
对象。而 F igu re 对 象 的 a x e s 厲性是一个有两个元素的列表,每个元素都是一个 AxesSubplot
对象。请读者仔细观察图 4 - 1 0 , 并 在 IPython 中输入相应的语句,确认各个 A rtist 对象之间的
关系。

30
matplotlib绘


美 -

闯4 ~ 1 0 使用GraphvizMatplotlib也成阁表对象中各个Artist对象之间的关系图

4 3 坐标变换和注释

与本节内容对应的 Notebook 为:04~matplotlib/matplotHb-300"transfoim.ipynb。


一幅阍表中涉及多种坐标系以及坐标变换,理解各利唑标系的含义并掌握其用法才能随心
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

def find_curve_intersects(Xj y2):


d = yl - y2
idx = np.where(d[:-l]*d[l:]<=0)[0]

xl, x2 = x[idx], x[idx+l]


dl, d2 = d[idx], d[idx+l]

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)

xl, x2 = f i n d _curve_intersects(x> fl, f2) ©


ax.plot(xl, f u n c l ( x l ) > "o")
ax.plot(x2, f u n c l ( x 2 ) > "o")

ax.fill_between(x, fl, f2, where=fl>f2, f a c e c o l o r = ’.green__. alpha=0.5) O

from matplotlib import transforms

trans = transforms.blended— transform— factory(ax.transData, ax.transAxes)


ax.fill_between([xl, x2], 0, 1, t r a n s f o m ^ t r a n s , alpha=0.1) ©

a = ax.text(0.05, 0.95, u" 直线和二次曲线的交点" , ©


t r a nsform=ax.transAxesJ
verticalalignment = "top",

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",

" connectionstyle":" a r c 3 , r a d= - 0 •3"}


ax.annotate(u" 交点", 0
xy=(xl, funcl(xl)), x y c o o r d s = ndata",

xytext=(0.05, 0.5), textcoords="axes fraction",


arrowprops = arrow)

ax.annotate(u" 交点", ©
xy=(x2, funcl(x2))) xycoords="data",

xytext=(0.05j 0.5)^ textcoords="axes fraction",


arrowprops = arrow)

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 )对曲线进行线性插值

即可。

〇接下来调用 fill_between〇绘 制 X 轴上在两个交点之间、Y 轴上在两条肋线之间的面积部


分,并通过 facecoloi•和 alpha 参数指定填充的颜色和透明度。fill_between()的调用参数如下:
matplotlib绘

fill 一b e t w e e n (x , yl, y2=0, where=None)

-其中,x 参数是长度为 N 的数纟Jl , y l 和 y 2 参数楚长度为 N 的数纟J1或单个数值。当 y l 或 y 2


- 制精美的图表

为单个数值时,它们相当于一个长度为 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)])

〈class 'm a t p l o t l i b .t r a n s f o r m s .C o m p o s i t eG e n e r i c T n a n s f o r m '>


array([[ 80., 32.],

[576., 288.]])

下面的程序计兑子图坐标系中的坐标点(0,0)和(1,1)在绘图窗口中的位置,得到的结果和上
面的相同。即子图的左下角坐标(0,0)和数据坐标系中的坐标(-3, -2)在屏幕上是一个点。观察图
4~11可以知道这显然是正确的。

ax.transAxes.transform([(0J0 ) > (1^1)])

array([[ 80., 32.],

matplotlib绘
[576., 288.]])

最后计兑图表坐标系中坐标点(〇,〇)和(1,1)在绘图窗口中的位置,可以看出绘图R 域的宽为

- 制精美的图表
640个像素,高 为 320个像素:

fig.tran sF i g u r e . t r a n s f o r m( [(0,0), (1,1)])

array([[ 0., 0.],


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

<class 'm a t p l o t l i b .t r a n s f o r m s .C o m p o s i t eG e n e r i c T r a n s f o r m '>


array([-0.09677419, 1.5 ])

请读者仔细观察程序所输出的图表,子图的上下余白相同,而左侧余白略大于右侧余白,
因此绘图区域的中心点(320, 160)并不是数据区域的中心点(0, 1.5)。
当 调 用 set」dim 〇修改子图所M 示 的 X 轴范围之后,它的数据坐标变换对象也同时发生了
变化:

print ax.set_xlim(-3, 2) # 设置 X 轴的范围为-3 到 2


print ax.transData.transform((3, 5)) # 数据坐标变换对象已经发生了变化

(-3, 2)

[675.2 288.]
Python科学计算 (第 2 版)

下面回头看看图4-11中绘制矩形区间的程序:

from matplotlib import transforms


trans = transforms.blended_transform_factory(ax.transData, ax.transAxes)

ax.fill 一between([xl, x2], 0, 1, transform=trans, alpha=0.1)

矩形区间使用 fill_between〇绘制。由于所绘制矩形的左右两边要始终经过两个交点,因此
矩 形 的 X 轴坐标必须使用数据坐标系中的坐标:x l 和 x 2。而由于矩形的高度始终充满整个子
图的高度,因此矩形的 Y 轴坐标必须是子图坐标系中的坐标:0 和 1。

使 用 axvspan〇和 axhspan()可以快速绘制垂直方向和水平方向上的区间。

程序中,使 用 blended_ transform_factoi;y()创建这种混合坐标系。它的两个参数都是坐标变换


对象,它从第•个参数获得 X 轴的坐标变换,从第二个参数获得 Y 轴的剩"示变换。因此它所返
m a t p l b绘

冋的坐标变换对象 trans 的 X 轴使用数据坐标系,而 Y 轴使用子图坐标系。程序中,将混合坐


标变换对象 trans传递给 fill_between〇的 _ sform 参数,这样所绘制的填充区域就能始终保持左
- 制精美的图表

右边通过两个交点,而上下边位于子图边框之上。

4 . 3 . 2 坐标变换的流水线

从一个坐标系变换到另一个坐标系,中间需耍经过儿个步骤。而且数据坐标系不一定是笛
卡尔坐标系,它可能是极坐标系或对数坐标系。因此坐标系的变换并不是简单的二维仿射变换
(2D Affine Transfomation )。让我们从最简中的图表坐标变换对象transFigure开始,介 绍 matplotlib
的坐标变换是如何进行的。
通过本书提供的 G m phvizM PLTm m fcm 可以将坐标变换对象.K 示为关系图,图 ‘12显示了
fig .transFigure的内部结构。

from scpy2.common import GraphvizMPLTransform

%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.transAxes是一个 BboxTransformTo 对象,因此它也将(0, 0)-(1, 1)区域变换为另一个区域。


而此E 域是一个 TransformedBbox 对象,它是将矩形区域(0.125, 0.1)-(0.9, 0.9)通过 fig.transFigure
变换之后的区域。因此在 transAxes对象内部使用了 transFigure变换:
Python科学计算(第2 版)

ax.tnansAxes._boxout._transform == fig.transFigure

True

而此变换中的矩形区域(0.125,0.1M 0.9,0.9)是子图在图表坐标系中的位置:

a x .get jDosition ()

Bbox('array([[ 0.125, 0.1 ],\n [ 0.9 , 0.9 ]])')

子图在窗口來标系中的矩形K 域为:

a x .t r a n s A x e s ._ b o x o u t .bounds

(80.0, 31.999999999999993, 496.0, 256.0)

因 此 ax.tm nsAxes 实际上是一个将矩形区域(0,0)-(1,


1)变换到矩形区域(80.0,32)-(496.0, 256.0)
的坐标变换对象。
最后我们观察数据坐标系的变换对象ax .tmnsData(内部结构参见图4-14)。
它 由 ax .tmnsScale、
ax.transLimits 和 ax.transAxes 井冋构成,
因此先看看 ax.transLimits 和 ax.transScale 的内容。 transLimits
m a t p l b绘

是 一 个 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):

print a x .t r a n s L i m i t s .t r a n s f o r m ( (-3, -2))

print a x .t r a n s L i m i t s .t r a n s f o r m ( {2, 5))

[0. 0.]

而矩形K 域(-3, -2)-(2,5)由 X 轴 和 Y 轴的显示范围决定:

print ax.get_xlim() # 获得X 轴的显示范围


print ax.get__ylim() # 获得Y 轴的显示范圃

23:
(-3.0, 2.0)

(-2.0, 5.0)

由于 transLimits将数据來标系的显示范围变换为单位矩形,而 tnmsAxes 将单位矩形变换为


以像素为单位的窗口矩形范围,因此这两个变换的综合效果就是将数据坐标变换为窗口坐标。
可 以 “+ ”号将两个变换连接起來创建一个新的变换对象,例 如 ax.tmnsLimits + ax.transAxes
表示先进行 ax .transLimits变换,然后进行 ax .tmnsAxes 变换,变换对象就像流水线上生产产品一
样,一步一步地对坐标点进行变换。下面的程序比较它和ax .transData的变换结果:

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]

为了支持不I机匕例的來标轴, transData 111还包括••个 transScale变换,


即 transData = transScale

matplotlib绘
+ transLimits + transAxes。本例中 transScale 是一个丨H 等变换,因此 ax.transLimits + ax.transAxes 和
ax .transData的变换效果一样:

- 制精美的图表
ax.transScale

T r a n sformWnapper(BlendedAffine2D(IdentityTransform()^IdentityTransform()))

当 使 semilogx()、semilogyO 以及 loglogO 等绘图函数绘制对数坐标轴的图表时,或者使用


A x e s 的 set_ xscale()和 set_yscale()等方法将坐标轴设置为对数坐标时,transScale 就不两足恒等变
换了,其内部结构如图4-15所示。

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

ax.set xscale("linear") # 将 X 轴改为线性坐标

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

offset = t r a n s f o r m s .Sca l e d T r an s l a t i o n (i^ -i, t r a n s f o r m s .IdentityTransform()) O


shadow_trans = plt.gca().transData + offset ©
ax. p l o t ( x Jy Jlinewidth=4,color="black",

transform=shadow_trans, ©
alpha=(N-i)/2.0/N)

a x .plot(x^ y , l i n e w i d t h = 4 , c o l o r = 'b l a c k ')


m a t p l b绘

ax.set_ylim((-1.5, 1.5))

1.5
- 制精美的图表

-1.5«---------------- '---------------- '---------------- '----------------


0.0 0.5 1.0 1.5 2.0

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

array([ 1., -1.])

24
©阴);如 ll线的來标变换htl shadow_trans兒成,
它由数据少标变换对象transData和 offset组成。

print ax.transData.transform((0,0)) # 对(0,0 )进行数据坐标变换


print shadow_trans.transform((0,0)) # 对(0,0 )进行数据坐标变换和偏移变换
[60. 120.]
[61. 119.]

©最 后 通 过 参 数 transform将 shadow jrans 传递给 plot()绘图。由于 shadow_trans足在完成数


据坐标到窗口坐标的变换之后,再进行偏移变换,因此无论当前的缩放比例如何,阴影效果将
始终保持一致。

4 . 3 . 4 添加注释

在 pyplot 模块中提供了两个绘制文字的闲数:text〇和 figtext()。它们分别调用当前 A x e s 对


象和当前 Figure 对 象 的 text〇方法进行绘图。text()默认在数据坐标系中添加文字,而 figtext()则
默认在图表坐标系中添加文字。可以通过_ sf〇
rm 参数改变文字所在的坐标系,下面的程序演

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)

for i, (_x, _y) in enumerate(zip(x, y ) ) :

ax.text(_x, _y, str(i), color="red", fontsize=i+10) O

ax.text(0.5, 0.8, u" 子图坐标系中的文字" ,color=__blue", ha="center",


t r a n s f o r m = a x .t r a n s A x e s ) ©

plt.figtext:(0.1, 0.92, u " 图表坐标系中的文字" ,color="green") ©

〇由于没有设置 transform 参数,text〇默认在数据坐标系中创建文字,这里通过 fontsize 参


数修改文字的人小。© 通 过 transform参数将文字的坐标变换改为ax .UmsAxes ,因此文字在子图
坐 标 系 中 。 h a 参 数 为 tenter^表 示 坐 标 点 (0.5,0.8)在 水 平 方 向 上 是 文 字 的 中 心 , h a 是
horizontalalignment的缩写,其含义足水平对齐。© 调 用 figtext()在图表坐标系中添加文字。
程序的输出如图4-17所示。请读者使用缩放和平移工具改变子图的显示范围,你会发现数
据坐标系屮的文字将跟随曲线变动,而其他两个坐标系中的文字位置不变。单击绘图窗口工具
栏小的倒数第二个图标按钮,打 开 “SubplotConfigurationTool”对话框,调 W top、right、 bottom
和 le ft 等参数,你会发现子图坐标系中的文字也会跟着改变位H , 水平方向上它和子图的中心
始终保持一致。而图表坐标系中文字的位置,只有在改变窗口大小时才会发生变化。
Python科学计算 (第 2 版)

图4>17三个坐标系中的文字
m a t p l b绘

绘制文字的闲数还有许多关键字参数用于设置文字、外框的样式,请 读 者 参 考 matplotlib
的用户手册,这里就不再详细介绍了。
- 制精美的图表

通 过 pyplot模块的 annotate()绘制带箭头的注释文字,其调用参数如下:

annotate(s, xy, xytext=None, x y c o o r d s = 'd a t a ' t e x t c o o r d s = 'd a t a ', arrowprops=None^ …)

其 中 s 参数是注释文木,x y 是箭头所指处的坐标,xytext 是注释文木所在的坐标。xycoords


和 textcoords分别指定箭头坐标和注释文本坐标的坐标变换方式。
带箭头的注释需要指定两个坐标:箭头所指处的坐标和注释文字所在的坐标。而这两个坐
标可以使用不N 的坐标变换。参 数 xycoords 和 textcoords都是字符串,它们可以有表 4^5所示的
儿种选项:

表 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.4.1 Path 与 Patch

Patch 对象(块)楚一种拥有填充和边线的Artist 对象,例如多边形、椭岡等都是 Patch 对象,


它的边线由 Path 对象描述。而 Path 对象的 vertices 和 codes 属性是两个数组,分别用于描述坐标
点和每个坐标点对应的绘图命令代码。表4~6列出了各利|命令代码:

表 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属性:

rectjDatch = plt.Rectangle((0, 1), 2, 1)


rect_path = rect_patch.get_path()
rect_path.vertices rect_path•codes

[[0., 0.], [ 1, 2, 2, 2, 79]

[ 1。 0小
[ l.L
[0., 1小
[0., 0.]]

对照前面的命令代码表,很容鉍理解矩形萣如何绘制出来的。但萣细心的读者会发现,
vertices 中的坐标并不是我们创建矩形时指定的4 个顶点坐标。这是因为所有的矩形对象都共用
同一个 Path 对象,然后通过前而介绍过的 _ s f o r m 对象将单位矩形的 Path 对象变换到指定的
matplotlib绘

坐标之上。下面通过 get_patch_transfrom()获 得 Patch 的巫标变换对象,并用它将巾.位矩形的顶点


坐标变换为我们所创建矩形的顶点坐标:
- 制精美的图表

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 )

array([[ 0., l.L


[2., l.L
2.],

[0.,
[0., 1.]])

对 于 表 示 复 杂 曲 线 P atch 对象,可以通过i者如 InkScape 的矢m 图设计软件绘制并将曲


线保存成 S V G 文档,然 后 从 S V G 文档中提取曲线信息,创建相应的 Patch 对象。本书提供了
从简单的 S V G 文件中获取路径的 i*ead_sVg _path〇闲数,下面是用它绘制的 Python 阁标,参见
m 4 -18 :

scpy2.matplotlib.svg_path: 从 S V G 文件中获取简单的路径信息。可以使用该模块将矢
妒 量 绘 图 软 件 创 建 的 图 形 转 换 为 Patch 对象。

from scpy2.matplotlib.svg_path import read_svg_path

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 集合

当需要绘制大量图形时,可以使用从 Collection 类派生的各种集合对象。在绘制图形时,


Collection 对象会将其中多个保存绘图信息的列表传递给C ++编写的绘图函数,从而提高绘图速
度。表4~7列出了这些列表属性:,当列表长度不统一时,短列表中的元素将被循环使用。

表 4-7 Collection类的列表属性及说明

属性 说明
paths 绘阁路径

transfoims 坐标变换对象

cdgecoloi^ 边线颜色
facccoloi's 填充颜色
lincwidtlis 边线宽度

offsets 坐标偏移景

Collection 对象巾的路径从路径來标到屏蒂來标要进行三次处标变换:
• transform: 主坐标变换。
• transforms: 与 Collection 中悔条路径对应的路径变换。
Python科学计算 (第 2 版)

• transOffset和 offsets:坐标偏移用的变换对象和偏移量,对 o ffse ts 中的每个坐标采用


transOffset进行坐标变换得到实际的坐标偏移量。
根 据 offset_position 参数的值,分为如下两利1情况:screen(默认值)和data。
• screen: 坐标变换的顺序为一 路径变换、主变换、坐标偏移。
• data: 坐标变换的顺序为一坐标偏移、路径变换、主变换。
下面通过几个实例帮助读者理解上述各利1属性和变换。

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绘

的对数作为颜色映射表的输入值。

from matplotlib import collections as me


- 制精美的图表

lines = [ ]

with openC_butterfly.txt", "r") as f:

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 )

fig, (axl, ax2) = p i t . s u b p lo t s (1, 2, figsize=(8, 4))

lcl = mc.LineCollection(lines, colors=_’k", linewidths=l) ©

lc2 = mc.LineCollection(lines, cmap="Paired", linewidths=l, O

a r r a y = n p .l o g 2 (n p .a r r a y ([l e n (l i n e ) for line in lines])))

a x l .add_ c o l l ec t i o n (l c l )

ax2.add_collection(lc2)

for ax in axl, ax2:

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 曲线

l c l 和 lc2 中 都 有 145条路径,l c l 的 edgecolors 属性长度为1 , 因此所有的曲线都采用相同


的颜色。而 lc2 的 edgecolors 属性长度为145,其中的每个颜色都提通过norm〇和 cmap()计算得
到的。

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

number of lcl paths: 145


number of lcl colors: 1
number of lc2 colors: 145

True

下面显示路径变换、主变换、坐标偏移,可以看到唯一起作用的是主变换,它就是数据坐
标变换对象,它将丨11|线上各个点的坐标从数据坐标系转换到屏幕坐标系。

print l c l .get_transforms 〇 # 路径变换


print lcl.get_transform() is axl.transData # 主变换为数据少标变换对象
print l c l .g et_offset_transform(), l c l .g e t _ o f f s e t s ()

[]
True

IdentityTransform() [[0. 0.]]

使 用 LineCollection 可以绘制颜色或宽度渐变的曲线,例如下而对一个二维平面上的矢董场
积分,并将所得的路径保存到 streams列表中。它的长度为2 5 , 其中每个数组表示一条积分路
径,形状为(50,2)。

from scipy.integrate import odeint

def field(s, t):

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)

print len(streams), s t r e a m s [ 0 ] .shape

25 (50, 2)

为了采用渐变颜色显示每条积分路径,先 将 他 earn s 转换成一个三维数组 lines, 形状为


matpotlib绘

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

for r in streams], axis=0)

time_value = n p . c o n c a te n a t e ( [ t [ :-1]] * len(streams))


X, y = lines.mean(axis=l).T

u, v = field([x, y], 0)
speed_value = np.sqrt(u ** 2 + v ** 2)

fig) (axl, ax2) = prt.subplots(l, 2, figsize=(10, 3.5))


fig.subplots— adjust(0, 0, 1, 1)

axl.plot(init_pos[:, 0], init_pos[:, 1], "x")


a x 2 .p l o t (init j d o s [:, 0], init_pos[:, 1], "x")

lcl = me.LineCollection(lines, linewidths=2, array=time_value)

lc2 = mc.LineCollection(lines, linewidths=2> array=speed— value)

a x l .add_ c o l l ec t i o n (l c l )
ax2.add_collection(lc2)

prt.colorbar(ax=axl, mappable=lcl, label=u_.时间__)

plt.colorbar(ax=ax2, mappable=lc2, label=u__速度•')

2
41
for ax in axl, ax2:

a x .p l o t (init j d o s [:, 0], init 一pos[:, 1 ],"x")


ax.autoscale()

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)

多边形集合 PolyCollection 的用法和 LineCollection 类似,不过它会丨动封闭并填充颜色。下


而 的 star_polygon〇创建以(x ,
y )为中心、r 为半径、旋 转 theta 的 N 角星,s 参数为内圆的半径与外
圆半径的比值,效果如图4-21所示。在随机创建1000个 N 角星后,调 用 PolyCollection 绘制。

from numpy.random import randint, rand, uniform

def starj3 〇
lygon(x, y, r, theta, n, s ) :

angles = np.arange(0, 2*np.pi, 2*np.pi/2/n) + theta

xs = n * np.cos(angles)

ys = n * np.sin(angles)

xs[l::2] *= s

ys[l::2] *= s

xs += x

ys += y

return np.vstack([xs, ys]).T

stars = [ ]

for i in nange(1000):

star = starj x ) l yg o n (r a n d i n t (800)^ randint(500)^

uniform(5, 20), uniform(0, 2*np.pi),

randint(3, 9), uniform(0.1, 0.7))

49
Python 科学计算 (第 2 版 )

stars.append(star)

fig, ax = pit.subplots(figsize=(10, 5))

polygons = me.PolyCollection(stars, alpha=0.5, a r r a y = n p .r a n d o m .r a n d (l e n (s t a r s )))


a x •add 一c o l l e c t i on (p o l y g o n s )

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绘制大量多边形

每个多边形的填充颜色由颜色映射表决定,因 此 facecolcm 的长度与多边形的个数相同,


而所有的多边形共用边线颜色,所 以 edgecolcm 的 长 度 为 1。P d yC ollection 的坐标变换方式与
LineCollection 相同,就不再蜇复了。

print "length of facecolors:", l e n (p o l y g o n s .g e t _ f a c e c o l o r s ())


print "length of e d g e c o l o rs : ", l e n (p o l y g o n s .g e t _ e d g e c o l o r s ())

length of facecolors: 1000

length of edgecolors: 1

3•路径集合(PathCollection)

scatter()用于绘制散列图,它返回的是一个 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)

p c 是 一 个 PathCollection 对象,它 的 facecolors 长度为散列点数,颜色由颜色映射表决定。


edgecolors 的长度为1,所以每个圆形的边线颜色相同。
所有散列点的形状都是相似的,因 此 paths 屈性的氏度为1。每个散列点的大小由路径变
换 transforms 决定,它是一个形状为(30, 3, 3)的三维数组。每个散列点对应其中的一个3 X 3
的变换矩阵。下面是下标为0 的散列点的路径变换对象,它将路径在 X 轴 和 Y 轴方向上放大
5.916 倍:

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

ti加 sfo m is 决定散列点的大小,offset一transform 和 o ffse ts 决定散列点的位置,丨天丨此主变换


transform对象无须做任何变换,它是一个恒等变换。

print p c .get_ t r a n sf o r m ( )

IdentityT r a n sform()

由于 offset_position 的位为 screen,冈此坐标变换的顺序为:路径变换、主变换、坐細偏移。


路径变换对单位P 丨进行缩放,而坐标偏移则把圆形移动到指定的位置。

p c .get_off set jDosition ()

u'screen'
Python科学计算 (第 2 版)

4.摘圆集合(EllipseCollection)

EllipseCoUectiori用于绘制大量的椭圆,每个椭圆都可以拥有独立的长轴和短轴长度以及旋

转角度。它的参数如下:
EllipseCollection(self, widths, heights, angles, u n i t s = _p o i n t s ', **kwargs)

其 中 widths、heights 和 angles 是三个长度相同的数组,分别为每个椭圆的两个轴的长度以


及旋转角度。而 units参数则指定 widths 和 heights 的单位,有如下选项:

'points' | 'inches' | 'dots' | 'width' | 'height' | 'x' | 'y' | 'xy'

其中'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 轴的单位
- 制精美的图表

距离,由于 Y 轴的单位距离的像素点数小于X 轴的单位距离,因此右图屮的椭圆比左图的更扁


一些,而且由于两个方向上的长度单位不同,椭丨Ml呈现的方向也与 angles 指定的值不同。如果
使川0\6511】
^_05卩6^〇1^")将\和丫轴的单位长度设置为相同,则椭丨创的角度和01'^5指定
的角度相同,图中的12个椭圆将均匀地分布在一个正圆的圆周上。

angles = np.linspace(0, 2*np.pi, 12, endpoint=False)


offsets = np.c_[3*np.cos(angles), 2*np.sin(angles)]

angles 一deg = np.rad2deg(angles)

widths = np.full 一like(angles, 2)


heights = np.full 一like(angles, 1)

fig, axes = p i t . s u b p lo t s (1, 2, figsize=(12, 4))

ec0 = me.EllipseCollection(widths, heights^ angles_deg, u n i ts="x"J array=angles,


offsGts=offsets^ trans 〇ffset=axes[0].transData)
axes[0].add_collection(ec0)

axes[0].axis((-5j 5, -5, 5))

eel = me.EllipseCollection(widths, heights, angles_deg, units="xy"^ a r r a y=angles>


offsets=offsets, transQffset=axes[l].transData)

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.数据空间中的圆形集合对象

仿照图4>22(右),可以使用 EllipseCollection 绘制以数据空间中的长度为半径的關的集合,


只 需 要 将 w id th s 和 h eigh ts 设置为丨创形的直径即可。虽 然 在 collections 模块中还有一个

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

def set 一sizes(self, s i z e s ) :

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

return Collection.draw(self, render)


P y th o n 科学计算(第2 版)

〇设置每个丨创形对应的路径变换,它们将IM丨形缩放指定的倍数。©使用数据坐标转换对象
的缩放系数创建一个新的二维仿射变换对象,并将其设置为主变换。路径变换与主变换叠加起
来,就将半径为1 的单位_ 缩放为数据空间中半径为sizes 的1MI形。
每个岡形对应的路径变换将单位岡变换为数据坐标系中指定大小的岡形,然后通过主坐标
变换将数据坐标系中的圆形变换为屏幕坐标系中的大小,最后再由坐标偏移变换将圆形放到指
定的位置。
下而用 DataCircleColleclion 绘制一幅漂高的丨创形拼阁,效果如阁4~23所示。’
'venus-face.csvn
中的每一行有6 个数值表示一个圆形,每个数值的含义为:圆形 X 轴坐标、圆形 Y 轴坐标、半
径、红色、绿色和蓝色。

data = np.loadtxtC'venus-face.csv", delimiter:",")

offsets = data[:, :2]


sizes = data[:, 2] * 1.05

colors = data[:, 3:] / 256.0


m a t p l b绘

■fig, axe = pit.subplots(figsize=(8, 8))

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

阁本2 3 使)lj DataCircleCollection绘制大fi!:的圆形


2
5>
4
4 . 5 绘图函数简介

与本节内容对应的 Notebook 为: 04-m /


a tp lo tlib m a tp lo tlib -500-p l o t -f u n c t i o n s .i p y n b 〇

本节介绍如何使用 matplotlib 绘制一些常用的图表。matplotlib 的每个绘图函数都有许多关


键字参数用来设置图表的各种屈性,由于篇幅有限,本书不能对其一 一 进行介绍。一般来说,
如果读者需要对图表进行某利1特殊的设置,可以在绘图函数的说明文档或者 matploblib 的演示
页面中找到相关的说明。

4 . 5 . 1 对数坐标图

前面介绍过如何使用 plot〇 绘制曲线图,所绘制图表的 X -Y 轴坐标都是兑术坐标。下面我


们看看如何在对数坐标系中绘图。
绘制对数坐标图的函数有三个:semilogx()、semilogyO 、loglogO 。它们分别绘制 X 轴为对
数坐标、Y 轴为对数坐标以及两个轴都为对数坐标的图表。
下面的程序使用4 种不同的坐标系绘制低通滤波器的频率响应曲线,结果如图本24所示。
其中,左上图为 plot()绘制的算术坐标系,右上图为 semilogx〇绘制的 X 轴对数坐标系,左下图
为 semilogy()绘制的 Y 轴对数坐标系,右下阁为 loglog()绘制的双对数坐标系。使用双对数坐标
系表示的频率响应llll线通常被称为波特图。

w = np.linspace(0.1, 1000, 1000)


p = np.abs(l/(l+0.1j*w)) # 计 算 低 通 滤 波 器 的 频 率 响 应

fig, axes = pit.subplots(2, 2)

functions = ("plot", "semilogx", "semilogy", "loglog")

for ax, f name in zip (axes, ravel ()_» functions):


func = getattr(ax<, fname)
func(w, linewidth=2)
ax.set_ylim(0J 1.5)
P y th o n 科学计算(第2 版)

I
K
.4
.2

I
.0
.8

n
.6

4n a
L
.2
.0

0 20 0 400 600 8CX I0CK


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所示。

data = np.loadtxt("china j3opulation.txt")

width = ( d a t a [1,0] - d a t a [0,0])*0.4 O


p i t .f i g u r e ( f i g s i z e = (8, 4))
cl, c2 = plt.rcParams['axes.color_cycle'] [ :2]

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 〇绘图时,如果指定样式参数为只绘制数据点,那么所绘制的就是•一幅散列图。
例如:

pit.plot(np.random.random(1 0 0 ) ^ np.rand o m. r a n d o m ( 1 0 0 "o")

但是这利「
方法所绘制的点无法单独指定颜色和大小。scatteit)所绘制的散列图可以指定每个
m a t p l b绘

点的颜色和大小。下面的程序演示了 scatterO的用法,效果如图本27所示。

plt.figure(figsize=(8<> 4))
- 制精美的图表

x = np.random.random(100)
y = np.random.random(100)

prt.scatter(x, y, s=x*1000, c=y, marker=(5, 1),


alpha=0.8, lw=2, f a c e c o l o rs = " n o n e " )
plt.xlim(0, 1)

plt.ylim(0, 1)

合 ☆

it ☆

02
图本2 7 可指定点的颜色和大小的散列

scatter〇的前两个参数是两个数组,分别指定每个点的 X 轴 和 Y 轴的坐标。s 参数指定点的

大小,其值和点的面积成正比,可以是单个数值或数组。

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)的单字节无符号整数数组。这是因为通常所使用的
图像采用单字节分别保存每个像素的红、绿、蓝三个通道的分

img = p i t .i m r e a d ("l e n a .j p g " )

print img.shape, img.dtype

(393, 512, 3) uint8

K 面使用 imshow〇显 示 im g 所表示的图像:


Oimshow ()可以用来显示 imread()所返回的数组。如果数组是表示多通道阁像的三维数组,

则每个像素的颜色由各个通道的值决定。
© 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))

f i g . s u b p lo t s _ a d j u s t (0, 0, 1, 1, 0.05, 0.05)

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

axe_img = axes[5].imshow(img[:, 0]) ©


p i t •colorbar(axe_img, a x = a x e s [5])

axe_img = axes[6].imshow(img[:, 0], cmap="copper") O


matplotlib绘

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

图4~28 用 imrcad()和 imshow()M 小•图像

颜色映射表萣一个 C o b iM a p 对象,m atpbtlib 中已经预先定义了很多颜色映射表,可以通


过下面的语句找到这些颜色映射表的名字:

import matplotlib.cm as cm
cm._cmapnames[:5]

[■Spectral', ’copper', 'RdYlGn', 'Set2', 'summer’]

使 用 imshow()可以显示任意的二维数据,例如卜面的程序使用图像直观地显示了二元阑数
f (x ,y ) = x e x2_y2,效果如图 4^29 所示。
y, x = np.ognid[-2:2:200j, -2:2:200j]

z = x * np.exp( - x**2 - y**2) O

extent = [np.min(x), np.max(x), np.min(y), np.max(y)] ©

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.imshow(z, extent=extent, cmap=cm.gray, origin="lower")

plt.colorbar()

matplotlib绘
- 制精美的图表
图4 ~ 2 9 使川imshow ()可视化二元函数

〇首先通过数组的广播功能计算出表示函数值的二维数组z , 注意它的第0 轴表示 Y 轴、


第 1 轴 表 示 X 轴。© 然 后 将 X 、Y 轴的取值范围保存到 extent列表中。€)将 extent列表传递给
imshow()的 extent参数,这样图表的 X 、Y 轴的刻度标签将使用 extent列表指定的范围。

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)

extent = [np.min(x), np.max(x), np.min(y), np.max(y)]

p i t .figu r e (figs i z e = (10,4))


p i t . s u b p lo t (121)
P y th o n 科学计算(第2 版)

cs = pit.contour(z, 10, extent=extent) ©

plt.clabel(cs) ©
p i t . s u b p lo t (122)

p i t .contourf(x.reshape(-1), y.reshape(-l)., z, 20) O


matplotlib绘

图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 的収值点,然后计算出数

组中每个 x 所对应的 y 值。可以使用等值线解决这个问题,显然隐函数的曲线就是值等于〇的


那条等值线。下面的程序绘制函数:
f (x ,y ) = (x 2 + y 2)4 —(
x 2 —y 2)2

在f (x ,y ) = 0和f (x ,y ) —0.1 = 0 时的曲线,效果如图丰31(左)所示。

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)

extent = [np.min(x), np.max(x), np.min(y), np.max(y)]


cs = pit.contour(f, extent=extent, levels=[0, 0.1], O
colors=["b", "r"], linestyles=["solid", "dashed"], linewidths=[2, 2])

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

〇在 调 contour()绘制等值线时,可以通 过 levels 参数指定等值线所对应的函数值,这里

- 制精美的图表
设 置 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(右)
所示。

0 -0.5 0.0 0.S 1.0 -1.0 -0.5 0.0 0.5

图使用等值线绘制隐函数曲线(左),获取等值线数据并绘图(右)
6
2
4/
Python科学计算 (第 2 版)

contour〇返问一个 QuadContourSet对象,其 collections 屈性是-个等值线列表,每条等值线


川一个 LineCollection 对象表示:

print cs

cs.collections

<matplotlib.contour.QuadContourSet instance at 0x057EFC10>

<a list of 2 mcoll.LineCollection objects>

每 个 LineCollection 对象都有它& 己的颜色、线型、线宽等屈性,注意这些屈性所获得结果


的外面还有一层包装,要获得其第0 个元素才是真正的配置:

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 屈性可以获得路径上所有点的來标:

path = c s . c o l l e ct i o n s [ 0 ] .get j3aths()[0]

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 个顶点:
第一个四边形的顶点第二个四边形的顶点

(0, 0 ), (1, 0 .2) (1, 0 .2), (2, 0 )


(0, 1) , ( 1, 0 .8) (1, 0 .8), (2, 1)

每个四边形的填充颜色与Z 中的一个元素对应:

X = np.array([[0, 1, 2],
[〇, 1, 2]])
Y = np.array([[0, 0.2, 0],

[1, 0.8, 1]])


Z = np.array([[0.5, 0.8]])

下面将 X 和 Y 平坦化之后用 pbt 〇绘 制 出 这 些 顶 点 的 坐 然 后 调 用 pcobrmeshO 绘制这两


个四边形。
与左边的四边形对应的颜色映射值为0.5,与右边的四边形对应的颜色映射值为0.8,
因此一个显示为蓝色,另一个显示为红色。

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 -

0.0 0.5 1.0 1.5 2.0

图4 * 3 2 演示pcolormeshO绘制的卩q 边形及其填充颜色

在下而的例子中,使 用 pcolormeshO绘制复数平而上的坐标变换。在 图 4~33 中,左侧的图

表显示 s 平面上的矩形K 域,右侧的图表显示通过公式z = ^坐 标 变 换 之 后 的 网 格 ,左侧中的


2-s

矩形被变换成右侧同颜色的四边形。由于 axes[2]和 axes[3]中的网格由近4 万个四边形组成,为


了在输出 S V G 图像时提高绘图速度,这 里 将 rasterized 参数设置为 T r u e , 这些叫 边形将 作为 •
幅点阵图像输出到 S V G 图像中。

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

fig, axes = p i t . s u b p lo t s (2, 2, figsize=(8, 8))

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)

a x e s [0].pcolormesh(si.real, sl.imag, np.abs(sl))


a x e s [1].pcolormesh(zl.real, zl.imag, np.abs(sl))

axes[2].pcolormesh(s2.neal, s2.imag, np.abs(s2), rasterized=True)

axes[3].pcolormesh(z2.real, z2.imag, np.abs(s2), rasterized=True)


m a t p l b绘
- 制精美的图表

图 使 用 pcolormeshO绘制复数平面上的生标变换

66
还可以在极坐标中使用 pcolormeshO绘制网格,下面的例子使叫 mgridU创建极坐标中的等
间隔网格,然后在 projection 为 polai•的子图中绘制这个网格:

def func(theta, r):

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)

a x = p l t .s u b p l o t (111, proje c t i on = "p o l a r "^ aspect=l.)

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:

data = { " p o i n t s " : [], " t r iangles":[], " v a l u e s " :[]}


values = None
for line in f:
line = line.strip()
if not line:
continue
if line.startswith(,,# n ):

values = d a t a [ l i n e[ l : ]]
continue

v a l u e s .a p p e n d ( [float(s ) for s in line.split()])

data = {key:np.array(data[key]) for key in data}

然后就可以调用 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 ( )

fig, ax = pit.subplots(figsize=(12, 4.5))


a x .set_a s p e ct ("e q u a l ")

mapper = ax.tripcolon(X, Y, triangles, values, cmap="gray") O


p i t •colorbar(mapper, label=u" 温度")

plt.triplot(X, Y, triangles, lw=0.5, alpha=0.3, color="k") ©

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~35 使用 tripcolor〇和 tricontour〇绘 制 : 格 等 值 线

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 是箭头对应的值。

def f(x, y):

return x * np.exp(- x**2 - y**2)

def vec_field(f, x, y, dx=le-6, dy=le-6):

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)

line, = pit.plot(x, y, lw=l)

lengths = np.cumsum(np.hypot(np.diff(x), np.diff(y)))

length = l e n g t h s [-1]

arrow_locations = np.linspace(0, length, n, endpoint=False)


index = n p .s e a r c h s o rt e d (lengths, arrow_l o ca t i o n s )
dx = x[index + 1] - x[index]
dy = y[index + 1] - y[index]
ds = np.hypot(dx, dy)
dx /= ds

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绘制起点和终点的箭头集合。下面的例子绘制神经网络结构示意图,效
- 制精美的图表

果如图本39所示。为了让箭头能够连接两个神经节点,将 scale_units 设置为”


xy ”
,将 angles 设
置为"xy " , 并且将 scale 设 置 为 1。这样箭头的长度就为箭头对应的矢量在数据空间中的长度。

levels = [4, 5, 3, 2]
x = np.linspace(0, 1, len(levels))

for i in range(len(levels) - 1):

j = i + 1
nl, n2 = levels[i], levels[j]

yl, y 2 = np.mgrid[0:l:nl*lj, 0:l:n2*lj]


xl = np.full_like(yl, x[i])
x2 = np.full_like(y2j x[j])
plt.quiver(xlj yl, x2-xl, y2-yl,
angles="xy", units="dots", scale 一units="xy .、
scale=l, width=2, headlength=10,

headaxislength=10, headwidth=4)

yp = np.concatenate([np.linspace(0, 1, n) for n in levels])

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 .维绘图

mpl」oolkits.mplot3d 模 块 在 m atplotlib 的 避 础 上 提 供了 三维 作 图的 功能 。 由于它使用


matplodib 的二维绘图功能实现三维图形的绘制工作,因此绘图速度有限,不适合;HJ于大规模数
据的三维绘图。如果读者需要更复杂的三维数据可视化功能,请阅读 T V T K 与 M ayavi 章节。
下面是绘制三维曲面的程序,程序的输出如图4>40所示。

matplotlib绘
import mpl_toolkits.mplot3d O

- 制精美的图表
x, y = np.mgrid[-2:2:20j, -2:2:20j] ©
z = x * np.exp( - x**2 - y**2)

fig = plt.figure(figsize=(8, 6))


ax = pit.subplot(111, projection='3 d ’) €)
ax.plot_sunface(Xj y, z, rstride=2, cs t r i d e = l : cmap = plt.cm.Blues_r) O
ax.set_xlabel("X")
a x . s e t _ylabel(MY M)
ax.set_zlabel("Z")

2.0 •邡

阁4 ~ 4 0 使用mplot3D 绘制的三维丨II丨而阁
Python科学计算 (第 2 版)

〇lvr 先载入 mplot3d 板块,


matplotlib中三维绘图相关的功能均在此板块T 定义。
© 使用 mgrid
创 建 X -Y 平面的网格并计算网格上每点的高度z 。[*1于绘制三维丨山面的函数要求芄X 、Y 和 Z
轴的数据都用相同形状的二维数组表示,
因此这里不能使用ogrid 创建。
和之前的 imsh〇
w ()不同,
数组的第0 轴可以表示 X 和 Y 轴中的任意一个,在本例中第0 轴表示 X 轴 ,第 1轴表示 Y 轴。
© 在当前图表中创建一个子图,通 过 projection 参数指定子图的投影模式为”
3d n,这样
subplot()将返回一个用于三维绘图的Axes 3D 子图对象。

投影模式
投影模式决定了点从数椐坐标转换为屏幕坐标的方式。可以通过下面的语句获得当前有效
的投影模式的名称:

>>> from matplotlib import projections


>>> p r o j e c t i o n s .get__projection_names()
['3d', 'aitoff', 'hammer', 'lambert', 'mollweide'^ 'polar', 'rectilinear']

只有在载入 mplot3d 模块之后此列表中才会出现'3d’


投影模式。’
aitoff 、’
hammer'、lam beif 、
matplotlib绘

'mollweide’
等均为地图投影,’
polar’
为极坐标投影,’
rectilinear'则是默认的直线投影模式。
- 制精美的图表

O 调 用 A xes3D 对象的 plot_surface〇绘制三维丨I丨丨面图。其中参数 x 、y 、 z 都是形状为(20,20)


的二维数组。
数 组 x 和 y 构成了 X -Y 〒面上的网格,而数组 z 则是网格上各点在曲面上的収值。
通 过 cm ap 参数指定值和颜色之间的映射,即曲面上各点的高度值与其颜色的对应关系。stride
和 cstride 参数分別是数纟11的 第 0 轴 和 第 1 轴的下标间隔。对于很大的数纟J1,使用较大的间隔可
以提高曲面的绘制速度。
除了绘制三维曲面之外,Axes 3D 对象还提供了许多其他的三维绘图方法。请读者在官方
网站查看各种三维绘图的演示程序。

4.6 m atplotlib 技巧集

与本节内容对应的 Notebook 为:04>matplotlib/matplotlil>600-tips.ipynb。


DVD

作为本章的最后一节,让我们介绍一些比较特别的用法。

4 . 6 . 1 使 用 a gg 后台在图像上绘图

matplotlib绘制的图表十分细腻,这是因为它的后台绘图库是用C ++开发的高质M 反锯齿二


维绘图库:Anti-Grain Geometry (A G G )。如果想绘制一些二维图形,但小需要 matplotlib的图表

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

am = np.frombuffer(buf, np.uint8) .reshape(h, w, -1)


print a m . s h a p e

(250, 250, 4)

matplotlib绘
RendererAgg 对象提供 T 一 些 draw_*()方法用于在I叫布上绘图,例如下面的代码首先创建一
个 Path 对象,然后调用 renderer.draw_path〇在 N 布上绘制该 Path 对象,如图4~41所示。其第一

- 制精美的图表
个参数为一个 GraphicsComextBase对象,用来设置绘图吋的一些屈性,例如线宽、线条颜色等。
第三个参数是•个來标变换对象,在本例中使用恒等变换,第 4 个参数为路径的填充颜色。

from matplotlib.path import Path

from matplotlib import transforms

path_data = [
(Path.MOVETO, (179, 1)),

(Path.CURVE4, (117, 75)),

(Path.CURVE4, (12, 230)),

(Path.CURVE4, (118, 230)),


(Path.LINETO, (142, 187)),

(Path.CURVE4, (210, 290)),

(Path.CURVE4, (250, 132)),

(Path.CURVE4, (200, 105)),

(Path.CLOSEPOLY, (179, 1)),

code, points = zip(*path_data)

path = Path(points, code)

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)

r e n d e r e r .d n a w _ p a t h (g c , path, t r a n s f o r m s .I d e n t ityTransform()y (0, 1, 0))

也可以使用 matplotlib中提供的 Artist对象绘图。下 面 首 先 创 建 -个 Circle 对象和一个 Text


对象,然后调用它们的 draw()方法在画布上绘图。由 于 T e x t 对象在绘图时需要获収画布的dpi
属性,因此在调用 draw()之前先将其 figure 属性设置为 renderer。

from matplotlib.patches import Circle


from matplotlib.text import Text

c = Circle((w/2, h/2), 50, edgecolor="blue", facecolor=.’yellow", linewidth=2, alpha=0.5)


c.draw(renderer)

text = Text(w/2, h/2, "Circle", va="center", ha="center")

text.figure = Tenderer
t e x t .draw(renderer)
m a t p l b绘

为了在 IPython Notebook 中显示 arr 所表示的图像,可以调用 pypolt.imsave()。其第一个参数


可以是文件名或者拥有文件接口的对象,这 里 使 用 B ytesIO 对 象 将 P N G 阁像的内容保存在
- 制精美的图表

png_b u f 中。然后使用 IPython 的 dLsplay_png〇juS:


示该内禪中的 P N G 阁像。

from io import BytesIO


from IPython.display import display_png

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 轴方向向上,和数学上的笛卡尔坐标系的定义相同。

scpy2.matplotlib.ImageDrawer: 使 用 RendererAgg 直接在图像上绘图,以方便用户在图


啄像上标注信息。

from scpy2.matplotlib import ImageDrawer


img = pit.imread("vinci 一
target.png")

drawer = ImageDrawe r (i m g )
d r a w e r .setj3arameters(lw=2, color="white", alpha=0.5)

drawer.line(8, 60, 280, 60)


drawer.circle(123, 1 3 0 ,50, facecolor="yellow", lw=4)
drawer.markersC'x", [82, 182], [218, 218], [50, 100])

drawer.rectangle(81, 330, 100, 30, facecolor="blue")


drawer.text(10, 50, u"Mona Lisa", fontsize=40)

drawer.ellipse(119, 255, 200, 100, 100, facecolor="red")

%array_image drawer.to_array()

图 4*42使用木书提供的ImageDrawer在图像上绘图

4 . 6 . 2 响应鼠标与键盘事件

为了在 Notebook 中执行本节代码,需要启动 G U I 事件处理线程,例如执行% gui q t 和


^, %matplotiib qt ,将启动 Q t 的 G U I 线程,并 将 Qt4A g g 设置为 matplotlib 的绘图后台。如
果希望切换到嵌入模式,可以再执R%matplotlib inline。

界面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 ()

f i g .c a n v a s •m p l _ c o n n e c t ('k e y _ p r e s s _ e v e n t ', on_key_press)


- 制精美的图表

control
ctrl+a
alt
alt+1

可 以 通 过 mpl_ co n n ect 的帮助文档查看所有的事件名称。表 4-8列出了其支持的所有事


件名:

表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:
(续表)

事件名 含义

figu re_ leave_ even t 鼠标移出阁表

axes_ enter_ event 鼠标移进子阁

a x e s 一leave_ even t 鼠标移出子阁

d o s e 一even t 关闭图表

当前所有注册的响应函数可以通过Figure.canvas.callbacks.callbacks 查看。下而的程序输出所
有的事件响应函数,响应函数的执行按照显示的顺序进行,可以看出除了我们绑定的响应函数
之外,matplotlib还绑定了处理快捷键的键盘响应函数。

for k e y , funcs in fig.canvas.callbacks.callbacks.iteritems() :


print key

for cid, wrap in sorted(funcs.items()):


func = wrap.func
print " {0}:{l}.{2}".format(cid, func._ module_ , func)

key_press_event

3:m a t p l o t l i b .b a c k e n d _ b a s e s .<function key_press at 0x093A4C30>


6 : _ main— •〈function on 一key 一press at 0x09B05FB0>

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〇重新

绘制整个图表。

scpy2.matplotlib.key _event_change_color: 通过按键修改曲线的颜色。

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: 鼠标在数据坐标系中的坐标。

下面的程序显示了鼠标事件的各种信总:

scpy2.matplotlib.mouse_event_ show_ info : 显示子图中的鼠标事件的各种信息,

import sys
fig, ax = pit.s u b p lo t s ()

text = a x . t e x t (0.5, 0.5, 'event", h a = ncenter", v a = ncenter", f o n t d i c t = { ,,size":20})

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(

event . n a me , event.button, event.x, event.y, event.xdata, event.ydata)


t e x t .s e t _ t e x t (info)
fig.canvas.draw()

f i g . c a n v a s . m p l _ c o n n e c t ( 'b u t t o n j 3 re s s _ e v e n t ', on_mouse)

f i g •c a n v a s •mpl 一c o n n e c t ('button 一release 一e v e n t 、 on_mouse)


fig.canvas.mpl 一c o n n e c t ('motion 一notify 一
e v e n t 、 on 一
mouse)
程序的执行结果如图4 4 3 所示:

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

°%.o 0.2 0.4 0.6 0.8 1.0

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

scpy2.matplotlib.mouse_event_ move_polygon : 演示通过鼠标移动 Patch 对象。

from numpy.random import rand, randint

from matplotlib.patches import RegularPolygon

class P a t c h M over(object):

def — init— (self, ax):

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)

def onj3 r e s s (s e l f > e v e n t ) : O


patches = s e l f . a x . pa t c h e s [:]
p a t c h e s .s o r t (key=lambda p a t c h :p a t c h .g e t _ z o r d e r ( ))

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:

pos = n p . a r r a y ([event.xdata^ event.ydata])


- 制精美的图表

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

zord e r = r an d i n t (10, 100))


a x .add j D a t c h (poly)
ax.relim()

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 )

x = np.linspace(0, np.pi*2, 100)


y = np.sin(x)
line, = pit.plot(x, y, picker=8.0)

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

f i g .c a n v a s .m p l _ c o n n e c t ('p i c k _ e v e n t ', on_pick)

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个元素为保符详细信息的字典。

scpy2.matplotlib.mouse_event_ highlight_curve: 鼠标移到曲线之上时高亮显示该曲线。


D VD

import matplotlib.pyplot as pit


import numpy as np

class C u r v e H i g hL i g h t e r ( o b j e c t ) :
matpotlib绘

def — init_ (self, ax, a l p ha=0.3> l i n e w i d t h = 3 ) :


self.ax = ax
- 制精美的图表

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:

lw = self.linewidth if line is target else 1


if line.get 一linewidth( ) 丨
= lw:

line.set_linewidth(lw)
need 一redraw = True

alpha = 1.0 if lw == self.linewidth else self.alpha

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

for line in ax.lines:


if line.co n ta i n s ( e v t ) [0]: ©

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)

from scipy.special import jn

for i in range(l, 10):

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

Line2D .set_ydata〇设S l l l l 线的 Y 轴数据。© 调用 Figure.canvas.dmw ()重绘整张图表。

import numpy as np

import matplotlib.pyplot as pit

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

x = n p . l inspace(0 ,10, 1000)

line, = ax.plot(x, np.sin(x), lw=2, animated=True) O

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 中制作动画的原理,不过在实际使用


中,一般会使用 animation模块制作动画效果。例 如 FuncAnimation 对象将定期调用用户定义的
函数来更新图表中的元素。
©创建曲线对象时,设 置 animated参数为 True 。© 在动画回调函数 update_ line〇中设置所有
动画元素的数据,它有•个参数为当前的 M 示巾贞数,这里使用巾贞数修改波形的相位,并 返 冋 …
个包含所有动画元素的序列。© 创 建 FuncAnimation 对象定时调用 update_ line(), interval参数为
每秒的帧数,b l i t 为 T r u e 表示使州缓存加速每帧图像的绘制。fm m es 参数设置最大帧数,

matplotlib绘
update_line〇的巾贞数参数将在0 到 99之间循环变化。

import numpy as np

- 制精美的图表
from matplotlib import pyplot as pit

from matplotlib.animation import FuncAnimation

fig, ax = p i t . s u b p lo t s ()

x = np.linspace(0, 4*np.pi, 200)


y = np.sin(x)

line, = ax.plot(Xj y, lw=2, animated=True) O

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] ©

ani = FuncAnimation(fig, update_line, blit=True, interval=25, frames=100) ©

若想将动画保存成动画文件,可以调用如下方法:

matplotlib会使用系统中安装的视频压缩软件(如 ffmpeg.exe )生成视频文件。


请读者确认
A 视频压缩软件的可执行文件的路径是否在 PATH 环境变量中。

a n i . s a v e ( 'sin_wave.m p 4 ' , fps=25)


Python科学计算 (第 2 版)

4 . 6 . 4 添 加 G U I面板

在 matplotlib 的主页中可以找到将图表嵌入各种主流界面库的演示程序,这些程序都是首
先创建一个 G U I 窗 U , 然后将图表作为控件嵌入窗口中。本节介绍一种更加简洁的方法:为
matplotlib 图表窗口添力口 G U I 控件的控制而板。

scpy2.matplotlib.gui_panel: 提供了 T K 与 Q T 界面库的滑标控件面板类 TkSliderPanel


和 QtSliderPanel。tk_panel_demo.py 和 qt_panel_demo.py 为其演示程序◊

下 面 是 使 用 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 ) : ©

return A * np.sin(2 * np.pi * f * x + p) * np.exp(z * x)

figj ax = pi.su b p l ot s ()

x = np.linspace(le-6, 1, 500)

pars = {"A":1.0, "z":_0.2, "p":0}

y = exp 一sin(x, **pars)


line, = pi.plot(x, y)

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)],

update, cols=2, min— value_width=80)


p a n e l .set_ p a r a me t e r s (* * p a r s ) O

matp — b绘
fig.show() Q

—制 精 美 的 图 表

图4 ^ 4 5 为图表添加G U I 控件:T K (左图)和 Q T (右图)


8
2
9
解5 亭

Pandas- 方便的数据分析库
N um Py 虽然提供了方便的数组处理功能,但它缺少数据处理、分析所需的许多快速工具。
Pandas 堪 于 N um Py 开发,提供了众多更高级的数据处理功能。Pandas 的帮助文档十分全面,因
此本章主要介绍 Pandas 的一些;
®本概念和帮助文档中说明不够详细的部分。希望读者在阅读本
章之后能更容易阅读官方文档。

import pandas as pd
pd._ version 一

•0.16.2'

5.1 Pandas中的数据对象

与本节内容对应的 Notebook 为:05-pandas/pandas-100~dataobjects.ipynb。

Series 和 DataFrame 是 Pandas 中最常用的两个对象。本节介绍这两种对象的基本概念以及

常用属性,在后续章节将介绍对它们进行操作和运箅的各种函数和方法。

5.1.1 Series 对象

Series 楚 Pandas 中最雜本的对象,它定义了 N um Py 的 ndarray对象的接口_ array_〇 ,因此


可以用 N um Py 的数组处理函数直接对Series 对象进行处理。Series 对象除了支持使用位置作为
下标存:取元素之外,还可以使用索引标签作为下标存取元素,这个功能与字典类似。每 个 Series
对象实际上都由两个数组组成:
• index: 它是从 ndarray数组继承的 Index 索引对象,保存标签信息。若创建 Series 对象时
不指定 index, 将自动创建一个表示位置下标的索引。
• values: 保存元素值的 ndamiy 数组,N um Py 的函数都对此数组进行处理。
下面创建一个 Series 对象,并查看上述两个屈性:

s = pd.Series([l, 2, 3, 4, 5], index=["a", "b", "c", "d", "e"])


print u " 索引:
" ,s.index

print u" 值数组:


" ,s.values
P y th o n 科学计算(第2 版)

索引:Index([u'a', u'b', u'c', u ’d', u'e']) d t y p e = ' o b j e c t ’



值数组: [1 2 3 4 5]

Series 对象的下标运算同时支持位置和标签两种形式:

print u "位置下标 s [ 2 ]:.、 s[2]

print u "标签下标 S [ . d . ] :", s[.d.]

位置下标 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 数组一样,还可以使用位置列表或位置数组存取元素,冋样也可以使用标签列
表和标签数组。

s[[l,3,2]] s[[ ' b ' / d ' / c ' ] ]

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

当两个 Series 对象进行操作符运总时,Pandas会按照标签对齐元素,也就是说运算操作符


会对标签相同的两个元素进行计兑。在下面的例子中,s 中标签为V 的元素和 s2 中标签为W的
元素相加得到结果中的22。当某一方的标签不存在时,默 认 以 NaN(Not a Number)填充。由于
N aN 萣浮点数中的一个特殊值,因此输出的 Series 对象的元素类型被转换为 float64。

s2 = pd.Series([20,30,40,50,60], i n d e x s r b ' V c ' V c T / V V f " ] )

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

DataFmme对象(数据表)是Pandas 中最常用的数据对象。Pandas 提供了将许多数据结构转换


为 DataFrame 对象的方法,还提供了许多输入输出函数来将各利1文件格式转换成 DataFrame X、j
象。在介绍这些函数之前,我们需要先理解 DataFrame对象中的一些概念。

1. DataFrame 的各个组成元素

下面的程序调用 read_csv〇从 Soils-simple.csv读入数据,通 过 index_col参数指定第 0 和 第 1

Pandas方
列为行索引,
用 parse_dates参数指定进行日期转换的列。
在指定列时可以使用列的序号或列名。
所得到的 DataFmme对象如图 5 - 1 所示,图中标识出了 DataFmme的各个组成部分的名称。

- 便的数据分析库
列索引名 列索引

M 镛 ltur嫌 t 1 pH D«nt C« Conduc 〇 Mt9 Nam«


行索引名 Contour
1
1 D«pr«tsion 5^525 0 9775 10 6850 1 4725 201M S-26 L〇4
|
0-10 Slop* 5 5075 1 0500 12 2475 2 0500 201MM-30 Roy
1
| Top 5 3325 1 0025 13 3850 1 3725 201W)5-21 Roy
行索引乂 1
| D«pr«Mlon 4 8800 V3575 7 5475 5.4600 20154)3-21 Los
Il0<30 Slop* 5 2825 1.3475 9 5150 49100 201S-02-06 Otana

l L Top 4*^ 00 1 M 25 JO 2375 3.5625 2015^)4-11 Duina

行 列 奴据
第e 圾 索 引 第 1圾
阁 5-1 DataFmme的结构

df_soil = p d .r ead_csv("data/Soils-simple.c s v " , index_col=[0, p a r s e_dates=["Date"])


df soil.columns.name = "Measures"

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

勺数组类似,通 过 shape属性可以得到 DataFrame 的彳/ 数和列数:

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方便的数据分析库

Index([u'pH,, u ’Dens', u'Ca', u'C o n d u c u ' D a t e ’, u ’Name'],


dtype=■object、 name=u'Measures')
Measures

行索引是一个表示多级索引的Multiindex对象,每级的索引名可以通过 names 属性存取:

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

与二维数组相同,DataFrame 对象也有两个轴,它 的 第 0 轴为纵轴,第 1 轴为横轴。当某


个方法或函数有 axis、orient等参数时,该参数可以使用整数0 和 1 或者Hi n d e x m Mcolumns”
来表
示纵轴方向和横轴方向。
U 运算符可以通过列索引标签获取指定的列,
当下标是单个标签时,
所得到的楚Series对象,
例如 df_soill’’pH'’],而当下标是列表时,
则得到一个新的DataFmme 对象,
例如 df_soilU”Densn,
’’C a”JJ:

df_soil[••pH" ] df_soil[["Dens, "Ca_•]]

Depth Contour Measures Dens Ca


0-10 Depression 5.4 Depth Contour
Slope 5.5 0-10 Depression 0.98 11
Top 5.3 Slope 1.1 12

94
10-30 Depression 4.9 Top 1 13

Slope 5.3 10-30 Depression 1.4 7.5

Top 4.8 Slope 1.3 9.5

Name: pH, dtype: float64 Top 1.3 10

.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[]的用法非常丰富,下一节还会详细介
绍它的各种用法。

df_soil.loc["0-10"> "Top"] d f _ s o i l .l o c ["10-30"]

Measures Measures pH Dens Ca Conduc Date Name


pH 5.3 Contour

Dens 1 Depression 4.9 1.4 7.5 5.5 2015-03-21 Lois

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

valu es 属 性 将 DataFrame 对象转换成数纟11,由于本例中的列类型不统一,所得到的数纟J1是


一个元素类型为 object 的数组。

d f _ s o i l .v a l u e s .dtype

dtype('CV)

2.将内存中的数据转换为 DataFrame 对象

调 用 DataFrame〇可以将多种格式的数据转换成DataFrame对象,它的三个参数 data、 index


和 columns 分别为数掘、行索引和列索弓丨。data 参数可以是:
•二维数组或者能转换为二维数组的嵌套列表。
• 字 典 :字典中的每对“键-值”将 成 为 DataFrame 对象的列。值可以是一维数组、歹嫌
或 Series 对象。
在下面的程序中,O 将一个形状为(4, 2) 的二维数组转换成 DataFrame 对象,通 过 index 和
colum ns 参数指定行和列的索弓丨。© 将字典转换为 DataFrame 对象,其列索引 hid字典的键决定,
行索引 hl:l index 参数指定。€)将结构数组转换为 DataFrame对象,其列索引由结构数组的字段名
决定,行索引默认为从0 幵始的整数序列。

dfl = pd.DataFrame(np.random.randint(0, 10, (4, 2)), O


index= [ __A " B " , " D " ],
2
9
4
0
Python 科学计算 (第 2 版)

col u m n s = [ Ma M:> Mb n ])

df2 = pd.DataFrame({"a":[l, 2, 3, 4], "b":[5, 6, 7, 8]}, ©


index=["A", "B", "C__, "D"])

arr = np.array([("iteml", 1 ),(


"item2% 2 ),(
"item3 ",3 ),(
"item4 ",4 )],

dtype=[("name", "10S"), ("count", int)])

df3 = pd.DataFrame(arr) ©

dfl df2 df3

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丨

此 外 还 可 以 调 用 以 from_ 开头的类方法,将 特 定 格 式 的 数 椐 转 换 成 DataFram e 对 象 。


a s方便的数据分析库

from_dict()将字典转换为 DataFrame 对象,其 orient参数可以指定字典键对应的方向,默认值为


"columns”
,表示把字典的键转换为列索引,
即字典中的每个值与一列对应。
而 orient参数为"index"
吋,字典中的每个值与一行对应。当字典为嵌套字典,即字典的值为字典时,另外一个轴的索
引值由第二层字典屮的键决定。下面分别将列表字典和嵌套字典转换为DataFmme对象。嵌套
字典中缺失的数据使用N aN 表示:

dictl = {"a":[l, 2, 3], "b":[4, 5, 6]}


dict2 = {"a":{"A_.:l, "b":{__A..:3, "C..:4}}

dfl = pd.DataFrame.from 一
diet(dictl, orient="index")
df2 = pd.DataFrame.from_dict(dictl, o r i e n t = ncolumns")

df3 = pd.DataFrame.from_dict(dict2, orient="index")


df4 = p d .D a t a F r a m e .from_dict(dict2, orient="columns")

dfl df2 df3 df4

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

from_ items〇将 “(键,


值)”序列转换为 DataFrame对象,其 中 “值 ”是表示一维数掘的列表、
数组或 Series 对象。当其 orient参数为"index"时,需要通过 columns 指定列索引。

items = d i c t l . i t em s ()

dfl = pd.DataFrame.from_items(itemsJ o r i e n t ="index"J columns=["A"J "B''^ "C"])


df2 = p d .D a t a F r a m e .f r o m _ i t e m s (i t e m s , orient="columns")

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

3.将 DataFrame 对象转换为其他格式的数据

to_dict〇方法将 DataFrame对象转换为字典,它 的 orient参数决定字典元素的类型:

print df2.to— d i e t ( orient="records •’


)#字典列表
print df2.to_dict(orient="list") #列表字典
print df2.to_dict(orient="dict") # 嵌®字典

[{'a': 1, 'b': 4>, {'a': 2, 'b': 5}, {'a': 3, *b': 6}]


{ • a 1: [1, 2, 3], 'b': [4, 5, 6]}

{*a': {0: 1, 1: 2, 2: 3>, 'b': {0: 4, 1: 5, 2: 6}}

Pandas方
to_ records〇方法可以将 DataFrame 对象转换为结构数纽,名:其 index 参数为 True(默认值),

- 便的数据分析库
则其返回的数组中包含行索引数据:

print d f 2 •to 一r e c o r d s ()•dtype


print df2.to_records(index=False).dtype

[ ( ’i n d e x 、 •<i8,), ( ’a 、 ,
<i8.), C b ’, .<i8’)]
[(•a., '<i8.), (.b、 ,
<i8.)]

Pandas 还提供了许多全局函数来从各利4各式的文件读取数据,而各种以 t o 开头的方法可以

将其输出到文件中,关于文件的输入输出将在后面详细介绍。

5.1.3 Index 对象

In d ex 对象保存索引标签数据,它可以快速找到标签对应的整数下标,这种将标签映射到
整数下标的功能5 Python 的字典类似。
其 values 属性可以获得保存标签的数组,
与 Series —样 ,
字符串使用 object类型的数组保存:

index = d f _ s o i l .columns

index.values

a r r a y ( [•pH', 'Dens’, 'Ca’, 'Conduc', 'Date', ’Name'], dtype=object)

Index 对象可当作一维数组,通 过 与 N um P y 数组相同的下标操作可以得到一个新的 Index


对象,但 是 Index 对象是只读的,因此一旦创建就无法修改其中的元素。

print index[[l, 3]]

print index[index > 'c']


print index[l::2]

2
9*
Python科学计算 (第 2 版)

Index([u'Dens', u ' C o n d u c '], d t y p e = _o b j e c t •, name=u' M ea s u r e s ' )


I n d e x ( [ u _ p H ’], d t y p e = ' o b j e c t ■, name=u'Measures')

Index([u'Dens', u ' C o n d u c 、 u 'N a m e '], d t y p e = ' o b j e c t _, n a m e =u'Measures')

Index 对象也具有字典的映射功能,它将数组中的值映射到其位置:
• Index.g e tjo c (value) : 获得巾•个值 value 的下标。
• Index.get_ indexer(values):获 得 -•组 值 values 的下标,当值3、
存在时,得到-1。

print i n d e x .g e t _ l o c ('C a ')

print index.get 一indexer(['Dens', ' C o n d u c t 'nothing'])

2
[1 3-1]

可以直接调用 Index()来创建 Index 对象,然后传递给 DataFrameO的 index 或 columns 参数。


由于 Index 是不可变对象,因此多个数据对象的索引可以是同一个Index 对象。

Tl index = p d . I n d e x ([ ,,A M, nB % MC % "D% ME M], n a m e = Mlevel")


an d丨

si = p d . S e r i e s ([1, 2, 3, 4j 5], index=index)


a s方便的数据分析库

dfl = pd.DataFrame({"a":[l, 2, 3, 4, 5], "b":[6, 7, 8, 9, 10]}, index=index)

print si.index is dfl.index

True

5.1.4 M ultiindex 对象

Multiindex 表示多级索引,它 从 Index 继承,其中的多级标签采用元组对象来表示。下而通


过□获取其中的单个元素,
调用 get_ loc〇和 getJndexer 〇以获取单个标签和多个标签对应的下标。

mindex = df_soil.index

print mindex[l]

print mindex.get 一l o c ( ( "0-10", "Slope"))

print mindex.get 一indexer([("10-30", "Top"), ("0-10", "Depression"), "nothing"])

( • 0 - 1 0 、 .Slope.)

[5 0-1]

在 Multiindex 内部并不直接保存元组对象,而是使用多个Index 对象保存索引中每级的标签:

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]

Index([u'0-10', u'10-30•], dtype='object', name=u'Depth')

Index([ lT D epres s i o n' , u'Slope', u'Top'], d t y p e = ' o b j e c t 、 n a m e=u'Contour')

然后使用多个整数数组保存这些标签的下标:

9
2:
print m i n d e x . l a b e l s [0]

print mindex.labels[l]

FrozenNDArray([0., 0, 0, 1, 1, 1], d t y p e = 'i n t 8 ')

FrozenNDArnay([0, 1, 2, 0, 1, 2], d t y p e = 'i n t 8 ')

下面的代码通过 levels 和 labels 屈性得到多级索引中所有元组的列表,该列表也可以通过


tolist()方法获得:

level©, levell = mindex.levels

label0, labell = mindex.labels

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.)]

当将一个元纟U列表传递给 Index〇时,将自动创述 Multiindex 对象。名:希望创連元素类型为

- 便的数据分析库
元纽的 Index 对象,可以设置 tupleize_co ls 参数为 False:

pd.Index([("A", "x"), ("A", "y"), ("B", "x"), ("B", "y")], name=["classl", "class2"])

MultiIndex (le v e ls =[[u 'A ,, u 'B'], [u 'x 、 u 'y ']],


labels=[[0, 0, 1, 1], [0, 1, 0, 1]],

names=[u'classl', u' c l a s s 2 ' ])

此 外 可 以 使 用 以 头 的 Multiindex 类方法从特定的数据结构创建Multiindex 对象。例


如 from_airays〇方法从多个数组创建Multiindex 对象:

classl = ["A", "A", "B", "B"]


class2 = [__x", "y", _.x", "y"]

p d •Multiindex.from 一arrays([classl, class2], names=["classl", "class2"])

MurtiIndex(levels=[[u'A', u ' B '],[ u ' x ',u ' y ']],

labels=[[0, 0, 1, 1], [0, 1, 0, 1]],


names=[u'classl', u' c l a s s 2 ' ])

from_product〇则 从 多 个 集 合 的 笛 卡 尔 积 创 建 M ultiindex 对 象 。下而的程序将所创建的


Multiindex 对象传递给 index 和 columns 参数,所创建的 DataFrame 对象的行和列使用同一个多

级索引对象:

midx = pd.MultiIndex.from_product([["A", "B", .•(:"], ["x", "y"]],


names=["classln, "class2"])

dfl = pd.DataFrame(np.random.randint(0, 10, (6, 6)), columns=midx, index=midx)


2
9 -
Python 科学计算 (第 2 版)

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 常用的函数参数

在后续章节我们会详细介绍各种常W 函数的W 法。表 5-1列出了一些常j |j 的函数参数。


■D
andas

表 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 的行

例 如 mean()函数计算平均值,如果不指定 axis 参数,则沿着第0 轴计算每列的平均值。如


果指定 axis 参 数 为 1,贝IJ计算每行的平均值。如果指定 level 参数,贝_对多级索引中指定级别
中相同标签对应的元素的平均值:

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)

Measures Depth Contour Measures pH Dens Ca Conduc


pH 5.2 0-10 Depression 4.6 Contour

Dens 1.2 Slope 5.2 Depression 5.1 1.2 9.1 3.5

Ca 11 Top 5.3 Slope 5.4 1.2 11 3.5


Conduc 3.1 10-30 Depression 4.8 Top 5.1 1.2 12 2.5

dtype: float64 Slope 5.3


Top 5

dtype: float64

5.1.6 DataFrame 的内咅结构

DataFrame 对象内部使用 N um Py 数组保存数据,因此也会出现和数组相同的共享数据存储


R 的问题。为了帮助读者理解 Pandas 的内存管理,让我们看看 DataFmme对象的内部结构。
下面的程序通过本书提供的GraphviziDataFmme绘制 df_soil的内部结构,
结果如图 5-2 所示。
图中的实线箭头表示一般属性,虚线箭头表示出 property 创建的属性,读取这些属性时实际上
是获得它们对应的函数的返回值。在 分 析 DataFmme对象的内部结构时,我们重点关注实线箭
头所表示的属性。

from scpy2.common import GraphvizDataFrame

%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

object OataFrame Ot^ectBlock rxJarray


da(eUr»e64[rYs]
n〇at64
OataFrame
6
6
6 OataFrame

图5-2 D a t a F m m c 对象的内部结构

301
Python 科学计算 (第 2 版)

DataFnmie对象的 columns 屈性足 Index 对象,而 index 屈性:足表示多级索引的Multiindex 对


象。Index 对象的索引功能由其_engine属性---- •个ObjectEngine对象提供,该对象使用一个哈
希 表 PyObjectHashTable对象将标签映射到与其对应的整数下标。下而的代码获取列标签"Date"
对应的整数下标:

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

DataFrame对象的数据都保存在_data 属性中,它是一个 BlockManager 对象,其 blocks 属性


是一个列表,其中有一个 FloatBlock 对象、一 个 DatetimeBlock 对象和一个 ObjectBlock 对象。这
些对象是管理实际数据的数据块,其 values 属性是保存数据的数组。
DataFm me 对象尽量用一个数组保存相同类型的列,而将不同类型的列保存在不同的数组

中。这些数组的形状为(4, 6)、(1,6) 和(1,6)。它们的第0 轴的长度对应 4 个浮点数列、1 个时间


列 和 1 个字符串列。由此可知 DataFmme 中的整列数据是保存在连续的内存空间中,这有助于
■D 提高数裾的荐取速度。
andas

当通过[]获取某一歹lj时,所得到的 Series 对象与原 DataFrame对象共享内存。下面查看保存


丨方便的数据分析库

Series 对象数据的数组的 base 属性,也就是 dlLsoil._data.blocks[0].values:

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

当通过[]获収多列时,将复制所有的数据,因此保存新 DataFrame 对象数据的数组的 base


屈性.为None:

print df_ s o i l [ [" D e n s " ] ] ._data.blocks[ 0 ] .values.base

None

如 果 DataFmme对象只有一个数据块,则 通 过 values 属性得到的数组是数据块中数组的转


置,因此它与 DataFrame对象共享A 存。例如在下而的程序中,d f jlo a t 中所有列的元素类型相
同,它只有一个数据块,因此 d〔float.values 所得到的数组与 d L flo a t 中保存数据的数组共享内
存。

df_float = df_soil[['pH'., 'Dens'., 'Ca', 'Conduc']]

d f _ f l o a t .v a l u e s .base is df_flo a t ._ d a t a . b l o c k s [ 0 ] .values

True

当 DataFrame对象只有一个数据块吋,
获収其行数据所得到的Series 对象也与其共享内存:

df_float.loc["0-10", "Top"].values.base is d f _ f l o a t ._ d a t a . b l o c k s [ 0 ] .values

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

S eries 和 DataFmme 提供了丰富的下标存取方法,除了直接使用[]运算符之外,还可以使

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

index=["rl", "r2", "r3", "r4", "r5"],


columns=rcl", "c2", "c3"])

5.2.1 □操作符

通过[]操作符对 DataFrame对象进行存取时,支持以下5 种下标对象:


•单 个 索 引 标 签 :获取标签对应的列,返冋一个 Series 对象。
•多 个 索 引 标 签 :获収以列表、数组(注意不能是元组)表示的多个标签对应的列,返冋一
个 DataFrame对象。
• 整 数 切 片 :以整数下标获取切片对应的行。
• 标 签 切 片 :当使用标签作为切片时包含终值。
• 布 尔 数 组 :获取数组中 True 对应的行。
• 布 尔 DataFrame: 将 DataFhime对象中 False 对应的元素设置为 NaN 。
Tl
下而显示整数切片和标签切片的结果,注 M 标签切片包含终值V T :
an d丨
a s方便的数据分析库

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:

df[df.cl > 4] df[df > 2]

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

Name: r2, d t y p e : int32

ctf.lcx [[”
r2n,"r3 M
]]获得nr2"和"r3"对应的行。40〇〇[["12","以],
[41",
"〇2,

]]则获得^
"c l M和"c 2"列上的数据,所得到的数据都是新的DataFrame对象。

df •loc [[__r 2 . r 3 " ]] df •loc [["r 2 " r 3••],[•■c l c 2"]]

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 轴的下标分别为标签切片和布尔数序列:

df.loc["r2":"r4", ["c 2","c 3"]] d f .lo c [d f .c l >2, ["c l ","c 2"]]

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]类似,不过它使用整数下标:

df.iloc[2] df.iloc[[2,4]] df.iloc[[l,3]] d f . i l o c [ [1,3],[0,2]]

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[]的存収器可以混用标签和位置下标,例如:

df.ix[2:4, ["cl", "c3"]] df.ix["rl":"r3", [0, 2]]

cl c3 cl c3

r3 2 7 rl 6 7
r4 4 7 r2 4 9

r3 2 7

如 果 DataFmme对象有整数索引,则应该使用.locn 和.ilocn 以避免混淆。

5 . 2 . 3 获取单个值

.at□和.iat□分别使用标签和整数下标获取单个值,此 外 get_Value〇与.atD类似,不过其执行
速度要快一些:

df.at["r2", "c2"] df.iat[l, 1] df.get_value("r2", "c2")


Tl
an d丨

6 6 6
a s方便的数据分析库

当.loc[]的下标对象是两个标签列表时,所获得的是这两个列表形成的网格上的元素,这与
N u m P y 的 数 组 下 标 操 作 不 样 。如果希望获収两个列表中每对标签所对应的元素,可以使用
lookupO, 它返冋一个包含指定元素的数组:

df.lookup(["r2", "r4", "r3"], ["cl", "c2", "cl"])

array([4, 3, 2])

5 . 2 . 4 多级标签的存取

.l〇
c [j 和.atU的下标可以指定多级索引中每级索引上的标签。这时多级索引轴对应的下标是
一个下标元组,该元组中的每个元素与索引中的每级索引对应。若下标不是元组,则将其转换
为长度为1 的元组,若元组的长度比索引的层数少,则在其后面补 sliCe(N 〇
ne)。

soil_df = p d .r e a d _ c s v (" d ata/Soils-simple.csv"^ index_col=[0^ 1 ] ^ parse_dates=["Daten ])

在下而的例子中,n10-30”
为第0 轴的标签,
根据前而的规则,
将其转换为('’
10-30’

,slice(None)),

即选择第0 级中"1〇~3〇"对应的行:

soil 一df.loc["10-30", ["pH", "Ca"]]

pH Ca
Contour

Depression 4.9 7.5

Slope 5.3 9.5


Top 4.8 10

06
如果需要选择第1级中"Top ”
对应的行,
则需要把 sliceCNone)作为第0 级的下标。由于 Python
中只有直接在□中才能使用以:分隔的切片语法,
因此这里使用np.s_对象创建第0 轴对应的下标:
(slice(None),
"Top ")。

soil_df.loc[np.sJ:, "Top"], ["pH", "Ca"]]

pH Ca

Depth Contour

0-10 Top 5.B 13

10-30 Top 4.8 10

5.2.5 query()方法

当需要根据-•定的条件对行进行过滤吋,通常可以先创建一个布尔数组,使用该数组获収
True 对应的行,例如下面的程序获得 p H 值大于5、C a 含 量 小 于 11%的行。由于 Python 中无法

1定 义 not、and 和 o r 等关键字的行为,因此需要改川〜、& 、I等位运算符。然而这些运算符的

pandas方
优先级比比较运算符要高,因此需要用括号将比较运算括起来:

soil_df[(soil_df.pH > 5) & (soil_df.Ca < 11)]

—便 的 数 据 分 析 库
使 用 queiy 〇可以简化上述程序:

print soil_df.query("|DH > 5 and Ca < 11")

pH Dens Ca Conduc Date Name


Depth Contour

0-10 Depression 5.4 0.98 11 1.5 2015-05-26 Lois

10-30 Slope 5.3 1.3 9.5 4.9 2015-02-06 Diana

queiy ()的参数是一个运兑表达式字符串。其屮可以使用 not、and 和 o r 等关键字进行向量布

尔运算,表达式中的变量名表示与其对应的列。如果希望在表达式中使W 其他全局或局域变量
的值,可以在变量名之前添加@,例如:

pH_low = 5
Ca_hi = 11

print soil_df.query("pH > @pH _ l o w and Ca < @Ca_hi")

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丨

可以将 skipinitialspace参数设置为 True。


a s方便的数据分析库

•如果数据使用空格或制表符分隔,可以不设置 se p 参数,而 将 ddim _whitespaCe参数设


置为 Tm e 。
•默认情况下第一行文木被作为列索引标签,如果数据文件中没有保存列名的行,可以
设 置 header参数为0。
•如果数据文件之前包含一些说明行,可以使用 skipmws 参数指定数据开始的行号。
• na_values、tnie_values 和 false_values 等参数分别指足 N aN、 True 和 False 对应的字符串
列表。
•如果希望从字符串读入数据,可以使用 io.BytesIO(string)将字符串包装成输入流。
•如果希望将宁•符串转换为时间, n丨以使用p〇
rse_dates 指定转换为时间的列。
•如果数据中包含中文,可 以 使 encoding 参数指定文件的编码,例如Hutf-8n、”
gbkn等。
指定编码之后得到的字符串列为Unicode 字符串。
• 可 以 使 用 usecols 参数指定需要读入的列。
•当 文 件 很 大 时 ,
可以用 chunksize参数指定一次读入的行数。
当使用 chunksize 时,
read_csv 〇

返回一个迭代器。
•当 文 件 名 包 含 中 文 时 ,需要使用 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 = []

for df in pd.read 一csv(


u Mdata/aqi/ 上海市一201406.csv",

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

PM2.5 100 PM2.5 int64

PM10 98 PM10 float64

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 字符串。

print type(df.loc[0, u" 监测点_•])

<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
参数指定压缩级别。

store = pd.HDFStore("a.hdf5", co m p l i b = ,,b l o s c " > complevel=9)

H D F S t o r e 对象支持字典接口,例如使用[]存取元素、get〇和 keys 〇等方法。

dfl = pd.DataFrame(np.random.rand(1 0 0 0 0 0 ^ 4)^ columns=list("ABCD"))

df2 = pd.DataFrame(np.random.randint(0, 10000, (10000, 3)),


columns=["One", "Two", "Three"])

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"])

[ ■/dataframes/dfl’, '/dataframes/df2', '/series/sl’]


True

H D F S t o r e 采 用 pytables 扩展库存取 H D F 5 文件,其 get_node 〇方法可以获得 pytables 中定义


的 Node 对象。使用该对象可以遍历文件中的所有节点,关 于 N o d e 对象的用法,馈读者参考
pytables 的文梢:

http://pytables.github.io/usersguide/libref/hierarchy_classes.html
pytables 官方文档。

下 面 用 get_node() 获得根节点,然后调用_f_walknodes() 遍历其包含的所有节点。由结果可


知,H D F S t o r e 中 的 Series 和 D a t a F r a m e 对象5 H D F 5 的 H 录对应, IeI 录中通过多个资料数据保
存具体的数据。

root = s t o r e . get_node("//")
for node in r<x>t._f_walknodes():
print node

/dataframes (Group) u ' '


/series (Group) u ' '
/dataframes/dfl (Group) u ''

/dataframes/df2 (Group) u ''


/series/sl (Group) u ' '
/series/sl/index (CArray(1000,), shuffle, blosc(9)) _•
/series/sl/values (CArray(1000,), shuffle, blosc(9)) '•

/dataframes/dfl/axis0 (CArray(4,), shuffle, blosc(9)) ’•

/dataframes/dfl/axisl (CArray(100000,), shuffle, b l o s c ( 9 ) ) ''


/dataframes/dfl/block0_items (CArray(4,), shuffle, blosc(9))

/dataframes/dfl/block0_values (CArray(100000, 4), shuffle, blosc(9))


/dataframes/df2/axis0 (CArray(3,), shuffle, blosc(9)) ’•

/dataframes/df2/axisl (CArray(10000,), shuffle, blosc(9)) •’


/dataframes/df2/block0— items (CArray(3,), shuffle, blosc(9)) ’•
/dataframes/df2/block0— values (CArray(10000, 3), shuffle, blosc(9)) •’

通过前面介绍的方法将DataFrame对象保存进 HDFStore 之后,无法界为jL:追加数据。在数


据采集和导入大量 C S V 文件时,我们通常希望能不断地往同一 DataFrame中添加新的数据。可
以使用 append()方法实现该功能。
Oappend 参数为 False 表示将蒗盖己存在的数据,如果指定键值不存在,可省略该参数。©
将 d B 追加到指定键,因此读取该键将得到一个长度为100100的 DataFhime对象。

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

s t o r e •a p p e n d (•d a t a f r a m e s / d f _ d y n a m i c l ', df3) ©


s t o r e ['d a t a f r a m e s / d f _ d y n a m i c l '].shape

(100100, 4)

使 川 append〇将 创 建 pytables 中支持索引的表格(Table)节点,默认使川 DataFrame 的 index


作为索引。通 过 sdect〇可以对表格进行查询以获取满足杳询条件的行。在下面的程序中,通过
where 参数指定2 B旬条件,index 表 示 DataFrame 的标签数据。该条件获取細签在97到 102之间
的所有行。由于我们将两个默认杬签的DataFmme添加进该表格,因 此 9 8 和 9 9 各对应两行数
裾 。使用该方式读取部分数据时,可以减少内稃使用M•和磁盘读取M , 提高数据的访问速度。

print s t o r e .s e l e c t ('d a t a f r a m e s / d f _ d y n a m i c l '^ w h e r e = 'index > 97 & index < 102')

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

98 0.058 0.18 0.91 0.083


99 0.47 0.81 0.71 0.59

如 果 希 望 对 D ataFram e 的指定列进行索引,可 以 在 用 append〇创建新的表格时,通过


data_columns 指定索引歹U,或将其设置为 T ru e 以对所有列创建索引。

stor e . a p pe n d ( 'dataframes/df_dynamicl', dfl, append=False, d a t a _ c olumns=[,,A"^ "B"])

print stor e . s e le c t ( 'dataframes/df_dynamicl'^ w h ere='A > 0.99 & B < 0.01')


Python 科学计算(第2 版)

A B C D

3656 0.99 0.0018 0.67 0.47


5091 1 0.004 0.43 0.15
17671 1 0.0042 0.99 0.31
41052 1 0.00081 0.9 0.32

45307 1 0.0093 0.72 0.065

67976 0.99 0.0096 0.93 0.79


69078 1 0.0055 0.97 0.88

87871 1 0.008 0.59 0.35


94421 0.99 0.0049 0.36 0.9

下面循环读入 data\aq i 路径之下的所有 C S V 文件,并将数据写入 HDF5 文件中。在将多个


文本文件的数据逐次写入HTF 5 文件时,需要注意如下几点事项:
• HDF 5 文件不支持 Unicode 字符串,冈此需要对 Unicode 字符串进行编码,转换为字节
字符带。在本例中直接从文件读取UTF -8编码的字符啤,因此在读入 C S V 文件吋无须
指 定 encoding 参数。O 但是由于文件可能包含UTF -8的 B O M , 因此需要先读入文件的
Pandas方

头三个字节并与 B O M 比较,这样才能保证读入的数椐中勹第一列对应的标签不包含
BOM 。
- 便的数据分析库

■由于可能存在缺失数据,因此读入的数值列的类型可能为整数和浮点数。由 于 HDF 5
文件中的每列数据只能对应一利喽型,© 因此需要使用 dtype 参数指定这些数值列的类
型为浮点数。
• €)需要为 HDF 5 文件中的字符串列指定最大长度,否则该最大长度将由第一个被添加进
HDF 5 文件的数据对象决定。

由于所有从 C S V 文件读入 DataFrame对象的行索引都为默认值,因 此 HDF 5 文件中数

A 据的行索引并不是唯一的 。

def r e a d _ a q i _files(fnj3attern):

from glob import glob

from os import path

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",

" 监测点••: "Position",

"质 量 等 级 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}

for idx, df in e n umerate(read_aqi_files(u"data/aqi/*.csv")):


store.append('a q i ', df, append=idx!=0, min_itemsize=string^_size, data_columns=True)©

store . c l os e ()

下而打开 aqi.hdf5 文件并读入所有数据:

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的行:

dfjDolluted = s t o r e . s e le c t ("a q i " ^ where="PM2_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 版)

from sqlalchemy import create 一engine


engine = c r e a t e _ e n g i n e ( 's q l i t e :/ / / d a t a / a q i / a q i .d b ')

为了避免重复写入,下面先通过 engine 对象执行 S Q L 语句,删 除 aq i 表:

try :
engine.execute("DROP TABLE aqi")

except:

pass

然后调用 to_sql〇将数据写入数据库,if_exists 参数为"append”


表示当表存在时,将新数据添
加到表中。由于本例中 DataFmme对象的行索引无实际意义,因此设置 index 参 数 为 False ,表
示不保存行索引。由于数据库要求使用Unicode 字符弟,因此在写入数据库之前对字符串列进
行解码,将其数据转换为 Unicode 字符串。如果在从 C S V 文件读入数据时,通 过 encoding 参数
指定了文本编码,则不必执行此步骤。

■D str_cols = ["Position", "City ","Level"]


andas
丨方便的数据分析库

for df in n ead_aqi_files("data / a q i/ * .c s v " ):


for col in str 一cols:
df[col] = df[col].str.decode("utf8")

df.to_sql("aqi", engine, if 一exists="append% index=False)

下而调用 read_sql〇从数椐库读入整个名为a q i 的表:

df_aqi = pd.read_sql("aqi", engine)

也可以通过 S Q L 查询语句读入部分数据,下面只读入 PM 2.5值大于500的行:

dfjDolluted = pd.read_sql("select * from aqi where PM2_5 > 500"^ engine)

print len(df_polluted)

87

5 . 3 . 4 使 用 Pickle 序列化

还可以使用 to_pickle〇和 read_pickle()对 DataFmme对象进行序列化和反序列化:

df_aqi,toj3ic k l e ( " d a t a / a qi / a q i .p i c k l e " )


df_aqi2 = pd.read_pickle("data/aqi/aqi.pickle")

d f _ a q i .equals(df_aqi2)

True

Pickle 是 Python 特有的对象序列化格式,因此很难使用其他软件、程序设计语言读取 Pickle

化之后的数椐,但是作为临时保存•运算的中间结果还是很方便的。
5 . 4 数值运算函数

与本节内容对应的 Notebook 为:05-pandas/pandas400~calculation.ipynb。

Series 和 DataFmme对象都支持 N um Py 的数组接口,


因此可以直接使用N um Py 提供的 ufunc
函数对它们进行运算。此外它们还提供各种运算方法,例 如 max〇、min〇、mean〇、std〇等。这
些函数都有如下三个常用参数:
• axis : 指定运算对应的轴。
• level: 指定运算对应的索引级别。
• skipna: 运算楚否自动跳过 N aN 。

Pandas方
print df_soil

pH Dens Ca Conduc

- 便的数据分析库
Depth Contour

0-10 Depression 5.4 0.98 11 1.5

Slope 5.5 1.1 12 2

Top 5.3 1 13 1.4

10-30 Depression 4.9 1.4 7.5 5.5

Slope 5.3 1.3 9.5 4.9

Top 4.8 1.3 10 3.6

下而分别汁算每列的平均值、每行的平均值以及行索引的第1 级 别 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 )

pH 5.2 Depth Contour pH Dens Ca Conduc

Dens 1.2 0-10 Depression 4.6 Contour

Ca 11 Slope 5.2 Depression 5.1 1.2 9.1 B.5


Conduc 3.1 Top 5.3 Slope 5.4 1.2 11 3.5

d t y p e : float64 10-30 Depression 4.8 Top 5.1 1.2 12 2.5


Slope 5.3

Top 5

dtype: float64

除了支持加减乘除等运算符之外,Pandas 还提供了 add〇、sub〇、mul〇、div()、mod〇等与


二元运兑符对应的函数。这些函数可以通过 axis 、level 和 fill_value 等参数控制其运兑行为。在
Python 科学计算 (第 2 版)

下面的例子中,对不同的等高线的 C a 的值乘上不同的系数,filLvalue 参 数 为 1表示对于不存在


的值或 N a N 使用默认值1。因此结果中,所 有 Depression对应的值为原来的0.9倍,Slope 对应
的值为原來的1.2倍,而 T o p 对应的值保持不变。

s = pd.Series(diet(Depression=0.9^ Slope=1.2))

df_soil.Ca.mul(s, level=l, fill 一value=l)

Depth Contour

0-10 Depression 9.6

Slope 15

Top 13

10-30 Depression 6.8

Slope 11

Top 10

d t y p e : float64

Pandas 还提供了 ro llin g %)函数来对序列中相邻的 N 个元素进行移动窗口运算。例如可以


pandas方

使 用 rdlingjnedianO 实现中值滤波,使 用 rdlingjiieanO 计算移动平均。图 5-3显示了使用这两个


—便 的 数 据 分 析 库

函数对带脉冲噪声的正弦波进行处理的结來。它们的第二个参数为窗口包含的元素个数,而
center 参数为 True 表示移动窗口以当前元素为中心。
由 于 rolling_ median()采用了更高效的算法,因此当窗口很大吋它的运算速度比S c iP y 章节
中介绍过的 signal.order_filter〇更快。

t = np.linspace(0, 10, 400)

x = np.sin(0.5*2*np.pi*t)

x[np.random.randint(0> len(t), 40)] += np.rand o m. n o r m a l (0^ 0.3^ 40)

s = pd.Series(x^ index=t)

s_mean = pd.rollingjnean(s, 5, center=True)

s_median = p d .r o l l i n g_median(s , 5, center=True)

一 罐 声 值 号 一 中 僅 戏 法 一 移动平均

图5 - 3 中值滤波和移动平均
expandir^*() 函数对序列进行扩展窗丨」运综,例 如 expanding_max()返回到每个元素为止的
历史最大值。图 5~4显示了 expanding_max()、expandingjnean 〇和 expanding_min〇的运算结果。

请读者思考如何使用 Num Py 提 供 的 ufunc 函数计算图 5~4 中的三条曲线。

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史最大值、平均值、最小值

字符串处理

与本节内容对应的 Notebook 为:05-pandas/pandas-500-string.ipynb。

Series 对象提供了大量的字符串处理方法,由于数量众多,因 此 Pandas 使用了一个类似名


称空间的对象 str 来包装这些字符串相关的方法。例如下面的程序调用 str.upper〇将序列中的所
有字母都转换为大写:

s_abc = pd.Series(["a", "b", "c"])


print s_abc.str.upper()
Python科学计算(第2 版)

2 C

d t y p e : object

Python 中包含两种字符串:字节字符串和 U nicode 字符串。通 过 str.decode()可以将字节字


符串按照指定的编码解码为Unicode 字符串。例如在 UTF -8编码屮,一•个汉字占用三个字符,
因此下面的 s_utf8 中的字符串长度分别为6、9、12。当调用 str.decode()将其转换为 Unicode 字符
串的序列之后,其各个元素的长度为实际的文字个数。str.encode〇n j 以把 Unicode 字符串按照指
定的编码转换为字节字符串,在常用的汉字编码 G B 2 3 1 2 中,一个汉字占州两个字节,因此
s_ gb2312的元素长度分别为4 、6、8。

s一utf8 = pd.Series([b" 北京" , b"北京市" , b" 北京地区••])

s_unicode = s_utf8.str.decode("utf-8")
s_gb2312 = s一Unicode.str.encode("gb2312")

s_utf8.str.len() s _ u n i c o d e .s t r .l e n () s_^gb2312.s t r .len ()

0 6 0 2 0 4
■o
andas

1 9 1 3 1 6
2 12 2 4 2 8

d t y p e : int64 d t y p e : int64 d t y p e : int64

无 论 Series 对象包含哪利•字符苹对象,其 dtype 属性都是 object, 因此无法根据它判断字符


串类型。在处理文本数据时,需要格外注意字符串的类型。
可以对 sti•使用整数或切片下标,相当于对 Series 对象中的每个元素进行下标运算,例如:
便

print s一U n i c o d e . st r [:2]

0 北京

1 北京
2 北京
d t y p e : object

字 符 串 序 列 与 字 符 样 ,支持加法和乘法运尊,例如:

+ s一

print s_unicode + u abc * 2

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

调 用 astypeO方法可以对 Series 对象中的所有元素进行类型转换,例如下而将整数序列转换


为字符串序列:

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

str 中的有些方法可以对元素类型为列表的Series 对象进行处理,例如下而调用 str.Split〇将 s


中的每个字符制史用字符T 分隔,所得到的结果 s_ list 的元素类型为列表。
然后调用它的 str.joinO
方法以逗号连接每个列表中的元素:

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

0 a|bc|de 0 [a) be, de] 0 a^bc^de


1 x|xyz|yz 1 [x, xyz^ yz] 1 x^xyz^yz

d t y p e : object dtype: object d t y p e : object

对字符串序列进行处理付,经常会得到元素类型为列表的序列。Pandas没有提供处理这种
序列的方法,不过可以通过 str[]获取其中的元素:

s一l i s t . s t r [1]

0 be
1 xyz

d t y p e : object

或者先将其转换为嵌套列表,然后再转换为 DataFrame对象:

print pd.DataFrame(s_list.tolist 〇, columns=["A", "B", "C"])

A B C
0 a be de
1 x xyz yz

Pandas 还提供了一些正则表达式相关的方法。例如使用其中的 str.extract〇可以从字符串序


列屮杣収出需要的部分,得 到 DataFrame对象。下面的例子中,df_extm ctl 对应的正则表达式包
含三个未命名的组,因此其结果包含三个动命名的列。而 df_extract2 对应的正则表达式包含
Python 科学计算 (第 2 版)

两个命名组,因此其列名为组名。

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

print pd.DataFrame({"from_node":from_node, "to_node":to— node}) *


1

from 一node to_node


0 A B

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
表示对应的行包含对应的标签:

print df[1].str.get_dummies(sep=" 1")


A B C D E F
0 0 1 1 1 0 0
1 0 0 0 0 1 1
2 1 0 0 0 0 0
3 0 1 1 0 0 0

当字符串操作很难用M 量化的字符串方法表示时,可 以 使 用 map〇闲数,将针对每个元素


运算的函数运用到整个序列之上:

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 列都是
表示分类的数据,因此有许多重复的字符串。

df_soil = pd.read_csv("Soils.csv", usecols=[2, 3, 4, 6])


print df一soil.dtypes
Contour object
Depth object
Gp object
pH float64
dtype: object

下而循环调用 astypefcategoty ”
)将这三列转换为分类列:

for col in ["Contour% "Depth", "Gp'*]:


df_soil[col] = df_soil[col],astype("category")
print df一soil.dtypes
Python 科学计算 (第 2 版)

Contour category
Depth category
Gp category

pH float64

d t y p e : object

勾名称空间对象 str 类似,元素类型为 category 的 Series 对象提供了名称空间对象cat,其中


保存了与分类序列相关的各种属性和方法。例 如 cat.categories是保存所有分类的 Index 对象:

Gp = df_soil.Gp

print Gp.cat.categories

Index([u'D0', lT D I', lT D 3', iTD6', lT S 0 、 i T S l 、 lT S 3', u'S6', tTT0', u ' T l 、


u'T3', lT T 6'],

d t y p e = ' o b j e c t ')

而 catxodes 则是保存下标的整数序列,元素类型为 im8 , 冈此一个元素用一个字节表示。


■D
andas

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

Categories (12, object): [D0, Dl, D3, Tl, T3, T6]

分类数据有无序和有序两种,无序分类中的不同分类无法比较大小,例如性别;有序分类
则可以比较大小,例如年龄段。上面创建的三个分类列为无序分类,可 以 通 过 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

categories = ["Top", "Slope", "Depression"]

contour•cat•reorder'一categories(categories, ordered=True).head()

0 Top

1 Top
2 Top
3 Top

4 Top
d t y p e : category

Categories (3 , o b j e c t ) : [Top < Slope < Depression]

5 . 5 时间序列

与本节内容对应的 Notebook 为: 05-pandas/pandas-600-datetime.ipynb〇

Pandas 提供了表示时间点、时间段和时间间隔等三利i与时间有关的类型,以及元素为这些

类型的索引对象,并提供了许多时间序列相关的函数。本节简要介绍一些与吋间相关的对象和
函数。在本章最后一节还会介绍一些相关的实例。

5 . 5 . 1 时间点、时间段、时间间隔

Timestamp 对 象 从 Python 标准伟中的 datetime 类继承,表示吋间轨卜.的一个时亥U。它提供


了方便的时区转换功能。下面调用 Timestamp.now ()获取当前时间 now ,它楚不包含时区信息的
木地时间。调用其 tz_localize()可以得到指定时区的Timestamp 对象。而带时区信息的Timestamp
对象可以通过其 tz_convert()转换时区。下而的 now_shanghai的时间以"+08:00"结尾,表示它是东
八区的时间,将其转换为东京时间得到n m vjo k yo , 它是东九区的时间:

now = pd.Timestamp.now()
now_shanghai = now.tz_localize("Asia/Shanghai")

nowjtokyo = now_shanghai.tz_convert("Asia/Tokyo")
print u" 本地 时 间 :", now

print u" 上 "


海 时 区 : , now_shanghai

print u ••东 "


京 时 区 : , now_tokyo

本地 时 间 : 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 时间无法比较:

now 一shanghai == now_tokyo

True

通 过 pytz 模块的 comnion_timezones〇可以获得常用的表示吋区的字符串:

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 ’

Period 对象表示一个标准的吋间段,例如某年、某月、莱 FI 、某小时等。吋间段的长短由


■D
ft-eq属性决定。下而的程序调用 Period.now (),分别获得包含当前时间的日周期时间段和小时周
a n d a丨

期时间段。
s 方便的数据分析库

now_day = p d •P e r i o d •now(f r e q =" D " )

now_hour = pd.Period.now(freq="H")

now— day niow_hour

P e r i o d C 2015-07-25', 'D') P e r i o d ('2015-07-25 11:00', 'H')

freq 属性萣一个描述时间段的字符串,其可选值可以通过下面的代码获得:

from pandas.tseries import frequencies


frequen c ie s . _ p e r i o d _ c o d e _m a p .k e y s ()
frequencies._period_alias_dictionary()

对于周期为年度和星期的时间段,可以通过 frcq 指定开始的时间。例如”


W”表示以星期天
开始的星期时间段,而’

W -M O N ”
则表示以星期一开始的星期时间段:

now_week_sun = p d .P e r i o d .n o w ( f r G q =" W " )


now_week_mon = pd.Period.now(freq="W-MON")

now_week__sun now_week_mon

P e r i o d ( '2015-07-20/2015-07-26', 'W-SUN') P e r i o d ( '2015-07-21/2015-07-27', 'W-MON*)

时间段的起点和终点可以通过 start_tim e 和 end_tim e 屈性获得,它们都是表示时间点的


Timestamp 对象:
n o w _ d a y .start_time n o w _ d a y .end_time

T i m e s t a m p C 2015-07-25 00:00:00') T i m e s t a m p ( '2015-07-25 23:59:59.999999999')

调用 Timestamp 对象的 to_period〇方法可以把时间点转换为包含该时间点的时间段。


注意时
间段不包含时区信息:

now_shanghai.tojD eriod ("H")


P e n i o d C 2015-07-25 11:00*, 'H')

Timestamp 和 Period 对象可以通过其属性获得年、月、曰等信息。下而分别获得年、月、

曰、星期几、一年巾的第几天、小时等信息:

now.year now.month now.day now.dayofweek now.dayofyear now.hour

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

Timedelta('67 days 12:09:04.039000')

时间点和时间间隔之间可以进行加减运算:

national_day + pd.Timedelta("20 days 10:20:30")

T i m e s t a m p C 2015-10-21 10:20:30')

Timedelta 对象的 days 、


seconds、
microseconds和 nanoseconds等属性分别获得它包含的天数、

秒数、微秒数和纳秒数。注意这些值与对应的单位相乘并求和才是该对象表示的总时间间隔:

td.days td.seconds td.microseconds

67L 43744L 39000L

也可以通过关键字参数直接指定时间间隔的天数、小时数、分钟数和秒数:

print p d .T i m e d e l t a (days=10, hours=l, minutes=2, seconds=10.5)

print pd.Timedelta(seconds=100000)

10 days 01:02:10.500000

1 days 03:46:40
Python 科学计算 (第 2 版)

5 . 5 . 2 时间序列

上节介绍的 Timestamp、Period 和 Timedelta 对象都楚表示单个值的对象,这些值可以放在


索 引 或 数 据 列 中 。下面的程序调丨U random_timestamps〇创 連 一 个 包 含 5 个随机时间点的
Datetimelndex 对 象 ts_ index, 然 后 通 过 ts_ in d e x 创 建 Periodlndex 类型的索引对象 pd_ in d ex 和
Timedeltalndex 类型的索引对象 td_ index。Datetimelndex、 Periodlndex 和 Timedeltalndex 都从 Index
继承,可以作为 Series 或 DataFmme 的索引。
random_timestamps〇中 的 date_range〇函数创建以 start 为起点、以 e n d 为终点、周 期 为 freq
的 Datetimelndex 对象。

def random_timestamps(start, end, freq, c o u n t ) :


index = pd.date_range(start, end, freq=freq)
locations = n p . r a n d o m . c h o i c e ( n p . a r a n g e ( l e n ( i n d e x ) s i z e = c o u n t , replace=False)
l o c a t i ons.sort()

return index[locations]

■D
a n d a丨

n p .r a n d o m .s e e d (42)

ts 一index = random 一timestamps("2015-01-01") "2015-10-01", freq=__Min", count=5)


s 方便的数据分析库

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

print ts_index, "\n"


print pd_index, "\n"
print td 一index, "Xn"

DatGtimeIndex(['2015-01-15 16:12:00', '2015-02-15 08:04:00',


*2015-02-28 12:30:00*, *2015-08-06 02:40:00',

'2015-08-18 13:13:00*],
dtype=' d at e t i m e 6 4 [ n s ]、 freq=None^ tz=None)

P e r i o d I n d e x ( 2015-01', •2015-022015-022015-08 、 '2015-08•], dtype='int64_,


freq='M')

T i m e d e l t a l n d e x ( [ '30 days 1 5 : 5 2 : 0 0 ’, '13 days 0 4 : 2 6 : 0 0 、 '158 days 1 4 : 1 0 : 0 0 ’,


•12 days 1 0 : 3 3 : 0 0 ,],

dtype='timedelta64[ns]freq=None)

下面査看这三种索弓丨对象的dtype 属性。其 中 M 8[ns]和 m 8[ns]是 N um Py 中表示时间点和时


间间隔的 dtype 类型,内部采用64位整数存储时间信息,其中[ns]表示时间的最小单位为纳秒,
能表示的时间范围大约是公元1678年到公元2262年。Periodlndex 也使用64位整数,但是最小
时间单位由其 freq 属性决定。
ts index.dtype
一 pd index•dtype
一 td index•dtype

d t y p e ( ' < M 8 [ n s ] ') d t y p e ( ' i nt 6 4 ' ) d t y p e ( ' < m 8 [ n s ] ')

这三种索引对象都提供了许多与时间相关的属性,例如:

t s _ i n d e x .weekday pd_index.month td_index.seconds

[3, 6, 5, 3, 1] [1, 2, 2, 8, 8] [57120, 15960, 51000, 37980]

Datetimelndex.shift(n,freq)可以移动时间点,将当前的时间移动 n 个 freq 时间单位。对于天


数、小时这样的精确单位,结果相当于与指定的时间间隔相加:

ts 一i n d e x . s h if t (1, "H")

Datetimelndex(['2015-01-15 17:12:00'., '2015-02-15 09:04:00',


'2015-02-28 13:30:00', '2015-08-06 0 3 : 4 0 : 0 0 ' ,

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

- 便的数据分析库
而对于月份这样不精确的时间单位,则移动一个单位相当于移到月头或月底:

ts— index.shift(l, "M")

Datetimelndex(['2015-01-31 16:12:00', '2015-02-28 08:04:00',

*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 ()

Datetimelndex(['2015- 0 1 - 15 ' , '2015-02-15', '2015-02-28', '2015-08-06',


'2015-08-18'],

d t y p e = ' d a t e t i m e 6 4 [ n s ] •, freq=None, tz=None)

如果希望对任总:的吋间周期取整,可以先通过 to_period()将其转换为 Periodlndex 对象,然


后冉调用 to_timestamp()方法转换回 Datetimelndex 对象。to_timestamp() 的 h o w 参数决走将时间段
的起点还是终点转换为时间点,默认值为nstarf。

t s _ i n d e x .t o j D e r i o d (nH " ).t o _ t i m e s t a m p ( )

Datetimelndex(['2015-01-15 16:00:00', '2015-02-15 08:00:00',


'2015-02-28 12:00:00', '2015-08-06 02:00:00',

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

下面的 Series对 象 ts series的索引为 Datetimelndex 对象,这 种 Series 对象被称为时间序列:


Python科学计算 (第 2 版)

ts_series = pd.Series(range(5), index=ts_index)

时间序列提供一些专门用于处理吋间的方法,
例 如 between_time()返 N 所有位于指定时间范
围之内的数据:

t s _ s e r i e s •between— t i m e ( " 9 :0 0 " , " 1 8 :00")

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

以 Periodlndex和 Timedeltalndex 为索^丨的序列也n j 以使)|j tshift〇对索引进行移动:

pd_series = pd.Series(range(5), index=pd_index)


td_series = pd.Series(range(4), index=td_index)

p d _ s e r i e s .t s h i f t (1) td— series .t:shift( 10, freq="H__)

2015-02 0 31 days 01:52:00 0


2015-03 1 13 days 14:26:00 1

2015-03 2 159 days 00:10:00 2


2015-09 3 12 days 20:33:00 3

2015-09 4 d t y p e : int64
Freq: d t y p e : int64

时间信息除了可以作为索引之外,还可以作为 Series 或 DataFrame 的列。下而分别将上述


三种索引对象转换为Series 对象,并查看其 d t y p e 属性:

ts 一data = pd.Series(ts 一index)

pd_data = pd.Series(pd 一index)

td_data = pd.Series(td_index)

ts 一
data.dtype pd 一
d a t a •dtype td 一
d a t a •dtype

d t y p e ( '<M8[ns ]') dtype('O') d t y p e ( ' < m 8 [ n s ] ')


可以看到 Pandas 的 Series 对象0 前尚不支持使用6 4 位整数表示吋间段,因此使用对象数
组保存所有的 Period 对象。而对于时间点和时间间隔数据则采用64位整数数纟11保存。
当序列的值为时间数据时,可以通过名字空间对象d t 调州时间相关的属性和方法。例如:

ts_data.dt.hour pd— data.dt.month t d _ d a t a .d t .days

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)

df 一int = pd.DataFrame(np.random.randint(0, 10, (10, 3)), columns=list("ABC"))


d f _ i n t [ " A " ;l += 10
df_nan = df_int.where(df_int > 2)

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

isnull〇和 notnull()用于判断元素值是否为 N aN , 它们返回全是布尔值的 DataFmme 对象。


df .notnull()和〜df .isnull〇的结果相同,但 是 由 于 notnull()少创建一个临时对象,其运算效率更高
一些。

df_nan.isnull() df_nan.notnull()

A B C A B C

0 False False True 0 True True False

1 False True False 1 True False True


Tl

) 2 False False False 2 True True True
Q. 3 False False False 3 True True True
JD
C/)
1 4 False False True 4 True True False

便 5 False False False 5 True True True



数 6 False False False 6 True True True
据 7 False False True 7 True True False

8 False True True 8 True False False
库 9 False False True 9 True True False

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

对于包含 N aN 元素的数掘,最简单的办法就是调用dropnaO以删除包含N a N 的行或列,当


全部使用默认参数时,将删除包含 N a N 的所有行。可以通过 thresh参数指定 N a N 个数的阈值,

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

interpolate〇默认使用等距线性插值,可以通过其 method 参数指定插值算法。在下面的例子


中,第 0 个元素和第2 个元素的数值分别为3.0和 7.0,冈此当 method 参数默认辑时下标为1 的
N a N 被填充为前后两个元素的平均值5.0。而 当 method 为”
index”
时,贝IH史用索引值进行插值运
算。由于第1个元素的索引与第2 个元素的索引接近,因此其插值结果也接近第二个元素的值。

s = p d . S e r i e s ([3, np.NaN, 7], index=[0, 8^ 9])

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

dtype: float64 dtype: float64

此外还可以使用字典参数让fillna()对不同的列使用不同的值填充NaN :

print df_nan. f il l n a ( { ,,B":-999, "C":0})

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

各种聚合方法的 skipna 参数默认为 True , 因此计兑时将忽略 N a N 元素,注意每行或每列


丨方便的数据分析库

是单独运兑的。如果需要忽略包含 N a N 的整行,需要先调用 dropnaO。若 将 skipna 参数设置为


False , 则包含 N a N 的行或列的运算结果为NaN 。

df _nan.sum() d f _ n a n .sum(skipna=False) df_ n a n .d r o p n a ().s u m ( )

A 143 A 143 A 64
B 42 B NaN B 24

C 24 C NaN C 21

dtype: float64 dtype: float64 dtype: float64

df.combine_ first(other)使 用 other 填 充 d f 中 的 N aN 元素。它 将 d f 中 的 N aN 元素替换为 other


中对应标签的元素。在下面的例子中,
df_nan 中索引为1、
2、8、
9 的行中的 N aN 被替换为 df_olher
中相应的值:

df 一other = pd.DataFrame(np.random.randint(0, 10, (4, 2)),

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 的形状

与本节内容对应的 Notebook 为: 05-pandas/pandas-800-changeshape.ipynb〇

本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 对象的行索引是多级索引:

soils = pd.read_csv("Soils.csv", index 一col=0)[["Depth", "Contour", "Group", "pH", "N"]]


soils_mean = soils.groupby(["Depth", "Contour"]).mean()

s o i l s . h e ad () s o i l s _ m e a n .h e a d ()

Depth Contour Group pH N Group pH N

1 0-10 Top 1 5.4 0.19 Depth Contour

2 0-10 Top 1 5.7 0.17 0-10 Depression 9 5.4 0.18

3 0-10 Top 1 5.1 0.26 Slope 5 5.5 0.22


4 0-10 Top 1 5.1 0.17 Top 1 5.3 0.2

5 10-30 Top 2 5.1 0.16 10-30 Depression 10 4.9 0.08

Slope 6 5.3 0.1


Python科学计算 (第 2 版)

1.添加删除列或行

由于 DataFmme可 以看作一个 Series 对象的字典,因此通过 DataFrame[colname] = values 即


可添加新列。有时新添加的列是从已经存在的列计算而来,这时可以使用 eval()方法计算。例
如下面的代码添加一个名为N_percent的新列,其值为 N 列 乘 上 100:

soils["NjDercent"] = soils.eval("N * 100")

assign〇方法添加由关键字参数指定的列,它返|n |—个 新 的 DataFrame对象,原数据的内容

保持不变:

print soils.assign(pH2 = soils.pH + l).head()

Depth Contour Group pH N N_percent pH2

1 0-10 Top 1 5.4 0.19 19 6.4


2 0-10 Top 1 5.7 0.17 16 6.7

3 0-10 Top 1 5.1 0.26 26 6.1

■D 4 0-10 Top 1 5.1 0.17 17 6.1


andas

5 10-30 Top 2 5.1 0.16 16 6.1


丨方便的数据分析库

append()方法用于添加行,它 没 有 inplace 参数,只能返冋一个全新对象。由于每次调用


appendO都会复制所有的数据,因此在循环屮使用append〇添加数据会极大地降低程序的运总速
度。可以使用一个列表缓存所有的分块数据,然后调用 concatO将所有这些数据沿着指定轴拼贴
到一起。下面的程序比较二者的运箅速度:

def random 一
dataframe(n):

columns = ["A", "B", "C"]


for i in range(n):

nrow = np.random.randint(10, 20)

yield p d .D a t a F r a m e ( n p .random, r a n d i n t (0., 100, s ize=(nrow> 3)), columns=columns)

df_list = list(random_dataframe(1000))

%%time
df_resl = pd.DataFrame([])

for df in df_list:
df_resl = df_resl.append(df)

df_res2 = pd.concat(df 一list, axis=0)

Wall time: 118 ms


可以使用 k eys 参数指定与每块数据对应的键,这样结果屮的拼接轴将使用多级索引,方便
快速获取原始的数据块。下面获取拼接之后的DataFrame对象中第0 级标签为3 0 的数据,并使
用 equalsO方法判断它是否与原始数据中下标为3 0 的数据块相同:

df_res3 = pd.concat(df_list> axis=0, keys=range(len(df_list)))

d f _ r e s 3 .l o c [30].e q u a l s (d f _ l i s t [30])

True

drop()删除指定标签对应的行或列,下面删除名为 N 和 G roup 的两列:

print soils.drop(["N", "Group"], axis=l).head()

Depth Contour pH N_percent

1 0-10 Top 5.4 19


2 0-10 Top 5.7 16

3 0-10 Top 5.1 26


4 0-10 Top 5.1 17

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

0-10 Depression 9 5.4 0.18


0-10 Slope 5 5.5 0.22
0-10 Top 1 5.3 0.2
10-30 Depression 10 4.9 0.08

10-30 Slope 6 5.3 0.1

setJndexO 将列转换为行索弓丨,如 果 append 参 数 为 False(默认值),贝_ 除当前的行索引;


若为 Tm e , 贝ij为当前的索引添加新的级别。

print soils_mean.set 一index ( " G ro u p '、 append=True).head()

pH N
Depth Contour Group

0-10 Depression 9 5.4 0.18

Slope 5 5.5 0.22


Top 1 5.3 0.2
10-30 Depression 10 4.9 0.08

Slope 6 5.3 0.1


Python 科学计算 (第 2 版)

3.行索引和列索引的相互转换

stack〇方法把指定级別的列索引转换为行索弓丨,而 unstackO则把行索引转换为列索弓I。下

面的程序将行索引中的第一级转换为列索引的第一级,所得到的结果中行索引为单级索引,而
列索引为多级索引:

print soils_mean.unstack(l)[["Group", "pH"]].head()

Group pH
Contour Depression Slope Top Depression Slope Top
Depth

0-10 9 5 15.4 5.5 5.B

10-30 10 6 24.9 5.3 4.8


30-60 11 7 34.4 4.3 4.2
60-90 12 8 44.2 3.9 3.9

无 论 是 stack()还 是 unstack〇,当所有的索引被转换到同一个轴上时,将 得 到 一 个 Series


■D 对象:
andas

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.交换索引的等级

reorder_levels〇和 swaplevel()交换指走轴的索引级别。下 面 调 用 swaplevel()交换行索引的两


个级别,然后调用 sort_indeX()对新的索引进行排序:

print soils_mean.swaplevel(0, 1 ) .sort 一index()

Group pH N
Contour Depth

0-10 9 5.4 0.18


10-30 10 4.9 0.08

30-60 11 4.4 0.051

60-90 12 4.2 0.04


Slope 0-10 5 5.5 0.22
10-30 6 5.3 0.1
30-60 7 4.3 0.061
60-90 8 3.9 0.043
Top 0-10 1 5.3 0.2

10-30 2 4.8 0.12

30-60 3 4.2 0.08

60-90 4 3.9 0.058

5.透视表

pivot〇可以将 DataFmme 中的三列数据分别作为行索引、列索引和元素值,将这三列数据转


换为二维表格:

df = soils_mean.reset_index()[["Depth", "Contour", "pH", "N"]]


df_pivot_pH = df.pivot("Depth", "Contour", "pH")

df df 一pivot-pH

Pandas方
Depth Contour pH N Contour Depression Slope Top

- 便的数据分析库
0 0-10 Depression 5.4 0.18 Depth

1 0-10 Slope 5.5 0.22 0-10 5.4 5.5 5.3


2 0-10 Top 5.3 0.2 10-30 4.9 5.3 4.8

3 10-30 Depression 4.9 0.08 30-60 4.4 4.3 4.2


4 10-30 Slope 5.3 0.1 60-90 4.2 3.9 3.9

5 10-30 Top 4.8 0.12


6 30-60 Depression 4.4 0.051

7 30-60 Slope 4.3 0.061


8 30-60 Top 4.2 0.08
9 60-90 Depression 4.2 0.04

10 60-90 Slope 3.9 0.043


11 60-90 Top 3.9 0.058

pivot〇的三个参数 index、columns 和 values 只支持指定一列数据。若不指定 values 参数,就

将剩余的列都当作元素值列,得到多级列索引:

print df.pivot("Depth", "Contour")

pH N
Contour Depression Slope Top Depression Slope Top
Depth

0-10 5.4 5.5 5.3 0.18 0.22 0.2

10-30 4.9 5.3 4.8 0.08 0.1 0.12

30-60 4.4 4.3 4.2 0.051 0.061 0.08

60-90 4.2 3.9 3.9 0.04 0.043 0.058


Python 科学计算 (第 2 版)

melt〇可 以 看 作 pivot〇的逆变换。由于它不能对行索引进行操作,因此先调用 i^etJndexO


将行索引转换为列,然后用 id_vars 参数指定该列为标识列:

df_before_melt = df_pivot_pH.reset_index()
df_after— melt = pd.melt(df— before— melt, id— vars="Depth", value— name="pH")

df before melt df after melt

Contour Depth Depression Slope Top Depth Contour pH

0 0-10 5.4 5.5 5.3 0 0-10 Depression 5.4


1 10-30 4.9 5.3 4.8 1 10-30 Depression 4.9
2 30-60 4.4 4.3 4.2 2 30-60 Depression 4.4

3 60-90 4.2 3.9 3.9 3 60-90 Depression 4.2

4 0-10 Slope 5.5


5 10-30 Slope 5.3

6 30-60 Slope 4.3


7 60-90 Slope 3.9
■D
8 0-10 Top 5.3
andas

9 10-30 Top 4.8


丨方便的数据分析库

10 30-60 Top 4.2

11 60-90 Top 3.9

5 . 6 分组运算

与本节内容对应的 Notebook 为:05-pandas/pandas-900"groupby.ipynb。

所谓分组运算是指使用特定的条件将数裾分为多个分组,然后对每个分组进行运算,最
后再将结果整合起来。
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)

Dose Responsel Response2 Tmt Age Gender

0 50 9.9 10 C 60s F

1 15 0.002 0.004 D 60s F

2 25 0.63 0.8 C 50s M


5.6.1 g ro u p b y ()方 法

如 图 5-5所示,分组操作中涉及两组数据:源数据和分组数据。将分纟11数据传递给源数据
的 groupbyO方法以完成分组。groupbyO的 a x is 参数默认为0 , 表示对源数据的行进行分组。源
数据中的每行与分组数据中的每个兀素对应,分组数据中的每个唯一值对应一个分组。由于图
中的分组数据中有两个唯一值,因此得到两个分组。

groupbyO并不立即执行分组操作,而只是返回保存源数据和分组数据的 GroupBy 对象,


⑷ 在 需 要 获 取 每 个 分 组 的 实 际 数 据 时 ,GroupBy 对象才会执行分组操作。

源数据 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)

<class 'p a n d a s .c o r e .g n o u p b y .D a t a F n a m e G r o u p B y '>

还可以使用列表传递多组分组数据给groupbyO ,例如下面的程序使用处理方式与年龄对源
数据分组:

tmt_age_group = dose_df.groupby(["Tmt"J "Age"])

当分组数据不在源数据中时,可以直接传递分组数椐。在下而的例子中对长度与源数据的
行数相同、取值范围为[0,5)的随机整数数组进行分组,这样就将源数据随机分成了 5 组:

random_values = n p .r a n d o m .r a n d i n t (0, dose_df. s h a p e [0])

random_group = dose_df.groupby(random_values)

当分组数据可以通过源数据的行索引计箅时,可以将计算函数传递给groupbyO 。下面的例
子使川行索引值除以3 的余数进行分纟U , 因此将源数据的每行交替地分为3 组。这是因为源数
39
Python 科学计算 (第 2 版)

据的行索引为从0 开始的整数序列。

alternating group = dose df.groupby(lambda n:n % 3)

上述三种分组数据可以任意囱由组合,例如下而的例子同时使用源数据中的性别列、函数
以及数组进行分组:

crazy_group = dose_df.groupby(["Gender", lambda n: n % 2, random_values])

5.6.2 GroupBy 对象

使 用 len()可以获取分组数:

print len(tmt_age_group), len(crazy_group)

10 20

GroupBy 对象支持迭代接口,它与字典的 iteritems()方法炎似,每次迭代得到分组的键和数


Pandas方

据。当使用多列数据分组时,与每个组对应的键是一个元组:

for key, df in t m t _ a g e _ g r o u p :
- 便的数据分析库

print "key = ' key, _•) shape =_、 df. shape

key = ( ’A 、 '50s’) , shape = (39, 6)

key = ('A', '60s') , shape = (26, 6)

key = ('B', '40s') , shape = (13, 6)

key = ( ’B', '50s') , shape = (13, 6)

key = ( ’B ’
,’6 0 s ’
), shape = (39, 6)
key = ('C', '40s') , shape = (13, 6)

key = (•〔、 ’5 0 s ’) , shape = (13, 6)

key = ('C', '60s') , shape = (39, 6)

key = ('D', '50s') , shape = (52, 6)

key = ('D', '60s') ^ shape = (13, 6)

由于 Python 的赋值语句支持迭代接口,因此可以使用下面的语句快速为每个分组数据指定
变•!£名。这是因为我们知道只有4 种药剂处理方式,并 且 GroupBy 对象默认会对分组键进行排
序。可以将 groupbyO的 sort参数设置为 F alse 以关闭排序功能,这样可以稍微提高大M 分组时的
运算速度。

(_, df 一 A), (_, df_B), (一,df_C), (一,df_D) = tmt_group

由 于 GroupBy 对 象 有 k eys 属性,因此无法通过 dict(tmt_group)直接将其转换为字典,


可以先将其转换为迭代器,再转换为字典 dict(iter(tmt_group))。

34
get_^roup()力‘
法可以获得与指定的分组键对应的数据,例如:

t m t ^ g r o u p .g e t _ g r o u p ("A " ).h e a d (3)

Dose Responsel Response2 Tmt Age Gender


6 1 0 0 A 50s F
10 15 5.2 5.2 A 60s F
12 5 0 0.001 A 60s F

tmt__age_group.get_group((,,A ,,J ''50s")).head(3)

Dose Responsel Response2 Tmt Age Gender

6 1 0 0 A 50s F

17 5 0 0.003 A 50s M
34 40 11 10 A 50s M

对 G roupBy 的下标操作将获得一个只包含源数据中指定列的新GroupBy 对象,通过这种方

Pandas方
式可以先使用源数据中的某些列进行分组,然后选择另一些列进行后续计算。

- 便的数据分析库
print tmt_group["Dose"]
print tmt_group[["Responsel", "Response2"]]

<pandas.core.groupby.SeriesGroupBy object at 0x0C6076F0>

<pandas.core.groupby.DataFrameGroupBy object at 0x0C6077F0>

G roupBy 类中定义了_ getattr_〇 方法,因此当获取 G roupBy 中未定义的属性时,将按照下

面的顺序操作:
•如果属性名是源数据对象的某列的名称,则相当于 GroupBy 丨
name], 获取针对该列的
GroupBy 对象。

•如果属性名是源数据对象的方法,则相当于通过 apply()对每个分组调用该方法。注意
Pandas 中走义/ 转换为 apply()的方法集合,
只有在此集合之中的方法才会被自动转换。
关 于 apply()方法将在下一小节详细介绍。
下面的程序得到对源数据屮的D ose 列进行分组的 GroupBy 对象:

print t m t _ g r o u p .Dose

<pandas.core.groupby.SeriesGroupBy object at 0x05D96B70>

5 . 6 . 3 分组一运算一合并

通 过 GroupBy 对象提供的 agg 〇、transformO、filter〇以及 apply〇等方法可以实现各种分组运


算。每个方法的第一个参数都是一个回调函数,该函数对每个分组的数据进行运算并返回结果。
这些方法根据回调函数的返回结果生成最终的分组运算结果。
Python科学计算(第 2 版)

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 对象传递给回调函数。回调函数对其参数进

行聚合运算,将 Series 对象转换为单个数值,或 将 DataFrame对象转换为 Series 对象。agg()返冋


一•个DataFmme对象,其行索引为每个分组的键,而列索引为源数据的列索引。
在 图 5-6中,上方两个表格显示了两个分组中的数据,而下方左侧两个表格显示了对这两
个分组执行聚合运算之后的结果。其中最左侧的表格是执行g.agg(np.max)的结果。由于 np.max〇
能 对 Series 对象进行运算,因 此 agg()将 分 组 a 和分纟11 b 中的每列数据分别传递给np.max()以计
算每列的最大值,并将所有最大值聚合成一个 DataFrame 对象。例如分纟11 a 中 的 B 列传递给
np.max〇的计算结果为6,该数值存放在结果的第a 行、第 B 列中。

左侧第二个表格对应的程序为:

g.agg(lambda df: df.loc[(df.A + df.B).idxmax()])

由于在回调函数中访问了屈性A 和 B , 这两个屈性在表示毎列数据的Series对象中不存•在,
因 此 传 递 S eries 对象给回调函数的尝试失败。于 是 agg()接下来尝试将表示整个分组数据的
DataFmme 对象传递给回调函数。该回调函数每次返回结果中的一行,例如图中分组 b 对应的

运算结果为第 b 行。该回调函数返回每个分组中A + B 最大的那一行。


下 面 是 对 tmt_gr〇
Up 进行聚合运算的例子。O 计算每个分组中每列的〒均值,注意结果中
自动剔除了无法求平均值的字符串列。©找 到 每 个 分 组 中 R esp on sel 最大的那一行,由于回调
函数对表示整个分组的DataFmme进行运算,因此结果中包含了源数据中的所有列。

agg_resl = tmt_group.agg(np.mean) O
agg_res2 = tmt_group.agg(lambda df:df.loc[df.Responsel.idxmax()]) ©

agg_resl agg_res2

Dose Responsel Response2 Dose Responsel Response2 Age Gender


Tmt Tmt
A 34 6.7 6.9 A 80 11 10 60s F
B 34 5.6 5.5 B le+02 11 10 50s M

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_resl = tmt_group. transform (lambda s:s - s . m e a n 〇) O

transform_res2 = tmt_group.transform(

lambda d f :d f .a s s i g n (R e s p o n s e l = d f .Responsel - d f .R e s p o n s e l .m e a n ())) ©

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)

Dose Responsel Response2 Dose Responsel Response2 Age Gender

0 16 5.8 5.9 0 50 5.8 10 60s F

1 -19 -3.3 -3.2 1 15 -3.3 0.004 60s F

2 -8.5 -3.4 -3.3 2 25 -3.4 0.8 50s M

3 -8.5 -2.7 -2.6 3 25 -2.7 1.6 60s F

4 -19 -4 -4.1 4 15 -4 0.02 60s F

3. filter()— 过滤

filter()对每个分纟J1进行条件判断。它将表示每个分纟J1的 DataFrame对象传递给回调函数,该

闲数返 N T m e 或 False , 以决定是否保留该分组。filterO的返丨Hi结果是过滤掉一些行之后的


Python 科学计算 (第 2 版)

DataFmme 对象,其行索引与源数据的行索引的顺序一致。

下面的程序保留Responsel 列的最大值小于11的分组,
注意结果中包含用于分组的列Tm t :

print tmt^group.filter(lambda df:df.Responsel.max() < 1 1 ) ,head()

Dose Responsel Response2 Tmt Age Gender

0 50 9.9 10 C 60s F

1 15 0.002 0.004 D 60s F

2 25 0.63 0.8 C 50s M

3 25 1.4 1.6 C 60s F

4 15 0.01 0.02 C 60s F

4. apply()—运用

applyO将表示每个分组的 DataFmme 对象传递给回凋函数并收集其返回值,并将这些返回

值按照茶种规则合并。apply()的用法十分灵活,可以实现上述 agg ()、transformO和 filter〇方法的

Tl 功能。它会根据回调函数的返回值的类型选择恰当的合并方式,然而这种自动选择有时会得到
a n d a丨

令人费解的结果。
s方便的数据分析库

图 5-7域示了对包含 a 和 b 两个分组的 G ro u p B y 对 象 g 执 行 a p p l y ()后得到的4 种结果:


如图5-7(左一)所示,M 调函数为 DataFrame.max ,它计算 DataFrame对象中每列的最大值,
返回一个以列名为索引的 Series 对象,因此对于所有的分组数据返回的索引都是相同的。这种
情况下 apply()的结果与 agg ()相同,是一个以每个分组的键为行索引、以所有返回对象的索引为
列索引的 DataFrame对象。
如 图 5-7(左二)所示,当回调闲数返回的 S e r i e s 对象的索引不是全部一致时, a p p l y O 将这些
S e r ie s 对象沿垂:直方丨⑷连接在一起,得到一个多级索引的S e r i e s 对象。多级索引由分组的键和每
个 S e r i e s 对象的索引构成。
在阁5-7(左三)中,回调函数返回的是DataFmme对象,并且结果的行索引与参数的行索引
是同一索引对象,即满足如下条件:

df.index is (df - d f . m i n 〇).index

则 apply()返回的 DataFmme对象的行索引与源数据的索引一致,这 与 filterO的结果相同。


在图5-7(左四)中,回调函数的返回结果与(图5-7左三)相同,但由于使用[:]复制了整个数
据,因此其返冋对象与参数的索引不是N —•对象。这种情况下,将按照分组的顺序沿垂直方向
将所有返冋结果连接在-•起,得 到 •个 多 级 索 引 的 D a ta F m m e 对象。多级索引由分组的键和每
个D a ta F ra m e 对象的索引构成。
3
a b
A B A B
0 1 2 1 7 0
2 5 6 4 3 8
3 9 4

.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 的每个分组中每列的最大值和平均值。注意最大值的结果中包含字符
串列,而平均值的结果中不包含字符串列:

tmt_jgroup.apply (p d .Data Frame.m a x ) tmtjgroup.apply(pd.DataFrame.mean)

Dose Responsel Response2 Tmt Age Gender Dose Responsel Response2


Tmt Tmt
A le+02 11 11 A 60s M A 34 6.7 6.9
B le+02 11 10 B 60s M B 34 5.6 5.5
C le+02 10 11 C 60s M C 34 4 4.1
D le+02 11 9.9 D 60s M D 34 3.3 3.2

下面的程序从每个分组的Responsel 列随机取两个数值。O 由于 sampleO保留与数值对应的


标签,因此结果是一个多级标签的Series 对象。© 对 sample()的结果调j:U reset_ index〇方法,这
样所有返回结果的标签全部相同,因此得到的结果是一个 DataFrame对象,其每一行与一个分
组对应。

sample_resl = tmt^roup. apply (lambda df:df.Responsel. sample (2)) O


sample_res2 = tmt_group.apply(
lambda df:df.Response!.sample(2).rGSGt_indGx(drop=True)) ©

45
Python科学计算 (第 2 版)

sample 一resl sample 一res2

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对象时,根据其行标签是否与参数对象的行标签为同-
对象,会得到不同的结果:

■D group = tmt_group[[,,RGsponsGl,', "Responsel"]]


andas

apply一resl = group .apply (lambda df :df - df .mean〇 )


丨方便的数据分析库

apply一res2 = group.apply(lambda df:(df - df.mean())[:])


apply_resl.head() apply_res2.head()

Responsel Responsel Responsel Responsel

0 5.8 5.8 Tmt


1 -3.3 -3.3 A 6 -6.7 -6.7
2 -3.4 -3.4 10 -1.5 -1.5
3 -2.7 -2.7 12 -6.7 -6.7
4 -4 -4 17 -6.7 -6.7
32 2.6 2.6

当 冋调函数返冋 N o n e 吋,将忽略该返冋值,因此可以实现 filterO的功能。下面的程序从


R e s p o n s e l 的均值大于 5 的分组中随机取两行数据:

print tmt_group.apply(lambda df:None if df. R e s p o ns e l . m e a n () < 5 else df.sample(2))

Dose Responsel Response2 Tmt Age Gender


Tmt

A 235 60 9.8 10 A 50s M


164 20 10 10 A 50s F

B 9 40 11 10 B 60s F

16 30 9.8 10 B 60s F

P a n d a s 使 用 C y t h o n 对一些常用的聚合功能进行了优化处理,例 如 m e a n 〇、m e d i a n ( ) 、var 〇

等。此外,G r o u p B y 还自动将一些常用的 D a t a F m m e 方 法 用 a p p l y 〇包装。因此,通 过 G r o u p B y


对象调用这些方法就相当于将这些方法作为回调函数传递给a p p l y 〇。

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)

Dose Responsel Response2 Dose Responsel Response2

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雖 处 理 和 可 视 化 实 例

与本节内容对应的 Notebook为: 05-pandas/pandas-A 00-examples.ipynb〇

作为木章的最后一节,让我们看两个用 Pandas 分析实际数椐的例子。木节使 用 Pandas 提


供的绘阁方法 pl〇
t()将计算结果显示为阁表,其内部使用 matpbtlib 绘图。关于绘阁方而的洋细
信息请读者参考 matpbtlib 章节。

5 . 7 . 1 分 析 Pandas 项目的提交历史

Pandas 的源代码托管于 GitHub ,由川:界各地的爱好者共冋开发。下面让我们使用 Pandas


分析它的提交记$ 文 件 data/pandas.log 。如果读者的计兑机中安装了 G it 版本控制软件,可以通
过如下命令创建该文件:

git clone https://github.com/pydata/pandas.git

cd pandas

git log > ../pandas.log

该文件中的一条提交记录凼多行文本构成,例如:

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

由于该文件不是由特定分隔符分隔的文本文件,无法使用 rcad_CSv 〇读収,因此我们自己编


写数据读取函数 reacU itJogO 。它是一个生成器函数,每次迭代返冋一个包含作者名、日期和
提交说明的元组。

def r e a d _git_log(l o g _ f n ) :

import io
with io.open(log_fn, "r", encoding="utf8") as f :

author = datetime = None

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方便的数据分析库

if author is not None:


yield author, datetime, u"\n".join(message)

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

message 一start = True


elif message_start:
message.append(line)

下面将生成器的数据转换为DataFmme对象,其中包括了 12109条提交记录:

df-C 〇
mmit = p d .DataFrame(read__git_log("data/pandas.l o g " ),

c o lu m n s = ["A u th o r", " D a te S trin g ", "M e ssa g e "])


print df 一c o m m i t •shape

(12109, 3)

为了分析时间数据,需要将提交时间的字符串转换为吋间列,使 用 t〇
_datetime()可以快速
完成这个任务,它会自动尝试各种常用的日期格式。由下面的输出可知,它进行了时区转换,
将所有的时间都转换成了世界标准时间:

d f _ c o m m i t ["Date"] = pd.to 一
datetime(df — commit.Datestring)

print df_commit[["DateString"> "Date"]].head()

4
3:
DateString Date
0 Tue Dul 7 23:43:31 2015 -0500 2015-07-08 04:43:31

1 Tue 〕ul 7 12:18:50 2015 -0700 2015-07-07 19:18:50


2 Tue 〕ul 7 13:37:38 2015 -0500 2015-07-07 18:37:38
3 Sat Dun 27 15:11:12 2015 -0700 2015-06-27 22:11:12

4 Tue Jul 7 10:53:55 2015 -0500 2015-07-07 15:53:55

为了统计每个时区的提交次数,下而将表示时区的部分提取出来,保存到 Timezone 列中:

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

-0400 5057 BUG 3005


-0500 2793 ENH 1720

-0700 1141 DOC 1666


+0200 1052 TST 1117

-0800 519 CLN 424

d t y p e : int64 d t y p e : int64

为了方便后续处理,下 面 将 D a te 列设置为行索引,并且按照吋间顺序排序。注意我们设
置 drop 参数为 False,保 留 D ate 列。

d f _ c o m m i t .set_index("Date", drop= F a l se > inplace=True)


d f _ c o m m i t .s o r t _ i n d ex (i n p l a c e = Tr u e )

下面统计两次连续提交之间的时间间隔,结果如图5-8所示。O 对 D ate 列调用 diff()方法计


兑前后两个时间点的差,由于最开始的数据无法计算,因此得到的值为N aN , 这里调用 dropnaO
来删除 N aN 。© 为了方便绘制直方统计图,
这里将时间差转换为小时数。© 调 用 plot〇方法绘图,

49
Python 科学计算 (第 2 版)

通 过 k i n d 参数指定图表的类型S nhist", figsize 参数指定图表的大小,其余的参数都传递给实际

的绘图函数 hist〇, plot()方法返回表示子图的对象。

time_delta = df_commit.Date.diff(l).dropna() O

hours_delta = time_delta.dt.days * 24 + time_delta.dt.seconds / 3600.0 ©


ax = houns_delta.plot(kind=Mhist", figsize=(8, 3)^ ©
bins=100, h i s t t y pe="step ",range=(0, 5 ),linewidth=2)

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 = df_commit.Author.resample("W") how="count").plot(kind="area", f i g s i z e = (8, 2.5))


ax.grid(True)

ax •set_ylabel (u _•提交次数" )

201 ( 201 2012 2013 2014 2015


Date

图5 - 9 每个期的提交次数

351
h o w 参数还支持回调函数,例如下面的程序绘制每个月的提交人数,结果如图 5 - 1 0 所示。
这里使用 len(s.imique 〇)得到每个分组去重之后的长度:

ax = df _commit.Author.resample("M", how=lambda s:len(s.unique())).plot(

kind="area", figsize=(8, 2.5))

a x .set_ylabel (u "提交人数••)

2014 2015
Dot

Pandas方
图5 - 1 0 每个月的提交人数

- 便的数据分析库
请读者思考如何使用 groupbyO 实现与上述 resample〇相同的运算。

下面使用 value_counts〇方法统计每位作者的提交数。Pandas 的创始人 Wes M cK in n ey 目前


还是排在第一位:

s_authors = df_commit.Author.value_counts()

print s_authors.head()

Wes McKinney 3115


jreback 3009

y-p 943
Chang She 629

Phillip Cloud 596

d t y p e : int64

下而使用 crosstabO 统计每个月每位作者的提交数,所得到的结果 d f_C〇unt s 的行索引为月份,


列索引为作者。这M 通 过 D a t e t i m e l n d e x 的 to_peiiod 〇将时间点转换为以月为市位的时间段。结
果中包含72个月、485位作者的提交数:

df_counts = p d .c r o s s t a b (d f _ c o m m i t .i n d e x .t o j D e r i o d ("M"), df_commit.Author)


d f _ c o u n t s .i n d e x .name = "Month"

print d f _ c o u n t s .shape
Python 科学计算(第2 版)

(72, 485)

下面获収 s_authors中排前5 名的作者对应的列,并调用 plot〇绘图,结果如图5-11所示。


默认情况下 plot()将多列数据绘制在一个子图中。这里将 subplots参数设置为 Tm e ,让每列数据
绘制在不同的子图中。通过将 sharex 和 sharey 参数设置为 True ,让所有子图的 X -Y 轴的数据范
围保持一致,以便比较数据。由图可知创始人已于2013年下半年离开该项R , 目前由 jreback
接手。

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

下而我们绘制与 GitHub 类似的活动记录阁,效果如阁5-12所示。阁中的每个方块表示一


天的提交数,每一列表示一个星期。〇首先将索引转换为以天为周期的时间段索引,然后统计
次数,就得到了每天的提交数,其索引为以天为周期的时间段。© 将该索引转换多级索引,第
0 级是以星期为周期的索引,第 1级是表示星期几的整数,0 表示星期一。

352
© 使 川 unstack()将 访 0 级索引转换为列索引,得到一个 DataFrame对象,获取其最后60周
的数据之后,将其中的 N aN 填充为0。这 样 就 得 到 了 于 做 图 的 active_data 数据。
注 意 daily_com m it 的索引不是按照时间先后顺序排列的,但 unstack〇返回的结果中行索弓I
和列索引都是按从小到大的顺序排列的:

daily 一commit = df 一commit.index.to_period("D").value 一c o u n t s () O


d a i l y _ c o m m i t .index = p d •Multilndex.from 一arrays([daily 一c o m m i t . i n d e x . a s f r e q C ’W"), ©
daily 一c o m m i t •inde x . w e ek d a y ])

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

下面是绘图部分,O 调 用 pcolonneshO绘制填充方格,然后调用 set_ xticks〇等函数修改 X 和


Y 轴上刻度的位置和文本:

fig, ax = pi.subplots(figsize=(1 5 ^ 4))

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)

from pandas.tseries.frequencies import DAYS

ax.set_yticklabels(DAYS)

阁5-12 Pandas 项冃的活动记录阁

也可以使用木书提供的pl〇t_dalafVaine_as_colormesh 〇来绘制,效果见图 5-12:

from scpy2.pandas import plot 一dataframe— as 一colormesh

plot_dataframe 一as 一colormesh(active 一data, x t i c k_start=3> xtick_step=10,

xtick_format=lambda p e r i o d :s t r ( p e r i o d •start 一t i m e •d a t e ( )),

ytick_format=dict(zip(range(7), DAYS)).get^

cmap="Greens__, vmin=0, v m a x = a c t i v e _ d a t a .values .max() * 0.75)


Python 科学计算(第 2 版)

5 . 7 . 2 分析空气质量数据

在前面介绍输入输出的小节里,我们将 data/a q i 文件夹下的所有空气质量数据的C S V 文件


写入了 HDF 5 文 件 aqi.hdf5。下 面 我 们 利 Pandas分析这些数据。首先使川 HDFStore 打开保存
数据的 HDF 5 文件,并调用 selectO来读入所有数据,如 表 5-5所示:

表 5 - 5 空气质量数据
index T ime City Position AQI Level PM2_5 PM10 CO N〇2 03 S〇2

1 2014-04-11 上海 普陀 76 良 49 101 0.0 0 0 0

15:00:00

2 2014-04-11 上海 十五厂 72 良 52 94 0.479 53 124 9

15:00:00

3 2014^)4-11 上海 虹口 80 良 59 98 0.612 52 115 11

15:00:00

■D 4 2014-04-11 上海 徐汇上师大 74 良 54 87 0.706 43 113 14


andas

15:00:00
丨方便的数据分析库

5 2014-04-11 上海 杨浦四漂 84 良 62 99 0.456 43 82 9

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

我们发现每座城市都有两利1表示形式,下面使用 str.replace()将 结 尾 的 “市”删除,并将其


转换为分类类型:

df_aqi["City"] = df_aqi.City.str.r e p l a c e ("?)?", " " ) .astype("category")

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

AQI PM2_5 PM10 CO N02 03 S02

AQI 1 0.944 0.694 0.611 0.5B4 -0.136 0.42

PM2_5 0.944 1 0.569 0.633 0.556 -0.169 0.426

PM10 0.694 0.569 1 0.46 0.472 -0.136 0.414

CO 0.611 0.633 0.46 1 0.565 -0.233 0.538

N02 0.534 0.556 0.472 0.565 1 -0.439 0.448

03 -0.136 -0.169 -0.136 -0.233 -0.439 1 -0.198

S02 0.42 0.426 0.414 0.538 0.448 -0.198 1

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 空气质量参数:乙间的相关性

下面比较每座城市每天的PM 2.5值的分布情况。O 首先调用 gmupbyO , 按照 t l 期和城市分


组,© 然后计兑每个分组的 PM 2.5的平均值,结果是一个多级索引的 Series 对象。索引的第0
级为日期,第 1级为城市。接着调用 unstackO将 第 1级转换为列,因此 mean_pm2_5是一个行索
引为口期、列索引为城市的 DataFmme 对象。€)调 川 pl〇
t(kind=”
hist”
)绘制直方统计图,它会对
DataFmme 对象中的每列数据进行运算,结果如图5-14(左)所示。
Python科学计算(第 2 版)

daily_city_groupby = df_aqi.groupby([df_aqi.Time.dt.date^ "City"]) O


mean_pm2_5 = daily_city_groupby.PM2_5.mean().unstack() ©

m e a n J 3 m 2 _ 5 .p l o t (k i n d = "h i s t ", histtype=__step", bins=20, normed=True, lw=2) ©

还可以将 kind 参数设置为"kde”


以绘制核密度估计分布,结果如图5-14(右)所示。由于使用
了高斯核,因此图屮均值小于0 的概率密度不为零。

ax = mean J3m2_5.plot(kind="kde")

a x . s et_xlim(0J 400)


力H
OtOM

0U0I2

0010

0002
&000 —
Pandas方

50 100 ISO 200 »0 300 ISO

图5 - 1 4 每座城市的日平均P M 2 . 5 的分布图
- 便的数据分析库

下而计算三座城市每天的PM 2.5之间的相关性,如 表 5-6所示。可以看出上海和北京基木


无相关性,而天津和北京则有较强的相关性。

m e a n _ p m 2 _ 5 .c o m ()

表 5 * 6 三座城市 P M 2 . 5 之间的相关性

index 上海 北京 天津
上海 1.0 -0.146 0.0718

北京 -0.146 1.0 0.691

天津 0.0718 0.691 1.0

K 而统计一个星期中每天的 PM 2.5的平均值,通 过 dt.day〇


lV e e k 可以获得表示星期几的整
数序列,其 中 0 表示星期一。结果如阁5-15所示,可以看出北京市星期四、星期五、星期六的
空气质M 要比其他时间的差一些。

week_mean = df _ a q i .g r o u p b y ([d f _ a q i .T i m e .d t .d a y o f w e e k , "City"]).P M 2 _ 5 .m e a n ()


ax = week_mean.unstack().plot(kind="Bar")

ax.legend(bbox_to 一anchor=(0., 1.02, 1.) .102), loc=3,


ncol=3, m o d e = "e x p a n d ", b o r d eraxespad=0.)

from pandas.tseries.frequencies import DAYS

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

ax.legend(bbox 一to_anchor=(0., 1.02, 1.) .102), loc=3)


ncol=3j b o r d eraxespad=0.)

■■上渴 ■■北铒 xm

阁5-16 — 天中+ 同时段的P M 2 . 5 的平均值

下 面 计 算 北 京 市 每 个 观 测 点 的 每 个 月 的 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
平均值:

places = place_mean.iloc[[0, 1, -2, - 1 ] ] .index

m o n t h j 3 l a c G _ m e a n . u n s t a c k O . l o c t :, places].plot(kind=_'bar' 、 f i g s i z e = (1 0 > 3 ) y width=0.8)

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从伊冊始

与本节内容对应的 Notebook 为 :0 6 " S y m p y / s y m p y - 1 0 0 - i n t r o . i p y n b 。

在详细介绍 S y m P y 的语法结构和各种运算功能之前,木节先通过儿个实例说明用 SymPy


解决符号运算问题的一般步骤。

6 . 1 . 1 封面上的经典公式

下面是本书封面左上角的数学公式:
e i7T + 1 = 0
该公式被称为欧拉恒等式,其 中 e 是自然常数,i 是虚数单位,7T是圆周率。它被誉为数学
中最奇妙的公式,它 将 5 个蕋本数学常数用加法、乘法和¥运兑联系起来。下 面 Sym P y 验证
这个公式。
首 先 从 sym py 萍载入所有名字,其 中 E 表示自然常数,I 表示虚数单位,p i 表示同|周率,
因此可以用它们直接求欧拉公式的值:

from sympy import


E**(I*pi) + 1

Sym P y 还可以帮助我们做数学公式的推导和证明。欧拉恒等式可以将 tc代入卜面的欧拉公

式来得到:
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〇的参数 com plex 设置为 True ,则表达式将被分为实数和虚


数两个部分:

expand(exp(I*x)j complex=True)

ie_3xsin〇
Hx) + e_3xcos 〇Rx)

这次表达式展开了,但是得到的结果相当复杂。其中5Rx 是取实数值的函数,3 x 是取虚数


值的函数。之所以会出现这两个函数,是因为 expandO将 x 当作复数处理。为了指定 x 为实数,
S y m py符

需要如下重新定义X:
—号 运 算 好 帮 手

x = Symbol("x", real=True)
expand(exp(I*x), complex=True)

isin(x) + cos(x)

终于得到了欧拉公式,那么如何证明它呢?可以用泰勒多项式对其进行展开:

tmp = series(exp(I*x), x, 0, 10)


tmp

x2 ix3 x4 ix5 x6 ix7 x8 ix9


1 + ix — — ---- — + — + 0 (x 1 0 ,
2 6 24 120 720 5040 40320 362880

展开之后的虚数项和实数项交替出现。根据欧拉公式,虚数项之和应该等于 sin(x )的泰勒


级数,而实数项之和应该等于cos(x )的泰勒展开。下面获得 tm p 的实部:

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

下而对 sin(x )进行泰勒展开,其各项也与上面的结果一致:

series(sin(x), x, 0, 10)

x° x5 X X
x—-+ + 0(x 10 '

6 120 5040 362880

由于e u 的展开公式的实部和虚部分别等于cos x 和sin x , 因此验证了欧拉公式的正确性。

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则做定积分运总:

integrate(x*sin(x)j (x, 0, 2*pi))

—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 进行替代:

circle_area = c i r c l e _ a r e a .s u b s (r y sqrt(r**2 - x**2))


cincle_anea

7i(r2 — x 2)

然后对 circle_area中的变量 )c 在区间-i•到 r 上进行定积分,得到球体的体积公式:

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 是表示函数的符号,而 f (x )则是自变量为 x 的函数,下面调用其 diff()方法,对 x 求一阶

导数:

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〇方法计算导函数:

sym_dexpr = f_diff.subs(f(x), x*exp(-x**2)).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()
可以对数组进行运算:

sym— dfunc = lambdify([x], sym_dexpr, modules="numpy")

s y m _dfunc(np.array([-1, 0, 1]))

a r r a y ( [ -0.36787944, 1. , -0.36787944])

由于 eX pr_diff是一个加法表达式,因此通过其 args 属性可以获得所有的加法项:

print expr_diff.args

(-3*f(-h + x)/h, + x)/(3*h), 3*f(-2*h + x)/(2*h), ll*f(x)/(6*h))

上面的加法项没有按照自变量从小到大的顺序排列。
下面使用通配符 w 和 c 组成的模板 c *
f (w )对每个加法项进行匹配,提収出每项的系数和函数参数:

w = Wild("w")
c = Wild(..c")

patterns = [arg.match(c * f(w)) for arg in expr_diff.args]

每个匹配结果是一个以通配符为键的字典,例如下面是第一项的匹配结果,它表示该项的
系数为-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 对应的表达式:

coefficients = [t[c] for t in sorted(patterns, key=lambda t:t[w])]


print coefficients
[-l/(3*h), 3/(2*h), -3/h, ll/(6*h)]

下面将系数表达式列表屮的h 替换为 0.001, 得到系数数组。注 意 S y m P y 的浮点数运算得


到的楚 S y m P y 屮的 Float对象,还需要调用 float〇来将:jL:转换为 Python的 float对象:

coeff_arr = np.array([float(coeff.subs(h, le-3)) for coeff in coefficients])


print coeff_arr
[-333.33333333 1500. -3000. 1833.33333333]*
x

接下来使用 N u m P y 计纟7:数值微分的值,并 与 sym_dflinc()的运算结果进行比较,输出最大


S y m py符

绝对误差:
—号 运 算 好 帮 手

def moving_window(x, size):


from numpy.lib.stride_tricks import as_strided
x = np.ascontiguousarray(x)
return as_strided(x, shape=(x.shape[0] - size + 1, size),
strides=(x.itemsize, x.itemsize))

x_arr = np.arange(-2, 2, le-3)


y_arr = x_arr * np.exp(-x_arr * x_arr)
num_res = (moving 一
window(y_arr, 4) * c o e f f _ a m ) .sum(axis=l)
sym 一res = s y m _dfunc(x_arr[3:])

print np.max(abs(num_res - sym_res))

4.08944167418e-09

为了比较点数与误差之间的关系,在下面的 finite_diff_coefficients〇函数屮计算间隔为h 、点
数为 order的系数,并绘制 2 、3 、4 点公式对应的误差曲线,结果如图 6-1所示,注 意 Y 轴为对
数轴。

def finite_diff_coefficients(f_diff, order, h):


v = variables[0]
points = [x - i * h for i in range(order)]
expr_diff = as_finite_diff(f_diff, points)
w = Wild(V)
c = Wild("c")
patterns = [arg•match(c*f(w)) for arg in expr_diff.args]
coefficients = np.array([float(t[c])

64
for t in sorted(patterns, key=lambda t:t[w])])
return coefficients

sy m p y符
阁6 ~ 1 比较不同点数的数值微分的误差

—号 运 算 好 帮 手
6.2数 学 式

与本节内容对应的 Notebook 为:06-sympy/sympy-200-expression.ipynb。

本节详细介绍数学表达式的结构,虽然这部分内界比较枯燥,似只有了解表达式的结构,
才能随心所欲地对其进行处理,将 Sym P y 运州到更复杂的计算中。

6 . 2 . 1 符号

数学符号用 Sym bol 对象表不,符号对象的 name 属性是符号名,符号名在显不itl 此符号构


成的表达式时使用。Sym bol 对象的符号名和 Python 的变M 名没有内在联系,但是为了使用起来
方便,通常让变M 名与符号名相同。为 了 快 速 创 建 符 号 以 及 与 之 同 名 的 变 可 以 使 用 var〇,
例如:

print var ( " x 0 , y0 , x l , y l M )

(x0, y0, xl, yl)

上而的语句创建了名为 xO、yO、x l 、y l 的 4 个 Sym bol 对象,同时在当前名称空间中创建


了 4 个名为 xO、yO、x l 、y l 的变量来分别表示这些Sym bol 对象。因为符号对象在转换为字符

65
Python 科学计算 (第 2 版)

串时直接使用name 属性,因此在交互式环境中看到变量xO 显示的值就是 xO:

type(x0) x0.name type(x0.name)

s y m p y .c o r e .s y m b o l .Symbol 'x0' str

在交互环境屮使用 var()能快速创建变量和 Sym bol 对象,但在程序屮使用容易引起混淆,


这吋可以使用 symbols〇创 建 Sym bol 对象,再将它们显式地赋值给变量:

yl, yl = symbols("xl, yl")

type(xl)
s y m p y .c o r e .s y m b o l .Symbol

当然,如果不嫌麻烦也可以直接使用Sym bol 类创建对象:

x2 = S y m b o l ( Mx2")

变量名和符号名当然也可以是不一样的,下面使用变量 t 表示符号 xO, 然后创建两个名为


S y m py符

alpha和 beta 的符号,并用变量 a 和 b 分別表示。当符号使用希腊字母名时,可以M 示为希腊字母。


—号 运 算 好 帮 手

t = X0
a, b = symbols("alpha, beta")

sin(a) + sin(b) + t

x 〇+ sin (a ) + sin (p)

数学公式中的符号一般都有特定的假设,例 如 m 、11通常是整数,而2经常)|;
|来表示复数。
在 用 vm<)、symbols()或 Sym tol ()创 建 Sym bol 对象时,可以通过关键宁•参数指定所创述符号的假
设条件,这些假设条件会影响到它们所参与的计算。例如,下面创建了两个整数符号m 和 n 以
及一个正数符号 X:

m, n = symbols("m, n", integer=True)

x = Symbol("x''^ positive=True)

每个符号都有许多 is_*属性,用以判断符号的各种假设条件,在 IPythoii 中使用自动完成


功能可以快速查看这些假设的名称。注意下划线后为大写字母的属性用来判断对象的类型,而
全小写字母的属性则用来判断符号的假设条件。

[attr for attr in dir(x) if attr.startswith("is_") and a t t r . l o w e r () == attr]

[•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

True True FalseTrue

使 用 assumptions!)屈性可以快速查看所有的假设条件,其111 commutative为 T ru e 表示该符


号满妃交换律,其余的假设条件根据英文名很容易知道它们的含义,这里就不再详细叙述了。

x.assumptions©

{ ' c o m m u t a t i v e ': True, ’complex': True, 'h e r m i t i a n ': True,

'imaginary': False, 'n e g a t i v e ': False, 'n o n n e g a t i v e ': True,

'n o n p o s i t i v e ': False, 'nonzero': Truej 'positive': True,

'r e a l ': True, •zero': False}

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 数值

为了实现符号运算,在 S y m P y 内部有一整套数值运算系统,因 此 S ym P y 的数值和 Python


的整数、浮点数是完全不同的对象。为了使用方便,S y m P y 会尽量自动将 Python 的数值类型
转换为 S y m P y 的数值类型。S ym P y 提供了一个 S 对象以方便用户快速将 Python 的数值转换成
S y m P y 的数值。在下而的例子中,当 有 S y m P y 的数值参与计算时,结 果 也 为 S y m P y 的数值

对象:

1/2 + 1/3 S(l)/2 + 1/S(3)

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

Rational(5, 10) # 有理数会自动进行约分处理

1
2

实数用 F l o a t 对象表示,它和标准的浮点数类似,但足它的精度(有效数字)u丨以通过参数指
定。兩于在浮点数或 F l o a t 对象内部都使用二进制方式表示数值,冈此它们都可能无法精确表
示丨•进制中的精确小数。可以使用 N ()査看浮点数的实际数值,例如下而的语句査看浮点数0.1
和 10000.1的 60位有效数字,可以看到数值的绝对精度随着数值的增大而减小:
S y m py符

print N(0.1, 60)


—号 运 算 好 帮 手

print N(10000.1, 60)

0.100000000000000005551115123125782702118158340454101562500000

10000.1000000000003637978807091712951660156250000000000000000

因为浮点数的精度有限,所以在使用它创建 F lo at 对象时,即使指定精度参数也不能缩小
它与理想值之间的误差,这时可以使用字符弟表示数值:

print N(Float(0.1, 60), 60) # 用浮点数创連R e a l 对象时,精度和浮点数相同

print N(Float("0.1", 60), 60) # 用字符串创建R e a l 对象时,所指定的精度有效


print N(Float("0.1", 60), 65) 度W 高,也不是完全精确的
0.100000000000000005551115123125782702118158340454101562500000
0 . 100000000000000000000000000000000000000000000000000000000000
0.099999999999999999999999999999999999999999999999999999999999996111

N ()〇T以将数值算式按照指定精度转换成Float 对象,下面计兑7C和W 的 5 0 位精度的浮点

数值:

print N(pi, 50)


print N(sqrt(2), 50)

3.1415926535897932384626433832795028841971693993751
1.4142135623730950488016887242096980785696718753769

6 . 2 . 3 运算符和函数

Sym Py 重新定义了所W 的数学运算符和数学函数。


例如 A d d 类表示加法,M u l 类表示乘法,
8
6
而 P o w 类表示指数运算,
sin 类表示正弦函数。
和 Sym bol 对象一•样,
这搜运算符和函数都从Basic
类继承,请读者自行在 IPython 中查看它们的继承列表,例 如 Add .mro()。可以使用这些类创逑
复杂的表达式:

var("x, y, z__)
Add(x, y, z)

x + y + z

Add(Mul(x, y, z), Pow(x, y), sin(z))

xyz + x y + sin(z)

由于在 B a sic 类中重新定义T _ add_()等操作符朴I关的魔法方法,因此可以使用和 Python


表达式相N 的方式创建 S ym P y 的表达式:

x*y*z + 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

t.func t.args t . a r g s [ 0 ] .func t . a r g s [ 0 ] .args

s y m p y .c o r e .a d d .Add (-y, x) s y m p y .c o r e .m u l .Mul (-i, y)

通过上而的例子可以看出,表达式 x - y 在 S y m P y 中头际上是用Add (Mul(-l ,


y), x) 表示的。

同样,S y m P y 中没有除法类,请读者使用和上而相同的方法观察 x / y 在 S y m P y 中是如何表


示的。
S y m P y 的表达式实际上是一个由 B a s ic 类的各种对象多层嵌套结构的树状结构。使用
dotprint()可以将表达式转换成 Graphviz 的 D O T 语言描述的图形,使用本书提供的%d ot 命令可以
yjx2- y 2
将 其 示 在 Noteb(x )k 中。图6~2 S 示了表达式xy 的结构:
x+y

from sympy.printing.dot import dotprint

graph dotprint(x * y * sqrt(x ** 2 - y ** 2) / (x + y))

%dot -f svg graph

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

注 意 F u n ction M 然是一个类,但是上面的语句所得到的 f 并不是 F u n c t i o n 类的实例。和预


定义的数学函数一样,f 是一个类,它 从 F u n c t i o n 类继承 :

issubclass(f, Function)

True

当 用 f 创建一个表达式时,就相当于创建它的一个实例:

t = f(x, y)

type(t) t.func t.args

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

%sympy_latex (3 * x * (x + y)**2).match(a * b**2)

S ym P y符
{a: 3x, b: x + y}

而 find() 方法则在表达式的树状结构中搜索所有与模板匹配的子树。下而在x 3 +

- 号运算好帮手
3 x 2y +

3xy2 + y 3 中搜索与模板匹配的所有子表达式:

expr = expand((x + y)**3)


%sympy_latex expr

%sympy_latex expr.find(a * b**2)

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

return [e .m a t c h (p a t t e r n ) for e in expr.find(pattern)]

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 和 b 都不能包含整数1,而 b 不能包含指数运算,注


意 在 S ym P y 中幵平方使用指数表达式表示,见表6~2。
—号 运 算 好 帮 手

a = Wild("a", exclude=[l])
b = Wild("b", exclude=[l, Pow])

fin d _ m a t ch ( e x p r > a * b**2)

表 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:

expr.replace(a * b**2, (a + b)**2)

4 x 2 + 4 y 2 + (x + 3y)2 + (3x + y)2

使 用 WildFunction 可以定义与任意函数匹配的通配符。在下面的例子中,f 与 exp 、s in 和


abs 这三个闲数匹配,匹配结果包含闲数的参数。而指数运算则被当作运算符,sqrt 实际上使用
指数运算表示,它们都不与 f 匹配,见 表 6^3:

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对表达式的分母进行有理化,结果中的分母部分不含无理数。例如:

radsimp(l / (sqrt(5) + 2 * sqrt(2)))

- ( - V 5 + 2V2)

也可以对带符号的表达式进行处理:

radsimp(l / (y * sqrt(x) + x * sqrt(y)))

-Vxy + Xyfy
xy(x — y)

ratsimpO对表达式中的分母进行通分运兑,即将表达式转换为分子除分母的形式:

37:
Python 科学计算 (第 2 版)

natsimp(x / (x + y) + y / (x - y))

2y2
xz — y z

fraction〇返1h |包含表达式的分子与分母的兀姐, 拕 n j 以获得 ratsimpO通分之后的分子或

分母:

%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 参数可以选择化简兑法:

tnigsimp(sin(x) ** 2 + 2 * sin(x) * cos(x) + cos(x) ** 2)

sin(2x) + 1

expand_trig()展开三角函数表达式:

expand 一
trig(sin(2 * x + y))

(2 cos2(x) - l)sin(y) + 2sin(x)cos(x)cos(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 展开对幕函数的指数和

可以将默认为 T r u e 的标志参数设置为False, 不展开与之对应的表达式。在下而的例子中,


将 m u l 设置为 F a l s e , 因此不展开乘法:

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)

识x识y + i识x3 y + i识y 3 x —3 x3 y

• fimc: 对一些特殊函数进行展开。

e x p a n d (g a m m a (1 + x), func=True)

x「 (x)

• t r i g : 展开三角函数。

e x p a n d (sin(x + y), tnig=True)

sin(x)cos(y) + sin(y)cos(x)

expand_log() 、e x p a n d _ m u l 〇、expand_ c om p l e x ( ) 、expand_trig 〇、expand_func 〇等函数则通过将


3

75
Python 科学计算 (第 2 版)

朴阪的标志参数设置为T rue 来 对 expando进行包装。


factor〇可以对多项式表达式进行因式分解:

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)

a 3 x 3 + x 2 (3a 2 + b 2) + x(3a + 2b) + 2

在默认参数的惜况下,collectO 返 N 的是一个整理之后的表达式。如果希望得到 x 的各次幂


S y m py符

的系数,可以设置参数 e v a l u a t e 为 F a l s e , 让它返回一个以 x 的幂为键、以系数为值的字典。注


意常数项对应的键为S y m P y 中的符号对象1 , 因此需要使用 S(l) 作为键:
—号 运 算 好 帮 手

p = collect(eq2, x, evaluate=False)

P[S(1)] p[x**2]

2 3*a**2 + b**2

也可以使用 coeffO 方法获得系数,下而分别获得常数项和x 2的系数:

eq 2 .coeff(x, 0 ) e q 2 .coeff(x, 2 )

2 3*a**2 + b**2

collect()也可以收集表达式的各次¥的系数,例如下面的程序收集表达式sin2x 的系数:

collect(a * s i n ( 2 * x) + b * s i n ( 2 * x), s i n ( 2 * x))

(a+b)sin(2x)

6 . 3 . 2 方程

在 S ym P y 中表达式可以直接表示值为0 的方程,也可以使用 Eq〇创建方程。sdveO 可以对


方程进行符号求解,它的第一个参数是表示方程的表达式,其后的参数是表示方程中未知变量
的符号。下面的例子使用 s〇M )对一元二次方程进行求解:

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

rl:l于方程的解可能W 多纟11,因此 sdveO 返回一个保存所有解的列表。可以传递包含多个表


达式的元组或列表来让solveO对方程组求解,得到的解是两层嵌$ 的列表,其中的每个元组表
示方程组的一组解:

%sympy_latex solve((x ** 2 + x * y + 1 , y ** 2 + x * y + 2 ), x, y)

V 3i 2i 广、 /V5 i 2i 广、

mots〇可以计算单变量多项式的根:

%sympy_latex roots(x**3 - 3*x**2 + x + 1)

{1:1, 1 + V 2:l , - V 2 + 1:1}

6 . 3 . 3 微分

S ym P y符
Derivative 是表示导函数的类,它的第一个参数是需要进行求导的表达式,第二个参数是求

- 号运算好帮手
导的& 变量。请注意 Derivative 所得到的是一个导函数,它并不会进行求导运算:

t = D erivative(sin(x)> x)

S Sin(X)

调用其 doit〇方法可计算求导的结果:

t.doit()

cos(x)

也可以直接使用 diffO 函数或表达式的diff〇方法来计算导闲数:

diff(sin( 2 *x)j x)

2cos(2x)

使 用 Derivative 对象可以表示 f-|定义的数学函数的导函数,例如

Denivative(f(x), x)

d
f(x)
dx

由 于 Sym Py 不知道如何对自定义的数学函数进行求导,因 此 diff〇会返回和上面相同的结


果。添加更多的自变量符号参数可以表示高阶导函数,例如:

37*
Python 科学计算(第 2 版)

Derivative(f(x), x, x, x) # 也可以写作 Derivative(f(x), x, 3)

d3
f(x)
dx3

也可以对不同的自变量符号计兑偏导数:

Denivative(f(Xj y), x, 2 y y, 3)

d5
f(x,y)
dx2 dy3

diff〇的参数和 Derivative 相同,例如下面的程序计算函数s in (x y )对 x 两次求导、对 y 三次

求导的结果:

di 幵(sin(x * y), x, 2, y, 3)

x(x2y2cos(xy) + 6xysin(xy) —6cos(xy))

6 . 3 . 4 微分方程
S y m py符

ds〇M )可以对微分方程进行符号求解。它的第一个参数是带未知函数的表达式,第二个参
—号 运 算 好 帮 手

数是需要进行求解的未知函数。例如下面的程序对微分方程f '(x ) _ f (x ) = 0进行求解。所得到


的结果是一个0 然指数函数,它有一个待定系数Q 。

x = s y m b o l s( ' x ' )

f = s y m b o l s ('f ', cls=Function)


dsolve(Derivative(f(x), x) - f(x), f(x))

f(x) = Cxex

不同形式的微分方程需要使用不N 的解法,使 用 classify_ode()可以查看与指定微分方程对


应的解法列表。下面查看方程f (x ) + f (x ) = (c o s (x ) - s in (x )) f (x )2对应的解法:

eq = Eq(f(x).diff(x) + f(x), (cos(x) - sin(x)) * f(x)**2)


classify 一
ode(eq, f(x))

(•lst_power 一s e r i e s ', 'l i e _ g r o u p ')

可以通过 dsolve〇的 hint参数指定解法,默认值为'default^ 表示采用 classify_ode()返问值中


的第 个解法:

dsolve(eq, f(x))

Q x2 C!x3 Cax4 CtX5


f(x) = Cx - (—C1(C1 —3) —€^(0^ + 1) + 4Cj 12) + O(x ^)
6 4 120

上面的解是采用泰勒级数展开之后的结果。如果使firiie ^ group•解法,则可以得到更简洁的
结果:

37:
dsolve(eq, f(x), hint="lie_group")

f W = C.e^-sinCx)

也可以将 hint 设置为’


all',让 dsolve()尝 试 classify_ ode()返回的所响解法:

dsolve(eq, f(x), hint="all")

{'l s t _ p o w e r _ s e r i e s ': f(x) == Cl - Cl*x**2/2 - Cl*x**3/6 + Cl*x**4/4 + Cl*x**5*(-Cl*(Cl -


3) - C1*(C1 + 1) + 4*C1 + 12)/120 + 0(x**6),
'best': f(x) == Cl - Cl*x**2/2 - Cl*x**B/6 + Cl*x**4/4 + Cl*x**5*(-Cl*(Cl - 3) - C1*(C1
+ 1) + 4*C1 + 12)/120 + 0(x**6),
'b e s t _ h i n t ': •l s t _ p o w e r _ s e r i e s ',
'default': 'l s t _ p o w e r _ s e r i e s ',

•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

如果要对多个变S 计算多重积分,只需要将被积分的变S 依次列出即可:


• integrate(f ,
x,y ) : 计算双重不定积分J V fd x d y

• 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 版)

调用积分对象的 ddt 〇方法以进行积分运算:

e.doit()

—xcos(x) + sin(x)

有些积分表达式无法进行符号化简,这时可以调川求值方法 evalfO或求值函数 N ()来对其


进行数值运算:

e 2 = Integnal(sin(x)/x, (x, 0 , 1 ))

e 2 .doit()

Si⑴

doit〇返 N 的是一个用特殊闲数表示的值。下而用 evalf〇求其数值:

print e 2 .evalf()

print e2.evalf(50) # 可以指定精度


0.946083070367183
S y m py符

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 输出符号表达式

与本节内容对应的 Notebook 为:06~sympy/sympy"400-output.ipynb。

80
Sym P y 可以将表达式输出成各利1格式,除了 I者如 LaTeX 、M athM L 等显示用的格式之外,
还 n j 以将表达式转换成Python、C 、Fortran 函数,从而将符号表达式转换成数值运算函数。

6.4.1 lambdify

lambdifyO可将表达式转换为数值运算闲数。它的第一个参数是作为参数的符号序列,第二
个 参 数 是 表 达 式 或 表 达 式 序 列 。例 如 下 而 用 solveO 得 到 一 元 二 次 方 程 的 两 个 符 号 解
quadmtic_roots, 然后调用 lambdify〇将其转换成数值运算函数:

b, c, x = s y m b o l s ("a , b, c, x", real=True)

quadratic_roots = solve(a*x * * 2 + b*x + x)


lam 一
quadratic 一roots_real = lambdify([a^ b, c], quadratic 一roots)
lam_quadratic_roots_real( 2 , -3, 1 )

[1.0, 0.5]

所创建的函数默认使用 m ath 模块中定义的数值函数,因此无法计兑根为复数的方程。可


以通过 modules参数指定表达式中函数所在的模块。下而改用 cmath 模块中的 sqrt()闲数,因此

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

还可以使用 numpy 模块中的函数计算多个一元二次方程的解。由于 N um Py 中的部分函数


名 与 S y m P y 中的函数名不同,冈此下而的程序中使用字符串’
numpy’
表 示 n u m p y 模块,这样
lambdilVO会在其内部对闲数名进行自动转换。下面的程序计算4 个一元二次方程的解,为了得
到复数解,需要使用 dtype 为 com plex 的系数数组,运算结果是两个长度为4 的数组:

lam_quadratic_roots_numpy = l a mbdify((aJ b, c), quadratic_roots, modules="numpy")


A = np.array([2, 2, 1, 2], np.complex)

B = np.array([l, 4, 2, 1], np.complex)

C = np.array([l, 1, 1 } 2], np.complex)


lam_quadratic_roots_numpy(A> B, C)

[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()编译表达式

l a m b d i f y O 将表达式转换成 P y t h o n 的函数。然而对于需要大量运兑的表达式,P y t h o n 函数的

运兑能力有限。如果希望更快的运兑速度,可以使用 a u t o w r a p O 将表达式转换成 C 语言或 Fortran


Python 科学计算 (第 2 版)

语言的函数,并编译为扩展模块以供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 头文件所在的路径添加进编译器的头文件搜索路径。

from sympy.utilities.autowrap import autowrap


matrix_roots = Matrix(quadratic_roots)
quadratic_roots_f 2 py = autowrap(matrix_roots, args=[a, b, c], tempdir=r".\tmp")

quadratic 一roots 一cython = autowrap(matrix 一roots, args=[a, b, c], temp d i r = r" . \ t m p ,、


backend="cython"J flags=["-I" + np.get_include()])
sy m p y符

quadratic 一roots_f 2 py( 2 , -3, 1 ) quadratic 一roots 一cython^ -3^ 1 )


—号 运 算 好 帮 手

[[1. ], [[ 1.]>
[0.5]] [ 0.5]]

还可以使用 u f u n c i f y O 将表达式包装为 N u m P y 的 u f u n c 函数。由于不支持将M a t r i x 对象作为


输出,这里将其中的第一个解表达式转换为u f t m c 函数:

from sympy.utilities.autowrap import ufuncify


quadratic_roots_ufunc = ufuncify((a, b, c), q u a d r a t i c _ r o o t s [0]^ tGmpdir=r".\tmp")

quadratic_roots_ufunc([l, 2, 10.0], [6 , 7, 12.0], [4, S, 1.0])

array([-0.76393202, -1. , -0.09009805])

c o d e g e n 〇将表达式转换成源程序。它的第一个参数是一个二元元组或是一个包含多个二元

元组的列表,每个元组由函数名和表达式构成。l a n g u a g e 参数指定输出的源代码的语 言 , prefix


参数指定输出的源代码的文件名。它返冋两个元组,分別为源程序文件和头文件的文件名及其
内容。
如果符号表达式是表示矩阵的M a t r i x 对象,
在输出代码中使用数组保存其中的每个元素。

from sympy.utilities.codegen import codegen

(c_name, c一code), (h一name, c一header) = c o d e g e n (

[("rootO", quadratic_rcx)ts[ 0 ]),


("rootl", quadratic_roots[l]),
("roots", matrix_roots)],

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

double root 0 (double a, double b, double c);


double rootl(double a, double b, double c);
void roots(double a, double b, double c, double *out_1451769269);

sy m p y符
#endif

—号 运 算 好 帮 手
q u a d r a t i c _ r o o t s .c

#include " q u a dratic_roots.h"

#include <math.h>

double root 0 (double a, double b, double c) {

double root 0 _result;


root0_result = (1.0L/2.0L)*(-b + sqrt(-4*a*c + pow(b^ 2)))/a;
return root 0 _result;

double rootl(double a, double b, double c) {

double root 1 一result;


rootl— result = -1.0L/2.0L*(b + sqrt(-4*a*c + pow(b, 2 ) ) )/a;
return rootl_result;

void roots(double a, double b, double c, double *out 1451769269) {


Python 科学计算 (第 2 版)

out_1451769269[0] = (1.0L/2.0L)*(-b + sqrt(-4*a*c + powCb, 2)))/a;


out_1451769269[l] = -1.0L/2.0L*(b + sqrt(-4*a*c + pow(b, 2)))/a;

此外,还可以)ij ccode() 和 fcode() 将符号表达式输出为 C 和 F o r t r a n 语言的表达式:

print ccode(matrix_noots, assi g n _ t o= " y M )

y[0] = (1.0L/2.0L)*(-b + sqrt(-4*a*c + pow(b, 2)))/a;


y[l] = -1.0L/2.0L*(b + sqrt(-4*a*c + p o w ( b > 2)))/a;

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)

%sympy 一latex reduced— exprs

[XoC-b + X!), -XoCb + X!)]

临时符号默认以 X 开头,如果临时符号与表达式中的符号相冲突,可以使用 sym bols 参数


修改••
replacements, reduced_exprs = cse(quadratic_roots^ symbols=numbered_symbols(" t mp"))

%sympy 一latex replacements

(tmp〇
, ▲ ), (tmp” ^/-4ac + b2)

M 然 使 用 cseO 能将复杂的表达式简化为一系列简单的运算步骤,但 是 目 前 S y m P y 的代码


输出功能尚未使用 cse()。因此木书提供/ cse2func()来将表达式通过 cse()转换之后冉输出为
Python 函数。下面用该函数将 roots 中的两个表达式转换为quadmtic_ roots()函数:

from scpy 2 .sympy.cseprinter import cse 2 func

code = cse 2 f u n c ("cse_quadratic_roots(a, b, c )'、 quadratic 一roots)

84
exec code

print code

def cse 一
quadratic 一roots(a, b. c ):
from math import sqrt

_ t m p 0 = 0.5/a

_tmpl = sqrt((b)**( 2 .0 ) - 4.0*a*c)

return (_tmp 0 *(_tmpl - b).» -_tmp 0 *(_tmpl + b))

下面调用生成的 quadmtic_ rootS()函数来计算某个一元二次方程的解:

cse— quadratic 一r o o t s (1 ,-4 ,2)

(3.41421356237, 0.585786437627)

由于在函数中使用 math.sqrt〇进行开方运算,因此不支持结果为复数的情况。可以通过
module 参数指定使用 cmath 模块进行计算:

import cmath

exec c s e 2 f u n c ("c s e _ quadratic_roots(a^ b, c)", quadratic_rootSj module=cmath)

cse_quadratic_roots(l> -4, 10)

((2+2.449489742783178j ), (2-2.449489742783178j ))

此夕卜,cse2ftmc〇还 有 auto_import、calc_number、symbols 等参数,请感兴趣的读者分析该闲


数的源代码以理解这些参数的作用。

6.5麵 运 纖 拟

与本节内容对应的 Notebook 为: 06-syi'npy/sympy-500-mechanics.ipynb〇

S ym P y 还提供了许多专业领域的符号运兑功能,例 如 physics.mechanics 模块可以用于计算

刚体系统的运动方程。作为本章的最后一节,
我们用该模块计算如图所示系统的运动方程,
并进行数值模拟。在图中,滑动方块可以沿参照系/ 的 X 轴自由运动,小球与滑块使用无质量
连杆相连接,可以 A 由摆动。小球的的初始摆动角度为0u。我们希望计算小球释放之后的运动
轨迹。
Python 科学计算(第 2 版)

阁滑块中.摆系统的参照系示意图

6 . 5 . 1 推导系统的微分方程

首先从 sympy.physics.mechanics载 入 所 有 符 ^,
并使)I』
其中的 ReferenceFrame定义参照系/,
用 Point 定义参照点0 。最后调用 O .set_vel〇,设置点0 在参照系/ 中的运动速度为0。

from sympy.physics.mechanics import *


S ym P y符

I = R e f e r e n c e F r a m e ( 'I ') # 定义惯性参照系


0 = PointCO') # 定义原点
- 号运算好帮手

0 .set_vel(I, 0 ) # 设置点0 在参照系I 中的速度为0


g = symbols("g")

http://www.pydy.org/
本 节 只 介 绍 m e c h a n i c s 模块最基本的用法,若读者对使用 S y m P y 求解多刚体系统感兴
趣 ,可 以 参 考 P y D y 扩展库。

下而定义方块在参照系I 中的位置 q 和速度 u ,它们使用 dynamicsymbolsO定义。然后定义


方 块 的 质 量 为 ,质心为点 R 。接下来调用点 P , 的 set_pas 〇方法以设置它相对于点0 的位移,
set_vel 〇方法设置它在参照系 I 中的速度,它们的方向沿着参照系I 的 X 轴。最后在 P , 处创建一
个质量为 rrh 的质点来表示方块。

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 的角速度

细杆的长度为1,小球的质量为 m2。点 P2为小球的质心,它的位置相对于点 P j ,沿着参照


系 B 的 Y 轴负方向偏移1,并通过 v 2pt_theoiy()设麗口2在参照系I 中的速度。若 6和 ^在 参 照 系
B 中相对静止,当 &在 参 照 系 I 中的速度以及参照系 B 与参照系 I 之间的关系都确定时,可以
通 过 P2.v 2pUheory (P l ,
I,B )计 算 P2在 I 中的速度。最后在 P2处放置质量为 m2的小球。

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 的小球

下而显示 P2在 I 中的速度:

P2.vel(I) # 显示P 2 在 I 中的速度


uix + l〇
)t)x

到此为止,各个惯性参照系、坐标点、质点之间的关系已经确定。下而创建 KanesMethod
对象,使用它可以推导出系统的微分方程组。q j i i d 参数为系统中所有与位移相关的独立状态
列表,u j n d 参数为所有与速度相关的独立状态列表,而 kd_e q s 参数则是这些状态之间需要满
足的微分方程。在本例中,方块的位移 q 的导数为速度u ,细杆的旋转角度0 的导数为角速度〇
):

eqs = [q.diff() - u, th.diff() - w] #q 的导数为 u , th 的导数为 w


kane = KanesMethod(I, q_ind=[q, th], u_ind=[u, w], kd_eqs=eqs)

然后调用 kanes_equations〇方法推导微分方程。其中,particles为系统中所包含质点的列表,
fo rc e s 是系统所受外力的列表。每个作用力由作用点和矢量决定,这里定义两个质点上所受的

重力。

particles = [box, ball] #系统包含的所有质点


forces = [(Pl , (
P2, -m2*g*I.y)] # 系统所受的外力
fr, frstar = kane.kanes_equations(forces, particles)

1〇11^_€ 91^〇似0返 问 系 统 的 微 分 方 程 组 。其 |||的 每 个 方 程 为 :1^和&81〇1'中对应的表达式之


Python 科学计算 (第 2 版)

和等于0。

%sympy__latex Eq(fr[0] + frstar[0], 0)


%sympy_latex Eq(fr[l] + frstar[l], 0)

lm2〇)2(t)sin(0(t)) - lm2cos(0(t))^co(t) - (mi + m2) ^ u ( t ) = 0

-glm 2sin(0(t)) - l2m2^ 〇)(t) - lm2cos(0(t)) ^ u (t) = 0

使 用 K a n e s M e t h o d 对象的 m a s s _ m a t r i x _ f u l l 和 forcing_full 厲性可以写出求解系统状态的微分


方程组,其中包含 e q s 中的两个方程:

from IPython import display

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

0 0 lm2cos(0(t)) l2m〇 一 glm2sin(0(t))


l

dt
rv.
3

6 . 5 . 2 将符号表达式转换为程序

当已知系统的状态 q 、0、u 、co 吋,可以通过上面的公式计算出各个状态的导数,因此可


以llj sdpy .integrate.odeint〇对该微分方程纟11进行数值求解,从而计兑出系统的运动轨迹。我们可
以使用 S ym P y 的表达式输出功能,自动生成计算各个状态的导数的函数。下面首先使用矩阵求
逆与矩阵乘法计算线性方程组的解,结 果 difLstatus 是一个形状为(4,1)的 Matrix 对象:

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

为了使用 autowrapO将 diff_status编译成扩展模块,需要将其中与 t 相关的表示状态的符号


替换为一般符号。Ostatus 中的每个元素都是与t 相关的闲数,可以通过 sym .func._ name_ 获取
sym 对应的函数名,并使用该函数名创建勹其同名的一般符号。© 调 用 subsO方法将所有与 t 相
关的符号替换为一般符号,得到与 t 无关的矩阵 expr。© 调用 autowrapO将 expr 转换为计算其值
的函数_ func_diff_status〇:

from sympy.utilities.autowrap import autowrap

status_symbols = [Symbol(sym.func._ name— ) for sym in status] O

expr = diff_status.subs(zip(status, status 一symbols)) ©


_func_diff_status = autowrap(expr, args=[ml, m 2 , 1 , g] + status 一symbols,

t e m p d i r = r " .\tmp_mechanics") @

由于_ func_diff_status()的参数和返回值的形状不符合 odeint()的要求,因此需要使用下而的


func_diff_ status()对其进行包装:

def func_diff 一status(status, t, ml, m 2 , 1 , g):


q, th, u, w = status
return _func_diff_status(ml, m 2 , 1 , g, q, th, u, w ) . r a v e l ()

init_status = np.array([0, np.deg2rad(45), 0, 0])


args = l.Qj 2.0j 1.0, 9.8

func 一diff— status(init_status, 0 , *args)

array([ 0. , 0. , 4.9 , -10.39446968])

K 面调用 odeint〇对 func_difLstatus〇进行积分,得到如图6-4所示的运动轨迹:

S ym P y符
from scipy.integrate import odeint

- 号运算好帮手
t = np.linspace(0, 10, 500)

res = odeint(func— diff 一status, init— status, t, args=args)

fig, (axl, ax 2 ) = p i t . s u b p lo t s (2 , 1 )

axl.plot(t, res[:, 0 ], label=u"$q$")


a x l . l e g e n d ()

ax 2 .plot(t, res[:, 1 ], label=u"$\\theta$")


ax 2 .legend()

0 2 4 6 8 10

图6 4 使用odeint〇计寫的运动轨迹

6 . 5 . 3 动画演示

可以通过 inatplotlib的动画制作功能,直观地显示系统的运动状况,效果如图卜5所示。关

89
Python科学计算 (第 2 版)

于这段程序的编写方法,请参照 matplotlib —章中的相关说明。

0.S

阁动画演示效果

from matplotlib import animation


sy m p y符

from matplotlib.patches import Rectangle, Circle


from matplotlib.lines import Line2D
—号 运 算 好 帮 手

def animate_system(t, states, b l i t = T r u e ) :


thj u, w = states.T
fig = p i t . f i g u r e ()
w, h = 0 .2 , 0 .1

ax = plt.axes(xlim=(-0.5, 1.5), ylim=(-1.5, 0.5), a s p e c t = •e q u a l •)

rect = Rectangle([q[0], 0, - w / 2.0, - h / 2],


vt, h, fill=True, c o l o r = 'r e d ', e c = ' b l a c k 、 axes=ax, animated=blit)
a x .a d d _ p a t c h (r e c t )

line = Line2D([]j [], lw=2, m a r k e r = ' o 、 markersize= 6 > animated=blitj axes=ax)


a x •add 一a r t i s t (l i n e )

circle = Circle((0, 0), 0.1, axes=ax, animated=blit)

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 rect, line^ circle


anim = animation.FuncAnimation(fig, animate, frames=len(t),
intervals[-1] / len(t) * 1000, blit=blit, repeat=False)

return anim

下而使用%1^吨31〇〖
1化 命令将 matplotlib的后台绘阁库改为qt,以便M 外 JT•启窗口来显示动画
演示:

%gui qt
%matplotlib qt
anim = animate一system(t, res)

sy m p y符
—号 运 算 好 帮 手
解7 亭

Traits & TraitsUI-轻松制作图形界面


Python 作为一种高级的动态编程语言,它的变量没有类型,这种灵活性给快速开发带来很
多便利,不过它也不是没有缺点。通 过 Traits 库可以为对象的属性添加类型校验功能,从而提
高程序的可读性,降低出错率。
Traits 库 是 由 Enthought公司开发的一套开源扩展库。虽 然 Traits 库本身和科学计算没有关
系,但它是该公司的興他各种科学计算库的蕋础,因此本书用一整章的篇幅对其进行详细介绍。

7.1 Traits类型入门

与本节内容对应的 Notebook 为:07-traits/traits-KXHntro.ipynb。

Traits 库最初是为了开发交互式绘图库 C h a co 而设计的。通常绘图库中有许多表示图形的


对象,每个对象都有很多诸如线型、颜色和字体之类的屈性。为了方便用户使用,每个属性可
以允许多种形式的值。例如颜色屈性可以是ted ’
、OxfTOOOO或(255,0,0)。也就是说,可以用字符
串、整数或元组等类型的数据表示颜色。这样的需求初看起来用Python 的无类型属性是一个很
好的选择,因为我们可以把各种各样的值赋值给颜色属性。但是颜色属性虽然可以接受多样的
值,却不是能接受所有的值,比如’
abc1和 0.5等就不能很好地表示颜色。而且虽然为了方便用户
使用,对外的接口可以接受多种类型的值,但是在程序内部必须有一个统一的表达方式来简化
程序内部的实现。用 Tm it 属性可以很好地解决这样的问题:
•它可以接受能表示颜色的各种类型的值。
•当给它赋值为不能表达颜色的值时,它能够立即捕捉到错误,并且提供一个有用的错
误报告,告诉用户它能够接受什么样的值。
♦ 它提供内部的、标准的用于表达颜色的数据类型。

7 . 1 . 1 什么是 Traits 属性

下面我们通过一个简单的实例演示Tm it 属性的功能:

from traits.api import HasTraits^ Color O

class C i r c l e ( H a s T r a i t s ) : O
colon = Color ©
Python科学计算(第2 版)

〇T丫先载入HasTraits和 Color ,推荐读者在使用 Enthought公司开发的扩展)军时米用和本例


相同的载入方式。©所 有 拥 有 Trait 属性的类都需要从 HasTmits继承。© C olor 是 Tm it 类型,在
Circle 类中用它定义了 color 属性。
熟 悉 Python 的读者可能会觉得这个程序有些奇怪:按照标准的 Python 语法,直 接 在 class
下定义的属性 color 应该是 C ircle 类的属性,而程序的 FI的是为 C ircle 类的实例添加 color 属性,
是不是应该在初始化方法_ init_〇 中运行 self.color = C olor 呢?答案是否定的,记 住 Trait 属性像
类的属性一样定义,像实例的属性一样使用。我们不管 HasTmits是如何实现这一点的,先看看
如何使用 Tm it 属性:

c = Circle()
Circle.color #Circle 类没有 color 屈性

AttributeEnror Tnaceback (most recent call last)

<ipython-input-4-8335a9908186> in <module>()
1 c = Circle()
Traits & T ra its u轻

----> 2 Circle.color #Circle 类没有 color 厲性

AttributeError: type object 'Circle' has no attribute 'color'


T 松制作图形界面

print c.color

print c.color.getRgb()

<PyQt4.QtGui.QColor object at 0x0542F270>

(255, 255, 255, 255)

从上而的运行结果可以看出Circle 类 没 有 c o l o r 属性,而它的实例 c 则拥有 c o l o r 属性,其


默认值为白色, P y Q t 4 . Q t G u i . Q C o l o r 是 P y Q t 4 界面库所使用的颜色类型。

c.color = "red"

print c.color.getRgb()

c.color = 0 x 0 0 ff 0 0

print c.color.getRgb()

c •color = (0, 255, 255)

print c.color.getRgb()

from traits.api import TraitError

try:

c.colon = 0 . 5

except TraitError as ex:

print ex[0][:350], "•••"

(255, 0, 0, 255)

(0, 255, 0, 255)


(0, 255, 255, 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

#RRRRGGGGBBBB or 'aliceblue' or 'a n t i q u e w h i t e ' or 'aqua' or 'aquamarine' or …

由上而的运行结果可知,我们可以将’
red’
、OxOOmX)和(0, 255, 255)等值赋给 color 属性,它
们都被正确地转换为QColoi•类型的值。而当赋值为0.5时抛出 TmitError异常,并且JS 示了一条
很详细的出错信息来说明color 属性能支持的所有值。最后看一个很酷的功能:

c .c onfigure_traits()

当使用 wxPython 作为后台界面库时,由 于 TraitsUI4.4.0中的一个错误,程序退出时会


A 导 致 进 程 崩 溃 。 请 读 者 将 本 书 提 供 的 scpy2\patches\to o lk it.p y 复 制 到
site-packages\traitsui\wx 目录下,覆盖原有的 toolkit.py 文件。

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

(83, 120, 255, 255)

^ FHir prop#»m>^

Cofcr: TO2(2SS)
Gchcel

点击弹出
颜色选择框

图7 - 1 自动生成的修改颜色屈性的对话框
Python科学计算 (第 2 版)

对 于 从 H a s T r a i t s 继/兵的对象,都可以调用 configure_traits()方法以快速广:生一个设置 Trait


属性的川户界面。在本例中,通过界面中的颜色输入框可以直接输入表示颜色的值,或者使用
按钮打开颜色选择对话框。关于界面方面的功能将在本章的后半部分详细介绍。

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 屈性的上述功能:

from traits.api import Delegate, HasTraits, I n s t a n c e , 工


nt, Str
Traits & T ra its u轻

class Parent ( HasTraits ):


# 初始化:l a s t _ n a m e 被初始化为7 h a n g '
last_name = Str( 'Zhang' ) O
T 松制作图形界面

class Child ( HasTraits ):


age = Int

# 验证:f a t h e r 厲性的值必须是P a r e n t 类的实例


father = Instance( Parent ) ©

# 代理:将 C h i l d 类的实例的l a s t _ n a m e 屈性代理给其f a t h e r 屈性的last_name


last_name = D e l e g a t e ( 'father' ) ©

# 监听:当a g e 属性的值被修改时,下面的函数将被运行
def _age_changed ( self, old, new ): O

print 'Age changed from %s to %s ' % ( old, new )

p = P a r e n t ()

c = Child()

程序中定义了 Parent和 C hild 这两个从 HasTraits继承的类,并分别创建它们的对象p 和 c 。


O 用 Str 类型定义 Parent对象的 last_name 属性是一个字符串,并且它的默认值为'Zhang’

3

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

AttributeError Traceback (most recent call last)


<ipython-input-10-fff30c984flb> in <module>()

----> 1 c.last— name

AttributeError: 'N o n e T y p e ' object has no attribute 'last 一name'

设置了对象 c 的 father 屈性之后,就可以正确获収它的 l a s t _ n a m e 屈性了,并且对象 c 和 p


的 l a s t _ n a m e 属性将始终保持一致:

Traits & T ra its u轻


c.father = p
print 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

Age changed from 0 to 4

下面调用 configure_tmits 〇以显示一个修改属性值的对话框,如 图 7-2( 左)所示:

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

如果在编批 Fathei •对象的对话框111修 改 l a s t _ n a m e 屈性,C h i l d 对象的 l a s t _ n a m e 屈 〔


也同吋
被修改,这是因为 C h i l d 对 象 的 l a s t _ n a m e 属性楚一个代理属性,其值和 father.last_name 始终保
持一致。
还可以调用 p r i n U m i t s O 以输出所有的Trait 属性名和属性值:

c.print_traits()
age: 4
father: < _ main_ .Parent object at 0x05D9CC90>
last_name: 'ZHANG'

或调用 get〇以得到一个描述对象所有Trait 属性的字典:

c.get()
{'age1: 4j 'father': <__main_.Parent at 0x5d9cc90>, 'last^ame': 'ZHANG'}

此外还可以调用 set〇以设® T m i t 属性的值,用 set〇可以同时配當多个 Trait 属性:


Traits & T ra its u轻

c.set(age = 6)
Age changed from 4 to 6
<__main_ .Child at 0x5d9c600>

在创建 H a s T m i t s 的派生类的对象时可以使用关键字参数设置各个T m i t 属性的值,例如 :


T 松制作图形界面

c2 = Child(father=p, age=3)
Age changed from 0 to 3

当在派生类中定义了」 n i t _ ( ) 时,在 其 中 必 须 调 用 其 父 类 的 方 法 ,否 则 Trait 属性


的-•些功能将无效。也许读者会对 T m i t 屈性的工作原现感兴趣,下面简单地介绍这些功能是如
何实现的。
首先,
Trait 属性本身和普通 P y t h o n 对象的属性是一样的。
但是每个 Trait 属性都柯一个 CTrait
对象与之对应,这 个 C T r a i t 对象为 Trait 属性提供了许多额外的功能。可以通过 trait〇属性名’
)获
得与某个属性相对应的 C T r a i t 对象,或者j|j traits()获得包含所W C T r a i t 对象的字典。下面的语
句获得 a g e 属性对应的 C T m i t 对象:

c.trait("age")
<tnaits.traits.CTnait at 0x9e23870>

Trait 属性的默认值保稃在勹其对应的C T n d t 对象中:

p.trait("last_name").default
'Zhang'

给 Trait 属性赋值时的验证工作由C T m i t 对象的 validate()完成。当验证失败吋抛出异常,验


证成功吋则返回所要赋的值。因 此 validate()还可以用于修改所赋的值。下而直接调用 father 属
性所对应的 C T m i t 对象的 validate():
3

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)

< _ main_ .Parent at 0x5d9cc90>

当 T r a i t 属 性 的 值 被 改 变 时 , H asT raits 对 象 的 trait_property_changed()会 被 调 用 ,


trait_property_changed〇在 HasTraits的父类 CHasTmits 中定义。在此方法中将会调用用户定义的属
性监听函数。
注怠只调用监听函数,并不会修改属性的值,因此下而的语句将调用_age_Changed〇,
但不会修改 a g e 属性的值:

Traits & T ra its u轻


c .t r a i t j 3 r o p erty_changed("age", 8 , 10 )
c. age # a g e 屈性的值没有发生变化

Age changed from 8 to 10

T 松制作图形界面
CTrait 对象楚迩接 Trait 属性和 Trait 类别的纽带,通 过 CTm it 对象的 trait_type 属性可以获得
定义 Trait 属性时所使用的 Trait类型:

print c . t r a i t (" a ge") .trait— type

print c .t r a i t ("father").trait_type

<traits.trait_types.Int object at 0x09DC0490>


<traits.trait 一types.Instance object at 0x09DC0830>

7.1.3 T rait类型对象

在程序中使用 Trait 属性需要按照下面三个步骤进行:


(1) 从 traits.a p i 中载入所需的类型。
(2) 创 建 Tm it 类型对象。
(3) 创建一个从 HasTmits类继承的新类,
在其中使用所创建的Trait类型对象记义 Tm it 属性。
通常步骤(2)和(3)是放在一起的,也就是说创建 Trait类型对象的同时定义Tm it 属性,本书
的大部分例子都是釆用这种方式。例如:

from traits.api import Float, Int, HasTraits

class P e r s on(HasTraits):

age = Int(30)
weight = Float

99
Python科学计算 (第 2 版)

上面的程序为 Person 类定义了两个 Trait 屈性:ag e 和 weight。其1丨1a g e 屈性.使用Int 类型对


象定义,3 0 为默认值。而 weight 属性则直接使j|j Float 类型定义,实际上它也会创連一个Float
类型对象,苏具体的实现在 HasTraits类的内部进行。在上一节我们介绍过,每 个 T rait 属性都
对应一个 CTm it 对象,而通过 CTrait 对象的 trait_type 属性可以获得 Trait 类型对象。实际上,这
些 CTmit 对象和 Trait类型对象都是在类中保存的,
因此对于同一个HasTmits派生类的多个实例,
它们的某个 Trait 属性所对应的 CTrait 对象都楚同一个对象。下面创建两个 Person 类的实例,并
分别查看它们的 Trait 属性所对应的 CTm it 对 象 和 Trait 类型对象,可以看出多个实例之间共享
CTm it 对象的 Trait 类型对象:

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 属性:

from traits.api import HasTraits, Range


T 松制作图形界面

coefficient = Range(-1.0, 1.0, 0.0)

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)

上述程序中,Quadratic类有多个类型为 R ange 的 Trait 屈性,并且取值范围都是-1.0到 1.0,


初始值为0.0。为了尽量重用代码,我们先创建了一个 Range 类型对象,然后使用它定义了三个
Trait 屈性。为了比较,在 Quadmtic2 类中定义 Trait 属性.11、
丨创建Range 类型对象。
Quadratic对象的三个属性所对应的类型对象都楚coefficient:

q = Q u a d r a t i c ()

print coefficient is q . t r a i t ("c 0 ") .trait_type


print coefficient is q.trait("cl").trait_typG

True
True
而 Quadratic2 对象的屈性.所对应的类型对象则楚不同的对象:

q2 = Quadratic2()

q 2 .trait("c 0 ") .trait_type is q 2 .trait("cl").trait_type

False

7.1.4 T rait的元数据

Trait 类型可以拥有元数据屈性,这些屈性保存在与 Trait 屈性对应的 C T m i t 对象中。下面以

一个实例解释什么是元数据属性。

from traits.api import HasTraits, Int, Str, Array, List

class M e t a d a t a T e s t ( H a s T r a i t s ) :

i = Int(99, myinfo="test my info") O


s = Str("test", label=u" 字符串" ) ©
# N u m P y 的数组

Traits & T ra its u轻


a = Array ©

# 元素为I n t 的列表
list = List(Int) O

T 松制作图形界面
test = MetadataTest()

下面杳看所有的 Tm it 属性:

test.traits()

{'a': <traits.traits.CTrait at 0x9e4fbe0>,

'i': <traits.traits.CTrait at 0x9e4f9d0>,

'list': <traits.traits.CTrait at 0x9e4fb30>,

's': <tnaits.traits.CTrait at 0x9e4fa80>^

•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 屈性,

例如:test.trait('i')描述 test.i, s')描述 test.s。 test 对象有两个额外的 CTrait


test.frait(’ 对象:trait_added
和 trait_modified , 它们在 HasTraits 类中定义。
每 个 T m i t 屈性都有一个与之对应的 C T m i t 对象用于描述它。而所谓元数据屈性就是描述
T m i t 属性的属性,它们保存在 C T r a i t 对象中。元数据属性可以分为三类:

• 内 部 属 性 :这些属性是 C T m i t 对象自带的,只读不能写。
•识 别 属 性 :这些属性可以自由设置,它们可以改变 T m i t 属性的一些行为。
•用 户 属 性 :用户自己添加的属性,需要自己编写程序来使用它们。
下而是一些内部元数据属性,可以读取它们的值,但不能修改:
Python科学计算 (第 2 版)

• airay : 是否是数组,不是数组的 Tm it 屈性没有此屈性。


• default: Trait 属性的默认值。
• default_kind: —个描述默认值的类型的字符串,值可以是nvaluen、nlistn、ndictn、"self '、
"factory”
、"method"等。
• tmit_type: 定义 Trait 属性时所使用的 Trait 类型对象。
• inner_tmits: 内部的 CTm it 对象,在 List、D ie t 中使用,用来描述 L ist 和 D iet 的内部元素。
• type: Trait 属性的分类,可以是"constant"、"delegate"、"event"、"property"、"trait"。
下而的元数椐属性不是预定义的,但是可以被 HasTmits对象使用:
• cksc : 描 述 T rait 属性用的字符串,在生成界面时中使用它作为所创建的编辑器的帮助
信息。
• editor: 指定在界面中编辑Tm it 属性时所使用的编辑器类型。
• label: 界面中的 Tm it 屈性编辑器的标签字符串。
• rich_compare: 指定判断 Trait 属性值发生变化的方式。默认值为 True ,表示按值比较,
False 表示按照对象地址比较。
Traits & T ra its u轻

• trait_value: 指定 Trait 属性是西接受 TraitValue 类的对象,默认值为 False。当为 T rue 时,


将 Trait 属性设置为 TraitValueO , 重置 Trait 属性为默认值。
• transient: 指定当对象被保存(持久化)时是否保存此T rait 属性值,当此厲性不芬在时使
用默认值 Tm e 。
T 松制作图形界面

下而查看前而示例中test对象的各个 Trait 属性的元数据属性:


O 在创建 im 类型对象时,
设置其默认值为99,并设置一个名为m yinfo 的用户元数据属性。
这些信息都保存在与属性i 对应的 CTrait 对象中:

print t e s t . t r a i t ("i ").default


print t e s t . t r a i t (Mi ").myinfo
print t e s t . t r a i t ( " i " ) .trait_type

99

test my info

<traits.trait_types.Int object at 0x05DA4F50>

© 属 性 s 的默认值为’

test' 并且它有一个识别元数据属性label。在生成界面时,使用它作
为编辑器的标签。
为了在界而中使用中文,
需要使用 1 )11丨〇)也字符$。
如果运行 teSt.C〇
nfigw i _ tmits()

来显示图形界而,可以看到该厲性对应的标签文本为“字符串”。

print test.trait("s").label

字符串

©array ji j 于定义 N um Py 数纟11类型的 Trait 属性,因此属性 a 的元数据属性 array 为 True :

test.trait("a").array

True
〇屈 性 list 是一个元素类型为整数的列表。通 过 inner_traits元数据屈性可以获得与列表元素
对应的 CTrait 属性:

print test.trait("list")
print test.trait("list").trait 一type

print test .trait ("list") .inner_traits # list 屈性的内部元素所对应的 CTrait 对象


print test.trait("list").inner_traits[0].trait_type # 内部元素所对应的 Trait 类型对象

<traits.traits.CTnait object at 0x09E4FB30>


<traits.trait_types.List object at 0x05DA46D0>
(<traits.traits.CTrait object at 0x09E 4 F C 38 > > )

<traits.trait_types.Int object at 0x05DA4E50>

72 Trait 类型

Traits & T ra its u轻


DVD

本节介绍 Traits 库中提供的各种 Traits 类型以及如M 监听 Tmits 属性值的变化。

T 松制作图形界面
7 . 2 . 1 预定义的 T rait类型

Traits 库 为 Python 的许多数据类型提供了预定义的Trait 类型。对 于 Python 的每个简单数据


类型都有两种 Tm it 类型与之对应,见 表 7-1:
• 强 制 Trait 类型:当强制类型的 Trait 属性被赋值为类型不匹配的数据时,会抛出异常。
• 自 动 Trait 类型:类型不匹配时会自动调用此类型对应的转换闲数进行类型转换。

表 7-1 预定义的Trait类型

强制类型 自动类型 内置默认值 自动转换函数


Bool CBool False bool〇
Complex CComplex 〇
+〇i complex()
Float CFloat 0.0 float。
Int CInt 0 int()
Long CLong 0L int()
Sir CSlr it slr()
Unicode CUnicode u" unicode()

下面的例子比较这两种类
Python科学计算 (第 2 版)

from traits.api import HasTraits, CFloat, Float, TraitEnror

class P e r s on(HasTraits):
cweight = C F l o a t (50.0)

weight = F l o a t (50.0)

程序中用[^动 Trait 类 型 CFloat 定义了一个 cweight 厲性,它可以接收能转换为数值的字符


串"90”
,而 weight 属性则使用强制 Tm it 类 型 Float 定义,将它赋值为"90"则会抛出异常:

p = P e r s o n ()
p.cweight = "90"

print p.cweight
try:
p •weight = "90"

except TraitError as ex:


print ex
Traits & T ra its u轻

90.0
The 'weight' trait of a Person instance must be a float, but a value of '90' <type 's t r ' >

was specified.

除了简单类型以外,Tm its 库还定义了许多其他的常用数据类型。表 7-2列出了一些常用的


T 松制作图形界面

预定义 Trait 类型:

表 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])

Callable Callable( [value=None,


料metadata]) 可调用对象
Code ’,
Code( [value=…minlen=0, maxlen=sys.maxint, 某种编程语言的字符串
regex=”", **metadatal)
Color Coloit |*ai^s, **metadata|) 界面库中所采用的颜色对象
CSet CSet( [tmit=None, valuc=None, items=True, 自动转换类型的集合对象
**metadata])
Constant Constaiit( valuc*[, ***mctadata]) 常量对象,K 值不能改变,必须指定初
始值
4

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]) 触发事件用的对象

Traits & T ra its u轻


Expression , ^^metadata])
Exprcssion( [valuc="0M Python的丧达式对象
File File( [viilue="", filtei-None, auto_set=Fiilse, 表示文件路径的字符串
entries=10, exists=Fiilse, **metadata ] )
Font Font( [*args, **metadata]) 界而库屮表示字体的对象

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

The 'count' 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.

05
Python科学计算(第2 版)

如果希望候选值是动态的,可以用 values 参数指定候选值所对应的屈性名:

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

在上面的 Items类中,先 用 L ist 定义了一个列表类型的co u n tjist 屈性,并为其指定了默认


值。然后在用 Enum 定义枚举类型的屈牲时,用 values 参数指定枚举屈性的候选值为countjist
属性中的元素。

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 松制作图形界面

在上面的例子中,因为5 不 在 cou n tjist 属性中,第一次将 count属性赋值为5 时抛出异常。


当将5 添加进 co u n tjist 属性中之后,就可以将 count属性设置为5 了。

7.2.2 Property 属性

在标准的 Python 语法中可以使用 property()为类创建 Properly 属性。Property属性的用法和


一般属性相同,但是在获取它的值或者给它赋值时会调用相应的方法。在 Traits 库中也提供了
类似的功能,但是用法比标准 Python 更简洁。我们先看一个例子:

from traits.api import HasTraits, Float, Property, cached_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)

# a r e a 是一个属性,当w i d t h 、h e i g h t 的值变化时,它对应的_ g e t _ a r e a 函数将被调用


area = Property(depends_on=['width', 'height']) O

# 通过c a c h e d _ p r o p e r t y 修饰器缓存_ g e t _ a r e a ()的输出


^cached 一property ©
def _ g e t _ a r e a ( s e l f ) : ©
" a r e a 的g e t 函数,注意此函数名和对应的P r o e r t y 名的关系"
print 'recalculating'
return self.width * self.height

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

通 过 depends_o n 系统可以跟踪 area 属性的状态,判断足llT需要调用


_get_area()以重新计算 area 属性的值。注意在运行 r.width=10之后,并没有立即调用_get_area(),
而只是保存一个需要重新计兑的标志,等到真正需要 area 的值吋,^ get_ area〇才会被调用。
下 llL^ j % qtconsole 启动一个 QtConsole,并在其中连续调用两次edit_traits(),弹出图7-3所示
的两个编辑界面:
s !i @ n
v # q ir
IFython QtCon^olv
0«C !■ 1 2 :2 m ) v .l
T>-p« - c〇
t7 >■ rl|n
i g nt*f , •" c re it!** or • lic t n x * * for « 〇
r e d its r« in fo A ru MOO
l^^rttton l . t •••• An •onanctd
•onanci fn ttr « < tiv « I
> •> Irrtroductloo •〇〇 〇vinri*« o f lP>xhcn't f t
%〇
ulCkrt# •> (\ilck VMtt 2«q
•> PyXf*〇n'% Wlp tytXtm.
e ejec t? *M ut •〇 « > » • • ut» 付 0良 Clnc«l
•> a iftowt xr^t use

XP>tnon Kcip>〇 Mk2 ^ 3 FcTit uporiiV%




, 〇

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

阁7 - 3 修改两个对iS框屮的Height或 Width M 性会巫新计兑Area,弁同时更新对iS 框屮的显示

407
Python科学计算(第2 版)

修改任意 个界面中的 width或 heightjS性,


一 在输入数值的N 时,
两个界丨II]中的 Area、Height
和 Width 等各个文本框同时更新,每次键盟按键都会调jjj_5et_area()。此 时 在 IPython窗 U 中直
接修改 width的值,也会调用_get_area〇。
当打幵界面之后,界面对象开始监听对象r 的各个属性,因此在修改 r.width之后,系统设
置 r.arca的标志为需要重新计算,然后发现 r.area的值有对象在监听,冈此直接调用„get_area〇
更新其值,并且通知所有的监听对象,冈此界面就一齐更新了。每个界面都会在 Trait属性所对
应 的 CTmit 对象中添加监听对象:

t = r.trait("area") # 获得与area 屈性对应的CTrait对象


t._notifiers(True) #_notifiers 方法返丨"丨所有的通知对象,当aera 屈性改变时,这里对象将被通知
[<traits.trait_notifiers.FastUITraitChangeNotifyWrapper at 0x8b9e3f0>j
<traits.trait_notifiens.FastUITnaitChangeNotifyWrappen at 0x8bd4el0>]

由于我们弹出了两个界面,因此有两个需要通知的对象。如果再运行一次 r.ediUraitsO, 这
个列表将有 3 个元素。
Traits & T ra its u轻

7.2.3 Trait属性监听

HasTmits 对象的所有 Trait属性都自动支持监听功能。当某个 Trait属性的值发生变化时,


HasTmits对象会通知所有监听此屈性的函数。监听函数分为静态和动态两种,下面的程序演示
T 松制作图形界面

了这两种监听方式:

from traits.api import HasTraits., Str, Int

class Child ( HasTraits ):


name = Str
age = Int
doing = Str

def — str一 (self):


return n%s<%x>" % (self.name, id(self))

# 当age 属性的值被修改时,下面的函数将被运行
def _age一changed ( self, old, new ): O
print "%s.age changed: form %s to %s" % (self, old, new)

def _anytrait一changed(self, name, old, new): ©


print "anytrait changed: %s.%s from %s to %s" % (self^ name, old, new)

def log_trait_changed(obj, name, old, new): ©


print "log: %s.%s changed from %s to %s" % (obj, name, old, new)
h = Child(name = "HaiYue", age=9)
k = Child(name = "KaiWen", age=2)

h.on_trait_change(log_trait_changed, name="doing") O

anytrait changed: <8b823f0>.age from 0 to 9


<8b823f0>.age changed: form 0 to 9
anytrait changed: HaiYue<8b823f0>.name from to HaiYue
anytrait changed: <8b823c0>.age from 0 to 2

<8b823c0>.age changed: form 0 to 2


anytrait changed: KaiWen<8b823c0>.name from to KaiWen

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 的属性:

Traits & T ra its u轻


h.age = 10

h.doing = "sleeping"

k.doing = "playing"

anytrait changed: HaiYue<8b823f0>.age from 9 to 10

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)

_ a g e _ c h a n g e d (self, old, new)


_ a g e _ c h a n g e d (self, name, old, new)

而动态监听函数的参数有如下儿种形式:

observer。

observer^ new)
observen(name, new)
observer(obj, name, new)

observer(obj, name, old, new)

其 中 o b j 是拥有 Tm it 属性的对象,nam e 为值发生变化的属性名,o ld 为改变之前的值, new


为改变之后的值。
当多个 Trait 属性都需要使用同一个监听函数时,可以使用@ on_trait_change 装饰器:

09
Python科学计算(第2 版)

@on 一
trait 一c h a n g e ( names )
def any_method_name( s e l f , … ):
• • •

当 names 所描述的 Tm it 屈性改变时,将调用 any_melhod_name〇, names 足一个字符审或列


表,它能够很灵活地描述一组Trait 属性。下而列举了一些常用的属性匹配语法,当与之匹配的
屈性发生变化时将调用被修饰的监听函数:
•用逗号隔开多个属性名:’
foo ,
bar',当 self.fo o 或 self.bar 改变时。
• 片例表描述多个属性名:[’
fooVbar’
j ,功能同上。
•描 述 嵌 套 的 属 性 名 :’
foo .bar’
,当 self.foo .bar 或 self.fo o 改变时。
•描 述 嵌 套 的 属 性 名 :’
foo :bar',当 self.foo .bar 改变时。
•列 表 属 性 :Too[]’
,self.fo o 是一个列表,当它本身或它的元素改变时。
•指定属性名开头的字符串:Too+’
,当以 fo o 开头的属性改变时。
•指 定 元 数 掘 :’
+ f〇
〇’,当有名为 fo o 的元数据的属性改变时。
完整的匹配方法请参考Tmits 库的用户手册。下面的程序演示了上述匹配语法:
Traits & T ra its u轻

from traits.api import HasTraits, Str, Int, Instance, List, on_trait_change

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

@on_trait 一change( "x,y, inner • [x,y] ,test+,+monitor, alist []_•)


def event(self, obj, name, old, n e w ) :
print obj, name, old, new
下面是各利1屈性值改变时的运行结果:

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 ]匹配

d.inner = Inner(name="inner2") # 与 inner. [x_>y]匹配

d.testl = "ok" #与 t est +匹配


d.test 2 = "hello" #与t e s t +匹配
d •z = 30 # 与+monitor 匹配

d.alist = [3] # 与 a l i s t []匹配


d •alist •extend ([4,5 ]) #与 alist □匹配
d.alist[ 2 ] = 1 0 # 与 a l i s t []匹配

<Demo demo> x 0 10

<Demo demo> y 0 20

Traits & T ra its u轻


<Inner innerl> x 0 1

<Inner innerl> y 0 2

<Demo d e m o > inner <Inner i n n e r l x l n n e r innen 2 >


<Demo demo> testl ok

<Demo demo> test2 hello

T 松制作图形界面
<Demo demo> z 0 30

<Demo demo> alist [] [3]


<Demo demo> alist_items [] [4, 5]

<Demo demo> alist— items [5] [10]

7.2.4 Event 和 Button 属性

Event 和 Button 是两个专门用以处理事件的Trait 类型,Button 从 Event 继承,它除了 Event


的事件触发功能之外,还可以通过 TraitsU I 库自动生成界面中的按钮控件。E v e n t 屈性和其他
Trait 属性相比有如下区别:
• 对 E vent 属性赋值将触发与其绑定的属性监听事件,而通常的 T rait 属性只有在其值改
变时才触发事件。
• E v e n t 属性不存储值,因此对其赋值只是起到触发事件的作用,而所赋的值将被忽略。
试图获取 Event 厲性值将抛出异常。由于 Event 属性所触发的事件不表示某个属性值的
变化,因此它们所对应的静态监听函数名为_event_fire d 而不是_event_changed。下而是
使 用 Event 属性的例子:

from traits.api import HasTraits, Float, Event, on_trait_change

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 pos 一changed(self): ©


self.updated = True

def _updated_fired(self): €)

self.redraw()

def r e d r a w ( s el f ) : O

print "redraw at %s, %s" % (self.x, self.y)

® 在 P o i n t 类中定义 / x 、y 和 u p d a t e d 三个 Trait 屈性.。


© 使用@ o n _ t r a i t _ c h a n g e 对 p o s _ c h a n g e d ( )
方法进行修饰,当 x 或 y 属性被修改时 p o s _ c h a n g e d 〇会被调j+j。.其 中通过设置 u p d a t e d 属性触发
u p d a t e d 事件。© 布 u p d a t e d 的事件处理方法 _11[3<如 6 ( 1 _ 1 1 ^ ( ) 中调jjj r e d r a w ( ) 以重新绘制。下面楚修

改各种属性时的事件触发情况:
Traits & T ra its u轻

p = P o i n t ()
p.x = 1

p.y = 1

p.x = 1 # 由于x 的值已经为1 , 因此不触发事件


T 松制作图形界面

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属性

前面介绍的都是在类的定义中声明 T r a it 属性,以及在类的对象中使用 T m it 属性。由于


P y th o n 是动态语言,因此 T r a i t s 库也提供了直接为某个特定的对象添加T r a i t 屈性的方法。
在下面的例子中,直接生成一个 H a s T m its 类的实例 a ,然后调用 a d d _t r a i t 〇方法以动态地为
a 添加一个名为 x 的 T m it 屈性,其类型为 F l o a t , 初始值为3.0:

a = HasTraits()
a.add_trait("x", F l o a t (3.0))

a.x

3.0

接下来创建一个 HasTmits 类 的 实 例 b ,用 add_trait〇为 b 添加一个屈性 a ,指定其类型为


HasTraits类的实例。然后把实例 a 赋值给实例 b 的属性 a :
b = HasTraits()
b.add— trait("a", Instance(HasTraits))

b.a = a

然 后 为 实 例 b 添加一个类型为 D elegate 的 属 性 y ,在 b.y 和 b .a.x 之间建立代理连接。


modify= True 表示可以通过 b .y 修 改 b .a .x 的值。可以看到当将 b .y 的值改为10时,a .x 的值也同

时改变了。

from traits.api import Delegate

b.add_trait("y"> Delegate("a", "x", modify=True))


print b.y
b.y = 10

print a.x

3.0

10.0

实际上,通过赋值语句为 HasTmits对象添加新属性吋,这些属性都是 Trait 属性:

Traits & T ra its u轻


class A ( H a s T r a i t s ) :
pass

T 松制作图形界面
a = A()
a.x = 3
a.y = "string"
a . t r a i t s ()

{ ' t r a i ^ a d d e d ' : <traits.traits.CTrait at 0x3927c90>^


•trait 一m o d i f i e d ': <traits.traits.CTrait at 0x3927c38>^
'x': <traits.traits.CTrait at 0 x 3 9 2 7 f 5 0> >
'y': <traits.traits.CTrait at 0x3927f50>}

只不过它们的类型为Python, 因此它们能够接收任何类型的对象,起不到校验的作用:

a •trait (_’x " ). trait 一


type

<traits.tnait_types.Python at 0x39399b0>

7.3 TraitsUI 入门

与本节内容对应的 Notebook 为:07-traits/tmits-300~uiintro.ipynb。


D VD

Python 有着丰富的界面开发库,除了缺省安装的 Tkinter 以外,wxPython 、PyQt4 等都是非

常优秀的界面开发库。但是它们有如下共N 的问题:需要开发者掌M 众多的 A P I 函数,许多细


Python科学计算 (第 2 版)

节需要开发者自己配置,例如控件的屈性、位置以及雜件响应等。
在开发科学计算程序时,通常希望快速实现一个够用的界面,让用户能够交互式地处理数
掘 ,而又不希望在界而制作上花费过多的精力。以 Traits 库 为 _础 、以 M V C 模式为设计思想的
T r a i t s U I 库就是实现这一理想的最佳方案。

M V C 的英文全称为 Model-View -Controller,它的 FI的是实现一种动态的程序设计,简化程

序的修改和扩展工作,并且使程序的各个部分能够充分被重复利用。
• Model(模型):程序中存储数据以及对数据进行处理的部分。
• View (视图):程序的界面部分,实现数据的显示。
• Controller(控制器):起到视图和模型之间的组织作用,控制程序的流程,例如将界面操
作转换为对模型的处理。

7 . 3 . 1 默认界面

T r a i t s U I 库是一套建立在 Traits 库基础之上的用广界面库。它 和 Traits 库紧密相连,如果读


Traits & T ra its u轻

者已经设计好了一个从H a s T r a i t s 继承的类,那么直接调用 configure_traits〇方法,系统将会使用


TmteUI 库丨'-|动生成对话框界而,以供用户交互式地修改对象的T r a i t 属性。下而是一个简单的
例子:
T

from traits.api import HasTraits, Str, Int


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 ______________________________

Department: Depart ment:

Name: Name:

Salary; 0 Salary: hello|

OK Cancel OK Cancel

阁7 4 白动生成的Employee 类的对话框(左),提醒非法的输入数据并且使O K 按钮无效(右)

没有写一行界而相关的代码,就能得到一个够实用的界而,应该还是很令人满意的。为了
手工控制界面的设计和布局,就需要添加自己的代码了。

7 . 3 . 2 用 V iew 定义界面

HasTraits 的 派 生 类 Trait 属性保存数据,它相当于 M V C 模式中的模型。当没有指定界面


显示方式时,T raits 库会自动创建一个默认的界面。可以通过视图对象为模型设计更加实用的
界面。

Traits & T ra its u轻


1.外部视图和内部视图

下面是用视图对象定义界面的完整程序,图 7-5显示了界面截图。

T 松制作图形界面
柃礼与

妊各张m
1*5 10000

X iL 2000


妇7 - 5 使用视阁对象描述界而

from traits.api import HasTnaits, Str, Int

from traitsui.api import View, Item O

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 ( ©

Item('department', label=u" 部门" ,toolt i p = u" 在哪个部门十活" ), ©


ItemCname,
,label=u" 姓 名 ")

Item(.salary,
,label=u" 工资 _•),
Item( •bonus •, l a b e l = u __奖 金 ,
title=u"员 工 资 料 " , width=250, height=150, resizable=True O
Python科学计算 (第 2 版)

p = Em p l o y e e ()

p .c o n f i gure_traits()

此程序在模型类 Em ployee 的蕋础之上添加了界面®示相关的代码。


O 界而相关的内容都在
toitsui.a p i 中足义,这姐从其中载入 V ie w 和 Item。V ie w 是描述界面的视图类,Item 足描述界面
中的控件和模型对象的Trait 屈性之间关系的类。
© 在 Employee 类下创建一个 V ie w 对象,其 中 为 Employee 类的每个 Trait 屈性创建了对应
的 Item 对象。©创 建 多 个 Item 对象,并作为参数传递给View 〇。Item 对象是视图的蕋本组成单
位 ,每 个 Item 对象描述界面中的一个编辑器,这些编辑器j |j 于编辑模型对象中对应的 T rait 属
性的值。界面中的编辑器按照Item 对象传递给 View ()的先后顺序显示,而不丙按照 Traits 属性
名排序。Item 对象有许多参数,它们对 Item 对象的内容、表现以及行为进行描述。第一个参数
指定与编辑器对应的Tm it 属性名,label和 tooltip 参数设置编辑器的标签和提示文本。Item 对象
还有很多其他厲性,请读者参考 TraitsUI 的用户手册,或 在 IPylhon 中输入 Item??直接查看源代
T r a it s & T r a i t s u 轻

码。Item 类 从 HasTraits继承,因此它的厲性都是Trait 属性。


除了 Item 之外,TraitsUT还定义/ Item 的儿个派生类:Label、Heading 和 Spring。它们只用
于辅助界面布局,因此不需要和模型对象的Tm it 属性关联。
© V ie w 类也从 HasTraits继承,可以直接在创建 V ie w 对象吋,通过关键字参数设置其Trait
T 松制作图形界面

屈性.。title 屈性.为窗口标题栏中的文字,width 和 height屈性.为窗口的大小,resizable屈性.为True


表示窗口的大小可变。
同一个模型对象可以通过多个视图对象用不同的界面显示,下面看一个例子:

from traits.api import HasTraits^ Str, Int


from traitsui.api import View, Group, Item O

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

Group(*g2, label = u ’收入 show 一border = T r u e ),


title = u" 缺畨内部视图")
anothen_view = View( O

Group(*gl, label = u • 个 人 信 息 show_border = True),

Group(*g2, label = u • 收 入 show_border = True),


title = u " 另一个内部视图")

global 一
view = View( 0
Group(*gl, label = u • 个 人 信 息 show_border = True),
Group(*g2, label = u •收入、 show_border = True),

title = u" 外部视图")

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 属性是保存所有内部视图的字典:

Employee._ view_traits— .content.keys()

[•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 版)

S 铁省内部视图 ® 另一^内浮视图 S 外测图


个人信息 个人f言息 收入 个人丨言息 收入

部门:开发部 工 资 10000 部门:开发部


姓名:张开发 奖 金 2000 妗名:张开发

图7 ^ 6 使用外部视图和内部视图定义界面

edit_traits()和 configureJraits 〇—样用于生成界面,它们的区别在于 editJraits 〇显示界面之后

不进入后台界面库的消息循环,因此如果直接运行只调用 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 松制作图形界面

在本节的例子中,Em ployee 类用于保存数据,因此它属于 M V C 模式中的模型;而 View


对象定义了 E m p loyee 的界面鼠示部分,它属于视图。通过将视图对象传递给模型对象的
configure」raits()方法,将模型对象和视图对象联系起来。在定义编辑器的 Item 对象中,不直接

引用模型对象的属性,而是通过属性名与模型对象相关联。这样模型和视图之间的耦合性将会
很弱,只需要属性名匹配,同一个视图对象可以运用到不同的模型对象之上。
有时候我们希望模型类知道如何显示自己,这时可以在模型类内部定义视图。在模型类中
定义的视图可以被其派生类继承,因此派生类能使用父类的视图。在 调 用 configure_tmits 〇时如
果不设置 v^ w 参数,将使用模型对象内部的缺宵视图对象生成界面。如果在模型类中定义了多
个视图对象,则缺省使用名为 toits_V i e w 的视阁对象。

2.多模型视图

在上节的例子中,一个模型可以对应多个视图。同样,使用一个视阁可以将多个模型对象
的数据显示在一个界面窗口中。下面是用一个视图对象同时显示多个模型对象的例子,程序的
运行界面如图7-7所示:

from traits.api import HasTraits, Str, Int


from traitsui.api import View, Group, Item

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" 工资•■),

Traits & T ra its u轻


I t e m ( ’p 2 . b o n u s •, label=u__奖金••),
show 一border=True

orientation = 'h o r i z o n t a l '

T 松制作图形界面
)>
t i tle = u" 员工对比"

employeel = Employee (department = u"Jl:® n, name = salary = 3000, bonus = 300) ©


employee2 = Employee(department = u "销1丨
; name = u" 李网", salary = 4000, bonus = 400)

HasTraits().configure_traits(view=comp_view, context={"pl'_:employeel, "p2":employee2}) ©

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

© 接下来,创建组成模型对象的两个E m p l o y e e 对象:e m p l o y e e l 和 e m p l o y e e 〗。© 在显示界


面时,使 用 context 参数将包含两个模型对象的字典传递给configure_traits〇。通 过 context 参数传
递的实际上是视图所对应的模型。这里的模型对象是一个字典,它的键和 I t e m 对象的 object 属
性的值相冋。由于己经通过context 参数传递了模型对象,因此 configure_traits〇方法原本所属的
对象将不会被用作界面的模型对象。这 里 直 接 创 建 一 个 临 时 的 H a s T m i t s 对 象 ,然后调用
configure一traits()方法。

如果读者认为这种写法有些取巧,也可以直接调用视图对象的 ui()方法来显示界而,它的
参数就是界而所要显示的模型对象。由 于 ui()和 editjraitsO—样,不会启动界而库的消息循环,

因此需要在运行 ui()之后添加开始消息循环的代码。下而的消息循环代码支持所有的后台界
面库:

from pyface.api import GUI


comp— view.ui({_'pi” :
employeel, Mp 2 " :employee 2 })

GUI() . s t ar t _ e v e n t _ l o o p 〇 # 开始后台界而库的消息循环
Traits & T ra its u轻

3. Group 对象

在前面例子的视图定义中,我们通过 Group 对象将一组相关的 Item 对象组织在一起。本节


评•细介绍如何使用Group 对象组织界面。
在前面的两个例子中,将 V ie w 对 象 中 外 G roup 的 orientation屈性设置为’
horizontal',这样
T 松制作图形界面

内部的两个Group 对象将水〒方向排列。下面的代码显示了 V ie w 对象中 G roup 的嵌套关系:

View(
Group(

G r o u p (… )

G r o u p (… )

orientation = 'h o r i z o n t a l '

在创建 G r o u p 时,可以通过设置 orientation 和 l a y o u t 等属性,改 变 G r o u p 内容的呈现方式。


由于某些设置会经常用到,因 此 T m i t s U I 还提供了儿个 G r o u p 的派生类来覆盖这些厲性的缺省
值。例如下面是从 G r o u p 类继承的 H S p l i t 类的代码:

class HSplit ( Group ) :


• • • • • «

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 traits.api import HasTraits, Str, Int

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

first— name = Str

last_name = Str

department = Str

employee_number = Str

salary = Int

bonus = Int

Traits & T ra its u轻


items 1 = [I t e m (name = 'employee 一n u m b e r •, label=u' 编 号 ' ),
rtem(name = 'department', label=u" 部 门 " , tooltip=u" 在 哪 个 部 门 干 活 ••),

Item(name = ’l a s t j i a m e ’, label=u__姓 ••),

Iten^names'firstjiame'labehirg")]

T 松制作图形界面
items2 = [Item(name = 'salary' , label=u" 工 资 " ),

Item(name = 'bonus’, label=u__奖 金 ")]

viewl = View(

G r o u p (*items1, label u •个 人 信 息 、 show_border = True),

Group(*items2, label u •收 入 、 show 一border = True),

title = u" 标 签 页 方 式 "

resizable = True

view2 = View(

VGroup(

VGrid(*itemsl, label = u •个 人 信 息 show_border = True, scrollable = True)

VGroup(*items2, label = u •收 入 ', show_border = True),

resizable = True, width = 400, height = 250, title = u"IB 直 分 组 "

view3 = View(

HSplit(
Python科学计算(第 2 版)

VGroup(*itemslj show_border = True, scrollable = True),


VGroup(*items2, show_bonder = True, scrollable = True),

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

resizable = True, width = 200, height = 300, title = •直 分 组 (带 调 节 栏 )

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对 象 进 行 分 组

表 7-3是 G roup 的各种派生类及对应属性的缺省设置:


/ ^

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

V 1 xil\ column 2 按照多列的网格 〇


1 n
网格的列数
V t W、layout: \! 垂直排列,中间

XT. it、show」 ;
ls 、colum ns 等属性之夕卜,

轻松制作图形界面
其介 丨属1 〇 n Item —样,读者可以在 Group 类的涡
细 ij 1〇
:而: dsible_when 和 enabled_when 属性的用

对1 :界j 5有效,效果如图7-9所示。

It 也提供了 hen 和 enabled_when 属 性 ,用法和 Grc

frc :raits.api import F rraits, Int, Bool, Enum, Property


frc zraitsui.api imporl iew, HGroup, VGroup, Item

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

在上述程序中,S h ap e 是一个表示矩形或圆形的类,具体形状由 shape_t y p e M 性决定,而


图形的参数则由x 、y 、w 、h 、r 等属性决定。editable属性决定是否能通过用户界面修改图形参
数。在视图定义中,使 用 V G m u p 对象定义了两个编辑器组,分别编辑矩形参数和圆形参数。
通过设置 V G roup 的 visible_when 和 enabled_when 屈性:,将模型对象的 shape_type 和 editable屈性.
与编辑器的界面显示联系起来。
visible_ when 和 enabled_ when 属性都是表示布尔表达式的字符$。当布尔表达式中涉及的模
型对象的属性发生变化时,将会对字符弟求值,并根据求值结果更新界而的显示。图 7-9是程
序的显示效果,其屮左图对应的 shape_type 屈性为"rectangle",editable屈性为 True 。当 shape_type
屈性为nrectangle"H、
j ,将 M 示矩形参数的编辑器,隐藏圆形参数的编辑器。当 editable 屈性.为False
Traits & T ra its u轻

时,所有编辑器都变成无效。

^ Edit propert.. cu || S ~1 ^ Edit propert- a || © ||^&| ^ Edit propert.. 1 cz> |~B~|

bhape type: circle ▼ tditable: 3 bhape type : c rc le hditable : L J


T 松制作图形界面

图7 - 9 演示visible_when和 enabled_when属性的用法

4.配置视图

前而介绍了如何使用Item 和 Group 等对象组织窗口界而中的内容,本小节介绍如何配置窗


口本身的屈性。通 过 kind 属性可以修改 V ie w 对象的显示类型(见表7~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整套按钮:

OKCancelButtons = [ OKButton, CancelButton ]


ModalButtons = [ ApplyButton, RevertButton) OKButton, CancelButton, HelpButton ]

LiveButtons = [ UndoButton, RevertButton, OKButton, CancelButton) HelpButton ]

from traitsui import menu


[btn.name for btn in menu.ModalButtons]

[ u ’A p p l y 、 u ' R e v e r t ’,u ' O K ’,u ' C a n c e l u ' H e l p ’]

7 . 4 用 Handler控制界面和模型

与本节内容对应的 Notebook 为:07-traits/traits~40Q>handler.ipynb

虽 然 TmitsUI 库的界而设计使用 M V C 模式,但是在前而的介绍中,没有出现过任何有关


控制器的代码,这是因为 TmitsUI库提供了缺省的控制器。我们可以通过继承Handler类来创建
囱己的控制器类,对视图和模型进行更自由的控制。
控制器最主要的功能是界面的事件处理,这些事件或对模型对象进行修改,或对界面进行
修改。模型、视图以及控制器都是单独的实体。一个视图对象可以为多个模型对象生成界面,
N 样,一个控制器也可以用来处理不N 视图界面中所产生的事件。因此控制器和视图以及模型
之间不存在静态的联系。但是为了让控制器能够真正起作用,它必须知道自己所要处理的视图
和模型。在 TraitsUI 中,控制器使用 U linfo 对象获得它所对应的视图和模型。
当 TmitsUI 从视图创建界圃 I寸,将创建一个U lln fo 对象,其中包拈界面和模型对象的引用。
Python 科学计算(第 2 版)

当界丨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 按钮。
下面通过一个实例演示上述各个方法的用法:

from traits.api import HasTraits, Str, Int


from traitsui.api import View, Item, Group, Handler

from tnaitsui.menu import ModalButtons

gl = [ I t e m ( ' d e p a r t m e n t l a b e l = u " 部门")



rtem(_name', label=u__姓名_•)]
g 2 = [Item(.salary ,
,label=u" 工资••)

r t e m ( ’bonus', label=u" 奖金" ) ]

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

print self, "department changed to ", self.department

def _ str_ (self): ©

return "<Employee at 0 x%x>" % id(self)

viewl = View(

Group(*gl, label = u •个 人 信 息 show 一border = T r u e ),


Group(*g2, label = u •收入•,show— border = True),
tit l e = u" 外部视图",

kind = "modal", ©
buttons = ModalButtons

class E m p l o y e e Ha n dler(Handler): O

Traits & T ra its u轻


def init(self, i n f o ) :
s u p e r (EmployeeHandler, self).init(info)

print "init called"

def init 一info(self, i n f o ) :

T 松制作图形界面
super( EmployeeHandler, self).init 一info(info)

print "init info called"

def position(self, i n f o ) :
super(EmployeeHandler, s e l f ) .position(info)

print "position called"

def setattr(self, info, obj, name, v a l u e ) :


s u p e r (EmployeeHandler, s e l f ) .setattr(info^ obj, name, value)

print "setattr called:%s.%s=%s" % (obj, name, value)

def apply(self, i n f o ) :
supen(EmployeeHandler, self).apply(info)

print "apply called"

def close(self, info, i s _ o k ) :

s u p e r (EmployeeHandler, self).close(info, is 一ok)


print "close called: %s" % is_ok
peturn True

def closed(self, info, is 一


ok):
super(EmployeeHandler, s e l f ) .c l o s e d (i n f i s _ o k )
Python 科学计算(第 2 版)

print "closed called: %s" % is_ok

def revert (self ,i n f o ) :


super(EmployeeHandler, s e l f ) .revert(info)

print "revert called"

zhang = Employee(name=,,Zhang")
print "zhang is ", zhang
zhang.configure_traits(view=viewl, handler=EmployeeHandler() ) 0
zhang is < Employee at 0x91efcf0>

init info called


init called
position called

< Employee at 0x9622Bc0> department changed to 开发


setattr c a l l e d :〈 Employee at 0x96223c0>.department =开发

〈 Employee at 0x96223c0> department changed to 开发部门


Traits & T ra its u轻

setattr c a l l e d :〈 Employee at 0x96223c0>.departments 开发部门


〈 Employee at 0x91efcf0> department changed to 开发部门
apply called
close called: True
closed called: True
T 松制作图形界面

True

O 布 Employee 模型类中,定义了 department属性的事件处理方法_ department_changed()。©


覆盖标准的字符串转换方法_ sn_ 0 ,用以显示模型对象所占用的地址。
©为了显示对话框的标准按钮,在创建视图对象时设置 V ie w 的 kind 和 button 参数分别为

modal’
和 ModalButtcms。这样界而所显示的对话框是“模式窗口、非即时更新”,并且有 Apply 、
Revert、O K 、Cancel、H elp 等按钮,如图 7-10所示。

图 7 - 1 0 带标准按钮的模式对话框

©EmployeeHandler 从 H a n d l e r 继承,它搜盖了 Handler 中的 init、 init_info、 position 、setattr、

a p p l y 、close 、closed 、revert 等方法。如果 close 〇返丨Hi T r u e , 则窗口会被关闭;如果它返丨H| False ,


则不会关闭窗口。在这些方法中首先调用父类中被覆盖的方法,从而实现缺省的控制器的功能。
实 际 上 父 类 Handlei •中的大部分方法不执行任何任务,因此也可以不运行它们,请读者阅读
H a n d l e r 类的源代码以了解毎个方法的缺省功能。

© 最后创建控制器对象,并将它传逆给 configure_traits〇的 h a n d l e r 参数。


ijl

42
在对话框的“部门”文本输入框中输入“开发部门”,然后单fir? A p p ly 按钮,最后单击
0 K 按钮关闭对话框。程序在命令行窗口中输出控制器的各个方法的调叫情况。
首先,对 象 zhang 所表示对象的地址为Ox91efcfO:

zhang is <Employee at 0x91efcf0>

调用 configure_tmits()之后,在窗口显示之前,运行了 init_info〇、init〇、position()这三个方法:

init info called


init called
position called

接 下 来 输 入 “开发部门”,每次文本输入框内的内容发生改变时,都会修改模型对象的
depmtment属性,从 而 调 用 模 型 对 象 接 着 会 调 j |j 控 制 器 的 setattr〇。因
此控制器的 setattrO是在模型数据更新之后被调用的。

〈 Employee at 0x96223c0> department changed to 开发

Traits & T ra its u轻


setattr c a l l e d :〈 Employee at 0 x 9 6 2 2 3 c0>.department =开发
〈 Employee at 0x96223c0> department changed to 开发部门

setattr called :<Employee at 0x96223c0>.department =开发部门

setattrO的调用参数如下:

T 松制作图形界面
s e t a t t r (s e l f , info, obj, name, value)

其 中 i n f o 是 U l l n f o 对象,o b j 是被修改属性的模型对象,n a m e 是被修改的属性名,而 value


楚被修改之后的值。仔细比较前而输出的对象地址就会发现:被修改属性的模型对象的地址并
不是对象 z h a n g 的地址。这 是 因 为 “非即时更新”的对话框会对一个副本对象进行修改,在单
市 Apply或 0 K 按钮时,才会将副木的内容写回原对象;而对副木对象的修改是“即时更新”的。
当单it? A p p l y 按钮时,程序输出了以下内容,可以看到原对象的 d e p a r t m e n t 属性也被更新

为 “开发部门”了:

〈 Employee at 0x91efcf0> department changed to 幵发部门


apply called

最后单击 O K 按钮,程序输出下而两行,其中的 T m e 是 is_o k 参数的值,True 表示用户单


击的是0 K 按钮,如果单击 Cancel 按钮则为 False:

close called: True

closed called: True

7.4.2 Controller 和 Ullnfo 对象

Handler 类的每个芈件处理方法的第•个参数都是 U lln fo 对象,通过它可以获得控制器对


Python 科学计算(第 2 版)

应的模型对象和视图对象所产生的界面。但是有时我们希望通过控制器的屈性访问它们。
TraitsUI提供了从 Handler继承的 Controller类,它W 两 个 Trait 属性:model和 info ,分別保存模
型对象和 U lln fo 对象。
在下面的程序中,模型和视图采用上节的定义,为了在显示窗口之后,在 Notebook 中继续
运行命令,这 里 将 view Lkind 属性修改为非模式窗口。创建模型对象、控制器以及显示界面的
代码如下:

from traitsui.api import Controller

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 )

在创建 Controller控制器时把模型对象传递给它,就可以通过 c .model访问此模型对象。而


调 川 Controller对象的 edit_traits〇不会显示控制器本身的Trait 属性编辑窗 U ,而楚显示模型对象
Traits & T ra its u轻

的属性编辑窗口。
由于无论是控制器类、视图类还是模型类,最终都从 HasTmits类继承,因此可以调用get()
来快速查看其内容:
T 松制作图形界面

c.get()

{ ’_ipython 一display 一•:None,


'_repr_html_': None,
'_repr_javascript_': None,
,_ r e p r _ j p e g _ ,: None,
,_ r e p r _ J s o n _ ,: None,
'_repn_latex_': None,
’一r e p r _ p d f _ ' : None,
,_ r e p r _ p n g _ ,: None,
'_repr_svg_': None,
'info': <traitsui.ui_info.UIInfo at 0 x 5 6 1 4 8 1 0 〉

'model': < _ main— .Employee at 0x55b71e0>}

c.info.get()

{ 'initialized': True, 'ui': <traitsui.ui.UI at 0x55b7570>}

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

{'controller': <traitsui.handler.Controller at 0 x 5 6 1 4 3 f 0> <,

'handler': <traitsui.handler.Controller at 0 x 3 6 0 b 0 9 0> >

'object': < _ main 一 .Employee at 0x5b58030>}

ui.control # u i 对象所表示的实际界而控件

<traitsui.qt4.ui_base._StickyDialog at 0x5658780>

ui.view

( Group(

Traits & T ra its u轻


Item( 'department'
object = 'object 、
label = u •\u90e8\u95e8•,

• • •

T 松制作图形界面
ui._editors

[<traitsui.qt4.text_editor.SimpleEditor at 0x5abe480>^
<traitsui.qt4.text_editor.SimpleEditon at 0x5b00510>,

• • •

7 . 4 . 3 响应 Trait属性的事件

前面介绍过,从 H asTraits 继承的模型类中,可以通过定义_的丨恤11^_(:


11〇1职(1()来响应
traitname属性值改变的事件。这是在模型类中响应事件,如果要在控制器类中响应,可以通过
定 义 setattr()来响应模型对象的Trait 属性的改变。
如果希望只响应模型对象中某个特定属性的事件,可以在控制器类中定义如下格式的事件
响应方法:

extended_traitname_changed(self, info)

其中的 extended 是视阁所产生的 U I 对象的 context属性中勹模型对象相对应的键,通常为


'object'〇
这样的事件响应方法在界而窗口初始化时,以及对应的属性改变时都会被调用。为了 K 分
二者,可以使用 info 参数的 initialized屈性来判断。下面是一个例子:
Python 科学计算(第 2 版)

from traits.api import HasTraits, Bool


from traitsui.api import View, Handler

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

def object 一updated_changed(self, i n f o ) : ©


print "updated changed", "initialized=%s" % info.initialized
if info.initialized:
info.ui.title += "*M

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)

viewl = V i e w ( ' b l 、 'b2', 'b3 ,



T 松制作图形界面

h a n d l e r=MyHandler 〇,

title = "Test",
buttons = ['OK', 'Cancel'])

tc = TestClass()
t c .configure_traits(view=viewl)

setattr b 2

updated changed initialized=False

OMyHandler 类中定义了 setattr()方法,在修改了模型对象的任何一个Trait 属性之后,它都


将被调用。© 在 setattr〇中修改模型对象的updated属性为 True 。
€)当模型对象的u p d a t e d 屈性被修改时,
它在控制器对象中所对应的object_updated_changed()
将被调用。当用户通过界而上的单选框或者通过程序修改模型对象的属性时,将调用该方法,
在窗口的标题栏中添加一个。

7.5属餓辑器

与本节内容对应的 Notebook 为:OT-traits/traits-SOOeditors.ipynIx


每 个 Trait 类型都有一种缺省的界面编辑器(控件)与之对应,如果在视图对象中不指定编辑
器,将使用缺省的编辑器生成界面。每利1编辑器都可以有如下4 种样式:
• "simple”
:缺省值,使用一个比较简单的编辑器,尽量少占用界面空间。
• "custom”
:使用较复杂的编辑器,尽量呈现更多的内容。
• "text”
:使用一个文本编辑器。
• "readonly”
:使用只读控件显示。
由于 Tm itsUI 的编辑器利喽繁多,木书不能 一 一 洋细介绍,谐感兴趣的读者运行位于木书
附盘的 co d e s 目录下的演示程序,运行界面如图7-11所示。

taitsuidemo.demo: TmitsUI 官方提供的演示程序。

图 7-11 TmilsUI 演示程序的运行界而

下血以几个实例简单地介绍如何使用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提供的各种编辑器

在下而的 E d i t o r D e m o I t e m 类的视图定义中,使用 4 种样式为 i t e m 属性定义编辑器。请注意


在 E d i t o r D e m o I t e m 类的定义中并没有i t e m 属性,
但是由于视图中使用属性名字符串定义编辑器,
Traits & T ra its u轻

因此只有在真正使用视阁创建界而时,才会访问 i t e m 属性,这时己经通过 add_trait〇为其添加了


i t e m 屈性。
O l t e m 对象的 w i d t h 属性可以指定编輯器的宽度,以像素点为单位的长度用整数表示,
负数表示强制设置其宽度。w i d t h 属性还有多种设®宽度的用法,请读者查看 I t e m 类的源代码
中的注释。© 使用下划线字符串在界而中创建分隔线。
T 松制作图形界面

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", style="custom", label="custom"),

Item(__item", style=__text", label="text"),

rtem(__item_、 s t y l e = V e a d o n l y % label=__readonly"),

),

在表示主界而的 E d i t o r D e m o 类中,c o d e s 属性保存一组用来创建各种T r a i t 厲性的字符串,


selectedjtem 屈性是 E d i t o r D e m o I t e m 的对象。
在 E d i t o r D e m o 类的视图)义中,
使用 HSplit 将 c o d e s
和 s e l e c t e d j t e m s 所对应的编辑器水 H V r 开 。〇用 editoi• 参 数 设 置 c o d e s 屈性的编辑器为
ListStrEditoi•,它是一个姐不一组字符串的列表选择框控件。其 editable 屈 性 为 False , 表不列表

选择框中的字符串都是只读的。selected 屈性是保存被选中字符串的Trait 属性名,在 E d i t o r D e m o


类 中 用 s e l e c t e d _ c o d e 屈性保存列表选择框中被选中的字符串。可以通过 editor 参数设置 I t e m 对
象的编辑器,这样界面中将使用指定的编辑器显示Tm it 屈性。
©当用户通过列表选择框选中了某个字符串时,selected_c o d e 属性将发生变化,因此
_selected_code_changed()会被调 j U。
在该方法中创述一个 EditorDemoItem 对象,
并调 j|j 其 add_trait()
方法,动态地为其创建一个名为 item 的 Trait 属性,其类型则通过 eval()对 selected_cod e 字符串
进行求值获得。

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

Traits & T ra its u轻


resizable=True_»
width = 800,
height = 400,
tit l e = u " 各种编辑器演示"

T 松制作图形界面
)

def 一selected 一code 一changed ( s e l f ) :


item = EditorDemoItem(code=self.selected_code)
item.add 一
trait("item’
、 e v a l (s e l f .s e l e c ted_code)) ©
self.selected 一item = item

最后是定义各种 Tm it 类型的程序。可以在定义 Tm it 类型时,通 过 editor参数设置对应的编


辑器,这样就不需要在视阁的 Item 对象中定义了。请读者 A 行研究每个 Trait 类型的定义以及
它们所创建的界而控件,这里就不再进行详细说明了。

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

List(Str, demo 一list)


Range(l, 10, 5)
List(editor=SetEditor(values=demo_list))
List(demo_list, editor=ListStrEditor())

S tr ( " h e llo " )


Password("hello")

S tr ( " H e llo " , e d ito r = T itle E d ito r ( ) )


T u p le ( C o lo r ( " r e d " ) , R a n g e (l,4 ), S t r ( " h e l l o n) )
I n s t a n c e (EditorDemoItem, employee)
I n s t a n c e (EditorDemoItem., employee, e d i t or=ValueEditor())
Instance(time, time(), e d itor=TimeEditor())
ii it it
Traits & T ra its u轻

demo = EditorDemo()

demo.codes = [ s . s p l i t (••#•• ) [ 0 ] . s t r i p ( ) f o r s in t r a i t _ d e f i n e s . s p l i t ( " \ n " ) i f s . s t r i p ( ) !=■ •"]


d e m o .c o n f igune_traits()
T 松制作图形界面

7 . 5 . 2 对象编辑器

随着程序开发的进行,界面中的控件数F1会逐渐增多,功能会越来越复杂,这意味着与界
面对应的模型类也会变得复杂起来。为了便于代码的理解、管理以及重用,我们需要对模型类
及其对应的界而视图对象进行重构。将程序中重复使用、相对独立的部分作为组件分离出来,
单独为其设计模型类和视图对象,最终的应用程序由一系列这样的组件构成。这些组件可以在
程序的不同地方重复使用,从而起到功能分离、代码重用等多方面的作用。Tm itsUI 的 M V C 模
式非常适合这利1组件开发方式,下而让我们通过一些实例深入理解M V C 模式所带来的便利。
下面介绍的程序创建如图7-13所示的界面,用户可以通过上方的下拉选择框选择一种形状,
选择框下面的控件会自动根据所选的形状发生变化。当通过这些控件输入形状数据时,界面下
方的信息栏会£1动更新。由于程序较长,下面将它分为几个部分进行分析。
根 据 下拉选择框 创 建 不 同 的 编 辑 界 面

^ hdil pivprflic !t=> ©bJil l_〇 i=j

Center X;

aJycIciiuUi: 1.000000, 1.000000

图7 - 1 3 组件演示,根据下拉选择框创建不同的编辑界面
6
3
scpy2.traits.traitsui_component: TraitsUI 的组件演示程序

class Point(H a sT r a i t s ) :

x = Int
y = Int

view = V i G w ( H G r ou p ( I t e m ( ,,x " ) J Item("y")))

上面的程序定义了用于保存平面上点的坐标的P o i n t 类。
我们还为它指定了一个视图对象,
视图中 X 和 Y 轴的坐标值输入框是横向排列的。
运 行 Point 〇.configure_tmits 〇即可看到 P o i n t 对象
所创建的界面效果。
我们可以将 Point 类当作组件使用,将它嵌入更复杂的界而中。在下面的程序中定义了一
个基类 Shape 及其两个派生类 Triangle 和 Circle ,使 用 Point类定义所有表示二维坐标点的属性:

Traits & T ra its u轻


class S h a p e ( H a s T r a i t s ) :
info = Str O

def — init— (self, **traits):


super(Shape> s e l f ) .— init— (**traits)

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"),

)
)

@on— trait 一change("a.[x,y],b.[x,y],c.[x,y]")

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

self.info = "edge length: %f, %f, %f" % (11,12,13)

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轻

s e l f •info = "area:%f" % (pi*self.r** 2 )

O 在 S h a p e 类中定义 i n f o 属性,©在初始化方法中调用派生类的 set_info()以修改 i n f o 属性。


© 在丁1彳
〇1^ ^ 类中使用 Instance(Point, 〇)定义了表示三角形三个顶点坐标的属性:a 、b 和 c 。
在 C i r c l e 类中使用同样的方式定义了表示圆心坐标的 centei•属性,这些属性都是 P o i n t 对象。
T 松制作图形界面

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

select = Enum(*[cls._ name_ for els in Shape._ subclasses— ()]) O

shape = Instance(Shape) ©

view = V i e w (

VGroup(

Item( " s e le c t "),

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 — init— (self, **traits):


super(ShapeSelector, s e l f ) .— init 一 (**traits)
s e l f ._ s e l e c t _ c h a n g e d ()

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

〇下拉选择框所对应的 select 屈性为枚举类型,为了让程序显得更动化一些,这里不直


接指定枚举类型的候选值,而 是 通 过 S h a p e 的派生类名创建候选值列表。这样当添加其他的
S h a p e 派生类时,不需要修改这段代码。

© shape 属性的类型是 Shape,|±1于不需要创建缺雀的Shape 对象,因此不)|j 指 定 Instance


的第二个参数。© 当 select属性发生变化时,在事件处理方法_select_changed()中创建 select属性
所对应的类的实例,并 赋 值 给 shape 属性。© shape 属性对应的编辑器是"custom”
样式,冈此它
的编辑界而将作为组件嵌入ShapeSdectoi•的界而中。并且它能根据当前的 shape 属性值,动态
更新界而上的编辑器。也就是说,当 shape 属性是 Triangle 对象时将使用 Triangle 类的视图创建
编辑器,而 当 shape 属性是 Circle 对象时将使用 Circle 类的视图创建编辑器。
O 通 过 object.shape.in fo 可 以 为 shape 屈 性 的 info 屈性在界面中创建编辑器。当为某个 Trait
属性的属性创建编辑器时,注意需要在属性名之前添加“object.”。
读者也许会认为这种为屈性的屈性创建编辑器的做法有些混乱,一个比较简单的解决方法
就是从 ShapeSelector的视图中删除object.shape.in fo 的 Item 对象,并分別给 Triangle 和 C ircle 的视
图 添 加 显 示 in fo 属性的编辑器:Item(’

info",style="custom’
')。这种做法的缺点楚潘要给每个从
Shape 派生的类的视图添加 in fo 属性的编辑器,而当我们不想显示 in fo 属性时,代码的修改量
也会随着 Shape 的派生类的增加而增加。
还有一种使用多个视图对象的方法,
它充分体现了 M V C 模式将模型和视图完全分离的优点。

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 一info = View(Item("info", style="custom", show_label=False))


Python 科学计算(第 2 版)

def _ init_ (self, **traits):


super(Shape, self).— init— (**traits)
self.set一inf〇()

首 先 为 Shape 类添加一个 viewJnfo 视 图 专 门 用 于 示 其 info属性。这 样 Shape 的派生类


Triangle和 Circle都具有两个视图:view 和 viewjnfo。如果模型类有多个视图,将其嵌入其他视
图中时需要指走使用哪个视图创建编辑器。因此 ShapeSelector类的视图需要做如下修改:

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轻

width = 350, height = 300, resizable = True


)
T 松制作图形界面

〇为了和前面的例子有所区别,这里用•个垂直分隔容器将形状数据输入界面和显示形状
信息的控件分隔开。© 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 位置用不同的

视图,为同-个模型对象创建多个不 N 的编辑器,因此使用 TmitsUI创建的界面是非常灵活的。

7 . 5 . 3 自定义编辑器

Enthought的官方绘图库采用的是Chaco , 不过如果读者对 matplotlib更为熟悉,也可以在界


面中使用 matplotlib的绘图控件。为了实现这个冃的,需要自己编写一个 Trait 编辑器,用它包
装 matplotlib的绘图控件。
4
由 于 T m itsU I 俾 和 m atplotlib 都 支 持 w x 和 Q t 界 面 库 ,因此下面的程序首先根据
ETSConfig .toolkit选择载入 matplotlib 中对应的绘图控件FigureCanvas 和工具条 Toolbar,
并从 traits
对应的后台库中载入所有编辑器的父类Editor:

import matplotlib
from traits.api import Bool

from tnaitsui.api import toolkit


from t r a i t s u i .basic_editor_factory import BasicEditorFactory

from traits.etsconfig.api import ETSConfig

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

from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas


from matplotlib.backends.backend_wx import NavigationToolbar2Wx as Toolbar

T r a it s & T r a i t s u 轻
from traitsui.wx.editor import Editor

elif ETSConfig.toolkit == "qt4":

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

from pyface.qt import QtGui

对于每个界而库都需要编写从 Editor 类继承的包装 matplotlib 图表的编辑器类,下而是从


traitsui.qt4.editor.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 版)

if event.xdata is not None:


x, y = event.xdata, event.ydata

name = "Axes"

else:
x, y = event.x, event.y

name = "Figure"

panel.info.setText("%s: %g, %g" % (name, x, y))

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)

if hasattr(self.value, ,,c a n v a s _ e v e n t s " ) :


T r a it s & T r a i t s u 轻

for event_namej callback in self.value.canvas_events:

mpl 一c o n t r o l •m p l _ c o n n e c t (event 一n a m e , callback)

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

相当于t r a i t s . u i 中的E d i t o rFactory , 它返回真正创逑控件的类


it it it

if ETSConfig.toolkit == "wxM :
/ ^

44
klass = _WxFigureEditor

elif ETSConfig.toolkit == " q t 4 " :

klass = _QtFigureEditor O

toolbar = Bool(True) ©

〇类属性 klass 为编辑器类,这里根据当前的界而库选择编辑器类。© 最后定义工厂类中的


Traits 属 性 toolbar, 它的缺省值为 True 。
下而是使用 MPLFigurcEditor将 matplotlib的图表嵌入 TraitUI 界面中的例子:

import numpy as np

from matplotlib.figure import Figure

from scpy2.traits import MPLFigureEditor

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 (

Item( "figure"^ editor=MPLFigureEditor(toolbar=True), show 一label=False)

width = 400,

T 松制作图形界面
height = 300,

resizable = True)

def _ init_ (self, **kw):

super(SinWave, s e l f )•一init_ (**kw)

self.figure.canvas 一events = [

("b u t t o n j 3 ress_ e v e nt ", s e l f •figure— button— p r e s s e d )

]
axes = s e l f .f i g u r e .a d d _ s u b p lo t (1 1 1 )

t = np.linspace( 0 , 2 *np.pi, 200 )

axes.plot(np.sin(t))

def f i g u r e_button_pressed(self, e v e n t ) :

print event.xdata, event.ydata

model = SinWave()

m o d e l •edit 一t r a i t s ()
Python 科学计算(第 2 版)

7 . 6 函数曲线绘制工具

与本节内容对应的 Notebook 为:07-traits/traits-600~example.ipynb<

作为本章的最后一节,让我们用学到的内荞编写一个绘制函数丨|||线的小程序,界面如图
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 函数曲线绘制工具的界面

scpy2.traits.traitsui_function_jplotter: 采用 TraitsUI 编写的函数曲线绘制工具,

本函数仲线绘制工具有如下功能:
•在图表上单击鼠标左键添加点,单击鼠标右键删除最后添加的点。也可以通过表格添
加点或者修改点的坐标。
•在 运 行 代 码 之 前 ,将表格中的点转换为形状为(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〇

C o d e 对 应 的 编 辑 器 代 码 存 在 BUG, 请 读 者 将 patches\pygments_ highlighter.py 复制到

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

Item("points", editor=point_table一editor, show_label=False)


)

width=800j height=600, title="Function Plotter"^ resizable=True


)

下 面 是 Point类 和 point_table_editor 的定义。TableEditor为表格数据的编辑器类,表格的每


Python 科学计算(第 2 版)

—列与 个 ObjectColumn 对象对应。row_factory 为创建新行时调用的对象。


class P o i n t ( H asTraits) :

x = F l o a t ()

y = F l o a t ()

point 一table 一editor = TableEditor(

columns = [O b j e c t C o l u m n ( n a me = ’x 、 width=100, format="%g"),


O b j e c t C o l u m n ( n a m e = ' y 、 width=100, format="%g")],

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 轻

下 面 是 FunctionPlotter类的初始化函数。O 在调用_ init_()时,Figure 对象已经创建,但是


对 应 的 后 台 界 面 库 中 的 绘 图 控 件 can vas尚 未 创 建 , 因 此 这 里 无 法 使 用
self.figure.canvas.mpl_connect〇设置事件响应函数。在上节介绍的 MPLFigureEditor编辑器中,在
创連绘图控件时会绑定 Figure 对 象 的 canvas_events 属性中定义的事件。© 创連子图对象 a x e 之
T 松制作图形界面

后,调Wj callbacks.connect〇以绑定子图的 xlim_changed 事件,该事件在子图的 X 轴显示范围发


生变化时触发。

def _ init_ (self, **kw):

super(FunctionPlotter, self).— init— (**kw)


self.figure.canvas— events = [ O
("button_press _event ", s e l f .memory_lo ca tio n )^
("button 一r e l e a s e _ e v e n t , s e l f •update 一location)

]
self •button_press_status = None # 保存鼠标按键按下时的状态

self.lines = [] # 保存所有曲线

self, functions = □ # 保存所有的曲线函数


self.env = {} # 代码的执行环境

s e lf.a x e = s e l f .fig u r e .add_subplot (1^ 1, 1)


s e l f .a x e .c a l l b a c k s .c o n n e c t ('x l i m _ c h a n g e d 'y s e l f .u p d a t e _ d at a ) ©
self.axe.set— xlim( 0 , 1 )

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

if evt.button in (1, 3):


self.buttori-press 一status = time.clock(), evt.x, evt.y

else:
self,button_press_status = None

def update— location(self, e v t ) :


if evt.button in (1, 3) and self,button_press_status is not None:

last_clock, last— x, last_y = self.button_press 一status


if t i m e . c l o c k () - last_clock > 0.5: O

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:

point = Point(x=evt.xdata, y=evt.ydata) @


s e l f .p o i n t s .a p p e n d (p o i n t )
elif evt.button == 3:
if self.points:

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

for point in new:


point.on— trait— change(self.updatejDoints, name="x, y") O

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

arr = n p .a r r a y ([( p o i n t .x, point.y) for point in self.points])


if arr.shape[ 0 ] > 0 :

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

当 A x e s 对象的 X 轴显示范围发生变化时,调 用 update_data()重新计算位于显示范围之内的


曲线数据:

def update 一data(self, a x e ) :


xmin, xmax = axe.get_xlim()

x = np.linspace(xmin, xmax, 500)


T r a it s & T r a i t s u 轻

for line, func in zip(self.lines, s e l f . f u n c t i o n s ) :

y = func(x)
line.set_data(x, y)

self.update— f i g u r e ()
T 松制作图形界面

最后,当 Plot 按钮按下时_draw_ button_fired()会被调用,在其中调用 plot_Hnes()运行代码编


辑器中的程序,并绘制函数丨11|线。
® 调用 axe.get_ xlim 〇获得子图的 X 轴显示范围,
并调用 linspace〇创建等分此区间的数组X。
© 创建代码的运行环境env ,
它 是 •个 字 典 ,
其 points键对应由 points列表转换而来的二维数组,
并调i+j ex ec 运 行 co d e 中保存的代码。
对一些属性进行初始化操作之后,© 对 e n v 中的所有键-值对进行循环,找到其中符合条件
的函数并调用,然后把结果保存到 results列表中。O 如果曲线闲数有 pbLpammeters 属性,则将
之作为绘图参数传递给pl〇
t()。
© 最后将执行环境中的points数组转换为 Point对象的列表。

def 一draw 一button 一


fired(self):

self.plot_lines()

def plot 一lines(self):

xmin, xmax = self.axe.get_xlim() O

x = np.linspace(xmin, xmax, 500)

self.env = {"points": n p .array([(point.x^ point.y) for point in self.points])} ©

exec self.code in self.env

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)

line, = self.axe.plot(x, label=label, **kw)

s e l f .l i n e s .append(line)

points = self.env.get("points", None) 0


if points is not None:

self.points = [Point(x=x, y=y) for x, y in np.asarray(points) .tolist 〇]

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 文档的
工具。可以通过下面的语句运行它:

from tvtk.tools import tvtk_doc


tvtk_doc.main()

scpy2.tvtk.tvtk_class_doc: 更方便的 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 费

rttfcltttioo > 29 a Jd cent hatfe reaalucxon cf iidt


1C c r e t c e d . It ^ l t s i 廉 p 〇 0 丨i b l t c c n t r o l w& trher t h e b c tc c
c o n i s capi;« d v i r h a < r€ 5 s lu c io a « a id e ^ > polygon* m d t o
and r^ d iu d e f t h e c e n t .
Cr^phlAyOutView ?0
HiminKt^lGraphV»r
LightAncf >3 Tr^itfi
•feMW/iSouttr__

阁8-1 TVTK 文機刘览器(使 w x 库时的界而截闯)


Python科学计算(第 2 版)

第一次运行此文档工具时,它将对 T V T K 库中所有的类进行扫描,并将类的继承关系和文

3 全部保存在 tvtk_classes.cache 中,这个过程可能斋要等待较长的时间。界 面 的左上部分使j|J一
个树状控件显示 T V T K 中各个类之间的继承关系。在中间的文本框中输入搜索文本,其下方的
列表框中会实时显示搜索结果。输入全小写字母进行忽略大小写的搜索,而输入带大写字母的
文本则进行精确搜索。

8.1 VTK 的流水线(Pipeline)

与本节内容对应的 Notebook 为:08-tvtk_mayavi/tvtk_mayavi-200~pipeline.ipynb。

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 绘制简单的圆锥

由于在 IPython Notebook屮显示 V T K 的窗口之G 将无法关闭,因此下面使用%9^ /出〇11魔


法命令在新的 Python进程中运行显示圆锥的程序:

45:
%%python

#coding=utf- 8

from tvtk.api import tvtk O

# 创建一个圆锥数掘源,并且同时设置其高度、底面半径和底而圆的分辨率(用 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 ()

O f f 先载入 tvtk 对象,它帮助我们创建 T V T K 库中的各种对象。© 创建了一个 C o n e S o u r c e


对象,它是计算圆锥形状的数据源对象。在 T V T K 中,所有类都从 HasTraits继承,因此可以在
创建对象的同时,使用关键字参数直接设置Tm it 属性的值。在这个例子中,同时设置了丨细锥的
高度、底面半径和底面岡的边数等属性。
事实上,我们载入的 t v t k 并不是一个模块,而是某个类的实例。之所以如此设计,是因为
V T K 庳有近 T •个类,而 T V T K 对所有这些类都进行了包装。如果一次性载入这么多类,会极大
地影响库的载入速度。
我们载入的 t v t k 虽然是某个实例对象,但是用起来就和模块一样:
•通 过 它 可 以 使 用 所 有 的 T V T K 类。
•它 不 需 要 载 入 近 千 个 T V T K 类就能支持类名的自动补全。
•只 有 在 真 正 使 用 时 ,T V T K 类才会被载入。
所有对 V T K 进行包装的类全部保存在 tvtk_classes.zip 文件中,而 tvtk 对象的类则在此压缩
文件里的 t v t k _ h e l p e r . p y 中定义。对 于 T V T K 中的每个类,t v t k 对象都有一个同名的属性与之
对应。
下H 丨查看ConeSource 对象的所有 Traits 属性名,并显示 height、radius 和 resolution等属性
的值:
Python 科学计算(第 2 版)

from tvtk.api import tvtk


cs = tvtk.ConeSource(height=3.0, radius=1.0, resolution=36)
m = tvtk.PolyDataMapper(input_connection=cs.output j 3 〇
rt)

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 、

• • •

cs.height cs.radius cs.resolution

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

print type(cs.output), cs.output is m.input

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

然后图形数据界依次通过Actor 、Renderei•最终花 RenderWindow 中品示出来,这一部分就

45
是图形流水线。O A c to r 对象代表场景中的一个实体,它 的 mappei•属性是表示图形数据的
PolyDataMapper对象,A ctor 对象还存许多属性以控制实体的位置、方向、大小等。

print a.mapper is m

print a.scale # A c t o r 对象的s c a l e 屈性衣示各个轴的缩放比例

True

© Rendere丨•对象表示三维场景,它可以包含多个 Acto 丨•对象,这 些 A ctor 对象都保存在 actors


列表属性中。在本例中,它只包含一个显示圆锥的A ctor 对象:

ren.actors

[ '<tvtk.tvtk_classes.acton.Acton object at 0 x 0 D 7 C 7 B D 0 > ']

©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 版)

from tvtk.api import tvtk


from scpy 2 .tvtk.tvtkhelp import ivtk_scene, event 一loop

cs = tvtk.ConeSource(height=3.0, nadius=1.0, resolution=36)


m = tvtk.PolyDataMapper(input_connection=cs.output 一port)
a = tvtk.Actor(mapper=m)

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 () ©

O 将 Actoi•对象列表传递给ivtk_ scene〇,创建并显示一个包含所有A ctor 对象的 T V T K Scene


窗口。© 调 用 evenUoopO 开始界面消息循环,若 在 N otebook 中通过%g u i 命令A 动了界面消息
循环,贝何以省略该行。下而看看 T V T K S c e n e 窗口的各个组成部分:
• 场 景 :用于显示可视化的结果,这里只显示了一个圆锥。
•场 景 工 具 条 :位于场景的上方,主要提供了各种视角、全屏显示、保存阁像等功能。
TV T与

•流 水 线 浏 览 器 :场景左边是一个表示流水线的树状控件。从 子 节 点 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 据的三维可视化

对象的 points 属性,即构成圆锥图形的各个顶点的三维坐标。


流水线浏览器中显示的各个对象的类都从HasTmits继承,因此它们可以提供一个jtJ户界面
以交互式地修改K Tm it 属性。图 8 4 是双击流水线中的 ConeSource 对象之后弹出的属性编辑界
面。通过此界面川‘
以直接修改 height、radius、resolution等属性,并且修改之后场M 中的N 锥会
根据最新的属性值立即更新显示。

阁 8 4 编 辑 ConeSource对象的厲性的对话框

45(
1.照相机

在 iv tk 的窗 I」左侧的流水线浏览器中可以找到场景中的照相机对象OpenGLCamera,双击
它会弹出如图8-5所示的编辑照相机对象属性的窗口。

TV T与
K
M a y a v数
T 据的三维可视化
阁8 - 5 编辑照相机屈性的对话框

也可以使用下面的程序从窗丨」对 象 w indow 获得照相机对象,然后查看或修改它的属性:

camera = window.scene.renderer.active 一camera


print c a m e r a .clipping_range
camera.view_up = Q, 1, 0

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 : 照相机的上方向矢量。

• paralleLprojection: True 表示采用平行透视,即在三维场景中平行的直线在屏蘇上也


是平行的。
这些属性虽然可以完全控制照相机的位置和方向,但是实际操作起来并不方便。如果已经
将照相机的焦点岡定在某个位置,可以调用照相机对象的如下两个方法,在以焦点为原点的球
而坐标系中对照相机进行操作。它们保持照相机的 v i e w _ u p 属性不变。
• a z i m u t h ( a n g l e ) : 沿着纯度线旋转指定角度,即水平旋转,改变其经度。

• elevation (angle ): 沿着经度线方向旋转指定角度,即垂直旋转, 改变其纬度。

2.光源

在 ivtk 窗口中,单击场景上方的工具栏中的最后一个齿轮形状的图标,将打开如图8-6所
示的编辑场景和光源的对话框。在此对话框中可以添加和删除光源以及修改它们的一些屈性。
TV T与
K
|\/^3<丁 数 据 的 三 维 可 视 化

阁 8 - 6 设®场眾和光源的对话框

场景中的光源可以通过R e n d e r e r 对象的 lights属性获得,


它是一个光源对象的列表。
Renderer

对象还有 add_light 〇和 remove_light() 等方法jij于添加I或删除光源对象。

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.实体

A ctor 对象表示场景中的实体,在圆锥的流水线浏览器中,可以看到一个表示圆锥的 Actor


对象,双击它会打开如图8-7所示的对话框。

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

在此对ii1!•框中可以编辑Actoi•对象的 origin、position、orientation和 scale 等属性,修改场景


屮实体的位置、方向以及大小。这 4 个屈性通过一系列复杂的计算得到一个4 X 4 的三维空间的
变换矩阵。变换步骤如下:
(1) 以 origin 为中心,使 用 scale 对物体在三个轴上进行缩放。
(2) 以 origin 为中心,使 用 rotate对物体在三个轴上进行旋转,旋转的顺序是 Y 轴—X 轴
—Z 轴。
(3) 将物体放到 position 处。
我们通过下而的例子理解坐标变换的步骤。首先运行下而的程序,在场景中添加一个坐
标轴:
Python 科学计算(第 2 版)

axe = tvtk.AxesActor(total_length=(3,3,3)) # 在场景中添加坐标轴


w i n d o w .s c e n e .a d d _ a c t o r ( axe )

可以看到屏幕的横轴方向是X 轴 ,纵轴方向是 Y 轴,而从屏幕里往外是 Z 轴方向。整个


圆锥的长度为3 , 它的底面在 X =-1.5的平面之上。双击流水线对话框中的表示圆锥的 Actor ,
打开编辑其属性的对话框。
我们将 origin 修改为(-1.5,0,0),这样将以丨剧锥的底面圆心为中心进行缩放和旋转。依次按
照 图 8-8的顺序修改各个属性的值。对话框中的“FD”、 “F 1”和 “F 2 ”等标签分别表示 X 、
Y 和 Z 轴的分董。

scale x = 0.5 orientation y

i. 4


TV T与

origin = (-1 .5 ,0 #0) a


3
K

X
N
M a y a v数

vO
T 据的三维可视化

position y = 1.5 position x ** 1.5

阁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 属性是实体的不透明度。输入下面的语句可
以打开编辑这些属性的对话框:

a •property •edit_traits ( ) # a 是表示岡锥的 Actor 对象

由于 OpenGLProperty对象的屈性太多,这M 小--- 进行介绍。在后面的实例屮用到时得对


其进行说明。
8.2關 集

与本节内容对应的 Notebook 为: 08-tvtk_mayavi/tvtk_mayavi-300-dataset.ipynb 〇

数据可视化的第一步是用合适的数据结构表示数据,
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
属性:

img = tvtk.ImageData(spacing=(0.1,0.1,0.1), origin=(0.1,0.2,0.3), dimensions=(3,4)5))

origin 属性为三维网格数据的起点坐标,spacing 属性为三维网格在 X 、Y 和 Z 轴上的间距,


dimensions属性为 X 、Y 和 Z 轴上的网格数。
img.get_point(n)〇T以获得网格中第n 个点的坐标值,
n 为点的序号,起始点的序号为0 , 序号依次沿着 X 、Y 和 Z 轴递增。下面的程序输出前6 个点

的坐标:

for n in range( 6 ):
print %.lf, %.lf" % img.get_point(n)

0.1, 0.2, 0.3


0.2, 0.2, 0.3
0.3, 0.2, 0.3

0.1, 0.3, 0.3


0.2, 0.3, 0.3
0.3, 0.3, 0.3
Python 科学计算(第 2 版)

与每个点对应的数据都保存在p〇int_data屈性_中,它是一个 PointData对象:

img.point_data
<tvtk.tvtk_classes.point_data.PointData at 0xal87780>

PointData对象可以保存多组数据,它 的 scalar 屈 性 是 V T K 库中的一个数组对象,用来保


存与每个点相对应的标量值。当将 _ •个 N um P y 数组赋值给它时,T V T K 将自动创建 V T K 中相
应的数组对象,并保存 N um P y 数组的内容。例如下面的程序运行之后,scalar 属性从 None 变
成一个 DoubleArray 数纟11。程序为每个点添加了一个与其序号相同的标量值:

print img. point一data .scalars # 没有数据


img.point_data.scalars = np.arange(0.0> img.number_of一points)
print type(img.point_data. scalars)
img. point_data. scalars
None
〈 class ' tv tk .tvtk_classes. double_array. DoubleAmay' >
TV T与

[0.0, 59.0], length = 60


K

D onbleA m iy 数组只能以整数为下标,不支持切片以及其他高级下标运算。可以通过它的
IV la ya丁

to_airay()方法获得与其共享数据存储空间的N um P y 数组,通 过 此 N um P y 数组可以进行更高级


v 数据的三维可视化

的数据存収操作:

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

V T K 的 数 组 对 象 有 许 多 属 性 ,可以 j 丨彳print_traits()方 法 查 看 其 各 个 属 性 的 值 ,例如


number_olLtuples属性表示数组的长度:

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 屈性.

获得,可 以 通 过 a d d _ m r a y 〇方法添加新的数纟JI对象,r e m o v e _ a r r a y ( ) 方法j|j于删除数纟J1对象,


get_array() 和 g e t _ a r r a y _ n a m e ( ) 分別用于获得数组对象及其名字。下面演示这些方法的)|j法。首先

创建一个 T V T K 的数组对象,并调用其 f r o m _ a r r a y 〇方法通过 N u m P y 数组设置其内容,数组的


长度为 I m a g e D a t a 对象的点数:

data = tvtk.DoubleArray() # 创建一个空的 D o u b l e A r r a y 数组


d a t a .f r o m _ a r n a y (n p .z e r o s (i m g .n u m b e r _ o f _ p o i n t s ))

接下来将 T V T K 数组对象的名字设置为"zerodata":

data.name = "zerodata"

然 后 调 用 PointData 对 象 的 add_array〇方法,将所创建的数组添加进 PointData 对象。当


PointData 对象已经有相同名字的数组时,将会禮盖原来的数组。add_array()方法的返回值是新

数组的序号,可以通过此序号获得数组或数组名:

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()方法通过数组名删除数组:

img.point 一data •remove_array ("zerodata ") # 删除名为"zerodata "的数组


img.point _data .number_of_arrays
1

可以用上述方法对 PointData对象中的多个数组进行管理,同吋为了方便使用,它 的 scalar


屈性也可用于保存数组。与每个点所对应的值除了标量之外,还可以是矢量或张量(矩阵),矢
量数组可以使用 vectors 屈性保存,而张量数组可以使用te n s e r 屈性保存。下面的程序将一•个形
状为(N ,3)的 N um Py 数组赋值给 vectors 属性,其 中 N 为点数,这 样 ImageData对象屮的每个点
都对应三维空间中的一个矢量:

vectors = np.arange (0.0> img.number_of _ points *3).reshape (-1^ 3)


img.point _data.vectors = vectors
print repr (img.point _data .vecto rs )

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 ]

[(0.0, 1.0, 2.0), (177.0, 178.0, 179.0)], length = 60


〈class 't v t k .t v t k _ c l a s s e s .d o u b l e _ a r r a y .D o u b l e A r r a y '>
(0 .0 , 1 .0 , 2 .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

同样,直接使 用 D oubleAm iy 对象的方法或属性对其进行操作比较烦琐,因此建议读者仍


然使用 to_array()方法将其转换为 N um Py 数组之后再进行操作。
在 ImageData对象中,点的坐标足通过 spacing、origin 和 dimensions等属性隐式足义的。与
TV T与

之类似,点和单元之间的关系也是隐式定义的。单元和点之间的关系如图8-9所示。单元是由
K

8 个邻近的点构成的立方体,图中使用半透明灰色立方体标识出第0 个单元。
M a y a v数
T 据的三维可视化

点(
P o in t )

维元(C e l l 〉

图8 - 9 单元和点之间的关系

scpy2.tvtk.figure_ imagedata: 用于绘制图 8-9 的程序。

通 过 get_cell()方 法 可 以 获 得 表 示 单 元 的 Voxel( 体素 ) 对 象 ,它 的 number_of_points、


number_of_edges和 number_of_faces等屈性分別是构成体素对象的点数、边数以及面数。

cell = img.get_cell( 0 )

print repr(cell)

<tvtk.tvtk_classes.voxel.Voxel object at 0x0A184C30>


c e l l .number_ofjDoints c e l l .number_of_edges c e l l •number'一
of 一
faces

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

[0, 1, 3, 4, 12, 13, 15, 16]

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

ImageData 对 象 的 number_ of_c e lls 属性是数据集中的单元数,也就是图8-9中小立方体的

数0 〇

i m g •number 一
of_cells

24

数据集提供了许多获収点和单元之间关系的方法。例 如 g et_p〇i m _ cells〇获得包含某个点的


所有单元的序号,而 get_Cell_pointS〇则获得某个单元包含的所有点的序号。由于这两个方法将
结果写入一个 IdList对象中,因此需要先创建一个空的IdList对象川于保存结果:

a = tvtk.IdList()
i m g .g e t j 3 〇
i n t _ c e l l s (3, a)

print "cells of point 3:", repr(a)


i m g .g e t _ c e l l jD o i n t s (0 , a)

print "points of cell 0 :", repr(a) # 和 cell .point 一ids 的值相同


cells of point 3: [2, 0]
points of cell 0: [0, 1, 3, 4, 12, 13, 15, 16]

IdList 是 V T K 中管理序号的列表对象,它 和 P y t h o n 的标准列表一样支持a p p e n d 〇和 extend()


Python 科学计算(第 2 版)

方法,并且可以通过 from_array()将列表或数组转换为 IdList对象:

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

< t v t k .t v t k _ c l a s s e s .c e l l _ d a t a .CellData at 0xa285c60>

8.2.2 RectilinearGrid
TV T与

Im ageData 是最简单的数据集,它的所有点都在一个等间距的三维M 格之上,因此只需要


K

起始处标、网格大小以及网格间距等信息就可以计兑出网格上所有点的坐标。如果要表示间距
M a y a v数

不均匀的网格,可以使用如图8-10所示的 RectilineaKirid 数据集。


T 据的三维可视化

(〇
.〇.〇
)

r
X

get 一c e l l ⑴
(15,Q.〇
r

图8 ~1 0 使用RectilinearGrid创建分布不均匀的网格

scpy2.tvtk.figure_ rectilineargrid: 绘制图 8-10 的程序

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 = np.arange( 0 number— of_points) ©

r.point_data.scalars.name = 'scalars'

RectilineaiGrid 和 ImageData 相同,所有点都在一个正交的网格之上,所不同的是网格的分


布是不均匀的,因此需要通过一些属性设置 X 、Y 和 Z 轴的各个网格平而的位置。O 通过
RectilinemGrid对象的 x_coordinates、y _coordinates和 z_coordinates等屈性,分 别 设 格 中 与 X
轴、Y 轴 和 Z 轴乖直的平面的位置。RectilinearGrid对象中的点就是所有这些平面的交点。©由
于 RectilinearGrid对象不会根据这三个数组的长度自动调整dimensions属性,因此需要根据数组
的长度设置 dimensions屈性。©最 后 通 过 point_data 屈性设置每个点所对应的数据。
和 ImageData对象一样,点的序号依次沿着X 、Y 和 Z 轴递增,例如:

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)

(15.0, 0.0, 0.0)


(0 .0 , 1 .0 , 0 .0 )

(3.0, 1.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)

虏的下标《序

g«t _cell <2)

X
点的下择JB序

图8-11 用 StructuredGrid创建的网格结构

下而对创逑这两个网格的程序进行分析:
TV T与

scpy2.tvtk.figure_structuredgrid: 绘制图 8-11 的程序。


K
M a y a v数

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'

〇首先用 N um Py 的 mgrid 对象创建了三个数组 x 、y 和 z 。它们的形状都是(3,5,4)。其中数


组 x 的数据在第2 轴上变化,数 组 y 的数据在第1轴上变化,数 组 z 的数据在第0 轴上变化。
在这三个数组中,由对应下标的数值组成等间距M 格上的点。
© 将每个点的 X 和 Y 轴的坐标值,
乘以由 Z 轴坐标值决定的系数。沿 着 Z 轴正方向乘积系数逐渐变小,相当于将垂B :
Z 轴的网格
进行不同比例的收缩,最终形成一个如图8-11(左)所示的梯形网格。
© StmcturedGrid对 象 的 points属性楚数据集中每个点的坐标。它楚一个表示 N 个点的坐标
的数组,形状为(N ,3)。因此耑要将三个形状为(3,5,4)的多维数组合并成一个形状为(3*5*4, 3)的
二维数组。这个转换工作由 make_points_array〇完成。其中首先调用参数数组的ravel〇方法,得
到其平坦化之后的一维数组,然后通过 np.cJ C j 象将三个一维数组按列组合成二维数组。
O 将 S 如cturedGrid 对 象 的 dimensions属性设置为(4,5,3)。在 points 屈性中只保存点的坐标,
点之间的关系_ dimensions属性决定。dimensions和 N um Py 数组的 shape 屈性类似,{曰.是K 中第
0 轴的变化最快,因此需要将数紅〖
的 shape 属性倒序之后再赋值给它。经 过 dimensions属性处理
之后,各个点之间的关系如图8-12所示。图中每个小方块代表points属性中的一个点,方块上
的数字表示点在points属性中的下标。

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
-- — —

图8-12 dimensions 为(4»5,3)的StructuredGrid 的点的结构

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)

print "number— of— f a c e s : ", c.number— of— faces #中元的而数


f = c.get_face( 0 ) #获得第 0 个而
print "face type:", type(f) # 每个面用一个Quad 对象表示
print "points of face 0:", repr(f.point_ids) #构成第 0 面的 4 个点的下标

print "edge count of cell:", c.number_of_edges # 雄元的边数


e = c.get_edge( 0 ) #获得第 0 个边
print "edge t y p e : ' type(e)

print "points of edge 0 :", repr(e.point_ids) #构成第 0 边的两个点的下标


cell type: 〈class 't v t k .t v t k _ c l a s s e s .h e x a h e d r o n .H e x a h e d r o n '>

number_of_faces: 6

face type: 〈class 'tvtk.tvtk 一classes.quad.Quad'>


points of face 0 : [2, 22, 26, 6 ]

edge count of cell: 12

edge type: 〈 class 'tvtk.tvtk_classes.line.Line'>


points of edge 0: [2, 3]

使 用 StmcturedGrid 可以创建出任怠形状的网格,例如下而的程序创建阁 8 -ll(右)所示的半

69
Python 科学计算(第 2 版)

个空心圆柱。程序屮,首先在圆柱坐标系屮创建等距网格,然后将各点坐标转换到直角來标系
中,具体的步骤留给读者自行分析。

r, theta, z2 = np.mgrid[2:3:3j, - n p .p i / 2 :n p .p i / 2 :6 j^ 0:4:7j]

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 = np.arange( 0 > s 2 .number 一


of_points)

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

ConeSource 对象的输出数掘是一个 PolyData 对象。PolyData 对 象 的 points 属性是一个保存:


点的坐标的数组。为了方便查看各个点的坐标,我 们 用 N um P y 的 array_str〇函数输出此数组的
内容,利 用 suppress_sm all参数将很小的数显示为0:

print n p .a r r a y _ s t r ( c o n e .p o i n t s .t o _ a r r a y ()^ suppress_small=True)

[[0.5 0. 0.]

[-0.5 0.5 0.]

[-0.5 0. 0.5]

[-0.5 -0.5 0.]

[-0.5-0. -0.5]]

由于设置了 ConeSource 对象的 resolution属性为4 ,因此|M|锥的底面楚一个正方形。由各个


点的坐标很容易看出底面垂直于X 轴,并且在 X =-0.5的平面上,而岡锥的顶点在X 轴上的 X =0.5
处。各个点之间的联系由polys 属性决定:

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]

p o ly s 属性是一个 C e llA m iy 对象,其中保存各个而和点之间的关系。我们创建的圆锥有5


个面,因 此 CellArray 对象的 number_of_cells 属性为5。CellArray 对象内部使用一个一维整数数
组保存构成各个面的点的下标。由于构成每个面的点数可能不N , 因此还需要保存构成每个面
的点数。我们可以把这个一维数组现解为如下所示的构造.•其中每一行对应一•个面,冒号前面
的数值是构成此面的点数,冒号后面的一串数字是每个点在points 属性中的下标。由下面的数
据可知,方锥凼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()

pinpoints = [( 1 , 1 ,0 ),( 1 ,- 1 ,0 ),(- 1 ,- 1 ,0 ),(- 1 , 1 ,0 ),( 0 ,0 , 2 )] O

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 () ©

cells.set 一c e l l s (5, faces) ©

pi.polys = cells

pi.point_data.scalars = np.linspace( 0 .0 , 1 .0 , len(pi.points))

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创逑的多而体

scpy2.tvtk.figure_ polydata: 绘制图 8-13 的程序。


TV T与
K

下面获取第0 面 和 第 1面上的点的序号:
M a y a v数

print repr(pi.get 一c e l l (0 ) .point 一ids)


T 据的三维可视化

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 ]

下面是使用 P d yD ata 创建半球面的程序,图 8-13(右)是半球面的显示效果。为了显示出整


个半球上的点,图中将面隐藏,仅显示各个面的边框。

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 = np.zeros(((N-l)**2 ,4 ),np.int) ©


tlj t2 = n p . m g r i d [ :(N-1)*N:N, :N-1]

faces[:, 0 ] = (tl+t 2 ).ravel()


faces[:
jl] = f a c e s [ :
j0 ] + 1

faces[:,2] = facesfi^l] + N

faces[:,3] = faces[:,0] + N

p2 = tvtk.PolyData(points = p o i n t s , polys = faces)

p 2 .point 一data.scalars = np.linspace( 0 .0 , 1 .0 , len(p 2 .points))


/ ^

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 ]

array([ 4, 0, 1, 11, 10, 4, 1, 2, 12, 11, 4, 2, 3, 13, 12, 4, 3,

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•的用法基本相

同。为了让读者能有效地川T V T K 替 代 V T K 扩展库,本节对 T V T K 的一些改进进行总结。首


先看一个使用标准的V T K 扩展库显示圆锥的例子:

%%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 ( )

此程序和 C ++程序的R 別仅仅是没有声明变量的类型,其他的用法完全和 C ++的 V T K API


相同。官方所提供的 V T K -Python 包 和 G h ■语言的接口相似,许多地方没有体现出 Python 作为
TV T与

动态语言的优势,可以说标准的 V T K -Python 库 不 是 Python 风格的。为了弥补这些不足之处,


K

Enthought公司开发的 T V T K 库进一步对 V T K -Python 进行包装。它具有如下优点:


• 支 持 Trait 属性
IV la ya丁

• 支 持 元 素 的 Pickle 操作
v 数据的三维可视化

• A P I 更接近 Python 风格
•能丨二
1动处理N um Py 数组或列表对象
• 流 水 线 浏 览 器 ivtk

8.3.1 TV TK 的基本用法

下面是前面介绍过的用T V T K ffi 示圆锥的程序:

from tvtk.api import tvtk

cs = tvtk.ConeSource(height=3.0, radius=1.0, resolution=36)


m = tvtk.PolyDataMapper(input 一connection = cs.output_port)

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

可以看到它比标准 V T K 版本要简短许多,从屮可以看到 T V T K 的-•些重要改进:


• T V T K 厍中的类名除去了前缀"vtk"。有些类名在”
vtk ”
Z 后是数字,T V T K 厍对这种类
名进行特殊处理:如果首字符为数字,就用K 英文单词代替,例 如 vtk3DSlmpoiter 变
成 ThreeDSImporter〇
• 蚋 数 名 按 照 Python 的惯例,采用下划线连接单词,例 如 Addltem 变 成 add_item。
• 许 多 V T K 对象的方法在 T V T K 中用 Trait 属性替代,例 如 K 面列出了岡锥实例中的两
处改进:

m .Setlnpu t Co n n e c t i o n (c s .G e t O u t p u t P o r t ()) # VTK


m.input_connection = cs.output_pont # TVTK

p .SetRepresentationToWireframe() # VTK

p.representation = 'w' # TVTK

• 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")

调 用 edit_traits()或 configure_traits〇可以M 示编辑屈性.的对话框:

p.edit_traits()

可以通过 tvtk.to_tvtk()得到任何 T V T K 对象所包装的 T V K 对象,在必要的吋候直接对V T K


对象进行操作:

print p.representation

p_vtk = tvtk.to— vtk(p)

p _ v t k .SetRepresentationToSurface()
print p.representation

wireframe

surface

也可以通过 T V T K 对象的_ v t k _ o b j 屈性获得其中的 V T K 对象,


而 tvtk.to_tvtk〇则可以将 V T K
Python 科学计算 (第 2 版)

对象包装成 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与

象的状态,可以如下调用_ setstate_〇 来实现:


K

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

对比一卜 V T K 的相应程序,就能体会出 T V T K 库的优点了:

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 .],

[0., 0., 1.]])


T V T与

如 果 T V T K 对象的属性或方法能够接受 DataArray、Points、IdList 以 及 CellArray 等对象,


K

那么它也同时能够接受数组和列表:
M ayavT

points = np.array([[0,0,0],[1,0,0],[0,1,0],[0,0,1]], 1')


数据的三维可视化

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可视化实例

与本节内容对应的 Notebook 为:08-tvtk_mayavi/tvtk_ mayavi-500-tvtk-examples.ipynb«

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 显示可视化结果,冈此可以使用界而左侧的流水线浏
览器观察组成整个场景的流水线。

scpy2.tvtk.example_cut_ plane: 切面演示程序。


Python 科学计算 (第 2 版)

def read 一data():

# 读入数据
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

MultiBlockPLOT 3DReader 对象从数掘文件读入三维数掘信息。© 为了让它真正从文件读入数掘,


IVlayav

需要调用其 i丨
pdate〇方法。运行此方法之后,© 就可以通过其 output属性获取读入的数据集对象
丁数据的三维可视化

了。通 过 output屈性获得的是一个 MultiBlockDataSet对象,通 过 get_block(0)可以获得其中下标


0 对应的 StructuredGrid数据集。

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

〈class 't v t k .t v t k _ c l a s s e s .m u l t i _ b l o c k _ d a t a _ s e t .M u l t i B l o c k D a t a S e t '>


<class 't v t k •tvtk 一c l a s s e s •s t r u c t u r ed _ g r i d •S t r u c t u r e d G r i d •>

MultiBlockPLOT 3DReader 读 入 PLOT 3D 格式的文件,PLOT 3D 是一种流体动力学数据可视


化的程序。程序中通过设置 scalar_ function_ number和 vector_ function_number屈性,分别指足标
量数组和矢量数组为流体的密度和速度。各个参数的具体含义请读者沓看 V T K 的相关文档。
MultiBlockPLOT 3DReader 对象输出的是一个 StructuredGrid 数据集。下面观察它的一些屈性:

print "dimensions:", grid.dimensions

print grid.points.to_array()

print "cell a r r a y s :'、 g r i d .c e l l _ d a t a .number_of_arrays

print "point a r r a y s : ", g r i d .p o i n t _ d a t a .number_of_arrays

print "arrays n a m e : "

for i in xrange(grid.point 一data.number 一of_arrays):

print " ", grid.point 一data.get_array_name(i)


print "scalars n a m e :’
、 grid.point 一
data.scalars.name
print "vectors n a m e : ", g r i d .p o i n t _ d a t a .v e c t o r s .name

dimensions: [57 33 25]


[[2.66700006 -3.77476001 23.83292007]
[2.94346499 -3.74825287 23.66555977]

[3.21985817 -3.72175312 23.49823952]

[15.84669018 5.66214085 35.7493782 ]


[16.17829895 5.66214085 35.7493782 ]
[16.51000023 5.66214085 35.7493782 ]]

cell arrays: 0

point a r r a y s : 4
arrays name:

Density
Momentum
StagnationEnergy
Velocity

scalars name: Density

vectors name: Velocity

凼上面的各个属性可以得知,数据集 grid 是一个形状为(57, 33, 25)的网格。由于它是一个


StructuredGrid对象,因此网格中的每个点的坐标保存在points属性中。网格中的单元没有数据,
而每个点对应4 种数据,
它们的名字分别为"Density”
、nMomentumn、
"StagnationEnergy”
和"Velocity "。
其中通过 point_data 的 scdm*s 属性可以获得名为’
'〇61!_''的数组,
它是一个标|3:数组,
而'’
\^1〇〇办’

数组则可以通过 p〇int_data 的 vectors 属性获得,它是一个三维矢M 数组。切而将针对 scalars 属
性的数据进行运算,因此切面所显示的是切面上每个点的密度值。
〇我们需要把切面上的密度用某种颜色来表示,因此需要进行值到颜色的转换。在 V T K
中这利1转换工作由 LookupTable 对象完成。它 的 table屈性是一个保存颜色表的数组。这里我们
使 用 matplotlib库的颜色映射对象计兑LookupTable 对象的颜色。
接下来显示 StructuredGrid中某一层网格面上的数据:

# 显示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) ©

plane_mapper.scalar_range = grid.scalar 一range O


plane_actor = tvtk.Actor(mapper = plane_mapper) ©

OSlmcturedGridGeometryFiltei• 对 象 能 从 StructuredGrid 对象中提取部分网格。程序中通过


extent属性指定了提取的网格的范围:第 0 轴 和 第 1轴的范围是0 到 100,而 第 2 轴的范围是6
P y th o n 科学计算(第 2 版)

到 6。.其 中 第 0 轴、第 1轴的范围大于网格在这两个方向上的长度,因此它们提取这两个轴上


的整个范围,而在第2 轴上只提取第6 层的数据。© 当直接把数据集对象当作输入时,应当调
叫 VTK 中的 SednputData()函数,
在 TVTK 中该函数为 set_input_data〇(>StructuredGridGeometryFilter
对象输出一个 PolyData 数据集:

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

它 由 1881个点构成,因为它是 StmcturedGrid对象中第2 轴上的一层,所以它的点数等于


数据的三维可视化

原始数据集的第0 轴 和 第 1轴长度的乘积。下面比较 StructuredGrid对象和 PolyData 对象中点的


坐标:

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

pointsl 是 将 StmcturedGrid对象的点还原成三维网格之后的数组,数组的第3 轴表示每个点


的 X 、Y 和 Z 轴的坐.标值。 fct丨于N um Py 数组的 shape 屈性和 StructuredGrid对象的 dimensions屈
性为倒序关系,
因此 dimensions属性中的第0 轴相当于 shape属性的策2 轴 。
同样,
我们将 PolyData
对象的点还原成表示二维网格数组的points2。因为我们提取的是StmcturedGrid对象的第2 轴的
第 6 层数据,因此 pointsl[6]应该和 ponits2 相等。
使 用 StructuredGridGeometryFilter对象不但从数据源提取了点的坐标,还提取了每个点所对
应的数裾,因此 PolyData 对象的每个点也对应有4 组数椐,并且数组名也保持不变。

print p .point _data .number_of_arrays


print p .point _data .scalars.name
4
Density

€)使 用 PolyDataMapper对象将 PolyData 对象转换为图形数据,同时用前面创建的颜色映射


表 lut 对 PolyData 对象中的每个点进行着色。O 为了能使用颜色映射表中的所有颜色,我们将颜
色映射表的范围设置成和密度数组的范围一致。© 最后创建在场景屮显示图形数据的 Actor
对象。
接下来是创逑平面切面的程序:

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 )

t v t k •tvtk 一c l a s s e s •poly 一d a t a •PolyData

© 创 建 - 个 Cuttei•对象,它使用传递给 cut_function 屈性的 Plane 对象,对 set_ input_data()设


置的0 标数据集进行切面插值。Cutter对象的输出也是 PolyData 对象,由 于 StructuredGrid对象
中的点不一定能正好落在Plane 对象所指定的平面之上,因 此 Cuttei•对象会对 StructuredGrid对
象中的数据进行插值计算。它所输出的 PolyData 对象中的点不丙是数据源中的点。

cut.update()
c u t •o u t p u t •number 一
of 一points

2537

PolyData 对象中的每个点仍然与4 组数据相对应,这些数据都是通过对数据源进行插值计

算得到的:

c u t •o u t p u t •point 一
d a t a •number 一
of— arrays

为了在.场景中显示切面,和前面所讲的曲面一样,使HI PolyDataMapper和 A ctor 将 PolyData


对象加工成场景中的物体。接下来创建 StructuredGrid对象的外边框部分:
Python 科学计算 (第 2 版)

def make 一outline(input_obj):


from tvtk.common import configure_input
outline = tvtk.StructuredGridOutlineFilter()

configure_input(outline, in p u t _ o b j )
outline 一
mapper = tvtk.PolyDataMapper(input 一connection = outline.output 一port)

outline_acton = tvtk.Actor(mapper = outline_mapper)


outline 一actor.property.color = 0.3, 0.3^ 0.3
return outline_actor

outline 一
actor = make 一
outline(grid)

在 make_outline〇中使j|j StmcUiredGridOutlineFilter对象计兑出一个表示外边框的 PolyData 对


象。其中调川 tvtk.common.configure_ i叩ut〇来设置 StructuredGridOutlineFilter 对象的输入。该闲数
会根据输入对象的类型选杼i叩ut_connection 或 set_ input_data()〇
请读者根据前而介绍的方法观察此PolyData 对象的内容,这里就不再举例了。最后使用本
书提供的tvtkhelp模块中定义的ivtk_scene()显示前而创建的三个Actoi•对象:plane_actor、
cut_actor、
T V T与

outline actor。
K
M ayavT

win = ivtk_scene([plane_actor, cut_actor, o u t l i n e _ ac t o r ] )

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 等值面

等值面楚标量场中标量值相等的曲面,和地图中的等高线类似。在 V T K 中使用 ComourFilter


计算等值而。下面的程序对丨:节的流体数据进行等值而讨视化,效果如图8-16所示。
图8 - 1 6 使用等值而对标:舉:场进行可视化

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 ©

outline 一actor = make 一


outline(grid)

win = ivtk 一scene([actor, outline 一actor])

w i n .s c e n e .isometric_view()

® 创建一个 ContourFilter对象,并且调用 generate_values〇方法来创建8 个等值而,等值面


的収值范围凼标量值数组scalars 的范围决定。
©等值面的颜色映射也由 s c a la r 数组的范围决定。
由于没有设置映射器的颜色表,将使用系统缺省的映射表,它将最小值映射为红色,将最大值
映射为蓝色。
© 由 于 8 个等值面是嵌套的,我们需要修改等值III丨的A ctor 对象的透明度,以便观察等值
面的内部构造。除了使用 generate_values〇创建等差等值面之外,还可以使用 set_value〇方法直接
设置每个等值面的值,而 g et_ v a l lie〇方法町以获得等值面的值。下面的程序修改第〇个等值面

的值:
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与

mapper = tvtk.PolyDataMapper(input_connection = c o n t o u r s .output_port^

scalar 一range = grid.point 一data.get 一


K

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) ©
丁数据的三维可视化

acton = tvtk.Actor(mapper = mapper)


actor.property.opacity = 0 .6

outline— actor = make_outline(grid)

win = i v t k _ s c e ne ( [a c t o r , outline 一actor])

w i n .s c e n e .isometr i c_ v i e w ( )

〇首先调用 PLOT 3DReadei•对象的 add_ftmction〇方法,为每个点添加一组标量值。所增加


的新数组名为’
'VelocityMagnitude’

,表示每点所对应的速度大小, K 而将使用此数组对等饥面进
行着色。

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。

• ,,use_point_data ":使用 point_data.scalars 〇

• "use cell data ”


:使用 cell data.scalars。
• nuse_point_field_datan: 使j |jpoint _data 中的某个数组,具体的数组需要另外指定。
• nuse_cell_field_data'’
:使jlj cell_data 中的某个数纟11,具体的数纟U斋要另外指定。
© 通 过 color_by_army_component()指 定 着 色 所 使 的 数 据 。它的笫一个参数楚数纟丨1名,第二
个参数是从数组中选杼的列。由于’

VelocityMagnitude”
是一个标量数组,它只有一列数据,因此
第二个参数为0。如果第一个参数是矢:设数组名,例如'’
Velocity " , 那么可以通过第二个参数选

择矢S 数组中的不同分M 。请读者修改这两个参数,使用其他的数据对等值而进行着色。

T V T与
K
M ayavT
阁8 - 1 7 在等值而上丨丨]颜乜显示邛他标贵值

数据的三维可视化
8 . 4 . 3 流线

在前面的实例中,我们使用切面和等值面对流体的密度分布进行了可视化。空间中每一点
的密度可以用一个数值(标M )表示,因此可以将密度分布理解为一个标M 场。而流体在每一点
的速度是一个矢M , 因此速度的分布情况需要使用矢S 场来描述。木节介绍如何使用随机散布
的矢量箭头和流线对矢量场进行可视化,效果如图8-18所示。由此图可知,整个场景由4 个实
体构成:外框、随机散布的矢量箭头、表示流线源的球体以及流线。

图8 - 1 8 矢量场的可视化
Python 科学计算 (第 2 版)

scpy2.tvtk.example_ streamline: 使用流线和箭头可彳见化矢量场。

下面的程序创建随机散布的矢量箭头。这些箭头的起始点是数据集中的点的坐标,箭头的
方向由点所对应的矢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 ()))

a r r o w s •set— source_connection(arrow 一source.output_port)


arrows 一
mapper = tvtk.PolyDataMapper(input 一connection = arrows .output 一port

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 对象对数据集中的数据进行随机选収,某点被选中

的概率为 1 / 5 0 , 即每 5 0 个点选择一个点。下面的程序查看 M a s k P o i m 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

© ArrowSource 对象创建表示箭头的PolyData 数据集。


© Glyph 3D 对象对箭头数据进行复制,
在 masks 输出的 PolyData 数据集的每个点之上都放置一个箭头。箭头的方向、长度和颜色由勹
点对应的矢M•和标M•数椐决定。scale_faCto r 参数是所有箭头共同的缩放系数,这里使用标M 数
组的最大值对缩放系数进行正规化。Glyph 3D 对象可以对任意的 PolyData 数据进行复制,读者
可以尝试将箭头数据源ArrowSource 改为圆锥数据源 ConeSource。
由于 T V T K 没有提供 source_connection属性,因此只能通过set_ source_connection()设置
A Glyph 3D 对象的输入。

a r r o w s . u pd a t e ()
print arrow 一source.output .number_of_points # 个点 t"箭头有 31

print a r r o w s . o u t p u t •number_of_points # 箭头被复制了 N 份,因此有 N*31 个点


31
29512

还可以使用流线直观地观察矢量场。流线上每一点的切线方向就是矢量场在该点的方向。
下面是显示流线的程序:

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)

tube 一actor = tvtk.Actor(mapper=tube 一mapper)


tube_actor.property.backface_culling = True

89
Python 科学计算 (第 2 版)

outline_acton = make_outline(grid)

win = ivtk_scene([outline_actor, sphere 一actor, tube_actor, arro w s _ a ct o r ] )

w i n .s c e n e .isometric_view()

© SphereSource对象创建表示球体的 PolyData 数据集。通 过 centei•和 radius屈性指足球体的


球心位置和半径。
phi_resolution和 theta_resolution屈性指足球体的经度和韩度方向上的等分次数,
值越大输出的 PolyData 数据集越接近球体。
© SfreamLine对象在矢m 场中计丨7:流线。通 过 set_source_connection()设H 决定流线起点的数
据集的输入端口,这 里 使 用 SphereSource 对象输出的球面上的点作为流线的起点。如果通过
start_ position 属性设置起始点的坐标,贝II只计兑一条流线。stepjength 属性决定了流线上的点的
间隔,此值越小流线上的点越多,流线越平滑,计 所 需 的 时 间 也 越 长 。integration_direction属
性决定流线的计算方丨(彳,fl1[可以为加ckwmd’
、’forward’
和’integmte_both_diiiections'。’
forward'表示
计算起点之后的流线,backward'表示计算起点之前的流线。©流线的计算需要使用由 integrator
属性指定的数值积分算法,这里使用 RungeKutta4 , 它是一个4 阶 Runge-Kutta积分算法。
T V T与

StreamLine 对象的输出是一个 PolyData 数掘集,它的所有单元都是表示流线的路径,因此


K

边(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 jDoints

print s t r e a m e r .o u t p u t .number_of jDolys

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

这里为了节省内存空间和计算时间,PolyData 对象使用 TriangleStrip对象表示三角面。一个


TriangleStrip对象由一组点构成。每连续的三个点构成一个三角形而:

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 ⑴

print t.number 一of_points

138

〈class 't v t k .t v t k _ c l a s s e s .t r i a n g l e _ s t r i p .T r i a n g l e S t n i p '>

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 库 中 的 VectorNorm 。它计算输入数据的 v e c t o r 数组中各个矢量的长度,


并且保存到 scalars数组中。只需将之前程序中的grid 修改为 vnorm.output_p o it 即可:

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 条相贯线,下而我们用 P d yD ata 的布尔运算功能生成两


个互相■直的圆管,并计算它们的相贯线。程序的运行结果如图8-19所示:

图8 - 1 9 两个互相垂直的圆管(左),打通圆管并显示相贯线(右)
Python 科学计算 (第 2 版)

scpy2.tvtk.example_tube_ intersection: 计算两个圆管的相贯线,可通过界面中的滑块控


件修改圆管的内径和外径。

首 先 定 义 make_uibe()函数,它创建指定方向和大小的圆管,生成圆管的流水线如阁8-20
所示。

阁 8-20生成圆管的流水线

def make_tube(height, radius, resolution, rx= 0 , ry= 0 , rz= 0 ) :

csl = tvtk.CylinderSounce(height=height^ radius=radius[0 ],resolution=resolution) O


cs2 = tvtk.CylinderSounce(height=height+0.1, radius=radius[l], resolution=resolution)
trianglel = t v t k.TriangleFilter(input_connection=csl.outputj 3 〇
rt) ©
triangle2 = tvtk.TriangleFilter(input_connection=cs 2 .outputj 3 〇
rt)
tr = tvtk.Transform()

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

return bf, a, tfl, t f 2

tubel, tubel_actor, tubel_outer, tubel— inner = make— tube(5, [1, 0.8], 32)

tube2, t u b e 2 _ a c to r <, tube2_outer^ tube2_inner = make_tube(5^ [0.7, 0.55], 3 2 ^ rx=90)

win = ivtk— scene([tubel 一actor, t u b e 2 一actor])

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 属性是一个描述旋转、偏

移以及缩放操作的T r a n s f o r m 对象。O 最后使用 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 计算飢圆柱体勹细


圆柱体的差分布尔运算,得到表示圆管的 P o l y D a t a 对象。为了让差分布尔运算能正常工作,在
创建圆柱时让细圆柱比粗圆柱略微 L<:一些。由于布尔运算需要两个输入对象,因此需要调用
set_input_connection() 来指定母个输入端口的输入对象。

由 图 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"

bf.set 一input 一c o n n e c t i on (0 , pdl.outputjD 〇


rt)

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

def intersection (pdl, pd 2 , color=( 1 .0 , 0 , 0 )., wid t h = 2 .0 ):

ipd = t v t k .IntersectionPolyDataFilten()

ipd.set_input_connection( 0 , p d l .o u t p u t _ p o r t )

ipd.set 一input 一c o n n e c t i on (1 , pd 2 .output_port)


Python 科学计算 (第 2 版)

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, tubel_hole 一actor = difference(tubel, t u b e 2 一inner)

t u b e 2 _hole, t u b e 2 _hole_actor = difference(tube 2 ^ tubel_inner)


intersGcting_linG, intersecting_line 一actor = intersection(tubel^ tube2)

tubel_hole_actor.property.opacity = 0 .8

t u b e 2 _hole_actor.property.opacity = 0 .8

tubel— hole— actor.property.color = 0.5, 0.5, 0.5


tube2_hole_actor.property.color = 0.5, 0.5, 0.5

win = ivtk_scene([tubel_hole_actor, t u b e 2 _ h o l e _ a c t o r > intersecting^line 一actor])

w i n .s c e n e .is 〇
T V T与

fnetric_view()
K

intersectingjine 足 由 1624条线段构成的 PolyData 对象,如果希望获得8 条表示相贯线的曲


IVlayav

线,还需要做进一步处理。
丁数据的三维可视化

print i n t e r s e c ti n g _ l i n e .o u t p u t .p o i n t s .number_of jDoints

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

IntersectionPolyDataFilter输出的 PolyData 对象中,有一些点的坐标非常接近,它们应该是一


个点,但是由于计算误差,在 PolyData 中被表示为多个点。下面用 scipy.spatial.distance中的 pdist()
和 squareform()计算所有点之间的距离,并显示最小的10个距离:

from scipy.spatial import distance

dist = distance.squareform( distance.pdist(intersecting_line.output.points.to_array 〇) )

d i s t [ n p .diag_indices(dist.shape[0 ])] = np.inf

dist = d i s t . r a v el ()

print n p . s o r t ( di s t ) [: 1 0 ]

[ 1.60932541e-06 1.60932541e-06 1.66893005e-06 1.66893005e-06

3.23077305e-06 3.23077305e-06 3.23077305e-06 3.23077305e-06

3.72555819e-06 3.72555819e-06]

下面使用 ClearPolyData 对 PolyData 进行淸理丁.作,tolemnce_is_absolute 为 True 时,使用绝


对阈值 a b s d m td e r n n c e 对坐标点进行合并,可以看到合并之后的点数减少了:
cpd = tvtk.CleanPolyData(tolerance 一is 一
absolute=True,
absolute 一
tolerance=le-5,
input_connection=intersecting_line.output_port)

cpd.update()
c p d .o u t p u t .p o i n t s .number_of jDoints

1578

下而的 connect_line()根 掘 PolyData.lines属性中的信息,


将线段合并成曲线,
并使用 matplotlib
的 三 维 绘 阁 功 能 绘 制 合 并 之 后 的 8 条 llll线 ,如 阁 8-22所 示 。请感兴趣的读者自行分析
connect_line〇的算法,这里就不多做解释了。

from collections import defaultdict

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

poly = [e d g e s .i t e r k e y s ().n e x t ()]


while True:

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

edges = {k:v for k,v in edges.iteritems() if v}

from mpl_tcx)lkits.mplot3d import Axes3D

fig = pl.figure(figsize=(5, 5))


ax = f i g . g c a ( projection='3d')

points = cpd.output.points.to 一a r r a y ()
P y th o n 科学计算(第 2 版)

for line in connect 一lines(cpd.output.lines):

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快速绘图

与本节内容对应的 Notebook 为:OS-tvtkjnayavi/tvtkjnayavi-GOO-mlab.ipyrL

最新版本的Mayavi 4.4.0中存在G U I 操作不更新3 D 场景的问题,可以通过本书提供


A 的 scipy.tvtk.fix_mayavi_bugs 〇修复这些问题。

虽 然 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 编

写扩展,嵌入到用户编写的 P y t h o n 程序中,并且提供了而向脚木的 m l a b 模块,以方便用户快


速绘制三维图。
和 matplotlib 的 p y l a b —样,
M a y a v i 的 m l a b 模块提供了方便快徒的三维绘制函数。

只耍将数据准备好,通常只需要调用一次 m l a b 模块的绘图函数,就可以看到数据的三维品示

49
效果,非常适合在 IPython 中交互使用。

8 . 5 . 1 点和线

三维空间中独立的点使用point3d 〇绘制,而 plot3d 〇则将一系列的点连接起来绘制三维|丨1丨线


它们可以有 3 个或4 个数组参数。这些数组的形状完全相同,前 3 个参数 x 、y 、z 对应点的 X 、
Y 和 Z 轴的坐标值;而如果有第4 个参数 s , 它指定每个坐标点所对应的数值(标M 值)。point3d 〇
的第4 个参数还可以是通过坐标计算标量值的函数。下而是这两个函数的调用格式:

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线使用很细的圆管绘制

from scipy.integrate import odeint


Python 科学计算 (第 2 版)

def lorenz(w, t, p, r, b ) :

x, y, z = w

return np.array([p*(y-x), x*(r_z)_y, x*y-b*z])

t = np.arange(0, 30, 0.01)

trackl = odeint(lorenz, (0 .0 , 1 .0 0 , 0 .0 ), t, args=( 1 0 .0 , 28.0) 3.0)) O

from mayavi import mlab

X, Y, Z = trackl.T

mlab.plot3d(X, Y, Z, t, tube 一radius=0.2) ©

mlab.show()

O 使 用 oddm ()得到的轨迹数据 tra c k l 是一个二维数组,它 的 第 0 轴的长度是轨迹的点数,

第 1轴的长度为3 , 分别为轨迹上每点的X 、Y 、Z 轴坐标。


© 将 trn d d 拆分为三个一维数组之后,调 用 p bt 3d()绘制轨迹曲线。这里用时间数组 t 作为
T V T与

标量值数组,冈此轨迹上每个点所对应的标量值就是到达此点的时间。tube_radius 参数设置曲
K

线的粗细,曲线实际.丨
:采用极细的岡管绘制。
Mayavi数

在三维场景中可以使用键盘和鼠标对场M 照相机或场M 中的物体进行操作:


•照 相 机 模 式 :此模式为缺省模式,在此模式下所有的鼠标操作都是针对照相机进行,
- 据的三维可视化

按 “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

<mayavi.core.scene.Scene object at 0xl3A320F0>

(0.5, 0.5, 0.5)

L in e S o iirc e : 线数据源。在其配置界面中,笫一项为每个点所对应的标量数据的名称,在
本例中只有一个名为s c a l a r 的标量数据,它就足我们传递给 plot3d〇的第 4 个数组:表示轨迹中
每点的时间的数组t 。下面的语句从场景中获取LineSource 对象,并且获取其中的各种数据:

source = s.children[0] # 获得场景的第一个子节点,也就足LineSource


print repr(source)
print source.name # 节点的名字,也就流水线中显示的文字
print r e p r ( s o u rc e . d a t a .points) # LineSource 中的坐标点

print r e p r ( s o u rc e . d a t a •p o i n t _ d a t a •scalars) # 符个点所对应的标丨丨丨:数组

<mayavi.sources.vtk_data_source.VTKDataSounce object at 0xl3A06CF0>


LineSource
[(0.0, 1.0, 0.0), •••, (0.021550891680468726, 1.6938271906706417, 20.31711497016887)],
length = 3000

[0.0, .... 29.99], length = 3000

S tr ip p e r : 根据内部 filter对象的 m a x i m u m j e n g t h 属性对线数据源进行分段处理。


在本例中,
输入的线数据源有 3 0 0 0 个点,而 m a x i n u i m j e n g l h 属 性 为 1 0 0 0 , 即 每 1 0 0 0 个点将对应一条线。
Python 科学计算 (第 2 版)

因此 strippei•对象输出的P o l y D a t a 对象中有 3 条线:

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

print strippe r .o u t p u t s [0 ] .number_ofj 3 〇


ints
print r e pr(stripper.outputs[0 ])
print s t r i p p e r .o u t p u t s [0 ].number_of_lines

1000
3000
<tvtk.tvtk_classes.poly_data.PolyData object at 0x0CD527B0>

T u b e : 将输入的 PolyData 数据中的每条线转换为表示三维圆管的PolyData 数据。它的配置


界面中有许多参数可以改变生成岡管的方式。例如:将 ”Vary radius"设置为nvary_radius_by_scalar’
',
贝帽管的粗细由每个点对应的标量值决定。而加粗的比例则由MRadius factor"参数决定,我们将
其设置为3,于是此圆管最籼处(终点)的半径是最细处(起点)的3 倍。
下而的语句获得Tube 对象,
T V T与

并查看输出对象的类型:
K

tube = stripper.children[ 0 ] # 获得T u b e 对象


IVlayav

print repr(tube.outputs[0]) # t u b e 的输出是一个P o l y D a t a 对象,它楚一个三维圆管


丁数据的三维可视化

<tvtk.tvtk_classes.poly_data.PolyData object at 0x0CD52210>

Colors and legends : 在其配:1!界而中的’'Scalar L U T '选项卡中可以设置将标S 值转换为颜色


的查询表(Look Up Table)。例如,将’

L u t m o d e ^ S Blues , 于是场景中的圆管完成了从白色到深
蓝色的渐变。勾选"Show legend”
选择框,在场景中将添加一个颜色条来®示颜色和标量值之间
的关系。也可以通过下面的程序修改这两个选项:

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'

m a n a g e r .sc a l a r _ l ut _ m a n a g e r .show_legend = True

Surface : 它 将 T u be 所输出的 PolyData 数据转换为最终在场景中显示的三维实体。通过其


配置界面的nActor"选项卡,可以对实体进行配置。例如,将"Representation选择为"wireframe",
并将"Line width”
设置为0,则实体采用细线框模型M 示。将"Opacity"设置为0.6,则实体变为半
透明状态。也可以通过下面的程修改这两个配置:

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

face = mlab.surf(x, y, z, warp_scale=2) ©


axes = mlab.axes(xlabel='x、 ylabel='y', zlabel: , color=(0, 0, 0)) O
outline = mlab.outline(face, colon=(0, 0, 0))

501
Python科学计算(第 2 版)
T V T与

图8-26 surfO绘制的III丨而及流水线对话框
K

〇先通过 ogrid 对象计赏两个形状分别为(20,1)和(1,


20)的数组 x 和 y 。
© 然G 通过广播运算,
M ayavT

计算出由 x 和 y 构成的等距网格上每点的函数值z , 它是一个形状为(20, 20)的数组。© 接着调


数据的三维可视化

用 mlab.surf 〇, 将数组2绘制成三维空间中的曲面,所绘制的曲面在 X -Y 平面上的投影是一个


等距离网格。© 最后通过 mlab.axes() 和 mlab.outline 〇,分别在三维场景中添加坐标轴和曲面区域
的外框。需要注意的是,与 niatplotlib相反,在 M a y a v i 屮二维数组的第0 轴表示 X 轴 ,第 1 轴
表 不 Y 轴。
仔细观察丨⑴面的颜色就会发现颜色和丨11|面的高度有 一 一 对应的关系,llll面上的点越高,颜
色越红,越低则颜色越蓝。打开流水线对话框,川’
以看到数据源楚一个 Am iy 2DS 〇
ur c e 对象。

它的输出是一个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

< t v t k •tvtk 一c l a s s e s •image 一d a t a •ImageData at 0xl3a243f0>

ImageData 对象是一个表示三维阁像的数掘集。在 ImageData 对象中,实际上不保存空间中


每点的坐标,
而是通过 origin、
spacing 和 dimensions等属性计算出一个三维空间中的等距离网格,
网格中的每个点所对应的标量值保存在point_data.scalars 中,这些值决定/ 丨11|而的高度和颜色。

print img .origin # X 、Y 、 Z 轴的起点


print img .spacing # X 、Y 、 Z 轴上点的间隔

print img.dimensions # X 、Y 、 Z 轴上点的个数

print repr(img.point_data.scalars) # 每个点所对应的标® :值


[-2 . -2 . 0 .]
[0.21052632 0.21052632 1. ]
[20 20 1]
[-0.000670925255805, •••, 0.000670925255805], length = 400

通过上而的分析可以看出,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 )

extent参数是一个有6 个元素的序列,分别指定 X 轴最小值、X 轴最大值、Y 轴最小值、 Y


轴最大值、Z 轴最小值、Z 轴最大值。这些值将修改数据范围,因此所绘制曲面的 X 、Y 轴范
围相同,而曲面在 Z 轴上的高度是 X 、Y 轴范围的一半,效果如图8-27(中)所示。由 于 extent
参数所改变的是数裾的范围,因此坐标轴上的刻度值也随之发生变化。为了解决这个问题,可
以给 axes()传 递 ranges 参数:

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

ranges 参数也是一个有6 个元素的序列,分別指定三个坐标轴上的刻度范围,效果如图


8-27(右)所示。

图8 - 2 7 修改坐标轴的显示比例

除 surf〇之外,i m s h o w ( ) 和 contour_surf 〇也是可视化二维图像的工具。i m s h o w ( ) 将二维图像

放在三维空间中显示,它与将 surf()的 w a i p _ s c a l e 参数设置为 0 的效果一样。下而的语句将二维


数 组 z 用 i m s h o w ( ) 绘制成图,效果如图 8-28( 左)所示:
T V T与

x, y = np.ognid[-2:2:20j, -2:2:20j]
K

z = x * np.exp( - x**2 - y**2)


M ayavT

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

如果需要绘制更复杂的三维曲面,可以使用 mesh〇。下面是使用 mesh〇绘制复杂曲面的程


序 ,结果如图8-29所示:

f rom numpy import sin, cos

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 =

r = sin(m 0 *phi)**ml + cos(m 2 *phi)**mB + sin(m4*theta)**m5 + cos(m6*theta)**m7 O


x = r*sin(phi)*cos(theta) ©

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旋转体

© 程序中调用 mesh〇绘制曲面,它 和 smf()类似,其三个数组参数 x 、y 、 z 都是形状相同的


二维数组。这些数组的相同下标的三个数值组成曲而上某点的三维坐标。点之间的连接关系(边
和面)由其在x 、y 、z 数组中的位置关系决记。Olll 丨面上各点的坐标在球坐标系中计算,© 然后
按照坐标转换公式将球坐标系转换为笛卡尔坐标系。
为了方便读者理解mesh()是如何绘制出_ 面的,下面通过手工输入坐标的方式,绘制如图
8-30所示的立方体表面的一部分:

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 ]]

box = mlab.mesh(Xj y, z, representation="surface")


mlab.axes(xlabel='x', ylabel='y’,zlabel='z')
mlab.outline(box)
mlab.show()

a
T V T与
K
M ayavT
数据的三维可视化

1.001.00
阁 8-30组成立方体的各个而和顶点坐标

传递给 mesh()的三个数组 x 、y 、z 中下标相同的三个数字组成一个三维坐标,因此这三个


数组实际描述的坐标点为:

[
[(-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()

旋转而上点的坐标很容易在圆柱坐标系(p, 中, z) 中计算。O 首先在(p, 中)平而中创建一个


40 X 4 0 的二维网格。© 通过p 计算出抛物而上每点的卨度z 。由于是旋转而,因此高度和中无关。
© 将圆柱坐标系转换为直角坐标系,得 到 mesh()所需的三个数组。
还可以使用 mesh()绘制出 surf()所绘制的丨1丨
|面。下面是用 mesh()绘制丨111面的程序:

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

〇这里使用 mgrid对象产斗:数 组 x 和 y 。这是因为用 m e s h 〇绘制 llllllij时,必须给出丨Ulliil上每


点的坐标值,因此 x 和 y 参数不能是 ogrid产生的数绀。
© 为了演示 meshO 绘制曲面的优点,我们另外计算一个二维数组c ,
© 并且把它传递给 mesh 〇
的 scalar参数。于是曲面上每点所对应的标量值将使用数组c 中 相 应 K 标的值。这样可以使用
数 组 c 对曲而上的每点进行着色,得到一个颜色和高度无关的曲而,它能比 surf()所绘制的曲而
表达更多的信息。阁 8-32为程序绘制结果及对应的流水线。
T V T与
K
M ayavT
数据的三维可视化

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

face = mlab.surf(x, y, z, warp_scale=2 )


mlab.axes(xlabel='x ', ylabel='y', zlabel='z')
mlab.outline(face)

我们需要为每个点添加一个新的标量值以控制它们的颜色。因此,首先获得流水线中
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"

修改了 A m i y S o u r c e 对 象 的 输 入 数 据 之 后 ,调 用 updateO计 算 其 输 出 数 据 ,并设置


pipeline_changed 事件屈性,让流水线上后续的对象更新它们的输入输出。

source.update()
source.pipeline一changed = True

如果读者对为什么需要转置有疑问,查看- •下数组z 在 ImageData 对象屮的保存顺序,可

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 和 Colors and legends 之间插入一个 SetActiveAtlribute


对象,它 将 PolyDataNormals的输出数据中名为"color"的标量数组设置为当前标量数组。下而先
获得 PolyDataNormals 对象:

normals = mlab.gcf().children[0 ].children[0 ].children[0 ]

通过下面的语句可以看到,PolyDataNormals输出的 PolyData对象的当前标量数组为数组z :

normals.outputs[0 ].point_data.scalars.to_array()[:3]
array([-0.00067093, -0.00133304, -0.00239035])

接下来是插入操作。首先获得 n o m a l s 的下一级对象,并将其从 children列表中删除:

surf = normals.children[0 ]
del normals.children[0 ]

然后调用 pipeline.set_active_ attribute〇,创建 一■个 SetActiveAttribute 对象并将其添加进


noimakchildrcn列表。通 过 point_ scalars参数将名为"color"的数组设置为缺雀标量值:
09
Python 科学计算(第 2 版)

active一attr = mlab.pipeline.set一
active一attribute(normals, point一scalars="color")

最后将 surf 对象添加进 SetActiveAttribute 对象的子列表:

active_attr.children.append(surf)

可以看到,现 在 PolyDataNormals输出的 PolyData对象的当前标量数纟£ 已经变为数组c 了:

normals.children[0 ].outputs[0 ].point_data.scalars.to_array()[:


3]
array([-6 . , -5.57894737, -5.15789474])

于是其后的颜色查询表将使用数组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〇绘制高度和颜色不同的曲面

也可以不调用 surfO, 而直接创建流水线中的每个对象。完整的程序如下:

src = mlab.pipeline.array2d_source(x, y, z) #创述 ArraySource 数据源


#添加color数组
image = src.image_data
array 一
id = image.point_data.add_array(c.T .ravel())
image.point_data.get_amay(array_id).name = "color"
src.update() #更新数据源的输出

# 创建流水线上的后续对象
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)区间的电势场:

x, y, z = np.ogrid[-2:2:40j, -2:2:40j, -2:0:40j]


s = 2 /np.sqrt((x-l) * * 2 + y* * 2 + z**2 ) + l/np.sqrt((x+l) * * 2 + y * * 2 + z**2 )

使 用 contour3d() 可以快速绘制此电势场的等位面:

surface = mlab.contour3d(s)

缺省情况下,contour3d 〇绘 制 5 个等分标M 值范围的等值而,它不能很好地显示整个电势


场的结构,因此用下而的语句修改等值而的范围、数 H 以及透明度等属性:

surface. contour •maximum_contour = 15 # 等值面的上限值为 15


sunface.contour.number_of_contours = 10 # 在M 小值到 15 之间绘制 10 个等值面
surface.actor.property.opacity = 0.4 # 0.4

这些属性也可以通过流水线对话框来修改,程序的绘制结果如图 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)

此程序首先通过 scalar_fidd()在流水线中创建一个标M 场数据源,然后通过 volumeO 将此数


据源用体素呈像进行可视化,效果如图8-35(左)所示。
由于电势强度随着距离的平方袞减,因此整体的润色效果并没有突出电势强的部分。为了

解决这个问题,可以给 voliimeO传递两个关键字参数:v m i n 和 v m a x 。它们指记标量值的润色范


围,即只绘制标量值在v m i n 到 v m a x 之间的区域:

mlab.pipeline.volume(field, vmin=1.5, vmax=1 0 )

效果如图8-35(右)所示,它很清楚地表现出了电荷附近的电势情况。

■ ^ ||‘卜

图8-35用体素呈像可视化电势场:(
左)缺€
1'效果,(
右)通过vmin和 vmax 指定电势值的润色范围

512
还可以使用切片工具观察标量场在某个平面之上的数据,它通常和艽他的工具同时使用,
并且可以直接在三维场景中交互式地改变平面的位置和方向。下面的程序在流水线中添加一个
标量切而,在流水线中它是"〇)1〇1'5 01^咕611(^"的子节点。通过卩1〇116_01*161伽丨011参数指定切面的
法线方向为 Y 轴 ,即切面和 Y 轴垂直:

cut = mlab.pipeline.scalan_cutj3 lane(field.children[0 ]^ plane_orientation=,,y_axes")

然后通过下而的程序设置切面工A 的一些属性:

cut.enable_contours = True # 开启等高线显示


cut.contour.number_of_contours = 40 # 等高线的数目为 40

切而工具的效果如图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 :

p, n, b = (10.0, 28.0, 3.0)


x, y, z = np.mgrid[-17:20:20j, -21:28:20j, 0:48:20j]
u, v, w = p*(y-x), x*(r-z)-y, x*y-b*z

这三个速度分量构成•-个矢量场,
下面调用 quiver3d 〇将每个点的速度欠量用一个箭头表示:

vectors = mlab.quiver3d(x, y, z, u, v, w)

效果如图8-37(左)所示。由于矢量场数据的网格过密,无法看清矢量场的内部结构。此吋
可以州下丨丨11的语句修改 V e c t o r s 对象的一些属性以减少箭头的数量并增加箭头的长度,效果如
图8-37(右)所示。

vectors.glyph•mask_input_points = True # 开启使jTj部分数据的选项


vectors.glyph•mask_points•on_ratio = 2 0 # 随机选择原始数据中的1 /2 0 个点进行描绘
vectors .glyph, glyph •scale_factor = 5.0 # 设置箭头的缩放比例
T V T与

^So 〇
n» 2
K

Cdorv and l««»rdE


M ayavT

^ 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

图 8-38( 左)是矢量切面工具和等模值面的显示效果。下面的语句分别获取m a g n i t u d e 所输出


的 I m a g e D a t a 对象的标量数组和矢量数组:

print repr(magnitude.outputs[0 ].point_data.scalars)


print repr(magnitude.outputs[0 ].point_data.vectors)
[579.71887207, •••, 602.195983887], length = 8000
[(-40.0, -455.0, 357.0), •••, (80.0, -428.0, 416.0)], length = 8000

最后,还可以使用 fl〇w ()观察洛伦茨吸引子的轨迹。它相当于绘制矢M 场的场线,空间中


每点所对应的矢量等于过此点的场线的切线方向:

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.6.1 TVTK 场景的嵌入

在下而的例子中,⑴户讨以通过如图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界血'

scpy2.tvtk.example_embed _ tube: 演 示 如 何 将 T V T K 场 景 嵌 入 TraitsUI界 面 ,可通过界


面中的控件调节圆管的内径和外径。

from traits.api import HasTraits, Instance, Range, on_trait_change


from traitsui.api import View, Item, VGroup, HGroup, Controller
from tvtk.api import tvtk
from tvtk.pyface.scene一editor import SceneEditor
from tvtk.pyface.scene import Scene
from tvtk.pyface.scene_model import SceneModel

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

@on_trait_change("radiusl, radius2 ") O


def update一radius(self):
self.csl.radius = max(self.radiusl, self.radius2 )
self.cs2 .radius = min(self.radiusl, self.radius2 )
self.scene.render_window.render()

if 一name_ == " _ main一


app = TubeDemoApp()
app.configure一rtaits(handler=TVTKSceneController(app))

OSceneModel 表 示 T V T K 的场景模型,它 在 M V C 模式中属于模型对象,因此程序中用它


定义 Trait厲 性 scene。
©scene 屈性在界面中将呈现为一个T V T K 的三维场景,因此在视图的Item泣义中,
用 editor
Python 科学计算(第 2 版)

参数指定一个编辑器,让E l 拒正确显示 scene 所代表的模型。SceneEditor是用来创建场景编辑


器的工厂类,通 过 scene_class参数指定真正创建场景的对象类Scene。
© 在控制器的 position()方法中调用模型对象的plot()方法以生成场景中的物体。position()方
法在窗口显示之前调用,此时界而中的所有控件都己经被创建,因此能保证 plot()中的语句正确
运行。
O 通 过 on_trait_change()响应属性 radiusl和 radius2 的变化事件,修改流水线最上层的两个
CylinderSource 对象的 radius 属性,并调用 self.sc^ ne.render_window .rendeit)以刷新场】
?U 因
户通过界面中的控件修改半径时,场景中的圆管会同步更新。

8.6.2 Mayavi场景的嵌入

下丨立1看一个Mayavi 场景嵌入的例子。用户输入一个由 x 、y 、z 等变量构成的表达式,例如


x *x + y *y + z*z 。程序使用此表达式计算指定范围之内的三维标量场,并且添加等值面和切面工具
对标量场进行可视化。等值面的数值可以自动计算,也 可 以 通 过 界 而 上 的 滚 动 条 配 而 切 面
的位置和方昀则可以直接在场鼠中用鼠标操作。程序的界面如图8 . 所示。
T V T与
K

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

width = 500, resizable=True, title=u"三维标量场观察器"


)

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

if —name— == '___main— ':

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^ 的 数 组 对 象 表 示 图 像 。为

了兼容 O p e n C V 1.x A P I , 在 c v 模块下提供了原来的 O p e n C V 1.x A P I 的扩展挥。本章所介绍的代


码均采用如下方式载入这两套接口的模块:

import cv2
from cv2 import cv

9 . 1 图像的输鳩出

与本节内容对应的 Notebook 为:0 9 - o p e n c v / o p e n c v - K X M n p u t - o u t p u t . i p y n b 。

本节介绍如何使用图像的输入输出函数。由 于 c v 2 模块中的函数使用 N u m P y 数组表示图


像,因此很容舄将从文件中读入的图像数据传递给其他截于N u m P y 数组的扩展辟,也可以调
用 c v 2 提供的图像函数对 N u m P y 数组进行处理。

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

imread()函数读入彩色图像时,第 2 轴按照蓝、绿 、红的顺序排列。这种顺序与 matplotlib


A 的 imshow 〇函数所需的三通道的顺序正好相反。因此,
若需要在 matplotlib中显示图像,
则需要反转第2 轴:img [:,:,::-1]。

〇最后调用 cv.w a i t K e y ( O ) 等待用户按下按键,其参数为等待的毫秒数,0 表示永远等待。


O penCV图

在 IPythonNotebook显示窗口时,waitKey(O )会阻塞运算核,因此无法再运行其他命令。
- 像处理和计

为了不阻塞运行,可以在新的线程中显示窗口,并等待窗口关闭,例如通过本书提供
的%% thread魔法命令。

IT -
机视觉

下而的程序将i m g 表示的彩色阁像通过c v t C o l o r O 转换成表示黑白阁像的二维数组i m g ^ r a y ,


其第二个参数为颜色转换常数,所有的颜色转换常数均以 C O L O R _ j r * ,可以使用 I P y t h o n 的
自动完成功能或者本书提供的%记沉( ± 魔法命令搜索颜色转换常数。B G R 2 G R A Y 表示将B G R

顺序的彩色图像转换为灰度图像。

cv2 模块下的图像处理函数都能直接对 N u m P y 数组进行操作,在这些函数内部会将


N u m P y 数组转换成 O p e n C V 中表示图像的对象,
并传递给实际进行运算的C / C + + 函数。
W 虽然在调用cv2 模块下的函数时会进行类型转换,
但 是 N u m P y 数组与 O p e n C V 的图像
对象能够共享内存,因此并不会太浪费内存和C P U 运算时间。

img gray = cv2.cvtColor(img, CV2.C0L0R一BGR2GRAY)


print img^ray.shape
(512, 512)

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 文件:

img = cv2 .imread("lena.jpg")


for quality in [90, 6d, 30]:
cv2 .imwrite("lena_q{:0 2 d}.jpg".format(quality), img,
[cv2.IMWRITE_JPEG_QUALITY, quality])
2
5
Python 科学计算(第 2 版)

当图像格式支持更高的通道比特数时,imwriteO会保持图像数组的精度。在下面的例子|丨I,
O 通 过 N u m P y 计箅出下面的二元函数的值:

(x2 - y 2)sin(^—
f(x, y ) = ------------ -------

©然 后 调 用 matplotlib 的 S c a l a r M a p p a b l e 〇,使用颜色映射表jet 把闲数值转换成彩色图像,


注 怠 O p e n C V 的所有输入输出函数都采用蓝绿红的通道顺序。由 于 to_rgba 〇的输出为元素值在
0 到 1 之间的浮点数数组,因此还需要将其转换成整数数组。

fro m m a tp lo tlib .c m im p o rt S calarM appable


fro m IP y th o n .d is p la y im p o rt Image

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图

d e f make—im a ge(x, y, a, d t y p e = " u in t 8 " ) :


z = fu n c ( x , y , a) O
img_rgba = S c a la rM a p p a b le (c m a p = "je t").to _ n g b a (z )
- 像处理和计筇机视觉

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可以将数组中的二进制数据转换成字符串。

with open("img_8 bit.png", "rb") as f:


png_stn = f.read()

png_data = np.frombuffer(png_str, np.uint8 ) O


img = cv2.imdecode(png_data, cv2.IMREAD_UNCHANGED) ©
res, jpg_data = cv2 .imencode(".jpg", img) ©

jPg-Str = jpg_data.tobytes〇 O

可 以 使 用 I m a g e 将 编 码 之 后 的 图 像 数 据 嵌 入 IPython N o t e b o o k 中 。在下丨1l]的 例 子 中 ,将编

码之后的字符串数据传递给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 使丨HImage将 imencodeO编码的结來直接嵌入 Notebook中

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〇方 法 关 闭 视 频 文 件 。

def test_avi_output(fn, fourcc):


fourcc = cv.FOURCC(*founce) O
Python 科学计算(第 2 版)

vw = cv2.VideoWriter(fn, fourcc, 15, (500, 250), True) ©


if not vw.isOpened():
return
for a in np.linspace(0 .1 , 2, 1 0 0 ):
img = make一image(x, y, a)
vw.write(img) ©
vw.release() O

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 编 码 器 的 下 载 地 址 。

x 264v f w 编码器的配ffi保存在注册表的H K E Y _C U R R E N T _U S E R \Software\G N U \x264路径下。


木书提供了 set_qUamizeit)来设置其编码画质,
读者可以仿照该程序,根据需要修改其他项的值。
下面的程序输出不同的Pi质选项所得到的视频文件大小:

from scpy2.opencv.x264_settings import set_quantizer


from os import path

for quantizer in [1, 10, 20, 30, 40]:


set_quantizer(quantizer)
fn = "x264_q{:02d}.avi".format(quantizer)
test_avi_output(fnJ "x264")

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

print "CURRENT FRAME:", video.get(cv.CV_CAP_PROP_POS_FRAMES)


res, frame50 = video.read()

print "CURRENT FRAME:% video.get(cv.CV_CAP_PROP_POS_FRAMES)


video.release()
FPS: 15.0
FRAMES: 100.0
WIDTH: 500.0
HEIGHT: 250.0
CURRENT FRAME: 0.0
CURRENT FRAME: 1.0
CURRENT FRAME: 50.0
CURRENT FRAME:51.0

当传递给 V i d e o C a p t u r e 的参数是整数吋,将打开该整数对应的视频设备。下面的程序从笔
者的笔记本电脑0 带的摄像头读収一帧图像:

camera = cv2.VideoCapture(0)
O penCV图

res, frame = camera.read()


camera.release()
- 像处理和计筇机视觉

print res, frame.shape


True (480, 640, 3)

9 . 2 图像

与本节内容对应的 Notebook 为 :09-opencv/opencv-200~imgprocess.ipynb。

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 〇来完成图像的卷积运算,调用方式如下:

filter2D(src, ddepth, kerneldst[, anchor^ delta[, borderType]]]])

其 中 src参数是原始图像,dst参数是目标图像。若 畨 略 dst参数,将创造一个新的数组来
保存图像数据。ddepth 参数用于指定 F!标图像的每个通道的数据类型,负数表示其数据类型和
原始图像相同。kernel参数设置卷积核,它将与原始图像的每个通道进行卷积计兑,并将结果
存储到目标图像的对应通道中。anchor参数指定卷积核的锚点位置,当它为默认值(-1,-1)时,
以卷积核的中心为锚点。delta参数指定在将计算结果存储到dst中之前对数值的偏移量。
filter2D 〇的卷积运算过程如下:
(1)对于图像 src中 的 每 个 像 素 点 让 它 和 卷 积 核 的 锚 点 对 齐 。
(2) 对于阁像 src中勹卷积核東抒的部分,计算像素值和卷积核的值乘积。
(3) 图像 dst中的像素点(x ,y )的值为上而所有乘积的总和。
显然当卷积核的尺寸很大时,上述方法的运算速度将会很慢。因此对于较大的卷积核,
filter2D ()将使用离散傅立叶变换相关的算法进行卷积运算。
下面的程序演示了使用不同卷积核对图像进行处理之后的效果,如 图 9-3所示。

src = cv2 .imread("lena.jpg")

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() 来进行这种分步卷积,调用参数如下:

sepFilter2D(src, ddepth, kernelX, kernelY[, dst[, anchor[, deltaborderType]]]])

其中 k e m e l X 和 kernel Y 分别为行卷积核和列卷积核。
下面的程序比较filter2D()和 sepFilter2D()
的计算速度:

img = np.random.rand(1 0 0 0 ,1 0 0 0 ) O
〇pencv图

row = cv2.getGaussianKernel(7, -1) ©


col = cv2.getGaussianKernel(5> -1)
- 像处理和计筇机视觉

kernel = np.dot(col[:]^ row[:].T) ©

%time img2 = cv2 .filter2 D(img> -1, kernel) O


%time img3 = cv2.sepFilter2D(img, -1, row, col) 0
print "Gnror=,,<> np.max(np.abs(img2[:] - img3[:]))
Wall time: 31 ms
Wall time: 25 ms
error= 4.4408920985e-16

〇首先随机产生一幅比较大的图像 i m g 。 © 调 用 g e t G a u s s i a n K e m e l ( ) 分别获得长度为 7 和 5

的两个高斯模糊卷积核r o w 和 c o l 。 © 计 算 r o w 和 c o l 的矩阵乘积 kernel ,它是一个形状为(5, 7)

的二维数组。O © 分別使用 filer2D 〇和 s e p F i l t e r 2 D 〇对 图 像 i n i g 进行卷积,并测量它们的计兑

时间。
卷积核的尺寸越大,计兑时间的差别越大。请读者更改 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()的调用参数相同:

dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])

其 中 src 参数是原始图像;kernel参数是结构元素,它指定针对哪些周围像素进行计算;
a n c h o i * 参数指定锚点的位置,其默认值为结构元素的中心;iterations 参数指定处理次数。

m o r p h o l o g y E x ( ) 的参数如下:

morphologyEx(src, op, kernel[, dst[, anchoriterationsborderType[, borderValue]]]]])

它 比 dilate()多了一 t" o p 参数,用于指定运兑的类型。


膨胀运兑可以用下面的公式描述:
dst(x ,y ) =
一 eT(xa, W 〇
src(x + x’
'y + y ’
)

将结构元素的锚点与原始图像中的每个像素(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 是界面截图。请读者通过此界面修
改结构元素、处理类型以及迭代次数等参数,并观察经过处理之后的图像,理解各种运兑的
公式。

scpy2.opencv.moiphology_demo: 演示O penCV 中的各种形态学运算。


Python 科学计算(第 2 版)

阍9 4 形态学阁像处理演示界而

9.2.3 填充-floodFill
o p e n c v图

填充函数 floodFillO在图像处理中经常用于标识或分离阁像中的某些特定部分。它的调用方
式为:
- 像处理和计算机视觉

floodFill(image, mask, seedPoint, newVal[, loDiff[, upDiff[, flags]]])


其 中 image 参数是需要填充的阁像;seedPoint参数为填充的起始点,我们称之为种子点;
n e w V a l 参数为填充所使用的颜色值;loDiff和 upDiff参数是填充的下限和上限容差;flags参数
是填充的算法标志。
填充从 seedPoint指定的种子坐标开始,图像中与当前的填充区域颜色相近的点将被添加进
填充区域,从而逐步扩大填充区域,直到没有新的点能添加进填充区域为止。颜色相近的判断
方法有两种:
•默认使用相邻点为莶点进行判断。
• 如 果 开 启 了 flags中 的 F L 0 0 D FILL _ F 1X E D _ R A N G E 标志位,则以种子点为基点进行判
断。
假设图像中某个点(x ,y )的颜色为C (x ,y ), C Q 为;
®点 颜 色 ,贝ij下面的条件满记时,(
x ,y )将
被添加进填充区域:
C 〇—loDiff < C (x ;y ) < C 〇+ hiDiff
此外还可以通过 flags指定相邻点的定义:四连通或八连通。
当 m a s k 参数不为 N o n e 时,
它是一个宽和高比image 都人两个像素的中.通道8 位阁像 。image
图像中的像素(x ,y )与 m a s k 中的(x + l,y + 1)对应。填充只针对 m a s k 中的值为0 的像素进行。
进行填充之后,m a s k 中所有被填充的像素将被赋值为1。如果只希望修改 mask , 而不对原始图
像进行填充,可以开启 flags标志中的 F L O O D F T L L _M A S K _O N L Y 。
在下 liU的例子中,第一次调用 floodFillO时,由于设置了 F L O O D F I L L _ M A S K _O N L Y 标志,
因此填充只在 m a s k 中进行,并未修改 i m g 中的数据:
img = cv2 .imread("coins.png")
seedl = 344, 188
seed2 = 152, 126
diff = (13, 13, 13)
h, w = img.shape[:2 ]
mask = np.zeros((h+2 , w+2 ), np.uint8 )
cv2.floodFill(img, mask, seedl, (0, 0, 0), diff, diff, CV2.FLOODFILL一
HASK_ONLY)
cv2.floodFill(img, None, seed2,( 0) 0, 255), diff, diff)

fig, axes = pi.subplots(1, 2, figsize=(9, 4))


axes[0 ]•imshow(Mnask, cmap="gray")
axes[1 ],imshow(img)

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

scpy2.opencv.floodfill_d e m o : 演 示 填 充 函 数 floodFill()的 各 个 参 数 的 用 法 <

演示程序中使用两个鮝加在一起的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可以从图像上去除指定区域中的物体,可以用于去除图像上的水印、划痕、
污渍等瑕疵。它的调用参数如下:
- 像处理和计算机视觉

inpaint(src, inpaintMask, inpaintRadius, flags[, dst])

其中,src参数是原始阁像,inpainlMask参数是人小和 ksrc相同的单通道8 位图像,其中不


为 0 的像素表示需要去除的区域。dst参数用于保存处理结果。i叩aintRange参数是处理半径,
半径越大处理时间越长,结果越平滑。flags参 数 选 择 inpaint的算法,目前有两个候选算法:
INPAINT_NS 和 INPIANT—TELEA。
下 面 的 程 序 演 示 inpaim()的用法,界面如图9-7(左)所示。右上图中用白色冈域表示
i叩aintMask参数中不为0 的像素,即需要处理的区域,右下图显示了对此区域进行处理之后的
效果。
^ SnpaiH0 •«*«〇
suffgp9
2〇 J〇 IO jO

今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 对图像进行仿射变换,调用参数如下:

warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]])

其 中 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 版)

仿射变换矩阵中有6 个参数,因此只需要指定变换前后3 个坐标点的坐标,就可以通过解


线性方程纟11获得变换矩卩彳:。O p e n C V 提供了 getAffineTransform(src,
dst)来快速完成这种计算。src
和 dst参数是变换前后的三个点的坐标,它们都是形状为(3, 2)的单精度浮点数数组。下面的程
序演示了这两个函数的用法,效果如图9-8所示:

img = cv2 .imread("lena.jpg")


h, w = img.shape[:2 ]
src = np.array([[0 , 0 ], [w - 1, 0 ], [0 , h - 1 ]], dtype=np.float32) O
dst = np.array([[300, 300], [873, 78], [161^ 923]], dtype=np.float32) ©

m = cv2 .getAffineTransform(src, dst) € )


result = cv2.warpAffine(
img, m, ( 2 * w, 2 * h), borderValue=(255, 255, 255, 255)) O
O penCV图
- 像处理和计筇机视觉

阍 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 来完成这种计算,其调用参数如下:

remap(srcj mapl, map2, interpolation[, dst[^ borderMode[_» borderValue]]])

其 中 m a p l 和 m a p 2 参数是两个大小与原始图像 src相同的数组,它们的元素值是图像 dst


中对应下标的像素点在阁像src中的坐标值,其元素可以是整数或单精度浮点数。m a p l 中存储
映射的 X 轴坐标,而 m a p 2 中存储映射的 Y 轴坐标。下而的数学公式表示了这种映射关系,其
中 x 和 y 是目标图像中每个像素的坐标,通 过 m a p l 和 m a p 2 分别获得它们在 src中的坐标。
dst(x ,y ) = src(m a p l (x ,y ), m a p 2(x ,y ))
下面的程序使用 remapO 将 图 像 Y 轴方向缩小为原来的三分之一,将 X 轴方向缩小为原来
的一半,img2[y ,x ] 的像素值与 img [m a p y [y ,X ], mapx [y ,x ] ] 的像素值相同。程序中通过 interpolation
参数指定采用线性插值 I N T E R _L I N E A R , 读者可以通过 IPython的 & 动完成功能查看其他的插
〇pencv图

值选项。
- 像处理和计筇机视觉

mapy, mapx = np.mgrid[0 :h * 3:3, 0 :w * 2 :2 ]


img2 = cv2.remap(img, mapx.astype("f32"), mapy.astypG("f32"), cv2.INTER_LINEAR)
x, y = 12, 40 #用于验证映射公式的坐标点
assert np.all(img[mapy[y, x], mapx[y, x]] == img2 [y, x])

缩小之后的图像 img 2 的大小仍然和原始图像 i m g 相丨nj, 但是其中只有左上部分有图像数


据。这里使用 mgrid 对象直接创建两个映射数组m a p x 和 m a p y 。
为 了 演 示 remapO 的强大功能,下面的程序让用户输入一个三维空间的曲面函数,程序将
根据此函数所计算的曲面对图像进行变形。
效果如图> 1 1 所示,
就像是将图像贴在曲面上一样。

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

for ax, (expr^ ry height) in zip(axes, settings):


mapx, mapy = make一surf_map(make_func(expr), r, viy h, height)

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 的圆形遮草数组 mask 。© 将 offsetx和 offsety中与 mask


对应的值修改为拖曳的起点与终点的坐标差。如果使用这吋的 gridx + offsetx和 gridy + offsety作
- 像处理和计算机视觉

为坐标映射,就会将以起点坐标为中心、半 径 为 r 的圆形区域复制到终点对应的区域。O 为了
让变形更加柔和,我们使用 GaussianBlur〇对两个 offset数组进行高斯模糊处理,sigma 参数决定
模糊的程度,该值越大越模糊。
图 9-13显示了变形之后的效果,其中虚线圆表示拖曳的起始位置,实线圆表示拖曳的终点
位置。

scpy2.opencv.remap_d e m o :演 示 remapO 的拖戈效果。在图像上按住鼠标左键进行拖戈,


每次拖曳完成之后,都将修改原始图像,可以按鼠标右键撤销上次的拖曳操作。

img = cv2 .imread("lena.jpg")


h, w = img.shape[:2 ]
gridy, gridx = np.mgrid[:h^ :w] O
tx, ty = 313, 316
sx, sy = 340, 332
r = 40.0
sigma = 2 0

mask = ((gridx - sx) ** 2 + (gridy - sy) ** 2 ) < r ** 2 0


offsetx = np.zeros((hj w))
offsety = np.zeros((h, w))
offsetx[mask] = tx - sx € )
offsety[mask] = ty - sy
offsetx一blur = cv2.GaussianBlur(offsetx, (0, 0), sigma) O
offsety_blur = cv2.GaussianBlur(offsety, (0, 0), sigma)
img2 = cv2 .remap(img,
(offsetx_blur + gridx) .astype(,,f4"),
(offsety_blur + gridy).astype("f4"), cv2.INTER_LINEAR)

o p e n c v图
- 像处理和计® 机视觉
图9-13使用rcmapO实现图像拖曳效果

9 . 3 . 3 直方图

在 N u n i P y 中有三个直方图统计函数:histogram() 、histogram2d() 和 h i s t o g n m d d (),分另ij对应


- •维数据、二维数据以及多维数据的情况。下面的程序用 h i s t o g r a m O 和 h i s t o g r a m 2 d 〇对图像的颜
色分布进行统计,输出如图 9 - 1 4 所示的统计结果。

img = cv2 .imread("lena.jpg")


fig, ax = pi.subplots(1, 2, figsize=(12, 5))
colons = ["blue", "green", "red"]

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

ax[0 ].legend(loc="upper left")


ax[0].set_xlim(0, 256)
hist2 , x2, y2 = np.histogram2 d( ©
img[:,:, 0 ].ravel(), img[:,:, 2 ].ravel(),
bins=(100, 100), range=[(0, 256), (0, 256)])
ax[l].imshow(hist2, extent=(0, 256, 0, 256),origin="lower", cmap="gray")
ax[l].set_ylabel(MblueM)
ax[l].set_xlabel(Mred")
Python 科学计算(第 2 版)

图 9-14 lenajpg的三个通道的直方图统计(左),通道 0 和通道2 的二维直方图统计(右)

O 通 过 histo g r a m 〇对 图 像 i m g 的三个通道分別进行一维直方图统计,丨
t丨于被统计的数姐必
须是一维的,因此这里调用数组的m v d ( ) 方法将二维数组转换为一维数组。通 过 m n g e 参数指定
O penCV图

统计区间为 0 〜 256,b i n 参数指定将统计区间等分为256份。histogram() 返丨111两个数组 hist 和 X ,


其 中 hist 为统计结果,长度为 b i n 。而 x 为统计区间,长度为 b i n + 1 。hist[i]的值为数组中满足x[i]
- 像处理和计

<=v< x[i+l] 的元素 v 的个数。


©用 h i s t o g r a m 2 〇对 通 道 0 和 通 道 2 进行二维直方图统计。被统计的数组是两个一维数组,
因此也需要用 r a v d 〇方法进行转换。它们分别为图像的通道0 和通道 2 的数据。b i n s 和 r a n g e 参
IT -
机视觉

数都变成了有两个元素的序列,
分别与两个数组相对应。
返回的统计结果 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方法找出图像中和
直方图相匹配的区域。下面用一个实际的例子加以说明:

img = cv2 .imread("fruits_section.jpg") O


img_hsv = cv2.cvtColor(img, cv2.C0L0R_BGR2HSV)

result = cv2.calcHist([img_hsv],[0,1 ],None, ©


[40, 40], [0, 256, Q, 256])

result /= np.max(result) / 255 ©

img2 = cv2 .imread("fruits.jpg") O

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 。

def histogram_match(src> dst):


res = np.zeros_like(dst)
cdf_src = np.zeros((3, 256))
cdf_dst = np.zeros((B, 256))
cdf_res = np.zeros((3, 256))
kw = dict(bins=256, range=(0, 256), normed=True)

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)

return res, (cdf_src, cdf_dst, cdf 一


res)

下而调用 hist〇
gram _ matCh ()将一幅秋景的直方图复制给夏景图像。图 9-16显示了对夏景图
进行直方图匹配处理前后的图像,图中下层的三个图表S 示图像的蓝绿红三个通道的累计分布
丨||)线。其中点线为处理之前S 景图的累计分布,实线为处理之后的累计分布,而虚线为秋最图
的累计分布。可以看到图屮实线与虚线几乎完全重合,因此完美地将秋景图的直方图复制给了
夏景图:

src = cv2 .imread("autumn.jpg")


dst = cv2 .imread("summer.jpg")

res, cdfs = histogram一match(src, dst)

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。下面我们用程序验证一下:

from numpy import fft


x = np.random.rand(8 , 8 )
X = fft.fft2(x)
print np.allclose(X[l:, 1:], X[7:0:-l, 7:0:-l].conj()) # 共扼复数
print X[::4, ::4] # 虚数为零
True
[[31.48765415+0.j -2.80563949+0.j]
[0.75758598+0.j -0.53147589+0.j]]
〇pencv图

频域信号通过 iffi2 〇可以转换冋空域信号,结果和原始的空域信号完全相等。但 是 所


得到的仍然是一个复数数组,只是每个元素的虚部都十分接近于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)

rects = [(80, 125, 85, 130), (90, 90, 95, 95),


(150, 10, 250, 250), (110, 110, 146, 146)]

最后选择频域信号中的一部分,将其转换回空域信号。图 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-17 (左上)用 fft2〇计算的频域信号,(


中上)使用ffishiftO移位
之后的频域佶; (
其他)各个领域所对应的空域信兮
Python科学计算(第 2 版)

9 . 3 . 5 用双目视觉图像计算深度信息

所谓双目视觉萣指模拟人眼处理场M 的方式, 两台照相机从不同视点观察同一场景获得


两幅图像。通过在左右两幅图像中匹配场景对应的点,计算出场景中各点距离照相机的距离,
从而重建三维信息,原理如图9-18所示。
o p e n c v图

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

img_left = cv2.cvtColor(img_leftJ cv2.C0L0R_BGR2RGB)


img^right = cv2.cvtColor(img_right, cv2.C0L0R_BGR2RGB)

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)

fig, axes = pi.subplots(1, 3, figsize=(9, B))


axes[0 ].imshow(img_left)
axes[0 ].imshow(imgL_right, alpha=0.5)
axes[l].imshow(disparity, cmap="gray")
axes [2 ]•imshow( imgjeft)
axes[2 ],imshow(res, alpha=0.5)
for ax in axes:
ax.axis("off")
fig.subplots_adjust(0 , 0 , l y 1, 0 , 0 )

阁 9-19 [fj remap遺脅左右网幅阁像


Python科学计算(第2 版)

下面设置焦距与相机间距的乘积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图像获取。

mask = (z > 0) & (z < 30)


points = np.c_[x, y, z][mask]
〇pencv图

colons = imgjeft.reshape(-1, 3) [mask]


- 像处理和计筇机视觉

有了点的坐标和颜色,就可以使用 T V T K 的 PdyData 将这些数据显示为点云。所创建的


PolyData对 象 的 points屈性为所有点的坐标,为了对每个点进行着色,设 置 point_data.scalars属
性为每个点的颜色,注意这里需要使用单字节的数组表示R G B 颜色。该 PolyData对象只苕顶
点(verts),没有边线和面,每个顶点与 points中的一个坐标相对应。

from tvtk.api import tvtk


from tvtk.tools import ivtk
from pyface.api import GUI

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)

from scpy2 import vtk一scene, vtk一scene 一 ot_array


scene = vtk_scene([a], viewangle=2 2 )
scene.camera.position = ( 0 , 2 0 , -60)
scene.camera.view_up = 0 , 1 , 0
%array 一
image vtk_scene一to_array(scene)
图9-20使用VTK M 示三维点云

下面的程序在 N o t e b o o k 中用%gui启 动 G U I 线程,打幵三维窗口来观察点云:

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 图像识别

与本节内容对应的 Notebook 为:09-opencv/opencv~400~identify.ipynb 。

O p e n C V 除了能够对图像进行各种处理和变换之外,还提供了大量的图像识别函数。本节
介绍一些常用的图像识别和分割算法。

9.4.1 用霍夫变换检测直线和圆

用霍夫变换( H o u g h transform) 能够找出阁像中的直线和圆。O p e n C V 提供了如下三种霍夫变


P y th o n 科学计算(第2 版)

换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 ()的调用参数如下:

HoughLinesP(image, rho, theta, threshold[, linesminLineLengthf^ maxLineGaplll)


5
5
Python 科学计算(第 2 版)

其 中 i m a g e 参数为进行直线检测的图像,r h o 和 theta 参数分別为累加器中每个点所表示的r


和0 的大小。其 中 riio 的单位是像素点,而 t h e t a 是以弧度表示的角度。值越小则累加器的尺寸

越大,最后找出的直线的参数的精度越高,但是运算时间也越长。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〇检测阁像屮的直线

由 于 HoughLinesPO 需要针对二值图像进行操作,因此先用 CannyO 对灰度图像进行边缘检


测,得到一幅二值图像 img^binaiy。Canny 〇有两个阈值参数,它们直接影响边缘检测的结果。
阀值越小,从图像中检测出来的边缘细节越多。

img = cv2.imread("building.jpg", CV2.IMREAD 一RGAYSCALE)


img 一
binary = cv2.Canny(img, 100, 255)
lines = cv2.HoughLinesP(img_binaryJ rho=l, theta=np.deg2rad(0.1),
threshold=96, minLineLength=33>
maxLineGap=4)

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()的参数如下:

HoughCircles(image^ method, dp,minDist


[,circles[, paraml[, param2[> minRadius[, maxRadius]]]]])

其 :中 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 ( ) 内部会进行边缘检测,

其 中 p a r a m l 参数相当于边缘检测 C a n n y 〇的第二个阈值, C a n n y 〇的第一个阈值肉动设置为它的


一半。p 期 参 数 足 累 加 器 上 的 阈 值 ,它的值越小检测出的圆形越多。m i n R a d i u s 和 r n a x R a d i u s
参数指定圆形的半径范围,缺省都为0 表示范围不限。

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

img = cv2.imread("coins.png", cv2.IMREAD_GRAYSCALE)


img一blur = cv2.GaussianBlur(img, (0, 0 ),1.8) O
circles = cv2.HoughCircles(img_blur, cv.CV_HOUGH_GRADIENT, dp=2.0, minDist=20.0,
paraml=170, param2=44, minRadius=16, maxRadius=40)

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参数如下:

pynMeanShiftFiltering(src^ sp, sr[, dst[, maxLevelf^ termcrit]]])

pyrMeanShiftFilteringO以 src中的每个点(x , y )为初始点,寻找与它邻近的点。这里的邻近点


必须满足下面两个条件:
• 在 以 (x ,
y )为中心的边长为2*s p 的正方形范围内。
• 和 点 (x ,y )的颜色距离小于 sr参数,也就是将 3 个颜色通道当作三维向£!:
, 在此颜色空
间中,两个点的距离小于sr。
然后计箅邻近点的坐标平均值和颜色平均值,并以此平均点抖次寻找图像中的邻近点。如
此迭代下去,直到达到迭代终止条件。将迭代终止时的颜色平均值写进图像dst的坐标点(x ,
y )。
在 O p e n C V 中,所有的迭代算法都可以通过 termcrit设置迭代相关的参数,它是一个有三
个元素的元组:(
type,
maxCount ,epsilon)。maxCount 为最大迭代次数;epsilon为迭代终止时的误
差,即两次迭代结果的差小于此值时将结束计算;type 指定哪种终止条件有效,3 表示两利终
止条件都有效。
m a x j e v d 参数指定使用阁像金字塔进行计算。当使用图像金字塔时,先对低分辨率的图像
进行分割计算,然后利用此结果对高分辨率的图像进行分割。
下面是使用 pyrMeanShiftFilteringO进行图像分割的演示程序,效果如图9-26所示。

fig, axes = pi.subplots(1, 3, figsize=(9, 3))

img = cv2 .imread("fruits.jpg")

srs = [20, 40, 80]


for ax, sr in zip(axes, srs):
img2 = cv2.pyrMeanShiftFiltering(img, sp=20^ sr=sr, maxLevel=l)

O penCV图
ax.imshow(img2 [ :-1 ])
ax•set一axis—off()

- 像处理和计® 机视觉
ax.set_title("sr = {}".format(sr))

fig.subplots—adjust(0.02, 0, 0.98, 1, 0.02, 0)

sr= 20 sr = 40 sr = 80

图9-26使用pyiMcanShiftFilteringO进行图像分割,从左到右参数sr分別为20、40、80

2.分水岭算法

分水岭箅法(Watershed)的基本思想是将图像的梯度当作地形图。图像中变化小的区域相当
于地形图中的山谷,而变化大的区域相当于山峰。从指定的几个初始区域同吋开始向地形灌不
同颜色的水,水面上升逐渐淹没山谷,并且范PI逐渐扩大。请注意这里所说的“颜色”是用来
区分不同区域的一个数值,和图像的颜色没有任何关系。当所有区域的水面连接到一起时,所
得到的不同颜色的灌溉区域就是最终的阁像分割结果,最终的分割区域数和初始区域数相同。
watershed()实现此算法,它的调用形式如下:

watershed(imagemarkers)

59
Python 科学计算(第 2 版)

image 参数是需要进行分割处理的图像,它必须是-•个3 通 道 8 位图像。mark 奶参数是一


个 32位整数数组,其:大小必须和image 相同。markers 中值大于0 的点构成初始灌溉区域,其
值可以理解为水的颜色。调 用 watershedO之后,m a r k e r 中几乎所有的点都将被赋值为某个初始
区域的值,而在两个区域的境界线上的点将被赋值为-1。
下 面 用 watershedO对如图9-27(左)所示的药丸图片进行分割。图中每片药丸上的蓝色区域
为使用下而的程序找到的局域最亮像素。我们使用这些像素作为每片药丸的初始灌溉区域。为
了让每片药丸只有一个局域最亮像素,0 先调用 blur()对药丸的灰度阁像进行模糊处理,这样可
以有效消除噪声,减少局域最值的个数。©我们只希 M 找到药丸区域对应的局域最焭像素,因
此对灰度图像进行二值化处理,imgjDinaiy 中白色区域与药丸对应。€)局域最亮像素就是比周
边临近的像素都亮的像素,使 用 dilateO对灰度图像进行膨胀处理,将每个像素都设置为邻近像
素屮的最大值,然后与原始图像中的值比较,如果值保持不变,该像素即为局域最亮像素。

img = cv2 .imread("pills.png")


img_gray = cv2.cvtColor(img, cv2.C0L0R 一BGR2GRAY)
img_gnay = cv2 .blur(img_gnay, (15, 15)) O
〇pencv图

img_binary = cv2.threshold(img gray,150, 255, cv2.THRESH—BINARY) ©


peaks = img gray == cv2.dilate(img gray,np.ones((7, 7)), 1) ©
- 像处理和计筇机视觉

peaks &= img_binary


peaks[1 ^ 1 ] = True O

from scipy.ndimage import label


markers, count = label(peaks) 0
cv2 .watershed(img, markers)

® 接下来需要对 peaks 中的每块区域进行编号,O p e n C V 中没有提供相应的函数,我们可以


使 川 S d P y 的图像处理一节中介绍的labels()对每块区域进行编号。然后使用编号之后的结果作
为 watershed()的 markers参数。O 由于分水岭兑法需要我们在每块分割区域中都设置初始值,O
因此为了区分背景与药丸,在背景区域中的某点之上也需要设置初始值。
watershed〇采用分水岭算法将 markers 中值为零的元素设置为某个初始区域的值。
在 markers
中-1表示区域边界,而 1到 count则为分割之后的每个区域的编号。图9-27(右)显示了对这些编
号涂色之后的结果。阁中将 markers[U ]对应的编号设置为白色,将-1对应的编号设S 为黑色,
将其余的编号设置为随机颜色。

scpy2.opencv.watershed_d e m o : 分水岭算法的演示程序。用鼠标在图像上绘制初始区
域,初始区域将使用“当前标签”填充,按鼠标右键切换到下一个标签。每次绘制
初始区域之后,将显示分割结果。
图 9-27使用 watershed分割药丸

9.4.3 SURF 特征匹配

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 算法检测出的关键点对应的图像模式。

img_grayl = cv2.imread("lena.jpg", cv2.IMREAD_GRAYSCALE) O


surf = cv2.SURF(2000, 2) ©
keyjDointsl = surf.detect(img_gray1 ) €
)
keyj3 〇
intsi.sort( key=lambda kpikp.size^ reverse=True) O

img_colonl = cv2.cvtColor(img_grayl> cv2.C0L0R_GRAY2RGB)


cv2.drawKeypoints(img_colorl, key_pointsl[:25], img_colorl, color=(255, 0, 0), ©
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
P y th o n 科学计算(第 2 版)

图9-28 SURF0 找到的关键点和句:


•个关键点的局部图像

通过比对关键点对应的图块,可以在两幅图像中寻找相似的点。但楚直接比较阁块的像素
O penCV图

值并不明智,S U R F 算法为每个关键点计算128个時征,这些特征与关键点的大小和方向无关。
下面调用 S U R F .compute()计算关键点列表 key_pointsl对应的特征向M featuresl:
- 像处理和计筇机视觉

featuresl = surf.compute(img grayl, keyjDointsl)


featuresl.shape
(145, 128)

还可以调用 S U R F . d e t e c t A n d C o m p u t e ( ) 直接计算关键点以及与之对应的特征叫量:

img_gray2 = cv2.imread("lena2.jpg"j cv2.IMREAD_GRAYSCALE)


img_colon2 = cv2.cvtColor(img gray2 f cv2.C0L0R_GRAY2RGB)
surf2 = cv2.SURF(2000, 2)
keyj3 〇
ints2 ., features2 = surf2.detectAndCompute(img gray2, None)

通过比对 featuresl 和 f e a t u r e s 2 , 可以找到两幅图像中最接近的关键点。在本例中由于关键


点数不多,可以使W 穷举法计箅所有关键点对的距离。如果关键点数很多,可以使州 O p e n C V
提供的 F l a n n B a s e d M a t c h e r 寻找匹配的特征向董。

http://docs.opencv.org/modules/flann/doc/flarm.html
O p e n C V 关于 F L A N N 的文档。

在下丨fil的 程 序 中 ,© 创 建 FlannBasedMatcher 对 象 时 传 递 两 个 字 典 index_p a r a m s 和


search_params ,分別设置其索引参数和搜索参数。这两个参数在 C ++语言中分別为 IndexParams
和 SearchPamms 对象。索引参数的 algorithm属性决定搜索筵法,候选值如表9-2所示。
表 9-2 搜索算法及说明

算法名 编号 说明
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矩阵将变形之后的图像还原成原始图像<

key_positionsl = np.array([kp.pt for kp in key_pointsl])


key_positions2 = np.array([kp.pt for kp in key_points2 ])

indexl = np.array([m[0 ].queryldx for m in match_list])


index2 = np.array([m[0 ].trainldx for m in match_list])

distances = np.array([m[0 ].distance for m in match一list])

best—index = np.argsort(distances)[: 50] O


matched_positionsl = key_positionsl[indexl[best_index]]
matchedj3〇sitions2 = key_positions2[index2[bGSt_index]]
O penCV图

matrix, mask = cv2.findHomography(matched_positionsl, matched_positions2_» cv2.RANSAC) O


- 像处理和计筇机视觉

scpy2.opencv.surf_d e m o : S U R F 图像匹配演示程序。用鼠标修改右侧图像的4 个角的


位置计算出透视变换之后的图像,然后在原始图像和变换之后的图像之间搜索匹配
点,并计算透视变换的矩阵。

图9-29 © 示特征匹配的关键点

9 . 5 職与结构分析

与本节内容对应的 Notebook 为:C^-opencv/opencv-SOOshapes .ipynl^


从二值图像提収形状信息有助于对图像进行更高级的处现和识別。本节介绍如何使用
f i n d C o n t o u r s O 搜索图像中的轮廓,并对-其进行处理和计算。

9 . 5 . 1 轮廓检测

f m d C o i i t o u r s O 用于在二值图像中寻找黑白区域的边界轮廊,并返 In丨描述轮廊的多边形,它

的调用形式如下:

findContours(image^ mode, method[, contourshierarchyoffset]]])

mode 参数为搜索模式,有以下 4 种选项:


R E T R _ E X T E R N A L :只返回最外层的轮廓。
RETR_LIST: 返回所有的轮廓,但是不建立边界的嵌套信息。
R E T R _ C C O M P : 建立一层嵌套信息。
R E T R J T R E E : 建立完整嵌套信息。
m e t h o d 参数为轮廓多边形的近似方法。由于轮廓信息是从像素点阵获得,因此不够〒消,

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(左)所示的二值图像。

img_coin = cv2.imnead("coins.png", cv2.IMREAD_C0L0R)


img一coin^gray = cv2.cvtColor(img一 coin,cv2.C0L0R一 BGR2GRAY)
img_coin_blur = cv2.GaussianBlur(img^coin_gray> (0,0 ),1.5, 1.5)
img一coin一binary = cv2 .Canny(img_coin_blur.copy()^ 60, 60)
img_coin一binary = cv2.morphologyEx(img^coin—binary, cv2.MORPH—CLOSE,
np.ones((3, 3), "uint8 "))

由于图像中没有嵌套结构,在下面的程序中使用 R E T R _ E X T E R N A L 模式搜索轮廓,并比
较各种轮廓近似选项所得到的多边形的总点数:

for approx in ["NONE", "SIMPLE", "TC89_KC0S", "TC89_L1"]:


approx_flag = getattr(cv2, "CHAIN_APPROX_{}".format(approx))
coin一contours, hierarchy = cv2.findContours(img_coin_binary.copy()>

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
- 像处理和计筇机视觉

return perimeter * perimeter / (4 * np.pi * area)

coin一contours = [contour for contour in coin一contours


if 0 . 8 < circularity(contour) < 1 .2 ]
cv2.drawContours(img_coin, coin_contours, -ly (255, 0, 0))

阁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表示无效下标。

img_pattern = cv2 .imread("nested一patterns.png")


img_pattern_gray = cv2.cvtColor(img pattern, cv2.C0L0R_BGR2GRAY)
img_pattern—binary = cv2.threshold(img pattern gray, 100, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(img pattern binary.copy(),
CV2•RETR 一 TREE, CV2•CHAIN一PAPR0X_TC89一LI)
hierarchy.shape = -1, 4

所有父轮廓下标为- 1 的轮廓就是阁像中最外层的轮廓,下而的程序找到所有最外层轮廓的
下标:

root_index = [i for i in range(len(hierarchy)) if hierarchy[i^ 3] < 0]


root_index
[0, 7) 19]

使用下而的 get_children() 可以获取 h i e r a r c h y 中 i n d e x 对应的轮廓的所有子轮廓的下标,

o p e n c v图
而 g e U d e s c e i K l a m O 则可以获得所有嵌套轮廓的层次和下标。下而显示下标为 0 的轮廓的所有
嵌套轮廓的级别和下标。图 9-31 示了所有的轮廓,并用颜色映射表示各个轮廓对应的

- 像处理和计筇机视觉
层次。

def get_children(hierarchy, index):


first一child = hierarchy.item(index, 2 )
if first-Child >= 0 :
yield first一child
brother = hierarchy.item(first一child, 0 )
while brother >= 0 :
yield brother
brother = hierarchy,item(brother, 0 )

def get一descendant(hierarchy, index, level=l):


for child in get_children(hierarchy, index):
yield level, child
for item in get一
descendant(hierarchy, child, level + 1 ):
yield item

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〇比较两个简化之后的轮廊之间的近似程度。
- 像处理和计筇机视觉

在下而的例子中,我们要从 pattems.p n g 阁像中找到与 targets.p n g 中最匹配的轮廓。首先获


取轮廓信息,〇并将所有轮廊的坐标最小值都修改为0 , 这样便于使用 matplotlib绘制轮廓。©
然 后 调 用 approxPdyDPO 对轮廓进行近似处理。它的第二个参数为近似的误差允许范围,该值
越大,近似之后的轮廓的点数越少。第三个参数指示轮廓是否为封闭形状。由于 pattem^ p n g 中
的轮廓都是标准图形,而 targets.p n g 中的轮廓为手绘图形,这里将手绘图形的近似误差参数设
置得更大一些。

imgjDatterns = cv2.imread("patterns.png"., cv2.IMREAD_GRAYSCALE)


patterns, _ = cv2.findContours(img patterns,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
img_targets = cv2.imread("targets.png",cv2.IMREAD—GRAYSCALE)
targets, _ = cv2.findContours(img_targets, cv2.RETR_EXTERNAL, cv2.CHAIN_APPR0X_SIMPLE)

patterns = [pattern - np.min(p a t t e r n 0 , keepdims=True) for pattern in patterns] O


targets = [target - np.min(target^ 0 , keepdims=True) for target in targets]

patterns_simple = [cv2.approxPolyDP(pattern, 5, True) for pattern in patterns] ©


targets一simple = [cv2.approxPolyDP(target, 8j True) for target in targets]

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
荃口米。

for method in [1, 2, 3]:


method_str = "CV— CONTOURS_MATCHJ:{}".format(method)
method = getattr(cv, method一 str)
scores = [cv2.matchShapes(targets_simple[0]^ patterns一 simple[pidx], method, 0)
for pidx in range(5)]
print method一str^ join("{: 8 .4f}".format(score) for score in scores)
CV_C0NT0URS_MATCH_I1 11.3737, 0.3456, 0.0289, 1.0495, 0.0020
CV_C0NT0URS_MATCH_I2 4.8051, 2.2220, 0.0179, 0.3624, 0.0013
CV_C0NT0URS_MATCH_I3 0.9164, 0.4778, 0.0225, 0.4552, 0.0016

O penCV图
0.8628 0 0221 875 939
丨 1144

- 像处理和计® 机视觉
□ 0.9225 0.5160 034 1407 0282

J 03808 5.4120 073 »352 0285

0.7890 0.7661 274 H8S 872C

0.9164 0.4778 22S 1552 001

0 t 2
f
3 4

閱 9-32使用 matchShapes()比较丨丨丨approxPolyDP()近似之后的轮廓

9 . 6 类型转换

与本节内容对应的 Notebook 为:OQ -opencv/opencv-GOO^ype-convert.ipynlx

cv2 中的函数所需的参数类型尽量使用数组或Python 的标准数据类型,因此在 cv2 模块中


并没有 O p e n C V 的 C + + A P I 中的 M a t 、Point、Size、V e c 等各利1数据类型,而是用列表、元组或
69
Python 科学计算(第 2 版)

数组表示这些类型。调 用 CV2 模块中的函数比使用 O p e n C V 的 C ++ A P I 史加便捷,然而需要了


解 cv2 模块的数据类型转换规则,才能把正确的对象传递给函数。作为本章的最后一节,让我
们看看在 cv2 模块内部如何实现 Python 对象和 C ++对象之间的相互转换。

9 . 6 . 1 分 析 cv2 的源程序

为了了解 cv2 模块的数裾转换工作,需要分析其源程序。将 OpeiiCV 的源程序解压之后,


可以在 opencv\modules^>ython\src2 路径下找到相关的源程序。cv2 中各个包装函数是通过cv2.py
自动生成的。在命令行中切换到 src2 目录下,并运行命令"python cv2.p y .”
,即可在该目录下生
成 O p e n C V 的包装函数。

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

我 们 需 要 知 道 cv2.1ine ()在 调 用 C + + 的 l i n e O 函 数 时 做 了 哪些 类型 转 换工 作。下而是

pyopencv_generated_funcs.h 中该函数的源代码:

static PyObject* pyopencv_line(PyObject* PyObject* args, PyObject* kw)


{
PyObject* pyobj_img = NULL;
Mat img;
PyObject* pyobj_ptl = NULL;
Point ptl;
PyObject* pyobj_pt2 = NULL;
Point pt2;
PyObject* pyobj_color = NULL;
Scalar color;
int thickness=l;
int lineType=8 ;
int shift=0 :

const char* keywords[] = { "img", "ptl", "pt2", "color", "thickness", "lineType",


"shift", NULL };
if( PyArg_ParseTupleAndKeywords(args, kw,"0000|iii:line",
(char**)keywords, &pyobj_img, &pyobj_ptl, &pyobj_pt2 ,
&pyobj_color, &thickness, &lineType, &shift) &&
pyopencv—to(pyobj一img, img, ArgInfo("img", 1)) &&
pyopencv_to(pyobj_ptl, ptl, ArgInfo("ptl", 0)) &&
pyopencv_to(pyobj_pt2, pt2, ArgInfo("pt2"J 0)) &&
pyopencv 一ot(pyobj_color, color, ArgInfo("color'、 0)))
{
ERRWRAP2( cv::line(img, ptl, pt2, color, thickness, lineType^ shift));
Py_RETURN_NONE;
}

return NULL;
}

C ++函 数 cv ::line()所 需 的 4 个参数类型为 Mat 、Point、P o in t 和 Scalar, 程序 中 调 用 4 次

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类型对应的转换函数:

static inline bool pyopencv一


to(PyObject* obj, Point& p, const char* name = "〈
unknown〉
")
{
(void)name;
if(!obj || obj == Py_None)
return true;
if(!!PyComplex_CheckExact(obj))
{
Py一complex c = PyComplex_AsCComplex(obj);
p.x = saturate一cast<int>(c.real);
p.y = saturate_cast<int>(c.imag);
return true;
}
return PyArg_ParseTuple(obj, "ii", &p.x, &p.y) >0;
}

分析该程序可知它能将Python 的复数和元组转换为Point对象,
例 如 100f 200j 或(100,200)。
Python 科学计算(第 2 版)

请读者使用同样的方法找到与Scalar 类型对应的 pyopencv_to()函数,并分析它能将何


种类型的对象转换成Scalars 对象。

在 调 用 C + + 的 函 数 之 后 ,还 需 要 将 其 返 冋 值 转 换 为 P y th o n 的 对 象 。这种转换由
pyopencv_from〇函数实现。例 如 minAreaRect()函数找到一个包含所有点的最小矩开帮助文档

中的调用说明如下:

C++: RotatedRect minAreaRect(InputArray points)


Python: cv2.minAreaRect(points) —retval

只看该函数说明无法知道cv 2.rninAreaRect〇会返|H丨何种对象来表示C ++中的 RotatedRect对


象。在 cv 2.c p p 中搜索对应的类型转换函数为:

static inline PyObject* pyopencv 一


from(const RotatedRect& src)
{
〇pencv图

return Py_BuildValue("((ff)(ff)f)", src.center.x, src.center.


src.size.widths src.size.height^ src.angle);
}
- 像处理和计筇机视觉

分析该函数可知 minAreaRectO返冋一个有三个元素的元组,而其中第0、第 1个元素是有


两个元素的元组,因此可以写出如下测试程序,其 中 x 和 y 为矩形的中心坐标,w 和 h 为矩形
的宽和高,angle 为旋转角度:

points = np.random.rand(2 0 , 2).astype(np.float32)


(Xj y)j (vjj h), angle = cv2. minAreaRect (points)

9.6.2 M at 对 象

在 C + + A P I 中jU M at 对象表示图像,在调用 cv 2 模块中的函数时,会自动将整数、浮点数、


元组以及数组转换为 M a t 对象。为了说明 cv 2 对 M a t 的自动转换,需要了解 M a t 对象的内部
构造。
M a t 对象可以看作一个二维像素阵列,我们可以使用 c v 模 块 的 CrcateMatO函 数创建 Mat
对象。以下程序创建的cvm at 是一幅高为200、宽 为 100、3 个通道、像素类型编号为18的图像。
step 属性为图像中相邻两行像素的字节偏移量。甶于6 0 0 = 100*3*2,因此 cvm at 中的图像数据
是连续存储的。与 N um Py 数组的 strides不同,step 屈性只能表示第0 轴方向上两个相邻元素之
间的偏移量,因此 M at 对象无法与某些非连续的数组共享内存。

cvmat = cv.CreateMat(200, 100, cv2.CV_16UC3)


cvmat.height cvmat.width cvmat.channels cvmat.type cvmat.step

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

将数组转换成 M at 对象时,数组的第0 轴为阁像的纵轴方向,第 1轴为阁像的横轴方向。


荇数组有第2 轴 ,则 第 2 轴为图像的通道数;若无第2 轴,则阁像的通道数为1。数组的 dtype

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 双字节符号整数

int32 CV_32S 4 字节符号整数


float32 CV.32F 单精度浮点数
float64 CV_64F 双精度浮点数

调用 cv 2 模块中的函数时,Num Py 数组会被转换为M at 对象,


它尽可能与原数组共享内存,
但如果数组的 dtype 类型不在表9-3中,或者由于数组的数据存储K 的非连续性,在把数组转换
为 M at 对象时会复制数据存储R 。为了保险起见,建议读者传递给 cv 2 模块的数组都是 C 语言
连续的。
除了数组之外,cv 2 还能自动将整数和浮点数转换成高为4 、宽 为 1、单通道的双精度浮点
数 M at 对象,而元组则会被转换为高为元组长度、宽 为 1、单通道的双精度浮点数M at 对象。
例 如 normalizeO函数对 M at 对象进行正规化运兑,它先将参数转换为 M at 对象,把正规化之后
的 M at 对象转换为数组返丨Hi。根据上述规则可以得知下面的程序返N —个形状为(4,1)的双精度
浮点数组:

cv2 .normalize(l)
Python 科学计算(第 2 版)

a r r a y ( [ [ 1.],

[
[0.],
[0.]])

9 .3 .3 在 CV和 CV2 之间转换图像对象

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

cvmat— iplimage cv.Getlmage(cvmat)

iplimage—K:
vmat iplimage [:]或 cv.GelMat(ipl image)
〇pencv图

下而通 过 cv.LoadlmageO和 c v . L o a d l m a g e M O 分 别 将 图 像 读 入 为 iplimage和 c v m a t 对 象 :


- 像处理和计筇机视觉

img = cv2 .imread("lena.jpg")


iplimage = cv.Loadlmage("lena.jpg")
cvmat = cv.LoadImageM("lena.jpg")
print iplimage
print cvmat
<iplimage(nChannels=3 width=512 height=512 widthStep=1536 )>
<cvmat(type=42424010 8UC3 rows=512 cols=512 step=1536 )>

如果需要在 N u m P y 数组和 i p l i m a g e 之间转换,可以通过 c v m a t 作为桥梁,例如:

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 配置编译器

与本节内容对应的 Notebook 为:10-cython/cython-100-compiler.ipynb

为了使)l:l Cython , 雷先要选择好 C 语言编译器。在 WinPython 和 Anaconda 中已经自带了


mingw 3 2 编译器,为 了 让 P yth on 使用它作为缺省的编译器,需 要 编 辑 P yth o n 安装路径下
Lib/distutils/distutils.cfg 文件,添加如卜内容:

[build]
compiler = mingw32

读者可以使用本书提供的 sh〇
w _C〇mpiler()显示默认的 C 语言编译器。使 用 set_C〇
mpiler()可
以快速设S distutils.c f g 中的 compiler选项:
Python 科学计算(第 2 版)

from scpy2.utils import show一compiler, set一compiler


set_compiler("mingw32")
show一compiler()
mingw32 defined by C :\WinPython-32bit-2.7.9.2\python-2.7.9\lib\distutils\distutils.cfg

如果未通过 distutils.cfg文 件 指 编 译 器 ,
distutils会尝试使用编译 Python 2.7的编译器 Visual
C + + 2008,如果操作系统中未安装该编译器,会出现如下错误:

DistutilsPlatformError: Unable to find vcvarsall.bat

由于现在 Visual C ++ 2008已经很难入手,因此微软为 Python 2.7提供了专门的 Visual C ++


编译器供下载:

http://www .microsoft.com/en-us/download/conflrmation.aspx?id=44266
% ^ 微 软 提 供 的 编 译 Python 2.7的编译器。
cython编

该编译器缺省安装在用户路径之下,为了让 Python 的 distutils模块能正确找到它,需要运


—译

行下而的命令来更新setuptools模块:
Python

pip install --upgrade setuptools


程序

先载入 setuptools模块就可以让 distutils正确找到 Visual C ++编译器:

import setuptools # 先 载 入 setuptools


import distutils
from distutils.msvc9compiler import find_vcvarsall
find_vcvarsall(9.0)
u 'C :\\Users\\RY\\AppData\\Local\\Programs\\Common\\Microsoft\\Visual C++ for
Python\\9.0\\vcvarsall.bat'

然后就可以使m % % cyth o n 命 令 在 N otebook 中编译 Cython 程序,测试编译器是否正确设


置了:

% % cython 魔法命令缺省没有载入到IPyhton 的运算核中,需要先通过%load_ext cython


舄j 命令载入该命令。

%%cython

def add(a, b):


return a + b

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 时所使用的编译器,因此无法使用最新版本的编译器。

distutils从 sys.version 获取编译 P y t h o n 的编译器版本,可通过本书提供的 set_msvc_version()

修改该信息,从而实现选杼新版本的编译器:

from scpy2.utils import set一msvc一version


set_compiler("msvc")
set_msvc_version(12)
show_compilen()
msvc 12.0 defined by C:\WinPython-32bit-2.7.9.2\python-2.7.9\lib\distutils\distutils.cfg

下面将编译器设置还原成m i n g w 3 2 ,接下来的章节中均使M J 此编译器:

set_msvc_version(9)
set_compiler("mingw32")

cython编
10.2 Cython 入门

Python—译
程序
与本节内容对应的 Notebook 为: 1 0 ~cython /cython -2 0 0 ~intro.ipynb 。

当需要在 P y t h o n 中对数组的元素进行大量循环计算时,P y t h o n 的运行效率会比C 语言的效


率低上百倍。
让我们首先通过-•个数组运兑函数的实例,
演 示 C y t h o n 如何实现数组的高速运算。

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

为了验证程序的结果是否正确,下 面 用 S d P y 的 pdistO进行 N 样的计兑,并比较二者的运


算速度:可以看出二者的运算速度相差近200倍。

from scipy.spatial.distance import pdist, squareform


%timeit squareform(pdist(X))
c y t h o n编

%timeit pairwise_dist_python(X)
np.allclose(squareform(pdist(X))J pairwise_distjDython(X))
—译

1000 loops, best of 3: 443 [as per loop


10 loops, best of 3: 92.7 ms per loop
P y th o n

True
程序

Cython 程序需要编译,通常需要编写一个 setup.p y 程序,稍后会详细介绍这方面的内容。


现在我们先用 IPylhon Notebook 中的%% cython 魔法命令快速编译运行Cython 程序。
% % Cyth〇
n 魔法命令是一个单元命令,整个单元中的程序都会调用Cython 编译成扩展模块,
并自动从编译之后的模块中载入所有对象。由于 Cython 程序是一个单独的模块,因此需要在此
模 块 中 重 新 载 入 n u m p y 库 。下而的程序完全和 pairwise_dist_python()相同,其运算速度也和
pairwise_dist_python〇相当,只有很小的进步:

%%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 的所有优化手段,其 中 cd e f 为声明变量类型的关键字,@ cython 为


指 导 Cython 编译的命令。可以看出 Cython 编译出比 pdist〇更快的代码。

%%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 程序编译成扩展模块

在 Notebook 中测试了 Cython 程序的速度与正确性之后,我们希望将其编译成扩展模块,


以供其他的Python程序调用。实际上,%%cython 魔法命令扫动将 Cython 代码编译成扩展模块,
并从该模块载入所有的全局对象。下曲通过 sys.modules 找 到 pairwise_dist_cython2()函数所在模

79
Python 科学计算(第 2 版)

块的文件路径。如果读者希望立即使州该扩展模块,可将其复制到自己的 Python 程序目录下。


然后就可以像使叫一般模块一样使用它了:

import _cython_magic_f9e6211d48d0b874fa7ae6ce345d297b as fast_pdist


fast_pdist•pairwise一idst一 cython2(X)
import sys
sys.modules [pairwise_dist_cython2 ._ module—]
<module ,_cython_magic_f9e6211d48d0b874fa7ae6ce345d297b, from
'C :\Users\RY\Dropbox\scipybcx)k2\settings\.ipython\cython\_cython_magic_f9e6211d48d0b874fa7a
e6ce345d297b.pyd'>

可以通& % % cyth o n 命令的-n 参数指定编译之后的4广展模块名,例如 % %cython -n


fast__pdist〇

如果希望使jij distutils库自动编译扩展模块,可以编写如下 setup_fast_pdist.py安装文件。在


C y t h o n编

其•中定义了一个表示扩展模块的 Extension对象,名为 "fast_pdistn,包 含 一 个 Cython 源程序


-译

"fast_pdist.pyxn,该文件的内容与前面定义 pairwise_dist_cython2 ()的程序相同。由于它使用了


N u m P y 的功能,冈此斋要通过 numpy.get_include()指 定 N u m P y 的头文件的路径。
P y th o n

%%file setup_fast_pdist.py
程序

from distutils.core import setup


from distutils.extension import Extension
from Cython.Distutils import build_ext
import numpy as np

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
)

在命令行中运行如下命令,即可对 Cython 文件进行编译,生成扩展模块 fast_pdist.pyd:

python setup_fast_pdist.py build一ext --inplace

接下来即可载入该扩展模块,并调jlj其中的函数:

80
import fast_pdist
np•allclose(fast_pdist•pairwise_dist_cython2(X), pairwise_dist_python(X))
True

除了使用 setup.py 之外,还可以使用 c y t h o n i z e 命令。读者可以在 P A T H 环境变量的搜索路


径(例如P y t h o n 的 Scripts 文件夹)下创建一个名为cythonize.bat 的批处理文件,
并添加如下内容:

@echo off
python -m Cython.Build.Cythonize %*

然后就可以在命令行下输入 cythonize-i fast_pdist.p yx ,将指定的 Cython 文件编译成扩展模


块,并保存在相同的路径之下。

10.2.3 C 语言中的 Python 对象类型

使W Cython 语言编写的程序可通过Cython 编译器编译成 C 语言程序,然后再通过 C 语言


编译器编译为扩展模块供Python 调用。在 Cython 语言中可以对 Python 的对象类型和 C 语言类

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;

在 3 2 位系统中,P y _ s s i z e _ t 和指针都使用 4 个字节表示,而 d o u b l e 类型的长度为 8 个字节,


所 以 P y t h o n 中的一个 float对象占据 1 6 个字节。下面通过 sys.geLsizeof〇获得浮点数对象1.0的字
节数:

import sys
sys.getsizeof(1 .0 )
16

由于所有 P y t h o n 对象的头两个字段类型是相同的,因此在 C 语言中使用 P y O b j e c t *类型的


指针表示 P y t h o n 对象。在 C y t h o n 中,所有没有声明类型的变量都表示 P y t h o n 对象,因此编译
Python 科学计算(第 2 版)

成 C 语言之后,这些变量都是 PyObject *类型的指针。Python 提供了 Python/C A P I 以方便用户使


用 C 语言编写扩展模块。
下面我们为%% cythcm魔法命令添加-a 参数,显 示 Cython 程序及其编译之后的 C 语言程序
之间的关系。在 N otebook 中运行下面的程序将输出一段如图10-1所示的可折叠的代码。通过
颜色显示每行 Cython 代码编译之后的 C 语言代码的多少。颜色越深表示该行代码的运行速度
可能会越慢。请注意由于这些 C 语言代码都是 Cython 0 动生成的,冈此阅读起来可能有些困
难,读者无须完全理解这些代码,只需要人致了解工作原理即可。
♦1: a 1.0
■►2: b = 2.8
十3; c = a ♦ b
_^pyx.t.l • __Pyx GetModuleGlobalName(, pyx n^s.a); i : (u
tW; — ■=■ 3; 一 pyx_c'Hnt;"o ; — LINE— ; goto — py>c
^ Pyx^OOTREF ( ^ pyx^t .l );
_ pyx_t_2 = 一 Pyx_GctHQdulcGlobalNiin»c{_ pyx_n_5_b); f u>
【 © J j • • j j y x 一I i f i e n o ,3 1 uyx cl Ingno , J goto • • pyx;

_l>yx.G0lftCr(_pyx.t_2);
C y t h o n编

_ pyx_t_3 = PyNumber_Atld(— pyx_t_l, — pyx_t_2) ; i


_^pyx_1 inpno r 3 J _ ^ p y x ^ , c l |ru? n o ^ l一

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

if (Py01ct.SeUtem(..pyx.df _ p y x.n.s_ct _pyx.t_3) < 6)


i pyx cl i n e n o s ftif _ ^ 1 ^ o r §}
_ l > y x „ D b C R E h ( _ p y x ^ 3 ) ; _ p y x . t ^ 3 = 0;
程序

图 K M 使用“
-a ”参数査看编译之后的C 语言程序

%%cython -a
a = 1 .0
b = 2 .0
c = a + b

在上面的程序中,变 量 a 、b 、c 均 为 Python 对象,因此 c = a + b 被编译成如图 K M 所示的


C 语言代码。其中_ Pyx _G O T R E F 和 _1^_0£〇 1£「足为了保证垃圾回收机制能正常运行,而

对对象的引用计数器进行操作。
_ 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 ) 从全局字典中获

取%"表示的P y t h o n 对象。P y N u m b e r _ A d d 〇为 P y t h o n / C API 函数,它将两个 P y t h o n 对象当作数值


相加,并返回一个新的 P y t h o n 对象_ p y x _ t _ 3 。最后调用 P y D i c t _ S e t I t e m 〇将_ p y x _ t _ 3 添加进全

局字典— p y x 一 d 中,对应的键为— pyx一n一s _ c 所表示的字符串对象V 、


因此一个简单的加法运算需要两次查询全局字典,获収变量 a 和 b 所表示的对象,然后调
用一次 A P I 函数来完成加法运算,最后再将结果写入全局字典。

1 0 . 2 . 4 使 用 edef关键字声明变量类型

为了大幅度地提高程序的运行速度,耑要对Python 程序中频繁川到的变量使j|j ed ef 关键字


声明变量类型。州 cd ef 关键字声明变量类型起到如下两个作州:
•提 高 变 量 的 存 取 速 度 :在 Python 中,全局变量和对象的属性与它们对应的值之间的关
系保存在字典中,每次读写这些变量或属性都相当于对字典进行存取。而如果使叫 cdef
关键字声明变量,那么该变量与其值的对应关系在编译期已经确定,可以省下字典查
询所需的时间。
•提 高 数 值 的 处 理 速 度 :在 Python 中所有的对象都是C 语言中的结构体,对其进行运算
吋需要调用 Python 提 供 的 A P I 函数。而当知道值的类型时,Cython 会尽可能地使用 C
语言提供的运算功能,从而极大地提高计算速度。
在下而的例子中,使 用 cd ef 定 义 a 、b 、c 三个变量为 double 类型。所生成的 C 语言代码大
致如下:

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 闲数的代码相比要简洁
许多。

请注意这里使用 c d e f 定义的三个全局变量为 C 语言的全局变量,并不能在 Python 中


A 通过编译之后的扩展模块获取它们的值。

%%cython -a
cdef double a = 1 . 0
cdef double b = 2 . 0
cdef double c = a + b

当 C 语言的变量与 Python 对象进行运算时,会 将 C 语言的变量转换成 Python 对象之后再


调 用 A P I 闲数进行运算。
在下面的例子中,
s 为 C 语言类型的变量,
而 a 为 Python 的 float 对象。
〇s 和 a 直接相加时,会大致运行如下 C 语言代码,其中」^ 为 double 类型变M , 丄 1 到
丄 3 为 Python 对象指针,_s_a 为字符串对象”
a”:

_t_l = PyFloat_FromDouble(_v_s); //将 s 转换成 Python 对象


_t_2 = _ Pyx_GetModuleGlobalName(_s_a); //获得全局字典中"a"对应的对象
Python 科学计算(第 2 版)

_ t_ 3 = PyNum ber_Add(_t_l, _ t_ 2 ) ; / / 加法运;y/-


_v_s = _ p y x _ P y F lo a t_ A sD o u b le (_ t_ 3 ); //将加法运算的结来转换为 d o u b le 类型

© 使 用 < 也 此 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 标 对 应 的 对 象 :

—Pyx_GetItemInt_List(clist^ —pyx_v_4demo_cindexy int, —Pyx_PyInt_From_int} 1, l y 1)

© 由 于 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;

首 先 调 从 全 局 变 M 字典中获取 pylist 和 p y i n d e x 对应的对象,


然后调用 P y t h o n 的 A P I 函数 P y 〇bject_GetItem() 获得下标对应的对象。
除了使用[]的下标存取操作之外,C y t h o n 还能识别许多内部类型的方法和属性。表 K M 列
出了目前 C y t h o n 所能识别的 P y t h o n 数据类型以及相关的运算操作,当遇到表中已知的属性或
方法吋,C y t h o n 将生成高效代码。

表 10-1 C y t h o n 所能识别的 P y t h o n 数据类型

数据类型 可识别的运算
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定义函数

通常为了提高程序的运算速度,我们在 Cython 中用 d e f 关键字定义闲数,然后在 Python 中


调用它们。由于需要在 Python 中调用,d e f 所定义闲数的参数和返N 值 均 为 Python 对象。然而
在 Cython 中可以为函数的参数添加类型定义,Cython 会对这些参数进行类型检查并A 动转换成
对应的 C 语言类型。
下面的 p y _ s q u a r e _ a d d 〇函数的两个参数均为d o u b l e 类型,当在 P y t h o n 中调用时,传递的是
两个Python的 float 对象。在 p y _ s q u a r e _ a d d ( ) 内部会将这两个 float 对象转换成 C 语 言 的 double
类型的变量,计算出 d o u b l e 类型的结果之后,再将其转换成 P y t h o n 的 float 对象并返冋。
对应的 C 语言代码大致如下,其 中 X、乂和_1*均为Python 的对象指针类型,而_v」^ n _ v j
为匸语言的 double 类型变量。通过 API 函数 PyFloat_AsDouble()和 PyFloat_FromDouble()在 float
对象和 double 类型变量之间进行转换。

double _v一x = PyFloat_AsDouble(x);


double _v_y = PyFloat_AsDouble(y);
85
Python 科学计算(第 2 版)

PyObject * _r;
_r = PyFloat_FromDouble(((_v_x * _v_x) + (_v_y * _v_y)));

%%cython -a

def py_square_add(double x, double y):


return x*x + y*y

当使用 Python 的类型(例如list)声明参数时,Cython 会进行类型检査,并将可识别的运算转


换成更高效的代码。在下而的例子中,声 明 a lis t 参 数 为 lis t 类型,因 此 legalist )将被编译为
PyList_GET _SIZE (_ pyx_ v_alist), 而 alist[i]则被编译为调用_ Pyx _GetItemInt_LLst()函数:

%%cython -a

def sum_list(list alist):


cdef double s = 0
cdef int i = 0
C y t h o n编

for i in range(len(alist)):
s += <double>alist[i]
-译

return s
P y th o n

1 0 . 2 . 6 使 用 cdef定 义 C 语言函数
程序

d e f 闲数采用 Python 的调用接口,即使萣在 Cython 程序内部调用这些函数,也需要对参数


和返丨n 丨值进行类型转换。在大簠循环中调用这样的W 数会有较大的损耗。可以使用 cd ef 关键字
定义只能在 Cylhon 程序内部调用的函数,调用的开销和 C 语言的函数相同。
下 而 的 c_square_add〇使 用 c d e f 定 义 ,参 数 和 返 回 值 均 为 d o u b le 类 型 。其 中 a =
c_ square_add(l .0,2.0)被编译为如下代码:

_ pyx_v_a = _ pyx_f_c_square_add(1 .0 , 2 .0 );

^ 如果不声明 c d e f 函数的返回值类型,则其类型为 Python 对象。

%%cython -a
cdef double c_square_add(double x, double y):
return x*x + y*y

cdef double a = c_square_add(1 .0 , 2 .0 )

如果希望同一个函数能在Cython 中快速调用,并且能在 Python 中调用,可以使用 q x le f 关


键字。它将同时生成一个C 语言函数和一个供 Python 调用的包装函数。在 C ylh on 中调用 cpdef

86
函数时会比 cd e f 函数稍微耗时一些,但要比调用 d e f 函数快得多。

%%cython -a
cpdef double cp square_add(double x, double y):

return x*x + y*y

cp_square_add(1.0, 2.0)

1 0 .3 高效处® ^

与本节内容对应的Notebook 为: 10-cython/cython-300-memoryview.ipynb。

科学计算程序中存在大M 的数组操作,前而已经简要地介绍过如何使用 C y th o n 的内存视


图( M e m o r y V i e w ) 快速访问数组的元素,本节详细介绍用法。

10.3.1 Cython 的内存视图

Cython 屮的内存视图采用如下语法进行声明:

cdef 元素类型[维数声明] 变fi名

其中维数声明的形式如下,使用:代表轴,:
: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 版)

© 当将内符视图返M 到 P y t h o n 环境吋 , C y t h o n 也会将其转换成 M e m o r y V i e w 对象。因此通


过 get_view() n 丨获取转换之后的M e m o i y V i e w 对象,方便我们查看其中的内容。
@ 在 C y t h o n 中获取内存视图的s h a p e 属性相当于获取内存视图结构体中s h a p e 字段的数据,
而对内存视图的下标操作,都会被编译成对数据存储区相应地址的直接访问,因此都是十分高
效的。

%%cython
import numpy as np

cdef double buf[3][4] 〇


cdef d o u b l e t : : 1 ] view = buf 0

def fill一avlue(double value):


np.asarray(view).fill(value) e

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]

下面通过 get_view() 获 取 M e m o r y V i e w 对象,并查看其各种屈性.,可以看到它们和 N u m P y


数组是完全一样的:

view = get_view()
view.shape view.strides view.itemsize view.nbytes

(3, 4) (32, 8 ) 8 96

下 面 使 用 fill_view ()、square_ view ()、sum_view ()等 函 数 对 C 语言中的全局数组进行操作,


Memory V ie w 对象也支持 F 标运算:

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

下而将 Memory V ie w 对象转换成 N um Py 数组,可以符出二者是共李数据存储K 的:

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.]]

通过内存视图的 base 属性可以获得保存实际数据的对象,由于我们的数据保存在C 语言的

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 设 置 boundscheck和 wraparound编译选项。


程序

%%cython
#cython: boundscheck=False, wraparound=False
import numpy as np
import cython

cpdef norm(doublet:] x):


cdef double s
cdef int i
s = 0
for i in range(x.shape[0 ]):
s += x[i]*x[i]
s = 1 / s**0.5
for i in nange(x.shape[0 ]):
x[i] *= s

def norm一axis(double[:, :] x, int axis=0 ^ bint inplace=True):


cdef int i
cdef doublef:, :] data
if not inplace:

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

def copy_memview(double[::1 ] src, double[::1 ] dst):


memcpy(&dst[0 ], &src[0 ], sizeof(double)*dst.shape[0 ]) ©

a = np.random.rand(1 0 )
b = np.zeros一like(a)
copy_memview(a, b)
assert np.all(a == b)

还可以通过类型转换操作将C 语言的指针转换成内存视图。例如,如 果 a d d r 是一个指针,


可以通& < d o u b l e [ : 1 0 j > a d d r 将其转换为一个长度为 1 0 的双精度浮点数的内存视图,长度可以使
州变量表示。不过在进行这种转换时,要十分注意内存的分配和释放,否则可能会出现野指针,
9
5
Python 科学计算(第 2 版)

造成整个程序崩溃。

1 0 . 3 . 2 用降釆样提高绘图速度

当 使 用 m atpbtlib 显示一条拥有大量数据点的曲线时,绘图速度会明显降低。由于屏笳的
分辨率有限,绘制大量的线段并不能增加阁表显示的信息,冈此一般在显示大量数据时都会对
其进行降采样运算。由于这种运算需要对数组中的每个元素进行迭代,因此需要使用 Cython
提高运算速度。
下面演示如何使用 C y th o n 进行快速降采样运算。首先创建测试数据,在正弦波信号中夹
杂 着 1% 的脉冲信号。

import numpy as np

def make—noise_sin_wave(period, n):


np.random.seed(42)
C y t h o n编

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

y [np.random.randint(0, n, m)] += np.random.randn(m) * 0.4


return x, y
程序

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 阁

下而首先使用 Python 实现上述算法,get_peaks_py ()的 x 和 y 参数分别是曲线上各点的X 轴


和 Y 轴坐細,x 中的值必须是递增且等间隔的。n 为降低取样频率之后的数据点数,xO 和 X1 为
H 标区域。如果为 None , 就对整条曲线进行处理。
为了测试程序是否运行正确,读者可以将由 x 、y 表示的原始曲线和由 xr 、y r 表示的降取
样曲线绘制在同一个图表中,比较二者的差别。
2
5
9
def get_peaks_py(x, y, n, x0=None, xl=None):
if x0 is None:
x0 = x[0 ]
if xl is None:
xl = x[-l]
index©, indexl = np.searchsorted(x, [x0 , xl])
indexl = min(indexl) len(x) - 1 )
xQj xl = x[index0 ], x[indexl]
dx = (xl - x0 ) / n

i = index0
x_min = x 一max = x[i]
y_min = y_max = y[i]
x_next = x0 + dx

xr, yr = np.empty( 2 * n), np.empty( 2 * n)

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

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 = xc, yc
elif y_max < yc:
x_max, y_max = xc, yc
i += 1

return xr[:j], yr[:j]

xr, yr = get-peaks一py(x,y, 200 )

59:
Python 科学计算(第 2 版)

下面对上述代码添加Cython 的类型声明,就可以将其编译成能快速执行的C 语言程序。


〇为了获得最佳运行速度,将 wraparound 和 boundscheck 设 置 为 False ,© 由于禁止了
wraparound,因此不能使用负数作为下标,所以这里将 x [_l ]改为 x [len(x )-1]。
© 当将内存视图传递给Python 闲数吋,它将被转换为 M em oryView 对象,而在 searchsorted〇
中会通过 asarrayO将其转换成 N um Py 数组。当然此处也可以使用x .base 直接将数组对象传递给
searchsorted()〇
O 为了让函数返回数组,需要通过 base 属性获得内存视阁对应的 N um Py 数组,然后对该

数组进行切片运算。

%%cython
import numpy as np
import cython

@cython.wraparound(False) O
@cython.boundscheck(False)
C y t h o n编

def getj3 eaks(double[::1] 乂, double[::1] y y int x0=None, xl=None):


cdef int i, j, index©, indexl
-译

cdef double x一imn, x_max, y_min, y_max, xc, yc, x一next, dx


cdef doublet::1 ] xr, yr
P y th o n

if x0 is None:
程序

x0 = x[0 ]
if xl is None:
xl = x[len(x) - 1 ] ©

index0 , indexl = np.searchsorted(x, [x0 , xl]) €


)
indexl = min(indexl> len(x) - 1 )
x0 , xl = xfindexO], x[indexl]
dx = (xl - x0 ) / n

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

return xr.base[:j], yr.base[:j] O

下 面 比 较 get_peaks_py()和 g e t _ p eaks () 的运算结果和运行速度。可以看到结果相同,而

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 )

%timeit get_peaks_py(x, y, 200 )


%timeit get_peaks(x, y, 200 )
True True
100 loops, best of 3: 9.55 ms per loop
10000 loops, best of 3: 78.7 ns per loop

scpy2.cython.fast_curve_d raw 演示了使用降采样提高 matplotlib 的曲线绘制速度。降采


样函数为 scpy2.cython.get_peaks〇。

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

与本节内容对应的 Notebook 为: KVcython/cylhorH^OOpython-api.ipynb,

在 10.2节中已经介绍过 Cython 可以识别 Python 的许多内置类型的常用操作,提高运算速


度 。此外还可以在 C y th o n 中 调 用 Python/C A P I 函数,实 现 -•些只有在 C 语言中汴能完成的
操作。

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 函数,提高创建列表的速度。
-译

下而的代码对比使用 A P I 函数和 append〇方法创建列表的速度。在 my_range()中调用如下


三 个 A P I 函数:
P y th o n

• objectPyList_New (Py_ssize_tlen ) : 创建指定大小的列表,列表的元素设置为N U L L 。注


程序

意这M 的 N U L L 是 C 语言的空地址,而不是 Python 的 N one 对象。也就是说,这个列表


M 然已经被创建了,但是其中的内容还没有初始化,无法在 Python 中使用。
• void PyList_ SET _ ITEM(object list,
Py _ ssize_t i, object 〇):设置列表 list 的指定下标 i 的内
容为〇。它实际上是 C 语言中的一个宏,可以用于快速设置元素为 N U L L 的列表中的
元素。
• voidPy _!NCREF (objecto ) : 将对象 〇的引用计数器加 1。当调j |jP y U s t _SET _ITEM ()将对
象 〇添加进列表时,该列表就应该增加对象〇的引用计数器,然 而 PyList_SET _ITEM ()
并不会丨自动增加被添加的对象的引用计数器,因此需要调用 PyJN C R E FO 。这种不增加
弓丨用计数的函数在Python A P I 说明文档中会被注明为: “steals a reference ”。

%%cython
#cython: boundscheck=False, wraparound=False
from cpython.list cimport PyList_New, PyList_SET_ITEM O
from cpython.ref cimport Py_INCREF

def my_nange(int n):


cdef int i
cdef object obj ©
cdef list result
result = PyList 一
New(n)

96
for i in range(n):
obj = i
PyList—SET_ITEM(result, i, obj)
Py_INCREF(obj)
return result

def my_nange2 (int n):


cdef int i
cdef list result
result =[]
for i in range(n):
result.append(i)
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 的头文件以. p x d 为扩展名,和 C 语言的头文件类似,可以包含各种函数和类型的定义。

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

目朵下包含N u m P y 相关的类型和 A P I 函数的声明,l i b c 目录下包含C 语言标准库中的各个函数

—译
的声明,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

由 于 m y _ m n g e 〇直接创建 FI 标大小的列表, 省去了逐步扩容带来的损耗,因此速度是使用


a p p e n d ( ) 的两倍多。

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

在 P y t h o n 中 tuple 对象是不可变的,但是可以在 C y t h o n 中 调 API 函数,在创建 tuple 对象


时设置其内容,从而实现快速创建 tuple 对象。下面的 t o j u p l e J i s t O 通过调用 A P I 闲数将二维数
组转换成元组列表。
和列表相同,元组也有对应的初始化函数:P y T u p l e _ N e w 和 P y T u p l e _ S E T _ I T E M 。 它们的

用法和列表的相同,这里不再重复了。

%%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 版)

from cpython.ref cimport Py_INCREF

def to_tuple_list(double[:, :] arr):


cdef int n
cdef int i, j
cdef list result
cdef tuple t
cdef object obj

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

下面比较 N um Py 数组的 tolist〇方法和 to_tuple_list〇的速度:


程序

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作为动态数组

在 NumPy — 章 中我们曾介绍过,可 以 将 P y t h o n 的标准模块 a r r a y 中 的 a r r a y 对象当作一维


动态数组使用。
在 C y 出o n 代码中,
也同样可以用array.aiTay 对象作为动态数组。
然而如果在C y t h o n
代 码 中 调 用 array.append 〇添加元素,则需要 调 用 P y t h o n 的函数对象,起不到提速的作用。在
C y t h o n 的 cpython/ a r r a y . p x d 头文件中提供了快速操作 a r r a y 对象的 C 函数。
下面的 in_circle〇收集二维坐标数组points 屮所有位于由c x 、
c y 和 r 表示的圆形内部的來标。
其中(cx ,
cy )为圆心坐标,^•为圆的半径。由于事先无法知道有多少点位于圆形内部,因此程序中
使 用 array.array 动态数组逐个添加满记条件的点。
%%cython -c-Ofast
#cython: boundscheck=False, wraparound=False
import numpy as np
from cpython cimport 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

O 用 cimport 关键字载入 a m y 的头文件之后,使)|j 其 中 的 array.array 类型定义一个 Cython

-译
变 量 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

%timeit points[(points[:,0 ] - cx) * * 2 + (points[:, 1 ] - cy) * * 2 < :]


%timeit in_circle(points, cx, cy, r)
10000 loops^ best of 3: 97.7 |as per loop
10000 loops^ best of 3: 38.6 |as per loop

99
Python 科学计算(第 2 版)

1 0 3 扩展类型

与本节内容对应的Notebook 为: 10-cython/cylhon-500-cdef-class.ipynb〇

在 Python 中通过 class 定义的类采用字典保存实例的属性,


然而为了提高屈性的访问速度,
P yth o n 内置的类型则直接将其屈性保存在对象结构体的字段屮。在 C y th o n 屮则可以通过 cdef
class 定义扩展类型。扩展类型和 Python 的内置类型一样,采 用 C 语言的结构体保存对象的各个
属性,因 此 在 Cython 程序中能够快速存取这些属性。扩展类型很适合j|j 于 包 装 C 语言的函数
库,提供面向对象的 Python 调用接口。

1 0 . 5 . 1 扩展类型的基本结构


‘而的程序使用cdefclass 定义扩展类型Point2D ,
并使用 cd ef 定义属性 x 和 y 。
注意和 Python
的类不同,扩展类型的属性在类中定义,而不是在_ init_()方法中生成。

%%cython

cdef class Point2D:


cdef public double x, y

Cython将0 动定义如下结构体来表示P o i n t 2 D 对象,由于 o b j r e f c n t 和 o b _ t y p e 两个字段是所


有 P y t h o n 对象必须具备•的,
因此在P y t h o n 的 C 语言代码中它们通常使)|j P y O b j e c t _ H E A D 宏定义。

struct _ pyx_obj_Point2D {
PyObject_HEAD
double x;
double y;
};

在 Cython 程序内部,当它明确知道对象 p 的类型为 Point2D 时,p .x 将被转换成直接访问


Point2D 结构体的 x 字段,因此对扩展类型变量的屈性存収是非常迅捷的。为 了 在 Python 中访
问 Point2D 的 x 和 y 屈性,
需要在声明屈性时使用public 关键字。
Cython 能自动为整型、
浮点型、
字符串类型以及 Python 对象类型这4 种 c d e f 属性创建属性访问用的描述器。这些描述器包含
_ get_〇和_ set_〇 方法,j (J 以获取和设置属性。对于只读属性,可 以 将 p u b lic 关键字替换为
readonly。下面的代码查看 x 对应的属性访问描述器:

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

cdef class Point2D:


cdef public double x, y

cdef class Point3D(Point2D):


cdef public double z

cdef Point3D p = Point3D()


p.x = 1 . 0
p.y = 2 . 0
p.z = 3.0

Point3D 对象对应的 C 语言结构体如下:

struct —pyx_obj_Point3D {
struct _ pyx_obj_Point2D —pyx一base;
double z;
};

它的第•个字段_ pyx_ base 是其基类 Point2D 对应的结构体。在 Cython 111通过_ pyx_base.x


访问® 类中定义的屈性.x ,而 在 Python 中,则 通 过 Point3D ._ base_ 中 x 对应的描述器访问该
属性。

10.5.2 —维浮点数向量类型

下面我们以一维浮点数以量Vector 类型为例介绍扩展类型的用法。Vector 对象拥有两个私


有属性:count表示数组的长度,data 为存储数组数据的首地址。

cdef class Vector:


cdef int count
cdef double * data

在创建一个新的 Vector 对象时,会从堆内存中分配一个结构体。在结构体的内存分配完毕


之后,应该立即初始化其中的属性。这个初始化工作Fll_ dnit_〇完成,可以将之理解为 C 语言
Python 科学计算(第 2 版)

级別的_ init_ 0 。V e cto r 对象支持两种初始化方式:分配指定大小的内存,或者使用一个序列


对象初始化数纟J1的内容。这里使丨丨j Python A P I 中的 PyMem _Malloc〇从 Python 管理的堆中分配存
储数组数据的内存。为了使用该 A P I 函数,需要使j IJ from cpython cimport m em 载入声明内存管
理 A P I 函数的头文件。

def —cinit一 (self, data):


cdef int i
if isinstance(data, int):
self.count = data
else:
self.count = len(data)
self.data = <double *>mem.PyMem_Malloc(sizeof(double)*self.count)
if self.data is NULL:
raise MemoryError

if not isinstance(dataj int):


C y t h o n编

for i in range(self.count):
self.data[i] = data[i]
-译

当对象的引用计数变为0 时,将被垃圾回收。在对象结构体被回收之前,C yth o n 会调用


P y th o n

_ dealloc_(),在这里需要释放 data 属性指向的内存:


程序

def _ dealloc_ (self):


if self.data is not NULL:
mem.PyMem_Free(self.data)

为 了 让 V e cto r 对象支持整数下标存取和循环迭代,需要定义_ len_〇 、_ getitem_(^U


_ setitem_()等方法。这种以两个卜‘
划线开头和结尾的方法被称为魔法方法,通过定义这些方法
可以改变对象在某种特定语法中的行为。例如查看对象长度的内置函数 len(obj)实际上返 N
obj._ len_ ()的 值 ,而 objfindex]会调用 obj._ getitem_ (index),obj[index] = value 则会调用
obj._ setitem_ ( index,
value)。如果某个类型定义了_ len_()和_ getitem_(),则其对象会 A 动支持
for 循环以迭代其中的元素。
在下面的程序中,_ getitem_〇 和_ setitem_ ()要求其下标参数为整型,因 此 V ector 对象不
支持切片下标。我们将支持负数下标和下标越界检查单独放在_check_index()方法中,该方法的
参数是一个整型指针,可以直接修改传进来的下标变量。如果下标越界,则 抛 出 IndexEiror•异
常。在 Cython 中使用 p [0]访问指针变量 p 指向的地址。

def —len一 (self):


return self.count

cdef _check_index(self, int *index):


if index[0] < 0:
02
index[0] = self.count + index[0]
if index[0] < 0 or index[0] > self.count - 1:
raise IndexError(nVector index out of range")

def — getitem_ (self, int index):


self,_check_index(&index)
return self.data[index]

def _ setitem_ (self, int index, double value):


self•一check—index(&index)
self.data[index] = value

为了让 Vector对象支持加法运总符,斋要定义_ add_ ()方法:

def _ add_ (self, other):


cdef Vector new,一self, —other

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

对于_ a d d _ 〇、_ m u l _ ( ) 这样的二元运算的魔法方法,第一个参数 self可能不是当前的对


象。当第一个参数对象无法完成运算时,将调用第二个参数对象的魔法方法,而参数的顺序不
变。例如,如果调用 1 + v , 其 中 v 是一个 Vector对象,那么整数 1 的_ a d d _ 〇函数调用将失败,
因此调用 Vector._ add_ ()时参数 self为 1,参 数 othei•为 v 。
程序中,O 首先判断 self的类型是否为 Vector,如果不是,就交换 self和 other所表示的对
象。
© 由于_ a d d _ 〇能处理数值和Vector对象 ,因此需要根据other对象的类型进行不同的处理。
© 由 于 self和 other变量没有类型声明,因此无法通过它们获取保存于C 语言结构体中的属性。
通过< \ ^ 〖
〇1> 将 Python对象转换成拥柯类型的变it_self和_ 〇此1-,
通过这两个变量可以访问count
和 data属性。© 创建保存运算结果的 Vector对象,并调用 add_array〇或 add_number〇进行计算,
稍后会介绍这两个函数的代码。

60
Python 科学计算(第 2 版)

下面是+ = 操作符对应的魔法方'?i_iadd_0:

def — iadd— (self, other):


cdef Vector _other
if isinstance(other, Vector):
_other = <Vector>other
if self.count != _other.count:
raise ValueError("Vector size not equal")
add_array(self.data, _other.data, self.data^ self.count)
else:
add_number(self.data, <double>otherJ self.data, self.count)
return self

和二元操作符不同,_ i a d d _ 〇的第一个参数就是当前对象,因 此 Cython 知道它的类型,


无须再进行类型转换。
下面用 cpdef定 义 norm()方法以计算矢量的长度,并在_ str_〇中调用它。当将对象转换成
C y t h o n编

字符审时,将调用_ 你_() 方法。cpdef定义的方法会同时生成 Cython 和 Python 调用接口,在


_ str_()中通过 Cylhon 的调用接口运行 norm 〇,而 在 Python 中则通过较慢的接口调用norm ()。
-译

def 一str_ (self):


P y th o n程 序

values = .join(str(self.data[i]) for i in range(self.count))


norm = self.norm()
return "Vector[{}]({})".format(norm^ values)

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

此外,扩展类型可以在 Python 中继承,禮盖■ 类中定义的 def和 cpdef方法,Python 中定


义的覆盖方法可以在Cython 中正确调用。由于在 Cython 中调用 cpdef方法时,需要检查此方法
是否被覆盖,因此其调用速度要略比cdef慢。
最后在代码的头部添加进行计算的add_airay()和 add_number〇两个函数,它们采用 cdef〉ii
义,只能在 Cython 代码内部调用。

cdef add_array(double *opl, double *op2, double *res, int count):


cdef int i
for i in range(count):

04
res[i] = opl[i] + op2 [i]

cdef add_number(double *opl, double op2 , double *res, int count):


cdef int i
for i in range(count):
res[i] = opl[i] + op2

读者可以试着完成其他二元计算函数以及元素存取函数。下而演示 Vector 对象的用法:

from scpy2.cython.vector import Vector


vl = Vector(range(5))
v2 = Vector(range(100, 105))
print len(vl)
print vl + v2
print vl + 2

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]

下面比较 Vector 对象和 N um Py 数组的矢量加法的运算速度:

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

下而比较 Vector 对象和 N um Py 数组的元素存取速度:

%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 0 0 0 0 0 0 loops, best of 3: 62.2 ns per loop


10 0 0 0 0 0 0 loops, best of 3: 62.1 ns per loop
10 0 0 0 0 0 0 loops, best of 3: 1 2 2 ns per loop
10 0 0 0 0 0 0 loops, best of 3: 108 ns per loop

10.5.3 包装 ahocorasick 库

扩展类型经常用于对 C 语言函数库进行包装,提供一个而向对象的 Python 调用接口。本


节以包装“多模式匹配兑法”的 C 语言库 ahocomsick 为例,介绍使用扩展类型包装C 语言函数
库的方法。
下面演示一下该扩展类型的使川方法。首先使用一组关键字创建MultiSearch 对象,然后调
用其• isin()方法,在目标字符串中搜索关键字,只要有一个关键字存在于该字符串中,就返回
True ,否则返N False 。

scpy2.cython.multisearch模 块 对 C 语言函数库 ahocorasick 进行包装。使用该模块可以


快速在大量文本中同时;}变索多个关键字。

from scpy2 .cython import MultiSearch

ms = MultiSearch(["abc", "xyz"])
print ms.isin("123abcdef")
print ms.isin("123uvwxyz")
print ms.isin("123456789")
True

False

search()方法可用于在目标字符串中搜索关键字所在的位置,它的第二个参数为一个回调函

数,每找到一个匹配位置就将该位置和匹配的关键字传递给该回调函数。回调函数返回0 表示
继续搜索,返 回 1表示结束搜索。

def process(pos, pattern):


print "found {0 } at {1 }".format(pattern^ pos)
return 0

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"
};

#define PATTERN_NUMBER (sizeof(allstr)/sizeof(AC_ALPHABET_t *))

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;

printf ("@ %ld : %s\n", m->position, m->patterns->astning);


/ * 返 回 0 继续搜索,返 回 1 停 止 搜 索 */
return 0 ;
}

int main (int argc, char ** argv)


{
unsigned int i;

AC_AUTOMATA_t * acap;
AC一PATTERN—t tmp_patt;
AC一ETXT一ttmp一 text;

//*** 创建AC_AUTOMATA_t结构体,并传递回调函数
acap = ac_automata一init();

/ / * * * 添加关键字

60
Python 科学计算(第 2 版)

for (i=0; i<PATTERN_NUMBER; i++)


{
tmpjDatt.astring = allstr[i];
tmpjDatt.rep.number = i+1 ; // optional
tmp_patt.length = strlen(tmp_patt.astning);
ac_automata_add (acap, &tmp_patt);
}

//***结束添加关键字
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 语言的闲数厍使用扩展类型进行包装。

cdef extern from "ahocorasick.h": O


ctypedef int (*AC__MATCH_CALBACK_f)(AC_MATCH__t void *) ©
ctypedef enum AC_STATUS_t: 0
ACERR__SUCCESS = 0
ACERR_DUPLICATE_PATTERN
ACERR_LONG_PATTERN
ACERR_ZERO_PATTERN
ACERR_AUTOMATA_CLOSED

ctypedef struct AC_MATCH_t: O


AC_PATTERN_t * patterns
long position
unsigned int match_num

60
ctypedef struct AC_AUTOMATA_t:
AC_MATCH_t match

ctypedef struct AC_PATTERN_t:


char * astring
unsigned int length

ctypedef struct AC_TEXT_t:


char * astring
unsigned int length

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 扩展类型的定义:

cdef class MultiSearch:

cdef AC_AUTOMATA_t * _auto O


cdef bint found
cdef object callback

09
Python 科学计算(第 2 版)

cdef object exc_info

def —cinit—(self, keywords):


self•一auto = ac一automata_init()
if self•一auto is NULL:
raise MemoryError
self.add(keywords) ©

def _ dealloc_ (self):


if self.一auto is not NULL:
ac_automata_release(self._auto)

cdef add(self, keywords):


cdef AC_PATTERN__t pattern
cdef bytes keyword
cdef AC_STATUS_t err
c y t h o n编

for keyword in keywords: €)


pattern.astring = <char *>keyword
—译

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〇方法的定义:

def isin(self, bytes text, bint keep=False):


cdef AC_TEXT_t temp_text O
temp一text.astring = <char *>text
temp_text.length = len(text)
self.found = False ©
ac 一a u t o m a t a -S e a r c h ( s e lf •一a u t o , & te m p _ te x t, k e e p , i s i n 一c a llb a c k , < v o id * > s e lf ) €)
re tu rn s e lf.fo u n d
〇将 13标字符串用 A C _TEXT _ t 结构体打包Z 后,© 传 递 给 ac_ automata_ search()进行搜索。
在搜索之前,© 设 置 found 属性为 False。将 isin_callback()函数的地址传递给ac_automata_search〇
作为搜索的回调函数。
其最后一个参数为传递给回调函数的)I护 数 据 ,
这里通过它将MultiSearch
对象的地址传递给N 调函数。这样在N 调闲数中就可以访问MultiSearch 对象的属性了。
下面是 isiii_CanbaCk ()回调函数的定义,注意由于该函数在 C 语言厍内部被调用,只能使用
cd ef 定义:

cdef int isin_callback(AC_MATCH_t * match, void * param):


cdef MultiSearch ms = <MultiSearch> param O
ms.found = True ©
return 1 ©

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程 序
对象,每找到一个匹配位置就调用该对象进行处理:

为了介绍 C 语言回调函数和 Python 回调函数的用法,这里使用 ac_automata_search(),


每j 实际上使用后面介绍的ac_automata_ findnext〇可以更方便地编写 isin〇和 search()函数。

def search (self bytes text, callback, bint keep=False):


cdef AC_TEXT_t temp_text
temp_text.astring = <char *>text
temp_text.length = len(text)
self.found = False
self.callback = callback O
self.exc_info = None
ac_automata一search(self._auto, &temp_text, keep, search_callback, 〈
void *>self) ©
if self.exc_info is not None:
raise self.excjnfofl]., None, self.exc_info[2] ©

© ac_automata_ semrh()的回调函数为search_callback()。为 / 在其中调用 Python 的可调用对象,


O 将 callback 参数传递给 self.callback 。© 由于 C 语言的函数无法向上传递 Python 函数所抛出的
异常信息,因此还需要通过e x c jn fb 属性传递 Python 回凋函数中可能抛出的异常。
Python 科学计算(第 2 版)

下面是 search_callback 〇回调函数的定义:

cdef int search_callback(AC_MATCH__t * match, void * param):


cdef MultiSearch ms = <MultiSearch> param
cdef bytes pattern = match.patterns.astring
cdef int res = 1
try:
res = ms.callback(match.position - len(patternpattern) O
except Exception as ex:
import sys
ms.exc_info = sys.exc_info() ©
return res

将興第二个参数转换为M u l t i S e a r c h 对象之后,O 调)|j callback 指向的 P y t h o n 丨


H] 调函数,并

捕捉可能抛出的异常。© 将异常及回溯信息保存在属性e x c _ i n f o 中。在 search() 的最后检查属性


e x c _ i n f o ,若被设置,贝抛出其中的异常对象。注意这里使用 sys.exc_info 〇获取异常信息,而不

c y t h 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程 序

def iter_search(self, bytes text, bint keep=False):


cdef AC_TEXT_t temp_text
cdef AC_MATCH_t * match
cdef bytes matchedjDattern
temp_text.astring = <char *>text
temp_text.length = len(text)
ac一automata一settext(self•一 auto, &temp_text, keep)
while True:
match = ac_automata_findnext(self._auto)
if match == NULL:
break
matchedjDattern = <bytes>match.patterns.astring
yield match.position - len(matchedjDattern)^ matched_pattern

10.6 C ython 技巧集

与本节内容对应的 Notebook 为:10~cython/cython-600~tips.ipynb。


DVD
相信读者通过前面章节的介绍,已经掌握了使川 C y t h o n 提 高 P y t h o n 程序运算速度的一些
-基本方法。作为本章的最后一节,让我们看看 C y t h 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

O 使用N u m P y 的 C - A P I 编 写 uflinc 函数的教程。

由于 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丨值的字符数组。

• d a t a : 空指针数组,其中的指针指向传递给 functions 中对应函数的额外数据。

下而的程序创建一个汁算L o g i s t i c 函数的 u f u n c 函数 logistic〇:

%%cython
from libc.math cimport exp
from numpy cimport (PyUFuncGenericFunction^ npy_intp, import_ufunc,
NPY_D0UBLE, PyUFunc_None, PyUFunc_FromFuncAndData) O

import一ufunc() ©

edef void double 一


logistic(char **args, npy_intp *dimensions,
npy 一
intp* steps, void* data):
edef: #用缩进可以定义多行edef 变量
npy一intp i
npy_intp n = dimensions[0 ]
char *in_ptr = args[0 ]
char *outjDtr = args[l]
npy一intp in一step = steps[0 ]
npy_intp out一step = steps[1 ]

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] ©

logisticl = PyUFunc_FromFuncAndData(functions, data, signatures, ©


1, #ntypes
1, #nin

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
—译

O C ython 包含了 N um Py 的 C -A P I 的声明文件,首先通过 cimport从其中载入将要用到的闲


P y th o n程

数和类型声明。© 在 调 用 N u m P y 的 任 何 u ftm c 相 关 的 C -A P I 函数之前,需要首先调用


import_ufunc〇,否则程序会异常终止。
我们所创建的 l〇
gistic()函数只对元素类型为 double 的数组进行计算,© 因 此 functions 中只
保存对一维 double 数组进行计算的 double_ logistic()函数的地址。
PyUFuncGenericFunction为 double_ logistic()的函数指针类型,参数的含义如下:
• args: 保存所有输入输出数组的地址,args[0]为输入数组的地址,args[l ]是输出数组的
地址。
• dimensions: 保存循环的次数。
• steps: 保存各个数组的元素之间的间隔,steps〖
0j 为输入数纟J1的元素间隔,stepsllj 为输出
数组的元素间隔。
• data: 传递给该闲数的额外参数,在 C 语言中,为了表示任意类型的额外参数,通常使

用 void *类型的指针。
由上而的参数可知,PyUFuncGenericFunction类型的函数可以处理各种元素类型的数组,而
数组元素无须连续存储,可以处理使用切片获得的数组视图,并且输入数组的个数和输出数组
的个数是任意的。
Osignatiu^ 中保存表示 ufim c 函数的输入和输出数组的元素类型,本例中输入和输出都是
双精度浮点数,因此值为[N PY _D OUBLE ,
N PY _ DOUBLE ]。注意这里使用的是 Cython 中初始化

数组的语法。它相当于:

cdef char signatures[2 ]

61
signatures [0] = NPY_CXXJBLE
signatures[1] = NPY_DOUBLE

© double_ logistic()无须额外的参数,因此 data 被初始化为一个指向 N U L L 的空指针。


©最 后 调 用 PyUFunc_FromFuncAndData()创建 ufunc 函数对象。
它的头三个参数为functions、
data、signatures,接下来的参数为:
• ntypes: 该 uflinc 函数支持的数据类型的个数,即 functions 数纽的长度。由于本例中只
支持双精度浮点数,因此值为1。
• nin: ufunc 闲数的输入数组的个数。
• nout: ullinc 闲数的输出数组的个数。
• identity: 可选值为 PyUFunc_One 、PyUFunc_Zero 、PyUFunc_None 。该参数指定 uflmc 函
数的 reduce()方法的参数为空数组时的返回值。由于木例中的ufunc 函数为单输入函数,
因此 reduce〇方法无效。
• name 和 doc_string: 分别指〉ii ufunc 函数名和帮助文杉1。
如果希望 logistic()能够处理单精度浮点数,
可以添加进行单精度浮点数运兑的float_ logistic〇

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]

最后将 PyUFunc_FromFuncAndData〇的 ntypes 参数设:置为 2。


下面测试 bgisticU )函数。虽然只定义了一个double 类型的计兑函数,但是它仍然可以处理
各种类型的数组,甚至列表,这是因为在 uflm c 函数内部会将参数转换成double 数组,然后再
传递给 double_logistic〇进行计算。如果参数楚多维数组,uflinc 函数会对每个轴进行循环,多次
调 用 double_logistic〇实现对整个数纟J1的计算。

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

from numpy cimport (PyUFuncGenericFunction, import—ufunc, PyUFunc_d_dJ


NPY_DOUBLE, PyUFunc_None, PyUFunc—FromFuncAndData)

impont_ufunc()

cdef double scalar_logistic(double x): O


return 1 . 0 / (1 . 0 + exp(-x))

cdef:
PyUFuncGenericFunction *functions = [PyUFunc_d_d] ©
char *signatures = [NPYJXXJBLE, NPYJXXJBLE]
void **data = [&scalar_logistic] €)

logistic2 = PyUFunc_FromFuncAndData(functionsdata, signatures,


1, 1, 1, PyUFunc—None,
"logistic",
c y t h o n编

"a sigmoid function: y = 1 / ( 1 + exp(-x))",


—译

0)

logistic2〇与 logisticlO 的差别仅仅是多了一次C 语言的函数调用。下面测试该调用所带来的


P y th o n

损耗,从结果可以看出二者的差距并不大,而 bgistic 2〇的实现更加简洁、更容易维护:


程序

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

实际上,已经有人将N um Py 的 C -A P I 中与PyUFunc_d_ d()类似的函数包装在Cython 的 include


文件中。include 文件通过 include 关键字载入,它 和 C i吾言的#include 预处理命令类似,将指定
文件的内界插入0 标文件1丨
1。在 numpy_ufuncs.p x i 屮使用了前面介绍的方法来创建uflm c 函数,
请感兴趣的读者0 行阅读源代码。
在 numpy_ufuncs.p x i 中提供了与 functions、signatures、data 类似的三个数纟11,它们可以容纳
100 个 PyUFuncGenericFunction 涵数。
在下面的例子中,
通过 register_ufunc_d()和 register_ufunc_dd()
分别将一元闲数 scalar_logistic()和二元闲数 scalar_peaks〇转换成 ufunc 闲数:

%%cython
include "numpy_ufuncs.pxi"
from libc.math cimport exp

cdef double scalar_logistic(double x):


return 1 . 0 / (1 . 0 + exp(-x))
cdef double scalar Djeaks(double x, double y):
return x * exp(-x*x - y*y)

logistic3 = register一ufunc_d(scalar_logistic,
"logistic", "logistic function' PyUFunc—None)

peaks = register_ufunc一
dd(scalar_peaks,
"peaks", "peaks function"^ PyUFunc_None)

下面通过 ogrid 创建两个可广播成二维网格的数组X 和 Y , 然后调州 peaks〇计算网格上所


有点对应的函数值:

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);
}

unsigned int get_addr()


{
return (unsigned int)(void *)peaks;
}

接下来调用 g c c 将 peaks.c 编译成 peaks.dll:

!gcc -Ofast -shared -o peaks.dll peaks.c

然后通过 ctypes 载 入 peaks.dll,并为其中的 peaks()函数声明参数类型和返回值类型,最后


Python 科学计算(第 2 版)

调用该函数:

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

lib.peaks是 ctypes对 C 语言的函数地址进行包装之后的对象,为了获取其中的 C 语言函数


地址,可以将该对象通过 cast()转换成空指针类型,然后通过空指针对象的value属性获得该指
针的内容,即 C 语言函数的地址。下面的程序获得 C 语言函数的地址,并 与 get_addr〇的返回
值进行比较:

addr = ctypes.cast(lib.peaks, ctypes.c一


void一p).value
addr == lib.get_addr()
True
c y t h o n编

K 面 用 Cython 编 写 vectorize_2d ()函数,其 func 参数是 ctypes的闲数指针对象,x 和 y 楚两


P y th o n —译

〇首先通过 ctypedef关键字声明二元双精度抒点数函数指针类型Function。
© 在 vectorize_2d 〇
中首先通过 cast〇获得函数的地址,然后©将该地址转换成 Function类型的函数指针,接下来就
程序

可以在双重循环中通过该函数指针快速调用其指向的C 语言函数了。

%%cython
import cython
import numpy as np
from ctypes import cast, c_void_p

ctypedef double(*Function)(double x, double y) O

@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

for i in range(x.shape[0 ]):


for j in range(x.shape[l]):
res[i, j] = func_ptr(x[i, j], y[i, j])

return res.base
下面通过 vectorize_2d 〇调 用 lib.peaks 〇, 并与通过 vectorizeO 创建的 u f i m c 函数的结果比较:

Y , X = np.mgrid[-2:2:200j , -2:2:200j]

vectorize_peaks = np.vectorize(lib.peaks, o t y p e s = [ ' f 8 ' ])


n p .allclose(vectorizejDeaks(X, Y), v e c t o r i z e _ 2 d ( l i b .p e a k s ^ X, Y))

True

vectorize_2d〇只能对两个连续存储的二维数组进彳j 循环,如果希塑像 ufunc 函数一样对任总


数组进行计算,可以使用扩展类型对numpy_ufuncs.p x i 中的函数进行简单包装。
OUFunc _d d 扩展类只有一个 ufunc 属性,用来保存创建的 ufunc 函数对象。© 在其_ cinit_()
方法中创建一个调用其参数 fu n c 的 ufunc 函数对象。使 用 set_ func()方法可以修改 ufunc 函数的
额外参数。© 将 ufim c 屈恍转换为 C 语言的 ufim c 结构体,该结构体的定义可通过 from numpy
cimpoit u t o c 载入,该载入操作已经在 iuuripy_ ufuncs.p x i 定义〇© 然后将传入的函数地址写入
u fu n c 结 构 体 的 d a ta 字段所指向的数组中。由 于 register_ ufunc_dd()创 建 的 u fu n c 函数包含两个

PyUFuncGenericFunction 函数指针,因此滞要同时修改这两个函数的额外参数。

C y t h o n编
%%cython

include " n u m p y_ufuncs.pxi"

-译
from ctypes import cast, c一void_p

P y th o n程 序
cdef class UFunc_dd:
cdef public object ufunc O

def — cinit— (self, f u n c ) : ©

cdef size_t addr = cast(func, c_void 一p).value


self.ufunc = register_ufunc_dd(<double (*)(double, double)>addr,
"ufunc", "variable ufunc", PyUFunc_None)

def set 一func(self, f u n c ) :


cdef ufunc f = <ufunc> self.ufunc ©

cdef size_t addr = cast(func> c_void_p).value


f.data[0] = <void *>addr O

f.data[l] = <void *>addr

下而先让所创建的 ufunc 函数使用 lib.peaks〇进彳」


:计算,然后通过 set_ func()将其指向的运算
函数修改为 ctypes.cdll.msvcrt.atan2,即 C 语 言 的 math.h 中定义的 atan2〇函数。使用这种方法,
可以将任何动态链接库中的类型为ciouble(double,
double)的函数通过set_ func()传递给 ufunc_ dd 对
象,使其与 N um P y 内置的 ufunc 函数一样能对数组进行广播运算。

ufunc 一dd = UFunc_dd(lib.peaks)

Y, X = np.ogrid[-2:2:200j, -2:2:200j]

assert n p .allclose(vectorize_peaks(X, Y), u f u n c _ d d .ufunc(X, Y))


Python 科学计算(第 2 版)

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 )

assert np.allclose(np.arctan2(X, Y), ufunc_dd.ufunc(X, Y))

1 0 . 6 . 3 调 用 BLAS 函数

B L A S 是•套基础线性代数程序集的 A P I 标准,S c iP y 的许多高速线性代数运兑函数内部都


会调用用 Fortran语言编写的 B L A S 函数。虽然这些 Fortran 函数的运算效率很高,但 Python 的
调州接口会造成一定的效率损耗,在大量循环中调州时这种损耗不能忽略不计。可以使 WJ
Cython 循环调用这些函数,从而彻底摆脱 Python 调用接口的束缚。

1.包 装 saxpy()函数

B L A S 中的 A P I 闲数可以通过 scipy.linalg.blas 模块访问,下面演示调用其中的 saxpy〇:

from scipy.linalg import bias

import numpy as np

x = np.array([l, 2, 3], np.float32)


C y t h o n编

y = np.array([l, 3, 5], np.float32)

blas.saxpy(x, y, a=0.5)
-译

array([ 1.5, 4. , 6.5], dtype=float32)


P y th o n程 序

saxpy 是 • 个 < fortranobject> ,其代码可以在 fortranobject.h 和 fortranobject.c U ^ J^ lj。这两个


文件可以在 N um Py 的安装0 录下找到,位 于 numpy\f2py\src 文件夹下。

blas.saxpy

<fortran o b j e c t >

K _cpointer屈性.为包装fortran 函数地址的 PyCObject 对象。要获得 PyCObject 包装的指针,


可以调川 Python 的 C-API 中的 PyCObject_AsVoidPtr:

void* PyCObject_AsVoidPtr(PyObject* self)

下面的程序通过 ctypes 调j|j Python 的 C -A P I 函数,首先通过 py_object〇将 Python 对象转换


成指叫 PyObject 结构体的指针,然后调用 PyCObject_AsVoidPtr〇获得该 PyCObject 对象所包装的
地址,saxpy_addr就 楚 B L A S 扩展萍中 Fortran 函数 saxpy()的地址:

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,

real, dimension(*) SY,


integer INCY

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 , 并通过前面介绍的方法获得

F o r t r a n 闲数的地址,并转换为闲数指针_$〇\9>^。然后定义调用_ s a x p y 的 blas_saxpy() 和直接在

P y th o n程 序
Cython 中循环的 cython_saxpy():

%%cython

import cython
from cpython cimport PyCObject_AsVoidPtr

from scipy.linalg import bias

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)

def b l a s _ s a x py ( f l o a t [ :] y, float a, f l o a t [:] x ) :

cdef int n = y.shape[0]


cdef int inc_x = x . s t r i d e s [0] / sizeof(float)
cdef int inc_y = y . s t r i d e s [0] / sizeof(float)
_saxpy(&n, &a, & x [ 0 ] , &inc_x, & y [ 0 ] , &inc_y)

@ 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 版)

下面比较二者的运行速度,blas_ saxpy〇要 比 Cython 的循环快三倍左右:

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 的说明文档。

B L A S 中 的 DGEM M 〇实现如下矩阵乘积运算,当参数 alpha 为 1、beta 为 0 吋,结 果 C 为


矩 阵 A 和 B 的乘积:
C y t h o n编

C = alpha*op(A)*op(B) + beta*C
-译

其中的 op〇可以对矩阵进行转置。1±1于Fortmn数纟11与 C 语言数组的轴的顺序相反,为了对


两 个 C 语言的数组表示的矩阵进行乘积运算,耑要设置 op〇为转置。而无论是否转置,运算结
P y th o n程 序

果 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 语言的调用函数原型:

void dgemm( char *ta, char *tb,


int *m, int *n, int *k,

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

ctypedef void (*dgemm_ptr) (char *ta, char *tb,


int int *n, int *k,
double *alpha,
double *a, int *lda,
double *b, int *ldb,
double *beta,
double *c, int *ldc) nogil

cdef dgemm_ptr _dgemm=<dgemm_ptr>PyCObject_AsVoidPtr(blas.dgemm._cpointer)

@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

for i in prange(length, nogil=True): ©


idx_a = index[i, 0 ]
idx_b = index[i, 1 ]
_dgemm(&ta, &tb, &n, &alpha,
&A[idx_a, 0, 0], &k,
&B[idx_b, 0, 0], &n,
&beta,
&C[i, 0, 0], &m)
C y t h o n编

return C.base
-译

N u m P y 中新增加的 gu ftm c 闲数能把单个矩阵的运算通过广播运用到整个数组之上。虽然


P y th o n程 序

numpy.lin alg 中提供了许多 gufunc 数,但楚缺少矩阵乘积的 gufunc 函数。类似的功能可以利


用上面的 dgemm()来实现。下面的 matrix_multiply(a,
b )对两个任意维数的数组的最后两个轴进行
矩阵乘积运算,而对其他轴进行广播运算。例如,若 a 的形状为(12,1,10,100,30),b 的形状为(1,
15,1,
30,50),最后两轴对应的矩阵乘积之后的形状为(100,50),而其他轴广播之后的形状为(12,
15,10),因此结果数组的形状为(12,15,10,100,50)。一共进行了 12X 15X 10次矩阵乘积运算。
该程序的实现思路如下,请读者自行研究,细节部分就不详细叙述了 :
•对 a 中的每个矩阵编号,并把编号的形状修改为a 的广播部分的形状,得 到 i d x _ a 。
•对 b 进行同样的运算,得 到 i d x _ b 。
• 使 broadcast_arrays 〇计 算 i d x _ a 和 i d x _ b 广播之后的数組。

•将上述两个数组平坦化之后排成两列,就得到了 dgemmO函数的 index 参数。


• 将 a 和 b 的形状都修改为三维数组并传递给dgemm()函数。

def matrix_multiply(a> b):


if a.ndim <= 2 and b.ndim <= 2 :
return np.dot(a, b)

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)

idx_a = np.arange(len_a> dtype=np.int32).reshape(shape_a)


idx一b = np.arange(len_b> dtype=np.int32).reshape(shape_b)
idx一a, idx_b = np.broadcast一arrays(idx_a, idx_b)

index = np.column_stack((idx_a.ravel()> idx_b.ravel()))


bshape = idx_a.shape

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

N um Py 的 umath_tests 模块中提供了一个测试j |j 的计兑矩陈乘积的gufunc 函数。


下面与该函
数比较,可以看出运算结果是-•致的:

import numpy.core.umath_tests as umath


a = np.random.rand(12, 1, 10, 100, 30)
b = np.random.rancK 1 , 1% 1 , 30, 50)
np.allclose(matrix_multiply(a> b), umath.matrix_multiply(a> b))
True

下面是二者的运算速度比较,如果读者查看 C P U 的使用率,就会发现运行 matri^multiplyO


时,所 有 C P U 都会用于计算矩阵乘积。「
II于采用了并行运算与高速的B L A S 闲数,因此二者的

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使用泊松混合合成图像

与本节内容对应的 Notebook 为:11•examples/examples-100~possion.ipynb。

本节通过一个图像处理的实例程序复习N um Py 数组、O p en C V 以及 S d P y 中的稀疏矩阵的


川法。泊松混合是一种图像合成算法。它将源图像中指定区域内的纹理信怠复制到目标图像的
对应区域内。效果如图11-1所示,从左侧开始依次为:源图像、混合区域、 R 标图像以及泊松
混合的结果。

图 1 1 -1 泊松混合示意图

11.1.1泊松混合算法

纹理信息通过拉普拉斯算子计算,可以直接通过 O p e n C V 中 的 Laplacian〇来计算。对于
3 x 3 的情况,LaplacianO实际上就是使用如下卷积核与图像进行卷积运算:

0 1 0

即图像中每个像素值乘以-4后 加 I:其丨:下左右4 个像素的值。


所谓泊松混合,就是指对于指定的区域,让 PI标图像的 Lapladan 运算结果与源图像的运算
Python 科学计算(第2 版)

结果相冋。图 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编写代码

我们要编写的函数的参数如下:

poisson_blending(src, dst, src_mask, offset_x, offset_y, mix=False)

其 中 src 和 dst 分别为源图像和目标图像,它们的形状不一定相同。src__m ask 是宽和高与 src


相同的二维数组,其中不为零的元素指定混合的区域。o f f s e t s 和 o f f s e t 为冃标图像中指定区
域和源图像区域之间的坐标差值,通过这两个参数可以调整IeI 标阁像中混合区域的位H 。 mix
参数指定混合方式,稍后再进行详细解释。
首先需要解决的问题是计算源图像中src_ im s k 指定 K 域通过拉普拉斯算子运算之后的值:

import cv2

offset_x, offset_y = -36, 42


src = cv2 .imread("vinci_src.png", 1 )
dst = cv2 .imread("vinci_target.png", 1 )
mask = cv2 .imread("vinci—mask.png'、 0 )
src_mask = (mask > 128),astype(np.uint8)

src_y^ src_x = np.where(src_mask) O


src_laplacian = cv2.Laplacian(src, cv2.CV_16S, ksize=l)[src_y, src_x, :] ©

O 由于指定R 域不•定为矩形,我们用 n u m p y .where()获得R 域中各像素的下标,这样得到


的 srcjaplacian数组是一个二维数组,它的第0 轴的长度为冈域内像素的个数,第 1轴的长度为
3 , 分别与三个颜色通道对应。
© 由 于 LaplacianO计算的结果中存在负数,因此这里通过它的第二个参数ddepth指定结果
数组的元素类型,这里指定它为 C V _16S , 表示结果为一个16位的符号整数数组。接下来计算
H 标图像中的区域信息:

dst_mask = np.zeros(dst.shape[:2 ], np.uint8 )


dst一 dst_y = src_x + offset_x, src_y + offset_y
dst_mask[dst_y, dst_x] = 1 O

kernel = np.array([[0 ,1 ,0 ],[1 ,1 ,1 ],[0 ,1 ,0 ] dtype=np.uint8 )


dst_mask2 = cv2 .dilate(dst_mask, kernel=kernel) ©

dst_y2 , dst_x2 = np.where(dst_mask2 ) ©


dst_ye, dst_xe = np.where(dst_mask2 - dst_mask) O

〇甶于遮罩图像在目标图像和源图像中存在位置偏移,因此先创建固标图像的一个遮罩数
Python 科学计算(第 2 版)

组 dst_mask ,并将偏移之后的下标中对应的元素设置为1。© 将 EI标遮罩数组进行膨胀处理得


到 dst_ mask2 。€)计 算 dst_ mask2 中 1对应的坐标(dst_x2, dst_ y2),© 计算膨胀处理新增加的像素
的坐标(dst_Xe ,
dst_j e ) , 即目标区域的边缘像素的坐标。
对照图11-1(右)可知,(
dst_x 2,
dst_j 2)为所有灰色区域的像素坐标,(
dst_x ,
dst_y )为浅灰色区
域的像素坐标,(
dst_xe ,
d s t j e )为深灰色区域的像素坐标。

variable_count = len(dst_x2 )
variable—index = np.arange(variable_count) O

variables = np.zeros(dst.shape[:2 ], np.int)


variables[dst_y2 , dst_x2 ] = variable—index

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 :

from scipy.sparse import coo_matrix


inner一count = len(x0 )
edge_count = len(x一edge)

r = np.r_[x0 , x0 , xQ, xQ, xQ, x一edge]


c = np.r_[x0, xl, xl, x3, x4, x_edge]
v = np.ones(inner_count*5 + edge_count)
v[:inner_count] = -4

A = coo_matrix((Vj (r) c))).tocsc()

与(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(右)为泊松混合的计算结果。

fig, axes = pit.subplots(1, 4, figsize=(10, 4))


axl, ax2j ax3, ax4 = axes.ravel()
a x l . i m sh o w ( s r c[ , ::-1 ])
ax2 .imshow(maskj cmap="gray")
ax3.imshow(dst[:, ::-1])
ax4.imshow(result[:j :y ::-1])

for ax in axes.ravel():
ax.axis("off")

fig.subplots_adjust(wspace=0.05)

图 1 1 -2 使用泊松混合算法将吉内薇拉•班琪肖像中的眼睛和鼻子部分复制到蒙娜丽莎的肖像之上
Python 科学计算(第 2 版)

11.1.3演示程序

为了方便读者观察泊松混合的效果,本书提供了一个使ffl TmitsUI 编写的泊松混合演示程


序 ,界面如图11-3所示。该界面的用法如下:
•首先选杼两幅图像的路径,然后按 Load 按钮载入图像。
•使用鼠标左键在阁像上绘制混合区域,可以使用鼠标滚轴修改画笔的粗细。按住鼠标
右键可以移动混合区域。
• 按 M ix 按钮进行泊松混合,将左阁中区域的内容混合到右图中对应的位置。为了显示
混合效果,右图屮的区域将自动隐藏,单击可重新显示区域。

scpy2.examples.possion : 使 用 T raitsU I 编 写 的 泊 松 混 合 演 示 程 序 。该程序使用


scpy2.matplotlib.freedraw_widget 中提供的 ImageMaskDrawei•在图像上绘制半透明的白
色区域。

图 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

其中a 为决定下乘•程度的系数,下 而 的 catenaiyO函数对悬链线方程进行处标平移,使得它


经过(0,0)和(1,
0)两个点,效果如图1M 所示:

def catenary(x, a):


return a*np.cosh((x - 0.5)/a) - a*np.cosh((-0.5)/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

定积分中的积分项可以使用S y m P y 进行符号运算,并 使 用 l a m b d i f y O 将最终的表达式转换


成函数,然后使用 integrate.quad 〇进行定积分运爵::

from sympy import symbols, cosh, S, sqrt, lambdify


import sympy
x, a = symbols("x, a")
y = a * cosh((x - S(l)/2) / a)
s = sqrt(l + y.diff(x)**2)
fs = lambdify((x, a), s, modules=,,math")

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)

def diff_status(status, t):


x, y, vx, vy = status.reshape(4, -1)

634
dvx = np.zeros一like(x)
dvy = np.zeros一like(x)
dx = vx
dy = vy

s = np.s_[l:-l]

1 1 = np.sqrt((x[s] - x[:-2 ] ) * * 2 + (y[s] - y[:-2 ])**2 )


1 2 = np.sqrt((x[s] - x[2 : ] ) * * 2 + (y[s] - y[2 :])**2 )

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]

status0 = np.r_[x0 , y0 , vx0 , vy0 ]

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

scpy2.examples.catenary: 使 用 TraitsUI 制作的悬链线的动画演示程序,可通过界面修


改基个参数。

在 阁 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):

return np.sum(l * np.cos(theta)) - 1 . 0

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)

theta0 = np.arccos(1.0 / length)


theta_init = [-theta0] * (N // 2) + [theta0] * (N // 2) O

theta = optimize.fmin_slsqp(P, theta_init,


eqcons=[gl, g2], ©

bounds=[(-np.pi/2, np.pi/2)]*N) ©

Optimization terminated successfully. (Exit mode 0)


Current function value: -7.76529946378

Iterations: 9
Function evaluations: 288
Gradient evaluations: 9

可以看到只迭代了 9 次就找到了最优解,下面根据仏计算出每个质点的位置,如 图 11-7所


示。图中蓝色曲线为悬链线方程所得的理论值。

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

x = np.r_[0, np.cumsum(l * np.cos(theta))]

637
Python 科学计算(第2 版)

y = np.r_[0, np.cumsum(l * np.sin(theta))]

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)

I ).0 0.2 0.4 0.6 0.3 1.C

图 1 1 - 7 使用 M n j d s q p O 计舞:能i B ® 低的状态,叉点表示® 优化的初始状态

11.2.2最速降线

所谓最速降线问题,是指在两点之间建立一条无摩擦的轨道,使得小球从高点到低点所需
的时间最短。考虑两点高度相同的极端情况,显然这条llll线不是直线。根据维S 百科的相关介
绍,下降高度为D 的最速降线满足如下方程:

y
dx = dy
D-y

由公式可知,y 的取值范围为0 到D 之间。下面先用数值定积分计算D 为 1 时,最速降线终


点的 X 轴坐标:

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 的值判断£的符号。

def brachistochrone_curve(D, N=1000):


eps = le- 8
def f(y^ x):
y = min(max(epSj y), D) O
flag = -1 if x >= D * np.pi / 2 else 1 ©
return flag * ((D - y) / y) ** 0.5

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.使用优化算法计算最速降线

可以使用优化算法近似计算最速降线。首先将 X 轴 从 0 到 target等分为 N -1份 ,每个点对


应 的 Y 轴坐标y 就是要优化的^变 M 。优化的目标是计算小球经过该曲线所需的时间。根据能
量守能定理,可以计算出小球到达每个坐标点时的速度为v = ^ j 2 g y 〇
小球经过每条线段的速度按照两个端点速度的平均值计算,线段的长度根据两个端点的距
离计算,由此可以得出小球经过每条线段的时间。优化的目标就是所有这些时间之和最小,即
小球通过整条曲线的时间最短。
下面是使用优化箅法计兑最速降线的程序,计算结果如图11-9所示。由图可知最优化的结
果和积分运算的结果相N 。

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)

pl.plot(x2 , -y2 , label=u"最速降线••)


pi.legend(loc="best")
Optimization terminated successfully. (Exit mode 0)
Current function value: 2.53634462725
Iterations: 72
Function evaluations: 7370
Gradient evaluations: 72

初始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所示:

from math import sin

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

track = integrate.odeint(pendulum一equationsj (1 .0 ^ 0 ), t, args=(1 .0 ,))


pl.plot(t, track[:, 0 ])
pl.xlabel(u__ 时间(秒)")
pi •ylabel (u"振动角度(弧度)••)

时间(妙)

图 1 1 -1 1 初始角度为1 弧度的单摆摆动角度和时间的关系
I
1.小角度时的摆动周期

高中物理课介绍过当® 大摆动角度很小时,单摆的摆动周期可以使用如下公式计像

i
T 〇= 2 ti
g

这是因为当e 《 i 时,sin 0 « e ,这样微分方程就变成了:


d 20 g
十 T 0
d t2 t

此微分方程的解是一个简谐振动方程,很容易计算其摆动周期。下而我们用 S y m P y 对这
个微分方程进行符号求解:

from sympy import symbols, Function, dsolve


t, g, 1 = symbols("t,g,l", positive=True) # 分别表示时间、重力加速度和长度
y = Function("y") # 摆角函数用y(t)表示
dsolve(y(t).diff(t,2 ) + g/l*y(t), y(t))

y(t) = CiSin + C2cos

可以看到简谐振动方程的解是由两个频率相同的三角函数构成的,周期为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 的摆角:

def pendulum_th(t, 1 ^ th0 ): s


track = integrate.odeint(pendulum_equationSj (th0^ 0)^ [0,t]^ args=(l>))
return track[-1 , 0 ]

此闲数仍然使用 Odeimo进行微分方程组求解,只是我们只需要计算时刻t 的摆角,因此传


递 给 odeintO的时间序列为[0, t]。odeintO内部会对时间进行细分,保证最终的解是正确的。
接下来只需要找到第一个使pendulum_th〇的结果为0 的时间即可。
这相当于对pendulum_th〇
求解,可以使用 scipy.optimize.fsolve()对这利1非线性方程进行求解:

from scipy.optimize import fsolve

def pendulum_period(l, th0 ):


t0 = 2*np.pi*(l / g)**0.5 / 4
t = fsolve(pendulum_thj t0 , args = (1 ^ th0 ))
return t * 4

fsdveO 求解时需要一个初始值尽量接近真实的解,用小角度单摆的周期的^作为这个初始
4

值是一个很不错的选杼。下面利用 penduliim_period〇计算出初始摆动角度从0 到 9 0 度的摆动


周期:

ths = np.arange(0 , np.pi/2 .0 , 0 .0 1 )


periods = [pendulum_period(l_» th) for th in ths]

为了验证 fsdveO 求解摆动周期的正确性,下而的公式是从维截百科中找到的摆动周期的精


确解:

643
Python 科学计算(第 2 版)

t
一 K
g

其中的函数K 为第一类完全椭岡积分函数,定义如下:
r /2 de
K (k ) = 「__________
J〇 V l —k 2sin 20
可以用 scipy.special.ellipkO汁算此函数的值:

from scipy.special import ellipk


peniods2 = 4 * (1.0 / g)**0.5 * ellipk(np.sin(ths / 2) **2 )

图 11-12比较两种计算方法,可以看到结果是完全一致的:

ths = np.arange(0 j np.pi/2 .0 , 0 .0 1 )


periods = [pendulum_period(l, th) for th in ths]
periods2 = 4 * (1.0/g)**0.5 *ellipk(np.sin(ths/2)**2) # 计算单摆周期的精确值
I pi .plot (ths, periods, label = u"fsolve 计算的单摆周期",linewidth=4.0)
pl.plot(ths, periods2 , "r__, label = u"单摆周期精确值",linewidth=2 .0 )
pi•legend(loc='upper left')
pi •xlabel (u_•初始摆角(弧度)")
pi •ylabel (u"摆动周期(秒)")

图 1 1 - 1 2 单摆的摆动周期和初始角度的关系

1 1 3 推荐算法

与本节内容对应的Notebook 为: ll-examples/examples-300-movielens.ipynbo

644
推荐兑法是指利用用户的-•些行为,通过一些数学兑法,推测出用户可能喜欢的东两。本
节介绍如何通过 M ovieLens 提供的)丨』
户对电影的评分数据进行一些推卷算法方面的研究。

11.3.1读入数据

m ovdens.data 文件中是以制表符分隔的评分数据,其第一列为用户 ID 、第二列为电影 ID 、


第三列为用户对电影的5 分制评分值。下 而 用 pandas.read_table ()读入该数据,并指定列名与列
的数椐类型。由于通过推荐算法计算的评分值不是整数,因此这里将评分值的数值类型设S 为
浮点数。原数据中用户 I D 和电影 I D 都 是 从 1开始的,为了方便后续的稀疏矩阵表达,将它们
都 减 去 1。

columns = [•user_icT, •movie一id', 'rating']


dtypes = [np.int32, np.int32, np.float]
ratings = pd.read_table('data/movielens.data'^
names=columns>
usecols=[0 ^ 1 , 2 ],
dtype=dict(zip(columns, dtypes)))
ratings["user 一id"] -= 1
ratings [’
_movie_id ••]-= 1

下面将评分次数少于1 0 的)户和电影的评分删除,这有助于推荐兑法将运算集中在有足
够数据可以分析的用户和电影之上:

Uj v, r = ratings.user_id.values, ratings.movie_id.values^ ratings.rating.values

#删除评分数少于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 和测试用的电影评分。

def train_test_split(arrays, test_size=0 .1 ):


np.random.seed(0 )
mask—test = np.random.rand(len(arrays[0 ])) < test—size
mask train = Mnask test
Python 科学计算(第 2 版)

arrays一train = [arr[mask一train] for arr in arrays]


arrays_test = [arr[mask_test] for arr in arrays]

return arrays一rtain + arrays_test

ujtrairij v_train, r_train, u_test, v_test, r_test = train_test_split([u^ v, r])


nu = np.max(u) + 1
nv = np.max(v) + 1
nr = len(u一 train)

下面检测学习用的数据包含所有的电影和用户,如果下面的程序抛出异常,则建议修改
seed〇的系数:

assert np.all(np.unique(u_train) == np.unique(u))


assert np.all(np.unique(v一rtain) == np.unique(v))

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越好。

def rmsG_score(y_truG, y_pred):


d = y_true - y_pred
return np.mean(d**2)**0.5

def r2 —score(y_true, y_pred):


d = y_true - y_pred
return 1 - (d**2 ).sum() / ((y—true - y_true.mean())**2 ).sum()

下而用这两个评价标准对每个电影的平均评分值的预测性能进行评价。通 过 Pan das 的


groupby 对评分值按照电影 ID 进行分组,并计算每组的评分值。测试用数据中每部电影的预测
评分值S r _pred, 将之与测试数据中的实际评分值进行比较,计 算 R M S E 和r 2:

movies_mean = pd.Series(r_train),groupby(v_train).mean()
r_pred = movies_mean[v_test]

rmse_avgJ r2 _avg = rmse_score(r_test> r_pred), n2 _score(r_test, r_pred)

646
nmse_avg n2 _avg

1.0122857327818662 0.17722524926979077

通过计算每个电影的平均评分值,可以为所有用户推荐整体最受欢迎的电影,但是无法根
据每位用户的喜好推荐他最可能喜欢的电影。为了实现为不同的用户推荐不同的电影,需要对
电影评分进行矩阵分解。

11.3.3矩阵分解

准备好测试数据以及评价标准之后,下面正式幵始实现推荐算法。该兑法应该能从训练数
据中找到更多的规律,使得其对于测试数据的推荐性能超过上面以均值为预测值的性能。
可以把用户 i 对电影 j 的评分1^按照下面的公式分解成4 部分的和:

r ”= + Ui + Vj + z UikVjk
k=l

其中p 是一个标量,它是所有评分的平均值。u 是一个矢量,其长度为Nu ,Ui为用户 i 的评 s


分系数。v 是一个矢量,其长度为Nv, Vj为电影 j 的评分系数。这里Nu表示用户数,Nv表示电
影数。
U 是一个与用户对应的矩阵,其大小为 Nu x K 。V 是一个与电影对应的矩阵,其大小为
Nv x K 。这里K 的长度可任意指定。计算U 的 第 i 行与V 的第 j 行上对应元素的乘积和,就得到
了州户 i 对电影 j 的评分系数。可以这样理解U 和V :每个电影都用 K 个属性进行描述(V ),而每
个用户都对这K 个属性有不同的喜好权值(U )。这两组值的乘积和就是某个用户对某部电影的喜
好程度。
下面先计算叫和%, 我们希望找到一组解最符合如下方程组:
^ + Ui + Vj

可以看出这是一个最小二乘法的问题:u 和v 中有Nu + Nv f 未知数,它们需要满足~个方


程 ,这里用卜表示评分数。最小二乘法中的矩阵A 的大小为叫 x (Nu + Nv),这是一个非常大
的矩阵,而其中绝大部分的元素都为0 , 实际上A 的每行中只有两个不为零的元素,它们的值
都 为 1。因此需要使用稀疏矩阵表示矩阵A :

from scipy import sparse


from scipy.sparse import linalg

r一avg = r_train.mean()

row_idx = np.r_[np.arange(nr), np.arange(nr)]


col一idx = np.r_[u一train, v_train + nu]
values = np.ones_like(row_idx)

A = sparse.coo_matrix((values, (row_idx, col_idx)), shape=(nrJ nu+nv))

647
Python 科学计算(第 2 版)

矩陈A 中每行有两个1,其佘的值都为0。我们采用 coo_matrix()创建该稀疏矩阵, ro w jd x


为每个非零元素所在的行,col_ id x 为非零元素所在的列,values 是这些非零元素的值。它们都
是长度为2 N 1•的一维数组。
下面使用 scipy.sparse.linalg 中 的 lsqrQ进行最小二乘法求解。得到一组解 X,其中前N u个元
素为u ,后Nv f 元素为V 。

x = linalg.lsqr(A, r_train - r_avg)[0]

ub = x[:nu]
vb = x[nu:]

下面按照公式q ^ + Ui + Vj计算测试数据的预测评分,并与实际评分进行比较:

rjDred = r_avg + ub[u_test] + vb[v_test]

rmse, r2 = rmse一score(r一test, r一pred), r2一score(r一test, r_pred)


rmse r2

0.93056259728693425 0.30471011860212072

接下来我们将注意力集中到计算u 和V 的矩阵分解算法上。矩阵分解的自标是尽量接近下
面的 r_train2:

r_train2 = r_train - (r_avg + ub[u_train] + vb[v_train])


r_test2 = r_test - r_pred
#以下程序从该array元组获取数据
arrays = u—train, v_train, r_train2, u—test, v—test, n_test2

11.3.4使用最小二乘法实现矩阵分解

将 r_miin2 分解为两个矩阵 U 和 V 的乘积也是一个最小化的问题。矩阵 U 和 V 中共有


Nu • K + Nv • K 个未知数,而 最 小 化 的 目 标 方 程 有 个 。下面是与第 i 个用户对第 j 个电影的评
分值对应的方程:
U i 〇 * V j 〇 4 - 1141 • V j X H = r”

由于每个方程都包含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 ,为了保证这两个矩阵相乘后得

到的结果大小基本一致,需要以^为缩放冈子。

© 使用 c o o _ m a t r i x 〇创建当 U 为 未 知 数 、V 为已知数时的系数矩阵 A v , 并将其转换成


csr_matrix 〇格式的稀疏矩阵。假设电影评分中的第 r 行对应用户为 i、 屯影为 j , 则A v 中 的 第 r
行中不为零的元素为从第j •K 列到第j •K + K - 1列:

A 乂j .K .. .j K + K - l = Vj

而当 U 为己知数时的系数矩阵A u 的第 r 行中不为零的元素为从第i •K 列到第i • K + K - 1列:

A ? ,i . K . .. i K + K - l = U i

© 调 用 lsmr〇进行最小二乘计算,并将计算结果写入U 。由于V 的值只是假设已知,我们并


不希望使用它作为系数找到精确的最小二乘解,因此通过系数 maxiter 设置计算最小二乘解时的
迭代次数,系数 damp 控制解的大小。当它不为;时被称为正则化最小二乘解。
〇由于 A u 是一个 C S R 格式的稀疏矩阵,其中非零元素的值按照行的顺序保存在data 属性
中。它正好与数纽 U 平坦化之后的每个元素相对应,因此通过 A u .data 更新稀疏矩阵A u中的数
据。这里通过参数 m u 控 制 A u 中元素变化的快慢。m u 越大则 A u 越 接 近 U 的值。接下来使JIJ
新的A u计算V ,并 更 新 A v。
© 使用1;
和\^计尊测试数据的预测评分,并计算与实际评分数据的 R M S E 评价值。将最佳
的 R M S E 评价值保存在 best_rm se 中,而与之对应的U 和V 则保存在 best_U 和 best_V 中。
© 由于对大型稀疏矩阵进行最小二乘运算会消耗大M 内存:,这 里 调 用 gc .collect()强制进行
垃圾回收,释放内存空间。

from scipy import sparse


from scipy.sparse import linalg
import gc

def uv一decompose(arrays, loop—count, k,maxiter^ mu, damp):


u一rtain, v一rtain, r_train, ujtest, v 一
etst, r_test = arrays

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

idxv_col = (u_train[:, None]*k + np.arange(k)).ravel()


idx_row = np.repeat(np.arange(nr), k)
Av = sparse.coo_matrix((V[v_train].ravel(), (idx_now, idxv_col)),
shape=(nr> nu*k)).tocsr() ©

idxu一col = (v一rtain[:, None]*k + np.arange(k)).navel()


Au = sparse.coo_matrix((U[u_train].ravelO^ (idx_row, idxu_col)))
shape=(nr> nv*k)).tocsr()

bestJJ, best_V = None, None


best_nmse = 1 0 0 . 0
rmse—list =[]

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

; rjDred = U.dot(V.T)[u_test, v_test] 0


rmse = rmse_score(r_test, r_pred)
rmse_list.append(rmse)
if rmse < best_rmse:
best_rmse = rmse
; best_U, best_V = U.copy(), V.copy()
gc.collect() ©

return best_U, best一,V best_rmse, rmse_list

程序虽然不复杂,但 楚 maxiter、k 、m u 和 damp 等参数均会对结果有影响,冈此找到最佳


组合是十分耗时的工作。下面比较了 dam p 为 3.5和 3.0时的 R M S E 和迭代次数的关系。结果如
图 11-13所示,由于 dam p 的丨Ej的是防止过度学习,因 此 3.5比 3.0的收敛速度慢,但 R M S E 能
收敛到更小的值。

Ul, best_rmsel^ rmsesl = uv_decompose(arrays,


Ml,
loop_count=20^ maxiter=6 , k=B0> mu=0.4> damp=3.5)
U2, V2, best_rmse2, rmses2 = uv_decompose(arrays,
loop_count=20j maxiter=6 , k=30> mu=0.4> damp=3.0)
print best一rmsel, best_nmse2
0.901107139807 0.904096209664

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

图 11-13 damp 系数对R M S E 的影响

11.3.5使 用 Cython 迭代实现矩阵分解

还可以使用随机梯度下降法对矩阵进行分解。下面是具体的计算公式。和前而的方法相同,
首先用随机数初始化用户矩阵U 和电影矩阵V 。随机挑选一个评分% , 它是用户 i 对电影 j 的评
分。计算预测评分值
K

^ = 2 U i k . Vjk
k=l

然后计算评分误差 e :
e = Tij - fjj

使用误差 e 通过下面两个公式N 时更新U ik和Vjk ,其中k = l … K 。n 为学习系数,P 为防止


过度学习的系数。值得注怠的是这两个公式是同时更新的,因此第一个公式计算的结果1^并
不代入到第二个公式中运算。
uik = u ik + n .(e .vjk - p .u ik)
Vjk = Vjk + r i .(e .U i k - p .Vjk)

由于需要大量的循环运兑,因此我们使用 Cython 编写迭代程序◊ uV_update〇的参数分別为


用户编号、屯影编号、评分、用户矩阵U 、电影矩阵V 、学习系数和防止过度学习的系数。

%%cython
#cython: boundscheck=False
#cython: wnaparound=False
import numpy as np

651
Python 科学计算(第 2 版)

cdef double dot(double[:, ::1 ] x, d o u b l e t : : 1 ] y, int i, int j):


cdef int k
cdef double s = 0
for k in range(x.shape[l]):
s += x[i, k] * y[j, k]
return s

def uv_update(int[::1 ] userid, int[::1 ] movieid^ double[::1 ] ratings


d o u b l e [ : : 1 ] uservalue, d o u b l e t **1 ] movievalue^
double eta, double beta):
cdef int j, k
cdef int ratecount = rating.shape[0 ]
cdef int uid, mid
cdef double rvalue, pvalue, error
cdef double tmp
cdef int nk = uservalue.shape[l]

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

下面的 uv_decompose2()循 环 iter_count次调用 uv_update()以更新U 和V 。在每次调用之前,


通 过 shuffleO打乱用户编号、电影编号、评分这三个数组的顺序,实现评分的随机选择。
V*

def uv_decompose2 (arrays, k, eta, beta, iten_count):


u一
train, v 一
rtain, rjtrain, u_test, v 一
etst, r 一etst = arrays

U = np.random.rand(nu, k) * 0.1 / k**0.5


V = np.random.rand(nv, k) * 0.1 / k**0.5

bestJJ, best_V = None, None


best_nmse = 1 0 0 .0

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

return best—U, best_V, best_rmse, rmses

下面绘制k = 3 0 、 ti = 0.008、 p = 0.08的收敛曲线,结果如图11-14所示:

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 个等级,每个盒子表示每个等级对应的预测评分的分布情况。

r_pred3 = U3.dot(V3.T)[u_test, v_test] + r_pred


s = pd.DataFrame({"r":r_test> M$\hat{r}$":r_pred3})
s.boxplot(column="$\hat{r}$", by="r", figsize=(1 2 , 6 ))

653
Python 科学计算(第 2 版)

阁 1 M 5 以实际评分对〖
獅!If评分分纟丨I,绘制每姐的分布惜况

I 11.4 频 域 信 号 麵

与本节内容对应的Notebook 为: 1l -examples/examples-400-fft.ipynb〇

FFT (快速傅立叶变换)能将时域信号转换为频域信号。转换为频域信号之后,可以很方便
地分析出信号的频率成分,在频域上进行处理,最终还可以将处理完毕的频域信号通过IFFT (逆
变换)转换为时域信号,实现许多在时域无法完成的信号处理算法。木章将通过许多实例,简单
地介绍有关频域信号处理的一些_础知识。

11.4.1 FFT 知识复习

F F T 变换是针对一个数组的运算,数组的 L<:度 N 通常是2 的整数次幕,例 如 64、128、256

等。数值可以是实数或复数,通常的时域信号都是实数,因此下面都以实数为例。可以把这一
组实数想象成对某个连续信号按照一定収样周期进行収样而得,如果对有 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])

所谓直流信号,就楚其值不随时间变化,因此我们创連一个值全为1 的数纟11 x , 它 的 FFT


结果除了下标为〇的数值不为〇以外,其余的都为0。这表示时域信号是直流的,并且其能量
为 1〇
下而我们产生一个周期为8 个取样的正弦波,然后观察其 F F T 结果:

x = np.arange(0 j 2 *np.pi, 2 *np.pi/8 )


y = np.sin(x)
tmp = np.fft.fft(y)/len(y)
print np.array_str(tmp, suppress_small=True)
[ 0 .+0 .j -0 .-0 .5j 0 .-0 .j 0 .-0 .j 0 .+O.j 0 .-0 .j 0 .+0 .j 0.+0.5j]

为了便 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。再看两个
例子:

tmp = np.fft.fft(2 *np.sin(2 *x))/len(x)


print np.array_str(tmp, suppress_small=True)
tmp = np.fft.fft(0 .8 *np.cos(2 *x))/len(x)
print np.array_str(tmp, suppress一small=True)
[ 0 .+0 .j 0 .+0 .j -0 .-1 .j 0 .-0 .j 0 .+0 .j 0 .+0 .j -0 .+l.j 0 .-0 .j]
[-0.0+0.j -0.0+0.j 0.4-0.j 0.0-0.j 0.a+0.j 0.0-0.j 0.4+0.j -0.0+0.j]

上而产生的是周期为4 个取样点的正弦和余弦信号,其 F F T 的有效成分在下标为2 的复数


中,其中止弦波的振幅为2 , 其频域虚数部分的值为- 1 ; 余弦波的振幅为0.8,频域中对应的值
为 0.4。
如果将两个N 频率的正弦波和余弦波通过不同的系数进行叠加,就可以得到N 样频率的各
种相位的余弦波。因此我们可以这样来理解频域中的复数:
•复数的模(绝对值)的两倍为对应频率的余弦波的振幅。
•复数的辐角表示对放频率的余弦波的相位。
最后再看一个例子:

x = np.arange(0 , 2 *np.pi, 2*np.pi/128)


y = 0.3*np.cos(x) + 0.5*np.cos(2*x+np.pi/4) + 0.8*np.cos(3*x-np.pi/3)
yf = np.fft.fft(y)/len(y)
print np.anray_str(yf[:A], suppress一 small=True)
print np.abs(yf[l]), np.rad2 deg(np.angle(yf[l])) # 周期为 128 取样点的余弦波的振幅和相位
print np.abs(yf[2]), np.rad2deg(np.angle(yf[2])) # 周期为 64 取样点的余弦波的振幅和相位
# 周期为42.667取样点的余弦波的振幅和相位
print np.abs(yf[3]), np.rad2deg(np.angle(yf[3]))
[0.000+0.j 0.150+0.j 0.177+0.177j 0.200-0.346j]
0.15 2.48480834489e-15
0.25 45.0
0.4 -60.0

这 里 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 计算•结果bins 中的前n 项进行合成,返回合成结果,计算loops个周期的波形


def fft一combine(bins, n, loops=l): ©
length = len(bins) * loops
data = np.zeros(length)
index = loops * np.arange(0 , length, 1 .0 ) / length * ( 2 * np.pi)
for k, p in enumerate(bins[:n]):
if k != 0 : p *= 2 # 除去直流成分之外,其余的系数都* 2
data += np.real(p) * np.cos(k*index) # 余弦成分的系数为实数部分
data -= np.imag(p) * np.sin(k*index) # 正弦成分的系数为负的虚数部分
return index^ data

fft size = 256


Python 科学计算(第2 版)

# 计箅三角波及其FFT
x, y = tniangle_wave(fft_size)
fy = np.fft.fft(y) / fft_size

# 绘制三角波的FFT 的前 20 项的振幅,由丁•不含下标为偶数的值均为0 ,因此収


# log 之后无穷小,无法绘图,用 np.clip函数设置数组值的上下限,保证绘图正确
fig, axes = pi.subplots(2 , 1, figsize=(8 , 6 ))
axes[0 ].plot(np.clip(2 0 *np.logl0 (np.abs(fy[:2 0 ]))^ -1 2 0 , 1 2 0 ), "o")
axes[0 ].set一 xlabel(u"频率窗口( frequency bin)")
axes [0 ]•set_ylabel (u"幅值(dB)")

# 绘制原始的三角波和用正弦波逐级合成的结果,使用的取样点为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 三角波的频谱(上),使用频谱中的部分频率重建的三角波(下)

Otriangle_ wave 〇产生-•个周期的三角波形,这里使用 np.where〇计览分段函数。triangle〇返


冋两个数组,分別表示 X 轴 和 Y 轴的值。后面的计算和绘图不使用 X 轴坐标,而是立接用収
样序号作为 X 轴坐标。
© ffi_combine()使j F F T 结果 bins 中的前 n 个数据重新合成时域信兮, loops
参数指定计箅的周期数。通过这个例子可知,合成时域信号吋,使用的频率越多,波形越接近
原始的三角波。

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

fig, axes = pi.subplots(2 , 1 , figsize=(8 , 6 ))


axes[0 ].plot(np.clip(2 0 *np.logl0 (np.abs(fy[:2 0 ])), -1 2 0 , 1 2 0 ), ••〇")
axes[0 ].set一xlabel(u"频率谢口( frequency bin)")
axes[0] .set_ylabel(u"幅值(dB)")
axes[l].plot(y, label=u"原始方波",linewidth=2 )
for i in [0 ,1 ,3,5,7,9]:
index, data = fft_combine(fy, i+1 , 2 ) # 计算两个周期的合成波形
axes[1].plot(data, label = "N=%s" % i)
axes[1 ].legend(loc="best")

阁 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 转换为频域信号之后,将其各个频率分量的幅值绘制成图,可以很直
观地观察信号的频谱。下面的程序能完成这一任务:

sampling—rate) fft_size = 8000, 512 O


t = np.arange(0 , 1 .0 , 1 .0 /sampling_rate) ©
x = np.sin(2*np.pi*156.25*t) + 2*np.sin(2*np.pi*2B4.375*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。
如果放大频谱中的两个峰值,可以看到其值分别为:

print xfp[[10, 15]]


[ -6.021e+00 -9.643e-16]

图 11-19 156.25Hz和 234.375Hz的波形(上)和频谱(下)

即 156.25H z 的幅值大小为-6d B , 而 234.375H z 的幅值大小为OdB。下面详细介绍程序的各


个部分:
〇首先定义了两个常数 s a m p l i i ^ m t e 和 f t U i z e , 它们分别表示数字信号的取样频率和F F T
的长度。©然 后 调 用 a r a n g e O 产 生 1 秒钟的取样时间,t 中的每个数值直接表示取样点的时间,
因此其间隔为取样周期 1/sampline_rate 。
© 用取样时间数组 t 可以很方便地计算出波形数据,这里计算的是两个正弦波的S 加, 一

个 频 率 是 156.25H z , 另一个是234.375H z 。为什么选择这两个奇怪的频率呢?因为这两个频率


的正弦波在512个取样点中正好有整数个周期。只有整数个周期的波形的F F T 结果能精确地反
映其频率。
假设取样频率为 fs, 取波形中的 N 个数据进行 F F T 变换。当这 N 点数据包含整数个周期的
波形时,F F T 所计算的结果是精确的。因此能精确计算的波形的周期是:n *fs / N 。对 于 8kHz
的取样频率、512点 的 F F T 来说,8_512.0=15.625 H z , 即只能精确表示15.625H z 的整数倍频
率。156.25H z 和 234.375H z 正好是其10倍 和 15倍。
〇这里使用 rfft〇对从波形数裾 x 中截取 fft_size 个取样点进行 F F T 计算,
所得到的结果不包
括共轭部分。
根据 F F T 计算公式,为了正确显示波形能M ,
还需要将结果除以F F T 的长度ffu size 。
对于长度为 N 的 F F T 运算,rfft()返回N /2 + 1个S 数,分别表示从0到^/2的各点频率的
成分。© 因此可以通过 limpaceO计算出 rfft()的返回值中每个数值对应的真正频率。也可以使用
N u m P y 提 供 的 fftfreqO函数,它的第一个参数为 FTFT的长度,第二个参数为信号的収样周期。

661
Python 科学计算(第 2 版)

它返回与 F F T 结果对应的 ffi_size 个频率,前半部分频率大于等于0 , 后半部分频率为负值。其


中频率为负值的部分也可以将其频率理解为该负值加上取样频率。此外,rffifreqO计 兑 与 rffi()
结果对应的频率。

freqs = np.fft.fftfreq(fft_size^ 1 .0 /sampling 一rate)


for i in [0 ,1 ) fft_size//2 -l, fft_size//2 , fft_size//2 +l, fft—size-2 ,fft一size-1 ]:
print i, "\t", freqs[i]
0 0 .0
1 15.625
255 3984.375
256 -4000.0
257 -3984.375
510 -31.25
511 -15.625

® 最后计算每个频率分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 的波形。

'.00 0.01 0.02 0.0B 0.04 0.0S 0.06

时蝴秒1

0 SO0 1000 1S00 2000 2S00 3000 3S00

阁 11-20非完整Ml期(200Hz和 300Hz)的正弦波经过FFT 变换之后出现频{«•液漏

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

阁 11-21 50Hz正弦波的512点FFT 所计算.频if_ 实际波形

1.窗函数

为了减少 F F T 所截取的数椐段前后的跳变,可以把数据勹一个窗函数相乘,使得其前后数
据能平滑过渡。例如,常用的 Hann 窗函数的定义如下:
27111
w (n ) = 0.5 1 —cos

N

其 中 N 为窗函数的点数,图 11-22是一个512点 Hann 窗的|11|线:

from scipy import signal


pl.figure(figsize=(6 , 2 ))
pi.plot(signal.hann(512))

663
Python 科学计算(第2 版)

阁 11-22 H a n n 窗函数

窗函数都在 scipy.sig m l 库中定义,它们的第一个参数为点数N 。可以看出 Hanr»窗函数是


完全对称的,也就是说第0 点和第511点的值完全相同,都 为 0。如果将这样的窗函数与信号
数据相乘,
结果中会出现前后两个连续的0,这样会对 F F T 变换之后的信号频谱有-•定的影响。
为了解决连续0 值的问题,hann 函数提供了 sym 参数,如果其值为0 , 则产生一个 N + i 点
的 Hann 窗函数,并舍去最末端的数值,这样得到的窗函数就适合于周期信号了:

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

图 1 1 - 2 3 加 H a n n 窗的5 0 H z 正弦波的5 1 2 点 F F T 所计算的实际波形

冋到前而的例子,将 200H z 和 300H z 的叠加波形与 Hann 窗相乘之后再计兑其频谱,得到


如 图 11-24所示的频谱图。

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

a = pl.axes([.4, .2, A , .4])


a .plot (freqs, xfp, label=u"矩形窗")
a.plot(freqs, yfp, label=u"hannl£f")
a.set_xlim(100, 400)
tS
a.set_ylim(-40J 0)

图 11-24加 Hann窗前后的频谱, Hann 窗能降低频谱泄漏

可以看到勹 Harm 窗乘积之后的信号的频谱能M 更加集中于200H z 和 300H z ,但是其能M


有所降低。这是因为 Hann 窗本身有一定的能M 衰减:

np.mean(signal.hann(512, sym=0 ))
0.5

如果需要严格保持信号的能M , 还需要在勹 HaniT窗相乘之后再把信号扩大一倍。

2.频谱平均

对于频谱特性不随时间变化的信号,例如引擎、压缩机等机器噪声,可以对其进行长时间
的采样,然后分段进行 F F T 计算,最后对每个频率分量的幅值求平均值,就可以准确地测量信

665
Python 科学计算(第 2 版)

号的频谱。下面的程序完成这一计算:

def average_fft(x, fft一size):


n = len(x) // fft_size * fft_size
tmp = x[:n].reshape(-1, fft—size) O
tmp *= signal.hann(fft_size, sym=0) ©
xf = np.abs(np.fft.rfft(tmp)/fft_size) ©
avgf = np.mean(xf, axis=0)
return 20*np.logl0(avgf)

average_ fft(x ,fft_size)对数组 x 进 行 fft_size 点 F F T 运算,并返回以 d B 为度M 的平均幅值。


由 于 x 的长度可能不是 f f U i z e 的整数倍,O 因此首先将其缩短为 f f U i z e 的整数倍,然后用
reshape〇将其转换成一个二维数组 tmp。tm p 的 第 1轴的长度为 fft_ size 。
© 将 tm p 数组的第1轴上的数据和 Hann 窗函数相乘。© 调用 rfft()对 tm p 数组中的每行数据
进 行 F F T 计箅,并求其幅值。® 最后,用 mean〇对 x f 沿着第0 轴进行平均,这样就得到了每个
频率分量的平均幅值。
I 图 11-25是利用 average_ffi()计算随机数序列频谱的例子。

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)

0 SO 100 150 200 250


W车S□(Frequency Bin)

图 11-25白色噪声的频® 接近水平直线(注意Y 轴的范围)

可以看到随机噪声的频谱接近一条水平直线,也就是说每个频率窗口的能S 都相同,这种
噪声被称作白噪声。如果让白噪声通过一个 IIR 低通滤波器,绘制其输出信号的平均频谱,就

666
能够观察到1I R 滤波器的频率响应特性。下面的程序利j|j iirdesign()设计一个8k H z 取 样 的 1kHz
的 ChebyshevI 型低通滤波器,
ihdesignO需要用正规化的频率(取值范围为0〜 1),然后调用 filtfiltO
对 A 色噪声信号进行低通滤波。如果用 aVerage_ffi〇计算滤波器输出信号的平均频潜•,将得到如
图 11-26所示的频谱图。

b, a = signal.iirdesign(1000/4000.0, 1100/4000.0, 1, 40, 0, "chebyl")


x = np.random.randn(1 0 0 0 0 0 )
y = signal.firtfirt(b, a, x)
yf = avenage_fft(y, 512)
pl.figure(figsize=(7, 3.5))
pi.plot(yf)
p l . x l a b e l ( u " 频 率 窗 口 ( Frequency B i n ) " )
pl.ylabel(u"幅值(dB)")
p l . x l i m ( 0 , 257)
pi.subplots_adj ust(bottom=0.15)
20
40
60
80 tS
8

20

ffl
psil

0 SO 100 ISO 200 2S0


M車B 口(Frequency Bin>

图 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

t = np.anange(0 , time, 1 /sampling一rate)


sweep = signal.chirp(t^ f0 =1 0 0 , tl = time^ fl=0 .8 *sampling_rate/2 , method="logarithmic")

pl.specgram(sweep, fft_size, sampling一rate, noverlap = 1024-step)


pl.xlabel(u"时间(秒)•_)
pl.ylabel(u"频率(Hz)")

图 11-27频率扫描波的谱图

这 里 使 用 matplotlib 提供的绘制谱图的函数 specgmmO, 其第•个参数是表示信号的数组,


第二个参数是 F F T 的 L<:度 ,第三个参数是信号的取样频率。noverlap 参数是连续两块数据之间
重费部分的长度,该参数越接近 F F T 的长度,F F T 运算的次数越多,时间轴上的精度也越大。
specgmmO还有许多其他的关键字参数,请读者阅读其函数文档以了解详细用法。

本书提供了一个用 PyAudio 、TraitsUI等制作的谱图观察的程序。它实时地从声卡读入声音


数据,并绘制出声音信号的时间波形、频谱以及谱图。由于本程序对计算机的配置要求较高,
请读者在较快的机器上运行本程序。另外,为了计算效率,程序中没有使用重叠处理。图 11-28
是程序的界面截图。

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 〇

def make_wave(amp<> freq, phase, tend, rate):


period = 1 . 0 / rate
t = np.arange(0 , tend, period)
x = np.zeros_like(t)
for a, f, p in zip(amp, fneq, phase):
x += a * np.sin(2 *np.pi*f*t + p)
return tj x

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

下 面 用 1024点 F F T 计兑频谱峰值对应的频率。峰值需要满足两个条件:O 可以使用


signal.argrelmaxO计算局域最大值。其 order参数指定比较窗口的大小,3 表示峰值需要比K •左右
相邻的3 个值都大。© 峰值耑要大丁•平均值的3 倍,这样可以剔除巾白色噪声产生的微小局域
峰值。

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

spect2 = np.fft.rfft(x[COUNT:COUNT+FFT_SIZE] * np.hanning(FFT_SIZE))

phasel = np.angle(spectl[loc])
phase2 = np.angle(spect2[loc])

phase_delta = phase2 - phasel


print phase—delta
[2.595 -1.29 -2.899]

然后利用相位差计算信号中各个峰值的频率。〇为了减少运算次数,首先利用频谱峰值中
的最高频率计算 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) ©

idx = np.angmin(np.abs(peak_freqs - possible_freqs), axis=0) ©


peak_freqs2 = possible_freqs[idx, np.arange(len(peak_freqs))]
print "Peak Frequencies:", peak_freqs2
Peak Frequencies: [ 44.155 149.833 329.33 ]

11.4.4卷积运算

信号x 经过系统h 之后的输出y 是x 和h 的卷积,虽然卷积的计算方法很简单,但是当x 和h 都


很长的时候,卷积计算是非常耗费时间的。因此对于响应时间很长的系统h , 需要找到比直接
计算卷积更快的方法。
信号系统理论中有这样一个规律:时域的卷积等于频域的乘积,因此要计算时域的卷积,
可以将时域信号转换为频域信号,进行乘积运算之后再将结果转换为时域信号,实现快速卷积。

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

np.allclose(cJ fft_convolve(a, b))


True

由于立接计兑卷积和使用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卷积的运箅速度

由 于 F F T 卷积很常用,因 此 Sd p y .Sign al 中提供了 fficoiw dveO 。此函数采用 F F T 运算,并


可计算多维数组的卷积。下而是在 Notebook 中测试直接卷积与快速卷积的速度的程序:

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.卷积的分段运算

现在考虑输入信号x 和系统响应h 的卷积运兑。通常输入信号是非常长的,例如要对某段录


音进行滤波处理,假设取样频率为8kHz , 录音长度为1分钟,则 数 据 的 长 度 为 48_。而如果
需要对麦克风的输入信号进行连续的滤波处理,那么输入信号的长度可以看作无限长的。系统
响应h 的长度通常都足固定的,例如它可能足某个房间的脉冲响应,或是某种 F IR 滤波器的系数。
根据前而的介绍,为了有效地利用 F F T 计算卷积,我们希望它的两个输入长度相当,于是
就需要对输入信号进行分段处理。卷积的分段运算被称作 cw eriapadd 运算,其计算方法如图
11-30所示:
分« « 枳濡示

图 11-30使川oveiiap~add法进行分段益积的过程演示
Python科学计算(第 2 版)

图中原始信号 X 的长度为300,将它分为三段,分別与滤波器系数 h 进行卷积计算。h 的长


度 为 1 0 1 , 因此每段输出200个数据,图中j |j 绿色标出每段输出的200个数据。这 3 段数据按
照时间顺序求和,得到的结果和直接卷积的结果是相同的。
输入信号 x 和滤波器 h 的分段卷积运算可以按照如下步骤进行,假 设 h 的长度为 M :
(1) 建立一个缓存,其大小为 N + M - 1 , 初始值为0。
(2) 每次从 x 中读取 N 个数据,和 h 进行卷积,得 到 N+M - 1个数据。将这些数据与缓存
中的数据进行求和,并将结果保存进缓存中,最后输出缓存的前 N 个数椐。
(3) 将缓存中的数裾向左移动N 个元素,也就是让缓存中的第 N 个元素成为第0 个元素,
左移完成之后将缓存中后而的N 个元素全部设置为0。
(4) 跳转到(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

〇注意需要输出缓存前N 个数据的拷贝,否则输出的是数组的视图。当此后缓存 buffer 更


新时,视图巾的数据会•起更新。程序输出直接卷积和 overlap-add分段卷积的最大误差。将 FFT
快速卷积和 overlap^add相结合,n j 以制作出一些快速的实时数据滤波_兑法。但是甶于 FFT 卷积
对于两个长度相当的数纟II最为有效,因此在分段时也会有所限制。例如,如果滤波器的长度为
2048,那么理想的分段长度也为2048;如果将分段长度设置得过低,反而会增加运算量。因此
在实吋性要求很强的系统中,只能采用直接卷积。

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

我们用 A 、B 、C 和 D 这 4 个布尔变量分別表示甲、乙、丙、丁 4 位嫌疑人是否是罪犯。


表达式 A 表示甲是罪犯,〜A 表示甲不是罪犯。由此可以得出表11-1所示的4 个布尔表达式:

表 1 1 -1 布尔表达式
嫌疑犯 供述 布尔表达式
甲 不逛我作的案 Sl=〜A
乙 丁就是罪犯 S2=D
丙 乙是罪犯 S3=B
丁 乙有意诬陷我 S4=〜D

由 于 4 个 人 中 只 有 一 人 说 了 真 话 ,因 此 有 如 下 4 种 可 能 ,即 列 举 出 只 有 一 个 表 达 式 为 真 的

所 有 组 合 ,其 中 & 表 示 与 运 算 ,丨表示或运算。

SI & ~S2 & ~S3 & ~S4 I


〜 SI & S2 & 〜 S3 & 〜 S4 |
〜 S I & 〜 S2 & S3 & ~S4 |
〜 S I & ~S2 & ~S3 & S4

在 上 面 的 表 达 式 中 ,每 行 的 表 达 式 是 一 个 与 运 算 ,而 行 之 间 是 或 运 算 。这 种 逻 辑 公 式 被 称
为 析 取 范 式 (D N F )。然 而 S A T 求 解 器 只 能 对 C N F 表 达 式 求 解 。 一 个 满 足 C N F 的 布 尔 表 达 式 由

多 个 子 表 达 式 的 与 运 兑 构 成 ,而 每 个 子 表 达 式 则 山 逻 辑 变 量 的 或 运 兑 构 成 。因 此 需 要 使 用 另 外
的 方 法 来 表 示 S I 、S 2 、S 3 、S 4 中 只 有 一 个 为 真 :

~(S1 & S2) &


〜 (S1 & S3) &
〜 (S I & S4) &
〜 (S2 & S3) &
-(S 2 & S4) &
~(S3 & S4) &
〜 ( 〜 S1 & 〜 S2 & ~S3 & 〜 S4)

在 上 而 的 表 达 式 中 ,列 举 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

将 D N F 转换为 C N F 是一件比较麻烦的工作,可以借助 S y m P y 的布尔代数模块& 动转换。


〇酋先定义A 到 D 共 4 个符号,© 并使用位操作符创建D N F 表达式。©然 后 调 用 to_cnf()将 D N F
表达式转换为 C N F 表达式。得到的结果比上面的手工转换结果更加简洁:

from sympy import symbols


from sympy.logic.boolalg import to_cnf

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 为真,
即¥ 是罪犯:

from sympy.logic.inference import satisfiable


satisfiable(cnf)
{B: False, A: True, D: False}

下面使用本章将要介绍的 cycosat 扩展库对这个逻辑问题进行求解。C N F 公式可以使用一


个嵌套列表表示,列表中的每个整数与一个布尔变量对应,负数表示逻辑非。例 如 1与 A 对应,
1表 示 A , 而 表 示 ~ A 。
C o S A T 类 是 C y th o n 编写的扩展类,它 对 C 语 言 编 写 的 P ic o S A T 进行包装。O 调 JIJ
add_clauses〇将嵌套歹!J表表示的 C N F 公式添加进求解器。可以多次调用 add_clauses()逐步添加更
多的表达式。© 调 用 solve〇进行求解得到一个解列表,列表中的每个兀素与一个布尔变童对应,
1表示该布尔变M 为真,-1表示假。由结果可知甲是罪犯。

from scpy2.cycosat import CoSAT

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]

11.5.1 用 Cython 包装 PicoSAT

P icoSA T 采 用 C 语言编写,其源代码只有两个文件:picosat.c 和 picosat.h 。我们编写的 Cython


扩展库将对其中的如下函数进行包装:

#define PICOSAT_UNKNOWN 0
#define PICOSAT_SATISFIABLE 10
#define PICOSAT_UNSATISFIABLE 20

typedef struct PicoSAT PicoSAT;

PicoSAT * picosat一init (void);


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);
void picosat_assume (PicoSAT *, int lit);

PicoSAT 求解器的所有状态都保存在PicoSAT 结构体中,调 用 picosatjnit〇将返回一个指向


新分配的结构体的指针,而 picosat_res〇则释放该指针所指向的结构体。
调 用 picosat_add〇和 picosat_add_lits〇函数往结构体中添加逻辑子句。调 用 • 次 picosat_add()
只能添加一条子句中的-个变量,而 pic〇sat_add_lits()则可添加整条子句,用 0 表示子句结朿。
例如添加子句1、- 4 , 可以如下调用:

picosat_add(sat, 1 );
picosat_add(sat, -4);
picosat_add(sat^ 0 );

或者:

int clause[3] = {1, -4, 0};


picosat_add_lits(sat, clause);

调 用 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 结构体的字段内容,因此无须对其各个字段进行声明。

cdef extern from "picosat.h":


ctypedef enum:
PICOSAT_UNKNOWN
PICOSAT_SATISFIABLE
PICOSAT_UNSATISFIABLE

ctypedef struct PicoSAT:


pass

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)

我们使用下而的扩展类 C oSA T 对 PicoSAT 结构体进行包装。O 为了在 Cythoii程序中快速


重 建 PicoSAT 结构体,这里采用 clauses属性缓存将被添加进 PicoSAT 结构体中的所有子句。它
是 Python标准库 array中的数组对象。由于 airay 对象同时具有 C 语言数组的连续存储数值元素
的功能,以及列表的动态扩容功能,因此很适合作为子句的缓存使用。在 clauses中,每条子句
都以 0 开始,以 0 结朿,因此在初始化时为其添加一个元素0。
©buf_pos属性保存 clauses中已经添加进 PocoSAT 结构体的子句的最终位置,_ 1 表示需要
创建新的 PicoSAT 结构体。

from cpython cimport array

cdef class CoSAT:

cdef PicoSAT * sat


cdef public array.array clauses O
cdef int bufjDos ©

def —cinit_ (self):


self.bufjDOS = -1
self.clauses = array.array("i", [0 ])

def — dealloc— (self):


self.close一sat()

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〇完成。

cdef b u ild _ c la u s e s (s e lf):


cdef in t * p
cdef in t i

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

cdef b u ild _ s a t (s e lf): ©


if s e l f . b u f j d o s == - 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 solve(self, limit=-l):


s
cdef int res
self.build_sat() ©
res = picosat_sat(self.sat, limit) O
if res == PICOSAT__SATISFIABLE:
return self.get_solution()
elif res == PICOSAT_UNSATISFIABLE:
return "PICOSAT^NSATISFIABLE"
elif res == PICOSAT_UNKNOWN:
return "PICOSAT UNKNOWN' 1

上 而 的 sdveO 方法只能获得一个解,下 而 的 iteosdveO 返回一个能遍历所有解的迭代器。


O 在每次获得解 solution 之后,添加一条否定该解的子句。例如,如果解为[-1,1,1,
-11,其含义
为 〜 則 & 6 2 & 8 3 & 〜 3 4 , 其 中 & 为 第 1 个 布 尔 变 量 。对 该 子 句 取 反 得 到 則 卜 82卜 83134,转
换 为 P ico S A T 的子句为:1,-2,-3,4。© 由于 iter_solve()修改了结构体的内容,无法将其还原到
求解之前的状态,因此调用 iter_reset()将 buf_p o s 屈性设置为_1,这样下次求解时,将重新创建
新 的 P ico S A T 结构体。

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

下血让我们看看如何使Wj C o S A T 类解决两个较W 难的逻辑题。

11.5.2数独游戏

数独是一种数字填充游戏,玩家需要把从1到 9 的数字填进每一格,保证每行、每列和每
个宫(3X 3 的方块)都 有 1至 9 所有数字。如 阁 11-31所示,黑色数字为游戏设计者提供的部分数
字,它们使该谜题只有一个答案,灰色数字显示该游戏的解答。

图 11-31数独游戏示例

由 于 S A T 是一个布尔求解器,其中的每个变M 只有两个候选值:False 或 True 。为了表示


数独的表格,我们需要一个三维数组bools, 其 第 0 轴对应数独表格的行,第 1轴对应列,第 2
轴对应每个中.元的候选数字。例如,如果在最终的解中,bools[4,l ,
3]对应的布尔变量为真,则
表示数独表格中的第5 行、第 2 列的值为4 。注意这里数独游戏中的行、歹I似 及 数 字 都 从 1 开
始,而程序中所有的数组下标都从0 开始。
下面的 bools 数组中保存用于 S A T 求解的布尔变量的序号,注 意 S A T 屮的布尔变量的序号
从 1 开始:
bools = np .arange(l , 9 * 9 * 9 + l ).reshape(9,9,9)

根据数独的约束条件:
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)。© 创建第一个条件对应的逻辑表达式,即最终轴上每组逻辑变量的序号。 ©创
建第二个条件对应的逻辑表达式,即最终轴上每两个逻辑变量序号的负值。

from itertools import combinations

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

下面是对于1、2、3 和 4 、5、6 这两组逻辑变量的运算结果:

print get一conditions(np.array([[1,2, 3], [4,5,6 ]]))


[[1, 2, 3], [4, 5, 6 ], [-1, -2], [-1, -3], [-2, -3], [-4, -5], [-4, -6 ], [-5, -6 ]]

下 面 使 用 geUconditionsO计算数独的前三个约朿条件,由于它只针对最终轴进行运算,因
此 对 于 “每行的数字不能重复”和 “每列的数字不能重复”这两个条件,需要将条件对应的轴
调换为最终轴。

cl = get_conditions(bools) # 每个单元格只能取1-9 中的一个数字


丨 c2 = get_conditions(np.swapaxes(bools, 1 , 2 )) # 每行的数字不能重复
: c3 = get_conditions(np.swapaxes(bools, 0, 2)) # 每列的数字不能重复

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

| 最后使用 C o S A T 对数独游戏求解。O sdveO 方法返冋的是一个列表,首先将其还原成形状


| 为(9, 9, 9)的数组,© 然后找到该数纟J1中最终轴上1对应的下标,而实际应该填写的数字为该下
丨 标 加 1。
| 请读者仔细观察程序的输出,分析其是否满足数独游戏的约束条件。

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"

sudoku = np.array([[int(x) for x in line]


for line in sudoku_str.strip().split()])
r, c = np.where(sudoku != 0 )
v = sudoku[r^ c] - 1

conditions2 = [[x] for x in bools[r, c, v]] O


print "conditions2 :__
conditions2
sat = CoSAT()
sat.add_clauses(conditions + conditions2 ) ©
solution = sat.solve()
format_solution(solution)
conditions^:
[[55], [71], [77], [106], [1 2 0 ], [2 0 0 ],
[208], [2 2 0 ], [251], [308], [345], [360],
[374], [384], [419], [481], [508], [521],
[528], [607], [624], [657], [660], [667]]
arnay([[3, 6 , 2, 7, 9, 4, 1 , 8 , 5],
Python 科学计算(第 2 版)

我们注意到 conditions〕中的每条或表示式只包含一个布尔变量,对于这种简单的表达式,
可以使川 void picosat_assume(PicoSAT *, int lit)添加假设条件。其 中 lit 为布尔变量的序号,正数
表示假设该布尔变量为真,
负数表示假。
下面楚 C o S A T 类 的 assume_ solve〇方法,
它在调j|j solve〇
方法求解之前添加 assumes指定的所有假设条件。下面是使用 assume_solve()求解的程序:

def assume_solve(self, assumes):


self.build一sat()
for assume in assumes:
picosat_assume(self.sat, assume)
return self.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 对象对多个数独游戏求解。

scpy2.examples.sudoku_ solver: 采用 matplotlib 制作的数独游戏求解器。

本书提供了一个交互式数独游戏求解器程序,可以使用如下键盘按键来操作:
•使用方向键改变当前单元格的位置。
• 使 用 数 字 键 1-9填写当前单元格,0 清除当前单元格,用户输入的数字采用黑色显示,
空白单元格的解采用浅灰色显示。
• 使 用 Ctrl+ E 淸除所有中.元格的内容。

11.5.3扫雷游戏

扫雷游戏是 W indows 操作系统中最著名的附带游戏,玩家点开方块后如果没有地雷,则会


有一•个数字显现其上,这个数字为邻近的8 个方块小的地雷数,玩家运用逻辑推断哪些方块有

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

X0, Y0, SIZE, COLS, ROWS = 30, 30, 18, 30, 16


SHAPE = ROWS, SIZE, COLS, SIZE, -1

mine—area = np.s_[Y0:Y0 + SIZE * ROWS, X0:X0 + SIZE * COLS, :] O

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 与已打开方块是一
一对应的。

可以通过 pl.hist〇绘 制 mask_mean数组的直方图,找到最佳阈值。


Python 科学计算(第 2 版)

mask = (img_init != img__mine).reshape (SHAPE)


mask_mean = np.mean(mask, axis=(l, 3, 4))
block_mask = mask—mean > 0.3

fig, axes = pi.subplots(1, 2, figsize=(12, 4))


axes[0 ].imshow(block_mask, interpolations"nearest% cmap="gray")
axes[l].imshow(img_mine[::-l])
axes[0 ].set_axis_off()
axes[1].set一 axiS-〇ff()
fig.subplots_adj ust(wspace=0 .0 1 )

图 11-32计算已打开方块的位置

下面调用 scipy.spatial.distance()比较每个方块中的图片与im u iu m b e rs 中的差别,并选择差


别最小的下标作为方块中的数字numbers,数字所在的行与列保存在row s 和 cols 数组中,效果
如 图 11-33所示。为了减少干扰,程序中将每个方块的叫边都减少3 个像素。这段程序运用
reshape〇和 swap狀es()实现多维数红I轴和幵多W 的转换,如果读者对这段® 序感到 W 惑,请参考
NumPy —章的相关小节。

from scipy.spatial import distance

img 一
imne2 = np.swapaxes(img一imne.reshape(SHAPE)j 1, 2)

blocks = img_mine2[block一mask][:, 3:-3,3:-3, :].copy()


blocks = blocks.reshape(blocks.shape[0 ], -1 )

img 一
numbers.shape = 8 , - 1
numbers = np.argmin(distance.cdist(blocks, img_numbers), axis=l)
rows, cols = np.where(block_mask)

下而用本书提供的 draw_grid〇函数绘制识别出的数字:

from scpy2 .matplotlib import draw_grid


table = np.full((ROWS> COLS), u" dtype="Unicode")
table[rows, cols] = numbers.astype(Unicode)
draw_gnid(table^ fontsize=1 2 )
图 11-33识别扫雷界而中的数字

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

clauses.append([-x for x in vs])

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字典保存与每个方格编兮
相邻的方格的编号。

from collections import defaultdict

variable_neighbors = defaultdict(list)

directs = [(_1 , -1 ),(- 1 , 0 ),(-l, 1 ),( 0 , -1 ),


(0 , 1 ), (1 , -1 ), (1 , 0 ), (1 , 1 )]

variables = np.arange(l, COLS * ROWS + 1).reshape(ROWS, COLS)

for (i, j), v in np.ndenumerate(variables):


for di, dj in directs:
i2 = i + di
j2 = j + dj
if 0 <= i2 < ROWS and 0 <= j2 < COLS:
variable_neighbors[v].append(variables[i2 ^ j2 ])

下而查看勹编号为5 0 的方格相邻的方格的编号:

variable_neighbors[50]
[19, 20, 21, 49, 51, 79, 80, 81]

用 variablm dghbon ; 字 典 以 及 前 而 介 绍 的 生 成 逻 辑 表 达 式 的 方 法 ,很容易编写如下


get_clauses(var_id,num)函数。它返回编号为 var_ i d 的方块中的数字为num 时的逻樹表达式。
def get一clauses(var一id, num):
clauses =[]
neighbors = variable一neighbors[var—id]
neg_neighbors = [-x for x in neighbors]
clauses.extend(combinations(neg_neighbons^ num + 1 ))
clauses.extend(combinations(neighbors^ len(neighbors) - num + 1 ))
clauses.append([-var_id])
return clauses

扫雷游戏与前面介绍的逻辑问题不同,它并不是要找到一组可能的解,而足要找到绝对足
雷以及绝对不是雷的方块。下面是 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.自动扫雷

本书提供了一个自动扫笛实例程序,它能够自动启动扫笛游戏,并自动操纵鼠細进行扫雷。

scpy 2.examples .automine : 在 Windows 7 系统下自动担雷,需将担雷游戏的难度设

置为高级(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

其中复数函数fe (z )的自变量为z 。而c 是一个复数参数,对于每一个c , 从z = 0开始对函数


fe (z )进行迭代。序列(0,fe 〇):
),fe (:
fe (:
0 :a 的值或者延伸到无限大,或者只停留在有限半径的
圆盘内。Mandelbrot集合就是使以上序列不发散的所有参数c 的集合。
从数学上来讲,Mandelbrot集合是一个复数的集合。 一 个给) 的复数 c 或者屈于 Mandelbrot
集合,或者不是。但是用程序绘制 Mandelbrot集合时不能进行无限次迭代,最简单的方法是使
用逃逸时间(迭代次数)进行绘制,具体算法如下:
•判断每次调用函数匕⑷得到的结果是否在半径 R 之内,即复数的模是否小于R 。
• 记 录 K迭代结果的模值大于R 时的迭代次数,也称之为逃逸时间。
• 迭 代 最 多 进 行 N 次。
•不同迭代次数的点使叫不同的颜色绘制。

693
Python科学计算(第2 版)

1.纯 P yth o n 实现

下面是完整的绘制 Mandelbrot集合的程序,它所绘制的图案如图11-35所示。

from m atplotlib import cm

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 # 返回迭代次数

def mandelbrot(cx> cy, d, n=2 0 0 ):


x0 , xlj y0 , yl = cx-d, cx+d, cy-d, cy+d
y, x = np.ogrid[y0 :yl:n*lj, x0 :xl:n*lj]
c = x + y*lj ©
return np.frompyfunc(iter_point,1 ,1 )(c).astype(np.float) O

def draw_mandelbrot(cx, cy, d, n=2 0 0 ): ©


■曹 i 雪 ■ 曹

绘制点(cx, cy)附近正负d 范围内的Mandelbrot


it it it

pi.imshow(mandelbrot(cx, cy, 6, n), cmap=cm.Blues_r) 0

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

O 函 数 iter_ point〇计 算 点 c 的逃逸时间,逃 逸 半 径 R 为 2.0,最 大 迭 代 次 数 为 100。 |


© draw_ mandelbrot〇绘制以点(cx ,cy )为中心,边 长 为 2*d 的正方形区域内的 Mandelbrot集合的 i
图案。 丨实
€)计算指定范围内的参数c , 它是一个二维的复数数组,形状为(200,200)。这里用 ogd d 对 | ^
象快速产生实部和虚部网格x 和 y , 然后通过广播运算得到数组c 。 |
® 接下来通过 fr〇
mpyfunc()将 iter_point〇转换为 uflinc 函数,
这样它可以〔
-1动对c 中的毎个元 丨
素调用 iter_point〇进行运算。由于结果数组的元素类型为object, 还需要调用 astypeO将其元素类 |
型转换为浮点数类型。©最 后 调 用 imshowO将结果数组绘制成图,通过关键字参数 cm ap 指定颜 |
色映射表。 |

2.用 Cython 提速 |

使 用 Python 绘 制 Mandelbrot集合,最大的问题就是运算速度太慢: 丨

%timeit mandelbrot(-0.5, 0 , 1.5) ;


1 loops, best of B: 398 ms per loop

而 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 倍:

%timeit mandelbrot(-0.5, 0 , 1.5)


100 loops, best of 3: 8.89 ms per loop

3.连续的逃逸时间

修改逃逸半径 R 和最大迭代次数 N , 可以绘制出不N 效果的 Mandelbrot集合图案。似是前


述方法计兑出的逃逸时间是大于逃逸半径时的迭代次数,因此输出图像最多只有N 种不同的颜
色值,有很强的梯度感。为了在不同的梯度之间进行渐变处理,可是使川下面的公式计算逃逸
时间:
n - lo g 2lo g 2|z n|
z n是迭代 ri 次之后的结果,通过在逃逸时间的计算中引入迭代结果的模值,结果将不再是
整数,而是平滑渐变的。下面是计算此逃逸时间的程序:

%%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 版)

scpy2.examples.fmctal.mandelbrot_demo : 使用 TraitsUI 和 matplotlib 实时绘制 Mandelbrot


图像,按住鼠标左键进行平移,使用鼠标滚轴进行缩放。

下 而 是 计 算 M andelbrot 集 合 图 像 的 C y th o n 函 数 。该 函 数 所 在 模 块 的 完 整 路 径 为
scpy2.examples.fractal.fastfractal。参 数 c x 和 c y 是复平而上的计算范刚的中心点,参 数 d 是中心点
到计算边界的实轴上的长度,参 数 out 是保存计算结果的二维数组。如果不指定 out, 则需通过
h 和 w 参数指定返回数组的大小。参 数 n 为最大迭代次数,R 为逃逸半径。

from libc.math cimport log2


import numpy as np
import cython

cdef double iter一point(complex c, int n, double 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

y(n+l) = 0.16 * y(n)


2.
x(n+l) = 0 . 2 * x(n) - 0.26 * y(n)
y(n+l) = 0.23 * x(n) + 0.22 * y(n) + 1.6
3.
x(n+l) = -0.15 * x(n) + 0.28 * y(n)
y(n+l) = 0.26 * x(n) + 0.24 * y(n) + 0.44
4.
x(n+l) = 0.85 * x(n) + 0.04 * y(n)
y(n+l) = -0.04 * x(n) + 0.85 * y(n) + 1.6

所谓迭代函数系统,是指将函数的输出再次当作输入进行迭代计算,因此上面的公式都是
通过坐标 x (n),y (n)计算变换后的坐标x (n+ l ),y (n+ l )。问题是有4 个迭代函数,迭代时选择哪个
函数进行计算呢?我们为每个函数指定一个概率值,它们依次为1 % 、7 % 、7 % 和 8 5 % 。通过每
个闲数的概率随机选杼一个函数进行迭代。在上面的例子中,第 4 个闲数被选杼进行迭代的概
率最高。
最后我们从坐标原点(0,0)开始迭代,绘制每次迭代后得到的坐标点,就得到了迭代函数系
统的分形图案。下面的程序演示了这一计兑过程:

%config InlineBackend.figure一ofrmat = 'png'


eql = np.array([[0,0,0],[0,0.16,0]])
pi = 0 .0 1
Python科学计算(第2 版)

eq2 = np.array([[0.2,-0.26,0],[0.23,0.22,1.6]])
p2 = 0.07

eq3 = np.array([[-0.15, 0.28^ 0],[0.26,0.24^0.44]])


p3 = 0.07

eq4 = np.array([[0.85, 0.04,0],[-0.04, 0.85, 1.6]])


p4 = 0.85

def ifs(p, eq, init, n):


it i t it

进行函数迭代
p : 每个函数的选抒概率列表
eq : 迭代函数列表
init: 迭代初始点

s n : 迭代次数

返回值:每次迭代所得的X 坐标数组、Y 坐标数组,计算所用的函数下标

# 迭代向量的初始化
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

return result[:,0 ], result[:, 1 ], c

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 )

ifs ()萣进行闲数迭代的主闲数,O 我们希望通过矩阵乘法计算迭代方程的输出,冈此需要

将乘法向量扩充为三维:这样每次和迭代函数系数进行矩阵乘积运算的向量就变成了 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所计算

的是叶杆部分(深蓝色),概率为7 % 的两个函数计算的是左右两片子叶,而概率为8 5 % 的函数计

算的是整片叶子的迭代,即最下面的三种颜色的点通过此闲数的迭代产生上面所有的深红色的

点。可以看出整片叶子呈现出完美的自相似特性,任意取其中的一片子叶,将其旋转放大之后
都和整片叶子相同。
Python科学计算(第 2 版)

图 11-38函数迭代系统所绘制的蕨类植物的叶,

1.2D 仿射变换
I
上面所介绍的4 个变换方程的一般形式如下:

x(n+l) = A * x(n) + B * y(n) + C


y(n+l) = D * x(n) + E * y(n) + F

这种变换被称为2D 仿射变换,它是从2D 坐标到其他2D 坐标的线性映射,保留直线性和


平行性。即原来是一条直线上的点,变换之后仍然在一条直线上,原来是平行的直线,变换之
后仍然是平行的。这种变换可以看作是由一系列平移、缩放、翻转和旋转变换构成的。
可以使用平面上的两个三角形直观地表示仿射变换。因为仿射变换公式中有6 个未知
数 一 ~ A 、B 、C 、D 、E 、F , 而每两个点之间的变换是两个方程,因此一共需要3 组点来决定6
个变换方程,正好是两个三角形,如 图 11-39所示:

图 11-39两个三角形决定一个2 D 仿射变换的6 个参数

从红色三角形的每个顶点变换到绿色三角形的对应顶点,正好能够决定仿射变换中的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 版)

def solve一eq(trianglel, triangle2 ):


it it ii

解方程,从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

S a[2, 0:3] = xl, yl, 1


a[3, 3:6] = xl, yl, 1
a[4, 0:3] = 1
a[5, 3:6] = y2, yl, 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

绘图界而采用 matplotlib 绘图序,由于绘制大童散列点会导致界而刷新速度变慢,冈此在


本演示程序中对迭代生成的坐标点进行二维直方图统计,并使用 imsh〇
w ()绘制统计结果。为了
提高程序运行速度,方程迭代以及二维直方图统计均在CythcMi编写的 IF S 扩展类中实现。下而
通过一个例子说明这些函数的用法。IF S 扩展类的源代码可以在fastfmctal.p y x 中找到。
O 在下面的 triangles 中保存着3 个三角形的顶点坐标,其中第一个三角形为-基础三角形,

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所示。

from s c p y 2 .e x a m p l e s .f r a c t a l .ifs_demo import solve_eq, triangle— area

from scpy2.examples.fractal.fastfractal import IFS

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)

total_area = areal + area2

p = [areal / total— area, area2 / total 一area]

eql = solve_eq(base 一triangle, trianglel) ©

eq2 = solve 一eq(base_triangle, triangle2)

eqs = np.vstack([eql, eq2])

ifs = IFS(p, eqs, 2000000, size=600) O

counts = i f s . u p d a te () ©

705
Python 科学计算(第 2 版)

print "shape of counts:", counts.shape

from matplotlib.colors import LogNorm

fig, ax = pl.subplots(figsize=(5, 8))

pi.imshow(counts, cmap="Blues", norm=LogNorm(), origin="lower") ©

ax.axis("off")

shape of counts: (600, 477)

图 1141 使ffl IFS类绘制迭代函数系统

11.6.3 L-System 分形

前面所绘制的分形图案都是使用数学W 数的迭代产生的,而 L-System 分形则采用符号的递


归迭代产生。首先定义如下儿个有含义的符号:
* F : 向前走固定单位
• + : 正方向旋转同定角度
♦ 负方向旋转同定角度
使用这三个符号很容易描述图1142中左上方由4 条线段构成的图案:

F+F--F+F

如果将此符号串中的所有F 都替换为 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"

"X":"F-[[X]+X]+F[+FX]-X", "F":"FI=", "S":"X",


"direct":-45,
"angle":25,
••iter": 6,

"title":"Plant"

"S":"X", "X":"-YF+XFX+FY-", "Y":"+XF-YFY-FX+",


"direct":0,
"angle":90,
"iter":6,

"title":"Hilbert"

{
"S":"L— F--L--F", "L":"+R-F-R+", "R":"-L+F+L-",
"direct":0,

"angle":45,
"iter":10,

" t i t l e " :"Sierpinski"

},

其中:
• direct: 绘阁的初始角度,通过指定不同的值可以旋转整个阁案
• angle: 定义符号+和-旋转时的角度,不同的值能产生完全不同的图案
• iter: 迭代次数
下面的程序将上述字典转换为需要绘制的线段坐标:

class L一S y s t e m ( o bj e c t ) :

def — init— (self, r u l e ) :


info = rul e [ ' S ' ]
for i in r a n g e (r u l e ['i t e r ']):
ninfo = [ ]

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()完成迭代计算和绘图工作:

def draw(ax> rule, iter=None):


from matplotlib import collections
if iten!=None:
rule["iter"] = iter
lines = L_System(rule).get一lines() O
linecollections = collections.LineCollection(lines, lw=0.7, color="black") O
ax.add_collection(linecollections, autolim=True) ©
ax.axis("equal")
ax.set_axis_off ()
ax.set_xlim(ax.dataLim.xminJ ax.dataLim.xmax)
ax.invert_yaxis()

709
Python科学计算(第 2 版)

0 用 L _System 的 getjin eso 计兑出每个线段的坐标之后,© 创建-•个表示所有线段集合的


LineCollection 对象,© 并调川 A x e s 对象的 add_collection()将此线段集合添加进 ax .collections 列表
中。这样能一次添加多条线段,提高显示速度。图 1143是程序所绘制的几种L -System 的分形
图案。

%config InlineBackend.figure一 format = 'png'


fig = pi.figure(figsize=(1 0 , 6 ))
fig.patch.set_facecolor("w")

for i in xrange(6 ):
ax = fig.add一 subplot(231+i)
draw(ax, rules[i])

fig.subplots_adjust(left=0 ,right=l,bottom=0 ,top=l,wspace=0 >hspace=0 )

图 1 1 4 3 儿种 L - S y s t c m 的迭代图案

1 1 . 6 . 4 分形山脉

前而介绍的分形图案都是严格按照指定的规则迭代生成的,然而向然界中的山川、云彩、
树木等都不是精确的自相似图形,而是在统计怠义上的A 相似阁形。木节将介绍儿种经典的山
脉地形的生成算法,以及如何用Python 快速实现这些算法。

1. 一维中点移位法

让我们从绘制一条分形llll线开始。使用中点位移算法(Midpoint Displacement) , 能够有效地


模拟山脉或海岸线的分形形状,算法如下:
(1)首先在 X 轴上収两个初始点 A 和 B 。
⑵ 找 到 A 、B 两点的屮点,并 在 Y 轴方向上进行随机移位,移位后的点为 C 。
⑶ 对 于 线 段 A C 和 B C 重复步骤⑵。

710
每次迭代时,随机移位的最大幅度都成比例地衰减。迭代足够多次之后,将所得到的点连
接起来,就得到了一条随机的分形曲线。
下面是实现此算法的源程序,程序所绘制的山脉曲线如图11^44所示。

def hillld(n> d):


ii ii ii

绘制山脉曲线,2**n+l为曲线在X 轴上的长度,d 为S 减系数


19 II II

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

100 200 300 400 S0<

图丨144 一维分形山脉曲线,袞减值越小,最大幅度的袞减越快,曲线越平滑

程序中,hillld ()计算一维分形山脉曲线。O 为了运算方便,我们用一维数组 a 保存丨11|线上


每点的《度 ,而丨11|线上每点的X 轴坐标则由数组的下标决定。这要求每次计算中点时所得到的

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

© 数组中每次要计算的中点的下标是一个等差数列,起始下标为 s=2H , 间隔为 s=2、 O 而


每个中点都出艽左右下标相差 s 的两个数值计兑。© 给每个中点一定的随机位移。这里使JIJ
I normalO产生一个正态分布的随机数组,其期望值为0 , 标准偏差为 scale 。这样产生的i l l 脉曲线
才能既有山峰也有山谷。© 最后将下一次迭代的标准偏差乘上系数d , 因 此 d 越小,标准偏差
的衰减越快。
接下来绘制 d 为 0.4、0.5、0.6时的山脉曲线。© 这里为了对不同的衰减系数所产生的曲线
进行比较,需要保证每次都使用相同的随机数计算III丨线,因 此 使 用 .seed〇指定生成随机数的种
子。在需要真正随机产生曲线时,请将此句注释掉。

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所示。

def hill2d(n, d):


MMII

绘 制 山 脉 曲 面 , 曲 面 是 一 个 (2**n + l)*(2**n + 1) 的 图 像 , d 为 衰 减 系 数
II M II

from numpy.random import normal


size = 2**n + 1
scale = 1 . 0
a = np.zeros((size, size))

for i in xrange(n> 0, -1):


s = 2**(i-l)
s2 = s*2
tmp = a[::s2,::s2]
tmpl = (tmp[l:,:] + tmp[:-l,:])*0.5
tmp2 = (tmp[:,l:] + tmp[:,:-l])*0.5
tmp3 = (tmpl[:,l:] + t m p l [ :-1])*0.5 s
a[s: :s2, ::s2] = tmpl+ norma1(0, scale, tmpl.shape)
a[::s2, s::s2] = tmp2+ norma1(0, scale, tmp2.shape)
a[s::s2,s::s2] = tmp3+ normal(0^ scale, tmp3.shape)
scale *= d

return a

from scpy2 import vtk_scene_to一array


from mayavi import mlab
from scipy.ndimage.filters import convolve

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 的变化可以帮
助理解程序和菱形方形算法。

def hill2 d_ds(n, d):


from numpy.random import normal
size = 2 **n + 1
scale = 1 . 0
a = np.zeros((size, size))

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)

buf = a[::s2 , ::s2 ]

# 菱形平均分两步,分别计箅水平和®直方向上的点
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
请尽量支持正版
如果资金充裕,
请购买纸质版。

You might also like