Co-occurrence Matrices › Code Implementation
Language AI HandbookAll chapters Co-occurrence Matrices › Code ImplementationOne section · Sections in this chapter9 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's implement co-occurrence matrix construction from scratch, then use it to demonstrate the key concepts.
We will start by building a simple corpus and vocabulary pipeline.
# Sample corpus
corpus = [
"the cat sat on the mat",
"the cat ate the rat",
"the dog sat on the log",
"the dog ran in the park",
"the cat chased the rat in the park",
"dogs and cats are common pets",
"a cat or a dog makes a good pet",
"the rat ran from the cat",
"cats and dogs play in the garden",
"the mat sat under the cat",
]
# Tokenize and build vocabulary
all_tokens = []
sentences = []
for sent in corpus:
tokens = sent.lower().split()
sentences.append(tokens)
all_tokens.extend(tokens)
# Count frequencies and build vocabulary (keep words with freq >= 2)
from collections import Counter
freq = Counter(all_tokens)
vocab = sorted(w for w, c in freq.items() if c >= 2)
word2idx = {w: i for i, w in enumerate(vocab)}
idx2word = {i: w for w, i in word2idx.items()}
V = len(vocab)Corpus size: 10 sentences, 65 tokens Vocabulary size: 14 words (after frequency cutoff of 2) Vocabulary: ['a', 'and', 'cat', 'cats', 'dog', 'dogs', 'in', 'mat', 'on', 'park', 'ran', 'rat', 'sat', 'the']
Our corpus has a small vocabulary after applying the minimum frequency cutoff. In practice, corpora have vocabularies of 50,000 to 500,000 words, but the construction logic is identical.
Now let's build the word-word co-occurrence matrix with uniform weights, then with distance weighting, to compare them.
def build_cooccurrence_matrix(
sentences, word2idx, window_size=2, weighted=False
):
"""Build a word-word co-occurrence matrix.
Args:
sentences: List of tokenized sentences
word2idx: Mapping from word to index
window_size: Context window half-size
weighted: If True, weight co-occurrences by 1/distance
Returns:
Dense numpy array of shape (V, V)
"""
V = len(word2idx)
C = np.zeros((V, V), dtype=float)
for sent in sentences:
# Map tokens to indices, skipping OOV words
indices = [word2idx[w] for w in sent if w in word2idx]
for i, target_idx in enumerate(indices):
# Context window bounds
left = max(0, i - window_size)
right = min(len(indices), i + window_size + 1)
for j in range(left, right):
if i == j:
continue
context_idx = indices[j]
distance = abs(i - j)
weight = 1.0 / distance if weighted else 1.0
C[target_idx, context_idx] += weight
return C
# Build both versions
C_uniform = build_cooccurrence_matrix(
sentences, word2idx, window_size=2, weighted=False
)
C_weighted = build_cooccurrence_matrix(
sentences, word2idx, window_size=2, weighted=True
)Co-occurrence matrix (uniform weights) for 'cat': the : 7.0 a : 2.0 rat : 2.0 sat : 2.0 dog : 1.0 on : 1.0 ran : 1.0
Co-occurrence matrix (distance-weighted) for 'cat': the : 7.00 a : 2.00 sat : 1.50 rat : 1.00 dog : 0.50 on : 0.50 ran : 0.50
Notice that distance weighting shifts the emphasis away from "the" (which appears near everything but at variable distances) and toward words that consistently appear immediately adjacent to "cat." The function word "the" is still present, but its relative weight decreases because many of its co-occurrences with "cat" happen at distance 2 rather than distance 1. Words that co-occur reliably at distance 1 get full weight, and their signal is more prominent in the weighted matrix. This tends to give better semantic signal in downstream similarity computations.
Now let's examine the sparsity of the matrix.
total_entries = V * V
nonzero_count = np.count_nonzero(C_uniform)
sparsity = 1.0 - (nonzero_count / total_entries)
density = nonzero_count / total_entriesMatrix dimensions: 14 x 14 = 196 total entries Nonzero entries: 68 Density: 34.7% Sparsity: 65.3% Memory (dense, float64): 1.5 KB Memory (sparse COO): 1.6 KB (value + row + col)
Even in this tiny example, the matrix is sparse. For a real vocabulary of 50,000 words, over 99.9% of entries would be zero.
Now let's build the word-document matrix and compare what it captures versus the word-word matrix.
def build_word_document_matrix(corpus, word2idx):
"""Build a word-document co-occurrence matrix.
Returns:
Dense array of shape (V, D)
"""
V = len(word2idx)
D = len(corpus)
M = np.zeros((V, D), dtype=float)
for doc_idx, sent in enumerate(corpus):
tokens = sent.lower().split()
for word in tokens:
if word in word2idx:
M[word2idx[word], doc_idx] += 1
return M
M_doc = build_word_document_matrix(corpus, word2idx)Word-document matrix shape: (14, 10) (V=14 words x D=10 documents) 'cat' appears in documents: [0, 1, 4, 6, 7, 9] 'dog' appears in documents: [2, 3, 6] Shared documents (cat & dog): [np.int64(6)]
# Compare word similarity from word-word vs word-document matrices
from numpy.linalg import norm
def cosine_similarity(u, v):
"""Compute cosine similarity between two vectors."""
u_norm = norm(u)
v_norm = norm(v)
if u_norm == 0 or v_norm == 0:
return 0.0
return np.dot(u, v) / (u_norm * v_norm)
# Find most similar words to 'cat' in each matrix
target = "cat"
target_idx = word2idx[target]
# Word-word similarities
ww_sims = {}
for w, idx in word2idx.items():
if w != target:
ww_sims[w] = cosine_similarity(C_uniform[target_idx], C_uniform[idx])
# Word-document similarities
wd_sims = {}
for w, idx in word2idx.items():
if w != target:
wd_sims[w] = cosine_similarity(M_doc[target_idx], M_doc[idx])Top-5 words similar to 'cat' (word-word matrix): mat : 0.905 ran : 0.866 sat : 0.829 rat : 0.808 in : 0.800 Top-5 words similar to 'cat' (word-document matrix): the : 0.770 rat : 0.707 mat : 0.577 sat : 0.471 a : 0.408
The word-word matrix tends to surface words that appear in the same immediate grammatical contexts (words that co-occur locally with "cat"), while the word-document matrix surfaces words that appear in the same documents (thematically related words). Both are valid and useful depending on the downstream task.
Visualizing the Co-occurrence Structure
Let's visualize the co-occurrence matrix as a heatmap and the effect of window size on the captured relationships.
# Build matrices with different window sizes to compare
C_window1 = build_cooccurrence_matrix(
sentences, word2idx, window_size=1, weighted=False
)
C_window4 = build_cooccurrence_matrix(
sentences, word2idx, window_size=4, weighted=False
)

The heatmaps reveal the qualitative difference between small and large windows. With window size 1, only words that are grammatically adjacent accumulate high counts, and most cells remain at or near zero. The matrix is visually sparse. With window size 4, the matrix fills in substantially, and the rows corresponding to high-frequency function words ("the," "a," "in") become uniformly bright because these words co-occur with nearly every other word within a wide window. The content words still show more differentiated patterns, but they are increasingly dominated by function word noise.



The comparison highlights the complementary strengths of each matrix type. In the word-word matrix, words that function similarly in sentences tend to cluster near each other, even if they appear in different documents. In the word-document matrix, words that populate similar topics and genres have high similarity, even if they rarely appear in the same sentence. For semantic search and information retrieval tasks, the word-document matrix is usually more appropriate. For semantic similarity at the word level, the word-word matrix is typically preferred.
Window Size Effects on Similarity
Let's quantify how window size changes the semantic neighborhood of words.
# Build matrices at several window sizes
window_sizes = [1, 2, 3, 4, 5]
target_word = "dog"
target_idx = word2idx[target_word]
compare_word = "cat"
compare_idx = word2idx[compare_word]
# Track similarity between 'dog' and 'cat' across window sizes
dog_cat_sims = []
for ws in window_sizes:
C = build_cooccurrence_matrix(
sentences, word2idx, window_size=ws, weighted=False
)
sim = cosine_similarity(C[target_idx], C[compare_idx])
dog_cat_sims.append(sim)
The window size effect is clear: as the window grows, "dog" and "cat" accumulate more shared context words ("the," "park," "ran," "in"), which pushes their cosine similarity higher. This confirms the theoretical prediction: larger windows capture more of the topical similarity between semantically related words. A small window captures only the tightest syntactic patterns, where "dog" and "cat" might differ (because they appear in slightly different sentence frames), while a large window captures the broader distributional environment they share.
Sparse Matrix Construction
For real-scale corpora, we need sparse storage. Let's demonstrate building a sparse co-occurrence matrix using scipy's CSR format, and compare memory usage against dense storage.
from scipy.sparse import lil_matrix
def build_sparse_cooccurrence(
sentences, word2idx, window_size=2, weighted=False
):
"""Build co-occurrence matrix in sparse LIL format, then convert to CSR."""
V = len(word2idx)
# LIL (List of Lists) format is efficient for incremental construction
C = lil_matrix((V, V), dtype=float)
for sent in sentences:
indices = [word2idx[w] for w in sent if w in word2idx]
for i, target_idx in enumerate(indices):
left = max(0, i - window_size)
right = min(len(indices), i + window_size + 1)
for j in range(left, right):
if i == j:
continue
context_idx = indices[j]
distance = abs(i - j)
weight = 1.0 / distance if weighted else 1.0
C[target_idx, context_idx] += weight
# Convert to CSR for efficient row-wise operations
return C.tocsr()
C_sparse = build_sparse_cooccurrence(
sentences, word2idx, window_size=2, weighted=True
)Sparse matrix: (14, 14), 68 nonzeros Dense storage: 1.5 KB Sparse storage: 0.9 KB Memory savings: 44.1% Projected for V=50,000 vocabulary: Dense storage: 20.0 GB Sparse storage: 10408 MB (assuming same sparsity fraction)
The memory advantage of sparse storage scales quadratically with vocabulary size. A vocabulary of 50,000 words would require 20 GB of RAM in dense format but only hundreds of megabytes in sparse format, making large-scale co-occurrence matrices practically tractable. This quadratic scaling is the reason sparse storage is non-negotiable for real applications: the savings become astronomically large as vocabulary size grows.
Visualizing the Count Distribution
One of the most instructive properties of co-occurrence matrices is the distribution of their nonzero counts. Let's visualize it to see the power law behavior in action.
# Get all nonzero counts from the uniform matrix
nonzero_counts = C_uniform[C_uniform > 0].flatten()
nonzero_counts_sorted = np.sort(nonzero_counts)[::-1]
The histogram confirms the heavy-tailed structure: most observed word pairs co-occur only once or twice, while a few pairs involving function words accumulate much higher counts. On a log scale, this distribution has a roughly linear shape, which is characteristic of power laws. The long tail of high counts is dominated by common grammatical patterns: "the" co-occurring with every noun, "in" co-occurring with every location noun, and so on. These patterns carry almost no discriminative semantic information, which is precisely why PMI weighting (coming in the next chapter) is essential.
Key Parameters
The key parameters for co-occurrence matrix construction are:
- window_size: Controls the context window half-size . Smaller values (1-2) capture syntactic relationships; larger values (5-10) capture semantic and topical relationships.
- weighted: Whether to apply harmonic distance weighting (). Distance weighting reduces function word dominance and typically improves downstream word vector quality.
- min_frequency: Minimum word frequency for vocabulary inclusion. Rare words have unreliable statistics; values of 5-10 are common in practice.
- symmetric: Whether to treat left and right contexts equally (symmetric) or separately (directional). Symmetric is standard for most semantic similarity tasks.
- sentence_boundary: Whether context windows respect sentence boundaries. Enforcing sentence boundaries is linguistically motivated and standard practice.