{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Working with Image Data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_digits\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "\n",
    "digits_data = load_digits()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "dict_keys(['data', 'target', 'frame', 'feature_names', 'target_names', 'images', 'DESCR'])"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "digits_data.keys()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "labels = pd.Series(digits_data['target'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>0</th>\n",
       "      <th>1</th>\n",
       "      <th>2</th>\n",
       "      <th>3</th>\n",
       "      <th>4</th>\n",
       "      <th>5</th>\n",
       "      <th>6</th>\n",
       "      <th>7</th>\n",
       "      <th>8</th>\n",
       "      <th>9</th>\n",
       "      <th>...</th>\n",
       "      <th>54</th>\n",
       "      <th>55</th>\n",
       "      <th>56</th>\n",
       "      <th>57</th>\n",
       "      <th>58</th>\n",
       "      <th>59</th>\n",
       "      <th>60</th>\n",
       "      <th>61</th>\n",
       "      <th>62</th>\n",
       "      <th>63</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>13.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>...</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>13.0</td>\n",
       "      <td>10.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>1 rows × 64 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    0    1    2     3    4    5    6    7    8    9   ...   54   55   56   57  \\\n",
       "0  0.0  0.0  5.0  13.0  9.0  1.0  0.0  0.0  0.0  0.0  ...  0.0  0.0  0.0  0.0   \n",
       "\n",
       "    58    59    60   61   62   63  \n",
       "0  6.0  13.0  10.0  0.0  0.0  0.0  \n",
       "\n",
       "[1 rows x 64 columns]"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "data = pd.DataFrame(digits_data['data'])\n",
    "data.head(1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.image.AxesImage at 0x7f7415ff4a00>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 7004 characters]\n",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "first_image = data.iloc[0]\n",
    "np_image = first_image.values\n",
    "np_image = np_image.reshape(8,8)\n",
    "\n",
    "plt.imshow(np_image, cmap='gray_r')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.image.AxesImage at 0x7f74148eb730>"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 8920 characters]\n",
      "text/plain": [
       "<Figure size 600x400 with 8 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "f, axarr = plt.subplots(2, 4)\n",
    "\n",
    "axarr[0, 0].imshow(data.iloc[0].values.reshape(8,8), cmap='gray_r')\n",
    "axarr[0, 1].imshow(data.iloc[99].values.reshape(8,8), cmap='gray_r')\n",
    "axarr[0, 2].imshow(data.iloc[199].values.reshape(8,8), cmap='gray_r')\n",
    "axarr[0, 3].imshow(data.iloc[299].values.reshape(8,8), cmap='gray_r')\n",
    "\n",
    "axarr[1, 0].imshow(data.iloc[999].values.reshape(8,8), cmap='gray_r')\n",
    "axarr[1, 1].imshow(data.iloc[1099].values.reshape(8,8), cmap='gray_r')\n",
    "axarr[1, 2].imshow(data.iloc[1199].values.reshape(8,8), cmap='gray_r')\n",
    "axarr[1, 3].imshow(data.iloc[1299].values.reshape(8,8), cmap='gray_r')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## K-Nearest Neighbors Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.9888728037614452"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.model_selection import KFold\n",
    "\n",
    "# 50% Train / test validation\n",
    "def train_knn(nneighbors, train_features, train_labels):\n",
    "    knn = KNeighborsClassifier(n_neighbors = nneighbors)\n",
    "    knn.fit(train_features, train_labels)\n",
    "    return knn\n",
    "\n",
    "def test(model, test_features, test_labels):\n",
    "    predictions = model.predict(test_features)\n",
    "    train_test_df = pd.DataFrame()\n",
    "    train_test_df['correct_label'] = test_labels\n",
    "    train_test_df['predicted_label'] = predictions\n",
    "    overall_accuracy = sum(train_test_df[\"predicted_label\"] == train_test_df[\"correct_label\"])/len(train_test_df)    \n",
    "    return overall_accuracy\n",
    "\n",
    "def cross_validate(k):\n",
    "    fold_accuracies = []\n",
    "    kf = KFold(n_splits = 4, random_state=2, shuffle=True)\n",
    "    for train_index, test_index in kf.split(data):\n",
    "        train_features, test_features = data.loc[train_index], data.loc[test_index]\n",
    "        train_labels, test_labels = labels.loc[train_index], labels.loc[test_index]\n",
    "        model = train_knn(k, train_features, train_labels)\n",
    "        overall_accuracy = test(model, test_features, test_labels)\n",
    "        fold_accuracies.append(overall_accuracy)\n",
    "    return fold_accuracies\n",
    "        \n",
    "knn_one_accuracies = cross_validate(1)\n",
    "np.mean(knn_one_accuracies)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<matplotlib.lines.Line2D at 0x7f741052ed00>]"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 37740 characters]\n",
      "text/plain": [
       "<Figure size 800x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "k_values = list(range(1,10))\n",
    "k_overall_accuracies = []\n",
    "\n",
    "for k in k_values:\n",
    "    k_accuracies = cross_validate(k)\n",
    "    k_mean_accuracy = np.mean(k_accuracies)\n",
    "    k_overall_accuracies.append(k_mean_accuracy)\n",
    "    \n",
    "plt.figure(figsize=(8,4))\n",
    "plt.title(\"Mean Accuracy vs. k\")\n",
    "plt.plot(k_values, k_overall_accuracies)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Neural Network with One Hidden Layer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.model_selection import KFold\n",
    "from sklearn.neural_network import MLPClassifier\n",
    "\n",
    "# 50% Train / test validation\n",
    "def train_nn(neuron_arch, train_features, train_labels):\n",
    "    mlp = MLPClassifier(hidden_layer_sizes=neuron_arch)\n",
    "    mlp.fit(train_features, train_labels)\n",
    "    return mlp\n",
    "\n",
    "def test(model, test_features, test_labels):\n",
    "    predictions = model.predict(test_features)\n",
    "    train_test_df = pd.DataFrame()\n",
    "    train_test_df['correct_label'] = test_labels\n",
    "    train_test_df['predicted_label'] = predictions\n",
    "    overall_accuracy = sum(train_test_df[\"predicted_label\"] == train_test_df[\"correct_label\"])/len(train_test_df)    \n",
    "    return overall_accuracy\n",
    "\n",
    "def cross_validate(neuron_arch):\n",
    "    fold_accuracies = []\n",
    "    kf = KFold(n_splits = 4, random_state=2, shuffle=True)\n",
    "    for train_index, test_index in kf.split(data):\n",
    "        train_features, test_features = data.loc[train_index], data.loc[test_index]\n",
    "        train_labels, test_labels = labels.loc[train_index], labels.loc[test_index]\n",
    "       \n",
    "        model = train_nn(neuron_arch, train_features, train_labels)\n",
    "        overall_accuracy = test(model, test_features, test_labels)\n",
    "        fold_accuracies.append(overall_accuracy)\n",
    "    return fold_accuracies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[<matplotlib.lines.Line2D at 0x7f7410c80f10>]"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 30796 characters]\n",
      "text/plain": [
       "<Figure size 800x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "nn_one_neurons = [\n",
    "    (8,),\n",
    "    (16,),\n",
    "    (32,),\n",
    "    (64,),\n",
    "    (128,),\n",
    "    (256,)\n",
    "]\n",
    "nn_one_accuracies = []\n",
    "\n",
    "for n in nn_one_neurons:\n",
    "    nn_accuracies = cross_validate(n)\n",
    "    nn_mean_accuracy = np.mean(nn_accuracies)\n",
    "    nn_one_accuracies.append(nn_mean_accuracy)\n",
    "\n",
    "plt.figure(figsize=(8,4))\n",
    "plt.title(\"Mean Accuracy vs. Neurons In Single Hidden Layer\")\n",
    "\n",
    "x = [i[0] for i in nn_one_neurons]\n",
    "plt.plot(x, nn_one_accuracies)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Summary:\n",
    "\n",
    "It looks like adding more neurons to the single hidden layer improved simple accuracy to approximately `97%`. Simple accuracy computes the number of correct classifications the model made, but doesn't tell us anything about false or true positives or false or true negatives.\n",
    "\n",
    "Given that k-nearest neighbors achieved approximately `98%` accuracy, there doesn't seem to be any advantages to using a single hidden layer neural network for this problem."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Neural Network with Two Hidden Layers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<matplotlib.lines.Line2D at 0x7f740f793f70>]"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 38340 characters]\n",
      "text/plain": [
       "<Figure size 800x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "nn_two_neurons = [\n",
    "    (64,64),\n",
    "    (128, 128),\n",
    "    (256, 256)\n",
    "]\n",
    "nn_two_accuracies = []\n",
    "\n",
    "for n in nn_two_neurons:\n",
    "    nn_accuracies = cross_validate(n)\n",
    "    nn_mean_accuracy = np.mean(nn_accuracies)\n",
    "    nn_two_accuracies.append(nn_mean_accuracy)\n",
    "\n",
    "plt.figure(figsize=(8,4))\n",
    "plt.title(\"Mean Accuracy vs. Neurons In Two Hidden Layers\")\n",
    "\n",
    "x = [i[0] for i in nn_two_neurons]\n",
    "plt.plot(x, nn_two_accuracies)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[0.9710752289037367, 0.978304875030933, 0.9849802029200694]"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "nn_two_accuracies"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Summary\n",
    "\n",
    "Using two hidden layers improved our simple accuracy to `98%`. While, traditionally, we might worry about overfitting, using four-fold cross validation also gives us a bit more assurance that the model is generalizing to achieve the extra `1%` in simple accuracy over the single hidden layer networks we tried earlier."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Neural Network with Three Hidden Layers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.model_selection import KFold\n",
    "\n",
    "# 50% Train / test validation\n",
    "def train_nn(neuron_arch, train_features, train_labels):\n",
    "    mlp = MLPClassifier(hidden_layer_sizes=neuron_arch)\n",
    "    mlp.fit(train_features, train_labels)\n",
    "    return mlp\n",
    "\n",
    "def test(model, test_features, test_labels):\n",
    "    predictions = model.predict(test_features)\n",
    "    train_test_df = pd.DataFrame()\n",
    "    train_test_df['correct_label'] = test_labels\n",
    "    train_test_df['predicted_label'] = predictions\n",
    "    overall_accuracy = sum(train_test_df[\"predicted_label\"] == train_test_df[\"correct_label\"])/len(train_test_df)    \n",
    "    return overall_accuracy\n",
    "\n",
    "def cross_validate_six(neuron_arch):\n",
    "    fold_accuracies = []\n",
    "    kf = KFold(n_splits = 6, random_state=2, shuffle=True)\n",
    "    for train_index, test_index in kf.split(data):\n",
    "        train_features, test_features = data.loc[train_index], data.loc[test_index]\n",
    "        train_labels, test_labels = labels.loc[train_index], labels.loc[test_index]\n",
    "       \n",
    "        model = train_nn(neuron_arch, train_features, train_labels)\n",
    "        overall_accuracy = test(model, test_features, test_labels)\n",
    "        fold_accuracies.append(overall_accuracy)\n",
    "    return fold_accuracies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n",
      "/dataquest/system/env/python3/lib/python3.8/site-packages/sklearn/neural_network/_multilayer_perceptron.py:582: ConvergenceWarning:\n",
      "\n",
      "Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n",
      "\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[<matplotlib.lines.Line2D at 0x7f740f76ffa0>]"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 35596 characters]\n",
      "text/plain": [
       "<Figure size 800x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "nn_three_neurons = [\n",
    "    (10, 10, 10),\n",
    "    (64, 64, 64),\n",
    "    (128, 128, 128)\n",
    "]\n",
    "\n",
    "nn_three_accuracies = []\n",
    "\n",
    "for n in nn_three_neurons:\n",
    "    nn_accuracies = cross_validate_six(n)\n",
    "    nn_mean_accuracy = np.mean(nn_accuracies)\n",
    "    nn_three_accuracies.append(nn_mean_accuracy)\n",
    "\n",
    "plt.figure(figsize=(8,4))\n",
    "plt.title(\"Mean Accuracy vs. Neurons In Three Hidden Layers\")\n",
    "\n",
    "x = [i[0] for i in nn_three_neurons]\n",
    "plt.plot(x, nn_three_accuracies)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[0.9326830174656262, 0.9694072835377184, 0.9744072835377183]"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "nn_three_accuracies"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Summary\n",
    "\n",
    "Using three hidden layers returned a simple accuracy of nearly `98%`, even with six-fold cross validation."
   ]
  }
 ],
 "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.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
