Diego Alzate

🤖 Proyectos de IA

  • Natural Language Processing
  • Computer Vision
  • Deep Learning
  • Supervised Learning
  • Unsupervised Learning
  • Ensemble Techniques

📍 Navegación

  • Inicio
  • Proyectos

Problem Statement¶

Context¶

AllLife Bank wants to focus on its credit card customer base in the next financial year. They have been advised by their marketing research team, that the penetration in the market can be improved. Based on this input, the Marketing team proposes to run personalized campaigns to target new customers as well as upsell to existing customers. Another insight from the market research was that the customers perceive the support services of the back poorly. Based on this, the Operations team wants to upgrade the service delivery model, to ensure that customer queries are resolved faster. Head of Marketing and Head of Delivery both decide to reach out to the Data Science team for help

Objective¶

To identify different segments in the existing customer, based on their spending patterns as well as past interaction with the bank, using clustering algorithms, and provide recommendations to the bank on how to better market to and service these customers.

Data Description¶

The data provided is of various customers of a bank and their financial attributes like credit limit, the total number of credit cards the customer has, and different channels through which customers have contacted the bank for any queries (including visiting the bank, online and through a call center).

Data Dictionary

  • Sl_No: Primary key of the records
  • Customer Key: Customer identification number
  • Average Credit Limit: Average credit limit of each customer for all credit cards
  • Total credit cards: Total number of credit cards possessed by the customer
  • Total visits bank: Total number of Visits that customer made (yearly) personally to the bank
  • Total visits online: Total number of visits or online logins made by the customer (yearly)
  • Total calls made: Total number of calls made by the customer to the bank or its customer service department (yearly)

Importing necessary libraries and data¶

In [ ]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# to scale the data using z-score
from sklearn.preprocessing import StandardScaler
# to perform k-means clustering and compute silhouette scores
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
# to compute distances
from scipy.spatial.distance import cdist
# to perform hierarchical clustering, compute cophenetic correlation, and create dendrograms
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage, cophenet
# to compute distances
from scipy.spatial.distance import pdist
# to visualize the elbow curve and silhouette scores
from yellowbrick.cluster import KElbowVisualizer, SilhouetteVisualizer

Data Overview¶

  • Observations
  • Sanity checks
In [ ]:
data = pd.read_excel('Credit+Card+Customer+Data.xlsx')
df = data.copy()
df.head()
Out[ ]:
Sl_No Customer Key Avg_Credit_Limit Total_Credit_Cards Total_visits_bank Total_visits_online Total_calls_made
0 1 87073 100000 2 1 1 0
1 2 38414 50000 3 0 10 9
2 3 17341 50000 7 1 3 4
3 4 40496 30000 5 1 1 4
4 5 47437 100000 6 0 12 3

Observations¶

Sl_No and Customer Key columns are not required

In [ ]:
# Dropping Sl_No and Customer Key columns
df.drop("Sl_No", axis=1, inplace=True)
df.drop("Customer Key", axis=1, inplace=True)
In [ ]:
# Number of Columns and rows
df.shape
Out[ ]:
(660, 5)
In [ ]:
# Summary of the data
df.describe().T
Out[ ]:
count mean std min 25% 50% 75% max
Avg_Credit_Limit 660.0 34574.242424 37625.487804 3000.0 10000.0 18000.0 48000.0 200000.0
Total_Credit_Cards 660.0 4.706061 2.167835 1.0 3.0 5.0 6.0 10.0
Total_visits_bank 660.0 2.403030 1.631813 0.0 1.0 2.0 4.0 5.0
Total_visits_online 660.0 2.606061 2.935724 0.0 1.0 2.0 4.0 15.0
Total_calls_made 660.0 3.583333 2.865317 0.0 1.0 3.0 5.0 10.0
In [ ]:
# Structure of the data
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 660 entries, 0 to 659
Data columns (total 5 columns):
 #   Column               Non-Null Count  Dtype
---  ------               --------------  -----
 0   Avg_Credit_Limit     660 non-null    int64
 1   Total_Credit_Cards   660 non-null    int64
 2   Total_visits_bank    660 non-null    int64
 3   Total_visits_online  660 non-null    int64
 4   Total_calls_made     660 non-null    int64
dtypes: int64(5)
memory usage: 25.9 KB
In [ ]:
# Unique values in each column
data.nunique()
Out[ ]:
Sl_No                  660
Customer Key           655
Avg_Credit_Limit       110
Total_Credit_Cards      10
Total_visits_bank        6
Total_visits_online     16
Total_calls_made        11
dtype: int64

Exploratory Data Analysis (EDA)¶

  • EDA is an important part of any project involving data.
  • It is important to investigate and understand the data better before building a model with it.
  • A few questions have been mentioned below which will help you approach the analysis in the right manner and generate insights from the data.
  • A thorough analysis of the data, in addition to the questions mentioned below, should be done.

Questions¶

In [ ]:
# function to plot a boxplot and a histogram along the same scale.


def histogram_boxplot(data, feature, figsize=(12, 7), kde=False, bins=None):
    """
    Boxplot and histogram combined

    data: dataframe
    feature: dataframe column
    figsize: size of figure (default (12,7))
    kde: whether to the show density curve (default False)
    bins: number of bins for histogram (default None)
    """
    f2, (ax_box2, ax_hist2) = plt.subplots(
        nrows=2,  # Number of rows of the subplot grid= 2
        sharex=True,  # x-axis will be shared among all subplots
        gridspec_kw={"height_ratios": (0.25, 0.75)},
        figsize=figsize,
    )  # creating the 2 subplots
    sns.boxplot(
        data=data, x=feature, ax=ax_box2, showmeans=True, color="violet"
    )  # boxplot will be created and a triangle will indicate the mean value of the column
    sns.histplot(
        data=data, x=feature, kde=kde, ax=ax_hist2, bins=bins, palette="winter"
    ) if bins else sns.histplot(
        data=data, x=feature, kde=kde, ax=ax_hist2
    )  # For histogram
    ax_hist2.axvline(
        data[feature].mean(), color="green", linestyle="--"
    )  # Add mean to the histogram
    ax_hist2.axvline(
        data[feature].median(), color="black", linestyle="-"
    )  # Add median to the histogram
In [ ]:
# function to create labeled barplots


def labeled_barplot(data, feature, perc=False, n=None):
    """
    Barplot with percentage at the top

    data: dataframe
    feature: dataframe column
    perc: whether to display percentages instead of count (default is False)
    n: displays the top n category levels (default is None, i.e., display all levels)
    """

    total = len(data[feature])  # length of the column
    count = data[feature].nunique()
    if n is None:
        plt.figure(figsize=(count + 1, 5))
    else:
        plt.figure(figsize=(n + 1, 5))

    plt.xticks(rotation=90, fontsize=15)
    ax = sns.countplot(
        data=data,
        x=feature,
        palette="Paired",
        order=data[feature].value_counts().index[:n].sort_values(),
    )

    for p in ax.patches:
        if perc == True:
            label = "{:.1f}%".format(
                100 * p.get_height() / total
            )  # percentage of each class of the category
        else:
            label = p.get_height()  # count of each level of the category

        x = p.get_x() + p.get_width() / 2  # width of the plot
        y = p.get_height()  # height of the plot

        ax.annotate(
            label,
            (x, y),
            ha="center",
            va="center",
            size=12,
            xytext=(0, 5),
            textcoords="offset points",
        )  # annotate the percentage

    plt.show()  # show the plot
  1. How is the distrubution of average credit limit of customer?
In [ ]:
# histogram_box plot
histogram_boxplot(df, "Avg_Credit_Limit")
In [ ]:
df["Avg_Credit_Limit"].mean()
Out[ ]:
31981.51001540832

Answer:¶

The data distribution is right skewed with a mean value of 31981.51

  1. What percenatage of the customers have never made a service-related call to the bank?
In [ ]:
 labeled_barplot(df, "Total_calls_made", perc=True)

Answer:¶

Percentage of customer = 14.7%

  1. What percentage of customers have never logged in to the online portal?
In [ ]:
 labeled_barplot(df, "Total_visits_online", perc=True)

Answer:¶

Total = 21.8%

  1. How many customers have never visited bank?
In [ ]:
 labeled_barplot(df, "Total_visits_bank", perc=True)

Answer¶

Total_Visits_0=100

  1. How are the different variables correlated with each other?
In [ ]:
plt.figure(figsize=(15, 7))
sns.heatmap(df.corr(), annot=True, vmin=-
            1, vmax=1, fmt=".2f", cmap="Spectral")
plt.show()

Answer:¶

The highest positive correlations are:

  • Avg_Credit_Limit vs. Total_Credit_Cards: 0.621365
  • Avg_Credit_Limit vs. Total_visits_online: 0.340154
  • Total_Credit_Cards vs. Total_visits_bank: 0.311524

The highest negative correlations are:

  • Total_Credit_Cards vs. Total_calls_made: -0.653325
  • Total_visits_bank vs. Total_calls_made: -0.504256
  • Avg_Credit_Limit vs. Total_calls_made: -0.457166

Univariate analysis¶

In [ ]:
# histogram_box plot for all the numerical values
for column in df.columns:
    histogram_boxplot(df, column)

Bivariate Analysis¶

In [ ]:
sns.pairplot(data=df, diag_kind="kde")
Out[ ]:
<seaborn.axisgrid.PairGrid at 0x7f4b89b850d0>
In [ ]:
sns.pairplot(df, hue="Total_Credit_Cards",)
Out[ ]:
<seaborn.axisgrid.PairGrid at 0x7f4b89e16d50>

Data Preprocessing¶

  • Duplicate value check
In [ ]:
# Checking for duplicate observations
df.duplicated().sum()
Out[ ]:
11

Observation¶

There are 11 duplicate observations. We will remove them from the data.

In [ ]:
#Removing duplicated data
df = df[(~df.duplicated())].copy()
df.shape
Out[ ]:
(649, 5)
In [ ]:
# Checking for duplicate observations
df.duplicated().sum()
Out[ ]:
0

Observations¶

Now there is no duplicated values

  • Missing value treatment
In [ ]:
# Checking missing values
df.isnull().sum()
Out[ ]:
Avg_Credit_Limit       0
Total_Credit_Cards     0
Total_visits_bank      0
Total_visits_online    0
Total_calls_made       0
dtype: int64
In [ ]:
# Checking missing values
df.isna().sum()
Out[ ]:
Avg_Credit_Limit       0
Total_Credit_Cards     0
Total_visits_bank      0
Total_visits_online    0
Total_calls_made       0
dtype: int64

Observations¶

There is no missing values

  • Outlier check
In [ ]:
threshold = 3
outlier = {}
for col in df.columns:
    i = df[col]
    mean = np.mean(df[col])
    std = np.std(df[col])
    list1 = []
    for v in i:
        z = (v - mean) / std
        if z > threshold:
            list1.append(v)
    list1.sort()
    outlier[i.name] = list1

print("The following are the outliers in the data:")
for key, value in outlier.items():
    print("\n", key, ":", value)
The following are the outliers in the data:

 Avg_Credit_Limit : [153000, 155000, 156000, 156000, 157000, 158000, 163000, 163000, 166000, 166000, 167000, 171000, 172000, 172000, 173000, 176000, 178000, 183000, 184000, 186000, 187000, 195000, 195000, 200000]

 Total_Credit_Cards : []

 Total_visits_bank : []

 Total_visits_online : [12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15]

 Total_calls_made : []
  • Outlier Treatment
In [ ]:
# functions to treat outliers by flooring and capping


def treat_outliers(df, col):
    """
    Treats outliers in a variable

    df: dataframe
    col: dataframe column
    """
    Q1 = df[col].quantile(0.25)  # 25th quantile
    Q3 = df[col].quantile(0.75)  # 75th quantile
    IQR = Q3 - Q1
    Lower_Whisker = Q1 - 1.5 * IQR
    Upper_Whisker = Q3 + 1.5 * IQR

    # all the values smaller than Lower_Whisker will be assigned the value of Lower_Whisker
    # all the values greater than Upper_Whisker will be assigned the value of Upper_Whisker
    df[col] = np.clip(df[col], Lower_Whisker, Upper_Whisker)

    return df


def treat_outliers_all(df, col_list):
    """
    Treat outliers in a list of variables

    df: dataframe
    col_list: list of dataframe columns
    """
    for c in col_list:
        df = treat_outliers(df, c)

    return df

New data frame without outliers¶

In [ ]:
new_df = treat_outliers_all(df, df.columns)
  • Any other preprocessing steps (if needed)
In [ ]:
# scaling the dataset before clustering
scaler = StandardScaler()
subset = new_df.copy()
subset_scaled = scaler.fit_transform(subset)
# creating a dataframe of the scaled columns
subset_scaled_df = pd.DataFrame(subset_scaled, columns=subset.columns)
subset_scaled_df.head()
Out[ ]:
Avg_Credit_Limit Total_Credit_Cards Total_visits_bank Total_visits_online Total_calls_made
0 2.366936 -1.247087 -0.860606 -0.623244 -1.248443
1 0.627015 -0.786701 -1.476410 2.686783 1.881237
2 0.627015 1.054846 -0.860606 0.259430 0.142526
3 -0.068953 0.134072 -0.860606 -0.623244 0.142526
4 2.366936 0.594459 -1.476410 2.686783 -0.205216

EDA¶

  • It is a good idea to explore the data once again after manipulating it.
In [ ]:
# histogram_box plot for all the numerical values
for column in subset_scaled_df.columns:
    histogram_boxplot(subset_scaled_df, column)
In [ ]:
sns.pairplot(data=subset_scaled_df, diag_kind="kde")
Out[ ]:
<seaborn.axisgrid.PairGrid at 0x7f4b84a6d210>
In [ ]:
plt.figure(figsize=(15, 7))
sns.heatmap(subset_scaled_df.corr(), annot=True, vmin=-
            1, vmax=1, fmt=".2f", cmap="Spectral")
plt.show()

K-means Clustering¶

In [ ]:
# Finding optimal no. of clusters
k_means_df = subset_scaled_df.copy()

clusters = range(1, 10)
meanDistortions = []

for k in clusters:
    model = KMeans(n_clusters=k)
    model.fit(k_means_df)
    prediction = model.predict(k_means_df)
    distortion = (
        sum(
            np.min(cdist(k_means_df,
                   model.cluster_centers_, "euclidean"), axis=1)
        )
        / k_means_df.shape[0]
    )

    meanDistortions.append(distortion)

    print("Number of Clusters:", k, "\tAverage Distortion:", distortion)

plt.plot(clusters, meanDistortions, "bx-")
plt.xlabel("k")
plt.ylabel("Average distortion")
plt.title("Selecting k with the Elbow Method")
plt.show()
Number of Clusters: 1 	Average Distortion: 2.073136602035325
Number of Clusters: 2 	Average Distortion: 1.4978375339862937
Number of Clusters: 3 	Average Distortion: 1.1772224293618558
Number of Clusters: 4 	Average Distortion: 1.0689466335012168
Number of Clusters: 5 	Average Distortion: 1.0016555965826015
Number of Clusters: 6 	Average Distortion: 0.9493370148679439
Number of Clusters: 7 	Average Distortion: 0.9120600596999384
Number of Clusters: 8 	Average Distortion: 0.8885545708522814
Number of Clusters: 9 	Average Distortion: 0.8656344324222063

Observations¶

Appropriate value for k seems to be 3 or 4.

Silhouette score¶

In [ ]:
sil_score = []
cluster_list = list(range(2, 10))
for n_clusters in cluster_list:
    clusterer = KMeans(n_clusters=n_clusters)
    preds = clusterer.fit_predict((k_means_df))
    score = silhouette_score(k_means_df, preds)
    sil_score.append(score)
    print("For n_clusters = {}, the silhouette score is {})".format(
        n_clusters, score))

plt.plot(cluster_list, sil_score)
plt.show()
For n_clusters = 2, the silhouette score is 0.41999597804777794)
For n_clusters = 3, the silhouette score is 0.5180797381243987)
For n_clusters = 4, the silhouette score is 0.37986496617829596)
For n_clusters = 5, the silhouette score is 0.3547325270386784)
For n_clusters = 6, the silhouette score is 0.27229030080435984)
For n_clusters = 7, the silhouette score is 0.26726998767685833)
For n_clusters = 8, the silhouette score is 0.2574106133109355)
For n_clusters = 9, the silhouette score is 0.24344136490539667)

Observations¶

Silhouette score for 3 is higher than that for 2. So, we will choose 3 as value of k.

In [ ]:
# finding optimal no. of clusters with silhouette coefficients
visualizer = SilhouetteVisualizer(KMeans(2, random_state=1))
visualizer.fit(k_means_df)
visualizer.show()
Out[ ]:
<Axes: title={'center': 'Silhouette Plot of KMeans Clustering for 649 Samples in 2 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
In [ ]:
# finding optimal no. of clusters with silhouette coefficients
visualizer = SilhouetteVisualizer(KMeans(3, random_state=1))
visualizer.fit(k_means_df)
visualizer.show()
Out[ ]:
<Axes: title={'center': 'Silhouette Plot of KMeans Clustering for 649 Samples in 3 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
In [ ]:
# finding optimal no. of clusters with silhouette coefficients
visualizer = SilhouetteVisualizer(KMeans(4, random_state=1))
visualizer.fit(k_means_df)
visualizer.show()
Out[ ]:
<Axes: title={'center': 'Silhouette Plot of KMeans Clustering for 649 Samples in 4 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>

Observations¶

take 3 as number of clusters

Final model¶

In [ ]:
kmeans = KMeans(n_clusters=3, random_state=0)
kmeans.fit(k_means_df)

# creating a copy of the original data
df1 = df.copy()

# adding kmeans cluster labels to the original and scaled dataframes
k_means_df["K_means_segments"] = kmeans.labels_
df1["K_means_segments"] = kmeans.labels_
In [ ]:
k_means_df.head()
Out[ ]:
Avg_Credit_Limit Total_Credit_Cards Total_visits_bank Total_visits_online Total_calls_made K_means_segments
0 2.366936 -1.247087 -0.860606 -0.623244 -1.248443 2
1 0.627015 -0.786701 -1.476410 2.686783 1.881237 1
2 0.627015 1.054846 -0.860606 0.259430 0.142526 2
3 -0.068953 0.134072 -0.860606 -0.623244 0.142526 2
4 2.366936 0.594459 -1.476410 2.686783 -0.205216 0
In [ ]:
df1.head()
Out[ ]:
Avg_Credit_Limit Total_Credit_Cards Total_visits_bank Total_visits_online Total_calls_made K_means_segments
0 100000 2 1 1.0 0 2
1 50000 3 0 8.5 9 1
2 50000 7 1 3.0 4 2
3 30000 5 1 1.0 4 2
4 100000 6 0 8.5 3 0

Hierarchical Clustering¶

In [ ]:
hc_df = subset_scaled_df.copy()

# list of distance metrics
distance_metrics = ["euclidean", "chebyshev", "mahalanobis", "cityblock"]

# list of linkage methods
linkage_methods = ["single", "complete", "average", "weighted"]

high_cophenet_corr = 0
high_dm_lm = [0, 0]

for dm in distance_metrics:
    for lm in linkage_methods:
        Z = linkage(hc_df, metric=dm, method=lm)
        c, coph_dists = cophenet(Z, pdist(hc_df))
        print(
            "Cophenetic correlation for {} distance and {} linkage is {}.".format(
                dm.capitalize(), lm, c
            )
        )
        if high_cophenet_corr < c:
            high_cophenet_corr = c
            high_dm_lm[0] = dm
            high_dm_lm[1] = lm
Cophenetic correlation for Euclidean distance and single linkage is 0.6687980551717484.
Cophenetic correlation for Euclidean distance and complete linkage is 0.8455463448314544.
Cophenetic correlation for Euclidean distance and average linkage is 0.8922750152191901.
Cophenetic correlation for Euclidean distance and weighted linkage is 0.8874605817813397.
Cophenetic correlation for Chebyshev distance and single linkage is 0.6904171632443976.
Cophenetic correlation for Chebyshev distance and complete linkage is 0.8256908105714492.
Cophenetic correlation for Chebyshev distance and average linkage is 0.8890063310233792.
Cophenetic correlation for Chebyshev distance and weighted linkage is 0.8820500156009561.
Cophenetic correlation for Mahalanobis distance and single linkage is 0.659249865882007.
Cophenetic correlation for Mahalanobis distance and complete linkage is 0.4243540966152363.
Cophenetic correlation for Mahalanobis distance and average linkage is 0.7988622717266841.
Cophenetic correlation for Mahalanobis distance and weighted linkage is 0.6423539449312025.
Cophenetic correlation for Cityblock distance and single linkage is 0.6718341146221024.
Cophenetic correlation for Cityblock distance and complete linkage is 0.8328113244932631.
Cophenetic correlation for Cityblock distance and average linkage is 0.8914496698322413.
Cophenetic correlation for Cityblock distance and weighted linkage is 0.8559905391810056.
In [ ]:
# printing the combination of distance metric and linkage method with the highest cophenetic correlation
print(
    "Highest cophenetic correlation is {}, which is obtained with {} distance and {} linkage.".format(
        high_cophenet_corr, high_dm_lm[0].capitalize(), high_dm_lm[1]
    )
)
Highest cophenetic correlation is 0.8922750152191901, which is obtained with Euclidean distance and average linkage.

Exploring different linkage methods with Euclidean distance only

In [ ]:
# list of linkage methods
linkage_methods = ["single", "complete",
                   "average", "centroid", "ward", "weighted"]

high_cophenet_corr = 0
high_dm_lm = [0, 0]

for lm in linkage_methods:
    Z = linkage(hc_df, metric="euclidean", method=lm)
    c, coph_dists = cophenet(Z, pdist(hc_df))
    print("Cophenetic correlation for {} linkage is {}.".format(lm, c))
    if high_cophenet_corr < c:
        high_cophenet_corr = c
        high_dm_lm[0] = "euclidean"
        high_dm_lm[1] = lm
Cophenetic correlation for single linkage is 0.6687980551717484.
Cophenetic correlation for complete linkage is 0.8455463448314544.
Cophenetic correlation for average linkage is 0.8922750152191901.
Cophenetic correlation for centroid linkage is 0.8882199967774133.
Cophenetic correlation for ward linkage is 0.7714411007322578.
Cophenetic correlation for weighted linkage is 0.8874605817813397.
In [ ]:
# printing the combination of distance metric and linkage method with the highest cophenetic correlation
print(
    "Highest cophenetic correlation is {}, which is obtained with {} linkage.".format(
        high_cophenet_corr, high_dm_lm[1]
    )
)
Highest cophenetic correlation is 0.8922750152191901, which is obtained with average linkage.

Observation¶

The cophenetic correlation is maximum with Euclidean distance and average linkage.

Dendrograms for the different linkage methods¶

In [ ]:
# list of linkage methods
linkage_methods = [
    "average", "centroid", "ward", "weighted"]

# lists to save results of cophenetic correlation calculation
compare_cols = ["Linkage", "Cophenetic Coefficient"]
compare = []

# to create a subplot image
fig, axs = plt.subplots(len(linkage_methods), 1, figsize=(15, 30))

# We will enumerate through the list of linkage methods above
# For each linkage method, we will plot the dendrogram and calculate the cophenetic correlation
for i, method in enumerate(linkage_methods):
    Z = linkage(hc_df, metric="euclidean", method=method)

    dendrogram(Z, ax=axs[i])
    axs[i].set_title(f"Dendrogram ({method.capitalize()} Linkage)")

    coph_corr, coph_dist = cophenet(Z, pdist(hc_df))
    axs[i].annotate(
        f"Cophenetic\nCorrelation\n{coph_corr:0.3f}",
        (0.80, 0.80),
        xycoords="axes fraction",
    )

    compare.append([method, coph_corr])
In [ ]:
# create and print a dataframe to compare cophenetic correlations for different linkage methods
df_cc = pd.DataFrame(compare, columns=compare_cols)
df_cc = df_cc.sort_values(by="Cophenetic Coefficient")
df_cc
Out[ ]:
Linkage Cophenetic Coefficient
2 ward 0.771441
3 weighted 0.887461
1 centroid 0.888220
0 average 0.892275

Observations¶

  • The cophenetic correlation is highest for Euclidean distance and average linkage.
  • We will move ahead with average linkage.
  • 5 appears to be the appropriate number of clusters from the dendrogram for average linkage.

Dendrogram for average linkage¶

In [ ]:
# list of linkage methods
linkage_methods = ["average"]

# lists to save results of cophenetic correlation calculation
compare_cols = ["Linkage", "Cophenetic Coefficient"]

# to create a subplot image
fig, axs = plt.subplots(len(linkage_methods), 1, figsize=(16, 16))

# We will enumerate through the list of linkage methods above
# For each linkage method, we will plot the dendrogram and calculate the cophenetic correlation
for i, method in enumerate(linkage_methods):
    Z = linkage(hc_df, metric="euclidean", method=method)

    dendrogram(Z, ax=axs)
    axs.set_title(f"Dendrogram ({method.capitalize()} Linkage)")

    coph_corr, coph_dist = cophenet(Z, pdist(hc_df))
    axs.annotate(
        f"Cophenetic\nCorrelation\n{coph_corr:0.3f}",
        (0.80, 0.80),
        xycoords="axes fraction",
    )
dendrogram(Z, color_threshold=7.3)
plt.axhline(y=2.6, c="red", lw=1, linestyle="dashdot")
Out[ ]:
<matplotlib.lines.Line2D at 0x7f4b6e01f490>

Final model¶

In [ ]:
# Complete the code to define the hierarchical clustering model
HCmodel = AgglomerativeClustering(
    n_clusters=5, affinity='euclidean', linkage='average')
HCmodel.fit(hc_df)
Out[ ]:
AgglomerativeClustering(affinity='euclidean', linkage='average', n_clusters=5)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
AgglomerativeClustering(affinity='euclidean', linkage='average', n_clusters=5)
In [ ]:
# creating a copy of the original data
df2 = df.copy()

# adding hierarchical cluster labels to the original and scaled dataframes
hc_df["HC_segments"] = HCmodel.labels_
df2["HC_segments"] = HCmodel.labels_
In [ ]:
hc_df.head()
Out[ ]:
Avg_Credit_Limit Total_Credit_Cards Total_visits_bank Total_visits_online Total_calls_made HC_segments
0 2.366936 -1.247087 -0.860606 -0.623244 -1.248443 2
1 0.627015 -0.786701 -1.476410 2.686783 1.881237 4
2 0.627015 1.054846 -0.860606 0.259430 0.142526 1
3 -0.068953 0.134072 -0.860606 -0.623244 0.142526 1
4 2.366936 0.594459 -1.476410 2.686783 -0.205216 3
In [ ]:
df2.head()
Out[ ]:
Avg_Credit_Limit Total_Credit_Cards Total_visits_bank Total_visits_online Total_calls_made HC_segments
0 100000 2 1 1.0 0 2
1 50000 3 0 8.5 9 4
2 50000 7 1 3.0 4 1
3 30000 5 1 1.0 4 1
4 100000 6 0 8.5 3 3

Cluster Profiling and Comparison¶

Cluster Profiling: K-means Clustering¶

In [ ]:
k_cluster_profile = df1.groupby("K_means_segments").mean()
k_cluster_profile["count_in_each_segments"] = (
    df1.groupby("K_means_segments")["Avg_Credit_Limit"].count().values
)
In [ ]:
k_cluster_profile.groupby("K_means_segments").mean().plot.bar(figsize=(15, 6))
Out[ ]:
<Axes: xlabel='K_means_segments'>
In [ ]:
fig, axes = plt.subplots(1, 5, figsize=(16, 6))
fig.suptitle(
    "Boxplot of numerical variables for each cluster obtained using K-means Clustering",
    fontsize=20,
)

counter = 0

for ii in range(5):
    sns.boxplot(
        ax=axes[ii], y=df1[df1.columns[counter]
                           ], x=k_means_df["K_means_segments"]
    )
    counter = counter + 1

fig.tight_layout(pad=2.0)

Cluster Profiling: Hierarchical Clustering¶

In [ ]:
hc_cluster_profile = df2.groupby("HC_segments").mean()
hc_cluster_profile["count_in_each_segment"] = (
    df2.groupby("HC_segments")["Avg_Credit_Limit"].count().values
)
In [ ]:
hc_cluster_profile.groupby("HC_segments").mean().plot.bar(figsize=(15, 6))
Out[ ]:
<Axes: xlabel='HC_segments'>
In [ ]:
fig, axes = plt.subplots(1, 5, figsize=(16, 6))
fig.suptitle(
    "Boxplot of numerical variables for each cluster obtained using Hierarchical Clustering",
    fontsize=20,
)

counter = 0

for ii in range(5):
    sns.boxplot(
        ax=axes[ii], y=df2[df2.columns[counter]
                           ], x=hc_df["HC_segments"]
    )
    counter = counter + 1

fig.tight_layout(pad=2.0)

K-means vs Hierarchical Clustering¶

You compare several things, like:

  • Which clustering technique took less time for execution?
  • Which clustering technique gave you more distinct clusters, or are they the same?
  • How many observations are there in the similar clusters of both algorithms?
  • How many clusters are obtained as the appropriate number of clusters from both algorithms?

You can also mention any differences or similarities you obtained in the cluster profiles from both the clustering techniques.

In [ ]:
k_cluster_profile.style.highlight_max(color="lightgreen", axis=0)
Out[ ]:
  Avg_Credit_Limit Total_Credit_Cards Total_visits_bank Total_visits_online Total_calls_made count_in_each_segments
K_means_segments            
0 103440.000000 8.740000 0.600000 8.180000 1.080000 50
1 12239.819005 2.411765 0.945701 3.554299 6.891403 221
2 34071.428571 5.518519 3.484127 0.981481 1.992063 378

Observations¶

Cluster 0:

  • Average Credit Limit: $103,440
  • Total Credit Cards: 8.74.
  • Total Visits to the Bank: 0.6
  • Total Visits Online: 8.18
  • Total Calls Made: 1.08
  • Count in this Cluster: 50

Cluster 1:

  • Average Credit Limit: $12,239.82
  • Total Credit Cards: 2.41
  • Total Visits to the Bank: 0.95
  • Total Visits Online: 3.55
  • Total Calls Made: 6.89
  • Count in this Cluster: 221

Cluster 2:

  • Average Credit Limit: $34,071.43
  • Total Credit Cards: 5.52
  • Total Visits to the Bank: 3.48
  • Total Visits Online: 0.98
  • Total Calls Made: 1.99
  • Count in this Cluster: 378

Cluster 0 represents customers with relatively high credit limits, a small number of credit cards, minimal visits to the bank, a significant number of online visits, and few calls made. These customers seem to prefer online interactions and have a low frequency of calls.

Cluster 1 represents customers with the lowest average credit limit, a small number of credit cards, regular visits to the bank, moderate online visits, and the highest number of calls made. These customers are likely less affluent, rely on bank visits, and have a higher need for phone support.

Cluster 2 represents customers with medium to high credit limits, a moderate number of credit cards, frequent visits to the bank, low online visits, and low calls made. These customers prefer visiting the bank and have low online interaction and call frequency.

In [ ]:
hc_cluster_profile.style.highlight_max(color="lightgreen", axis=0)
Out[ ]:
  Avg_Credit_Limit Total_Credit_Cards Total_visits_bank Total_visits_online Total_calls_made count_in_each_segment
HC_segments            
0 12091.324201 2.401826 0.945205 3.538813 6.894977 219
1 33825.396825 5.523810 3.486772 0.984127 2.002646 378
2 100000.000000 2.000000 1.000000 1.000000 0.000000 1
3 103440.000000 8.740000 0.600000 8.180000 1.080000 50
4 50000.000000 3.000000 0.000000 8.500000 9.000000 1

Observations¶

Cluster 0:

  • Average Credit Limit: $12,091.32
  • Total Credit Cards: 2.40
  • Total Visits to the Bank: 0.95
  • Total Visits Online: 3.54
  • Total Calls Made: 6.89
  • Count in this Cluster: 219

Cluster 1:

  • Average Credit Limit: $33,825.40
  • Total Credit Cards: 5.52
  • Total Visits to the Bank: 3.49
  • Total Visits Online: 0.98
  • Total Calls Made: 2.00
  • Count in this Cluster: 378

Cluster 2:

  • Average Credit Limit: $100,000.00
  • Total Credit Cards: 2.00
  • Total Visits to the Bank: 1.00
  • Total Visits Online: 1.00
  • Total Calls Made: 0.00
  • Count in this Cluster: 1

Cluster 3:

  • Average Credit Limit: $103,440.00
  • Total Credit Cards: 8.74
  • Total Visits to the Bank: 0.60
  • Total Visits Online: 8.18
  • Total Calls Made: 1.08
  • Count in this Cluster: 50

Cluster 4:

  • Average Credit Limit: $50,000.00
  • Total Credit Cards: 3.00
  • Total Visits to the Bank: 0.00
  • Total Visits Online: 8.50
  • Total Calls Made: 9.00
  • Count in this Cluster: 1

Cluster 0 represents customers with relatively low credit limits, a small number of credit cards, moderate online visits, and a relatively high number of calls made. These customers have minimal in-person visits to the bank and seem to prefer phone interactions.

Cluster 1 represents customers with medium to high credit limits, a moderate number of credit cards, regular visits to the bank, and moderate online visits. These customers have a balanced interaction pattern and are moderately engaged across multiple channels.

Cluster 2 represents a unique customer with a high credit limit, a small number of credit cards, and limited interactions both online and via phone. This Cluster may represent a distinct group of customers with specific needs.

Cluster 3 represents customers with high credit limits, a significant number of credit cards, frequent online visits, and moderate calls made. These customers prefer online interactions but may occasionally contact the bank through phone calls.

Cluster 4 represents customers with a $50,000 credit limit, three credit cards, and no bank visits. They have a strong preference for online interactions and make the highest number of calls among all Clusters.

Actionable Insights and Recommendations¶

These results provide valuable insights into the different customer segments based on their behavior and credit-related characteristics. Businesses can now tailor their marketing strategies, product offerings, and customer service approaches to better serve the distinct needs of each segment.

K-means clustering as selected technique:

Cluster 0: The bank could target these customers with marketing campaigns that promote online banking and phone calls. The bank could also offer them special incentives, such as rewards points or discounts, for using these channels.

Cluster 1: The bank could target these customers with marketing campaigns that promote in-person banking. The bank could also offer them help and support with online banking and phone calls.

Cluster 2: The bank could target these customers with general marketing campaigns that promote all of your banking channels.