Python-统计和微积分研讨会(四)

Python 统计和微积分研讨会(四)

原文:zh.annas-archive.org/md5/6cbaed7d834977b8ea96cc7aa6d8a083

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:7. 使用 Python 进行基本统计

概述

在本章中,我们将学习如何使用主要的描述性统计指标,并且产生和理解探索性数据分析中使用的主要可视化。

在本章结束时,你将能够加载和准备一个数据集进行基本的统计分析,计算和使用主要的描述性统计指标,使用描述性统计来理解数值和分类变量,并使用可视化来研究变量之间的关系。

介绍

Python 及其分析库,如 pandas 和 Matplotlib,使得在许多类型的数据集上执行简单和复杂的统计计算变得非常容易。本章介绍了任何统计分析的第一步:定义和理解问题,加载和准备数据集,之后,逐个理解变量并探索它们之间的一些关系。

本章包括三个部分:在第一部分中,我们介绍本章将使用的数据集以及一个假设的(但非常现实的)业务问题。然后我们加载数据集并执行许多常见的数据准备任务,包括更改变量类型和过滤有用的观察。有了准备好的数据集,第二部分介绍了描述性统计主要指标的简要概念介绍,然后这些知识立即应用到我们正在处理的数据集上。作为这一部分的一部分,我们将产生一个如何将描述性统计信息转化为知识的示例。第三部分介绍了学习者对探索性数据分析EDA)的实践。从一些问题和基本计算开始,我们用一些最有用的统计可视化,如直方图、箱线图和散点图,来补充我们对基本统计的理解。

本章将采用与其他对待这个主题的传统方法不同的方法;我们不仅仅是呈现统计概念,而是更加实际地将它们作为进行数据分析的工具,这意味着将数据转化为信息,将信息转化为知识。

数据准备

所有应用统计都始于一个数据集和一个需要解决的问题。在现实世界中,我们从不孤立地进行统计分析;总是有一个需要解决的业务问题,一个需要定量理解的主题,或者一个需要提出科学问题。理解问题总是任何统计分析的第一步。第二步是收集和准备数据。数据收集不是本书的主题,所以我们将直接进行数据准备。因此,在进行一些统计计算之前,我们需要确保我们理解我们的业务问题,并且已经准备好我们的数据集。

介绍数据集

在这个小节中,我们将介绍本章将使用的数据集,并进行一些基本的数据准备任务。了解数据集将在我们定义业务问题时给你更多的背景信息。

我们将使用策略游戏数据集,其中包含来自苹果应用商店的策略游戏的真实世界信息(可在www.kaggle.com/tristan581/17k-apple-app-store-strategy-games获取,遵循以下许可证:署名 4.0 国际(CC BY 4.0))。它是在 2019 年 8 月收集的,包含约 17,000 个策略游戏的 18 列。文件包含的列的描述如下:

  • 网址:游戏的网址

  • ID:分配的 ID

  • 名称:游戏的名称

  • 副标题:名称下的次要文本

  • 图标网址:512 像素 x 512 像素的 JPG

  • 平均用户评分:四舍五入到最接近的 0.5

  • 用户评分计数:国际评分数量;空值表示低于 5

  • 价格:美元价格

  • 应用内购买:可用应用内购买的价格

  • 描述:应用描述

  • 开发者:应用开发者

  • 年龄评级:4+、9+、12+或 17+

  • 语言:ISO2A 语言代码

  • 大小:应用的大小(以字节为单位)

  • 主要类型:主要类型

  • 类型:应用的类型

  • 原始发布日期:发布日期

  • 当前版本发布日期:最后更新日期

注意

你也可以从 GitHub 仓库下载数据集packt.live/2O1hv2B

介绍业务问题

我们将使用这个数据集以及一个虚构的业务问题来学习如何将数据转化为信息,将信息转化为有用的建议。想象一下,这是一个光荣的星期一早晨,你正在享受一杯优质的危地马拉咖啡。作为一个才华横溢的分析团队的一部分,你收到了以下消息:

你所在的游戏开发公司的 CEO 提出了一项计划,以加强公司在游戏市场的地位。根据他的行业知识和其他商业报告,他知道吸引新客户的一个非常有效的方法是在移动游戏领域建立良好的声誉。鉴于这一事实,他有以下计划:为 iOS 平台开发一款策略游戏,这将吸引大量积极的关注,从而将大量新客户带到公司。他确信只有游戏得到用户的好评,他的计划才会奏效。由于他是移动游戏领域的新手,他请求你帮助回答以下问题:什么类型的策略游戏得到了用户的好评?

现在,你是这个业务问题的所有者。在处理数据之前,你必须确保你理解了问题,并且至少在原则上,问题是可以通过你拥有的数据集来解决的(部分或完全)。我们将使用之前介绍的数据集来进行一些统计分析,并就在游戏行业的这个子市场中,什么因素使得策略游戏获得良好的评价提出一些建议。

准备数据集

让我们开始加载数据集和本章将使用的库。首先,让我们加载我们现在将使用的库,它们是 NumPy、Seaborn、pandas 和 Matplotlib:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# line to allow the plots to be shown in the Jupyter Notebook
%matplotlib inline

现在让我们读取数据集。由于 pandas 的强大功能,这可以通过一行代码完成。数据集包含 17,007 个策略游戏的 18 列。这行代码将读取 CSV 文件并创建一个准备好使用的 DataFrame:

games = pd.read_csv('data/appstore_games.csv')

检查我们是否从文件中加载了正确/预期的行数和列数总是一个好主意:

games.shape

这给我们以下输出:

(17007, 18)

好的,现在我们知道我们已经从文件中读取了所有的行和列。

现在让我们快速查看我们新创建的 DataFrame 的前五行,看看它是什么样子的:

games.head()

这给出了以下输出:

图 7.1:DataFrame 的前五行和九列

图 7.1:DataFrame 的前五行和九列

这里有几件事情需要注意:

  • 列名已经被正确读取(URL、名称、ID 等);然而,文件中的名称并不那么友好,如果我们想要轻松输入,因为它们包含大写和小写字母,有些包含单词之间的空格。

  • DataFrame 使用自动生成的整数索引加载。我们可以通过查看最左边的列和粗体打印的整数来看到这一点(012,…)。

让我们分别解决这两个问题。

首先,对于 DataFrame 列的名称,有一个统一的标准格式是很有用的。当然,这是个人选择,关于这个没有严格的指导方针。然而,我建议以下格式:

  • 小写名称

  • 没有空格-使用下划线代替空格

使用这种格式将使您能够通过实现以下目标更快地输入:

  • 在输入时避免混淆(“这个字母是大写还是小写?”)。

  • 利用您喜欢的 IDE 和/或 Jupyter Notebook 中的自动补全功能(在 Jupyter 中使用Tab键)。

为了进行更改,让我们创建一个字典(使用 Python 的字典推导功能),其中包含原始列名和转换后的版本:

original_colums_dict = {x: x.lower().replace(' ','_') \
                        for x in games.columns}
original_colums_dict 

得到的字典如下:

图 7.2:原始和转换后的列名的字典

图 7.2:原始和转换后的列名的字典

现在我们可以使用这个字典来使用rename函数更改列名:

games.rename(columns = original_colums_dict,\
             inplace = True)

第二个问题与 DataFrame 的索引有关。始终建议使用有意义的 DataFrame 索引,因为这将使我们的数据处理更容易,特别是与其他表合并的问题。在这种情况下,我们有一列包含我们数据集每一行的唯一 ID(id),所以让我们使用这列作为 DataFrame 的索引:

games.set_index(keys = 'id', inplace = True)

现在我们可以再次使用以下代码行来查看修改后 DataFrame 的前几行和列:

games.head()

结果如下:

图 7.3:修改后 DataFrame 的前几行和列

图 7.3:修改后 DataFrame 的前几行和列

现在看起来更好了;然而,它仍然需要一些准备工作。

我们知道这个数据集中有 18 列;然而,有一些列我们可以预料到不会提供有用的信息。我们如何知道一列是否会提供有用的信息?一切都取决于上下文:记住,在这种情况下,我们的目标是了解什么使一个策略游戏获得很高的评分。在这种情况下,可以安全地假设游戏的 URL 和游戏图标的 URL 不会提供有关这个问题的任何有用信息。因此,没有理由保留这些列。要删除这些列,运行以下代码:

games.drop(columns = ['url', 'icon_url'], inplace = True)

另一个重要的处理步骤是确保我们的 DataFrame 中的列被正确类型化,这将使其他所有操作更容易:计算、可视化和新特征的创建。我们可以使用info()方法来检查我们的列的类型以及其他有用的信息:

games.info()

这会产生以下输出:

图 7.4:info 方法的输出

图 7.4:info 方法的输出

如您所见,在概念上是数值和分类变量的列似乎具有正确的类型:float64object。然而,我们有两列是日期,并且它们的类型是object。让我们使用以下代码将这两列转换为datetime类型:

games['original_release_date'] = pd.to_datetime\
                                 (games['original_release_date'])
games['current_version_release_date'] =\
pd.to_datetime(games['current_version_release_date'])

运行前面的代码行后,我们可以再次使用info方法检查列类型:

games.info()

这会产生以下输出:

图 7.5:更改两个日期列类型后 info 方法的输出

图 7.5:更改两个日期列类型后 info 方法的输出

请注意,包含日期的两列(original_release_datecurrent_version_release_date)现在具有正确的datetime64类型。

我们的数据集现在似乎已经准备好开始分析一些数据了。使用术语“似乎”是因为当我们分析数据集时,我们可能会发现需要进行一些额外的准备/清理工作。

您可以再次使用head()方法查看处理后的 DataFrame:

games.head()

输出如下:

图 7.6:处理后 DataFrame 的前几行和列

图 7.6:处理后 DataFrame 的前几行和列

在处理真实世界的数据集时,几乎可以肯定会在某些列中找到缺失值,因此检查数据集的每一列中有多少缺失值是一个好主意。我们可以使用以下代码行来做到这一点:

games.isnull().sum()

前面的代码行显示了以下输出:

图 7.7:按列计算的空值数量

图 7.7:按列计算的空值数量

我们看到subtitle列中有超过 11,000 个缺失值,尽管对于我们将在本章进行的分析来说,也许该列并不那么重要(也许我们应该将其删除?你觉得呢?)。另一方面,average_user_ratinguser_rating_count具有相同数量的缺失值:9,446。这表明这些缺失值可能是相关的。让我们使用np.array_equal NumPy 函数来验证这一猜测。该函数评估两个数组是否在元素级别上相等,并在这种情况下返回True。我们将使用此函数来检查这些列在相应位置是否存在缺失值。通过这样做,我们将确认缺失值发生在相同的行上。以下代码行实现了我们刚刚解释的内容:

np.array_equal(games['average_user_rating'].isnull(),\
               games['user_rating_count'].isnull())

这给出了结果True

从结果中,我们可以得出结论,每当我们在其中一列中有一个缺失值时,另一列也显示出缺失值。因此,我们的猜测是正确的:如果我们没有user_rating_count,那么我们也没有average_user_rating(如果是这种情况,那么我们将不得不分别处理这两列的缺失值)。回到列的描述(介绍数据集部分)中,对于用户评分计数,我们发现“null 表示低于 5”,因此如果少于 5 个人对游戏进行评分,那么我们根本没有评分。

继续探索缺失值,对于应用内购买列,我们有 9,324 个相对较高的值。最后,对于价格、语言和大小,我们分别有 24、60 和 1 个缺失值,鉴于我们数据的维度,这对我们的目的来说并不是什么大问题。

现在我们知道我们的数据集中的某些列中有缺失值,有许多方法可以处理它们。插补基本上意味着用合理的值替换缺失值。有非常复杂的方法可以做到这一点,这超出了本书的范围。我们将使用非常简单的方法来处理一些缺失值;但是,我们将等到完成准备工作后再决定如何处理这些缺失值。插补通常是数据准备过程中的最后一步。

现在是我们决定哪些观察结果(游戏)对我们的分析相关的时候了;换句话说,我们必须回答这个问题:我们应该保留所有我们拥有的游戏吗?

从我们分析的背景来看,有一件事是清楚的:没有评分的游戏对我们的目标毫无用处,因为我们想要了解评分。因此,我们应该将它们排除在我们的分析之外。以下代码行将仅保留average_user_rating不为空的行:

games = games.loc[games['average_user_rating'].notnull()]

请记住,average_user_rating中的空值数量为9446,因此最后一行代码将删除这些行。

我们应该意识到另一个事实:评分游戏的人数。从数据集描述中,我们知道至少有五个用户必须对游戏进行评分,游戏才能有评分。出于我们将在第九章《使用 Python 进行中级统计》中解释的原因,我们将只保留那些至少有 30 个用户评分的游戏(基本上,出于技术原因,大小为30可以保证平均评分可用于分析)。以下代码将执行我们刚刚描述的操作:

games = games.loc[games['user_rating_count'] >= 30]

让我们使用shape方法来查看我们还剩下多少观察结果:

games.shape

结果如下:

(4311, 15)

现在,再次检查每列中有多少缺失值是一个好主意。我们将使用与此任务之前使用的相同代码:

games.isnull().sum()

这向我们展示了以下输出:

图 7.8:按列计算空值的数量

图 7.8:按列计算空值的数量

现在我们看到,从可能相关的列中,只有两个存在缺失值:in-app_purchases1,313)和languages14)。

注意

关于所有练习和相关测试脚本的一个快速说明:如果您正在使用命令行界面CLI)(如 Windows 的命令提示符或 Mac 的终端)运行测试脚本,它将抛出一个错误,即Implement enable_gui in a subclass,这与笔记本中使用的一些命令有关(如%matplotlib inline)。因此,如果您想运行测试脚本,请使用 IPython shell。最好在 Jupyter 笔记本上运行练习的代码。

练习 7.01:使用字符串列生成数值列

在这个练习中,我们将在我们的 DataFrame 中创建一个新的数值变量,该变量将包含游戏可用语言的数量信息。这将是如何将文本数据转换为数值信息的一个示例。

  1. 创建一个我们迄今为止一直在使用的 DataFrame 的副本,并将其命名为games2,这样我们就有了另一个要处理的对象:
games2 = games.copy()
  1. 使用head方法查看languages列的前五个值:
games2['languages'].head()

结果如下:

图 7.9:语言列的前五个值

图 7.9:语言列的前五个值

  1. 如您所见,它由逗号分隔的语言缩写字符串组成。使用fillna()方法将该列中的缺失值替换为EN,这是最常见的值:
games2['languages'] = games2['languages'].fillna('EN')
  1. 使用str访问器方法中的split(其工作方式与字符串的split方法相同)方法获取不同语言的列表,并将结果系列保存在名为list_of_languages的对象中:
list_of_languages = games2['languages'].str.split(',')
  1. 最后,让我们在 DataFrame 中创建一个名为n_languages的列,该列包含每个结果列表中的元素数量。为此,使用一个lambda函数和返回列表长度的apply方法:
games2['n_languages'] = list_of_languages.apply(lambda x: len(x))
  1. 新列的前 10 个元素应该是这样的:
id
284921427    17
284926400     1
284946595     1
285755462    17
286210009     1
286313771     1
286363959     1
286566987     1
286682679     1
288096268     1
Name: n_languages, dtype: int64

在这个练习中,我们使用了一个文本列,并通过使用 pandas 的str.split方法和 lambda 函数,从基于文本的列生成了一个数值列。

注意

要访问此特定部分的源代码,请参阅packt.live/31xEnyy

您也可以在packt.live/38lpuk2上在线运行此示例。

在这一部分,我们谈到了定义业务问题的重要性,并介绍了我们将在本章剩余部分使用的数据集。在我们所做的所有工作之后,数据集已经到了我们可以开始理解价值的地步。为此,我们将使用描述性统计,这是下一节的主题。

计算和使用描述性统计

描述性统计是一组我们用来总结一组测量(数据)信息的方法,这有助于我们理解它。在本节中,我们将首先解释描述性统计的必要性。之后,我们将介绍描述性统计中最常见的指标,包括均值、中位数和标准偏差。首先,我们将在概念层面上理解它们,使用一个简单的测量集,然后我们将应用我们在上一节中学到的关于它们的知识到我们准备好的数据集上。

描述性统计的必要性

我们为什么需要描述性统计?以下是一个示例,将向您展示为什么我们需要这些类型的分析工具:我们的大脑非常擅长各种任务,比如识别人脸表达的情感。试着注意一下,你的大脑在阅读以下面孔的情感时付出了多少努力:

图 7.10:一种面部表情

图 7.10:一种面部表情

答案:几乎没有,当然你可以对图片中发生的事情说出有意义的话。

现在,相比之下,让我们的大脑做一个不同的任务:我们将使用上一节的游戏数据集,随机选择 300 个观察结果从平均用户评分列中,然后打印它们。以下代码行就是这样做的:

random_ratings = games['average_user_rating'].sample(n=300)
for r in random_ratings:
    print(r, end=', ')

输出如下:

图 7.11:300 个平均用户评分的随机样本

图 7.11:300 个平均用户评分的随机样本

看一下输出,试着分析它,并回答以下问题:

  • 需要付出多少心理努力才能说出有意义的话?

  • 仅凭数字,你能对平均评分说些什么呢?

你对这些问题的答案很可能是:

  • 需要付出多少心理努力才能说出有意义的话?"我花了一些时间看这些数字。"

  • 仅凭数字,你能对平均评分说些什么?"不多,数字以.0 或.5 结尾。"

仅仅通过观察图 7.10,我们就可以自动地通过说女人笑来总结和理解包含在成千上万像素中的信息。然而,在图 7.11的情况下,我们无法自动地了解有关它们的信息。这就是为什么我们需要描述性统计:它允许我们通过执行相对简单的计算来总结和理解数值信息。

统计概念简要回顾

如果你正在阅读这本书,很可能你已经学习或使用了一些最常见的描述性统计,对于我来说很难提供关于已经在成百上千本统计学书籍中介绍的概念的原始定义。因此,你可以将以下页面视为描述性统计测量中最重要概念的简要回顾。

在本小节中,为了使概念性解释更容易理解,我们将稍微偏离我们的策略游戏数据集,并使用一个包含 24 个观察结果的小数据集。假设我们有 24 个男性的身高(以米为单位)。这里是原始观察结果:

    1.68, 1.83, 1.75, 1.80, 1.88, 1.80, 1.89, 1.84,
    1.90, 1.65, 1.67, 1.62, 1.81, 1.73, 1.84, 1.78,
    1.76, 1.97, 1.81, 1.75, 1.65, 1.87, 1.85, 1.64.

我们将使用这个小数据集来概念性地介绍最重要的描述性统计。为了使我们的计算更容易,让我们创建一个包含这些值的 pandas 系列:

mens_heights = pd.Series\
               ([1.68, 1.83, 1.75, 1.8, 1.88, 1.8, 1.89, 1.84,\
                 1.9, 1.65, 1.67,1.62, 1.81, 1.73, 1.84, 1.78,\
                 1.76, 1.97, 1.81, 1.75, 1.65, 1.87, 1.85, 1.64])

现在我们准备好复习最常用的描述性统计了。

算术平均值:也简称为平均值,这是一种分布中心或一组数字的中心的度量。给定一组观察结果,平均值通过将观察结果相加并将该总和除以观察数量来计算。公式如下,其中x bar ()是平均值,n是观察数量:

图 7.12:算术平均值公式

图 7.12:算术平均值公式

当大多数值聚集在一个中心周围时,平均值尤其有用且信息丰富,因为在这种情况下,平均值将接近该中心,因此将接近许多值,使其成为一组观察结果的代表数字。你在真实世界数据中遇到的许多(也许大多数)数值变量将具有聚集在平均值周围的值,因此平均值通常是一组测量的典型值的良好指标。

让我们使用内置的 pandas 方法计算男性身高的平均值:

mens_heights.mean()

结果如下:

1.7820833

这个数字告诉我们 1.78 米是 24 个男性身高集合的代表值。进行了这个计算后,我们知道一个典型男性(来自我们得到样本的总体)的身高大约为1.78米。

关于平均值需要注意的一点是,它会受到极端值的影响。当分析极端值可能比最常见值大几个数量级的变量时,这一点尤为棘手。

最后,值得一提的是,算术平均值通常是大多数人在说“平均值”时所指的,尽管还有其他平均值,比如中位数,我们稍后会定义。

标准偏差:这是数据的分散程度的一种度量。它衡量了一组测量结果中观察结果的不同或分散程度。数学公式基于算术平均值,供您参考,如下所示:

图 7.13:标准偏差公式

图 7.13:标准偏差公式

在前面的公式中,s代表标准偏差,x bar ()代表平均值,N代表观测次数。由于一个技术细节(超出我们的范围),公式的分母是N – 1而不仅仅是N,但让我们假装一会儿分母是N:如果你仔细看公式,你会发现根号符号内的是关于平均值的平方偏差的平均值。因此,标准偏差是观测结果与其平均值的平均距离。公式中有平方根,所以得到的数字与原始测量结果具有相同的单位。

让我们再次使用 pandas 内置方法计算男性身高的小样本的标准偏差:

mens_heights.std()

这给出了以下结果:

0.0940850

我们的答案是0.094米,或者 9.4 厘米。这个数字的解释是,平均而言,一个男人的身高与平均值(在这种情况下为 1.78 米)相差 9.4 厘米。请注意我们说的是平均而言,这意味着我们可以预期个别男性的身高与平均值相差的距离会比 9.4 厘米更近或更远,但 9.4 是一个能够告诉我们典型观测结果与平均值相差多远的信息性数字。

为了更好地理解标准偏差的概念,将前面的计算与另一组假设的测量结果进行对比将会很有用。假设我们有另外 24 个男性的身高,平均值相似:

mens_heights_2 = pd.Series\
                 ([1.77, 1.75, 1.75, 1.75, 1.73, 1.75, 1.73, 1.75,\
                   1.74, 1.76, 1.75, 1.75, 1.74, 1.76, 1.75, 1.76,\
                   1.76, 1.76, 1.75, 1.73, 1.74, 1.76, 1.76, 1.76])

让我们计算平均值,看看这 24 个男性与我们第一组的平均值相比如何:

mens_heights_2.mean()

结果如下:

1.750416

这比第一组的平均值低 3 厘米。现在让我们计算第二组的标准偏差:

mens_heights_2.std()

结果如下:

0.01082

这只有 1 厘米,这意味着第二组的身高更加均匀。换句话说,第二组的身高彼此更接近(也就是说,它们的分散程度更小)比第一组的身高。由于我们的观测次数很少,我们可以通过仔细观察第二组的测量结果来得知:请注意所有的测量结果都在 1.73 和 1.77 之间,因此与第一组相比,变异性较小。

在极端情况下,如果所有观察结果完全相同,也就是说它们之间没有变化,那么标准偏差将为零。观测结果越不同,标准偏差就会越大。

数据的分散程度还有其他度量方法,但标准偏差可能是最重要的。您应该确保知道如何解释它。

四分位数:四分位数是位置测量。它们表明当观察值从最小值(最小值)到最大值(最大值)排序时,该值处于某个相对位置。通常,第一、第二和第三个四分位数通常表示为Q1Q2Q3

  • Q1:将数据分成这样一种方式,以便 25%的观察值低于这个值。

  • Q2:也称为中位数,这是将数字集合分成两半的数字,意味着 50%的观察值低于这个值,另外 50%的观察值高于这个值。中位数是另一种类型的平均值。

  • Q3:将数据分成这样一种方式,以便 75%的观察值低于这个值。

再次使用我们的前 24 个身高,我们可以对它们进行排序,并将这个小数据集分成四部分,每部分包括 6 个观察值。这在下图中有直观展示:

图 7.14:四分位数示例

图 7.14:四分位数示例

在这个例子中,我们可以将数据集完全分成四部分。第一部分包括最小的六个观察值;第六个观察值是1.68,下一个(属于第二个四分位数)是1.73。从技术上讲,1.681.73之间的任何数字(1.68 < Q1 < 1.73)都可以称为第一个四分位数,例如1.70,因为陈述25%的观察值低于 1.70是正确的。我们也可以选择1.71,因为陈述25%的观察值低于 1.71也是正确的。

中位数(Q2)将观察分成两半。在这种情况下,1.80是第 12 和第 13 个观察值,所以中位数是1.80。(如果有一个数字在第 12 和第 13 个观察值之间,中位数将是中间的数字。)

最后,对于第三个四分位数(Q3),我们从图中看到,1.841.85之间的任何数字(比如 1.845)都是一个值,将底部 75%的观察值与顶部 25%的观察值分开。

请注意,我没有给出任何计算四分位数的公式,那么四分位数是如何计算的呢?有一些方法我们在这本书中不会详细介绍。确切的方法并不重要,真正重要的是你理解这个概念。现在让我们看看如何计算这些值。在下面的代码行中,我们将使用 pandas 的quantile方法(四分位数是更一般的分位数概念的特例,这个概念与百分位数的概念接近):

mens_heights.quantile([0.25, 0.5, 0.75])

结果如下:

0.25    1.7175
0.50    1.8000
0.75    1.8425
dtype:  float64

我们必须向这个方法传递一个列表,指示我们想要进行分割的观察的百分比(比例)——这些分别是 25%,50%和 75%,分别对应Q1Q2Q3。正如你所看到的,第一个四分位数(1.7175)是 1.68 和 1.73 之间的一个数字,中位数是1.80,第三个四分位数(1.8425)是 1.84 和 1.85 之间的一个数字。

要计算分位数,而不是四分位数,我们可以使用任何比例将观察分成两部分:例如,假设我们想要将观察分成底部 80%和顶部 20%,80th 百分位数等同于 0.8 分位数,是一个数字,下面有 80%的观察。类似地,33rd 百分位数等同于 0.33 分位数,是一个数字,下面有 33%的观察。这就解释了为什么我们必须向 pandas 的分位数函数传递一个分位数列表;例如,下面的代码计算了 0.33 和 0.8 分位数(分别对应 33rd 和 80th 百分位数):

mens_heights.quantile([0.33, 0.80])

结果是:

0.33    1.750
0.80    1.858
dtype: float64

根据这个结果,我们的 80%观察值低于1.858。作为一个小练习,通过比较其值与图 7.14中所示范围,检查 33rd 百分位数是否符合你的预期。

描述性统计不仅仅是关于标准的度量指标,比如平均值、中位数等。对数据进行的任何简单的描述性计算也被认为是描述性统计,包括求和、比例、百分比等。例如,让我们计算身高超过 1.80 米的男性比例。有很多方法可以做到这一点,但我们将使用两步方法。首先,让我们运行以下代码行,如果观察满足条件则给出值True,否则给出False

mens_heights >= 1.8

结果看起来像这样:

图 7.15:布尔系列

图 7.15:布尔系列

第二步是计算我们有多少个True值。我们可以使用sum方法来做到这一点,它将True转换为 1,False转换为 0。然后我们可以简单地通过shape方法将观察值的数量除以。整行代码看起来像这样:

(mens_heights >= 1.8).sum()/mens_heights.shape[0]

结果如下:

0.54166

或者我们 24 个身高中有 54%等于或大于 1.8 米。这个比例也被认为是描述性统计,因为它也是在描述我们的数据发生了什么。

前面的计算可以更简洁地进行,如下所示:

(mens_heights >= 1.8).mean()

作为一个小练习,您可以使用图 7.12中给出的平均值公式来推断为什么mean方法会给出布尔系列中True值的比例。

使用描述性统计

现在我们已经重新理解了描述性统计的最重要的度量标准,是时候回到我们的原始数据集,并开始在给定的业务问题背景下使用这些概念了。

我们在前一节中审查的描述性统计数据是如此重要,以至于 pandas 的describe方法(属于 series 和 DataFrames)会计算所有这些统计数据。此外,我们还会发现以下内容:

  • 计数:列中非空值的数量

  • 最小值:最小值

  • 最大值:最大值

当在 DataFrame 上使用describe方法时,它会取所有数值类型的列并计算它们的描述性统计数据:

games.describe()

这会得到以下输出:

图 7.16:DataFrame 的数值列的描述性统计

图 7.16:DataFrame 的数值列的描述性统计

在解释结果之前,您需要知道,默认情况下,pandas 以科学计数法显示输出:例如,4.311000e+03表示 4.311 x 10³ = 4,311。符号"e+k"表示(x 10^k),而"e-k"表示(x 10^-k)。

为了将这些概念付诸实践,让我们读取并解释user_rating_count变量的统计数据:

  • count:输出中的计数值为 4,311。这是我们变量中非空观察值的数量。

  • mean:平均值的输出为 5,789.75。现在我们知道,平均而言我们的数据集中大约有 5,800 个用户对游戏进行评分。为了提取关于这个数字更多的信息(以及它是否有用或不有用),让我们读取其他统计数据。

  • std:标准偏差值为 5,592.43。平均而言,我们数据集中评分游戏的用户数量与平均值相差近 5,600 个用户:想想看,平均评分数量大约为 5,800,而与该数字的典型偏差几乎为 5,600。这告诉我们这个变量的可变性非常大(离散度很高),换句话说:我们有一些游戏只有少数用户评分,而有一些游戏有非常多的用户评分。

  • min25%50%75%max:这些数字告诉我们关于变量观察值分布的重要信息。我们看到最小值是 30(记住我们明确选择了至少 30 个评分的游戏),最大值超过了 300 万!第一四分位告诉我们数据集中 25%的游戏拥有少于 70 个用户评分;考虑到均值为 5,789.75,这是非常少的。中位数是 221 个用户评分,因此一半的游戏拥有少于 221 个评分!最后,第三四分位表明 75%的观察值低于 1,192。

到目前为止,我们大致上只是读取了结果,现在让我们更多地讨论一下,以便将这些数据转化为有用的信息。

知道最大值(超过 300 万),我们可以确定这个变量的均值受到极端值的极大影响,即极其受欢迎的游戏。也许均值对于这个变量来说并不那么有信息量。也许谈论游戏的典型用户评分数量并没有意义,因为数据表明没有典型的数量。事实上,我们知道超过 75%的观察值拥有少于 1,200 个用户评分。这意味着我们应该观察一些受欢迎的游戏。让我们测试一下这个直觉,并深入研究一下评分最多的游戏,使用系列的sort_values方法,以降序查看user_rating_count的前 10 个值:

games['user_rating_count'].sort_values(ascending=False).head(10)

结果如下:

图 7.17:用户评分计数的前 10 个值

图 7.17:用户评分计数的前 10 个值

请注意,只有两个游戏拥有超过一百万的用户评分,这解释了我们从标准差(5,592.43)观察到的高变异性。让我们检查有多少游戏至少拥有 100,000 个用户评分。首先,我们将按条件>= 100000过滤我们的列,然后我们将使用sum方法,该方法将计算满足该条件的值的数量:

(games['user_rating_count'] >= 100000).sum()

结果如下:

40

因此,只有 40 个游戏拥有超过 100,000 个评分,这不到当前数据集的 1%(4,311 个游戏)和原始数据集的 0.23%(17,007 个游戏)。

总之,游戏的用户评分数量变化很大。此外,我们样本中有 25%的游戏拥有少于 70 个用户评分,一半的游戏拥有少于 221 个。此外,我们只有 40 个游戏拥有超过 100,000 个用户评分,其中排名第一和第二的最受欢迎的游戏分别拥有超过 300 万和超过 120 万个评分。

以下是我们向 CEO 呈现信息的方式:

用户评分数量的数据,这是游戏受欢迎程度的代理,表明策略游戏要成为极其受欢迎是非常困难的。数据告诉我们,不到 1%的策略游戏可以被认为是极其受欢迎的(就用户评分数量而言),而超过 75%的游戏拥有少于 1,200 个用户评分,表明用户基数相对较低。请记住,对于这个练习,我们已经排除了那些拥有少于 30 个用户评分的游戏,因此策略游戏成为极其受欢迎的几率远低于 1%。

上述段落是如何从描述性统计中提取有价值信息的一个例子。请注意,我们无需提及任何统计术语,如均值、中位数或标准差,也无需提及分位数、百分位数等术语。还要注意,均值为 5,789.75 这个事实并没有被使用,因为对于我们试图传达的信息来说,这个事实并不是必要的。这是像 CEO 这样的人想要听到的信息,因为它清晰、有信息量且可操作。

我们用一个非常重要的建议来结束本节:不要犯一个错误,把描述性统计计算的列表作为你的分析。另一个常见的错误是包括一个包含分析的段落,比如平均值是 x标准差是 y最大值是 88,基本上只是重复了统计表中包含的信息。请记住,你的工作不仅是进行计算,还要解释这些数字的含义以及它们对你试图解决的问题的影响。

练习 7.02:计算描述性统计

在这个练习中,我们将使用描述性统计来计算平均用户评分变量的值。为此,我们将使用我们在上一节讨论的描述性统计指标。此外,我们还将进行其他描述性计算,包括计数和比例。

  1. 计算average_user_rating列的描述性统计:
games['average_user_rating'].describe()

结果如下:

count    4311.000000
mean        4.163535
std         0.596239
min         1.500000
25%         4.000000
50%         4.500000
75%         4.500000
max         5.000000
Name: average_user_rating, dtype: float64

如你所见,中位数和第三四分位数的值相同,为4.5,这意味着至少有 25%的游戏的平均评分为4.5

  1. 计算评分恰好为4.5的游戏的数量和比例。可以使用mean方法来实现这个目标:
ratings_of_4_5 = (games['average_user_rating'] == 4.5).sum()
proportion_of_ratings_4_5 = (games['average_user_rating'] == 4.5)\
                            .mean()
print(f'''The number of games with an average rating of 4.5 is \{ratings_of_4_5}, \
which represents a proportion of {proportion_of_ratings_4_5:.3f} or \
{100*proportion_of_ratings_4_5:.1f}%''')

我们得到的输出如下:

The number of games with an average rating of 4.5 is 2062, which represents a proportion of 0.478 or 47.8%
  1. 使用unique方法查看该变量的唯一值。你注意到了什么?
games['average_user_rating'].unique()

在这个练习中,我们使用描述性统计来理解我们商业问题的另一个关键变量。

注意

要访问本节的源代码,请参阅packt.live/2VUWhI3

你也可以在packt.live/2Zp0Z1u上在线运行这个例子。

以下是一些可能会出现在脑海中的问题:

  • 这个变量的平均值是你用来理解这个变量的统计量吗?你会选择它作为变量的典型值,还是会选择另一个值?

  • 根据标准差,你认为这个变量的变异性高还是低?

  • 你如何用描述性统计获得的信息来总结成一段话?

在本节中,我们了解了为什么需要和使用描述性统计。从我们介绍的第一个例子中,我们了解到我们的大脑没有自动理解数值信息的能力。因此,如果我们想要理解数值数据,就有必要拥有这些类型的分析工具。

然后我们简要回顾了(或介绍了)一些最常用的描述性统计指标,包括平均值、标准差和分位数。然后我们立即应用了这些知识,使用策略游戏数据集计算了数值变量的描述性统计。我们分析了用户评分计数变量的结果,并生成了一个包含非技术人员可以理解的相关信息的摘要。

最后,我们使用了 pandas 内置方法,如meanstddescribequantile等进行了所有计算。

现在我们知道了描述性统计的基础知识,我们可以扩展我们的统计工具包,并通过可视化来补充我们的分析,这将在下一节中介绍。

探索性数据分析

在本节中,我们将回顾我们在本章第一节对一个商业问题进行了一些初步分析,该问题如下:

您所在的游戏开发公司的 CEO 提出了一项计划,以加强公司在游戏市场的地位。根据他的行业知识和其他商业报告,他知道吸引新客户的一种非常有效的方法是在移动游戏领域建立良好的声誉。鉴于这一事实,他制定了以下计划:为 iOS 平台开发一款策略游戏,这将吸引大量积极的关注,从而为公司带来大量新客户。他确信只有游戏得到用户的好评,他的计划才会奏效。由于他是移动游戏领域的新手,他请求您帮助回答以下问题:哪种类型的策略游戏得到了用户的好评?

在本节中,我们将进行一些探索性数据分析。您可以将本节视为上一节的延续,因为我们将继续使用描述性统计,并且除此之外,我们还将扩展我们的分析工具包,其中包括最强大的分析工具之一:可视化。

探索性数据分析EDA)领域(就像与数据相关的任何其他领域一样)发展迅速,本节内容仅涵盖了一些非常基本的概念和可视化。尽管如此,很可能您会在遇到的每个数据分析项目中使用本节中呈现的可视化。

什么是探索性数据分析?

现在我们已经复习了基本的统计定义,我们准备利用它们并用可视化来补充我们从中获得的信息。简而言之,探索性数据分析是通过将描述性统计与可视化相结合来分析数据的过程。进行探索性数据分析的原因和目标包括以下内容:

  • 理解变量的分布

  • 理解两个或多个变量之间的关系

  • 检测无法通过数值计算找到的模式

  • 发现数据中的异常或离群值

  • 制定关于因果关系的假设

  • 告诉我们如何构建新变量(特征工程)

  • 告诉我们可能的正式推断统计检验

根据定义,在进行探索性数据分析时,我们正在探索数据,因此没有固定步骤或一套固定的步骤要遵循,需要涉及很多创造力。然而,为这个过程提供一些结构也非常重要,否则我们可能会产生没有明确结束点的图表和计算。在数据分析中(就像在生活中一样),如果没有明确的目的,很容易迷失方向,因为有无数种方法来查看任何规模适中的数据集。在开始进行探索性数据分析之前,我们需要定义我们试图找到的是什么,这就是业务问题理解的关键所在。

我建议分两步进行探索性数据分析:

  1. 单变量步骤:了解数据集中最重要的每个变量:分布、关键特征、极端值等。

  2. 寻找关系:在考虑业务/科学问题的情况下,制定一系列问题,这些问题的答案将为您提供有关您试图解决的问题的有用信息,然后让这些问题引导探索性数据分析过程。

第一步通常被称为单变量分析,相对来说比较简单。对于第二步,我们使用双变量多变量技术,因为我们需要寻找变量之间的关系。在下一节中,我们将进行单变量探索性数据分析。

单变量探索性数据分析

正如其名称所示,单变量探索性数据分析是关于一次分析一个变量。在上一节中,我们看到使用 pandas 计算主要描述性统计有多么容易。现在我们将使用适当的可视化来补充我们从描述性统计中获得的信息。

在进行探索性数据分析之前,了解我们正在处理的变量类型是绝对必要的。作为复习,这里有我们可能遇到的主要变量类型:

  1. 数值变量:那些可以取数值的变量:

a. 连续:这些是可以在某个区间内取一整个范围的实值的变量,例如身高、体重和质量。

b. 离散:这些是只取特定有限数值的变量,通常是整数。例如,家庭中的孩子数量或雇员数量。

  1. 分类变量:只能取指定数量的类别作为值的变量:

a. 有序:变量的类别具有一定的自然顺序。例如,变量年龄组可以有类别 20-29、30-39、40-49 和 50+。请注意,这些类别中有一定的顺序:20-29 小于 40-49。

b. 名义:这些是分类变量,其中没有排序关系。例如,蓝色、绿色和红色是没有任何排序的类别。

  1. 与时间相关的变量:与日期或日期时间相关的变量。

让我们首先识别数据集中的数值变量。就像我们在准备数据集时做的那样,我们意识到概念上是数值的变量具有相应的 Python 数值数据类型。让我们再次使用修改后的游戏 DataFrame 中的info方法来检查这一点:

games.info()

输出如下:

图 7.18:数据集中的数值变量

图 7.18:数据集中的数值变量

正如我们所看到的,概念上是数值的列:average_user_ratinguser_rating_countpricesize的数据类型为float64

单个数值变量最常用的有用可视化是直方图。让我们看看它的样子,然后描述它是如何构建的。为此,我们将使用size变量。由于这个变量是以字节为单位的,所以在可视化之前,我们将把它转换为兆字节:

games['size'] = games['size']/1e6

现在我们可以使用 pandas 内置的hist方法:

games['size'].hist(bins=30, ec='black');

我们还包括了两个额外的参数:bins,它是我们看到的柱子的数量,以及ec(边缘颜色),它是柱子边缘的颜色。得到的图如下:

图 7.19:尺寸直方图

图 7.19:尺寸直方图

直方图是通过将变量的范围分成等大小的间隔(称为区间)来构建的。在这种情况下,最大值和最小值分别为 4,005.6 和 0.2,因此范围约为 4,005。由于我们指定了 30 作为区间的数量,每个区间的大小约为 133 ~ 4,005 / 30。因此,第一个区间大约从最小值(约为 0)到 133,第二个区间从 133 到 266 = 133 + 133,依此类推。柱子的高度对应于落入特定区间的观察数量。例如,我们看到第一个柱子略微超过 2,500 个观察,这是落入第一个区间的观察数量(从 0 到约 133)。与四分位数一样,构建直方图的确切算法因使用的软件而异。Pandas 使用 Matplotlib 实现,因此如果想了解详情,请查阅 Matplotlib 的文档。

你应该习惯看直方图并尝试阅读它们。例如,尺寸的直方图显示如下:

  • 重要比例的游戏落在第一个区间(最高的柱子)中。

  • 大多数值都在 500MB 以下。

  • 观察数量(频率)随着变量增加而减少:随着尺寸的增加,我们观察到的游戏越来越少。

  • 图表的x轴延伸到大约 4000 MB;然而,我们甚至看不到一个柱形,原因是我们的观测值很少,那个柱形非常小,几乎可以忽略不计。这意味着我们至少有一个极端值(一个非常大尺寸的游戏)。

  • 在 1000 MB 以上的柱形的高度非常低,所以 1000 MB 以上的游戏非常少。

直方图是对我们从描述性统计中得到的数值信息的完美补充:

games['size'].describe()

输出如下:

count        4311.000000
mean         175.956867
std          286.627800
min          0.215840
25%          40.736256
50%          97.300480
75%          208.517632
max          4005.591040
NameL size,  dtype: float64

中位数告诉我们,超过一半的游戏大小小于 97.3 MB,而最大大小的游戏比中位数大 40 倍以上,我们可以认为这是一个异常值或者是远远超过变量大部分值的观测值。与用户评分计数一样,我们可以通过按降序对系列进行排序然后显示前 12 个值来检查最大的游戏:

games['size'].sort_values(ascending=False).head(12)

输出如下:

图 7.20:尺寸最大的值

图 7.20:尺寸最大的值

我们看到实际上有一些异常值,不仅仅是最大值。实际上,异常值没有标准定义——它取决于上下文。我们之所以称这些观测值为异常值,是因为它们可以被认为是一组游戏中的大尺寸,其中 75%的游戏尺寸小于 208 MB。

好的,回到直方图。pandas 的一个非常酷的功能是它能够使用DataFrame类的hist方法同时为许多数值变量生成直方图。当数据集中有很多数值变量并且您想快速查看它们时,这个功能将非常有用:

games.hist(figsize = (10, 4), bins = 30, ec = 'black');
# This line prints the four plots without overlap
plt.tight_layout()

pandas 自动获取所有数值变量并生成一个网格(可调整大小)的图表。在我们的情况下,有四个数值变量,结果如下:

图 7.21:DataFrame 直方图示例

图 7.21:DataFrame 直方图示例

pricesizeuser_rating_count中有一些极端值(异常值),这导致我们无法真正看到这些变量的值是如何分布的。

凭借我们对分位数(和百分位数)的了解,让我们创建一个过滤器,将在这三个变量中排除最大值的 1%,这样就有望更好地理解分布:

filter_price = games['price'] <= games['price'].quantile(0.99)
filter_user_rating_count = games['user_rating_count'] \
                          <= games['user_rating_count'].quantile(0.99)
filter_size = games['size'] <= games['size'].quantile(0.99)
filter_exclude_top_1_percent = filter_price \
                               & filter_user_rating_count \
                               & filter_size
games[filter_exclude_top_1_percent].hist(figsize = (10, 4),\
                                         bins = 30, ec = 'black');
# This line prints the four plots without overlap
plt.tight_layout()

结果如下:

图 7.22:DataFrame 直方图示例

图 7.22:DataFrame 直方图示例

现在直方图已经揭示了更多信息,我们可以从直方图中读取以下几点:

  • 大多数游戏是免费的,而极少数不免费的游戏中,绝大多数价格低于 10 美元。

  • 4.5 是最常见的平均用户评分;事实上,我们观察到很少有低于 3 的平均评分的游戏。

  • 大尺寸的游戏很少见。

  • 大多数游戏的用户评分很少。

作为练习,尝试从这些图表中提取更多信息,并结合这些变量的描述性统计进行补充:例如,评论大小变量的衰减模式,或者我们在用户评分计数中看到的高度集中模式:是什么导致了这些形状?

现在让我们谈谈另一个用于查看连续变量分布的有用图表:箱线图。箱线图是位置统计量Q1、中位数和Q3的图形表示,通常还显示最小值和最大值。可以使用以下代码生成包含 24 个观测值的样本数据集的箱线图:

mens_heights.plot(kind='box');

结果如下(已在图中添加了描述性统计的注释):

图 7.23:箱线图示例

图 7.23:箱线图示例

箱线图由两个边缘线和一个箱子组成。第一条边缘线(通常)从最小值开始,然后延伸到Q1,标志着箱子的开始,因此第一条边缘线覆盖了底部 25%的观察结果。箱子从Q1Q3,覆盖了观察结果的中间一半。箱子的高度称为四分位距IQR),是离散度的度量,告诉我们观察结果的中间 50%有多么集中:较大的 IQR 意味着更多的离散度。箱子中间的线对应于中位数,最后,上边缘线(通常)结束于最大值。

请注意,我在括号中添加了一对“通常”来表示最大值和最小值。当一个观察值高于Q3 + 1.5 x IQR(或低于Q1 - 1.5 x IQR)时,通常被认为是异常值的候选,并被绘制为一个点。如果我们有这样的观察结果,那么上(下)边缘线将结束于Q3 + 1.5 x IQR(或Q1 - 1.5 x IQR)。例如,这是size变量的箱线图:

games['size'].plot(kind='box');

结果如下:

图 7.24:尺寸的箱线图

图 7.24:尺寸的箱线图

在这种情况下,上边缘线并不是在最大值结束,而是延伸到Q3 + 1.5 x IQR。从这个图中,我们可以说这个变量有很多极端值。尽管箱线图有时对于单变量 EDA 可能有用,但直方图更可取。箱线图最好用于分析数值变量与分类变量的关系。我们将在下一小节中回到箱线图。

要完成本小节,让我们看看如何生成条形图,用于显示分类变量的计数、比例或百分比。让我们看看age_rating列,这是一个分类变量。以下代码将计算变量各个值的游戏数量:

games['age_rating'].value_counts()

结果如下:

4+     2287
9+     948
12+    925
17+    151
Name: age_rating,  dtype: int64

由于结果也是一个 pandas 系列,我们可以链接方法,并使用plot方法和参数kind='bar'来获得我们的条形图:

games['age_rating'].value_counts().plot(kind = 'bar');

结果如下:

图 7.25:年龄评级的绝对计数的条形图

图 7.25:年龄评级的绝对计数的条形图

如果我们想要可视化比例,我们可以通过在value_counts方法中添加normalize=True参数来修改前面的代码行:

games['age_rating'].value_counts(normalize=True).plot(kind='bar');

图表看起来几乎相同,唯一的变化在于y轴标签,现在显示比例:

图 7.26:年龄评级比例的条形图

图 7.26:年龄评级比例的条形图

最后,可视化比例的另一种选择是使用饼图。饼图已知存在一些问题,其中包括它们不是传达信息的好方法,这就是为什么我从不使用它们。然而,它们用于呈现业务信息,所以如果你的老板要求你做一个饼图,这里是如何生成一个:

games['age_rating'].value_counts().plot(kind = 'pie');

这是结果:

图 7.27:年龄评级的饼图

图 7.27:年龄评级的饼图

饼图的问题在于它们只是美化文件的一种方式,而不是传达定量信息的好方法;可视化是用于在我们想要补充和超越数值计算所能传达的内容时使用的。如果我们想要传达比例(或百分比),最好的方法就是显示实际值,如下所示:

percentages = 100*games['age_rating'].value_counts(normalize=True)
for k, x in percentages.items():
    print(f'{k}: {x:0.1f}%')

这将产生以下输出:

4+: 53.1%
9+: 22.0%
12+: 21.5%
17+: 3.5%

双变量 EDA:探索变量之间的关系

探索变量之间的关系是统计分析中最有趣的方面之一。在探索变量对之间的关系时,考虑到只有数值和分类变量的最广泛的划分,我们有三种情况:

  • 数值与数值

  • 数值与分类

  • 分类与分类

对于第一种情况,散点图是首选的可视化方式。对于第二种情况,根据我们想要找到的内容,我们有一些选择,但通常箱线图最有用。最后,对于第三种情况,最常见的选择是呈现所谓的列联表:尽管存在一些用于比较分类数据的可视化选项,但它们并不常见。

正如我们在探索性数据分析中所说的,当进行这种类型的分析时,通常在开始生成可视化之前制定要回答的问题列表是一个好主意。牢记我们的业务问题,我们将尝试回答以下三个问题:

  • 尺寸和平均用户评分之间的关系是什么?

  • 年龄评级和平均用户评分之间的关系是什么?

  • 有无应用内购买和游戏评分之间的关系是什么?

我们将尝试使用散点图来回答第一个问题。在其最简单的版本中,散点图使用笛卡尔平面显示一对变量的每个点。每对点由一个点表示,点的模式表明两个绘制变量之间是否存在某种关系。以下表格显示了使用散点图可以检测到的一些模式的示例:

图 7.28:散点图中的模式示例

图 7.28:散点图中的模式示例

请记住,表中的示例仅供参考:通常,现实世界的数据集不会提供如此容易识别的模式。

虽然我们可以使用 pandas 创建图表,但我们将使用 Seaborn,这是一个非常流行的统计可视化工具,可以仅用几行代码生成美丽和复杂的图表。我们将使用scatterplot函数,该函数接受将放在每个轴上的变量的名称和 DataFrame 的名称:

sns.scatterplot(x='size', y='average_user_rating',\
                data=games, \
                # this is for controlling the size of the points
                s=20);

这里是输出:

图 7.29:尺寸与平均用户评分的散点图

图 7.29:尺寸与平均用户评分的散点图

如果我们看一下图的右上角,似乎某种尺寸的游戏,比如超过 1,500 MB 的游戏,倾向于具有 3.5 及以上的评分。由于尺寸是图形质量和游戏复杂性的代理,这个图似乎表明提高获得体面平均评分的机会的一种方法是制作一定复杂性和视觉质量的游戏。然而,图表还显示相对较小的游戏获得了 5 的平均评分。

现在让我们探索数值和分类变量之间的关系。也许如果我们将平均评分视为分类变量,我们可以更多地了解评分。毕竟,由于数据集的一些怪癖,这个变量是离散的而不是连续的;它只取整数和半数值。以下代码使用字典中定义的映射对变量进行分类:

ratings_mapping = {1.5: '1_poor', 2.: '1_poor',\
                   2.5: '1_poor', 3: '1_poor',\
                   3.5: '2_fair', 4\. : '2_fair',\
                   4.5: '3_good',5\. : '4_excellent'}
games['cat_rating'] = games['average_user_rating']\
                      .map(ratings_mapping)

我们创建了一个使用分类变量的新平均用户评分比例。现在我们可以使用箱线图来查看不同类型评分的尺寸值分布是否发生变化:

sns.boxplot(x='cat_rating', y='size', \
            data=games[games['size'] <= 600], \
            order=['1_poor', '2_fair', '3_good', '4_excellent']);

结果如下:

图 7.30:尺寸与分类用户评分的箱线图

图 7.30:尺寸与分类用户评分的箱线图

我们将数据集限制在 600 MB 以下的游戏,以查看尺寸评分的关系是否适用于不太大的游戏。我们看到分布实际上是不同的,一般来说,评分较低的游戏的尺寸要小于其他类别(箱线图低于其他)。请注意,好评和优秀评分的游戏的分布几乎相同,也许表明对于 600 MB 以下的游戏,复杂性和高质量图形在一定程度上影响评分。

最后,让我们回顾第三种情况:如何探索两个分类变量之间的关系。为此,让我们探索年龄评分与我们刚刚创建的分类评分之间的关系。我们可以生成一个表,计算我们两个变量的值组合中有多少观察结果。这通常被称为列联表。Pandas 有一个方便的crosstab函数来实现这个目的:

pd.crosstab(games['age_rating'], games['cat_rating'])

结果如下:

图 7.31:年龄评分与分类用户评分的列联表

图 7.31:年龄评分与分类用户评分的列联表

拥有计数是很好的,但是仍然有点难以理解这些数据。为了找出这两个变量是否相关,我们需要找出年龄评分的比例是否根据游戏的好坏而变化。例如,如果我们发现 4+游戏中有 90%的游戏评分较低,同时 17+游戏中只有 15%的游戏评分较低,那么可以合理地假设这些变量之间存在某种关系。为了进行这种计算,我们必须对前一个表格进行标准化。我们通过添加normalize='index'参数来实现这一点:

100*pd.crosstab(games['age_rating'],\
                games['cat_rating'], \
                normalize='index')

我们已经将整个表乘以 100,这样更容易阅读为百分比:

图 7.32:行标准化的年龄评分与分类用户评分的列联表

图 7.32:行标准化的年龄评分与分类用户评分的列联表

由于行已经被标准化,每行的总和应该为 100。现在我们可以轻松比较不同用户评分在不同年龄评分中的分布。例如,我们观察到,无论年龄评分如何,优秀评分的游戏比例几乎相同,其他列也是如此(多多少少)。这表明也许游戏的年龄评分对游戏的评分并不是一个重要因素。

这就是统计分析成为一门艺术的地方。最初的探索结果产生了新的问题和假设,我们将进一步使用更多的数值和可视化分析来探索,希望经过几次迭代后,我们将产生有关手头问题的有用信息。

我将通过说明,尽管本书的范围集中在可视化两个变量之间的关系上,但也可以通过可视化探索三个或更多变量之间的关系。但是,请记住,在可视化中使用两个以上的变量通常会大幅增加分析的复杂性。

练习 7.03:练习 EDA

在这个练习中,我们将使用箱线图来可视化免费游戏和付费游戏的评分是否不同。

  1. 首先,让我们看看数据集中有哪些不同的价格。为此,查看价格的唯一值:
games['price'].unique()

输出如下:

array([  2.99,   1.99,   0\.  ,   0.99,   5.99,   7.99,   4.99,
         3.99, 9.99,  19.99,   6.99,  11.99,   8.99, 139.99,
         14.99,  59.99])
  1. 看起来所有的游戏都是以一定数量的美元加 99 美分出售的。我们知道在实际中 2.99 意味着 3 美元。使用内置的 round 方法将这个变量转换为整数值,这样值就是漂亮的整数:
games['price'] = games['price'].round()
  1. 由于这是一个离散数值变量,请使用条形图来可视化每个价格的游戏分布:
games['price'].value_counts().sort_index().plot(kind='bar');

输出如下:

图 7.33:按价格观察次数的条形图

图 7.33:按价格观察次数的条形图

  1. 看起来大多数游戏都是免费的。为了简化分析,创建一个名为cat_price的分类变量,指示游戏是免费还是付费:
games['cat_price'] = (games['price'] == 0).astype(int)\
                      .map({0:'paid', 1:'free'})
  1. 使用箱线图来可视化前一点中创建的变量之间的关系:
sns.boxplot(x='cat_price', y='average_user_rating', \
            data=games);

输出如下:

图 7.34:箱线图:cat_price 与平均用户评分

图 7.34:箱线图:cat_price 与平均用户评分

从图表中,我们可以看到免费和付费游戏的平均用户评分分布几乎相同。这表明免费与付费游戏的状态不会影响游戏的评分。

在这个练习中,我们使用箱线图来探索变量价格的分布,并查看这个变量与平均用户评分之间是否存在某种关系。

注意

要访问此特定部分的源代码,请参阅packt.live/2VBV2gI

您也可以在packt.live/2YUGv1I上线运行此示例。

在本节中,我们学习了 EDA,这是将描述性统计与可视化相结合的过程。我们了解了几种在几乎每种统计分析中使用的最有用的可视化类型。您已经熟悉了直方图、箱线图、条形图和散点图,这些都是强大的工具,可以补充数值分析并揭示有关数据集的有用信息。

在每个统计分析中,EDA 是一个必不可少的步骤,因为它允许我们了解数据集中的变量,识别它们之间的潜在关系,并生成可以使用形式推断方法进行正式测试的假设。现在我们已经学会了描述性统计的基础知识,我们可以继续学习推断统计学,但首先,我们必须学习一些概率论的基础知识,这是下一章的主题。

活动 7.01:查找评分高的策略游戏

您所在的游戏开发公司已经提出了一项计划,以加强其在游戏市场的地位。根据行业知识和其他商业报告,很明显吸引新客户的一个非常有效的方法是在移动游戏领域建立良好的声誉。鉴于这一事实,您的公司有以下计划:为 iOS 平台开发一款策略游戏,这款游戏将受到很多积极关注,从而将大量新客户带到公司。公司相信只有游戏得到用户的好评,这个计划才会奏效。由于您在移动游戏领域有丰富的经验,因此您被要求回答以下问题:哪种类型的策略游戏得到了很高的用户评分?

这项活动的目标是双重的:首先是基于两个分类变量的组合创建一个新变量。然后,使用groupby方法对用户评分进行描述性统计,以查看平均用户评分与新创建变量之间是否存在关系。

完成步骤:

  1. 加载numpypandas库。

  2. 加载策略游戏数据集(在本章的data文件夹中)。

  3. 执行本章第一部分中的所有转换:

a. 更改变量的名称。

b. 将id列设置为index

c. 删除urlicon_url列。

d. 将original_release_datecurrent_version_release_date更改为datetime

e. 从 DataFrame 中删除average_user_rating为空的行。

f. 仅保留 DataFrame 中user_rating_count等于或大于 30 的行。

  1. 打印数据集的维度。您必须有一个包含 4,311 行和 15 列的 DataFrame。

  2. 用字符串EN填充languages列中的缺失值,以表示这些游戏只能用英语进行游玩。

  3. 创建一个名为free_game的变量,如果游戏价格为零,则其值为free,如果价格大于零,则其值为paid

  4. 创建一个名为multilingual的变量,如果语言列只有一个语言字符串,则其值为monolingual,如果语言列至少有两个语言字符串,则其值为multilingual

  5. 创建一个变量,其中包含在上一步中创建的两个变量的四种组合(free-monolingualfree-multilingualpaid-monolingualpaid-multilingual)。

  6. 计算price_language变量中每种类型的观察数量。

  7. 在 games DataFrame 上使用groupby方法,按照新创建的变量进行分组,然后选择average_user_rating变量并计算描述统计信息。

Note

此活动的解决方案可在第 681 页找到。

在这个活动中,我们展示了创建一个新的分类变量的一种方法,该变量是由两个其他分类变量的可能组合产生的。然后,我们使用groupby方法来计算新创建变量的可能值的描述统计信息。

Summary

在本章中,我们学习了进行任何统计分析的第一步:首先,我们定义了我们的业务问题并介绍了数据集。根据我们想要解决的问题,我们相应地准备了数据集:删除了一些记录,填补了缺失值,转换了一些变量的类型,并创建了新变量。然后,我们了解了描述统计的必要性;我们学会了使用 pandas 轻松计算它们以及如何使用和解释这些计算。在最后一部分,我们学习了如何将可视化与描述统计相结合,以更深入地了解数据集中变量之间的关系。在本章中学到的概念和技术,您可以在进行任何数据分析时进行实践。然而,要在分析中变得更复杂,您需要对概率论的基础有很好的掌握,这是我们下一章的主题。

QDN92

MWM57

第八章:基础概率概念及其应用

概述

在本章结束时,您将熟悉概率论中的基本和基础概念。您将学习如何使用 NumPy 和 SciPy 模块进行模拟,并通过计算概率来解决问题。本章还涵盖了如何使用模拟计算事件的概率和理论概率分布。除此之外,我们还将概念化地定义和使用包含在 scipy.stats 模块中的随机变量。我们还将了解正态分布的主要特征,并通过计算概率分布曲线下的面积来计算概率。

介绍

在上一章中,我们学习了如何进行任何统计分析的第一步。给定一个业务或科学问题和相关数据集,我们学习了如何加载数据集并准备进行分析。然后,我们学习了如何计算和使用描述性统计来理解变量。最后,我们进行了探索性数据分析,以补充我们从描述性统计中收集到的信息,并更好地理解变量及其可能的关系。在对分析问题有了基本了解之后,您可能需要再进一步使用更复杂的定量工具,其中一些工具在以下领域中使用:

  • 推断统计学

  • 机器学习

  • 规范分析

  • 优化

所有这些领域有什么共同点?很多:例如,它们具有数学性质,它们大量使用计算工具,并且它们以某种方式使用概率论,这是应用数学中最有用的分支之一,为其他学科提供了基础和工具,比如前面提到的学科。

在本章中,我们将对概率论进行非常简要的介绍。与传统的统计书籍不同,在本章中,我们将大量使用模拟来将理论概念付诸实践,并使其更具体化。为此,我们将广泛使用 NumPy 和 SciPy 的随机数生成能力,并学习如何使用模拟来解决问题。在介绍了必要的基础概念之后,我们将向您展示如何使用 NumPy 生成随机数,并利用这些能力来计算概率。在这样做之后,我们将定义随机变量的概念。

在本章后期,我们将更深入地探讨两种随机变量:离散和连续,并且对于每种类型,我们将学习如何使用 SciPy 创建随机变量,以及如何使用这些分布计算确切的概率。

随机性、概率和随机变量

这是一个密集的部分,有许多理论概念需要学习。虽然很沉重,但你将以对概率论中一些最基本和基础概念非常好的掌握完成这一部分。我们还将介绍非常有用的方法,您可以使用 NumPy 进行模拟,以便您可以使用一些代码进行操作。通过使用模拟,我们希望向您展示理论概念如何转化为实际数字和可以用这些工具解决的问题。最后,我们将定义随机变量概率分布,这是在使用统计学解决现实世界问题时需要了解的两个最重要的概念之一。

随机性和概率

我们都对随机性的概念有直观的了解,并在日常生活中使用这个术语。随机性意味着某些事件以不可预测或无规律的方式发生。

关于随机事件的一个悖论事实是,尽管个别随机事件根据定义是不可预测的,但在考虑许多这样的事件时,可以以非常高的信心预测某些结果。例如,当一次抛硬币时,我们无法知道我们将看到两种可能结果中的哪一个(正面或反面)。另一方面,当抛硬币1,000次时,我们几乎可以肯定我们会得到 450 到 550 个正面。

我们如何从个别不可预测的事件转变为能够预测一系列事件的有意义结果?关键在于概率论,这是数学的一个分支,形式化了对随机性的研究和对某些结果可能性的计算。概率可以被理解为不确定性的度量,概率论给了我们理解和分析不确定事件的数学工具。这就是为什么概率论作为决策工具如此有用:通过严谨和逻辑地分析不确定事件,我们可以做出更好的决策,尽管存在不确定性。

不确定性可能来自无知或纯粹的随机性,以至于抛硬币并不是一个真正的随机过程,如果你知道硬币的质量、手指的确切位置、投掷时施加的确切力量、确切的重力吸引力等等。有了这些信息,你原则上可以预测结果,但在实践中,我们不知道所有这些变量或者实际上做出确切预测的方程。另一个例子可能是足球比赛的结果、总统选举的结果,或者一周后是否会下雨。

鉴于我们对未来发生的事情一无所知,分配概率是我们能够做出最佳猜测的方法。

概率论也是一个大生意。整个行业,如彩票、赌博和保险,都是建立在概率法则和如何从中获利的基础上的。赌场不知道玩轮盘的人在下一局会赢,但由于概率法则,赌场老板完全确定轮盘是一款盈利的游戏。保险公司不知道客户明天是否会发生车祸,但他们确信有足够的汽车保险客户支付保费是一项盈利的业务。

尽管下一节会感觉有点理论性,但在我们能够用它们解决分析问题之前,有必要了解最重要的概念。

基础概率概念

我们将从大多数这一主题的处理中找到的基本术语开始。我们必须学习这些概念,以便能够严格解决问题,并以技术上正确的方式传达我们的结果。

我们将从实验的概念开始:在受控条件下发生的情况,我们从中得到一个观察。我们观察到的结果称为实验的结果。以下表格呈现了一些实验的示例,以及一些可能的结果:

图 8.1:示例实验和结果

图 8.1:示例实验和结果

实验的样本空间包括所有可能结果的数学集。最后,事件是样本空间的任何子集。样本空间的每个元素称为简单事件,因为它由单个元素组成。现在我们有了四个相互关联且对概率论至关重要的术语:实验、样本空间、事件和结果。继续使用前表中的示例,以下表格呈现了实验的样本空间和事件的示例:

图 8.2:示例实验、样本空间和事件

图 8.2:示例实验、样本空间和事件

注意

请注意,上表中的细节假设每分钟最多进行 1 笔交易。

值得注意的是,我们将样本空间定义为数学意义上的集合。因此,我们可以使用从集合论中知道的所有数学运算,包括获取子集(即事件)。由于事件是较大集合的子集,它们本身也是集合,因此我们可以使用并集、交集等。事件的常规表示法是使用大写字母,如 A、B 和 C。

当实验的结果属于事件时,我们说事件发生了。例如,在实验掷骰子中,如果我们对事件得到奇数感兴趣,并且观察到任何结果,即 1、3 或 5,那么我们可以说事件发生了。

在进行随机实验时,我们不知道会得到哪个结果。在概率理论中,我们为与实验相关的所有可能事件分配一个数字。这个数字就是我们所知道的事件的概率

我们如何为事件分配概率?有几种选择。但是,无论我们使用哪种方法为事件分配概率,只要我们为事件分配概率的方式满足以下四个条件,概率理论及其结果就成立。给定事件AB及其概率,表示为P(A)P(B)

  1. a:事件的概率始终是 0 到 1 之间的数字。越接近 1,事件发生的可能性就越大。极端情况下,事件无法发生为 0,事件一定会发生为 1。

  2. b:如果 A 是空集,则概率必须为 0。例如,对于实验掷骰子,事件得到大于 10 的数字不存在,因此这是空集,其概率为 0。

  3. c:这基本上表示进行实验时,一定会发生某种结果。

  4. d对于不相交的事件 A 和 B:如果我们有一组不重叠的事件 A 和 B,则事件(A U B),也称为A 或 B,的概率可以通过添加各自的概率来获得。这些规则也适用于两个以上的事件。

在概念和理论方面,本小节内容较多,但现在理解这些内容很重要,以避免以后出现错误。幸运的是,我们有 Python 和 NumPy 以及它们出色的数值能力,将帮助我们将这些理论付诸实践。

注意

关于所有练习和相关测试脚本的快速说明:如果您正在使用 CLI(如 Windows 的命令提示符或 Mac 的终端)运行测试脚本,它会抛出错误,比如在子类中实现 enable_gui。这与笔记本中使用的一些命令有关(如%matplotlib inline)。因此,如果您想运行测试脚本,请使用 IPython shell。本书中的练习代码最好在 Jupyter 笔记本上运行。

使用 NumPy 进行模拟的介绍

要开始将所有这些理论付诸实践,让我们从加载本章中将使用的库开始:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# line to allow the plots to be shown in the Jupyter Notebook
%matplotlib inline

我们将广泛使用 NumPy 的随机数生成能力。我们将使用np.random模块,它能够生成遵循许多最重要的概率分布的随机数(稍后详细介绍概率分布)。

让我们开始模拟一个随机实验:掷一个普通骰子

让我们学习如何使用 NumPy 执行这个实验。有不同的方法可以做到这一点。我们将使用random模块中的randint函数,该函数生成low(包括)和high(不包括)参数之间的随机整数。由于我们想生成 1 到 6 之间的数字,我们的函数将如下所示:

def toss_die():
    outcome = np.random.randint(1, 7)
    return outcome

让我们连续十次使用我们的函数来观察它的工作方式:

for x in range(10):
    print(toss_die())

以下是一个示例输出:

6, 2, 6, 5, 1, 3, 3, 6, 6, 5 

由于这些数字是随机生成的,你很可能会得到不同的值。

对于这个函数和几乎每个产生随机数(或其他随机结果)的函数,有时需要以任何时刻运行代码的人都能获得相同结果的方式生成随机数。为此,我们需要使用“种子”。

让我们添加一行代码来创建种子,然后连续十次使用我们的函数来观察它的工作方式:

np.random.seed(123)
for x in range(10):
    print(toss_die(), end=', ')

结果如下:

6, 3, 5, 3, 2, 4, 3, 4, 2, 2

只要运行包含种子函数内部的数字 123 的第一行,任何运行此代码的人(使用相同的 NumPy 版本)都将获得相同的输出数字。

numpy.random模块中另一个有用的函数是np.random.choice,它可以从向量中抽样元素。假设我们有一个由 30 名学生组成的班级,我们想随机选择其中的四个。首先,我们生成虚构的学生名单:

students = ['student_' + str(i) for i in range(1,31)]

现在,可以使用np.random.choice来随机选择其中四个:

sample_students = np.random.choice(a=students, size=4,\
                                   replace=False)
sample_students

以下是输出:

array(['student_16', 'student_11', 'student_19', \
       'student_26'], dtype='<U10')

replace=False参数确保一旦选择了一个元素,就不能再次选择它。这被称为无重复抽样

相反,带替换抽样意味着在产生每个样本时考虑向量的所有元素。想象一下,向量的所有元素都在一个袋子里。我们每次随机抽取一个元素作为样本,然后在抽取下一个样本之前将我们得到的元素放回袋子里。这样的应用可能是:假设我们将在 12 周内每周给小组中的一个学生出一次突击测验。所有学生都是可能被给予测验的对象,即使该学生在之前的周被选中过。为此,我们可以使用replace=True,如下所示:

sample_students2 = np.random.choice(a=students, \
                                    size=12, replace=True)
for i, s in enumerate(sample_students2):
    print(f'Week {i+1}: {s}')

结果如下:

Week 1: student_6
Week 2: student_23
Week 3: student_4
Week 4: student_26
Week 5: student_5
Week 6: student_30
Week 7: student_23
Week 8: student_30
Week 9: student_11
Week 10: student_6
Week 11: student_13
Week 12: student_5

正如你所看到的,可怜的学生 6 在第 1 周和第 10 周被选中,学生 30 在第 6 周和第 8 周被选中。

现在我们知道如何使用 NumPy 生成骰子结果和获取样本(带或不带替换),我们可以用它来练习概率。

练习 8.01:带和不带替换的抽样

在这个练习中,我们将使用random.choice来产生带和不带替换的随机样本。按照以下步骤完成这个练习:

  1. 导入 NumPy 库:
import numpy as np
  1. 创建两个包含四种不同花色和 13 种不同等级的标准牌组的列表:
suits = ['hearts', 'diamonds', 'spades', 'clubs']
ranks = ['Ace', '2', '3', '4', '5', '6', '7', '8', \
         '9', '10', 'Jack', 'Queen', 'King']
  1. 创建一个名为cards的列表,其中包含标准牌组的 52 张牌:
cards = [rank + '-' + suit for rank in ranks for suit in suits]
  1. 使用np.random.choice函数从牌组中抽取一手(五张牌)。使用replace=False,以便每张牌只被选择一次:
print(np.random.choice(cards, size=5, replace=False)) 

结果应该看起来像这样(你可能会得到不同的卡片):

['Ace-clubs' '5-clubs' '7-clubs' '9-clubs' '6-clubs']
  1. 现在,创建一个名为deal_hands的函数,返回两个列表,每个列表中都有五张牌,从同一副牌中抽取。在np.random.choice函数中使用replace=False。这个函数将执行替换的抽样:
def deal_hands():
    drawn_cards = np.random.choice(cards, size=10, \
                                   replace=False)
    hand_1 = drawn_cards[:5].tolist()
    hand_2 = drawn_cards[5:].tolist()
    return hand_1, hand_2

要打印输出,请这样运行函数:

deal_hands()

你应该得到类似这样的结果:

(['9-spades', 'Ace-clubs', 'Queen-diamonds', '2-diamonds', 
  '9-diamonds'],
 ['Jack-hearts', '8-clubs', '10-clubs', '4-spades', 
  'Queen-hearts'])
  1. 创建一个名为deal_hands2的第二个函数,它与上一个函数相同,但在np.random.choice函数中使用了replace=True参数。这个函数将执行替换的抽样:
def deal_hands2():
    drawn_cards = np.random.choice(cards, size=10, \
                                   replace=True)
    hand_1 = drawn_cards[:5].tolist()
    hand_2 = drawn_cards[5:].tolist()
    return hand_1, hand_2
  1. 最后,运行以下代码:
np.random.seed(2)
deal_hands2()

结果如下:

(['Jack-hearts', '4-clubs', 'Queen-diamonds', '3-hearts', 
  '6-spades'],
 ['Jack-clubs', '5-spades', '3-clubs', 'Jack-hearts', '2-clubs'])

正如你所看到的,通过允许带替换抽样,Jack-hearts牌在两手中都被抽中,这意味着在抽取每张牌时,考虑了所有 52 张牌。

在这个练习中,我们练习了带和不带替换的抽样的概念,并学会了如何使用np.random.choice函数应用它。

注意

要访问此特定部分的源代码,请参考packt.live/2Zs7RuY

你也可以在packt.live/2Bm7A4Y上在线运行这个示例。

概率作为相对频率

让我们回到概念部分的问题:我们如何为事件分配概率?在相对频率方法下,我们所做的是重复进行实验很多次,然后将事件的概率定义为它发生的相对频率,也就是我们观察到事件发生的次数除以我们进行实验的次数:

图 8.3:计算概率的公式

图 8.3:计算概率的公式

让我们用一个实际的例子来了解这个概念。首先,我们将进行 100 万次掷骰子的实验:

np.random.seed(81)
one_million_tosses = np.random.randint(low=1, \
                                       high=7, size=int(1e6))

我们可以从数组中获取前 10 个值:

one_million_tosses[:10]

看起来是这样的:

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

记住这个实验的样本空间是 S = {1, 2, 3, 4, 5, 6}。让我们使用相对频率方法定义一些事件并为它们分配概率。首先,让我们使用一些简单的事件:

  • A:观察到数字 2

  • B:观察到数字 6

我们可以利用 NumPy 的向量化能力,通过对比操作得到的布尔向量求和来计算发生简单事件的次数:

N_A_occurs = (one_million_tosses == 2).sum()
Prob_A = N_A_occurs/one_million_tosses.shape[0]
print(f'P(A)={Prob_A}')

结果如下:

P(A)=0.16595

按照完全相同的程序,我们可以计算事件B的概率:

N_B_occurs = (one_million_tosses == 6).sum()
Prob_B = N_B_occurs/one_million_tosses.shape[0]
print(f'P(B)={Prob_B}')

结果如下:

P(B)=0.166809

现在,我们将尝试一些复合事件(它们有多个可能的结果):

  • C:观察到奇数(或{1, 3, 5})

  • D:观察到小于 5 的数字(或{1, 2, 3, 4})

因为事件观察到奇数将在我们得到 1 3 5 时发生,我们可以将我们在口语中使用的翻译成数学中的OR运算符。在 Python 中,这是|运算符:

N_odd_number = (
    (one_million_tosses == 1) | 
    (one_million_tosses == 3) | 
    (one_million_tosses == 5)).sum()
Prob_C = N_odd_number/one_million_tosses.shape[0]
print(f'P(C)={Prob_C}')

结果如下:

P(C)=0.501162

最后,让我们计算D的概率:

N_D_occurs = (one_million_tosses < 5).sum()
Prob_D = N_D_occurs/one_million_tosses.shape[0]
print(f'P(D)={Prob_D}')

我们得到以下值:

P(D)=0.666004

在这里,我们使用了相对频率方法来计算以下事件的概率:

  • A:观察到数字 2:0.16595

  • B:观察到数字 6:0.166809

  • C:观察到奇数:0.501162

  • D:观察到小于 5 的数字:0.666004

总之,在相对频率方法下,当我们有一组来自重复实验的结果时,我们用来计算事件概率的方法是计算事件发生的次数,然后将该次数除以总实验次数。就是这么简单。

在其他情况下,概率的分配可能基于定义而产生。这就是我们可能称之为理论概率的东西。例如,按定义,一个公平硬币有相等的概率显示两种结果中的任何一种,比如正面或反面。由于这个实验只有两种结果{正面,反面},并且概率必须加起来等于 1,每个简单事件必须有 0.5 的发生概率。

另一个例子如下:公平骰子是指六个数字具有相同的发生概率,因此投掷任何数字的概率必须等于1/6 = 0.1666666。事实上,numpy.randint函数的默认行为是模拟选择的整数数字,每个数字出现的概率相同。

使用理论定义,并知道我们用 NumPy 模拟了一个公平骰子,我们可以得出我们之前提到的事件的概率:

  • A:观察到数字 2,P(A) = 1/6 = 0.1666666

  • B:观察到数字 6,P(B) = 1/6 = 0.1666666

  • C:观察到奇数:P(观察到 1 观察到 3 观察到 5) = P(观察到 1) + 或 P(观察到 3) + P(观察到 5) = 1/6 + 1/6 + 1/6 = 3/6 = 0.5

  • D:观察到小于 5 的数字:P(观察到 1 观察到 2 观察到 3 观察到 4) = P(观察到 1) + 或 P(观察到 2) + P(观察到 3) + P(观察到 4) = 1/6 + 1/6 + 1/6 + 1/6 = 4/6 = 0.666666

注意这里有两件事:

  • 这些数字令人惊讶(或者如果你已经知道这个结果,那就不足为奇了)地接近我们用相对频率方法得到的结果。

  • 我们可以根据基本概率概念部分的规则 4 来分解CD的和。

定义随机变量

通常,您会发现其值是(或似乎是)随机过程的结果的数量。以下是一些例子:

  • 两个骰子的结果的和

  • 掷十枚硬币时出现的正面数

  • IBM 股票一周后的价格

  • 一个网站的访问者数量

  • 一个人一天摄入的卡路里数量

所有这些都是可以变化的数量的例子,这意味着它们是变量。此外,由于它们的值部分或完全取决于随机性,我们称它们为随机变量:其值由随机过程确定的数量。随机变量的典型表示法是大写字母,如XYZ。相应的小写字母用于指代它们的值。例如,如果X两个骰子的结果的和,以下是一些如何阅读符号的示例:

  • P(X = 10)X取值为 10 的概率

  • P(X > 5)X取大于 5 的值的概率

  • P(X = x)X取值为x的概率(当我们在做一般性陈述时)

由于X是两个骰子的和,X可以取以下值:{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}。根据我们在上一节学到的知识,我们可以这样模拟我们的随机变量X的大量值:

np.random.seed(55)
number_of_tosses = int(1e5)
die_1 = np.random.randint(1,7, size=number_of_tosses)
die_2 = np.random.randint(1,7, size=number_of_tosses)
X = die_1 + die_2

我们已经模拟了两个骰子投掷了 100,000 次,并得到了X的相应值。这些是我们向量的第一个值:

print(die_1[:10])
print(die_2[:10])
print(X[:10])

结果如下:

[6 3 1 6 6 6 6 6 4 2]
[1 2 3 5 1 3 3 6 3 1]
[7  5  4 11  7  9  9 12  7  3]

因此,在第一次模拟的投掷中,我们在第一个骰子上得到了6,在第二个骰子上得到了1,所以X的第一个值是7

就像实验一样,我们可以定义随机变量上的事件,并计算这些事件的相应概率。例如,我们可以使用相对频率定义来计算以下事件的概率:

  • X = 10X取值为 10 的概率

  • X > 5X取大于 5 的值的概率

计算这些事件的概率的计算基本上与我们之前所做的相同:

Prob_X_is_10 = (X == 10).sum()/X.shape[0]
print(f'P(X = 10) = {Prob_X_is_10}')

结果如下:

P(X = 10) = 0.08329

对于第二个事件,我们有以下内容:

Prob_X_is_gt_5 = (X > 5).sum()/X.shape[0]
print(f'P(X > 5) = {Prob_X_is_gt_5}')

结果如下:

P(X > 5) = 0.72197

我们可以使用条形图来可视化我们的模拟中每个可能值出现的次数。这将帮助我们更好地了解我们的随机变量:

X = pd.Series(X)
# counts the occurrences of each value
freq_of_X_values = X.value_counts()
freq_of_X_values.sort_index().plot(kind='bar')
plt.grid();

生成的图表如下:

图 8.4:X 的值的频率

图 8.4:X 的值的频率

我们可以看到,在 100,000 次X中,它大约 5800 次取到了值 3,大约 14000 次取到了值 6,这也非常接近值 8 出现的次数。我们还可以观察到最常见的结果是数字 7。

根据概率的相对频率定义,如果我们将频率除以X的值的数量,我们可以得到观察到X的每个值的概率:

Prob_of_X_values = freq_of_X_values/X.shape[0]
Prob_of_X_values.sort_index().plot(kind='bar')
plt.grid();

这给我们带来了以下的图表:

图 8.5:X 的值的概率分布

图 8.5:X 的值的概率分布

这个图看起来几乎和上一个一样,但在这种情况下,我们可以看到观察到X的所有可能值的概率。这就是我们所说的随机变量的概率分布(或简称分布):观察到随机变量可以取的每个值的概率。

让我们用另一个例子来说明随机变量和概率分布这两个概念。首先,我们来定义随机变量:

Y: 投掷 10 枚公平硬币时出现的正面数。

现在,我们的任务是估计概率分布。我们知道这个随机变量可以取 11 个可能的值:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}。对于这些值中的每一个,都有一个相应的概率,使得Y变量取得该值。直觉上,我们知道观察到变量的极端值是非常不可能的:得到 10 个正面(Y=10)或 10 个反面(Y=0)是非常不可能的。我们也期望Y变量大部分时间取得 4、5 和 6 这样的值。我们可以计算概率分布来验证我们的直觉。

再次,让我们模拟抛 10 枚硬币的实验。然后,我们可以观察这个随机变量的值。让我们开始模拟 1 百万次抛 10 枚公平硬币:

np.random.seed(97)
ten_coins_a_million_times = np.random.randint(0, 2, \
                                              size=int(10e6))\
                                              .reshape(-1,10) 

前面的代码将产生一个 1,000,000 x 10 的矩阵,每一行代表抛 10 枚硬币的实验。我们可以将 0 视为反面,1 视为正面。这里是前 12 行:

ten_coins_a_million_times[:12, :]

结果如下:

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

为了产生不同的Y值,我们需要将每一行相加,如下所示:

Y = ten_coins_a_million_times.sum(axis=1)

现在,我们可以使用之前计算的对象(Y)来计算某些事件的概率,例如获得零个正面的概率:

Prob_Y_is_0 = (Y == 0).sum() / Y.shape[0]
print(f'P(Y = 0) = {Prob_Y_is_0}')

输出如下:

P(Y = 0) = 0.000986

这是一个非常小的数字,与我们的直觉一致:得到 10 个反面的可能性非常小。实际上,在 100 万次实验中只发生了 986 次。

就像之前一样,我们可以绘制Y的概率分布:

Y = pd.Series(Y)
# counts the occurrences of each value
freq_of_Y_values = Y.value_counts()
Prob_of_Y_values = freq_of_Y_values/Y.shape[0]
Prob_of_Y_values.sort_index().plot(kind='bar')
plt.grid();

这是输出:

图 8.6:Y 的概率分布

图 8.6:Y 的概率分布

得到 5 个正面的概率约为 0.25,所以大约 25%的时间,我们可以期望得到 5 个正面。得到 4 或 6 个正面的机会也相对较高。得到 4、5 或 6 个正面的概率是多少?我们可以通过使用Prob_of_Y_values来轻松计算,通过添加得到 4、5 或 6 个正面的相应概率:

print(Prob_of_Y_values.loc[[4,5,6]])
print(f'P(4<=Y<=6) = {Prob_of_Y_values.loc[[4,5,6]].sum()}')

结果如下:

4    0.205283
5    0.246114
6    0.205761
dtype: float64
P(4<=Y<=6) = 0.657158

因此,大约有 2/3(约 66%)的时间,当抛 10 枚公平硬币时,我们会观察到 4、5 或 6 个正面。回到概率作为不确定性度量的定义,我们可以说,我们有 66%的信心,当抛 10 枚公平硬币时,我们会看到 4 到 6 个正面。

练习 8.02:计算轮盘赌中的平均赢数

在这个练习中,我们将学习如何使用np.random.choice来模拟真实世界的随机过程。然后,我们将利用这个模拟,计算如果我们玩很多次,平均会赢得/失去多少钱。

我们将模拟去赌场玩轮盘赌。欧洲轮盘赌包括一个球随机落在 0 到 36 之间的任何整数上,每个数字落球的机会均等。允许许多种投注方式,但我们将只以一种方式进行(相当于著名的押红色或黑色的方式)。规则如下:

  • 在 19 到 36 的数字上押注m个单位(你喜欢的货币)。

  • 如果轮盘赌的结果是所选数字中的任何一个,那么你赢得m个单位。

  • 如果轮盘赌的结果是 0 到 18 之间的任何数字(包括 18),那么你就输掉m个单位。

为了简化,让我们假设赌注是 1 单位。让我们开始吧:

  1. 导入 NumPy 库:
import numpy as np
  1. 使用np.random.choice函数编写一个名为roulette的函数,模拟欧洲轮盘赌的任意次数的游戏:
def roulette(number_of_games=1):

    # generate the Roulette numbers
    roulette_numbers = np.arange(0, 37)

    outcome = np.random.choice(a = roulette_numbers, \
                               size = number_of_games,\
                               replace = True)
    return outcome
  1. 编写一个名为payoff的函数,它编码了前面的赔付逻辑。它接收两个参数:outcome,轮盘赌的数字(0 到 36 之间的整数);以及units,默认值为 1 的赌注单位:
def payoff(outcome, units=1):
    # 1\. Bet m units on the numbers from 19 to 36
    # 2\. If the outcome of the roulette is any of the 
    #    selected numbers, then you win m units
    if outcome > 18:
        pay = units
    else:
    # 3\. If the outcome of the roulette is any number 
    #    between 0 and 18 (inclusive) then you lose m units
        pay = -units
    return pay
  1. 使用np.vectorize对函数进行矢量化,以便它也可以接受轮盘赌结果的矢量。这将允许你传递一个结果的矢量,并获得相应的赔付:
payoff = np.vectorize(payoff)
  1. 现在,模拟玩 20 次轮盘赌(押注一单位)。使用payoff函数获得结果向量:
outcomes = roulette(20)
payoffs = payoff(outcomes)
print(outcomes)
print(payoffs)

输出如下:

[29 36 11  6 11  6  1 24 30 13  0 35  7 34 30  7 36 32 12 10]
[ 1  1 -1 -1 -1 -1 -1  1  1 -1 -1  1 -1  1  1 -1  1  1 -1 -1]
  1. 模拟 100 万次轮盘赌游戏,并使用结果获得相应的赔偿。将赔偿保存在名为payoffs的向量中:
number_of_games = int(1e6)
outcomes = roulette(number_of_games)
payoffs = payoff(outcomes)
  1. 使用np.mean函数计算赔偿向量的平均值。你得到的值应该接近-0.027027:
np.mean(payoffs)

负数意味着平均每下注一单位就会损失-0.027027。请记住,你的损失就是赌场的利润。这是他们的生意。

在这个练习中,我们学会了如何使用 NumPy 的随机数生成功能来模拟真实世界的过程。我们还模拟了大量事件以获得长期平均值。

注意

要访问本节的源代码,请参阅packt.live/2AoiyGp

你也可以在packt.live/3irX6Si上在线运行这个例子。

通过这样,我们学会了如何通过分配概率来量化不确定性来理解随机事件。然后,我们定义了概率论中一些最重要的概念。我们还学会了如何使用相对频率定义为事件分配概率。此外,我们介绍了随机变量的重要概念。在计算上,我们学会了如何使用 NumPy 模拟值和样本,以及如何使用模拟来回答关于某些事件概率的问题。

根据随机变量可以取的值的类型,我们可以有两种类型:

  • 离散随机变量

  • 连续随机变量

我们将在接下来的两节中提供一些例子。

离散随机变量

在本节中,我们将继续学习和处理随机变量。我们将研究一种特定类型的随机变量:离散随机变量。这些类型的变量在各种应用领域中都会出现,如医学、教育、制造等,因此了解如何处理它们非常有用。我们将学习可能是最重要的,也是最常见的离散分布之一:二项分布。

离散随机变量的定义

离散随机变量只能取特定数量的值(技术上来说是可数数量的值)。通常,它们可以取的值是特定的整数值,尽管这并非必要。例如,如果一个随机变量可以取{1.25, 3.75, 9.15}这组值,它也被认为是离散随机变量。我们在上一节介绍的两个随机变量就是离散随机变量的例子。

假设你是一家生产汽车零部件的工厂的经理。生产零件的机器平均会有 4%的次品。我们可以将这 4%解释为生产次品的概率。这些汽车零件被包装在包含 12 个单位的箱子中,因此,原则上每个箱子可以包含从 0 到 12 个次品。假设我们不知道哪个零件是次品(直到使用时),也不知道何时会生产次品。因此,我们有一个随机变量。首先,让我们正式定义它:

Z:12 箱装中次品的数量。

作为工厂经理,你最大的客户之一问了你以下问题:

  • 有 12 个非次品零件(零次品)的箱子占百分之多少?

  • 有 3 个或更多次次品的箱子占百分之多少?

如果你知道变量的概率分布,你可以回答这两个问题,所以你问自己以下问题:

Z 的概率分布是什么样子?

为了回答这个问题,我们可以再次使用模拟。要模拟一个单独的箱子,我们可以使用np.random.choice并通过p参数提供概率:

np.random.seed(977)
np.random.choice(['defective', 'good'], \
                 size=12, p=(0.04, 0.96))

结果如下:

array(['good', 'good', 'good', 'good', 'good', 'good', 'good', \
       'defective', 'good', 'good', 'good', 'good'], dtype='<U9')

我们可以看到这个特定的盒子包含一个次品。请注意,在函数中使用的概率向量必须加起来为 1:因为观察到次品的概率为 4%(0.04),观察到好品的概率为100% – 4% = 96%(0.96),这些值被传递给 p 参数。

现在我们知道如何模拟单个盒子,为了估计我们的随机变量的分布,让我们模拟大量的盒子;100 万个已经足够了。为了使我们的计算更简单更快,让我们使用 1 和 0 来表示次品和好品。要模拟 100 万个盒子,只需将大小参数更改为一个大小为12 x 1,000,000的元组即可:

np.random.seed(10)
n_boxes = int(1e6)
parts_per_box = 12
one_million_boxes = np.random.choice\
                    ([1, 0], \
                     size=(n_boxes, parts_per_box), \
                     p=(0.04, 0.96))

前五个盒子可以使用以下公式找到:

one_million_boxes[:5,:]

输出将如下所示:

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

输出中的每个零代表一个非次品,一个代表一个次品。现在,我们计算每个盒子中有多少次品,然后我们可以计算观察到 0、1、2、...、12 个次品的次数,然后我们可以绘制 Z 的概率分布:

# count defective pieces per box
defective_pieces_per_box = one_million_boxes.sum(axis=1)
# count how many times we observed 0, 1,…, 12 defective pieces
defective_pieces_per_box = pd.Series(defective_pieces_per_box)
frequencies = defective_pieces_per_box.value_counts()
# probability distribution
probs_Z = frequencies/n_boxes

最后,让我们将其可视化:

print(probs_Z.sort_index())
probs_Z.sort_index().plot(kind='bar')
plt.grid()

输出将如下所示:

0    0.612402
1    0.306383
2    0.070584
3    0.009630
4    0.000940
5    0.000056
6    0.000004
7    0.000001

概率分布将如下所示:

图 8.7:Z 的概率分布

图 8.7:Z 的概率分布

从这个模拟中,我们可以得出结论,大约 61%的盒子将被运送到零次品,大约 30%的盒子将包含一个次品。我们还可以看到,在一个盒子中观察到三个或更多次品是非常非常不可能的。现在,你可以回答你的客户的问题了:

  • 有多少百分比的盒子有 12 个非次品? 答案:61%的盒子将包含 12 个非次品。

  • 有多少百分比的盒子有 3 个或更多次品? 答案:只有大约 1%的盒子会包含 3 个或更多次品。

二项分布

事实证明,在某些条件下,我们可以找出某些离散随机变量的确切概率分布。二项分布是适用于随机变量并满足以下三个特征的理论分布:

  • 条件 1:对于单个观察,通常表示为成功失败,只有两种可能的结果。如果成功的概率是p,那么失败的概率必须是1 – p

  • 条件 2:实验被固定次数执行,通常用n表示。

  • 条件 3:所有实验都是独立的,这意味着知道一个实验的结果不会改变下一个实验的概率。因此,成功(和失败)的概率保持不变。

如果满足这些条件,那么我们说随机变量遵循二项分布,或者随机变量是二项随机变量。我们可以使用以下公式得到二项随机变量X的确切概率分布:

图 8.8:计算 X 的概率分布的公式

图 8.8:计算 X 的概率分布的公式

从技术上讲,数学函数,它接受离散随机变量(x)的可能值并返回相应的概率被称为概率质量函数。请注意,一旦我们从前一个方程中知道了np的值,概率就只取决于x的值,因此前一个方程为二项随机变量定义了概率质量函数。

好的,这听起来和看起来非常理论化和抽象(因为它是)。然而,我们已经介绍了两个遵循二项分布的随机变量。让我们验证以下条件是否成立:

Y: 抛掷 10 枚公平硬币时出现的正面数量。

  • 条件 1:对于每个单独的硬币,只有两种可能的结果,正面反面,每种结果的固定概率为 0.5。由于我们对正面的数量感兴趣,正面可以被认为是我们的成功反面是我们的失败

  • 条件 2:硬币的数量固定为 10 枚。

  • 条件 3:每次抛硬币都是独立的:我们隐含地(逻辑上)假设一枚硬币的结果不会影响任何其他硬币的结果。

所以,我们有了在前述公式中使用的数字:

  • p = 0.5

  • n = 10

如果我们想要得到得到五个正面的概率,那么我们只需要在公式中用已知的pn替换x = 5

图 8.9:在概率分布公式中替换 x、p 和 n 的值

图 8.9:在概率分布公式中替换 x、p 和 n 的值

现在,让我们用 Python 进行这些理论计算。是时候介绍另一个 Python 模块了,我们将在本章和接下来的章节中大量使用。scipy.stats模块包含许多统计函数。其中有许多可以用来创建遵循许多常用概率分布的随机变量的函数。让我们使用这个模块来创建一个遵循理论二项分布的随机变量。首先,我们用适当的参数实例化随机变量:

import scipy.stats as stats
Y_rv = stats.binom(
    n = 10, # number of coins
    p = 0.5 # probability of heads (success)
)

创建后,我们可以使用该对象的pmf方法来计算Y可以取的每个可能值的精确理论概率。首先,让我们创建一个包含Y可以取的所有值(从 0 到 10 的整数)的向量:

y_values = np.arange(0, 11)

现在,我们可以简单地使用pmf(代表概率质量函数)方法来获得前述值的相应概率:

Y_probs = Y_rv.pmf(y_values) 

我们可以像这样可视化我们得到的pmf

fig, ax = plt.subplots()
ax.bar(y_values, Y_probs)
ax.set_xticks(y_values)
ax.grid()

我们得到的输出如下:

图 8.10:Y 的 pmf

图 8.10:Y 的 pmf

这看起来与我们使用模拟得到的结果非常相似。现在,让我们比较两个图。我们将创建一个 DataFrame 来使绘图过程更容易:

Y_rv_df = pd.DataFrame({'Y_simulated_pmf': Prob_of_Y_values,\
                        'Y_theoretical_pmf':  Y_probs},\
                        index=y_values)
Y_rv_df.plot(kind='bar')
plt.grid();

输出如下:

图 8.11:Y 的 pmf 与模拟结果

图 8.11:Y 的 pmf 与模拟结果

这两组柱状图几乎是相同的;我们从模拟中得到的概率非常接近理论值。这显示了模拟的力量。

练习 8.03:检查随机变量是否符合二项分布

在这个练习中,我们将练习如何验证随机变量是否符合二项分布。我们还将使用scipy.stats创建一个随机变量并绘制分布。这将是一个大部分概念性的练习。

在这里,我们将检查随机变量Z:12 盒装中有缺陷汽车零件的数量是否符合二项分布(记住我们认为 4%的汽车零件有缺陷)。按照以下步骤完成这个练习:

  1. 按照通常的惯例导入 NumPy、Matplotlib 和scipy.stats
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
%matplotlib inline
  1. 就像我们在定义离散随机变量部分所做的那样,尝试概念性地检查Z是否满足二项随机变量的三个特征:

a. 条件 1:对于每个单独的汽车零件,只有两种可能的结果,有缺陷好的。由于我们对有缺陷零件感兴趣,那么这个结果可以被认为是成功,成功的固定概率为 0.04(4%)。

b. 条件 2:每盒的零件数量固定为 12,因此实验在每盒中进行了固定次数。

c. 条件 3:我们假设有缺陷的零件之间没有关系,因为机器随机产生平均 4%的有缺陷零件。

  1. 确定该变量的分布的pn参数,即p = 0.04n = 12

  2. 使用前面的参数使用理论公式来获得每箱恰好有一个次品的精确理论概率(使用x = 1):图 8.12:用概率分布公式替换 x、p 和 n 的值

图 8.12:用概率分布公式替换 x、p 和 n 的值

  1. 使用scipy.stats模块生成Z随机变量的实例。将其命名为Z_rv
# number of parts per box
parts_per_box = 12
Z_rv = stats.binom\
       (n = parts_per_box,\
        p = 0.04 # probability of defective piece (success)
        )
  1. 绘制Z的概率质量函数:
z_possible_values = np.arange(0, parts_per_box + 1)
Z_probs = Z_rv.pmf(z_possible_values)
fig, ax = plt.subplots()
ax.bar(z_possible_values, Z_probs)
ax.set_xticks(z_possible_values)
ax.grid();

结果如下:

图 8.13:Z 的 pmf

图 8.13:Z 的 pmf

在这个练习中,我们学习了如何检查离散随机变量具有二项分布所需的三个条件。我们得出结论,我们分析的变量确实具有二项分布。我们还能够计算其参数并使用它们创建一个二项随机变量,并绘制了分布。

注意

要访问此特定部分的源代码,请参阅packt.live/3gbTm5k

您也可以在packt.live/2Anhx1k上在线运行此示例。

在本节中,我们专注于离散随机变量。现在,我们知道它们是可以取特定数值的随机变量的一种。通常,这些是整数值。通常,这些类型的变量与计数有关:将通过考试的学生人数,穿过桥的汽车数量等。我们还了解了离散随机变量的最重要的分布,称为二项分布,以及如何使用 Python 获得二项随机变量的精确理论概率。

在下一节中,我们将专注于连续随机变量。

连续随机变量

在本节中,我们将继续处理随机变量。在这里,我们将讨论连续随机变量。我们将学习连续和离散概率分布之间的关键区别。此外,我们将介绍所有分布的鼻祖:著名的正态分布。我们将学习如何使用scipy.stats处理这种分布,并回顾其最重要的特征。

定义连续随机变量

在原则上,有些随机量可以在一个区间内取任意实数值。一些例子如下:

  • 一周后 IBM 股票的价格

  • 一个人一天摄入的卡路里数量

  • 英镑和欧元之间的收盘汇率

  • 特定群体中随机选择的男性的身高

由于它们的性质,这些变量被称为连续随机变量。与离散随机变量一样,有许多理论分布可以用来模拟现实世界的现象。

为了介绍这种类型的随机变量,让我们看一个我们已经熟悉的例子。再次加载我们在第七章《使用 Python 进行基本统计》中介绍的游戏数据集:

games = pd.read_csv('./data/appstore_games.csv')
original_colums_dict = {x: x.lower().replace(' ','_') \
                        for x in games.columns}
# renaming columns
games.rename(columns = original_colums_dict, inplace = True)

数据集中的一个变量是游戏大小(以字节为单位)。在可视化此变量的分布之前,我们将把它转换为兆字节:

games['size'] = games['size']/(1e6)
# replacing the one missing value with the median
games['size'] = games['size'].fillna(games['size'].median())
games['size'].hist(bins = 50, ec='k');

输出如下:

图 8.14:游戏大小的分布

图 8.14:游戏大小的分布

让我们定义我们的随机变量X如下:

X:从应用商店随机选择的策略游戏的大小。

定义了这个随机变量后,我们可以开始询问关于某些事件的概率的问题:

  • P(X > 100)X严格大于 100MB 的概率

  • P(100 ≤ X ≤ 400)X在 100 和 400MB 之间的概率

  • P(X = 152.53)X恰好为 152.53MB 的概率

到目前为止,您知道如何使用概率的相对频率定义来估计这些概率:计算事件发生的次数,然后除以总事件数(在这种情况下是游戏数):

# get the number of games to use as denominator
number_of_games = games['size'].size
# calculate probabilities
prob_X_gt_100 = (games['size'] > 100).sum()/number_of_games
prob_X_bt_100_and_400 = ((games['size'] >= 100) & \
                         (games['size'] <= 400))\
                         .sum()/number_of_games
prob_X_eq_152_53 = (games['size'] == 152.53).sum()/number_of_games
# print the results
print(f'P(X > 100) = {prob_X_gt_100:0.5f}')
print(f'P(100 <= X <= 400) = {prob_X_bt_100_and_400:0.5f}')
print(f'P(X = 152.53) = {prob_X_eq_152_53:0.5f}')

结果如下:

P(X > 100) = 0.33098
P(100 <= X <= 400) = 0.28306
P(X = 152.53) = 0.00000

注意我们计算的最后一个概率,P(X = 152.53)。随机变量取特定值(如 152.53)的概率是零。对于任何连续随机变量,这总是如此。由于这些类型的变量原则上可以取无限多个值,因此取确切特定值的概率必须为零。

前面的例子表明,当我们有足够多关于连续随机变量的数据点时,我们可以使用数据来估计随机变量在某些区间内取值的概率。然而,关于一个变量有大量观察结果的情况可能并非总是如此。鉴于这一事实,让我们考虑以下问题:

  • 如果我们根本没有数据呢?

  • 如果我们没有足够的数据呢?

  • 我们能否进行模拟来估计某些事件的概率(就像我们对离散随机变量所做的那样)?

这些都是合理的问题,我们可以通过更多了解理论连续概率分布来回答它们:

  • 如果我们根本没有数据呢?我们可以对变量做出一些合理的假设,然后使用众多理论连续概率分布之一对其进行建模。

  • 如果我们没有足够的数据呢?我们可以对变量做出一些合理的假设,用数据支持这些假设,并使用估计技术(下一章的主题)来估计所选择的理论连续概率分布的参数。

  • 我们能否进行模拟来估计某些事件的概率(就像我们对离散随机变量所做的那样)?可以。一旦我们选择了概率分布以及其参数,我们可以使用模拟来回答复杂的问题。

为了澄清前面的答案,在接下来的小节中,我们将介绍最重要的连续概率分布:正态分布。

值得注意的是,对于连续随机变量,概率分布也被称为概率密度函数pdf

正态分布

让我们介绍概率论中最著名和重要的分布:正态分布。正态分布的概率密度函数由以下方程定义:

图 8.15:正态分布的概率密度函数

图 8.15:正态分布的概率密度函数

在这里,πe是众所周知的数学常数。不要试图理解方程;您需要知道的只有两件事:首先,当我们有两个参数时,分布就完全确定了:

  • µ:分布的均值

  • σ:分布的标准差

其次,如果X是一个遵循正态分布的随机变量,那么对于可能的值x,前面的公式将给出一个与变量接近 x的概率直接相关的值。与二项分布的公式不同,我们通过直接将值x代入公式来得到概率,在连续随机变量的情况下,情况不同:公式给出的值没有直接解释。下面的例子将澄清这一点。

我们将使用scipy.stats模块创建一个遵循正态分布的随机变量。假设某一男性人群的身高服从均值为 170 厘米,标准差为 10 厘米的正态分布。要使用scipy.stats创建这个随机变量,我们需要使用以下代码:

# set the mu and sigma parameters of the distribution
heights_mean = 170
heights_sd = 10
# instantiate the random variable object
heights_rv = stats.norm(
        loc = heights_mean, # mean of the distribution
        scale = heights_sd  # standard deviation
)

前面的代码创建了正态分布的随机变量,其概率密度函数如下所示:

图 8.16:正态分布随机变量的概率密度函数

图 8.16:正态分布随机变量的概率密度函数

对于每个值x,比如175,我们可以使用pdf方法得到 pdf 的值,就像这样:

heights_rv.pdf(175)

结果如下:

0.03520653267642

这个数字是如果你在前面的公式中用175替换x会得到的:

图 8.17 替换 x=175 的值

图 8.17 替换 x=175 的值

要明确,这不是观察到一个身高为 175 厘米的男性的概率(请记住,这个变量取特定值的概率应该是零),因为这个数字没有简单的直接解释。但是,如果我们绘制整个密度曲线,那么我们就可以开始理解我们的随机变量的分布。要绘制整个概率密度函数,我们必须创建一个包含该变量可能取的一系列可能值的向量。根据男性身高的背景,假设我们想要绘制介于 130 厘米和 210 厘米之间的值的 pdf,这些是健康成年男性的可能值。首先,我们使用np.linspace创建值的向量,在这种情况下,它将在 120 和 210 之间(包括这两个值)创建 200 个等间距的数字:

values = np.linspace(130, 210, num=200)

现在,我们可以生成 pdf 并将其与创建的值绘制在一起:

heights_rv_pdf = heights_rv.pdf(values)
plt.plot(values, heights_rv_pdf)
plt.grid();

曲线看起来是这样的:

图 8.18:均值=170,标准差=10 的正态分布示例

图 8.18:均值=170,标准差=10 的正态分布示例

曲线越高,观察到与相应x轴数值周围的值的可能性就越大。例如,我们可以看到,我们更有可能观察到男性身高在 160 厘米到 170 厘米之间,而不是在 140 厘米到 150 厘米之间。

现在我们已经定义了这个正态分布的随机变量,我们可以使用模拟来回答关于它的某些问题吗?当然可以。事实上,现在,我们将学习如何使用已定义的随机变量来模拟样本值。我们可以使用rvs方法来生成这些值,该方法从概率分布中生成随机样本:

sample_heighs = heights_rv.rvs\
                (size = 5, \
                 random_state = 998 # similar to np.seed)
for i, h in enumerate(sample_heighs):
    print(f'Men {i + 1} height: {h:0.1f}')

结果如下:

Men 1 height: 171.2
Men 2 height: 173.3
Men 3 height: 157.1
Men 4 height: 164.9
Men 5 height: 179.1

在这里,我们正在模拟从人口中随机抽取五名男性并测量他们的身高。请注意,我们使用了random_state参数,它起到了与numpy.seed类似的作用:确保运行相同代码的人会得到相同的随机值。

与之前一样,我们可以使用模拟来回答与这个随机变量相关的事件的概率问题。例如,找到一个身高超过 190 厘米的男性的概率是多少?以下代码使用我们之前定义的随机变量计算了这个模拟:

# size of the simulation
sim_size = int(1e5)
# simulate the random samples
sample_heights = heights_rv.rvs\
                 (size = sim_size,\
                  random_state = 88 # similar to np.seed)
Prob_event = (sample_heights > 190).sum()/sim_size
print(f'Probability of a male > 190 cm: {Prob_event:0.5f} \
 (or {100*Prob_event:0.2f}%)')

结果是:

Probability of a male > 190 cm: 0.02303 (or 2.30%)

正如我们将在下一节中看到的,有一种方法可以从density函数中获得精确的概率,而无需模拟值,这有时可能是计算昂贵且不必要的。

正态分布的一些特性

宇宙和数学的一个令人印象深刻的事实是,现实世界中许多变量都遵循正态分布:

  • 人类身高

  • 大多数哺乳动物物种的体重

  • 标准化测试的分数

  • 制造过程中与产品规格的偏差

  • 诸如舒张压、胆固醇和睡眠时间等医学测量

  • 诸如某些证券的回报率等金融变量

正态分布描述了许多现象,并且在概率和统计学中被广泛使用,因此了解两个关键属性是值得的:

  • 正态分布完全由其两个参数确定:均值和标准差。

  • 正态分布的经验法则告诉我们,根据标准偏差的数量,我们将找到多少比例的观察值。

让我们了解这两个关键特性。首先,我们将说明分布的参数如何决定其形状:

  • 平均值决定了分布的中心。

  • 标准差决定了分布的宽度(或扩散程度)。

为了说明这一特性,假设我们有以下三个男性身高的人口。每个人口对应一个不同的国家:

  • 国家 A:平均值=170 厘米,标准差=10 厘米

  • 国家 B:平均值=170 厘米,标准差=5 厘米

  • 国家 C:平均值=175 厘米,标准差=10 厘米

有了这些参数,我们可以可视化和对比三个不同国家的分布。在可视化之前,让我们创建随机变量:

# parameters of distributions
heights_means = [170, 170, 175]
heights_sds = [10, 5, 10]
countries = ['Country A', 'Country B', 'Country C']
heights_rvs = {}
plotting_values = {}
# creating the random variables
for i, country in enumerate(countries):
    heights_rvs[country] = stats.norm(
        loc = heights_means[i], # mean of the distribution
        scale = heights_sds[i]  # standard deviation
    )

有了这些创建的对象,我们可以进行可视化:

# getting x and y values for plotting the distributions
for i, country in enumerate(countries):
    x_values = np.linspace(heights_means[i] - 4*heights_sds[i], \
                           heights_means[i] + 4*heights_sds[i])
    y_values = heights_rvs[country].pdf(x_values)
    plotting_values[country] = (x_values, y_values)

# plotting the three distributions
fig, ax = plt.subplots(figsize = (8, 4))
for i, country in enumerate(countries):
    ax.plot(plotting_values[country][0], \
            plotting_values[country][1], \
            label=country, lw = 2)

ax.set_xticks(np.arange(130, 220, 5))
plt.legend()
plt.grid();

这个情节看起来是这样的:

图 8.19:不同参数的正态分布比较

图 8.19:不同参数的正态分布比较

尽管国家 A国家 B的人口具有相同的平均值(170 厘米),但标准差的差异意味着国家 B的分布更集中在 170 厘米附近。我们可以说这个国家的男性身高更加均匀。国家 A国家 C的曲线基本上是一样的;唯一的区别是国家 C的曲线向右偏移了 5 厘米,这意味着在国家 C比在国家 A国家 B更有可能找到身高在 190 厘米及以上的男性(在x=190及以上,绿色曲线的y轴值大于其他两个曲线)。

正态分布的第二个重要特征被称为经验法则。让我们拿我们的例子来说明,男性身高的人口服从正态分布,平均身高为 170 厘米,标准差为 10 厘米:

  • 约 68%的观察结果将落在区间内:平均值±1 个标准差。对于男性的身高,我们会发现大约 68%的男性身高在 160 厘米和 180 厘米(170±10)之间。

  • 约 95%的观察结果将落在区间内:平均值±2 个标准差。对于男性的身高,我们会发现大约 95%的男性身高在 150 厘米和 190 厘米(170±20)之间。

  • 超过 99%的观察结果将落在区间内:平均值±3 个标准差。几乎所有的观察结果将在距离平均值不到三个标准差的距离内。对于男性的身高,我们会发现大约 99.7%的男性身高在 150 厘米和 200 厘米(170±30)之间。

经验法则可以快速地让我们了解当我们考虑到离平均值几个标准差时,我们期望看到的观察比例。

为了完成本节和本章,你应该知道关于任何连续随机变量的一个非常重要的事实,那就是概率分布下的面积将给出变量在某个范围内的概率。让我们用正态分布来说明这一点,并将其与经验法则联系起来。假设我们有一个正态分布的随机变量,平均值=170标准差=10。在x=160x=180之间(离平均值一个标准差),概率分布下的面积是多少?经验法则告诉我们 68%的观察结果将落在这个区间内,所以我们期望a,这将对应于曲线下的面积在区间[160, 180]内。我们可以用 matplotlib 可视化这个图。生成图的代码有点长,所以我们将把它分成两部分。首先,我们将创建绘图函数,确定x轴上的限制,并定义要绘制的向量:

from matplotlib.patches import Polygon
def func(x):
    return heights_rv.pdf(x)
lower_lim = 160
upper_lim = 180
x = np.linspace(130, 210)
y = func(x)

现在,我们将创建一个有阴影区域的图:

fig, ax = plt.subplots(figsize=(8,4))
ax.plot(x, y, 'blue', linewidth=2)
ax.set_ylim(bottom=0)
# Make the shaded region
ix = np.linspace(lower_lim, upper_lim)
iy = func(ix)
verts = [(lower_lim, 0), *zip(ix, iy), (upper_lim, 0)]
poly = Polygon(verts, facecolor='0.9', edgecolor='0.5')
ax.add_patch(poly)
ax.text(0.5 * (lower_lim + upper_lim), 0.01, \
        r"$\int_{160}^{180} f(x)\mathrm{d}x$", \
        horizontalalignment='center', fontsize=15)
fig.text(0.85, 0.05, '$height$')
fig.text(0.08, 0.85, '$f(x)$')
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.xaxis.set_ticks_position('bottom')
ax.set_xticks((lower_lim, upper_lim))
ax.set_xticklabels(('$160$', '$180$'))
ax.set_yticks([]);

输出将如下所示:

图 8.20:概率密度函数下的面积作为事件的概率

图 8.20:pdf 下的面积作为事件的概率

我们如何计算将给我们曲线下面积的积分?scipy.stats模块将使这变得非常容易。通过随机变量的cdf累积分布函数)方法,本质上是 pdf 的积分,我们可以通过减去下限和上限来轻松评估积分(记住微积分的基本定理):

# limits of the integral
lower_lim = 160
upper_lim = 180
# calculating the area under the curve
Prob_X_in_160_180 = heights_rv.cdf(upper_lim) - \
                    heights_rv.cdf(lower_lim)
# print the result
print(f'Prob(160 <= X <= 180) = {Prob_X_in_160_180:0.4f}')

结果如下:

Prob(160 <= X <= 180) = 0.6827

这就是我们如何从pdf中获得概率,而无需进行模拟。让我们看最后一个例子,通过与之前的结果联系起来,来澄清这一点。几页前,对于同一人群,我们问道,找到一个身高超过 190 厘米的男性的概率是多少?我们通过进行模拟得到了答案。现在,我们可以这样获得确切的概率:

# limits of the integral
lower_lim = 190
upper_lim = np.Inf # since we are asking X > 190
# calculating the area under the curve
Prob_X_gt_190 = heights_rv.cdf(upper_lim) - \
                heights_rv.cdf(lower_lim)
# print the result
print(f'Probability of a male > 190 cm: {Prob_X_gt_190:0.5f} \
      (or {100*Prob_X_gt_190:0.2f}%)')

结果如下:

Probability of a male > 190 cm: 0.02275 (or 2.28%)

如果您将此与我们之前得到的结果进行比较,您会发现它几乎是一样的。然而,这种方法更好,因为它是精确的,不需要我们进行任何计算密集或占用内存的模拟。

练习 8.04:在教育中使用正态分布

在这个练习中,我们将使用scipy.stats中的正态分布对象和cdf及其逆ppf来回答教育问题。

在心理测量学和教育领域,一个众所周知的事实是,许多与教育政策相关的变量都呈正态分布。例如,标准化数学测试的分数遵循正态分布。在这个练习中,我们将探讨这一现象:在某个国家,高中学生参加一项标准化数学测试,其分数遵循以下参数的正态分布:平均值=100标准差=15。按照以下步骤完成这个练习:

  1. 按照通常的惯例导入 NumPy、Matplotlib 和scipy.stats
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
%matplotlib inline
  1. 使用scipy.stats模块生成一个名为X_rv的正态分布随机变量实例,其平均值=100标准差=15
# producing the normal distribution
X_mean = 100
X_sd = 15
# create the random variable
X_rv = stats.norm(loc = X_mean, scale = X_sd)
  1. 绘制X的概率分布:
x_values = np.linspace(X_mean - 4 * X_sd, X_mean + 4 * X_sd)
y_values = X_rv.pdf(x_values)
plt.plot(x_values, y_values, lw=2)
plt.grid();

输出将如下:

图 8.21:测试分数的概率分布

图 8.21:测试分数的概率分布

  1. 教育部决定,被认为在数学上胜任的人的最低分数是 80。使用cdf方法计算将获得高于该分数的学生的比例:
Prob_X_gt_80 = X_rv.cdf(np.Inf) - X_rv.cdf(80)
print(f'Prob(X >= 80): {Prob_X_gt_80:0.5f} \
(or {100*Prob_X_gt_80:0.2f}%)')

结果如下:

Prob(X >= 80): 0.90879 (or 90.88%)

大约 91%的学生在数学上被认为是胜任的。

  1. 一个非常严格的大学希望为被录取到他们的项目的高中生设定非常高的标准。该大学的政策是只录取人口中数学分数处于前 2%的学生。使用ppf方法(本质上是cdf方法的逆函数)并使用参数1-0.02=0.98来获得录取的分数线:
proportion_of_admitted = 0.02
cut_off = X_rv.ppf(1-proportion_of_admitted)
print(f'To admit the top {100*proportion_of_admitted:0.0f}%, \
the cut-off score should be {cut_off:0.1f}')
top_percents = np.arange(0.9, 1, 0.01)

结果应该如下:

To admit the top 2%, the cut-off score should be 130.8

在这个练习中,我们使用了正态分布和cdfppf方法来回答关于教育政策的现实问题。

注意

要访问此特定部分的源代码,请参阅packt.live/3eUizB4

您也可以在packt.live/2VFyF9X上在线运行此示例。

在本节中,我们学习了关于连续随机变量的知识,以及这些类型变量的最重要的分布:正态分布。本节的关键是,连续随机变量由其概率密度函数确定,而概率密度函数又由其参数确定。在正态分布的情况下,它的两个参数是平均值和标准差。我们使用一个例子来演示这些参数如何影响分布的形状。

另一个重要的收获是,你可以使用概率密度函数下方的面积来计算某些事件的概率。这对于任何连续随机变量都是成立的,当然也包括遵循正态分布的随机变量。

最后,我们还学习了正态分布的经验法则,这是一个很好的经验法则,如果你想快速了解分布均值附近k个标准差处的值的比例。

现在你已经熟悉了这个重要的分布,我们将在下一章中继续使用它,当我们再次在中心极限定理的背景下遇到它时。

活动 8.01:在金融中使用正态分布

在这个活动中,我们将探讨使用正态分布来理解股票价格的每日收益的可能性。在活动结束时,你应该对正态分布是否适合描述股票的每日收益有自己的看法。

在这个例子中,我们将使用 Yahoo! Finance 提供的有关微软股票的每日信息。按照以下步骤完成此活动:

注意

完成此活动所需的数据集可以在packt.live/3imSZqr找到。

  1. 使用 pandas 从data文件夹中读取名为MSFT.csv的 CSV 文件。

  2. 可选地,重命名列,使它们易于处理。

  3. date列转换为适当的datetime列。

  4. date列设置为 DataFrame 的索引。

  5. 在金融中,股票的每日收益被定义为每日收盘价的百分比变化。通过计算adj close列的百分比变化,创建 MSFT DataFrame 中的returns列。使用pct_change系列 pandas 方法来实现。

  6. 将分析期限限制在2014-01-012018-12-31(包括在内)之间的日期。

  7. 使用直方图可视化returns列的分布,使用 40 个箱子。它看起来像正态分布吗?

输出应该如下所示:

图 8.22:微软股票收益的直方图

图 8.22:微软股票收益的直方图

  1. 计算returns列的描述统计信息:
count    1258.000000
mean        0.000996
std         0.014591
min        -0.092534
25%        -0.005956
50%         0.000651
75%         0.007830
max         0.104522
Name: returns, dtype: float64
  1. 创建一个名为R_rv的随机变量,它将代表微软股票的每日收益。使用收益列的平均值和标准差作为该分布的参数。

  2. 绘制R_rv的分布和实际数据的直方图。然后,使用plt.hist()函数和density=True参数,使真实数据和理论分布以相同的比例出现:图 8.23:微软股票收益的直方图

图 8.23:微软股票收益的直方图

  1. 在查看前面的图后,你会说正态分布是否为微软股票的每日收益提供了准确的模型?

  2. 额外活动:使用包含宝洁公司股票信息的PG.csv文件重复前面的步骤。

这个活动是关于观察现实世界的数据,并尝试使用理论分布来描述它。这很重要,因为通过拥有一个理论模型,我们可以利用其已知的特性得出现实世界的结论和影响。例如,你可以使用经验法则来描述公司的每日收益,或者你可以计算一天内损失一定金额的概率。

注意

此活动的解决方案可以在第 684 页找到。

总结

本章简要介绍了概率论的数学分支。

我们定义了概率的概念,以及一些最重要的规则和相关概念,如实验、样本空间和事件。我们还定义了随机变量这一非常重要的概念,并举例说明了两种主要的离散和连续随机变量。在本章的后面,我们学习了如何使用scipy.stats模块创建随机变量,我们还用它来生成概率质量函数和概率密度函数。我们还讨论了宇宙中两个最重要的随机变量:正态分布和二项分布。这些在许多应用领域被用来解决现实世界的问题。

当然,这只是对这个主题的简要介绍,目标是向您介绍并让您熟悉概率论中一些基本和基础概念,特别是那些对理解和使用推断统计学至关重要的概念,而这将是下一章的主题。

WUE84

JNP97

posted @ 2024-04-18 12:00  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报