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.
TruncatedSVD(n_components=50, random_state=42)
"
+ ],
+ "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": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHHCAYAAABDUnkqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAA9hAAAPYQGoP6dpAABZpUlEQVR4nO3deVxU5f4H8M9hgBl2AWUTBBRFccFdwRWX1Mz0llmmqbncX6Xl1qK323Wr0G5WWqZppZlXrTSsLLfAXdxAVEAUEBCVRURmAAFh5vz+QCcnFgFhzjDzeb9e51VzznPOfAfK+fic5zyPIIqiCCIiIiIjYSZ1AURERET1ieGGiIiIjArDDRERERkVhhsiIiIyKgw3REREZFQYboiIiMioMNwQERGRUWG4ISIiIqPCcENERERGheGGiColCAIWL16sfb148WIIgoCcnBzpijJQPj4+eOqppxr8fQ4dOgRBEHDo0KEGfy+ixozhhsiEbNq0CYIgVLmdPHlS6hLrzMfHB4IgYMiQIZUe37Bhg/Zznj17ttbXj4+Px+LFi5GamvqYlRJRQzOXugAi0r+lS5fC19e3wn4/Pz8Jqqk/CoUCBw8eRGZmJtzc3HSO/e9//4NCoUBxcXGdrh0fH48lS5Zg4MCB8PHxqYdqiaihMNwQmaARI0age/fuUpdR7/r06YMzZ87ghx9+wOzZs7X7r1+/jqNHj+If//gHdu7cKWGFRKQPvC1FRLWSk5ODcePGwd7eHs7Ozpg9e3aF3pCysjIsW7YMrVq1glwuh4+PD/71r3+hpKRE22bevHlwdnaGKIrafa+//joEQcDq1au1+7KysiAIAtauXfvI2hQKBZ555hls3bpVZ/+2bdvg6OiIYcOGVXpeQkICxo4dCycnJygUCnTv3h2//vqr9vimTZvw3HPPAQBCQkK0t7f+Pvbl2LFj6NmzJxQKBVq2bInNmzdXeK+rV6/iueeeg5OTE6ytrdG7d2/8/vvvFdpdv34dY8aMgY2NDVxcXDB37lydnx8RVY3hhsgEKZVK5OTk6Gy3b9+u0bnjxo1DcXExQkND8eSTT2L16tX45z//qdNm+vTp+M9//oOuXbvi008/xYABAxAaGooXXnhB26Zfv37Izc1FXFycdt/Ro0dhZmaGo0eP6uwDgP79+9eovhdffBGnT59GcnKydt/WrVsxduxYWFhYVGgfFxeH3r1749KlS1iwYAFWrlwJGxsbjBkzBmFhYdr3fuONNwAA//rXv/D999/j+++/R7t27bTXSUpKwtixYzF06FCsXLkSjo6OmDJlis7ny8rKQnBwMPbt24fXXnsNH3zwAYqLi/H0009r3wsAioqKMHjwYOzbtw+zZs3Cu+++i6NHj+Ltt9+u0c+AyOSJRGQyNm7cKAKodJPL5TptAYiLFi3Svl60aJEIQHz66ad12r322msiAPH8+fOiKIpiTEyMCECcPn26Trs333xTBCBGRESIoiiK2dnZIgDxyy+/FEVRFPPy8kQzMzPxueeeE11dXbXnvfHGG6KTk5Oo0Wiq/Wze3t7iyJEjxbKyMtHNzU1ctmyZKIqiGB8fLwIQDx8+rP38Z86c0Z43ePBgsWPHjmJxcbF2n0ajEYODg8XWrVtr9/30008iAPHgwYOVvjcA8ciRI9p92dnZolwuF+fPn6/dN2fOHBGAePToUe2+/Px80dfXV/Tx8RHVarUoiqL42WefiQDEH3/8UduusLBQ9PPzq7IGIvoLe26ITNCaNWtw4MABnW3Pnj01OnfmzJk6r19//XUAwB9//KHzz3nz5um0mz9/PgBob8E0a9YMbdu2xZEjRwAAx48fh0wmw1tvvYWsrCwkJiYCKO+56du3LwRBqFF9MpkM48aNw7Zt2wCUDyT28vJCv379KrTNzc1FREQExo0bh/z8fJ1erGHDhiExMRE3btyo0fsGBATovEezZs3g7++Pq1evavf98ccf6NmzJ/r27avdZ2tri3/+859ITU1FfHy8tp27uzvGjh2rbWdtbV2hh4yIKmfS4ebIkSMYNWoUPDw8IAgCdu3aVetriKKIjz/+GG3atIFcLkfz5s3xwQcf1H+xRPWoZ8+eGDJkiM4WEhJSo3Nbt26t87pVq1YwMzPTPiKdlpYGMzOzCk9eubm5oUmTJkhLS9Pu69evn/a209GjR9G9e3d0794dTk5OOHr0KFQqFc6fP19pMKnOiy++iPj4eJw/fx5bt27FCy+8UGk4SkpKgiiKeO+999CsWTOdbdGiRQCA7OzsGr1nixYtKuxzdHTEnTt3tK/T0tLg7+9fod2D21sPfjZpaWnw8/OrUHNl5xJRRSb9tFRhYSECAwMxdepUPPPMM3W6xuzZs7F//358/PHH6NixI3Jzc5Gbm1vPlRIZrqp6VGrS09K3b19s2LABV69exdGjR9GvXz8IgoC+ffvi6NGj8PDwgEajqXW46dWrF1q1aoU5c+YgJSUFL774YqXtNBoNAODNN9+scrBxTR+Pl8lkle4XHxowTUT6YdLhZsSIERgxYkSVx0tKSvDuu+9i27ZtyMvLQ4cOHbBixQoMHDgQAHDp0iWsXbsWsbGx2r9RVTZ3CJExSUxM1PnvPCkpCRqNRjv3i7e3NzQaDRITE3UG3GZlZSEvLw/e3t7afQ9Cy4EDB3DmzBksWLAAQPkA3rVr18LDwwM2Njbo1q1brescP3483n//fbRr1w6dO3eutE3Lli0BABYWFlVO/vdATW+LVcfb2xuXL1+usD8hIUF7/ME/Y2NjIYqizvtWdi4RVWTSt6UeZdasWYiMjMT27dtx4cIFPPfccxg+fLh2LMBvv/2Gli1bYvfu3fD19YWPjw+mT5/OnhsyamvWrNF5/fnnnwOA9i8KTz75JADgs88+02n3ySefAABGjhyp3efr64vmzZvj008/RWlpKfr06QOgPPQkJydjx44d6N27N8zNa//3sOnTp2PRokVYuXJllW1cXFwwcOBAfPXVV8jIyKhw/NatW9p/t7GxAQDk5eXVupYHnnzySZw+fRqRkZHafYWFhVi/fj18fHwQEBCgbXfz5k3s2LFD2+7u3btYv359nd+byJSYdM9Nda5du4aNGzfi2rVr8PDwAFDedb13715s3LgRH374Ia5evYq0tDT89NNP2Lx5M9RqNebOnYuxY8ciIiJC4k9AVLU9e/ZoewseFhwcrO3NqEpKSgqefvppDB8+HJGRkdiyZQtefPFFBAYGAgACAwMxefJkrF+/Hnl5eRgwYABOnz6N7777DmPGjKkwtqdfv37Yvn07OnbsCEdHRwBA165dYWNjgytXrlR5S+lRvL29ddbGqsqaNWvQt29fdOzYETNmzEDLli2RlZWFyMhIXL9+HefPnwcAdO7cGTKZDCtWrIBSqYRcLsegQYPg4uJS45oWLFiAbdu2YcSIEXjjjTfg5OSE7777DikpKdi5cyfMzMr/vjljxgx88cUXmDRpEqKiouDu7o7vv/8e1tbWdfpZEJkcaR/WMhwAxLCwMO3r3bt3iwBEGxsbnc3c3FwcN26cKIqiOGPGDBGAePnyZe15UVFRIgAxISFB3x+B6JGqexQcgLhx40ZtW1TxKHh8fLw4duxY0c7OTnR0dBRnzZolFhUV6bxPaWmpuGTJEtHX11e0sLAQvby8xIULF+o8bv3AmjVrRADiq6++qrN/yJAhIgAxPDy8Rp/twaPgNfn8Dz8KLoqimJycLE6aNEl0c3MTLSwsxObNm4tPPfWUuGPHDp12GzZsEFu2bCnKZDKdR7Kreu8BAwaIAwYMqPBeY8eOFZs0aSIqFAqxZ8+e4u7duyucm5aWJj799NOitbW12LRpU3H27Nni3r17+Sg4UQ0IosjRbkD5/fSwsDCMGTMGAPDDDz9gwoQJiIuLqzBQ0NbWFm5ubli0aBE+/PBDlJaWao8VFRXB2toa+/fvx9ChQ/X5EYiIiAi8LVWlLl26QK1WIzs7u8onNfr06YOysjIkJyejVatWAIArV64AgM6gSSIiItIfk+65KSgoQFJSEoDyMPPJJ58gJCQETk5OaNGiBSZOnIjjx49j5cqV6NKlC27duoXw8HB06tQJI0eOhEajQY8ePWBra4vPPvsMGo0GM2fOhL29Pfbv3y/xpyMiIjJNJh1uDh06VOnEZZMnT8amTZtQWlqK999/H5s3b8aNGzfQtGlT9O7dG0uWLEHHjh0BADdv3sTrr7+O/fv3w8bGBiNGjMDKlSvh5OSk749DREREMPFwQ0RERMaH89wQERGRUWG4ISIiIqNick9LaTQa3Lx5E3Z2dvUynToRERE1PFEUkZ+fDw8PD+2El1UxuXBz8+ZNeHl5SV0GERER1UF6ejo8PT2rbWNy4cbOzg5A+Q/H3t5e4mqIiIioJlQqFby8vLTf49UxuXDz4FaUvb09ww0REVEjU5MhJRxQTEREREaF4YaIiIiMCsMNERERGRWGGyIiIjIqDDdERERkVBhuiIiIyKgw3BAREZFRYbghIiIio8JwQ0REREbF5GYobihqjYjTKbnIzi+Gi50CPX2dIDPjwpxERET6xnBTD/bGZmDJb/HIUBZr97k7KLBoVACGd3CXsDIiIiLTYzC3pZYvXw5BEDBnzpxq2/30009o27YtFAoFOnbsiD/++EM/BVZhb2wGXt0SrRNsACBTWYxXt0Rjb2yGRJURERGZJoMIN2fOnMFXX32FTp06VdvuxIkTGD9+PKZNm4Zz585hzJgxGDNmDGJjY/VUqS61RsSS3+IhVnLswb4lv8VDramsBRERETUEycNNQUEBJkyYgA0bNsDR0bHatqtWrcLw4cPx1ltvoV27dli2bBm6du2KL774Qk/V6jqdkluhx+ZhIoAMZTFOp+TqrygiIiITJ3m4mTlzJkaOHIkhQ4Y8sm1kZGSFdsOGDUNkZGSV55SUlEClUuls9SU7v+pgU5d2RERE9PgkHVC8fft2REdH48yZMzVqn5mZCVdXV519rq6uyMzMrPKc0NBQLFmy5LHqrIqLnaJe2xEREdHjk6znJj09HbNnz8b//vc/KBQN9+W/cOFCKJVK7Zaenl5v1+7p6wR3BwWqeuBbQPlTUz19nertPYmIiKh6koWbqKgoZGdno2vXrjA3N4e5uTkOHz6M1atXw9zcHGq1usI5bm5uyMrK0tmXlZUFNze3Kt9HLpfD3t5eZ6svMjMBi0YFAECVAWfRqADOd0NERKRHkoWbwYMH4+LFi4iJidFu3bt3x4QJExATEwOZTFbhnKCgIISHh+vsO3DgAIKCgvRVdgXDO7hj7cSucHOo2Pv0bNfmnOeGiIhIzyQbc2NnZ4cOHTro7LOxsYGzs7N2/6RJk9C8eXOEhoYCAGbPno0BAwZg5cqVGDlyJLZv346zZ89i/fr1eq//YcM7uGNogJt2huL4myp8deQq/kzIhqq4FPYKC0nrIyIiMiWSPy1VnWvXriEj469J8IKDg7F161asX78egYGB2LFjB3bt2lUhJElBZiYgqJUzRndujreG+cPPxRZ5d0ux4chVqUsjIiIyKYIoiiY1w5xKpYKDgwOUSmW9jr/5u72xmXhlSxSsLGQ4/PZAPjFFRET0GGrz/W3QPTeN2bD2rujs1QRFpWp8Hp4kdTlEREQmg+GmgQiCgHeGtwUAbDt9DWm3CyWuiIiIyDQw3DSgoFbOGNCmGco0IlbuvyJ1OURERCaB4aaBvT3cHwDw6/mbiLuplLgaIiIi48dw08Daezjg6UAPAMBHey9LXA0REZHxY7jRg/lPtIG5mYDDV24hMvm21OUQEREZNYYbPfB2tsH4ni0AACv2JsDEnr4nIiLSK4YbPXl9sB+sLGSISc/DvrisR59AREREdcJwoycudgpM7+cLAPjvvgSUqTUSV0RERGScGG70aEb/lnC0tkDyrUL8HH1D6nKIiIiMEsONHtkrLDAzxA8A8OmfV1Bcqpa4IiIiIuPDcKNnE3t7w8NBgQxlMTZHpkpdDhERkdFhuNEzhYUMc4a2AQCsOZgMZVGpxBUREREZF4YbCTzb1ROtXWyhLCrF+iPJUpdDRERkVBhuJCAzE/DmsPJlGb49lopsVbHEFRERERkPhhuJPBHgiq4tmqCoVI3VEYlSl0NERGQ0GG4kIggC3hneFgCw/XQ6UnMKJa6IiIjIODDcSKhXS2cM9G+GMo2IlQeuSF0OERGRUWC4kdjbw8p7b347fxOxN5QSV0NERNT4MdxILMDDHqM7ewAoX1STiIiIHg/DjQGYP9QfFjIBRxNzcCIpR+pyiIiIGjWGGwPQwtkaL/ZsAQBYse8yRFGUuCIiIqLGi+HGQMwa1BrWljKcT8/DvrhMqcshIiJqtBhuDEQzOzmm9/UFAHy07zLK1BqJKyIiImqcGG4MyIz+LeFobYGrtwqxI+q61OUQERE1Sgw3BsROYYGZIX4AgM/+TERxqVriioiIiBofhhsDM7G3N5o3sUKmqhjfnUiVuhwiIqJGh+HGwCgsZJgzpDUA4MtDyVAWlUpcERERUePCcGOAnunqiTautlAWleKrw8lSl0NERNSoMNwYIJmZgLfuL8vw7fEUZKmKJa6IiIio8WC4MVBD2rmgm7cjiks1WBWeKHU5REREjQbDjYESBAHvDC/vvfnhTDpScgolroiIiKhxYLgxYD19nRDi3wxqjYiP91+WuhwiIqJGgeHGwL09vC0EAfj9QgYuXldKXQ4REZHBY7gxcO3c7TGmc3MAwEf7EiSuhoiIyPAx3DQC84a2gYVMwNHEHBxPypG6HCIiIoPGcNMIeDlZY0IvbwDAir0JEEVR4oqIiIgMF8NNIzFrkB+sLWW4cF2JPbGZUpdDRERksCQNN2vXrkWnTp1gb28Pe3t7BAUFYc+ePVW237RpEwRB0NkUCoUeK5ZOU1s5pvdrCQD4eN9llKk1EldERERkmCQNN56enli+fDmioqJw9uxZDBo0CKNHj0ZcXFyV59jb2yMjI0O7paWl6bFiac3o5wsnG0tczSnET1HXpS6HiIjIIEkabkaNGoUnn3wSrVu3Rps2bfDBBx/A1tYWJ0+erPIcQRDg5uam3VxdXfVYsbTsFBaYGeIHAPjszysouqeWuCIiIiLDYzBjbtRqNbZv347CwkIEBQVV2a6goADe3t7w8vJ6ZC8PAJSUlEClUulsjdnE3i3QvIkVslQl2HQiVepyiIiIDI7k4ebixYuwtbWFXC7HK6+8grCwMAQEBFTa1t/fH99++y1++eUXbNmyBRqNBsHBwbh+vepbNKGhoXBwcNBuXl5eDfVR9EJuLsO8oW0AAGsPJUF5t1TiioiIiAyLIEr8XPG9e/dw7do1KJVK7NixA19//TUOHz5cZcB5WGlpKdq1a4fx48dj2bJllbYpKSlBSUmJ9rVKpYKXlxeUSiXs7e3r7XPok1oj4slVR3E5Kx+vDGiFBSPaSl0SERFRg1KpVHBwcKjR97fkPTeWlpbw8/NDt27dEBoaisDAQKxatapG51pYWKBLly5ISkqqso1cLtc+jfVga+xkZgLeGuYPANh4PAWZymKJKyIiIjIckoebv9NoNDo9LdVRq9W4ePEi3N3dG7gqwzO4nQu6ezuipEyDVeGJUpdDRERkMCQNNwsXLsSRI0eQmpqKixcvYuHChTh06BAmTJgAAJg0aRIWLlyobb906VLs378fV69eRXR0NCZOnIi0tDRMnz5dqo8gGUEQ8M7921E/nk1H8q0CiSsiIiIyDOZSvnl2djYmTZqEjIwMODg4oFOnTti3bx+GDh0KALh27RrMzP7KX3fu3MGMGTOQmZkJR0dHdOvWDSdOnKjR+Bxj1MPHCYPbuiA8IRuf7L+CNRO6Sl0SERGR5CQfUKxvtRmQ1BgkZKowYtVRiCLw66w+6OTZROqSiIiI6l2jGlBMj6etmz3+0bk5gPJFNYmIiEwdw40RmDu0DSxkAo4n3caxxBypyyEiIpIUw40R8HKyxoRe3gDKe280GpO600hERKSD4cZIzBrkBxtLGS7eUGJPbKbU5RAREUmG4cZINLWVY3q/lgCAj/dfRqlaI3FFRERE0mC4MSIz+reEs40lUnIK8ePZdKnLISIikgTDjRGxlZtj1iA/AMCqPxNRdE8tcUVERET6x3BjZF7s1QKejlbIzi/BxhMpUpdDRESkdww3RkZuLsO8oW0AAGsPJSPv7j2JKyIiItIvhhsjNLpzc/i72iG/uAxrDydLXQ4REZFeMdwYIZmZgLeH+wMANh1PRYaySOKKiIiI9IfhxkgNauuCHj6OKCnTYNWfiVKXQ0REpDcMN0ZKEAS8M7wtAODHs+lIyi6QuCIiIiL9YLgxYt19nDCknQs0IrBy/2WpyyEiItILhhsj9+YwfwgCsCc2E+fT86Quh4iIqMEx3Bi5tm72+EeX5gDKF9UURS6qSURExo3hxgTMHdIGljIznEi+jaOJOVKXQ0RE1KAYbkyAl5M1JvRuAQD4aF8CNBr23hARkfFiuDERs0L8YCs3R+wNFX6/mCF1OURERA2G4cZEONvKMaNfSwDlT06VqjUSV0RERNQwGG5MyLR+vnC2sUTq7bv44Uy61OUQERE1CIYbE2IrN8frg/wAAKvCE3H3XpnEFREREdU/hhsTM75XC3g6WuFWfgk2Hk+VuhwiIqJ6x3BjYuTmMsx/og0AYN2hZNwpvCdxRURERPWL4cYEjQ5sjrZudsgvKcPaw8lSl0NERFSvGG5MkJmZgLeH+wMANp1IRYaySOKKiIiI6g/DjYkK8XdBTx8n3CvT4LMDiVKXQ0REVG8YbkyUIAh4Z0R5781PUelIys6XuCIiIqL6wXBjwrp5O2FIO1doRODjfVekLoeIiKheMNyYuLeH+0MQgL1xmTh37Y7U5RARET02hhsT18bVDs908QQArNibAFHkoppERNS4MdwQ5g5tDUuZGU5ezcWRxBypyyEiInosDDcET0drvBTkDQBY/sclnEjKwS8xNxCZfBtqDXtyiIiocRFEE7sPoVKp4ODgAKVSCXt7e6nLMRi5hfcQHBqO4jLd1cLdHRRYNCoAwzu4S1QZERFR7b6/2XNDAIDTKbcrBBsAyFQW49Ut0dgbmyFBVURERLXHcENQa0Qs+S2+0mMPuvWW/BbPW1RERNQoMNwQTqfkIkNZXOVxEUCGshinU3L1VxQREVEdSRpu1q5di06dOsHe3h729vYICgrCnj17qj3np59+Qtu2baFQKNCxY0f88ccfeqrWeGXnVx1s6tKOiIhISpKGG09PTyxfvhxRUVE4e/YsBg0ahNGjRyMuLq7S9idOnMD48eMxbdo0nDt3DmPGjMGYMWMQGxur58qNi4udol7bERERScngnpZycnLCf//7X0ybNq3Cseeffx6FhYXYvXu3dl/v3r3RuXNnrFu3rkbX59NSFak1IvquiECmshiV/ccgAHBzUODYO4MgMxP0XR4REVHjfFpKrVZj+/btKCwsRFBQUKVtIiMjMWTIEJ19w4YNQ2RkpD5KNFoyMwGLRgUAKA8ylVk0KoDBhoiIGgXJw83Fixdha2sLuVyOV155BWFhYQgICKi0bWZmJlxdXXX2ubq6IjMzs8rrl5SUQKVS6WxU0fAO7lg7sSvcHCreehrX3Yvz3BARUaNhLnUB/v7+iImJgVKpxI4dOzB58mQcPny4yoBTW6GhoViyZEm9XMvYDe/gjqEBbjidkovs/GKcT8/Dt8dTcfjKLZSUqSE3l0ldIhER0SNJ3nNjaWkJPz8/dOvWDaGhoQgMDMSqVasqbevm5oasrCydfVlZWXBzc6vy+gsXLoRSqdRu6enp9Vq/sZGZCQhq5YzRnZvjnRFt4WavQKaqGD+e4c+NiIgaB8nDzd9pNBqUlJRUeiwoKAjh4eE6+w4cOFDlGB0AkMvl2kfNH2xUM3JzGV4d2AoA8OWhZJSUqSWuiIiI6NEkDTcLFy7EkSNHkJqaiosXL2LhwoU4dOgQJkyYAACYNGkSFi5cqG0/e/Zs7N27FytXrkRCQgIWL16Ms2fPYtasWVJ9BKP3fA8vuNrLkaEsxk9nr0tdDhER0SNJGm6ys7MxadIk+Pv7Y/DgwThz5gz27duHoUOHAgCuXbuGjIy/1jQKDg7G1q1bsX79egQGBmLHjh3YtWsXOnToINVHMHoKCxleGVDee7P2UDLuVbL+FBERkSExuHluGhrnuam94lI1+n10ELfyS/DhPzrixV4tpC6JiIhMTKOc54YM18O9N2sOJrH3hoiIDBrDDdXIhF4t0NRWjht5RdgZzbE3RERkuBhuqEbKe29aAijvvSlVs/eGiIgME8MN1diEXt5oamuJ63eK8DN7b4iIyEAx3FCNWVnK8H/9y8fefMHeGyIiMlAMN1QrE3q3gLONJdJzixB27obU5RAREVXAcEO1Ym1pjn/2/2vsTRl7b4iIyMAw3FCtvRTkDScbS6TdvotdMTelLoeIiEgHww3VmrWlOWb0K++9+SIikb03RERkUBhuqE4mBXnD0doCqbfv4tfz7L0hIiLDwXBDdWIjN8d0be9NEtQak1rFg4iIDBjDDdXZ5GAfNLG2wNWcQvzG3hsiIjIQDDdUZ7Zyc0zv6wsAWB2RyN4bIiIyCAw39FgmB/vAwcoCV28VYvcF9t4QEZH0GG7osdgpLDDtfu/N5xx7Q0REBoDhhh7blD4+sFeYIym7AH9czJC6HCIiMnEMN/TY7BUWmPpg7E14IjTsvSEiIgkx3FC9eLmPL+wU5kjMLsAfsey9ISIi6TDcUL1wsLLA1D7svSEiIukx3FC9mdrHF3Zyc1zJKsDeuEypyyEiIhPFcEP1xsHaAi/38QHA3hsiIpIOww3Vq6l9fWErN0dCZj72x7P3hoiI9I/hhupVE2tLTAn2AQCsCk9i7w0REekdww3Vu2l9fWFjKcOlDBUOXMqSuhwiIjIxDDdU7xxtLDH5fu/N6vBEiCJ7b4iISH8YbqhBTO/XEtaWMsTdVOHPS9lSl0NERCaE4YYahJONJSYF+QAAVoVfYe8NERHpDcMNNZgZ/XxhZSFD7A0VIhLYe0NERPrBcEMNxtlWjklB3gCAVRx7Q0REesJwQw1qRv+WsLKQ4cJ1JQ5dviV1OUREZAIYbqhBNbWV46X7vTefsfeGiIj0gOGGGtyMfi2hsDDD+fQ8HL7C3hsiImpYDDfU4JrZyTGx1/3emz/Ze0NERA2L4Yb04p8DWkJuboaY9DwcScyRuhwiIjJiDDekFy52Cky433uz6k/Oe0NERA2H4Yb05pX7vTfR1/JwLIm9N0RE1DAYbkhvXOwVGN+zBQBgFcfeEBFRA2G4Ib16dWArWJqb4WzaHZxIvi11OUREZIQkDTehoaHo0aMH7Ozs4OLigjFjxuDy5cvVnrNp0yYIgqCzKRQKPVVMj8vVXoHxPbwAsPeGiIgahqTh5vDhw5g5cyZOnjyJAwcOoLS0FE888QQKCwurPc/e3h4ZGRnaLS0tTU8VU314ZWArWMrMcDo1F5FX2XtDRET1y1zKN9+7d6/O602bNsHFxQVRUVHo379/lecJggA3N7eGLo8aiLuDFZ7v4YXvT6Zh1Z+JCG7VVOqSiIjIiBjUmBulUgkAcHJyqrZdQUEBvL294eXlhdGjRyMuLq7KtiUlJVCpVDobSe/Vga1gIRNwKiUXJ9l7Q0RE9chgwo1Go8GcOXPQp08fdOjQocp2/v7++Pbbb/HLL79gy5Yt0Gg0CA4OxvXr1yttHxoaCgcHB+3m5eXVUB+BasGjiRXGdf9r7A0REVF9EUQDGdH56quvYs+ePTh27Bg8PT1rfF5paSnatWuH8ePHY9myZRWOl5SUoKSkRPtapVLBy8sLSqUS9vb29VI71c2NvCIM/O9BlKpF/Ph/QejpW32PHRERmS6VSgUHB4cafX8bRM/NrFmzsHv3bhw8eLBWwQYALCws0KVLFyQlJVV6XC6Xw97eXmcjw9C8iRWee9B7E35F4mqIiMhY1HlA8dmzZ/Hjjz/i2rVruHfvns6xn3/+uUbXEEURr7/+OsLCwnDo0CH4+vrWug61Wo2LFy/iySefrPW5JL3XBrbCT2fTcTzpNs6m5qK7D3tviIjo8dSp52b79u0IDg7GpUuXEBYWhtLSUsTFxSEiIgIODg41vs7MmTOxZcsWbN26FXZ2dsjMzERmZiaKioq0bSZNmoSFCxdqXy9duhT79+/H1atXER0djYkTJyItLQ3Tp0+vy0chiXk6WmNst/LeulXhHHtDRESPr07h5sMPP8Snn36K3377DZaWlli1ahUSEhIwbtw4tGjRosbXWbt2LZRKJQYOHAh3d3ft9sMPP2jbXLt2DRkZGdrXd+7cwYwZM9CuXTs8+eSTUKlUOHHiBAICAuryUcgAvDbQD+ZmAo4m5iAqLVfqcoiIqJGr04BiGxsbxMXFwcfHB87Ozjh06BA6duyIS5cuYdCgQTphxNDUZkAS6c87Oy7gh7Pp6Ne6Kb6f1kvqcoiIyMA0+IBiR0dH5OfnAwCaN2+O2NhYAEBeXh7u3r1bl0uSiZsZ4gfZ/d6b6Gt3pC6HiIgasTqFm/79++PAgQMAgOeeew6zZ8/GjBkzMH78eAwePLheCyTT0MLZGs90aQ6A894QEdHjqdPTUl988QWKi4sBAO+++y4sLCxw4sQJPPvss/j3v/9drwWS6Zg1yA8/n7uBw1duISY9D529mkhdEhERNUIGM4mfvnDMjWGb/+N57Iy+jhD/Ztj4ck+pyyEiIgPRIGNuHl6T6e9rNXHtJqovswb5wUwADl6+hfPpeVKXQ0REjVCNw42joyOys7MBAE2aNIGjo2OF7cF+orrybWqDMZ3Lx96s5rw3RERUBzUecxMREaFdrfvgwYMNVhDRrEF+2BVzA+EJ2bh4XYmOnjWfGJKIiKjG4WbAgAHaf/f19YWXlxcEQdBpI4oi0tPT6686Mkktm9lidOfmCDt3A6vCE/H15O5Sl0RERI1InR4F9/X1xa1btyrsz83NrdP6UER/92DszZ+XshB7Qyl1OURE1IjUKdyIolih1wYACgoKoFAoHrsoolbNbDEq0AMAx94QEVHt1Gqem3nz5gEABEHAe++9B2tra+0xtVqNU6dOoXPnzvVaIJmu1wf54dfzN7E/PgvxN1UI8OCj+0RE9Gi1Cjfnzp0DUN5zc/HiRVhaWmqPWVpaIjAwEG+++Wb9Vkgmy8/FDk918sBv529idXgi1r3UTeqSiIioEahVuHnwlNTLL7+M1atXw87OrkGKInrgjUF+2H3hJvbGZeJShgrt3Nl7Q0RE1av1mJvS0lJ8//33SEtLa4h6iHS0drXDkx3dAQCfR3DsDRERPVqtw42FhQVatGgBtVrdEPUQVfDGoNYAgD8uZuJyZr7E1RARkaGr09NS7777Lv71r38hNze3vushqsDfzQ5PdnQDwCeniIjo0eq8KnhSUhI8PDzg7e0NGxsbnePR0dH1UhzRA28Mbo0/Lmbij9gMXMnKRxtXjvciIqLK1SncjBkzpp7LIKpeWzd7DG/vhr1xmVgdnogvXuwqdUlERGSgBFEURamL0KfaLJlOhiX+pgpPrj4KQQD2z+mP1uy9ISIyGbX5/q7TmBsAyMvLw9dff42FCxdqx95ER0fjxo0bdb0kUbUCPOwxrL0rRBH4PCJJ6nKIiMhA1SncXLhwAW3atMGKFSvw8ccfIy8vDwDw888/Y+HChfVZH5GONwaXPzn124WbSMoukLgaIiIyRHUKN/PmzcOUKVOQmJios5bUk08+iSNHjtRbcUR/197DAUMDyntvvuC8N0REVIk6hZszZ87g//7v/yrsb968OTIzMx+7KKLqzL7fe/Pr+Zu4eou9N0REpKtO4UYul0OlUlXYf+XKFTRr1uyxiyKqTofmDhjSzgUaEfiCY2+IiOhv6hRunn76aSxduhSlpaUAylcJv3btGt555x08++yz9VogUWVmD24DANgVcwMpOYUSV0NERIakTuFm5cqVKCgogIuLC4qKijBgwAD4+fnBzs4OH3zwQX3XSFRBR08HDGrL3hsiIqrosea5OXbsGC5cuICCggJ07doVQ4YMqc/aGgTnuTEe59PzMHrNccjMBETMHwBvZ5tHn0RERI1Sbb6/OYkfNWpTNp7Gocu38Fw3T/z3uUCpyyEiogZSm+/vOi2/AADh4eEIDw9HdnY2NBqNzrFvv/22rpclqpXZg1vj0OVb+PncDbw+qDVaOFtLXRIREUmsTmNulixZgieeeALh4eHIycnBnTt3dDYifenSwhH92zSDWiNizUGOvSEiojr23Kxbtw6bNm3CSy+9VN/1ENXa7MGtceTKLeyMvo5Zg/zg5cTeGyIiU1annpt79+4hODi4vmshqpNu3o7o17opyth7Q0REqGO4mT59OrZu3VrftRDV2YNZi3dEXUd67l2JqyEiIinV6bZUcXEx1q9fjz///BOdOnWChYWFzvFPPvmkXoojqqnuPk7o69cUx5Jy8OWhZIQ+01HqkoiISCJ1CjcXLlxA586dAQCxsbH1WQ9Rnc0e0hrHknKwIyodswb5oXkTK6lLIiIiCdQp3Bw8eLC+6yB6bD18nBDcyhknkm/jy4NJ+OAf7L0hIjJFtQo3zzzzzCPbCIKAnTt31rkgoscxe3BrnEi+jR/OXENQS2eoRREudgr09HWCzEyQujwiItKDWoUbBweHhqqDqF70aumMNq62uJJVgFnbzmn3uzsosGhUAIZ3cJewOiIi0gdJl18IDQ3Fzz//jISEBFhZWSE4OBgrVqyAv79/tef99NNPeO+995CamorWrVtjxYoVePLJJ2v0nlx+wbjtjc3AK1uiK+x/0GezdmJXBhwiokaoNt/fdXoUvL4cPnwYM2fOxMmTJ3HgwAGUlpbiiSeeQGFhYZXnnDhxAuPHj8e0adNw7tw5jBkzBmPGjOHAZoJaI2LJb/GVHnuQ4Jf8Fg+1xqSWUyMiMjkGtXDmrVu34OLigsOHD6N///6Vtnn++edRWFiI3bt3a/f17t0bnTt3xrp16x75Huy5MV6RybcxfsPJR7bbNqM3glo566EiIiKqL42m5+bvlEolAMDJyanKNpGRkRgyZIjOvmHDhiEyMrLS9iUlJVCpVDobGafs/OJ6bUdERI2TwYQbjUaDOXPmoE+fPujQoUOV7TIzM+Hq6qqzz9XVFZmZmZW2Dw0NhYODg3bz8vKq17rJcLjYKeq1HRERNU4GE25mzpyJ2NhYbN++vV6vu3DhQiiVSu2Wnp5er9cnw9HT1wnuDgpU98C3i50cPX2r7hkkIqLGzyDCzaxZs7B7924cPHgQnp6e1bZ1c3NDVlaWzr6srCy4ublV2l4ul8Pe3l5nI+MkMxOwaFQAAFQbcHIL7+mnICIikoSk4UYURcyaNQthYWGIiIiAr6/vI88JCgpCeHi4zr4DBw4gKCioocqkRmR4B3esndgVbg66t55c7eRwtrFEdn4JJn97GqriUokqJCKihibp01KvvfYatm7dil9++UVnbhsHBwdYWZWvCzRp0iQ0b94coaGhAMofBR8wYACWL1+OkSNHYvv27fjwww8RHR1d7VidB/i0lGlQa0ScTslFdn6xdobi9Ny7GLsuEjkFJejp44TvpvaElaVM6lKJiKgGavP9LWm4EYTKbx5s3LgRU6ZMAQAMHDgQPj4+2LRpk/b4Tz/9hH//+9/aSfw++ugjTuJHNRJ/U4Xn10civ7gMg9q64KuXusFCZhB3Z4mIqBqNJtxIgeGGzqTm4qVvTqG4VIPRnT3w6bjOMOO6U0REBq3RznNDpA89fJywdmI3mJsJ+CXmJhb/FgcTy/hEREaN4YZMUoi/C1aOC4QgAJsj0/Dpn4lSl0RERPWE4YZM1ujOzbF0dPkg9NXhifjmWIrEFRERUX1guCGT9lJvb7z5RBsAwLLd8dgZdV3iioiI6HEx3JDJmxnih+l9y+dYenvnBeyPq3wpDyIiahwYbsjkCYKAd0e2w9hunlBrRMzadg6RybelLouIiOqI4YYI5QFn+TMd8USAK+6VaTBj81lcuJ4ndVlERFQHDDdE95nLzLB6fBcEt3JGQUkZpmw8g6TsAqnLIiKiWmK4IXqIwkKG9ZO6I9DTAbmF9/DSN6dwI69I6rKIiKgWGG6I/sZWbo6NL/eEn4stMpTFeOnrU8gpKJG6LCIiqiGGG6JKONlY4vtpPdG8iRWu5hRyJXEiokaE4YaoCu4OVvh+Wk8421gi7qYK0787i+JStdRlERHRIzDcEFWjZTNbfDe1J+zk5jidkouZ/4tGqVojdVlERFQNhhuiR+jQ3AHfTOkBubkZwhOy8faOC9BouNAmEZGhYrghqoGevk5YO7ErzM0EhJ27gaW747mSOBGRgWK4IaqhQW1dtSuJbzqRilXhXEmciMgQMdwQ1cLozs2x5On2AIDP/kzExuNcSZyIyNAw3BDV0qQgH8wbWr6S+JLf4hF2jiuJExEZEoYbojp4fZAfXu7jAwB486cL+DM+S9qCiIhIi+GGqA4EQcB7IwPwTNfmUGtEzNwajZNXuZI4EZEhYLghqiMzMwEfPdsJQ9q5oqRMg+nfnUXsDaXUZRERmTyGG6LHYC4zwxcvdkHvlk4oKCnD5G9PI/kWVxInIpISww3RY1JYyLBhUnd0bO6A24X38NLXp3CTK4kTEUmG4YaoHtgpLLDp5R5o2cwGN5XFmPjNKdzmSuJERJJguCGqJ862cmyZ1gseDgpcvVWIKRvPIJ8riRMR6R3DDVE98mhihe+n94KTjSUu3lBixmauJE5EpG8MN0T1rFUzW2ye2hO2cnOcvJqLWVvPoYwriRMR6Q3DDVED6NDcAV9P7g5LczP8eSkLb+/kSuJERPrCcEPUQHq3dMaXL3aFzEzAz9E3sOx3riRORKQPDDdEDWhIgCv+O7YTAGDj8VR8HpEkcUVERMaP4YaogT3T1ROLRgUAAD45cAWbI1OlLYiIyMgx3BDpwct9fDF7cGsAwH9+icMvMTckroiIyHgx3BDpyZwhrTEl2AcAMP/H84hI4EriREQNgeGGSE8EQcB/ngrAP7o0R5lGxKtbonE6JVfqsoiIjA7DDZEemZkJ+GhsJwxu64KSMg2mbTrDlcSJiOoZww2RnlnIzLBmQlf09HVC/v2VxK9yJXEionrDcEMkAYWFDF9P7o72HvblK4l/cxoZSq4kTkRUHyQNN0eOHMGoUaPg4eEBQRCwa9euatsfOnQIgiBU2DIzM/VTMFE9sldY4LupPdGyqQ1u5BXhpW9OI7fwntRlERE1epKGm8LCQgQGBmLNmjW1Ou/y5cvIyMjQbi4uLg1UIVHDamorx+ZpPeHuoEBSdgGmbDyNgpIyqcsiImrUzKV88xEjRmDEiBG1Ps/FxQVNmjSp/4KIJODpaI3vp/XEc+siceG6Ev/cfBbfTukBhYVM6tKIiBqlRjnmpnPnznB3d8fQoUNx/Phxqcshemx+Lnb4bmpP2FjKcCL5Nt7YxpXEiYjqqlGFG3d3d6xbtw47d+7Ezp074eXlhYEDByI6OrrKc0pKSqBSqXQ2IkPUybMJNtxfSXx/fBYW/HwRpWUaRCbfxi8xNxCZfBtqrixORPRIgmggyxQLgoCwsDCMGTOmVucNGDAALVq0wPfff1/p8cWLF2PJkiUV9iuVStjb29elVKIGtT8uE6/+LxpqjQgbSxkK76m1x9wdFFg0KgDDO7hLWCERkf6pVCo4ODjU6Pu7UfXcVKZnz55ISqp6peWFCxdCqVRqt/T0dD1WR1R7T7R3w8ReLQBAJ9gAQKayGK9uicbe2AwpSiMiahQkHVBcH2JiYuDuXvXfYuVyOeRyuR4rIno8ao2I/fGVrzslAhAALPktHkMD3CAzE/RaGxFRYyBpuCkoKNDpdUlJSUFMTAycnJzQokULLFy4EDdu3MDmzZsBAJ999hl8fX3Rvn17FBcX4+uvv0ZERAT2798v1UcgqnenU3KRoSyu8rgIIENZjNMpuQhq5ay/woiIGglJw83Zs2cREhKifT1v3jwAwOTJk7Fp0yZkZGTg2rVr2uP37t3D/PnzcePGDVhbW6NTp074888/da5B1Nhl51cdbB72+8WbCPRygLVlo++AJSKqVwYzoFhfajMgiUgKkcm3MX7DyRq1tbGUYWQnd4zt5oUePo4QBN6mIiLjVJvvb/6Vj8jA9PR1gruDApnKYlT1Nw87uTkcbSxwLbcIP569jh/PXoe3szXGdvXEM9080byJlV5rJiIyJOy5ITJAe2Mz8OqW8vmbHv4f9EG/zNqJXTGsvRvOpN7Bjqh0/H4hQ/tklSAAfVo1xdhunhjW3g1WlpzpmIgav9p8fzPcEBmovbEZWPJbvM7g4qrmubl7rwx7LmZiR9R1RF69rd1vKzfHU53cMbabJ7p587YVETVeDDfVYLihxkStEXE6JRfZ+cVwsVOgp6/TIx//Ts+9i53R17Ez+jrSc4u0+32b2mBsN0/8o0tzePC2FRE1Mgw31WC4IVOh0Yg4nZqLn85exx8XM1BU+tdtq75+f9224gKdRNQYMNxUg+GGTFFBSRn2XMzAjqjrOJWSq91vpzDHqEAPjO3miS5eTXjbiogMFsNNNRhuyNSl3S7Ezugb2Bl1HTfy/rpt1bJZ+W2rZ7p4ws1BIWGFREQVMdxUg+GGqJxGI+Lk1dvYEXUdf8RmoLhUAwAwE4B+rZthbDdPDA1w5W0rIjIIDDfVYLghqii/uBR7Lmbip6h0nEm9o91vrzDH0509MLabFwI9HXjbiogkw3BTDYYbouql5hSWP20VdR03H3oM3c/F9v5tq+ZwsedtKyLSL4abajDcENWMRiPiRPJt7IhKx57YTJSU/XXbakCbZhjbzQtDAlwgN+dtKyJqeAw31WC4Iao9VXEpfr9Q/rRVVNpft60crCwwunP501Ydm/O2FRE1HIabajDcED2eq7cK7t+2uoFM1V+3rfxd7TC2mydGd/GAi53ubau6TEZIRPQwhptqMNwQ1Q+1RsTxpBzsiLqOvXGZuHf/tpXMTMDANs3wXHdPDGrrioiErBovI0FEVBWGm2ow3BDVP2VRKXZfuIkdUddx7lqedr+NpUy7oOfDHl4AlAGHiGqC4aYaDDdEDSspuwA7oq5jZ1Q6bhXcq7KdAMDNQYFj7wziLSoieqTafH+b6akmIjIRfi62WDCiLT59vnO17UQAGcpi7IvL1EtdRGQ6zKUugIiM0+3CqnttHvba/6LRxtUWffyaol/rpujp6wxbOf9oIqK6458gRNQg/v7EVHWuZBXgSlYBNh5PhbmZgK4tHNG3dVP08WuKQE8HmMvYyUxENcdwQ0QNoqevE9wdFMhUFqOygX0Pxtz8NqsvTqXk4lhSDo4l3UJ6bhFOp+bidGouPjlwBXZyc/Ru5Yx+rZuir19T+Da14Xw6RFQtDigmogazNzYDr26JBgCdgFPd01LXbt/F0aRbOJ6Ug+NJt6EsKtU57uGg0Pbq9PFriqa28gb8BERkKPi0VDUYboj0a29sRp3nuVFrRMTeUJb36iTmICrtDu6pNTptAtzt0fd+r04PHydYWXI5CCJjxHBTDYYbIv2rrxmKi+6pcTo1F8eTcnA0MQeXMlQ6xy3NzdDd21Ebdtp7OPAxcyIjwXBTDYYbIuORU1CC4/d7dY4l5ej0DgFAE2sLBLdyRl+/Zujr1xQtnK0lqpSIHhfDTTUYboiMkyiKuJpTqO3VOZl8G/klZTptWjhZa3t1gls5o4m1pUTVElFtMdxUg+GGyDSUqTU4f12JY4k5OJ6Ug+hrd1Cm+euPO0EAOjZ3QF+/pujbuim6eTtCbl79eB0uAEokHYabajDcEJmmgpIynE65jaP3w86VrAKd4woLM/T0dUZfv/LbWG3d7GD2UHB5nIHRRPT4GG6qwXBDRACQpSrW9uocS8pBdn6JzvGmtpYIblXeq1Om1uDdsNgK8/VwAVAi/WG4qQbDDRH9nSiKSMwu0PbqnLx6G3crWc28MlwAlEg/avP9zRmKicjkCYKANq52aONqh2l9fXGvTINz1+7geFIO/ojNQFJ2YZXnPlgA9HRKLoJaOeuvaCKqEsMNEdHfWJqboVdLZ/Rq6YxWLraYvT3mkedkqYof2YaI9IOr0RERVaOmC4CG7rmEzZGpKPzb4+dEpH8MN0RE1XiwAGh1o2kEAFmqEvznlzgEhYYjdM8l3Mwr0leJRPQ3DDdERNWQmQlYNCoAACoEHOH+9unznbF0dHv4OFtDVVyGrw5fRb+PDuL1becQk56n54qJiE9LERHVQE3mudFoRIQnZOObY1dx8mqutl13b0dM6+uLJ9q78Ykqojrio+DVYLghorqqzQzFsTeU+PZ4Cn47fxOl6vI/Zj0drfByH1+M6+4JO4WFPksnavQYbqrBcENE+pStKsbmyDT871Qa7twtBQDYys3xfA8vTAn2gZcTF/MkqgmGm2ow3BCRFIruqRF27ga+OXYVybfK580xE4Bh7d0wvZ8vurZwhCDwlhVRVWrz/S3pgOIjR45g1KhR8PDwgCAI2LVr1yPPOXToELp27Qq5XA4/Pz9s2rSpweskInpcVpYyvNirBQ7MHYCNL/dAv9ZNoRGBPbGZeHZtJMZ8eQK/nr+JUrVG6lKJGj1Jw01hYSECAwOxZs2aGrVPSUnByJEjERISgpiYGMyZMwfTp0/Hvn37GrhSIqL6YWYmIMTfBd9P64V9c/rj+e5esDQ3w/n0PLyx7RwGfHQQXx1OhrKoVOpSiRotg7ktJQgCwsLCMGbMmCrbvPPOO/j9998RGxur3ffCCy8gLy8Pe/furdH78LYUERmanIISbDmZhi0n05BTcA8AYG0pw3PdPPFyH1/4NLWRuEIi6TWa21K1FRkZiSFDhujsGzZsGCIjI6s8p6SkBCqVSmcjIjIkTW3lmDOkDY69Mwgfje0Ef1c73L2nxneRaQhZeQgzNp/Fyau3YSB/FyUyeI0q3GRmZsLV1VVnn6urK1QqFYqKKp8NNDQ0FA4ODtrNy8tLH6USEdWawkKGcd29sHdOP2yZ1gsh/s0gisCB+Cy8sP4knvr8GMLOXce9Mo7LIapOowo3dbFw4UIolUrtlp6eLnVJRETVEgQBfVs3xcaXe+LPeQPwYq8WUFiYIe6mCnN/OI++KyKw5mAS7hTek7pUIoPUqFYFd3NzQ1ZWls6+rKws2Nvbw8rKqtJz5HI55HK5PsojIqp3fi62+PAfHfHWE/7YevoavjuRiuz8Evx332V8HpGIZ7p6YmofX/i52EpdKpHBaFQ9N0FBQQgPD9fZd+DAAQQFBUlUERGRfjjaWGJmiB+OvTMIn4wLRHsPexSXarD11DUM+eQwXt54GscSczguhwgS99wUFBQgKSlJ+zolJQUxMTFwcnJCixYtsHDhQty4cQObN28GALzyyiv44osv8Pbbb2Pq1KmIiIjAjz/+iN9//12qj0BEpFeW5mZ4pqsn/tGlOU6l5OKbYyn481IWDl6+hYOXb6Gtmx2m9vXF04EeUFjIpC6XSBKSPgp+6NAhhISEVNg/efJkbNq0CVOmTEFqaioOHTqkc87cuXMRHx8PT09PvPfee5gyZUqN35OPghORsUnNKcTG4yn4Keo67t5TAwCa2lpiYm9vTOztjaa2urfma7NGFpGh4PIL1WC4ISJjpbxbiu1nrmHTiVTt6uWW5mYY09kD0/q2hL+bXY1WNycyRAw31WC4ISJjV6rWYE9sJr45loLz6Xna/e3c7XApI79C+wd9NmsndmXAIYPFcFMNhhsiMhWiKCL62h18cywFey5moro/7AUAbg4KHHtnEG9RkUEy2hmKiYio5gRBQDdvJ3w5oRtWvdC52rYigAxlMU5dva2X2ogaUqOa54aIiOqmpl30UzedQSfPJgjwsEeAhz3ae9ijtYsdLM35d2FqPBhuiIhMgIudokbtiss0OJ2ai9Opudp9FjIBrV3s0P5+2AnwcEA7dzvYKSwaqlyix8JwQ0RkAnr6OsHdQYFMZXGlvTgPxtx8Pbk7EjLyEZ+hQtxNJeJvqqAqLkN8hgrxGSr8FPXXOd7O1vcDjwMC3MuDj4t9zUIUUUPigGIiIhOxNzYDr26JBqB7m6q6p6VEUcT1O0WIu1kebuJvKhF3U6XzKPnDmtrKtbezHgQeH2cbmHGQMj0mPi1VDYYbIjJl9TXPTW7hPcTfvN+7k6FC3E0Vrt4qgKaSbxRrSxnauT8ceBzQxs0WcnPOoEw1x3BTDYYbIjJ1DTVDcdE9NRIyVdpenribKiRkqFBSpqnQ1txMgJ+LbfnA5fuBJ8DDHg5WtRvHw9mWTQfDTTUYboiI9KdMrUFKTiHi/tbLk3e3tNL2no5W93t4HMrH8zS3h5u9AoJQMbBwtmXTwnBTDYYbIiJpiaKIDGXxX4HnZnnguZFXVGl7JxtL7fidB+N5rmQWYObW6AqDoznbsvFiuKkGww0RkWHKu3vv/qBllTbwJN0qgLqygTzV4GzLxqk23998FJyIiAxCE2tLBLdqiuBWTbX7ikvVuJKVr9PLE3tDhXvqiuN4Hngw2/LplFwEtXLWQ+VkaBhuiIjIYCksZOjk2QSdPJto94Wdu4G5P8Q88twVexPwUm9vDPRvBmdbecMVSQaH4YaIiBoVtxpOFBiTnoeY9DwIAhDo2QSD27ogpK0L2nvYVzpAmYwHww0RETUqNZlt2cnWEi/08MLBhFuIz1Bpg87KA1fgZq9ASNtmCPF3Qd/WTWFtya9CY8MBxURE1OjUZrblDGURDibcQkRCNo4n5aCoVK1tb2luht4tnTG4rQsGtXWBl5O1nj4B1RaflqoGww0RkXGoyzw3xaVqnLx6GwcTshGekI3rd3QfP/dzsdXevurm7QgLGVdDNxQMN9VguCEiMh6PM0OxKIpIyi5AxP2gE5V2R+exczuFOQa0aYZBbV0w0N8FTjaWDfUxqAYYbqrBcENERJVR3i3F4cRbOJiQjUOXs3HnoVmUBQHo4tUEg9u5IsTfBe3c7TgoWc8YbqrBcENERI+i1oiISb+DiIRsRCTcwqUMlc5xdwcFQtq6YJC/C/r4NYWVJRcBbWgMN9VguCEiotq6mVeEg5ezcTAhG8eSclBc+tckgnJzMwS1csagti4I8eeg5IbCcFMNhhsiInocxaVqRF69jYhL2YhIyK6wJlYbV1sMauuKQW1d0LVFE5jXYFAyVzd/NIabajDcEBFRfRFFEYnZBQi/VN6rczYtFw8vheVgZaEdlDygTTM4VjIomaub1wzDTTUYboiIqKHk3b2Hw1fuD0q+cgt5Dw1KNhOAri0cEdLWBYPbucDf1Q774jLx6haubl4TDDfVYLghIiJ9UGtEnLv2YFByNhIy83WOu9vLoSwuw9176krP5+rmuhhuqsFwQ0REUriRV4SIhPLbV8eTclBSVvXK5g/bNqM3VzcHw021GG6IiEhqRffU+PTPy1h/JOWRbbt7N0FIW1f4u9qhjasdPB2tYGaCPTm1+f7mamFERER6ZmUpQ4i/a43Czdm0PJxNy/vrXAsZWrvaoo2rHdq42qK1qx38Xe3g7qDgxIL3MdwQERFJ4FGrmwOAo7UFpvb1RVJ2Aa5kFSA5uwBFpWpcuK7EhetKnbZ2cvOHQo+dNvw0s5ObXOhhuCEiIpKAzEzAolEBeHVLNARUvrp56DMddZ6WKlNrkJZ7F1cy83ElqwBXsvNxJTMfKTmFyC8pQ/S1PERfy9N5nybWFmjjYoc2brrBx5jXyuKYGyIiIgnVxzw398o0SMkpxJWsfO2WmFWA1NuFOvPuPKyprWWFXp7WrnZwsLKo82dpyMkIOaC4Ggw3RERkaBoqFBSXqpF8q+B+4Cko7/HJzkd6blGV57jZK9Da1VY7gLmNmx1au9jCRl79zZ6GnoyQ4aYaDDdERGTqCkvK7o/jyf8r+GTl6wSTv/N0tEIbVzud4OPnYguFhQx7YzMafDJChptqMNwQERFVTlVcisSHws6D4HMrv6TS9mYC4OVohUxVSZXz9tTXZIR8FJyIiIhqzV5hgW7eTujm7aSz/07hPZ2wczkrH4lZ+bhztxRp1dziAsoHSmcoi3E6JVdvkxEy3BAREVG1HG0s0aulM3q1/CuciKKIWwUl+O5EKtYcTH7kNbLzq77lVd8evQ67HqxZswY+Pj5QKBTo1asXTp8+XWXbTZs2QRAEnU2hUOixWiIiIhIEAS52CvT1a1aj9i52+vuuljzc/PDDD5g3bx4WLVqE6OhoBAYGYtiwYcjOzq7yHHt7e2RkZGi3tLQ0PVZMREREDzyYjLCq0TQCyp+a6unrVEWL+id5uPnkk08wY8YMvPzyywgICMC6detgbW2Nb7/9tspzBEGAm5ubdnN1ddVjxURERPTAg8kIAVQIOA9eLxoVoNeVzSUNN/fu3UNUVBSGDBmi3WdmZoYhQ4YgMjKyyvMKCgrg7e0NLy8vjB49GnFxcfool4iIiCoxvIM71k7sCjcH3VtPbg6KenkMvLYkHVCck5MDtVpdoefF1dUVCQkJlZ7j7++Pb7/9Fp06dYJSqcTHH3+M4OBgxMXFwdPTs0L7kpISlJT89QibSqWq3w9BREREGN7BHUMD3BpshuLaaHRPSwUFBSEoKEj7Ojg4GO3atcNXX32FZcuWVWgfGhqKJUuW6LNEIiIikyQzE/T2uHd1JL0t1bRpU8hkMmRlZensz8rKgpubW42uYWFhgS5duiApKanS4wsXLoRSqdRu6enpj103ERERGS5Jw42lpSW6deuG8PBw7T6NRoPw8HCd3pnqqNVqXLx4Ee7uld/Pk8vlsLe319mIiIjIeEl+W2revHmYPHkyunfvjp49e+Kzzz5DYWEhXn75ZQDApEmT0Lx5c4SGhgIAli5dit69e8PPzw95eXn473//i7S0NEyfPl3Kj0FEREQGQvJw8/zzz+PWrVv4z3/+g8zMTHTu3Bl79+7VDjK+du0azMz+6mC6c+cOZsyYgczMTDg6OqJbt244ceIEAgICpPoIREREZEC4cCYREREZvNp8f0s+iR8RERFRfWK4ISIiIqPCcENERERGheGGiIiIjIrkT0vp24Px01yGgYiIqPF48L1dk+egTC7c5OfnAwC8vLwkroSIiIhqKz8/Hw4ODtW2MblHwTUaDW7evAk7OzsIgv4X82oMVCoVvLy8kJ6ezsflDQB/H4aFvw/Dw9+JYWmo34coisjPz4eHh4fO/HeVMbmeGzMzs0pXD6eKuFyFYeHvw7Dw92F4+DsxLA3x+3hUj80DHFBMRERERoXhhoiIiIwKww1VIJfLsWjRIsjlcqlLIfD3YWj4+zA8/J0YFkP4fZjcgGIiIiIybuy5ISIiIqPCcENERERGheGGiIiIjArDDRERERkVhhvSCg0NRY8ePWBnZwcXFxeMGTMGly9flrosArB8+XIIgoA5c+ZIXYpJu3HjBiZOnAhnZ2dYWVmhY8eOOHv2rNRlmSS1Wo333nsPvr6+sLKyQqtWrbBs2bIarTtEj+/IkSMYNWoUPDw8IAgCdu3apXNcFEX85z//gbu7O6ysrDBkyBAkJibqrT6GG9I6fPgwZs6ciZMnT+LAgQMoLS3FE088gcLCQqlLM2lnzpzBV199hU6dOkldikm7c+cO+vTpAwsLC+zZswfx8fFYuXIlHB0dpS7NJK1YsQJr167FF198gUuXLmHFihX46KOP8Pnnn0tdmkkoLCxEYGAg1qxZU+nxjz76CKtXr8a6detw6tQp2NjYYNiwYSguLtZLfXwUnKp069YtuLi44PDhw+jfv7/U5ZikgoICdO3aFV9++SXef/99dO7cGZ999pnUZZmkBQsW4Pjx4zh69KjUpRCAp556Cq6urvjmm2+0+5599llYWVlhy5YtElZmegRBQFhYGMaMGQOgvNfGw8MD8+fPx5tvvgkAUCqVcHV1xaZNm/DCCy80eE3suaEqKZVKAICTk5PElZiumTNnYuTIkRgyZIjUpZi8X3/9Fd27d8dzzz0HFxcXdOnSBRs2bJC6LJMVHByM8PBwXLlyBQBw/vx5HDt2DCNGjJC4MkpJSUFmZqbOn1sODg7o1asXIiMj9VKDyS2cSTWj0WgwZ84c9OnTBx06dJC6HJO0fft2REdH48yZM1KXQgCuXr2KtWvXYt68efjXv/6FM2fO4I033oClpSUmT54sdXkmZ8GCBVCpVGjbti1kMhnUajU++OADTJgwQerSTF5mZiYAwNXVVWe/q6ur9lhDY7ihSs2cOROxsbE4duyY1KWYpPT0dMyePRsHDhyAQqGQuhxCeeDv3r07PvzwQwBAly5dEBsbi3Xr1jHcSODHH3/E//73P2zduhXt27dHTEwM5syZAw8PD/4+iLelqKJZs2Zh9+7dOHjwIDw9PaUuxyRFRUUhOzsbXbt2hbm5OczNzXH48GGsXr0a5ubmUKvVUpdoctzd3REQEKCzr127drh27ZpEFZm2t956CwsWLMALL7yAjh074qWXXsLcuXMRGhoqdWkmz83NDQCQlZWlsz8rK0t7rKEx3JCWKIqYNWsWwsLCEBERAV9fX6lLMlmDBw/GxYsXERMTo926d++OCRMmICYmBjKZTOoSTU6fPn0qTI1w5coVeHt7S1SRabt79y7MzHS/wmQyGTQajUQV0QO+vr5wc3NDeHi4dp9KpcKpU6cQFBSklxp4W4q0Zs6cia1bt+KXX36BnZ2d9t6og4MDrKysJK7OtNjZ2VUY62RjYwNnZ2eOgZLI3LlzERwcjA8//BDjxo3D6dOnsX79eqxfv17q0kzSqFGj8MEHH6BFixZo3749zp07h08++QRTp06VujSTUFBQgKSkJO3rlJQUxMTEwMnJCS1atMCcOXPw/vvvo3Xr1vD19cV7770HDw8P7RNVDU4kug9ApdvGjRulLo1EURwwYIA4e/Zsqcswab/99pvYoUMHUS6Xi23bthXXr18vdUkmS6VSibNnzxZbtGghKhQKsWXLluK7774rlpSUSF2aSTh48GCl3xeTJ08WRVEUNRqN+N5774murq6iXC4XBw8eLF6+fFlv9XGeGyIiIjIqHHNDRERERoXhhoiIiIwKww0REREZFYYbIiIiMioMN0RERGRUGG6IiIjIqDDcEBERkVFhuCEiIiKjwnBDRJVKTU2FIAiIiYmRuhSthIQE9O7dGwqFAp07d36sawmCgF27dtVLXYYgPDwc7dq1q9WiquvWrcOoUaMasCoiaTDcEBmoKVOmQBAELF++XGf/rl27IAiCRFVJa9GiRbCxscHly5d1FuX7u8zMTLz++uto2bIl5HI5vLy8MGrUqGrPeRyHDh2CIAjIy8trkOvXxNtvv41///vf2kVVN23ahCZNmui0uXTpEry8vPDcc8/h3r17mDp1KqKjo3H06FEJKiZqOAw3RAZMoVBgxYoVuHPnjtSl1Jt79+7V+dzk5GT07dsX3t7ecHZ2rrRNamoqunXrhoiICPz3v//FxYsXsXfvXoSEhGDmzJl1fm99EEURZWVltT7v2LFjSE5OxrPPPltlmzNnzqBfv34YPnw4fvjhB1haWsLS0hIvvvgiVq9e/ThlExkchhsiAzZkyBC4ubkhNDS0yjaLFy+ucIvms88+g4+Pj/b1lClTMGbMGHz44YdwdXVFkyZNsHTpUpSVleGtt96Ck5MTPD09sXHjxgrXT0hIQHBwMBQKBTp06IDDhw/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
+}