{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.9","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":24800,"datasetId":1042002,"databundleVersionId":1831594},{"sourceType":"datasetVersion","sourceId":2000191,"datasetId":1196612,"databundleVersionId":2039566},{"sourceType":"datasetVersion","sourceId":1809508,"datasetId":1074034,"databundleVersionId":1846985},{"sourceType":"kernelVersion","sourceId":58015934}],"dockerImageVersionId":30061,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\n#         print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-12T16:25:36.756012Z","iopub.execute_input":"2026-05-12T16:25:36.756549Z","iopub.status.idle":"2026-05-12T16:25:36.76201Z","shell.execute_reply.started":"2026-05-12T16:25:36.756511Z","shell.execute_reply":"2026-05-12T16:25:36.761192Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import MultiLabelBinarizer\nimport timm\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ==========================================\n# 1. LOAD FILE CSV GỐC CỦA VINBIGDATA\n# ==========================================\n# Vẫn phải lấy danh sách nhãn từ file CSV gốc\nCSV_PATH = '/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/train.csv'\n\n# Trỏ vào thư mục chứa file Numpy\nNPY_DIR = '/kaggle/input/xraynumpy/images/train'\n\ndf_original = pd.read_csv(CSV_PATH)\n\n# Gộp nhãn của 15 classes lại\ndf_grouped = df_original.groupby('image_id')['class_name'].apply(lambda x: list(set(x))).reset_index()\n\nmlb = MultiLabelBinarizer()\nlabels_onehot = mlb.fit_transform(df_grouped['class_name'])\nnum_classes = len(mlb.classes_)\ndf_grouped['label_onehot'] = list(labels_onehot)\n\nprint(f\"Tổng số ảnh: {len(df_grouped)} | Tổng số lớp bệnh: {num_classes}\")\n\n# Hàm kiểm tra file .npy có tồn tại không\ndef check_npy_exists(image_id):\n    return os.path.exists(os.path.join(NPY_DIR, f\"{image_id}.npy\"))\n\nprint(\"Đang kiểm tra số lượng file Numpy thực tế...\")\ndf_grouped['npy_exists'] = df_grouped['image_id'].apply(check_npy_exists)\ndf_valid = df_grouped[df_grouped['npy_exists'] == True].reset_index(drop=True)\nprint(f\"Số lượng ảnh Numpy THỰC TẾ dùng để train: {len(df_valid)}\")\n\n# ==========================================\n# 2. TÍNH TRỌNG SỐ (SMOOTH WEIGHTS)\n# ==========================================\nclass_counts = np.sum(list(df_valid['label_onehot']), axis=0)\nraw_weights = (len(df_valid) - class_counts) / (class_counts + 1e-5)\nsmooth_weights = np.clip(np.sqrt(raw_weights), 1.0, 5.0) \npos_weights = torch.tensor(smooth_weights, dtype=torch.float32).to(DEVICE)\n\n# ==========================================\n# 3. DATASET & DATALOADER CHO FILE NUMPY\n# ==========================================\ntrain_transform = transforms.Compose([\n    transforms.Resize((256, 256)), # Set theo kích thước Numpy tác giả đã tạo\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1),\n    transforms.ToTensor(),\n    # Thông số Normalize chuẩn ImageNet (DenseNet pre-trained cần cái này)\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((256, 256)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nclass NumpyXrayDataset(Dataset):\n    def __init__(self, df, image_dir, transform):\n        self.df = df\n        self.image_dir = image_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_path = os.path.join(self.image_dir, f\"{row['image_id']}.npy\")\n        \n        # Load mảng Numpy (Rất nhanh!)\n        image_array = np.load(image_path)\n        \n        # Ảnh numpy này thường là 1 kênh xám (Grayscale), ta convert sang RGB \n        # (DenseNet121 nhận đầu vào RGB)\n        # Sửa lỗi: nhân với 255 nếu ảnh đang ở khoảng [0, 1]\n        if image_array.max() <= 1.0:\n             image_array = (image_array * 255).astype(np.uint8)\n        else:\n             image_array = image_array.astype(np.uint8)\n\n        # Xóa channel dimension nếu nó là (H, W, 1)\n        if len(image_array.shape) == 3 and image_array.shape[2] == 1:\n             image_array = np.squeeze(image_array, axis=-1)\n\n        image_pil = Image.fromarray(image_array).convert(\"RGB\")\n        image = self.transform(image_pil)\n        \n        label = torch.tensor(row['label_onehot'], dtype=torch.float32)\n        return image, label\n\n# Chia train/test\ntrain_df, test_df = train_test_split(df_valid, test_size=0.15, random_state=42)\n\n# Batch size 32 (vì ảnh 256 nhỏ, load .npy nhanh, có thể tăng Batch lên)\ntrain_dataset = NumpyXrayDataset(train_df, NPY_DIR, train_transform)\ntest_dataset = NumpyXrayDataset(test_df, NPY_DIR, val_transform)\n\ntrain_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2)\ntest_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-12T16:25:36.764411Z","iopub.execute_input":"2026-05-12T16:25:36.764739Z","iopub.status.idle":"2026-05-12T16:26:02.227125Z","shell.execute_reply.started":"2026-05-12T16:25:36.764705Z","shell.execute_reply":"2026-05-12T16:26:02.226338Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 📊 4. VISUALIZE CLASS DISTRIBUTION\n# ==========================================\nimport matplotlib.pyplot as plt\n\n# Lấy số lượng ảnh của từng class\nclass_counts = np.sum(list(df_valid['label_onehot']), axis=0)\n\n# Lấy tên class\nclass_names = mlb.classes_\n\n# Tạo DataFrame cho dễ nhìn\ndf_stats = pd.DataFrame({\n    \"Class\": class_names,\n    \"Count\": class_counts\n}).sort_values(by=\"Count\", ascending=False)\n\nprint(df_stats)\n\n# Vẽ biểu đồ\nplt.figure(figsize=(12,6))\nplt.bar(df_stats[\"Class\"], df_stats[\"Count\"])\nplt.xticks(rotation=45, ha='right')\nplt.title(\"Distribution of Diseases in VinBigData Dataset\")\nplt.xlabel(\"Disease Class\")\nplt.ylabel(\"Number of Images\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-12T16:26:02.228309Z","iopub.execute_input":"2026-05-12T16:26:02.228564Z","iopub.status.idle":"2026-05-12T16:26:02.557092Z","shell.execute_reply.started":"2026-05-12T16:26:02.22854Z","shell.execute_reply":"2026-05-12T16:26:02.556258Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torchvision.models as models\n\n# ==========================================\n# 1. KHỞI TẠO MÔ HÌNH DENSENET121 (TỪ TORCHVISION)\n# ==========================================\nprint(\"Khởi tạo mô hình DenseNet121 (Torchvision gốc)...\")\n# Tải mô hình DenseNet121 có sẵn trong PyTorch (Không dùng timm nữa để tránh lỗi)\nmodel_ft = models.densenet121(pretrained=True)\n\n# Thay đổi lớp Classifier cuối cùng để xuất ra số lượng bệnh (14 hoặc 15 lớp)\nnum_ftrs = model_ft.classifier.in_features\nmodel_ft.classifier = nn.Linear(num_ftrs, num_classes)\nmodel_ft = model_ft.to(DEVICE)\n\n# ==========================================\n# 2. THIẾT LẬP LOSS & OPTIMIZER\n# ==========================================\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\noptimizer = optim.AdamW(model_ft.parameters(), lr=2e-4, weight_decay=1e-3)\n# Tăng T_max lên 30 để phù hợp với số Epochs mới\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30, eta_min=1e-6)\n\n# ==========================================\n# 3. THIẾT LẬP EARLY STOPPING & VÒNG LẶP HUẤN LUYỆN\n# ==========================================\nnum_epochs = 30 # Tăng số lượng Epoch lên 30\npatience = 5    # Sẽ dừng nếu sau 5 Epoch mà Validation Loss không giảm\nepochs_no_improve = 0\nbest_val_loss = float('inf')\n\ntrain_losses, val_losses = [], []\n\nprint(\"Bắt đầu huấn luyện...\")\nfor epoch in range(num_epochs):\n    model_ft.train()\n    running_loss = 0.0\n    \n    # Train\n    for images, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs}\"):\n        images, labels = images.to(DEVICE), labels.to(DEVICE)\n        \n        optimizer.zero_grad()\n        outputs = model_ft(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        \n        # Clip Gradient: Chống \"nổ\" gradient\n        torch.nn.utils.clip_grad_norm_(model_ft.parameters(), max_norm=1.0)\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    scheduler.step()\n    epoch_train_loss = running_loss / len(train_dataset)\n    train_losses.append(epoch_train_loss)\n    \n    # Validation\n    model_ft.eval()\n    val_loss = 0.0\n    with torch.no_grad():\n        for images, labels in test_loader:\n            images, labels = images.to(DEVICE), labels.to(DEVICE)\n            outputs = model_ft(images)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * images.size(0)\n            \n    epoch_val_loss = val_loss / len(test_dataset)\n    val_losses.append(epoch_val_loss)\n    \n    current_lr = scheduler.get_last_lr()[0]\n    print(f\"Train Loss: {epoch_train_loss:.4f} | Val Loss: {epoch_val_loss:.4f} | LR: {current_lr:.6f}\")\n    \n    # Cơ chế Early Stopping\n    if epoch_val_loss < best_val_loss:\n        best_val_loss = epoch_val_loss\n        epochs_no_improve = 0\n        # Lưu mô hình khi có Val Loss tốt nhất\n        torch.save(model_ft.state_dict(), '/kaggle/working/best_densenet121_torchvision.pth')\n        print(\"Đã lưu mô hình đạt Val Loss tốt nhất!\")\n    else:\n        epochs_no_improve += 1\n        print(f\" Val Loss không giảm (Patience: {epochs_no_improve}/{patience})\")\n        if epochs_no_improve >= patience:\n            print(\"Kích hoạt Early Stopping! Dừng huấn luyện để chống Overfitting.\")\n            break\n\nprint(\"Hoàn tất quá trình huấn luyện!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-12T16:26:02.559196Z","iopub.execute_input":"2026-05-12T16:26:02.559442Z","iopub.status.idle":"2026-05-12T16:51:50.86748Z","shell.execute_reply.started":"2026-05-12T16:26:02.559419Z","shell.execute_reply":"2026-05-12T16:51:50.866174Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import classification_report, f1_score\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ==========================================\n# 1. VẼ BIỂU ĐỒ LOSS CURVE\n# ==========================================\n# Lấy số epoch thực tế đã chạy\nactual_epochs = len(train_losses)\n\nplt.figure(figsize=(8, 5))\nplt.plot(range(1, actual_epochs+1), train_losses, label='Train Loss', marker='o')\nplt.plot(range(1, actual_epochs+1), val_losses, label='Validation Loss', marker='s')\nplt.title('Biểu đồ Loss: DenseNet121 - VinBigData (Numpy)', fontsize=14)\nplt.xlabel('Epoch')\nplt.ylabel('BCE Loss')\nplt.legend()\nplt.grid(True)\nplt.show()\n\n# ==========================================\n# 2. LẤY PREDICTIONS TỪ MÔ HÌNH TỐT NHẤT\n# ==========================================\n# Phải load lại cái model tốt nhất vừa được lưu để test\nmodel_ft.load_state_dict(torch.load('/kaggle/working/best_densenet121_torchvision.pth'))\nmodel_ft.eval()\nall_probs, all_labels = [], []\n\nprint(\"Đang chạy suy luận trên tập Test...\")\nwith torch.no_grad():\n    for images, labels in tqdm(test_loader):\n        images = images.to(DEVICE)\n        labels = labels.cpu().numpy()\n        outputs = model_ft(images)\n        probs = torch.sigmoid(outputs).cpu().numpy() \n        \n        all_probs.extend(probs)\n        all_labels.extend(labels)\n\nall_probs = np.array(all_probs)\nall_labels = np.array(all_labels)\n\n# ==========================================\n# 3. AUTO-THRESHOLDING CHO TỪNG BỆNH\n# ==========================================\nbest_thresholds = []\nprint(\"\\nTÌM NGƯỠNG TỐI ƯU (THRESHOLD) CỨU CÁC BỆNH HIẾM:\")\nfor i, class_name in enumerate(mlb.classes_):\n    thresholds = np.linspace(0.05, 0.95, 91)\n    # Tìm ngưỡng cho điểm F1 cao nhất cho mỗi lớp\n    f1_scores_cls = [f1_score(all_labels[:, i], (all_probs[:, i] >= t).astype(int), zero_division=0) for t in thresholds]\n    best_t = thresholds[np.argmax(f1_scores_cls)]\n    best_thresholds.append(best_t)\n    print(f\" - {class_name:<20}: {best_t:.2f}\")\n\n# Áp dụng ngưỡng tìm được để ra quyết định 0 hoặc 1\nfinal_preds = np.zeros_like(all_probs)\nfor i in range(len(mlb.classes_)):\n    final_preds[:, i] = (all_probs[:, i] >= best_thresholds[i]).astype(int)\n\n# ==========================================\n# 4. BÁO CÁO & BIỂU ĐỒ\n# ==========================================\nmacro_f1_final = f1_score(all_labels, final_preds, average='macro', zero_division=0)\nprint(f\"\\nĐIỂM F1 TRUNG BÌNH (MACRO F1): {macro_f1_final:.4f}\")\nprint(\"=\"*65)\nprint(classification_report(all_labels, final_preds, target_names=mlb.classes_, zero_division=0))\n\nf1_scores_final = f1_score(all_labels, final_preds, average=None, zero_division=0)\n\nplt.figure(figsize=(12, 8))\nsns.barplot(x=f1_scores_final, y=mlb.classes_, palette='magma')\nplt.title('F1-Score từng loại bệnh (DenseNet121 + Auto-Threshold)', fontsize=15)\nplt.xlabel('F1-Score')\nplt.ylabel('Phân loại chẩn đoán')\nplt.xlim(0, 1.0)\nplt.grid(axis='x', linestyle='--')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-12T16:51:50.869832Z","iopub.execute_input":"2026-05-12T16:51:50.870202Z","iopub.status.idle":"2026-05-12T16:52:00.031345Z","shell.execute_reply.started":"2026-05-12T16:51:50.870164Z","shell.execute_reply":"2026-05-12T16:52:00.030462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import random\n\ndef test_random_numpy(df_test, image_dir, model, transform, thresholds, classes):\n    # Lấy ngẫu nhiên 1 hàng từ tập test\n    sample = df_test.sample(1).iloc[0]\n    img_name = f\"{sample['image_id']}.npy\"\n    img_path = os.path.join(image_dir, img_name)\n    \n    # Lấy nhãn thực tế\n    actual_diseases = [classes[i] for i, val in enumerate(sample['label_onehot']) if val == 1.0]\n    if not actual_diseases: \n        actual_diseases = [\"Không rõ\"]\n        \n    # Đọc và xử lý y hệt quy trình Dataset Numpy\n    image_array = np.load(img_path)\n    if image_array.max() <= 1.0:\n        image_array = (image_array * 255).astype(np.uint8)\n    else:\n        image_array = image_array.astype(np.uint8)\n\n    if len(image_array.shape) == 3 and image_array.shape[2] == 1:\n        image_array = np.squeeze(image_array, axis=-1)\n\n    img_pil = Image.fromarray(image_array).convert(\"RGB\")\n    \n    # Đưa vào model\n    img_tensor = transform(img_pil).unsqueeze(0).to(DEVICE)\n    \n    model.eval()\n    with torch.no_grad():\n        output = model(img_tensor)\n        probs = torch.sigmoid(output)[0].cpu().numpy() \n        \n    predicted_diseases = []\n    \n    # Rà soát qua 14 bệnh\n    for i, class_name in enumerate(classes):\n        if class_name == 'No finding': continue\n        \n        prob = probs[i]\n        thresh = max(thresholds[i], 0.15) # Ngưỡng sàn 15% để bắt tổn thương mờ\n        \n        if prob >= thresh:\n            predicted_diseases.append(f\"{class_name} ({prob*100:.1f}%)\")\n\n    # Xử lý nhãn No finding (chỉ báo bình thường nếu không có bệnh nào vuợt ngưỡng)\n    if len(predicted_diseases) == 0:\n        no_finding_idx = list(classes).index('No finding')\n        if probs[no_finding_idx] > 0.40:\n            predicted_diseases.append(f\"BÌNH THƯỜNG (Tự tin: {probs[no_finding_idx]*100:.1f}%)\")\n        else:\n            predicted_diseases.append(\"Ảnh khó chẩn đoán / Mờ\")\n\n    # Hiển thị kết quả lên màn hình\n    plt.figure(figsize=(9, 9))\n    plt.imshow(img_pil) # In luôn ảnh X-quang đen trắng gốc\n    plt.axis('off')\n    \n    actual_text = \"THỰC TẾ (GROUND TRUTH):\\n\" + \"\\n\".join([f\"- {d}\" for d in actual_diseases])\n    ai_text = \"AI DENSENET CHẨN ĐOÁN:\\n\" + \"\\n\".join([f\"- {d}\" for d in predicted_diseases])\n    \n    title_text = f\"Ảnh ID: {sample['image_id']}\\n\\n{actual_text}\\n\\n{ai_text}\"\n    color = 'darkgreen' if 'BÌNH THƯỜNG' in \"\".join(predicted_diseases) else 'darkred'\n    plt.title(title_text, fontsize=13, color=color, loc='left', pad=15, fontweight='bold')\n    plt.show()\n\nprint(\"Bốc ngẫu nhiên 1 ảnh Numpy để phân tích...\")\ntest_random_numpy(test_df, NPY_DIR, model_ft, val_transform, best_thresholds, mlb.classes_)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-12T16:52:00.033002Z","iopub.execute_input":"2026-05-12T16:52:00.033279Z","iopub.status.idle":"2026-05-12T16:52:00.253994Z","shell.execute_reply.started":"2026-05-12T16:52:00.033238Z","shell.execute_reply":"2026-05-12T16:52:00.253154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import multilabel_confusion_matrix\nimport torch\n\n# 1. Nạp model tốt nhất đã lưu\nmodel_best = models.densenet121(pretrained=False)\nnum_ftrs = model_best.classifier.in_features\nmodel_best.classifier = nn.Linear(num_ftrs, num_classes) # num_classes = 15\nmodel_best.load_state_dict(torch.load('/kaggle/working/best_densenet121_torchvision.pth', map_location=DEVICE))\nmodel_best.to(DEVICE)\nmodel_best.eval()\n\n# 2. Thực hiện dự đoán trên tập Test\nall_labels = []\nall_probs = []\n\nprint(\"Đang lấy dự đoán từ tập Test...\")\nwith torch.no_grad():\n    for images, labels in tqdm(test_loader):\n        images = images.to(DEVICE)\n        outputs = model_best(images)\n        probs = torch.sigmoid(outputs).cpu().numpy()\n        \n        all_probs.append(probs)\n        all_labels.append(labels.cpu().numpy())\n\ny_true = np.vstack(all_labels)\ny_probs = np.vstack(all_probs)\n\n# 3. Áp dụng Threshold riêng biệt cho từng lớp bệnh đã tính ở bước trước\ny_pred_optimal = np.zeros_like(y_probs)\nfor i in range(len(mlb.classes_)):\n    # Sử dụng mảng best_thresholds đã có trong notebook của bạn\n    y_pred_optimal[:, i] = (y_probs[:, i] >= best_thresholds[i]).astype(int)\n\n# 4. Tính toán và vẽ Ma trận nhầm lẫn\nmcm = multilabel_confusion_matrix(y_true, y_pred_optimal)\nclass_names = mlb.classes_\n\nplt.figure(figsize=(20, 20))\nfor i, class_name in enumerate(class_names):\n    plt.subplot(4, 4, i + 1)\n    # mcm[i] là ma trận 2x2: [[TN, FP], [FN, TP]]\n    sns.heatmap(mcm[i], annot=True, fmt='d', cmap='Blues', cbar=False)\n    plt.title(f'{class_name}\\n(Thresh: {best_thresholds[i]:.2f})', fontsize=12, fontweight='bold')\n    plt.xlabel('Dự đoán (0/1)')\n    plt.ylabel('Thực tế (0/1)')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-12T16:52:00.255385Z","iopub.execute_input":"2026-05-12T16:52:00.255694Z","iopub.status.idle":"2026-05-12T16:52:10.193736Z","shell.execute_reply.started":"2026-05-12T16:52:00.255664Z","shell.execute_reply":"2026-05-12T16:52:10.192824Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}