用 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 类
我们将创建下面的骰子类来模拟掷骰子:
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,打印结果,并检查结果是否看起来合理:
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 的结果:
--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 生成可视化效果:
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。
该图表是动态和交互式的。如果改变浏览器窗口的大小,图表将根据可用空间调整大小。如果将鼠标悬停在任何一个柱形图上,就会出现一个弹出窗口,突出显示与该柱形图相关的具体数据。

自定义绘图
现在我们知道我们拥有了正确的绘图类型,数据也得到了准确的表达,我们可以集中精力为图表添加适当的标签和样式了。
使用 Plotly 自定义图表的第一种方法是在生成图表的初始调用中使用一些可选参数,在本例中就是 px.bar()
。下面介绍如何为每个轴添加总标题和标签:
--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 所示,现在生成的曲线图包含了适当的标题和每个轴的标签。

掷两个骰子
掷两颗骰子的结果数字更大,结果的分布也不同。让我们修改代码,创建两个 D6 骰子来模拟掷一对骰子的方式。每次掷出一对骰子时,我们会将两个数字相加(每个骰子一个数字),并将总和存储在结果中。将 die_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 的图表。

这张图显示了当你掷一对 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 次后会发生什么:
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 有六种方法,这些都是最常见的结果,掷出其中任何一种的可能性都是一样的。

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