{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":94147,"databundleVersionId":11390004,"sourceType":"competition"},{"sourceId":11834298,"sourceType":"datasetVersion","datasetId":7425840}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Ensemble \n\nThis notebook ensembles the best submissions from public notebooks. These have been manually downloaded and upldoade in the elucidata-submissions dataset.\n\nEnjoy and good luck!","metadata":{}},{"cell_type":"code","source":"import os\nimport re\nimport numpy as np\nimport pandas as pd\nfrom sklearn.linear_model import Ridge\nfrom sklearn.metrics import mean_squared_error\nfrom scipy.stats import spearmanr\n\ndef ensemble_submissions(\n    submission_folder,\n    use_rank=False,\n    rank_axis=\"columns\",\n    output_prefix=\"\"\n):\n    assert rank_axis in {\"columns\", \"rows\"}, \"rank_axis must be 'columns' or 'rows'\"\n\n    pattern = re.compile(r\"submission_(\\d+)\\.csv\")\n    predictions = []\n    weights = []\n    submission_names = []\n\n    for fname in os.listdir(submission_folder):\n        match = pattern.match(fname)\n        if not match:\n            continue\n        score_str = match.group(1)\n        score = float(\"0.\" + score_str.lstrip(\"0\"))\n        df = pd.read_csv(os.path.join(submission_folder, fname))\n        pred_cols = [col for col in df.columns if col != \"ID\"]\n        pred = df[pred_cols].copy()\n\n        if use_rank:\n            axis = 0 if rank_axis == \"columns\" else 1\n            pred = pred.rank(axis=axis, method=\"average\")\n\n        predictions.append(pred)\n        weights.append(score)\n        submission_names.append(fname)\n\n    pred_array = np.stack([df.values for df in predictions], axis=-1)  # (N, C, M)\n    weights = np.array(weights)\n    weights = weights / weights.sum()\n    n_samples, n_targets, n_models = pred_array.shape\n    ID = df[\"ID\"]\n    columns = predictions[0].columns\n\n    rank_suffix = f\"_ranked_{rank_axis}\" if use_rank else \"_raw\"\n\n    def save(df_values, name):\n        df_out = pd.DataFrame(df_values, columns=columns)\n        df_out.insert(0, \"ID\", ID)\n        filename = f\"{output_prefix}{name}{rank_suffix}.csv\"\n        df_out.to_csv(filename, index=False)\n\n    # 1. Power Weighted Average\n    power = 3\n    w_pow = weights**power\n    w_pow /= w_pow.sum()\n    ensemble_power = np.average(pred_array, axis=-1, weights=w_pow)\n    save(ensemble_power, \"ensemble_power_weighted\")\n\n    # 2. Top-k Averaging\n    k = 3\n    top_k_idx = np.argsort(weights)[-k:]\n    top_k_preds = pred_array[:, :, top_k_idx]\n    top_k_weights = weights[top_k_idx]\n    top_k_weights = top_k_weights / top_k_weights.sum()\n    ensemble_top_k = np.average(top_k_preds, axis=-1, weights=top_k_weights)\n    save(ensemble_top_k, \"ensemble_top_k\")\n\n    # 3. Blend Best + Average of Others\n    best_idx = np.argmax(weights)\n    best_model = pred_array[:, :, best_idx]\n    others = np.delete(pred_array, best_idx, axis=2)\n    others_avg = np.mean(others, axis=2)\n    alpha = 0.8\n    ensemble_blend = alpha * best_model + (1 - alpha) * others_avg\n    save(ensemble_blend, \"ensemble_blend_best_avg\")\n\n    # 4. Threshold-Based Ensemble\n    threshold = 0.3\n    mask = weights >= threshold\n    if mask.sum() > 0:\n        selected_preds = pred_array[:, :, mask]\n        selected_weights = weights[mask] / weights[mask].sum()\n        ensemble_thresh = np.average(selected_preds, axis=-1, weights=selected_weights)\n        save(ensemble_thresh, \"ensemble_threshold_0.3\")\n\n    # 5. Residual-Based Per-Feature Weighting\n    baseline = ensemble_power.copy()\n    errors = np.zeros((n_targets, n_models))\n    for i in range(n_models):\n        pred_i = pred_array[:, :, i]\n        errors[:, i] = ((pred_i - baseline) ** 2).mean(axis=0)\n    weights_per_target = 1 / (errors + 1e-6)\n    weights_per_target /= weights_per_target.sum(axis=1, keepdims=True)\n    ensemble_residual = np.einsum(\"ncm,cm->nc\", pred_array, weights_per_target)\n    save(ensemble_residual, \"ensemble_residual_weighted\")\n\n    # 6. Stacking with Ridge Regression\n    X_all = np.transpose(pred_array, (0, 2, 1))  # (N, M, C)\n    final_preds = np.zeros((n_samples, n_targets))\n    y_meta = ensemble_power.copy()\n    for i in range(n_targets):\n        X_meta = X_all[:, :, i]\n        y_target = y_meta[:, i]\n        model = Ridge()\n        model.fit(X_meta, y_target)\n        final_preds[:, i] = model.predict(X_meta)\n    save(final_preds, \"ensemble_stacking\")\n\n    # 7. Greedy Ensemble (maximize Spearman vs power ensemble)\n    def average_models(indices):\n        return np.mean(pred_array[:, :, indices], axis=-1)\n\n    base = [np.argmax(weights)]\n    best = average_models(base)\n    for i in range(n_models):\n        if i in base:\n            continue\n        candidate = average_models(base + [i])\n        score_best = np.mean([spearmanr(best[:, j], y_meta[:, j])[0] for j in range(n_targets)])\n        score_candidate = np.mean([spearmanr(candidate[:, j], y_meta[:, j])[0] for j in range(n_targets)])\n        if score_candidate > score_best:\n            base.append(i)\n            best = candidate\n    save(best, \"ensemble_greedy\")\n\n    # 8. Correlation-Weighted Ensemble (Spearman corr to power baseline)\n    corr_weights = []\n    for i in range(n_models):\n        r = np.mean([spearmanr(pred_array[:, j, i], y_meta[:, j])[0] for j in range(n_targets)])\n        corr_weights.append(max(0, r))  # avoid negative weights\n    corr_weights = np.array(corr_weights)\n    if corr_weights.sum() > 0:\n        corr_weights = corr_weights / corr_weights.sum()\n        ensemble_corr = np.average(pred_array, axis=-1, weights=corr_weights)\n        save(ensemble_corr, \"ensemble_correlation_weighted\")\n\n    # Save diagnostics\n    pd.DataFrame(errors, index=columns, columns=submission_names).to_csv(f\"{output_prefix}model_target_errors{rank_suffix}.csv\")\n    pd.DataFrame(weights_per_target, index=columns, columns=submission_names).to_csv(f\"{output_prefix}residual_weights{rank_suffix}.csv\")\n\n    print(\"✅ Ensembles saved using \" + (\"ranked \" if use_rank else \"raw \") + f\"predictions (axis={rank_axis})\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:35.313014Z","iopub.execute_input":"2025-05-17T11:44:35.313310Z","iopub.status.idle":"2025-05-17T11:44:39.852469Z","shell.execute_reply.started":"2025-05-17T11:44:35.313271Z","shell.execute_reply":"2025-05-17T11:44:39.851010Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Raw value ensembling\nensemble_submissions(\"/kaggle/input/elucidata-submissions\", use_rank=False)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:39.853791Z","iopub.execute_input":"2025-05-17T11:44:39.854335Z","iopub.status.idle":"2025-05-17T11:44:43.966173Z","shell.execute_reply.started":"2025-05-17T11:44:39.854298Z","shell.execute_reply":"2025-05-17T11:44:43.965121Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Rank-based ensembling across columns (C1 to C36)\nensemble_submissions(\"/kaggle/input/elucidata-submissions\", use_rank=True, rank_axis=\"columns\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:43.967951Z","iopub.execute_input":"2025-05-17T11:44:43.968270Z","iopub.status.idle":"2025-05-17T11:44:47.686814Z","shell.execute_reply.started":"2025-05-17T11:44:43.968244Z","shell.execute_reply":"2025-05-17T11:44:47.685966Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Rank-based ensembling across rows (per sample)\nensemble_submissions(\"/kaggle/input/elucidata-submissions\", use_rank=True, rank_axis=\"rows\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:47.687749Z","iopub.execute_input":"2025-05-17T11:44:47.688071Z","iopub.status.idle":"2025-05-17T11:44:50.980655Z","shell.execute_reply.started":"2025-05-17T11:44:47.688042Z","shell.execute_reply":"2025-05-17T11:44:50.979689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\n\ndef visualize_ensemble_diagnostics(errors_csv, weights_csv):\n    \"\"\"\n    Generate heatmap visualizations from ensemble diagnostics.\n\n    Args:\n        errors_csv (str): Path to 'model_target_errors*.csv'.\n        weights_csv (str): Path to 'residual_weights*.csv'.\n    \"\"\"\n    # Load diagnostic CSVs\n    errors_df = pd.read_csv(errors_csv, index_col=0)\n    weights_df = pd.read_csv(weights_csv, index_col=0)\n\n    # Heatmap: Model-Target Residual Errors (MSE)\n    plt.figure(figsize=(14, 6))\n    sns.heatmap(errors_df, cmap=\"Reds\", cbar_kws={'label': 'MSE'})\n    plt.title(\"Model-Target Errors (MSE vs Power-Weighted Ensemble)\")\n    plt.xlabel(\"Submission\")\n    plt.ylabel(\"Target Variable\")\n    plt.xticks(rotation=45, ha=\"right\")\n    plt.tight_layout()\n    plt.show()\n\n    # Heatmap: Residual-Based Feature Weights\n    plt.figure(figsize=(14, 6))\n    sns.heatmap(weights_df, cmap=\"Blues\", cbar_kws={'label': 'Normalized Weight'})\n    plt.title(\"Residual-Based Per-Target Model Weights\")\n    plt.xlabel(\"Submission\")\n    plt.ylabel(\"Target Variable\")\n    plt.xticks(rotation=45, ha=\"right\")\n    plt.tight_layout()\n    plt.show()\n\n    # Barplot: Average Model Weight (residual-weighted)\n    avg_weights = weights_df.mean(axis=0).sort_values(ascending=False)\n    plt.figure(figsize=(12, 4))\n    sns.barplot(x=avg_weights.index, y=avg_weights.values, palette=\"coolwarm\")\n    plt.title(\"Average Model Weight Across Targets (Residual-Based)\")\n    plt.ylabel(\"Mean Weight\")\n    plt.xlabel(\"Submission\")\n    plt.xticks(rotation=45, ha=\"right\")\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:50.981781Z","iopub.execute_input":"2025-05-17T11:44:50.982066Z","iopub.status.idle":"2025-05-17T11:44:51.511141Z","shell.execute_reply.started":"2025-05-17T11:44:50.982034Z","shell.execute_reply":"2025-05-17T11:44:51.510157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:51.512113Z","iopub.execute_input":"2025-05-17T11:44:51.512622Z","iopub.status.idle":"2025-05-17T11:44:51.646492Z","shell.execute_reply.started":"2025-05-17T11:44:51.512590Z","shell.execute_reply":"2025-05-17T11:44:51.645246Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_ensemble_diagnostics(\n    errors_csv=\"model_target_errors_raw.csv\",\n    weights_csv=\"residual_weights_raw.csv\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:51.647863Z","iopub.execute_input":"2025-05-17T11:44:51.648227Z","iopub.status.idle":"2025-05-17T11:44:53.063445Z","shell.execute_reply.started":"2025-05-17T11:44:51.648193Z","shell.execute_reply":"2025-05-17T11:44:53.062382Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_ensemble_diagnostics(\n    errors_csv=\"model_target_errors_ranked_rows.csv\",\n    weights_csv=\"residual_weights_ranked_rows.csv\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:53.064488Z","iopub.execute_input":"2025-05-17T11:44:53.064861Z","iopub.status.idle":"2025-05-17T11:44:54.371652Z","shell.execute_reply.started":"2025-05-17T11:44:53.064828Z","shell.execute_reply":"2025-05-17T11:44:54.370621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_ensemble_diagnostics(\n    errors_csv=\"model_target_errors_ranked_columns.csv\",\n    weights_csv=\"residual_weights_ranked_columns.csv\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:54.374450Z","iopub.execute_input":"2025-05-17T11:44:54.374745Z","iopub.status.idle":"2025-05-17T11:44:55.824938Z","shell.execute_reply.started":"2025-05-17T11:44:54.374723Z","shell.execute_reply":"2025-05-17T11:44:55.823959Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#visualize_ensemble_diagnostics(\n#    errors_csv=\"model_target_errors.csv\",\n#    weights_csv=\"residual_weights.csv\"\n#)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:55.825839Z","iopub.execute_input":"2025-05-17T11:44:55.826088Z","iopub.status.idle":"2025-05-17T11:44:55.830514Z","shell.execute_reply.started":"2025-05-17T11:44:55.826070Z","shell.execute_reply":"2025-05-17T11:44:55.829449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import h5py\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport math\n\ndef plot_celltype_abundance(slide_name, image, spots, plots_per_line=4, show_image=False, plot_title=\"\"):\n    \"\"\"\n    Plot the slide image with spot overlays for cell-type abundances (C1 to C35).\n\n    Parameters:\n        slide_name (str): Name of the slide.\n        image (np.array): 2D or 3D array representing the slide image.\n        spots (np.array): Structured NumPy array containing spot information with fields:\n                          'x', 'y', 'C1', 'C2', ..., 'C35'.\n        plots_per_line (int): Number of subplots per row (default is 4).\n\n    The function creates a figure with subplots arranged in a grid and plots:\n      - The slide image as a background.\n      - A scatter overlay at spot (x, y) positions, colored by the abundance of each cell type.\n      - A common colorbar that indicates the mapping from color to abundance.\n    \"\"\"\n    # Extract the x and y coordinates from the spots array\n    x = spots[\"x\"]\n    y = spots[\"y\"]\n    num_celltypes = 35  # There are 35 cell types (C1 to C35)\n    num_rows = math.ceil(num_celltypes / plots_per_line)\n    \n    # Create a figure with a grid of subplots\n    fig, axes = plt.subplots(num_rows, plots_per_line, figsize=(plots_per_line * 3, num_rows * 3))\n    axes = axes.flatten()  # Flatten to simplify indexing\n    \n    # Loop over each cell type field from C1 to C35\n    for i in range(num_celltypes):\n        var_name = f\"C{i+1}\"  # Create field name e.g. \"C1\", \"C2\", ...\n        c_values = spots[var_name]  # Get the abundance values for this cell type\n        \n        ax = axes[i]\n        if show_image is True:\n            ax.imshow(image, aspect=\"auto\")\n        else:\n            # Manually set the axis limits to match the image dimensions\n            height, width = image.shape[:2]\n            ax.set_xlim(0, width)\n            ax.set_ylim(height, 0)  # invert y-axis to mimic image display\n\n        sc = ax.scatter(x, y, c=c_values, cmap=\"viridis\", s=2, alpha=1)\n        #sc = ax.scatter(x, y, c=c_values, cmap=\"plasma\", s=2, alpha=0.7)\n\n        ax.set_title(var_name, fontsize=8)\n        ax.axis(\"off\")\n    \n    # If there are any extra subplots (in case grid has one more cell), hide them\n    for j in range(num_celltypes, len(axes)):\n        axes[j].axis(\"off\")\n    \n\n    if plot_title != \"\":\n        fig.suptitle(plot_title)\n    else:\n        # Set an overall title for the figure\n        fig.suptitle(f\"Slide {slide_name}\", fontsize=14)\n\n    # Add a common colorbar (using the last scatter object)\n    #fig.colorbar(sc, ax=axes.tolist(), label=\"Abundance\")\n    #fig.subplots_adjust(right=0.85)\n    #cbar_ax = fig.add_axes([0.88, 0.15, 0.03, 0.7])  # [left, bottom, width, height]\n    #fig.colorbar(sc, cax=cbar_ax, label=\"Abundance\")\n    \n    plt.tight_layout(rect=[0, 0, 1, 0.95])\n    plt.show()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:55.831812Z","iopub.execute_input":"2025-05-17T11:44:55.832129Z","iopub.status.idle":"2025-05-17T11:44:56.080438Z","shell.execute_reply.started":"2025-05-17T11:44:55.832101Z","shell.execute_reply":"2025-05-17T11:44:56.079607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_submission(submission_file):\n    slice_name = \"S_7\"\n    with h5py.File(\"/kaggle/input/el-hackathon-2025/elucidata_ai_challenge_data.h5\", \"r\") as h5file:\n        image = np.array(h5file[\"images/Test\"][slice_name])\n        spots = np.array(h5file[\"spots/Test\"][slice_name])\n        submission_df = pd.read_csv(submission_file)\n        submission_df\n        spots_df = pd.DataFrame(spots).reset_index(names=\"ID\")\n        spots_merged = spots_df.drop(columns=\"Test_Set\").merge(submission_df)\n        spots_merged\n        plot_celltype_abundance(slice_name, image, spots_merged, plot_title=submission_file)\n    #plot_total_abundance(slice_name, image, spots, show_colorbar=True, colorbar_outside=True, show_image=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:56.081462Z","iopub.execute_input":"2025-05-17T11:44:56.082031Z","iopub.status.idle":"2025-05-17T11:44:56.087865Z","shell.execute_reply.started":"2025-05-17T11:44:56.081996Z","shell.execute_reply":"2025-05-17T11:44:56.086926Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_total_abundance(slide_name, image, spots, show_colorbar=True, colorbar_outside=True, show_image=True, show_spots=True):\n    \"\"\"\n    Plot the slide image with spot overlays where each spot's color represents \n    the total abundance of cell types C1 to C35.\n\n    Parameters:\n        slide_name (str): Name of the slide.\n        image (np.array): The slide image.\n        spots (np.array): Structured NumPy array with fields 'x', 'y', and 'C1' ... 'C35'.\n        show_colorbar (bool): Whether to display a colorbar (default True).\n        colorbar_outside (bool): If True, place the colorbar to the right of the plot.\n    \"\"\"\n    # Extract x and y coordinates from the spots array\n    x = spots[\"x\"]\n    y = spots[\"y\"]\n    \n    # Compute total abundance by summing C1 through C35 for each spot\n    total_abundance = np.zeros_like(x, dtype=float)\n    if slide_name == \"S_7\":\n        total_abundance = spots[\"Test_Set\"]\n    else:\n        for i in range(1, 36):  # Fields C1 to C35\n            total_abundance += spots[f\"C{i}\"]\n    \n    # Create a figure\n    fig, ax = plt.subplots(figsize=(6, 6))\n    if show_image is True:\n        ax.imshow(image, aspect=\"auto\")\n    else:\n        # Manually set the axis limits to match the image dimensions\n        height, width = image.shape[:2]\n        ax.set_xlim(0, width)\n        ax.set_ylim(height, 0)  # invert y-axis to mimic image display\n\n    if show_spots:\n        sc = ax.scatter(x, y, c=total_abundance, cmap=\"viridis\", s=2, alpha=0.7)\n    \n        # Add a colorbar if desired\n        if show_colorbar:\n            if colorbar_outside:\n                # Adjust the right margin to make room for the colorbar\n                fig.subplots_adjust(right=0.85)\n                cbar_ax = fig.add_axes([0.88, 0.15, 0.03, 0.7])\n                fig.colorbar(sc, cax=cbar_ax, label=\"Total Abundance\")\n            else:\n                fig.colorbar(sc, ax=ax, label=\"Total Abundance\")\n    ax.set_title(f\"Total Abundance for Slide {slide_name}\")\n    ax.axis(\"off\")\n    \n    plt.tight_layout()\n    plt.show()\nslice_name = \"S_7\"\n\n\nwith h5py.File(\"/kaggle/input/el-hackathon-2025/elucidata_ai_challenge_data.h5\", \"r\") as h5file:\n    image = np.array(h5file[\"images/Test\"][\"S_7\"])\n    spots = np.array(h5file[\"spots/Test\"][\"S_7\"])\n    submission_df = pd.read_csv(\"ensemble_blend_best_avg_ranked_rows.csv\") # any file works\n    test_spots = pd.merge(\n        pd.DataFrame(spots).reset_index(names=\"ID\"),\n        submission_df\n    )\n    test_spots\n    plot_total_abundance(\"S_7\", image, test_spots, show_colorbar=True, colorbar_outside=True, show_spots=False)\n    plot_total_abundance(\"S_7\", image, test_spots, show_colorbar=False, colorbar_outside=True, show_spots=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:56.088816Z","iopub.execute_input":"2025-05-17T11:44:56.089079Z","iopub.status.idle":"2025-05-17T11:44:58.433224Z","shell.execute_reply.started":"2025-05-17T11:44:56.089058Z","shell.execute_reply":"2025-05-17T11:44:58.432253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import glob\nfor submission_file in glob.glob(\"*ensemble*rows*\"):\n    plot_submission(submission_file)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:44:58.434215Z","iopub.execute_input":"2025-05-17T11:44:58.434503Z","iopub.status.idle":"2025-05-17T11:46:27.420774Z","shell.execute_reply.started":"2025-05-17T11:44:58.434480Z","shell.execute_reply":"2025-05-17T11:46:27.419582Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_submission(\"ensemble_blend_best_avg_ranked_rows.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-17T11:46:27.421884Z","iopub.execute_input":"2025-05-17T11:46:27.422152Z","iopub.status.idle":"2025-05-17T11:46:31.627069Z","shell.execute_reply.started":"2025-05-17T11:46:27.422130Z","shell.execute_reply":"2025-05-17T11:46:31.625796Z"}},"outputs":[],"execution_count":null}]}