Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
suyashi29
GitHub Repository: suyashi29/python-su
Path: blob/master/ML Clustering Analysis/ 4 Hierarchical-Clustering.ipynb
3074 views
Kernel: Python 3 (ipykernel)

Hierarchical Clustering

What is Hierarchical Clustering ?

Hierarchical clustering is a method of cluster analysis that seeks to build a hierarchy of clusters. It is particularly useful when the number of clusters is unknown, as it provides a dendrogram, a tree-like diagram that shows the arrangement of the clusters formed by the algorithm.

Types of Hierarchical Clustering

  1. Agglomerative (Bottom-Up) Clustering:

  • Start with each data point as its own cluster.

  • At each step, merge the two closest clusters.

  • Continue until all data points are merged into a single cluster.

Divisive (Top-Down) Clustering:

  • Start with all data points in a single cluster.

  • At each step, split the cluster into two.

  • Continue until each data point is in its own cluster.

  • It is a type of connectivity model clustering which is based on the fact that data points that are closer to each other are more similar than the data points lying far away in a data space.

As the name speaks for itself, the hierarchical clustering forms the hierarchy of the clusters that can be studied by visualising dendogram.

image.png

image.png

How to measure closeness of points?

image.png

Hierarchical Clustering - Agglomerative

We will be looking at a clustering technique, which is Agglomerative Hierarchical Clustering.

  1. Centroid Distance: Euclidean distance between mean of data points in the two clusters

  2. Minimum Distance: Euclidean distance between two data points in the two clusters that are closest to each other

  3. Maximum Distance: Euclidean distance between two data points in the two clusters that are farthest to each other

Understanding Algoritm

  1. Let there be N data points. Firstly, these N data points are assigned to N different clusters with one data point in each cluster.

  2. Then, two data points with minimum euclidean distance between them are merged into a single cluster.

  3. Then, two clusters with minimum centroid distance between them are merged into a single cluster.

  4. This process is repeated until we are left with a single cluster, hence forming hierarchy of clusters.

image.png

  • a) Agglomerative clustering

One of the simplest and easily understood algorithms used to perform agglomerative clustering is single linkage. In this algorithm, we start with considering each data point as a subcluster. We define a metric to measure the distance between all pairs of subclusters at each step and keep merging the nearest two subclusters in each step. We repeat this procedure till there is only one cluster in the system.

  • b) Divisive clustering

One of the algorithms used to perform divisive clustering is recursive k-means. As the name suggests, you recursively perform the procedure of k-means on each intermediate cluster till you encounter all the data samples in the system or the minimum number of data samples you desire to have in a cluster. In each step of this algorithm, you have to be mindful of how many clusters would you like to create next.

Explanation: Steps to Perform Agglomerative Hierarchical Clustering

  • linkage method: Defines how the distance between clusters is computed. The ward method minimizes the total variance within clusters. (We can experiment with different linkage methods (single, complete, average, etc.)

  • dendrogram: Visualizes the merging of clusters. The y-axis represents the distance (or dissimilarity) at which clusters are merged.

  • fcluster: Forms flat clusters by cutting the dendrogram at a specific height.

import numpy as np import matplotlib.pyplot as plt from scipy.cluster.hierarchy import dendrogram, linkage, fcluster # Example dataset np.random.seed(42) X = np.random.rand(10, 2) # 10 points in 2D space # Perform hierarchical/agglomerative clustering Z = linkage(X, method='ward') #single, complete, average # Plot the Dendrogram plt.figure(figsize=(10, 7)) dendrogram(Z) plt.title("Dendrogram for Hierarchical Clustering") plt.xlabel("Data Points") plt.ylabel("Distance") plt.show() # Decide the number of clusters max_d = 0.6 # This value determines the cutting height of the dendrogram clusters = fcluster(Z, max_d, criterion='distance') print("Cluster labels:", clusters)
Image in a Jupyter notebook
Cluster labels: [1 3 2 1 3 1 3 2 2 2]

How many Clusters to form:

  1. Visualising dendogram: Best choice of no. of clusters is no. of vertical lines that can be cut by a horizontal line, that can transverse maximum distance vertically without intersecting other cluster. For eg., in the below case, best choice for no. of clusters will be 4.

  2. Intuition and prior knowledge of the data set. image.png

Good Cluster Analysis

Data-points within same cluster share similar profile: Statistically, check the standard deviation for each input variable in each cluster. A perfect separation in case of cluster analysis is rarely achieved. Hence, even one standard deviation distance between two cluster means is considered to be a good separation.

  • Well spread proportion of data-points among clusters: There are no standards for this requirement. But a minimum of 5% and maximum of 35% of the total population can be assumed as a safe range for each cluster.

import numpy as np import pandas as pd from scipy import ndimage from scipy.cluster import hierarchy from scipy.spatial import distance_matrix from matplotlib import pyplot as plt from sklearn import manifold, datasets from sklearn.cluster import AgglomerativeClustering from sklearn.datasets import make_blobs %matplotlib inline import warnings # Ignore warning related to pandas_profiling warnings.filterwarnings('ignore')

Generating Random Data

We will be generating a set of data using the make_blobs class.

Input these parameters into make_blobs:
  • n_samples: The total number of points equally divided among clusters.
    • Choose a number from 10-1500
  • centers: The number of centers to generate, or the fixed center locations.
    • Choose arrays of x,y coordinates for generating the centers. Have 1-10 centers (ex. centers=[[1,1], [2,5]])
  • cluster_std: The standard deviation of the clusters. The larger the number, the further apart the clusters
    • Choose a number between 0.5-1.5

Save the result to X1 and y1.
X1, y1 = make_blobs(n_samples=500, centers=[[5,5], [-2, -1],[10,4]], cluster_std=0.5)

Plot the scatter plot of the randomly generated data

plt.scatter(X1[:, 0], X1[:, 1], marker='*')

Agglomerative Clustering

We will start by clustering the random data points we just created.

The Agglomerative Clustering class will require two inputs:

  • n_clusters: The number of clusters to form as well as the number of centroids to generate.
    • Value will be: 4
  • linkage: Which linkage criterion to use. The linkage criterion determines which distance to use between sets of observation. The algorithm will merge the pairs of cluster that minimize this criterion.
    • Value will be: 'complete'
    • Note: It is recommended you try everything with 'average' as well

Save the result to a variable called agglom
agglom = AgglomerativeClustering(n_clusters = 3, linkage = 'complete')

Fit the model with X1 and y1 from the generated data above.

agglom.fit(X1,y1)

Run the following code to show the clustering!
Remember to read the code and comments to gain more understanding on how the plotting works.

a=np.array([1,1,2,45,6,-7]) np.max(a) np.min(a)
# Create a figure of size 10 inches by 8 inches. plt.figure(figsize=(10,8)) # These two lines of code are used to scale the data points down, # Or else the data points will be scattered very far apart. # Create a minimum and maximum range of X1. x_min, x_max = np.min(X1, axis=0), np.max(X1, axis=0) # Get the average distance for X1. X1 = (X1 - x_min) / (x_max - x_min) # This loop displays all of the datapoints. for i in range(X1.shape[0]): # Replace the data points with their respective cluster value # (ex. 0) and is color coded with a colormap (plt.cm.spectral) plt.text(X1[i, 0], X1[i, 1], str(y1[i]), color=plt.cm.nipy_spectral(agglom.labels_[i] / 10.), fontdict={'weight': 'bold', 'size': 9}) # Remove the x ticks, y ticks, x and y axis plt.xticks([]) plt.yticks([]) #plt.axis('off') # Display the plot of the original data before clustering plt.scatter(X1[:, 0], X1[:, 1], marker='.') # Display the plot plt.show()

Dendrogram Associated for the Agglomerative Hierarchical Clustering

Remember that a distance matrix contains the distance from each point to every other point of a dataset .
Use the function distance_matrix, which requires two inputs. Use the Feature Matrix, X2 as both inputs and save the distance matrix to a variable called dist_matrix

Remember that the distance values are symmetric, with a diagonal of 0's. This is one way of making sure your matrix is correct.
(print out dist_matrix to make sure it's correct)
dist_matrix = distance_matrix(X1,X1) print(dist_matrix)

Using the linkage class from hierarchy, pass in the parameters:

  • The distance matrix
  • 'complete' for complete linkage

Save the result to a variable called Z
Z = hierarchy.linkage(dist_matrix, 'complete')

A Hierarchical clustering is typically visualized as a dendrogram as shown in the following cell. Each merge is represented by a horizontal line. The y-coordinate of the horizontal line is the similarity of the two clusters that were merged, where cities are viewed as singleton clusters. By moving up from the bottom layer to the top node, a dendrogram allows us to reconstruct the history of merges that resulted in the depicted clustering.

Next, we will save the dendrogram to a variable called dendro. In doing this, the dendrogram will also be displayed. Using the dendrogram class from hierarchy, pass in the parameter:

  • Z
dendro = hierarchy.dendrogram(Z)

Practice

We used complete linkage for our case, change it to average linkage to see how the dendogram changes.

# write your code here Z = hierarchy.linkage(dist_matrix, 'average') dendro = hierarchy.dendrogram(Z)

Clustering on Vehicle dataset

Imagine that an automobile manufacturer has developed prototypes for a new vehicle. Before introducing the new model into its range, the manufacturer wants to determine which existing vehicles on the market are most like the prototypes--that is, how vehicles can be grouped, which group is the most similar with the model, and therefore which models they will be competing against.

Our objective here, is to use clustering methods, to find the most distinctive clusters of vehicles. It will summarize the existing vehicles and help manufacturers to make decision about the supply of new models.

Read data

lets read dataset to see what features the manufacturer has collected about the existing models.

import pandas as pd filename = 'cars_clus.csv' #Read csv pdf = pd.read_csv(filename) print ("Shape of dataset: ", pdf.shape) pdf.head(5)
Shape of dataset: (159, 16)

The feature sets include price in thousands (price), engine size (engine_s), horsepower (horsepow), wheelbase (wheelbas), width (width), length (length), curb weight (curb_wgt), fuel capacity (fuel_cap) and fuel efficiency (mpg).

Data Cleaning

lets simply clear the dataset by dropping the rows that have null value:
print ("Shape of dataset before cleaning: ", pdf.size) pdf[[ 'sales', 'resale', 'type', 'price', 'engine_s', 'horsepow', 'wheelbas', 'width', 'length', 'curb_wgt', 'fuel_cap', 'mpg', 'lnsales']] = pdf[['sales', 'resale', 'type', 'price', 'engine_s', 'horsepow', 'wheelbas', 'width', 'length', 'curb_wgt', 'fuel_cap', 'mpg', 'lnsales']].apply(pd.to_numeric, errors='coerce') pdf = pdf.dropna() pdf = pdf.reset_index(drop=True) print ("Shape of dataset after cleaning: ", pdf.size) pdf.head(5)
Shape of dataset before cleaning: 2544 Shape of dataset after cleaning: 1872
pdf.describe(include="object") pdf.describe()
pdf.isnull().sum()
manufact 0 model 0 sales 0 resale 0 type 0 price 0 engine_s 0 horsepow 0 wheelbas 0 width 0 length 0 curb_wgt 0 fuel_cap 0 mpg 0 lnsales 0 partition 0 dtype: int64

Feature selection

Lets select our feature set:

featureset = pdf[['engine_s', 'horsepow', 'wheelbas', 'width', 'length', 'curb_wgt', 'fuel_cap', 'mpg']]

Normalization

Now we can normalize the feature set. MinMaxScaler transforms features by scaling each feature to a given range. It is by default (0, 1). That is, this estimator scales and translates each feature individually such that it is between zero and one.

from sklearn.preprocessing import MinMaxScaler x = featureset.values #returns a numpy array min_max_scaler = MinMaxScaler() feature_mtx = min_max_scaler.fit_transform(x) feature_mtx [0:5]
array([[0.11428571, 0.21518987, 0.18655098, 0.28143713, 0.30625832, 0.2310559 , 0.13364055, 0.43333333], [0.31428571, 0.43037975, 0.3362256 , 0.46107784, 0.5792277 , 0.50372671, 0.31797235, 0.33333333], [0.35714286, 0.39240506, 0.47722343, 0.52694611, 0.62849534, 0.60714286, 0.35483871, 0.23333333], [0.11428571, 0.24050633, 0.21691974, 0.33532934, 0.38082557, 0.34254658, 0.28110599, 0.4 ], [0.25714286, 0.36708861, 0.34924078, 0.80838323, 0.56724368, 0.5173913 , 0.37788018, 0.23333333]])

Clustering using Scipy

In this part we use Scipy package to cluster the dataset: First, we calculate the distance matrix.
import scipy leng = feature_mtx.shape[0] D = scipy.zeros([leng,leng]) for i in range(leng): for j in range(leng): D[i,j] = scipy.spatial.distance.euclidean(feature_mtx[i], feature_mtx[j])
C:\Users\suyashi144893\AppData\Local\Temp\1\ipykernel_13728\3007105269.py:3: DeprecationWarning: scipy.zeros is deprecated and will be removed in SciPy 2.0.0, use numpy.zeros instead D = scipy.zeros([leng,leng])

In agglomerative clustering, at each iteration, the algorithm must update the distance matrix to reflect the distance of the newly formed cluster with the remaining clusters in the forest. The following methods are supported in Scipy for calculating the distance between the newly formed cluster and each: - single - complete - average - weighted - centroid

We use complete for our case, but feel free to change it to see how the results change.

import pylab import scipy.cluster.hierarchy Z = hierarchy.linkage(D, 'complete')

Essentially, Hierarchical clustering does not require a pre-specified number of clusters. However, in some applications we want a partition of disjoint clusters just as in flat clustering. So you can use a cutting line:

from scipy.cluster.hierarchy import fcluster max_d = 3 clusters = fcluster(Z, max_d, criterion='distance') clusters
array([ 1, 5, 5, 6, 5, 4, 6, 5, 5, 5, 5, 5, 4, 4, 5, 1, 6, 5, 5, 5, 4, 2, 11, 6, 6, 5, 6, 5, 1, 6, 6, 10, 9, 8, 9, 3, 5, 1, 7, 6, 5, 3, 5, 3, 8, 7, 9, 2, 6, 6, 5, 4, 2, 1, 6, 5, 2, 7, 5, 5, 5, 4, 4, 3, 2, 6, 6, 5, 7, 4, 7, 6, 6, 5, 3, 5, 5, 6, 5, 4, 4, 1, 6, 5, 5, 5, 6, 4, 5, 4, 1, 6, 5, 6, 6, 5, 5, 5, 7, 7, 7, 2, 2, 1, 2, 6, 5, 1, 1, 1, 7, 8, 1, 1, 6, 1, 1], dtype=int32)

Also, you can determine the number of clusters directly:

from scipy.cluster.hierarchy import fcluster k = 5 clusters = fcluster(Z, k, criterion='maxclust') clusters
array([1, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 3, 2, 2, 3, 1, 3, 3, 3, 3, 2, 1, 5, 3, 3, 3, 3, 3, 1, 3, 3, 4, 4, 4, 4, 2, 3, 1, 3, 3, 3, 2, 3, 2, 4, 3, 4, 1, 3, 3, 3, 2, 1, 1, 3, 3, 1, 3, 3, 3, 3, 2, 2, 2, 1, 3, 3, 3, 3, 2, 3, 3, 3, 3, 2, 3, 3, 3, 3, 2, 2, 1, 3, 3, 3, 3, 3, 2, 3, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 3, 3, 1, 1, 1, 3, 4, 1, 1, 3, 1, 1], dtype=int32)

Now, plot the dendrogram:

fig = pylab.figure(figsize=(50,50)) def llf(id): return '[%s %s %s]' % (pdf['manufact'][id], pdf['model'][id], int(float(pdf['type'][id])) ) dendro = hierarchy.dendrogram(Z, leaf_label_func=llf, leaf_rotation=0, leaf_font_size =12, orientation = 'right')
Image in a Jupyter notebook

Clustering using scikit-learn

Lets redo it again, but this time using scikit-learn package:
dist_matrix = distance_matrix(feature_mtx,feature_mtx) print(dist_matrix)
[[0. 0.57777143 0.75455727 ... 0.28530295 0.24917241 0.18879995] [0.57777143 0. 0.22798938 ... 0.36087756 0.66346677 0.62201282] [0.75455727 0.22798938 0. ... 0.51727787 0.81786095 0.77930119] ... [0.28530295 0.36087756 0.51727787 ... 0. 0.41797928 0.35720492] [0.24917241 0.66346677 0.81786095 ... 0.41797928 0. 0.15212198] [0.18879995 0.62201282 0.77930119 ... 0.35720492 0.15212198 0. ]]

Now, we can use the 'AgglomerativeClustering' function from scikit-learn library to cluster the dataset. The AgglomerativeClustering performs a hierarchical clustering using a bottom up approach. The linkage criteria determines the metric used for the merge strategy:

  • Weighted average minimizes the sum of squared differences within all clusters. It is a variance-minimizing approach and in this sense is similar to the k-means objective function but tackled with an agglomerative hierarchical approach.

  • Maximum or complete linkage minimizes the maximum distance between observations of pairs of clusters.

  • Average linkage minimizes the average of the distances between all observations of pairs of clusters.

agglom = AgglomerativeClustering(n_clusters = 5, linkage = 'complete') agglom.fit(feature_mtx) agglom.labels_
array([3, 0, 0, 3, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 2, 3, 4, 3, 3, 0, 3, 0, 3, 3, 3, 2, 1, 1, 1, 0, 0, 3, 0, 3, 0, 0, 0, 0, 1, 0, 1, 3, 3, 3, 0, 0, 3, 3, 3, 0, 3, 3, 0, 0, 0, 0, 0, 0, 3, 3, 3, 0, 3, 0, 0, 3, 3, 0, 0, 0, 0, 3, 0, 0, 2, 3, 3, 0, 0, 0, 3, 0, 0, 0, 3, 3, 0, 3, 3, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 3, 3, 3, 0, 1, 3, 3, 3, 3, 3], dtype=int64)

And, we can add a new field to our dataframe to show the cluster of each row:

Z = hierarchy.linkage(dist_matrix, 'complete')
dendro = hierarchy.dendrogram(Z)
Image in a Jupyter notebook
pdf['cluster_'] = agglom.labels_ pdf.head()
import matplotlib.cm as cm n_clusters = max(agglom.labels_)+1 colors = cm.rainbow(np.linspace(0, 1, n_clusters)) cluster_labels = list(range(0, n_clusters)) # Create a figure of size 6 inches by 4 inches. plt.figure(figsize=(16,14)) for color, label in zip(colors, cluster_labels): subset = pdf[pdf.cluster_ == label] for i in subset.index: plt.text(subset.horsepow[i], subset.mpg[i],str(subset['model'][i]), rotation=25) plt.scatter(subset.horsepow, subset.mpg, s= subset.price*10, c=color, label='cluster'+str(label),alpha=0.5) # plt.scatter(subset.horsepow, subset.mpg) plt.legend() plt.title('Clusters') plt.xlabel('horsepow') plt.ylabel('mpg')
*c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points. *c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points. *c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points. *c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points. *c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points.
Text(0, 0.5, 'mpg')
Image in a Jupyter notebook

As you can see, we are seeing the distribution of each cluster using the scatter plot, but it is not very clear where is the centroid of each cluster. Moreover, there are 2 types of vehicles in our dataset, "truck" (value of 0 in the type column) and "car" (value of 1 in the type column). So, we use them to distinguish the classes, and summarize the cluster. First we count the number of cases in each group:

pdf.groupby(['cluster_','type'])['cluster_'].count()
cluster_ type 0 0.0 37 1.0 18 1 1.0 6 2 0.0 3 3 0.0 47 1.0 5 4 0.0 1 Name: cluster_, dtype: int64

Now we can look at the characteristics of each cluster:

agg_cars = pdf.groupby(['cluster_','type'])['horsepow','engine_s','mpg','price'].mean() agg_cars

It is obvious that we have 3 main clusters with the majority of vehicles in those.

Cars:

  • Cluster 1: with almost high mpg, and low in horsepower.

  • Cluster 2: with good mpg and horsepower, but highest price

  • Cluster 3: with good mpg, Aveage horsepower, avearge Price

  • Cluster 4: with low mpg, horse low and lowest price

Trucks:

  • Cluster 1: with almost highest mpg among trucks, and lowest in horsepower and price.

  • Cluster 2: with almost low mpg and medium horsepower, but average price

  • Cluster 3: with good mpg and horsepower, low price.

Please notice that we did not use type , and price of cars in the clustering process, but Hierarchical clustering could forge the clusters and discriminate them with quite high accuracy.

plt.figure(figsize=(16,10)) for color, label in zip(colors, cluster_labels): subset = agg_cars.loc[(label,),] for i in subset.index: plt.text(subset.loc[i][0]+5, subset.loc[i][2], 'type='+str(int(i)) + ', price='+str(int(subset.loc[i][3]))+'k') plt.scatter(subset.horsepow, subset.mpg, s=subset.price*20, c=color, label='cluster'+str(label)) plt.legend() plt.title('Clusters') plt.xlabel('horsepow') plt.ylabel('mpg')
*c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points. *c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points. *c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points. *c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points. *c* argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with *x* & *y*. Please use the *color* keyword-argument or provide a 2D array with a single row if you intend to specify the same RGB or RGBA value for all points.
Text(0, 0.5, 'mpg')
Image in a Jupyter notebook