{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "3ed630fa-e47f-4ab4-a290-ca90caae2168",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Final Project: Applying Machine Learning on Stroke Dataset with Feature Selection\n",
    "# Presented to: Dr. Hajar Saleh\n",
    "\n",
    "# دكتورة هاجر صالح المحترمة،\n",
    "# هذا المشروع يهدف إلى تطبيق تقنيات تعلم الآلة على مجموعة بيانات السكتة الدماغية خطوة بخطوة.\n",
    "# الخطوات التي سأتبعها في هذا المشروع هي:\n",
    "# 1. تحميل مجموعة البيانات والتحقق من هيكلها.\n",
    "# 2. التعامل مع القيم المفقودة بشكل مناسب لتحضير البيانات لتعلم الآلة.\n",
    "# 3. تطبيق نموذج Random Forest مع GridSearchCV لإيجاد أفضل المعلمات وتقييم أداء النموذج.\n",
    "# 4. استخدام طريقتين لاختيار الميزات (Chi-Square وRFE) لتحديد أهم الميزات.\n",
    "# 5. تطبيق نموذج Random Forest مرة أخرى على الميزات المختارة وتقييم الأداء.\n",
    "# 6. حفظ الكود ورفعه مع مجموعة البيانات.\n",
    "\n",
    "# النتائج المتوقعة:\n",
    "# - بعد التعامل مع القيم المفقودة، يجب ألا تحتوي البيانات على أي قيم مفقودة.\n",
    "# - نموذج Random Forest من المتوقع أن يعطي دقة وأداء جيد (Accuracy, Precision, Recall, F1 Score).\n",
    "# - اختيار الميزات سيساعدنا في تحديد أهم الميزات التي تؤثر على التنبؤ بالسكتة الدماغية.\n",
    "# - أداء النموذج قد يتحسن أو يظل مشابهًا بعد اختيار الميزات.\n",
    "# سأبدأ الآن بالخطوة الأولى."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "a9745f89-bdea-4eaf-8f71-4ea28e1c21da",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 1: Import the required libraries\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "from sklearn.feature_selection import SelectKBest, chi2, RFE\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.model_selection import train_test_split, GridSearchCV\n",
    "from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "76711dd0-708c-405a-8bf2-7881c5853df2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "First 5 rows of the dataset:\n",
      "      id  gender   age  hypertension  heart_disease  ever_married  work_type  \\\n",
      "0   9046       1  67.0           0.0            NaN             1        2.0   \n",
      "1  51676       0  61.0           0.0            0.0             1        3.0   \n",
      "2  31112       1  80.0           0.0            NaN             1        2.0   \n",
      "3  60182       0  49.0           0.0            0.0             1        2.0   \n",
      "4   1665       0  79.0           NaN            0.0             1        3.0   \n",
      "\n",
      "   Residence_type  avg_glucose_level        bmi  smoking_status  stroke  \n",
      "0               1                NaN  36.600000               1     NaN  \n",
      "1               0                NaN  28.893237               2     NaN  \n",
      "2               0             105.92  32.500000               2     NaN  \n",
      "3               1                NaN  34.400000               3     NaN  \n",
      "4               0                NaN  24.000000               2     NaN  \n",
      "\n",
      "Columns and data types:\n",
      "id                     int64\n",
      "gender                 int64\n",
      "age                  float64\n",
      "hypertension         float64\n",
      "heart_disease        float64\n",
      "ever_married           int64\n",
      "work_type            float64\n",
      "Residence_type         int64\n",
      "avg_glucose_level    float64\n",
      "bmi                  float64\n",
      "smoking_status         int64\n",
      "stroke               float64\n",
      "dtype: object\n"
     ]
    }
   ],
   "source": [
    "# Step 2: Load the dataset and check its structure\n",
    "data = pd.read_csv('healthcare-dataset-stroke-data-cleaned.csv')\n",
    "\n",
    "# Display the first 5 rows\n",
    "print(\"First 5 rows of the dataset:\")\n",
    "print(data.head())\n",
    "\n",
    "# Display the columns and data types\n",
    "print(\"\\nColumns and data types:\")\n",
    "print(data.dtypes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "2bdf3afb-7480-4e93-9918-c0448c77d14d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Dropped the 'id' column.\n",
      "Missing values before handling:\n",
      "gender                 0\n",
      "age                    0\n",
      "hypertension         498\n",
      "heart_disease        276\n",
      "ever_married           0\n",
      "work_type            657\n",
      "Residence_type         0\n",
      "avg_glucose_level    627\n",
      "bmi                  126\n",
      "smoking_status         0\n",
      "stroke               249\n",
      "dtype: int64\n"
     ]
    }
   ],
   "source": [
    "# Step 3: Check for missing values\n",
    "# I will drop the 'id' column if it exists, then check for missing values in all columns including 'stroke'\n",
    "if 'id' in data.columns:\n",
    "    data = data.drop('id', axis=1)\n",
    "    print(\"Dropped the 'id' column.\")\n",
    "else:\n",
    "    print(\"The 'id' column is already dropped or does not exist.\")\n",
    "\n",
    "# Check for missing values\n",
    "print(\"Missing values before handling:\")\n",
    "print(data.isnull().sum())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "9850c7a9-a4bc-42f1-b4f5-d5a1139ec333",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Number of rows before dropping missing values in 'stroke': 4861\n",
      "Number of rows after dropping missing values in 'stroke': 4861\n",
      "Missing values in 'stroke' after dropping: 0\n",
      "Missing values after handling:\n",
      "gender               0\n",
      "age                  0\n",
      "hypertension         0\n",
      "heart_disease        0\n",
      "ever_married         0\n",
      "work_type            0\n",
      "Residence_type       0\n",
      "avg_glucose_level    0\n",
      "bmi                  0\n",
      "smoking_status       0\n",
      "stroke               0\n",
      "dtype: int64\n"
     ]
    }
   ],
   "source": [
    "# Step 4: Handle missing values\n",
    "# First, handle missing values in the target column 'stroke'\n",
    "# I will drop rows where 'stroke' has missing values because it is the target column\n",
    "print(\"Number of rows before dropping missing values in 'stroke':\", len(data))\n",
    "data = data.dropna(subset=['stroke'])\n",
    "print(\"Number of rows after dropping missing values in 'stroke':\", len(data))\n",
    "\n",
    "# Double-check missing values in 'stroke' to confirm they are all handled\n",
    "print(\"Missing values in 'stroke' after dropping:\", data['stroke'].isnull().sum())\n",
    "if data['stroke'].isnull().sum() > 0:\n",
    "    print(\"Warning: There are still missing values in 'stroke'. This should not happen!\")\n",
    "\n",
    "# Now handle missing values in the features\n",
    "# Fill missing values in numerical columns with the mean\n",
    "data['avg_glucose_level'] = data['avg_glucose_level'].fillna(data['avg_glucose_level'].mean())\n",
    "data['bmi'] = data['bmi'].fillna(data['bmi'].mean())\n",
    "\n",
    "# Fill missing values in categorical columns with the mode\n",
    "data['hypertension'] = data['hypertension'].fillna(data['hypertension'].mode()[0])\n",
    "data['heart_disease'] = data['heart_disease'].fillna(data['heart_disease'].mode()[0])\n",
    "data['work_type'] = data['work_type'].fillna(data['work_type'].mode()[0])\n",
    "data['smoking_status'] = data['smoking_status'].fillna(data['smoking_status'].mode()[0])\n",
    "\n",
    "# Check for missing values after handling\n",
    "print(\"Missing values after handling:\")\n",
    "print(data.isnull().sum())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "e940db1e-4cf4-421b-9bc9-92fffe4ff140",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Checking for missing values in y_train:\n",
      "0\n",
      "Checking for missing values in y_test:\n",
      "0\n",
      "Shape of X_train: (3888, 10)\n",
      "Shape of X_test: (973, 10)\n",
      "Shape of y_train: (3888,)\n",
      "Shape of y_test: (973,)\n"
     ]
    }
   ],
   "source": [
    "# Step 5: Split the data into features and target, then into training and testing sets\n",
    "# Separate features (X) and target (y)\n",
    "X = data.drop('stroke', axis=1)\n",
    "y = data['stroke']\n",
    "\n",
    "# Split the data into training (80%) and testing (20%) sets\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n",
    "\n",
    "# Check for missing values in y_train and y_test\n",
    "print(\"Checking for missing values in y_train:\")\n",
    "print(y_train.isnull().sum())\n",
    "print(\"Checking for missing values in y_test:\")\n",
    "print(y_test.isnull().sum())\n",
    "\n",
    "# Display the shapes of the training and testing sets\n",
    "print(\"Shape of X_train:\", X_train.shape)\n",
    "print(\"Shape of X_test:\", X_test.shape)\n",
    "print(\"Shape of y_train:\", y_train.shape)\n",
    "print(\"Shape of y_test:\", y_test.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "3ee179f6-d0fd-4879-9b88-731d4359430a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Evaluation Results (Before Feature Selection):\n",
      "Model Name: RandomForestClassifier (Before Feature Selection)\n",
      "Accuracy: 1.0000\n",
      "Precision: 1.0000\n",
      "Recall: 1.0000\n",
      "F1 Score: 1.0000\n",
      "\n",
      "Best hyperparameters found by GridSearchCV (Before Feature Selection):\n",
      "{'max_depth': 10, 'min_samples_leaf': 1, 'min_samples_split': 2, 'n_estimators': 100}\n"
     ]
    }
   ],
   "source": [
    "# Step 6: Apply Random Forest with GridSearchCV Before Feature Selection\n",
    "# Initialize the Random Forest Classifier\n",
    "rf_classifier = RandomForestClassifier(random_state=42)\n",
    "\n",
    "# Define the parameter grid for GridSearchCV\n",
    "param_grid = {\n",
    "    'n_estimators': [100, 200],\n",
    "    'max_depth': [10, 20, None],\n",
    "    'min_samples_split': [2, 5],\n",
    "    'min_samples_leaf': [1, 2]\n",
    "}\n",
    "\n",
    "# Initialize GridSearchCV with 5-fold cross-validation\n",
    "grid_search = GridSearchCV(estimator=rf_classifier, param_grid=param_grid, cv=5, scoring='accuracy', n_jobs=-1)\n",
    "\n",
    "# Fit the grid search to the data\n",
    "grid_search.fit(X_train, y_train)\n",
    "\n",
    "# Get the best estimator from grid search\n",
    "best_rf_classifier = grid_search.best_estimator_\n",
    "\n",
    "# Make predictions using the best model\n",
    "y_pred = best_rf_classifier.predict(X_test)\n",
    "\n",
    "# Evaluate the model\n",
    "evaluation_results = evaluate_model('RandomForestClassifier (Before Feature Selection)', y_test, y_pred)\n",
    "\n",
    "# Print the evaluation results\n",
    "print(\"Evaluation Results (Before Feature Selection):\")\n",
    "for key, value in evaluation_results.items():\n",
    "    print(f\"{key}: {value:.4f}\" if isinstance(value, float) else f\"{key}: {value}\")\n",
    "\n",
    "# Print the best parameters found by grid search\n",
    "print(\"\\nBest hyperparameters found by GridSearchCV (Before Feature Selection):\")\n",
    "print(grid_search.best_params_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "abe017bb-db7c-4788-8e44-b7ccf89c487b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Checking for missing values in X_train before Chi-Square:\n",
      "gender               0\n",
      "age                  0\n",
      "hypertension         0\n",
      "heart_disease        0\n",
      "ever_married         0\n",
      "work_type            0\n",
      "Residence_type       0\n",
      "avg_glucose_level    0\n",
      "bmi                  0\n",
      "smoking_status       0\n",
      "dtype: int64\n",
      "\n",
      "Checking for missing values in X_test before Chi-Square:\n",
      "gender               0\n",
      "age                  0\n",
      "hypertension         0\n",
      "heart_disease        0\n",
      "ever_married         0\n",
      "work_type            0\n",
      "Residence_type       0\n",
      "avg_glucose_level    0\n",
      "bmi                  0\n",
      "smoking_status       0\n",
      "dtype: int64\n",
      "\n",
      "Checking for missing values in X_train_non_negative after clipping:\n",
      "gender               0\n",
      "age                  0\n",
      "hypertension         0\n",
      "heart_disease        0\n",
      "ever_married         0\n",
      "work_type            0\n",
      "Residence_type       0\n",
      "avg_glucose_level    0\n",
      "bmi                  0\n",
      "smoking_status       0\n",
      "dtype: int64\n",
      "\n",
      "Checking for missing values in X_test_non_negative after clipping:\n",
      "gender               0\n",
      "age                  0\n",
      "hypertension         0\n",
      "heart_disease        0\n",
      "ever_married         0\n",
      "work_type            0\n",
      "Residence_type       0\n",
      "avg_glucose_level    0\n",
      "bmi                  0\n",
      "smoking_status       0\n",
      "dtype: int64\n",
      "\n",
      "Feature Scores (Chi-Square Test):\n",
      "             Feature  Score\n",
      "0             gender    NaN\n",
      "1                age    NaN\n",
      "2       hypertension    NaN\n",
      "3      heart_disease    NaN\n",
      "4       ever_married    NaN\n",
      "5          work_type    NaN\n",
      "6     Residence_type    NaN\n",
      "7  avg_glucose_level    NaN\n",
      "8                bmi    NaN\n",
      "9     smoking_status    NaN\n"
     ]
    }
   ],
   "source": [
    "# Step 7: Feature Selection - Method 1 (Chi-Square Test)\n",
    "# Select the top 5 features using chi2\n",
    "# First, I will double-check for any missing values in X_train and X_test\n",
    "print(\"Checking for missing values in X_train before Chi-Square:\")\n",
    "print(X_train.isnull().sum())\n",
    "print(\"\\nChecking for missing values in X_test before Chi-Square:\")\n",
    "print(X_test.isnull().sum())\n",
    "\n",
    "# If there are any missing values, I will handle them again\n",
    "X_train = X_train.fillna(X_train.mean())\n",
    "X_test = X_test.fillna(X_test.mean())\n",
    "\n",
    "# Now, I will ensure all values are non-negative for Chi-Square\n",
    "X_train_non_negative = X_train.copy()\n",
    "X_test_non_negative = X_test.copy()\n",
    "for col in X_train_non_negative.columns:\n",
    "    X_train_non_negative[col] = X_train_non_negative[col].clip(lower=0)\n",
    "    X_test_non_negative[col] = X_test_non_negative[col].clip(lower=0)\n",
    "\n",
    "# Double-check again to make sure there are no NaN values after clipping\n",
    "print(\"\\nChecking for missing values in X_train_non_negative after clipping:\")\n",
    "print(X_train_non_negative.isnull().sum())\n",
    "print(\"\\nChecking for missing values in X_test_non_negative after clipping:\")\n",
    "print(X_test_non_negative.isnull().sum())\n",
    "\n",
    "# Apply SelectKBest with chi2 to select the top 5 features\n",
    "select_feature = SelectKBest(chi2, k=5).fit(X_train_non_negative, y_train)\n",
    "\n",
    "# Create a DataFrame to store feature scores\n",
    "feature_scores = pd.DataFrame({'Feature': X.columns, 'Score': select_feature.scores_})\n",
    "print(\"\\nFeature Scores (Chi-Square Test):\")\n",
    "print(feature_scores)\n",
    "\n",
    "# Transform the training and testing sets with selected features\n",
    "X_train_selected = select_feature.transform(X_train_non_negative)\n",
    "X_test_selected = select_feature.transform(X_test_non_negative)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "7d13ca57-b95c-4d74-867c-3b1f39bee22c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Evaluation Results (Chi-Square Selected Features):\n",
      "Model Name: RandomForestClassifier (Chi-Square)\n",
      "Accuracy: 1.0000\n",
      "Precision: 1.0000\n",
      "Recall: 1.0000\n",
      "F1 Score: 1.0000\n",
      "\n",
      "Best hyperparameters found by GridSearchCV (Chi-Square):\n",
      "{'max_depth': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'n_estimators': 100}\n"
     ]
    }
   ],
   "source": [
    "# Step 8: Apply Random Forest on selected features (Chi-Square)\n",
    "# Initialize GridSearchCV with the same parameters\n",
    "grid_search = GridSearchCV(estimator=rf_classifier, param_grid=param_grid, cv=5, scoring='accuracy', n_jobs=-1)\n",
    "\n",
    "# Fit the grid search to the selected features\n",
    "grid_search.fit(X_train_selected, y_train)\n",
    "\n",
    "# Get the best estimator from grid search\n",
    "best_rf_classifier = grid_search.best_estimator_\n",
    "\n",
    "# Make predictions using the best model\n",
    "y_pred = best_rf_classifier.predict(X_test_selected)\n",
    "\n",
    "# Evaluate the model\n",
    "evaluation_results = evaluate_model('RandomForestClassifier (Chi-Square)', y_test, y_pred)\n",
    "\n",
    "# Print the evaluation results\n",
    "print(\"Evaluation Results (Chi-Square Selected Features):\")\n",
    "for key, value in evaluation_results.items():\n",
    "    print(f\"{key}: {value:.4f}\" if isinstance(value, float) else f\"{key}: {value}\")\n",
    "\n",
    "# Print the best parameters found by grid search\n",
    "print(\"\\nBest hyperparameters found by GridSearchCV (Chi-Square):\")\n",
    "print(grid_search.best_params_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "857d4eb6-722d-4ca5-96cd-6e21f2f13b9b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Selected Features (RFE): [5 6 7 8 9]\n"
     ]
    }
   ],
   "source": [
    "# Step 9: Feature Selection - Method 2 (Recursive Feature Elimination - RFE)\n",
    "# Use RFE to select the top 5 features\n",
    "rfe_selector = RFE(estimator=RandomForestClassifier(random_state=42), n_features_to_select=5, step=1)\n",
    "\n",
    "# Fit RFE to the training data\n",
    "X_train_selected_rfe = rfe_selector.fit_transform(X_train, y_train)\n",
    "X_test_selected_rfe = rfe_selector.transform(X_test)\n",
    "\n",
    "# Get the selected feature indices\n",
    "selected_features = rfe_selector.get_support(indices=True)\n",
    "print(\"\\nSelected Features (RFE):\", selected_features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "bf49957f-56fc-4a8e-ab0c-b2b106e34fae",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Evaluation Results (RFE Selected Features):\n",
      "Model Name: RandomForestClassifier (RFE)\n",
      "Accuracy: 1.0000\n",
      "Precision: 1.0000\n",
      "Recall: 1.0000\n",
      "F1 Score: 1.0000\n",
      "\n",
      "Best hyperparameters found by GridSearchCV (RFE):\n",
      "{'max_depth': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'n_estimators': 100}\n"
     ]
    }
   ],
   "source": [
    "# Step 10: Apply Random Forest on selected features (RFE)\n",
    "# Initialize GridSearchCV with the same parameters\n",
    "grid_search = GridSearchCV(estimator=rf_classifier, param_grid=param_grid, cv=5, scoring='accuracy', n_jobs=-1)\n",
    "\n",
    "# Fit the grid search to the selected features\n",
    "grid_search.fit(X_train_selected_rfe, y_train)\n",
    "\n",
    "# Get the best estimator from grid search\n",
    "best_rf_classifier = grid_search.best_estimator_\n",
    "\n",
    "# Make predictions using the best model\n",
    "y_pred = best_rf_classifier.predict(X_test_selected_rfe)\n",
    "\n",
    "# Evaluate the model\n",
    "evaluation_results = evaluate_model('RandomForestClassifier (RFE)', y_test, y_pred)\n",
    "\n",
    "# Print the evaluation results\n",
    "print(\"Evaluation Results (RFE Selected Features):\")\n",
    "for key, value in evaluation_results.items():\n",
    "    print(f\"{key}: {value:.4f}\" if isinstance(value, float) else f\"{key}: {value}\")\n",
    "\n",
    "# Print the best parameters found by grid search\n",
    "print(\"\\nBest hyperparameters found by GridSearchCV (RFE):\")\n",
    "print(grid_search.best_params_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "5d016bee-47ca-42dc-bfaf-0317212734f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#  #  الخاتمة وحفظ الكود (Conclusion and Save the Code)  \n",
    "# دكتورة هاجر صالح المحترمة،  \n",
    "# لقد أكملت جميع خطوات المشروع:  \n",
    "# - تعاملت مع القيم المفقودة (Missing Values) بملء الأعمدة العددية (Numerical Columns) بالمتوسط (Mean) والأعمدة التصنيفية (Categorical Columns) بالقيمة الأكثر تكرارًا (Mode).  \n",
    "# - قمت بحذف الصفوف (Rows) التي تحتوي على قيم مفقودة في العمود الهدف 'stroke' (Target Column 'stroke') لتجنب التحيز (Bias).  \n",
    "# - طبقت نموذج Random Forest (Random Forest Model) مع GridSearchCV (GridSearchCV) قبل وبعد اختيار الميزات (Feature Selection).  \n",
    "# - استخدمت طريقتين لاختيار الميزات (Feature Selection Methods): (Chi-Square وRFE) لاختيار أفضل 5 ميزات (Top 5 Features).  \n",
    "# - قيّمت أداء النموذج (Model Performance) في جميع الحالات باستخدام الدقة (Accuracy)، الدقة الموجبة (Precision)، الاستدعاء (Recall)، ومقياس F1 (F1 Score).  \n",
    "# النتائج تُظهر أداء النموذج (Model Performance) مع وبدون اختيار الميزات (With and Without Feature Selection).  \n",
    "# سأحفظ هذا الكود باسم 'Hassan_Alzahrani.py' وأرفعه مع مجموعة البيانات (Dataset) 'healthcare-dataset-stroke-data-cleaned.csv'.  \n",
    "# شكرًا لمراجعتكِ لمشروعي!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f9e5d8b9-e67e-41ed-b29c-8713e18b67b9",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
