Unsupervised Learning: Trade & Ahead¶

Problem Statement¶

Context¶

The stock market has consistently proven to be a good place to invest in and save for the future. There are a lot of compelling reasons to invest in stocks. It can help in fighting inflation, create wealth, and also provides some tax benefits. Good steady returns on investments over a long period of time can also grow a lot more than seems possible. Also, thanks to the power of compound interest, the earlier one starts investing, the larger the corpus one can have for retirement. Overall, investing in stocks can help meet life's financial aspirations.

It is important to maintain a diversified portfolio when investing in stocks in order to maximise earnings under any market condition. Having a diversified portfolio tends to yield higher returns and face lower risk by tempering potential losses when the market is down. It is often easy to get lost in a sea of financial metrics to analyze while determining the worth of a stock, and doing the same for a multitude of stocks to identify the right picks for an individual can be a tedious task. By doing a cluster analysis, one can identify stocks that exhibit similar characteristics and ones which exhibit minimum correlation. This will help investors better analyze stocks across different market segments and help protect against risks that could make the portfolio vulnerable to losses.

Objective¶

Trade&Ahead is a financial consultancy firm who provide their customers with personalized investment strategies. They have hired you as a Data Scientist and provided you with data comprising stock price and some financial indicators for a few companies listed under the New York Stock Exchange. They have assigned you the tasks of analyzing the data, grouping the stocks based on the attributes provided, and sharing insights about the characteristics of each group.

Data Dictionary¶

  • Ticker Symbol: An abbreviation used to uniquely identify publicly traded shares of a particular stock on a particular stock market
  • Company: Name of the company
  • GICS Sector: The specific economic sector assigned to a company by the Global Industry Classification Standard (GICS) that best defines its business operations
  • GICS Sub Industry: The specific sub-industry group assigned to a company by the Global Industry Classification Standard (GICS) that best defines its business operations
  • Current Price: Current stock price in dollars
  • Price Change: Percentage change in the stock price in 13 weeks
  • Volatility: Standard deviation of the stock price over the past 13 weeks
  • ROE: A measure of financial performance calculated by dividing net income by shareholders' equity (shareholders' equity is equal to a company's assets minus its debt)
  • Cash Ratio: The ratio of a company's total reserves of cash and cash equivalents to its total current liabilities
  • Net Cash Flow: The difference between a company's cash inflows and outflows (in dollars)
  • Net Income: Revenues minus expenses, interest, and taxes (in dollars)
  • Earnings Per Share: Company's net profit divided by the number of common shares it has outstanding (in dollars)
  • Estimated Shares Outstanding: Company's stock currently held by all its shareholders
  • P/E Ratio: Ratio of the company's current stock price to the earnings per share
  • P/B Ratio: Ratio of the company's stock price per share by its book value per share (book value of a company is the net difference between that company's total assets and total liabilities)

Importing necessary libraries and data¶

In [1]:
# Installing the libraries with the specified version.
# uncomment and run the following line if Google Colab is being used
#!pip install scikit-learn==1.2.2 seaborn==0.13.1 matplotlib==3.7.1 numpy==1.25.2 pandas==1.5.3 yellowbrick==1.5 -q --user
In [2]:
# Installing the libraries with the specified version.
# uncomment and run the following lines if Jupyter Notebook is being used
#!pip install scikit-learn==1.2.2 seaborn==0.13.1 matplotlib==3.7.1 numpy==1.25.2 pandas==1.5.2 yellowbrick==1.5 -q --user
#!pip install --upgrade -q jinja2

Note: After running the above cell, kindly restart the notebook kernel and run all cells sequentially from the start again.

In [3]:
# Libraries to help with reading and manipulating data
import numpy as np
import pandas as pd

# Libraries to help with data visualization
import matplotlib.pyplot as plt
import seaborn as sns

# Removes the limit for the number of displayed columns
pd.set_option("display.max_columns", None)
# Sets the limit for the number of displayed rows
pd.set_option("display.max_rows", 200)

# to scale the data using z-score
from sklearn.preprocessing import StandardScaler

# to compute distances
from scipy.spatial.distance import cdist, pdist

# to perform k-means clustering and compute silhouette scores
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

# to visualize the elbow curve and silhouette scores
from yellowbrick.cluster import KElbowVisualizer, SilhouetteVisualizer

# to perform hierarchical clustering, compute cophenetic correlation, and create dendrograms
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage, cophenet
from sklearn.manifold import TSNE
from sklearn.cluster import KMeans


# To suppress scientific notations
pd.set_option("display.float_format", lambda x: "%.3f" % x)

# to suppress warnings
import warnings
warnings.filterwarnings("ignore")

sns.set_style("whitegrid")
sns.set_palette("icefire")
In [4]:
# uncomment and run the following lines for Google Colab
# from google.colab import drive
# drive.mount('/content/drive')
In [5]:
stock_data_path = 'stock_data.csv'

data = pd.read_csv(stock_data_path)

Data Overview¶

  • Observations
  • Sanity checks
In [6]:
print(f"There are {data.shape[0]} rows and {data.shape[1]} columns.")
There are 340 rows and 15 columns.
In [7]:
data.head()
Out[7]:
Ticker Symbol Security GICS Sector GICS Sub Industry Current Price Price Change Volatility ROE Cash Ratio Net Cash Flow Net Income Earnings Per Share Estimated Shares Outstanding P/E Ratio P/B Ratio
0 AAL American Airlines Group Industrials Airlines 42.350 10.000 1.687 135 51 -604000000 7610000000 11.390 668129938.500 3.718 -8.784
1 ABBV AbbVie Health Care Pharmaceuticals 59.240 8.339 2.198 130 77 51000000 5144000000 3.150 1633015873.000 18.806 -8.750
2 ABT Abbott Laboratories Health Care Health Care Equipment 44.910 11.301 1.274 21 67 938000000 4423000000 2.940 1504421769.000 15.276 -0.394
3 ADBE Adobe Systems Inc Information Technology Application Software 93.940 13.977 1.358 9 180 -240840000 629551000 1.260 499643650.800 74.556 4.200
4 ADI Analog Devices, Inc. Information Technology Semiconductors 55.320 -1.828 1.701 14 272 315120000 696878000 0.310 2247993548.000 178.452 1.060

Exploratory Data Analysis (EDA)¶

In [8]:
df = data.copy()

Data Preprocessing¶

  • Duplicate value check
  • Missing value treatment
  • Outlier check
  • Feature engineering (if needed)
  • Any other preprocessing steps (if needed)
In [9]:
df.sample(n=10, random_state=1)
Out[9]:
Ticker Symbol Security GICS Sector GICS Sub Industry Current Price Price Change Volatility ROE Cash Ratio Net Cash Flow Net Income Earnings Per Share Estimated Shares Outstanding P/E Ratio P/B Ratio
102 DVN Devon Energy Corp. Energy Oil & Gas Exploration & Production 32.000 -15.478 2.924 205 70 830000000 -14454000000 -35.550 406582278.500 93.089 1.786
125 FB Facebook Information Technology Internet Software & Services 104.660 16.224 1.321 8 958 592000000 3669000000 1.310 2800763359.000 79.893 5.884
11 AIV Apartment Investment & Mgmt Real Estate REITs 40.030 7.579 1.163 15 47 21818000 248710000 1.520 163625000.000 26.336 -1.269
248 PG Procter & Gamble Consumer Staples Personal Products 79.410 10.661 0.806 17 129 160383000 636056000 3.280 491391569.000 24.070 -2.257
238 OXY Occidental Petroleum Energy Oil & Gas Exploration & Production 67.610 0.865 1.590 32 64 -588000000 -7829000000 -10.230 765298142.700 93.089 3.345
336 YUM Yum! Brands Inc Consumer Discretionary Restaurants 52.516 -8.699 1.479 142 27 159000000 1293000000 2.970 435353535.400 17.682 -3.838
112 EQT EQT Corporation Energy Oil & Gas Exploration & Production 52.130 -21.254 2.365 2 201 523803000 85171000 0.560 152091071.400 93.089 9.568
147 HAL Halliburton Co. Energy Oil & Gas Equipment & Services 34.040 -5.102 1.966 4 189 7786000000 -671000000 -0.790 849367088.600 93.089 17.346
89 DFS Discover Financial Services Financials Consumer Finance 53.620 3.654 1.160 20 99 2288000000 2297000000 5.140 446887159.500 10.432 -0.376
173 IVZ Invesco Ltd. Financials Asset Management & Custody Banks 33.480 7.067 1.581 12 67 412000000 968100000 2.260 428362831.900 14.814 4.219
In [10]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 340 entries, 0 to 339
Data columns (total 15 columns):
 #   Column                        Non-Null Count  Dtype  
---  ------                        --------------  -----  
 0   Ticker Symbol                 340 non-null    object 
 1   Security                      340 non-null    object 
 2   GICS Sector                   340 non-null    object 
 3   GICS Sub Industry             340 non-null    object 
 4   Current Price                 340 non-null    float64
 5   Price Change                  340 non-null    float64
 6   Volatility                    340 non-null    float64
 7   ROE                           340 non-null    int64  
 8   Cash Ratio                    340 non-null    int64  
 9   Net Cash Flow                 340 non-null    int64  
 10  Net Income                    340 non-null    int64  
 11  Earnings Per Share            340 non-null    float64
 12  Estimated Shares Outstanding  340 non-null    float64
 13  P/E Ratio                     340 non-null    float64
 14  P/B Ratio                     340 non-null    float64
dtypes: float64(7), int64(4), object(4)
memory usage: 40.0+ KB
In [11]:
df.duplicated().sum()
Out[11]:
0
In [12]:
df.isna().sum()
Out[12]:
Ticker Symbol                   0
Security                        0
GICS Sector                     0
GICS Sub Industry               0
Current Price                   0
Price Change                    0
Volatility                      0
ROE                             0
Cash Ratio                      0
Net Cash Flow                   0
Net Income                      0
Earnings Per Share              0
Estimated Shares Outstanding    0
P/E Ratio                       0
P/B Ratio                       0
dtype: int64
In [13]:
df.describe().T
Out[13]:
count mean std min 25% 50% 75% max
Current Price 340.000 80.862 98.055 4.500 38.555 59.705 92.880 1274.950
Price Change 340.000 4.078 12.006 -47.130 -0.939 4.820 10.695 55.052
Volatility 340.000 1.526 0.592 0.733 1.135 1.386 1.696 4.580
ROE 340.000 39.597 96.548 1.000 9.750 15.000 27.000 917.000
Cash Ratio 340.000 70.024 90.421 0.000 18.000 47.000 99.000 958.000
Net Cash Flow 340.000 55537620.588 1946365312.176 -11208000000.000 -193906500.000 2098000.000 169810750.000 20764000000.000
Net Income 340.000 1494384602.941 3940150279.328 -23528000000.000 352301250.000 707336000.000 1899000000.000 24442000000.000
Earnings Per Share 340.000 2.777 6.588 -61.200 1.558 2.895 4.620 50.090
Estimated Shares Outstanding 340.000 577028337.754 845849595.418 27672156.860 158848216.100 309675137.800 573117457.325 6159292035.000
P/E Ratio 340.000 32.613 44.349 2.935 15.045 20.820 31.765 528.039
P/B Ratio 340.000 -1.718 13.967 -76.119 -4.352 -1.067 3.917 129.065
In [14]:
df.describe(include='object').T
Out[14]:
count unique top freq
Ticker Symbol 340 340 AAL 1
Security 340 340 American Airlines Group 1
GICS Sector 340 11 Industrials 53
GICS Sub Industry 340 104 Oil & Gas Exploration & Production 16

Questions:

  1. What does the distribution of stock prices look like?
  2. The stocks of which economic sector have seen the maximum price increase on average?
  3. How are the different variables correlated with each other?
  4. Cash ratio provides a measure of a company's ability to cover its short-term obligations using only cash and cash equivalents. How does the average cash ratio vary across economic sectors?
  5. P/E ratios can help determine the relative value of a company's shares as they signify the amount of money an investor is willing to invest in a single share of a company per dollar of its earnings. How does the P/E ratio vary, on average, across economic sectors?
In [15]:
def histogram_boxplot(df, feature, figsize=(12, 5), kde=True, 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=df, x=feature, ax=ax_box2, showmeans=True
    )  # boxplot will be created and a star will indicate the mean value of the column
    sns.histplot(
        data=df, x=feature, kde=kde, ax=ax_hist2, bins=bins
    ) if bins else sns.histplot(
        data=df, x=feature, kde=kde, ax=ax_hist2
    )  # For histogram
    ax_hist2.axvline(
        df[feature].mean(), color="green", linestyle="--"
    )  # Add mean to the histogram
    ax_hist2.axvline(
        df[feature].median(), color="black", linestyle="-"
    )  # Add median to the histogram
In [16]:
for column in df.columns:
    if df[column].dtype != 'object':
        histogram_boxplot(df, column)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Observations:¶

  • Current Price: Range 0 - 1300. Observations are highly skewed to the right with the majority of the observations being low-priced with a few extremely high priced observations.
  • Prices Change: Normally distributed between -50 to 60.
  • Volatility: Skewed right, most points on low end (0) with few outliers on the high end (5).
  • ROE: Range 0 - 800. Highly right skewed.
  • Cash Ratio: Range 0 - 1,000. Highly right skewed.
  • Net Cash Flow: Normally distributed between -1 and 2 with slight righ skew and some outliers on both ends.
  • Net Income: Normally distributed between -2 and 2 with some outliers on both ends.
  • Earnings Per Share: Range from -60 to 60. Normally distributed about center with most values between -5 and 10 and a peak near zero.
  • Estimated Shares Outstanding: Range from 0-7 billion. Highly right skewed with most values between 0 - 0.5 billion.
  • P/E Ratio: Range 0 - 600. Highly right skeweed with most avlue below 50.
  • P/B Ratio: Range from -100 to 150. Normally distributed around the mean of zero.
In [17]:
# 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)
    value_counts = data[feature].value_counts(normalize=perc)[:n].sort_values(ascending=False)

    ax = sns.countplot(
        data=data,
        x=feature,
        order=value_counts.index,
    )

    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
In [18]:
labeled_barplot(df, 'GICS Sector', perc=True)
No description has been provided for this image
In [19]:
labeled_barplot(df, 'GICS Sub Industry', perc=True, n=15)
No description has been provided for this image
In [ ]:
 

Observations:¶

  • Industrials and Finacials are the top two secors making up 30%.
  • Oil & Gas Exploration & Production, REITs, and Industrial Conglomerates are the top 3 sub industries making up about 14%

Outlier Check¶

  • Let's plot the boxplots of all numerical columns to check for outliers.
In [20]:
plt.figure(figsize=(15, 12))

numeric_columns = df.select_dtypes(include=np.number).columns.tolist()

for i, variable in enumerate(numeric_columns):
    plt.subplot(3, 4, i + 1)
    plt.boxplot(df[variable], whis=1.5)
    plt.tight_layout()
    plt.title(variable)

plt.show()
No description has been provided for this image
In [21]:
plt.figure(figsize=(12, 8))
sns.heatmap(df[numeric_columns].corr(), annot=True, vmin=-1, vmax=1, fmt=".2f", cmap='crest_r')
plt.show()
No description has been provided for this image

Scaling¶

In [22]:
scaler = StandardScaler()
subset = df[numeric_columns]
subset_scaled = scaler.fit_transform(subset)
In [23]:
subset_scaled_df = pd.DataFrame(subset_scaled, columns=subset.columns)
subset_scaled_df.head()
Out[23]:
Current Price Price Change Volatility ROE Cash Ratio Net Cash Flow Net Income Earnings Per Share Estimated Shares Outstanding P/E Ratio P/B Ratio
0 -0.393 0.494 0.273 0.990 -0.211 -0.339 1.554 1.309 0.108 -0.652 -0.507
1 -0.221 0.355 1.137 0.938 0.077 -0.002 0.928 0.057 1.250 -0.312 -0.504
2 -0.367 0.602 -0.427 -0.193 -0.033 0.454 0.744 0.025 1.098 -0.392 0.095
3 0.134 0.826 -0.285 -0.317 1.218 -0.152 -0.220 -0.231 -0.092 0.947 0.424
4 -0.261 -0.493 0.296 -0.266 2.237 0.134 -0.203 -0.375 1.978 3.293 0.199
In [24]:
sns.pairplot(subset_scaled_df, height=2, aspect=2 , diag_kind='kde')
plt.show()
No description has been provided for this image

K-means Clustering¶

Checking Elbow Plot¶

In [25]:
k_means_df = subset_scaled_df.copy()
In [26]:
clusters = range(1, 10)
meanDistortions = []

for k in clusters:
    model = KMeans(n_clusters=k, n_init=30, random_state=1)
    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", fontsize=20)
plt.show()
Number of Clusters: 1 	Average Distortion: 2.5425069919221697
Number of Clusters: 2 	Average Distortion: 2.382318498894466
Number of Clusters: 3 	Average Distortion: 2.2692367155390745
Number of Clusters: 4 	Average Distortion: 2.175554082632614
Number of Clusters: 5 	Average Distortion: 2.1270176856392657
Number of Clusters: 6 	Average Distortion: 2.0537507716340393
Number of Clusters: 7 	Average Distortion: 2.0176443096256684
Number of Clusters: 8 	Average Distortion: 1.966634759121479
Number of Clusters: 9 	Average Distortion: 1.9283782062891306
No description has been provided for this image
In [27]:
model = KMeans(n_init=30, random_state=1)
visualizer = KElbowVisualizer(model, k=(2, 11), timings=True)
visualizer.fit(k_means_df)
visualizer.show()
No description has been provided for this image
Out[27]:
<Axes: title={'center': 'Distortion Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='distortion score'>
In [28]:
visualizer = KElbowVisualizer(model, k=(2, 11), metric="silhouette", timings=True)
visualizer.fit(k_means_df)
visualizer.show()
No description has been provided for this image
Out[28]:
<Axes: title={'center': 'Silhouette Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='silhouette score'>
In [29]:
visualizer = SilhouetteVisualizer(KMeans(n_clusters=2, n_init=30, random_state=1))
visualizer.fit(k_means_df)
visualizer.show()
No description has been provided for this image
Out[29]:
<Axes: title={'center': 'Silhouette Plot of KMeans Clustering for 340 Samples in 2 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
In [30]:
visualizer = SilhouetteVisualizer(KMeans(n_clusters=4, n_init=30, random_state=1))
visualizer.fit(k_means_df)
visualizer.show()
No description has been provided for this image
Out[30]:
<Axes: title={'center': 'Silhouette Plot of KMeans Clustering for 340 Samples in 4 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
In [31]:
visualizer = SilhouetteVisualizer(KMeans(n_clusters=6, n_init=30, random_state=1))
visualizer.fit(k_means_df)
visualizer.show()
No description has been provided for this image
Out[31]:
<Axes: title={'center': 'Silhouette Plot of KMeans Clustering for 340 Samples in 6 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
In [32]:
visualizer = SilhouetteVisualizer(KMeans(n_clusters=7, n_init=30, random_state=1))
visualizer.fit(k_means_df)
visualizer.show()
No description has been provided for this image
Out[32]:
<Axes: title={'center': 'Silhouette Plot of KMeans Clustering for 340 Samples in 7 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>

Observation¶

  • Increased n_init to 30 to get more consistent and stable results.
  • There distortion score has good balance of low distortion and tight clusters at 6 clusters.
  • The silhouette score is highest with best separation at 2 clusters.
  • The silhouette plots suggest that 2 clusters the best average score indicating good separation. k=4 or k=6 gests lower average scores and more observations iwth a negative silhouette score indicating poor clustering.
  • In conlusion k=2 is the best option.

Final Model¶

In [33]:
kmeans = KMeans(n_clusters=2, n_init=30, random_state=1)
kmeans.fit(k_means_df)
Out[33]:
KMeans(n_clusters=2, n_init=30, random_state=1)
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.
KMeans(n_clusters=2, n_init=30, random_state=1)
In [34]:
df1 = df.copy()

k_means_df["KM_segments"] = kmeans.labels_
df1["KM_segments"] = kmeans.labels_
In [35]:
km_cluster_profile = df1.groupby("KM_segments").mean(numeric_only=True)
In [36]:
km_cluster_profile["count_in_each_segment"] = (df1.groupby("KM_segments")["Security"].count().values)
In [37]:
km_cluster_profile.style.highlight_max(color='lightgreen',axis=0)
Out[37]:
  Current Price Price Change Volatility ROE Cash Ratio Net Cash Flow Net Income Earnings Per Share Estimated Shares Outstanding P/E Ratio P/B Ratio count_in_each_segment
KM_segments                        
0 82.786278 5.649217 1.391766 33.781759 70.159609 44922850.162866 1993143179.153095 3.896270 581977441.138534 24.244484 -2.080438 307
1 62.963940 -10.537087 2.774534 93.696970 68.757576 154287151.515152 -3145581545.454545 -7.639091 530986678.995152 110.461063 1.651207 33
In [38]:
tsne = TSNE(n_components=2, random_state=1, perplexity=150)
X_tsne = tsne.fit_transform(k_means_df)

plt.figure(figsize=(10, 8))
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=kmeans.labels_)
plt.xlabel('TSNE Component 1')
plt.ylabel('TSNE Component 2')
plt.colorbar(label='Cluster')
plt.show()
No description has been provided for this image
In [39]:
plt.figure(figsize=(10, 10))
plt.suptitle("Boxplot of numerical variables for each cluster")

for i, variable in enumerate(numeric_columns):
    plt.subplot(3, 4, i + 1)
    sns.boxplot(data=df1, x="KM_segments", y=variable)

plt.tight_layout()
No description has been provided for this image
In [40]:
k_means_df.groupby("KM_segments").mean(numeric_only=True).plot.bar(figsize=(10,10))
plt.show()
No description has been provided for this image

Observation¶

  • Cluster 0 is more stable with less volatility, 1 has more more extreme prices changes and significant outliers.

Observations¶

  • Cluster 0 has much higher count of observations at 307 to only 33 in cluster 1
  • Cluster 1 has lower average price and negative price change.
  • Cluster 1 has higher volotility, very high ROE, Net cash flow, P/E.
  • Cluster 0 is larger and more stable and profitable
  • Cluster 1 is smaller and less profitable, higher risk.
  • Cluster 0 has less variability, 1 has more more extreme prices changes and significant outliers.

Hierarchical Clustering¶

In [41]:
hc_df = subset_scaled_df.copy()
In [42]:
# 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

# printing the combination of distance metric and linkage method with the highest cophenetic correlation
print('*'*100)
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]
    )
)
Cophenetic correlation for Euclidean distance and single linkage is 0.9232271494002922.
Cophenetic correlation for Euclidean distance and complete linkage is 0.7873280186580672.
Cophenetic correlation for Euclidean distance and average linkage is 0.9422540609560814.
Cophenetic correlation for Euclidean distance and weighted linkage is 0.8693784298129404.
Cophenetic correlation for Chebyshev distance and single linkage is 0.9062538164750717.
Cophenetic correlation for Chebyshev distance and complete linkage is 0.598891419111242.
Cophenetic correlation for Chebyshev distance and average linkage is 0.9338265528030499.
Cophenetic correlation for Chebyshev distance and weighted linkage is 0.9127355892367.
Cophenetic correlation for Mahalanobis distance and single linkage is 0.9259195530524591.
Cophenetic correlation for Mahalanobis distance and complete linkage is 0.7925307202850004.
Cophenetic correlation for Mahalanobis distance and average linkage is 0.9247324030159734.
Cophenetic correlation for Mahalanobis distance and weighted linkage is 0.8708317490180426.
Cophenetic correlation for Cityblock distance and single linkage is 0.9334186366528574.
Cophenetic correlation for Cityblock distance and complete linkage is 0.7375328863205818.
Cophenetic correlation for Cityblock distance and average linkage is 0.9302145048594667.
Cophenetic correlation for Cityblock distance and weighted linkage is 0.731045513520281.
****************************************************************************************************
Highest cophenetic correlation is 0.9422540609560814, which is obtained with Euclidean distance and average linkage.
In [43]:
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

print('*'*100)
print(
    "Highest cophenetic correlation is {}, which is obtained with {} linkage.".format(
        high_cophenet_corr, high_dm_lm[1]
    )
)
Cophenetic correlation for single linkage is 0.9232271494002922.
Cophenetic correlation for complete linkage is 0.7873280186580672.
Cophenetic correlation for average linkage is 0.9422540609560814.
Cophenetic correlation for centroid linkage is 0.9314012446828154.
Cophenetic correlation for ward linkage is 0.7101180299865353.
Cophenetic correlation for weighted linkage is 0.8693784298129404.
****************************************************************************************************
Highest cophenetic correlation is 0.9422540609560814, which is obtained with average linkage.
In [44]:
# list of linkage methods
linkage_methods = ["single", "complete", "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.2f}",
        (0.80, 0.80),
        xycoords="axes fraction",
    )

    compare.append([method, coph_corr])
No description has been provided for this image
In [45]:
# 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[45]:
Linkage Cophenetic Coefficient
4 ward 0.710
1 complete 0.787
5 weighted 0.869
0 single 0.923
3 centroid 0.931
2 average 0.942
In [46]:
HCmodel = AgglomerativeClustering(n_clusters=2, metric="euclidean", linkage="ward")
HCmodel.fit(hc_df)
Out[46]:
AgglomerativeClustering()
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()
In [47]:
# 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 [48]:
hc_cluster_profile = df2.select_dtypes('number').groupby("HC_segments").mean()
In [49]:
hc_cluster_profile["count_in_each_segment"] = (df2.groupby("HC_segments")["Security"].count().values)
In [50]:
hc_cluster_profile.style.highlight_max(color="lightgreen", axis=0)
Out[50]:
  Current Price Price Change Volatility ROE Cash Ratio Net Cash Flow Net Income Earnings Per Share Estimated Shares Outstanding P/E Ratio P/B Ratio count_in_each_segment
HC_segments                        
0 83.926100 5.508733 1.426736 24.961415 72.797428 106958009.646302 1969166752.411576 3.845868 585486687.603955 28.649694 -1.676813 311
1 48.006208 -11.263107 2.590247 196.551724 40.275862 -495901724.137931 -3597244655.172414 -8.689655 486319827.294483 75.110924 -2.162622 29
In [51]:
for cl in df2["HC_segments"].unique():
    print("In cluster {}, the following companies are present:".format(cl))
    print(df2[df2["HC_segments"] == cl]["Security"].unique())
    print()
In cluster 0, the following companies are present:
['American Airlines Group' 'AbbVie' 'Abbott Laboratories'
 'Adobe Systems Inc' 'Analog Devices, Inc.' 'Archer-Daniels-Midland Co'
 'Alliance Data Systems' 'Ameren Corp' 'American Electric Power'
 'AFLAC Inc' 'American International Group, Inc.'
 'Apartment Investment & Mgmt' 'Assurant Inc' 'Arthur J. Gallagher & Co.'
 'Akamai Technologies Inc' 'Albemarle Corp' 'Alaska Air Group Inc'
 'Allstate Corp' 'Alexion Pharmaceuticals' 'Applied Materials Inc'
 'AMETEK Inc' 'Affiliated Managers Group Inc' 'Amgen Inc'
 'Ameriprise Financial' 'American Tower Corp A' 'Amazon.com Inc'
 'AutoNation Inc' 'Anthem Inc.' 'Aon plc' 'Amphenol Corp' 'Arconic Inc'
 'Activision Blizzard' 'AvalonBay Communities, Inc.' 'Broadcom'
 'American Water Works Company Inc' 'American Express Co' 'Boeing Company'
 'Bank of America Corp' 'Baxter International Inc.' 'BB&T Corporation'
 'Bard (C.R.) Inc.' 'BIOGEN IDEC Inc.' 'The Bank of New York Mellon Corp.'
 'Ball Corp' 'Bristol-Myers Squibb' 'Boston Scientific' 'BorgWarner'
 'Boston Properties' 'Citigroup Inc.' 'Caterpillar Inc.' 'Chubb Limited'
 'CBRE Group' 'Crown Castle International Corp.' 'Carnival Corp.'
 'Celgene Corp.' 'CF Industries Holdings Inc' 'Citizens Financial Group'
 'Church & Dwight' 'C. H. Robinson Worldwide' 'CIGNA Corp.'
 'Cincinnati Financial' 'Comerica Inc.' 'CME Group Inc.'
 'Chipotle Mexican Grill' 'Cummins Inc.' 'CMS Energy'
 'Centene Corporation' 'CenterPoint Energy' 'Capital One Financial'
 'The Cooper Companies' 'CSX Corp.' 'CenturyLink Inc'
 'Cognizant Technology Solutions' 'Citrix Systems' 'CVS Health'
 'Chevron Corp.' 'Dominion Resources' 'Delta Air Lines' 'Du Pont (E.I.)'
 'Deere & Co.' 'Discover Financial Services' 'Quest Diagnostics'
 'Danaher Corp.' 'The Walt Disney Company' 'Discovery Communications-A'
 'Discovery Communications-C' 'Delphi Automotive' 'Digital Realty Trust'
 'Dun & Bradstreet' 'Dover Corp.' 'Dr Pepper Snapple Group' 'Duke Energy'
 'DaVita Inc.' 'eBay Inc.' 'Ecolab Inc.' 'Consolidated Edison'
 'Equifax Inc.' "Edison Int'l" 'Eastman Chemical' 'Equinix'
 'Equity Residential' 'EQT Corporation' 'Eversource Energy'
 'Essex Property Trust, Inc.' 'E*Trade' 'Eaton Corporation'
 'Entergy Corp.' 'Edwards Lifesciences' 'Exelon Corp.' "Expeditors Int'l"
 'Expedia Inc.' 'Extra Space Storage' 'Ford Motor' 'Fastenal Co'
 'Facebook' 'Fortune Brands Home & Security' 'FirstEnergy Corp'
 'Fidelity National Information Services' 'Fiserv Inc' 'FLIR Systems'
 'Fluor Corp.' 'Flowserve Corporation' 'FMC Corporation'
 'Federal Realty Investment Trust' 'First Solar Inc'
 'Frontier Communications' 'General Dynamics'
 'General Growth Properties Inc.' 'Gilead Sciences' 'Corning Inc.'
 'General Motors' 'Genuine Parts' 'Garmin Ltd.' 'Goodyear Tire & Rubber'
 'Grainger (W.W.) Inc.' 'Halliburton Co.' 'Hasbro Inc.'
 'Huntington Bancshares' 'HCA Holdings' 'Welltower Inc.' 'HCP Inc.'
 'Hartford Financial Svc.Gp.' 'Harley-Davidson' "Honeywell Int'l Inc."
 'Hewlett Packard Enterprise' 'HP Inc.' 'Hormel Foods Corp.'
 'Henry Schein' 'Host Hotels & Resorts' 'The Hershey Company'
 'Humana Inc.' 'International Business Machines' 'IDEXX Laboratories'
 'Intl Flavors & Fragrances' 'Intel Corp.' 'International Paper'
 'Interpublic Group' 'Iron Mountain Incorporated'
 'Intuitive Surgical Inc.' 'Illinois Tool Works' 'Invesco Ltd.'
 'J. B. Hunt Transport Services' 'Jacobs Engineering Group'
 'Juniper Networks' 'JPMorgan Chase & Co.' 'Kimco Realty'
 'Coca Cola Company' 'Kansas City Southern' 'Leggett & Platt'
 'Lennar Corp.' 'Laboratory Corp. of America Holding' 'LKQ Corporation'
 'L-3 Communications Holdings' 'Lilly (Eli) & Co.' 'Lockheed Martin Corp.'
 'Alliant Energy Corp' 'Leucadia National Corp.' 'Southwest Airlines'
 'Level 3 Communications' 'LyondellBasell' 'Mastercard Inc.'
 'Mid-America Apartments' 'Macerich' "Marriott Int'l." 'Masco Corp.'
 'Mattel Inc.' "McDonald's Corp." "Moody's Corp" 'Mondelez International'
 'MetLife Inc.' 'Mohawk Industries' 'Mead Johnson' 'McCormick & Co.'
 'Martin Marietta Materials' 'Marsh & McLennan' '3M Company'
 'Monster Beverage' 'Altria Group Inc' 'The Mosaic Company'
 'Marathon Petroleum' 'Merck & Co.' 'M&T Bank Corp.' 'Mettler Toledo'
 'Mylan N.V.' 'Navient' 'NASDAQ OMX Group' 'NextEra Energy'
 'Newmont Mining Corp. (Hldg. Co.)' 'Netflix Inc.' 'Nielsen Holdings'
 'Norfolk Southern Corp.' 'Northern Trust Corp.' 'Nucor Corp.'
 'Newell Brands' 'Realty Income Corporation' 'Omnicom Group'
 "O'Reilly Automotive" "People's United Financial" 'Pitney-Bowes'
 'PACCAR Inc.' 'PG&E Corp.' 'Priceline.com Inc'
 'Public Serv. Enterprise Inc.' 'PepsiCo Inc.' 'Pfizer Inc.'
 'Principal Financial Group' 'Procter & Gamble' 'Progressive Corp.'
 'Pulte Homes Inc.' 'Philip Morris International' 'PNC Financial Services'
 'Pentair Ltd.' 'Pinnacle West Capital' 'PPG Industries' 'PPL Corp.'
 'Prudential Financial' 'Phillips 66' 'Quanta Services Inc.'
 'Praxair Inc.' 'PayPal' 'Ryder System' 'Royal Caribbean Cruises Ltd'
 'Regeneron' 'Robert Half International' 'Roper Industries'
 'Republic Services Inc' 'SCANA Corp' 'Charles Schwab Corporation'
 'Sealed Air' 'Sherwin-Williams' 'SL Green Realty'
 'Scripps Networks Interactive Inc.' 'Southern Co.'
 'Simon Property Group Inc' 'Stericycle Inc' 'Sempra Energy'
 'SunTrust Banks' 'State Street Corp.' 'Skyworks Solutions'
 'Synchrony Financial' 'Stryker Corp.' 'AT&T Inc'
 'Molson Coors Brewing Company' 'Tegna, Inc.' 'Torchmark Corp.'
 'Thermo Fisher Scientific' 'TripAdvisor' 'The Travelers Companies Inc.'
 'Tractor Supply Company' 'Tyson Foods' 'Tesoro Petroleum Co.'
 'Total System Services' 'Texas Instruments' 'Under Armour'
 'United Continental Holdings' 'UDR Inc' 'Universal Health Services, Inc.'
 'United Health Group Inc.' 'Unum Group' 'Union Pacific'
 'United Parcel Service' 'United Technologies' 'Varian Medical Systems'
 'Valero Energy' 'Vulcan Materials' 'Vornado Realty Trust'
 'Verisk Analytics' 'Verisign Inc.' 'Vertex Pharmaceuticals Inc'
 'Ventas Inc' 'Verizon Communications' 'Waters Corporation'
 'Wec Energy Group Inc' 'Wells Fargo' 'Whirlpool Corp.'
 'Waste Management Inc.' 'Western Union Co' 'Weyerhaeuser Corp.'
 'Wyndham Worldwide' 'Wynn Resorts Ltd' 'Xcel Energy Inc' 'XL Capital'
 'Exxon Mobil Corp.' 'Dentsply Sirona' 'Xerox Corp.' 'Xylem Inc.'
 'Yahoo Inc.' 'Yum! Brands Inc' 'Zimmer Biomet Holdings' 'Zions Bancorp'
 'Zoetis']

In cluster 1, the following companies are present:
['Allegion' 'Apache Corporation' 'Anadarko Petroleum Corp'
 'Baker Hughes Inc' 'Chesapeake Energy' 'Charter Communications'
 'Colgate-Palmolive' 'Cabot Oil & Gas' 'Concho Resources'
 'Devon Energy Corp.' 'EOG Resources' 'Freeport-McMoran Cp & Gld'
 'Hess Corporation' 'Kimberly-Clark' 'Kinder Morgan' 'Marathon Oil Corp.'
 'Murphy Oil' 'Noble Energy Inc' 'Newfield Exploration Co'
 'National Oilwell Varco Inc.' 'ONEOK' 'Occidental Petroleum'
 'Range Resources Corp.' 'Spectra Energy Corp.' 'S&P Global, Inc.'
 'Southwestern Energy' 'Teradata Corp.' 'Williams Cos.' 'Cimarex Energy']

In [52]:
df2.groupby(["HC_segments", "GICS Sector"])['Security'].count()
Out[52]:
HC_segments  GICS Sector                
0            Consumer Discretionary         39
             Consumer Staples               17
             Energy                          8
             Financials                     48
             Health Care                    40
             Industrials                    52
             Information Technology         32
             Materials                      19
             Real Estate                    27
             Telecommunications Services     5
             Utilities                      24
1            Consumer Discretionary          1
             Consumer Staples                2
             Energy                         22
             Financials                      1
             Industrials                     1
             Information Technology          1
             Materials                       1
Name: Security, dtype: int64
In [53]:
plt.figure(figsize=(20, 20))
plt.suptitle("Boxplot of numerical variables for each cluster")

for i, variable in enumerate(numeric_columns):
    plt.subplot(3, 4, i + 1)
    sns.boxplot(data=df2, x="HC_segments", y=variable)

plt.tight_layout(pad=2.0)
No description has been provided for this image

Actionable Insights and Recommendations¶