Python-数学指南-全-

Python 数学指南(全)

原文:zh.annas-archive.org/md5/29688fa5d1f2cb17e6b155bc37e5c030

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

image

本书的目标是将我心爱的三个主题——编程、数学和科学——结合在一起。这具体意味着什么呢?在本书中,我们将以编程的方式探索一些高中水平的主题,比如操作测量单位;研究抛体运动;计算均值、中位数和众数;确定线性相关性;求解代数方程;描述简单摆的运动;模拟骰子游戏;创建几何形状;以及求解函数的极限、导数和积分。这些对许多人来说都是熟悉的话题,但我们不再使用纸笔,而是通过计算机来探索它们。

我们将编写一些程序,这些程序将接受数字和公式作为输入,进行繁琐的计算,然后输出解或绘制图形。其中一些程序是功能强大的计算器,用来解决数学问题。它们能够求解方程,计算数据集之间的相关性,并确定函数的最大值等任务。在其他程序中,我们将模拟现实生活中的事件,如抛体运动、掷硬币或掷骰子。使用程序来模拟这些事件,给我们提供了一个轻松分析和深入了解它们的方式。

你还会发现,一些没有程序的帮助,探索起来会非常困难的主题。例如,手工绘制分形图无论如何都是一项繁琐的工作,最糟糕的情况几乎是不可能完成的。有了程序,我们所需要做的就是运行一个for循环,并在循环体内执行相关操作。

我认为你会发现,这种“做数学”的新方式使得编程和数学的学习更加激动人心、有趣且富有成就感。

谁应该阅读本书

如果你正在学习编程,你一定会欣赏本书展示的使用计算机解决问题的方法。同样,如果你是教这类学习者的老师,我希望你能从本书中找到有用的示例,展示编程技能在计算机科学的抽象世界之外的应用。

本书假设读者已经了解 Python 3 编程的基本知识——具体来说,函数是什么,函数参数是什么,Python 类和类对象的概念,以及循环的知识。附录 B 涵盖了程序中使用的一些其他 Python 主题,但本书并不假定读者掌握这些额外的知识。如果你觉得自己需要更多背景知识,建议阅读 Jason Briggs 的《Python for Kids》(No Starch Press,2013)。

本书内容是什么?

本书共由七章和两个附录组成。每一章结束时都有挑战题留给读者。我建议大家尝试完成这些挑战,因为从尝试编写自己的原创程序中,你会学到很多东西。部分挑战题将要求你探索新主题,这是加深学习的一个好方法。

第一章与数字打交道,从基本的数学运算开始,逐渐过渡到需要更高数学水平的主题。

第二章使用图表可视化数据,讨论了如何使用 matplotlib 库从数据集中创建图表。

第三章用统计学描述数据,继续处理数据集的主题,涵盖基本的统计概念——均值、中位数、众数,以及数据集中变量的线性相关性。你还将学习如何处理 CSV 文件中的数据,这是分发数据集的常用文件格式。

第四章使用 SymPy 进行代数和符号数学,介绍了使用 SymPy 库进行符号数学的内容。它从表示和操作代数表达式的基础开始,然后介绍更复杂的内容,例如求解方程。

第五章玩转集合和概率,讨论了数学集合的表示,并讲解了基本的离散概率。你还将学习如何模拟均匀和非均匀随机事件。

第六章绘制几何图形和分形,讨论了使用 matplotlib 绘制几何图形和分形并创建动画图形。

第七章求解微积分问题,讨论了 Python 标准库和 SymPy 中的一些数学函数,然后介绍了如何求解微积分问题。

附录 A软件安装,涵盖了在 Microsoft Windows、Linux 和 Mac OS X 上安装 Python 3、matplotlib 和 SymPy。

附录 BPython 主题概述,讨论了一些可能对初学者有帮助的 Python 主题。

脚本、解答和提示

本书的伴随网站是 www.nostarch.com/doingmathwithpython/。在这里,你可以下载本书中的所有程序,以及挑战题的提示和解答。你还可以找到我认为有用的其他数学、科学和 Python 资源的链接,以及本书的任何更正或更新。

软件不断更新;Python、SymPy 或 matplotlib 的新版本可能会导致本书中展示的某些功能表现不同。你可以在网站上找到这些变化的记录。

我希望本书能让你进入计算机编程的旅程变得更加有趣和切合实际。让我们一起做点数学吧!

第一章:与数字打交道

image

让我们迈出使用 Python 探索数学和科学世界的第一步。现在我们保持简单,帮助你掌握如何使用 Python 本身。我们将从执行基本的数学运算开始,然后编写简单的程序来操作和理解数字。让我们开始吧!

基本数学运算

Python 交互式 shell 将成为我们在本书中的好朋友。启动 Python 3 IDLE shell 并打个“招呼”(见 图 1-1),通过输入 print('Hello IDLE') 然后按 ENTER。 (有关如何安装 Python 和启动 IDLE 的说明,请参见 附录 A)。IDLE 会执行你的命令并将文字打印回屏幕。恭喜——你刚刚编写了一个程序!

当你再次看到 >>> 提示符时,IDLE 准备好接受更多指令。

image

图 1-1:Python 3 IDLE shell

Python 可以像一个高级计算器一样进行简单的计算。只需输入表达式,Python 就会评估它。按下 ENTER 后,结果会立即显示。

试试吧。你可以使用加法(+)和减法()运算符来进行加减法运算。例如:

>>> 1 + 2
3
>>> 1 + 3.5
4.5
>>> -1 + 2.5
1.5
>>> 100 – 45
55
>>> -1.1 + 5
3.9

要进行乘法运算,使用乘法(*)运算符:

>>> 3 * 2
6
>>> 3.5 * 1.5
5.25

要进行除法运算,使用除法(/)运算符:

>>> 3 / 2
1.5
>>> 4 / 2
2.0

如你所见,当你让 Python 执行除法运算时,它还会返回数字的小数部分。如果你希望结果是整数形式,并去掉任何小数值,你应该使用 floor 除法(//)运算符:

>>> 3 // 2
1

floor 除法运算符将第一个数字除以第二个数字,然后将结果向下舍入到下一个最小整数。当其中一个数字为负时,这个操作会变得有趣。例如:

>>> -3 // 2
-2

最终结果是除法操作结果的下一个整数(-3/2 = -1.5,因此最终结果为 -2)。

另一方面,如果你只需要余数,应该使用模运算符(%):

>>> 9 % 2
1

你可以使用指数(**)运算符计算数字的幂。以下示例演示了这一点:

>>> 2 ** 2
4
>>> 2 ** 10
1024
>>> 1 ** 10
1

我们还可以使用指数符号来计算小于 1 的幂。例如,数字 n 的平方根可以表示为 n^(1/2),立方根可以表示为 n^(1/3):

>>> 8 ** (1/3)
2.0

正如这个例子所示,你可以使用括号将数学运算结合成更复杂的表达式。Python 将按照标准的 PEMDAS 规则来评估表达式的计算顺序——括号、指数、乘法、除法、加法和减法。考虑以下两个表达式——一个没有括号,一个有括号:

>>> 5 + 5 * 5
30
>>> (5 + 5) * 5
50

在第一个例子中,Python 先进行乘法运算:5 乘以 5 得 25;25 加 5 得 30。在第二个例子中,括号内的表达式首先被评估,正如我们所期望的那样:5 加 5 得 10;10 乘以 5 得 50。

这些是在 Python 中操作数字的基本知识。现在让我们学习如何将数字赋予名称。

标签:为数字附加名称

随着我们开始设计更复杂的 Python 程序,我们会给数字赋予名称——有时是为了方便,但大多数时候是出于必要性。这里有一个简单的例子:

➊ >>> a = 3
   >>> a + 1
   4
➋ >>> a = 5
   >>> a + 1
   6

在 ➊,我们将名称 a 赋给数字 3。当我们让 Python 计算表达式 a + 1 的结果时,它看到 a 所代表的数字是 3,然后它加上 1 并显示结果(4)。在 ➋,我们将 a 的值改为 5,这个变化反映在第二次加法操作中。使用名称 a 很方便,因为你可以简单地改变 a 所指向的数字,之后每次引用 a 时,Python 都会使用这个新的值。

这种名称被称为标签。你可能在其他地方接触过术语 变量 来描述相同的概念。然而,考虑到 变量 也是一个数学术语(用于表示像 x 这样的东西,比如在方程 x + 2 = 3 中),在本书中我仅在数学方程和表达式的上下文中使用术语 变量

不同种类的数字

你可能注意到,我用两种不同的数字来演示数学运算——没有小数点的数字,你已经知道是整数,以及带有小数点的数字,程序员称之为浮点数。我们人类在识别和处理数字时,无论它们是以整数、浮点小数、分数还是罗马数字形式出现,都不会遇到问题。但在我们在本书中编写的一些程序中,只对某种特定类型的数字执行任务才有意义,因此我们通常需要编写一些代码,检查我们输入的数字是否属于正确的类型。

Python 将整数和浮点数视为不同的类型。如果你使用 type() 函数,Python 会告诉你你输入的数字是哪种类型。例如:

>>> type(3)
<class 'int'>

>>> type(3.5)
<class 'float'>

>>> type(3.0)
<class 'float'>

在这里,你可以看到 Python 将数字 3 分类为整数(类型 'int'),而将 3.0 分类为浮点数(类型 'float')。我们都知道 3 和 3.0 在数学上是等价的,但在许多情况下,Python 会将这两个数字视为不同的类型。

本章中我们编写的一些程序只有在输入为整数时才能正确工作。正如我们刚才看到的,Python 不会将像 1.0 或 4.0 这样的数字识别为整数,所以如果我们想让这些数字在程序中作为有效输入,我们需要将它们从浮点数转换为整数。幸运的是,Python 中有一个内置函数可以完成这项转换:

>>> int(3.8)
3
>>> int(3.0)
3

函数 int() 接受输入的浮点数字,去掉小数点后的部分,并返回结果整数。float() 函数类似,用于执行反向转换:

>>> float(3)
3.0

float() 函数接收输入的整数,并在其后添加小数点,将其转换为浮动点数。

分数操作

Python 也可以处理分数,但为了实现这一点,我们需要使用 Python 的 fractions 模块。你可以将模块视为他人编写的程序,你可以在自己的程序中使用。一个模块可以包含类、函数,甚至是标签定义。它可以是 Python 的标准库的一部分,或者来自第三方的位置。在后一种情况下,你需要在使用之前先安装该模块。

fractions 模块是标准库的一部分,这意味着它已经安装好了。它定义了一个 Fraction 类,我们将使用这个类将分数输入到程序中。在使用之前,我们需要先导入它,这是一种告诉 Python 我们希望使用该模块中类的方法。让我们看一个简单的例子——我们将创建一个新标签 f,它表示分数 3/4:

➊ >>> from fractions import Fraction
➋ >>> f = Fraction(3, 4)
➌ >>> f
   Fraction(3, 4)

我们首先从 fractions 模块导入 Fraction 类 ➊。接下来,我们通过传入分子和分母作为参数来创建该类的一个对象 ➋。这将创建一个表示分数 3/4 的 Fraction 对象。当我们打印该对象 ➌ 时,Python 会以 Fraction(numerator, denominator) 的形式显示该分数。

基本的数学运算,包括比较运算,对于分数都是有效的。你还可以将分数、整数和浮动点数结合在一个表达式中:

>>> Fraction(3, 4) + 1 + 1.5
3.25

当表达式中包含浮动点数时,表达式的结果将返回浮动点数。

另一方面,当表达式中只有分数和整数时,结果将是分数,即使结果的分母为 1。

>>> Fraction(3, 4) + 1 + Fraction(1/4)
Fraction(2, 1)

现在你已经了解了在 Python 中使用分数的基本知识。接下来我们将介绍另一种数字类型。

复数

到目前为止,我们看到的数字是所谓的实数。Python 还支持复数,其虚部用字母 jJ 表示(而不是数学符号中使用的字母 i)。例如,复数 2 + 3i 在 Python 中将写作 2 + 3j

>>> a = 2 + 3j
>>> type(a)
<class 'complex'>

如你所见,当我们在复数上使用 type() 函数时,Python 会告诉我们这是一个 complex 类型的对象。

你也可以使用 complex() 函数来定义复数:

>>> a = complex(2, 3)
>>> a
(2 + 3j)

在这里,我们将复数的实部和虚部分别作为两个参数传递给 complex() 函数,它会返回一个复数。

你可以像处理实数一样处理复数的加法和减法:

>>> b = 3 + 3j
>>> a + b
(5 + 6j)
>>> a - b
(-1 + 0j)

复数的乘法和除法也采用类似的方式进行:

>>> a * b
(-3 + 15j)
>>> a / b
(0.8333333333333334 + 0.16666666666666666j)

模运算符(%)和地板除法(//)运算符对复数无效。

复数的实部和虚部可以通过其 realimag 属性来获取,如下所示:

>>> z = 2 + 3j
>>> z.real
2.0
>>> z.imag
3.0

复数的共轭具有相同的实部,但虚部的大小相同并且符号相反。它可以通过conjugate()方法获得:

>>> z.conjugate()
(2 - 3j)

实部和虚部都是浮点数。使用实部和虚部,你可以使用以下公式计算复数的大小,其中xy分别是复数的实部和虚部:image。在 Python 中,这样写:

>>> (z.real ** 2 + z.imag ** 2) ** 0.5
3.605551275463989

找到复数的大小的一种更简单的方法是使用abs()函数。abs()函数在传入实数时返回绝对值。例如,abs(5)abs(-5)都返回 5。然而,对于复数,它返回的是复数的大小:

>>> abs(z)
3.605551275463989

标准库的cmath模块(cmath代表复数数学)提供了许多其他专门的函数,用于处理复数。

获取用户输入

当我们开始编写程序时,拥有一个简单的方式通过input()函数接受用户输入会非常有帮助。这样,我们可以编写要求用户输入数字、对数字执行特定操作并显示操作结果的程序。让我们看看它的实际应用:

➊ >>> a = input()
➋ 1

在➊处,我们调用了input()函数,它会等待你输入内容,如➋所示,按下 ENTER 键。输入的内容会存储在a中:

   >>> a
➌ '1'

注意在➌处 1 周围的单引号。input()函数返回的输入是一个字符串。在 Python 中,字符串是任何位于两个引号之间的字符集合。当你想创建一个字符串时,可以使用单引号或双引号:

>>> s1 = 'a string'
>>> s2 = "a string"

在这里,s1s2都指向相同的字符串。

即使字符串中的唯一字符是数字,Python 也不会将该字符串视为数字,除非我们去除这些引号。因此,在进行任何数学运算之前,我们需要将其转换为正确的数字类型。字符串可以分别通过int()float()函数转换为整数或浮点数:

>>> a = '1'
>>> int(a) + 1
2
>>> float(a) + 1
2.0

这些是我们之前看到的相同的int()float()函数,但这次它们不是将输入从一种数字类型转换为另一种,而是将一个字符串作为输入(如'1')并返回一个数字(22.0)。然而,需要注意的是,int()函数不能将包含浮点小数的字符串转换为整数。如果你将一个包含浮点数的字符串(如'2.5'甚至'2.0')传递给int()函数,它会返回一个错误信息:

>>> int('2.0')
Traceback (most recent call last):

  File "<pyshell#26>", line 1, in <module>
    int('2.0')
ValueError: invalid literal for int() with base 10: '2.0'

这是一个异常的例子——Python 用来告诉你,由于错误它无法继续执行程序。在这种情况下,异常的类型是ValueError。(有关异常的快速复习,请参见附录 B)

类似地,当你输入像 3/4 这样的分数时,Python 无法将其转换为等效的浮点数或整数。再次出现 ValueError 异常:

>>> a = float(input())
3/4
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    a=float(input())
ValueError: could not convert string to float: '3/4'

你可能会发现将转换操作放在 try...except 块中很有用,这样你就可以处理这个异常,并提醒用户程序遇到了无效输入。接下来,我们将讨论 try...except 块。

处理异常和无效输入

如果你不熟悉 try...except,其基本思想是这样的:如果你在 try...except 块中执行一个或多个语句,并且执行时发生了错误,程序不会崩溃并打印出 Traceback。相反,执行会转移到 except 块,在这里你可以执行适当的操作,例如打印一条有帮助的错误信息或尝试其他方法。

这是如何在 try...except 块中执行上述转换并在输入无效时打印有帮助的错误信息:

>>> try:
        a = float(input('Enter a number: '))
except ValueError:
        print('You entered an invalid number')

请注意,我们需要指定要处理的异常类型。在这里,我们要处理 ValueError 异常,所以我们指定为 except ValueError

现在,当你输入无效的内容时,例如 3/4,它会打印出一个有帮助的错误信息,如 ➊ 所示:

   Enter a number: 3/4
➊ You entered an invalid number

你还可以在 input() 函数中指定一个提示信息,告诉用户期望的输入类型。例如:

>>> a = input('Input an integer: ')

用户现在会看到提示信息,提示输入一个整数:

Input an integer: 1

在本书的许多程序中,我们会要求用户输入一个数字,因此我们必须确保在执行任何操作之前处理好转换。你可以将输入和转换结合在一条语句中,如下所示:

>>> a = int(input())
1
>>> a + 1
2

如果用户输入的是整数,这个方法运行得非常好。但正如我们之前看到的,如果输入的是浮点数(即使是与整数等价的数字,比如 1.0),这会导致错误:

>>> a = int(input())
1.0
Traceback (most recent call last):
  File "<pyshell#42>", line 1, in <module>
    a=int(input())
ValueError: invalid literal for int() with base 10: '1.0'

为了避免这个错误,我们可以像之前处理分数那样设置一个 ValueError 捕获机制。这样,程序就会捕获浮点数,这在处理整数的程序中是不可行的。然而,它也会标记像 1.0 和 2.0 这样的数字,虽然 Python 视为 浮点数,但它们等价于整数,如果按正确的 Python 类型输入,它们完全能正常工作。

为了避免这个问题,我们将使用 is_integer() 方法来过滤掉小数点后有有效数字的数字。(此方法仅适用于 Python 中的 float 类型数字;它无法处理已经以整数形式输入的数字。)

这是一个例子:

>>> 1.1.is_integer()
False

在这里,我们调用 is_integer() 方法来检查 1.1 是否为整数,结果是 False,因为 1.1 确实是浮点数。另一方面,当使用 1.0 这个浮点数调用该方法时,结果是 True

>>> 1.0.is_integer()
True

我们可以使用 is_integer() 来过滤掉非整数输入,同时保留像 1.0 这样的输入,虽然它是浮点数表示,但与整数等价。稍后我们将看到该方法如何融入一个更大的程序中。

分数和复数作为输入

我们之前学习过的 Fraction 类也可以将类似 '3/4' 的字符串转换为 Fraction 对象。实际上,这就是我们接受分数作为输入的方式:

>>> a = Fraction(input('Enter a fraction: '))
Enter a fraction: 3/4
>>> a
Fraction(3, 4)

尝试输入一个分数,如 3/0:

>>> a = Fraction(input('Enter a fraction: '))
Enter a fraction: 3/0
Traceback (most recent call last):
  File "<pyshell#2>", line 1, in <module>
    a = Fraction(input('Enter a fraction: '))
  File "/usr/lib64/python3.3/fractions.py", line 167, in __new__
    raise ZeroDivisionError('Fraction(%s, 0)' % numerator)
ZeroDivisionError: Fraction(3, 0)

ZeroDivisionError 异常消息告诉你(如你所知),分母为 0 的分数是无效的。如果你计划让用户在你的程序中输入分数作为输入,最好总是捕获此类异常。以下是你如何处理类似情况的示例:

>>> try:
        a = Fraction(input('Enter a fraction: '))
except ZeroDivisionError:
        print('Invalid fraction')

Enter a fraction: 3/0
Invalid fraction

现在,每当程序的用户输入一个分母为 0 的分数时,它会打印出 Invalid fraction 消息。

同样,complex() 函数可以将类似 '2+3j' 的字符串转换为复数:

>>> z = complex(input('Enter a complex number: '))
Enter a complex number: 2+3j
>>> z
(2+3j)

如果你输入的字符串是 '2 + 3j'(带有空格),将会导致 ValueError 错误消息:

>>> z = complex(input('Enter a complex number: '))
Enter a complex number: 2 + 3j
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
    z = complex(input('Enter a complex number: '))
ValueError: complex() arg is a malformed string

在将字符串转换为复数时,像我们对其他数字类型所做的那样,捕获 ValueError 异常是一个好主意。

编写为你做数学运算的程序

现在我们已经学习了一些基本概念,我们可以将它们与 Python 的条件语句和循环语句结合起来,编写一些稍微复杂一些的有用程序。

计算整数的因子

当一个非零整数 a 能够整除另一个整数 b 并且余数为 0 时,称 ab因子。例如,2 是所有偶数的因子。我们可以编写如下函数来判断一个非零整数 a 是否是另一个整数 b 的因子:

>>> def is_factor(a, b):
        if b % a == 0:
            return True
        else:
            return False

我们使用本章前面介绍的 % 运算符来计算余数。如果你曾经问过类似“4 是 1024 的因子吗?”这样的问题,你可以使用 is_factor() 函数:

>>> is_factor(4, 1024)
True

对于任何正整数 n,我们如何找到它的所有正因子?对于从 1 到 n 之间的每个整数,我们检查将 n 除以该整数后的余数。如果余数为 0,它就是一个因子。我们将使用 range() 函数编写一个程序,遍历 1 到 n 之间的每个数字。

在编写完整程序之前,让我们先看看 range() 是如何工作的。range() 函数的典型用法如下:

>>> for i in range(1, 4):
        print(i)
1
2
3

在这里,我们设置了一个for循环,并给range()函数传递了两个参数。range()函数从作为第一个参数给定的整数(起始值)开始,一直到第二个参数给定的整数的前一个整数(停止值)。在这个例子中,我们告诉 Python 打印出这个范围内的数字,从 1 开始,停在 4 之前。注意,这意味着 Python 不会打印 4,因此它打印的最后一个数字是停止值之前的那个数字(3)。同样需要注意的是,range()函数只接受整数作为其参数。

你也可以在不指定起始值的情况下使用range()函数,这时默认的起始值为 0。例如:

>>> for i in range(5):
        print(i)
0
1
2
3
4

range()函数产生的两个连续整数之间的差值被称为步长值。默认情况下,步长值为 1。如果你想指定不同的步长值,可以将其作为第三个参数来指定(当你指定步长值时,起始值不是可选的)。例如,下面的程序打印出小于 10 的奇数:

>>> for i in range(1,10,2):
        print(i)
1
3
5
7
9

好的,现在我们已经了解了range()函数的工作原理,我们可以开始编写一个计算因子的程序了。因为我要写一个比较长的程序,所以我不在交互式的 IDLE 提示符中编写程序,而是使用 IDLE 编辑器。在 IDLE 中,你可以通过选择文件新建窗口来启动编辑器。注意,我们在代码中开始时用三个连续的单引号(')来注释代码。单引号之间的文本不会被 Python 执行,它只是供我们人类参考的注释。

   '''
   Find the factors of an integer
   '''

   def factors(b):

➊     for i in range(1, b+1):
           if b % i == 0:
               print(i)

   if __name__ == '__main__':

       b = input('Your Number Please: ')
       b = float(b)

➋     if b > 0 and b.is_integer():
           factors(int(b))
       else:
           print('Please enter a positive integer')

factors()函数定义了一个for循环,使用range()函数迭代从 1 到输入整数之间的每个整数,在➊处迭代。这里,我们想要迭代到用户输入的整数b,所以停止值设置为b+1。对于每一个整数i,程序会检查它是否能整除输入的数字,并在能够整除时打印该整数。

当你运行这个程序(通过选择运行运行模块),它会要求你输入一个数字。如果你的数字是一个正整数,它会打印出这个数字的因子。例如:

Your Number Please: 25
1
5
25

如果你输入一个非整数或负整数作为输入,程序会打印一条错误信息,要求你输入一个正整数:

Your Number Please: 15.5
Please enter a positive integer

这是一个例子,展示了我们如何通过在程序中检查无效输入来让程序更具用户友好性。因为我们的程序仅适用于查找正整数的因子,所以我们使用is_integer()方法➋来检查输入的数字是否大于 0 且为整数,从而确保输入有效。如果输入的不是正整数,程序会打印一条用户友好的提示信息,而不是一个错误信息。

生成乘法表

考虑三个数字,abn,其中 n 是一个整数,满足

a × n = b

我们可以在这里说 ban 次倍数。例如,4 是 2 的第二个倍数,1024 是 2 的第 512 个倍数。

一个数字的乘法表列出了该数字的所有倍数。例如,2 的乘法表如下(这里展示前三个倍数):

2 × 1 = 2

2 × 2 = 4

2 × 3 = 6

我们的下一个程序会生成用户输入的任意数字的乘法表,最高到 10。在这个程序中,我们将使用 format() 方法配合 print() 函数,以帮助使程序的输出看起来更加美观和易读。如果你以前没有见过这个方法,我现在简要说明一下它是如何工作的。

format() 方法允许你插入标签并设置它们,以便它们以漂亮、可读的字符串格式打印出来,并且加上一些额外的格式化。例如,如果我有一个我在杂货店买的所有水果的名字,每个名字都有单独的标签,并且我想把它们打印成一个连贯的句子,我可以使用 format() 方法,像这样:

>>> item1 = 'apples'
>>> item2 = 'bananas'
>>> item3 = 'grapes'
>>> print('At the grocery store, I bought some {0} and {1} and {2}'.format(item1, item2, item3))
At the grocery store, I bought some apples and bananas and grapes

首先,我们创建了三个标签(item1item2item3),每个标签对应一个不同的字符串(applesbananasgrapes)。然后,在 print() 函数中,我们输入了一个包含三个占位符的字符串,分别是 {0}{1}{2}。接着我们使用 .format(),它包含了我们创建的三个标签。这告诉 Python 用这些标签中存储的值按顺序替换这些占位符,从而 Python 会打印出文本,{0} 被第一个标签替换,{1} 被第二个标签替换,以此类推。

不一定需要有标签来指向我们想要打印的值。我们也可以直接将值输入到 .format() 中,像下面这个例子:

>>> print('Number 1: {0} Number 2: {1} '.format(1, 3.578))
Number 1: 1 Number 2: 3.578

请注意,位置占位符的数量和标签或值的数量必须相等。

现在我们已经了解了 format() 是如何工作的,我们可以来看看我们乘法表打印程序的代码:

   '''
   Multiplication table printer
   '''

   def multi_table(a):

➊     for i in range(1, 11):
           print('{0} x {1} = {2}'.format(a, i, a*i))

   if __name__ == '__main__':
       a = input('Enter a number: ')
       multi_table(float(a))

函数 multi_table() 实现了程序的主要功能。它接收一个参数 a,这个参数是将要打印乘法表的数字。因为我们想打印从 1 到 10 的乘法表,所以我们在 ➊ 处有一个 for 循环,它会遍历这些数字,打印出该数字和 a 的乘积。

当你执行程序时,它会要求你输入一个数字,然后程序打印出该数字的乘法表:

Enter a number : 5
5.0 x 1 = 5.0
5.0 x 2 = 10.0
5.0 x 3 = 15.0
5.0 x 4 = 20.0
5.0 x 5 = 25.0
5.0 x 6 = 30.0
5.0 x 7 = 35.0
5.0 x 8 = 40.0
5.0 x 9 = 45.0
5.0 x 10 = 50.0

看看这张表格多么整齐和有序?这正是因为我们使用了 .format() 方法按可读的、统一的模板打印了输出。

你可以使用 format() 方法进一步控制数字的打印方式。例如,如果你想显示只有两位小数的数字,可以通过 format() 方法来指定。下面是一个例子:

>>> '{0}'.format(1.25456)
'1.25456'
>>> '{0:.2f}'.format(1.25456)
'1.25'

上面的第一个格式语句仅仅是按照我们输入的方式打印数字。在第二个语句中,我们修改了占位符为{0:.2f},意味着我们只想保留小数点后两位,f表示浮动小数点数。如你所见,接下来的输出中只有两个小数位。请注意,如果小数点后有比指定更多的数字,数字会四舍五入。例如:

>>> '{0:.2f}'.format(1.25556)
'1.26'

这里,1.25556 被四舍五入到最接近的百分位,并打印为 1.26。如果你使用.2f并且你要打印的数字是整数,零会被加在末尾:

>>> '{0:.2f}'.format(1)
'1.00'

添加两个零是因为我们指定了要在小数点后打印出恰好两个数字。

单位转换

国际单位制定义了七个基本量。这些基本量用于推导其他量,称为导出量。长度(包括宽度、高度和深度)、时间、质量和温度是七个基本量中的四个。每个基本量都有一个标准的计量单位:米、秒、千克和开尔文,分别对应。

但这些标准计量单位也有多个非标准的计量单位。你更熟悉将温度报告为 30 摄氏度或 86 华氏度,而不是 303.15 开尔文。这是否意味着 303.15 开尔文比 86 华氏度热三倍?当然不是!我们不能仅通过数值比较 86 华氏度和 303.15 开尔文,因为它们使用了不同的计量单位,尽管它们衡量的是相同的物理量——温度。只有当物理量的两个测量值使用相同的计量单位时,才能进行比较。

不同计量单位之间的转换可能比较复杂,这也是为什么在高中时你经常被要求解决涉及单位转换的问题。这是测试你基本数学技能的好方法。但 Python 也有很多数学技能,而且与一些高中生不同,它不会在循环中反复进行数值计算时感到疲倦!接下来,我们将探索编写程序来为你执行这些单位转换。

我们从长度开始。在美国和英国,英寸和英里通常用于衡量长度,而大多数其他国家使用厘米和公里。

一英寸等于 2.54 厘米,你可以使用乘法运算将英寸单位转换为厘米单位。然后,你可以将厘米单位的测量值除以 100 以得到米单位的测量值。例如,下面是如何将 25.5 英寸转换为米:

>>> (25.5 * 2.54) / 100
0.6476999999999999

另一方面,一英里大约等于 1.609 公里。所以如果你看到目的地距离 650 英里,你实际上是 650 × 1.609 公里远:

>>> 650 * 1.609
1045.85

现在,让我们看看温度转换——将温度从华氏度转换为摄氏度,反之亦然。用华氏度表示的温度可以使用公式转换为其等效的摄氏度值。

image

F 是华氏温度,C 是其等效的摄氏温度。你知道 98.6 华氏度是正常人体体温。要找出其对应的摄氏温度,我们在 Python 中计算上述公式:

>>> F = 98.6
>>> (F - 32) * (5 / 9)
37.0

首先,我们创建一个标签F,其值为华氏温度 98.6。接着,我们使用公式将此温度转换为其等效的摄氏度,结果是 37.0 摄氏度。

要将温度从摄氏度转换为华氏度,你需要使用以下公式:

image

你可以以类似的方式来评估这个公式:

>>> C = 37
>>> C * (9 / 5) + 32
98.60000000000001

我们创建一个标签C,其值为 37(正常人体体温,摄氏度)。然后,我们使用公式将其转换为华氏度,结果是 98.6 度。

一遍又一遍地编写这些转换公式真是麻烦。让我们写一个单位转换程序来帮助我们进行转换。这个程序会展示一个菜单,允许用户选择他们想要执行的转换,要求输入相关数据,然后打印计算结果。程序如下:

   '''
   Unit converter: Miles and Kilometers
   '''

   def print_menu():
       print('1. Kilometers to Miles')
       print('2. Miles to Kilometers')

   def km_miles():
       km = float(input('Enter distance in kilometers: '))
       miles = km / 1.609

       print('Distance in miles: {0}'.format(miles))

   def miles_km():
       miles = float(input('Enter distance in miles: '))
       km = miles * 1.609

       print('Distance in kilometers: {0}'.format(km))

   if __name__ == '__main__':
➊     print_menu()
➋     choice = input('Which conversion would you like to do?: ')
       if choice == '1':
           km_miles()

       if choice == '2':
           miles_km()

这是一个稍微长一点的程序,但不用担心,实际上很简单。我们从➊开始。调用print_menu()函数,它会打印一个包含两种单位转换选择的菜单。在➋,程序会要求用户选择其中一个转换。如果选择输入为 1(公里到英里),则调用km_miles()函数。如果选择输入为 2(英里到公里),则调用miles_km()函数。在这两个函数中,程序首先会要求用户输入所选择单位的距离(km_miles()使用公里,miles_km()使用英里)。然后,程序使用对应的公式进行转换,并显示结果。

下面是该程序的一个示例运行:

   1. Kilometers to Miles
   2. Miles to Kilometers
➊ Which conversion would you like to do?: 2
   Enter distance in miles: 100
   Distance in kilometers: 160.900000

用户在➊处被要求输入选择。用户选择了 2(英里转公里)。程序接着要求用户输入需要转换为公里的英里数,并打印转换结果。

这个程序仅仅是进行英里与公里之间的转换,但在以后的编程挑战中,你将扩展这个程序,使其能够进行其他单位的转换。

求解二次方程的根

当你有一个像 x + 500 – 79 = 10 这样的方程,并且需要找出未知变量 x 的值时,你需要重新排列方程,使得常数(500、-79 和 10)在方程的一边,变量 (x) 在另一边。这样就得到了以下方程: x = 10 – 500 + 79。

通过求解右边表达式的值,你将得到 x 的值,这个值就是该方程的解,也称为该方程的 。在 Python 中,你可以按如下方式进行计算:

>>> x = 10 - 500 + 79
>>> x
-411

这是一个 线性方程 的例子。一旦你将方程两边的项重新排列,该表达式就足够简单,能够进行求解。另一方面,对于如 x² + 2x + 1 = 0 这样的方程,求解 x 的根通常涉及到求解一个复杂的表达式,称为 二次公式。这样的方程被称为 二次方程,通常表示为 ax² + bx + c = 0,其中 abc 是常数。用于计算根的二次公式如下所示:

image

二次方程有两个根——即使这两个根的值有时可能相同,它们也表示 x 的两个值,使得二次方程的两边相等。这一点在二次公式中的 x[1] 和 x[2] 处有所体现。

将方程 x² + 2x + 1 = 0 与通用的二次方程进行比较,我们可以看到 a = 1,b = 2,c = 1。我们可以将这些值直接代入二次公式中来计算 x[1] 和 x[2] 的值。在 Python 中,我们首先将 abc 的值存储为标签 abc,并赋予相应的数值:

>>> a = 1
>>> b = 2
>>> c = 1

然后,考虑到两个公式中都有 b² – 4ac 这一项,我们将定义一个新的标签 D,使得 image

>>> D = (b**2 – 4*a*c)**0.5

如你所见,我们通过将 b² – 4ac 的平方根提升到 0.5 次方来进行计算。现在,我们可以写出求解 x[1] 和 x[2] 的表达式:

>>> x_1 = (-b + D)/(2*a)
>>> x_1
-1.0
>>> x_2 = (-b - D)/(2*a)
>>> x_2
-1.0

在这种情况下,两个根的值是相同的,如果你将这个值代入方程 x² + 2x + 1,方程的结果将为 0。

我们的下一个程序将这些步骤整合在一个名为 roots() 的函数中,该函数将 abc 的值作为参数,计算出根并打印出来:

'''
Quadratic equation root calculator
'''

def roots(a, b, c):

    D = (b*b - 4*a*c)**0.5
    x_1 = (-b + D)/(2*a)
    x_2 = (-b - D)/(2*a)

    print('x1: {0}'.format(x_1))
    print('x2: {0}'.format(x_2))

if __name__ == '__main__':
    a = input('Enter a: ')
    b = input('Enter b: ')
    c = input('Enter c: ')
    roots(float(a), float(b), float(c))

最初,我们使用标签 abc 来引用二次方程中三个常数的值。然后,我们调用 roots() 函数,将这三个值作为参数(在将它们转换为浮动点数之后)。该函数将 abc 代入二次公式,计算该方程的根,并打印出来。

当你执行程序时,它将要求用户输入对应二次方程的 abc 的值,用以求解该方程的根。

Enter a: 1
Enter b: 2
Enter c: 1

x1: -1.000000
x2: -1.000000

尝试解决一些具有不同常数值的二次方程,程序将正确地找到根。

你很可能知道,二次方程也可以有复数根。例如,方程 x² + x + 1 = 0 的根就是复数。上述程序也可以找到这些根。让我们再次运行程序试试看(常数为 a = 1,b = 1,c = 1):

Enter a: 1
Enter b: 1
Enter c: 1
x1: (-0.49999999999999994+0.8660254037844386j)
x2: (-0.5-0.8660254037844386j)

上面打印出的根是复数(由j表示),程序能够正确计算和显示它们。

你学到了什么

恭喜你完成了第一章!你学会了编写识别整数、浮点数、分数(可以用分数或浮点数表示)和复数的程序。你编写了生成乘法表、执行单位转换和求解二次方程根的程序。我相信你已经很激动,因为你已经迈出了编写能为你做数学计算的程序的第一步。在我们继续之前,以下是一些编程挑战,给你提供了一个进一步应用所学知识的机会。

编程挑战

这里有一些挑战,给你一个机会来练习这一章的概念。每个问题都有多种解法,你可以在www.nostarch.com/doingmathwithpython/找到示例解答。

#1: 偶数-奇数自动售货机

试着编写一个“偶数-奇数自动售货机”,它将接受一个数字作为输入,并做两件事:

1. 打印数字是偶数还是奇数。

2. 显示数字后跟着下一个 9 个偶数或奇数。

如果输入是2,程序应该打印even,然后打印2, 4, 6, 8, 10, 12, 14, 16, 18, 20。同样,如果输入是1,程序应该打印odd,然后打印1, 3, 5, 7, 9, 11, 13, 15, 17, 19

你的程序应该使用is_integer()方法,如果输入的是小数点后有显著数字的数字,则显示错误消息。

#2: 增强型乘法表生成器

我们的乘法表生成器很酷,但它只打印前 10 个倍数。增强该生成器,使用户可以指定数字和最多显示到哪个倍数。例如,我应该能够输入我想查看列出 9 的前 15 个倍数的表格。

#3: 增强型单位转换器

我们在这一章写的单位转换程序仅限于公里与英里之间的转换。试着扩展该程序,使其支持质量单位(如千克和磅)和温度单位(如摄氏度和华氏度)之间的转换。

#4: 分数计算器

编写一个可以对两个分数进行基本数学运算的计算器。它应该询问用户两个分数以及用户想要执行的操作。作为起点,这里是只实现加法操作的程序示例:

   '''
   Fraction operations
   '''
   from fractions import Fraction

   def add(a, b):
       print('Result of Addition: {0}'.format(a+b))

   if __name__ == '__main__':
➊     a = Fraction(input('Enter first fraction: '))
➋     b = Fraction(input('Enter second fraction: '))
       op = input('Operation to perform - Add, Subtract, Divide, Multiply: ')
       if op == 'Add':
           add(a,b)

你已经看到了该程序中的大部分元素。在➊和➋,我们让用户输入两个分数。然后,我们询问用户对这两个分数要进行什么操作。如果用户输入'Add'作为操作,程序会调用我们定义的add()函数,用于计算传入的两个分数的和。add()函数执行操作并打印结果。例如:

Enter first fraction: 3/4
Enter second fraction: 1/4
Operation to perform - Add, Subtract, Divide, Multiply: Add
Result of Addition: 1

尝试添加对其他操作的支持,比如减法、除法和乘法。例如,下面是你的程序应该如何计算两个分数之差:

Enter first fraction: 3/4
Enter second fraction: 1/4
Operation to perform - Add, Subtract, Divide, Multiply: Subtract
Result of Subtraction: 2/4

在除法的情况下,你应该让用户知道是第一个分数除以第二个分数,还是相反。

#5:赋予用户退出的权力

到目前为止,我们写的所有程序只适用于一次输入和输出。例如,考虑打印乘法表的程序:用户执行程序并输入一个数字;然后程序打印乘法表并退出。如果用户想要打印另一个数字的乘法表,就必须重新运行程序。

如果用户可以选择是否退出或继续使用程序,那会更方便。编写这类程序的关键是设置一个无限循环,即一个除非明确要求退出,否则不会结束的循环。下面,你可以看到这种程序布局的示例:

   '''
   Run until exit layout
   '''

   def fun():
       print('I am in an endless loop')

   if __name__ == '__main__':
➊     while True:
           fun()
➋         answer = input('Do you want to exit? (y) for yes ')
           if answer == 'y':
               break

我们在➊使用while True定义了一个无限循环。while循环会持续执行,除非条件评估为False。因为我们选择了循环条件为常量值True,所以它会一直运行,除非我们以某种方式中断它。在循环内部,我们调用fun()函数,它打印字符串I am in an endless loop。在➋,会询问用户“你想退出吗?”如果用户输入y,程序就会通过break语句退出循环(break语句会退出最内层循环,不执行该循环中的任何其他语句)。如果用户输入其他内容(或者什么也不输入,只按 ENTER 键),while循环将继续执行——也就是说,它会再次打印字符串,并且会继续这样做,直到用户希望退出。以下是程序的示例运行:

I am in an endless loop
Do you want to exit? (y) for yes n
I am in an endless loop
Do you want to exit? (y) for yes n
I am in an endless loop
Do you want to exit? (y) for yes n
I am in an endless loop
Do you want to exit? (y) for yes y

基于这个例子,让我们重新编写乘法表生成器,使其在用户希望退出之前一直运行。程序的新版本如下所示:

'''
Multiplication table printer with
exit power to the user
'''

def multi_table(a):

    for i in range(1, 11):
        print('{0} x {1} = {2}'.format(a, i, a*i))

if __name__ == '__main__':

    while True:
        a = input('Enter a number: ')
        multi_table(float(a))

        answer = input('Do you want to exit? (y) for yes ')
        if answer == 'y':
            break

如果你将这个程序与我们之前写的程序进行对比,你会发现唯一的变化就是添加了while循环,该循环包含了提示用户输入数字的部分,以及调用multi_table()函数的部分。

当你运行程序时,程序会像以前一样要求输入一个数字并打印它的乘法表。然而,它还会接着询问用户是否希望退出程序。如果用户不想退出,程序将准备好打印另一个数字的乘法表。以下是一个示例运行:

Enter a number: 2
2.000000 x 1.000000 = 2.000000
2.000000 x 2.000000 = 4.000000
2.000000 x 3.000000 = 6.000000
2.000000 x 4.000000 = 8.000000

2.000000 x 5.000000 = 10.000000
2.000000 x 6.000000 = 12.000000
2.000000 x 7.000000 = 14.000000
2.000000 x 8.000000 = 16.000000
2.000000 x 9.000000 = 18.000000
2.000000 x 10.000000 = 20.000000
Do you want to exit? (y) for yes n
Enter a number:

尝试重写本章中的一些其他程序,使它们在用户要求退出之前持续执行。

第二章:通过图形可视化数据

image

本章将向你介绍一种强大的数值数据呈现方式:通过 Python 绘制图形。我们将从数轴和笛卡尔平面开始讨论。接下来,我们将学习强大的绘图库 matplotlib 以及如何使用它创建图形。然后,我们将探索如何制作能够清晰、直观地呈现数据的图形。最后,我们将使用图形探索牛顿的万有引力定律和抛体运动。让我们开始吧!

理解笛卡尔坐标平面

考虑一个数轴,如图 2-1 所示。数轴上标记了从 -3 到 3 的整数,但在这两个数字之间(例如,1 和 2)存在所有可能的数字:1.1、1.2、1.3 等等。

image

图 2-1:数轴

数轴使得某些属性变得直观。例如,0 右侧的所有数字是正数,左侧的则是负数。当一个数字 a 位于另一个数字 b 右侧时,a 总是大于 b,而 b 总是小于 a

数轴两端的箭头表示该数轴无限延伸,数轴上的每一个点都对应某个实数,无论这个数字多大。一个数字足以描述数轴上的一个点。

现在考虑两个数轴,如图 2-2 所示。两个数轴相交于直角,并且在各自的 0 点交叉。这形成了一个笛卡尔坐标平面,或称 x-y 平面,其中水平数轴称为 x 轴,垂直数轴称为 y 轴。

image

图 2-2:笛卡尔坐标平面

与数轴一样,我们可以在平面上有无数多个点。我们用一对数字来描述一个点,而不是一个数字。例如,我们用两个数字 xy 来描述图中的点 A,通常写作 (x, y),这对数字被称为该点的 坐标。如图 2-2 所示,x 是该点沿 x 轴从原点起的距离,y 是该点沿 y 轴的距离。两个坐标轴交叉的点称为 原点,其坐标为 (0, 0)。

笛卡尔坐标平面使我们能够可视化两组数字之间的关系。在这里,我用集合这个词泛指一组数字。(我们将在第五章学习数学集合以及如何在 Python 中使用它们。)无论这两组数字代表什么——温度、棒球得分,还是班级测试分数——你所需要的只是这些数字本身。然后,你可以将它们绘制出来——无论是在图纸上,还是通过用 Python 编写的程序在计算机上绘制。接下来,在本书中,我将用绘制这个动词来描述绘制两组数字的行为,而用图形来描述结果——一条线、一条曲线,或者仅仅是笛卡尔平面上的一组点。

处理列表和元组

在使用 Python 绘制图形时,我们将处理列表元组。在 Python 中,这两者是存储一组值的两种不同方式。元组和列表在大多数情况下非常相似,主要的区别在于:创建列表后,你可以向其中添加值,并且可以改变值的顺序。另一方面,元组中的值一旦创建就固定了,不能更改。我们将使用列表来存储我们想要绘制的点的xy坐标。元组将在《自定义图形》(第 41 页)中出现,届时我们将学习如何自定义图形的范围。首先,让我们回顾一下列表的一些特性。

你可以通过在方括号中输入用逗号分隔的值来创建一个列表。以下语句创建了一个列表,并使用标签simplelist来引用它:

>>> simplelist = [1, 2, 3]

现在你可以通过标签和列表中数字的位置(即索引)来引用单个数字——1、2 和 3。因此,simplelist[0]指的是第一个数字,simplelist[1]指的是第二个数字,simplelist[2]指的是第三个数字:

>>> simplelist[0]
1
>>> simplelist[1]
2
>>> simplelist[2]
3

请注意,列表的第一个项目位于索引 0,第二个项目位于索引 1,依此类推——也就是说,列表中的位置从 0 开始计数,而不是从 1 开始。

列表也可以存储字符串:

>>> stringlist = ['a string','b string','c string']
>>> stringlist[0]
'a string'
>>> stringlist[1]
'b string'
>>> stringlist[2]
'c string'

创建列表的一个优点是,你不必为每个值创建单独的标签;你只需为列表创建一个标签,并使用索引位置来引用每个项目。此外,当你需要存储新值时,可以随时向列表添加内容,因此如果你事先不知道需要存储多少数字或字符串,列表是存储数据的最佳选择。

一个空列表就是一个没有任何项目或元素的列表,可以像这样创建:

>>> emptylist = []

空列表主要在你事先不知道列表中会有哪些项目,但计划在程序执行过程中填充值时非常有用。在这种情况下,你可以创建一个空列表,然后使用append()方法稍后添加项目:

➊ >>> emptylist
   []
➋ >>> emptylist.append(1)
   >>> emptylist
   [1]
➌ >>> emptylist.append(2)
   >>> emptylist
➍ [1, 2]

在 ➊ 处,emptylist 初始为空。接下来,在 ➋ 处我们将数字 1 添加到列表中,然后在 ➌ 处添加 2。到 ➍ 这一行时,列表已经是 [1, 2]。注意,当你使用 .append() 时,值会被添加到列表的末尾。这只是一种向列表添加值的方式。还有其他方法,但在这一章中我们不需要它们。

创建元组与创建列表类似,不过是使用圆括号而不是方括号:

>>> simpletuple = (1, 2, 3)

你可以像对待列表一样,通过对应的索引来引用 simpletuple 中的单个数字:

>>> simpletuple[0]
1

>>> simpletuple[1]
2
>>> simpletuple[2]
3

你也可以对列表和元组使用 负索引。例如,simplelist[-1]simpletuple[-1] 将引用列表或元组中的最后一个元素,simplelist[-2]simpletuple[-2] 将引用倒数第二个元素,依此类推。

元组与列表一样,也可以包含字符串作为值,你可以创建一个没有元素的 空元组,表示为 emptytuple=()。然而,元组没有 append() 方法来向现有元组添加新值,因此你不能向空元组中添加值。一旦创建了元组,元组的内容就无法更改。

迭代遍历列表或元组

我们可以使用 for 循环遍历列表或元组,如下所示:

>>> l = [1, 2, 3]
>>> for item in l:
        print(item)

这将打印列表中的元素:

1
2
3

元组中的元素可以以相同的方式进行检索。

有时你可能需要知道列表或元组中某个元素的位置或索引。你可以使用 enumerate() 函数遍历列表中的所有元素,并返回元素的索引以及元素本身。我们用 indexitem 标签来引用它们:

>>> l = [1, 2, 3]
>>> for index, item in enumerate(l):
        print(index, item)

这将产生如下输出:

0 1
1 2
2 3

这对元组也适用。

使用 Matplotlib 创建图表

我们将使用 matplotlib 来绘制 Python 图表。Matplotlib 是一个 Python ,意味着它是一个包含相关功能模块的集合。在这个案例中,这些模块用于绘制数字和生成图表。Matplotlib 并不包含在 Python 的标准库中,因此你需要安装它。安装说明请参见 附录 A。安装完成后,启动 Python shell。正如安装说明中所述,你可以继续使用 IDLE shell,或者使用 Python 自带的 shell。

现在我们准备好创建我们的第一个图表。我们将从一个简单的图表开始,只有三个点:(1, 2)、(2, 4) 和 (3, 6)。为了创建这个图表,我们首先将创建两个数字列表,一个存储这些点的 x 坐标,另一个存储 y 坐标。下面的两个语句正是做了这件事,创建了两个列表 x_numbersy_numbers

>>> x_numbers = [1, 2, 3]
>>> y_numbers = [2, 4, 6]

从这里开始,我们可以创建图表:

>>> from pylab import plot, show
>>> plot(x_numbers, y_numbers)
[<matplotlib.lines.Line2D object at 0x7f83ac60df10>]

在第一行中,我们从 pylab 模块导入了 plot()show() 函数,这个模块是 matplotlib 包的一部分。接下来,我们在第二行调用了 plot() 函数。plot() 函数的第一个参数是我们想要在 x 轴上绘制的数字列表,第二个参数是我们想要在 y 轴上绘制的对应数字列表。plot() 函数返回一个对象——更准确地说,是一个包含对象的列表。这个对象包含了我们请求 Python 创建的图形信息。在这个阶段,你可以向图形添加更多信息,比如标题,或者你也可以直接显示图形。现在我们先只显示图形。

plot() 函数仅创建图形。为了实际显示图形,我们还需要调用 show() 函数:

>>> show()

你应该能够在 matplotlib 窗口中看到图形,如 图 2-3 所示。(显示窗口可能会因操作系统的不同而有所不同,但图形应该是一样的。)

image

图 2-3:一条经过点 (1, 2)、(2, 4) 和 (3, 6) 的线图

请注意,x 轴不是从原点 (0, 0) 开始,而是从数字 1 开始,y 轴从数字 2 开始。这是两个列表中的最小值。同时,你还可以看到每个坐标轴上标有增量(例如 y 轴上有 2.5、3.0、3.5 等)。在 “自定义图形” 章节的 第 41 页,我们将学习如何控制这些图形的各个方面,包括如何添加坐标轴标签和图形标题。

你会注意到,在交互式命令行中,直到你关闭 matplotlib 窗口之前,你无法输入更多的语句。关闭图形窗口后,你就可以继续编程了。

在图形上标记点

如果你希望图形标记出你提供的绘制点,可以在调用 plot() 函数时使用额外的关键字参数:

>>> plot(x_numbers, y_numbers, marker='o')

通过输入 marker='o',我们告诉 Python 用一个看起来像 o 的小圆点来标记列表中的每个点。再次输入 show() 后,你会看到每个点都被一个圆点标记(参见 图 2-4)。

image

图 2-4:一条经过点 (1, 2)、(2, 4) 和 (3, 6) 的线图,并且各点由小圆点标记

位于 (2, 4) 的标记非常明显,而其他标记则隐藏在图形的角落里。你可以从多个 marker 选项中进行选择,包括 'o''*''x''+'。使用 marker= 会在各点之间连接一条线(这是默认设置)。你还可以绘制仅标记指定点的图形,不连接它们的线,只需省略 marker= 参数即可:

>>> plot(x_numbers, y_numbers, 'o')
[<matplotlib.lines.Line2D object at 0x7f2549bc0bd0>]

这里,'o' 表示每个点应当用一个点标记,但点与点之间不应有连接线。调用 show() 函数以显示图表,图表应该和 图 2-5 中显示的一样。

image

图 2-5:显示点 (1, 2)、(2, 4) 和 (3, 6) 的图表

如你所见,现在图表上只显示了点,没有连接这些点的线条。和之前的图表一样,第一个和最后一个点几乎不可见,但我们很快就会看到如何改变这一点。

绘制纽约市的平均年温度

让我们看一下一个稍大的数据集,以便探索 matplotlib 的更多功能。2000 年到 2012 年间,纽约市——具体来说是中央公园——的年平均气温如下:53.9、56.3、56.4、53.4、54.5、55.8、56.8、55.0、55.3、54.0、56.7、56.4 和 57.3 华氏度。现在,这看起来像是随机的数字杂乱无章,但我们可以将这些温度数据绘制在图表上,以便更清晰地展示每年温度的升降:

>>> nyc_temp = [53.9, 56.3, 56.4, 53.4, 54.5, 55.8, 56.8, 55.0, 55.3, 54.0, 56.7, 56.4, 57.3]
>>> plot(nyc_temp, marker='o')
[<matplotlib.lines.Line2D object at 0x7f2549d52f90>]

我们将平均温度存储在一个列表 nyc_temp 中。然后,我们调用 plot() 函数,只传入这个列表(以及标记字符串)。当你在单个列表上使用 plot() 时,这些数字会自动绘制在 y 轴上。x 轴上对应的值是每个值在列表中的位置。也就是说,第一个温度值 53.9 对应的 x 轴值是 0,因为它在列表的位置是 0(记住,列表的位置是从 0 开始计数的,不是从 1 开始)。因此,绘制在 x 轴上的数字是从 0 到 12 的整数,我们可以认为它们对应于我们拥有温度数据的 13 年。

输入 show() 来显示图表,图表显示在 图 2-6 中。图表显示了平均温度年年波动。如果你看看我们绘制的数字,它们实际上并不相距太远。然而,图表使得这些变化看起来相当剧烈。那么,发生了什么呢?原因在于 matplotlib 会选择 y 轴的范围,使其恰好足够包含提供的绘图数据。所以在这张图表中,y 轴从 53.0 开始,最高值为 57.5。这使得即使是小的差异也显得被放大了,因为 y 轴的范围非常小。我们将在 “自定义图表” 中学习如何控制每个轴的范围,第 41 页。

image

图 2-6:显示纽约市 2000-2012 年间的年平均气温的图表

你也可以看到,y 轴上的数字是浮点数(因为我们要求绘制的是浮动的数字),而 x 轴上的数字是整数。Matplotlib 可以处理这两者。

不显示对应年份的温度变化图是快速且简便的方式来观察不同年份之间的差异。然而,如果你打算向别人展示这张图表,你可能会希望通过标明每个温度对应的年份来使图表更加清晰。我们可以通过创建一个包含年份的列表,并调用 plot() 函数来轻松实现这一点:

>>> nyc_temp = [53.9, 56.3, 56.4, 53.4, 54.5, 55.8, 56.8, 55.0, 55.3, 54.0, 56.7, 56.4, 57.3]
>>> years = range(2000, 2013)
>>> plot(years, nyc_temp, marker='o')
[<matplotlib.lines.Line2D object at 0x7f2549a616d0>]
>>> show()

我们使用在第一章中学到的 range() 函数来指定从 2000 年到 2012 年的年份。现在你会看到这些年份显示在 x 轴上(参见图 2-7)。

image

图 2-7:显示纽约市的平均年度温度,年份显示在 x 轴上

比较纽约市的月度温度趋势

继续观察纽约市的情况,我们来看看平均月温度在这些年份中的变化。这将帮助我们了解如何在一张图上绘制多条线。我们选择 2000 年、2006 年和 2012 年这三年,分别绘制这三年的 12 个月的平均温度。

首先,我们需要创建三个列表来存储温度(单位为华氏度)。每个列表将包含 12 个数字,分别对应每年从一月到十二月的平均温度:

>>> nyc_temp_2000 = [31.3, 37.3, 47.2, 51.0, 63.5, 71.3, 72.3, 72.7, 66.0, 57.0, 45.3, 31.1]
>>> nyc_temp_2006 = [40.9, 35.7, 43.1, 55.7, 63.1, 71.0, 77.9, 75.8, 66.6, 56.2, 51.9, 43.6]
>>> nyc_temp_2012 = [37.3, 40.9, 50.9, 54.8, 65.1, 71.0, 78.8, 76.7, 68.8, 58.0, 43.9, 41.5]

第一个列表对应于 2000 年,接下来的两个列表分别对应 2006 年和 2012 年。我们可以将这三组数据绘制在三个不同的图表上,但那样就不容易比较每一年之间的差异了。试试看吧!

比较这些温度的最清晰方式是将这三组数据绘制在 同一 图表上,就像这样:

>>> months = range(1, 13)
>>> plot(months, nyc_temp_2000, months, nyc_temp_2006, months, nyc_temp_2012)
[<matplotlib.lines.Line2D object at 0x7f2549c1f0d0>, <matplotlib.lines.Line2D
object at 0x7f2549a61150>, <matplotlib.lines.Line2D object at 0x7f2549c1b550>]

首先,我们创建一个列表(months),使用 range() 函数将数字 1、2、3 等依次存储到 12。接着,我们用三对列表调用 plot() 函数。每对列表包含一个表示月份的 x 轴列表和一个表示每年(2000、2006 和 2012 年)平均月温度的 y 轴列表。到目前为止,我们每次只使用 plot() 函数绘制一对列表,但实际上你可以将多个列表对传递给 plot() 函数。每对列表之间用逗号分隔,plot() 函数会自动为每对列表绘制不同的线条。

plot() 函数返回的是三个对象的列表,而不是一个。Matplotlib 将这三条曲线视为相互独立的,当你调用 show() 时,它会知道将它们绘制在同一张图上。我们可以调用 show() 来显示图表,如图 2-8 所示。

image

图 2-8:显示纽约市在 2000、2006 和 2012 年的平均月温度的图表

现在,我们将三条图线绘制在同一张图表上。Python 会自动为每条线选择不同的颜色,以表明这些线条来源于不同的数据集。

我们可以选择分别调用三次plot函数,而不是一次性将所有三个数据对传入,分别为每个数据对调用一次:

>>> plot(months, nyc_temp_2000)
[<matplotlib.lines.Line2D object at 0x7f1e51351810>]
>>> plot(months, nyc_temp_2006)
[<matplotlib.lines.Line2D object at 0x7f1e5ae8e390>]
>>> plot(months, nyc_temp_2012)
[<matplotlib.lines.Line2D object at 0x7f1e5136ccd0>]
>>> show()

Matplotlib 会记录哪些图形尚未显示。因此,只要在调用plot()三次之后再调用show(),这些图形都会在同一张图表上显示。

然而,我们遇到了一个问题,因为我们不知道哪种颜色代表哪一年。为了解决这个问题,我们可以使用legend()函数来为图表添加图例。图例是一个小的显示框,用来标识图表中不同部分的含义。在这里,我们将使用图例来标明每条彩色线条对应的年份。要添加图例,首先像之前一样调用plot()函数:

>>> plot(months, nyc_temp_2000, months, nyc_temp_2006, months, nyc_temp_2012)
[<matplotlib.lines.Line2D object at 0x7f2549d6c410>, <matplotlib.lines.Line2D
object at 0x7f2549d6c9d0>, <matplotlib.lines.Line2D object at 0x7f2549a86850>]

然后,从pylab模块导入legend()函数,并按如下方式调用:

>>> from pylab import legend
>>> legend([2000, 2006, 2012])
<matplotlib.legend.Legend object at 0x7f2549d79410>

我们通过legend()函数传入一个标签列表,用来标识图表中每一条线的含义。这些标签的顺序与plot()函数中输入的列表数据对顺序相匹配。也就是说,2000将作为我们在plot()函数中输入的第一对数据的标签;2006作为第二对数据的标签;2012作为第三对数据的标签。你还可以为该函数指定第二个参数,来设置图例的位置。默认情况下,图例总是位于图表的右上角。但是,你可以指定一个特定的位置,比如'lower center''center left''upper left'。或者,你可以将位置设置为'best',这样图例将自动放置在不会干扰图表的地方。

最后,我们调用show()来显示图表:

>>> show()

正如你在图表中看到的(参见图 2-9),现在右上角有了一个图例框。它告诉我们哪条线表示 2000 年的月平均气温,哪条线表示 2006 年,哪条线表示 2012 年。

从图表中可以得出两个有趣的结论:三个年份的最高气温都出现在 7 月左右(对应于X轴的 7),而且从 2000 年到 2012 年之间,气温逐年升高,尤其是 2000 年到 2006 年之间的升幅最为显著。将三条线绘制在同一个图表中,能够更容易地看出这些关系。这比单纯查看一长串的数字或是将三条线分别绘制在三个不同图表中要清晰得多。

image

图 2-9:展示纽约市平均月气温的图表,并有图例标明每条颜色对应的年份

自定义图形

我们已经了解了一种自定义图形的方法——通过添加图例。接下来,我们将学习更多自定义图形的方法,通过给X轴和Y轴添加标签、给图表添加标题,并控制坐标轴的范围和步长,来使图表更加清晰。

添加标题和标签

我们可以使用title()函数为图表添加标题,并使用xlabel()ylabel()函数分别为* x 轴和 y *轴添加标签。让我们重新创建上一个图表,并添加所有这些附加信息:

>>> from pylab import plot, show, title, xlabel, ylabel, legend
>>> plot(months, nyc_temp_2000, months, nyc_temp_2006, months, nyc_temp_2012)
[<matplotlib.lines.Line2D object at 0x7f2549a9e210>, <matplotlib.lines.Line2D
object at 0x7f2549a4be90>, <matplotlib.lines.Line2D object at 0x7f2549a82090>]
>>> title('Average monthly temperature in NYC')
<matplotlib.text.Text object at 0x7f25499f7150>
>>> xlabel('Month')
<matplotlib.text.Text object at 0x7f2549d79210>
>>> ylabel('Temperature')
<matplotlib.text.Text object at 0x7f2549b8b2d0>

>>> legend([2000, 2006, 2012])
<matplotlib.legend.Legend object at 0x7f2549a82910>

所有三个函数——title()xlabel()ylabel()——都通过字符串传入我们希望显示在图表上的对应文本。调用show()函数将显示带有所有新增信息的图表(参见图 2-10)。

image

图 2-10:图表中已添加坐标轴标签和标题。

添加了这三项新信息后,图表变得更容易理解。

自定义坐标轴

到目前为止,我们让 Python 根据传递给plot()函数的数据自动确定了两个坐标轴上的数字范围。对于大多数情况,这样做是可以的,但有时这种自动范围的方式并不是最清晰的数据呈现方式,就像我们在绘制纽约市年平均温度的图表时所看到的那样(参见图 2-7)。在那里,即使温度的变化很小,由于自动选择的* y *轴范围非常狭窄,看起来变化也很大。我们可以通过axis()函数来调整坐标轴的范围。这个函数既可以用来获取当前的范围,也可以用来设置坐标轴的新范围。

再次考虑 2000 到 2012 年间纽约市的年平均温度,并像之前一样创建图表。

>>> nyc_temp = [53.9, 56.3, 56.4, 53.4, 54.5, 55.8, 56.8, 55.0, 55.3, 54.0, 56.7, 56.4, 57.3]
>>> plot(nyc_temp, marker='o')
[<matplotlib.lines.Line2D object at 0x7f3ae5b767d0>]

现在,导入axis()函数并调用它:

>>> from pylab import axis
>>> axis()
(0.0, 12.0, 53.0, 57.5)

函数返回了一个包含四个数字的元组,这些数字对应于* x 轴的范围(0.0, 12.0)和 y 轴的范围(53.0, 57.5)。这些值与我们之前绘制的图形中的范围值相同。现在,让我们将 y *轴的起始值从 53.0 改为 0:

>>> axis(ymin=0)
(0.0, 12.0, 0, 57.5)

调用axis()函数并设置新的* y 轴起始值(由ymin=0指定)会改变范围,返回的元组也会确认这一点。如果通过调用show()函数显示图表, y *轴将从 0 开始,连续年份之间的差异看起来就不那么剧烈了(参见图 2-11)。

image

图 2-11:显示 2000 到 2012 年间纽约市年平均温度的图表。 y 轴已被自定义为从 0 开始。

类似地,你可以使用xminxmaxymax分别设置* x 轴的最小值和最大值,以及 y 轴的最大值。如果你要更改所有四个值,可能会觉得调用axis()函数,并将所有四个范围值作为列表传入(例如axis([0, 10, 0, 20]))更为方便。这样将会把 x 轴的范围设置为(0, 10),而 y *轴的范围设置为(0, 20)。

使用 pyplot 绘图

pylab模块适用于在交互式 shell 中创建图表,例如我们一直在使用的 IDLE shell。然而,在 IDLE shell 之外使用 matplotlib 时——例如作为更大程序的一部分——pyplot模块更加高效。别担心——你在使用pylab时学到的所有方法,在使用pyplot时也会以相同的方式工作。

以下程序使用pyplot模块重新创建了本章中的第一个图表:

   '''
   Simple plot using pyplot
   '''

➊ import matplotlib.pyplot

➋ def create_graph():
       x_numbers = [1, 2, 3]
       y_numbers = [2, 4, 6]

       matplotlib.pyplot.plot(x_numbers, y_numbers)
       matplotlib.pyplot.show()

   if __name__ == '__main__':
       create_graph()

首先,我们通过语句import matplotlib.pyplot导入pyplot模块 ➊。这意味着我们正在从 matplotlib 包中导入整个pyplot模块。为了引用该模块中定义的任何函数或类定义,你必须使用语法matplotlib.pyplot.item,其中item是你要使用的函数或类。

这与我们之前按需导入单个函数或类有所不同。例如,在第一章中,我们通过from fractions import Fraction导入了Fraction类。导入整个模块在你打算使用该模块中的多个函数时非常有用。你可以一次性导入整个模块,然后在需要时引用不同的函数,而不必单独导入它们。

create_graph()函数中(见➋),我们创建了要绘制在图表上的两个数字列表,然后将这两个列表传递给plot()函数,方式与我们之前使用pylab时相同。然而,这一次,我们调用函数为matplotlib.pyplot.plot(),这意味着我们正在调用在 matplotlib 包的pyplot模块中定义的plot()函数。然后,我们调用show()函数来显示图表。与之前绘制数字的方式相比,唯一的不同之处是调用函数的机制。

为了减少输入量,我们可以通过输入import matplotlib.pyplot as plt来导入pyplot模块。然后,在程序中,我们可以使用标签plt来引用pyplot,而不必每次都输入matplotlib.pyplot

'''
Simple plot using pyplot
'''
import matplotlib.pyplot as plt

def create_graph():
    x_numbers = [1, 2, 3]
    y_numbers = [2, 4, 6]
    plt.plot(x_numbers, y_numbers)
    plt.show()

if __name__ == '__main__':
    create_graph()

现在,我们可以通过在函数前添加简写plt来调用这些函数,而不是使用matplotlib.pyplot

接下来,在本章和本书的其余部分,我们将使用pylab在交互式 shell 中,而在其他情况下使用pyplot

保存图表

如果你需要保存你的图表,可以使用savefig()函数。此函数将图表保存为图像文件,你可以在报告或演示文稿中使用。你可以选择多种图像格式,包括 PNG、PDF 和 SVG。

这是一个示例:

>>> from pylab import plot, savefig
>>> x = [1, 2, 3]
>>> y = [2, 4, 6]
>>> plot(x, y)
>>> savefig('mygraph.png')

本程序将把图形保存为图像文件 mygraph.png,并保存在当前目录中。在 Microsoft Windows 上,当前目录通常是 C:\Python33(即你安装 Python 的目录)。在 Linux 上,当前目录通常是你的主目录 (/home/),其中 是你登录的用户名。在 Mac 上,IDLE 默认将文件保存到 ~/Documents。如果你想将其保存到不同的目录中,可以指定完整的路径。例如,要将图像保存在 Windows 的 *C:* 目录下并命名为 mygraph.png,你可以按照如下方式调用 savefig() 函数:

>>> savefig('C:\mygraph.png')

如果你在图像查看程序中打开该图像,你会看到与调用 show() 函数时相同的图形。(你会注意到,图像文件只包含图形——而不是包含 show() 函数弹出窗口的整个内容)。要指定不同的图像格式,只需使用适当的扩展名命名文件。例如,mygraph.svg 会创建一个 SVG 图像文件。

另一种保存图形的方法是使用弹出窗口中的“保存”按钮,该窗口在调用 show() 时会显示。

使用公式绘图

到目前为止,我们一直在根据观测到的科学数据在图表上绘制点。在这些图表中,我们已经预先拥有了 xy 的所有数值。例如,在我们想要创建纽约市的温度变化图时,记录的温度和日期已经可以使用了。在本节中,我们将基于数学公式创建图表。

牛顿万有引力定律

根据牛顿的万有引力定律,一个质量为 m[1] 的物体会根据公式与另一个质量为 m[2] 的物体相互吸引,产生的力为 F

image

其中 r 是两个物体之间的距离,G 是引力常数。我们想观察随着两个物体之间的距离增加,引力如何变化。

假设有两个物体的质量:第一个物体的质量(m[1])为 0.5 kg,第二个物体的质量(m[2])为 1.5 kg。引力常数的值为 6.674 × 10^(–11) N m² kg^(–2)。现在我们准备计算这两个物体在 19 个不同距离下的引力:100 m、150 m、200 m、250 m、300 m,一直到 1000 m。以下程序执行这些计算,并绘制图形:

   '''
   The relationship between gravitational force and
   distance between two bodies
   '''

   import matplotlib.pyplot as plt

   # Draw the graph
   def draw_graph(x, y):
       plt.plot(x, y, marker='o')
       plt.xlabel('Distance in meters')

       plt.ylabel('Gravitational force in newtons')
       plt.title('Gravitational force and distance')
       plt.show()

   def generate_F_r():
       # Generate values for r
➊     r = range(100, 1001, 50)
       # Empty list to store the calculated values of F
       F = []

       # Constant, G
       G = 6.674*(10**-11)
       # Two masses
       m1 = 0.5
       m2 = 1.5

       # Calculate force and add it to the list, F
➋     for dist in r:
           force = G*(m1*m2)/(dist**2)
           F.append(force)

       # Call the draw_graph function
➌     draw_graph(r, F)

   if __name__=='__main__':
       generate_F_r()

generate_F_r()函数在上面的程序中完成了大部分工作。在➊处,我们使用range()函数创建一个名为r的列表,列出不同距离的数值,步长为 50\。最终值设置为 1001,因为我们希望包含 1000。然后我们创建一个空列表(F),在其中存储这些距离对应的重力值。接下来,我们创建了表示重力常数(G)和两个质量(m1m2)的标签。然后使用for循环 ➋,对距离列表(r)中的每一个值计算重力。在计算过程中,我们使用force作为标签来表示计算出的力,并将其添加到列表(F)中。最后,在➌处,我们调用draw_graph()函数,传入距离列表和计算出的力列表。图表的x轴表示力,y轴表示距离。该图在图 2-12 中展示。

随着距离(r)的增加,重力逐渐减小。根据这种关系,我们可以说重力与两个物体之间的距离成反比。另外,请注意,当两个变量中的一个发生变化时,另一个变量的变化不一定是成比例的。这种关系被称为非线性关系。因此,我们在图表上得到的是一条曲线,而不是直线。

image

图 2-12:重力与平方距离之间关系的可视化

抛体运动

现在,让我们绘制一个你日常生活中熟悉的图形。如果你把球扔过一个田野,它会沿着类似于图 2-13 中所示的轨迹运动。

image

图 2-13:从点 A以一定角度(θ)和初速度(U)投掷的球体运动,最终在点 B落地

在图中,球体从点A被投掷,最终落在点B。这种运动称为抛体运动。我们的目标是利用抛体运动的方程,绘制一个物体的轨迹图,展示从球体投掷的起点到它再次落地的整个过程。

当你投掷球体时,它具有一个初速度,并且该速度的方向与地面之间形成一定的角度。我们将初速度称为u,与地面形成的角度称为θ(theta),如图 2-13 所示。球体具有两个速度分量:一个沿着x方向,由u[x] = u cosθ计算,另一个沿着y方向,其中u[y] = u sinθ

随着球的运动,速度发生变化,我们将用v来表示这种变化的速度:水平分量是v[x],竖直分量是v[y]。为简化起见,假设水平分量(v[x])在物体运动过程中保持不变,而竖直分量(v[y])因重力作用而减小,按照方程v[y] = u[y] – gt来表示。在这个方程中,g是重力加速度,t是测量速度的时间。因为u[y] = u sinθ,我们可以代入得到

v[y] = u sinθgt

由于水平速度分量保持不变,水平位移(S[x])由公式S[x] = u(cosθ)t给出。不过,竖直分量的速度发生变化,竖直位移由下列公式给出:

image

换句话说,S[x]S[y] 给出了球在飞行过程中任意时刻的 xy 坐标。我们将在编写程序绘制轨迹时使用这些方程。使用这些方程时,时间(t)以秒为单位,速度以米每秒(m/s)为单位,投射角度(θ)以度为单位,重力加速度(g)以米每秒平方(m/s²)为单位。

然而,在编写程序之前,我们需要先计算出球体飞行多长时间才会撞击地面,以便知道程序何时停止绘制球的轨迹。为此,我们首先需要找出球体达到最高点所需的时间。当竖直速度分量(v[y])为 0 时,球体达到最高点,即v[y] = u sin θgt = 0。因此,我们要利用以下公式求解时间t

image

我们将这个时间称为t_peak。当球达到最高点后,它将在空中再停留t_peak秒,然后撞击地面,因此球的总飞行时间(t_flight)为

image

假设我们投掷一个初速度(u)为 5 m/s,投射角度(θ)为 45 度的球体。为了计算总飞行时间,我们将 u = 5,θ = 45,g = 9.8 代入我们上面看到的方程中:

image

在这种情况下,球体的飞行时间为 0.72154 秒(四舍五入到小数点后五位)。球体将在空中停留这段时间,因此为了绘制轨迹,我们将在这段时间内定期计算其 xy 坐标。我们应该多频繁地计算坐标呢?理想情况下,越频繁越好。在本章中,我们将在每 0.001 秒计算一次坐标。

生成等间隔的浮点数

我们使用了range()函数来生成等间隔的整数——也就是说,如果我们想要一个从 1 到 10 的整数列表,每个整数之间相隔 1,我们将使用 range(1, 10)。如果我们想要不同的步长值,可以将其作为第三个参数传递给 range 函数。不幸的是,对于浮点数并没有类似的内建函数。例如,并没有一个函数可以帮助我们创建从 0 到 0.72 的数字列表,其中每两个连续数字之间相隔 0.001。我们可以通过如下的while循环来创建自己的函数:

   '''
   Generate equally spaced floating point
   numbers between two given values
   '''

   def frange(start, final, increment):

       numbers = []
➊     while start < final:
➋         numbers.append(start)
           start = start + increment

       return numbers

我们定义了一个函数frange()(“浮点数”范围),它接收三个参数:startfinal分别表示数值范围的起始点和终点,increment表示两个连续数字之间的差值。在➊处,我们初始化了一个while循环,只要start所指的数字小于final,循环就会继续执行。在➋处,我们将start所指的数字存储到列表numbers中,然后在每次循环迭代中将我们输入的increment值加到start上。最后,我们返回列表numbers

我们将在接下来的轨迹绘制程序中使用这个函数来生成等间隔的时间点。

绘制轨迹

以下程序绘制了一个以特定速度和角度投掷的球的轨迹——这两个值作为输入传递给程序:

   '''
   Draw the trajectory of a body in projectile motion
   '''

   from matplotlib import pyplot as plt
   import math

   def draw_graph(x, y):
       plt.plot(x, y)
       plt.xlabel('x-coordinate')
       plt.ylabel('y-coordinate')
       plt.title('Projectile motion of a ball')

   def frange(start, final, interval):

       numbers = []
       while start < final:
           numbers.append(start)
           start = start + interval

       return numbers

   def draw_trajectory(u, theta):

➊     theta = math.radians(theta)
       g = 9.8

       # Time of flight
➋     t_flight = 2*u*math.sin(theta)/g
       # Find time intervals
       intervals = frange(0, t_flight, 0.001)

       # List of x and y coordinates
       x = []
       y = []
➌     for t in intervals:
           x.append(u*math.cos(theta)*t)
           y.append(u*math.sin(theta)*t - 0.5*g*t*t)

       draw_graph(x, y)

   if __name__ == '__main__':
➍     try:
           u = float(input('Enter the initial velocity (m/s): '))
           theta = float(input('Enter the angle of projection (degrees): '))
       except ValueError:
           print('You entered an invalid input')
       else:
           draw_trajectory(u, theta)
           plt.show()

在这个程序中,我们使用了标准库math模块中的radians()cos()sin()函数,所以我们在一开始就导入了这个模块。draw_trajectory()函数接受两个参数,utheta,分别对应投掷球的速度和角度。math模块中的正弦和余弦函数要求输入角度为弧度制,因此在➊处,我们使用math.radians()函数将角度(theta)从度数转换为弧度。接着,我们创建了一个标签(g)来表示重力加速度的值,即 9.8 米/秒²。在➋处,我们计算了飞行时间,并调用了frange()函数,传入了startfinalincrement的值,分别为 0、t_flight和 0.001。然后,我们在每个时间点计算* x y 坐标,并将它们分别存储在两个列表中,xy ➌。为了计算这些坐标,我们使用了之前讨论过的 S[x] S[y]*的距离公式。

最后,我们调用draw_graph()函数并传入xy坐标来绘制轨迹。请注意,draw_graph()函数没有调用show()函数(我们将在下一个程序中看到原因)。我们使用try...except块➍来报告错误信息,以防用户输入无效数据。此程序的有效输入为任何整数或浮动点数。当你运行程序时,它会要求输入这些值,然后绘制轨迹(见图 2-14):

Enter the initial velocity (m/s): 25
Enter the angle of projection (degrees): 60

image

图 2-14:以 25 m/s 的速度,角度为 60 度时,投掷球体的轨迹

比较不同初速度下的轨迹

之前的程序让你能够进行有趣的实验。例如,对于三个以不同速度投掷但初始角度相同的球体,轨迹会是什么样的?为了同时绘制三条轨迹,我们可以用以下代码替换之前程序中的main代码块:

   if __name__ == '__main__':

       # List of three different initial velocities
➊     u_list = [20, 40, 60]
       theta = 45
       for u in u_list:
           draw_trajectory(u, theta)

       # Add a legend and show the graph
➋     plt.legend(['20', '40', '60'])
       plt.show()

在这里,我们不再要求程序的用户输入速度和投射角度,而是在➊处创建一个包含 20、40 和 60 的速度列表(u_list),并将投射角度设置为 45 度(使用标签theta)。然后,我们使用u_list中的每个值调用draw_trajectory()函数,并使用相同的theta值,计算xy坐标列表并调用draw_graph()函数。当我们调用show()函数时,所有三条轨迹将在同一图表上显示。由于现在我们有一个包含多个图表的图表,在➋处我们为图表添加一个图例,然后调用show()以显示每条线的速度。当你运行上述程序时,你将看到图 2-15 所示的图表。

image

图 2-15:以 60 度角、速度分别为 20、40 和 60 m/s 的投掷球体轨迹

你学到的内容

在本章中,你学习了使用 matplotlib 创建图表的基础知识。你了解了如何绘制一组值的图表,如何在同一图表上创建多个图表,以及如何标注图表的不同部分使其更加信息化。你使用图表分析了一个城市的温度变化,研究了牛顿的万有引力定律,并研究了物体的抛体运动。在下一章中,你将使用 Python 开始探索统计学,并了解如何通过绘制图表帮助更容易理解一组数字之间的关系。

编程挑战

这里有一些挑战,基于你在本章中所学的内容。你可以在 www.nostarch.com/doingmathwithpython/ 找到示例解决方案。

#1:白天气温是如何变化的?

如果你在谷歌搜索引擎中输入类似“纽约天气”这样的搜索词,你会看到其中包括一个图表,展示当天不同时间的温度。你的任务是重新创建这样一个图表。

选择你喜欢的城市,找到不同时间点的温度数据。使用这些数据在你的程序中创建两个列表,并绘制一个图表,图表的横坐标是一天中的时间,纵坐标是对应的温度。这个图表应该展示温度如何随一天中的时间变化。尝试使用另一个城市,并通过将两座城市的曲线绘制在同一张图表上,看看它们的比较结果。

一天中的时间可以通过类似'10:11 AM''09:21 PM'这样的字符串表示。

#2: 直观展示二次函数

在第一章中,你学会了如何求解二次方程的根,比如x² + 2x + 1 = 0。我们可以通过将其写为y = x² + 2x + 1,将此方程转化为一个函数。对于任何值的x,二次函数都会产生一个对应的y值。例如,当x = 1 时,y = 4。以下是一个计算六个不同x值对应y值的程序:

   '''
   Quadratic function calculator
   '''

   # Assume values of x
➊ x_values = [-1, 1, 2, 3, 4, 5]
➋ for x in x_values:
       # Calculate the value of the quadratic function
       y = x**2 + 2*x + 1
       print('x={0} y={1}'.format(x, y))

在➊处,我们创建一个包含六个不同x值的列表。➋处开始的for循环为这些值计算上述函数的值,并使用标签y表示结果列表。接下来,我们打印出x值和对应的y值。当你运行程序时,你应该看到如下输出:

x=-1 y=0
x=1 y=4
x=2 y=9

x=3 y=16
x=4 y=25
x=5 y=36

注意,输出的第一行是二次方程的一个根,因为它是一个使得函数值为 0 的x值。

你的编程挑战是增强这个程序,创建该函数的图表。尝试使用至少 10 个x值,而不是上面提到的 6 个。使用该函数计算对应的y值,然后使用这两个值集创建图表。

一旦你创建了图表,花一些时间分析y值是如何随x值变化的。变化是线性的吗?还是非线性?

#3: 增强型抛射轨迹比较程序

你在这里的挑战是以几种方式增强轨迹比较程序。首先,程序应打印出每种速度和抛射角度组合的飞行时间、最大水平距离和最大垂直距离。

另一个增强功能是让程序能够接受任意数量的初速度和抛射角度值,由用户提供。例如,程序应该这样询问用户输入:

How many trajectories? 3
Enter the initial velocity for trajectory 1 (m/s): 45
Enter the angle of projection for trajectory 1 (degrees): 45
Enter the initial velocity for trajectory 2 (m/s): 60
Enter the angle of projection for trajectory 2 (degrees): 45
Enter the initial velocity for trajectory(m/s) 3: 45
Enter the angle of projection for trajectory(degrees) 3: 90

你的程序还应确保通过使用try...except块来正确处理错误输入,就像原始程序一样。

#4: 可视化你的支出

我总是在月末问自己:“那笔钱都去哪了?”我相信这不仅是我个人的困扰。

对于这个挑战,你将编写一个程序,用于轻松比较每周的支出。程序首先会询问支出的类别数量以及每个类别的每周总支出,然后它将创建显示这些支出的柱状图。

这是程序运行的示例:

Enter the number of categories: 4
Enter category: Food
Expenditure: 70

Enter category: Transportation
Expenditure: 35
Enter category: Entertainment
Expenditure: 30
Enter category: Phone/Internet
Expenditure: 30

图 2-16 展示了将要创建的柱状图,用于比较支出。如果你每周都保存柱状图,到月底时,你就能看到不同类别的支出在各周之间的变化。

image

图 2-16:显示一周内各类别支出的柱状图

我们还没有讨论如何使用 matplotlib 创建柱状图,所以让我们尝试一个示例。

可以使用 matplotlib 的 barh() 函数来创建柱状图,该函数也定义在 pyplot 模块中。图 2-17 显示了一张柱状图,展示了我过去一周走的步数。星期天、星期一、星期二等被称为标签。每个水平条形图都从 y 轴开始,我们需要为每个条形图指定该位置的 y 坐标的 中心。每个条形图的长度与步数相对应。

image

图 2-17:显示一周内步数的柱状图

以下程序将创建柱状图:

   '''
   Example of drawing a horizontal bar chart
   '''
   import matplotlib.pyplot as plt
   def create_bar_chart(data, labels):
       # Number of bars
       num_bars = len(data)
       # This list is the point on the y-axis where each
       # Bar is centered. Here it will be [1, 2, 3...]
➊     positions = range(1, num_bars+1)
➋     plt.barh(positions, data, align='center')
       # Set the label of each bar
       plt.yticks(positions, labels)
       plt.xlabel('Steps')
       plt.ylabel('Day')
       plt.title('Number of steps walked')
       # Turns on the grid which may assist in visual estimation
       plt.grid(
       plt.show()

   if __name__ == '__main__':
       # Number of steps I walked during the past week
       steps = [6534, 7000, 8900, 10786, 3467, 11045, 5095]
       # Corresponding days
       labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
       create_bar_chart(steps, labels)

create_bar_chart() 函数接受两个参数——data,这是一个我们想用条形图表示的数字列表,和相应的 labels 列表。每个条形图的中心必须指定,我在 ➊ 处使用 range() 函数随意选择了中心点,分别为 1、2、3、4 等等。

接下来,我们调用 barh() 函数,传入 positionsdata 作为前两个参数,然后在 ➋ 处传入关键字参数 align='center'。该关键字参数指定了条形图在 y 轴上的位置居中。接着,我们使用 yticks() 函数设置每个条形图的标签、坐标轴标签和标题。我们还调用 grid() 函数来打开网格,这对于大致估算步数可能有帮助。最后,我们调用 show() 函数。

#5: 探索斐波那契数列与黄金比例之间的关系

斐波那契数列(1, 1, 2, 3, 5, ...)是一个数列,其中第 i 个数是前两个数的和——即位置在 (i – 2) 和 (i – 1) 的数。这个数列中的连续数字展示了一个有趣的关系。当你增加数列中的项数时,连续数字对的比率几乎相等。这个值趋近于一个特殊的数字,称为 黄金比。从数字上看,黄金比是 1.618033988 ...,它已经成为音乐、建筑和自然界广泛研究的对象。对于这个挑战,编写一个程序,在图表上绘制前 100 个斐波那契数之间的比率,这将展示这些值趋近于黄金比。

你可能会发现以下函数有用,它返回前 n 个斐波那契数的列表,这对实现你的解决方案有帮助:

def fibo(n):
    if n == 1:
        return [1]
    if n == 2:
        return [1, 1]
    # n > 2
    a = 1
    b = 1
    # First two members of the series
    series = [a, b]
    for i in range(n):
        c = a + b
        series.append(c)
        a = b
        b = c

    return series

你解决方案的输出应该是一个图表,如图 2-18 所示。

image

图 2-18:连续斐波那契数列之间的比率接近黄金比。

第三章:用统计学描述数据

image

在本章中,我们将使用 Python 来探索统计学,以便研究、描述和更好地理解数据集。在介绍一些基本的统计量——均值、中位数、众数和极差之后,我们将进入一些更高级的统计量,如方差和标准差。然后,我们将学习如何计算相关系数,这使得你可以量化两组数据之间的关系。最后,我们将学习如何绘制散点图。在这个过程中,我们还将更多地了解 Python 语言和标准库模块。让我们从最常用的统计量——均值开始吧。

注意

在统计学中,一些统计量的计算方法会根据你是拥有整个总体的数据还是仅有样本数据而有所不同。为了简化起见,本章我们将使用总体的计算方法。

计算均值

均值 是一种常见且直观的方式,用来总结一组数字。它通常被我们称为“平均数”,尽管正如我们所见,还有其他类型的平均数。让我们取一个样本数据集并计算均值。

假设有一个学校慈善机构,它在过去的 12 天里(我们称之为周期 A)接受了捐款。在这段时间里,以下 12 个数字表示每天收到的捐款总额:100、60、70、900、100、200、500、500、503、600、1000 和 1200。我们可以通过将这些总额相加,然后将总和除以天数来计算均值。在这个例子中,数字的总和是 5733。如果我们将这个数字除以 12(天数),我们得到 477.75,这就是每一天的均值捐款。这个数字给了我们一个大致的了解,即每一天捐赠的金额。

稍后,我们将编写一个程序来计算并打印一组数字的均值。正如我们刚才看到的,计算均值时,我们需要先求出数字列表的总和,然后再将总和除以列表中的项数。我们来看看两个非常简单的 Python 函数:sum()len(),它们使这两个操作变得非常简单。

当你对一组数字使用sum()函数时,它会将列表中的所有数字加起来并返回结果:

>>> shortlist = [1, 2, 3]
>>> sum(shortlist)
6

我们可以使用len()函数来获取列表的长度:

>>> len(shortlist)
3

当我们对列表使用len()函数时,它返回3,因为shortlist中有三个项。现在我们可以编写一个程序来计算捐款列表的均值了。

   '''
   Calculating the mean
   '''

   def calculate_mean(numbers):
➊     s = sum(numbers)
➋     N = len(numbers)
       # Calculate the mean
➌     mean = s/N

       return mean

   if __name__ == '__main__':
➍     donations = [100, 60, 70, 900, 100, 200, 500, 500, 503, 600, 1000, 1200]
➎     mean = calculate_mean(donations)
       N = len(donations)
➏     print('Mean donation over the last {0} days is {1}'.format(N, mean))

首先,我们定义一个函数calculate_mean(),它接受参数numbers,这是一个数字列表。在➊处,我们使用sum()函数将列表中的数字相加,并创建一个标签s来表示总和。同样,在➋处,我们使用len()函数获取列表的长度,并创建一个标签N来表示它。然后,正如你在➌处看到的,我们通过将总和(s)除以元素个数(N)来计算均值。在➍处,我们创建一个列表donations,其中包含之前列出的捐赠值。然后,我们调用calculate_mean()函数,在➎处将此列表作为参数传递给它。最后,在➏处我们打印出计算出的均值。

当你运行程序时,你应该看到如下内容:

Mean donation over the last 12 days is 477.75

calculate_mean()函数将计算任何列表的总和和长度,因此我们也可以重复使用它来计算其他数字集合的均值。

我们计算得出每日均捐赠额为 477.75。值得注意的是,前几天的捐赠额远低于我们计算的均值,而最后几天的捐赠额则远高于均值。均值为我们提供了一种总结数据的方式,但它并没有提供完整的图景。然而,其他统计量可以在与均值比较时,给我们更多有关数据的信息。

查找中位数

数字集合的中位数是另一种平均值。为了找到中位数,我们将数字按升序排列。如果数字列表的长度是奇数,则列表中间的数字就是中位数。如果数字列表的长度是偶数,我们通过取中间两个数字的均值来得到中位数。让我们找出之前捐赠列表的中位数:100, 60, 70, 900, 100, 200, 500, 500, 503, 600, 1000 和 1200。

按照从小到大的顺序排序后,数字列表变为 60, 70, 100, 100, 200, 500, 500, 503, 600, 900, 1000 和 1200。我们有一个偶数个数的项目(12 个),因此为了得到中位数,我们需要取中间两个数字的均值。在这个例子中,中间两个数字是第六个和第七个数字——500 和 500——这两个数字的均值是(500 + 500)/2,结果是 500。这意味着中位数是 500。

现在假设——仅仅是为了这个示例——我们在第 13 天有了另一个捐赠总额,因此列表现在看起来像这样:100, 60, 70, 900, 100, 200, 500, 500, 503, 600, 1000, 1200 和 800。

再次,我们需要对列表进行排序,排序后的列表为 60, 70, 100, 100, 200, 500, 500, 503, 600, 800, 900, 1000 和 1200。这个列表中有 13 个数字(奇数个),因此这个列表的中位数就是中间的数字。在这个例子中,它是第七个数字,即 500。

在编写程序以查找数字列表的中位数之前,让我们思考如何自动计算列表中的中间元素。如果列表的长度(N)是奇数,则中间数字位于位置(N + 1)/2。如果N是偶数,则两个中间元素位于N/2 和(N/2) + 1 的位置。在本节的第一个示例中,N = 12,所以两个中间元素是 12/2(第六个)和 12/2 + 1(第七个)元素。在第二个示例中,N = 13,所以第七个元素((N + 1)/2)是中间元素。

为了编写一个计算中位数的函数,我们还需要将列表按升序排序。幸运的是,sort()方法正好可以实现这一点:

>>> samplelist = [4, 1, 3]
>>> samplelist.sort()
>>> samplelist
[1, 3, 4]

现在我们可以编写下一个程序,它可以查找数字列表的中位数:

   '''
   Calculating the median
   '''

   def calculate_median(numbers):
➊     N = len(numbers)
➋     numbers.sort()

       # Find the median
       if N % 2 == 0:
           # if N is even
           m1 = N/2
           m2 = (N/2) + 1
           # Convert to integer, match position
➌         m1 = int(m1) - 1
➍         m2 = int(m2) - 1
➎         median = (numbers[m1] + numbers[m2])/2
       else:
➏         m = (N+1)/2
           # Convert to integer, match position
           m = int(m) - 1
           median = numbers[m]

       return median

   if __name__ == '__main__':
       donations = [100, 60, 70, 900, 100, 200, 500, 500, 503, 600, 1000, 1200]

       median = calculate_median(donations)
       N = len(donations)
       print('Median donation over the last {0} days is {1}'.format(N, median))

程序的整体结构类似于前面计算均值的程序。calculate_median()函数接受一个数字列表并返回中位数。在➊处,我们计算列表的长度并创建一个标签N来引用它。接下来,在➋处,我们使用sort()方法对列表进行排序。

然后,我们检查N是否为偶数。如果是,我们找到中间的两个元素,m1m2,它们分别是排序后列表中位置为N/2(N/2) + 1的数字。接下来的两个语句(➌和➍)以两种方式调整m1m2。首先,我们使用int()函数将m1m2转换为整数。这是因为除法运算符的结果总是以浮动点数返回,即使结果相当于整数。例如:

>>> 6/2
3.0

我们不能使用浮动点数作为列表的索引,所以我们使用int()将该结果转换为整数。我们还从m1m2中各减去 1,因为 Python 中的列表位置是从 0 开始的。这意味着要从列表中获取第六和第七个数字,我们必须请求索引为 5 和 6 的数字。在➎处,我们通过计算中间两个位置的数字的平均值来得到中位数。

从➏开始,程序在列表中有奇数项时找到中位数,再次使用int()并减去 1 来找到正确的索引。最后,程序计算捐赠列表的中位数并返回结果。当你执行程序时,它计算出中位数是 500:

Median donation over the last 12 days is 500.0

如你所见,均值(477.75)和中位数(500)在这个特定的列表中相当接近,但中位数稍微高一些。

查找众数并创建频率表

与其找出一组数字的平均值或中位数值,不如问问哪个数字最常出现?这个数字叫做众数。例如,考虑一场数学考试的分数(满分 10 分),20 个学生的成绩分别为:7、8、9、2、10、9、9、9、9、4、5、6、1、5、6、7、8、6、1 和 10。这个列表的众数告诉我们哪个分数在班级中最常见。从列表中可以看到,分数 9 出现的次数最多,所以 9 是这个数字列表的众数。计算众数没有符号公式——你只需要统计每个独特数字出现的次数,并找出出现最多的那个。

要编写一个程序来计算众数,我们需要让 Python 计算每个数字在列表中出现的次数,并打印出出现次数最多的那个数字。collections模块中的Counter类,作为标准库的一部分,使这一操作变得非常简单。

查找最常见的元素

在数据集中找出最常见的数字,可以看作是找出若干个最常见数字的子问题。例如,除了找出最常见的分数之外,如果你想知道最常见的五个分数呢?Counter类的most_common()方法让我们轻松回答这些问题。让我们看一个例子:

>>> simplelist = [4, 2, 1, 3, 4]
>>> from collections import Counter
>>> c = Counter(simplelist)
>>> c.most_common()
[(4, 2), (1, 1), (2, 1), (3, 1)]

在这里,我们从五个数字的列表开始,并从collections模块导入Counter。然后,我们创建一个Counter对象,用c来引用这个对象。接着,我们调用most_common()方法,它返回一个按照最常见元素排序的列表。

列表中的每个成员都是一个元组。第一个元组的第一个元素是出现频率最高的数字,第二个元素是它出现的次数。第二、第三和第四个元组包含其他数字及其出现次数。这个结果告诉我们,4 是出现次数最多的(出现两次),而其他数字仅出现一次。请注意,most_common()方法返回的数字顺序是随机的,若多个数字出现相同次数。

当你调用most_common()方法时,你还可以提供一个参数,告诉它你希望返回最常见元素的数量。例如,如果我们只想找到最常见的元素,可以使用参数1来调用:

>>> c.most_common(1)
[(4, 2)]

如果你再次调用该方法,并将2作为参数,你将看到以下结果:

>>> c.most_common(2)
[(4, 2), (1, 1)]

现在,most_common方法返回的结果是一个包含两个元组的列表,第一个是最常见的元素,第二个是次常见的元素。当然,在这种情况下,有几个元素的出现次数并列为最多,因此,函数返回 1(而不是 2 或 3)是随机的,正如之前所提到的。

most_common()方法返回的既有数字,也有它们出现的次数。如果我们只想要数字,而不关心它们出现的次数,应该如何获取这个信息呢?你可以这样来获取:

➊ >>> mode = c.most_common(1)
   >>> mode
   [(4, 2)]
➋ >>> mode[0]
   (4, 2)
➌ >>> mode[0][0]
   4

在 ➊,我们使用标签mode来表示most_common()方法返回的结果。我们通过mode[0] ➋获取该列表的第一个(也是唯一的)元素,这会返回一个元组。因为我们只需要元组的第一个元素,所以可以通过mode[0][0] ➌来获取。这会返回 4——最常见的元素,即众数。

现在我们已经了解了most_common()方法的工作原理,我们将应用它来解决接下来的两个问题。

寻找众数

我们已经准备好编写一个程序来找出数字列表的众数:

   '''
   Calculating the mode
   '''

   from collections import Counter

   def calculate_mode(numbers):
➊     c = Counter(numbers)
➋     mode = c.most_common(1)
➌     return mode[0][0]

   if __name__=='__main__':
       scores = [7,8,9,2,10,9,9,9,9,4,5,6,1,5,6,7,8,6,1,10]
       mode = calculate_mode(scores)

       print('The mode of the list of numbers is: {0}'.format(mode))

calculate_mode()函数找到并返回作为参数传入的数字的众数。为了计算众数,我们首先从collections模块导入Counter类,并在 ➊ 使用它来创建一个Counter对象。然后,在 ➋ 我们使用most_common()方法,正如之前所看到的,它会返回一个包含最常见数字及其出现次数的元组的列表。我们将该列表命名为mode。最后,我们使用mode[0][0] ➌来访问我们想要的数字:列表中最频繁的数字,即众数。

程序的其余部分应用了calculate_mode函数来处理我们之前看到的测试分数列表。当你运行程序时,应该会看到以下输出:

The mode of the list of numbers is: 9

如果你有一组数据,其中两个或更多的数字出现的次数相同且为最大次数怎么办?例如,在数字列表 5、5、5、4、4、4、9、1 和 3 中,4 和 5 都出现了三次。在这种情况下,数字列表被认为有多个众数,我们的程序应当找到并打印所有的众数。修改后的程序如下:

   '''
   Calculating the mode when the list of numbers may
   have multiple modes
   '''

   from collections import Counter

   def calculate_mode(numbers):

       c = Counter(numbers)
➊     numbers_freq = c.most_common()
➋     max_count = numbers_freq[0][1]

       modes = []
       for num in numbers_freq:
➌         if num[1] == max_count:
               modes.append(num[0])
       return modes

   if __name__ == '__main__':
       scores = [5, 5, 5, 4, 4, 4, 9, 1, 3]
       modes = calculate_mode(scores)
       print('The mode(s) of the list of numbers are:')
➍     for mode in modes:
           print(mode)

在 ➊,我们不仅仅找出最常见的元素,而是获取所有的数字以及每个数字出现的次数。接下来,在 ➋,我们找到最大计数的值——即任何数字出现的最大次数。然后,对于每个数字,我们检查它出现的次数是否等于最大计数 ➌。每个满足此条件的数字都是众数,我们将它们添加到modes列表中并返回该列表。

在 ➍,我们遍历calculate_mode()函数返回的列表,并打印每个数字。

当你执行前面的程序时,应该会看到以下输出:

The mode(s) of the list of numbers are:
4
5

如果你想找出每个数字出现的次数,而不仅仅是众数怎么办?频率表,顾名思义,是一张表格,显示了每个数字在数字集合中出现的次数。

创建频率表

让我们再考虑一下测试分数列表:7、8、9、2、10、9、9、9、9、4、5、6、1、5、6、7、8、6、1 和 10。该列表的频率表如下所示:表 3-1。对于每个数字,我们在第二列列出它出现的次数。

表 3-1: 频率表

分数 频率
1 2
2 1
4 1
5 2
6 3
7 2
8 2
9 5
10 2

注意,第二列中各个频率的总和等于所有分数的总数(在本例中为 20)。

我们将再次使用most_common()方法打印给定数字集合的频率表。回顾一下,当我们不向most_common()方法提供参数时,它会返回一个包含所有数字及其出现次数的元组列表。我们可以直接打印每个数字及其频率,从而显示频率表。

这是程序:

   '''
   Frequency table for a list of numbers
   '''

   from collections import Counter

   def frequency_table(numbers):
➊     table = Counter(numbers)
       print('Number\tFrequency')
➋     for number in table.most_common():
           print('{0}\t{1}'.format(number[0], number[1]))

   if __name__=='__main__':
       scores = [7,8,9,2,10,9,9,9,9,4,5,6,1,5,6,7,8,6,1,10]
       frequency_table(scores)

函数frequency_table()打印传递给它的数字列表的频率表。在➊处,我们首先创建一个Counter对象,并为其创建标签table。接下来,在使用for循环➋遍历每个元组时,打印第一个元素(数字本身)和第二个元素(该数字的频率)。我们使用\t来在每个值之间打印制表符,以便表格对齐。当你运行程序时,你将看到以下输出:

Number  Frequency
9       5
6       3
1       2
5       2
7       2
8       2
10      2
2       1
4       1

在这里,你可以看到数字按频率从高到低的顺序列出,因为most_common()函数会以这种顺序返回数字。如果你希望程序打印一个按值从低到高排序的频率表,如表 3-1 所示,你需要重新排序元组列表。

sort()方法是我们修改早期频率表程序所需的全部:

   '''
   Frequency table for a list of numbers
   Enhanced to display the table sorted by the numbers
   '''

   from collections import Counter

   def frequency_table(numbers):
       table = Counter(numbers)
➊     numbers_freq = table.most_common()
➋     numbers_freq.sort()

       print('Number\tFrequency')
➌     for number in numbers_freq:
           print('{0}\t{1}'.format(number[0], number[1]))

   if __name__ == '__main__':
       scores = [7,8,9,2,10,9,9,9,9,4,5,6,1,5,6,7,8,6,1,10]
       frequency_table(scores)

在这里,我们在➊处将most_common()方法返回的列表存储在numbers_freq中,然后通过调用sort()方法在➋处对其进行排序。最后,我们使用for循环遍历排序后的元组,打印每个数字及其频率➌。现在,当你运行程序时,你会看到以下表格,它与表 3-1 完全相同:

Number  Frequency
1       2
2       1
4       1
5       2
6       3
7       2
8       2
9       5
10      2

在本节中,我们已经讨论了均值、中位数和众数,它们是描述一组数字的三种常见度量方法。每个度量方法都有其用处,但在单独考虑时也可能掩盖数据的其他方面。接下来,我们将探讨其他更高级的统计度量方法,它们可以帮助我们对一组数字做出更多的结论。

衡量离散程度

接下来,我们要看的统计计算是衡量离散程度,它告诉我们一组数据中数字与数据集均值的偏差程度。我们将学习计算三种不同的离散程度度量:极差、方差和标准差。

计算一组数字的极差

再次考虑期间 A 的捐款列表:100、60、70、900、100、200、500、500、503、600、1000 和 1200。我们发现每日捐款的均值为 477.75。但仅仅看均值,我们无法知道所有捐款是否都落在一个狭窄的范围内——例如在 400 和 500 之间,还是它们的差异比这大得多——比如在 60 和 1200 之间,就像这个例子一样。对于一组数字,范围是最高值和最低值之间的差异。你可以有两个均值完全相同的数字组,但它们的范围却差异巨大,所以了解范围能为我们提供关于一组数字的更多信息,超越了仅仅通过均值、中位数和众数所能获得的信息。

下一个程序查找前面提到的捐款列表的范围:

   '''
   Find the range
   '''

   def find_range(numbers):

➊     lowest = min(numbers)
➋     highest = max(numbers)
       # Find the range
       r = highest-lowest

➌     return lowest, highest, r

   if __name__ == '__main__':
       donations = [100, 60, 70, 900, 100, 200, 500, 500, 503, 600, 1000, 1200]
➍     lowest, highest, r = find_range(donations)
       print('Lowest: {0} Highest: {1} Range: {2}'.format(lowest, highest, r))

find_range()函数接受一个列表作为参数并找到范围。首先,它在 ➊ 和 ➋ 使用min()max()函数来计算最低值和最高值。正如函数名所示,它们分别用于找到列表中数字的最小值和最大值。

然后,我们通过计算最高值和最低值之间的差异来计算范围,用标签r表示这个差异。在 ➌ 处,我们返回三个数字——最低值、最高值和范围。这是书中第一次从函数返回多个值——不是只返回一个值,而是返回三个值。在 ➍ 处,我们使用三个标签来接收find_range()函数返回的三个值。最后,我们打印这些值。当你运行程序时,应该会看到以下输出:

Lowest: 60 Highest: 1200 Range: 1140

这告诉我们,捐款的总额相对分散,范围为 1140,因为我们的日捐款总额从 60 到 1200 不等。

方差和标准差的计算

范围告诉我们一组数字中两个极端值之间的差异,但如果我们想了解所有单独的数字与平均值的差异呢?它们是都相似,接近均值,还是差异很大,接近极端值?有两个相关的离散度度量可以告诉我们更多关于数字列表的变化情况:方差标准差。要计算这两者中的任何一个,我们首先需要找到每个数字与平均值的差异。方差是这些差异的平方的平均值。高方差意味着值远离均值;低方差意味着值聚集在均值附近。我们使用以下公式来计算方差:

image

在这个公式中,x[i] 代表单个数字(在这里是每日总捐款),x[mean] 代表这些数字的均值(即每日捐款的均值),n 是列表中数值的数量(即接收捐款的天数)。对于列表中的每个值,我们计算该数值与均值之间的差异并进行平方。然后,我们将所有这些平方差异相加,最后除以 n 来得到方差。

如果我们还想计算标准差,只需对方差取平方根即可。值与均值相差一个标准差以内的可以被认为是比较典型的,而与均值相差三倍或更多标准差的值则可以认为是非常不典型的——我们称这样的值为 离群值

为什么我们有这两种离散度的度量——方差和标准差?简而言之,这两种度量在不同情况下有不同的用途。回到我们用来计算方差的公式,你可以看到,方差以平方单位表示,因为它是平均偏差的平方。对于某些数学公式,使用这些平方单位更方便,而不是通过求平方根来计算标准差。另一方面,标准差与数据的单位相同。例如,如果你计算我们的捐款列表的方差(稍后我们将这样做),结果会以“美元平方”表示,这没有太大意义。与此同时,标准差则简单地以“美元”表示,这是每笔捐款的单位。

以下程序用于计算一个数字列表的方差和标准差:

   '''
   Find the variance and standard deviation of a list of numbers
   '''

   def calculate_mean(numbers):
       s = sum(numbers)
       N = len(numbers)
       # Calculate the mean
       mean = s/N

       return mean

   def find_differences(numbers):
       # Find the mean
       mean = calculate_mean(numbers)
       # Find the differences from the mean
       diff = []
       for num in numbers:
           diff.append(num-mean)

       return diff

   def calculate_variance(numbers):

       # Find the list of differences
➊     diff = find_differences(numbers)
       # Find the squared differences
       squared_diff = []
➋     for d in diff:
           squared_diff.append(d**2)
       # Find the variance
       sum_squared_diff = sum(squared_diff)
➌     variance = sum_squared_diff/len(numbers)
       return variance

   if __name__ == '__main__':
       donations = [100, 60, 70, 900, 100, 200, 500, 500, 503, 600, 1000, 1200]
       variance = calculate_variance(donations)
       print('The variance of the list of numbers is {0}'.format(variance))

➍     std = variance**0.5
       print('The standard deviation of the list of numbers is {0}'.format(std))

函数 calculate_variance() 计算传递给它的数字列表的方差。首先,它调用 find_differences() 函数在 ➊ 处计算每个数字与均值的差异。find_differences() 函数返回每笔捐款与均值的差异,并将结果作为一个列表返回。在这个函数中,我们使用之前编写的 calculate_mean() 函数来计算均值捐款。然后,从 ➋ 开始,计算这些差异的平方并将其保存在标记为 squared_diff 的列表中。接下来,我们使用 sum() 函数来计算这些平方差异的总和,最后在 ➌ 处计算方差。在 ➍ 处,我们通过对方差取平方根来计算标准差。

当你运行前面的程序时,你应该会看到以下输出:

The variance of the list of numbers is 141047.35416666666
The standard deviation of the list of numbers is 375.5627166887931

方差和标准差都非常大,这意味着单个每日捐款总额与平均值差异很大。现在,让我们比较另一组捐款的方差和标准差,这组捐款的平均值相同:382、389、377、397、396、368、369、392、398、367、393 和 396。在这种情况下,方差和标准差分别为 135.38888888888889 和 11.63567311713804。较低的方差和标准差值表明个别数据点更接近平均值。图 3-1 形象地展示了这一点。

image

图 3-1:捐款围绕平均捐款的变化

两组捐款的平均值相似,因此图中的两条线重叠,呈现为一条线。然而,第一组的捐款与平均值差异较大,而第二组的捐款则非常接近平均值,这验证了我们从较低方差值中推断出的结论。

计算两个数据集之间的相关性

在本节中,我们将学习如何计算一个统计量,该统计量告诉我们两个数字集合之间关系的性质和强度:皮尔逊相关系数,我将简称为 相关系数。请注意,该系数衡量的是线性关系的强度。如果两个集合存在非线性关系,我们需要使用其他度量(这里不讨论)来找到系数。该系数可以是正值或负值,且其绝对值范围在 -1 到 1 之间(包括 -1 和 1)。

相关系数为 0 表示这两个量之间没有线性相关性。(注意,这并不意味着这两个量是相互独立的,它们之间仍然可能存在非线性关系,例如)。相关系数为 1 或接近 1 表示存在强的正线性相关性;相关系数恰好为 1 被称为完美正相关。同样,相关系数为 –1 或接近 –1 表示存在强的负相关性,其中 –1 表示完美负相关。

相关性与因果性

在统计学中,你常常会遇到“相关性不代表因果性”这一说法。这是提醒我们,即使两个观察集合之间存在很强的相关性,也不意味着一个变量导致另一个变量。当两个变量之间的相关性很强时,有时是第三方因素同时影响这两个变量,从而解释了这种相关性。一个经典的例子是冰淇淋销量与犯罪率之间的相关性——如果你在一个典型的城市追踪这两个变量,你很可能会发现它们之间有相关性,但这并不意味着冰淇淋销量导致了犯罪(或反之亦然)。冰淇淋销量和犯罪的相关性是因为它们在夏天天气变热时都会增加。当然,这并不意味着炎热的天气直接导致了犯罪的增加;这种相关性背后也有更复杂的原因。

计算相关系数

相关系数通过以下公式计算:

image

在上述公式中,n是每个数字集合中存在的总值个数(这两个集合的长度必须相等)。这两个数字集合分别用xy表示(哪个表示哪个不重要)。其他项的描述如下:

Σxy 两个数字集合xy中各个元素乘积的和
Σx 集合x中数字之和
Σy 集合y中数字之和
x 集合x中数字之和的平方
y 集合y中数字之和的平方
Σx² 集合x中数字的平方和
Σy² 集合y中数字的平方和

一旦我们计算了这些项,就可以根据前述公式将它们结合起来,找到相关系数。对于较小的列表,这可以通过手动计算完成而不费太多力气,但随着每个数字集合大小的增加,这个过程会变得复杂。

一会儿,我们将编写一个程序来计算相关系数。在这个程序中,我们将使用zip()函数,它帮助我们计算两个数字集合的乘积和。以下是zip()函数如何工作的示例:

>>> simple_list1 = [1, 2, 3]
>>> simple_list2 = [4, 5, 6]
>>> for x, y in zip(simple_list1, simple_list2):
        print(x, y)

1 4
2 5
3 6

zip()函数返回xy中对应元素的配对,然后可以在循环中使用这些配对进行其他操作(如在前面的代码中显示的打印)。如果两个列表长度不相等,函数会在较小列表的所有元素被读取完后终止。

现在我们准备写一个程序,计算相关系数:

   def find_corr_x_y(x,y):
       n = len(x)

       # Find the sum of the products
       prod = []
➊     for xi,yi in zip(x,y):
           prod.append(xi*yi)

➋     sum_prod_x_y = sum(prod)
➌     sum_x = sum(x)
➍     sum_y = sum(y)
       squared_sum_x = sum_x**2
       squared_sum_y = sum_y**2

       x_square = []
➎     for xi in x:
           x_square.append(xi**2)
       # Find the sum
       x_square_sum = sum(x_square)

       y_square=[]
        for yi in y:
           y_square.append(yi**2)
       # Find the sum
       y_square_sum = sum(y_square)

       # Use formula to calculate correlation
➏     numerator = n*sum_prod_x_y - sum_x*sum_y
       denominator_term1 = n*x_square_sum - squared_sum_x
       denominator_term2 = n*y_square_sum - squared_sum_y
➐     denominator = (denominator_term1*denominator_term2)**0.5
➑     correlation = numerator/denominator

       return correlation

find_corr_x_y() 函数接受两个参数,xy,这两个是我们要计算相关性的两个数字集。在此函数的开始部分,我们先找出列表的长度,并创建一个标签 n 来表示它。接下来,在 ➊ 处,我们使用 for 循环,通过 zip() 函数计算每个列表中对应值的乘积(先将每个列表的第一个元素相乘,然后是第二个元素,依此类推)。我们使用 append() 方法将这些乘积添加到标签为 prod 的列表中。

在 ➋ 处,我们使用 sum() 函数计算存储在 prod 中的乘积之和。在 ➌ 和 ➍ 处的语句中,我们分别计算了 xy 中数字的总和(同样使用 sum() 函数)。然后,我们计算了 xy 中元素总和的平方,并分别创建标签 squared_sum_xsquared_sum_y 来表示这两个值。

在从 ➎ 开始的循环中,我们计算了 x 中每个元素的平方,并找出这些平方的和。然后,我们对 y 中的元素做相同的操作。现在我们已经获得了计算相关性所需的所有项,并在 ➏、➐ 和 ➑ 处的语句中进行了相关性计算。最后,我们返回相关性。相关性是统计研究中经常引用的一个指标——在流行媒体和科学文章中都很常见。有时我们事先知道存在相关性,只是想找出这种相关性的强度。我们将在 “从 CSV 文件读取数据” 一节中,在 第 86 页 看到这个例子,届时我们将计算从文件中读取数据的相关性。其他时候,我们可能只是怀疑存在相关性,需要通过数据调查来验证是否真的存在相关性(如下例所示)。

高中成绩与大学入学考试成绩

在本节中,我们将考虑一个虚构的 10 人高中生小组,并研究他们的高中成绩和大学入学考试成绩之间是否存在关系。表 3-2 列出了我们假设的研究数据,并基于这些数据进行实验。表中的“高中成绩”列列出了学生的高中成绩百分位数,而“大学入学考试成绩”列列出了他们的大学入学考试百分位数。

表 3-2: 高中成绩与大学入学考试成绩

高中成绩 大学入学考试成绩
90 85
92 87
95 86
96 97
87 96
87 88
90 89
95 98
98 98
96 87

为了分析这些数据,让我们看一个 散点图。 图 3-2 展示了前述数据集的散点图,其中 x 轴表示高中成绩,y 轴表示对应的大学入学考试成绩。

image

图 3-2:高中成绩与大学入学考试成绩的散点图

数据图表显示,成绩最好的高中生并不一定在大学入学考试中表现更好,反之亦然。有些高中成绩差的学生在大学入学考试中表现非常好,而一些高中成绩优秀的学生则在大学考试中相对较差。如果我们计算这两个数据集的相关系数(使用我们之前的程序),我们会发现它大约是 0.32。这意味着有一定的相关性,但并不很强。如果相关性接近 1,我们会在散点图中看到这种趋势——数据点会更贴近一条直线。

假设表 3-2 中显示的高中成绩是数学、科学、英语和社会科学各科目成绩的平均值。我们还假设大学考试特别重视数学——比其他学科要重得多。与其看学生的总体高中成绩,不如只看他们的数学成绩,以判断数学成绩是否更能预测他们在大学考试中的表现。表 3-3 现在只显示数学成绩(按百分位数)和大学入学考试成绩。相应的散点图显示在图 3-3 中。

表 3-3: 高中数学成绩与大学入学考试表现

高中数学成绩 大学入学考试成绩
83 85
85 87
84 86
96 97
94 96
86 88
87 89
97 98
97 98
85 87

现在,散点图(图 3-3)显示数据点几乎完全沿一条直线分布。这表明高中数学成绩与大学入学考试成绩之间有很高的相关性。此时,相关系数大约为 1。通过散点图和相关系数的帮助,我们可以得出结论:在这个数据集中,高中数学成绩与大学入学考试成绩之间确实存在很强的关系。

image

图 3-3:高中数学成绩与大学入学考试成绩的散点图

散点图

在上一节中,我们看到散点图如何为我们提供两个数据集之间是否存在相关性的初步指示。在这一节中,我们将通过查看一组四个数据集来了解分析散点图的重要性。对于这些数据集,传统的统计指标结果都是相同的,但每个数据集的散点图揭示了重要的差异。

首先,让我们了解如何在 Python 中创建散点图:

   >>> x = [1, 2, 3, 4]
   >>> y = [2, 4, 6, 8]
   >>> import matplotlib.pyplot as plt
➊ >>> plt.scatter(x, y)
   <matplotlib.collections.PathCollection object at 0x7f351825d550>
   >>> plt.show()

scatter()函数用于创建两个数值列表xy之间的散点图➊。这张图与我们在第二章中创建的图表唯一的不同之处是,在这里我们使用了scatter()函数,而不是plot()函数。我们仍然需要调用show()来显示图表。

为了深入了解散点图,我们来看一个重要的统计学研究:“统计分析中的图表”,作者是统计学家弗朗西斯·安斯科姆。¹ 该研究考虑了四个不同的数据集—统称为安斯科姆四重奏—它们具有相同的统计属性:均值、方差和相关系数。

数据集如表 3-4 所示(摘自原始研究)。

表 3-4: 安斯科姆四重奏—四个几乎相同统计度量的不同数据集

A B C D
X1 Y1 X2 Y2
--- --- --- ---
10.0 8.04 10.0 9.14
8.0 6.95 8.0 8.14
13.0 7.58 13.0 8.74
9.0 8.81 9.0 8.77
11.0 8.33 11.0 9.26
14.0 9.96 14.0 8.10
6.0 7.24 6.0 6.13
4.0 4.26 4.0 3.10
12.0 10.84 12.0 9.13
7.0 4.82 7.0 7.26
5.0 5.68 5.0 4.74

我们将一对对数据(X1, Y1)、(X2, Y2)、(X3, Y3)和(X4, Y4)分别称为数据集 A、B、C 和 D。表 3-5 展示了这些数据集的统计度量,并四舍五入保留两位小数。

表 3-5: 安斯科姆四重奏—统计度量

数据集 X Y
均值 标准差 均值
--- --- ---
A 9.00 3.32
B 9.00 3.32
C 9.00 3.32
D 9.00 3.32

每个数据集的散点图展示在图 3-4 中。

image

图 3-4:安斯科姆四重奏的散点图

如果我们只看传统的统计度量(见表 3-5),比如均值、标准差和相关系数,这些数据集似乎几乎相同。但是散点图显示,这些数据集实际上彼此之间有很大的不同。因此,散点图可以成为一个重要的工具,应该与其他统计度量一起使用,才能在得出关于数据集的结论之前进行全面的分析。

从文件中读取数据

在本章的所有程序中,我们用于计算的数字列表都是明确写入程序中的,或者说是硬编码的。如果你想为一个不同的数据集计算度量值,你必须在程序中输入整个新数据集。你也知道如何制作允许用户输入数据的程序,但对于大型数据集来说,每次让用户输入长长的数字列表并不是很方便。

一个更好的替代方案是从文件中读取用户数据。让我们看一个简单的例子,看看如何从文件中读取数字并对它们进行数学运算。首先,我将展示如何从一个简单的文本文件中读取数据,文件中的每一行包含一个新的数据元素。然后,我会向你展示如何从一个文件中读取数据,这些数据是以著名的 CSV 格式存储的,这为我们打开了许多可能性,因为你可以从互联网上下载许多有用的数据集,它们都是以 CSV 格式存储的。(如果你不熟悉 Python 中的文件操作,可以参考附录 B 进行简短的介绍。)

从文本文件中读取数据

假设我们有一个文件,mydata.txt,其中包含了在 A 期内的捐款列表(每行一个捐款),这是我们在本章开始时考虑过的内容:

100
60
70
900
100
200
500
500
503
600
1000
1200

以下程序将读取这个文件并打印出文件中存储的数字的总和:

   # Find the sum of numbers stored in a file
   def sum_data(filename):
       s = 0
➊     with open(filename) as f:
           for line in f:
➋             s = s + float(line)
       print('Sum of the numbers: {0}'.format(s))

   if __name__ == '__main__':
       sum_data('mydata.txt')

sum_data() 函数在 ➊ 打开由参数 filename 指定的文件,并逐行读取它(f 被称为文件对象,你可以把它看作是指向一个打开的文件)。在 ➋,我们使用 float() 函数将每个数字转换为浮动点数字,然后继续累加,直到读取完所有数字。最终的数字 s 保存了这些数字的总和,最后在函数末尾打印出来。

在运行程序之前,你必须首先创建一个名为 mydata.txt 的文件,并在其中放入适当的数据,然后将其保存到与程序相同的目录下。你可以通过在 IDLE 中点击 文件新建窗口,在新窗口中输入数字(每行一个),然后将文件保存为 mydata.txt,并确保它与程序保存在同一目录下。现在,如果你运行程序,你会看到如下输出:

Sum of the numbers: 5733.0

本章中的所有程序都假设输入数据是以列表的形式存在的。为了在文件数据上使用我们之前的程序,我们首先需要从文件中创建一个列表。一旦我们拥有了这个列表,我们就可以使用之前编写的函数来计算相应的统计数据。以下程序计算存储在文件 mydata.txt 中数字的平均值:

   '''
   Calculating the mean of numbers stored in a file
   '''
   def read_data(filename):

       numbers = []
       with open(filename) as f:
           for line in f:
➊             numbers.append(float(line))

       return numbers

   def calculate_mean(numbers):
       s = sum(numbers)
       N = len(numbers)
       mean = s/N

       return mean

   if __name__ == '__main__':
➋     data = read_data('mydata.txt')
       mean = calculate_mean(data)
       print('Mean: {0}'.format(mean))

在我们调用 calculate_mean() 函数之前,需要先读取文件中存储的数字并将其转换为列表。为此,使用 read_data() 函数,该函数逐行读取文件。与其求和,函数将数字转换为浮动点数并添加到 numbers 列表中 ➊。然后返回该列表,并用标签 data 来表示 ➋。接着,我们调用 calculate_mean() 函数,它返回数据的平均值。最后,我们打印出结果。

当你运行程序时,应该会看到以下输出:

Mean: 477.75

当然,如果文件中的数字与此示例中的不同,平均值也会有所不同。

请参见 附录 B,了解如何提示用户输入文件名,并相应修改你的程序。这样可以让程序的用户指定任何数据文件。

从 CSV 文件读取数据

逗号分隔值(CSV)文件由行和列组成,列之间通过逗号分隔。你可以使用操作系统上的文本编辑器或专用软件(如 Microsoft Excel、OpenOffice Calc 或 LibreOffice Calc)查看 CSV 文件。

这是一个示例 CSV 文件,包含一些数字及其平方:

Number,Squared
10,100
9,81
22,484

第一行被称为 表头。在本例中,它告诉我们文件中第一列的条目是数字,第二列是相应的平方。接下来的三行(或行)包含一个数字及其平方,数字和平方之间用逗号分隔。你可以使用类似我展示过的 .txt 文件读取方法来读取该文件的数据。然而,Python 的标准库中有一个专门用于读取(和写入)CSV 文件的模块(csv),这使得操作稍微简单了一些。

将数字及其平方保存到与程序位于同一目录下的文件 numbers.csv 中。以下程序展示了如何读取此文件并创建一个散点图,展示数字与其平方之间的关系:

   import csv
   import matplotlib.pyplot as plt

   def scatter_plot(x, y):
       plt.scatter(x, y)
       plt.xlabel('Number')
       plt.ylabel('Square')
       plt.show()

   def read_csv(filename):

       numbers = []
       squared = []
       with open(filename) as f:
➊         reader = csv.reader(f)
           next(reader)
➋         for row in reader:
               numbers.append(int(row[0]))
               squared.append(int(row[1]))
           return numbers, squared

   if __name__ == '__main__':
       numbers, squared = read_csv('numbers.csv')
       scatter_plot(numbers, squared)

read_csv() 函数使用在 csv 模块中定义的 reader() 函数读取 CSV 文件(该模块在程序开头导入)。此函数通过将文件对象 f 作为参数传入来调用 ➊。然后,该函数返回指向 CSV 文件第一行的 指针。我们知道文件的第一行是表头,通常我们希望跳过它,因此我们使用 next() 函数将指针移动到下一行。接着,我们读取文件的每一行,每行用标签 row 来表示 ➋,其中 row[0] 表示数据的第一列,row[1] 表示第二列。对于这个特定文件,我们知道这两列的数据都是整数,因此我们使用 int() 函数将这些字符串转换为整数,并将它们存储到两个列表中。然后,这两个列表会被返回——一个包含数字,另一个包含它们的平方。

然后,我们调用scatter_plot()函数,并传入这两个列表来创建散点图。我们之前编写的find_corr_x_y()函数也可以轻松用于计算这两个数值集之间的相关系数。

现在让我们尝试处理一个更复杂的 CSV 文件。在浏览器中打开www.google.com/trends/correlate/,输入你想要的任何搜索查询(例如,summer),然后点击搜索相关性按钮。你会看到在“与夏季相关”这一标题下返回了多个结果,第一个结果是相关性最高的(每个结果左侧的数字)。点击图表上方的散点图选项,查看带有x-轴标签为summery-轴标签为最高相关结果的散点图。忽略两轴上的具体数字,因为我们这里只关注相关性和散点图。

在散点图上方,点击导出数据为 CSV,下载文件将开始。将此文件保存在与你的程序相同的目录下。

这个 CSV 文件与我们之前看到的略有不同。在文件的开头,你会看到一些空行和带有'#'符号的行,直到最后你才会看到表头和数据。这些行对我们没有用处——请使用你打开文件时所用的软件手动删除它们,确保文件的第一行是表头。同时,删除文件末尾的空行。现在保存文件。这个步骤——我们清理文件以便用 Python 处理——通常被称为数据预处理

表头包含多个列。第一列包含每一行数据的日期(每一行的数据对应于以此列日期开始的那一周)。第二列是你输入的搜索查询,第三列显示与你的搜索查询有最高相关性的搜索查询,其他列包含许多其他搜索查询,按与输入查询的相关性降序排列。这些列中的数字是相应搜索查询的z-分数。z-分数表示某个术语在特定一周的搜索次数与该术语每周平均搜索次数之间的差异。正z-分数表示该周的搜索次数高于该术语的周平均搜索次数,负z-分数则表示低于平均值。

现在,让我们只处理第二列和第三列。你可以使用以下read_csv()函数来读取这两列:

   def read_csv(filename):

       with open(filename) as f:
           reader = csv.reader(f)
           next(reader)

           summer = []
           highest_correlated = []
➊         for row in reader:
               summer.append(float(row[1]))
               highest_correlated.append(float(row[2]))

       return summer, highest_correlated

这与之前版本的read_csv函数差不多;这里的主要变化是在➊处我们如何将值附加到每个列表:我们现在读取每一行的第二和第三个成员,并将它们作为浮动点数存储。

以下程序使用这个函数来计算你提供的搜索查询的值与与其相关性最高的查询的值之间的相关性。它还创建了这些值的散点图:

   import matplotlib.pyplot as plt
   import csv

   if __name__ == '__main__':
➊     summer, highest_correlated = read_csv('correlate-summer.csv')
       corr = find_corr_x_y(summer, highest_correlated)
       print('Highest correlation: {0}'.format(corr))
       scatter_plot(summer, highest_correlated)

假设 CSV 文件保存为correlate-summer.csv,我们调用read_csv()函数来读取第二列和第三列的数据➊。然后,我们调用之前编写的find_corr_x_y()函数,传入两个列表summerhighest_correlated。它返回相关系数,我们将其打印出来。接下来,我们再次调用之前编写的scatter_plot()函数,传入这两个列表。在运行这个程序之前,你需要包含read_csv()find_corr_x_y()scatter_plot()函数的定义。

运行时,你会看到它打印出相关系数并且创建散点图。两者应该与 Google 相关性网站上显示的数据非常相似。

你学到了什么

在本章中,你学习了如何计算描述一组数字及其之间关系的统计量。你还使用图表来帮助理解这些统计量。在编写程序计算这些统计量时,你学会了许多新的编程工具和概念。

编程挑战

接下来,将你所学的应用于以下编程挑战。

#1: 更好的相关系数寻找程序

我们之前编写的find_corr_x_y()函数用于查找两组数字之间的相关系数,它假设这两组数字的长度相同。改进该函数,使其首先检查列表的长度。如果它们相等,函数才继续进行剩余的计算;否则,函数应打印出无法找到相关性的信息。

#2: 统计计算器

实现一个统计计算器,读取文件mydata.txt中的数字列表,然后使用我们在本章中编写的函数来计算并打印它们的均值、中位数、众数、方差和标准差。

#3: 试验其他 CSV 数据

你可以自由地尝试互联网上提供的许多有趣的数据源。网站* www.quandl.com/* 就是一个这样的数据源。对于这个挑战,下载以下数据作为 CSV 文件,来源于 www.quandl.com/WORLDBANK/USA_SP_POP_TOTL/:1960 年至 2012 年间美国每年年末的总人口。然后,计算这些年人口差异的均值、中位数、方差和标准差,并创建一张展示这些差异的图表。

#4: 查找百分位数

百分位数是常用的统计量,用于表示给定百分比的观测值低于某个特定值。例如,如果一名学生在考试中获得了 95 百分位的分数,意味着 95%的学生得分低于或等于该学生的分数。再举个例子,在数字列表 5, 1, 9, 3, 14, 9, 7 中,第 50 百分位数是 7,第 25 百分位数是 3.5,这是一个列表中不存在的数字。

有许多方法可以找到与给定百分位数对应的观测值,以下是一种方法。²

假设我们想计算百分位数p对应的观测值:

1. 按升序排序给定的数字列表,我们可以将其称为data

2. 计算

image

其中 ndata中的项数。

3. 如果i是整数,则data[i]是百分位数p对应的数字。

4. 如果i不是整数,将k设为i的整数部分,f设为i的分数部分。数字(1-f)*data[k] + f*data[k+1]就是百分位数p对应的数字。

使用这种方法,编写一个程序,从文件中读取一组数字并显示与程序输入的特定百分位数对应的数字。

#5: 创建分组频率表

对于这个挑战,你的任务是编写一个程序,从一组数字中创建一个分组频率表。分组频率表显示了不同中的数据频率。例如,考虑我们在“创建频率表”中讨论过的分数,出现在第 69 页:7, 8, 9, 2, 10, 9, 9, 9, 9, 4, 5, 6, 1, 5, 6, 7, 8, 6, 1, 10。一个分组频率表将以如下方式显示这些数据:

成绩 频率
1–6 6
6–11 14

该表将成绩分为两类:1–6(包括 1 但不包括 6)和 6–11(包括 6 但不包括 11)。它显示了每个类别中属于的成绩数量。确定类的数量和每个类中数字的范围是创建该表的两个关键步骤。在这个例子中,我展示了两个类,并且每个类的数字范围在两个类之间均等分配。

这里有一种简单的方法来创建类,它假设类的数量可以任意选择:

def create_classes(numbers, n):
    low = min(numbers)
    high = max(numbers)

    # Width of each class
    width = (high - low)/n
    classes = []
    a = low
    b = low + width
    classes = []
    while a < (high-width):
        classes.append((a, b))
        a = b
        b = a + width
    # The last class may be of a size that is less than width
    classes.append((a, high+1))
    return classes

create_classes()函数接受两个参数:一个数字列表numbersn,即要创建的类的数量。它将返回一个包含元组的列表,每个元组表示一个类。例如,如果调用时传入数字 7, 8, 9, 2, 10, 9, 9, 9, 9, 4, 5, 6, 1, 5, 6, 7, 8, 6, 1, 10,并且n = 4,它将返回如下列表:[(1, 3.25), (3.25, 5.5), (5.5, 7.75), (7.75, 11)]。获得这个列表后,下一步是遍历每个数字,找出它属于哪个返回的类。

你的挑战是编写一个程序,从文件中读取一组数字,然后利用create_classes()函数打印分组频率表。

第四章:使用 SymPy 进行代数和符号数学

image

到目前为止,我们程序中的数学问题和解决方案都涉及数字的操作。但是,数学的学习和应用还有另一种方式,那就是通过符号以及它们之间的运算。想想典型代数问题中的所有 xy 吧。我们把这种数学称为 符号数学。我相信你一定记得在数学课上那些让人头疼的“因式分解 x³ + 3x² + 3x + 1”的题目。别怕,在这一章中,我们将学习如何编写程序来解决这些问题,甚至更多。为此,我们将使用 SymPy——一个 Python 库,它允许你编写包含符号的表达式并对其进行运算。因为这是一个第三方库,你需要在使用它之前先安装它。安装说明请参见附录 A。

定义符号和符号运算

符号 是符号数学的构建块。符号 这个术语就是指代你在方程式和代数表达式中使用的 xyab 等。创建和使用符号将让我们以不同的方式进行操作。考虑以下语句:

>>> x = 1
>>> x + x + 1
3

在这里,我们创建了一个标签 x,表示数字 1。然后,当我们写下语句 x + x + 1 时,它会被自动计算,结果是 3。那么,如果你希望结果以符号 x 来表示呢?也就是说,如果你希望 Python 告诉你结果是 2x + 1,而不是 3,你不能直接写 x + x + 1 而不写上语句 x = 1,因为 Python 不知道 x 指的是什么。

SymPy 让我们编写程序,通过符号来表示和计算数学表达式。要在程序中使用符号,你需要创建一个 Symbol 类的对象,像这样:

>>> from sympy import Symbol
>>> x = Symbol('x')

首先,我们从 sympy 库中导入 Symbol 类。然后,我们创建这个类的一个对象,传入 'x' 作为参数。请注意,这里的 'x' 是作为字符串写在引号中的。现在,我们可以用这个符号来定义表达式和方程。例如,这里是之前的表达式:

>>> from sympy import Symbol
>>> x = Symbol('x')
>>> x + x + 1
2*x + 1

现在,结果是以符号 x 来表示的。在语句 x = Symbol('x') 中,左边的 x 是 Python 标签。它是我们之前使用过的标签,只不过这次它表示的是符号 x,而不是数字——更具体地说,它是一个表示符号 'x'Symbol 对象。这个标签不一定非要与符号匹配——我们也可以使用标签 avar1 等。因此,将之前的语句写成如下也是完全可以的:

>>> a = Symbol('x')
>>> a + a + 1
2*x + 1

然而,使用不匹配的标签可能会让人困惑,因此我建议选择与符号相同字母的标签。

查找由符号对象表示的符号

对于任何 Symbol 对象,它的 name 属性是一个字符串,表示它所代表的实际符号:

>>> x = Symbol('x')
>>> x.name
'x'
>>> a = Symbol('x')
>>> a.name
'x'

你可以在标签上使用 .name 来检索它所存储的符号。

为了明确起见,你创建的符号必须作为字符串指定。例如,你不能通过 x = Symbol(x) 来创建符号 x——你必须像这样定义它:x = Symbol('x')

要定义多个符号,你可以创建单独的 Symbol 对象,或者使用 symbols() 函数更简洁地定义它们。假设你想在程序中使用三个符号——xyz。你可以像之前那样逐个定义它们:

>>> x = Symbol('x')
>>> y = Symbol('y')
>>> z = Symbol('z')

但一种更简洁的方法是使用 symbols() 函数一次性定义所有三个符号:

>>> from sympy import symbols
>>> x,y,z = symbols('x,y,z')

首先,我们从 SymPy 导入 symbols() 函数。然后,调用该函数并传入我们想要创建的三个符号,符号名称用逗号分隔。在执行完这行代码后,xyz 将分别代表符号 'x''y''z'

一旦你定义了符号,你可以对它们执行基本的数学运算,使用你在第一章中学到的相同运算符(+-/***)。例如,你可能会这样做:

>>> from sympy import Symbol
>>> x = Symbol('x')
>>> y = Symbol('y')

>>> s = x*y + x*y
>>> s
2*x*y

让我们看看是否能够找到 x(x + x) 的积:

>>> p = x*(x + x)
>>> p
2*x**2

SymPy 会自动进行这些简单的加法和乘法运算,但如果我们输入更复杂的表达式,它将保持不变。让我们看看当我们输入表达式 (x + 2)*(x + 3) 时会发生什么:

>>> p = (x + 2)*(x + 3)
>>> p
(x + 2)*(x + 3)

你可能预期 SymPy 会将所有内容展开并输出 x**2 + 5*x + 6。然而,表达式被打印得完全和我们输入的一样。SymPy 只会自动简化最基本的表达式,像前面的例子这种情况,需要程序员显式要求简化。如果你想将表达式展开以获得扩展版本,你需要使用 expand() 函数,稍后我们会看到如何使用它。

处理表达式

现在我们知道如何定义自己的符号表达式,接下来让我们了解如何在程序中使用它们。

因式分解与展开表达式

factor() 函数将表达式分解为它的因子,而 expand() 函数将表达式展开,表示为各个单独项的和。让我们用基本的代数恒等式 x² – y² = (x + y)(xy) 来测试这些函数。恒等式的左边是展开后的版本,右边则是相应的因式分解。由于我们有两个符号,接下来我们会创建两个 Symbol 对象:

>>> from sympy import Symbol
>>> x = Symbol('x')
>>> y = Symbol('y')

接下来,我们导入 factor() 函数,并用它将展开版本(恒等式左边)转换为因式分解版本(右边):

>>> from sympy import factor
>>> expr = x**2 - y**2
>>> factor(expr)
(x - y)*(x + y)

正如预期的那样,我们得到了表达式的因式分解版本。现在让我们展开这些因子,恢复成原来的展开版本:

>>> factors = factor(expr)
>>> expand(factors)
x**2 - y**2

我们将因式分解的表达式存储在一个新的标签 factors 中,然后用它调用 expand() 函数。当我们这样做时,我们会得到我们最初的表达式。让我们用更复杂的恒等式 x³ + 3x²y + 3xy² + y³ = (x + y)³ 来试试:

>>> expr = x**3 + 3*x**2*y + 3*x*y**2 + y**3
>>> factors = factor(expr)
>>> factors
(x + y)**3

>>> expand(factors)
x**3 + 3*x**2*y + 3*x*y**2 + y**3

factor() 函数能够对表达式进行因式分解,然后 expand() 函数会展开已因式分解的表达式,返回原始表达式。

如果你尝试对无法因式分解的表达式进行因式分解,factor() 函数会返回原始表达式。例如,见下:

>>> expr = x + y + x*y
>>> factor(expr)
x*y + x + y

同样,如果你将一个不能进一步展开的表达式传递给 expand(),它将返回相同的表达式。

漂亮打印

如果你希望我们一直在处理的表达式在打印时看起来更美观,可以使用 pprint() 函数。这个函数会以一种更接近我们通常在纸上书写的方式打印表达式。例如,下面是一个表达式:

>>> expr = x*x + 2*x*y + y*y

如果我们像之前那样打印,或者使用 print() 函数,结果如下所示:

>>> expr
x**2 + 2*x*y + y**2

现在,让我们使用 pprint() 函数打印上面的表达式:

>>> from sympy import pprint
>>> pprint(expr)
x2 + 2·x·y + y2

现在,表达式看起来干净多了——例如,指数出现在数字的上方,而不是一堆丑陋的星号。

你还可以在打印表达式时改变项的顺序。考虑表达式 1 + 2x + 2x²:

>>> expr = 1 + 2*x + 2*x**2
>>> pprint(expr)
2·x2 + 2·x + 1

这些项按 x 的幂次顺序排列,从最高到最低。如果你希望以相反的顺序打印表达式,即将 x 的最高次幂放在最后,可以通过以下方式使用 init_printing() 函数来实现:

>>> from sympy import init_printing
>>> init_printing(order='rev-lex')
>>> pprint(expr)
1 + 2·x + 2·x2

init_printing() 函数首先被导入并调用,使用关键字参数 order='rev-lex'。这表示我们希望 SymPy 打印表达式时按照 逆字典顺序 排列。在这种情况下,关键字参数告诉 Python 先打印低次幂的项。

注意

虽然我们在这里使用了 init_printing() 函数来设置表达式的打印顺序,但这个函数可以以许多其他方式使用,以配置如何打印表达式。欲了解更多选项以及如何在 SymPy 中打印,参见文档:docs.sympy.org/latest/tutorial/printing.html

让我们应用到目前为止学到的内容,来实现一个序列打印程序。

打印一个序列

考虑以下序列:

image

让我们编写一个程序,要求用户输入一个数字 n,并打印该数字的序列。在这个序列中,x 是一个符号,n 是程序用户输入的整数。该序列中的第 n 项由以下公式给出:

image

我们可以使用以下程序打印这个序列:

   '''
   Print the series:
   x + x**2 + x**3 + ... + x**n
       ____  _____         _____
         2    3              n
   '''

   from sympy import Symbol, pprint, init_printing
   def print_series(n):

       # Initialize printing system with reverse order
       init_printing(order='rev-lex')

       x = Symbol('x')
➊     series = x
➋         for i in range(2, n+1):
➌         series = series + (x**i)/i
       pprint(series)

   if __name__ == '__main__':
       n = input('Enter the number of terms you want in the series: ')
➍     print_series(int(n))

print_series() 函数接受一个整数 n 作为参数,这是将要打印的级数的项数。请注意,我们在调用该函数时,在 ➍ 处使用 int() 函数将输入转换为整数。然后,我们调用 init_printing() 函数,将级数设置为按反向字典顺序打印。

在 ➊,我们创建标签 series,并将其初始值设置为 x。然后,在 ➋,我们定义一个 for 循环,循环遍历从 2 到 n 的整数。每次循环迭代时,都会将每一项加到 series 中,具体如下:

i = 2, series = x + x**2 / 2
i = 3, series = x + x**2/2 + x**3/3

--snip--

series 的值开始时只是单纯的 x,但随着每次迭代,x**i/i 会被添加到 series 的值中,直到完成我们想要的级数。你可以看到 SymPy 加法在这里得到了很好的应用。最后,使用 pprint() 函数打印出级数。

当你运行程序时,它会提示你输入一个数字,然后打印出直到该项的级数:

Enter the number of terms you want in the series: 5

    x2 x3 x4 x5
x + -- + -- + -- + --
    2    3    4    5

每次尝试使用不同数量的项。接下来,我们将看到如何计算在特定值的x下,这个级数的和。

替代数值

让我们看看如何使用 SymPy 将数值代入代数表达式中。这将使我们能够计算在特定变量值下的表达式值。考虑以下数学表达式 x² + 2xy + y²,它可以定义如下:

>>> x = Symbol('x')
>>> y = Symbol('y')
>>> x*x + x*y + x*y + y*y
x**2 + 2*x*y + y**2

如果你想要计算这个表达式,你可以使用 subs() 方法将数字替代符号:

➊ >>> expr = x*x + x*y + x*y + y*y
   >>> res = expr.subs({x:1, y:2})

首先,我们创建一个新的标签来引用 ➊ 处的表达式,然后调用 subs() 方法。subs() 方法的参数是一个 Python 字典,其中包含两个符号标签和我们想要替代每个符号的数值。让我们来看看结果:

>>> res
9

你还可以将一个符号表示为另一个符号,并根据需要进行替代,使用 subs() 方法。例如,如果你知道 x = 1 – y,你可以通过以下方式计算前面的表达式:

>>> expr.subs({x:1-y})
y**2 + 2*y*(-y + 1) + (-y + 1)**2

PYTHON 字典

字典是 Python 中的另一种数据结构(列表和元组是其他数据结构的例子,你之前见过)。字典包含键值对,放在大括号内,其中每个键与一个值匹配,并通过冒号分隔。在前面的代码示例中,我们将字典 {x:1, y:2} 作为参数传递给 subs() 方法。这个字典包含两个键值对——x:1y:2,其中 xy 是键,12 是相应的值。你可以通过在方括号中输入关联的键来从字典中检索一个值,就像我们通过索引从列表中检索元素一样。例如,在这里我们创建了一个简单的字典,然后检索与 key1 相关联的值:

>>> sampledict = {"key1": 5, "key2": 20}
>>> sampledict["key1"]
5

要了解更多关于字典的内容,请参见 附录 B。

如果你希望进一步简化结果——例如,如果有些项可以互相抵消,我们可以使用 SymPy 的 simplify() 函数,方法如下:

➊ >>> expr_subs = expr.subs({x:1-y})
   >>> from sympy import simplify
➋ >>> simplify(expr_subs)
   1

在 ➊ 处,我们创建了一个新标签 expr_subs,用来表示将 x = 1 – y 代入表达式后的结果。然后我们从 SymPy 导入 simplify() 函数,并在 ➋ 处调用它。结果是 1,因为表达式中的其他项相互抵消了。

尽管在前面的示例中有一个简化版本的表达式,但你必须通过 simplify() 函数来请求 SymPy 对其进行简化。再一次,这是因为 SymPy 不会自动简化任何表达式,除非明确要求。

simplify() 函数还可以简化复杂的表达式,例如包括对数和三角函数的表达式,但我们这里不会深入讨论。

计算级数的值

让我们重新审视一下级数打印程序。除了打印级数之外,我们希望程序能够计算给定 x 值时,级数的和。也就是说,我们的程序现在将从用户那里接收两个输入——级数项数和要计算级数值的 x 值。然后,程序将输出级数和级数的和。以下程序扩展了级数打印程序,加入了这些功能:

   '''
   Print the series:
   x + x**2 + x**3 + ... + x**n
       ____  _____         _____
         2     3             n
   '''

   from sympy import Symbol, pprint, init_printing
   def print_series(n, x_value):

       # Initialize printing system with reverse order
       init_printing(order='rev-lex')

       x = Symbol('x')
       series = x
       for i in range(2, n+1):
           series = series + (x**i)/i

       pprint(series)

       # Evaluate the series at x_value
➊     series_value = series.subs({x:x_value})
       print('Value of the series at {0}: {1}'.format(x_value, series_value))

   if __name__ == '__main__':
       n = input('Enter the number of terms you want in the series: ')
➋     x_value = input('Enter the value of x at which you want to evaluate the series: ')

       print_series(int(n), float(x_value))

print_series() 函数现在需要一个额外的参数 x_value,它是我们用于计算级数的 x 值。在 ➊ 处,我们使用 subs() 方法来执行评估,并用标签 series_value 来表示结果。在接下来的代码行中,我们显示该结果。

在 ➋ 处新增的输入语句要求用户输入 x 的值,使用 x_value 标签来引用它。在调用 print_series() 函数之前,我们使用 float() 函数将该值转换为浮动点数。

如果现在运行程序,它将要求你输入这两个参数,并打印出级数和级数值:

Enter the number of terms you want in the series: 5
Enter the value of x at which you want to evaluate the series: 1.2

    x2 x3 x4 x5
x + -- + -- + -- + --
    2    3    4    5
Value of the series at 1.2: 3.51206400000000

在这个示例运行中,我们请求输入五项级数,其中 x 设置为 1.2,程序会打印并计算该级数。

将字符串转换为数学表达式

到目前为止,我们每次想要处理一个表达式时,都需要手动写出单独的表达式。但是,如果你想编写一个更通用的程序,能够处理用户提供的任何表达式呢?为此,我们需要一种方法,将用户输入的字符串转换为我们可以进行数学运算的形式。SymPy 的 sympify() 函数正是用来完成这个任务的。之所以称为“sympify”,是因为它将字符串转换为一个 SymPy 对象,这样就可以对输入应用 SymPy 的函数了。让我们看一个例子:

➊ >>> from sympy import sympify
   >>> expr = input('Enter a mathematical expression: ')
   Enter a mathematical expression: x**2 + 3*x + x**3 + 2*x
➋ >>> expr = sympify(expr)

我们首先在 ➊ 导入 sympify() 函数。然后,我们使用 input() 函数请求输入一个数学表达式,并使用标签 expr 来引用它。接下来,在 ➋ 我们调用 sympify() 函数,并将 expr 作为参数传入,使用相同的标签引用转换后的表达式。

你可以对这个表达式执行各种操作。例如,让我们尝试将表达式乘以 2:

>>> 2*expr
2*x**3 + 2*x**2 + 10*x

当用户提供无效的表达式时会发生什么呢?我们来看一下:

>>> expr = input('Enter a mathematical expression: ')
Enter a mathematical expression: x**2 + 3*x + x**3 + 2x
>>> expr = sympify(expr)
Traceback (most recent call last):
  File "<pyshell#146>", line 1, in <module>
    expr = sympify(expr)
  File "/usr/lib/python3.3/site-packages/sympy/core/sympify.py", line 180, in sympify
    raise SympifyError('could not parse %r' % a)
sympy.core.sympify.SympifyError: SympifyError: "could not parse 'x**2 + 3*x + x**3 + 2x'"

最后一行告诉我们 sympify() 无法转换提供的输入表达式。由于这个用户没有在 2x 之间加上运算符,SymPy 不明白它的意思。你的程序应该预期到这种无效输入,并在出现时打印错误信息。我们来看一下如何通过捕获 SympifyError 异常来实现:

>>> from sympy import sympify
>>> from sympy.core.sympify import SympifyError
>>> expr = input('Enter a mathematical expression: ')
Enter a mathematical expression: x**2 + 3*x + x**3 + 2x
>>> try:
    expr = sympify(expr)
except SympifyError:
    print('Invalid input')

Invalid input

前面程序的两个改动是,我们从 sympy.core.sympify 模块导入了 SympifyError 异常类,并在 try...except 块中调用了 sympify() 函数。现在,如果出现 SympifyError 异常,就会打印错误信息。

表达式乘法器

让我们应用 sympify() 函数编写一个程序来计算两个表达式的乘积:

   '''
   Product of two expressions
   '''

   from sympy import expand, sympify
   from sympy.core.sympify import SympifyError

   def product(expr1, expr2):
       prod = expand(expr1*expr2)
       print(prod)

   if __name__=='__main__':
➊     expr1 = input('Enter the first expression: ')
➋     expr2 = input('Enter the second expression: ')

       try:
           expr1 = sympify(expr1)
           expr2 = sympify(expr2)
       except SympifyError:
           print('Invalid input')
       else:
➌         product(expr1, expr2)

在 ➊ 和 ➋,我们要求用户输入两个表达式。然后,我们使用 sympify() 函数将其转换为 SymPy 可以理解的形式,并放在 try...except 块中。如果转换成功(由 else 块指示),我们在 ➌ 调用 product() 函数。在这个函数中,我们计算两个表达式的乘积并打印出来。请注意,我们如何使用 expand() 函数打印乘积,使得所有的项都作为其组成项的和来表示。

下面是程序的示例执行:

Enter the first expression: x**2 + x*2 + x
Enter the second expression: x**3 + x*3 + x
x**5 + 3*x**4 + 4*x**3 + 12*x**2

最后一行显示了两个表达式的乘积。输入中也可以包含多个符号在任意一个表达式中:

Enter the first expression: x*y+x
Enter the second expression: x*x+y
x**3*y + x**3 + x*y**2 + x*y

求解方程

SymPy 的 solve() 函数可以用来求解方程。当你输入一个包含表示变量的符号(如 x)的表达式时,solve() 会计算该符号的值。此函数总是通过假设你输入的表达式等于零来进行计算——也就是说,它会输出当该符号被代入时,使整个表达式等于零的值。让我们从简单的方程 x – 5 = 7 开始。如果我们想使用 solve() 来求解 x 的值,我们首先需要将方程的一边变为零(x – 5 – 7 = 0)。然后,我们就可以使用 solve() 了,如下所示:

>>> from sympy import Symbol, solve
>>> x = Symbol('x')
>>> expr = x - 5 - 7
>>> solve(expr)
[12]

当我们使用 solve() 时,它计算出 'x' 的值为 12,因为这是使得表达式 (x – 5 – 7) 等于零的值。

注意,结果 12 以列表的形式返回。一个方程可以有多个解——例如,一个二次方程有两个解。在这种情况下,列表会包含所有解作为其成员。你还可以要求solve()函数返回结果,每个成员作为字典。每个字典由符号(变量名)及其值(解)组成。当求解联立方程时,这种方式特别有用,因为我们有多个变量需要求解,返回字典格式的解能够帮助我们知道每个解对应哪个变量。

求解二次方程

在第一章中,我们通过写出二次方程ax² + bx + c = 0 的两根公式,并代入常数abc的值来求解方程的根。现在,我们将学习如何使用 SymPy 的solve()函数来求解根,而无需写出公式。让我们看一个例子:

➊ >>> from sympy import solve
   >>> x = Symbol('x')
➋ >>> expr = x**2 + 5*x + 4
➌ >>> solve(expr, dict=True)
➍ [{x: -4}, {x: -1}]

solve()函数首先在➊处被导入。然后,我们在➋处定义一个符号x,并编写与二次方程x**2 + 5*x + 4相对应的表达式。接着,在➌处调用solve()函数来解这个方程。传递给solve()函数的第二个参数(dict=True)指定我们希望结果以 Python 字典的列表形式返回。

返回列表中的每个解都是一个字典,字典使用符号作为键,并与其对应的值匹配。如果解为空,将返回一个空列表。前面方程的根是-4 和-1,如你在➍处看到的那样。

我们在第一章中发现,方程的根是

x² + x + 1 = 0

这些是复数。我们来尝试使用solve()求解它们:

>>> x=Symbol('x')
>>> expr = x**2 + x + 1
>>> solve(expr, dict=True)
[{x: -1/2 - sqrt(3)*I/2}, {x: -1/2 + sqrt(3)*I/2}]

这两个根都是虚数,正如预期的那样,虚部由I符号表示。

求解一个变量并表示为其他变量的函数

除了求解方程的根,我们还可以利用符号计算,使用solve()函数将方程中一个变量用其他变量表示。让我们来看一个求解通用二次方程ax² + bx + c = 0 的例子。为此,我们将定义x和三个额外的符号——abc,它们分别对应三个常数:

>>> x = Symbol('x')
>>> a = Symbol('a')
>>> b = Symbol('b')
>>> c = Symbol('c')

接下来,我们编写与方程对应的表达式,并对其使用solve()函数:

>>> expr = a*x*x + b*x + c
>>> solve(expr, x, dict=True)
[{x: (-b + sqrt(-4*a*c + b**2))/(2*a)}, {x: -(b + sqrt(-4*a*c + b**2))/(2*a)}]

在这里,我们必须为solve()函数添加一个额外的参数x。因为方程中有多个符号,我们需要告诉solve()应解哪个符号,这就是为什么我们将x作为第二个参数传递给它。正如我们所预期的,solve()打印出了二次公式:用于求解多项式表达式中x值的通用公式。

为了明确,当我们对包含多个符号的方程使用solve()时,我们将要求解的符号作为第二个参数指定(而现在,第三个参数指定我们希望如何返回结果)。

接下来,让我们考虑一个物理学中的例子。根据运动方程之一,物体在初速度u和恒定加速度a下,在时间t内所走的距离s可以表示为:

image

然而,给定ua,如果你想找出在给定距离s下所需的时间t,你必须首先将t表示为其他变量的函数。下面是如何使用 SymPy 的solve()函数来做到这一点:

>>> from sympy import Symbol, solve, pprint
>>> s = Symbol('s')
>>> u = Symbol('u')
>>> t = Symbol('t')
>>> a = Symbol('a')
>>> expr = u*t + (1/2)*a*t*t - s
>>> t_expr = solve(expr,t, dict=True)
>>> pprint(t_expr)

结果如下所示:

image

现在,我们有了t的表达式(由标签t_expr表示),可以使用subs()方法替换sua的值,从而找到t的两个可能值。

求解线性方程组

考虑以下两个方程:

2x + 3y = 6

3x + 2y = 12

假设我们想找到满足这两个方程的值对(x, y)。我们可以使用solve()函数来求解像这样的方程组。

首先,我们定义这两个符号并创建这两个方程:

>>> x = Symbol('x')
>>> y = Symbol('y')
>>> expr1 = 2*x + 3*y - 6
>>> expr2 = 3*x + 2*y – 12

这两个方程分别由expr1expr2表示。注意我们是如何重新排列表达式,使它们都等于零(我们将给定方程的右边移到了左边)。为了找到解,我们调用solve()函数,并将这两个表达式组成一个元组:

>>> solve((expr1, expr2), dict=True)
[{y: -6/5, x: 24/5}]

正如我之前提到的,得到一个字典形式的解在这里是非常有用的。我们可以看到x的值是 24/5,y的值是–6/5。让我们验证一下我们得到的解是否真的满足这些方程。为此,我们首先创建一个标签soln来表示我们得到的解,然后使用subs()方法将xy的对应值代入两个表达式中:

>>> soln = solve((expr1, expr2), dict=True)
>>> soln = soln[0]
>>> expr1.subs({x:soln[x], y:soln[y]})
0
>>> expr2.subs({x:soln[x], y:soln[y]})
0

xy的值代入两个表达式中得到的结果是零。

使用 SymPy 绘图

在第二章中,我们学会了绘制图形,其中我们明确指定了要绘制的数字。例如,要绘制重力与两个物体之间距离的关系图,你需要为每个距离值计算重力,并将距离和重力的列表提供给 matplotlib。而使用 SymPy,你只需要告诉 SymPy 你想绘制的直线方程,图形就会为你创建出来。让我们绘制一个方程为y = 2x + 3 的直线:

>>> from sympy.plotting import plot
>>> from sympy import Symbol
>>> x = Symbol('x')
>>> plot(2*x+3)

我们只需要导入plotSymbol,创建一个符号x,然后调用plot()函数,并传入表达式2*x+3。SymPy 会处理其余的工作,绘制出该函数的图形,如图 4-1 所示。

image

图 4-1: y = 2x + 3 的线性图

图表显示了一个自动选择的默认x值范围:-10 到 10。你可能注意到,图表窗口看起来与第二章和第三章中看到的非常相似。这是因为 SymPy 在后台使用 matplotlib 来绘制图表。还要注意,我们不需要调用show()函数来显示图表,因为 SymPy 会自动完成这项工作。

假设你想将前面图表中'x'的值限制在-5 到 5 的范围内(而不是-10 到 10)。你可以按照以下方式进行:

>>> plot((2*x + 3), (x, -5, 5))

这里,指定了一个包含符号、下限和上限范围的元组——(x, -5, 5),作为plot()函数的第二个参数。现在,图表仅显示与-5 到 5 之间x值对应的y值(见图 4-2)。

image

图 4-2: y = 2x + 3 的线性图,x的值限制在-5 到 5 的范围内

你可以在plot()函数中使用其他关键字参数,比如title来输入标题,或者使用xlabelylabel来分别标记x-轴和y-轴。以下plot()函数指定了前面提到的三个关键字参数(参见图 4-3 中的相应图表):

>>> plot(2*x + 3, (x, -5, 5), title='A Line', xlabel='x', ylabel='2x+3')

image

图 4-3: y = 2x + 3 的线性图,x的范围和其他属性已指定

在图 4-3 中显示的图表现在有了标题和x-轴与y-轴的标签。你还可以向plot()函数指定其他一些关键字参数,以自定义函数和图表本身的行为。show关键字参数允许我们指定是否希望显示图表。当你调用plot()函数时,传入show=False将导致图表不显示:

>>> p = plot(2*x + 3, (x, -5, 5), title='A Line', xlabel='x', ylabel='2x+3', show=False)

你会看到没有图表显示。标签p指的是创建的图表,因此你现在可以调用p.show()来显示图表。你还可以使用save()方法将图表保存为图像文件,如下所示:

>>> p.save('line.png')

这将把图表保存为当前目录下的line.png文件。

用户输入的表达式绘图

你传递给plot()函数的表达式必须仅以x为变量。例如,我们之前绘制了y = 2x + 3,并将其直接作为 2x + 3 输入到plot()函数中。如果表达式最初不是这个形式,我们就需要重写它。当然,我们可以在程序外部手动完成这个过程。但是,如果你想编写一个允许用户绘制任意表达式的程序呢?假设用户输入一个形式为 2x + 3y – 6 的表达式,我们就需要先将其转换。solve()函数可以在这里帮助我们。我们来看一个例子:

   >>> expr = input('Enter an expression: ')
   Enter an expression: 2*x + 3*y - 6
➊ >>> expr = sympify(expr)
➋ >>> y = Symbol('y')
   >>> solve(expr, y)
➌ [-2*x/3 + 2]

在 ➊ 处,我们使用 sympify() 函数将输入的表达式转换为 SymPy 对象。在 ➋ 处,我们创建一个 Symbol 对象来表示 'y',以便告诉 SymPy 我们希望解方程的变量。然后,我们通过将 y 作为第二个参数传递给 solve() 函数来解出表达式,从而找到 y 关于 x 的解。 在 ➌ 处,这将返回以 x 为变量的方程,这是绘图所需要的。

请注意,这个最终的表达式存储在一个列表中,因此在使用它之前,我们必须从列表中提取它:

   >>> solutions = solve(expr, 'y')
➍ >>> expr_y = solutions[0]
   >>> expr_y
   -2*x/3 + 2

我们创建一个标签 solutions,用来表示 solve() 函数返回的结果,它是一个仅包含一个项的列表。然后,我们在 ➍ 处提取该项。现在,我们可以调用 plot() 函数来绘制该表达式。接下来的代码展示了一个完整的绘图程序:

'''
Plot the graph of an input expression
'''

from sympy import Symbol, sympify, solve
from sympy.plotting import plot

def plot_expression(expr):

    y = Symbol('y')
    solutions = solve(expr, y)
    expr_y = solutions[0]
    plot(expr_y)

if __name__=='__main__':

    expr = input('Enter your expression in terms of x and y: ')

    try:
        expr = sympify(expr)
    except SympifyError:
        print('Invalid input')
    else:
        plot_expression(expr)

请注意,前面的程序包括一个 try...except 块来检查无效输入,就像我们之前使用 sympify() 一样。当您运行程序时,它会要求您输入一个表达式,并创建相应的图表。

绘制多个函数

您可以在调用 SymPy 的 plot 函数时输入多个表达式,将多个表达式绘制在同一张图上。例如,以下代码一次绘制两条线(见 图 4-4):

>>> from sympy.plotting import plot
>>> from sympy import Symbol
>>> x = Symbol('x')
>>> plot(2*x+3, 3*x+1)

image

图 4-4:在同一张图上绘制两条线

这个例子展示了在 matplotlib 和 SymPy 中绘图的另一个区别。在 SymPy 中,两个线条是相同颜色的,而 matplotlib 会自动使线条呈现不同颜色。要在 SymPy 中为每条线设置不同的颜色,我们需要执行一些额外的步骤,如下所示的代码,这段代码还向图表中添加了图例:

   >>> from sympy.plotting import plot
   >>> from sympy import Symbol
   >>> x = Symbol('x')
➊ >>> p = plot(2*x+3, 3*x+1, legend=True, show=False)
➋ >>> p[0].line_color = 'b'
➌ >>> p[1].line_color = 'r'
   >>> p.show()

在 ➊ 处,我们调用 plot() 函数,并传入两个线条的方程式,同时传递两个额外的关键字参数——legendshow。通过将 legend 参数设置为 True,我们向图表中添加了图例,就像在第二章中看到的那样。不过请注意,图例中显示的文本将与您绘制的表达式一致——您不能指定其他文本。我们还将 show=False 设置为 False,因为我们希望在绘制图表之前先设置线条的颜色。在 ➋ 处的语句 p[0] 指代第一个线条 2x + 3,我们将其属性 line_color 设置为 'b',表示我们希望这条线是蓝色的。同样,我们使用字符串 'r' 设置第二条线的颜色为红色 ➌。最后,我们调用 show() 来显示图表(见 图 4-5)。

image

图 4-5:两条线的绘制,每条线采用不同的颜色

除了红色和蓝色,您还可以使用绿色、青色、品红色、黄色、黑色和白色绘制线条(每种颜色使用该颜色的首字母)。

您学到的内容

在本章中,你学习了使用 SymPy 进行符号数学的基础知识。你了解了如何声明符号、使用符号和数学运算符构建表达式、求解方程和绘制图形。在后续章节中,你将学习 SymPy 的更多功能。

编程挑战

以下是一些编程挑战,帮助你进一步应用所学的知识。你可以在www.nostarch.com/doingmathwithpython/找到示例解答。

#1: 因数查找器

你学会了factor()函数,它用于打印表达式的因数。现在你知道程序如何处理用户输入的表达式,写一个程序,让用户输入一个表达式,计算它的因数并打印出来。你的程序应该能够通过异常处理来应对无效的输入。

#2: 图形方程求解器

之前,你学会了如何编写一个程序,让用户输入类似于 3x + 2y - 6 的表达式并绘制相应的图形。编写一个程序,要求用户输入两个表达式,然后同时绘制它们的图形,示例如下:

>>> expr1 = input('Enter your first expression in terms of x and y: ')
>>> expr2 = input('Enter your second expression in terms of x and y: ')

现在,expr1expr2将存储用户输入的两个表达式。你应该使用try...except块中的sympify()步骤,将这两个表达式转换为 SymPy 对象。

接下来,你只需要绘制这两个表达式,而不是一个。

完成此操作后,增强程序的功能,打印出解—即同时满足这两个方程的xy值。这也将是图中两条线交点的位置。(提示:参考我们如何使用solve()函数来求解两个线性方程组的解。)

#3: 求序列和

我们在《打印序列》中看到过如何求一个序列的和,该内容位于第 99 页。在那里,我们通过循环遍历所有项手动相加。以下是该程序的一部分:

for i in range(2, n+1):
    series = series + (x**i)/i

SymPy 的summation()函数可以直接用于求和。以下示例打印了我们之前讨论的序列前五项的和:

   >>> from sympy import Symbol, summation, pprint
   >>> x = Symbol('x')
   >>> n = Symbol('n')
➊ >>> s = summation(x**n/n, (n, 1, 5))
   >>> pprint(s)
   x5   x4   x3   x2
   -- + -- + -- + -- + x
   5     4   3     2

我们在➊处调用了summation()函数,第一个参数是序列的第 n项,第二个参数是一个元组,表示n的范围。我们这里要求前五项的和,因此第二个参数为(n, 1, 5)

一旦得到了和,你可以使用subs()方法为x替换一个值,从而得到和的数值:

>>> s.subs({x:1.2})
3.51206400000000

你的挑战是编写一个程序,能够根据你提供的序列的第 n项和项数,求出该序列的和。以下是程序如何工作的示例:

Enter the nth term: a+(n-1)*d
Enter the number of terms: 3
3·a + 3·d

在这个例子中,提供的第 n 项是一个 等差数列。从ad作为 公差 开始,求和的项数为 3。求和结果是 3a + 3d,这与已知的公式一致。

#4: 解单变量不等式

你已经学会了如何使用 SymPy 的solve()函数解方程。但是,SymPy 也能够解单变量不等式,例如 x + 5 > 3 和 sinx – 0.6 > 0。也就是说,SymPy 不仅能解等式,还能解不等式,如 >、< 等。因此,创建一个名为isolve()的函数,它可以接受任何不等式,解出并返回解决方案。

首先,让我们了解一下 SymPy 中可以帮助你实现这些功能的函数。解不等式的函数分为三个独立的函数:用于多项式、不等式和其他所有不等式的函数。我们需要选择正确的函数来解不同的不等式,否则会遇到错误。

多项式是由一个变量和系数组成的代数表达式,仅涉及加法、减法和乘法运算,并且只有变量的正整数次幂。一个多项式不等式的例子是 x² + 4 < 0。

要解多项式不等式,使用 solve_poly_inequality() 函数:

   >>> from sympy import Poly, Symbol, solve_poly_inequality
   >>> x = Symbol('x')
➊ >>> ineq_obj = -x**2 + 4 < 0
➋ >>> lhs = ineq_obj.lhs
➌ >>> p = Poly(lhs, x)
➍ >>> rel = ineq_obj.rel_op
   >>> solve_poly_inequality(p, rel)
   [(-oo, -2), (2, oo)]

首先,在 ➊ 创建表示不等式的表达式 –x² + 4 < 0,并用标签ineq_obj来引用此表达式。接着,使用 lhs 属性在 ➋ 提取不等式的左边部分——也就是代数表达式 –x² + 4。然后,在 ➌ 创建一个 Poly 对象来表示我们在 ➋ 提取的多项式。创建对象时传入的第二个参数是代表变量 x 的符号对象。接下来,在 ➍ 使用 rel 属性从不等式对象中提取关系运算符。最后,使用 solve_poly_inequality() 函数,传入多项式对象 p 和关系符号 rel 作为两个参数。程序将返回一个由元组组成的解决方案列表,每个元组表示不等式的解,包含数值范围的下限和上限。对于这个不等式,解是所有小于 –2 的数和所有大于 2 的数。

有理表达式是一个代数表达式,其中分子和分母都是多项式。这里有一个有理不等式的例子:

image

对于有理不等式,使用 solve_rational_inequalities() 函数:

   >>> from sympy import Symbol, Poly, solve_rational_inequalities
   >>> x = Symbol('x')
➊ >>> ineq_obj = ((x-1)/(x+2)) > 0
   >>> lhs = ineq_obj.lhs
➋ >>> numer, denom = lhs.as_numer_denom()
   >>> p1 = Poly(numer)
   >>> p2 = Poly(denom)
   >>> rel = ineq_obj.rel_op
➌ >>> solve_rational_inequalities([[((p1, p2), rel)]])
   (-oo, -2) U (1, oo)

创建一个表示我们示例有理不等式的对象,位于➊,然后使用lhs属性提取有理表达式。通过as_numer_denom()方法在➋处将分子和分母分别提取为numerdenom标签,它会返回一个包含分子和分母的元组。然后,创建两个多项式对象p1p2,分别表示分子和分母。获取关系运算符并调用solve_rational_inequalities()函数,将两个多项式对象——p1p2——以及关系运算符传递给它。

程序返回解(-oo, -2) U (1, oo),其中U表示解是两个集合并集,这些集合分别包含所有小于–2 的数和所有大于 1 的数。(我们将在第五章中学习集合。)

最后,sinx – 0.6 > 0 是一个既不属于多项式也不属于有理表达式类别的不等式示例。如果你有这样的不等式需要求解,可以使用solve_univariate_inequality()函数:

>>> from sympy import Symbol, solve, solve_univariate_inequality, sin
>>> x = Symbol('x')
>>> ineq_obj = sin(x) - 0.6 > 0
>>> solve_univariate_inequality(ineq_obj, x, relational=False)
(0.643501108793284, 2.49809154479651)

创建一个表示不等式sin(x) – 0.6 > 0的不等式对象,然后调用solve_univariate_inequality()函数,第一个和第二个参数为不等式对象ineq_obj和符号对象x。关键字参数relational=False指定函数返回解时应以集合的形式返回。该不等式的解最终是所有位于程序返回的元组的第一个和第二个成员之间的数字。

提示:实用函数

现在记住——你的挑战是(1)创建一个函数isolve(),它将接受任何不等式,(2) 选择本节中讨论的适当函数来求解它并返回解。以下提示可能对实现此函数有所帮助。

is_polynomial()方法可以用来检查一个表达式是否为多项式:

>>> x = Symbol('x')
>>> expr = x**2 - 4
>>> expr.is_polynomial()
True
>>> expr = 2*sin(x) + 3
>>> expr.is_polynomial()
False

is_rational_function()可以用来检查一个表达式是否为有理表达式:

>>> expr = (2+x)/(3+x)
>>> expr.is_rational_function()
True
>>> expr = 2+x
>>> expr.is_rational_function()
True
>>> expr = 2+sin(x)
>>> expr.is_rational_function()
False

sympify()函数可以将表示为字符串的不等式转换为不等式对象:

>>> from sympy import sympify
>>> sympify('x+3>0')
x + 3 > 0

当你运行程序时,它应该要求用户输入不等式表达式并打印出解。

第五章:玩转集合与概率

image

在这一章中,我们将首先学习如何使我们的程序理解和操作数字集合。接着,我们将看到集合如何帮助我们理解概率的基本概念。最后,我们将学习如何生成随机数以模拟随机事件。让我们开始吧!

什么是集合?

集合是一个包含不同对象的集合,通常称为元素成员。集合有两个特点使它不同于任何其他对象的集合。集合是“明确定义”的,这意味着问题“某个特定对象是否在这个集合中?”总是有一个明确的“是”或“不是”的答案,通常基于某种规则或给定的标准。第二个特点是集合的任何两个成员都不相同。集合可以包含任何东西——数字、人物、事物、单词等等。

让我们通过一些基本的集合特性来学习如何使用 SymPy 在 Python 中操作集合。

集合构造

在数学符号中,集合通常通过将集合成员括在大括号中表示。例如,{2, 4, 6}表示一个包含 2、4 和 6 作为成员的集合。要在 Python 中创建集合,我们可以使用来自sympy包的FiniteSet类,如下所示:

>>> from sympy import FiniteSet
>>> s = FiniteSet(2, 4, 6)
>>> s
{2, 4, 6}

在这里,我们首先从 SymPy 导入FiniteSet类,然后通过将集合成员作为参数传递来创建该类的一个对象。我们将标签s赋给我们刚创建的集合。

我们可以在同一个集合中存储不同类型的数字——包括整数、浮点数和分数:

>>> from sympy import FiniteSet
>>> from fractions import Fraction
>>> s = FiniteSet(1, 1.5, Fraction(1, 5))
>>> s
{1/5, 1, 1.5}

集合的基数是集合中成员的数量,你可以通过使用len()函数来找到:

>>> s = FiniteSet(1, 1.5, 3)
>>> len(s)
3
检查一个数字是否在集合中

要检查一个数字是否是已存在集合的成员,可以使用in运算符。这个运算符会问 Python:“这个数字是否在这个集合中?”如果数字属于该集合,它返回True,如果不属于,则返回False。例如,如果我们想检查 4 是否在之前的集合中,我们可以这样做:

>>> 4 in s
False

由于 4 不在集合中,因此运算符返回False

创建空集合

如果你想创建一个空集合,即没有任何元素或成员的集合,只需创建一个不传递任何参数的FiniteSet对象。结果是一个EmptySet对象:

>>> s = FiniteSet()
>>> s
EmptySet()
从列表或元组创建集合

你还可以通过将一个列表或元组作为集合成员传递给FiniteSet来创建一个集合:

>>> members = [1, 2, 3]
>>> s = FiniteSet(*members)
>>> s
{1, 2, 3}

在这里,我们并没有直接将集合成员传递给FiniteSet,而是先将它们存储在一个我们称为members的列表中。然后,我们使用这种特殊的 Python 语法将列表传递给FiniteSet,这基本上相当于创建一个FiniteSet对象,并将列表成员作为单独的参数传递,而不是作为列表。也就是说,这种创建FiniteSet对象的方法等同于FiniteSet(1, 2, 3)。当集合成员在运行时计算时,我们将使用这种语法。

集合的重复性与顺序

Python 中的集合(像数学中的集合一样)会忽略成员的重复项,并且不跟踪集合成员的顺序。例如,如果你从一个包含多个相同数字的列表中创建集合,那么该数字只会被添加一次,其他重复的实例会被丢弃:

>>> from sympy import FiniteSet
>>> members = [1, 2, 3, 2]
>>> FiniteSet(*members)
{1, 2, 3}

在这里,即使我们传入了一个包含两个数字 2 的列表,但从该列表创建的集合中,数字 2 只出现了一次。

在 Python 列表和元组中,每个元素都是按特定顺序存储的,但集合并不总是如此。例如,我们可以通过以下方式遍历集合并打印出其中的每个成员:

>>> from sympy import FiniteSet
>>> s = FiniteSet(1, 2, 3)
>>> for member in s:
        print(member)

2
1
3

当你运行这段代码时,元素可能会以任何可能的顺序打印出来。这是因为集合在 Python 中的存储方式——它跟踪集合中包含的成员,但并不跟踪这些成员的任何特定顺序。

让我们看另一个例子。当两个集合具有相同的元素时,它们是 相等 的。在 Python 中,你可以使用相等运算符 == 来检查两个集合是否相等:

>>> from sympy import FiniteSet
>>> s = FiniteSet(3, 4, 5)
>>> t = FiniteSet(5, 4, 3)
>>> s == t
True

虽然这两个集合的成员顺序不同,但它们仍然是相等的。

子集、超集和幂集

如果集合 s 中的所有成员也都是集合 t 的成员,那么集合 s 就是集合 t子集。例如,集合 {1} 是集合 {1, 2} 的子集。你可以使用 is_subset() 方法来检查一个集合是否是另一个集合的子集:

>>> s = FiniteSet(1)
>>> t = FiniteSet(1,2)
>>> s.is_subset(t)
True
>>> t.is_subset(s)
False

请注意,空集合是每个集合的子集。同时,任何集合都是自身的子集,正如你在以下示例中看到的:

>>> s.is_subset(s)
True
>>> t.is_subset(t)
True

同样,如果集合 t 包含了集合 s 中的所有成员,那么集合 t 就是集合 s超集。你可以使用 is_superset() 方法来检查一个集合是否是另一个集合的超集:

>>> s.is_superset(t)
False
>>> t.is_superset(s)
True

集合 s幂集s 所有可能子集的集合。任何集合 s 的子集数量正好是 2^(|s|),其中 |s| 是集合的基数。例如,集合 {1, 2, 3} 的基数为 3,所以它有 2³ 或 8 个子集:{}(空集合)、{1}、{2}、{3}、{1, 2}、{2, 3}、{1, 3} 和 {1, 2, 3}。

所有这些子集的集合形成了幂集,我们可以使用 powerset() 方法来找出幂集:

>>> s = FiniteSet(1, 2, 3)
>>> ps = s.powerset()
>>> ps
{{1}, {1, 2}, {1, 3}, {1, 2, 3}, {2}, {2, 3}, {3}, EmptySet()}

由于幂集本身就是一个集合,你可以使用 len() 函数来找出它的基数:

>>> len(ps)
8

幂集的基数是 2^(|s|),即 2³ = 8。

根据我们对子集的定义,任何两个具有完全相同成员的集合,既是彼此的子集,又是彼此的超集。相反,集合 s 只有在 t 中包含 s 的所有成员,并且 t 至少有一个不在 s 中的成员时,s 才是 t真子集。例如,如果 s = {1, 2, 3},它只有在 t 包含 1、2 和 3,并且还有至少一个额外的成员时,才是 t 的真子集。这也意味着 ts真超集。你可以使用 is_proper_subset() 方法和 is_proper_superset() 方法来检查这些关系:

>>> from sympy import FiniteSet
>>> s = FiniteSet(1, 2, 3)
>>> t = FiniteSet(1, 2, 3)
>>> s.is_proper_subset(t)
False
>>> t.is_proper_superset(s)
False

现在,如果我们重新创建集合t,使其包含另一个成员,则s将被认为是t的真子集,而ts的真超集:

>>> t = FiniteSet(1, 2, 3, 4)
>>> s.is_proper_subset(t)
True
>>> t.is_proper_superset(s)
True

常见数集

在第一章中,我们学习了不同种类的数字——整数、浮动点数、分数和复数。所有这些数字构成不同的数集,并且它们有特殊的名称。

所有正整数和负整数构成整数集合。所有正整数构成自然数集合(有时,0 也包括在这个集合中,尽管它不是正数,但有时也不包括)。这意味着自然数集合是整数集合的一个真子集。

有理数集合包括所有可以表示为分数的数字,这包括所有整数,以及任何终止或重复的小数(包括像 1/4 或 0.25,以及 1/3 或 0.33333 ...这样的数字)。相比之下,不重复、不终止的小数被称为无理数。2 的平方根和π都是无理数的例子,因为它们永远不会重复且无限延续。

如果你将所有有理数和无理数放在一起,你就得到了实数集合。比这更大的集合是复数集合,它包括所有实数以及所有包含虚部的数字。

这些数集都是无限集,因为它们包含无限个成员。相比之下,我们在本章中讨论的集合具有有限个成员,这也是我们使用的 SymPy 类被称为FiniteSet的原因。

集合运算

集合运算,如并集、交集和笛卡尔积,允许你以某种有规律的方式组合集合。当我们需要一起考虑多个集合时,这些集合运算在现实世界的解决问题中非常有用。稍后在本章中,我们将看到如何使用这些运算将公式应用于多个数据集合,并计算随机事件的概率。

并集与交集

两个集合的并集是一个包含这两个集合所有不同成员的集合。在集合论中,我们使用符号∪来表示并集运算。例如,{1, 2} ∪ {2, 3}将得到一个新集合{1, 2, 3}。在 SymPy 中,这两个集合的并集可以通过union()方法创建:

>>> from sympy import FiniteSet
>>> s = FiniteSet(1, 2, 3)
>>> t = FiniteSet(2, 4, 6)
>>> s.union(t)
{1, 2, 3, 4, 6}

我们通过对s应用union方法并将t作为参数传递给s,来找到st的并集。结果是一个包含这两个集合所有不同成员的第三个集合。换句话说,这个第三个集合中的每个成员都是前两个集合中一个或两个的成员。

两个集合的交集从两个集合中共有的元素创建一个新的集合。例如,集合{1, 2}和{2, 3}的交集将结果为一个包含唯一公共元素{2}的新集合。在数学中,这个运算表示为{1, 2} ∩ {2, 3}。

在 SymPy 中,使用intersect()方法来查找交集:

>>> s = FiniteSet(1, 2)
>>> t = FiniteSet(2, 3)
>>> s.intersect(t)
{2}

而并集操作找到的是属于一个集合另一个集合的成员,交集操作找到的是同时存在于两个集合中的元素。这两种操作也可以应用于超过两个集合。例如,这是你如何找到三个集合的并集:

>>> from sympy import FiniteSet
>>> s = FiniteSet(1, 2, 3)
>>> t = FiniteSet(2, 4, 6)
>>> u = FiniteSet(3, 5, 7)
>>> s.union(t).union(u)
{1, 2, 3, 4, 5, 6, 7}

类似地,这是你如何找到三个集合交集的方法:

>>> s.intersect(t).intersect(u)
EmptySet()

集合stu的交集实际上是一个空集合,因为没有任何元素是这三个集合共有的。

笛卡尔积

两个集合的笛卡尔积创建了一个由每个集合中取一个元素组成的所有可能的对组成的集合。例如,集合{1, 2}和{3, 4}的笛卡尔积是{(1, 3), (1, 4), (2, 3), (2, 4)}。在 SymPy 中,你可以通过简单地使用乘法运算符来找到两个集合的笛卡尔积:

>>> from sympy import FiniteSet
>>> s = FiniteSet(1, 2)
>>> t = FiniteSet(3, 4)
>>> p = s*t
>>> p
{1, 2} x {3, 4}

这会获取集合st的笛卡尔积,并将其存储为p。为了实际查看笛卡尔积中的每一对,我们可以通过遍历并打印它们,如下所示:

>>> for elem in p:
        print(elem)
(1, 3)
(1, 4)
(2, 3)
(2, 4)

这个笛卡尔积的每个元素是一个元组,包含来自第一个集合的成员和来自第二个集合的成员。

笛卡尔积的基数是各个集合基数的乘积。我们可以在 Python 中演示这一点:

>>> len(p) == len(s)*len(t)
True

如果我们对一个集合应用指数运算符(**),我们得到的是该集合与自身的笛卡尔积,次数由指定的次数决定。

>>> from sympy import FiniteSet
>>> s = FiniteSet(1, 2)
>>> p = s**3
>>> p
{1, 2} x {1, 2} x {1, 2}

例如,这里我们将集合s的指数提升到 3。由于我们正在进行三个集合的笛卡尔积,这给我们带来一个包含每个集合成员的所有可能三元组的集合:

>>> for elem in p:
        print(elem)
(1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 2, 2)
(2, 1, 1)
(2, 1, 2)
(2, 2, 1)
(2, 2, 2)

查找集合的笛卡尔积对于找出集合成员的所有可能组合非常有用,接下来我们将探讨这一点。

将公式应用于多个变量集合

考虑一个长度为L的简单摆。这个摆的周期T,即摆完成一个完整摆动所需的时间,可以通过以下公式给出:

image

这里,π是数学常数pig是局部的重力加速度,地球上的值大约是 9.8 m/s²。由于πg是常数,长度L是方程右边唯一一个没有常数值的变量。

如果你想查看一个简单摆的周期如何随其长度变化,你可以假设不同的长度值,并使用公式测量每个这些值对应的周期。一个典型的高中实验是将你用前面的公式得到的周期(理论结果)与实验室测量的周期(实验结果)进行比较。例如,我们选择五个不同的长度值:15、18、21、22.5 和 25(单位是厘米)。使用 Python,我们可以编写一个快速程序来加速理论结果的计算:

   from sympy import FiniteSet, pi
➊ def time_period(length):
       g = 9.8
       T = 2*pi*(length/g)**0.5
       return T

   if __name__ == '__main__':
➋     L = FiniteSet(15, 18, 21, 22.5, 25)
       for l in L:
➌         t = time_period(l/100)
           print('Length: {0} cm Time Period: {1:.3f} s'. format(float(l), float(t)))

我们首先在➊定义了time_period函数。这个函数将前面展示的公式应用于给定的长度,长度作为length传入。然后,我们的程序在➋定义了一个长度集合,并在➌将time_period函数应用于每个值。注意,当我们将长度值传递给time_period时,我们将其除以 100。这个操作将长度从厘米转换为米,以便与重力加速度的单位(米/秒²)相匹配。最后,我们打印出计算得到的周期。当你运行程序时,你会看到如下输出:

Length: 15.0 cm Time Period: 0.777 s
Length: 18.0 cm Time Period: 0.852 s
Length: 21.0 cm Time Period: 0.920 s
Length: 22.5 cm Time Period: 0.952 s
Length: 25.0 cm Time Period: 1.004 s
不同的重力,不同的结果

现在,假设我们在三个不同的地方进行这个实验——我现在的位置,澳大利亚布里斯班;北极;以及赤道。重力的大小会根据你所在纬度略有变化:在赤道稍低(约为 9.78 m/s²),而在北极则较高(9.83 m/s²)。这意味着我们可以把重力看作公式中的一个变量,而不是常数,并根据三个不同的重力加速度值进行计算:{9.8, 9.78, 9.83}。

如果我们想要计算每个位置的五个不同长度的摆钟周期,可以通过计算这些值的笛卡尔积来系统地得出所有组合,以下程序展示了这一过程:

   from sympy import FiniteSet, pi

   def time_period(length, g):

       T = 2*pi*(length/g)**0.5
       return T

   if __name__ == '__main__':

       L = FiniteSet(15, 18, 21, 22.5, 25)
       g_values = FiniteSet(9.8, 9.78, 9.83)
➊     print('{0:¹⁵}{1:¹⁵}{2:¹⁵}'.format('Length(cm)', 'Gravity(m/s²)', 'Time Period(s)'))
➋     for elem in L*g_values:
➌         l = elem[0]
➍         g = elem[1]
           t = time_period(l/100, g)

➎         print('{0:¹⁵}{1:¹⁵}{2:¹⁵.3f}'.format(float(l), float(g), float(t)))

在➋,我们取两个变量集Lg_values的笛卡尔积,然后遍历每一个组合来计算周期。每个组合都表示为一个元组,对于每个元组,我们在➌提取第一个值,即长度,在➍提取第二个值,即重力。然后,就像之前一样,我们调用time_period()函数,将这两个标签作为参数传入,并打印出长度(l)、重力(g)和相应的周期(T)值。

输出以表格的形式呈现,便于查看。表格是通过➊和➎的print语句格式化的。格式化字符串{0:¹⁵} {1:¹⁵}{2:¹⁵.3f}创建了三个字段,每个字段宽度为 15 个字符,^符号将每个条目居中。在➎的print语句的最后一个字段中,'.3f'限制小数点后数字的位数为三位。

当你运行程序时,你会看到如下输出:

Length(cm)   Gravity(m/s²)   Time Period(s)
    15.0           9.78             0.778
    15.0            9.8             0.777
    15.0           9.83             0.776

    18.0           9.78             0.852
    18.0            9.8             0.852
    18.0           9.83             0.850
    21.0           9.78             0.921
    21.0            9.8             0.920
    21.0           9.83             0.918
    22.5           9.78             0.953
    22.5            9.8             0.952
    22.5           9.83             0.951
    25.0           9.78             1.005
    25.0            9.8             1.004
    25.0           9.83             1.002

这个实验展示了一个简单的场景,在这种情况下,你需要多个集合(或数字组)所有可能的组合。在这种情况下,笛卡尔积正是你所需要的。

概率

集合帮助我们推理概率的基本概念。我们将从几个定义开始:

实验 实验就是我们想要进行的测试。我们进行测试是因为我们对每个可能结果的概率感兴趣。掷骰子、抛硬币和从一副扑克牌中抽牌都是实验的例子。一次实验的单次运行被称为试验

样本空间 所有实验的可能结果构成一个集合,称为样本空间,我们通常在公式中称它为S。例如,当掷一次六面骰子时,样本空间是{1, 2, 3, 4, 5, 6}。

事件 事件是我们想要计算概率的结果集合,它是样本空间的一个子集。例如,我们可能想知道某个特定结果的概率,比如掷出 3,或者多个结果的概率,例如掷出偶数(2、4 或 6)。我们将在公式中使用字母E表示一个事件。

如果存在均匀分布——也就是说,样本空间中的每个结果发生的可能性相等——那么事件P(E)发生的概率可以使用以下公式计算(稍后我会在本章中讨论非均匀分布):

image

这里,n(E)和n(S)分别是事件E和样本空间S的基数。P(E)的值范围从 0 到 1,较高的值表示事件发生的可能性较大。

我们可以应用这个公式来计算某个特定掷骰子的概率——比如,掷出 3:

image

这验证了我们一直以来的直觉:某个特定的掷骰子结果的概率是 1/6。你可以轻松地用脑算出这个计算结果,但我们可以使用这个公式在 Python 中写出以下函数,用来计算任何事件event在任何样本空间space中的概率:

def probability(space, event):
    return len(event)/len(space)

在这个函数中,两个参数spaceevent——样本空间和事件——不必是通过FiniteSet创建的集合。它们也可以是列表,或者任何支持len()函数的其他 Python 对象。

使用这个函数,让我们写一个程序来计算当掷一个 20 面骰子时,出现质数的概率:

     def probability(space, event):
         return len(event)/len(space)

➊   def check_prime(number):
         if number != 1:
             for factor in range(2, number):
                 if number % factor == 0:
                     return False
         else:
             return False
         return True

     if __name__ == '__main__':
➋       space = FiniteSet(*range(1, 21))
         primes = []
         for num in s:
➌           if check_prime(num):
                 primes.append(num)
➍           event= FiniteSet(*primes)
         p = probability(space, event)

         print('Sample space: {0}'.format(space))
         print('Event: {0}'.format(event))
         print('Probability of rolling a prime: {0:.5f}'.format(p))

我们首先在➋处使用range()函数创建一个表示样本空间space的集合。为了创建事件集合,我们需要从样本空间中找到质数,因此我们在➊处定义了一个函数check_prime()。这个函数接受一个整数并检查它是否能被 2 到它自身之间的任何数字整除(没有余数)。如果能整除,则返回False。因为质数只能被 1 和它本身整除,所以如果整数是质数,这个函数返回True,否则返回False

我们在 ➌ 处调用此函数为样本空间中的每个数字生成随机数,并将素数添加到列表 primes 中。然后,在 ➍ 处,我们从这个列表中创建事件集合 event。最后,我们调用之前创建的 probability() 函数。当我们运行程序时,得到以下输出:

Sample space: {1, 2, 3, ..., 18, 19, 20}
Event: {2, 3, 5, 7, 11, 13, 17, 19}
Probability of rolling a prime: 0.40000

这里,n(E) = 8,n(S) = 20,因此概率 P 为 0.4。

在我们的 20 面骰子程序中,我们其实不需要创建这些集合;相反,我们可以将样本空间和事件作为列表传递给 probability() 函数:

if __name__ == '__main__':
    space = range(1, 21)
    primes = []
    for num in space:
        if check_prime(num):
            primes.append(num)
    p = probability(space, primes)

probability() 函数在这种情况下同样有效。

事件 A 或事件 B 的概率

假设我们对两个可能的事件感兴趣,并且想要找到任意一个事件发生的概率。例如,回到简单的掷骰子,让我们考虑以下两个事件:

A = 数字是素数。

B = 数字是奇数。

就像之前一样,样本空间 S 是 {1, 2, 3, 4, 5, 6}。事件 A 可以表示为子集 {2, 3, 5},即样本空间中的素数集合,事件 B 可以表示为 {1, 3, 5},即样本空间中的奇数集合。为了计算任意一个结果集合的概率,我们可以计算两个集合的并集的概率。在我们的符号中,我们可以这样表示:

image

现在让我们用 Python 执行这个计算:

   >>> from sympy import FiniteSet
   >>> s = FiniteSet(1, 2, 3, 4, 5, 6)
   >>> a = FiniteSet(2, 3, 5)
   >>> b = FiniteSet(1, 3, 5)
➊ >>> e = a.union(b)
   >>> len(e)/len(s)
   0.6666666666666666

我们首先创建一个集合 s,表示样本空间,接着创建两个集合 ab。然后,在 ➊ 处,我们使用 union() 方法来找到事件集合 e。最后,我们使用之前的公式计算两个集合的并集的概率。

事件 A 和事件 B 的概率

假设你有两个事件,并且想要计算两者同时发生的概率——例如,骰子掷点是既是素数又是奇数的概率。要确定这一点,你需要计算两个事件集合的交集的概率:

E = AB = {2, 3, 5} ∩ {1, 3, 5} = {3, 5}

我们可以通过使用 intersect() 方法来计算 A 和 B 同时发生的概率,这与我们在前一个例子中做的类似:

>>> from sympy import FiniteSet
>>> s = FiniteSet(1, 2, 3, 4, 5, 6)
>>> a = FiniteSet(2, 3, 5)
>>> b = FiniteSet(1, 3, 5)
>>> e = a.intersect(b)
>>> len(e)/len(s)
0.3333333333333333

生成随机数

概率概念让我们能够推理并计算事件发生的概率。为了实际模拟这些事件——比如一个简单的骰子游戏——我们需要一种生成随机数的方法。

模拟骰子掷点

为了模拟一个六面骰子的掷点,我们需要一种方法来生成一个 1 到 6 之间的随机整数。Python 标准库中的 random 模块提供了多种生成随机数的函数。本章将使用两个函数,randint() 函数,它在给定的范围内生成一个随机整数,以及 random() 函数,它生成一个介于 0 和 1 之间的浮动数。让我们看一个 randint() 函数如何工作的简短示例:

>>> import random
>>> random.randint(1, 6)
4

randint()函数接受两个整数作为参数,返回介于这两个数字之间的一个随机整数(包括这两个数字)。在这个例子中,我们传入了范围(1, 6),它返回了数字 4,但如果我们再次调用它,很可能会得到不同的数字:

>>> random.randint(1, 6)
6

调用randint()函数可以模拟我们虚拟骰子的掷骰过程。每次调用此函数时,我们都会得到一个介于 1 和 6 之间的数字,就像我们在掷一个六面骰子一样。请注意,randint()要求你首先提供较小的数字,所以randint(6, 1)是无效的。

你能掷出那个分数吗?

我们的下一个程序将模拟一个简单的掷骰游戏,我们将不断掷六面骰子,直到我们总共掷出 20 点:

   '''
   Roll a die until the total score is 20
   '''

   import matplotlib.pyplot as plt
   import random

   target_score = 20

   def roll():
       return random.randint(1, 6)

   if __name__ == '__main__':
       score = 0
       num_rolls = 0
➊     while score < target_score:
           die_roll = roll()
           num_rolls += 1
           print('Rolled: {0}'.format(die_roll))
           score += die_roll

       print('Score of {0} reached in {1} rolls'.format(score, num_rolls))

首先,我们定义之前创建的相同的roll()函数。然后,在➊处我们使用while循环调用此函数,跟踪掷骰次数,打印当前的掷骰结果,并累计总分数。循环会一直进行,直到分数达到 20,然后程序会打印总分数和掷骰次数。

这是一次示例运行:

Rolled: 6
Rolled: 2
Rolled: 5
Rolled: 1
Rolled: 3
Rolled: 4
Score of 21 reached in 6 rolls

如果你多次运行该程序,你会注意到,达到 20 点所需的掷骰次数是不同的。

目标分数可能吗?

我们的下一个程序类似,但它会告诉我们是否能在最大掷骰次数内达到某个目标分数:

   from sympy import FiniteSet
   import random

   def find_prob(target_score, max_rolls):

       die_sides = FiniteSet(1, 2, 3, 4, 5, 6)
       # Sample space
➊     s = die_sides**max_rolls
       # Find the event set
       if max_rolls > 1:
           success_rolls = []
➋         for elem in s:
               if sum(elem) >= target_score:
                   success_rolls.append(elem)
       else:
           if target_score > 6:
➌             success_rolls = []
           else:
               success_rolls = []
               for roll in die_sides:
➍                 if roll >= target_score:
                       success_rolls.append(roll)
➎     e = FiniteSet(*success_rolls)
       # Calculate the probability of reaching target score
       return len(e)/len(s)

   if __name__ == '__main__':

       target_score = int(input('Enter the target score: '))
       max_rolls = int(input('Enter the maximum number of rolls allowed: '))

       p = find_prob(target_score, max_rolls)
       print('Probability: {0:.5f}'.format(p))

当你运行这个程序时,它会要求输入目标分数和允许的最大掷骰次数,然后它会打印出达到该目标的概率。

这里有两个示例执行:

Enter the target score: 25
Enter the maximum number of rolls allowed: 4
Probability: 0.00000

Enter the target score: 25
Enter the maximum number of rolls allowed: 5
Probability: 0.03241

让我们理解find_prob()函数的工作原理,该函数执行概率计算。这里的样本空间是笛卡尔积,die_sides^(max_rolls) ➊,其中die_sides是集合{1, 2, 3, 4, 5, 6},表示六面骰子上的数字,max_rolls是允许的最大掷骰次数。

事件集是样本空间中所有有助于我们达到目标分数的集合。这里有两种情况:当剩余的掷骰次数大于 1 时,以及当我们进入最后一次掷骰时。对于第一种情况,在➋处,我们遍历笛卡尔积中的每个元组,将那些总和等于或超过target_score的元组加入success_rolls列表。第二种情况是特殊的:我们的样本空间只有{1, 2, 3, 4, 5, 6}这一集合,并且我们只剩下一次掷骰机会。如果目标分数大于 6,则无法实现,且我们会在➌处将success_rolls设置为空列表。然而,如果target_score小于或等于 6,我们会遍历每个可能的掷骰结果,并在➍处将那些大于或等于target_score的结果加入列表。

在➎处,我们从之前构建的success_rolls列表中计算出事件集e,然后返回达到目标分数的概率。

非均匀随机数

我们对概率的讨论至今假设样本空间中的每个结果都是等可能的。例如,random.randint() 函数返回一个指定范围内的整数,假设每个整数的出现是等可能的。我们将这种概率称为均匀概率,并将通过 randint() 函数生成的随机数称为均匀随机数。但是,假设我们要模拟一个偏向的硬币投掷——一个加重的硬币,正面出现的概率是反面的两倍。那么,我们就需要一种生成不均匀随机数的方法。

在我们编写程序之前,我们先回顾一下其背后的思想。

考虑一个长度为 1 的数轴,分成两个相等的区间,如 图 5-1 所示。

image

图 5-1:一个长度为 1 的数轴,分成两个相等的区间,分别对应于硬币投掷时正面或反面的概率

我们将这条线称为概率数轴,其中每个分割代表一个等可能的结果——例如,公正硬币投掷时的正面或反面。现在,在 图 5-2 中,考虑这个不同版本的数轴。

image

图 5-2:一个长度为 1 的数轴,分成两个不相等的区间,分别对应于偏向的硬币投掷中正面或反面的概率

这里,正面对应的分割是总长度的 2/3,反面对应的分割是 1/3。这表示在 2/3 的投掷中,硬币更可能出现正面,而在 1/3 的投掷中则更可能出现反面。以下 Python 函数将模拟这种硬币投掷,考虑到正面或反面出现的不均匀概率:

   import random

   def toss():
       # 0 -> Heads, 1-> Tails
➊     if random.random() < 2/3:
           return 0
       else:
           return 1

我们假设函数返回 0 表示正面,1 表示反面,然后在 ➊ 处使用 random.random() 函数生成一个介于 0 和 1 之间的随机数。如果生成的数字小于 2/3——我们偏向硬币投掷时正面的概率——程序返回 0;否则返回 1(反面)。

现在我们来看一下如何将前述函数推广,用于模拟具有多个可能结果的非均匀事件。假设我们有一台虚拟的自动取款机,当按下按钮时,它会发放 $5、$10、$20 或 $50 美元的钞票。这些不同面额的钞票有不同的发放概率,如 图 5-3 所示。

image

图 5-3:一个长度为 1 的数轴,分成四个不同长度的区间,分别对应于发放不同面额钞票的概率

这里,$5 或 $10 美元钞票被发放的概率是 1/6,而 $20 或 $50 美元钞票被发放的概率是 1/3。

我们创建了一个列表来存储概率的滚动和,然后生成一个 0 到 1 之间的随机数。我们从存储和的列表的左端开始,并返回该列表中第一个元素,其对应的和小于或等于生成的随机数。get_index()函数实现了这个思路:

   '''
   Simulate a fictional ATM that dispenses dollar bills
   of various denominations with varying probability
   '''

   import random

   def get_index(probability):
       c_probability = 0
➊     sum_probability = []
       for p in probability:
           c_probability += p
           sum_probability.append(c_probability)
➋     r = random.random()
       for index, sp in enumerate(sum_probability):
➌         if r <= sp:
               return index
➍     return len(probability)-1

   def dispense():

       dollar_bills = [5, 10, 20, 50]
       probability = [1/6, 1/6, 1/3, 2/3]
       bill_index = get_index(probability)
       return dollar_bills[bill_index]

我们调用get_index()函数,传入一个包含相应位置事件发生概率的列表。然后,在 ➊ 处构建列表sum_probability,其中第i个元素是列表probability中前i个元素的和。也就是说,sum_probability中的第一个元素等于probability中的第一个元素,第二个元素等于probability中前两个元素的和,以此类推。在 ➋ 处,使用标签r生成一个 0 到 1 之间的随机数。接下来,在 ➌ 处,我们遍历sum_probability并返回第一个超过r的元素的索引。

函数的最后一行,在 ➍ 处,处理了一个特殊情况,最好通过一个例子来说明。考虑一个包含三个事件的列表,每个事件的发生概率为 0.33。在这种情况下,列表sum_probability看起来像[0.33, 0.66, 0.99]。现在,假设生成的随机数r0.99314。对于这个r值,我们希望选择事件列表中的最后一个元素。你可能会争辩说,这并不完全正确,因为最后一个事件的选择概率超过了 33%。根据 ➌ 处的条件,sum_probability中没有大于r的元素,因此函数不会返回任何索引。 ➍处的语句处理了这个问题并返回了最后一个索引。

如果你调用dispense()函数来模拟 ATM 机发放大量纸币,你会看到每种纸币出现的次数比例与指定的概率紧密遵循。我们将在下一章中创建分形时发现这个技巧非常有用。

你学到了什么

在本章中,你首先学习了如何在 Python 中表示集合。然后,我们讨论了各种集合概念,你学习了集合的并集、交集和笛卡尔积。你将一些集合概念应用于探索概率的基础知识,并最终学习了如何在程序中模拟均匀和非均匀的随机事件。

编程挑战

接下来,你将解决一些编程挑战,这些挑战将为你提供应用本章所学内容的机会。

#1: 使用维恩图可视化集合之间的关系

维恩图是查看集合关系的一种简单方式,它以图形的形式告诉我们两个集合之间有多少共同元素,有多少元素只存在于一个集合中,以及有多少元素不属于任何集合。考虑集合A,它表示小于 20 的正奇数,即A = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};再考虑集合B,它表示小于 20 的质数,即B = {2, 3, 5, 7, 11, 13, 17, 19}。我们可以使用 Python 的matplotlib_venn包绘制维恩图(有关该包的安装说明,请参见附录 A)。安装完成后,可以按如下方式绘制维恩图:

'''
Draw a Venn diagram for two sets
'''

from matplotlib_venn import venn2
import matplotlib.pyplot as plt
from sympy import FiniteSet

def draw_venn(sets):

    venn2(subsets=sets)
    plt.show()

if __name__ == '__main__':

    s1 = FiniteSet(1, 3, 5, 7, 9, 11, 13, 15, 17, 19)
    s2 = FiniteSet(2, 3, 5, 7, 11, 13, 17, 19)

    draw_venn([s1, s2])

一旦我们导入了所有必需的模块和函数(venn2()函数、matplotlib.pyplot以及FiniteSet类),我们所需要做的就是创建这两个集合,然后调用venn2()函数,使用subsets关键字参数将集合作为元组指定。

图 5-4 展示了前述程序创建的维恩图。集合AB共享七个共同元素,因此数字 7 被写在共同区域内。每个集合也都有独特的元素,因此唯一元素的数量——分别为 3 和 1——被写在各自的独立区域内。两个集合下方的标签显示为AB。你可以使用set_labels关键字参数指定自己的标签:

>>> venn2(subsets=(a,b), set_labels=('S', 'T'))

这将把集合标签更改为ST

image

图 5-4:展示两个集合 A 和 B 之间关系的维恩图

作为你的挑战,假设你已经创建了一个在线问卷,询问你的同学以下问题:你玩足球、其他运动,还是不玩运动? 一旦你得到结果,创建一个 CSV 文件,sports.csv,如下所示:

StudentID,Football,Others
1,1,0
2,1,1
3,0,1
--snip--

为你的班级中的 20 个学生创建 20 行数据。第一列是学生 ID(该调查并非匿名),第二列为 1 表示该学生标记了“足球”作为他们喜欢的运动,第三列为 1 表示该学生玩其他运动或根本不玩任何运动。编写程序创建一个维恩图,以显示调查结果的汇总,如图 5-5 所示。

image

图 5-5:展示喜欢踢足球的学生人数与喜欢其他运动的学生人数的维恩图

根据你创建的sports.csv文件中的数据,每个集合中的数字将有所不同。以下函数读取 CSV 文件,并返回两个列表,对应于那些玩足球和其他运动的学生 ID:

def read_csv(filename):
    football = []
    others = []
    with open(filename) as f:
        reader = csv.reader(f)
        next(reader)
        for row in reader:
            if row[1] == '1':
                football.append(row[0])
            if row[2] == '1':
                others.append(row[0])

    return football, others

#2:大数法则

我们已经提到过掷骰子和掷硬币作为两个可以通过随机数模拟的随机事件的例子。我们使用“事件”这个术语来表示在掷骰子时出现的某个数字,或者在掷硬币时出现的正面或反面,每个事件都有一个关联的概率值。在概率论中,随机变量——通常表示为X——描述了一个事件。例如,X = 1 描述了掷骰子时出现 1 的事件,而P(X = 1)描述了关联的概率。随机变量有两种类型:(1)离散随机变量,它们只取整数值,并且是我们在本章中看到的唯一类型的随机变量;(2)连续随机变量,正如其名所示,它们可以取任何实数值。

离散随机变量的期望E,相当于我们在第三章学到的平均值或均值。期望值可以按以下方式计算:

E = x[1]P(x[1]) + x[2]P(x[2]) + x[3]P(x[3]) + ... + x[n]P(x[n]*)

因此,对于一个六面骰子,掷骰子的期望值可以像这样计算:

>>> e = 1*(1/6) + 2*(1/6) + 3*(1/6) + 4*(1/6) + 5*(1/6) + 6*(1/6)
>>> e
3.5

根据大数法则,经过多次试验后的结果的平均值会随着试验次数的增加而趋近于期望值。你在这个任务中的挑战是验证这个法则,模拟掷六面骰子时在以下试验次数下的情况:100 次、1000 次、10000 次、100000 次和 500000 次。以下是你完整程序的一个预期运行示例:

Expected value: 3.5
Trials: 100 Trial average 3.39
Trials: 1000 Trial average 3.576
Trials: 10000 Trial average 3.5054
Trials: 100000 Trial average 3.50201
Trials: 500000 Trial average 3.495568

#3:在你没钱之前需要多少次掷硬币?

假设我们有一个简单的公平掷硬币的游戏。玩家掷到正面时赢得$1,掷到反面时损失$1.50。当玩家的余额达到$0 时,游戏结束。给定用户指定的初始金额,你的挑战是编写一个程序来模拟这个游戏。假设计算机有一个无限的现金储备——它是你的对手。以下是一个可能的游戏过程:

Enter your starting amount: 10
Tails! Current amount: 8.5
Tails! Current amount: 7.0
Tails! Current amount: 5.5
Tails! Current amount: 4.0
Tails! Current amount: 2.5
Heads! Current amount: 3.5
Tails! Current amount: 2.0
Tails! Current amount: 0.5
Tails! Current amount: -1.0
Game over :( Current amount: -1.0. Coin tosses: 9

#4:洗牌一副扑克牌

考虑一副标准的 52 张扑克牌。你的挑战是编写一个程序来模拟这副牌的洗牌过程。为了保持实现简单,我建议你使用整数 1、2、3、...、52 来表示这副牌。每次运行程序时,它应该输出一副洗过的牌——在这个例子中,是一组洗过的整数列表。

这是你程序的一个可能输出:

[3, 9, 21, 50, 32, 4, 20, 52, 7, 13, 41, 25, 49, 36, 23, 45, 1, 22, 40, 19, 2,
35, 28, 30, 39, 44, 29, 38, 48, 16, 15, 18, 46, 31, 14, 33, 10, 6, 24, 5, 43,
47, 11, 34, 37, 27, 8, 17, 51, 12, 42, 26]

Python 标准库中的random模块有一个函数shuffle(),用于执行这个操作:

   >>> import random
   >>> x = [1, 2, 3, 4]
➊ >>> random.shuffle(x)
   >>> x
   [4, 2, 1, 3]

创建一个列表x,包含数字[1, 2, 3, 4]。然后,调用shuffle()函数 ➊,将这个列表作为参数传入。你会看到列表x中的数字已经被洗牌。请注意,列表是“就地洗牌”的,也就是说,原始顺序丢失了。

但是,如果你想在卡片游戏中使用这个程序呢?在游戏中,仅仅输出打乱后的整数列表是不够的。你还需要一种方法将整数映射回每张卡片的具体花色和点数。你可以通过创建一个 Python 类来表示一张卡片来实现这一点:

class Card:
    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank

为了表示梅花 A,创建一个卡片对象card1 = Card('clubs', 'ace')。然后,对其他所有卡片进行相同的操作。接着,创建一个包含每个卡片对象的列表并打乱这个列表。程序的输出应该类似于这样:

10 of spades
6 of clubs
jack of spades
9 of spades

#5: 估算圆的面积

考虑一个飞镖板,半径为* r 的圆内切于边长为 2r的正方形中。现在假设你开始向其投掷大量飞镖。一部分飞镖会击中圆内——假设为N,而另一部分会落在圆外——假设为M*。如果我们考虑落在圆内的飞镖的比例,

image

那么f × A的值,其中A是正方形的面积,将大致等于圆的面积(参见图 5-6)。飞镖在图中由小圆点表示。我们将f × A的值称为估算面积。实际的面积当然是πr²。

image

图 5-6:一个半径为 r 的圆内切于边长为 2r* 的正方形板上。点表示随机投掷在板上的飞镖。*

作为本次挑战的一部分,编写一个程序,给定任意半径,使用这种方法估算圆的面积。程序应该打印出三种不同飞镖数量的圆的估算面积:10³、10⁵ 和 10⁶。那可是很多飞镖!你会发现,增加飞镖数量会使估算面积越来越接近实际面积。以下是完成解决方案的示例输出:

Radius: 2
Area: 12.566370614359172, Estimated (1000 darts): 12.576
Area: 12.566370614359172, Estimated (100000 darts): 12.58176
Area: 12.566370614359172, Estimated (1000000 darts): 12.560128

投掷飞镖可以通过调用random.uniform(a, b)函数来模拟,该函数将返回一个介于ab之间的随机数。在此案例中,a = 0,b = 2r(正方形的边长)。

估算π的值

再次考虑图 5-6。正方形的面积是 4r²,内切圆的面积是πr²。如果我们将圆的面积除以正方形的面积,就得到π/4。我们之前计算出的比例f

image

因此,这是π/4 的近似值,这也意味着

image

这个值应该接近π的值。你接下来的挑战是编写一个程序,假设半径为任意值,来估算π的值。当你增加投掷飞镖的数量时,估算的π值应该接近已知的常数值。

第六章:绘制几何形状和分形

image

在本章中,我们将首先学习 matplotlib 中的补丁(patches),它们使我们能够绘制几何形状,如圆形、三角形和多边形。接着,我们将学习 matplotlib 的动画支持,并编写一个程序来展示投射物的轨迹。最后一部分,我们将学习如何绘制 分形——通过反复应用简单的几何变换创建的复杂几何形状。让我们开始吧!

使用 Matplotlib 补丁绘制几何形状

在 matplotlib 中,补丁允许我们绘制几何形状,每个几何形状都被称为一个 补丁。例如,您可以指定圆形的半径和圆心,以便将相应的圆形添加到您的图表中。这与我们迄今为止使用 matplotlib 的方式有很大不同,之前我们通过提供点的 xy 坐标来绘制图形。然而,在编写一个程序来利用补丁功能之前,我们需要稍微了解一下 matplotlib 图表是如何创建的。考虑以下程序,它使用 matplotlib 绘制点 (1, 1),(2, 2) 和 (3, 3):

>>> import matplotlib.pyplot as plt
>>> x = [1, 2, 3]
>>> y = [1, 2, 3]
>>> plt.plot(x, y)
[<matplotlib.lines.Line2D object at 0x7fe822d67a20>]
>>> plt.show()

这个程序创建了一个 matplotlib 窗口,展示了一条经过给定点的直线。在背后,当调用 plt.plot() 函数时,首先会创建一个 Figure 对象,其中创建坐标轴,最后数据会被绘制到坐标轴中(参见 图 6-1)。¹

image

图 6-1:matplotlib 图表的架构

以下程序重新创建了这个图表,但我们将显式地创建 Figure 对象并向其中添加坐标轴,而不是仅仅调用 plot() 函数并依赖它来创建这些对象:

   >>> import matplotlib.pyplot as plt
   >>> x = [1, 2, 3]
   >>> y = [1, 2, 3]
➊ >>> fig = plt.figure()
➋ >>> ax = plt.axes()
   >>> plt.plot(x, y)
   [<matplotlib.lines.Line2D object at 0x7f9bad1dcc18>]
   >>> plt.show()
   >>>

在这里,我们在 ➊ 使用 figure() 函数创建了 Figure 对象,然后在 ➋ 使用 axes() 函数创建了坐标轴。axes() 函数还会将坐标轴添加到 Figure 对象中。最后两行与之前的程序相同。这一次,当我们调用 plot() 函数时,它会看到已经存在一个包含 Axes 对象的 Figure 对象,并直接绘制提供给它的数据。

除了手动创建 FigureAxes 对象外,您还可以使用 pyplot 模块中的两个不同函数来获取当前 FigureAxes 对象的引用。当调用 gcf() 函数时,它返回当前 Figure 的引用,而当调用 gca() 函数时,它返回当前 Axes 的引用。一个有趣的特点是,如果这些对象尚不存在,每个函数都会自动创建它们。随着我们在本章中逐步使用这些函数,它们的工作原理会变得更加清晰。

绘制一个圆形

要绘制一个圆形,您可以将 Circle 补丁添加到当前的 Axes 对象中,如下所示:

   '''
   Example of using matplotlib's Circle patch
   '''
   import matplotlib.pyplot as plt

   def create_circle():
➊     circle = plt.Circle((0, 0), radius = 0.5)
       return circle

   def show_shape(patch):
➋     ax = plt.gca()
       ax.add_patch(patch)
       plt.axis('scaled')
       plt.show()

   if __name__ == '__main__':
➌     c = create_circle()
       show_shape(c)

在这个程序中,我们将Circle补丁对象的创建与将补丁添加到图形中的操作分成了两个函数:create_circle()show_shape()。在create_circle()中,我们通过传入中心坐标(0, 0)和半径 0.5,使用相同名称的关键字参数在第➊步创建了一个Circle对象。该函数返回创建的Circle对象。

show_shape()函数的编写方式使其能够与任何 matplotlib 补丁一起使用。首先,在第➋步,它通过gca()函数获取当前Axes对象的引用。然后,它使用add_patch()函数将传入的补丁添加到图形中,最后调用show()函数显示图形。我们在这里调用了axis()函数,并传入scaled参数,这基本上告诉 matplotlib 自动调整坐标轴的范围。我们需要在所有使用补丁的程序中加入此语句,以便自动缩放坐标轴。当然,你也可以像我们在第二章中看到的那样,指定固定的坐标轴范围。

在第➌步,我们通过标签c调用create_circle()函数来引用返回的Circle对象。然后,我们调用show_shape()函数,并将c作为参数传入。当你运行程序时,你会看到一个 matplotlib 窗口显示圆形(见图 6-2)。

image

图 6-2:中心在(0, 0),半径为 0.5 的圆形

正如你所看到的,这里的圆形看起来并不像圆形。这是因为自动的纵横比(aspect ratio)决定了* x 轴和 y 轴的长度比。如果你在第➋步后插入语句ax.set_aspect('equal'),你会看到圆形确实变得像一个圆。set_aspect()函数用于设置图形的纵横比;通过使用equal参数,我们告诉 matplotlib 将 x 轴和 y *轴的长度比设置为 1:1。

通过ecfc关键字参数,可以更改补丁的边缘颜色和填充颜色。例如,传入fc='g'ec='r'将创建一个具有绿色填充和红色边缘的圆形。

Matplotlib 支持多种其他补丁类型,如EllipsePolygonRectangle

创建动画图形

有时我们可能希望创建带有移动形状的图形。Matplotlib 的动画支持将帮助我们实现这一点。在本节的最后,我们将创建一个动画版本的抛物线轨迹绘制程序。

首先,让我们看看一个更简单的例子。我们将绘制一个 matplotlib 图形,其中的圆形一开始很小,然后会无限增大到某个半径(除非关闭 matplotlib 窗口):

   '''
   A growing circle
   '''

   from matplotlib import pyplot as plt
   from matplotlib import animation

   def create_circle():
       circle = plt.Circle((0, 0), 0.05)
       return circle

   def update_radius(i, circle):
       circle.radius = i*0.5
       return circle,

   def create_animation():
➊     fig = plt.gcf()
       ax = plt.axes(xlim=(-10, 10), ylim=(-10, 10))
       ax.set_aspect('equal')
       circle = create_circle()
➋     ax.add_patch(circle)
➌     anim = animation.FuncAnimation(
           fig, update_radius, fargs = (circle,), frames=30, interval=50)
       plt.title('Simple Circle Animation')
       plt.show()

   if __name__ == '__main__':
       create_animation()

我们首先从 matplotlib 包中导入animation模块。create_animation()函数在这里实现了核心功能。它通过在➊处使用gcf()函数获取当前Figure对象的引用,然后创建了坐标轴,坐标轴的限制为-10 和 10,分别适用于x轴和y轴。之后,它创建了一个Circle对象,表示一个半径为 0.05、中心在(0, 0)的圆,并在➋处将这个圆添加到当前坐标轴。然后,我们创建了一个FuncAnimation对象➌,并传递了以下关于我们想要创建的动画的数据:

fig 这是当前的Figure对象。

update_radius 这个函数负责绘制每一帧。它接受两个参数——一个帧编号,该编号在调用时会自动传递给它,另一个是我们希望每帧更新的补丁对象。这个函数还必须返回该对象。

fargs 这个元组包含所有传递给update_radius()函数的参数,除了帧编号。如果没有需要传递的参数,可以不指定这个关键字参数。

frames 这是动画中的帧数。我们的update_radius()函数会被调用这么多次。在这里,我们任意选择了 30 帧。

interval 这是两帧之间的时间间隔(以毫秒为单位)。如果动画看起来太慢,可以减小这个值;如果看起来太快,可以增大这个值。

然后我们使用title()函数设置标题,最后使用show()函数显示图形。

如前所述,update_radius()函数负责更新每一帧会发生变化的圆的属性。在这里,我们将半径设置为i*0.5,其中i是帧编号。因此,你会看到一个在 30 帧内逐渐增大的圆——也就是说,最大圆的半径是 15。由于坐标轴的限制被设置为-10 和 10,这就造成了圆超出图形尺寸的效果。当你运行程序时,你将看到你的第一个动画图形,如图 6-3 所示。

你会注意到动画会一直持续,直到你关闭 matplotlib 窗口。这是默认行为,你可以通过在创建FuncAnimation对象时将关键字参数设置为repeat=False来更改此行为。

image

图 6-3:简单的圆形动画

FUNCANIMATION 对象与持久性

你可能注意到,在动画圆圈程序中,我们将创建的FuncAnimation对象赋值给了标签anim,尽管我们在其他地方没有再次使用它。这是由于 matplotlib 当前的行为问题——它不会存储对FuncAnimation对象的任何引用,这使得该对象可能会被 Python 的垃圾回收机制回收。这意味着动画不会被创建。创建一个引用该对象的标签可以防止这种情况发生。

欲了解更多相关问题,您可以关注在github.com/matplotlib/matplotlib/issues/1656/上的讨论。

动画化投射物的轨迹

在第二章中,我们绘制了一个抛物线运动中球的轨迹。在这里,我们将基于该图形,利用 matplotlib 的动画功能将轨迹动画化,以便更接近于展示在现实生活中看到球运动的效果:

   '''
   Animate the trajectory of an object in projectile motion
   '''

   from matplotlib import pyplot as plt
   from matplotlib import animation
   import math

   g = 9.8

   def get_intervals(u, theta):

       t_flight = 2*u*math.sin(theta)/g
       intervals = []
       start = 0
       interval = 0.005
       while start < t_flight:
           intervals.append(start)
           start = start + interval
       return intervals

   def update_position(i, circle, intervals, u, theta):

       t = intervals[i]
       x = u*math.cos(theta)*t
       y = u*math.sin(theta)*t - 0.5*g*t*t
       circle.center = x, y
       return circle,

   def create_animation(u, theta):

       intervals = get_intervals(u, theta)

       xmin = 0
       xmax = u*math.cos(theta)*intervals[-1]
       ymin = 0
       t_max = u*math.sin(theta)/g
➊     ymax = u*math.sin(theta)*t_max - 0.5*g*t_max**2
       fig = plt.gcf()
➋     ax = plt.axes(xlim=(xmin, xmax), ylim=(ymin, ymax))

       circle = plt.Circle((xmin, ymin), 1.0)
       ax.add_patch(circle)

➌     anim = animation.FuncAnimation(fig, update_position,
                           fargs=(circle, intervals, u, theta),
                           frames=len(intervals), interval=1,
                           repeat=False)

       plt.title('Projectile Motion')
       plt.xlabel('X')
       plt.ylabel('Y')
       plt.show()

   if __name__ == '__main__':
       try:
           u = float(input('Enter the initial velocity (m/s): '))
           theta = float(input('Enter the angle of projection (degrees): '))
       except ValueError:
           print('You entered an invalid input')
       else:
           theta = math.radians(theta)
           create_animation(u, theta)

create_animation() 函数接受两个参数:utheta。这些参数对应程序输入的初速度和发射角度 (θ)。get_intervals() 函数用于找到计算 xy 坐标的时间间隔。此函数通过利用我们在第二章中使用的相同逻辑来实现,当时我们实现了一个单独的函数 frange() 来帮助我们。

为了设置动画的坐标轴范围,我们需要找出 xy 的最小值和最大值。每个坐标的最小值都是 0,这是它们的初始值。x 坐标的最大值是球飞行结束时的坐标值,即列表 intervals 中最后的时间间隔。y 坐标的最大值是球的最高点——也就是在 ➊ 点,我们通过公式计算这个点的位置。

image

一旦我们得到了这些值,我们就在 ➋ 创建坐标轴,并传入适当的坐标轴范围。在接下来的两条语句中,我们创建了一个球的表示,并通过在 (xmin, ymin)(即 xy 轴的最小坐标)创建一个半径为 1.0 的圆,将其添加到图形的 Axes 对象中。

然后我们创建 FuncAnimation 对象 ➌,并为其提供当前的图形对象以及以下参数:

update_position 该函数将在每一帧中改变圆心的位置。这里的想法是每个时间间隔都会创建一个新的帧,因此我们将帧数设置为时间间隔的大小(请参阅此列表中 frames 的描述)。我们计算在第 i 个时间间隔的时间瞬间,球的 xy 坐标,并将圆心的位置设置为这些值。

fargs update_position() 函数需要访问时间间隔列表 intervals、初速度和角度 theta,这些都通过这个关键字参数指定。

frames 因为我们将在每个时间间隔绘制一帧,所以将帧数设置为 intervals 列表的大小。

repeat 正如我们在第一个动画示例中讨论的,动画默认情况下会无限重复。我们不希望在这个情况下发生这种情况,因此我们将该关键字设置为 False

当您运行程序时,它会要求输入初始值,然后生成动画,如图 6-4 所示。

image

图 6-4:抛射物轨迹的动画

绘制分形

分形是从出奇简单的数学公式中产生的复杂几何图案或形状。与几何形状(如圆形和矩形)相比,分形看起来不规则且没有明显的模式或描述,但如果仔细观察,你会发现图案会显现出来,并且整个形状是由无数自我复制的部分组成的。由于分形涉及在平面上对点进行相同几何变换的重复应用,因此计算机程序非常适合生成它们。在本章中,我们将学习如何绘制巴恩斯利蕨、谢尔宾斯基三角形和曼德尔布罗集合(后两者在挑战中介绍)——这是分形领域中常见的例子。分形在自然界中也随处可见——常见的例子包括海岸线、树木和雪花。

平面上点的变换

创建分形的基本思想是点的变换。给定平面上的一个点 P(x, y),变换的一个例子是 P (x, y) → Q (x + 1, y + 1),这意味着在应用变换后,会创建一个新点 Q,它位于 P 上方和右侧各一单位。如果然后把 Q 作为起点,你会得到另一个点 R,它位于 Q 上方和右侧各一单位。假设起始点 P 为 (1, 1)。图 6-5 显示了点的变化情况。

image

图 6-5:点 Q R 通过对点 P 进行两次变换获得。

因此,这个变换是一个规则,描述了一个点在 x-y 平面中如何从初始位置开始,经过每次迭代后移动到不同的点。我们可以把变换看作是点在平面上的轨迹。现在,考虑到不仅有一个变换规则,而是有两个这样的规则,并且在每一步中随机选择其中一个变换规则。我们来看一下这些规则:

规则 1: P 1 (x, y) → P 2 (x + 1, y – 1)

规则 2: P 1 (x, y) → P 2 (x + 1, y + 1)

假设 P1(1, 1) 为起始点。如果进行四次迭代,我们可以得到以下的点序列:

P 1 (1, 1) → P 2 (2, 0)(规则 1)

P 2 (2, 0) → P 3 (3, 1)(规则 2)

P 3 (3, 1) → P 4 (4, 2)(规则 2)

P 4 (4, 2) → P 5 (5, 1)(规则 1)

…依此类推。

变换规则是随机选取的,每个规则被选中的概率相等。无论选中哪个规则,点都会朝右移动,因为在两种情况下我们都会增加 x 坐标。当点朝右移动时,它们要么向上,要么向下,从而形成锯齿状路径。以下程序绘制了一个点在进行某一变换规则下,经过指定次数迭代后的路径:

   '''
   Example of selecting a transformation from two equally probable
   transformations
   '''
   import matplotlib.pyplot as plt
   import random

   def transformation_1(p):
       x = p[0]
       y = p[1]
       return x + 1, y - 1

   def transformation_2(p):
       x = p[0]
       y = p[1]
       return x + 1, y + 1

   def transform(p):
➊     # List of transformation functions
       transformations = [transformation_1, transformation_2]
       # Pick a random transformation function and call it
➋     t = random.choice(transformations)
➌     x, y = t(p)
       return x, y

   def build_trajectory(p, n):
       x = [p[0]]
       y = [p[1]]
       for i in range(n):
           p = transform(p)
           x.append(p[0])
           y.append(p[1])

           return x, y

   if __name__ == '__main__':
       # Initial point
       p = (1, 1)
       n = int(input('Enter the number of iterations: '))
➍     x, y = build_trajectory(p, n)
       # Plot
➎     plt.plot(x, y)
       plt.xlabel('X')
       plt.ylabel('Y')
       plt.show()

我们定义了两个函数,transformation_1()transformation_2(),分别对应之前的两个变换。在transform()函数中,我们在➊处创建一个包含这两个函数名的列表,并使用random.choice()函数在➋处从列表中选择一个变换。现在我们已经选择了要应用的变换,我们用点P调用它,并将变换后的点的坐标存储在标签xy中 ➌,并返回它们。

从列表中选择随机元素

我们在第一个分形程序中看到的random.choice()函数可以用来从列表中选择一个随机元素。每个元素都有相等的机会被返回。以下是一个示例:

>>> import random
>>> l = [1, 2, 3]
>>> random.choice(l)
3
>>> random.choice(l)
1
>>> random.choice(l)
1
>>> random.choice(l)
3
>>> random.choice(l)
3
>>> random.choice(l)
2

该函数同样适用于元组和字符串。在后者的情况下,它从字符串中返回一个随机字符。

当你运行程序时,它会询问你迭代的次数n——即变换应用的次数。然后,它会调用build_trajectory()函数,传入n和初始点P,其值为(1, 1) ➍。build_trajectory()函数会重复调用transform()函数n次,使用两个列表xy来存储所有变换点的x坐标和y坐标。最后,它返回这两个列表,并将其绘制出来 ➎。

图 6-6 和 6-7 分别展示了点在 100 次和 10,000 次迭代中的轨迹。在这两幅图中,锯齿状运动非常明显。这种锯齿路径通常被称为线上的随机游走

image

图 6-6: 点(1, 1)在经过两种变换之一随机进行 100 次迭代时所描绘的锯齿路径

image

图 6-7: 点(1, 1)在经过两种变换之一随机进行 10,000 次迭代时所描绘的锯齿路径。

这个示例展示了创建分形的基本思路——从一个初始点出发,并反复对该点应用变换。接下来,我们将看到将相同的思路应用于绘制巴恩斯利蕨的示例。

绘制巴恩斯利蕨

英国数学家迈克尔·巴恩斯利描述了如何通过对一个点反复应用简单的变换来创建蕨类植物结构(见图 6-8)。

image

图 6-8: 女士蕨²

他提出了以下步骤来创建类似蕨类植物的结构:从点(0, 0)开始,并随机选择以下变换之一,按指定的概率

变换 1 (0.85 概率):

x[n+1] = 0.85x[n] + 0.04y[n]

y[n+1] = –0.04y[n] + 0.85y[n] + 1.6

变换 2 (0.07 概率):

x[n+1] = 0.2x[n] – 0.26y[n]

y[n+1] = 0.23y[n] + 0.22y[n] + 1.6

变换 3 (0.07 概率):

x[n+1] = –0.15x[n] – 0.28x[n]

y[n+1] = 0.26y[n] + 0.24y[n] + 0.44

变换 4(概率 0.01):

x[n+1] = 0

y[n+1] = 0.16y[n]

这些变换中的每一个都负责创建蕨类植物的一部分。第一个变换以最高的概率被选中——因此应用的次数最多——它创建了蕨类植物的茎和底部的羽叶。第二个和第三个变换分别创建了左侧和右侧的底部羽叶,第四个变换则创建了蕨类植物的茎。

这是一个非均匀概率选择的例子,我们在 第五章 中首次学习过。以下程序会根据指定的点数绘制 Barnsley 蕨类植物:

   '''
   Draw a Barnsley Fern
   '''
   import random
   import matplotlib.pyplot as plt

   def transformation_1(p):
       x = p[0]
       y = p[1]
       x1 = 0.85*x + 0.04*y
       y1 = -0.04*x + 0.85*y + 1.6
       return x1, y1

   def transformation_2(p):
       x = p[0]
       y = p[1]
       x1 = 0.2*x - 0.26*y
       y1 = 0.23*x + 0.22*y + 1.6
       return x1, y1

   def transformation_3(p):
       x = p[0]
       y = p[1]
       x1 = -0.15*x + 0.28*y
       y1 = 0.26*x + 0.24*y + 0.44
       return x1, y1

   def transformation_4(p):
       x = p[0]
       y = p[1]
       x1 = 0
       y1 = 0.16*y
       return x1, y1

   def get_index(probability):
       r = random.random()
       c_probability = 0
       sum_probability = []
       for p in probability:
           c_probability += p
           sum_probability.append(c_probability)
       for item, sp in enumerate(sum_probability):
           if r <= sp:
               return item
       return len(probability)-1

   def transform(p):
       # List of transformation functions
       transformations = [transformation_1, transformation_2,
                              transformation_3, transformation_4]
➊     probability = [0.85, 0.07, 0.07, 0.01]
       # Pick a random transformation function and call it
       tindex = get_index(probability)
➋     t = transformations[tindex]
       x, y = t(p)
       return x, y

   def draw_fern(n):
       # We start with (0, 0)
       x = [0]
       y = [0]

       x1, y1 = 0, 0
       for i in range(n):
          x1, y1 = transform((x1, y1))
          x.append(x1)
          y.append(y1)
       return x, y

   if __name__ == '__main__':
       n = int(input('Enter the number of points in the Fern: '))
       x, y = draw_fern(n)
       # Plot the points
       plt.plot(x, y, 'o')
       plt.title('Fern with {0} points'.format(n))
       plt.show()

当你运行这个程序时,它会要求指定蕨类植物的点数,然后创建蕨类植物。图 6-9 和 6-10 分别展示了含有 1,000 和 10,000 个点的蕨类植物。

image

图 6-9:含 1,000 个点的蕨类植物

image

图 6-10:含 10,000 个点的蕨类植物

四个变换规则分别在 transformation_1()transformation_2()transformation_3()transformation_4() 函数中定义。每个变换被选中的概率在 ➊ 处声明,然后在每次 draw_fern() 函数调用 transform() 时,都会随机选取其中一个进行应用 ➋。

初始点 (0, 0) 被变换的次数与程序输入的蕨类植物中指定的点数相同。

你学到了什么

在本章中,你首先学习了如何绘制基本的几何形状以及如何为它们添加动画。这个过程让你接触到了一些新的 matplotlib 功能。然后,你学习了几何变换,并看到重复的简单变换如何帮助你绘制出复杂的几何形状——分形

编程挑战

这里有一些编程挑战,应该能帮助你进一步应用所学的内容。你可以在 www.nostarch.com/doingmathwithpython/ 找到示例解决方案。

#1:将圆形填充到正方形中

我之前提到过,matplotlib 支持创建其他几何形状。Polygon 补丁尤其有趣,因为它允许你绘制具有不同边数的多边形。以下是如何绘制一个边长为 4 的正方形的方法:

'''
Draw a square
'''

from matplotlib import pyplot as plt

def draw_square():
    ax = plt.axes(xlim = (0, 6), ylim = (0, 6))
    square = plt.Polygon([(1, 1), (5, 1), (5, 5), (1, 5)], closed = True)
    ax.add_patch(square)
    plt.show()

if __name__ == '__main__':
    draw_square()

Polygon 对象通过将顶点坐标的列表作为第一个参数来创建。因为我们绘制的是一个正方形,所以我们传入四个顶点的坐标:(1, 1)、(5, 1)、(5, 5) 和 (1, 5)。传入 closed=True 告诉 matplotlib 我们想绘制一个封闭的多边形,其中起点和终点是相同的。

在这个挑战中,你将尝试解决一个简化版的“将圆圈填入正方形”问题。半径为 0.5 的圆圈能在这段代码生成的正方形中填充多少个?绘制并找出答案!图 6-11 展示了最终图像的样子。

image

图 6-11:圆圈填充进正方形

这里的技巧是从正方形的左下角开始——也就是(1, 1)——然后继续添加圆,直到整个正方形填满。以下代码片段展示了如何创建圆并将其添加到图形中:

y = 1.5
while y < 5:
    x = 1.5
    while x < 5:
        c = draw_circle(x, y)
        ax.add_patch(c)

        x += 1.0
    y += 1.0

这里值得注意的一点是,这并不是将圆圈填入正方形的最优方法,或者说,解决此问题的唯一方法,寻找不同的解决方案在数学爱好者中非常流行。

#2: 绘制谢尔宾斯基三角形

谢尔宾斯基三角形是以波兰数学家瓦茨瓦夫·谢尔宾斯基的名字命名的,它是一个分形,构成它的是一个等边三角形,其中嵌入了更小的等边三角形。图 6-12 展示了一个由 10,000 个点组成的谢尔宾斯基三角形。

image

图 6-12:带有 10,000 个点的谢尔宾斯基三角形

有趣的是,绘制蕨类植物时使用的相同过程也能绘制谢尔宾斯基三角形——只是变换规则及其概率会有所变化。以下是绘制谢尔宾斯基三角形的方法:从点(0, 0)开始,应用以下某一变换:

变换 1:

x[n+1] = 0.5x[n]

y[n+1] = 0.5y[n]

变换 2:

x[n+1] = 0.5x[n] + 0.5

y[n+1] = 0.5y[n] + 0.5

变换 3:

x[n+1] = 0.5x[n] + 1

y[n+1] = 0.5y[n]

每个变换的选择概率是相等的——1/3。你的挑战是编写一个程序,绘制出由指定数量的点组成的谢尔宾斯基三角形。

#3: 探索亨农函数

1976 年,米歇尔·亨农(Michel Hénon)提出了亨农函数,它描述了如下的点 P(x, y) 的变换规则:

P (x, y) → Q (y + 1 – 1.4x², 0.3x)

无论初始点在哪里(只要它距离原点不太远),你会发现随着点的增加,它们开始沿着弯曲的线排列,如图 6-13 所示。

image

图 6-13:带有 10,000 个点的亨农函数

你的挑战是编写一个程序,绘制出显示 20,000 次迭代的图形,从点(1, 1)开始。

额外加分项:编写另一个程序,创建一个动画图形,展示点开始沿着曲线排列的过程!示例请见 www.youtube.com/watch?v=76ll818RlpQ

这是一个动态系统的例子,所有点似乎都被吸引到的曲线被称为吸引子。要了解更多关于这个函数、动态系统和分形的一般知识,您可以参考肯尼斯·法尔科纳(Kenneth Falconer)所著的《分形:非常简短的介绍》(牛津大学出版社,2013 年)。

#4: 绘制曼德尔布罗集

你的挑战是编写一个程序来绘制曼德尔布罗集——这是应用简单规则形成复杂形状的另一个例子(请参见图 6-14)。不过,在我讲解具体步骤之前,我们先了解一下 matplotlib 的 imshow() 函数。

image

图 6-14:曼德尔布罗集,平面范围为 (–2.5, –1.0) 到 (1.0, 1.0)

imshow() 函数

imshow() 函数通常用于显示外部图像,如 JPEG 或 PNG 图像。你可以查看matplotlib.org/users/image_tutorial.html上的示例。不过,在这里,我们将使用这个函数通过 matplotlib 绘制我们自己创建的新图像。

考虑笛卡尔平面中 xy 均在 0 到 5 之间的部分。现在,考虑在每个轴上六个等距的点:(0, 1, 2, 3, 4, 5)沿 x-轴分布,沿 y-轴同样分布一组相同的点。如果我们将这些点的笛卡尔积考虑进去,我们会得到 36 个等距的点,坐标为 (0, 0)、(0, 1) … (0, 5)、(1, 0)、(1, 1) … (1, 5) … (5, 5)。现在,假设我们想为这些点中的每一个着上灰色的阴影——也就是说,其中一些点将是黑色的,有些是白色的,其他的将随机选择一个中间的灰色阴影。图 6-15 展示了这个情形。

image

图 6-15:部分 x-y 平面, x y 的范围均为 0 到 5。我们在该区域内考虑了 36 个等距的点,并为每个点着上了不同深浅的灰色。

要创建这个图形,我们必须创建一个由六个列表组成的列表。这六个列表中的每一个将包含六个整数,范围从 0 到 10。每个数字代表一个点的颜色,0 代表黑色,10 代表白色。然后我们将这个列表传递给 imshow() 函数,并附上其他必要的参数。

创建一个列表的列表

一个列表也可以包含其他列表作为其成员:

>>> l1 = [1, 2, 3]
>>> l2 = [4, 5, 6]
>>> l = [l1, l2]

在这里,我们创建了一个列表 l,它由两个列表 l1l2 组成。列表的第一个元素 l[0] 就是 l1 列表,而列表的第二个元素 l[1] 就是 l2 列表:

>>> l[0]
[1, 2, 3]

>>> l[1]
[4, 5, 6]

若要引用其中一个成员列表中的单个元素,我们必须指定两个索引——l[0][1] 指的是第一个列表中的第二个元素,l[1][2] 指的是第二个列表中的第三个元素,以此类推。

现在我们知道如何处理列表的列表,我们可以编写程序来创建一个类似于图 6-15 的图形:

   import matplotlib.pyplot as plt
   import matplotlib.cm as cm
   import random

➊ def initialize_image(x_p, y_p):
       image = []
       for i in range(y_p):
          x_colors = []
           for j in range(x_p):
               x_colors.append(0)
           image.append(x_colors)
       return image

   def color_points():
       x_p = 6
       y_p = 6
       image = initialize_image(x_p, y_p)
       for i in range(y_p):
           for j in range(x_p):
➋             image[i][j] = random.randint(0, 10)
➌     plt.imshow(image, origin='lower', extent=(0, 5, 0, 5),
                  cmap=cm.Greys_r, interpolation='nearest')
       plt.colorbar()
       plt.show()

   if __name__ == '__main__':
       color_points()

在➊处,initialize_image()函数创建了一个元素都初始化为 0 的列表列表。它接受两个参数,x_py_p,分别对应于* x 轴和 y *轴上的点数。这实际上意味着初始化后的列表image将由x_p个列表组成,每个列表包含y_p个零。

color_points()函数中,一旦你从initialize_image()获取到图像列表,就将一个介于 0 和 10 之间的随机整数赋值给➋处的元素image[i][j]。当我们将这个随机整数赋给元素时,我们实际上是将一个颜色分配给笛卡尔平面中距离原点* i 步沿 y 轴和 j 步沿 x 轴的点。需要注意的是,imshow()函数会根据点在image列表中的位置自动推断该点的颜色,而不关心其具体的 x y *坐标。

然后,在➌处调用imshow()函数,将image作为第一个参数传入。关键字参数origin='lower'指定image[0][0]中的数字对应点(0, 0)的颜色。关键字参数extent=(0, 5, 0, 5)将图像的左下角和右上角分别设置为(0, 0)和(5, 5)。关键字参数cmap=cm.Greys_r指定我们将创建一张灰度图像。

最后的关键字参数interpolation='nearest'指定 matplotlib 应该为那些没有指定颜色的点着上与其最近的点相同的颜色。这是什么意思呢?请注意,我们只考虑并为区域(0, 5)和(5, 5)中的 36 个点指定了颜色。因为该区域中有无限多个点,所以我们告诉 matplotlib 将没有指定颜色的点设置为与其最近的点相同的颜色。这就是你在图形中看到每个点周围出现“颜色框”的原因。

调用colorbar()函数在图形中显示一个颜色条,显示哪个整数对应哪个颜色。最后,调用show()来展示图像。请注意,由于使用了random.randint()函数,你的图像将与图 6-15 中的图像有所不同。

如果你通过将color_points()中的x_py_p设为,比如说20,增加每个轴上的点的数量,你会看到类似于图 6-16 的图形。请注意,颜色框的大小变小了。如果你继续增加点的数量,你会看到框的大小进一步缩小,从而产生每个点颜色不同的错觉。

image

图 6-16:部分 x-y 平面,其中 x y 都从 0 到 5。我们考虑了该区域中等距离的 400 个点,并为每个点着上了一种灰色阴影。

绘制曼德尔布罗集

我们将考虑* x y平面中(–2.5, –1.0)和(1.0, 1.0)之间的区域,并将每个轴划分为 400 个均匀间隔的点。这些点的笛卡尔积将给我们 1,600 个均匀分布的点。我们将这些点称为(x[1], y[1]),(x[1], y[2]) ... (x*[400], y[400])。

通过调用我们之前看到的initialize_image()函数并将x_py_p都设置为 400 来创建一个列表image。然后,按照以下步骤处理每个生成的点(x[i]y[k]):

1. 首先,创建两个复数,z[1] = 0 + 0jc = x[i] + y[k] j。(请记住,我们用j表示image。)

2. 创建一个标签iteration并将其设置为 0——即iteration=0

3. 创建一个复数,image

4. 将iteration中的值加 1——即iteration = iteration + 1

5. 如果abs(z1) < 2并且iteration < max_iteration,则返回第 3 步;否则,继续到第 6 步。max_iteration的值越大,图像越详细,但生成图像所需的时间也会更长。这里将max_iteration设置为 1,000。

6. 将点(x[i]y[k])的颜色设置为iteration中的值——即image[k][i] = iteration

一旦你完成了整个image列表,调用imshow()函数,并将extent关键字参数修改为表示由(–2.5, –1.0)和(1.0, 1.0)所界定的区域。

这个算法通常被称为逃逸时间算法。当一个点的幅值在达到最大迭代次数之前没有超过 2 时,该点属于曼德尔布罗集,并被涂成白色。那些在较少迭代次数内幅值超过 2 的点被称为“逃逸”;它们不属于曼德尔布罗集,并被涂成黑色。你可以通过减少或增加每个轴上的点的数量来进行实验。减少点的数量会导致图像出现颗粒感,而增加点的数量则会生成更详细的图像。

第七章:解答微积分问题

image

在本章的最后部分,我们将学习如何解答微积分问题。我们首先了解数学函数,然后快速回顾 Python 标准库和 SymPy 中常见的数学函数。接着,我们将学习如何求解函数的极限,计算导数和积分——也就是你在任何微积分课堂上会做的事情。让我们开始吧!

什么是函数?

让我们从一些基本定义开始。函数是输入集合和输出集合之间的映射。函数的特殊条件是输入集合中的每个元素都与恰好一个输出集合中的元素相关联。例如,图 7-1 显示了两个集合,其中输出集合中的一个元素是属于输入集合的某个元素的平方。

image

图 7-1:一个函数描述了输入集合和输出集合之间的映射关系。在这里,输出集合中的一个元素是输入集合中一个元素的平方。

使用常见的函数表示法,我们可以将这个函数写为 f(x) = x²,其中 x 是自变量。所以 f(2) = 4, f(100) = 10000,依此类推。我们称 x 为自变量,因为我们可以自由地为其假设一个值,只要该值在其定义域内(参见下一部分)。

函数也可以根据多个变量来定义。例如,f(x, y) = x² + y² 定义了一个关于两个变量 xy 的函数。

函数的定义域与值域

函数的定义域是自变量可以合法取值的输入值集合。函数的输出集合叫做值域

例如,函数 f(x) = 1/x 的定义域是所有非零的实数和复数,因为 1/0 是未定义的。值域是通过将定义域中的每个数代入 1/x 得到的值集合,因此在这个例子中,值域也是所有非零的实数和复数。

注意

函数的定义域和值域可以是不同的。例如,对于函数 x²,定义域是所有正负数,但值域只有正数。

常见数学函数概述

我们已经使用了 Python 标准库 math 模块中的许多常见数学函数。几个熟悉的例子是 sin()cos() 函数,它们对应于三角函数正弦和余弦。其他三角函数——tan() 以及这些函数的反函数 asin()acos()atan() 也都有定义。

math模块还包含一些函数,用于计算一个数字的对数——自然对数函数log(),以 2 为底的对数log2(),以及以 10 为底的对数log10()——还有exp()函数,用于计算e^x的值,其中e是欧拉数(约等于 2.71828)。

所有这些函数的一个缺点是,它们不适合处理符号表达式。如果我们想要操作包含符号的数学表达式,就必须开始使用 SymPy 定义的等效函数。

让我们看一个简单的例子:

>>> import math
>>> math.sin(math.pi/2)
1.0

这里,我们使用标准库math模块定义的sin()函数来计算角度π/2 的正弦值。然后,我们可以使用 SymPy 做同样的事情。

>>> import sympy
>>> sympy.sin(math.pi/2)
1.00000000000000

与标准库的sin()函数类似,SymPy 的sin()函数期望角度以弧度表示。两个函数都返回 1。

现在,让我们尝试使用符号来调用每个函数,看看会发生什么:

   >>> from sympy import Symbol
   >>> theta = Symbol('theta')
➊ >>> math.sin(theta) + math.sin(theta)
   Traceback (most recent call last):
     File "<pyshell#53>", line 1, in <module>
       math.sin(theta) + math.sin(theta)
     File "/usr/lib/python3.4/site-packages/sympy/core/expr.py", line 225, in
   __float__
       raise TypeError("can't convert expression to float")
   TypeError: can't convert expression to float

➋ >>> sympy.sin(theta) + sympy.sin(theta)
   2*sin(theta)

标准库的sin()函数不知道当我们以theta为➊调用它时该怎么办,因此它会引发异常,表示它期望一个数值作为sin()函数的参数。另一方面,SymPy 能够在➋执行相同的操作,并返回2*sin(theta)作为结果。现在我们已经不觉得这有什么意外,但它说明了标准库的数学函数在某些任务中可能会有所不足。

让我们考虑另一个例子。假设我们想推导出一个物体在投射运动中达到最高点所需的时间表达式,如果它以初速度u和角度theta被投掷(参见“抛体运动”在第 48 页)。

在最高点,u*sin(theta)-g*t = 0,因此为了求解t,我们将使用在第四章中学到的solve()函数:

>>> from sympy import sin, solve, Symbol
>>> u = Symbol('u')
>>> t = Symbol('t')
>>> g = Symbol('g')
>>> theta = Symbol('theta')
>>> solve(u*sin(theta)-g*t, t)
[u*sin(theta)/g]

如我们之前学到的,t的表达式是u*sin(theta)/g,这也展示了如何使用solve()函数来求解包含数学函数的方程。

SymPy 中的假设

在我们所有的程序中,我们都在 SymPy 中创建了一个Symbol对象,像这样定义变量:x = Symbol('x')。假设作为你要求 SymPy 执行的操作的结果,SymPy 需要检查表达式x + 5 是否大于 0。我们来看一下会发生什么:

>>> from sympy import Symbol
>>> x = Symbol('x')
>>> if (x+5) > 0:
    print('Do Something')
else:
    print('Do Something else')

Traceback (most recent call last):
  File "<pyshell#45>", line 1, in <module>
    if (x + 5) > 0:
  File "/usr/lib/python3.4/site-packages/sympy/core/relational.py", line 103,
in __nonzero__
    raise TypeError("cannot determine truth value of\n%s" % self)
TypeError: cannot determine truth value of
x + 5 > 0

由于 SymPy 不知道x的符号,它无法推断x + 5 是否大于 0,因此会显示错误。但是基础数学告诉我们,如果x是正数,x + 5 永远大于 0,如果x是负数,只有在某些情况下它才会大于 0。

所以,如果我们创建一个指定positive=TrueSymbol对象,我们告诉 SymPy 只考虑正值。现在,它确定了x + 5 一定大于 0:

>>> x = Symbol('x', positive=True)
>>> if (x+5) > 0:
    print('Do Something')
else:
    print('Do Something else')

Do Something

请注意,如果我们指定negative=True,我们可能会得到与第一个情况相同的错误。就像我们可以将符号声明为positivenegative一样,也可以将其指定为realintegercompleximaginary等。这些声明在 SymPy 中被称为假设

求函数的极限

微积分中的常见任务是求函数的极限值(或简称极限),当变量的值趋近于某个特定值时。考虑一个函数f(x) = 1/x,其图像如图 7-2 所示。

x的值增加时,f(x)的值趋近于 0。使用极限符号,我们可以将其写为

imageimage

图 7-2:显示函数 1/x的图像,随着x值的增加

我们可以通过创建Limit类的对象来在 SymPy 中找到函数的极限,方法如下:

➊ >>> from sympy import Limit, Symbol, S
➋ >>> x = Symbol('x')
➌ >>> Limit(1/x, x, S.Infinity)
   Limit(1/x, x, oo, dir='-')

在➊,我们导入LimitSymbol类,以及S,这是一个特殊的 SymPy 类,包含了无穷大(正无穷和负无穷)以及其他特殊值的定义。然后在➋,我们创建一个符号对象x,表示x。在➌,我们创建Limit对象,传入三个参数:1/x,变量x,以及最后我们希望计算函数极限的值(无穷大,由S.Infinity表示)。

结果作为一个未求值对象返回,oo符号表示正无穷,dir='-'符号表示我们从负侧逼近极限。

要求得极限值,我们使用doit()方法:

>>> l = Limit(1/x, x, S.Infinity)
>>> l.doit()
0

默认情况下,极限是从正方向计算的,除非要计算极限的值是正无穷或负无穷。在正无穷的情况下,方向为负,反之亦然。你可以通过以下方式更改默认方向:

>>> Limit(1/x, x, 0, dir='-').doit()
-oo

在这里,我们计算

image

当我们从负侧逼近 0 时,x的极限值趋近于负无穷。另一方面,如果我们从正侧逼近 0,极限值趋近于正无穷:

>>> Limit(1/x, x, 0, dir='+').doit()
oo

Limit类还处理具有不定形极限的函数,

image

自动地:

>>> from sympy import Symbol, sin
>>> Limit(sin(x)/x, x, 0).doit()
1

你很可能使用了 l'Hôpital 法则来找到这种极限,但正如我们在这里看到的,Limit类为我们处理了这个问题。

连续复利

假设你在银行存入了 1 美元。这笔存款是本金,它会支付你利息——在这种情况下,利息是每年复利n次,年利率为 100%。你在 1 年后的最终金额为:

image

杰出的数学家詹姆斯·伯努利发现,当n的值增加时,项(1 + 1/n)^n趋近于e的值——这个常数我们可以通过求函数的极限来验证:

>>> from sympy import Limit, Symbol, S
>>> n = Symbol('n')
>>> Limit((1+1/n)**n, n, S.Infinity).doit()
E

对于任何本金 p、任何利率 r 和任何年份 t,复利的计算公式为:

image

假设连续复利,我们可以通过以下公式求出 A 的表达式:

>>> from sympy import Symbol, Limit, S
>>> p = Symbol('p', positive=True)
>>> r = Symbol('r', positive=True)
>>> t = Symbol('t', positive=True)
>>> Limit(p*(1+r/n)**(n*t), n, S.Infinity).doit()
p*exp(r*t)

我们创建了三个符号对象,分别表示本金 p、利率 r 和年份 t。我们还告诉 SymPy 这些符号将假定为正值,在创建 Symbol 对象时传入 positive=True 关键字参数。如果我们不指定,SymPy 不知道符号可以假定哪些数值,可能无法正确计算极限。然后,我们将复利表达式输入,创建 Limit 对象,并使用 doit() 方法求值。结果是 p*exp(r*t),这告诉我们复利随着时间的推移呈指数增长,假设利率是固定的。

瞬时变化率

假设一辆汽车沿着道路行驶。它均匀加速,使得行驶的距离 S 由函数给出:

S(t) = 5t² + 2t + 8。

在这个函数中,自变量是 t,它表示汽车开始移动以来经过的时间。

如果我们测量在时间 t[1] 和 t[2] 之间的行驶距离,其中 t[2] > t[1],那么我们可以使用以下表达式计算汽车在 1 单位时间内的行驶距离:

image

这也被称为函数 S(t) 相对于变量 t 的平均变化率,或者换句话说,平均速度。如果我们将 t[2] 写作 t[1] + δ[t]——其中 δ[t]t[2] 和 t[1] 之间的时间差——我们可以将平均速度的表达式重写为:

image

这个表达式也是一个以 t[1] 为变量的函数。现在,如果我们进一步假设 δ[t] 非常小,以至于它接近于 0,我们可以使用极限符号将其写作:

image

我们现在将计算上述极限。首先,让我们创建各种表达式对象:

   >>> from sympy import Symbol, Limit
   >>> t = Symbol('t')
➊ >>> St = 5*t**2 + 2*t + 8

   >>> t1 = Symbol('t1')
   >>> delta_t = Symbol('delta_t')

➋ >>> St1 = St.subs({t: t1})
➌ >>> St1_delta = St.subs({t: t1 + delta_t})

我们首先在 ➊ 处定义函数 S(t)。然后,我们定义两个符号,t1delta_t,分别对应 t[1] 和 δ[t]。通过使用 subs() 方法,我们然后通过在 ➋ 和 ➌ 处分别将 t 的值替换为 t1t1_delta_t,来找到 S(t[1]) 和 S(t[1] + δ[t])。

现在,让我们计算这个极限:

>>> Limit((St1_delta-St1)/delta_t, delta_t, 0).doit()
10*t1 + 2

极限结果是 10*t1 + 2,它是 S(t) 在时间 t1 的变化率,或者说是瞬时变化率。这个变化通常被称为汽车在瞬时时间 t1瞬时速度

我们在这里计算的极限被称为函数的导数,并且我们可以直接使用 SymPy 的 Derivative 类来计算它。

求函数的导数

函数的导数y = f(x)表示因变量y相对于自变量x的变化率。它通常表示为f′(x)或dy/dx。我们可以通过创建Derivative类的对象来求得一个函数的导数。让我们使用之前的函数——表示汽车运动的函数——作为示例:

➊ >>> from sympy import Symbol, Derivative

   >>> t = Symbol('t')
   >>> St = 5*t**2 + 2*t + 8

➋ >>> Derivative(St, t)
   Derivative(5*t**2 + 2*t + 8, t)

我们在➊导入了Derivative类。在➋,我们创建了一个Derivative类的对象。创建对象时传入的两个参数是函数St和符号t,它对应变量t。和Limit类一样,Derivative类的对象被返回,但导数并没有实际计算。我们在未求值的Derivative对象上调用doit()方法来求导:

>>> d = Derivative(St, t)
>>> d.doit()
10*t + 2

导数的表达式为10*t + 2。现在,如果我们想计算导数在某个特定值的值——例如,t = t[1]或t = 1——我们可以使用subs()方法:

>>> d.doit().subs({t:t1})
10*t1 + 2
>>> d.doit().subs({t:1})
12

让我们尝试一个复杂的任意函数,以x为唯一变量:(x³ + x² + x) × (x² + x)。

>>> from sympy import Derivative, Symbol
>>> x = Symbol('x')
>>> f = (x**3 + x**2 + x)*(x**2+x)
>>> Derivative(f, x).doit()
(2*x + 1)*(x**3 + x**2 + x) + (x**2 + x)*(3*x**2 + 2*x + 1)

你可以将这个函数视为两个独立函数的乘积,这意味着,如果手动求导,我们需要使用乘积法则来求导。但是在这里我们不需要担心这些,因为我们只需创建一个Derivative类的对象,它会为我们处理这些。

尝试一些其他复杂的表达式,比如涉及三角函数的表达式。

一个导数计算器

现在让我们编写一个导数计算器程序,它将接受一个函数作为输入,然后输出对指定变量求导后的结果:

   '''
   Derivative calculator
   '''

   from sympy import Symbol, Derivative, sympify, pprint
   from sympy.core.sympify import SympifyError

   def derivative(f, var):
       var = Symbol(var)
       d = Derivative(f, var).doit()
       pprint(d)

   if __name__=='__main__':

➊     f = input('Enter a function: ')
       var = input('Enter the variable to differentiate with respect to: ')
       try:
➋         f = sympify(f)
       except SympifyError:
           print('Invalid input')
       else:
➌         derivative(f, var)

在➊,我们要求用户输入一个待求导的函数,然后询问对哪个变量进行求导。在➋,我们使用sympify()函数将输入的函数转换为 SymPy 对象。我们将这个函数放在try...except块中,这样如果用户输入了无效的内容,我们可以显示错误信息。如果输入表达式是有效的,我们会在➌调用导数函数,传入转换后的表达式和待求导的变量作为参数。

derivative()函数中,我们首先创建一个Symbol对象,表示要对其求导的变量。我们使用标签var来表示这个变量。接下来,我们创建一个Derivative对象,传入待求导的函数和符号对象var。我们立即调用doit()方法来计算导数,然后使用pprint()函数打印结果,以便它与数学表达式的形式接近。以下是程序的示例执行:

Enter a function: 2*x**2 + 3*x + 1
Enter the variable to differentiate with respect to: x
4·x + 3

下面是使用两个变量函数时的一个示例运行:

Enter a function: 2*x**2 + y**2
Enter the variable to differentiate with respect to: x
4·x

计算偏导数

在前面的程序中,我们看到使用 Derivative 类可以计算多变量函数相对于任何变量的导数。这种计算通常称为偏导数,其中意味着我们假设只有一个变量发生变化,而其他变量保持不变。

让我们考虑函数 f(x, y) = 2xy + xy²。关于 x 的偏导数是

image

上述程序能够找到偏导数,因为这仅仅是指定正确的变量的问题:

Enter a function: 2*x*y + x*y**2
Enter the variable to differentiate with respect to: x
y2 + 2·y

注意

在本章中,我做出的一个关键假设是,我们计算导数的所有函数在其各自的定义域内都是可导的。

高阶导数及寻找极值

默认情况下,通过 Derivative 类创建导数对象时,会计算一阶导数。要计算高阶导数,只需在创建 Derivative 对象时,将导数的阶数作为第三个参数指定即可。在本节中,我将向你展示如何使用一阶和二阶导数来找出函数在某个区间内的最大值和最小值。

考虑函数 x⁵ – 30x³ + 50x,其定义域为 [–5, 5]。请注意,我使用方括号表示一个闭区间,意味着变量 x 可以取任何大于或等于 –5 且小于或等于 5 的实数值(见 图 7-3)。

image

图 7-3:函数 x⁵ – 30x³ + 50x* 的图像,其中 –5* ≤ x ≤ 5

从图中可以看出,函数在区间 –2 ≤ x ≤ 0 上的点 B 达到了最小值。同样,在区间 0 ≤ x ≤ 2 上,函数在点 C 达到了最大值。另一方面,函数在我们考虑的整个 x 定义域上分别在点 AD 达到了最大值和最小值。因此,当我们考虑整个区间 [–5, 5] 时,点 BC 分别被称为局部最小值局部最大值,而点 AD 则分别是全局最大值全局最小值

极值(复数形式为 极值点)指的是函数达到局部最大值或最小值的点。如果 x 是函数 f(x) 的一个极值点,那么 fx 处的一阶导数 f′(x) 必须为零。这个性质表明,寻找可能的极值点的一个有效方法是尝试解方程 f′(x) = 0。这样的解称为函数的临界点。让我们来试试:

>>> from sympy import Symbol, solve, Derivative
>>> x = Symbol('x')
>>> f = x**5 - 30*x**3 + 50*x
>>> d1 = Derivative(f, x).doit()

现在我们已经计算了一级导数 f′(x),接下来我们将解 f′(x) = 0 来找到临界点:

>>> critical_points = solve(d1)
>>> critical_points
[-sqrt(-sqrt(71) + 9), sqrt(-sqrt(71) + 9), -sqrt(sqrt(71) + 9),
sqrt(sqrt(71) + 9)]

这里列出的critical_points中的数字分别对应点BCAD。我们将创建标签来引用这些点,然后可以在命令中使用这些标签:

>>> A = critical_points[2]
>>> B = critical_points[0]
>>> C = critical_points[1]
>>> D = critical_points[3]

因为该函数的所有关键点都位于考虑的区间内,所以它们对于我们寻找f(x)的全局最大值和最小值都是相关的。现在,我们可以应用所谓的二阶导数测试来缩小哪些关键点可能是全局最大值或最小值。

首先,我们计算函数f(x)的二阶导数。请注意,为此,我们输入2作为第三个参数:

>>> d2 = Derivative(f, x, 2).doit()

现在,我们通过将每个关键点的值依次代入x来求得二阶导数的值。如果结果小于 0,则该点是局部最大值;如果结果大于 0,则该点是局部最小值。如果结果为 0,则测试结果不确定,我们无法推断出关键点x是局部最小值、最大值,还是两者都不是。

>>> d2.subs({x:B}).evalf()
127.661060789073
>>> d2.subs({x:C}).evalf()
-127.661060789073
>>> d2.subs({x:A}).evalf()
-703.493179468151
>>> d2.subs({x:D}).evalf()
703.493179468151

在关键点处评估二阶导数测试告诉我们,点AC是局部最大值,点BD是局部最小值。

在区间[–5, 5]上,f(x)的全局最大值和最小值要么出现在关键点x处,要么出现在域的端点(x = –5 和 x = 5)。我们已经找到了所有关键点,分别是点ABCD。由于点AC是局部最大值,因此函数不能在这两个点处达到全局最小值。通过类似的逻辑,函数也不能在点BD处达到全局最大值。

因此,为了找到全局最大值,我们必须计算f(x)在点AC、–5 和 5 处的值。在这些点中,f(x)值最大的地方必须是全局最大值。

我们将创建两个标签,x_minx_max,用来引用域边界,并在点ACx_minx_max处评估函数:

>>> x_min = -5
>>> x_max = 5

>>> f.subs({x:A}).evalf()
705.959460380365
>>> f.subs({x:C}).evalf()
25.0846626340294
>>> f.subs({x:x_min}).evalf()
375.000000000000
>>> f.subs({x:x_max}).evalf()
-375.000000000000

通过这些计算,以及通过检查所有关键点和区间边界的函数值(见图 7-3),我们发现点A是全局最大值。

类似地,为了确定全局最小值,我们必须计算f(x)在点BD、–5 和 5 处的值:

>>> f.subs({x:B}).evalf()
-25.0846626340294
>>> f.subs({x:D}).evalf()
-705.959460380365
>>> f.subs({x:x_min}).evalf()
375.000000000000
>>> f.subs({x:x_max}).evalf()
-375.000000000000

f(x)取最小值的点必须是该函数的全局最小值;这就是点D

这种寻找函数极值的方法——通过考虑所有关键点(在可能通过二阶导数测试排除某些点后)和边界值的函数值——只要函数是二次可导的,就总是有效的。也就是说,函数的第一导数和第二导数在域内必须处处存在。

对于像e^x这样的函数,可能在定义域内没有任何临界点,但在这种情况下,该方法仍然有效:它只告诉我们极值出现在定义域的边界上。

使用梯度上升法寻找全局最大值

有时候,我们仅仅关注找到一个函数的全局最大值,而不是所有局部和全局的最大值和最小值。例如,我们可能想要发现一个投球的投射角度,使得球能覆盖最大水平距离。我们将学习一种新的、更实用的方法来解决这种问题。这个方法仅使用一阶导数,因此仅适用于那些可以计算一阶导数的函数。

该方法被称为梯度上升法,它是一种寻找全局最大值的迭代方法。由于梯度上升法涉及大量的计算,因此它是通过编程而非手工解决的理想方法。让我们通过寻找投射角度的例子来尝试一下。在第二章中,我们推导出了以下表达式:

image

计算一个物体在投射运动中的飞行时间,该物体以速度u在角度θ下投射。投射的射程R,是物体所走的总水平距离,由u[x] × t[flight]给出。这里,u[x]是初速度的水平分量,等于u cosθ。代入u[x]t[flight]的公式,我们得到以下表达式:

image

图 7-4 中的图表显示了θ在 0 到 90 度之间的取值,以及每个角度对应的射程(飞行距离)。从图中我们可以看到,当投射角度约为 45 度时,射程最大。接下来我们将学习如何使用梯度上升法来数值求解这个θ值。

image

图 7-4:初速度为 25 m/s 的投射物在不同投射角度下的射程

梯度上升法是一种迭代方法:我们从一个初始值θ开始——假设为 0.001,或者θ[old] = 0.001——然后逐渐接近与最大射程对应的θ值(见图 7-5)。使我们逐渐接近的步骤是以下方程:

image

其中,λ步长,并且

image

Rθ的导数。我们将θ[old] = 0.001 后,执行如下步骤:

  1. 使用上述方程计算θ[new]。

  2. 如果θ[new] – θ[old]的绝对差值大于某个值ε,我们将θ[old]设置为θ[new]并返回第 1 步。否则,进入第 3 步。

  3. θ[new]是一个近似值,它是使R取得最大值的θ

epsilonε)的值决定了我们何时决定停止算法的迭代。在《步长和 Epsilon 的作用》中有详细讨论,见第 197 页。

image

图 7-5:梯度上升法通过迭代不断接近函数的最大值。

以下grad_ascent()函数实现了梯度上升算法。参数x0是开始迭代时变量的初始值,f1x是我们想要找到最大值的函数的导数,x是与该函数变量对应的Symbol对象。

   '''
   Use gradient ascent to find the angle at which the projectile
   has maximum range for a fixed velocity, 25 m/s
   '''

   import math
   from sympy import Derivative, Symbol, sin

   def grad_ascent(x0, f1x, x):
➊     epsilon = 1e-6
➋     step_size = 1e-4
➌     x_old = x0
➍     x_new = x_old + step_size*f1x.subs({x:x_old}).evalf()
➎     while abs(x_old - x_new) > epsilon:
           x_old = x_new
           x_new = x_old + step_size*f1x.subs({x:x_old}).evalf()

       return x_new

➏ def find_max_theta(R, theta):
       # Calculate the first derivative
       R1theta = Derivative(R, theta).doit()
       theta0 = 1e-3
       theta_max = grad_ascent(theta0, R1theta, theta)
➐     return theta_max

   if __name__ == '__main__':

       g = 9.8
       # Assume initial velocity
       u = 25
       # Expression for range
       theta = Symbol('theta')
➑     R = u**2*sin(2*theta)/g

➒     theta_max = find_max_theta(R, theta)
       print('Theta: {0}'.format(math.degrees(theta_max)))
       print('Maximum Range: {0}'.format(R.subs({theta:theta_max})))

我们在➊和➋处分别将 epsilon 值设置为1e-6,步长设置为1e-4。epsilon 值必须始终是一个非常小的正值,接近 0,步长应该选择得足够小,以便在每次算法迭代时变量的增量较小。epsilon 值和步长的选择在《步长和 Epsilon 的作用》中有更详细的讨论,见第 197 页。

我们在➌处将x_old设置为x0,并在➍处首次计算x_new。我们使用subs()方法将x_old的值代入变量,然后使用evalf()计算数值。如果绝对差abs(x_old – x_new)大于epsilon,则在➎处的while循环将继续执行,我们将根据梯度上升算法的第 1 步和第 2 步不断更新x_oldx_new的值。一旦退出循环——即abs(x_old – x_new) > epsilon——我们返回x_new,即对应最大函数值的变量值。

我们在➏处开始定义find_max_theta()函数。在这个函数中,我们计算R的一阶导数;创建一个标签theta0,并将其设置为1e-3;然后调用grad_ascent()函数,传入这两个值作为参数,以及第三个参数,即符号对象theta。当我们得到对应最大函数值的θ(即theta_max)后,我们在➐处返回该值。

最后,我们在➑处创建表示水平射程的表达式,设置初速度为u = 25,并使用与角度θ对应的theta符号对象。然后,我们在➒处调用find_max_theta()函数,传入Rtheta

当你运行这个程序时,你应该看到如下输出:

Theta: 44.99999978475661
Maximum Range: 63.7755102040816

θ的值以度为单位打印,结果接近 45 度,正如预期的那样。如果你将初速度改为其他值,你会发现达到最大射程的投射角始终接近 45 度。

一个通用的梯度上升程序

我们可以稍微修改前面的程序,制作一个通用的梯度上升程序:

   '''
   Use gradient ascent to find the maximum value of a
   single-variable function
   '''

   from sympy import Derivative, Symbol, sympify

   def grad_ascent(x0, f1x, x):
       epsilon = 1e-6
       step_size = 1e-4
       x_old = x0
       x_new = x_old + step_size*f1x.subs({x:x_old}).evalf()
       while abs(x_old - x_new) > epsilon:
           x_old = x_new
           x_new = x_old + step_size*f1x.subs({x:x_old}).evalf()

       return x_new

   if __name__ == '__main__':

       f = input('Enter a function in one variable: ')
       var = input('Enter the variable to differentiate with respect to: ')
       var0 = float(input('Enter the initial value of the variable: '))
       try:
           f = sympify(f)
       except SympifyError:
           print('Invalid function entered')
       else:
➊         var = Symbol(var)
➋         d = Derivative(f, var).doit()
➌         var_max = grad_ascent(var0, d, var)
           print('{0}: {1}'.format(var.name, var_max))
           print('Maximum value: {0}'.format(f.subs({var:var_max})))

函数grad_ascent()在这里保持不变。不过,现在程序要求用户输入函数、函数中的变量以及变量的初始值,从哪里开始进行梯度上升。一旦我们确定 SymPy 能够识别用户输入,我们会在➊处创建一个与变量对应的 Symbol 对象,在➋处找到关于它的第一阶导数,并使用这三个参数调用grad_ascent()函数。在➌处返回最大值。

这里是一次示例运行:

Enter a function in one variable: 25*25*sin(2*theta)/9.8
Enter the variable to differentiate with respect to: theta
Enter the initial value of the variable: 0.001
theta: 0.785360029379083
Maximum value: 63.7755100185965

这里的函数输入与我们第一次实现梯度上升时相同,θ的值以弧度显示。

这是程序的另一次运行,它将找到 cosy的最大值:

Enter a function in one variable: cos(y)
Enter the variable to differentiate with respect to: y
Enter the initial value of the variable: 0.01
y: 0.00999900001666658
Maximum value: 0.999950010415832

这个程序对于像cos(y) + k这样的函数也能正确工作,其中k是常数:

Enter a function in one variable: cos(y) + k
Enter the variable to differentiate with respect to: y
Enter the initial value of the variable: 0.01
y: 0.00999900001666658
Maximum value: k + 0.999950010415832

然而,像cos(ky)这样的函数无法正常工作,因为它的一级导数kcos(ky)仍然包含k,而 SymPy 对k的值一无所知。因此,SymPy 无法执行梯度上升算法中的一个关键步骤——即比较abs(x_old - x_new) > epsilon

关于初始值的警告

从哪个初始值开始迭代梯度上升法在算法中起着非常重要的作用。考虑函数 x⁵ – 30x³ + 50x,我们在图 7-3 中作为例子使用过它。我们用我们通用的梯度上升程序来寻找最大值:

Enter a function in one variable: x**5 - 30*x**3 + 50*x
Enter the variable to differentiate with respect to: x
Enter the initial value of the variable: -2
x: -4.17445116397103
Maximum value: 705.959460322318

梯度上升算法在找到最接近的峰值时停止,这不一定是全局最大值。在这个例子中,当你从初始值-2 开始时,它停在了一个峰值,这个峰值也对应于考虑区间中的全局最大值(大约是 706)。为了进一步验证这一点,让我们尝试不同的初始值:

Enter a function in one variable: x**5 - 30*x**3 + 50*x
Enter the variable to differentiate with respect to: x
Enter the initial value of the variable: 0.5
x: 0.757452532565767
Maximum value: 25.0846622605419

在这种情况下,梯度上升算法停止的最接近的峰值不是函数的真实全局最大值。图 7-6 展示了这两种情况的梯度上升算法结果。

image

图 7-6:不同初始值下梯度上升算法的结果。梯度上升始终将我们带到最接近的峰值。

因此,在使用该方法时,必须小心选择初始值。算法的一些变体试图解决这个局限性。

步长和ε的作用

在梯度上升算法中,变量的下一个值是通过以下公式计算的:

image

其中λ步长。步长决定了下一步的距离。它应该足够小,以避免越过峰值。也就是说,如果当前的x值接近函数最大值所对应的值,那么下一步就不应该超越这个峰值,否则算法将失败。另一方面,步长太小会导致计算时间过长。我们使用了一个固定的步长 10^(–3),但这可能不是对所有函数最合适的值。

确定何时停止算法迭代的ε(ε)值应该足够小,以至于我们可以确认x的值不再发生变化。我们期望在最大值点,第一导数f′(x)为 0,并且理想情况下,绝对差值|θ[new] – θ[old]|为 0(参见第 192 页的梯度上升算法第 2 步)。然而,由于数值不准确,我们可能无法精确得到 0 的差值;因此,ε的值被选为接近 0 的一个值,实际上它可以告诉我们x的值已经不再变化。我使用了 10^(–6)作为所有函数的ε值。这个值尽管足够小,并且适用于那些具有f′(x) = 0 解的函数,例如sin(x),但对于其他函数,这个值可能并不合适。因此,最好在最后验证最大值以确保其正确性,并在需要时相应地调整epsilon值。

梯度上升算法的第 2 步还意味着,为了让算法终止,方程f′(x) = 0 必须有解,但对于像e^x或 log(x)这样的函数,这并不成立。因此,如果你将这些函数作为输入提供给前面的程序,程序将不会给出解,并且将继续运行。我们可以通过增加一个检查是否f′(x) = 0 有解的步骤,使梯度上升程序在这种情况下变得更有用。以下是修改后的程序:

   '''
   Use gradient ascent to find the maximum value of a
   single-variable function. This also checks for the existence
   of a solution for the equation f'(x)=0.
   '''

   from sympy import Derivative, Symbol, sympify, solve

   def grad_ascent(x0, f1x, x):
       # Check if f1x=0 has a solution
➊     if not solve(f1x):
           print('Cannot continue, solution for {0}=0 does not exist'.format(f1x))
           return
       epsilon = 1e-6
       step_size = 1e-4
       x_old = x0
       x_new = x_old + step_size*f1x.subs({x:x_old}).evalf()
       while abs(x_old - x_new) > epsilon:
           x_old = x_new
           x_new = x_old + step_size*f1x.subs({x:x_old}).evalf()

       return x_new

   if __name__ == '__main__':

       f = input('Enter a function in one variable: ')
       var = input('Enter the variable to differentiate with respect to: ')
       var0 = float(input('Enter the initial value of the variable: '))
       try:
           f = sympify(f)
       except SympifyError:
           print('Invalid function entered')
       else:
           var = Symbol(var)
           d = Derivative(f, var).doit()
           var_max = grad_ascent(var0, d, var)
➋         if var_max:
              print('{0}: {1}'.format(var.name, var_max))
              print('Maximum value: {0}'.format(f.subs({var:var_max})))

grad_ascent()函数的这个修改中,我们在➊处调用了 SymPy 的solve()函数来判断方程f′(x) = 0(这里是f1x)是否有解。如果没有解,我们将打印一条信息并返回。另一个修改出现在➋处的__main__块中。我们检查grad_ascent()函数是否成功返回了结果;如果返回了结果,我们接着打印函数的最大值以及相应的变量值。

这些变化使得程序可以处理像 log(x)和e^x这样的函数:

Enter a function in one variable: log(x)
Enter the variable to differentiate with respect to: x
Enter the initial value of the variable: 0.1
Cannot continue, solution for 1/x=0 does not exist

对于e^x你也会看到类似的情况。

梯度下降算法

梯度上升算法的反向算法是梯度下降算法,它是一种寻找函数最小值的方法。它与梯度上升算法类似,但我们不是“向上爬”沿着函数,而是“向下爬”。第 205 页的挑战#2 讨论了这两种算法之间的区别,并给出了实现反向算法的机会。

寻找函数的积分

一个函数fx)的不定积分反导数是另一个函数Fx),使得F′(x) = fx)。也就是说,一个函数的积分是另一个函数,其导数为原函数。在数学上,它写作Fx) = ∫ fxdx。而定积分则是

image

它实际上是Fb)– Fa),其中Fb)和Fa)分别是函数的反导数在x = bx = a时的值。我们可以通过创建Integral对象来找到这两个积分。

这是我们如何找到积分∫ kxdx,其中k是常数项:

>>> from sympy import Integral, Symbol
>>> x = Symbol('x')
>>> k = Symbol('k')
>>> Integral(k*x, x)
Integral(k*x, x)

我们导入IntegralSymbol类,并创建两个对应于kxSymbol对象。然后,我们创建一个Integral对象,使用函数kx,并指定要积分的变量是x。类似于LimitDerivative类,我们现在可以使用doit()方法来求解积分:

>>> Integral(k*x, x).doit()
k*x**2/2

该积分的结果为kx²/2。如果你计算kx²/2 的导数,你会得到原始函数kx

要找到定积分,我们只需在创建Integral对象时,指定变量、下限和上限作为一个元组:

>>> Integral(k*x, (x, 0, 2)).doit()
2*k

返回的结果是定积分

image

通过在几何上下文中讨论定积分,可能对我们有帮助。考虑图 7-7,该图显示了函数fx) = xx = 0 和x = 5 之间的图形。

现在考虑图形ABDE下方的区域,该区域由x轴和点x = 2 与x = 4(分别为点AB)之间的区域所界定。该区域的面积可以通过加上正方形ABCE的面积和直角三角形ECD的面积来计算,面积为 2 × 2 + (1/2) × 2 × 2 = 6。

image

图 7-7:一个函数在两点之间的定积分是由函数图形与x轴所围成的面积。

现在让我们计算积分!image

>>> from sympy import Integral, Symbol
>>> x = Symbol('x')
>>> Integral(x, (x, 2, 4)).doit()
6

积分的值恰好等于区域ABDE的面积。这并不是巧合;你会发现,对于任何可以确定积分的x函数,情况都是如此。

理解定积分是由函数在指定的 x 轴点之间包围的面积,这是理解涉及连续随机变量的随机事件概率计算的关键。

概率密度函数

让我们考虑一个虚构班级的学生及其在数学小测验中的成绩。每个学生的成绩可以在 0 到 20 之间,包括小数成绩。如果我们把成绩看作一个随机事件,那么成绩本身是一个连续随机变量,因为它可以在 0 到 20 之间取任何值。如果我们想计算一个学生在 11 和 12 之间得到成绩的概率,我们不能应用我们在第五章中学到的策略。为了说明为什么,假设均匀概率,我们来看一下公式,

image

其中 E 是 11 到 12 之间所有可能成绩的集合,S 是所有可能成绩的集合——即 1 到 20 之间的所有实数。根据我们对前述问题的定义,n(E) 是无限的,因为不可能计算出 11 和 12 之间所有可能的实数;n(S) 同样也是如此。因此,我们需要一种不同的方法来计算概率。

概率密度函数P(x), 表示随机变量取值接近 x 的概率,一个任意的值。¹ 它也可以告诉我们 x 落在某个区间内的概率。也就是说,如果我们知道表示我们虚构班级成绩概率的概率密度函数,计算 P(11 < x < 12) 就能给我们我们需要的概率。但我们该如何计算呢?原来,这个概率就是由概率密度函数的图形与 x 轴之间、x = 11 和 x = 12 之间的区域面积。假设一个任意的概率密度函数,图 7-8 展示了这一点。

image

图 7-8:数学小测验成绩的概率密度函数

我们已经知道这个区域的面积等于积分的值,

image

因此,我们有了一种简单的方法来找到成绩在 11 和 12 之间的概率。数学问题解决之后,我们现在可以找出概率是多少了。我们先前假设的概率密度函数是

image

其中 x 是所获得的成绩。这个函数的选择使得成绩接近 10(不论是大于还是小于 10)的概率较高,但随后会急剧下降。

现在,让我们计算积分

image

其中 p(x) 是前面提到的函数:

>>> from sympy import Symbol, exp, sqrt, pi, Integral
>>> x = Symbol('x')
>>> p = exp(-(x - 10)**2/2)/sqrt(2*pi)
>>> Integral(p, (x, 11, 12)).doit().evalf()
0.135905121983278

我们为这个函数创建了 Integral 对象,p 代表概率密度函数,指定我们想要计算 x 轴上 11 和 12 之间的定积分。我们使用 doit() 评估函数,并通过 evalf() 找到数值结果。因此,成绩在 11 和 12 之间的概率接近 0.14。

概率密度函数:一个警告

严格来说,这个密度函数为低于 0 或高于 20 的成绩分配了一个非零概率。然而,正如您可以通过本节中的思想检查到的那样,这样的事件的概率非常小,以至于对于我们的目的来说可以忽略不计。

概率密度函数有两个特殊性质:(1)对于任何 x,函数值总是大于 0,因为概率不能小于 0;(2)定积分的值

image

等于 1。第二个性质值得讨论。因为 p(x) 是一个概率密度函数,它所围成的面积,也就是积分的值

image

在任何两个点 x = ax = b 之间,给出了 x 位于 x = ax = b 之间的概率。这也意味着无论 ab 的值是多少,积分的值都不能超过 1,因为根据定义,概率不能大于 1。因此,即使 ab 是非常大的值,趋近于–∞ 和 ∞,积分的值仍然是 1,正如我们可以自己验证的那样:

>>> from sympy import Symbol, exp, sqrt, pi, Integral, S
>>> x = Symbol('x')
>>> p = exp(-(x – 10)**2/2)/sqrt(2*pi)
>>> Integral(p, (x, S.NegativeInfinity, S.Infinity)).doit().evalf()
1.00000000000000

S.NegativeInfinityS.Infinity 分别表示负无穷和正无穷,我们在创建 Integral 对象时将它们指定为下限和上限。

当我们处理连续随机变量时,可能会出现一个棘手的情况。在离散概率中,像公平六面骰子掷出 7 点这样的事件的概率是 0。我们称概率为 0 的事件为不可能事件。在连续随机变量的情况下,变量取某个确切值的概率是 0,即使这个事件是可能的。例如,学生的成绩恰好是 11.5 是可能的,但由于连续随机变量的特性,概率为 0。要了解为什么这样,考虑到概率将是积分的值

image

因为这个积分的下限和上限相同,所以它的值为 0。这有点违反直觉且显得矛盾,因此我们来尝试理解它。

考虑我们之前讨论的成绩范围——0 到 20。学生可以获得的成绩可以是这个区间内的任何数字,这意味着有无限多个数字。如果每个数字被选中的概率相等,那么这个概率是多少呢?根据离散概率公式,这应该是 1/∞,也就是一个非常小的数字。事实上,这个数字小到在所有实际应用中,我们通常认为它是 0。因此,成绩为 11.5 的概率是 0。

你学到的内容

在本章中,你学习了如何求解函数的极限、导数和积分。你学习了使用梯度上升法寻找函数最大值,并且看到如何应用积分原理来计算连续随机变量的概率。接下来,你有一些任务需要尝试。

编程挑战

以下挑战建立在你在本章所学的基础上。你可以在www.nostarch.com/doingmathwithpython/找到示例解答。

#1:验证函数在某一点的连续性

函数在某一点可导的必要但不充分条件是该函数在该点必须是连续的。也就是说,函数必须在该点有定义,并且其左极限和右极限必须存在并等于该点的函数值。如果fx)是函数,x = a是我们感兴趣的评估点,那么这在数学上可以表示为

image

你的挑战是编写一个程序,要求(1)接受一个单变量函数及该变量的值作为输入,且(2)检查输入函数在该点处是否连续,其中该点为变量的输入值。

这里是完成解决方案的示例:

Enter a function in one variable: 1/x
Enter the variable: x
Enter the point to check the continuity at: 1
1/x is continuous at 1.0

函数 1/x在 0 处是不连续的,我们来验证一下:

Enter a function in one variable: 1/x
Enter the variable: x
Enter the point to check the continuity at: 0
1/x is not continuous at 0.0

#2:实现梯度下降法

梯度下降法用于寻找函数的最小值。与梯度上升法类似,梯度下降法是一种迭代方法:我们从变量的初始值开始,逐渐接近对应于函数最小值的变量值。使我们接近的步骤是

image

其中λ是步长,且

image

这是对函数进行求导的结果。因此,和梯度上升法的唯一区别在于我们如何从x_old获取x_new的值。

你的挑战是使用梯度下降算法实现一个通用程序,用于找到用户指定的单变量函数的最小值。该程序还应该创建函数的图形,并展示在找到最小值之前找到的所有中间值。(你可能需要参考图 7-5 中的内容,见第 193 页)。

#3: 两条曲线之间的面积

我们学到,积分

image

表示由函数 f(x) 和 x 轴之间的 x = ax = b 所围成的面积。两条曲线之间的面积可以表示为积分

image

ab 是两条曲线交点的坐标,且 a < b。函数 f(x) 被称为 上函数,而 g(x) 被称为 下函数。图 7-9 展示了这一点,假设 f(x) = xg(x) = x²,且 a = 0,b = 1。

你这里的挑战是编写一个程序,允许用户输入任意两个单变量函数 x,并打印这两个函数之间围成的面积。程序应明确要求第一个输入的函数为上函数,并询问计算面积的 x 值范围。

image

图 7-9:函数 f(x) = x * 和 g(x) =* x*² 在 *x = *0 到 x = 1 之间围成的面积。

#4: 求曲线长度

假设你刚刚骑行完一条大致像图 7-10 那样的道路。因为你没有里程表,所以你想知道是否有数学方法来确定你骑行的距离。首先,我们需要找到一个方程——即使是近似方程——来描述这条路径。

image

图 7-10:骑行路径的近似图

注意它看起来和我们在前几章讨论过的二次函数非常相似?事实上,在这个挑战中,假设方程为 y = f(x) = 2x² + 3x + 1,且你从点 A (–5, 36) 循环到点 B (10, 231)。要找到这段弧线的长度——即你骑行的距离——我们需要计算积分

image

其中 y 描述了前述函数。你这里的挑战是编写一个程序来计算弧线 AB 的长度。

你也可能想要将你的解决方案泛化,使其能够找到任何两个点之间的弧线长度,适用于任何任意的函数 f(x)。

第八章:后记

image

你已经完成了这本书——干得不错!现在,你已经学会了如何处理数字、生成图表、应用数学运算、操作集合和代数表达式、创建动画可视化以及解决微积分问题——哇!接下来应该做什么呢?这里有一些可以尝试的内容。¹

接下来可以探索的内容

我希望这本书能激励你去解决你自己的数学问题。但通常很难自己想出这样的挑战。

Project Euler

寻找需要你实现编程解决方案的数学问题的一个权威网站是 Project Euler (projecteuler.net/),该网站提供了 500 多个不同难度的数学问题。你可以创建一个免费账户,提交你的解决方案以检查它们是否正确。

Python 文档

你可能还想开始探索 Python 的各种功能文档。

• 数学模块: docs.python.org/3/library/math.html

• 其他数字和数学模块: docs.python.org/3/library/numeric.html

• 统计模块: docs.python.org/3/library/statistics.html

我们没有讨论浮动点数是如何存储在计算机内存中的,或者由此可能产生的错误和问题。你可能想查阅十进制模块的文档,以及 Python 教程中关于“浮动点运算”的讨论,以了解这个话题:

• 十进制模块: docs.python.org/3/library/decimal.html

• 浮动点运算: docs.python.org/3.4/tutorial/floatingpoint.html

书籍

如果你有兴趣探索更多数学和编程话题,可以查看以下书籍:

使用 Python 发明你自己的计算机游戏使用 Python 和 Pygame 制作游戏(由 Al Sweigart 编著,两者都可以在 inventwithpython.com/ 免费获取)并未专门讨论解决数学问题,但它们在使用 Python 编写计算机游戏时应用了数学知识。

Think Stats: Probability and Statistics for Programmers(Allen B. Downey 著)是一本免费书籍 (greenteapress.com/thinkstats/)。正如书名所示,它深入探讨了统计学和概率学的话题,超出了本书所讨论的内容。

教你的孩子编程(Bryson Payne 著,No Starch Press,2015)是一本面向初学者的书,涵盖了各种 Python 主题。你将学习到海龟图形、使用 Python random 模块的各种有趣方式,以及如何使用 Pygame 创建游戏和动画。

使用 Python 进行计算物理(Mark Newman,2013)重点讲解了一些针对物理问题的高级数学话题。然而,本书也包含了许多章节,适合任何有兴趣了解如何编写程序来解决数值和数学问题的人。

获取帮助

如果你在本书中遇到具体问题,欢迎通过电子邮件联系我,邮箱地址是 doingmathwithpython@gmail.com。如果你想深入了解我们在程序中使用的任何函数或类,首先要查看相关项目的官方文档:

• Python 3 标准库: docs.python.org/3/library/index.html

• SymPy: docs.sympy.org/

• matplotlib: matplotlib.org/contents.html

如果你在解决问题时遇到困难,也可以通过电子邮件联系项目特定的邮件列表。你可以在本书网站找到这些邮件列表的链接。

结论

最后,我们已经来到了本书的结尾。我希望你在跟随本书学习的过程中收获了很多。现在,拿起 Python,去解决更多问题吧!

第九章:A

软件安装

image

本书中的程序和解决方案已经过测试,可以在 Python 3.4、matplotlib 1.4.2、matplotlib-venn 0.11 和 SymPy 0.7.6 上运行。这些版本只是最低要求,程序也应该可以在软件的较新版本上运行。有关更改和更新的信息将发布在本书的网站上,* www.nostarch.com/doingmathwithpython/*。

虽然有许多方法可以获取 Python 以及所需的库,但最简单的方法之一是使用 Anaconda Python 3 软件分发版,它可以自由用于 Microsoft Windows、Linux 和 Mac OS X。在撰写本文时,Anaconda 的最新版本是 2.1.0,搭载 Python 3.4。Anaconda (store.continuum.io/cshop/anaconda/)是安装 Python 3 以及许多数学和数据分析包的快捷方式,所有内容都可以通过一个简易的安装程序完成。如果您想添加新的数学 Python 库,Anaconda 还允许您使用 condapip 命令快速添加它们。Anaconda 还有许多其他功能,使其在 Python 开发中非常有用。它内置了 conda 包管理器,方便安装第三方包,正如我们很快会看到的那样。它支持创建隔离的 Python 环境,这意味着您可以使用相同的 Anaconda 安装拥有多个 Python 安装——例如 Python 2、Python 3.3 和 Python 3.4。您可以通过 Anaconda 网站和 conda 文档了解更多信息 (conda.pydata.org/docs/intro.html)。

接下来的章节将简要描述在 Microsoft Windows、Linux 和 Mac OS X 上安装 Anaconda,因此请跳到适合您的部分。您需要一个互联网连接来进行安装,但仅此而已。

如果遇到任何问题,您还可以访问 continuum.io/ 获取故障排除信息。

Microsoft Windows

continuum.io/downloads 下载 Anaconda GUI 安装程序用于 Python 3。双击安装程序,然后按照以下步骤进行操作:

1. 点击下一步并接受许可协议:

image

2. 您可以选择仅为您的用户名安装该分发版,或为使用此计算机的所有用户安装。

3. 选择您希望 Anaconda 安装程序安装的位置。默认设置应该可以正常工作。

4. 请确保在高级选项对话框中勾选两个框,以便您可以从命令提示符的任何位置调用 Python shell 和其他程序,例如condapipidle。此外,任何其他寻找 Python 3.4 安装的 Python 程序将指向 Anaconda 安装的版本:

image

5. 点击 安装 开始安装。安装完成后,点击 下一步,然后点击 完成 完成安装。你应该能够在开始菜单中找到 Python。

6. 打开 Windows 命令提示符并执行以下步骤。

更新 SymPy

安装程序可能已经包含了 SymPy,但我们想确保安装至少是 0.7.6 版本,因此我们将使用以下命令进行安装:

$ conda install sympy=0.7.6

这将安装或升级到 SymPy 0.7.6。

安装 matplotlib-venn

要安装 matplotlib-venn,请使用以下命令:

$ pip install matplotlib-venn

你的计算机现在已设置好运行所有程序。

启动 Python Shell

打开 Windows 命令提示符并输入 idle 启动 IDLE shell,或输入 python 启动 Python 3 默认 shell。

Linux

Linux 安装程序作为 shell 脚本分发,因此你需要从 continuum.io/downloads 下载 Anaconda Python 安装程序。然后,通过执行以下命令启动安装程序:

$ bash Anaconda3-2.1.0-Linux-x86_64.sh

Welcome to Anaconda3 2.1.0 (by Continuum Analytics, Inc.)

In order to continue the installation process, please review the license
agreement.
Please, press ENTER to continue
>>>

将显示“Anaconda 终端用户许可协议”。阅读完毕后,输入 yes 继续安装:

Do you approve the license terms? [yes|no]
[no] >>> yes

Anaconda3 will now be installed into this location:
/home/testuser/anaconda3

  - Press ENTER to confirm the location
  - Press CTRL-C to abort the installation
  - Or specify a different location below

在提示时按 ENTER 键,安装将开始:

[/home/testuser/anaconda3] >>>
PREFIX=/home/testuser/anaconda3
installing: python-3.4.1-4 ...
installing: conda-3.7.0-py34_0
..

creating default environment...
installation finished.
Do you wish the installer to prepend the Anaconda3 install location
to PATH in your /home/testuser/.bashrc ? [yes|no]

当被要求确认安装位置时,输入 yes,以便每次从终端调用 Python 程序时,始终调用 Anaconda 安装的 Python 3.4 解释器:

[no] >>> yes

Prepending PATH=/home/testuser/anaconda3/bin to PATH in /home/testuser/.bashrc
A backup will be made to: /home/testuser/.bashrc-anaconda3.bak

For this change to become active, you have to open a new terminal.

Thank you for installing Anaconda3!

打开一个新的终端以进行下一步操作。

更新 SymPy

首先,确保已安装 SymPy 0.7.6:

$ conda install sympy=0.7.6

安装 matplotlib-venn

使用以下命令安装 matplotlib-venn:

$ pip install matplotlib-venn

启动 Python Shell

一切就绪。打开一个新的终端并输入 idle3 启动 IDLE 编辑器,或输入 python 启动 Python 3.4 shell。现在你应该能够运行所有程序并尝试新的程序。

Mac OS X

continuum.io/downloads 下载图形安装程序。然后双击 .pkg 文件并按照指示操作:

1. 在每个信息窗口上点击 继续

imageimage

2. 点击 同意 接受“Anaconda 终端用户许可协议”:

image

3. 在以下对话框中,选择“仅为我安装”选项。你看到的错误信息是安装程序软件中的一个 bug。只需点击它,它会消失。点击 继续 继续操作。

image

4. 选择 安装

image

5. 安装完成后,打开终端应用程序并按照以下步骤更新 SymPy 并安装 matplotlib-venn。

更新 SymPy

首先,确保已安装 SymPy 0.7.6:

$ conda install sympy=0.7.6

安装 matplotlib-venn

使用以下命令安装 matplotlib-venn:

$ pip install matplotlib-venn

启动 Python Shell

一切就绪。关闭终端窗口,打开一个新窗口,输入idle3启动 IDLE 编辑器,或输入python启动 Python 3.4 命令行。现在你应该能够运行所有程序并尝试新的程序。

第十章:B

Python 主题概览

image

本附录的目标是双重的:一方面是快速回顾一些在章节中没有深入介绍的 Python 主题,另一方面是引入一些可以帮助你编写更好 Python 程序的主题。

if name == 'main'

在本书中,我们使用了以下代码块,其中func()是我们在程序中定义的函数:

if __name__ == '__main__':
    # Do something
    func()

这段代码确保只有在程序独立运行时,代码块中的语句才会被执行。

当程序运行时,特殊变量__name__会自动被设置为__main__,因此if条件的结果为True,然后调用函数func()。然而,当你将程序导入到另一个程序时,__name__的值会有所不同(见“重用代码”在第 235 页)。

这是一个简短的演示。考虑以下程序,我们将其命名为factorial.py

   # Find the factorial of a number
   def fact(n):
       p = 1
       for i in range(1, n+1):
           p = p*i
       return p

➊ print(__name__)

   if __name__ == '__main__':
       n = int(input('Enter an integer to find the factorial of: '))
       f = fact(n)
       print('Factorial of {0}: {1}'.format(n, f))

程序定义了一个函数fact(),它计算传入整数的阶乘。当你运行它时,它会打印__main__,这对应于➊处的print语句,因为__name__会自动被设置为__main__。然后,它会要求输入一个整数,计算其阶乘并打印出来:

__main__
Enter an integer to find the factorial of: 5
Factorial of 5: 120

现在,假设你需要在另一个程序中计算阶乘。你决定通过导入这个函数来重用它,而不是再次编写该函数:

from factorial import fact
if __name__ == '__main__':
    print('Factorial of 5: {0}'.format(fact(5)))

请注意,两个程序必须位于同一个目录下。当你运行此程序时,你将看到以下输出:

factorial
Factorial of 5: 120

当你的程序被另一个程序导入时,变量__main__的值会被设置为那个程序的文件名,不带扩展名。在这种情况下,__name__的值是factorial而不是__main__。因为条件__name__ == '__main__'现在的结果是False,程序就不会再要求用户输入了。移除这个条件,看看会发生什么!

总结来说,在你的程序中使用if __name__ == '__main__'是个好习惯,这样你希望在程序独立运行时执行的语句在程序被导入到其他程序时就不会被执行。

列表推导式

假设我们有一个整数列表,并且希望创建一个新的列表,其中包含原始列表中元素的平方。以下是我们可以使用的已知方法:

   >>> x = [1, 2, 3, 4]
   >>> x_square = []
➊ >>> for n in x:
➋         x_square.append(n**2)
   >>> x_square
   [1, 4, 9, 16]

在这里,我们使用了在本书中多个程序中使用的代码模式。我们创建了一个空列表x_square,然后在计算平方时逐步将值附加到该列表。我们可以使用列表推导式更高效地完成此操作:

➌ >>> x_square = [n**2 for n in x]
   >>> x_square
   [1, 4, 9, 16]

➌处的语句被称为 Python 中的列表推导式。它由一个表达式——这里是n**2——和一个for循环for n in x组成。请注意,它基本上允许我们将➊和➋处的两个语句合并成一个语句,来创建一个新的列表。

作为另一个例子,考虑我们在“绘制轨迹”一文中编写的程序,在第 51 页绘制了抛体运动中物体的轨迹。在这些程序中,我们有以下代码块来计算物体在每个时刻的xy坐标:

# Find time intervals
intervals = frange(0, t_flight, 0.001)
# List of x and y coordinates
x = []
y = []
for t in intervals:
    x.append(u*math.cos(theta)*t)
    y.append(u*math.sin(theta)*t - 0.5*g*t*t)

使用列表推导式,你可以将代码块重写如下:

# Find time intervals
intervals = frange(0, t_flight, 0.001)
# List of x and y coordinates

x = [u*math.cos(theta)*t for t in intervals]
y = [u*math.sin(theta)*t - 0.5*g*t*t for t in intervals]

现在这段代码更简洁了,因为你不再需要创建空列表、编写for循环以及向列表中添加元素。列表推导式让你可以在一行代码中完成这些操作。

你还可以在列表推导式中添加条件,以便有选择地确定哪些列表项会在表达式中进行评估。再考虑一下第一个例子:

>>> x = [1, 2, 3, 4]
>>> x_square = [n**2 for n in x if n%2 == 0]
>>> x_square
[4, 16]

在这个列表推导式中,我们使用if条件显式地告诉 Python,只在x的偶数列表项上计算表达式n**2

字典数据结构

我们在第四章首次使用了 Python 字典,当时在实现 SymPy 中的subs()方法时。让我们更详细地探讨一下 Python 字典。考虑一个简单的字典:

>>> d = {'key1': 5, 'key2': 20}

这段代码创建了一个包含两个键'key1''key2'的字典,它们的值分别是520。在 Python 字典中,只有字符串、数字和元组可以作为键。这些数据类型被称为不可变数据类型——一旦创建,它们不能被更改——因此,列表不能作为键,因为我们可以向列表中添加和移除元素。

我们已经知道,要获取字典中'key1'对应的值,我们需要指定d['key1']。这是字典最常见的使用场景之一。一个相关的使用场景是检查字典中是否包含某个特定的键,'x'。我们可以这样检查:

>>> d = {'key1': 5, 'key2': 20}
>>> 'x' in d
False

一旦我们创建了字典,就可以像向列表中添加元素一样向字典中添加新的键值对。这里是一个例子:

>>> d = {'key1': 5, 'key2': 20}
>>> if 'x' in d:
        print(d['x'])
else:
        d['x'] = 1

>>> d
{'key1': 5, 'x': 1, 'key2': 20}

这段代码检查字典d中是否已经存在键'x'。如果存在,它将打印与之对应的值;如果不存在,它将把这个键添加到字典中,并将1作为对应的值。与 Python 处理集合的方式类似,Python 不能保证字典中键值对的顺序。键值对可以按任何顺序排列,与插入顺序无关。

除了将键指定为字典的索引外,我们还可以使用get()方法来获取与键对应的值:

>>> d.get('x')
1

如果你向get()方法指定一个不存在的键,返回值将是None。另一方面,如果你使用索引方式来获取,指定不存在的键时会报错。

get()方法还允许你为不存在的键设置默认值:

>>> d.get('y', 0)
0

字典d中没有键'y',因此返回0。然而,如果存在这个键,返回的是相应的值:

>>> d['y'] = 1
>>> d.get('y', 0)
1

keys()values()方法分别返回一个类似列表的数据结构,其中包含字典中所有的键和值:

>>> d.keys()
dict_keys(['key1', 'x', 'key2', 'y'])
>>> d.values()
dict_values([5, 1, 20, 1])

要遍历字典中的键值对,可以使用 items() 方法:

>>> d.items()
dict_items([('key1', 5), ('x', 1), ('key2', 20), ('y', 1)])

这种方法返回的是一个元组的 视图,每个元组都是一个键值对。我们可以使用以下代码片段来优雅地打印它们:

>>> for k, v in d.items():
        print(k, v)

key1 5
x 1
key2 20
y 1

视图比列表更节省内存,而且它们不允许你添加或删除项。

多个返回值

在我们到目前为止编写的程序中,大多数函数返回单一的值,但有时函数也会返回多个值。在 “测量离散度” 中,我们在 第 71 页 的范围计算程序中就看到过一个返回三个值的例子。以下是我们在那个程序中采取的方法:

import math
def components(u, theta):
    x = u*math.cos(theta)
    y = u*math.sin(theta)
    return x, y

components() 函数接受一个速度 u 和一个角度 theta(以弧度为单位)作为参数,计算 xy 组件并返回它们。为了返回计算得到的组件,我们只需在返回语句中列出相应的 Python 标签,并用逗号分隔。这将创建并返回一个包含 xy 项的元组。在调用代码中,我们接收多个值:

if __name__ == '__main__':
    theta = math.radians(45)
    x, y = components(theta)

由于 components() 函数返回一个元组,我们可以通过元组的索引来获取返回的值:

c = components(theta)
x = c[0]
y = c[1]

这样做有优势,因为我们不必知道所有返回的不同值。例如,当函数返回三个值时,你不必写 x,y,z = myfunc1();当函数返回四个值时,也不必写 a,x,y,z = myfunc1(),依此类推。

在上述任一情况下,调用 components() 函数的代码必须知道返回值与速度的哪个分量对应,因为从值本身无法得知这一点。

一种更友好的做法是改为返回一个字典对象,就像我们在使用 SymPy 的 solve() 函数时通过 dict=True 关键字参数所看到的那样。下面是我们如何重写之前的 components 函数以返回字典:

import math

def components(theta):
    x = math.cos(theta)
    y = math.sin(theta)

    return {'x': x, 'y': y}

在这里,我们返回一个字典,键 'x''y' 分别对应 xy 组件及其相应的数值。通过这种新的函数定义,我们不需要担心返回值的顺序。我们只需使用键 'x' 来获取 x 组件,使用键 'y' 来获取 y 组件:

if __name__ == '__main__':
    theta = math.radians(45)
    c = components(theta)
    y = c['y']
    x = c['x']
    print(x, y)

这种方法消除了使用索引来引用特定返回值的需要。以下代码重写了计算范围的程序(参见 “测量离散度” 中的 第 71 页),使结果作为字典而不是元组返回:

   '''
   Find the range using a dictionary to return values
   '''
   def find_range(numbers):
       lowest = min(numbers)
       highest = max(numbers)
       # Find the range
       r = highest-lowest
       return {'lowest':lowest, 'highest':highest, 'range':r}

   if __name__ == '__main__':
       donations = [100, 60, 70, 900, 100, 200, 500, 500, 503, 600, 1000, 1200]
       result = find_range(donations)
➊     print('Lowest: {0} Highest: {1} Range: {2}'.
              format(result['lowest'], result['highest'], result['range']))

find_range() 函数现在返回一个字典,包含键 lowesthighestrange,它们的值分别对应最小值、最大值和范围。在 ➊ 处,我们简单地使用相应的键来获取相应的值。

如果我们只对一组数字的范围感兴趣,而不关心最小和最大数字,我们只需要使用 result['range'],而不必担心返回的其他值。

异常处理

在第一章中,我们学到过尝试将字符串 '1.1' 转换为整数时,使用 int() 函数会导致 ValueError 异常。但通过使用 try...except 块,我们可以打印一个用户友好的错误信息:

>>> try:
        int('1.1')
except ValueError:
        print('Failed to convert 1.1 to an integer')

Failed to convert 1.1 to an integer

try 块中的任何语句引发异常时,所引发的异常类型会与 except 语句中指定的类型进行匹配。如果匹配成功,程序将继续在 except 块中执行。如果异常类型不匹配,程序执行将会停止,并显示异常信息。以下是一个例子:

>>> try:
        print(1/0)
except ValueError:
        print('Division unsuccessful')

Traceback (most recent call last):
  File "<pyshell#66>", line 2, in <module>
    print(1/0)
ZeroDivisionError: division by zero

这个代码块尝试进行 0 除法运算,导致 ZeroDivisionError 异常。尽管除法操作在 try...except 块中执行,但异常类型被错误指定,导致异常没有被正确处理。处理此异常的正确方法是将 ZeroDivisionError 指定为异常类型。

指定多个异常类型

你还可以指定多个异常类型。考虑下面的 reciprocal() 函数,它返回传递给它的数字的倒数:

def reciprocal(n):
    try:
        print(1/n)
    except (ZeroDivisionError, TypeError):
        print('You entered an invalid number')

我们定义了函数 reciprocal(),它会打印用户输入的倒数。我们知道,如果该函数以 0 为参数调用,它会引发 ZeroDivisionError 异常。然而,如果传入一个字符串,它会引发 TypeError 异常。该函数将这两种情况视为无效输入,并在 except 语句中将 ZeroDivisionErrorTypeError 作为元组指定。

让我们尝试用一个有效的输入调用函数——即一个非零数字:

>>> reciprocal(5)
0.2

接下来,我们用 0 作为参数调用函数:

>>> reciprocal(0)
Enter an integer: 0
You entered an invalid number

参数 0 引发了 ZeroDivisionError 异常,而这个异常类型在指定给 except 语句的异常类型元组中,因此代码会打印错误信息。

现在,让我们输入一个字符串:

>>> reciprocal('1')

在这种情况下,我们输入了一个无效的数字,导致引发了 TypeError 异常。该异常也在指定异常的元组中,因此代码会打印错误信息。如果你想提供更具体的错误信息,我们可以像下面这样指定多个 except 语句:

def reciprocal(n):
    try:
        print(1/n)
    except TypeError:
        print('You must specify a number')
    except ZeroDivisionError:
        print('Division by 0 is invalid')

>>> reciprocal(0)
Division by 0 is invalid
>>> reciprocal('1')
You must specify a number

除了 TypeErrorValueErrorZeroDivisionError,还有许多其他内置异常类型。Python 3.4 的文档中列出了内置的异常类型,地址为 docs.python.org/3.4/library/exceptions.html#bltin-exceptions

else 块

else 块用于指定在没有异常发生时要执行的语句。考虑一下我们编写的一个程序示例,用来绘制抛物体的轨迹(见 “绘制轨迹” 在 第 51 页):

   if __name__ == '__main__':
       try:
           u = float(input('Enter the initial velocity (m/s): '))
           theta = float(input('Enter the angle of projection (degrees): '))
       except ValueError:
           print('You entered an invalid input')
➊     else:
           draw_trajectory(u, theta)
           plt.show()

如果无法将 utheta 的输入转换为浮动点数,那么程序调用 draw_trajectory()plt.show() 函数就没有意义了。相反,我们将这两个语句指定在 ➊ 处的 else 块中。使用 try...except...else 结构可以让你在运行时管理不同类型的错误,并在发生错误时或没有错误时采取适当的措施:

1. 如果发生异常且有与之对应的 except 语句,执行会转到对应的 except 块。

2. 如果没有发生异常,执行将转到 else 块。

在 Python 中读取文件

打开文件是从中读取数据的第一步。我们先看一个简单的例子。假设有一个文件,其中包含了一系列数字,每行一个数字:

100
60
70
900
100
200
500
500
503
600
1000
1200

我们想编写一个函数,它读取文件并返回一个包含这些数字的列表:

   def read_data(path):
       numbers = []
➊     f = open(path)
➋     for line in f:
           numbers.append(float(line))
       f.close()
       return numbers

首先,我们定义函数 read_data() 并创建一个空列表来存储所有数字。在 ➊ 处,我们使用 open() 函数打开通过参数 path 指定位置的文件。路径的示例可能是 Linux 下的 /home/username/mydata.txt,Microsoft Windows 下的 C:\mydata.txt,或者 OS X 下的 /Users/Username/mydata.txtopen() 函数返回一个文件对象,我们使用标签 f 来引用它。我们可以通过在 ➋ 处使用 for 循环遍历文件中的每一行。由于每一行返回的是一个字符串,我们将其转换为数字并追加到列表 numbers 中。循环在所有行读取完毕后停止,我们使用 close() 方法关闭文件。最后,我们返回 numbers 列表。

这与我们在第三章中从文件读取数字的方式类似,尽管因为我们在那里的方法不同,所以不需要显式关闭文件。使用我们在第三章中采用的方法,我们可以将前面的函数重写如下:

   def read_data(path):
       numbers = []
➊     with open(path) as f:
           for line in f:
               numbers.append(float(line))
➋     return numbers

这里的关键语句在 ➊。它类似于编写 f = open(path),但仅是部分相似。除了打开文件并将 open() 返回的文件对象赋值给 f,它还会创建一个新的上下文,包含该块中的所有语句——在此情况下,所有 return 语句之前的语句。当该块中的所有语句执行完毕后,文件会自动关闭。也就是说,当执行到 ➋ 语句时,文件会在不需要显式调用 close() 方法的情况下自动关闭。这种方法还意味着,如果在处理文件时发生任何异常,文件将在程序退出之前被关闭。这是处理文件的推荐方法。

一次性读取所有行

我们不需要一行一行地读取文件来构建列表,而可以使用 readlines() 方法一次性将所有行读取到一个列表中。这会导致一个更加简洁的函数:

   def read_data(path):
       with open(path) as f:
➊         lines = f.readlines()
       numbers = [float(n) for n in lines]
       return numbers

我们通过readlines()方法将文件的所有行读入一个列表,在 ➊ 处。然后,我们使用float()函数和列表推导式将列表中的每一项转换为浮点数。最后,我们返回列表numbers

指定文件名作为输入

read_data()函数将文件路径作为参数。如果你的程序允许你指定文件名作为输入,那么这个函数应该适用于任何文件,只要文件包含我们预期读取的数据。以下是一个示例:

if __name__=='__main__':
    data_file = input('Enter the path of the file: ')
    data = read_data(data_file)
    print(data)

一旦你将这段代码添加到read_data()函数的末尾并运行,它将要求你输入文件的路径。然后,它会打印从文件中读取的数字:

输入文件路径 /home/amit/work/mydata.txt

[100.0,60.0,70.0,900.0,100.0,200.0,500.0,500.0,503.0,600.0,1000.0,1200.0]

处理读取文件时的错误

在读取文件时可能会出错的情况有几个:(1) 文件无法读取,或者 (2) 文件中的数据格式不符合预期。以下是文件无法读取时发生的情况示例:

Enter the path of the file: /home/amit/work/mydata2.txt
Traceback (most recent call last):
  File "read_file.py", line 11, in <module>
    data = read_data(data_file)
  File "read_file.py", line 4, in read_data
    with open(path) as f:
FileNotFoundError: [Errno 2] No such file or directory: '/home/amit/work/
mydata2.txt'

因为我输入了一个不存在的文件路径,所以当我们尝试打开文件时,会引发FileNotFoundError异常。我们可以通过如下方式修改read_data()函数,使程序显示用户友好的错误信息:

def read_data(path):
    numbers = []
    try:
        with open(path) as f:
            for line in f:
                numbers.append(float(line))
    except FileNotFoundError:
        print('File not found')
    return numbers

现在,当你指定一个不存在的文件路径时,将会收到一条错误信息:

Enter the path of the file: /home/amit/work/mydata2.txt
File not found

错误的第二个来源可能是文件中的数据不是你的程序预期的内容。例如,考虑一个包含以下内容的文件:

10
20
3o
1/5
5.6

文件中的第三行无法转换为浮点数,因为它包含字母o而不是数字0,而第四行包含了1/5,这是一个字符串形式的分数,float()无法处理。

如果你将这个数据文件提供给之前的程序,它将产生如下错误:

Enter the path of the file: bad_data.txt
Traceback (most recent call last):
  File "read_file.py", line 13, in <module>
    data = read_data(data_file)
  File "read_file.py", line 6, in read_data
    numbers.append(float(line))
ValueError: could not convert string to float: '3o\n'

文件中的第三行是3o,而不是数字30,所以当我们尝试将其转换为浮点数时,会得到ValueError。当文件中出现这种数据时,你可以采取两种方法。第一种是报告错误并退出程序。修改后的read_data()函数如下所示:

   def read_data(path):
       numbers = []
       try:
           with open(path) as f:
               for line in f:
➊               try:
➋                   n = float(line)
                 except ValueError:
                     print('Bad data: {0}'.format(line))
➌                   break
➍               numbers.append(n)
       except FileNotFoundError:
           print('File not found')
       return numbers

我们在函数中插入了另一个try...except块,从 ➊ 开始,然后在 ➋ 处将该行转换为浮点数。如果程序引发ValueError异常,我们将打印带有错误行的错误信息,并在 ➌ 处使用break退出for循环。程序随后停止读取文件。返回的列表numbers包含在遇到错误数据之前成功读取的所有数据。如果没有错误,我们将在 ➍ 处将浮点数追加到numbers列表中。

现在,当你将文件bad_data.txt提供给程序时,它只会读取前两行,显示错误信息,并退出:

Enter the path of the file: bad_data.txt
Bad data: 3o

[10.0, 20.0]

返回部分数据可能不太理想,因此我们可以将➌处的break语句替换为return,这样就不会返回任何数据。

第二种方法是忽略错误,继续处理文件的其余部分。下面是一个修改过的read_data()函数,它实现了这一点:

   def read_data(path):
       numbers = []
       try:
           with open(path) as f:
               for line in f:
                   try:
                       n = float(line)
                   except ValueError:
                       print('Bad data: {0}'.format(line))
➊                     continue
                   numbers.append(n)
       except FileNotFoundError:
           print('File not found')
       return numbers

唯一的变化是,我们不再退出for循环,而是使用continue语句在➊处继续执行下一个迭代。程序的输出现在如下所示:

Bad data: 3o

Bad data: 1/5

[10.0, 20.0, 5.6]

你读取文件的具体应用场景将决定你选择哪种方法来处理不良数据。

重用代码

在本书中,我们使用的类和函数要么是 Python 标准库的一部分,要么是通过安装第三方包(如 matplotlib 和 SymPy)后提供的。现在,我们将快速展示如何将我们自己的程序导入到其他程序中。

考虑我们在“计算两个数据集之间的相关性”一节中编写的find_corr_x_y()函数,它出现在第 75 页。我们将创建一个单独的文件,correlation.py,该文件仅包含函数定义:

'''
Function to calculate the linear correlation coefficient
'''

def find_corr_x_y(x,y):
    # Size of each set
    n = len(x)

    # Find the sum of the products
    prod=[]
    for xi,yi in zip(x,y):
        prod.append(xi*yi)

    sum_prod_x_y = sum(prod)
    sum_x = sum(x)
    sum_y = sum(y)
    squared_sum_x = sum_x**2
    squared_sum_y = sum_y**2

    x_square=[]
    for xi in x:
        x_square.append(xi**2)
    x_square_sum = sum(x_square)

    y_square=[]
    for yi in y:
        y_square.append(yi**2)
    y_square_sum = sum(y_square)

    numerator = n*sum_prod_x_y - sum_x*sum_y
    denominator_term1 = n*x_square_sum - squared_sum_x
    denominator_term2 = n*y_square_sum - squared_sum_y
    denominator = (denominator_term1*denominator_term2)**0.5

    correlation = numerator/denominator

    return correlation

没有.py扩展名时,Python 文件被称为模块。这通常用于定义将用于其他程序的类和函数的文件。以下程序从我们刚才定义的相关性模块中导入find_corr_x_y()函数:

from correlation import find_corr_x_y
if __name__ == '__main__':
    high_school_math = [83, 85, 84, 96, 94, 86, 87, 97, 97, 85]
    college_admission = [85, 87, 86, 97, 96, 88, 89, 98, 98, 87]
    corr = find_corr_x_y(high_school_math, college_admission)
    print('Correlation coefficient: {0}'.format(corr))

这个程序找到我们在第 3-3 表格和第 80 页中考虑的高中数学成绩和大学录取分数之间的相关性。我们从相关性模块中导入find_corr_x_y()函数,创建代表两组成绩的列表,并使用这两个列表作为参数调用find_corr_x_y()函数。当你运行程序时,它将打印出相关系数。请注意,这两个文件必须位于同一目录下——这是为了简化操作。

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