ecg-arrhythmia-detection / step4_classification.py
Ranjith445's picture
ECG Arrhythmia Detection System
1cb40e2
Raw
History Blame Contribute Delete
8.46 kB
# step4_classification.py
# Purpose: Train ML models on combined 4-record MIT-BIH dataset
# Records used: 100, 101, 105, 200 (7008 beats total)
# Models: SVM + Random Forest
# Input: D:\ecg_arr\feature_table.csv
# Output: best_model.pkl + scaler.pkl + step4_output.png
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (classification_report,
confusion_matrix,
accuracy_score)
from sklearn.utils import resample
import pickle
import warnings
warnings.filterwarnings('ignore')
# -------------------------------------------------------
# SECTION 1: Load Feature Table
# -------------------------------------------------------
csv_path = r'D:\ecg_arr\feature_table.csv'
features_df = pd.read_csv(csv_path)
print("=== Step 4: ECG Beat Classification (Multi-Record) ===\n")
print(f"Dataset loaded : {csv_path}")
print(f"Total beats : {len(features_df)}")
print(f"\nBeats per record :")
print(features_df.groupby('record')['label'].count())
print(f"\nBeat type distribution :")
print(features_df['label'].value_counts())
# -------------------------------------------------------
# SECTION 2: Define Features and Labels
# -------------------------------------------------------
feature_cols = [
'rr_interval_ms',
'heart_rate_bpm',
'qrs_duration_ms',
'st_deviation_mv',
'rr_variability_ms',
'pr_interval_ms'
]
print(f"\nFeatures used : {feature_cols}")
print(f"Input shape : {features_df[feature_cols].shape}")
# -------------------------------------------------------
# SECTION 3: Handle Class Imbalance
# -------------------------------------------------------
# N=6194, V=769, A=45 — still imbalanced
# Strategy: oversample minority, undersample majority
# Target: balance to 700 per class (enough data, fair training)
df_N = features_df[features_df['label'] == 'N']
df_V = features_df[features_df['label'] == 'V']
df_A = features_df[features_df['label'] == 'A']
print(f"\nBefore balancing :")
print(f" N (Normal) : {len(df_N)}")
print(f" V (Ventricular) : {len(df_V)}")
print(f" A (Atrial) : {len(df_A)}")
# Balance target — use min of 700 or available V beats
target_count = min(700, len(df_V))
df_N_bal = resample(df_N,
n_samples=target_count,
random_state=42,
replace=False) # Undersample N (enough data)
df_V_bal = resample(df_V,
n_samples=target_count,
random_state=42,
replace=len(df_V) < target_count) # Oversample if needed
df_A_bal = resample(df_A,
n_samples=target_count,
random_state=42,
replace=True) # Oversample A (only 45 samples)
# Combine and shuffle
df_balanced = pd.concat([df_N_bal, df_V_bal, df_A_bal])
df_balanced = df_balanced.sample(frac=1, random_state=42)
X_bal = df_balanced[feature_cols].values
y_bal = df_balanced['label'].values
print(f"\nAfter balancing :")
print(f" N (Normal) : {len(df_N_bal)}")
print(f" V (Ventricular) : {len(df_V_bal)}")
print(f" A (Atrial) : {len(df_A_bal)}")
print(f" Total balanced : {len(df_balanced)}")
# -------------------------------------------------------
# SECTION 4: Train / Test Split
# -------------------------------------------------------
# 80% training, 20% testing
# stratify ensures equal class representation in both sets
X_train, X_test, y_train, y_test = train_test_split(
X_bal, y_bal,
test_size=0.2,
random_state=42,
stratify=y_bal
)
print(f"\nTrain set size : {len(X_train)} beats")
print(f"Test set size : {len(X_test)} beats")
# -------------------------------------------------------
# SECTION 5: Feature Scaling
# -------------------------------------------------------
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# -------------------------------------------------------
# SECTION 6: Train Model 1 — SVM
# -------------------------------------------------------
print("\n--- Training Model 1: SVM ---")
svm_model = SVC(
kernel='rbf',
C=10,
gamma='scale',
class_weight='balanced',
random_state=42,
probability=True # Needed for confidence scores
)
svm_model.fit(X_train, y_train)
svm_predictions = svm_model.predict(X_test)
svm_accuracy = accuracy_score(y_test, svm_predictions)
print(f"SVM Accuracy : {svm_accuracy * 100:.2f}%")
print(f"\nSVM Classification Report:")
print(classification_report(y_test, svm_predictions,
target_names=['A', 'N', 'V']))
# -------------------------------------------------------
# SECTION 7: Train Model 2 — Random Forest
# -------------------------------------------------------
print("--- Training Model 2: Random Forest ---")
rf_model = RandomForestClassifier(
n_estimators=200, # More trees = more robust
max_depth=15, # Deeper trees for complex patterns
min_samples_split=5, # Prevents overfitting
min_samples_leaf=2, # Prevents overfitting
class_weight='balanced',
random_state=42,
n_jobs=-1 # Use all CPU cores
)
rf_model.fit(X_train, y_train)
rf_predictions = rf_model.predict(X_test)
rf_accuracy = accuracy_score(y_test, rf_predictions)
print(f"Random Forest Accuracy : {rf_accuracy * 100:.2f}%")
print(f"\nRandom Forest Classification Report:")
print(classification_report(y_test, rf_predictions,
target_names=['A', 'N', 'V']))
# -------------------------------------------------------
# SECTION 8: Cross Validation
# -------------------------------------------------------
print("--- Cross Validation (5-fold) ---")
svm_cv = cross_val_score(svm_model, X_bal,
y_bal, cv=5,
scoring='accuracy',
n_jobs=-1)
rf_cv = cross_val_score(rf_model, X_bal,
y_bal, cv=5,
scoring='accuracy',
n_jobs=-1)
print(f"SVM CV Accuracy : {svm_cv.mean()*100:.2f}% "
f"(+/- {svm_cv.std()*100:.2f}%)")
print(f"RF CV Accuracy : {rf_cv.mean()*100:.2f}% "
f"(+/- {rf_cv.std()*100:.2f}%)")
# -------------------------------------------------------
# SECTION 9: Pick Best Model and Save
# -------------------------------------------------------
if rf_accuracy >= svm_accuracy:
best_model = rf_model
best_model_name = 'Random Forest'
best_accuracy = rf_accuracy
best_predictions = rf_predictions
else:
best_model = svm_model
best_model_name = 'SVM'
best_accuracy = svm_accuracy
best_predictions = svm_predictions
print(f"\nBest Model : {best_model_name}")
print(f"Best Accuracy : {best_accuracy * 100:.2f}%")
model_path = r'D:\ecg_arr\best_model.pkl'
scaler_path = r'D:\ecg_arr\scaler.pkl'
with open(model_path, 'wb') as f:
pickle.dump(best_model, f)
with open(scaler_path, 'wb') as f:
pickle.dump(scaler, f)
print(f"Model saved to : {model_path}")
print(f"Scaler saved to : {scaler_path}")
# -------------------------------------------------------
# SECTION 10: Plot Confusion Matrices + Feature Importance
# -------------------------------------------------------
fig = plt.figure(figsize=(18, 10))
fig.suptitle('ECG Classification Results — Multi-Record Dataset (4 Records)',
fontsize=14, fontweight='bold')
class_names = sorted(set(y_bal))
# --- Confusion Matrix: SVM ---
ax1 = fig.add_subplot(2, 3, 1)
cm_svm = confusion_matrix(y_test, svm_predictions, labels=class_names)
im1 = ax1.imshow(cm_svm, interpolation='nearest', cmap='Blues')
ax1.set_title(f'SVM\nAccuracy: {svm_accuracy*100:.2f}%', fontsize=11)
ax1.set_xlabel('Predicted')
ax1.set_ylabel('True')
ax1.set_xticks(range(len(class_names)))
ax1.set_yticks(range(len(class_names)))
ax1.set_xticklabels(class_names)
ax1.set_yticklabels(class_names)
plt.colorbar(im1, ax=ax1)
for i in range(len(class_names)):
for j in range(len(class_names)):
ax1.text