Letters and Secrets
Words by the Numbers
Chapter 3 · Letters and Secrets
Words by the Numbers 2 counted whole words and found a steep, lopsided pattern. This chapter drops down a level and counts letters. Letters are far fewer than words (26 of them, in the English alphabet) so even a short text gives a usable count, and the counts turn out to be stubbornly regular. That regularity has two famous uses: breaking secret messages, which is where this story starts, and recognising which language a text is written in without understanding a word of it. The maths is light: percentages, a remainder after division, and a distance that is just a sum of differences.
pydoc_data.topics module), about 363,000 letters of technical English; it is not typical prose, and its counts change a little between Python versions. The short passages used for the language-recognition snippet were written for this course (the same small story in five languages, with my own translations, not checked by native speakers), so treat that experiment as a demonstration on tiny, parallel, same-topic samples and not as a measurement of the languages. Background facts come from web searches that returned summaries of web pages; such fetched pages are often truncated or model-summarised, so treat the wording as a paraphrase. The sources agree on Al-Kindi’s treatise and its method but disagree on the date of Babbage’s work on the Vigenère cipher (conflict, below). Messages and shift keys in the cipher snippets are invented, illustrative examples. Statements marked (check) were not confirmed against a source.
Letters in Real Text
Start by counting. The snippet below keeps only the 26 unaccented letters, lowercased, from the whole Python reference documentation and prints the eight commonest. If every letter were equally likely each would take 1 ÷ 26, about 3.8%. Real English is nothing like that.
The letter e takes 13.5% of all letters, more than three times the even-spread share, and the top four letters (e, t, a, i) together make up about 39%. At the other end, z accounts for under a tenth of one percent. The familiar “e, t, a…” order is the standard story about English; here the order is e, t, a, i, n, s, o, r, because this is documentation text with plenty of i from words such as is, if and in. Different texts give slightly different shares, but a text of any decent length reorders only the middle of the list. One source in my search also notes that th is the most likely pair of letters in English; pairs are the subject of Words by the Numbers 4, so take that as a preview, not a result.
| Unit counted | Types to count | Text needed for a stable picture |
|---|---|---|
| Letters | 26 | a few dozen letters gave usable results in the experiment below |
| Words | thousands, with a long tail (Words by the Numbers 2) | tens of thousands of tokens for a steady slope |
Al-Kindi and the Shift Cipher
Regular letter counts are a gift to anyone trying to read a message not meant for them. According to the sources I found, the earliest known description of breaking a cipher by counting letters is a treatise by the Arab scholar Al-Kindi, A Manuscript on Deciphering Cryptographic Messages, usually dated to about 850 CE in Baghdad, with a copy rediscovered in an Ottoman archive in Istanbul in 1987 (a Simon Singh article and a history-of-information page; second-hand, check). The method is simple: count how often each symbol appears in the ciphertext, then match the commonest symbols to the commonest letters of the language. One summary says he began from the observation that in a given language the letters settle at stable frequencies. Another source cautions that the manuscript’s examples are easiest to explain with the English alphabet, but that Al-Kindi worked on Arabic.
The simplest cipher to attack this way is the shift cipher, usually called the Caesar cipher after the tradition that Julius Caesar used a shift of three (my recollection of the Suetonius account; not confirmed in my search; check). Each letter moves a fixed number of places along the alphabet, wrapping round from z to a. Wrapping is where a little arithmetic comes in: the new position is (old position + k) remainder 26, which Python writes (i + k) % 26. There are only 26 possible shifts, so you could try them all, but frequency gives a shortcut: assume the commonest ciphertext letter stands for e, and the shift is the distance between them.
The commonest cipher letter is h, which is three places after e, so the shift is 3 and the message comes back intact. The message here is 116 characters long, invented for the example, and plenty of es made the guess easy. The next section shows what happens when it is not.
When the “e” Guess Fails
Frequency analysis is a statistical argument, and statistics need data. A very short message may have no e at all. Here are two ways to attack the same ciphertexts: the “commonest letter is e” rule, and a more careful best-fit rule that tries all 26 shifts and keeps the one whose letter shares are closest to the English reference profile (using the sum of absolute differences, so the distance is 0 for identical profiles). The reference profile comes from the documentation text counted earlier. All four messages are enciphered with the same invented key, 7.
On the 70-character message both rules succeed. On attack at dawn (14 characters) and buy milk and a loaf of bread (28 characters) both fail; on send help to the old mill (25 characters) only best-fit succeeds. Four messages prove nothing statistical, but they show the shape of the problem: with a few dozen letters the counts are noisy, and a smarter rule, or a human who notices that the output is gibberish, has to take over. Note, too, that the reference profile here comes from technical documentation, not from the kind of message a spy would write, so a profile that matches the sender’s style would do better.
| Cipher | Keys | Letter counts of the ciphertext |
|---|---|---|
| Shift (Caesar) | 26 shifts, including the do-nothing shift of 0 | same shape as the plaintext, only moved along the alphabet |
| Simple substitution | 26! = 403,291,461,126,605,635,584,000,000 (about 4.0 × 1026, computed by Python) | same set of counts, each tied to one symbol: Al-Kindi’s method still applies |
| Vigenère | a keyword; each letter is shifted by the matching keyword letter in turn | flattened, because one plaintext letter becomes several different symbols |
That last row is why the Vigenère cipher was thought unbreakable for a long time. A search on the Kasiski examination reports that Friedrich Kasiski published a method in 1863, based on spotting repeated strings in the ciphertext that reveal the keyword length, and that Charles Babbage had independently found a way into the cipher earlier without ever explaining it. The sources conflict on when: one gives 1846, another 1854 (conflict; check). Once the length is known, each position is a separate shift cipher and the e-guess works again.
Letter Profiles: Telling Languages Apart
Turn the idea around. Instead of using a known language to read a hidden message, use the letter counts to identify the language. A profile is a list of 26 numbers: the share of each letter. Two texts in the same language should have close profiles, and two different languages should differ. To compare profiles use the distance from the last section: add up the absolute differences, letter by letter. The smaller the sum, the more alike the texts.
The experiment below uses five short training passages (the same invented harbour story in English, French, German, Spanish and Italian), builds a profile from each, and then tries to say which language each of five different test sentences is in. Accents are stripped first (é becomes e, ö becomes o), which keeps the profile to 26 letters. To see how much text is needed, it classifies the first 20, 40 and 80 letters of each test sentence, and then all of it. The passages are long because the data is in the snippet, so every line can be run as it stands.
The top-three lists already look different: German and French put e at 17.4%, while Spanish and Italian lead with a. English has h as its second letter in this small sample, from words like the. The classifier gets all five right given 40 letters or more, and four of five at 20 letters, where an Italian sentence is taken for French. That is a nice result, but it is a very small, friendly test: the passages are translations of each other, there are five sentences, and published frequency tables, built from far larger collections, would differ from these small-sample shares (the 14.7% for e in English here, for instance, is well above the figure I recall for large English corpora; check). Real language identifiers use more than single letters, typically groups of two or three, which is where the next chapter goes.
| Sample of the test sentence | Correct of 5 | Mistake |
|---|---|---|
| first 20 letters | 4 | Italian taken for French |
| first 40 letters | 5 | none |
| first 80 letters | 5 | none |
| whole sentence | 5 | none |
e (short messages, or messages with unusual vocabulary, break this); building a profile from a text in a different genre and expecting a perfect match; forgetting to strip accents or to lowercase, so that E and é count as different letters; treating a result on five sentences as a measured accuracy; and forgetting that the shift formula needs % 26 to wrap around the end of the alphabet.
Count the letters in a short invented passage of your own (two sentences will do). Print the top five letters with their percentage shares, the share of e compared with the even-spread 3.8%, and how many of the 26 letters never appear.
Encrypt five short messages with different shift keys. Break each with two rules: (A) the commonest letter is e; (B) try each of the three commonest letters as e and keep the shift that makes the words “the”, “and”, “to” appear most often. How many does each rule get right?
Using the five training passages from this chapter, print the three closest pairs of languages by profile distance and the furthest pair. Then add the Python documentation as a second kind of English and see whether it sits closer to the story English or to the other languages.
📄 View solutionQuick Reference
- Letter profile: a list of 26 shares, one per letter, summing to 100%
- Even spread: 1 ÷ 26, about 3.8% each; real English is far from it (
ewas 13.5% in the documentation text) - Frequency analysis: match the commonest ciphertext symbols to the commonest letters of the language (Al-Kindi, about 850 CE per the sources)
- Shift cipher:
ALPHA[(i + k) % 26]; to undo it, shift by-k - Distance between profiles:
sum(abs(a - b) for a, b in zip(p, q)), smaller means more alike - Limits: short messages, unusual vocabulary and genre differences all weaken the argument
- Strip accents:
unicodedata.normalize("NFD", text), then keep onlyatoz