shap_plots.ipynb 18.9 KB
Newer Older
1 2 3 4 5 6
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
7 8 9 10 11 12 13 14 15
    "**SHAP Explainability Plots** \\\n",
    "_Author: Joaquín Torres Bravo_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Libraries"
16 17 18 19
   ]
  },
  {
   "cell_type": "code",
20
   "execution_count": 1,
21
   "metadata": {},
22 23 24 25 26 27 28 29 30 31
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "c:\\Users\\Joaquín Torres\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    }
   ],
32 33 34 35
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "import shap\n",
36
    "import matplotlib.pyplot as plt\n",
Joaquin Torres's avatar
Joaquin Torres committed
37
    "import matplotlib.patches as mpatches # Legend\n",
38
    "import matplotlib.colors as mcolors # Colormap\n",
Joaquin Torres's avatar
Joaquin Torres committed
39 40
    "import seaborn as sns\n",
    "from scipy.stats import zscore # Normalization"
41 42 43 44 45 46
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
47
    "### Data"
48 49 50 51
   ]
  },
  {
   "cell_type": "code",
Joaquin Torres's avatar
Joaquin Torres committed
52
   "execution_count": null,
53
   "metadata": {},
Joaquin Torres's avatar
Joaquin Torres committed
54
   "outputs": [],
55 56
   "source": [
    "# Retrieve attribute names in order\n",
Joaquin Torres's avatar
Joaquin Torres committed
57
    "attribute_names = attribute_names = list(np.load('../01-EDA/results/feature_names/all_features.npy', allow_pickle=True))\n",
58 59
    "\n",
    "# Load test data\n",
Joaquin Torres's avatar
Joaquin Torres committed
60 61 62 63
    "X_test_pre = np.load('../02-training_data_generation/results/pre/X_test_pre.npy', allow_pickle=True)\n",
    "y_test_pre = np.load('../02-training_data_generation/results/pre/y_test_pre.npy', allow_pickle=True)\n",
    "X_test_post = np.load('../02-training_data_generation/results/post/X_test_post.npy', allow_pickle=True)\n",
    "y_test_post = np.load('../02-training_data_generation/results/post/y_test_post.npy', allow_pickle=True)\n",
64 65 66 67 68 69 70 71 72 73 74 75
    "\n",
    "# Type conversion needed    \n",
    "data_dic = {\n",
    "    \"X_test_pre\": pd.DataFrame(X_test_pre, columns=attribute_names).convert_dtypes(),\n",
    "    \"y_test_pre\": y_test_pre,\n",
    "    \"X_test_post\": pd.DataFrame(X_test_post, columns=attribute_names).convert_dtypes(),\n",
    "    \"y_test_post\": y_test_post,\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
76
   "execution_count": 2,
77 78 79 80 81 82 83 84 85 86 87 88 89 90
   "metadata": {},
   "outputs": [],
   "source": [
    "method_names = {\n",
    "    0: \"ORIG\",\n",
    "    1: \"ORIG_CW\",\n",
    "    2: \"OVER\",\n",
    "    3: \"UNDER\"\n",
    "}\n",
    "model_choices = {\n",
    "    \"ORIG\": \"XGB\",\n",
    "    \"ORIG_CW\": \"RF\",\n",
    "    \"OVER\": \"XGB\",\n",
    "    \"UNDER\": \"XGB\"\n",
91 92
    "}\n",
    "\n",
93
    "# Load names of social and individual attributes\n",
Joaquin Torres's avatar
Joaquin Torres committed
94
    "soc_var_names = np.load('../01-EDA/results/feature_names/social_factors.npy', allow_pickle=True)\n",
95 96 97
    "ind_var_names = np.load('../01-EDA/results/feature_names/individual_factors.npy', allow_pickle=True)\n",
    "# Retrieve attribute names in order\n",
    "attribute_names = attribute_names = list(np.load('../01-EDA/results/feature_names/all_features.npy', allow_pickle=True))"
98 99 100 101 102 103
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
104
    "### SHAP Plots"
105 106 107 108
   ]
  },
  {
   "cell_type": "code",
109
   "execution_count": null,
110
   "metadata": {},
111
   "outputs": [],
112
   "source": [
113
    "method_name = 'OVER'\n",
114
    "\n",
115
    "plt.figure(figsize=(35, 75))\n",
116 117 118 119
    "for i, group in enumerate(['pre', 'post']):\n",
    "            X_test = data_dic['X_test_' + group]\n",
    "            y_test = data_dic['y_test_' + group]\n",
    "            model_name = model_choices[method_name]\n",
Joaquin Torres's avatar
Joaquin Torres committed
120
    "            shap_vals = np.load(f'./results/shap_values/{group}_{method_name}.npy')\n",
121 122
    "            ax = plt.subplot(2,1,i+1) # 2 rows (pre - post) 1 column\n",
    "            # show = False to modify plot before showing\n",
123
    "            shap.summary_plot(shap_vals, X_test, max_display=len(attribute_names), show=False)\n",
124 125
    "            plt.title(group.upper(), fontsize = 12, fontweight='bold')\n",
    "            plt.xlabel('SHAP Value')\n",
126
    "            plt.xlim(-3,5)\n",
127
    "            used_colors = {'purple': 'Social factor', 'green': 'Individual factor'}\n",
128
    "            # Modify color of attributes\n",
129 130 131 132 133 134 135 136 137 138
    "            for label in ax.get_yticklabels():\n",
    "                label_text = label.get_text()  # Get the text of the label\n",
    "                label.set_fontsize(8)\n",
    "                if label_text in soc_var_names:\n",
    "                        label.set_color('purple')\n",
    "                else:\n",
    "                        label.set_color('green')\n",
    "                # Create custom legend for each subplot\n",
    "                handles = [mpatches.Patch(color=color, label=label) for color, label in used_colors.items()]\n",
    "                ax.legend(handles=handles, loc='lower right', fontsize=8)\n",
139
    "            \n",
140 141
    "plt.suptitle(f'SHAP Summary Plots PRE vs POST - Pipeline: Oversampling - Model: {model_name}\\n\\n')\n",
    "plt.subplots_adjust(wspace=1)\n",
142
    "plt.tight_layout()\n",
Joaquin Torres's avatar
Joaquin Torres committed
143
    "plt.savefig(f'./results/plots/shap_summary/{method_name}_{model_name}.svg', format='svg', dpi=1250)\n",
Joaquin Torres's avatar
Joaquin Torres committed
144
    "plt.show()"
145
   ]
146 147 148
  },
  {
   "cell_type": "code",
Joaquin Torres's avatar
Joaquin Torres committed
149
   "execution_count": null,
150
   "metadata": {},
Joaquin Torres's avatar
Joaquin Torres committed
151
   "outputs": [],
152
   "source": [
Joaquin Torres's avatar
Joaquin Torres committed
153 154 155 156 157 158
    "method_name = 'ORIG_CW'\n",
    "plt.figure(figsize=(35, 75))\n",
    "for i, group in enumerate(['pre', 'post']):\n",
    "            X_test = data_dic['X_test_' + group]\n",
    "            y_test = data_dic['y_test_' + group]\n",
    "            model_name = model_choices[method_name]\n",
Joaquin Torres's avatar
Joaquin Torres committed
159
    "            shap_vals = np.load(f'./results/shap_values/{group}_{method_name}.npy')\n",
Joaquin Torres's avatar
Joaquin Torres committed
160 161 162 163 164 165 166 167
    "            shap_vals = shap_vals[:,:,1] # Select shap values for positive class\n",
    "            ax = plt.subplot(2,1,i+1)\n",
    "            shap.summary_plot(shap_vals, X_test, max_display=len(attribute_names), show=False)\n",
    "            plt.title(group.upper(), fontsize = 12, fontweight='bold')\n",
    "            plt.xlabel('SHAP Value')\n",
    "            plt.xlim(-0.5,0.5)\n",
    "            used_colors = {'purple': 'Social factor', 'green': 'Individual factor'}\n",
    "            for label in ax.get_yticklabels():\n",
168
    "                label_text = label.get_text()\n",
Joaquin Torres's avatar
Joaquin Torres committed
169 170 171 172 173 174 175
    "                label.set_fontsize(8)\n",
    "                if label_text in soc_var_names:\n",
    "                        label.set_color('purple')\n",
    "                else:\n",
    "                        label.set_color('green')\n",
    "                handles = [mpatches.Patch(color=color, label=label) for color, label in used_colors.items()]\n",
    "                ax.legend(handles=handles, loc='lower right', fontsize=8)\n",
176
    "\n",
Joaquin Torres's avatar
Joaquin Torres committed
177 178 179
    "plt.suptitle(f'SHAP Summary Plots PRE vs POST - Pipeline: Original with Class Weight - Model: {model_name}\\n\\n')\n",
    "plt.subplots_adjust(wspace=1)\n",
    "plt.tight_layout()\n",
Joaquin Torres's avatar
Joaquin Torres committed
180
    "plt.savefig(f'./results/plots/shap_summary/{method_name}_{model_name}.svg', format='svg', dpi=1250)\n",
Joaquin Torres's avatar
Joaquin Torres committed
181
    "plt.show()"
182
   ]
183 184 185 186 187
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
188 189 190 191 192 193 194
    "### SHAP Interaction Plots"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
Joaquin Torres's avatar
Joaquin Torres committed
195 196
    "**IMPORTANT NOTE** \\\n",
    "For the code to work as intended, the SHAP source code had to be modified: .venv/lib/python3.9/site-packages/shap/plots/_beeswarm.py \n",
197 198 199 200 201 202
    "\n",
    "sort_inds = np.argsort(-np.abs(shap_values.sum(1)).sum(0)) \\\n",
    "**replaced by** \\\n",
    "sort_inds = np.arange(39)\n",
    "\n",
    "The idea is to display the features in the original order instead of sorting them according to their absolute SHAP impact, as the library originally does\n"
203 204 205 206
   ]
  },
  {
   "cell_type": "code",
Joaquin Torres's avatar
Joaquin Torres committed
207
   "execution_count": null,
208
   "metadata": {},
Joaquin Torres's avatar
Joaquin Torres committed
209
   "outputs": [],
210
   "source": [
211 212
    "for method_name in ['ORIG_CW', 'OVER']:\n",
    "    for group in ['pre', 'post']:\n",
213 214 215
    "        X_test = data_dic['X_test_' + group]\n",
    "        y_test = data_dic['y_test_' + group]\n",
    "        model_name = model_choices[method_name]\n",
216
    "\n",
Joaquin Torres's avatar
Joaquin Torres committed
217
    "        shap_inter_vals = np.load(f'./results/shap_inter_values/{group}_{method_name}.npy')\n",
218 219
    "        if method_name == 'ORIG_CW':\n",
    "            shap_inter_vals = shap_inter_vals[:,:,:,1]  # Take info about positive class\n",
Joaquin Torres's avatar
Joaquin Torres committed
220
    "\n",
221 222
    "        num_instances = shap_inter_vals.shape[0]  # Dynamically get the number of instances\n",
    "        num_features = shap_inter_vals.shape[1]  # Assuming the number of features is the second dimension size\n",
223
    "\n",
224 225 226 227 228 229 230 231
    "        # Loop over each instance and set the diagonal and lower triangle of each 39x39 matrix to NaN\n",
    "        for i in range(num_instances):\n",
    "            # Mask the diagonal\n",
    "            np.fill_diagonal(shap_inter_vals[i], np.nan)\n",
    "            # Get indices for the lower triangle, excluding the diagonal\n",
    "            lower_triangle_indices = np.tril_indices(num_features, -1)  # -1 excludes the diagonal\n",
    "            # Set the lower triangle to NaN\n",
    "            shap_inter_vals[i][lower_triangle_indices] = np.nan\n",
232
    "\n",
233 234 235 236 237
    "        plt.figure(figsize=(10,10))\n",
    "        shap.summary_plot(shap_inter_vals, X_test, show=False, sort=False, max_display=len(attribute_names))\n",
    "        fig = plt.gcf()\n",
    "        attr_names = []\n",
    "        used_colors = {'purple': 'Social factor', 'green': 'Individual factor'}\n",
238
    "\n",
239 240 241 242 243 244 245 246 247 248 249
    "        # Iterate over all axes in the figure\n",
    "        for ax in fig.get_axes():\n",
    "            # Customize the y-axis tick labels\n",
    "            for label in ax.get_yticklabels():\n",
    "                label_text = label.get_text()  # Get the text of the label\n",
    "                attr_names.append(label_text)\n",
    "                label.set_fontsize(12)\n",
    "                if label_text in soc_var_names:\n",
    "                    label.set_color('purple')\n",
    "                else:\n",
    "                    label.set_color('green')\n",
250
    "\n",
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
    "        # Assuming the top labels are treated as titles, let's try to modify them\n",
    "        total_axes = len(fig.axes)\n",
    "        for i, ax in enumerate(fig.axes):\n",
    "            reverse_index = total_axes - 1 - i\n",
    "            title = attr_names[reverse_index]\n",
    "            ax.set_title(title, color='purple' if title in soc_var_names else 'green', fontsize=12, rotation=90)\n",
    "            if method_name == 'ORIG_CW':\n",
    "                ax.set_xlim(-0.15, 0.15)  # Use same scale for pre and post\n",
    "            elif method_name == 'OVER':\n",
    "                ax.set_xlim(-2, 2)\n",
    "\n",
    "        # Create a single general legend for the whole figure\n",
    "        handles = [mpatches.Patch(color=color, label=label) for color, label in used_colors.items()]\n",
    "        fig.legend(handles=handles, loc='lower right', fontsize=12)\n",
    "\n",
    "        # plt.suptitle(f'Simplified Example SHAP Summary Interaction Plot\\n', fontsize=15, fontweight='bold', x=0.5, y=0.95, ha='center')\n",
    "        plt.suptitle(f'SHAP Summary Interaction Plot - {method_name} - {str.upper(group)}\\n', fontsize=20, fontweight='bold') #, x=0.5, y=0.95, ha='center'\n",
    "        plt.tight_layout()\n",
Joaquin Torres's avatar
Joaquin Torres committed
269
    "        plt.savefig(f'./results/plots/shap_inter_summary/{str.upper(group)}_{method_name}_{model_name}.svg', format='svg', dpi=700)\n",
270
    "        # plt.show()"
271
   ]
272 273 274 275 276
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
277 278 279 280 281 282 283 284
    "### SHAP Interaction Analysis"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Distance Heatmaps"
285 286 287 288
   ]
  },
  {
   "cell_type": "code",
289
   "execution_count": 3,
290 291 292
   "metadata": {},
   "outputs": [],
   "source": [
Joaquin Torres's avatar
Ready  
Joaquin Torres committed
293
    "method_name = 'OVER'"
294 295 296 297
   ]
  },
  {
   "cell_type": "code",
298
   "execution_count": 4,
299 300 301
   "metadata": {},
   "outputs": [],
   "source": [
Joaquin Torres's avatar
Joaquin Torres committed
302 303 304
    "# Define the z-score normalization function\n",
    "def zscore_normalize(matrix):\n",
    "    return zscore(matrix, axis=None)  # Normalize across the entire matrix"
305 306 307 308
   ]
  },
  {
   "cell_type": "code",
309
   "execution_count": 5,
310 311 312 313
   "metadata": {},
   "outputs": [],
   "source": [
    "# Load array of shap inter matrices for pre and post for the chosen method\n",
Joaquin Torres's avatar
Joaquin Torres committed
314 315
    "shap_inter_vals_pre = np.load(f'./results/shap_inter_values/pre_{method_name}.npy')\n",
    "shap_inter_vals_post = np.load(f'./results/shap_inter_values/post_{method_name}.npy')\n",
316
    "if method_name == 'ORIG_CW':\n",
317
    "    shap_inter_vals_pre = shap_inter_vals_pre[:,:,:,1] # Take info about positive class\n",
318 319 320
    "    shap_inter_vals_post = shap_inter_vals_post[:,:,:,1]\n",
    "\n",
    "# Normalize each matrix in each of the two arrays\n",
Joaquin Torres's avatar
Joaquin Torres committed
321 322
    "norm_shap_inter_vals_pre = np.array([zscore_normalize(matrix) for matrix in shap_inter_vals_pre])\n",
    "norm_shap_inter_vals_post = np.array([zscore_normalize(matrix) for matrix in shap_inter_vals_post])\n",
323 324 325 326 327
    "\n",
    "# Aggregate matrices in each group by calculating the mean\n",
    "agg_shap_inter_vals_pre = np.mean(norm_shap_inter_vals_pre, axis=0)\n",
    "agg_shap_inter_vals_post = np.mean(norm_shap_inter_vals_post, axis=0)\n",
    "\n",
328 329
    "# Compute the difference between the aggregated matrices\n",
    "dist_matrix = agg_shap_inter_vals_post - agg_shap_inter_vals_pre"
330 331 332 333
   ]
  },
  {
   "cell_type": "code",
334
   "execution_count": 6,
335 336 337
   "metadata": {},
   "outputs": [],
   "source": [
338 339 340 341 342
    "# Color map\n",
    "colors = [(1, 0, 0), (1, 1, 1), (0, 1, 0)]  # Red, White, Green\n",
    "n_bins = 100  # Discretize the colormap into 100 values\n",
    "cmap_name = 'custom_red_white_dark_green'\n",
    "cmap = mcolors.LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bins)"
343 344 345 346
   ]
  },
  {
   "cell_type": "code",
347
   "execution_count": 7,
348 349 350 351 352 353 354 355 356
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_distance_heatmap(matrix, feature_names, soc_var_names, method_name):\n",
    "    # Create a mask for the upper triangle\n",
    "    mask = np.triu(np.ones_like(matrix, dtype=bool))\n",
    "\n",
    "    # Create the heatmap using Seaborn\n",
    "    plt.figure(figsize=(12, 12))\n",
357
    "    ax = sns.heatmap(matrix, mask=mask, cmap=cmap, center=0, annot=False, cbar=True,\n",
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
    "                xticklabels=feature_names, yticklabels=feature_names)\n",
    "\n",
    "    for tick_label in ax.get_yticklabels():\n",
    "        if tick_label.get_text() in soc_var_names:\n",
    "            tick_label.set_color('purple')  # Specific social variables\n",
    "        else:\n",
    "            tick_label.set_color('green')  # Other variables\n",
    "    \n",
    "    for tick_label in ax.get_xticklabels():\n",
    "        if tick_label.get_text() in soc_var_names:\n",
    "            tick_label.set_color('purple')  # Specific social variables\n",
    "        else:\n",
    "            tick_label.set_color('green')  # Other variables\n",
    "        \n",
    "    plt.title(f'Distance Interaction Matrix between PRE and POST - Pipeline: {method_name}\\n', fontdict={'fontstyle': 'normal', 'weight': 'bold'})\n",
    "    # Create a custom legend\n",
    "    purple_patch = mpatches.Patch(color='purple', label='Social factor')\n",
    "    green_patch = mpatches.Patch(color='green', label='Individual factor')\n",
    "    plt.legend(handles=[purple_patch, green_patch], loc='upper right')\n",
    "    # Add a title to the color bar\n",
    "    cbar = ax.collections[0].colorbar\n",
379
    "    cbar.set_label('Interaction POST - Interaction PRE', labelpad=15, rotation=270, verticalalignment='bottom')\n",
380
    "\n",
Joaquin Torres's avatar
Joaquin Torres committed
381
    "    plt.savefig(f'./results/plots/heatmaps_interactions/DIST_{method_name}.svg', format='svg', dpi=600)\n",
382
    "    plt.tight_layout()\n",
383 384 385 386 387
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
388
   "execution_count": 8,
389
   "metadata": {},
390 391 392 393 394 395 396 397 398 399 400 401 402
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'attribute_names' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mNameError\u001b[0m                                 Traceback (most recent call last)",
      "Cell \u001b[1;32mIn[8], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m plot_distance_heatmap(dist_matrix, \u001b[43mattribute_names\u001b[49m, soc_var_names, method_name)\n",
      "\u001b[1;31mNameError\u001b[0m: name 'attribute_names' is not defined"
     ]
    }
   ],
403
   "source": [
404
    "plot_distance_heatmap(dist_matrix, attribute_names, soc_var_names, method_name)"
405
   ]
Joaquin Torres's avatar
Joaquin Torres committed
406
  },
407 408 409 410 411 412 413
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Excel Differences Sorted"
   ]
  },
Joaquin Torres's avatar
Joaquin Torres committed
414 415
  {
   "cell_type": "code",
Joaquin Torres's avatar
Ready  
Joaquin Torres committed
416
   "execution_count": null,
Joaquin Torres's avatar
Joaquin Torres committed
417
   "metadata": {},
Joaquin Torres's avatar
Ready  
Joaquin Torres committed
418
   "outputs": [],
419 420
   "source": [
    "# Define tolerance\n",
421 422
    "tolerance = np.median(np.abs(dist_matrix))  # Use the median of the absolute values for tolerance\n",
    "\n",
423 424 425 426 427 428
    "# Create a DataFrame to hold the interactions\n",
    "interactions = []\n",
    "\n",
    "# Iterate over the matrix to extract interactions above the tolerance\n",
    "for i in range(1, dist_matrix.shape[0]):\n",
    "    for j in range(i):  # Lower triangle exclduing diagonal\n",
429
    "        if abs(dist_matrix[i, j]) > tolerance: \n",
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
    "            var1 = attribute_names[i]\n",
    "            var2 = attribute_names[j]\n",
    "            if var1 in soc_var_names and var2 in soc_var_names:\n",
    "                inter_type = 'Social'\n",
    "            elif var1 in ind_var_names and var2 in ind_var_names:\n",
    "                inter_type = 'Individual'\n",
    "            else:\n",
    "                inter_type = 'Mixed'\n",
    "            interactions.append({\n",
    "                'Variable 1': var1, \n",
    "                'Variable 2': var2,\n",
    "                'SHAP Inter Variation PRE-POST': dist_matrix[i, j],\n",
    "                'Interaction Type': inter_type\n",
    "            })\n",
    "\n",
    "# Convert the list of dictionaries to a DataFrame\n",
    "interactions_df = pd.DataFrame(interactions)\n",
    "\n",
448 449
    "sorted_interactions_df = interactions_df.reindex(\n",
    "    interactions_df['SHAP Inter Variation PRE-POST'].abs().sort_values(ascending=False).index)\n",
450 451
    "\n",
    "# Export to Excel\n",
452
    "sorted_interactions_df.to_excel(f'./results/pre_post_inter_diff/inter_variation_{method_name}.xlsx', index=False)\n",
453 454 455
    "\n",
    "print(\"Excel file has been created.\")"
   ]
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "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",
Joaquin Torres's avatar
Joaquin Torres committed
474
   "version": "3.12.2"
475 476 477 478 479
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}