{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "1f7a94b2-11b0-4147-b10f-7ee27f3f1931", "metadata": {}, "outputs": [], "source": [ "# Get your tools ready\n", "import pandas as pd\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "from statsmodels.tsa.seasonal import seasonal_decompose\n", "from statsmodels.tsa.holtwinters import ExponentialSmoothing\n", "from sklearn.metrics import mean_absolute_error, mean_squared_error\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "# Make our plots look nice\n", "plt.style.use('seaborn-v0_8-darkgrid')\n", "sns.set_palette(\"husl\")" ] }, { "cell_type": "code", "execution_count": null, "id": "1352968e-7cf4-4f6f-b3d8-7ff9125ab9ec", "metadata": {}, "outputs": [], "source": [ "# Generate synthetic ATM withdrawal data\n", "np.random.seed(42)\n", "\n", "# Create date range - 2 years of daily data\n", "dates = pd.date_range(start='2022-01-01', end='2023-12-31', freq='D')\n", "\n", "# Base demand pattern\n", "base_demand = 50000 # Average daily withdrawal amount\n", "\n", "# Create realistic patterns\n", "time_trend = np.linspace(0, 1000, len(dates)) # Slight growth over time\n", "\n", "# Weekly pattern (weekends are busy!)\n", "weekly_pattern = []\n", "for date in dates:\n", " if date.dayofweek in [4, 5]: # Friday, Saturday\n", " weekly_pattern.append(1.4)\n", " elif date.dayofweek == 6: # Sunday\n", " weekly_pattern.append(1.2)\n", " else: # Weekdays\n", " weekly_pattern.append(0.9)\n", "\n", "# Monthly pattern (paydays and month-end)\n", "monthly_pattern = []\n", "for date in dates:\n", " if date.day in [1, 2, 15, 16, 28, 29, 30, 31]: # Paydays\n", " monthly_pattern.append(1.3)\n", " else:\n", " monthly_pattern.append(0.95)\n", "\n", "# Holiday spikes\n", "holiday_effect = []\n", "for date in dates:\n", " if date.month == 12 and date.day in range(20, 32): # Christmas season\n", " holiday_effect.append(1.5)\n", " elif date.month == 11 and date.day in range(23, 30): # Black Friday week\n", " holiday_effect.append(1.3)\n", " else:\n", " holiday_effect.append(1.0)\n", "\n", "# Combine all patterns with some randomness\n", "daily_withdrawals = (\n", " base_demand + time_trend +\n", " base_demand * np.array(weekly_pattern) * 0.3 +\n", " base_demand * np.array(monthly_pattern) * 0.2 +\n", " base_demand * np.array(holiday_effect) * 0.2 +\n", " np.random.normal(0, 5000, len(dates)) # Random noise\n", ")\n", "\n", "# Create DataFrame\n", "atm_data = pd.DataFrame({\n", " 'date': dates,\n", " 'withdrawals': daily_withdrawals\n", "})\n", "\n", "print(\"ATM Data Summary:\")\n", "print(f\"Average daily withdrawal: ${atm_data['withdrawals'].mean():,.0f}\")\n", "print(f\"Peak day: ${atm_data['withdrawals'].max():,.0f}\")\n", "print(f\"Minimum day: ${atm_data['withdrawals'].min():,.0f}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "abf3f039-2787-4e5c-a7fb-51bb006e3c37", "metadata": {}, "outputs": [], "source": [ "# Let's see what our data looks like\n", "fig, axes = plt.subplots(2, 1, figsize=(15, 10))\n", "\n", "# Full time series\n", "axes[0].plot(atm_data['date'], atm_data['withdrawals'], linewidth=0.5)\n", "axes[0].set_title('Daily ATM Withdrawals - 2 Year View', fontsize=14, fontweight='bold')\n", "axes[0].set_ylabel('Withdrawal Amount ($)', fontsize=12)\n", "axes[0].set_xlabel('Date', fontsize=12)\n", "\n", "# Zoom in on last 3 months to see patterns\n", "last_90_days = atm_data.tail(90)\n", "axes[1].plot(last_90_days['date'], last_90_days['withdrawals'], linewidth=2, marker='o', markersize=3)\n", "axes[1].set_title('Last 90 Days - See the Weekly Pattern?', fontsize=14, fontweight='bold')\n", "axes[1].set_ylabel('Withdrawal Amount ($)', fontsize=12)\n", "axes[1].set_xlabel('Date', fontsize=12)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "id": "eee096b8-346a-4005-8709-5e3725692c5f", "metadata": {}, "outputs": [], "source": [ "# Set date as index for time series analysis\n", "atm_data.set_index('date', inplace=True)\n", "\n", "# Decompose the time series\n", "decomposition = seasonal_decompose(atm_data['withdrawals'], model='additive', period=30)\n", "\n", "# Plot decomposition\n", "fig, axes = plt.subplots(4, 1, figsize=(15, 12))\n", "\n", "atm_data['withdrawals'].plot(ax=axes[0], title='Original ATM Withdrawals')\n", "axes[0].set_ylabel('Amount ($)')\n", "\n", "decomposition.trend.plot(ax=axes[1], title='Trend - Growing Slowly Over Time')\n", "axes[1].set_ylabel('Amount ($)')\n", "\n", "decomposition.seasonal.plot(ax=axes[2], title='Seasonal Pattern - Monthly Cycle')\n", "axes[2].set_ylabel('Amount ($)')\n", "\n", "decomposition.resid.plot(ax=axes[3], title='Residuals - The Random Stuff')\n", "axes[3].set_ylabel('Amount ($)')\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\n📊 What This Decomposition Tells Us:\")\n", "print(\"- Trend: Withdrawals are growing about $500/month - inflation and customer growth\")\n", "print(\"- Seasonal: Clear monthly pattern with payday spikes\")\n", "print(\"- Residuals: Mostly random = good! Our patterns capture the signal\")" ] }, { "cell_type": "code", "execution_count": null, "id": "9c3ec29d-5780-4a28-8e6f-d3f35e0bd969", "metadata": {}, "outputs": [], "source": [ "# Split data: last 60 days for testing\n", "train_size = len(atm_data) - 60\n", "train_data = atm_data[:train_size]\n", "test_data = atm_data[train_size:]\n", "\n", "print(f\"Training on {train_size} days, testing on 60 days\")\n", "\n", "# Build Exponential Smoothing model\n", "# We're telling it: \"Hey, we have trend AND seasonal patterns\"\n", "model = ExponentialSmoothing(\n", " train_data['withdrawals'],\n", " trend='add',\n", " seasonal='add',\n", " seasonal_periods=30 # Monthly pattern\n", ")\n", "\n", "# Fit the model\n", "fitted_model = model.fit()\n", "\n", "# Make predictions\n", "predictions = fitted_model.forecast(steps=60)\n", "\n", "# Calculate accuracy metrics\n", "mae = mean_absolute_error(test_data['withdrawals'], predictions)\n", "rmse = np.sqrt(mean_squared_error(test_data['withdrawals'], predictions))\n", "mape = np.mean(np.abs((test_data['withdrawals'] - predictions) / test_data['withdrawals'])) * 100\n", "\n", "print(f\"\\n🎯 Model Performance:\")\n", "print(f\"Mean Absolute Error: ${mae:,.0f} (typical prediction error)\")\n", "print(f\"RMSE: ${rmse:,.0f}\")\n", "print(f\"MAPE: {mape:.1f}% (we're off by about {mape:.1f}% on average)\")\n", "print(f\"\\nIn practical terms: We can predict daily cash needs within ${mae:,.0f}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "9861e6dc-423c-4985-8bac-e822591c22e5", "metadata": {}, "outputs": [], "source": [ "# Plot actual vs predicted\n", "plt.figure(figsize=(15, 6))\n", "\n", "# Plot full series with train/test split\n", "plt.plot(train_data.index, train_data['withdrawals'], label='Historical Data', alpha=0.7)\n", "plt.plot(test_data.index, test_data['withdrawals'], label='Actual Test Data', linewidth=2)\n", "plt.plot(test_data.index, predictions, label='Our Predictions', linewidth=2, linestyle='--')\n", "\n", "plt.axvline(x=train_data.index[-1], color='red', linestyle=':', alpha=0.5, label='Forecast Start')\n", "plt.title('ATM Cash Demand Forecast - How Did We Do?', fontsize=14, fontweight='bold')\n", "plt.xlabel('Date', fontsize=12)\n", "plt.ylabel('Daily Withdrawals ($)', fontsize=12)\n", "plt.legend(loc='best')\n", "plt.grid(True, alpha=0.3)\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "id": "6257f1bb-a125-41b7-a2d7-893e4ab5efe8", "metadata": {}, "outputs": [], "source": [ "# Forecast next 30 days into the future\n", "future_forecast = fitted_model.forecast(steps=30)\n", "future_dates = pd.date_range(start=atm_data.index[-1] + pd.Timedelta(days=1), periods=30)\n", "\n", "# Create a forecast DataFrame with confidence intervals\n", "forecast_df = pd.DataFrame({\n", " 'date': future_dates,\n", " 'forecast': future_forecast,\n", " 'lower_bound': future_forecast - 2 * rmse, # 95% confidence interval\n", " 'upper_bound': future_forecast + 2 * rmse\n", "})\n", "\n", "print(\"\\n💰 Next Week's Cash Requirements:\")\n", "print(forecast_df.head(7).to_string())\n", "\n", "# Visualize with confidence intervals\n", "plt.figure(figsize=(15, 6))\n", "\n", "# Recent history\n", "recent_history = atm_data.tail(60)\n", "plt.plot(recent_history.index, recent_history['withdrawals'], \n", " label='Recent History', linewidth=2, alpha=0.7)\n", "\n", "# Forecast with confidence interval\n", "plt.plot(forecast_df['date'], forecast_df['forecast'], \n", " label='Forecast', linewidth=2, color='red')\n", "plt.fill_between(forecast_df['date'], \n", " forecast_df['lower_bound'], \n", " forecast_df['upper_bound'], \n", " alpha=0.2, color='red', label='95% Confidence')\n", "\n", "plt.title('30-Day ATM Cash Forecast with Confidence Intervals', fontsize=14, fontweight='bold')\n", "plt.xlabel('Date', fontsize=12)\n", "plt.ylabel('Daily Withdrawals ($)', fontsize=12)\n", "plt.legend(loc='best')\n", "plt.grid(True, alpha=0.3)\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(f\"\\n📋 Actionable Insights:\")\n", "print(f\"• Load ${forecast_df['forecast'].sum():,.0f} total for next 30 days\")\n", "print(f\"• Peak day needs: ${forecast_df['forecast'].max():,.0f}\")\n", "print(f\"• Keep ${forecast_df['upper_bound'].max():,.0f} buffer for busy days\")" ] }, { "cell_type": "markdown", "id": "47aaaeb9-cc16-4acd-b494-7e9c5ebf88ef", "metadata": {}, "source": [ "Use Case 2: Manufacturing - Predictive Maintenance\n", "Now let's switch gears. You're running a factory with expensive machines. When they break unexpectedly, you lose $10,000/hour. Let's catch problems before they happen." ] }, { "cell_type": "code", "execution_count": null, "id": "3bab96b6-2d9b-4e2d-b843-43c645531e09", "metadata": {}, "outputs": [], "source": [ "# Create synthetic machine temperature data\n", "np.random.seed(42)\n", "\n", "# 6 months of hourly readings\n", "hours = pd.date_range(start='2023-07-01', end='2023-12-31', freq='h')\n", "\n", "# Normal operating temperature: 70°C\n", "base_temp = 70\n", "\n", "# Daily pattern (cooler at night)\n", "daily_pattern = []\n", "for hour in hours:\n", " hour_of_day = hour.hour\n", " if 6 <= hour_of_day <= 18: # Day shift\n", " daily_pattern.append(5) # Hotter during operation\n", " else:\n", " daily_pattern.append(-2) # Cooler at night\n", "\n", "# Degradation pattern - machine slowly wearing out\n", "degradation = np.linspace(0, 15, len(hours)) # Gradual increase\n", "\n", "# Random fluctuations\n", "noise = np.random.normal(0, 2, len(hours))\n", "\n", "# Add some anomalies (early warning signs)\n", "anomalies = np.zeros(len(hours))\n", "# Small spikes starting 2 weeks before failure\n", "for i in range(len(hours) - 336, len(hours) - 168): # 2 weeks to 1 week before end\n", " if np.random.random() > 0.9:\n", " anomalies[i] = np.random.uniform(5, 10)\n", "\n", "# Bigger spikes in final week\n", "for i in range(len(hours) - 168, len(hours)): # Last week\n", " if np.random.random() > 0.7:\n", " anomalies[i] = np.random.uniform(10, 25)\n", "\n", "# Combine everything\n", "temperature = base_temp + np.array(daily_pattern) + degradation + noise + anomalies\n", "\n", "# Create DataFrame\n", "machine_data = pd.DataFrame({\n", " 'timestamp': hours,\n", " 'temperature': temperature,\n", " 'hour': hours.hour,\n", " 'day_of_week': hours.dayofweek\n", "})\n", "\n", "print(\"Machine Temperature Data Summary:\")\n", "print(f\"Normal operating range: 68-75°C\")\n", "print(f\"Current average (last 24h): {machine_data.tail(24)['temperature'].mean():.1f}°C\")\n", "print(f\"Maximum recorded: {machine_data['temperature'].max():.1f}°C\")" ] }, { "cell_type": "code", "execution_count": null, "id": "c41a8ddd-bc51-4ebf-bc6c-e93eaaecbc70", "metadata": {}, "outputs": [], "source": [ "# Set timestamp as index\n", "machine_data.set_index('timestamp', inplace=True)\n", "\n", "# Create multiple views of the data\n", "fig, axes = plt.subplots(3, 1, figsize=(15, 12))\n", "\n", "# Full timeline\n", "axes[0].plot(machine_data.index, machine_data['temperature'], linewidth=0.5, alpha=0.7)\n", "axes[0].axhline(y=75, color='orange', linestyle='--', label='Warning Threshold')\n", "axes[0].axhline(y=85, color='red', linestyle='--', label='Critical Threshold')\n", "axes[0].set_title('6 Months of Machine Temperature - See the Degradation?', fontsize=14, fontweight='bold')\n", "axes[0].set_ylabel('Temperature (°C)', fontsize=12)\n", "axes[0].legend()\n", "\n", "# Last month - zoomed in\n", "last_month = machine_data.tail(30*24)\n", "axes[1].plot(last_month.index, last_month['temperature'], linewidth=1)\n", "axes[1].axhline(y=75, color='orange', linestyle='--', label='Warning')\n", "axes[1].axhline(y=85, color='red', linestyle='--', label='Critical')\n", "axes[1].set_title('Last 30 Days - Anomalies Increasing!', fontsize=14, fontweight='bold')\n", "axes[1].set_ylabel('Temperature (°C)', fontsize=12)\n", "axes[1].legend()\n", "\n", "# Last week - see the problem clearly\n", "last_week = machine_data.tail(7*24)\n", "axes[2].plot(last_week.index, last_week['temperature'], linewidth=2, marker='o', markersize=3)\n", "axes[2].axhline(y=75, color='orange', linestyle='--', label='Warning')\n", "axes[2].axhline(y=85, color='red', linestyle='--', label='Critical')\n", "axes[2].set_title('Last 7 Days - Houston, We Have a Problem!', fontsize=14, fontweight='bold')\n", "axes[2].set_ylabel('Temperature (°C)', fontsize=12)\n", "axes[2].set_xlabel('Timestamp', fontsize=12)\n", "axes[2].legend()\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "id": "9fd33f9b-1994-4426-b998-aadab56968d9", "metadata": {}, "outputs": [], "source": [ "# Calculate rolling statistics for anomaly detection\n", "window_size = 24 # 24-hour window\n", "\n", "machine_data['rolling_mean'] = machine_data['temperature'].rolling(window=window_size, center=True).mean()\n", "machine_data['rolling_std'] = machine_data['temperature'].rolling(window=window_size, center=True).std()\n", "\n", "# Define anomaly thresholds (2 and 3 standard deviations)\n", "machine_data['upper_bound_2std'] = machine_data['rolling_mean'] + 2 * machine_data['rolling_std']\n", "machine_data['upper_bound_3std'] = machine_data['rolling_mean'] + 3 * machine_data['rolling_std']\n", "\n", "# Flag anomalies\n", "machine_data['warning'] = machine_data['temperature'] > machine_data['upper_bound_2std']\n", "machine_data['critical'] = machine_data['temperature'] > machine_data['upper_bound_3std']\n", "\n", "# Count anomalies over time\n", "anomaly_summary = machine_data.resample('D')[['warning', 'critical']].sum()\n", "\n", "print(\"🚨 Anomaly Detection Results:\")\n", "print(f\"Total warnings (2σ): {machine_data['warning'].sum()} hours\")\n", "print(f\"Total critical (3σ): {machine_data['critical'].sum()} hours\")\n", "print(\"\\nDaily Anomaly Count (Last 14 Days):\")\n", "print(anomaly_summary.tail(14).to_string())" ] }, { "cell_type": "code", "execution_count": null, "id": "2e2c322b-1544-48e0-98e3-17dfc3707efb", "metadata": {}, "outputs": [], "source": [ "# Prepare features for prediction\n", "# We'll predict if maintenance is needed in next 24 hours\n", "\n", "# Feature engineering\n", "machine_data['temp_change_1h'] = machine_data['temperature'].diff(1)\n", "machine_data['temp_change_24h'] = machine_data['temperature'].diff(24)\n", "machine_data['max_temp_24h'] = machine_data['temperature'].rolling(window=24).max()\n", "machine_data['std_temp_24h'] = machine_data['temperature'].rolling(window=24).std()\n", "machine_data['anomaly_count_24h'] = machine_data['warning'].rolling(window=24).sum()\n", "\n", "# Create target: Will temperature exceed critical in next 24 hours?\n", "machine_data['needs_maintenance'] = (\n", " machine_data['temperature'].shift(-24).rolling(window=24).max() > 85\n", ").astype(int)\n", "\n", "# Drop NaN values\n", "model_data = machine_data.dropna()\n", "\n", "# Split features and target\n", "feature_cols = ['temperature', 'temp_change_1h', 'temp_change_24h', \n", " 'max_temp_24h', 'std_temp_24h', 'anomaly_count_24h']\n", "X = model_data[feature_cols]\n", "y = model_data['needs_maintenance']\n", "\n", "# Train/test split (last 2 weeks for testing)\n", "split_point = len(X) - 14*24\n", "X_train, X_test = X[:split_point], X[split_point:]\n", "y_train, y_test = y[:split_point], y[split_point:]\n", "\n", "# Simple decision rules (you'd use ML in production)\n", "def predict_maintenance(row):\n", " if row['anomaly_count_24h'] > 10:\n", " return 1 # High risk\n", " elif row['max_temp_24h'] > 82:\n", " return 1 # Temperature too high\n", " elif row['std_temp_24h'] > 5:\n", " return 1 # Too volatile\n", " else:\n", " return 0 # Normal\n", "\n", "# Apply predictions\n", "test_predictions = X_test.apply(predict_maintenance, axis=1)\n", "\n", "# Evaluate\n", "from sklearn.metrics import classification_report\n", "\n", "print(\"\\n🔧 Maintenance Prediction Performance:\")\n", "print(classification_report(y_test, test_predictions, \n", " target_names=['Normal', 'Needs Maintenance']))" ] }, { "cell_type": "code", "execution_count": null, "id": "57d389e2-2af1-463d-b2b8-3600bf6d26c0", "metadata": {}, "outputs": [], "source": [ "# Simulate real-time monitoring\n", "fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n", "\n", "# Current temperature trend\n", "recent = machine_data.tail(168) # Last week\n", "axes[0, 0].plot(recent.index, recent['temperature'], linewidth=2)\n", "axes[0, 0].axhline(y=75, color='orange', linestyle='--', alpha=0.5)\n", "axes[0, 0].axhline(y=85, color='red', linestyle='--', alpha=0.5)\n", "axes[0, 0].set_title('Temperature Trend (Last 7 Days)', fontsize=12, fontweight='bold')\n", "axes[0, 0].set_ylabel('Temperature (°C)')\n", "\n", "# Anomaly frequency\n", "daily_anomalies = machine_data.resample('D')['warning'].sum().tail(30)\n", "axes[0, 1].bar(daily_anomalies.index, daily_anomalies.values, \n", " color=['red' if x > 10 else 'orange' if x > 5 else 'green' \n", " for x in daily_anomalies.values])\n", "axes[0, 1].set_title('Daily Anomalies (Last 30 Days)', fontsize=12, fontweight='bold')\n", "axes[0, 1].set_ylabel('Anomaly Count')\n", "\n", "# Temperature distribution comparison\n", "axes[1, 0].hist(machine_data.head(1000)['temperature'], bins=30, alpha=0.5, \n", " label='Normal Period', color='green')\n", "axes[1, 0].hist(machine_data.tail(1000)['temperature'], bins=30, alpha=0.5, \n", " label='Recent Period', color='red')\n", "axes[1, 0].set_title('Temperature Distribution Shift', fontsize=12, fontweight='bold')\n", "axes[1, 0].set_xlabel('Temperature (°C)')\n", "axes[1, 0].set_ylabel('Frequency')\n", "axes[1, 0].legend()\n", "\n", "# Risk score over time\n", "risk_score = machine_data['anomaly_count_24h'].rolling(window=24).mean().tail(168)\n", "axes[1, 1].plot(risk_score.index, risk_score.values, linewidth=2, color='red')\n", "axes[1, 1].fill_between(risk_score.index, 0, risk_score.values, alpha=0.3, color='red')\n", "axes[1, 1].set_title('Risk Score Trend (7 Days)', fontsize=12, fontweight='bold')\n", "axes[1, 1].set_ylabel('Risk Score')\n", "\n", "plt.suptitle('🏭 PREDICTIVE MAINTENANCE DASHBOARD', fontsize=16, fontweight='bold')\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "# Generate maintenance recommendations\n", "current_temp = machine_data['temperature'].iloc[-1]\n", "current_anomalies = machine_data['anomaly_count_24h'].iloc[-1]\n", "avg_temp_24h = machine_data['temperature'].tail(24).mean()\n", "\n", "print(\"\\n⚙️ MAINTENANCE RECOMMENDATIONS:\")\n", "print(\"=\"*50)\n", "print(f\"Current Status:\")\n", "print(f\" • Temperature: {current_temp:.1f}°C\")\n", "print(f\" • 24h Average: {avg_temp_24h:.1f}°C\")\n", "print(f\" • Recent Anomalies: {int(current_anomalies)}\")\n", "\n", "if current_anomalies > 10 or avg_temp_24h > 80:\n", " print(\"\\n🔴 IMMEDIATE ACTION REQUIRED:\")\n", " print(\" • Schedule maintenance within 24 hours\")\n", " print(\" • Reduce load if possible\")\n", " print(\" • Prepare replacement parts\")\n", " print(f\" • Estimated downtime cost saved: ${10000 * 4:,}\")\n", "elif current_anomalies > 5 or avg_temp_24h > 75:\n", " print(\"\\n🟡 PREVENTIVE MAINTENANCE RECOMMENDED:\")\n", " print(\" • Schedule maintenance within 72 hours\")\n", " print(\" • Monitor closely\")\n", " print(\" • Order spare parts\")\n", "else:\n", " print(\"\\n🟢 NORMAL OPERATION:\")\n", " print(\" • No immediate action required\")\n", " print(\" • Next scheduled maintenance in 30 days\")" ] }, { "cell_type": "code", "execution_count": null, "id": "0a002227-46bb-47cb-942e-776a2fa3426b", "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.10.6" } }, "nbformat": 4, "nbformat_minor": 5 }