{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./images/DLI_Header.png\" width=400/>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Fundamentals of Accelerated Data Science # "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 06 - XGBoost ##\n",
    "\n",
    "**Table of Contents**\n",
    "<br>\n",
    "This notebook uses use GPU-accelerated gradient boosting to predict the probability that a given individual is infected with the simulated virus. This notebook covers the below sections: \n",
    "1. [Environment](#Environment)\n",
    "2. [Load Data](#Load-Data)\n",
    "3. [Train-Test Split](#Train-Test-Split)\n",
    "4. [XGBoost](#XGBoost)\n",
    "    * [Setting XGBoost Parameters](#Setting-XGBoost-Parameters)\n",
    "    * [Training the Model](#Training-the-Model)\n",
    "5. [Inspecting the Model](#Inspecting-the-Model)\n",
    "6. [Making Predictions](#Making-Predictions)\n",
    "7. [(Optional) Comparison: CPU-Only XGBoost](#(Optional)-Comparison:-CPU-Only-XGBoost)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Environment ##\n",
    "We will be using the [XGBoost](https://xgboost.readthedocs.io/en/latest/) library to create the gradient boosted model for this exercise.\n",
    "\n",
    "In addition to the usual RAPIDS components, we import several libraries that will help us understand and evaluate the XGBoost model once we have trained it. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "XGBoost version:  2.1.1\n"
     ]
    }
   ],
   "source": [
    "import cudf\n",
    "import cuml\n",
    "import cupy as cp\n",
    "\n",
    "from cuml.model_selection import train_test_split\n",
    "\n",
    "# visualization\n",
    "%matplotlib inline\n",
    "import matplotlib\n",
    "import matplotlib.pyplot as plt\n",
    "import graphviz\n",
    "\n",
    "# model analysis\n",
    "import numpy as np\n",
    "from sklearn.metrics import roc_curve\n",
    "from sklearn.metrics import auc\n",
    "\n",
    "import xgboost as xgb\n",
    "\n",
    "# xgboost version 1.0 or later is required to directly convert from cudf Dataframes to xgboost DMatrix format\n",
    "print('XGBoost version: ', xgb.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load Data ##\n",
    "For this notebook we load a subset of the population data, including both the columns we used before for logistic regression and the coordinate columns. XGBoost allows us to use data that have nonlinear relationships with the outcome of interest, and geospatial data often fall into that category."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "gdf = cudf.read_csv('./data/clean_uk_pop_full.csv', usecols=['age', 'sex', 'northing', 'easting', 'infected'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "age         float64\n",
       "sex         float64\n",
       "northing    float64\n",
       "easting     float64\n",
       "infected    float64\n",
       "dtype: object"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "gdf.dtypes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(58479894, 5)"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "gdf.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "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>age</th>\n",
       "      <th>sex</th>\n",
       "      <th>northing</th>\n",
       "      <th>easting</th>\n",
       "      <th>infected</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>515491.5313</td>\n",
       "      <td>430772.1875</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>503572.4688</td>\n",
       "      <td>434685.8750</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>517903.6563</td>\n",
       "      <td>432565.5313</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>517059.9063</td>\n",
       "      <td>427660.6250</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>509228.6875</td>\n",
       "      <td>425527.7813</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   age  sex     northing      easting  infected\n",
       "0  0.0  0.0  515491.5313  430772.1875       0.0\n",
       "1  0.0  0.0  503572.4688  434685.8750       0.0\n",
       "2  0.0  0.0  517903.6563  432565.5313       0.0\n",
       "3  0.0  0.0  517059.9063  427660.6250       0.0\n",
       "4  0.0  0.0  509228.6875  425527.7813       0.0"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "gdf.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before splitting the data for training and testing, we check our memory status. We want to be below half the memory usage on the active GPU so that the temporary increase from the split still fits in memory."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Tue Nov 11 12:20:47 2025       \n",
      "+-----------------------------------------------------------------------------+\n",
      "| NVIDIA-SMI 525.85.12    Driver Version: 525.85.12    CUDA Version: 12.0     |\n",
      "|-------------------------------+----------------------+----------------------+\n",
      "| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |\n",
      "| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |\n",
      "|                               |                      |               MIG M. |\n",
      "|===============================+======================+======================|\n",
      "|   0  Tesla T4            On   | 00000000:00:1B.0 Off |                    0 |\n",
      "| N/A   29C    P0    25W /  70W |   3686MiB / 15360MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   1  Tesla T4            On   | 00000000:00:1C.0 Off |                    0 |\n",
      "| N/A   28C    P0    25W /  70W |    168MiB / 15360MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   2  Tesla T4            On   | 00000000:00:1D.0 Off |                    0 |\n",
      "| N/A   28C    P0    25W /  70W |    168MiB / 15360MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   3  Tesla T4            On   | 00000000:00:1E.0 Off |                    0 |\n",
      "| N/A   27C    P0    25W /  70W |    168MiB / 15360MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "                                                                               \n",
      "+-----------------------------------------------------------------------------+\n",
      "| Processes:                                                                  |\n",
      "|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |\n",
      "|        ID   ID                                                   Usage      |\n",
      "|=============================================================================|\n",
      "+-----------------------------------------------------------------------------+\n"
     ]
    }
   ],
   "source": [
    "!nvidia-smi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train-Test Split ##\n",
    "We again use the splitting method to create training and testing data subsets, keeping in mind that we will be using additional memory by doing so."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "x_train, x_test, y_train, y_test = train_test_split(gdf[['age', 'sex', 'northing', 'easting']], gdf['infected'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Tue Nov 11 12:20:49 2025       \n",
      "+-----------------------------------------------------------------------------+\n",
      "| NVIDIA-SMI 525.85.12    Driver Version: 525.85.12    CUDA Version: 12.0     |\n",
      "|-------------------------------+----------------------+----------------------+\n",
      "| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |\n",
      "| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |\n",
      "|                               |                      |               MIG M. |\n",
      "|===============================+======================+======================|\n",
      "|   0  Tesla T4            On   | 00000000:00:1B.0 Off |                    0 |\n",
      "| N/A   30C    P0    31W /  70W |   7266MiB / 15360MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   1  Tesla T4            On   | 00000000:00:1C.0 Off |                    0 |\n",
      "| N/A   28C    P0    25W /  70W |    168MiB / 15360MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   2  Tesla T4            On   | 00000000:00:1D.0 Off |                    0 |\n",
      "| N/A   28C    P0    25W /  70W |    168MiB / 15360MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   3  Tesla T4            On   | 00000000:00:1E.0 Off |                    0 |\n",
      "| N/A   27C    P0    25W /  70W |    168MiB / 15360MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "                                                                               \n",
      "+-----------------------------------------------------------------------------+\n",
      "| Processes:                                                                  |\n",
      "|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |\n",
      "|        ID   ID                                                   Usage      |\n",
      "|=============================================================================|\n",
      "+-----------------------------------------------------------------------------+\n"
     ]
    }
   ],
   "source": [
    "!nvidia-smi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Note**: Now that we have training and testing subsets, we can delete the original data so we have room for the algorithm's memory overhead--not critical in this case, but a useful practice when trying to train on as much data as possible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "del(gdf)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## XGBoost ##"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setting XGBoost Parameters ###\n",
    "We can now set the parameters for this XGBoost training run. These will determine the type and size of trees being created, as well as the way we measure success. \n",
    "\n",
    "The `cuda` device is a critical parameter: it tells XGBoost that we want the training to run on GPU.\n",
    "\n",
    "For our use case, we again want to predict the probability that an individual is infected with the virus, so we set the `binary:logistic` objective (binary outcome, using the logistic method to get probabilities).\n",
    "\n",
    "Other parameter options and meanings can be found at [XGBoost parameters](https://xgboost.readthedocs.io/en/latest/parameter.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [],
   "source": [
    "params = {\n",
    "    'max_depth':    8,\n",
    "    'max_leaves':   2**8,\n",
    "    'device': 'cuda',\n",
    "    'tree_method':  'hist',\n",
    "    'objective':    'binary:logistic',\n",
    "    'grow_policy':  'lossguide',\n",
    "    'eval_metric':  'logloss',\n",
    "    'subsample':    '0.8'\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Training the Model ###\n",
    "XGBoost uses a special, highly efficient data structure called a `DMatrix`, so we pass the training dataframes into it to create one.\n",
    "\n",
    "Note that the data is remaining on GPU, passing directly from cuDF to XGBoost."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [],
   "source": [
    "dtrain = xgb.DMatrix(x_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we are ready to train the model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 17.8 s, sys: 318 ms, total: 18.2 s\n",
      "Wall time: 17.2 s\n"
     ]
    }
   ],
   "source": [
    "%time model = xgb.train(params, dtrain, num_boost_round=100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.save_model('xgboost_model.json')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           age  sex     northing      easting\n",
      "3774403   10.0  0.0  300590.6563  463512.2813\n",
      "19981063  53.0  0.0  260185.9375  558031.7500\n",
      "23179505  62.0  0.0  440637.3750  400370.0625\n",
      "35323249  19.0  1.0  401843.2813  383316.1250\n",
      "58105125  89.0  1.0  176550.6094  498036.3438\n",
      "...        ...  ...          ...          ...\n",
      "22276378  59.0  0.0  205883.0469  515342.6563\n",
      "49922303  56.0  1.0  343180.5000  350500.5625\n",
      "55683841  74.0  1.0  271719.6875  277716.1250\n",
      "3385275    9.0  0.0  425986.0000  404063.0938\n",
      "5529815   15.0  0.0  343445.5938  395184.2500\n",
      "\n",
      "[43859920 rows x 4 columns]\n",
      "3774403     0.0\n",
      "19981063    0.0\n",
      "23179505    0.0\n",
      "35323249    0.0\n",
      "58105125    0.0\n",
      "           ... \n",
      "22276378    0.0\n",
      "49922303    0.0\n",
      "55683841    0.0\n",
      "3385275     0.0\n",
      "5529815     0.0\n",
      "Name: infected, Length: 43859920, dtype: float64\n"
     ]
    }
   ],
   "source": [
    "print(x_train)\n",
    "print(y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Note**: As a point of comparison, code to run the CPU-only version of XGBoost is provided at the bottom of this exercise."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Inspecting the Model ##\n",
    "We can examine the model in several ways. First, we can see which features the model believes to be most important in its assessment. Higher F scores indicate higher estimated importance.\n",
    "\n",
    "There appears to be a strong geospatial component to the infection distribution, since the easting and northing features have the highest F scores. In addition, age appears to have a stronger impact than sex in determining infection rates (consistent with the results we received from the logistic regression analysis)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 24556 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "ax = xgb.plot_importance(model, height=0.8)\n",
    "ax.grid(False)\n",
    "ax.set_title('F score by feature')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can also pull out individual trees within the model and see exactly which decisions it uses for its contributions to the ensemble. Notice that as with all ensemble methods, individual trees may not appear to produce major differences in outcomes (values on the leaf nodes); the aggregation of often individually-weak trees into a strong model gives XGBoost its power.\n",
    "\n",
    "Try changing `num_trees` to examine different trees in the model. Changing `rankdir` to `'TB'` will reorient the trees to top-down."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 7047528 characters]",
      "text/plain": [
       "<Figure size 10000x10000 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "xgb.plot_tree(model, num_trees=0, rankdir='LR')\n",
    "\n",
    "# get current figure to set the size\n",
    "fig = matplotlib.pyplot.gcf()\n",
    "fig.set_size_inches(100, 100)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Making Predictions ##\n",
    "Once we are comfortable with the model, we move to making predictions with it. We confirm that we will be making predictions on over 11 million rows of data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(14619974, 4)"
      ]
     },
     "execution_count": 45,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "x_test.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We convert the *X* matrix to a `DMatrix` as before, then make a prediction for each row. Notice the amount of time it takes to make over 11 million predictions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 440 ms, sys: 35.3 ms, total: 475 ms\n",
      "Wall time: 475 ms\n"
     ]
    }
   ],
   "source": [
    "dtest = xgb.DMatrix(x_test)\n",
    "%time y_pred = model.predict(dtest)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we would like to see how good those predictions were. One common evaluation method is to calculate the area under the curve (AUC) of the receiver operating characteristic (ROC) curve.\n",
    "\n",
    "The predictions are a `numpy` array, so we convert the test labels to match, then run the ROC curve calculation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_test_cpu = cp.asnumpy(cp.array(y_test))\n",
    "false_pos_rate, true_pos_rate, thresholds = roc_curve(y_test_cpu, y_pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Last, we can plot the curve and calculate the AUC score to help us assess the trade offs between true and false positive rates."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 41564 characters]",
      "text/plain": [
       "<Figure size 500x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "auc_result = auc(false_pos_rate, true_pos_rate)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(5, 5))\n",
    "ax.plot(false_pos_rate, true_pos_rate, lw=3,\n",
    "        label='AUC = {:.2f}'.format(auc_result))\n",
    "ax.plot([0, 1], [0, 1], 'k--', lw=2)\n",
    "ax.set(\n",
    "    xlim=(0, 1),\n",
    "    ylim=(0, 1),\n",
    "    title=\"ROC Curve\",\n",
    "    xlabel=\"False Positive Rate\",\n",
    "    ylabel=\"True Positive Rate\",\n",
    ")\n",
    "ax.legend(loc='lower right');\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## (Optional) Comparison: CPU-Only XGBoost ##\n",
    "Below we provide code for training and inferring from a CPU-only XGBoost using the same model parameters other than switching the histogram tree method from GPU to CPU."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 26min 49s, sys: 18.9 s, total: 27min 8s\n",
      "Wall time: 1min\n"
     ]
    }
   ],
   "source": [
    "params['device'] = 'cpu'\n",
    "dtrain_cpu = xgb.DMatrix(x_train.to_pandas(), y_train.to_pandas())\n",
    "%time model_cpu = xgb.train(params, dtrain_cpu, num_boost_round=100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 1min 5s, sys: 81.5 ms, total: 1min 5s\n",
      "Wall time: 1.5 s\n"
     ]
    }
   ],
   "source": [
    "dtest_cpu = xgb.DMatrix(x_test.to_pandas())\n",
    "%time y_pred_cpu = model_cpu.predict(dtest_cpu)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'status': 'ok', 'restart': True}"
      ]
     },
     "execution_count": 51,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import IPython\n",
    "app = IPython.Application.instance()\n",
    "app.kernel.do_shutdown(True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Well Done!** Let's move to the [next notebook](3-07_triton.ipynb). "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./images/DLI_Header.png\" width=400/>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.10.15"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
