From 55bc11d9a70a8c4545a5a10cd6ac8bb6f9c06f2e Mon Sep 17 00:00:00 2001 From: Bogdan Cebere Date: Wed, 14 Dec 2022 15:02:22 +0200 Subject: [PATCH] More tutorials, bugfixing (#30) * cleanup * bump version * cleanup * cleanup * cleanup * improvements * debug * bugfixing * debug * debug --- src/hyperimpute/plugins/core/base_plugin.py | 11 + src/hyperimpute/plugins/imputers/base.py | 5 +- .../plugins/prediction/classifiers/base.py | 11 +- .../prediction/classifiers/plugin_catboost.py | 2 +- .../prediction/classifiers/plugin_gpboost.py | 2 +- .../classifiers/plugin_kneighbors.py | 2 +- .../prediction/classifiers/plugin_lgbm.py | 2 +- .../classifiers/plugin_logistic_regression.py | 2 +- .../classifiers/plugin_neural_nets.py | 2 +- .../classifiers/plugin_random_forest.py | 2 +- .../prediction/classifiers/plugin_svc.py | 2 +- .../prediction/classifiers/plugin_xgboost.py | 2 +- .../plugins/prediction/regression/base.py | 4 +- src/hyperimpute/version.py | 2 +- tests/imputers/test_em.py | 2 +- tests/imputers/test_gain.py | 2 +- tests/imputers/test_hyperimpute.py | 9 +- tests/imputers/test_ice.py | 2 +- tests/imputers/test_mice.py | 2 +- tests/imputers/test_missforest.py | 2 +- tests/imputers/test_miwae.py | 2 +- tests/imputers/test_sinkhorn.py | 2 +- tests/imputers/test_sklearn_ice.py | 2 +- tests/imputers/test_softimpute.py | 2 +- ...al_03_simulating_multiple_imputation.ipynb | 248 ++++++++++++++++++ 25 files changed, 298 insertions(+), 28 deletions(-) create mode 100644 tutorials/tutorial_03_simulating_multiple_imputation.ipynb diff --git a/src/hyperimpute/plugins/core/base_plugin.py b/src/hyperimpute/plugins/core/base_plugin.py index 74bf666..6d74f94 100644 --- a/src/hyperimpute/plugins/core/base_plugin.py +++ b/src/hyperimpute/plugins/core/base_plugin.py @@ -38,6 +38,8 @@ class Plugin(Serializable, metaclass=ABCMeta): def __init__(self) -> None: super().__init__() + self.drop_consts = [] + @staticmethod @abstractmethod def hyperparameter_space(*args: Any, **kwargs: Any) -> List[Params]: @@ -119,6 +121,13 @@ def fit_predict(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> pd.DataFram def fit(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> Any: X = cast.to_dataframe(X) + + for col in X.columns: + if len(X.loc[X[col].notna(), col].unique()) <= 1: + self.drop_consts.append(col) + + X = X.drop(columns=self.drop_consts) + self.columns = X.columns return self._fit(X, *args, **kwargs) @abstractmethod @@ -127,6 +136,7 @@ def _fit(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> "Plugin": def transform(self, X: pd.DataFrame) -> pd.DataFrame: X = cast.to_dataframe(X) + X = X.drop(columns=self.drop_consts) return pd.DataFrame(self._transform(X)) @abstractmethod @@ -135,6 +145,7 @@ def _transform(self, X: pd.DataFrame) -> pd.DataFrame: def predict(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> pd.DataFrame: X = cast.to_dataframe(X) + X = X.drop(columns=self.drop_consts) return pd.DataFrame(self._predict(X, *args, *kwargs)) @abstractmethod diff --git a/src/hyperimpute/plugins/imputers/base.py b/src/hyperimpute/plugins/imputers/base.py index 4f642f4..412fd8c 100644 --- a/src/hyperimpute/plugins/imputers/base.py +++ b/src/hyperimpute/plugins/imputers/base.py @@ -7,6 +7,7 @@ # hyperimpute absolute import hyperimpute.plugins.core.base_plugin as plugin +from hyperimpute.utils.distributions import enable_reproducible_results class ImputerPlugin(_BaseImputer, plugin.Plugin): @@ -24,8 +25,10 @@ class ImputerPlugin(_BaseImputer, plugin.Plugin): """ def __init__(self, random_state: int = 0) -> None: - super().__init__() + _BaseImputer.__init__(self) + plugin.Plugin.__init__(self) + enable_reproducible_results(random_state) self.random_state = random_state @staticmethod diff --git a/src/hyperimpute/plugins/prediction/classifiers/base.py b/src/hyperimpute/plugins/prediction/classifiers/base.py index d73b8c5..eb92610 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/base.py +++ b/src/hyperimpute/plugins/prediction/classifiers/base.py @@ -9,6 +9,7 @@ import hyperimpute.plugins.core.base_plugin as plugin import hyperimpute.plugins.prediction.base as prediction_base import hyperimpute.plugins.utils.cast as cast +from hyperimpute.utils.distributions import enable_reproducible_results from hyperimpute.utils.tester import Eval @@ -26,10 +27,15 @@ class ClassifierPlugin( If any method implementation is missing, the class constructor will fail. """ - def __init__(self, **kwargs: Any) -> None: + def __init__(self, random_state: int = 0, **kwargs: Any) -> None: self.args = kwargs + self.random_state = random_state - super().__init__() + enable_reproducible_results(self.random_state) + + ClassifierMixin.__init__(self) + BaseEstimator.__init__(self) + prediction_base.PredictionPlugin.__init__(self) @staticmethod def subtype() -> str: @@ -37,6 +43,7 @@ def subtype() -> str: def fit(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> plugin.Plugin: X = cast.to_dataframe(X) + enable_reproducible_results(self.random_state) if len(args) == 0: raise RuntimeError("Please provide the training labels as well") diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_catboost.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_catboost.py index e9e0a0b..6bb9fd1 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_catboost.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_catboost.py @@ -54,7 +54,7 @@ def __init__( random_strength: float = 1, **kwargs: Any ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) if model is not None: self.model = model return diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_gpboost.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_gpboost.py index 3b0c472..8ef08e6 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_gpboost.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_gpboost.py @@ -65,7 +65,7 @@ def __init__( hyperparam_search_iterations: Optional[int] = None, **kwargs: Any ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) if hyperparam_search_iterations: n_estimators = int(hyperparam_search_iterations) diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_kneighbors.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_kneighbors.py index 9e95f62..e212cfe 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_kneighbors.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_kneighbors.py @@ -36,7 +36,7 @@ def __init__( model: Any = None, **kwargs: Any ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) if model is not None: self.model = model return diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_lgbm.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_lgbm.py index 35c8716..3909acc 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_lgbm.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_lgbm.py @@ -65,7 +65,7 @@ def __init__( random_state: int = 0, **kwargs: Any ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) if model is not None: self.model = model return diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_logistic_regression.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_logistic_regression.py index 827ca50..f51fbb5 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_logistic_regression.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_logistic_regression.py @@ -53,7 +53,7 @@ def __init__( hyperparam_search_iterations: Optional[int] = None, **kwargs: Any ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) if model is not None: self.model = model return diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_neural_nets.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_neural_nets.py index 7b9efe1..c9b4dfa 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_neural_nets.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_neural_nets.py @@ -251,7 +251,7 @@ def __init__( hyperparam_search_iterations: Optional[int] = None, **kwargs: Any, ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) enable_reproducible_results(random_state) diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_random_forest.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_random_forest.py index 70c1d39..a5d4c80 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_random_forest.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_random_forest.py @@ -56,7 +56,7 @@ def __init__( hyperparam_search_iterations: Optional[int] = None, **kwargs: Any ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) if hyperparam_search_iterations: n_estimators = int(hyperparam_search_iterations) diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_svc.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_svc.py index fa5d49b..b598032 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_svc.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_svc.py @@ -33,7 +33,7 @@ def __init__( random_state: int = 0, **kwargs: Any ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) if hyperparam_search_iterations: max_iter = int(hyperparam_search_iterations) * 100 diff --git a/src/hyperimpute/plugins/prediction/classifiers/plugin_xgboost.py b/src/hyperimpute/plugins/prediction/classifiers/plugin_xgboost.py index 8f3082f..3e2a531 100644 --- a/src/hyperimpute/plugins/prediction/classifiers/plugin_xgboost.py +++ b/src/hyperimpute/plugins/prediction/classifiers/plugin_xgboost.py @@ -80,7 +80,7 @@ def __init__( hyperparam_search_iterations: Optional[int] = None, **kwargs: Any ) -> None: - super().__init__(**kwargs) + super().__init__(random_state=random_state, **kwargs) if hyperparam_search_iterations: n_estimators = int(hyperparam_search_iterations) diff --git a/src/hyperimpute/plugins/prediction/regression/base.py b/src/hyperimpute/plugins/prediction/regression/base.py index 94f755b..b99cb04 100644 --- a/src/hyperimpute/plugins/prediction/regression/base.py +++ b/src/hyperimpute/plugins/prediction/regression/base.py @@ -27,7 +27,9 @@ def __init__( self, **kwargs: Any, ) -> None: - super().__init__() + RegressorMixin.__init__(self) + BaseEstimator.__init__(self) + prediction_base.PredictionPlugin.__init__(self) self.args = kwargs diff --git a/src/hyperimpute/version.py b/src/hyperimpute/version.py index 6f8bb52..0365f9f 100644 --- a/src/hyperimpute/version.py +++ b/src/hyperimpute/version.py @@ -1,2 +1,2 @@ -__version__ = "0.1.10" +__version__ = "0.1.11" MAJOR_VERSION = "0.1" diff --git a/tests/imputers/test_em.py b/tests/imputers/test_em.py index cc54232..5a7a8fd 100644 --- a/tests/imputers/test_em.py +++ b/tests/imputers/test_em.py @@ -71,7 +71,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 10 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_gain.py b/tests/imputers/test_gain.py index 22c5364..53294bd 100644 --- a/tests/imputers/test_gain.py +++ b/tests/imputers/test_gain.py @@ -71,7 +71,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 10 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_hyperimpute.py b/tests/imputers/test_hyperimpute.py index b73e311..9a1bad2 100644 --- a/tests/imputers/test_hyperimpute.py +++ b/tests/imputers/test_hyperimpute.py @@ -76,8 +76,7 @@ def test_hyperimpute_plugin_fit_transform(test_plugin: ImputerPlugin) -> None: [[1, 1, 1, 1], [np.nan, np.nan, np.nan, np.nan], [3, 3, 9, 9], [2, 2, 2, 2]] ) ) - - assert not np.all(np.isnan(res)) + assert not np.any(np.isnan(res)) @pytest.mark.parametrize("test_plugin", [from_api(), from_module(), from_serde()]) @@ -92,7 +91,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 20 + n = 50 p = 4 mean = np.repeat(0, p) @@ -127,7 +126,7 @@ def test_compare_optimizers( np.random.seed(0) - n = 20 + n = 50 p = 4 mean = np.repeat(0, p) @@ -166,7 +165,7 @@ def test_imputation_order( np.random.seed(0) - n = 20 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_ice.py b/tests/imputers/test_ice.py index 2e9ed07..b96ded7 100644 --- a/tests/imputers/test_ice.py +++ b/tests/imputers/test_ice.py @@ -74,7 +74,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 10 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_mice.py b/tests/imputers/test_mice.py index 6c71336..344a926 100644 --- a/tests/imputers/test_mice.py +++ b/tests/imputers/test_mice.py @@ -81,7 +81,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 100 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_missforest.py b/tests/imputers/test_missforest.py index 8ea2f43..ecfc42f 100644 --- a/tests/imputers/test_missforest.py +++ b/tests/imputers/test_missforest.py @@ -72,7 +72,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 20 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_miwae.py b/tests/imputers/test_miwae.py index 3997515..51b7632 100644 --- a/tests/imputers/test_miwae.py +++ b/tests/imputers/test_miwae.py @@ -71,7 +71,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 100 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_sinkhorn.py b/tests/imputers/test_sinkhorn.py index 65b204b..c7d3da9 100644 --- a/tests/imputers/test_sinkhorn.py +++ b/tests/imputers/test_sinkhorn.py @@ -71,7 +71,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 10 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_sklearn_ice.py b/tests/imputers/test_sklearn_ice.py index 8b54095..8999262 100644 --- a/tests/imputers/test_sklearn_ice.py +++ b/tests/imputers/test_sklearn_ice.py @@ -74,7 +74,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 10 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tests/imputers/test_softimpute.py b/tests/imputers/test_softimpute.py index 6e5d690..d89343c 100644 --- a/tests/imputers/test_softimpute.py +++ b/tests/imputers/test_softimpute.py @@ -73,7 +73,7 @@ def test_compare_methods_perf( ) -> None: np.random.seed(0) - n = 20 + n = 50 p = 4 mean = np.repeat(0, p) diff --git a/tutorials/tutorial_03_simulating_multiple_imputation.ipynb b/tutorials/tutorial_03_simulating_multiple_imputation.ipynb new file mode 100644 index 0000000..5307efc --- /dev/null +++ b/tutorials/tutorial_03_simulating_multiple_imputation.ipynb @@ -0,0 +1,248 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "molecular-moscow", + "metadata": {}, + "source": [ + "# Simulating multiple imputation" + ] + }, + { + "cell_type": "markdown", + "id": "corrected-basis", + "metadata": {}, + "source": [ + "You can simulate multiple imputation using HyperImpute, using multiple random seeds." + ] + }, + { + "cell_type": "markdown", + "id": "auburn-hygiene", + "metadata": {}, + "source": [ + "### Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "wanted-point", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import warnings\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "from hyperimpute.plugins.utils.metrics import RMSE\n", + "from hyperimpute.plugins.utils.simulate import simulate_nan\n", + "\n", + "\n", + "from IPython.display import HTML, display\n", + "import tabulate\n", + "\n", + "if not sys.warnoptions:\n", + " warnings.simplefilter(\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "id": "devoted-console", + "metadata": {}, + "source": [ + "### Loading the Imputation plugins\n", + "\n", + "Make sure that you have installed HyperImpute in your workspace.\n", + "\n", + "You can do that by running `pip install .` in the root of the repository." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "coated-innocent", + "metadata": {}, + "outputs": [], + "source": [ + "from hyperimpute.plugins.imputers import Imputers, ImputerPlugin\n", + "\n", + "imputers = Imputers()" + ] + }, + { + "cell_type": "markdown", + "id": "advance-expert", + "metadata": {}, + "source": [ + "### Load the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "preceding-vermont", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from sklearn.preprocessing import LabelEncoder\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "# third party\n", + "from pathlib import Path\n", + "def download_dataset() -> pd.DataFrame:\n", + " Path(\"data\").mkdir(parents=True, exist_ok=True)\n", + " bkp_file = Path(\"data\") / \"anneal.csv\"\n", + " \n", + " if bkp_file.exists():\n", + " return pd.read_csv(bkp_file)\n", + " \n", + " df = pd.read_csv(\n", + " \"https://archive.ics.uci.edu/ml/machine-learning-databases/annealing/anneal.data\",\n", + " header=None,\n", + " )\n", + " df.to_csv(bkp_file, index = None)\n", + " \n", + " return df\n", + "\n", + "def dataset(random_state: int = 0) -> pd.DataFrame:\n", + " df = download_dataset()\n", + " df = df.replace('?', np.nan)\n", + "\n", + " for col in df.columns:\n", + " df.loc[df[col].notna(), col] = LabelEncoder().fit_transform(df.loc[df[col].notna(), col] )\n", + "\n", + " drop = []\n", + " for col in df.columns:\n", + " if len(df.loc[df[col].notna(), col].unique()) <= 1:\n", + " drop.append(col)\n", + " \n", + " df = df.drop(columns = drop).astype(float)\n", + " X = df.drop(columns = [df.columns[-1]])\n", + " y = df[df.columns[-1]]\n", + "\n", + " X = pd.DataFrame(X)\n", + " y = pd.Series(y)\n", + "\n", + " X.columns = X.columns.astype(str)\n", + " return train_test_split(X, y, test_size=0.2, stratify = y, random_state = random_state)\n", + "\n", + "\n", + "def ampute(x, mechanism, p_miss):\n", + " x_simulated = simulate_nan(np.asarray(x), p_miss, mechanism)\n", + "\n", + " mask = x_simulated[\"mask\"]\n", + " x_miss = x_simulated[\"X_incomp\"]\n", + "\n", + " return pd.DataFrame(x), pd.DataFrame(x_miss, columns = x.columns), pd.DataFrame(mask, columns = x.columns)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "chief-jimmy", + "metadata": {}, + "outputs": [], + "source": [ + "ampute_mechanism = \"MCAR\"\n", + "p_miss = 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "b331e636", + "metadata": {}, + "source": [ + "## Load model" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "42971b3b", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn import metrics\n", + "from hyperimpute.plugins.imputers import Imputers, ImputerPlugin\n", + "import xgboost as xgb\n", + "\n", + "\n", + "metrics_headers = [\"Seed\", \"AUROC\"]\n", + "test_score = []\n", + "\n", + "def get_metrics(X_train, y_train, X_test, y_test):\n", + " xgb_clf = xgb.XGBClassifier(verbosity=0)\n", + " xgb_clf = xgb_clf.fit(X_train, y_train)\n", + "\n", + " y_pred = xgb_clf.predict_proba(X_test)\n", + "\n", + " auroc = metrics.roc_auc_score(\n", + " y_test,\n", + " y_pred,\n", + " multi_class=\"ovr\",\n", + " )\n", + "\n", + " return auroc\n", + "\n", + "plugin = \"ice\"\n", + "\n", + "for seed in range(5):\n", + " X_train, X_test, y_train, y_test = dataset(random_state = seed)\n", + " x, x_miss, mask = ampute(X_train, ampute_mechanism, p_miss)\n", + "\n", + " model = Imputers().get(plugin, random_state = seed)\n", + " X_train_imp = model.fit_transform(x_miss.copy()).astype(float)\n", + " \n", + " drop = []\n", + " for col in X_test.columns:\n", + " if col not in X_train_imp.columns:\n", + " drop.append(col)\n", + " \n", + " X_test_eval = X_test.drop(columns = drop)\n", + " assert X_train_imp.shape[1] == X_test_eval.shape[1]\n", + " auroc = get_metrics(X_train_imp, y_train, X_test_eval, y_test)\n", + "\n", + " test_score.append([seed, auroc]) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10301aea", + "metadata": {}, + "outputs": [], + "source": [ + "display(\n", + " HTML(\n", + " tabulate.tabulate(test_score, headers=metrics_headers, tablefmt=\"html\")\n", + " )\n", + ")" + ] + } + ], + "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.9.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}