Open the "Spam" folder in any Gmail or Outlook account and you will find dozens of messages that never bothered your inbox: "You have WON ₹50,00,000! Click to claim NOW," fake courier-delivery alerts, and lottery scams pretending to be from a bank. Nobody at Google reads your email and manually drags each one into that folder. A program does it, automatically, for billions of messages a day, and it is right an overwhelming majority of the time. This chapter builds a working version of that program from scratch — not a metaphor for one, an actual one, in Python, that you can trace by hand and verify produces the numbers it claims to produce. By the end you will understand precisely how a machine "learns" to tell spam from a real message, why the obvious way to measure how good it is (accuracy) is dangerously misleading, and why the standard fix — precision and recall — is something CBSE expects you to be able to compute cold.
What a Classifier Actually Is
Strip away the buzzwords and a classifier is a function. It takes an input and returns one label out of a fixed, known set of labels. For a spam filter, the input is a message (some text), and the output is one of exactly two labels: spam or ham (the standard term, going back to early spam-filtering research, for "a normal, wanted message" — the opposite of spam). Because there are only two possible outputs, this is called binary classification. If you built a system that sorted a photo into "cat," "dog," or "neither," that would be classification too, just with three labels instead of two — the machinery you are about to learn generalizes directly.
The word "machine learning" in this chapter means something specific: instead of a programmer writing down the rules for telling spam from ham by hand, the computer works out the rules itself by looking at thousands of messages that humans have already labeled correctly. The labeled messages are called training data. The rules the computer extracts from that data are the model. Once trained, the model is tested on messages it has never seen before — the test data — to find out honestly how well it actually works.
Why You Cannot Just Hardcode the Rules
Before reaching for machine learning, it is worth asking why you would not simply write: "if the message contains the word 'win,' mark it spam." Try it, and it breaks almost immediately in both directions. It produces false positives — good messages wrongly flagged as spam — because "Congratulations, your quiz team won the inter-school round!" contains "win" too. It also produces false negatives — real spam that slips through — because spammers deliberately misspell trigger words ("w1n," "fr33," "c@sh") specifically to dodge keyword filters. A hardcoded rule is a single brittle guess. What you actually want is a system that weighs many pieces of evidence at once and updates that weighing automatically as new spam patterns appear — which is exactly what learning from data buys you.
Step 1 — Build a Baseline, and Notice How Sneaky It Is
Before building anything clever, always build the simplest possible classifier first, called a baseline. It exists to answer one question: how good does "doing almost nothing" look? Here is the laziest baseline imaginable: always predict ham, no matter what the message says.
Suppose we test it on 100 real messages that humans have already labeled: 10 are genuinely spam, 90 are genuinely ham. Since the baseline predicts "ham" every single time, it gets every one of the 90 real ham messages right, and every one of the 10 real spam messages wrong (it lets all of them through). That gives an accuracy — the fraction of all 100 predictions that were correct — of 90 out of 100, or 90%.
Read that number again: a classifier that examines nothing, learns nothing, and always outputs the same fixed answer scores 90% accuracy. If a friend told you "my spam filter is 90% accurate," you would be impressed — until you realized a filter that does literally nothing hits that exact number, because 90% of the test set happened to be ham to begin with. This is called the accuracy paradox, and it is the single most common mistake beginners make when evaluating a classifier. It happens whenever the classes are imbalanced — one label is much more common than the other, which is true of spam vs. ham in real life (most email is not spam). Accuracy alone cannot be trusted on imbalanced data. You need a sharper tool.
Reading a Confusion Matrix
The sharper tool is called a confusion matrix — a 2-by-2 grid that separates a classifier's mistakes by type, not just by count. For a spam filter, the four cells have standard names:
- True Positive (TP): actually spam, predicted spam — correctly caught.
- False Negative (FN): actually spam, predicted ham — spam that slipped through.
- False Positive (FP): actually ham, predicted spam — a real message wrongly flagged.
- True Negative (TN): actually ham, predicted ham — correctly left alone.
For the always-predict-ham baseline on our 100-message test set: TP = 0 (it never predicts spam, so it can never be a correct spam catch), FN = 10 (all 10 real spam messages predicted as ham), FP = 0 (it never predicts spam, so it can never falsely flag ham), TN = 90 (all 90 real ham messages predicted as ham). Notice those four numbers sum to 100, the full test set, as they always must.
Now suppose we run an actual trained filter (the one you are about to build) on the same 100 test messages, and it produces TP = 7, FP = 5, FN = 3, TN = 85. Check the bookkeeping first: TP + FN = 7 + 3 = 10, the true count of spam messages — correct. FP + TN = 5 + 85 = 90, the true count of ham messages — correct. All four numbers together sum to 100 — correct. The diagram below shows this confusion matrix as a grid, colour-coded by whether the cell represents a correct prediction (green/blue) or a mistake (red/orange).
From these four numbers we can compute three different scores, each answering a different question:
- Accuracy = (TP + TN) / (TP + TN + FP + FN) = (7 + 85) / 100 = 92 / 100 = 92%. "Out of everything, what fraction did I get right?"
- Precision = TP / (TP + FP) = 7 / (7 + 5) = 7 / 12 ≈ 58.3%. "Of the messages I called spam, how many really were spam?"
- Recall = TP / (TP + FN) = 7 / (7 + 3) = 7 / 10 = 70%. "Of all the real spam that existed, how many did I actually catch?"
Compare this honestly to the baseline. Accuracy only rose from 90% to 92% — a two-point gain that might look unimpressive given all the work of building a real classifier. But recall tells the real story: the baseline caught 0% of spam; this filter catches 70% of it. Accuracy alone hid almost the entire improvement. At the same time, precision of 58.3% exposes a real weakness: for every 12 messages this filter flags as spam, roughly 5 are actually perfectly good messages — a fairly high false-alarm rate that a user would notice and find annoying. This is exactly the kind of number CBSE Informatics Practices/Computer Science questions expect you to compute directly from a given confusion matrix, so it is worth re-deriving these three formulas from memory rather than just recognising them.
Step 2 — Teaching the Computer to Look at Words
Now build the actual model. Instead of one hardcoded keyword, we will let the computer estimate, from real labeled examples, how strongly each word is associated with spam versus ham — then combine several words' evidence at once. Suppose our training set has 20 short messages that a human has already labeled: 8 spam and 12 ham. For five words that show up often, here is how many of the training messages in each class contained that word:
| Word | Spam messages containing it (out of 8) | Ham messages containing it (out of 12) |
|---|---|---|
| win | 4 | 1 |
| cash | 5 | 1 |
| now | 6 | 3 |
| free | 5 | 2 |
| meeting | 0 | 6 |
Read the first row in plain language before formalizing anything: "win" appeared in 4 of the 8 spam training messages — exactly half of them — but in only 1 of the 12 ham messages. That is real evidence: seeing "win" in a message should shift your suspicion toward spam. We turn "4 out of 8" into a probability the normal way, by dividing: 4 ÷ 8 = 0.5. In the standard notation you will meet in more advanced material, this is written P(win | spam) = 0.5, read as "the probability of seeing the word 'win,' given that the message is spam." The vertical bar just means "given" — it is not division. Doing the same division for every cell in the table above gives us:
- P(win|spam) = 4/8 = 0.5, P(win|ham) = 1/12 ≈ 0.083
- P(cash|spam) = 5/8 = 0.625, P(cash|ham) = 1/12 ≈ 0.083
- P(now|spam) = 6/8 = 0.75, P(now|ham) = 3/12 = 0.25
- P(free|spam) = 5/8 = 0.625, P(free|ham) = 2/12 ≈ 0.167
- P(meeting|spam) = 0/8 = 0, P(meeting|ham) = 6/12 = 0.5
We also need the overall split of the training set itself: 8 of the 20 training messages are spam, so P(spam) = 8/20 = 0.4, and P(ham) = 12/20 = 0.6. This is called the prior — what you'd guess about a random message's label before reading a single word of it.
To score a new message, we multiply the prior by the per-word probability for every word it contains, once for the "spam" hypothesis and once for the "ham" hypothesis, then see which product is larger. This method is called a Naive Bayes classifier — "Bayes" because it is built from conditional probabilities in the style of the 18th-century mathematician Thomas Bayes, and "naive" because it makes one simplifying assumption: it treats every word's presence as independent of every other word, ignoring grammar, word order, and word-to-word relationships entirely. That assumption is technically false — real language is not word-independent — but the resulting classifier is fast, needs very little training data, and historically worked well enough that it powered some of the earliest practical email spam filters in the early 2000s, an approach popularised by the programmer and essayist Paul Graham in his widely-read 2002 essay on Bayesian spam filtering.
Worked Example: Scoring "win cash now"
Take the message "win cash now" and compute both hypothesis scores by hand, one multiplication at a time.
Spam score = P(spam) × P(win|spam) × P(cash|spam) × P(now|spam)
= 0.4 × 0.5 × 0.625 × 0.75
Multiply left to right: 0.4 × 0.5 = 0.2. Then 0.2 × 0.625 = 0.125. Then 0.125 × 0.75 = 0.09375.
Ham score = P(ham) × P(win|ham) × P(cash|ham) × P(now|ham)
= 0.6 × (1/12) × (1/12) × 0.25
Multiply left to right: 0.6 × (1/12) = 0.05. Then 0.05 × (1/12) ≈ 0.004167. Then 0.004167 × 0.25 ≈ 0.0010417.
The spam score (0.09375) is roughly 90 times larger than the ham score (0.0010417), so the classifier confidently predicts SPAM — correctly. Notice these two numbers are not probabilities that sum to 1 (they don't need to, and they don't); they are just relative scores, and Naive Bayes only ever needs to know which of the two is bigger, not what they individually mean in isolation. The chart below shows the size difference directly — the ham bar has been kept to a minimum visible height so it does not disappear entirely, since in true proportion it would be less than one pixel tall.
Tracing the Classifier in Python
Here is the exact same computation as a program, so you can check that the "by hand" arithmetic above and the code agree digit for digit.
p_spam = 0.4 # P(spam) = 8/20
p_ham = 0.6 # P(ham) = 12/20
word_probs = {
"win": {"spam": 4/8, "ham": 1/12},
"cash": {"spam": 5/8, "ham": 1/12},
"now": {"spam": 6/8, "ham": 3/12},
"free": {"spam": 5/8, "ham": 2/12},
"meeting": {"spam": 0/8, "ham": 6/12},
}
def score(message, label, prior):
total = prior
for word in message.split():
probs = word_probs.get(word)
if probs is not None:
total *= probs[label]
return total
def classify(message):
spam_score = score(message, "spam", p_spam)
ham_score = score(message, "ham", p_ham)
verdict = "SPAM" if spam_score > ham_score else "HAM"
return verdict, spam_score, ham_score
verdict, s, h = classify("win cash now")
print(verdict)
print(round(s, 6), round(h, 6))
Trace it exactly as Python would run it. Inside score("win cash now", "spam", 0.4), total starts at 0.4. The loop splits the message into the words ["win", "cash", "now"] and visits them in order. For "win," total becomes 0.4 × 0.5 = 0.2. For "cash," it becomes 0.2 × 0.625 = 0.125. For "now," it becomes 0.125 × 0.75 = 0.09375, which the function returns. The ham call follows the identical path with the ham probabilities and returns approximately 0.0010417. Back in classify, since 0.09375 > 0.0010417, verdict is set to "SPAM". The final two lines print SPAM on the first line, then 0.09375 0.001042 on the second — the second number is the decimal equivalent of the ham score rounded to six places, matching the hand calculation above exactly.
The Zero-Frequency Trap
Try the same code on a different message: classify("free meeting now"). Walk through the spam score by hand: 0.4 × P(free|spam) × P(meeting|spam) × P(now|spam) = 0.4 × 0.625 × 0 × 0.75. The moment that multiplication hits P(meeting|spam) = 0, the entire product becomes exactly 0 — no matter how spammy the other words in the message are. Meanwhile the ham score works out to 0.6 × 0.167 × 0.5 × 0.25 ≈ 0.0125, which is greater than zero, so the message is classified HAM. In this particular case that happens to be the right answer, but for a troubling reason: the word "meeting" simply never occurred in any of our 8 spam training examples, so our table says its spam-probability is a hard zero, and one hard zero anywhere in the multiplication wipes out every other word's evidence. A spammer who knows this could deliberately pad a message with a word your training data never saw in spam — "team meeting: win free cash now" — and guarantee a spam score of exactly zero, regardless of anything else in the message. This is a genuine, well-known weakness of Naive Bayes called the zero-frequency problem, and it is different from the case of a word your model has never seen at all (not in the table in either class) — the code above simply skips those with word_probs.get(word) returning None, treating them as uninformative rather than catastrophic. Real spam filters fix the zero-count case with a technique called Laplace smoothing — adding a small constant (often 1) to every count before dividing, so that no probability is ever allowed to reach exactly zero. You will not need to compute smoothed probabilities in Grade 9, but you should be able to explain, as you now can, why the unsmoothed version is fragile.
Step 3 — Evaluate Honestly, on Data the Model Never Trained On
It would be tempting to check how well our classifier works by running it on the same 20 messages used to build the word-probability table. Do not do this — it tells you almost nothing. A model can "memorize" quirks of its own training data (this failure mode is called overfitting) and look artificially perfect on it while performing much worse on messages it has never encountered. This is precisely why the 100-message confusion matrix (TP=7, FP=5, FN=3, TN=85) used earlier in this chapter came from a separate test set — 100 fresh, previously unseen messages, kept apart from training from the start specifically so the accuracy/precision/recall numbers we compute from it are trustworthy. This training/test split is one of the most important habits in all of machine learning, well beyond spam filtering, and CBSE's AI curriculum tests it directly as part of the "Modelling" stage of the AI Project Cycle.
Precision vs. Recall: Two Different Kinds of Mistakes
A false positive and a false negative are not equally costly, and which one matters more depends entirely on what the filter is protecting. If a spam filter has a false positive, a genuine message — say, a scholarship result or a college admission email — gets buried in the spam folder, and a student might never see it in time. If it has a false negative, one junk message slips into the inbox, which is mildly annoying but rarely damaging. Because of this asymmetry, most real email providers deliberately tune their filters to favour precision over recall — they would rather let a few extra spam messages through than risk silently hiding something important. There is no single "correct" balance; it is a design decision made by weighing the two costs, and a well-answered exam question on this topic should name both failure types and say which one is worse for the specific scenario given, rather than just quoting the formulas.
Common Misconception: "High Accuracy Means a Good Classifier"
This is false, and the baseline classifier earlier in this chapter is the proof: it achieves 90% accuracy while catching zero spam and using zero intelligence. Accuracy only becomes a trustworthy number when the two classes are roughly balanced in the test set. Whenever one label vastly outnumbers the other — which is the normal situation for spam detection, medical screening, and fraud detection alike — always report precision and recall (or, at minimum, check what a do-nothing baseline scores) before trusting an accuracy figure at all.
Common Misconception: "Naive" Means the Classifier Is Bad or Unreliable
The name is confusing on first encounter. "Naive" here is a precise technical description of one specific assumption the model makes — that the presence of each word is statistically independent of every other word — not a judgement that the whole method is weak. That assumption is literally false for real sentences (word order and combinations of words obviously carry meaning that isolated words don't), yet the classifier you built above still correctly separated "win cash now" from ham with a 90-times score gap, and its real-world descendants filtered a meaningful share of global email spam for years. A model can rest on a simplifying, technically-inaccurate assumption and still be extremely useful — the two facts are not in conflict.
Summary
A classifier is a function that sorts input into one of a fixed set of labels; a spam filter sorts messages into spam or ham. Before building anything sophisticated, always measure a baseline, because with imbalanced classes a baseline that does nothing can still post a deceptively high accuracy — always check precision and recall, computed from a confusion matrix's four counts (TP, FP, FN, TN), before trusting an accuracy number. A Naive Bayes classifier learns, from labeled training data, how often each word appears in spam versus ham, converts those counts into probabilities, and multiplies a message's word-probabilities together (with the class prior) to see which label scores higher — an approach that is fast and effective despite its "naive" independence assumption, though it is vulnerable to the zero-frequency problem when a word never appeared in one class during training. A model must always be evaluated on a separate test set it never trained on, because performance on the training data itself is not a trustworthy measure. Finally, false positives and false negatives are different kinds of mistakes with different real costs, and a spam filter is tuned by deciding, deliberately, which kind of mistake is worse to make.
Practice Questions
Q1. Suppose the filter is retrained and re-tested on the same 100 messages (10 actual spam, 90 actual ham), producing a new confusion matrix: TP = 9, FP = 8, FN = 1, TN = 82. First verify the bookkeeping (do TP+FN and FP+TN match the known 10 spam / 90 ham split?). Then compute its accuracy, precision, and recall, and decide: is this new version clearly better than the original filter (TP=7, FP=5, FN=3, TN=85), clearly worse, or a trade-off? Justify your answer using the actual numbers.
Answer. Check: TP+FN = 9+1 = 10 ✓, FP+TN = 8+82 = 90 ✓. Accuracy = (9+82)/100 = 91%. Precision = 9/(9+8) = 9/17 ≈ 52.9%. Recall = 9/(9+1) = 90%. Compared with the original (accuracy 92%, precision 58.3%, recall 70%): this version is not simply "better" — it is a trade-off. Recall jumped sharply (70% → 90%), meaning it now catches far more real spam, but precision dropped (58.3% → 52.9%) and accuracy fell slightly (92% → 91%), meaning it now falsely flags proportionally more good messages. Whether this new version is an improvement depends on whether missed spam or wrongly-flagged good mail is the costlier mistake for this particular user.
Q2. Recall that the baseline classifier from earlier in this chapter predicts ham for every single message, with no exceptions. Using the same 100 test messages (10 actual spam, 90 actual ham), work out the baseline's own TP, FP, FN, and TN from that rule, then compute its precision and recall. Something unusual happens when you try to compute one of the two — explain what, and why.
Answer. Because the baseline always predicts ham, it never predicts spam, so TP = 0 and FP = 0. All 10 real spam messages are wrongly predicted ham, so FN = 10. All 90 real ham messages are correctly predicted ham, so TN = 90. Recall = TP/(TP+FN) = 0/10 = 0% — it catches no spam at all. Precision = TP/(TP+FP) = 0/0, which is — division by zero. This makes sense conceptually: precision asks "of the messages I called spam, how many really were," but the baseline never calls anything spam, so there is nothing for precision to even be measured against.
Q3. Using the word-probability table from this chapter, compute the spam score and ham score for the message "cash meeting" by hand, state which label the classifier would output, and explain whether the zero-frequency problem is responsible for that result.
Answer. Spam score = P(spam) × P(cash|spam) × P(meeting|spam) = 0.4 × 0.625 × 0 = 0. Ham score = P(ham) × P(cash|ham) × P(meeting|ham) = 0.6 × (1/12) × 0.5 = 0.6 × 0.0833 × 0.5 ≈ 0.025. Since 0.025 > 0, the classifier outputs HAM. Yes, the zero-frequency problem is exactly what caused this: "meeting" had zero occurrences among the 8 spam training messages, so P(meeting|spam) = 0, and multiplying by zero forces the entire spam score to zero regardless of how suspicious "cash" is on its own.
Q4. In one or two sentences, explain what the word "naive" refers to in "Naive Bayes classifier," and why that assumption being technically false does not automatically make the classifier useless.
Answer. "Naive" refers to the assumption that every word's presence in a message is statistically independent of every other word — the model never considers word order or combinations, only individual word frequencies. This assumption is not literally true of real language, but the classifier can still separate spam from ham effectively in practice, because even without modelling word relationships, individual word frequencies alone carry enough signal to make the right call most of the time — as shown by the roughly 90-times score gap between spam and ham for "win cash now" earlier in this chapter.
Think About It
Think about this: How would you explain your first ml classifier: building a spam filter to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.