Implementation — reading a minimal Poisson sampler
Walk through a single loop of a dependency-free vanilla JavaScript implementation of the Poisson pmf / cdf / sampler.
Transcribe the formula directly into a function
This course's JavaScript is a minimal implementation that moves the math into functions without hiding anything. No external libraries; inputs that violate the preconditions raise an exception instead of being silently accepted.
Formula → function → screen: in three stages you can see in one view where the Poisson calculation actually happens.
The minimal JavaScript implementation
Here is the minimal implementation actually used by the Poisson calculations and the simulator.
function assertFiniteNonNegative(name, value) {
if (!Number.isFinite(value) || value < 0) {
throw new RangeError(`${name} must be a finite non-negative number`);
}
}
function assertNonNegativeInteger(name, value) {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError(`${name} must be a non-negative integer`);
}
}
function factorialInt(n) {
assertNonNegativeInteger("n", n);
let acc = 1;
for (let i = 2; i <= n; i += 1) {
acc *= i;
}
return acc;
}
function poissonPmf(lambda, k) {
assertFiniteNonNegative("lambda", lambda);
assertNonNegativeInteger("k", k);
return Math.exp(-lambda) * (lambda ** k) / factorialInt(k);
}
function poissonCdf(lambda, k) {
assertFiniteNonNegative("lambda", lambda);
assertNonNegativeInteger("k", k);
let sum = 0;
for (let i = 0; i <= k; i += 1) {
sum += poissonPmf(lambda, i);
}
return sum;
}
function samplePoissonKnuth(lambda, rng = Math.random) {
assertFiniteNonNegative("lambda", lambda);
if (lambda === 0) return 0;
const limit = Math.exp(-lambda);
let product = 1;
let count = 0;
while (product > limit) {
product *= rng();
count += 1;
}
return count - 1;
}
poissonPmf gives a single bar, poissonCdf is the left-to-right cumulative sum, and samplePoissonKnuth draws one sample from a stream of uniform random numbers.
Code-to-formula mapping
| Code fragment | Formula / meaning |
|---|---|
factorialInt(k) | The denominator k! |
Math.exp(-lambda) | e^(−λ) |
lambda ** k | λ^k |
poissonCdf | P(X ≤ k) = Σ P(X = i) |
samplePoissonKnuth | Procedure to draw one Poisson variate |
As an introduction, being able to read poissonPmf and poissonCdf is plenty. The sampler is your entry point when you later move on to simulation or Monte Carlo work.
Why the Knuth sampler works
samplePoissonKnuth is a classic algorithm (Donald Knuth, 1969) for drawing a single sample from a Poisson distribution. The code looks like a black box at first, but the idea behind it is the well-known fact that "the inter-arrival times in a Poisson process follow an exponential distribution."
- For a Poisson process with rate
λ(per unit interval), the gap between consecutive events followsExponential(λ). - The number of events
Xarriving in the interval[0, 1]followsPoisson(λ). - Therefore "how many exponential gaps fit before we exhaust 1 unit of time" gives a Poisson sample.
Each exponential variate E_i can be produced from a uniform U_i ∈ (0, 1] as E_i = -ln(U_i) / λ. A Poisson(λ) sample is "the largest n such that E_1 + E_2 + ... + E_n ≤ 1." Multiplying both sides by -λ and exponentiating turns the sum into a product:
E_1 + ... + E_n ≤ 1 ⇔ U_1 × U_2 × ... × U_n ≥ e^(−λ)That equivalence is the heart of the algorithm. The code fixes limit = e^(−λ) up front, multiplies product by uniforms, and counts how many multiplications it takes for product to fall below limit. Concretely:
product: the running product of uniforms drawn so far,U_1 × U_2 × ... × U_count.while (product > limit): keep looping while the product is still abovee^(−λ), i.e., while the cumulative exponential sum has not yet exceeded 1.count - 1: the loop body incrementscountimmediately afterproduct *= rng(), so the iteration that finally breaks the condition has already incrementedcount. Subtracting 1 removes that overshoot, giving the desired count.
The early return 0 for lambda === 0 avoids the edge case where e^0 = 1 equals the initial product = 1, which would either stall the loop or behave unstably under floating-point rounding.
This algorithm is short and intuitive when λ is small, but it does O(λ) uniform draws per sample. Once λ gets large (say above 30) it becomes slow, and serious applications switch to algorithms designed for large λ such as PTRS or PA.
Implementation notes
This implementation is intentionally minimal for teaching. Four points to keep in mind if you push it toward production use:
lambda ** kand ES2016: the exponentiation operator**was added in ES2016. It is fine on modern Node.js and modern browsers, but if you need to support older environments,Math.pow(lambda, k)is broader. We chose**here because it visually mirrors the math notationλ^k.- Overflow in
factorialInt:factorialIntis a plain loop. JavaScriptNumberhas 53 bits of precision, so floating-point error creeps in around22!, and171!overflows toInfinity. For large λ paired with largek, you should switch to the recurrenceP(X = k+1) = P(X = k) × λ / (k+1)or compute in log space. This implementation assumes the small-λ teaching regime. - The Knuth sampler's
count - 1: as explained above, by the time the loop exits,countalready includes the iteration that broke the condition, so we returncount - 1to land on a validPoisson(λ)sample. Startingcountfrom 0 keeps the offset natural. - Choice of exception:
RangeError:RangeErroris the right fit when the type is correct but the value is out of range, such aslambda < 0or a non-integerk.TypeErroris conventionally reserved for cases where the argument's type itself is wrong (for example, a string supplied where a number is required). For a teaching implementation, raising explicitly is much easier to trace than silent rounding or returningNaN.
Hands-on — match the code to the formulas
Check how each line of code lines up with the hand calculations so far.
Q1. What is the return value of factorialInt(5)?
5! = 5 × 4 × 3 × 2 × 1 = 120.
Q2. Enter the value returned by poissonPmf(2, 3), to roughly three decimal places.
Plugging straight into the formula: e^-2 × 2^3 / 3! ≈ 0.180.
Q3. poissonCdf(2, 1) returns P(X ≤ 1). Enter its value.
P(X ≤ 1) = P(0) + P(1) = e^-2 + 2e^-2 ≈ 0.406.
Q4. When this implementation receives an invalid input such as lambda < 0, which exception does it throw?
Inputs that violate the preconditions stop explicitly with RangeError. As teaching code, that is much easier to follow than silently rounding the value.
Key takeaways from Chapter 6
- The Poisson pmf / cdf transcribe directly into JavaScript.
- The Knuth sampler rewrites "how many exponential gaps fit in interval 1" as a product of uniforms compared with
e^(−λ). - Raising a
RangeErroron invalid input makes the code easier to follow, both as a lesson and as an implementation. - The simulator's numbers come from exactly this minimal implementation.