codex/past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2003/iii/paper-42-dendrogram.py

past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2003/iii/paper-42-dendrogram.py
"""Reconstruct the derived complete-linkage tree; emit one PNG to caller CWD.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Only declared root dependencies; caller controls MPLCONFIGDIR and backend.
"""
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

names=['Philip','Chad','Graham','Tim','Mark','Juliet','Garfield','Nicolas','Frederic','John','Sauli','Fred','Gbenga']
merges=[(-12,-13),(-3,-4),(-2,-8),(-9,3),(-10,4),(-1,-5),(-7,1),(2,6),(5,7),(-11,8),(-6,10),(9,11)]
heights=[0,1/8,1/6,1/3,1/3,3/8,2/5,4/9,1/2,5/8,3/4,7/8]
children={i+1:p for i,p in enumerate(merges)}
def leaves(node):
    if node<0:
        return [node]
    a,b=children[node]
    return leaves(a)+leaves(b)
order=leaves(12)
x={node:position for position,node in enumerate(order)}
y={node:0.0 for node in order}
fig,ax=plt.subplots(figsize=(9.0,4.6),dpi=120,facecolor='white')
ax.set_facecolor('white')
for index,((a,b),height) in enumerate(zip(merges,heights),1):
    ax.plot([x[a],x[a],x[b],x[b]],[y[a],height,height,y[b]],color='#244d73',lw=1.8)
    x[index]=(x[a]+x[b])/2
    y[index]=height
ax.set_xticks(range(13),[names[-node-1] for node in order],rotation=55,ha='right')
ax.set_ylim(-.02,.94)
ax.set_ylabel('Complete-linkage Jaccard distance')
ax.set_title('Binary profiles: reconstructed clustering tree')
ax.spines[['top','right']].set_visible(False)
ax.grid(axis='y',color='#dddddd',lw=.6)
fig.tight_layout()
fig.savefig('paper-42-dendrogram.png',facecolor='white',transparent=False)
plt.close(fig)