Bag-of-Words Representation
A bag-of-words representation describes a text as its word occurrences, treating it as a bag or set and ignoring grammar and word order.
A Review Cut Into a Bag
A classifier cannot read a movie review the way a person does. It learns from numbers, so a review written as a sentence has to be converted into a numeric form before any learning can happen. The bag-of-words representation is one of the simplest ways to make that conversion. It describes a document by its word occurrences: which words appear and how many times each one appears.
What do you think happens?
Two sentences use exactly the same three words: 'dog bites man' and 'man bites dog'. Before reading on, predict whether a bag-of-words representation gives them the same count vector or different count vectors.
Reveal answer
The same count vector
Each sentence contains dog, bites, and man exactly once. A bag keeps the words and their counts but throws away their arrangement, so the two sentences look identical to the representation even though they mean different things.
Keep that surprise in mind. The rest of this article follows the representation step by step, so you can see exactly where the order of words is lost and where the counts come from.
What Fit Learns: The Vocabulary
Building a bag-of-words representation takes two steps. First, the text is split into tokens, which are the units you choose to count, most often individual words. Second, the distinct tokens that appear across the whole collection are gathered into a vocabulary, and that vocabulary is given a fixed order. When you call fit on a CountVectorizer, both of these things happen: it tokenizes the training text and builds the vocabulary from the unique words those tokens produce.
The learned vocabulary is stored in the vocabulary_ attribute. You read it the same way you would read any attribute of an object. Its keys are words and its values are column indexes. The printed dictionary is not in the order of the sentences. It is the mapping from each word to its position, and that position is what the later counting step relies on.
In the two-sentence example the source uses, the index numbers run from 0 to 12, so the 13 vocabulary words occupy 13 positions. Read in alphabetical order, 'be' is index 0, 'but' is index 1, 'doth' is index 2, and 'wise' is index 12. The vocabulary runs from 'be' to 'wise', and the words that appear in both sentences sit at indexes 3, 9, and 12, which are 'fool', 'the', and 'wise'. Notice that the diagram above shows only the words needed to read those indexes; the vocabulary contains every distinct word.
{'acting': 0, 'boring': 1, 'great': 2, 'movie': 3}- fit tokenizes both documents and builds the vocabulary; it does not yet produce any counts
- vocabulary_ maps each learned word to its column index
- The indexes follow the sorted word order, not the order of the sentences
Building the Count Matrix With transform
To produce the bag-of-words data, call transform on the same list of documents. The result is a matrix with one row per document and one column per vocabulary word. Each cell holds a count: how often that column's word appears in that row's document. Every document ends up with a vector of the same length, because every document is measured against the same vocabulary.
Counting one document against a fixed vocabulary
The vocabulary is acting, boring, great, movie, in that fixed order. Find the count vector for the document great movie great acting.
- 1Read the vocabulary positions
acting is position 0, boring is position 1, great is position 2, and movie is position 3.
- 2Count acting
acting appears once in the document, so position 0 holds 1.
- 3Count boring
boring never appears in this document, so position 1 holds 0. It is still in the vocabulary, which keeps every vector the same length.
- 4Count great
great appears twice, so position 2 holds 2.
- 5Count movie
movie appears once, so position 3 holds 1.
The count vector is [1, 0, 2, 1]. Each number lines up with a vocabulary word in the same fixed order.
The same counting procedure can be written directly in Python. The sketch below uses plain whitespace splitting as a stand-in for tokenization, which is enough to show the mechanics. It builds a sorted vocabulary from all documents and then produces one count vector per document.
['acting', 'boring', 'great', 'movie']
[1, 0, 2, 1]
[0, 1, 0, 1]doc.split()is the whitespace tokenizer used as a stand-in here- The vocabulary is built from every document, then fixed in sorted order
- Each inner list has one position per vocabulary term, so both vectors have length 4
Each printed list is one document's bag-of-words vector, and the position of each number matches the position of a term in the printed vocabulary. A library tool such as CountVectorizer handles tokenization more carefully, but the representation it produces has this same shape.
Reading the Sparse Storage Format
The matrix returned by transform is stored as a SciPy sparse matrix, which keeps only the nonzero entries. This matters because most documents use only a small part of the vocabulary, so most cells in the grid are zero. Storing every zero would waste memory.
The summary that repr prints for the transform result reports three things: the shape, which is 2 rows by 13 columns; the element type, which is numpy.int64; and the storage format, which is a sparse matrix in Compressed Sparse Row form holding 16 stored elements. A full grid for these two sentences would have 26 cells, so only 16 are nonzero and the other 10 are not stored at all. In a real corpus, such as movie reviews compared against the whole English vocabulary, the share of zeros is far larger.
A sparse matrix hides its zeros, so reading an individual count from it is awkward. The easiest way to check a count is to convert the matrix to a dense NumPy array with toarray. The dense version stores every cell, zeros included, so you can read any count directly. The code below uses the same two documents as before.
(2, 4)
[[1 0 2 1]
[0 1 0 1]]fit_transformdoes fitting and counting in one call- shape gives rows then columns: one row per document, one column per vocabulary word
- toarray turns the stored entries into a full grid with every zero visible
Tracing a Count Back to Its Cause
When a count looks wrong, the useful question is where your expectation and the output part ways. Three links connect a sentence to a cell in the matrix, and you can check them in order. First, the input text: is the sentence exactly what you think it is? Second, the column that vocabulary_ gives for the word: which index does the word actually have? Third, the value at that column in the dense row: is the number there the one you expected?
Common mistake Reading the vocabulary in the order of the sentences
What it looks likeAssuming the first word of the first sentence is column 0 because it appears first in the text.
Why it failsvocabulary_ is a word-to-index mapping, and its indexes follow the fixed vocabulary order, not the order of appearance.
Do this insteadLook up the word's index in vocabulary_, then read that column.
Common mistake Reading a count from the sparse matrix directly
What it looks likePrinting the transform result and trying to read a particular cell from its summary.
Why it failsThe sparse form stores only nonzero entries, so a zero cell is not visible in the output at all.
Do this insteadConvert with toarray and then read the value in the correct row and column.
Common mistake Reading the wrong row
What it looks likeChecking the second document's count in the first row of the dense array.
Why it failsEach row is one document, in the same order as the input list.
Do this insteadMatch the document's position in the list to its row index before reading the value.
Common mistake Assuming tokenization matches what you see
What it looks likeExpecting a word to be counted in a place where punctuation or capitalization changes how the text is split.
Why it failsThe count depends on the tokens the tokenizer produces, not on the words as you read them on the page.
Do this insteadCheck how the text is tokenized, then confirm that the token exists in vocabulary_.
Why Word Order Disappears
The name is a metaphor. Take a review, cut it into individual words, and drop every word into a bag. Once the words are in the bag, you no longer know which came first, which words were attached to which, or how the sentence was built. What you still know is which words appeared and how many times. A document is therefore represented as a bag, or set, of its word occurrences.
Where the Bag Helps Classifiers
The source names three kinds of task where bag-of-words representations are a strong starting point: spam and fraud detection, sentiment analysis, and the movie review classification used as the running example. In each case, the question is which category a document belongs to, and the words it uses are often a useful clue. A message full of promotional wording or a review full of praising words gives a classifier something to work with, even without any understanding of sentence structure.
The same ideas extend to the terms the representation is built from. Tokenization decides what counts as a unit to count, and n-grams are groups of consecutive tokens. Choosing these units is part of the representation work, not a separate afterthought. Because the vocabulary is readable, you can also examine the tokens and features a model depends on. The source encourages this kind of introspection for text applications, both supervised and unsupervised, rather than treating the model as a black box.
Practice Reading a Bag
Two documents are: 'spam offer spam' and 'meeting notes'. Build the sorted vocabulary from both documents, then write the count vector for each document in that vocabulary order. Finally, state the index that vocabulary_ would give to the word offer.
Hints
- Gather every distinct word from both documents first, then sort them alphabetically.
- The vocabulary has four distinct words, so each count vector has four positions.
- A word that does not appear in a document still gets a 0 in that document's vector.
Key Takeaways
- CountVectorizer tokenizes text during fit and stores the learned words and their column indexes in vocabulary_, which is a word-to-index mapping rather than an ordered list of sentences.
- transform produces one row per document and one column per vocabulary word, where each cell holds that word's count in that document, and the matrix is stored sparsely with only nonzero entries kept.
- Use toarray to read every count directly, and when a count looks wrong, check the input text, the word's column in vocabulary_, and the value in the correct row, in that order.
- A bag-of-words representation discards grammar and word order on purpose, so sentences with the same words in different arrangements receive identical vectors, a trade that still suits spam detection, sentiment analysis, and similar classification tasks.