高尔顿板的介绍
高尔顿板(Galton Board),有时也称为贝尔图(Bean Machine),是由英国统计学家弗朗西斯·高尔顿(Francis Galton)于19世纪末发明的一种物理装置,用于演示随机分布和大数法则的概念。它通过简单的机械原理展示了概率和统计的基本概念。
高尔顿板是一个简单而有效的工具,通过直观的物理演示使得复杂的概率和统计概念变得易于理解。它不仅是教育的有效工具,也是研究随机性和分布特性的重要模型。
结构与原理
-
结构:
- 高尔顿板通常由一个倾斜的木板或其他材料制成,面板上排列着若干个固定的小钉或障碍物,形成一个网格状的结构。底部有多个接收容器(例如小盒子或小槽),用于收集掉落的颗粒或小球。
-
工作原理:
- 顶部的槽(或投入口)用于放置小球。当小球从顶部落下时,它们会碰到网格中的钉子。每次碰撞时,小球都有50%的概率向左或向右偏移,导致小球沿着随机路径向下移动。
- 随着小球不断下落,它们最终将停在底部的接收容器中。由于每个球的下落路径是随机的,经过多次实验后,落入各个槽中的小球数量会呈现出明显的钟形正态分布。
数学与统计意义
- 大数法则: 高尔顿板是展示大数法则的经典案例之一。随着投入的小球数量的增加,落入各个接收容器的数量趋向于正态分布,即使小球的每次下落是随机的,但总体的结果表现出稳定的模式。
- 中立性和随机性: 高尔顿板展示了随机性下的平衡现象。虽然每个小球的移动路径是随机的,但它们最终的数量分布却可以预测。
应用
- 高尔顿板常用于教育和教学,帮助学生理解概率、统计、正态分布、大数法则等概念。
- 也被广泛应用于统计学、心理学和经济学等其他学科的可视化实验中。
创建manim代码
from manim import *
import random
class GaltonBoard(Scene):
# 配置信息
config = {
"runTime": 16, # 动画运行时间
"itemsTotal": 100, # 总点数
"itemDelayFrames": 1, # 点出现间隔(帧数)
"hexSize": .2, # 六边形的大小
"hexVerticalShift": .6, # 六边形的垂直偏移
"hexGorizontalShift": .4, # 六边形的水平偏移
"hexRowsCount": 7, # 六边形的行数
"firstHexCenterX": -3, # 第一个六边形的中心x坐标
"firstHexCenterY": 3, # 第一个六边形的中心y坐标
"durationSeconds": 2, # 每个点的运动持续时间
"circleRadius": .05, # 小圆点的半径
"firstDot": [-3, 4.3, 0] # 第一个点的位置
}
frameNumber = 0 # 帧计数器
def construct(self):
# 创建表格、计数器、六边形、顶点和小点
table = self.createTable() # 生成表格
counter = self.createCounter() # 生成计数器
hexagons = self.createHexagons() # 生成六边形
vertices = self.createVertices() # 生成六边形的顶点
items = self.createItems(vertices) # 生成小点
# 帧更新函数
def updateFrameFunction(table):
durationSeconds = GaltonBoard.config["durationSeconds"]
durationFrames = durationSeconds * self.camera.frame_rate # 单位时间内的帧数
self.frameNumber += 1
for item in items:
if item.isActive and self.frameNumber > item.startFrame:
alpha = (self.frameNumber - item.startFrame) / durationFrames
if (alpha <= 1.0):
point = item.path.point_from_proportion(rate_functions.linear(alpha)) # 获取小点在路径上的位置
item.circle.move_to(point) # 移动小点
else:
updateCounter() # 更新计数器
updateStackValue(item.stackIndex) # 更新堆叠值
item.isActive = False # 设置点为非活动状态
# 更新计数器函数
def updateCounter():
val = counter[0].get_value() # 获取计数器当前值
val += 1 # 增加计数
counter[0].set_value(val) # 更新计数器的值
# 更新堆叠值的函数
def updateStackValue(stackValueIndex):
cell = table.get_entries((1, stackValueIndex + 1)) # 获取表格中对应单元格
val = cell.get_value() # 获取该单元格的当前值
val += 1 # 增加堆叠值
cell.set_value(val) # 更新单元格值
# 渲染六边形和表格与计数器
self.play(FadeIn(hexagons, run_time=1))
self.play(FadeIn(table, run_time=1))
self.play(FadeIn(counter, run_time=1))
# 为更新函数准备需要更新的对象
wrapper = VGroup(table, counter)
for item in items:
wrapper.add(item.circle)
runTime = GaltonBoard.config["runTime"]
# 开始更新动画
self.play(UpdateFromFunc(wrapper, updateFrameFunction), run_time=runTime)
self.wait(3) # 等待3秒以查看结果
def createTable(self):
# 创建一个整数表来显示点的堆叠数量
table = IntegerTable(
[[0, 0, 0, 0, 0, 0, 0, 0],], # 初始化表格
line_config={"stroke_width": 1, "color": Y
line_config={"stroke_width": 1, "color": YELLOW}, # 表格线的样式设置
cell_config={"stroke_width": 1, "color": WHITE}, # 单元格的样式设置
)
table.move_to(UP * 3) # 将表格移动到画面上方
return table
def createCounter(self):
# 创建一个计数器用于计数通过的点
counter = DecimalNumber(0) # 创建一个数值对象,初始值为0
counter.move_to(UP * 3 + RIGHT * 5) # 将计数器移动到适当位置
return [counter] # 返回计数器对象列表
def createHexagons(self):
hexagons = VGroup() # 创建一个用于存放六边形的组
hexSize = GaltonBoard.config["hexSize"] # 获取六边形的大小
hexVerticalShift = GaltonBoard.config["hexVerticalShift"] # 获取垂直偏移量
hexGorizontalShift = GaltonBoard.config["hexGorizontalShift"] # 获取水平偏移量
hexRowsCount = GaltonBoard.config["hexRowsCount"] # 获取行数
# 循环生成六边形
for row in range(hexRowsCount):
for col in range(3):
hexagon = RegularPolygon(n=6, radius=hexSize) # 创建一个六边形
hexagon.move_to(
(col * hexGorizontalShift, row * hexVerticalShift, 0) # 设置六边形位置
)
hexagons.add(hexagon) # 将六边形加入组中
return hexagons # 返回所有六边形
def createVertices(self):
# 创建六边形的顶点坐标
vertices = []
hexSize = GaltonBoard.config["hexSize"] # 获取六边形的大小
hexVerticalShift = GaltonBoard.config["hexVerticalShift"] # 获取垂直偏移量
# 根据行数计算每行的顶点坐标
for row in range(GaltonBoard.config["hexRowsCount"]):
vertexRow = []
for i in range(3): # 每行有3个顶点
vertexRow.append(np.array([
i * GaltonBoard.config["hexGorizontalShift"],
row * hexVerticalShift,
0
]))
vertices.append(vertexRow) # 将顶点按行添加到列表中
return vertices # 返回所有顶点
def createItems(self, vertices):
# 创建小点并为其分配路径
itemsTotal = GaltonBoard.config["itemsTotal"] # 获取总点数
circleRadius = GaltonBoard.config["circleRadius"] # 小圆点半径
itemDelayFrames = GaltonBoard.config["itemDelayFrames"] # 小点出现间隔
firstDot = GaltonBoard.config["firstDot"] # 第一个小点的位置
items = [] # 存放小点的列表
startFrame = 0 # 起始帧计数
stackValues = [0] * 9 # 存储堆叠数的列表,初始化为0
for k in range(itemsTotal):
item = Item() # 初始化点
circle = Circle(radius=circleRadius, color=GREEN, fill_opacity=1) # 创建小圆点
pathIndex = self.createPathIndex() # 生成路径索引
stackIndex = pathIndex.bit_count() # 计算堆叠索引
stackValues[stackIndex] += 1 # 增加堆叠值
path = self.createPath(vertices, pathIndex, stackValues[stackIndex]) # 创建路径
item.path = path # 分配路径
item.circle = circle # 分配圆点
item.stackIndex = stackIndex # 设置堆叠索引
item.startFrame = startFrame # 设置起始帧
startFrame += itemDelayFrames # 更新起始帧
self.add(circle) # 将圆点添加到场景中
circle.move_to(firstDot) # 移动圆点到第一个位置
items.append(item) # 将点添加到列表中
# 如果需要可以显示路径
# self.add(path)
return items # 返回所有小点
def createPathIndex(self):
# 随机生成一个路径索引
return random.randrange(128) # 返回0到127之间的随机整数
def createPath(self, vertices, pathIndex, itemsCountInStack):
# 根据路径索引和堆叠数创建路径
firstDot = GaltonBoard.config["firstDot"] # 获取第一个点的位置
rowCapacity = 3 # 每行最大容量
# 计算最后一个点在网格中的位置
lastDotRowIndex = (itemsCountInStack - 1) // rowCapacity
lastDotColIndex = (itemsCountInStack - 1) % rowCapacity
path = Line(firstDot, vertices[0][0], stroke_width=1) # 创建起始点到第一个点的线
previousDot = vertices[0][0]
binary = bin(pathIndex)[2:].zfill(7) # 将路径索引转为二进制,左侧填0到7位
rowIndex, colIndex = 1, 0 # 初始化行列索引
# 根据路径索引的二进制值生成路径
for digit in binary:
if digit == '0':
pathTmp = ArcBetweenPoints(previousDot, vertices[rowIndex][colIndex], angle=PI / 2, stroke_width=1) # 向左转90度
else:
colIndex += 1
pathTmp = ArcBetweenPoints(previousDot, vertices[rowIndex][colIndex], angle=-PI / 2, stroke_width=1) # 向右转90度
previousDot = vertices[rowIndex][colIndex]
path.append_vectorized_mobject(pathTmp) # 将路径片段添加到路径中
rowIndex += 1
# 计算最后一个点的坐标
lastDotWidth = .1 # 最后一个点的宽度
lastDotHeight = .1 # 最后一个点的高度
lastDotX = previousDot[0] # 获取最后一个点的x坐标
# 根据最后点的位置调整x坐标
if lastDotColIndex == 0:
lastDotX -= lastDotWidth
elif lastDotColIndex == 2:
lastDotX += lastDotWidth
lastDotY = previousDot[1] - 2.4 + lastDotHeight * lastDotRowIndex # 计算最后一个点的y坐标
pathLast = Line(previousDot, [lastDotX, lastDotY, 0], stroke_width=1) # 连接到最后一个点的路径
path.append_vectorized_mobject(pathLast) # 将最后的路径段添加到路径中
return path # 返回生成的路径
def showDotMap(self, showAxes):
# 显示点的坐标图
for x in range(-7, 8):
for y in range(-4, 5):
dot = Dot(np.array([x, y, 0]), radius=0.02) # 创建一个小点
self.add(dot) # 将点添加到场景中
if showAxes:
ax = Axes(x_range=[-7, 7], y_range=[-4, 4], x_length=14, y_length=8) # 创建坐标轴
self.add(ax) # 将坐标轴添加到场景中
class Item:
# 定义小点的类
circle = None # 圆点
path = None # 路径
startFrame = 0 # 开始帧
stackIndex = 0 # 堆叠索引
isActive = True # 是否活动的标志
我想要的理想型结果:
实际运行结果:
代码解释
-
GaltonBoard 类: 该类继承自 Manim 的
Scene
,用于创建高尔顿板的动画。配置参数定义了高尔顿板的运行时间、点的总数、点之间的延迟、圆点的大小和位置等信息。 -
构造函数:
construct
方法是动画的主入口,创建所有组件(表格、计数器、六边形、顶点、小点等),并控制它们的动画效果。 -
创建六边形和顶点:
createHexagons
和createVertices
方法用于生成高尔顿板上的六边形及其顶点,以便点沿着这些顶点掉落。 -
生成路径和小点:
createItems
方法创建小点并为其分配路径,路径的生成基于随机索引,决定了每个点在高尔顿板上掉落的方向。 -
动画更新: 动画通过
UpdateFromFunc
不断更新每个小点的位置,直到所有小点都掉落完毕。 -
路径生成:
createPath
方法根据随机生成的索引创建路径,通过计算每个点的坐标来绘制连线。 -
计数器和堆叠统计: 使用计数器记录每个点通过的次数,并在界面上显示。
总结
此代码实现了一种经典的概率分布演示工具,通过高尔顿板的随机掉落过程展示大数法则,提供了视觉化的理解,并使用 Manim 库进行高效的动画展示。