{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE 10**\n",
    "\n",
    "Load in Olivetti dataset, split it into train/val/test sets with stratified sampling, cluster using K-means and visualize"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from matplotlib import pyplot as plt\n",
    "from sklearn.datasets import fetch_olivetti_faces\n",
    "from sklearn.cluster import KMeans\n",
    "from sklearn.model_selection import StratifiedShuffleSplit as SSS\n",
    "import random"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "downloading Olivetti faces from https://ndownloader.figshare.com/files/5976027 to C:\\Users\\TSB\\scikit_learn_data\n"
     ]
    }
   ],
   "source": [
    "olivetti = fetch_olivetti_faces(shuffle=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "dict_keys(['data', 'images', 'target', 'DESCR'])"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "olivetti.keys()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(64, 64)"
      ]
     },
     "execution_count": 51,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X = olivetti['data']\n",
    "images = olivetti['images']\n",
    "y = olivetti['target']\n",
    "\n",
    "images[0].shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initial split of train/test\n",
    "\n",
    "sss = SSS(test_size = 0.1)\n",
    "for train_idx, test_idx in sss.split(X, y):\n",
    "    X_train, X_test = X[train_idx], X[test_idx]\n",
    "    y_train, y_test = y[train_idx], y[test_idx]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Further split train set into train/val\n",
    "\n",
    "sss = SSS(test_size=40)\n",
    "for train_idx, val_idx in sss.split(X_train, y_train):\n",
    "    X_train, X_val = X[train_idx], X[val_idx]\n",
    "    y_train, y_val = y[train_idx], y[val_idx]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Generate a bunch of k-means models to check silhouette_score on\n",
    "\n",
    "k_means_models = [KMeans(n_clusters=k).fit(X_train) for k in range(1,15)]\n",
    "silhouette_scores = [silhouette_score(X_train, model.labels_) for model in k_means_models[1:]]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 14348 characters]\n",
      "text/plain": [
       "<Figure size 576x216 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.figure(figsize=(8,3))\n",
    "plt.plot(range(2,15), silhouette_scores, 'bo-')\n",
    "plt.xlabel('K', fontsize=13)\n",
    "plt.ylabel('Silhouette Score', fontsize=13)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here we can see peaks at k=10 and k=13 with the absolute maximum being at 2 and the end of the graph trending upwards."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Thanks to the sklearn documentation found at:\n",
      "    https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html\n",
      "\n",
      "For n_clusters = 4 The average silhouette_score is : 0.111416936\n",
      "For n_clusters = 8 The average silhouette_score is : 0.09379276\n",
      "For n_clusters = 9 The average silhouette_score is : 0.08636401\n",
      "For n_clusters = 12 The average silhouette_score is : 0.08856778\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 67796 characters]\n",
      "text/plain": [
       "<Figure size 1296x504 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 73916 characters]\n",
      "text/plain": [
       "<Figure size 1296x504 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 75364 characters]\n",
      "text/plain": [
       "<Figure size 1296x504 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 79328 characters]\n",
      "text/plain": [
       "<Figure size 1296x504 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "\"\"\"\n",
    "Thanks to the sklearn documentation found at:\n",
    "    https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html\n",
    "\"\"\"\n",
    "\n",
    "from sklearn.metrics import silhouette_samples\n",
    "import matplotlib.cm as cm\n",
    "\n",
    "print(__doc__)\n",
    "\n",
    "range_n_clusters = [4, 8, 9, 12]\n",
    "\n",
    "for n_clusters in range_n_clusters:\n",
    "    # Create a subplot with 1 row and 2 columns\n",
    "    fig, (ax1, ax2) = plt.subplots(1, 2)\n",
    "    fig.set_size_inches(18, 7)\n",
    "\n",
    "    # The 1st subplot is the silhouette plot\n",
    "    # The silhouette coefficient can range from -1, 1 but in this example all\n",
    "    # lie within [-0.1, 1]\n",
    "    ax1.set_xlim([-0.1, 1])\n",
    "    # The (n_clusters+1)*10 is for inserting blank space between silhouette\n",
    "    # plots of individual clusters, to demarcate them clearly.\n",
    "    ax1.set_ylim([0, len(X_train) + (n_clusters + 1) * 10])\n",
    "\n",
    "    # Initialize the clusterer with n_clusters value and a random generator\n",
    "    # seed of 10 for reproducibility.\n",
    "    clusterer = KMeans(n_clusters=n_clusters)\n",
    "    cluster_labels = clusterer.fit_predict(X_train)\n",
    "\n",
    "    # The silhouette_score gives the average value for all the samples.\n",
    "    # This gives a perspective into the density and separation of the formed\n",
    "    # clusters\n",
    "    silhouette_avg = silhouette_score(X_train, cluster_labels)\n",
    "    print(\"For n_clusters =\", n_clusters,\n",
    "          \"The average silhouette_score is :\", silhouette_avg)\n",
    "\n",
    "    # Compute the silhouette scores for each sample\n",
    "    sample_silhouette_values = silhouette_samples(X_train, cluster_labels)\n",
    "\n",
    "    y_lower = 10\n",
    "    for i in range(n_clusters):\n",
    "        # Aggregate the silhouette scores for samples belonging to\n",
    "        # cluster i, and sort them\n",
    "        ith_cluster_silhouette_values = \\\n",
    "            sample_silhouette_values[cluster_labels == i]\n",
    "\n",
    "        ith_cluster_silhouette_values.sort()\n",
    "\n",
    "        size_cluster_i = ith_cluster_silhouette_values.shape[0]\n",
    "        y_upper = y_lower + size_cluster_i\n",
    "\n",
    "        color = cm.nipy_spectral(float(i) / n_clusters)\n",
    "        ax1.fill_betweenx(np.arange(y_lower, y_upper),\n",
    "                          0, ith_cluster_silhouette_values,\n",
    "                          facecolor=color, edgecolor=color, alpha=0.7)\n",
    "\n",
    "        # Label the silhouette plots with their cluster numbers at the middle\n",
    "        ax1.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))\n",
    "\n",
    "        # Compute the new y_lower for next plot\n",
    "        y_lower = y_upper + 10  # 10 for the 0 samples\n",
    "\n",
    "    ax1.set_title(\"The silhouette plot for the various clusters.\")\n",
    "    ax1.set_xlabel(\"The silhouette coefficient values\")\n",
    "    ax1.set_ylabel(\"Cluster label\")\n",
    "\n",
    "    # The vertical line for average silhouette score of all the values\n",
    "    ax1.axvline(x=silhouette_avg, color=\"red\", linestyle=\"--\")\n",
    "\n",
    "    ax1.set_yticks([])  # Clear the yaxis labels / ticks\n",
    "    ax1.set_xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1])\n",
    "\n",
    "    # 2nd Plot showing the actual clusters formed\n",
    "    colors = cm.nipy_spectral(cluster_labels.astype(float) / n_clusters)\n",
    "    ax2.scatter(X_train[:, 0], X_train[:, 1], marker='.', s=30, lw=0, alpha=0.7,\n",
    "                c=colors, edgecolor='k')\n",
    "\n",
    "    # Labeling the clusters\n",
    "    centers = clusterer.cluster_centers_\n",
    "    # Draw white circles at cluster centers\n",
    "    ax2.scatter(centers[:, 0], centers[:, 1], marker='o',\n",
    "                c=\"white\", alpha=1, s=200, edgecolor='k')\n",
    "\n",
    "    for i, c in enumerate(centers):\n",
    "        ax2.scatter(c[0], c[1], marker='$%d$' % i, alpha=1,\n",
    "                    s=50, edgecolor='k')\n",
    "\n",
    "    ax2.set_title(\"The visualization of the clustered data.\")\n",
    "    ax2.set_xlabel(\"Feature space for the 1st feature\")\n",
    "    ax2.set_ylabel(\"Feature space for the 2nd feature\")\n",
    "\n",
    "    plt.suptitle((\"Silhouette analysis for KMeans clustering on sample data \"\n",
    "                  \"with n_clusters = %d\" % n_clusters),\n",
    "                 fontsize=14, fontweight='bold')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "None of the graphs appear to have clusters below the threshold. However when n_clusters = 9 appears to be the most uniform and so we'll go with this for our n_clusters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [],
   "source": [
    "kmeans = KMeans(n_clusters=9)\n",
    "kmeans.fit(X_train)\n",
    "y_train_pred = kmeans.predict(X_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [],
   "source": [
    "cluster1 = X_train[y_train_pred==1]\n",
    "cluster4 = X_train[y_train_pred==4]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "45\n"
     ]
    }
   ],
   "source": [
    "print(cluster1.shape[0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.image.AxesImage at 0x1a863677648>"
      ]
     },
     "execution_count": 73,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 14952 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Display images falling into cluster 1\n",
    "# Note that most of these images appear to be men and a few have glasses\n",
    "\n",
    "plt.imshow(cluster1[random.randint(0,cluster1.shape[0] - 1)].reshape(64,64), cmap='gray')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 81,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.image.AxesImage at 0x1a8671e6dc8>"
      ]
     },
     "execution_count": 81,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 15612 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Display images falling into cluster 4\n",
    "# Note that many of these images have people with smaller, rounded chins\n",
    "\n",
    "plt.imshow(cluster4[random.randint(0,cluster4.shape[0] - 1)].reshape(64,64), cmap='gray')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE 11**\n",
    "\n",
    "Train a classifier on the training set and evaluate on val set. Then reduce dimensionality using K-means and train a new classifier. search for best number of clusters. See how it performs and compare."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 83,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0,\n",
       "    decision_function_shape='ovr', degree=3, gamma='scale', kernel='rbf',\n",
       "    max_iter=-1, probability=False, random_state=None, shrinking=True,\n",
       "    tol=0.001, verbose=False)"
      ]
     },
     "execution_count": 83,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.svm import SVC\n",
    "\n",
    "svc = SVC()\n",
    "svc.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 90,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.996875"
      ]
     },
     "execution_count": 90,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "svc.score(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 92,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.975"
      ]
     },
     "execution_count": 92,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "svc.score(X_val, y_val)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 101,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Pipeline(memory=None,\n",
       "         steps=[('kmeans',\n",
       "                 KMeans(algorithm='auto', copy_x=True, init='k-means++',\n",
       "                        max_iter=300, n_clusters=50, n_init=10, n_jobs=None,\n",
       "                        precompute_distances='auto', random_state=None,\n",
       "                        tol=0.0001, verbose=0)),\n",
       "                ('svc',\n",
       "                 SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None,\n",
       "                     coef0=0.0, decision_function_shape='ovr', degree=3,\n",
       "                     gamma='scale', kernel='rbf', max_iter=-1,\n",
       "                     probability=False, random_state=None, shrinking=True,\n",
       "                     tol=0.001, verbose=False))],\n",
       "         verbose=False)"
      ]
     },
     "execution_count": 101,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.pipeline import Pipeline\n",
    "\n",
    "pipeline = Pipeline([\n",
    "    ('kmeans', KMeans(n_clusters=50)),\n",
    "    ('svc', SVC())\n",
    "])\n",
    "\n",
    "pipeline.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 102,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.475"
      ]
     },
     "execution_count": 102,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Note that this dimensionality reduction does terribly\n",
    "\n",
    "pipeline.score(X_val, y_val)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 107,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fitting 4 folds for each of 98 candidates, totalling 392 fits\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "c:\\users\\tsb\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\sklearn\\model_selection\\_split.py:667: UserWarning: The least populated class in y has only 3 members, which is less than n_splits=4.\n",
      "  % (min_groups, self.n_splits)), UserWarning)\n",
      "[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.\n",
      "[Parallel(n_jobs=-1)]: Done  25 tasks      | elapsed:   15.7s\n",
      "[Parallel(n_jobs=-1)]: Done 146 tasks      | elapsed:  3.4min\n",
      "[Parallel(n_jobs=-1)]: Done 349 tasks      | elapsed: 16.0min\n",
      "[Parallel(n_jobs=-1)]: Done 392 out of 392 | elapsed: 19.6min finished\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "GridSearchCV(cv=4, error_score=nan,\n",
       "             estimator=Pipeline(memory=None,\n",
       "                                steps=[('kmeans',\n",
       "                                        KMeans(algorithm='auto', copy_x=True,\n",
       "                                               init='k-means++', max_iter=300,\n",
       "                                               n_clusters=50, n_init=10,\n",
       "                                               n_jobs=None,\n",
       "                                               precompute_distances='auto',\n",
       "                                               random_state=None, tol=0.0001,\n",
       "                                               verbose=0)),\n",
       "                                       ('svc',\n",
       "                                        SVC(C=1.0, break_ties=False,\n",
       "                                            cache_size=200, class_weight=None,\n",
       "                                            coef0=0.0,\n",
       "                                            decision_function_shape='ovr',\n",
       "                                            degree=3, gamma='scale',\n",
       "                                            kernel='rbf', max_iter=-1,\n",
       "                                            probability=False,\n",
       "                                            random_state=None, shrinking=True,\n",
       "                                            tol=0.001, verbose=False))],\n",
       "                                verbose=False),\n",
       "             iid='deprecated', n_jobs=-1,\n",
       "             param_grid={'kmeans__n_clusters': range(2, 100)},\n",
       "             pre_dispatch='2*n_jobs', refit=True, return_train_score=False,\n",
       "             scoring=None, verbose=2)"
      ]
     },
     "execution_count": 107,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.model_selection import GridSearchCV\n",
    "\n",
    "param_grid = dict(kmeans__n_clusters=range(2,100))\n",
    "grid_clf = GridSearchCV(pipeline, param_grid, cv=4, verbose=2, n_jobs=-1)\n",
    "grid_clf.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 108,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'kmeans__n_clusters': 68}"
      ]
     },
     "execution_count": 108,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "grid_clf.best_params_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 110,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.80625"
      ]
     },
     "execution_count": 110,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "grid_clf.score(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 109,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.5"
      ]
     },
     "execution_count": 109,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "grid_clf.score(X_val, y_val)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE 12**\n",
    "\n",
    "Train a Gaussian mixture on Olivetti dataset. Speed up with PCA(.99 variance). Generate some new faces using sample() method and visualize with PCA inverse_transform() method. Mess with images and see if the model can handle it. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 113,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Pipeline(memory=None,\n",
       "         steps=[('pca',\n",
       "                 PCA(copy=True, iterated_power='auto', n_components=0.99,\n",
       "                     random_state=None, svd_solver='auto', tol=0.0,\n",
       "                     whiten=False)),\n",
       "                ('bgm',\n",
       "                 BayesianGaussianMixture(covariance_prior=None,\n",
       "                                         covariance_type='full',\n",
       "                                         degrees_of_freedom_prior=None,\n",
       "                                         init_params='kmeans', max_iter=100,\n",
       "                                         mean_precision_prior=None,\n",
       "                                         mean_prior=None, n_components=50,\n",
       "                                         n_init=10, random_state=None,\n",
       "                                         reg_covar=1e-06, tol=0.001, verbose=0,\n",
       "                                         verbose_interval=10, warm_start=False,\n",
       "                                         weight_concentration_prior=None,\n",
       "                                         weight_concentration_prior_type='dirichlet_process'))],\n",
       "         verbose=False)"
      ]
     },
     "execution_count": 113,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.decomposition import PCA\n",
    "from sklearn.mixture import BayesianGaussianMixture as BGM\n",
    "\n",
    "pipeline = Pipeline([\n",
    "    ('pca', PCA(n_components=0.99)),\n",
    "    ('bgm', BGM(n_components=50, n_init=10))\n",
    "])\n",
    "\n",
    "pipeline.fit(X_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 127,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Generate new samples from our gaussian distributions\n",
    "X_new, y_new = pipeline['bgm'].sample(6)\n",
    "\n",
    "# Reverse our PCA dimensional reduction\n",
    "X_new = pipeline['pca'].inverse_transform(X_new)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 135,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.image.AxesImage at 0x1a86927c588>"
      ]
     },
     "execution_count": 135,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 14884 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Display random recreated image\n",
    "plt.imshow(X_new[random.randint(0, X_new.shape[0]-1)].reshape(64,64), cmap='gray')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
