{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":31254,"databundleVersionId":3103714,"sourceType":"competition"},{"sourceId":13688515,"sourceType":"datasetVersion","datasetId":8705918}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# 🚀 Smart image-to-CSV matching dataset loader for H&M dataset\n# ============================================================\n\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport torch\n\n# -------------- CONFIG -------------------\nCSV_PATH = \"/kaggle/input/merges-articles-and-transactions/merged_articles_transactions.csv\"\nIMAGES_ROOT = \"/kaggle/input/h-and-m-personalized-fashion-recommendations/images\"\n# -----------------------------------------\n\n# Known class mappings (from your project definition)\nCOLOR_CLASSES = [\"Black\", \"White\", \"Red\", \"Blue\", \"Navy\", \"Grey\", \"Beige\", \"Pink\", \"Green\", \"Brown\"]\nPRODUCT_CLASSES = [\"T-shirt\", \"Dress\", \"Shirt\", \"Blouse\", \"Sweater\", \"Jacket\", \"Trousers\", \"Shorts\", \"Skirt\", \"Vest Top\"]\n\ncolor_map = {c.lower(): i for i, c in enumerate(COLOR_CLASSES)}\nproduct_map = {p.lower(): i for i, p in enumerate(PRODUCT_CLASSES)}\n\n# Reproducibility\ntorch.manual_seed(42)\nnp.random.seed(42)\nrandom.seed(42)\n\n# Sanity check\nif not os.path.exists(IMAGES_ROOT):\n    raise FileNotFoundError(f\"Images directory not found at: {IMAGES_ROOT}\")\n\nprint(\"✅ Imports and config successful.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:40:58.094916Z","iopub.execute_input":"2025-11-11T07:40:58.095505Z","iopub.status.idle":"2025-11-11T07:41:01.334251Z","shell.execute_reply.started":"2025-11-11T07:40:58.095482Z","shell.execute_reply":"2025-11-11T07:41:01.333594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 1 — Read and preprocess the CSV\n# ------------------------------------------------------------\nprint(\"Reading merged CSV...\")\ndf = pd.read_csv(CSV_PATH, dtype=str)\n\n# Normalize column names\ndf.columns = [c.strip().lower() for c in df.columns]\n\n# Ensure required column exists\nif \"article_id\" not in df.columns:\n    raise KeyError(\"Missing required column 'article_id' in CSV\")\n\n# Clean article IDs\ndf[\"article_id\"] = df[\"article_id\"].astype(str).str.strip().str.strip('\"').str.strip(\"'\")\ndf = df.sort_values(\"article_id\").reset_index(drop=True)\n\nprint(f\"✅ Loaded {len(df)} rows from CSV.\")\nprint(\"Unique article_ids:\", df[\"article_id\"].nunique())\nprint(\"Sample rows:\")\ndisplay(df.head(3))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:41:01.335213Z","iopub.execute_input":"2025-11-11T07:41:01.335627Z","iopub.status.idle":"2025-11-11T07:41:01.839061Z","shell.execute_reply.started":"2025-11-11T07:41:01.335607Z","shell.execute_reply":"2025-11-11T07:41:01.838239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# ✅ Step 2 — Map each article_id to its actual image path (with leading zero fix)\n# ------------------------------------------------------------\nprint(\"Building image path mapping directly from CSV IDs (auto-correcting leading zeros)...\")\n\nimage_records = []\n\n# Fix dropped leading zeros\ndf[\"article_id\"] = df[\"article_id\"].astype(str).str.strip().str.strip('\"').str.strip(\"'\")\ndf[\"article_id_fixed\"] = df[\"article_id\"].apply(lambda x: x if x.startswith(\"0\") else \"0\" + x)\n\n# Get cleaned + sorted IDs\ncsv_ids = sorted(df[\"article_id_fixed\"].tolist())\n\nfor aid in csv_ids:\n    folder = aid[:3]  # first three digits → image folder\n    folder_path = os.path.join(IMAGES_ROOT, folder)\n\n    if not os.path.isdir(folder_path):\n        continue\n\n    found = False\n    for ext in [\".jpg\", \".jpeg\", \".png\"]:\n        img_path = os.path.join(folder_path, aid + ext)\n        if os.path.exists(img_path):\n            image_records.append((aid, img_path))\n            found = True\n            break\n\n    if not found:\n        pass  # skip unmatched IDs silently\n\nprint(f\"✅ Successfully matched {len(image_records)} image files out of {len(csv_ids)} total article IDs.\")\n\nif len(image_records) == 0:\n    raise RuntimeError(\"❌ No matching images found — check IMAGES_ROOT and CSV formatting.\")\n\n# Build DataFrame and merge (using fixed IDs)\nimg_df = pd.DataFrame(image_records, columns=[\"article_id_fixed\", \"image_path\"])\nmerged_df = pd.merge(img_df, df, on=\"article_id_fixed\", how=\"left\")\n\nprint(f\"✅ Final merged dataset: {len(merged_df)} samples\")\ndisplay(merged_df.head(5))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:41:01.839893Z","iopub.execute_input":"2025-11-11T07:41:01.840105Z","iopub.status.idle":"2025-11-11T07:43:56.470718Z","shell.execute_reply.started":"2025-11-11T07:41:01.840088Z","shell.execute_reply":"2025-11-11T07:43:56.469918Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 3 — Define custom Dataset\n# ------------------------------------------------------------\nclass FashionImageDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\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        img = Image.open(row[\"image_path\"]).convert(\"RGB\")\n\n        if self.transform:\n            img = self.transform(img)\n\n        # Handle flexible column naming\n        prod_col = None\n        color_col = None\n        for c in [\"product_type_name\", \"product_name\", \"prod_name\"]:\n            if c in self.df.columns:\n                prod_col = c\n                break\n        for c in [\"colour_group_name\", \"perceived_colour_master_name\", \"colour_name\"]:\n            if c in self.df.columns:\n                color_col = c\n                break\n\n        prod_raw = str(row.get(prod_col, \"\")).lower().strip()\n        color_raw = str(row.get(color_col, \"\")).lower().strip()\n\n        prod_label = product_map.get(prod_raw, -1)\n        color_label = color_map.get(color_raw, -1)\n\n        return img, prod_label, color_label, row[\"article_id\"]\n\nprint(\"✅ FashionImageDataset class ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:44:19.872285Z","iopub.execute_input":"2025-11-11T07:44:19.873069Z","iopub.status.idle":"2025-11-11T07:44:19.879941Z","shell.execute_reply.started":"2025-11-11T07:44:19.873043Z","shell.execute_reply":"2025-11-11T07:44:19.879136Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Ensure all labels are within your defined classes\nvalid_prod = [p.lower() for p in PRODUCT_CLASSES]\nvalid_color = [c.lower() for c in COLOR_CLASSES]\n\nbefore = len(merged_df)\n\nmerged_df = merged_df[\n    merged_df[\"product_type_name\"].str.lower().isin(valid_prod)\n    & merged_df[\"colour_group_name\"].str.lower().isin(valid_color)\n]\n\nprint(f\"✅ Filtered dataset from {before} → {len(merged_df)} valid entries\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:44:22.054018Z","iopub.execute_input":"2025-11-11T07:44:22.054489Z","iopub.status.idle":"2025-11-11T07:44:22.083266Z","shell.execute_reply.started":"2025-11-11T07:44:22.054466Z","shell.execute_reply":"2025-11-11T07:44:22.082647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 4 — Setup transforms and dataloaders\n# ------------------------------------------------------------\ntrain_transform = transforms.Compose([\n    transforms.Resize((256, 256)),\n    transforms.RandomResizedCrop(224),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                         std=[0.229, 0.224, 0.225]),\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                         std=[0.229, 0.224, 0.225]),\n])\n\n# Optional: subsample for faster debugging (comment out for full dataset)\n# merged_df = merged_df.sample(5000, random_state=42).reset_index(drop=True)\n\n# Shuffle and split\nindices = np.arange(len(merged_df))\nnp.random.shuffle(indices)\nsplit = int(0.85 * len(indices))\ntrain_df = merged_df.iloc[indices[:split]]\nval_df = merged_df.iloc[indices[split:]]\n\n# Create datasets\ntrain_ds = FashionImageDataset(train_df, transform=train_transform)\nval_ds = FashionImageDataset(val_df, transform=val_transform)\n\n# Create dataloaders\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True)\nval_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)\n\nprint(f\"📦 Train samples: {len(train_ds)}, Val samples: {len(val_ds)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:44:24.309401Z","iopub.execute_input":"2025-11-11T07:44:24.309976Z","iopub.status.idle":"2025-11-11T07:44:24.334419Z","shell.execute_reply.started":"2025-11-11T07:44:24.309951Z","shell.execute_reply":"2025-11-11T07:44:24.333787Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# ------------------------------------------------------------\n# Step 5 — Visualize a few samples\n# ------------------------------------------------------------\ndef show_samples(dataset, n=9):\n    fig, axes = plt.subplots(3, 3, figsize=(8, 8))\n    for i in range(n):\n        img, prod_label, color_label, aid = dataset[i]\n        ax = axes[i // 3, i % 3]\n        img_np = img.permute(1, 2, 0).numpy()\n        img_np = (img_np * 0.229 + 0.485).clip(0, 1)\n        ax.imshow(img_np)\n        ax.set_title(f\"{PRODUCT_CLASSES[prod_label] if prod_label >= 0 else '?'} / \"\n                     f\"{COLOR_CLASSES[color_label] if color_label >= 0 else '?'}\\n({aid})\",\n                     fontsize=8)\n        ax.axis(\"off\")\n    plt.tight_layout()\n    plt.show()\n\nshow_samples(train_ds)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:44:27.664358Z","iopub.execute_input":"2025-11-11T07:44:27.664861Z","iopub.status.idle":"2025-11-11T07:44:28.816658Z","shell.execute_reply.started":"2025-11-11T07:44:27.664838Z","shell.execute_reply":"2025-11-11T07:44:28.815479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 6 — Define Multi-Head CNN Model (ResNet50 Backbone)\n# ------------------------------------------------------------\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\n\nclass ResNetMultiHead(nn.Module):\n    def __init__(self, backbone_name=\"resnet50\", pretrained=True,\n                 num_product_classes=10, num_color_classes=10, dropout=0.5):\n        super().__init__()\n\n        if backbone_name == \"resnet50\":\n            self.backbone = models.resnet50(pretrained=pretrained)\n            feat_dim = self.backbone.fc.in_features\n            self.backbone.fc = nn.Identity()\n        else:\n            raise ValueError(\"Only ResNet50 is implemented in this version.\")\n\n        # Define two separate heads\n        self.product_head = nn.Sequential(\n            nn.Dropout(dropout),\n            nn.Linear(feat_dim, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, num_product_classes)\n        )\n        self.color_head = nn.Sequential(\n            nn.Dropout(dropout),\n            nn.Linear(feat_dim, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_color_classes)\n        )\n\n    def forward(self, x):\n        features = self.backbone(x)\n        product_output = self.product_head(features)\n        color_output = self.color_head(features)\n        return product_output, color_output\n\n# Instantiate model\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = ResNetMultiHead(pretrained=True,\n                        num_product_classes=len(PRODUCT_CLASSES),\n                        num_color_classes=len(COLOR_CLASSES)).to(device)\n\nprint(\"✅ Model initialized on device:\", device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:44:31.282310Z","iopub.execute_input":"2025-11-11T07:44:31.282634Z","iopub.status.idle":"2025-11-11T07:44:31.728246Z","shell.execute_reply.started":"2025-11-11T07:44:31.282611Z","shell.execute_reply":"2025-11-11T07:44:31.727550Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 8 — Safer loss and accuracy with masking\n# ------------------------------------------------------------\ncriterion = nn.CrossEntropyLoss(reduction=\"none\")\n\ndef compute_masked_loss(outputs, targets):\n    # Ignore invalid labels (negative or >= num_classes)\n    valid_mask = (targets >= 0) & (targets < outputs.size(1))\n    if not valid_mask.any():\n        return torch.tensor(0.0, device=targets.device, requires_grad=True)\n    losses = criterion(outputs, targets)\n    return losses[valid_mask].mean()\n\ndef accuracy(outputs, targets):\n    valid_mask = (targets >= 0) & (targets < outputs.size(1))\n    if not valid_mask.any():\n        return 0.0\n    preds = outputs.argmax(1)\n    correct = (preds[valid_mask] == targets[valid_mask]).sum().item()\n    total = valid_mask.sum().item()\n    return 100.0 * correct / total if total > 0 else 0.0\n\n\ndef train_one_epoch(model, loader, optimizer, device):\n    model.train()\n    running_loss, running_acc_p, running_acc_c = 0.0, 0.0, 0.0\n    total_p, total_c = 0, 0\n\n    for imgs, prod_labels, color_labels, _ in loader:\n        # ✅ Convert to tensors on CPU first (safe)\n        if isinstance(prod_labels, torch.Tensor) is False:\n            prod_labels = torch.tensor(prod_labels, dtype=torch.long)\n        if isinstance(color_labels, torch.Tensor) is False:\n            color_labels = torch.tensor(color_labels, dtype=torch.long)\n\n        # ✅ Clamp invalid values (-1 or >class range)\n        prod_labels = torch.clamp(prod_labels, 0, len(PRODUCT_CLASSES) - 1)\n        color_labels = torch.clamp(color_labels, 0, len(COLOR_CLASSES) - 1)\n\n        imgs = imgs.to(device)\n        prod_labels = prod_labels.to(device)\n        color_labels = color_labels.to(device)\n\n        optimizer.zero_grad()\n        out_prod, out_color = model(imgs)\n\n        loss_p = compute_masked_loss(out_prod, prod_labels)\n        loss_c = compute_masked_loss(out_color, color_labels)\n        loss = loss_p + loss_c\n\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * imgs.size(0)\n        running_acc_p += accuracy(out_prod, prod_labels) * imgs.size(0)\n        running_acc_c += accuracy(out_color, color_labels) * imgs.size(0)\n        total_p += imgs.size(0)\n        total_c += imgs.size(0)\n\n    return (\n        running_loss / len(loader.dataset),\n        running_acc_p / total_p,\n        running_acc_c / total_c\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:44:35.101059Z","iopub.execute_input":"2025-11-11T07:44:35.101880Z","iopub.status.idle":"2025-11-11T07:44:35.111804Z","shell.execute_reply.started":"2025-11-11T07:44:35.101854Z","shell.execute_reply":"2025-11-11T07:44:35.111025Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef evaluate(model, loader, device):\n    model.eval()\n    total_loss, total_acc_prod, total_acc_color = 0.0, 0.0, 0.0\n    count = 0\n\n    for imgs, prod_labels, color_labels, _ in loader:\n        imgs = imgs.to(device)\n        prod_labels = torch.tensor(prod_labels, dtype=torch.long, device=device)\n        color_labels = torch.tensor(color_labels, dtype=torch.long, device=device)\n\n        out_prod, out_color = model(imgs)\n        loss_p = compute_masked_loss(out_prod, prod_labels)\n        loss_c = compute_masked_loss(out_color, color_labels)\n        loss = loss_p + loss_c\n\n        total_loss += loss.item()\n        total_acc_prod += accuracy(out_prod, prod_labels)\n        total_acc_color += accuracy(out_color, color_labels)\n        count += 1\n\n    return total_loss / count, total_acc_prod / count, total_acc_color / count\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:44:40.144894Z","iopub.execute_input":"2025-11-11T07:44:40.145631Z","iopub.status.idle":"2025-11-11T07:44:40.150882Z","shell.execute_reply.started":"2025-11-11T07:44:40.145606Z","shell.execute_reply":"2025-11-11T07:44:40.150134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 8 — Define Loss and Optimizer\n# ------------------------------------------------------------\n\nimport torch.nn as nn\nimport torch.optim as optim\n\n# CrossEntropy for both heads\ncriterion = nn.CrossEntropyLoss(reduction=\"none\")\n\n# AdamW optimizer works best with ResNet\noptimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)\n\nprint(\"Optimizer and criterion defined successfully.\")\nprint(optimizer)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T08:13:52.999379Z","iopub.execute_input":"2025-11-11T08:13:53.000232Z","iopub.status.idle":"2025-11-11T08:13:53.008365Z","shell.execute_reply.started":"2025-11-11T08:13:53.000204Z","shell.execute_reply":"2025-11-11T08:13:53.007669Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 9 — Run Training Loop\n# ------------------------------------------------------------\nnum_epochs = 5\nbest_val_loss = float(\"inf\")\nsave_path = \"best_model.pth\"\n\nfor epoch in range(num_epochs):\n    train_loss, train_acc_p, train_acc_c = train_one_epoch(model, train_loader, optimizer, device)\n    val_loss, val_acc_p, val_acc_c = evaluate(model, val_loader, device)\n\n    print(f\"Epoch [{epoch+1}/{num_epochs}]\")\n    print(f\"  🔹 Train Loss: {train_loss:.4f} | Product Acc: {train_acc_p:.2f}% | Color Acc: {train_acc_c:.2f}%\")\n    print(f\"  🔸 Val   Loss: {val_loss:.4f} | Product Acc: {val_acc_p:.2f}% | Color Acc: {val_acc_c:.2f}%\")\n\n    # Save best model\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        torch.save({\n            \"model_state_dict\": model.state_dict(),\n            \"optimizer_state_dict\": optimizer.state_dict(),\n            \"product_classes\": PRODUCT_CLASSES,\n            \"color_classes\": COLOR_CLASSES\n        }, save_path)\n        print(f\"✅ Saved new best model to {save_path}\")\n\nprint(f\"Training complete. Best validation loss: {best_val_loss:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T07:46:26.903289Z","iopub.execute_input":"2025-11-11T07:46:26.903899Z","iopub.status.idle":"2025-11-11T08:12:29.739635Z","shell.execute_reply.started":"2025-11-11T07:46:26.903872Z","shell.execute_reply":"2025-11-11T08:12:29.738377Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 10 — Convert & Save Model as best_model.h5\n# ------------------------------------------------------------\nimport h5py\n\n# Save standard PyTorch weights\ntorch.save(model.state_dict(), \"best_model.pth\")\nprint(\"✅ Saved PyTorch model as best_model.pth\")\n\n# Create a small .h5 checkpoint with metadata\nwith h5py.File(\"best_model.h5\", \"w\") as f:\n    f.attrs[\"description\"] = \"ResNet50 multi-head model for H&M product + color classification\"\n    f.create_dataset(\"product_classes\", data=np.array(PRODUCT_CLASSES, dtype=\"S\"))\n    f.create_dataset(\"color_classes\", data=np.array(COLOR_CLASSES, dtype=\"S\"))\nprint(\"✅ Created metadata file: best_model.h5\")\n\n!ls -lh best_model.*\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T08:12:30.368837Z","iopub.execute_input":"2025-11-11T08:12:30.369694Z","iopub.status.idle":"2025-11-11T08:12:30.795027Z","shell.execute_reply.started":"2025-11-11T08:12:30.369658Z","shell.execute_reply":"2025-11-11T08:12:30.794283Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Step 11 — Quick Inference on Random Samples\n# ------------------------------------------------------------\nimport random\nimport matplotlib.pyplot as plt\n\nmodel.eval()\n\nsamples = random.sample(range(len(val_ds)), 3)\nfor idx in samples:\n    img, prod_label, color_label, aid = val_ds[idx]\n    with torch.no_grad():\n        out_p, out_c = model(img.unsqueeze(0).to(device))\n        pred_prod = PRODUCT_CLASSES[out_p.argmax(1).item()]\n        pred_color = COLOR_CLASSES[out_c.argmax(1).item()]\n\n    plt.imshow(img.permute(1, 2, 0).numpy() * 0.25 + 0.5)\n    plt.axis(\"off\")\n    plt.title(f\"Pred: {pred_prod} / {pred_color}\\nID: {aid}\", fontsize=8)\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T08:13:18.215818Z","iopub.execute_input":"2025-11-11T08:13:18.216369Z","iopub.status.idle":"2025-11-11T08:13:18.694958Z","shell.execute_reply.started":"2025-11-11T08:13:18.216347Z","shell.execute_reply":"2025-11-11T08:13:18.694221Z"}},"outputs":[],"execution_count":null}]}