marimo-team/marimo-learn
3
1# /// script2# requires-python = ">=3.10"3# dependencies = [4# "marimo",5# "matplotlib==3.10.8",6# "scipy==1.17.1",7# "numpy==2.4.3",8# "plotly==5.18.0",9# "wigglystuff==0.2.37",10# ]11# ///12 13import marimo14 15__generated_with = "0.18.4"16app = marimo.App(width="medium", app_title="Central Limit Theorem")17 18 19@app.cell(hide_code=True)20def _(mo):21 mo.md(r"""22 # Central Limit Theorem23 24 _This notebook is a computational companion to ["Probability for Computer Scientists"](https://chrispiech.github.io/probabilityForComputerScientists/en/part4/clt/), by Stanford professor Chris Piech._25 26 The central limit theorem is honestly mind-blowing — it's like magic that no matter what distribution you start with, the sampling distribution of means approaches a normal distribution as sample size increases.27 28 Mathematically, if we have:29 30 $X_1, X_2, \ldots, X_n$ as independent, identically distributed random variables with:31 32 - Mean: $\mu$33 - Variance: $\sigma^2 < \infty$34 35 Then as $n \to \infty$:36 37 $$\sqrt{n}\left(\frac{1}{n}\sum_{i=1}^{n}X_i - \mu\right) \xrightarrow{d} \mathcal{N}(0, \sigma^2)$$38 39 > _Note:_ The above LaTeX derivation is included as a reference. Credit for this formulation goes to the original source linked at the top of the notebook.40 """)41 return42 43 44@app.cell(hide_code=True)45def _(mo):46 mo.md(r"""47 ## Central Limit Theorem Statement48 49 There are two ways to state the central limit theorem:50 51 ### Sum Version52 53 Let $X_1, X_2, \dots, X_n$ be independent and identically distributed random variables. The sum of these random variables approaches a normal distribution as $n \rightarrow \infty$:54 55 $\sum_{i=1}^{n}X_i \sim \mathcal{N}(n \cdot \mu, n \cdot \sigma^2)$56 57 Where $\mu = E[X_i]$ and $\sigma^2 = \text{Var}(X_i)$. Since each $X_i$ is identically distributed, they share the same expectation and variance.58 59 ### Average Version60 61 Let $X_1, X_2, \dots, X_n$ be independent and identically distributed random variables. The average of these random variables approaches a normal distribution as $n \rightarrow \infty$:62 63 $\frac{1}{n}\sum_{i=1}^{n}X_i \sim \mathcal{N}(\mu, \frac{\sigma^2}{n})$64 65 Where $\mu = E[X_i]$ and $\sigma^2 = \text{Var}(X_i)$.66 67 The CLT is incredible because it applies to almost any distribution (as long as it has a finite mean and variance), regardless of its shape.68 """)69 return70 71 72@app.cell(hide_code=True)73def _(mo):74 mo.md(r"""75 ## Central Limit Theorem Intuition76 77 Let's explore what happens when you add random variables together. For example, what if we add 100 different uniform random variables?78 79 ```python80 from random import random81 82 def add_100_uniforms():83 total = 084 for i in range(100):85 # returns a sample from uniform(0, 1)86 x_i = random()87 total += x_i88 return total89 ```90 91 The value returned by this function will be a random variable. Click the button below to run the function and observe the resulting value of total:92 """)93 return94 95 96@app.cell(hide_code=True)97def _(mo):98 run_button = mo.ui.run_button(label="Run add_100_uniforms()")99 100 run_button.center()101 return (run_button,)102 103 104@app.cell(hide_code=True)105def _(mo, random, run_button):106 def add_100_uniforms():107 total = 0108 for i in range(100):109 # returns a sample from uniform(0, 1)110 x_i = random.random() 111 total += x_i112 return total113 114 # Display the result when the button is clicked115 if run_button.value:116 uniform_result = add_100_uniforms()117 display = mo.md(f"**total**: {uniform_result:.5f}")118 else:119 display = mo.md("")120 121 display122 return (add_100_uniforms,)123 124 125@app.cell(hide_code=True)126def _(mo):127 mo.md(r"""128 What does total look like as a distribution? Let's calculate total many times and visualize the histogram of values it produces.129 """)130 return131 132 133@app.cell(hide_code=True)134def _(mo):135 # Simulation control136 run_simulation_button = mo.ui.button(137 value=0, 138 on_click=lambda value: value + 1, 139 label="Run 10,000 more samples", 140 kind="warn"141 )142 143 run_simulation_button.center()144 return (run_simulation_button,)145 146 147@app.cell(hide_code=True)148def _(add_100_uniforms, go, mo, np, run_simulation_button, stats, time):149 # store the results150 def get_simulation_results():151 if not hasattr(get_simulation_results, "results"):152 get_simulation_results.results = []153 get_simulation_results.last_button_value = -1 # track button clicks154 return get_simulation_results155 156 # grab the results157 sim_storage = get_simulation_results()158 simulation_results = sim_storage.results159 160 # Check if button was clicked (value changed)161 if run_simulation_button.value != sim_storage.last_button_value:162 # Update the last seen button value163 sim_storage.last_button_value = run_simulation_button.value164 165 with mo.status.spinner(title="Running simulation...") as progress_status:166 sim_count = 10000167 new_results = []168 for _ in mo.status.progress_bar(range(sim_count)):169 sim_result = add_100_uniforms()170 new_results.append(sim_result)171 time.sleep(0.0001) # tiny pause172 173 simulation_results.extend(new_results)174 175 progress_status.update(f"✅ Added {sim_count:,} samples (total: {len(simulation_results):,})")176 177 if simulation_results:178 # Numbers179 mean = np.mean(simulation_results)180 std_dev = np.std(simulation_results)181 182 theoretical_mean = 100 * 0.5 # = 50183 theoretical_variance = 100 * (1/12) # = 8.33...184 theoretical_std = np.sqrt(theoretical_variance) # ≈ 2.89185 186 # should be 10k times the click number (mainly for the y-axis label)187 total_samples = run_simulation_button.value * 10000188 189 fig = go.Figure()190 191 # histogram of samples192 fig.add_trace(go.Histogram(193 x=simulation_results,194 histnorm='probability density',195 name='Sum Distribution',196 marker_color='royalblue',197 opacity=0.7198 ))199 200 x_vals = np.linspace(min(simulation_results), max(simulation_results), 1000)201 y_vals = stats.norm.pdf(x_vals, theoretical_mean, theoretical_std)202 203 fig.add_trace(go.Scatter(204 x=x_vals,205 y=y_vals,206 mode='lines',207 name='Normal approximation',208 line=dict(color='red', width=2)209 ))210 211 fig.add_vline(212 x=mean, 213 line_dash="dash", 214 line_width=1.5,215 line_color="green",216 annotation_text=f"Sample Mean: {mean:.2f}",217 annotation_position="top right"218 )219 220 # some notes221 fig.add_annotation(222 x=0.02, y=0.95,223 xref="paper", yref="paper",224 text=f"Sum of 100 Uniform(0,1) variables<br>" +225 f"Sample size: {total_samples:,}<br>" +226 f"Sample mean: {mean:.2f} (expected: {theoretical_mean})<br>" +227 f"Sample std: {std_dev:.2f} (expected: {theoretical_std:.2f})<br>" +228 f"According to CLT: Normal({theoretical_mean}, {theoretical_variance:.2f})",229 showarrow=False,230 align="left",231 bgcolor="white",232 opacity=0.8233 )234 235 fig.update_layout(236 title=f'Distribution of Sum of 100 Uniforms (Click #{run_simulation_button.value})',237 xaxis_title='Values',238 yaxis_title=f'Probability Density ({total_samples:,} runs)',239 template='plotly_white',240 height=500241 )242 243 # show244 histogram = mo.ui.plotly(fig)245 else:246 histogram = mo.md("Click the button to run the simulation!")247 248 # display249 histogram250 return251 252 253@app.cell(hide_code=True)254def _(mo):255 mo.md(r"""256 That is interesting! The sum of 100 independent uniforms looks normal. Is that a special property of uniforms? No! It turns out to work for almost any type of distribution (as long as the distribution has finite mean and variance).257 258 - Sum of 40 $X_i$ where $X_i \sim \text{Beta}(a = 5, b = 4)$? Normal.259 - Sum of 90 $X_i$ where $X_i \sim \text{Poisson}(\lambda = 4)$? Normal.260 - Sum of 50 dice-rolls? Normal.261 - Average of 10000 $X_i$ where $X_i \sim \text{Exp}(\lambda = 8)$? Normal.262 263 For any distribution, the sum or average of a sufficiently large number of independent, identically distributed random variables will be approximately normally distributed.264 """)265 return266 267 268@app.cell(hide_code=True)269def _(mo):270 mo.md(r"""271 ## Continuity Correction272 273 When using the Central Limit Theorem with discrete random variables (like a Binomial or Poisson), we need to apply a continuity correction. This is because we're approximating a discrete distribution with a continuous one (normal).274 275 The continuity correction involves adjusting the boundaries in probability calculations by ±0.5 to account for the discrete nature of the original variable.276 277 You should use a continuity correction any time your normal is approximating a discrete random variable. The rules for a general continuity correction are the same as the rules for the [binomial-approximation continuity correction](http://marimo.app/https://github.com/marimo-team/learn/blob/main/probability/14_binomial_distribution.py).278 279 In our example above, where we added 100 uniforms, a continuity correction isn't needed because the sum of uniforms is continuous. However, in examples with dice or other discrete distributions, a continuity correction would be necessary.280 """)281 return282 283 284@app.cell(hide_code=True)285def _(mo):286 mo.md(r"""287 ## Examples288 289 Let's work through some practical examples to see how the Central Limit Theorem is applied.290 """)291 return292 293 294@app.cell(hide_code=True)295def _(mo):296 mo.md(r"""297 ### Example 1: Dice Game298 299 > _Note:_ The following application demonstrates the practical use of the Central Limit Theorem. The mathematical derivation is based on concepts from ["Probability for Computer Scientists"](https://chrispiech.github.io/probabilityForComputerScientists/en/part2/clt/) by Chris Piech.300 301 Let's solve a fun probability problem: You roll a 6-sided die 10 times and let $X$ represent the total value of all 10 dice: $X = X_1 + X_2 + \dots + X_{10}$. You win if $X \leq 25$ or $X \geq 45$. What's your probability of winning?302 303 For a single die roll $X_i$, we know:304 - $E[X_i] = 3.5$305 - $\text{Var}(X_i) = \frac{35}{12}$306 307 **Solution Approach:**308 309 This is where the Central Limit Theorem shines! Since we're summing 10 independent, identically distributed random variables, we can approximate this sum with a normal distribution $Y$:310 311 $Y \sim \mathcal{N}(10 \cdot E[X_i], 10 \cdot \text{Var}(X_i)) = \mathcal{N}(35, 29.2)$312 313 Now calculating our winning probability:314 315 $P(X \leq 25 \text{ or } X \geq 45) = P(X \leq 25) + P(X \geq 45)$316 317 Since we're approximating a discrete distribution with a continuous one, we apply a continuity correction:318 319 $\approx P(Y < 25.5) + P(Y > 44.5) = P(Y < 25.5) + [1 - P(Y < 44.5)]$320 321 Converting to standard normal form:322 323 $\approx \Phi\left(\frac{25.5 - 35}{\sqrt{29.2}}\right) + \left[1 - \Phi\left(\frac{44.5 - 35}{\sqrt{29.2}}\right)\right]$324 325 $\approx \Phi(-1.76) + [1 - \Phi(1.76)]$326 327 $\approx 0.039 + (1 - 0.961) \approx 0.078$328 329 So your chance of winning is about 7.8% — not great odds, but that's probability for you!330 """)331 return332 333 334@app.cell(hide_code=True)335def _(create_dice_game_visualization, fig_to_image, mo):336 # Display visualization337 dice_game_fig = create_dice_game_visualization()338 dice_game_image = mo.image(fig_to_image(dice_game_fig), width="100%")339 340 dice_explanation = mo.md(341 r"""342 **Understanding the Visualization:**343 344 This graph shows our dice game in action. The blue bars represent the exact probability distribution for summing 10 dice, while the red curve shows our normal approximation from the Central Limit Theorem.345 346 I've highlighted the winning regions in orange:347 - The left region where $X \leq 25$348 - The right region where $X \geq 45$349 350 Together these regions cover about 7.8% of the total probability.351 352 What's fascinating here is how closely the normal curve approximates the actual discrete distribution — this is the Central Limit Theorem working its magic, even with just 10 random variables.353 """354 )355 356 mo.vstack([dice_game_image, dice_explanation])357 return358 359 360@app.cell(hide_code=True)361def _(mo):362 mo.md(r"""363 ### Example 2: Algorithm Runtime Estimation364 365 > _Note:_ The following derivation demonstrates the practical application of the Central Limit Theorem for experimental design. The mathematical approach is based on concepts from ["Probability for Computer Scientists"](https://chrispiech.github.io/probabilityForComputerScientists/en/part2/clt/) by Chris Piech.366 367 Here's a practical problem I encounter in performance testing: You've developed a new algorithm and want to measure its average runtime. You know the variance is $\sigma^2 = 4 \text{ sec}^2$, but need to estimate the true mean runtime $t$.368 369 The question: How many test runs do you need to be 95% confident your estimated mean is within ±0.5 seconds of the true value?370 371 Let $X_i$ represent the runtime of the $i$-th test (for $1 \leq i \leq n$).372 373 **Solution:**374 375 We need to find $n$ such that:376 377 $0.95 = P\left(-0.5 \leq \frac{\sum_{i=1}^n X_i}{n} - t \leq 0.5\right)$378 379 The Central Limit Theorem tells us that as $n$ increases, the sample mean approaches a normal distribution. Let's standardize this to work with the standard normal distribution:380 381 $Z = \frac{\left(\sum_{i=1}^n X_i\right) - n\mu}{\sigma \sqrt{n}} = \frac{\left(\sum_{i=1}^n X_i\right) - nt}{2 \sqrt{n}}$382 383 Rewriting our probability constraint in terms of $Z$:384 385 $0.95 = P\left(-0.5 \leq \frac{\sum_{i=1}^n X_i}{n} - t \leq 0.5\right) = P\left(\frac{-0.5 \sqrt{n}}{2} \leq Z \leq \frac{0.5 \sqrt{n}}{2}\right)$386 387 Using the properties of the standard normal CDF:388 389 $0.95 = \Phi\left(\frac{\sqrt{n}}{4}\right) - \Phi\left(-\frac{\sqrt{n}}{4}\right) = 2\Phi\left(\frac{\sqrt{n}}{4}\right) - 1$390 391 Solving for $\Phi\left(\frac{\sqrt{n}}{4}\right)$:392 393 $0.975 = \Phi\left(\frac{\sqrt{n}}{4}\right)$394 395 Using the inverse CDF:396 397 $\Phi^{-1}(0.975) = \frac{\sqrt{n}}{4}$398 399 $1.96 = \frac{\sqrt{n}}{4}$400 401 $n = 61.4$402 403 Rounding up, we need 62 test runs to achieve our desired confidence interval — a practical result we can immediately apply to our testing protocol.404 """)405 return406 407 408@app.cell(hide_code=True)409def _(create_algorithm_runtime_visualization, fig_to_image, mo):410 # Display visualization411 runtime_fig = create_algorithm_runtime_visualization()412 runtime_image = mo.image(fig_to_image(runtime_fig), width="100%")413 414 runtime_explanation = mo.md(415 r"""416 **Visualization Explanation:**417 418 The graph illustrates how the standard error of the mean (SEM) decreases as the number of trials increases. The standard error is calculated as $\frac{\sigma}{\sqrt{n}}$.419 420 - When we conduct 62 trials, the standard error is approximately 0.254 seconds.421 - With a 95% confidence level, this gives us a margin of error of about ±0.5 seconds (1.96 × 0.254 ≈ 0.5).422 - The shaded region shows how the confidence interval narrows as the number of trials increases.423 424 This demonstrates why 62 trials are sufficient to meet our requirements of estimating the mean runtime within ±0.5 seconds with 95% confidence.425 """426 )427 428 mo.vstack([runtime_image, runtime_explanation])429 return430 431 432@app.cell(hide_code=True)433def _(mo):434 mo.md(r"""435 ## Interactive CLT Explorer436 437 Let's explore how the Central Limit Theorem works with different underlying distributions. You can select a distribution type and see how the distribution of the sample mean changes as the sample size increases.438 """)439 return440 441 442@app.cell(hide_code=True)443def _(controls):444 controls445 return446 447 448@app.cell(hide_code=True)449def _(450 distribution_type,451 fig_to_image,452 mo,453 np,454 plt,455 run_explorer_button,456 sample_size,457 sim_count_slider,458 stats,459):460 # Run simulation when button is clicked461 if run_explorer_button.value:462 # Set distribution parameters based on selection463 if distribution_type.value == "uniform":464 dist_name = "Uniform(0, 1)"465 # For uniform(0,1): mean = 0.5, variance = 1/12466 true_mean = 0.5467 true_var = 1/12468 469 # generate samples470 def generate_sample():471 return np.random.uniform(0, 1, sample_size.value)472 473 elif distribution_type.value == "exponential":474 rate = 1.0475 dist_name = f"Exponential(λ={rate})"476 # For exponential(λ): mean = 1/λ, variance = 1/λ²477 true_mean = 1/rate478 true_var = 1/(rate**2)479 480 def generate_sample():481 return np.random.exponential(1/rate, sample_size.value)482 483 elif distribution_type.value == "binomial":484 n_param, p = 10, 0.3485 dist_name = f"Binomial(n={n_param}, p={p})"486 # For binomial(n,p): mean = np, variance = np(1-p)487 true_mean = n_param * p488 true_var = n_param * p * (1-p)489 490 def generate_sample():491 return np.random.binomial(n_param, p, sample_size.value)492 493 elif distribution_type.value == "poisson":494 rate = 3.0495 dist_name = f"Poisson(λ={rate})"496 # For poisson(λ): mean = λ, variance = λ497 true_mean = rate498 true_var = rate499 500 def generate_sample():501 return np.random.poisson(rate, sample_size.value)502 503 # Generate the simulation data using a spinner for progress504 with mo.status.spinner(title="Running simulation...") as explorer_progress:505 sample_means = []506 original_samples = []507 508 # Run simulations509 for _ in mo.status.progress_bar(range(sim_count_slider.value)):510 sample = generate_sample()511 512 # Store the first simulation's individual values for visualizing original distribution513 if len(original_samples) < 1000: # limit to prevent memory issues514 original_samples.extend(sample)515 516 # sample mean517 sample_means.append(np.mean(sample))518 519 # progress520 explorer_progress.update(f"✅ Completed {sim_count_slider.value:,} simulations")521 522 # Create visualization523 explorer_fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))524 525 # Original distribution histogram526 ax1.hist(original_samples, bins=30, density=True, alpha=0.7, color='royalblue')527 ax1.set_title(f"Original Distribution: {dist_name}")528 529 # Theoretical mean line530 ax1.axvline(x=true_mean, color='red', linestyle='--', 531 label=f'True Mean = {true_mean:.3f}')532 533 ax1.set_xlabel("Value")534 ax1.set_ylabel("Density")535 ax1.legend()536 537 # Sample means histogram and normal approximation538 sample_mean_mean = np.mean(sample_means)539 sample_mean_std = np.std(sample_means)540 expected_std = np.sqrt(true_var / sample_size.value) # CLT prediction541 542 ax2.hist(sample_means, bins=30, density=True, alpha=0.7, color='forestgreen',543 label=f'Sample Size = {sample_size.value}')544 545 # Normal approximation from CLT546 explorer_x = np.linspace(min(sample_means), max(sample_means), 1000)547 explorer_y = stats.norm.pdf(explorer_x, true_mean, expected_std)548 ax2.plot(explorer_x, explorer_y, 'r-', linewidth=2, label='CLT Normal Approximation')549 550 # Add mean line551 ax2.axvline(x=true_mean, color='purple', linestyle='--',552 label=f'True Mean = {true_mean:.3f}')553 554 ax2.set_title(f"Distribution of Sample Means\n(CLT Prediction: N({true_mean:.3f}, {true_var/sample_size.value:.5f}))")555 ax2.set_xlabel("Sample Mean")556 ax2.set_ylabel("Density")557 ax2.legend()558 559 # Add CLT description560 explorer_fig.text(0.5, 0.01, 561 f"Central Limit Theorem: As sample size increases, the distribution of sample means approaches\n" +562 f"a normal distribution with mean = {true_mean:.3f} and variance = {true_var:.3f}/{sample_size.value} = {true_var/sample_size.value:.5f}",563 ha='center', fontsize=10, bbox=dict(facecolor='white', alpha=0.8))564 565 plt.tight_layout(rect=[0, 0.05, 1, 1])566 567 # Display plot568 explorer_image = mo.image(fig_to_image(explorer_fig), width="100%")569 else:570 explorer_image = mo.md("Click the 'Run Simulation' button to see how the Central Limit Theorem works.")571 572 explorer_image573 return574 575 576@app.cell(hide_code=True)577def _(mo):578 mo.md(r"""579 ## 🤔 Test Your Understanding580 581 /// details | What is the shape of the distribution of the sum of many independent random variables?582 The sum of many independent random variables approaches a normal distribution, regardless of the shape of the original distributions (as long as they have finite mean and variance). This is the essence of the Central Limit Theorem.583 ///584 585 /// details | If $X_1, X_2, \dots, X_{100}$ are IID random variables with $E[X_i] = 5$ and $Var(X_i) = 9$, what is the distribution of their sum?586 By the Central Limit Theorem, the sum $S = X_1 + X_2 + \dots + X_{100}$ follows a normal distribution with:587 588 - Mean: $E[S] = 100 \cdot E[X_i] = 100 \cdot 5 = 500$589 - Variance: $Var(S) = 100 \cdot Var(X_i) = 100 \cdot 9 = 900$590 591 Therefore, $S \sim \mathcal{N}(500, 900)$, or equivalently $S \sim \mathcal{N}(500, 30^2)$.592 ///593 594 /// details | When do you need to apply a continuity correction when using the Central Limit Theorem?595 You need to apply a continuity correction when you're using the normal approximation (through CLT) for a discrete random variable.596 597 For example, when approximating a binomial or Poisson distribution with a normal distribution, you should adjust boundaries by ±0.5 to account for the discrete nature of the original variable. This makes the approximation more accurate.598 ///599 600 /// details | If $X_1, X_2, \dots, X_{n}$ are IID random variables, how does the variance of their sample mean $\bar{X} = \frac{1}{n}\sum_{i=1}^{n}X_i$ change as $n$ increases?601 The variance of the sample mean decreases as the sample size $n$ increases. Specifically:602 603 $Var(\bar{X}) = \frac{Var(X_i)}{n}$604 605 This means that as we take more samples, the sample mean becomes more concentrated around the true mean of the distribution. This is why larger samples give more precise estimates.606 ///607 608 /// details | Why is the Central Limit Theorem so important in statistics?609 The Central Limit Theorem is foundational in statistics because:610 611 1. It allows us to make inferences about population parameters using sample statistics, regardless of the population's distribution.612 2. It explains why the normal distribution appears so frequently in natural phenomena.613 3. It enables the construction of confidence intervals and hypothesis tests for means, even when the underlying population distribution is unknown.614 4. It justifies many statistical methods that assume normality, even when working with non-normal data, provided the sample size is large enough.615 616 In essence, the CLT provides the theoretical justification for much of statistical inference.617 ///618 """)619 return620 621 622@app.cell(hide_code=True)623def _(mo):624 mo.md(r"""625 ## Appendix (helper code and functions)626 """)627 return628 629 630@app.cell631def _():632 import marimo as mo633 return (mo,)634 635 636@app.cell(hide_code=True)637def _():638 from wigglystuff import TangleSlider639 return640 641 642@app.cell(hide_code=True)643def _():644 # Import libraries645 import numpy as np646 import matplotlib.pyplot as plt647 from scipy import stats648 import io649 import base64650 import random651 import time652 import plotly.graph_objects as go653 import plotly.io as pio654 return base64, go, io, np, plt, random, stats, time655 656 657@app.cell(hide_code=True)658def _(base64, io):659 from matplotlib.figure import Figure660 661 # Helper function to convert matplotlib figures to images662 def fig_to_image(fig):663 buf = io.BytesIO()664 fig.savefig(buf, format='png', bbox_inches='tight')665 buf.seek(0)666 img_str = base64.b64encode(buf.getvalue()).decode('utf-8')667 return f"data:image/png;base64,{img_str}"668 return (fig_to_image,)669 670 671@app.cell(hide_code=True)672def _(np, plt, stats):673 def create_dice_game_visualization():674 """Create a visualization for the dice game example."""675 # Parameters676 n_dice = 10677 dice_values = np.arange(1, 7) # 1 to 6678 679 # Theoretical values680 single_die_mean = np.mean(dice_values) # 3.5681 single_die_var = np.var(dice_values) # 35/12682 683 # Sum distribution parameters684 sum_mean = n_dice * single_die_mean685 sum_var = n_dice * single_die_var686 sum_std = np.sqrt(sum_var)687 688 # Possible outcomes for the sum of 10 dice689 min_sum = n_dice * min(dice_values) # 10690 max_sum = n_dice * max(dice_values) # 60691 sum_values = np.arange(min_sum, max_sum + 1)692 693 # Create figure694 fig, ax = plt.subplots(figsize=(10, 6))695 696 # Calculate PMF through convolution697 # For one die698 single_pmf = np.ones(6) / 6699 700 sum_pmf = single_pmf.copy()701 for _ in range(n_dice - 1):702 sum_pmf = np.convolve(sum_pmf, single_pmf)703 704 # Plot the PMF705 ax.bar(sum_values, sum_pmf, alpha=0.7, color='royalblue', label='Exact PMF')706 707 # Normal approximation708 x = np.linspace(min_sum - 5, max_sum + 5, 1000)709 y = stats.norm.pdf(x, sum_mean, sum_std)710 ax.plot(x, y, 'r-', linewidth=2, label='Normal Approximation')711 712 # Win conditions (x ≤ 25 or x ≥ 45)713 win_region_left = sum_values <= 25714 win_region_right = sum_values >= 45715 716 # Shade win regions717 ax.bar(sum_values[win_region_left], sum_pmf[win_region_left], 718 color='darkorange', alpha=0.7, label='Win Region')719 ax.bar(sum_values[win_region_right], sum_pmf[win_region_right], 720 color='darkorange', alpha=0.7)721 722 # Calculate win probability723 win_prob = np.sum(sum_pmf[win_region_left]) + np.sum(sum_pmf[win_region_right])724 725 # Add vertical lines for critical values726 ax.axvline(x=25.5, color='red', linestyle='--', linewidth=1.5, label='Critical Points')727 ax.axvline(x=44.5, color='red', linestyle='--', linewidth=1.5)728 729 # Add mean line730 ax.axvline(x=sum_mean, color='green', linestyle='--', linewidth=1.5, 731 label=f'Mean = {sum_mean}')732 733 # Text box with relevant information734 textstr = '\n'.join((735 f'Number of dice: {n_dice}',736 f'Sum Mean: {sum_mean}',737 f'Sum Std Dev: {sum_std:.2f}',738 f'Win Probability: {win_prob:.4f}',739 f'CLT Approximation: {0.078:.4f}'740 ))741 props = dict(boxstyle='round', facecolor='wheat', alpha=0.5)742 ax.text(0.05, 0.95, textstr, transform=ax.transAxes, fontsize=10,743 verticalalignment='top', bbox=props)744 745 # Formatting746 ax.set_xlabel('Sum of 10 Dice')747 ax.set_ylabel('Probability')748 ax.set_title('Central Limit Theorem: Dice Game Example')749 ax.legend()750 ax.grid(alpha=0.3)751 752 plt.tight_layout()753 plt.gca()754 return fig755 return (create_dice_game_visualization,)756 757 758@app.cell(hide_code=True)759def _(np, plt):760 def create_algorithm_runtime_visualization():761 """Create a visualization for the algorithm runtime example."""762 # Parameters763 variance = 4 # σ² = 4 sec²764 std_dev = np.sqrt(variance) # σ = 2 sec765 confidence_level = 0.95766 z_score = 1.96 # for 95% confidence767 target_error = 0.5 # ±0.5 seconds768 769 # Calculate n needed for desired precision770 n_required = int(np.ceil((z_score * std_dev / target_error) ** 2)) # ≈ 62771 772 n_values = np.arange(1, 100)773 774 # standard error775 standard_errors = std_dev / np.sqrt(n_values)776 777 # margin of error778 margins_of_error = z_score * standard_errors779 780 # Create figure781 fig, ax = plt.subplots(figsize=(10, 6))782 783 # standard error vs sample size plot784 ax.plot(n_values, standard_errors, 'b-', linewidth=2, label='Standard Error of Mean')785 786 # Plot margin of error vs sample size787 ax.plot(n_values, margins_of_error, 'r--', linewidth=2, 788 label=f'{confidence_level*100}% Margin of Error')789 790 ax.axvline(x=n_required, color='green', linestyle='-', linewidth=1.5,791 label=f'Required n = {n_required}')792 793 ax.axhline(y=target_error, color='purple', linestyle='--', linewidth=1.5,794 label=f'Target Error = ±{target_error} sec')795 796 # Shade the region below target error797 ax.fill_between(n_values, 0, target_error, alpha=0.2, color='green')798 799 # intersection point800 ax.plot(n_required, target_error, 'ro', markersize=8)801 ax.annotate(f'({n_required}, {target_error} sec)',802 xy=(n_required, target_error),803 xytext=(n_required + 5, target_error + 0.1),804 arrowprops=dict(facecolor='black', shrink=0.05, width=1))805 806 # Text box with appropriate information807 textstr = '\n'.join((808 f'Algorithm Variance: {variance} sec²',809 f'Standard Deviation: {std_dev} sec',810 f'Confidence Level: {confidence_level*100}%',811 f'Z-score: {z_score}',812 f'Target Error: ±{target_error} sec',813 f'Required Sample Size: {n_required}'814 ))815 props = dict(boxstyle='round', facecolor='wheat', alpha=0.5)816 ax.text(0.05, 0.95, textstr, transform=ax.transAxes, fontsize=10,817 verticalalignment='top', bbox=props)818 819 # Formatting820 ax.set_xlabel('Sample Size (n)')821 ax.set_ylabel('Error (seconds)')822 ax.set_title('Sample Size Determination for Algorithm Runtime Estimation')823 ax.set_xlim(0, 100)824 ax.set_ylim(0, 2)825 ax.legend()826 ax.grid(alpha=0.3)827 828 plt.tight_layout()829 return fig830 return (create_algorithm_runtime_visualization,)831 832 833@app.cell(hide_code=True)834def _(mo):835 mo.md(r"""836 ## Summary837 838 The Central Limit Theorem is truly one of the most remarkable ideas in all of statistics. It tells us that when we add up many independent random variables, their sum will follow a normal distribution, regardless of what the original distributions looked like. This is why we see normal distributions so often in real life – many natural phenomena are the result of numerous small, independent factors adding up.839 840 What makes the CLT so powerful is its universality. Whether we're working with dice rolls, measurement errors, or stock market returns, as long as we have enough independent samples, their average or sum will be approximately normal. For sums, the distribution will be $\mathcal{N}(n\mu, n\sigma^2)$, and for averages, it's $\mathcal{N}(\mu, \frac{\sigma^2}{n})$.841 842 The CLT gives us the foundation for confidence intervals, hypothesis testing, and many other statistical tools. Without it, we'd have a much harder time making sense of data when we don't know the underlying population distribution. Just remember that if you're working with discrete distributions, you'll need to apply a continuity correction to get more accurate results.843 844 Next time you see a normal distribution in data, think about the Central Limit Theorem – it might be the reason behind that familiar bell curve!845 """)846 return847 848 849@app.cell(hide_code=True)850def _(mo):851 # controls for the interactive explorer852 distribution_type = mo.ui.dropdown(853 options=["uniform", "exponential", "binomial", "poisson"],854 value="uniform",855 label="Distribution Type"856 )857 858 sample_size = mo.ui.slider(859 start =1,860 stop =100,861 step=1,862 value=30,863 label="Sample Size (n)"864 )865 866 sim_count_slider = mo.ui.slider(867 start =100,868 stop =10000,869 step=100,870 value=1000,871 label="Number of Simulations"872 )873 874 run_explorer_button = mo.ui.run_button(label="Run Simulation", kind="warn")875 876 controls = mo.hstack([877 mo.vstack([distribution_type, sample_size, sim_count_slider]),878 run_explorer_button879 ], justify='space-around')880 return (881 controls,882 distribution_type,883 run_explorer_button,884 sample_size,885 sim_count_slider,886 )887 888 889if __name__ == "__main__":890 app.run()891 