N-grams and Markov chains

By Allison Parrish

Markov chain text generation is one of the oldest strategies for predictive text generation. This notebook takes you through the basics of implementing a simple and concise Markov chain text generation procedure in Python.

If all you want is to generate text with a Markov chain and you don't care about how the functions are implemented (or if you already went through this notebook and want to use the functions without copy-and-pasting them), you can download a Python file with all of the functions here. Just download the file, put it in the same directory as your code, type from shmarkov import * at the top, and you're good to go.

Tuples: a quick introduction

Before we get to all that, I need to review a helpful Python data structure: the tuple.

Tuples (rhymes with "supple") are data structures very similar to lists. You can create a tuple using parentheses (instead of square brackets, as you would with a list):

In [273]:
t = ("alpha", "beta", "gamma", "delta")
t
Out[273]:
('alpha', 'beta', 'gamma', 'delta')

You can access the values in a tuple in the same way as you access the values in a list: using square bracket indexing syntax. Tuples support slice syntax and negative indexes, just like lists:

In [274]:
t[-2]
Out[274]:
'gamma'
In [275]:
t[1:3]
Out[275]:
('beta', 'gamma')

The difference between a list and a tuple is that the values in a tuple can't be changed after the tuple is created. This means, for example, that attempting to .append() a value to a tuple will fail:

In [276]:
t.append("epsilon")
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-276-bd64cbe45262> in <module>()
----> 1 t.append("epsilon")

AttributeError: 'tuple' object has no attribute 'append'
In [277]:
t[2] = "bravo"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-277-442965e372c2> in <module>()
----> 1 t[2] = "bravo"

TypeError: 'tuple' object does not support item assignment

"So," you think to yourself. "Tuples are just like... broken lists. That's strange and a little unreasonable. Why even have them in your programming language?" That's a fair question, and answering it requires a bit of knowledge of how Python works with these two kinds of values (lists and tuples) behind the scenes.

Essentially, tuples are faster and smaller than lists. Because lists can be modified, potentially becoming larger after they're initialized, Python has to allocate more memory than is strictly necessary whenever you create a list value. If your list grows beyond what Python has already allocated, Python has to allocate more memory. Allocating memory, copying values into memory, and then freeing memory when it's when no longer needed, are all (perhaps surprisingly) slow processes---slower, at least, than using data already loaded into memory when your program begins.

Because a tuple can't grow or shrink after it's created, Python knows exactly how much memory to allocate when you create a tuple in your program. That means: less wasted memory, and less wasted time allocating a deallocating memory. The cost of this decreased resource footprint is less versatility.

Tuples are often called an immutable data type. "Immutable" in this context simply means that it can't be changed after it's created.

For our purposes, the most important aspect of tuples is that–unlike lists–they can be keys in dictionaries. The utility of this will become apparent later in this tutorial, but to illustrate, let's start with an empty dictionary:

In [278]:
my_stuff = {}

I can use a string as a key, of course, no problem:

In [279]:
my_stuff["cheese"] = 1

I can also use an integer:

In [280]:
my_stuff[17] = "hello"
In [281]:
my_stuff
Out[281]:
{'cheese': 1, 17: 'hello'}

But I can't use a list as a key:

In [282]:
my_stuff[ ["a", "b"] ] = "asdf"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-282-b5ab00a12c7e> in <module>()
----> 1 my_stuff[ ["a", "b"] ] = "asdf"

TypeError: unhashable type: 'list'

That's because a list, as a mutable data type, is "unhashable": since its contents can change, it's impossible to come up with a single value to represent it, as is required of dictionary keys. However, because tuples are immutable, you can use them as dictionary keys:

In [283]:
my_stuff[ ("a", "b") ] = "asdf"
In [284]:
my_stuff
Out[284]:
{'cheese': 1, 17: 'hello', ('a', 'b'): 'asdf'}

This behavior is helpful when you want to make a data structure that maps sequences as keys to corresponding values. As we'll see below!

It's easy to make a list that is a copy of a tuple, and a tuple that is a copy of a list, using the list() and tuple() functions, respectively:

In [298]:
t = (1, 2, 3)
In [299]:
list(t)
Out[299]:
[1, 2, 3]
In [300]:
things = [4, 5, 6]
In [301]:
tuple(things)
Out[301]:
(4, 5, 6)

N-grams

The first kind of text analysis that we’ll look at today is an n-gram model. An n-gram is simply a sequence of units drawn from a longer sequence; in the case of text, the unit in question is usually a character or a word. For convenience, we'll call the unit of the n-gram is called its level; the length of the n-gram is called its order. For example, the following is a list of all unique character-level order-2 n-grams in the word condescendences:

co
on
nd
de
es
sc
ce
en
nc

And the following is an excerpt from the list of all unique word-level order-5 n-grams in The Road Not Taken:

Two roads diverged in a
roads diverged in a yellow
diverged in a yellow wood,
in a yellow wood, And
a yellow wood, And sorry
yellow wood, And sorry I

N-grams are used frequently in natural language processing and are a basic tool text analysis. Their applications range from programs that correct spelling to creative visualizations to compression algorithms to stylometrics to generative text. They can be used as the basis of a Markov chain algorithm—and, in fact, that’s one of the applications we’ll be using them for later in this lesson.

Finding and counting word pairs

So how would we go about writing Python code to find n-grams? We'll start with a simple task: finding word pairs in a text. A word pair is essentially a word-level order-2 n-gram; once we have code to find word pairs, we’ll generalize it to handle n-grams of any order.

To find word pairs, we'll first need some words!

In [285]:
text = open("genesis.txt").read()
words = text.split()

The data structure we want to end up with is a list of tuples, where the tuples have two elements, i.e., each successive pair of words from the text. There are a number of clever ways to go about creating this list. Here's one: imagine our starting list of strings, with their corresponding indices:

['a', 'b', 'c', 'd', 'e']
 0    1    2    3    4

The first item of the list of pairs should consist of the elements at index 0 and index 1 from this list; the second item should consist of the elements at index 1 and index 2; and so forth. We can accomplish this using a list comprehension over the range of numbers from zero up until the end of the list minus one:

In [286]:
pairs = [(words[i], words[i+1]) for i in range(len(words)-1)]

(Why len(words) - 1? Because the final element of the list can only be the second element of a pair. Otherwise we'd be trying to access an element beyond the end of the list.)

The corresponding way to write this with a for loop:

In [287]:
pairs = []
for i in range(len(words)-1):
    this_pair = (words[i], words[i+1])
    pairs.append(this_pair)

In either case, the list of n-grams ends up looking like this. (I'm only showing the first 25 for the sake of brevity; remove [:25] to see the whole list.)

In [291]:
pairs[:25]
Out[291]:
[('In', 'the'),
 ('the', 'beginning'),
 ('beginning', 'God'),
 ('God', 'created'),
 ('created', 'the'),
 ('the', 'heaven'),
 ('heaven', 'and'),
 ('and', 'the'),
 ('the', 'earth.'),
 ('earth.', 'And'),
 ('And', 'the'),
 ('the', 'earth'),
 ('earth', 'was'),
 ('was', 'without'),
 ('without', 'form,'),
 ('form,', 'and'),
 ('and', 'void;'),
 ('void;', 'and'),
 ('and', 'darkness'),
 ('darkness', 'was'),
 ('was', 'upon'),
 ('upon', 'the'),
 ('the', 'face'),
 ('face', 'of'),
 ('of', 'the')]

Now that we have a list of word pairs, we can count them using a Counter object.

In [292]:
from collections import Counter
In [294]:
pair_counts = Counter(pairs)

The .most_common() method of the Counter shows us the items in our list that occur most frequently:

In [295]:
pair_counts.most_common(10)
Out[295]:
[(('And', 'God'), 21),
 (('of', 'the'), 15),
 (('it', 'was'), 13),
 (('and', 'the'), 12),
 (('upon', 'the'), 10),
 (('And', 'the'), 9),
 (('God', 'said,'), 9),
 (('in', 'the'), 9),
 (('the', 'earth'), 8),
 (('said,', 'Let'), 8)]

So the phrase "And God" occurs 21 times, by far the most common word pair in the text. In fact, "And God" comprises about 3% of all word pairs found in the text:

In [296]:
pair_counts[("And", "God")] / sum(pair_counts.values())
Out[296]:
0.026381909547738693

You can do the same calculation with character-level pairs with pretty much exactly the same code, owing to the fact that strings and lists can be indexed using the same syntax:

In [26]:
char_pairs = [(text[i], text[i+1]) for i in range(len(text)-1)]

The variable char_pairs now has a list of all pairs of characters in the text. Using Counter again, we can find the most common pairs of characters:

In [297]:
char_pair_counts = Counter(char_pairs)
char_pair_counts.most_common(10)
Out[297]:
[(('t', 'h'), 184),
 (('e', ' '), 172),
 ((' ', 't'), 162),
 (('d', ' '), 154),
 (('h', 'e'), 144),
 (('n', 'd'), 114),
 ((' ', 'a'), 88),
 (('t', ' '), 72),
 (('e', 'r'), 71),
 (('a', 'n'), 70)]

What are the practical applications of this kind of analysis? For one, you can use n-gram counts to judge similarity between two texts. If two texts have the same n-grams in similar proportions, then those texts probably have similar compositions, meanings, or authorship. N-grams can also be a basis for fast text searching; Google Books Ngram Viewer works along these lines.

N-grams of arbitrary lengths

The step from pairs to n-grams of arbitrary lengths is a only a matter of using slice indexes to get a slice of length n, where n is the length of the desired n-gram. For example, to get all of the word-level order 7 n-grams from the list of words in genesis.txt:

In [307]:
seven_grams = [tuple(words[i:i+7]) for i in range(len(words)-6)]
In [310]:
seven_grams[:20]
Out[310]:
[('In', 'the', 'beginning', 'God', 'created', 'the', 'heaven'),
 ('the', 'beginning', 'God', 'created', 'the', 'heaven', 'and'),
 ('beginning', 'God', 'created', 'the', 'heaven', 'and', 'the'),
 ('God', 'created', 'the', 'heaven', 'and', 'the', 'earth.'),
 ('created', 'the', 'heaven', 'and', 'the', 'earth.', 'And'),
 ('the', 'heaven', 'and', 'the', 'earth.', 'And', 'the'),
 ('heaven', 'and', 'the', 'earth.', 'And', 'the', 'earth'),
 ('and', 'the', 'earth.', 'And', 'the', 'earth', 'was'),
 ('the', 'earth.', 'And', 'the', 'earth', 'was', 'without'),
 ('earth.', 'And', 'the', 'earth', 'was', 'without', 'form,'),
 ('And', 'the', 'earth', 'was', 'without', 'form,', 'and'),
 ('the', 'earth', 'was', 'without', 'form,', 'and', 'void;'),
 ('earth', 'was', 'without', 'form,', 'and', 'void;', 'and'),
 ('was', 'without', 'form,', 'and', 'void;', 'and', 'darkness'),
 ('without', 'form,', 'and', 'void;', 'and', 'darkness', 'was'),
 ('form,', 'and', 'void;', 'and', 'darkness', 'was', 'upon'),
 ('and', 'void;', 'and', 'darkness', 'was', 'upon', 'the'),
 ('void;', 'and', 'darkness', 'was', 'upon', 'the', 'face'),
 ('and', 'darkness', 'was', 'upon', 'the', 'face', 'of'),
 ('darkness', 'was', 'upon', 'the', 'face', 'of', 'the')]

Two tricky things in this expression: in tuple(words[i:i+7]), I call tuple() to convert the list slice (words[i:i+7]) into a tuple. In range(len(words)-6), the 6 is there because it's one fewer than the length of the n-gram. Just as with the pairs above, we need to stop counting before we reach the end of the list with enough room to make sure we're always grabbing slices of the desired length.

For the sake of convenience, here's a function that will return n-grams of a desired length from any sequence, whether list or string:

In [311]:
def ngrams_for_sequence(n, seq):
    return [tuple(seq[i:i+n]) for i in range(len(seq)-n+1)]

Using this function, here are random character-level n-grams of order 9 from genesis.txt:

In [314]:
import random
genesis_9grams = ngrams_for_sequence(9, open("genesis.txt").read())
random.sample(genesis_9grams, 10)
Out[314]:
[('l', 'e', 't', ' ', 'f', 'o', 'w', 'l', ' '),
 ('h', 'i', 'n', 'g', ' ', 't', 'h', 'a', 't'),
 ('n', 'g', ' ', 's', 'e', 'e', 'd', ';', ' '),
 ('n', 'g', ' ', 't', 'h', 'i', 'n', 'g', ' '),
 ('e', ' ', 'w', 'a', 't', 'e', 'r', 's', ' '),
 ('o', 'd', '.', ' ', '\n', 'A', 'n', 'd', ' '),
 ('a', 'i', 'd', ',', ' ', 'L', 'e', 't', ' '),
 ('e', ' ', 'e', 'a', 'r', 't', 'h', ',', ' '),
 ('s', 't', 'a', 'r', 's', ' ', 'a', 'l', 's'),
 ('r', 'o', 'm', ' ', 't', 'h', 'e', ' ', 'n')]

Or all the word-level 5-grams from frost.txt:

In [316]:
frost_word_5grams = ngrams_for_sequence(5, open("frost.txt").read().split())
frost_word_5grams
Out[316]:
[('Two', 'roads', 'diverged', 'in', 'a'),
 ('roads', 'diverged', 'in', 'a', 'yellow'),
 ('diverged', 'in', 'a', 'yellow', 'wood,'),
 ('in', 'a', 'yellow', 'wood,', 'And'),
 ('a', 'yellow', 'wood,', 'And', 'sorry'),
 ('yellow', 'wood,', 'And', 'sorry', 'I'),
 ('wood,', 'And', 'sorry', 'I', 'could'),
 ('And', 'sorry', 'I', 'could', 'not'),
 ('sorry', 'I', 'could', 'not', 'travel'),
 ('I', 'could', 'not', 'travel', 'both'),
 ('could', 'not', 'travel', 'both', 'And'),
 ('not', 'travel', 'both', 'And', 'be'),
 ('travel', 'both', 'And', 'be', 'one'),
 ('both', 'And', 'be', 'one', 'traveler,'),
 ('And', 'be', 'one', 'traveler,', 'long'),
 ('be', 'one', 'traveler,', 'long', 'I'),
 ('one', 'traveler,', 'long', 'I', 'stood'),
 ('traveler,', 'long', 'I', 'stood', 'And'),
 ('long', 'I', 'stood', 'And', 'looked'),
 ('I', 'stood', 'And', 'looked', 'down'),
 ('stood', 'And', 'looked', 'down', 'one'),
 ('And', 'looked', 'down', 'one', 'as'),
 ('looked', 'down', 'one', 'as', 'far'),
 ('down', 'one', 'as', 'far', 'as'),
 ('one', 'as', 'far', 'as', 'I'),
 ('as', 'far', 'as', 'I', 'could'),
 ('far', 'as', 'I', 'could', 'To'),
 ('as', 'I', 'could', 'To', 'where'),
 ('I', 'could', 'To', 'where', 'it'),
 ('could', 'To', 'where', 'it', 'bent'),
 ('To', 'where', 'it', 'bent', 'in'),
 ('where', 'it', 'bent', 'in', 'the'),
 ('it', 'bent', 'in', 'the', 'undergrowth;'),
 ('bent', 'in', 'the', 'undergrowth;', 'Then'),
 ('in', 'the', 'undergrowth;', 'Then', 'took'),
 ('the', 'undergrowth;', 'Then', 'took', 'the'),
 ('undergrowth;', 'Then', 'took', 'the', 'other,'),
 ('Then', 'took', 'the', 'other,', 'as'),
 ('took', 'the', 'other,', 'as', 'just'),
 ('the', 'other,', 'as', 'just', 'as'),
 ('other,', 'as', 'just', 'as', 'fair,'),
 ('as', 'just', 'as', 'fair,', 'And'),
 ('just', 'as', 'fair,', 'And', 'having'),
 ('as', 'fair,', 'And', 'having', 'perhaps'),
 ('fair,', 'And', 'having', 'perhaps', 'the'),
 ('And', 'having', 'perhaps', 'the', 'better'),
 ('having', 'perhaps', 'the', 'better', 'claim,'),
 ('perhaps', 'the', 'better', 'claim,', 'Because'),
 ('the', 'better', 'claim,', 'Because', 'it'),
 ('better', 'claim,', 'Because', 'it', 'was'),
 ('claim,', 'Because', 'it', 'was', 'grassy'),
 ('Because', 'it', 'was', 'grassy', 'and'),
 ('it', 'was', 'grassy', 'and', 'wanted'),
 ('was', 'grassy', 'and', 'wanted', 'wear;'),
 ('grassy', 'and', 'wanted', 'wear;', 'Though'),
 ('and', 'wanted', 'wear;', 'Though', 'as'),
 ('wanted', 'wear;', 'Though', 'as', 'for'),
 ('wear;', 'Though', 'as', 'for', 'that'),
 ('Though', 'as', 'for', 'that', 'the'),
 ('as', 'for', 'that', 'the', 'passing'),
 ('for', 'that', 'the', 'passing', 'there'),
 ('that', 'the', 'passing', 'there', 'Had'),
 ('the', 'passing', 'there', 'Had', 'worn'),
 ('passing', 'there', 'Had', 'worn', 'them'),
 ('there', 'Had', 'worn', 'them', 'really'),
 ('Had', 'worn', 'them', 'really', 'about'),
 ('worn', 'them', 'really', 'about', 'the'),
 ('them', 'really', 'about', 'the', 'same,'),
 ('really', 'about', 'the', 'same,', 'And'),
 ('about', 'the', 'same,', 'And', 'both'),
 ('the', 'same,', 'And', 'both', 'that'),
 ('same,', 'And', 'both', 'that', 'morning'),
 ('And', 'both', 'that', 'morning', 'equally'),
 ('both', 'that', 'morning', 'equally', 'lay'),
 ('that', 'morning', 'equally', 'lay', 'In'),
 ('morning', 'equally', 'lay', 'In', 'leaves'),
 ('equally', 'lay', 'In', 'leaves', 'no'),
 ('lay', 'In', 'leaves', 'no', 'step'),
 ('In', 'leaves', 'no', 'step', 'had'),
 ('leaves', 'no', 'step', 'had', 'trodden'),
 ('no', 'step', 'had', 'trodden', 'black.'),
 ('step', 'had', 'trodden', 'black.', 'Oh,'),
 ('had', 'trodden', 'black.', 'Oh,', 'I'),
 ('trodden', 'black.', 'Oh,', 'I', 'kept'),
 ('black.', 'Oh,', 'I', 'kept', 'the'),
 ('Oh,', 'I', 'kept', 'the', 'first'),
 ('I', 'kept', 'the', 'first', 'for'),
 ('kept', 'the', 'first', 'for', 'another'),
 ('the', 'first', 'for', 'another', 'day!'),
 ('first', 'for', 'another', 'day!', 'Yet'),
 ('for', 'another', 'day!', 'Yet', 'knowing'),
 ('another', 'day!', 'Yet', 'knowing', 'how'),
 ('day!', 'Yet', 'knowing', 'how', 'way'),
 ('Yet', 'knowing', 'how', 'way', 'leads'),
 ('knowing', 'how', 'way', 'leads', 'on'),
 ('how', 'way', 'leads', 'on', 'to'),
 ('way', 'leads', 'on', 'to', 'way,'),
 ('leads', 'on', 'to', 'way,', 'I'),
 ('on', 'to', 'way,', 'I', 'doubted'),
 ('to', 'way,', 'I', 'doubted', 'if'),
 ('way,', 'I', 'doubted', 'if', 'I'),
 ('I', 'doubted', 'if', 'I', 'should'),
 ('doubted', 'if', 'I', 'should', 'ever'),
 ('if', 'I', 'should', 'ever', 'come'),
 ('I', 'should', 'ever', 'come', 'back.'),
 ('should', 'ever', 'come', 'back.', 'I'),
 ('ever', 'come', 'back.', 'I', 'shall'),
 ('come', 'back.', 'I', 'shall', 'be'),
 ('back.', 'I', 'shall', 'be', 'telling'),
 ('I', 'shall', 'be', 'telling', 'this'),
 ('shall', 'be', 'telling', 'this', 'with'),
 ('be', 'telling', 'this', 'with', 'a'),
 ('telling', 'this', 'with', 'a', 'sigh'),
 ('this', 'with', 'a', 'sigh', 'Somewhere'),
 ('with', 'a', 'sigh', 'Somewhere', 'ages'),
 ('a', 'sigh', 'Somewhere', 'ages', 'and'),
 ('sigh', 'Somewhere', 'ages', 'and', 'ages'),
 ('Somewhere', 'ages', 'and', 'ages', 'hence:'),
 ('ages', 'and', 'ages', 'hence:', 'Two'),
 ('and', 'ages', 'hence:', 'Two', 'roads'),
 ('ages', 'hence:', 'Two', 'roads', 'diverged'),
 ('hence:', 'Two', 'roads', 'diverged', 'in'),
 ('Two', 'roads', 'diverged', 'in', 'a'),
 ('roads', 'diverged', 'in', 'a', 'wood,'),
 ('diverged', 'in', 'a', 'wood,', 'and'),
 ('in', 'a', 'wood,', 'and', 'I—'),
 ('a', 'wood,', 'and', 'I—', 'I'),
 ('wood,', 'and', 'I—', 'I', 'took'),
 ('and', 'I—', 'I', 'took', 'the'),
 ('I—', 'I', 'took', 'the', 'one'),
 ('I', 'took', 'the', 'one', 'less'),
 ('took', 'the', 'one', 'less', 'travelled'),
 ('the', 'one', 'less', 'travelled', 'by,'),
 ('one', 'less', 'travelled', 'by,', 'And'),
 ('less', 'travelled', 'by,', 'And', 'that'),
 ('travelled', 'by,', 'And', 'that', 'has'),
 ('by,', 'And', 'that', 'has', 'made'),
 ('And', 'that', 'has', 'made', 'all'),
 ('that', 'has', 'made', 'all', 'the'),
 ('has', 'made', 'all', 'the', 'difference.')]

All of the bigrams (ngrams of order 2) from the string condescendences:

In [317]:
ngrams_for_sequence(2, "condescendences")
Out[317]:
[('c', 'o'),
 ('o', 'n'),
 ('n', 'd'),
 ('d', 'e'),
 ('e', 's'),
 ('s', 'c'),
 ('c', 'e'),
 ('e', 'n'),
 ('n', 'd'),
 ('d', 'e'),
 ('e', 'n'),
 ('n', 'c'),
 ('c', 'e'),
 ('e', 's')]

This function works with non-string sequences as well:

In [319]:
ngrams_for_sequence(4, [5, 10, 15, 20, 25, 30])
Out[319]:
[(5, 10, 15, 20), (10, 15, 20, 25), (15, 20, 25, 30)]

And of course we can use it in conjunction with a Counter to find the most common n-grams in a text:

In [321]:
Counter(ngrams_for_sequence(3, open("genesis.txt").read())).most_common(20)
Out[321]:
[((' ', 't', 'h'), 144),
 (('t', 'h', 'e'), 127),
 (('h', 'e', ' '), 114),
 (('n', 'd', ' '), 99),
 (('a', 'n', 'd'), 66),
 ((' ', 'a', 'n'), 64),
 ((',', ' ', 'a'), 40),
 (('i', 'n', 'g'), 37),
 (('d', ' ', 't'), 36),
 (('n', 'g', ' '), 34),
 (('A', 'n', 'd'), 33),
 ((' ', 'G', 'o'), 32),
 (('G', 'o', 'd'), 32),
 (('o', 'd', ' '), 32),
 (('.', ' ', '\n'), 29),
 ((' ', '\n', 'A'), 29),
 (('\n', 'A', 'n'), 29),
 ((' ', 'w', 'a'), 28),
 (('d', ' ', 'G'), 28),
 (('r', 't', 'h'), 27)]

Markov models: what comes next?

Now that we have the ability to find and record the n-grams in a text, it’s time to take our analysis one step further. The next question we’re going to try to answer is this: Given a particular n-gram in a text, what is most likely to come next?

We can imagine the kind of algorithm we’ll need to extract this information from the text. It will look very similar to the code to find n-grams above, but it will need to keep track not just of the n-grams but also a list of all units (word, character, whatever) that follow those n-grams.

Let’s do a quick example by hand. This is the same character-level order-2 n-gram analysis of the (very brief) text “condescendences” as above, but this time keeping track of all characters that follow each n-gram:

n-grams next?
co n
on d
nd e, e
de s, n
es c, (end of text)
sc e
ce n, s
en d, c
nc e

From this table, we can determine that while the n-gram co is followed by n 100% of the time, and while the n-gram on is followed by d 100% of the time, the n-gram de is followed by s 50% of the time, and n the rest of the time. Likewise, the n-gram es is followed by c 50% of the time, and followed by the end of the text the other 50% of the time.

The easiest way to represent this model is with a dictionary whose keys are the n-grams and whose values are all of the possible "nexts." Here's what the Python code looks like to construct this model from a string. We'll use the special token $ to represent the notion of the "end of text" in the table above.

In [336]:
src = "condescendences"
src += "$" # to indicate the end of the string
model = {}
for i in range(len(src)-2):
    ngram = tuple(src[i:i+2]) # get a slice of length 2 from current position
    next_item = src[i+2] # next item is current index plus two (i.e., right after the slice)
    if ngram not in model: # check if we've already seen this ngram; if not...
        model[ngram] = [] # value for this key is an empty list
    model[ngram].append(next_item) # append this next item to the list for this ngram
In [338]:
model
Out[338]:
{('c', 'e'): ['n', 's'],
 ('c', 'o'): ['n'],
 ('d', 'e'): ['s', 'n'],
 ('e', 'n'): ['d', 'c'],
 ('e', 's'): ['c', '$'],
 ('n', 'c'): ['e'],
 ('n', 'd'): ['e', 'e'],
 ('o', 'n'): ['d'],
 ('s', 'c'): ['e']}

The functions in the cell below generalize this to n-grams of arbitrary length (and use the special Python value None to indicate the end of a sequence). The markov_model() function creates an empty dictionary and takes an n-gram length and a sequence (which can be a string or a list) and calls the add_to_model() function on that sequence. The add_to_model() function does the same thing as the code above: iterates over every index of the sequence and grabs an n-gram of the desired length, adding keys and values to the dictionary as necessary.

In [339]:
def add_to_model(model, n, seq):
    # make a copy of seq and append None to the end
    seq = list(seq[:]) + [None]
    for i in range(len(seq)-n):
        # tuple because we're using it as a dict key!
        gram = tuple(seq[i:i+n])
        next_item = seq[i+n]            
        if gram not in model:
            model[gram] = []
        model[gram].append(next_item)

def markov_model(n, seq):
    model = {}
    add_to_model(model, n, seq)
    return model

So, e.g., an order-2 character-level Markov model of condescendences:

In [340]:
markov_model(2, "condescendences")
Out[340]:
{('c', 'e'): ['n', 's'],
 ('c', 'o'): ['n'],
 ('d', 'e'): ['s', 'n'],
 ('e', 'n'): ['d', 'c'],
 ('e', 's'): ['c', None],
 ('n', 'c'): ['e'],
 ('n', 'd'): ['e', 'e'],
 ('o', 'n'): ['d'],
 ('s', 'c'): ['e']}

Or an order 3 word-level Markov model of genesis.txt:

In [345]:
genesis_markov_model = markov_model(3, open("genesis.txt").read().split())
In [346]:
genesis_markov_model
Out[346]:
{('And', 'God', 'blessed'): ['them,', 'them,'],
 ('And', 'God', 'called'): ['the', 'the', 'the'],
 ('And', 'God', 'created'): ['great'],
 ('And', 'God', 'made'): ['the', 'two', 'the'],
 ('And', 'God', 'said,'): ['Let',
  'Let',
  'Let',
  'Let',
  'Let',
  'Let',
  'Let',
  'Let',
  'Behold,'],
 ('And', 'God', 'saw'): ['the', 'every'],
 ('And', 'God', 'set'): ['them'],
 ('And', 'let', 'them'): ['be'],
 ('And', 'the', 'Spirit'): ['of'],
 ('And', 'the', 'earth'): ['was', 'brought'],
 ('And', 'the', 'evening'): ['and', 'and', 'and', 'and', 'and', 'and'],
 ('And', 'to', 'every'): ['beast'],
 ('And', 'to', 'rule'): ['over'],
 ('Be', 'fruitful,', 'and'): ['multiply,', 'multiply,'],
 ('Behold,', 'I', 'have'): ['given'],
 ('Day,', 'and', 'the'): ['darkness'],
 ('Earth;', 'and', 'the'): ['gathering'],
 ('God', 'blessed', 'them,'): ['saying,', 'and'],
 ('God', 'called', 'the'): ['light', 'firmament', 'dry'],
 ('God', 'created', 'great'): ['whales,'],
 ('God', 'created', 'he'): ['him;'],
 ('God', 'created', 'man'): ['in'],
 ('God', 'created', 'the'): ['heaven'],
 ('God', 'divided', 'the'): ['light'],
 ('God', 'made', 'the'): ['firmament,', 'beast'],
 ('God', 'made', 'two'): ['great'],
 ('God', 'moved', 'upon'): ['the'],
 ('God', 'said', 'unto'): ['them,'],
 ('God', 'said,', 'Behold,'): ['I'],
 ('God', 'said,', 'Let'): ['there',
  'there',
  'the',
  'the',
  'there',
  'the',
  'the',
  'us'],
 ('God', 'saw', 'every'): ['thing'],
 ('God', 'saw', 'that'): ['it', 'it', 'it', 'it', 'it'],
 ('God', 'saw', 'the'): ['light,'],
 ('God', 'set', 'them'): ['in'],
 ('Heaven.', 'And', 'the'): ['evening'],
 ('I', 'have', 'given'): ['you', 'every'],
 ('In', 'the', 'beginning'): ['God'],
 ('Let', 'the', 'earth'): ['bring', 'bring'],
 ('Let', 'the', 'waters'): ['under', 'bring'],
 ('Let', 'there', 'be'): ['light:', 'a', 'lights'],
 ('Let', 'us', 'make'): ['man'],
 ('Night.', 'And', 'the'): ['evening'],
 ('Seas:', 'and', 'God'): ['saw'],
 ('So', 'God', 'created'): ['man'],
 ('Spirit', 'of', 'God'): ['moved'],
 ('a', 'firmament', 'in'): ['the'],
 ('a', 'tree', 'yielding'): ['seed;'],
 ('above', 'the', 'earth'): ['in'],
 ('above', 'the', 'firmament:'): ['and'],
 ('abundantly', 'the', 'moving'): ['creature'],
 ('abundantly,', 'after', 'their'): ['kind,'],
 ('after', 'his', 'kind,'): ['whose', 'and', 'cattle,', 'and'],
 ('after', 'his', 'kind:'): ['and', 'and', 'and', 'and'],
 ('after', 'our', 'likeness:'): ['and'],
 ('after', 'their', 'kind,'): ['and', 'and'],
 ('air,', 'and', 'over'): ['the', 'every'],
 ('air,', 'and', 'to'): ['every'],
 ('all', 'the', 'earth,'): ['and', 'and'],
 ('also.', 'And', 'God'): ['set'],
 ('and', 'God', 'divided'): ['the'],
 ('and', 'God', 'said'): ['unto'],
 ('and', 'God', 'saw'): ['that', 'that', 'that', 'that', 'that'],
 ('and', 'beast', 'of'): ['the'],
 ('and', 'cattle', 'after'): ['their'],
 ('and', 'creeping', 'thing,'): ['and'],
 ('and', 'darkness', 'was'): ['upon'],
 ('and', 'divided', 'the'): ['waters'],
 ('and', 'every', 'living'): ['creature'],
 ('and', 'every', 'thing'): ['that'],
 ('and', 'every', 'tree,'): ['in'],
 ('and', 'every', 'winged'): ['fowl'],
 ('and', 'female', 'created'): ['he'],
 ('and', 'fill', 'the'): ['waters'],
 ('and', 'for', 'days,'): ['and'],
 ('and', 'for', 'seasons,'): ['and'],
 ('and', 'fowl', 'that'): ['may'],
 ('and', 'have', 'dominion'): ['over'],
 ('and', 'herb', 'yielding'): ['seed'],
 ('and', 'it', 'was'): ['so.', 'so.', 'so.', 'so.', 'so.', 'so.'],
 ('and', 'let', 'fowl'): ['multiply'],
 ('and', 'let', 'it'): ['divide'],
 ('and', 'let', 'the'): ['dry'],
 ('and', 'let', 'them'): ['be', 'have'],
 ('and', 'multiply,', 'and'): ['fill', 'replenish'],
 ('and', 'over', 'all'): ['the'],
 ('and', 'over', 'every'): ['creeping', 'living'],
 ('and', 'over', 'the'): ['night,', 'fowl', 'cattle,', 'fowl'],
 ('and', 'replenish', 'the'): ['earth,'],
 ('and', 'subdue', 'it:'): ['and'],
 ('and', 'the', 'darkness'): ['he'],
 ('and', 'the', 'earth.'): ['And'],
 ('and', 'the', 'fruit'): ['tree'],
 ('and', 'the', 'gathering'): ['together'],
 ('and', 'the', 'lesser'): ['light'],
 ('and', 'the', 'morning'): ['were', 'were', 'were', 'were', 'were', 'were'],
 ('and', 'the', 'tree'): ['yielding'],
 ('and', 'there', 'was'): ['light.'],
 ('and', 'to', 'divide'): ['the'],
 ('and', 'to', 'every'): ['fowl', 'thing'],
 ('and', 'void;', 'and'): ['darkness'],
 ('and', 'years:', 'And'): ['let'],
 ('and,', 'behold,', 'it'): ['was'],
 ('appear:', 'and', 'it'): ['was'],
 ('be', 'a', 'firmament'): ['in'],
 ('be', 'for', 'lights'): ['in'],
 ('be', 'for', 'meat.'): ['And'],
 ('be', 'for', 'signs,'): ['and'],
 ('be', 'gathered', 'together'): ['unto'],
 ('be', 'light:', 'and'): ['there'],
 ('be', 'lights', 'in'): ['the'],
 ('bearing', 'seed,', 'which'): ['is'],
 ('beast', 'of', 'the'): ['earth', 'earth', 'earth,'],
 ('beginning', 'God', 'created'): ['the'],
 ('behold,', 'it', 'was'): ['very'],
 ('blessed', 'them,', 'and'): ['God'],
 ('blessed', 'them,', 'saying,'): ['Be'],
 ('bring', 'forth', 'abundantly'): ['the'],
 ('bring', 'forth', 'grass,'): ['the'],
 ('bring', 'forth', 'the'): ['living'],
 ('brought', 'forth', 'abundantly,'): ['after'],
 ('brought', 'forth', 'grass,'): ['and'],
 ('called', 'Night.', 'And'): ['the'],
 ('called', 'he', 'Seas:'): ['and'],
 ('called', 'the', 'dry'): ['land'],
 ('called', 'the', 'firmament'): ['Heaven.'],
 ('called', 'the', 'light'): ['Day,'],
 ('cattle', 'after', 'their'): ['kind,'],
 ('cattle,', 'and', 'creeping'): ['thing,'],
 ('cattle,', 'and', 'over'): ['all'],
 ('created', 'great', 'whales,'): ['and'],
 ('created', 'he', 'him;'): ['male'],
 ('created', 'he', 'them.'): ['And'],
 ('created', 'man', 'in'): ['his'],
 ('created', 'the', 'heaven'): ['and'],
 ('creature', 'after', 'his'): ['kind,'],
 ('creature', 'that', 'hath'): ['life,'],
 ('creature', 'that', 'moveth,'): ['which'],
 ('creepeth', 'upon', 'the'): ['earth', 'earth.', 'earth,'],
 ('creeping', 'thing', 'that'): ['creepeth'],
 ('creeping', 'thing,', 'and'): ['beast'],
 ('darkness', 'he', 'called'): ['Night.'],
 ('darkness', 'was', 'upon'): ['the'],
 ('darkness.', 'And', 'God'): ['called'],
 ('darkness:', 'and', 'God'): ['saw'],
 ('day', 'and', 'over'): ['the'],
 ('day', 'from', 'the'): ['night;'],
 ('day,', 'and', 'the'): ['lesser'],
 ('day.', 'And', 'God'): ['said,', 'said,', 'said,', 'said,', 'said,'],
 ('days,', 'and', 'years:'): ['And'],
 ('deep.', 'And', 'the'): ['Spirit'],
 ('divide', 'the', 'day'): ['from'],
 ('divide', 'the', 'light'): ['from'],
 ('divide', 'the', 'waters'): ['from'],
 ('divided', 'the', 'light'): ['from'],
 ('divided', 'the', 'waters'): ['which'],
 ('dominion', 'over', 'the'): ['fish', 'fish'],
 ('dry', 'land', 'Earth;'): ['and'],
 ('dry', 'land', 'appear:'): ['and'],
 ('earth', 'after', 'his'): ['kind:', 'kind,', 'kind:'],
 ('earth', 'bring', 'forth'): ['grass,', 'the'],
 ('earth', 'brought', 'forth'): ['grass,'],
 ('earth', 'in', 'the'): ['open'],
 ('earth', 'was', 'without'): ['form,'],
 ('earth,', 'And', 'to'): ['rule'],
 ('earth,', 'and', 'every'): ['tree,'],
 ('earth,', 'and', 'over'): ['every'],
 ('earth,', 'and', 'subdue'): ['it:'],
 ('earth,', 'and', 'to'): ['every'],
 ('earth,', 'wherein', 'there'): ['is'],
 ('earth.', 'And', 'God'): ['said,'],
 ('earth.', 'And', 'the'): ['earth', 'evening'],
 ('earth.', 'So', 'God'): ['created'],
 ('earth:', 'and', 'it'): ['was', 'was'],
 ('evening', 'and', 'the'): ['morning',
  'morning',
  'morning',
  'morning',
  'morning',
  'morning'],
 ('every', 'beast', 'of'): ['the'],
 ('every', 'creeping', 'thing'): ['that'],
 ('every', 'fowl', 'of'): ['the'],
 ('every', 'green', 'herb'): ['for'],
 ('every', 'herb', 'bearing'): ['seed,'],
 ('every', 'living', 'creature'): ['that'],
 ('every', 'living', 'thing'): ['that'],
 ('every', 'thing', 'that'): ['creepeth', 'creepeth', 'he'],
 ('every', 'tree,', 'in'): ['the'],
 ('every', 'winged', 'fowl'): ['after'],
 ('face', 'of', 'all'): ['the'],
 ('face', 'of', 'the'): ['deep.', 'waters.'],
 ('female', 'created', 'he'): ['them.'],
 ('fifth', 'day.', 'And'): ['God'],
 ('fill', 'the', 'waters'): ['in'],
 ('firmament', 'Heaven.', 'And'): ['the'],
 ('firmament', 'from', 'the'): ['waters'],
 ('firmament', 'in', 'the'): ['midst'],
 ('firmament', 'of', 'heaven.'): ['And'],
 ('firmament', 'of', 'the'): ['heaven', 'heaven', 'heaven'],
 ('firmament,', 'and', 'divided'): ['the'],
 ('firmament:', 'and', 'it'): ['was'],
 ('first', 'day.', 'And'): ['God'],
 ('fish', 'of', 'the'): ['sea,', 'sea,'],
 ('fly', 'above', 'the'): ['earth'],
 ('for', 'days,', 'and'): ['years:'],
 ('for', 'lights', 'in'): ['the'],
 ('for', 'meat.', 'And'): ['to'],
 ('for', 'meat:', 'and'): ['it'],
 ('for', 'seasons,', 'and'): ['for'],
 ('for', 'signs,', 'and'): ['for'],
 ('form,', 'and', 'void;'): ['and'],
 ('forth', 'abundantly', 'the'): ['moving'],
 ('forth', 'abundantly,', 'after'): ['their'],
 ('forth', 'grass,', 'and'): ['herb'],
 ('forth', 'grass,', 'the'): ['herb'],
 ('forth', 'the', 'living'): ['creature'],
 ('fourth', 'day.', 'And'): ['God'],
 ('fowl', 'after', 'his'): ['kind:'],
 ('fowl', 'multiply', 'in'): ['the'],
 ('fowl', 'of', 'the'): ['air,', 'air,', 'air,'],
 ('fowl', 'that', 'may'): ['fly'],
 ('from', 'the', 'darkness.'): ['And'],
 ('from', 'the', 'darkness:'): ['and'],
 ('from', 'the', 'night;'): ['and'],
 ('from', 'the', 'waters'): ['which'],
 ('from', 'the', 'waters.'): ['And'],
 ('fruit', 'after', 'his'): ['kind,'],
 ('fruit', 'of', 'a'): ['tree'],
 ('fruit', 'tree', 'yielding'): ['fruit'],
 ('fruit,', 'whose', 'seed'): ['was'],
 ('fruitful,', 'and', 'multiply,'): ['and', 'and'],
 ('gathered', 'together', 'unto'): ['one'],
 ('gathering', 'together', 'of'): ['the'],
 ('give', 'light', 'upon'): ['the', 'the'],
 ('given', 'every', 'green'): ['herb'],
 ('given', 'you', 'every'): ['herb'],
 ('good.', 'And', 'God'): ['said,', 'blessed', 'said,'],
 ('good.', 'And', 'the'): ['evening', 'evening', 'evening'],
 ('good:', 'and', 'God'): ['divided'],
 ('grass,', 'and', 'herb'): ['yielding'],
 ('grass,', 'the', 'herb'): ['yielding'],
 ('great', 'lights;', 'the'): ['greater'],
 ('great', 'whales,', 'and'): ['every'],
 ('greater', 'light', 'to'): ['rule'],
 ('green', 'herb', 'for'): ['meat:'],
 ('had', 'made,', 'and,'): ['behold,'],
 ('hath', 'life,', 'and'): ['fowl'],
 ('have', 'dominion', 'over'): ['the', 'the'],
 ('have', 'given', 'every'): ['green'],
 ('have', 'given', 'you'): ['every'],
 ('he', 'Seas:', 'and'): ['God'],
 ('he', 'called', 'Night.'): ['And'],
 ('he', 'had', 'made,'): ['and,'],
 ('he', 'him;', 'male'): ['and'],
 ('he', 'made', 'the'): ['stars'],
 ('he', 'them.', 'And'): ['God'],
 ('heaven', 'and', 'the'): ['earth.'],
 ('heaven', 'be', 'gathered'): ['together'],
 ('heaven', 'to', 'divide'): ['the'],
 ('heaven', 'to', 'give'): ['light', 'light'],
 ('heaven.', 'And', 'God'): ['created'],
 ('herb', 'bearing', 'seed,'): ['which'],
 ('herb', 'for', 'meat:'): ['and'],
 ('herb', 'yielding', 'seed'): ['after'],
 ('herb', 'yielding', 'seed,'): ['and'],
 ('him;', 'male', 'and'): ['female'],
 ('his', 'kind,', 'and'): ['the', 'cattle'],
 ('his', 'kind,', 'cattle,'): ['and'],
 ('his', 'kind,', 'whose'): ['seed'],
 ('his', 'kind:', 'and'): ['God', 'God', 'it', 'God'],
 ('his', 'own', 'image,'): ['in'],
 ('image', 'of', 'God'): ['created'],
 ('image,', 'after', 'our'): ['likeness:'],
 ('image,', 'in', 'the'): ['image'],
 ('in', 'his', 'own'): ['image,'],
 ('in', 'itself,', 'after'): ['his'],
 ('in', 'itself,', 'upon'): ['the'],
 ('in', 'our', 'image,'): ['after'],
 ('in', 'the', 'earth.'): ['And'],
 ('in', 'the', 'firmament'): ['of', 'of', 'of'],
 ('in', 'the', 'image'): ['of'],
 ('in', 'the', 'midst'): ['of'],
 ('in', 'the', 'open'): ['firmament'],
 ('in', 'the', 'seas,'): ['and'],
 ('in', 'the', 'which'): ['is'],
 ('is', 'in', 'itself,'): ['upon'],
 ('is', 'life,', 'I'): ['have'],
 ('is', 'the', 'fruit'): ['of'],
 ('is', 'upon', 'the'): ['face'],
 ('it', 'divide', 'the'): ['waters'],
 ('it', 'shall', 'be'): ['for'],
 ('it', 'was', 'good.'): ['And', 'And', 'And', 'And', 'And'],
 ('it', 'was', 'good:'): ['and'],
 ('it', 'was', 'so.'): ['And', 'And', 'And', 'And', 'And', 'And'],
 ('it', 'was', 'very'): ['good.'],
 ('it:', 'and', 'have'): ['dominion'],
 ('itself,', 'after', 'his'): ['kind:'],
 ('itself,', 'upon', 'the'): ['earth:'],
 ('kind,', 'and', 'cattle'): ['after'],
 ('kind,', 'and', 'every'): ['winged', 'thing'],
 ('kind,', 'and', 'the'): ['tree'],
 ('kind,', 'cattle,', 'and'): ['creeping'],
 ('kind,', 'whose', 'seed'): ['is'],
 ('kind:', 'and', 'God'): ['saw', 'saw', 'saw'],
 ('kind:', 'and', 'it'): ['was'],
 ('land', 'Earth;', 'and'): ['the'],
 ('land', 'appear:', 'and'): ['it'],
 ('lesser', 'light', 'to'): ['rule'],
 ('let', 'fowl', 'multiply'): ['in'],
 ('let', 'it', 'divide'): ['the'],
 ('let', 'the', 'dry'): ['land'],
 ('let', 'them', 'be'): ['for', 'for'],
 ('let', 'them', 'have'): ['dominion'],
 ('life,', 'I', 'have'): ['given'],
 ('life,', 'and', 'fowl'): ['that'],
 ('light', 'Day,', 'and'): ['the'],
 ('light', 'from', 'the'): ['darkness.', 'darkness:'],
 ('light', 'to', 'rule'): ['the', 'the'],
 ('light', 'upon', 'the'): ['earth:', 'earth,'],
 ('light,', 'that', 'it'): ['was'],
 ('light.', 'And', 'God'): ['saw'],
 ('light:', 'and', 'there'): ['was'],
 ('lights', 'in', 'the'): ['firmament', 'firmament'],
 ('lights;', 'the', 'greater'): ['light'],
 ('likeness:', 'and', 'let'): ['them'],
 ('living', 'creature', 'after'): ['his'],
 ('living', 'creature', 'that'): ['moveth,'],
 ('living', 'thing', 'that'): ['moveth'],
 ('made', 'the', 'beast'): ['of'],
 ('made', 'the', 'firmament,'): ['and'],
 ('made', 'the', 'stars'): ['also.'],
 ('made', 'two', 'great'): ['lights;'],
 ('made,', 'and,', 'behold,'): ['it'],
 ('make', 'man', 'in'): ['our'],
 ('male', 'and', 'female'): ['created'],
 ('man', 'in', 'his'): ['own'],
 ('man', 'in', 'our'): ['image,'],
 ('may', 'fly', 'above'): ['the'],
 ('meat.', 'And', 'to'): ['every'],
 ('meat:', 'and', 'it'): ['was'],
 ('midst', 'of', 'the'): ['waters,'],
 ('morning', 'were', 'the'): ['first',
  'second',
  'third',
  'fourth',
  'fifth',
  'sixth'],
 ('moved', 'upon', 'the'): ['face'],
 ('moveth', 'upon', 'the'): ['earth.'],
 ('moveth,', 'which', 'the'): ['waters'],
 ('moving', 'creature', 'that'): ['hath'],
 ('multiply', 'in', 'the'): ['earth.'],
 ('multiply,', 'and', 'fill'): ['the'],
 ('multiply,', 'and', 'replenish'): ['the'],
 ('night,', 'and', 'to'): ['divide'],
 ('night:', 'he', 'made'): ['the'],
 ('night;', 'and', 'let'): ['them'],
 ('of', 'God', 'created'): ['he'],
 ('of', 'God', 'moved'): ['upon'],
 ('of', 'a', 'tree'): ['yielding'],
 ('of', 'all', 'the'): ['earth,'],
 ('of', 'heaven.', 'And'): ['God'],
 ('of', 'the', 'air,'): ['and', 'and', 'and'],
 ('of', 'the', 'deep.'): ['And'],
 ('of', 'the', 'earth'): ['after', 'after'],
 ('of', 'the', 'earth,'): ['and'],
 ('of', 'the', 'heaven'): ['to', 'to', 'to'],
 ('of', 'the', 'sea,'): ['and', 'and'],
 ('of', 'the', 'waters'): ['called'],
 ('of', 'the', 'waters,'): ['and'],
 ('of', 'the', 'waters.'): ['And'],
 ('one', 'place,', 'and'): ['let'],
 ('open', 'firmament', 'of'): ['heaven.'],
 ('our', 'image,', 'after'): ['our'],
 ('our', 'likeness:', 'and'): ['let'],
 ('over', 'all', 'the'): ['earth,'],
 ('over', 'every', 'creeping'): ['thing'],
 ('over', 'every', 'living'): ['thing'],
 ('over', 'the', 'cattle,'): ['and'],
 ('over', 'the', 'day'): ['and'],
 ('over', 'the', 'fish'): ['of', 'of'],
 ('over', 'the', 'fowl'): ['of', 'of'],
 ('over', 'the', 'night,'): ['and'],
 ('own', 'image,', 'in'): ['the'],
 ('place,', 'and', 'let'): ['the'],
 ('replenish', 'the', 'earth,'): ['and'],
 ('rule', 'over', 'the'): ['day'],
 ('rule', 'the', 'day,'): ['and'],
 ('rule', 'the', 'night:'): ['he'],
 ('said', 'unto', 'them,'): ['Be'],
 ('said,', 'Behold,', 'I'): ['have'],
 ('said,', 'Let', 'the'): ['waters', 'earth', 'waters', 'earth'],
 ('said,', 'Let', 'there'): ['be', 'be', 'be'],
 ('said,', 'Let', 'us'): ['make'],
 ('saw', 'every', 'thing'): ['that'],
 ('saw', 'that', 'it'): ['was', 'was', 'was', 'was', 'was'],
 ('saw', 'the', 'light,'): ['that'],
 ('saying,', 'Be', 'fruitful,'): ['and'],
 ('sea,', 'and', 'over'): ['the', 'the'],
 ('seas,', 'and', 'let'): ['fowl'],
 ('seasons,', 'and', 'for'): ['days,'],
 ('second', 'day.', 'And'): ['God'],
 ('seed', 'after', 'his'): ['kind,'],
 ('seed', 'is', 'in'): ['itself,'],
 ('seed', 'was', 'in'): ['itself,'],
 ('seed,', 'and', 'the'): ['fruit'],
 ('seed,', 'which', 'is'): ['upon'],
 ('seed;', 'to', 'you'): ['it'],
 ('set', 'them', 'in'): ['the'],
 ('shall', 'be', 'for'): ['meat.'],
 ('signs,', 'and', 'for'): ['seasons,'],
 ('so.', 'And', 'God'): ['called', 'called', 'made', 'made', 'saw'],
 ('so.', 'And', 'the'): ['earth'],
 ('stars', 'also.', 'And'): ['God'],
 ('subdue', 'it:', 'and'): ['have'],
 ('that', 'creepeth', 'upon'): ['the', 'the', 'the'],
 ('that', 'hath', 'life,'): ['and'],
 ('that', 'he', 'had'): ['made,'],
 ('that', 'it', 'was'): ['good:', 'good.', 'good.', 'good.', 'good.', 'good.'],
 ('that', 'may', 'fly'): ['above'],
 ('that', 'moveth', 'upon'): ['the'],
 ('that', 'moveth,', 'which'): ['the'],
 ('the', 'Spirit', 'of'): ['God'],
 ('the', 'air,', 'and'): ['over', 'over', 'to'],
 ('the', 'beast', 'of'): ['the'],
 ('the', 'beginning', 'God'): ['created'],
 ('the', 'cattle,', 'and'): ['over'],
 ('the', 'darkness', 'he'): ['called'],
 ('the', 'darkness.', 'And'): ['God'],
 ('the', 'darkness:', 'and'): ['God'],
 ('the', 'day', 'and'): ['over'],
 ('the', 'day', 'from'): ['the'],
 ('the', 'day,', 'and'): ['the'],
 ('the', 'deep.', 'And'): ['the'],
 ('the', 'dry', 'land'): ['appear:', 'Earth;'],
 ('the', 'earth', 'after'): ['his', 'his', 'his'],
 ('the', 'earth', 'bring'): ['forth', 'forth'],
 ('the', 'earth', 'brought'): ['forth'],
 ('the', 'earth', 'in'): ['the'],
 ('the', 'earth', 'was'): ['without'],
 ('the', 'earth,', 'And'): ['to'],
 ('the', 'earth,', 'and'): ['over', 'subdue', 'every', 'to'],
 ('the', 'earth,', 'wherein'): ['there'],
 ('the', 'earth.', 'And'): ['the', 'the', 'God'],
 ('the', 'earth.', 'So'): ['God'],
 ('the', 'earth:', 'and'): ['it', 'it'],
 ('the', 'evening', 'and'): ['the', 'the', 'the', 'the', 'the', 'the'],
 ('the', 'face', 'of'): ['the', 'the', 'all'],
 ('the', 'fifth', 'day.'): ['And'],
 ('the', 'firmament', 'Heaven.'): ['And'],
 ('the', 'firmament', 'from'): ['the'],
 ('the', 'firmament', 'of'): ['the', 'the', 'the'],
 ('the', 'firmament,', 'and'): ['divided'],
 ('the', 'firmament:', 'and'): ['it'],
 ('the', 'first', 'day.'): ['And'],
 ('the', 'fish', 'of'): ['the', 'the'],
 ('the', 'fourth', 'day.'): ['And'],
 ('the', 'fowl', 'of'): ['the', 'the'],
 ('the', 'fruit', 'of'): ['a'],
 ('the', 'fruit', 'tree'): ['yielding'],
 ('the', 'gathering', 'together'): ['of'],
 ('the', 'greater', 'light'): ['to'],
 ('the', 'heaven', 'and'): ['the'],
 ('the', 'heaven', 'be'): ['gathered'],
 ('the', 'heaven', 'to'): ['divide', 'give', 'give'],
 ('the', 'herb', 'yielding'): ['seed,'],
 ('the', 'image', 'of'): ['God'],
 ('the', 'lesser', 'light'): ['to'],
 ('the', 'light', 'Day,'): ['and'],
 ('the', 'light', 'from'): ['the', 'the'],
 ('the', 'light,', 'that'): ['it'],
 ('the', 'living', 'creature'): ['after'],
 ('the', 'midst', 'of'): ['the'],
 ('the', 'morning', 'were'): ['the', 'the', 'the', 'the', 'the', 'the'],
 ('the', 'moving', 'creature'): ['that'],
 ('the', 'night,', 'and'): ['to'],
 ('the', 'night:', 'he'): ['made'],
 ('the', 'night;', 'and'): ['let'],
 ('the', 'open', 'firmament'): ['of'],
 ('the', 'sea,', 'and'): ['over', 'over'],
 ('the', 'seas,', 'and'): ['let'],
 ('the', 'second', 'day.'): ['And'],
 ('the', 'sixth', 'day.'): [None],
 ('the', 'stars', 'also.'): ['And'],
 ('the', 'third', 'day.'): ['And'],
 ('the', 'tree', 'yielding'): ['fruit,'],
 ('the', 'waters', 'bring'): ['forth'],
 ('the', 'waters', 'brought'): ['forth'],
 ('the', 'waters', 'called'): ['he'],
 ('the', 'waters', 'from'): ['the'],
 ('the', 'waters', 'in'): ['the'],
 ('the', 'waters', 'under'): ['the'],
 ('the', 'waters', 'which'): ['were', 'were'],
 ('the', 'waters,', 'and'): ['let'],
 ('the', 'waters.', 'And'): ['God', 'God'],
 ('the', 'which', 'is'): ['the'],
 ('their', 'kind,', 'and'): ['every', 'every'],
 ('them', 'be', 'for'): ['signs,', 'lights'],
 ('them', 'have', 'dominion'): ['over'],
 ('them', 'in', 'the'): ['firmament'],
 ('them,', 'Be', 'fruitful,'): ['and'],
 ('them,', 'and', 'God'): ['said'],
 ('them,', 'saying,', 'Be'): ['fruitful,'],
 ('them.', 'And', 'God'): ['blessed'],
 ('there', 'be', 'a'): ['firmament'],
 ('there', 'be', 'light:'): ['and'],
 ('there', 'be', 'lights'): ['in'],
 ('there', 'is', 'life,'): ['I'],
 ('there', 'was', 'light.'): ['And'],
 ('thing', 'that', 'creepeth'): ['upon', 'upon', 'upon'],
 ('thing', 'that', 'he'): ['had'],
 ('thing', 'that', 'moveth'): ['upon'],
 ('thing,', 'and', 'beast'): ['of'],
 ('third', 'day.', 'And'): ['God'],
 ('to', 'divide', 'the'): ['day', 'light'],
 ('to', 'every', 'beast'): ['of'],
 ('to', 'every', 'fowl'): ['of'],
 ('to', 'every', 'thing'): ['that'],
 ('to', 'give', 'light'): ['upon', 'upon'],
 ('to', 'rule', 'over'): ['the'],
 ('to', 'rule', 'the'): ['day,', 'night:'],
 ('to', 'you', 'it'): ['shall'],
 ('together', 'of', 'the'): ['waters'],
 ('together', 'unto', 'one'): ['place,'],
 ('tree', 'yielding', 'fruit'): ['after'],
 ('tree', 'yielding', 'fruit,'): ['whose'],
 ('tree', 'yielding', 'seed;'): ['to'],
 ('tree,', 'in', 'the'): ['which'],
 ('two', 'great', 'lights;'): ['the'],
 ('under', 'the', 'firmament'): ['from'],
 ('under', 'the', 'heaven'): ['be'],
 ('unto', 'one', 'place,'): ['and'],
 ('unto', 'them,', 'Be'): ['fruitful,'],
 ('upon', 'the', 'earth'): ['after'],
 ('upon', 'the', 'earth,'): ['And', 'wherein'],
 ('upon', 'the', 'earth.'): ['So', 'And'],
 ('upon', 'the', 'earth:'): ['and', 'and'],
 ('upon', 'the', 'face'): ['of', 'of', 'of'],
 ('us', 'make', 'man'): ['in'],
 ('very', 'good.', 'And'): ['the'],
 ('void;', 'and', 'darkness'): ['was'],
 ('was', 'good.', 'And'): ['God', 'the', 'the', 'God', 'God'],
 ('was', 'good:', 'and'): ['God'],
 ('was', 'in', 'itself,'): ['after'],
 ('was', 'light.', 'And'): ['God'],
 ('was', 'so.', 'And'): ['God', 'God', 'the', 'God', 'God', 'God'],
 ('was', 'upon', 'the'): ['face'],
 ('was', 'very', 'good.'): ['And'],
 ('was', 'without', 'form,'): ['and'],
 ('waters', 'bring', 'forth'): ['abundantly'],
 ('waters', 'brought', 'forth'): ['abundantly,'],
 ('waters', 'called', 'he'): ['Seas:'],
 ('waters', 'from', 'the'): ['waters.'],
 ('waters', 'in', 'the'): ['seas,'],
 ('waters', 'under', 'the'): ['heaven'],
 ('waters', 'which', 'were'): ['under', 'above'],
 ('waters,', 'and', 'let'): ['it'],
 ('waters.', 'And', 'God'): ['said,', 'made'],
 ('were', 'above', 'the'): ['firmament:'],
 ('were', 'the', 'fifth'): ['day.'],
 ('were', 'the', 'first'): ['day.'],
 ('were', 'the', 'fourth'): ['day.'],
 ('were', 'the', 'second'): ['day.'],
 ('were', 'the', 'sixth'): ['day.'],
 ('were', 'the', 'third'): ['day.'],
 ('were', 'under', 'the'): ['firmament'],
 ('whales,', 'and', 'every'): ['living'],
 ('wherein', 'there', 'is'): ['life,'],
 ('which', 'is', 'the'): ['fruit'],
 ('which', 'is', 'upon'): ['the'],
 ('which', 'the', 'waters'): ['brought'],
 ('which', 'were', 'above'): ['the'],
 ('which', 'were', 'under'): ['the'],
 ('whose', 'seed', 'is'): ['in'],
 ('whose', 'seed', 'was'): ['in'],
 ('winged', 'fowl', 'after'): ['his'],
 ('without', 'form,', 'and'): ['void;'],
 ('years:', 'And', 'let'): ['them'],
 ('yielding', 'fruit', 'after'): ['his'],
 ('yielding', 'fruit,', 'whose'): ['seed'],
 ('yielding', 'seed', 'after'): ['his'],
 ('yielding', 'seed,', 'and'): ['the'],
 ('yielding', 'seed;', 'to'): ['you'],
 ('you', 'every', 'herb'): ['bearing'],
 ('you', 'it', 'shall'): ['be']}

We can now use the Markov model to make predictions. Given the information in the Markov model of genesis.txt, what words are likely to follow the sequence of words and over the? We can find out simply by getting the value for the key for that sequence:

In [347]:
genesis_markov_model[('and', 'over', 'the')]
Out[347]:
['night,', 'fowl', 'cattle,', 'fowl']

This tells us that the sequence and over the is followed by fowl 50% of the time, night, 25% of the time and cattle, 25% of the time.

Markov chains: Generating text from a Markov model

The Markov models we created above don't just give us interesting statistical probabilities. It also allows us generate a new text with those probabilities by chaining together predictions. Here’s how we’ll do it, starting with the order 2 character-level Markov model of condescendences: (1) start with the initial n-gram (co)—those are the first two characters of our output. (2) Now, look at the last n characters of output, where n is the order of the n-grams in our table, and find those characters in the “n-grams” column. (3) Choose randomly among the possibilities in the corresponding “next” column, and append that letter to the output. (Sometimes, as with co, there’s only one possibility). (4) If you chose “end of text,” then the algorithm is over. Otherwise, repeat the process starting with (2). Here’s a record of the algorithm in action:

co
con
cond
conde
conden
condend
condendes
condendesc
condendesce
condendesces

As you can see, we’ve come up with a word that looks like the original word, and could even be passed off as a genuine English word (if you squint at it). From a statistical standpoint, the output of our algorithm is nearly indistinguishable from the input. This kind of algorithm—moving from one state to the next, according to a list of probabilities—is known as a Markov chain.

Implementing this procedure in code is a little bit tricky, but it looks something like this. First, we'll make a Markov model of condescendences:

In [348]:
cmodel = markov_model(2, "condescendences")
In [349]:
cmodel
Out[349]:
{('c', 'e'): ['n', 's'],
 ('c', 'o'): ['n'],
 ('d', 'e'): ['s', 'n'],
 ('e', 'n'): ['d', 'c'],
 ('e', 's'): ['c', None],
 ('n', 'c'): ['e'],
 ('n', 'd'): ['e', 'e'],
 ('o', 'n'): ['d'],
 ('s', 'c'): ['e']}

We're going to generate output as we go. We'll initialize the output to the characters we want to start with, i.e., co:

In [379]:
output = "co"

Now what we have to do is get the last two characters of the output, look them up in the model, and select randomly among the characters in the value for that key (which should be a list). Finally, we'll append that randomly-selected value to the end of the string:

In [380]:
ngram = tuple(output[-2:])
next_item = random.choice(cmodel[ngram])
output += next_item
print(output)
con

Try running the cell above multiple times: the output variable will get longer and longer—until you get an error. You can also put it into a for loop:

In [391]:
output = "co"
for i in range(100):
    ngram = tuple(output[-2:])
    next_item = random.choice(cmodel[ngram])
    output += next_item
    print(output)
con
cond
conde
conden
condend
condende
condenden
condendend
condendende
condendenden
condendendend
condendendende
condendendendes
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-391-fa7de5d18422> in <module>()
      3     ngram = tuple(output[-2:])
      4     next_item = random.choice(cmodel[ngram])
----> 5     output += next_item
      6     print(output)

TypeError: must be str, not NoneType

The TypeError you see above is what happens when we stumble upon the "end of text" condition, which we'd chosen above to represent with the special Python value None. When this value comes up, it means that statistically speaking, we've reached the end of the text, and so can stop generating. We'll obey this directive by skipping out of the loop early with the break keyword:

In [401]:
output = "co"
for i in range(100):
    ngram = tuple(output[-2:])
    next_item = random.choice(cmodel[ngram])
    if next_item is None:
        break # "break" tells Python to immediately exit the loop, skipping any remaining values
    else:
        output += next_item
    print(output)
con
cond
conde
condes
condesc
condesce
condescen
condescenc
condescence
condescencen
condescencend
condescencende
condescencendes

Why range(100)? No reason, really—I just picked 100 as a reasonable number for the maximum number of times the Markov chain should produce attempt to append to the output. Because there's a loop in this particular model (nd -> e, de -> n, en -> d), any time you generate text from this Markov chain, it could potentially go on infinitely. Limiting the number to 100 makes sure that it doesn't ever actually do that. You should adjust the number based on what you need the Markov chain to do.

A function to generate from a Markov model

The gen_from_model() function below is a more general version of the code that we just wrote that works with lists and strings and n-grams of any length:

In [407]:
import random
def gen_from_model(n, model, start=None, max_gen=100):
    if start is None:
        start = random.choice(list(model.keys()))
    output = list(start)
    for i in range(max_gen):
        start = tuple(output[-n:])
        next_item = random.choice(model[start])
        if next_item is None:
            break
        else:
            output.append(next_item)
    return output

The gen_from_model() function's first parameter is the length of n-gram; the second parameter is a Markov model, as returned from markov_model() defined above, and the third parameter is the "seed" n-gram to start the generation from. The gen_from_model() function always returns a list:

In [408]:
gen_from_model(2, cmodel, ('c', 'o'))
Out[408]:
['c', 'o', 'n', 'd', 'e', 'n', 'c', 'e', 'n', 'd', 'e', 's', 'c', 'e', 's']

So if you're working with a character-level Markov chain, you'll want to glue the list back together into a string:

In [467]:
''.join(gen_from_model(2, cmodel, ('c', 'o')))
Out[467]:
'condendescendendencendencesces'

If you leave out the "seed," this function will just pick a random n-gram to start with:

In [498]:
sea_model = markov_model(3, "she sells seashells by the seashore")
for i in range(12):
    print(''.join(gen_from_model(3, sea_model)))
ashells seashore
s seashore
hells seashe seashe seashore
y the sells by the seashe sells by the sells seashells by the seashore
s seashe seashe sells seashore
she seashore
ells by the seashore
ore
y the sells sells by the seashore
she seashore
ore
sells by the seashe sells sells sells by the sells seashe seashe seashore

Advanced Markov style: Generating lines

You can use the gen_from_model() function to generate word-level Markov chains as well:

In [499]:
genesis_word_model = markov_model(2, open("genesis.txt").read().split())
In [503]:
generated_words = gen_from_model(2, genesis_word_model, ('In', 'the'))
print(' '.join(generated_words))
In the beginning God created great whales, and every living creature after his kind: and God said unto them, Be fruitful, and multiply, and fill the waters in the firmament of the waters, and let it divide the waters which were under the firmament Heaven. And the evening and the morning were the fourth day. And God said, Let the earth after his kind, and cattle after their kind, and cattle after their kind, and the gathering together of the earth, and to every fowl of the air, and over the night, and to divide the day and over all the earth,

This looks good! But there's a problem: the generation of the text just sorta... keeps going. Actually it goes on for exactly 100 words, which is also the maximum number of iterations specified in the function. We can make it go even longer by supplying a fourth parameter to the function:

In [504]:
generated_words = gen_from_model(2, genesis_word_model, ('In', 'the'), 500)
print(' '.join(generated_words))
In the beginning God created he him; male and female created he him; male and female created he him; male and female created he him; male and female created he him; male and female created he him; male and female created he them. And God said, Let there be light: and there was light. And God said, Let the waters which were above the firmament: and it was so. And the earth bring forth the living creature after his kind, cattle, and creeping thing, and beast of the air, and over the cattle, and creeping thing, and beast of the sea, and over the fish of the waters. And God made two great lights; the greater light to rule the day, and the morning were the third day. And God said, Let the earth was without form, and void; and darkness was upon the earth brought forth grass, the herb yielding seed after his kind, and every tree, in the which is upon the earth. And the earth brought forth abundantly, after their kind, and cattle after their kind, and cattle after their kind, and every living thing that moveth upon the earth. So God created the heaven and the morning were the sixth day.

The reason for this is that unless the Markov chain generator reaches the "end of text" token, it'll just keep going on forever. And the longer the text, the less likely it is that the "end of text" token will be reached.

Maybe this is okay, but the underlying text actually has some structure in it: each line of the file is actually a verse. If you want to generate individual verses, you need to treat each line separately, producing an end-of-text token for each line. The following function does just this by creating a model, adding each item from a list to the model as a separate item, and returning the combined model:

In [505]:
def markov_model_from_sequences(n, sequences):
    model = {}
    for item in sequences:
        add_to_model(model, n, item)
    return model

This function expects to receive a list of sequences (the sequences can be either lists or strings, depending on if you want a word-level model or a character-level model). So, for example:

In [515]:
genesis_lines = open("genesis.txt").readlines() # all of the lines from the file
# genesis_lines_words will be a list of lists of words in each line
genesis_lines_words = [line.strip().split() for line in genesis_lines] # strip whitespace and split into words
genesis_lines_model = markov_model_from_sequences(2, genesis_lines_words)

The genesis_lines_model variable now contains a Markov model with end-of-text tokens where they should be, at the end of each line. Generating from this model, we get:

In [516]:
for i in range(10):
    print("verse", i, "-", ' '.join(gen_from_model(2, genesis_lines_model)))
verse 0 - firmament from the waters called he Seas: and God saw that it was so.
verse 1 - creature that moveth, which the waters bring forth the living creature after his kind, whose seed was in itself, upon the face of the air, and over all the earth, and to divide the day from the darkness: and God saw the light, that it was good.
verse 2 - cattle, and creeping thing, and beast of the heaven be gathered together unto one place, and let it divide the light from the night; and let them have dominion over the cattle, and over the fowl of the heaven and the morning were the fourth day.
verse 3 - seed after his kind, whose seed is in itself, after his kind: and God saw the light, that it was good.
verse 4 - forth grass, the herb yielding seed, and the morning were the fourth day.
verse 5 - for signs, and for days, and years:
verse 6 - whose seed is in itself, after his kind: and it was good.
verse 7 - were the third day.
verse 8 - over every creeping thing that creepeth upon the earth: and it was good.
verse 9 - seed, and the darkness he called Night. And the evening and the lesser light to rule over the fish of the heaven be gathered together unto one place, and let them have dominion over the fowl of the earth after his kind, whose seed was in itself, after his kind, and every tree, in the firmament of the deep. And the earth was without form, and void; and darkness was upon the earth.

Better—the verses are ending at appropriate places—but still not quite right, since we're generating from random keys in the Markov model! To make this absolutely correct, we'd want to start each line with an n-gram that also occurred at the start of each line in the original text file. To do this, we'll work in two passes. First, get the list of lists of words:

In [517]:
genesis_lines = open("genesis.txt").readlines() # all of the lines from the file
# genesis_lines_words will be a list of lists of words in each line
genesis_lines_words = [line.strip().split() for line in genesis_lines] # strip whitespace and split into words

Now, get the n-grams at the start of each line:

In [518]:
genesis_starts = [item[:2] for item in genesis_lines_words if len(item) >= 2]

Now create the Markov model:

In [519]:
genesis_lines_model = markov_model_from_sequences(2, genesis_lines_words)

And generate from it, picking a random "start" for each line:

In [520]:
for i in range(10):
    start = random.choice(genesis_starts)
    generated = gen_from_model(2, genesis_lines_model, random.choice(genesis_starts))
    print("verse", i, "-", ' '.join(generated))
verse 0 - And the evening and the gathering together of the earth was without form, and void; and darkness was upon the earth, and to every beast of the sea, and over all the earth,
verse 1 - And God made the stars also.
verse 2 - And the evening and the lesser light to rule over the fowl of the air, and over the fish of the heaven be gathered together unto one place, and let them be for lights in the earth.
verse 3 - And the Spirit of God created the heaven to give light upon the face of the heaven to give light upon the earth, and every tree, in the firmament of the heaven to divide the day from the waters.
verse 4 - And God said, Let us make man in his own image, in the image of God moved upon the face of the heaven to give light upon the face of all the earth, and to every beast of the air, and over all the earth,
verse 5 - And God called the dry land appear: and it was good.
verse 6 - And the evening and the morning were the second day.
verse 7 - And the Spirit of God created he him; male and female created he them.
verse 8 - And God set them in the midst of the earth, and to every fowl of the air, and over the day from the waters which were above the firmament: and it was good.
verse 9 - And God said, Let there be light: and there was light.

Putting it together

The markov_generate_from_sequences() function below wraps up everything above into one function that takes an n-gram length, a list of sequences (e.g., a list of lists of words for a word-level Markov model, or a list of strings for a character-level Markov model), and a number of lines to generate, and returns that many generated lines, starting the generation only with n-grams that begin lines in the source file:

In [530]:
def markov_generate_from_sequences(n, sequences, count, max_gen=100):
    starts = [item[:n] for item in sequences if len(item) >= n]
    model = markov_model_from_sequences(n, sequences)
    return [gen_from_model(n, model, random.choice(starts), max_gen)
           for i in range(count)]

Here's how to use this function to generate from a character-level Markov model of frost.txt:

In [531]:
frost_lines = [line.strip() for line in open("frost.txt").readlines()]
for item in markov_generate_from_sequences(5, frost_lines, 20):
    print(''.join(item))
I doubted if I should not travel both that the one as fair,
And sorry I could ever come back.
Then took the difference.
I took the other, as just as for another, as just as for that morning equally about the other day!
Then took the one less travel both that has made all the undergrowth;
Had worn them really lay
And looked down one less traveler, long I stood
And that has made all the difference.
Because it was grassy and wanted wear;
Somewhere it was grassy and I—
And that has made all the better claim,
And be one as for another day!
In leaves no step had trodden black.
I doubted if I should ever come back.
And looked down one as fair,
In leaves no step had trodden black.
And be one travelled by,
To where ages and I—
And that has made all the better claim,
I shall be telling there

And from a word-level Markov model of Shakespeare's sonnets:

In [532]:
sonnets_words = [line.strip().split() for line in open("sonnets.txt").readlines()]
for item in markov_generate_from_sequences(2, sonnets_words, 14):
    print(' '.join(item))
The beast that bears the strong offence's cross.
Like stones of worth they thinly placed are,
Come in the breath that from my face she turns my foes,
The sun itself sees not, till heaven clears.
But rising at thy name doth point out thee,
And captive good attending captain ill:
The more I hear and see just cause of this excess,
And life no longer than thy sins are;
And thither hied, a sad slave, stay and think of nought
Shall reasons find of settled gravity;
How can I then did feel,
Crooked eclipses 'gainst his glory fight,
And you in me can nothing worthy prove;
Perforce am thine, and born of thee:

A fun thing to do is combine two source texts and make a Markov model from the combination. So for example, read in the lines of both The Road Not Taken and genesis.txt and put them into the same list:

In [556]:
frost_lines = [line.strip() for line in open("frost.txt").readlines()]
genesis_lines = [line.strip() for line in open("genesis.txt").readlines()]
both_lines = frost_lines + genesis_lines
for item in markov_generate_from_sequences(5, both_lines, 14, max_gen=150):
    print(''.join(item))
And God said unto one travel both that may fly above the morning and for that moveth upon the firmament of the waters brought forth grassy and for meat.
And both
And God said, Let the fish of the better claim,
Because it bent in the waters brought from the evening were the heaven be gathere be a firmament in the difference.
And the earth bring forth grass, the heaven and the second day.
And God said, Let there is life, I have dominion over all the air, and the second day.
And looked down one lesser lights in the living created man in his kind, cattle, and over every good.
In the first day.
And sorry I could every good.
And have given every fowl that hath life, and them.
And God saw that the earth,
Had worn the waters, and fill the darkness he called the heaven be gathering fruitful, and over every thing, Be fruitful, and replenish the day, and every 
And be one place, and over the darkness: and the undergrowth;
Because it bent in the passing that creeping thing thing that it was in there be light from the earth,

The resulting text has properties of both of the underlying source texts! Whoa.

Putting it all even more together

If you're really super lazy, the markov_generate_from_lines_in_file() function below does allll the work for you. It takes an n-gram length, an open filehandle to read from, the number of lines to generate, and the string char for a character-level Markov model and word for a word-level model. It returns the requested number of lines generated from a Markov model of the desired order and level.

In [543]:
def markov_generate_from_lines_in_file(n, filehandle, count, level='char', max_gen=100):
    if level == 'char':
        glue = ''
        sequences = [item.strip() for item in filehandle.readlines()]
    elif level == 'word':
        glue = ' '
        sequences = [item.strip().split() for item in filehandle.readlines()]
    generated = markov_generate_from_sequences(n, sequences, count, max_gen)
    return [glue.join(item) for item in generated]

So, for example, to generate twenty lines from an order-3 model of H.D.'s Sea Rose:

In [544]:
for item in markov_generate_from_lines_in_file(3, open("sea_rose.txt"), 20, 'char'):
    print(item)
single of leaf?
single of petals,
hardened and
you are flung on a wet rose, harsh rose,
you are flower, the crisp such acrid fragrance
single on the sand,
drip such acrisp sand,
drip such acrisp such acrisp such acrisp sand,
that drives in the drip sand
meagrance
more of petals,
Stunted, with stem --
you are on the caught in the sand
spare precious
meagrance
more flung on a wet rose,
you are flower, the with small leaf,
meagrance
marred in the caught in the spice-rose
spare precious

Or an order-3 word-level model of genesis.txt:

In [545]:
for item in markov_generate_from_lines_in_file(3, open("genesis.txt"), 5, 'word'):
    print(item)
    print("")
And God created great whales, and every living creature that moveth, which the waters brought forth abundantly, after their kind, and every winged fowl after his kind: and God saw that it was good: and God divided the light from the darkness.

And to rule over the day and over the fowl of the air, and to every fowl of the air, and over the cattle, and over all the earth, and to every fowl of the air, and to every thing that he had made, and, behold, it was very good. And the evening and the morning were the fifth day.

In the beginning God created the heaven and the earth.

And God made the firmament, and divided the waters which were above the firmament: and it was so.

And God said, Behold, I have given every green herb for meat: and it was so.