{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "21ce2b41", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score\n", "\n", "import tensorflow as tf\n", "from tensorflow.keras.models import Sequential\n", "from tensorflow.keras.layers import (\n", " LSTM,\n", " Bidirectional,\n", " Dense,\n", " Dropout,\n", " Masking\n", ")\n", "\n", "from tensorflow.keras.callbacks import (\n", " EarlyStopping,\n", " ReduceLROnPlateau\n", ")\n", "\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", "execution_count": 2, "id": "7b5e2a73", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
match_iddelivery_numberlegal_ballover_ballscorewicketstarget_score
01001349110.100168
11001349220.200168
21001349330.310168
31001349440.430168
41001349550.530168
\n", "
" ], "text/plain": [ " match_id delivery_number legal_ball over_ball score wickets \\\n", "0 1001349 1 1 0.1 0 0 \n", "1 1001349 2 2 0.2 0 0 \n", "2 1001349 3 3 0.3 1 0 \n", "3 1001349 4 4 0.4 3 0 \n", "4 1001349 5 5 0.5 3 0 \n", "\n", " target_score \n", "0 168 \n", "1 168 \n", "2 168 \n", "3 168 \n", "4 168 " ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df = pd.read_csv(\n", " \"data/run_prediction_dataset_raw.csv\"\n", ")\n", "\n", "df.head()" ] }, { "cell_type": "code", "execution_count": 3, "id": "4b258bec", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
match_iddelivery_numberlegal_ballover_ballscorewicketstarget_scoreruns_this_ballprevious_scorelast_6_runslast_12_runsboundarieslast_12_boundariesdotslast_12_dotsballs_remainingrun_rate
514915211028110.1001790.00.00.00.000.010.01190.0
514916211028220.2101791.00.00.00.000.000.01183.0
514917211028330.3101790.01.00.00.000.010.01172.0
514918211028440.4101790.01.00.00.000.010.01161.5
514919211028550.5101790.01.00.00.000.010.01151.2
\n", "
" ], "text/plain": [ " match_id delivery_number legal_ball over_ball score wickets \\\n", "514915 211028 1 1 0.1 0 0 \n", "514916 211028 2 2 0.2 1 0 \n", "514917 211028 3 3 0.3 1 0 \n", "514918 211028 4 4 0.4 1 0 \n", "514919 211028 5 5 0.5 1 0 \n", "\n", " target_score runs_this_ball previous_score last_6_runs \\\n", "514915 179 0.0 0.0 0.0 \n", "514916 179 1.0 0.0 0.0 \n", "514917 179 0.0 1.0 0.0 \n", "514918 179 0.0 1.0 0.0 \n", "514919 179 0.0 1.0 0.0 \n", "\n", " last_12_runs boundaries last_12_boundaries dots last_12_dots \\\n", "514915 0.0 0 0.0 1 0.0 \n", "514916 0.0 0 0.0 0 0.0 \n", "514917 0.0 0 0.0 1 0.0 \n", "514918 0.0 0 0.0 1 0.0 \n", "514919 0.0 0 0.0 1 0.0 \n", "\n", " balls_remaining run_rate \n", "514915 119 0.0 \n", "514916 118 3.0 \n", "514917 117 2.0 \n", "514918 116 1.5 \n", "514919 115 1.2 " ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df = df.sort_values(\n", " [\n", " \"match_id\",\n", " \"delivery_number\"\n", " ]\n", ")\n", "\n", "\n", "# runs scored this ball\n", "df[\"runs_this_ball\"] = (\n", " df.groupby(\"match_id\")[\"score\"]\n", " .diff()\n", " .fillna(\n", " df[\"score\"]\n", " )\n", ")\n", "\n", "\n", "# previous score\n", "df[\"previous_score\"] = (\n", " df[\"score\"]\n", " -\n", " df[\"runs_this_ball\"]\n", ")\n", "\n", "\n", "# rolling scoring form\n", "\n", "df[\"last_6_runs\"] = (\n", " df.groupby(\"match_id\")\n", " [\"runs_this_ball\"]\n", " .rolling(6)\n", " .sum()\n", " .reset_index(level=0,drop=True)\n", " .fillna(0)\n", ")\n", "\n", "\n", "df[\"last_12_runs\"] = (\n", " df.groupby(\"match_id\")\n", " [\"runs_this_ball\"]\n", " .rolling(12)\n", " .sum()\n", " .reset_index(level=0,drop=True)\n", " .fillna(0)\n", ")\n", "\n", "\n", "df[\"boundaries\"] = (\n", " df[\"runs_this_ball\"] >= 4\n", ").astype(int)\n", "\n", "\n", "df[\"last_12_boundaries\"] = (\n", " df.groupby(\"match_id\")\n", " [\"boundaries\"]\n", " .rolling(12)\n", " .sum()\n", " .reset_index(level=0,drop=True)\n", " .fillna(0)\n", ")\n", "\n", "\n", "df[\"dots\"] = (\n", " df[\"runs_this_ball\"] == 0\n", ").astype(int)\n", "\n", "\n", "df[\"last_12_dots\"] = (\n", " df.groupby(\"match_id\")\n", " [\"dots\"]\n", " .rolling(12)\n", " .sum()\n", " .reset_index(level=0,drop=True)\n", " .fillna(0)\n", ")\n", "\n", "# Existing features above...\n", "\n", "\n", "# New features\n", "df[\"balls_remaining\"] = 120 - df[\"legal_ball\"]\n", "\n", "df[\"run_rate\"] = (\n", " df[\"score\"] / (df[\"legal_ball\"] / 6)\n", ").replace(\n", " [float(\"inf\"), -float(\"inf\")],\n", " 0\n", ").fillna(0)\n", "\n", "df.head()" ] }, { "cell_type": "code", "execution_count": 4, "id": "1fc0e0b9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(440081, 17)\n" ] } ], "source": [ "df = df[\n", " df[\"legal_ball\"] >= 30\n", "]\n", "\n", "\n", "print(\n", " df.shape\n", ")" ] }, { "cell_type": "code", "execution_count": 5, "id": "370d0cfb", "metadata": {}, "outputs": [], "source": [ "FEATURES = [\n", " \"score\",\n", " \"wickets\",\n", " \"legal_ball\",\n", " \"runs_this_ball\",\n", " \"last_6_runs\",\n", " \"last_12_runs\",\n", " \"last_12_boundaries\",\n", " \"last_12_dots\",\n", " \"run_rate\",\n", " \"balls_remaining\"\n", "]\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "d43f7fc0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Training matches: 3686\n", "Testing matches : 922\n", "Training rows: 352215\n", "Testing rows : 87866\n" ] } ], "source": [ "from sklearn.model_selection import train_test_split\n", "\n", "\n", "# ==========================================\n", "# Split by MATCH, not by sequence\n", "# ==========================================\n", "\n", "all_matches = df[\"match_id\"].unique()\n", "\n", "\n", "train_matches, test_matches = train_test_split(\n", " all_matches,\n", " test_size=0.2,\n", " random_state=42\n", ")\n", "\n", "\n", "train_df = df[\n", " df[\"match_id\"].isin(train_matches)\n", "].copy()\n", "\n", "\n", "test_df = df[\n", " df[\"match_id\"].isin(test_matches)\n", "].copy()\n", "\n", "\n", "print(\"Training matches:\", len(train_matches))\n", "print(\"Testing matches :\", len(test_matches))\n", "\n", "print(\"Training rows:\", len(train_df))\n", "print(\"Testing rows :\", len(test_df))" ] }, { "cell_type": "code", "execution_count": 7, "id": "55e46374", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n" ] } ], "source": [ "print(\n", " len(\n", " set(train_matches)\n", " &\n", " set(test_matches)\n", " )\n", ")" ] }, { "cell_type": "code", "execution_count": 8, "id": "66f4ae4e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Train sequences: 241635\n", "Test sequences: 60206\n" ] } ], "source": [ "def create_sequences(data):\n", "\n", " X = []\n", " y = []\n", "\n", " for match_id, innings in data.groupby(\"match_id\"):\n", "\n", " innings = innings.sort_values(\n", " \"legal_ball\"\n", " )\n", "\n", " sequence = innings[FEATURES].values\n", "\n", " target = innings[\"target_score\"].iloc[0]\n", "\n", "\n", " for i in range(30, len(sequence)):\n", "\n", " X.append(\n", " sequence[:i+1]\n", " )\n", "\n", " y.append(\n", " target\n", " )\n", "\n", " return X, y\n", "\n", "\n", "\n", "X_train_sequences, y_train_targets = create_sequences(train_df)\n", "\n", "X_test_sequences, y_test_targets = create_sequences(test_df)\n", "\n", "\n", "print(\n", " \"Train sequences:\",\n", " len(X_train_sequences)\n", ")\n", "\n", "print(\n", " \"Test sequences:\",\n", " len(X_test_sequences)\n", ")" ] }, { "cell_type": "code", "execution_count": 9, "id": "1aa07812", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Train: (241635, 120, 10) (241635,)\n", "Test : (60206, 120, 10) (60206,)\n" ] } ], "source": [ "from tensorflow.keras.preprocessing.sequence import pad_sequences\n", "import numpy as np\n", "\n", "\n", "# X_train_sequences and X_test_sequences\n", "# should already be created from separate match splits\n", "\n", "X_train = pad_sequences(\n", " X_train_sequences,\n", " maxlen=120,\n", " dtype=\"float32\",\n", " padding=\"pre\"\n", ")\n", "\n", "X_test = pad_sequences(\n", " X_test_sequences,\n", " maxlen=120,\n", " dtype=\"float32\",\n", " padding=\"pre\"\n", ")\n", "\n", "\n", "y_train = np.array(y_train_targets)\n", "y_test = np.array(y_test_targets)\n", "\n", "\n", "print(\"Train:\", X_train.shape, y_train.shape)\n", "print(\"Test :\", X_test.shape, y_test.shape)" ] }, { "cell_type": "code", "execution_count": 10, "id": "e470e33f", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/keshav/code/cricket-dls/.venv/lib/python3.12/site-packages/keras/src/layers/core/masking.py:48: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.\n", " super().__init__(**kwargs)\n" ] }, { "data": { "text/html": [ "
Model: \"sequential\"\n",
       "
\n" ], "text/plain": [ "\u001b[1mModel: \"sequential\"\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n",
       "┃ Layer (type)                     Output Shape                  Param # ┃\n",
       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n",
       "│ masking (Masking)               │ (None, 120, 10)        │             0 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ bidirectional (Bidirectional)   │ (None, 64)             │        11,008 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dropout (Dropout)               │ (None, 64)             │             0 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense (Dense)                   │ (None, 32)             │         2,080 │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_1 (Dense)                 │ (None, 1)              │            33 │\n",
       "└─────────────────────────────────┴────────────────────────┴───────────────┘\n",
       "
\n" ], "text/plain": [ "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mLayer (type) \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m Param #\u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n", "│ masking (\u001b[38;5;33mMasking\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m120\u001b[0m, \u001b[38;5;34m10\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ bidirectional (\u001b[38;5;33mBidirectional\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m11,008\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dropout (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dense (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m2,080\u001b[0m │\n", "├─────────────────────────────────┼────────────────────────┼───────────────┤\n", "│ dense_1 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m33\u001b[0m │\n", "└─────────────────────────────────┴────────────────────────┴───────────────┘\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
 Total params: 13,121 (51.25 KB)\n",
       "
\n" ], "text/plain": [ "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m13,121\u001b[0m (51.25 KB)\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
 Trainable params: 13,121 (51.25 KB)\n",
       "
\n" ], "text/plain": [ "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m13,121\u001b[0m (51.25 KB)\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
 Non-trainable params: 0 (0.00 B)\n",
       "
\n" ], "text/plain": [ "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m0\u001b[0m (0.00 B)\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "model = Sequential([\n", "\n", " Masking(\n", " mask_value=0.,\n", " input_shape=(120,len(FEATURES))\n", " ),\n", "\n", " Bidirectional(\n", " LSTM(32)\n", " ),\n", "\n", " Dropout(0.25),\n", "\n", " Dense(\n", " 32,\n", " activation=\"relu\"\n", " ),\n", "\n", " Dense(1)\n", "\n", "])\n", "\n", "model.compile(\n", " optimizer=tf.keras.optimizers.Adam(\n", " learning_rate=0.001\n", " ),\n", "\n", " loss=\"mse\",\n", "\n", " metrics=[\n", " \"mae\"\n", " ]\n", ")\n", "\n", "\n", "model.summary()" ] }, { "cell_type": "code", "execution_count": null, "id": "495daca8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Epoch 1/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m32s\u001b[0m 254ms/step - loss: 19128.5391 - mae: 131.3299 - val_loss: 12421.8320 - val_mae: 103.6737 - learning_rate: 0.0010\n", "Epoch 2/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 321ms/step - loss: 6210.3130 - mae: 65.5333 - val_loss: 1969.1512 - val_mae: 33.6969 - learning_rate: 0.0010\n", "Epoch 3/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 321ms/step - loss: 1224.2498 - mae: 24.7959 - val_loss: 525.3225 - val_mae: 15.5029 - learning_rate: 0.0010\n", "Epoch 4/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 322ms/step - loss: 610.5830 - mae: 17.7209 - val_loss: 320.0849 - val_mae: 12.2769 - learning_rate: 0.0010\n", "Epoch 5/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 322ms/step - loss: 483.1981 - mae: 16.2305 - val_loss: 249.6277 - val_mae: 11.1048 - learning_rate: 0.0010\n", "Epoch 6/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m41s\u001b[0m 345ms/step - loss: 434.6412 - mae: 15.6104 - val_loss: 226.9275 - val_mae: 10.6671 - learning_rate: 0.0010\n", "Epoch 7/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m40s\u001b[0m 341ms/step - loss: 414.1877 - mae: 15.3427 - val_loss: 211.9790 - val_mae: 10.3182 - learning_rate: 0.0010\n", "Epoch 8/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m39s\u001b[0m 331ms/step - loss: 395.6093 - mae: 15.0844 - val_loss: 207.4465 - val_mae: 10.3751 - learning_rate: 0.0010\n", "Epoch 9/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m44s\u001b[0m 376ms/step - loss: 385.9764 - mae: 14.9372 - val_loss: 198.0012 - val_mae: 10.2073 - learning_rate: 0.0010\n", "Epoch 10/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m47s\u001b[0m 395ms/step - loss: 374.2676 - mae: 14.7318 - val_loss: 194.2088 - val_mae: 10.1279 - learning_rate: 0.0010\n", "Epoch 11/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m46s\u001b[0m 392ms/step - loss: 367.1200 - mae: 14.5911 - val_loss: 188.0659 - val_mae: 9.9135 - learning_rate: 0.0010\n", "Epoch 12/100\n", "\u001b[1m118/118\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m49s\u001b[0m 412ms/step - loss: 362.3481 - mae: 14.5265 - val_loss: 188.1493 - val_mae: 9.9198 - learning_rate: 0.0010\n", "Epoch 13/100\n", "\u001b[1m 27/118\u001b[0m \u001b[32m━━━━\u001b[0m\u001b[37m━━━━━━━━━━━━━━━━\u001b[0m \u001b[1m41s\u001b[0m 451ms/step - loss: 357.6455 - mae: 14.4617" ] } ], "source": [ "callbacks = [\n", "\n", " EarlyStopping(\n", " patience=10,\n", " restore_best_weights=True\n", " ),\n", "\n", " ReduceLROnPlateau(\n", " patience=5,\n", " factor=0.5\n", " )\n", "\n", "]\n", "\n", "\n", "history = model.fit(\n", "\n", " X_train,\n", " y_train,\n", "\n", " validation_data=(\n", " X_test,\n", " y_test\n", " ),\n", "\n", " epochs=100,\n", "\n", " batch_size=2048,\n", "\n", " callbacks=callbacks,\n", "\n", " verbose=1\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": ".venv (3.14.6)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.13" } }, "nbformat": 4, "nbformat_minor": 5 }