diff --git a/user_contributed/nhanes_EDA.ipynb b/user_contributed/nhanes_EDA.ipynb new file mode 100644 index 0000000..1fa469f --- /dev/null +++ b/user_contributed/nhanes_EDA.ipynb @@ -0,0 +1,554 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "44d585ac-8ff5-480d-8359-2f0940d07f9e", + "metadata": {}, + "source": [ + "Univariate data analyses - NHANES case study - https://www.cdc.gov/nchs/nhanes/index.htm\n", + "\n", + "NHANES is a U.S. Government study with data properly anomonized to protect privacy. The study captures body biometrics along\n", + "with blood pressure and education levels. The study results for the 2015-2016 year of the study are used in this notebook. \n", + "Here is a link for the data file: https://wwwn.cdc.gov/nchs/nhanes/continuousnhanes/default.aspx?BeginYear=2015\n", + "\n", + "This notebook will compare and contrast using numpy and the Arkouda equivalents to perform some exploratory descriptive statistical analysis of the dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1141be04-537d-4bd2-9d96-2292c735faa5", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# setup pandas and other std packages\n", + "#\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "import pandas as pd\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53f3d2e9-02ae-4df5-a693-c1afc42a0e1c", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# setup arkouda packages and connection to arkouda\n", + "#\n", + "\n", + "import arkouda as ak\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "ak.connect()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5af4cca5-9906-432c-b1af-3edcf5295ae7", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# load NHANES data from file into Pandas and dump some results\n", + "#\n", + "\n", + "pd_df = pd.read_csv(\"NHanes/nhanes_2015_2016.csv\")\n", + "pd_df.head()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f12114ff-2eda-40e8-9cee-10d2c9fed7a8", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# LOAD NHANES data from pandas into arkouda dataframe and dump some results\n", + "#\n", + "ak_df = ak.DataFrame(pd_df)\n", + "ak_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "8557fb6a-6e3b-4839-9885-12d0960b2683", + "metadata": {}, + "source": [ + "The numbers 1, 2, 3, 4, 5, 9 seen below are integer codes for the 6 possible non-missing values of the DMDEDUC2 variable. The meaning of these codes is given in the NHANES codebook. This table shows, for example, that 1621 people in the data file have DMDEDUC=4, which indicates that the person has completed some college, but has not graduated with a four-year degree." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c763793-e1f2-4d97-842e-ad70e8f5ad12", + "metadata": {}, + "outputs": [], + "source": [ + "pd_df.DMDEDUC2.value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed59d234-8df2-44f9-977e-47972e502e76", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# We need to force these categorical values, which are floats, to int values\n", + "#\n", + "result = ak.value_counts(ak.cast(ak_df[\"DMDEDUC2\"],\"int\"))\n", + "display(result)" + ] + }, + { + "cell_type": "markdown", + "id": "e592b48f-7fc2-4362-94ae-c539adf700c1", + "metadata": {}, + "source": [ + "Notice that the Arkouda count method includes missing values that show up as a category of zero(Pandas by contrast does not show these values). Below you will see the results from a Pandas sum and an arkouda array sum which has difference of 261 (the number of missing values, aka NaN)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92dc9a74-a4f7-44c8-b948-fba7de66c27d", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Pandas sum of unique column values\n", + "#\n", + "pd_df.DMDEDUC2.value_counts().sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de25f0d3-bb68-4d5f-b884-e81d22a0b99d", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Arkouda sum of unique colum values, second array contains sum the first contast the categorical values\n", + "#\n", + "result[1].sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d36121a5-7c6f-459c-bea5-f02a89acac06", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# quick way to determine the missing values in the Pandas dataframe (aka NaN)\n", + "#\n", + "pd_df.DMDEDUC2.isnull().sum() " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab5e39d3-26d8-4828-8938-103e12eb22df", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# equivalent quick way to determine the missing values in the Arkouda dataframe (aka NaN)\n", + "#\n", + "ak.isnan(ak_df[\"DMDEDUC2\"]).sum()" + ] + }, + { + "cell_type": "markdown", + "id": "674682c5-1040-41ae-8709-6d3c31073313", + "metadata": {}, + "source": [ + "For many purposes it is more relevant to consider the proportion of the sample with each of the possible category values, rather than the number of people in each category. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcf69a3f-e4c6-4f18-b6ca-41eb6bb04595", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# convert value counts to a proportion\n", + "#\n", + "p = pd_df.DMDEDUC2.value_counts() # convert value counts to a proportion\n", + "p/p.sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92573653-3bf2-47b8-9a76-1a745c8966fc", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# We need to work a bit harder to get the porportion values in Arkouda\n", + "#\n", + "display(\"portions for column DMDEDUC2\")\n", + "display(result[0][1:]) # need to ignore zero counts as they are for nan columns\n", + "display(result[1][1:]/result[1][1:].sum())" + ] + }, + { + "cell_type": "markdown", + "id": "4cd804eb-bbf5-4901-858e-b99646788397", + "metadata": {}, + "source": [ + "A quick way to get a set of numerical summaries for a quantitative variable is with the describe data frame method. Below we demonstrate how to do this using the body weight variable (BMXWT). As with many surveys, some data values are missing, so we explicitly drop the missing cases using the dropna method before generating the summaries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9850db56-2a46-4a69-92c6-7949c340806e", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Pandas describe to get descriptive stats on a column\n", + "#\n", + "pd_df.BMXWT.dropna().describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3723e692-003b-4680-8930-05be673f09f3", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Arkouda to get descriptive stats on a column\n", + "# We need to drop nan values and add median and percentile using numpy, since Arkouda does not have implementations for these\n", + "# functions.\n", + "#\n", + "ak_bmxwt = ak_df[~ak.isnan(ak_df[\"BMXWT\"])][\"BMXWT\"] # get values that are not nan for \"BMXWT\" column\n", + "\n", + "display(np.count_nonzero(ak_bmxwt.to_ndarray()))\n", + "display(ak.mean(ak_bmxwt))\n", + "display(np.median(ak_bmxwt.to_ndarray()))\n", + "display(ak.std(ak_bmxwt))\n", + "display(ak.min(ak_bmxwt))\n", + "display(np.percentile(ak_bmxwt.to_ndarray(),25))\n", + "display(np.percentile(ak_bmxwt.to_ndarray(),50))\n", + "display(np.percentile(ak_bmxwt.to_ndarray(),75))\n", + "display(ak.max(ak_bmxwt))\n" + ] + }, + { + "cell_type": "markdown", + "id": "5fd386c5-51f6-4180-b77d-512fb6a6eb9c", + "metadata": {}, + "source": [ + "The blood pressure measurements are captured in the fields with the \"BPX\" prefix. \"BPXSYS1 & 2\" are the systolic blood pressure values\n", + "and \"BPXDI1 & 2\" are the diastolic blood pressure values.\n", + "\n", + "A person is generally considered to have pre-hypertension when their systolic blood pressure is between 120 and 139, or their diastolic blood pressure is between 80 and 89. Considering only the systolic condition, we can calculate the proprotion of the NHANES sample who would be considered to have pre-hypertension." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a858a601-0c89-441c-8229-1b8558a24fa5", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# First we will calculate the mean values in numpy\n", + "#" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a051ba8c-ef66-45b1-8ed4-de64955ab3a6", + "metadata": {}, + "outputs": [], + "source": [ + "display(np.mean((pd_df.BPXSY1 >= 120) & (pd_df.BPXSY2 <= 139)))\n", + "\n", + "display(np.mean((pd_df.BPXDI1 >= 80) & (pd_df.BPXDI2 <= 89)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cc7f298-9a8e-4b30-9344-d432ac2ddf72", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Now we will calculate the same values in Arkouda\n", + "# Again we will need to force a cast of the float values to int values in order to call Arkouda's\n", + "# stat functions\n", + "#\n", + "\n", + "display(ak.mean((ak.cast(ak_df[\"BPXSY1\"],\"int\") >= 120) & (ak.cast(ak_df[\"BPXSY2\"],\"int\") <= 139)))\n", + "\n", + "display(ak.mean((ak.cast(ak_df[\"BPXDI1\"],\"int\") >= 80) & (ak.cast(ak_df[\"BPXDI2\"],\"int\") <= 89)))\n" + ] + }, + { + "cell_type": "markdown", + "id": "939e7ca9-f9d7-41a0-9db2-2d0366c414b9", + "metadata": {}, + "source": [ + "Finally we calculate the proportion of NHANES subjects who are pre-hypertensive based on either systolic or diastolic blood pressure. Since some people are pre-hypertensive under both criteria, the proportion below is less than the sum of the two proportions calculated above.\n", + "\n", + "Since the combined systolic and diastolic condition for pre-hypertension is somewhat complex, below we construct temporary variables 'a' and 'b' that hold the systolic and diastolic pre-hypertensive status separately, then combine them with a \"logical or\" to obtain the final status for each subject." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d973808-1c88-4770-b126-f58c4e49d81f", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# pandas calulations\n", + "#\n", + "a = (pd_df.BPXSY1 >= 120) & (pd_df.BPXSY2 <= 139)\n", + "b = (pd_df.BPXDI1 >= 80) & (pd_df.BPXDI2 <= 89)\n", + "display(np.mean(a | b)) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad9833aa-de39-467d-9896-f96b7cfcc3fa", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# arkoudas dataframe calcs\n", + "#\n", + "a = (ak.cast(ak_df[\"BPXSY1\"],\"int\") >= 120) & (ak.cast(ak_df[\"BPXSY2\"],\"int\") <= 139)\n", + "b = (ak.cast(ak_df[\"BPXDI1\"],\"int\") >= 80) & (ak.cast(ak_df[\"BPXDI2\"],\"int\") <= 89)\n", + "\n", + "display(ak.mean(a | b)) " + ] + }, + { + "cell_type": "markdown", + "id": "eb15675f-73ce-48c5-8642-ac42b081235c", + "metadata": {}, + "source": [ + "Blood pressure measurements are affected by a phenomenon called \"white coat anxiety\", in which a subject's bood pressure may be slightly elevated if they are nervous when interacting with health care providers. Typically this effect subsides if the blood pressure is measured several times in sequence. In NHANES, both systolic and diastolic blood pressure are meausred three times for each subject (e.g. BPXSY2 is the second measurement of systolic blood pressure). We can calculate the extent to which white coat anxiety is present in the NHANES data by looking at the mean difference between the first two systolic or diastolic blood pressure measurements." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "beff425f-4942-4516-87ae-15f9cc64cdcf", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# pandas/numpy calulations (systolic and diastolic)\n", + "# pandas has smoke and mirrors to handle NaN\n", + "#\n", + "display(np.mean(pd_df.BPXSY1 - pd_df.BPXSY2))\n", + "display(np.mean(pd_df.BPXDI1 - pd_df.BPXDI2))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d28bbd42-08d8-41bf-a2a9-d5a8dfc1c829", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# arkouda calulations (systolic and diastolic)\n", + "# Need a mask to handle NaN columns\n", + "# Note the mask technique may increase the precision of the arkouda calculated values in prior cells\n", + "#\n", + "from functools import reduce\n", + "mask = reduce(lambda x, y: x&y,[~ak.isnan(ak_df[c_name]) if ak_df[c_name].dtype == ak.float64 else ak.ones(ak_df.size, 'bool') for c_name in [\"BPXSY1\", \"BPXSY2\"]])\n", + "masked_df = ak_df[mask]\n", + "display(ak.mean(masked_df[\"BPXSY1\"] - masked_df[\"BPXSY2\"]))\n", + "display(ak.mean(masked_df[\"BPXDI1\"] - masked_df[\"BPXDI2\"]))\n" + ] + }, + { + "cell_type": "markdown", + "id": "e9a11097-487a-4469-9063-0629a06adae6", + "metadata": {}, + "source": [ + "Graphical summaries\n", + "Quantitative variables can be effectively summarized graphically. Below we see the distribution of body weight (in Kg), shown as a histogram. It is evidently right-skewed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c530f6c-988d-4727-9660-de62f9b24f24", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# pandas body weight plot \n", + "#\n", + "\n", + "sns.histplot(\n", + " pd_df.BMXWT.dropna(), kde=True,\n", + " stat=\"density\", kde_kws=dict(cut=3))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12ebb66e-7373-4521-a595-997b1a6295bd", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# arkouda body weight plot\n", + "# Note: I had difficulty getting seaborn to accept an arkouda array, so I converted the arkouda array to \n", + "# an ndarray.\n", + "#\n", + "masked_df = ak_df[mask]\n", + "ak_bmxwt = masked_df[\"BMXWT\"]\n", + "\n", + "sns.histplot(\n", + " ak_bmxwt.to_ndarray(), kde=True,\n", + " stat=\"density\", kde_kws=dict(cut=3))" + ] + }, + { + "cell_type": "markdown", + "id": "483ba030-cc8a-4d08-bb9d-2c96b4dd43f6", + "metadata": {}, + "source": [ + "Next we look at the histogram of systolic blood pressure measurements. You can see that there is a tendency for the measurements to be rounded to the nearest 5 or 10 units." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc139264-36a4-444f-97f5-2c28b86ee453", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# pandas plot of systolic pressure\n", + "#\n", + "sns.histplot(\n", + " pd_df.BPXSY1.dropna(), kde=True,\n", + " stat=\"density\", kde_kws=dict(cut=3))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58b7932d-89c6-4119-b4dd-ed78afc7f9e5", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# arkouda plot of systolic pressure, again ndarray is the currency of the realm for plotting\n", + "#\n", + "masked_df = ak_df[mask]\n", + "ak_bpxsy1 = masked_df[\"BPXSY1\"]\n", + "\n", + "sns.histplot(\n", + " ak_bpxsy1.to_ndarray(), kde=True,\n", + " stat=\"density\", kde_kws=dict(cut=3))" + ] + }, + { + "cell_type": "markdown", + "id": "c0dbf019-4e59-48cd-9105-dbb6d7ccc8d3", + "metadata": {}, + "source": [ + "To compare several distributions, we can use side-by-side boxplots. Below we compare the distributions of the first and second systolic blood pressure measurements (BPXSY1, BPXSY2), and the first and second diastolic blood pressure measurements (BPXDI1, BPXDI2). As expected, diastolic measurements are substantially lower than systolic measurements. Above we saw that the second blood pressure reading on a subject tended on average to be slightly lower than the first measurement. This difference was less than 1 mm/Hg, so is not visible in the \"marginal\" distributions shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3354fad-e492-428e-95ca-b2accafaec4f", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# pandas boxplot of systolic diastolic pressures\n", + "#\n", + "bp = sns.boxplot(data=pd_df.loc[:, [\"BPXSY1\", \"BPXSY2\", \"BPXDI1\", \"BPXDI2\"]])\n", + "_ = bp.set_ylabel(\"Blood pressure in mm/Hg\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6745f734-2947-4d44-a69f-9d3fa541458e", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# arkouda boxplot of systolic diastolic pressures, again converting the arkouda dataframe back\n", + "# to a Pandas dataframe makes the plotting task easier\n", + "#\n", + "masked_df = ak_df[mask][\"BPXSY1\", \"BPXSY2\", \"BPXDI1\", \"BPXDI2\"]\n", + "df_x = masked_df.to_pandas()\n", + "\n", + "bp = sns.boxplot(data=df_x.loc[:,[\"BPXSY1\", \"BPXSY2\", \"BPXDI1\", \"BPXDI2\"]])\n", + "_ = bp.set_ylabel(\"Blood pressure in mm/Hg\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92d65ebd-ddde-42f6-8e37-e88d0fb6c1c7", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# tear down connection to arkouda\n", + "#\n", + "ak.disconnect()\n", + "#ak.shutdown()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9 (pytorch)", + "language": "python", + "name": "pytorch" + }, + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/user_contributed/pytorch.ipynb b/user_contributed/pytorch.ipynb new file mode 100644 index 0000000..51a55c4 --- /dev/null +++ b/user_contributed/pytorch.ipynb @@ -0,0 +1,153 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "30ce30ae-b80f-4218-9498-9fdb275bb8ab", + "metadata": {}, + "outputs": [], + "source": [ + "# \n", + "# This is a simple test to see if arkouda and pytorch can co-exist\n", + "# This test was run on an Apple Macbook Pro M2 Max using Applie Silcon with '\n", + "# 12 CPU cores and 38 GPU cores\n", + "# Pytorch version used was 2.1.0.dev20230831\n", + "#" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a715cc23-2ce4-425e-9ae5-565b07868dc0", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Import arkouda and numpy package\n", + "#\n", + "import arkouda as ak\n", + "import pandas as pd\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8c4f110-e1a5-48c0-9acc-34ece07385cf", + "metadata": {}, + "outputs": [], + "source": [ + "ak.connect()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80be03b4-1809-4df2-837b-0b23c8663ffe", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Some basic code to interrogate the version of GPU support software that is running\n", + "#\n", + "# What version of Python do you have?\n", + "import sys\n", + "import platform\n", + "import torch\n", + "import pandas as pd\n", + "import sklearn as sk\n", + "\n", + "has_gpu = torch.cuda.is_available()\n", + "has_mps = getattr(torch,'has_mps',False)\n", + "device = \"mps\" if getattr(torch,'has_mps',False) \\\n", + " else \"gpu\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + "print(f\"Python Platform: {platform.platform()}\")\n", + "print(f\"PyTorch Version: {torch.__version__}\")\n", + "print()\n", + "print(f\"Python {sys.version}\")\n", + "print(f\"Pandas {pd.__version__}\")\n", + "print(f\"Scikit-Learn {sk.__version__}\")\n", + "print(\"GPU is\", \"available\" if has_gpu else \"NOT AVAILABLE\")\n", + "print(\"MPS (Apple Metal) is\", \"AVAILABLE\" if has_mps else \"NOT AVAILABLE\")\n", + "print(f\"Target device is {device}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da20f4e4-ec6d-41fd-a730-cde8ee38c0a2", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Use Pytorch to create a tensor array from numpy\n", + "#\n", + "ndarray = np.array([0, 1, 2])\n", + "t = torch.from_numpy(ndarray)\n", + "display(t)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a60f5879-b904-49f1-8837-0c8a690298de", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Creation arkouda array from python list\n", + "#\n", + "a = [0, 1, 2, 3, 4]\n", + "ak_array = ak.array(a)\n", + "display(ak_array)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a468646d-eeda-4f80-b916-8d966ee865a2", + "metadata": {}, + "outputs": [], + "source": [ + "#\n", + "# Take the same arkouda array and create a tensor from it using GPUs\n", + "#\n", + "t = torch.from_numpy(ak_array.to_ndarray())\n", + "display(t)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85c4859a-90b9-40d5-ad8d-18321a8bf37c", + "metadata": {}, + "outputs": [], + "source": [ + "#ak.shutdown()\n", + "ak.disconnect()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9 (pytorch)", + "language": "python", + "name": "pytorch" + }, + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}