{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":59093,"databundleVersionId":7469972,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install torch-geometric\n!pip install numpy","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-19T13:26:52.867174Z","iopub.execute_input":"2025-12-19T13:26:52.867589Z","iopub.status.idle":"2025-12-19T13:27:02.351148Z","shell.execute_reply.started":"2025-12-19T13:26:52.867546Z","shell.execute_reply":"2025-12-19T13:27:02.349605Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==================== GCN ON 50K SAMPLES - FIXED VERSION ====================\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch_geometric.nn import GCNConv, global_mean_pool\nfrom torch_geometric.data import Data, DataLoader\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, classification_report, confusion_matrix\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport time\nimport os\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom tqdm import tqdm\n\nprint(\"=\"*80)\nprint(\"🧠 GCN TRAINING ON 50,000 SAMPLES - FIXED VERSION\")\nprint(\"=\"*80)\n\n# 1. DATA LOADING\nprint(\"\\n📂 LOADING DATA...\")\ncsv_path = '/kaggle/input/hms-harmful-brain-activity-classification/train.csv'\ntrain_csv = pd.read_csv(csv_path)\nprint(f\"✅ Loaded {len(train_csv):,} samples\")\n\n# Process labels\nlabel_cols = ['seizure_vote', 'lpd_vote', 'gpd_vote', 'lrda_vote', 'grda_vote', 'other_vote']\nvotes = train_csv[label_cols].values.astype(float)\nrow_sums = votes.sum(axis=1, keepdims=True)\nrow_sums[row_sums == 0] = 1\nprobabilities = votes / row_sums\ntrain_csv['dominant_class'] = np.argmax(probabilities, axis=1)\n\n# Select 50K samples\nn_samples = 50000\nif len(train_csv) > n_samples:\n    _, sample_indices = train_test_split(\n        range(len(train_csv)),\n        test_size=n_samples,\n        random_state=42,\n        stratify=train_csv['dominant_class']\n    )\n    selected_df = train_csv.iloc[sample_indices]\nelse:\n    selected_df = train_csv\n    n_samples = len(selected_df)\n\nprint(f\"Selected {n_samples:,} samples\")\n\n# 2. IMPROVED GRAPH DATA CREATION\nprint(\"\\n🔧 CREATING GRAPH DATA...\")\n\nDATA_DIR = '/kaggle/input/hms-harmful-brain-activity-classification/train_eegs'\n\ndef robust_normalize(data):\n    \"\"\"Robust normalization that handles outliers\"\"\"\n    # Clip extreme values first\n    data = np.clip(data, np.percentile(data, 1), np.percentile(data, 99))\n    \n    # Z-score normalization with epsilon\n    mean = np.mean(data)\n    std = np.std(data)\n    if std < 1e-8:\n        std = 1.0\n    \n    normalized = (data - mean) / std\n    \n    # Ensure no extreme values remain\n    normalized = np.clip(normalized, -5, 5)\n    return normalized\n\ndef create_graph_data(eeg_data, label):\n    \"\"\"Create graph data from EEG with robust preprocessing\"\"\"\n    if eeg_data.size == 0 or eeg_data.shape[0] < 10:\n        return None\n    \n    try:\n        # Downsample to 100 timesteps (slightly more for better features)\n        if eeg_data.shape[0] > 100:\n            indices = np.linspace(0, eeg_data.shape[0]-1, 100).astype(int)\n            eeg_data = eeg_data[indices, :]\n        \n        # Use first 8 channels (better representation)\n        num_channels = min(eeg_data.shape[1], 8)\n        if eeg_data.shape[1] > num_channels:\n            eeg_data = eeg_data[:, :num_channels]\n        elif eeg_data.shape[1] < num_channels:\n            # Pad with zeros if needed\n            padding = num_channels - eeg_data.shape[1]\n            eeg_data = np.pad(eeg_data, ((0, 0), (0, padding)), mode='constant')\n        \n        # Robust normalization per channel\n        for ch in range(num_channels):\n            eeg_data[:, ch] = robust_normalize(eeg_data[:, ch])\n        \n        # Extract richer node features (10 features per channel)\n        node_features = []\n        for ch in range(num_channels):\n            channel_data = eeg_data[:, ch]\n            \n            # Basic statistical features\n            mean_val = float(np.mean(channel_data))\n            std_val = float(np.std(channel_data))\n            min_val = float(np.min(channel_data))\n            max_val = float(np.max(channel_data))\n            median_val = float(np.median(channel_data))\n            \n            # Additional features\n            mad = float(np.mean(np.abs(channel_data - mean_val)))  # Mean absolute deviation\n            energy = float(np.sum(channel_data ** 2))  # Signal energy\n            skewness = float(pd.Series(channel_data).skew())  # Skewness\n            kurt = float(pd.Series(channel_data).kurt())  # Kurtosis\n            pos_ratio = float(np.sum(channel_data > 0) / len(channel_data))  # Positive ratio\n            \n            node_features.append([mean_val, std_val, min_val, max_val, median_val,\n                                 mad, energy, skewness, kurt, pos_ratio])\n        \n        # Create edges (fully connected for simplicity)\n        num_nodes = len(node_features)\n        edges = []\n        for i in range(num_nodes):\n            for j in range(num_nodes):\n                if i != j:\n                    edges.append([i, j])\n        \n        if edges:\n            edges = torch.tensor(edges, dtype=torch.long).t().contiguous()\n            \n            # Add self-loops\n            self_loops = torch.tensor([[i, i] for i in range(num_nodes)], dtype=torch.long).t().contiguous()\n            edge_index = torch.cat([edges, self_loops], dim=1)\n            \n            graph_data = Data(\n                x=torch.tensor(node_features, dtype=torch.float32),\n                edge_index=edge_index,\n                y=torch.tensor([label], dtype=torch.long)\n            )\n            return graph_data\n        \n        return None\n    except Exception as e:\n        #print(f\"Error creating graph: {e}\")\n        return None\n\n# Process samples - increased to 50K\nprint(f\"Processing {n_samples:,} samples...\")\ngraph_data_list = []\nlabels_list = []\nfailed = 0\n\nprocessing_start = time.time()\n\n# Process in chunks to manage memory\nchunk_size = 5000\nnum_chunks = (n_samples + chunk_size - 1) // chunk_size\n\nfor chunk_idx in range(num_chunks):\n    start_idx = chunk_idx * chunk_size\n    end_idx = min((chunk_idx + 1) * chunk_size, n_samples)\n    \n    print(f\"\\nProcessing chunk {chunk_idx + 1}/{num_chunks} (samples {start_idx}-{end_idx})...\")\n    \n    for idx in tqdm(range(start_idx, end_idx), desc=f\"Chunk {chunk_idx + 1}\"):\n        try:\n            sample_id = selected_df.iloc[idx]['eeg_id']\n            eeg_path = f\"{DATA_DIR}/{sample_id}.parquet\"\n            \n            if os.path.exists(eeg_path):\n                eeg_df = pd.read_parquet(eeg_path)\n                if not eeg_df.empty and eeg_df.shape[0] > 10:\n                    # Select only numeric columns and handle NaNs\n                    eeg_df = eeg_df.select_dtypes(include=[np.number])\n                    eeg_df = eeg_df.fillna(method='ffill').fillna(0)\n                    \n                    eeg_data = eeg_df.values.astype(np.float32)\n                    label = selected_df.iloc[idx]['dominant_class']\n                    graph_data = create_graph_data(eeg_data, label)\n                    \n                    if graph_data is not None:\n                        graph_data_list.append(graph_data)\n                        labels_list.append(label)\n                    else:\n                        failed += 1\n                else:\n                    failed += 1\n            else:\n                failed += 1\n        except Exception as e:\n            #print(f\"Error processing sample {idx}: {e}\")\n            failed += 1\n    \n    print(f\"  Chunk completed: {len(graph_data_list):,} graphs so far, {failed:,} failed\")\n\nprocessing_time = time.time() - processing_start\nprint(f\"\\n✅ Created {len(graph_data_list):,} graphs from {n_samples:,} samples\")\nprint(f\"   Success rate: {len(graph_data_list)/n_samples*100:.1f}%\")\nprint(f\"   Failed: {failed:,}\")\nprint(f\"   Time: {processing_time:.1f}s\")\n\n# Check if we have enough data\nif len(graph_data_list) < 1000:\n    print(\"❌ Not enough valid graphs created. Exiting...\")\n    exit()\n\n# 3. DATA PREPARATION\n# Split data\ntrain_idx, val_idx = train_test_split(\n    range(len(graph_data_list)),\n    test_size=0.15,\n    random_state=42,\n    stratify=labels_list\n)\n\ntrain_dataset = [graph_data_list[i] for i in train_idx]\nval_dataset = [graph_data_list[i] for i in val_idx]\n\nprint(f\"\\n📊 DATASET SIZES:\")\nprint(f\"   Training graphs: {len(train_dataset):,}\")\nprint(f\"   Validation graphs: {len(val_dataset):,}\")\nprint(f\"   Node features: {graph_data_list[0].x.shape[1]}\")\nprint(f\"   Nodes per graph: {graph_data_list[0].x.shape[0]}\")\n\n# 4. IMPROVED GCN MODEL\nclass EEG_GCN(nn.Module):\n    def __init__(self, num_node_features=10, hidden_dim=128, num_classes=6):\n        super().__init__()\n        \n        # GCN layers with residual connections\n        self.conv1 = GCNConv(num_node_features, hidden_dim)\n        self.conv2 = GCNConv(hidden_dim, hidden_dim)\n        self.conv3 = GCNConv(hidden_dim, hidden_dim)\n        \n        # Batch normalization\n        self.bn1 = nn.BatchNorm1d(hidden_dim)\n        self.bn2 = nn.BatchNorm1d(hidden_dim)\n        self.bn3 = nn.BatchNorm1d(hidden_dim)\n        \n        # Attention pooling\n        self.attention = nn.Sequential(\n            nn.Linear(hidden_dim, hidden_dim),\n            nn.Tanh(),\n            nn.Linear(hidden_dim, 1)\n        )\n        \n        # Classifier with gradient clipping friendly layers\n        self.classifier = nn.Sequential(\n            nn.Linear(hidden_dim, 64),\n            nn.BatchNorm1d(64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, 32),\n            nn.BatchNorm1d(32),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(32, num_classes)\n        )\n        \n        # Initialize weights properly\n        self._init_weights()\n    \n    def _init_weights(self):\n        for m in self.modules():\n            if isinstance(m, nn.Linear):\n                nn.init.xavier_uniform_(m.weight)\n                nn.init.zeros_(m.bias)\n            elif isinstance(m, GCNConv):\n                nn.init.xavier_uniform_(m.lin.weight)\n                nn.init.zeros_(m.bias)\n    \n    def forward(self, data):\n        x, edge_index, batch = data.x, data.edge_index, data.batch\n        \n        # Layer 1 with residual\n        x1 = F.relu(self.bn1(self.conv1(x, edge_index)))\n        x1 = F.dropout(x1, p=0.3, training=self.training)\n        \n        # Layer 2 with residual\n        x2 = F.relu(self.bn2(self.conv2(x1, edge_index)))\n        x2 = F.dropout(x2, p=0.3, training=self.training)\n        \n        # Layer 3\n        x3 = F.relu(self.bn3(self.conv3(x2, edge_index)))\n        \n        # Skip connection\n        x = x1 + x2 + x3\n        \n        # Graph-level pooling\n        graph_embedding = global_mean_pool(x, batch)\n        \n        # Attention weighting\n        attention_weights = torch.softmax(self.attention(graph_embedding), dim=0)\n        attended = graph_embedding * attention_weights\n        \n        # Classification - use logits instead of log_softmax\n        logits = self.classifier(attended)\n        \n        return logits  # Return raw logits\n\n# 5. TRAINING WITH IMPROVED SETTINGS\nprint(f\"\\n{'='*60}\")\nprint(\"🧠 TRAINING GCN WITH IMPROVED SETTINGS\")\nprint(f\"{'='*60}\")\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n\n# Create data loaders with appropriate batch size\nbatch_size = 32 if len(train_dataset) > 10000 else 64\ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=2, pin_memory=True)\n\n# Initialize model\nmodel = EEG_GCN(num_node_features=10, hidden_dim=128, num_classes=6)\nmodel = model.to(device)\n\n# Calculate class weights for balanced loss\nclass_counts = torch.bincount(torch.tensor(labels_list))\nclass_weights = (1.0 / class_counts.float()) * len(class_counts) / 2.0\nclass_weights = class_weights / class_weights.sum()\nprint(f\"Class weights: {class_weights.tolist()}\")\n\n# Use CrossEntropyLoss instead of NLLLoss (combines log_softmax + NLL)\ncriterion = nn.CrossEntropyLoss(weight=class_weights.to(device))\n\n# Gradient clipping and weight decay\noptimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-3)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20, eta_min=1e-5)\n\n# Track metrics\ntrain_losses = []\nval_losses = []\ntrain_accuracies = []\nval_accuracies = []\nlearning_rates = []\n\nnum_epochs = 30\nbest_val_acc = 0\npatience_counter = 0\nmax_patience = 7\n\nprint(f\"\\nTraining for {num_epochs} epochs...\")\nprint(f\"Training set: {len(train_dataset):,} graphs\")\nprint(f\"Validation set: {len(val_dataset):,} graphs\")\nprint(f\"Batch size: {batch_size}\")\nprint(f\"Learning rate: {optimizer.param_groups[0]['lr']}\")\n\nstart_time = time.time()\n\nfor epoch in range(num_epochs):\n    # Training phase\n    model.train()\n    train_loss = 0\n    train_correct = 0\n    train_total = 0\n    \n    train_loader_tqdm = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs} [Train]\", leave=False)\n    \n    for batch in train_loader_tqdm:\n        batch = batch.to(device)\n        optimizer.zero_grad()\n        \n        outputs = model(batch)\n        loss = criterion(outputs, batch.y)\n        \n        # Gradient clipping to prevent NaN\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        \n        loss.backward()\n        optimizer.step()\n        \n        train_loss += loss.item()\n        _, predicted = torch.max(outputs.data, 1)\n        train_total += batch.y.size(0)\n        train_correct += (predicted == batch.y).sum().item()\n        \n        # Update progress bar\n        train_loader_tqdm.set_postfix({\n            'loss': loss.item(),\n            'acc': train_correct / train_total\n        })\n    \n    avg_train_loss = train_loss / len(train_loader)\n    train_acc = train_correct / train_total\n    train_losses.append(avg_train_loss)\n    train_accuracies.append(train_acc)\n    \n    # Validation phase\n    model.eval()\n    val_loss = 0\n    val_correct = 0\n    val_total = 0\n    all_preds = []\n    all_labels = []\n    \n    with torch.no_grad():\n        val_loader_tqdm = tqdm(val_loader, desc=f\"Epoch {epoch+1}/{num_epochs} [Val]\", leave=False)\n        \n        for batch in val_loader_tqdm:\n            batch = batch.to(device)\n            outputs = model(batch)\n            loss = criterion(outputs, batch.y)\n            \n            val_loss += loss.item()\n            _, predicted = torch.max(outputs.data, 1)\n            val_total += batch.y.size(0)\n            val_correct += (predicted == batch.y).sum().item()\n            \n            all_preds.extend(predicted.cpu().numpy())\n            all_labels.extend(batch.y.cpu().numpy())\n            \n            val_loader_tqdm.set_postfix({\n                'loss': loss.item(),\n                'acc': val_correct / val_total\n            })\n    \n    avg_val_loss = val_loss / len(val_loader)\n    val_acc = val_correct / val_total\n    val_losses.append(avg_val_loss)\n    val_accuracies.append(val_acc)\n    learning_rates.append(optimizer.param_groups[0]['lr'])\n    \n    # Update scheduler\n    scheduler.step()\n    \n    print(f\"\\n  Epoch {epoch+1}:\")\n    print(f\"    Train Loss: {avg_train_loss:.4f}, Train Acc: {train_acc:.4f}\")\n    print(f\"    Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.4f}\")\n    print(f\"    LR: {optimizer.param_groups[0]['lr']:.6f}\")\n    \n    # Check for NaN\n    if np.isnan(avg_train_loss) or np.isnan(avg_val_loss):\n        print(f\"  ⚠️  NaN detected in loss! Adjusting learning rate...\")\n        for param_group in optimizer.param_groups:\n            param_group['lr'] *= 0.1\n        continue\n    \n    # Early stopping\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        patience_counter = 0\n        # Save best model\n        torch.save({\n            'epoch': epoch,\n            'model_state_dict': model.state_dict(),\n            'optimizer_state_dict': optimizer.state_dict(),\n            'val_acc': val_acc,\n        }, 'best_gcn_model.pth')\n        print(f\"    💾 Saved best model with val_acc: {val_acc:.4f}\")\n    else:\n        patience_counter += 1\n        if patience_counter >= max_patience:\n            print(f\"  ⏹️  Early stopping at epoch {epoch+1}\")\n            break\n    \n    print(f\"    Best val_acc so far: {best_val_acc:.4f}\")\n    print(f\"    Patience counter: {patience_counter}/{max_patience}\")\n\ntotal_time = time.time() - start_time\n\n# Load best model\ncheckpoint = torch.load('best_gcn_model.pth')\nmodel.load_state_dict(checkpoint['model_state_dict'])\nbest_epoch = checkpoint['epoch']\n\n# Final evaluation\nmodel.eval()\nall_preds = []\nall_labels = []\nval_probs = []\n\nwith torch.no_grad():\n    for batch in val_loader:\n        batch = batch.to(device)\n        outputs = model(batch)\n        probs = F.softmax(outputs, dim=1)\n        _, predicted = torch.max(outputs.data, 1)\n        all_preds.extend(predicted.cpu().numpy())\n        all_labels.extend(batch.y.cpu().numpy())\n        val_probs.extend(probs.cpu().numpy())\n\nfinal_val_acc = accuracy_score(all_labels, all_preds)\n\nprint(f\"\\n✅ GCN Training Complete!\")\nprint(f\"   Best Validation Accuracy: {best_val_acc:.4f} (epoch {best_epoch + 1})\")\nprint(f\"   Final Validation Accuracy: {final_val_acc:.4f}\")\nprint(f\"   Total Training Time: {total_time:.1f}s ({total_time/60:.1f} minutes)\")\n\n# Save final model\ntorch.save({\n    'model_state_dict': model.state_dict(),\n    'node_features': graph_data_list[0].x.shape[1],\n    'hidden_dim': 128,\n    'num_classes': 6,\n    'val_acc': final_val_acc\n}, 'gcn_50k_model_fixed.pth')\nprint(\"   Model saved as 'gcn_50k_model_fixed.pth'\")\n\n# 6. CLASSIFICATION REPORT\nprint(f\"\\n{'='*60}\")\nprint(\"📊 CLASSIFICATION REPORT\")\nprint(f\"{'='*60}\")\n\nclass_names = ['Seizure', 'LPD', 'GPD', 'LRDA', 'GRDA', 'Other']\n\nprint(\"\\n📋 Detailed Classification Report:\")\nreport = classification_report(all_labels, all_preds, target_names=class_names, digits=4)\nprint(report)\n\n# Confusion Matrix\ncm = confusion_matrix(all_labels, all_preds)\n\nplt.figure(figsize=(12, 10))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n            xticklabels=class_names, yticklabels=class_names)\nplt.title(f'GCN - Confusion Matrix ({len(graph_data_list):,} Samples)', fontsize=14, fontweight='bold')\nplt.xlabel('Predicted Label', fontsize=12)\nplt.ylabel('True Label', fontsize=12)\nplt.tight_layout()\nplt.savefig('gcn_confusion_matrix_fixed.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# 7. LOSS CURVES VISUALIZATION\nprint(f\"\\n{'='*60}\")\nprint(\"📈 LOSS AND ACCURACY CURVES\")\nprint(f\"{'='*60}\")\n\nfig, axes = plt.subplots(2, 2, figsize=(15, 12))\n\n# Loss Curves\nepochs_range = range(1, len(train_losses) + 1)\n\nax1 = axes[0, 0]\nax1.plot(epochs_range, train_losses, 'o-', linewidth=2, markersize=6, label='Training Loss', color='blue')\nax1.plot(epochs_range, val_losses, 's-', linewidth=2, markersize=6, label='Validation Loss', color='red')\nax1.set_xlabel('Epoch', fontsize=12)\nax1.set_ylabel('Loss', fontsize=12)\nax1.set_title('Loss Curves', fontsize=14, fontweight='bold')\nax1.grid(True, alpha=0.3)\nax1.legend()\n\n# Accuracy Curves\nax2 = axes[0, 1]\nax2.plot(epochs_range, train_accuracies, 'o-', linewidth=2, markersize=6, label='Training Accuracy', color='blue')\nax2.plot(epochs_range, val_accuracies, 's-', linewidth=2, markersize=6, label='Validation Accuracy', color='red')\nax2.axhline(y=1/6, color='gray', linestyle='--', alpha=0.5, label='Random Chance')\nax2.set_xlabel('Epoch', fontsize=12)\nax2.set_ylabel('Accuracy', fontsize=12)\nax2.set_title('Accuracy Curves', fontsize=14, fontweight='bold')\nax2.grid(True, alpha=0.3)\nax2.legend()\n\n# Learning Rate Schedule\nax3 = axes[1, 0]\nax3.plot(epochs_range, learning_rates, 'o-', linewidth=2, markersize=6, color='green')\nax3.set_xlabel('Epoch', fontsize=12)\nax3.set_ylabel('Learning Rate', fontsize=12)\nax3.set_title('Learning Rate Schedule', fontsize=14, fontweight='bold')\nax3.grid(True, alpha=0.3)\nax3.set_yscale('log')\n\n# Convergence Analysis\nax4 = axes[1, 1]\n# Calculate convergence metric (train_acc - val_acc gap)\nconvergence_gap = [t - v for t, v in zip(train_accuracies, val_accuracies)]\nax4.plot(epochs_range, convergence_gap, 'o-', linewidth=2, markersize=6, color='purple')\nax4.axhline(y=0, color='gray', linestyle='--', alpha=0.5)\nax4.set_xlabel('Epoch', fontsize=12)\nax4.set_ylabel('Train-Val Accuracy Gap', fontsize=12)\nax4.set_title('Overfitting Analysis', fontsize=14, fontweight='bold')\nax4.grid(True, alpha=0.3)\n\n# Add optimal point marker\nif convergence_gap:\n    best_epoch_gap = np.argmin(np.abs(convergence_gap))\n    ax4.scatter(best_epoch_gap + 1, convergence_gap[best_epoch_gap], color='red', s=200, zorder=5)\n    ax4.annotate(f'Best Balance\\nGap: {convergence_gap[best_epoch_gap]:.4f}', \n                 (best_epoch_gap + 1, convergence_gap[best_epoch_gap]),\n                 xytext=(10, 10), textcoords='offset points',\n                 bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5))\n\nplt.suptitle(f'GCN Training Analysis - {len(graph_data_list):,} Valid Samples', fontsize=16, fontweight='bold')\nplt.tight_layout()\nplt.savefig('gcn_loss_curves_fixed.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# 8. PERFORMANCE SUMMARY\nprint(f\"\\n{'='*60}\")\nprint(\"📊 PERFORMANCE SUMMARY\")\nprint(f\"{'='*60}\")\n\nprint(f\"\\n🎯 Training Statistics:\")\nprint(f\"   Total Samples Processed: {n_samples:,}\")\nprint(f\"   Valid Graphs Created: {len(graph_data_list):,}\")\nprint(f\"   Success Rate: {len(graph_data_list)/n_samples*100:.1f}%\")\nprint(f\"   Best Epoch: {best_epoch + 1}\")\nprint(f\"   Best Validation Accuracy: {best_val_acc:.4f}\")\nprint(f\"   Final Validation Accuracy: {final_val_acc:.4f}\")\nprint(f\"   Random Chance Baseline: {1/6:.4f}\")\nprint(f\"   Improvement over Random: {final_val_acc - 1/6:.4f}\")\n\nif convergence_gap:\n    print(f\"   Final Overfitting Gap: {convergence_gap[-1]:.4f}\")\n    print(f\"   Minimum Overfitting Gap: {min(convergence_gap):.4f}\")\n\nprint(f\"\\n⏱️  Timing Statistics:\")\nprint(f\"   Graph Creation: {processing_time:.1f}s ({processing_time/60:.1f} minutes)\")\nprint(f\"   Model Training: {total_time:.1f}s ({total_time/60:.1f} minutes)\")\nprint(f\"   Total Time: {processing_time + total_time:.1f}s ({ (processing_time + total_time)/60:.1f} minutes)\")\nprint(f\"   Epochs Completed: {len(train_losses)}\")\nprint(f\"   Time per Epoch: {total_time/len(train_losses):.1f}s\")\nprint(f\"   Graphs per Second: {len(graph_data_list)/processing_time:.1f}\")\n\nprint(f\"\\n📈 Learning Insights:\")\nprint(f\"   1. Model converged at epoch {best_epoch + 1}\")\nprint(f\"   2. Best validation accuracy: {best_val_acc:.4f}\")\nprint(f\"   3. Improvement over random chance: {(final_val_acc - 1/6)*100:.1f}%\")\nprint(f\"   4. Final learning rate: {learning_rates[-1]:.6f}\")\n\nprint(f\"\\n🔧 Model Architecture:\")\nprint(f\"   Node features: {graph_data_list[0].x.shape[1]}\")\nprint(f\"   Hidden dimension: 128\")\nprint(f\"   Number of GCN layers: 3\")\nprint(f\"   Classifier layers: 3\")\nprint(f\"   Total parameters: {sum(p.numel() for p in model.parameters()):,}\")\n\nprint(f\"\\n✅ GCN training on {len(graph_data_list):,} valid samples completed successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-19T13:27:02.353254Z","iopub.execute_input":"2025-12-19T13:27:02.353695Z","iopub.status.idle":"2025-12-19T14:09:53.856880Z","shell.execute_reply.started":"2025-12-19T13:27:02.353647Z","shell.execute_reply":"2025-12-19T14:09:53.855473Z"}},"outputs":[],"execution_count":null}]}