flowchart LR A["'Trash, uninstall!!'"] --> B["lowercase<br/>'trash, uninstall!!'"] B --> C["remove punctuation<br/>'trash uninstall'"] C --> D["split on spaces<br/>['trash', 'uninstall']"]
IAT 461 / 882 · Data Science for Human-Centered Systems · Summer 2026 · Alireza Karduni
Which ones are abusive? It’s not always obvious.
But models compute with numbers, not sentences. First, we need to turn text into numbers.
flowchart LR A["'Trash, uninstall!!'"] --> B["lowercase<br/>'trash, uninstall!!'"] B --> C["remove punctuation<br/>'trash uninstall'"] C --> D["split on spaces<br/>['trash', 'uninstall']"]
We’ll come back to that last one — for abuse detection, “you” might actually matter.
| # | Message | Class |
|---|---|---|
| 1 | trash uninstall noob game | Abusive |
| 2 | idiot trash chat | Abusive |
| 3 | well played gg chat | Clean |
| 4 | nice play well game | Clean |
Small on purpose — small enough to calculate by hand.
Every unique token across all four messages:
chat, game, gg, idiot, noob, nice, play, played, trash, uninstall, well
11 words. This is our vocabulary.
Each message becomes a row of word counts:
| Message | chat | game | gg | idiot | noob | nice | play | played | trash | uninstall | well |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 (Abusive) | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
| 2 (Abusive) | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
| 3 (Clean) | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
| 4 (Clean) | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 |
This is bag-of-words: word order is thrown away, only counts remain.
\[P(A \mid B) = \frac{P(B \mid A)\, P(A)}{P(B)}\]
Four pieces. Let’s name each one.
\[P(A \mid B) = \frac{P(B \mid A)\, P(A)}{P(B)}\]
\[P(\text{class} \mid \text{message}) = \frac{P(\text{message} \mid \text{class})\, P(\text{class})}{P(\text{message})}\]

2 Abusive + 2 Clean messages → P(Abusive) = 0.5, P(Clean) = 0.5. No fancy math — just counting.
\[P(\text{class} \mid \text{message}) \;\propto\; P(\text{message} \mid \text{class}) \, P(\text{class})\]
\[P(\text{message} \mid \text{class}) \;\approx\; \prod_{i} P(\text{word}_i \mid \text{class})\]
Next: let’s calculate this by hand on our toy dataset.
| Class | chat | game | gg | idiot | noob | nice | play | played | trash | uninstall | well | Total |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Abusive | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 2 | 1 | 0 | 7 |
| Clean | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 2 | 8 |
Vocabulary size: 11 words. Watch trash and game — that’s our test message.
\[P(\text{word} \mid \text{class}) = \frac{\text{count(word, class)}}{\text{total words in class}}\]
Just a proportion — how often this word shows up, within this class.

2 of the 7 words in Abusive messages are “trash.”

Only 1 of the 7 — “game” shows up, but it’s not a strong Abusive signal on its own.

Not one square is colored — “trash” never appears in a Clean message.

1 of 8 — about the same odds as in Abusive. This word isn’t telling us much.
\[P(\text{word} \mid \text{class}) = \frac{\text{count(word, class)} + 1}{\text{total words in class} + |V|}\]

Same idea applies to Clean: 8 real + 11 ghost = 19 total.
| Class | P(trash | class) | P(game | class) |
|---|---|---|
| Abusive | (2+1)/(7+11) = 0.167 | (1+1)/(7+11) = 0.111 |
| Clean | (0+1)/(8+11) = 0.053 | (1+1)/(8+11) = 0.105 |
“trash” is no longer a hard zero — just a small number.
\[P(\text{message} \mid \text{class}) = P(\text{word}_A \mid \text{class}) \times P(\text{word}_B \mid \text{class}) \times \dots\]
For “trash game,” each class has exactly 2 words to multiply:
\[P(\text{message} \mid \text{class}) = P(\text{trash} \mid \text{class}) \times P(\text{game} \mid \text{class})\]
| Class | Prior | × Likelihood | = Posterior (unnormalized) |
|---|---|---|---|
| Abusive | 0.5 | 0.0185 | 0.00926 |
| Clean | 0.5 | 0.0055 | 0.00277 |

\(0.00926 > 0.00277\) → classify “trash game” as Abusive

Words that show up equally in every class carry little information — the model figures this out from data, without being told which words matter.
flowchart LR A["tokens<br/>[not, toxic, very, kind]"] --> B["slide a window of 2<br/>across the tokens"] B --> C["bigrams<br/>[(not,toxic), (toxic,very), (very,kind)]"]
"this streamer is not toxic, very kind"
(this,streamer) → (streamer,is) → (is,not) → (not,toxic) → (toxic,very) → (very,kind)
"this streamer is very toxic, not kind"
(this,streamer) → (streamer,is) → (is,very) → (very,toxic) → (toxic,not) → (not,kind)
Same starting words, but the sequences split apart right where the meaning flips.

Naive Bayes now has something to actually distinguish these two messages by.

"idiot" → idi dio iot
"idi0t" → idi di0 i0t
Word-level tokenization sees “idiot” and “idi0t” as completely different, unrelated tokens — zero overlap.
Character trigrams still share “idi” — some signal survives the obfuscation.
\[TF(\text{word}, \text{doc}) = \frac{\text{count(word, doc)}}{\text{total words in doc}}\]
This is the same calculation we already did for Naive Bayes likelihoods — just applied per-document here instead of per-class.
| Word | Appears in docs | DF |
|---|---|---|
| chat | 2, 3 | 2 |
| idiot | 2 | 1 |
| well | 3, 4 | 2 |
Only 4 documents total in our toy corpus — so DF can only be 1, 2, 3, or 4.
\[IDF(\text{word}) = \log \left( \frac{N}{DF(\text{word})} \right)\]

Gray = shared by both classes · Red = Abusive-only · Blue = Clean-only
IDF measures rareness, not classification usefulness — those are related, but not the same thing.
\[ \begin{aligned} TF\text{-}IDF(\text{word}, \text{doc}) &= TF(\text{word}, \text{doc}) \\ &\times IDF(\text{word}) \end{aligned} \]
High score = frequent in this message, and rare across the corpus. That’s what makes a word distinctive.

Both words appear once in their message — same TF story almost. But “idiot” is rarer across the corpus, so it ends up with the higher TF-IDF score.
Count vectors tell you what’s there. TF-IDF tells you what’s distinctive.
Real messages, real vocabulary — nowhere near 11 words.

Each message is still just one row — but now it’s a row with 50,000 columns.

That’s just 300 columns shown. A real vector might be 50,000 columns wide — almost entirely zero.

This time, two messages break the pattern: an Abusive one contains “well,” and a Clean one doesn’t.

Not a perfect split this time — both sides still contain a mix of classes. The tree needs to go one level deeper.

“well trash noob” gives itself away on the second question — pure leaves now.

Same idea on the other branch — one more question, and it’s pure too.

Two levels of questions, four pure leaves. Every leaf now contains only one class.
This is the overfitting risk in miniature — and exactly why random forests average across many trees instead of trusting one.

Any single tree can overfit. A crowd of imperfect trees, voting together, usually can’t.
As designers of these systems, the question isn’t just “does this model work?” — it’s “who does it work for, and who does it fail?”
IAT 461 · Data Science for Human-Centered Systems · Summer 2026