<jupyter_start><jupyter_text>Simple Hierarchical Clustering<jupyter_code># 
# Importing required libraries/modules:
%reset -f
import numpy as np

# 
# Declaring data items and the dissimilarity matrix:
items = np.array(["A", "B", "C", "D", "E", "F", "G"], dtype=object)
mtrx = np.array([
        [0.0000, 0.5000, 0.4286, 1.0000, 0.2500, 0.6250, 0.3750],
        [0.5000, 0.0000, 0.7143, 0.8333, 0.6667, 0.2000, 0.7778],
        [0.4286, 0.7143, 0.0000, 1.0000, 0.4286, 0.6667, 0.3333],
        [1.0000, 0.8333, 1.0000, 0.0000, 1.0000, 0.8000, 0.8571],
        [0.2500, 0.6667, 0.4286, 1.0000, 0.0000, 0.7778, 0.3750],
        [0.6250, 0.2000, 0.6667, 0.8000, 0.7778, 0.0000, 0.7500],
        [0.3750, 0.7778, 0.3333, 0.8571, 0.3750, 0.7500, 0.0000],
])<jupyter_output><empty_output><jupyter_text>---<jupyter_code># 
for (i, j) in np.argwhere(mtrx == 0):
    mtrx[i, j] = 99

print(mtrx)
# 
min_value = np.min(mtrx)
print(f'{min_value= }')

idxs_min_value = np.argwhere(mtrx == min_value)
print(f'{idxs_min_value.tolist()= }')

items_min_value = items[idxs_min_value]
print(f'{items_min_value.tolist()= }')

# 
idxs_min_value = idxs_min_value[0]
items_min_value = items_min_value[0]

items[idxs_min_value[0]] = "".join(items[idxs_min_value])
items = np.delete(items, idxs_min_value[1])

print(f'{items.tolist()= }')

#
# create a copy of the original matrix
single_linkage_mtrx = mtrx.copy()

# merge row and column entries for `B` and `F` using the minimum values
merged_result = np.min([mtrx[:, idxs_min_value[0]], mtrx[:, idxs_min_value[1]]], axis=0)

# replace the `B` row and column with the merged result
single_linkage_mtrx[:, idxs_min_value[0]] = merged_result
single_linkage_mtrx[idxs_min_value[0], :] = merged_result

# delete the `F` row and column 
single_linkage_mtrx = np.delete(single_linkage_mtrx, idxs_min_value[1], axis=0)
single_linkage_mtrx = np.delete(single_linkage_mtrx, idxs_min_value[1], axis=1)

# restore the dummy diagonal value
single_linkage_mtrx[idxs_min_value[0], idxs_min_value[0]] = 99

single_linkage_mtrx

#
# create a copy of the original matrix
complete_linkage_mtrx = mtrx.copy()

# merge row and column entries for `B` and `F` using the maximum values
merged_row = np.max([mtrx[:, idxs_min_value[0]], mtrx[:, idxs_min_value[1]]], axis=0)

# replace the `B` row and column with the merged result
complete_linkage_mtrx[:, idxs_min_value[0]] = merged_row
complete_linkage_mtrx[idxs_min_value[0], :] = merged_row

# delete the `F` row and column 
complete_linkage_mtrx = np.delete(complete_linkage_mtrx, idxs_min_value[1], axis=0)
complete_linkage_mtrx = np.delete(complete_linkage_mtrx, idxs_min_value[1], axis=1)

# restore the dummy diagonal value
complete_linkage_mtrx[idxs_min_value[0], idxs_min_value[0]] = 99

complete_linkage_mtrx

# 
print(f"single-linkage @ `BF`  : {single_linkage_mtrx[1]}")
print(f"complete-linkage @ `BF`: {complete_linkage_mtrx[1]}")<jupyter_output>single-linkage @ `BF`  : [ 0.5    99.      0.6667  0.8     0.6667  0.75  ]
complete-linkage @ `BF`: [ 0.625  99.      0.7143  0.8333  0.7778  0.7778]<jupyter_text>---<jupyter_code># 
# function `hierarchical_clustering()` :
# collapse-hide
def hierarchical_clustering(mtrx, n_clusters=None, items=None, criterion="max", verbose=False):
    assert criterion in ["min", "max"], "Pass ['min'/'max'] for clustering linkage criterion!"

    mtrx = np.array(mtrx.copy(), dtype=float)
    assert len(mtrx.shape) == 2, "The distance matrix must be a square, symmetric matrix!"
    assert mtrx.shape[0] == mtrx.shape[1], "The distance matrix must be a square, symmetric matrix!"

    # create auto-generated items array if not provided
    if items is None:
        items = [f"P{i}" for i in range(mtrx.shape[0])]
        print(f"auto-generated {items[:5]= } ...")

    items = np.array(items.copy(), dtype=object)
    criterion = np.min if criterion == "min" else np.max
    hist_clustering = [tuple(items)]

    # prevent the zero-diagonal from interfering with np.min()
    for (i, j) in np.argwhere(mtrx == 0):
        mtrx[i, j] = 99

    while len(items) != (n_clusters if n_clusters else 1):
        # get closest clusters
        min_value = np.min(mtrx)
        idxs_min_value = np.argwhere(mtrx == min_value)[0]
        idx_0, idx_1 = idxs_min_value

        # create current clusters and update items and clustering history list
        items[idx_0] = "".join(items[idxs_min_value])
        items = np.delete(items, idx_1)
        hist_clustering.append(tuple(items))

        # update distance matrix
        mtrx[:, idx_0] = criterion([mtrx[:, idx_0], mtrx[:, idx_1]], axis=0)
        mtrx[idx_0, :] = criterion([mtrx[:, idx_0], mtrx[:, idx_1]], axis=0)
        mtrx = np.delete(mtrx, idx_1, axis=0)
        mtrx = np.delete(mtrx, idx_1, axis=1)
        mtrx[idx_0, idx_0] = 99

        # print clustering progress; if verbose
        print(f"{'-' * 75}\n {min_value= :.4f} @ {list(idxs_min_value)= }") if verbose else None
        print(f" {list(items)= }\n {mtrx}") if verbose else None

    print(f"{'-' *75}") if verbose else None
    return hist_clustering[-1] if n_clusters else hist_clustering

#collapse-output
clusters = hierarchical_clustering(mtrx, n_clusters=3, verbose=True, criterion="max")
clusters

#collapse-output
clusters = hierarchical_clustering(mtrx, n_clusters=3, verbose=True, criterion="min")
clusters

#  
# decoding the output of the `hierarchical_clustering` function
items = np.array(["A", "B", "C", "D", "E", "F", "G"], dtype=object)

for idx, c in enumerate(clusters):
    _c_items = [int(i) for i in str(c).replace("P", "")]
    
    for i in _c_items:
        print(f"item <{items[i]}> belongs to cluster <{idx}>")<jupyter_output>item <A> belongs to cluster <0>
item <E> belongs to cluster <0>
item <C> belongs to cluster <0>
item <G> belongs to cluster <0>
item <B> belongs to cluster <1>
item <F> belongs to cluster <1>
item <D> belongs to cluster <2>
{'fraction_non_alphanumeric': 0.1520745131244708, 'fraction_numerical': 0.06265876375952582, 'mean_word_length': 4.171628721541156, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '3', 'dir': 'jupyter-structured-clean-dedup', 'id': '254759', 'license': 'Apache-2.0', 'n_tokens_mistral': 2584, 'n_tokens_neox': 2328, 'n_words': 544, 'path': '_notebooks/ipynb_data/Simple-Hierarchical-Clustering.ipynb', 'repo_name': 'Outsiders17711/Fastpages.Massively.Oriol', 'size': '5905'}