用 Plotly 掷骰子

在本节中,我们将使用 Plotly 制作交互式可视化效果。在创建将在浏览器中显示的可视化效果时,Plotly 尤其有用,因为可视化效果会自动缩放以适应浏览者的屏幕。这些可视化还具有交互性;当用户将鼠标悬停在屏幕上的某些元素上时,这些元素的相关信息就会突出显示。我们将使用 Plotly Express(Plotly 的一个子集,专注于用尽可能少的代码生成图表)编写几行代码,就能完成初始可视化。一旦我们知道我们的绘图是正确的,我们就会像使用 Matplotlib 一样定制输出。

在这个项目中,我们将分析掷骰子的结果。当你掷一颗普通的六面骰子时,你有相同的机会掷出 1 到 6 中的任何一个数字。但是,当您使用两个骰子时,您掷出某些数字的几率会比其他数字大。我们将生成一个表示掷骰子的数据集,尝试确定哪些数字最有可能出现。然后,我们将绘制大量掷骰子的结果,以确定哪些结果比其他结果更有可能出现。

这项工作有助于模拟涉及骰子的游戏,但其核心思想也适用于涉及任何机会的游戏,如纸牌游戏。它还与现实世界中随机性起重要作用的许多情况有关。

安装 Plotly

使用 pip 安装 Plotly,就像安装 Matplotlib 一样:

$ python -m pip install --user plotly
$ python -m pip install --user pandas

Plotly Express 依赖于 pandas,这是一个用于高效处理数据的库,因此我们也需要安装它。如果你在安装 Matplotlib 时使用了 python3 或其他语言,请确保在此处使用相同的命令。

要了解 Plotly 可以实现哪些可视化,请访问 https://plotly.com/python 上的图表类型图库。每个示例都包含源代码,因此你可以看到 Plotly 是如何生成可视化效果的。

创建 Die 类

我们将创建下面的骰子类来模拟掷骰子:

die.py
from random import randint

class Die:
    """A class representing a single die."""

    def __init__(self, num_sides=6): (1)
        """Assume a six-sided die."""
        self.num_sides = num_sides

    def roll(self):
        """"Return a random value between 1 and number of sides."""
        return randint(1, self.num_sides) (2)

__init__() 方法需要一个可选参数❶。通过 Die 类,当我们创建一个骰子实例时,如果不包含参数,边数将是 6。如果包含参数,该参数值将设置骰子的边数。(骰子根据其边数命名:六面骰为 D6,八面骰为 D8,以此类推)。

roll() 方法使用 randint() 函数返回一个介于 1 和边数 ❷ 之间的随机数。该函数可以返回起始值(1)、终止值(num_sides)或两者之间的任意整数。

掷骰子

在创建基于 "骰子" 类的可视化之前,让我们先掷一次 D6,打印结果,并检查结果是否看起来合理:

die_visual.py
from die import Die

# Create a D6.
die = Die()

# Make some rolls, and store results in a list.
results = []
for roll_num in range(100):
    result = die.roll()
    results.append(result)

print(results)

我们创建一个默认为六面的 Die 实例❶。然后我们掷骰子 100 次❷,并将每次掷骰子的 results 存储在结果列表中。下面是一组结果示例:

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

对这些结果的快速扫描显示,Die 类似乎在工作。我们看到了 1 和 6 的值,因此我们知道返回的是可能的最小值和最大值,而且因为我们没有看到 0 或 7,所以我们知道所有结果都在适当的范围内。我们还看到了从 1 到 6 的每个数字,这表明所有可能的结果都得到了体现。让我们来确定每个数字到底出现了多少次。

分析结果

我们将通过计算掷每个数字的次数来分析掷一个 D6 的结果:

die_visual.py
--snip--
# Make some rolls, and store results in a list.
results = []
for roll_num in range(1000):
    result = die.roll()
    results.append(result)

# Analyze the results.
frequencies = []
poss_results = range(1, die.num_sides+1)
for value in poss_results:
    frequency = results.count(value)
    frequencies.append(frequency)

print(frequencies)

由于不再打印结果,我们可以将模拟掷骰子的次数增加到 1000 ❶。为了分析掷骰子的结果,我们要创建一个空列表频率来存储每个值的掷骰子次数。然后,我们会生成所有可能得到的结果;在本例中,就是骰子从 1 到多少面❷ 的所有数字。我们在可能的数值中循环,计算每个数字在结果❸中出现的次数,然后将这个数值追加到频率❹中。在进行可视化之前,我们先打印这个列表:

[155, 167, 168, 170, 159, 181]

这些结果看起来很合理:我们看到六个频率,掷 D6 时每个可能的数字都有一个频率。我们还可以看到,没有哪个频率明显高于其他频率。现在,让我们将这些结果形象化。

制作直方图

现在我们有了想要的数据,只需几行代码就能使用 Plotly Express 生成可视化效果:

die_visual.py
import plotly.express as px
from die import Die
--snip--

for value in poss_results:
    frequency = results.count(value)
    frequencies.append(frequency)

# Visualize the results.
fig = px.bar(x=poss_results, y=frequencies)
fig.show()

首先,我们使用传统的别名 px 导入 plotly.express 模块。然后,我们使用 px.bar() 函数创建条形图。在该函数的最简单使用中,我们只需传递一组 x 值和一组 y 值。这里的 x 值是掷一次骰子的可能结果,y 值是每种可能结果的频率。

最后一行调用 fig.show(),告诉 Plotly 将生成的图表渲染为 HTML 文件,并在新的浏览器标签页中打开该文件。结果如图 15-12 所示。

这只是一个非常简单的图表,当然并不完整。但这正是 Plotly Express 的使用方法:编写几行代码,查看图表,确保它按照你想要的方式表示数据。如果你喜欢你所看到的,你可以继续自定义图表元素,如标签和样式。但是,如果您想探索其他可能的图表类型,现在就可以进行,而无需在定制工作上花费额外的时间。现在就把 px.bar() 改为 px.scatter()px.line() 来试试吧。有关可用图表类型的完整列表,请访问 https://plotly.com/python/plotly-express。

该图表是动态和交互式的。如果改变浏览器窗口的大小,图表将根据可用空间调整大小。如果将鼠标悬停在任何一个柱形图上,就会出现一个弹出窗口,突出显示与该柱形图相关的具体数据。

image 2023 12 04 18 18 33 432
Figure 1. Figure 15-12: The initial plot produced by Plotly Express

自定义绘图

现在我们知道我们拥有了正确的绘图类型,数据也得到了准确的表达,我们可以集中精力为图表添加适当的标签和样式了。

使用 Plotly 自定义图表的第一种方法是在生成图表的初始调用中使用一些可选参数,在本例中就是 px.bar()。下面介绍如何为每个轴添加总标题和标签:

die_visual.py
--snip--
# Visualize the results.
title = "Results of Rolling One D6 1,000 Times" (1)
labels = {'x': 'Result', 'y': 'Frequency of Result'} (2)
fig = px.bar(x=poss_results, y=frequencies, title=title, labels=labels)
fig.show()

我们首先要定义我们想要的标题,这里指定为 title ❶。为了定义轴标签,我们编写了一个 dictionary ❷。字典中的键指的是我们要自定义的标签,而值则是我们要使用的自定义标签。在这里,我们给 x 轴的标签是 "结果",给 y 轴的标签是 "结果的频率"。现在对 px.bar() 的调用包含了标题和标签这两个可选参数。

如图 15-13 所示,现在生成的曲线图包含了适当的标题和每个轴的标签。

image 2023 12 04 18 21 04 905
Figure 2. Figure 15-13: A simple bar chart created with Plotly

掷两个骰子

掷两颗骰子的结果数字更大,结果的分布也不同。让我们修改代码,创建两个 D6 骰子来模拟掷一对骰子的方式。每次掷出一对骰子时,我们会将两个数字相加(每个骰子一个数字),并将总和存储在结果中。将 die_visual.py 保存为 dice_visual.py,并进行以下修改:

dice_visual.py
import plotly.express as px
from die import Die

# Create two D6 dice.
die_1 = Die()
die_2 = Die()

# Make some rolls, and store results in a list.
results = []
for roll_num in range(1000):
    result = die_1.roll() + die_2.roll() (1)
    results.append(result)

# Analyze the results.
frequencies = []
max_result = die_1.num_sides + die_2.num_sides (2)
poss_results = range(2, max_result+1) (3)
for value in poss_results:
    frequency = results.count(value)
    frequencies.append(frequency)

# Visualize the results.
title = "Results of Rolling Two D6 Dice 1,000 Times"
labels = {'x': 'Result', 'y': 'Frequency of Result'}
fig = px.bar(x=poss_results, y=frequencies, title=title, labels=labels)
fig.show()

创建两个骰子实例后,我们掷骰子并计算每次掷出的两个骰子之和❶。最小的可能结果(2)是每个骰子上最小数字的和。最大的可能结果(12)是每个骰子上最大数字的总和,我们将其赋值给 max_result ❷。变量 max_result 使生成 poss_results 的代码更容易阅读❸。我们本可以写 range(2,13),但这只适用于两个 D6 骰子。在模拟实际情况时,最好编写能轻松模拟各种情况的代码。这段代码允许我们模拟掷一对任意边数的骰子。

运行这段代码后,您将看到一个类似图 15-14 的图表。

image 2023 12 04 18 24 20 417
Figure 3. Figure 15-14: Simulated results of rolling two six-sided dice 1,000 times

这张图显示了当你掷一对 D6 骰子时可能得到的结果的大致分布。正如您所看到的,掷出 2 或 12 的可能性最小,而掷出 7 的可能性最大。这是因为掷出 7 有六种方式:1 和 6、2 和 5、3 和 4、4 和 3、5 和 2 以及 6 和 1。

进一步定制

在我们刚刚生成的曲线图中,有一个问题需要解决。现在有 11 个条形图,X 轴的默认设置会使一些条形图没有标注。虽然默认设置对大多数可视化效果都很好,但如果能标注所有柱形,这个图表会更好看。

Plotly 有一个 update_layout() 方法,可用于在创建图表后对其进行各种更新。下面是如何告诉 Plotly 给每个条形图加上自己的标签:

--snip--
fig = px.bar(x=poss_results, y=frequencies, title=title, labels=labels)

# Further customize chart.
fig.update_layout(xaxis_dtick=1)

fig.show()

update_layout() 方法作用于表示整个图表的 fig 对象。在这里,我们使用 xaxis_dtick 参数来指定 x 轴上刻度线之间的间距。我们将间距设为 1,这样每个柱形图都有标记。再次运行 dice_visual.py,就会在每个柱形图上看到标签。

掷不同大小的骰子

让我们制作一个六面骰子和一个十面骰子,看看掷 50000 次后会发生什么:

dice_visual_d6d10.py
import plotly.express as px
from die import Die

# Create a D6 and a D10.
die_1 = Die()
die_2 = Die(10) (1)

# Make some rolls, and store results in a list.
results = []
for roll_num in range(50_000):
    result = die_1.roll() + die_2.roll()
    results.append(result)

# Analyze the results.
--snip--

# Visualize the results.
title = "Results of Rolling a D6 and a D10 50,000 Times" (2)
labels = {'x': 'Result', 'y': 'Frequency of Result'}
--snip--

为了制作 D10,我们在创建第二个模具实例时传递参数 10 ❶,并更改第一个循环来模拟 50,000 次掷骰子而不是 1,000 次。我们还修改了图表的标题❷。

图 15-15 显示了生成的图表。最可能的结果不是一个,而是五个。出现这种情况的原因是,掷出最小值(1 和 1)和最大值(6 和 10)的方法仍然只有一种,但较小的骰子限制了产生中间数字的方法数量。掷出 7、8、9、10 或 11 有六种方法,这些都是最常见的结果,掷出其中任何一种的可能性都是一样的。

image 2023 12 04 18 28 47 046
Figure 4. Figure 15-15: The results of rolling a six-sided die and a ten-sided die 50,000 times

我们能够使用 Plotly 来模拟掷骰子的过程,这为我们探索这一现象提供了相当大的自由度。只需几分钟,您就可以使用各种骰子模拟大量的掷骰子过程。

图象存储

有了喜欢的图表后,您可以通过浏览器将其保存为 HTML 文件。不过,您也可以通过编程来实现。要将图表保存为 HTML 文件,请将对 fig.show() 的调用替换为对 fig.write_html() 的调用:

fig.write_html('dice_visual_d6d10.xhtml')

write_html() 方法需要一个参数:要写入的文件名。如果只提供文件名,文件将保存在与 .py 文件相同的目录中。您也可以使用 Path 对象调用 write_html(),然后将输出文件写入系统中的任意位置。

亲身体验

15-6. 两个 D8:模拟掷两个八面骰子 1000 次的结果。在运行模拟之前,试着想象一下你认为的可视化效果,然后看看你的直觉是否正确。逐渐增加掷骰子的次数,直到你开始发现系统能力的极限。

15-7. 三颗骰子:当您掷三颗 D6 骰子时,您能掷出的最小数字是 3,最大数字是 18。创建一个可视化界面,显示掷三颗 D6 骰子时发生的情况。

15-8. 乘法:掷两颗骰子时,通常会将两个数字相加得出结果。创建一个可视化图,显示如果将这两个数字相乘会发生什么。

15-9. 骰子理解:为了清晰起见,本节中的列表使用了 for 循环的长形式。如果你能熟练使用列表理解,可以尝试为这些程序中的一个或两个循环编写一个理解。

15-10. 练习使用这两个库:尝试使用 Matplotlib 制作掷骰子的可视化程序,并使用 Plotly 制作随机行走的可视化程序(要完成本练习,您需要查阅每个库的文档)。

总结

在本章中,您学会了生成数据集和创建数据可视化。你用 Matplotlib 创建了简单的图,并用散点图来探索随机游走。你还用 Plotly 创建了直方图,并用它来探索掷不同大小骰子的结果。

用代码生成自己的数据集是一种有趣而强大的方法,可以模拟和探索现实世界中的各种情况。当你继续完成后面的数据可视化项目时,请留意你可能可以用代码建模的情况。看看你在新闻媒体中看到的可视化,看看你是否能找出那些使用与你在这些项目中学到的方法类似的方法生成的可视化。

在第 16 章中,你将从网上下载数据,并继续使用 Matplotlib 和 Plotly 来探索这些数据。