{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"datasetVersion","sourceId":11154087,"datasetId":6925704,"databundleVersionId":11552441}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, optimizers\nfrom tensorflow.keras.applications import EfficientNetB0\nfrom sklearn.metrics import confusion_matrix, classification_report\n\n# =========================================================\n# 1. PRÉPARATION DES DONNÉES (80/10/10)\n# =========================================================\nBASE_PATH = \"/kaggle/input/\"\nDATA_DIR = None\nfor root, dirs, files in os.walk(BASE_PATH):\n    if any(d in ['Stroke', 'Normal', 'Ischemia', 'Bleeding'] for d in dirs):\n        DATA_DIR = root\n        break\n\nIMG_SIZE = 224\nBATCH_SIZE = 32\n\ntrain_ds = tf.keras.utils.image_dataset_from_directory(\n    DATA_DIR, validation_split=0.2, subset=\"training\", seed=123,\n    image_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE, label_mode='categorical'\n)\n\ntemp_val_ds = tf.keras.utils.image_dataset_from_directory(\n    DATA_DIR, validation_split=0.2, subset=\"validation\", seed=123,\n    image_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE, label_mode='categorical'\n)\n\nval_batches = tf.data.experimental.cardinality(temp_val_ds)\ntest_ds = temp_val_ds.take(val_batches // 2)\nval_ds = temp_val_ds.skip(val_batches // 2)\n\nclass_names = train_ds.class_names\nnum_classes = len(class_names)\n\nAUTOTUNE = tf.data.AUTOTUNE\ntrain_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)\nval_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)\ntest_ds = test_ds.cache().prefetch(buffer_size=AUTOTUNE)\n\n# =========================================================\n# 2. MÉTRIQUES ET ARCHITECTURE\n# =========================================================\ndef dice_coefficient(y_true, y_pred):\n    y_true = tf.cast(y_true, tf.float32)\n    y_pred = tf.one_hot(tf.argmax(y_pred, axis=-1), num_classes)\n    intersection = tf.reduce_sum(y_true * y_pred)\n    return (2. * intersection + 1.) / (tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) + 1.)\n\nbase_model = EfficientNetB0(weights='imagenet', include_top=False, input_shape=(IMG_SIZE, IMG_SIZE, 3))\nbase_model.trainable = False \n\nmodel = models.Sequential([\n    base_model,\n    layers.GlobalAveragePooling2D(),\n    layers.BatchNormalization(),\n    layers.Dropout(0.4),\n    layers.Dense(256, activation='relu'),\n    layers.Dense(num_classes, activation='softmax')\n])\n\nmodel.compile(\n    optimizer=optimizers.Adam(learning_rate=1e-4),\n    loss='categorical_crossentropy',\n    metrics=[\n        'accuracy', \n        dice_coefficient, \n        tf.keras.metrics.Precision(name='precision'), \n        tf.keras.metrics.Recall(name='recall')\n    ]\n)\n\n# =========================================================\n# 3. ENTRAÎNEMENT\n# =========================================================\nprint(\"\\n🚀 Lancement de l'entraînement...\")\nhistory = model.fit(\n    train_ds, \n    validation_data=val_ds, \n    epochs=50, \n    callbacks=[tf.keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True)],\n    verbose=1\n)\n\n# =========================================================\n# 4. ÉVALUATION ET COURBES DE PERFORMANCE\n# =========================================================\nprint(\"\\n📈 Génération des graphiques...\")\nres_test = model.evaluate(test_ds, verbose=0)\ndice_test_val = res_test[2]\n\nplt.figure(figsize=(20, 6))\n\nplt.subplot(1, 3, 1)\nplt.plot(history.history['accuracy'], label='Train Acc', lw=2)\nplt.plot(history.history['val_accuracy'], label='Val Acc', lw=2)\nplt.title('Précision (Accuracy)')\nplt.legend(); plt.grid(True, alpha=0.3)\n\nplt.subplot(1, 3, 2)\nplt.plot(history.history['dice_coefficient'], label='Train Dice', color='orange', lw=2)\nplt.plot(history.history['val_dice_coefficient'], label='Val Dice', color='red', lw=2)\nplt.axhline(y=dice_test_val, color='black', linestyle='--', label=f'Test Dice ({dice_test_val:.2f})')\nplt.title('Score Dice')\nplt.legend(); plt.grid(True, alpha=0.3)\n\nplt.subplot(1, 3, 3)\nplt.plot(history.history['loss'], label='Train Loss', color='gray')\nplt.plot(history.history['val_loss'], label='Val Loss', color='black')\nplt.title('Perte (Loss)')\nplt.legend(); plt.grid(True, alpha=0.3)\nplt.show()\n\n# =========================================================\n# 5. BILAN CLINIQUE CORRIGÉ (SENSIBILITÉ & SPÉCIFICITÉ)\n# =========================================================\nprint(\"\\n\" + \"=\"*75)\nprint(\"📊 RÉSULTATS DÉTAILLÉS DU MODÈLE (SET DE TEST)\")\nprint(\"=\"*75)\n\ny_true, y_pred = [], []\nfor images, labels in test_ds:\n    p = model.predict(images, verbose=0)\n    y_true.extend(np.argmax(labels.numpy(), axis=1))\n    y_pred.extend(np.argmax(p, axis=1))\n\nprint(\"\\n📜 RAPPORT DE CLASSIFICATION :\")\nprint(classification_report(y_true, y_pred, target_names=class_names))\n\ncm = confusion_matrix(y_true, y_pred)\nprint(\"\\n🔬 MÉTRIQUES MÉDICALES PAR CLASSE :\")\nheader = f\"{'Classe':<15} | {'Sensibilité':<12} | {'Spécificité':<12} | {'Précision':<12}\"\nprint(header)\nprint(\"-\" * len(header))\n\nfor i in range(num_classes):\n    tp = cm[i, i]\n    fn = np.sum(cm[i, :]) - tp\n    fp = np.sum(cm[:, i]) - tp\n    tn = np.sum(cm) - (tp + fp + fn)\n    \n    sens = tp / (tp + fn) if (tp + fn) > 0 else 0\n    spec = tn / (tn + fp) if (tn + fp) > 0 else 0\n    prec = tp / (tp + fp) if (tp + fp) > 0 else 0\n    \n    # Correction du formatage pour éviter ValueError\n    s_str, sp_str, p_str = f\"{sens:.4f}\", f\"{spec:.4f}\", f\"{prec:.4f}\"\n    print(f\"{class_names[i]:<15} | {s_str:<12} | {sp_str:<12} | {p_str:<12}\")\n\nprint(\"-\" * len(header))\n\n# =========================================================\n# 6. GALERIES DE PRÉDICTIONS\n# =========================================================\ndef plot_gallery(dataset, title):\n    images, labels = next(iter(dataset.take(1)))\n    preds = model.predict(images, verbose=0)\n    plt.figure(figsize=(12, 10))\n    plt.suptitle(title, fontsize=16, fontweight='bold')\n    for i in range(min(9, BATCH_SIZE)):\n        plt.subplot(3, 3, i + 1)\n        plt.imshow(images[i].numpy().astype(\"uint8\"))\n        actual, predicted = class_names[np.argmax(labels[i])], class_names[np.argmax(preds[i])]\n        color = 'green' if actual == predicted else 'red'\n        plt.title(f\"R:{actual} | P:{predicted}\\nConf:{np.max(preds[i])*100:.1f}%\", color=color)\n        plt.axis(\"off\")\n    plt.tight_layout(); plt.show()\n\nplot_gallery(val_ds, \"🖼️ PRÉDICTIONS : VALIDATION SET\")\nplot_gallery(test_ds, \"🖼️ PRÉDICTIONS : TEST SET\")\n\nmodel.save('pfe_stroke_efficientnet_final.h5')\nprint(\"\\n✅ Terminé. Modèle sauvegardé avec succès.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:26:57.313029Z","iopub.execute_input":"2026-04-11T17:26:57.313818Z","iopub.status.idle":"2026-04-11T17:34:48.563153Z","shell.execute_reply.started":"2026-04-11T17:26:57.313786Z","shell.execute_reply":"2026-04-11T17:34:48.562342Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, optimizers\nfrom tensorflow.keras.applications import MobileNetV2\nfrom sklearn.metrics import confusion_matrix, classification_report\n\n# =========================================================\n# 1. PRÉPARATION DES DONNÉES (80/10/10)\n# =========================================================\nBASE_PATH = \"/kaggle/input/\"\nDATA_DIR = None\nfor root, dirs, files in os.walk(BASE_PATH):\n    if any(d in ['Stroke', 'Normal', 'Ischemia', 'Bleeding'] for d in dirs):\n        DATA_DIR = root\n        break\n\nIMG_SIZE = 224\nBATCH_SIZE = 32\n\ntrain_ds = tf.keras.utils.image_dataset_from_directory(\n    DATA_DIR, validation_split=0.2, subset=\"training\", seed=123,\n    image_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE, label_mode='categorical'\n)\n\ntemp_val_ds = tf.keras.utils.image_dataset_from_directory(\n    DATA_DIR, validation_split=0.2, subset=\"validation\", seed=123,\n    image_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE, label_mode='categorical'\n)\n\nval_batches = tf.data.experimental.cardinality(temp_val_ds)\ntest_ds = temp_val_ds.take(val_batches // 2)\nval_ds = temp_val_ds.skip(val_batches // 2)\n\nclass_names = train_ds.class_names\nnum_classes = len(class_names)\n\n# Prétraitement spécifique à MobileNetV2\npreprocess_input = tf.keras.applications.mobilenet_v2.preprocess_input\n\nAUTOTUNE = tf.data.AUTOTUNE\ntrain_ds = train_ds.map(lambda x, y: (preprocess_input(x), y)).cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)\nval_ds = val_ds.map(lambda x, y: (preprocess_input(x), y)).cache().prefetch(buffer_size=AUTOTUNE)\ntest_ds = test_ds.map(lambda x, y: (preprocess_input(x), y)).cache().prefetch(buffer_size=AUTOTUNE)\n\n# =========================================================\n# 2. MÉTRIQUES ET ARCHITECTURE MOBILENETV2\n# =========================================================\ndef dice_coefficient(y_true, y_pred):\n    y_true = tf.cast(y_true, tf.float32)\n    y_pred = tf.one_hot(tf.argmax(y_pred, axis=-1), num_classes)\n    intersection = tf.reduce_sum(y_true * y_pred)\n    return (2. * intersection + 1.) / (tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) + 1.)\n\nbase_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(IMG_SIZE, IMG_SIZE, 3))\nbase_model.trainable = False \n\nmodel = models.Sequential([\n    base_model,\n    layers.GlobalAveragePooling2D(),\n    layers.BatchNormalization(),\n    layers.Dropout(0.3),\n    layers.Dense(256, activation='relu'),\n    layers.Dense(num_classes, activation='softmax')\n])\n\nmodel.compile(\n    optimizer=optimizers.Adam(learning_rate=1e-4),\n    loss='categorical_crossentropy',\n    metrics=['accuracy', dice_coefficient]\n)\n\n# =========================================================\n# 3. ENTRAÎNEMENT\n# =========================================================\nprint(\"\\n🚀 Entraînement MobileNetV2 en cours...\")\nhistory = model.fit(\n    train_ds, \n    validation_data=val_ds, \n    epochs=50, \n    callbacks=[tf.keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True)],\n    verbose=1\n)\n\n# =========================================================\n# 4. ÉVALUATION ET COURBES (Accuracy, Dice, Loss)\n# =========================================================\nres_test = model.evaluate(test_ds, verbose=0)\ndice_test_val = res_test[2]\n\nplt.figure(figsize=(20, 6))\n\n# 1. Précision\nplt.subplot(1, 3, 1)\nplt.plot(history.history['accuracy'], label='Train Acc', lw=2)\nplt.plot(history.history['val_accuracy'], label='Val Acc', lw=2)\nplt.title('Précision (Accuracy)')\nplt.legend(); plt.grid(True, alpha=0.3)\n\n# 2. Dice Score\nplt.subplot(1, 3, 2)\nplt.plot(history.history['dice_coefficient'], label='Train Dice', color='orange', lw=2)\nplt.plot(history.history['val_dice_coefficient'], label='Val Dice', color='red', lw=2)\nplt.axhline(y=dice_test_val, color='black', linestyle='--', label=f'Test Dice ({dice_test_val:.2f})')\nplt.title('Score Dice')\nplt.legend(); plt.grid(True, alpha=0.3)\n\n# 3. Loss (La perte)\nplt.subplot(1, 3, 3)\nplt.plot(history.history['loss'], label='Train Loss', color='gray')\nplt.plot(history.history['val_loss'], label='Val Loss', color='black')\nplt.title('Perte (Loss)')\nplt.legend(); plt.grid(True, alpha=0.3)\nplt.show()\n\n# =========================================================\n# 5. BILAN CLINIQUE (Sensibilité & Spécificité)\n# =========================================================\nprint(\"\\n\" + \"=\"*75)\nprint(\"📊 RÉSULTATS DÉTAILLÉS - MOBILENETV2\")\nprint(\"=\"*75)\n\ny_true, y_pred = [], []\nfor images, labels in test_ds:\n    p = model.predict(images, verbose=0)\n    y_true.extend(np.argmax(labels.numpy(), axis=1))\n    y_pred.extend(np.argmax(p, axis=1))\n\nprint(\"\\n📜 RAPPORT DE CLASSIFICATION :\")\nprint(classification_report(y_true, y_pred, target_names=class_names))\n\ncm = confusion_matrix(y_true, y_pred)\nprint(\"\\n🔬 MÉTRIQUES MÉDICALES PAR CLASSE :\")\nheader = f\"{'Classe':<15} | {'Sensibilité':<12} | {'Spécificité':<12}\"\nprint(header)\nprint(\"-\" * len(header))\n\nfor i in range(num_classes):\n    tp = cm[i, i]\n    fn = np.sum(cm[i, :]) - tp\n    fp = np.sum(cm[:, i]) - tp\n    tn = np.sum(cm) - (tp + fp + fn)\n    sens = tp / (tp + fn) if (tp + fn) > 0 else 0\n    spec = tn / (tn + fp) if (tn + fp) > 0 else 0\n    s_str, sp_str = f\"{sens:.4f}\", f\"{spec:.4f}\"\n    print(f\"{class_names[i]:<15} | {s_str:<12} | {sp_str:<12}\")\n\n# =========================================================\n# 6. GALERIES D'IMAGES (VAL ET TEST)\n# =========================================================\ndef plot_gallery(dataset, title):\n    images, labels = next(iter(dataset.take(1)))\n    preds = model.predict(images, verbose=0)\n    plt.figure(figsize=(12, 10))\n    plt.suptitle(title, fontsize=16, fontweight='bold')\n    for i in range(min(9, BATCH_SIZE)):\n        plt.subplot(3, 3, i + 1)\n        # On dé-normalise pour l'affichage (MobileNet -1..1 -> 0..255)\n        img = (images[i].numpy() + 1.0) * 127.5\n        plt.imshow(img.astype(\"uint8\"))\n        actual = class_names[np.argmax(labels[i])]\n        predicted = class_names[np.argmax(preds[i])]\n        color = 'green' if actual == predicted else 'red'\n        plt.title(f\"R:{actual} | P:{predicted}\\nConf:{np.max(preds[i])*100:.1f}%\", color=color)\n        plt.axis(\"off\")\n    plt.tight_layout(); plt.show()\n\nplot_gallery(val_ds, \"🖼️ PRÉDICTIONS : VALIDATION SET\")\nplot_gallery(test_ds, \"🖼️ PRÉDICTIONS : TEST SET\")\n\nmodel.save('pfe_stroke_mobilenet_final.h5')\nprint(\"\\n✅ Terminé. Modèle sauvegardé.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:34:48.564936Z","iopub.execute_input":"2026-04-11T17:34:48.565174Z","iopub.status.idle":"2026-04-11T17:39:22.935108Z","shell.execute_reply.started":"2026-04-11T17:34:48.56515Z","shell.execute_reply":"2026-04-11T17:39:22.934473Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, optimizers\nfrom sklearn.metrics import confusion_matrix, classification_report\n\n# =========================================================\n# 1. PRÉPARATION DES DONNÉES (80/10/10)\n# =========================================================\nBASE_PATH = \"/kaggle/input/\"\nDATA_DIR = None\nfor root, dirs, files in os.walk(BASE_PATH):\n    if any(d in ['Stroke', 'Normal', 'Ischemia', 'Bleeding'] for d in dirs):\n        DATA_DIR = root\n        break\n\nIMG_SIZE = 224\nBATCH_SIZE = 32\n\ntrain_ds_raw = tf.keras.utils.image_dataset_from_directory(\n    DATA_DIR, validation_split=0.2, subset=\"training\", seed=123,\n    image_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE, label_mode='categorical'\n)\n\ntemp_val_ds = tf.keras.utils.image_dataset_from_directory(\n    DATA_DIR, validation_split=0.2, subset=\"validation\", seed=123,\n    image_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE, label_mode='categorical'\n)\n\nval_batches = tf.data.experimental.cardinality(temp_val_ds)\ntest_ds_raw = temp_val_ds.take(val_batches // 2)\nval_ds_raw = temp_val_ds.skip(val_batches // 2)\n\nclass_names = train_ds_raw.class_names\nnum_classes = len(class_names)\n\ndef preprocess_vit(image, label):\n    return tf.cast(image, tf.float32) / 255.0, label\n\nAUTOTUNE = tf.data.AUTOTUNE\ntrain_ds = train_ds_raw.map(preprocess_vit).cache().shuffle(1000).prefetch(AUTOTUNE)\nval_ds = val_ds_raw.map(preprocess_vit).cache().prefetch(AUTOTUNE)\ntest_ds = test_ds_raw.map(preprocess_vit).cache().prefetch(AUTOTUNE)\n\n# =========================================================\n# 2. MODÈLE VISION TRANSFORMER (VERSION STABLE)\n# =========================================================\ndef dice_coefficient(y_true, y_pred):\n    y_true = tf.cast(y_true, tf.float32)\n    y_pred = tf.one_hot(tf.argmax(y_pred, axis=-1), num_classes)\n    intersection = tf.reduce_sum(y_true * y_pred)\n    return (2. * intersection + 1.) / (tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) + 1.)\n\n# Utilisation d'une architecture robuste compatible Keras 3\n# On utilise ConvNeXt-Tiny (Architecture SOTA \"Transformer-like\" intégrée nativement)\nfrom tensorflow.keras.applications import ConvNeXtTiny\nbase_model = ConvNeXtTiny(weights='imagenet', include_top=False, input_shape=(IMG_SIZE, IMG_SIZE, 3))\nbase_model.trainable = False \n\nmodel = models.Sequential([\n    base_model,\n    layers.GlobalAveragePooling2D(),\n    layers.BatchNormalization(),\n    layers.Dense(256, activation='relu'),\n    layers.Dropout(0.3),\n    layers.Dense(num_classes, activation='softmax')\n])\n\nmodel.compile(\n    optimizer=optimizers.Adam(learning_rate=1e-4),\n    loss='categorical_crossentropy',\n    metrics=['accuracy', dice_coefficient]\n)\n\n# =========================================================\n# 3. ENTRAÎNEMENT\n# =========================================================\nprint(\"\\n🚀 Lancement de l'entraînement...\")\nhistory = model.fit(\n    train_ds, \n    validation_data=val_ds, \n    epochs=50, \n    callbacks=[tf.keras.callbacks.EarlyStopping(patience=6, restore_best_weights=True)],\n    verbose=1\n)\n\n# =========================================================\n# 4. ÉVALUATION ET COURBE DES 3 DICES\n# =========================================================\nres_test = model.evaluate(test_ds, verbose=0)\ntest_dice_final = res_test[2]\n\nplt.figure(figsize=(18, 6))\n\n# Graphique de l'Accuracy\nplt.subplot(1, 2, 1)\nplt.plot(history.history['accuracy'], label='Train Acc', lw=2)\nplt.plot(history.history['val_accuracy'], label='Val Acc', lw=2)\nplt.title('Précision (Accuracy)')\nplt.legend(); plt.grid(True, alpha=0.3)\n\n# Graphique des 3 DICES combinés\nplt.subplot(1, 2, 2)\nplt.plot(history.history['dice_coefficient'], label='Train Dice', color='blue', lw=2)\nplt.plot(history.history['val_dice_coefficient'], label='Val Dice', color='green', lw=2)\nplt.axhline(y=test_dice_final, color='red', linestyle='--', label=f'Test Dice ({test_dice_final:.4f})')\nplt.title('Comparaison des Scores Dice (Train, Val, Test)')\nplt.legend(); plt.grid(True, alpha=0.3)\nplt.show()\n\n# =========================================================\n# 5. GALERIES D'IMAGES (VALIDATION ET TEST)\n# =========================================================\ndef plot_gallery(dataset, title):\n    images, labels = next(iter(dataset.take(1)))\n    preds = model.predict(images, verbose=0)\n    plt.figure(figsize=(12, 10))\n    plt.suptitle(title, fontsize=16, fontweight='bold')\n    for i in range(min(9, BATCH_SIZE)):\n        plt.subplot(3, 3, i + 1)\n        plt.imshow(images[i].numpy()) \n        actual = class_names[np.argmax(labels[i])]\n        predicted = class_names[np.argmax(preds[i])]\n        color = 'green' if actual == predicted else 'red'\n        plt.title(f\"Réel: {actual}\\nPrédit: {predicted}\", color=color)\n        plt.axis(\"off\")\n    plt.tight_layout(); plt.show()\n\nplot_gallery(val_ds, \"🖼️ GALERIE DE VALIDATION\")\nplot_gallery(test_ds, \"🖼️ GALERIE DE TEST\")\n\n# =========================================================\n# 6. BILAN CLINIQUE FINAL\n# =========================================================\ny_true, y_pred = [], []\nfor images, labels in test_ds:\n    p = model.predict(images, verbose=0)\n    y_true.extend(np.argmax(labels.numpy(), axis=1))\n    y_pred.extend(np.argmax(p, axis=1))\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"🔬 MÉTRIQUES CLINIQUES FINALES\")\nprint(\"=\"*50)\nprint(classification_report(y_true, y_pred, target_names=class_names))\n\nmodel.save('pfe_stroke_final_transformer.h5')\nprint(\"\\n✅ Terminé. Modèle sauvegardé.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:58:17.71353Z","iopub.execute_input":"2026-04-11T17:58:17.713813Z","iopub.status.idle":"2026-04-11T18:17:17.462365Z","shell.execute_reply.started":"2026-04-11T17:58:17.71379Z","shell.execute_reply":"2026-04-11T18:17:17.461577Z"}},"outputs":[],"execution_count":null}]}