codexprobability-generating-function-compound-random-count.py
probability-generating-function-compound-random-count.py
"""Compound count coefficients, not observations; Python 3.14 / matplotlib 3.10.7.
Run from _media; writes the matching PNG to the current directory.
"""
from decimal import Decimal, localcontext
from math import factorial
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
def model_coefficients(limit=20):
with localcontext() as ctx:
ctx.prec = 60
p = [Decimal(-2).exp()]
for n in range(1, limit+1):
p.append((p[n-1] + (2*p[n-2] if n > 1 else 0))/n)
for n, value in enumerate(p):
exact = p[0]*sum((Decimal(1)/Decimal(factorial(n-2*b)*factorial(b))
for b in range(n//2+1)), Decimal())
assert abs(value-exact) < Decimal('1e-58')
tail = Decimal(1) - sum(p)
return p, tail
def main():
p, tail = model_coefficients()
plt.rcParams.update({'font.size': 12, 'font.family': 'DejaVu Sans'})
fig = plt.figure(figsize=(10, 6.5), dpi=120, facecolor='white')
fig.suptitle('A random number of jumps changes both the mean and the spread',
fontsize=17, y=.97)
flow = fig.add_axes([.03, .64, .94, .25])
flow.set(xlim=(0, 1), ylim=(0, 1))
flow.axis('off')
boxes = [(.015, .25, .24, '#466b9c', 'Draw the count', r'$N\sim\mathrm{Poisson}(2)$'),
(.365, .25, .28, '#168477', 'Attach N independent jumps', r'$P(Y_j=1)=P(Y_j=2)=1/2$'),
(.755, .25, .23, '#99538a', 'Add the jumps', r'$S=\sum_{j=1}^{N}Y_j$')]
for x, y, width, color, title, formula in boxes:
flow.add_patch(FancyBboxPatch((x,y), width, .60,
boxstyle='round,pad=0.008,rounding_size=0.025',
facecolor=color+'15', edgecolor=color, linewidth=1.8))
flow.text(x+width/2, y+.43, title, ha='center', va='center', fontsize=11)
flow.text(x+width/2, y+.20, formula, ha='center', va='center', fontsize=13)
for a, b in [(.27, .35), (.66, .74)]:
flow.annotate('', xy=(b,.55), xytext=(a,.55),
arrowprops={'arrowstyle':'->', 'lw':2, 'color':'#405060'})
flow.text(.5, .06, r'The count is independent of the entire jump sequence; $N=0$ gives $S=0$.',
ha='center', fontsize=11)
fig.text(.5, .60, r'$G_S(s)=G_N(G_Y(s))=e^{s+s^2-2}$', ha='center', fontsize=18)
ax = fig.add_axes([.085, .17, .87, .35])
ax.bar(range(21), [float(v) for v in p], color='#168477', width=.72)
ax.set(xlim=(-.7, 20.8), ylim=(0, .235), xticks=range(0,21,2),
xlabel='Aggregate count n', ylabel=r'$p_n=P(S=n)$')
ax.spines[['top','right']].set_visible(False)
ax.grid(axis='y', alpha=.18)
ax.set_axisbelow(True)
ax.text(.985, .94, r'$p_0=e^{-2},\quad np_n=p_{n-1}+2p_{n-2}$'+'\n'
+r'$\mathbb{E}[S]=3,\quad\mathrm{Var}(S)=5$'+'\n'
+f'Omitted P(S > 20) = {tail:.6E}',
transform=ax.transAxes, ha='right', va='top', fontsize=12,
bbox={'facecolor':'white','edgecolor':'#b9c7cf','boxstyle':'round,pad=.6'})
fig.text(.5, .065, 'Calculated model coefficients for n = 0,…,20 • no observed data • no renormalization',
ha='center', fontsize=12)
fig.savefig(Path(__file__).stem+'.png', facecolor='white')
plt.close(fig)
print(f'P(S > 20) = {tail}')
if __name__ == '__main__':
main()