{
"cells": [
{
"cell_type": "markdown",
"id": "c0cdf0b4",
"metadata": {},
"source": [
"# Import Library & Configuration"
]
},
{
"cell_type": "code",
"execution_count": 54,
"id": "d37034b9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Libraries loaded\n",
"Time: 2026-02-14 11:47:49\n"
]
}
],
"source": [
"import os\n",
"os.environ['PYTHONHASHSEED'] = '0'\n",
"os.environ['TF_DETERMINISTIC_OPS'] = '1'\n",
"os.environ['TF_CUDNN_DETERMINISM'] = '1'\n",
"\n",
"import random\n",
"random.seed(42)\n",
"\n",
"import math\n",
"import pandas as pd\n",
"import numpy as np\n",
"np.random.seed(42)\n",
"\n",
"from datetime import datetime\n",
"import pickle\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"\"\"\"\n",
"Deep Learning Framework\n",
"TensorFlow & Keras: Autoencoder architecture with Dense, BatchNormalization, Dropout layers\n",
"\"\"\"\n",
"import tensorflow as tf\n",
"tf.random.set_seed(42)\n",
"from tensorflow.keras import layers, Model, optimizers, callbacks\n",
"from tensorflow.keras.layers import Input, Dense, Dropout, BatchNormalization\n",
"\n",
"\"\"\"\n",
"Machine Learning & Metrics\n",
"- KMeans: K-means clustering for shoe recommendation groups\n",
"- StandardScaler/MinMaxScaler: Feature normalization for ML models\n",
"- Clustering Metrics: Silhouette, Davies-Bouldin, Calinski-Harabasz indices\n",
"- Similarity: Cosine similarity for recommendation ranking\n",
"\"\"\"\n",
"from sklearn.cluster import KMeans\n",
"from sklearn.preprocessing import StandardScaler, MinMaxScaler\n",
"from sklearn.metrics import (\n",
" silhouette_score, davies_bouldin_score, calinski_harabasz_score,\n",
" adjusted_rand_score\n",
")\n",
"from sklearn.metrics.pairwise import cosine_similarity, euclidean_distances\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"\n",
"\"\"\"\n",
"Data Visualization\n",
"Matplotlib & Seaborn for statistical plots and cluster visualization\n",
"\"\"\"\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"sns.set_style('whitegrid')\n",
"plt.rcParams['figure.figsize'] = (14, 6)\n",
"\n",
"np.random.seed(42)\n",
"tf.random.set_seed(42)\n",
"\n",
"print('Libraries loaded')\n",
"print(f'Time: {datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")}')"
]
},
{
"cell_type": "markdown",
"id": "9409bb62",
"metadata": {},
"source": [
"# Load Data"
]
},
{
"cell_type": "code",
"execution_count": 55,
"id": "93ac2c18",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loaded: 158 shoes × 37 columns\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" brand | \n",
" name | \n",
" lightweight | \n",
" terrain_light | \n",
" terrain_moderate | \n",
" terrain_technical | \n",
" shock_absorption | \n",
" energy_return | \n",
" traction_scaled | \n",
" arch_neutral | \n",
" ... | \n",
" heel_stiff | \n",
" lug_dept_mm | \n",
" heel_lab_mm | \n",
" forefoot_lab_mm | \n",
" season_summer | \n",
" season_winter | \n",
" season_all | \n",
" removable_insole | \n",
" waterproof | \n",
" water_repellent | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" adidas | \n",
" terrex agravic speed ultra | \n",
" 0 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
" 3 | \n",
" 5 | \n",
" 0 | \n",
" 1 | \n",
" ... | \n",
" 1 | \n",
" 2.5 | \n",
" 30.6 | \n",
" 30.3 | \n",
" 0 | \n",
" 0 | \n",
" 1 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
"
\n",
" \n",
" | 1 | \n",
" adidas | \n",
" terrex speed ultra | \n",
" 0 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
" 0 | \n",
" 0 | \n",
" 0 | \n",
" 1 | \n",
" ... | \n",
" 1 | \n",
" 2.6 | \n",
" 32.8 | \n",
" 24.6 | \n",
" 0 | \n",
" 0 | \n",
" 0 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
"
\n",
" \n",
" | 2 | \n",
" altra | \n",
" experience wild | \n",
" 0 | \n",
" 1 | \n",
" 1 | \n",
" 0 | \n",
" 3 | \n",
" 1 | \n",
" 0 | \n",
" 1 | \n",
" ... | \n",
" 3 | \n",
" 3.6 | \n",
" 34.5 | \n",
" 30.2 | \n",
" 0 | \n",
" 0 | \n",
" 1 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
"
\n",
" \n",
" | 3 | \n",
" altra | \n",
" experience wild 2 | \n",
" 0 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
" 3 | \n",
" 1 | \n",
" 5 | \n",
" 1 | \n",
" ... | \n",
" 1 | \n",
" 3.5 | \n",
" 32.3 | \n",
" 26.2 | \n",
" 0 | \n",
" 0 | \n",
" 1 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
"
\n",
" \n",
" | 4 | \n",
" altra | \n",
" lone peak 5.0 | \n",
" 0 | \n",
" 1 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
" 0 | \n",
" 0 | \n",
" 1 | \n",
" ... | \n",
" 0 | \n",
" 3.7 | \n",
" 24.5 | \n",
" 24.3 | \n",
" 0 | \n",
" 0 | \n",
" 0 | \n",
" 1 | \n",
" 0 | \n",
" 0 | \n",
"
\n",
" \n",
"
\n",
"
5 rows × 37 columns
\n",
"
"
],
"text/plain": [
" brand name lightweight terrain_light \\\n",
"0 adidas terrex agravic speed ultra 0 1 \n",
"1 adidas terrex speed ultra 0 1 \n",
"2 altra experience wild 0 1 \n",
"3 altra experience wild 2 0 1 \n",
"4 altra lone peak 5.0 0 1 \n",
"\n",
" terrain_moderate terrain_technical shock_absorption energy_return \\\n",
"0 0 0 3 5 \n",
"1 0 0 0 0 \n",
"2 1 0 3 1 \n",
"3 0 0 3 1 \n",
"4 1 0 0 0 \n",
"\n",
" traction_scaled arch_neutral ... heel_stiff lug_dept_mm heel_lab_mm \\\n",
"0 0 1 ... 1 2.5 30.6 \n",
"1 0 1 ... 1 2.6 32.8 \n",
"2 0 1 ... 3 3.6 34.5 \n",
"3 5 1 ... 1 3.5 32.3 \n",
"4 0 1 ... 0 3.7 24.5 \n",
"\n",
" forefoot_lab_mm season_summer season_winter season_all \\\n",
"0 30.3 0 0 1 \n",
"1 24.6 0 0 0 \n",
"2 30.2 0 0 1 \n",
"3 26.2 0 0 1 \n",
"4 24.3 0 0 0 \n",
"\n",
" removable_insole waterproof water_repellent \n",
"0 1 0 0 \n",
"1 1 0 0 \n",
"2 1 0 0 \n",
"3 1 0 0 \n",
"4 1 0 0 \n",
"\n",
"[5 rows x 37 columns]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"file = '../../data/trail_dataset.csv'\n",
"\n",
"try:\n",
" df = pd.read_csv(file)\n",
" print(f'Loaded: {df.shape[0]} shoes × {df.shape[1]} columns')\n",
" display(df.head())\n",
"except FileNotFoundError:\n",
" print(f\"WARNING: '{file}' not found.\")\n",
" print(\"Please upload the correct dataset file to run with actual data.\")"
]
},
{
"cell_type": "code",
"execution_count": 56,
"id": "5b87ce3b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"RangeIndex: 158 entries, 0 to 157\n",
"Data columns (total 37 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 brand 158 non-null str \n",
" 1 name 158 non-null str \n",
" 2 lightweight 158 non-null int64 \n",
" 3 terrain_light 158 non-null int64 \n",
" 4 terrain_moderate 158 non-null int64 \n",
" 5 terrain_technical 158 non-null int64 \n",
" 6 shock_absorption 158 non-null int64 \n",
" 7 energy_return 158 non-null int64 \n",
" 8 traction_scaled 158 non-null int64 \n",
" 9 arch_neutral 158 non-null int64 \n",
" 10 arch_stability 158 non-null int64 \n",
" 11 weight_lab_oz 158 non-null float64\n",
" 12 drop_lab_mm 158 non-null float64\n",
" 13 strike_heel 158 non-null int64 \n",
" 14 strike_mid 158 non-null int64 \n",
" 15 strike_forefoot 158 non-null int64 \n",
" 16 midsole_softness 158 non-null int64 \n",
" 17 toebox_durability 158 non-null int64 \n",
" 18 heel_durability 158 non-null int64 \n",
" 19 outsole_durability 158 non-null int64 \n",
" 20 breathability_scaled 158 non-null int64 \n",
" 21 plate_rock_plate 158 non-null int64 \n",
" 22 plate_carbon_plate 158 non-null int64 \n",
" 23 width_fit 158 non-null int64 \n",
" 24 toebox_width 158 non-null int64 \n",
" 25 stiffness_scaled 158 non-null int64 \n",
" 26 torsional_rigidity 158 non-null int64 \n",
" 27 heel_stiff 158 non-null int64 \n",
" 28 lug_dept_mm 158 non-null float64\n",
" 29 heel_lab_mm 158 non-null float64\n",
" 30 forefoot_lab_mm 158 non-null float64\n",
" 31 season_summer 158 non-null int64 \n",
" 32 season_winter 158 non-null int64 \n",
" 33 season_all 158 non-null int64 \n",
" 34 removable_insole 158 non-null int64 \n",
" 35 waterproof 158 non-null int64 \n",
" 36 water_repellent 158 non-null int64 \n",
"dtypes: float64(5), int64(30), str(2)\n",
"memory usage: 45.8 KB\n"
]
}
],
"source": [
"df.info()"
]
},
{
"cell_type": "markdown",
"id": "e66e0f9b",
"metadata": {},
"source": [
"# Preprocessing"
]
},
{
"cell_type": "markdown",
"id": "099b2d8c",
"metadata": {},
"source": [
"## Feature Engineering\n",
"Separates numeric features into two categories for different preprocessing strategies."
]
},
{
"cell_type": "code",
"execution_count": 57,
"id": "44420bdb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Features: 35 total\n",
" Binary : 17\n",
" Continuous : 18\n"
]
}
],
"source": [
"numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()\n",
"\n",
"binary_cols = [col for col in numeric_cols if set(df[col].unique()).issubset({0, 1})]\n",
"continuous_cols = [col for col in numeric_cols if col not in binary_cols]\n",
"\n",
"print(f'Features: {len(numeric_cols)} total')\n",
"print(f' Binary : {len(binary_cols)}')\n",
"print(f' Continuous : {len(continuous_cols)}')"
]
},
{
"cell_type": "code",
"execution_count": 58,
"id": "71d852a6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Features: 35 total\n",
" Binary : 17\n",
" Continuous : 18\n"
]
}
],
"source": [
"numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()\n",
"\n",
"binary_cols = [col for col in numeric_cols if set(df[col].unique()).issubset({0, 1})]\n",
"continuous_cols = [col for col in numeric_cols if col not in binary_cols]\n",
"\n",
"print(f'Features: {len(numeric_cols)} total')\n",
"print(f' Binary : {len(binary_cols)}')\n",
"print(f' Continuous : {len(continuous_cols)}')"
]
},
{
"cell_type": "markdown",
"id": "5890ac84",
"metadata": {},
"source": [
"## Normalization\n",
"- Binary features: kept as-is (0-1 range)\n",
"- Continuous features: MinMaxScaler to [0, 1]\n",
"- Combined array: binary + continuous scaled features\n",
"This ensures neural network compatibility and distance metric compatibility."
]
},
{
"cell_type": "code",
"execution_count": 59,
"id": "074d2042",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Neural input shape: (158, 35)\n",
"Range: [0.000000, 1.000000]\n"
]
}
],
"source": [
"feature_cols = numeric_cols.copy()\n",
"X = df[feature_cols]\n",
"\n",
"X_binary = X[binary_cols].values\n",
"X_continuous = X[continuous_cols].values\n",
"\n",
"scaler_continuous = MinMaxScaler()\n",
"X_continuous_scaled = scaler_continuous.fit_transform(X_continuous)\n",
"\n",
"X_combined = np.concatenate([X_binary, X_continuous_scaled], axis=1)\n",
"\n",
"scaler_standard = StandardScaler()\n",
"X_standard = scaler_standard.fit_transform(X)\n",
"\n",
"print(f'Neural input shape: {X_combined.shape}')\n",
"print(f'Range: [{X_combined.min():.6f}, {X_combined.max():.6f}]')"
]
},
{
"cell_type": "markdown",
"id": "eb77513e",
"metadata": {},
"source": [
"# Auto-Encoder"
]
},
{
"cell_type": "markdown",
"id": "38b73e33",
"metadata": {},
"source": [
"## Modelling\n",
"- Purpose: Dimensionality reduction (high-D features → 8D latent space)\n",
"- Architecture: Encoder [input → 32 → 16 → 8] + Decoder [8 → 16 → 32 → reconstructed]\n",
"- Regularization: BatchNormalization + Dropout(0.3) at each dense layer\n",
"- Loss: MSE (reconstruction error) | Optimizer: Adam(lr=0.001)"
]
},
{
"cell_type": "code",
"execution_count": 60,
"id": "ce2eb639",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Autoencoder architecture:\n"
]
},
{
"data": {
"text/html": [
"Model: \"functional_4\"\n",
"\n"
],
"text/plain": [
"\u001b[1mModel: \"functional_4\"\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n",
"┃ Layer (type) ┃ Output Shape ┃ Param # ┃\n",
"┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n",
"│ input_layer_2 (InputLayer) │ (None, 35) │ 0 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_12 (Dense) │ (None, 32) │ 1,152 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_10 │ (None, 32) │ 128 │\n",
"│ (BatchNormalization) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_10 (Dropout) │ (None, 32) │ 0 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_13 (Dense) │ (None, 16) │ 528 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_11 │ (None, 16) │ 64 │\n",
"│ (BatchNormalization) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_11 (Dropout) │ (None, 16) │ 0 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_14 (Dense) │ (None, 8) │ 136 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_12 │ (None, 8) │ 32 │\n",
"│ (BatchNormalization) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_12 (Dropout) │ (None, 8) │ 0 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_15 (Dense) │ (None, 16) │ 144 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_13 │ (None, 16) │ 64 │\n",
"│ (BatchNormalization) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_13 (Dropout) │ (None, 16) │ 0 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_16 (Dense) │ (None, 32) │ 544 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_14 │ (None, 32) │ 128 │\n",
"│ (BatchNormalization) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_14 (Dropout) │ (None, 32) │ 0 │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_17 (Dense) │ (None, 35) │ 1,155 │\n",
"└─────────────────────────────────┴────────────────────────┴───────────────┘\n",
"\n"
],
"text/plain": [
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n",
"┃\u001b[1m \u001b[0m\u001b[1mLayer (type) \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m Param #\u001b[0m\u001b[1m \u001b[0m┃\n",
"┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n",
"│ input_layer_2 (\u001b[38;5;33mInputLayer\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m35\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_12 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m1,152\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_10 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m128\u001b[0m │\n",
"│ (\u001b[38;5;33mBatchNormalization\u001b[0m) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_10 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_13 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m528\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_11 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m64\u001b[0m │\n",
"│ (\u001b[38;5;33mBatchNormalization\u001b[0m) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_11 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_14 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m136\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_12 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m32\u001b[0m │\n",
"│ (\u001b[38;5;33mBatchNormalization\u001b[0m) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_12 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m8\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_15 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m144\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_13 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m64\u001b[0m │\n",
"│ (\u001b[38;5;33mBatchNormalization\u001b[0m) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_13 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_16 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m544\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ batch_normalization_14 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m128\u001b[0m │\n",
"│ (\u001b[38;5;33mBatchNormalization\u001b[0m) │ │ │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dropout_14 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n",
"├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
"│ dense_17 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m35\u001b[0m) │ \u001b[38;5;34m1,155\u001b[0m │\n",
"└─────────────────────────────────┴────────────────────────┴───────────────┘\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
" Total params: 4,075 (15.92 KB)\n",
"\n"
],
"text/plain": [
"\u001b[1m Total params: \u001b[0m\u001b[38;5;34m4,075\u001b[0m (15.92 KB)\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
" Trainable params: 3,867 (15.11 KB)\n",
"\n"
],
"text/plain": [
"\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m3,867\u001b[0m (15.11 KB)\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
" Non-trainable params: 208 (832.00 B)\n",
"\n"
],
"text/plain": [
"\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m208\u001b[0m (832.00 B)\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"input_dim = X_combined.shape[1]\n",
"encoding_dims = [32, 16, 8]\n",
"\n",
"input_layer = Input(shape=(input_dim,))\n",
"x = input_layer\n",
"for dim in encoding_dims:\n",
" x = Dense(dim, activation='relu')(x)\n",
" x = BatchNormalization()(x)\n",
" x = Dropout(0.3)(x)\n",
"\n",
"latent = x\n",
"\n",
"for dim in reversed(encoding_dims[:-1]):\n",
" x = Dense(dim, activation='relu')(x)\n",
" x = BatchNormalization()(x)\n",
" x = Dropout(0.3)(x)\n",
"\n",
"output_layer = Dense(input_dim, activation='sigmoid')(x)\n",
"\n",
"autoencoder = Model(input_layer, output_layer)\n",
"encoder = Model(input_layer, latent)\n",
"\n",
"autoencoder.compile(\n",
" optimizer=optimizers.Adam(0.001),\n",
" loss='mse',\n",
" metrics=['mae']\n",
")\n",
"\n",
"print('Autoencoder architecture:')\n",
"autoencoder.summary()"
]
},
{
"cell_type": "markdown",
"id": "f6aaab33",
"metadata": {},
"source": [
"## Training\n",
"Training Configuration\n",
"- Epochs: 300 (with early stopping)\n",
"- Batch size: 64\n",
"- Validation split: 20%\n",
"- Early stopping: patience=20 (stop if val_loss doesn't improve)\n",
"- LR reduction: factor=0.5, patience=10, min_lr=1e-5\n",
"- Output: X_latent (8D embeddings) for KMeans clustering"
]
},
{
"cell_type": "code",
"execution_count": 61,
"id": "a6fb0de5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Training done!\n",
"Final loss: 0.078108\n",
" Val loss: 0.078716\n",
"Latent space: (158, 8) (8D embeddings)\n"
]
}
],
"source": [
"history = autoencoder.fit(\n",
" X_combined, X_combined,\n",
" epochs=300,\n",
" batch_size=64,\n",
" validation_split=0.25,\n",
" shuffle=False,\n",
" callbacks=[\n",
" callbacks.EarlyStopping(monitor='val_loss', patience=20, restore_best_weights=True),\n",
" callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=10, min_lr=1e-5)\n",
" ],\n",
" verbose=0\n",
")\n",
"\n",
"print(f'Training done!')\n",
"print(f'Final loss: {history.history[\"loss\"][-1]:.6f}')\n",
"print(f' Val loss: {history.history[\"val_loss\"][-1]:.6f}')\n",
"\n",
"X_latent = encoder.predict(X_combined, verbose=0)\n",
"print(f'Latent space: {X_latent.shape} (8D embeddings)')"
]
},
{
"cell_type": "markdown",
"id": "70cb6321",
"metadata": {},
"source": [
"# Metrics Function"
]
},
{
"cell_type": "markdown",
"id": "4789df65",
"metadata": {},
"source": [
"## Interpretability Score"
]
},
{
"cell_type": "code",
"execution_count": 62,
"id": "601dd47c",
"metadata": {},
"outputs": [],
"source": [
"def calculate_interpretability_score(df, cluster_col, binary_cols, top_n=5, threshold=0.70):\n",
" \"\"\"\n",
" Calculate cluster interpretability based on top feature clarity.\n",
" \n",
" Strategy: Focus on strongest features (not averages) to reduce noise.\n",
" Strength Definition: Distance from neutral point (0.5)\n",
" - Features closer to 0 or 1 = strong patterns\n",
" - Features closer to 0.5 = ambiguous patterns\n",
" \n",
" Args:\n",
" df (pd.DataFrame): Cluster-labeled dataset\n",
" cluster_col (str): Column with cluster assignments\n",
" binary_cols (list): Binary feature names\n",
" top_n (int): Number of top features to evaluate (default: 5)\n",
" threshold (float): Unused parameter (API compatibility)\n",
" \n",
" Returns:\n",
" dict: {'mean_interpretability': float [0, 1]}\n",
" 1.0 = clear feature patterns, 0.0 = no patterns\n",
" \"\"\"\n",
" scores = []\n",
" unique_clusters = df[cluster_col].unique()\n",
" \n",
" for cid in unique_clusters:\n",
" cdata = df[df[cluster_col] == cid]\n",
" n = len(cdata)\n",
" if n == 0: \n",
" scores.append(0)\n",
" continue\n",
" \n",
" feature_strength = []\n",
" for col in binary_cols:\n",
" if col in cdata.columns:\n",
" avg = cdata[col].mean()\n",
" strength = abs(avg - 0.5) * 2 # Normalize distance from neutral (0.5) to [0, 1]\n",
" feature_strength.append(strength)\n",
" \n",
" if feature_strength:\n",
" feature_strength.sort(reverse=True)\n",
" top_features = feature_strength[:top_n]\n",
" scores.append(np.mean(top_features)) # Average of top-N features\n",
" else:\n",
" scores.append(0)\n",
" \n",
" return {'mean_interpretability': np.mean(scores) if scores else 0}"
]
},
{
"cell_type": "markdown",
"id": "36f4699d",
"metadata": {},
"source": [
"## Cluster Purity"
]
},
{
"cell_type": "code",
"execution_count": 63,
"id": "41ebefdf",
"metadata": {},
"outputs": [],
"source": [
"def calculate_cluster_purity(df, cluster_col, binary_cols):\n",
" \"\"\"\n",
" Measure internal cluster homogeneity via majority class dominance.\n",
" \n",
" Purity Calculation: For each feature, compute max(class0_pct, class1_pct)\n",
" Average across all features = cluster purity\n",
" Range: [0.5, 1.0] where 1.0 = perfect homogeneity\n",
" \n",
" Args:\n",
" df (pd.DataFrame): Cluster-labeled dataset\n",
" cluster_col (str): Column with cluster assignments\n",
" binary_cols (list): Binary feature names\n",
" \n",
" Returns:\n",
" dict: {'mean_purity': float [0.5, 1.0]}\n",
" \"\"\"\n",
" purity_by_cluster = []\n",
" unique_clusters = df[cluster_col].unique()\n",
" \n",
" for cid in unique_clusters:\n",
" cdata = df[df[cluster_col] == cid]\n",
" n = len(cdata)\n",
" if n == 0: continue\n",
" \n",
" dominances = []\n",
" for col in binary_cols:\n",
" if col in cdata.columns:\n",
" avg = cdata[col].mean()\n",
" dominances.append(max(avg, 1 - avg)) # Majority class percentage\n",
" \n",
" if dominances:\n",
" purity_by_cluster.append(np.mean(dominances))\n",
" \n",
" return {'mean_purity': np.mean(purity_by_cluster) if purity_by_cluster else 0}"
]
},
{
"cell_type": "markdown",
"id": "d373c67f",
"metadata": {},
"source": [
"## Cluster Stability"
]
},
{
"cell_type": "code",
"execution_count": 64,
"id": "059bd68b",
"metadata": {},
"outputs": [],
"source": [
"def calculate_cluster_stability(X, labels, model_func, n_iter=5, seed=42):\n",
" \"\"\"\n",
" Bootstrap stability testing via Adjusted Rand Index (ARI).\n",
" \n",
" Process:\n",
" 1. Train model on bootstrap sample (with replacement)\n",
" 2. Compare original vs bootstrap clustering using ARI\n",
" 3. Average ARI across iterations\n",
" \n",
" ARI Range: [-1, 1]\n",
" > 0.5: excellent stability\n",
" 0.2-0.5: fair stability\n",
" < 0.2: poor stability\n",
" \n",
" Args:\n",
" X (np.ndarray): Feature matrix\n",
" labels (np.ndarray): Original cluster assignments\n",
" model_func (callable): Returns instantiated clustering model\n",
" n_iter (int): Bootstrap iterations (default: 5)\n",
" \n",
" Returns:\n",
" dict: {'mean_ari': float [-1, 1]}\n",
" \"\"\"\n",
" if len(np.unique(labels)) < 2:\n",
" return {'mean_ari': 0}\n",
"\n",
" n = len(X)\n",
" ari_scores = []\n",
"\n",
" for i in range(n_iter):\n",
" rng = np.random.default_rng(seed=42 + i)\n",
" idx = rng.choice(n, n, replace=True)\n",
" try:\n",
" boot_model = model_func()\n",
" boot_labels = boot_model.fit_predict(X[idx])\n",
" ari = adjusted_rand_score(labels[idx], boot_labels)\n",
" ari_scores.append(ari)\n",
" except Exception:\n",
" continue\n",
"\n",
" m = np.mean(ari_scores) if ari_scores else 0\n",
" return {'mean_ari': m}"
]
},
{
"cell_type": "markdown",
"id": "6d0b1f8d",
"metadata": {},
"source": [
"## Comprehensive Evaluation"
]
},
{
"cell_type": "code",
"execution_count": 65,
"id": "acbf4808",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Optimized Metrics Function Ready.\n"
]
}
],
"source": [
"def evaluate_clustering_comprehensive(X, labels, df_original, model_func, binary_cols):\n",
" \"\"\"\n",
" Multi-metric clustering evaluation combining geometric and business metrics.\n",
" \n",
" Evaluation Framework:\n",
" \n",
" 1. GEOMETRIC METRICS (Scikit-learn):\n",
" Silhouette [-1, 1]: cluster separation quality\n",
" Davies-Bouldin [0, ∞): intra-cluster density (lower better)\n",
" Calinski-Harabasz [0, ∞): cluster definition (higher better)\n",
" \n",
" 2. BUSINESS METRICS:\n",
" Purity: internal homogeneity\n",
" Interpretability: feature pattern clarity\n",
" Stability: clustering consistency\n",
" \n",
" 3. COMPOSITE SCORING (strategic weights):\n",
" Structure (40%): 40% Silhouette + 30% Davies-Bouldin + 40% Calinski-Harabasz\n",
" Explainability (30%): 50% Interpretability + 50% Purity\n",
" Reliability (30%): Bootstrap ARI stability\n",
" \n",
" Args:\n",
" X (np.ndarray): Latent feature space (typically autoencoder output)\n",
" labels (np.ndarray): Cluster assignments [0, K-1]\n",
" df_original (pd.DataFrame): Original shoe metadata\n",
" model_func (callable): KMeans factory function\n",
" binary_cols (list): Binary feature column names\n",
" \n",
" Returns:\n",
" dict: {\n",
" 'metrics': {silhouette, davies_bouldin, calinski_harabasz, purity, stability, interpretability},\n",
" 'composite_score': float [0, 1]\n",
" }\n",
" \"\"\"\n",
" df_eval = df_original.copy()\n",
" df_eval['cluster'] = labels\n",
" \n",
" sil = silhouette_score(X, labels)\n",
" db = davies_bouldin_score(X, labels)\n",
" ch = calinski_harabasz_score(X, labels)\n",
" \n",
" purity_res = calculate_cluster_purity(df_eval, 'cluster', binary_cols)\n",
" interp_res = calculate_interpretability_score(df_eval, 'cluster', binary_cols, top_n=5)\n",
" stability_res = calculate_cluster_stability(X, labels, model_func, n_iter=3)\n",
" \n",
" val_purity = purity_res['mean_purity']\n",
" val_interp = interp_res['mean_interpretability']\n",
" val_stability = stability_res['mean_ari']\n",
"\n",
" sil_norm = (sil + 1) / 2 # Map Silhouette [-1, 1] → [0, 1]\n",
" db_norm = np.exp(-0.5 * db) # Exponential decay: DB lower is better\n",
" \n",
" if ch > 0:\n",
" ch_log = np.log1p(ch)\n",
" ch_norm = min(ch_log / 8, 1.0) # Log scaling: assume max log(CH) ≈ 9.2\n",
" else:\n",
" ch_norm = 0\n",
"\n",
" score_structure = (0.4 * sil_norm) + (0.3 * db_norm) + (0.3 * ch_norm) # Weight: Silhouette 40%, DB 30%, CH 30%\n",
" score_explain = (0.5 * val_interp) + (0.5 * val_purity) # Weight: Interpretability 50%, Purity 50%\n",
" score_reliability = max(val_stability, 0) # Clip negative ARI to 0\n",
"\n",
" composite = (0.40 * score_structure) + (0.30 * score_explain) + (0.30 * score_reliability)\n",
" \n",
" return {\n",
" 'metrics': {\n",
" 'silhouette': sil, \n",
" 'davies_bouldin': db, \n",
" 'calinski_harabasz': ch,\n",
" 'purity': val_purity, \n",
" 'stability': val_stability,\n",
" 'interpretability': val_interp\n",
" },\n",
" 'composite_score': composite\n",
" }\n",
"\n",
"print('Optimized Metrics Function Ready.')"
]
},
{
"cell_type": "markdown",
"id": "a26ba812",
"metadata": {},
"source": [
"# Model Selection\n",
"Model Selection Pipeline: K-means Clustering (K=3 to K=9)\n",
"\n",
"For each K value:\n",
" - Train KMeans model\n",
" - Evaluate using comprehensive metrics\n",
" - Compute composite score\n",
"\n",
"Select K with highest composite score (40% geometry, 30% explainability, 30% reliability)"
]
},
{
"cell_type": "code",
"execution_count": 66,
"id": "8e1f4985",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"| K | Score | Sil. | DB | CH | Purity | Stab. | Interp |\n",
"|-----+----------+----------+----------+------------+----------+----------+----------|\n",
"| 3 | 0.765033 | 0.375950 | 1.123361 | 72.019419 | 0.857093 | 0.827530 | 0.968952 |\n",
"| 4 | 0.823837 | 0.434731 | 0.908956 | 92.039301 | 0.879525 | 0.953780 | 0.978846 |\n",
"| 5 | 0.846234 | 0.466877 | 0.762381 | 119.549157 | 0.881545 | 0.983211 | 0.985616 |\n",
"| 6 | 0.785558 | 0.469993 | 0.784870 | 108.848327 | 0.888658 | 0.778595 | 0.996970 |\n",
"| 7 | 0.751643 | 0.431342 | 0.899164 | 102.980001 | 0.889365 | 0.692877 | 0.997714 |\n",
"| 8 | 0.783562 | 0.382712 | 0.941965 | 99.927615 | 0.887725 | 0.818811 | 1.000000 |\n",
"| 9 | 0.801397 | 0.380008 | 0.995231 | 98.352993 | 0.898996 | 0.880696 | 1.000000 |\n"
]
}
],
"source": [
"results = []\n",
"\n",
"print(f\"| {'K':^3} | {'Score':^8} | {'Sil.':^8} | {'DB':^8} | {'CH':^10} | {'Purity':^8} | {'Stab.':^8} | {'Interp':^8} |\")\n",
"print(f\"|{'-'*5}+{'-'*10}+{'-'*10}+{'-'*10}+{'-'*12}+{'-'*10}+{'-'*10}+{'-'*10}|\")\n",
"\n",
"for i in range(3, 10):\n",
" model_factory = lambda: KMeans(n_clusters=i, random_state=42, n_init=20)\n",
" \n",
" model = model_factory()\n",
" labels = model.fit_predict(X_latent)\n",
"\n",
" metrics_res = evaluate_clustering_comprehensive(\n",
" X_latent, labels, df.copy(),\n",
" model_factory,\n",
" binary_cols\n",
" )\n",
"\n",
" raw_metrics = metrics_res['metrics'] \n",
" comp_score = metrics_res['composite_score']\n",
"\n",
" record = {\n",
" 'k': i,\n",
" 'model': model,\n",
" 'labels': labels,\n",
" 'composite_score': comp_score,\n",
" **raw_metrics\n",
" }\n",
" results.append(record)\n",
"\n",
" print(f\"| {i:^3} | {comp_score:<8.6f} | {raw_metrics['silhouette']:<6.6f} | \"\n",
" f\"{raw_metrics['davies_bouldin']:<6.6f} | {raw_metrics['calinski_harabasz']:<8.6f} | \"\n",
" f\"{raw_metrics['purity']:<6.6f} | {raw_metrics['stability']:<6.6f} | {raw_metrics['interpretability']:<6.6f} |\")"
]
},
{
"cell_type": "code",
"execution_count": 67,
"id": "3aa23de1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--------------------------------------------------------------------------------\n",
"SELECTED BEST K: 5\n",
" Silhouette : 0.466877\n",
" Composite Score : 0.846234\n"
]
}
],
"source": [
"df_results = pd.DataFrame(results)\n",
"\n",
"best_idx = df_results['composite_score'].idxmax()\n",
"best_config = df_results.loc[best_idx]\n",
"\n",
"best_model = best_config['model']\n",
"best_labels = best_config['labels']\n",
"best_k = best_config['k']\n",
"X_for_clustering = X_latent\n",
"\n",
"print(\"-\" * 80)\n",
"print(f'SELECTED BEST K: {best_k}')\n",
"print(f' Silhouette : {best_config[\"silhouette\"]:.6f}') \n",
"print(f' Composite Score : {best_config[\"composite_score\"]:.6f}')"
]
},
{
"cell_type": "markdown",
"id": "84409589",
"metadata": {},
"source": [
"# Generate Cluster Labels"
]
},
{
"cell_type": "markdown",
"id": "dcedd13e",
"metadata": {},
"source": [
"## Binning\n",
"Divides each continuous feature into 3 quantile bins (tertiles).\n",
"\n",
"Labels: 0 (low), 0.5 (medium), 1 (high)\n",
"\n",
"Enables interpretable cluster profiling and feature discretization."
]
},
{
"cell_type": "code",
"execution_count": 68,
"id": "ecb087b7",
"metadata": {},
"outputs": [],
"source": [
"for col in df.select_dtypes('float64').columns.tolist():\n",
" new_col_name = col + '_bin'\n",
" df[new_col_name] = pd.qcut(df[col], q=3, labels=[0, 0.5, 1]).astype(int)\n",
"\n",
"non_numeric_cols = df.select_dtypes(exclude=[np.number]).columns.tolist()\n",
"\n",
"new_column_order = []\n",
"\n",
"for col in non_numeric_cols:\n",
" if col in df.columns:\n",
" new_column_order.append(col)\n",
"\n",
"for col in binary_cols:\n",
" if col in df.columns:\n",
" new_column_order.append(col)\n",
"\n",
"for col in continuous_cols:\n",
" if col in df.columns:\n",
" new_column_order.append(col)\n",
" bin_col_name = col + '_bin'\n",
" if bin_col_name in df.columns:\n",
" new_column_order.append(bin_col_name)\n",
"\n",
"if 'cluster' in df.columns and 'cluster' not in new_column_order:\n",
" new_column_order.append('cluster')\n",
"\n",
"df = df[new_column_order]"
]
},
{
"cell_type": "code",
"execution_count": 69,
"id": "9e638024",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"RangeIndex: 158 entries, 0 to 157\n",
"Data columns (total 42 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 brand 158 non-null str \n",
" 1 name 158 non-null str \n",
" 2 lightweight 158 non-null int64 \n",
" 3 terrain_light 158 non-null int64 \n",
" 4 terrain_moderate 158 non-null int64 \n",
" 5 terrain_technical 158 non-null int64 \n",
" 6 arch_neutral 158 non-null int64 \n",
" 7 arch_stability 158 non-null int64 \n",
" 8 strike_heel 158 non-null int64 \n",
" 9 strike_mid 158 non-null int64 \n",
" 10 strike_forefoot 158 non-null int64 \n",
" 11 plate_rock_plate 158 non-null int64 \n",
" 12 plate_carbon_plate 158 non-null int64 \n",
" 13 season_summer 158 non-null int64 \n",
" 14 season_winter 158 non-null int64 \n",
" 15 season_all 158 non-null int64 \n",
" 16 removable_insole 158 non-null int64 \n",
" 17 waterproof 158 non-null int64 \n",
" 18 water_repellent 158 non-null int64 \n",
" 19 shock_absorption 158 non-null int64 \n",
" 20 energy_return 158 non-null int64 \n",
" 21 traction_scaled 158 non-null int64 \n",
" 22 weight_lab_oz 158 non-null float64\n",
" 23 weight_lab_oz_bin 158 non-null int64 \n",
" 24 drop_lab_mm 158 non-null float64\n",
" 25 drop_lab_mm_bin 158 non-null int64 \n",
" 26 midsole_softness 158 non-null int64 \n",
" 27 toebox_durability 158 non-null int64 \n",
" 28 heel_durability 158 non-null int64 \n",
" 29 outsole_durability 158 non-null int64 \n",
" 30 breathability_scaled 158 non-null int64 \n",
" 31 width_fit 158 non-null int64 \n",
" 32 toebox_width 158 non-null int64 \n",
" 33 stiffness_scaled 158 non-null int64 \n",
" 34 torsional_rigidity 158 non-null int64 \n",
" 35 heel_stiff 158 non-null int64 \n",
" 36 lug_dept_mm 158 non-null float64\n",
" 37 lug_dept_mm_bin 158 non-null int64 \n",
" 38 heel_lab_mm 158 non-null float64\n",
" 39 heel_lab_mm_bin 158 non-null int64 \n",
" 40 forefoot_lab_mm 158 non-null float64\n",
" 41 forefoot_lab_mm_bin 158 non-null int64 \n",
"dtypes: float64(5), int64(35), str(2)\n",
"memory usage: 52.0 KB\n"
]
}
],
"source": [
"df.info()"
]
},
{
"cell_type": "markdown",
"id": "2cb72334",
"metadata": {},
"source": [
"## Cluster Summary\n",
"Creates interpretable profile for each cluster showing:\n",
"- Size (count + percentage)\n",
"- Continuous features (mean values)\n",
"- Binary features (dominant variant + prevalence)"
]
},
{
"cell_type": "code",
"execution_count": 70,
"id": "c58df965",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Cluster Summary:\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" count | \n",
" percentage | \n",
" shock_absorption | \n",
" energy_return | \n",
" traction_scaled | \n",
" weight_lab_oz | \n",
" drop_lab_mm | \n",
" midsole_softness | \n",
" toebox_durability | \n",
" heel_durability | \n",
" ... | \n",
" lightweight | \n",
" terrain | \n",
" arch | \n",
" strike | \n",
" plate_rock_plate | \n",
" plate_carbon_plate | \n",
" season | \n",
" removable_insole | \n",
" waterproof | \n",
" water_repellent | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 45 | \n",
" 28.5% | \n",
" 1.84 | \n",
" 1.04 | \n",
" 0.84 | \n",
" 10.27 | \n",
" 10.88 | \n",
" 3.40 | \n",
" 2.07 | \n",
" 2.29 | \n",
" ... | \n",
" no (4%) | \n",
" light (87%) | \n",
" neutral (96%) | \n",
" heel (93%) | \n",
" no (7%) | \n",
" no (16%) | \n",
" all (93%) | \n",
" yes (100%) | \n",
" no (2%) | \n",
" no (0%) | \n",
"
\n",
" \n",
" | 1 | \n",
" 60 | \n",
" 38.0% | \n",
" 2.02 | \n",
" 1.45 | \n",
" 1.33 | \n",
" 10.00 | \n",
" 5.21 | \n",
" 3.67 | \n",
" 2.97 | \n",
" 3.07 | \n",
" ... | \n",
" no (12%) | \n",
" light (93%) | \n",
" neutral (100%) | \n",
" mid (100%) | \n",
" no (20%) | \n",
" no (3%) | \n",
" all (95%) | \n",
" yes (92%) | \n",
" no (5%) | \n",
" no (5%) | \n",
"
\n",
" \n",
" | 2 | \n",
" 22 | \n",
" 13.9% | \n",
" 0.00 | \n",
" 0.00 | \n",
" 0.00 | \n",
" 10.13 | \n",
" 5.75 | \n",
" 0.59 | \n",
" 0.00 | \n",
" 0.00 | \n",
" ... | \n",
" no (9%) | \n",
" moderate (73%) | \n",
" neutral (100%) | \n",
" mid (82%) | \n",
" no (45%) | \n",
" no (5%) | \n",
" all (23%) | \n",
" yes (73%) | \n",
" no (0%) | \n",
" no (5%) | \n",
"
\n",
" \n",
" | 3 | \n",
" 11 | \n",
" 7.0% | \n",
" 0.45 | \n",
" 0.27 | \n",
" 0.00 | \n",
" 10.90 | \n",
" 11.18 | \n",
" 2.64 | \n",
" 4.09 | \n",
" 3.09 | \n",
" ... | \n",
" no (0%) | \n",
" moderate (73%) | \n",
" neutral (82%) | \n",
" heel (82%) | \n",
" no (27%) | \n",
" no (0%) | \n",
" winter (91%) | \n",
" yes (100%) | \n",
" yes (82%) | \n",
" no (9%) | \n",
"
\n",
" \n",
" | 4 | \n",
" 20 | \n",
" 12.7% | \n",
" 1.90 | \n",
" 1.60 | \n",
" 0.50 | \n",
" 10.28 | \n",
" 6.37 | \n",
" 4.20 | \n",
" 3.30 | \n",
" 3.50 | \n",
" ... | \n",
" no (0%) | \n",
" moderate (85%) | \n",
" neutral (100%) | \n",
" mid (100%) | \n",
" no (35%) | \n",
" no (5%) | \n",
" all (90%) | \n",
" yes (100%) | \n",
" no (0%) | \n",
" no (5%) | \n",
"
\n",
" \n",
"
\n",
"
5 rows × 30 columns
\n",
"
"
],
"text/plain": [
" count percentage shock_absorption energy_return traction_scaled \\\n",
"0 45 28.5% 1.84 1.04 0.84 \n",
"1 60 38.0% 2.02 1.45 1.33 \n",
"2 22 13.9% 0.00 0.00 0.00 \n",
"3 11 7.0% 0.45 0.27 0.00 \n",
"4 20 12.7% 1.90 1.60 0.50 \n",
"\n",
" weight_lab_oz drop_lab_mm midsole_softness toebox_durability \\\n",
"0 10.27 10.88 3.40 2.07 \n",
"1 10.00 5.21 3.67 2.97 \n",
"2 10.13 5.75 0.59 0.00 \n",
"3 10.90 11.18 2.64 4.09 \n",
"4 10.28 6.37 4.20 3.30 \n",
"\n",
" heel_durability ... lightweight terrain arch \\\n",
"0 2.29 ... no (4%) light (87%) neutral (96%) \n",
"1 3.07 ... no (12%) light (93%) neutral (100%) \n",
"2 0.00 ... no (9%) moderate (73%) neutral (100%) \n",
"3 3.09 ... no (0%) moderate (73%) neutral (82%) \n",
"4 3.50 ... no (0%) moderate (85%) neutral (100%) \n",
"\n",
" strike plate_rock_plate plate_carbon_plate season \\\n",
"0 heel (93%) no (7%) no (16%) all (93%) \n",
"1 mid (100%) no (20%) no (3%) all (95%) \n",
"2 mid (82%) no (45%) no (5%) all (23%) \n",
"3 heel (82%) no (27%) no (0%) winter (91%) \n",
"4 mid (100%) no (35%) no (5%) all (90%) \n",
"\n",
" removable_insole waterproof water_repellent \n",
"0 yes (100%) no (2%) no (0%) \n",
"1 yes (92%) no (5%) no (5%) \n",
"2 yes (73%) no (0%) no (5%) \n",
"3 yes (100%) yes (82%) no (9%) \n",
"4 yes (100%) no (0%) no (5%) \n",
"\n",
"[5 rows x 30 columns]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"df['cluster'] = best_labels \n",
"\n",
"bin_groups = {}\n",
"for col in binary_cols:\n",
" parts = col.split('_')\n",
" \n",
" if len(parts) > 1:\n",
" prefix = '_'.join(parts[:-1])\n",
" else:\n",
" prefix = col\n",
" \n",
" bin_groups.setdefault(prefix, []).append(col)\n",
"\n",
"rows = []\n",
"for cid in sorted(df['cluster'].unique()):\n",
" subset = df[df['cluster'] == cid]\n",
" n = len(subset)\n",
" \n",
" row = {'count': n, 'percentage': f\"{n/len(df)*100:.1f}%\"}\n",
"\n",
" for col in continuous_cols:\n",
" row[col.lower()] = round(subset[col].mean(), 2)\n",
"\n",
" for prefix, cols in bin_groups.items():\n",
" means = subset[cols].mean()\n",
" best_col = means.idxmax()\n",
" best_val = means.max()\n",
" \n",
" if len(cols) > 1:\n",
" header = prefix.lower()\n",
" val_str = best_col.replace(f\"{prefix}_\", \"\").lower()\n",
" row[header] = f\"{val_str} ({best_val*100:.0f}%)\"\n",
" \n",
" else:\n",
" header = cols[0].lower()\n",
" val_str = \"yes\" if best_val > 0.5 else \"no\"\n",
" row[header] = f\"{val_str} ({best_val*100:.0f}%)\"\n",
"\n",
" rows.append(row)\n",
"\n",
"df_summary = pd.DataFrame(rows, index=sorted(df['cluster'].unique()))\n",
"df_summary.index.name = None \n",
"\n",
"print(\"Cluster Summary:\")\n",
"display(df_summary)"
]
},
{
"cell_type": "markdown",
"id": "a9fa3aff",
"metadata": {},
"source": [
"# Deep Learn Recommender"
]
},
{
"cell_type": "markdown",
"id": "f35b760e",
"metadata": {},
"source": [
"## Priority Handler"
]
},
{
"cell_type": "code",
"execution_count": 71,
"id": "9225b9c3",
"metadata": {},
"outputs": [],
"source": [
"def get_priority_val(user_input, priority_list, mapping_dicts):\n",
" \"\"\"\n",
" Extract feature value from user input with priority hierarchy.\n",
" \n",
" Strategy: Checks inputs in priority order, returns mapped value from\n",
" first non-empty input, ignores lower-priority inputs if higher-priority exists.\n",
" \n",
" Args:\n",
" user_input (dict): User preferences {'running_purpose': 'Daily', ...}\n",
" priority_list (list): Input sources in priority order\n",
" mapping_dicts (dict): Maps {source: {option: feature_value}}\n",
" \n",
" Returns:\n",
" float: Feature value [0, 1] or 0.5 (neutral) if not found\n",
" \"\"\"\n",
" for source_key in priority_list:\n",
" if source_key in user_input and user_input[source_key]:\n",
" user_choice = user_input[source_key]\n",
" if source_key in mapping_dicts:\n",
" mapping = mapping_dicts[source_key]\n",
" if user_choice in mapping:\n",
" return mapping[user_choice]\n",
" return 0.5"
]
},
{
"cell_type": "markdown",
"id": "19abb5f6",
"metadata": {},
"source": [
"## Input Handler"
]
},
{
"cell_type": "code",
"execution_count": 72,
"id": "a44b15a3",
"metadata": {},
"outputs": [],
"source": [
"def preprocess_user_input_with_mask(user_input, binary_cols, continuous_cols):\n",
" \"\"\"\n",
" Translates user inputs (Terrain, Pace, etc.) into 34 engineered features \n",
" based on the specific logic provided.\n",
" \"\"\"\n",
" feats = {col: 0.0 for col in binary_cols + continuous_cols}\n",
" \n",
" feats['terrain_light'] = get_priority_val(user_input, ['terrain'], \n",
" {'terrain': {'Light': 1.0, 'Mixed': 0.5, 'Rocky': 0.0, 'Muddy': 0.0}})\n",
" feats['terrain_moderate'] = get_priority_val(user_input, ['terrain'], \n",
" {'terrain': {'Light': 0.5, 'Mixed': 1.0, 'Rocky': 0.5, 'Muddy': 0.5}})\n",
" feats['terrain_technical'] = get_priority_val(user_input, ['terrain'], \n",
" {'terrain': {'Light': 0.0, 'Mixed': 0.5, 'Rocky': 1.0, 'Muddy': 1.0}})\n",
"\n",
" feats['shock_absorption'] = get_priority_val(user_input, ['rock_sensitive', 'terrain'], \n",
" {'rock_sensitive': {'Yes': 1.0, 'No': 0.0}, \n",
" 'terrain': {'Light': 0.2, 'Mixed': 0.6, 'Rocky': 1.0, 'Muddy': 0.0}})\n",
"\n",
" feats['energy_return'] = 1.0\n",
" feats['traction_scaled'] = get_priority_val(user_input, ['terrain'], \n",
" {'terrain': {'Light': 0.5, 'Mixed': 1.0, 'Rocky': 0.5, 'Muddy': 1.0}})\n",
"\n",
" feats['arch_neutral'] = get_priority_val(user_input, ['arch_type'], \n",
" {'arch_type': {'Flat': 0.0, 'Normal': 0.8, 'High': 1.0}})\n",
" feats['arch_stability'] = get_priority_val(user_input, ['arch_type'], \n",
" {'arch_type': {'Flat': 1.0, 'Normal': 0.2, 'High': 0.0}})\n",
"\n",
" feats['drop_lab_mm'] = get_priority_val(user_input, ['pace'], \n",
" {'pace': {'Easy': 1.0, 'Steady': 0.5, 'Fast': 0.0}})\n",
"\n",
" prio_strike = ['strike_pattern', 'pace']\n",
" feats['strike_heel'] = get_priority_val(user_input, prio_strike, {\n",
" 'strike_pattern': {'Heel': 1.0, 'Mid': 0.5, 'Forefoot': 0.0}, \n",
" 'pace': {'Easy': 1.0, 'Steady': 0.5, 'Fast': 0.0}})\n",
" feats['strike_mid'] = get_priority_val(user_input, prio_strike, {\n",
" 'strike_pattern': {'Heel': 0.5, 'Mid': 1.0, 'Forefoot': 0.5}, \n",
" 'pace': {'Easy': 0.5, 'Steady': 1.0, 'Fast': 0.5}})\n",
" feats['strike_forefoot'] = get_priority_val(user_input, prio_strike, {\n",
" 'strike_pattern': {'Heel': 0.0, 'Mid': 0.0, 'Forefoot': 1.0}, \n",
" 'pace': {'Easy': 0.0, 'Steady': 0.5, 'Fast': 1.0}})\n",
"\n",
" feats['midsole_softness'] = get_priority_val(user_input, ['pace'], \n",
" {'pace': {'Easy': 1.0, 'Steady': 0.6, 'Fast': 0.2}})\n",
"\n",
" feats['toebox_durability'] = 1.0\n",
" feats['heel_durability'] = 1.0\n",
" feats['outsole_durability'] = 1.0\n",
" feats['breathability'] = 1.0\n",
"\n",
" feats['plate_rock_plate'] = get_priority_val(user_input, ['pace', 'terrain'], \n",
" {'pace': {'Easy': 0.5, 'Steady': 0.5, 'Fast': 0.5}, \n",
" 'terrain': {'Light': 0.5, 'Mixed': 1.0, 'Rocky': 1.0, 'Muddy': 1.0}})\n",
" feats['plate_carbon_plate'] = get_priority_val(user_input, ['pace', 'terrain'], \n",
" {'pace': {'Easy': 0.5, 'Steady': 0.5, 'Fast': 1.0}, \n",
" 'terrain': {'Light': 0.5, 'Mixed': 0.5, 'Rocky': 0.5, 'Muddy': 0.5}})\n",
"\n",
" feats['width_fit'] = get_priority_val(user_input, ['foot_width'], \n",
" {'foot_width': {'Narrow': 0.2, 'Regular': 0.6, 'Wide': 1.0}})\n",
" feats['toebox_width'] = get_priority_val(user_input, ['foot_width'], \n",
" {'foot_width': {'Narrow': 0.2, 'Regular': 0.6, 'Wide': 1.0}})\n",
"\n",
" feats['stiffness_scaled'] = get_priority_val(user_input, ['pace'], \n",
" {'pace': {'Easy': 0.2, 'Steady': 0.6, 'Fast': 1.0}})\n",
"\n",
" feats['torsional_rigidity'] = get_priority_val(user_input, ['arch_type', 'pace'], \n",
" {'arch_type': {'Flat': 1.0, 'Normal': 0.5, 'High': 0.5}, \n",
" 'pace': {'Easy': 0.2, 'Steady': 0.6, 'Fast': 1.0}})\n",
"\n",
" feats['heel_stiff'] = get_priority_val(user_input, ['arch_type'], \n",
" {'arch_type': {'Flat': 1.0, 'Normal': 0.6, 'High': 0.2}})\n",
"\n",
" feats['lug_depth'] = get_priority_val(user_input, ['terrain'], \n",
" {'terrain': {'Light': 0.5, 'Mixed': 1.0, 'Rocky': 0.5, 'Muddy': 1.0}})\n",
"\n",
" prio_stack_h = ['strike_pattern', 'pace', 'terrain']\n",
" feats['heel_lab_mm'] = get_priority_val(user_input, prio_stack_h, {\n",
" 'strike_pattern': {'Heel': 1.0, 'Mid': 0.5, 'Forefoot': 0.0}, \n",
" 'pace': {'Easy': 1.0, 'Steady': 0.5, 'Fast': 0.0}, \n",
" 'terrain': {'Light': 0.5, 'Mixed': 1.0, 'Rocky': 1.0, 'Muddy': 1.0}})\n",
"\n",
" prio_stack_f = ['strike_pattern', 'pace', 'terrain']\n",
" feats['forefoot_lab_mm'] = get_priority_val(user_input, prio_stack_f, {\n",
" 'strike_pattern': {'Heel': 0.0, 'Mid': 0.5, 'Forefoot': 1.0}, \n",
" 'pace': {'Easy': 0.0, 'Steady': 0.5, 'Fast': 1.0}, \n",
" 'terrain': {'Light': 0.5, 'Mixed': 0.5, 'Rocky': 0.5, 'Muddy': 0.5}})\n",
"\n",
" feats['season_summer'] = get_priority_val(user_input, ['season'], \n",
" {'season': {'Summer': 1.0, 'Spring & Fall': 0.5, 'Winter': 0.0}})\n",
" feats['season_winter'] = get_priority_val(user_input, ['season'], \n",
" {'season': {'Summer': 0.0, 'Spring & Fall': 0.0, 'Winter': 1.0}})\n",
" feats['season_all'] = get_priority_val(user_input, ['season'], \n",
" {'season': {'Summer': 0.5, 'Spring & Fall': 1.0, 'Winter': 0.0}})\n",
"\n",
" feats['removable_insole'] = get_priority_val(user_input, ['orthotic_usage'], \n",
" {'orthotic_usage': {'Yes': 1.0, 'No': 0.5}})\n",
"\n",
" feats['waterproof'] = get_priority_val(user_input, ['water_resistance', 'terrain'], \n",
" {'water_resistance': {'Waterproof': 1.0, 'Water Repellent': 0.5}, \n",
" 'terrain': {'Light': 0.5, 'Mixed': 0.5, 'Rocky': 0.5, 'Muddy': 1.0}})\n",
" feats['water_repellent'] = get_priority_val(user_input, ['water_resistance', 'terrain'], \n",
" {'water_resistance': {'Waterproof': 1.0, 'Water Repellent': 1.0}, \n",
" 'terrain': {'Light': 0.5, 'Mixed': 1.0, 'Rocky': 0.5, 'Muddy': 1.0}})\n",
"\n",
" feats['lightweight'] = get_priority_val(user_input, ['pace'], \n",
" {'pace': {'Easy': 0.5, 'Steady': 0.5, 'Fast': 1.0}})\n",
"\n",
" provided_inputs = {k for k, v in user_input.items() if v} # Track which inputs user provided\n",
"\n",
" feature_sources = {\n",
" 'terrain_light': ['terrain'], 'terrain_moderate': ['terrain'], 'terrain_technical': ['terrain'],\n",
" 'shock_absorption': ['rock_sensitive', 'terrain'],\n",
" 'traction_scaled': ['terrain'],\n",
" 'arch_neutral': ['arch_type'], 'arch_stability': ['arch_type'],\n",
" 'drop_lab_mm': ['pace'],\n",
" 'strike_heel': ['strike_pattern', 'pace'], 'strike_mid': ['strike_pattern', 'pace'], 'strike_forefoot': ['strike_pattern', 'pace'],\n",
" 'midsole_softness': ['pace'],\n",
" 'plate_rock_plate': ['pace', 'terrain'], 'plate_carbon_plate': ['pace', 'terrain'],\n",
" 'width_fit': ['foot_width'], 'toebox_width': ['foot_width'],\n",
" 'stiffness_scaled': ['pace'],\n",
" 'torsional_rigidity': ['arch_type', 'pace'],\n",
" 'heel_stiff': ['arch_type'],\n",
" 'lug_depth': ['terrain'],\n",
" 'heel_lab_mm': ['strike_pattern', 'pace', 'terrain'], \n",
" 'forefoot_lab_mm': ['strike_pattern', 'pace', 'terrain'],\n",
" 'season_summer': ['season'], 'season_winter': ['season'], 'season_all': ['season'],\n",
" 'removable_insole': ['orthotic_usage'],\n",
" 'waterproof': ['water_resistance', 'terrain'], 'water_repellent': ['water_resistance', 'terrain'],\n",
" 'lightweight': ['pace'],\n",
" 'energy_return': [], 'toebox_durability': [], 'heel_durability': [], 'outsole_durability': [], 'breathability': []\n",
" }\n",
"\n",
" all_cols = binary_cols + continuous_cols\n",
" full_vector_raw = []\n",
" for col in binary_cols:\n",
" full_vector_raw.append(feats.get(col, 0.0))\n",
" for col in continuous_cols:\n",
" full_vector_raw.append(feats.get(col, 0.5))\n",
"\n",
" valid_indices = []\n",
" for i, col in enumerate(all_cols):\n",
" sources = feature_sources.get(col, [])\n",
" if any(src in provided_inputs for src in sources):\n",
" valid_indices.append(i)\n",
" \n",
" if not valid_indices:\n",
" valid_indices = list(range(len(all_cols)))\n",
" \n",
" return full_vector_raw, valid_indices"
]
},
{
"cell_type": "markdown",
"id": "3b0ec844",
"metadata": {},
"source": [
"## Reccomendation"
]
},
{
"cell_type": "code",
"execution_count": 73,
"id": "8f25c530",
"metadata": {},
"outputs": [],
"source": [
"def recommend_shoes_deep_masked(user_input, df_data, encoder_model, kmeans_model, binary_cols, continuous_cols, X_combined_data):\n",
" \"\"\"\n",
" Deep Learning Recommendation Pipeline with Masked Similarity.\n",
" \n",
" Pipeline:\n",
" 1. USER PREPROCESSING: Convert user preferences to feature vector with masking\n",
" 2. CLUSTER ROUTING: Encode user→latent space, select top K/3 closest clusters\n",
" 3. CANDIDATE RANKING: Score shoes via masked cosine similarity\n",
" 4. RESULT: Return top 10 ranked recommendations\n",
" \n",
" Masking Benefits:\n",
" - Reduces noise from unanswered questions\n",
" - Focuses similarity on user-provided dimensions only\n",
" - Example: if user only provided 'pace', similarity computed on pace-related features\n",
" \n",
" Args:\n",
" user_input (dict): User questionnaire responses\n",
" df_data (pd.DataFrame): Shoe catalog\n",
" encoder_model: Trained keras encoder\n",
" kmeans_model: Trained KMeans model (K clusters)\n",
" binary_cols (list): Binary feature names\n",
" continuous_cols (list): Continuous feature names\n",
" X_combined_data (np.ndarray): Preprocessed feature matrix (n_shoes, n_features)\n",
" \n",
" Returns:\n",
" pd.DataFrame: Top 10 shoes with index (row number) and match_score, sorted descending\n",
" \"\"\"\n",
" full_vector, valid_idx = preprocess_user_input_with_mask(user_input, binary_cols, continuous_cols)\n",
" full_vector = np.array([full_vector])\n",
"\n",
" user_latent = encoder_model.predict(full_vector, verbose=0)\n",
" distances = kmeans_model.transform(user_latent)[0]\n",
" n_select = math.ceil(kmeans_model.n_clusters / 3) # Select top 1/3 clusters for diversity\n",
" closest_clusters = np.argsort(distances)[:n_select]\n",
" \n",
" print(f\"User mapped to Clusters: {closest_clusters}\")\n",
" \n",
" candidates = df_data[df_data['cluster'].isin(closest_clusters)].copy()\n",
" if candidates.empty: \n",
" return pd.DataFrame()\n",
" \n",
" candidate_vectors = X_combined_data[candidates.index]\n",
" \n",
" user_vec_masked = full_vector[:, valid_idx] # Slice user vector to only relevant features\n",
" cand_vecs_masked = candidate_vectors[:, valid_idx] # Slice candidate vectors accordingly\n",
" \n",
" if np.all(user_vec_masked == 0):\n",
" scores = np.zeros(len(candidates))\n",
" else:\n",
" scores = cosine_similarity(user_vec_masked, cand_vecs_masked)[0] # Masked similarity calculation\n",
" \n",
" candidates['match_score'] = scores\n",
" \n",
" # Return: sorted by match_score descending, take top 10, keep only match_score (index included as row identifier)\n",
" return candidates.sort_values('match_score', ascending=False).head(10)[['match_score']]"
]
},
{
"cell_type": "markdown",
"id": "d8f17b84",
"metadata": {},
"source": [
"# Testing\n",
"Input options for recommendation engine test cases.\n",
"\n",
"Allows generation of random user preference combinations."
]
},
{
"cell_type": "markdown",
"id": "4feb81e3",
"metadata": {},
"source": [
"## Define Options"
]
},
{
"cell_type": "code",
"execution_count": 74,
"id": "c9071f9d",
"metadata": {},
"outputs": [],
"source": [
"input_options = {\n",
" 'terrain': ['Light', 'Mixed', 'Rocky', 'Muddy'],\n",
" 'rock_sensitive': ['Yes', 'No'],\n",
" 'pace': ['Easy', 'Steady', 'Fast'],\n",
" 'orthotic_usage': ['Yes', 'No'],\n",
" 'arch_type': ['Flat', 'Normal', 'High'],\n",
" 'strike_pattern': ['Heel', 'Mid', 'Forefoot'],\n",
" 'foot_width': ['Narrow', 'Regular', 'Wide'],\n",
" 'season': ['Summer', 'Spring & Fall', 'Winter'],\n",
" 'water_resistance': ['Waterproof', 'Water Repellent'],\n",
"}\n",
"\n",
"def generate_random_user_input(num_features):\n",
" \"\"\"\n",
" Generate randomized user preference input for testing and validation.\n",
" \n",
" Purpose: Creates realistic test cases with variable input completeness.\n",
" \n",
" Args:\n",
" num_features (int): Number of random features to include\n",
" \n",
" Returns:\n",
" dict: User preferences with num_features random keys/values\n",
" e.g., {'pace': 'Fast', 'arch_type': 'Normal', 'season': 'Summer'}\n",
" \"\"\"\n",
" all_keys = list(input_options.keys())\n",
" selected_keys = random.sample(all_keys, k=min(num_features, len(all_keys)))\n",
" \n",
" user_input = {}\n",
" for key in selected_keys:\n",
" user_input[key] = random.choice(input_options[key])\n",
" \n",
" return user_input"
]
},
{
"cell_type": "markdown",
"id": "080a36ee",
"metadata": {},
"source": [
"## Execution\n",
"Test Suite Execution\n",
"Runs recommendation engine on multiple test cases with varying input completeness.\n",
"\n",
"Tests: 3 features (partial), 6 features (moderate), 9 features (complete)"
]
},
{
"cell_type": "code",
"execution_count": 88,
"id": "116ec942",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== RECOMMENDATION ENGINE TEST SUITE ===\n",
"\n",
"------------------------------------------------------------\n",
"TEST CASE #1: User providing 3 preferences\n",
"User Input:\n",
"{'foot_width': 'Regular', 'rock_sensitive': 'No', 'strike_pattern': 'Forefoot'}\n",
"User mapped to Clusters: [1 4]\n",
"\n",
"Top 10 Recommendations:\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" brand | \n",
" name | \n",
" match_score | \n",
" cluster | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" altra | \n",
" outroad | \n",
" 0.905823 | \n",
" 1 | \n",
"
\n",
" \n",
" | 1 | \n",
" altra | \n",
" mont blanc carbon | \n",
" 0.901664 | \n",
" 4 | \n",
"
\n",
" \n",
" | 2 | \n",
" altra | \n",
" outroad 2 | \n",
" 0.894104 | \n",
" 1 | \n",
"
\n",
" \n",
" | 3 | \n",
" nike | \n",
" terra kiger 9 | \n",
" 0.892593 | \n",
" 4 | \n",
"
\n",
" \n",
" | 4 | \n",
" topo | \n",
" traverse | \n",
" 0.890732 | \n",
" 1 | \n",
"
\n",
" \n",
" | 5 | \n",
" salomon | \n",
" thundercross | \n",
" 0.889245 | \n",
" 4 | \n",
"
\n",
" \n",
" | 6 | \n",
" inov8 | \n",
" trailfly zero | \n",
" 0.887240 | \n",
" 1 | \n",
"
\n",
" \n",
" | 7 | \n",
" altra | \n",
" lone peak 9 | \n",
" 0.886799 | \n",
" 1 | \n",
"
\n",
" \n",
" | 8 | \n",
" scarpa | \n",
" spin planet | \n",
" 0.886360 | \n",
" 1 | \n",
"
\n",
" \n",
" | 9 | \n",
" altra | \n",
" outroad 3 | \n",
" 0.886279 | \n",
" 1 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" brand name match_score cluster\n",
"0 altra outroad 0.905823 1\n",
"1 altra mont blanc carbon 0.901664 4\n",
"2 altra outroad 2 0.894104 1\n",
"3 nike terra kiger 9 0.892593 4\n",
"4 topo traverse 0.890732 1\n",
"5 salomon thundercross 0.889245 4\n",
"6 inov8 trailfly zero 0.887240 1\n",
"7 altra lone peak 9 0.886799 1\n",
"8 scarpa spin planet 0.886360 1\n",
"9 altra outroad 3 0.886279 1"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"------------------------------------------------------------\n",
"TEST CASE #2: User providing 6 preferences\n",
"User Input:\n",
"{'rock_sensitive': 'No', 'arch_type': 'High', 'season': 'Spring & Fall', 'pace': 'Fast', 'orthotic_usage': 'Yes', 'strike_pattern': 'Heel'}\n",
"User mapped to Clusters: [0 1]\n",
"\n",
"Top 10 Recommendations:\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" brand | \n",
" name | \n",
" match_score | \n",
" cluster | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" new balance | \n",
" fuelcell supercomp trail | \n",
" 0.864520 | \n",
" 0 | \n",
"
\n",
" \n",
" | 1 | \n",
" kailas | \n",
" fuga yao | \n",
" 0.804054 | \n",
" 0 | \n",
"
\n",
" \n",
" | 2 | \n",
" on | \n",
" cloudvista | \n",
" 0.792374 | \n",
" 0 | \n",
"
\n",
" \n",
" | 3 | \n",
" asics | \n",
" metafuji trail | \n",
" 0.776813 | \n",
" 0 | \n",
"
\n",
" \n",
" | 4 | \n",
" merrell | \n",
" morphlite | \n",
" 0.771201 | \n",
" 0 | \n",
"
\n",
" \n",
" | 5 | \n",
" nike | \n",
" ultrafly | \n",
" 0.764686 | \n",
" 0 | \n",
"
\n",
" \n",
" | 6 | \n",
" la sportiva | \n",
" mutant | \n",
" 0.763105 | \n",
" 0 | \n",
"
\n",
" \n",
" | 7 | \n",
" hoka | \n",
" mafate x | \n",
" 0.757556 | \n",
" 0 | \n",
"
\n",
" \n",
" | 8 | \n",
" kailas | \n",
" fuga elite 2 | \n",
" 0.754621 | \n",
" 0 | \n",
"
\n",
" \n",
" | 9 | \n",
" new balance | \n",
" tektrel | \n",
" 0.748259 | \n",
" 1 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" brand name match_score cluster\n",
"0 new balance fuelcell supercomp trail 0.864520 0\n",
"1 kailas fuga yao 0.804054 0\n",
"2 on cloudvista 0.792374 0\n",
"3 asics metafuji trail 0.776813 0\n",
"4 merrell morphlite 0.771201 0\n",
"5 nike ultrafly 0.764686 0\n",
"6 la sportiva mutant 0.763105 0\n",
"7 hoka mafate x 0.757556 0\n",
"8 kailas fuga elite 2 0.754621 0\n",
"9 new balance tektrel 0.748259 1"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"------------------------------------------------------------\n",
"TEST CASE #3: User providing 9 preferences\n",
"User Input:\n",
"{'foot_width': 'Regular', 'water_resistance': 'Waterproof', 'strike_pattern': 'Mid', 'season': 'Winter', 'rock_sensitive': 'No', 'pace': 'Steady', 'arch_type': 'Normal', 'terrain': 'Muddy', 'orthotic_usage': 'Yes'}\n",
"User mapped to Clusters: [3 4]\n",
"\n",
"Top 10 Recommendations:\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" brand | \n",
" name | \n",
" match_score | \n",
" cluster | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" hoka | \n",
" challenger 7 gtx | \n",
" 0.831882 | \n",
" 3 | \n",
"
\n",
" \n",
" | 1 | \n",
" merrell | \n",
" agility peak 5 gtx | \n",
" 0.785247 | \n",
" 3 | \n",
"
\n",
" \n",
" | 2 | \n",
" salomon | \n",
" speedcross 6 gtx | \n",
" 0.773513 | \n",
" 3 | \n",
"
\n",
" \n",
" | 3 | \n",
" hoka | \n",
" speedgoat 5 gtx | \n",
" 0.748381 | \n",
" 3 | \n",
"
\n",
" \n",
" | 4 | \n",
" hoka | \n",
" speedgoat 6 gtx | \n",
" 0.735479 | \n",
" 3 | \n",
"
\n",
" \n",
" | 5 | \n",
" salomon | \n",
" thundercross | \n",
" 0.726806 | \n",
" 4 | \n",
"
\n",
" \n",
" | 6 | \n",
" salomon | \n",
" genesis | \n",
" 0.720264 | \n",
" 4 | \n",
"
\n",
" \n",
" | 7 | \n",
" salomon | \n",
" speedcross 6 | \n",
" 0.714786 | \n",
" 3 | \n",
"
\n",
" \n",
" | 8 | \n",
" icebug | \n",
" järv rb9x | \n",
" 0.706436 | \n",
" 4 | \n",
"
\n",
" \n",
" | 9 | \n",
" nike | \n",
" pegasus trail 4 gtx | \n",
" 0.699563 | \n",
" 3 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" brand name match_score cluster\n",
"0 hoka challenger 7 gtx 0.831882 3\n",
"1 merrell agility peak 5 gtx 0.785247 3\n",
"2 salomon speedcross 6 gtx 0.773513 3\n",
"3 hoka speedgoat 5 gtx 0.748381 3\n",
"4 hoka speedgoat 6 gtx 0.735479 3\n",
"5 salomon thundercross 0.726806 4\n",
"6 salomon genesis 0.720264 4\n",
"7 salomon speedcross 6 0.714786 3\n",
"8 icebug järv rb9x 0.706436 4\n",
"9 nike pegasus trail 4 gtx 0.699563 3"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"target_counts = [3, 6, 9]\n",
"\n",
"print(\"=== RECOMMENDATION ENGINE TEST SUITE ===\")\n",
"\n",
"for i, count in enumerate(target_counts):\n",
" print(f\"\\n{'-'*60}\")\n",
" print(f\"TEST CASE #{i+1}: User providing {count} preferences\")\n",
" \n",
" random_input = generate_random_user_input(count)\n",
" print(f\"User Input:\\n{random_input}\")\n",
" \n",
" try:\n",
" recommendations = recommend_shoes_deep_masked(\n",
" random_input, \n",
" df, \n",
" encoder, \n",
" best_model, \n",
" binary_cols, \n",
" continuous_cols, \n",
" X_combined\n",
" )\n",
" \n",
" if not recommendations.empty:\n",
" print(\"\\nTop 10 Recommendations:\")\n",
" # Get brand, name, cluster from original df using index, add match_score from recommendations\n",
" result_df = pd.DataFrame({\n",
" 'brand': df.loc[recommendations.index, 'brand'].values,\n",
" 'name': df.loc[recommendations.index, 'name'].values,\n",
" 'match_score': recommendations['match_score'].values,\n",
" 'cluster': df.loc[recommendations.index, 'cluster'].values\n",
" })\n",
" display(result_df)\n",
" else:\n",
" print(\"\\nNo recommendations found (cluster empty).\")\n",
" \n",
" except NameError:\n",
" print(\"\\nERROR: Ensure model and preprocessing functions are loaded.\")\n",
" except Exception as e:\n",
" print(f\"\\nERROR: {e}\")"
]
},
{
"cell_type": "markdown",
"id": "0b07ca18",
"metadata": {},
"source": [
"# Save Artifacts\n",
"Saves 4 artifacts for complete model reconstruction:\n",
"1. shoe_encoder.keras: Trained autoencoder (feature encoding)\n",
"2. kmeans_model.pkl: Trained K-means clusters\n",
"3. shoe_metadata.pkl: Complete shoe dataset with cluster assignments\n",
"4. shoe_features.pkl: Preprocessed feature matrix (X_combined)\n",
"\n",
"Artifacts stored in timestamped versioned directories for traceability."
]
},
{
"cell_type": "code",
"execution_count": 76,
"id": "02e2f531",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Saving models to: ../../model_artifacts/trail/v_20260214_114839\n",
"Models saved successfully!\n"
]
}
],
"source": [
"timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
"save_dir = f\"../../model_artifacts/trail/v_{timestamp}\"\n",
"\n",
"os.makedirs(save_dir, exist_ok=True)\n",
"print(f\"Saving models to: {save_dir}\")\n",
"\n",
"encoder.save(f'{save_dir}/shoe_encoder.keras')\n",
"\n",
"with open(f'{save_dir}/kmeans_model.pkl', 'wb') as f:\n",
" pickle.dump(best_model, f)\n",
"\n",
"df.to_pickle(f'{save_dir}/shoe_metadata.pkl')\n",
"\n",
"with open(f'{save_dir}/shoe_features.pkl', 'wb') as f:\n",
" pickle.dump(X_combined, f)\n",
"\n",
"print(\"Models saved successfully!\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "env (3.13.1)",
"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.13.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}