{"metadata":{"kaggle":{"accelerator":"none","dataSources":[{"sourceId":53666,"databundleVersionId":6589269,"sourceType":"competition"},{"sourceId":6938561,"sourceType":"datasetVersion","datasetId":3833478}],"dockerImageVersionId":30558,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false},"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CMI - Detect Sleep States competition - **prediction notebook** (LightGBM)","metadata":{}},{"cell_type":"markdown","source":"The general idea of this notebook is to optimize the memory usage for test data. It is very easy to exceed Kaggle RAM limit. \n  \nI received my competition metric (~0.7) using neural networks, but I still decided to upload this notebook for LightGBM, as this approach might be useful for future projects where memory management is important.\n  \nMy code during the competition was quite messy, so I arranged it into a more readable and reusable form using classes.  \nFollowing main principles are implemented for memory optimization:\n\n1. Using [polars](https://github.com/pola-rs/polars) library for all data processing.\n2. Data types conversion (something similar to [this notebook](https://www.kaggle.com/code/henriupton/efficient-loading-memory-usage-visualizations-cmi)). In particular, a mapping (str -> int) was used for the `series_id` column during both training and prediction steps.\n3. Running data preprocessing on chunks of data. Number of chunks is controlled by DATA_CHUNKS parameter.\n4. Running LightGBM predictions on chunks of the chunked data (yep, with these many features even chunked data causes memory error).\n\nI added some visualisations at the end of notebook. To run this notebook for competition prediction set DEV = False.","metadata":{}},{"cell_type":"markdown","source":"## Loading libraries and data","metadata":{"papermill":{"duration":0.008356,"end_time":"2023-10-14T12:12:34.521306","exception":false,"start_time":"2023-10-14T12:12:34.512950","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import gc\nimport math\nimport os\nimport random\n\nimport lightgbm as lgb\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nfrom tqdm import tqdm","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":2.989242,"end_time":"2023-10-14T12:12:37.518785","exception":false,"start_time":"2023-10-14T12:12:34.529543","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:15:02.209730Z","iopub.execute_input":"2023-12-25T18:15:02.210216Z","iopub.status.idle":"2023-12-25T18:15:04.222184Z","shell.execute_reply.started":"2023-12-25T18:15:02.210179Z","shell.execute_reply":"2023-12-25T18:15:04.220878Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"DEV = True\nDATA_CHUNKS = 3","metadata":{"execution":{"iopub.status.busy":"2023-12-25T18:15:04.228867Z","iopub.execute_input":"2023-12-25T18:15:04.229858Z","iopub.status.idle":"2023-12-25T18:15:04.233961Z","shell.execute_reply.started":"2023-12-25T18:15:04.229760Z","shell.execute_reply":"2023-12-25T18:15:04.232752Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"DATA_PATH = \"data/\" if \"data\" in os.listdir(\"./\") else \"/kaggle/input/child-mind-institute-detect-sleep-states/\"\nMODEL_PATH = \"processed_data/\" if \"data\" in os.listdir(\"./\") else \"/kaggle/input/sleep-states/\"\nMAPPINGS_PATH = \"mappings/\" if \"mappings\" in os.listdir(\"./\") else \"/kaggle/input/sleep-states/\"\n\nTEST_FILE = \"train_series.parquet\" if DEV else \"test_series.parquet\"","metadata":{"papermill":{"duration":0.016641,"end_time":"2023-10-14T12:12:37.543447","exception":false,"start_time":"2023-10-14T12:12:37.526806","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:15:04.235955Z","iopub.execute_input":"2023-12-25T18:15:04.237089Z","iopub.status.idle":"2023-12-25T18:15:04.247574Z","shell.execute_reply.started":"2023-12-25T18:15:04.237041Z","shell.execute_reply":"2023-12-25T18:15:04.246503Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"gbm = lgb.Booster(model_file=MODEL_PATH + 'model_0.txt')\nFINAL_FEATURES = gbm.feature_name()","metadata":{"papermill":{"duration":0.137194,"end_time":"2023-10-14T12:12:37.871717","exception":false,"start_time":"2023-10-14T12:12:37.734523","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:15:04.250692Z","iopub.execute_input":"2023-12-25T18:15:04.251118Z","iopub.status.idle":"2023-12-25T18:15:04.322694Z","shell.execute_reply.started":"2023-12-25T18:15:04.251083Z","shell.execute_reply":"2023-12-25T18:15:04.319980Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"series_id_map = pd.read_csv(MAPPINGS_PATH + \"series_id_map.csv\", index_col=0).to_dict()['0']\nid_map = pd.read_csv(MAPPINGS_PATH + \"series_id_inverse_map.csv\", index_col=0).to_dict()['0']","metadata":{"papermill":{"duration":0.174243,"end_time":"2023-10-14T12:12:37.726140","exception":false,"start_time":"2023-10-14T12:12:37.551897","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:15:04.324207Z","iopub.execute_input":"2023-12-25T18:15:04.324613Z","iopub.status.idle":"2023-12-25T18:15:04.345675Z","shell.execute_reply.started":"2023-12-25T18:15:04.324578Z","shell.execute_reply":"2023-12-25T18:15:04.344479Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"loaded_file = pl.scan_parquet(DATA_PATH + TEST_FILE, low_memory=True)\n\nif DEV:\n    random.seed(1)\n    id_filter = random.sample(list(series_id_map.values()), 10) # initially 200\n    loaded_file = loaded_file.filter(pl.col(\"series_id\").is_in([id_map[f] for f in id_filter]))\n\nLENGTH = loaded_file.select(pl.count()).collect().item()","metadata":{"papermill":{"duration":0.015025,"end_time":"2023-10-14T12:12:37.894785","exception":false,"start_time":"2023-10-14T12:12:37.879760","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:15:04.347349Z","iopub.execute_input":"2023-12-25T18:15:04.348052Z","iopub.status.idle":"2023-12-25T18:15:09.569740Z","shell.execute_reply.started":"2023-12-25T18:15:04.348009Z","shell.execute_reply":"2023-12-25T18:15:09.568846Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Test data processing / getting predictions","metadata":{"papermill":{"duration":0.007801,"end_time":"2023-10-14T12:12:37.910532","exception":false,"start_time":"2023-10-14T12:12:37.902731","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class DataManager:\n    \"\"\"\n    This class slices polars dataframe in accordance with provided offset and \n    slice length, then lazily preprocesses data and generates features using polars\n    upon initialization. Two methods are designed to be used outside of the class:\n\n    Methods\n    -------\n    process_data(self) -> pd.core.frame.DataFrame\n        Runs polars evaluations and returns result as a pandas dataframe.\n\n    select_columns_for_sub(self) -> pd.core.frame.DataFrame\n        Provides a backbone for the competition submission. \n    \"\"\"\n\n    def __init__(self, data: pl.lazyframe.frame.LazyFrame, offset: int, length: int):\n        self._data = self._slice(data, offset, length)\n        self._preprocess()\n        self._make_datetime_features()\n        self._make_features()\n\n    def _slice(\n        self, data: pl.lazyframe.frame.LazyFrame, offset: int, length: int\n    ) -> pl.lazyframe.frame.LazyFrame:\n        return data.slice(offset=offset, length=length)\n\n    def _preprocess(self):\n        conversions = [\n            pl.col(\"timestamp\")\n            .str.strptime(pl.Datetime, \"%Y-%m-%dT%H:%M:%S%Z\")\n            .dt.replace_time_zone(None),\n            pl.col(\"anglez\").cast(pl.Int16),\n            (pl.col(\"enmo\") * 1000).cast(pl.UInt16),\n            #     pl.col(\"series_id\")\n            #     .map_dict(series_id_map)\n            #     .cast(pl.UInt16),\n        ]\n        fill_na = [\n            pl.col(\"step\").fill_nan(0).cast(pl.UInt32),\n        ]\n        self._data = self._data.with_columns(conversions + fill_na)\n\n    def _make_datetime_features(self):\n        dt_transforms = [\n            pl.col(\"timestamp\")\n            .dt.offset_by(\n                \"-18h\"\n            )  # starting the day at 18:00 to not get 2 onset events for one evening\n            .dt.ordinal_day()\n            .cast(pl.UInt16)\n            .alias(\"night\"),\n            pl.col(\"timestamp\").dt.hour().cast(pl.UInt8).alias(\"hour\"),\n            (2 * np.pi * pl.col(\"timestamp\").dt.hour() / 24)\n            .sin()\n            .cast(pl.Float32)\n            .alias(\"sin\"),\n        ]\n        self._data = self._data.with_columns(dt_transforms)\n\n    def _make_features(self):\n        features = [\n            pl.col(\n                \"enmo\"\n            )  # https://www.kaggle.com/code/renatoreggiani/feature-eng-ideas-and-lightgbm-cmi\n            .apply(lambda enmo: max(0.0, enmo - 0.02))\n            .rolling_sum(12 * 5, center=True, min_periods=1)\n            .apply(lambda lids: 100 / (lids + 1))\n            .rolling_mean(12 * 15, center=True, min_periods=1)\n            .cast(pl.Float32)\n            .alias(\"lids\"),\n        ]\n        \n        # https://www.kaggle.com/code/lccburk/feature-engineering-and-random-forest-prediction\n        for mins in [5, 30, 60 * 2, 60 * 8]:\n            for var in [\"enmo\", \"anglez\"]:\n                features += [\n                    pl.col(var)\n                    .rolling_mean(12 * mins, center=True, min_periods=1)\n                    .abs()\n                    .cast(pl.UInt16)\n                    .alias(f\"{var}_{mins}m_mean\"),\n                    pl.col(var)\n                    .rolling_max(12 * mins, center=True, min_periods=1)\n                    .abs()\n                    .cast(pl.UInt16)\n                    .alias(f\"{var}_{mins}m_max\"),\n                    pl.col(var)\n                    .rolling_std(12 * mins, center=True, min_periods=1)\n                    .abs()\n                    .cast(pl.UInt16)\n                    .alias(f\"{var}_{mins}m_std\"),\n                \n                    # getting first variations\n                    (\n                        pl.col(var)\n                        .diff()\n                        .abs()\n                        .rolling_mean(12 * mins, center=True, min_periods=1)\n                        * 10\n                    )\n                    .abs()\n                    .cast(pl.UInt16)\n                    .alias(f\"{var}_1v_{mins}m_mean\"),\n                    (\n                        pl.col(var)\n                        .diff()\n                        .abs()\n                        .rolling_std(12 * mins, center=True, min_periods=1)\n                        * 10\n                    )\n                    .abs()\n                    .cast(pl.UInt16)\n                    .alias(f\"{var}_1v_{mins}m_std\"),\n                    (\n                        pl.col(var)\n                        .diff()\n                        .abs()\n                        .rolling_max(12 * mins, center=True, min_periods=1)\n                        * 10\n                    )\n                    .abs()\n                    .cast(pl.UInt32)\n                    .alias(f\"{var}_1v_{mins}m_max\"),\n                ]\n        self._data = self._data.with_columns(features)\n\n    def process_data(self) -> pd.core.frame.DataFrame:\n        # selecting only features that are present in the model\n        final_data = self._data.select(FINAL_FEATURES).collect().to_pandas()\n        \n        # case test data has unknown series_id\n        for i in final_data.series_id.unique():\n            if i not in series_id_map:\n                series_id_map[i] = max(series_id_map.values()) + 1\n                id_map[max(series_id_map.values()) + 1] = i\n    \n        final_data[\"series_id\"] = final_data[\"series_id\"].map(series_id_map).astype(\"uint16\")\n        return final_data\n        \n    def select_columns_for_sub(self) -> pd.core.frame.DataFrame:\n        selected = self._data.select([\"night\", \"series_id\", \"step\"])\n        selected = selected.collect().to_pandas()\n        selected = selected.rename({\"night\": \"day\"}, axis=1)\n        return selected","metadata":{"execution":{"iopub.status.busy":"2023-12-25T18:15:09.571036Z","iopub.execute_input":"2023-12-25T18:15:09.571572Z","iopub.status.idle":"2023-12-25T18:15:09.593662Z","shell.execute_reply.started":"2023-12-25T18:15:09.571542Z","shell.execute_reply":"2023-12-25T18:15:09.591242Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class Model:\n    \"\"\"\n    This class gets predictions from a trained model using data chunks\n    and adds these predictions to submission file. \n\n    Methods\n    -------\n    make_predictions(self, test: pd.core.frame.DataFrame, num_chunks: int = 10)\n        Runs LightGBM predictions for passed data and saves predictions to self.preds.\n\n    add_preds_to_submission(\n        self, sub: pd.core.frame.DataFrame\n    ) -> pd.core.frame.DataFrame)\n        Adds predictions to a provided submission file. The length of predictions should\n        match the length of submission file, this is ensured by DataManager class.\n    \"\"\"\n\n    def __init__(self):\n        pass\n        \n    def make_predictions(self, test: pd.core.frame.DataFrame, num_chunks: int = 10):\n        \"\"\"\n        This function predicts results for test sample.\n        It divides test sample into chunks, predicts for every chunk\n        and then combines all predictions into one array.\n        \"\"\"\n\n        test = test[FINAL_FEATURES]  # just in case\n        list_of_arrays = []\n        chunking_step = math.ceil(len(test) / num_chunks)\n        \n        for i in tqdm(range(num_chunks)):\n            test_slice = test.iloc[i * chunking_step: (i + 1) * chunking_step]\n            current_preds = gbm.predict(test_slice)\n            gc.collect()\n            list_of_arrays.append(current_preds)\n        \n        self.preds = np.vstack(list_of_arrays)\n        \n    def add_preds_to_submission(\n        self, sub: pd.core.frame.DataFrame\n    ) -> pd.core.frame.DataFrame:\n        sub[\"onset_prob\"] = self.preds.T[0]\n        sub[\"wakeup_prob\"] = self.preds.T[1]\n        del self.preds\n        \n        onset_idx = sub.groupby([\"series_id\", \"day\"])[\"onset_prob\"].idxmax()  \n        wakeup_idx = sub.groupby([\"series_id\", \"day\"])[\"wakeup_prob\"].idxmax()\n        sub.loc[onset_idx, \"event\"] = \"onset\"\n        sub.loc[wakeup_idx, \"event\"] = \"wakeup\"\n        sub.dropna(inplace=True)  # leaving only events\n        return sub","metadata":{"execution":{"iopub.status.busy":"2023-12-25T18:15:09.595558Z","iopub.execute_input":"2023-12-25T18:15:09.595921Z","iopub.status.idle":"2023-12-25T18:15:09.631750Z","shell.execute_reply.started":"2023-12-25T18:15:09.595890Z","shell.execute_reply":"2023-12-25T18:15:09.630638Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# running LightGBM predictions on chunks of data\n# then concatenating to get predictions for the whole data\n\nchunk_size = math.ceil(LENGTH / DATA_CHUNKS)\nsub = pd.DataFrame()\n\nfor i in tqdm(range(DATA_CHUNKS)):\n    data = DataManager(loaded_file, offset=i * chunk_size, length=chunk_size)\n    test = data.process_data()\n    current_sub = data.select_columns_for_sub()\n    model = Model()\n    model.make_predictions(test)\n    current_sub = model.add_preds_to_submission(current_sub)\n    sub = pd.concat([sub, current_sub])","metadata":{"scrolled":true,"execution":{"iopub.status.busy":"2023-12-25T18:15:09.633428Z","iopub.execute_input":"2023-12-25T18:15:09.633998Z","iopub.status.idle":"2023-12-25T18:17:57.738203Z","shell.execute_reply.started":"2023-12-25T18:15:09.633966Z","shell.execute_reply":"2023-12-25T18:17:57.737426Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"sub.head()","metadata":{"execution":{"iopub.status.busy":"2023-12-25T18:17:57.739476Z","iopub.execute_input":"2023-12-25T18:17:57.739954Z","iopub.status.idle":"2023-12-25T18:17:57.756113Z","shell.execute_reply.started":"2023-12-25T18:17:57.739926Z","shell.execute_reply":"2023-12-25T18:17:57.754886Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Postprocessing and submission","metadata":{"papermill":{"duration":0.01237,"end_time":"2023-10-14T12:12:41.443963","exception":false,"start_time":"2023-10-14T12:12:41.431593","status":"completed"},"tags":[]}},{"cell_type":"code","source":"sub.loc[sub[\"event\"] == \"onset\", \"score\"] = sub[\"onset_prob\"]\nsub.loc[sub[\"event\"] != \"onset\", \"score\"] = sub[\"wakeup_prob\"]","metadata":{"papermill":{"duration":0.02464,"end_time":"2023-10-14T12:12:41.713091","exception":false,"start_time":"2023-10-14T12:12:41.688451","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:17:57.757387Z","iopub.execute_input":"2023-12-25T18:17:57.757717Z","iopub.status.idle":"2023-12-25T18:17:57.767968Z","shell.execute_reply.started":"2023-12-25T18:17:57.757688Z","shell.execute_reply":"2023-12-25T18:17:57.766752Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# leaving out all unprobable events\n# sub = sub[sub[\"score\"] > 0.3]","metadata":{"papermill":{"duration":0.022484,"end_time":"2023-10-14T12:12:41.747730","exception":false,"start_time":"2023-10-14T12:12:41.725246","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:17:57.769269Z","iopub.execute_input":"2023-12-25T18:17:57.769581Z","iopub.status.idle":"2023-12-25T18:17:57.778998Z","shell.execute_reply.started":"2023-12-25T18:17:57.769555Z","shell.execute_reply":"2023-12-25T18:17:57.778084Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# appropriate format for submission\nsub.sort_values([\"series_id\",\"step\"], ascending=[0,1], inplace=True)\nsub.reset_index(drop=True, inplace=True)\nsub[\"row_id\"] = sub.index\nsub = sub[[\"row_id\", \"series_id\", \"step\", \"event\",  \"score\"]]\n\n# just in case setting right submission formats\nsub.dropna(subset=\"row_id\")\n# sub.loc[:, \"series_id\"] = sub[\"series_id\"]\nsub.loc[:, \"step\"] = sub[\"step\"].fillna(0).astype(\"uint32\")\nsub.loc[:, \"event\"] = sub[\"event\"].fillna(\"onset\").astype(\"object\")\nsub.loc[:, \"score\"] = sub[\"score\"].fillna(0).astype(\"float64\")","metadata":{"papermill":{"duration":0.027781,"end_time":"2023-10-14T12:12:41.786385","exception":false,"start_time":"2023-10-14T12:12:41.758604","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:17:57.782328Z","iopub.execute_input":"2023-12-25T18:17:57.783280Z","iopub.status.idle":"2023-12-25T18:17:57.800807Z","shell.execute_reply.started":"2023-12-25T18:17:57.783240Z","shell.execute_reply":"2023-12-25T18:17:57.799705Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"sub.to_csv(\"submission.csv\", index=False)","metadata":{"papermill":{"duration":0.022521,"end_time":"2023-10-14T12:12:41.819800","exception":false,"start_time":"2023-10-14T12:12:41.797279","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:17:57.802524Z","iopub.execute_input":"2023-12-25T18:17:57.802948Z","iopub.status.idle":"2023-12-25T18:17:57.813860Z","shell.execute_reply.started":"2023-12-25T18:17:57.802911Z","shell.execute_reply":"2023-12-25T18:17:57.812692Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"sub.head(50)","metadata":{"papermill":{"duration":0.024593,"end_time":"2023-10-14T12:12:41.854763","exception":false,"start_time":"2023-10-14T12:12:41.830170","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:17:57.815743Z","iopub.execute_input":"2023-12-25T18:17:57.816211Z","iopub.status.idle":"2023-12-25T18:17:57.833911Z","shell.execute_reply.started":"2023-12-25T18:17:57.816173Z","shell.execute_reply":"2023-12-25T18:17:57.832863Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"lgb.plot_importance(gbm, figsize=(15, 15));","metadata":{"papermill":{"duration":0.818439,"end_time":"2023-10-14T12:12:42.683326","exception":false,"start_time":"2023-10-14T12:12:41.864887","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:17:57.835639Z","iopub.execute_input":"2023-12-25T18:17:57.835992Z","iopub.status.idle":"2023-12-25T18:17:58.685709Z","shell.execute_reply.started":"2023-12-25T18:17:57.835963Z","shell.execute_reply":"2023-12-25T18:17:58.684570Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# plotting predictions vs labels\n\nif DEV: \n    for ID in sub.series_id.unique():\n        train = (\n            pl.scan_parquet(\n                MODEL_PATH + \"train_series.parquet\",\n            )\n            .filter(pl.col(\"series_id\").is_in([series_id_map[ID]]))\n        )\n        events = (\n            pl.scan_parquet(\n                MODEL_PATH + \"train_events.parquet\"\n            )\n        )\n        train = train.join(events, how=\"left\", on=[\"series_id\", \"step\"]).collect().to_pandas()\n\n        preds = sub[(sub.series_id == ID)].step\n        real = train[(train.series_id == series_id_map[ID]) & (~train.event.isna())].step\n\n        plt.figure(figsize=(15,5))\n\n        plt.plot(train.step, train.enmo)\n        plt.title(f'series_id = {ID}')\n        plt.vlines(preds, 0, 4, color=\"red\", alpha=sub[sub.series_id == ID].score.values)\n        plt.vlines(0, 0, 0, color=\"red\", label='predictions')\n        plt.vlines(real, 0, 4, color=\"green\", linestyle=\":\", label='true labels')\n        plt.legend()\n        plt.show()","metadata":{"papermill":{"duration":0.023377,"end_time":"2023-10-14T12:12:42.719669","exception":false,"start_time":"2023-10-14T12:12:42.696292","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2023-12-25T18:17:58.687378Z","iopub.execute_input":"2023-12-25T18:17:58.691154Z","iopub.status.idle":"2023-12-25T18:19:22.764983Z","shell.execute_reply.started":"2023-12-25T18:17:58.691106Z","shell.execute_reply":"2023-12-25T18:19:22.763840Z"},"trusted":true},"execution_count":null,"outputs":[]}]}