Statistics for Data Science · Day 7

Introduction to Probability

The mathematical engine behind every prediction, from Netflix recommendations to fraud detection.

By Shikhar · Series continued · ~16 min read

Every time Netflix decides what to recommend you next, it's really asking one question: given what this user has watched, what's the probability they'll enjoy this title? Every time Gmail filters your inbox, it's asking a version of the same question: given the words in this email, what's the probability it's spam?

Underneath all of this is probability, the mathematical language for describing uncertainty. Before you can really understand machine learning models, A/B testing, or statistical inference, you need to be comfortable with probability. That's what this article is for.

What is probability?

Probability is just a number that tells you how likely something is to happen. It always lives somewhere between 0 and 1.

0 Impossible Sahara snow in summer 0.5 50/50 chance a fair coin flip 1 Certain the sun rises tomorrow
Every probability lives somewhere on this line. Nothing is ever below 0 or above 1.

There's one rule that never breaks: the probabilities of every possible outcome have to add up to 1. Flip a coin, and P(Heads) + P(Tails) = 0.5 + 0.5 = 1. That makes sense, since something has to happen.

Google · real-world use

When you type a search query, Google's ranking system assigns a probability score to every page in its index, essentially P(this page is relevant | your query). The pages with the highest scores rise to the top. That's probability running at a scale of billions of queries a day.

A quick walkthrough: say Google evaluates 3 webpages for the query "best running shoes," and its model scores each one for relevance:

Page A: 0.91
Page B: 0.74
Page C: 0.48

Google ranks them in descending order, A, then B, then C, so Page A lands at position 1, even though no page is a guaranteed perfect match (nobody scored a full 1.0). When someone clicks a result, that click becomes new evidence, and Google updates its probability estimates so pages that get clicked more often earn higher scores for similar future searches. That updating process is Bayes' Theorem running at internet scale, which we'll get to properly later in this article.

Google doesn't actually "know" which page is best. It computes a probability, and that probability can shift as new clicks roll in. Every search result you see is a live, constantly-updating probability in action.

The building blocks: key terms

Like any subject, probability has its own vocabulary. These five terms are the foundation for everything that follows.

The core formula

P(A) = Number of favourable outcomesTotal number of possible outcomes

Let's make this concrete with two examples, then check them with Python.

Example 1: flipping a coin twice

Sample space: {HH, HT, TH, TT}, 4 total outcomes.

HH HT TH TT 3 outcomes have at least one head → 3/4 = 0.75
All 4 equally likely outcomes of flipping a coin twice. Only TT (dashed) has zero heads.

P(no heads) is the flip side of that: only TT qualifies, so that's 1/4 = 0.25.

Example 2: two dice

Sample space: 36 outcomes, every pair from (1,1) to (6,6).

Die 2Die 1 1,1 1,2 1,3 1,4 1,5 1,6 2,1 2,2 2,3 2,4 2,5 2,6 3,1 3,2 3,3 3,4 3,5 3,6 4,1 4,2 4,3 4,4 4,5 4,6 5,1 5,2 5,3 5,4 5,5 5,6 6,1 6,2 6,3 6,4 6,5 6,6 1 2 3 4 5 6 1 2 3 4 5 6 11 of 36 pairs have a 5 somewhere → 11/36 ≈ 0.306
All 36 equally likely outcomes of rolling two dice. The highlighted cells are every pair where at least one die shows a 5.

Simulating probabilities with Python

You can always double-check a theoretical probability by simulating it. This is one of the most useful tricks in a data scientist's toolkit: when you're not sure your formula is right, just run the experiment thousands of times and see what actually happens.

pythonimport random # Simulate flipping a coin twice, 100,000 times def coin_flip_simulation(trials=100_000): at_least_one_head = 0 for _ in range(trials): flip1 = random.choice(['H', 'T']) flip2 = random.choice(['H', 'T']) if flip1 == 'H' or flip2 == 'H': at_least_one_head += 1 return at_least_one_head / trials simulated = coin_flip_simulation() print(f"Simulated P(at least one head): {simulated:.4f}") print(f"Theoretical P(at least one head): {3/4:.4f}")
OutputSimulated P(at least one head): 0.7496 Theoretical P(at least one head): 0.7500

The simulated result lands almost exactly on the theoretical value. This trick, checking a formula against a simulation, is called Monte Carlo simulation, and it's used constantly at companies like Google, Uber, and Airbnb for situations too messy for a clean formula.

Scaling up: combinatorics

Counting by hand works fine for dice and coins. But in data science, your sample space can easily run into the millions. Combinatorics is the math of counting efficiently without listing every single possibility by hand. There are two main tools.

1. Permutations: when order matters

Use permutations whenever the sequence of items actually matters.

1.1 With replacement: nk

Use this version when each choice is independent and items are allowed to repeat.

nk
Google Ads · A/B testing

You're testing a landing page with 3 headline variants, 4 hero image options, and 2 CTA button colours. Every combination is its own unique page. For each of the 3 headlines, there are 4 images, giving 3 × 4 = 12 pairs. Each of those 12 pairs can take either button colour, so the total comes to 12 × 2 = 24 unique page variants. This is a with-replacement permutation, since you're choosing independently from each category.

Say Google's ad team splits 240,000 daily visitors evenly across all 24 variants. Each one gets exactly 10,000 visits. If Variant #7 earns 1,200 clicks, its click-through rate is P(click | Variant 7) = 1,200 / 10,000 = 0.12, a 12% CTR. Whichever variant posts the highest CTR wins.

Those 24 variants aren't just a counting exercise, each one is a separate hypothesis to test. Combinatorics tells you exactly how many you're signing up for. Add just one more button option (3 instead of 2) and you jump to 36 variants, which means 50% more traffic needed to test everything fairly. That's exactly why product teams design experiments carefully: more combinations means more data needed, which means longer tests, which means higher cost.

1.2 Without replacement: n! ⁄ (n−k)!

Use this version once an item is used up, it's gone. Order still matters.

n!(n − k)!
Amazon · delivery routes

Amazon's last-mile team has 10 delivery stops, and a driver can only complete 5 of them in a shift. For the first stop, there are 10 choices. For the second, 9 remain. Then 8, then 7, then 6. Multiply them together: 10 × 9 × 8 × 7 × 6 = 30,240 possible ordered routes. Finding the single best one is the famous Travelling Salesman Problem, which Amazon's routing engine solves millions of times a day.

If Amazon's algorithm just picked a route at random, the odds of landing on the single fastest one would be 1 / 30,240, about 0.0033%, essentially zero by chance. Now scale that up: real drivers often have 50 or more stops per route, and the number of possible orderings explodes past the number of atoms on Earth (the same scale of number behind the famous fact that a shuffled deck of 52 cards can land in more orders than that). Random guessing was never going to cut it, which is exactly why Amazon invests heavily in operations research instead. Even a 1% improvement in route efficiency across 100,000+ daily drivers translates into millions of dollars saved per day in fuel and labor.

2. Combinations: when order doesn't matter

Use combinations when you only care about which items get picked, not the order they come in. This is written as "n choose k," or C(n, k).

n!k! × (n − k)!
Machine learning · feature selection

You have a dataset with 20 features and want to test every possible subset of 5 to find the best combination for your model. C(20, 5) = 20! ⁄ (5! × 15!) = 15,504 subsets. That already sounds like a lot. A dataset with 50 features, testing subsets of 10, gives C(50, 10) ≈ 10 billion subsets to check.

If training just one model takes a single second, checking all 15,504 subsets from the 20-feature example takes about 4.3 hours of compute time. Scale up to 50 features and pick 10, and you're looking at roughly 325 years of compute time at one second per model. This is exactly why machine learning teams reach for smarter search strategies instead, like Recursive Feature Elimination, LASSO regularization, or genetic algorithms, all designed to find good feature subsets without ever testing every single one.

pythonfrom math import comb, perm # Permutations: ordered delivery routes (10 stops, choose 5) delivery_routes = perm(10, 5) print(f"Ordered delivery routes: {delivery_routes:,}") # Combinations: feature subsets (20 features, choose 5) feature_subsets = comb(20, 5) print(f"Feature subsets (20 choose 5): {feature_subsets:,}") # How bad does it get with 50 features, choosing 10? large_feature_search = comb(50, 10) print(f"Feature subsets (50 choose 10): {large_feature_search:,}")
OutputOrdered delivery routes: 30,240 Feature subsets (20 choose 5): 15,504 Feature subsets (50 choose 10): 10,272,278,170

Combinatorics doesn't just count possibilities, it quantifies exactly how big a search problem really is. When a data scientist says "we can't do an exhaustive feature search," what they mean is that combinatorics has already proven it's computationally impossible within a human lifetime. That single fact shapes which algorithm gets picked.

Types of events: independent vs. dependent

Before we get to conditional probability, we need one more distinction: independent events versus dependent events.

Independent events: the outcome of one doesn't affect the other at all. Two coin flips in a row are independent, since the second flip has no idea what the first one landed on.

Dependent events: the outcome of one changes the probability of the other. Drawing cards from a deck without putting them back is dependent, since once you've drawn the Ace of Spades, it simply can't be drawn again.

Why this matters in ML: plenty of machine learning algorithms, Naive Bayes being the classic example, assume features are independent of each other. In reality they usually aren't, but that assumption simplifies the math enormously and still produces remarkably good results in practice, as you'll see shortly.

Two versions of the multiplication rule

When two events really are independent, multiplying their probabilities together is simple:

P(A and B) = P(A) × P(B)
Only true when A and B are genuinely independent, knowing one tells you nothing about the other.

For example: the probability that a fair coin lands heads AND it happens to be raining outside right now. Those two things have nothing to do with each other, so you'd just multiply P(heads) by P(rain).

But most interesting real-world pairs of events aren't actually independent. A user has to click an ad before they can buy something through it, so "clicked" and "purchased" are dependent by definition. For cases like that, there's a version of the multiplication rule that always works, independent or not:

P(A and B) = P(A) × P(B | A)
This one always works, independent or not. When A and B really are independent, P(B | A) is just P(B), and this collapses into the rule above.
Meta · ad click prediction

Meta's ad system wants the probability that a user clicks an ad and then goes on to make a purchase. Say P(click) = 0.03 and P(purchase | click) = 0.10. Notice that second number is already conditional, purchase given a click, precisely because purchases can't happen independently of clicks. So this calls for the general rule, not the independent-events one: P(click and purchase) = P(click) × P(purchase | click) = 0.03 × 0.10 = 0.003, or 0.3%.

Now scale that to Meta's real volume, roughly 10 billion ad impressions a day: expected conversions per day = 10,000,000,000 × 0.003 = 30,000,000 purchases. If Meta's ML team nudges P(click) up by just 0.001, from 3% to 3.1%, daily conversions become 10B × 0.031 × 0.10 = 31,000,000, a full 1 million extra purchases a day from a change too small to even see on a chart. That's exactly why companies at this scale employ entire teams obsessing over tiny probability improvements: at internet scale, small gains are worth billions.

Conditional probability

This is where things get genuinely useful. Conditional probability answers the question: how does knowing one thing change the probability of something else?

Formally, P(A | B) is read as "the probability of A, given that B has already happened."

P(A | B) = P(A ∩ B)P(B)

Here's the intuition: conditioning on B means shrinking your entire sample space down to only the outcomes where B actually happened. Then you ask what fraction of just those outcomes also satisfy A.

Example: credit card fraud detection

Imagine a bank's data shows this, out of 10,000 transactions: 100 are fraud (a 1% baseline rate), 500 came from a new location (5%), and 80 transactions are both fraud and from a new location (0.8%).

What's the probability a transaction is fraudulent, given that it came from a new location?

P(Fraud | New Location) = P(Fraud ∩ New Location) / P(New Location)

= 0.008 / 0.05 = 0.16 (16%)
All 10,000 transactions New location: 500 transactions Fraud: 80 of these 500 Fraud rate overall: 1% Fraud rate inside "new location": 16%
Zoom into just the "new location" circle, then ask what share of it is fraud. That's conditioning in one picture: 16%, not the 1% you'd guess from the whole population.
python# Fraud detection: conditional probability total_transactions = 10_000 fraud_count = 100 new_location_count = 500 fraud_and_new_location = 80 p_fraud = fraud_count / total_transactions p_new_location = new_location_count / total_transactions p_fraud_and_new_loc = fraud_and_new_location / total_transactions # Conditional probability: P(Fraud | New Location) p_fraud_given_new_loc = p_fraud_and_new_loc / p_new_location print(f"P(Fraud) = {p_fraud:.2%}") print(f"P(Fraud | New Location) = {p_fraud_given_new_loc:.2%}") print(f"Risk increased by {p_fraud_given_new_loc/p_fraud:.1f}x just from location signal")
OutputP(Fraud) = 1.00% P(Fraud | New Location) = 16.00% Risk increased by 16.0x just from location signal

Just knowing a transaction came from a new location took the fraud probability from 1% all the way to 16%. That's precisely the kind of signal Stripe, PayPal, and every major bank's fraud engine leans on in real time.

A few things worth sitting with here. First, that 1% baseline matters: if Stripe reviews a million transactions a day, flagging 1% means 10,000 investigations, manageable but not cheap. Second, a single signal, new location, made a transaction 16 times more suspicious, which lets the bank prioritize review without manually checking all million transactions. Third, real fraud systems stack multiple signals together (new location, an unusual amount, a new device, an odd hour), and each one narrows the sample space further. If P(Fraud | new location AND large amount AND new device) reaches 0.72, the system can auto-decline with real confidence, no human needed. More signals means a sharper, more confident probability estimate, which means fewer fraudulent transactions slipping through unnoticed.

Bayes' Theorem: updating beliefs with evidence

Bayes' Theorem might be the single most important result in probability for data science. It tells you exactly how to update a belief once new evidence comes in, which is precisely what a learning algorithm does every time it sees more data.

P(A | B) = P(B | A) × P(A)P(B)

Let's decode each piece:

Real-world example: Gmail's spam filter

Say Gmail has learned the following from millions of emails: 50% of all emails are spam, so P(Spam) = 0.5. The word "free" shows up in 80% of spam emails, P("free" | Spam) = 0.80. And "free" shows up in only 10% of legitimate emails, P("free" | Not Spam) = 0.10.

A new email arrives containing the word "free." What's the probability it's spam?

First, work out the overall probability of seeing "free" at all, across both spam and legitimate email:

P("free") = P("free"|Spam)·P(Spam) + P("free"|Not Spam)·P(Not Spam)

= (0.80 × 0.5) + (0.10 × 0.5) = 0.40 + 0.05 = 0.45

Now apply Bayes' Theorem:

P(Spam | "free") = (0.80 × 0.5) / 0.45 = 0.40 / 0.45 ≈ 0.889 (88.9%)

Before seeing the word "free," Gmail's best guess was a 50% chance of spam. After seeing it, that belief jumps to 88.9%. That jump is Bayes' Theorem doing exactly its job.

python# Bayes' Theorem: spam filter p_spam = 0.50 p_not_spam = 1 - p_spam p_free_given_spam = 0.80 p_free_given_not_spam = 0.10 # Total probability of seeing "free" at all p_free = (p_free_given_spam * p_spam) + (p_free_given_not_spam * p_not_spam) # Bayes' Theorem p_spam_given_free = (p_free_given_spam * p_spam) / p_free print(f"Prior P(Spam): {p_spam:.1%}") print(f"P(Spam | email contains 'free'): {p_spam_given_free:.1%}") # What if the email also contains "lottery"? p_lottery_given_spam = 0.60 p_lottery_given_not_spam = 0.02 # Naive Bayes: treat the two words as independent, given the class numerator = p_free_given_spam * p_lottery_given_spam * p_spam denominator = ( p_free_given_spam * p_lottery_given_spam * p_spam + p_free_given_not_spam * p_lottery_given_not_spam * p_not_spam ) p_spam_given_both = numerator / denominator print(f"P(Spam | 'free' AND 'lottery'): {p_spam_given_both:.1%}")
OutputPrior P(Spam): 50.0% P(Spam | email contains 'free'): 88.9% P(Spam | 'free' AND 'lottery'): 99.6%
50.0% Before any evidence 88.9% After seeing "free" 99.6% After "free" + "lottery"
Each new piece of evidence pushes the belief further. That's Bayes' Theorem: start with a prior, update it as evidence arrives, end with a posterior.

Each extra piece of evidence pushes the probability further. This chaining of evidence is the core idea behind the Naive Bayes Classifier, one of the fastest and most interpretable algorithms in machine learning, and still genuinely useful for text classification and spam detection today.

Gmail · Naive Bayes in production

Google's early spam filters, and even parts of today's, are built on exactly this logic, just extended to thousands of words instead of two. Every word in an email becomes a piece of evidence, and Bayes' Theorem gets applied across all of them to land on a final spam probability. The "naive" part of the name comes from that independence assumption: treating each word as if it tells you nothing about the others, which isn't really true, but it's computationally cheap and works surprisingly well anyway.

What this looks like at Gmail's actual scale

Chaining just two words, "free" and "lottery," drove the spam probability up to 99.6%. That sounds nearly perfect. But what does the remaining 0.4% miss rate look like once you're operating at Gmail's scale?

Gmail processes roughly 1.8 billion emails a day, and about 45% of them are spam, which works out to 810,000,000 spam emails daily. With a 99.6% detection rate, the spam that slips through anyway (a false negative) comes to 810,000,000 × (1 − 0.996) = 3,240,000 missed spam emails, every single day.

False positives matter just as much. If 0.05% of legitimate emails get mistakenly flagged as spam, and roughly 990,000,000 legitimate emails arrive daily, that's 990,000,000 × 0.0005 = 495,000 real emails wrongly blocked per day.

This is exactly why Gmail doesn't just chase the highest possible spam-detection probability. It's constantly balancing two competing probabilities: catching spam versus wrongly blocking a real email. Push one up too hard and you tend to push the other one up right along with it. Even a filter running at 99.6% accuracy still misses millions of spam emails daily at this scale, which is exactly why spam filters are never really "finished." They're retrained continuously to keep their probability estimates current, because the fight between spammers and filters is, underneath everything, a fight over probability estimates.

Key takeaways

Probability is the foundation for reasoning about uncertainty. Conditional probability shows exactly how new evidence shifts your beliefs. Bayes' Theorem formalizes that shift into a precise update rule. Every time a model makes a prediction, underneath it all, it's computing a probability. The better you understand probability, the better you'll understand what your models are actually doing.

Quick check

Four questions to test whether this stuck.

You flip a fair coin 3 times. What's the sample space, and what's P(at least one tail)?

The sample space has 2³ = 8 outcomes: HHH, HHT, HTH, HTT, THH, THT, TTH, TTT. Only HHH has zero tails, so P(at least one tail) = 7/8 = 0.875.

You have 6 different books and want to arrange 3 of them on a shelf, where order matters. Should you use a permutation or a combination, and how many arrangements are there?

A permutation, since order matters here. That's 6! / (6−3)! = 6 × 5 × 4 = 120 possible arrangements.

Someone tells you "80% of people who like jazz also like classical music." Is that P(classical | jazz) or P(jazz | classical)?

P(classical | jazz), the probability of liking classical, given that you already like jazz. The thing that comes right after "given" or "who" in the sentence is what you're conditioning on, and it goes on the right side of the bar.

A rare disease affects 1 in 1,000 people. A test for it is 99% accurate. If someone tests positive, are they necessarily sick?

Not necessarily, and this is exactly why Bayes' Theorem matters. Because the disease is so rare to begin with (a low prior), even a 99%-accurate test will flag a meaningful number of healthy people as false positives, sometimes more than the truly sick people it correctly catches. You have to combine the test's accuracy with the disease's actual prevalence to get the real answer, which is precisely what P(A | B) = P(B | A) × P(A) / P(B) is built to do.