David Dale's answer is a nice demo but unfortunately has a typo. The log likelihood scaling should be by freq_of_word_counts
and not word_counts
. This is why he gets a clearly invalid slope of -0.5366
.
ivangtorre's answer is also nice in that it plots the MLE-fitted distribution. However his answer is also incorrect in that it computes the normalization term of the Zipf distribution in terms of the observed quantities. This is why he also gets an invalid slope of -1.62
.
I believe the correct scaling factor is is -1.674
. This is approximately the slope of the graph in David Dale's plot and as pointed out by ivangtorre in his comment to David Dale's post ("The slope in your data looks to be 1.66 (10/6).").
The correct calculation is as follows.
from collections import Counter
import matplotlib.pyplot as plt
import nltk
import numpy as np
import scipy as sp
try:
nltk.corpus.brown.words
except LookupError:
nltk.download('brown')
counter_of_words = Counter(nltk.corpus.brown.words())
counter_of_counts = Counter(counter_of_words.values())
counter_of_counts = sorted(
counter_of_counts.items(),
key=lambda pair: pair[1],
reverse=True)
word_counts, freq_of_word_counts = np.asarray(counter_of_counts).T
def negloglik_zipf(s, word_counts, freq_of_word_counts):
# Reference: https://www.johndcook.com/blog/2021/12/11/zeta-zipf-yule/
log_probs = -s * np.log(word_counts)
document_length = sum(freq_of_word_counts)
all_possible_counts = np.arange(1, document_length + 1)
log_probs -= sp.special.logsumexp(-s * np.log(all_possible_counts))
return -np.sum(freq_of_word_counts * log_probs)
loss_fn = lambda s: negloglik_zipf(s, word_counts, freq_of_word_counts)
s_best = sp.optimize.minimize_scalar(loss_fn, [0.1, 3.0])
print(s_best)
f, ax = plt.subplots()
ax.scatter(word_counts, freq_of_word_counts, label='data')
ax.set_title('Power law for word frequencies')
ax.set_xlabel('Word frequency')
ax.set_ylabel('Number of such words')
ax.set_xscale('log')
ax.set_yscale('log')
g = np.arange(1, 500)
ax.plot(g, g**(-s_best.x) * max(freq_of_word_counts),
'--', color='orange', lw=3, label='MLE Zipf')
ax.legend()