# Der Quellcode zum Video 'Der zentrale Grenzwertsatz' # # wurde entwickelt von # # Oğulcan Aşık # Chavoush Kalhor # Christian Müller # # an der Heinrich-Heine-Universität Düsseldorf. # # Dieses Werk ist lizenziert unter einer Creative Commons Namensnennung-Weitergabe unter gleichen Bedingungen 4.0 International Lizenz. # Um eine Kopie der Lizenz zu erhalten, besuchen Sie https://creativecommons.org/licenses/by-sa/4.0/. # # SPDX-License-Identifier: CC-BY-SA-4.0 from manim import * import numpy as np from scipy.stats import norm, binom from scipy.special import comb from math import floor from typing import Iterable ORCAblue = "#002b44" ORCAred = "#e61300" ORCAgreen = "#4cb011" ORCAgrey = "#f5f5f5" ORCAwhite = "#ffffff" def triangle_number(n): '''Computes the nth triangle number. Parameters ---------- n the number of rows in the triangle Returns ------- The number of dots arranged in a triangle with n dots on each side. Equal to the sum of the natural numbers from 1 to n. ''' return n * (n + 1) // 2 def safe_div(a, b): if b == 0: return a return a / b def bin_pmf(k, n, p, use_smoothing=True): '''Computes the probability mass function of the binomial distribution Parameters ---------- k the number of successes n number of trials p probability of success Returns ------- The probability of exactly k successes in n trials. ''' k = floor(k) if not use_smoothing: n = int(n) return comb(n, k) * p ** k * (1 - p) ** (n - k) def bin_cdf(k, n, p, use_smoothing=True): '''Computes the probability density function of the binomial distribution Parameters ---------- k the number of successes n number of trials p probability of success Returns ------- The probability of getting k or fewer successes in n trials. ''' return sum([bin_pmf(i, n, p, use_smoothing) for i in range(0, floor(k) + 1)]) # TODO: rewrite this as a class and move to CustomMobjects def bar_chart( axes, x_values: Iterable[float], y_values: Iterable[float], x_range, color, **kwargs ): if len(x_values) == len(y_values): bars = VGroup() for i, val in enumerate(zip(x_values, y_values)): x, y = val[0:2] if x < x_range[0] or x > x_range[1]: continue if i == 0: bar_w = min( abs(axes.c2p(x, 0)[0] - axes.c2p(x_values[i + 1], 0)[0]), abs(axes.c2p(x, 0)[0] - axes.c2p(x_range[0], 0)[0]), ) elif i == len(x_values) - 1: bar_w = min( abs(axes.c2p(x, 0)[0] - axes.c2p(x_values[i - 1], 0)[0]), abs(axes.c2p(x, 0)[0] - axes.c2p(x_range[1], 0)[0]), ) else: bar_w = min( abs(axes.c2p(x_values[i + 1], 0)[0] - axes.c2p(x, 0)[0]), abs(axes.c2p(x, 0)[0] - axes.c2p(x_values[i - 1], 0)[0]), ) bar_h = axes.c2p(0, y)[1] - axes.c2p(0, 0)[1] bar = Rectangle(height=bar_h, width=bar_w, ** kwargs).set_fill(color, 0.7) bar.move_to(axes.c2p(x, 0), DOWN) bars.add(bar) return bars DEFAULT_GALTON_BOARD_CIRCLE_RADIUS = 0.2 class PolygonalChain(Line): def __init__( self, *vertices, **kwargs ): if len(vertices) < 2: raise Exception('You must provide at least two vertices.') super().__init__(vertices[0], vertices[1], **kwargs) if len(vertices) > 2: for vertex in vertices[2:]: self.add_line_to(vertex) class GaltonBoard(VGroup): def __init__( self, rows: int | None = 10, circle_radius: float | None = DEFAULT_GALTON_BOARD_CIRCLE_RADIUS, include_buckets: bool | None = True, **kwargs ): super().__init__(**kwargs) self.rows = rows self.circle_radius = circle_radius self.add( Circle(radius=circle_radius, fill_opacity=1, stroke_width=0) ) for i in range(1, rows): self += self[0].copy().shift((DOWN*np.sqrt(12) + LEFT*2) * i*circle_radius) for _ in range(i): self += self[len(self) - 1].copy().shift(RIGHT*4*circle_radius) if include_buckets: self += self[0].copy().shift((DOWN*np.sqrt(12) + LEFT*2) * rows*circle_radius) for _ in range(rows): self += self[len(self) - 1].copy().shift(RIGHT*4*circle_radius) self._add_buckets() def _add_buckets(self): for i in range(triangle_number(self.rows), len(self)): rect = ( Rectangle(height=8*self.circle_radius, width=4*self.circle_radius) .move_to(self[i]) ) bucket = PolygonalChain( rect.get_corner(UL), rect.get_corner(DL), rect.get_corner(DR), rect.get_corner(UR), ) self[i] = bucket # at the moment, this breaks when scale != 1 # TODO: fix this def generate_path(self, p): path = VGroup( Arc(2*self.circle_radius, TAU / 4, TAU / 4, arc_center=self[0].get_center()) ) path[-1].add_line_to(path[-1].get_end() + DOWN * (np.sqrt(12) - 2)*self.circle_radius) if binom.rvs(1, 1-p): path[-1].flip().shift(RIGHT*2*self.circle_radius) for _ in range(1, self.rows): path += Arc(2*self.circle_radius, TAU / 4, TAU / 4, arc_center=path[-1].get_end() + DOWN*2*self.circle_radius) path[-1].add_line_to(path[-1].get_end() + DOWN*(np.sqrt(12) - 2)*self.circle_radius) if binom.rvs(1, p): path[-1].flip().shift(RIGHT*2*self.circle_radius) return path np.random.seed(seed=1319) config.background_color = "#c4d1d7" config.max_files_cached = -1 # video sections and learning outcomes sections = Tex( r"\textsf{\textbf{Der zentrale Grenzwertsatz}}\\", r"\textsf{\textbf{Kapitel 1: \\ Das Galtonbrett}}\\", r"\textsf{\textbf{Kapitel 2: \\ Stochastische Modellierung}}\\", r"\textsf{\textbf{Kapitel 3: \\ Der zentrale Grenzwertsatz von de Moivre-Laplace}}\\", r"\textsf{\textbf{Zusammenfassung}}", color=ORCAblue, ) outcomes = Tex( r"\textsf{\textbf{In diesem Video}}", r"\begin{itemize} \item \textsf{Das Galtonbrett} \end{itemize}", r"\begin{itemize} \item \textsf{Stochastische Modellierung des Galtonbretts} \end{itemize}", r"\begin{itemize} \item \textsf{Mathematische Formulierung des zentralen Grenzwertsatzes} \end{itemize}", color=ORCAblue, ) # animate video sections and learning outcomes class CLTSection0(Scene): # scene 0: introduction and learning outcomes def construct(self): sections[0].set(font_size=56).move_to(ORIGIN) outcomes[0].to_edge(UL) outcomes[1:4].to_edge(LEFT) logo_license = SVGMobject( file_name="..\Anderes\Logo_CC_BY_SA.svg", width=1.5) self.add(logo_license.to_edge(DR)) self.wait(2) self.play(FadeIn(sections[0])) self.wait(2) self.play(FadeOut(sections[0])) self.wait(2) self.play(FadeIn(outcomes[0])) self.wait(2) self.play(FadeIn(outcomes[1])) self.wait(2) self.play(FadeIn(outcomes[2])) self.wait(2) self.play(FadeIn(outcomes[3])) self.wait(2) self.play(*[FadeOut(mobject) for mobject in self.mobjects]) class CLTSection1(Scene): def construct(self): sections[1].move_to(ORIGIN) self.wait(2) self.play(FadeIn(sections[1])) self.wait(2) self.play(FadeOut(sections[1])) self.wait(2) class CLTSection2(Scene): def construct(self): sections[2].move_to(ORIGIN) self.wait(2) self.play(FadeIn(sections[2])) self.wait(2) self.play(FadeOut(sections[2])) self.wait(2) class CLTSection3(Scene): def construct(self): sections[3].move_to(ORIGIN) self.wait(2) self.play(FadeIn(sections[3])) self.wait(2) self.play(FadeOut(sections[3])) self.wait(2) class CLTSection4(Scene): def construct(self): sections[4].move_to(ORIGIN) self.wait(2) self.play(FadeIn(sections[4])) self.wait(2) self.play(FadeOut(sections[4])) self.wait(2) # begin with regular animations ROW_COUNT = 10 CIRCLE_RADIUS = 0.15 class CLT1(ThreeDScene): # scene 1: the Galton board def construct(self): # the number of balls to be dropped ball_count = 500 # probability for the ball to bounce to the right at any given obstacle p = ValueTracker(0.5) # display the current values of n and p n_label = MathTex(f"n={ROW_COUNT}", color=ORCAblue).to_edge( LEFT).shift(2 * UP) p_label = always_redraw( lambda: MathTex( f"p={'{0:.2f}'.format(p.get_value())}", color=ORCAblue) .to_edge(LEFT) .shift(UP) ) board = GaltonBoard(rows=ROW_COUNT, circle_radius=CIRCLE_RADIUS, include_buckets=True).set_color(ORCAblue) board.move_to(ORIGIN + 0.5 * UP) paths = VGroup() for _ in range(ball_count): paths += board.generate_path(p.get_value()) ball = Circle(radius=CIRCLE_RADIUS, color=ORCAred, fill_opacity=1, stroke_width=0).move_to(config.top + UP) axes_x_len = abs( board.get_boundary_point(DL)[0] - board.get_boundary_point(DR)[0] ) # this is currently only used to calculate, which bucket the ball falls into # TODO: simplify this process axes = Axes( x_range=[0, ROW_COUNT, ROW_COUNT + 1], y_range=[0, 1, 2], x_length=axes_x_len, y_length=6*CIRCLE_RADIUS, tips=False, ).align_to(board, DOWN) freqs = np.zeros(ROW_COUNT + 1) hist = always_redraw( lambda: BarChart( safe_div(freqs, np.max(freqs)) + 0.01, y_range=[0, 1], bar_width=1, x_length=axes_x_len, x_axis_config={"include_ticks": False, "font_size": 34}, y_length=6*CIRCLE_RADIUS, bar_names=[str(int(freqs[i])) for i in range(len(freqs))], )[0:2] .set_color(ORCAblue) .align_to(board, DOWN) .shift(0.5 * DOWN) ) self.play( LaggedStart( *[ DrawBorderThenFill( board[triangle_number(i): triangle_number(i + 1)] ) for i in range(ROW_COUNT) ], lag_ratio=0.3, run_time=5, ) ) self.wait(2) self.play( LaggedStart( *[ Indicate( board[triangle_number(i): triangle_number(i + 1)], scale_factor=(i + 2) / (i + 1), color=None, ) for i in range(ROW_COUNT) ], lag_ratio=0.3, run_time=5, ) ) self.wait(2) self.add(ball) self.play(ball.animate.move_to(paths[0][0].get_start())) self.wait(2) self.play(MoveAlongPath(ball, paths[0][0]), rate_func=linear, ) self.wait(2) self.play(MoveAlongPath(ball, paths[0][1]), rate_func=linear) self.wait(2) self.play(FadeIn(n_label)) self.wait(2) self.play(FadeIn(p_label)) self.add_fixed_in_frame_mobjects(n_label, p_label) self.wait(2) for subpath in paths[0][2:]: self.play(MoveAlongPath(ball, subpath), rate_func=linear) self.wait(2) self.play(Create(board[triangle_number(ROW_COUNT):])) self.wait(2) self.bring_to_back(hist[0]) self.wait(2) self.play( ball.animate.shift(DOWN*0.7).set_opacity(0), rate_func=linear, run_time=0.5 ) bucket = round(axes.p2c(ball.get_center())[0]) freqs[bucket] += 1 hist.save_state() hist.update() self.wait(2) self.play(Write(hist[1])) self.wait(2) for path in paths[1:]: ball.set_opacity(1).move_to(config.top + UP) ball.move_to(path[0].get_start()) self.wait(1 / 60) for subpath in path: ball.move_to(subpath.get_end()) self.wait(1 / 60) bucket = round(axes.p2c(ball.get_center())[0]) freqs[bucket] += 1 hist.update() ball.set_opacity(0) self.wait(2) # for path in paths[1:]: # ball.set_opacity(1).move_to(config.top + UP) # self.play(ball.animate.move_to(path[0].get_start()), run_time=0, rate_func=linear) # for subpath in path: # self.play(MoveAlongPath(ball, subpath), run_time=0, rate_func=linear) # self.play(ball.animate.shift(DOWN*0.7).set_opacity(0), run_time=0, rate_func=linear) # bucket = round(axes.p2c(ball.get_center())[0]) # freqs[bucket] += 1 # hist.update() # self.wait(2) freqs = np.zeros(ROW_COUNT + 1) self.play(Restore(hist)) self.wait(2) # emulate anti-clockwise rotation of the board by rotating the camera, # actual rotation of board causes problems with updaters self.move_camera(theta=-100 * DEGREES) self.wait(2) self.play(p.animate.set_value(0.4)) self.wait(2) paths = VGroup() for _ in range(ball_count): paths += board.generate_path(p.get_value()) for path in paths: ball.set_opacity(1).move_to(config.top + UP) ball.move_to(path[0].get_start()) self.wait(1 / 60) for subpath in path: ball.move_to(subpath.get_end()) self.wait(1 / 60) bucket = round(axes.p2c(ball.get_center())[0]) freqs[bucket] += 1 hist.update() ball.set_opacity(0) self.wait(2) # for path in paths: # ball.set_opacity(1).move_to(config.top + UP) # self.play(ball.animate.move_to(path[0].get_start()), run_time=0, rate_func=linear) # for subpath in path: # self.play(MoveAlongPath(ball, subpath), run_time=0, rate_func=linear) # self.play( # ball.animate.shift( # (DOWN*0.7 + np.tan(10 * DEGREES) * LEFT*0.7) # / np.sqrt(1 + np.tan(10 * DEGREES) ** 2) # ).set_opacity(0), run_time=0, rate_func=linear # ) # bucket = round(axes.p2c(ball.get_center())[0]) # freqs[bucket] += 1 # hist.update() # self.wait(2) # TODO save freqs values for the histogram in CLT2b print(freqs) self.wait(2) p_label.clear_updaters() self.remove_fixed_in_frame_mobjects(n_label, p_label) # this breaks the labels, fade out in post instead # self.play(FadeOut(board, hist, ball, n_label, p_label)) self.wait(2) ZOOM_FACTOR = 0.3 class CLT2(ZoomedScene): # scene 2: stochastic modelling def __init__(self, **kwargs): ZoomedScene.__init__( self, zoom_factor=ZOOM_FACTOR, zoomed_display_height=2.5, zoomed_display_width=3.5, zoomed_camera_config={ "default_frame_stroke_width": 2.5, "default_frame_stroke_color": ORCAblue, "background_opacity": 0, }, **kwargs, ) def construct(self): p = 0.5 board = GaltonBoard(rows=ROW_COUNT, circle_radius=CIRCLE_RADIUS, include_buckets=True).set_color(ORCAblue) board.move_to(ORIGIN + 0.5 * UP) ball = Circle(radius=CIRCLE_RADIUS, color=ORCAred, fill_opacity=1, stroke_width=0).move_to(config.top + UP) ber = board[0].copy().to_edge(UL) bin = board[-1].copy().scale(0.5).to_edge(UL).shift(DOWN) ber_text = MathTex(r"=Y_i\sim\textsf{Ber}(p)", color=ORCAblue).next_to( ber, RIGHT ) bin_text = MathTex(r"=S_n\sim\textsf{Bin}(n,p)", color=ORCAblue).next_to( bin, RIGHT ) self.play( LaggedStart( *[ DrawBorderThenFill( board[triangle_number(i): triangle_number(i + 1)] ) for i in range(ROW_COUNT) ], lag_ratio=0.3, ), run_time=2, ) self.play(Create(board[triangle_number(ROW_COUNT):]), run_time=2) self.play(ball.animate.move_to(board[0].get_top() + 0.2 * UP)) self.wait(2) self.zoomed_camera.frame.move_to(board[0].get_top()) self.zoomed_display.display_frame.set_color(ORCAblue) self.activate_zooming(animate=True) self.zoomed_display.display_frame.set_color(config.background_color) self.zoomed_display.set(height=self.zoomed_display_height * 1.025) self.zoomed_display.set(width=self.zoomed_display_width * 1.025) self.wait(2) self.play(FadeIn(ber), FadeIn(ber_text)) # store the outcomes in each row as 0s and 1s successes = [] # store the outcomes in each row as MathTex bernoulli_process = VGroup() # store the sum of outcomes as MathTex binomial_process = VGroup() i = 0 for subpath in board.generate_path(p): i = i + 1 # Is there a simpler way to access the outcomes in each row? successes.append( 1 if subpath.get_end()[0] > ball.get_center()[0] else 0 ) bernoulli_process.add( MathTex( "Y_{" + str(len(successes)) + "}=" + str(successes[-1]), color=ORCAblue, ).next_to(self.zoomed_display, DOWN) ) binomial_process.add( MathTex( "S_{" + str(len(successes)) + "}=" + str(sum(successes)), color=ORCAblue, ).next_to(self.zoomed_display, DOWN * 3) ) if i < ROW_COUNT: self.play( MoveAlongPath(ball, subpath), rate_func=rate_functions.ease_out_bounce, ) else: self.play( MoveAlongPath(ball, subpath), rate_func=rate_functions.linear, run_time=0.7 ) if len(successes) == 1: self.play(FadeIn(bernoulli_process[0]), run_time=0.5) self.play(FadeIn(binomial_process[0]), run_time=0.5) else: self.play( TransformMatchingShapes( bernoulli_process[-2], bernoulli_process[-1] ), run_time=0.5, ) self.play( TransformMatchingShapes( binomial_process[-2], binomial_process[-1]), run_time=0.5, ) self.play( self.zoomed_camera.frame.animate.move_to(ball.get_bottom()), run_time=0.5, rate_func=linear, ) self.wait(2) self.play(Indicate(binomial_process[-1], color=None)) self.zoom_activated = False self.wait(2) self.play( ball.animate.shift(DOWN*0.7).set_opacity(0), rate_func=linear, run_time=0.5 ) self.play(FadeIn(bin), FadeIn(bin_text)) self.wait(2) self.play( VGroup( board, self.zoomed_camera.frame, bernoulli_process[-1], binomial_process[-1], ).animate.shift(20 * RIGHT), self.zoomed_display.animate.shift(20 * RIGHT), ) self.wait(2) class CLT2b(ThreeDScene): def construct(self): p = 0.4 ball_count = 10 x_len = 10 y_len = 5 board = GaltonBoard(rows=ROW_COUNT, circle_radius=CIRCLE_RADIUS, include_buckets=True).set_color(ORCAblue) board.move_to( ORIGIN + 0.5 * UP + 20 * RIGHT + 20 * np.tan(10 * DEGREES) * DOWN ) # ber = board[0].copy().to_edge(UL) # bin = board[-1].copy().scale(0.5).to_edge(UL).shift(DOWN) # ber_text = MathTex(r"{{\sim\textsf{Ber}(}}p{{)}}", color=ORCAblue).next_to( # ber, RIGHT # ) # bin_text = MathTex(r"{{\sim\textsf{Bin}(}}n{{, p)}}", color=ORCAblue).next_to( # bin, RIGHT # ) # bin_text2 = MathTex(r"{{\sim\textsf{Bin}(}}10{{, p)}}", color=ORCAblue).next_to( # bin, RIGHT # ) freqs = [2, 9, 47, 108, 121, 117, 68, 26, 2, 0, 0] axes_x_len = abs( board.get_boundary_point(DL)[0] - board.get_boundary_point(DR)[0] ) hist = always_redraw( lambda: BarChart( safe_div(freqs, np.max(freqs))*(0.24609) + 0.01, y_range=[0, 0.24609, 0.1], bar_width=1, x_length=axes_x_len, x_axis_config={"include_ticks": False, "font_size": 34}, y_length=6*CIRCLE_RADIUS, bar_names=[str(int(freqs[i])) for i in range(len(freqs))], )[0:3] .set_color(ORCAblue) .align_to(board, DR) .shift(0.5 * DOWN) ).add_updater(lambda m: m[2].set_opacity(0)) # on of these line has to be commented out # self.add(ber, ber_text, bin, bin_text) self.set_camera_orientation(theta=-100 * DEGREES) self.add(hist, board) self.play( board.animate.shift(20 * LEFT + 20 * np.tan(10 * DEGREES) * UP) ) self.wait(2) # if bin and ber are not added, comment this out and wait 1 second # self.play(TransformMatchingTex(bin_text, bin_text2, transform_mismatches=True)) self.wait(1) self.wait(2) # if bin and ber are not added, comment this out and wait 1 second # bin_text2.become( # MathTex(r"{{\sim\textsf{Bin}(10,}} p {{)}}", color=ORCAblue).next_to( # bin, RIGHT # ) # ) self.wait(1) self.wait(2) # if bin and ber are not added, comment this out and wait 1 second # self.play( # TransformMatchingTex( # ber_text, # MathTex(r"{{\sim\textsf{Ber}(}}0.4{{)}}", color=ORCAblue).next_to( # ber, RIGHT # ), # transform_mismatches=True, # ), # TransformMatchingTex( # bin_text2, # MathTex(r"{{\sim\textsf{Bin}(10,}} 0.4 {{)}}", color=ORCAblue).next_to( # bin, RIGHT # ), # transform_mismatches=True, # ), # ) self.wait(1) self.wait(2) axes = Axes( x_range=[0, ROW_COUNT, ROW_COUNT + 1], y_range=[0, 1, 2], x_length=axes_x_len, y_length=6*CIRCLE_RADIUS, tips=False, ).align_to(board, DOWN) ball = Dot(color=ORCAred, radius=CIRCLE_RADIUS).move_to( config.top + UP) paths = VGroup() for _ in range(ball_count): paths += board.generate_path(p) for path in paths: ball.set_opacity(1).move_to(config.top + UP) self.play(ball.animate.move_to( path[0].get_start()), run_time=0.1, rate_func=linear) for subpath in path: self.play(MoveAlongPath(ball, subpath), run_time=0.1, rate_func=linear) self.play( ball.animate.shift( (DOWN*0.7 + np.tan(10 * DEGREES) * LEFT*0.7) / np.sqrt(1 + np.tan(10 * DEGREES) ** 2) ).set_opacity(0), run_time=0.07, rate_func=linear ) bucket = round(axes.p2c(ball.get_center())[0]) freqs[bucket] += 1 hist.update() self.wait(2) self.move_camera(theta=-90 * DEGREES) # the value of 0.24609 (=bin_pmf(5,10,0.5)) is chosen to equalize the max height of bin_graph1 and hist bin_graph1 = BarChart( [bin_pmf(k, 10, p) for k in np.arange(0, 10 + 1)], y_range=[0, 0.24609, 0.1], bar_width=1, x_length=axes_x_len, x_axis_config={"include_ticks": False, "font_size": 34}, y_length=6*CIRCLE_RADIUS, bar_names=[str(i) for i in range(ROW_COUNT + 1)] )[0:].set_color(ORCAblue).align_to(hist, UL) hist.clear_updaters() axes2 = ( Axes( x_range=[0, 100, 10], y_range=[0, 0.3, 0.1], x_length=x_len, y_length=y_len, tips=False, axis_config={"color": ORCAblue, "stroke_width": 3}, ) .add_coordinates(color=ORCAblue) ) axes_labels = axes2.get_axis_labels(x_label="x", y_label="P(S_n=x)").set_color( ORCAblue ) bin_graph1_labels = VGroup( axes_labels[0].copy().next_to(bin_graph1, DR).shift(UP), axes_labels[1].copy().next_to( bin_graph1, UL).shift(RIGHT*1.5 + UP*0.2), ) self.wait(2) self.play( FadeOut(VGroup(board, hist[1])), # FadeOut(VGroup(ber, ber_text, bin, bin_text)) ) self.wait(2) self.play(hist[2].animate.set_opacity(1)) self.wait(2) self.play( ReplacementTransform(VGroup(hist[0], hist[2]), VGroup( bin_graph1[0], bin_graph1[2])), FadeIn(bin_graph1[1]), FadeIn(bin_graph1_labels), ) self.wait(2) bin_graph2 = bar_chart( axes2, np.arange(0, 10 + 1), [bin_pmf(k, 10, p, True) for k in np.arange(0, 10 + 1)], x_range=[0, 100], color=ORCAblue, stroke_width=3, stroke_color=ORCAblue ) # self.play(FadeOut(bin_graph1[1:])) self.play( ReplacementTransform(bin_graph1[0], bin_graph2), ReplacementTransform(bin_graph1[1:], axes2), bin_graph1_labels.animate.become(axes_labels) ) self.wait(5) class CLT2bLabels(ThreeDScene): def construct(self): p = 0.4 ball_count = 10 board = GaltonBoard(rows=ROW_COUNT, circle_radius=CIRCLE_RADIUS, include_buckets=True).set_color(ORCAblue) self.wait(1) ber = board[0].copy().to_edge(UL) bin = board[-1].copy().scale(0.5).to_edge(UL).shift(DOWN) ber_text = MathTex(r"{{\sim\textsf{Ber}(}}p{{)}}", color=ORCAblue).next_to( ber, RIGHT ) ber_text2 = MathTex(r"{{\sim\textsf{Ber}(}} 0.4 {{)}}", color=ORCAblue).next_to( ber, RIGHT ) bin_text = MathTex(r"{{\sim\textsf{Bin}(}}n{{,}} p {{)}}", color=ORCAblue).next_to( bin, RIGHT ) bin_text2 = MathTex(r"{{\sim\textsf{Bin}(}}10{{,}} p {{)}}", color=ORCAblue).next_to( bin, RIGHT ) bin_text3 = MathTex(r"{{\sim\textsf{Bin}(}}10{{,}} 0.4 {{)}}", color=ORCAblue).next_to( bin, RIGHT ) self.play(FadeIn(ber, ber_text, bin, bin_text)) self.wait(2) self.play(TransformMatchingTex( bin_text, bin_text2, transform_mismatches=True)) self.wait(2) self.wait(2) self.play( TransformMatchingTex( ber_text, ber_text2, transform_mismatches=True, ), TransformMatchingTex( bin_text2, bin_text3, transform_mismatches=True, ), ) self.wait(2) paths = VGroup() for _ in range(ball_count): paths += board.generate_path(p) for path in paths: self.wait(0.1) for _ in path: self.wait(0.1) self.wait(0.07) self.wait(5) self.play( FadeOut(VGroup(ber, ber_text2, bin, bin_text3)) ) self.wait(14) n = ValueTracker(10) p = ValueTracker(0.4) n_label = always_redraw( lambda: MathTex(f"n={int(n.get_value())}") .set(color=ORCAblue) .align_to((5, 0.5, 0), DL) ) p_label = always_redraw( lambda: MathTex(f"p={p.get_value()}").set( color=ORCAblue).align_to((5, -0.5, 0), DL) ) # round down n if no smoothing is desired ev = ( ValueTracker() .add_updater(lambda m: m.set_value(n.get_value() * p.get_value())) .update() ) # round down n if no smoothing is desired sd = ( ValueTracker() .add_updater( lambda m: m.set_value( np.sqrt(n.get_value() * p.get_value() * (1 - p.get_value())) ) ) .update() ) x_len = 10 y_len = 5 # CLT_3a axes_3a = Axes( x_range=[0, 100, 10], y_range=[0, 0.3, 0.1], x_length=x_len, y_length=y_len, tips=False, axis_config={"color": ORCAblue, "stroke_width": 3}, ).add_coordinates(color=ORCAblue) labels_3a = axes_3a.get_axis_labels(x_label="x", y_label="{{P(}}S_n{{=x)}}").set_color( ORCAblue ) bin_graph_3a_old = always_redraw( lambda: bar_chart( axes_3a, np.arange(0, 100 + 1), [bin_pmf(k, n.get_value(), p.get_value(), True) for k in np.arange(0, 100 + 1)], x_range=[0, 100], color=ORCAblue, stroke_width=3, stroke_color=ORCAblue ) ) bin_graph_3a_new = always_redraw( lambda: bar_chart( axes_3a, np.arange(0, 100 + 1), [bin_pmf(k, n.get_value(), p.get_value(), True) for k in np.arange(0, 100 + 1)], x_range=[-19, 19], color=ORCAblue, stroke_width=3, stroke_color=ORCAblue ) ) # CLT_3b axes_3b = Axes( x_range=[-20, 20, 5], y_range=[0, 0.4, 0.1], x_length=x_len, y_length=y_len, tips=False, axis_config={"color": ORCAblue, "stroke_width": 3}, ).add_coordinates(color=ORCAblue) labels_3b_old = axes_3b.get_axis_labels( x_label="x", y_label="{{P(}}S_n{{=x)}}" ).set_color(ORCAblue) labels_3b_new = axes_3b.get_axis_labels( x_label="x", y_label="{{P(}}S_n-\\textsf{E}[S_n]{{=x)}}" ).set_color(ORCAblue) x_vals = [] for i in np.arange(0, 100 + 1): x_vals.append(ValueTracker(i)) bin_graph_3b_old = always_redraw( lambda: bar_chart( axes_3b, [x.get_value() for x in x_vals], [bin_pmf(k, n.get_value(), p.get_value(), True) for k in np.arange(0, 100 + 1)], x_range=[-19, 19], color=ORCAblue, stroke_width=3, stroke_color=ORCAblue ) ) bin_graph_3b_new = always_redraw( lambda: bar_chart( axes_3b, [(x.get_value() - ev.get_value()) for x in x_vals], [bin_pmf(k, n.get_value(), p.get_value(), True) for k in np.arange(0, 100 + 1)], x_range=[-4, 4], color=ORCAblue, stroke_width=3, stroke_color=ORCAblue ) ) # CLT_3c axes_3c = Axes( x_range=[-5, 5, 1], y_range=[0, 0.4, 0.1], x_length=x_len, y_length=y_len, tips=False, axis_config={"color": ORCAblue, "stroke_width": 3}, ).add_coordinates(color=ORCAblue) labels_3c_old = axes_3c.get_axis_labels( x_label="x", y_label="{{P(}}S_n-\\textsf{E}[S_n]{{=x)}}" ).set_color(ORCAblue) labels_3c_new1 = axes_3c.get_axis_labels( x_label="x", y_label="{{P\\left(}}\\frac{S_n-\\textsf{E}[S_n]}{\\sqrt{\\textsf{Var}(S_n)}}" + "{{=x\\right)}}", ).set_color(ORCAblue) labels_3c_new1[1].shift(0.5 * DOWN) labels_3c_new2 = axes_3c.get_axis_labels( x_label="x", y_label="{{P(}}S_n^*" + "{{=x)}}", ).set_color(ORCAblue) labels_3c_new3 = axes_3c.get_axis_labels( x_label="x", y_label="\\sqrt{\\textsf{Var}(S_n)}" + "{{P(}}S_n^*" + "{{=x)}}" ).set_color(ORCAblue) bin_graph_3c_old = always_redraw( lambda: bar_chart( axes_3c, [(k - ev.get_value()) for k in np.arange(0, 100 + 1)], [bin_pmf(k, n.get_value(), p.get_value(), True) for k in np.arange(0, 100 + 1)], x_range=[-4, 4], color=ORCAblue, stroke_width=3, stroke_color=ORCAblue ) ) bin_graph_3c_new1 = always_redraw( lambda: bar_chart( axes_3c, [(k - ev.get_value()) / sd.get_value() for k in np.arange(0, 100 + 1)], [bin_pmf(k, n.get_value(), p.get_value(), True) for k in np.arange(0, 100 + 1)], x_range=[-4, 4], color=ORCAblue, stroke_width=3, stroke_color=ORCAblue ) ) bin_graph_3c_new2 = always_redraw( lambda: bar_chart( axes_3c, [(k - ev.get_value()) / sd.get_value() for k in np.arange(0, 100 + 1)], [ sd.get_value() * bin_pmf(k, n.get_value(), p.get_value(), True) for k in np.arange(0, 100 + 1) ], x_range=[-4, 4], color=ORCAblue, stroke_width=3, stroke_color=ORCAblue ) ) norm_graph_pdf = axes_3c.plot( lambda t: norm.pdf(t), x_range=[-5, 5, 0.1], color=ORCAred ) class CLT3(Scene): # scene 3: de Moivre-Laplace theorem for PDF def construct(self): # plot PMF of binomial distribution self.add(ev, sd, axes_3a, labels_3a, bin_graph_3a_old) self.wait(2) self.play(FadeIn(n_label), FadeIn(p_label)) # vary value of n self.wait(2) self.play(n.animate.set_value(100), run_time=10) self.wait(2) self.play(n.animate.set_value(10), run_time=10) # transition to next scene self.remove(bin_graph_3a_old) self.add(bin_graph_3a_new) self.play( ReplacementTransform(axes_3a, axes_3b), ReplacementTransform(labels_3a, labels_3b_old), ReplacementTransform(bin_graph_3a_new, bin_graph_3b_old), ) # adjust for expected value ev_10 = 10 * p.get_value() ev_100 = 100 * p.get_value() self.wait(2) self.play( *[x.animate.increment_value(-ev_10) for x in x_vals], ReplacementTransform(labels_3b_old, labels_3b_new), ) # vary value of n self.wait(2) self.play( n.animate.set_value(100), *[x.animate.increment_value(ev_10 - ev_100) for x in x_vals], run_time=10, ) self.wait(2) self.play( n.animate.set_value(10), *[x.animate.increment_value(ev_100 - ev_10) for x in x_vals], run_time=10, ) # transition to next scene self.wait(2) self.remove(bin_graph_3b_old) self.add(bin_graph_3b_new) self.play( ReplacementTransform(axes_3b, axes_3c), ReplacementTransform(labels_3b_new, labels_3c_old), ReplacementTransform(bin_graph_3b_new, bin_graph_3c_old), ) # adjust for variance part 1 self.wait(2) self.play( ReplacementTransform(labels_3c_old, labels_3c_new1), ReplacementTransform(bin_graph_3c_old, bin_graph_3c_new1), ) self.wait(2) self.play(ReplacementTransform(labels_3c_new1, labels_3c_new2)) # vary value of n self.wait(2) self.play(n.animate.set_value(100), run_time=10) self.wait(2) self.play(n.animate.set_value(10), run_time=10) # adjust for variance part 2 self.wait(2) self.play( ReplacementTransform(labels_3c_new2, labels_3c_new3), ReplacementTransform(bin_graph_3c_new1, bin_graph_3c_new2), ) # vary value of n self.wait(2) self.play(n.animate.set_value(100), run_time=10) # plot PDF of standard normal distribution self.wait(2) self.play(Write(norm_graph_pdf)) self.wait(2) self.play(n.animate.set_value(200), run_time=10) self.wait(2) # CLT_4a axes_4a = Axes( x_range=[0, 100, 10], y_range=[0, 1, 0.2], x_length=x_len, y_length=y_len, tips=False, axis_config={"color": ORCAblue}, ).add_coordinates(color=ORCAblue) labels_4a = axes_4a.get_axis_labels( x_label="t", y_label="{{P(S_n}}" + "{{\\leq t)}}" ).set_color(ORCAblue) bin_graph_4a = always_redraw( lambda: axes_4a.plot( lambda t: bin_cdf(t, n.get_value(), p.get_value()), discontinuities=[t for t in range(-100, 100)], use_smoothing=False, color=ORCAblue, ) ) # CLT_4b axes_4b = Axes( x_range=[-20, 20, 5], y_range=[0, 1, 0.2], x_length=x_len, y_length=y_len, tips=False, axis_config={"color": ORCAblue}, ).add_coordinates(color=ORCAblue) labels_4b_old = axes_4b.get_axis_labels( x_label="t", y_label="{{P(S_n}} {{\\leq t)}}" ).set_color(ORCAblue) labels_4b_new = axes_4b.get_axis_labels( x_label="t", y_label="{{P(S_n}}-\\textsf{E}[S_n] {{\\leq t)}}" ).set_color(ORCAblue) bin_graph_4b_old = always_redraw( lambda: axes_4b.plot( lambda t: bin_cdf(t, n.get_value(), p.get_value()), discontinuities=[t for t in range(-100, 100)], use_smoothing=False, color=ORCAblue, ) ) bin_graph_4b_new = always_redraw( lambda: axes_4b.plot( lambda t: bin_cdf(t + ev.get_value(), n.get_value(), p.get_value()), discontinuities=[t - ev.get_value() for t in range(-100, 100)], use_smoothing=False, color=ORCAblue, ) ) # CLT_4c axes_4c = Axes( x_range=[-5, 5, 1], y_range=[0, 1, 0.2], x_length=x_len, y_length=y_len, tips=False, axis_config={"color": ORCAblue}, ).add_coordinates(color=ORCAblue) labels_4c_old = axes_4c.get_axis_labels( x_label="t", y_label="{{P(S_n}}-\\textsf{E}[S_n] {{\\leq t)}}" ).set_color(ORCAblue) labels_4c_new = axes_4c.get_axis_labels( x_label="t", y_label="{{P(}} S_n^* {{\\leq t)}}" ).set_color(ORCAblue) bin_graph_4c_old = always_redraw( lambda: axes_4c.plot( lambda t: bin_cdf(t + ev.get_value(), n.get_value(), p.get_value()), discontinuities=[t - ev.get_value() for t in range(-100, 100)], use_smoothing=False, color=ORCAblue, ) ) bin_graph_4c_new = always_redraw( lambda: axes_4c.plot( lambda t: bin_cdf( t * sd.get_value() + ev.get_value(), n.get_value(), p.get_value() ), discontinuities=[ (t - ev.get_value()) / sd.get_value() for t in range(-100, 100) ], use_smoothing=False, color=ORCAblue, ) ) norm_graph_cdf = axes_4c.plot( lambda t: norm.cdf(t), x_range=[-5, 5, 0.1], color=ORCAred ) class CLT4(Scene): # scene 4: de Moivre-Laplace theorem for CDF def construct(self): # plot CDF of binomial distribution n.set_value(10) self.add(ev, sd) self.wait(2) self.play( Write(VGroup(n_label, p_label, axes_4a, labels_4a)), run_time=5 ) self.play( FadeIn(bin_graph_4a) ) # vary value of n self.wait(2) self.play(n.animate.set_value(100), run_time=10) self.wait(2) self.play(n.animate.set_value(10), run_time=10) # transition to next scene self.wait(2) self.play( ReplacementTransform(axes_4a, axes_4b), ReplacementTransform(labels_4a, labels_4b_old), ReplacementTransform(bin_graph_4a, bin_graph_4b_old), ) # adjust for expected value self.wait(2) self.play( TransformMatchingTex(labels_4b_old, labels_4b_new), ReplacementTransform(bin_graph_4b_old, bin_graph_4b_new), ) # vary value of n self.wait(2) self.play(n.animate.set_value(100), run_time=10) self.wait(2) self.play(n.animate.set_value(10), run_time=10) # transition to next scene self.wait(2) self.play( ReplacementTransform(axes_4b, axes_4c), TransformMatchingTex(labels_4b_new, labels_4c_old), ReplacementTransform(bin_graph_4b_new, bin_graph_4c_old), ) # adjust for variance self.wait(2) self.play( TransformMatchingTex(labels_4c_old, labels_4c_new), ReplacementTransform(bin_graph_4c_old, bin_graph_4c_new), ) self.wait(2) # vary value of n self.wait(2) self.play(n.animate.set_value(100), run_time=10) # plot CDF of standard normal distribution self.wait(2) self.play(Write(norm_graph_cdf)) self.wait(2) class CLT5(Scene): # scene 5: mathematical formulation def construct(self): assumption1 = MathTex( r"\textsf{Seien $(Y_i)_{i\in\mathbb{N}}$ u.i.v. mit }", r"Y_i\sim\textsf{Ber}(p).", ) assumption1_general = MathTex( r"\textsf{Seien $(Y_i)_{i\in\mathbb{N}}$ u.i.v. mit }", r"\textsf{E}[Y_i]=\mu\textsf{ und }\textsf{Var}(Y_i)=\sigma^2.", ) assumption2 = MathTex( r"S_n", "=", r"\sum\limits_{i=1}^nY_i", ) assumption3 = MathTex( r"S_n^*", "=", r"\frac{S_n-\textsf{E}[S_n]}{\sqrt{\textsf{Var}(S_n)}}", ) assumption4 = MathTex( r"S_n^*", "=", r"\frac{S_n-np}{\sqrt{\textsf{Var}(S_n)}}", ) assumption5 = MathTex( r"S_n^*", "=", r"\frac{S_n-np}{\sqrt{np(1-p)}}", ) assumption5_general = MathTex( r"S_n^*", "=", r"\frac{S_n-n\mu}{\sqrt{n\sigma^2}}", ) assumption6 = MathTex(r"Z\sim\textsf{N}(0,1)") assumption1.set(color=ORCAblue).to_edge(LEFT).shift(UP * 3) assumption1_general.set(color=ORCAblue).to_edge(LEFT).shift(UP * 3) assumption2.set(color=ORCAblue).to_edge(LEFT).shift(UP) assumption3.set(color=ORCAblue).shift(UP) assumption4.set(color=ORCAblue).shift(UP) assumption5.set(color=ORCAblue).shift(UP) assumption5_general.set(color=ORCAblue).shift(UP) assumption6.set(color=ORCAblue).to_edge(RIGHT).shift(UP) clt_text = Tex( r"\textbf{\textsf{Zentraler Grenzwertsatz}}", r"\textbf{\textsf{ (von de Moivre-Laplace)}}", ) clt_text.set(color=ORCAblue).to_edge(DOWN).shift(UP * 2) clt_formula = MathTex( r"F_{S_n^*}(t)", "=", r"P(S_n^*\leq t)", r"\xrightarrow{n\to\infty}", r"P(Z\leq t)", "=", r"\Phi(t)", ) clt_formula.set(color=ORCAblue).to_edge(DOWN).shift(UP) clt_box = SurroundingRectangle( VGroup(clt_text, clt_formula), color=ORCAblue) self.wait(2) self.play(FadeIn(assumption1)) self.wait(2) self.play(FadeIn(assumption2)) self.wait(2) self.play(TransformMatchingShapes(assumption2.copy(), assumption3, transform_mismatches=False)) self.wait(2) self.play(TransformMatchingShapes( assumption3[2], assumption4[2], transform_mismatches=False)) self.wait(2) self.play(TransformMatchingShapes( assumption4[2], assumption5[2], transform_mismatches=False)) self.wait(2) self.play(FadeIn(clt_text)) self.wait(2) self.play(FadeIn(clt_formula[:3])) self.wait(2) self.play(FadeIn(clt_formula[3])) self.wait(2) self.play(FadeIn(clt_formula[4])) self.play(FadeIn(assumption6)) self.wait(2) self.play(FadeIn(clt_formula[5:])) self.wait(2) self.play(Create(clt_box)) self.wait(2) self.play( TransformMatchingShapes( assumption1[1], assumption1_general[1], transform_mismatches=False) ) self.wait(2) self.play( TransformMatchingShapes( assumption5[2], assumption5_general[2], transform_mismatches=False), FadeOut(clt_text[1]), ) self.wait(2)