Python-统计和微积分研讨会(二)
Python 统计和微积分研讨会(二)
原文:
zh.annas-archive.org/md5/6cbaed7d834977b8ea96cc7aa6d8a083译者:飞龙
第三章:Python 的统计工具箱
概述
在上一章中,我们了解了 Python 中三个主要的库,这些库帮助我们在统计学/机器学习项目中执行各种任务。而本章则开始了统计学及其相关概念的正式话题。虽然其中包含了一些理论讨论点,但我们也将使用直观的例子和实际编码活动来帮助理解。本章学到的内容将为我们在本工作坊的后续统计学相关章节做好准备。
在本章结束时,您将了解统计学和统计方法的基本概念。您还将能够使用 Python 工具和库执行各种与统计学相关的任务,并且将对 Python 中一些高级统计库进行概述,例如 statsmodels 和 PyMC3。
介绍
到目前为止,我们已经学会了如何使用 Python 语言,特别是它的三个核心库——NumPy、pandas 和 Matplotlib,用于统计学和数据科学。然而,为了充分利用这些工具,我们需要对统计学本身有扎实的理论理解。通过了解统计检验和技术背后的思想,我们将能够更有效地利用 Python 提供的工具。
在统计学和机器学习中,Python 库提供了很好的选择——从数据清洗/处理到建模和推断。然而,仍然需要对统计学有基本的理解,这样我们才能根据手头的数据做出关于应该在我们的过程中使用什么样的技术的初步决定。
因此,在本章中,我们将学习统计学的核心概念,例如推断、抽样、变量等。我们还将介绍一系列可以帮助促进更高级统计技术和需求的 Python 工具。所有这些都将通过实际讨论和示例进行演示。
统计学概述
在本节中,我们将简要讨论统计学这一总体领域的目标,并谈论一些其基本思想。这次对话将为本章和本书的后续主题设定背景。
一般来说,统计学是关于处理数据的,无论是处理、分析还是从我们手头的数据中得出结论。在给定数据集的情境下,统计学有两个主要目标:描述数据和从中得出结论。这些目标与统计学的两个主要类别——描述性统计和推断性统计——分别相吻合。
在描述性统计中,会有关于数据集的一般特征的问题:平均数是多少?最大值和最小值之间的差异是多少?哪个值出现最多?等等。这些问题的答案帮助我们了解所讨论的数据集构成了什么,以及数据集的主题是什么。我们在上一章中看到了这方面的简要示例。
在推断性统计中,目标是更进一步:在从给定数据集中提取适当的见解之后,我们希望利用这些信息并推断未知数据。其中一个例子是根据观察到的数据对未来进行预测。这通常是通过各种统计和机器学习模型来实现的,每种模型只适用于某些类型的数据。这就是为什么了解统计学中有哪些类型的数据是非常重要的,这将在下一节中描述。
总的来说,统计学可以被认为是研究数据的领域,这就是为什么它是数据科学和机器学习的基础。使用统计学,我们可以通过我们有时有限的数据集来了解世界的状态,并从中做出适当和可操作的决策,这些决策是基于我们获得的数据驱动知识。这就是为什么统计学在各个研究领域被广泛使用,从科学到社会科学,有时甚至是人文科学,当研究中涉及到分析元素时。
说到这里,让我们开始本章的第一个技术主题:区分数据类型。
统计学中的数据类型
在统计学中,数据主要分为两种类型:分类数据和数值数据。根据数据集中属性或变量所属的类型,其数据处理、建模、分析和可视化技术可能会有所不同。在本节中,我们将解释这两种主要数据类型的细节,并讨论每种类型的相关要点,这些要点总结在下表中:

图 3.1:数据类型比较
在本节的其余部分,我们将更详细地讨论前述比较中的每一个,从下一小节开始讨论分类数据。
分类数据
当属性或变量是分类的时,它可以取的可能值属于一个预定的固定值集。例如,在与天气相关的数据集中,您可能有一个属性来描述每天的整体天气,这种情况下该属性可能属于一个离散值列表,如"晴天"、"有风"、"多云"、"雨"等。这个属性列中的单元格必须取这些可能的值之一;一个单元格不能包含,例如,一个数字或一个不相关的字符串,比如"苹果"。这种数据的另一个术语是名义数据。
由于数据的性质,在大多数情况下,分类属性的可能值之间没有顺序关系。例如,我们之前描述的天气相关数据没有可以应用的比较操作:"晴天"既不大于也不小于"有风",依此类推。这与数值数据形成对比,尽管我们还没有讨论它,但它表达了明显的顺序性。
在讨论数据类型的差异时,让我们现在通过一些要点来了解处理分类数据时需要牢记的一些要点。
如果要使用概率分布对一个未知的分类属性进行建模,就需要一个分类分布。这种分布描述了变量是预定义的K个可能类别之一的概率。幸运的是,当我们从各自的库中调用它们时,大多数建模将在各种统计/机器学习模型的后端完成,所以我们现在不必担心建模的问题。
在数据处理方面,通常使用编码方案来转换属性中的分类值为数值,机器可解释的值。因此,在分类数据中,常见的字符串值不能被输入到只接受数值数据的模型中。
例如,有些人倾向于使用简单的编码,将每个可能的值分配一个正整数,并用其相应的数值替换它们。考虑以下样本数据集(存储在名为weather_df的变量中):
weather_df
输出将如下所示:
temp weather
0 55 windy
1 34 cloudy
2 80 sunny
3 75 rain
4 53 sunny
现在,您可以在weather属性上调用map()方法,并传入字典{'有风': 0, '多云': 1, '晴天': 2, '雨': 3}(map()方法简单地将字典定义的映射应用于属性)来对分类属性进行编码,如下所示:
weather_df['weather_encoded'] = weather_df['weather'].map(\
{'windy': 0, 'cloudy': 1, \
'sunny': 2, 'rain': 3})
这个 DataFrame 对象现在将包含以下数据:
weather_df
输出如下:
temp weather weather_encoded
0 55 windy 0
1 34 cloudy 1
2 80 sunny 2
3 75 rain 3
4 53 sunny 2
我们看到分类列weather已经成功通过一对一映射转换为了weather_encoded中的数值数据。然而,这种技术可能存在潜在的危险:新属性在数据上隐含地放置了一个顺序。由于0 < 1 < 2 < 3,我们无意中对原始分类数据施加了相同的排序;如果我们使用的模型特别将其解释为真正的数值数据,这是特别危险的。
这就是为什么当我们将分类属性转换为数值形式时必须小心。实际上,我们在上一章中已经讨论了一种能够在不施加数值关系的情况下转换分类数据的特定技术:独热编码。在这种技术中,我们为分类属性中的每个唯一值创建一个新属性。然后,对于数据集中的每一行,如果该行具有原始分类属性中的相应值,则在新创建的属性中放置1,在其他新属性中放置0。
以下代码片段重申了我们如何可以使用 pandas 实现独热编码,以及它对我们当前的样本天气数据集会产生什么影响:
pd.get_dummies(weather_df['weather'])
这将产生以下输出:
cloudy rain sunny windy
0 0 0 0 1
1 1 0 0 0
2 0 0 1 0
3 0 1 0 0
4 0 0 1 0
在本章后面我们将讨论的各种描述性统计中,众数——出现最多的值——通常是唯一可以用于分类数据的统计量。因此,当我们的数据集中的分类属性中有缺失值,并且我们希望用一个中心趋势统计量填充它们时,这是我们将在本章后面定义的一个概念,应该考虑的唯一统计量是众数。
在进行预测方面,如果一个分类属性是我们机器学习流水线的目标(也就是说,如果我们想要预测一个分类属性),则需要使用分类模型。与回归模型相反,回归模型对数值连续数据进行预测,分类模型或简称分类器,要记住其目标属性可能取值的可能值,并且只在这些值中进行预测。因此,在决定应该对数据集进行训练以预测分类数据的机器学习模型时,请确保只使用分类器。
分类数据和数值数据之间的最后一个重大区别在于可视化技术。在上一章中讨论了许多适用于分类数据的可视化技术,其中最常见的两种是条形图(包括堆叠和分组条形图)和饼图。
这些类型的可视化关注每个唯一值在整个数据集中所占的比例。
例如,对于前面的天气数据集,我们可以使用以下代码创建一个饼图:
weather_df['weather'].value_counts().plot.pie(autopct='%1.1f%%')
plt.ylabel('')
plt.show()
这将创建以下可视化:

图 3.2:天气数据的饼图
我们可以看到在整个数据集中,值为'sunny'的出现了 40%的时间,而其他每个值都出现了 20%的时间。
到目前为止,我们已经涵盖了分类属性和数值属性之间最大的理论差异,我们将在下一节中讨论。然而,在继续之前,还有一个应该提到的分类数据类型的子类型:二进制数据。
二进制属性,其值只能是True和False,是一个分类属性,其可能值集合包含了上述两个布尔值。由于布尔值可以被机器学习和数学模型轻松解释,通常不需要将二进制属性转换为其他形式。
实际上,原本不是布尔形式的二进制属性应该被转换为True和False值。在上一章的示例学生数据集中,我们遇到了这样的例子:
student_df
输出如下:
name sex class gpa num_classes
0 Alice female FY 90 4
1 Bob male SO 93 3
2 Carol female SR 97 4
3 Dan male SO 89 4
4 Eli male JR 95 3
5 Fran female SR 92 2
在这里,列'sex'是一个分类属性,其值可以是'female'或'male'。因此,为了使这些数据更适合机器处理(同时确保不会丢失或添加任何信息),我们可以对属性进行二值化,我们已经通过以下代码完成了这一步骤:
student_df['female_flag'] = student_df['sex'] == 'female'
student_df = student_df.drop('sex', axis=1)
student_df
输出如下:
name class gpa num_classes female_flag
0 Alice FY 90 4 True
1 Bob SO 93 3 False
2 Carol SR 97 4 True
3 Dan SO 89 4 False
4 Eli JR 95 3 False
5 Fran SR 92 2 True
注
由于新创建的列'female_flag'包含了来自列'sex'的所有信息,而且只有这些信息,我们可以简单地从数据集中删除后者。
除此之外,二进制属性可以以任何其他方式(处理、预测和可视化)处理为分类数据。
让我们现在将我们迄今讨论的内容应用到以下练习中。
练习 3.01:可视化天气百分比
在这个练习中,我们得到了一个样本数据集,其中包括特定城市在五天内的天气情况。这个数据集可以从packt.live/2Ar29RG下载。我们的目标是使用迄今为止讨论的分类数据可视化技术,来可视化这个数据集中的分类信息,以检查不同类型天气的百分比:
- 在一个新的 Jupyter 笔记本中,导入 pandas、Matplotlib 和 seaborn,并使用 pandas 读取上述数据集:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
weather_df = pd.read_csv('weather_data.csv')
weather_df.head()
当打印出此数据集的前五行时,您应该看到以下输出:

图 3.3:天气数据集
正如您所看到的,此数据集的每一行告诉我们在给定城市的给定日期的天气情况。例如,在0号那天,St Louis是晴天,而New York是多云。
- 在笔记本中的下一个代码单元中,计算数据集中所有天气类型的计数(发生次数),并使用
plot.bar()方法可视化该信息:
weather_df['weather'].value_counts().plot.bar()
plt.show()
此代码将产生以下输出:

图 3.4:天气类型的计数
- 使用
plot.pie(autopct='%1.1f%%')方法将与上一步相同的信息可视化为饼图:
weather_df['weather'].value_counts().plot.pie(autopct='%1.1f%%')
plt.ylabel('')
plt.show()
此代码将产生以下输出:

图 3.5:天气类型的计数
- 现在,我们想要可视化这些天气类型的计数,以及每种天气类型在每个城市中所占百分比的信息。首先,可以使用
groupby()方法计算这些信息,如下所示:
weather_df.groupby(['weather', 'city'])['weather'].count()\
.unstack('city')
输出如下:
city New York San Francisco St Louis
weather
cloudy 3.0 NaN 3.0
rain 1.0 NaN 1.0
sunny 1.0 4.0 1.0
windy NaN 1.0 NaN
我们看到这个对象包含了我们想要的信息。例如,看看表中cloudy行,我们可以看到cloudy天气类型在纽约出现了三次,在圣路易斯也出现了三次。我们有多个地方有NaN值,表示没有发生。
- 最后,我们将上一步中的表可视化为堆叠条形图:
weather_df.groupby(['weather', 'city'])\
['weather'].count().unstack('city')\
.fillna(0).plot(kind='bar', stacked=True)
plt.show()
这将产生以下图表:

图 3.6:天气类型的计数与城市相关
在整个练习过程中,我们已经将关于分类数据的知识付诸实践,以可视化从样本天气数据集中计算出的各种计数类型。
注
要访问本节的源代码,请参阅packt.live/2ArQAtw。
您也可以在packt.live/3gkIWAw上在线运行此示例。
有了这个,让我们继续讨论第二种主要类型的数据:数值数据。
数值数据
这个术语在帮助我们理解这是什么类型的数据方面是直观的。数值属性应该包含数值和连续值或实数。数值属性的值可以具有特定的范围;例如,它们可以是正数、负数或在 0 和 1 之间。然而,数值属性意味着其数据可以在给定范围内取任何值。这与分类属性中的值明显不同,后者只属于给定的离散值集。
数值数据有许多例子:人口成员的身高、学校学生的体重、某些地区待售房屋的价格、田径运动员的平均速度等。只要数据可以表示为实数,它很可能是数值数据。
鉴于其性质,数值数据与分类数据有很大的不同。在接下来的文本中,我们将阐述一些在统计和机器学习方面最重要的差异,我们应该牢记。
与可以用于建模分类数据的少数概率分布相反,数值数据有许多概率分布。这些包括正态分布(也称为钟形曲线分布)、均匀分布、指数分布、学生 t 分布等。每种概率分布都设计用于建模特定类型的数据。例如,正态分布通常用于建模具有线性增长的数量,如年龄、身高或学生的考试成绩,而指数分布则用于建模给定事件发生之间的时间量。
因此,重要的是研究哪种特定的概率分布适合你试图建模的数值属性。适当的分布将允许一致的分析和准确的预测;另一方面,选择不当的概率分布可能导致不直观和不正确的结论。
另一个话题是,许多处理技术可以应用于数值数据。其中最常见的两种包括缩放和归一化。
缩放涉及将数值属性中的所有值添加和/或乘以固定数量,以将原始数据的范围缩放到另一个范围。当统计和机器学习模型只能处理给定范围内的值时(例如,正数或 0 到 1 之间的数字可以更容易地处理和分析),就会使用这种方法。
最常用的缩放技术之一是最小-最大缩放方法,其公式如下所示,其中a和b为正数:

图 3.7:最小-最大缩放的公式
X'和X分别表示变换后和变换前的数据,而Xmax 和Xmin 分别表示数据中的最大值和最小值。可以数学证明,公式的输出始终大于a且小于b,但我们不需要在这里详细讨论。我们将在下一个练习中再次回到这种缩放方法。
至于归一化,尽管有时这个术语与缩放可以互换使用,但它指的是将数值属性特定地缩放到其概率分布的归一化形式的过程。我们的目标是获得一个转换后的数据集,它很好地遵循我们选择的概率分布的形状。
例如,假设我们在一个数值属性中拥有的数据遵循均值为4,标准差为10的正态分布。以下代码随机生成了这些数据,并对其进行可视化:
samples = np.random.normal(4, 10, size=1000)
plt.hist(samples, bins=20)
plt.show()
这产生了以下图表:

图 3.8:正态分布数据的直方图
现在,假设你有一个模型,假设这些数据符合正态分布的标准形式,其中均值为0,标准差为1,如果输入数据不符合这种形式,模型将难以学习。因此,你希望以某种方式将前述数据转换为这种标准形式,而不牺牲数据的真实模式(特别是一般形状)。
在这里,我们可以应用正态分布数据的归一化技术,其中我们从数据点中减去真实均值,并将结果除以真实标准差。这个缩放过程更普遍地被称为标准缩放器。由于前述数据已经是一个 NumPy 数组,我们可以利用矢量化并进行如下归一化:
normalized_samples = (samples - 4) / 10
plt.hist(normalized_samples, bins=20)
plt.show()
这段代码将生成我们新转换的数据的直方图,如下所示:

图 3.9:归一化数据的直方图
我们看到,虽然数据已成功转移到我们想要的范围,现在它以0为中心,大部分数据位于-3和3之间,这是正态分布的标准形式,但数据的一般形状并没有改变。换句话说,数据点之间的相对差异没有改变。
另外,在实践中,当真实均值和/或真实标准差不可用时,我们可以用样本均值和标准差来近似这些统计量,如下所示:
sample_mean = np.mean(samples)
sample_sd = np.std(samples)
对于大量样本,这两个统计量提供了一个可以进一步用于这种转换的良好近似。有了这个,我们现在可以将这些归一化的数据输入到我们的统计和机器学习模型中进行进一步分析。
说到均值和标准差,这两个统计量通常用于描述数值数据。为了填补数值属性中的缺失值,通常使用均值和中位数等集中趋势测量。在一些特殊情况下,比如时间序列数据集,可以使用更复杂的缺失值插补技术,比如插值,我们可以估计缺失值在序列中在两者之间的某个位置。
当我们想要训练一个预测模型来针对数值属性时,会使用回归模型。与分类器不同,回归模型不是对条目可能取的分类值进行预测,而是寻找连续数值范围内的合理预测。因此,与我们讨论过的类似,我们必须小心地只在目标值是数值属性的数据集上应用回归模型。
最后,在可视化数值数据方面,我们已经看到了一系列可用的可视化技术。就在这之前,我们看到直方图被用来描述数值属性的分布,告诉我们数据在其范围内是如何分布的。
此外,折线图和散点图通常是可视化属性与其他属性模式的良好工具。(例如,我们绘制了各种概率分布的概率密度函数作为折线图。)最后,我们还看到热图被用来可视化二维结构,可以用来表示数据集中数值属性之间的相关性。
在我们继续讨论下一个话题之前,让我们对缩放/归一化的概念进行快速练习。再次,最流行的缩放/归一化方法之一被称为最小-最大缩放,它允许我们将数值属性中的所有值转换为任意的范围[a, b]。我们将在下面探讨这种方法。
练习 3.02:最小-最大缩放
在这个练习中,我们将编写一个函数,以便简化将最小-最大缩放应用于数值属性的过程。该函数应该接受三个参数:data、a和b。data应该是一个 NumPy 数组或 pandas 的Series对象,a和b应该是实数正数,表示data应该转换成的数值范围的端点。
回顾数值数据部分中包含的公式,最小-最大缩放由以下给出:

图 3.10:最小-最大缩放的公式
让我们看看需要遵循的步骤:
- 创建一个新的 Jupyter 笔记本,在第一个代码单元格中,导入我们将在本练习中使用的库,如下所示:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
在我们将要使用的数据集中,第一列名为'Column 1',包含来自均值为 4,标准差为 10 的正态分布的 1,000 个样本。第二列名为'Column 2',包含来自 1 到 2 的均匀分布的 1,000 个样本。第三列名为'Column 3',包含参数为 2 和 5 的 Beta 分布的 1,000 个样本。在下一个代码单元格中,读取我们预先为您生成的'data.csv'文件(可以在packt.live/2YTrdKt找到),使用 pandas 作为DataFrame对象,并打印出前五行:
df = pd.read_csv('data.csv')
df.head()
您应该看到以下数字:
Column 1 Column 2 Column 3
0 -1.231356 1.305917 0.511994
1 7.874195 1.291636 0.155032
2 13.169984 1.274973 0.183988
3 13.442203 1.549126 0.391825
4 -8.032985 1.895236 0.398122
-
在下一个单元格中,编写一个名为
min_max_scale()的函数,它接受三个参数:data、a和b。如前所述,data应该是数据集属性中的值数组,而a和b指定输入数据要转换成的范围。 -
考虑到我们对
data的(隐含)要求(一个 NumPy 数组或 pandas 的Series对象——两者都可以利用矢量化),使用矢量化操作实现缩放函数:
def min_max_scale(data, a, b):
data_max = np.max(data)
data_min = np.min(data)
return a + (b - a) * (data - data_min) / (data_max \
- data_min)
- 首先我们将考虑
'Column 1'属性中的数据。为了观察这个函数对我们数据的影响,让我们首先可视化当前数据的分布:
plt.hist(df['Column 1'], bins=20)
plt.show()
这段代码将生成类似以下的图表:

图 3.11:未缩放数据的直方图
- 现在,使用相同的
plt.hist()函数来可视化调用df['Column 1']上的min_max_scale()函数返回的值,将数据缩放到范围[-3, 3]:
plt.hist(min_max_scale(df['Column 1'], -3, 3), bins=20)
plt.show()
这将产生以下结果:

图 3.12:缩放数据的直方图
我们看到,虽然数据分布的一般形状保持不变,但数据的范围已经有效地改变为从-3到3。
- 对于
'Column 2'属性,进行相同的过程(使用直方图可视化缩放前后的数据)。首先,我们可视化原始数据:
plt.hist(df['Column 2'], bins=20)
plt.show()
- 现在我们可视化缩放后的数据,应该缩放到范围
[0, 1]:
plt.hist(min_max_scale(df['Column 2'], 0, 1), bins=20)
plt.show()
- 第二个代码块应该生成类似以下的图表:
![图 3.13:缩放数据的直方图]()
图 3.13:缩放数据的直方图
- 对于
'Column 3'属性,进行相同的过程(使用直方图可视化缩放前后的数据)。首先,我们可视化原始数据:
plt.hist(df['Column 3'], bins=20)
plt.show()
- 现在我们可视化缩放后的数据,应该缩放到范围
[10, 20]:
plt.hist(min_max_scale(df['Column 3'], 10, 20), \
bins=20)
plt.show()
- 第二个代码块应该生成类似以下的图表:
![图 3.14:缩放数据的直方图]()
图 3.14:缩放数据的直方图
在这个练习中,我们更详细地考虑了数值数据的缩放/归一化概念。我们还重新访问了plt.hist()函数作为可视化数值数据分布的方法。
注意
要访问此特定部分的源代码,请参阅packt.live/2VDw3JP。
您还可以在packt.live/3ggiPdO上在线运行此示例。
这个练习结束了本章关于数值数据的讨论。连同分类数据一起,它构成了您可能在给定数据集中看到的大多数数据类型。然而,实际上除了这两种数据类型之外,还有另一种数据类型,这种数据类型较少见,我们将在下一节中讨论。
序数数据
序数数据在某种程度上是分类数据(序数属性中的值属于特定给定集合)和数值数据(其中值为数字——这一事实意味着它们之间存在有序关系)的组合。序数数据的最常见示例是字母分数("A","B","C","D"和"E"),整数评分(例如,在 1 到 10 的范围内),或者质量排名(例如,“优秀”,“好”,和“差”,其中“优秀”意味着比“好”更高的质量级别,而“好”本身又比“差”更好)。
由于序数属性中的条目只能取特定一组值中的一个,应该使用分类概率分布来对这种类型的数据进行建模。出于同样的原因,序数属性中的缺失值可以使用属性的众数来填充,分类数据的可视化技术也可以应用于序数数据。
然而,其他过程可能与我们讨论的分类数据有所不同。在数据处理方面,您可能会为每个序数值分配一个一对一的映射,以及一个数字值/范围。
在字母分数的例子中,通常情况下,等级"A"对应于原始分数的范围[90, 100],其他字母等级也有它们自己的连续范围。在质量排名的例子中,“优秀”,“好”和“差”可以分别映射为 10,5 和 0,但是,除非可以量化值之间的质量差异程度,否则这种转换是不可取的。
在将机器学习模型拟合到数据并让其预测序数属性的未见值方面,应该使用分类器来执行此任务。此外,由于排名是构成许多不同学习结构的独特任务,已经付出了相当大的努力来进行机器学习排名,其中专门设计和训练模型以预测排名数据。
这个讨论结束了统计学和机器学习中的数据类型主题。总的来说,我们已经了解到数据集中常见的两种主要数据类型:分类和数值数据。根据您的数据属于哪种类型,您将需要使用不同的数据处理、机器学习和可视化技术。
在下一节中,我们将讨论描述统计以及如何在 Python 中进行计算。
描述统计
如前所述,描述统计和推断统计是统计学领域的两个主要类别。通过描述统计,我们的目标是计算特定的数量,可以传达关于我们的数据的重要信息,或者换句话说,描述我们的数据。
在描述统计中,有两个主要的子类别:中心趋势统计和离散统计。实际术语暗示了它们各自的含义:中心趋势统计负责描述给定数据的中心,而离散统计传达有关数据远离其中心的传播或范围的信息。
这种区别最清晰的例子之一来自熟知的正态分布,其统计数据包括均值和标准差。均值是通过计算概率分布中所有值的平均值得出的,适用于估计分布的中心。正如我们所见,标准形式的正态分布的均值为 0,表明其数据围绕着轴上的 0 点。
另一方面,标准差表示数据点与均值的变化程度。在不深入细节的情况下,在正态分布中,它被计算为与分布均值的平均距离。低值的标准差表明数据与均值的偏差不大,而高值的标准差意味着个别数据点与均值相差很大。
总的来说,这些类型的统计数据及其特性可以总结在以下表中:

图 3.15:描述性统计类型
还有其他更专业的描述性统计,比如衡度,用于衡量数据分布的不对称性,或者峰度,用于衡量分布峰值的陡峭程度。然而,这些并不像我们之前列出的那些常用,因此本章不会涉及。
在下一小节中,我们将开始更深入地讨论前述统计数据,从集中趋势测量开始。
集中趋势
形式上,常用的集中趋势统计数据有均值、中位数和众数。中位数被定义为当所有数据点沿着轴排序时的中间值。众数,正如我们之前提到的,是出现最多次的值。由于它们的特性,均值和中位数仅适用于数值数据,而众数通常用于分类数据。
这三个统计数据都很好地捕捉了集中趋势的概念,以不同的方式代表了数据集的中心。这也是为什么它们经常被用来替换属性中的缺失值。因此,对于缺失的数值,你可以选择均值或中位数作为潜在的替代,而如果分类属性包含缺失值,则可以使用众数。
特别地,均值经常被用来填补数值属性中的缺失值并非是任意的。如果我们要将概率分布拟合到给定的数值属性上,那么该属性的均值实际上将是样本均值,对真实总体均值的估计。总体均值的另一个术语是该总体内未知值的期望值,换句话说,就是我们应该期望来自该总体的任意值是多少。
这就是为什么均值,或者来自相应分布的值的期望,应该在某些情况下用来填补缺失值。虽然对于中位数来说情况并非如此,但对于它在替换缺失数值方面的作用可以提出类似的论点。另一方面,众数是替换缺失分类值的良好估计,因为它是属性中出现最频繁的值。
离散度
与集中趋势统计不同,离散度统计再次试图量化数据集中的变化程度。一些常见的离散度统计包括标准差、范围(最大值和最小值之间的差异)和四分位数。
标准差,正如我们所提到的,计算了每个数据点与数值属性的均值之间的差异,对它们进行平方,取平均值,最后取结果的平方根。个别数据点离均值越远,这个数量就越大,反之亦然。这就是为什么它是一个很好的指标,用来衡量数据集的离散程度。
范围——最大值和最小值之间的距离,或者 0%和 100%分位数之间的距离——是描述数据集离散程度的另一种更简单的方法。然而,由于其简单性,有时这个统计量并不能传达与标准差或四分位数一样多的信息。
四分位数被定义为数据集中特定部分落在其下的阈值。例如,中位数,数值数据集的中间值,是该数据集的 50%分位数,因为(大致上)一半的数据集小于该数字。同样,我们可以计算常见的四分位数数量,如 5%,25%,75%和 95%分位数。这些四分位数在量化我们的数据分散程度方面可能更具信息量,因为它们可以解释数据的不同分布。
此外,四分位距,另一个常见的离散统计量,被定义为数据集的 25%和 75%分位数之间的差异。
到目前为止,我们已经讨论了中心趋势统计和离散统计的概念。让我们通过一个快速练习来加强一些重要的想法。
练习 3.03:可视化概率密度函数
在第二章的Python 统计学的主要工具的练习 2.04中,我们考虑了比较概率分布的概率密度函数与其抽样数据的直方图的任务。在这里,我们将实现该程序的扩展,我们还将可视化每个分布的各种描述性统计信息:
- 在新的 Jupyter 笔记本的第一个单元格中,导入 NumPy 和 Matplotlib:
import numpy as np
import matplotlib.pyplot as plt
- 在一个新的单元格中,使用
np.random.normal()随机生成来自正态分布的 1,000 个样本。计算均值、中位数和 25%和 75%四分位数的描述性统计如下:
samples = np.random.normal(size=1000)
mean = np.mean(samples)
median = np.median(samples)
q1 = np.percentile(samples, 25)
q2 = np.percentile(samples, 75)
- 在下一个单元格中,使用直方图可视化样本。我们还将通过绘制垂直线来指示各种描述性统计的位置——在均值点处绘制红色垂直线,在中位数处绘制黑色垂直线,在每个四分位数处绘制蓝色线:
plt.hist(samples, bins=20)
plt.axvline(x=mean, c='red', label='Mean')
plt.axvline(x=median, c='black', label='Median')
plt.axvline(x=q1, c='blue', label='Interquartile')
plt.axvline(x=q2, c='blue')
plt.legend()
plt.show()
请注意,我们在各种绘图函数调用中结合了label参数的规范和plt.legend()函数。这将帮助我们创建一个带有适当标签的图例,如下所示:

图 3.16:正态分布的描述性统计
这里有一件有趣的事情:均值和中位数几乎在 x 轴上重合。这是正态分布的许多数学上方便的特性之一,在许多其他分布中找不到:它的均值等于它的中位数和众数。
- 将相同的过程应用于参数为
2和5的 Beta 分布,如下所示:
samples = np.random.beta(2, 5, size=1000)
mean = np.mean(samples)
median = np.median(samples)
q1 = np.percentile(samples, 25)
q2 = np.percentile(samples, 75)
plt.hist(samples, bins=20)
plt.axvline(x=mean, c='red', label='Mean')
plt.axvline(x=median, c='black', label='Median')
plt.axvline(x=q1, c='blue', label='Interquartile')
plt.axvline(x=q2, c='blue')
plt.legend()
plt.show()
这应该生成一个类似于以下的图表:

图 3.17:Beta 分布的描述性统计
- 将相同的过程应用于参数为
5的 Gamma 分布,如下所示:
samples = np.random.gamma(5, size=1000)
mean = np.mean(samples)
median = np.median(samples)
q1 = np.percentile(samples, 25)
q2 = np.percentile(samples, 75)
plt.hist(samples, bins=20)
plt.axvline(x=mean, c='red', label='Mean')
plt.axvline(x=median, c='black', label='Median')
plt.axvline(x=q1, c='blue', label='Interquartile')
plt.axvline(x=q2, c='blue')
plt.legend()
plt.show()
这应该生成一个类似于以下的图表:

图 3.18:Gamma 分布的描述性统计
通过这个练习,我们学会了如何使用 NumPy 计算数据集的各种描述性统计,并在直方图中可视化它们。
注意
要访问此特定部分的源代码,请参阅packt.live/2YTobpm。
您也可以在packt.live/2CZf26h上在线运行此示例。
除了计算描述性统计信息,Python 还提供其他附加方法来描述数据,我们将在下一节讨论。
与 Python 相关的描述性统计
在这里,我们将讨论描述数据的两种中间方法。第一种是在DataFrame对象上调用的describe()方法。根据官方文档(可在pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.describe.html找到),该函数“生成总结数据集分布的集中趋势、离散度和形状的描述性统计,不包括NaN值。”
让我们看看这种方法的效果。首先,我们将创建一个包含数值属性、分类属性和顺序属性的样本数据集,如下所示:
df = pd.DataFrame({'numerical': np.random.normal(size=5),\
'categorical': ['a', 'b', 'a', 'c', 'b'],\
'ordinal': [1, 2, 3, 5, 4]})
现在,如果我们在df变量上调用describe()方法,将生成一个表格摘要:
df.describe()
输出如下:
numerical ordinal
count 5.000000 5.000000
mean -0.251261 3.000000
std 0.899420 1.581139
min -1.027348 1.000000
25% -0.824727 2.000000
50% -0.462354 3.000000
75% -0.192838 4.000000
max 1.250964 5.000000
正如你所看到的,打印输出中的每一行表示数据集中每个属性的不同描述统计信息:值的数量(count)、均值、标准差和各种四分位数。由于numerical和ordinal属性都被解释为数值数据(根据它们包含的数据),describe()默认只为它们生成这些报告。另一方面,categorical列被排除在外。为了强制报告适用于所有列,我们可以指定include参数如下:
df.describe(include='all')
输出如下:
numerical categorical ordinal
count 5.000000 5 5.000000
unique NaN 3 NaN
top NaN a NaN
freq NaN 2 NaN
mean -0.251261 NaN 3.000000
std 0.899420 NaN 1.581139
min -1.027348 NaN 1.000000
25% -0.824727 NaN 2.000000
50% -0.462354 NaN 3.000000
75% -0.192838 NaN 4.000000
max 1.250964 NaN 5.000000
这迫使该方法计算适用于分类数据的其他统计信息,例如唯一值的数量(unique)、众数(top)和众数的计数/频率(freq)。正如我们讨论过的,大多数数值数据的描述统计不适用于分类数据,反之亦然,这就是为什么在前面的报告中使用NaN值来指示这种非适用性。
总的来说,pandas 的describe()方法提供了一种快速概括数据集及其属性的方法。这在探索性数据分析任务中特别方便,当我们想要广泛地探索一个我们尚不熟悉的新数据集时。
Python 支持的第二种与描述统计相关的方法是箱线图的可视化。显然,箱线图是一种可视化技术,不仅仅是语言本身的独特之处,但是 Python,特别是其 seaborn 库,提供了一个相当方便的 API,即sns.boxplot()函数,以便于这个过程。
理论上,箱线图是可视化数值数据集分布的另一种方法。同样,可以使用sns.boxplot()函数生成它:
sns.boxplot(np.random.normal(2, 5, size=1000))
plt.show()
这段代码将产生一个与以下类似的图形:

图 3.19:使用 seaborn 的箱线图
在前面的箱线图中,中间的蓝色框表示输入数据的四分位距(从 25%到 75%的四分位数)。框中间的垂直线是中位数,而框外左右两个阈值分别表示输入数据的最小值和最大值。
重要的是要注意,最小值被计算为 25%四分位数减去四分位距乘以 1.5,最大值是 75%四分位数加上四分位距也乘以 1.5。通常做法是将最小值和最大值之间的任何数字视为异常值,在前面的图中显示为黑点。
实质上,箱线图可以在视觉上表示由 pandas 的 describe() 函数计算的统计数据。这个 seaborn 中的函数与其他可视化工具的不同之处在于,它可以轻松地根据 seaborn 提供的标准创建多个箱线图。
让我们在下一个示例中看到这一点,我们将扩展样本数据集到 1000 行,并生成随机数据:
df = pd.DataFrame({'numerical': np.random.normal(size=1000),\
'categorical': np.random.choice\
(['a', 'b', 'c'], size=1000),\
'ordinal': np.random.choice\
([1, 2, 3, 4, 5], size=1000)})
在这里,'numerical' 属性包含来自标准正态分布的随机抽样,'categorical' 属性包含从列表 ['a','b','c'] 中随机选择的值,而 'ordinal' 也包含从列表 [1, 2, 3, 4, 5] 中随机选择的值。
我们的目标是使用这个数据集生成一个稍微复杂的箱线图可视化——一个表示 'categorical' 中不同值的 'numerical' 数据分布的箱线图。一般的过程是将数据集分成不同的组,每个组对应于 'categorical' 中的唯一值,并且对于每个组,我们希望使用 'numerical' 属性中的相应数据生成一个箱线图。
然而,使用 seaborn,我们可以通过为 sns.boxplot() 函数指定 x 和 y 参数来简化这个过程。具体来说,我们将使我们的 x 轴包含 'categorical' 中不同的唯一值,y 轴表示 'numerical' 中的数据,代码如下:
sns.boxplot(y='numerical', x='categorical', data=df)
plt.show()
这将生成以下图表:

图 3.20:使用 seaborn 的多重箱线图
这个可视化包含了我们想要显示的内容:'numerical' 属性数据的分布,表示为箱线图,并按 'categorical' 属性中的唯一值进行分隔。考虑到 'ordinal' 中的唯一值,我们可以按照以下相同的过程进行:
sns.boxplot(y='numerical', x='ordinal', data=df)
plt.show()
这将生成以下图表:

图 3.21:使用 seaborn 的多重箱线图
如你所想象的,当我们想要分析数值属性在分类或有序数据方面的分布差异时,这种可视化方法是理想的。
这就结束了本章关于描述性统计的话题。在下一节中,我们将讨论另一类统计学:推断统计。
推断统计
与描述性统计不同,我们的目标是使用特定的数量描述数据集的各种特征,而在推断统计中,我们希望对数据集执行特定的统计建模过程,以便我们可以推断进一步的信息,无论是关于数据集本身还是关于来自同一总体的未见数据点。
在这一部分,我们将介绍一些不同的推断统计方法。通过这些讨论,我们将看到每种方法都是针对特定数据和情况设计的,统计学家或机器学习工程师有责任适当地应用它们。
我们将讨论的第一种方法是古典统计学中最基本的方法之一:t 检验。
T-Tests
通常,t 检验(也称为学生 t 检验)用于比较两个均值(平均)统计量,并得出它们是否足够不同的结论。t 检验的主要应用是比较事件(例如实验药物、锻炼常规等)对总体的影响与对照组。如果均值足够不同(我们称之为统计显著),那么我们有充分的理由相信给定事件的影响。
统计学中有三种主要类型的 t 检验:独立样本 t 检验(用于比较两个独立样本的均值),配对样本 t 检验(用于比较同一组在不同时间的均值),以及单样本 t 检验(用于将一组的均值与预定均值进行比较)。
t 检验的一般工作流程是首先声明这两个均值确实相等的零假设,然后考虑 t 检验的输出,即相应的 p 值。如果 p 值大于一个固定的阈值(通常选择 0.05),那么我们就不能拒绝零假设。另一方面,如果 p 值低于阈值,我们就可以拒绝零假设,这意味着这两个均值是不同的。我们看到这是一种推断统计方法,因为我们可以从中推断出关于我们的数据的事实;在这种情况下,我们可以推断出我们感兴趣的两个均值是否不同。
我们不会深入讨论这些测试的理论细节;相反,我们将看到如何简单地利用 Python 提供的 API,或者具体来说是 SciPy 库。我们在上一章中使用了这个库,所以如果你还不熟悉这个工具,一定要回到第二章,Python 的统计主要工具,看看它如何在你的环境中安装。
让我们设计我们的样本实验。假设我们有两个数字数组,每个数组都是从一个未知分布中抽取的,我们想要找出它们各自的均值是否相等。因此,我们有我们的零假设,即这两个数组的均值相等,如果我们的 t 检验的 p 值小于 0.05,则可以拒绝这个假设。
为了生成这个例子的合成数据,我们将从标准正态分布(均值为 0,标准差为 1)中使用20个样本来生成第一个数组,然后从均值为0.2,标准差为 1 的正态分布中使用另外20个样本来生成第二个数组:
samples_a = np.random.normal(size=20)
samples_b = np.random.normal(0.2, 1, size=20)
为了快速可视化这个数据集,我们可以使用plt.hist()函数,如下所示:
plt.hist(samples_a, alpha=0.2)
plt.hist(samples_b, alpha=0.2)
plt.show()
这生成了以下的图表(注意你自己的输出可能会有所不同):

图 3.22:t 检验样本数据的直方图
现在,我们将从scipy.stats包中调用ttest_ind()函数。这个函数便利了独立样本 t 检验,并将返回一个具有名为pvalue的属性的对象;这个属性包含了 p 值,将帮助我们决定是否拒绝我们的零假设:
scipy.stats.ttest_ind(samples_a, samples_b).pvalue
以下是输出结果:
0.8616483548091348
根据这个结果,我们不拒绝我们的零假设。再次强调,你的 p 值可能与前面的输出不同,但很可能也不会低于 0.05。我们最终的结论是,我们没有足够的证据表明我们的两个数组的均值是不同的(即使它们实际上是从两个均值不同的正态分布中生成的)。
让我们重复这个实验,但这次我们有更多的数据——每个数组现在包含 1,000 个数字:
samples_a = np.random.normal(size=1000)
samples_b = np.random.normal(0.2, 1, size=1000)
plt.hist(samples_a, alpha=0.2)
plt.hist(samples_b, alpha=0.2)
plt.show()
现在的直方图看起来像这样:

图 3.23:t 检验样本数据的直方图
再次运行 t 检验,我们看到这次我们得到了不同的结果:
scipy.stats.ttest_ind(samples_a, samples_b).pvalue
以下是输出结果:
3.1445050317071093e-06
这个 p 值远低于 0.05,因此拒绝了零假设,并给了我们足够的证据表明这两个数组的均值是不同的。
这两个实验展示了我们应该牢记的一个现象。在第一个实验中,我们的 p 值不够低,无法拒绝零假设,即使我们的数据确实是从两个均值不同的分布中生成的。在第二个实验中,有了更多的数据,t 检验在区分这两个均值方面更具有决定性。
实质上,每个数组中只有 20 个样本,第一个 t 检验没有足够高的置信水平来输出较低的 p 值,即使两个均值确实不同。有了 1,000 个样本,这种差异更加一致和稳健,因此第二个 t 检验能够积极地输出较低的 p 值。一般来说,许多其他统计方法在使用更多数据作为输入时也会同样证明更具有决定性。
我们已经看过独立样本 t 检验的一个例子,作为推断统计的一种方法,用于测试两个给定总体的平均值之间的差异程度。总的来说,scipy.stats包提供了一系列广泛的统计测试,它们处理所有的计算工作,并且只返回最终的测试输出。这符合 Python 语言的一般哲学,保持 API 在高层次,以便用户可以以灵活和便利的方式利用复杂的方法。
注意
有关scipy.stats包中可用内容的更多详细信息可以在其官方文档中找到docs.scipy.org/doc/scipy-0.15.1/reference/tutorial/stats.html。
可以从该包中调用的一些最常用的测试包括:用于均值差异的 t 检验或 ANOVA;用于确定样本是否来自正态分布的正态性测试;以及计算样本总体的均值和标准差的贝叶斯可信区间。
摆脱scipy.stats包,我们已经看到 pandas 库也支持各种统计功能,特别是其方便的describe()方法。在下一节中,我们将研究第二种推断统计方法:数据集的相关矩阵。
相关矩阵
相关矩阵是一个二维表,包含给定数据集中每对属性之间的相关系数。两个属性之间的相关系数量化了它们的线性相关程度,或者换句话说,它们在线性方面的行为有多相似。相关系数的范围在-1 到+1 之间,其中+1 表示完美的线性相关,0 表示没有相关性,-1 表示完美的负相关。
如果两个属性具有很高的线性相关性,那么当一个属性增加时,另一个属性也倾向于以相同的常数乘以增加。换句话说,如果我们在散点图上绘制两个属性的数据,个别点倾向于沿着一个具有正斜率的直线。对于没有相关性的两个属性,最佳拟合线倾向于水平,而具有负相关性的两个属性则由具有负斜率的直线表示。
两个属性之间的相关性在某种程度上可以告诉我们属性之间共享多少信息。我们可以从两个相关的属性中推断出,无论是正相关还是负相关,它们之间都存在某种潜在的关系。这就是相关矩阵作为推断统计工具的背后思想。
在一些机器学习模型中,建议如果我们有高度相关的特征,应该在将其输入模型之前只保留一个特征。在大多数情况下,拥有另一个与模型已经训练的特征高度相关的属性并不会提高其性能;更重要的是,在某些情况下,相关特征甚至会误导我们的模型,使其预测走向错误的方向。
这意味着两个数据属性之间的相关系数,以及数据集的相关矩阵,对我们来说是一个重要的统计对象。让我们通过一个快速的例子来看看这一点。
假设我们有一个包含三个属性'x'、'y'和'z'的数据集。'x'和'z'中的数据是以独立的方式随机生成的,因此它们之间不应该有相关性。另一方面,我们将'y'生成为'x'中的数据乘以 2 再加上一些随机噪音。这可以通过以下代码实现,该代码创建了一个包含 500 个条目的数据集:
x = np.random.rand(500,)
y = x * 2 + np.random.normal(0, 0.3, 500)
z = np.random.rand(500,)
df = pd.DataFrame({'x': x, 'y': y, 'z': z})
从这里开始,相关矩阵(其中包含数据集中每对属性的相关系数)可以很容易地通过corr()方法计算出来:
df.corr()
输出如下:
x y z
x 1.000000 0.8899950.869522 0.019747 -0.017913
y 0.8899950.869522 1.000000 0.045332 -0.023455
z 0.019747 -0.017913 0.045332 -0.023455 1.000000
我们看到这是一个 3x3 的矩阵,因为在调用的 DataFrame 对象中有三个属性。每个数字表示行和列属性之间的相关性。这种表示的一个效果是矩阵中的对角线值都为 1,因为每个属性与自身完全相关。
对我们来说更有趣的是不同属性之间的相关性:由于'z'是独立于'x'(因此也独立于'y')生成的,'z'行和列中的值相对接近 0。相比之下,'x'和'y'之间的相关性接近 1,因为一个属性被生成为大约是另一个属性的两倍。
此外,通常使用热力图来直观表示相关矩阵。这是因为当数据集中有大量属性时,热力图可以帮助我们更有效地识别高度相关的属性所对应的区域。可以使用 seaborn 库中的sns.heatmap()函数来可视化热力图:
sns.heatmap(df.corr(), center=0, annot=True)
bottom, top = plt.ylim()
plt.ylim(bottom + 0.5, top - 0.5)
plt.show()
annot=True参数指定矩阵中的值应该打印在热力图的每个单元格中。
这段代码将产生以下结果:

图 3.24:代表相关矩阵的热力图
在这种情况下,当通过视觉检查相关矩阵热力图时,我们可以专注于明亮的区域,除了对角线单元格,以识别高度相关的属性。如果数据集中存在负相关的属性(在我们当前的例子中没有),那么这些属性也可以通过暗区域来检测。
总的来说,给定数据集的相关矩阵可以成为我们理解数据集中不同属性之间关系的有用工具。我们将在接下来的练习中看到一个例子。
练习 3.04:识别和测试均值的相等性
在这个练习中,我们将练习两种推断统计方法来分析我们为您生成的一个合成数据集。可以从 GitHub 仓库packt.live/3ghKkDS下载数据集。
在这里,我们的目标是首先确定数据集中哪些属性彼此相关,然后应用 t 检验来确定任何一对属性是否具有相同的均值。
有了这些说法,让我们开始吧:
- 在一个新的 Jupyter 笔记本中,导入
pandas、matplotlib、seaborn,以及从 SciPy 的stats模块中导入ttest_ind()方法:
import pandas as pd
from scipy.stats import ttest_ind
import matplotlib.pyplot as plt
import seaborn as sns
- 读取您下载的数据集。前五行应该如下所示:
![图 3.25:读取数据集的前五行]()
图 3.25:读取数据集的前五行
- 在下一个代码单元中,使用 seaborn 生成代表该数据集相关矩阵的热力图。从可视化中,确定哪对属性彼此相关性最高:
sns.heatmap(df.corr(), center=0, annot=True)
bottom, top = plt.ylim()
plt.ylim(bottom + 0.5, top - 0.5)
plt.show()
这段代码应该产生以下可视化效果:

图 3.26:数据集的相关矩阵
从这个输出中,我们可以看到属性'x'和'y'的相关系数相当高:0.94。
- 使用 seaborn 中的
jointplot()方法,创建一个组合图,其中包括一个二维平面上的散点图,点的坐标分别对应于'x'和'y'中的个别值,以及代表这些值分布的两个直方图。观察输出并决定这两个分布是否具有相同的均值:
sns.jointplot(x='x', y='y', data=df)
plt.show()
这将产生以下输出:

图 3.27:相关属性的组合图
从这个可视化中,不清楚这两个属性是否具有相同的均值。
- 不使用可视化,而是使用 0.05 的显著性水平运行 t 检验,以决定这两个属性是否具有相同的均值:
ttest_ind(df['x'], df['y']).pvalue
该命令将产生以下输出:
0.011436482008949079
这个 p 值确实低于 0.05,使我们能够拒绝这两个分布具有相同均值的零假设,尽管它们高度相关。
在这个练习中,我们应用了本节中学到的两种推断统计方法来分析数据集中一对相关属性。
注意
要访问本特定部分的源代码,请参阅packt.live/31Au1hc。
您也可以在packt.live/2YTt7L7上在线运行此示例。
在关于推断统计的下一个和最后一节中,我们将讨论使用统计和机器学习模型作为使用统计进行推断的方法。
统计和机器学习模型
使用数学或机器学习模型对给定数据集进行建模,这本身就能够将数据集中的任何潜在模式和趋势推广到未见数据点,是推断统计学的另一种形式。机器学习本身可以说是计算机科学中增长最快的领域之一。然而,大多数机器学习模型实际上利用了数学和统计理论,这就是为什么这两个领域密切相关的原因。在本节中,我们将考虑在给定数据集上训练模型的过程以及 Python 如何帮助促进该过程。
重要的是要注意,机器学习模型实际上并不像人类那样学习。大多数情况下,模型试图解决一个最小化训练误差的优化问题,这代表了它在训练数据中处理模式的能力,希望模型能够很好地推广到从相同分布中抽取的未见数据。
例如,线性回归模型生成最佳拟合线,通过给定数据集中的所有数据点。在模型定义中,这条线对应于具有最小距离和的线,通过解决最小化距离和的优化问题,线性回归模型能够输出最佳拟合线。
总的来说,每个机器学习算法以不同的方式对数据和优化问题进行建模,每种适用于特定的设置。然而,Python 语言内置的不同抽象级别使我们能够跳过这些细节,并在高层次应用不同的机器学习模型。我们需要记住的是,统计和机器学习模型是推断统计的另一种方法,我们能够根据训练数据集中的模式对未见数据进行预测。
假设我们的任务是在前一节中拥有的样本数据集上训练模型,其中学习特征是'x'和'z',我们的预测目标是'y'。也就是说,我们的模型应该学习'x'或'z'与'y'之间的任何潜在关系,并从中知道如何从'x'和'z'的数据中预测'y'的未见值。
由于'y'是一个数值属性,我们将需要一个回归模型来训练我们的数据,而不是一个分类器。在这里,我们将使用统计学和机器学习中最常用的回归器之一:线性回归。为此,我们将需要 scikit-learn 库,这是 Python 中最受欢迎的预测数据分析工具之一,如果不是最受欢迎的。
要安装 scikit-learn,请运行以下pip命令:
$ pip install scikit-learn
你也可以使用conda命令来安装它:
$ conda install scikit-learn
现在,我们导入线性回归模型并将其拟合到我们的训练数据:
from sklearn import linear_model
model = linear_model.LinearRegression()
model.fit(df[['x', 'z']], df['y'])
一般来说,由机器学习模型对象调用的fit()方法接受两个参数:独立特征(即用于进行预测的特征),在这种情况下是'x'和'z',以及依赖特征或预测目标(即我们想要进行预测的属性),在这种情况下是'y'。
这个fit()方法将在给定数据上启动模型的训练过程。根据模型的复杂性以及训练数据的大小,这个过程可能需要相当长的时间。然而,对于线性回归,训练过程应该相对快速。
一旦我们的模型训练完成,我们可以查看它的各种统计数据。可用的统计数据取决于所使用的具体模型;对于线性回归,我们通常考虑系数。回归系数是独立特征和预测目标之间线性关系的估计值。实质上,回归系数是线性回归模型为特定预测变量(在我们的情况下是'x'或'z')和我们想要预测的特征的最佳拟合线的斜率估计值。
这些统计数据可以按如下方式访问:
model.coef_
这将给我们以下输出:
array([1.98861194, 0.05436268])
你自己实验的输出可能与前面的不完全相同。然而,这些系数存在明显的趋势:第一个系数(表示'x'和'y'之间的估计线性关系)约为 2,而第二个系数(表示'z'和'y'之间的估计线性关系)接近于 0。
这个结果与我们生成这个数据集的方法非常一致:'y'被生成为大致等于'x'中的元素乘以 2,而'z'是独立生成的。通过观察这些回归系数,我们可以获得关于哪些特征是最佳(线性)预测器的信息。一些人认为这些类型的统计数据是可解释性统计数据,因为它们为我们提供了关于预测过程如何进行的见解。
对我们来说更有趣的是对未见数据进行预测的过程。这可以通过在模型对象上调用predict()方法来完成,如下所示:
model.predict([[1, 2], [2, 3]])
输出将如下所示:
array([2.10790143, 4.15087605])
在这里,我们将任何能表示二维表的数据结构传递给predict()方法(在前面的代码中,我们使用了嵌套列表,但理论上,你也可以使用二维 NumPy 数组或 pandas 的DataFrame对象)。这个表的列数需要等于训练数据中独立特征的数量;在这种情况下,我们有两个('x'和'z'),所以[[1, 2], [2, 3]]中的每个子列表都有两个元素。
从模型产生的预测中,我们看到当'x'等于 1 且'z'等于 2(我们的第一个测试案例)时,相应的预测大约为 2。这与'x'的系数约为 2 和'z'的系数接近 0 的事实一致。第二个测试案例也是如此。
这就是机器学习模型如何用于对数据进行预测的一个例子。总的来说,scikit-learn 库为不同类型的问题提供了各种模型:分类、回归、聚类、降维等等。模型之间的 API 与我们所见到的fit()和predict()方法一致。这使得灵活性和流程化程度更高。
机器学习中的一个重要概念是模型选择。并非所有模型都是平等的;由于其设计或特性,某些模型更适合于给定的数据集。这就是为什么模型选择是整个机器学习流程中的重要阶段。在收集和准备训练数据集之后,机器学习工程师通常会将数据集馈送到多个不同的模型中,一些模型可能由于性能不佳而被排除在外。
我们将在接下来的练习中演示这一点,我们将介绍模型选择的过程。
练习 3.05:模型选择
在这个练习中,我们将进行一个样本模型选择过程,尝试将三种不同的模型拟合到一个合成数据集中,并考虑它们的性能:
- 在新的 Jupyter 笔记本的第一个代码单元中,导入以下工具:
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.ensemble import GradientBoostingClassifier
import matplotlib.pyplot as plt
注意
我们还不熟悉一些工具,但随着我们进行这个练习,它们将被解释给我们。
现在,我们想要创建一个位于二维平面上的合成数据集。这些点中的每一个都属于一个特定的组,属于同一组的点应该围绕一个共同的中心点旋转。
- 这些合成数据可以使用我们从
sklearn.datasets包中导入的make_blobs函数生成:
n_samples = 10000
centers = [(-2, 2), (0, 0), (2, 2)]
X, y = make_blobs(n_samples=n_samples, centers=centers, \
shuffle=False, random_state=0)
正如我们所看到的,这个函数接受一个名为n_samples的参数,该参数指定应该生成的数据点的数量。另一方面,centers参数指定了个体点所属的总组数以及它们各自的坐标。在这种情况下,我们有三组围绕着(-2, 2),(0, 0)和(2, 2)的点。
- 最后,通过将
random_state参数指定为0,我们确保每次重新运行此笔记本时生成相同的数据。正如我们在第一章,Python 基础中提到的,这在可重现性方面是一个良好的实践。
我们的目标是在这些数据上训练各种模型,以便当馈送一个新的点列表时,模型可以以高准确度决定每个点应该属于哪个组。
这个函数返回一个包含两个对象的元组,我们分别将它们分配给变量X和y。元组中的第一个元素包含数据集的独立特征;在这种情况下,它们是点的* x 和 y *坐标。第二个元组元素是我们的预测目标,即每个点所属组的索引。约定是将独立特征存储在名为X的矩阵中,将预测目标存储在名为y的向量中,就像我们正在做的那样。
- 打印出这些变量,看看我们正在处理什么。将
X作为输入类型:
X
这将产生以下输出:
array([[-0.23594765, 2.40015721],
[-1.02126202, 4.2408932 ],
[-0.13244201, 1.02272212],
...,
[ 0.98700332, 2.27166174],
[ 1.89100272, 1.94274075],
[ 0.94106874, 1.67347156]])
现在,将y作为输入类型:
y
这将产生以下输出:
array([0, 0, 0, ..., 2, 2, 2])
- 现在,在一个新的代码单元中,我们想要使用散点图来可视化这个数据集:
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.show()
我们使用数据集中的第一个属性作为* x 坐标,第二个属性作为散点图中点的 y *坐标。我们还可以通过将我们的预测目标y传递给参数c来快速指定属于同一组的点应该具有相同的颜色。
这个代码单元将产生以下散点图:

图 3.28:用于机器学习问题的散点图
模型选择过程中最常见的策略是首先将数据分成训练数据集和测试/验证数据集。训练数据集用于训练我们想要使用的机器学习模型,而测试数据集用于验证这些模型的性能。
sklearn.model_selection包中的train_test_split()函数简化了将数据集拆分为训练和测试数据集的过程。在下一个代码单元中,输入以下代码:
X_train, X_test, \
y_train, y_test = train_test_split(X, y, shuffle=True, \
random_state=0)
正如我们所看到的,这个函数返回了四个对象的元组,我们将其分配给了前面的四个变量:X_train包含训练数据集中独立特征的数据,而X_test包含测试数据集中相同特征的数据,y_train和y_test也是如此。
- 我们可以通过考虑我们的训练数据集的形状来检查拆分是如何进行的:
X_train.shape
(7500, 2)
默认情况下,训练数据集是从输入数据的 75%中随机选择的,而测试数据集是剩余的数据,随机洗牌。这由前面的输出所示,我们的训练数据集中有 7500 条记录,原始数据中有 10000 条记录。
- 在下一个代码单元中,我们将初始化我们导入的机器学习模型,而不指定任何超参数(稍后会详细介绍):
models = [KNeighborsClassifier(), SVC(),\
GradientBoostingClassifier()]
- 接下来,我们将循环遍历每个模型,在我们的训练数据集上对它们进行训练,并最终使用
accuracy_score函数计算它们在测试数据集上的准确性,该函数比较y_test中存储的值和我们模型在y_pred中生成的预测值:
for model in models:
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(f'{type(model).__name__}: {accuracy_score(y_pred, y_test)}')
同样,fit()方法用于在X_train和y_train上训练每个模型,而predict()用于让模型对X_test进行预测。这将产生类似以下的输出:
KNeighborsClassifier: 0.8792
SVC: 0.8956
GradientBoostingClassifier: 0.8876
从这里,我们可以看到SVC模型表现最好,这在某种程度上是预期的,因为它是使用的三种模型中最复杂的模型。在实际的模型选择过程中,您可能会加入更多的任务,比如交叉验证,以确保最终选择的模型是最佳选项。
这是我们模型选择练习的结束。通过这个练习,我们熟悉了使用 scikit-learn 模型的一般流程。正如我们所看到的,fit/predict API 在库中实现的所有模型中都是一致的,这为 Python 程序员提供了高度的灵活性和便利性。
这个练习也结束了推断统计的一般主题。
注意
要访问本节的源代码,请参阅packt.live/2BowiBI。
您也可以在packt.live/3dQdZ5h上线运行此示例。
在本章的下一个和最后一节中,我们将迭代许多其他库,这些库可以支持各种特定的统计程序。
Python 的其他统计工具
在上一章中,我们考虑了 Python 的三个主要库,它们构成了常见数据科学/科学计算流程的大部分内容:NumPy 用于多维矩阵计算,pandas 用于表格数据操作,Matplotlib 用于数据可视化。
在这个过程中,我们还讨论了一些支持工具,这些工具很好地补充了这三个库;它们是 seaborn 用于实现复杂可视化、SciPy 用于统计和科学计算能力、scikit-learn 用于高级数据分析需求。
不用说,还有其他工具和库,即使它们在我们的讨论中没有很好地融入,也为科学计算中的特定任务提供了独特而强大的功能。在本节中,我们将简要讨论其中一些,以便我们可以全面了解 Python 工具可用于哪些特定任务。
这些工具包括:
- statsmodels:这个库最初是 SciPy 整体生态系统的一部分,但最终分拆成了自己的项目。该库提供了广泛的统计测试和分析技术、模型和绘图功能,全部组合成一个具有一致 API 的综合工具,包括时间序列分析能力,而其前身 SciPy 在这方面有些欠缺。
statsmodels 的主网站可以在这里找到:www.statsmodels.org/stable/index.html。
- PyMC3:在称为贝叶斯统计学的统计学子领域中,有许多独特的概念和程序,可以在建模和预测方面提供强大的能力,而这些能力在我们考虑的库中得不到很好的支持。
在 PyMC3 中,贝叶斯统计建模和概率编程技术被实现为其自己的生态系统,具有绘图、测试和诊断能力,这使得它成为可能是最受欢迎的概率编程工具,不仅适用于 Python 用户,还适用于所有科学计算工程师。
有关如何开始使用 PyMC3 的更多信息,请访问其主页docs.pymc.io/。
- SymPy:远离统计学和机器学习,如果您正在寻找一个支持符号数学的 Python 库,SymPy 很可能是您最好的选择。该库涵盖了代数、微积分、离散数学、几何和与物理相关的应用等广泛的核心数学子领域。SymPy 也以其相对简单的 API 和可扩展的源代码而闻名,这使得它成为寻找 Python 中符号数学库的用户的热门选择。
您可以从 SymPy 的网站了解更多信息www.sympy.org/en/index.html。
- Bokeh:我们列表中的最后一个条目是一个可视化库。与 Matplotlib 或 seaborn 不同,Bokeh 是一个专门设计用于交互和网页浏览的可视化工具。Bokeh 通常是可视化工程师的首选工具,他们需要在 Python 中处理大量数据,但希望生成交互式报告/仪表板作为 Web 应用程序。
要阅读官方文档并查看一些示例的画廊,您可以访问主网站docs.bokeh.org/en/latest/index.html。
这些库为各自的统计学和数学子领域提供了很好的支持。同样,也总是可能找到其他符合您特定需求的工具。使用像 Python 这样受欢迎的编程语言的最大优势之一是,许多开发人员每天都在努力开发新的工具和库,以满足各种目的和需求。到目前为止,我们讨论过的库将帮助我们完成大部分统计计算和建模的基本任务,然后我们可以整合其他更高级的工具来进一步扩展我们的项目。
在我们结束本章之前,我们将通过一项活动来巩固我们迄今为止学到的一些重要概念。
活动 3.01:重新审视社区和犯罪数据集
在这个活动中,我们将再次考虑我们在上一章中分析过的“社区和犯罪”数据集。这一次,我们将应用本章学到的概念,从这个数据集中获得额外的见解:
-
在存储数据集的同一目录中,创建一个新的 Jupyter 笔记本。或者,您可以再次在
packt.live/2CWXPdD下载数据集。 -
在第一个代码单元格中,导入我们将使用的库:
numpy、pandas、matplotlib和seaborn。 -
与上一章一样,读取数据集并打印出它的前五行。
-
用 NumPy 中的
nan对象替换每个'?'字符。 -
关注以下列:
'population'(包括给定地区的总人口数量)、'agePct12t21'、'agePct12t29'、'agePct16t24'和'agePct65up',每个列中包括该人口中不同年龄组的百分比。 -
编写代码,在我们的数据集中创建包含这些年龄组实际人数的新列。这些应该是列
'population'中的数据和每个年龄百分比列的乘积。 -
使用 pandas 的
groupby()方法计算每个州不同年龄组的总人数。 -
调用我们数据集上的
describe()方法,打印出其各种描述性统计信息。 -
关注
'burglPerPop'、'larcPerPop'、'autoTheftPerPop'、'arsonsPerPop'和'nonViolPerPop'列,每个列描述了各种犯罪(入室盗窃、偷窃、汽车盗窃、纵火和非暴力犯罪)每 10 万人中的数量。 -
在一个单一的可视化中,通过箱线图来展现这些列中数据的分布。从图中识别出五种犯罪中哪一种最常见,哪一种最不常见。
-
关注
'PctPopUnderPov'、'PctLess9thGrade'、'PctUnemployed'、'ViolentCrimesPerPop'和'nonViolPerPop'列。前三个描述了给定地区人口中属于相应类别的百分比(生活在贫困线以下的人口比例、25 岁以上没有完成九年级教育的人口比例、劳动力中失业的人口比例)。最后两个给出了每 10 万人中的暴力和非暴力犯罪数量。 -
计算适当的统计对象,并相应地对其进行可视化以回答这个问题。识别与彼此相关性最大的一对列。
注意
此活动的解决方案可在 659 页找到。
总结
本章正式阐述了统计学和机器学习中的各种入门概念,包括不同类型的数据(分类、数值和有序),以及统计学的不同子类别(描述性统计和推断统计)。在我们的讨论中,我们还介绍了相关的 Python 库和工具,可以帮助促进相应主题的程序。最后,我们简要介绍了一些其他 Python 库,如 statsmodels、PyMC3 和 Bokeh,它们可以在统计和数据分析中提供更复杂和高级的用途。
在下一章中,我们将开始书中的新部分,涉及数学密集型主题,如序列、向量、复数和矩阵。具体来说,在下一章中,我们将深入研究函数和代数方程。
PSQ66
WRC42
第四章:函数和代数与 Python
概述
在前一章中,我们讨论了大量与统计相关的主题,包括变量、描述性统计和推断。在本章中,我们回到数学的一般主题,并研究其中两个最基本的组成部分:函数和代数。这些主题将与它们在 Python 中的实现并行介绍和理论讨论。掌握这些主题将使您能够解决一些可以使用数学和编程解决的最常见的现实生活问题,我们将在本章的最后一个活动中看到一个例子。
通过本章结束时,您将对数学函数的概念以及域、范围和绘图等相关概念有牢固的掌握。此外,您还将学习如何通过手工解决代数方程或方程组,以及通过 Python 编程解决。
介绍
虽然数学可以分为多个子领域,如微积分、数论和几何学,但有一些基本概念是每个数学学生都必须熟悉的。其中两个概念是函数和代数,这是本章的主要内容。
函数是描述从一个对象到另一个对象的某种映射的一般数学过程。函数可以接受一个数字并产生另一个数字。它还可以接受一组数字或向量并返回单个输出,甚至多个输出。函数如此重要,以至于它们也广泛应用于其他科学领域,包括物理学、经济学,正如我们在本书中所看到的,编程。
本章的目标是在数学背景下建立关于函数概念的具体基础讨论。这个讨论将与其他相关主题结合,如域、范围和函数的绘图。对这些主题的扎实理解将使您能够在以后的章节中探索更复杂的数学分析。
除了函数,我们还将考虑代数,这是数学中最重要的部分之一。虽然这个术语通常表示对数学对象的分析和操作,但我们将在代数方程和方程组的背景下考虑它。这将使我们能够研究它在数学中的重要作用,同时学习如何将这些知识应用于实际问题。
函数
如前所述,函数是数学对象,通常接受一些输入并产生所需的输出。因此,函数通常被认为是一个数学对象到另一个数学对象的映射。当函数接收输入并随后产生输出时,还可以使用关系的概念,这强调了函数本身建立的可能输入集和可能输出集之间的关系。
函数通常由小写字母f和括号表示,括号中包围着f接受的输入。这个符号,f(x),也表示f在接受x作为输入时产生的输出。例如,假设函数f输出其输入的平方;f可以表示为f(x) = x2。
我们看到,在 Python 中声明函数的语法也遵循这种约定。例如,要在 Python 中声明相同的平方函数,代码将如下所示:
def f(x):
return x ** 2
当我们想要获得f作为其输入的值时,我们只需说我们在输入上调用f。例如:
print(f(2))
print(f(-3))
这段代码将分别打印出4和9。正如我们所知,函数返回的值也可以通过赋值存储在变量中。
函数最重要的特征之一是没有输入可以映射到不同的输出。一旦一个输入x与相应的输出f(x)关联起来,那个输出是确定的,不能有多个可能的值。另一方面,完全可能多个输入映射到相同的输出。换句话说,多个x的值可以导致f(x)成为一个常见值。
也有可能一个函数不需要接受任何输入,也不一定需要产生任何输出。例如,在编程的情境中,一个函数的工作是读取并返回特定文件中包含的数据,不需要接受任何输入。另一个例子是更新全局变量的值的函数,在这种情况下不需要返回任何东西。也就是说,这些函数可以被认为属于一类特定的一般函数的子集,因此我们的讨论仍将围绕具有输入和输出的函数展开。
在接下来的小节中,让我们考虑数学和编程中一些常见类型的函数。
常见函数
虽然每个函数在自己的方式上都是独特的,但有一些特殊的分类或家族函数,我们需要了解。这些是常数、线性、多项式、对数和指数函数,它们总结在下表中:

图 4.1:特殊函数家族表
花点时间考虑我们表格的第三列,其中包含每个函数家族的样本函数的图。我们将在本节后面更深入地讨论函数的图的理论细节,但现在我们看到每个函数家族都给我们一个独特的图形风格;事实上,从它们的图形中识别函数是我们即将进行的练习的主题。
请注意,常数和线性函数实际上是多项式函数家族的子集(当x的较大幂的系数都为零时)。你可能已经注意到的另一个有趣的事实是对数函数的输入必须是正数,这就是为什么它的图不会延伸到y轴的左侧。相反,指数函数的输出(假设常数是正的)总是正的;相应地,它的图始终在x轴上方。这些观点直接过渡到我们的下一个主题:函数的域和值域。
域和值域
域和值域是函数概念中的两个基本概念。函数的域表示函数接受的所有可能输入的集合,而值域指定了所有可能输出的集合。
大多数情况下,可以通过考虑函数的公式表达来确定给定函数的域和值域。例如,线性函数f(x) = mx + c接受任何实数值的x来产生一个实数值的mx + c,因此它的域和值域都是实数集合R。另一方面,二次函数f(x) = x2*只产生非负输出,因此它的值域是非负实数集合。
函数的域和值域也可以通过其图来检查。考虑一个具有单个输入和单个输出的函数的图的投影:其域对应于图在x轴上的投影;同样,当图在y轴上投影时得到值域。这就是为什么我们可以说对数函数f(x) = ln(x)的域是正数集合。相反,指数函数f(x) = ex*的值域也是正数集合。
总的来说,函数的定义域和值域取决于函数本身的形式,并且可以对函数的各种行为提供高度信息。其中一个经常感兴趣的行为是函数的根,我们将在下一小节中讨论。
函数的根和方程
函数的根是使输出等于零的属于其定义域的值。再次强调,函数的根取什么值高度依赖于函数本身。仍然使用前面表中包含的例子,图 4.1,我们看到f(x) = mx + c接受* x = - c / m作为其唯一根,如果m不为零,而f(x) = ln(x)的唯一根是x = 1。一些函数可能有多个根:f(x) = x2 - 3x + 2有x = 1和x = 2作为其根,而f(x) = 0(其图对应于x轴)接受所有x的值作为其根。最后,如果一个函数的值域不包括 0,那么函数本身就没有任何根;这样的例子包括f(x) = ex,f(x) = x2 + 1和f(x) = 3。
找到函数f(x)的所有根的过程等同于解方程f(x) = 0。这里的方程一词表示我们有两个分开的量,f(x)和0,它们在数学表达式中相等。解方程可以说是数学中最核心的任务之一,有多种技术可应用于特定类型的方程。
我们在这里只是作为函数主题的一部分介绍了方程的概念,我们将在本章的后面再回到它。现在,我们将继续讨论函数的最后一个重要组成部分:图。
函数的图
在前面的例子中,函数的图是输出行为的视觉表示,关于函数输入的。具体地说,通过函数图,我们旨在检查在函数范围内,随着函数的输入在其定义域内的变化,输出如何变化。
在编程的背景下,函数的图可以通过连接散点对应于函数在x轴上一组细粒度均匀间隔值上的各个值来生成。例如,假设我们想要可视化函数f(x) = x + 1在-10和10之间的图,我们首先使用 NumPy 声明相应的均匀间隔的x值:
x = np.linspace(-10, 10, 1000)
这个 NumPy 函数生成了一个在-10和10之间的 1,000 个均匀间隔的数字的数组,这由x的输出所说明:

图 4.2:NumPy 生成的均匀间隔的数字
然后可以使用 Matplotlib 的plot()函数生成图:
plt.plot(x, x + 1)
plt.show()
请记住,由于矢量化,表达式x + 1将计算出一个与x大小相同的数组,其元素是x的元素加 1。这是 Python 语言的一个很好的特性,或者更具体地说,是 NumPy 库,它允许我们快速生成构成函数图的点。
这段代码应该产生以下可视化:

图 4.3:Python 中 f(x) = x + 1 的图
相同的逻辑可以应用于不同形式的函数。我们将在下一个练习中回到这个过程。现在,让我们回到我们的理论讨论。
函数的图是其公式表达的直观可视化,并包含了我们需要了解的有关该函数的所有信息。特别地,我们已经论证过函数图可以帮助我们确定函数的定义域和值域。此外,给定一个图,我们甚至可以确定该图是否是一个有效函数的图。这是通过垂直线测试来完成的,该测试规定如下。
给定二维平面上的图形,如果对于每条垂直线(与y轴平行的每条线),图形有多于一个交点,则它不是有效函数的绘图。这是我们之前所述的函数要求的一个直接推论:一个单一的输入不能映射到多于一个输出。如果一个图形确实与一条垂直线有至少两个交点,那么这意味着x轴上的一个点可以映射到y轴上至少两个点,这必然意味着这不是一个函数的绘图。
例如,考虑单位圆的以下图形(其中心为O(0, 0),半径等于1),它未通过垂直线测试,由红线表示:

图 4.4:单位圆的垂直线测试
这意味着单位圆实际上不是我们考虑的二维平面上函数的绘图。
这个话题也标志着我们对函数定义的介绍结束。在我们进入下一节之前,让我们通过一个旨在巩固我们迄今学到的所有概念的练习。
练习 4.01:从图形中识别函数
在这个练习中,我们将练习分析给定图形的函数行为的技能。这个过程将使我们能够结合我们之前提到的各种主题,并理解函数行为与其图形之间的联系。
对于以下每个图形:
-
确定它是否对应于一个函数,如果是,继续下一步。
-
确定函数的定义域、值域和公式(提示:使用标记的刻度)。
-
确定函数是否至少有一个根。
-
使用 Python 重现绘图(轴及其箭头不是必需的)。
- 水平线:
![图 4.5:水平线]()
图 4.5:水平线
该图对应于一个函数。函数是f(x) = 2,定义域是实数集,值域是{2}。函数没有任何根。
以下代码可用于重现绘图:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-5, 5, 1000)
plt.plot(
x, # evenly spaced numbers in the x-axis
np.ones(1000) * 2 # all 2s in the y-axis
)
plt.show()
- 旋转的二次曲线:
![图 4.6:旋转的二次曲线]()
图 4.6:旋转的二次曲线
该图不对应于一个函数,因为它未通过垂直线测试。
- 直线:
![图 4.7:直线]()
图 4.7:直线
图对应于函数f(x) = x的绘图。这个函数的定义域和值域都是一组实数。函数有一个根:x = 0。
以下代码可用于重现绘图(使用与水平线解决方案中相同的x变量):
plt.plot(x, x)
plt.show()
- 二次曲线:
![图 4.8:二次曲线]()
图 4.8:二次曲线
该图对应于函数f(x) = x2 的绘图。函数的定义域是所有实数的集合,而值域是非负数的集合。函数也有一个根:x = 0。
以下代码可用于重现绘图(感谢 NumPy 数组的矢量化):
plt.plot(x, x ** 2)
plt.show()
通过这个快速练习,我们巩固了对函数和许多相关概念的理解,包括定义域、值域、垂直线测试以及使用 Python 绘制图形的过程。
注意
要访问此特定部分的源代码,请参阅packt.live/2YRMZhL。
您也可以在packt.live/2YSBgj2上在线运行此示例。
在下一节中,我们将讨论函数的转换。
函数转换
变换是数学函数中最重要的概念之一。正如术语的名称所暗示的那样,函数的变换是在将函数的返回值通过特定的变换技术(如移动或缩放)后得到的输出。在最一般的意义上,我们可以将这个过程看作是复合函数:将一个函数的输出通过另一个函数。然而,由于它们的特定特性和有用性,有特定类型的函数通常被用作变换,我们将在接下来的小节中介绍它们,从移动开始。
由于变换最容易在应用到函数的图表上时理解,我们也将根据这一点来进行下面的讨论。
移动
函数的移动发生在函数的图表沿着x轴和/或y轴的特定距离上移动。例如,在下面的可视化中,蓝色曲线是函数f(x) = x2的图表,而红色曲线是同一图表向上移动1*后的图表:

图 4.9:函数的垂直移动
我们看到属于f(x) = x2图表的每个点(x, y)实际上被转换为(x, y + 1)。由于如果(x, y)属于f(x)的图表,那么y = x2,移动的输出本质上是函数f(x) = x2 + 1的图表。
这个例子让我们能够概括垂直移动的每种情况:任何给定函数f(x)的垂直移动的输出是新函数f(x) + c。在我们的例子中,这是c = 1,对应着向上水平移动1。然而,c也可以是负数,这种情况下函数向下移动,甚至可以是零,这种情况下变换是恒等变换,函数的图表不会改变。
我们看到当一个改变被加到(或者从中减去)函数的输出值时,或者换句话说,图表上的点的y坐标发生了垂直移动。同样,通过对函数的输入值进行改变(当一个数字被加到点的x坐标时),可以对函数进行水平移动。
一般来说,当函数f(x)的图表在二维平面上向左移动了一个量c时,结果图表是函数f(x + c)的图表。相反,函数f(x - c)的图表对应于函数f(x)向右移动c的水平移动。
仍然使用函数f(x) = x2的例子,以下图表可视化了函数向右移动2,换句话说,函数f(x) = (x - 2)2的图表:

图 4.10:函数的水平移动
也可以结合垂直移动和水平移动来变换函数,使整个图表朝任意方向移动。例如,假设我们想要将函数f(x) = x2向东北方向(上和右)移动一个向量(2, 1),那么变换后的函数将是f(x) = (x - 2)*2 + 1。
总的来说,变换作为一种移动函数图表的方式,垂直和/或水平地移动。因此,移动也是一种仿射变换,它被定义为将图表的所有点以相同方向和恒定距离移动。然而,移动不能改变图表的大小和比例。在下一节中,我们将讨论另一种可以改变大小和比例的变换方法:缩放。
缩放
缩放变换通过特定的比例因子拉伸或收缩函数的图形,具体取决于缩放因子。考虑以下可视化中对我们熟悉的函数f(x) = x2*应用缩放变换的输出:

图 4.11:函数的缩放
通过前面的缩放变换,函数图上的每个点(x, y)都被转换为(x, y / 2),有效地将图形水平缩放,使其更靠近x轴。变换后的图形对应于函数f(x) = x2 / 2,比原始图形更宽,因为曲线被缩放到更靠近x轴。除了原点(0, 0)之外,原始图形的任何点都被拉下,使其更靠近x轴。这也会使图形的整体斜率变得不那么陡峭。相反,将变换后的图形远离x轴的缩放变换可能是f(x) = 2x2,或f(x) = 3x2,从而使变换后的图形的斜率更陡。
在这些变换中,我们将函数图的y坐标乘以一个常数,这使我们能够控制相对于x轴的缩放。类似地,当通过将函数图的x坐标乘以一个常数来应用缩放时,图形将相对于y轴被拉伸或收缩。
总的来说,缩放变换的效果由缩放因子控制——图形的x或y坐标被乘以的常数。正的缩放因子不会改变图形相对于坐标轴的相对位置。
当进行垂直缩放(即缩放y坐标)时,小于 1 的正因子将拉图形靠近x轴,而大因子将推图形远离轴。水平缩放(即缩放x坐标)也是如此;小于 1 的正因子将图形推离y轴,而大因子将图形拉。
虽然这种拉/推效应对于负缩放因子也是一样的,但当函数被负常数缩放时,其图形将沿着相应的轴翻转:

图 4.12:函数的负缩放
就像我们在平移的情况下看到的那样,可以同时对函数应用多个缩放变换,以获得组合效果。
总的来说,平移和缩放构成了函数变换中最常见的两种方法。在下一个练习中,我们将练习识别这两种变换的技能,从它们对函数图的影响来识别。
练习 4.02:函数变换识别
在这里,我们的目标是分析特定变换对函数图的影响,并识别变换的类型和特征。这个练习将帮助我们熟悉变换如何操纵函数的行为。
以下图形包括三次函数f(x) = x3 - x的绘图,以及正弦函数f(x) = sin(x)的绘图,也因其周期性而常被称为正弦波:

图 4.13:三次函数和正弦波的图形
每个图形包括这两个函数绘图中的一个作为蓝色曲线,以及从中得到的特定变换的结果作为红色曲线。对于每个图形:
-
识别可能产生这种效果的变换。
-
如果是平移,确定平移向量的值(即上/下移动多少,左/右移动多少)。
-
如果是缩放,确定缩放因子是正数还是负数,并估计其值(使用刻度标记作为提示)。
-
通过使用 Python 生成相同的图表来验证您的估计(不包括坐标轴和箭头)。
现在让我们来看一下图表:
- 三次曲线的第一次变换:
![图 4.14:三次曲线的第一次变换]()
图 4.14:三次曲线的第一次变换
红色曲线是原始正弦波的移动结果。它是向左水平移动2,因此移动是-2。
以下代码可用于重现绘图:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-5, 5, 1000)
plt.plot(x, x ** 3 - x, c='blue')
plt.plot(x, (x + 2) ** 3 - (x + 2), c='red')
plt.ylim(-5, 5)
plt.show()
- 三次曲线的第二次变换:
![图 4.15:三次曲线的第二次变换]()
图 4.15:三次曲线的第二次变换
红色曲线是原始正弦波的复合移动的结果。它是向右水平移动2,再向上垂直移动2。
以下代码可用于重现绘图(使用相同的变量x):
plt.plot(x, x ** 3 - x, c='blue') # original func
plt.plot(x, (x - 2) ** 3 - (x - 2) + 2, c='red') # transformed func
plt.ylim(-5, 5)
plt.show()
- 正弦波的第一次变换:
![图 4.16:正弦波的第一次变换]()
图 4.16:正弦波的第一次变换
红色曲线是通过缩放得到的。它是沿着x轴的垂直缩放,缩放因子为2。
以下代码可用于重现绘图(使用相同的变量x):
plt.plot(x, np.sin(x), c='blue') # original func
plt.plot(x, np.sin(x) * 2, c='red') # transformed func
plt.ylim(-5, 5)
plt.show()
- 正弦波的第二次变换:
![图 4.17:正弦波的第二次变换]()
图 4.17:正弦波的第二次变换
红色曲线是通过缩放得到的。它是沿着y轴的水平缩放,将曲线拉近到2的因子,因此缩放因子是2。
以下代码可用于重现绘图(使用相同的变量x):
plt.plot(x, np.sin(x), c='blue') # original func
plt.plot(x, np.sin(x * 2), c='red') # transformed func
plt.ylim(-5, 5)
plt.show()
因此,我们已经学会了通过检查变换对函数图形的影响来识别变换类型及其变化程度。这个练习也结束了本章关于函数的主题。
注意
要访问此特定部分的源代码,请参阅packt.live/2D2U7iR。
您还可以在packt.live/2YPtHcJ上在线运行此示例。
在下一节中,我们将深入探讨一个之前简要提到的相关概念:方程。
方程
当一个函数被赋予值 0 时,就形成了一个方程,我们被要求找到满足方程的函数输入值,通常是x。这些值被称为原始函数的根。找到这些值的过程称为解方程,这是数学中一个丰富的主题,特别是代数。
在本节中,我们将讨论手工解方程的两种基本方法,并检查 Python 中可用的计算工具,以便自动解方程的过程。我们将从第一种方法开始,即代数操作。
代数操作
虽然我们将这个过程归类为一种方法,但代数学通常是一种将方程转化为更简单形式的技术,以便可以轻松找到解。一些典型的转化方程的方法包括在方程的两边加/减一个常数,将方程的两边乘/除以一个非零常数,或将方程的所有项移到一边。
一个简单的例子是3x - 5 = 6方程。
要解出x,我们首先将左边的数字5移到右边,即在方程的两边都加上5。这给我们3x = 11。
最后,我们将两边都乘以1/3,这样我们就得到了变量x的值,即x = 11/3。
这个简单的例子说明了代数操作整个过程的思想,以便我们可以隔离x的值。让我们通过另一个例子来强调这一点。假设我们有一个方程:3x - 7 - 2(19x - 7) = (5x + 9) / 3 + 12。
虽然这个方程似乎比第一个复杂得多,但过程实际上是相同的。我们首先展开括号内的项,并将涉及x的项收集到一组中,然后将剩余的项收集到另一组中。
这将给我们以下代数变换:
图 4.18:替换值以找到 x 的值
](image/B15968_04_18.jpg)
图 4.18:替换值以找到 x 的值
我们看到,总的来说,这个过程是相当简单的,特别是当我们只处理x的线性项时。术语线性表示的是变量x乘以一个常数的数量。总的来说,这两个方程的一般术语是线性方程,它只包含x的线性项。正如我们所见,解线性方程是一个直接的过程,即使我们手工进行解也是如此。
相比之下,多项式方程是具有大于1的次数的变量x的项的方程。多项式方程可以通过一种特定的技术有效地解决,这将在下一小节中讨论。现在,让我们考虑一个非线性方程的例子,3ex+2 + 3 = 2(ex + 100),它可以简单地通过代数操作来解决。
请注意,e是自然对数函数的数学常数;它约为2.71828。
注意
您可以在mathworld.wolfram.com/e.html找到有关这个常数的更多信息。
要解决这个方程,我们首先展开这个方程中的项,如下所示:3exe2 + 3 = 2ex + 200
左侧的变换是可能的,因为对于所有正数a和实数x和y,恒等式ax + y = ax ay成立。现在,我们看到即使这个方程中没有x的线性项,我们仍然可以采用隔离涉及x*的项并将它们分组的策略:

图 4.19:替换方程中的值以找到 ex
现在,有了ex 的值,我们想要提取出x项。为此,我们将自然对数函数f(x) = ln(x)应用到方程的两边。由于对于所有实数x,ln( ex ) = x,这一步将把方程的左侧转换为简单的x:

图 4.20:替换方程中的值以找到 x
总的来说,使用代数变换来解方程的一般思想是将涉及x的所有项分组并将它们操纵成一个单一项。同样,这种策略并不总是适用于任何方程,因为有时不可能将所有x项简化为一个单一项。这是多项式方程的情况,我们将在下一种解方程的方法——因式分解的背景下讨论它。
因式分解
虽然它在技术上属于代数的大类,因式分解特指将给定方程操纵成以下形式的过程:

图 4.21:因式分解公式
如果这些项的乘积等于零,那么至少其中一个项必须等于零才能满足方程。换句话说,解原方程等价于解每个方程f1(x) = 0,f2(x) = 0,…,和fn(x) = 0。理想情况下,我们希望每个这些fi(x) = 0方程都比原方程更容易解决。
让我们考虑一个起始例子:x2 = 100
使用恒等式x2 - y2 = (x - y) (x + y)对于所有实数x和y值,方程等价于(x - 10) (x + 10) = 0。
由于它们的乘积为零,x - 10或x + 10也必须为零。解这两个方程给出了原方程的解:x = 10或x = -10。
虽然这是一个相当简单的例子,但它能够说明一些要点。首先,通过将方程分解为相乘等于 0 的不同项,问题被转化为一组更简单的子问题。此外,通过分解,我们可以实现简单的加法/乘法操作无法实现的事情:解多项式方程。
让我们考虑下一个例子:x3 - 7x2 + 15x = 9
我们看到,即使所有涉及x的项已经被分组在一起,也不清楚我们应该如何进行简单的代数运算。
在这里,一个有洞察力的数学学生可能会注意到这个方程接受x = 1或x = 3作为解(因为将这些值代入使方程的左边评估为 0)。多项式方程接受x = c作为解不仅意味着通过在方程中用c替换x,它将评估为零,而且还意味着方程本身可以被分解成形式(x - c) g(x) = 0,其中g(x)是方程的另一个分解项。这个技巧还有另一个名字,鲁菲尼法则,关于这个你可以在mathworld.wolfram.com/RuffinisRule.html找到更多信息。考虑到这一点,我们尝试根据(x - 1)的项来分解给定的方程如下:
图 4.22:分解给定的方程
](image/B15968_04_22.jpg)
图 4.22:分解给定的方程
记住方程也接受x = 3作为解,我们继续将(x2 - 6x + 9)分解为(x - 3)乘以另一个项。如果你熟悉二次方程公式,你可能已经能够判断方程可以分解为(x - 1) (x - 3)2 = 0。
最后,我们证明了给定的方程接受两个解:x = 1和x = 3。
- n 的多项式方程是指x的最大次数为n的方程。总的来说,我们希望将这样的方程分解为n个不同的因子。这是因为可以数学证明,n次多项式方程最多只能有n*个唯一解。
换句话说,如果我们成功地将一个方程转化为n个不同的因子,那么这些因子中的每一个都是x的线性项,可以很容易地使用我们上面讨论的第一种方法来解决。例如,方程2x3 - 7x2 + 7x - 2 = 0可以分解为(x - 1) (x - 2) (2x - 1) = 0,这给出了三个解:x = 1,x = 2和x = 1 / 2。
当然,有些情况下,n次多项式方程不能被分解为x的n个不同线性项。考虑以下例子方程x3 + 4x - 5 = 0。
这接受了x = 1的解,因此有一个因子为(x - 1):
图 4.23:x = 1 的因子
](image/B15968_04_23.jpg)
图 4.23:x = 1 的因子
现在,考虑x2 + x + 5这个项。如果我们尝试将各种x的值代入方程,我们会发现没有一个值能满足方程。这表明这个方程没有解,或者更具体地说,x2 + x + 5对所有的x值都大于 0,我们将证明这个陈述。
当我们想要证明一个变量的二次函数始终大于 0 时,我们可以利用(g(x))2 对于所有实数值的x和所有函数g始终是非负的这一事实(这是因为任何实数的平方都是非负的)。如果我们能够将x2 + x + 5重写为(g(x))2 + c的形式,其中c是一个正常数,我们就可以证明这个项始终是正的。
在这里,我们使用完成平方技术将x项分组成一个平方。这种技术涉及使用恒等式(a + b)2 = a2 + 2ab + b2对所有的a和b值来构造(g(x))2。具体来说,项x可以重写为2 x (1/2),因为我们需要它以 2 乘以x再乘以另一个数字的形式。所以,我们有x2 和2 x (1/2);因此我们需要(1/2)2 = 1/4来将这三个数字的和完成为一个平方:x*2 + x + 1/4 = x2 + 2 x (1/2) + (1/2)2 = (x + 1/2)2。
因此整个项可以转换为x2 + x + 5 = (x2 + x + 1/4) + 19/4 = (x + 1/2)2 + 19/4。
(x + 1/2)2 对于任何实数值的x都是非负的,因此整个项(x + 1/2)2 + 19/4大于或等于19/4。这就是说,没有任何实数值的x使得项x2 + x + 5等于0;换句话说,方程x2 + x + 5 = 0没有任何解。
这就是解决多项式方程的因式分解技术的概述。最后,关于方程的主题,我们将讨论使用 Python 自动化解方程的过程。
使用 Python
除了手动解方程的两种方法之外,我们还可以利用 Python 的计算能力自动解决任何方程。在本节中,我们将在SymPy库的背景下探讨这个过程。
广义上说,SymPy 是 Python 中用于符号数学的最佳库之一,这是一个涵盖符号(如x、y和f(x))的代数计算的总称。虽然 SymPy 提供了广泛的 API,包括对不同数学子领域的支持,包括微积分、几何、逻辑和数论,但在本章中,我们只会探索它解方程和(在下一节中)解方程组的选项。
注意
您可以在其官方网站docs.sympy.org/latest/index.html上找到有关该库的更多信息。
首先,我们需要为我们的 Python 环境安装该库。这个过程,像往常一样,可以通过pip和conda来完成。运行以下命令中的任意一个:
$ pip install sympy
$ conda install sympy
在成功安装了该库之后,让我们使用一个特定的示例来探索它提供的选项,即我们在上一节中考虑的方程:x3 - 7x2 + 15x = 9。
作为符号数学工具,SymPy 提供了一个简单的 API 来声明变量和函数。为此,我们首先从 SymPy 库中导入Symbol类并声明一个名为x的变量:
from sympy import Symbol
x = Symbol('x')
当在 Jupyter 笔记本中打印x时,我们会看到这个字母实际上被格式化为一个数学符号:

图 4.24:Jupyter 笔记本中的 SymPy 符号
现在,为了解决给定的方程,我们从sympy.solvers包中导入solve()函数。这个solve()函数接受一个包含 SymPy 符号的表达式(在这种情况下,就是我们的变量x),并找到使表达式等于 0 的x的值。换句话说,要解x3 - 7x2 + 15x = 9,我们输入以下代码:
from sympy.solvers import solve
solve(x ** 3 - 7 * x ** 2 + 15 * x - 9, x)
此代码片段返回x的解列表,这种情况下是[1, 3]。我们看到这对应于我们之前通过因式分解找到的解。
让我们检查另一个我们之前解决过的例子:3ex + 2 + 3 = 2(ex + 100. 记住这个方程有一个根,x = ln( 197 / (3e2 - 2) ),约为 2.279。现在,我们将这个方程输入到solve()函数中(在导入内置的math库中的常数e之后):
from math import e
solve(3 * e ** (x + 2) + 3 - 2 * (e ** x + 100), x)
这将给我们以下输出:
[2.27914777845756]
正如我们所看到的,这与我们代数分析得到的解决方案相同。总的来说,通过声明变量并将任何形式的函数作为solve()函数的输入,SymPy 为我们提供了一种灵活和方便的方式来在 Python 中计算解方程。
这个话题也结束了我们对方程和找到它们解决方法的讨论。在我们进入本章的下一个话题之前,让我们通过一个练习来练习我们在本节学到的知识。
练习 4.03:盈亏分析简介
盈亏分析是经济学和金融工程中的常见实践。盈亏分析的目标是找到企业收入平衡成本的特定时间点。因此,找到这些时间点对于对业务所有者和利益相关者非常重要,他们希望知道是否以及何时会获利。
这种情景可以很容易地使用数学变量和函数进行建模,这是我们将在本练习中进行的。具体来说,我们的目标是通过解决盈亏平衡点来对一个简单的业务进行建模和进行盈亏分析。最终,您将更加熟悉使用数学模型、函数和变量来表示现实生活情况的过程。
情景:一家汉堡餐厅每卖出一份汉堡的原料成本为 6.56 美元。它还每个月有固定成本 1312.13 美元,用于厨师工资、租金、水电费等。餐厅的老板想进行盈亏分析,以确定何时收入将覆盖成本。
- 在第一个代码单元格中创建一个新的 Jupyter 笔记本,并导入 NumPy、Matplotlib 和 SymPy:
import numpy as np
import matplotlib.pyplot as plt
from sympy.solvers import solve
from sympy import Symbol
- 假设餐厅将每个卖出的汉堡的价格定为 8.99 美元,让x表示每个月需要卖出的汉堡的数量,以便收入等于成本。写出这种情况下x的方程。
当x是卖出的汉堡的数量时,8.99x是餐厅将获得的收入,而6.56x + 1312.13是餐厅将发生的成本。因此x的方程将是:
8.99x = 6.56x + 1312.13
- 通过手工解出x并在 Jupyter 笔记本的下一个单元格中使用 Python 验证结果。为了测试目的,将 SymPy 返回的解决方案列表存储到名为
sols的变量中。
通过简单的代数变换,我们可以解出x为x = 1312.13 / (8.99 – 6.56) = 539.97。因此,餐厅需要大约卖出 540 份汉堡才能实现盈亏平衡。
以下代码可以使用 SymPy 来解决x的问题:
x = Symbol('x')
sols = solve(8.99 * x - 6.56 * x - 1312.13, x)
sols变量应该有值[539.971193415638],这对应于我们的解决方案。
- 不要解出x作为盈亏平衡点,构建一个关于x的函数,表示每个月餐厅的总利润(收入减去成本)。
该函数应为f(x) = 8.99x - 6.56x - 1312.13 = 2.43x - 1312.13。
- 在 Jupyter 笔记本的下一个代码单元格中,使用 NumPy 和 Matplotlib 绘制这个函数,x的值在 0 到 1000 之间,并在 0 处绘制一条水平线,颜色应为黑色:
xs = np.linspace(0, 1000, 1000)
plt.plot(xs, 2.43 * xs - 1312.13)
plt.axhline(0, c='k')
plt.show()
这应该产生以下图表:

图 4.25:盈亏分析的可视化
我们的利润曲线与水平线的交点代表盈亏平衡点。在这种情况下,我们看到它大约在x坐标为540处,这对应于实际的盈亏平衡点。
- 假设餐厅平均每月销售 400 个汉堡,现在让x成为餐厅可以设置的汉堡价格,以便他们可以实现盈亏平衡。写下这种情况下x的方程。
当x是汉堡的价格时,400x是餐厅将获得的利润,而(400) 6.56 + 1312.13 = 3936.13(每个汉堡6.56美元和固定金额1312.13美元)是餐厅将发生的成本。因此,x的方程将是400x = 3936.13。
- 通过手工求解x,并在 Jupyter 笔记本中使用 SymPy 验证结果。将 SymPy 返回的解列表存储在名为
sols1的变量中。
通过将两边都除以 400,可以简单地解出方程,得到x = 9.84。解决相同方程的 Python 代码如下,也得到相同的结果:
sols1 = solve(400 * x - 3936.13, x)
sols1
- 在下一个代码单元格中,绘制代表利润和成本之间差异的函数,x值在
0和10之间,以及水平线在0处:
xs = np.linspace(0, 10, 1000)
plt.plot(xs, 400 * xs - 3936.13)
plt.axhline(0, c='k')
plt.show()
这应该产生以下图表:

图 4.26:盈亏分析的可视化
再次,两条线的交点(代表盈亏平衡点)与我们得出的实际解相符。
这就是我们练习的结束。在其中,我们通过建模一个样本现实生活中的业务,使用数学函数和变量,介绍了盈亏分析的概念。我们已经学会了如何找到要生产的产品数量,以及设置盈亏平衡的正确价格。
注意
要访问此特定部分的源代码,请参阅packt.live/3gn3JU3。
您也可以在packt.live/3gkeA0V上在线运行此示例。
当然,现实生活中的业务场景更加复杂,涉及更多因素。我们将在本章末尾的活动中回到盈亏分析的任务,但在此之前,我们需要讨论本章的最后一节:方程组。
方程组
方程是我们需要通过解出特定变量的值来满足的相等式。在方程组中,我们有涉及多个变量的多个方程,目标仍然是相同的:解出这些变量的值,使系统中的每个方程都得到满足。
总的来说,系统可以有任意数量的方程。然而,可以严格证明,当系统的方程数量不等于其变量数量时,系统要么有无穷多个解,要么没有解。在本节中,我们只考虑这两个数字相等的情况。
此外,我们将考虑两种不同类型的方程组:线性方程组和非线性方程组。我们将考虑解决这两种类型的方程组的方法,无论是手工还是使用 Python。首先,让我们讨论线性方程组的概念。
线性方程组
与线性方程类似,线性方程组只包含常数和其变量的线性项,由线性方程组组成,这些方程也只包含其变量和常数的线性组合。
这种系统的一个简单示例如下:

图 4.27:线性方程组的示例
正如我们所看到的,这个方程组有两个变量:x 和 y。这两个方程中包含这些变量与常数(线性项)相乘以及常数本身。
要解决这个方程组,您可能已经注意到,如果我们将两个方程的各自的两边相加,我们将得到一个额外的方程,3y=8,然后我们可以解出y=8/3,随后解出x=5-8/3=7/3。
总的来说,这种方法涉及将我们提供的方程分别乘以不同的常数并将它们相加,以依次消除变量。目标是获得一个只剩下单个变量的线性项(可能还有常数)的方程。然后我们可以解出这个变量。然后将这个变量的解值代入原方程,这个过程将继续进行下去,直到解出所有的变量。
尽管当我们拥有的变量/方程的数量相对较小时,这个过程是直接的,但随着这个数量的增加,它可能会变得非常混乱。在本小节中,我们将考虑一种称为行简化或高斯消元的方法,这将帮助我们规范化并自动化解方程组的过程。
假设我们被要求解下面的一般线性方程组,其中有n个变量和n个方程:

图 4.28:具有 n 个变量和 n 个方程的线性方程组
在这里,cij 是第i个方程中变量xj 的常数系数。同样,这些cij 值可以取任何常数值,这个方程组是任何线性方程组的最一般形式。
为了应用行简化方法,我们构造了所谓的增广矩阵,即以下内容:

图 4.29:一个增广矩阵
矩阵的左侧部分是一个n乘以n的子矩阵,其元素对应于原方程组中的常数系数;矩阵的右侧部分是一个包含n个值的列,这些值对应于原方程组中方程右侧的常数值。
现在,从这个增广矩阵中,我们可以进行三种类型的变换:
-
交换任意两行的位置。
-
将一行乘以非零常数。
-
将一行添加到任何其他行(可能也要乘以非零常数)。
该方法的目标是将增广矩阵转换成简化行梯阵形式,或者,因为我们有一个n个方程和n个变量的系统,转换成一个单位矩阵,其中第i行的第i个元素为 1,该行的其他元素为0。基本上,我们希望将增广矩阵转换成这个矩阵:

图 4.30:矩阵变换
完成这一步之后,ci'值对应于构成原方程组解的值。换句话说,解将是x1=c1,x2=c2,依此类推。
尽管这种数学概括可能看起来令人生畏,但让我们通过考虑一个具体的例子来揭开这个过程的神秘面纱。假设我们要解下面的线性方程组:

图 4.31:线性方程组
我们首先构造相应的增广矩阵:

图 4.32:相应的增广矩阵
现在,我们的目标是通过使用三种提到的变换方法将这个矩阵转换成单位形式。我们首先将第二行减去三倍的第一行,然后除以 4 得到:

图 4.33:将矩阵转换为单位矩阵的第 1 步
同样,目标是在左侧创建单位矩阵的结构,这可以通过强制非对角元素为零来实现。我们已经对第二行的第一个元素做到了这一点,所以现在让我们尝试对第三行做同样的事情,通过减去两倍的第一行:

图 4.34:将矩阵转换为单位矩阵的第 2 步
第三行的第二个元素要转换为 0,可以通过减去两倍的第二行来实现:

图 4.35:将矩阵转换为单位矩阵的第 3 步
一旦最后一行处于正确形式,转换其他行也相对容易。现在我们将第二行减去三倍的第三行,并乘以-1,得到:

图 4.36:将矩阵转换为单位矩阵的第 4 步
至于第一行,我们首先加上三倍的第三行,以消除最后一个元素:

图 4.37:将矩阵转换为单位矩阵的第 5 步
最后,我们减去三倍的第二行,这样就可以得到增广矩阵的行阶梯形式(左侧为单位矩阵):

图 4.38:单位矩阵
这对应于解x = 1,y = 2,和z = 3。我们可以通过将这些值代入原始方程组来确保我们的解确实是正确的,这显示它们确实满足该系统。
这就是使用行简化方法的过程。如前所述,解线性方程组的另一种方法是矩阵解法。这涉及将给定系统表示为矩阵方程。具体来说,从任何线性方程组的一般形式开始:

图 4.39:线性方程组
我们可以将其重写为矩阵表示形式Ax = c,其中A是包含常数系数的n乘n矩阵,x是包含我们要求解的变量的向量:x1,x2,…,xn,c同样是包含常数系数c1,c2,…,cn 的向量。由于矩阵和向量的乘积的定义,方程Ax = c确实等价于原始方程组。
在这个矩阵表示中,向量x可以很容易地求解为x = A-1 c,其中A-1 是A的逆矩阵。任何给定矩阵M的逆矩阵M-1 是满足方程A A-1 = I的矩阵,其中I是单位矩阵。
矩阵和向量之间的这种乘积称为点积,它输出另一个向量,其元素等于原始矩阵和向量中对应元素的乘积之和。在我们的情况下,A-1 和c之间的点积将给出构成系统解的向量。
有些矩阵没有对应的逆矩阵;这些矩阵称为奇异矩阵。我们可以用来判断矩阵是否奇异的标志之一是,如果矩阵的一行恰好是另一行乘以一个常数得到的。
从理论上讲,这类似于一个系统中的一个方程的系数是另一个方程的系数的精确乘法。如果是这种情况,我们要么有重复信息(当两个方程具有相同信息时,系统有无穷多个解),要么有冲突信息(当两个方程右侧的常数不匹配时,系统没有解)。
这背后的理论不在本书的范围之内。目前,我们只需要知道,如果线性方程组的对应系数矩阵没有逆矩阵,那么该系统就没有明确的解。
因此,并非每个矩阵都有其自己的逆矩阵,即使给定的矩阵有,计算逆矩阵的过程也可能非常复杂。幸运的是,在 Python 中可以相对容易地使用 NumPy 来完成这个过程,我们将在即将进行的练习中看到。具体来说,NumPy 中的linalg(代表线性代数)包提供了许多与线性代数相关的算法的高效实现。在这里,我们对inv()函数感兴趣,它接受表示矩阵的二维 NumPy 数组,并返回相应的逆矩阵。我们将在下一个练习中首次亲身体验这个函数的效果;有关该包的更多信息也可以在docs.scipy.org/doc/numpy/reference/routines.linalg.html找到。
练习 4.04:使用 NumPy 进行矩阵解法
在这个练习中,我们将编写一个程序,该程序接受一个线性方程组,并使用矩阵解法方法产生其解。同样,这将通过使用 NumPy 计算系数矩阵的逆矩阵来完成:
- 创建一个 Jupyter 笔记本。在其第一个单元格中,导入 NumPy 及其
linalg包中的inv()函数:
import numpy as np
from numpy.linalg import inv
- 在下一个代码单元格中,声明一个名为
solve_eq_sys()的函数(用于测试目的),它接受两个参数:coeff_matrix,它存储线性方程组中常数系数的矩阵,以及c,它存储方程右侧的常数值向量:
def solve_eq_sys(coeff_matrix, c):
这两个参数完全定义了线性方程组的一个实例,solve_eq_sys()函数的工作是计算其解。我们进一步假设这些参数都存储为 NumPy 数组。
- 回想一下,系统的解为x = A-1 c,我们只需返回
coeff_matrix的逆矩阵和c的乘积。
可以使用 NumPy 的inv()函数计算逆矩阵:
inv_matrix = inv(coeff_matrix)
最后,可以使用dot()方法计算解决方案,该方法计算矩阵和向量的点积:
return inv_matrix.dot(c)
我们的函数应该如下所示:
def solve_eq_sys(coeff_matrix, c):
inv_matrix = inv(coeff_matrix)
return inv_matrix.dot(c)
- 在下一个代码单元格中,为我们之前考虑的方程组声明相应的系数矩阵和
c向量,并对它们调用solve_eq_sys()函数:![图 4.40:线性方程组]()
图 4.40:线性方程组
这段代码应该是:
coeff_matrix = np.array([[1, 3, -2],\
[3, 5, 6],\
[2, 4, 3]])
c = np.array([1, 31, 19])
solve_eq_sys(coeff_matrix, c)
这段代码应该产生以下输出:
array([1., 2., 3.])
我们看到,这个输出恰好对应于使用行简化方法得出的方程组的实际解:x = 1,y = 2,z = 3。
- 现在,我们想考虑系数矩阵是奇异的情况。我们通过在以下没有解的示例线性方程组上测试我们的代码来做到这一点:
![图 4.41:线性方程组示例]()
图 4.41:线性方程组示例
我们看到,如果我们将第一个方程乘以2,得到的方程与第三个方程矛盾。换句话说,没有一组变量x、y和z的值可以满足该系统。
在下一个代码单元格中,对此系数矩阵调用inv()函数:
inv(np.array([[1, 3, -2],\
[3, 5, 6],\
[2, 6, -4]]))
我们将看到这段代码产生了一个LinAlgError: Singular matrix错误,我们将在下一步中修复这个错误。
为了测试目的,取消注释此单元格。
- 回到我们的代码,并用一个
try...except块修改我们当前的solve_eq_sys()函数来处理这个错误,这将需要从 NumPy 中导入:
from numpy.linalg import inv, LinAlgError
现在,如果输入矩阵是奇异的,函数应该返回False。它应该如下所示:
def solve_eq_sys(coeff_matrix, c):
try:
inv_matrix = inv(coeff_matrix)
return inv_matrix.dot(c)
except LinAlgError:
return False
- 在下一个代码单元格中,对我们在步骤 5中使用的示例方程组调用此函数:
coeff_matrix = np.array([[1, 3, -2],\
[3, 5, 6],\
[2, 6, -4]])
c = np.array([1, 31, 19])
solve_eq_sys(coeff_matrix, c)
这次,函数返回值False,这是我们期望的行为。
通过这个练习,我们学会了如何使用 NumPy 实现矩阵解法来解决线性方程组。这也结束了线性方程组的主题。
注意
要访问此特定部分的源代码,请参阅packt.live/2NPpQpK。
您也可以在packt.live/2VBNg6w上在线运行此示例。
在本章的下一节和最后一节中,我们将考虑不完全线性的方程组。
非线性方程组
当一个系统包含一个方程,其中包含一些变量的非线性项时,我们在上一节讨论的方法不适用。例如,考虑以下系统:

图 4.42:非线性方程组示例
问题出在非线性项x2 上,这使我们想要对系统应用的任何转换变得复杂。
然而,我们仍然可以有一个系统化的方法来解决这些类型的系统。具体来说,注意到从任一方程中,我们都可以解出一个变量的另一个变量。为了做到这一点,我们对每个方程进行代数变换,使得一个变量可以纯粹地表示为另一个变量。特别地,y可以表示为x的函数,如下所示:

图 4.43:替换方程以找到 y 的值
因此,为了使系统有一个有效的解,y的两个值需要匹配。换句话说,我们有以下只包含x的方程:

图 4.44:在两边替换 y 的值
这只是一个关于x的多项式方程,正如我们所知,可以通过因式分解来解决。具体来说,该方程可以因式分解为(x - 2) (x - 1) = 0,显然接受x = 1和x = 2作为解。这些x的每个值对应于y的一个值,可以通过将 1 和 2 代入原始方程组来找到。最终,该系统有两个解:(x = 1, y = 4)和(x = 2, y = 3)。
总的来说,这种方法称为替换,表示我们能够通过转换方程来解出一个变量的另一个变量。然后将这个解代入另一个方程中,这样我们就得到了一个单变量的方程。
让我们看另一个例子,应用这种方法来解决以下方程组:

图 4.45:方程组示例
虽然有多种解决方法,但一种明显的方法是解出第二个方程中的y,得到y = (x2 - 1) / 2,然后将其代入第一个方程中,如x2 - 2x - (x2 - 1)2 / 4 = -1。
通过一些代数运算,我们可以将方程简化为x4 -6x2 + 8x -3 = 0。
现在我们有一个只包含一个变量的方程,所以我们可以应用我们在上一节学到的技巧来解出x。一旦我们得到x的解,我们也可以使用之前的y = (x2 - 1) / 2代换来解出y。
在这里,可以应用因式分解来找到满足这个方程的x的值。让我们尝试代入一些x的值,比如-1、0、1或2,看哪个能使函数的值为 0。注意到x = 1是一个有效的解,我们首先将方程关于(x - 1)进行因式分解,得到(x - 1) (x3 + x2 - 5x + 3) = 0。
我们再次注意到x = 1仍然满足方程x3 + x2 – 5x + 3 = 0,因此我们再次进行因式分解得到(x - 1)2 (x2 + 2x - 3) = 0。
二次函数x2 + 2x - 3可以因式分解为(x - 1) (x + 3)。最终,我们得到以下方程(x - 1)3 (x + 3) = 0。
有两个x的值满足方程:x = 1和x = -3。通过将它们代入原方程组,我们可以解出y,得到方程组的两个解:(x = 1, y = 0)和(x = -3, y = 4)。
不幸的是,并非所有的非线性方程组都允许我们以如此简单的方式使用代换法。在许多情况下,需要使用微妙而巧妙的技巧来解决复杂的方程组。
那么,如果我们想要自动化地找到这些方程组的解,该怎么办呢?这就是sympy库提供的符号计算能力再次派上用场的地方。我们已经看到,使用 SymPy,我们可以解决任何一个一元方程。同样的想法也可以应用于非线性方程组,只是在这种情况下,我们将一系列符号函数传递给solve()函数。
在这一节中,我们想要使用 SymPy 来解决我们有的两个方程组;首先:

图 4.46:第一个方程组
其次:

图 4.47:第二个方程组
为此,我们首先将我们的变量声明为 SymPy 中Symbol类的实例:
x = Symbol('x')
y = Symbol('y')
我们可以调用 SymPy 中的solve()函数来找到我们有的方程组的解。对于第一个方程组:
solve([x + y - 5, x ** 2 - x + 2 * y - 8],\
x, y)
这段代码将返回[(1, 4), (2, 3)],这是我们之前得到的x和y的有效解的列表。至于第二个方程组:
solve([x ** 2 - 2 * x - y ** 2 + 1, x ** 2 - 2 * y - 1],\
x, y)
这段代码返回[(-3, 4), (1, 0)],这也对应着我们得到的解。正如我们所看到的,SymPy 为我们提供了一个简单的语法,让我们轻松地解决方程和方程组。
这个例子也标志着这一节材料的结束。为了结束这一章,我们将考虑对我们之前进行的盈亏分析练习的扩展。
活动 4.01:多变量盈亏分析
正如我们在第一个盈亏分析练习的结尾提到的,随着我们模型中变量的数量增加,盈亏分析可能变得非常复杂。当模型中有多个变量时,需要使用方程组来找到盈亏平衡点,这就是我们将在这个活动中做的事情。
回想一下,在我们的例子中,汉堡餐厅的商业模型中,我们每生产一个汉堡的成本是 6.56 美元,每个月的固定成本是 1312.13 美元,用于水电费、房租和其他费用。在这个活动中,我们将探讨企业的总利润如何随着我们销售的汉堡数量和每个汉堡的价格而变化。
我们需要这个模型的一个额外信息是餐厅所在地区的人们对汉堡的需求。假设,平均而言,餐厅观察到他们的收入每个月大约是 4000 美元,所以对汉堡的需求大约是 4000 除以一个汉堡的价格。
要完成此活动,请执行以下步骤:
-
将餐厅每月生产的汉堡数量和每个汉堡的价格视为我们模型的两个变量。用这两个变量表示餐厅的月收入、成本和总利润。
-
构建一个与盈亏平衡点相对应的方程组:当餐厅生产的汉堡数量满足需求且收入等于成本时。
-
通过手工解决这个方程组,并在 Jupyter 笔记本中使用 SymPy 验证结果。
-
在同一个 Jupyter 笔记本中,编写一个 Python 函数,该函数接受生产的汉堡数量和每个汉堡的价格的任何组合。该函数应返回餐厅的总利润。
-
在下一个代码单元格中,创建一个潜在值列表,表示每月生产的汉堡数量,范围从 300 到 500。使用每个汉堡 9.76 美元的固定价格生成相应利润列表,并将其存储在名为
profits_976的变量中(用于测试)。将这个利润列表作为生产的汉堡数量的函数绘制出来。 -
在下一个代码单元格中,生成相同的利润列表,这次使用每个汉堡 9.99 美元的固定价格,并将其存储在名为
profits_999的变量中。创建相同的图表,并解释它与盈亏平衡点的关系。 -
在下一个单元格中,创建一个潜在值列表,表示要生产的汉堡数量;它应该是 300 到 500 之间的每个偶数(例如,300、302、304,…,500)。另外,创建一个 NumPy 数组,其中包含 5 到 10 之间的 100 个均匀间隔的潜在汉堡价格。
-
最后,生成一个二维列表,其中第一个列表中第i个数字表示将要生产的汉堡数量,第二个列表中第j个数字(NumPy 数组)表示每个汉堡的价格。将此列表存储在名为
profits的变量中以进行测试。 -
使用 Matplotlib 创建一个热力图,以可视化前一步生成的利润的二维列表,作为生产的汉堡数量(作为y轴)和每个汉堡的价格(作为x轴)的函数。
注意
此活动的解决方案可在第 665 页找到。
总结
本章正式介绍了数学上函数和变量的定义。还讨论了与函数相关的各种主题,如定义域、值域和函数的图。在本章的第二部分,我们讨论了方程和方程组的概念,以及寻找它们解的特殊方法。在这些讨论中,还检查了 SymPy 库和 NumPy 中计算矩阵的逆的函数。我们通过完成一个使用代数和函数构建多变量盈亏分析的任务来结束本章。
在下一章中,我们将继续讨论数学中的另一个重要主题:序列和级数。
FKV27
GCH43















浙公网安备 33010602011771号