akshayka commited on
Commit
1cd4542
·
unverified ·
2 Parent(s): b0625d6 163af44

Merge pull request #76 from marimo-team/haleshot/015_poisson

Browse files
probability/15_poisson_distribution.py ADDED
@@ -0,0 +1,805 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = [
4
+ # "marimo",
5
+ # "matplotlib==3.10.0",
6
+ # "numpy==2.2.4",
7
+ # "scipy==1.15.2",
8
+ # "altair==5.2.0",
9
+ # "wigglystuff==0.1.10",
10
+ # "pandas==2.2.3",
11
+ # ]
12
+ # ///
13
+
14
+ import marimo
15
+
16
+ __generated_with = "0.11.25"
17
+ app = marimo.App(width="medium", app_title="Poisson Distribution")
18
+
19
+
20
+ @app.cell(hide_code=True)
21
+ def _(mo):
22
+ mo.md(
23
+ r"""
24
+ # Poisson Distribution
25
+
26
+ _This notebook is a computational companion to ["Probability for Computer Scientists"](https://chrispiech.github.io/probabilityForComputerScientists/en/part2/poisson/), by Stanford professor Chris Piech._
27
+
28
+ A Poisson random variable gives the probability of a given number of events in a fixed interval of time (or space). It makes the Poisson assumption that events occur with a known constant mean rate and independently of the time since the last event.
29
+ """
30
+ )
31
+ return
32
+
33
+
34
+ @app.cell(hide_code=True)
35
+ def _(mo):
36
+ mo.md(
37
+ r"""
38
+ ## Poisson Random Variable Definition
39
+
40
+ $X \sim \text{Poisson}(\lambda)$ represents a Poisson random variable where:
41
+
42
+ - $X$ is our random variable (number of events)
43
+ - $\text{Poisson}$ indicates it follows a Poisson distribution
44
+ - $\lambda$ is the rate parameter (average number of events per time interval)
45
+
46
+ ```
47
+ X ~ Poisson(λ)
48
+ ↑ ↑ ↑
49
+ | | +-- Rate parameter:
50
+ | | average number of
51
+ | | events per interval
52
+ | +-- Indicates Poisson
53
+ | distribution
54
+ |
55
+ Our random variable
56
+ counting number of events
57
+ ```
58
+
59
+ The Poisson distribution is particularly useful when:
60
+
61
+ 1. Events occur independently of each other
62
+ 2. The average rate of occurrence is constant
63
+ 3. Two events cannot occur at exactly the same instant
64
+ 4. The probability of an event is proportional to the length of the time interval
65
+ """
66
+ )
67
+ return
68
+
69
+
70
+ @app.cell(hide_code=True)
71
+ def _(mo):
72
+ mo.md(
73
+ r"""
74
+ ## Properties of Poisson Distribution
75
+
76
+ | Property | Formula |
77
+ |----------|---------|
78
+ | Notation | $X \sim \text{Poisson}(\lambda)$ |
79
+ | Description | Number of events in a fixed time frame if (a) events occur with a constant mean rate and (b) they occur independently of time since last event |
80
+ | Parameters | $\lambda \in \mathbb{R}^{+}$, the constant average rate |
81
+ | Support | $x \in \{0, 1, \dots\}$ |
82
+ | PMF equation | $P(X=x) = \frac{\lambda^x e^{-\lambda}}{x!}$ |
83
+ | Expectation | $E[X] = \lambda$ |
84
+ | Variance | $\text{Var}(X) = \lambda$ |
85
+
86
+ Note that unlike many other distributions, the Poisson distribution's mean and variance are equal, both being $\lambda$.
87
+
88
+ Let's explore how the Poisson distribution changes with different rate parameters.
89
+ """
90
+ )
91
+ return
92
+
93
+
94
+ @app.cell(hide_code=True)
95
+ def _(TangleSlider, mo):
96
+ # interactive elements using TangleSlider
97
+ lambda_slider = mo.ui.anywidget(TangleSlider(
98
+ amount=5,
99
+ min_value=0.1,
100
+ max_value=20,
101
+ step=0.1,
102
+ digits=1,
103
+ suffix=" events"
104
+ ))
105
+
106
+ # interactive controls
107
+ _controls = mo.vstack([
108
+ mo.md("### Adjust the Rate Parameter to See How Poisson Distribution Changes"),
109
+ mo.hstack([
110
+ mo.md("**Rate parameter (λ):** "),
111
+ lambda_slider,
112
+ mo.md("**events per interval.** Higher values shift the distribution rightward and make it more spread out.")
113
+ ], justify="start"),
114
+ ])
115
+ _controls
116
+ return (lambda_slider,)
117
+
118
+
119
+ @app.cell(hide_code=True)
120
+ def _(lambda_slider, np, plt, stats):
121
+ def create_poisson_pmf_plot(lambda_value):
122
+ """Create a visualization of Poisson PMF with annotations for mean and variance."""
123
+ # PMF for values
124
+ max_x = max(20, int(lambda_value * 3)) # Show at least up to 3*lambda
125
+ x = np.arange(0, max_x + 1)
126
+ pmf = stats.poisson.pmf(x, lambda_value)
127
+
128
+ # Relevant key statistics
129
+ mean = lambda_value # For Poisson, mean = lambda
130
+ variance = lambda_value # For Poisson, variance = lambda
131
+ std_dev = np.sqrt(variance)
132
+
133
+ # plot
134
+ fig, ax = plt.subplots(figsize=(10, 6))
135
+
136
+ # PMF as bars
137
+ ax.bar(x, pmf, color='royalblue', alpha=0.7, label=f'PMF: P(X=k)')
138
+
139
+ # for the PMF values
140
+ ax.plot(x, pmf, 'ro-', alpha=0.6, label='PMF line')
141
+
142
+ # Vertical lines - mean and key values
143
+ ax.axvline(x=mean, color='green', linestyle='--', linewidth=2,
144
+ label=f'Mean: {mean:.2f}')
145
+
146
+ # Stdev region
147
+ ax.axvspan(mean - std_dev, mean + std_dev, alpha=0.2, color='green',
148
+ label=f'±1 Std Dev: {std_dev:.2f}')
149
+
150
+ ax.set_xlabel('Number of Events (k)')
151
+ ax.set_ylabel('Probability: P(X=k)')
152
+ ax.set_title(f'Poisson Distribution with λ={lambda_value:.1f}')
153
+
154
+ # annotations
155
+ ax.annotate(f'E[X] = {mean:.2f}',
156
+ xy=(mean, stats.poisson.pmf(int(mean), lambda_value)),
157
+ xytext=(mean + 1, max(pmf) * 0.8),
158
+ arrowprops=dict(facecolor='black', shrink=0.05, width=1))
159
+
160
+ ax.annotate(f'Var(X) = {variance:.2f}',
161
+ xy=(mean, stats.poisson.pmf(int(mean), lambda_value) / 2),
162
+ xytext=(mean + 1, max(pmf) * 0.6),
163
+ arrowprops=dict(facecolor='black', shrink=0.05, width=1))
164
+
165
+ ax.grid(alpha=0.3)
166
+ ax.legend()
167
+
168
+ plt.tight_layout()
169
+ return plt.gca()
170
+
171
+ # Get parameter from slider and create plot
172
+ _lambda = lambda_slider.amount
173
+ create_poisson_pmf_plot(_lambda)
174
+ return (create_poisson_pmf_plot,)
175
+
176
+
177
+ @app.cell(hide_code=True)
178
+ def _(mo):
179
+ mo.md(
180
+ r"""
181
+ ## Poisson Intuition: Relation to Binomial Distribution
182
+
183
+ The Poisson distribution can be derived as a limiting case of the [binomial distribution](http://marimo.app/https://github.com/marimo-team/learn/blob/main/probability/14_binomial_distribution.py).
184
+
185
+ Let's work on a practical example: predicting the number of ride-sharing requests in a specific area over a one-minute interval. From historical data, we know that the average number of requests per minute is $\lambda = 5$.
186
+
187
+ We could approximate this using a binomial distribution by dividing our minute into smaller intervals. For example, we can divide a minute into 60 seconds and treat each second as a [Bernoulli trial](http://marimo.app/https://github.com/marimo-team/learn/blob/main/probability/13_bernoulli_distribution.py) - either there's a request (success) or there isn't (failure).
188
+
189
+ Let's visualize this concept:
190
+ """
191
+ )
192
+ return
193
+
194
+
195
+ @app.cell(hide_code=True)
196
+ def _(fig_to_image, mo, plt):
197
+ def create_time_division_visualization():
198
+ # visualization of dividing a minute into 60 seconds
199
+ fig, ax = plt.subplots(figsize=(12, 2))
200
+
201
+ # Example events hardcoded at 2.75s and 7.12s
202
+ events = [2.75, 7.12]
203
+
204
+ # array of 60 rectangles
205
+ for i in range(60):
206
+ color = 'royalblue' if any(i <= e < i+1 for e in events) else 'lightgray'
207
+ ax.add_patch(plt.Rectangle((i, 0), 0.9, 1, color=color))
208
+
209
+ # markers for events
210
+ for e in events:
211
+ ax.plot(e, 0.5, 'ro', markersize=10)
212
+
213
+ # labels
214
+ ax.set_xlim(0, 60)
215
+ ax.set_ylim(0, 1)
216
+ ax.set_yticks([])
217
+ ax.set_xticks([0, 15, 30, 45, 60])
218
+ ax.set_xticklabels(['0s', '15s', '30s', '45s', '60s'])
219
+ ax.set_xlabel('Time (seconds)')
220
+ ax.set_title('One Minute Divided into 60 Second Intervals')
221
+
222
+ plt.tight_layout()
223
+ plt.gca()
224
+ return fig, events, i
225
+
226
+ # Create visualization and convert to image
227
+ _fig, _events, i = create_time_division_visualization()
228
+ _img = mo.image(fig_to_image(_fig), width="100%")
229
+
230
+ # explanation
231
+ _explanation = mo.md(
232
+ r"""
233
+ In this visualization:
234
+
235
+ - Each rectangle represents a 1-second interval
236
+ - Blue rectangles indicate intervals where an event occurred
237
+ - Red dots show the actual event times (2.75s and 7.12s)
238
+
239
+ If we treat this as a binomial experiment with 60 trials (seconds), we can calculate probabilities using the binomial PMF. But there's a problem: what if multiple events occur within the same second? To address this, we can divide our minute into smaller intervals.
240
+ """
241
+ )
242
+ mo.vstack([_fig, _explanation])
243
+ return create_time_division_visualization, i
244
+
245
+
246
+ @app.cell(hide_code=True)
247
+ def _(mo):
248
+ mo.md(
249
+ r"""
250
+ The total number of requests received over the minute can be approximated as the sum of the sixty indicator variables, which conveniently matches the description of a binomial — a sum of Bernoullis.
251
+
252
+ Specifically, if we define $X$ to be the number of requests in a minute, $X$ is a binomial with $n=60$ trials. What is the probability, $p$, of a success on a single trial? To make the expectation of $X$ equal the observed historical average $\lambda$, we should choose $p$ so that:
253
+
254
+ \begin{align}
255
+ \lambda &= E[X] && \text{Expectation matches historical average} \\
256
+ \lambda &= n \cdot p && \text{Expectation of a Binomial is } n \cdot p \\
257
+ p &= \frac{\lambda}{n} && \text{Solving for $p$}
258
+ \end{align}
259
+
260
+ In this case, since $\lambda=5$ and $n=60$, we should choose $p=\frac{5}{60}=\frac{1}{12}$ and state that $X \sim \text{Bin}(n=60, p=\frac{5}{60})$. Now we can calculate the probability of different numbers of requests using the binomial PMF:
261
+
262
+ $P(X = x) = {n \choose x} p^x (1-p)^{n-x}$
263
+
264
+ For example:
265
+
266
+ \begin{align}
267
+ P(X=1) &= {60 \choose 1} (5/60)^1 (55/60)^{60-1} \approx 0.0295 \\
268
+ P(X=2) &= {60 \choose 2} (5/60)^2 (55/60)^{60-2} \approx 0.0790 \\
269
+ P(X=3) &= {60 \choose 3} (5/60)^3 (55/60)^{60-3} \approx 0.1389
270
+ \end{align}
271
+
272
+ This is a good approximation, but it doesn't account for the possibility of multiple events in a single second. One solution is to divide our minute into even more fine-grained intervals. Let's try 600 deciseconds (tenths of a second):
273
+ """
274
+ )
275
+ return
276
+
277
+
278
+ @app.cell(hide_code=True)
279
+ def _(fig_to_image, mo, plt):
280
+ def create_decisecond_visualization(e_value):
281
+ # (Just showing the first 100 for clarity)
282
+ fig, ax = plt.subplots(figsize=(12, 2))
283
+
284
+ # Example events at 2.75s and 7.12s (convert to deciseconds)
285
+ events = [27.5, 71.2]
286
+
287
+ for i in range(100):
288
+ color = 'royalblue' if any(i <= event_val < i + 1 for event_val in events) else 'lightgray'
289
+ ax.add_patch(plt.Rectangle((i, 0), 0.9, 1, color=color))
290
+
291
+ # Markers for events
292
+ for event in events:
293
+ if event < 100: # Only show events in our visible range
294
+ ax.plot(event/10, 0.5, 'ro', markersize=10) # Divide by 10 to convert to deciseconds
295
+
296
+ # Add labels
297
+ ax.set_xlim(0, 100)
298
+ ax.set_ylim(0, 1)
299
+ ax.set_yticks([])
300
+ ax.set_xticks([0, 20, 40, 60, 80, 100])
301
+ ax.set_xticklabels(['0s', '2s', '4s', '6s', '8s', '10s'])
302
+ ax.set_xlabel('Time (first 10 seconds shown)')
303
+ ax.set_title('One Minute Divided into 600 Decisecond Intervals (first 100 shown)')
304
+
305
+ plt.tight_layout()
306
+ plt.gca()
307
+ return fig
308
+
309
+ # Create viz and convert to image
310
+ _fig = create_decisecond_visualization(e_value=5)
311
+ _img = mo.image(fig_to_image(_fig), width="100%")
312
+
313
+ # Explanation
314
+ _explanation = mo.md(
315
+ r"""
316
+ With $n=600$ and $p=\frac{5}{600}=\frac{1}{120}$, we can recalculate our probabilities:
317
+
318
+ \begin{align}
319
+ P(X=1) &= {600 \choose 1} (5/600)^1 (595/600)^{600-1} \approx 0.0333 \\
320
+ P(X=2) &= {600 \choose 2} (5/600)^2 (595/600)^{600-2} \approx 0.0837 \\
321
+ P(X=3) &= {600 \choose 3} (5/600)^3 (595/600)^{600-3} \approx 0.1402
322
+ \end{align}
323
+
324
+ As we make our intervals smaller (increasing $n$), our approximation becomes more accurate.
325
+ """
326
+ )
327
+ mo.vstack([_fig, _explanation])
328
+ return (create_decisecond_visualization,)
329
+
330
+
331
+ @app.cell(hide_code=True)
332
+ def _(mo):
333
+ mo.md(
334
+ r"""
335
+ ## The Binomial Distribution in the Limit
336
+
337
+ What happens if we continue dividing our time interval into smaller and smaller pieces? Let's explore how the probabilities change as we increase the number of intervals:
338
+ """
339
+ )
340
+ return
341
+
342
+
343
+ @app.cell(hide_code=True)
344
+ def _(mo):
345
+ intervals_slider = mo.ui.slider(
346
+ start = 60,
347
+ stop = 10000,
348
+ step=100,
349
+ value=600,
350
+ label="Number of intervals to divide a minute")
351
+ return (intervals_slider,)
352
+
353
+
354
+ @app.cell(hide_code=True)
355
+ def _(intervals_slider):
356
+ intervals_slider
357
+ return
358
+
359
+
360
+ @app.cell(hide_code=True)
361
+ def _(intervals_slider, np, pd, plt, stats):
362
+ def create_comparison_plot(n, lambda_value):
363
+ # Calculate probability
364
+ p = lambda_value / n
365
+
366
+ # Binomial probabilities
367
+ x_values = np.arange(0, 15)
368
+ binom_pmf = stats.binom.pmf(x_values, n, p)
369
+
370
+ # True Poisson probabilities
371
+ poisson_pmf = stats.poisson.pmf(x_values, lambda_value)
372
+
373
+ # DF for comparison
374
+ df = pd.DataFrame({
375
+ 'Events': x_values,
376
+ f'Binomial(n={n}, p={p:.6f})': binom_pmf,
377
+ f'Poisson(λ=5)': poisson_pmf,
378
+ 'Difference': np.abs(binom_pmf - poisson_pmf)
379
+ })
380
+
381
+ # Plot both PMFs
382
+ fig, ax = plt.subplots(figsize=(10, 6))
383
+
384
+ # Bar plot for the binomial
385
+ ax.bar(x_values - 0.2, binom_pmf, width=0.4, alpha=0.7,
386
+ color='royalblue', label=f'Binomial(n={n}, p={p:.6f})')
387
+
388
+ # Bar plot for the Poisson
389
+ ax.bar(x_values + 0.2, poisson_pmf, width=0.4, alpha=0.7,
390
+ color='crimson', label='Poisson(λ=5)')
391
+
392
+ # Labels and title
393
+ ax.set_xlabel('Number of Events (k)')
394
+ ax.set_ylabel('Probability')
395
+ ax.set_title(f'Comparison of Binomial and Poisson PMFs with n={n}')
396
+ ax.legend()
397
+ ax.set_xticks(x_values)
398
+ ax.grid(alpha=0.3)
399
+
400
+ plt.tight_layout()
401
+ return df, fig, n, p
402
+
403
+ # Number of intervals from the slider
404
+ n = intervals_slider.value
405
+ _lambda = 5 # Fixed lambda for our example
406
+
407
+ # Cromparison plot
408
+ df, fig, n, p = create_comparison_plot(n, _lambda)
409
+ return create_comparison_plot, df, fig, n, p
410
+
411
+
412
+ @app.cell(hide_code=True)
413
+ def _(df, fig, fig_to_image, mo, n, p):
414
+ # table of values
415
+ _styled_df = df.style.format({
416
+ f'Binomial(n={n}, p={p:.6f})': '{:.6f}',
417
+ f'Poisson(λ=5)': '{:.6f}',
418
+ 'Difference': '{:.6f}'
419
+ })
420
+
421
+ # Calculate the max absolute difference
422
+ _max_diff = df['Difference'].max()
423
+
424
+ # output
425
+ _chart = mo.image(fig_to_image(fig), width="100%")
426
+ _explanation = mo.md(f"**Maximum absolute difference between distributions: {_max_diff:.6f}**")
427
+ _table = mo.ui.table(df)
428
+
429
+ mo.vstack([_chart, _explanation, _table])
430
+ return
431
+
432
+
433
+ @app.cell(hide_code=True)
434
+ def _(mo):
435
+ mo.md(
436
+ r"""
437
+ As you can see from the interactive comparison above, as the number of intervals increases, the binomial distribution approaches the Poisson distribution! This is not a coincidence - the Poisson distribution is actually the limiting case of the binomial distribution when:
438
+
439
+ - The number of trials $n$ approaches infinity
440
+ - The probability of success $p$ approaches zero
441
+ - The product $np = \lambda$ remains constant
442
+
443
+ This relationship is why the Poisson distribution is so useful - it's easier to work with than a binomial with a very large number of trials and a very small probability of success.
444
+
445
+ ## Derivation of the Poisson PMF
446
+
447
+ Let's derive the Poisson PMF by taking the limit of the binomial PMF as $n \to \infty$. We start with:
448
+
449
+ $P(X=x) = \lim_{n \rightarrow \infty} {n \choose x} (\lambda / n)^x(1-\lambda/n)^{n-x}$
450
+
451
+ While this expression looks intimidating, it simplifies nicely:
452
+
453
+ \begin{align}
454
+ P(X=x)
455
+ &= \lim_{n \rightarrow \infty} {n \choose x} (\lambda / n)^x(1-\lambda/n)^{n-x}
456
+ && \text{Start: binomial in the limit}\\
457
+ &= \lim_{n \rightarrow \infty}
458
+ {n \choose x} \cdot
459
+ \frac{\lambda^x}{n^x} \cdot
460
+ \frac{(1-\lambda/n)^{n}}{(1-\lambda/n)^{x}}
461
+ && \text{Expanding the power terms} \\
462
+ &= \lim_{n \rightarrow \infty}
463
+ \frac{n!}{(n-x)!x!} \cdot
464
+ \frac{\lambda^x}{n^x} \cdot
465
+ \frac{(1-\lambda/n)^{n}}{(1-\lambda/n)^{x}}
466
+ && \text{Expanding the binomial term} \\
467
+ &= \lim_{n \rightarrow \infty}
468
+ \frac{n!}{(n-x)!x!} \cdot
469
+ \frac{\lambda^x}{n^x} \cdot
470
+ \frac{e^{-\lambda}}{(1-\lambda/n)^{x}}
471
+ && \text{Using limit rule } \lim_{n \rightarrow \infty}(1-\lambda/n)^{n} = e^{-\lambda}\\
472
+ &= \lim_{n \rightarrow \infty}
473
+ \frac{n!}{(n-x)!x!} \cdot
474
+ \frac{\lambda^x}{n^x} \cdot
475
+ \frac{e^{-\lambda}}{1}
476
+ && \text{As } n \to \infty \text{, } \lambda/n \to 0\\
477
+ &= \lim_{n \rightarrow \infty}
478
+ \frac{n!}{(n-x)!} \cdot
479
+ \frac{1}{x!} \cdot
480
+ \frac{\lambda^x}{n^x} \cdot
481
+ e^{-\lambda}
482
+ && \text{Rearranging terms}\\
483
+ &= \lim_{n \rightarrow \infty}
484
+ \frac{n^x}{1} \cdot
485
+ \frac{1}{x!} \cdot
486
+ \frac{\lambda^x}{n^x} \cdot
487
+ e^{-\lambda}
488
+ && \text{As } n \to \infty \text{, } \frac{n!}{(n-x)!} \approx n^x\\
489
+ &= \lim_{n \rightarrow \infty}
490
+ \frac{\lambda^x}{x!} \cdot
491
+ e^{-\lambda}
492
+ && \text{Canceling } n^x\\
493
+ &=
494
+ \frac{\lambda^x \cdot e^{-\lambda}}{x!}
495
+ && \text{Simplifying}\\
496
+ \end{align}
497
+
498
+ This gives us our elegant Poisson PMF formula: $P(X=x) = \frac{\lambda^x \cdot e^{-\lambda}}{x!}$
499
+ """
500
+ )
501
+ return
502
+
503
+
504
+ @app.cell(hide_code=True)
505
+ def _(mo):
506
+ mo.md(
507
+ r"""
508
+ ## Poisson Distribution in Python
509
+
510
+ Python's `scipy.stats` module provides functions to work with the Poisson distribution. Let's see how to calculate probabilities and generate random samples.
511
+
512
+ First, let's calculate some probabilities for our ride-sharing example with $\lambda = 5$:
513
+ """
514
+ )
515
+ return
516
+
517
+
518
+ @app.cell
519
+ def _(stats):
520
+ _lambda = 5
521
+
522
+ # Calculate probabilities for X = 1, 2, 3
523
+ p_1 = stats.poisson.pmf(1, _lambda)
524
+ p_2 = stats.poisson.pmf(2, _lambda)
525
+ p_3 = stats.poisson.pmf(3, _lambda)
526
+
527
+ print(f"P(X=1) = {p_1:.5f}")
528
+ print(f"P(X=2) = {p_2:.5f}")
529
+ print(f"P(X=3) = {p_3:.5f}")
530
+
531
+ # Calculate cumulative probability P(X ≤ 3)
532
+ p_leq_3 = stats.poisson.cdf(3, _lambda)
533
+ print(f"P(X≤3) = {p_leq_3:.5f}")
534
+
535
+ # Calculate probability P(X > 10)
536
+ p_gt_10 = 1 - stats.poisson.cdf(10, _lambda)
537
+ print(f"P(X>10) = {p_gt_10:.5f}")
538
+ return p_1, p_2, p_3, p_gt_10, p_leq_3
539
+
540
+
541
+ @app.cell(hide_code=True)
542
+ def _(mo):
543
+ mo.md(r"""We can also generate random samples from a Poisson distribution and visualize their distribution:""")
544
+ return
545
+
546
+
547
+ @app.cell(hide_code=True)
548
+ def _(np, plt, stats):
549
+ def create_samples_plot(lambda_value, sample_size=1000):
550
+ # Random samples
551
+ samples = stats.poisson.rvs(lambda_value, size=sample_size)
552
+
553
+ # theoretical PMF
554
+ x_values = np.arange(0, max(samples) + 1)
555
+ pmf_values = stats.poisson.pmf(x_values, lambda_value)
556
+
557
+ # histograms to compare
558
+ fig, ax = plt.subplots(figsize=(10, 6))
559
+
560
+ # samples as a histogram
561
+ ax.hist(samples, bins=np.arange(-0.5, max(samples) + 1.5, 1),
562
+ alpha=0.7, density=True, label='Random Samples')
563
+
564
+ # theoretical PMF
565
+ ax.plot(x_values, pmf_values, 'ro-', label='Theoretical PMF')
566
+
567
+ # labels and title
568
+ ax.set_xlabel('Number of Events')
569
+ ax.set_ylabel('Relative Frequency / Probability')
570
+ ax.set_title(f'1000 Random Samples from Poisson(λ={lambda_value})')
571
+ ax.legend()
572
+ ax.grid(alpha=0.3)
573
+
574
+ # annotations
575
+ ax.annotate(f'Sample Mean: {np.mean(samples):.2f}',
576
+ xy=(0.7, 0.9), xycoords='axes fraction',
577
+ bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.3))
578
+ ax.annotate(f'Theoretical Mean: {lambda_value:.2f}',
579
+ xy=(0.7, 0.8), xycoords='axes fraction',
580
+ bbox=dict(boxstyle='round,pad=0.5', fc='lightgreen', alpha=0.3))
581
+
582
+ plt.tight_layout()
583
+ return plt.gca()
584
+
585
+ # Use a lambda value of 5 for this example
586
+ _lambda = 5
587
+ create_samples_plot(_lambda)
588
+ return (create_samples_plot,)
589
+
590
+
591
+ @app.cell(hide_code=True)
592
+ def _(mo):
593
+ mo.md(
594
+ r"""
595
+ ## Changing Time Frames
596
+
597
+ One important property of the Poisson distribution is that the rate parameter $\lambda$ scales linearly with the time interval. If events occur at a rate of $\lambda$ per unit time, then over a period of $t$ units, the rate parameter becomes $\lambda \cdot t$.
598
+
599
+ For example, if a website receives an average of 5 requests per minute, what is the distribution of requests over a 20-minute period?
600
+
601
+ The rate parameter for the 20-minute period would be $\lambda = 5 \cdot 20 = 100$ requests.
602
+ """
603
+ )
604
+ return
605
+
606
+
607
+ @app.cell(hide_code=True)
608
+ def _(mo):
609
+ rate_slider = mo.ui.slider(
610
+ start = 0.1,
611
+ stop = 10,
612
+ step=0.1,
613
+ value=5,
614
+ label="Rate per unit time (λ)"
615
+ )
616
+
617
+ time_slider = mo.ui.slider(
618
+ start = 1,
619
+ stop = 60,
620
+ step=1,
621
+ value=20,
622
+ label="Time period (t units)"
623
+ )
624
+
625
+ controls = mo.vstack([
626
+ mo.md("### Adjust Parameters to See How Time Scaling Works"),
627
+ mo.hstack([rate_slider, time_slider], justify="space-between")
628
+ ])
629
+ return controls, rate_slider, time_slider
630
+
631
+
632
+ @app.cell
633
+ def _(controls):
634
+ controls.center()
635
+ return
636
+
637
+
638
+ @app.cell(hide_code=True)
639
+ def _(mo, np, plt, rate_slider, stats, time_slider):
640
+ def create_time_scaling_plot(rate, time_period):
641
+ # scaled rate parameter
642
+ lambda_value = rate * time_period
643
+
644
+ # PMF for values
645
+ max_x = max(30, int(lambda_value * 1.5))
646
+ x = np.arange(0, max_x + 1)
647
+ pmf = stats.poisson.pmf(x, lambda_value)
648
+
649
+ # plot
650
+ fig, ax = plt.subplots(figsize=(10, 6))
651
+
652
+ # PMF as bars
653
+ ax.bar(x, pmf, color='royalblue', alpha=0.7,
654
+ label=f'PMF: Poisson(λ={lambda_value:.1f})')
655
+
656
+ # vertical line for mean
657
+ ax.axvline(x=lambda_value, color='red', linestyle='--', linewidth=2,
658
+ label=f'Mean = {lambda_value:.1f}')
659
+
660
+ # labels and title
661
+ ax.set_xlabel('Number of Events')
662
+ ax.set_ylabel('Probability')
663
+ ax.set_title(f'Poisson Distribution Over {time_period} Units (Rate = {rate}/unit)')
664
+
665
+ # better visualization if lambda is large
666
+ if lambda_value > 10:
667
+ ax.set_xlim(lambda_value - 4*np.sqrt(lambda_value),
668
+ lambda_value + 4*np.sqrt(lambda_value))
669
+
670
+ ax.legend()
671
+ ax.grid(alpha=0.3)
672
+
673
+ plt.tight_layout()
674
+
675
+ # Create relevant info markdown
676
+ info_text = f"""
677
+ When the rate is **{rate}** events per unit time and we observe for **{time_period}** units:
678
+
679
+ - The expected number of events is **{lambda_value:.1f}**
680
+ - The variance is also **{lambda_value:.1f}**
681
+ - The standard deviation is **{np.sqrt(lambda_value):.2f}**
682
+ - P(X=0) = {stats.poisson.pmf(0, lambda_value):.4f} (probability of no events)
683
+ - P(X≥10) = {1 - stats.poisson.cdf(9, lambda_value):.4f} (probability of 10 or more events)
684
+ """
685
+
686
+ return plt.gca(), info_text
687
+
688
+ # parameters from sliders
689
+ _rate = rate_slider.value
690
+ _time = time_slider.value
691
+
692
+ # store
693
+ _plot, _info_text = create_time_scaling_plot(_rate, _time)
694
+
695
+ # Display info as markdown
696
+ info = mo.md(_info_text)
697
+
698
+ mo.vstack([_plot, info], justify="center")
699
+ return create_time_scaling_plot, info
700
+
701
+
702
+ @app.cell(hide_code=True)
703
+ def _(mo):
704
+ mo.md(
705
+ r"""
706
+ ## 🤔 Test Your Understanding
707
+ Pick which of these statements about Poisson distributions you think are correct:
708
+
709
+ /// details | The variance of a Poisson distribution is always equal to its mean
710
+ ✅ Correct! For a Poisson distribution with parameter $\lambda$, both the mean and variance equal $\lambda$.
711
+ ///
712
+
713
+ /// details | The Poisson distribution can be used to model the number of successes in a fixed number of trials
714
+ ❌ Incorrect! That's the binomial distribution. The Poisson distribution models the number of events in a fixed interval of time or space, not a fixed number of trials.
715
+ ///
716
+
717
+ /// details | If $X \sim \text{Poisson}(\lambda_1)$ and $Y \sim \text{Poisson}(\lambda_2)$ are independent, then $X + Y \sim \text{Poisson}(\lambda_1 + \lambda_2)$
718
+ ✅ Correct! The sum of independent Poisson random variables is also a Poisson random variable with parameter equal to the sum of the individual parameters.
719
+ ///
720
+
721
+ /// details | As $\lambda$ increases, the Poisson distribution approaches a normal distribution
722
+ ✅ Correct! For large values of $\lambda$ (generally $\lambda > 10$), the Poisson distribution is approximately normal with mean $\lambda$ and variance $\lambda$.
723
+ ///
724
+
725
+ /// details | The probability of zero events in a Poisson process is always less than the probability of one event
726
+ ❌ Incorrect! For $\lambda < 1$, the probability of zero events ($e^{-\lambda}$) is actually greater than the probability of one event ($\lambda e^{-\lambda}$).
727
+ ///
728
+
729
+ /// details | The Poisson distribution has a single parameter $\lambda$, which always equals the average number of events per time period
730
+ ✅ Correct! The parameter $\lambda$ represents the average rate of events, and it uniquely defines the distribution.
731
+ ///
732
+ """
733
+ )
734
+ return
735
+
736
+
737
+ @app.cell(hide_code=True)
738
+ def _(mo):
739
+ mo.md(
740
+ r"""
741
+ ## Summary
742
+
743
+ The Poisson distribution is one of those incredibly useful tools that shows up all over the place. I've always found it fascinating how such a simple formula can model so many real-world phenomena - from website traffic to radioactive decay.
744
+
745
+ What makes the Poisson really cool is that it emerges naturally as we try to model rare events occurring over a continuous interval. Remember that visualization where we kept dividing time into smaller and smaller chunks? As we showed, when you take a binomial distribution and let the number of trials approach infinity while keeping the expected value constant, you end up with the elegant Poisson formula.
746
+
747
+ The key things to remember about the Poisson distribution:
748
+
749
+ - It models the number of events occurring in a fixed interval of time or space, assuming events happen at a constant average rate and independently of each other
750
+
751
+ - Its PMF is given by the elegantly simple formula $P(X=k) = \frac{\lambda^k e^{-\lambda}}{k!}$
752
+
753
+ - Both the mean and variance equal the parameter $\lambda$, which represents the average number of events per interval
754
+
755
+ - It's related to the binomial distribution as a limiting case when $n \to \infty$, $p \to 0$, and $np = \lambda$ remains constant
756
+
757
+ - The rate parameter scales linearly with the length of the interval - if events occur at rate $\lambda$ per unit time, then over $t$ units, the parameter becomes $\lambda t$
758
+
759
+ From modeling website traffic and customer arrivals to defects in manufacturing and radioactive decay, the Poisson distribution provides a powerful and mathematically elegant way to understand random occurrences in our world.
760
+ """
761
+ )
762
+ return
763
+
764
+
765
+ @app.cell(hide_code=True)
766
+ def _(mo):
767
+ mo.md(r"""Appendix code (helper functions, variables, etc.):""")
768
+ return
769
+
770
+
771
+ @app.cell
772
+ def _():
773
+ import marimo as mo
774
+ return (mo,)
775
+
776
+
777
+ @app.cell(hide_code=True)
778
+ def _():
779
+ import numpy as np
780
+ import matplotlib.pyplot as plt
781
+ import scipy.stats as stats
782
+ import pandas as pd
783
+ import altair as alt
784
+ from wigglystuff import TangleSlider
785
+ return TangleSlider, alt, np, pd, plt, stats
786
+
787
+
788
+ @app.cell(hide_code=True)
789
+ def _():
790
+ import io
791
+ import base64
792
+ from matplotlib.figure import Figure
793
+
794
+ # Helper function to convert mpl figure to an image format mo.image can hopefully handle
795
+ def fig_to_image(fig):
796
+ buf = io.BytesIO()
797
+ fig.savefig(buf, format='png')
798
+ buf.seek(0)
799
+ data = f"data:image/png;base64,{base64.b64encode(buf.read()).decode('utf-8')}"
800
+ return data
801
+ return Figure, base64, fig_to_image, io
802
+
803
+
804
+ if __name__ == "__main__":
805
+ app.run()