{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":87331,"databundleVersionId":10191418,"sourceType":"competition"}],"dockerImageVersionId":30919,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Imports & Configuration","metadata":{}},{"cell_type":"code","source":"import gc\nimport os\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport random\nimport torch\nimport torchaudio\n\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import accuracy_score, f1_score\nfrom transformers import (\n    ASTFeatureExtractor,\n    ASTForAudioClassification,\n    TrainingArguments,\n    Trainer,\n    set_seed,\n)","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:06.975480Z","iopub.execute_input":"2025-04-24T16:53:06.975725Z","iopub.status.idle":"2025-04-24T16:53:30.212157Z","shell.execute_reply.started":"2025-04-24T16:53:06.975699Z","shell.execute_reply":"2025-04-24T16:53:30.211451Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Hyperparameter Sweep on W&B: https://wandb.ai/ncduy0303/comp562-cough-sound-classification/sweeps/mtu964xg\n# Current best run id: electric-sweep-47 (we will round some of the found hyperparameters for easier reading)\n\nCONFIG = {\n    \"seed\": 42,\n\n    \"hf_model_name\": \"MIT/ast-finetuned-audioset-10-10-0.4593\",\n\n    \"num_labels\": 3,\n    \"sample_rate\": 16000,   # Should match the pretrained model's sample rate\n    \"num_mel_bins\": 128,    # Frequency dimension of the spectrograms\n    \"max_length\": 1024,     # Temporal dimension of the spectrograms\n\n    \"do_random_crop\": False,                 # Whether to random crop before truncation\n    \"time_shift_pct\": 0.2406222097862671,    # Fraction of the audio length to shift\n    \"add_noise\": False,                      # Whether to add noise\n    \"noise_std\": 3.635836060248842e-05,      # Standard deviation of the noise\n    \"freq_mask_frac\": 0.17216293333926824,   # Fraction of frequency bins to mask\n    \"time_mask_frac\": 0.07754007415493458,   # Number of time steps to mask\n\n    \"mixup_alpha\": 0.15724932986585213,      # Mixup alpha parameter\n    \"mixup_rate\" : 1,                        # Probability of applying mixup\n\n    \"data_dir\": \"/kaggle/input/airs-ai-in-respiratory-sounds\",\n\n    # TrainingArguments\n    \"num_epochs\": 10,\n    \"batch_size\": 16,\n    \"lr_scheduler_type\": \"linear\",\n    \"warmup_ratio\": 0.0,\n    \"learning_rate\": 2.163877478133885e-05,\n    \"weight_decay\": 0.0008463629935799428,\n    \"class_weights\": [1, 1, 1],\n    \"label_smoothing_factor\": 0.1,\n    \"fp16\": False,\n\n    # k-fold\n    \"n_splits\": 5,\n}\n\nCONFIG[\"freq_mask_param\"] = int(CONFIG[\"num_mel_bins\"] * CONFIG[\"freq_mask_frac\"])\nCONFIG[\"time_mask_param\"] = int(CONFIG[\"max_length\"] * CONFIG[\"time_mask_frac\"])\n\n# Set random seed for reproducibility\nset_seed(CONFIG[\"seed\"])","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:30.212997Z","iopub.execute_input":"2025-04-24T16:53:30.213503Z","iopub.status.idle":"2025-04-24T16:53:30.224629Z","shell.execute_reply.started":"2025-04-24T16:53:30.213468Z","shell.execute_reply":"2025-04-24T16:53:30.223821Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data","metadata":{}},{"cell_type":"markdown","source":"### Getting the Data","metadata":{}},{"cell_type":"code","source":"DATA_DIR = CONFIG[\"data_dir\"]\nAUDIO_DIR = os.path.join(DATA_DIR, \"sounds\", \"sounds\")\nTRAIN_CSV_PATH = os.path.join(DATA_DIR, \"train.csv\")\nTEST_CSV_PATH = os.path.join(DATA_DIR, \"test.csv\")","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:30.225533Z","iopub.execute_input":"2025-04-24T16:53:30.225831Z","iopub.status.idle":"2025-04-24T16:53:30.269220Z","shell.execute_reply.started":"2025-04-24T16:53:30.225798Z","shell.execute_reply":"2025-04-24T16:53:30.268395Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train = pd.read_csv(TRAIN_CSV_PATH)\ndf_test  = pd.read_csv(TEST_CSV_PATH)\n\n# Remove the corrupted patient from the training set\ndf_train = df_train[df_train[\"candidateID\"] != \"5ee582f2832c2\"]","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:30.269932Z","iopub.execute_input":"2025-04-24T16:53:30.270135Z","iopub.status.idle":"2025-04-24T16:53:30.308742Z","shell.execute_reply.started":"2025-04-24T16:53:30.270116Z","shell.execute_reply":"2025-04-24T16:53:30.308162Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Dataset Class","metadata":{}},{"cell_type":"code","source":"class CoughASTDataset(Dataset):\n    def __init__(\n        self,\n        df: pd.DataFrame,\n        audio_dir: str,\n        feature_extractor: ASTFeatureExtractor,\n        sample_rate: int = 16000,\n        training: bool = True,\n        # ── waveform‑domain aug ────────────────────────────────\n        do_random_crop: bool = False,\n        time_shift_pct: float = 0.3,\n        add_noise: bool = False,\n        noise_std: float = 1e-3,\n        # ── spec‑domain aug (SpecAugment) ─────────────────────\n        freq_mask_param: int = 48,\n        time_mask_param: int = 192,\n    ):\n        \"\"\"\n        Parameters\n        ----------\n        df : DataFrame      expects a 'candidateID' column and a 'disease' int label.\n        audio_dir : str     root that contains <candidateID>/cough.wav\n        feature_extractor   HF ASTFeatureExtractor (fbank, normalization, padding/truncation)\n        sample_rate : int   MUST match the extractor & model (16 kHz for MIT/ast‑*)\n        training : bool     switch to `False` for val/test ⇒ no augmentation\n        \"\"\"\n        self.df                = df.reset_index(drop=True)\n        self.audio_dir         = audio_dir\n        self.feat_extractor    = feature_extractor\n        self.sr                = sample_rate\n        self.training          = training\n\n        # ─ waveform‑level augmenters ─\n        self._do_random_crop   = do_random_crop\n        self._num_samples      = self.feat_extractor.max_length * 160\n        self._time_shift_pct   = time_shift_pct\n        self._add_noise        = add_noise\n        self._noise_std        = noise_std\n\n        # ─ SpecAugment blocks (instantiated once, faster than per‑item) ─\n        self._freq_mask = torchaudio.transforms.FrequencyMasking(freq_mask_param)\n        self._time_mask = torchaudio.transforms.TimeMasking(time_mask_param)\n\n        # one‑off resampler if files aren’t already 16 kHz\n        self._resampler   = torchaudio.transforms.Resample(orig_freq=self.sr, new_freq=self.sr)\n\n    # --------------------------------------------------------\n    # helpers\n    # --------------------------------------------------------\n    def _random_crop(self, wav: torch.Tensor) -> torch.Tensor:\n        \"\"\"Randomly crop a fixed-length segment from the waveform.\"\"\"\n        num_samples = self._num_samples\n        total_len   = wav.shape[-1]\n\n        if total_len <= num_samples:\n            return wav\n        else:\n            start = random.randint(0, total_len - num_samples)\n            return wav[..., start:start + num_samples]\n    \n    def _random_time_shift(self, wav: torch.Tensor) -> torch.Tensor:\n        \"\"\"Roll the waveform left / right by up to `time_shift_pct`.\"\"\"\n        max_shift = int(wav.shape[-1] * self._time_shift_pct)\n        shift     = random.randint(-max_shift, max_shift)\n        return torch.roll(wav, shifts=shift, dims=-1)\n\n    # --------------------------------------------------------\n    # core\n    # --------------------------------------------------------\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx: int):\n        cid   = self.df.loc[idx, \"candidateID\"]\n        label = self.df.loc[idx, \"disease\"] if \"disease\" in self.df.columns else None\n\n        # ────────── load waveform ──────────\n        wav_path          = os.path.join(self.audio_dir, cid, \"cough.wav\")\n        waveform, orig_sr = torchaudio.load(wav_path)\n\n        # mono + resample\n        if waveform.shape[0] > 1:\n            waveform = waveform.mean(dim=0, keepdim=True)\n        if orig_sr != self.sr:\n            waveform = self._resampler(waveform)\n\n        # ────────── waveform‑domain aug ──────────\n        if self.training:\n            # Crop a random chunk of fixed length\n            if self._do_random_crop:\n                waveform = self._random_crop(waveform)\n            # Apply small time shift to that chunk\n            waveform = self._random_time_shift(waveform)\n            # additive noise\n            if self._add_noise:\n                waveform += torch.randn_like(waveform) * self._noise_std\n\n        # ────────── extract log‑Mel patches with HF helper ──────────\n        feats = self.feat_extractor(\n            waveform.squeeze().numpy(),\n            sampling_rate=self.sr,\n            return_tensors=\"pt\",\n        )[\"input_values\"].squeeze(0)       # tensor [frames, mel]\n\n        # ────────── SpecAugment (Freq & Time masking) ──────────\n        if self.training:\n            feats = feats.transpose(0, 1)  # torchaudio maskers expect [mel, frames]\n            feats = feats.unsqueeze(0)     # torchaudio maskers expect 3‑D\n            feats = self._freq_mask(feats)\n            feats = self._time_mask(feats)\n            feats = feats.squeeze(0)\n            feats = feats.transpose(0, 1)\n\n        return {\n            \"input_values\": feats,         # [frames, mel]\n            \"label\":        torch.tensor(label, dtype=torch.long) if label is not None else None,\n        }","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:30.309517Z","iopub.execute_input":"2025-04-24T16:53:30.309777Z","iopub.status.idle":"2025-04-24T16:53:30.321359Z","shell.execute_reply.started":"2025-04-24T16:53:30.309735Z","shell.execute_reply":"2025-04-24T16:53:30.320605Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Creating Dataset","metadata":{}},{"cell_type":"code","source":"# Load the feature extractor\nfeature_extractor = ASTFeatureExtractor.from_pretrained(\n    CONFIG['hf_model_name'], \n    max_length=CONFIG['max_length'],\n)\nfeature_extractor","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:30.323984Z","iopub.execute_input":"2025-04-24T16:53:30.324188Z","iopub.status.idle":"2025-04-24T16:53:30.557863Z","shell.execute_reply.started":"2025-04-24T16:53:30.324159Z","shell.execute_reply":"2025-04-24T16:53:30.557084Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualize the data augmentation for sanity check\nfull_ds = CoughASTDataset(\n    df_train, AUDIO_DIR, feature_extractor, training=True,\n    sample_rate=CONFIG[\"sample_rate\"],\n    do_random_crop=CONFIG[\"do_random_crop\"],\n    time_shift_pct=CONFIG[\"time_shift_pct\"],\n    add_noise=CONFIG[\"add_noise\"],\n    noise_std=CONFIG[\"noise_std\"],\n    freq_mask_param=CONFIG[\"freq_mask_param\"],\n    time_mask_param=CONFIG[\"time_mask_param\"],\n)\n\nidx = random.randint(0, len(full_ds) - 1)\nsample = full_ds[idx]\nprint(sample)\nprint(sample[\"input_values\"].shape) # (time_frames, mel_bins)\n\n# Plot the specialtrogram\nplt.figure(figsize=(16, 8))\nplt.suptitle(f\"Spectrograms for train index: {idx}\", fontsize=18)\nfor i in range(4):\n    sample = full_ds[idx]  # Each call applies augmentation if training=True\n    plt.subplot(2, 2, i + 1)\n    plt.imshow(sample[\"input_values\"].numpy().T, origin=\"lower\", aspect=\"auto\")\n    plt.title(f\"Augmentation #{i+1}\")\n    plt.xlabel(\"Time Frames\")\n    plt.ylabel(\"Mel-Frequency Bins\")\n    plt.colorbar(label=\"Normalized Amplitude\")\nplt.tight_layout(rect=[0, 0, 1, 0.96])\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:30.561517Z","iopub.execute_input":"2025-04-24T16:53:30.561811Z","iopub.status.idle":"2025-04-24T16:53:32.497463Z","shell.execute_reply.started":"2025-04-24T16:53:30.561779Z","shell.execute_reply":"2025-04-24T16:53:32.496272Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"code","source":"# Vision Transformer Model\nmodel = ASTForAudioClassification.from_pretrained(\n    CONFIG['hf_model_name'],\n    num_labels=CONFIG['num_labels'],\n    num_mel_bins=CONFIG['num_mel_bins'],\n    max_length=CONFIG['max_length'],\n    ignore_mismatched_sizes=True\n)\nmodel","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:32.498416Z","iopub.execute_input":"2025-04-24T16:53:32.498641Z","iopub.status.idle":"2025-04-24T16:53:35.105439Z","shell.execute_reply.started":"2025-04-24T16:53:32.498622Z","shell.execute_reply":"2025-04-24T16:53:35.104784Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Use accuracy and macro F1 score as metrics\ndef compute_metrics(eval_pred):\n    logits, labels = eval_pred\n    predictions = np.argmax(logits, axis=-1)\n    return {\n        \"accuracy\": accuracy_score(labels, predictions),\n        \"f1_macro\": f1_score(labels, predictions, average='macro')\n    }","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:35.106224Z","iopub.execute_input":"2025-04-24T16:53:35.106554Z","iopub.status.idle":"2025-04-24T16:53:35.110863Z","shell.execute_reply.started":"2025-04-24T16:53:35.106522Z","shell.execute_reply":"2025-04-24T16:53:35.110026Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MixupTrainer(Trainer):\n    \"\"\"\n    Implements mixup inside `compute_loss`.\n\n    Parameters\n    ----------\n    weights : list of float\n              class weights for the loss function.\n    mixup_alpha : float (α in the Beta(α, α) distribution);\n                   0 ⇒ no mixup. Default = 10 same as the original AST paper.\n    mixup_rate  : float (probability of applying mixup);\n                   0 ⇒ no mixup. Default = 0.5 same as the original AST paper.\n    \"\"\"\n    def __init__(self, *args, weights: list = None,\n                 mixup_alpha: float = 10, mixup_rate: float = 0.5, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.mixup_alpha = mixup_alpha\n        self.mixup_rate = mixup_rate\n        self.loss_fn = torch.nn.CrossEntropyLoss(label_smoothing=self.args.label_smoothing_factor)\n        if weights is not None:\n            self.loss_fn = torch.nn.CrossEntropyLoss(\n                weight=torch.tensor(weights).float().to(self.args.device),\n                label_smoothing=self.args.label_smoothing_factor\n            )\n\n    # --------------------------------------------------------\n    # override loss computation\n    # --------------------------------------------------------\n    def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None):\n        labels = inputs.pop(\"labels\")                     # [B]\n        input_values = inputs[\"input_values\"]             # [B, …]\n\n        if model.training and self.mixup_alpha > 0.0 and random.random() < self.mixup_rate:\n            lam    = np.random.beta(self.mixup_alpha, self.mixup_alpha)\n            batch  = labels.size(0)\n            perm   = torch.randperm(batch).to(labels.device)\n\n            # mix the inputs\n            mixed_inputs = lam * input_values + (1.0 - lam) * input_values[perm]\n\n            # convert labels → one‑hot → mix\n            num_classes  = model.config.num_labels\n            one_hot      = torch.nn.functional.one_hot(labels, num_classes).float()\n            one_hot_perm = torch.nn.functional.one_hot(labels[perm], num_classes).float()\n            mixed_labels = lam * one_hot + (1.0 - lam) * one_hot_perm\n\n            outputs = model(**{\"input_values\": mixed_inputs})\n            loss    = self.loss_fn(outputs.logits, mixed_labels)\n\n        else:\n            outputs = model(**inputs)\n            loss    = self.loss_fn(outputs.logits, labels)\n\n        return (loss, outputs) if return_outputs else loss","metadata":{"execution":{"iopub.status.busy":"2025-04-24T16:53:35.111704Z","iopub.execute_input":"2025-04-24T16:53:35.111950Z","iopub.status.idle":"2025-04-24T16:53:35.126859Z","shell.execute_reply.started":"2025-04-24T16:53:35.111929Z","shell.execute_reply":"2025-04-24T16:53:35.126225Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training & Inference\n\nUsing 5-fold cross validating and ensembling to make inference on test set.","metadata":{}},{"cell_type":"code","source":"# Load the test dataset\ntest_ds = CoughASTDataset(\n    df_test, AUDIO_DIR, feature_extractor, training=False,\n    sample_rate=CONFIG[\"sample_rate\"],\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-24T16:53:35.127669Z","iopub.execute_input":"2025-04-24T16:53:35.127956Z","iopub.status.idle":"2025-04-24T16:53:35.144206Z","shell.execute_reply.started":"2025-04-24T16:53:35.127925Z","shell.execute_reply":"2025-04-24T16:53:35.143396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"skf = StratifiedKFold(n_splits=CONFIG[\"n_splits\"], shuffle=True, random_state=CONFIG[\"seed\"])\nall_test_logits = []\nval_metrics = []\n\nfor fold, (train_idx, val_idx) in enumerate(skf.split(df_train, df_train[\"disease\"])):\n    print(f\"\\n===== Fold {fold+1} =====\")\n    \n    # Split fold data\n    train_fold = df_train.iloc[train_idx].reset_index(drop=True)\n    val_fold   = df_train.iloc[val_idx].reset_index(drop=True)\n    \n    # Build datasets\n    train_ds = CoughASTDataset(\n        train_fold, AUDIO_DIR, feature_extractor, training=True,\n        sample_rate=CONFIG[\"sample_rate\"],\n        do_random_crop=CONFIG[\"do_random_crop\"],\n        time_shift_pct=CONFIG[\"time_shift_pct\"],\n        add_noise=CONFIG[\"add_noise\"],\n        noise_std=CONFIG[\"noise_std\"],\n        freq_mask_param=CONFIG[\"freq_mask_param\"],\n        time_mask_param=CONFIG[\"time_mask_param\"],\n    )\n    val_ds = CoughASTDataset(\n        val_fold, AUDIO_DIR, feature_extractor, training=False,\n        sample_rate=CONFIG[\"sample_rate\"],\n    )\n    \n    # TrainingArguments\n    training_args = TrainingArguments(\n        output_dir=f\"./cv_fold_{fold}\",\n        eval_strategy=\"epoch\",\n        save_strategy=\"epoch\",\n        lr_scheduler_type=CONFIG[\"lr_scheduler_type\"],\n        warmup_ratio=CONFIG[\"warmup_ratio\"],\n        learning_rate=CONFIG['learning_rate'],\n        per_device_train_batch_size=CONFIG['batch_size'],\n        per_device_eval_batch_size=CONFIG['batch_size'],\n        num_train_epochs=CONFIG['num_epochs'],\n        weight_decay=CONFIG[\"weight_decay\"],\n        label_smoothing_factor=CONFIG[\"label_smoothing_factor\"],\n        logging_dir='./logs',\n        logging_steps=10,\n        load_best_model_at_end=True,\n        metric_for_best_model=\"f1_macro\",\n        remove_unused_columns=True,\n        save_total_limit=1,\n        fp16=CONFIG[\"fp16\"],\n        report_to=\"none\",\n    )\n    \n    # Model & Trainer\n    model = ASTForAudioClassification.from_pretrained(\n        CONFIG[\"hf_model_name\"],\n        num_labels=CONFIG[\"num_labels\"],\n        num_mel_bins=CONFIG[\"num_mel_bins\"],\n        max_length=CONFIG[\"max_length\"],\n        ignore_mismatched_sizes=True,\n    )\n    \n    trainer = MixupTrainer(\n        model=model,\n        args=training_args,\n        train_dataset=train_ds,\n        eval_dataset=val_ds,\n        compute_metrics=compute_metrics,\n        weights=CONFIG[\"class_weights\"],\n        mixup_alpha=CONFIG[\"mixup_alpha\"],\n        mixup_rate=CONFIG[\"mixup_rate\"],\n    )\n    \n    # Train & Validate\n    trainer.train()\n    metrics = trainer.evaluate()\n    print(f\"Fold {fold+1} validation metrics:\", metrics)\n    val_metrics.append(metrics)\n\n    # Inference on test set\n    test_out = trainer.predict(test_ds)\n    all_test_logits.append(test_out.predictions)\n    \n    # Cleanup\n    del trainer, model, train_ds, val_ds\n    gc.collect()\n    torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-24T16:53:35.144941Z","iopub.execute_input":"2025-04-24T16:53:35.145127Z","iopub.status.idle":"2025-04-24T17:04:51.441332Z","shell.execute_reply.started":"2025-04-24T16:53:35.145109Z","shell.execute_reply":"2025-04-24T17:04:51.440404Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Average validation metrics\navg_val = {k: np.mean([m[k] for m in val_metrics]) for k in val_metrics[0]}\nprint(\"\\nAverage validation across folds:\")\nfor k, v in avg_val.items():\n    print(f\"{k}: {v:.4f}\")","metadata":{"execution":{"iopub.status.busy":"2025-04-24T17:04:51.442428Z","iopub.execute_input":"2025-04-24T17:04:51.442748Z","iopub.status.idle":"2025-04-24T17:04:51.448396Z","shell.execute_reply.started":"2025-04-24T17:04:51.442718Z","shell.execute_reply":"2025-04-24T17:04:51.447729Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create submission file\navg_logits = np.mean(np.stack(all_test_logits, axis=0), axis=0)\npredictions = np.argmax(avg_logits, axis=1)\nsubmission_df = pd.DataFrame({\n    'candidateID': df_test['candidateID'],\n    'disease': predictions\n})\nsubmission_df.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-24T17:04:51.449116Z","iopub.execute_input":"2025-04-24T17:04:51.449327Z","iopub.status.idle":"2025-04-24T17:04:51.471818Z","shell.execute_reply.started":"2025-04-24T17:04:51.449306Z","shell.execute_reply":"2025-04-24T17:04:51.471128Z"}},"outputs":[],"execution_count":null}]}