From 585937d88bddde581abb8d596eda7233356c9ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maha=20Alshehri=C2=A0?= Date: Sun, 21 Sep 2025 20:51:32 +1000 Subject: [PATCH] implement user segmentation clustering model Built K-Means clustering using RFM features to group users for segmentation-based recommendations. --- .gitignore | 3 + .vscode/settings.json | 5 + ...rediction_based_preferences_features.ipynb | 512 ++++++++++++++++++ 3 files changed, 520 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 ML/Smart Cart Maha/smart_cart_prediction_based_preferences_features.ipynb diff --git a/.gitignore b/.gitignore index e823f743..7ab8e872 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ Backend/.env Frontend/node_modules Frontend/.expo Scrapping/Australia_GroceriesScraper/configuration.ini +*.csv +*.png +*.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..a8c20032 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda", + "python-envs.pythonProjects": [] +} \ No newline at end of file diff --git a/ML/Smart Cart Maha/smart_cart_prediction_based_preferences_features.ipynb b/ML/Smart Cart Maha/smart_cart_prediction_based_preferences_features.ipynb new file mode 100644 index 00000000..6881a980 --- /dev/null +++ b/ML/Smart Cart Maha/smart_cart_prediction_based_preferences_features.ipynb @@ -0,0 +1,512 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1f5fe132", + "metadata": {}, + "source": [ + "## Add Smart Cart Features \n", + " Objective: \n", + "
\n", + "Add compute recency/frequency/budget alignment, behavioral features, and product relationships." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3726c2be", + "metadata": {}, + "outputs": [], + "source": [ + "# Feature Engineering\n", + "import pandas as pd\n", + "import numpy as np\n", + "from datetime import datetime" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "b1b6c357", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.read_csv('data/all_features_preference_features.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e88e686f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 1338619 entries, 0 to 1338618\n", + "Data columns (total 25 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 Unnamed: 0.2 1338619 non-null int64 \n", + " 1 Unnamed: 0.1 1338619 non-null int64 \n", + " 2 Unnamed: 0 1338619 non-null int64 \n", + " 3 transaction_id 1338619 non-null object \n", + " 4 user_id 1338619 non-null object \n", + " 5 product_code 1338619 non-null int64 \n", + " 6 category 1338619 non-null object \n", + " 7 item_name 1338619 non-null object \n", + " 8 discount_percentage 1338619 non-null float64\n", + " 9 transaction_date 1338619 non-null object \n", + " 10 transaction_price 1338619 non-null float64\n", + " 11 age_group 1338619 non-null object \n", + " 12 gender 1338619 non-null object \n", + " 13 income_bracket 1338619 non-null object \n", + " 14 customer_type 1338619 non-null object \n", + " 15 state 1338619 non-null object \n", + " 16 month 1338619 non-null int64 \n", + " 17 seasonal_factor 1338619 non-null float64\n", + " 18 adjusted_spend 1338619 non-null float64\n", + " 19 promotion_applied 1338619 non-null int64 \n", + " 20 discount_amount 1338619 non-null float64\n", + " 21 final_spend 1338619 non-null float64\n", + " 22 recency_days 1338619 non-null int64 \n", + " 23 freq_30d 1338619 non-null float64\n", + " 24 budget_alignment 1338619 non-null float64\n", + "dtypes: float64(8), int64(7), object(10)\n", + "memory usage: 255.3+ MB\n" + ] + } + ], + "source": [ + "df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "635b2286", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1338619" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "a9239589", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total transactions: 1338619\n", + "Training transactions: 1070877\n", + "Testing transactions: 267742\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "from sklearn.decomposition import TruncatedSVD\n", + "from scipy.sparse import csr_matrix\n", + "\n", + "# Assuming your DataFrame is named 'df' and is already loaded.\n", + "# It should have columns: 'user_id', 'product_code', and a value column for interactions.\n", + "# If 'transaction_price' exists, use it. Otherwise, a simple 'interaction' column of 1s is fine.\n", + "# For example: df['interaction'] = 1.0\n", + "\n", + "# Split data into training and testing for each user\n", + "train_df = pd.DataFrame(columns=df.columns)\n", + "test_df = pd.DataFrame(columns=df.columns)\n", + "\n", + "for user in df['user_id'].unique():\n", + " user_data = df[df['user_id'] == user].copy()\n", + " split_point = int(len(user_data) * 0.8) # 80% for training\n", + " train_df = pd.concat([train_df, user_data.iloc[:split_point]])\n", + " test_df = pd.concat([test_df, user_data.iloc[split_point:]])\n", + "\n", + "# Create the user-item matrix from the training data\n", + "# This matrix will be the input for our recommendation model.\n", + "train_matrix = train_df.pivot_table(\n", + " index='user_id',\n", + " columns='product_code',\n", + " values=df.columns[-1], # Use the last column as the interaction value\n", + " aggfunc='sum'\n", + ").fillna(0)\n", + "\n", + "print(f\"Total transactions: {len(df)}\")\n", + "print(f\"Training transactions: {len(train_df)}\")\n", + "print(f\"Testing transactions: {len(test_df)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "76b7f75f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
TruncatedSVD(n_components=50, random_state=42)
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.
" + ], + "text/plain": [ + "TruncatedSVD(n_components=50, random_state=42)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Align columns to ensure the model sees all products from the original dataset\n", + "full_product_codes = df['product_code'].unique()\n", + "train_matrix = train_matrix.reindex(columns=full_product_codes, fill_value=0)\n", + "train_matrix_sparse = csr_matrix(train_matrix.values)\n", + "\n", + "# Initialize and train the SVD model\n", + "svd = TruncatedSVD(n_components=50, random_state=42) # n_components is a tunable hyperparameter\n", + "svd.fit(train_matrix_sparse)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "d5867912", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Precision@5 for User 'user_1': 0.00 (0%)\n", + "Precision@5 for User 'user_10': 0.00 (0%)\n", + "Precision@5 for User 'user_11': 0.00 (0%)\n", + "Precision@5 for User 'user_12': 0.00 (0%)\n", + "Precision@5 for User 'user_13': 0.00 (0%)\n", + "Precision@5 for User 'user_14': 0.00 (0%)\n", + "Precision@5 for User 'user_15': 0.00 (0%)\n", + "Precision@5 for User 'user_16': 0.00 (0%)\n", + "Precision@5 for User 'user_17': 0.00 (0%)\n", + "Precision@5 for User 'user_18': 0.00 (0%)\n", + "Precision@5 for User 'user_19': 0.00 (0%)\n", + "Precision@5 for User 'user_2': 0.00 (0%)\n", + "Precision@5 for User 'user_20': 0.00 (0%)\n", + "Precision@5 for User 'user_21': 0.00 (0%)\n", + "Precision@5 for User 'user_22': 0.00 (0%)\n", + "Precision@5 for User 'user_23': 0.00 (0%)\n", + "Precision@5 for User 'user_24': 0.00 (0%)\n", + "Precision@5 for User 'user_25': 0.00 (0%)\n", + "Precision@5 for User 'user_26': 0.40 (40%)\n", + "Precision@5 for User 'user_27': 0.00 (0%)\n", + "Precision@5 for User 'user_28': 0.00 (0%)\n", + "Precision@5 for User 'user_29': 0.00 (0%)\n", + "Precision@5 for User 'user_3': 0.00 (0%)\n", + "Precision@5 for User 'user_30': 0.00 (0%)\n", + "Precision@5 for User 'user_31': 0.00 (0%)\n", + "Precision@5 for User 'user_32': 0.00 (0%)\n", + "Precision@5 for User 'user_33': 0.00 (0%)\n", + "Precision@5 for User 'user_34': 0.20 (20%)\n", + "Precision@5 for User 'user_35': 0.00 (0%)\n", + "Precision@5 for User 'user_36': 0.00 (0%)\n", + "Precision@5 for User 'user_37': 0.00 (0%)\n", + "Precision@5 for User 'user_38': 0.00 (0%)\n", + "Precision@5 for User 'user_39': 0.00 (0%)\n", + "Precision@5 for User 'user_4': 0.00 (0%)\n", + "Precision@5 for User 'user_40': 0.00 (0%)\n", + "Precision@5 for User 'user_41': 0.00 (0%)\n", + "Precision@5 for User 'user_42': 0.00 (0%)\n", + "Precision@5 for User 'user_43': 0.00 (0%)\n", + "Precision@5 for User 'user_44': 0.00 (0%)\n", + "Precision@5 for User 'user_45': 0.00 (0%)\n", + "Precision@5 for User 'user_46': 0.00 (0%)\n", + "Precision@5 for User 'user_47': 0.00 (0%)\n", + "Precision@5 for User 'user_48': 0.00 (0%)\n", + "Precision@5 for User 'user_49': 0.00 (0%)\n", + "Precision@5 for User 'user_5': 0.20 (20%)\n", + "Precision@5 for User 'user_50': 0.00 (0%)\n", + "Precision@5 for User 'user_6': 0.00 (0%)\n", + "Precision@5 for User 'user_7': 0.20 (20%)\n", + "Precision@5 for User 'user_8': 0.00 (0%)\n", + "Precision@5 for User 'user_9': 0.00 (0%)\n", + "\n", + "Overall Mean Precision@5: 0.02 (2%)\n" + ] + } + ], + "source": [ + "def precision_at_k(recommended_items, relevant_items, k):\n", + " \"\"\"Calculates Precision@K.\"\"\"\n", + " if not relevant_items:\n", + " return 0.0\n", + " \n", + " recommended_set = set(recommended_items[:k])\n", + " relevant_set = set(relevant_items)\n", + " \n", + " hits = len(recommended_set.intersection(relevant_set))\n", + " return hits / k if k > 0 else 0.0\n", + "\n", + "k = 5 # Number of top recommendations to consider\n", + "user_precision_scores = {}\n", + "relevant_items_per_user = test_df.groupby('user_id')['product_code'].apply(list).to_dict()\n", + "\n", + "for user_id in relevant_items_per_user:\n", + " # Skip users not present in the training data\n", + " if user_id not in train_matrix.index:\n", + " continue\n", + "\n", + " # Get the user's vector from the training matrix\n", + " user_row = train_matrix.loc[user_id].values.reshape(1, -1)\n", + " \n", + " # Make predictions for this user\n", + " predicted_scores_user = svd.inverse_transform(svd.transform(user_row))\n", + " predicted_scores_series = pd.Series(\n", + " predicted_scores_user.flatten(),\n", + " index=train_matrix.columns\n", + " )\n", + " \n", + " # Exclude items the user has already seen to avoid recommending them again\n", + " seen_items = list(train_df[train_df['user_id'] == user_id]['product_code'].unique())\n", + " predicted_scores_filtered = predicted_scores_series[~predicted_scores_series.index.isin(seen_items)]\n", + " \n", + " # Get the top K recommended items\n", + " recommended_items = predicted_scores_filtered.sort_values(ascending=False).head(k).index.tolist()\n", + " \n", + " # Get the actual items the user interacted with in the test set (the ground truth)\n", + " relevant_items = relevant_items_per_user[user_id]\n", + " \n", + " # Calculate and store the precision score for this user\n", + " score = precision_at_k(recommended_items, relevant_items, k)\n", + " user_precision_scores[user_id] = score\n", + " print(f\"Precision@{k} for User '{user_id}': {score:.2f} ({score*100:.0f}%)\")\n", + "\n", + "# Calculate and print the overall average precision\n", + "if user_precision_scores:\n", + " overall_precision = np.mean(list(user_precision_scores.values()))\n", + " print(f\"\\nOverall Mean Precision@{k}: {overall_precision:.2f} ({overall_precision*100:.0f}%)\")\n", + "else:\n", + " print(\"No users found in the test set to evaluate.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "9e1e7986", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\threadpoolctl.py:1010: RuntimeWarning: \n", + "Found Intel OpenMP ('libiomp') and LLVM OpenMP ('libomp') loaded at\n", + "the same time. Both libraries are known to be incompatible and this\n", + "can cause random crashes or deadlocks on Linux when loaded in the\n", + "same Python program.\n", + "Using threadpoolctl may cause crashes or deadlocks. For more\n", + "information and possible workarounds, please see\n", + " https://github.com/joblib/threadpoolctl/blob/master/multiple_openmp.md\n", + "\n", + " warnings.warn(msg, RuntimeWarning)\n" + ] + }, + { + "data": { + "image/png": "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GHz44YdwdXVFkyZNsHTpUpSVleGtt96Ck5MTPD09sXHjxgrXT0hIQHBwMBQKBTp06IDDhw/rHI+NjcWIESNga2sLV1dXvPTSS8jJydEeHzhwIGbNmoU5c+agadOmGDZsWKWfQ6PRYOnSpfD09IRcLkfnzp2xd+9e7XFBEBAVFYWlS5dCEAQsXry40uu89tprEAQBp0+fxrPPPos2bdqgffv2mDdvHk6ePFnpOZX1vMTExEAQBKSmpgIA0tLSMGrUKDg6OsLGxgbt27fHH3/8gdTUVISEhAAAHB0dIQgCpkyZov1MoaGh8PX1hZWVFQIDA7Fjx44K77tnzx5069YNcrkcx44dw/nz5xESEgI7OzvY29ujW7duOHv2bKW1A8D27dsxdOhQKBSKSo9HRERg0KBBmDZtGjZs2AAzs7/+6B81ahR+/fVXFBUVVXl9osaG4YbIgMlkMnz44Yf4/PPPcf369ce6VkREBG7evIkjR47gk08+waJFi/DUU0/B0dERp06dwiuvvIL/+7//q/A+b731FubPn49z584hKCgIo0aNwu3btwEAeXl5GDRoELp06YKzZ89i7969yMrKwrhx43Su8d1338HS0hLHjx/HunXrKq1v1apVWLlyJT7++GNcuHABw4YNw9NPP43ExEQAQEZGBtq3b4/58+cjIyMDb775ZoVr5ObmYu/evZg5cyZsbGwqHP/7bZramDlzJkpKSnDkyBFcvHgRK1asgK2tLby8vLBz504AwOXLl5GRkYFVq1YBAEJDQ7F582asW7cOcXFxmDt3LiZOnFghIC5YsADLly/HpUuX0KlTJ0yYMAGenp44c+YMoqKisGDBAlhYWFRZ29GjR9G9e/dKj4WFhWHkyJH497//jRUrVlQ43r17d5SVleHUqVN1/dEQGR69rT9ORLUyefJkcfTo0aIoimLv3r3FqVOniqIoimFhYeLD/+suWrRIDAwM1Dn3008/Fb29vXWu5e3tLarVau0+f39/sV+/ftrXZWVloo2Njbht2zZRFEUxJSVFBCAuX75c26a0tFT09PQUV6xYIYqiKC5btkx84okndN47PT1dBCBevnxZFEVRHDBggNilS5dHfl4PDw/xgw8+0NnXo0cP8bXXXtO+DgwMFBctWlTlNU6dOiUCEH/++edHvh8AMSwsTBRFUTx48KAIQLxz5472+Llz50QAYkpKiiiKotixY0dx8eLFlV6rsvOLi4tFa2tr8cSJEzptp02bJo4fP17nvF27dum0sbOzEzdt2vTIz/CAg4ODuHnzZp19GzduFGUymSiTycT33nuv2vMdHR1r9X5Eho49N0SNwIoVK/Ddd9/h0qVLdb5G+/btdW5HuLq6omPHjtrXMpkMzs7OyM7O1jkvKChI++/m5ubo3r27to7z58/j4MGDsLW11W5t27YFUD4+5oFu3bpVW5tKpcLNmzfRp08fnf19+vSp1WcWRbHGbWvrjTfewPvvv48+ffpg0aJFuHDhQrXtk5KScPfuXQwdOlTn57N582adnw2ACr0u8+bNw/Tp0zFkyBAsX768Qvu/KyoqqvSWlJWVFYYOHYoNGzZU+3O0srLC3bt3q30PosaE4YaoEejfvz+GDRuGhQsXVjhmZmZW4Uu9tLS0Qru/39YQBKHSfRqNpsZ1FRQUYNSoUYiJidHZEhMT0b9/f227ym4RNYTWrVtDEAQkJCTU6rwHoe/hn+Pff4bTp0/H1atX8dJLL+HixYvo3r07Pv/88yqvWVBQAAD4/fffdX428fHxOuNugIo/n8WLFyMuLg4jR45EREQEAgICEBYWVuV7NW3atNJB5zKZDLt27ULXrl0REhJSZcDJzc1Fs2bNqrw+UWPDcEPUSCxfvhy//fYbIiMjdfY3a9YMmZmZOl/M9Tk3zcODcMvKyhAVFYV27doBALp27Yq4uDj4+PjAz89PZ6tNoLG3t4eHhweOHz+us//48eMICAio8XWcnJwwbNgwrFmzBoWFhRWOV/Wo9oMv9oyMDO2+yn6GXl5eeOWVV/Dzzz9j/vz52LBhAwDA0tISAHTmmAkICIBcLse1a9cq/Gy8vLwe+VnatGmDuXPnYv/+/XjmmWcqHez9QJcuXRAfH1/pMblcjp9//hk9evRASEhIhXbJyckoLi5Gly5dHlkTUWPBcEPUSHTs2BETJkyo8NjuwIEDcevWLXz00UdITk7GmjVrsGfPnnp73zVr1iAsLAwJCQmYOXMm7ty5g6lTpwIoH2Sbm5uL8ePH48yZM0hOTsa+ffvw8ssv12oyOaB84PKKFSvwww8/4PLly1iwYAFiYmIwe/bsWterVqvRs2dP7Ny5E4mJibh06RJWr16tc4vtYQ8Cx+LFi5GYmIjff/8dK1eu1GkzZ84c7Nu3DykpKYiOjsbBgwe1Ic/b2xuCIGD37t24desWCgoKYGdnhzfffBNz587Fd999h+TkZERHR+Pzzz/Hd999V2X9RUVFmDVrFg4dOoS0tDQcP34cZ86c0b5XZYYNG4Zjx45VeVwul2Pnzp3o1asXQkJCEBcXpz129OhRtGzZEq1ataryfKLGhuGGqBFZunRphdtG7dq1w5dffok1a9YgMDAQp0+frvRJorpavnw5li9fjsDAQBw7dgy//vormjZtCgDa3ha1Wo0nnngCHTt2xJw5c9CkSROd8T018cYbb2DevHmYP38+OnbsiL179+LXX39F69ata3Wdli1bIjo6GiEhIZg/fz46dOiAoUOHIjw8HGvXrq30HAsLC2zbtg0JCQno1KkTVqxYgffff1+njVqtxsyZM9GuXTsMHz4cbdq0wZdffgkAaN68OZYsWYIFCxbA1dUVs2bNAgAsW7YM7733HkJDQ7Xn/f777/D19a2yfplMhtu3b2PSpElo06YNxo0bhxEjRmDJkiVVnjNhwgTExcXh8uXLVbaxtLTEjh07EBwcjJCQEMTGxgIAtm3bhhkzZlR5HlFjJIgNOQKPiIj04q233oJKpcJXX31V43Pi4uIwaNAgXLlyBQ4ODg1YHZF+seeGiMgIvPvuu/D29q7VgPCMjAxs3ryZwYaMDntuiIiIyKiw54aIiIiMCsMNERERGRWGGyIiIjIqDDdERERkVBhuiIiIyKgw3BAREZFRYbghIiIio8JwQ0REREaF4YaIiIiMyv8DkEVT2CFzAMwAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using the elbow method plot, choose a K where the decrease in inertia slows down.\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.cluster import KMeans\n", + "from sklearn.metrics import silhouette_score\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Assuming your DataFrame is named 'df' and contains the new columns.\n", + "# --- 1. Prepare the Data ---\n", + "# Select the RFM-like features\n", + "features = ['recency_days', 'freq_30d', 'budget_alignment']\n", + "X = df[features]\n", + "\n", + "# Standardize the features\n", + "scaler = StandardScaler()\n", + "X_scaled = scaler.fit_transform(X)\n", + "\n", + "# --- 2. Determine the Optimal Number of Clusters (K) ---\n", + "# Use the Elbow Method to find the best K\n", + "inertia = []\n", + "for k in range(1, 11):\n", + " kmeans = KMeans(n_clusters=k, random_state=42, n_init='auto')\n", + " kmeans.fit(X_scaled)\n", + " inertia.append(kmeans.inertia_)\n", + "\n", + "plt.plot(range(1, 11), inertia, marker='o')\n", + "plt.title('Elbow Method')\n", + "plt.xlabel('Number of Clusters (K)')\n", + "plt.ylabel('Inertia')\n", + "plt.show()\n", + "print(\"Using the elbow method plot, choose a K where the decrease in inertia slows down.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "497c2fd7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "K-Means Model Trained. Users are now assigned to a cluster.\n", + " Unnamed: 0.2 Unnamed: 0.1 Unnamed: 0 transaction_id user_id \\\n", + "0 0 0 0 452a413df6 user_1 \n", + "1 1 1 1 452a413df6 user_1 \n", + "2 2 2 2 452a413df6 user_1 \n", + "3 3 3 3 dd22f95cce user_1 \n", + "4 4 4 4 dd22f95cce user_1 \n", + "\n", + " product_code category \\\n", + "0 5355182 MENS DEOS & GROOMING \n", + "1 9050664 SNACKS \n", + "2 5055940 INFANT FOOD \n", + "3 3994635 BAKING MIXES \n", + "4 4842440 DENTAL HEALTH \n", + "\n", + " item_name discount_percentage \\\n", + "0 Deo Roll On Men Intense Protection Fresh 0.500000 \n", + "1 Original Multipack Potato Chips 0.416667 \n", + "2 Puffcorn BBQ 0.157895 \n", + "3 Deluxe Chocolate Layer Cake Mix 0.309091 \n", + "4 Advanced Whitening Charcoal Toothpaste 0.500000 \n", + "\n", + " transaction_date ... month seasonal_factor adjusted_spend \\\n", + "0 2023-08-21 ... 8 1.0 5.168301 \n", + "1 2023-08-21 ... 8 1.0 6.082251 \n", + "2 2023-08-21 ... 8 1.0 4.819962 \n", + "3 2023-07-22 ... 7 1.0 5.577268 \n", + "4 2023-07-22 ... 7 1.0 10.193634 \n", + "\n", + " promotion_applied discount_amount final_spend recency_days freq_30d \\\n", + "0 0 0.0 5.168301 91 0.0 \n", + "1 0 0.0 6.082251 102 0.0 \n", + "2 0 0.0 4.819962 84 0.0 \n", + "3 0 0.0 5.577268 64 0.0 \n", + "4 0 0.0 10.193634 471 0.0 \n", + "\n", + " budget_alignment cluster \n", + "0 0.403699 3 \n", + "1 0.476220 1 \n", + "2 0.380002 3 \n", + "3 0.436140 3 \n", + "4 0.787659 2 \n", + "\n", + "[5 rows x 26 columns]\n" + ] + }, + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[23], line 20\u001b[0m\n\u001b[0;32m 10\u001b[0m \u001b[38;5;28mprint\u001b[39m(df\u001b[38;5;241m.\u001b[39mhead())\n\u001b[0;32m 12\u001b[0m \u001b[38;5;66;03m# --- 4. Analyze Cluster Profiles for Recommendations ---\u001b[39;00m\n\u001b[0;32m 13\u001b[0m \u001b[38;5;66;03m# Here you would link the clusters to product data\u001b[39;00m\n\u001b[0;32m 14\u001b[0m \u001b[38;5;66;03m# For example: df_with_products.groupby('cluster')['product_code'].value_counts()\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 18\u001b[0m \u001b[38;5;66;03m# For clustering, the primary evaluation is **internal validation**.\u001b[39;00m\n\u001b[0;32m 19\u001b[0m \u001b[38;5;66;03m# It measures how well-defined the clusters are, not predictive accuracy.\u001b[39;00m\n\u001b[1;32m---> 20\u001b[0m silhouette_avg \u001b[38;5;241m=\u001b[39m \u001b[43msilhouette_score\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX_scaled\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdf\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mcluster\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 21\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124mSilhouette Score: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msilhouette_avg\u001b[38;5;132;01m:\u001b[39;00m\u001b[38;5;124m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 23\u001b[0m \u001b[38;5;66;03m# A silhouette score close to +1 indicates well-separated clusters.\u001b[39;00m\n\u001b[0;32m 24\u001b[0m \u001b[38;5;66;03m# A score near 0 indicates overlapping clusters.\u001b[39;00m\n\u001b[0;32m 25\u001b[0m \u001b[38;5;66;03m# A score near -1 indicates bad clustering.\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\utils\\_param_validation.py:211\u001b[0m, in \u001b[0;36mvalidate_params..decorator..wrapper\u001b[1;34m(*args, **kwargs)\u001b[0m\n\u001b[0;32m 205\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 206\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m config_context(\n\u001b[0;32m 207\u001b[0m skip_parameter_validation\u001b[38;5;241m=\u001b[39m(\n\u001b[0;32m 208\u001b[0m prefer_skip_nested_validation \u001b[38;5;129;01mor\u001b[39;00m global_skip_validation\n\u001b[0;32m 209\u001b[0m )\n\u001b[0;32m 210\u001b[0m ):\n\u001b[1;32m--> 211\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 212\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m InvalidParameterError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 213\u001b[0m \u001b[38;5;66;03m# When the function is just a wrapper around an estimator, we allow\u001b[39;00m\n\u001b[0;32m 214\u001b[0m \u001b[38;5;66;03m# the function to delegate validation to the estimator, but we replace\u001b[39;00m\n\u001b[0;32m 215\u001b[0m \u001b[38;5;66;03m# the name of the estimator by the name of the function in the error\u001b[39;00m\n\u001b[0;32m 216\u001b[0m \u001b[38;5;66;03m# message to avoid confusion.\u001b[39;00m\n\u001b[0;32m 217\u001b[0m msg \u001b[38;5;241m=\u001b[39m re\u001b[38;5;241m.\u001b[39msub(\n\u001b[0;32m 218\u001b[0m \u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mparameter of \u001b[39m\u001b[38;5;124m\\\u001b[39m\u001b[38;5;124mw+ must be\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 219\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mparameter of \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfunc\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__qualname__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m must be\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 220\u001b[0m \u001b[38;5;28mstr\u001b[39m(e),\n\u001b[0;32m 221\u001b[0m )\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\metrics\\cluster\\_unsupervised.py:131\u001b[0m, in \u001b[0;36msilhouette_score\u001b[1;34m(X, labels, metric, sample_size, random_state, **kwds)\u001b[0m\n\u001b[0;32m 129\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 130\u001b[0m X, labels \u001b[38;5;241m=\u001b[39m X[indices], labels[indices]\n\u001b[1;32m--> 131\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m np\u001b[38;5;241m.\u001b[39mmean(\u001b[43msilhouette_samples\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlabels\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmetric\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmetric\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m)\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\utils\\_param_validation.py:184\u001b[0m, in \u001b[0;36mvalidate_params..decorator..wrapper\u001b[1;34m(*args, **kwargs)\u001b[0m\n\u001b[0;32m 182\u001b[0m global_skip_validation \u001b[38;5;241m=\u001b[39m get_config()[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mskip_parameter_validation\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n\u001b[0;32m 183\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m global_skip_validation:\n\u001b[1;32m--> 184\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 186\u001b[0m func_sig \u001b[38;5;241m=\u001b[39m signature(func)\n\u001b[0;32m 188\u001b[0m \u001b[38;5;66;03m# Map *args/**kwargs to the function signature\u001b[39;00m\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\metrics\\cluster\\_unsupervised.py:283\u001b[0m, in \u001b[0;36msilhouette_samples\u001b[1;34m(X, labels, metric, **kwds)\u001b[0m\n\u001b[0;32m 279\u001b[0m kwds[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmetric\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m metric\n\u001b[0;32m 280\u001b[0m reduce_func \u001b[38;5;241m=\u001b[39m functools\u001b[38;5;241m.\u001b[39mpartial(\n\u001b[0;32m 281\u001b[0m _silhouette_reduce, labels\u001b[38;5;241m=\u001b[39mlabels, label_freqs\u001b[38;5;241m=\u001b[39mlabel_freqs\n\u001b[0;32m 282\u001b[0m )\n\u001b[1;32m--> 283\u001b[0m results \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mzip\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mpairwise_distances_chunked\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mreduce_func\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mreduce_func\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 284\u001b[0m intra_clust_dists, inter_clust_dists \u001b[38;5;241m=\u001b[39m results\n\u001b[0;32m 285\u001b[0m intra_clust_dists \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mconcatenate(intra_clust_dists)\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\metrics\\pairwise.py:2017\u001b[0m, in \u001b[0;36mpairwise_distances_chunked\u001b[1;34m(X, Y, reduce_func, metric, n_jobs, working_memory, **kwds)\u001b[0m\n\u001b[0;32m 2015\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 2016\u001b[0m X_chunk \u001b[38;5;241m=\u001b[39m X[sl]\n\u001b[1;32m-> 2017\u001b[0m D_chunk \u001b[38;5;241m=\u001b[39m \u001b[43mpairwise_distances\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX_chunk\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mY\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmetric\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmetric\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_jobs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mn_jobs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 2018\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (X \u001b[38;5;129;01mis\u001b[39;00m Y \u001b[38;5;129;01mor\u001b[39;00m Y \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;129;01mand\u001b[39;00m PAIRWISE_DISTANCE_FUNCTIONS\u001b[38;5;241m.\u001b[39mget(\n\u001b[0;32m 2019\u001b[0m metric, \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 2020\u001b[0m ) \u001b[38;5;129;01mis\u001b[39;00m euclidean_distances:\n\u001b[0;32m 2021\u001b[0m \u001b[38;5;66;03m# zeroing diagonal, taking care of aliases of \"euclidean\",\u001b[39;00m\n\u001b[0;32m 2022\u001b[0m \u001b[38;5;66;03m# i.e. \"l2\"\u001b[39;00m\n\u001b[0;32m 2023\u001b[0m D_chunk\u001b[38;5;241m.\u001b[39mflat[sl\u001b[38;5;241m.\u001b[39mstart :: _num_samples(X) \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m] \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0\u001b[39m\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\metrics\\pairwise.py:2195\u001b[0m, in \u001b[0;36mpairwise_distances\u001b[1;34m(X, Y, metric, n_jobs, force_all_finite, **kwds)\u001b[0m\n\u001b[0;32m 2192\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m distance\u001b[38;5;241m.\u001b[39msquareform(distance\u001b[38;5;241m.\u001b[39mpdist(X, metric\u001b[38;5;241m=\u001b[39mmetric, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwds))\n\u001b[0;32m 2193\u001b[0m func \u001b[38;5;241m=\u001b[39m partial(distance\u001b[38;5;241m.\u001b[39mcdist, metric\u001b[38;5;241m=\u001b[39mmetric, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwds)\n\u001b[1;32m-> 2195\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_parallel_pairwise\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mY\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_jobs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\metrics\\pairwise.py:1765\u001b[0m, in \u001b[0;36m_parallel_pairwise\u001b[1;34m(X, Y, func, n_jobs, **kwds)\u001b[0m\n\u001b[0;32m 1762\u001b[0m X, Y, dtype \u001b[38;5;241m=\u001b[39m _return_float_dtype(X, Y)\n\u001b[0;32m 1764\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m effective_n_jobs(n_jobs) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m-> 1765\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mY\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 1767\u001b[0m \u001b[38;5;66;03m# enforce a threading backend to prevent data communication overhead\u001b[39;00m\n\u001b[0;32m 1768\u001b[0m fd \u001b[38;5;241m=\u001b[39m delayed(_dist_wrapper)\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\metrics\\pairwise.py:338\u001b[0m, in \u001b[0;36meuclidean_distances\u001b[1;34m(X, Y, Y_norm_squared, squared, X_norm_squared)\u001b[0m\n\u001b[0;32m 332\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m Y_norm_squared\u001b[38;5;241m.\u001b[39mshape \u001b[38;5;241m!=\u001b[39m (\u001b[38;5;241m1\u001b[39m, Y\u001b[38;5;241m.\u001b[39mshape[\u001b[38;5;241m0\u001b[39m]):\n\u001b[0;32m 333\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[0;32m 334\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIncompatible dimensions for Y of shape \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mY\u001b[38;5;241m.\u001b[39mshape\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m and \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 335\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mY_norm_squared of shape \u001b[39m\u001b[38;5;132;01m{\u001b[39;00moriginal_shape\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 336\u001b[0m )\n\u001b[1;32m--> 338\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_euclidean_distances\u001b[49m\u001b[43m(\u001b[49m\u001b[43mX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mY\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mX_norm_squared\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mY_norm_squared\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msquared\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mc:\\Users\\rayed\\anaconda3\\envs\\dolfin\\Lib\\site-packages\\sklearn\\metrics\\pairwise.py:382\u001b[0m, in \u001b[0;36m_euclidean_distances\u001b[1;34m(X, Y, X_norm_squared, Y_norm_squared, squared)\u001b[0m\n\u001b[0;32m 380\u001b[0m distances \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m XX\n\u001b[0;32m 381\u001b[0m distances \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m YY\n\u001b[1;32m--> 382\u001b[0m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmaximum\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdistances\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdistances\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 384\u001b[0m \u001b[38;5;66;03m# Ensure that distances between vectors and themselves are set to 0.0.\u001b[39;00m\n\u001b[0;32m 385\u001b[0m \u001b[38;5;66;03m# This may not be the case due to floating point rounding errors.\u001b[39;00m\n\u001b[0;32m 386\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m X \u001b[38;5;129;01mis\u001b[39;00m Y:\n", + "\u001b[1;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "# In a real scenario, you would inspect the plot and choose an appropriate K.\n", + "# Let's assume we choose K=4 for this example.\n", + "optimal_k = 4\n", + "\n", + "# --- 3. Train the K-Means Model ---\n", + "kmeans = KMeans(n_clusters=optimal_k, random_state=42, n_init='auto')\n", + "df['cluster'] = kmeans.fit_predict(X_scaled)\n", + "\n", + "print(\"\\nK-Means Model Trained. Users are now assigned to a cluster.\")\n", + "print(df.head())\n", + "\n", + "# --- 4. Analyze Cluster Profiles for Recommendations ---\n", + "# Here you would link the clusters to product data\n", + "# For example: df_with_products.groupby('cluster')['product_code'].value_counts()\n", + "# This allows you to find the most popular items within each cluster.\n", + "\n", + "# --- 5. Evaluation ---\n", + "# For clustering, the primary evaluation is **internal validation**.\n", + "# It measures how well-defined the clusters are, not predictive accuracy.\n", + "silhouette_avg = silhouette_score(X_scaled, df['cluster'])\n", + "print(f\"\\nSilhouette Score: {silhouette_avg:.2f}\")\n", + "\n", + "# A silhouette score close to +1 indicates well-separated clusters.\n", + "# A score near 0 indicates overlapping clusters.\n", + "# A score near -1 indicates bad clustering." + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "dolfin", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}