Probability Basics
Table of Contents
- Chance Is a Number
- Probability as Favorable Over Total
- Independent Events and the Word AND
- The Trick: Count the Opposite
- Expected Value
- Random Integers, Done Correctly
- Simulation: Let the Computer Count
- The Birthday Paradox and ID Collisions
- A/B Tests: Is It Luck or Real?
- Math.random Is Not for Secrets
- Recipe: The probability Module
- Try It Yourself
1. Chance Is a Number
Will it rain today? Will this A/B test variant win? Will two users get the same random ID? Your code often has to deal with things that might or might not happen. Probability gives chance a number between 0 and 1:
| Probability | Meaning |
|---|---|
0 | Impossible. It never happens |
0.5 | Half the time, like a fair coin landing heads |
1 | Certain. It always happens |
You will also see probabilities as percentages: 0.25 is 25%. Programs
usually keep them as decimals between 0 and 1, because they multiply
nicely, as you will see.
This chapter builds on Counting: Permutations and Combinations. When every outcome is equally likely, probability is just counting.
2. Probability as Favorable Over Total
For equally likely outcomes:
probability = (number of outcomes you want) / (total number of outcomes)
A fair six-sided die has 6 outcomes. The chance of rolling a 6 is 1 / 6.
The chance of rolling an even number is 3 / 6 (the outcomes 2, 4, and 6),
which is 0.5.
const favorable = 3; // 2, 4, 6
const total = 6;
console.log(favorable / total); // 0.5
The word "equally likely" is doing real work. If you ask "will I win the
lottery? It either happens or doesn't, so it is 50%", you have counted
outcomes that are not equally likely. The correct count for a 6-from-49
lottery comes from the last chapter: one winning ticket out of
C(49, 6) = 13,983,816, which is about 0.00000007.
Programmers use this to answer questions like "if a server is picked
uniformly at random from 8 servers, how likely is it that a request lands
on server 3?" Answer: 1 / 8 = 0.125.
3. Independent Events and the Word AND
Two events are independent when one has no effect on the other. Two coin flips, two dice, or two different users' lottery tickets are examples.
For independent events, the chance that both happen is the product of their probabilities:
P(A and B) = P(A) * P(B)
The chance of rolling two sixes with two dice is 1/6 * 1/6 = 1/36:
console.log(1 / 6 * (1 / 6)); // 0.027777777777777776
Why does multiplying work? This is the rule of product from last chapter in
disguise: there are 6 * 6 = 36 equally likely pairs of dice results, and
only one is a double six.
Probabilities are at most 1, so multiplying them always makes the result smaller or equal: "this and that" is never more likely than "this".
Another everyday example: a service promises 99% uptime (0.99) and
depends on a database that is also 99% available. Your site is up only when
both are up:
console.log(0.99 * 0.99); // 0.9801
About 98%, lower than either one. Chains of dependencies quietly reduce reliability, and that is multiplication of probabilities.
Be careful: this rule only holds for independent events. If the second event depends on the first (drawing cards without putting them back), you cannot just multiply the plain probabilities.
4. The Trick: Count the Opposite
What is the chance of rolling at least one six in four rolls? Counting every way to get one, two, three, or four sixes is painful. Flip the question instead: the only way to not get a six at all is to roll a non-six four times in a row.
P(at least one) = 1 - P(none)
The chance of a non-six on one roll is 5/6, and four independent rolls
multiply:
const none = (5 / 6) ** 4;
console.log(none); // 0.4822530864197531
console.log(1 - none); // 0.5177469135802468
About 51.8%. This "one minus the opposite" trick is the single most useful move in basic probability, and you will use it again for the birthday paradox below.
5. Expected Value
Probability tells you how likely each outcome is. Expected value tells you the average result you would see if you repeated the situation many times. You compute it by multiplying each outcome's value by its probability and adding up the results:
expected value = (value1 * prob1) + (value2 * prob2) + ...
For a die, each of the values 1 to 6 has probability 1/6:
const outcomes = [1, 2, 3, 4, 5, 6].map((value) => ({
value,
probability: 1 / 6,
}));
const ev = outcomes.reduce((sum, o) => sum + o.value * o.probability, 0);
console.log(ev); // 3.5
No single roll is ever 3.5. The number describes the long-run average.
Expected value is how you judge a bet or a decision with money. Imagine a
raffle ticket costs 2 dollars. There is a 0.0005 chance (1 in 2,000) of
winning a 4,000 dollar prize and nothing otherwise:
const prize = 4000 * 0.0005;
console.log(prize); // 2
The ticket is worth 2 dollars on average, exactly its price, so across many tickets you break even. If the prize chance were 1 in 4,000, the expected value would be 1 dollar, and you would lose 1 dollar per ticket on average. Games and lotteries are set up so that the expected value is below the price.
The same idea sizes up engineering decisions: "this retry succeeds 70% of the time and costs 200 ms" gives you an expected number of tries and a total delay to plan with.
6. Random Integers, Done Correctly
Math.random() returns a decimal from 0 up to but not including 1.
To get a whole number in a range, scale it, round down, and shift it.
For a die roll, 1 to 6 inclusive:
const roll = Math.floor(Math.random() * 6) + 1;
Reading it from the inside out:
Math.random()gives0 <= r < 1.* 6gives0 <= x < 6.Math.floor(...)gives one of0, 1, 2, 3, 4, 5, each equally likely.+ 1shifts that to1through6.
The general version, with both ends included, is:
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
The + 1 is easy to forget. Without it, max could never be returned,
which is a classic off-by-one bug.
Two warnings about common but wrong versions:
Math.round(Math.random() * 5) + 1is not uniform.roundgives the first and last outcomes only half the chance of the middle ones. Always usefloor.- Using
%on a large random integer (randomBits % 6) can be slightly biased when the range does not divide evenly. For games and shuffles the bias is negligible. For security it is not, and we handle it in the recipe with a retry loop.
7. Simulation: Let the Computer Count
Sometimes the exact math is hard, or you just want to check your answer. A simulation runs the experiment many times and counts how often the event happens. By the law of large numbers, the more trials you run, the closer that fraction gets to the true probability.
A generic helper: run experiment(), which returns true or false, a
number of times and report the fraction of true:
function simulate(trials, experiment) {
let hits = 0;
for (let i = 0; i < trials; i++) {
if (experiment()) hits++;
}
return hits / trials;
}
// Chance of two sixes on two dice. The exact answer is 1/36 = 0.0277...
const p = simulate(100000, () => randomInt(1, 6) === 6 && randomInt(1, 6) === 6);
console.log(p); // varies each run, typically between 0.026 and 0.030
Your output will differ each time, because the dice are random. With 100,000 trials the answer is usually within about 0.001 of the real value. With 1,000 trials it wobbles much more, so use enough trials for the precision you need.
Simulation also confirms a fair generator. Roll a die 60,000 times and count:
const counts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
for (let i = 0; i < 60000; i++) counts[randomInt(1, 6)]++;
console.log(counts);
// varies, but each count lands near 10000, typically 9800 to 10200
If one face were wildly off, you would know the generator or your formula had a bug.
8. The Birthday Paradox and ID Collisions
How many people must be in a room before two probably share a birthday? Most people guess around 180 (half of 365). The real answer is 23.
It feels wrong because we compare one person against the rest. But with 23
people there are C(23, 2) = 253 pairs, and any pair matching counts.
Use the opposite trick. The chance that nobody shares a birthday:
- The second person must avoid 1 date:
364/365. - The third must avoid 2 dates:
363/365. - And so on, multiplying (independent choices).
Then collision probability is 1 - that product:
function birthdayCollision(people, days = 365) {
let allDifferent = 1;
for (let i = 0; i < people; i++) {
allDifferent *= (days - i) / days;
}
return 1 - allDifferent;
}
console.log(birthdayCollision(10).toFixed(4)); // 0.1169
console.log(birthdayCollision(23).toFixed(4)); // 0.5073
console.log(birthdayCollision(30).toFixed(4)); // 0.7063
console.log(birthdayCollision(50).toFixed(4)); // 0.9704
console.log(birthdayCollision(70).toFixed(4)); // 0.9992
At 23 people, it passes 50%. At 70 it is almost certain.
This is not just a party trick. It is exactly how you reason about ID
collisions. Imagine giving each new order a random 32-bit ID. That is
about 4.3 billion possible values (2 ** 32), which sounds huge. But the
birthday maths says you do not need billions of orders before a duplicate
shows up:
console.log(birthdayCollision(100000, 2 ** 32).toFixed(4)); // 0.6878
That is about 0.69. With only 100,000 IDs, there is a roughly 69% chance that at least two share the same value. Collisions appear at around the square root of the number of possible IDs.
That is why random IDs for databases are made much longer. A UUID version 4
has 122 random bits (2 ** 122), about 5.3 x 10^36 values. At that size,
you would need to create billions of IDs per second for decades to get a
meaningful collision chance. Short random codes (like 6-character
coupon codes or short links) need a collision check or a retry on conflict.
9. A/B Tests: Is It Luck or Real?
You change a button color. Variant A converts 100 of 1,000 visitors (10%), and variant B converts 113 of 1,000 (11.3%). Is B better, or is that luck?
You can feel the answer with a simulation. Pretend both variants have the same true conversion rate of 10%, and see how different two groups of 1,000 can look just by chance:
function conversions(visitors, rate) {
return simulate(visitors, () => Math.random() < rate);
}
for (let i = 0; i < 5; i++) {
const a = conversions(1000, 0.1);
const b = conversions(1000, 0.1);
console.log(a, b);
}
// varies, e.g. 0.113 0.099 / 0.094 0.108 / ...
// Both groups share the same true rate, yet they often differ by a point or more.
Even with identical true rates, the two samples routinely differ by one percentage point or more. So 10% versus 11.3% on 1,000 visitors each proves very little. Two takeaways, with no formulas required:
- Small samples are noisy. More visitors shrink the noise, just as more trials made the simulation settle near the exact answer.
- Decide the sample size in advance. If you peek every hour and stop the moment B looks ahead, you are likely to stop on a lucky streak.
Real statistics gives exact tools (confidence intervals, p-values), which are beyond this series. What you now have is the intuition for why they exist.
10. Math.random Is Not for Secrets
Math.random() is a pseudo-random generator: fast and good for games,
shuffles, animations, and simulations. But it is not designed to resist
someone trying to guess its output. Given a few results, an attacker can
sometimes work out the generator's internal state and predict the next
values. Never use it for:
- password reset tokens
- session IDs or API keys
- one-time codes (OTPs)
For those, use the cryptographically secure source built into
JavaScript, crypto.getRandomValues, which uses the operating system's
random source. It is available as a global in Node.js and in browsers:
const bytes = new Uint32Array(1);
crypto.getRandomValues(bytes);
console.log(bytes[0]); // a random whole number from 0 to 4294967295
It fills a typed array with random bits. To turn that into a number from
0 to max - 1 without bias, we throw away any value in the uneven
leftover part at the top and try again. You will see this in the recipe.
A 6-digit code from a banking or KHQR payment app, or a login one-time
password, must come from this kind of generator, not Math.random. (In
Node you can also use crypto.randomInt(0, 10) from the node:crypto
module, which does the same thing.)
11. Recipe: The probability Module
Create src/probability.js in mathlab:
// src/probability.js — chance, expectation, and simulation.
// Whole number from min to max, both ends included.
export function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Unbiased and safe for tokens: uses the OS random source.
export function secureRandomInt(max) {
// Returns 0 .. max-1 (max must be between 1 and 2**32).
const limit = 2 ** 32 - (2 ** 32 % max);
const buf = new Uint32Array(1);
do {
crypto.getRandomValues(buf);
} while (buf[0] >= limit);
return buf[0] % max;
}
// outcomes: [{ value, probability }, ...]
export function expectedValue(outcomes) {
return outcomes.reduce((sum, o) => sum + o.value * o.probability, 0);
}
// Chance that at least two of `people` share a value out of `days` options.
export function birthdayCollision(people, days = 365) {
let allDifferent = 1;
for (let i = 0; i < people; i++) {
allDifferent *= (days - i) / days;
}
return 1 - allDifferent;
}
// Run an experiment many times; return the fraction of true results.
export function simulate(trials, experiment) {
let hits = 0;
for (let i = 0; i < trials; i++) {
if (experiment()) hits++;
}
return hits / trials;
}
In secureRandomInt, the number 2 ** 32 % max is the size of the uneven
leftover. For max = 10, 2 ** 32 is not a multiple of 10, so the last 6
values would make small digits slightly more likely. The do/while loop
rejects those values, and the remaining range divides evenly, so % is
now fair.
Add the export to src/index.js:
export * from "./probability.js";
Try it with node demo.js:
import {
randomInt, secureRandomInt, expectedValue, birthdayCollision, simulate,
} from "./src/index.js";
console.log(randomInt(1, 6)); // varies: 1 to 6
const die = [1, 2, 3, 4, 5, 6].map((value) => ({ value, probability: 1 / 6 }));
console.log(expectedValue(die)); // 3.5
console.log(birthdayCollision(23).toFixed(4)); // 0.5073
// Chance two dice add to 7. Exact answer: 6/36 = 0.1666...
console.log(simulate(100000, () => randomInt(1, 6) + randomInt(1, 6) === 7));
// varies, typically between 0.163 and 0.170
const code = Array.from({ length: 6 }, () => secureRandomInt(10)).join("");
console.log(code); // varies: a 6-digit string like "026161"
Run it twice. The deterministic lines (3.5 and 0.5073) stay the same,
and the random lines change. Notice that the code keeps its leading zero
because we built it as a string, not a number.
12. Try It Yourself
- Two dice. List by hand the ways two dice can add up to 7, then
divide by 36. Compare with the
simulateresult. Which sum is the most likely, and why? - Uptime. Three services each have 99.5% uptime and your app needs all three. Use the multiplication rule to find the combined uptime. Then use the "count the opposite" trick to find how many minutes of downtime that means in a 30-day month (30 * 24 * 60 minutes).
- Expected value. A game costs 1 dollar to play. You roll a die: a 6
pays 5 dollars, anything else pays nothing. Write the outcomes for
expectedValueand decide whether the game favors you or the house. - Find the fifty-fifty point. Using
birthdayCollision, find the smallest group size for which the chance of a shared value among 1,000,000 possible IDs passes 50%. Looppeopleupward. You should land close to the square root of 1,000,000, times about 1.18. - Check fairness. Roll
randomInt(1, 6)600,000 times and count each face. Then do the same withMath.round(Math.random() * 5) + 1and see how the faces 1 and 6 compare with the others. - Secure token. Write
makeToken(length)that usessecureRandomIntto build a token from the 62 charactersa-z A-Z 0-9. UsepasswordSpacefrom the last chapter to say how many different 16-character tokens exist.
Probability closes the "how much and how likely" half of the series.
The final chapter turns to something you can see on screen:
Coordinates, Geometry and Putting It Together
covers points, distance, angles, animation, and then assembles all of
mathlab into one report.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!