Writing-style fingerprints
Why it matters People write "anonymous" reviews, posts and tips believing nobody can tell who wrote them. But your writing has habits as personal as your typing rhythm. Knowing how style is matched helps you judge when an anonymous text is really anonymous, and what to change if it matters.
Small words give you away
If you wanted to guess who wrote a letter, you might look for big, unusual words. Researchers found the opposite works better: the tiny words. Words like "upon", "by", "also" and "there" turn up in every text on every topic, and each writer uses them at their own steady rate without noticing.
Stylometry: measuring writing style with numbers, for example how often someone uses certain words, how long their sentences are, or which punctuation they prefer.
Function words: small, common words that hold sentences together ("upon", "by", "on", "while") rather than carrying the topic ("tent", "budget", "army"). Because they do not depend on the subject, their rates reflect the writer.
The classic case
In 1787 and 1788, 85 essays called the Federalist Papers were published under one shared pen name. Most were later known to be by Alexander Hamilton or James Madison, but both men's lists claimed twelve of the same papers. In the 1960s two statisticians, Frederick Mosteller and David Wallace, counted function words. One striking clue: Hamilton wrote "upon" far more often than Madison, and Madison wrote "whilst" where Hamilton wrote "while". Their conclusion was that Madison very probably wrote all twelve disputed papers.
Our practice papers
The real papers are public domain, but this lab uses generated practice papers from practice_papers() so it runs quickly and offline. Each one is word salad built with an author's habits: the rates are invented, with "upon" and "while"/"whilst" pointing the same way as in the historical study. The same code would work on the real texts. Each paper is a dict: {"id": "H03", "author": "Hamilton", "text": "..."}, and the disputed ones are named after the real disputed numbers, such as D49.
Rates per 1,000 words
A long paper naturally has more of every word than a short one, so raw counts are unfair. Instead, compute a rate: how many times per 1,000 words.
text_words = ["upon", "the", "law", "upon"] # 4 words, 2 of them "upon"
rate = 1000 * text_words.count("upon") / len(text_words) # 500.0
Python's .count(x) counts how many times x appears in a list. First clean the text: text.lower() makes it lower case, .replace(",", " ") swaps commas for spaces, and .split() cuts it into words at spaces.
Then the method is the same as for typists: average each author's rates into a profile, and give each disputed paper to the author whose profile is closest.
In other languages
Word rates in JavaScript:
function wordRates(text, words) {
const all = text.toLowerCase().replace(/[,.]/g, " ").split(/\s+/).filter(Boolean);
const rates = {};
for (const w of words) {
rates[w] = 1000 * all.filter(x => x === w).length / all.length;
}
return rates;
}And in Go:
func wordRates(text string, words []string) map[string]float64 {
clean := strings.NewReplacer(",", " ", ".", " ").Replace(strings.ToLower(text))
all := strings.Fields(clean)
counts := map[string]int{}
for _, w := range all {
counts[w]++
}
rates := map[string]float64{}
for _, w := range words {
rates[w] = 1000 * float64(counts[w]) / float64(len(all))
}
return rates
}The defence
If you ever need to write something truly anonymous (a whistle-blowing tip, a review of your own employer), style matching is the risk to plan for. Practical steps: keep it short, because short texts carry fewer clues; write it in a deliberately plain style unlike your usual one; and avoid your pet phrases and spellings. Running your text through a translation tool and back, or asking an on-device writing assistant to rephrase it, also scrambles many habits. None of these is perfect, so the safest anonymous text is the one with the least of you in it.
Recap
- Function words are used at steady, personal rates whatever the topic.
- Compare rates per 1,000 words, not raw counts, so long and short texts are fair.
- The Federalist Papers case used these rates to attribute twelve disputed essays to Madison.
- Short, plain, rephrased writing leaves fewer style clues.
Write five functions. words_of(text) returns the lower-cased words of text with commas and full stops replaced by spaces (use .split()). word_rates(text, words) returns a dict mapping each word in words to its count per 1,000 words of text. author_profile(papers, author, words) returns a dict of the average rate of each word over all papers whose "author" equals author. style_distance(rates_a, rates_b, words) returns the sum of abs(rates_a[w] - rates_b[w]) over words. attribute(text, profiles, words) returns the key of profiles (an author name) whose profile is closest to the text's rates.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.