<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Mariangela Bonghi on Medium]]></title>
        <description><![CDATA[Stories by Mariangela Bonghi on Medium]]></description>
        <link>https://medium.com/@mariangela.bonghi?source=rss-111017e80548------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*rSix9wCc8jT8LhFaUE5xjg.jpeg</url>
            <title>Stories by Mariangela Bonghi on Medium</title>
            <link>https://medium.com/@mariangela.bonghi?source=rss-111017e80548------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 10:37:43 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mariangela.bonghi/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[From Math to Marketing: Validating Clusters and Extracting Actionable Insights]]></title>
            <link>https://pub.towardsai.net/from-math-to-marketing-validating-clusters-and-extracting-actionable-insights-264d4443ee3b?source=rss-111017e80548------2</link>
            <guid isPermaLink="false">https://medium.com/p/264d4443ee3b</guid>
            <category><![CDATA[silhouette-score]]></category>
            <category><![CDATA[calinski-harabasz-score]]></category>
            <category><![CDATA[davies-bouldin-index]]></category>
            <category><![CDATA[clustering]]></category>
            <dc:creator><![CDATA[Mariangela Bonghi]]></dc:creator>
            <pubDate>Mon, 23 Feb 2026 22:01:01 GMT</pubDate>
            <atom:updated>2026-02-23T23:30:56.491Z</atom:updated>
            <content:encoded><![CDATA[<h4>Customer Segmentation Series</h4><h4>Part 3 — Validation, Selection, and Visualization: Evaluating metrics and extracting actionable business insights</h4><p>by Mariangela Bonghi</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q_WXejgJ2u-0APaLjkQufQ.png" /></figure><p>In <strong>Part 2</strong>, we put our four dataset configurations to find optimal parameters for K-Means, Agglomerative Clustering, and GMM.</p><p>As a reminder, our journey is divided into three specialized stages to ensure a complete data science pipeline:</p><ul><li><strong>Part 1: EDA, outliers, and feature selection(Completed) </strong>— We’ll start by exploring the data, handling outliers, and analyzing features.</li><li><strong>Part 2: Identifying optimal parameters for K-Means, GMM, and Linkage methods </strong>for different configurations of the dataset <strong>(Completed)</strong>;</li><li><strong>Part 3: Validation, Selection, and Visualization — Evaluating metrics and extracting actionable business insights(You are here!)</strong></li></ul><h3>Clustering Validation: Assessing the quality and consistency of the identified models</h3><p>After identifying the potential optimal parameters for K-Means, Agglomerative Clustering, and GMM algorithms, we define a dictionary to store our candidate configurations. We will now apply specific metrics to evaluate the effectiveness of each approach. Among the available indices, we will use:</p><ul><li><strong>Silhouette Score[1]:</strong> Evaluates how compact and well-separated the clusters are. It ranges from -1 to 1, where a value of 1 represents the ideal clustering (points are very close to their own cluster and far from others).</li><li><strong>Calinski-Harabasz Index[2]</strong>: It is the ratio of the sum of between-cluster dispersion to within-cluster dispersion. A higher value indicates better clustering, as it signifies that the clusters are dense (low internal dispersion) and well-separated (high external dispersion).</li><li><strong>Davies-Bouldin Index[3]:</strong> This index signifies the average similarity between each cluster and its most similar one. Unlike the previous metrics, a lower value (closer to 0) indicates better clustering, as it means the clusters are less similar to each other and more distinct.</li></ul><p>By balancing these scores, we will determine which algorithm and feature set provide the most meaningful and stable customer segments.</p><pre># Define optimal_k_map dictionary based on the conclusions of the previous sections:<br>optimal_k_map = {<br>    &#39;Income-Spending (All Data)&#39;: <br>    {&#39;K-Means&#39;: [5], &#39;Ward&#39;: [5], &#39;Complete&#39;: [7], &#39;Average&#39;: [7], <br>         &#39;GMM&#39;: [{&#39;n_components&#39;: 5, &#39;covariance_type&#39;: &#39;spherical&#39;},<br>                {&#39;n_components&#39;: 5, &#39;covariance_type&#39;: &#39;diag&#39;},<br>                {&#39;n_components&#39;: 6, &#39;covariance_type&#39;: &#39;spherical&#39;},<br>                {&#39;n_components&#39;: 6, &#39;covariance_type&#39;: &#39;diag&#39;}]},<br>    &#39;Income-Spending (No Outliers)&#39;: <br>    {&#39;K-Means&#39;: [6], &#39;Ward&#39;: [5], &#39;Complete&#39;: [7], &#39;Average&#39;: [5],<br>         &#39;GMM&#39;: [{&#39;n_components&#39;: 5, &#39;covariance_type&#39;: &#39;spherical&#39;},<br>                {&#39;n_components&#39;: 5, &#39;covariance_type&#39;: &#39;diag&#39;},<br>                {&#39;n_components&#39;: 6, &#39;covariance_type&#39;: &#39;spherical&#39;},<br>                {&#39;n_components&#39;: 6, &#39;covariance_type&#39;: &#39;diag&#39;}]},<br>    &#39;Age-Income-Spending (All Data)&#39;: <br>    {&#39;K-Means&#39;: [6], &#39;Ward&#39;: [6], &#39;Complete&#39;: [5], &#39;Average&#39;: [5], <br>     &#39;GMM&#39;: [{&#39;n_components&#39;: 5, &#39;covariance_type&#39;: &#39;diag&#39;},<br>            {&#39;n_components&#39;: 6, &#39;covariance_type&#39;: &#39;diag&#39;},<br>            {&#39;n_components&#39;: 7, &#39;covariance_type&#39;: &#39;diag&#39;}]},<br>    &#39;Age-Income-Spending (No Outliers)&#39;: <br>    {&#39;K-Means&#39;: [6], &#39;Ward&#39;: [6], &#39;Complete&#39;: [6], &#39;Average&#39;: [5],<br>          &#39;GMM&#39;: [{&#39;n_components&#39;: 5, &#39;covariance_type&#39;: &#39;diag&#39;},<br>            {&#39;n_components&#39;: 6, &#39;covariance_type&#39;: &#39;diag&#39;},<br>            {&#39;n_components&#39;: 7, &#39;covariance_type&#39;: &#39;diag&#39;}]}<br>}<br># Our dataset configurations<br>dataset_configurations = [<br>    {&#39;dataset&#39;: X_IncomeSpending_all, &#39;label&#39;: &#39;Income-Spending (All Data)&#39;},<br>    {&#39;dataset&#39;: X_IncomeSpending_no_outliers, &#39;label&#39;: &#39;Income-Spending (No Outliers)&#39;},<br>    {&#39;dataset&#39;: X_AgeIncomeSpending_all, &#39;label&#39;: &#39;Age-Income-Spending (All Data)&#39;},<br>    {&#39;dataset&#39;: X_AgeIncomeSpending_no_outliers, &#39;label&#39;: &#39;Age-Income-Spending (No Outliers)&#39;}<br>]<br>def evaluate_clustering(X, labels):<br>    &quot;&quot;&quot;<br>    Calculate clustering metrics (Silhouette, Calinski-Harabasz, Davies-Bouldin)<br>    INPUTS:<br>    - X: The feature matrix (data points).<br>    - labels: The cluster labels assigned to each data point by the clustering algorithm.<br>    OUTPUTS (Tuple):<br>    - n_clusters (int): The number of clusters <br>    - n_samples (int): The number of data points<br>    - silhouette (float): The Silhouette Score.<br>    - calinski (float): The Calinski-Harabasz Index.<br>    - davies_bouldin (float): The Davies-Bouldin Index.<br>    &quot;&quot;&quot;<br>    n_samples = len(X)<br>    silhouette = silhouette_score(X, labels)<br>    calinski = calinski_harabasz_score(X, labels)<br>    davies_bouldin = davies_bouldin_score(X, labels)<br>    return n_samples, silhouette, calinski, davies_bouldin<br>def highlight_best(s):<br>    &quot;&quot;&quot;<br>    Apply a light green background to the best value for each clustering metric.<br>    INPUT:<br>    s (pd.Series): A Pandas Series representing a column of metric scores <br>    OUTPUT:<br>    - Returns &#39;background-color: lightgreen&#39; for the best value(s) and &#39;&#39; otherwise.<br>    &quot;&quot;&quot;<br>    if s.name not in [&#39;Silhouette (Max is 1)&#39;, &#39;Calinski-Harabasz (Max is Inf)&#39;, &#39;Davies-Bouldin (Min is 0)&#39;]:<br>        return [&#39;&#39; for _ in s]<br>    if s.name in [&#39;Silhouette (Max is 1)&#39;, &#39;Calinski-Harabasz (Max is Inf)&#39;]:<br>        # Max is the best. We use max() to find the max not-NaN and np.isclose to manage float<br>        max_val = s.max()<br>        is_best = np.isclose(s, max_val, equal_nan=False) if not pd.isna(max_val) else [False] * len(s)<br>    else: <br>        # Davies-Bouldin Min is the best<br>        min_val = s.min()<br>        is_best = np.isclose(s, min_val, equal_nan=False) if not pd.isna(min_val) else [False] * len(s) <br>    return [&#39;background-color: lightgreen&#39; if v else &#39;&#39; for v in is_best]<br># Execution<br>results_dfs = {}<br>for config in dataset_configurations:<br>    X = config[&#39;dataset&#39;]<br>    label = config[&#39;label&#39;]<br>    # Retrieve optimized K values for the current dataset<br>    optimal_ks = optimal_k_map[label]<br>    all_results = []<br>    total_points = len(X)<br>    # K-Means execution<br>    for K in optimal_ks[&#39;K-Means&#39;]:<br>        kmeans = KMeans(n_clusters=K, init=&#39;k-means++&#39;, n_init=&#39;auto&#39;, random_state=42)<br>        labels_kmeans = kmeans.fit_predict(X)<br>        all_results.append([&#39;K-Means&#39;, K, *evaluate_clustering(X, labels_kmeans)])<br>    # GMM execution<br>    for gmm_cfg in optimal_ks[&#39;GMM&#39;]:<br>        K = gmm_cfg[&#39;n_components&#39;]<br>        cov_type = gmm_cfg[&#39;covariance_type&#39;]<br>        gmm = GaussianMixture(n_components=K, covariance_type=cov_type, random_state=42,n_init=5)<br>        labels_gmm = gmm.fit_predict(X)<br>        all_results.append([f&#39;GMM ({cov_type})&#39;, K, *evaluate_clustering(X, labels_gmm)])<br>    # AgglomerativeClustering execution<br>    for method in [&#39;ward&#39;, &#39;complete&#39;, &#39;average&#39;]:<br>        K_list = optimal_ks[method.capitalize()]<br>        for K in K_list: <br>            agglo = AgglomerativeClustering(n_clusters=K, linkage=method)<br>            labels_agglo = agglo.fit_predict(X)<br>            all_results.append([f&#39;Linkage {method.capitalize()}&#39;, K, *evaluate_clustering(X, labels_agglo)])  <br>    # Report Creation and Display<br>    print(f&quot;\n Comparative Results: {label}&quot;)<br>    df_result = pd.DataFrame(all_results, columns=[<br>        &#39;Method&#39;, &#39;Found K&#39;, &#39;Evaluated Points&#39;, <br>        &#39;Silhouette (Max is 1)&#39;, &#39;Calinski-Harabasz (Max is Inf)&#39;, &#39;Davies-Bouldin (Min is 0)&#39;<br>    ])<br>    #Cleaning and Sorting<br>    sort_order = [&#39;K-Means&#39;, &#39;Linkage Ward&#39;, &#39;Linkage Complete&#39;, &#39;Linkage Average&#39;, &#39;GMM&#39;]<br>    df_result[&#39;Method_Sort&#39;] = pd.Categorical(df_result[&#39;Method&#39;], categories=sort_order, ordered=True)<br>    df_result = df_result.sort_values(by=[&#39;Method_Sort&#39;, &#39;Found K&#39;]).drop(columns=&#39;Method_Sort&#39;)<br>    styled_df = df_result.style.apply(highlight_best, axis=0).format({<br>            &#39;Silhouette (Max is 1)&#39;: &quot;{:.3f}&quot;, &#39;Calinski-Harabasz (Max is Inf)&#39;: &quot;{:.0f}&quot;, &#39;Davies-Bouldin (Min is 0)&#39;: &quot;{:.3f}&quot;,<br>            &#39;Found K&#39;: &quot;{:.0f}&quot;,<br>        }, na_rep=&#39;N/A&#39;) \<br>        .hide(axis=&#39;index&#39;)<br>    display(styled_df)<br>    results_dfs[label] = df_result</pre><figure><img alt="Comparative Performance Metrics for 2D Clustering Models" src="https://cdn-images-1.medium.com/max/849/1*OrRDadNQ6dC9Rxu5KJgg8Q.png" /><figcaption><strong>Figure 3</strong>: Comparative Performance Metrics for 2D Clustering Models</figcaption></figure><p>Looking at the results objectively, the GMM with spherical covariance and K=5 would be the theoretically optimal choice. Its Silhouette Score is the absolute maximum (0.560), the Calinski-Harabasz index (263) is essentially at its peak (second only to the maximum of 264), and the Davies-Bouldin index is among the lowest recorded.</p><p>However, when examining the metrics for the dataset containing outliers, K-Means with K=5 clearly emerges as the winner. Comparing the performance of this model against the GMM (spherical, K=5), we do not observe significant discrepancies. Although the indices for the full dataset are slightly lower, they are so close to the “No Outliers” version that they do not justify a strong preference for one model over the other.</p><p>This observation, combined with the fact that the removed outliers do not appear to be data entry errors based on their values, leads us to conclude that removing them is unnecessary. Therefore, we choose to proceed with the full dataset using K-Means with K=5.</p><figure><img alt="Comparative Performance Metrics for 3D Clustering Models" src="https://cdn-images-1.medium.com/max/866/1*MKP7DMthDR9kpS6nzDcN0g.png" /><figcaption><strong>Figure 4</strong>: Comparative Performance Metrics for 3D Clustering Models</figcaption></figure><p>In this case, for both the dataset with and without outliers, K-Means with K=6 emerges as the superior model. The conclusions align with those from the 2D analysis.</p><p>While the metrics for the “No Outliers” dataset show slight improvements, the differences are so marginal that there is no significant statistical evidence to justify using the cleaned dataset over the original one. This fact, combined with the observation that the two removed outliers do not appear to be data entry errors, leads us to conclude that K-Means with K=6 using the full dataset is the most appropriate choice.</p><h3>Clustering Validation: The Final Verdict</h3><ul><li><strong>2D Top Performer:</strong> <strong>K-Means (K=5)</strong> for the full dataset;</li><li><strong>3D Top Performer:</strong> <strong>K-Means (K=6)</strong> for the full dataset.</li></ul><h4>Clustering and Interpretation of Results: “Annual Income” and “Spending Score” features — KMeans with K=5</h4><p>Let’s evaluate and visualize our winner for the 2D configuration:</p><pre>kmeans = KMeans(n_clusters=5, init=&#39;k-means++&#39;, n_init=&#39;auto&#39;, random_state=42)<br>labels_kmeans = kmeans.fit_predict(X_IncomeSpending_all)<br>df[&#39;Cluster_KMeans_2feat&#39;] = labels_kmeans<br>total_population = len(df)<br># View the Statistical Table with Percentage Data<br>stats_kmeans_2D = df.groupby(&#39;Cluster_KMeans_2feat&#39;).agg(<br>    Cluster_Size=(&#39;Cluster_KMeans_2feat&#39;, &#39;size&#39;),<br>    Average_Income=(&#39;Annual Income (k$)&#39;, &#39;mean&#39;),<br>    Average_Spending_Score=(&#39;Spending Score (1-100)&#39;, &#39;mean&#39;),<br>    Average_Age=(&#39;Age&#39;, &#39;mean&#39;),<br>    # Male and Female count<br>    Male_Count=(&#39;Gender&#39;, lambda x: (x == &#39;Male&#39;).sum()),<br>    Female_Count=(&#39;Gender&#39;, lambda x: (x == &#39;Female&#39;).sum())<br>).reset_index()<br># Percentage evaluation<br>stats_kmeans_2D[&#39;Population_%&#39;] = (stats_kmeans_2D[&#39;Cluster_Size&#39;] / total_population) * 100<br>stats_kmeans_2D[&#39;Male_%&#39;] = (stats_kmeans_2D[&#39;Male_Count&#39;] / stats_kmeans_2D[&#39;Cluster_Size&#39;]) * 100<br>stats_kmeans_2D[&#39;Female_%&#39;] = (stats_kmeans_2D[&#39;Female_Count&#39;] / stats_kmeans_2D[&#39;Cluster_Size&#39;]) * 100<br># Select the columns<br>stats_final = stats_kmeans_2D[[&#39;Cluster_KMeans_2feat&#39;, &#39;Population_%&#39;,&#39;Average_Income&#39;, &#39;Average_Spending_Score&#39;,&#39;Average_Age&#39;, &#39;Male_%&#39;, &#39;Female_%&#39;]]<br># View final table<br>display(stats_final.style.hide(axis=&#39;index&#39;).set_caption(&quot;K-Means (K=5) Cluster Statistics (Percentages)&quot;).format<br>        ({&#39;Population_%&#39;: &quot;{:.1f}%&quot;, &#39;Average_Income&#39;: &quot;{:.1f}&quot;, &#39;Average_Spending_Score&#39;: &quot;{:.1f}&quot;, &#39;Average_Age&#39;: &quot;{:.1f}&quot;,<br>           &#39;Male_%&#39;: &quot;{:.1f}%&quot;, &#39;Female_%&#39;: &quot;{:.1f}%&quot;}))<br># Scatter Plot <br>plt.figure(figsize=(12, 8))<br>sns.scatterplot(x=&#39;Annual Income (k$)&#39;, y=&#39;Spending Score (1-100)&#39;, hue=&#39;Cluster_KMeans_2feat&#39;, data=df, palette=&#39;viridis&#39;, style=&#39;Gender&#39;, s=100)<br>plt.title(&#39;K-Means Clustering (K=5) &quot;Spendig Score vs Annual Income&quot; &#39;)<br>plt.xlabel(&#39;Annual Income (k$)&#39;)<br>plt.ylabel(&#39;Spending Score (1-100)&#39;)<br>plt.grid(True, linestyle=&#39;--&#39;, alpha=0.6)<br># legend<br>plt.legend(title=&#39;Legenda&#39;, loc=&#39;lower left&#39;, bbox_to_anchor=(1.01, 0), borderaxespad=0)<br>plt.tight_layout(rect=[0, 0, 1, 1])<br>plt.show()</pre><figure><img alt="Table and scatter plot 2D Configuration" src="https://cdn-images-1.medium.com/max/875/1*M2lCIrNv4WOW3-XjX8TCSw.png" /><figcaption><strong>Figure 5</strong>: Table and scatter plot 2D Configuration</figcaption></figure><h4><strong>Business Insights:</strong></h4><p><strong>“Low-Income Groups” (Clusters 2 &amp; 4)</strong><br>These segments represent customers with limited annual income but opposite spending habits:</p><ul><li><strong>Cluster 2 — “Young High Spenders”</strong>: High spending score and the youngest average age. These are customers who prioritize trends and immediate gratification despite budget constraints. Strategy: Social media marketing, trendy/viral product drops, and “Buy Now, Pay Later” options.</li><li><strong>Cluster 4 — “Mature Low Spenders”</strong>: Lowest spending score and mature age. These are cost-conscious customers who focus on essentials. Strategy: Focus instead on utility-based marketing, durability, and “essential-only” promotions to build trust.</li></ul><p><strong>“High-Income Groups” (Clusters 1 &amp; 3)</strong><br>These are customers with the highest annual income:</p><ul><li><strong>Cluster 1 — “Luxury Enthusiasts”</strong>: Extremely high spending (82.1) and relatively young (32.7). Strategy: Exclusive VIP events, personalized concierge services, and premium loyalty rewards.</li><li><strong>Cluster 3 — “High Income Savers”</strong>: High income, but the lowest spending score (17.6). More mature (40.7) and selective. They likely prioritize investments or high-end durable goods over frequent shopping. Strategy: “Quiet luxury” marketing. Focus on quality, craftsmanship, and long-term value to justify the price point.</li></ul><p><strong>“Balanced Cluster” (Cluster 0)</strong><br>Represent the majority of the population (40.5%). Income (55.3k), spending (49.5), and age (42.7 years) values are close to the dataset’s overall average. No extreme behaviors. Spending is proportional to income. Strategy: As the largest group, they need mass-market marketing. The goal is to increase loyalty and purchase volume via standard promotions, value bundles, and direct communication on everyday products.</p><h4>Clustering and Interpretation of Results: “Age”, “Annual Income” and “Spending Score” features — KMeans with K=6</h4><p>Let’s evaluate and visualize our winner for the 3D configuration:</p><pre>K = 6<br>kmeans = KMeans(n_clusters=K, init=&#39;k-means++&#39;, n_init=&#39;auto&#39;, random_state=42)<br>labels_kmeans = kmeans.fit_predict(X_AgeIncomeSpending_all)<br>df[&#39;Cluster_KMeans_3feat&#39;] = labels_kmeans<br>total_population = len(df) <br># View the Statistical Table with Percentage Data<br>stats_kmeans_3D = df.groupby(&#39;Cluster_KMeans_3feat&#39;).agg(<br>    Cluster_Size=(&#39;Cluster_KMeans_3feat&#39;, &#39;size&#39;),<br>    Average_Income=(&#39;Annual Income (k$)&#39;, &#39;mean&#39;),<br>    Average_Spending_Score=(&#39;Spending Score (1-100)&#39;, &#39;mean&#39;),<br>    Average_Age=(&#39;Age&#39;, &#39;mean&#39;),<br>    # Male and Female count<br>    Male_Count=(&#39;Gender&#39;, lambda x: (x == &#39;Male&#39;).sum()),<br>    Female_Count=(&#39;Gender&#39;, lambda x: (x == &#39;Female&#39;).sum())<br>).reset_index()<br># Percentage evaluation<br>stats_kmeans_3D[&#39;Population_%&#39;] = (stats_kmeans_3D[&#39;Cluster_Size&#39;] / total_population) * 100<br>stats_kmeans_3D[&#39;Male_%&#39;] = (stats_kmeans_3D[&#39;Male_Count&#39;] / stats_kmeans_3D[&#39;Cluster_Size&#39;]) * 100<br>stats_kmeans_3D[&#39;Female_%&#39;] = (stats_kmeans_3D[&#39;Female_Count&#39;] / stats_kmeans_3D[&#39;Cluster_Size&#39;]) * 100<br># Select the columns<br>stats_final = stats_kmeans_3D[[&#39;Cluster_KMeans_3feat&#39;,&#39;Population_%&#39;, &#39;Average_Income&#39;,&#39;Average_Spending_Score&#39;, &#39;Average_Age&#39;, &#39;Male_%&#39;, &#39;Female_%&#39;]]<br># View final table<br>display(stats_final.style.hide(axis=&#39;index&#39;).set_caption(f&quot;K-Means (K={K}) Cluster Statistics (Percentages)&quot;).format({<br>            &#39;Population_%&#39;: &quot;{:.1f}%&quot;, &#39;Average_Income&#39;: &quot;{:.1f}&quot;, &#39;Average_Spending_Score&#39;: &quot;{:.1f}&quot;, &#39;Average_Age&#39;: &quot;{:.1f}&quot;,<br>            &#39;Male_%&#39;: &quot;{:.1f}%&quot;, &#39;Female_%&#39;: &quot;{:.1f}%&quot;}))<br>#Plot 3D<br>fig = plt.figure(figsize=(12, 8)) <br>ax = fig.add_subplot(111, projection=&#39;3d&#39;)<br>cmap = plt.colormaps.get_cmap(&#39;viridis&#39;) <br>markers = {&#39;Male&#39;: &#39;o&#39;, &#39;Female&#39;: &#39;s&#39;} # Different markers for gender<br>scatters = []<br>for gender_val, marker in markers.items():<br>    # Filter data (using numeric 0/1)<br>    df_gender = df[df[&#39;Gender&#39;] == gender_val]<br>    if not df_gender.empty:<br>        scatter = ax.scatter(<br>            df_gender[&#39;Age&#39;], df_gender[&#39;Annual Income (k$)&#39;], df_gender[&#39;Spending Score (1-100)&#39;], <br>            c=df_gender[&#39;Cluster_KMeans_3feat&#39;], cmap=cmap, marker=marker, s=60, label=gender_val, vmin=0, vmax=K-1) <br>        scatters.append(scatter)<br>ax.set_title(f&#39;K-Means Clustering (K={K}) in 3D: Cluster vs Gender&#39;)<br>ax.set_xlabel(&#39;Age&#39;)<br>ax.set_ylabel(&#39;Annual Income (k$)&#39;)<br>ax.set_zlabel(&#39;Spending Score (1-100)&#39;)<br>if scatters:<br>    legend1 = ax.legend(*scatters[0].legend_elements(), title=&quot;Clusters&quot;, loc=&#39;upper left&#39;, bbox_to_anchor=(1.05, 1.0))<br>    ax.add_artist(legend1)<br># Gender Marker Legend<br>handles = [plt.Line2D([], [], marker=&#39;o&#39;, color=&#39;gray&#39;, linestyle=&#39;None&#39;, label=&#39;Male&#39;),<br>           plt.Line2D([], [], marker=&#39;s&#39;, color=&#39;gray&#39;, linestyle=&#39;None&#39;, label=&#39;Female&#39;)]<br>ax.legend(handles=handles, title=&quot;Gender&quot;, loc=&#39;upper left&#39;, bbox_to_anchor=(1.05, 0.6))<br>plt.tight_layout()<br>plt.show()</pre><figure><img alt="Table and 3D plot 3D Configuration" src="https://cdn-images-1.medium.com/max/761/1*sAEoue_4S0QeH8SwpZEs5Q.png" /><figcaption><strong>Figure 6</strong>: Table and 3D plot 3D Configuration</figcaption></figure><p><strong>3D Cluster Analysis: The Impact of Age</strong><br>While the core clusters remain consistent with the 2D model, adding Age allows us to significantly refine the previously vague “Balanced Cluster”. In the 2-feature model, Cluster 0 appeared as a monolithic “average” group; however, the 3D analysis reveals it is actually composed of two distinct generational segments:<br>- <strong>Cluster 0 — “Senior Traditionalists”:</strong><br>Mature customers (avg. age 56.3) with a majority female (57.8%) profile and Average income/spending; likely the most loyal but least impulsive.<br>Strategy: Traditional loyalty programs, comfort-focused products, and high-touch in-store assistance.<br>- <strong>Cluster 3 — “Junior Pragmatists”:</strong><br>Very young (avg. age 26.1), predominantly female (60%)<br>Strategy: Aesthetic-driven social campaigns (Instagram/TikTok) and mobile-app gamification.</p><p><strong>Conclusion</strong><br>The 3-feature model proves that Age is a useful differentiator. While Income and Spending tell us <strong>what</strong> they can buy, Age tells us <strong>how</strong> to talk to them and which channels to use.</p><h3>Conclusion &amp; Final Thoughts</h3><p>After a rigorous comparison across three algorithmic families and four dataset configurations, a clear pattern has emerged. While advanced probabilistic models like <strong>GMM</strong> offer theoretical elegance, <strong>K-Means</strong> remains the most robust and interpretable choice for this business case.</p><p>The transition from a 2D to a 3D model proved that <strong>Age</strong> is more than just a variable; it is a lens that splits ‘average’ consumers into generational segments with vastly different digital habits.</p><p>This journey demonstrates that clustering is as much an art as it is a science. Moving between the precision of coefficients and the subjectivity of analytical judgment, we learn that there is no universal recipe. Instead, we must find our own way through a labyrinth of trade-offs, where the best model is not just the most statistically sound, but the one that makes the most sense for our goals.</p><p><em>Thanks for following this series! The complete code for this project is available on </em><a href="https://github.com/mariangelabonghi/Clustering-Mall-Customers-in-Python.git"><em>GitHub</em></a></p><h4><strong>References</strong></h4><p><strong>[1] GeeksforGeeks,</strong> What is Silhouette Score (2026), <a href="https://www.geeksforgeeks.org/machine-learning/what-is-silhouette-score/"><strong>Technical Tutorial</strong></a><br><strong>[2] Misha Sv,</strong> Calinski-Harabasz Index for K-Means Clustering Evaluation using Python (2022), <strong>Towards Data Science, </strong><a href="https://towardsdatascience.com/calinski-harabasz-index-for-k-means-clustering-evaluation-using-python-4fefeeb2988e/">Article Link</a><br><strong>[3] Misha Sv,</strong> Davies-Bouldin Index for K-Means Clustering Evaluation in Python (2021), <strong>Towards Data Science,</strong> <a href="https://towardsdatascience.com/davies-bouldin-index-for-k-means-clustering-evaluation-in-python-57f66da15cd/">Article Link</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=264d4443ee3b" width="1" height="1" alt=""><hr><p><a href="https://pub.towardsai.net/from-math-to-marketing-validating-clusters-and-extracting-actionable-insights-264d4443ee3b">From Math to Marketing: Validating Clusters and Extracting Actionable Insights</a> was originally published in <a href="https://pub.towardsai.net/">Towards AI</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[3 Algorithms and 4 Configurations for Customer Segmentation]]></title>
            <link>https://pub.towardsai.net/3-algorithms-and-4-configurations-for-customer-segmentation-4a6b244bb3c1?source=rss-111017e80548------2</link>
            <guid isPermaLink="false">https://medium.com/p/4a6b244bb3c1</guid>
            <category><![CDATA[agglomerative-clustering]]></category>
            <category><![CDATA[gaussian-mixture-model]]></category>
            <category><![CDATA[clustering]]></category>
            <category><![CDATA[kmeans-clustering]]></category>
            <dc:creator><![CDATA[Mariangela Bonghi]]></dc:creator>
            <pubDate>Mon, 23 Feb 2026 21:01:01 GMT</pubDate>
            <atom:updated>2026-02-23T23:29:36.056Z</atom:updated>
            <content:encoded><![CDATA[<h4>Customer Segmentation Series</h4><h4>Part 2 — <strong>Identifying optimal parameters for K-Means, GMM, and Linkage methods</strong></h4><p>by Mariangela Bonghi</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/983/1*OxF8MmQ3I--Prd0v5Z3wBw.png" /><figcaption>Image created using plots generated by the author’s code, refined with the assistance of Gemini</figcaption></figure><p>In <strong>Part 1</strong>, we laid the analytical foundation by exploring our data, handling outliers, and using Mutual Information to identify our most informative features. We moved from raw data to a strategic framework of four different dataset configurations.</p><p>As a reminder, our journey is divided into three specialized stages to ensure a complete data science pipeline:</p><ul><li><strong>Part 1: EDA, outliers, and feature selection(Completed) </strong>— We’ll start by exploring the data, handling outliers, and analyzing features.</li><li><strong>Part 2: Identifying optimal parameters for K-Means, GMM, and Linkage methods </strong>for different configurations of the dataset <strong>(You are here!).</strong></li><li><strong>Part 3: Validation, Selection, and Visualization — Evaluating metrics and extracting actionable business insights.</strong></li></ul><h4>Moving from Preparation to Pattern Discovery</h4><p>Now that our data is primed, it’s time to move into the modeling phase. In this part, we will identify the optimal parameters for four dataset configurations across three core algorithms: <strong>K-Means</strong>, <strong>Agglomerative Hierarchical Clustering</strong>, and <strong>Gaussian Mixture Models (GMM)</strong>.”</p><h4>K-Means Clustering</h4><p><strong>K-Means</strong> [1,2] is a clustering algorithm that groups n observations into k clusters based on the similarity between data points.<br>The algorithm begins with an initialization phase: given the desired number of clusters (the value of k), the centroids of each cluster are typically initialized (e.g., randomly or via K-Means++).</p><p>From this point, the following steps are iterated:</p><ul><li>Each data point is evaluated and assigned to its nearest centroid.</li><li>The centroids are recalculated based on the new assignments.</li></ul><p>The process stops when the centroids don’t move significantly.</p><p><strong>Finding the Optimal K</strong></p><p>To apply this algorithm effectively, the optimal value for k must be determined. This is achieved using the <strong>Elbow Method</strong>: a graph is plotted showing Inertia (also known as the Within-Cluster Sum of Squares, or WCSS) as a function of the number of clusters (k).<br>Inertia quantifies the internal cohesion of the clusters; a lower value indicates that points are closer to their respective centroids. The optimal k is found at the “elbow” of the curve. Beyond this point, adding more clusters does not significantly improve the result (i.e., the decrease in inertia becomes marginal).</p><pre># Define the range of K (number of clusters) to test<br>K_range = range(1, 12) <br># Define variables that we use in the algorithms <br>dataset_configurations = [<br>    {&#39;dataset&#39;: X_IncomeSpending_all,&#39;label&#39;: &#39;Income-Spending (All Data)&#39;},<br>    {&#39;dataset&#39;: X_AgeIncomeSpending_all,&#39;label&#39;: &#39;Age-Income-Spending (All Data)&#39;},<br>    {&#39;dataset&#39;: X_IncomeSpending_no_outliers,&#39;label&#39;: &#39;Income-Spending (No Outliers)&#39;},<br>    {&#39;dataset&#39;: X_AgeIncomeSpending_no_outliers,&#39;label&#39;: &#39;Age-Income-Spending (No Outliers)&#39;}<br>]<br># Evaluation: Run the clustering process for each dataset configuration<br>results = {}<br>for dc in dataset_configurations:<br>    data_label = dc[&#39;label&#39;]<br>    data_set = dc[&#39;dataset&#39;]<br>    # Calculate inertia for each k in the range<br>    inertias = [KMeans(n_clusters=k, init=&#39;k-means++&#39;, n_init=&#39;auto&#39;, random_state=42).fit(data_set).inertia_ for k in K_range]<br>    k_mapping = dict(zip(K_range, inertias))   <br>    results[data_label] = {&#39;inertias&#39;: inertias, &#39;k_mapping&#39;: k_mapping}<br># Visualize the Elbow Method results<br>fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(18, 10)) <br>axes = axes.flatten() <br>labels = list(results.keys())<br>for i, ax in enumerate(axes):<br>    label = labels[i]<br>    inertias = results[label][&#39;inertias&#39;]    <br>    # Plotting the elbow curve<br>    ax.plot(K_range, inertias, marker=&#39;o&#39;, linestyle=&#39;-&#39;, color=&#39;b&#39;) <br>    # Adding text annotations for each inertia value<br>    for k, val in zip(K_range, inertias):<br>        ax.annotate(f&#39;{val:.1f}&#39;, xy=(k, val), xytext=(0, 7), textcoords=&#39;offset points&#39;, ha=&#39;center&#39;, fontsize=8)<br>    ax.set_title(f&quot;Elbow Method: {label}&quot;, fontsize=12)<br>    ax.set_xlabel(&quot;Number of Clusters (K)&quot;, fontsize=10)<br>    # Set Y-axis label for the leftmost plots in each row<br>    ax.set_ylabel(&quot;Inertia&quot;, fontsize=10)        <br>    ax.set_xticks(K_range)<br>    ax.grid(True, linestyle=&#39;--&#39;, alpha=0.6)<br>fig.tight_layout()<br>plt.show()</pre><figure><img alt="Comparative Elbow Method analysis across four dataset configurations" src="https://cdn-images-1.medium.com/max/1024/1*v8YBOLTvq11btqei8a3m7g.png" /><figcaption><strong>Figure 1:</strong> Comparative Elbow Method analysis across four dataset configurations</figcaption></figure><h4>Elbow Method Analysis Findings</h4><ul><li><strong>Income &amp; Spending (2D)</strong> — Graphs on the left: In the complete dataset, a clear elbow appears at K=5. However, when outliers are removed, we observe a significant secondary drop in inertia between K=5 and K=6. This suggests that removing extreme values unmasks a sixth distinct customer segment that was previously obscured by noise.</li><li><strong>Age, Income &amp; Spending (3D)</strong> — Graphs on the right: The introduction of the ‘Age’ dimension smoothens the curve, indicating a more complex data distribution. A slight bend is visible at K=6, suggesting that this is the optimal number of clusters.</li></ul><p><strong>Impact of Outliers</strong>: Across both feature sets, the “No Outliers” plots exhibit lower overall inertia. This suggests that removing outliers in “Annual Income” could lead to more compact clusters from a purely mathematical standpoint. However, whether this actually results in a more meaningful or stable segmentation remains to be seen; in the validation section, we will employ statistical metrics to quantify if this impact is truly significant or merely marginal, as previously suggested by the Mutual Information analysis.</p><h4>Candidate Selection for Validation</h4><p>We will carry forward the following settings for the validation phase:</p><ul><li>2D Features: K=5 (Original dataset) and K=6 (No outliers dataset);</li><li>3D Features: K= 6 (both Original &amp; No outliers dataset).</li></ul><h4>Agglomerative Clustering</h4><p><strong>Agglomerative Clustering</strong> is a hierarchical algorithm used to group data points, representing their relationships via a tree-like structure called a dendrogram.</p><p>It follows a bottom-up approach:</p><ul><li>It begins by treating every single data point as an individual cluster.</li><li>It then proceeds by iteratively merging the two closest clusters based on a specific distance metric.</li></ul><p>This process continues until all points are merged into a single large cluster.</p><p>The way the “distance” between clusters is calculated is defined by the linkage method. The four most common methods are:</p><ul><li><strong>Ward’s Method</strong>: Merges clusters that result in the minimum increase in total within-cluster variance;</li><li><strong>Single Linkage (Minimum Distance)</strong>: Defines the distance between two clusters as the minimum distance between any single point in the first cluster and any single point in the second;</li><li><strong>Complete Linkage (Maximum Distance)</strong>: Defines the distance as the maximum distance between any single point in the first cluster and any single point in the second;</li><li><strong>Average Linkage (Mean Distance)</strong>: Calculates the distance as the average distance between all possible pairs of points between two clusters.</li></ul><pre># Define a list for the linkage methods<br>LINKAGE_METHODS = [&#39;ward&#39;, &#39;complete&#39;, &#39;average&#39;, &#39;single&#39;]<br># Define variables that we use in the algorithms <br>dataset_configurations = [<br>    {&#39;dataset&#39;: X_IncomeSpending_all, &#39;label&#39;: &#39;Income-Spending (All Data)&#39;, &#39;heights&#39;: [8, 2, 1.5]},<br>    {&#39;dataset&#39;: X_IncomeSpending_no_outliers, &#39;label&#39;: &#39;Income-Spending (No Outliers)&#39;, &#39;heights&#39;: [8, 2, 1.5]},<br>    {&#39;dataset&#39;: X_AgeIncomeSpending_all, &#39;label&#39;: &#39;Age-Income-Spending (All Data)&#39;, &#39;heights&#39;: [7.5, 3.4, 2.1]},<br>    {&#39;dataset&#39;: X_AgeIncomeSpending_no_outliers, &#39;label&#39;: &#39;Age-Income-Spending (No Outliers)&#39;, &#39;heights&#39;: [7.6, 3.4, 2.1]}<br>]<br>fig, axes = plt.subplots(nrows=4, ncols=4, figsize=(28, 20))<br># Outer Loop: Iterates over Dataset Configurations<br>for row_idx, dc in enumerate(dataset_configurations):<br>    data_set = dc[&#39;dataset&#39;]<br>    data_label = dc[&#39;label&#39;]<br>    # Inner Loop: Iterates over Linkage Methods<br>    for col_idx, method in enumerate(LINKAGE_METHODS):<br>        # Linkage matrix<br>        Z = linkage(data_set, method=method)<br>        # Dendrogram generation<br>        ax_dendro = axes[row_idx, col_idx]<br>        dendrogram(Z, truncate_mode=&#39;lastp&#39;, ax=ax_dendro)<br>        # Add a horizontal threshold line <br>        if &#39;heights&#39; in dc and col_idx &lt; len(dc[&#39;heights&#39;]):<br>            h = dc[&#39;heights&#39;][col_idx]<br>            ax_dendro.axhline(y=h, color=&#39;r&#39;, linestyle=&#39;--&#39;, linewidth=1.5)<br>        title = f&#39;{data_label} - {method.capitalize()}&#39;<br>        ax_dendro.set_title(title, fontsize=12)<br>        ax_dendro.set_xlabel(&#39;Formed Clusters&#39;, fontsize=10)<br>        ax_dendro.set_ylabel(&#39;Distance&#39;, fontsize=10) <br>        ax_dendro.tick_params(axis=&#39;x&#39;, which=&#39;both&#39;, bottom=True, labelbottom=False)<br><br>fig.tight_layout()<br>plt.show()</pre><p>The output of the previous code is a grid of 16 dendrograms. Below, we present them using a different layout, organized configuration by configuration, along with their respective results.</p><figure><img alt="A grid of 4 dendrograms focused on the “Income and Spending” feature set. The visualization compares two linkage methods (Ward &amp; Complete): the first row represents the original dataset, while the second one the dataset after outlier removal." src="https://cdn-images-1.medium.com/max/532/1*AQdL_aMJr4juO9il8e4yug.png" /><figcaption>Figure 2: A grid of 4 dendrograms focused on the “Income and Spending” feature set. The visualization compares two linkage methods (Ward &amp; Complete): the first row represents the original dataset, while the second one the dataset after outlier removal.</figcaption></figure><p>Dataset with Incoming &amp; Spending Score Features:</p><ul><li><strong>Ward’s Method:</strong> At h=8, this method produces distinct and compact clusters. Comparing “All Data” to “No Outliers,” it is evident that outliers do not influence cluster stability;</li><li><strong>Complete Method:</strong> A cut at approximately h=2 yields 7 clusters. In this case as well, the presence of outliers does not alter the overall structure.</li></ul><figure><img alt="A grid of 4 dendrograms focused on the “Income and Spending” feature set. The visualization compares two linkage methods (Average &amp; Single): the first row represents the original dataset, while the second one the dataset after outlier removal." src="https://cdn-images-1.medium.com/max/539/1*VlRqAckwxSsOhBao4d3XMA.png" /><figcaption>Figure 3: A grid of 4 dendrograms focused on the “Income and Spending” feature set. The visualization compares two linkage methods (Average &amp; Single): the first row represents the original dataset, while the second one the dataset after outlier removal.</figcaption></figure><p>Dataset with Incoming &amp; Spending Score Features:</p><ul><li><strong>Average Method:</strong> The cut at approximately 1,5 highlights the impact of data cleaning; removing outliers reveals 5 clusters instead of 7, suggesting that this method is more sensitive to extreme values.</li><li><strong>Single Method:</strong> Exhibits a clear “chaining” effect. Most points merge at very low distance levels, making it unsuitable for capturing underlying spherical cluster shapes.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/541/1*rkHbtr6s6HMenDKk8tKrlA.png" /><figcaption>Figure 4: A grid of 4 dendrograms focused on the “Age, Income and Spending” feature set. The visualization compares two linkage methods (Ward &amp; Complete): the first row represents the original dataset, while the second one the dataset after outlier removal.</figcaption></figure><p>Dataset with Age, Incoming &amp; Spending Score Features:</p><ul><li><strong>Ward Method: </strong>Shows high stability at h=7.5 across both datasets. It remains the most robust method, maintaining a consistent cluster count regardless of the added dimension or noise.</li><li><strong>Complete Method:</strong> A cut at approximately h=3.3 reveals the impact of outlier removal, which unmasks 6 clusters instead of 5.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/535/1*OsKBcu3PUb1ZAi0G9dY0Bw.png" /><figcaption>Figure 5: A grid of 4 dendrograms focused on the “Age, Income and Spending” feature set. The visualization compares two linkage methods (Average &amp; Single): the first row represents the original dataset, while the second one the dataset after outlier removal.</figcaption></figure><p>Dataset with Age, Incoming &amp; Spending Score Features:</p><ul><li><strong>Average Method: </strong>At approximately h=2, this method produces 5 clusters. In this case, the outliers do not change the result.</li><li><strong>Single Method:</strong> The chaining effect is evident, confirming this method is not recommended for this dataset.</li></ul><h4>Candidate Selection for Validation</h4><p>We will carry forward the following settings for the validation phase:</p><ul><li>2D Features: Ward K=5; Complete K=7 for both Original and No outliers dataset; Average K=7 for original and 5 for No outliers dataset;</li><li>3D Features: Ward K=6; Average K=5 for both Original and No outliers dataset; Complete K=5 for original and 6 for No outliers dataset.</li></ul><h4>Gaussian Mixture Model (GMM)</h4><p><strong>A Gaussian Mixture Model (GMM) [4, 2] </strong>is a probabilistic clustering method that assumes the data is generated from a mixture of several Gaussian distributions, each representing a distinct cluster.<br>The algorithm follows the Expectation-Maximization (EM) framework, which iterates through two main steps until convergence:</p><ul><li><strong>E-step (Expectation)</strong>: Each data point is assigned a probability of belonging to each Gaussian cluster (soft assignment);</li><li><strong>M-step (Maximization)</strong>: The parameters of the Gaussians — specifically the mean, covariance, and mixing weight — are updated based on the probabilities calculated in the E-step.</li></ul><p>To determine the ideal number of clusters and model structure, two primary metrics are used:</p><ul><li><strong>Bayesian Information Criterion (BIC)</strong>;</li><li><strong>Akaike Information Criterion (AIC)</strong></li></ul><p>Both metrics estimate the quality of the model by balancing goodness-of-fit with model complexity; the lower the score, the better the model.</p><p>In the analysis below, a grid search is performed to systematically test various GMM configurations by varying the number of components and the covariance type (spherical, tied, diag, and full). The results are plotted to visualize how complexity affects the BIC and AIC scores, identifying the optimal cluster structure for each dataset.</p><pre>dataset_configurations = [<br>    {&#39;dataset&#39;: X_IncomeSpending_all,&#39;label&#39;: &#39;Income-Spending (All Data)&#39;},<br>    {&#39;dataset&#39;: X_AgeIncomeSpending_all,&#39;label&#39;: &#39;Age-Income-Spending (All Data)&#39;},<br>    {&#39;dataset&#39;: X_IncomeSpending_no_outliers,&#39;label&#39;: &#39;Income-Spending (No Outliers)&#39;},<br>    {&#39;dataset&#39;: X_AgeIncomeSpending_no_outliers,&#39;label&#39;: &#39;Age-Income-Spending (No Outliers)&#39;}<br>]<br># Parameter grid for GMM<br>param_grid = {<br>    &quot;n_components&quot;: range(3, 10),<br>    &quot;covariance_type&quot;: [&quot;spherical&quot;, &quot;tied&quot;, &quot;diag&quot;, &quot;full&quot;]<br>}<br># GridSearchCV is designed to MAXIMIZE a score. Since the best BIC/AIC is the <br># lowest value, we return the negative value so the algorithm can maximize it.<br>def gmm_bic_score(estimator, X):<br>    &quot;&quot;&quot;Returns the NEGATIVE BIC: allows GridSearchCV to find the minimum by maximizing.&quot;&quot;&quot;<br>    return -estimator.bic(X)<br>def gmm_aic_score(estimator, X):<br>    &quot;&quot;&quot;Returns the NEGATIVE AIC: allows GridSearchCV to find the minimum by maximizing.&quot;&quot;&quot;<br>    return -estimator.aic(X)<br># Set up a grid to accommodate all onfigurations<br>fig, axes = plt.subplots(2, 2, figsize=(22, 12))<br>axes = axes.flatten()<br>for i, dc in enumerate(dataset_configurations):<br>    ax = axes[i]<br>    data_set = dc[&#39;dataset&#39;]<br>    # The &#39;scoring&#39; parameter uses our negative function to correctly rank the models.<br>    grid_search = GridSearchCV(GaussianMixture(random_state=42, n_init=5), param_grid=param_grid, scoring=gmm_bic_score,<br>        #Determines the cross-validation splitting strategy: we use identical indices to force calculation on the full dataset                      <br>        cv=[(np.arange(len(data_set)), np.arange(len(data_set)))]<br>    )<br>    grid_search.fit(data_set)<br>    # Process results<br>    results = []<br>    # Loop through the grid results (which are currently stored as negative scores)<br>    for params, score in zip(grid_search.cv_results_[&#39;params&#39;], grid_search.cv_results_[&#39;mean_test_score&#39;]):<br>        # To display real (positive) AIC and BIC values, we fit the model briefly.<br>        # This is necessary because GridSearchCV only stores the primary &#39;scoring&#39; metric.<br>        model = GaussianMixture(**params, random_state=42, n_init=5).fit(data_set)<br>        results.append({<br>            **params,&#39;bic&#39;: model.bic(data_set),&#39;aic&#39;: model.aic(data_set)<br>        })<br>    df_result = pd.DataFrame(results)<br>    cov_types = df_result[&quot;covariance_type&quot;].unique()<br>    n_comps = sorted(df_result[&quot;n_components&quot;].unique())<br>    # Plotting<br>    x = np.arange(len(n_comps))<br>    width = 0.2 <br>    for j, cov in enumerate(cov_types):<br>        subset = df_result[df_result[&quot;covariance_type&quot;] == cov].sort_values(&quot;n_components&quot;)<br>        # Plot BIC (Bar)<br>        ax.bar(x + (j * width), subset[&quot;bic&quot;], width, label=f&quot;{cov} (BIC)&quot;, alpha=0.8)<br>        # Plot AIC (Points)<br>        ax.plot(x + (j * width), subset[&quot;aic&quot;], marker=&#39;o&#39;, linestyle=&#39;&#39;, markersize=4, color=&#39;black&#39;, alpha=0.5)<br>    ax.set_title(f&quot;BIC &amp; AIC Score: {dc[&#39;label&#39;]}\n(Black Points = AIC)&quot;, fontsize=14)<br>    ax.set_xticks(x + width * 1.5)<br>    ax.set_xticklabels(n_comps)<br>    ax.set_xlabel(&quot;Number of Clusters&quot;)<br>    ax.set_ylabel(&quot;Score (Lower is Better)&quot;)<br>    ax.legend(title=&quot;Covariance&quot;, fontsize=&#39;x-small&#39;, ncol=2)<br>    ax.grid(axis=&#39;y&#39;, linestyle=&#39;--&#39;, alpha=0.3)<br>plt.tight_layout()<br>plt.show()</pre><figure><img alt="Plot BIC and AIC analysis" src="https://cdn-images-1.medium.com/max/1024/1*qZ8-dtyszMC-a3Yr6hQkQw.png" /><figcaption><strong>Figure 6:</strong> Plot BIC and AIC analysis</figcaption></figure><p>The BIC analysis reveals a relatively flat profile across the tested range of clusters (K=3 to K=9), and the removal of outliers does not significantly alter this trend. This lack of a clear minimum suggests that a Gaussian Mixture Model might not be the most effective algorithm for capturing the underlying structure of this specific dataset.<br>Nevertheless, I have selected candidate configurations that balance the lowest AIC/BIC scores with model parsimony to avoid unnecessary complexity.</p><h4>Candidate Selection for Validation</h4><p>We will carry forward the following settings for the validation phase:</p><ul><li>2D Features (Income-Spending): K=5 and K=6 using Spherical and Diag covariance types for both versions of the dataset;</li><li>3D Features (Age-Income-Spending): K=5, K=6, and K=7 using Diag covariance for both versions of the dataset.</li></ul><h3>Conclusion &amp; Next Steps</h3><p>We have put our four dataset configurations to the test, uncovering how different algorithms react to noise and dimensionality. While <strong>K-Means</strong> and <strong>Ward’s linkage</strong> showed remarkable stability, the <strong>GMM</strong> analysis suggested a more nuanced underlying structure.</p><p>But which of these candidates truly represents our customer base?</p><p>In <strong>Part 3</strong>, we will move from parameter tuning to final validation. We will use statistical metrics to select the winning model and, most importantly, translate these clusters into <strong>actionable marketing strategies</strong>.</p><p><strong><em>Code Availability</em></strong><em>: The full GitHub repository will be shared in Part 3.</em></p><p><a href="https://medium.com/towards-artificial-intelligence/from-math-to-marketing-validating-clusters-and-extracting-actionable-insights-264d4443ee3b"><strong>Read Part 3</strong></a><strong>: Validation, Selection, and Visualization — Evaluating metrics and extracting actionable business insights →</strong></p><p><strong>References</strong></p><p>[1] Imad Dabbura, <a href="https://towardsdatascience.com/k-means-clustering-algorithm-applications-evaluation-methods-and-drawbacks-aa03e644b48a/">K-means Clustering: Algorithm, Applications, Evaluation Methods, and Drawbacks</a> (2018), <strong>Towards Data Science</strong></p><p>[2] <strong>Scikit-Learn Documentation (2024).</strong><a href="https://scikit-learn.org/stable/modules/clustering.html#k-means"> Kmeans</a>, <a href="https://scikit-learn.org/stable/auto_examples/mixture/plot_gmm_selection.html">Gaussian Mixture Model Selection</a>, <a href="https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html">GridSearchCV</a></p><p>[3] Mounica Kommajosyula, <a href="https://python.plainenglish.io/clustering-101-mastering-agglomerative-hierarchical-clustering-18752b7f4e6d">Clustering 101: Mastering Agglomerative Hierarchical Clustering</a> (2024), Python in Plain English</p><p>[4] Vivienne DiFrancesco,<a href="https://towardsdatascience.com/gaussian-mixture-models-for-clustering-3f62d0da675/"> Gaussian Mixture Models for Clustering</a> (2021), <strong>Towards Data Science</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4a6b244bb3c1" width="1" height="1" alt=""><hr><p><a href="https://pub.towardsai.net/3-algorithms-and-4-configurations-for-customer-segmentation-4a6b244bb3c1">3 Algorithms and 4 Configurations for Customer Segmentation</a> was originally published in <a href="https://pub.towardsai.net/">Towards AI</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Outlier and Feature Selection Dilemma: Preparing Data for Clustering]]></title>
            <link>https://pub.towardsai.net/the-outlier-and-feature-selection-dilemma-preparing-data-for-clustering-ebf228b5810c?source=rss-111017e80548------2</link>
            <guid isPermaLink="false">https://medium.com/p/ebf228b5810c</guid>
            <category><![CDATA[agglomerative-clustering]]></category>
            <category><![CDATA[gaussian-mixture-model]]></category>
            <category><![CDATA[clustering]]></category>
            <category><![CDATA[mutual-information]]></category>
            <category><![CDATA[k-means]]></category>
            <dc:creator><![CDATA[Mariangela Bonghi]]></dc:creator>
            <pubDate>Sun, 22 Feb 2026 17:01:01 GMT</pubDate>
            <atom:updated>2026-02-23T23:28:58.029Z</atom:updated>
            <content:encoded><![CDATA[<h4>Customer Segmentation Series</h4><h4>Part 1: Exploratory Data Analysis, handling outliers, and feature selection using Mutual Information</h4><p>by Mariangela Bonghi</p><figure><img alt="A custom data visualization created with Seaborn showing pair plots and box plots for the Mall Customer dataset" src="https://cdn-images-1.medium.com/max/420/1*NvKOkBbFjxF9voWZ0mC__A.png" /><figcaption><strong>Image by Author, generated via Seaborn and Paint.</strong></figcaption></figure><p>A short time ago, I attended a course on Unsupervised Learning. The concepts were fascinating, and I couldn’t wait to put all that new theory into practice. I decided to focus my attention on clustering, using the classic <a href="https://www.kaggle.com/datasets/shwetabh123/mall-customers"><strong>Kaggle</strong></a><strong> Mall Customer Dataset</strong> as my playground.</p><p><strong>The clustering process</strong> can be summarized into four steps (often iterative):</p><ul><li>Feature extraction and selection;</li><li>Algorithm selection and tuning;</li><li>Cluster validation;</li><li>Interpretation of results.</li></ul><p>When I started, I was looking for a “universal recipe” to apply to any dataset. I quickly discovered that such a recipe doesn’t exist. Instead, clustering is a field where every step requires a choice — some obvious, <br>some less so — guided by data knowledge, business logic, and analytical intuition. <br>While I didn’t find a magic formula, this experience helps me answer some questions: Do I need to consider all the features? Are there outliers, and how significantly will they warp my results? Should I try other algorithms instead of just K-means?</p><p>This guide is built to answer the very questions I had to solve for myself.</p><h4>The Roadmap</h4><p>Since the journey is extensive, I have divided this project into <strong>three specialized parts</strong> to ensure we cover every detail of the pipeline:</p><ul><li><strong>Part 1: EDA, outliers, and feature selection(You are here!) </strong>— We’ll start by exploring the data, handling outliers, and analyzing features.</li><li><strong>Part 2: Identifying optimal parameters for K-Means, GMM, and Linkage methods </strong>for different configurations of the dataset.</li><li><strong>Part 3: Validation, Selection, and Visualization — Evaluating metrics and extracting actionable business insights</strong></li></ul><h4>Let’s start from the basics: What is Clustering?</h4><p>In short, clustering is the branch of Unsupervised Learning that helps us find hidden patterns in our data, grouping observations into homogeneous clusters with similar characteristics.<br>Consider how powerful this is in marketing: if you truly understand your customers’ behaviors and traits, you can create hyper-targeted campaigns. You’ll know exactly how to communicate with each group, which channel to use, and which products or offers will resonate most with them.</p><h4>Exploratory Data Analysis (EDA)</h4><p>First, let’s import the libraries and take a look at our raw data.</p><pre># Import Libraries<br>import pandas as pd<br>import seaborn as sns<br>import matplotlib.pyplot as plt<br>import numpy as np<br>from IPython.display import display, HTML<br><br>from sklearn.preprocessing import StandardScaler<br>from sklearn.feature_selection import mutual_info_regression, mutual_info_classif<br>from sklearn.cluster import KMeans, AgglomerativeClustering<br>from scipy.cluster.hierarchy import linkage, dendrogram<br>from sklearn.model_selection import GridSearchCV<br>from sklearn.mixture import GaussianMixture<br>from sklearn.neighbors import NearestNeighbors<br>from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score<br># Load Data<br>df = pd.read_csv(&quot;Mall_Customers.csv&quot;)<br>df.head()</pre><figure><img alt="A table showing the first 5 rows of the Mall Customers dataset with columns: CustomerID, Gender, Age, Annual Income (k$), and Spending Score (1–100)." src="https://cdn-images-1.medium.com/max/504/1*V9NU28fi3FE2JnFdgquIww.png" /><figcaption><strong>Figure 1:</strong> <em>Initial preview of the Mall Customer dataset</em></figcaption></figure><p>There are four features: “Gender”, “Age”, “Annual Income”, and “Spending Score”.</p><p>“Customer ID&quot;: Serves only as an identifier and will be dropped from the dataset that we’ll use for the clustering.</p><pre>df.describe()</pre><figure><img alt="A table showing the summary statistics generated via df.describe()" src="https://cdn-images-1.medium.com/max/516/1*guVRf_11y7TZX5nGMy8R_g.png" /><figcaption><strong>Figure 2:</strong> <em>Summary statistics</em></figcaption></figure><ul><li>“Annual Income” ranges up to 137k, while “Age” only goes up to 70. This suggests that <strong>scaling </strong>is needed.</li><li>The jump from the 75th percentile (78k) to the maximum (137k) in “Annual Income” suggests <strong>outliers</strong>.</li></ul><p>Before analyzing individual features, we must ensure our dataset is structurally sound.</p><pre># Check for missing values<br>print(&quot;Missing Values\n&quot;)<br>print(df.isna().sum())<br><br># Check for duplicated rows<br>print(&quot;\nDuplicated Rows\n&quot;)<br>print(df.duplicated().sum())</pre><figure><img alt="The figure shows that the dataset has no missing values and no duplicated rows" src="https://cdn-images-1.medium.com/max/243/1*_AAEwNDpD9QdVDgXvx84mA.png" /><figcaption><strong>Figure 3:</strong> <em>Verification of missing values and duplicated rows.</em></figcaption></figure><p>As shown above, a quick check reveals a perfectly “clean” dataset with no missing values or duplicates.</p><p>To visualize the relationships between features, we use a pair plot.</p><pre># Remove unnecessary CustomerID column<br>df_features= df.copy()<br>df_features.drop([&quot;CustomerID&quot;],axis=1,inplace=True)<br># Plot pairwise relationships in the dataset<br>custom_palette = {&#39;Male&#39;: &#39;#4C72B0&#39;, &#39;Female&#39;: &#39;#DD8452&#39;}<br>sns.pairplot(df_features,hue=&#39;Gender&#39;,palette=custom_palette)<br>plt.show()</pre><figure><img alt="A 3x3 grid of Seaborn pair plots visualizing a customer dataset. The variables analyzed are Age, Annual Income (k$), and Spending Score (1–100). The data points are color-coded by Gender: blue for Male and orange for Female." src="https://cdn-images-1.medium.com/max/856/1*dHOEdOC-nvk6d7gWwh2b-w.png" /><figcaption><strong>Figure 4:</strong>Pairwise relationships and distributions of Age, Annual Income, and Spending Score, categorized by Gender</figcaption></figure><p><strong>Key insights</strong></p><ul><li><strong>Spending Score vs Annual Income (Row 3, Column 2):</strong><br>This plot is the most informative for clustering purposes. It reveals five distinct clusters forming a clear pattern. This visual evidence strongly suggests that a K-Means algorithm with K=5 would likely be optimal for this dataset when focusing on these two variables.</li><li><strong>Spending Score vs Age (Row 3, Column 1): </strong><br>High spending scores are notably concentrated among younger customers. Age appears to be a significant factor for isolating high-value segments.</li><li><strong>The role of Gender (Color: Orange = Female, Blue = Male): </strong><br>Observing the various plots, Gender doesn’t appear to be a variable that makes a clear, stark difference in the overall cluster formation. However, the Age vs. Spending Score plot suggests a slight prevalence of male customers (blue) in the low-spending segments.</li></ul><h4>Detecting and handling outliers</h4><p>Next, it’s time to detect and handle outliers: we’ll use the <strong>IQR (Interquartile Range) method</strong>. [1,2,3]</p><p>The IQR method involves the following steps:</p><ul><li>Calculate the first quartile (Q1) and the third quartile (Q3): these represent the thresholds below which 25% and 75% of the data fall, respectively.</li><li>Calculate the difference Q3 — Q1: this is what we call the <strong>Interquartile Range</strong> (IQR) and identifies the central part of the dataset.</li><li>Establish boundaries beyond this range — typically Q1–1.5 times IQR and Q3 + 1.5 times IQR: data points falling outside these boundaries are defined as outliers.</li></ul><p>We can visualize them, if any, with the boxplot:</p><pre># Visualizing data distribution and potential outliers using boxplots<br>fig, axes = plt.subplots(nrows=1,ncols=3,figsize=(15,5))<br>sns.boxplot(y=df_features[&#39;Age&#39;], ax=axes[0],color=&quot;#789fb3&quot;)<br>sns.boxplot(y=df_features[&#39;Annual Income (k$)&#39;], ax=axes[1],color=&quot;#789fb3&quot;)<br>sns.boxplot(y=df_features[&#39;Spending Score (1-100)&#39;], ax=axes[2],color=&quot;#789fb3&quot;)<br>plt.show()</pre><figure><img alt="Boxplot for Age, Annual Income, and Spending Score using Seaborn" src="https://cdn-images-1.medium.com/max/1024/1*vq9DPe_cw9mEv4P0a8CwXA.png" /><figcaption><strong>Figure 6</strong>: Boxplot for Age, Annual Income, and Spending Score using Seaborn</figcaption></figure><p>As shown in the box plots, there is only one visible outlier in the ‘Annual Income (k$)’ column. We will now determine the exact number of outliers, as the visual representation may hide multiple overlapping data points. To do this, we will find how many points there are above the Q3 + 1.5 times the IQR.</p><pre># Evaluation of the first and third quartiles<br>Q1 = np.percentile(df_features[&#39;Annual Income (k$)&#39;], 25, method=&#39;midpoint&#39;)<br>Q3 = np.percentile(df_features[&#39;Annual Income (k$)&#39;], 75, method=&#39;midpoint&#39;)<br># IQR is calculated as the difference between Q3 and Q1<br>IQR = Q3 - Q1<br># Upper bound: Q3+1.5∗IQR<br>upper_bound = Q3 + 1.5 * IQR<br># Outlier Count<br>outlier = df_features[(df_features[&#39;Annual Income (k$)&#39;] &gt;= upper_bound)]<br>print(f&quot;Number of Outliers for the column Annual Income (k$): {len(outlier)}&quot;)   </pre><pre>Number of Outliers for the column Annual Income (k$): 2</pre><pre># Outlier removal: filter the dataframe to keep only records within the upper bound<br>upper_array = np.where(df_features[&#39;Annual Income (k$)&#39;] &gt;= upper_bound)[0]<br>df_features_no_outliers=df_features.drop(index=upper_array)<br># Result verification<br>print(&quot;Old Shape: &quot;, df_features.shape)<br>print(&quot;New Shape: &quot;, df_features_no_outliers.shape)</pre><pre>Old Shape:  (200, 4)<br>New Shape:  (198, 4)</pre><p>Now we have:</p><pre>- df_features: <br>our original dataset without “Customer ID” (200 rows)<br>- df_features_no_outliers: <br>our original dataset without “Customer ID” and without outliers (198 rows)</pre><h4>Feature Selection via Mutual Information</h4><p>Before proceeding with the clustering phase, it is essential to determine which features provide the most value to our model. In our dataset, there are four features: Spending Score, Annual Income, Age, and Gender. We need to identify which ones contribute to meaningful patterns and which ones act like noise.</p><p>To answer this, I employed <strong>Mutual Information (MI) [4,5,6]</strong>. Like correlation, MI measures the relationship between two variables. However, while standard correlation coefficients (such as Pearson’s) are limited to detecting linear dependencies, MI captures any kind of statistical dependency, whether linear or non-linear.</p><p><strong>How it works</strong><br>Mathematically, given two variables X and Y, MI quantifies how much knowing the value of X reduces our uncertainty about Y.</p><ul><li>Low MI Value: Indicates that the variables are independent. Knowing the value of X provides no information about Y.</li><li>High MI Value: Indicates a significant degree of dependency. Knowing X allows us to infer information about Y with greater confidence.</li></ul><p>In the context of clustering, we prioritize features with high MI scores for two practical reasons:</p><p><strong>Noise Reduction</strong>: A feature with a low MI score is statistically independent of the others. In clustering, such a variable behaves like “noise” — it scatters data points randomly, making it harder for algorithms like K-Means to find cohesive groups.</p><p><strong>Pattern Recognition</strong>: A high MI score means the variable “participates” in the dataset’s internal logic. For example, if Annual Income has a high MI with Spending Score, they work together to define distinct customer segments (e.g., “high earners who spend little”).</p><p><strong>Practical Application</strong></p><p>In our analysis, the function <strong>make_mi_scores </strong>treats each feature as a target and calculates its relationship with all other variables. By summing these scores, we measure the “centrality” of a feature. A high cumulative MI indicates a variable that shares significant information with the rest of the system, making it essential for defining stable clusters. We distinguish between data types to ensure accuracy:</p><ul><li>For the Gender feature (categorical/discrete), we utilized <strong>mutual_info_classif</strong>;</li><li>For continuous features (Age, Income, Score), we employed <strong>mutual_info_regression</strong>.</li></ul><p>This distinction is crucial because the underlying entropy estimation — the mathematical foundation of MI — differs when dealing with discrete labels versus continuous distributions.</p><p><strong>Comparison: Raw Data vs. No Outliers</strong><br>Finally, by comparing mi_scores with mi_scores_no_outliers, we can observe if the perceived importance of a feature is driven by anomalous data points or if it represents a robust statistical trend. This dual-check ensures that our feature selection is not biased by extreme values in “Annual Income”.</p><pre># Label encoding for Gender<br>df_features[&#39;Gender&#39;], _ = df_features[&#39;Gender&#39;].factorize()<br>df_features_no_outliers[&#39;Gender&#39;], _ = df_features_no_outliers[&#39;Gender&#39;].factorize()<br>def make_mi_scores(df_input):<br>    # Initialize dictionary to store the total MI (centrality) for each feature<br>    result = {}<br>    for col in df_input:<br>        # Define &#39;col&#39; as target (y) and all other columns as predictors (X)<br>        X = df_input.copy()<br>        y = X.pop(col)<br>        # Create a boolean mask for discrete predictors remaining in X<br>        is_discrete = (X.columns == &#39;Gender&#39;)<br>        # Use classification MI for categorical target (Gender), <br>        # regression MI for continuous targets (Age, Income, Spending Score)<br>        if col==&#39;Gender&#39;:<br>            mi_scores = mutual_info_classif(X, y, discrete_features=is_discrete, random_state=42)<br>        else:        <br>            mi_scores = mutual_info_regression(X, y, discrete_features=is_discrete, random_state=42)<br>        result[col]=sum(mi_scores)<br>    return result<br><br>mi_scores = make_mi_scores(df_features)<br>mi_scores_no_outliers = make_mi_scores(df_features_no_outliers)<br><br># Convert dictionaries to DataFrames for easier manipulation and sorting<br>scores_df = pd.DataFrame.from_dict(mi_scores, orient=&#39;index&#39;, columns=[&#39;MI Score&#39;])<br>scores_df_no_outliers = pd.DataFrame.from_dict(mi_scores_no_outliers, orient=&#39;index&#39;, columns=[&#39;MI Score&#39;])<br># Sort scores in descending order to highlight the most influential features<br>scores_df = scores_df.sort_values(by=&quot;MI Score&quot;, ascending=False)<br>scores_df_no_outliers = scores_df_no_outliers.sort_values(by=&quot;MI Score&quot;, ascending=False)<br># Merge the two results into a single table<br>score_union = pd.concat([scores_df, scores_df_no_outliers], axis=1)<br># Rename columns<br>score_union.columns = [&#39;With Outliers&#39;, &#39;Without Outliers&#39;]<br># Apply a color gradient<br>score_union.style.background_gradient(cmap=&#39;YlGnBu&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/381/1*fjpvLhN35rqx5pnhc2_3Ag.png" /><figcaption><strong>Figure 7</strong>: MI values for the original and cleaned dataset</figcaption></figure><p><strong>MI Analysis Results:</strong></p><ul><li>The marginal difference in MI scores between the two datasets suggests that the underlying structure of the data is stable, and the identified outliers do not significantly distort the global relationship between features.</li><li><strong>Dominant Features</strong>: The Spending Score remains the most informative feature in both scenarios. While its score slightly decreased (from 0.794 to 0.785), it maintains the highest MI, confirming it will be the primary driver for our clusters.</li><li><strong>Income Stability</strong>: Annual Income shows remarkable stability, with its MI score virtually unchanged (from 0.627 to 0.625). This confirms that the relationship between income and the other features is robust and not dependent on anomalous data points.</li><li><strong>Age Dynamics</strong>: Interestingly, Age is the feature that benefited most from the cleaning process, with its MI score increasing from 0.513 to 0.560. However, this increment is still relatively minor and does not fundamentally alter the feature’s importance ranking.</li><li><strong>The Gender Decision</strong>: In both datasets, Gender shows an extremely low MI score (below 0.10). This statistically supports our earlier hypothesis from the pairplot: gender does not share significant information with the other features. Including it in the clustering process might add more noise than value.</li></ul><h4>Dataset Configurations for Model Evaluation</h4><p>Based on the previous Mutual Information and Pairplot analyses, the most promising configuration appears to be the subset including Age, Annual Income, and Spending Score with Gender removed.</p><p>For educational and comparative purposes, we will not limit our study to a single scenario. Instead, we will evaluate our clustering algorithms across four different dataset configurations:</p><ul><li><strong>2D Scenario</strong> (Annual Income, Spending Score): tested both with and without outliers;</li><li><strong>3D Scenario</strong> (Age, Annual Income, Spending Score): tested both with and without outliers.</li></ul><p>This multi-scenario approach allows us to empirically observe how the inclusion of specific features or the presence of outliers affects the performance, shape, and stability of the resulting clusters.</p><h4>Feature Scaling: StandardScaler</h4><p>Before clustering, it is essential to normalize the data to ensure that all features contribute equally to the Euclidean distance calculation. This prevents variables with larger scales from disproportionately dominating the distance metric.</p><p>We have opted for the <strong>StandardScaler method [7,8]</strong>; given that only two outliers were identified, their impact is not significant enough to negatively bias the scaling process.<br>The scaling is applied to both versions of the dataset (with and without outliers) as we intend to analyze two distinct hypotheses: one retaining the original data distribution and the other refined by outlier removal, allowing for a comparative evaluation of the clustering results.</p><pre>std_scaler = StandardScaler()<br># 2D Scenario: Annual Income and Spending Score <br># Original dataset<br>X_IncomeSpending_all = std_scaler.fit_transform(df_features.loc[:, [ &#39;Annual Income (k$)&#39;, &#39;Spending Score (1-100)&#39;]].values)<br># Dataset without outliers<br>X_IncomeSpending_no_outliers = std_scaler.fit_transform(df_features_no_outliers.loc[:, [ &#39;Annual Income (k$)&#39;, &#39;Spending Score (1-100)&#39;]].values)<br># 3D Scenario: Age, Annual Income, and Spending Score <br># Original dataset<br>X_AgeIncomeSpending_all = std_scaler.fit_transform(df_features.loc[:, [&#39;Age&#39;, &#39;Annual Income (k$)&#39;, &#39;Spending Score (1-100)&#39;]].values)<br># Dataset without outliers<br>X_AgeIncomeSpending_no_outliers = std_scaler.fit_transform(df_features_no_outliers.loc[:, [&#39;Age&#39;, &#39;Annual Income (k$)&#39;, &#39;Spending Score (1-100)&#39;]].values)</pre><h3>Conclusion &amp; Next Steps</h3><p>We have successfully laid the groundwork. By combining visual EDA with statistical <strong>Mutual Information</strong> analysis, we have moved from raw data to a refined set of four dataset configurations.</p><p>Rather than simply looking for the easiest solution, we are prepared to test multiple <strong>“experimental paths”</strong> — comparing how algorithms behave across different feature sets and evaluating the true impact of outliers on the final segments.</p><p>In <strong>Part 2</strong>, we move from analysis to action. We will <strong>identify optimal parameters for K-Means, GMM, and Linkage methods </strong>for our four dataset configurations.</p><p><strong><em>Code Availability</em></strong><em>: The full GitHub repository will be shared in Part 3.</em></p><p><a href="https://medium.com/towards-artificial-intelligence/3-algorithms-and-4-configurations-for-customer-segmentation-4a6b244bb3c1"><strong>Read Part 2</strong></a><strong>: Identifying optimal parameters for K-Means, GMM, and Linkage methods →</strong></p><p><strong>References</strong></p><p><strong>[1] Penn State University, </strong><em>Identifying Outliers (2023),</em> STAT 200: Elementary Statistics. <a href="https://online.stat.psu.edu/stat200/lesson/3/3.2">Online Resource</a></p><p><strong>[2] GeeksforGeeks, </strong><em>Detect and Remove the Outliers using Python(2023).</em> <a href="https://www.geeksforgeeks.org/data-science/detect-and-remove-the-outliers-using-python/">Technical Tutorial</a></p><p><strong>[3] A. McDonald, </strong>Creating Boxplots with the Seaborn Python Library (2022), <strong>Towards Data Science</strong>. <a href="https://towardsdatascience.com/creating-boxplots-with-the-seaborn-python-library-f0c20f09bd57/">Article Link</a></p><p><strong>[4] Wikipedia,</strong> <em>Mutual Information (2024).</em> <a href="https://en.wikipedia.org/wiki/Mutual_information">General Reference</a></p><p><strong>[5] R. Holbrook, Mutual Information</strong> (2021). <a href="https://www.kaggle.com/code/ryanholbrook/mutual-information">Kaggle Course</a></p><p><strong>[6] Scikit-Learn Documentation (2024),</strong> <a href="https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.mutual_info_regression.html"><em>mutual_info_regression</em></a><em> &amp; </em><a href="https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.mutual_info_classif.html"><em>mutual_info_classif</em></a></p><p><strong>[7] S. Sharma, </strong><em>Scaling Numerical Data Explained: A Visual Guide (2020), </em><strong>Towards Data Science. </strong><a href="https://towardsdatascience.com/scaling-numerical-data-explained-a-visual-guide-with-code-examples-for-beginners-11676cdb45cb/">Article Link</a></p><p><strong>[8] Scikit-Learn.</strong> <em>Comparing different clustering algorithms on toy datasets (2024). </em><a href="https://scikit-learn.org/stable/auto_examples/cluster/plot_cluster_comparison.html">Documentation Example</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ebf228b5810c" width="1" height="1" alt=""><hr><p><a href="https://pub.towardsai.net/the-outlier-and-feature-selection-dilemma-preparing-data-for-clustering-ebf228b5810c">The Outlier and Feature Selection Dilemma: Preparing Data for Clustering</a> was originally published in <a href="https://pub.towardsai.net/">Towards AI</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>